{"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":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31193,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader, Subset, random_split\nimport pandas as pd\nimport numpy as np\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nimport os\nfrom pathlib import Path\nimport matplotlib.pyplot as plt\nimport os\nos.environ[\"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION\"] = \"python\"\nfrom datetime import datetime\nfrom sklearn.metrics import precision_score, recall_score, f1_score, confusion_matrix, precision_recall_curve\nimport io\nfrom PIL import Image\nimport polars as pl\nimport random\nimport kaggle_evaluation.cmi_inference_server\nimport torch.nn.functional as F\n\nfrom sklearn.metrics import f1_score\nimport warnings\nwarnings.filterwarnings(\"ignore\")\nwarnings.filterwarnings(\"ignore\", category=RuntimeWarning)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:20:16.797587Z","iopub.execute_input":"2025-12-10T15:20:16.797883Z","iopub.status.idle":"2025-12-10T15:20:16.803859Z","shell.execute_reply.started":"2025-12-10T15:20:16.797861Z","shell.execute_reply":"2025-12-10T15:20:16.803188Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Using {device} device\")\nif device == \"cuda\":\n    print(f\"GPU: {torch.cuda.get_device_name()}\")\n\ndevice = \"cuda\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:20:16.809133Z","iopub.execute_input":"2025-12-10T15:20:16.809343Z","iopub.status.idle":"2025-12-10T15:20:16.823572Z","shell.execute_reply.started":"2025-12-10T15:20:16.809323Z","shell.execute_reply":"2025-12-10T15:20:16.822922Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data loading","metadata":{}},{"cell_type":"code","source":"training_data_path = Path(\"data/CMI/train.csv\")\ntraining_data_demo_path = Path(\"data/CMI/train_demographics.csv\")\n\ntest_data_path = Path(\"data/CMI/test.csv\")\ntest_data_demo_path = Path(\"data/CMI/test_demographics.csv\")\n\n\ntrain_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\ntrain_demo_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv')\ntest_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv')\ntest_demo_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:20:16.824599Z","iopub.execute_input":"2025-12-10T15:20:16.824838Z","iopub.status.idle":"2025-12-10T15:20:32.145938Z","shell.execute_reply.started":"2025-12-10T15:20:16.824817Z","shell.execute_reply":"2025-12-10T15:20:32.145332Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data visualization","metadata":{}},{"cell_type":"code","source":"train_df.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:20:32.147243Z","iopub.execute_input":"2025-12-10T15:20:32.147913Z","iopub.status.idle":"2025-12-10T15:20:32.169828Z","shell.execute_reply.started":"2025-12-10T15:20:32.147884Z","shell.execute_reply":"2025-12-10T15:20:32.169235Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset","metadata":{}},{"cell_type":"code","source":"class CMIIMUDataset(Dataset):\n    def __init__(self, df, demographics_df=None, max_len=256, is_train=True):\n        self.max_len = max_len\n        self.is_train = is_train\n        self.df = df.copy()\n        self.demographics = demographics_df\n\n        # Merge demographics\n        if self.demographics is not None:\n            self.df = self.df.merge(self.demographics, on='subject', how='left')\n\n        # === COLUMNS ===\n        self.imu_cols = [c for c in df.columns if c.startswith(('acc_', 'rot_'))]\n        self.thermal_cols = [f\"thm_{i}\" for i in range(1, 6)]\n        self.tof_pixel_cols = [c for c in df.columns if c.startswith('tof_') and '_v' in c]\n\n        # === SCALERS ===\n        self.scaler_imu_raw = StandardScaler()      # Cho 7 cột gốc (nếu cần sau này)\n        self.scaler_imu_enhanced = StandardScaler() # ← MỚI: dành riêng cho enhanced IMU\n        self.scaler_thermal = StandardScaler()\n        self.scaler_tof = StandardScaler()\n\n        # Fit trên dữ liệu thô gốc (7 cột) - giữ lại nếu cần\n        imu_raw_all = df[self.imu_cols].ffill().bfill().fillna(0).values\n        self.scaler_imu_raw.fit(imu_raw_all)\n\n        # Fit scaler cho enhanced IMU (trên tất cả sequences)\n        enhanced_all = []\n        for _, group in df.groupby('sequence_id'):\n            raw = group[self.imu_cols].ffill().bfill().fillna(0).values.astype(np.float32)\n            df_raw = pd.DataFrame(raw, columns=self.imu_cols)\n            df_enh = add_features_enhanced(df_raw)\n            cols = [c for c in df_enh.columns if c not in ['sequence_id']]\n            enhanced_all.append(df_enh[cols].values)\n        enhanced_all_array = np.concatenate(enhanced_all, axis=0)\n        self.scaler_imu_enhanced.fit(enhanced_all_array) # ← Fit trên đúng 18 features\n\n        # Thermal và TOF giữ nguyên\n        thm_all = df[self.thermal_cols].ffill().bfill().fillna(0).values\n        self.scaler_thermal.fit(thm_all)\n\n        # TOF scaler\n        tof_all_feats = []\n        for _, group in df.groupby('sequence_id'):\n            raw = group[self.tof_pixel_cols].replace(-1, np.nan).values\n            feats = []\n            for s in range(5):\n                pix = raw[:, s*64:(s+1)*64]\n                mean_v = np.nanmean(pix, axis=1, keepdims=True)\n                std_v  = np.nanstd(pix, axis=1, keepdims=True)\n                valid  = np.sum(pix > -1, axis=1, keepdims=True) / 64.0\n                feats.append(np.concatenate([mean_v, std_v, valid], axis=1))\n            tof_all_feats.append(np.concatenate(feats, axis=1))\n        tof_all_array = np.concatenate(tof_all_feats, axis=0)\n        tof_all_array = np.nan_to_num(tof_all_array, nan=0.0)\n        self.scaler_tof.fit(tof_all_array)\n\n        # === BUILD SEQUENCES ===\n        self.data = []\n        self.labels = []\n        self.sequence_ids = []\n        self.subjects = []\n\n        label_encoder = LabelEncoder()\n        if 'gesture' in df.columns:\n            label_encoder.fit(df['gesture'].astype(str))\n\n        for seq_id, group in df.groupby('sequence_id'):\n            seq_len = len(group)\n            pad_len = max(0, max_len - seq_len)\n\n            # 1. IMU THÔ (để tính enhanced features)\n            imu_raw = group[self.imu_cols].ffill().bfill().fillna(0).values.astype(np.float32)\n            imu_df_raw = pd.DataFrame(imu_raw, columns=self.imu_cols)\n            imu_df_enhanced = add_features_enhanced(imu_df_raw)\n            enhanced_cols = [c for c in imu_df_enhanced.columns if c not in ['sequence_id']]\n            imu_enhanced = imu_df_enhanced[enhanced_cols].values\n\n            # Scale IMU enhanced (quan trọng!)\n            imu_enhanced = self.scaler_imu_enhanced.transform(imu_enhanced)\n\n            # 2. Thermal\n            thm_raw = group[self.thermal_cols].ffill().bfill().fillna(0).values.astype(np.float32)\n            thm_scaled = self.scaler_thermal.transform(thm_raw)\n\n            # 3. TOF 15 feats\n            raw_tof = group[self.tof_pixel_cols].replace(-1, np.nan).values\n            tof_feat = []\n            for s in range(5):\n                pix = raw_tof[:, s*64:(s+1)*64]\n                mean_v = np.nanmean(pix, axis=1, keepdims=True)\n                std_v  = np.nanstd(pix, axis=1, keepdims=True)\n                valid  = np.sum(pix > -1, axis=1, keepdims=True) / 64.0\n                tof_feat.append(np.concatenate([mean_v, std_v, valid], axis=1))\n            tof_feat = np.concatenate(tof_feat, axis=1)\n            tof_feat = np.nan_to_num(tof_feat, nan=0.0)\n            tof_scaled = self.scaler_tof.transform(tof_feat)\n\n            # Pad tất cả về max_len\n            if pad_len > 0:\n                imu_enhanced = np.pad(imu_enhanced, ((0, pad_len), (0, 0)), 'constant')\n                thm_scaled   = np.pad(thm_scaled,   ((0, pad_len), (0, 0)), 'constant')\n                tof_scaled   = np.pad(tof_scaled,   ((0, pad_len), (0, 0)), 'constant')\n\n            imu_enhanced = imu_enhanced[:max_len]\n            thm_scaled   = thm_scaled[:max_len]\n            tof_scaled   = tof_scaled[:max_len]\n\n            # Ghép lại: [enhanced_imu, thermal, tof] → ~38 channels\n            final_seq = np.concatenate([imu_enhanced, thm_scaled, tof_scaled], axis=1).astype(np.float32)\n\n            # Left-handed mirroring (nếu có demographics)\n            if self.demographics is not None:\n                subject = group['subject'].iloc[0]\n                handedness = self.demographics[self.demographics['subject'] == subject]['handedness'].iloc[0]\n                if handedness == 0:  # left-handed\n                    final_seq = self.mirror_sequence(final_seq)\n\n            self.data.append(final_seq)\n            self.sequence_ids.append(seq_id)\n\n            if 'gesture' in group.columns:\n                label = label_encoder.transform([group['gesture'].iloc[0]])[0]\n                self.labels.append(label)\n            if self.demographics is not None:\n                self.subjects.append(subject)\n\n        self.data = torch.tensor(np.stack(self.data))  # [N, 256, ~38]\n        if self.labels:\n            self.labels = torch.tensor(self.labels, dtype=torch.long)\n            self.label_encoder = label_encoder\n\n        print(f\"Enhanced Dataset ready: {len(self)} sequences | Shape: {self.data.shape}\")\n\n    def mirror_sequence(self, seq):\n        seq = seq.copy()\n        seq[:, 0] *= -1   # acc_x\n        seq[:, 4] *= -1   # rot_x (quaternion w,-x,y,z)\n        return seq\n\n    def __len__(self): return len(self.data)\n\n    def __getitem__(self, idx):\n        x = self.data[idx]\n        if self.is_train and random.random() < 0.5:\n            x = x.clone()\n            x[:, -20:-15] = 0  # dropout thermal\n            x[:, -15:] = 0     # dropout tof\n        if hasattr(self, 'labels'):\n            return x, self.labels[idx]\n        return x\n\n    def print(self):\n        print(f\"CMIIMUDataset - {len(self)} sequences | Shape: {self.data.shape}\")\n        if hasattr(self, 'labels'):\n            print(f\"   Classes: {len(self.label_encoder.classes_)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:26:16.577200Z","iopub.execute_input":"2025-12-10T15:26:16.577985Z","iopub.status.idle":"2025-12-10T15:26:16.599047Z","shell.execute_reply.started":"2025-12-10T15:26:16.577957Z","shell.execute_reply":"2025-12-10T15:26:16.598357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nclass CMIIMUOnlyDataset(Dataset):\n    def __init__(self, df, max_len=256):\n        self.max_len = max_len\n        self.df = df.copy()\n\n        imu_cols = [c for c in df.columns if c.startswith(('acc_', 'rot_'))]\n        self.scaler_imu = StandardScaler()\n        imu_all = df[imu_cols].ffill().bfill().fillna(0).values\n        self.scaler_imu.fit(imu_all)\n\n        self.data = []\n        self.labels = []\n\n        label_encoder = LabelEncoder()\n        if 'gesture' in df.columns:\n            label_encoder.fit(df['gesture'].astype(str))\n\n        for seq_id, group in df.groupby('sequence_id'):\n            seq_len = len(group)\n            pad_len = max(0, max_len - seq_len)\n\n            # Chỉ lấy IMU\n            imu_raw = group[imu_cols].ffill().bfill().fillna(0).values\n            imu = self.scaler_imu.transform(imu_raw)\n            if pad_len > 0:\n                imu = np.pad(imu, ((0, pad_len), (0, 0)), 'constant')\n            imu = imu[:max_len]\n\n            # Enhanced features\n            df_enh = pd.DataFrame(imu_raw, columns=imu_cols)\n            df_enh = add_features_enhanced(df_enh)\n            x = df_enh.drop(columns=['sequence_id'], errors='ignore').values.astype(np.float32)\n            if pad_len > 0:\n                x = np.pad(x, ((0, pad_len), (0, 0)), 'constant')\n            x = x[:max_len]\n\n            self.data.append(x)\n            if 'gesture' in group.columns:\n                label = label_encoder.transform([group['gesture'].iloc[0]])[0]\n                self.labels.append(label)\n\n        self.data = torch.tensor(np.stack(self.data))\n        if self.labels:\n            self.labels = torch.tensor(self.labels, dtype=torch.long)\n            self.label_encoder = label_encoder\n\n        print(f\"IMU-Only Dataset ready: {len(self)} sequences, shape {self.data.shape}\")\n\n    def __len__(self): return len(self.data)\n    def __getitem__(self, idx):\n        return self.data[idx], self.labels[idx]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:25:17.706004Z","iopub.execute_input":"2025-12-10T15:25:17.706543Z","iopub.status.idle":"2025-12-10T15:25:17.714869Z","shell.execute_reply.started":"2025-12-10T15:25:17.706516Z","shell.execute_reply":"2025-12-10T15:25:17.714103Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfrom scipy.spatial.transform import Rotation as R\n\ndef handle_quaternion_missing_values(rot_data: np.ndarray) -> np.ndarray:\n    \"\"\"Xử lý NaN trong quaternion, đảm bảo |q| = 1\"\"\"\n    rot_cleaned = rot_data.copy()\n    for i in range(len(rot_data)):\n        row = rot_data[i]\n        missing_count = np.isnan(row).sum()\n\n        if missing_count == 0:\n            norm = np.linalg.norm(row)\n            if norm > 1e-8:\n                rot_cleaned[i] = row / norm\n            else:\n                rot_cleaned[i] = [1.0, 0.0, 0.0, 0.0]\n        elif missing_count == 1:\n            missing_idx = np.where(np.isnan(row))[0][0]\n            valid_values = row[~np.isnan(row)]\n            sum_squares = np.sum(valid_values**2)\n            if sum_squares <= 1.0:\n                missing_value = np.sqrt(max(0, 1.0 - sum_squares))\n                if i > 0 and not np.isnan(rot_cleaned[i-1, missing_idx]):\n                    if rot_cleaned[i-1, missing_idx] < 0:\n                        missing_value = -missing_value\n                rot_cleaned[i, missing_idx] = missing_value\n                rot_cleaned[i, ~np.isnan(row)] = valid_values\n            else:\n                rot_cleaned[i] = [1.0, 0.0, 0.0, 0.0]\n        else:\n            rot_cleaned[i] = [1.0, 0.0, 0.0, 0.0]\n    return rot_cleaned\n\n\ndef remove_gravity_from_acc(acc: np.ndarray, rot: np.ndarray) -> np.ndarray:\n    \"\"\"Loại bỏ trọng lực bằng cách chuyển về world frame\"\"\"\n    try:\n        rot_scipy = rot[:, [1, 2, 3, 0]]  # scipy format: [x,y,z,w]\n        r = R.from_quat(rot_scipy)\n        gravity_world = np.array([0, 0, 9.81])\n        gravity_device = r.apply(gravity_world[None, :]).squeeze(1)\n        return acc - gravity_device\n    except:\n        return acc.copy()\n\n\ndef calculate_angular_velocity_from_quat(rot: np.ndarray, dt: float = 1/200.0) -> np.ndarray:\n    \"\"\"Tính vận tốc góc từ quaternion liên tiếp\"\"\"\n    angular_vel = np.zeros((len(rot), 3))\n    for i in range(len(rot) - 1):\n        q1, q2 = rot[i], rot[i + 1]\n        if np.any(np.isnan(q1)) or np.any(np.isnan(q2)):\n            continue\n        try:\n            r1 = R.from_quat(q1[[1,2,3,0]])\n            r2 = R.from_quat(q2[[1,2,3,0]])\n            delta_rot = r1.inv() * r2\n            ang_vel_vec = delta_rot.as_rotvec()\n            angular_vel[i] = ang_vel_vec / dt\n        except:\n            pass\n    if len(rot) > 1:\n        angular_vel[-1] = angular_vel[-2]\n    return angular_vel\n\n\ndef add_features_enhanced(df):\n    df = df.copy()\n    acc_cols = ['acc_x', 'acc_y', 'acc_z']\n    rot_cols = ['rot_w', 'rot_x', 'rot_y', 'rot_z']\n\n    # 1. Fix quaternion\n    rot = df[rot_cols].values\n    rot = handle_quaternion_missing_values(rot)\n    df[rot_cols] = rot\n\n    # 2. Remove gravity\n    acc = df[acc_cols].values\n    linear_acc = remove_gravity_from_acc(acc, rot)\n    df['acc_x_lin'] = linear_acc[:, 0]\n    df['acc_y_lin'] = linear_acc[:, 1]\n    df['acc_z_lin'] = linear_acc[:, 2]\n\n    # 3. Angular velocity\n    angular_vel = calculate_angular_velocity_from_quat(rot)\n    df['ang_vel_x'] = angular_vel[:, 0]\n    df['ang_vel_y'] = angular_vel[:, 1]\n    df['ang_vel_z'] = angular_vel[:, 2]\n\n    # 4. Jerk\n    df['jerk_x'] = np.gradient(df['acc_x_lin'])\n    df['jerk_y'] = np.gradient(df['acc_y_lin'])\n    df['jerk_z'] = np.gradient(df['acc_z_lin'])\n\n    # 5. Magnitude\n    df['acc_mag'] = np.sqrt(df['acc_x_lin']**2 + df['acc_y_lin']**2 + df['acc_z_lin']**2)\n    df['jerk_mag'] = np.sqrt(df['jerk_x']**2 + df['jerk_y']**2 + df['jerk_z']**2)\n\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:25:20.407559Z","iopub.execute_input":"2025-12-10T15:25:20.408214Z","iopub.status.idle":"2025-12-10T15:25:20.421092Z","shell.execute_reply.started":"2025-12-10T15:25:20.408187Z","shell.execute_reply":"2025-12-10T15:25:20.420387Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# Tạo dataset\ndataset = CMIIMUDataset(train_df, max_len=256)\ndataset.print()\n\n# In danh sách gesture và mã encoded\nprint(\"\\n\" + \"=\"*60)\nprint(\"UNIQUE GESTURES AND THEIR ENCODED VALUES\")\nprint(\"=\"*60)\n\n# Lấy danh sách gesture gốc (từ train_df)\nunique_gestures = np.unique(train_df['gesture'])\nprint(f\"Total unique gestures: {len(unique_gestures)}\")\n\n# In ra từng cái kèm mã\nfor i, gesture_name in enumerate(sorted(unique_gestures)):\n    encoded = dataset.label_encoder.transform([gesture_name])[0]\n    print(f\"{gesture_name:35} → Encoded: {encoded:2d}\")\n\nprint(\"=\"*60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:26:22.618645Z","iopub.execute_input":"2025-12-10T15:26:22.619180Z","iopub.status.idle":"2025-12-10T15:29:40.231824Z","shell.execute_reply.started":"2025-12-10T15:26:22.619150Z","shell.execute_reply":"2025-12-10T15:29:40.231131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nUnique sequence types:\")\nprint(train_df['sequence_type'].unique())\n\n# Print example gestures for each sequence type\nprint(\"\\nExample gestures for each sequence type:\")\nfor seq_type in train_df['sequence_type'].unique():\n    gestures = train_df[train_df['sequence_type'] == seq_type]['gesture'].unique()\n    print(f\"\\n{seq_type}:\")\n    for g in gestures:\n        print(f\"- {g}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:30:46.053739Z","iopub.execute_input":"2025-12-10T15:30:46.054447Z","iopub.status.idle":"2025-12-10T15:30:46.858342Z","shell.execute_reply.started":"2025-12-10T15:30:46.054420Z","shell.execute_reply":"2025-12-10T15:30:46.857654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Debug đúng tên thuộc tính (phiên bản mới nhất)\ndef check_modality(tensor, name):\n    tensor = tensor.cpu() if tensor.is_cuda else tensor\n    has_nan = torch.isnan(tensor).any().item()\n    num_nan = torch.isnan(tensor).sum().item()\n    print(f\"\\n[{name}]\")\n    print(f\"  Shape : {tensor.shape}\")\n    print(f\"  Has NaN : {has_nan}\")\n    print(f\"  Num NaN : {num_nan:,}\")\n    print(f\"  Min   : {tensor.min().item():8.4f}\")\n    print(f\"  Max   : {tensor.max().item():8.4f}\")\n    print(f\"  Mean  : {tensor.mean().item():8.4f}\")\n    print(f\"  Std   : {tensor.std().item():8.4f}\")\n\nprint(\"Dataset loaded successfully!\")\ndataset.print()\n\nprint(\"\\n\" + \"=\"*70)\nprint(\"DATA STATISTICS (Final processed tensor)\")\nprint(\"=\"*70)\n\n# Lấy tensor cuối cùng đã xử lý (enhanced IMU + thermal + tof)\nfinal_tensor = dataset.data  # shape: [N, 256, 38]\n\nprint(f\"\\n[FINAL PROCESSED SEQUENCE]\")\nprint(f\" Shape : {final_tensor.shape}\")\nprint(f\" Has NaN : {torch.isnan(final_tensor).any().item()}\")\nprint(f\" Num NaN : {torch.isnan(final_tensor).sum().item():,}\")\nprint(f\" Min : {final_tensor.min().item():8.4f}\")\nprint(f\" Max : {final_tensor.max().item():8.4f}\")\nprint(f\" Mean : {final_tensor.mean().item():8.4f}\")\nprint(f\" Std : {final_tensor.std().item():8.4f}\")\n\n# Nếu muốn xem riêng từng phần (ước lượng)\nprint(f\"\\nEstimated channel breakdown:\")\nprint(f\"   → Enhanced IMU : ~18 channels (cols 0–17)\")\nprint(f\"   → Thermal      : 5 channels  (cols 18–22)\")\nprint(f\"   → TOF feats    : 15 channels (cols 23–37)\")\n\nprint(\"=\"*70)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:30:48.942666Z","iopub.execute_input":"2025-12-10T15:30:48.943479Z","iopub.status.idle":"2025-12-10T15:30:49.695926Z","shell.execute_reply.started":"2025-12-10T15:30:48.943442Z","shell.execute_reply":"2025-12-10T15:30:49.695097Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Model","metadata":{}},{"cell_type":"markdown","source":"## Linear","metadata":{}},{"cell_type":"code","source":"class SimpleNN(nn.Module):\n    def __init__(self, input_size, hidden_size, num_classes):\n        super(SimpleNN, self).__init__()\n        self.fc1 = nn.Linear(input_size, hidden_size)\n        self.relu = nn.ReLU()\n        self.fc2 = nn.Linear(hidden_size, num_classes)\n\n    def forward(self, x):\n        out = self.fc1(x)\n        out = self.relu(out)\n        out = self.fc2(out)\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:30:53.510832Z","iopub.execute_input":"2025-12-10T15:30:53.511491Z","iopub.status.idle":"2025-12-10T15:30:53.517504Z","shell.execute_reply.started":"2025-12-10T15:30:53.511458Z","shell.execute_reply":"2025-12-10T15:30:53.516629Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class ComplexNN(nn.Module):\n    def __init__(self, input_size, hidden_size, num_classes):\n        super(ComplexNN, self).__init__()\n        self.fc1 = nn.Linear(input_size, hidden_size)\n        self.relu = nn.ReLU()\n        self.fc2 = nn.Linear(hidden_size, hidden_size // 2)\n        self.relu2 = nn.ReLU()\n        self.fc3 = nn.Linear(hidden_size // 2, hidden_size // 4)\n        self.relu3 = nn.ReLU()\n        self.fc4 = nn.Linear(hidden_size // 4, hidden_size // 4)\n        self.relu4 = nn.ReLU()\n        self.fcout = nn.Linear(hidden_size // 4, num_classes)\n\n    def forward(self, x):\n        out = self.fc1(x)\n        out = self.relu(out)\n        out = self.fc2(out)\n        out = self.relu2(out)\n        out = self.fc3(out)\n        out = self.relu3(out)\n        out = self.fc4(out)\n        out = self.relu4(out)\n        out = self.fcout(out)\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:30:56.369078Z","iopub.execute_input":"2025-12-10T15:30:56.369757Z","iopub.status.idle":"2025-12-10T15:30:56.375774Z","shell.execute_reply.started":"2025-12-10T15:30:56.369734Z","shell.execute_reply":"2025-12-10T15:30:56.375204Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## LSTMs","metadata":{}},{"cell_type":"code","source":"class LSTMModel(nn.Module):\n    def __init__(self, input_size, hidden_size, num_layers, num_classes, dropout_rate=0.2):\n        super(LSTMModel, self).__init__()\n        self.hidden_size = hidden_size\n        self.num_layers = num_layers\n        \n        # Batch normalization dla danych wejściowych\n        self.batch_norm_input = nn.BatchNorm1d(input_size)\n        \n        # LSTM z dropout\n        self.lstm = nn.LSTM(\n            input_size, \n            hidden_size, \n            num_layers, \n            batch_first=True,\n            dropout=dropout_rate if num_layers > 1 else 0\n        )\n        \n        # Batch normalization po LSTM\n        self.batch_norm_hidden = nn.BatchNorm1d(hidden_size)\n        \n        # Dropout przed warstwą liniową\n        self.dropout = nn.Dropout(dropout_rate)\n        \n        # Warstwa liniowa\n        self.fc1 = nn.Linear(hidden_size, hidden_size)\n        self.relu1 = nn.ReLU()\n        self.fc2 = nn.Linear(hidden_size, hidden_size // 4)\n        self.relu2 = nn.ReLU()\n        self.dropout2 = nn.Dropout(dropout_rate)\n        self.fcout = nn.Linear(hidden_size // 4, num_classes)\n\n    def forward(self, x):\n\n        x = x.permute(0, 2, 1)  # (batch_size, input_size, seq_length)\n        # Normalizacja danych wejściowych\n        x = self.batch_norm_input(x)\n\n        x = x.permute(0, 2, 1)  # (batch_size, seq_length, input_size)\n        \n        # Forward LSTM\n        out, _ = self.lstm(x, None)  # None oznacza, że stany ukryte są inicjalizowane zerami\n        \n        # Bierzemy ostatni output\n        out = out[:, -1, :]\n        \n        # Normalizacja po LSTM\n        out = self.batch_norm_hidden(out)\n        \n        # Dropout\n        out = self.dropout(out)\n        \n        # Warstwa liniowa\n        out = self.fc1(out)\n        out = self.relu1(out)\n        out = self.fc2(out)\n        out = self.relu2(out)\n        out = self.dropout2(out)\n        out = self.fcout(out)\n        \n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:30:58.702662Z","iopub.execute_input":"2025-12-10T15:30:58.702923Z","iopub.status.idle":"2025-12-10T15:30:58.710399Z","shell.execute_reply.started":"2025-12-10T15:30:58.702898Z","shell.execute_reply":"2025-12-10T15:30:58.709678Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1D CNN","metadata":{}},{"cell_type":"code","source":"class CNN1D(nn.Module):\n    def __init__(self, input_size, num_classes):\n        super(CNN1D, self).__init__()\n\n        #Block in\n        self.convin = nn.Conv1d(in_channels=input_size, out_channels=512, kernel_size=7, padding=1)\n        self.bnin = nn.BatchNorm1d(512)\n        self.relubin = nn.ReLU()\n        self.poolin = nn.MaxPool1d(kernel_size=2)\n        self.dropoutin = nn.Dropout(0.2)\n        \n        #Block 1\n        self.conv1 = nn.Conv1d(in_channels=512, out_channels=768, kernel_size=5, padding=1)\n        self.bn1 = nn.BatchNorm1d(768)\n        self.relu1 = nn.ReLU()\n        self.pool1 = nn.MaxPool1d(kernel_size=2)\n        self.dropout1 = nn.Dropout(0.3)\n        \n        #Block 2\n        self.conv2 = nn.Conv1d(in_channels=768, out_channels=1024, kernel_size=3, padding=1)\n        self.bn2 = nn.BatchNorm1d(1024)\n        self.relu2 = nn.ReLU()\n        self.pool2 = nn.MaxPool1d(kernel_size=2)\n        self.dropout2 = nn.Dropout(0.3)\n        \n        #Block 3\n        self.conv3 = nn.Conv1d(in_channels=1024, out_channels=1536, kernel_size=3, padding=1)\n        self.bn3 = nn.BatchNorm1d(1536)\n        self.relu3 = nn.ReLU()\n        self.pool3 = nn.MaxPool1d(kernel_size=2)\n        self.dropout3 = nn.Dropout(0.4)\n\n        #Block 4\n        self.conv4 = nn.Conv1d(in_channels=1536, out_channels=2048, kernel_size=3, padding=1)\n        self.bn4 = nn.BatchNorm1d(2048)\n        self.relu4 = nn.ReLU()\n        self.pool4 = nn.AdaptiveAvgPool1d(1)\n        self.dropout4 = nn.Dropout(0.4)\n\n        #Dense layers\n        self.fc1 = nn.Linear(2048, 2048)\n        self.relu_fc1 = nn.ReLU()\n        self.dropout_fc1 = nn.Dropout(0.5)\n        \n        self.fc2 = nn.Linear(2048, 1024)\n        self.relu_fc2 = nn.ReLU()\n        self.dropout_fc2 = nn.Dropout(0.4)\n\n        self.fc3 = nn.Linear(1024, 512)\n        self.relu_fc3 = nn.ReLU()\n        self.dropout_fc3 = nn.Dropout(0.3)\n\n        self.fcout = nn.Linear(512, num_classes)\n\n\n    def forward(self, x):\n        # x shape: (batch_size, seq_length, input_size)\n        x = x.permute(0, 2, 1)  # Change to (batch_size, input_size, seq_length) for Conv1d\n\n        x = self.convin(x)\n        x = self.bnin(x)\n        x = self.relubin(x)\n        x = self.poolin(x)\n        x = self.dropoutin(x)\n        \n        x = self.conv1(x)\n        x = self.bn1(x)\n        x = self.relu1(x)\n        x = self.pool1(x)\n        x = self.dropout1(x)\n\n        x = self.conv2(x)\n        x = self.bn2(x)\n        x = self.relu2(x)\n        x = self.pool2(x)\n        x = self.dropout2(x)\n\n        x = self.conv3(x)\n        x = self.bn3(x)\n        x = self.relu3(x)\n        x = self.pool3(x)\n        x = self.dropout3(x)\n\n        x = self.conv4(x)\n        x = self.bn4(x)\n        x = self.relu4(x)\n        x = self.pool4(x)\n        x = self.dropout4(x)\n\n        x = x.squeeze(-1)\n\n        x = self.fc1(x)\n        x = self.relu_fc1(x)\n        x = self.dropout_fc1(x)\n\n        x = self.fc2(x)\n        x = self.relu_fc2(x)\n        x = self.dropout_fc2(x)\n\n        x = self.fc3(x)\n        x = self.relu_fc3(x)\n        x = self.dropout_fc3(x)\n\n        x = self.fcout(x)\n\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:04.309493Z","iopub.execute_input":"2025-12-10T15:31:04.310165Z","iopub.status.idle":"2025-12-10T15:31:04.321352Z","shell.execute_reply.started":"2025-12-10T15:31:04.310142Z","shell.execute_reply":"2025-12-10T15:31:04.320646Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1D CNN + LSTM + Attn","metadata":{}},{"cell_type":"code","source":"# Thermal Model (tương tự IMU, 1D sequence)\nclass ThermalCNNLSTMAttention(nn.Module):\n    def __init__(self, input_channels=5, num_classes=18, lstm_hidden=128, lstm_layers=1, bidirectional=False, attn_hidden=128, dropout=0.5):\n        super(ThermalCNNLSTMAttention, self).__init__()\n        # CNN feature extractor (giống IMU)\n        self.cnn = nn.Sequential(\n            nn.Conv1d(in_channels=input_channels, out_channels=512, kernel_size=7, padding=3),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(512),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n\n            nn.Conv1d(in_channels=512, out_channels=768, kernel_size=5, padding=2),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(768),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n\n            nn.Conv1d(in_channels=768, out_channels=1024, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(1024),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n\n            nn.Conv1d(in_channels=1024, out_channels=1536, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(1536),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n        )\n\n        # LSTM (giống IMU)\n        self.lstm = nn.LSTM(\n            input_size=1536,\n            batch_first=True,\n            bidirectional=bidirectional,\n            dropout=0.0 if lstm_layers == 1 else 0.1,\n            num_layers=lstm_layers,\n            hidden_size=lstm_hidden\n        )\n\n        self.dir_mult = 2 if bidirectional else 1\n        feat_dim = lstm_hidden * self.dir_mult\n\n        # Additive Attention (giống IMU)\n        self.attn_fc = nn.Linear(feat_dim, attn_hidden)\n        self.attn_v = nn.Linear(attn_hidden, 1, bias=False)\n\n        # MLP head\n        self.head = nn.Sequential(\n            nn.Dropout(0.5),\n            nn.Linear(feat_dim, 2048),\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.4),\n            nn.Linear(2048, 1024),\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.3),\n            nn.Linear(1024, 512),\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        x = x.permute(0, 2, 1)  # (N, L, C) --> (N, C, L)\n        x = self.cnn(x)\n        x = x.permute(0, 2, 1)  # (N, C_feat, L') --> (N, L', C_feat)\n        outputs, _ = self.lstm(x)   # (N, L', H*dirs)\n        attn_scores = torch.tanh(self.attn_fc(outputs))     # (N, L', attn_hidden)\n        attn_scores = self.attn_v(attn_scores).squeeze(-1)  # (N, L')\n        attn_weights = F.softmax(attn_scores, dim=1)\n        context = torch.bmm(attn_weights.unsqueeze(1), outputs).squeeze(1)  # (N, H*dirs)\n        logits = self.head(context) # (N, num_classes)\n        return logits\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:11.195181Z","iopub.execute_input":"2025-12-10T15:31:11.195479Z","iopub.status.idle":"2025-12-10T15:31:11.205160Z","shell.execute_reply.started":"2025-12-10T15:31:11.195456Z","shell.execute_reply":"2025-12-10T15:31:11.204496Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class CNNLSTMAttention(nn.Module):\n    def __init__(self, imu_channels, num_classes, lstm_hidden=256, lstm_layers=1, bidirectional=False, attn_hidden=128, dropout=0.3):\n        super(CNNLSTMAttention, self).__init__()\n\n        #CNN feature extractor\n        self.cnn = nn.Sequential(\n            nn.Conv1d(in_channels=imu_channels, out_channels=512, kernel_size=7, padding=3),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(512),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n\n            nn.Conv1d(in_channels=512, out_channels=768, kernel_size=5, padding=2),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(768),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n\n            nn.Conv1d(in_channels=768, out_channels=1024, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(1024),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n\n            nn.Conv1d(in_channels=1024, out_channels=1536, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(1536),\n            nn.MaxPool1d(2),\n            nn.Dropout(dropout),\n        )\n\n        #LSTM\n        self.lstm = nn.LSTM(\n            input_size=1536,\n            batch_first=True,\n            bidirectional=bidirectional,\n            dropout=0.0 if lstm_layers == 1 else 0.1,\n            num_layers=lstm_layers,\n            hidden_size=lstm_hidden\n        )\n\n        self.dir_mult = 2 if bidirectional else 1\n        feat_dim = lstm_hidden * self.dir_mult\n\n        #Additive Attention\n        self.attn_fc = nn.Linear(feat_dim, attn_hidden)\n        self.attn_v = nn.Linear(attn_hidden, 1, bias=False)\n\n        #MLP\n        self.head = nn.Sequential(\n            nn.Dropout(0.5),\n            nn.Linear(feat_dim, 2048),\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.4),\n            nn.Linear(2048, 1024),\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.3),\n            nn.Linear(1024, 512),\n            nn.ReLU(inplace=True),\n\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n\n    def forward(self, x):\n        # x: (N, L, C_in) --> (N, C_in, L)\n        x = x.permute(0, 2, 1)\n        x = self.cnn(x)\n        x = x.permute(0, 2, 1)\n        # x: (N, C_feat, L') --> (N, L', C_feat)\n\n        outputs, _ = self.lstm(x)   # (N, L', H*dirs)\n\n        # Attention\n        attn_scores = torch.tanh(self.attn_fc(outputs))     # (N, L', attn_hidden)\n        attn_scores = self.attn_v(attn_scores).squeeze(-1)  # (N, L')\n        attn_weights = F.softmax(attn_scores, dim=1)\n        context = torch.bmm(attn_weights.unsqueeze(1), outputs).squeeze(1)  # (N, H*dirs)\n\n        #MLP\n        logits = self.head(context) # (N, num_classes)\n\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:17.400739Z","iopub.execute_input":"2025-12-10T15:31:17.401028Z","iopub.status.idle":"2025-12-10T15:31:17.411262Z","shell.execute_reply.started":"2025-12-10T15:31:17.401005Z","shell.execute_reply":"2025-12-10T15:31:17.410628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# TOF Model (CNN2D, dựa trên file mẫu)\nclass TOFCNN2D(nn.Module):\n    def __init__(self, input_channels=5, num_classes=18, hidden=64, dropout=0.3):  # 5 sensors as channels\n        super(TOFCNN2D, self).__init__()\n        self.conv1 = nn.Conv3d(input_channels, 32, kernel_size=(1, 3, 3), padding=(0, 1, 1))  # Temporal dim=1 (per frame)\n        self.conv2 = nn.Conv3d(32, hidden, kernel_size=(1, 3, 3), padding=(0, 1, 1))\n        self.pool = nn.AdaptiveAvgPool3d((None, 1, 1))  # Pool spatial, keep temporal\n        self.dropout = nn.Dropout(dropout)\n        self.fc = nn.Linear(hidden, num_classes)\n\n    def forward(self, x):\n        # x: [batch, seq_len, 5, 8, 8] → [batch, 5, seq_len, 8, 8] (channels first for Conv3D)\n        x = x.permute(0, 2, 1, 3, 4)\n        x = F.relu(self.conv1(x))\n        x = F.relu(self.conv2(x))\n        x = self.pool(x).squeeze(-1).squeeze(-1)  # [batch, hidden, seq_len]\n        x = x.mean(dim=2)  # Mean temporal\n        x = self.dropout(x)\n        return self.fc(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:23.014034Z","iopub.execute_input":"2025-12-10T15:31:23.014632Z","iopub.status.idle":"2025-12-10T15:31:23.020801Z","shell.execute_reply.started":"2025-12-10T15:31:23.014606Z","shell.execute_reply":"2025-12-10T15:31:23.020097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class MultimodalCNNLSTMAttention(nn.Module):\n    def __init__(self, imu_channels=18, thermal_channels=5, tof_channels=15, \n                 num_classes=18, lstm_hidden=256, bidirectional=True):\n        super().__init__()\n        total_channels = imu_channels + thermal_channels + tof_channels  # 38\n\n        self.cnn = nn.Sequential(\n            # Layer 1\n            nn.Conv1d(total_channels, 64, kernel_size=7, padding=3),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(64),\n            nn.MaxPool1d(2),                    # → length: 256 → 128\n\n            # Layer 2 – SỬA TỪ 512 → 64\n            nn.Conv1d(64, 128, kernel_size=5, padding=2),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(128),\n            nn.MaxPool1d(2),                   # → length: 128 → 64\n\n            # Layer 3 – SỬA TỪ 768 → 128\n            nn.Conv1d(128, 256, kernel_size=3, padding=1),\n            nn.ReLU(inplace=True),\n            nn.BatchNorm1d(256),\n            nn.MaxPool1d(2),                    # → length: 64 → 32\n        )\n\n        # Sau 3 lần MaxPool(2) → sequence length = 256 / 8 = 32\n        self.lstm = nn.LSTM(256, lstm_hidden, num_layers=2, batch_first=True,\n                            bidirectional=bidirectional, dropout=0.3)\n\n        lstm_out_dim = lstm_hidden * (2 if bidirectional else 1)\n\n        self.attn = nn.MultiheadAttention(lstm_out_dim, num_heads=8, batch_first=True, dropout=0.1)\n\n        self.head = nn.Sequential(\n            nn.Dropout(0.5),\n            nn.Linear(lstm_out_dim, 512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.3),\n            nn.Linear(512, num_classes)\n        )\n        self.orientation_head = nn.Linear(lstm_out_dim, 4)\n\n    def forward(self, x):\n        assert x.shape[1:] == (256, 38), f\"Wrong shape: {x.shape}\"\n        \n        x = x.permute(0, 2, 1)          # [B, 38, 256]\n        x = self.cnn(x)                 # [B, 256, 32]\n        x = x.permute(0, 2, 1)          # [B, 32, 256]\n    \n        x, _ = self.lstm(x)             # [B, 32, 512]\n    \n        # === CHỈ CẦN 2 DÒNG NÀY LÀ XONG, KHÔNG CẦN attn_weights ===\n        x, _ = self.attn(x, x, x)       # Self-attention\n        x = x.mean(dim=1)               # Global average pooling → [B, 512]\n    \n        # Hoặc nếu muốn mạnh hơn tí:\n        # x = torch.cat([x.mean(1), x.max(1)[0]], dim=1)  # concat mean + max\n    \n        gesture_logits = self.head(x)\n        orientation_logits = self.orientation_head(x)\n        return gesture_logits, orientation_logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:27.605395Z","iopub.execute_input":"2025-12-10T15:31:27.605711Z","iopub.status.idle":"2025-12-10T15:31:27.614142Z","shell.execute_reply.started":"2025-12-10T15:31:27.605691Z","shell.execute_reply":"2025-12-10T15:31:27.613496Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Training loops","metadata":{}},{"cell_type":"code","source":"generator1 = torch.Generator().manual_seed(42)\ntrain_data, val_data = random_split(dataset, [0.8, 0.2], generator=generator1)\ndef get_dataloaders(train_data, val_data, batch_size=1024):\n    train_dataloader = DataLoader(\n            train_data, \n            batch_size=batch_size, \n            shuffle=True,  # Turn on shuffle for training data\n            num_workers= 0,  # Use multiple workers to load data\n            pin_memory=True  # Speed up transfer to GPU\n        )\n        \n    val_dataloader = DataLoader(\n            val_data, \n            batch_size=batch_size, \n            shuffle=False,  # Nie shuffle'uj danych walidacyjnych\n            num_workers= 0,\n            pin_memory=True\n        )\n    return train_dataloader, val_dataloader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:41.384785Z","iopub.execute_input":"2025-12-10T15:31:41.385059Z","iopub.status.idle":"2025-12-10T15:31:41.390644Z","shell.execute_reply.started":"2025-12-10T15:31:41.385038Z","shell.execute_reply":"2025-12-10T15:31:41.390040Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_training_metrics(predictions, labels):\n    accuracy = (predictions == labels).mean()\n    precision = precision_score(y_true=labels, y_pred=predictions, average='weighted', zero_division=0)\n    recall = recall_score(y_true=labels, y_pred=predictions, average='weighted', zero_division=0)\n    f1 = f1_score(y_true=labels, y_pred=predictions, average='weighted', zero_division=0)\n\n    # Add tp, fp, fn, tn calculations for each class\n    cm = confusion_matrix(labels, predictions)\n    tp = np.diag(cm)\n    fp = cm.sum(axis=0) - tp\n    fn = cm.sum(axis=1) - tp\n    tn = cm.sum() - (tp + fp + fn)\n\n    return accuracy, precision, recall, f1, cm, tp, fp, fn, tn, labels, predictions","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:44.001278Z","iopub.execute_input":"2025-12-10T15:31:44.002022Z","iopub.status.idle":"2025-12-10T15:31:44.007120Z","shell.execute_reply.started":"2025-12-10T15:31:44.001996Z","shell.execute_reply":"2025-12-10T15:31:44.006560Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_one_epoch_multimodal(loader, model, loss_fn, optimizer, epoch_idx):\n    model.train()\n    total_loss = 0.0\n    correct = 0\n    total = 0\n\n    for x, y in loader:  # x: [B, 256, 38], y: labels\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n\n        gesture_logits, _ = model(x)  # Multimodal trả về (gesture, orientation)\n        loss = loss_fn(gesture_logits, y)\n        loss = loss * 1.2\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=0.5)\n        optimizer.step()\n\n        total_loss += loss.item()\n        pred = gesture_logits.argmax(dim=1)\n        correct += (pred == y).sum().item()\n        total += y.size(0)\n\n    avg_loss = total_loss / len(loader)\n    acc = correct / total\n    return avg_loss, [acc]\n\n\ndef validate_multimodal(loader, model, loss_fn):\n    model.eval()\n    total_loss = 0.0\n    correct = 0\n    total = 0\n\n    with torch.no_grad():\n        for x, y in loader:\n            x, y = x.to(device), y.to(device)\n            gesture_logits, _ = model(x)\n            loss = loss_fn(gesture_logits, y)\n            total_loss += loss.item()\n\n            pred = gesture_logits.argmax(dim=1)\n            correct += (pred == y).sum().item()\n            total += y.size(0)\n\n    avg_loss = total_loss / len(loader)\n    acc = correct / total\n    return avg_loss, [acc], None\n\n\ndef train_one_epoch_imu_only(loader, model, loss_fn, optimizer, epoch_idx):\n    model.train()\n    total_loss = 0.0\n    correct = 0\n    total = 0\n\n    for x, y in loader:  # x: [B, 256, 18+]\n        x, y = x.to(device), y.to(device)\n        optimizer.zero_grad()\n\n        gesture_logits = model(x)  # IMU-only chỉ trả về 1 output\n        loss = loss_fn(gesture_logits, y)\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n        optimizer.step()\n\n        total_loss += loss.item()\n        pred = gesture_logits.argmax(dim=1)\n        correct += (pred == y).sum().item()\n        total += y.size(0)\n\n    avg_loss = total_loss / len(loader)\n    acc = correct / total\n    return avg_loss, [acc]\n\n\ndef validate_imu_only(loader, model, loss_fn):\n    model.eval()\n    total_loss = 0.0\n    correct = 0\n    total = 0\n\n    with torch.no_grad():\n        for x, y in loader:\n            x, y = x.to(device), y.to(device)\n            gesture_logits = model(x)\n            loss = loss_fn(gesture_logits, y)\n            total_loss += loss.item()\n\n            pred = gesture_logits.argmax(dim=1)\n            correct += (pred == y).sum().item()\n            total += y.size(0)\n\n    avg_loss = total_loss / len(loader)\n    acc = correct / total\n    return avg_loss, [acc], None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:49.515486Z","iopub.execute_input":"2025-12-10T15:31:49.516185Z","iopub.status.idle":"2025-12-10T15:31:49.526882Z","shell.execute_reply.started":"2025-12-10T15:31:49.516159Z","shell.execute_reply":"2025-12-10T15:31:49.526018Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class 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.DataFrame,\n        sub: pd.DataFrame\n    ) -> float:\n        # Validate gestures\n        invalid_types = {i for i in sub['gesture'].unique() if i not in self.all_classes}\n        if invalid_types:\n            raise ValueError(f\"Invalid gesture values in submission: {invalid_types}\")\n\n        # Compute binary F1 (Target vs Non-Target)\n        y_true_bin = sol['gesture'].isin(self.target_gestures).values\n        y_pred_bin = sub['gesture'].isin(self.target_gestures).values\n        f1_binary = f1_score(\n            y_true_bin,\n            y_pred_bin,\n            pos_label=True,\n            zero_division=0,\n            average='binary'\n        )\n\n        # Build multi-class labels for gestures\n        y_true_mc = sol['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n\n        # Compute macro F1 over all gesture classes\n        f1_macro = f1_score(\n            y_true_mc,\n            y_pred_mc,\n            average='macro',\n            zero_division=0\n        )\n\n        return 0.5 * f1_binary + 0.5 * f1_macro","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:56.406262Z","iopub.execute_input":"2025-12-10T15:31:56.406835Z","iopub.status.idle":"2025-12-10T15:31:56.413273Z","shell.execute_reply.started":"2025-12-10T15:31:56.406810Z","shell.execute_reply":"2025-12-10T15:31:56.412660Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def validate(val_loader: DataLoader, model: nn.Module, loss_fn: nn):\n    running_loss = 0.0\n    n_in_window = 0\n    all_predictions = []\n    all_labels = []\n    all_probabilities = []\n    \n    with torch.no_grad():\n        for i, data in enumerate(val_loader):\n            #inputs, labels = data\n            #inputs, labels = inputs.to(device), labels.to(device)\n\n            imu, thermal, tof, labels = data\n            imu, thermal, tof, labels = imu.to(device), thermal.to(device), tof.to(device), labels.to(device)\n            outputs = model(imu, thermal, tof)\n            #outputs = model(inputs)\n            if outputs.dim() == 2:\n                logits = outputs\n            else:\n                logits = outputs.permute(0, 2, 1).mean(dim=2)  # Adjust dimensions for CrossEntropyLoss\n            loss = loss_fn(logits, labels)\n\n            running_loss += loss.item()\n            n_in_window += 1\n\n            probabilities = torch.softmax(logits, dim=1)\n            _, predicted = torch.max(logits.data, 1)\n            all_predictions.extend(predicted.cpu().numpy())\n            all_labels.extend(labels.cpu().numpy())\n            all_probabilities.extend(probabilities.cpu().numpy())\n\n    # Calculate standard metrics\n    all_predictions = np.array(all_predictions)\n    all_labels = np.array(all_labels)\n    all_probabilities = np.array(all_probabilities)\n    metrics = calculate_training_metrics(all_predictions, all_labels)\n    \n    avg_loss = running_loss / n_in_window\n    print(f\"  Validation - loss: {avg_loss:.6f}, accuracy: {metrics[0]*100:.2f}%, precision: {metrics[1]:.4f}, recall: {metrics[2]:.4f}, f1: {metrics[3]:.4f}\")\n\n    return avg_loss, [acc, f1], None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:31:59.173854Z","iopub.execute_input":"2025-12-10T15:31:59.174594Z","iopub.status.idle":"2025-12-10T15:31:59.181148Z","shell.execute_reply.started":"2025-12-10T15:31:59.174567Z","shell.execute_reply":"2025-12-10T15:31:59.180602Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class EarlyStopping_Accuracy:\n    \"\"\"Early stops the training if validation accuracy doesn't improve after a given patience.\"\"\"\n    def __init__(self, patience=7, verbose=False, delta=0, path='checkpoint.pt'):\n        \"\"\"\n        Args:\n            patience (int): How long to wait after last time validation accuracy improved.\n                            Default: 7\n            verbose (bool): If True, prints a message for each validation accuracy improvement. \n                            Default: False\n            delta (float): Minimum change in the monitored quantity to qualify as an improvement.\n                            Default: 0\n            path (str): Path for the checkpoint to be saved to.\n                            Default: 'checkpoint.pt'\n        \"\"\"\n        self.patience = patience\n        self.verbose = verbose\n        self.counter = 0\n        self.best_score = None\n        self.early_stop = False\n        self.val_accuracy_max = 0.0\n        self.delta = delta\n        self.path = path\n\n    def __call__(self, val_accuracy, model):\n\n        score = val_accuracy\n\n        if self.best_score is None:\n            self.best_score = score\n            self.save_checkpoint(val_accuracy, model)\n        elif score < self.best_score + self.delta:\n            self.counter += 1\n            if self.verbose:\n                print(f'EarlyStopping counter: {self.counter} out of {self.patience}')\n            if self.counter >= self.patience:\n                self.early_stop = True\n        else:\n            self.best_score = score\n            self.save_checkpoint(val_accuracy, model)\n            self.counter = 0\n\n    def save_checkpoint(self, val_accuracy, model):\n        if self.verbose:\n            if self.best_score is None:\n                print(f'First improvement: Validation accuracy {val_accuracy:.6f}. Saving model...')\n            else:\n                print(f'Validation accuracy increased ({self.best_score:.6f} --> {val_accuracy:.6f}). Saving model...')\n        \n        torch.save(model.state_dict(), self.path)\n        self.val_accuracy_max = val_accuracy  # vẫn giữ để tương thích nếu ai dùng\n        # Quan trọng: cập nhật cả best_score (dù đã làm ở __call__ rồi nhưng an toàn thì cập nhật luôn)\n        self.best_score = val_accuracy","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:01.996511Z","iopub.execute_input":"2025-12-10T15:32:01.997185Z","iopub.status.idle":"2025-12-10T15:32:02.003869Z","shell.execute_reply.started":"2025-12-10T15:32:01.997158Z","shell.execute_reply":"2025-12-10T15:32:02.003281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def calculate_metrics(predictions, labels):\n\n    precision = precision_score(labels, predictions, average='weighted')\n    recall = recall_score(labels, predictions, average='weighted')\n    f1 = f1_score(labels, predictions, average='weighted')\n    cm = confusion_matrix(labels, predictions)\n\n    return precision, recall, f1, cm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:04.877476Z","iopub.execute_input":"2025-12-10T15:32:04.878173Z","iopub.status.idle":"2025-12-10T15:32:04.881968Z","shell.execute_reply.started":"2025-12-10T15:32:04.878131Z","shell.execute_reply":"2025-12-10T15:32:04.881284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_loop_multimodal(\n    train_data: Subset,\n    val_data: Subset,\n    model: nn.Module,\n    loss_fn: nn.Module,\n    optimizer: torch.optim.Optimizer,\n    scheduler: torch.optim.lr_scheduler._LRScheduler,\n    name: str,\n    patience: int = 10,\n    epochs: int = 100,\n    batch_size: int = 128,\n    warmup_epochs: int = 0,\n    warmup_lr: float = 1e-5\n):\n    os.makedirs(\"/kaggle/working/models\", exist_ok=True)\n\n    # TỰ ĐỘNG CHỌN DỰA VÀO TÊN MODEL – KHÔNG CẦN TRUYỀN THÊM THAM SỐ!\n    is_imu_only = 'imu_only' in name.lower()\n\n    \n    best_acc_path  = '/kaggle/working/models/best_accuracy_multimodal.pt'\n    best_loss_path = '/kaggle/working/models/best_loss_multimodal.pt'\n\n    early_stopping = EarlyStopping_Accuracy(patience=patience, verbose=True, path=best_acc_path)\n    train_loader, val_loader = get_dataloaders(train_data, val_data, batch_size=batch_size)\n\n    best_vloss = float('inf')\n    best_vloss_epoch = -1\n\n    print(f\"\\nBẮT ĐẦU TRAIN: {name.upper()}\")\n    print(f\" → Epochs: {epochs} | Batch: {batch_size} | Patience: {patience}\")\n    print(f\" → Model sẽ được lưu tại: {best_acc_path}\")\n    print(\"-\" * 80)\n\n    for epoch in range(epochs):\n        # TRAIN\n        model.train()\n        avg_loss, train_metrics = train_one_epoch_multimodal(train_loader, model, loss_fn, optimizer, epoch)\n\n        # VALIDATE\n        model.eval()\n        avg_vloss, val_metrics, _ = validate_multimodal(val_loader, model, loss_fn)\n\n        # Scheduler\n        current_lr = optimizer.param_groups[0]['lr']\n        if epoch < warmup_epochs and warmup_epochs > 0:\n            warmup_lr = (epoch + 1) / warmup_epochs * optimizer.defaults['lr']\n            for g in optimizer.param_groups:\n                g['lr'] = warmup_lr\n            current_lr = warmup_lr\n        else:\n            scheduler.step(val_metrics[0])\n\n        # In kết quả\n        print(f\"EPOCH {epoch+1:3d} | \"\n              f\"Train Loss: {avg_loss:.4f} | Val Loss: {avg_vloss:.4f} | \"\n              f\"Val Acc: {val_metrics[0]:.4f} | LR: {current_lr:.2e}\")\n        # Lưu best loss\n        if avg_vloss < best_vloss:\n            best_vloss = avg_vloss\n            best_vloss_epoch = epoch\n            torch.save(model.state_dict(), best_loss_path)\n            print(f\" → NEW BEST VAL LOSS: {best_vloss:.6f} → saved!\")\n\n        # Early stopping\n        early_stopping(val_metrics[0], model)\n        if early_stopping.early_stop:\n            print(f\"EARLY STOPPING tại epoch {epoch+1}\")\n            break\n            \n    print(f\"\\nHOÀN THÀNH TRAIN: {name.upper()}\")\n    print(f\"Best val loss: {best_vloss:.6f} tại epoch {best_vloss_epoch + 1}\")\n    print(f\"Model đã lưu: {best_acc_path}\")\n    print(\"=\" * 80)\n\n    return {\"model\": model, \"best_val_loss\": best_vloss}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:06.826008Z","iopub.execute_input":"2025-12-10T15:32:06.826645Z","iopub.status.idle":"2025-12-10T15:32:06.835401Z","shell.execute_reply.started":"2025-12-10T15:32:06.826618Z","shell.execute_reply":"2025-12-10T15:32:06.834618Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\ndef train_loop_imu_only(\n    train_data: Subset,\n    val_data: Subset,\n    model: nn.Module,\n    loss_fn: nn.Module,\n    optimizer: torch.optim.Optimizer,\n    scheduler: torch.optim.lr_scheduler._LRScheduler,\n    name: str = \"imu_only_v1\",\n    patience: int = 15,\n    epochs: int = 100,\n    batch_size: int = 256,\n    warmup_epochs: int = 3,\n    warmup_lr: float = 1e-5\n):\n    timestamp = datetime.now().strftime('%Y-%m-%d_%H-%M-%S')\n    os.makedirs(\"/kaggle/working/models\", exist_ok=True)\n\n    # Đường dẫn lưu model IMU-only\n    best_acc_path  = '/kaggle/working/models/best_accuracy_imu_only.pt'\n    best_loss_path = '/kaggle/working/models/best_loss_imu_only.pt'\n\n    early_stopping = EarlyStopping_Accuracy(patience=patience, verbose=True, path=best_acc_path)\n    train_loader, val_loader = get_dataloaders(train_data, val_data, batch_size=batch_size)\n\n    best_vloss = float('inf')\n    best_vloss_epoch = -1\n\n    print(f\"\\nBẮT ĐẦU TRAIN IMU-ONLY MODEL\")\n    print(f\" → Epochs: {epochs} | Batch: {batch_size} | Patience: {patience}\")\n    print(f\" → Model sẽ được lưu tại: {best_acc_path}\")\n    print(\"-\" * 80)\n\n    for epoch in range(epochs):\n        # ================= TRAIN =================\n        model.train()\n        total_loss = 0.0\n        correct = 0\n        total = 0\n\n        for x, y in train_loader:\n            x, y = x.to(device), y.to(device)\n            optimizer.zero_grad()\n\n            # IMU-only model chỉ trả về 1 output\n            gesture_logits = model(x)           # ← KHÔNG unpack tuple!\n            loss = loss_fn(gesture_logits, y)\n            loss.backward()\n            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)\n            optimizer.step()\n\n            total_loss += loss.item()\n            pred = gesture_logits.argmax(dim=1)\n            correct += (pred == y).sum().item()\n            total += y.size(0)\n\n        avg_loss = total_loss / len(train_loader)\n        train_acc = correct / total\n\n        # ================= VALIDATE =================\n        model.eval()\n        val_loss = 0.0\n        val_correct = 0\n        val_total = 0\n\n        with torch.no_grad():\n            for x, y in val_loader:\n                x, y = x.to(device), y.to(device)\n                gesture_logits = model(x)\n                loss = loss_fn(gesture_logits, y)\n                val_loss += loss.item()\n\n                pred = gesture_logits.argmax(dim=1)\n                val_correct += (pred == y).sum().item()\n                val_total += y.size(0)\n\n        avg_vloss = val_loss / len(val_loader)\n        val_acc = val_correct / val_total\n\n        # ================= SCHEDULER & WARMUP =================\n        current_lr = optimizer.param_groups[0]['lr']\n        if epoch < warmup_epochs and warmup_epochs > 0:\n            lr = warmup_lr + (epoch / warmup_epochs) * (optimizer.defaults['lr'] - warmup_lr)\n            for g in optimizer.param_groups:\n                g['lr'] = lr\n            current_lr = lr\n        else:\n            scheduler.step(val_acc)\n\n        # ================= LOG =================\n        print(f\"EPOCH {epoch+1:3d} | \"\n              f\"Train Loss: {avg_loss:.4f} | Val Loss: {avg_vloss:.4f} | \"\n              f\"Val Acc: {val_acc:.4f} | LR: {current_lr:.2e}\")\n\n        # Save best loss\n        if avg_vloss < best_vloss:\n            best_vloss = avg_vloss\n            best_vloss_epoch = epoch\n            torch.save(model.state_dict(), best_loss_path)\n            print(f\" → NEW BEST VAL LOSS: {best_vloss:.6f} → saved!\")\n\n        # Early stopping\n        early_stopping(val_acc, model)\n        if early_stopping.early_stop:\n            print(f\"EARLY STOPPING tại epoch {epoch+1}\")\n            break\n            \n\n    print(f\"\\nHOÀN THÀNH TRAIN IMU-ONLY MODEL\")\n    print(f\"Best val loss: {best_vloss:.6f} tại epoch {best_vloss_epoch + 1}\")\n    print(f\"Model đã lưu: {best_acc_path}\")\n    print(\"=\" * 80)\n\n    return {\"model\": model, \"best_val_loss\": best_vloss}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:10.432691Z","iopub.execute_input":"2025-12-10T15:32:10.433384Z","iopub.status.idle":"2025-12-10T15:32:10.444701Z","shell.execute_reply.started":"2025-12-10T15:32:10.433356Z","shell.execute_reply":"2025-12-10T15:32:10.443822Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, f1_score, classification_report\nfrom cmi_2025_metric_copy_for_import import CompetitionMetric\nimport numpy as np\n\ndef evaluate_model(model, val_data, batch_size=128):\n    model.eval()\n    val_loader = DataLoader(val_data, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)\n\n    all_preds = []\n    all_labels = []\n    all_probs = []\n\n    print(\"Evaluating on validation set...\")\n    with torch.no_grad():\n        for x, y in val_loader:  # ← CHỈ 2 GIÁ TRỊ: x (38 channels) và y\n            x, y = x.to(device), y.to(device)\n            gesture_logits, _ = model(x)\n            probs = torch.softmax(gesture_logits, dim=1)\n            preds = probs.argmax(dim=1)\n\n            all_preds.extend(preds.cpu().numpy())\n            all_labels.extend(y.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    all_preds = np.array(all_preds)\n    all_labels = np.array(all_labels)\n    all_probs = np.array(all_probs)\n\n    # Standard metrics\n    acc = accuracy_score(all_labels, all_preds)\n    f1_weighted = f1_score(all_labels, all_preds, average='weighted')\n    f1_macro = f1_score(all_labels, all_preds, average='macro')\n\n    print(f\"Validation Accuracy: {acc:.4f}\")\n    print(f\"Validation F1-weighted: {f1_weighted:.4f}\")\n    print(f\"Validation F1-macro: {f1_macro:.4f}\")\n\n    # Competition metric (Hierarchical F1)\n    metric = CompetitionMetric()\n    pred_gestures = dataset.label_encoder.inverse_transform(all_preds)\n    true_gestures = dataset.label_encoder.inverse_transform(all_labels)\n\n    val_submission = pd.DataFrame({'gesture': pred_gestures})\n    val_solution = pd.DataFrame({'gesture': true_gestures})\n    competition_score = metric.calculate_hierarchical_f1(val_solution, val_submission)\n\n    print(f\"\\nHIERARCHICAL F1 (OFFICIAL): {competition_score:.6f}\")\n\n    return {\n        'accuracy': acc,\n        'f1_weighted': f1_weighted,\n        'f1_macro': f1_macro,\n        'competition_score': competition_score,\n        'predictions': all_preds,\n        'probabilities': all_probs,\n        'true_labels': all_labels\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:13.611345Z","iopub.execute_input":"2025-12-10T15:32:13.611961Z","iopub.status.idle":"2025-12-10T15:32:13.619913Z","shell.execute_reply.started":"2025-12-10T15:32:13.611931Z","shell.execute_reply":"2025-12-10T15:32:13.619160Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import accuracy_score, f1_score\nfrom cmi_2025_metric_copy_for_import import CompetitionMetric\nimport numpy as np\n\ndef evaluate_imu_only(model, val_data, batch_size=256):\n    \"\"\"\n    Hàm đánh giá dành riêng cho IMU-only model.\n    - Model chỉ trả về 1 output (gesture_logits)\n    - Dùng imu_only_dataset.label_encoder (đúng dataset bạn train)\n    \"\"\"\n    model.eval()\n    val_loader = DataLoader(val_data, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True)\n\n    all_preds = []\n    all_labels = []\n    all_probs = []\n\n    print(\"Evaluating IMU-only model on validation set...\")\n    with torch.no_grad():\n        for x, y in val_loader:\n            x, y = x.to(device), y.to(device)\n\n            # IMU-only model chỉ trả về 1 tensor → KHÔNG UNPACK!\n            logits = model(x)                    # ← Đây là điểm khác biệt quan trọng\n            probs = torch.softmax(logits, dim=1)\n            preds = probs.argmax(dim=1)\n\n            all_preds.extend(preds.cpu().numpy())\n            all_labels.extend(y.cpu().numpy())\n            all_probs.extend(probs.cpu().numpy())\n\n    all_preds = np.array(all_preds)\n    all_labels = np.array(all_labels)\n\n    # === Các metric cơ bản ===\n    acc = accuracy_score(all_labels, all_preds)\n    f1_weighted = f1_score(all_labels, all_preds, average='weighted')\n    f1_macro = f1_score(all_labels, all_preds, average='macro')\n\n    print(f\"Validation Accuracy       : {acc:.4f}\")\n    print(f\"Validation F1-weighted    : {f1_weighted:.4f}\")\n    print(f\"Validation F1-macro       : {f1_macro:.4f}\")\n\n    # === Competition metric (Hierarchical F1) ===\n    # Dùng đúng label_encoder của imu_only_dataset\n    pred_gestures = imu_only_dataset.label_encoder.inverse_transform(all_preds)\n    true_gestures = imu_only_dataset.label_encoder.inverse_transform(all_labels)\n\n    val_submission = pd.DataFrame({'gesture': pred_gestures})\n    val_solution   = pd.DataFrame({'gesture': true_gestures})\n\n    metric = CompetitionMetric()\n    competition_score = metric.calculate_hierarchical_f1(val_solution, val_submission)\n\n    print(f\"\\nHIERARCHICAL F1 (OFFICIAL): {competition_score:.6f}\")\n\n    return {\n        'accuracy': acc,\n        'f1_weighted': f1_weighted,\n        'f1_macro': f1_macro,\n        'competition_score': competition_score,\n        'predictions': all_preds,\n        'true_labels': all_labels,\n        'probabilities': np.array(all_probs)\n    }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:17.487001Z","iopub.execute_input":"2025-12-10T15:32:17.487269Z","iopub.status.idle":"2025-12-10T15:32:17.495840Z","shell.execute_reply.started":"2025-12-10T15:32:17.487249Z","shell.execute_reply":"2025-12-10T15:32:17.495104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_pr_curve(y_true, probabilities, class_names=None):\n    \"\"\"\n    Plots the Precision-Recall (PR) curves for each class.\n    \n    Args:\n    y_true: true labels\n    probabilities: probability matrix for each class (softmax score)\n    class_names: optional list of class names\n    \"\"\"\n    from sklearn.metrics import precision_recall_curve\n    import matplotlib.pyplot as plt\n    \n    num_classes = probabilities.shape[1]\n    \n    for i in range(num_classes):\n        class_name = class_names[i] if class_names is not None else f\"class_{i}\"\n        \n        # Convert labels to binary format (1 for a given class, 0 for the rest)\n        y_true_binary = (y_true == i).astype(np.int32)\n        \n        # Get probabilities for a given class\n        class_probs = probabilities[:, i]\n        \n        # Calculate precision and recall for different thresholds\n        precision, recall, thresholds = precision_recall_curve(y_true_binary, class_probs)\n        \n        # Draw the PR curve\n        plt.figure(figsize=(8, 6))\n        plt.plot(recall, precision, label=f'Class {class_name}')\n        plt.xlabel('Recall')\n        plt.ylabel('Precision')\n        plt.title(f'Precision-Recall Curve - {class_name}')\n        plt.grid(True)\n        plt.legend()\n        \n        # Save the graph to a buffer and add it to TensorBoard\n        buf = io.BytesIO()\n        plt.savefig(buf, format='png')\n        buf.seek(0)\n        \n        # Convert buffer to tensor\n        img = Image.open(buf)\n        img_tensor = torch.from_numpy(np.array(img))\n        img_tensor = img_tensor.permute(2, 0, 1)  # HWC -> CHW\n    \n        \n        plt.close()\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:20.645951Z","iopub.execute_input":"2025-12-10T15:32:20.646243Z","iopub.status.idle":"2025-12-10T15:32:20.652547Z","shell.execute_reply.started":"2025-12-10T15:32:20.646215Z","shell.execute_reply":"2025-12-10T15:32:20.651785Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train","metadata":{}},{"cell_type":"code","source":"\nmodel = MultimodalCNNLSTMAttention(\n   imu_channels=18,        # ← ĐÚNG: enhanced IMU ~18 features\n    thermal_channels=5,     # ← Thermal luôn 5\n    tof_channels=15,        # ← TOF reduced: mean + std + valid = 15\n    num_classes=len(dataset.label_encoder.classes_),\n    lstm_hidden=256,        # Tăng nhẹ để mạnh hơn với 38 channels\n    bidirectional=True\n)\nmodel.to(device)\nprint(model)\ntotal_params = sum(p.numel() for p in model.parameters())\ntrainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"Total parameters: {total_params:,}\")\nprint(f\"Trainable parameters: {trainable_params:,}\")\nprint(f\"Input shape expected: [B, 256, 38] → YES! Matches dataset.data.shape\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:26.720712Z","iopub.execute_input":"2025-12-10T15:32:26.720997Z","iopub.status.idle":"2025-12-10T15:32:26.768010Z","shell.execute_reply.started":"2025-12-10T15:32:26.720974Z","shell.execute_reply":"2025-12-10T15:32:26.767215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pytorch_total_params = sum(p.numel() for p in model.parameters())\nprint(f\"Total model parameters: {pytorch_total_params:,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:30.384756Z","iopub.execute_input":"2025-12-10T15:32:30.385098Z","iopub.status.idle":"2025-12-10T15:32:30.390225Z","shell.execute_reply.started":"2025-12-10T15:32:30.385070Z","shell.execute_reply":"2025-12-10T15:32:30.389411Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pytorch_trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\nprint(f\"Trainable model parameters: {pytorch_trainable_params:,}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:32.646568Z","iopub.execute_input":"2025-12-10T15:32:32.647052Z","iopub.status.idle":"2025-12-10T15:32:32.651068Z","shell.execute_reply.started":"2025-12-10T15:32:32.647025Z","shell.execute_reply":"2025-12-10T15:32:32.650322Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"name = 'CMI_Multimodal_CNNLSTMAttn_v1'\n\n# 1. Loss với Label Smoothing – cực kỳ quan trọng với 18 lớp không cân bằng!\nloss_fn = nn.CrossEntropyLoss().to(device)\n\n\noptimizer = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-4)\n\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='max', factor=0.5, patience=8, verbose=True  # từ 5 → 8\n)\n\n# 4. Khởi tạo trọng số tốt nhất cho CNN + LSTM + Attention\ndef init_weights(m):\n    if isinstance(m, (nn.Conv1d, nn.Linear)):\n        # Dùng fan_in + scale nhỏ hơn cho safety\n        nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')\n        if m.bias is not None:\n            nn.init.constant_(m.bias, 0)\n        # QUAN TRỌNG: Giảm scale cho head classifier\n        if isinstance(m, nn.Linear) and m.out_features == 18:\n            nn.init.normal_(m.weight, mean=0.0, std=0.1)  # Rất nhỏ cho lớp cuối\n            nn.init.constant_(m.bias, 0)\n\n    elif isinstance(m, nn.LSTM):\n        for name, param in m.named_parameters():\n            if 'weight_ih' in name:\n                nn.init.xavier_uniform_(param, gain=1.0)\n            elif 'weight_hh' in name:\n                nn.init.orthogonal_(param)\n            elif 'bias' in name:\n                nn.init.constant_(param, 0)\n                # Chỉ set forget bias = 1 cho layer 0, layer 1 để nhẹ hơn\n                n = param.size(0)\n                param.data[n//4:n//2].fill_(1.3)  # Giảm từ 1.0 xuống 0.3\n\n    elif isinstance(m, nn.MultiheadAttention):\n        if m.in_proj_weight is not None:\n            nn.init.xavier_uniform_(m.in_proj_weight, gain=0.5)  # Giảm gain\n        if m.in_proj_bias is not None:\n            nn.init.constant_(m.in_proj_bias, 0)\n        if hasattr(m.out_proj, 'weight') and m.out_proj.weight is not None:\n            nn.init.xavier_uniform_(m.out_proj.weight, gain=0.5)\n\n    elif isinstance(m, nn.BatchNorm1d):\n        nn.init.constant_(m.weight, 1.0)\n        nn.init.constant_(m.bias, 0.0)\n\nprint(\"Initializing model weights...\")\nmodel = MultimodalCNNLSTMAttention(\n    imu_channels=18, thermal_channels=5, tof_channels=15,\n    num_classes=18, lstm_hidden=256, bidirectional=True\n).to(device)\n\nmodel.apply(init_weights)   # ← BÂY GIỜ SẼ CHẠY MƯỢT\nprint(\"INIT WEIGHTS HOÀN TẤT – FORGET BIAS = 1 + ATTENTION XAVIER!\")\n\nprint(f\"\\nModel: {name}\")\nprint(f\"Total parameters: {sum(p.numel() for p in model.parameters()):,}\")\nprint(f\"Total parameters: {sum(p.numel() for p in model.parameters()):,}\")\nprint(f\"Trainable parameters: {sum(p.numel() for p in model.parameters() if p.requires_grad):,}\")\nprint(f\"Loss: CrossEntropy + LabelSmoothing(0.1)\")\nprint(f\"Scheduler: ReduceLROnPlateau (patience=5, factor=0.5)\")\nprint(f\"Device: {device}\")\nprint(\"-\" * 80)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:21:09.044373Z","iopub.execute_input":"2025-12-10T16:21:09.044989Z","iopub.status.idle":"2025-12-10T16:21:09.100119Z","shell.execute_reply.started":"2025-12-10T16:21:09.044966Z","shell.execute_reply":"2025-12-10T16:21:09.099492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imu_model = CNNLSTMAttention(\n    imu_channels=18,    \n    num_classes=len(dataset.label_encoder.classes_),\n    lstm_hidden=384,\n    bidirectional=True\n).to(device)\n\nname_imu = 'CMI_IMUOnly_CNNLSTMAttn_v1'\n\n# 1. Loss cho IMU-only (ít smoothing hơn)\nloss_fn_imu = nn.CrossEntropyLoss(label_smoothing=0.05).to(device)\n\n# 2. Optimizer cho IMU-only (học nhanh hơn)\nlearning_rate_imu = 4e-4  # Cao hơn vì model đơn giản\noptimizer_imu = torch.optim.AdamW(\n    imu_model.parameters(),\n    lr=learning_rate_imu,\n    weight_decay=5e-5,  # Thấp hơn vì ít overfit risk\n    betas=(0.9, 0.999),\n    eps=1e-8\n)\n\n# 3. Scheduler cho IMU-only\nscheduler_imu = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer_imu,\n    mode='max', patience=5, factor=0.6, min_lr=1e-6, verbose=True\n)\n\n# 4. Khởi tạo trọng số cho IMU-only\ndef init_weights_imu(m):\n    if isinstance(m, (nn.Conv1d, nn.Linear)):\n        nn.init.kaiming_normal_(m.weight, mode='fan_in', nonlinearity='relu')\n        if m.bias is not None:\n            nn.init.zeros_(m.bias)\n    elif isinstance(m, nn.LSTM):\n        for name, param in m.named_parameters():\n            if 'weight_ih' in name:\n                nn.init.xavier_uniform_(param)\n            elif 'weight_hh' in name:\n                nn.init.orthogonal_(param)\n            elif 'bias' in name:\n                nn.init.zeros_(param)\n    elif isinstance(m, nn.BatchNorm1d):\n        nn.init.ones_(m.weight)\n        nn.init.zeros_(m.bias)\n\nprint(\"Initializing IMU-only model weights...\")\nimu_model.apply(init_weights_imu)\nimu_model.to(device)\nprint(imu_model)\nprint(f\"\\n{'='*60}\")\nprint(f\"IMU-ONLY MODEL CONFIGURATION\")\nprint(f\"{'='*60}\")\nprint(f\"Total parameters: {sum(p.numel() for p in imu_model.parameters()):,}\")\nprint(f\"Learning rate: {learning_rate_imu}\")\nprint(f\"Weight decay: {optimizer_imu.param_groups[0]['weight_decay']}\")\nprint(f\"{'='*60}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:38.367066Z","iopub.execute_input":"2025-12-10T15:32:38.367561Z","iopub.status.idle":"2025-12-10T15:32:38.520265Z","shell.execute_reply.started":"2025-12-10T15:32:38.367538Z","shell.execute_reply":"2025-12-10T15:32:38.519688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"imu_only_dataset = CMIIMUOnlyDataset(train_df, max_len=256)\ntrain_imu, val_imu = random_split(imu_only_dataset, [int(0.8*len(imu_only_dataset)), len(imu_only_dataset)-int(0.8*len(imu_only_dataset))])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:32:45.511249Z","iopub.execute_input":"2025-12-10T15:32:45.511864Z","iopub.status.idle":"2025-12-10T15:34:03.221560Z","shell.execute_reply.started":"2025-12-10T15:32:45.511839Z","shell.execute_reply":"2025-12-10T15:34:03.220865Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:21:17.389225Z","iopub.execute_input":"2025-12-10T16:21:17.389547Z","iopub.status.idle":"2025-12-10T16:21:17.393705Z","shell.execute_reply.started":"2025-12-10T16:21:17.389524Z","shell.execute_reply":"2025-12-10T16:21:17.393095Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# (Khởi chạy quá trình huấn luyện) – PHIÊN BẢN ĐÚNG NHẤT!\n'''\nprint(\"\\nBắt đầu train MULTIMODAL model...\")\nresults = train_loop_multimodal(\n    model=model,\n    train_data=train_data,\n    val_data=val_data,\n    loss_fn=loss_fn,\n    optimizer=optimizer,\n    scheduler=scheduler,\n    name=\"multimodal_v1\",   # ← TỰ ĐỘNG CHỌN multimodal\n    patience=20,\n    epochs=120,\n    batch_size=128\n)\n\nprint(\"\\nFinal evaluation of the model:\")\nevaluation = evaluate_model(model, val_data, batch_size=128)\n\n'''","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:21:50.978059Z","iopub.execute_input":"2025-12-10T16:21:50.978451Z","iopub.status.idle":"2025-12-10T16:25:16.366137Z","shell.execute_reply.started":"2025-12-10T16:21:50.978427Z","shell.execute_reply":"2025-12-10T16:25:16.365455Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n'''\nprint(\"\\nBắt đầu train IMU-ONLY model...\")\nresults_imu = train_loop_imu_only(\n    model=imu_model,\n    train_data=train_imu,\n    val_data=val_imu,\n    loss_fn=loss_fn_imu,\n    optimizer=optimizer_imu,\n    scheduler=scheduler_imu,\n    name=\"imu_only_v1\",     # ← TỰ ĐỘNG CHỌN imu_only\n    patience=15,\n    epochs=120,\n    batch_size=256\n)\n\nprint(\"\\nFinal evaluation IMU-only:\")\neval_imu = evaluate_imu_only(imu_model, val_imu, batch_size=256)  # dùng val_imu cho chính xác\n\nprint(f\"\\n{'='*70}\")\nprint(f\"TRAINING SUMMARY – HOÀN THÀNH 100%\")\nprint(f\"{'='*70}\")\ntry:\n    print(f\"Multimodal Hierarchical F1 : {evaluation.get('competition_score', 'N/A'):.6f}\")\nexcept:\n    print(\"Multimodal eval chưa có competition_score\")\ntry:\n    print(f\"IMU-only Hierarchical F1   : {eval_imu.get('competition_score', 'N/A'):.6f}\")\nexcept:\n    print(\"IMU-only eval chưa có competition_score\")\n\nprint(f\"Multimodal params          : {sum(p.numel() for p in model.parameters()):,}\")\nprint(f\"IMU-only params            : {sum(p.numel() for p in imu_model.parameters()):,}\")\nprint(f\"{'='*70}\")'''\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T15:42:38.915030Z","iopub.execute_input":"2025-12-10T15:42:38.915735Z","iopub.status.idle":"2025-12-10T15:54:25.779105Z","shell.execute_reply.started":"2025-12-10T15:42:38.915709Z","shell.execute_reply":"2025-12-10T15:54:25.778365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class PostProcessor:\n    def __init__(self, label_encoder):\n        self.label_encoder = label_encoder\n        self.target_gestures = [\n            'Above ear - pull hair', 'Cheek - pinch skin', 'Eyebrow - pull hair',\n            'Eyelash - pull hair', 'Forehead - pull hairline', 'Forehead - scratch',\n            'Neck - pinch skin', 'Neck - scratch'\n        ]\n        self.orientation_boost = {\n            'Lie on Back': ['Above ear - pull hair', 'Forehead - pull hairline'],\n            'Lie on Side - Non Dominant': ['Cheek - pinch skin', 'Neck - pinch skin'],\n        }\n        self.subject_history = {}  # subject_id -> last predicted gesture\n\n    def process(self, probs: np.ndarray, subject: int, orientation_pred: str = \"\") -> np.ndarray:\n        probs = probs.copy().astype(np.float64)\n\n        # 1. Boost theo orientation (nếu có dự đoán orientation)\n        if orientation_pred in self.orientation_boost:\n            for g in self.orientation_boost[orientation_pred]:\n                if g in self.label_encoder.classes_:\n                    idx = np.where(self.label_encoder.classes_ == g)[0]\n                    if len(idx) > 0:\n                        probs[idx] *= 1.20  # boost 20%\n\n        # 2. Subject consistency – ưu tiên gesture gần nhất của cùng subject\n        if subject in self.subject_history:\n            last_g = self.subject_history[subject]\n            if last_g in self.label_encoder.classes_:\n                idx = np.where(self.label_encoder.classes_ == last_g)[0]\n                if len(idx) > 0:\n                    probs[idx] *= 1.15\n\n        # 3. Suppress non-target nếu target gestures có confidence cao\n        target_mask = np.isin(self.label_encoder.classes_, self.target_gestures)\n        if probs[target_mask].max() > 0.65:\n            probs[~target_mask] *= 0.80\n\n        # Cập nhật lịch sử\n        pred_idx = probs.argmax()\n        pred_gesture = self.label_encoder.classes_[pred_idx]\n        self.subject_history[subject] = pred_gesture\n\n        # Renormalize\n        probs = probs / probs.sum()\n        return probs\n\n\n# Khởi tạo PostProcessor (dùng chung cho cả 2 loại model)\npost_processor = PostProcessor(dataset.label_encoder)\n\n# ==================================================================\n# LOAD ENSEMBLE + FALLBACK (100% KHÔNG LỖI)\n# ==================================================================\n\ngesture_models = []   # Multimodal models (có thể thiếu Thermal/TOF)\nimu_only_models = []  # IMU-only models\n\nMODEL_DIR = Path(\"/kaggle/working/models\")\nMODEL_DIR.mkdir(exist_ok=True)\n\nprint(\"Loading trained models...\")\n\n# ------------------- Load Multimodal models (5-fold) -------------------\nfor fold in range(5):\n    path = MODEL_DIR / f\"best_multimodal_fold{fold}.pt\"\n    if path.exists():\n        try:\n            model = MultimodalCNNLSTMAttention(\n                imu_channels=18,\n                thermal_channels=5,\n                tof_channels=15,\n                num_classes=len(dataset.label_encoder.classes_),\n                lstm_hidden=256,\n                bidirectional=True\n            ).to(device)\n            state = torch.load(path, map_location=device, weights_only=True)\n            model.load_state_dict(state, strict=False)\n            model.eval()\n            gesture_models.append(model)\n            print(f\"Loaded multimodal fold {fold}\")\n        except Exception as e:\n            print(f\"Failed to load multimodal fold {fold}: {e}\")\n\n# ------------------- Load IMU-only models -------------------\nimu_paths = [\n    MODEL_DIR / \"best_accuracy_imu_only.pt\",\n    MODEL_DIR / \"best_loss_imu_only.pt\"\n]\nfor path in imu_paths:\n    if path.exists():\n        try:\n            model = CNNLSTMAttention(\n                imu_channels=18,\n                num_classes=len(imu_only_dataset.label_encoder.classes_),\n                lstm_hidden=384,\n                bidirectional=True\n            ).to(device)\n            state = torch.load(path, map_location=device, weights_only=True)\n            model.load_state_dict(state, strict=False)\n            model.eval()\n            imu_only_models.append(model)\n            print(f\"Loaded IMU-only model: {path.name}\")\n        except Exception as e:\n            print(f\"Failed to load {path.name}: {e}\")\n\n# ------------------- Fallback nếu không có model nào -------------------\nif len(gesture_models) == 0 and len(imu_only_models) == 0:\n    print(\"No trained model found → using best single model from training\")\n    if 'imu_model' in globals() and imu_model is not None:\n        imu_only_models.append(imu_model)\n        print(\"Using currently trained imu_model as fallback\")\n    else:\n        raise RuntimeError(\"KHÔNG CÓ MODEL NÀO ĐỂ INFERENCE! Hãy train trước!\")\n\nprint(f\"\\nLoaded {len(gesture_models)} multimodal + {len(imu_only_models)} IMU-only models\")\nprint(\"Ready for inference!\")\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:26:14.541108Z","iopub.execute_input":"2025-12-10T16:26:14.541658Z","iopub.status.idle":"2025-12-10T16:26:14.947849Z","shell.execute_reply.started":"2025-12-10T16:26:14.541632Z","shell.execute_reply":"2025-12-10T16:26:14.947178Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def preprocess_imu_only(seq_df: pd.DataFrame) -> torch.Tensor:\n    imu_cols = [c for c in seq_df.columns if c.startswith(('acc_', 'rot_'))]\n    imu_raw = seq_df[imu_cols].ffill().bfill().fillna(0).values\n    \n    # Enhanced features (giống như trong dataset)\n    df_enh = pd.DataFrame(imu_raw, columns=imu_cols)\n    df_enh = add_features_enhanced(df_enh)\n    x = df_enh.drop(columns=['sequence_id'], errors='ignore').values.astype(np.float32)\n    \n    x = torch.tensor(x, dtype=torch.float32)\n    x = x[:256]\n    x = torch.nn.functional.pad(x, (0, 0, 0, 256 - x.shape[0]))\n    return x.unsqueeze(0).to(device)  # [1, 256, 18]\n\n\ndef preprocess_multimodal(seq_df: pd.DataFrame) -> torch.Tensor:\n    imu_cols = [c for c in seq_df.columns if c.startswith(('acc_', 'rot_'))]\n    thermal_cols = [f\"thm_{i}\" for i in range(1,6)]\n    tof_cols = [c for c in seq_df.columns if c.startswith('tof_') and '_v' in c]\n\n    # === IMU enhanced ===\n    imu_raw = seq_df[imu_cols].ffill().bfill().fillna(0).values\n    imu_df = pd.DataFrame(imu_raw, columns=imu_cols)\n    imu_df = add_features_enhanced(imu_df)\n    imu_enh = imu_df.drop(columns=['sequence_id'], errors='ignore').values.astype(np.float32)\n\n    # === Thermal ===\n    thm_raw = seq_df[thermal_cols].ffill().bfill().fillna(0).values\n    thm = dataset.scaler_thermal.transform(thm_raw)\n\n    # === TOF 15 feats ===\n    if len(tof_cols) > 0 and (seq_df[tof_cols] > -1).any().any():\n        raw = seq_df[tof_cols].replace(-1, np.nan).values\n        tof_feat = []\n        for s in range(5):\n            pix = raw[:, s*64:(s+1)*64]\n            mean_v = np.nanmean(pix, axis=1, keepdims=True)\n            std_v  = np.nanstd(pix, axis=1, keepdims=True)\n            valid  = np.sum(pix > -1, axis=1, keepdims=True) / 64.0\n            tof_feat.append(np.concatenate([mean_v, std_v, valid], axis=1))\n        tof_feat = np.concatenate(tof_feat, axis=1)\n        tof_feat = np.nan_to_num(tof_feat, nan=0.0)\n        tof_feat = dataset.scaler_tof.transform(tof_feat)\n    else:\n        tof_feat = np.zeros((len(seq_df), 15))\n\n    # Ghép lại\n    x = np.concatenate([imu_enh, thm, tof_feat], axis=1).astype(np.float32)\n    x = torch.tensor(x)[:256]\n    x = torch.nn.functional.pad(x, (0, 0, 0, 256 - x.shape[0]))\n    return x.unsqueeze(0).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:26:18.908796Z","iopub.execute_input":"2025-12-10T16:26:18.909536Z","iopub.status.idle":"2025-12-10T16:26:18.920343Z","shell.execute_reply.started":"2025-12-10T16:26:18.909508Z","shell.execute_reply":"2025-12-10T16:26:18.919597Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    seq_df = sequence.to_pandas()\n    subject = seq_df['subject'].iloc[0]\n\n    # Kiểm tra modality\n    has_thermal = all(f\"thm_{i}\" in seq_df.columns for i in range(1,6)) and seq_df[[f\"thm_{i}\" for i in range(1,6)]].notna().all().all()\n    has_tof = any(c.startswith('tof_') and '_v' in c for c in seq_df.columns)\n    has_tof = has_tof and (seq_df.filter(like='tof_') > -1).any().any()\n\n    use_imu_only = not (has_thermal and has_tof)\n\n    # Chọn model\n    if not use_imu_only and len(gesture_models) > 0:\n        models = gesture_models\n        is_multimodal = True\n    else:\n        models = imu_only_models if len(imu_only_models) > 0 else gesture_models\n        is_multimodal = False\n\n    if len(models) == 0:\n        return \"Text on phone\"  # Ultimate fallback\n\n    # Preprocess\n    try:\n        if is_multimodal:\n            x = preprocess_multimodal(seq_df)\n        else:\n            x = preprocess_imu_only(seq_df)\n    except Exception as e:\n        print(f\"Preprocess error → fallback IMU-only: {e}\")\n        x = preprocess_imu_only(seq_df)\n        models = imu_only_models if len(imu_only_models) > 0 else models\n\n    # Ensemble\n    all_probs = []\n    with torch.no_grad():\n        for model in models:\n            try:\n                output = model(x)\n                if isinstance(output, tuple):\n                    logits = output[0]\n                else:\n                    logits = output\n                prob = torch.softmax(logits, dim=1)\n                all_probs.append(prob.cpu().numpy())\n            except:\n                continue\n\n    if len(all_probs) == 0:\n        return \"Text on phone\"\n\n    ens_prob = np.mean(all_probs, axis=0).squeeze()\n\n    # Post-processing\n    final_prob = post_processor.process(ens_prob, subject, orientation_pred=\"\")\n\n    pred_idx = final_prob.argmax()\n    prediction = dataset.label_encoder.classes_[pred_idx]\n\n    # Final safety\n    valid_gestures = CompetitionMetric().all_classes\n    if prediction not in valid_gestures:\n        prediction = 'Text on phone'\n\n    return prediction","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:26:21.489575Z","iopub.execute_input":"2025-12-10T16:26:21.489850Z","iopub.status.idle":"2025-12-10T16:26:21.499929Z","shell.execute_reply.started":"2025-12-10T16:26:21.489829Z","shell.execute_reply":"2025-12-10T16:26:21.499099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_sequence(\n    sequence_df: pd.DataFrame,\n    multimodal_model: nn.Module = None,\n    imu_only_model: nn.Module = None,\n    device: str = 'cuda'\n) -> str:\n    \"\"\"\n    Dự đoán gesture cho 1 sequence – TỰ ĐỘNG chọn model phù hợp\n    - Nếu có Thermal + TOF → dùng multimodal_model\n    - Nếu thiếu → dùng imu_only_model (ưu tiên) hoặc fallback multimodal với zero padding\n    \"\"\"\n    sequence_df = sequence_df.copy()\n\n    # === 1. Kiểm tra modality có sẵn không ===\n    thermal_cols = [f\"thm_{i}\" for i in range(1, 6)]\n    tof_pixel_cols = [c for c in sequence_df.columns if c.startswith('tof_') and '_v' in c]\n\n    has_thermal = sequence_df[thermal_cols].notna().all().all() and sequence_df[thermal_cols].gt(-100).all().all()\n    has_tof = len(tof_pixel_cols) > 0 and (sequence_df[tof_pixel_cols] > -1).any().any()\n\n    use_imu_only = not (has_thermal and has_tof)\n    print(f\"→ Thermal: {'OK' if has_thermal else 'MISSING'} | TOF: {'OK' if has_tof else 'MISSING'} → {'IMU-ONLY' if use_imu_only else 'MULTIMODAL'}\")\n\n    # === 2. Chọn model ===\n    if use_imu_only and imu_only_model is not None:\n        model = imu_only_model\n        print(\"   Using IMU-only model\")\n    elif multimodal_model is not None:\n        model = multimodal_model\n        print(\"   Using Multimodal model\")\n    else:\n        raise ValueError(\"Không có model nào khả dụng!\")\n\n    model.eval()\n\n    # === 3. Preprocess IMU (luôn có) ===\n    imu_cols = [c for c in sequence_df.columns if c.startswith(('acc_', 'rot_'))]\n    imu_raw = sequence_df[imu_cols].ffill().bfill().fillna(0).values\n    imu = dataset.scaler_imu.transform(imu_raw)\n    imu = torch.tensor(imu, dtype=torch.float32)[:256]\n    imu = torch.nn.functional.pad(imu, (0, 0, 0, 256 - imu.shape[0]))  # pad to 256\n    imu = imu.unsqueeze(0).to(device)  # [1, 256, 7]\n\n    # === 4. Nếu là IMU-only model → chỉ cần IMU enhanced features ===\n    if use_imu_only and imu_only_model is not None:\n        # Dùng enhanced features (world acc, angular vel, jerk...) → 18 channels\n        imu_enhanced = add_features_enhanced(pd.DataFrame(imu_raw, columns=imu_cols))\n        x = imu_enhanced.drop(columns=['sequence_id'], errors='ignore').values.astype(np.float32)\n        x = torch.tensor(x, dtype=torch.float32)[:256]\n        x = torch.nn.functional.pad(x, (0, 0, 0, 256 - x.shape[0]))\n        x = x.unsqueeze(0).to(device)  # [1, 256, 18+]\n\n        with torch.no_grad():\n            logits = model(x)\n    else:\n        # === Multimodal: preprocess Thermal + TOF ===\n        # Thermal\n        thm_raw = sequence_df[thermal_cols].ffill().bfill().fillna(0).values\n        thm = dataset.scaler_thermal.transform(thm_raw)\n        thm = torch.tensor(thm, dtype=torch.float32)[:256]\n        thm = torch.nn.functional.pad(thm, (0, 0, 0, 256 - thm.shape[0]))\n        thm = thm.unsqueeze(0).to(device)\n\n        # TOF → 15 feats\n        if has_tof and len(tof_pixel_cols) > 0:\n            raw = sequence_df[tof_pixel_cols].replace(-1, np.nan).values\n            tof_feat = []\n            for s in range(5):\n                pix = raw[:, s*64:(s+1)*64]\n                mean_v = np.nanmean(pix, axis=1, keepdims=True)\n                std_v = np.nanstd(pix, axis=1, keepdims=True)\n                valid = np.sum(pix > -1, axis=1, keepdims=True) / 64.0\n                tof_feat.append(np.concatenate([mean_v, std_v, valid], axis=1))\n            tof_feat = np.concatenate(tof_feat, axis=1)\n            tof_feat = np.nan_to_num(tof_feat, nan=0.0)\n            tof_feat = dataset.scaler_tof.transform(tof_feat)\n        else:\n            tof_feat = np.zeros((len(sequence_df), 15))\n\n        tof = torch.tensor(tof_feat, dtype=torch.float32)[:256]\n        tof = torch.nn.functional.pad(tof, (0, 0, 0, 256 - tof.shape[0]))\n        tof = tof.unsqueeze(0).to(device)\n\n        # Enhanced IMU\n        imu_enhanced_df = add_features_enhanced(pd.DataFrame(imu_raw, columns=imu_cols))\n        imu_enhanced = imu_enhanced_df.drop(columns=['sequence_id'], errors='ignore').values.astype(np.float32)\n        imu_enhanced = torch.tensor(imu_enhanced)[:256]\n        imu_enhanced = torch.nn.functional.pad(imu_enhanced, (0, 0, 0, 256 - imu_enhanced.shape[0]))\n        imu_enhanced = imu_enhanced.unsqueeze(0).to(device)\n\n        # Ghép: [enhanced_imu, thermal, tof]\n        x = torch.cat([imu_enhanced, thm, tof], dim=2)  # [1, 256, ~38]\n\n        with torch.no_grad():\n            logits, _ = model(x) if hasattr(model, 'orientation_head') else model(x)\n\n    # === 5. Dự đoán ===\n    probabilities = torch.softmax(logits, dim=1)\n    predicted_idx = torch.argmax(probabilities, dim=1).item()\n    predicted_gesture = dataset.label_encoder.inverse_transform([predicted_idx])[0]\n\n    return predicted_gesture","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-10T16:26:24.066003Z","iopub.execute_input":"2025-12-10T16:26:24.066315Z","iopub.status.idle":"2025-12-10T16:26:24.081908Z","shell.execute_reply.started":"2025-12-10T16:26:24.066271Z","shell.execute_reply":"2025-12-10T16:26:24.081348Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_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-12-10T16:26:27.044585Z","iopub.execute_input":"2025-12-10T16:26:27.044882Z","iopub.status.idle":"2025-12-10T16:26:28.946799Z","shell.execute_reply.started":"2025-12-10T16:26:27.044849Z","shell.execute_reply":"2025-12-10T16:26:28.946131Z"}},"outputs":[],"execution_count":null}]}