{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12505161,"sourceType":"datasetVersion","datasetId":7892521}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import gc\nimport kaggle_evaluation.cmi_inference_server\nimport math\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport pandas as pd\nimport polars as pl\nimport plotly.express as px\nimport plotly.io as pio\nimport random\nimport time\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n\nfrom glob import glob\nfrom torch.utils.data import DataLoader, Dataset, Subset\nfrom tqdm import tqdm\n\n!mkdir output\n# os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"0,1\"\ndevice = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\npio.renderers.default = 'iframe'\nclass config:\n    BATCH_SIZE_TEST = 32\n    NUM_WORKERS = 4 # multiprocessing.cpu_count()\n    PRINT_FREQ = 20\n    SEED = 20\n\nclass paths:\n    OUTPUT_DIR = \"/kaggle/working/output\"\n    TEST_CSV = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\"\n    TEST_DEMOGRAPHICS = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\"\n    TRAIN_CSV = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\"\n    TRAIN_DEMOGRAPHICS = \"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\"\ndef format_for_scoring(df_preds: pd.DataFrame) ->tuple[pd.DataFrame, pd.DataFrame]: \n    solution = df_preds[[\"sequence_id\", \"y_true\"]].copy()\n    solution.columns = [\"id\", \"gesture\"]\n    solution[\"gesture\"] = solution[\"gesture\"].map(num_to_label)\n\n    submission = df_preds[[\"sequence_id\", \"y_pred\"]].copy()\n    submission.columns = [\"id\", \"gesture\"]\n    submission[\"gesture\"] = submission[\"gesture\"].map(num_to_label)\n    \n    return solution, submission\n\n\ndef seed_everything(seed: int):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed) \n    \n\ndef sep():\n    print(\"—\"*100)\n\n\nlabel_to_num = {\n    'Above ear - pull hair': 0,  # < ------- TARGETS START\n    'Cheek - pinch skin': 1,\n    'Eyebrow - pull hair': 2,\n    'Eyelash - pull hair': 3,\n    'Forehead - pull hairline': 4,\n    'Forehead - scratch': 5,\n    'Neck - pinch skin': 6,\n    'Neck - scratch': 7,  # < ------- TARGETS END\n    'Drink from bottle/cup': 8,  # < ------- NON-TARGETS START\n    'Feel around in tray and pull out an object': 9,\n    'Glasses on/off': 10,\n    'Pinch knee/leg skin': 11,\n    'Pull air toward your face': 12,\n    'Scratch knee/leg skin': 13,\n    'Text on phone': 14,\n    'Wave hello': 15,\n    'Write name in air': 16,\n    'Write name on leg': 17  # < ------- NON-TARGETS END\n}\ntype_to_num = {\"Target\": 1, \"Non-Target\":0}\nnum_to_label = {v: k for k, v in label_to_num.items()}\nnum_to_type = {v: k for k, v in type_to_num.items()}\nseed_everything(config.SEED)\ndf_test = pd.read_csv(paths.TEST_CSV)\ndf_test_demographics = pd.read_csv(paths.TEST_DEMOGRAPHICS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-18T06:09:54.951707Z","iopub.execute_input":"2025-07-18T06:09:54.952283Z","iopub.status.idle":"2025-07-18T06:10:01.350255Z","shell.execute_reply.started":"2025-07-18T06:09:54.952250Z","shell.execute_reply":"2025-07-18T06:10:01.349544Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from scipy.spatial.transform import Rotation as R\n\ndef remove_gravity_from_acc(acc_data, rot_data):\n\n    if isinstance(acc_data, pd.DataFrame):\n        acc_values = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n    else:\n        acc_values = acc_data\n\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = acc_values.shape[0]\n    linear_accel = np.zeros_like(acc_values)\n    \n    gravity_world = np.array([0, 0, 9.81])\n\n    for i in range(num_samples):\n        if np.all(np.isnan(quat_values[i])) or np.all(np.isclose(quat_values[i], 0)):\n            linear_accel[i, :] = acc_values[i, :] \n            continue\n\n        try:\n            rotation = R.from_quat(quat_values[i])\n            gravity_sensor_frame = rotation.apply(gravity_world, inverse=True)\n            linear_accel[i, :] = acc_values[i, :] - gravity_sensor_frame\n        except ValueError:\n             linear_accel[i, :] = acc_values[i, :]\n             \n    return linear_accel\n\ndef calculate_angular_velocity_from_quat(rot_data, time_delta=1/200): # Assuming 200Hz sampling rate\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_vel = np.zeros((num_samples, 3))\n\n    for i in range(num_samples - 1):\n        q_t = quat_values[i]\n        q_t_plus_dt = quat_values[i+1]\n\n        if np.all(np.isnan(q_t)) or np.all(np.isclose(q_t, 0)) or \\\n           np.all(np.isnan(q_t_plus_dt)) or np.all(np.isclose(q_t_plus_dt, 0)):\n            continue\n\n        try:\n            rot_t = R.from_quat(q_t)\n            rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n\n            # Calculate the relative rotation\n            delta_rot = rot_t.inv() * rot_t_plus_dt\n            \n            # Convert delta rotation to angular velocity vector\n            # The rotation vector (Euler axis * angle) scaled by 1/dt\n            # is a good approximation for small delta_rot\n            angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n        except ValueError:\n            # If quaternion is invalid, angular velocity remains zero\n            pass\n            \n    return angular_vel\n\ndef calculate_angular_distance(rot_data):\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_dist = np.zeros(num_samples)\n\n    for i in range(num_samples - 1):\n        q1 = quat_values[i]\n        q2 = quat_values[i+1]\n\n        if np.all(np.isnan(q1)) or np.all(np.isclose(q1, 0)) or \\\n           np.all(np.isnan(q2)) or np.all(np.isclose(q2, 0)):\n            angular_dist[i] = 0 # Или np.nan, в зависимости от желаемого поведения\n            continue\n        try:\n            # Преобразование кватернионов в объекты Rotation\n            r1 = R.from_quat(q1)\n            r2 = R.from_quat(q2)\n\n            # Вычисление углового расстояния: 2 * arccos(|real(p * q*)|)\n            # где p* - сопряженный кватернион q\n            # В scipy.spatial.transform.Rotation, r1.inv() * r2 дает относительное вращение.\n            # Угол этого относительного вращения - это и есть угловое расстояние.\n            relative_rotation = r1.inv() * r2\n            \n            # Угол rotation vector соответствует угловому расстоянию\n            # Норма rotation vector - это угол в радианах\n            angle = np.linalg.norm(relative_rotation.as_rotvec())\n            angular_dist[i] = angle\n        except ValueError:\n            angular_dist[i] = 0 # В случае недействительных кватернионов\n            pass\n            \n    return angular_dist\ndef feature_engineering(train_df):\n    # IMU magnitude\n    train_df['acc_mag'] = np.sqrt(train_df['acc_x']**2 + train_df['acc_y']**2 + train_df['acc_z']**2)\n    \n    # IMU angle\n    train_df['rot_angle'] = 2 * np.arccos(train_df['rot_w'].clip(-1, 1))\n    \n    # IMU jerk, angular velocity\n    train_df['acc_mag_jerk'] = train_df.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n    train_df['rot_angle_vel'] = train_df.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n    \n    # Remove gravity\n    def get_linear_accel(df):\n        res = remove_gravity_from_acc(\n            df[['acc_x', 'acc_y', 'acc_z']],\n            df[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        )\n        res = pd.DataFrame(res, columns=['linear_acc_x', 'linear_acc_y', 'linear_acc_z'], index=df.index)\n        return res\n    \n    linear_accel_df = train_df.groupby('sequence_id').apply(get_linear_accel, include_groups=False)\n    linear_accel_df = linear_accel_df.droplevel('sequence_id')\n    train_df = train_df.join(linear_accel_df)\n    \n    train_df['linear_acc_mag'] = np.sqrt(train_df['linear_acc_x']**2 + train_df['linear_acc_y']**2 + train_df['linear_acc_z']**2)\n    train_df['linear_acc_mag_jerk'] = train_df.groupby('sequence_id')['linear_acc_mag'].diff().fillna(0)\n\n    # Calc angular velocity\n    def calc_angular_velocity(df):\n        res = calculate_angular_velocity_from_quat( df[['rot_x', 'rot_y', 'rot_z', 'rot_w']] )\n        res = pd.DataFrame(res, columns=['angular_vel_x', 'angular_vel_y', 'angular_vel_z'], index=df.index)\n        return res\n    \n    angular_velocity_df = train_df.groupby('sequence_id').apply(calc_angular_velocity, include_groups=False)\n    angular_velocity_df = angular_velocity_df.droplevel('sequence_id')\n    train_df = train_df.join(angular_velocity_df)\n\n    # Calculating angular distance\n    def calc_angular_distance(df):\n        res = calculate_angular_distance(df[['rot_x', 'rot_y', 'rot_z', 'rot_w']])\n        res = pd.DataFrame(res, columns=['angular_distance'], index=df.index)\n        return res\n    \n    angular_distance_df = train_df.groupby('sequence_id').apply(calc_angular_distance, include_groups=False)\n    angular_distance_df = angular_distance_df.droplevel('sequence_id')\n    train_df = train_df.join(angular_distance_df)\n\n    train_df[imu_cols] = train_df[imu_cols].ffill().bfill().fillna(0).values.astype('float32')\n    \n    return train_df\ndef standard_scale(arr: np.ndarray) -> np.ndarray:\n    means = np.nanmean(arr, axis=0)\n    stds = np.nanstd(arr, axis=0)\n    stds = np.where(stds == 0, 1, stds)  # Prevent division by zero for constant columns\n    scaled = (arr - means) / stds\n    return scaled\n\n\ndef pad_or_truncate(\n    arr: np.ndarray,\n    max_length: int = 128,\n    pad_value: int = 0,\n    mode: str = \"random\"  # \"regular\" or \"random\"\n) -> np.ndarray:\n    L, D = arr.shape\n\n    if L > max_length:\n        return arr[:max_length, :]\n\n    elif L < max_length:\n        if mode == \"regular\":\n            padding = np.full((max_length - L, D), pad_value)\n            return np.vstack((arr, padding))\n        \n        elif mode == \"random\":\n            total_padding = max_length - L\n            pad_start = np.random.randint(0, total_padding + 1)\n            pad_end = total_padding - pad_start\n\n            start_padding = np.full((pad_start, D), pad_value)\n            end_padding = np.full((pad_end, D), pad_value)\n\n            return np.vstack((start_padding, arr, end_padding))\n        \n        else:\n            raise ValueError(f\"Unknown mode: {mode}. Use 'regular' or 'random'.\")\n\n    else:\n        return arr\n\nimu_cols = [\n    'acc_x', 'acc_y', 'acc_z','acc_mag','acc_mag_jerk', 'linear_acc_x', 'linear_acc_y', 'linear_acc_z', \n    'linear_acc_mag', 'linear_acc_mag_jerk',\n    'rot_w', 'rot_x', 'rot_y', 'rot_z',\n    'rot_angle', 'rot_angle_vel',\n    'angular_vel_x', 'angular_vel_y', 'angular_vel_z',\n    'angular_distance'\n]\ndf_test=feature_engineering(df_test)\nX_test  = []\n\nfor sequence_id in tqdm(df_test.sequence_id.unique()):\n    ds = df_test[df_test[\"sequence_id\"] == sequence_id]\n    X = ds[imu_cols].values\n    X = pad_or_truncate(X)\n    X = np.concatenate((standard_scale(X[:, 0:3]),X[:, 3:]), axis=1)\n    X = np.where(np.isnan(X), 0.0, X)  # fill NaNs\n    X_test.append(X)\n\nX_test = np.array(X_test)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-18T06:10:56.682780Z","iopub.execute_input":"2025-07-18T06:10:56.683075Z","iopub.status.idle":"2025-07-18T06:10:56.926660Z","shell.execute_reply.started":"2025-07-18T06:10:56.683044Z","shell.execute_reply":"2025-07-18T06:10:56.926061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CustomDataset(Dataset):\n    def __init__(\n        self, config, df: pd.DataFrame, X: np.ndarray\n    ): \n        \n        self.config = config\n        self.df = df\n        self.X = X\n        self.indexes = self.df.sequence_id.unique()\n        \n    def __len__(self):\n        \"\"\"\n        Length of dataset.\n        \"\"\"\n        return len(self.indexes)\n        \n    def __getitem__(self, index):\n        \"\"\"\n        Get one item.\n        \"\"\"\n        sequence_id = self.indexes[index]\n        X = self.X[index]\n        output = {\n            \"X\": torch.tensor(X, dtype=torch.float32),\n            \"sequence_id\": sequence_id\n        }\n        return output \ntest_dataset = CustomDataset(config, df_test, X_test)\ntest_loader = DataLoader(\n    test_dataset,\n    batch_size=config.BATCH_SIZE_TEST,\n    shuffle=False,\n    num_workers=config.NUM_WORKERS, pin_memory=True, drop_last=False\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-18T06:12:27.544945Z","iopub.execute_input":"2025-07-18T06:12:27.545914Z","iopub.status.idle":"2025-07-18T06:12:27.551711Z","shell.execute_reply.started":"2025-07-18T06:12:27.545882Z","shell.execute_reply":"2025-07-18T06:12:27.551049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass EnhancedSEBlock(nn.Module):\n    \"\"\"\n    An enhanced Squeeze-and-Excitation block that uses both average and max pooling,\n    inspired by the reference implementation.\n    \"\"\"\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.avg_pool = nn.AdaptiveAvgPool1d(1)\n        self.max_pool = nn.AdaptiveMaxPool1d(1)\n        self.excitation = nn.Sequential(\n            nn.Linear(channels * 2, channels // reduction, bias=False),\n            nn.SiLU(inplace=True),  # Using SiLU (swish) as in TF reference\n            nn.Linear(channels // reduction, channels, bias=False),\n            nn.Sigmoid()\n        )\n    \n    def forward(self, x):\n        b, c, _ = x.size()\n        avg_y = self.avg_pool(x).view(b, c)\n        max_y = self.max_pool(x).view(b, c)\n        y = torch.cat([avg_y, max_y], dim=1)\n        y = self.excitation(y).view(b, c, 1)\n        return x * y.expand_as(x)\n\nclass MultiScaleConv1d(nn.Module):\n    \"\"\"Multi-scale temporal convolution block\"\"\"\n    def __init__(self, in_channels, out_channels, kernel_sizes=[3, 5, 7]):\n        super().__init__()\n        self.convs = nn.ModuleList()\n        for ks in kernel_sizes:\n            self.convs.append(nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, ks, padding=ks//2, bias=False),\n                nn.BatchNorm1d(out_channels),\n                nn.ReLU(inplace=True)\n            ))\n        \n    def forward(self, x):\n        outputs = [conv(x) for conv in self.convs]\n        return torch.cat(outputs, dim=1)\n\nclass ResidualSEBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size, pool_size=2, dropout=0.3):\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size, padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        \n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size, padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n        \n        # Use the new EnhancedSEBlock\n        self.se = EnhancedSEBlock(out_channels, reduction=8)\n        \n        self.shortcut = nn.Sequential()\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, 1, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n        \n        self.pool = nn.MaxPool1d(pool_size)\n        self.dropout = nn.Dropout(dropout)\n        \n    def forward(self, x):\n        shortcut = self.shortcut(x)\n        \n        out = F.relu(self.bn1(self.conv1(x)))\n        out = self.bn2(self.conv2(out))\n        \n        out = self.se(out)\n        \n        out += shortcut\n        out = F.relu(out)\n        \n        out = self.pool(out)\n        out = self.dropout(out)\n        \n        return out\n\nclass MetaFeatureExtractor(nn.Module):\n    \"\"\"Extract statistical meta-features from input sequence\"\"\"\n    def forward(self, x):\n        # x shape: (B, L, C)\n        mean = torch.mean(x, dim=1)\n        std = torch.std(x, dim=1)\n        max_val, _ = torch.max(x, dim=1)\n        min_val, _ = torch.min(x, dim=1)\n        \n        # Calculate slope: (last - first) / seq_len\n        seq_len = x.size(1)\n        if seq_len > 1:\n            slope = (x[:, -1, :] - x[:, 0, :]) / (seq_len - 1)\n        else:\n            slope = torch.zeros_like(x[:, 0, :])\n        \n        return torch.cat([mean, std, max_val, min_val, slope], dim=1)\n\nclass AttentionLayer(nn.Module):\n    \"\"\"Pools the output of a sequence-based layer (like LSTM or Attention) over the time dimension.\"\"\"\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.attention = nn.Linear(hidden_dim, 1)\n        \n    def forward(self, x):\n        # x shape: (batch, seq_len, hidden_dim)\n        scores = torch.tanh(self.attention(x))\n        weights = F.softmax(scores.squeeze(-1), dim=1)\n        context = torch.sum(x * weights.unsqueeze(-1), dim=1)\n        return context\n\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n\n# EnhancedSEBlock, MetaFeatureExtractor, AttentionLayer\n\n\nclass ModelVariant_GRU(nn.Module):\n    def __init__(self, num_classes):\n        super().__init__()\n        num_channels = 20  # Hardcoded for IMU data\n\n        # 1. \n        self.meta_extractor = MetaFeatureExtractor()\n        self.meta_dense = nn.Sequential(\n            nn.Linear(5 * num_channels, 32),\n            nn.BatchNorm1d(32),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n        )\n\n        # 2. \n        self.branches_0 = nn.Sequential(\n                    # 输出通道数减少，从16*3=48 -> 12*3=36\n                    MultiScaleConv1d(10, 36, kernel_sizes=[3, 5, 7]),\n                    # 输出通道数相应调整\n                    ResidualSEBlock(108, 192, 3, dropout=0.3),\n                    ResidualSEBlock(192, 192, 3, dropout=0.3),\n                )\n        self.branches_1 = nn.Sequential(\n                    # 输出通道数减少，从16*3=48 -> 12*3=36\n                    MultiScaleConv1d(10, 36, kernel_sizes=[3, 5, 7]),\n                    # 输出通道数相应调整\n                    ResidualSEBlock(108, 192, 3, dropout=0.3),\n                    ResidualSEBlock(192, 192, 3, dropout=0.3),\n                )\n\n        # 3. 序列核心：使用BiGRU替换BiLSTM，并移除MultiHeadAttention\n        self.bigru = nn.GRU(\n            input_size=192 * 2,\n            hidden_size=128,  # 保持hidden_size\n            num_layers=2,\n            batch_first=True,\n            bidirectional=True,\n            dropout=0.2,\n        )\n\n        # 4. Attention Pooling层保持不变\n        self.attention_pooling = AttentionLayer(256)  # 128 * 2 for bidirectional\n\n        # 5. \n        self.head_1 = nn.Sequential(\n            nn.Linear(256 + 32, 128),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(128, num_classes),\n        )\n\n        self.head_2 = nn.Sequential(\n            nn.Linear(256 + 32, 128),\n            nn.BatchNorm1d(128),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(128, 1),  # For regression task\n        )\n\n    def forward(self, x: torch.Tensor):\n        # Meta features\n        meta = self.meta_extractor(x)\n        meta_proj = self.meta_dense(meta)\n\n        # CNN branches\n        branch_outputs = []\n        channel_input = x[:, :, :10].transpose(1, 2)\n        processed = self.branches_0(channel_input)\n        branch_outputs.append(processed.transpose(1, 2))\n        channel_input = x[:, :, 10:].transpose(1, 2)\n        processed = self.branches_1(channel_input)\n        branch_outputs.append(processed.transpose(1, 2))\n        combined = torch.cat(branch_outputs, dim=2)\n        # BiGRU processing\n        gru_out, _ = self.bigru(combined)  # (B, L/k, 256)\n\n        # Attention pooling\n        pooled_output = self.attention_pooling(gru_out)  # (B, 256)\n\n        # Combine with meta features\n        fused = torch.cat([pooled_output, meta_proj], dim=1)\n\n        # Final predictions\n        z1 = self.head_1(fused)\n        z2 = self.head_2(fused)\n        return z1, z2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-18T06:13:10.596364Z","iopub.execute_input":"2025-07-18T06:13:10.597092Z","iopub.status.idle":"2025-07-18T06:13:10.752766Z","shell.execute_reply.started":"2025-07-18T06:13:10.597063Z","shell.execute_reply":"2025-07-18T06:13:10.752098Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_test = sequence.to_pandas()\n    imu_cols = [\n    'acc_x', 'acc_y', 'acc_z','acc_mag','acc_mag_jerk', 'linear_acc_x', 'linear_acc_y', 'linear_acc_z', \n    'linear_acc_mag', 'linear_acc_mag_jerk',\n    'rot_w', 'rot_x', 'rot_y', 'rot_z',\n    'rot_angle', 'rot_angle_vel',\n    'angular_vel_x', 'angular_vel_y', 'angular_vel_z',\n    'angular_distance'\n     ]\n    X_test, y_test, y_hard_test = [], [], []\n    df_test=feature_engineering(df_test)\n    \n    for sequence_id in tqdm(df_test.sequence_id.unique()):\n        ds = df_test[df_test[\"sequence_id\"] == sequence_id]\n        X = ds[imu_cols].values\n        X = pad_or_truncate(X)\n        X = np.concatenate((standard_scale(X[:, 0:3]),X[:, 3:]), axis=1)\n        X = np.where(np.isnan(X), 0.0, X)  # fill NaNs\n        X_test.append(X)\n\n    X_test = np.array(X_test)\n    model_paths = glob(\"/kaggle/input/weight00/weight/*.pth\")\n    all_preds = []\n    for model_path in model_paths:\n        test_dataset = valid_dataset = CustomDataset(config, df_test, X_test)\n        test_loader = DataLoader(\n            test_dataset,\n            batch_size=config.BATCH_SIZE_TEST,\n            shuffle=False,\n            num_workers=config.NUM_WORKERS, \n            pin_memory=True, drop_last=False\n        )\n        model = ModelVariant_GRU(num_classes=18)\n        checkpoint = torch.load(model_path, map_location=device)\n        model.load_state_dict(checkpoint)\n        model.to(device)\n        model.eval()\n        softmax = nn.Softmax(dim=1)\n        \n        with tqdm(test_loader, unit=\"test_batch\", desc='Test') as tqdm_test_loader:\n            for step, batch in enumerate(tqdm_test_loader):\n                X = batch.pop(\"X\").to(device)\n                batch_size = X.size(0)\n                with torch.no_grad():\n                    y_preds, y_preds_hard = model(X)\n                y_preds = softmax(y_preds).to('cpu').numpy()\n                all_preds.append(y_preds)\n    all_preds = np.concatenate(all_preds)\n    all_preds = np.argmax(all_preds.mean(axis=0)).item()\n    prediction = num_to_label[all_preds]\n    return prediction\n\n# Launch inference server\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    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-18T06:15:16.464237Z","iopub.execute_input":"2025-07-18T06:15:16.464671Z","iopub.status.idle":"2025-07-18T06:15:21.121627Z","shell.execute_reply.started":"2025-07-18T06:15:16.464646Z","shell.execute_reply":"2025-07-18T06:15:21.120885Z"}},"outputs":[],"execution_count":null}]}