{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Import Necessary Libraries","metadata":{}},{"cell_type":"code","source":"import os\nimport json\nimport joblib\nimport numpy as np\nimport pandas as pd\nimport random\nfrom pathlib import Path\nimport warnings\nimport shutil\nwarnings.filterwarnings(\"ignore\")\n\n# Scikit-learn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.model_selection import StratifiedGroupKFold # Still from sklearn\n\n# PyTorch\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.nn.utils.rnn import pad_sequence\nfrom torch.optim.lr_scheduler import LambdaLR\n\nimport polars as pl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:43:17.001166Z","iopub.execute_input":"2025-08-18T07:43:17.001540Z","iopub.status.idle":"2025-08-18T07:43:20.463676Z","shell.execute_reply.started":"2025-08-18T07:43:17.001509Z","shell.execute_reply":"2025-08-18T07:43:20.462907Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tải tập dữ liệu\n#Dữ liệu cảm biến cho tập huấn luyện (Train Sensor Data): \ntrain_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\n\n#Thông tin nhân khẩu học của đối tượng trong tập huấn luyện (giới tính, tuổi, ...): \ntrain_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\n\n#Dữ liệu cảm biến cho tập kiểm tra:\ntest_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")\n\n# Thông tin nhân khẩu học của đối tượng trong tập kiểm tra:\ntest_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:43:20.464785Z","iopub.execute_input":"2025-08-18T07:43:20.465189Z","iopub.status.idle":"2025-08-18T07:43:51.513631Z","shell.execute_reply.started":"2025-08-18T07:43:20.465168Z","shell.execute_reply":"2025-08-18T07:43:51.513041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"datasets = {\n    \"Train Data\": train_df,\n    \"Train Demographics\": train_dem_df,\n    \"Test Data\": test_df,\n    \"Test Demographics\": test_dem_df,\n}\n\nfor name, df in datasets.items():\n    print(f\"Missing values in {name}:\")\n    missing = df.isnull().sum()\n    missing = missing[missing > 0]\n    if not missing.empty:\n        print(missing)\n    else:\n        print(\"  ✅ No missing values.\")\n    print()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:43:51.514369Z","iopub.execute_input":"2025-08-18T07:43:51.514643Z","iopub.status.idle":"2025-08-18T07:43:52.258881Z","shell.execute_reply.started":"2025-08-18T07:43:51.514614Z","shell.execute_reply":"2025-08-18T07:43:52.258104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- 2.4 Hợp nhất dữ liệu nhân khẩu học và cảm biến ---\nmerged_train = pd.merge(train_df, train_dem_df, on=\"subject\", how=\"left\")\nmerged_test = pd.merge(test_df, test_dem_df, on=\"subject\", how=\"left\")\n\nprint(\"Merged Train Shape:\", merged_train.shape)\nprint(\"Merged Test Shape:\", merged_test.shape)\ndisplay(merged_train.head(2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:43:52.261226Z","iopub.execute_input":"2025-08-18T07:43:52.261464Z","iopub.status.idle":"2025-08-18T07:43:52.916111Z","shell.execute_reply.started":"2025-08-18T07:43:52.261447Z","shell.execute_reply":"2025-08-18T07:43:52.915508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Thống kê mô tả toàn bộ merged_train ---\nmerged_train.describe().T","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:43:52.917118Z","iopub.execute_input":"2025-08-18T07:43:52.917362Z","iopub.status.idle":"2025-08-18T07:44:00.470247Z","shell.execute_reply.started":"2025-08-18T07:43:52.917345Z","shell.execute_reply":"2025-08-18T07:44:00.469542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Kiểm tra thiếu giá trị & thống kê cảm biến: \n#Danh sách các cột không thuộc cảm biến (có thể là ID, thông tin khác). \n\nexcluded_prefixes = ('acc_', 'rot_', 'thm_', 'tof_')\nsensor_cols = [col for col in train_df.columns if not col.startswith(excluded_prefixes)]\n\n# Sensor Data Summary for TRAIN\n#isnull().sum(): Đếm số giá trị bị thiếu.\nmissing_sensor_train = pd.DataFrame({\n    'Feature': sensor_cols,\n    '[TRAIN] Missing Count': train_df[sensor_cols].isnull().sum().values,\n    '[TRAIN] Missing %': (train_df[sensor_cols].isnull().sum().values / len(train_df)) * 100\n})\n\n#nunique(): Đếm số lượng giá trị duy nhất.\nunique_sensor_train = pd.DataFrame({\n    'Feature': sensor_cols,\n    'Unique Values [TRAIN]': train_df[sensor_cols].nunique().values\n})\n\n#dtypes: Lấy kiểu dữ liệu của từng cột.\ndtypes_sensor = pd.DataFrame({\n    'Feature': sensor_cols,\n    'Data Type': train_df[sensor_cols].dtypes.values\n})\n\n# Merge all summaries (NO test set)\n#merge: Gộp các bảng thống kê thành bảng duy nhất theo Feature.\nsensor_summary = missing_sensor_train \\\n    .merge(unique_sensor_train, on='Feature', how='left') \\\n    .merge(dtypes_sensor, on='Feature', how='left')\n\n# Display styled DataFrame (mask NaNs just for styling)\n#fillna(0): Điền giá trị thiếu bằng 0 (cho đẹp mắt khi hiển thị).\n#.style.background_gradient: Tô màu nền theo giá trị giúp dễ nhìn.\nstyled_df = sensor_summary.fillna(0)\nstyled_df.style.background_gradient(cmap='viridis')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:00.471023Z","iopub.execute_input":"2025-08-18T07:44:00.471300Z","iopub.status.idle":"2025-08-18T07:44:01.419619Z","shell.execute_reply.started":"2025-08-18T07:44:00.471281Z","shell.execute_reply":"2025-08-18T07:44:01.419019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Thống kê tương tự cho nhân khẩu học: Tương tự như phần thống kê cảm biến nhưng áp dụng cho dữ liệu nhân khẩu học.\n\n# Cột nhân khẩu học (không loại trừ)\ndem_cols = train_dem_df.columns\n\n# Giá trị bị thiếu trong dữ liệu nhân khẩu học của train \nmissing_demo_train = pd.DataFrame({\n    'Feature': dem_cols,\n    '[TRAIN DEMO] Missing Count': train_dem_df[dem_cols].isnull().sum().values,\n    '[TRAIN DEMO] Missing %': (train_dem_df[dem_cols].isnull().sum().values / len(train_dem_df)) * 100\n})\n\n# Giá trị duy nhất được tính trong dữ liệu nhân khẩu học của train \nunique_demo_train = pd.DataFrame({\n    'Feature': dem_cols,\n    'Unique Values [TRAIN DEMO]': train_dem_df[dem_cols].nunique().values\n})\n\n# Data types\ndtypes_demo = pd.DataFrame({\n    'Feature': dem_cols,\n    'Data Type': train_dem_df[dem_cols].dtypes.values\n})\n\n# Tóm tắt hợp nhất (chỉ dành cho đào tạo)\ndemo_summary = (\n    missing_demo_train\n    .merge(unique_demo_train, on='Feature', how='left')\n    .merge(dtypes_demo, on='Feature', how='left')\n)\n\n# Hiển thị tóm tắt theo phong cách\ndemo_summary.style.background_gradient(cmap='viridis')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:01.420307Z","iopub.execute_input":"2025-08-18T07:44:01.420580Z","iopub.status.idle":"2025-08-18T07:44:01.440397Z","shell.execute_reply.started":"2025-08-18T07:44:01.420556Z","shell.execute_reply":"2025-08-18T07:44:01.439673Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\n\n# 1) Sao chép đào tạo và kiểm tra để chúng ta không sửa đổi DataFrame gốc\ntrain_temp = train_df.copy()\ntest_temp  = test_df.copy()\n\n# 2) GIA TỐC KẾ: tính toán độ lớn tại mỗi dấu thời gian\ntrain_temp['acc_mag'] = np.sqrt(\n    train_temp['acc_x']**2 + train_temp['acc_y']**2 + train_temp['acc_z']**2\n)\ntest_temp['acc_mag'] = np.sqrt(\n    test_temp['acc_x']**2 + test_temp['acc_y']**2 + test_temp['acc_z']**2\n)\n\n# 3) ROTATION: tính toán “góc quay” từ thành phần quaternion w\n# (Lưu ý: rot_w nằm trong [-1,1], do đó arccos hợp lệ. Chúng tôi bỏ qua NaN nếu có.)\ntrain_temp['rot_angle'] = 2 * np.arccos(train_temp['rot_w'].clip(-1,1))\ntest_temp['rot_angle']  = 2 * np.arccos(test_temp['rot_w'].clip(-1,1))\n\n# 4) Nhóm theo sequence_id và tổng hợp các tóm tắt gia tốc kế\nacc_agg_funcs = {\n    'acc_mag': ['mean', 'std', 'max']\n}\ntrain_acc_summary = train_temp.groupby('sequence_id').agg(acc_agg_funcs)\ntest_acc_summary  = test_temp.groupby('sequence_id').agg(acc_agg_funcs)\n\n# Làm phẳng cột MultiIndex\ntrain_acc_summary.columns = ['acc_mag_' + stat for stat in ['mean', 'std', 'max']]\ntest_acc_summary.columns  = ['acc_mag_' + stat for stat in ['mean', 'std', 'max']]\n\n# 5) Nhóm theo sequence_id và tổng hợp tóm tắt vòng quay\nrot_agg_funcs = {\n    'rot_angle': ['mean', 'std', 'max']\n}\ntrain_rot_summary = train_temp.groupby('sequence_id').agg(rot_agg_funcs)\ntest_rot_summary  = test_temp.groupby('sequence_id').agg(rot_agg_funcs)\n\ntrain_rot_summary.columns = ['rot_angle_' + stat for stat in ['mean', 'std', 'max']]\ntest_rot_summary.columns  = ['rot_angle_' + stat for stat in ['mean', 'std', 'max']]\n\n# 6) NHIỆT ĐỘ: năm cảm biến thm_1 … thm_5\nthm_cols = [f\"thm_{i}\" for i in range(1, 6)]\n\n# Xác định hàm tổng hợp: trung bình + độ lệch chuẩn\nthm_agg_funcs = {col: ['mean', 'std'] for col in thm_cols}\n\ntrain_thm_summary = train_temp.groupby('sequence_id').agg(thm_agg_funcs)\ntest_thm_summary  = test_temp.groupby('sequence_id').agg(thm_agg_funcs)\n\n# Làm phẳng các cột MultiIndex\nflattened_thm_cols = []\nfor sensor in thm_cols:\n    for stat in ['mean','std']:\n        flattened_thm_cols.append(f\"{sensor}_{stat}\")\n\ntrain_thm_summary.columns = flattened_thm_cols\ntest_thm_summary.columns  = flattened_thm_cols\n\n# 7) THỜI GIAN BAY: mỗi cảm biến i có 64 cột pixel: tof_i_v0 … tof_i_v63\n# Chúng ta sẽ tạo một “tof_i_mean_at_ts” cho mỗi dấu thời gian, sau đó tổng hợp theo từng chuỗi.\n\ndef compute_tof_sequence_summary(df):\n    # Khởi tạo một dict để giữ DataFrames theo từng chuỗi\n    seq_summaries = {}\n\n    for i in range(1, 6):\n        # Xây dựng danh sách các cột cho cảm biến i\n        tof_cols = [f\"tof_{i}_v{pix}\" for pix in range(64)]\n        # Thay thế -1 bằng NaN để chúng không làm lệch giá trị trung bình; chuyển đổi sang float\n        ts_grid = df[tof_cols].replace(-1, np.nan).astype(float)\n        # Tính toán “trung bình trên tất cả 64 pixel” cho mỗi dấu thời gian\n        df[f\"tof_{i}_mean_at_ts\"] = ts_grid.mean(axis=1)\n    \n   # Bây giờ, nhóm theo id chuỗi và tính giá trị trung bình và độ lệch chuẩn của các giá trị trung bình đó\n    agg_dict = {f\"tof_{i}_mean_at_ts\": ['mean','std'] for i in range(1, 6)}\n    summary = df.groupby('sequence_id').agg(agg_dict)\n    # Làm phẳng các cột MultiIndex\n    flat_cols = [f\"tof_{i}_{stat}\" for i in range(1, 6) for stat in ['mean','std']]\n    summary.columns = flat_cols\n    return summary\n\ntrain_tof_summary = compute_tof_sequence_summary(train_temp)\ntest_tof_summary  = compute_tof_sequence_summary(test_temp)\n\n# 8) Hợp nhất các tóm tắt accel, rotation, thm, tof (trên sequence_id)\ntrain_sensor_summary = (\n    train_acc_summary\n    .join(train_rot_summary, how='outer')\n    .join(train_thm_summary, how='outer')\n    .join(train_tof_summary, how='outer')\n)\n\ntest_sensor_summary = (\n    test_acc_summary\n    .join(test_rot_summary, how='outer')\n    .join(test_thm_summary, how='outer')\n    .join(test_tof_summary, how='outer')\n)\n\n# 9) Thêm cột “Dataset” để chúng ta có thể thực hiện box+hist song song\ntrain_sensor_summary['Dataset'] = 'Train'\ntest_sensor_summary['Dataset']  = 'Test'\n\n# 10) Ghép nối thành một DataFrame để vẽ đồ thị\ncombined_sensor_summary = pd.concat(\n    [train_sensor_summary, test_sensor_summary],\n    axis=0\n).reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:01.440965Z","iopub.execute_input":"2025-08-18T07:44:01.441175Z","iopub.status.idle":"2025-08-18T07:44:06.885546Z","shell.execute_reply.started":"2025-08-18T07:44:01.441153Z","shell.execute_reply":"2025-08-18T07:44:06.884964Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\n\n# (1) Sáp nhập train_demographics vào train_df nếu chưa thực hiện\ntrain_df = train_df.merge(\n    train_dem_df,\n    on=\"subject\",\n    how=\"left\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:06.886194Z","iopub.execute_input":"2025-08-18T07:44:06.886441Z","iopub.status.idle":"2025-08-18T07:44:07.501324Z","shell.execute_reply.started":"2025-08-18T07:44:06.886417Z","shell.execute_reply":"2025-08-18T07:44:07.500729Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def fix_imu(df):\n    imu_cols = [c for c in df.columns if c.startswith((\"acc_\", \"rot_\"))]\n    def _seq_fix(g):\n        g[imu_cols] = g[imu_cols].interpolate(limit_direction=\"both\")\n        # renormalize quaternion\n        q = g[[\"rot_w\",\"rot_x\",\"rot_y\",\"rot_z\"]].to_numpy(float)\n        n = (q**2).sum(1)**0.5\n        n[n==0] = 1.0\n        q = q / n[:,None]\n        g[[\"rot_w\",\"rot_x\",\"rot_y\",\"rot_z\"]] = q\n        return g\n    return df.groupby(\"sequence_id\", group_keys=False).apply(_seq_fix)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.503600Z","iopub.execute_input":"2025-08-18T07:44:07.503814Z","iopub.status.idle":"2025-08-18T07:44:07.508678Z","shell.execute_reply.started":"2025-08-18T07:44:07.503798Z","shell.execute_reply":"2025-08-18T07:44:07.508145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def fix_thm(df):\n    thm = [f\"thm_{i}\" for i in range(1,6)]\n    return df.groupby(\"sequence_id\", group_keys=False)[thm].apply(\n        lambda g: g.interpolate(limit_direction=\"both\").ffill().bfill()\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.509435Z","iopub.execute_input":"2025-08-18T07:44:07.509752Z","iopub.status.idle":"2025-08-18T07:44:07.531036Z","shell.execute_reply.started":"2025-08-18T07:44:07.509732Z","shell.execute_reply":"2025-08-18T07:44:07.530334Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def fix_tof_and_reduce(df):\n    # 1) sentinel -> NaN + flag\n    tof_pix = [c for c in df.columns if c.startswith(\"tof_\") and \"_v\" in c]\n    df[tof_pix] = df[tof_pix].replace(-1, np.nan)\n    # optional flags per sensor\n    for i in range(1,6):\n        cols = [f\"tof_{i}_v{p}\" for p in range(64) if f\"tof_{i}_v{p}\" in df]\n        df[f\"tof_{i}_missing_frac\"] = df[cols].isna().mean(axis=1)\n        df[f\"tof_{i}_mean\"] = df[cols].mean(axis=1)  # dimension reduction\n    # 2) interpolate the 5 means\n    mean_cols = [f\"tof_{i}_mean\" for i in range(1,6)]\n    df[mean_cols] = df.groupby(\"sequence_id\", group_keys=False)[mean_cols].apply(\n        lambda g: g.interpolate(limit_direction=\"both\").ffill().bfill()\n    )\n    # 3) drop heavy pixel columns to save RAM\n    df.drop(columns=tof_pix, inplace=True)\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.531784Z","iopub.execute_input":"2025-08-18T07:44:07.532211Z","iopub.status.idle":"2025-08-18T07:44:07.549406Z","shell.execute_reply.started":"2025-08-18T07:44:07.532193Z","shell.execute_reply":"2025-08-18T07:44:07.548559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.preprocessing import LabelEncoder\nle_gesture = LabelEncoder().fit(train_df[\"gesture\"].astype(str))\ntrain_df[\"gesture_id\"] = le_gesture.transform(train_df[\"gesture\"].astype(str))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.550152Z","iopub.execute_input":"2025-08-18T07:44:07.550386Z","iopub.status.idle":"2025-08-18T07:44:07.665059Z","shell.execute_reply.started":"2025-08-18T07:44:07.550365Z","shell.execute_reply":"2025-08-18T07:44:07.664272Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def minimal_preprocess(df):\n    df = df.copy()\n    # 1) IMU\n    df = fix_imu(df)\n    # 2) Thermopile\n    thm_fixed = fix_thm(df)  # returns only thm cols\n    df[thm_fixed.columns] = thm_fixed\n    # 3) ToF -> means + flags, drop pixels\n    df = fix_tof_and_reduce(df)\n    # 4) Feature cơ bản (ví dụ):\n    df[\"acc_mag\"] = np.sqrt(df[\"acc_x\"]**2 + df[\"acc_y\"]**2 + df[\"acc_z\"]**2)\n    df[\"rot_angle\"] = 2*np.arccos(np.clip(df[\"rot_w\"], -1, 1))\n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.665849Z","iopub.execute_input":"2025-08-18T07:44:07.666065Z","iopub.status.idle":"2025-08-18T07:44:07.670508Z","shell.execute_reply.started":"2025-08-18T07:44:07.666049Z","shell.execute_reply":"2025-08-18T07:44:07.669861Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def seed_everything(seed):\n    \"\"\"\n    Seeds various random number generators for reproducibility in PyTorch.\n\n    Args:\n        seed (int): The seed value to use.\n    \"\"\"\n    os.environ['PYTHONHASHSEED'] = str(seed) # Set Python hash seed\n    random.seed(seed)                        # Seed Python's random module\n    np.random.seed(seed)                     # Seed NumPy\n    torch.manual_seed(seed)                  # Seed PyTorch for CPU operations\n    torch.cuda.manual_seed(seed)             # Seed PyTorch for all GPU operations (if available)\n    torch.cuda.manual_seed_all(seed)         # Seed all GPUs if multiple are used\n\n    # Ensure deterministic operations for cuDNN (GPU backend)\n    # This can sometimes slightly reduce performance but ensures reproducibility\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False   # Disable cuDNN benchmarking for reproducibility\n\n# Call the function to set the seed\nseed_everything(seed=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.671400Z","iopub.execute_input":"2025-08-18T07:44:07.671663Z","iopub.status.idle":"2025-08-18T07:44:07.701654Z","shell.execute_reply.started":"2025-08-18T07:44:07.671643Z","shell.execute_reply":"2025-08-18T07:44:07.701097Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Configuration","metadata":{}},{"cell_type":"code","source":"# (Competition metric will only be imported when TRAINing)\nTRAIN = True\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\n\n# used when TRAIN=False\nPRETRAINED_DIR = Path(\"/kaggle/input/pretrained-model\")\n\n# artifacts will be saved here\nEXPORT_DIR = Path(\"./\")                 \n\nBATCH_SIZE = 64\nPAD_PERCENTILE = 95\nLR_INIT = 5e-4\nWD = 3e-3\nMIXUP_ALPHA = 0.4\nEPOCHS = 160\nPATIENCE = 40\n\nprint(\"▶ imports ready · pytorch\", torch.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.702413Z","iopub.execute_input":"2025-08-18T07:44:07.702666Z","iopub.status.idle":"2025-08-18T07:44:07.707648Z","shell.execute_reply.started":"2025-08-18T07:44:07.702645Z","shell.execute_reply":"2025-08-18T07:44:07.707146Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Utility Functions","metadata":{}},{"cell_type":"code","source":"# --- Tensor Manipulations ---\ndef time_sum(x):\n    return torch.sum(x, dim=1)\n\ndef squeeze_last_axis(x):\n    return x.squeeze(-1) # or torch.squeeze(x, dim=-1)\n\ndef expand_last_axis(x):\n    return x.unsqueeze(-1) # or torch.unsqueeze(x, dim=-1)\n\n# --- SE Block (Squeeze-and-Excitation Block) ---\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super(SEBlock, self).__init__()\n        self.avg_pool = nn.AdaptiveAvgPool1d(1)\n        self.fc1 = nn.Linear(channels, channels // reduction, bias=False)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc2 = nn.Linear(channels // reduction, channels, bias=False)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        # Squeeze operation\n        # For PyTorch (B, C, L), AdaptiveAvgPool1d(1) gives (B, C, 1)\n        # Then squeeze to (B, C)\n        b, c, _ = x.size() # Get batch size and channels\n        se = self.avg_pool(x).view(b, c) # (B, C, 1) -> (B, C)\n\n        # Excitation operation\n        se = self.fc1(se)\n        se = self.relu(se)\n        se = self.fc2(se)\n        se = self.sigmoid(se).view(b, c, 1) # Reshape to (B, C, 1) for multiplication\n\n        return x * se # Element-wise multiplication\n\n\n# --- Residual CNN Block with SE ---\nclass ResidualSECNNBlock(nn.Module):\n    def __init__(self, in_filters, out_filters, kernel_size, pool_size=2, drop=0.3, wd=1e-4):\n        super(ResidualSECNNBlock, self).__init__()\n        # Use a list to store sequential layers for repeated blocks\n        self.conv_block = nn.Sequential(\n            nn.Conv1d(in_filters, out_filters, kernel_size, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters),\n            nn.ReLU(inplace=True),\n            nn.Conv1d(out_filters, out_filters, kernel_size, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters),\n            nn.ReLU(inplace=True)\n        )\n        self.se_block = SEBlock(out_filters) # Apply SE block\n\n        # Shortcut connection\n        self.shortcut_conv = None\n        if in_filters != out_filters:\n            self.shortcut_conv = nn.Sequential(\n                nn.Conv1d(in_filters, out_filters, 1, padding='same', bias=False),\n                nn.BatchNorm1d(out_filters)\n            )\n\n        self.max_pool = nn.MaxPool1d(pool_size)\n        self.dropout = nn.Dropout(drop)\n\n        # L2 regularization (weight decay) is typically handled by the optimizer in PyTorch\n        # We define it here to show where it would apply, but it's passed to optim.Adam etc.\n        self.wd = wd\n\n    def forward(self, x):\n        shortcut = x\n\n        # Conv layers + Batch Norm + ReLU\n        x = self.conv_block(x)\n\n        # SE Block\n        x = self.se_block(x)\n\n        # Shortcut connection handling\n        if self.shortcut_conv:\n            shortcut = self.shortcut_conv(shortcut)\n\n        # Add (residual connection)\n        x = x + shortcut # Keras 'add'\n\n        x = F.relu(x) # Activation after addition\n\n        x = self.max_pool(x)\n        x = self.dropout(x)\n        return x\n\n\n# --- Attention Layer ---\nclass AttentionLayer(nn.Module):\n    def __init__(self, input_dim):\n        super(AttentionLayer, self).__init__()\n        self.score_dense = nn.Linear(input_dim, 1)\n        self.tanh = nn.Tanh()\n\n    def forward(self, inputs):\n        # Calculate raw scores; This will give (batch_size, sequence_length, 1)\n        score = self.score_dense(inputs)\n        score = self.tanh(score)\n\n        # Squeeze last axis to get (batch_size, sequence_length)\n        score = squeeze_last_axis(score)\n\n        # Calculate attention weights\n        weights = F.softmax(score, dim=1)\n\n        # Expand last axis back to (batch_size, sequence_length, 1)\n        weights = expand_last_axis(weights)\n\n        # Apply weights to inputs\n        context = inputs * weights\n\n        # Sum along the time axis\n        context = time_sum(context)\n\n        return context","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.708393Z","iopub.execute_input":"2025-08-18T07:44:07.708595Z","iopub.status.idle":"2025-08-18T07:44:07.728242Z","shell.execute_reply.started":"2025-08-18T07:44:07.708572Z","shell.execute_reply":"2025-08-18T07:44:07.727672Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Data Helpers**","metadata":{}},{"cell_type":"code","source":"# --- Preprocessing Function ---\nclass MixupDataset(Dataset):\n    def __init__(self, X: torch.Tensor, y: torch.Tensor, alpha: float = 0.2):\n        \"\"\"\n        Initializes the MixupDataset for PyTorch.\n\n        Args:\n            X (torch.Tensor): The input features. Expected shape: (num_samples, sequence_length, num_features).\n                              This dataset will NOT perform internal permutation for Conv1D.\n            y (torch.Tensor): The labels. Expected shape: (num_samples, num_classes) for one-hot (float),\n                              or (num_samples,) for integer labels (long).\n            alpha (float): Mixup alpha parameter for beta distribution.\n        \"\"\"\n        self.X = X\n        self.y = y\n\n        self.alpha = alpha\n\n    def __len__(self):\n        \"\"\"Returns the total number of samples.\"\"\"\n        return len(self.X)\n\n    def __getitem__(self, idx: int):\n        \"\"\"\n        Generates one sample of data, applying Mixup by mixing the current sample\n        with a randomly selected other sample from the dataset.\n\n        Args:\n            idx (int): Index of the sample to retrieve.\n\n        Returns:\n            tuple: (mixed_X, mixed_y) as PyTorch tensors, where mixed_X is\n                   (sequence_length, num_features).\n        \"\"\"\n        x1, y1 = self.X[idx], self.y[idx]\n\n        # Select a random sample from the dataset for mixing\n        rand_idx = random.randint(0, len(self.X) - 1)\n        x2, y2 = self.X[rand_idx], self.y[rand_idx]\n\n        # Generate lambda from Beta distribution. Ensure alpha > 0.\n        # This dataset is only for training where MIXUP_ALPHA > 0\n        lam = np.random.beta(self.alpha, self.alpha)\n        lam = max(0, min(1, lam))\n\n        x_mixed = lam * x1 + (1 - lam) * x2\n        y_mixed = lam * y1 + (1 - lam) * y2\n\n        return x_mixed, y_mixed","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.728880Z","iopub.execute_input":"2025-08-18T07:44:07.729149Z","iopub.status.idle":"2025-08-18T07:44:07.763817Z","shell.execute_reply.started":"2025-08-18T07:44:07.729127Z","shell.execute_reply":"2025-08-18T07:44:07.763096Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Model Definition - Two Branch Architecture","metadata":{}},{"cell_type":"markdown","source":"## Two-Branch Model","metadata":{}},{"cell_type":"code","source":"# --- A Simple CNN Block for the ToF Branch ---\nclass SimpleCNNBlock(nn.Module):\n    def __init__(self, in_filters, out_filters, kernel_size=3, pool_size=2, drop=0.2):\n        super(SimpleCNNBlock, self).__init__()\n        self.block = nn.Sequential(\n            nn.Conv1d(in_filters, out_filters, kernel_size, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters),\n            nn.ReLU(inplace=True),\n            nn.MaxPool1d(pool_size),\n            nn.Dropout(drop)\n        )\n    def forward(self, x):\n        return self.block(x)\n\n# --- Two-Branch Model ---\nclass TwoBranchModel(nn.Module):\n    def __init__(self, pad_len, imu_dim, tof_dim, n_classes):\n        super(TwoBranchModel, self).__init__()\n        \n        # Store input dimensions for splitting in the forward pass\n        self.imu_dim = imu_dim\n        self.tof_dim = tof_dim\n\n        # --- IMU Branch (ResidualSECNNBlock is well-designed) ---\n        self.imu_branch_block1 = ResidualSECNNBlock(imu_dim, 64, 3, drop=0.1)\n        self.imu_branch_block2 = ResidualSECNNBlock(64, 128, 5, drop=0.1)\n\n        # --- TOF Branch (Using our cleaner block) ---\n        self.tof_branch_block1 = SimpleCNNBlock(tof_dim, 64, drop=0.2)\n        self.tof_branch_block2 = SimpleCNNBlock(64, 128, drop=0.2)\n        \n        # --- Shared Recurrent Layer (Choose ONE: LSTM or GRU) ---\n        merged_cnn_features = 128 + 128 # 128 from IMU, 128 from TOF\n        \n        self.recurrent_layer = nn.LSTM(\n            input_size=merged_cnn_features, \n            hidden_size=128, \n            bidirectional=True, \n            batch_first=True\n        )\n        \n        # Recurrent layer output features: 2 * hidden_size\n        recurrent_output_features = 128 * 2\n\n        # --- Attention Layer ---\n        # The input dimension now correctly matches the output of our single recurrent layer\n        self.attention_layer = AttentionLayer(input_dim=recurrent_output_features)\n        \n        # --- Classifier Head ---\n        # Input dimension matches the output of the attention layer\n        self.classifier_head = nn.Sequential(\n            nn.Linear(recurrent_output_features, 256, bias=False),\n            nn.BatchNorm1d(256),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.5),\n\n            nn.Linear(256, 128, bias=False),\n            nn.BatchNorm1d(128),\n            nn.ReLU(inplace=True),\n            nn.Dropout(0.3)\n        )\n\n        self.output_layer = nn.Linear(128, n_classes)\n\n    def forward(self, x):\n        # Input x shape: (batch_size, pad_len, imu_dim + tof_dim)\n        \n        # Split branches\n        imu = x[:, :, :self.imu_dim]\n        tof = x[:, :, self.imu_dim:]\n\n        # Permute for Conv1D: (batch, seq_len, features) -> (batch, features, seq_len)\n        imu = imu.permute(0, 2, 1)\n        tof = tof.permute(0, 2, 1)\n\n        # --- Process Branches ---\n        # IMU Branch\n        x1 = self.imu_branch_block1(imu)\n        x1 = self.imu_branch_block2(x1) # Shape: (batch, 128, new_seq_len)\n\n        # TOF Branch\n        x2 = self.tof_branch_block1(tof)\n        x2 = self.tof_branch_block2(x2) # Shape: (batch, 128, new_seq_len)\n        \n        # --- Merge and Process Sequentially ---\n        # Concatenate branches along the feature dimension\n        merged = torch.cat([x1, x2], dim=1) # Shape: (batch, 256, new_seq_len)\n        \n        # Permute for Recurrent Layer: (batch, features, seq_len) -> (batch, seq_len, features)\n        merged = merged.permute(0, 2, 1)\n\n        # Pass through the single recurrent layer\n        recurrent_out, _ = self.recurrent_layer(merged) # Shape: (batch, seq_len, 256)\n        \n        # Apply attention to the recurrent output\n        attention_out = self.attention_layer(recurrent_out) # Shape: (batch, 256)\n        \n        # --- Classifier ---\n        # Pass the clean, attention-weighted features to the classifier\n        classified = self.classifier_head(attention_out)\n        \n        # --- Output ---\n        # Get the raw logits. DO NOT apply softmax here.\n        logits = self.output_layer(classified)\n        \n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.764552Z","iopub.execute_input":"2025-08-18T07:44:07.764738Z","iopub.status.idle":"2025-08-18T07:44:07.789741Z","shell.execute_reply.started":"2025-08-18T07:44:07.764723Z","shell.execute_reply":"2025-08-18T07:44:07.789023Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"✅ Giải thích thông số:\n\npad_len=127: input sequence đã chuẩn hoá độ dài về 127 bước thời gian.\n\nimu_dim=7: mỗi timestep có 7 feature từ IMU sensor.\n\ntof_dim=325: mỗi timestep có 325 feature từ ToF sensor.\n\nn_classes=18: output phân loại 18 lớp hành vi.","metadata":{}},{"cell_type":"code","source":"# Instantiate the model\nmodel = TwoBranchModel(pad_len=127, imu_dim=7, tof_dim=325, n_classes=18)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.790552Z","iopub.execute_input":"2025-08-18T07:44:07.790831Z","iopub.status.idle":"2025-08-18T07:44:07.896510Z","shell.execute_reply.started":"2025-08-18T07:44:07.790814Z","shell.execute_reply":"2025-08-18T07:44:07.896005Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training / Inference Pipeline","metadata":{}},{"cell_type":"code","source":"def save_checkpoint(state, is_best, directory=\".\", filename=\"latest_checkpoint.pth\", best_filename=\"best_checkpoint.pth\"):\n    filepath = os.path.join(directory, filename)\n    torch.save(state, filepath)\n    if is_best:\n        # Only copy if it's the best, don't delete previous 'best_checkpoint.pth' if no improvement\n        shutil.copyfile(filepath, os.path.join(directory, best_filename))\n        print(f\"    New best model saved to {best_filename}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.897113Z","iopub.execute_input":"2025-08-18T07:44:07.897306Z","iopub.status.idle":"2025-08-18T07:44:07.901817Z","shell.execute_reply.started":"2025-08-18T07:44:07.897292Z","shell.execute_reply":"2025-08-18T07:44:07.901006Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"DELETE_EXISTING_CHECKPOINTS = True\n\nif TRAIN:\n    # --- Data Loading and Preprocessing ---\n    if DELETE_EXISTING_CHECKPOINTS:\n        print(f\"--- '{DELETE_EXISTING_CHECKPOINTS=}' is True. Checking for existing checkpoints in {EXPORT_DIR} to remove... ---\")\n        latest_checkpoint_path = os.path.join(EXPORT_DIR, \"latest_checkpoint.pth\")\n        best_checkpoint_path = os.path.join(EXPORT_DIR, \"best_checkpoint.pth\")\n        \n        # Check and delete latest_checkpoint.pth\n        if os.path.isfile(latest_checkpoint_path):\n            os.remove(latest_checkpoint_path)\n            print(f\"  Removed existing checkpoint: {latest_checkpoint_path}\")\n        else:\n            print(f\"  No existing latest_checkpoint.pth found at {latest_checkpoint_path}.\")\n\n        # Check and delete best_checkpoint.pth\n        if os.path.isfile(best_checkpoint_path):\n            os.remove(best_checkpoint_path)\n            print(f\"  Removed existing best_checkpoint.pth: {best_checkpoint_path}\")\n        else:\n            print(f\"  No existing best_checkpoint.pth found at {best_checkpoint_path}.\")\n    \n    # --- Data Loading and Preprocessing ---\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(os.path.join(RAW_DIR, \"train.csv\"))\n    le = LabelEncoder()\n    df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(os.path.join(EXPORT_DIR, \"gesture_classes.npy\"), le.classes_)\n    gesture_classes = le.classes_\n    print(\"  Calculating engineered IMU features...\")\n    df['acc_mag'] = np.sqrt(df['acc_x']**2 + df['acc_y']**2 + df['acc_z']**2)\n    df['rot_angle'] = 2 * np.arccos(df['rot_w'].clip(-1, 1))\n    df['acc_mag_jerk'] = df.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n    df['rot_angle_vel'] = df.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n    print(\"  Calculating ToF features with vectorized NumPy...\")\n    tof_pixel_cols = [f\"tof_{i}_v{p}\" for i in range(1, 6) for p in range(64)]\n    tof_data_np = df[tof_pixel_cols].replace(-1, np.nan).to_numpy()\n    reshaped_tof = tof_data_np.reshape(len(df), 5, 64)\n    with warnings.catch_warnings():\n        warnings.filterwarnings('ignore', r'Mean of empty slice'); warnings.filterwarnings('ignore', r'Degrees of freedom <= 0 for slice')\n        mean_vals, std_vals = np.nanmean(reshaped_tof, axis=2), np.nanstd(reshaped_tof, axis=2)\n        min_vals, max_vals = np.nanmin(reshaped_tof, axis=2), np.nanmax(reshaped_tof, axis=2)\n    tof_agg_cols = []\n    for i in range(1, 6):\n        df[f'tof_{i}_mean'], df[f'tof_{i}_std'] = mean_vals[:, i-1], std_vals[:, i-1]\n        df[f'tof_{i}_min'], df[f'tof_{i}_max'] = min_vals[:, i-1], max_vals[:, i-1]\n        tof_agg_cols.extend([f'tof_{i}_mean', f'tof_{i}_std', f'tof_{i}_min', f'tof_{i}_max'])\n    imu_cols = [c for c in df.columns if c.startswith(('acc_', 'rot_'))]\n    thm_cols = [c for c in df.columns if c.startswith('thm_')]\n    final_feature_cols = imu_cols + thm_cols + tof_agg_cols\n    imu_dim_final = len(imu_cols)\n    tof_thm_aggregated_dim_final = len(thm_cols) + len(tof_agg_cols)\n    np.save(os.path.join(EXPORT_DIR, \"feature_cols.npy\"), np.array(final_feature_cols))\n    print(\"  Building, scaling, and padding sequences...\")\n    X_list, y_list, lens = [], [], []\n    for seq_id, seq_df in df.groupby('sequence_id'):\n        X_list.append(seq_df[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32'))\n        y_list.append(seq_df['gesture_int'].iloc[0])\n        lens.append(len(seq_df))\n        \n    # FIX: Rename StandardScaler to feature_scaler\n    feature_scaler = StandardScaler().fit(np.concatenate(X_list, axis=0))\n    joblib.dump(feature_scaler, os.path.join(EXPORT_DIR, \"scaler.pkl\"))\n    X_scaled_list = [feature_scaler.transform(x) for x in X_list]\n    \n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    np.save(os.path.join(EXPORT_DIR, \"sequence_maxlen.npy\"), pad_len)\n    X_padded = np.zeros((len(X_scaled_list), pad_len, len(final_feature_cols)), dtype='float32')\n    for i, seq in enumerate(X_scaled_list):\n        seq_len = min(len(seq), pad_len)\n        X_padded[i, :seq_len] = seq[:seq_len]\n    y_np = np.array(y_list)\n    y_one_hot = F.one_hot(torch.from_numpy(y_np).long(), num_classes=len(gesture_classes)).float().numpy()\n    X_tr, X_val, y_tr_oh, y_val_oh, y_tr_int, y_val_int = train_test_split(\n        X_padded, y_one_hot, y_np, test_size=0.1, random_state=82, stratify=y_np\n    )\n    X_tr_t, X_val_t = torch.from_numpy(X_tr).float(), torch.from_numpy(X_val).float()\n    y_tr_t, y_val_t = torch.from_numpy(y_tr_oh).float(), torch.from_numpy(y_val_oh).float()\n    train_dataset = MixupDataset(X_tr_t, y_tr_t, alpha=MIXUP_ALPHA)\n    train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True, num_workers=os.cpu_count()//2 or 1, pin_memory=True)\n    val_dataset = torch.utils.data.TensorDataset(X_val_t, y_val_t)\n    val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False, num_workers=os.cpu_count()//2 or 1, pin_memory=True)\n    cw_vals = compute_class_weight('balanced', classes=np.arange(len(gesture_classes)), y=y_tr_int)\n    class_weight = torch.from_numpy(cw_vals).float()\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    class_weight = class_weight.to(device)\n    print(f\"Using device: {device}\")\n    model = TwoBranchModel(pad_len, imu_dim_final, tof_thm_aggregated_dim_final, len(gesture_classes))\n    checkpoint_path = os.path.join(EXPORT_DIR, \"latest_checkpoint.pth\")\n    if os.path.exists(checkpoint_path):\n        print(f\"--- Resuming training from checkpoint: {checkpoint_path} ---\")\n        checkpoint = torch.load(checkpoint_path, map_location='cpu', weights_only=False)\n        saved_state_dict = checkpoint['model_state_dict']\n        if any(key.startswith('_orig_mod.') for key in saved_state_dict.keys()):\n            print(\"   Checkpoint is from a compiled model. Cleaning keys...\")\n            cleaned_state_dict = {key.replace('_orig_mod.', ''): value for key, value in saved_state_dict.items()}\n        else:\n            cleaned_state_dict = saved_state_dict\n        model.load_state_dict(cleaned_state_dict)\n    else:\n        checkpoint = None \n        print(\"--- No checkpoint found, starting training from scratch. ---\")\n\n    model.to(device)\n    optimizer = optim.Adam(model.parameters(), lr=LR_INIT, weight_decay=WD)\n    lr_scheduler = LambdaLR(optimizer, lambda step: 0.5*(1+np.cos(np.pi*step/(EPOCHS*len(train_loader)))))\n\n    grad_scaler = torch.cuda.amp.GradScaler(enabled=(device.type == 'cuda'))\n    \n    if checkpoint:\n        optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n        lr_scheduler.load_state_dict(checkpoint['scheduler_state_dict'])\n        grad_scaler.load_state_dict(checkpoint['scaler_state_dict']) # FIX: use grad_scaler here\n        start_epoch = checkpoint['epoch'] + 1\n        best_val_accuracy = checkpoint['best_val_accuracy']\n        print(f\"--- Optimizer and scheduler states loaded. Resuming from epoch {start_epoch}. ---\")\n    else:\n        start_epoch = 0\n        best_val_accuracy = -1.0\n        \n    try:\n        model = torch.compile(model)\n        print(\"  Model compiled successfully.\")\n    except Exception as e:\n        print(f\"Could not compile model: {e}. Running uncompiled.\")\n    \n    # --- Training Loop ---\n    print(\"  Starting model training...\")\n    for epoch in range(start_epoch, EPOCHS):\n        model.train()\n        running_loss, correct_predictions, total_samples = 0.0, 0, 0\n        for data, targets_one_hot in train_loader:\n            data, targets_one_hot = data.to(device), targets_one_hot.to(device)\n            targets_idx = torch.argmax(targets_one_hot, dim=1)\n            with torch.cuda.amp.autocast(enabled=(device.type == 'cuda')):\n                outputs = model(data)\n                smoothed_targets = targets_one_hot * (1 - 0.1) + (0.1 / len(gesture_classes))\n                per_sample_loss = F.cross_entropy(outputs, smoothed_targets, reduction='none')\n                loss = (per_sample_loss * class_weight[targets_idx]).mean()\n            optimizer.zero_grad(set_to_none=True)\n            grad_scaler.scale(loss).backward()\n            grad_scaler.step(optimizer)\n            grad_scaler.update()\n            lr_scheduler.step()\n            running_loss += loss.item() * data.size(0)\n            _, predicted = torch.max(outputs.data, 1)\n            total_samples += targets_idx.size(0)\n            correct_predictions += (predicted == targets_idx).sum().item()\n        \n        epoch_loss = running_loss / total_samples\n        epoch_accuracy = correct_predictions / total_samples\n\n        model.eval()\n        val_loss, val_correct_predictions, val_total_samples = 0.0, 0, 0\n        with torch.no_grad():\n            for data, targets_one_hot in val_loader:\n                data, targets_one_hot = data.to(device), targets_one_hot.to(device)\n                targets_idx = torch.argmax(targets_one_hot, dim=1)\n                with torch.cuda.amp.autocast(enabled=(device.type == 'cuda')):\n                    outputs = model(data)\n                    v_loss = F.cross_entropy(outputs, targets_idx, weight=class_weight)\n                val_loss += v_loss.item() * data.size(0)\n                _, predicted = torch.max(outputs.data, 1)\n                val_total_samples += targets_idx.size(0)\n                val_correct_predictions += (predicted == targets_idx).sum().item()\n        \n        val_epoch_loss = val_loss / val_total_samples\n        val_epoch_accuracy = val_correct_predictions / val_total_samples\n\n        print(f\"Epoch {epoch+1}/{EPOCHS} | Train Loss: {epoch_loss:.4f} Acc: {epoch_accuracy:.4f} | Val Loss: {val_epoch_loss:.4f} Acc: {val_epoch_accuracy:.4f}\")\n\n        is_best = val_epoch_accuracy > best_val_accuracy\n        if is_best:\n            best_val_accuracy = val_epoch_accuracy\n            epochs_no_improve = 0\n            print(f\"  Validation accuracy improved to {best_val_accuracy:.4f}. Saving best model checkpoint.\")\n        else:\n            epochs_no_improve += 1\n        \n        if hasattr(model, '_orig_mod'):\n            model_state_to_save = model._orig_mod.state_dict()\n        else:\n            model_state_to_save = model.state_dict()\n            \n        checkpoint_state = {\n            'epoch': epoch,\n            'model_state_dict': model_state_to_save,\n            'optimizer_state_dict': optimizer.state_dict(),\n            'scheduler_state_dict': lr_scheduler.state_dict(),\n            'scaler_state_dict': grad_scaler.state_dict(),\n            'best_val_accuracy': best_val_accuracy\n        }\n        save_checkpoint(checkpoint_state, is_best=is_best, directory=EXPORT_DIR)\n\n        if epochs_no_improve >= PATIENCE:\n            print(f\"  Early stopping after {PATIENCE} epochs without improvement.\")\n            break\n    print(\"✔ Training done.\")\n\nelse:\n    # =====================================================================================\n    # INFERENCE PIPELINE (FOR KAGGLE SUBMISSION)\n    # =====================================================================================\n    print(\"▶ INFERENCE MODE\")\n    PRETRAINED_DIR = EXPORT_DIR\n    \n    print(\"  Loading artifacts...\")\n    final_feature_cols = np.load(os.path.join(PRETRAINED_DIR, \"feature_cols.npy\"), allow_pickle=True).tolist()\n    pad_len = int(np.load(os.path.join(PRETRAINED_DIR, \"sequence_maxlen.npy\")))\n    gesture_classes = np.load(os.path.join(PRETRAINED_DIR, \"gesture_classes.npy\"), allow_pickle=True)\n    \n    # FIX: Load the correct scaler into the correct variable name\n    feature_scaler = joblib.load(os.path.join(PRETRAINED_DIR, \"scaler.pkl\"))\n\n    imu_dim_final = len([c for c in final_feature_cols if c.startswith(('acc_', 'rot_'))])\n    tof_thm_aggregated_dim_final = len(final_feature_cols) - imu_dim_final\n\n    print(\"  Loading model...\")\n    best_model_path = os.path.join(PRETRAINED_DIR, \"best_checkpoint.pth\")\n    model = TwoBranchModel(pad_len, imu_dim_final, tof_thm_aggregated_dim_final, len(gesture_classes))\n    \n    checkpoint = torch.load(best_model_path, map_location='cpu', weights_only=False)\n    \n    # Use the same robust key-cleaning logic for inference\n    saved_state_dict = checkpoint['model_state_dict']\n    if any(key.startswith('_orig_mod.') for key in saved_state_dict.keys()):\n        cleaned_state_dict = {key.replace('_orig_mod.', ''): value for key, value in saved_state_dict.items()}\n    else:\n        cleaned_state_dict = saved_state_dict\n    \n    model.load_state_dict(cleaned_state_dict)\n    \n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    model.to(device)\n    model.eval()\n\n    print(\"  Model, feature_scaler, and artifacts loaded – ready for evaluation.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:44:07.902639Z","iopub.execute_input":"2025-08-18T07:44:07.902848Z","iopub.status.idle":"2025-08-18T07:52:16.611458Z","shell.execute_reply.started":"2025-08-18T07:44:07.902825Z","shell.execute_reply":"2025-08-18T07:52:16.610509Z"},"scrolled":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n#  EVALUATION METRICS (Overall Accuracy, Precision, Recall, F1)\n# =============================================================================\nfrom sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report\n\nprint(\"\\n=== Final Evaluation on Validation Set ===\")\nmodel.eval()\nall_preds, all_labels = [], []\n\nwith torch.no_grad():\n    for data, targets_one_hot in val_loader:\n        data, targets_one_hot = data.to(device), targets_one_hot.to(device)\n        targets_idx = torch.argmax(targets_one_hot, dim=1)\n        outputs = model(data)\n        preds = torch.argmax(outputs, dim=1)\n\n        all_preds.extend(preds.cpu().numpy())\n        all_labels.extend(targets_idx.cpu().numpy())\n\n# Convert to numpy arrays\nall_preds = np.array(all_preds)\nall_labels = np.array(all_labels)\n\n# Overall Accuracy\noverall_acc = accuracy_score(all_labels, all_preds)\n\n# Precision, Recall, F1 (macro and binary - binary không phù hợp với multi-class nên dùng weighted/macro)\nmacro_prec = precision_score(all_labels, all_preds, average=\"macro\", zero_division=0)\nmacro_rec = recall_score(all_labels, all_preds, average=\"macro\", zero_division=0)\nmacro_f1 = f1_score(all_labels, all_preds, average=\"macro\", zero_division=0)\n\nweighted_prec = precision_score(all_labels, all_preds, average=\"weighted\", zero_division=0)\nweighted_rec = recall_score(all_labels, all_preds, average=\"weighted\", zero_division=0)\nweighted_f1 = f1_score(all_labels, all_preds, average=\"weighted\", zero_division=0)\n\nprint(f\"Overall Accuracy : {overall_acc:.4f}\")\nprint(f\"Macro Precision  : {macro_prec:.4f}\")\nprint(f\"Macro Recall     : {macro_rec:.4f}\")\nprint(f\"Macro F1-score   : {macro_f1:.4f}\")\nprint(f\"Weighted Precision: {weighted_prec:.4f}\")\nprint(f\"Weighted Recall   : {weighted_rec:.4f}\")\nprint(f\"Weighted F1-score : {weighted_f1:.4f}\")\n\n# Optional: Full classification report\nprint(\"\\nDetailed Classification Report:\")\nprint(classification_report(all_labels, all_preds, target_names=gesture_classes))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:52:16.612670Z","iopub.execute_input":"2025-08-18T07:52:16.613402Z","iopub.status.idle":"2025-08-18T07:52:25.842708Z","shell.execute_reply.started":"2025-08-18T07:52:16.613376Z","shell.execute_reply":"2025-08-18T07:52:25.841914Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Predict","metadata":{}},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Predicts the gesture for a single sequence using the trained PyTorch model.\n    This is an optimized version with vectorized feature engineering.\n    \"\"\"\n    df_seq = sequence.to_pandas()\n\n    # --- OPTIMIZATION 1: Vectorized Feature Engineering ---\n    # IMU Features (this part was already efficient)\n    df_seq['acc_mag'] = np.sqrt(df_seq['acc_x']**2 + df_seq['acc_y']**2 + df_seq['acc_z']**2)\n    df_seq['rot_angle'] = 2 * np.arccos(df_seq['rot_w'].clip(-1, 1))\n    df_seq['acc_mag_jerk'] = df_seq['acc_mag'].diff().fillna(0)\n    df_seq['rot_angle_vel'] = df_seq['rot_angle'].diff().fillna(0)\n\n    # ToF Features (using the fast NumPy method)\n    tof_pixel_cols = [f\"tof_{i}_v{p}\" for i in range(1, 6) for p in range(64)]\n    tof_data_np = df_seq[tof_pixel_cols].replace(-1, np.nan).to_numpy()\n    reshaped_tof = tof_data_np.reshape(len(df_seq), 5, 64)\n    with warnings.catch_warnings():\n        warnings.filterwarnings('ignore', r'Mean of empty slice'); warnings.filterwarnings('ignore', r'Degrees of freedom <= 0 for slice')\n        mean_vals, std_vals = np.nanmean(reshaped_tof, axis=2), np.nanstd(reshaped_tof, axis=2)\n        min_vals, max_vals = np.nanmin(reshaped_tof, axis=2), np.nanmax(reshaped_tof, axis=2)\n    for i in range(1, 6):\n        df_seq[f'tof_{i}_mean'], df_seq[f'tof_{i}_std'] = mean_vals[:, i-1], std_vals[:, i-1]\n        df_seq[f'tof_{i}_min'], df_seq[f'tof_{i}_max'] = min_vals[:, i-1], max_vals[:, i-1]\n\n    # --- OPTIMIZATION 2: Efficient Column Reordering & Preprocessing ---\n    # Select columns in the correct order and fill any missing values\n    mat_unscaled = df_seq[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32')\n\n    # --- FIX: Use the correct 'feature_scaler' variable ---\n    mat_scaled = feature_scaler.transform(mat_unscaled)\n\n    # --- Padding ---\n    padded_sequence = np.zeros((pad_len, len(final_feature_cols)), dtype='float32')\n    seq_len = min(len(mat_scaled), pad_len)\n    padded_sequence[:seq_len] = mat_scaled[:seq_len]\n\n    # --- Prepare for Model ---\n    model_input = torch.from_numpy(padded_sequence).float().unsqueeze(0).to(device)\n\n    # --- Model Inference ---\n    model.eval()\n    with torch.no_grad():\n        outputs = model(model_input) # Get raw logits\n        # --- OPTIMIZATION 3: No need for extra softmax ---\n        predicted_idx = torch.argmax(outputs, dim=1).item()\n\n    # --- Map to Gesture Class ---\n    return str(gesture_classes[predicted_idx])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:52:25.843762Z","iopub.execute_input":"2025-08-18T07:52:25.844429Z","iopub.status.idle":"2025-08-18T07:52:25.854541Z","shell.execute_reply.started":"2025-08-18T07:52:25.844405Z","shell.execute_reply":"2025-08-18T07:52:25.853939Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Submit Inference server","metadata":{}},{"cell_type":"code","source":"import kaggle_evaluation.cmi_inference_server\n\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    inference_server.run_local_gateway(\n        data_paths=(\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n            '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n        )\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-18T07:52:25.855240Z","iopub.execute_input":"2025-08-18T07:52:25.855513Z","iopub.status.idle":"2025-08-18T07:52:34.663312Z","shell.execute_reply.started":"2025-08-18T07:52:25.855490Z","shell.execute_reply":"2025-08-18T07:52:34.662636Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}