{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":5048,"databundleVersionId":868335}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Homework 3 — Transfer Learning with ResNet18\n### Niki Orfanou | s21077","metadata":{}},{"cell_type":"code","source":"import os, random, warnings\nfrom tqdm.auto import tqdm\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom PIL import Image\nfrom sklearn.model_selection import GroupShuffleSplit, train_test_split\nfrom sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay\nimport torch\nfrom torch import nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms, models\n\nwarnings.filterwarnings(\"ignore\")\npd.set_option(\"display.max_columns\", 100)\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\nif torch.cuda.is_available():\n    print(\"GPU name:\", torch.cuda.get_device_name(0))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:14.290240Z","iopub.execute_input":"2026-05-13T14:00:14.290438Z","iopub.status.idle":"2026-05-13T14:00:25.475381Z","shell.execute_reply.started":"2026-05-13T14:00:14.290415Z","shell.execute_reply":"2026-05-13T14:00:25.474766Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\ndef seed_everything(seed=None):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nseed_everything(np.random.randint(1,10000))\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.477411Z","iopub.execute_input":"2026-05-13T14:00:25.478013Z","iopub.status.idle":"2026-05-13T14:00:25.487134Z","shell.execute_reply.started":"2026-05-13T14:00:25.477986Z","shell.execute_reply":"2026-05-13T14:00:25.486566Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\nROOT = \"/kaggle/input/competitions/state-farm-distracted-driver-detection\"\nTRAIN_DIR = \"/kaggle/input/competitions/state-farm-distracted-driver-detection/imgs/train\"\ncsv_path = ROOT +'/'+ \"driver_imgs_list.csv\"\ndf = pd.read_csv(csv_path)\n\nCLASS_NAMES = {\n    \"c0\": \"safe driving\",\n    \"c1\": \"texting - right\",\n    \"c2\": \"talking on the phone - right\",\n    \"c3\": \"texting - left\",\n    \"c4\": \"talking on the phone - left\",\n    \"c5\": \"operating the radio\",\n    \"c6\": \"drinking\",\n    \"c7\": \"reaching behind\",\n    \"c8\": \"hair and makeup\",\n    \"c9\": \"talking to passenger\",\n}\n\nCLASS_ORDER = [f\"c{i}\" for i in range(10)]\nNUM_CLASSES = len(CLASS_ORDER)\n\ndf[\"class_name\"] = df[\"classname\"].map(CLASS_NAMES)\ndf[\"path\"] = df.apply(lambda row: str(TRAIN_DIR +'/'+ row[\"classname\"] +'/'+ row[\"img\"]), axis=1)\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\n\nprint(\"Dataset shape:\", df.shape)\nprint(\"Unique drivers:\", df[\"subject\"].nunique())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.488058Z","iopub.execute_input":"2026-05-13T14:00:25.488411Z","iopub.status.idle":"2026-05-13T14:00:25.683158Z","shell.execute_reply.started":"2026-05-13T14:00:25.488378Z","shell.execute_reply":"2026-05-13T14:00:25.682290Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\nTEST_SUBJECTS = [\"p014\", \"p021\", \"p061\", \"p082\", \"p050\", \"p051\", \"p072\"]\n\ntest_df = df[df[\"subject\"].isin(TEST_SUBJECTS)].reset_index(drop=True)\ntrain_valid_df = df[~df[\"subject\"].isin(TEST_SUBJECTS)].reset_index(drop=True)\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\n\nprint(f\"Test set   : {len(test_df):,} images | {test_df['subject'].nunique()} drivers\")\nprint(f\"Train+Val  : {len(train_valid_df):,} images | {train_valid_df['subject'].nunique()} drivers\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.684122Z","iopub.execute_input":"2026-05-13T14:00:25.684316Z","iopub.status.idle":"2026-05-13T14:00:25.705074Z","shell.execute_reply.started":"2026-05-13T14:00:25.684298Z","shell.execute_reply":"2026-05-13T14:00:25.704376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Group split by driver to avoid data leakage\ngss = GroupShuffleSplit(n_splits=1, test_size=0.20, random_state=42)\ntr_idx, va_idx = next(gss.split(train_valid_df, groups=train_valid_df[\"subject\"]))\n\ntrain_df = train_valid_df.iloc[tr_idx].reset_index(drop=True)\nvalid_df = train_valid_df.iloc[va_idx].reset_index(drop=True)\n\nprint(f\"Train: {len(train_df):,} images | {train_df['subject'].nunique()} drivers\")\nprint(f\"Valid: {len(valid_df):,} images | {valid_df['subject'].nunique()} drivers\")\nprint(f\"Driver overlap: {set(train_df['subject']) & set(valid_df['subject'])}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.706013Z","iopub.execute_input":"2026-05-13T14:00:25.706375Z","iopub.status.idle":"2026-05-13T14:00:25.726203Z","shell.execute_reply.started":"2026-05-13T14:00:25.706352Z","shell.execute_reply":"2026-05-13T14:00:25.725369Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMG_SIZE = 224\nIMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\n\n# Strong augmentation for training\ntrain_transform = transforms.Compose([\n    transforms.Resize((256, 256)),\n    transforms.RandomCrop(IMG_SIZE),\n    transforms.RandomHorizontalFlip(p=0.3),\n    transforms.RandomRotation(degrees=10),\n    transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05),\n    transforms.RandomGrayscale(p=0.05),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n])\n\n# No augmentation for validation/test\neval_transform = transforms.Compose([\n    transforms.Resize((IMG_SIZE, IMG_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n])\n\ntest_transform = eval_transform\nprint(\"Transforms ready.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.727047Z","iopub.execute_input":"2026-05-13T14:00:25.727318Z","iopub.status.idle":"2026-05-13T14:00:25.897366Z","shell.execute_reply.started":"2026-05-13T14:00:25.727286Z","shell.execute_reply":"2026-05-13T14:00:25.896592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def sample_per_class(dataframe, max_per_class, seed=42):\n    parts = []\n    for c in CLASS_ORDER:\n        sub = dataframe[dataframe[\"classname\"] == c]\n        n = min(max_per_class, len(sub))\n        if n > 0:\n            parts.append(sub.sample(n=n, random_state=seed))\n    return pd.concat(parts).sample(frac=1, random_state=seed).reset_index(drop=True)\n\n# Sample up to 800 images per class for training, 200 for validation\ntrain_sampled = sample_per_class(train_df, max_per_class=1000)\nvalid_sampled  = sample_per_class(valid_df, max_per_class=200)\n\nprint(f\"Train sampled: {len(train_sampled):,} images\")\nprint(f\"Valid sampled: {len(valid_sampled):,} images\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.899653Z","iopub.execute_input":"2026-05-13T14:00:25.899863Z","iopub.status.idle":"2026-05-13T14:00:25.947925Z","shell.execute_reply.started":"2026-05-13T14:00:25.899843Z","shell.execute_reply":"2026-05-13T14:00:25.947360Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_to_idx = {class_id: i for i, class_id in enumerate(CLASS_ORDER)}\nidx_to_class = {i: class_id for class_id, i in class_to_idx.items()}\n\nclass DriverImageDataset(torch.utils.data.Dataset):\n    def __init__(self, dataframe, transform=None):\n        self.dataframe = dataframe.reset_index(drop=True)\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.dataframe)\n\n    def __getitem__(self, idx):\n        row   = self.dataframe.iloc[idx]\n        image = Image.open(row[\"path\"]).convert(\"RGB\")\n        if self.transform is not None:\n            image = self.transform(image)\n        label = class_to_idx[row[\"classname\"]]\n        return image, label\n\n# Create datasets\ntrain_dataset = DriverImageDataset(train_sampled, transform=train_transform)\nvalid_dataset = DriverImageDataset(valid_sampled, transform=eval_transform)\ntest_dataset  = DriverImageDataset(test_df,       transform=test_transform)\n\nprint(\"Train dataset:\", len(train_dataset))\nprint(\"Valid dataset:\", len(valid_dataset))\nprint(\"Test dataset :\", len(test_dataset))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.948812Z","iopub.execute_input":"2026-05-13T14:00:25.949397Z","iopub.status.idle":"2026-05-13T14:00:25.956681Z","shell.execute_reply.started":"2026-05-13T14:00:25.949373Z","shell.execute_reply":"2026-05-13T14:00:25.955986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BATCH_SIZE = 8\n\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,  num_workers=0, pin_memory=True)\nvalid_loader = DataLoader(valid_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=True)\ntest_loader  = DataLoader(test_dataset,  batch_size=BATCH_SIZE, shuffle=False, num_workers=0, pin_memory=True)\n\nimages, labels = next(iter(train_loader))\nprint(\"Batch image tensor shape:\", images.shape)\nprint(\"Batch label tensor shape:\", labels.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:25.957919Z","iopub.execute_input":"2026-05-13T14:00:25.958199Z","iopub.status.idle":"2026-05-13T14:00:26.848176Z","shell.execute_reply.started":"2026-05-13T14:00:25.958179Z","shell.execute_reply":"2026-05-13T14:00:26.847391Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def create_resnet18_transfer_model(num_classes=10, freeze_features=True):\n    # ---------------------------- MANDATORY (don't change this) --------------------------------------->\n    weights = models.ResNet18_Weights.DEFAULT\n    model = models.resnet18(weights=weights)\n    print(\"Loaded ImageNet-pretrained ResNet18 weights.\")\n    # <---------------------------- MANDATORY (don't change this) --------------------------------------\n\n    if freeze_features:\n        for param in model.parameters():\n            param.requires_grad = False\n\n    # Improved classifier head\n    in_features = model.fc.in_features\n    model.fc = nn.Sequential(\n        nn.Dropout(p=0.4),\n        nn.Linear(in_features, 256),\n        nn.BatchNorm1d(256),\n        nn.ReLU(),\n        nn.Dropout(p=0.3),\n        nn.Linear(256, num_classes),\n    )\n    return model\n\ndef count_trainable_parameters(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\ndef count_total_parameters(model):\n    return sum(p.numel() for p in model.parameters())\n\nmodel = create_resnet18_transfer_model(num_classes=NUM_CLASSES, freeze_features=True).to(device)\n\nprint(f\"Total parameters    : {count_total_parameters(model):,}\")\nprint(f\"Trainable parameters: {count_trainable_parameters(model):,}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:26.849167Z","iopub.execute_input":"2026-05-13T14:00:26.849580Z","iopub.status.idle":"2026-05-13T14:00:27.428217Z","shell.execute_reply.started":"2026-05-13T14:00:26.849553Z","shell.execute_reply":"2026-05-13T14:00:27.427587Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, dataloader, loss_fn, optimizer, device, freeze_frozen_bn=True):\n    model.train()\n    total_loss = total_correct = total_examples = 0\n    progress_bar = tqdm(dataloader, desc=\"Training\", leave=False)\n    for images, labels in progress_bar:\n        images = images.to(device, non_blocking=True)\n        labels = labels.to(device, non_blocking=True)\n        outputs = model(images)\n        loss    = loss_fn(outputs, labels)\n        optimizer.zero_grad(set_to_none=True)\n        loss.backward()\n        optimizer.step()\n        batch_size      = labels.size(0)\n        total_loss     += loss.item() * batch_size\n        total_correct  += (outputs.argmax(dim=1) == labels).sum().item()\n        total_examples += batch_size\n    return total_loss / total_examples, total_correct / total_examples\n\n# ---------------------------- MANDATORY (don't change this) --------------------------------------->\ndef evaluate(model, dataloader, loss_fn, device):\n    model.eval()\n    total_loss = total_correct = total_examples = 0\n    all_predictions, all_labels, all_probabilities = [], [], []\n    progress_bar = tqdm(dataloader, desc=\"Evaluating\", leave=False)\n    with torch.no_grad():\n        for images, labels in progress_bar:\n            images = images.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n            outputs      = model(images)\n            loss         = loss_fn(outputs, labels)\n            probabilities = torch.softmax(outputs, dim=1)\n            predictions  = outputs.argmax(dim=1)\n            batch_size   = labels.size(0)\n            total_loss     += loss.item() * batch_size\n            total_correct  += (predictions == labels).sum().item()\n            total_examples += batch_size\n            all_predictions.append(predictions.cpu().numpy())\n            all_labels.append(labels.cpu().numpy())\n            all_probabilities.append(probabilities.cpu().numpy())\n    return (total_loss / total_examples,\n            total_correct / total_examples,\n            np.concatenate(all_predictions),\n            np.concatenate(all_labels),\n            np.concatenate(all_probabilities))\n# <---------------------------- MANDATORY (don't change this) --------------------------------------\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:27.429067Z","iopub.execute_input":"2026-05-13T14:00:27.429405Z","iopub.status.idle":"2026-05-13T14:00:27.438429Z","shell.execute_reply.started":"2026-05-13T14:00:27.429380Z","shell.execute_reply":"2026-05-13T14:00:27.437732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"loss_fn   = nn.CrossEntropyLoss(label_smoothing=0.1)\noptimizer = torch.optim.AdamW(\n    [p for p in model.parameters() if p.requires_grad],\n    lr=1e-3, weight_decay=1e-4\n)\nscheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=5)\n\nprint(\"=== Phase 1: Training head only ===\")\nPHASE1_EPOCHS = 8\nbest_val_acc = 0.0\n\nfor epoch in range(PHASE1_EPOCHS):\n    train_loss, train_acc = train_one_epoch(model, train_loader, loss_fn, optimizer, device)\n    valid_loss, valid_acc, _, _, _ = evaluate(model, valid_loader, loss_fn, device)\n    scheduler.step()\n    print(f\"Epoch {epoch+1}/{PHASE1_EPOCHS} | train_loss={train_loss:.4f} | train_acc={train_acc:.4f} | valid_loss={valid_loss:.4f} | valid_acc={valid_acc:.4f}\")\n    if valid_acc > best_val_acc:\n        best_val_acc = valid_acc\n        torch.save(model.state_dict(), \"best_model.pt\")\n\nprint(f\"\\nBest val acc (Phase 1): {best_val_acc:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:00:27.439180Z","iopub.execute_input":"2026-05-13T14:00:27.439428Z","iopub.status.idle":"2026-05-13T14:29:29.150087Z","shell.execute_reply.started":"2026-05-13T14:00:27.439408Z","shell.execute_reply":"2026-05-13T14:29:29.149441Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Unfreeze layer3 and layer4 of ResNet backbone\nfor name, param in model.named_parameters():\n    if any(layer in name for layer in [\"layer3\", \"layer4\", \"fc\"]):\n        param.requires_grad = True\n\nprint(f\"Trainable parameters after unfreeze: {count_trainable_parameters(model):,}\")\n\n# Lower LR for fine-tuning\noptimizer_ft = torch.optim.AdamW(\n    [p for p in model.parameters() if p.requires_grad],\n    lr=1e-4, weight_decay=1e-4\n)\nscheduler_ft = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer_ft, T_max=10)\n\nprint(\"\\n=== Phase 2: Fine-tuning layer3 + layer4 ===\")\nPHASE2_EPOCHS = 10\n\nfor epoch in range(PHASE2_EPOCHS):\n    train_loss, train_acc = train_one_epoch(model, train_loader, loss_fn, optimizer_ft, device)\n    valid_loss, valid_acc, _, _, _ = evaluate(model, valid_loader, loss_fn, device)\n    scheduler_ft.step()\n    print(f\"Epoch {epoch+1}/{PHASE2_EPOCHS} | train_loss={train_loss:.4f} | train_acc={train_acc:.4f} | valid_loss={valid_loss:.4f} | valid_acc={valid_acc:.4f}\")\n    if valid_acc > best_val_acc:\n        best_val_acc = valid_acc\n        torch.save(model.state_dict(), \"best_model.pt\")\n\nprint(f\"\\nBest val acc overall: {best_val_acc:.4f}\")\n\n# Load best model\nmodel.load_state_dict(torch.load(\"best_model.pt\"))\nprint(\"Loaded best model checkpoint.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:29:29.151017Z","iopub.execute_input":"2026-05-13T14:29:29.151347Z","iopub.status.idle":"2026-05-13T14:59:05.914756Z","shell.execute_reply.started":"2026-05-13T14:29:29.151323Z","shell.execute_reply":"2026-05-13T14:59:05.913941Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- MANDATORY (don't change this) --------------------------------------->\ntest_loss, test_acc, test_pred, test_true, test_probs = evaluate(\n    model,\n    test_loader,\n    loss_fn,\n    device\n)\n\nprint(f\"Test loss: {test_loss:.4f}\")\nprint(f\"Test accuracy: {test_acc:.4f}\")\n# <---------------------------- MANDATORY (don't change this) --------------------------------------","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T14:59:05.915960Z","iopub.execute_input":"2026-05-13T14:59:05.916312Z","iopub.status.idle":"2026-05-13T15:00:43.591928Z","shell.execute_reply.started":"2026-05-13T14:59:05.916286Z","shell.execute_reply":"2026-05-13T15:00:43.591236Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Classification report\nprint(classification_report(test_true, test_pred, target_names=CLASS_ORDER))\n\n# Confusion matrix\ncm = confusion_matrix(test_true, test_pred)\nfig, ax = plt.subplots(figsize=(10, 8))\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=CLASS_ORDER)\ndisp.plot(ax=ax, colorbar=False)\nplt.title(f\"Confusion Matrix — Test Accuracy: {test_acc:.4f}\")\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-13T15:00:43.592740Z","iopub.execute_input":"2026-05-13T15:00:43.592985Z","iopub.status.idle":"2026-05-13T15:00:43.902743Z","shell.execute_reply.started":"2026-05-13T15:00:43.592962Z","shell.execute_reply":"2026-05-13T15:00:43.902082Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"-group split by driver Για αποφυγή data leakage\n-Tow-phase training:frozen backbone, fine-tune layer3+layer4\n-Test accuracy:85,66%\n-c8,c9 Οι πιο δύσκολες κλάσεις οπτικά\n-οι υπόλοιπες κλάσεις έχουν f1-score 89%-99%","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}