{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","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,"sourceType":"competition"},{"sourceId":245653776,"sourceType":"kernelVersion"},{"sourceId":450392,"sourceType":"modelInstanceVersion","modelInstanceId":359040,"modelId":380352}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"DEBUG = False\nTRAIN = True\n\n\nMAX_SEQ_LENGTH = 128\n\nFEATURE_NAMES = [\n    'acc_x', 'acc_y', 'acc_z',\n    'rot_w', 'rot_x', 'rot_y', 'rot_z',\n    'acc_mag', 'rot_angle', 'acc_mag_jerk', 'rot_angle_vel',\n    'linear_acc_x', 'linear_acc_y', 'linear_acc_z', 'linear_acc_mag', 'linear_acc_mag_jerk',\n    'angular_vel_x', 'angular_vel_y', 'angular_vel_z',\n    'angular_distance',\n]\nCATEGORICAL_FEATURES = []\nNUMERICAL_FEATURES = [f for f in FEATURE_NAMES if f not in CATEGORICAL_FEATURES]\n\nLABEL_NAMES = [\n    'Forehead - pull hairline',\n    'Neck - pinch skin',\n    'Forehead - scratch',\n    'Eyelash - pull hair',\n    'Text on phone',\n    'Eyebrow - pull hair',\n    'Neck - scratch',\n    'Above ear - pull hair',\n    'Cheek - pinch skin',\n    'Wave hello',\n    'Write name in air',\n    'Pull air toward your face',\n    'Feel around in tray and pull out an object',\n    'Write name on leg',\n    'Pinch knee/leg skin',\n    'Scratch knee/leg skin',\n    'Drink from bottle/cup',\n    'Glasses on/off'\n]\nIDX2LABEL = {x: i for i, x in enumerate(LABEL_NAMES)}\n\nOUTPUT_DIR = './saved_models'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:03:12.530732Z","iopub.execute_input":"2025-06-29T08:03:12.531019Z","iopub.status.idle":"2025-06-29T08:03:12.53658Z","shell.execute_reply.started":"2025-06-29T08:03:12.530999Z","shell.execute_reply":"2025-06-29T08:03:12.53579Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nif not os.path.exists(OUTPUT_DIR):\n    os.mkdir(OUTPUT_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:03:13.935233Z","iopub.execute_input":"2025-06-29T08:03:13.935535Z","iopub.status.idle":"2025-06-29T08:03:13.939591Z","shell.execute_reply.started":"2025-06-29T08:03:13.935515Z","shell.execute_reply":"2025-06-29T08:03:13.938777Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Data","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nimport joblib","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:03:16.024079Z","iopub.execute_input":"2025-06-29T08:03:16.02435Z","iopub.status.idle":"2025-06-29T08:03:16.029124Z","shell.execute_reply.started":"2025-06-29T08:03:16.02433Z","shell.execute_reply":"2025-06-29T08:03:16.028563Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = '/kaggle/input/cmi-detect-behavior-with-sensor-data/'\n\ntrain_df = pd.read_csv(f'{DATA_ROOT}/train.csv')\ntrain_demo_df = pd.read_csv(f'{DATA_ROOT}/train_demographics.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:03:16.479648Z","iopub.execute_input":"2025-06-29T08:03:16.480246Z","iopub.status.idle":"2025-06-29T08:03:34.849032Z","shell.execute_reply.started":"2025-06-29T08:03:16.480226Z","shell.execute_reply":"2025-06-29T08:03:34.848218Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.merge(\n    train_df,\n    train_demo_df,\n    how='left',\n    on='subject'\n)\n\ntrain_df.shape[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:03:34.850264Z","iopub.execute_input":"2025-06-29T08:03:34.850547Z","iopub.status.idle":"2025-06-29T08:03:35.478973Z","shell.execute_reply.started":"2025-06-29T08:03:34.850509Z","shell.execute_reply":"2025-06-29T08:03:35.478246Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Engineering","metadata":{}},{"cell_type":"code","source":"# Remove Gravity\n\nfrom 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:03:49.750882Z","iopub.execute_input":"2025-06-29T08:03:49.75116Z","iopub.status.idle":"2025-06-29T08:03:49.763328Z","shell.execute_reply.started":"2025-06-29T08:03:49.751142Z","shell.execute_reply":"2025-06-29T08:03:49.762732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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[FEATURE_NAMES] = train_df[FEATURE_NAMES].ffill().bfill().fillna(0).values.astype('float32')\n    \n    return train_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:05:18.137948Z","iopub.execute_input":"2025-06-29T08:05:18.138239Z","iopub.status.idle":"2025-06-29T08:05:18.146469Z","shell.execute_reply.started":"2025-06-29T08:05:18.138219Z","shell.execute_reply":"2025-06-29T08:05:18.145765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = feature_engineering(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:05:21.660331Z","iopub.execute_input":"2025-06-29T08:05:21.660891Z","iopub.status.idle":"2025-06-29T08:08:38.950275Z","shell.execute_reply.started":"2025-06-29T08:05:21.66087Z","shell.execute_reply":"2025-06-29T08:08:38.949675Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Standard","metadata":{}},{"cell_type":"code","source":"feature_scaler = StandardScaler()\nfeature_scaler.fit(train_df[NUMERICAL_FEATURES])\njoblib.dump(feature_scaler, f'{OUTPUT_DIR}/feature_scaler.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:38.95165Z","iopub.execute_input":"2025-06-29T08:08:38.951951Z","iopub.status.idle":"2025-06-29T08:08:39.0862Z","shell.execute_reply.started":"2025-06-29T08:08:38.951928Z","shell.execute_reply":"2025-06-29T08:08:39.085457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[NUMERICAL_FEATURES] = feature_scaler.transform(train_df[NUMERICAL_FEATURES])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:39.087105Z","iopub.execute_input":"2025-06-29T08:08:39.087382Z","iopub.status.idle":"2025-06-29T08:08:39.213594Z","shell.execute_reply.started":"2025-06-29T08:08:39.087365Z","shell.execute_reply":"2025-06-29T08:08:39.213005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(NUMERICAL_FEATURES)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:39.215139Z","iopub.execute_input":"2025-06-29T08:08:39.215335Z","iopub.status.idle":"2025-06-29T08:08:39.219826Z","shell.execute_reply.started":"2025-06-29T08:08:39.215321Z","shell.execute_reply":"2025-06-29T08:08:39.219314Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Convert to Sequence","metadata":{}},{"cell_type":"code","source":"agg_train_df = train_df.groupby(['sequence_id', 'subject', 'gesture']).apply(\n    lambda df: df[FEATURE_NAMES].to_dict(orient='records'),\n    include_groups=False,\n)\n\nagg_train_df = agg_train_df.reset_index()\nagg_train_df.columns = ['sequence_id', 'subject', 'gesture', 'sequence']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:39.220505Z","iopub.execute_input":"2025-06-29T08:08:39.220778Z","iopub.status.idle":"2025-06-29T08:08:56.004002Z","shell.execute_reply.started":"2025-06-29T08:08:39.220756Z","shell.execute_reply":"2025-06-29T08:08:56.003089Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['label'] = agg_train_df.gesture.map(IDX2LABEL)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:56.004897Z","iopub.execute_input":"2025-06-29T08:08:56.005152Z","iopub.status.idle":"2025-06-29T08:08:56.010667Z","shell.execute_reply.started":"2025-06-29T08:08:56.005129Z","shell.execute_reply":"2025-06-29T08:08:56.009883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:56.011552Z","iopub.execute_input":"2025-06-29T08:08:56.011781Z","iopub.status.idle":"2025-06-29T08:08:56.122595Z","shell.execute_reply.started":"2025-06-29T08:08:56.011766Z","shell.execute_reply":"2025-06-29T08:08:56.121908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DataLoader","metadata":{}},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedGroupKFold, GroupKFold\nimport pandas as pd\nfrom typing import List, Dict\n\nclass SequenceDataset(Dataset):\n    def __init__(self, sequences: List[List[Dict]], labels: List[int]):\n        \"\"\"\n        Args:\n            sequences: 序列数据列表，每个序列包含多个时间步的特征字典\n            labels: 对应的标签列表\n        \"\"\"\n        self.sequences = sequences\n        self.labels = labels\n        \n        # 转换为numpy数组格式并截断\n        self.processed_sequences = self._process_sequences()\n        \n        \n    def _process_sequences(self):\n        \"\"\"将字典格式的序列转换为numpy数组并截断到最大长度\"\"\"\n        processed = []\n        \n        for seq in self.sequences:\n            # 截断到最大长度\n            seq = seq[:MAX_SEQ_LENGTH]\n            \n            seq_array = []\n            for timestep in seq:\n                # 按照feature_names的顺序提取特征\n                features = [timestep[feature] for feature in FEATURE_NAMES]\n                seq_array.append(features)\n            processed.append(np.array(seq_array, dtype=np.float32))\n        \n        return processed\n    \n    def __len__(self):\n        return len(self.sequences)\n    \n    def __getitem__(self, idx):\n        sequence = torch.FloatTensor(self.processed_sequences[idx])\n        label = torch.LongTensor([self.labels[idx]])[0]\n        return sequence, label\n\n\ndef collate_fn(batch):\n    \"\"\"自定义批处理函数，处理不同长度的序列\"\"\"\n    sequences, labels = zip(*batch)\n    \n    # 获取批次中的最大序列长度\n    max_length = max(seq.shape[0] for seq in sequences)\n    \n    # 填充序列到相同长度\n    padded_sequences = []\n    lengths = []\n    \n    for seq in sequences:\n        seq_len = seq.shape[0]\n        lengths.append(seq_len)\n        \n        if seq_len < max_length:\n            # 使用零填充\n            padding = torch.zeros(max_length - seq_len, seq.shape[1])\n            padded_seq = torch.cat([seq, padding], dim=0)\n        else:\n            padded_seq = seq\n        \n        padded_sequences.append(padded_seq)\n    \n    return torch.stack(padded_sequences), torch.stack(labels), torch.LongTensor(lengths)\n\n\ndef create_kfold_splits(sequences, labels, subjects, n_splits=5, random_state=42):\n    \"\"\"使用StratifiedGroupKFold创建K折分割\"\"\"\n    sequences = np.array(sequences, dtype=object)\n    labels = np.array(labels)\n    subjects = np.array(subjects)\n\n    if sequences.shape[0] > 1000:\n        sgkf = StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=random_state)\n    else:\n        n_splits = 2\n        sgkf = GroupKFold(n_splits=n_splits, shuffle=True, random_state=random_state)\n    \n    splits = []\n    for fold_idx, (train_idx, val_idx) in enumerate(sgkf.split(sequences, labels, subjects)):\n        splits.append((train_idx, val_idx))\n        print(f\"   Fold {fold_idx+1}: 训练={len(train_idx)}, 验证={len(val_idx)}\")\n    \n    print(f\"✅ 成功创建{n_splits}折交叉验证分割\")\n    return splits\n\n\ndef create_fold_loaders(sequences, labels, train_idx, val_idx, batch_size=32):\n    \"\"\"为特定的折创建数据加载器，使用全局scaler\"\"\"\n    # 根据索引分割数据\n    train_sequences = [sequences[i] for i in train_idx]\n    train_labels = [labels[i] for i in train_idx]\n    val_sequences = [sequences[i] for i in val_idx]\n    val_labels = [labels[i] for i in val_idx]\n    \n    # 创建数据集，使用全局scaler\n    train_dataset = SequenceDataset(train_sequences, train_labels)\n    val_dataset = SequenceDataset(val_sequences, val_labels)\n    \n    # 创建数据加载器\n    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)\n    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)\n    \n    return train_loader, val_loader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:08:56.123524Z","iopub.execute_input":"2025-06-29T08:08:56.12381Z","iopub.status.idle":"2025-06-29T08:09:00.117321Z","shell.execute_reply.started":"2025-06-29T08:08:56.123777Z","shell.execute_reply":"2025-06-29T08:09:00.116782Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.squeeze = nn.AdaptiveAvgPool1d(1)\n        self.excitation = 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.squeeze(x).view(b, c)\n        y = self.excitation(y).view(b, c, 1)\n        return x * y.expand_as(x)\n\nclass ResidualSECNNBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size, pool_size=2, dropout=0.3, weight_decay=1e-4):\n        super().__init__()\n        \n        # First conv block\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        # Second conv block\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        # SE block\n        self.se = SEBlock(out_channels)\n        \n        # Shortcut connection\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        # First conv\n        out = F.relu(self.bn1(self.conv1(x)))\n        # Second conv\n        out = self.bn2(self.conv2(out))\n        \n        # SE block\n        out = self.se(out)\n        \n        # Add shortcut\n        out += shortcut\n        out = F.relu(out)\n        \n        # Pool and dropout\n        out = self.pool(out)\n        out = self.dropout(out)\n        \n        return out\n\nclass AttentionLayer(nn.Module):\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))  # (batch, seq_len, 1)\n        weights = F.softmax(scores.squeeze(-1), dim=1)  # (batch, seq_len)\n        context = torch.sum(x * weights.unsqueeze(-1), dim=1)  # (batch, hidden_dim)\n        return context\n\nclass TwoBranchModel(nn.Module):\n    def __init__(self, imu_dim, tof_dim, n_classes, weight_decay=1e-4):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.tof_dim = tof_dim\n        self.n_classes = n_classes\n        self.weight_decay = weight_decay\n        \n        # IMU deep branch\n        self.imu_block1 = ResidualSECNNBlock(imu_dim, 64, 3, dropout=0.3, weight_decay=weight_decay)\n        self.imu_block2 = ResidualSECNNBlock(64, 128, 5, dropout=0.3, weight_decay=weight_decay)\n        \n        # # TOF/Thermal lighter branch\n        # self.tof_conv1 = nn.Conv1d(tof_dim, 64, 3, padding=1, bias=False)\n        # self.tof_bn1 = nn.BatchNorm1d(64)\n        # self.tof_pool1 = nn.MaxPool1d(2)\n        # self.tof_drop1 = nn.Dropout(0.3)\n        \n        # self.tof_conv2 = nn.Conv1d(64, 128, 3, padding=1, bias=False)\n        # self.tof_bn2 = nn.BatchNorm1d(128)\n        # self.tof_pool2 = nn.MaxPool1d(2)\n        # self.tof_drop2 = nn.Dropout(0.3)\n        \n        # BiLSTM\n        # self.bilstm = nn.LSTM(256, 128, bidirectional=True, batch_first=True)\n        # self.bilstm = nn.LSTM(128, 128, bidirectional=True, batch_first=True)\n        # self.lstm_dropout = nn.Dropout(0.4)\n        self.bigru = nn.GRU(128, 128, bidirectional=True, batch_first=True)\n        self.gru_dropout = nn.Dropout(0.4)\n        \n        # Attention\n        self.attention = AttentionLayer(256)  # 128*2 for bidirectional\n        \n        # Dense layers\n        self.dense1 = nn.Linear(256, 256, bias=False)\n        self.bn_dense1 = nn.BatchNorm1d(256)\n        self.drop1 = nn.Dropout(0.5)\n        \n        self.dense2 = nn.Linear(256, 128, bias=False)\n        self.bn_dense2 = nn.BatchNorm1d(128)\n        self.drop2 = nn.Dropout(0.3)\n        \n        self.classifier = nn.Linear(128, n_classes)\n        \n    def forward(self, x):\n        # Split input\n        # imu = x[:, :, :self.imu_dim].transpose(1, 2)  # (batch, imu_dim, seq_len)\n        # tof = x[:, :, self.imu_dim:].transpose(1, 2)  # (batch, tof_dim, seq_len)\n\n        imu = x[:, :, :self.imu_dim]  # imu only   batch, seq_len, hidden_dim\n        imu = imu.transpose(1, 2)  # (batch, imu_dim, seq_len)\n\n        \n        # IMU branch\n        x1 = self.imu_block1(imu)\n        x1 = self.imu_block2(x1)\n        \n        # TOF branch\n        # x2 = F.relu(self.tof_bn1(self.tof_conv1(tof)))\n        # x2 = self.tof_drop1(self.tof_pool1(x2))\n        # x2 = F.relu(self.tof_bn2(self.tof_conv2(x2)))\n        # x2 = self.tof_drop2(self.tof_pool2(x2))\n        \n        # Concatenate branches\n        # merged = torch.cat([x1, x2], dim=1).transpose(1, 2)  # (batch, seq_len, 256)\n        merged = x1.transpose(1, 2)  # (batch, seq_len, 128)\n        \n        # BiLSTM\n        # lstm_out, _ = self.bilstm(merged)\n        # lstm_out = self.lstm_dropout(lstm_out)\n        gru_out, _ = self.bigru(merged)\n        gru_out = self.gru_dropout(gru_out)\n        \n        # Attention\n        attended = self.attention(gru_out)\n        \n        # Dense layers\n        x = F.relu(self.bn_dense1(self.dense1(attended)))\n        x = self.drop1(x)\n        x = F.relu(self.bn_dense2(self.dense2(x)))\n        x = self.drop2(x)\n        \n        # Classification\n        logits = self.classifier(x)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:00.118145Z","iopub.execute_input":"2025-06-29T08:09:00.11856Z","iopub.status.idle":"2025-06-29T08:09:00.133211Z","shell.execute_reply.started":"2025-06-29T08:09:00.118541Z","shell.execute_reply":"2025-06-29T08:09:00.132361Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train","metadata":{}},{"cell_type":"markdown","source":"### Metric","metadata":{}},{"cell_type":"code","source":"import kagglehub\nmetric = kagglehub.package_import('jiazhuang/cmi-2025-metric')\n\ndef get_competition_score(true, pred):\n    assert len(true) == len(pred)\n    N = len(true)\n    true = pd.DataFrame({'id': range(N), 'gesture': true})\n    pred = pd.DataFrame({'id': range(N), 'gesture': pred})\n    return metric.score(true, pred, 'id')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:00.135455Z","iopub.execute_input":"2025-06-29T08:09:00.135691Z","iopub.status.idle":"2025-06-29T08:09:00.931268Z","shell.execute_reply.started":"2025-06-29T08:09:00.135676Z","shell.execute_reply":"2025-06-29T08:09:00.930481Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom sklearn.metrics import accuracy_score, classification_report, confusion_matrix\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm import tqdm\nimport json\n\n# from metric import get_competition_score\n\n\nclass Trainer:\n    \"\"\"LSTM分类器训练器\"\"\"\n    \n    def __init__(self, model, device, class_names):\n        self.model = model.to(device)\n        self.device = device\n        self.class_names = class_names\n        self.train_losses = []\n        self.val_losses = []\n        self.train_accuracies = []\n        self.val_accuracies = []\n        self.val_scores = []\n        \n    def train_epoch(self, train_loader, optimizer, criterion):\n        \"\"\"训练一个epoch\"\"\"\n        self.model.train()\n        total_loss = 0\n        correct_predictions = 0\n        total_predictions = 0\n        \n        progress_bar = tqdm(train_loader, desc=\"训练中\", disable=True)\n        \n        for batch_idx, (sequences, labels, lengths) in enumerate(progress_bar):\n            sequences = sequences.to(self.device)\n            labels = labels.to(self.device)\n            \n            # 前向传播\n            optimizer.zero_grad()\n            outputs = self.model(sequences)\n            loss = criterion(outputs, labels)\n            \n            # 反向传播\n            loss.backward()\n            \n            # 梯度裁剪\n            torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)\n            \n            optimizer.step()\n            \n            # 统计\n            total_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n            correct_predictions += (predicted == labels).sum().item()\n            total_predictions += labels.size(0)\n            \n            # 更新进度条\n            progress_bar.set_postfix({\n                'Loss': f'{loss.item():.4f}',\n                'Acc': f'{correct_predictions/total_predictions:.4f}'\n            })\n        \n        avg_loss = total_loss / len(train_loader)\n        accuracy = correct_predictions / total_predictions\n        \n        return avg_loss, accuracy\n    \n    def validate(self, val_loader, criterion):\n        \"\"\"验证\"\"\"\n        self.model.eval()\n        total_loss = 0\n        correct_predictions = 0\n        total_predictions = 0\n        all_predictions = []\n        all_labels = []\n        \n        with torch.no_grad():\n            for sequences, labels, lengths in tqdm(val_loader, desc=\"验证中\", disable=True):\n                sequences = sequences.to(self.device)\n                labels = labels.to(self.device)\n                \n                outputs = self.model(sequences)\n                loss = criterion(outputs, labels)\n                \n                total_loss += loss.item()\n                _, predicted = torch.max(outputs.data, 1)\n                correct_predictions += (predicted == labels).sum().item()\n                total_predictions += labels.size(0)\n                \n                all_predictions.extend(predicted.cpu().numpy())\n                all_labels.extend(labels.cpu().numpy())\n        \n        avg_loss = total_loss / len(val_loader)\n        accuracy = correct_predictions / total_predictions\n        \n        return avg_loss, accuracy, all_predictions, all_labels\n    \n    def train(self, train_loader, val_loader, num_epochs=50, learning_rate=0.001,\n              weight_decay=1e-5, patience=10, save_path='best_model.pth'):\n        \"\"\"完整训练流程\"\"\"\n        \n        # 优化器和损失函数\n        optimizer = optim.Adam(self.model.parameters(), lr=learning_rate, weight_decay=weight_decay)\n        criterion = nn.CrossEntropyLoss()\n        \n        # 学习率调度器\n        scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n            optimizer, mode='max', factor=0.5, patience=patience//2\n        )\n        \n        # 早停\n        best_val_score = -float('inf')\n        epochs_without_improvement = 0\n        \n        print(f\"开始训练，设备: {self.device}\")\n        print(f\"模型参数量: {sum(p.numel() for p in self.model.parameters()):,}\")\n        \n        # 记录当前学习率用于检测变化\n        current_lr = learning_rate\n        \n        for epoch in range(num_epochs):\n            # 训练\n            train_loss, train_acc = self.train_epoch(train_loader, optimizer, criterion)\n            \n            # 验证\n            val_loss, val_acc, val_preds, val_labels = self.validate(val_loader, criterion)\n            val_preds = [self.class_names[pred] for pred in val_preds]\n            val_labels = [self.class_names[label] for label in val_labels]\n            val_score = get_competition_score(val_labels, val_preds)\n            \n            # 记录历史\n            self.train_losses.append(train_loss)\n            self.val_losses.append(val_loss)\n            self.train_accuracies.append(train_acc)\n            self.val_accuracies.append(val_acc)\n            self.val_scores.append(val_score)\n            \n            # 更新学习率\n            old_lr = optimizer.param_groups[0]['lr']\n            scheduler.step(val_score)\n            new_lr = optimizer.param_groups[0]['lr']\n            \n            # 手动记录学习率变化\n            # if new_lr != old_lr:\n            #     print(f\"学习率从 {old_lr:.6f} 降低到 {new_lr:.6f}\")\n\n            if epoch % 10 == 0:\n                print(f\"\\nEpoch {epoch+1}/{num_epochs}\")\n                print(\"-\" * 50)\n                print(f\"训练 - Loss: {train_loss:.4f}, Acc: {train_acc:.4f}\")\n                print(f\"验证 - Loss: {val_loss:.4f}, Acc: {val_acc:.4f}, Score: {val_score:.4f}\")\n                print(f\"当前学习率: {new_lr:.6f}\")\n            \n            # 早停检查\n            if val_score > best_val_score:\n                best_val_score = val_score\n                epochs_without_improvement = 0\n                # 保存最佳模型\n                torch.save({\n                    'model_state_dict': self.model.state_dict(),\n                    'optimizer_state_dict': optimizer.state_dict(),\n                    'epoch': epoch,\n                    'val_loss': val_loss,\n                    'val_acc': val_acc,\n                    'val_score': val_score,\n                    'model_config': {                        \n                        'imu_dim': self.model.imu_dim,\n                        'tof_dim': self.model.tof_dim,\n                        'n_classes': self.model.n_classes,\n                        'weight_decay': self.model.weight_decay\n                    }\n                }, save_path)\n                # print(f\"保存最佳模型到 {save_path}\")\n            else:\n                epochs_without_improvement += 1\n                \n            if epochs_without_improvement >= patience:\n                print(f\"早停：{patience} 个epoch无改善\")\n                break\n        \n        print(\"训练完成！\")\n        return self.train_losses, self.val_losses, self.train_accuracies, self.val_accuracies, self.val_scores\n    \n    def plot_training_history(self, save_path='training_history.png'):\n        \"\"\"绘制训练历史\"\"\"\n        fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))\n        \n        # 损失曲线\n        ax1.plot(self.train_losses, label='train loss')\n        ax1.plot(self.val_losses, label='val loss')\n        ax1.set_title('train and val loss')\n        ax1.set_xlabel('Epoch')\n        ax1.set_ylabel('Loss')\n        ax1.legend()\n        ax1.grid(True)\n        \n        # 准确率曲线\n        ax2.plot(self.train_accuracies, label='train accuracy')\n        ax2.plot(self.val_accuracies, label='val accuracy')\n        ax2.set_title('train and val accuracy')\n        ax2.set_xlabel('Epoch')\n        ax2.set_ylabel('Accuracy')\n        ax2.legend()\n        ax2.grid(True)\n\n        # 分数曲线\n        ax3.plot(self.val_scores, label='val score')\n        ax3.set_title('val score')\n        ax3.set_xlabel('Epoch')\n        ax3.set_ylabel('Score')\n        ax3.legend()\n        ax3.grid(True)\n        \n        plt.tight_layout()\n        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n        plt.show()\n        print(f\"训练历史图保存到 {save_path}\")\n\n\ndef evaluate_model(model, test_loader, device, class_names=None):\n    \"\"\"评估模型\"\"\"\n    model.eval()\n    all_predictions = []\n    all_labels = []\n    all_probabilities = []\n    \n    with torch.no_grad():\n        for sequences, labels, lengths in tqdm(test_loader, desc=\"评估中\", disable=True):\n            sequences = sequences.to(device)\n            labels = labels.to(device)\n            \n            outputs = model(sequences)\n            probabilities = torch.softmax(outputs, dim=1)\n            _, predicted = torch.max(outputs, 1)\n            \n            all_predictions.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probabilities.extend(probabilities.cpu().numpy())\n    \n    # 计算指标\n    accuracy = accuracy_score(all_labels, all_predictions)\n    score = get_competition_score(\n        [class_names[label] for label in all_labels],\n        [class_names[pred] for pred in all_predictions]\n    )\n\n    # 分类报告\n    if class_names is None:\n        class_names = [f'Class_{i}' for i in range(len(set(all_labels)))]\n    \n    # import pdb; pdb.set_trace()\n    sub_class_names = sorted(set(all_labels + all_predictions))\n    sub_class_names = [class_names[i] for i in sub_class_names]\n    report = classification_report(all_labels, all_predictions, \n                                 target_names=sub_class_names, output_dict=True)\n    \n    print(f\"测试准确率: {accuracy:.4f}\")\n    print(f\"测试分数: {score:.4f}\")\n    print(\"\\n分类报告:\")\n    print(classification_report(all_labels, all_predictions, target_names=sub_class_names))\n    \n    # 混淆矩阵\n    cm = confusion_matrix(all_labels, all_predictions)\n    plt.figure(figsize=(12, 10))\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n                xticklabels=sub_class_names, yticklabels=sub_class_names)\n    plt.title('confusion matrix')\n    plt.ylabel('true label')\n    plt.xlabel('predicted label')\n    plt.tight_layout()\n    plt.savefig(f'{OUTPUT_DIR}/confusion_matrix.png', dpi=300, bbox_inches='tight')\n    plt.show()\n    \n    return accuracy, score, report, all_predictions, all_probabilities","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:00.932285Z","iopub.execute_input":"2025-06-29T08:09:00.932625Z","iopub.status.idle":"2025-06-29T08:09:01.27728Z","shell.execute_reply.started":"2025-06-29T08:09:00.932598Z","shell.execute_reply":"2025-06-29T08:09:01.276767Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_single_fold(sequences, labels, subjects, train_idx, val_idx,\n                     fold_num, total_folds, device, num_epochs=50, \n                     learning_rate=0.001, patience=10, batch_size=16):\n    \"\"\"训练单个折\"\"\"\n    print(f\"\\n{'='*60}\")\n    print(f\"🔄 开始训练第 {fold_num}/{total_folds} 折\")\n    print(f\"{'='*60}\")\n    \n    # 创建数据加载器\n    train_loader, val_loader = create_fold_loaders(\n        sequences, labels, train_idx, val_idx, batch_size=batch_size\n    )\n    \n    print(f\"📊 第{fold_num}折数据信息:\")\n    print(f\"   训练样本: {len(train_loader.dataset)}\")\n    print(f\"   验证样本: {len(val_loader.dataset)}\")\n    \n    # 检查subject分布\n    train_subjects_fold = [subjects[i] for i in train_idx]\n    val_subjects_fold = [subjects[i] for i in val_idx]\n    print(f\"   训练subjects: {len(set(train_subjects_fold))}\")\n    print(f\"   验证subjects: {len(set(val_subjects_fold))}\")\n    \n    # 创建模型\n    num_classes = len(LABEL_NAMES)\n    model = TwoBranchModel(\n        imu_dim=len(FEATURE_NAMES),\n        tof_dim=0,\n        n_classes=num_classes,\n        weight_decay=3e-3\n    )\n    \n    # 初始化权重\n    if hasattr(model, 'init_weights'):\n        model.init_weights()\n    \n    print(f\"✅ 模型参数量: {sum(p.numel() for p in model.parameters()):,}\")\n    \n    # 创建训练器\n    trainer = Trainer(model, device, LABEL_NAMES)\n    \n    # 训练参数\n    model_save_path = f'{OUTPUT_DIR}/fold_{fold_num}_model.pth'\n    \n    # 训练\n    print(f\"🏋️ 开始第{fold_num}折训练...\")\n    train_losses, val_losses, train_accuracies, val_accuracies, val_scores = trainer.train(\n        train_loader, val_loader,\n        num_epochs=num_epochs,\n        learning_rate=learning_rate,\n        patience=patience,\n        save_path=model_save_path\n    )\n    \n    print(f\"✅ 第{fold_num}折训练完成!\")\n    print(f\"📈 最终训练准确率: {train_accuracies[-1]:.4f}\")\n    print(f\"📊 最终验证准确率: {val_accuracies[-1]:.4f}\")\n    print(f\"📊 最终验证分数: {val_scores[-1]:.4f}\")\n\n    # 绘制训练历史\n    history_plot_path = f'{OUTPUT_DIR}/fold_{fold_num}_training_history.png'\n    trainer.plot_training_history(history_plot_path)\n    \n    # 加载最佳模型进行评估\n    checkpoint = torch.load(model_save_path, map_location=device, weights_only=False)\n    model.load_state_dict(checkpoint['model_state_dict'])\n    \n    # 最终评估\n    accuracy, score, report, predictions, probabilities = evaluate_model(\n        model, val_loader, device, LABEL_NAMES\n    )\n    \n    # 保存该折的结果\n    fold_results = {\n        'fold': fold_num,\n        'model_path': model_save_path,\n        'final_train_acc': train_accuracies[-1],\n        'final_val_acc': val_accuracies[-1],\n        'final_val_score': val_scores[-1],\n        'test_accuracy': accuracy,\n        'test_score': score,\n        'num_epochs': len(train_losses),\n        'train_subjects': len(set(train_subjects_fold)),\n        'val_subjects': len(set(val_subjects_fold))\n    }\n    \n    print(f\"🎯 第{fold_num}折最终评估:\")\n    print(f\"   测试准确率: {accuracy:.4f}\")\n    print(f\"   测试分数: {score:.4f}\")\n    \n    return fold_results\n\n\ndef run_kfold_training(train_df, n_folds=5, **kwargs):\n    \"\"\"运行K折交叉验证训练\"\"\"\n    print(\"🚀 使用K折交叉验证进行LSTM序列分类训练\")\n    print(\"=\" * 60)\n    \n    # 设置设备\n    device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'\n    print(f\"🧠 使用设备: {device}\")\n    \n    # 加载数据\n    print(\"📊 加载训练数据...\")\n    # train_df = pd.read_json(train_path, lines=True)\n    sequences = train_df['sequence'].tolist()\n    labels = train_df['label'].tolist()\n    subjects = train_df['subject'].tolist()\n    \n    num_classes = len(LABEL_NAMES)\n    print(f\"\\n📋 数据信息:\")\n    print(f\"   总样本数: {len(sequences)}\")\n    print(f\"   类别数量: {num_classes}\")\n    print(f\"   总subjects数: {len(set(subjects))}\")\n    \n    # 显示类别信息\n    print(f\"\\n🏷️  类别示例:\")\n    for i, class_name in enumerate(LABEL_NAMES[:8]):\n        print(f\"   {i}: {class_name}\")\n    if len(LABEL_NAMES) > 8:\n        print(f\"   ... 还有 {len(LABEL_NAMES) - 8} 个类别\")\n    \n    # 创建K折分割\n    print(f\"\\n📂 创建{n_folds}折交叉验证分割...\")\n    splits = create_kfold_splits(sequences, labels, subjects, n_splits=n_folds, random_state=42)\n    \n    # K折训练\n    fold_results = []\n    for fold_num, (train_idx, val_idx) in enumerate(splits, 1):\n        fold_result = train_single_fold(\n            sequences, labels, subjects, train_idx, val_idx,\n            fold_num, n_folds, device, **kwargs\n        )\n        fold_results.append(fold_result)\n    \n    # 计算总体统计\n    print(f\"\\n{'='*60}\")\n    print(f\"🎉 {n_folds}折交叉验证完成!\")\n    print(f\"{'='*60}\")\n    \n    val_accuracies = [r['final_val_acc'] for r in fold_results]\n    val_scores = [r['final_val_score'] for r in fold_results]\n    test_accuracies = [r['test_accuracy'] for r in fold_results]\n    test_scores = [r['test_score'] for r in fold_results]\n    \n    print(f\"\\n📊 K折交叉验证统计:\")\n    print(f\"   验证准确率: {np.mean(val_accuracies):.4f} ± {np.std(val_accuracies):.4f}\")\n    print(f\"   验证分数: {np.mean(val_scores):.4f} ± {np.std(val_scores):.4f}\")\n    print(f\"   测试准确率: {np.mean(test_accuracies):.4f} ± {np.std(test_accuracies):.4f}\")\n    print(f\"   测试分数: {np.mean(test_scores):.4f} ± {np.std(test_scores):.4f}\")\n    \n    # 显示每折结果\n    print(f\"\\n📋 各折详细结果:\")\n    for i, result in enumerate(fold_results):\n        print(f\"   Fold {i+1}: Val Acc={result['final_val_acc']:.4f}, \"\n              f\"Val Score={result['final_val_score']:.4f}, \"\n              f\"Test Acc={result['test_accuracy']:.4f}, \"\n              f\"Test Score={result['test_score']:.4f}\")\n    \n    # 保存K折结果\n    kfold_results = {\n        'n_folds': n_folds,\n        'fold_results': fold_results,\n        'summary': {\n            'val_acc_mean': np.mean(val_accuracies),\n            'val_acc_std': np.std(val_accuracies),\n            'val_score_mean': np.mean(val_scores),\n            'val_score_std': np.std(val_scores),\n            'test_acc_mean': np.mean(test_accuracies),\n            'test_acc_std': np.std(test_accuracies),\n            'test_score_mean': np.mean(test_scores),\n            'test_score_std': np.std(test_scores)\n        },\n    }\n    \n    with open(f'{OUTPUT_DIR}/kfold_results.json', 'w', encoding='utf-8') as f:\n        json.dump(kfold_results, f, ensure_ascii=False, indent=2)\n\n    print(f\"💾 K折结果已保存: kfold_results.json\")\n    \n    return kfold_results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:01.27799Z","iopub.execute_input":"2025-06-29T08:09:01.278239Z","iopub.status.idle":"2025-06-29T08:09:01.294196Z","shell.execute_reply.started":"2025-06-29T08:09:01.278226Z","shell.execute_reply":"2025-06-29T08:09:01.293676Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.manual_seed(42)\nnp.random.seed(42)\n\n# 训练参数\ntraining_params = {\n    'num_epochs': 1 if DEBUG else 100,\n    'learning_rate': 0.001,\n    'patience': 20,\n    'batch_size': 12\n}\n\nif DEBUG:\n    agg_train_df = agg_train_df.head(2000)\n\n# K折交叉验证训练\nn_folds = 5\nprint(\"🔄 开始K折交叉验证训练\")\n\nif TRAIN:\n    _ = run_kfold_training(agg_train_df, n_folds=n_folds, **training_params)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:01.294873Z","iopub.execute_input":"2025-06-29T08:09:01.29506Z","iopub.status.idle":"2025-06-29T08:09:31.39421Z","shell.execute_reply.started":"2025-06-29T08:09:01.295047Z","shell.execute_reply":"2025-06-29T08:09:31.393657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ls saved_models/","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:31.394924Z","iopub.execute_input":"2025-06-29T08:09:31.395361Z","iopub.status.idle":"2025-06-29T08:09:31.592085Z","shell.execute_reply.started":"2025-06-29T08:09:31.395344Z","shell.execute_reply":"2025-06-29T08:09:31.591327Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference","metadata":{}},{"cell_type":"code","source":"import os\n\ndef get_lastest_saved_models():\n    latest = 1\n    for version in os.listdir('/kaggle/input/cmi-imu-only-lstm-trained-output/pytorch/default/'):\n        try:\n            version = int(version)\n        except:\n            continue\n\n        if version > latest:\n            latest = version\n\n    return f'/kaggle/input/cmi-imu-only-lstm-trained-output/pytorch/default/{latest}/saved_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:31.593197Z","iopub.execute_input":"2025-06-29T08:09:31.5935Z","iopub.status.idle":"2025-06-29T08:09:31.598738Z","shell.execute_reply.started":"2025-06-29T08:09:31.593461Z","shell.execute_reply":"2025-06-29T08:09:31.598055Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SAVED_MODELS = './saved_models/' if TRAIN else get_lastest_saved_models()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:31.599526Z","iopub.execute_input":"2025-06-29T08:09:31.599785Z","iopub.status.idle":"2025-06-29T08:09:31.613235Z","shell.execute_reply.started":"2025-06-29T08:09:31.599765Z","shell.execute_reply":"2025-06-29T08:09:31.612466Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport pickle\nimport glob\nimport os\nfrom collections import Counter\nfrom typing import List, Dict\nimport polars as pl\n\nimport joblib","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:31.614082Z","iopub.execute_input":"2025-06-29T08:09:31.614399Z","iopub.status.idle":"2025-06-29T08:09:32.384285Z","shell.execute_reply.started":"2025-06-29T08:09:31.614377Z","shell.execute_reply":"2025-06-29T08:09:32.383565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class EnsemblePredictor:\n    \"\"\"集成预测器 - 使用多个fold模型投票预测\"\"\"\n    \n    def __init__(self, models_dir, device='cpu'):\n        \"\"\"\n        初始化集成预测器\n        \n        Args:\n            device: 计算设备\n        \"\"\"\n        self.device = device\n        self.models = []\n        self._load_models(models_dir)\n    \n    def _load_models(self, models_dir):\n        \"\"\"加载所有fold的模型\"\"\"\n        # 查找所有fold模型文件\n        model_files = sorted(glob.glob(f\"{models_dir}/fold_*_model.pth\"))\n        print(f\"📁 找到 {len(model_files)} 个fold模型\")\n        \n        # 加载每个fold的模型\n        for model_file in model_files:\n            checkpoint = torch.load(model_file, map_location=self.device, weights_only=False)\n            config = checkpoint['model_config']\n            \n            model = TwoBranchModel(**config)\n            \n            model.load_state_dict(checkpoint['model_state_dict'])\n            model.to(self.device)\n            model.eval()\n            self.models.append(model)\n            \n            print(f\"✅ 加载 {model_file}\")\n        \n    def predict(self, sequence: List[Dict]) -> str:\n        \"\"\"\n        预测单个序列的标签\n        \n        Args:\n            sequence: 输入序列，格式与训练数据相同\n        \n        Returns:\n            predicted_label: 预测的文本标签\n        \"\"\"\n        predictions = []\n        \n        # 使用每个fold的模型进行预测\n        for model in self.models:\n            # 使用SequenceDataset处理序列\n            dataset = SequenceDataset([sequence], [0])\n            \n            # 获取处理后的序列\n            processed_sequence = torch.FloatTensor(dataset.processed_sequences[0]).unsqueeze(0).to(self.device)\n            \n            # 预测\n            with torch.no_grad():\n                outputs = model(processed_sequence)\n                predicted_class = torch.argmax(outputs, dim=1).item()\n                predictions.append(predicted_class)\n        \n        # 投票决定最终预测结果\n        most_common_prediction = Counter(predictions).most_common(1)[0][0]\n        \n        # 转换为文本标签\n        predicted_label = LABEL_NAMES[most_common_prediction]\n        \n        return predicted_label\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:32.385173Z","iopub.execute_input":"2025-06-29T08:09:32.385448Z","iopub.status.idle":"2025-06-29T08:09:32.393111Z","shell.execute_reply.started":"2025-06-29T08:09:32.385407Z","shell.execute_reply":"2025-06-29T08:09:32.392448Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"feature_scaler = joblib.load(f'{SAVED_MODELS}/feature_scaler.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:32.39379Z","iopub.execute_input":"2025-06-29T08:09:32.394079Z","iopub.status.idle":"2025-06-29T08:09:32.41096Z","shell.execute_reply.started":"2025-06-29T08:09:32.394056Z","shell.execute_reply":"2025-06-29T08:09:32.410406Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"predictor = EnsemblePredictor(SAVED_MODELS, device='cuda')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:32.4116Z","iopub.execute_input":"2025-06-29T08:09:32.41178Z","iopub.status.idle":"2025-06-29T08:09:32.602245Z","shell.execute_reply.started":"2025-06-29T08:09:32.411766Z","shell.execute_reply":"2025-06-29T08:09:32.601638Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    sequence = sequence.to_pandas()\n    demographics = demographics.to_pandas()\n\n    sequence = pd.merge(\n        sequence,\n        demographics,\n        on='subject',\n        how='left'\n    )\n\n    sequence = feature_engineering(sequence)\n\n    sequence[NUMERICAL_FEATURES] = feature_scaler.transform(sequence[NUMERICAL_FEATURES])\n    \n    sequence = sequence.groupby(['sequence_id', 'subject']).apply(\n        lambda df: df[FEATURE_NAMES].to_dict(orient='records'),\n        include_groups=False,\n    )\n    \n    sequence = sequence.iloc[0]\n\n    # predict\n    predicted_label = predictor.predict(sequence)\n\n    return predicted_label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-29T08:09:32.603064Z","iopub.execute_input":"2025-06-29T08:09:32.6033Z","iopub.status.idle":"2025-06-29T08:09:32.608483Z","shell.execute_reply.started":"2025-06-29T08:09:32.603282Z","shell.execute_reply":"2025-06-29T08:09:32.607809Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## API","metadata":{}},{"cell_type":"code","source":"import kaggle_evaluation.cmi_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-06-29T08:09:32.60922Z","iopub.execute_input":"2025-06-29T08:09:32.609459Z","iopub.status.idle":"2025-06-29T08:09:34.363911Z","shell.execute_reply.started":"2025-06-29T08:09:32.609422Z","shell.execute_reply":"2025-06-29T08:09:34.363134Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}