{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12328761,"sourceType":"datasetVersion","datasetId":7771623},{"sourceId":242561727,"sourceType":"kernelVersion"}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":3671.920691,"end_time":"2025-09-07T15:42:51.180079","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2025-09-07T14:41:39.259388","version":"2.6.0"}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"6a27df84-70eb-4079-ab5e-f3f6a2477b9e","cell_type":"markdown","source":"# 基于多模态传感器融合与深度学习的BFRB行为识别方法研究","metadata":{}},{"id":"7c413298","cell_type":"markdown","source":"## 初始化","metadata":{"papermill":{"duration":0.004643,"end_time":"2025-09-07T14:41:43.150423","exception":false,"start_time":"2025-09-07T14:41:43.145780","status":"completed"},"tags":[]}},{"id":"76d70ab2","cell_type":"code","source":"import os, json, joblib, numpy as np, pandas as pd\nimport random, math\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\nimport warnings \nwarnings.filterwarnings(\"ignore\")\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.optim import Adam\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.model_selection import StratifiedKFold\nfrom timm.scheduler import CosineLRScheduler\nfrom scipy.signal import firwin\nfrom metric import CompetitionMetric\n\nimport polars as pl","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","papermill":{"duration":15.259192,"end_time":"2025-09-07T14:41:58.413341","exception":false,"start_time":"2025-09-07T14:41:43.154149","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:25:25.283179Z","iopub.execute_input":"2025-09-11T03:25:25.283360Z","iopub.status.idle":"2025-09-11T03:25:37.801742Z","shell.execute_reply.started":"2025-09-11T03:25:25.283344Z","shell.execute_reply":"2025-09-11T03:25:37.801180Z"}},"outputs":[],"execution_count":null},{"id":"168f7319","cell_type":"code","source":"# 配置\n# TRAIN = False                   # 训练时将其设置为 True\nTRAIN = True\nRAW_DIR = Path(\"../input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi3-models-p\") # 在 TRAIN=False 时使用\nEXPORT_DIR = Path(\"./\")         # 输出目录\nBATCH_SIZE = 64                 # 批处理大小\nPAD_PERCENTILE = 100            # 序列填充百分比\nmaxlen = PAD_PERCENTILE         # 最大序列长度\nLR_INIT = 1e-3                  # 初始学习率\nWD = 3e-3                       # 权重衰减\nPATIENCE = 40                   # 早停耐心值\nFOLDS = 5                       # 交叉验证折数\nrandom_state = 42               # 随机种子\nepochs_warmup = 20              # 预热周期数\nwarmup_lr_init = 1.822126131809773e-05  # 预热初始学习率\nlr_min = 3.810323058740104e-09  # 最小学习率\nEPOCHS = 125                    # 训练轮次\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"device: {device}\")\n\nmean = torch.tensor([\n    0,  0, 0, 0, 0,\n    0,  9.0319e-03,  1.0849e+00, -2.6186e-03,  3.7651e-03,\n    -5.3660e-03, -2.8177e-03,  1.3318e-03, -1.5876e-04,  6.3495e-01,\n     6.2877e-01,  6.0607e-01,  6.2142e-01,  6.3808e-01,  6.5420e-01,\n     7.4102e-03, -3.4159e-03, -7.5237e-03, -2.6034e-02,  2.9704e-02,\n    -3.1546e-02, -2.0610e-03, -4.6986e-03, -4.7216e-03, -2.6281e-02,\n     1.5799e-02,  1.0016e-02\n], dtype=torch.float32).view(1, -1, 1).to(device)         \n\nstd = torch.tensor([\n    1, 1, 1, 1, 1, 1, 0.2067, 0.8583, 0.3162,\n    0.2668, 0.2917, 0.2341, 0.3023, 0.3281, 1.0264, 0.8838, 0.8686, 1.0973,\n    1.0267, 0.9018, 0.4658, 0.2009, 0.2057, 1.2240, 0.9535, 0.6655, 0.2941,\n    0.3421, 0.8156, 0.6565, 1.1034, 1.5577\n], dtype=torch.float32).view(1, -1, 1).to(device) + 1e-8  \n\nif TRAIN:\n    print(\"训练模式\")\nelse:\n    print(\"推理模式\")","metadata":{"papermill":{"duration":0.251098,"end_time":"2025-09-07T14:41:58.668458","exception":false,"start_time":"2025-09-07T14:41:58.417360","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:25:37.803234Z","iopub.execute_input":"2025-09-11T03:25:37.803612Z","iopub.status.idle":"2025-09-11T03:25:38.048112Z","shell.execute_reply.started":"2025-09-11T03:25:37.803594Z","shell.execute_reply":"2025-09-11T03:25:38.047451Z"}},"outputs":[],"execution_count":null},{"id":"2f66ca5d","cell_type":"markdown","source":"## 数据描述","metadata":{"papermill":{"duration":0.003502,"end_time":"2025-09-07T14:41:58.675880","exception":false,"start_time":"2025-09-07T14:41:58.672378","status":"completed"},"tags":[]}},{"id":"8d98c085","cell_type":"code","source":"print(\"正在加载数据...\")\ntrain_df = pd.read_csv(RAW_DIR / \"train.csv\")\ntest_df = pd.read_csv(RAW_DIR / \"test.csv\")\ndemographics_df = pd.read_csv(RAW_DIR / \"train_demographics.csv\")\ntest_demo_df = pd.read_csv(RAW_DIR / \"test_demographics.csv\")\n\nprint(f\"训练集形状: {train_df.shape}\")\nprint(f\"测试集形状: {test_df.shape}\")\nprint(f\"训练集人口统计信息形状: {demographics_df.shape}\")\nprint(f\"测试集人口统计信息形状: {test_demo_df.shape}\")\n\nprint(\"\\n人口统计信息\")\nprint(demographics_df.describe())\n\nprint(\"\\n=== Sequence ID 分析 ===\")\ntrain_sequence_counts = train_df['sequence_id'].value_counts()\nprint(f\"真实样本数量（Sequence ID数）: {train_df['sequence_id'].nunique()}\")\nprint(f\"样本时间序列长度统计:\")\nprint(f\"  最小值: {train_sequence_counts.min()}\")\nprint(f\"  最大值: {train_sequence_counts.max()}\")\nprint(f\"\\n样本时间序列长度前10的sequence_id:\")\nprint(train_sequence_counts.head(10))\n\ncount_frequency = train_sequence_counts.value_counts().sort_index()\n\nplt.figure(figsize=(12, 6))\nplt.bar(count_frequency.index, count_frequency.values, alpha=0.7, color='steelblue')\nplt.xlabel('Sample Time Series Length', fontsize=12)\nplt.ylabel('Frequency (Number of Samples)', fontsize=12)\nplt.title('Distribution of Sample Time Series Lengths', fontsize=14)\nplt.grid(axis='y', alpha=0.3)\n\nplt.tight_layout()\nplt.savefig(EXPORT_DIR / 'sequence_length_distribution.png', dpi=300, bbox_inches='tight')\nplt.show()\n\nprint(\"\\n=== 手势标签分布 ===\")\ngesture_counts = train_df['gesture'].value_counts()\nprint(\"\\n标签总数量\\n\", gesture_counts)\n\ngesture_sequence_counts = train_df.groupby('sequence_id')['gesture'].first().value_counts()\nprint(\"\\n标签对应样本数量\\n\", gesture_sequence_counts)\n\nprint(\"\\n=== 手势标签的平均序列长度 ===\")\nsequence_lengths = train_df.groupby('sequence_id').size().reset_index(name='length')\nsequence_gestures = train_df.groupby('sequence_id')['gesture'].first().reset_index()\nsequence_info = pd.merge(sequence_lengths, sequence_gestures, on='sequence_id')\n\ngesture_avg_length = sequence_info.groupby('gesture')['length'].agg(['mean', 'count']).reset_index()\ngesture_avg_length.columns = ['gesture', '平均长度', '序列数量']\ngesture_avg_length = gesture_avg_length.sort_values('平均长度', ascending=False)\n\nprint(\"每个手势标签的平均序列长度:\")\nprint(gesture_avg_length.to_string(index=False, float_format=\"%.2f\"))\n\nprint(\"\\n=== Gesture 和 Orientation 组合统计 ===\")\n\nsequence_gesture_orientation = train_df.groupby('sequence_id')[['gesture', 'orientation']].first()\ngesture_orientation_counts = sequence_gesture_orientation.groupby(['gesture', 'orientation']).size().reset_index(name='sequence_count')\ngesture_orientation_counts = gesture_orientation_counts.sort_values('gesture')\n\nprint(f\"\\n总组合数(实际可能需要分辨的标签数): {len(gesture_orientation_counts)}\")\nprint(\"每个(gesture, orientation)组合的序列数量:\")\nprint(gesture_orientation_counts.to_string(index=False))\n\nprint(\"\\n=== 缺失值分析 ===\")\ntrain_missing = train_df.isnull().sum()\ntrain_missing_pct = (train_missing / len(train_df)) * 100\n\ntest_missing = test_df.isnull().sum()\ntest_missing_pct = (test_missing / len(test_df)) * 100\n\nmissing_summary = pd.DataFrame({\n    '训练集缺失值': train_missing,\n    '训练集缺失比例(%)': train_missing_pct.round(2),\n    '测试集缺失值': test_missing,\n    '测试集缺失比例(%)': test_missing_pct.round(2)\n})\n\nmissing_summary = missing_summary[(missing_summary['训练集缺失值'] > 0) | \n                                 (missing_summary['测试集缺失值'] > 0)]\n\nprint(\"缺失值汇总（仅显示有缺失的列）:\")\nprint(missing_summary)\n\nprint(\"\\n=== 传感器数据分布 ===\")\nimu_cols = [col for col in train_df.columns if col.startswith(('acc_', 'rot_'))]\nprint(f\"IMU传感器特征数量: {len(imu_cols)}\")\n\nthm_cols = [col for col in train_df.columns if col.startswith('thm_')]\nprint(f\"温度传感器特征数量: {len(thm_cols)}\")\n\ntof_cols = [col for col in train_df.columns if col.startswith('tof_')]\nprint(f\"ToF传感器特征数量: {len(tof_cols)}\")\n\n","metadata":{"papermill":{"duration":29.710329,"end_time":"2025-09-07T14:42:28.389649","exception":false,"start_time":"2025-09-07T14:41:58.679320","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:25:38.049032Z","iopub.execute_input":"2025-09-11T03:25:38.049307Z","iopub.status.idle":"2025-09-11T03:26:08.753966Z","shell.execute_reply.started":"2025-09-11T03:25:38.049288Z","shell.execute_reply":"2025-09-11T03:26:08.753388Z"}},"outputs":[],"execution_count":null},{"id":"ddf83d32","cell_type":"markdown","source":"## 数据处理","metadata":{"papermill":{"duration":0.003661,"end_time":"2025-09-07T14:42:28.397460","exception":false,"start_time":"2025-09-07T14:42:28.393799","status":"completed"},"tags":[]}},{"id":"ecc87131","cell_type":"code","source":"# 序列预处理函数\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list, scaler: StandardScaler):\n    \"\"\"规范化并清理时间序列序列\"\"\"\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values     # 填充缺失值\n    return scaler.transform(mat).astype('float32')     # 标准化\n\n# 时间序列处理函数\ndef pad_sequences_torch(sequences, maxlen, padding='post', truncating='post', value=0.0):\n    result = []\n    for seq in sequences:\n        if len(seq) >= maxlen:\n            if truncating == 'post':\n                seq = seq[:maxlen]\n            else:  # 'pre'\n                seq = seq[-maxlen:]\n        else:\n            pad_len = maxlen - len(seq)\n            if padding == 'post':\n                seq = np.concatenate([seq, np.full((pad_len, seq.shape[1]), value)])\n            else:  # 'pre'\n                seq = np.concatenate([np.full((pad_len, seq.shape[1]), value), seq])\n        result.append(seq)\n    return np.array(result, dtype=np.float32)\n\n# 自定义数据集类\nclass CMI3Dataset(Dataset):\n    def __init__(self,\n                 X_list,\n                 y_list,\n                 maxlen,\n                 mode=\"train\",\n                 imu_dim=7,\n                 augment=None):\n        self.X_list = X_list\n        self.mode = mode\n        self.y_list = y_list\n        self.maxlen = maxlen\n        self.imu_dim = imu_dim     \n        self.augment = augment     # 数据增强\n\n    def pad_sequences_torch(self, seq, maxlen, padding='post', truncating='post', value=0.0):\n        # 序列填充或截断\n        if seq.shape[0] >= maxlen:\n            if truncating == 'post':\n                seq = seq[:maxlen]\n            else:  # 'pre'\n                seq = seq[-maxlen:]\n        else:\n            pad_len = maxlen - seq.shape[0]\n            if padding == 'post':\n                seq = np.concatenate([seq, np.full((pad_len, seq.shape[1]), value)])\n            else:  # 'pre'\n                seq = np.concatenate([np.full((pad_len, seq.shape[1]), value), seq])\n        return seq  \n        \n    def __getitem__(self, index):\n        X = self.X_list[index]\n        y = self.y_list[index]\n\n        # 数据增强（仅训练模式）\n        if self.mode == \"train\" and self.augment is not None:\n            X = self.augment(X, self.imu_dim)     \n\n        X = self.pad_sequences_torch(X, self.maxlen, 'pre', 'pre')\n        return X, y\n    \n    def __len__(self):\n        return len(self.X_list)\n","metadata":{"papermill":{"duration":0.014795,"end_time":"2025-09-07T14:42:28.415940","exception":false,"start_time":"2025-09-07T14:42:28.401145","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:26:08.754684Z","iopub.execute_input":"2025-09-11T03:26:08.754894Z","iopub.status.idle":"2025-09-11T03:26:08.764300Z","shell.execute_reply.started":"2025-09-11T03:26:08.754877Z","shell.execute_reply":"2025-09-11T03:26:08.763481Z"}},"outputs":[],"execution_count":null},{"id":"a8fac20b-49c7-404c-86b3-9be92ba6b7c9","cell_type":"markdown","source":"## 数据增强","metadata":{}},{"id":"3040a649","cell_type":"code","source":"# 数据增强类\nclass Augment:\n    def __init__(self,\n                 p_jitter=0.8, sigma=0.02, scale_range=[0.9,1.1],\n                 p_dropout=0.3,\n                 p_moda=0.5,          \n                 drift_std=0.005,     \n                 drift_max=0.25):      \n        self.p_jitter  = p_jitter  # 抖动概率\n        self.sigma     = sigma     # 噪声标准差\n        self.scale_min, self.scale_max = scale_range  # 缩放范围\n        self.p_dropout = p_dropout  # Dropout概率\n        self.p_moda    = p_moda     # 运动漂移概率\n        self.drift_std = drift_std  # 漂移标准差\n        self.drift_max = drift_max  # 最大漂移值\n\n    # 抖动与缩放增强\n    def jitter_scale(self, x: np.ndarray) -> np.ndarray:\n        noise  = np.random.randn(*x.shape) * self.sigma\n        scale  = np.random.uniform(self.scale_min,\n                                   self.scale_max,\n                                   size=(1, x.shape[1]))\n        return (x + noise) * scale\n\n    # 传感器随机屏蔽\n    def sensor_dropout(self,\n                       x: np.ndarray,\n                       imu_dim: int) -> np.ndarray:\n\n        if random.random() < self.p_dropout:\n            x[:, imu_dim:] = 0.0    # 将非IMU传感器数据置零\n        return x\n\n    # 运动漂移增强\n    def motion_drift(self, x: np.ndarray, imu_dim: int) -> np.ndarray:\n\n        T = x.shape[0]\n\n        # 生成漂移序列\n        drift = np.cumsum(\n            np.random.normal(scale=self.drift_std, size=(T, 1)),\n            axis=0\n        )\n        drift = np.clip(drift, -self.drift_max, self.drift_max)   \n\n        # 应用漂移到IMU数据\n        x[:, :6] += drift\n\n        if imu_dim > 6:\n            x[:, 6:imu_dim] += drift     \n        return x\n    \n    def __call__(self,\n                 x: np.ndarray,\n                 imu_dim: int) -> np.ndarray:\n        if random.random() < self.p_jitter:\n            x = self.jitter_scale(x)    # 应用抖动和缩放\n\n        if random.random() < self.p_moda:\n            x = self.motion_drift(x, imu_dim)    # 应用运动漂移\n \n        x = self.sensor_dropout(x, imu_dim)   # 应用传感器Dropout\n        return x\n\n","metadata":{"papermill":{"duration":0.012311,"end_time":"2025-09-07T14:42:28.524243","exception":false,"start_time":"2025-09-07T14:42:28.511932","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:26:08.764991Z","iopub.execute_input":"2025-09-11T03:26:08.765225Z","iopub.status.idle":"2025-09-11T03:26:08.825861Z","shell.execute_reply.started":"2025-09-11T03:26:08.765202Z","shell.execute_reply":"2025-09-11T03:26:08.825287Z"}},"outputs":[],"execution_count":null},{"id":"b17adf58","cell_type":"markdown","source":"## 特征提取","metadata":{"papermill":{"duration":0.003571,"end_time":"2025-09-07T14:42:28.423257","exception":false,"start_time":"2025-09-07T14:42:28.419686","status":"completed"},"tags":[]}},{"id":"942af74e","cell_type":"code","source":"# IMU特征提取器类\nclass ImuFeatureExtractor(nn.Module):\n    def __init__(self, fs=100., add_quaternion=False):\n        super().__init__()\n        self.fs = fs\n        self.add_quaternion = add_quaternion\n\n        k = 15\n        # 低通滤波器卷积层\n        self.lpf = nn.Conv1d(6, 6, kernel_size=k, padding=k//2,\n                             groups=6, bias=False)\n        nn.init.kaiming_uniform_(self.lpf.weight, a=math.sqrt(5))\n\n        # 加速度计和陀螺仪的低通滤波器\n        self.lpf_acc  = nn.Conv1d(3, 3, k, padding=k//2, groups=3, bias=False)\n        self.lpf_gyro = nn.Conv1d(3, 3, k, padding=k//2, groups=3, bias=False)\n\n    def forward(self, imu):\n        #分离加速度计和陀螺仪数据\n        B, C, T = imu.shape\n        acc  = imu[:, 0:3, :]                 # acc_x, acc_y, acc_z\n        gyro = imu[:, 3:6, :]                 # gyro_x, gyro_y, gyro_z\n        extra = imu[:, 6:, :]                 \n\n        # 1) 计算幅度\n        acc_mag  = torch.norm(acc,  dim=1, keepdim=True)          # (B,1,T)\n        gyro_mag = torch.norm(gyro, dim=1, keepdim=True)\n\n        # 2) 计算变化率（加速度变化率和角速度变化率）\n        jerk = F.pad(acc[:, :, 1:] - acc[:, :, :-1], (1,0))       # (B,3,T)\n        gyro_delta = F.pad(gyro[:, :, 1:] - gyro[:, :, :-1], (1,0))\n\n        # 3) 计算能量（平方）\n        acc_pow  = acc ** 2\n        gyro_pow = gyro ** 2\n\n        # 4) LPF / HPF (低通滤波器 / 高通滤波器)\n        acc_lpf  = self.lpf_acc(acc)\n        acc_hpf  = acc - acc_lpf\n        gyro_lpf = self.lpf_gyro(gyro)\n        gyro_hpf = gyro - gyro_lpf\n\n        # 合并所有特征\n        features = [\n            acc, gyro,\n            acc_mag, gyro_mag,\n            jerk, gyro_delta,\n            acc_pow, gyro_pow,\n            acc_lpf, acc_hpf,\n            gyro_lpf, gyro_hpf,\n        ]\n        return torch.cat(features, dim=1)  # (B, C_out, T)","metadata":{"papermill":{"duration":0.012918,"end_time":"2025-09-07T14:42:28.440633","exception":false,"start_time":"2025-09-07T14:42:28.427715","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:26:08.826551Z","iopub.execute_input":"2025-09-11T03:26:08.826760Z","iopub.status.idle":"2025-09-11T03:26:08.839696Z","shell.execute_reply.started":"2025-09-11T03:26:08.826744Z","shell.execute_reply":"2025-09-11T03:26:08.839187Z"}},"outputs":[],"execution_count":null},{"id":"0f19ddae","cell_type":"markdown","source":"## 模型定义","metadata":{"papermill":{"duration":0.003626,"end_time":"2025-09-07T14:42:28.447871","exception":false,"start_time":"2025-09-07T14:42:28.444245","status":"completed"},"tags":[]}},{"id":"ec580958","cell_type":"code","source":"# 压缩激励块（SEBlock）\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\n# 残差SE-CNN块\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        # 第一个卷积块\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        # 第二个卷积块\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块\n        self.se = SEBlock(out_channels)\n        \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        # 首次卷积\n        out = F.relu(self.bn1(self.conv1(x)))\n        # 第二次卷积\n        out = self.bn2(self.conv2(out))\n        \n        # SE 块\n        out = self.se(out)\n        \n        # 添加快捷连接\n        out += shortcut\n        out = F.relu(out)\n        \n        # 池化和Dropout\n        out = self.pool(out)\n        out = self.dropout(out)\n        \n        return out\n\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\n# 双分支模型（IMU和TOF/Thermal）\nclass TwoBranchModel(nn.Module):\n    def __init__(self, pad_len, imu_dim_raw, tof_dim, n_classes, dropouts=[0.3, 0.3, 0.3, 0.3, 0.4, 0.5, 0.3], feature_engineering=True, **kwargs):\n        super().__init__()\n        self.feature_engineering = feature_engineering\n        if feature_engineering:\n            self.imu_fe = ImuFeatureExtractor(**kwargs)    # IMU特征工程\n            imu_dim = 32            \n        else:\n            self.imu_fe = nn.Identity()    # 恒等映射\n\n            imu_dim = imu_dim_raw   \n            \n        self.imu_dim = imu_dim\n        self.tof_dim = tof_dim\n\n        self.fir_nchan = 7    # FIR滤波器通道数\n\n        weight_decay = 3e-3\n\n        # FIR滤波器初始化\n        numtaps = 33  \n        fir_coef = firwin(numtaps, cutoff=1.0, fs=10.0, pass_zero=False)\n        fir_kernel = torch.tensor(fir_coef, dtype=torch.float32).view(1, 1, -1)\n        fir_kernel = fir_kernel.repeat(7, 1, 1)  # (imu_dim, 1, numtaps)\n        self.register_buffer(\"fir_kernel\", fir_kernel)\n        \n        # IMU 深度分支\n        self.imu_block1 = ResidualSECNNBlock(imu_dim, 64, 3, dropout=dropouts[0], weight_decay=weight_decay)\n        self.imu_block2 = ResidualSECNNBlock(64, 128, 5, dropout=dropouts[1], weight_decay=weight_decay)\n        \n        # TOF/Thermal 轻量分支\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(dropouts[2])\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(dropouts[3])\n        \n        # BiLSTM层\n        self.bilstm = nn.LSTM(256, 128, bidirectional=True, batch_first=True)\n        self.lstm_dropout = nn.Dropout(dropouts[4])\n        \n        # 注意力层\n        self.attention = AttentionLayer(256)  # 128*2 for bidirectional\n        \n        # 全连接层\n        self.dense1 = nn.Linear(256, 256, bias=False)\n        self.bn_dense1 = nn.BatchNorm1d(256)\n        self.drop1 = nn.Dropout(dropouts[5])\n        \n        self.dense2 = nn.Linear(256, 128, bias=False)\n        self.bn_dense2 = nn.BatchNorm1d(128)\n        self.drop2 = nn.Dropout(dropouts[6])\n        \n        self.classifier = nn.Linear(128, n_classes)    # 分类器\n        \n    def forward(self, x):\n        # 分割输入数据        \n        imu = x[:, :, :self.fir_nchan].transpose(1, 2)  # (batch, imu_dim, seq_len)\n        tof = x[:, :, self.fir_nchan:].transpose(1, 2)  # (batch, tof_dim, seq_len)\n\n        imu = self.imu_fe(imu)   # (B, imu_dim, T)\n        # 应用FIR滤波器\n        filtered = F.conv1d(\n            imu[:, :self.fir_nchan, :],        # (B,7,T)\n            self.fir_kernel,\n            padding=self.fir_kernel.shape[-1] // 2,\n            groups=self.fir_nchan,\n        )\n        \n        imu = torch.cat([filtered, imu[:, self.fir_nchan:, :]], dim=1)  \n        imu = (imu - mean) / std    # 标准化\n        # IMU 分支处理\n        x1 = self.imu_block1(imu)\n        x1 = self.imu_block2(x1)\n        \n        # TOF 分支处理\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        # 合并分支\n        merged = torch.cat([x1, x2], dim=1).transpose(1, 2)  # (batch, seq_len, 256)\n        \n        # BiLSTM处理\n        lstm_out, _ = self.bilstm(merged)\n        lstm_out = self.lstm_dropout(lstm_out)\n        \n        # 注意力机制\n        attended = self.attention(lstm_out)\n        \n        # 全连接层处理\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        # 分类输出\n        logits = (self.classifier(x))\n        return logits\n","metadata":{"papermill":{"duration":0.024644,"end_time":"2025-09-07T14:42:28.476227","exception":false,"start_time":"2025-09-07T14:42:28.451583","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:26:08.841881Z","iopub.execute_input":"2025-09-11T03:26:08.842106Z","iopub.status.idle":"2025-09-11T03:26:08.862553Z","shell.execute_reply.started":"2025-09-11T03:26:08.842083Z","shell.execute_reply":"2025-09-11T03:26:08.861785Z"}},"outputs":[],"execution_count":null},{"id":"28fe319b","cell_type":"markdown","source":"## 训练设置","metadata":{"papermill":{"duration":0.00356,"end_time":"2025-09-07T14:42:28.483783","exception":false,"start_time":"2025-09-07T14:42:28.480223","status":"completed"},"tags":[]}},{"id":"88a60c8b","cell_type":"code","source":"# 早停\nclass EarlyStopping:\n    def __init__(self, patience=7, min_delta=0, restore_best_weights=True):\n        self.patience = patience\n        self.min_delta = min_delta\n        self.restore_best_weights = restore_best_weights\n        self.best_loss = None\n        self.counter = 0\n        self.best_weights = None\n        \n    def __call__(self, val_loss, model):\n        if self.best_loss is None:\n            self.best_loss = val_loss\n            self.save_checkpoint(model)\n        elif val_loss < self.best_loss - self.min_delta:\n            self.best_loss = val_loss\n            self.counter = 0\n            self.save_checkpoint(model)\n        else:\n            self.counter += 1\n            \n        if self.counter >= self.patience:\n            if self.restore_best_weights:\n                model.load_state_dict(self.best_weights)\n            return True\n        return False\n    \n    def save_checkpoint(self, model):\n        self.best_weights = model.state_dict().copy()\n\n\n# 指数移动平均（EMA）类\nclass EMA:\n    def __init__(self, model, decay=0.999):\n        self.decay = decay\n        self.shadow = {}\n        self.backup = {}\n\n        for name, param in model.named_parameters():\n            if param.requires_grad:\n                self.shadow[name] = param.data.clone()\n\n    def update(self, model):\n        for name, param in model.named_parameters():\n            if param.requires_grad:\n                assert name in self.shadow\n                new_average = (1.0 - self.decay) * param.data + self.decay * self.shadow[name]\n                self.shadow[name] = new_average.clone()\n\n    def apply_shadow(self, model):\n        self.backup = {}\n        for name, param in model.named_parameters():\n            if param.requires_grad:\n                self.backup[name] = param.data.clone()\n                param.data = self.shadow[name]\n\n    def restore(self, model):\n        for name, param in model.named_parameters():\n            if param.requires_grad and name in self.backup:\n                param.data = self.backup[name]\n        self.backup = {}\n\n# 设置随机种子\ndef set_seed(seed: int = 42):\n    random.seed(seed)\n\n    os.environ['PYTHONHASHSEED'] = str(seed)\n\n    np.random.seed(seed)\n\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed) ","metadata":{"papermill":{"duration":0.013345,"end_time":"2025-09-07T14:42:28.500893","exception":false,"start_time":"2025-09-07T14:42:28.487548","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:26:08.863239Z","iopub.execute_input":"2025-09-11T03:26:08.863453Z","iopub.status.idle":"2025-09-11T03:26:08.879713Z","shell.execute_reply.started":"2025-09-11T03:26:08.863429Z","shell.execute_reply":"2025-09-11T03:26:08.879185Z"}},"outputs":[],"execution_count":null},{"id":"89cad3c1","cell_type":"markdown","source":"## 模型训练","metadata":{"papermill":{"duration":0.003515,"end_time":"2025-09-07T14:42:28.531422","exception":false,"start_time":"2025-09-07T14:42:28.527907","status":"completed"},"tags":[]}},{"id":"fc079dcb","cell_type":"code","source":"# 训练模式下的数据处理\nif TRAIN:\n    print(\"TRAIN MODE – loading dataset\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n\n    # 标签编码\n    le = LabelEncoder()\n    df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n\n    # 特征列表\n    meta_cols = {'gesture', 'gesture_int', 'sequence_type', 'behavior', 'orientation',\n                 'row_id', 'subject', 'phase', 'sequence_id', 'sequence_counter'}\n    feature_cols = [c for c in df.columns if c not in meta_cols]\n\n    # 分离IMU和TOF/THM特征\n    imu_cols = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n    tof_cols = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n    print(f\"  IMU {len(imu_cols)} | TOF/THM {len(tof_cols)} | total {len(feature_cols)} features\")\n\n    # 全局缩放器\n    scaler = StandardScaler().fit(df[feature_cols].ffill().bfill().fillna(0).values)\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n\n    # 构建序列\n    seq_gp = df.groupby('sequence_id')\n    X_list, y_list, id_list = [], [], []\n    for seq_id, seq in seq_gp:\n        mat = preprocess_sequence(seq, feature_cols, scaler)\n        X_list.append(mat)\n        y_list.append(seq['gesture_int'].iloc[0])    # 获取序列的标签\n        id_list.append(seq_id)\n        # lens.append(len(mat))\n    \n    pad_len = PAD_PERCENTILE#int(np.percentile(lens, PAD_PERCENTILE))\n    print(pad_len)\n    np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(feature_cols))\n    id_list = np.array(id_list)\n    X_list_all = pad_sequences_torch(X_list, maxlen=pad_len, padding='pre', truncating='pre')\n    y_list_all = np.eye(len(le.classes_))[y_list].astype(np.float32)  # One-hot 编码\n\n    # 数据增强器\n    augmenter = Augment(\n        p_jitter=0.9844818619033621, \n        sigma=0.03291295776089293, \n        scale_range=(0.7542342630597011,1.1625052821731077),\n        p_dropout=0.41782786013520684,\n        p_moda=0.3910622476959722, \n        drift_std=0.0040285239353308015, \n        drift_max=0.3929358950258158    \n    )","metadata":{"papermill":{"duration":46.231841,"end_time":"2025-09-07T14:43:14.766935","exception":false,"start_time":"2025-09-07T14:42:28.535094","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:26:08.880393Z","iopub.execute_input":"2025-09-11T03:26:08.880625Z","iopub.status.idle":"2025-09-11T03:26:55.890737Z","shell.execute_reply.started":"2025-09-11T03:26:08.880604Z","shell.execute_reply":"2025-09-11T03:26:55.890181Z"}},"outputs":[],"execution_count":null},{"id":"4bbdeacb-4d51-4e0b-819f-d51f83d30358","cell_type":"code","source":"# 数据集划分统计\nif TRAIN:\n    print(\"\\n=== 数据集划分统计 ===\\n\")\n    \n    # 预先创建序列到元数据的映射\n    sequence_metadata = {}\n    for seq_id in id_list:\n        seq_data = df[df['sequence_id'] == seq_id].iloc[0]\n        sequence_metadata[seq_id] = {\n            'subject': seq_data['subject'],\n            'gesture': seq_data['gesture'],\n            'gesture_int': seq_data['gesture_int']\n        }\n    \n    # 获取所有受试者ID\n    all_subjects = demographics_df['subject'].unique()\n    \n    # 分层K折交叉验证\n    skf = StratifiedKFold(n_splits=FOLDS, shuffle=True, random_state=random_state)\n    \n    # 获取序列对应的标签（用于分层划分）\n    sequence_labels = np.array([sequence_metadata[seq_id]['gesture_int'] for seq_id in id_list])\n    \n    # 定义BFRB样手势\n    bfrb_gestures = [\n        'Eyebrow - pull hair', 'Eyelash - pull hair', 'Above ear - pull hair',\n        'Forehead - pull hairline', 'Neck - scratch', 'Neck - pinch skin',\n        'Forehead - scratch', 'Cheek - pinch skin'\n    ]\n    \n    # 获取所有手势类别（在训练模式下使用le.classes_）\n    if TRAIN:\n        all_gesture_classes = le.classes_\n    else:\n        all_gesture_classes = gesture_classes\n    \n    # 对于第一个fold进行统计\n    for fold, (train_idx, val_idx) in enumerate(skf.split(id_list, sequence_labels)):\n        if fold == 0:  # 只统计第一个fold\n            # 获取训练集和验证集的序列ID\n            train_sequences = id_list[train_idx]\n            val_sequences = id_list[val_idx]\n            \n            # 统计序列数量\n            train_seq_count = len(train_sequences)\n            val_seq_count = len(val_sequences)\n            total_seq_count = len(id_list)\n            \n            # 统计受试者数量（使用集合去重）\n            train_subjects = set(sequence_metadata[seq_id]['subject'] for seq_id in train_sequences)\n            val_subjects = set(sequence_metadata[seq_id]['subject'] for seq_id in val_sequences)\n            \n            train_subject_count = len(train_subjects)\n            val_subject_count = len(val_subjects)\n            total_subject_count = len(all_subjects)\n            \n            # 统计BFRB样手势样本数\n            train_bfrb_count = sum(1 for seq_id in train_sequences \n                                 if sequence_metadata[seq_id]['gesture'] in bfrb_gestures)\n            val_bfrb_count = sum(1 for seq_id in val_sequences \n                               if sequence_metadata[seq_id]['gesture'] in bfrb_gestures)\n            total_bfrb_count = sum(1 for seq_id in id_list \n                                 if sequence_metadata[seq_id]['gesture'] in bfrb_gestures)\n            \n            # 统计非BFRB样手势样本数\n            train_non_bfrb_count = train_seq_count - train_bfrb_count\n            val_non_bfrb_count = val_seq_count - val_bfrb_count\n            total_non_bfrb_count = total_seq_count - total_bfrb_count\n            \n            # 统计类别数量\n            train_classes = set(sequence_metadata[seq_id]['gesture'] for seq_id in train_sequences)\n            val_classes = set(sequence_metadata[seq_id]['gesture'] for seq_id in val_sequences)\n            \n            train_class_count = len(train_classes)\n            val_class_count = len(val_classes)\n            total_class_count = len(all_gesture_classes)\n            \n            # 使用pandas DataFrame输出统计结果\n            stats_data = {\n                '统计指标': ['序列数量', '受试者数量', 'BFRB样手势样本数', '非BFRB样手势样本数', '类别数量'],\n                '训练集 (Train Set)': [train_seq_count, train_subject_count, train_bfrb_count, train_non_bfrb_count, train_class_count],\n                '验证集 (Validation Set)': [val_seq_count, val_subject_count, val_bfrb_count, val_non_bfrb_count, val_class_count],\n                '全体数据集 (Total)': [total_seq_count, total_subject_count, total_bfrb_count, total_non_bfrb_count, total_class_count]\n            }\n            \n            stats_df = pd.DataFrame(stats_data)\n            print(\"数据集划分统计表:\")\n            print(\"=\" * 80)\n            print(stats_df.to_string(index=False))\n            print(\"=\" * 80)\n            break","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T04:53:26.431227Z","iopub.execute_input":"2025-09-11T04:53:26.431751Z","iopub.status.idle":"2025-09-11T04:58:47.208089Z","shell.execute_reply.started":"2025-09-11T04:53:26.431728Z","shell.execute_reply":"2025-09-11T04:58:47.207320Z"}},"outputs":[],"execution_count":null},{"id":"58bf42f6","cell_type":"code","source":"# 训练循环\nif TRAIN:\n    # 分层K折交叉验证\n    skf = StratifiedKFold(n_splits=FOLDS, shuffle=True, random_state=random_state)\n\n    # 为每个fold创建图表\n    for fold, (train_idx, val_idx) in enumerate(skf.split(id_list, np.argmax(y_list_all, axis=1))):\n        print(f\"\\n{'='*50}\")\n        print(f\"Starting training for fold {fold+1}/{FOLDS}\")\n        print(f\"{'='*50}\")\n\n        # 初始化存储训练历史的列表\n        train_losses = []\n        val_losses = []\n        train_accs = []\n        val_accs = []\n        \n        train_list = X_list_all[train_idx]\n        train_y_list = y_list_all[train_idx]\n        val_list = X_list_all[val_idx]\n        val_y_list = y_list_all[val_idx]\n        \n        # 数据加载器\n        train_dataset = CMI3Dataset(train_list, train_y_list, maxlen, mode=\"train\", imu_dim=len(imu_cols),\n                                augment=augmenter)\n        train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=4,drop_last=True)\n    \n        val_dataset = CMI3Dataset(val_list, val_y_list, maxlen, mode=\"val\")\n        val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=4,drop_last=True)\n    \n        # 模型初始化\n        model = TwoBranchModel(maxlen, len(imu_cols), len(tof_cols), \n                      len(le.classes_)).to(device)\n        ema = EMA(model, decay=0.999)   # 指数移动平均\n        \n        # 优化器和学习率调度器\n        optimizer = Adam(model.parameters(), lr=LR_INIT, weight_decay=WD)\n        steps_per_epoch = len(train_loader)\n        scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=5*steps_per_epoch)\n        \n        steps_per_epoch = len(train_loader)\n        nbatch = len(train_loader)\n        warmup = epochs_warmup * nbatch\n        nsteps = EPOCHS * nbatch\n        scheduler = CosineLRScheduler(optimizer,\n                          warmup_t=warmup, warmup_lr_init=warmup_lr_init, warmup_prefix=True,\n                          t_initial=(nsteps - warmup), lr_min=lr_min) \n    \n        early_stopping = EarlyStopping(patience=PATIENCE, restore_best_weights=True)\n    \n        train_loss = 0.0\n        train_acc = 0.0\n        val_loss = 0.0\n        val_acc = 0.0\n        val_best_acc = 0.0\n        i_scheduler = 0\n        \n        # 训练循环\n        for epoch in range(EPOCHS):\n            model.train()\n            train_preds = []\n            train_targets = []\n            train_loss_epoch = 0.0\n            \n            for X, y in (train_loader):  \n                X, y = X.float().to(device), y.to(device)\n                optimizer.zero_grad()\n                logits = model(X)\n    \n                loss = -torch.sum(F.log_softmax(logits, dim=1) * y, dim=1).mean()    # 交叉熵损失\n                loss.backward()\n                optimizer.step()\n                ema.update(model)    # 更新EMA \n                train_preds.extend(logits.argmax(dim=1).cpu().numpy())\n                train_targets.extend(y.argmax(dim=1).cpu().numpy())\n                scheduler.step(i_scheduler)\n                i_scheduler +=1\n    \n                train_loss_epoch += loss.item()\n                \n            # 计算训练损失和准确率\n            train_loss_epoch /= len(train_loader)\n            train_acc_epoch = CompetitionMetric().calculate_hierarchical_f1(\n                pd.DataFrame({'gesture': le.classes_[train_targets]}),\n                pd.DataFrame({'gesture': le.classes_[train_preds]}))\n            \n            model.eval()\n            val_preds = []\n            val_targets = []\n            val_loss_epoch = 0.0\n            with torch.inference_mode():\n                for X, y in (val_loader):  \n                    half = BATCH_SIZE // 2         \n\n                    # 数据增强：部分TOF数据置零\n                    x_front = X[:half]               \n                    x_back  = X[half:].clone()      \n                    \n                    x_back[:, :, 7:] = 0.0    \n                    X = torch.cat([x_front, x_back], dim=0)  # (B, C, T)\n                    X, y = X.float().to(device), y.to(device)\n                    \n                    logits = model(X)\n                    val_preds.extend(logits.argmax(dim=1).cpu().numpy())\n                    val_targets.extend(y.argmax(dim=1).cpu().numpy())\n                    \n                    loss = F.cross_entropy(logits, y)\n                    val_loss_epoch += loss.item()\n\n            # 计算验证损失和准确率\n            val_loss_epoch /= len(val_loader)\n            val_acc_epoch = CompetitionMetric().calculate_hierarchical_f1(\n                pd.DataFrame({'gesture': le.classes_[val_targets]}),\n                pd.DataFrame({'gesture': le.classes_[val_preds]}))\n            \n            # 存储历史记录\n            train_losses.append(train_loss_epoch)\n            val_losses.append(val_loss_epoch)\n            train_accs.append(train_acc_epoch)\n            val_accs.append(val_acc_epoch)\n            \n            # 打印进度\n            if (epoch + 1) % 10 == 0 or epoch == 0 or epoch == EPOCHS - 1:\n                print(f\"Epoch {epoch+1:3d}/{EPOCHS} - \"\n                      f\"Train Loss: {train_loss_epoch:.4f}, Train Acc: {train_acc_epoch:.4f} - \"\n                      f\"Val Loss: {val_loss_epoch:.4f}, Val Acc: {val_acc_epoch:.4f}\")\n            \n            # 早停检查\n            if early_stopping(val_loss_epoch, model):\n                print(f\"Early stopping triggered at epoch {epoch+1}\")\n                break\n        \n        # 保存模型\n        torch.save({\n            'model_state_dict': model.state_dict(),\n            'imu_dim': len(imu_cols),\n            'tof_dim': len(tof_cols),\n            'n_classes': len(le.classes_),\n            'pad_len': pad_len\n        }, EXPORT_DIR / f\"gesture_two_branch_fold{fold}.pth\")\n        \n        # 绘制训练历史图表\n        plt.figure(figsize=(12, 5))\n        \n        # 损失图表\n        plt.subplot(1, 2, 1)\n        plt.plot(train_losses, label='Train Loss')\n        plt.plot(val_losses, label='Validation Loss')\n        plt.title(f'Fold {fold+1} - Loss')\n        plt.xlabel('Epoch')\n        plt.ylabel('Loss')\n        plt.legend()\n        plt.grid(True)\n        \n        # 准确率图表\n        plt.subplot(1, 2, 2)\n        plt.plot(train_accs, label='Train Accuracy')\n        plt.plot(val_accs, label='Validation Accuracy')\n        plt.title(f'Fold {fold+1} - Accuracy')\n        plt.xlabel('Epoch')\n        plt.ylabel('Accuracy')\n        plt.legend()\n        plt.grid(True)\n        \n        plt.tight_layout()\n        plt.savefig(EXPORT_DIR / f'training_history_fold{fold}.png')\n        plt.show()\n        plt.close()\n        \n        print(f\"Fold {fold+1} completed - Best Val Acc: {max(val_accs):.4f}\")\n        print(\"Training history plot saved as\", EXPORT_DIR / f'training_history_fold{fold}.png')\n\n    print(\"Training done - all artefacts saved in\", EXPORT_DIR)","metadata":{"papermill":{"duration":3572.868266,"end_time":"2025-09-07T15:42:47.639464","exception":false,"start_time":"2025-09-07T14:43:14.771198","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T04:04:46.239114Z","iopub.execute_input":"2025-09-11T04:04:46.239712Z","iopub.status.idle":"2025-09-11T04:07:19.569704Z","shell.execute_reply.started":"2025-09-11T04:04:46.239688Z","shell.execute_reply":"2025-09-11T04:07:19.568689Z"}},"outputs":[],"execution_count":null},{"id":"46611a2e","cell_type":"markdown","source":"## 预测","metadata":{"papermill":{"duration":0.012556,"end_time":"2025-09-07T15:42:47.665875","exception":false,"start_time":"2025-09-07T15:42:47.653319","status":"completed"},"tags":[]}},{"id":"9fd5d55c","cell_type":"code","source":"# 推理模式 \nif TRAIN == False:\n    # 推理模式：加载预训练模型和预处理工具\n    print(\"INFERENCE MODE – loading artefacts from\", PRETRAINED_DIR)\n    feature_cols = np.load(PRETRAINED_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n    pad_len = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\n    scaler = joblib.load(PRETRAINED_DIR / \"scaler.pkl\")\n    gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n\n    imu_cols = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n    tof_cols = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n\n    \n    # 加载所有折的模型\n    MODELS = [f'gesture_two_branch_fold{i}.pth' for i in range(5)]\n    \n    models = []\n    for path in MODELS:\n        checkpoint = torch.load(PRETRAINED_DIR / path, map_location=device)\n        \n        model = TwoBranchModel(\n            checkpoint['pad_len'], \n            checkpoint['imu_dim'], \n            checkpoint['tof_dim'], \n            checkpoint['n_classes']\n            ).to(device)\n        \n        model.load_state_dict(checkpoint['model_state_dict'])\n        model.eval()\n        models.append(model)\n\n    print(\"  model, scaler, pads loaded – ready for evaluation\")\n\n# 确保gesture_classes在两个模式下都存在\nif TRAIN:\n    gesture_classes = le.classes_\n","metadata":{"papermill":{"duration":0.02121,"end_time":"2025-09-07T15:42:47.699339","exception":false,"start_time":"2025-09-07T15:42:47.678129","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:32:45.006965Z","iopub.status.idle":"2025-09-11T03:32:45.007407Z","shell.execute_reply.started":"2025-09-11T03:32:45.007220Z","shell.execute_reply":"2025-09-11T03:32:45.007240Z"}},"outputs":[],"execution_count":null},{"id":"538f01a4","cell_type":"code","source":"# 预测函数\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    global gesture_classes\n    if gesture_classes is None:\n        gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n\n    df_seq = sequence.to_pandas()\n    mat = preprocess_sequence(df_seq, feature_cols, scaler)   # 预处理\n    pad = pad_sequences_torch([mat], maxlen=pad_len, padding='pre', truncating='pre')    # 填充\n    \n    with torch.no_grad():\n        x = torch.FloatTensor(pad).to(device)\n        outputs = None\n        # 模型集成预测\n        for model in models:\n            model.eval()\n            p = torch.softmax(model(x), dim=1)\n            if outputs is None: outputs = p\n            else: outputs += p\n        outputs /= len(models)    # 平均预测结果\n        \n        idx = int(outputs.argmax(dim=1)[0].cpu().numpy())    # 获取预测类别\n    \n    return str(gesture_classes[idx])   # 返回手势名称\n","metadata":{"papermill":{"duration":0.018868,"end_time":"2025-09-07T15:42:47.730425","exception":false,"start_time":"2025-09-07T15:42:47.711557","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:32:45.008108Z","iopub.status.idle":"2025-09-11T03:32:45.008338Z","shell.execute_reply.started":"2025-09-11T03:32:45.008233Z","shell.execute_reply":"2025-09-11T03:32:45.008242Z"}},"outputs":[],"execution_count":null},{"id":"0ebeb905","cell_type":"code","source":"# Kaggle竞赛接口\nimport kaggle_evaluation.cmi_inference_server\nif TRAIN:\n    print(\"训练完成\")\nelse:\n    inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n    \n    if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        inference_server.serve()\n    else:\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    print(\"预测完成\")","metadata":{"papermill":{"duration":0.258191,"end_time":"2025-09-07T15:42:48.000791","exception":false,"start_time":"2025-09-07T15:42:47.742600","status":"completed"},"tags":[],"trusted":true,"execution":{"iopub.status.busy":"2025-09-11T03:32:45.009211Z","iopub.status.idle":"2025-09-11T03:32:45.009417Z","shell.execute_reply.started":"2025-09-11T03:32:45.009321Z","shell.execute_reply":"2025-09-11T03:32:45.009330Z"}},"outputs":[],"execution_count":null}]}