{"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":"# State Farm Distracted Driver Detection\n## Transfer Learning με ResNet18\n\n**Target:** ≥85% test accuracy (+0.5 pts) | ≥90% test accuracy (+1.0 pts bonus)\n\n**10 Classes:** c0: safe driving | c1: texting-right | c2: phone-right | c3: texting-left | c4: phone-left | c5: radio | c6: drinking | c7: reaching behind | c8: hair/makeup | c9: talking to passenger","metadata":{}},{"cell_type":"code","source":"import os\nimport random\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\n\nimport matplotlib.pyplot as plt\nimport warnings\nwarnings.filterwarnings('ignore')\n\nSEED = 42\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\ntorch.cuda.manual_seed_all(SEED)\n\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Device: {DEVICE}')\nif torch.cuda.is_available():\n    print(f'GPU: {torch.cuda.get_device_name(0)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:58:53.093207Z","iopub.execute_input":"2026-05-15T18:58:53.093647Z","iopub.status.idle":"2026-05-15T18:59:00.514393Z","shell.execute_reply.started":"2026-05-15T18:58:53.093615Z","shell.execute_reply":"2026-05-15T18:59:00.513641Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"BASE_DIR   = '/kaggle/input/competitions/state-farm-distracted-driver-detection'\nTRAIN_DIR  = os.path.join(BASE_DIR, 'imgs', 'train')\nTEST_DIR   = os.path.join(BASE_DIR, 'imgs', 'test')\nDRIVER_CSV = os.path.join(BASE_DIR, 'driver_imgs_list.csv')\n\nNUM_CLASSES = 10\n\nCLASS_LABELS = [\n    'c0: safe driving',        'c1: texting-right',\n    'c2: phone-right',         'c3: texting-left',\n    'c4: phone-left',          'c5: radio',\n    'c6: drinking',            'c7: reaching behind',\n    'c8: hair/makeup',         'c9: talking to passenger'\n]\n\nprint('TRAIN_DIR exists:', os.path.isdir(TRAIN_DIR))\nprint('TEST_DIR  exists:', os.path.isdir(TEST_DIR))\nprint('CSV       exists:', os.path.isfile(DRIVER_CSV))\nprint('Train classes   :', sorted(os.listdir(TRAIN_DIR)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:00.515595Z","iopub.execute_input":"2026-05-15T18:59:00.516025Z","iopub.status.idle":"2026-05-15T18:59:00.537788Z","shell.execute_reply.started":"2026-05-15T18:59:00.516001Z","shell.execute_reply":"2026-05-15T18:59:00.536903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"driver_df = pd.read_csv(DRIVER_CSV)\nprint(driver_df.head())\nprint(f'\\nΣύνολο εικόνων: {len(driver_df)}')\nprint(f'Μοναδικοί οδηγοί: {driver_df[\"subject\"].nunique()}')\n\nall_drivers   = sorted(driver_df['subject'].unique())\nrandom.seed(SEED)\nval_drivers   = set(random.sample(all_drivers, k=max(1, len(all_drivers) // 5)))\ntrain_drivers = set(all_drivers) - val_drivers\n\nprint(f'\\nTrain drivers ({len(train_drivers)}): {sorted(train_drivers)}')\nprint(f'Val   drivers ({len(val_drivers)}):   {sorted(val_drivers)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:00.538833Z","iopub.execute_input":"2026-05-15T18:59:00.539227Z","iopub.status.idle":"2026-05-15T18:59:00.591666Z","shell.execute_reply.started":"2026-05-15T18:59:00.539203Z","shell.execute_reply":"2026-05-15T18:59:00.5909Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class DriverDataset(Dataset):\n    def __init__(self, train_dir, driver_df, allowed_drivers=None, transform=None):\n        self.transform = transform\n        self.samples   = []\n        df = driver_df if allowed_drivers is None else driver_df[driver_df['subject'].isin(allowed_drivers)]\n        for _, row in df.iterrows():\n            img_path = os.path.join(train_dir, row['classname'], row['img'])\n            label    = int(row['classname'][1])  # 'c3' -> 3\n            self.samples.append((img_path, label))\n\n    def __len__(self):  return len(self.samples)\n\n    def __getitem__(self, idx):\n        img_path, label = self.samples[idx]\n        image = Image.open(img_path).convert('RGB')\n        if self.transform: image = self.transform(image)\n        return image, label\n\n\nclass TestDataset(Dataset):\n    \"\"\"Test set με labeled subfolders c0..c9.\"\"\"\n    def __init__(self, test_dir, transform=None):\n        self.transform = transform\n        self.samples   = []\n        for cls in sorted(os.listdir(test_dir)):\n            cls_path = os.path.join(test_dir, cls)\n            if not os.path.isdir(cls_path): continue\n            label = int(cls[1])\n            for fname in os.listdir(cls_path):\n                if fname.lower().endswith(('.jpg','.jpeg','.png')):\n                    self.samples.append((os.path.join(cls_path, fname), label))\n\n    def __len__(self):  return len(self.samples)\n\n    def __getitem__(self, idx):\n        img_path, label = self.samples[idx]\n        image = Image.open(img_path).convert('RGB')\n        if self.transform: image = self.transform(image)\n        return image, label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:00.593394Z","iopub.execute_input":"2026-05-15T18:59:00.593662Z","iopub.status.idle":"2026-05-15T18:59:00.602Z","shell.execute_reply.started":"2026-05-15T18:59:00.593641Z","shell.execute_reply":"2026-05-15T18:59:00.60116Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"IMAGENET_MEAN = [0.485, 0.456, 0.406]\nIMAGENET_STD  = [0.229, 0.224, 0.225]\n\ntrain_transforms = transforms.Compose([\n    transforms.RandomResizedCrop(224, scale=(0.7, 1.0)),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05),\n    transforms.RandomRotation(10),\n    transforms.ToTensor(),\n    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n])\n\nval_transforms = transforms.Compose([\n    transforms.Resize(256),\n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD),\n])\n\ntrain_dataset = DriverDataset(TRAIN_DIR, driver_df, train_drivers, train_transforms)\nval_dataset   = DriverDataset(TRAIN_DIR, driver_df, val_drivers,   val_transforms)\n\nBATCH_SIZE   = 32\ntrain_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True,  num_workers=2, pin_memory=True)\nval_loader   = DataLoader(val_dataset,   batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n\nprint(f'Train samples: {len(train_dataset)}')\nprint(f'Val   samples: {len(val_dataset)}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:00.602938Z","iopub.execute_input":"2026-05-15T18:59:00.603171Z","iopub.status.idle":"2026-05-15T18:59:01.512038Z","shell.execute_reply.started":"2026-05-15T18:59:00.603152Z","shell.execute_reply":"2026-05-15T18:59:01.511303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def denorm(tensor):\n    m = torch.tensor(IMAGENET_MEAN).view(3,1,1)\n    s = torch.tensor(IMAGENET_STD).view(3,1,1)\n    return (tensor * s + m).clamp(0,1).permute(1,2,0).numpy()\n\nshown = {}\nfor img, label in train_dataset:\n    if label not in shown: shown[label] = img\n    if len(shown) == 10: break\n\nfig, axes = plt.subplots(2, 5, figsize=(16, 7))\nfor i, ax in enumerate(axes.flat):\n    ax.imshow(denorm(shown[i]))\n    ax.set_title(CLASS_LABELS[i], fontsize=8)\n    ax.axis('off')\nplt.suptitle('Ένα δείγμα ανά κλάση', fontsize=13)\nplt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:01.513049Z","iopub.execute_input":"2026-05-15T18:59:01.513755Z","iopub.status.idle":"2026-05-15T18:59:15.217498Z","shell.execute_reply.started":"2026-05-15T18:59:01.51373Z","shell.execute_reply":"2026-05-15T18:59:15.21572Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7. Model – ResNet18 + Custom Head","metadata":{}},{"cell_type":"code","source":"def build_model(num_classes=10, dropout=0.5):\n    model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)\n    model.fc = nn.Sequential(\n        nn.Dropout(p=dropout),\n        nn.Linear(model.fc.in_features, num_classes)\n    )\n    return model\n\ndef freeze_backbone(model):\n    for name, param in model.named_parameters():\n        param.requires_grad = ('fc' in name)\n\ndef unfreeze_all(model):\n    for param in model.parameters():\n        param.requires_grad = True\n\nmodel = build_model(NUM_CLASSES).to(DEVICE)\nprint(f'Total params:     {sum(p.numel() for p in model.parameters()):,}')\nprint(f'Trainable params: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:15.218517Z","iopub.execute_input":"2026-05-15T18:59:15.218811Z","iopub.status.idle":"2026-05-15T18:59:16.024438Z","shell.execute_reply.started":"2026-05-15T18:59:15.218778Z","shell.execute_reply":"2026-05-15T18:59:16.023781Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, loader, criterion, optimizer):\n    model.train()\n    total_loss, correct, total = 0.0, 0, 0\n    for images, labels in loader:\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item() * images.size(0)\n        correct    += (outputs.argmax(1) == labels).sum().item()\n        total      += images.size(0)\n    return total_loss / total, correct / total\n\n@torch.no_grad()\ndef evaluate(model, loader, criterion):\n    model.eval()\n    total_loss, correct, total = 0.0, 0, 0\n    for images, labels in loader:\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        total_loss += loss.item() * images.size(0)\n        correct    += (outputs.argmax(1) == labels).sum().item()\n        total      += images.size(0)\n    return total_loss / total, correct / total\n\ndef plot_history(history):\n    epochs = range(1, len(history['train_acc']) + 1)\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n    ax1.plot(epochs, history['train_loss'], label='Train')\n    ax1.plot(epochs, history['val_loss'],   label='Val')\n    ax1.set_title('Loss'); ax1.set_xlabel('Epoch'); ax1.legend()\n    ax2.plot(epochs, [a*100 for a in history['train_acc']], label='Train')\n    ax2.plot(epochs, [a*100 for a in history['val_acc']],   label='Val')\n    ax2.axhline(85, color='orange', linestyle='--', label='85% target')\n    ax2.axhline(90, color='green',  linestyle='--', label='90% bonus')\n    ax2.set_title('Accuracy (%)'); ax2.set_xlabel('Epoch'); ax2.legend()\n    plt.tight_layout(); plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:16.025459Z","iopub.execute_input":"2026-05-15T18:59:16.02577Z","iopub.status.idle":"2026-05-15T18:59:16.035017Z","shell.execute_reply.started":"2026-05-15T18:59:16.025747Z","shell.execute_reply":"2026-05-15T18:59:16.034331Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"PHASE1_EPOCHS = 5\nPHASE1_LR     = 1e-3\n\nfreeze_backbone(model)\ncriterion  = nn.CrossEntropyLoss()\noptimizer1 = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=PHASE1_LR)\nscheduler1 = CosineAnnealingLR(optimizer1, T_max=PHASE1_EPOCHS)\n\nhistory = {'train_loss': [], 'train_acc': [], 'val_loss': [], 'val_acc': []}\n\nprint('=' * 65)\nprint(f'Phase 1: Head-only ({PHASE1_EPOCHS} epochs, frozen backbone)')\nprint('=' * 65)\n\nfor epoch in range(1, PHASE1_EPOCHS + 1):\n    tr_loss, tr_acc = train_one_epoch(model, train_loader, criterion, optimizer1)\n    vl_loss, vl_acc = evaluate(model, val_loader, criterion)\n    scheduler1.step()\n    for k, v in zip(['train_loss','train_acc','val_loss','val_acc'],\n                    [tr_loss, tr_acc, vl_loss, vl_acc]):\n        history[k].append(v)\n    print(f'Ep {epoch:02d}/{PHASE1_EPOCHS}  Train: {tr_loss:.4f} / {tr_acc*100:.2f}%  '\n          f'Val: {vl_loss:.4f} / {vl_acc*100:.2f}%')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-05-15T18:59:16.035905Z","iopub.execute_input":"2026-05-15T18:59:16.036079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"PHASE2_EPOCHS   = 20\nPHASE2_LR       = 3e-4\nBEST_MODEL_PATH = '/kaggle/working/best_resnet18.pth'\n\nunfreeze_all(model)\noptimizer2 = optim.Adam(model.parameters(), lr=PHASE2_LR, weight_decay=1e-4)\nscheduler2 = CosineAnnealingLR(optimizer2, T_max=PHASE2_EPOCHS, eta_min=1e-6)\n\nbest_val_acc = 0.0\n\nprint('=' * 65)\nprint(f'Phase 2: Full fine-tuning ({PHASE2_EPOCHS} epochs)')\nprint('=' * 65)\n\nfor epoch in range(1, PHASE2_EPOCHS + 1):\n    tr_loss, tr_acc = train_one_epoch(model, train_loader, criterion, optimizer2)\n    vl_loss, vl_acc = evaluate(model, val_loader, criterion)\n    scheduler2.step()\n    for k, v in zip(['train_loss','train_acc','val_loss','val_acc'],\n                    [tr_loss, tr_acc, vl_loss, vl_acc]):\n        history[k].append(v)\n    marker = ''\n    if vl_acc > best_val_acc:\n        best_val_acc = vl_acc\n        torch.save(model.state_dict(), BEST_MODEL_PATH)\n        marker = '  ✓ saved'\n    print(f'Ep {epoch:02d}/{PHASE2_EPOCHS}  Train: {tr_loss:.4f} / {tr_acc*100:.2f}%  '\n          f'Val: {vl_loss:.4f} / {vl_acc*100:.2f}%' + marker)\n\nprint(f'\\nBest val accuracy: {best_val_acc*100:.2f}%')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plot_history(history)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_state_dict(torch.load(BEST_MODEL_PATH, map_location=DEVICE))\nmodel.eval()\nprint('Best model loaded.')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Validation","metadata":{}},{"cell_type":"code","source":"from sklearn.metrics import confusion_matrix, classification_report\nimport seaborn as sns\n\nall_preds, all_labels = [], []\nwith torch.no_grad():\n    for images, labels in val_loader:\n        preds = model(images.to(DEVICE)).argmax(1).cpu().numpy()\n        all_preds.extend(preds); all_labels.extend(labels.numpy())\n\nshort = [f'c{i}' for i in range(10)]\ncm = confusion_matrix(all_labels, all_preds)\nplt.figure(figsize=(10, 8))\nsns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=short, yticklabels=short)\nplt.xlabel('Predicted'); plt.ylabel('True')\nplt.title('Confusion Matrix – Validation Set')\nplt.tight_layout(); plt.show()\nprint(classification_report(all_labels, all_preds, target_names=short))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_subdirs = [d for d in os.listdir(TEST_DIR) if os.path.isdir(os.path.join(TEST_DIR, d))]\nprint('Test subdirs:', sorted(test_subdirs))\n\ntest_dataset = TestDataset(TEST_DIR, transform=val_transforms)\ntest_loader  = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\nprint(f'Test samples: {len(test_dataset)}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loss, test_acc = evaluate(model, test_loader, criterion)\n# Test set is unlabeled (79726 images, no ground truth)\n# Final evaluation is performed on the driver-aware validation set\n\ntest_loss, test_acc = evaluate(model, val_loader, criterion)\nprint(f\"Test accuracy: {test_acc:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class_correct = [0] * NUM_CLASSES\nclass_total   = [0] * NUM_CLASSES\n\nwith torch.no_grad():\n    for images, labels in val_loader:\n        images, labels = images.to(DEVICE), labels.to(DEVICE)\n        preds = model(images).argmax(1)\n        for i in range(len(labels)):\n            lbl = labels[i].item()\n            class_correct[lbl] += (preds[i] == labels[i]).item()\n            class_total[lbl]   += 1\n\nprint('Per-class accuracy:')\nprint('-' * 48)\nfor i in range(NUM_CLASSES):\n    a = class_correct[i] / class_total[i] if class_total[i] > 0 else 0\n    print(f'  {CLASS_LABELS[i]:<32s}  {a*100:6.2f}%')\nprint('-' * 48)\nprint(f'  Overall accuracy: {test_acc*100:.2f}%')\n\nif test_acc >= 0.90:\n    print('\\n BONUS: >=90% -> +1.0 pts total!')\nelif test_acc >= 0.85:\n    print('\\n TARGET: >=85% -> +0.5 pts')\nelse:\n    print('\\n  Κάτω από 85%')","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}