{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"The approach:\n* Only using the IMU data for now\n* Stratified group k fold for validation split\n* Imputing the missing rotation data with identity quaternions\n* Adding extra features like: gravity removed acceleration, angular velocity, acceleration magnitude, etc.\n* Z-score normalization for all features\n* Equalizing the sequences to a length of 128 by padding or truncating\n* Modelling with 1d CNN layers (with residual squeeze excitation blocks) followed by a bidirectional GRU layer\n* Augmenting with MixUp\n* Label smoothing and 1cycle training for extra stability","metadata":{}},{"cell_type":"markdown","source":"Check out the inference notebook here: https://www.kaggle.com/code/utkarsh1232/lb-0-75-imu-only-inference-with-fastai","metadata":{}},{"cell_type":"code","source":"from collections import Counter\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.preprocessing import StandardScaler\nfrom scipy.spatial.transform import Rotation as R\nfrom sklearn.metrics import f1_score, ConfusionMatrixDisplay\nfrom fastai.text.all import *","metadata":{"_uuid":"f78bc327-fe72-4bbf-a467-bdabb2770e8a","_cell_guid":"f49ba662-5b5d-48ba-ae90-30b5f2ad47d5","trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:40:41.832832Z","iopub.execute_input":"2025-09-02T20:40:41.833050Z","iopub.status.idle":"2025-09-02T20:40:47.327759Z","shell.execute_reply.started":"2025-09-02T20:40:41.833032Z","shell.execute_reply":"2025-09-02T20:40:47.327019Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Analysis","metadata":{"_uuid":"886a594a-b1a4-41cd-978d-437b742c0be7","_cell_guid":"ac182b0b-5f14-4454-a467-e2b1110da742","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"data_folder = Path('/kaggle/input/cmi-detect-behavior-with-sensor-data')\ndf = pd.read_csv(data_folder/'train.csv')\ndemo_df = pd.read_csv(data_folder/'train_demographics.csv')\nprint(df.shape)\ndf.head(2)","metadata":{"_uuid":"77bc2a3c-2581-4aec-acba-1ae3cb457cad","_cell_guid":"3d5184fc-f464-43b2-8cce-8f4d00151e31","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:40:47.329559Z","iopub.execute_input":"2025-09-02T20:40:47.329885Z","iopub.status.idle":"2025-09-02T20:41:19.107624Z","shell.execute_reply.started":"2025-09-02T20:40:47.329867Z","shell.execute_reply":"2025-09-02T20:41:19.106778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"acc_cols = ['acc_x','acc_y','acc_z']\nrot_cols = ['rot_w','rot_x','rot_y','rot_z']\nimu_cols = acc_cols + rot_cols\nthm_cols = [f'thm_{i+1}' for i in range(5)]\ntof_cols = [f'tof_{i+1}_v{j}' for i in range(5) for j in range(64)]","metadata":{"_uuid":"467f7da2-a8c9-4ed2-b5cb-c091f50d9b19","_cell_guid":"f659772a-ae35-4cd5-b291-3be5bba5c379","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:41:19.108448Z","iopub.execute_input":"2025-09-02T20:41:19.108722Z","iopub.status.idle":"2025-09-02T20:41:19.113758Z","shell.execute_reply.started":"2025-09-02T20:41:19.108698Z","shell.execute_reply":"2025-09-02T20:41:19.112882Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Nature of missing values","metadata":{}},{"cell_type":"code","source":"df[imu_cols].isnull().sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:19.114510Z","iopub.execute_input":"2025-09-02T20:41:19.114772Z","iopub.status.idle":"2025-09-02T20:41:19.172560Z","shell.execute_reply.started":"2025-09-02T20:41:19.114756Z","shell.execute_reply":"2025-09-02T20:41:19.171873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# checking whether all the components of the quaternion are missing for the same timesteps\ndf[imu_cols].isnull().sum(axis=1).value_counts()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:19.173397Z","iopub.execute_input":"2025-09-02T20:41:19.173661Z","iopub.status.idle":"2025-09-02T20:41:19.266054Z","shell.execute_reply.started":"2025-09-02T20:41:19.173641Z","shell.execute_reply":"2025-09-02T20:41:19.265285Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# checking which sequences have only some timesteps missing\nseqs = df.groupby('sequence_id')\nfor seq_id, seq in seqs:\n    x = seq[rot_cols[0]].isnull().values\n    if x.any()!=x.all():\n        print(seq_id)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:19.266877Z","iopub.execute_input":"2025-09-02T20:41:19.267165Z","iopub.status.idle":"2025-09-02T20:41:20.536966Z","shell.execute_reply.started":"2025-09-02T20:41:19.267141Z","shell.execute_reply":"2025-09-02T20:41:20.536227Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Notes:\n\n* All acceleration values are present\n* Some sequences in the training data have all components of quaternion data missing across all timesteps\n* There is no sequence that has only a few timesteps of quaternion data missing","metadata":{}},{"cell_type":"markdown","source":"## Visualizing the sequences","metadata":{}},{"cell_type":"code","source":"df['imu'] = df[imu_cols].to_numpy().tolist()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:20.539620Z","iopub.execute_input":"2025-09-02T20:41:20.539854Z","iopub.status.idle":"2025-09-02T20:41:21.748642Z","shell.execute_reply.started":"2025-09-02T20:41:20.539837Z","shell.execute_reply":"2025-09-02T20:41:21.747839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"seq_df = df.groupby('sequence_id', as_index=False).agg(\n    subject = ('subject','first'),\n    orientation = ('orientation', 'first'),\n    imu = ('imu', list),\n    seq_len = ('sequence_counter', lambda x: x.max()+1),\n    gesture_start = ('phase', lambda x: Counter(x)['Transition']),\n    sequence_type = ('sequence_type', 'first'),\n    gesture = ('gesture', 'first')\n)","metadata":{"_uuid":"34d048e2-3a87-4896-9baf-d498e271ada0","_cell_guid":"d6e0f72c-9711-4845-bbdd-aca283cdbac4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:41:21.749386Z","iopub.execute_input":"2025-09-02T20:41:21.749605Z","iopub.status.idle":"2025-09-02T20:41:22.450924Z","shell.execute_reply.started":"2025-09-02T20:41:21.749588Z","shell.execute_reply":"2025-09-02T20:41:22.450160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"seq_df = seq_df.merge(demo_df, on='subject', how='left')\ntarg_gestures = seq_df.loc[seq_df.sequence_type=='Target', 'gesture'].unique()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:22.451762Z","iopub.execute_input":"2025-09-02T20:41:22.452039Z","iopub.status.idle":"2025-09-02T20:41:22.465693Z","shell.execute_reply.started":"2025-09-02T20:41:22.452017Z","shell.execute_reply":"2025-09-02T20:41:22.465009Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"seq_df.gesture.hist(figsize=(15,6), bins=18, xrot=90);","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:22.466504Z","iopub.execute_input":"2025-09-02T20:41:22.467356Z","iopub.status.idle":"2025-09-02T20:41:22.878219Z","shell.execute_reply.started":"2025-09-02T20:41:22.467332Z","shell.execute_reply":"2025-09-02T20:41:22.877345Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_seq(imu_seq, gesture, subject=None, ges_start=None):\n    \"\"\"Plots the imu sequences with optional visual phase separation\"\"\"\n    df = pd.DataFrame(imu_seq, columns=imu_cols)\n    _, axes = plt.subplots(1, 2, figsize=(15,5))\n    for ax, cols in zip(axes, [acc_cols, rot_cols]):\n        df.plot(y=cols, ax=ax)\n        if ges_start:\n            ax.axvspan(0, ges_start, color='grey', alpha=0.2)\n            ax.axvspan(ges_start, len(df), color='red', alpha=0.2)\n    plt.suptitle(f'Subject: {subject}, Gesture: {gesture}')\n    return ax","metadata":{"_uuid":"56f84731-7eed-4131-9f64-eee729b2f09b","_cell_guid":"1ffea64c-6bf1-4646-acdc-e9a8ea03bbb8","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:41:22.879048Z","iopub.execute_input":"2025-09-02T20:41:22.879310Z","iopub.status.idle":"2025-09-02T20:41:22.885992Z","shell.execute_reply.started":"2025-09-02T20:41:22.879289Z","shell.execute_reply":"2025-09-02T20:41:22.885291Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"ex = seq_df.iloc[random.randint(0,len(seq_df))]\nplot_seq(ex.imu, ex.gesture, ex.subject, ges_start=ex.gesture_start);","metadata":{"_uuid":"005ceee2-b7aa-4217-bb75-f06127ec0994","_cell_guid":"3729aa1d-50ba-447e-a439-80676558e5ab","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:41:22.886502Z","iopub.execute_input":"2025-09-02T20:41:22.886726Z","iopub.status.idle":"2025-09-02T20:41:23.325037Z","shell.execute_reply.started":"2025-09-02T20:41:22.886708Z","shell.execute_reply":"2025-09-02T20:41:23.324188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"seq_df.seq_len.describe()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.326056Z","iopub.execute_input":"2025-09-02T20:41:23.326394Z","iopub.status.idle":"2025-09-02T20:41:23.335672Z","shell.execute_reply.started":"2025-09-02T20:41:23.326366Z","shell.execute_reply":"2025-09-02T20:41:23.335004Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"(seq_df.seq_len>128).sum()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.336467Z","iopub.execute_input":"2025-09-02T20:41:23.336727Z","iopub.status.idle":"2025-09-02T20:41:23.348999Z","shell.execute_reply.started":"2025-09-02T20:41:23.336705Z","shell.execute_reply":"2025-09-02T20:41:23.348363Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Preparation","metadata":{"_uuid":"3f354783-d5ca-4e05-b40e-bb5f62e06105","_cell_guid":"7db18cf1-110c-4ce5-a3f3-c2929e9cd5c4","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"## Data Transforms","metadata":{}},{"cell_type":"code","source":"def impute_quat(imu):\n    \"\"\"Imputes missing quaternion values with identity quaternion\"\"\"\n    imu = np.array(imu)\n    if np.isnan(imu[:,3:]).all():\n        seq_len = imu.shape[0]\n        imu[:,3:] = np.full((seq_len,4), [1,0,0,0], dtype=np.float32)\n    return imu","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.370137Z","iopub.execute_input":"2025-09-02T20:41:23.370438Z","iopub.status.idle":"2025-09-02T20:41:23.377232Z","shell.execute_reply.started":"2025-09-02T20:41:23.370416Z","shell.execute_reply":"2025-09-02T20:41:23.376498Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def remove_gravity(acc: np.ndarray, quat: np.ndarray):\n    \"\"\"Removes effect of gravity from acceleration\"\"\"\n    rot = R.from_quat(quat[:, [1, 2, 3, 0]])\n    gravity_world = np.array([0, 0, 9.81])\n    gravity_sensor_frame = rot.apply(gravity_world, inverse=True)\n    return acc - gravity_sensor_frame","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.378049Z","iopub.execute_input":"2025-09-02T20:41:23.378298Z","iopub.status.idle":"2025-09-02T20:41:23.392723Z","shell.execute_reply.started":"2025-09-02T20:41:23.378275Z","shell.execute_reply":"2025-09-02T20:41:23.392013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calc_angular_vel(quat: np.ndarray, delta_t=1/10):\n    rot = R.from_quat(quat[:, [1, 2, 3, 0]])\n    rel_rot = rot[1:] * rot[:-1].inv()\n    vel = (2/delta_t)*rel_rot.as_rotvec()\n    return np.concatenate([vel, np.zeros((1,3))])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.393556Z","iopub.execute_input":"2025-09-02T20:41:23.393796Z","iopub.status.idle":"2025-09-02T20:41:23.409263Z","shell.execute_reply.started":"2025-09-02T20:41:23.393777Z","shell.execute_reply":"2025-09-02T20:41:23.408471Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calc_angular_dist(quat: np.ndarray):\n    quat = quat[:, [1, 2, 3, 0]]\n    rot = R.from_quat(quat)\n    delta_rot = rot[1:] * rot[:-1].inv()\n    rotvecs = delta_rot.as_rotvec()\n    dist = np.linalg.norm(rotvecs, axis=1, keepdims=True)\n    return np.concatenate([dist, np.zeros((1,1))])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.410185Z","iopub.execute_input":"2025-09-02T20:41:23.410488Z","iopub.status.idle":"2025-09-02T20:41:23.425673Z","shell.execute_reply.started":"2025-09-02T20:41:23.410471Z","shell.execute_reply":"2025-09-02T20:41:23.425019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def engineer_features(imu: np.ndarray):\n    acc, quat = imu[:, :3], imu[:, 3:]\n    acc_mag = np.linalg.norm(acc, axis=1, keepdims=True)\n    \n    lin_acc = remove_gravity(acc, quat)\n    lin_acc_mag = np.linalg.norm(lin_acc, axis=1, keepdims=True)\n    \n    rot_angle = 2*np.arccos(quat[:,0])[:, None]\n\n    return np.concatenate([\n        acc, acc_mag, lin_acc, lin_acc_mag, quat, rot_angle,\n        calc_angular_vel(quat), calc_angular_dist(quat)\n    ], axis=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.426467Z","iopub.execute_input":"2025-09-02T20:41:23.426719Z","iopub.status.idle":"2025-09-02T20:41:23.441136Z","shell.execute_reply.started":"2025-09-02T20:41:23.426697Z","shell.execute_reply":"2025-09-02T20:41:23.440314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Normalizer(Transform):\n    \"\"\"Applies z-score normalization to all the features\"\"\"\n    def setups(self, seqs: TfmdLists):\n        seqs = np.concatenate(list(seqs))\n        self.ss = StandardScaler().fit(seqs)\n\n    def encodes(self, seq: np.ndarray):\n        normed = self.ss.transform(seq).astype(np.float32)\n        return torch.from_numpy(normed)","metadata":{"_uuid":"0026403a-a811-48d8-a792-57c3635e6bf3","_cell_guid":"ad796a46-51fe-4894-9c16-e60126d6e8e2","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:41:23.442014Z","iopub.execute_input":"2025-09-02T20:41:23.442312Z","iopub.status.idle":"2025-09-02T20:41:23.464351Z","shell.execute_reply.started":"2025-09-02T20:41:23.442291Z","shell.execute_reply":"2025-09-02T20:41:23.463582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pad_or_trunc(seq: torch.Tensor, L=128):\n    \"\"\"Equalizes all the sequence lengths by padding or truncating them\"\"\"\n    # truncate by removing alternate timesteps\n    while seq.size(0)>L:\n        trunc_len = seq.size(0)-L\n        truncated = seq[:2*trunc_len:2, :]\n        seq = torch.cat([truncated, seq[2*trunc_len:]])\n    # add padding to the beginning and end of the sequence\n    pad_len = L-seq.size(0)\n    pre_pad = torch.zeros(pad_len//2, seq.size(1))\n    post_pad = torch.zeros((pad_len+1)//2, seq.size(1))\n    return torch.cat([pre_pad,seq,post_pad])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:41:23.467952Z","iopub.execute_input":"2025-09-02T20:41:23.468195Z","iopub.status.idle":"2025-09-02T20:41:23.479336Z","shell.execute_reply.started":"2025-09-02T20:41:23.468180Z","shell.execute_reply":"2025-09-02T20:41:23.478595Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imu_tfms = [ColReader('imu'), impute_quat, engineer_features, Normalizer(), pad_or_trunc]\n\nlbl_enc = Categorize(seq_df.gesture.unique())\nlbl_tfms = [ColReader('gesture'), lbl_enc]","metadata":{"_uuid":"bb1e066c-4423-43a9-bdb1-5e93646a1803","_cell_guid":"0755400a-c79a-4489-8a69-76183f567103","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:41:23.480127Z","iopub.execute_input":"2025-09-02T20:41:23.480372Z","iopub.status.idle":"2025-09-02T20:41:23.497450Z","shell.execute_reply.started":"2025-09-02T20:41:23.480350Z","shell.execute_reply":"2025-09-02T20:41:23.496739Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DataLoaders","metadata":{}},{"cell_type":"code","source":"problematic_subjs = ['SUBJ_045235', 'SUBJ_019262']\nvalid_mask = ~seq_df.subject.isin(problematic_subjs)\nseq_df = seq_df.loc[valid_mask].reset_index(drop=True).copy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:11.984706Z","iopub.execute_input":"2025-09-02T20:43:11.985495Z","iopub.status.idle":"2025-09-02T20:43:11.994439Z","shell.execute_reply.started":"2025-09-02T20:43:11.985464Z","shell.execute_reply":"2025-09-02T20:43:11.993700Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sgkf = StratifiedGroupKFold().split(seq_df, seq_df.gesture, seq_df.subject)\ntrn_idxs, val_idxs = next(iter(sgkf))\nlen(trn_idxs), len(val_idxs)","metadata":{"_uuid":"f8c90995-0586-4623-b396-f78f8f11bab3","_cell_guid":"08922c31-dbaa-41c0-9703-2c5511ebfc8e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:43:12.301714Z","iopub.execute_input":"2025-09-02T20:43:12.301939Z","iopub.status.idle":"2025-09-02T20:43:12.353424Z","shell.execute_reply.started":"2025-09-02T20:43:12.301922Z","shell.execute_reply":"2025-09-02T20:43:12.352735Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def oversample(idxs):\n    labels = seq_df.loc[idxs, 'gesture']\n    max_count = labels.value_counts().max()\n    sampled = labels.groupby(labels).apply(\n        lambda s: s.sample(max_count, replace=True)\n    )\n    return sampled.index.get_level_values(1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:16.236794Z","iopub.execute_input":"2025-09-02T20:43:16.237110Z","iopub.status.idle":"2025-09-02T20:43:16.241501Z","shell.execute_reply.started":"2025-09-02T20:43:16.237089Z","shell.execute_reply":"2025-09-02T20:43:16.240762Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"_trn_idxs = oversample(trn_idxs)\ndsets = Datasets(seq_df, [imu_tfms, lbl_tfms], splits=(_trn_idxs, val_idxs))\n\ndls = dsets.dataloaders(bs=64, shuffle=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:16.490825Z","iopub.execute_input":"2025-09-02T20:43:16.491375Z","iopub.status.idle":"2025-09-02T20:43:24.069835Z","shell.execute_reply.started":"2025-09-02T20:43:16.491349Z","shell.execute_reply":"2025-09-02T20:43:24.069007Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imus, lbls = dls.one_batch()\nimus.shape, lbls.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:25.941392Z","iopub.execute_input":"2025-09-02T20:43:25.942178Z","iopub.status.idle":"2025-09-02T20:43:26.060838Z","shell.execute_reply.started":"2025-09-02T20:43:25.942154Z","shell.execute_reply":"2025-09-02T20:43:26.060161Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Building the model","metadata":{}},{"cell_type":"code","source":"class LinearBlock(nn.Sequential):\n    def __init__(self, ni, nf, p=0.0):\n        super().__init__(\n            nn.Linear(ni, nf, bias=False), nn.BatchNorm1d(nf),\n            nn.ReLU(), nn.Dropout(p)\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:26.386860Z","iopub.execute_input":"2025-09-02T20:43:26.387121Z","iopub.status.idle":"2025-09-02T20:43:26.391489Z","shell.execute_reply.started":"2025-09-02T20:43:26.387102Z","shell.execute_reply":"2025-09-02T20:43:26.390633Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ConvBlock(nn.Sequential):\n    def __init__(self, ni, nf, ks=3, stride=1, do_relu=True, p=0.0):\n        activation = [nn.ReLU()] if do_relu else []\n        super().__init__(\n            nn.Conv1d(ni, nf, ks, stride=stride, padding=ks//2, bias=False),\n            nn.BatchNorm1d(nf), *activation, nn.Dropout(p)\n        )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:28.646811Z","iopub.execute_input":"2025-09-02T20:43:28.647457Z","iopub.status.idle":"2025-09-02T20:43:28.653247Z","shell.execute_reply.started":"2025-09-02T20:43:28.647426Z","shell.execute_reply":"2025-09-02T20:43:28.652424Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SEBlock(nn.Module):\n    def __init__(self, c, r=8):\n        super().__init__()\n        self.squeeze = nn.AdaptiveAvgPool1d(1)\n        self.excitation = nn.Sequential(\n            nn.Linear(c, c//r, bias=False), nn.ReLU(),\n            nn.Linear(c//r, c, bias=False), nn.Sigmoid()\n        )\n\n    def forward(self, x):\n        y = self.squeeze(x).squeeze()\n        y = self.excitation(y).unsqueeze(-1)\n        return x*y","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:28.834267Z","iopub.execute_input":"2025-09-02T20:43:28.834925Z","iopub.status.idle":"2025-09-02T20:43:28.841223Z","shell.execute_reply.started":"2025-09-02T20:43:28.834901Z","shell.execute_reply":"2025-09-02T20:43:28.840495Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ResSEBlock(nn.Module):\n    def __init__(self, ni, nf, p=0):\n        super().__init__()\n        self.cnns = nn.Sequential(ConvBlock(ni, nf), ConvBlock(nf, nf, do_relu=False))\n        self.se = SEBlock(nf)\n        self.idconv = noop if ni==nf else ConvBlock(ni, nf, ks=1, do_relu=False)\n        self.drop = nn.Dropout(p)\n\n    def forward(self, x):\n        x_ = self.se(self.cnns(x))\n        return self.drop(F.relu(x_ + self.idconv(x)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:29.003923Z","iopub.execute_input":"2025-09-02T20:43:29.004169Z","iopub.status.idle":"2025-09-02T20:43:29.009206Z","shell.execute_reply.started":"2025-09-02T20:43:29.004152Z","shell.execute_reply":"2025-09-02T20:43:29.008382Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class UniModalModel(nn.Module):\n    def __init__(self, n_classes, imu_dim, f_dim=128, rnn_dim=128, p=0.1):\n        super().__init__()\n        self.cnn = nn.Sequential(\n            ResSEBlock(imu_dim, f_dim//2, p=p), ResSEBlock(f_dim//2, f_dim, p=p)\n        )\n        self.rnn = nn.GRU(f_dim, rnn_dim, batch_first=True, bidirectional=True)\n        self.head = nn.Sequential(\n            LinearBlock(2*rnn_dim, rnn_dim, p=p), LinearBlock(rnn_dim, rnn_dim//2, p=p),\n            nn.Linear(rnn_dim//2, n_classes)\n        )\n\n    def forward(self, seqs):\n        # seqs: (bs, seq_len, imu_dim)\n        seqs = seqs.permute(0,2,1)              # (bs, imu_dim, seq_len)\n        f = self.cnn(seqs).permute(0,2,1)       # (bs, seq_len, f_dim)\n        output, _ = self.rnn(f)                 # (bs, seq_len, 2*rnn_dim)\n        last_step = output[:,-1,:]              # (bs, 2*rnn_dim)\n        return self.head(last_step)             # (bs, n_classes)","metadata":{"_uuid":"cabd87f3-378d-4a12-9781-c7794461ff61","_cell_guid":"d50172bb-2d51-4350-9aa6-74325e686559","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:43:48.935606Z","iopub.execute_input":"2025-09-02T20:43:48.936175Z","iopub.status.idle":"2025-09-02T20:43:48.944509Z","shell.execute_reply.started":"2025-09-02T20:43:48.936149Z","shell.execute_reply":"2025-09-02T20:43:48.943683Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"n_classes = len(lbl_enc.vocab)\nimu_dim = imus.size(-1)\nmodel = UniModalModel(n_classes, imu_dim)","metadata":{"_uuid":"be534628-1243-4e19-b75b-0b0f02e2fbc9","_cell_guid":"ed27ca5f-a760-4d69-92ed-e23152bba69e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:43:49.551535Z","iopub.execute_input":"2025-09-02T20:43:49.552392Z","iopub.status.idle":"2025-09-02T20:43:49.565718Z","shell.execute_reply.started":"2025-09-02T20:43:49.552362Z","shell.execute_reply":"2025-09-02T20:43:49.565085Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training","metadata":{}},{"cell_type":"code","source":"def init_weights(m):\n    if isinstance(m, nn.GRU):\n        for name, param in m.named_parameters():\n            if 'weight' in name:\n                nn.init.xavier_uniform_(param, gain=5/3)\n            elif 'bias_ih_l0' in name:\n                hidden_sz = param.size(0)//3\n                nn.init.constant_(param[:hidden_sz], 1.0)\n    elif isinstance(m, (nn.Conv1d, nn.Linear)):\n        nn.init.kaiming_normal_(m.weight)\n        if m.bias is not None: nn.init.zeros_(m.bias)","metadata":{"_uuid":"3a971337-a3f6-4546-aaf9-126ba24c9d22","_cell_guid":"30bedafb-4b64-47b4-a7ce-12d666a5739c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:43:51.814338Z","iopub.execute_input":"2025-09-02T20:43:51.814616Z","iopub.status.idle":"2025-09-02T20:43:51.819902Z","shell.execute_reply.started":"2025-09-02T20:43:51.814597Z","shell.execute_reply":"2025-09-02T20:43:51.819168Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def hierarchical_f1(targs: pd.Series, preds: pd.Series, targ_gestures):\n    # Compute binary F1 (Target vs Non-Target)\n    y_true_bi = targs.isin(targ_gestures).values\n    y_pred_bi = preds.isin(targ_gestures).values\n    f1_binary = f1_score(y_true_bi, y_pred_bi, pos_label=True, zero_division=0, average='binary')\n\n    # Build multi-class labels for gestures\n    y_true_mc = targs.apply(lambda x: x if x in targ_gestures else 'non_target')\n    y_pred_mc = preds.apply(lambda x: x if x in targ_gestures else 'non_target')\n\n    # Compute macro F1 over all gesture classes\n    f1_macro = f1_score(y_true_mc, y_pred_mc, average='macro', zero_division=0)\n\n    return 0.5*f1_binary + 0.5*f1_macro","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:51.986805Z","iopub.execute_input":"2025-09-02T20:43:51.987028Z","iopub.status.idle":"2025-09-02T20:43:51.992031Z","shell.execute_reply.started":"2025-09-02T20:43:51.987012Z","shell.execute_reply":"2025-09-02T20:43:51.991322Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class HierarchicalF1Metric(Metric):\n    def __init__(self, targ_gestures):\n        self.targ_gestures = targ_gestures\n        self.reset()\n\n    def reset(self): self.preds, self.targs = [], []\n\n    def accumulate(self, learn):\n        _, preds = learn.pred.max(dim=1)\n        self.preds.extend(preds.cpu()); self.targs.extend(learn.y.cpu())\n\n    @property\n    def value(self):\n        preds = pd.Series(self.preds).apply(lbl_enc.decode)\n        targs = pd.Series(self.targs).apply(lbl_enc.decode)\n        return hierarchical_f1(targs, preds, self.targ_gestures)","metadata":{"_uuid":"0da7683f-116e-404b-aa5b-2aa3acdcf22d","_cell_guid":"40cb81af-4d77-4330-9c18-476878497713","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-09-02T20:43:52.157953Z","iopub.execute_input":"2025-09-02T20:43:52.158182Z","iopub.status.idle":"2025-09-02T20:43:52.163162Z","shell.execute_reply.started":"2025-09-02T20:43:52.158165Z","shell.execute_reply":"2025-09-02T20:43:52.162452Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_learner(model, bs=512, wd=0.5, moms=(0.99,0.85,0.99), smoothing=0.05):\n    dls = dsets.dataloaders(bs, shuffle=True)\n    model = model.apply(init_weights)\n    model.head[-1].weight.data *= 0.1\n    loss_func = CrossEntropyLossFlat(label_smoothing=smoothing)\n    metrics = [HierarchicalF1Metric(targ_gestures), accuracy]\n    cbs = MixUp()\n    \n    return Learner(dls, model, loss_func=loss_func, cbs=cbs, metrics=metrics, wd=wd, moms=moms)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T22:02:27.614131Z","iopub.execute_input":"2025-09-02T22:02:27.614410Z","iopub.status.idle":"2025-09-02T22:02:27.620660Z","shell.execute_reply.started":"2025-09-02T22:02:27.614373Z","shell.execute_reply":"2025-09-02T22:02:27.619799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for mom in (0.99,0.97,0.95,0.90):\n    learn = get_learner(model, moms=(mom,mom,mom))\n    learn.lr_find()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T20:43:54.842273Z","iopub.execute_input":"2025-09-02T20:43:54.842549Z","iopub.status.idle":"2025-09-02T20:46:46.081262Z","shell.execute_reply.started":"2025-09-02T20:43:54.842528Z","shell.execute_reply":"2025-09-02T20:46:46.080437Z"},"scrolled":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = UniModalModel(n_classes, imu_dim, p=0.2)\nlearn = get_learner(model, wd=1.5, moms=(0.99,0.85,0.99))\nlearn.fit_one_cycle(80, lr_max=1e-2, div=100)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T22:24:34.005852Z","iopub.execute_input":"2025-09-02T22:24:34.006680Z","iopub.status.idle":"2025-09-02T22:33:36.488783Z","shell.execute_reply.started":"2025-09-02T22:24:34.006640Z","shell.execute_reply":"2025-09-02T22:33:36.487682Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"learn.recorder.plot_loss();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T22:35:27.386583Z","iopub.execute_input":"2025-09-02T22:35:27.387395Z","iopub.status.idle":"2025-09-02T22:35:27.627273Z","shell.execute_reply.started":"2025-09-02T22:35:27.387359Z","shell.execute_reply":"2025-09-02T22:35:27.626493Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Saving the model","metadata":{}},{"cell_type":"code","source":"learn.export('uni_modal.pkl')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-02T22:35:33.322024Z","iopub.execute_input":"2025-09-02T22:35:33.322934Z","iopub.status.idle":"2025-09-02T22:35:33.470099Z","shell.execute_reply.started":"2025-09-02T22:35:33.322903Z","shell.execute_reply":"2025-09-02T22:35:33.469395Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"To submit to the competition, check out the inference notebook: https://www.kaggle.com/code/utkarsh1232/lb-0-75-imu-only-inference-with-fastai","metadata":{}}]}