{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12924607,"sourceType":"datasetVersion","datasetId":7918617}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# CMI SOLUTION","metadata":{}},{"cell_type":"code","source":"# Cell 1: Import Libraries\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport pandas as pd\nimport numpy as np\nfrom pathlib import Path\nimport json\nimport joblib\nimport math\nimport warnings\nfrom sklearn.preprocessing import RobustScaler\n\nwarnings.filterwarnings('ignore')\n\n\n# Cell 2: Configuration\n# Ensure this matches the configuration used during training\nclass Config:\n    seed = 42\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Model parameters from training\n    imu_d_model = 128\n    thermopile_d_model = 64\n    tof_d_model = 256\n    transformer_nhead = 8\n    transformer_dim_feedforward = 512\n    transformer_num_layers = 4\n    transformer_dropout = 0.1\n    \n    # Augmentation parameters for TTA\n    augment_prob = 0.5\n    noise_scale = 0.05\n    time_warp_ratio = 0.2\n    \n    # TTA settings\n    tta_iterations = 5\n\nconfig = Config()\ntorch.manual_seed(config.seed)\nnp.random.seed(config.seed)\n\n\n# Cell 3: Model and Helper Class Definitions\n# These classes must be defined so torch.load can reconstruct the model object.\n\nclass PositionalEncoding(nn.Module):\n    def __init__(self, d_model: int, dropout: float = 0.1, max_len: int = 5000):\n        super().__init__()\n        self.dropout = nn.Dropout(p=dropout)\n        position = torch.arange(max_len).unsqueeze(1)\n        div_term = torch.exp(torch.arange(0, d_model, 2) * (-math.log(10000.0) / d_model))\n        pe = torch.zeros(max_len, 1, d_model)\n        pe[:, 0, 0::2] = torch.sin(position * div_term)\n        pe[:, 0, 1::2] = torch.cos(position * div_term)\n        self.register_buffer('pe', pe)\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        x = x + self.pe[:x.size(0)]\n        return self.dropout(x)\n\nclass SensorTransformer(nn.Module):\n    def __init__(self, input_dim, d_model, nhead, dim_feedforward, num_layers, dropout=0.1):\n        super().__init__()\n        self.input_proj = nn.Linear(input_dim, d_model)\n        self.pos_encoder = PositionalEncoding(d_model, dropout)\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward,\n            dropout=dropout, batch_first=True\n        )\n        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n        self.d_model = d_model\n    \n    def forward(self, x, attention_mask=None):\n        x = self.input_proj(x) * math.sqrt(self.d_model)\n        x = self.pos_encoder(x)\n        if attention_mask is not None:\n            key_padding_mask = (attention_mask == 0)\n            x = self.transformer_encoder(x, src_key_padding_mask=key_padding_mask)\n        else:\n            x = self.transformer_encoder(x)\n        if attention_mask is not None:\n            seq_lengths = attention_mask.sum(dim=1).long() - 1\n            batch_indices = torch.arange(x.size(0), device=x.device)\n            x = x[batch_indices, seq_lengths]\n        else:\n            x = x[:, -1, :]\n        return x\n\nclass AdvancedSensorModel(nn.Module):\n    def __init__(self, config, num_classes=18): # num_classes should match your trained model\n        super().__init__()\n        self.config = config\n        self.imu_transformer = SensorTransformer(\n            input_dim=7, d_model=config.imu_d_model, nhead=config.transformer_nhead,\n            dim_feedforward=config.transformer_dim_feedforward, num_layers=config.transformer_num_layers,\n            dropout=config.transformer_dropout\n        )\n        self.thermopile_transformer = SensorTransformer(\n            input_dim=5, d_model=config.thermopile_d_model, nhead=config.transformer_nhead,\n            dim_feedforward=config.transformer_dim_feedforward, num_layers=config.transformer_num_layers,\n            dropout=config.transformer_dropout\n        )\n        self.tof_transformer = SensorTransformer(\n            input_dim=320, d_model=config.tof_d_model, nhead=config.transformer_nhead,\n            dim_feedforward=config.transformer_dim_feedforward, num_layers=config.transformer_num_layers,\n            dropout=config.transformer_dropout\n        )\n        self.fusion_attention = nn.MultiheadAttention(\n            embed_dim=config.imu_d_model + config.thermopile_d_model + config.tof_d_model,\n            num_heads=4, dropout=config.transformer_dropout, batch_first=True\n        )\n        self.classifier = nn.Sequential(\n            nn.Linear(config.imu_d_model + config.thermopile_d_model + config.tof_d_model, 512),\n            nn.BatchNorm1d(512), nn.GELU(), nn.Dropout(0.3),\n            nn.Linear(512, 256), nn.BatchNorm1d(256), nn.GELU(), nn.Dropout(0.2),\n            nn.Linear(256, 128), nn.BatchNorm1d(128), nn.GELU(), nn.Dropout(0.1),\n            nn.Linear(128, num_classes)\n        )\n\n    def forward(self, imu, thermopile, tof, attention_mask=None, demographics=None):\n        imu_features = self.imu_transformer(imu, attention_mask)\n        thermopile_features = self.thermopile_transformer(thermopile, attention_mask)\n        tof_features = self.tof_transformer(tof, attention_mask)\n        combined_features = torch.cat([imu_features, thermopile_features, tof_features], dim=1)\n        attn_output, _ = self.fusion_attention(\n            combined_features.unsqueeze(1), combined_features.unsqueeze(1), combined_features.unsqueeze(1)\n        )\n        fused_features = attn_output.squeeze(1)\n        output = self.classifier(fused_features)\n        return output\n\nclass ModelEnsemble:\n    def __init__(self, models, config):\n        self.models = models\n        self.config = config\n        for model in self.models:\n            model.to(config.device)\n            model.eval()\n            \n    def predict(self, imu, thermopile, tof, attention_mask):\n        all_outputs = []\n        with torch.no_grad():\n            for model in self.models:\n                output = model(imu, thermopile, tof, attention_mask)\n                all_outputs.append(output)\n        avg_output = torch.mean(torch.stack(all_outputs), dim=0)\n        return avg_output\n\nclass SensorDataAugmentation:\n    def __init__(self, config):\n        self.config = config\n    \n    def add_gaussian_noise(self, data):\n        if torch.rand(1).item() < self.config.augment_prob:\n            noise = torch.randn_like(data) * self.config.noise_scale\n            return data + noise\n        return data\n        \n    def time_warp(self, data):\n        if torch.rand(1).item() < self.config.augment_prob:\n            seq_len, _ = data.shape\n            warp_points = int(seq_len * self.config.time_warp_ratio)\n            warp_indices = torch.randint(0, seq_len, (warp_points,))\n            for idx in warp_indices:\n                warp_factor = torch.rand(1).item() * 0.4 + 0.8\n                warp_idx = min(int(idx * warp_factor), seq_len - 1)\n                data[idx] = data[warp_idx]\n        return data\n\n    def sensor_dropout(self, data, sensor_type):\n        if torch.rand(1).item() < self.config.augment_prob:\n            if sensor_type in ['thermopile', 'tof']:\n                dropout_prob = torch.rand(1).item() * 0.5\n                mask = torch.rand_like(data) > dropout_prob\n                data = data * mask.float()\n        return data\n    \n    def random_scaling(self, data):\n        if torch.rand(1).item() < self.config.augment_prob:\n            scale_factor = torch.rand(1).item() * 0.4 + 0.8\n            return data * scale_factor\n        return data\n\n    def channel_shuffle(self, data):\n        if torch.rand(1).item() < self.config.augment_prob and data.shape[1] > 1:\n            perm = torch.randperm(data.shape[1])\n            return data[:, perm]\n        return data\n\n    def apply_augmentations(self, imu, thermopile, tof):\n        imu = self.add_gaussian_noise(imu)\n        imu = self.time_warp(imu)\n        imu = self.random_scaling(imu)\n        imu = self.channel_shuffle(imu)\n        thermopile = self.add_gaussian_noise(thermopile)\n        thermopile = self.sensor_dropout(thermopile, 'thermopile')\n        thermopile = self.random_scaling(thermopile)\n        thermopile = self.channel_shuffle(thermopile)\n        tof = self.add_gaussian_noise(tof)\n        tof = self.sensor_dropout(tof, 'tof')\n        tof = self.random_scaling(tof)\n        tof = self.channel_shuffle(tof)\n        return imu, thermopile, tof\n\n\n# Cell 4: Recreate Artifacts (Scalers and Gesture Map)\nprint(\"Recreating artifacts for inference...\")\n\n# 1. Recreate Gesture Map from training logs\ngesture_map = {\n    'Cheek - pinch skin': 0, 'Forehead - pull hairline': 1, 'Write name on leg': 2,\n    'Feel around in tray and pull out an object': 3, 'Neck - scratch': 4,\n    'Neck - pinch skin': 5, 'Eyelash - pull hair': 6, 'Eyebrow - pull hair': 7,\n    'Forehead - scratch': 8, 'Above ear - pull hair': 9, 'Wave hello': 10,\n    'Write name in air': 11, 'Text on phone': 12, 'Pull air toward your face': 13,\n    'Pinch knee/leg skin': 14, 'Scratch knee/leg skin': 15,\n    'Drink from bottle/cup': 16, 'Glasses on/off': 17\n}\nreverse_gesture_map = {v: k for k, v in gesture_map.items()}\nprint(f\"Gesture map recreated with {len(gesture_map)} classes.\")\n\n# 2. Re-fit Scalers using the original training data\n# This is much faster than re-training the entire model.\nprint(\"Loading training data to fit scalers...\")\ntrain_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\n\n# Define column groups\nimu_cols = [f'acc_{axis}' for axis in ['x', 'y', 'z']] + [f'rot_{axis}' for axis in ['w', 'x', 'y', 'z']]\nthermopile_cols = [f'thm_{i}' for i in range(1, 6)]\ntof_cols = [f'tof_{sensor}_v{i}' for sensor in range(1, 6) for i in range(64)]\n\n# Fit IMU scaler\nimu_scaler = RobustScaler()\nimu_scaler.fit(train_df[imu_cols].values)\n\n# Fit Thermopile scaler (with same NaN handling as training)\nthermopile_scaler = RobustScaler()\nthermopile_scaler.fit(train_df[thermopile_cols].fillna(-100).values)\n\n# Fit ToF scaler (with same NaN handling as training)\ntof_scaler = RobustScaler()\ntof_scaler.fit(train_df[tof_cols].fillna(0).values)\n\nscalers = {\n    'imu': imu_scaler,\n    'thermopile': thermopile_scaler,\n    'tof': tof_scaler\n}\nprint(\"Scalers successfully re-fitted.\")\n\n\n# Cell 5: Inference Preprocessing Function\ndef preprocess_for_inference(sequence_df: pd.DataFrame, scalers_dict: dict) -> tuple:\n    # IMU features\n    imu_data = scalers_dict['imu'].transform(sequence_df[imu_cols].values)\n    imu_data = np.nan_to_num(imu_data, nan=0.0, posinf=0.0, neginf=0.0)\n    \n    # Thermopile features\n    thermopile_data = sequence_df[thermopile_cols].fillna(-100).values\n    thermopile_data = scalers_dict['thermopile'].transform(thermopile_data)\n    thermopile_data = np.nan_to_num(thermopile_data, nan=0.0, posinf=0.0, neginf=0.0)\n    \n    # Time-of-flight features\n    tof_data = sequence_df[tof_cols].fillna(0).values\n    tof_data = scalers_dict['tof'].transform(tof_data)\n    tof_data = np.nan_to_num(tof_data, nan=0.0, posinf=0.0, neginf=0.0)\n    \n    # Convert to tensors\n    imu_tensor = torch.tensor(imu_data.astype(np.float32))\n    thermopile_tensor = torch.tensor(thermopile_data.astype(np.float32))\n    tof_tensor = torch.tensor(tof_data.astype(np.float32))\n    \n    return imu_tensor, thermopile_tensor, tof_tensor\n\n\n# Cell 6: Load Model\n# Make sure to replace '/kaggle/input/your-dataset-name/' with the correct path.\n# Cell 6: Load Model (Corrected)\n# Make sure to replace '/kaggle/input/your-dataset-name/' with the correct path.\nMODEL_PATH = Path('/kaggle/input/gaupardatamodel/sensor_model_ensemble.pth') # <<<--- CHANGE THIS\n\nprint(\"Loading trained model ensemble...\")\n\n# FIX: Added weights_only=False to allow loading the saved class object\nensemble_model = torch.load(MODEL_PATH, map_location=config.device, weights_only=False)\n\nprint(\"Ensemble model loaded and ready.\")\n\n# Initialize the augmentation class for TTA\naugmentor = SensorDataAugmentation(config)\n\n\n# Cell 7: Prediction Function (Corrected)\ndef predict(sequence: pd.DataFrame, demographics: pd.DataFrame) -> str:\n    \"\"\"\n    This is the main prediction function that will be called by the competition environment.\n    \"\"\"\n    # FIX: The competition environment provides a Polars DataFrame. Convert it to Pandas.\n    sequence = sequence.to_pandas()\n    \n    # Ensure all models in the ensemble are in evaluation mode\n    for model in ensemble_model.models:\n        model.eval()\n\n    # 1. Preprocess the input sequence\n    imu_tensor, thermopile_tensor, tof_tensor = preprocess_for_inference(sequence, scalers)\n    \n    # 2. Perform Test-Time Augmentation (TTA)\n    all_outputs = []\n    for _ in range(config.tta_iterations):\n        aug_imu, aug_thermopile, aug_tof = augmentor.apply_augmentations(\n            imu_tensor.clone(), thermopile_tensor.clone(), tof_tensor.clone()\n        )\n        \n        # Add batch dimension and move tensors to the correct device\n        aug_imu = aug_imu.unsqueeze(0).to(config.device)\n        aug_thermopile = aug_thermopile.unsqueeze(0).to(config.device)\n        aug_tof = aug_tof.unsqueeze(0).to(config.device)\n        \n        attention_mask = torch.ones(1, aug_imu.shape[1], device=config.device)\n\n        # 3. Predict with the ensemble\n        output = ensemble_model.predict(aug_imu, aug_thermopile, aug_tof, attention_mask)\n        all_outputs.append(output)\n        \n    # 4. Aggregate predictions\n    avg_output = torch.mean(torch.stack(all_outputs), dim=0)\n    prediction_idx = torch.argmax(avg_output, dim=1).cpu().item()\n    \n    # 5. Map the index back to the original gesture label\n    predicted_label = reverse_gesture_map[prediction_idx]\n    \n    return predicted_label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-31T18:52:32.528702Z","iopub.execute_input":"2025-08-31T18:52:32.529086Z","iopub.status.idle":"2025-08-31T18:53:26.622899Z","shell.execute_reply.started":"2025-08-31T18:52:32.529058Z","shell.execute_reply":"2025-08-31T18:53:26.619822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\nimport os\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\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    )\n\nif not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print(pd.read_parquet(\"submission.parquet\"))","metadata":{"trusted":true,"execution":{"execution_failed":"2025-08-31T18:45:58.579Z"}},"outputs":[],"execution_count":null}]}