{"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":"!pip install -q decord pytorchvideo","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:04:01.362076Z","iopub.execute_input":"2026-09-19T08:04:01.362587Z","iopub.status.idle":"2026-09-19T08:04:16.96807Z","shell.execute_reply.started":"2026-09-19T08:04:01.362548Z","shell.execute_reply":"2026-09-19T08:04:16.96721Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nprint(\"Python:\", sys.version)\n\nimport torch\nprint(\"PyTorch:\", torch.__version__)\nprint(\"CUDA available:\", torch.cuda.is_available())\n\nimport numpy as np\nprint(\"NumPy:\", np.__version__)\n\nimport pandas as pd\nprint(\"Pandas:\", pd.__version__)\n\nimport decord\nprint(\"decord OK\")\n\nimport pytorchvideo\nprint(\"pytorchvideo OK\")\n\nfrom sklearn import __version__ as sk_ver\nprint(\"sklearn:\", sk_ver)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:05:04.3574Z","iopub.execute_input":"2026-09-19T08:05:04.357744Z","iopub.status.idle":"2026-09-19T08:05:08.95304Z","shell.execute_reply.started":"2026-09-19T08:05:04.357704Z","shell.execute_reply":"2026-09-19T08:05:08.952211Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# Thử cả 2 path\npath1 = \"/kaggle/input/nexar-collision-prediction\"\npath2 = \"/kaggle/input/competitions/nexar-collision-prediction\"\n\nfor p in [path1, path2]:\n    if os.path.exists(p):\n        print(f\"✅ {p}\")\n        print(\"  Contents:\", os.listdir(p))\n    else:\n        print(f\"❌ {p} — không tồn tại\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:05:38.017073Z","iopub.execute_input":"2026-09-19T08:05:38.017633Z","iopub.status.idle":"2026-09-19T08:05:38.025519Z","shell.execute_reply.started":"2026-09-19T08:05:38.017601Z","shell.execute_reply":"2026-09-19T08:05:38.024735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ndata_root = \"/kaggle/input/competitions/nexar-collision-prediction\"\ndf = pd.read_csv(f\"{data_root}/train.csv\")\n\nprint(f\"Shape: {df.shape}\")\nprint(f\"\\nColumns: {list(df.columns)}\")\nprint(f\"\\nTarget distribution:\\n{df['target'].value_counts()}\")\nprint(f\"\\nFirst 10 rows:\")\ndf.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:06:31.237252Z","iopub.execute_input":"2026-09-19T08:06:31.23765Z","iopub.status.idle":"2026-09-19T08:06:31.302681Z","shell.execute_reply.started":"2026-09-19T08:06:31.237614Z","shell.execute_reply":"2026-09-19T08:06:31.301837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\ndata_root = \"/kaggle/input/competitions/nexar-collision-prediction\"\ntrain_dir = f\"{data_root}/train\"\n\n# Đếm videos\nvideos = sorted(os.listdir(train_dir))\nprint(f\"Total train videos: {len(videos)}\")\nprint(f\"First 5: {videos[:5]}\")\nprint(f\"Last 5: {videos[-5:]}\")\n\n# Xem format tên file\nprint(f\"\\nFilename format: {videos[0]}\")\n\n# Check 1 video positive\npos_id = 822  # từ train.csv, target=1\nfor v in videos:\n    if str(pos_id) in v:\n        print(f\"\\nFound positive video: {v}\")\n        print(f\"  Size: {os.path.getsize(f'{train_dir}/{v}') / 1024 / 1024:.1f} MB\")\n        break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:07:20.816765Z","iopub.execute_input":"2026-09-19T08:07:20.817096Z","iopub.status.idle":"2026-09-19T08:07:20.854301Z","shell.execute_reply.started":"2026-09-19T08:07:20.817067Z","shell.execute_reply":"2026-09-19T08:07:20.853376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from decord import VideoReader, cpu\n\ndata_root = \"/kaggle/input/competitions/nexar-collision-prediction\"\nvr = VideoReader(f\"{data_root}/train/00822.mp4\", ctx=cpu(0))\n\nprint(f\"Total frames: {len(vr)}\")\nprint(f\"FPS: {vr.get_avg_fps():.1f}\")\nprint(f\"Duration: {len(vr)/vr.get_avg_fps():.1f}s\")\nprint(f\"Frame shape: {vr[0].shape}\")  # H, W, C","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:07:41.2748Z","iopub.execute_input":"2026-09-19T08:07:41.275606Z","iopub.status.idle":"2026-09-19T08:07:41.952827Z","shell.execute_reply.started":"2026-09-19T08:07:41.275572Z","shell.execute_reply":"2026-09-19T08:07:41.951813Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom decord import VideoReader, cpu\n\ndata_root = \"/kaggle/input/competitions/nexar-collision-prediction\"\nvr = VideoReader(f\"{data_root}/train/00822.mp4\", ctx=cpu(0))\n\n# TOP: 16 frames, sample rate 2 → lấy 16 frames cách đều\nnum_frames = 16\ntotal = len(vr)\nindices = np.linspace(0, total - 1, num_frames, dtype=int)\n\nframes = vr.get_batch(indices).asnumpy()  # (16, H, W, C)\nprint(f\"Sampled frames shape: {frames.shape}\")\nprint(f\"Frame indices: {indices}\")\nprint(f\"Dtype: {frames.dtype}, Range: [{frames.min()}, {frames.max()}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:10:01.06669Z","iopub.execute_input":"2026-09-19T08:10:01.067049Z","iopub.status.idle":"2026-09-19T08:10:04.110312Z","shell.execute_reply.started":"2026-09-19T08:10:01.06702Z","shell.execute_reply":"2026-09-19T08:10:04.109434Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom decord import VideoReader, cpu\n\ndata_root = \"/kaggle/input/competitions/nexar-collision-prediction\"\nvr = VideoReader(f\"{data_root}/train/00822.mp4\", ctx=cpu(0))\n\n# Sample 16 frames\nnum_frames = 16\nindices = np.linspace(0, len(vr) - 1, num_frames, dtype=int)\nframes = vr.get_batch(indices).asnumpy()  # (16, 720, 1280, 3)\n\n# Resize to 224x224\nfrom PIL import Image\nresized = []\nfor f in frames:\n    img = Image.fromarray(f).resize((224, 224))\n    resized.append(np.array(img))\nframes_np = np.stack(resized)  # (16, 224, 224, 3)\n\n# To tensor: (C, T, H, W) — SlowOnly format\nframes_t = torch.from_numpy(frames_np).float() / 255.0\nframes_t = frames_t.permute(3, 0, 1, 2)  # (3, 16, 224, 224)\n\n# ImageNet normalize\nmean = torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1, 1)\nstd = torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1, 1)\nframes_t = (frames_t - mean) / std\n\nprint(f\"Final tensor: {frames_t.shape}\")  # (3, 16, 224, 224)\nprint(f\"Mean per channel: {frames_t.mean(dim=(1,2,3)).tolist()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:10:29.096665Z","iopub.execute_input":"2026-09-19T08:10:29.097403Z","iopub.status.idle":"2026-09-19T08:10:32.197802Z","shell.execute_reply.started":"2026-09-19T08:10:29.097369Z","shell.execute_reply":"2026-09-19T08:10:32.197024Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport math\n\n# === SlowOnly-R50 backbone ===\ntry:\n    from pytorchvideo.models.resnet import create_resnet\n    backbone = create_resnet(\n        input_channel=3,\n        model_depth=50,\n        model_num_class=400,\n        dropout_rate=0,\n        norm=nn.BatchNorm3d,\n        stem_dim_out=64,\n        stem_conv_kernel_size=(1, 7, 7),\n        stem_conv_stride=(1, 2, 2),\n        stage_conv_a_kernel_size=((1,1,1), (1,1,1), (1,1,1), (1,1,1)),\n        stage_conv_b_kernel_size=((1,3,3), (1,3,3), (1,3,3), (1,3,3)),\n        stage_conv_b_num_groups=(1, 1, 1, 1),\n        stage_spatial_h_stride=(1, 2, 2, 2),\n        stage_spatial_w_stride=(1, 2, 2, 2),\n        stage_temporal_stride=(1, 1, 1, 1),\n        head_pool_kernel_size=(16, 7, 7),\n    )\n    # Remove classification head, keep features\n    backbone.blocks = backbone.blocks[:-1]  # remove head\n    feat_dim = 2048\n    use_3d = True\n    print(\"✅ SlowOnly-R50 3D backbone loaded\")\nexcept Exception as e:\n    print(f\"⚠️ pytorchvideo failed: {e}\")\n    print(\"Using 2D ResNet-50 fallback\")\n    from torchvision.models import resnet50\n    backbone = resnet50(weights=None)\n    backbone.fc = nn.Identity()\n    feat_dim = 2048\n    use_3d = False\n\n# === Test backbone ===\nbackbone.eval()\ndummy = torch.randn(1, 3, 16, 224, 224) if use_3d else torch.randn(1, 3, 224, 224)\nwith torch.no_grad():\n    feat = backbone(dummy)\nprint(f\"Backbone output: {feat.shape}\")\nprint(f\"Feature dim: {feat_dim}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:11:22.017824Z","iopub.execute_input":"2026-09-19T08:11:22.018161Z","iopub.status.idle":"2026-09-19T08:11:28.666348Z","shell.execute_reply.started":"2026-09-19T08:11:22.018109Z","shell.execute_reply":"2026-09-19T08:11:28.665412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport math\n \nclass TOPDecoder(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.feat_dim = 2048\n        self.d_model = 256\n        self.gap = nn.AdaptiveAvgPool3d(1)\n        self.feat_proj = nn.Linear(2048, 256)\n        self.queries = nn.Parameter(self.make_pe(20, 256))\n        dl = nn.TransformerDecoderLayer(\n            d_model=256, nhead=8,\n            dim_feedforward=1024,\n            dropout=0.1, batch_first=True)\n        self.decoder = nn.TransformerDecoder(dl, num_layers=2)\n        self.head = nn.Linear(256, 1)\n \n    def make_pe(self, T, D):\n        pe = torch.zeros(T, D)\n        p = torch.arange(0, T).unsqueeze(1).float()\n        d = torch.exp(torch.arange(0, D, 2).float() * (-math.log(10000.0) / D))\n        pe[:, 0::2] = torch.sin(p * d)\n        pe[:, 1::2] = torch.cos(p * d)\n        return pe.unsqueeze(0)\n \n    def forward(self, feat):\n        B = feat.shape[0]\n        x = self.gap(feat).view(B, 2048)\n        x = self.feat_proj(x).unsqueeze(1)\n        q = self.queries.expand(B, -1, -1)\n        out = self.decoder(q, x)\n        return self.head(out).squeeze(-1)\n \ndec = TOPDecoder()\ndummy = torch.randn(1, 2048, 16, 7, 7)\nwith torch.no_grad():\n    s = dec(dummy)\nprint(\"Output shape:\", s.shape)\nprint(\"Min:\", s.min().item(), \"Max:\", s.max().item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:14:12.269767Z","iopub.execute_input":"2026-09-19T08:14:12.270116Z","iopub.status.idle":"2026-09-19T08:14:12.389164Z","shell.execute_reply.started":"2026-09-19T08:14:12.270083Z","shell.execute_reply":"2026-09-19T08:14:12.388313Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell11_dataset.py\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\nfrom decord import VideoReader, cpu\nfrom PIL import Image\n\nDATA = \"/kaggle/input/competitions/nexar-collision-prediction\"\nif not os.path.exists(DATA):\n    DATA = \"/kaggle/input/nexar-collision-prediction\"\n\nclass NexarDataset(Dataset):\n    def __init__(self, csv_path, video_dir, num_frames=16,\n                 size=224, max_videos=None):\n        self.df = pd.read_csv(csv_path)\n        if max_videos:\n            self.df = self.df.head(max_videos)\n        self.video_dir = video_dir\n        self.num_frames = num_frames\n        self.size = size\n        self.horizons = [0.1 * (i + 1) for i in range(20)]\n        self.mean = np.array([0.485, 0.456, 0.406])\n        self.std = np.array([0.229, 0.224, 0.225])\n\n    def __len__(self):\n        return len(self.df)\n\n    def _make_labels(self, row, fps, total_frames):\n        target = int(row[\"target\"])\n        if target == 0:\n            return torch.zeros(20)\n        toe = row[\"time_of_event\"]\n        if pd.isna(toe):\n            return torch.zeros(20)\n        duration = total_frames / fps\n        obs_times = np.linspace(0, duration, self.num_frames)\n        labels = torch.zeros(20)\n        for h_idx, horizon in enumerate(self.horizons):\n            for obs_t in obs_times:\n                time_to_event = toe - obs_t\n                if abs(time_to_event - horizon) < 0.05:\n                    labels[h_idx] = 1.0\n                    break\n        return labels\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        vid_id = str(int(row[\"id\"])).zfill(5)\n        vid_path = os.path.join(self.video_dir, vid_id + \".mp4\")\n        try:\n            vr = VideoReader(vid_path, ctx=cpu(0))\n            total = len(vr)\n            fps = vr.get_avg_fps()\n            inds = np.linspace(0, total - 1,\n                               self.num_frames, dtype=int)\n            frames = vr.get_batch(inds).asnumpy()\n        except Exception as e:\n            frames = np.zeros(\n                (self.num_frames, self.size, self.size, 3),\n                dtype=np.uint8)\n            fps = 30.0\n            total = 900\n        imgs = []\n        for f in frames:\n            img = Image.fromarray(f).resize(\n                (self.size, self.size))\n            imgs.append(np.array(img))\n        arr = np.stack(imgs).astype(np.float32) / 255.0\n        arr = (arr - self.mean) / self.std\n        tensor = torch.from_numpy(arr).float()\n        tensor = tensor.permute(3, 0, 1, 2)\n        labels = self._make_labels(row, fps, total)\n        vid_target = torch.tensor(\n            float(row[\"target\"]), dtype=torch.float32)\n        return tensor, labels, vid_target\n\nRồi cell tiếp:\n\n```python\nfrom cell11_dataset import NexarDataset, DATA\nds = NexarDataset(\n    csv_path=f\"{DATA}/train.csv\",\n    video_dir=f\"{DATA}/train\",\n    max_videos=3)\nprint(\"Dataset size:\", len(ds))\nfor i in range(len(ds)):\n    x, lab, tgt = ds[i]\n    print(\"Video\", i, \"input:\", x.shape,\n          \"pos_labels:\", int(lab.sum().item()),\n          \"target:\", int(tgt.item()))\n```","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:16:35.337357Z","iopub.execute_input":"2026-09-19T08:16:35.337701Z","iopub.status.idle":"2026-09-19T08:16:35.344876Z","shell.execute_reply.started":"2026-09-19T08:16:35.337671Z","shell.execute_reply":"2026-09-19T08:16:35.344077Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell11_dataset.py\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\nfrom decord import VideoReader, cpu\nfrom PIL import Image\n\nDATA = \"/kaggle/input/competitions/nexar-collision-prediction\"\nif not os.path.exists(DATA):\n    DATA = \"/kaggle/input/nexar-collision-prediction\"\n\nclass NexarDataset(Dataset):\n    def __init__(self, csv_path, video_dir, num_frames=16,\n                 size=224, max_videos=None):\n        self.df = pd.read_csv(csv_path)\n        if max_videos:\n            self.df = self.df.head(max_videos)\n        self.video_dir = video_dir\n        self.num_frames = num_frames\n        self.size = size\n        self.horizons = [0.1 * (i + 1) for i in range(20)]\n        self.mean = np.array([0.485, 0.456, 0.406])\n        self.std = np.array([0.229, 0.224, 0.225])\n\n    def __len__(self):\n        return len(self.df)\n\n    def _make_labels(self, row, fps, total_frames):\n        target = int(row[\"target\"])\n        if target == 0:\n            return torch.zeros(20)\n        toe = row[\"time_of_event\"]\n        if pd.isna(toe):\n            return torch.zeros(20)\n        duration = total_frames / fps\n        obs_times = np.linspace(0, duration, self.num_frames)\n        labels = torch.zeros(20)\n        for h_idx, horizon in enumerate(self.horizons):\n            for obs_t in obs_times:\n                time_to_event = toe - obs_t\n                if abs(time_to_event - horizon) < 0.05:\n                    labels[h_idx] = 1.0\n                    break\n        return labels\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        vid_id = str(int(row[\"id\"])).zfill(5)\n        vid_path = os.path.join(self.video_dir, vid_id + \".mp4\")\n        try:\n            vr = VideoReader(vid_path, ctx=cpu(0))\n            total = len(vr)\n            fps = vr.get_avg_fps()\n            inds = np.linspace(0, total - 1,\n                               self.num_frames, dtype=int)\n            frames = vr.get_batch(inds).asnumpy()\n        except Exception as e:\n            frames = np.zeros(\n                (self.num_frames, self.size, self.size, 3),\n                dtype=np.uint8)\n            fps = 30.0\n            total = 900\n        imgs = []\n        for f in frames:\n            img = Image.fromarray(f).resize(\n                (self.size, self.size))\n            imgs.append(np.array(img))\n        arr = np.stack(imgs).astype(np.float32) / 255.0\n        arr = (arr - self.mean) / self.std\n        tensor = torch.from_numpy(arr).float()\n        tensor = tensor.permute(3, 0, 1, 2)\n        labels = self._make_labels(row, fps, total)\n        vid_target = torch.tensor(\n            float(row[\"target\"]), dtype=torch.float32)\n        return tensor, labels, vid_target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:17:36.058728Z","iopub.execute_input":"2026-09-19T08:17:36.059051Z","iopub.status.idle":"2026-09-19T08:17:36.065815Z","shell.execute_reply.started":"2026-09-19T08:17:36.059022Z","shell.execute_reply":"2026-09-19T08:17:36.065054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from cell11_dataset import NexarDataset, DATA\nds = NexarDataset(\n    csv_path=f\"{DATA}/train.csv\",\n    video_dir=f\"{DATA}/train\",\n    max_videos=3)\nprint(\"Dataset size:\", len(ds))\nfor i in range(len(ds)):\n    x, lab, tgt = ds[i]\n    print(\"Video\", i, \"input:\", x.shape,\n          \"pos_labels:\", int(lab.sum().item()),\n          \"target:\", int(tgt.item()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:17:50.573672Z","iopub.execute_input":"2026-09-19T08:17:50.574043Z","iopub.status.idle":"2026-09-19T08:18:00.003626Z","shell.execute_reply.started":"2026-09-19T08:17:50.574008Z","shell.execute_reply":"2026-09-19T08:18:00.002775Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass WeightedBCE(nn.Module):\n    def __init__(self, pos_weight=10.0):\n        super().__init__()\n        self.loss_fn = nn.BCEWithLogitsLoss(\n            pos_weight=torch.tensor([pos_weight]))\n\n    def forward(self, logits, targets):\n        return self.loss_fn(logits, targets)\n\n# Test\nloss_fn = WeightedBCE(pos_weight=10.0)\nfake_logits = torch.randn(4, 20)\nfake_labels = torch.zeros(4, 20)\nfake_labels[0, 5] = 1.0\nfake_labels[1, 10] = 1.0\nloss = loss_fn(fake_logits, fake_labels)\nprint(\"Loss:\", loss.item())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:18:23.864647Z","iopub.execute_input":"2026-09-19T08:18:23.865452Z","iopub.status.idle":"2026-09-19T08:18:23.87965Z","shell.execute_reply.started":"2026-09-19T08:18:23.865412Z","shell.execute_reply":"2026-09-19T08:18:23.878792Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell13_model.py\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\n\nclass TOPModel(nn.Module):\n    def __init__(self, num_horizons=20, d_model=256):\n        super().__init__()\n\n        # --- Backbone: SlowOnly-R50 ---\n        from pytorchvideo.models.resnet import create_resnet\n        full = create_resnet(\n            input_channel=3,\n            model_depth=50,\n            model_num_class=400,\n            norm=nn.BatchNorm3d,\n            activation=nn.ReLU,\n        )\n        # Remove classification head, keep feature blocks only\n        self.backbone = nn.ModuleList(full.blocks[:-1])\n        self.feat_dim = 2048\n\n        # --- Decoder: Transformer ---\n        self.gap = nn.AdaptiveAvgPool3d(1)\n        self.feat_proj = nn.Linear(self.feat_dim, d_model)\n        self.queries = nn.Parameter(\n            self._make_pe(num_horizons, d_model))\n        dl = nn.TransformerDecoderLayer(\n            d_model=d_model, nhead=8,\n            dim_feedforward=1024,\n            dropout=0.1, batch_first=True)\n        self.decoder = nn.TransformerDecoder(dl, num_layers=2)\n        self.head = nn.Linear(d_model, 1)\n\n    def _make_pe(self, T, D):\n        pe = torch.zeros(T, D)\n        p = torch.arange(0, T).unsqueeze(1).float()\n        d = torch.exp(\n            torch.arange(0, D, 2).float()\n            * (-math.log(10000.0) / D))\n        pe[:, 0::2] = torch.sin(p * d)\n        pe[:, 1::2] = torch.cos(p * d)\n        return pe.unsqueeze(0)\n\n    def forward(self, x):\n        # x: (B, 3, T, H, W)\n        for blk in self.backbone:\n            x = blk(x)\n        # x: (B, 2048, T', H', W')\n        feat = self.gap(x).view(x.shape[0], self.feat_dim)\n        feat = self.feat_proj(feat).unsqueeze(1)\n        q = self.queries.expand(x.shape[0], -1, -1)\n        out = self.decoder(q, feat)\n        return self.head(out).squeeze(-1)\n\n# Quick test\nif __name__ == \"__main__\":\n    model = TOPModel()\n    total = sum(p.numel() for p in model.parameters())\n    print(f\"Total params: {total:,}\")\n    dummy = torch.randn(1, 3, 16, 224, 224)\n    with torch.no_grad():\n        out = model(dummy)\n    print(f\"Output shape: {out.shape}\")\n    print(f\"Output range: [{out.min():.4f}, {out.max():.4f}]\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:23:06.334563Z","iopub.execute_input":"2026-09-19T08:23:06.334886Z","iopub.status.idle":"2026-09-19T08:23:06.341999Z","shell.execute_reply.started":"2026-09-19T08:23:06.334851Z","shell.execute_reply":"2026-09-19T08:23:06.341335Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python cell13_model.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:23:13.41799Z","iopub.execute_input":"2026-09-19T08:23:13.418953Z","iopub.status.idle":"2026-09-19T08:23:20.882359Z","shell.execute_reply.started":"2026-09-19T08:23:13.418913Z","shell.execute_reply":"2026-09-19T08:23:20.881366Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell14_dataloader.py\nimport torch\nfrom torch.utils.data import DataLoader\nfrom cell11_dataset import NexarDataset, DATA\nfrom cell13_model import TOPModel\n\n# Load 6 videos\nds = NexarDataset(\n    csv_path=f\"{DATA}/train.csv\",\n    video_dir=f\"{DATA}/train\",\n    max_videos=6)\n\nloader = DataLoader(ds, batch_size=2, shuffle=False, num_workers=0)\n\nmodel = TOPModel()\nmodel.eval()\n\nfor batch_idx, (videos, labels, targets) in enumerate(loader):\n    print(f\"Batch {batch_idx}:\")\n    print(f\"  videos:  {videos.shape}\")\n    print(f\"  labels:  {labels.shape}\")\n    print(f\"  targets: {targets.shape}\")\n    with torch.no_grad():\n        out = model(videos)\n    print(f\"  output:  {out.shape}\")\n    print(f\"  out range: [{out.min():.4f}, {out.max():.4f}]\")\n    print()\n\nprint(\"DataLoader + Model pipeline OK!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:23:48.028958Z","iopub.execute_input":"2026-09-19T08:23:48.029348Z","iopub.status.idle":"2026-09-19T08:23:48.036332Z","shell.execute_reply.started":"2026-09-19T08:23:48.029306Z","shell.execute_reply":"2026-09-19T08:23:48.035593Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python cell14_dataloader.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:23:56.001681Z","iopub.execute_input":"2026-09-19T08:23:56.002015Z","iopub.status.idle":"2026-09-19T08:24:36.520961Z","shell.execute_reply.started":"2026-09-19T08:23:56.001984Z","shell.execute_reply":"2026-09-19T08:24:36.519972Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install -q decord pytorchvideo","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:28:28.71516Z","iopub.execute_input":"2026-09-19T08:28:28.715441Z","iopub.status.idle":"2026-09-19T08:28:44.496752Z","shell.execute_reply.started":"2026-09-19T08:28:28.715379Z","shell.execute_reply":"2026-09-19T08:28:44.495946Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"CUDA: {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"VRAM: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} GB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:29:12.623896Z","iopub.execute_input":"2026-09-19T08:29:12.62431Z","iopub.status.idle":"2026-09-19T08:29:21.311365Z","shell.execute_reply.started":"2026-09-19T08:29:12.624274Z","shell.execute_reply":"2026-09-19T08:29:21.310393Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Re-run all writefile cells (no need to re-execute, just re-write files)\n!python cell13_model.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:31:08.881688Z","iopub.execute_input":"2026-09-19T08:31:08.882558Z","iopub.status.idle":"2026-09-19T08:31:09.113246Z","shell.execute_reply.started":"2026-09-19T08:31:08.882524Z","shell.execute_reply":"2026-09-19T08:31:09.112187Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell11_dataset.py\nimport os\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\nfrom decord import VideoReader, cpu\nfrom PIL import Image\n\nDATA = \"/kaggle/input/competitions/nexar-collision-prediction\"\nif not os.path.exists(DATA):\n    DATA = \"/kaggle/input/nexar-collision-prediction\"\n\nclass NexarDataset(Dataset):\n    def __init__(self, csv_path, video_dir, num_frames=16,\n                 size=224, max_videos=None):\n        self.df = pd.read_csv(csv_path)\n        if max_videos:\n            self.df = self.df.head(max_videos)\n        self.video_dir = video_dir\n        self.num_frames = num_frames\n        self.size = size\n        self.horizons = [0.1 * (i + 1) for i in range(20)]\n        self.mean = np.array([0.485, 0.456, 0.406])\n        self.std = np.array([0.229, 0.224, 0.225])\n\n    def __len__(self):\n        return len(self.df)\n\n    def _make_labels(self, row, fps, total_frames):\n        target = int(row[\"target\"])\n        if target == 0:\n            return torch.zeros(20)\n        toe = row[\"time_of_event\"]\n        if pd.isna(toe):\n            return torch.zeros(20)\n        duration = total_frames / fps\n        obs_times = np.linspace(0, duration, self.num_frames)\n        labels = torch.zeros(20)\n        for h_idx, horizon in enumerate(self.horizons):\n            for obs_t in obs_times:\n                time_to_event = toe - obs_t\n                if abs(time_to_event - horizon) < 0.05:\n                    labels[h_idx] = 1.0\n                    break\n        return labels\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        vid_id = str(int(row[\"id\"])).zfill(5)\n        vid_path = os.path.join(self.video_dir, vid_id + \".mp4\")\n        try:\n            vr = VideoReader(vid_path, ctx=cpu(0))\n            total = len(vr)\n            fps = vr.get_avg_fps()\n            inds = np.linspace(0, total - 1,\n                               self.num_frames, dtype=int)\n            frames = vr.get_batch(inds).asnumpy()\n        except Exception as e:\n            print(f\"Error {vid_path}: {e}\")\n            frames = np.zeros(\n                (self.num_frames, self.size, self.size, 3),\n                dtype=np.uint8)\n            fps = 30.0\n            total = 900\n        imgs = []\n        for f in frames:\n            img = Image.fromarray(f).resize(\n                (self.size, self.size))\n            imgs.append(np.array(img))\n        arr = np.stack(imgs).astype(np.float32) / 255.0\n        arr = (arr - self.mean) / self.std\n        tensor = torch.from_numpy(arr).float()\n        tensor = tensor.permute(3, 0, 1, 2)\n        labels = self._make_labels(row, fps, total)\n        vid_target = torch.tensor(\n            float(row[\"target\"]), dtype=torch.float32)\n        return tensor, labels, vid_target","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:31:43.987292Z","iopub.execute_input":"2026-09-19T08:31:43.98779Z","iopub.status.idle":"2026-09-19T08:31:43.995879Z","shell.execute_reply.started":"2026-09-19T08:31:43.987754Z","shell.execute_reply":"2026-09-19T08:31:43.994894Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell13_model.py\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\n\nclass TOPModel(nn.Module):\n    def __init__(self, num_horizons=20, d_model=256):\n        super().__init__()\n\n        # --- Backbone: SlowOnly-R50 ---\n        from pytorchvideo.models.resnet import create_resnet\n        full = create_resnet(\n            input_channel=3,\n            model_depth=50,\n            model_num_class=400,\n            norm=nn.BatchNorm3d,\n            activation=nn.ReLU,\n        )\n        # Remove classification head, keep feature blocks only\n        self.backbone = nn.ModuleList(full.blocks[:-1])\n        self.feat_dim = 2048\n\n        # --- Decoder: Transformer ---\n        self.gap = nn.AdaptiveAvgPool3d(1)\n        self.feat_proj = nn.Linear(self.feat_dim, d_model)\n        self.queries = nn.Parameter(\n            self._make_pe(num_horizons, d_model))\n        dl = nn.TransformerDecoderLayer(\n            d_model=d_model, nhead=8,\n            dim_feedforward=1024,\n            dropout=0.1, batch_first=True)\n        self.decoder = nn.TransformerDecoder(dl, num_layers=2)\n        self.head = nn.Linear(d_model, 1)\n\n    def _make_pe(self, T, D):\n        pe = torch.zeros(T, D)\n        p = torch.arange(0, T).unsqueeze(1).float()\n        d = torch.exp(\n            torch.arange(0, D, 2).float()\n            * (-math.log(10000.0) / D))\n        pe[:, 0::2] = torch.sin(p * d)\n        pe[:, 1::2] = torch.cos(p * d)\n        return pe.unsqueeze(0)\n\n    def forward(self, x):\n        # x: (B, 3, T, H, W)\n        for blk in self.backbone:\n            x = blk(x)\n        # x: (B, 2048, T', H', W')\n        feat = self.gap(x).view(x.shape[0], self.feat_dim)\n        feat = self.feat_proj(feat).unsqueeze(1)\n        q = self.queries.expand(x.shape[0], -1, -1)\n        out = self.decoder(q, feat)\n        return self.head(out).squeeze(-1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:31:55.235834Z","iopub.execute_input":"2026-09-19T08:31:55.236378Z","iopub.status.idle":"2026-09-19T08:31:55.242115Z","shell.execute_reply.started":"2026-09-19T08:31:55.236349Z","shell.execute_reply":"2026-09-19T08:31:55.241362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell15_train.py\nimport os\nimport time\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom torch.amp import autocast, GradScaler\nfrom cell11_dataset import NexarDataset, DATA\nfrom cell13_model import TOPModel\n\n# ============ CONFIG ============\nSTART_EPOCH = 0\nEND_EPOCH = 10        # Phase 1: epoch 0-9\nBATCH_SIZE = 4\nLR = 0.01\nMOMENTUM = 0.9\nWEIGHT_DECAY = 1e-4\nPOS_WEIGHT = 10.0\nNUM_WORKERS = 2\nRESUME = None         # Path to checkpoint for resume\nOUTPUT_DIR = \"/kaggle/working\"\n# ================================\n\ndef train():\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    print(f\"Device: {device}\")\n\n    # Dataset & DataLoader\n    ds = NexarDataset(\n        csv_path=f\"{DATA}/train.csv\",\n        video_dir=f\"{DATA}/train\",\n        num_frames=16, size=224)\n    loader = DataLoader(\n        ds, batch_size=BATCH_SIZE, shuffle=True,\n        num_workers=NUM_WORKERS, pin_memory=True,\n        drop_last=True)\n    print(f\"Dataset: {len(ds)} videos, {len(loader)} batches/epoch\")\n\n    # Model\n    model = TOPModel(num_horizons=20, d_model=256).to(device)\n    total_params = sum(p.numel() for p in model.parameters())\n    print(f\"Model params: {total_params:,}\")\n\n    # Loss: Weighted BCE\n    pos_w = torch.tensor([POS_WEIGHT], device=device)\n    criterion = nn.BCEWithLogitsLoss(pos_weight=pos_w)\n\n    # Optimizer + Scheduler\n    optimizer = torch.optim.SGD(\n        model.parameters(), lr=LR,\n        momentum=MOMENTUM, weight_decay=WEIGHT_DECAY)\n\n    # Step LR decay at epoch 20 and 40\n    def get_lr(epoch):\n        if epoch < 20:\n            return 1.0\n        elif epoch < 40:\n            return 0.1\n        else:\n            return 0.01\n    scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, get_lr)\n\n    # AMP scaler\n    scaler = GradScaler()\n\n    # Resume checkpoint\n    if RESUME and os.path.exists(RESUME):\n        ckpt = torch.load(RESUME, map_location=device)\n        model.load_state_dict(ckpt[\"model\"])\n        optimizer.load_state_dict(ckpt[\"optimizer\"])\n        scheduler.load_state_dict(ckpt[\"scheduler\"])\n        scaler.load_state_dict(ckpt[\"scaler\"])\n        print(f\"Resumed from {RESUME}, epoch {ckpt['epoch']}\")\n\n    # Warm up scheduler to START_EPOCH\n    for _ in range(START_EPOCH):\n        scheduler.step()\n\n    best_loss = float(\"inf\")\n    print(f\"\\n{'='*50}\")\n    print(f\"Training Phase: epoch {START_EPOCH} -> {END_EPOCH-1}\")\n    print(f\"LR: {optimizer.param_groups[0]['lr']}\")\n    print(f\"{'='*50}\\n\")\n\n    for epoch in range(START_EPOCH, END_EPOCH):\n        model.train()\n        epoch_loss = 0.0\n        t0 = time.time()\n\n        for batch_idx, (videos, labels, targets) in enumerate(loader):\n            videos = videos.to(device, non_blocking=True)\n            labels = labels.to(device, non_blocking=True)\n\n            optimizer.zero_grad()\n            with autocast(device_type=\"cuda\"):\n                logits = model(videos)       # (B, 20)\n                loss = criterion(logits, labels)\n\n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n\n            epoch_loss += loss.item()\n\n            if (batch_idx + 1) % 50 == 0:\n                print(f\"  Epoch {epoch} [{batch_idx+1}/{len(loader)}] \"\n                      f\"loss={loss.item():.4f}\")\n\n        scheduler.step()\n        avg_loss = epoch_loss / len(loader)\n        elapsed = time.time() - t0\n        lr_now = optimizer.param_groups[0][\"lr\"]\n        print(f\"Epoch {epoch}: loss={avg_loss:.4f}, \"\n              f\"lr={lr_now:.6f}, time={elapsed:.0f}s\")\n\n        # Save best\n        if avg_loss < best_loss:\n            best_loss = avg_loss\n            torch.save({\n                \"epoch\": epoch + 1,\n                \"model\": model.state_dict(),\n                \"optimizer\": optimizer.state_dict(),\n                \"scheduler\": scheduler.state_dict(),\n                \"scaler\": scaler.state_dict(),\n                \"loss\": best_loss,\n            }, os.path.join(OUTPUT_DIR, \"best.pth\"))\n            print(f\"  -> Saved best.pth (loss={best_loss:.4f})\")\n\n    # Save final checkpoint for resume\n    torch.save({\n        \"epoch\": END_EPOCH,\n        \"model\": model.state_dict(),\n        \"optimizer\": optimizer.state_dict(),\n        \"scheduler\": scheduler.state_dict(),\n        \"scaler\": scaler.state_dict(),\n        \"loss\": avg_loss,\n    }, os.path.join(OUTPUT_DIR, f\"ckpt_ep{END_EPOCH}.pth\"))\n    print(f\"\\nSaved ckpt_ep{END_EPOCH}.pth\")\n    print(f\"Best loss: {best_loss:.4f}\")\n    print(\"Done!\")\n\nif __name__ == \"__main__\":\n    train()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:40:30.6949Z","iopub.execute_input":"2026-09-19T08:40:30.695378Z","iopub.status.idle":"2026-09-19T08:40:30.702435Z","shell.execute_reply.started":"2026-09-19T08:40:30.695349Z","shell.execute_reply":"2026-09-19T08:40:30.701559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python cell15_train.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T08:40:41.61337Z","iopub.execute_input":"2026-09-19T08:40:41.614329Z","iopub.status.idle":"2026-09-19T17:38:28.952247Z","shell.execute_reply.started":"2026-09-19T08:40:41.614295Z","shell.execute_reply":"2026-09-19T17:38:28.950978Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile cell16_eval.py\nimport torch, sys, os\nsys.path.insert(0, \"/kaggle/working\")\nfrom cell11_dataset import NexarDataset\nfrom cell13_model import TOPModel\n\nmodel = TOPModel().cuda()\nckpt = torch.load(\"best.pth\", map_location=\"cuda\")\nmodel.load_state_dict(ckpt[\"model\"])\nmodel.eval()\n\nDATA = \"/kaggle/input/nexar-collision-prediction\"\nif not os.path.exists(DATA):\n    DATA = \"/kaggle/input/competitions/nexar-collision-prediction\"\n\nds = NexarDataset(\n    csv_path=os.path.join(DATA, \"train.csv\"),\n    video_dir=os.path.join(DATA, \"train\")\n)\nloader = torch.utils.data.DataLoader(ds, batch_size=4, num_workers=2)\n\nall_scores, all_labels = [], []\nwith torch.no_grad():\n    for i, (frames, labels, target) in enumerate(loader):\n        logits = model(frames.cuda())\n        probs = torch.sigmoid(logits)\n        score = probs.max(dim=1).values\n        all_scores.append(score.cpu())\n        all_labels.append(target.float())\n        if (i+1) % 50 == 0:\n            print(f\"  [{i+1}/375]\")\n\nscores = torch.cat(all_scores).numpy()\nlabels = torch.cat(all_labels).numpy()\n\nfrom sklearn.metrics import average_precision_score, roc_auc_score\nmAP = average_precision_score(labels, scores)\nAUC = roc_auc_score(labels, scores)\nprint(f\"\\n{'='*40}\")\nprint(f\"Phase 1 (epoch 0-9) Results:\")\nprint(f\"  mAP = {mAP:.4f}\")\nprint(f\"  AUC = {AUC:.4f}\")\nprint(f\"{'='*40}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T17:44:28.899196Z","iopub.execute_input":"2026-09-19T17:44:28.90003Z","iopub.status.idle":"2026-09-19T17:44:28.905797Z","shell.execute_reply.started":"2026-09-19T17:44:28.899995Z","shell.execute_reply":"2026-09-19T17:44:28.90493Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python cell16_eval.py","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-09-19T17:44:43.835809Z","iopub.execute_input":"2026-09-19T17:44:43.836667Z","iopub.status.idle":"2026-09-19T18:36:43.92879Z","shell.execute_reply.started":"2026-09-19T17:44:43.836635Z","shell.execute_reply":"2026-09-19T18:36:43.927866Z"}},"outputs":[],"execution_count":null}]}