{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom sklearn.model_selection import train_test_split\nimport warnings\nwarnings.filterwarnings('ignore')\n\n# ============================================================\n# CONFIG\n# ============================================================\nCFG = {\n    'data_dir': '/kaggle/input/competitions/nexar-collision-prediction',\n    'img_size': 224,\n    'num_frames': 16,\n    'fps': 30,\n    'batch_size': 4,\n    'num_workers': 2,\n    'seed': 42,\n}\n\n# ============================================================\n# LOAD DATA\n# ============================================================\ntrain_df = pd.read_csv(f\"{CFG['data_dir']}/train.csv\")\ntest_df = pd.read_csv(f\"{CFG['data_dir']}/test.csv\")\nsample_sub = pd.read_csv(f\"{CFG['data_dir']}/sample_submission.csv\")\n\nprint(f\"Train: {len(train_df)}, Test: {len(test_df)}\")\nprint(train_df.head())\n\n# ============================================================\n# BUILD PATHS WITH ZERO-PADDING\n# ============================================================\ntrain_dir = Path(CFG['data_dir']) / 'train'\ntest_dir = Path(CFG['data_dir']) / 'test'\n\ntrain_df['video_path'] = train_df['id'].apply(lambda x: str(train_dir / f\"{int(x):05d}.mp4\"))\ntest_df['video_path'] = test_df['id'].apply(lambda x: str(test_dir / f\"{int(x):05d}.mp4\"))\n\n# Verify\nmissing_train = train_df[~train_df['video_path'].apply(os.path.exists)]\nmissing_test = test_df[~test_df['video_path'].apply(os.path.exists)]\nprint(f\"\\nMissing train: {len(missing_train)}, Missing test: {len(missing_test)}\")\n\nif len(missing_train) > 0:\n    print(\"First 5 missing:\", missing_train['id'].head().tolist())\n    raise ValueError(\"Some training videos not found! Check zero-padding.\")\n\nprint(f\"\\nClass distribution:\\n{train_df['target'].value_counts()}\")\n\n# ============================================================\n# FRAME SAMPLING\n# ============================================================\ndef get_frame_indices(video_path, num_frames=16, fps=30,\n                      time_of_event=None, time_of_alert=None,\n                      is_positive=False):\n    cap = cv2.VideoCapture(str(video_path))\n    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    cap.release()\n\n    if total_frames <= 0:\n        return np.zeros(num_frames, dtype=int)\n\n    if is_positive and time_of_event is not None and not pd.isna(time_of_event):\n        event_frame = int(float(time_of_event) * fps)\n        if time_of_alert is not None and not pd.isna(time_of_alert):\n            alert_frame = int(float(time_of_alert) * fps)\n        else:\n            alert_frame = max(0, event_frame - int(2.0 * fps))\n\n        start_frame = max(0, alert_frame - int(2.0 * fps))\n        end_frame = min(total_frames - 1, event_frame)\n\n        available = end_frame - start_frame + 1\n        if available >= num_frames:\n            indices = np.linspace(start_frame, end_frame, num_frames, dtype=int)\n        else:\n            indices = np.arange(start_frame, end_frame + 1)\n            while len(indices) < num_frames:\n                indices = np.append(indices, indices[-1])\n            indices = indices[:num_frames]\n    else:\n        indices = np.linspace(0, total_frames - 1, num_frames, dtype=int)\n\n    return np.clip(indices, 0, max(0, total_frames - 1))\n\n# ============================================================\n# SEQUENTIAL FRAME LOADING (reliable, no seeking bugs)\n# ============================================================\ndef load_frames(video_path, indices, img_size=224):\n    \"\"\"\n    Reads video SEQUENTIALLY and picks out the frames we need.\n    This avoids cv2 seeking bugs that return identical frames.\n    \"\"\"\n    target_indices = set(int(i) for i in indices)\n    max_idx = max(target_indices) if target_indices else 0\n    frames = {}\n\n    cap = cv2.VideoCapture(str(video_path))\n    frame_idx = 0\n    while frame_idx <= max_idx:\n        ret, frame = cap.read()\n        if not ret:\n            break\n        if frame_idx in target_indices:\n            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n            frame = cv2.resize(frame, (img_size, img_size))\n            frames[frame_idx] = frame\n        frame_idx += 1\n    cap.release()\n\n    # Assemble in requested order\n    result = []\n    for idx in indices:\n        idx = int(idx)\n        if idx in frames:\n            result.append(frames[idx])\n        else:\n            # Fallback: duplicate last or black frame\n            result.append(result[-1].copy() if result else np.zeros((img_size, img_size, 3), dtype=np.uint8))\n\n    return np.array(result)\n\n# Test\nsample = train_df.iloc[1]  # Pick a positive sample\nis_pos = sample['target'] == 1\ntest_indices = get_frame_indices(\n    sample['video_path'], CFG['num_frames'], CFG['fps'],\n    time_of_event=sample['time_of_event'],\n    time_of_alert=sample['time_of_alert'],\n    is_positive=is_pos\n)\ntest_frames = load_frames(sample['video_path'], test_indices, CFG['img_size'])\nprint(f\"\\nSample frames shape: {test_frames.shape}\")\nprint(f\"Frame means: {[f.mean() for f in test_frames]}\")\nprint(\"Cell 1 complete!\" if test_frames.mean() > 0 else \"ERROR: All frames are black!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-07T08:48:38.723848Z","iopub.execute_input":"2026-08-07T08:48:38.724438Z","iopub.status.idle":"2026-08-07T08:48:49.645343Z","shell.execute_reply.started":"2026-08-07T08:48:38.724404Z","shell.execute_reply":"2026-08-07T08:48:49.644536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# Augmentations\ntrain_transform = A.Compose([\n    A.Resize(CFG['img_size'], CFG['img_size']),\n    A.HorizontalFlip(p=0.5),\n    A.ShiftScaleRotate(shift_limit=0.05, scale_limit=0.1, rotate_limit=10, p=0.4),\n    A.RandomBrightnessContrast(brightness_limit=0.2, contrast_limit=0.2, p=0.4),\n    A.HueSaturationValue(hue_shift_limit=10, sat_shift_limit=20, val_shift_limit=10, p=0.3),\n    A.GaussNoise(var_limit=(10, 50), p=0.2),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\nval_transform = A.Compose([\n    A.Resize(CFG['img_size'], CFG['img_size']),\n    A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n    ToTensorV2()\n])\n\nclass NexarVideoDataset(Dataset):\n    def __init__(self, df, transform=None, num_frames=16, fps=30, is_train=True):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.num_frames = num_frames\n        self.fps = fps\n        self.is_train = is_train\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        is_positive = (row['target'] == 1) if self.is_train else False\n\n        indices = get_frame_indices(\n            row['video_path'], self.num_frames, self.fps,\n            time_of_event=row.get('time_of_event'),\n            time_of_alert=row.get('time_of_alert'),\n            is_positive=is_positive\n        )\n\n        frames = load_frames(row['video_path'], indices, CFG['img_size'])\n        processed = []\n        for frame in frames:\n            if self.transform:\n                aug = self.transform(image=frame)\n                processed.append(aug['image'])\n            else:\n                tensor = torch.from_numpy(frame).permute(2, 0, 1).float() / 255.0\n                processed.append(tensor)\n\n        video = torch.stack(processed)  # (T, C, H, W)\n\n        if self.is_train:\n            label = torch.tensor(row['target'], dtype=torch.float32)\n            return video, label\n        else:\n            return video, row['id']\n\n# Split\ntrain_split, val_split = train_test_split(\n    train_df, test_size=0.15, random_state=CFG['seed'], stratify=train_df['target']\n)\nprint(f\"Train: {len(train_split)}, Val: {len(val_split)}\")\n\n# Datasets\ntrain_dataset = NexarVideoDataset(train_split, transform=train_transform, num_frames=CFG['num_frames'])\nval_dataset = NexarVideoDataset(val_split, transform=val_transform, num_frames=CFG['num_frames'])\n\n# Weighted sampler\npos_count = (train_split['target'] == 1).sum()\nneg_count = (train_split['target'] == 0).sum()\nprint(f\"Positives: {pos_count}, Negatives: {neg_count}\")\n\nclass_weights = {0: 1.0, 1: neg_count / max(pos_count, 1)}\nsample_weights = train_split['target'].map(class_weights).values\nsampler = WeightedRandomSampler(weights=sample_weights, num_samples=len(sample_weights), replacement=True)\n\ndef collate_fn(batch):\n    videos, labels = zip(*batch)\n    return torch.stack(videos), torch.stack(labels)\n\ntrain_loader = DataLoader(train_dataset, batch_size=CFG['batch_size'],\n                          sampler=sampler, num_workers=CFG['num_workers'],\n                          pin_memory=True, collate_fn=collate_fn)\nval_loader = DataLoader(val_dataset, batch_size=CFG['batch_size'],\n                        shuffle=False, num_workers=CFG['num_workers'],\n                        pin_memory=True, collate_fn=collate_fn)\n\n# Sanity check\nbatch_videos, batch_labels = next(iter(train_loader))\nprint(f\"Batch shape: {batch_videos.shape}, Labels: {batch_labels.shape}\")\nprint(f\"Label values: {batch_labels[:8].tolist()}\")\nprint(f\"Frame mean (should be >0): {batch_videos[0].mean():.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-07T08:49:06.108398Z","iopub.execute_input":"2026-08-07T08:49:06.109061Z","iopub.status.idle":"2026-08-07T08:49:24.436551Z","shell.execute_reply.started":"2026-08-07T08:49:06.109031Z","shell.execute_reply":"2026-08-07T08:49:24.435474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\nimport torchvision.models as models\n\nclass BaselineCollisionNet(nn.Module):\n    def __init__(self, backbone='resnet50', pretrained=True, dropout=0.5):\n        super().__init__()\n        if backbone == 'resnet50':\n            self.backbone = models.resnet50(weights='IMAGENET1K_V2' if pretrained else None)\n            feat_dim = 2048\n        elif backbone == 'resnet18':\n            self.backbone = models.resnet18(weights='IMAGENET1K_V1' if pretrained else None)\n            feat_dim = 512\n        self.backbone.fc = nn.Identity()\n\n        self.classifier = nn.Sequential(\n            nn.Linear(feat_dim, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(512, 128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout / 2),\n            nn.Linear(128, 1)\n        )\n\n    def forward(self, x):\n        B, T, C, H, W = x.shape\n        x = x.view(B * T, C, H, W)\n        features = self.backbone(x)\n        features = features.view(B, T, -1).mean(dim=1)\n        return self.classifier(features).squeeze(-1)\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Device: {device}\")\n\nbaseline_model = BaselineCollisionNet(pretrained=True).to(device)\n\n# Quick forward test\nwith torch.no_grad():\n    out = baseline_model(batch_videos.to(device))\n    print(f\"Output shape: {out.shape}\")\n    print(f\"Output values: {out[:8].tolist()}\")\n    print(f\"Output std: {out.std():.4f} (should be >0 if model sees different inputs)\")\n\nprint(f\"Params: {sum(p.numel() for p in baseline_model.parameters()):,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-07T08:53:31.783822Z","iopub.execute_input":"2026-08-07T08:53:31.784322Z","iopub.status.idle":"2026-08-07T08:53:32.519475Z","shell.execute_reply.started":"2026-08-07T08:53:31.784238Z","shell.execute_reply":"2026-08-07T08:53:32.518818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class TemporalCollisionNet(nn.Module):\n    def __init__(self, cnn_backbone='resnet50', lstm_hidden=512,\n                 lstm_layers=2, attention=True, dropout=0.5, pretrained=True):\n        super().__init__()\n\n        if cnn_backbone == 'resnet50':\n            self.backbone = models.resnet50(weights='IMAGENET1K_V2' if pretrained else None)\n            cnn_dim = 2048\n        elif cnn_backbone == 'resnet18':\n            self.backbone = models.resnet18(weights='IMAGENET1K_V1' if pretrained else None)\n            cnn_dim = 512\n        self.backbone.fc = nn.Identity()\n\n        self.lstm = nn.LSTM(\n            cnn_dim, lstm_hidden, lstm_layers,\n            batch_first=True, bidirectional=True,\n            dropout=dropout if lstm_layers > 1 else 0\n        )\n\n        lstm_out = lstm_hidden * 2\n        self.use_attention = attention\n        if attention:\n            self.attention = nn.Sequential(\n                nn.Linear(lstm_out, 256),\n                nn.Tanh(),\n                nn.Linear(256, 1),\n                nn.Softmax(dim=1)\n            )\n\n        self.classifier = nn.Sequential(\n            nn.Linear(lstm_out, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(512, 128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout / 2),\n            nn.Linear(128, 1)\n        )\n\n    def forward(self, x):\n        B, T, C, H, W = x.shape\n        x = x.view(B * T, C, H, W)\n        cnn_feats = self.backbone(x)\n        cnn_feats = cnn_feats.view(B, T, -1)\n\n        lstm_out, _ = self.lstm(cnn_feats)\n\n        if self.use_attention:\n            attn = self.attention(lstm_out)\n            feats = (lstm_out * attn).sum(dim=1)\n        else:\n            feats = lstm_out.mean(dim=1)\n\n        return self.classifier(feats).squeeze(-1)\n\ntemporal_model = TemporalCollisionNet(pretrained=True).to(device)\n\nwith torch.no_grad():\n    out = temporal_model(batch_videos.to(device))\n    print(f\"Output shape: {out.shape}\")\n    print(f\"Output values: {out[:8].tolist()}\")\n    print(f\"Output std: {out.std():.4f}\")\n\nprint(f\"Params: {sum(p.numel() for p in temporal_model.parameters()):,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-07T08:54:37.453828Z","iopub.execute_input":"2026-08-07T08:54:37.454439Z","iopub.status.idle":"2026-08-07T08:54:38.171916Z","shell.execute_reply.started":"2026-08-07T08:54:37.454410Z","shell.execute_reply":"2026-08-07T08:54:38.171324Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import (average_precision_score, roc_auc_score,\n                            f1_score, precision_score, recall_score,\n                            confusion_matrix)\nfrom tqdm import tqdm\nimport matplotlib.pyplot as plt\n\nclass Trainer:\n    def __init__(self, model, device, model_name='model'):\n        self.model = model\n        self.device = device\n        self.model_name = model_name\n        self.history = {'train_loss': [], 'val_loss': [], 'val_auc': [], 'val_ap': []}\n\n    def train_epoch(self, loader, optimizer, criterion):\n        self.model.train()\n        total_loss = 0\n        for videos, labels in tqdm(loader, desc=f\"Train [{self.model_name}]\"):\n            videos, labels = videos.to(self.device), labels.to(self.device)\n            optimizer.zero_grad()\n            outputs = self.model(videos)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(self.model.parameters(), 1.0)\n            optimizer.step()\n            total_loss += loss.item() * videos.size(0)\n        return total_loss / len(loader.dataset)\n\n    @torch.no_grad()\n    def evaluate(self, loader, criterion):\n        self.model.eval()\n        total_loss, all_probs, all_labels = 0, [], []\n        for videos, labels in tqdm(loader, desc=f\"Eval [{self.model_name}]\"):\n            videos, labels = videos.to(self.device), labels.to(self.device)\n            outputs = self.model(videos)\n            loss = criterion(outputs, labels)\n            total_loss += loss.item() * videos.size(0)\n            probs = torch.sigmoid(outputs)\n            all_probs.extend(probs.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n\n        all_probs = np.array(all_probs)\n        all_labels = np.array(all_labels)\n\n        metrics = {\n            'loss': total_loss / len(loader.dataset),\n            'auc': roc_auc_score(all_labels, all_probs),\n            'ap': average_precision_score(all_labels, all_probs),\n            'f1': f1_score(all_labels, all_probs > 0.5, zero_division=0),\n            'precision': precision_score(all_labels, all_probs > 0.5, zero_division=0),\n            'recall': recall_score(all_labels, all_probs > 0.5, zero_division=0),\n            'cm': confusion_matrix(all_labels, all_probs > 0.5)\n        }\n        return metrics, all_probs, all_labels\n\n    def fit(self, train_loader, val_loader, epochs=5, lr=1e-3, weight_decay=1e-4):\n        # Use different LR for backbone vs head\n        backbone_params = []\n        head_params = []\n        for name, param in self.model.named_parameters():\n            if 'backbone' in name:\n                backbone_params.append(param)\n            else:\n                head_params.append(param)\n\n        optimizer = torch.optim.AdamW([\n            {'params': backbone_params, 'lr': lr * 0.1},  # 10x lower for pretrained backbone\n            {'params': head_params, 'lr': lr}\n        ], weight_decay=weight_decay)\n\n        pos_count = (train_loader.dataset.df['target'] == 1).sum()\n        neg_count = (train_loader.dataset.df['target'] == 0).sum()\n        pos_weight = torch.tensor([neg_count / max(pos_count, 1)]).to(self.device)\n        criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight)\n\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='max', factor=0.5, patience=2)\n\n        best_auc = 0\n        for epoch in range(epochs):\n            print(f\"\\n{'='*50}\")\n            print(f\"Epoch {epoch+1}/{epochs} | {self.model_name}\")\n            print(f\"{'='*50}\")\n\n            train_loss = self.train_epoch(train_loader, optimizer, criterion)\n            val_metrics, _, _ = self.evaluate(val_loader, criterion)\n\n            self.history['train_loss'].append(train_loss)\n            self.history['val_loss'].append(val_metrics['loss'])\n            self.history['val_auc'].append(val_metrics['auc'])\n            self.history['val_ap'].append(val_metrics['ap'])\n\n            print(f\"Train Loss: {train_loss:.4f}\")\n            print(f\"Val Loss: {val_metrics['loss']:.4f} | AUC: {val_metrics['auc']:.4f} | AP: {val_metrics['ap']:.4f}\")\n            print(f\"F1: {val_metrics['f1']:.4f} | Precision: {val_metrics['precision']:.4f} | Recall: {val_metrics['recall']:.4f}\")\n            print(f\"CM:\\n{val_metrics['cm']}\")\n\n            scheduler.step(val_metrics['auc'])\n            if val_metrics['auc'] > best_auc:\n                best_auc = val_metrics['auc']\n                torch.save({'model_state_dict': self.model.state_dict(), 'auc': best_auc},\n                          f\"{self.model_name}_best.pth\")\n                print(f\"Saved best (AUC: {best_auc:.4f})\")\n\n        return self.history\n\n# Train baseline (quick test: 3 epochs)\nprint(\"TRAINING BASELINE...\")\nbaseline_trainer = Trainer(baseline_model, device, 'baseline')\nbaseline_hist = baseline_trainer.fit(train_loader, val_loader, epochs=3, lr=1e-3)\n\n# Train temporal\nprint(\"\\nTRAINING TEMPORAL...\")\ntemporal_trainer = Trainer(temporal_model, device, 'temporal')\ntemporal_hist = temporal_trainer.fit(train_loader, val_loader, epochs=5, lr=1e-3)\n\n# Plot\nfig, axes = plt.subplots(1, 3, figsize=(18, 5))\nfor i, (key, title) in enumerate([('train_loss','Train Loss'), ('val_loss','Val Loss'), ('val_auc','Val AUC')]):\n    axes[i].plot(baseline_hist[key], 'b-o', label='Baseline')\n    axes[i].plot(temporal_hist[key], 'r-o', label='Temporal')\n    axes[i].set_title(title)\n    axes[i].legend()\n    axes[i].grid(True, alpha=0.3)\nplt.tight_layout()\nplt.savefig('comparison.png', dpi=150)\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}