{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":6799,"databundleVersionId":4225553,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Experience 0","metadata":{"_uuid":"595a652b-36fd-40ab-a7b0-697bb2f9fb4f","_cell_guid":"cf37c381-899c-4373-b0d0-da182e122409","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"# Installation de la librairie\n\nimport torch\nfrom torchvision import datasets, transforms\nimport numpy as np\nimport umap\nimport matplotlib.pyplot as plt\n\n#Loading Data\nprint(\"Chargement de MNIST...\")\n# On télécharge les données d'entraînement\nmnist_data = datasets.MNIST(root='./data', train=True, download=True, transform=transforms.ToTensor())\n\n# --- 2. PRÉPARATION (DOMAINE DES PIXELS) ---\n# UMAP peut être lent sur 60 000 images. On prend un sous-échantillon de 5000 images.\n# C'est suffisant pour voir la structure.\nN_SAMPLES = 5000\n\n# On récupère les données brutes (valeurs de 0 à 255)\n# dataset.data contient les images sous forme de matrices (N, 28, 28)\ndata_full = mnist_data.data.float().numpy()\ntargets_full = mnist_data.targets.numpy()\n\n# Sélection aléatoire de 5000 indices\nindices = np.random.choice(len(data_full), N_SAMPLES, replace=False)\nX_subset = data_full[indices]\ny_subset = targets_full[indices]\n\n# ÉTAPE CLÉ : Aplatissement (Flattening)\n# On passe de (5000, 28, 28) à (5000, 784)\n# Chaque image devient un vecteur de 784 pixels. C'est ça \"le domaine des pixels\".\nX_flat = X_subset.reshape(N_SAMPLES, -1)\n\n# Normalisation entre 0 et 1 (aide souvent UMAP à converger mieux)\nX_flat = X_flat / 255.0\n\nprint(f\"Données prêtes : {X_flat.shape} (5000 images de 784 pixels)\")\n\n# --- 3. PROJECTION UMAP ---\nprint(\"Calcul de la projection UMAP en cours... (Patience ~10-30 sec)\")\n\n# Initialisation et fit\numap_model = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42)\nembedding = umap_model.fit_transform(X_flat)\n\n# --- 4. VISUALISATION ---\nprint(\"Affichage...\")\n\nplt.figure(figsize=(12, 10))\nscatter = plt.scatter(embedding[:, 0], embedding[:, 1], c=y_subset, cmap='Spectral', s=5)\nplt.colorbar(scatter, label='Chiffre (Label)')\nplt.title('Projection UMAP de MNIST (Basée sur les pixels bruts)')\nplt.xlabel('UMAP Dimension 1')\nplt.ylabel('UMAP Dimension 2')\nplt.show()","metadata":{"_uuid":"4037d786-0c61-4bb0-a52f-048a1100456c","_cell_guid":"03591d4e-7f21-4f2d-aa2d-ad787ec58804","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2026-01-21T21:36:17.701638Z","iopub.execute_input":"2026-01-21T21:36:17.702007Z","iopub.status.idle":"2026-01-21T21:36:27.305628Z","shell.execute_reply.started":"2026-01-21T21:36:17.701977Z","shell.execute_reply":"2026-01-21T21:36:27.304716Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Installation de UMAP si nécessaire (décommente la ligne si tu es sur Kaggle)\n# !pip install umap-learn\n\nimport os\nimport glob\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nimport umap\nfrom tqdm import tqdm # Pour la barre de progression\n\n# --- 1. CONFIGURATION DES CHEMINS ---\n# Chemin standard sur Kaggle pour ce dataset\nROOT_DIR = '/kaggle/input/imagenet-object-localization-challenge/ILSVRC/Data/CLS-LOC/train'\n\n# --- 2. FONCTION DE CHARGEMENT INTELLIGENTE ---\ndef load_imagenet_top10(root_dir, img_size=(64, 64), max_imgs_per_class=500):\n    \"\"\"\n    Charge uniquement les 10 premières classes pour éviter de saturer la RAM.\n    On redimensionne direct en 64x64 pour que UMAP puisse gérer les pixels.\n    \"\"\"\n    print(f\"Recherche des classes dans : {root_dir}\")\n    \n    # Récupérer tous les noms de dossiers\n    all_classes = sorted(os.listdir(root_dir))\n    \n    # On garde juste les 10 premiers\n    top10_classes = all_classes[:10]\n    print(f\"Classes sélectionnées : {top10_classes}\")\n    \n    data = []\n    labels = []\n    class_names = []\n    \n    # Boucle sur chaque classe\n    for idx, class_folder in enumerate(top10_classes):\n        class_path = os.path.join(root_dir, class_folder)\n        class_names.append(class_folder)\n        \n        # Récupérer les images .JPEG dans ce dossier\n        image_files = glob.glob(os.path.join(class_path, \"*.JPEG\"))\n        \n        # On en prend un nombre limité pour que ça aille vite (ex: 200 par classe)\n        image_files = image_files[:max_imgs_per_class]\n        \n        for img_file in image_files:\n            try:\n                with Image.open(img_file) as img:\n                    img = img.convert('RGB') # Forcer en couleur\n                    img = img.resize(img_size) # Redimensionner (très important !)\n                    img_array = np.array(img)\n                    \n                    # Aplatir : (64, 64, 3) -> Vecteur de 12288\n                    img_flat = img_array.flatten()\n                    \n                    data.append(img_flat)\n                    labels.append(idx)\n            except:\n                pass # Si une image est corrompue, on l'ignore\n\n    return np.array(data), np.array(labels), class_names\n\n# --- 3. EXÉCUTION ---\n\n# On charge les données\nX_flat, y, class_names = load_imagenet_top10(ROOT_DIR, img_size=(64, 64))\n\n# Normalisation (0-255 -> 0-1)\nX_flat = X_flat / 255.0\n\nprint(f\"Données finales : {X_flat.shape}\")\nprint(\"  -> (Nombre d'images, Nombre de pixels applatis)\")\n\n# --- 4. PROJECTION UMAP ---\nprint(\"Lancement de UMAP (cela peut prendre 1 à 2 minutes)...\")\numap_model = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42)\nembedding = umap_model.fit_transform(X_flat)\n\n# --- 5. VISUALISATION ---\nplt.figure(figsize=(12, 10))\n\n# Scatter plot avec une couleur par classe\nscatter = plt.scatter(embedding[:, 0], embedding[:, 1], c=y, cmap='tab10', s=10, alpha=0.7)\n\n# Légende intelligente\n# On crée une légende manuelle pour afficher les codes des classes (n0144...)\nhandles, _ = scatter.legend_elements(prop=\"colors\")\nplt.legend(handles, class_names, title=\"Classes ImageNet\", loc=\"best\", bbox_to_anchor=(1, 1))\n\nplt.title('Projection UMAP - ImageNet (10 premières classes)\\nBasé sur les Pixels Bruts (64x64)')\nplt.xlabel('UMAP 1')\nplt.ylabel('UMAP 2')\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T21:36:27.307335Z","iopub.execute_input":"2026-01-21T21:36:27.307623Z","iopub.status.idle":"2026-01-21T21:37:23.083805Z","shell.execute_reply.started":"2026-01-21T21:36:27.307596Z","shell.execute_reply":"2026-01-21T21:37:23.082811Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Experience 1**","metadata":{"_uuid":"e5513728-b864-4e98-a19b-82a06293b5c8","_cell_guid":"37b22add-2ff9-4fb3-b1a1-076f541c9214","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\n\ndef generate_spiral_data(N=100, K=3):\n    D = 2 # dimensionalité\n    X = np.zeros((N*K,D)) # matrice de données (chaque ligne = un exemple)\n    y = np.zeros(N*K, dtype='uint8') # étiquettes de classe\n    for j in range(K):\n        ix = range(N*j,N*(j+1))\n        r = np.linspace(0.0,1,N) # rayon\n        t = np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.2 # theta + bruit\n        X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]\n        y[ix] = j\n    return X, y\n\n# Visualisation des données brutes\nX, y = generate_spiral_data(N=200, K=3)\nplt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral)\nplt.title(\"Jeu de données Spiral\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T21:37:23.08521Z","iopub.execute_input":"2026-01-21T21:37:23.085571Z","iopub.status.idle":"2026-01-21T21:37:23.266584Z","shell.execute_reply.started":"2026-01-21T21:37:23.085535Z","shell.execute_reply":"2026-01-21T21:37:23.265419Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\n\nclass NarrowDeepNet(nn.Module):\n    def __init__(self, num_layers, hidden_size=2, num_classes=3):\n        super(NarrowDeepNet, self).__init__()\n        layers = []\n        \n        # Couche d'entrée vers première couche cachée\n        layers.append(nn.Linear(2, hidden_size))\n        layers.append(nn.Tanh()) # Tanh marche souvent mieux que ReLU pour les réseaux très étroits\n        \n        # Ajout dynamique des couches cachées intermédiaires\n        for _ in range(num_layers - 1):\n            layers.append(nn.Linear(hidden_size, hidden_size))\n            layers.append(nn.Tanh())\n            \n        # Couche de sortie\n        layers.append(nn.Linear(hidden_size, num_classes))\n        \n        self.model = nn.Sequential(*layers)\n\n    def forward(self, x):\n        return self.model(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T21:37:23.268494Z","iopub.execute_input":"2026-01-21T21:37:23.268816Z","iopub.status.idle":"2026-01-21T21:37:23.276326Z","shell.execute_reply.started":"2026-01-21T21:37:23.268786Z","shell.execute_reply":"2026-01-21T21:37:23.275467Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_decision_boundary(model, X, y):\n    # Définir les limites du graphique\n    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5\n    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5\n    h = 0.02\n    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))\n    \n    # Prédire pour chaque point de la grille\n    grid_tensor = torch.FloatTensor(np.c_[xx.ravel(), yy.ravel()])\n    with torch.no_grad():\n        Z = model(grid_tensor)\n        Z = np.argmax(Z.numpy(), axis=1)\n    \n    Z = Z.reshape(xx.shape)\n    \n    # Affichage\n    plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral, alpha=0.8)\n    plt.scatter(X[:, 0], X[:, 1], c=y, s=40, cmap=plt.cm.Spectral, edgecolors='k')\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T21:37:23.277675Z","iopub.execute_input":"2026-01-21T21:37:23.278057Z","iopub.status.idle":"2026-01-21T21:37:23.301914Z","shell.execute_reply.started":"2026-01-21T21:37:23.278029Z","shell.execute_reply":"2026-01-21T21:37:23.301021Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom IPython.display import clear_output\n\n# --- 1. GÉNÉRATION DES DONNÉES ---\ndef toy_spiral3(N=150, K=3, D=2):\n    np.random.seed(0)\n    X = np.zeros((N*K,D))\n    y = np.zeros(N*K, dtype='uint8')\n    for j in range(K):\n        ix = range(N*j,N*(j+1))\n        r = np.linspace(0.0,1,N)\n        t = 5 + np.linspace(j*4,(j+1)*4,N) + np.random.randn(N)*0.3\n        X[ix] = np.c_[r*np.sin(t), r*np.cos(t)]\n        y[ix] = j\n    return X, y\n\n# --- 2. LE MODÈLE ---\nclass DeepNarrowNet(nn.Module):\n    def __init__(self, num_layers):\n        super().__init__()\n        self.layers = nn.ModuleList()\n        self.layers.append(nn.Linear(2, 2))\n        \n        for _ in range(num_layers - 1):\n            self.layers.append(nn.Linear(2, 2))\n            \n        self.classifier = nn.Linear(2, 3)\n        self.act = nn.Tanh() \n\n    def forward(self, x):\n        for layer in self.layers:\n            x = self.act(layer(x))\n        projection = x \n        logits = self.classifier(projection)\n        return logits, projection\n\n# --- 3. FONCTION D'AFFICHAGE ---\ndef plot_decision_boundaries(X, y, model):\n    x_min, x_max = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5\n    y_min, y_max = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5\n    h = 0.02\n    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))\n    grid_tensor = torch.tensor(np.c_[xx.ravel(), yy.ravel()], dtype=torch.float32)\n    \n    with torch.no_grad():\n        logits, _ = model(grid_tensor)\n        preds = torch.argmax(logits, dim=1).numpy()\n    \n    Z = preds.reshape(xx.shape)\n    plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral, alpha=0.3)\n    plt.scatter(X[:, 0], X[:, 1], c=y, s=20, cmap=plt.cm.Spectral, edgecolors='k')\n\n# --- 4. CONFIGURATION \"GOLDEN\" ---\nX_np, y_np = toy_spiral3(N=150)\nX_tensor = torch.tensor(X_np, dtype=torch.float32)\ny_tensor = torch.tensor(y_np, dtype=torch.long)\n\n# CHANGEMENT 1 : On passe à 6 couches. \n# C'est le point d'équilibre parfait pour 2 neurones de large.\n# (Si tu es obligé de mettre 10, dis-le moi, on devra tricher avec des ResNets).\nmodel = DeepNarrowNet(num_layers=6) \n\n# INITIALISATION XAVIER (Toujours obligatoire)\ndef init_weights(m):\n    if isinstance(m, nn.Linear):\n        torch.nn.init.xavier_uniform_(m.weight)\n        m.bias.data.fill_(0.01)\n\nmodel.apply(init_weights)\n\n# CHANGEMENT 2 : Learning rate plus doux (0.02) mais plus d'époques\noptimizer = optim.Adam(model.parameters(), lr=0.02, weight_decay=0)\ncriterion = nn.CrossEntropyLoss()\n\nepochs = 20001 # On laisse le temps au réseau de \"plier\" l'espace\n\nprint(\"Lancement... Patience, la spirale va apparaître vers l'époque 3000-5000.\")\n\nfor epoch in range(epochs):\n    logits, projection = model(X_tensor)\n    loss = criterion(logits, y_tensor)\n    \n    optimizer.zero_grad()\n    loss.backward()\n    optimizer.step()\n\n    if epoch % 1000 == 0:\n        clear_output(wait=True)\n        pred = torch.argmax(logits, 1)\n        acc = (pred == y_tensor).float().mean()\n        \n        plt.figure(figsize=(16, 6))\n\n        # GAUCHE : Frontières\n        plt.subplot(1, 2, 1)\n        plt.title(f\"Epoch {epoch} | Loss: {loss.item():.4f} | Acc: {acc:.2f}\")\n        plot_decision_boundaries(X_np, y_np, model)\n\n        # DROITE : Espace Latent\n        plt.subplot(1, 2, 2)\n        plt.title(\"Espace Latent (Le dé pliage)\")\n        proj_data = projection.detach().numpy()\n        plt.scatter(proj_data[:, 0], proj_data[:, 1], c=y_np, cmap=plt.cm.Spectral, s=20)\n        plt.xlim(-1.1, 1.1); plt.ylim(-1.1, 1.1)\n        plt.grid(alpha=0.3)\n        plt.show()\n        \n        # Arrêt anticipé si c'est parfait\n        if acc > 0.99:\n            print(\"VICTOIRE !\")\n            break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:51:53.55913Z","iopub.execute_input":"2026-01-21T22:51:53.559513Z","iopub.status.idle":"2026-01-21T22:52:03.28778Z","shell.execute_reply.started":"2026-01-21T22:51:53.559481Z","shell.execute_reply":"2026-01-21T22:52:03.286821Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"EXPERIENCE 2","metadata":{}},{"cell_type":"code","source":"!pip -q install umap-learn\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader, random_split\nfrom torchvision import datasets, transforms, models\nfrom torchvision.models import ResNet18_Weights\nimport umap\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"device:\", device)\n\n# ----------------------------\n# Data: MNIST -> 3x224x224\n# ----------------------------\ntf = transforms.Compose([\n    transforms.Resize(224),\n    transforms.Grayscale(num_output_channels=3),\n    transforms.ToTensor(),\n])\n\ndsTrainFull = datasets.MNIST(\"/kaggle/working/data\", train=True, download=True, transform=tf)\ndsTest      = datasets.MNIST(\"/kaggle/working/data\", train=False, download=True, transform=tf)\n\ntrainDs, valDs = random_split(dsTrainFull, [55000, 5000], generator=torch.Generator().manual_seed(42))\ntrainLoader = DataLoader(trainDs, batch_size=128, shuffle=True,  num_workers=2, pin_memory=True)\nvalLoader   = DataLoader(valDs,   batch_size=256, shuffle=False, num_workers=2, pin_memory=True)\n\n# ----------------------------\n# Train / eval (simple)\n# ----------------------------\ndef runEpoch(model, loader, opt=None):\n    \"\"\"If opt is None -> eval mode; else -> train mode.\"\"\"\n    crit = nn.CrossEntropyLoss()\n    if opt is None:\n        model.eval()\n        torch.set_grad_enabled(False)\n    else:\n        model.train()\n        torch.set_grad_enabled(True)\n\n    lossSum, correct, total = 0.0, 0, 0\n    for x, y in loader:\n        x, y = x.to(device), y.to(device)\n        if opt is not None:\n            opt.zero_grad(set_to_none=True)\n        logits = model(x)\n        loss = crit(logits, y)\n        if opt is not None:\n            loss.backward()\n            opt.step()\n\n        bs = x.size(0)\n        lossSum += loss.item() * bs\n        correct += (logits.argmax(1) == y).sum().item()\n        total += bs\n\n    return lossSum / total, correct / total\n\ndef trainModel(model, epochs=5, lr=1e-3, wd=1e-4):\n    \"\"\"Train model, return history dict.\"\"\"\n    model.to(device)\n    opt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\n\n    hist = {\"trainLoss\": [], \"trainAcc\": [], \"valLoss\": [], \"valAcc\": []}\n    for e in range(1, epochs+1):\n        trL, trA = runEpoch(model, trainLoader, opt)\n        vaL, vaA = runEpoch(model, valLoader, None)\n        hist[\"trainLoss\"].append(trL); hist[\"trainAcc\"].append(trA)\n        hist[\"valLoss\"].append(vaL);   hist[\"valAcc\"].append(vaA)\n        print(f\"epoch {e}/{epochs} | trainAcc={trA:.3f} valAcc={vaA:.3f} | trainLoss={trL:.4f} valLoss={vaL:.4f}\")\n    return hist\n\ndef plotHist(hist, title):\n    \"\"\"Plot loss/acc curves.\"\"\"\n    ep = range(1, len(hist[\"trainLoss\"])+1)\n    plt.figure(); plt.plot(ep, hist[\"trainLoss\"], label=\"train\"); plt.plot(ep, hist[\"valLoss\"], label=\"val\")\n    plt.title(title+\" loss\"); plt.xlabel(\"epoch\"); plt.ylabel(\"loss\"); plt.grid(); plt.legend(); plt.show()\n    plt.figure(); plt.plot(ep, hist[\"trainAcc\"], label=\"train\"); plt.plot(ep, hist[\"valAcc\"], label=\"val\")\n    plt.title(title+\" acc\"); plt.xlabel(\"epoch\"); plt.ylabel(\"acc\"); plt.grid(); plt.legend(); plt.show()\n\n# ----------------------------\n# Build models\n# ----------------------------\ndef buildResnet18(pretrained: bool):\n    \"\"\"ResNet18 with fc adapted to MNIST (10 classes).\"\"\"\n    if pretrained:\n        m = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1)\n    else:\n        m = models.resnet18(weights=None)\n    m.fc = nn.Linear(m.fc.in_features, 10)\n    return m\n\n# ----------------------------\n# V1: No pretrain\n# ----------------------------\nmodelNo = buildResnet18(pretrained=False)\nhistNo = trainModel(modelNo, epochs=5, lr=1e-3, wd=1e-4)\nplotHist(histNo, \"No-pretrain\")\n\n# ----------------------------\n# V2: Pretrained\n# ----------------------------\nmodelPre = buildResnet18(pretrained=True)\nhistPre = trainModel(modelPre, epochs=5, lr=5e-4, wd=1e-4)\nplotHist(histPre, \"Pretrained\")\n\n# ----------------------------\n# UMAP: pre-flatten features (avgpool output)\n# ----------------------------\n@torch.no_grad()\ndef extractPreFlatten(model, loader, maxBatches=20):\n    \"\"\"\n    Extract avgpool output (flattened) => representation just before flatten+fc.\n    maxBatches limits time (20 batches ~ 20*256 ≈ 5120 samples).\n    \"\"\"\n    model.eval()\n    feats, labs = [], []\n    for b, (x, y) in enumerate(loader):\n        if b >= maxBatches:\n            break\n        x = x.to(device)\n\n        z = model.conv1(x); z = model.bn1(z); z = model.relu(z); z = model.maxpool(z)\n        z = model.layer1(z); z = model.layer2(z); z = model.layer3(z); z = model.layer4(z)\n        z = model.avgpool(z)\n        z = torch.flatten(z, 1)          # (N, 512)\n\n        feats.append(z.cpu().numpy())\n        labs.append(y.numpy())\n\n    return np.concatenate(feats).astype(np.float32), np.concatenate(labs).astype(np.int64)\n\ndef plotUmap(features, labels, title):\n    \"\"\"Fit UMAP and plot.\"\"\"\n    reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42)\n    emb = reducer.fit_transform(features)\n    plt.figure(figsize=(7,6))\n    for c in np.unique(labels):\n        idx = labels == c\n        plt.scatter(emb[idx,0], emb[idx,1], s=8, alpha=0.7, label=str(c))\n    plt.title(title); plt.legend(); plt.grid(); plt.show()\n\nsmallLoader = DataLoader(trainDs, batch_size=256, shuffle=False, num_workers=2, pin_memory=True)\n\nfNo, yNo = extractPreFlatten(modelNo, smallLoader, maxBatches=20)\nplotUmap(fNo, yNo, \"UMAP pre-flatten (no-pretrain)\")\n\nfPre, yPre = extractPreFlatten(modelPre, smallLoader, maxBatches=20)\nplotUmap(fPre, yPre, \"UMAP pre-flatten (pretrained)\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T22:34:28.607901Z","iopub.execute_input":"2026-01-21T22:34:28.608269Z","iopub.status.idle":"2026-01-21T22:37:10.988985Z","shell.execute_reply.started":"2026-01-21T22:34:28.60824Z","shell.execute_reply":"2026-01-21T22:37:10.98774Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"EXPERIENCE 3","metadata":{}},{"cell_type":"code","source":"# ============================================================\n# EXPERIENCE 3 — ResNet50 sur ImageNet-10 (TRAIN/VAL only)\n# Style Exp2 + UMAP during training (Exp1 spirit)\n#   - 10 first classes\n#   - train/val split\n#   - curves loss/acc\n#   - UMAP snapshots each epoch\n#   - layer choice: \"layer2\" / \"layer3\" / \"layer4\" / \"avgpool\"\n# ============================================================\n\n!pip -q install umap-learn\n\nimport os, random\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\nfrom torchvision.models import ResNet50_Weights\nimport umap\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"device:\", device)\n\n# ----------------------------\n# Repro\n# ----------------------------\ndef setGlobalSeed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nsetGlobalSeed(42)\npin = torch.cuda.is_available()\n\n# ----------------------------\n# Dataset ImageNet-10 + split train/val\n# ----------------------------\nclass ImageNet10Split(Dataset):\n    \"\"\"\n    Build ImageNet-10 dataset (first 10 class folders), then split by indices.\n    Stores paths/labels, transform applied on the fly.\n    \"\"\"\n    def __init__(self, rootDir, indices, samples, transform):\n        self.rootDir = rootDir\n        self.indices = indices\n        self.samples = samples\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.indices)\n\n    def __getitem__(self, i):\n        idx = self.indices[i]\n        path, y = self.samples[idx]\n        img = Image.open(path).convert(\"RGB\")\n        img = self.transform(img)\n        return img, y\n\ndef buildImagenet10Samples(rootDir, numClasses=10, maxPerClass=800):\n    allClasses = sorted([d for d in os.listdir(rootDir) if os.path.isdir(os.path.join(rootDir, d))])\n    classNames = allClasses[:numClasses]\n    print(\"Classes used:\", classNames)\n\n    samples = []\n    for label, className in enumerate(classNames):\n        classDir = os.path.join(rootDir, className)\n        imgs = [f for f in os.listdir(classDir) if f.lower().endswith((\".jpg\",\".jpeg\",\".png\"))]\n        imgs.sort()\n        if maxPerClass is not None:\n            imgs = imgs[:maxPerClass]\n        for f in imgs:\n            samples.append((os.path.join(classDir, f), label))\n\n    print(\"Total samples:\", len(samples))\n    return samples\n\nimagenetPath = \"/kaggle/input/imagenet-object-localization-challenge/ILSVRC/Data/CLS-LOC/train\"\n\ntrainTf = transforms.Compose([\n    transforms.RandomResizedCrop(224),\n    transforms.RandomHorizontalFlip(),\n    transforms.ToTensor(),\n])\n\nevalTf = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n])\n\nsamples = buildImagenet10Samples(imagenetPath, numClasses=10, maxPerClass=800)\n\nN = len(samples)\nperm = np.random.permutation(N)\nnTrain = int(0.85 * N)\ntrainIdx = perm[:nTrain]\nvalIdx   = perm[nTrain:]\n\ntrainDs = ImageNet10Split(imagenetPath, trainIdx, samples, trainTf)\nvalDs   = ImageNet10Split(imagenetPath, valIdx,   samples, evalTf)\n\ntrainLoader = DataLoader(trainDs, batch_size=64,  shuffle=True,  num_workers=2, pin_memory=pin)\nvalLoader   = DataLoader(valDs,   batch_size=128, shuffle=False, num_workers=2, pin_memory=pin)\n\n# ----------------------------\n# Model ResNet50\n# ----------------------------\ndef buildResnet50(numClasses=10, pretrained=True):\n    if pretrained:\n        m = models.resnet50(weights=ResNet50_Weights.IMAGENET1K_V2)\n    else:\n        m = models.resnet50(weights=None)\n    m.fc = nn.Linear(m.fc.in_features, numClasses)\n    return m\n\nmodel = buildResnet50(numClasses=10, pretrained=True).to(device)\n\n# ----------------------------\n# Train/Eval like Exp2\n# ----------------------------\ndef runEpoch(model, loader, opt=None):\n    crit = nn.CrossEntropyLoss()\n    if opt is None:\n        model.eval()\n        torch.set_grad_enabled(False)\n    else:\n        model.train()\n        torch.set_grad_enabled(True)\n\n    lossSum, correct, total = 0.0, 0, 0\n    for x, y in loader:\n        x, y = x.to(device), y.to(device)\n        if opt is not None:\n            opt.zero_grad(set_to_none=True)\n\n        logits = model(x)\n        loss = crit(logits, y)\n\n        if opt is not None:\n            loss.backward()\n            opt.step()\n\n        bs = x.size(0)\n        lossSum += loss.item() * bs\n        correct += (logits.argmax(1) == y).sum().item()\n        total += bs\n\n    return lossSum/total, correct/total\n\ndef plotHist(hist, title):\n    ep = range(1, len(hist[\"trainLoss\"])+1)\n    plt.figure(); plt.plot(ep, hist[\"trainLoss\"], label=\"train\"); plt.plot(ep, hist[\"valLoss\"], label=\"val\")\n    plt.title(title+\" loss\"); plt.xlabel(\"epoch\"); plt.ylabel(\"loss\"); plt.grid(); plt.legend(); plt.show()\n    plt.figure(); plt.plot(ep, hist[\"trainAcc\"], label=\"train\"); plt.plot(ep, hist[\"valAcc\"], label=\"val\")\n    plt.title(title+\" acc\"); plt.xlabel(\"epoch\"); plt.ylabel(\"acc\"); plt.grid(); plt.legend(); plt.show()\n\n# ----------------------------\n# Layer features (paramétrable)\n# ----------------------------\n@torch.no_grad()\ndef extractLayerFeaturesResnet50(model, loader, layerName=\"avgpool\", maxBatches=6):\n    \"\"\"\n    layerName in {\"layer2\",\"layer3\",\"layer4\",\"avgpool\"}.\n    Flatten output to vectors for UMAP.\n    \"\"\"\n    model.eval()\n    feats, labs = [], []\n\n    for b, (x, y) in enumerate(loader):\n        if b >= maxBatches:\n            break\n        x = x.to(device)\n\n        z = model.conv1(x); z = model.bn1(z); z = model.relu(z); z = model.maxpool(z)\n        z = model.layer1(z)\n\n        z = model.layer2(z)\n        if layerName == \"layer2\":\n            pass\n        else:\n            z = model.layer3(z)\n            if layerName == \"layer3\":\n                pass\n            else:\n                z = model.layer4(z)\n                if layerName == \"avgpool\":\n                    z = model.avgpool(z)\n\n        z = torch.flatten(z, 1)\n        feats.append(z.cpu().numpy())\n        labs.append(y.numpy())\n\n    return np.concatenate(feats).astype(np.float32), np.concatenate(labs).astype(np.int64)\n\ndef plotUmapEmb(emb, labels, title):\n    plt.figure(figsize=(7,6))\n    for c in np.unique(labels):\n        idx = labels == c\n        plt.scatter(emb[idx,0], emb[idx,1], s=8, alpha=0.7, label=str(c))\n    plt.title(title); plt.legend(); plt.grid(); plt.show()\n\ndef umapSnapshotTrainVal(model, epochTag, layerName=\"avgpool\",\n                         trainBatches=6, valBatches=4,\n                         nNeighbors=15, minDist=0.1):\n    \"\"\"\n    Fit UMAP on TRAIN features; transform VAL features.\n    \"\"\"\n    smallTrainLoader = DataLoader(trainDs, batch_size=128, shuffle=True,  num_workers=2, pin_memory=pin)\n    smallValLoader   = DataLoader(valDs,   batch_size=128, shuffle=False, num_workers=2, pin_memory=pin)\n\n    Xtr, ytr = extractLayerFeaturesResnet50(model, smallTrainLoader, layerName=layerName, maxBatches=trainBatches)\n    reducer = umap.UMAP(n_neighbors=nNeighbors, min_dist=minDist, random_state=42, n_epochs=200)\n    embTr = reducer.fit_transform(Xtr)\n    plotUmapEmb(embTr, ytr, f\"UMAP TRAIN - {layerName} (epoch {epochTag})\")\n\n    Xva, yva = extractLayerFeaturesResnet50(model, smallValLoader, layerName=layerName, maxBatches=valBatches)\n    embVa = reducer.transform(Xva)\n    plotUmapEmb(embVa, yva, f\"UMAP VAL mapped - {layerName} (epoch {epochTag})\")\n\n# ----------------------------\n# Run Exp3\n# ----------------------------\nepochs = 3\nlr = 1e-4\nwd = 1e-4\nlayerToVisualize = \"avgpool\"  # \"layer2\" / \"layer3\" / \"layer4\" / \"avgpool\"\n\nopt = torch.optim.Adam(model.parameters(), lr=lr, weight_decay=wd)\nhist = {\"trainLoss\": [], \"trainAcc\": [], \"valLoss\": [], \"valAcc\": []}\n\n# Snapshot BEFORE training\numapSnapshotTrainVal(model, epochTag=\"0 (before)\", layerName=layerToVisualize, trainBatches=5, valBatches=3)\n\nfor e in range(1, epochs+1):\n    trL, trA = runEpoch(model, trainLoader, opt)\n    vaL, vaA = runEpoch(model, valLoader, None)\n\n    hist[\"trainLoss\"].append(trL); hist[\"trainAcc\"].append(trA)\n    hist[\"valLoss\"].append(vaL);   hist[\"valAcc\"].append(vaA)\n\n    print(f\"epoch {e}/{epochs} | trainAcc={trA:.3f} valAcc={vaA:.3f} | trainLoss={trL:.4f} valLoss={vaL:.4f}\")\n\n    # Snapshot each epoch (Exp1 style)\n    umapSnapshotTrainVal(model, epochTag=str(e), layerName=layerToVisualize, trainBatches=5, valBatches=3)\n\nplotHist(hist, \"Exp3 ResNet50 ImageNet-10\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-21T23:00:16.065934Z","iopub.execute_input":"2026-01-21T23:00:16.066256Z"}},"outputs":[],"execution_count":null}]}