{"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":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":7327,"databundleVersionId":861871,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🐶 Clasificarea Raselor de Câini: FFNN vs CNN vs ResNEXT\nAcest notebook antrenează două arhitecturi diferite pentru a demonstra superioritatea rețelelor convoluționale pe imagini.\nFolosim **TensorBoard** și **MLflow** pentru tracking profesionist.","metadata":{}},{"cell_type":"code","source":"# Pe Kaggle, MLflow trebuie instalat. PyTorch și TensorBoard sunt preinstalate.\n!pip install -q mlflow","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:12:22.445015Z","iopub.execute_input":"2026-03-26T18:12:22.445246Z","iopub.status.idle":"2026-03-26T18:12:36.747724Z","shell.execute_reply.started":"2026-03-26T18:12:22.445207Z","shell.execute_reply":"2026-03-26T18:12:36.74679Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, random_split\nfrom torchvision import transforms\nfrom torchvision.models import resnext101_32x8d, resnext50_32x4d\nfrom torch.utils.tensorboard import SummaryWriter\n\nimport mlflow\nimport mlflow.pytorch\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"🚀 Rulăm pe: {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:12:36.749928Z","iopub.execute_input":"2026-03-26T18:12:36.750232Z","iopub.status.idle":"2026-03-26T18:13:17.752161Z","shell.execute_reply.started":"2026-03-26T18:12:36.750192Z","shell.execute_reply":"2026-03-26T18:13:17.751403Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1. Crearea clasei Custom Dataset\nCitirea imaginilor `.jpg` din directorul de antrenament folosind fișierul `labels.csv`.","metadata":{}},{"cell_type":"code","source":"class DogBreedsDataset(Dataset):\n    def __init__(self, csv_file, root_dir, transform=None):\n        self.labels_df = pd.read_csv(csv_file)\n        self.root_dir = root_dir\n        self.transform = transform\n        self.classes = sorted(self.labels_df['breed'].unique())\n        self.class_to_idx = {breed: idx for idx, breed in enumerate(self.classes)}\n\n    def __len__(self):\n        return len(self.labels_df)\n\n    def __getitem__(self, idx):\n        img_name = os.path.join(self.root_dir, f\"{self.labels_df.iloc[idx, 0]}.jpg\")\n        image = Image.open(img_name).convert('RGB')\n        label = self.class_to_idx[self.labels_df.iloc[idx, 1]]\n        if self.transform:\n            image = self.transform(image)\n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.753073Z","iopub.execute_input":"2026-03-26T18:13:17.753745Z","iopub.status.idle":"2026-03-26T18:13:17.759999Z","shell.execute_reply.started":"2026-03-26T18:13:17.753712Z","shell.execute_reply":"2026-03-26T18:13:17.759248Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Pregătirea Datelor (Transformări și Minibatches)","metadata":{}},{"cell_type":"code","source":"# --- DATA AUGMENTATION ---\n# Antrenament: Modelul vede o versiune ușor modificată la fiecare epocă\ntrain_transform = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), # Mai mare pentru ResNeXt\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(15),\n    transforms.ColorJitter(brightness=0.2, contrast=0.2),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\n# Validare: Modelul vede poza pură (doar redimensionată și normalizată)\nval_transform = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.76104Z","iopub.execute_input":"2026-03-26T18:13:17.761466Z","iopub.status.idle":"2026-03-26T18:13:17.782418Z","shell.execute_reply.started":"2026-03-26T18:13:17.761419Z","shell.execute_reply":"2026-03-26T18:13:17.781608Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Căile standard Kaggle pentru acest dataset\nCSV_PATH = '/kaggle/input/competitions/dog-breed-identification/labels.csv' \nIMG_DIR = '/kaggle/input/competitions/dog-breed-identification/train'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.783478Z","iopub.execute_input":"2026-03-26T18:13:17.783855Z","iopub.status.idle":"2026-03-26T18:13:17.79825Z","shell.execute_reply.started":"2026-03-26T18:13:17.78381Z","shell.execute_reply":"2026-03-26T18:13:17.797495Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 64 # Putem folosi un batch mai mare pe GPU Kaggle (ex: T4 x2)\n\n# Trebuie să facem două instanțe separate de Dataset pentru că au transformări diferite!\nfull_dataset = DogBreedsDataset(csv_file=CSV_PATH, root_dir=IMG_DIR)\ntrain_size = int(0.8 * len(full_dataset))\nval_size = len(full_dataset) - train_size","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.799235Z","iopub.execute_input":"2026-03-26T18:13:17.799547Z","iopub.status.idle":"2026-03-26T18:13:17.836188Z","shell.execute_reply.started":"2026-03-26T18:13:17.799501Z","shell.execute_reply":"2026-03-26T18:13:17.835384Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Împărțim datele \"brute\" (doar indecșii)\ntrain_subset, val_subset = random_split(full_dataset, [train_size, val_size])\n\n# Aplicăm transformările corecte\ntrain_subset.dataset.transform = train_transform\nval_subset.dataset.transform = val_transform","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.838612Z","iopub.execute_input":"2026-03-26T18:13:17.838965Z","iopub.status.idle":"2026-03-26T18:13:17.855131Z","shell.execute_reply.started":"2026-03-26T18:13:17.838936Z","shell.execute_reply":"2026-03-26T18:13:17.854322Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader = DataLoader(train_subset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)\nval_loader = DataLoader(val_subset, batch_size=BATCH_SIZE, shuffle=False, num_workers=4)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.856221Z","iopub.execute_input":"2026-03-26T18:13:17.857015Z","iopub.status.idle":"2026-03-26T18:13:17.861298Z","shell.execute_reply.started":"2026-03-26T18:13:17.856978Z","shell.execute_reply":"2026-03-26T18:13:17.860636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NUM_CLASSES = len(full_dataset.classes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.862391Z","iopub.execute_input":"2026-03-26T18:13:17.862919Z","iopub.status.idle":"2026-03-26T18:13:17.871771Z","shell.execute_reply.started":"2026-03-26T18:13:17.862891Z","shell.execute_reply":"2026-03-26T18:13:17.871053Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(f\"Total imagini: {len(full_dataset)} | Clase: {NUM_CLASSES}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.872754Z","iopub.execute_input":"2026-03-26T18:13:17.873079Z","iopub.status.idle":"2026-03-26T18:13:17.882662Z","shell.execute_reply.started":"2026-03-26T18:13:17.873037Z","shell.execute_reply":"2026-03-26T18:13:17.881994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def imshow_batch_complex(inputs, labels, class_names, max_images=8):\n    \"\"\"\n    Afișează un grid aerisit de imagini, fiecare cu numele rasei corespunzătoare.\n    \"\"\"\n    # Limităm numărul de imagini pentru a păstra graficul curat (ex: 2 rânduri a câte 4 poze)\n    num_images = min(len(inputs), max_images)\n    rows = int(np.ceil(num_images / 4))\n    cols = min(num_images, 4)\n    \n    # Creăm grila de sub-grafice (figsize mai mare pentru 'aerisire')\n    fig, axes = plt.subplots(rows, cols, figsize=(16, 4 * rows))\n    \n    # Ne asigurăm că 'axes' este o listă plată, chiar dacă avem un singur rând\n    if num_images > 1:\n        axes = axes.flatten()\n    else:\n        axes = [axes]\n        \n    mean = np.array([0.485, 0.456, 0.406])\n    std = np.array([0.229, 0.224, 0.225])\n    \n    for i in range(num_images):\n        # 1. Extragem imaginea individuală și o transformăm pentru Matplotlib\n        img = inputs[i].numpy().transpose((1, 2, 0))\n        \n        # 2. Inversăm normalizarea\n        img = std * img + mean\n        img = np.clip(img, 0, 1)\n        \n        # 3. Desenăm imaginea în celula ei\n        axes[i].imshow(img)\n        \n        # 4. Găsim numele rasei folosind indexul din tensorul 'labels'\n        breed_name = class_names[labels[i].item()]\n        \n        # 5. Adăugăm titlul (curat, bold, cu padding pentru spațiu)\n        axes[i].set_title(breed_name, fontsize=12, fontweight='bold', pad=10)\n        axes[i].axis('off') # Ascundem axele X și Y\n        \n    # Ascundem celulele goale dacă numărul de imagini nu umple perfect grila\n    for j in range(num_images, len(axes)):\n        axes[j].axis('off')\n        \n    plt.suptitle(\"📸 Exemple Data Augmentation (Train)\", fontsize=18, fontweight='bold', y=1.05)\n    \n    # Această comandă magică previne suprapunerea textelor!\n    plt.tight_layout() \n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.883774Z","iopub.execute_input":"2026-03-26T18:13:17.884186Z","iopub.status.idle":"2026-03-26T18:13:17.894512Z","shell.execute_reply.started":"2026-03-26T18:13:17.884139Z","shell.execute_reply":"2026-03-26T18:13:17.893939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- CUM SE APELEAZĂ ACUM ---\n# Extragem un batch întreg de imagini și etichetele lor numerice\ninputs, labels = next(iter(train_loader))\n\n# Apelăm noua funcție (îi dăm și lista cu numele raselor din full_dataset)\nimshow_batch_complex(inputs, labels, full_dataset.classes, max_images=8)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:17.895321Z","iopub.execute_input":"2026-03-26T18:13:17.895637Z","iopub.status.idle":"2026-03-26T18:13:21.472636Z","shell.execute_reply.started":"2026-03-26T18:13:17.895566Z","shell.execute_reply":"2026-03-26T18:13:21.471395Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Definirea Modelelor\nAici declarăm un FFNN simplu (care va distruge informația spațială) și un CNN standard.","metadata":{}},{"cell_type":"code","source":"class FFNN(nn.Module):\n    def __init__(self, num_classes):\n        super(FFNN, self).__init__()\n        self.net = nn.Sequential(\n            nn.Flatten(),\n            nn.Linear(3 * 224 * 224, 1024),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(1024, 512),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(256, num_classes)\n        )\n\n    def forward(self, x):\n        return self.net(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:21.473897Z","iopub.execute_input":"2026-03-26T18:13:21.474312Z","iopub.status.idle":"2026-03-26T18:13:21.480199Z","shell.execute_reply.started":"2026-03-26T18:13:21.474277Z","shell.execute_reply":"2026-03-26T18:13:21.479366Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SimpleCNN(nn.Module):\n    def __init__(self, num_classes):\n        super(SimpleCNN, self).__init__()\n        self.net = nn.Sequential(\n            nn.Conv2d(3, 32, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 224 -> 112x112\n            \n            nn.Conv2d(32, 64, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 112 -> 56x56\n            \n            nn.Conv2d(64, 128, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.MaxPool2d(2, 2), # 56 -> 28x28\n            \n            nn.Flatten(),\n            # AICI ESTE MODIFICAREA CRUCIALĂ: 128 filtre * 28 lățime * 28 înălțime\n            nn.Linear(128 * 28 * 28, 512), \n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        return self.net(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:21.481303Z","iopub.execute_input":"2026-03-26T18:13:21.481633Z","iopub.status.idle":"2026-03-26T18:13:21.584644Z","shell.execute_reply.started":"2026-03-26T18:13:21.48157Z","shell.execute_reply":"2026-03-26T18:13:21.58382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DogBreedResNeXt101(nn.Module):\n    def __init__(self, num_classes):\n        super(DogBreedResNeXt101, self).__init__()\n        \n        # SECRETUL AICI: Folosim weights='DEFAULT' ca string. E mult mai sigur!\n        self.base_model = resnext50_32x4d(weights='DEFAULT')# resnext101_32x8d(weights='DEFAULT')\n        num_ftrs = self.base_model.fc.in_features\n        \n        # Înlocuim capul cu unul adaptat pentru clasele noastre\n        self.base_model.fc = nn.Sequential(\n            nn.Dropout(0.5),\n            nn.Linear(num_ftrs, num_classes)\n        )\n\n    def forward(self, x):\n        return self.base_model(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:21.585738Z","iopub.execute_input":"2026-03-26T18:13:21.58603Z","iopub.status.idle":"2026-03-26T18:13:21.598466Z","shell.execute_reply.started":"2026-03-26T18:13:21.585997Z","shell.execute_reply":"2026-03-26T18:13:21.597894Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Bucla de Antrenament cu Tracking\nAceastă funcție loghează automat totul în TensorBoard și MLflow.","metadata":{}},{"cell_type":"code","source":"def train_model(model, model_name, epochs, lr=0.001, custom_optimizer=None):\n    criterion = nn.CrossEntropyLoss()\n    \n    # Folosim AdamW care este superior pentru Weight Decay (Regularizare L2)\n    if custom_optimizer is not None:\n        optimizer = custom_optimizer\n    else:\n        optimizer = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4)\n        \n    # SCHEDULER: Scade LR-ul la jumătate dacă acuratețea pe validare nu crește 2 epoci la rând\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2)\n    \n    writer = SummaryWriter(f'/kaggle/working/runs/dog_breeds/{model_name}')\n    mlflow.set_tracking_uri(\"sqlite:////kaggle/working/mlflow.db\")\n    mlflow.set_experiment(\"Dog_Breeds_Classification\")\n    \n    # Istoric și Early Stopping\n    history = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': []}\n    best_val_acc = 0.0\n    patience_counter = 0\n    EARLY_STOPPING_PATIENCE = 5\n    \n    with mlflow.start_run(run_name=model_name):\n        mlflow.log_param(\"epochs\", epochs)\n        mlflow.log_param(\"optimizer\", \"AdamW\")\n        mlflow.log_param(\"weight_decay\", 1e-4)\n        \n        for epoch in range(epochs):\n            model.train()\n            running_loss, correct_train, total_train = 0.0, 0, 0\n            \n            for inputs, labels in train_loader:\n                inputs, labels = inputs.to(device), labels.to(device)\n                optimizer.zero_grad()\n                outputs = model(inputs)\n                loss = criterion(outputs, labels)\n                loss.backward()\n                optimizer.step()\n                \n                running_loss += loss.item() * inputs.size(0)\n                _, predicted = torch.max(outputs.data, 1)\n                total_train += labels.size(0)\n                correct_train += (predicted == labels).sum().item()\n                \n            epoch_loss = running_loss / len(train_loader.dataset)\n            epoch_acc = correct_train / total_train\n            \n            model.eval()\n            val_loss, correct_val, total_val = 0.0, 0, 0\n            with torch.no_grad():\n                for inputs, labels in val_loader:\n                    inputs, labels = inputs.to(device), labels.to(device)\n                    outputs = model(inputs)\n                    loss = criterion(outputs, labels)\n                    val_loss += loss.item() * inputs.size(0)\n                    _, predicted = torch.max(outputs.data, 1)\n                    total_val += labels.size(0)\n                    correct_val += (predicted == labels).sum().item()\n                    \n            val_epoch_loss = val_loss / len(val_loader.dataset)\n            val_epoch_acc = correct_val / total_val\n            \n            # Salvăm în istoric\n            history['train_loss'].append(epoch_loss)\n            history['val_loss'].append(val_epoch_loss)\n            history['train_acc'].append(epoch_acc)\n            history['val_acc'].append(val_epoch_acc)\n            \n            print(f\"[{model_name}] Epoca {epoch+1}/{epochs} | Train Loss: {epoch_loss:.4f} | Val Loss: {val_epoch_loss:.4f} | Train Acc: {epoch_acc:.4f} | Val Acc: {val_epoch_acc:.4f}\")\n            \n            # Logare MLflow / Tensorboard\n            writer.add_scalars('Loss', {'Train': epoch_loss, 'Val': val_epoch_loss}, epoch)\n            writer.add_scalars('Accuracy', {'Train': epoch_acc, 'Val': val_epoch_acc}, epoch)\n            mlflow.log_metrics({\"train_loss\": epoch_loss, \"val_loss\": val_epoch_loss, \"val_acc\": val_epoch_acc}, step=epoch)\n            \n            # SCHEDULER STEP (Ascultă după val_acc)\n            scheduler.step(val_epoch_acc)\n            \n            # EARLY STOPPING LOGIC\n            if val_epoch_acc > best_val_acc:\n                best_val_acc = val_epoch_acc\n                patience_counter = 0\n                # Salvăm cele mai bune greutăți\n                torch.save(model.state_dict(), f'/kaggle/working/{model_name}_best.pth')\n            else:\n                patience_counter += 1\n                if patience_counter >= EARLY_STOPPING_PATIENCE:\n                    print(f\"⚠️ Early Stopping declanșat la epoca {epoch+1}! Modelul a stagnat.\")\n                    break\n        \n        # Încărcăm cel mai bun model la final și îl dăm lui MLflow\n        model.load_state_dict(torch.load(f'/kaggle/working/{model_name}_best.pth'))\n        mlflow.pytorch.log_model(model, f\"{model_name}_final\")\n        \n    writer.close()\n    return history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:21.599436Z","iopub.execute_input":"2026-03-26T18:13:21.599702Z","iopub.status.idle":"2026-03-26T18:13:21.614987Z","shell.execute_reply.started":"2026-03-26T18:13:21.599676Z","shell.execute_reply":"2026-03-26T18:13:21.614239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Instanțiem modelul corectat\nresnext_model = DogBreedResNeXt101(num_classes=NUM_CLASSES)\n\n# --- MAGIA MULTI-GPU ---\nif torch.cuda.device_count() > 1:\n    print(f\"🔥 Am detectat {torch.cuda.device_count()} GPU-uri! Trecem pe Multi-GPU.\")\n    resnext_model = nn.DataParallel(resnext_model)\n\nresnext_model = resnext_model.to(device)\n\n\nprint(\"\\n🚀 PORNIM ResNeXt101 (Fine-Tuning & Diff LR)...\")\n# Extragem modelul real de sub 'DataParallel' pentru a-i accesa straturile\nactual_model = resnext_model.module if isinstance(resnext_model, nn.DataParallel) else resnext_model\n\n# Acum folosim 'actual_model' pentru a separa parametrii\nhead_params = list(actual_model.base_model.fc.parameters())\nbase_params = [p for n, p in actual_model.named_parameters() if not n.startswith('base_model.fc')]\n\n# Optimizator cu Learning Rate diferențiat ȘI Weight Decay\noptimizer_resnext = optim.AdamW([\n    {'params': base_params, 'lr': 1e-5, 'weight_decay': 1e-4},\n    {'params': head_params, 'lr': 1e-3, 'weight_decay': 1e-4} \n])\n\nhistory_resnext = train_model(\n    model=resnext_model, \n    model_name=\"ResNeXt101_FineTuned\", \n    epochs=5, \n    custom_optimizer=optimizer_resnext\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:13:21.615935Z","iopub.execute_input":"2026-03-26T18:13:21.61666Z","iopub.status.idle":"2026-03-26T18:20:23.073297Z","shell.execute_reply.started":"2026-03-26T18:13:21.616627Z","shell.execute_reply":"2026-03-26T18:20:23.072472Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 10\n\nprint(\"⏳ Inițializare modele...\")\nffnn_model = FFNN(num_classes=NUM_CLASSES).to(device)\ncnn_model = SimpleCNN(num_classes=NUM_CLASSES).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:20:23.074586Z","iopub.execute_input":"2026-03-26T18:20:23.075308Z","iopub.status.idle":"2026-03-26T18:20:24.774281Z","shell.execute_reply.started":"2026-03-26T18:20:23.075277Z","shell.execute_reply":"2026-03-26T18:20:24.773602Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n🚀 PORNIM FFNN...\")\ntrain_model(ffnn_model, \"FFNN_Baseline\", epochs=EPOCHS, lr=0.0005)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:20:24.775301Z","iopub.execute_input":"2026-03-26T18:20:24.775576Z","iopub.status.idle":"2026-03-26T18:23:46.148646Z","shell.execute_reply.started":"2026-03-26T18:20:24.775534Z","shell.execute_reply":"2026-03-26T18:23:46.147993Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\n🚀 PORNIM CNN...\")\ntrain_model(cnn_model, \"CNN_Standard\", epochs=EPOCHS, lr=0.0005)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:23:46.149884Z","iopub.execute_input":"2026-03-26T18:23:46.150151Z","iopub.status.idle":"2026-03-26T18:28:04.95015Z","shell.execute_reply.started":"2026-03-26T18:23:46.150111Z","shell.execute_reply":"2026-03-26T18:28:04.949434Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 🧠 Ghid de Interpretare: Saliency Maps (Hărți de Atenție)\n\nCând antrenăm o rețea neurală complexă (precum ResNeXt101), ea devine un \"black box\" (cutie neagră). Știm *ce* a decis, dar nu știm *de ce*. Aici intervin **Saliency Maps** — o tehnică de Explainable AI (XAI) care ne permite să facem o \"radiografie\" a procesului decizional.\n\n---\n\n## 🔍 1. Cum citim o Hartă de Saliență?\n\nHarta de saliență ne arată exact **care pixeli din imaginea originală au influențat cel mai mult decizia finală** a modelului. Nu este o detectare de contururi, ci o hartă a *importanței*.\n\nDacă o privim ca pe o hartă termică, o citim astfel:\n\n* ⬛ **Negru (Zona \"Rece\"):** Acești pixeli au avut importanță **0** în luarea deciziei. Modelul i-a ignorat complet. (Ex: Peretele din fundal, cerul, iarba).\n* 🟥 **Roșu (Zona \"Caldă\"):** Acești pixeli au contribuit moderat la decizie. Oferă context rețelei.\n* 🟨 **Galben / Alb (Zona \"Fierbinte\"):** Aceasta este **atenția maximă**. Dacă ai modifica acești pixeli, modelul și-ar schimba complet predicția. (Ex: Ochii, forma botului sau petele specifice rasei).\n\n**🏆 Semnul unui model excelent:** Dacă zonele \"fierbinți\" conturează fața sau corpul câinelui, iar fundalul este complet negru, înseamnă că modelul a învățat caracteristicile reale ale rasei și nu a suferit de *overfitting* pe elementele de decor.\n\n---\n\n## 🎨 2. Misterul Culorilor: De ce este Roșie?\n\nRețeaua neurală **nu generează culori**. Matematic, ea calculează doar derivata (gradientul) scorului clasei prezise în raport cu fiecare pixel de intrare:\n\n$$Gradient = \\frac{\\partial (Scor\\_Clas\\breve{a})}{\\partial (Pixel_{i,j})}$$\n\nNoi extragem valoarea absolută a acestor gradienți și îi normalizăm forțat în intervalul matematic $[0, 1]$. Rezultatul brut este doar o matrice de numere (o imagine alb-negru).\n\n**Culoarea roșie este adăugată artificial** în momentul plotării în Python, folosind biblioteca `matplotlib`. În codul nostru, am specificat `cmap='hot'`, ceea ce transformă matricea matematică într-o hartă termală:\n\n* Valoare $0.0 \\rightarrow$ Negru\n* Valoare $0.5 \\rightarrow$ Roșu\n* Valoare $1.0 \\rightarrow$ Alb / Galben\n\n---\n\n## 🔄 3. Alternative Vizuale (Colormaps)\n\nDacă dorești să schimbi estetica vizualizării, poți modifica argumentul `cmap` în funcția `imshow()`. Iată cele mai folosite standarde în industrie:\n\n| Argument Matplotlib | Stil Vizual | Utilizare |\n| :--- | :--- | :--- |\n| `cmap='hot'` | 🔴 Termal (Negru $\\rightarrow$ Roșu $\\rightarrow$ Alb) | Excelent pentru contrast puternic. |\n| `cmap='viridis'` | 🟢 Științific (Mov $\\rightarrow$ Verde $\\rightarrow$ Galben) | Standardul actual în Data Science; prietenos cu persoanele daltoniste. |\n| `cmap='jet'` | 🌈 Curcubeu (Albastru $\\rightarrow$ Verde $\\rightarrow$ Roșu) | Stilul clasic \"radar meteo\". Poate fi zgomotos vizual. |\n| `cmap='gray'` | ⚪ Alb-Negru (Negru $\\rightarrow$ Gri $\\rightarrow$ Alb) | Varianta pură, matematică, fără distragerea atenției prin culori. |\n\n> **💡 Sfat de cod:** Pentru a testa altă culoare, pur și simplu caută linia `ax[1].imshow(saliency, cmap='hot')` în funcția ta și înlocuiește `'hot'` cu `'viridis'`.","metadata":{}},{"cell_type":"code","source":"import torch.nn.functional as F\n\n# --- PASUL 1: SCANAREA ȘI EXTRAGEREA EXEMPLELOR INTERESANTE ---\ndef get_interesting_samples(model, loader):\n    model.eval()\n    correct_samples = []\n    incorrect_samples = []\n    all_samples = []\n    \n    with torch.no_grad(): # Scanăm rapid, fără să încărcăm memoria cu gradienți\n        for inputs, labels in loader:\n            inputs = inputs.to(device)\n            labels = labels.to(device)\n            \n            outputs = model(inputs)\n            probs = F.softmax(outputs, dim=1) # Transformăm logit-urile în procente (0-1)\n            max_probs, preds = torch.max(probs, dim=1)\n            \n            for i in range(inputs.size(0)):\n                sample_data = {\n                    'input': inputs[i].cpu(), # Mutăm pe CPU ca să nu umplem VRAM-ul\n                    'true_label': labels[i].item(),\n                    'pred_label': preds[i].item(),\n                    'confidence': max_probs[i].item()\n                }\n                \n                all_samples.append(sample_data)\n                \n                if sample_data['true_label'] == sample_data['pred_label']:\n                    correct_samples.append(sample_data)\n                else:\n                    incorrect_samples.append(sample_data)\n\n    # Sortăm pentru a găsi extremele\n    # 1. Top 10 Corecte (Cele mai sigure și corecte decizii)\n    top_correct = sorted(correct_samples, key=lambda x: x['confidence'], reverse=True)[:10]\n    \n    # 2. Top 10 Greșite (Modelul este foarte sigur, dar se înșală grav)\n    top_incorrect = sorted(incorrect_samples, key=lambda x: x['confidence'], reverse=True)[:10]\n    \n    # 3. Top 10 Borderline (Cea mai mică încredere dintre toate imaginile)\n    top_borderline = sorted(all_samples, key=lambda x: x['confidence'])[:10]\n    \n    return top_correct, top_incorrect, top_borderline\n\n# --- PASUL 2: GENERAREA HĂRȚILOR ȘI AFIȘAREA ÎN GRILĂ ---\ndef plot_saliency_grid(model, samples_list, classes_list, title):\n    print(f\"\\n{'-'*60}\\n{title}\\n{'-'*60}\")\n    model.eval()\n    \n    # Creăm o grilă de 5 rânduri x 4 coloane (10 poze cu perechea lor de saliency)\n    fig, axes = plt.subplots(5, 4, figsize=(18, 22))\n    fig.suptitle(title, fontsize=20, y=1.02)\n    \n    for idx, sample in enumerate(samples_list):\n        # Pregătim imaginea pentru Backward Pass\n        img_tensor = sample['input'].unsqueeze(0).to(device)\n        img_tensor.requires_grad_()\n        \n        output = model(img_tensor)\n        pred_label = output.argmax(dim=1).item()\n        \n        # Calculăm gradientul\n        loss = F.cross_entropy(output, torch.tensor([pred_label]).to(device))\n        model.zero_grad()\n        loss.backward()\n        \n        # Extragem Saliency\n        saliency, _ = torch.max(img_tensor.grad.abs().detach(), dim=1)\n        saliency = saliency.squeeze().cpu().numpy()\n        saliency = (saliency - saliency.min()) / (saliency.max() - saliency.min() + 1e-8)\n        \n        # Inversăm normalizarea originală\n        img_original = img_tensor.squeeze().detach().cpu().numpy().transpose((1, 2, 0))\n        img_original = np.clip([0.229, 0.224, 0.225] * img_original + [0.485, 0.456, 0.406], 0, 1)\n        \n        # Stabilim unde desenăm în grilă\n        row = idx // 2\n        col = (idx % 2) * 2\n        \n        true_name = classes_list[sample['true_label']]\n        pred_name = classes_list[pred_label]\n        conf = sample['confidence'] * 100\n        \n        # Desenăm imaginea originală\n        axes[row, col].imshow(img_original)\n        axes[row, col].set_title(f\"Adevărat: {true_name}\\nPrezis: {pred_name} ({conf:.1f}%)\", fontsize=10)\n        axes[row, col].axis('off')\n        \n        # Desenăm Saliency Map-ul\n        axes[row, col+1].imshow(saliency, cmap='hot')\n        axes[row, col+1].set_title(\"Saliency Map\", fontsize=10)\n        axes[row, col+1].axis('off')\n\n    plt.tight_layout()\n    plt.show()\n\n# --- EXECUȚIA COMPLETĂ ---\nprint(\"⏳ Scanăm setul de validare pentru a găsi exemplele extreme...\")\ntop_correct, top_incorrect, top_borderline = get_interesting_samples(resnext_model, val_loader)\n\n# Afișăm cele 3 categorii\nplot_saliency_grid(resnext_model, top_correct, full_dataset.classes, \"✅ TOP 10 CORECTE (Încredere Maximă)\")\nplot_saliency_grid(resnext_model, top_incorrect, full_dataset.classes, \"❌ TOP 10 GREȘITE (Foarte Sigur, dar Greșit)\")\nplot_saliency_grid(resnext_model, top_borderline, full_dataset.classes, \"🤷 TOP 10 BORDERLINE (Cea mai mare confuzie)\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:28:04.951349Z","iopub.execute_input":"2026-03-26T18:28:04.951622Z","iopub.status.idle":"2026-03-26T18:28:23.676624Z","shell.execute_reply.started":"2026-03-26T18:28:04.951561Z","shell.execute_reply":"2026-03-26T18:28:23.674984Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_saliency(model, loader, classes_list):\n    model.eval()\n    inputs, labels = next(iter(loader))\n    \n    # Luăm o singură imagine\n    img_tensor = inputs[0].unsqueeze(0).to(device)\n    img_tensor.requires_grad_()\n    true_label = labels[0].item()\n    \n    # Rulăm rețeaua și calculăm gradienții\n    output = model(img_tensor)\n    pred_label = output.argmax(dim=1).item()\n    \n    loss = F.cross_entropy(output, torch.tensor([pred_label]).to(device))\n    model.zero_grad()\n    loss.backward()\n    \n    # Extragem atenția (gradientul maxim per pixel)\n    saliency, _ = torch.max(img_tensor.grad.data.abs(), dim=1)\n    saliency = saliency.squeeze().cpu().numpy()\n    \n    # Normalizare inversă pentru imaginea originală\n    img_original = img_tensor.squeeze().detach().cpu().numpy().transpose((1, 2, 0))\n    img_original = np.clip([0.229, 0.224, 0.225] * img_original + [0.485, 0.456, 0.406], 0, 1)\n    \n    # Plotare\n    fig, ax = plt.subplots(1, 2, figsize=(10, 5))\n    ax[0].imshow(img_original)\n    ax[0].set_title(f\"Adevărat: {classes_list[true_label]} \\n Prezis: {classes_list[pred_label]}\")\n    ax[0].axis('off')\n    \n    ax[1].imshow(saliency, cmap='hot')\n    ax[1].set_title(\"Saliency Map (Atenția Modelului)\")\n    ax[1].axis('off')\n    plt.show()\n\n# Apelăm funcția pe o imagine de validare nevăzută\nprint(\"\\n🔍 Analizăm la ce se uită ResNeXt100...\")\nvisualize_saliency(resnext_model, val_loader, full_dataset.classes)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:28:23.681639Z","iopub.execute_input":"2026-03-26T18:28:23.682471Z","iopub.status.idle":"2026-03-26T18:28:25.324526Z","shell.execute_reply.started":"2026-03-26T18:28:23.682413Z","shell.execute_reply":"2026-03-26T18:28:25.32368Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Vizualizare și Export\nUrmătoarea celulă va afișa graficele TensorBoard direct în acest notebook. \nA doua celulă va arhiva datele MLflow pentru a le putea descărca local.","metadata":{}},{"cell_type":"code","source":"# 1. Închidem orice TensorBoard blocat\n!pkill -f tensorboard\n\n# 2. Pornim TensorBoard silențios în fundal pe portul standard (6006)\nget_ipython().system_raw('tensorboard --logdir /kaggle/working/runs/dog_breeds --port 6006 &')\n\n# 3. Instalăm localtunnel (un utilitar care ne scoate portul pe internet)\n!npm install -g localtunnel\n\n# 4. Aflăm adresa IP publică a serverului Kaggle (o vei folosi ca \"parolă\")\nprint(\"👇 COPIAZĂ ACEST IP (Îl vei folosi ca parolă pe site-ul următor):\")\n!curl ipv4.icanhazip.com\n\n# 5. Creăm link-ul magic!\nprint(\"\\n👇 DĂ CLICK PE LINK-UL GENERAT MAI JOS:\")\n!npx localtunnel --port 6006","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-26T18:28:25.325866Z","iopub.execute_input":"2026-03-26T18:28:25.326143Z","execution_failed":"2026-03-27T19:51:38.981Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Arhivăm folderul mlruns pentru descărcare\nprint(\"📦 Arhivăm rezultatele MLflow...\")\nshutil.make_archive('/kaggle/working/mlflow_results', 'zip', '/kaggle/working/mlruns')\nprint(\"✅ Gata! Acum poți descărca 'mlflow_results.zip' din panoul din dreapta (secțiunea Output).\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-03-27T19:51:38.997Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}