{"cells": [{"cell_type": "markdown", "metadata": {}, "source": "# SimpleTwoBranchLight v5 Inference\n\n- v5\u30e2\u30c7\u30eb: Modality Dropout + ToF\u7a7a\u9593\u7279\u5fb4\u91cf\uff08CV 76.45%\uff09\n- 5-fold\u30a2\u30f3\u30b5\u30f3\u30d6\u30eb\u5b9f\u88c5\n- CMI API v2\u5f62\u5f0f\u5bfe\u5fdc"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "import os\nimport sys\nimport gc\nimport warnings\nwarnings.filterwarnings('ignore')\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom pathlib import Path\nimport pickle\nimport json\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\n\nprint(f'PyTorch version: {torch.__version__}')\nprint(f'CUDA available: {torch.cuda.is_available()}')"}, {"cell_type": "markdown", "metadata": {}, "source": "## Feature Engineering Functions"}, {"cell_type": "code", "metadata": {}, "outputs": [], "source": "# SimpleTwoBranchLight v5\u30e2\u30c7\u30eb\u5b9a\u7fa9\nclass GaussianNoise(nn.Module):\n    \"\"\"\u30ac\u30a6\u30b7\u30a2\u30f3\u30ce\u30a4\u30ba\u30ec\u30a4\u30e4\u30fc\uff08\u5b66\u7fd2\u6642\u306e\u307f\u9069\u7528\uff09\"\"\"\n    def __init__(self, sigma=0.09):\n        super().__init__()\n        self.sigma = sigma\n        self.register_buffer('noise', torch.tensor(0))\n    \n    def forward(self, x):\n        if self.training and self.sigma > 0:\n            sampled_noise = self.noise.expand(*x.size()).float().normal_(mean=0, std=self.sigma)\n            x = x + sampled_noise\n        return x\n\n\nclass SEBlock(nn.Module):\n    \"\"\"Squeeze-and-Excitation Block\"\"\"\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.avg_pool = nn.AdaptiveAvgPool1d(1)\n        self.fc = nn.Sequential(\n            nn.Linear(channels, channels // reduction, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Linear(channels // reduction, channels, bias=False),\n            nn.Sigmoid()\n        )\n    \n    def forward(self, x):\n        b, c, _ = x.size()\n        y = self.avg_pool(x).view(b, c)\n        y = self.fc(y).view(b, c, 1)\n        return x * y.expand_as(x)\n\n\nclass ResidualSECNNBlock(nn.Module):\n    \"\"\"\u6b8b\u5dee\u63a5\u7d9a\u4ed8\u304dSE-CNN\u30d6\u30ed\u30c3\u30af\"\"\"\n    def __init__(self, in_channels, out_channels, kernel_size=3, dropout=0.1):\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size, padding=kernel_size//2)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size, padding=kernel_size//2)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n        self.se = SEBlock(out_channels)\n        self.dropout = nn.Dropout(dropout)\n        \n        # Skip connection\n        self.skip = nn.Identity() if in_channels == out_channels else \\\n                    nn.Conv1d(in_channels, out_channels, 1)\n    \n    def forward(self, x):\n        residual = x\n        \n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.dropout(out)\n        out = self.bn2(self.conv2(out))\n        out = self.se(out)\n        \n        out += self.skip(residual)\n        out = F.relu(out)\n        \n        return out\n\n\nclass SimpleTwoBranchLight(nn.Module):\n    \"\"\"\n    SimpleTwoBranchLight v5 - \u6539\u826f\u7248Two-Branch\u30e2\u30c7\u30eb\n    \n    v5\u306e\u4e3b\u306a\u6539\u826f\u70b9:\n    - Modality Dropout: \u5b66\u7fd2\u664250%\u3067ToF/Thermal\u3092\u30c9\u30ed\u30c3\u30d7\uff08CMI\u5236\u7d04\u5bfe\u5fdc\uff09\n    - IMU\u5165\u529b\u6b21\u5143: 7 \u2192 11\uff08\u7279\u5fb4\u91cf\u30a8\u30f3\u30b8\u30cb\u30a2\u30ea\u30f3\u30b0\u8ffd\u52a0\uff09\n    - ToF\u5165\u529b\u6b21\u5143: 320 \u2192 50\uff08\u7d71\u8a08\u91cf20 + \u7a7a\u9593\u7279\u5fb4\u91cf30\uff09\n    - Dropout: \u6bb5\u968e\u7684\u8abf\u6574\uff080.1\u21920.3\u21920.5\uff09\n    - RNN: 1\u5c64\u306e\u307f\uff08\u30b7\u30f3\u30d7\u30eb\u5316\uff09\n    \"\"\"\n    \n    def __init__(self, imu_dim=11, tof_dim=50, thermopile_dim=5, \n                 hidden_dim=256, num_classes=18, modality_dropout_rate=0.5):\n        super().__init__()\n        \n        # IMU Branch (ResidualSECNNBlock\u4f7f\u7528)\n        self.imu_branch = nn.Sequential(\n            ResidualSECNNBlock(imu_dim, 64, dropout=0.1),  # v5\u30aa\u30ea\u30b8\u30ca\u30eb\u30b5\u30a4\u30ba\n            nn.MaxPool1d(2),\n            ResidualSECNNBlock(64, 128, kernel_size=5, dropout=0.1),  # v5\u30aa\u30ea\u30b8\u30ca\u30eb\u30b5\u30a4\u30ba\n            nn.MaxPool1d(2)\n        )\n        \n        # ToF/Thermal Branch (\u30b7\u30f3\u30d7\u30eb\u306aCNN)\n        tof_thermal_dim = tof_dim + thermopile_dim  # 50 + 5 = 55 (v5\u3067\u62e1\u5f35)\n        self.tof_branch = nn.Sequential(\n            nn.Conv1d(tof_thermal_dim, 64, 3, padding=1),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.MaxPool1d(2),\n            nn.Dropout(0.2),\n            \n            nn.Conv1d(64, 128, 3, padding=1),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.MaxPool1d(2),\n            nn.Dropout(0.2)\n        )\n        \n        # Merge dimension\n        merged_dim = 256  # 128 + 128 (v5\u30aa\u30ea\u30b8\u30ca\u30eb)\n        \n        # Parallel RNN (1\u5c64\u306e\u307f\u3001LB 0.76\u6e96\u62e0)\n        self.lstm = nn.LSTM(\n            merged_dim, hidden_dim // 2, \n            num_layers=1,  # 1\u5c64\u306b\u524a\u6e1b\n            batch_first=True, \n            dropout=0,  # 1\u5c64\u306a\u306e\u3067LSTM\u5185dropout\u306f0\n            bidirectional=True\n        )\n        \n        self.gru = nn.GRU(\n            merged_dim, hidden_dim // 2,\n            num_layers=1,\n            batch_first=True,\n            dropout=0,\n            bidirectional=True\n        )\n        \n        # Noise branch (LB 0.76\u6e96\u62e0)\n        self.gaussian_noise = GaussianNoise(0.09)\n        self.noise_branch = nn.Sequential(\n            nn.Linear(merged_dim, 16),\n            nn.ELU()\n        )\n        \n        # Combined dimension: LSTM + GRU + Noise\n        combined_dim = hidden_dim * 2 + 16\n        \n        # Attention mechanism with LayerNorm for stability\n        self.attention_layernorm = nn.LayerNorm(combined_dim)\n        self.attention = nn.Sequential(\n            nn.Linear(combined_dim, hidden_dim),\n            nn.Tanh(),\n            nn.Linear(hidden_dim, 1)\n        )\n        \n        # Classification head (\u6bb5\u968e\u7684Dropout)\n        self.classifier = nn.Sequential(\n            nn.Linear(combined_dim, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.5),  # \u9ad8\u3081\n            \n            nn.Linear(256, 128),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.Dropout(0.3),  # \u4e2d\u7a0b\u5ea6\n            \n            nn.Linear(128, num_classes)\n        )\n        \n        # Binary BFRB classifier\n        self.bfrb_classifier = nn.Sequential(\n            nn.Linear(combined_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(64, 1)\n        )\n        \n        # Modality Dropout rate (v5.1\u8ffd\u52a0)\n        self.modality_dropout_rate = modality_dropout_rate\n        \n        # \u91cd\u307f\u521d\u671f\u5316\n        self._init_weights()\n    \n    def forward(self, x, return_features=False):\n        batch_size, seq_len, _ = x.size()\n        \n        # Split sensors\n        # IMU: 11\u6b21\u5143\uff087 + 4\u7279\u5fb4\u91cf\uff09\n        # ToF\u7d71\u8a08\u91cf: 20\u6b21\u5143 + \u7a7a\u9593\u7279\u5fb4\u91cf: 30\u6b21\u5143 = 50\u6b21\u5143\uff08v5\u3067\u62e1\u5f35\uff09\n        # Thermopile: 5\u6b21\u5143\n        imu = x[:, :, :11].transpose(1, 2)  # (B, C, T)\n        tof_thermal = x[:, :, 11:].transpose(1, 2)  # (B, C, T)\n        \n        # === Modality Dropout (v5.1) ===\n        # \u5b66\u7fd2\u6642\u306b\u6307\u5b9a\u78ba\u7387\u3067ToF/Thermal\u3092\u30c9\u30ed\u30c3\u30d7\uff08CMI\u5236\u7d04\u5bfe\u5fdc\uff09\n        # \u30b5\u30f3\u30d7\u30eb\u5358\u4f4d\u3067\u30c9\u30ed\u30c3\u30d7\u30a2\u30a6\u30c8\uff08\u3088\u308a\u5b89\u5b9a\u3057\u305f\u5b66\u7fd2\uff09\n        if self.training and self.modality_dropout_rate > 0:\n            # Create a mask for samples in the batch to drop\n            dropout_mask = (torch.rand(batch_size, 1, 1, device=x.device) < self.modality_dropout_rate)\n            tof_thermal = tof_thermal.masked_fill(dropout_mask, 0)\n        \n        # Branch processing\n        imu_feat = self.imu_branch(imu)  # (B, 128, T')\n        tof_feat = self.tof_branch(tof_thermal)  # (B, 128, T')\n        \n        # Ensure same temporal dimension\n        min_len = min(imu_feat.size(2), tof_feat.size(2))\n        imu_feat = imu_feat[:, :, :min_len]\n        tof_feat = tof_feat[:, :, :min_len]\n        \n        # Merge\n        merged = torch.cat([imu_feat, tof_feat], dim=1)  # (B, 256, T')\n        merged = merged.transpose(1, 2)  # (B, T', 256)\n        \n        # Parallel RNN processing\n        lstm_out, _ = self.lstm(merged)  # (B, T', hidden_dim)\n        gru_out, _ = self.gru(merged)  # (B, T', hidden_dim)\n        \n        # Noise branch (global pooling + dense)\n        merged_with_noise = self.gaussian_noise(merged)\n        noise_feat = F.adaptive_avg_pool1d(merged_with_noise.transpose(1, 2), 1).squeeze(-1)\n        noise_out = self.noise_branch(noise_feat).unsqueeze(1).expand(-1, merged.size(1), -1)\n        \n        # Combine all features\n        combined = torch.cat([lstm_out, gru_out, noise_out], dim=-1)  # (B, T', combined_dim)\n        \n        # Attention with stability improvements\n        combined = self.attention_layernorm(combined)  # LayerNorm for stability\n        attention_scores = self.attention(combined)  # (B, T', 1)\n        # Scale attention scores to prevent softmax overflow\n        import math\n        attention_scores = attention_scores / math.sqrt(combined.size(-1))\n        attention_weights = F.softmax(attention_scores, dim=1)\n        \n        # Weighted sum\n        context = torch.sum(combined * attention_weights, dim=1)  # (B, combined_dim)\n        \n        if return_features:\n            return context\n        \n        # Classification\n        class_logits = self.classifier(context)\n        bfrb_logits = self.bfrb_classifier(context)\n        \n        return class_logits, bfrb_logits\n    \n    def _init_weights(self):\n        \"\"\"\u91cd\u307f\u521d\u671f\u5316\"\"\"\n        for m in self.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.xavier_uniform_(m.weight)\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n            elif isinstance(m, nn.Conv1d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n            elif isinstance(m, nn.BatchNorm1d):\n                nn.init.ones_(m.weight)\n                nn.init.zeros_(m.bias)\n            elif isinstance(m, (nn.LSTM, nn.GRU)):\n                for param in m.parameters():\n                    if len(param.shape) >= 2:\n                        nn.init.orthogonal_(param.data)\n                    else:\n                        nn.init.zeros_(param.data)"}, {"cell_type": "markdown", "metadata": {}, "source": "## Model Definition"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# SimpleTwoBranchLight v5\u30e2\u30c7\u30eb\u5b9a\u7fa9\nclass GaussianNoise(nn.Module):\n    \"\"\"\u30ac\u30a6\u30b7\u30a2\u30f3\u30ce\u30a4\u30ba\u30ec\u30a4\u30e4\u30fc\uff08\u5b66\u7fd2\u6642\u306e\u307f\u9069\u7528\uff09\"\"\"\n    def __init__(self, sigma=0.09):\n        super().__init__()\n        self.sigma = sigma\n        self.register_buffer('noise', torch.tensor(0))\n    \n    def forward(self, x):\n        if self.training and self.sigma > 0:\n            sampled_noise = self.noise.expand(*x.size()).float().normal_(mean=0, std=self.sigma)\n            x = x + sampled_noise\n        return x\n\n\nclass SEBlock(nn.Module):\n    \"\"\"Squeeze-and-Excitation Block\"\"\"\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.avg_pool = nn.AdaptiveAvgPool1d(1)\n        self.fc = nn.Sequential(\n            nn.Linear(channels, channels // reduction, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Linear(channels // reduction, channels, bias=False),\n            nn.Sigmoid()\n        )\n    \n    def forward(self, x):\n        b, c, _ = x.size()\n        y = self.avg_pool(x).view(b, c)\n        y = self.fc(y).view(b, c, 1)\n        return x * y.expand_as(x)\n\n\nclass ResidualSECNNBlock(nn.Module):\n    \"\"\"\u6b8b\u5dee\u63a5\u7d9a\u4ed8\u304dSE-CNN\u30d6\u30ed\u30c3\u30af\"\"\"\n    def __init__(self, in_channels, out_channels, kernel_size=3, dropout=0.1):\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size, padding=kernel_size//2)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size, padding=kernel_size//2)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n        self.se = SEBlock(out_channels)\n        self.dropout = nn.Dropout(dropout)\n        \n        # Skip connection\n        self.skip = nn.Identity() if in_channels == out_channels else \\\n                    nn.Conv1d(in_channels, out_channels, 1)\n    \n    def forward(self, x):\n        residual = x\n        \n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.dropout(out)\n        out = self.bn2(self.conv2(out))\n        out = self.se(out)\n        \n        out += self.skip(residual)\n        out = F.relu(out)\n        \n        return out\n\n\nclass SimpleTwoBranchLight(nn.Module):\n    \"\"\"\n    SimpleTwoBranchLight v5 - \u6539\u826f\u7248Two-Branch\u30e2\u30c7\u30eb\n    \n    v5\u306e\u4e3b\u306a\u6539\u826f\u70b9:\n    - Modality Dropout: \u5b66\u7fd2\u664250%\u3067ToF/Thermal\u3092\u30c9\u30ed\u30c3\u30d7\uff08CMI\u5236\u7d04\u5bfe\u5fdc\uff09\n    - IMU\u5165\u529b\u6b21\u5143: 7 \u2192 11\uff08\u7279\u5fb4\u91cf\u30a8\u30f3\u30b8\u30cb\u30a2\u30ea\u30f3\u30b0\u8ffd\u52a0\uff09\n    - ToF\u5165\u529b\u6b21\u5143: 320 \u2192 50\uff08\u7d71\u8a08\u91cf20 + \u7a7a\u9593\u7279\u5fb4\u91cf30\uff09\n    - Dropout: \u6bb5\u968e\u7684\u8abf\u6574\uff080.1\u21920.3\u21920.5\uff09\n    - RNN: 1\u5c64\u306e\u307f\uff08\u30b7\u30f3\u30d7\u30eb\u5316\uff09\n    \"\"\"\n    \n    def __init__(self, imu_dim=11, tof_dim=50, thermopile_dim=5, \n                 hidden_dim=256, num_classes=18, modality_dropout_rate=0.5):\n        super().__init__()\n        \n        # IMU Branch (ResidualSECNNBlock\u4f7f\u7528) - v5.6: \u5bb9\u91cf\u3092\u5897\u5f37\n        self.imu_branch = nn.Sequential(\n            ResidualSECNNBlock(imu_dim, 128, dropout=0.1),  # 64\u2192128\u306b\u5897\u5f37\n            nn.MaxPool1d(2),\n            ResidualSECNNBlock(128, 256, kernel_size=5, dropout=0.1),  # 128\u2192256\u306b\u5897\u5f37\n            nn.MaxPool1d(2)\n        )\n        \n        # ToF/Thermal Branch (\u30b7\u30f3\u30d7\u30eb\u306aCNN)\n        tof_thermal_dim = tof_dim + thermopile_dim  # 50 + 5 = 55 (v5\u3067\u62e1\u5f35)\n        self.tof_branch = nn.Sequential(\n            nn.Conv1d(tof_thermal_dim, 64, 3, padding=1),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.MaxPool1d(2),\n            nn.Dropout(0.2),\n            \n            nn.Conv1d(64, 128, 3, padding=1),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.MaxPool1d(2),\n            nn.Dropout(0.2)\n        )\n        \n        # Merge dimension\n        merged_dim = 384  # 256 + 128 (IMU\u5f37\u5316\u306b\u3088\u308a\u5897\u52a0)\n        \n        # Parallel RNN (1\u5c64\u306e\u307f\u3001LB 0.76\u6e96\u62e0)\n        self.lstm = nn.LSTM(\n            merged_dim, hidden_dim // 2, \n            num_layers=1,  # 1\u5c64\u306b\u524a\u6e1b\n            batch_first=True, \n            dropout=0,  # 1\u5c64\u306a\u306e\u3067LSTM\u5185dropout\u306f0\n            bidirectional=True\n        )\n        \n        self.gru = nn.GRU(\n            merged_dim, hidden_dim // 2,\n            num_layers=1,\n            batch_first=True,\n            dropout=0,\n            bidirectional=True\n        )\n        \n        # Noise branch (LB 0.76\u6e96\u62e0)\n        self.gaussian_noise = GaussianNoise(0.09)\n        self.noise_branch = nn.Sequential(\n            nn.Linear(merged_dim, 16),\n            nn.ELU()\n        )\n        \n        # Combined dimension: LSTM + GRU + Noise\n        combined_dim = hidden_dim * 2 + 16\n        \n        # Attention mechanism with LayerNorm for stability\n        self.attention_layernorm = nn.LayerNorm(combined_dim)\n        self.attention = nn.Sequential(\n            nn.Linear(combined_dim, hidden_dim),\n            nn.Tanh(),\n            nn.Linear(hidden_dim, 1)\n        )\n        \n        # Classification head (\u6bb5\u968e\u7684Dropout)\n        self.classifier = nn.Sequential(\n            nn.Linear(combined_dim, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(0.5),  # \u9ad8\u3081\n            \n            nn.Linear(256, 128),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.Dropout(0.3),  # \u4e2d\u7a0b\u5ea6\n            \n            nn.Linear(128, num_classes)\n        )\n        \n        # Binary BFRB classifier\n        self.bfrb_classifier = nn.Sequential(\n            nn.Linear(combined_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.4),\n            nn.Linear(64, 1)\n        )\n        \n        # Modality Dropout rate (v5.1\u8ffd\u52a0)\n        self.modality_dropout_rate = modality_dropout_rate\n        \n        # \u91cd\u307f\u521d\u671f\u5316\n        self._init_weights()\n    \n    def forward(self, x, return_features=False):\n        batch_size, seq_len, _ = x.size()\n        \n        # Split sensors\n        # IMU: 11\u6b21\u5143\uff087 + 4\u7279\u5fb4\u91cf\uff09\n        # ToF\u7d71\u8a08\u91cf: 20\u6b21\u5143 + \u7a7a\u9593\u7279\u5fb4\u91cf: 30\u6b21\u5143 = 50\u6b21\u5143\uff08v5\u3067\u62e1\u5f35\uff09\n        # Thermopile: 5\u6b21\u5143\n        imu = x[:, :, :11].transpose(1, 2)  # (B, C, T)\n        tof_thermal = x[:, :, 11:].transpose(1, 2)  # (B, C, T)\n        \n        # === Modality Dropout (v5.1) ===\n        # \u5b66\u7fd2\u6642\u306b\u6307\u5b9a\u78ba\u7387\u3067ToF/Thermal\u3092\u30c9\u30ed\u30c3\u30d7\uff08CMI\u5236\u7d04\u5bfe\u5fdc\uff09\n        # \u30b5\u30f3\u30d7\u30eb\u5358\u4f4d\u3067\u30c9\u30ed\u30c3\u30d7\u30a2\u30a6\u30c8\uff08\u3088\u308a\u5b89\u5b9a\u3057\u305f\u5b66\u7fd2\uff09\n        if self.training and self.modality_dropout_rate > 0:\n            # Create a mask for samples in the batch to drop\n            dropout_mask = (torch.rand(batch_size, 1, 1, device=x.device) < self.modality_dropout_rate)\n            tof_thermal = tof_thermal.masked_fill(dropout_mask, 0)\n        \n        # Branch processing\n        imu_feat = self.imu_branch(imu)  # (B, 256, T') - v5.6\u3067\u5897\u5f37\n        tof_feat = self.tof_branch(tof_thermal)  # (B, 128, T')\n        \n        # Ensure same temporal dimension\n        min_len = min(imu_feat.size(2), tof_feat.size(2))\n        imu_feat = imu_feat[:, :, :min_len]\n        tof_feat = tof_feat[:, :, :min_len]\n        \n        # Merge\n        merged = torch.cat([imu_feat, tof_feat], dim=1)  # (B, 384, T') - v5.6\n        merged = merged.transpose(1, 2)  # (B, T', 384)\n        \n        # Parallel RNN processing\n        lstm_out, _ = self.lstm(merged)  # (B, T', hidden_dim)\n        gru_out, _ = self.gru(merged)  # (B, T', hidden_dim)\n        \n        # Noise branch (global pooling + dense)\n        merged_with_noise = self.gaussian_noise(merged)\n        noise_feat = F.adaptive_avg_pool1d(merged_with_noise.transpose(1, 2), 1).squeeze(-1)\n        noise_out = self.noise_branch(noise_feat).unsqueeze(1).expand(-1, merged.size(1), -1)\n        \n        # Combine all features\n        combined = torch.cat([lstm_out, gru_out, noise_out], dim=-1)  # (B, T', combined_dim)\n        \n        # Attention with stability improvements\n        combined = self.attention_layernorm(combined)  # LayerNorm for stability\n        attention_scores = self.attention(combined)  # (B, T', 1)\n        # Scale attention scores to prevent softmax overflow\n        import math\n        attention_scores = attention_scores / math.sqrt(combined.size(-1))\n        attention_weights = F.softmax(attention_scores, dim=1)\n        \n        # Weighted sum\n        context = torch.sum(combined * attention_weights, dim=1)  # (B, combined_dim)\n        \n        if return_features:\n            return context\n        \n        # Classification\n        class_logits = self.classifier(context)\n        bfrb_logits = self.bfrb_classifier(context)\n        \n        return class_logits, bfrb_logits\n    \n    def _init_weights(self):\n        \"\"\"\u91cd\u307f\u521d\u671f\u5316\"\"\"\n        for m in self.modules():\n            if isinstance(m, nn.Linear):\n                nn.init.xavier_uniform_(m.weight)\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n            elif isinstance(m, nn.Conv1d):\n                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')\n                if m.bias is not None:\n                    nn.init.zeros_(m.bias)\n            elif isinstance(m, nn.BatchNorm1d):\n                nn.init.ones_(m.weight)\n                nn.init.zeros_(m.bias)\n            elif isinstance(m, (nn.LSTM, nn.GRU)):\n                for param in m.parameters():\n                    if len(param.shape) >= 2:\n                        nn.init.orthogonal_(param.data)\n                    else:\n                        nn.init.zeros_(param.data)"}, {"cell_type": "markdown", "metadata": {}, "source": "## Configuration"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# Configuration\nclass Config:\n    # v5\u30e2\u30c7\u30eb\u306e\u30d1\u30b9\uff08\u5b66\u7fd2\u30ce\u30fc\u30c8\u30d6\u30c3\u30af\u306e\u51fa\u529b\uff09\n    model_base_path = Path('/kaggle/input/cmi-bfrb-simple-two-branch-light-v5-training')\n    \n    # Model parameters\n    sequence_length = 150\n    imu_dim = 11  # v5\u3067\u306f11\u6b21\u5143\n    tof_dim = 50  # v5\u3067\u306f50\u6b21\u5143\uff08\u7d71\u8a08\u91cf20 + \u7a7a\u9593\u7279\u5fb4\u91cf30\uff09\n    thermopile_dim = 5\n    hidden_dim = 256\n    num_classes = 18\n    batch_size = 32\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # 5-fold models\n    n_folds = 5\n\nconfig = Config()\nprint(f\"Device: {config.device}\")\nprint(f\"Model base path: {config.model_base_path}\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## Helper Classes"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "class CMIInferenceDataset(Dataset):\n    \"\"\"\u63a8\u8ad6\u7528\u30c7\u30fc\u30bf\u30bb\u30c3\u30c8\"\"\"\n    def __init__(self, sequences, sequence_ids, sequence_length=150):\n        self.sequences = sequences\n        self.sequence_ids = sequence_ids\n        self.sequence_length = sequence_length\n    \n    def __len__(self):\n        return len(self.sequences)\n    \n    def __getitem__(self, idx):\n        seq = self.sequences[idx]\n        seq_id = self.sequence_ids[idx]\n        \n        # \u30d1\u30c7\u30a3\u30f3\u30b0/\u30c8\u30e9\u30f3\u30b1\u30fc\u30c8\n        if len(seq) < self.sequence_length:\n            pad_length = self.sequence_length - len(seq)\n            seq = np.pad(seq, ((0, pad_length), (0, 0)), mode='constant')\n        else:\n            seq = seq[-self.sequence_length:]\n        \n        return torch.FloatTensor(seq), seq_id"}, {"cell_type": "markdown", "metadata": {}, "source": "## Model Loading (5-fold ensemble)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# \u30b0\u30ed\u30fc\u30d0\u30eb\u5909\u6570\nmodels = []\nscalers = []\nlabel_encoders = []\nMODEL_LOADED = False\n\nprint(\"=== Loading v5 models (5-fold ensemble) ===\")\n\n# CV\u7d50\u679c\u306e\u8aad\u307f\u8fbc\u307f\ncv_results_path = config.model_base_path / 'models' / 'cv_results_v5_convergence.json'\nif not cv_results_path.exists():\n    # \u30d5\u30e9\u30c3\u30c8\u5316\u3055\u308c\u3066\u3044\u308b\u53ef\u80fd\u6027\n    cv_results_path = config.model_base_path / 'cv_results_v5_convergence.json'\n\nif cv_results_path.exists():\n    with open(cv_results_path, 'r') as f:\n        cv_results = json.load(f)\n    print(f\"CV Mean Score: {cv_results['mean_score']:.4f} \u00b1 {cv_results['std_score']:.4f}\")\n    print(f\"Individual fold scores: {cv_results['cv_scores']}\")\n\n# \u5404fold\u306e\u30e2\u30c7\u30eb\u3092\u8aad\u307f\u8fbc\u307f\nfor fold in range(config.n_folds):\n    # \u30e2\u30c7\u30eb\u30d1\u30b9\u306e\u5019\u88dc\n    model_paths = [\n        config.model_base_path / 'models' / f'simple_two_branch_light_v5_convergence_fold{fold}_best.pth',\n        config.model_base_path / f'simple_two_branch_light_v5_convergence_fold{fold}_best.pth'\n    ]\n    \n    model_loaded = False\n    for model_path in model_paths:\n        if model_path.exists():\n            try:\n                print(f\"\\nLoading fold {fold} from: {model_path}\")\n                \n                # \u30c1\u30a7\u30c3\u30af\u30dd\u30a4\u30f3\u30c8\u3092\u8aad\u307f\u8fbc\u307f\n                checkpoint = torch.load(model_path, map_location=config.device, weights_only=False)\n                \n                # \u30e2\u30c7\u30eb\u4f5c\u6210\n                model = SimpleTwoBranchLight(\n                    imu_dim=config.imu_dim,\n                    tof_dim=config.tof_dim,\n                    thermopile_dim=config.thermopile_dim,\n                    hidden_dim=config.hidden_dim,\n                    num_classes=config.num_classes,\n                    modality_dropout_rate=0.0  # \u63a8\u8ad6\u6642\u306f\u30c9\u30ed\u30c3\u30d7\u30a2\u30a6\u30c8\u306a\u3057\n                ).to(config.device)\n                \n                # \u91cd\u307f\u3092\u8aad\u307f\u8fbc\u307f\n                model.load_state_dict(checkpoint['model_state_dict'])\n                model.eval()\n                models.append(model)\n                \n                # \u30b9\u30b1\u30fc\u30e9\u30fc\u3068\u30a8\u30f3\u30b3\u30fc\u30c0\u30fc\n                scalers.append(checkpoint['scaler'])\n                if fold == 0:  # \u6700\u521d\u306efold\u304b\u3089\u30e9\u30d9\u30eb\u30a8\u30f3\u30b3\u30fc\u30c0\u30fc\u3092\u53d6\u5f97\n                    label_encoder = checkpoint['label_encoder']\n                    print(f\"Classes: {label_encoder.classes_}\")\n                \n                print(f\"\u2713 Fold {fold} loaded - Val F1: {checkpoint.get('best_val_f1', 'N/A'):.4f}\")\n                model_loaded = True\n                break\n                \n            except Exception as e:\n                print(f\"Error loading model from {model_path}: {e}\")\n    \n    if not model_loaded:\n        print(f\"Warning: Fold {fold} model not found\")\n\nMODEL_LOADED = len(models) > 0\n\nif not MODEL_LOADED:\n    print(\"\\nError: No models loaded!\")\n    print(\"Using fallback configuration...\")\n    \n    # \u30d5\u30a9\u30fc\u30eb\u30d0\u30c3\u30af\u8a2d\u5b9a\n    scaler = StandardScaler()\n    dummy_data = np.random.randn(100, 66)  # v5\u306f66\u6b21\u5143\n    scaler.fit(dummy_data)\n    scalers = [scaler]\n    \n    label_encoder = LabelEncoder()\n    label_encoder.fit(['Above ear - pull hair', 'Cheek - pinch skin', 'Drink from bottle/cup',\n                      'Eyebrow - pull hair', 'Eyelash - pull hair', 'Feel around in tray and pull out an object',\n                      'Forehead - pull hairline', 'Forehead - scratch', 'Glasses on/off',\n                      'Neck - pinch skin', 'Neck - scratch', 'Pinch knee/leg skin',\n                      'Pull air toward your face', 'Scratch knee/leg skin', 'Text on phone',\n                      'Wave hello', 'Write name in air', 'Write name on leg'])\nelse:\n    print(f\"\\n\u2713 Successfully loaded {len(models)} models for ensemble\")\n\nprint(f\"\\nModel loaded: {MODEL_LOADED}\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## Prediction Function (with 5-fold ensemble)"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    CMI\u8a55\u4fa1API\u7528\u306e\u63a8\u8ad6\u95a2\u6570\uff08v2\u5f62\u5f0f\uff09\n    5-fold\u30a2\u30f3\u30b5\u30f3\u30d6\u30eb\u3067\u4e88\u6e2c\n    \n    Args:\n        sequence: \u30bb\u30f3\u30b5\u30fc\u30c7\u30fc\u30bf\uff08Polars DataFrame\uff09\n        demographics: \u30c7\u30e2\u30b0\u30e9\u30d5\u30a3\u30c3\u30af\u60c5\u5831\uff08Polars DataFrame\uff09\n        \n    Returns:\n        \u4e88\u6e2c\u3055\u308c\u305f\u30b8\u30a7\u30b9\u30c1\u30e3\u30fc\u540d\n    \"\"\"\n    global models, scalers, label_encoder, MODEL_LOADED\n    \n    try:\n        # \u30e2\u30c7\u30eb\u304c\u30ed\u30fc\u30c9\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\n        if not MODEL_LOADED:\n            return 'Wave hello'\n        \n        # Polars DataFrame\u3092Pandas\u306b\u5909\u63db\n        test_df = sequence.to_pandas() if isinstance(sequence, pl.DataFrame) else sequence\n        \n        # \u30b7\u30fc\u30b1\u30f3\u30b9ID\u3092\u53d6\u5f97\n        seq_id = test_df['sequence_id'].iloc[0] if 'sequence_id' in test_df.columns else 'unknown'\n        \n        # --- \u7279\u5fb4\u91cf\u30a8\u30f3\u30b8\u30cb\u30a2\u30ea\u30f3\u30b0 ---\n        # NaN\u88dc\u5b8c\n        test_df = impute_sensor_nans(test_df)\n        \n        # IMU\u7279\u5fb4\u91cf\u8ffd\u52a0\n        test_df = add_imu_features(test_df)\n        \n        # ToF\u7d71\u8a08\u91cf\u96c6\u7d04\n        test_df, _ = aggregate_tof_features(test_df)\n        \n        # ToF\u7a7a\u9593\u7279\u5fb4\u91cf\u8ffd\u52a0\uff08v5\u65b0\u6a5f\u80fd\uff09\n        test_df, _ = add_tof_spatial_features(test_df)\n        \n        # \u7279\u5fb4\u91cf\u30ab\u30e9\u30e0\u306e\u53d6\u5f97\n        _, _, _, _, all_feature_cols = get_feature_columns()\n        \n        # \u7279\u5fb4\u91cf\u62bd\u51fa\n        seq_features = test_df[all_feature_cols].values\n        \n        # NaN/inf\u30c1\u30a7\u30c3\u30af\n        seq_features = np.nan_to_num(seq_features, nan=0.0, posinf=0.0, neginf=0.0)\n        \n        # 5-fold\u30a2\u30f3\u30b5\u30f3\u30d6\u30eb\u4e88\u6e2c\n        all_probs = []\n        \n        for fold_idx, (model, scaler) in enumerate(zip(models, scalers)):\n            # \u30b9\u30b1\u30fc\u30ea\u30f3\u30b0\n            seq_normalized = scaler.transform(seq_features)\n            \n            # \u30c7\u30fc\u30bf\u30bb\u30c3\u30c8\u4f5c\u6210\n            dataset = CMIInferenceDataset(\n                [seq_normalized],\n                [seq_id],\n                sequence_length=config.sequence_length\n            )\n            \n            dataloader = DataLoader(\n                dataset, batch_size=1, shuffle=False, num_workers=0\n            )\n            \n            # \u4e88\u6e2c\n            with torch.no_grad():\n                for batch_data, _ in dataloader:\n                    batch_data = batch_data.to(config.device)\n                    \n                    # \u30e2\u30c7\u30eb\u4e88\u6e2c\n                    class_logits, _ = model(batch_data)\n                    \n                    # \u78ba\u7387\u8a08\u7b97\n                    probabilities = F.softmax(class_logits, dim=1).cpu().numpy()\n                    all_probs.append(probabilities[0])\n        \n        # \u30a2\u30f3\u30b5\u30f3\u30d6\u30eb\uff08\u78ba\u7387\u306e\u5e73\u5747\uff09\n        ensemble_probs = np.mean(all_probs, axis=0)\n        prediction = np.argmax(ensemble_probs)\n        \n        # \u30b8\u30a7\u30b9\u30c1\u30e3\u30fc\u540d\u306b\u5909\u63db\n        gesture_name = label_encoder.inverse_transform([prediction])[0]\n        \n        # \u30b8\u30a7\u30b9\u30c1\u30e3\u30fc\u540d\u306e\u5f62\u5f0f\u3092\u78ba\u8a8d\uff08CMI API\u306f\u30a2\u30f3\u30c0\u30fc\u30b9\u30b3\u30a2\u5f62\u5f0f\u3092\u671f\u5f85\uff09\n        gesture_name = gesture_name.replace(' ', '_')\n        \n        return gesture_name\n        \n    except Exception as e:\n        print(f\"\u30a8\u30e9\u30fc\u767a\u751f: {str(e)}\")\n        import traceback\n        traceback.print_exc()\n        # \u30a8\u30e9\u30fc\u6642\u306f\u30c7\u30d5\u30a9\u30eb\u30c8\u4e88\u6e2c\n        return 'Wave_hello'\n\nprint(\"Predict function defined with 5-fold ensemble\")"}, {"cell_type": "markdown", "metadata": {}, "source": "## Start Inference Server"}, {"cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": "# CMI\u63a8\u8ad6\u30b5\u30fc\u30d0\u30fc\u306e\u8d77\u52d5\nimport os\nfrom kaggle_evaluation.cmi_inference_server import CMIInferenceServer\n\n# \u63a8\u8ad6\u30b5\u30fc\u30d0\u30fc\u306e\u4f5c\u6210\nprint(\"\u63a8\u8ad6\u30b5\u30fc\u30d0\u30fc\u3092\u4f5c\u6210\u4e2d...\")\ninference_server = CMIInferenceServer(predict)\n\nprint(\"\\n=== SimpleTwoBranchLight v5 \u63a8\u8ad6\u6e96\u5099\u5b8c\u4e86 ===\")\nprint(f\"\u30e2\u30c7\u30eb\u8aad\u307f\u8fbc\u307f\u72b6\u614b: {MODEL_LOADED}\")\nif MODEL_LOADED:\n    print(f\"{len(models)}-fold \u30a2\u30f3\u30b5\u30f3\u30d6\u30eb\u3067\u63a8\u8ad6\u3092\u884c\u3044\u307e\u3059\")\nelse:\n    print(\"\u6ce8\u610f: \u30e2\u30c7\u30eb\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u3089\u306a\u3044\u305f\u3081\u3001\u30c7\u30d5\u30a9\u30eb\u30c8\u4e88\u6e2c\u3092\u8fd4\u3057\u307e\u3059\")\n\n# \u74b0\u5883\u306b\u5fdc\u3058\u3066\u5b9f\u884c\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print(\"\\nKaggle\u7af6\u6280\u74b0\u5883\u3092\u691c\u51fa\u3002\u63a8\u8ad6\u30b5\u30fc\u30d0\u30fc\u3092\u958b\u59cb\u3057\u307e\u3059\u3002\")\n    inference_server.serve()\nelse:\n    print(\"\\n\u63a8\u8ad6\u30b5\u30fc\u30d0\u30fc\u3092\u5b9f\u884c\u3057\u307e\u3059...\")\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )"}], "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.10.12"}}, "nbformat": 4, "nbformat_minor": 4}