{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# MABe Mouse Behavior Detection - Kaggle Submission\n\n**Model Info:**\n- \u2705 Conv1DBiLSTM (trained 23 epochs)\n- \u2705 Validation F1: 0.4332\n- \u2705 Input: 142 features (71 keypoints)\n- \u26a1 GPU inference ~30min\n\n**Submission Type:**\n- \ud83d\udcdd **Code Competition** - Direct notebook submission\n- Output will be automatically submitted to competition\n\n**Setup:**\n1. Add dataset: `mabe-submit` (contains best_model.pth)\n2. Enable GPU (T4 or P100)\n3. Enable Internet\n4. Run all cells\n5. Notebook output = submission"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom pathlib import Path\nfrom tqdm.notebook import tqdm\nimport warnings\nwarnings.filterwarnings('ignore')\n\nprint(\"=\"*60)\nprint(\"MABe Kaggle Submission - Inference Only\")\nprint(\"=\"*60)"}, {"cell_type": "markdown", "metadata": {}, "source": "## 1. Define Conv1DBiLSTM Model"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import torch.nn.functional as F\n\nclass Conv1DBiLSTM(nn.Module):\n    \"\"\"\n    Conv1D + BiLSTM Model (matches checkpoint architecture)\n    \"\"\"\n\n    def __init__(self, input_dim, num_classes,\n                 conv_channels=[64, 128, 256],\n                 lstm_hidden=256, lstm_layers=2,\n                 dropout=0.3):\n        super().__init__()\n\n        self.input_dim = input_dim\n        self.num_classes = num_classes\n\n        # Conv layers\n        conv_layers = []\n        in_channels = input_dim\n        for out_channels in conv_channels:\n            conv_layers.extend([\n                nn.Conv1d(in_channels, out_channels, kernel_size=3, padding=1),\n                nn.BatchNorm1d(out_channels),\n                nn.ReLU(),\n                nn.Dropout(dropout),\n                nn.MaxPool1d(kernel_size=2)\n            ])\n            in_channels = out_channels\n\n        self.conv_layers = nn.Sequential(*conv_layers)\n        self.pooling_factor = 2 ** len(conv_channels)\n\n        # Feature projection\n        self.feature_projection = nn.Linear(conv_channels[-1], lstm_hidden)\n\n        # BiLSTM\n        self.lstm = nn.LSTM(\n            input_size=lstm_hidden,\n            hidden_size=lstm_hidden,\n            num_layers=lstm_layers,\n            batch_first=True,\n            bidirectional=True,\n            dropout=dropout if lstm_layers > 1 else 0\n        )\n\n        # Classifier\n        self.classifier = nn.Sequential(\n            nn.Linear(lstm_hidden * 2, lstm_hidden),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(lstm_hidden, num_classes)\n        )\n\n    def forward(self, x):\n        batch_size, seq_len, input_dim = x.shape\n\n        # Conv: [batch, input_dim, seq_len]\n        x = x.transpose(1, 2)\n        conv_out = self.conv_layers(x)\n        conv_out = conv_out.transpose(1, 2)\n\n        # Feature projection\n        lstm_in = self.feature_projection(conv_out)\n\n        # BiLSTM\n        lstm_out, _ = self.lstm(lstm_in)\n\n        # Upsample to original seq_len\n        lstm_out = lstm_out.transpose(1, 2)\n        lstm_out = F.interpolate(lstm_out, size=seq_len, mode='linear', align_corners=False)\n        lstm_out = lstm_out.transpose(1, 2)\n\n        # Classify\n        output = self.classifier(lstm_out)\n\n        return output\n\nprint(\"\u2713 Conv1DBiLSTM model defined\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## 2. Load Checkpoint (Val F1: 0.4332)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Device\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Device: {device}\")\n\nif device.type == 'cuda':\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB\")\nprint()\n\n# Load checkpoint\nMODEL_PATH = Path('/kaggle/input/mabe-submit/best_model.pth')\ncheckpoint = torch.load(MODEL_PATH, map_location=device)\n\nprint(f\"\u2713 Loaded checkpoint\")\nprint(f\"  Epoch: {checkpoint['epoch'] + 1}\")\nif 'best_val_f1' in checkpoint:\n    print(f\"  Val F1: {checkpoint['best_val_f1']:.4f}\")\nprint()\n\n# Build model (exact config from checkpoint)\nmodel = Conv1DBiLSTM(\n    input_dim=142,  # 71 keypoints \u00d7 2\n    num_classes=4,\n    conv_channels=[64, 128, 256],\n    lstm_hidden=256,\n    lstm_layers=2,\n    dropout=0.3,\n)\n\nmodel.load_state_dict(checkpoint['model_state_dict'])\nmodel = model.to(device)\nmodel.eval()\n\nprint(f\"\u2713 Model ready: Conv1DBiLSTM\")\nprint(f\"  Parameters: {sum(p.numel() for p in model.parameters()):,}\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## 3. Load Test Data"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Try both possible dataset paths (case variations)\npossible_paths = [\n    Path('/kaggle/input/MABe-mouse-behavior-detection'),  # Official dataset\n    Path('/kaggle/input/mabe-mouse-behavior-detection'),  # Lowercase\n]\n\nDATA_DIR = None\nfor path in possible_paths:\n    if path.exists():\n        DATA_DIR = path\n        break\n\nif DATA_DIR is None:\n    raise FileNotFoundError(\"Cannot find MABe dataset. Please add it to notebook inputs.\")\n\nprint(f\"\u2713 Using dataset: {DATA_DIR}\")\n\n# Load test metadata\nif (DATA_DIR / 'test.csv').exists():\n    test_csv = pd.read_csv(DATA_DIR / 'test.csv')\n    print(f\"  Loaded test.csv with {len(test_csv)} videos\")\nelif (DATA_DIR / 'sample_submission.csv').exists():\n    # Use sample submission to get video list\n    sample_sub = pd.read_csv(DATA_DIR / 'sample_submission.csv')\n    test_videos = sample_sub['video_id'].unique()\n    # Infer lab_id from test_tracking directory structure\n    test_labs = []\n    for lab_dir in (DATA_DIR / 'test_tracking').iterdir():\n        if lab_dir.is_dir():\n            test_labs.append(lab_dir.name)\n    \n    # Create test_csv from available videos in test_tracking\n    test_data = []\n    for lab_id in test_labs:\n        lab_dir = DATA_DIR / 'test_tracking' / lab_id\n        for video_file in lab_dir.glob('*.parquet'):\n            video_id = video_file.stem\n            test_data.append({'video_id': video_id, 'lab_id': lab_id})\n    \n    test_csv = pd.DataFrame(test_data)\n    print(f\"  Constructed test set from test_tracking: {len(test_csv)} videos\")\nelse:\n    raise FileNotFoundError(\"Cannot find test.csv or sample_submission.csv\")\n\nprint(f\"  Total test videos: {len(test_csv)}\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## 4. Generate Predictions"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "print(\"Generating predictions...\")\nprint()\n\nall_predictions = []\nsequence_length = 100\nstride = 25  # 75% overlap\n\nwith torch.no_grad():\n    for idx, row in tqdm(test_csv.iterrows(), total=len(test_csv), desc=\"Processing\"):\n        video_id = row['video_id']\n        lab_id = row['lab_id']\n        \n        # Load tracking\n        tracking_file = DATA_DIR / 'test_tracking' / lab_id / f'{video_id}.parquet'\n        if not tracking_file.exists():\n            continue\n        \n        try:\n            tracking_df = pd.read_parquet(tracking_file)\n            \n            # Convert to wide format (142 features)\n            tracking_pivot = tracking_df.pivot_table(\n                index='video_frame',\n                columns=['mouse_id', 'bodypart'],\n                values=['x', 'y'],\n                aggfunc='first'\n            )\n            tracking_pivot.columns = ['_'.join(map(str, col)).strip() \n                                      for col in tracking_pivot.columns.values]\n            tracking_pivot = tracking_pivot.sort_index()\n            \n            keypoints = tracking_pivot.values.astype(np.float32)\n            keypoints = np.nan_to_num(keypoints, nan=0.0)\n            \n            # Ensure exactly 142 features\n            if keypoints.shape[1] != 142:\n                if keypoints.shape[1] < 142:\n                    # Pad with zeros\n                    padding = np.zeros((keypoints.shape[0], 142 - keypoints.shape[1]), dtype=np.float32)\n                    keypoints = np.concatenate([keypoints, padding], axis=1)\n                else:\n                    # Truncate\n                    keypoints = keypoints[:, :142]\n            \n            num_frames = len(keypoints)\n            \n            # Sliding window predictions\n            video_preds = np.zeros((num_frames, 4), dtype=np.float32)\n            video_counts = np.zeros(num_frames, dtype=np.int32)\n            \n            for start_idx in range(0, max(1, num_frames - sequence_length + 1), stride):\n                end_idx = min(start_idx + sequence_length, num_frames)\n                \n                # Handle last window\n                if end_idx - start_idx < sequence_length:\n                    start_idx = max(0, num_frames - sequence_length)\n                    end_idx = num_frames\n                \n                window = keypoints[start_idx:end_idx]\n                \n                # Pad if needed\n                if len(window) < sequence_length:\n                    padding = np.zeros((sequence_length - len(window), 142), dtype=np.float32)\n                    window = np.concatenate([window, padding], axis=0)\n                \n                # Predict\n                window_tensor = torch.FloatTensor(window).unsqueeze(0).to(device)\n                output = model(window_tensor)\n                probs = torch.softmax(output, dim=-1).squeeze(0).cpu().numpy()\n                \n                # Accumulate\n                actual_length = min(sequence_length, end_idx - start_idx)\n                video_preds[start_idx:start_idx + actual_length] += probs[:actual_length]\n                video_counts[start_idx:start_idx + actual_length] += 1\n            \n            # Average overlapping predictions\n            video_counts = np.maximum(video_counts, 1)\n            video_preds = video_preds / video_counts[:, np.newaxis]\n            final_preds = np.argmax(video_preds, axis=1)\n            \n            # Create submission rows\n            for frame_idx, pred in enumerate(final_preds):\n                all_predictions.append({\n                    'video_id': video_id,\n                    'frame': frame_idx,\n                    'prediction': int(pred),\n                })\n        \n        except Exception as e:\n            print(f\"Error: {video_id} - {e}\")\n            continue\n\nprint(f\"\\n\u2713 Generated {len(all_predictions):,} predictions\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## 5. Create Submission File"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "submission_df = pd.DataFrame(all_predictions)\nsubmission_df = submission_df.sort_values(['video_id', 'frame']).reset_index(drop=True)\n\nprint(\"=\"*60)\nprint(\"Converting to behavior intervals...\")\nprint(\"=\"*60)\n\n# Convert frame-level predictions to behavior intervals\nclass_names = {0: 'background', 1: 'social', 2: 'mating', 3: 'aggressive'}\naction_mapping = {\n    1: 'social',      # Social behaviors\n    2: 'mating',      # Mating behaviors  \n    3: 'aggressive',  # Aggressive behaviors\n}\n\nsubmission_rows = []\nrow_id = 0\n\nfor video_id in submission_df['video_id'].unique():\n    video_preds = submission_df[submission_df['video_id'] == video_id].sort_values('frame')\n    \n    # Group consecutive frames with same prediction\n    current_action = None\n    start_frame = None\n    \n    for idx, row in video_preds.iterrows():\n        frame = row['frame']\n        pred = row['prediction']\n        \n        # Skip background (class 0)\n        if pred == 0:\n            if current_action is not None:\n                # End previous action\n                submission_rows.append({\n                    'row_id': row_id,\n                    'video_id': video_id,\n                    'agent_id': 'mouse1',  # Default agent\n                    'target_id': 'mouse2',  # Default target\n                    'action': action_mapping.get(current_action, 'social'),\n                    'start_frame': start_frame,\n                    'stop_frame': frame - 1\n                })\n                row_id += 1\n                current_action = None\n            continue\n        \n        # Start new action or continue current\n        if current_action != pred:\n            if current_action is not None:\n                # End previous action\n                submission_rows.append({\n                    'row_id': row_id,\n                    'video_id': video_id,\n                    'agent_id': 'mouse1',\n                    'target_id': 'mouse2',\n                    'action': action_mapping.get(current_action, 'social'),\n                    'start_frame': start_frame,\n                    'stop_frame': frame - 1\n                })\n                row_id += 1\n            \n            # Start new action\n            current_action = pred\n            start_frame = frame\n    \n    # Close any remaining action\n    if current_action is not None and current_action != 0:\n        submission_rows.append({\n            'row_id': row_id,\n            'video_id': video_id,\n            'agent_id': 'mouse1',\n            'target_id': 'mouse2',\n            'action': action_mapping.get(current_action, 'social'),\n            'start_frame': start_frame,\n            'stop_frame': frame\n        })\n        row_id += 1\n\n# Create final submission\nfinal_submission = pd.DataFrame(submission_rows)\n\n# Add one row per video if no behaviors detected\nfor video_id in submission_df['video_id'].unique():\n    if video_id not in final_submission['video_id'].values:\n        final_submission = pd.concat([final_submission, pd.DataFrame([{\n            'row_id': row_id,\n            'video_id': video_id,\n            'agent_id': 'mouse1',\n            'target_id': 'mouse2',\n            'action': 'social',\n            'start_frame': 0,\n            'stop_frame': 1\n        }])], ignore_index=True)\n        row_id += 1\n\nprint(f\"\u2713 Converted to {len(final_submission)} behavior intervals\")\nprint(f\"  Action distribution:\")\nfor action, count in final_submission['action'].value_counts().items():\n    print(f\"    {action}: {count}\")\n\n# Save in correct format\nfinal_submission = final_submission[['row_id', 'video_id', 'agent_id', 'target_id', 'action', 'start_frame', 'stop_frame']]\nfinal_submission.to_csv('/kaggle/working/submission.csv', index=False)\n\nprint(f\"\\n\u2713 Saved to /kaggle/working/submission.csv\")\nprint(f\"  Total intervals: {len(final_submission):,}\")\nprint(f\"  Unique videos: {final_submission['video_id'].nunique()}\")\nprint(\"\\n\ud83c\udfaf Submission ready!\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## 6. Preview"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Preview submission\nsubmission_df.head(20)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Preview submission\nsubmission_df.head(20)"}], "metadata": {"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}, "language_info": {"codemirror_mode": {"name": "ipython", "version": 3}, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.0"}}, "nbformat": 4, "nbformat_minor": 4}