{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"e2a70827","cell_type":"code","source":"from functools import cached_property\nfrom pathlib import Path\nfrom scipy.spatial.transform import Rotation as R\nfrom sklearn.metrics import f1_score\nfrom sklearn.model_selection import KFold\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom torch.nn.utils.rnn import pad_sequence\nfrom torch.optim import Adam, AdamW\nfrom torch.optim.lr_scheduler import CosineAnnealingLR\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom tqdm.auto import tqdm\nimport gc\nimport glob\nimport logging\nimport math\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport os\nimport pandas as pd\nimport pickle\nimport polars as pl\nimport random\nimport threading\nimport time\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport warnings","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:25:56.737728Z","iopub.execute_input":"2025-08-11T04:25:56.737903Z","iopub.status.idle":"2025-08-11T04:26:06.132242Z","shell.execute_reply.started":"2025-08-11T04:25:56.737886Z","shell.execute_reply":"2025-08-11T04:26:06.131447Z"}},"outputs":[],"execution_count":null},{"id":"ecf2ac92-d068-48d9-a6de-7de0d3ef0ba7","cell_type":"code","source":"%cd /kaggle/input/cmi-detect-behavior-with-sensor-data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:06.135993Z","iopub.execute_input":"2025-08-11T04:26:06.136611Z","iopub.status.idle":"2025-08-11T04:26:06.144417Z","shell.execute_reply.started":"2025-08-11T04:26:06.136590Z","shell.execute_reply":"2025-08-11T04:26:06.143704Z"}},"outputs":[],"execution_count":null},{"id":"4ffd31df","cell_type":"code","source":"pd.set_option('display.width', 1000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:08.121185Z","iopub.execute_input":"2025-08-11T04:26:08.121465Z","iopub.status.idle":"2025-08-11T04:26:08.125463Z","shell.execute_reply.started":"2025-08-11T04:26:08.121444Z","shell.execute_reply":"2025-08-11T04:26:08.124719Z"}},"outputs":[],"execution_count":null},{"id":"8ecf255a","cell_type":"code","source":"df = pd.read_csv('train.csv', index_col=0)\ndf = df.reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:08.407538Z","iopub.execute_input":"2025-08-11T04:26:08.407814Z","iopub.status.idle":"2025-08-11T04:26:37.932326Z","shell.execute_reply.started":"2025-08-11T04:26:08.407790Z","shell.execute_reply":"2025-08-11T04:26:37.931692Z"}},"outputs":[],"execution_count":null},{"id":"c1df4ee0","cell_type":"code","source":"df['sequence_id'].nunique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:37.933494Z","iopub.execute_input":"2025-08-11T04:26:37.933709Z","iopub.status.idle":"2025-08-11T04:26:37.966169Z","shell.execute_reply.started":"2025-08-11T04:26:37.933692Z","shell.execute_reply":"2025-08-11T04:26:37.965569Z"}},"outputs":[],"execution_count":null},{"id":"2a620fb7","cell_type":"code","source":"# aggregate IMU features\nacc_features = ['acc_x', 'acc_y', 'acc_z']\nrot_features = ['rot_w', 'rot_x', 'rot_y', 'rot_z']\nimu_features = acc_features + rot_features\n\ny_feature = 'gesture'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:37.966802Z","iopub.execute_input":"2025-08-11T04:26:37.967004Z","iopub.status.idle":"2025-08-11T04:26:37.971187Z","shell.execute_reply.started":"2025-08-11T04:26:37.966987Z","shell.execute_reply":"2025-08-11T04:26:37.970411Z"}},"outputs":[],"execution_count":null},{"id":"7af8a202","cell_type":"code","source":"print(df[df['sequence_id'] == 'SEQ_000007'][imu_features + [y_feature]].head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:37.972831Z","iopub.execute_input":"2025-08-11T04:26:37.973021Z","iopub.status.idle":"2025-08-11T04:26:38.035285Z","shell.execute_reply.started":"2025-08-11T04:26:37.973005Z","shell.execute_reply":"2025-08-11T04:26:38.034481Z"}},"outputs":[],"execution_count":null},{"id":"325ac552","cell_type":"code","source":"label_encoder = LabelEncoder()\n_ = label_encoder.fit(df[y_feature])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:38.035947Z","iopub.execute_input":"2025-08-11T04:26:38.036179Z","iopub.status.idle":"2025-08-11T04:26:38.056343Z","shell.execute_reply.started":"2025-08-11T04:26:38.036161Z","shell.execute_reply":"2025-08-11T04:26:38.055771Z"}},"outputs":[],"execution_count":null},{"id":"00300254","cell_type":"code","source":"def ffill(arr):\n    mask = np.isnan(arr)\n    idx = np.where(~mask, np.arange(mask.shape[0])[:, None], 0)\n    np.maximum.accumulate(idx, axis=0, out=idx)\n    return arr[idx, np.arange(idx.shape[1])]\n\ndef bfill(arr): \n    return ffill(arr[::-1, :])[::-1, :]\n\ndef _replace_nans(seq):\n    arr = bfill(ffill(seq))\n    return np.where(np.isnan(arr), 0, arr)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:38.057002Z","iopub.execute_input":"2025-08-11T04:26:38.057691Z","iopub.status.idle":"2025-08-11T04:26:38.070806Z","shell.execute_reply.started":"2025-08-11T04:26:38.057668Z","shell.execute_reply":"2025-08-11T04:26:38.070041Z"}},"outputs":[],"execution_count":null},{"id":"c0f1750c","cell_type":"code","source":"cache = Path('/kaggle/working')\nimu_features_dir = cache / 'imu_features'\nif not imu_features_dir.exists():\n    grouped = df.groupby('sequence_id')\n    imu_features_dir.mkdir(parents=True, exist_ok=True)\n    # write IMU features to a file\n    overwrite = False\n    for seq_id, seq in tqdm(grouped):\n        seq = seq[imu_features].values\n        output_file = imu_features_dir / f\"{seq_id}.npy\"\n        if overwrite or not output_file.exists():\n            with open(output_file, \"wb\") as f:\n                np.save(f, _replace_nans(seq))\n\n    y_values = grouped[y_feature].first().values\n    y_values = label_encoder.transform(y_values)\n    gestures_file = cache / 'gestures.npy'\n    np.save(gestures_file, y_values)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:38.071463Z","iopub.execute_input":"2025-08-11T04:26:38.071629Z","iopub.status.idle":"2025-08-11T04:26:43.936417Z","shell.execute_reply.started":"2025-08-11T04:26:38.071610Z","shell.execute_reply":"2025-08-11T04:26:43.935694Z"}},"outputs":[],"execution_count":null},{"id":"52573810","cell_type":"code","source":"# --- Feature Engineering ---\ndef _remove_gravity_from_acc(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\ndef _calculate_angular_velocity_from_quat(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\ndef _calculate_angular_distance(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\ndef _imu_feature_engineering(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, Calculate acceleration magnitude\n    rot_angle = 2 * np.arccos(rot[:,0].clip(-1, 1))                                        # 1, Calculate quaternion rotation angle (radians)\n    acc_mag_jerk = np.diff(acc_mag, prepend=acc_mag[0])                                    # 1, Calculate jerk of acceleration magnitude (difference, prepend first element)\n    rot_angle_vel = np.diff(rot_angle, prepend=rot_angle[0])                               # 1, Calculate angular velocity of rotation angle (difference, prepend first element)\n    linear_acc = _remove_gravity_from_acc(acc, rot[:, [1, 2, 3, 0]])                  # 3, Linear acceleration after removing gravity (input quaternion format is [x, y, z, w])\n    linear_acc_mag = np.sqrt(linear_acc[:,0]**2 + linear_acc[:,1]**2 + linear_acc[:,2]**2) # 1, Linear acceleration magnitude\n    linear_acc_mag_jerk = np.diff(linear_acc_mag, prepend=linear_acc_mag[0])               # 1, Jerk of linear acceleration magnitude (difference, prepend first element)\n    angular_vel = _calculate_angular_velocity_from_quat(rot[:, [1, 2, 3, 0]])         # 3, Calculate angular velocity (input quaternion format is [x, y, z, w])\n    angular_distance = _calculate_angular_distance(rot[:, [1, 2, 3, 0]])              # 1, Calculate angular distance between adjacent frames (radians)\n    # Concatenate all features\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    return new_imu\n\n# this works even if we only have IMU features (the first 7 columns)\ndef _feature_engineering(seq):\n    imu = seq[:,:7]\n    new_imu = _imu_feature_engineering(imu)  # Apply IMU feature engineering\n    other_features = seq[:, 7:]  # Other features besides IMU\n    thm = other_features[:, :5]         # Temperature features\n    tof = other_features[:, 5:5+320]    # TOF features\n    demo = other_features[:, 5+320:]    # Demographic features\n    # Concatenate all features: IMU(20) + Temperature(5) + Demographic(7) + TOF(320)\n    features = np.concatenate([new_imu, thm, demo, tof], axis=1)\n    return features\n\n\n# --- Data Augmentation Methods (applied to normalized data) ---\ndef _jitter(sequence, sigma=0.1):\n    return sequence + np.random.normal(loc=0., scale=sigma, size=sequence.shape)\n\ndef _time_mask(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\ndef _feature_mask(sequence, max_mask_size=0.5):\n    num_features = sequence.shape[1]\n    mask_size = np.random.randint(1, int(num_features * max_mask_size) + 1)\n    masked_features = np.random.choice(num_features, mask_size, replace=False)\n    sequence[:, masked_features] = 0\n    return sequence\n\ndef _motion_drift(imu_features: np.ndarray, drift_std, drift_max) -> np.ndarray:\n    \"\"\"Simulate sensor drift on IMU features.\"\"\"\n    T = imu_features.shape[0]\n    # Generate drift signal\n    drift = np.cumsum(np.random.normal(scale=drift_std, size=(T, 1)), axis=0)\n    drift = np.clip(drift, -drift_max, drift_max)\n    # Apply drift to features related to acceleration and angular velocity\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\ndef _apply_augmentations(sequence, *, drift_std, drift_max, time_masking):\n    \"\"\"Apply augmentations to the normalized sequence.\"\"\"\n    # Augmentation 1: Jitter\n    if np.random.rand() < 0.7:\n        sequence = _jitter(sequence, sigma=0.05)\n    # Augmentation 2 & 3: Time and Feature Masking\n    if time_masking and np.random.rand() < 0.5:\n        sequence = _time_mask(sequence, max_mask_size=20)\n    if np.random.rand() < 0.5:\n        sequence = _feature_mask(sequence)\n    # Augmentation 4: Motion Drift (only applied to the IMU part)\n    if np.random.rand() < 0.5:\n        imu_features = sequence[:, :20]\n        other_features = sequence[:, 20:]\n        augmented_imu = _motion_drift(imu_features, drift_std=drift_std, drift_max=drift_max)\n        sequence = np.concatenate([augmented_imu, other_features], axis=1)\n    return sequence","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:43.937201Z","iopub.execute_input":"2025-08-11T04:26:43.937424Z","iopub.status.idle":"2025-08-11T04:26:43.955288Z","shell.execute_reply.started":"2025-08-11T04:26:43.937400Z","shell.execute_reply":"2025-08-11T04:26:43.954547Z"}},"outputs":[],"execution_count":null},{"id":"f337b8a9","cell_type":"code","source":"class IMUDataset(Dataset):\n    def __init__(self, sequence_dir, max_len=256, transform=None):\n        self.sequence_dir = Path(sequence_dir)\n        self.sequence_ids = sorted([seq_id.stem for seq_id in self.sequence_dir.glob(\"*.npy\")])\n        self.max_len = max_len\n        self.transform = transform\n\n    def set_mask_ratio(self, mask_ratio):\n        self.mask_ratio = mask_ratio\n\n    def __len__(self):\n        return len(self.sequence_ids)\n\n    @cached_property\n    def labels(self):\n        gestures_file = cache / 'gestures.npy'\n        return np.load(gestures_file, allow_pickle=True)\n\n    def __getitem__(self, idx):\n        sequence_file = self.sequence_dir / f\"{self.sequence_ids[idx]}.npy\"\n        seq = np.load(sequence_file, allow_pickle=True)\n\n        label = self.labels[idx]\n\n        output = {\"inputs\": seq, \"label\": label}\n        if self.transform:\n            output = self.transform(output)\n        \n        return output\n\nds = IMUDataset(imu_features_dir)\nds[0]['inputs'].shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:43.956107Z","iopub.execute_input":"2025-08-11T04:26:43.956345Z","iopub.status.idle":"2025-08-11T04:26:44.011834Z","shell.execute_reply.started":"2025-08-11T04:26:43.956320Z","shell.execute_reply":"2025-08-11T04:26:44.011021Z"}},"outputs":[],"execution_count":null},{"id":"54a27f86","cell_type":"code","source":"imu_features_engineered_dir = cache / 'imu_features_engineered'\nif not imu_features_engineered_dir.exists():\n    engineered = [_feature_engineering(ds[i]['inputs']) for i in tqdm(range(len(ds)))]\n    imu_features_engineered_dir.mkdir(parents=True, exist_ok=True)\n    for seq_id, seq in tqdm(zip(ds.sequence_ids, engineered), total=len(ds)):\n        output_file = imu_features_engineered_dir / f\"{seq_id}.npy\"\n        if not output_file.exists():\n            with open(output_file, \"wb\") as f:\n                np.save(f, seq)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:26:44.013751Z","iopub.execute_input":"2025-08-11T04:26:44.013946Z","iopub.status.idle":"2025-08-11T04:29:31.717475Z","shell.execute_reply.started":"2025-08-11T04:26:44.013931Z","shell.execute_reply":"2025-08-11T04:29:31.716790Z"}},"outputs":[],"execution_count":null},{"id":"30113d7e","cell_type":"code","source":"ds = IMUDataset(imu_features_engineered_dir)\nds[0]['inputs'].shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:31.718263Z","iopub.execute_input":"2025-08-11T04:29:31.718515Z","iopub.status.idle":"2025-08-11T04:29:31.743560Z","shell.execute_reply.started":"2025-08-11T04:29:31.718493Z","shell.execute_reply":"2025-08-11T04:29:31.742983Z"}},"outputs":[],"execution_count":null},{"id":"0e0cb71d","cell_type":"code","source":"# split dataset into train and validation\ntrain_indices, val_indices = train_test_split(range(len(ds)), test_size=0.1, random_state=42, stratify=ds.labels)\nlen(train_indices), len(val_indices)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:31.744221Z","iopub.execute_input":"2025-08-11T04:29:31.744716Z","iopub.status.idle":"2025-08-11T04:29:31.755576Z","shell.execute_reply.started":"2025-08-11T04:29:31.744696Z","shell.execute_reply":"2025-08-11T04:29:31.754904Z"}},"outputs":[],"execution_count":null},{"id":"70526a92","cell_type":"code","source":"scaler = StandardScaler()\ntrain_sequences = np.vstack([ds[i]['inputs'] for i in train_indices])\nassert not np.isnan(train_sequences).any(), \"There should be no NaNs in the training sequences\"\n_ = scaler.fit(train_sequences)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:31.756520Z","iopub.execute_input":"2025-08-11T04:29:31.757319Z","iopub.status.idle":"2025-08-11T04:29:32.781010Z","shell.execute_reply.started":"2025-08-11T04:29:31.757291Z","shell.execute_reply":"2025-08-11T04:29:32.780133Z"}},"outputs":[],"execution_count":null},{"id":"9a950fb3","cell_type":"code","source":"class CoordAttention(nn.Module):\n    \"\"\"\n    Coordinate Attention for Sequences.\n    Input Dimension: (B, C, T)\n    Output Dimension: (B, C, T)\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\n    def forward(self, x):\n        # Input x is expected to be (B, C, T)\n        \n        # Squeeze features\n        f = self.compression(x)  # (B, rC, T)\n        \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))  # Stays (B, 1, T)\n        \n        # Channel Attention (B, C, 1)\n        f_c = f.mean(dim=2, keepdim=True) # (B, rC, 1)\n        channel_attn = self.sigmoid(self.channel_conv(f_c)) # (B, C, 1)\n        \n        # Apply attention and return\n        out = x * time_attn * channel_attn\n        return out\n\n\nclass ResidualCNNBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size, attention, reduction=8, pool_size=2, dropout=0.3):\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 = attention(out_channels, reduction) # 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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:32.781830Z","iopub.execute_input":"2025-08-11T04:29:32.782144Z","iopub.status.idle":"2025-08-11T04:29:32.794485Z","shell.execute_reply.started":"2025-08-11T04:29:32.782118Z","shell.execute_reply":"2025-08-11T04:29:32.793632Z"}},"outputs":[],"execution_count":null},{"id":"7da9c4a1","cell_type":"code","source":"class Collator:\n    def __call__(self, batch):\n        inputs = [item['inputs'] for item in batch]\n        labels = [item['label'] for item in batch]\n        inputs = pad_sequence(inputs, batch_first=True)\n        labels = torch.tensor(labels, dtype=torch.long)\n\n        return {\"input\": inputs, \"label\": labels}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:32.795304Z","iopub.execute_input":"2025-08-11T04:29:32.795634Z","iopub.status.idle":"2025-08-11T04:29:32.813697Z","shell.execute_reply.started":"2025-08-11T04:29:32.795611Z","shell.execute_reply":"2025-08-11T04:29:32.813132Z"}},"outputs":[],"execution_count":null},{"id":"6e02edba","cell_type":"code","source":"n_features = ds[0]['inputs'].shape[1]\n\nclass ScaleInputs:\n    def __init__(self, scaler):\n        self.scaler = scaler\n\n    def __call__(self, sample):\n        n_features = self.scaler.n_features_in_\n        sample[\"inputs\"][:, :n_features] = self.scaler.transform(sample[\"inputs\"][:, :n_features])\n        return sample\n\n\nclass AugmentForFinetune:\n    def __init__(self, drift_std=0.1, drift_max=0.5, time_masking=True):\n        self.drift_std = drift_std\n        self.drift_max = drift_max\n        self.time_masking = time_masking\n\n    def __call__(self, sample):\n        sample[\"inputs\"] = _apply_augmentations(\n            sample[\"inputs\"],\n            drift_std=self.drift_std,\n            drift_max=self.drift_max,\n            time_masking=self.time_masking,\n        )\n        return sample\n\nclass ToTensor:\n    def __call__(self, sample):\n        sample[\"inputs\"] = torch.from_numpy(sample[\"inputs\"]).float()\n        sample[\"label\"] = torch.tensor(sample[\"label\"], dtype=torch.long)\n        return sample\n\nclass TfDataset(Dataset):\n    def __init__(self, ds, indices, transform=None):\n        self.ds = ds\n        self.transform = transform\n        self.indices = indices\n\n    def __len__(self):\n        return len(self.indices)\n\n    def __getitem__(self, idx):\n        sample = self.ds[self.indices[idx]]\n        if self.transform:\n            sample = self.transform(sample)\n        return sample","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:32.814483Z","iopub.execute_input":"2025-08-11T04:29:32.815119Z","iopub.status.idle":"2025-08-11T04:29:32.828444Z","shell.execute_reply.started":"2025-08-11T04:29:32.815069Z","shell.execute_reply":"2025-08-11T04:29:32.827820Z"}},"outputs":[],"execution_count":null},{"id":"5d47fd74","cell_type":"code","source":"finetune_train_transform = transforms.Compose([\n    ScaleInputs(scaler),\n    AugmentForFinetune(drift_std=0.01, drift_max=0.5, time_masking=True),\n    ToTensor(),\n])\nfinetune_val_transform = transforms.Compose([\n    ScaleInputs(scaler),\n    ToTensor(),\n])\n\nft_train_ds = TfDataset(ds, train_indices, transform=finetune_train_transform)\nft_val_ds = TfDataset(ds, val_indices, transform=finetune_val_transform)\nft_train_ds[0]['inputs'].shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:29:32.829487Z","iopub.execute_input":"2025-08-11T04:29:32.829893Z","iopub.status.idle":"2025-08-11T04:29:32.847240Z","shell.execute_reply.started":"2025-08-11T04:29:32.829874Z","shell.execute_reply":"2025-08-11T04:29:32.846656Z"}},"outputs":[],"execution_count":null},{"id":"f54781b7","cell_type":"code","source":"class 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\nclass IMUOnlyModel(nn.Module):\n    def __init__(self, imu_dim, n_classes):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.n_classes = n_classes\n        # IMU deep branch\n        self.imu_block1 = ResidualCNNBlock(imu_dim, 64, 3, dropout=0.3, attention=CoordAttention)\n        self.imu_block2 = ResidualCNNBlock(64, 128, 5, dropout=0.3, attention=CoordAttention)\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):\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-08-11T04:29:32.847973Z","iopub.execute_input":"2025-08-11T04:29:32.848284Z","iopub.status.idle":"2025-08-11T04:29:32.863533Z","shell.execute_reply.started":"2025-08-11T04:29:32.848229Z","shell.execute_reply":"2025-08-11T04:29:32.862859Z"}},"outputs":[],"execution_count":null},{"id":"2b76e5c6","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-08-11T04:29:32.864339Z","iopub.execute_input":"2025-08-11T04:29:32.864585Z","iopub.status.idle":"2025-08-11T04:29:32.880466Z","shell.execute_reply.started":"2025-08-11T04:29:32.864567Z","shell.execute_reply":"2025-08-11T04:29:32.879776Z"}},"outputs":[],"execution_count":null},{"id":"9aa7415d","cell_type":"code","source":"EPOCHS = 50\nLR = 1e-3\nLR_MIN = 1e-6\nBATCH_SIZE = 32\n\nn_classes = len(label_encoder.classes_)\ndevice = 'cuda'\n\nloader_train = DataLoader(ft_train_ds, batch_size=BATCH_SIZE, shuffle=True, collate_fn=Collator())\nloader_val = DataLoader(ft_val_ds, batch_size=BATCH_SIZE, shuffle=True, collate_fn=Collator())\n\nmodel = IMUOnlyModel(imu_dim=ft_train_ds[0]['inputs'].size(1), n_classes=n_classes).to(device)\n\ncriterion = nn.CrossEntropyLoss().to(device)\noptimizer = Adam(model.parameters(), lr=LR, weight_decay=1e-4)\n\nnum_training_steps = EPOCHS * len(loader_train)\n\ntrain_losses = []\nval_losses = []\ntrain_accs = []\nval_accs = []\n\nwarmup_epochs = 3\nlabel_smoothing = 0.1\nuse_label_smoothing = label_smoothing > 0\n\n# --- Training Loop ---\nfor epoch in range(EPOCHS):\n    model.train()\n    epoch_train_loss = 0.\n    epoch_train_acc = 0.\n    progress_bar = tqdm(loader_train, desc=f\"Epoch {epoch+1}/{EPOCHS}\", leave=False)\n    for batch_idx, batch in enumerate(progress_bar):\n        base_lr = optimizer.defaults['lr']\n        eta_min = LR_MIN\n\n        current_iter = epoch * len(loader_train) + batch_idx\n        current_epoch = current_iter / len(loader_train)  # Convert to decimal epoch format\n        # Phase 1: Linear warmup\n        if current_epoch < warmup_epochs:\n            # Linear warmup: increase from 0.001 * base_lr to base_lr\n            lr = base_lr * (0.001 + (current_epoch / warmup_epochs) * 0.999)\n        # Phase 2: Cosine annealing\n        else:\n            # Cosine annealing formula\n            cos_factor = 0.5 * (1 + math.cos(math.pi * (current_epoch - warmup_epochs) / (EPOCHS - warmup_epochs)))\n            lr = eta_min + (base_lr - eta_min) * cos_factor\n        # Apply calculated learning rate\n        for param_group in optimizer.param_groups:\n            param_group['lr'] = lr\n\n        inputs = batch[\"input\"].to(device)\n        labels = batch[\"label\"].to(device)\n\n        optimizer.zero_grad()\n\n        output = model(inputs)\n        loss = criterion(output, labels)\n\n        if use_label_smoothing:\n            # Convert hard labels to soft labels\n            soft_labels = apply_label_smoothing(labels, n_classes, label_smoothing)\n            # Use cross-entropy loss function (for soft labels, usually use log_softmax + sum directly)\n            loss = torch.nn.functional.kl_div(\n                torch.nn.functional.log_softmax(output, dim=1),\n                soft_labels,\n                reduction='batchmean'\n            )\n        else:\n            # Use original loss function\n            loss = criterion(output, labels)\n\n        acc = (output.argmax(dim=1) == labels).float().mean()\n\n        epoch_train_loss += loss.item()\n        epoch_train_acc += acc.item()\n\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n\n        progress_bar.set_postfix(loss=f\"{loss.item():.4f}\", acc=f\"{acc.item():.4f}\", lr=f\"{lr:.6f}\")\n\n    model.eval()\n    epoch_val_loss = 0.\n    epoch_val_acc = 0.\n    progress_bar = tqdm(loader_val, desc=f\"Epoch (validation) {epoch+1}/{EPOCHS}\", leave=False)\n    for batch in progress_bar:\n        inputs = batch[\"input\"].to(device)\n        labels = batch[\"label\"].to(device)\n\n        with torch.no_grad():\n            output = model(inputs)\n            val_loss = criterion(output, labels)\n            val_acc = (output.argmax(dim=1) == labels).float().mean()\n\n        epoch_val_loss += val_loss.item()\n        epoch_val_acc += val_acc.item()\n        progress_bar.set_postfix(val_loss=f\"{val_loss.item():.4f}, val_acc={val_acc.item():.4f}\")\n\n    epoch_train_loss /= len(loader_train)\n    epoch_train_acc /= len(loader_train)\n    epoch_val_loss /= len(loader_val)\n    epoch_val_acc /= len(loader_val)\n    train_losses.append(epoch_train_loss)\n    train_accs.append(epoch_train_acc)\n    val_losses.append(epoch_val_loss)\n    val_accs.append(epoch_val_acc)\n    print(f\"Epoch {epoch+1} - Train Loss: {epoch_train_loss:.4f}, Train Acc: {epoch_train_acc:.4f}, Val Loss: {epoch_val_loss:.4f}, Val Acc: {epoch_val_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:33:39.143811Z","iopub.execute_input":"2025-08-11T04:33:39.144115Z","iopub.status.idle":"2025-08-11T04:39:19.253390Z","shell.execute_reply.started":"2025-08-11T04:33:39.144092Z","shell.execute_reply":"2025-08-11T04:39:19.252593Z"}},"outputs":[],"execution_count":null},{"id":"627e1c9e","cell_type":"code","source":"fig, ax = plt.subplots(figsize=(10, 5), nrows=2, sharex=True)\n\nax[0].plot(train_losses, label='Train Loss', color='blue')\nax[1].plot(train_accs, label='Train Accuracy', color='blue')\nax[0].plot(val_losses, label='Validation Loss', color='orange')\nax[1].plot(val_accs, label='Validation Accuracy', color='orange')\nax[0].set_ylabel('Loss')\nax[1].set_ylabel('Accuracy')\nax[1].set_xlabel('Epoch')\nax[0].legend()\nax[1].legend()\nax[0].grid()\nax[1].grid()\nplt.tight_layout()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:39:27.892107Z","iopub.execute_input":"2025-08-11T04:39:27.892376Z","iopub.status.idle":"2025-08-11T04:39:28.287596Z","shell.execute_reply.started":"2025-08-11T04:39:27.892358Z","shell.execute_reply":"2025-08-11T04:39:28.286872Z"}},"outputs":[],"execution_count":null},{"id":"e641c716-7923-4751-939e-2b888c62c6da","cell_type":"code","source":"class ParticipantVisibleError(Exception):\n    \"\"\"Errors raised here will be shown directly to the competitor.\"\"\"\n    pass\n\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\n    def calculate_hierarchical_f1(\n        self,\n        sol: pd.Series,\n        sub: pd.Series\n    ) -> float:\n        invalid_types = {i for i in sub.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.isin(self.target_gestures).values\n        y_pred_bin = sub.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.apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub.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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:39:59.722317Z","iopub.execute_input":"2025-08-11T04:39:59.722592Z","iopub.status.idle":"2025-08-11T04:39:59.729895Z","shell.execute_reply.started":"2025-08-11T04:39:59.722572Z","shell.execute_reply":"2025-08-11T04:39:59.729030Z"}},"outputs":[],"execution_count":null},{"id":"5b803737-7fc0-4777-8fbf-acda4f3cad86","cell_type":"code","source":"model.eval()\nall_out_labels = []\ngt_labels = []\nprogress_bar = tqdm(loader_val, desc=f\"Epoch (validation) {epoch+1}/{EPOCHS}\", leave=False)\nfor batch in progress_bar:\n    inputs = batch[\"input\"].to(device)\n    labels = batch[\"label\"].to(device)\n\n    with torch.no_grad():\n        output = model(inputs)\n        out_labels = output.argmax(dim=1)\n\n    all_out_labels.extend([i.item() for i in out_labels])\n    gt_labels.extend([i.item() for i in labels])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:40:19.395745Z","iopub.execute_input":"2025-08-11T04:40:19.396023Z","iopub.status.idle":"2025-08-11T04:40:19.805705Z","shell.execute_reply.started":"2025-08-11T04:40:19.396002Z","shell.execute_reply":"2025-08-11T04:40:19.804913Z"}},"outputs":[],"execution_count":null},{"id":"335f3aad-41c7-4b50-b47b-036b55e7746b","cell_type":"code","source":"metric = CompetitionMetric()\nall_out_labels = pd.Series(all_out_labels, name='gesture').map(lambda x: label_encoder.inverse_transform([x])[0])\ngt_labels = pd.Series(gt_labels, name='gesture_gt').map(lambda x: label_encoder.inverse_transform([x])[0])\nhierarchical_f1, f1_binary, f1_macro = metric.calculate_hierarchical_f1(gt_labels, all_out_labels)\nprint(f\"Hierarchical F1: {hierarchical_f1:.4f}, Binary F1: {f1_binary:.4f}, Macro F1: {f1_macro:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-11T04:40:24.161314Z","iopub.execute_input":"2025-08-11T04:40:24.161583Z","iopub.status.idle":"2025-08-11T04:40:24.321194Z","shell.execute_reply.started":"2025-08-11T04:40:24.161561Z","shell.execute_reply":"2025-08-11T04:40:24.320295Z"}},"outputs":[],"execution_count":null},{"id":"5b6b13b0-d55a-4bca-903d-6f9eaf9c3ed5","cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}