{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":102335,"databundleVersionId":12518947},{"sourceType":"datasetVersion","sourceId":12466079,"datasetId":7740030,"databundleVersionId":13040355},{"sourceType":"modelInstanceVersion","sourceId":466189,"databundleVersionId":12993595,"modelInstanceId":376120}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"> **1. Attention Mechanism Replacement:**  \n>   - Substituted the original SE modules and linear attention with **CA (Coordinate Attention)** and **MLP Attention** mechanisms.  \n>   - Implementation referenced from: [https://www.kaggle.com/code/khoatran311/simplified-convolutional-gesture-classifier](https://www.kaggle.com/code/khoatran311/simplified-convolutional-gesture-classifier)  \n>   \n> **2. Feature Engineering & Model Architecture:**  \n>   - Leveraged feature engineering techniques and model structure from: [https://www.kaggle.com/code/jiazhuang/cmi-imu-only-lstm](https://www.kaggle.com/code/jiazhuang/cmi-imu-only-lstm)  \n>   \n> **3. Learning Rate Schedule:**  \n>   - Used **linear warm-up** for the first 3 epochs.  \n>   - Followed by **cosine annealing decay** for subsequent epochs.  \n>   \n> **4. Data Augmentation:**  \n>   - Applied various data augmentation techniques during training.  \n>   \n> **5. Dataset Preprocessing (`cmi-data`):**  \n>   - Processed raw CSV files by extracting and saving each data sample as **npy file**. For more details, please refer to the inference code section in this notebook.","metadata":{}},{"cell_type":"code","source":"# 2025-07-09 11:25:00 [INFO] Average F1 Score: 0.7858\n# 2025-07-09 11:25:00 [INFO] 各Fold分数详情:\n# 2025-07-09 11:25:00 [INFO]   Fold 1: 0.7734\n# 2025-07-09 11:25:00 [INFO]   Fold 2: 0.8004\n# 2025-07-09 11:25:00 [INFO]   Fold 3: 0.7856\n# 2025-07-09 11:25:00 [INFO]   Fold 4: 0.7701\n# 2025-07-09 11:25:00 [INFO]   Fold 5: 0.7994","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:47.081466Z","iopub.execute_input":"2025-07-14T12:14:47.081727Z","iopub.status.idle":"2025-07-14T12:14:47.086580Z","shell.execute_reply.started":"2025-07-14T12:14:47.081705Z","shell.execute_reply":"2025-07-14T12:14:47.085759Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.nn.utils.rnn import pad_sequence\nfrom tqdm import tqdm\nfrom pathlib import Path\nimport pickle\nfrom scipy.spatial.transform import Rotation as R\nimport warnings\nimport torch.nn as nn\nimport torch.optim as optim\nimport time\nimport torch.nn.functional as F\nfrom copy import deepcopy\nimport gc\nimport random, math\nimport warnings \nfrom torch.optim import Adam, AdamW, Adamax\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\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\nimport polars as pl\nfrom sklearn.model_selection import StratifiedGroupKFold, GroupKFold\nimport logging\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom sklearn.metrics import classification_report, confusion_matrix\n\nclass ToFScaler:\n    def __init__(self):\n        self.means_full = np.zeros(320)  # 320维均值向量\n        self.stds_full = np.ones(320)    # 320维标准差向量\n        self.failure_counts = {s: 0 for s in range(1, 6)}\n        # 预计算列索引\n        self.col_indices = {}\n        for sensor in range(1, 6):\n            start = (sensor-1)*64\n            end = sensor*64\n            self.col_indices[sensor] = (start, end)\n    \n    def fit(self, X_tof):\n        # 一次性提取所有TOF数据\n        tof_data = X_tof.values if isinstance(X_tof, pd.DataFrame) else X_tof\n        \n        # 重塑为(samples, sensor, values)\n        if tof_data.shape[1] != 320:\n            raise ValueError(f\"TOF数据应有320列，实际有{tof_data.shape[1]}列\")\n        \n        tof_data = tof_data.reshape(-1, 5, 64)\n        \n        for sensor in range(5):\n            sensor_idx = sensor + 1\n            sensor_data = tof_data[:, sensor, :]\n            # 故障检测\n            failure_mask = (sensor_data == 0)\n            self.failure_counts[sensor_idx] = failure_mask.sum()\n            # 有效值掩码\n            valid_mask = (sensor_data != -1) & (sensor_data != 0)\n            valid_data = sensor_data[valid_mask]\n            # 计算均值和标准差\n            if len(valid_data) > 0:\n                mean_val = valid_data.mean()\n                std_val = valid_data.std()\n            else:\n                mean_val = 0\n                std_val = 1\n            # 更新全量向量\n            start, end = self.col_indices[sensor_idx]\n            self.means_full[start:end] = mean_val\n            self.stds_full[start:end] = std_val if std_val > 1e-7 else 1.0\n        return self\n\n    def transform(self, X_tof):\n        # Get data\n        if isinstance(X_tof, pd.DataFrame):\n            tof_cols = X_tof.columns\n            tof_values = X_tof.values.copy()\n        else:\n            tof_values = X_tof.copy()\n        # Create masks for the entire data at once\n        valid_mask = (tof_values != -1) & (tof_values != 0)\n        no_signal_mask = (tof_values == -1)\n        failure_mask = (tof_values == 0)\n        # Normalize all valid values in one operation\n        normalized = np.zeros_like(tof_values)\n        normalized[valid_mask] = (tof_values[valid_mask] - self.means_full[np.where(valid_mask)[1]]) / self.stds_full[np.where(valid_mask)[1]]\n        # Set special values\n        normalized[no_signal_mask] = 10     # -1 → 10\n        normalized[failure_mask]   = -10    # 0 → -10\n        # Return result\n        if isinstance(X_tof, pd.DataFrame):\n            return pd.DataFrame(normalized, columns=tof_cols, index=X_tof.index)\n        return normalized\n\n\n\nclass GestureDataset(Dataset):\n    def __init__(self, sequence_dir, label_dir, metadata_df, max_len=None, \n                 is_train=False, scale_path=None, use_augmentation=False,\n                 drift_std=0.01, drift_max=0.05):\n        \"\"\"\n        参数:\n            sequence_dir: 序列数据根目录\n            label_dir: 标签数据根目录\n            metadata_df: 包含序列元数据的DataFrame\n            max_len: 序列最大长度\n            train_scalers: 训练集的归一化统计量（现在是非TOF和TOF归一化器的元组）\n        \"\"\"\n        self.sequence_dir = sequence_dir\n        self.label_dir = label_dir\n        self.metadata = metadata_df\n        self.sequence_ids = self.metadata['sequence_id'].tolist()\n        self.max_len = max_len\n        self.is_train = is_train\n        self.scale_path = scale_path\n        self.use_augmentation = use_augmentation and is_train # 增强只在训练时使用\n        self.drift_std = drift_std\n        self.drift_max = drift_max\n\n        # 传感器组定义\n        self.thm_tof_dim = 325  # 温度(5) + TOF(320)\n        self.tof_dim = 320  # TOF特征\n        self.demo_dim = 7    # 人口统计特征\n        \n        # 手势编码器\n        self.gesture_encoder = LabelEncoder()\n        self.gesture_encoder.fit(self.metadata['gesture'])\n        \n        # 归一化统计量\n        self.non_tof_scaler = StandardScaler()\n        self.tof_scaler = ToFScaler()\n        \n        # 加载序列数据\n        self.sequence_data = self._load_sequences()\n        \n        # 如果是训练集且需要计算归一化统计量\n        if is_train and scale_path is not None:\n            self._fit_scalers()\n            self._save_scalers(scale_path)\n            # 归一化并缓存所有序列\n            self._normalize_and_cache_sequences()\n        elif not is_train and scale_path is not None:\n            # 如果是验证集或测试集，加载预先计算的归一化统计量\n            if os.path.exists(os.path.join(scale_path, 'train_scalers.pkl')):\n                with open(os.path.join(scale_path, 'train_scalers.pkl'), 'rb') as f:\n                    self.non_tof_scaler, self.tof_scaler = pickle.load(f)\n                # 归一化并缓存所有序列\n                self._normalize_and_cache_sequences()\n            else:\n                warnings.warn(\"未找到训练集归一化统计量，载入归一化器失败\")\n        else:\n            warnings.warn(\"未提供归一化统计量路径，将无法进行归一化处理\")\n            # 即使没有归一化器，也缓存当前序列（未归一化）\n            self.cached_sequences = self.sequence_data.copy()\n\n        # 如果没有提供最大长度，则使用最长序列长度\n        print(f\"数据集初始化完成，包含 {len(self)} 个序列\")\n        print(f\"序列长度统计: 平均={np.mean([len(s) for s in self.cached_sequences]):.1f}, \" \n              f\"最大={np.max([len(s) for s in self.cached_sequences])}\")\n        \n\n    def _normalize_and_cache_sequences(self):\n        \"\"\"归一化所有序列并缓存结果\"\"\"\n        self.cached_sequences = []\n        for seq in tqdm(self.sequence_data, desc=\"归一化序列数据\", leave=False):\n            # 分离非TOF和TOF特征\n            non_tof_part = seq[:, :-self.tof_dim]\n            tof_part = seq[:, -self.tof_dim:]\n            # 分别归一化\n            norm_non_tof = self.non_tof_scaler.transform(non_tof_part)\n            norm_tof = self.tof_scaler.transform(tof_part)\n            # 拼接归一化后的特征\n            normalized_seq = np.concatenate([norm_non_tof, norm_tof], axis=1)\n            self.cached_sequences.append(normalized_seq)\n        # 释放原始数据内存\n        self.sequence_data = None\n\n\n    def _load_sequences(self):\n        \"\"\"加载所有序列数据\"\"\"\n        sequence_data = []\n        for seq_id in tqdm(self.sequence_ids, desc=\"加载序列数据\", leave=False):\n            subject_id = self.metadata[self.metadata['sequence_id'] == seq_id]['subject'].iloc[0]\n            seq_path = os.path.join(self.sequence_dir, str(subject_id), f\"{seq_id}.npy\")\n            if os.path.exists(seq_path):\n                seq = np.load(seq_path)  # 加载原始序列数据\n                seq = self._feature_engineering(seq)\n                sequence_data.append(seq)\n        return sequence_data\n\n\n    def _remove_gravity_from_acc(self, acc_values, quat_values):\n        num_samples = acc_values.shape[0]\n        linear_accel = np.zeros_like(acc_values)\n        gravity_world = np.array([0, 0, 9.81])\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            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        return linear_accel\n    def _calculate_angular_velocity_from_quat(self, quat_values, time_delta=1/10):\n        num_samples = quat_values.shape[0]\n        angular_vel = np.zeros((num_samples, 3))\n        for i in range(num_samples - 1):\n            q_t = quat_values[i]\n            q_t_plus_dt = quat_values[i+1]\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            try:\n                rot_t = R.from_quat(q_t)\n                rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n                delta_rot = rot_t.inv() * rot_t_plus_dt\n                angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n            except ValueError:\n                pass\n        return angular_vel\n    def _calculate_angular_distance(self, quat_values):\n        num_samples = quat_values.shape[0]\n        angular_dist = np.zeros(num_samples)\n        for i in range(num_samples - 1):\n            q1 = quat_values[i]\n            q2 = quat_values[i+1]\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\n                continue\n            try:\n                r1 = R.from_quat(q1)\n                r2 = R.from_quat(q2)\n                relative_rotation = r1.inv() * r2\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        return angular_dist\n\n\n\n    def _feature_engineering(self, seq):\n        imu = seq[:,:7]\n        other_features = seq[:, 7:]  # 除IMU外的其他特征\n        acc = imu[:,0:3]                                                                       # x, y, z\n        rot = imu[:,3:7]                                                                       # w, x, y, z\n        acc_mag = np.sqrt(acc[:,0]**2 + acc[:,1]**2 + acc[:,2]**2)                             # 1,计算加速度模长\n        rot_angle = 2 * np.arccos(rot[:,0].clip(-1, 1))                                        # 1,计算四元数旋转角度（弧度）\n        acc_mag_jerk = np.diff(acc_mag, prepend=acc_mag[0])                                    # 1,计算加速度模长的jerk（差分，首位补0）\n        rot_angle_vel = np.diff(rot_angle, prepend=rot_angle[0])                               # 1,计算旋转角度的速度（差分，首位补0）\n        linear_acc = self._remove_gravity_from_acc(acc, rot[:, [1, 2, 3, 0]])                  # 3,去除重力后的线性加速度（输入四元数格式为[x, y, z, w]）\n        linear_acc_mag = np.sqrt(linear_acc[:,0]**2 + linear_acc[:,1]**2 + linear_acc[:,2]**2) # 1,线性加速度模长\n        linear_acc_mag_jerk = np.diff(linear_acc_mag, prepend=linear_acc_mag[0])               # 1,线性加速度模长的jerk（差分，首位补0）\n        angular_vel = self._calculate_angular_velocity_from_quat(rot[:, [1, 2, 3, 0]])         # 3,计算角速度（输入四元数格式为[x, y, z, w]）\n        angular_distance = self._calculate_angular_distance(rot[:, [1, 2, 3, 0]])              # 1,计算相邻帧的角距离（弧度）\n        # 拼接所有特征\n        new_imu = np.concatenate([\n            acc,                          # 3\n            rot,                          # 4\n            acc_mag[:, None],             # 1\n            rot_angle[:, None],           # 1\n            acc_mag_jerk[:, None],        # 1\n            rot_angle_vel[:, None],       # 1\n            linear_acc,                   # 3\n            linear_acc_mag[:, None],      # 1\n            linear_acc_mag_jerk[:, None], # 1\n            angular_vel,                  # 3\n            angular_distance[:, None]     # 1\n        ], axis=1)\n        thm = other_features[:, :5]  # 温度特征\n        tof = other_features[:, 5:5+320]  # TOF特征\n        demo = other_features[:, 5+320:]  # 人口统计特征\n        # 拼接所有特征: IMU(20) + 温度(5) + 人口统计(7) + TOF(320)\n        features = np.concatenate([new_imu, thm,demo,tof], axis=1)\n        return features\n\n    def _fit_scalers(self):\n        \"\"\"在整个训练集上计算归一化统计量 (优化版)\"\"\"\n        all_sequences = np.vstack(self.sequence_data)\n        # 分离非TOF和TOF特征\n        non_tof_features = all_sequences[:, :-self.tof_dim]\n        tof_features = all_sequences[:, -self.tof_dim:]\n        # 计算归一化统计量\n        self.non_tof_scaler.fit(non_tof_features)\n        self.tof_scaler.fit(tof_features)  # 直接传入数组\n        print(\"训练集归一化统计量计算完成\")\n\n    def _save_scalers(self, scale_path):\n        \"\"\"保存归一化器到文件\"\"\"\n        Path(scale_path).mkdir(parents=True, exist_ok=True)\n        with open(os.path.join(scale_path, 'train_scalers.pkl'), 'wb') as f:\n            # 保存两个归一化器组成的元组\n            pickle.dump((self.non_tof_scaler, self.tof_scaler), f)\n        print(f\"保存训练集归一化统计量到 {scale_path}/train_scalers.pkl\")\n\n    def normalize_features(self, sequence):\n        \"\"\"归一化特征\"\"\"\n        # 分离非TOF和TOF特征\n        non_tof_part = sequence[:, :-self.tof_dim]\n        tof_part = sequence[:, -self.tof_dim:]  # TOF(320)\n        # 分别归一化 (TOF部分直接使用数组)\n        norm_non_tof = self.non_tof_scaler.transform(non_tof_part)\n        norm_tof = self.tof_scaler.transform(tof_part)  # 返回数组\n        # 拼接归一化后的特征\n        return np.concatenate([norm_non_tof, norm_tof], axis=1)\n\n\n    # --- 数据增强方法 (作用于归一化数据) ---\n    def _jitter(self, sequence, sigma=0.1):\n        return sequence + np.random.normal(loc=0., scale=sigma, size=sequence.shape)\n    def _time_mask(self, sequence, max_mask_size=25):\n        seq_len = sequence.shape[0]\n        mask_size = np.random.randint(1, max_mask_size)\n        start = np.random.randint(0, max(1, seq_len - mask_size))\n        sequence[start : start + mask_size] = 0\n        return sequence\n    def _feature_mask(self, sequence, max_mask_size=10):\n        num_features = sequence.shape[1]\n        mask_size = np.random.randint(1, max_mask_size)\n        masked_features = np.random.choice(num_features, mask_size, replace=False)\n        sequence[:, masked_features] = 0\n        return sequence\n    def _motion_drift(self, imu_features: np.ndarray) -> np.ndarray:\n        \"\"\"在IMU特征上模拟传感器漂移\"\"\"\n        T = imu_features.shape[0]\n        # 生成漂移信号\n        drift = np.cumsum(np.random.normal(scale=self.drift_std, size=(T, 1)),axis=0)\n        drift = np.clip(drift, -self.drift_max, self.drift_max)   \n        # 将漂移应用到加速度和角速度相关的特征上\n        # acc (cols 0-2), linear_acc (cols 10-12), angular_vel (cols 15-17)\n        imu_features[:, 0:3] += drift\n        imu_features[:, 10:13] += drift\n        imu_features[:, 15:18] += drift\n        return imu_features\n    def _apply_augmentations(self, sequence):\n        \"\"\"在归一化后的序列上应用增强\"\"\"\n        # 增强1: Jitter\n        if np.random.rand() < 0.7:\n            sequence = self._jitter(sequence, sigma=0.05)\n        # 增强2 & 3: Time and Feature Masking\n        if np.random.rand() < 0.5:\n            sequence = self._time_mask(sequence, max_mask_size=20)\n        if np.random.rand() < 0.5:\n            sequence = self._feature_mask(sequence, max_mask_size=15)\n        # 增强4: Motion Drift (只作用于IMU部分)\n        if np.random.rand() < 0.5:\n            imu_features = sequence[:, :20]\n            other_features = sequence[:, 20:]\n            augmented_imu = self._motion_drift(imu_features)\n            sequence = np.concatenate([augmented_imu, other_features], axis=1)\n        return sequence\n\n\n    def __len__(self):\n        return len(self.sequence_ids)\n\n    def __getitem__(self, idx):\n        # 直接使用缓存的归一化序列\n        sequence = self.cached_sequences[idx].copy()\n        if self.use_augmentation:\n            sequence = self._apply_augmentations(sequence)\n        seq_id = self.sequence_ids[idx]\n        subject_id = self.metadata[self.metadata['sequence_id'] == seq_id]['subject'].iloc[0]\n        # 加载阶段标签\n        label_path = os.path.join(self.label_dir, str(subject_id), f\"{seq_id}.npy\")\n        phase_labels = np.load(label_path).astype(np.float32)\n        # 截断序列\n        if self.max_len and len(sequence) > self.max_len:\n            sequence = sequence[-self.max_len:]\n            phase_labels = phase_labels[-self.max_len:]\n        # 分割特征\n        # 归一化后特征顺序: \n        #   [0:20] - IMU特征\n        #   [20:25] - 温度特征\n        #   [25:32] - 人口统计特征 (只在第一帧使用)\n        #   [32:352] - TOF特征\n        tof_features = sequence[:, -self.tof_dim:]  # TOF特征\n        imu_thm_demo_features = sequence[:, :-self.tof_dim]\n        demo_features = imu_thm_demo_features[0, -7:]  # 人口统计数据在整个序列中相同\n        thm_features = imu_thm_demo_features[:, :-7][:, -5:]  # 温度特征\n        imu_features = imu_thm_demo_features[:, :-7][:, :-5] # IMU特征\n        thm_tof_features = np.concatenate([thm_features, tof_features], axis=1)\n        # 获取元数据\n        meta = self.metadata[self.metadata['sequence_id'] == seq_id].iloc[0]\n        gesture_label = self.gesture_encoder.transform([meta['gesture']])[0]\n        # 转换为张量\n        imu_features = torch.tensor(imu_features, dtype=torch.float32)\n        thm_tof_features = torch.tensor(thm_tof_features, dtype=torch.float32)\n        demo_features = torch.tensor(demo_features, dtype=torch.float32)\n        phase_labels = torch.tensor(phase_labels, dtype=torch.long)\n        \n        return {\n            'imu': imu_features,\n            'thm_tof': thm_tof_features,\n            'demo': demo_features,\n            'phase_labels': phase_labels,\n            'length': len(imu_features),\n            'sequence_id': seq_id,\n            'gesture_label': gesture_label\n        }\n\n\ndef collate_fn(batch):\n    \"\"\"自定义批处理函数\"\"\"\n    # 按序列长度排序\n    batch.sort(key=lambda x: x['length'], reverse=True)\n    # 提取不同特征\n    imu_features = [item['imu'] for item in batch]\n    thm_tof_features = [item['thm_tof'] for item in batch]\n    demo_features = torch.stack([item['demo'] for item in batch])\n    phase_labels = [item['phase_labels'] for item in batch]\n    lengths = torch.tensor([item['length'] for item in batch], dtype=torch.long)\n    sequence_ids = [item['sequence_id'] for item in batch]\n    gesture_labels = torch.tensor([item['gesture_label'] for item in batch], dtype=torch.long)\n    # 填充序列\n    imu_padded = pad_sequence(imu_features, batch_first=True, padding_value=0)\n    thm_tof_padded = pad_sequence(thm_tof_features, batch_first=True, padding_value=0)\n    phase_padded = pad_sequence(phase_labels, batch_first=True, padding_value=3)\n    # 创建掩码\n    max_len = imu_padded.size(1)\n    mask = torch.arange(max_len)[None, :] < lengths[:, None]\n    mask = mask.float()\n    \n    return {\n        'imu': imu_padded,\n        'thm_tof': thm_tof_padded,\n        'demo': demo_features,\n        'phase_labels': phase_padded,\n        'mask': mask,\n        'lengths': lengths,\n        'gesture_labels': gesture_labels,\n        'sequence_ids': sequence_ids\n    }","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:47.311354Z","iopub.execute_input":"2025-07-14T12:14:47.311606Z","iopub.status.idle":"2025-07-14T12:14:59.277906Z","shell.execute_reply.started":"2025-07-14T12:14:47.311587Z","shell.execute_reply":"2025-07-14T12:14:59.277173Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def set_seed(seed: int = 42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.279337Z","iopub.execute_input":"2025-07-14T12:14:59.279781Z","iopub.status.idle":"2025-07-14T12:14:59.284444Z","shell.execute_reply.started":"2025-07-14T12:14:59.279755Z","shell.execute_reply":"2025-07-14T12:14:59.283796Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom sklearn.metrics import f1_score\nclass ParticipantVisibleError(Exception):\n    \"\"\"Errors raised here will be shown directly to the competitor.\"\"\"\n    pass\nclass CompetitionMetric:\n    \"\"\"Hierarchical macro F1 for the CMI 2025 challenge.\"\"\"\n    def __init__(self):\n        self.target_gestures = [\n            'Above ear - pull hair',\n            'Cheek - pinch skin',\n            'Eyebrow - pull hair',\n            'Eyelash - pull hair',\n            'Forehead - pull hairline',\n            'Forehead - scratch',\n            'Neck - pinch skin',\n            'Neck - scratch',\n        ]\n        self.non_target_gestures = [\n            'Write name on leg',\n            'Wave hello',\n            'Glasses on/off',\n            'Text on phone',\n            'Write name in air',\n            'Feel around in tray and pull out an object',\n            'Scratch knee/leg skin',\n            'Pull air toward your face',\n            'Drink from bottle/cup',\n            'Pinch knee/leg skin'\n        ]\n        self.all_classes = self.target_gestures + self.non_target_gestures\n    def calculate_hierarchical_f1(\n        self,\n        sol: pd.DataFrame,\n        sub: pd.DataFrame\n    ) -> float:\n        invalid_types = {i for i in sub['gesture'].unique() if i not in self.all_classes}\n        if invalid_types:\n            raise ParticipantVisibleError(\n                f\"Invalid gesture values in submission: {invalid_types}\"\n            )\n        y_true_bin = sol['gesture'].isin(self.target_gestures).values\n        y_pred_bin = sub['gesture'].isin(self.target_gestures).values\n        f1_binary = f1_score(\n            y_true_bin,\n            y_pred_bin,\n            pos_label=True,\n            zero_division=0,\n            average='binary'\n        )\n        y_true_mc = sol['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        f1_macro = f1_score(\n            y_true_mc,\n            y_pred_mc,\n            average='macro',\n            zero_division=0\n        )\n        return 0.5 * f1_binary + 0.5 * f1_macro, f1_binary, f1_macro\ndef score(\n    solution: pd.DataFrame,\n    submission: pd.DataFrame,\n    row_id_column_name: str\n) -> float:\n    for col in (row_id_column_name, 'gesture'):\n        if col not in solution.columns:\n            raise ParticipantVisibleError(f\"Solution file missing required column: '{col}'\")\n        if col not in submission.columns:\n            raise ParticipantVisibleError(f\"Submission file missing required column: '{col}'\")\n    metric = CompetitionMetric()\n    return metric.calculate_hierarchical_f1(solution, submission)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.285175Z","iopub.execute_input":"2025-07-14T12:14:59.285397Z","iopub.status.idle":"2025-07-14T12:14:59.313194Z","shell.execute_reply.started":"2025-07-14T12:14:59.285376Z","shell.execute_reply":"2025-07-14T12:14:59.312467Z"}},"outputs":[],"execution_count":null},{"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    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\nclass CoordAttention(nn.Module):\n    \"\"\"\n    Coordinate Attention for Sequences.\n    Input Dimension: (B, T, C)\n    Output Dimension: (B, T, C)\n    \"\"\"\n    def __init__(self, channels, reduction=8):\n        super(CoordAttention, self).__init__()\n        self.mid_channels = max(8, channels // reduction)\n\n        self.compression = nn.Sequential(\n            nn.Conv1d(channels, self.mid_channels, kernel_size=1, bias=False),\n            nn.BatchNorm1d(self.mid_channels),\n            nn.SiLU(inplace=True)\n        )\n        # Attention branches\n        self.time_conv = nn.Conv1d(1, 1, kernel_size=5, padding=2, bias=False)  \n        self.channel_conv = nn.Conv1d(self.mid_channels, channels, kernel_size=1, bias=False)\n        self.sigmoid = nn.Sigmoid()\n    def forward(self, x):\n        x = x.permute(0, 2, 1)\n        # x: (B, T, C)\n        x_p = x.permute(0, 2, 1)  # (B, C, T)\n        f   = self.compression(x_p)  # (B, rC, T)\n        ## Time Attention (B, 1, T)\n        f_t = f.mean(dim=1, keepdim=True)      \n        time_attn = self.sigmoid(self.time_conv(f_t))  \n        ## Channel Attention (B, C, 1)\n        f_c = f.mean(dim=2, keepdim=True)      \n        channel_attn = self.sigmoid(self.channel_conv(f_c)) \n        ## (B, T, C)\n        out = (x_p * time_attn * channel_attn).permute(0,2,1)\n        return out.permute(0, 2, 1)\n\n\nclass ResidualCNNBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size, Model, reduction=8, pool_size=2, dropout=0.3, weight_decay=1e-4):\n        super().__init__()\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        # 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        # attention block\n        self.attention = Model(out_channels, reduction)\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        self.pool = nn.MaxPool1d(pool_size)\n        self.dropout = nn.Dropout(dropout)\n        \n    def forward(self, x):\n        shortcut = self.shortcut(x)\n        # First conv\n        out = F.relu(self.bn1(self.conv1(x)))\n        # Second conv\n        out = self.bn2(self.conv2(out))\n        # attention block\n        out = self.attention(out)\n        # Add shortcut\n        out += shortcut\n        out = F.relu(out)\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    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 MLPAttention(nn.Module):\n    def __init__(self, feature_dim):\n        super(MLPAttention, self).__init__()\n        self.attn = nn.Sequential(\n            nn.Linear(feature_dim, feature_dim//8),\n            nn.SiLU(inplace=True),\n            nn.Linear(feature_dim//8, 1)\n        )\n    def forward(self, x):\n        # inputs shape: (B, T, C)\n        weights = self.attn(x)  # (B, T, 1)\n        weights = F.softmax(weights, dim=1)  # (B, T, 1)\n        context = (x * weights).sum(dim=1)  # (B, C)\n        return context\n\n\nclass IMUOnlyModel(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        # IMU deep branch\n        self.imu_block1 = ResidualCNNBlock(imu_dim, 64, 3, dropout=0.3, Model=CoordAttention,  weight_decay=weight_decay)\n        self.imu_block2 = ResidualCNNBlock(64, 128, 5, dropout=0.3, Model=CoordAttention, weight_decay=weight_decay)\n        # BiGRU\n        self.bigru = nn.GRU(128, 128, bidirectional=True, batch_first=True)\n        self.gru_dropout = nn.Dropout(0.4)\n        # Attention\n        self.attention = AttentionLayer(256)  # 128*2 for bidirectional\n        self.mlp_attention = MLPAttention(256)  # MLP attention for final context aggregation\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        self.dense2 = nn.Linear(256, 128, bias=False)\n        self.bn_dense2 = nn.BatchNorm1d(128)\n        self.drop2 = nn.Dropout(0.3)\n        self.classifier = nn.Linear(128, n_classes)\n        \n    def forward(self, imu, thm_tof, demo):\n        imu = imu.transpose(1, 2)  # (batch, imu_dim, seq_len)\n        # IMU branch\n        x1 = self.imu_block1(imu)\n        x1 = self.imu_block2(x1)\n        merged = x1.transpose(1, 2)  # (batch, seq_len, 128)\n        # BiGRU \n        gru_out, _ = self.bigru(merged)\n        gru_out = self.gru_dropout(gru_out)\n        # Attention\n        attended = self.mlp_attention(gru_out)\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        # Classification\n        logits = self.classifier(x)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.313934Z","iopub.execute_input":"2025-07-14T12:14:59.314196Z","iopub.status.idle":"2025-07-14T12:14:59.334839Z","shell.execute_reply.started":"2025-07-14T12:14:59.314179Z","shell.execute_reply":"2025-07-14T12:14:59.334177Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def apply_label_smoothing(targets, num_classes, smoothing_factor=0.1):\n    \"\"\"\n    Apply label smoothing to convert hard targets to soft targets.\n    Args:\n        targets (torch.Tensor): Hard label indices\n        num_classes (int): Total number of classes\n        smoothing_factor (float): Smoothing factor, typically a small value like 0.1 \n    Returns:\n        torch.Tensor: Smoothed label distribution (batch_size, num_classes)\n    \"\"\"\n    # Create a tensor of zeros with shape [batch_size, num_classes]\n    smoothed_labels = torch.zeros(targets.size(0), num_classes, device=targets.device)\n    # Fill in the tensor with the smoothing value\n    smoothed_labels.fill_(smoothing_factor / (num_classes - 1))\n    # Set the correct class with the main probability mass\n    smoothed_labels.scatter_(1, targets.unsqueeze(1), 1.0 - smoothing_factor)\n    return smoothed_labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.336609Z","iopub.execute_input":"2025-07-14T12:14:59.336801Z","iopub.status.idle":"2025-07-14T12:14:59.354041Z","shell.execute_reply.started":"2025-07-14T12:14:59.336786Z","shell.execute_reply":"2025-07-14T12:14:59.353300Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch(model, dataloader, optimizer, criterion, device, epoch, num_classes,\n                   warmup_epochs=5, total_epochs=100, label_smoothing=0.1, eta_min=1e-6):\n    \"\"\"\n    训练一个 epoch，支持标签平滑和两阶段学习率调度（线性warmup + 余弦退火）\n    :param warmup_epochs: warmup阶段的总epoch数\n    :param total_epochs: 整个训练过程的总epoch数\n    :param label_smoothing: 标签平滑系数，设为 0 则不使用标签平滑\n    :param eta_min: 余弦退火的最小学习率\n    \"\"\"    \n    model.train()\n    running_loss = 0.0\n    train_preds = []\n    train_targets = []\n    \n    # 根据是否使用标签平滑选择不同的损失函数处理\n    use_label_smoothing = label_smoothing > 0\n    \n    # 获取基础学习率\n    base_lr = optimizer.defaults['lr']  # 基础学习率\n    \n    for batch_idx, batch in enumerate(tqdm(dataloader, desc=f\"Epoch {epoch+1} [Train]\", leave=False)):\n        # ======== 学习率调整 (batch 级别) ========\n        # 计算当前训练进度（以epoch为单位，包含小数部分）\n        current_iter = epoch * len(dataloader) + batch_idx\n        current_epoch = current_iter / len(dataloader)  # 转换为小数形式的epoch\n        # 阶段1: 线性warmup\n        if current_epoch < warmup_epochs:\n            # 线性warmup：从0.001 * base_lr 增加到 base_lr\n            lr = base_lr * (0.001 + (current_epoch / warmup_epochs) * 0.999)\n        # 阶段2: 余弦退火\n        else:\n            # 余弦退火公式\n            cos_factor = 0.5 * (1 + math.cos(math.pi * (current_epoch - warmup_epochs) / (total_epochs - warmup_epochs)))\n            lr = eta_min + (base_lr - eta_min) * cos_factor\n        # 应用计算得到的学习率\n        for param_group in optimizer.param_groups:\n            param_group['lr'] = lr\n        \n        # ======== 训练步骤 ========\n        imu = batch['imu'].to(device)\n        thm_tof = batch['thm_tof'].to(device)\n        demo = batch['demo'].to(device)\n        labels = batch['gesture_labels'].to(device)\n        phase_labels = batch['phase_labels'].to(device)  # 阶段标签\n        \n        optimizer.zero_grad(set_to_none=True)  # 使用set_to_none=True更高效\n        outputs = model(imu, thm_tof, demo)\n        # ======== 应用标签平滑 ========\n        if use_label_smoothing:\n            # 将硬标签转换为软标签\n            soft_labels = apply_label_smoothing(labels, num_classes, label_smoothing)\n            # 使用交叉熵损失函数 (对于软标签，通常直接使用 log_softmax + sum)\n            loss = torch.nn.functional.kl_div(\n                torch.nn.functional.log_softmax(outputs, dim=1),\n                soft_labels,\n                reduction='batchmean'\n            )\n        else:\n            # 使用原始损失函数\n            loss = criterion(outputs, labels)\n        loss.backward()\n        # 添加梯度裁剪防止梯度爆炸\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n        # ======== 收集训练指标 ========\n        # 显式分离张量，减少内存占用\n        preds = outputs.argmax(dim=1).detach().cpu().numpy()\n        targets = labels.cpu().numpy()\n        train_preds.extend(preds)\n        train_targets.extend(targets)\n        running_loss += loss.item()\n    \n    # ======== 计算训练指标 ========\n    avg_loss = running_loss / len(dataloader)\n    f1, f1_binary, f1_macro = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': dataloader.dataset.gesture_encoder.inverse_transform(train_targets)}),\n        pd.DataFrame({'gesture': dataloader.dataset.gesture_encoder.inverse_transform(train_preds)}))\n    \n    return avg_loss, f1, f1_binary, f1_macro","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.354855Z","iopub.execute_input":"2025-07-14T12:14:59.355287Z","iopub.status.idle":"2025-07-14T12:14:59.374008Z","shell.execute_reply.started":"2025-07-14T12:14:59.355261Z","shell.execute_reply":"2025-07-14T12:14:59.373362Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate(model, dataloader, criterion, device):\n    model.eval()\n    running_loss = 0.0\n    val_preds = []\n    val_targets = []\n    \n    with torch.no_grad():\n        for batch in tqdm(dataloader, desc=\"[Val]\", leave=False):\n            imu = batch['imu'].to(device)\n            thm_tof = batch['thm_tof'].to(device)\n            demo = batch['demo'].to(device)\n            labels = batch['gesture_labels'].to(device)\n            \n            outputs = model(imu, thm_tof, demo)\n            loss = criterion(outputs, labels)\n            \n            # 显式分离张量\n            preds = outputs.argmax(dim=1).cpu().numpy()\n            targets = labels.cpu().numpy()\n            val_preds.extend(preds)\n            val_targets.extend(targets)\n            running_loss += loss.item()\n\n    \n    avg_loss = running_loss / len(dataloader)\n    f1, f1_binary, f1_macro = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': dataloader.dataset.gesture_encoder.inverse_transform(val_targets)}),\n        pd.DataFrame({'gesture': dataloader.dataset.gesture_encoder.inverse_transform(val_preds)}))\n    \n    return avg_loss, f1, f1_binary, f1_macro","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.374789Z","iopub.execute_input":"2025-07-14T12:14:59.375053Z","iopub.status.idle":"2025-07-14T12:14:59.393319Z","shell.execute_reply.started":"2025-07-14T12:14:59.375030Z","shell.execute_reply":"2025-07-14T12:14:59.392663Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN = False\n\nif TRAIN == True:\n    # 日志设置\n    log_dir = \"logs\"\n    os.makedirs(log_dir, exist_ok=True)\n    log_path = os.path.join(log_dir, \"imutrain.log\")\n    logging.basicConfig(\n        filename=log_path,\n        level=logging.INFO,\n        format=\"%(asctime)s [%(levelname)s] %(message)s\",\n        datefmt=\"%Y-%m-%d %H:%M:%S\",\n    )\n    \n    \n    metadata = pd.read_csv('/kaggle/input/cmi-data/data/train_labels.csv')\n    NUM_FOLDS = 5\n    SEED = 2\n    scale_path='CMI_data/data/scalers'\n    BATCH_SIZE = 32\n    sequence_dir='/kaggle/input/cmi-data/data/train_sequences'\n    label_dir='/kaggle/input/cmi-data/data/phase_labels'\n    max_len = 256\n    LR = 1e-3\n    LR_min = 1e-6\n    EPOCHS = 100\n    patience = 10\n    early_stopping_patience = 25  # 新增：早停策略的轮数\n    model_path = \"IMUMODEL\"\n    num_workers = 4\n    warmup_epochs = 3  # 新增：warmup阶段的轮数\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    \n    \n    print(f\"▶ imports ready · pytorch {torch.__version__} · device: {device}\")\n    set_seed(SEED)  # 设置随机种子\n    # 记录训练种子到日志\n    logging.info(f\"Training with seed: {SEED}\")\n    print(f\"Training with seed: {SEED}\")\n    sgkf = StratifiedGroupKFold(n_splits=NUM_FOLDS, shuffle=True, random_state=SEED)\n    Path(model_path).mkdir(parents=True, exist_ok=True)\n    labels = metadata['gesture'].values\n    sequences = metadata['sequence_id'].tolist()\n    subjects = metadata['subject'].tolist()\n    splits = []\n    \n    all_fold_f1_scores = []  # 存储每个fold的最佳验证F1分数\n    \n    for fold, (train_idx, val_idx) in enumerate(sgkf.split(sequences, labels, subjects)):\n        print(f\"\\n=== Starting Fold {fold+1}/{NUM_FOLDS} ===\")\n        logging.info(f\"=== Starting Fold {fold+1}/{NUM_FOLDS} ===\")\n    \n    \n        # 获取当前fold的训练集和验证集元数据\n        train_metadata = metadata.iloc[train_idx].reset_index(drop=True)\n        val_metadata = metadata.iloc[val_idx].reset_index(drop=True)\n    \n        # 创建数据集\n        train_dataset = GestureDataset(sequence_dir=sequence_dir, label_dir=label_dir,\n            metadata_df=train_metadata,max_len=max_len,is_train=True, scale_path=scale_path,use_augmentation=True\n            )    \n        val_dataset = GestureDataset(sequence_dir=sequence_dir, label_dir=label_dir,\n            metadata_df=val_metadata,max_len=max_len,is_train=False, scale_path=scale_path\n            )\n        # 创建数据加载器\n        train_loader = DataLoader(train_dataset,batch_size=BATCH_SIZE,shuffle=True,\n            collate_fn=collate_fn,num_workers=num_workers,pin_memory=True, drop_last=True)\n        val_loader = DataLoader(val_dataset,batch_size=BATCH_SIZE,shuffle=False,\n            collate_fn=collate_fn,num_workers=num_workers, pin_memory=True, drop_last=False)\n        \n        model = IMUOnlyModel(20, 325, n_classes=len(train_dataset.gesture_encoder.classes_))\n        optimizer = Adam(model.parameters(), lr=LR, weight_decay=1e-4)\n        criterion = nn.CrossEntropyLoss()\n        \n        model.to(device)\n        best_val_f1 = 0.0\n        epochs_without_improvement = 0  # 用于跟踪没有改善的轮数\n        # 计算当前 fold 的总 warmup 步数\n        total_warmup_steps = warmup_epochs * len(train_loader)\n        global_step = 0  # 全局步数计数器\n    \n        for epoch in range(EPOCHS):\n            train_loss, train_f1, train_f1_binary, train_f1_macro = \\\n                train_one_epoch(model, train_loader, optimizer, criterion, device, epoch,\n                num_classes=len(train_dataset.gesture_encoder.classes_),\n                warmup_epochs=warmup_epochs, total_epochs=EPOCHS,\n                label_smoothing=0.1, eta_min=LR_min)\n            val_loss, val_f1, val_f1_binary, val_f1_macro = \\\n                validate(model, val_loader, criterion, device)\n            logging.info(\n                f\"Fold {fold+1}, Epoch {epoch+1}/{EPOCHS}, LR: {optimizer.param_groups[0]['lr']:.6f}\\n\"\n                f\"Train Loss: {train_loss:.4f}, Train F1: {train_f1:.4f}, Train F1 binary: {train_f1_binary:.4f}, Train F1 macro: {train_f1_macro:.4f}\\n\"\n                f\"Val Loss: {val_loss:.4f}, Val F1: {val_f1:.4f}, Val F1 binary: {val_f1_binary:.4f}, Val F1 macro: {val_f1_macro:.4f}\"\n            )\n            if val_f1 > best_val_f1:\n                best_val_f1 = val_f1\n                epochs_without_improvement = 0  # 重置计数器\n                print(f\"Fold {fold+1}, Epoch {epoch+1}: 新的最佳验证F1分数: {best_val_f1:.4f}\")\n                logging.info(f\"Fold {fold+1}, Epoch {epoch+1}: 新的最佳验证F1分数: {best_val_f1:.4f}\")\n                # 保存最佳模型\n                model_save_path = os.path.join(model_path, f\"model_fold{fold+1}_best.pth\")\n                torch.save(model.state_dict(), model_save_path)\n            else:\n                epochs_without_improvement += 1\n                logging.info(f\"Epochs without improvement: {epochs_without_improvement}\")\n            \n            # 早停策略检查\n            if epochs_without_improvement >= early_stopping_patience:\n                print(f\"Early stopping triggered! No improvement for {early_stopping_patience} epochs.\")\n                logging.info(f\"Early stopping triggered after {epoch+1} epochs (no improvement for {early_stopping_patience} epochs)\")\n                break\n            # 保存模型\n            model_save_path = os.path.join(model_path, f\"model_fold{fold+1}_last.pth\")\n            torch.save(model.state_dict(), model_save_path)\n        # ======== 存储当前fold的最佳分数 ========\n        all_fold_f1_scores.append(best_val_f1)\n        print(f\"Fold {fold+1} 完成! 最佳验证F1: {best_val_f1:.4f}\")\n        logging.info(f\"Fold {fold+1} 完成! 最佳验证F1: {best_val_f1:.4f}\")\n    \n    \n        # 删除所有相关引用\n        del model, optimizer, train_loader, val_loader, train_dataset, val_dataset\n        gc.collect()\n        torch.cuda.empty_cache()  # 清空GPU缓存\n        time.sleep(2)  # 等待资源释放\n    \n    # ======== 所有fold完成后计算平均分数 ========\n    if all_fold_f1_scores:\n        average_f1 = sum(all_fold_f1_scores) / len(all_fold_f1_scores)\n        print(f\"\\n===== 最终结果 =====\")\n        print(f\"平均F1分数: {average_f1:.4f}\")\n        print(\"各Fold分数详情:\")\n        for i, score in enumerate(all_fold_f1_scores):\n            print(f\"  Fold {i+1}: {score:.4f}\")\n        logging.info(f\"\\n===== 交叉验证最终结果 =====\")\n        logging.info(f\"平均F1分数: {average_f1:.4f}\")\n        logging.info(\"各Fold分数详情:\")\n        for i, score in enumerate(all_fold_f1_scores):\n            logging.info(f\"  Fold {i+1}: {score:.4f}\")\n\n\n# 2025-07-09 11:25:00 [INFO] 平均F1分数: 0.7858\n# 2025-07-09 11:25:00 [INFO] 各Fold分数详情:\n# 2025-07-09 11:25:00 [INFO]   Fold 1: 0.7734\n# 2025-07-09 11:25:00 [INFO]   Fold 2: 0.8004\n# 2025-07-09 11:25:00 [INFO]   Fold 3: 0.7856\n# 2025-07-09 11:25:00 [INFO]   Fold 4: 0.7701\n# 2025-07-09 11:25:00 [INFO]   Fold 5: 0.7994","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.394170Z","iopub.execute_input":"2025-07-14T12:14:59.394473Z","iopub.status.idle":"2025-07-14T12:14:59.418572Z","shell.execute_reply.started":"2025-07-14T12:14:59.394457Z","shell.execute_reply":"2025-07-14T12:14:59.417886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport torch\nimport polars as pl\nimport threading\nimport pandas as pd  # 加载csv文件时需要\nimport glob\nimport os\n\n# 全局变量\nDM = None\nIMU_MODELS = []\nTOF_MODELS = []\nGESTURE_CLASSES = None\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nTRAIN_DS = None\nINITIALIZED = False\nINIT_LOCK = threading.Lock()\n\ndef load_models(models_dir, model_class, n_classes, device):\n    \"\"\"从指定文件夹加载所有模型文件，返回模型列表\"\"\"\n    model_paths = sorted(glob.glob(os.path.join(models_dir, \"model_fold*_best.pth\")))\n    if not model_paths:\n        raise FileNotFoundError(f\"No models found in {models_dir}\")\n    models = []\n    for path in model_paths:\n        model = model_class(20, 325, n_classes=n_classes).to(device)\n        model.load_state_dict(torch.load(path, map_location=device, weights_only=False))\n        model.eval()\n        models.append(model)\n    return models\n\n\n\n\ndef init_infer_module(\n    sequence_dir='/kaggle/input/cmi-data/data/train_sequences',\n    label_dir='/kaggle/input/cmi-data/data/phase_labels',\n    metadata_path='/kaggle/input/cmi-data/data/train_labels.csv',\n    scale_path='cmi-data/data/scalers',\n    imu_models_dir='IMUMODEL',\n):\n\n    \n    global DM, IMU_MODELS, TOF_MODELS, GESTURE_CLASSES, TRAIN_DS, INITIALIZED\n\n    if TRAIN == False:\n        imu_models_dir='/kaggle/input/imuonly/pytorch/imu-only-model/1'\n    \n    if INITIALIZED:\n        return\n\n    with INIT_LOCK:\n        if INITIALIZED:\n            return\n        print(\"Initializing inference module...\")\n\n        # 数据集准备\n        TRAIN_DS = GestureDataset(\n            sequence_dir=sequence_dir, \n            label_dir=label_dir,\n            metadata_df=pd.read_csv(metadata_path),\n            max_len=256,\n            is_train=True,\n            scale_path=scale_path\n        )\n        GESTURE_CLASSES = len(TRAIN_DS.gesture_encoder.classes_)\n\n        # 加载IMU模型\n        IMU_MODELS = load_models(\n            imu_models_dir, \n            IMUOnlyModel, \n            n_classes=GESTURE_CLASSES, \n            device=DEVICE\n        )\n\n\n        print(f\"Inference module initialized: {len(IMU_MODELS)} IMU models.\")\n        INITIALIZED = True\n\n# 特征列表\nTIME_SERIES_FEATURES = [\n    'acc_x', 'acc_y', 'acc_z',\n    'rot_w', 'rot_x', 'rot_y', 'rot_z',\n    'thm_1', 'thm_2', 'thm_3', 'thm_4', 'thm_5'\n]\n\nTOF_FEATURES = [f'tof_{sensor}_v{pixel}' for sensor in range(1, 6) for pixel in range(64)]\n\nSTATIC_FEATURES = [\n    'adult_child', 'age', 'sex', 'handedness',\n    'height_cm', 'shoulder_to_wrist_cm', 'elbow_to_wrist_cm'\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.419448Z","iopub.execute_input":"2025-07-14T12:14:59.419885Z","iopub.status.idle":"2025-07-14T12:14:59.502586Z","shell.execute_reply.started":"2025-07-14T12:14:59.419863Z","shell.execute_reply":"2025-07-14T12:14:59.501805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================================\n# Interpolation methods matching the training pipeline (updated)\n# ============================================================================\n\ndef interpolate_thm_column(col):\n    \"\"\"\n    Temperature sensor (thm) interpolation matching training pipeline:\n    1. Apply linear interpolation (both directions)\n    2. Fill remaining NaNs with 0\n    \"\"\"\n    # 创建pandas Series以利用内置插值功能\n    s = pd.Series(col)\n    # 应用线性插值（双向）,剩余NaN填充0\n    s = s.interpolate(method='linear', limit_direction='both').fillna(0)\n    return s.values\n\ndef interpolate_imu_column(col):\n    \"\"\"\n    IMU sensor (acc, rot) interpolation matching training pipeline:\n    1. Forward fill (ffill)\n    2. Backward fill (bfill)\n    3. Fill remaining NaNs with 0\n    \"\"\"\n    # 创建pandas Series以利用内置填充功能\n    s = pd.Series(col)\n    # 前向填充,后向填充,剩余NaN填充0\n    s = s.ffill().bfill().fillna(0)\n    return s.values\n\ndef interpolate_tof_column(col):\n    \"\"\"\n    TOF sensor interpolation matching training pipeline:\n    \"\"\"\n    # 创建pandas Series以利用内置填充功能\n    s = pd.Series(col)\n    # 前向填充,后向填充,剩余NaN填充0\n    s = s.ffill().bfill().fillna(0)\n    return s.values\n\ndef interpolate_sequence_polars(sequence, imu_features, thm_features, tof_features):\n    \"\"\"Apply consistent interpolation to a Polars DataFrame, matching training pipeline\"\"\"\n    # Convert to pandas for processing\n    df_pd = sequence.to_pandas()\n    # 应用IMU传感器插值\n    for feat in imu_features:\n        if feat in df_pd.columns:\n            df_pd[feat] = interpolate_imu_column(df_pd[feat])\n    # 应用温度传感器插值\n    for feat in thm_features:\n        if feat in df_pd.columns:\n            df_pd[feat] = interpolate_thm_column(df_pd[feat])\n    # 应用TOF传感器插值\n    for feat in tof_features:\n        if feat in df_pd.columns:\n            df_pd[feat] = interpolate_tof_column(df_pd[feat])\n    # Convert back to polars\n    return pl.from_pandas(df_pd)\n\n# ============================================================================\n# Modified prediction function with consistent interpolation\n# ============================================================================\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    sequence: Polars DataFrame, single time series data with all dynamic features\n    demographics: Polars DataFrame, single record with demographic features\n    return: predicted gesture label (string)\n    \"\"\"\n    init_infer_module()  # Initialize inference module\n\n    # Define feature groups\n    IMU_FEATURES = ['acc_x', 'acc_y', 'acc_z', 'rot_w', 'rot_x', 'rot_y', 'rot_z']\n    THM_FEATURES = ['thm_1', 'thm_2', 'thm_3', 'thm_4', 'thm_5']\n    TOF_FEATURES = [f'tof_{sensor}_v{pixel}' for sensor in range(1, 6) for pixel in range(64)]\n\n    # 判断原始数据的thm和tof是否全为nan（未插值前）\n    thm_all_nan = sequence.select(THM_FEATURES).to_pandas().isna().all().all()\n    tof_all_nan = sequence.select(TOF_FEATURES).to_pandas().isna().all().all()\n\n    # 1. Apply interpolation consistent with training pipeline\n    sequence = interpolate_sequence_polars(\n        sequence, \n        imu_features=IMU_FEATURES,\n        thm_features=THM_FEATURES,\n        tof_features=TOF_FEATURES\n    )\n\n    # 2. Process static features (fill nulls)\n    demographics = demographics.fill_null(0)\n    sequence = sequence.select(IMU_FEATURES + THM_FEATURES + TOF_FEATURES)\n    demographics = demographics.select(STATIC_FEATURES)\n    # 3. Convert to numpy arrays\n    seq_np = sequence.to_numpy().astype(np.float32)\n    demo_np = demographics.to_numpy().astype(np.float32)[0]\n    # 4. Concatenate static features to each time step\n    full_seq = np.concatenate([seq_np, np.tile(demo_np, (len(seq_np), 1))], axis=1)\n    # 5. Feature engineering and normalization\n    features = TRAIN_DS._feature_engineering(full_seq)\n    norm_features = TRAIN_DS.normalize_features(features)\n    # 5.1 分离特征\n    tof_features = norm_features[:, -TRAIN_DS.tof_dim:]  # TOF特征\n    imu_thm_demo_features = norm_features[:, :-TRAIN_DS.tof_dim]\n    demo_features = imu_thm_demo_features[0, -7:]  # 人口统计数据在整个序列中相同\n    thm_features = imu_thm_demo_features[:, :-7][:, -5:]  # 温度特征\n    imu_features = imu_thm_demo_features[:, :-7][:, :-5] # IMU特征\n    thm_tof_features = np.concatenate([thm_features, tof_features], axis=1)\n    # 6. Convert to tensor\n    imu_tensor = torch.tensor(imu_features, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n    thm_tof_tensor = torch.tensor(thm_tof_features, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n    demo_tensor = torch.tensor(demo_features, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n\n    pad_len = 40\n    imu_tensor = torch.cat([imu_tensor, torch.zeros(imu_tensor.shape[0], pad_len, imu_tensor.shape[2], device=imu_tensor.device)], dim=1)\n    thm_tof_tensor = torch.cat([thm_tof_tensor, torch.zeros(thm_tof_tensor.shape[0], pad_len, thm_tof_tensor.shape[2], device=thm_tof_tensor.device)], dim=1)\n\n    models = IMU_MODELS  # 使用IMU模型\n\n    # 8. Model inference (ensemble prediction)\n    with torch.no_grad():\n        all_preds = []\n        for model in models:\n            outputs = model(imu_tensor, thm_tof_tensor, demo_tensor)\n            probs = F.softmax(outputs, dim=1)\n            all_preds.append(probs)\n        \n        avg_pred = torch.stack(all_preds).mean(dim=0)\n        pred_idx = avg_pred.argmax(dim=1).item()\n        pred_label = TRAIN_DS.gesture_encoder.inverse_transform([pred_idx])[0]\n    \n    print(f\"Predicted gesture: {pred_label}\")\n    return str(pred_label)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.503485Z","iopub.execute_input":"2025-07-14T12:14:59.504023Z","iopub.status.idle":"2025-07-14T12:14:59.527836Z","shell.execute_reply.started":"2025-07-14T12:14:59.504000Z","shell.execute_reply":"2025-07-14T12:14:59.527099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\n\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-14T12:14:59.528598Z","iopub.execute_input":"2025-07-14T12:14:59.528918Z","iopub.status.idle":"2025-07-14T12:18:53.023667Z","shell.execute_reply.started":"2025-07-14T12:14:59.528902Z","shell.execute_reply":"2025-07-14T12:18:53.023087Z"}},"outputs":[],"execution_count":null}]}