{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":245653776,"sourceType":"kernelVersion"},{"sourceId":552395,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":359040,"modelId":380352}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"DEBUG = False\nTRAIN = False\n\n\nMAX_SEQ_LENGTH = 128\n\nFEATURE_NAMES = [\n    'acc_x', 'acc_y', 'acc_z',\n    'rot_w', 'rot_x', 'rot_y', 'rot_z',\n    'acc_mag', 'rot_angle', 'acc_mag_jerk', 'rot_angle_vel',\n    'linear_acc_x', 'linear_acc_y', 'linear_acc_z', 'linear_acc_mag', 'linear_acc_mag_jerk',\n    'angular_vel_x', 'angular_vel_y', 'angular_vel_z',\n    'angular_distance',\n]\nCATEGORICAL_FEATURES = []\nNUMERICAL_FEATURES = [f for f in FEATURE_NAMES if f not in CATEGORICAL_FEATURES]\n\nLABEL_NAMES = [\n    'Forehead - pull hairline',\n    'Neck - pinch skin',\n    'Forehead - scratch',\n    'Eyelash - pull hair',\n    'Text on phone',\n    'Eyebrow - pull hair',\n    'Neck - scratch',\n    'Above ear - pull hair',\n    'Cheek - pinch skin',\n    'Wave hello',\n    'Write name in air',\n    'Pull air toward your face',\n    'Feel around in tray and pull out an object',\n    'Write name on leg',\n    'Pinch knee/leg skin',\n    'Scratch knee/leg skin',\n    'Drink from bottle/cup',\n    'Glasses on/off'\n]\nIDX2LABEL = {x: i for i, x in enumerate(LABEL_NAMES)}\n\nOUTPUT_DIR = './saved_models'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:17:40.473005Z","iopub.execute_input":"2025-08-30T07:17:40.473245Z","iopub.status.idle":"2025-08-30T07:17:40.488584Z","shell.execute_reply.started":"2025-08-30T07:17:40.473227Z","shell.execute_reply":"2025-08-30T07:17:40.487905Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nif not os.path.exists(OUTPUT_DIR):\n    os.mkdir(OUTPUT_DIR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:17:40.489308Z","iopub.execute_input":"2025-08-30T07:17:40.489547Z","iopub.status.idle":"2025-08-30T07:17:40.505981Z","shell.execute_reply.started":"2025-08-30T07:17:40.489529Z","shell.execute_reply":"2025-08-30T07:17:40.505269Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Load Data","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n%matplotlib inline\n\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nimport joblib","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:17:40.506784Z","iopub.execute_input":"2025-08-30T07:17:40.507032Z","iopub.status.idle":"2025-08-30T07:17:40.520584Z","shell.execute_reply.started":"2025-08-30T07:17:40.507010Z","shell.execute_reply":"2025-08-30T07:17:40.519814Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DATA_ROOT = '/kaggle/input/cmi-detect-behavior-with-sensor-data/'\n\ntrain_df = pd.read_csv(f'{DATA_ROOT}/train.csv')\ntrain_demo_df = pd.read_csv(f'{DATA_ROOT}/train_demographics.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:17:40.522316Z","iopub.execute_input":"2025-08-30T07:17:40.522515Z","iopub.status.idle":"2025-08-30T07:18:15.615633Z","shell.execute_reply.started":"2025-08-30T07:17:40.522498Z","shell.execute_reply":"2025-08-30T07:18:15.615035Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = pd.merge(\n    train_df,\n    train_demo_df,\n    how='left',\n    on='subject'\n)\n\ntrain_df.shape[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:18:15.616390Z","iopub.execute_input":"2025-08-30T07:18:15.616632Z","iopub.status.idle":"2025-08-30T07:18:16.278943Z","shell.execute_reply.started":"2025-08-30T07:18:15.616606Z","shell.execute_reply":"2025-08-30T07:18:16.278254Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Feature Engineering","metadata":{}},{"cell_type":"code","source":"# Remove Gravity\n\nfrom scipy.spatial.transform import Rotation as R\n\ndef remove_gravity_from_acc(acc_data, rot_data):\n\n    if isinstance(acc_data, pd.DataFrame):\n        acc_values = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n    else:\n        acc_values = acc_data\n\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = acc_values.shape[0]\n    linear_accel = np.zeros_like(acc_values)\n    \n    gravity_world = np.array([0, 0, 9.81])\n\n    for i in range(num_samples):\n        if np.all(np.isnan(quat_values[i])) or np.all(np.isclose(quat_values[i], 0)):\n            linear_accel[i, :] = acc_values[i, :] \n            continue\n\n        try:\n            rotation = R.from_quat(quat_values[i])\n            gravity_sensor_frame = rotation.apply(gravity_world, inverse=True)\n            linear_accel[i, :] = acc_values[i, :] - gravity_sensor_frame\n        except ValueError:\n             linear_accel[i, :] = acc_values[i, :]\n             \n    return linear_accel\n\ndef calculate_angular_velocity_from_quat(rot_data, time_delta=1/200): # Assuming 200Hz sampling rate\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_vel = np.zeros((num_samples, 3))\n\n    for i in range(num_samples - 1):\n        q_t = quat_values[i]\n        q_t_plus_dt = quat_values[i+1]\n\n        if np.all(np.isnan(q_t)) or np.all(np.isclose(q_t, 0)) or \\\n           np.all(np.isnan(q_t_plus_dt)) or np.all(np.isclose(q_t_plus_dt, 0)):\n            continue\n\n        try:\n            rot_t = R.from_quat(q_t)\n            rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n\n            # Calculate the relative rotation\n            delta_rot = rot_t.inv() * rot_t_plus_dt\n            \n            # Convert delta rotation to angular velocity vector\n            # The rotation vector (Euler axis * angle) scaled by 1/dt\n            # is a good approximation for small delta_rot\n            angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n        except ValueError:\n            # If quaternion is invalid, angular velocity remains zero\n            pass\n            \n    return angular_vel\n\ndef calculate_angular_distance(rot_data):\n    if isinstance(rot_data, pd.DataFrame):\n        quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    else:\n        quat_values = rot_data\n\n    num_samples = quat_values.shape[0]\n    angular_dist = np.zeros(num_samples)\n\n    for i in range(num_samples - 1):\n        q1 = quat_values[i]\n        q2 = quat_values[i+1]\n\n        if np.all(np.isnan(q1)) or np.all(np.isclose(q1, 0)) or \\\n           np.all(np.isnan(q2)) or np.all(np.isclose(q2, 0)):\n            angular_dist[i] = 0 # Или np.nan, в зависимости от желаемого поведения\n            continue\n        try:\n            # Преобразование кватернионов в объекты Rotation\n            r1 = R.from_quat(q1)\n            r2 = R.from_quat(q2)\n\n            # Вычисление углового расстояния: 2 * arccos(|real(p * q*)|)\n            # где p* - сопряженный кватернион q\n            # В scipy.spatial.transform.Rotation, r1.inv() * r2 дает относительное вращение.\n            # Угол этого относительного вращения - это и есть угловое расстояние.\n            relative_rotation = r1.inv() * r2\n            \n            # Угол rotation vector соответствует угловому расстоянию\n            # Норма rotation vector - это угол в радианах\n            angle = np.linalg.norm(relative_rotation.as_rotvec())\n            angular_dist[i] = angle\n        except ValueError:\n            angular_dist[i] = 0 # В случае недействительных кватернионов\n            pass\n            \n    return angular_dist","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:18:16.279781Z","iopub.execute_input":"2025-08-30T07:18:16.280037Z","iopub.status.idle":"2025-08-30T07:18:16.292266Z","shell.execute_reply.started":"2025-08-30T07:18:16.280013Z","shell.execute_reply":"2025-08-30T07:18:16.291547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def feature_engineering(train_df):\n    # IMU magnitude\n    train_df['acc_mag'] = np.sqrt(train_df['acc_x']**2 + train_df['acc_y']**2 + train_df['acc_z']**2)\n    \n    # IMU angle\n    train_df['rot_angle'] = 2 * np.arccos(train_df['rot_w'].clip(-1, 1))\n    \n    # IMU jerk, angular velocity\n    train_df['acc_mag_jerk'] = train_df.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n    train_df['rot_angle_vel'] = train_df.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n    \n    # Remove gravity\n    def get_linear_accel(df):\n        res = remove_gravity_from_acc(\n            df[['acc_x', 'acc_y', 'acc_z']],\n            df[['rot_x', 'rot_y', 'rot_z', 'rot_w']]\n        )\n        res = pd.DataFrame(res, columns=['linear_acc_x', 'linear_acc_y', 'linear_acc_z'], index=df.index)\n        return res\n    \n    linear_accel_df = train_df.groupby('sequence_id').apply(get_linear_accel, include_groups=False)\n    linear_accel_df = linear_accel_df.droplevel('sequence_id')\n    train_df = train_df.join(linear_accel_df)\n    \n    train_df['linear_acc_mag'] = np.sqrt(train_df['linear_acc_x']**2 + train_df['linear_acc_y']**2 + train_df['linear_acc_z']**2)\n    train_df['linear_acc_mag_jerk'] = train_df.groupby('sequence_id')['linear_acc_mag'].diff().fillna(0)\n\n    # Calc angular velocity\n    def calc_angular_velocity(df):\n        res = calculate_angular_velocity_from_quat( df[['rot_x', 'rot_y', 'rot_z', 'rot_w']] )\n        res = pd.DataFrame(res, columns=['angular_vel_x', 'angular_vel_y', 'angular_vel_z'], index=df.index)\n        return res\n    \n    angular_velocity_df = train_df.groupby('sequence_id').apply(calc_angular_velocity, include_groups=False)\n    angular_velocity_df = angular_velocity_df.droplevel('sequence_id')\n    train_df = train_df.join(angular_velocity_df)\n\n    # Calculating angular distance\n    def calc_angular_distance(df):\n        res = calculate_angular_distance(df[['rot_x', 'rot_y', 'rot_z', 'rot_w']])\n        res = pd.DataFrame(res, columns=['angular_distance'], index=df.index)\n        return res\n    \n    angular_distance_df = train_df.groupby('sequence_id').apply(calc_angular_distance, include_groups=False)\n    angular_distance_df = angular_distance_df.droplevel('sequence_id')\n    train_df = train_df.join(angular_distance_df)\n\n    train_df[FEATURE_NAMES] = train_df[FEATURE_NAMES].ffill().bfill().fillna(0).values.astype('float32')\n    \n    return train_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:18:16.293063Z","iopub.execute_input":"2025-08-30T07:18:16.293242Z","iopub.status.idle":"2025-08-30T07:18:16.308948Z","shell.execute_reply.started":"2025-08-30T07:18:16.293228Z","shell.execute_reply":"2025-08-30T07:18:16.308204Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df = feature_engineering(train_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:18:16.309793Z","iopub.execute_input":"2025-08-30T07:18:16.310048Z","iopub.status.idle":"2025-08-30T07:21:37.446895Z","shell.execute_reply.started":"2025-08-30T07:18:16.310026Z","shell.execute_reply":"2025-08-30T07:21:37.446312Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Standard","metadata":{}},{"cell_type":"code","source":"feature_scaler = StandardScaler()\nfeature_scaler.fit(train_df[NUMERICAL_FEATURES])\njoblib.dump(feature_scaler, f'{OUTPUT_DIR}/feature_scaler.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:37.447801Z","iopub.execute_input":"2025-08-30T07:21:37.448107Z","iopub.status.idle":"2025-08-30T07:21:37.591115Z","shell.execute_reply.started":"2025-08-30T07:21:37.448085Z","shell.execute_reply":"2025-08-30T07:21:37.590351Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_df[NUMERICAL_FEATURES] = feature_scaler.transform(train_df[NUMERICAL_FEATURES])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:37.591973Z","iopub.execute_input":"2025-08-30T07:21:37.592283Z","iopub.status.idle":"2025-08-30T07:21:37.728939Z","shell.execute_reply.started":"2025-08-30T07:21:37.592260Z","shell.execute_reply":"2025-08-30T07:21:37.728401Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"len(NUMERICAL_FEATURES)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:37.729619Z","iopub.execute_input":"2025-08-30T07:21:37.729852Z","iopub.status.idle":"2025-08-30T07:21:37.734873Z","shell.execute_reply.started":"2025-08-30T07:21:37.729832Z","shell.execute_reply":"2025-08-30T07:21:37.734148Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Convert to Sequence","metadata":{}},{"cell_type":"code","source":"agg_train_df = train_df.groupby(['sequence_id', 'subject', 'gesture']).apply(\n    lambda df: df[FEATURE_NAMES].to_dict(orient='records'),\n    include_groups=False,\n)\n\nagg_train_df = agg_train_df.reset_index()\nagg_train_df.columns = ['sequence_id', 'subject', 'gesture', 'sequence']","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:37.735584Z","iopub.execute_input":"2025-08-30T07:21:37.735841Z","iopub.status.idle":"2025-08-30T07:21:54.670095Z","shell.execute_reply.started":"2025-08-30T07:21:37.735825Z","shell.execute_reply":"2025-08-30T07:21:54.669612Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df['label'] = agg_train_df.gesture.map(IDX2LABEL)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:54.670974Z","iopub.execute_input":"2025-08-30T07:21:54.671187Z","iopub.status.idle":"2025-08-30T07:21:54.676503Z","shell.execute_reply.started":"2025-08-30T07:21:54.671172Z","shell.execute_reply":"2025-08-30T07:21:54.675760Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"agg_train_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:54.679555Z","iopub.execute_input":"2025-08-30T07:21:54.679801Z","iopub.status.idle":"2025-08-30T07:21:54.790336Z","shell.execute_reply.started":"2025-08-30T07:21:54.679784Z","shell.execute_reply":"2025-08-30T07:21:54.789651Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Augmentation","metadata":{}},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport random\nfrom typing import Dict, List, Optional, Tuple\nfrom scipy import signal\nfrom scipy.interpolate import interp1d\n\n\nclass TimeSeriesAugmentation:\n    \"\"\"时序数据增强类，专门用于IMU和ToF传感器数据\"\"\"\n    \n    def __init__(self, \n                 time_stretch_range: Tuple[float, float] = (0.8, 1.2),\n                 time_shift_range: float = 0.1,\n                 noise_std: float = 0.02,\n                 magnitude_scale_range: Tuple[float, float] = (0.9, 1.1),\n                 rotation_angle_range: float = 0.1,\n                 mask_ratio: float = 0.1,\n                 freq_filter_range: Tuple[float, float] = (0.1, 0.9),\n                 aug_prob: float = 0.8):\n        \"\"\"\n        初始化数据增强参数\n        \n        Args:\n            time_stretch_range: 时间拉伸范围 (最小比例, 最大比例)\n            time_shift_range: 时间移位范围 (序列长度的比例)\n            noise_std: 高斯噪声标准差\n            magnitude_scale_range: 幅度缩放范围\n            rotation_angle_range: 旋转角度范围 (弧度)\n            mask_ratio: 时间遮盖比例\n            freq_filter_range: 频率滤波范围\n            aug_prob: 每种增强方法的应用概率\n        \"\"\"\n        self.time_stretch_range = time_stretch_range\n        self.time_shift_range = time_shift_range\n        self.noise_std = noise_std\n        self.magnitude_scale_range = magnitude_scale_range\n        self.rotation_angle_range = rotation_angle_range\n        self.mask_ratio = mask_ratio\n        self.freq_filter_range = freq_filter_range\n        self.aug_prob = aug_prob\n        \n        # IMU特征索引 (前7个特征: acc_x, acc_y, acc_z, rot_w, rot_x, rot_y, rot_z)\n        self.imu_indices = list(range(7))\n        self.acc_indices = [0, 1, 2]  # 加速度\n        self.rot_indices = [3, 4, 5, 6]  # 四元数旋转\n    \n    def __call__(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"\n        对序列应用随机增强\n        \n        Args:\n            sequence: 输入序列 (seq_len, num_features)\n        \n        Returns:\n            augmented_sequence: 增强后的序列\n        \"\"\"\n        augmented = sequence.copy()\n        \n        # 随机应用各种增强方法\n        if random.random() < self.aug_prob:\n            augmented = self.time_stretch(augmented)\n        \n        if random.random() < self.aug_prob:\n            augmented = self.time_shift(augmented)\n        \n        if random.random() < self.aug_prob:\n            augmented = self.add_noise(augmented)\n        \n        if random.random() < self.aug_prob:\n            augmented = self.magnitude_scale(augmented)\n        \n        if random.random() < self.aug_prob:\n            augmented = self.rotate_imu(augmented)\n        \n        if random.random() < self.aug_prob:\n            augmented = self.time_mask(augmented)\n        \n        if random.random() < self.aug_prob:\n            augmented = self.frequency_filter(augmented)\n        \n        return augmented\n    \n    def time_stretch(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"时间拉伸/压缩\"\"\"\n        seq_len, num_features = sequence.shape\n        stretch_factor = random.uniform(*self.time_stretch_range)\n        \n        # 创建新的时间采样点\n        original_time = np.linspace(0, 1, seq_len)\n        new_length = max(1, int(seq_len / stretch_factor))\n        new_time = np.linspace(0, 1, new_length)\n        \n        # 对每个特征进行插值\n        stretched_sequence = np.zeros((new_length, num_features))\n        for i in range(num_features):\n            interpolator = interp1d(original_time, sequence[:, i], \n                                  kind='linear', bounds_error=False, \n                                  fill_value='extrapolate')\n            stretched_sequence[:, i] = interpolator(new_time)\n        \n        # 如果序列变长，截断到原长度；如果变短，用零填充\n        if new_length > seq_len:\n            return stretched_sequence[:seq_len]\n        else:\n            padded = np.zeros((seq_len, num_features))\n            padded[:new_length] = stretched_sequence\n            return padded\n    \n    def time_shift(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"时间移位\"\"\"\n        seq_len, num_features = sequence.shape\n        max_shift = int(seq_len * self.time_shift_range)\n        shift = random.randint(-max_shift, max_shift)\n        \n        if shift == 0:\n            return sequence\n        \n        shifted = np.zeros_like(sequence)\n        if shift > 0:\n            shifted[shift:] = sequence[:-shift]\n        else:\n            shifted[:shift] = sequence[-shift:]\n        \n        return shifted\n    \n    def add_noise(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"添加高斯噪声\"\"\"\n        noise = np.random.normal(0, self.noise_std, sequence.shape)\n        return sequence + noise\n    \n    def magnitude_scale(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"幅度缩放\"\"\"\n        scale_factor = random.uniform(*self.magnitude_scale_range)\n        scaled = sequence.copy()\n        \n        # 只对IMU数据进行缩放，保持其他特征不变\n        scaled[:, self.imu_indices] *= scale_factor\n        \n        return scaled\n    \n    def rotate_imu(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"对IMU数据应用小角度旋转\"\"\"\n        rotated = sequence.copy()\n        \n        # 生成小角度旋转矩阵\n        angle = random.uniform(-self.rotation_angle_range, self.rotation_angle_range)\n        cos_a, sin_a = np.cos(angle), np.sin(angle)\n        \n        # 只旋转加速度数据 (简化版本，只在xy平面旋转)\n        acc_x = rotated[:, 0] * cos_a - rotated[:, 1] * sin_a\n        acc_y = rotated[:, 0] * sin_a + rotated[:, 1] * cos_a\n        \n        rotated[:, 0] = acc_x\n        rotated[:, 1] = acc_y\n        \n        return rotated\n    \n    def time_mask(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"时间遮盖 - 随机遮盖一段时间窗口\"\"\"\n        seq_len, num_features = sequence.shape\n        mask_length = int(seq_len * self.mask_ratio)\n        \n        if mask_length == 0:\n            return sequence\n        \n        start_idx = random.randint(0, seq_len - mask_length)\n        masked = sequence.copy()\n        \n        # 用前后平均值填充遮盖区域\n        if start_idx > 0 and start_idx + mask_length < seq_len:\n            before_mean = np.mean(masked[max(0, start_idx-5):start_idx], axis=0)\n            after_mean = np.mean(masked[start_idx+mask_length:min(seq_len, start_idx+mask_length+5)], axis=0)\n            fill_value = (before_mean + after_mean) / 2\n        else:\n            fill_value = np.mean(masked, axis=0)\n        \n        masked[start_idx:start_idx+mask_length] = fill_value\n        \n        return masked\n    \n    def frequency_filter(self, sequence: np.ndarray) -> np.ndarray:\n        \"\"\"频域滤波\"\"\"\n        seq_len, num_features = sequence.shape\n        if seq_len < 10:  # 序列太短，跳过滤波\n            return sequence\n        \n        filtered = sequence.copy()\n        \n        # 只对IMU数据进行滤波\n        for i in self.imu_indices:\n            # 应用低通滤波或高通滤波\n            if random.random() < 0.5:  # 低通滤波\n                cutoff = random.uniform(*self.freq_filter_range)\n                sos = signal.butter(2, cutoff, btype='low', output='sos')\n            else:  # 高通滤波\n                cutoff = random.uniform(0.05, self.freq_filter_range[0])\n                sos = signal.butter(2, cutoff, btype='high', output='sos')\n            \n            try:\n                filtered[:, i] = signal.sosfilt(sos, sequence[:, i])\n            except:\n                # 如果滤波失败，保持原数据\n                pass\n        \n        return filtered\n\n\nclass MixupAugmentation:\n    \"\"\"Mixup数据增强 - 混合两个不同的序列\"\"\"\n    \n    def __init__(self, alpha: float = 0.2, prob: float = 0.5):\n        \"\"\"\n        Args:\n            alpha: Beta分布参数\n            prob: 应用Mixup的概率\n        \"\"\"\n        self.alpha = alpha\n        self.prob = prob\n    \n    def __call__(self, batch_sequences: torch.Tensor, batch_labels: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, float]:\n        \"\"\"\n        对批次数据应用Mixup\n        \n        Args:\n            batch_sequences: (batch_size, seq_len, num_features)\n            batch_labels: (batch_size,)\n        \n        Returns:\n            mixed_sequences: 混合后的序列\n            labels_a: 原始标签\n            labels_b: 混合标签\n            lambda_: 混合比例\n        \"\"\"\n        if random.random() > self.prob:\n            return batch_sequences, batch_labels, batch_labels, 1.0\n        \n        batch_size = batch_sequences.size(0)\n        lambda_ = np.random.beta(self.alpha, self.alpha) if self.alpha > 0 else 1\n        \n        # 随机打乱顺序\n        index = torch.randperm(batch_size)\n        \n        # 混合序列\n        mixed_sequences = lambda_ * batch_sequences + (1 - lambda_) * batch_sequences[index]\n        \n        labels_a = batch_labels\n        labels_b = batch_labels[index]\n        \n        return mixed_sequences, labels_a, labels_b, lambda_\n\n\ndef mixup_criterion(criterion, outputs, labels_a, labels_b, lambda_):\n    \"\"\"Mixup损失函数\"\"\"\n    return lambda_ * criterion(outputs, labels_a) + (1 - lambda_) * criterion(outputs, labels_b)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:54.791177Z","iopub.execute_input":"2025-08-30T07:21:54.791523Z","iopub.status.idle":"2025-08-30T07:21:54.936804Z","shell.execute_reply.started":"2025-08-30T07:21:54.791506Z","shell.execute_reply":"2025-08-30T07:21:54.936198Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DataLoader","metadata":{}},{"cell_type":"code","source":"import torch\nimport numpy as np\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.model_selection import StratifiedGroupKFold, GroupKFold\nimport pandas as pd\nfrom typing import List, Dict\n\nclass SequenceDataset(Dataset):\n    def __init__(self, sequences: List[List[Dict]], labels: List[int], is_training=True, use_augmentation=True, augmentation_params=None):\n        \"\"\"\n        Args:\n            sequences: 序列数据列表，每个序列包含多个时间步的特征字典\n            labels: 对应的标签列表\n        \"\"\"\n        self.sequences = sequences\n        self.labels = labels\n        self.is_training = is_training\n        self.use_augmentation = use_augmentation and is_training\n        \n        # 转换为numpy数组格式并截断\n        self.processed_sequences = self._process_sequences()\n\n        # 初始化数据增强器\n        if self.use_augmentation:\n            if augmentation_params is None:\n                augmentation_params = {\n                    'time_stretch_range': (0.8, 1.2),\n                    'time_shift_range': 0.1,\n                    'noise_std': 0.02,\n                    'magnitude_scale_range': (0.9, 1.1),\n                    'rotation_angle_range': 0.1,\n                    'mask_ratio': 0.1,\n                    'freq_filter_range': (0.1, 0.9),\n                    'aug_prob': 0.5  # 训练时降低概率，避免过度增强\n                }\n            self.augmenter = TimeSeriesAugmentation(**augmentation_params)\n            print(f\"✅ 数据增强已启用 (训练模式: {self.is_training})\")\n        else:\n            self.augmenter = None\n            print(f\"❌ 数据增强已禁用 (训练模式: {self.is_training})\")\n        \n        \n    def _process_sequences(self):\n        \"\"\"将字典格式的序列转换为numpy数组并截断到最大长度\"\"\"\n        processed = []\n        \n        for seq in self.sequences:\n            # 截断到最大长度\n            seq = seq[:MAX_SEQ_LENGTH]\n            \n            seq_array = []\n            for timestep in seq:\n                # 按照feature_names的顺序提取特征\n                features = [timestep[feature] for feature in FEATURE_NAMES]\n                seq_array.append(features)\n            processed.append(np.array(seq_array, dtype=np.float32))\n        \n        return processed\n    \n    def __len__(self):\n        return len(self.sequences)\n    \n    def __getitem__(self, idx):\n        sequence = self.processed_sequences[idx].copy()\n        label = torch.LongTensor([self.labels[idx]])[0]\n\n        # 在训练时应用数据增强\n        if self.use_augmentation and self.augmenter is not None:\n            sequence = self.augmenter(sequence)\n        \n        sequence = torch.FloatTensor(sequence)\n        return sequence, label\n\n\ndef collate_fn(batch):\n    \"\"\"自定义批处理函数，处理不同长度的序列\"\"\"\n    sequences, labels = zip(*batch)\n    \n    # 获取批次中的最大序列长度\n    max_length = max(seq.shape[0] for seq in sequences)\n    \n    # 填充序列到相同长度\n    padded_sequences = []\n    lengths = []\n    \n    for seq in sequences:\n        seq_len = seq.shape[0]\n        lengths.append(seq_len)\n        \n        if seq_len < max_length:\n            # 使用零填充\n            padding = torch.zeros(max_length - seq_len, seq.shape[1])\n            padded_seq = torch.cat([seq, padding], dim=0)\n        else:\n            padded_seq = seq\n        \n        padded_sequences.append(padded_seq)\n    \n    return torch.stack(padded_sequences), torch.stack(labels), torch.LongTensor(lengths)\n\n\ndef create_kfold_splits(sequences, labels, subjects, n_splits=5, random_state=42):\n    \"\"\"使用StratifiedGroupKFold创建K折分割\"\"\"\n    sequences = np.array(sequences, dtype=object)\n    labels = np.array(labels)\n    subjects = np.array(subjects)\n\n    if sequences.shape[0] > 1000:\n        sgkf = StratifiedGroupKFold(n_splits=n_splits, shuffle=True, random_state=random_state)\n    else:\n        n_splits = 2\n        sgkf = GroupKFold(n_splits=n_splits, shuffle=True, random_state=random_state)\n    \n    splits = []\n    for fold_idx, (train_idx, val_idx) in enumerate(sgkf.split(sequences, labels, subjects)):\n        splits.append((train_idx, val_idx))\n        print(f\"   Fold {fold_idx+1}: 训练={len(train_idx)}, 验证={len(val_idx)}\")\n    \n    print(f\"✅ 成功创建{n_splits}折交叉验证分割\")\n    return splits\n\n\ndef create_fold_loaders(sequences, labels, train_idx, val_idx, batch_size=32, use_augmentation=True, augmentation_params=None, use_mixup=False, mixup_params=None):\n    \"\"\"为特定的折创建数据加载器，使用全局scaler\"\"\"\n    # 根据索引分割数据\n    train_sequences = [sequences[i] for i in train_idx]\n    train_labels = [labels[i] for i in train_idx]\n    val_sequences = [sequences[i] for i in val_idx]\n    val_labels = [labels[i] for i in val_idx]\n    \n    # 创建数据集，使用全局scaler\n    train_dataset = SequenceDataset(train_sequences, train_labels, is_training=True, use_augmentation=use_augmentation, augmentation_params=augmentation_params)\n    val_dataset = SequenceDataset(val_sequences, val_labels, is_training=False, use_augmentation=False)\n    \n    # 创建数据加载器\n    train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)\n    val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)\n\n    # 创建Mixup增强器\n    mixup_augmenter = None\n    if use_mixup:\n        if mixup_params is None:\n            mixup_params = {'alpha': 0.2, 'prob': 0.5}\n        mixup_augmenter = MixupAugmentation(**mixup_params)\n        print(f\"✅ Mixup数据增强已启用: alpha={mixup_params['alpha']}, prob={mixup_params['prob']}\")\n    \n    return train_loader, val_loader, mixup_augmenter","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:54.937537Z","iopub.execute_input":"2025-08-30T07:21:54.937884Z","iopub.status.idle":"2025-08-30T07:21:54.953246Z","shell.execute_reply.started":"2025-08-30T07:21:54.937859Z","shell.execute_reply":"2025-08-30T07:21:54.952590Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.squeeze = nn.AdaptiveAvgPool1d(1)\n        self.excitation = nn.Sequential(\n            nn.Linear(channels, channels // reduction, bias=False),\n            nn.ReLU(inplace=True),\n            nn.Linear(channels // reduction, channels, bias=False),\n            nn.Sigmoid()\n        )\n    \n    def forward(self, x):\n        b, c, _ = x.size()\n        y = self.squeeze(x).view(b, c)\n        y = self.excitation(y).view(b, c, 1)\n        return x * y.expand_as(x)\n\nclass ResidualSECNNBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size, pool_size=2, dropout=0.3, weight_decay=1e-4):\n        super().__init__()\n        \n        # First conv block\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size, padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        \n        # Second conv block\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size, padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n        \n        # SE block\n        self.se = SEBlock(out_channels)\n        \n        # Shortcut connection\n        self.shortcut = nn.Sequential()\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, 1, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n        \n        self.pool = nn.MaxPool1d(pool_size)\n        self.dropout = nn.Dropout(dropout)\n        \n    def forward(self, x):\n        shortcut = self.shortcut(x)\n        \n        # First conv\n        out = F.relu(self.bn1(self.conv1(x)))\n        # Second conv\n        out = self.bn2(self.conv2(out))\n        \n        # SE block\n        out = self.se(out)\n        \n        # Add shortcut\n        out += shortcut\n        out = F.relu(out)\n        \n        # Pool and dropout\n        out = self.pool(out)\n        out = self.dropout(out)\n        \n        return out\n\nclass AttentionLayer(nn.Module):\n    def __init__(self, hidden_dim):\n        super().__init__()\n        self.attention = nn.Linear(hidden_dim, 1)\n        \n    def forward(self, x):\n        # x shape: (batch, seq_len, hidden_dim)\n        scores = torch.tanh(self.attention(x))  # (batch, seq_len, 1)\n        weights = F.softmax(scores.squeeze(-1), dim=1)  # (batch, seq_len)\n        context = torch.sum(x * weights.unsqueeze(-1), dim=1)  # (batch, hidden_dim)\n        return context\n\nclass TwoBranchModel(nn.Module):\n    def __init__(self, imu_dim, tof_dim, n_classes, weight_decay=1e-4):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.tof_dim = tof_dim\n        self.n_classes = n_classes\n        self.weight_decay = weight_decay\n        \n        # IMU deep branch\n        self.imu_block1 = ResidualSECNNBlock(imu_dim, 64, 3, dropout=0.3, weight_decay=weight_decay)\n        self.imu_block2 = ResidualSECNNBlock(64, 128, 5, dropout=0.3, weight_decay=weight_decay)\n        \n        # # TOF/Thermal lighter branch\n        # self.tof_conv1 = nn.Conv1d(tof_dim, 64, 3, padding=1, bias=False)\n        # self.tof_bn1 = nn.BatchNorm1d(64)\n        # self.tof_pool1 = nn.MaxPool1d(2)\n        # self.tof_drop1 = nn.Dropout(0.3)\n        \n        # self.tof_conv2 = nn.Conv1d(64, 128, 3, padding=1, bias=False)\n        # self.tof_bn2 = nn.BatchNorm1d(128)\n        # self.tof_pool2 = nn.MaxPool1d(2)\n        # self.tof_drop2 = nn.Dropout(0.3)\n        \n        # BiLSTM\n        # self.bilstm = nn.LSTM(256, 128, bidirectional=True, batch_first=True)\n        # self.bilstm = nn.LSTM(128, 128, bidirectional=True, batch_first=True)\n        # self.lstm_dropout = nn.Dropout(0.4)\n        self.bigru = nn.GRU(128, 128, bidirectional=True, batch_first=True)\n        self.gru_dropout = nn.Dropout(0.4)\n        \n        # Attention\n        self.attention = AttentionLayer(256)  # 128*2 for bidirectional\n        \n        # Dense layers\n        self.dense1 = nn.Linear(256, 256, bias=False)\n        self.bn_dense1 = nn.BatchNorm1d(256)\n        self.drop1 = nn.Dropout(0.5)\n        \n        self.dense2 = nn.Linear(256, 128, bias=False)\n        self.bn_dense2 = nn.BatchNorm1d(128)\n        self.drop2 = nn.Dropout(0.3)\n        \n        self.classifier = nn.Linear(128, n_classes)\n        \n    def forward(self, x):\n        # Split input\n        # imu = x[:, :, :self.imu_dim].transpose(1, 2)  # (batch, imu_dim, seq_len)\n        # tof = x[:, :, self.imu_dim:].transpose(1, 2)  # (batch, tof_dim, seq_len)\n\n        imu = x[:, :, :self.imu_dim]  # imu only   batch, seq_len, hidden_dim\n        imu = imu.transpose(1, 2)  # (batch, imu_dim, seq_len)\n\n        \n        # IMU branch\n        x1 = self.imu_block1(imu)\n        x1 = self.imu_block2(x1)\n        \n        # TOF branch\n        # x2 = F.relu(self.tof_bn1(self.tof_conv1(tof)))\n        # x2 = self.tof_drop1(self.tof_pool1(x2))\n        # x2 = F.relu(self.tof_bn2(self.tof_conv2(x2)))\n        # x2 = self.tof_drop2(self.tof_pool2(x2))\n        \n        # Concatenate branches\n        # merged = torch.cat([x1, x2], dim=1).transpose(1, 2)  # (batch, seq_len, 256)\n        merged = x1.transpose(1, 2)  # (batch, seq_len, 128)\n        \n        # BiLSTM\n        # lstm_out, _ = self.bilstm(merged)\n        # lstm_out = self.lstm_dropout(lstm_out)\n        gru_out, _ = self.bigru(merged)\n        gru_out = self.gru_dropout(gru_out)\n        \n        # Attention\n        attended = self.attention(gru_out)\n        \n        # Dense layers\n        x = F.relu(self.bn_dense1(self.dense1(attended)))\n        x = self.drop1(x)\n        x = F.relu(self.bn_dense2(self.dense2(x)))\n        x = self.drop2(x)\n        \n        # Classification\n        logits = self.classifier(x)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:54.953896Z","iopub.execute_input":"2025-08-30T07:21:54.954161Z","iopub.status.idle":"2025-08-30T07:21:54.970887Z","shell.execute_reply.started":"2025-08-30T07:21:54.954139Z","shell.execute_reply":"2025-08-30T07:21:54.970203Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Train","metadata":{}},{"cell_type":"markdown","source":"### Metric","metadata":{}},{"cell_type":"code","source":"import kagglehub\nmetric = kagglehub.package_import('jiazhuang/cmi-2025-metric')\n\ndef get_competition_score(true, pred):\n    assert len(true) == len(pred)\n    N = len(true)\n    true = pd.DataFrame({'id': range(N), 'gesture': true})\n    pred = pd.DataFrame({'id': range(N), 'gesture': pred})\n    return metric.score(true, pred, 'id')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:54.971538Z","iopub.execute_input":"2025-08-30T07:21:54.971715Z","iopub.status.idle":"2025-08-30T07:21:55.551066Z","shell.execute_reply.started":"2025-08-30T07:21:54.971701Z","shell.execute_reply":"2025-08-30T07:21:55.550512Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom sklearn.metrics import accuracy_score, classification_report, confusion_matrix\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm import tqdm\nimport json\n\n# from metric import get_competition_score\n\n\nclass LabelSmoothingCrossEntropy(nn.Module):\n    \"\"\"Label Smoothing交叉熵损失函数\"\"\"\n    \n    def __init__(self, smoothing: float = 0.1, num_classes: int = None):\n        \"\"\"\n        Args:\n            smoothing: 平滑参数，范围[0, 1]，0表示不平滑，1表示均匀分布\n            num_classes: 类别数量\n        \"\"\"\n        super().__init__()\n        self.smoothing = smoothing\n        self.num_classes = num_classes\n        \n    def forward(self, pred, target):\n        \"\"\"\n        Args:\n            pred: 模型预测logits，形状[batch_size, num_classes]\n            target: 真实标签，形状[batch_size]\n        \"\"\"\n        batch_size, num_classes = pred.shape\n        if self.num_classes is None:\n            self.num_classes = num_classes\n            \n        # 计算log softmax\n        log_pred = F.log_softmax(pred, dim=1)\n        \n        # 创建平滑标签\n        with torch.no_grad():\n            # 创建one-hot标签\n            true_dist = torch.zeros_like(log_pred)\n            true_dist.scatter_(1, target.unsqueeze(1), 1.0)\n            \n            # 应用label smoothing\n            # 正确类别的概率 = 1 - smoothing + smoothing / num_classes\n            # 其他类别的概率 = smoothing / num_classes\n            true_dist = true_dist * (1 - self.smoothing) + self.smoothing / num_classes\n        \n        # 计算KL散度损失\n        return torch.mean(torch.sum(-true_dist * log_pred, dim=1))\n\n\nclass Trainer:\n    \"\"\"LSTM分类器训练器\"\"\"\n    \n    def __init__(self, model, device, class_names, label_smoothing=0.0):\n        self.model = model.to(device)\n        self.device = device\n        self.class_names = class_names\n        self.label_smoothing = label_smoothing\n        self.train_losses = []\n        self.val_losses = []\n        self.train_accuracies = []\n        self.val_accuracies = []\n        self.val_scores = []\n\n        # 根据label_smoothing参数选择损失函数\n        if label_smoothing > 0:\n            self.criterion = LabelSmoothingCrossEntropy(\n                smoothing=label_smoothing, \n                num_classes=len(class_names)\n            )\n            print(f\"✅ 使用Label Smoothing (α={label_smoothing:.3f})\")\n        else:\n            self.criterion = nn.CrossEntropyLoss()\n            print(\"✅ 使用标准交叉熵损失\")\n        \n    def train_epoch(self, train_loader, optimizer, criterion, mixup_augmenter=None):\n        \"\"\"训练一个epoch\"\"\"\n        self.model.train()\n        total_loss = 0\n        correct_predictions = 0\n        total_predictions = 0\n        \n        progress_bar = tqdm(train_loader, desc=\"训练中\", disable=True)\n        \n        for batch_idx, (sequences, labels, lengths) in enumerate(progress_bar):\n            sequences = sequences.to(self.device)\n            labels = labels.to(self.device)\n\n            # 应用Mixup增强\n            if mixup_augmenter is not None:\n                sequences, labels_a, labels_b, lambda_ = mixup_augmenter(sequences, labels)\n            else:\n                labels_a, labels_b, lambda_ = labels, labels, 1.0\n            \n            # 前向传播\n            optimizer.zero_grad()\n            outputs = self.model(sequences)\n\n            # 计算损失\n            if mixup_augmenter is not None and lambda_ != 1.0:\n                loss = mixup_criterion(criterion, outputs, labels_a, labels_b, lambda_)\n            else:\n                loss = criterion(outputs, labels)\n            \n            # 反向传播\n            loss.backward()\n            \n            # 梯度裁剪\n            torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)\n            \n            optimizer.step()\n            \n            # 统计\n            total_loss += loss.item()\n            _, predicted = torch.max(outputs.data, 1)\n\n            if mixup_augmenter is not None and lambda_ != 1.0:\n                # Mixup时使用lambda_加权的准确率\n                correct_predictions += (lambda_ * (predicted == labels_a).sum().item() + \n                                     (1 - lambda_) * (predicted == labels_b).sum().item())\n            else:\n                correct_predictions += (predicted == labels).sum().item()\n            \n            total_predictions += labels.size(0)\n            \n            # 更新进度条\n            progress_bar.set_postfix({\n                'Loss': f'{loss.item():.4f}',\n                'Acc': f'{correct_predictions/total_predictions:.4f}',\n                'Mixup': 'ON' if (mixup_augmenter is not None and lambda_ != 1.0) else 'OF'\n            })\n        \n        avg_loss = total_loss / len(train_loader)\n        accuracy = correct_predictions / total_predictions\n        \n        return avg_loss, accuracy\n    \n    def validate(self, val_loader, criterion):\n        \"\"\"验证\"\"\"\n        self.model.eval()\n        total_loss = 0\n        correct_predictions = 0\n        total_predictions = 0\n        all_predictions = []\n        all_labels = []\n        \n        with torch.no_grad():\n            for sequences, labels, lengths in tqdm(val_loader, desc=\"验证中\", disable=True):\n                sequences = sequences.to(self.device)\n                labels = labels.to(self.device)\n                \n                outputs = self.model(sequences)\n                loss = criterion(outputs, labels)\n                \n                total_loss += loss.item()\n                _, predicted = torch.max(outputs.data, 1)\n                correct_predictions += (predicted == labels).sum().item()\n                total_predictions += labels.size(0)\n                \n                all_predictions.extend(predicted.cpu().numpy())\n                all_labels.extend(labels.cpu().numpy())\n        \n        avg_loss = total_loss / len(val_loader)\n        accuracy = correct_predictions / total_predictions\n        \n        return avg_loss, accuracy, all_predictions, all_labels\n    \n    def train(self, train_loader, val_loader, num_epochs=50, learning_rate=0.001,\n              weight_decay=1e-5, patience=10, save_path='best_model.pth', mixup_augmenter=None):\n        \"\"\"完整训练流程\"\"\"\n        \n        # 优化器和损失函数\n        optimizer = optim.Adam(self.model.parameters(), lr=learning_rate, weight_decay=weight_decay)\n        criterion = self.criterion\n        \n        # 学习率调度器\n        scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n            optimizer, mode='max', factor=0.5, patience=patience//2\n        )\n        \n        # 早停\n        best_val_score = -float('inf')\n        epochs_without_improvement = 0\n        \n        print(f\"开始训练，设备: {self.device}\")\n        print(f\"模型参数量: {sum(p.numel() for p in self.model.parameters()):,}\")\n        \n        # 记录当前学习率用于检测变化\n        current_lr = learning_rate\n        \n        for epoch in range(num_epochs):\n            # 训练\n            train_loss, train_acc = self.train_epoch(train_loader, optimizer, criterion, mixup_augmenter)\n            \n            # 验证\n            val_loss, val_acc, val_preds, val_labels = self.validate(val_loader, criterion)\n            val_preds = [self.class_names[pred] for pred in val_preds]\n            val_labels = [self.class_names[label] for label in val_labels]\n            val_score = get_competition_score(val_labels, val_preds)\n            \n            # 记录历史\n            self.train_losses.append(train_loss)\n            self.val_losses.append(val_loss)\n            self.train_accuracies.append(train_acc)\n            self.val_accuracies.append(val_acc)\n            self.val_scores.append(val_score)\n            \n            # 更新学习率\n            old_lr = optimizer.param_groups[0]['lr']\n            scheduler.step(val_score)\n            new_lr = optimizer.param_groups[0]['lr']\n            \n            # 手动记录学习率变化\n            # if new_lr != old_lr:\n            #     print(f\"学习率从 {old_lr:.6f} 降低到 {new_lr:.6f}\")\n\n            if epoch % 10 == 0:\n                print(f\"\\nEpoch {epoch+1}/{num_epochs}\")\n                print(\"-\" * 50)\n                print(f\"训练 - Loss: {train_loss:.4f}, Acc: {train_acc:.4f}\")\n                print(f\"验证 - Loss: {val_loss:.4f}, Acc: {val_acc:.4f}, Score: {val_score:.4f}\")\n                print(f\"当前学习率: {new_lr:.6f}\")\n            \n            # 早停检查\n            if val_score > best_val_score:\n                best_val_score = val_score\n                epochs_without_improvement = 0\n                # 保存最佳模型\n                torch.save({\n                    'model_state_dict': self.model.state_dict(),\n                    'optimizer_state_dict': optimizer.state_dict(),\n                    'epoch': epoch,\n                    'val_loss': val_loss,\n                    'val_acc': val_acc,\n                    'val_score': val_score,\n                    'model_config': {                        \n                        'imu_dim': self.model.imu_dim,\n                        'tof_dim': self.model.tof_dim,\n                        'n_classes': self.model.n_classes,\n                        'weight_decay': self.model.weight_decay\n                    }\n                }, save_path)\n                # print(f\"保存最佳模型到 {save_path}\")\n            else:\n                epochs_without_improvement += 1\n                \n            if epochs_without_improvement >= patience:\n                print(f\"早停：{patience} 个epoch无改善\")\n                break\n        \n        print(\"训练完成！\")\n        return self.train_losses, self.val_losses, self.train_accuracies, self.val_accuracies, self.val_scores\n    \n    def plot_training_history(self, save_path='training_history.png'):\n        \"\"\"绘制训练历史\"\"\"\n        fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5))\n        \n        # 损失曲线\n        ax1.plot(self.train_losses, label='train loss')\n        ax1.plot(self.val_losses, label='val loss')\n        ax1.set_title('train and val loss')\n        ax1.set_xlabel('Epoch')\n        ax1.set_ylabel('Loss')\n        ax1.legend()\n        ax1.grid(True)\n        \n        # 准确率曲线\n        ax2.plot(self.train_accuracies, label='train accuracy')\n        ax2.plot(self.val_accuracies, label='val accuracy')\n        ax2.set_title('train and val accuracy')\n        ax2.set_xlabel('Epoch')\n        ax2.set_ylabel('Accuracy')\n        ax2.legend()\n        ax2.grid(True)\n\n        # 分数曲线\n        ax3.plot(self.val_scores, label='val score')\n        ax3.set_title('val score')\n        ax3.set_xlabel('Epoch')\n        ax3.set_ylabel('Score')\n        ax3.legend()\n        ax3.grid(True)\n        \n        plt.tight_layout()\n        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n        plt.show()\n        print(f\"训练历史图保存到 {save_path}\")\n\n\ndef evaluate_model(model, test_loader, device, class_names=None):\n    \"\"\"评估模型\"\"\"\n    model.eval()\n    all_predictions = []\n    all_labels = []\n    all_probabilities = []\n    \n    with torch.no_grad():\n        for sequences, labels, lengths in tqdm(test_loader, desc=\"评估中\", disable=True):\n            sequences = sequences.to(device)\n            labels = labels.to(device)\n            \n            outputs = model(sequences)\n            probabilities = torch.softmax(outputs, dim=1)\n            _, predicted = torch.max(outputs, 1)\n            \n            all_predictions.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probabilities.extend(probabilities.cpu().numpy())\n    \n    # 计算指标\n    accuracy = accuracy_score(all_labels, all_predictions)\n    score = get_competition_score(\n        [class_names[label] for label in all_labels],\n        [class_names[pred] for pred in all_predictions]\n    )\n\n    # 分类报告\n    if class_names is None:\n        class_names = [f'Class_{i}' for i in range(len(set(all_labels)))]\n    \n    # import pdb; pdb.set_trace()\n    sub_class_names = sorted(set(all_labels + all_predictions))\n    sub_class_names = [class_names[i] for i in sub_class_names]\n    report = classification_report(all_labels, all_predictions, \n                                 target_names=sub_class_names, output_dict=True)\n    \n    print(f\"测试准确率: {accuracy:.4f}\")\n    print(f\"测试分数: {score:.4f}\")\n    print(\"\\n分类报告:\")\n    print(classification_report(all_labels, all_predictions, target_names=sub_class_names))\n    \n    # 混淆矩阵\n    cm = confusion_matrix(all_labels, all_predictions)\n    plt.figure(figsize=(12, 10))\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n                xticklabels=sub_class_names, yticklabels=sub_class_names)\n    plt.title('confusion matrix')\n    plt.ylabel('true label')\n    plt.xlabel('predicted label')\n    plt.tight_layout()\n    plt.savefig(f'{OUTPUT_DIR}/confusion_matrix.png', dpi=300, bbox_inches='tight')\n    plt.show()\n    \n    return accuracy, score, report, all_predictions, all_probabilities","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:55.553963Z","iopub.execute_input":"2025-08-30T07:21:55.554207Z","iopub.status.idle":"2025-08-30T07:21:55.937338Z","shell.execute_reply.started":"2025-08-30T07:21:55.554186Z","shell.execute_reply":"2025-08-30T07:21:55.936566Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_single_fold(sequences, labels, subjects, train_idx, val_idx,\n                     fold_num, total_folds, device, num_epochs=50, \n                     learning_rate=0.001, patience=10, batch_size=16, label_smoothing=0.0,\n                     use_mixup=False, mixup_params=None):\n    \"\"\"训练单个折\"\"\"\n    print(f\"\\n{'='*60}\")\n    print(f\"🔄 开始训练第 {fold_num}/{total_folds} 折\")\n    print(f\"{'='*60}\")\n    \n    # 创建数据加载器\n    train_loader, val_loader, mixup_augmenter = create_fold_loaders(\n        sequences, labels, train_idx, val_idx, batch_size=batch_size,\n        use_mixup=use_mixup, mixup_params=mixup_params\n    )\n    \n    print(f\"📊 第{fold_num}折数据信息:\")\n    print(f\"   训练样本: {len(train_loader.dataset)}\")\n    print(f\"   验证样本: {len(val_loader.dataset)}\")\n    \n    # 检查subject分布\n    train_subjects_fold = [subjects[i] for i in train_idx]\n    val_subjects_fold = [subjects[i] for i in val_idx]\n    print(f\"   训练subjects: {len(set(train_subjects_fold))}\")\n    print(f\"   验证subjects: {len(set(val_subjects_fold))}\")\n    \n    # 创建模型\n    num_classes = len(LABEL_NAMES)\n    model = TwoBranchModel(\n        imu_dim=20,\n        tof_dim=len(FEATURE_NAMES) - 20,\n        n_classes=num_classes,\n        weight_decay=3e-3\n    )\n    \n    # 初始化权重\n    if hasattr(model, 'init_weights'):\n        model.init_weights()\n    \n    print(f\"✅ 模型参数量: {sum(p.numel() for p in model.parameters()):,}\")\n    \n    # 创建训练器\n    trainer = Trainer(model, device, LABEL_NAMES, label_smoothing=label_smoothing,)\n    \n    # 训练参数\n    model_save_path = f'{OUTPUT_DIR}/fold_{fold_num}_model.pth'\n    \n    # 训练\n    print(f\"🏋️ 开始第{fold_num}折训练...\")\n    train_losses, val_losses, train_accuracies, val_accuracies, val_scores = trainer.train(\n        train_loader, val_loader,\n        num_epochs=num_epochs,\n        learning_rate=learning_rate,\n        patience=patience,\n        save_path=model_save_path,\n        mixup_augmenter=mixup_augmenter\n    )\n    \n    print(f\"✅ 第{fold_num}折训练完成!\")\n    print(f\"📈 最终训练准确率: {train_accuracies[-1]:.4f}\")\n    print(f\"📊 最终验证准确率: {val_accuracies[-1]:.4f}\")\n    print(f\"📊 最终验证分数: {val_scores[-1]:.4f}\")\n\n    # 绘制训练历史\n    history_plot_path = f'{OUTPUT_DIR}/fold_{fold_num}_training_history.png'\n    trainer.plot_training_history(history_plot_path)\n    \n    # 加载最佳模型进行评估\n    checkpoint = torch.load(model_save_path, map_location=device, weights_only=False)\n    model.load_state_dict(checkpoint['model_state_dict'])\n    \n    # 最终评估\n    accuracy, score, report, predictions, probabilities = evaluate_model(\n        model, val_loader, device, LABEL_NAMES\n    )\n    \n    # 保存该折的结果\n    fold_results = {\n        'fold': fold_num,\n        'model_path': model_save_path,\n        'final_train_acc': train_accuracies[-1],\n        'final_val_acc': val_accuracies[-1],\n        'final_val_score': val_scores[-1],\n        'test_accuracy': accuracy,\n        'test_score': score,\n        'num_epochs': len(train_losses),\n        'train_subjects': len(set(train_subjects_fold)),\n        'val_subjects': len(set(val_subjects_fold))\n    }\n    \n    print(f\"🎯 第{fold_num}折最终评估:\")\n    print(f\"   测试准确率: {accuracy:.4f}\")\n    print(f\"   测试分数: {score:.4f}\")\n    \n    return fold_results\n\n\ndef run_kfold_training(train_df, n_folds=5, **kwargs):\n    \"\"\"运行K折交叉验证训练\"\"\"\n    print(\"🚀 使用K折交叉验证进行LSTM序列分类训练\")\n    print(\"=\" * 60)\n    \n    # 设置设备\n    device = 'cuda' if torch.cuda.is_available() else 'mps' if torch.backends.mps.is_available() else 'cpu'\n    print(f\"🧠 使用设备: {device}\")\n    \n    # 加载数据\n    print(\"📊 加载训练数据...\")\n    # train_df = pd.read_json(train_path, lines=True)\n    sequences = train_df['sequence'].tolist()\n    labels = train_df['label'].tolist()\n    subjects = train_df['subject'].tolist()\n    \n    num_classes = len(LABEL_NAMES)\n    print(f\"\\n📋 数据信息:\")\n    print(f\"   总样本数: {len(sequences)}\")\n    print(f\"   类别数量: {num_classes}\")\n    print(f\"   总subjects数: {len(set(subjects))}\")\n    \n    # 显示类别信息\n    print(f\"\\n🏷️  类别示例:\")\n    for i, class_name in enumerate(LABEL_NAMES[:8]):\n        print(f\"   {i}: {class_name}\")\n    if len(LABEL_NAMES) > 8:\n        print(f\"   ... 还有 {len(LABEL_NAMES) - 8} 个类别\")\n    \n    # 创建K折分割\n    print(f\"\\n📂 创建{n_folds}折交叉验证分割...\")\n    splits = create_kfold_splits(sequences, labels, subjects, n_splits=n_folds, random_state=42)\n    \n    # K折训练\n    fold_results = []\n    for fold_num, (train_idx, val_idx) in enumerate(splits, 1):\n        fold_result = train_single_fold(\n            sequences, labels, subjects, train_idx, val_idx,\n            fold_num, n_folds, device, **kwargs\n        )\n        fold_results.append(fold_result)\n    \n    # 计算总体统计\n    print(f\"\\n{'='*60}\")\n    print(f\"🎉 {n_folds}折交叉验证完成!\")\n    print(f\"{'='*60}\")\n    \n    val_accuracies = [r['final_val_acc'] for r in fold_results]\n    val_scores = [r['final_val_score'] for r in fold_results]\n    test_accuracies = [r['test_accuracy'] for r in fold_results]\n    test_scores = [r['test_score'] for r in fold_results]\n    \n    print(f\"\\n📊 K折交叉验证统计:\")\n    print(f\"   验证准确率: {np.mean(val_accuracies):.4f} ± {np.std(val_accuracies):.4f}\")\n    print(f\"   验证分数: {np.mean(val_scores):.4f} ± {np.std(val_scores):.4f}\")\n    print(f\"   测试准确率: {np.mean(test_accuracies):.4f} ± {np.std(test_accuracies):.4f}\")\n    print(f\"   测试分数: {np.mean(test_scores):.4f} ± {np.std(test_scores):.4f}\")\n    \n    # 显示每折结果\n    print(f\"\\n📋 各折详细结果:\")\n    for i, result in enumerate(fold_results):\n        print(f\"   Fold {i+1}: Val Acc={result['final_val_acc']:.4f}, \"\n              f\"Val Score={result['final_val_score']:.4f}, \"\n              f\"Test Acc={result['test_accuracy']:.4f}, \"\n              f\"Test Score={result['test_score']:.4f}\")\n    \n    # 保存K折结果\n    kfold_results = {\n        'n_folds': n_folds,\n        'fold_results': fold_results,\n        'summary': {\n            'val_acc_mean': np.mean(val_accuracies),\n            'val_acc_std': np.std(val_accuracies),\n            'val_score_mean': np.mean(val_scores),\n            'val_score_std': np.std(val_scores),\n            'test_acc_mean': np.mean(test_accuracies),\n            'test_acc_std': np.std(test_accuracies),\n            'test_score_mean': np.mean(test_scores),\n            'test_score_std': np.std(test_scores)\n        },\n    }\n    \n    with open(f'{OUTPUT_DIR}/kfold_results.json', 'w', encoding='utf-8') as f:\n        json.dump(kfold_results, f, ensure_ascii=False, indent=2)\n\n    print(f\"💾 K折结果已保存: kfold_results.json\")\n    \n    return kfold_results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:21:55.938229Z","iopub.execute_input":"2025-08-30T07:21:55.938784Z","iopub.status.idle":"2025-08-30T07:21:55.956347Z","shell.execute_reply.started":"2025-08-30T07:21:55.938759Z","shell.execute_reply":"2025-08-30T07:21:55.955331Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.manual_seed(42)\nnp.random.seed(42)\n\n# 训练参数\ntraining_params = {\n    'num_epochs': 1 if DEBUG else 100,\n    'learning_rate': 0.001,\n    'patience': 20,\n    'batch_size': 24,\n    'label_smoothing': 0.1,\n    'use_mixup': True,                  # 启用Mixup数据增强\n    'mixup_params': {                   # Mixup参数\n        'alpha': 0.2,                   # Beta分布参数，控制混合程度\n        'prob': 0.5                     # 应用Mixup的概率\n    }\n}\n\nif DEBUG:\n    agg_train_df = agg_train_df.head(2000)\n\n# K折交叉验证训练\nn_folds = 10\nprint(\"🔄 开始K折交叉验证训练\")\nprint(f\"   - Use Mixup: {training_params['use_mixup']}\")\nif training_params['use_mixup']:\n    print(f\"   - Mixup Alpha: {training_params['mixup_params']['alpha']}\")\n    print(f\"   - Mixup Probability: {training_params['mixup_params']['prob']}\")\n\nif TRAIN:\n    _ = run_kfold_training(agg_train_df, n_folds=n_folds, **training_params)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:27:52.407115Z","iopub.execute_input":"2025-08-30T07:27:52.407476Z","iopub.status.idle":"2025-08-30T07:29:22.409609Z","shell.execute_reply.started":"2025-08-30T07:27:52.407454Z","shell.execute_reply":"2025-08-30T07:29:22.408986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ls saved_models/","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:34.386866Z","iopub.execute_input":"2025-08-30T07:29:34.387151Z","iopub.status.idle":"2025-08-30T07:29:34.585490Z","shell.execute_reply.started":"2025-08-30T07:29:34.387129Z","shell.execute_reply":"2025-08-30T07:29:34.584689Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference","metadata":{}},{"cell_type":"code","source":"import os\n\ndef get_lastest_saved_models():\n    latest = 1\n    for version in os.listdir('/kaggle/input/cmi-imu-only-lstm-trained-output/pytorch/default/'):\n        try:\n            version = int(version)\n        except:\n            continue\n\n        if version > latest:\n            latest = version\n\n    return f'/kaggle/input/cmi-imu-only-lstm-trained-output/pytorch/default/{latest}/saved_models/'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:38.011392Z","iopub.execute_input":"2025-08-30T07:29:38.011689Z","iopub.status.idle":"2025-08-30T07:29:38.017116Z","shell.execute_reply.started":"2025-08-30T07:29:38.011663Z","shell.execute_reply":"2025-08-30T07:29:38.016325Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SAVED_MODELS = './saved_models/' if TRAIN else get_lastest_saved_models()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:39.375571Z","iopub.execute_input":"2025-08-30T07:29:39.375871Z","iopub.status.idle":"2025-08-30T07:29:39.379857Z","shell.execute_reply.started":"2025-08-30T07:29:39.375851Z","shell.execute_reply":"2025-08-30T07:29:39.379178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport pickle\nimport glob\nimport os\nfrom collections import Counter\nfrom typing import List, Dict\nimport polars as pl\n\nimport joblib","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:45.457131Z","iopub.execute_input":"2025-08-30T07:29:45.457501Z","iopub.status.idle":"2025-08-30T07:29:46.036573Z","shell.execute_reply.started":"2025-08-30T07:29:45.457478Z","shell.execute_reply":"2025-08-30T07:29:46.035999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class EnsemblePredictor:\n    \"\"\"集成预测器 - 使用多个fold模型投票预测\"\"\"\n    \n    def __init__(self, models_dir, device='cpu'):\n        \"\"\"\n        初始化集成预测器\n        \n        Args:\n            device: 计算设备\n        \"\"\"\n        self.device = device\n        self.models = []\n        self._load_models(models_dir)\n    \n    def _load_models(self, models_dir):\n        \"\"\"加载所有fold的模型\"\"\"\n        # 查找所有fold模型文件\n        model_files = sorted(glob.glob(f\"{models_dir}/fold_*_model.pth\"))\n        print(f\"📁 找到 {len(model_files)} 个fold模型\")\n        \n        # 加载每个fold的模型\n        for model_file in model_files:\n            checkpoint = torch.load(model_file, map_location=self.device, weights_only=False)\n            config = checkpoint['model_config']\n            \n            model = TwoBranchModel(**config)\n            \n            model.load_state_dict(checkpoint['model_state_dict'])\n            model.to(self.device)\n            model.eval()\n            self.models.append(model)\n            \n            print(f\"✅ 加载 {model_file}\")\n        \n    def predict(self, sequence: List[Dict]) -> str:\n        \"\"\"\n        预测单个序列的标签\n        \n        Args:\n            sequence: 输入序列，格式与训练数据相同\n        \n        Returns:\n            predicted_label: 预测的文本标签\n        \"\"\"\n        predictions = []\n        \n        # 使用每个fold的模型进行预测\n        for model in self.models:\n            # 使用SequenceDataset处理序列\n            dataset = SequenceDataset([sequence], [0], is_training=False, use_augmentation=False)\n            \n            # 获取处理后的序列\n            processed_sequence = torch.FloatTensor(dataset.processed_sequences[0]).unsqueeze(0).to(self.device)\n            \n            # 预测\n            with torch.no_grad():\n                outputs = model(processed_sequence)\n                predicted_class = torch.argmax(outputs, dim=1).item()\n                predictions.append(predicted_class)\n        \n        # 投票决定最终预测结果\n        most_common_prediction = Counter(predictions).most_common(1)[0][0]\n        \n        # 转换为文本标签\n        predicted_label = LABEL_NAMES[most_common_prediction]\n        \n        return predicted_label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:46.578685Z","iopub.execute_input":"2025-08-30T07:29:46.579233Z","iopub.status.idle":"2025-08-30T07:29:46.587095Z","shell.execute_reply.started":"2025-08-30T07:29:46.579211Z","shell.execute_reply":"2025-08-30T07:29:46.586187Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"feature_scaler = joblib.load(f'{SAVED_MODELS}/feature_scaler.joblib')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:49.359384Z","iopub.execute_input":"2025-08-30T07:29:49.359662Z","iopub.status.idle":"2025-08-30T07:29:49.364858Z","shell.execute_reply.started":"2025-08-30T07:29:49.359644Z","shell.execute_reply":"2025-08-30T07:29:49.364099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"predictor = EnsemblePredictor(SAVED_MODELS, device='cuda')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:50.172338Z","iopub.execute_input":"2025-08-30T07:29:50.172590Z","iopub.status.idle":"2025-08-30T07:29:50.865524Z","shell.execute_reply.started":"2025-08-30T07:29:50.172574Z","shell.execute_reply":"2025-08-30T07:29:50.864933Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    sequence = sequence.to_pandas()\n    demographics = demographics.to_pandas()\n\n    sequence = pd.merge(\n        sequence,\n        demographics,\n        on='subject',\n        how='left'\n    )\n\n    sequence = feature_engineering(sequence)\n\n    sequence[NUMERICAL_FEATURES] = feature_scaler.transform(sequence[NUMERICAL_FEATURES])\n    \n    sequence = sequence.groupby(['sequence_id', 'subject']).apply(\n        lambda df: df[FEATURE_NAMES].to_dict(orient='records'),\n        include_groups=False,\n    )\n    \n    sequence = sequence.iloc[0]\n\n    # predict\n    predicted_label = predictor.predict(sequence)\n\n    return predicted_label","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:56.593226Z","iopub.execute_input":"2025-08-30T07:29:56.594032Z","iopub.status.idle":"2025-08-30T07:29:56.598914Z","shell.execute_reply.started":"2025-08-30T07:29:56.594001Z","shell.execute_reply":"2025-08-30T07:29:56.598157Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## API","metadata":{}},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-30T07:29:58.687388Z","iopub.execute_input":"2025-08-30T07:29:58.687654Z","iopub.status.idle":"2025-08-30T07:30:00.729233Z","shell.execute_reply.started":"2025-08-30T07:29:58.687634Z","shell.execute_reply":"2025-08-30T07:30:00.728492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}