{"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 1. Tổng quan","metadata":{}},{"cell_type":"markdown","source":"Mục tiêu là khám phá các thuật toán học máy giám sát để phát hiện hành vi dựa trên dữ liệu cảm biến. Chúng ta sẽ xử lý các vấn đề như giá trị thiếu, xây dựng đường ống (pipelines), tạo ra sơ đồ phụ thuộc, và áp dụng mã hóa một lần.\n\nChúng ta sẽ sử dụng dữ liệu từ các cảm biến để dự đoán hành vi của các đối tượng trong môi trường. Các thuật toán học máy sẽ giúp phân loại hành vi dựa trên các đặc điểm đo lường được từ dữ liệu cảm biến. Mục tiêu cuối cùng là xây dựng một mô hình có khả năng dự đoán chính xác hành vi dựa trên các tính năng cảm biến được cung cấp.","metadata":{}},{"cell_type":"markdown","source":"# Outline","metadata":{}},{"cell_type":"markdown","source":"## 1.1 Tải và Khám Phá Dữ Liệu","metadata":{}},{"cell_type":"markdown","source":"Tải dữ liệu và tiến hành kiểm tra sơ bộ (kích thước, loại dữ liệu, và các thuộc tính).\n\nPhân tích thống kê cho các tính năng trong dataset, bao gồm:\n* Giá trị thiếu và cách xử lý.\n* Các tính năng đặc biệt như giới tính, tuổi, hành vi.\n* Kiểm tra sự phân phối mục tiêu và đặc tính dữ liệu qua biểu đồ phân phối và biểu đồ hộp.","metadata":{}},{"cell_type":"markdown","source":"## 1.2 Tiền Xử Lý Dữ Liệu","metadata":{}},{"cell_type":"markdown","source":"* Xử lý các giá trị thiếu bằng các phương pháp phù hợp.\n* Tách biệt dữ liệu thành các nhóm: cảm biến IMU, ToF, và mục tiêu.\n* Chuẩn hóa và chuẩn bị dữ liệu cho mô hình.\n* Feature Engineering để tối ưu hóa đặc trưng dữ liệu.","metadata":{}},{"cell_type":"markdown","source":"## 1.3 Khám Phá Dữ Liệu và Trực Quan Hóa","metadata":{}},{"cell_type":"markdown","source":"Trực quan hóa mối quan hệ giữa các tính năng:\n* Tương quan Pearson giữa các tính năng.\n* Biểu đồ phân tán giữa các tính năng chính (ví dụ: chiều cao, trọng lượng, v.v.).\n* Biểu đồ hình tròn cho các tính năng phân loại như giới tính.\n* Tính toán mối tương quan giữa các dữ liệu cảm biến (IMU, ToF, v.v.).","metadata":{}},{"cell_type":"markdown","source":"## 1.4 Xây Dựng Mô Hình Học Máy","metadata":{}},{"cell_type":"markdown","source":"Chọn mô hình học máy phù hợp: Linear Regression, Decision Tree, Random Forest, XGBoost, MLP, và các mô hình hồi quy khác.\n\nTối ưu hóa mô hình qua Grid Search và Cross-Validation (K-fold).\n\nTối ưu hóa siêu tham số để cải thiện độ chính xác.","metadata":{}},{"cell_type":"markdown","source":"## 1.5 Đánh Giá Mô Hình và Tối Ưu Hóa","metadata":{}},{"cell_type":"markdown","source":"Đánh giá mô hình dựa trên các chỉ số như F1-score, accuracy, precision, recall.\n\nSử dụng ensemble learning như bagging và boosting để kết hợp các mô hình và cải thiện hiệu suất.\n\nTối ưu hóa mô hình cuối cùng để đảm bảo tính ổn định và khả năng dự đoán chính xác trong các bộ dữ liệu thực tế.","metadata":{}},{"cell_type":"markdown","source":"## 1.6 Kết Quả và Thảo Luận\n","metadata":{}},{"cell_type":"markdown","source":"Trình bày kết quả mô hình với các chỉ số đánh giá chi tiết.\n\nPhân tích các yếu tố ảnh hưởng đến độ chính xác của mô hình.\n\nKết luận về mô hình tối ưu và khả năng áp dụng vào thực tế.","metadata":{}},{"cell_type":"markdown","source":"# 2. Tổng quan dữ liệu","metadata":{}},{"cell_type":"markdown","source":"## 2.1 Tải dữ liệu và thư viện","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport warnings\nfrom matplotlib.ticker import MaxNLocator\nfrom mpl_toolkits.mplot3d import Axes3D\nimport os\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\nfrom sklearn.metrics import accuracy_score, f1_score\nimport joblib\nfrom scipy.spatial.transform import Rotation as R","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:33:55.965006Z","iopub.execute_input":"2025-11-09T14:33:55.965364Z","iopub.status.idle":"2025-11-09T14:33:55.972899Z","shell.execute_reply.started":"2025-11-09T14:33:55.965340Z","shell.execute_reply":"2025-11-09T14:33:55.971664Z"}},"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-11-09T14:33:55.974527Z","iopub.execute_input":"2025-11-09T14:33:55.974878Z","iopub.status.idle":"2025-11-09T14:34:26.772852Z","shell.execute_reply.started":"2025-11-09T14:33:55.974855Z","shell.execute_reply":"2025-11-09T14:34:26.771642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2.1 Khám phá dữ liệu","metadata":{}},{"cell_type":"markdown","source":"### 2.1.3 Tóm tắt thống kê các tính năng ","metadata":{}},{"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()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:34:26.774665Z","iopub.execute_input":"2025-11-09T14:34:26.775682Z","iopub.status.idle":"2025-11-09T14:34:27.801043Z","shell.execute_reply.started":"2025-11-09T14:34:26.775654Z","shell.execute_reply":"2025-11-09T14:34:27.799980Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2.1.5 kiểm tra giá trị trùng lặp","metadata":{}},{"cell_type":"code","source":"#Kiểm tra trùng lặp (Duplicate Rows): .duplicated(): Trả về Series boolean, dòng nào trùng lặp sẽ là True.\n#                                     .sum(): Đếm tổng số dòng trùng lặp.\n#Kiểm tra xem dữ liệu có bản ghi nào bị trùng không — quan trọng cho việc làm sạch dữ liệu.\n\n# Đếm các hàng trùng lặp trong train_df\ntrain_duplicates = train_df.duplicated().sum()\n\n# Đếm các hàng trùng lặp trong  test_df\ntest_duplicates = test_df.duplicated().sum()\n\n# Đếm các hàng trùng lặp trong train_dem_df (optional)\ntrain_dem_duplicates = train_dem_df.duplicated().sum()\n# Đếm các hàng trùng lặp trong test_dem_df (optional)\ntest_dem_duplicates = test_dem_df.duplicated().sum()\n\n# In số lượng dòng trùng:\nprint(f\"Number of duplicate rows in train_df: {train_duplicates}\")\nprint(f\"Number of duplicate rows in test_df: {test_duplicates}\")\nprint(f\"Number of duplicate rows in train_dem_df: {train_dem_duplicates}\")\nprint(f\"Number of duplicate rows in test_dem_df: {test_dem_duplicates}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:34:27.802002Z","iopub.execute_input":"2025-11-09T14:34:27.802321Z","iopub.status.idle":"2025-11-09T14:34:33.507443Z","shell.execute_reply.started":"2025-11-09T14:34:27.802297Z","shell.execute_reply":"2025-11-09T14:34:33.506277Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Tiền xử lý dữ liệu","metadata":{}},{"cell_type":"markdown","source":"### Tỷ lệ phần trăm Null","metadata":{}},{"cell_type":"markdown","source":"#### UTILS/HELPERS","metadata":{}},{"cell_type":"code","source":"def null_percent(df):\n    per=((df.isnull().sum()/len(df))*100).round(2)\n    return per[per>0]\n\nprint(\"Nan Values in Train data\")\nprint(null_percent(train_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:34:33.509811Z","iopub.execute_input":"2025-11-09T14:34:33.510129Z","iopub.status.idle":"2025-11-09T14:34:34.375699Z","shell.execute_reply.started":"2025-11-09T14:34:33.510108Z","shell.execute_reply":"2025-11-09T14:34:34.374805Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Các cột có tỷ lệ NaN cao nhất là các cột có tên bắt đầu với tof_5_v (5.24% NaN).\n\nCác cột khác như rot_x, rot_w có tỷ lệ NaN thấp hơn nhiều (0.64%).","metadata":{}},{"cell_type":"markdown","source":"## 2.2 Quan sát hoặc xu hướng ban đầu","metadata":{}},{"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))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:34:34.376833Z","iopub.execute_input":"2025-11-09T14:34:34.377193Z","iopub.status.idle":"2025-11-09T14:34:35.131651Z","shell.execute_reply.started":"2025-11-09T14:34:34.377170Z","shell.execute_reply":"2025-11-09T14:34:35.130684Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Merged Train Shape:** (574945, 348) - Sau khi hợp nhất, merged_train có 574,945 dòng và 348 cột, tức là dữ liệu từ train_df (574,945 dòng và 341 cột) đã được kết hợp với 7 cột từ train_dem_df (vì train_dem_df có 8 cột, nhưng 1 cột là subject đã được sử dụng làm khóa hợp nhất).\n\n**Merged Test Shape:** (107, 343) - Tương tự, merged_test có 107 dòng và 343 cột, kết quả là hợp nhất giữa test_df (107 dòng và 336 cột) với test_dem_df (8 cột, với 1 cột subject là khóa hợp nhất).","metadata":{}},{"cell_type":"code","source":"# --- Thống kê mô tả toàn bộ merged_train ---\nmerged_train.describe().T\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:34:35.132471Z","iopub.execute_input":"2025-11-09T14:34:35.132713Z","iopub.status.idle":"2025-11-09T14:34:48.098327Z","shell.execute_reply.started":"2025-11-09T14:34:35.132693Z","shell.execute_reply":"2025-11-09T14:34:48.095020Z"}},"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-11-09T14:34:48.100855Z","iopub.execute_input":"2025-11-09T14:34:48.101609Z","iopub.status.idle":"2025-11-09T14:34:49.394108Z","shell.execute_reply.started":"2025-11-09T14:34:48.101586Z","shell.execute_reply":"2025-11-09T14:34:49.393089Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"- Không có giá trị thiếu: Tất cả các cột trong bảng thống kê đều không có giá trị thiếu (Missing Count = 0), cho thấy dữ liệu của bạn rất đầy đủ.\n\n- Sự đa dạng trong dữ liệu: Các cột như row_id, sequence_id, và subject có số lượng giá trị duy nhất khá lớn, trong khi các cột như sequence_type, phase, và behavior có ít giá trị duy nhất hơn, cho thấy chúng có thể là các thông tin phân loại.\n\n- Kiểu dữ liệu: Hầu hết các cột đều có kiểu dữ liệu object, ngoại trừ sequence_counter có kiểu int64, điều này cho thấy hầu hết các cột chứa thông tin phân loại (string), chỉ có một cột chứa giá trị số nguyên.","metadata":{}},{"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-11-09T14:34:49.395266Z","iopub.execute_input":"2025-11-09T14:34:49.395605Z","iopub.status.idle":"2025-11-09T14:34:49.426187Z","shell.execute_reply.started":"2025-11-09T14:34:49.395575Z","shell.execute_reply":"2025-11-09T14:34:49.425269Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"- Không có giá trị thiếu: Các cột nhân khẩu học đều không có giá trị thiếu, cho thấy dữ liệu đầy đủ và sẵn sàng để phân tích.\n\n- Sự đa dạng trong dữ liệu: Các cột như subject, age, và height_cm có sự đa dạng về giá trị duy nhất, trong khi các cột như sex, handedness có ít giá trị duy nhất hơn, cho thấy đây là các cột phân loại.\n\n- Kiểu dữ liệu: Các cột phần lớn có kiểu dữ liệu int64 hoặc float64, cho thấy dữ liệu chủ yếu là số nguyên hoặc số thực. Các cột như subject có kiểu dữ liệu object, có thể là các chuỗi ký tự.","metadata":{}},{"cell_type":"markdown","source":"### 2.2.2. Sensor-Based Features (Tính năng dựa trên cảm biến)","metadata":{}},{"cell_type":"markdown","source":"#### **NOTE:**\n* acc_: Gia tốc kế.\n* rot_: Cảm biến quay.\n* thm_: Nhiệt độ, độ ẩm.\n* tof_: Khoảng cách.\n","metadata":{}},{"cell_type":"markdown","source":"# 3. EDA","metadata":{}},{"cell_type":"markdown","source":"## 3.1. Numerical Feature Distribution Analysis (Phân tích phân phối tính năng số) (Phân tích đơn biến)","metadata":{}},{"cell_type":"markdown","source":"### 3.1.1 Participant & Contextual Features (Tính năng của người tham gia và bối cảnh)","metadata":{}},{"cell_type":"markdown","source":"### 3.1.2 Sensor-Based Features (Tính năng dựa trên cảm biến)","metadata":{}},{"cell_type":"markdown","source":"**Mục tiêu:**\nTrích xuất các đặc trưng thống kê (mean, std, max) từ dữ liệu sensor (acceleration, rotation, temperature, ToF) theo từng sequence_id để phục vụ mô hình học máy hoặc phân tích dữ liệu.","metadata":{}},{"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-11-09T14:34:49.427224Z","iopub.execute_input":"2025-11-09T14:34:49.427527Z","iopub.status.idle":"2025-11-09T14:34:56.926102Z","shell.execute_reply.started":"2025-11-09T14:34:49.427506Z","shell.execute_reply":"2025-11-09T14:34:56.924952Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3.3 Target Feature Analysis (Univariate Analysis)","metadata":{}},{"cell_type":"markdown","source":"### 3.4.2 Sensor‐Based Differentiation Across Demographics and Physiology Features","metadata":{}},{"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-11-09T14:34:56.929671Z","iopub.execute_input":"2025-11-09T14:34:56.929969Z","iopub.status.idle":"2025-11-09T14:34:57.867008Z","shell.execute_reply.started":"2025-11-09T14:34:56.929950Z","shell.execute_reply":"2025-11-09T14:34:57.865980Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Xử lý missing values","metadata":{}},{"cell_type":"markdown","source":"### 1. IMU – Gia tốc/quaternion (acc_*, rot_w/x/y/z)","metadata":{}},{"cell_type":"code","source":"# === ToF preprocessing: replace -1 -> NaN, create mask, impute 0, keep per-sensor validity ===\ntof_pix = [c for c in df.columns if c.startswith(\"tof_\") and \"_v\" in c]\n# create mask columns for pixel validity\nfor c in tof_pix:\n    mask_col = c + \"_mask\"\n    df[mask_col] = (df[c] != -1).astype(float)   # 1.0 valid, 0.0 missing\n    # Replace -1 with 0 for numeric pipeline; keep mask to indicate missingness\n    df[c] = df[c].replace(-1, 0).astype(float)\n\n# now compute per-sensor mean + missing fraction as features\nfor 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.columns]\n    mask_cols = [c + \"_mask\" for c in cols]\n    if len(cols)==0: \n        continue\n    df[f\"tof_{i}_missing_frac\"] = 1.0 - (df[mask_cols].mean(axis=1))  # fraction missing per ts\n    df[f\"tof_{i}_mean\"] = df[cols].mean(axis=1)\n    df[f\"tof_{i}_std\"]  = df[cols].std(axis=1).fillna(0.0)\n\n# If you later collapse time dimension, keep both 'mean' and 'missing_frac' as features.\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:34:57.868037Z","iopub.execute_input":"2025-11-09T14:34:57.868282Z","iopub.status.idle":"2025-11-09T14:34:57.877637Z","shell.execute_reply.started":"2025-11-09T14:34:57.868262Z","shell.execute_reply":"2025-11-09T14:34:57.876355Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 2. Thermopile (thm_1..thm_5)","metadata":{}},{"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-11-09T14:34:57.878654Z","iopub.execute_input":"2025-11-09T14:34:57.879037Z","iopub.status.idle":"2025-11-09T14:34:57.902871Z","shell.execute_reply.started":"2025-11-09T14:34:57.879012Z","shell.execute_reply":"2025-11-09T14:34:57.901881Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3. ToF – 5 cảm biến × 64 pixel (tof_i_vj)","metadata":{}},{"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-11-09T14:34:57.904476Z","iopub.execute_input":"2025-11-09T14:34:57.904860Z","iopub.status.idle":"2025-11-09T14:34:57.924264Z","shell.execute_reply.started":"2025-11-09T14:34:57.904829Z","shell.execute_reply":"2025-11-09T14:34:57.923114Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 4. Pha/nhãn & metadata","metadata":{}},{"cell_type":"code","source":"import random, torch, numpy as np\nSEED = CONFIG.get('SEED', 42)\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:46:06.597906Z","iopub.execute_input":"2025-11-09T14:46:06.598335Z","iopub.status.idle":"2025-11-09T14:46:06.613047Z","shell.execute_reply.started":"2025-11-09T14:46:06.598309Z","shell.execute_reply":"2025-11-09T14:46:06.611824Z"}},"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-11-09T14:46:08.554517Z","iopub.execute_input":"2025-11-09T14:46:08.554893Z","iopub.status.idle":"2025-11-09T14:46:08.711245Z","shell.execute_reply.started":"2025-11-09T14:46:08.554854Z","shell.execute_reply":"2025-11-09T14:46:08.710055Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 5. Pipeline tiền xử lý","metadata":{}},{"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-11-09T14:46:51.433538Z","iopub.execute_input":"2025-11-09T14:46:51.433922Z","iopub.status.idle":"2025-11-09T14:46:51.441522Z","shell.execute_reply.started":"2025-11-09T14:46:51.433896Z","shell.execute_reply":"2025-11-09T14:46:51.440184Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Notebook 2 – Deep Learning: Mô hình chuỗi\n\n**Mục tiêu:**  \nChứng minh mô hình deep learning vượt baseline cổ điển bằng cách huấn luyện các mô hình chuỗi cho **IMU & Thermopile** (1D) và **2D CNN** cho **ToF grid**, sau đó so sánh theo từng modality.\n\n**Bao gồm:**\n- **1D CNN / LSTM / GRU / BiLSTM** cho IMU & Thermopile.  \n- **2D CNN** cho ToF grid (xử lý từng khung rồi gộp theo thời gian).  \n- **Hybrid CNN+LSTM** cho chuỗi 1D.  \n- Chia **train/val/test**, **EarlyStopping**, **LR scheduler**.  \n- So sánh: **IMU-only / ToF-only / Multimodal (concat đơn giản)**.  \n- **Regularization test**: Dropout, LayerNorm để kiểm soát overfit.  \n- **Explainability**: saliency map (1D/2D) và/hoặc attention weights để minh hoạ cảm biến/thời điểm quan trọng.\n\n👉 **Vai trò:** Benchmark DL để đối chiếu và chứng minh vượt baseline từ Notebook 1.\n","metadata":{}},{"cell_type":"code","source":"# %% [config]\nCONFIG = {\n    'DATA_PATH': '',  # ví dụ: '/mnt/data/your_dataset.npz'\n\n    # Synthetic dataset nếu không có dữ liệu thật\n    'N_SAMPLES': 900,\n    'N_CLASSES': 6,\n    'T': 64,\n    'IMU_CH': 6,\n    'THERM_CH': 8,\n    'TOF_H': 8, 'TOF_W': 8,\n\n    # Huấn luyện\n    'BATCH_SIZE': 64,\n    'MAX_EPOCHS': 10,\n    'LR': 1e-3,\n    'WEIGHT_DECAY': 1e-4,\n    'EARLY_STOP_PATIENCE': 5,\n    'VAL_SPLIT': 0.15,\n    'TEST_SPLIT': 0.15,\n\n    # Regularization\n    'DROPOUT': 0.2,\n    'USE_LAYER_NORM': True,\n\n    # Danh sách thí nghiệm cần chạy\n    'RUN_SET': [\n        'IMU_CNN1D', 'IMU_LSTM', 'IMU_GRU', 'IMU_BiLSTM',\n        'TOF_CNN2D',\n        'MULTI_CONCAT_CNN1D+CNN2D'\n    ]\n}\nCONFIG","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:46:53.562093Z","iopub.execute_input":"2025-11-09T14:46:53.562454Z","iopub.status.idle":"2025-11-09T14:46:53.572142Z","shell.execute_reply.started":"2025-11-09T14:46:53.562428Z","shell.execute_reply":"2025-11-09T14:46:53.571087Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === Cell 19: Group-based split by subject (REPLACE original split cell) ===\nfrom sklearn.model_selection import GroupShuffleSplit\nimport numpy as np\n\n# Assumptions:\n# - You have arrays for your sequences like `imu`, `thermo`, `tof`, `y`\n# - You have a parallel array `sequence_subjects` that gives subject id for each sequence\n# If your variable name for subject per-sequence is different, adapt the name below.\n\ntry:\n    sequence_subjects  # try to use existing variable\nexcept NameError:\n    # Try common alternate names\n    if 'subjects' in globals():\n        sequence_subjects = np.array(subjects)\n    elif 'seq_subjects' in globals():\n        sequence_subjects = np.array(seq_subjects)\n    else:\n        raise NameError(\n            \"Không tìm thấy biến subject cho mỗi sequence. \"\n            \"Hãy gán một mảng 1D tên `sequence_subjects` song song với dữ liệu sequence.\"\n        )\n\nN = len(sequence_subjects)\nassert N == len(y), \"Length mismatch between sequence_subjects and labels y\"\n\nSEED = CONFIG.get('SEED', 42)\n# First split: train vs temp (val+test)\ngss = GroupShuffleSplit(n_splits=1, train_size=1.0 - (CONFIG['VAL_SPLIT'] + CONFIG['TEST_SPLIT']), test_size=(CONFIG['VAL_SPLIT']+CONFIG['TEST_SPLIT']), random_state=SEED)\ntrain_idx, temp_idx = next(gss.split(np.arange(N), groups=sequence_subjects))\n\n# Second split temp into val/test 50/50 (by group)\ntemp_subjects = sequence_subjects[temp_idx]\ngss2 = GroupShuffleSplit(n_splits=1, train_size=CONFIG['VAL_SPLIT'] / (CONFIG['VAL_SPLIT'] + CONFIG['TEST_SPLIT']), test_size=CONFIG['TEST_SPLIT'] / (CONFIG['VAL_SPLIT'] + CONFIG['TEST_SPLIT']), random_state=SEED)\nval_rel_idx, test_rel_idx = next(gss2.split(np.arange(len(temp_idx)), groups=temp_subjects))\nval_idx = temp_idx[val_rel_idx]\ntest_idx = temp_idx[test_rel_idx]\n\ndef take(a, ind):\n    return a[ind]\n\nsplit = {\n    'train': {\n        'imu': take(imu, train_idx),\n        'thermo': take(thermo, train_idx),\n        'tof': take(tof, train_idx),\n        'y': take(y, train_idx),\n        'subject': take(sequence_subjects, train_idx),\n    },\n    'val': {\n        'imu': take(imu, val_idx),\n        'thermo': take(thermo, val_idx),\n        'tof': take(tof, val_idx),\n        'y': take(y, val_idx),\n        'subject': take(sequence_subjects, val_idx),\n    },\n    'test': {\n        'imu': take(imu, test_idx),\n        'thermo': take(thermo, test_idx),\n        'tof': take(tof, test_idx),\n        'y': take(y, test_idx),\n        'subject': take(sequence_subjects, test_idx),\n    },\n}\n\nfor k in split:\n    print(k, {kk: getattr(v, 'shape', None) or (len(v) if isinstance(v, (list, np.ndarray)) else None) for kk, v in split[k].items()})\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:46:56.004050Z","iopub.execute_input":"2025-11-09T14:46:56.004401Z","iopub.status.idle":"2025-11-09T14:46:56.040912Z","shell.execute_reply.started":"2025-11-09T14:46:56.004377Z","shell.execute_reply":"2025-11-09T14:46:56.039672Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 1) Cấu hình thí nghiệm\n\n- Có thể dùng dữ liệu **thật** (file `.npz`) hoặc để trống để sinh **synthetic dataset** chạy thử.\n- File `.npz` kỳ vọng chứa các mảng:\n  - `imu (N,T,IMU_CH)`\n  - `thermo (N,T,THERM_CH)`\n  - `tof (N,T,H,W)`\n  - `label (N,)`\n\n## 2) Sinh dữ liệu / Tải dữ liệu\n\n- Dữ liệu gồm ba nhánh: **IMU**, **Thermopile** (chuỗi 1D theo thời gian), **ToF** (lưới 2D theo thời gian).\n- Nếu không có file thật, mã sẽ tạo **synthetic dataset** với pattern khác nhau giữa các lớp để demo pipeline.\n","metadata":{}},{"cell_type":"code","source":"# === Add collate_fn ===\nfrom torch.nn.utils.rnn import pad_sequence\nimport torch\n\ndef collate_fn(batch):\n    # batch: list of dicts {'imu': np.array (T,C) or (C,T) depending on your storage, 'thermo', 'tof', 'y': int}\n    # Convert and pad to same length in time dimension\n    # assume imu is (C, T) in your dataset; if it's (T,C) adapt accordingly\n    imus = [torch.tensor(b['imu']).float().permute(1,0) if torch.tensor(b['imu']).ndim==2 and b['imu'].shape[0] > b['imu'].shape[1] else torch.tensor(b['imu']).float().permute(1,0) for b in batch]\n    # The above tries to ensure imu tensors become (T, C); now pad to (maxT, C) then permute to (C, maxT)\n    lengths = torch.tensor([t.shape[0] for t in imus], dtype=torch.long)\n    imus_padded = pad_sequence(imus, batch_first=True)  # (B, T, C)\n    imus_padded = imus_padded.permute(0,2,1)  # -> (B, C, T)\n\n    # thermo and tof: if already fixed-size arrays per sequence, stack directly\n    thermos = torch.stack([torch.tensor(b['thermo']).float() for b in batch])\n    tofs = torch.stack([torch.tensor(b['tof']).float() for b in batch])\n\n    ys = torch.tensor([b['y'] for b in batch], dtype=torch.long)\n    return {'imu': imus_padded, 'thermo': thermos, 'tof': tofs, 'y': ys, 'lengths': lengths}\n\n# Use collate_fn in DataLoader creation (replace loader construction)\nloaders = {}\nfor name in ['train', 'val', 'test']:\n    ds = SeqDataset(split[name])\n    loaders[name] = DataLoader(\n        ds,\n        batch_size=CONFIG['BATCH_SIZE'],\n        shuffle=(name == 'train'),\n        collate_fn=collate_fn,\n        num_workers=2,\n        pin_memory=True\n    )\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:00.171192Z","iopub.execute_input":"2025-11-09T14:47:00.171557Z","iopub.status.idle":"2025-11-09T14:47:00.183824Z","shell.execute_reply.started":"2025-11-09T14:47:00.171528Z","shell.execute_reply":"2025-11-09T14:47:00.182642Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [data]\nfrom typing import Dict\n\ndef make_synthetic(cfg: Dict):\n    N, C, T = cfg['N_SAMPLES'], cfg['N_CLASSES'], cfg['T']\n    IMU_CH, THERM_CH = cfg['IMU_CH'], cfg['THERM_CH']\n    H, W = cfg['TOF_H'], cfg['TOF_W']\n    y = np.random.randint(0, C, size=N)\n    imu = np.random.randn(N, T, IMU_CH).astype('float32')\n    thermo = (0.3*np.random.randn(N, T, THERM_CH) + 0.1).astype('float32')\n    tof = (np.random.randn(N, T, H, W)*0.2 + 2.5).astype('float32')\n    # pattern theo lớp\n    for cls in range(C):\n        idx = y==cls\n        imu[idx] += (cls - C/2) * 0.15\n        thermo[idx] += (C/2 - cls) * 0.1\n        rr, cc = np.mgrid[:H, :W]\n        center_r, center_c = (cls % H), (cls % W)\n        mask = np.exp(-((rr-center_r)**2 + (cc-center_c)**2)/(2*(H/4)**2)).astype('float32')\n        tof[idx] += mask[None, None, :, :]\n    return imu, thermo, tof, y.astype('int64')\n\nDATA_PATH = CONFIG['DATA_PATH']\nif DATA_PATH and os.path.exists(DATA_PATH):\n    data = np.load(DATA_PATH)\n    imu = data['imu'].astype('float32')\n    thermo = data['thermo'].astype('float32')\n    tof = data['tof'].astype('float32')\n    y = data['label'].astype('int64')\n    print('Loaded real dataset:', DATA_PATH)\nelse:\n    imu, thermo, tof, y = make_synthetic(CONFIG)\n    print('Using synthetic dataset')\n\nN, T, IMU_CH = imu.shape\nTHERM_CH = thermo.shape[-1]\nH, W = tof.shape[2], tof.shape[3]\nN_CLASSES = int(y.max())+1\nprint({'N': N, 'T': T, 'IMU_CH': IMU_CH, 'THERM_CH': THERM_CH, 'ToF': (H, W), 'Classes': N_CLASSES})\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:02.322266Z","iopub.execute_input":"2025-11-09T14:47:02.322616Z","iopub.status.idle":"2025-11-09T14:47:02.534232Z","shell.execute_reply.started":"2025-11-09T14:47:02.322589Z","shell.execute_reply":"2025-11-09T14:47:02.533086Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3) Chia tập train / val / test\n\n- Trộn chỉ số mẫu rồi cắt theo tỷ lệ trong `CONFIG`:\n  - `TEST_SPLIT` → tập test\n  - `VAL_SPLIT` → tập validation\n  - phần còn lại → tập train\n\n## 4) Dataset & Dataloader (PyTorch)\n\n- `SeqDataset` trả về 1 mẫu gồm: `imu (T, IMU_CH)`, `thermo (T, THERM_CH)`, `tof (T, H, W)`, `y (int)`.\n- `DataLoader`:\n  - `shuffle=True` cho **train**\n  - batch size lấy từ `CONFIG['BATCH_SIZE']`\n","metadata":{}},{"cell_type":"code","source":"# === Reworked encoders: ensure outputs are (B, feat) ===\nimport torch.nn as nn\nimport torch.nn.functional as F\n\nclass LayerNorm1D(nn.Module):\n    def __init__(self, n_channels):\n        super().__init__()\n        self.ln = nn.LayerNorm(n_channels)\n    def forward(self, x):\n        # x: [B, C, T] -> [B, T, C] -> LN -> [B, C, T]\n        x = x.permute(0, 2, 1)\n        x = self.ln(x)\n        return x.permute(0, 2, 1)\n\nclass CNN1DEncoder(nn.Module):\n    def __init__(self, in_ch, hidden=64, dropout=0.0, use_ln=True):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Conv1d(in_ch, hidden, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.Conv1d(hidden, hidden, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.AdaptiveAvgPool1d(1),  # -> (B, hidden, 1)\n            nn.Flatten(),             # -> (B, hidden)\n        )\n        self.dropout = nn.Dropout(dropout)\n        self.use_ln = use_ln\n        if use_ln:\n            self.ln = LayerNorm1D(hidden)\n\n    def forward(self, x):\n        # expect x: (B, C, T)\n        out = self.net(x)\n        out = self.dropout(out)\n        return out  # (B, hidden)\n\nclass CNN2DEncoder(nn.Module):\n    def __init__(self, in_ch=1, hidden=64, dropout=0.0):\n        super().__init__()\n        self.conv = nn.Sequential(\n            nn.Conv2d(in_ch, hidden//2, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.Conv2d(hidden//2, hidden, kernel_size=3, padding=1),\n            nn.ReLU(),\n            nn.AdaptiveAvgPool2d((1,1)),  # (B, hidden, 1,1)\n        )\n        self.flatten = nn.Flatten()\n        self.dropout = nn.Dropout(dropout)\n\n    def forward(self, x):\n        # expect x: (B, in_ch, H, W)\n        out = self.conv(x)\n        out = self.flatten(out)   # (B, hidden)\n        out = self.dropout(out)\n        return out\n\nclass BiLSTMEncoder(nn.Module):\n    def __init__(self, in_ch, hidden=128, n_layers=1, dropout=0.0):\n        super().__init__()\n        self.rnn = nn.LSTM(input_size=in_ch, hidden_size=hidden, num_layers=n_layers, batch_first=True, bidirectional=True, dropout=dropout if n_layers>1 else 0.0)\n        self.fc = nn.Linear(hidden*2, hidden)  # project bidir hidden to fixed size\n\n    def forward(self, x, lengths=None):\n        # expect x: (B, C, T) -> transpose to (B, T, C) for RNN\n        x_rnn = x.permute(0,2,1)\n        if lengths is not None:\n            from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence\n            packed = pack_padded_sequence(x_rnn, lengths.cpu(), batch_first=True, enforce_sorted=False)\n            packed_out, (h_n, c_n) = self.rnn(packed)\n            # h_n: (num_layers*2, B, hidden)\n            h = torch.cat([h_n[-2], h_n[-1]], dim=1)  # (B, hidden*2)\n        else:\n            out, (h_n, c_n) = self.rnn(x_rnn)\n            h = torch.cat([h_n[-2], h_n[-1]], dim=1)\n        h = self.fc(h)  # (B, hidden)\n        return h\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:06.060019Z","iopub.execute_input":"2025-11-09T14:47:06.060359Z","iopub.status.idle":"2025-11-09T14:47:06.078546Z","shell.execute_reply.started":"2025-11-09T14:47:06.060334Z","shell.execute_reply":"2025-11-09T14:47:06.077549Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [dataset]\nif HAS_TORCH:\n    class SeqDataset(Dataset):\n        def __init__(self, pack):\n            self.imu = pack['imu']        # (N, T, IMU_CH)\n            self.thermo = pack['thermo']  # (N, T, THERM_CH)\n            self.tof = pack['tof']        # (N, T, H, W)\n            self.y = pack['y']            # (N,)\n        def __len__(self):\n            return self.y.shape[0]\n        def __getitem__(self, i):\n            return {\n                'imu': torch.from_numpy(self.imu[i]),\n                'thermo': torch.from_numpy(self.thermo[i]),\n                'tof': torch.from_numpy(self.tof[i]),\n                'y': torch.tensor(int(self.y[i])),\n            }\n\n    loaders = {}\n    for name in ['train', 'val', 'test']:\n        ds = SeqDataset(split[name])\n        loaders[name] = DataLoader(\n            ds,\n            batch_size=CONFIG['BATCH_SIZE'],\n            shuffle=(name == 'train')\n        )\nelse:\n    print('Chưa có PyTorch – bỏ qua bước Dataset/Dataloader')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:09.286867Z","iopub.execute_input":"2025-11-09T14:47:09.287187Z","iopub.status.idle":"2025-11-09T14:47:09.297021Z","shell.execute_reply.started":"2025-11-09T14:47:09.287165Z","shell.execute_reply":"2025-11-09T14:47:09.296079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# after training finishes for tag\nhist_df.to_csv(f\"history_{tag}.csv\", index=False)\n# model already saved when best improved\n# save metrics summary\nwith open(f\"metrics_{tag}.json\", \"w\") as f:\n    json.dump({'best_final': best_final, 'config': CONFIG}, f)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:13.666339Z","iopub.execute_input":"2025-11-09T14:47:13.666668Z","iopub.status.idle":"2025-11-09T14:47:13.676218Z","shell.execute_reply.started":"2025-11-09T14:47:13.666644Z","shell.execute_reply":"2025-11-09T14:47:13.675044Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5) Kiến trúc mô hình\n\n### 5.1 Encoder 1D (cho IMU & Thermopile)\n- **CNN1DEncoder**: 2 conv + (tuỳ chọn) **LayerNorm**, gộp thời gian bằng **AdaptiveAvgPool1d**.\n- **LSTMEncoder / GRUEncoder**: lấy **timestep cuối** làm đại diện.\n- **BiLSTM**: LSTM 2 chiều (bidir=True).\n- **CNN1D+LSTM** (hybrid): conv 1D → (LN) → LSTM.\n\n### 5.2 Encoder 2D (cho ToF)\n- **CNN2DEncoder**: xử lý từng frame (T × H × W) → pooling không gian → **trung bình theo thời gian**.\n\n### 5.3 Đầu phân loại\n- **ClassifierHead**: Linear → ReLU → Linear (kèm Dropout).\n- Metric chính sẽ là **macro-F1** ở phần huấn luyện.\n\n### 5.4 Mô hình đầy đủ theo modality\n- **ModelIMU(kind)**: chọn encoder 1D theo `kind ∈ {CNN1D, LSTM, GRU, BiLSTM, CNN1D+LSTM}`.\n- **ModelToF**: dùng CNN2DEncoder.\n- **ModelMultimodalConcat**: mã hoá **IMU**, **Thermo**, *(tuỳ chọn)* **ToF**, sau đó **concat** đặc trưng và phân loại.\n","metadata":{}},{"cell_type":"code","source":"# %% [models]\nif HAS_TORCH:\n    import torch\n    import torch.nn as nn\n    import torch.nn.functional as F\n\n    # ---- LayerNorm cho tensor 1D (B, C, T) ----\n    class LayerNorm1D(nn.Module):\n        def __init__(self, n_channels):\n            super().__init__()\n            self.ln = nn.LayerNorm(n_channels)\n        def forward(self, x):\n            # x: [B, C, T] -> [B, T, C] -> LN -> [B, C, T]\n            x = x.permute(0, 2, 1)\n            x = self.ln(x)\n            return x.permute(0, 2, 1)\n\n    # ---- CNN1D Encoder ----\n    class CNN1DEncoder(nn.Module):\n        def __init__(self, in_ch, hidden=64, dropout=0.0, use_ln=True):\n            super().__init__()\n            self.conv1 = nn.Conv1d(in_ch, hidden, kernel_size=5, padding=2)\n            self.conv2 = nn.Conv1d(hidden, hidden, kernel_size=3, padding=1)\n            self.ln = LayerNorm1D(hidden) if use_ln else nn.Identity()\n            self.pool = nn.AdaptiveAvgPool1d(1)\n            self.drop = nn.Dropout(dropout)\n        def forward(self, x):\n            # x: [B, T, C] -> [B, C, T]\n            x = x.transpose(1, 2)\n            x = F.relu(self.conv1(x))\n            x = F.relu(self.conv2(x))\n            x = self.ln(x)\n            x = self.pool(x).squeeze(-1)  # [B, hidden]\n            return self.drop(x)\n\n    # ---- LSTM/GRU Encoders ----\n    class LSTMEncoder(nn.Module):\n        def __init__(self, in_ch, hidden=64, bidir=False, dropout=0.0):\n            super().__init__()\n            self.rnn = nn.LSTM(\n                input_size=in_ch, hidden_size=hidden,\n                batch_first=True, bidirectional=bidir,\n                dropout=dropout if bidir else 0.0\n            )\n            self.out_ch = hidden * (2 if bidir else 1)\n        def forward(self, x):\n            out, _ = self.rnn(x)\n            return out[:, -1, :]  # [B, out_ch]\n\n    class GRUEncoder(nn.Module):\n        def __init__(self, in_ch, hidden=64, bidir=False, dropout=0.0):\n            super().__init__()\n            self.rnn = nn.GRU(\n                input_size=in_ch, hidden_size=hidden,\n                batch_first=True, bidirectional=bidir,\n                dropout=dropout if bidir else 0.0\n            )\n            self.out_ch = hidden * (2 if bidir else 1)\n        def forward(self, x):\n            out, _ = self.rnn(x)\n            return out[:, -1, :]\n\n    # ---- CNN1D + LSTM (Hybrid) ----\n    class CNN1D_LSTM_Encoder(nn.Module):\n        def __init__(self, in_ch, hidden=64, dropout=0.0, use_ln=True):\n            super().__init__()\n            self.conv = nn.Conv1d(in_ch, hidden, kernel_size=5, padding=2)\n            self.ln = LayerNorm1D(hidden) if use_ln else nn.Identity()\n            self.rnn = nn.LSTM(hidden, hidden, batch_first=True)\n            self.drop = nn.Dropout(dropout)\n        def forward(self, x):\n            h = x.transpose(1, 2)\n            h = F.relu(self.conv(h))\n            h = self.ln(h).transpose(1, 2)\n            out, _ = self.rnn(h)\n            return self.drop(out[:, -1, :])\n\n    # ---- CNN2D Encoder cho ToF ----\n    class CNN2DEncoder(nn.Module):\n        def __init__(self, in_ch=1, hidden=64, dropout=0.0):\n            super().__init__()\n            self.conv1 = nn.Conv2d(in_ch, 32, kernel_size=3, padding=1)\n            self.conv2 = nn.Conv2d(32, hidden, kernel_size=3, padding=1)\n            self.pool = nn.AdaptiveAvgPool2d((1, 1))\n            self.drop = nn.Dropout(dropout)\n        def forward(self, x):\n            # x: [B, T, 1, H, W]\n            B, T, C, H, W = x.shape\n            h = x.reshape(B*T, C, H, W)\n            h = F.relu(self.conv1(h))\n            h = F.relu(self.conv2(h))\n            h = self.pool(h).squeeze(-1).squeeze(-1)  # [B*T, hidden]\n            h = h.view(B, T, -1).mean(dim=1)          # [B, hidden]\n            return self.drop(h)\n\n    # ---- Classifier ----\n    class ClassifierHead(nn.Module):\n        def __init__(self, in_ch, n_classes, dropout=0.0):\n            super().__init__()\n            hid = in_ch // 2 if in_ch >= 64 else max(32, in_ch)\n            self.net = nn.Sequential(\n                nn.Dropout(dropout),\n                nn.Linear(in_ch, hid), nn.ReLU(),\n                nn.Linear(hid, n_classes)\n            )\n        def forward(self, x):\n            return self.net(x)\n\n    # ---- Multimodal Model ----\n    class ModelMultimodalConcat(nn.Module):\n        def __init__(self, imu_kind, thermo_kind, use_tof, in_imu, in_thermo, n_classes, cfg):\n            super().__init__()\n            dp = cfg['DROPOUT']\n            ln = cfg['USE_LAYER_NORM']\n\n            # Encoders\n            self.enc_imu = CNN1DEncoder(in_imu, dropout=dp, use_ln=ln) if imu_kind == 'CNN1D' else LSTMEncoder(in_imu, hidden=64, bidir=False, dropout=dp)\n            self.enc_thermo = CNN1DEncoder(in_thermo, dropout=dp, use_ln=ln) if thermo_kind == 'CNN1D' else LSTMEncoder(in_thermo, hidden=64, bidir=False, dropout=dp)\n\n            self.use_tof = use_tof\n            if use_tof:\n                self.enc_tof = CNN2DEncoder(in_ch=1, hidden=64, dropout=dp)\n\n            # Feature dims\n            d_imu = 64\n            d_th = 64\n            d_tof = 64 if use_tof else 0\n\n            self.head = ClassifierHead(d_imu + d_th + d_tof, n_classes, dropout=dp)\n\n        def forward(self, imu, thermo, tof=None):\n            feats = [self.enc_imu(imu), self.enc_thermo(thermo)]\n            if self.use_tof and tof is not None:\n                feats.append(self.enc_tof(tof))\n\n            # === assert sau khi chạy encoder ===\n            for f in feats:\n                assert f.dim() == 2, f\"Expected (B, feat) got {f.shape}\"\n\n            return self.head(torch.cat(feats, dim=-1))\n\nelse:\n    print('Chưa có torch – bỏ qua định nghĩa mô hình')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:31.235814Z","iopub.execute_input":"2025-11-09T14:47:31.236917Z","iopub.status.idle":"2025-11-09T14:47:31.268294Z","shell.execute_reply.started":"2025-11-09T14:47:31.236885Z","shell.execute_reply":"2025-11-09T14:47:31.267043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [test encoder outputs]\nimport torch\n\nB, T, C = 8, 100, 6\nx = torch.randn(B, T, C)\n\nenc_imu = CNN1DEncoder(in_ch=C, hidden=64)\nout = enc_imu(x)\nprint(\"Output shape CNN1DEncoder:\", out.shape)\nassert out.shape == (B, 64), f\"Expected (B,64), got {out.shape}\"\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:35.346165Z","iopub.execute_input":"2025-11-09T14:47:35.346491Z","iopub.status.idle":"2025-11-09T14:47:35.361243Z","shell.execute_reply.started":"2025-11-09T14:47:35.346468Z","shell.execute_reply":"2025-11-09T14:47:35.360385Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6) Tiện ích huấn luyện\n\n- **Metric chính**: `macro-F1` (cân bằng giữa các lớp).\n- **EarlyStopping**: dừng sớm khi `val_loss` không cải thiện sau `patience` epoch.\n- **LR Scheduler**: `ReduceLROnPlateau` (giảm LR khi `val_loss` không giảm).\n- Vòng lặp:\n  - `run_epoch(...)` cho train/val (tự phát hiện mô hình IMU/ToF/Multimodal).\n  - `fit_model(...)` huấn luyện đầy đủ, lưu lịch sử loss/F1/LR theo epoch, tải lại **trạng thái tốt nhất** (best `val_loss`).\n","metadata":{}},{"cell_type":"code","source":"from sklearn.utils.class_weight import compute_class_weight\nimport numpy as np\nunique_classes = np.unique(split['train']['y'])\nclass_weights = compute_class_weight('balanced', classes=unique_classes, y=split['train']['y'])\n# map to tensor in correct order (assumes labels 0..K-1)\nweights_t = torch.tensor(class_weights, dtype=torch.float).to(DEVICE)\ncriterion = nn.CrossEntropyLoss(weight=weights_t)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:37.070656Z","iopub.execute_input":"2025-11-09T14:47:37.072005Z","iopub.status.idle":"2025-11-09T14:47:37.082183Z","shell.execute_reply.started":"2025-11-09T14:47:37.071959Z","shell.execute_reply":"2025-11-09T14:47:37.080939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [train_utils]\nif HAS_TORCH:\n    from typing import Dict, Tuple, List\n\n    # ---- Macro-F1 tiện ích ----\n    def macro_f1(y_true, y_pred):\n        return f1_score(y_true, y_pred, average='macro')\n\n    # ---- EarlyStopping đơn giản ----\n    class EarlyStopping:\n        def __init__(self, patience: int = 5, min_delta: float = 0.0):\n            self.patience = patience\n            self.min_delta = min_delta\n            self.best = None\n            self.count = 0\n            self.stop = False\n\n        def step(self, value: float):\n            # value: val_loss hiện tại\n            if self.best is None or value < self.best - self.min_delta:\n                self.best = value\n                self.count = 0\n            else:\n                self.count += 1\n                if self.count >= self.patience:\n                    self.stop = True\n\n    # ---- Chạy một epoch (train hoặc val) ----\n    def run_epoch(model, loader, criterion, optimizer=None):\n        \"\"\"\n        Trả về: (mean_loss, macro_f1)\n        \"\"\"\n        is_train = optimizer is not None\n        model.train() if is_train else model.eval()\n\n        total_loss = 0.0\n        ys, yhs = [], []\n\n        for b in loader:\n            imu_b = b['imu'].to(DEVICE).float()\n            th_b  = b['thermo'].to(DEVICE).float()\n            tof_b = b['tof'].to(DEVICE).float().unsqueeze(2)  # [B, T, 1, H, W]\n            y_b   = b['y'].to(DEVICE)\n\n            if is_train:\n                optimizer.zero_grad()\n\n            # Chọn đường đi forward theo loại mô hình\n            if isinstance(model, ModelIMU):\n                logits = model(imu_b)                       # IMU-only\n            elif isinstance(model, ModelToF):\n                 logits = model(tof_b)                       # ToF-only\n            else:\n                 logits = model(imu_b, th_b, tof_b)\n\n            loss = criterion(logits, y_b)\n\n            if is_train:\n                loss.backward()\n                optimizer.step()\n\n            total_loss += loss.item() * y_b.size(0)\n            ys.append(y_b.detach().cpu().numpy())\n            yhs.append(logits.detach().cpu().argmax(dim=1).numpy())\n\n        ys = np.concatenate(ys)\n        yhs = np.concatenate(yhs)\n        return total_loss / len(loader.dataset), macro_f1(ys, yhs)\n\n    # ---- Huấn luyện đầy đủ với EarlyStopping + LR scheduler ----\n    def fit_model(model, loaders: Dict[str, torch.utils.data.DataLoader], cfg: Dict, tag: str = 'model'):\n        model = model.to(DEVICE)\n        criterion = nn.CrossEntropyLoss()\n        optimizer = Adam(model.parameters(), lr=cfg['LR'], weight_decay=cfg['WEIGHT_DECAY'])\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n            optimizer, mode='min', factor=0.5, patience=2\n        )\n        early = EarlyStopping(patience=cfg['EARLY_STOP_PATIENCE'])\n\n        hist = {'epoch': [], 'train_loss': [], 'val_loss': [], 'train_f1': [], 'val_f1': [], 'lr': []}\n        best_state = None\n        best_val = float('inf')\n\n        for ep in range(cfg['MAX_EPOCHS']):\n            tr_loss, tr_f1 = run_epoch(model, loaders['train'], criterion, optimizer)\n            vl_loss, vl_f1 = run_epoch(model, loaders['val'],   criterion, optimizer=None)\n\n            scheduler.step(vl_loss)\n            lr_now = optimizer.param_groups[0]['lr']\n\n            hist['epoch'].append(ep + 1)\n            hist['train_loss'].append(tr_loss)\n            hist['val_loss'].append(vl_loss)\n            hist['train_f1'].append(tr_f1)\n            hist['val_f1'].append(vl_f1)\n            hist['lr'].append(lr_now)\n\n            # Lưu trạng thái tốt nhất theo val_loss\n            if vl_loss < best_val:\n                best_val = vl_loss\n                best_state = {k: v.detach().cpu().clone() for k, v in model.state_dict().items()}\n\n            # In log ngắn gọn\n            print(f\"[EP {ep+1:02d}] \"\n                  f\"tr_loss={tr_loss:.4f} val_loss={vl_loss:.4f} \"\n                  f\"tr_f1={tr_f1:.4f} val_f1={vl_f1:.4f} lr={lr_now:.2e}\")\n\n            # Early stopping\n            early.step(vl_loss)\n            if early.stop:\n                print(\"Dừng sớm (EarlyStopping).\")\n                break\n\n        # Tải lại trọng số tốt nhất\n        if best_state is not None:\n            model.load_state_dict(best_state)\n\n        return model, hist\nelse:\n    print('Chưa có torch – bỏ qua utilities huấn luyện')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:38.802659Z","iopub.execute_input":"2025-11-09T14:47:38.803152Z","iopub.status.idle":"2025-11-09T14:47:38.824189Z","shell.execute_reply.started":"2025-11-09T14:47:38.803127Z","shell.execute_reply":"2025-11-09T14:47:38.822656Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7) Chạy thí nghiệm & so sánh theo modality\n\n- Chạy lần lượt các cấu hình trong `CONFIG['RUN_SET']`:\n  - **IMU-only**: `IMU_CNN1D`, `IMU_LSTM`, `IMU_GRU`, `IMU_BiLSTM`\n  - **ToF-only**: `TOF_CNN2D`\n  - **Multimodal**: `MULTI_CONCAT_CNN1D+CNN2D` (IMU+Thermo+(ToF))\n\n- Mỗi mô hình:\n  - Huấn luyện với **EarlyStopping** + **ReduceLROnPlateau**.\n  - Ghi lại **macro-F1** trên train/val/test.\n  - Tổng hợp bảng kết quả để dễ so sánh mô hình & modality.\n\n> Lưu ý:\n> - Bạn có thể chỉnh danh sách `RUN_SET` để chạy ít mô hình hơn cho nhanh.\n> - Nếu môi trường chưa cài `torch`, phần này sẽ bị bỏ qua.\n","metadata":{}},{"cell_type":"code","source":"# %% [experiments]\nimport pandas as pd\nfrom pathlib import Path\n\nresults = []\nhistories = {}\n\nif HAS_TORCH:\n    @torch.no_grad()\n    def evaluate_test(model, loader):\n        model.eval()\n        ys, yhs = [], []\n        for b in loader:\n            imu_b = b['imu'].to(DEVICE).float()\n            th_b  = b['thermo'].to(DEVICE).float()\n            tof_b = b['tof'].to(DEVICE).float().unsqueeze(2)  # [B, T, 1, H, W]\n            y_b   = b['y'].to(DEVICE)\n\n            if isinstance(model, ModelIMU):\n                logits = model(imu_b)\n            elif isinstance(model, ModelToF):\n                logits = model(tof_b)\n            else:\n                logits = model(imu_b, th_b, tof_b)\n\n            ys.append(y_b.cpu().numpy())\n            yhs.append(logits.argmax(dim=1).cpu().numpy())\n\n        ys = np.concatenate(ys)\n        yhs = np.concatenate(yhs)\n        return f1_score(ys, yhs, average='macro')\n\n    RUN_SET = CONFIG['RUN_SET']\n    for tag in RUN_SET:\n        if tag.startswith('IMU_'):\n            kind = tag.split('_', 1)[1]\n            model = ModelIMU(kind, in_ch=IMU_CH, n_classes=N_CLASSES, cfg=CONFIG)\n            modality = 'IMU'\n        elif tag == 'TOF_CNN2D':\n            model = ModelToF(n_classes=N_CLASSES, cfg=CONFIG)\n            modality = 'ToF'\n        elif tag.startswith('MULTI_CONCAT'):\n            model = ModelMultimodalConcat(\n                imu_kind='CNN1D',\n                thermo_kind='CNN1D',\n                use_tof=True,\n                in_imu=IMU_CH, in_thermo=THERM_CH,\n                n_classes=N_CLASSES, cfg=CONFIG\n            )\n            modality = 'Multimodal'\n        else:\n            print(f'Bỏ qua tag không hỗ trợ: {tag}')\n            continue\n\n        model, hist = fit_model(model, loaders, CONFIG, tag)\n        hist_df = pd.DataFrame(hist)\n        histories[tag] = hist_df\n\n        tr_f1 = hist_df['train_f1'].iloc[-1]\n        val_f1 = hist_df['val_f1'].iloc[-1]\n        te_f1 = evaluate_test(model, loaders['test'])\n\n        results.append({\n            'exp': tag,\n            'modality': modality,\n            'train_f1': tr_f1,\n            'val_f1': val_f1,\n            'test_f1': te_f1\n        })\n        print(f\"==> {tag}: test macro-F1 = {te_f1:.4f}\")\n\n    res_df = pd.DataFrame(results).sort_values('test_f1', ascending=False)\n    display(res_df)\n\n    # Lưu bảng kết quả (tùy chọn)\n    out_dir = Path('/mnt/data')\n    out_dir.mkdir(parents=True, exist_ok=True)\n    res_path = out_dir / 'dl_results.csv'\n    res_df.to_csv(res_path, index=False)\n    print('Đã lưu bảng kết quả tại:', res_path)\nelse:\n    print('Chưa có PyTorch – bỏ qua phần chạy thí nghiệm')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:47:42.991184Z","iopub.execute_input":"2025-11-09T14:47:42.991520Z","iopub.status.idle":"2025-11-09T14:48:59.733124Z","shell.execute_reply.started":"2025-11-09T14:47:42.991494Z","shell.execute_reply":"2025-11-09T14:48:59.732055Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [experiments]\nimport pandas as pd\nimport numpy as np\nimport time\nfrom pathlib import Path\nfrom sklearn.metrics import f1_score\n\n# === Đảm bảo macro_f1 là hàm ===\ndef macro_f1(y_true, y_pred):\n    \"\"\"Compute macro F1 safely\"\"\"\n    return f1_score(y_true, y_pred, average='macro')\n\nresults = []\nhistories = {}\n\nif HAS_TORCH:\n    @torch.no_grad()\n    def evaluate_test_full(model, loader, non_target_class=6):\n        \"\"\"\n        Tính Binary F1, Macro F1 và Final Score.\n        Binary F1: target (0–5) vs non-target (6)\n        Macro F1: F1 trung bình giữa các lớp (gộp non-target)\n        Final Score = (Binary F1 + Macro F1) / 2\n        \"\"\"\n        model.eval()\n        ys, yhs = [], []\n\n        for b in loader:\n            imu_b = b['imu'].to(DEVICE).float()\n            th_b  = b['thermo'].to(DEVICE).float()\n            tof_b = b['tof'].to(DEVICE).float().unsqueeze(2)\n            y_b   = b['y'].to(DEVICE)\n\n            # Forward theo loại model\n            if isinstance(model, ModelIMU):\n                logits = model(imu_b)\n            elif isinstance(model, ModelToF):\n                logits = model(tof_b)\n            else:\n                logits = model(imu_b, th_b, tof_b)\n\n            ys.append(y_b.cpu().numpy())\n            yhs.append(logits.argmax(dim=1).cpu().numpy())\n\n        ys = np.concatenate(ys)\n        yhs = np.concatenate(yhs)\n\n        # Binary F1: target (0–5) vs non-target (6)\n        y_true_binary = (ys != non_target_class).astype(int)\n        y_pred_binary = (yhs != non_target_class).astype(int)\n        binary_f1 = f1_score(y_true_binary, y_pred_binary, average='binary')\n\n        # Macro F1: trên toàn bộ lớp (gộp non-target)\n        macro_f1_value = f1_score(ys, yhs, average='macro')\n\n        # Final score\n        final_score = (binary_f1 + macro_f1_value) / 2\n        return binary_f1, macro_f1_value, final_score\n\n    # === Vòng lặp chạy các mô hình trong RUN_SET ===\n    RUN_SET = CONFIG['RUN_SET']\n    for tag in RUN_SET:\n        # --- Chọn model ---\n        if tag.startswith('IMU_'):\n            kind = tag.split('_', 1)[1]\n            model = ModelIMU(kind, in_ch=IMU_CH, n_classes=N_CLASSES, cfg=CONFIG)\n            modality = 'IMU'\n        elif tag == 'TOF_CNN2D':\n            model = ModelToF(n_classes=N_CLASSES, cfg=CONFIG)\n            modality = 'ToF'\n        elif tag.startswith('MULTI_CONCAT'):\n            model = ModelMultimodalConcat(\n                imu_kind='CNN1D',\n                thermo_kind='CNN1D',\n                use_tof=True,\n                in_imu=IMU_CH, in_thermo=THERM_CH,\n                n_classes=N_CLASSES, cfg=CONFIG\n            )\n            modality = 'Multimodal'\n        else:\n            print(f'⚠️ Bỏ qua tag không hỗ trợ: {tag}')\n            continue\n\n        # --- Huấn luyện ---\n        t0 = time.time()\n        model, hist = fit_model(model, loaders, CONFIG, tag)\n        train_time = time.time() - t0\n\n        hist_df = pd.DataFrame(hist)\n        histories[tag] = hist_df\n\n        tr_f1 = hist_df['train_f1'].iloc[-1]\n        val_f1 = hist_df['val_f1'].iloc[-1]\n        val_acc = hist_df['val_acc'].iloc[-1] if 'val_acc' in hist_df.columns else None\n\n        # --- Đánh giá ---\n        t1 = time.time()\n        binary_f1, macro_f1_value, final_score = evaluate_test_full(model, loaders['test'])\n        inference_time = time.time() - t1\n\n        params_M = sum(p.numel() for p in model.parameters()) / 1e6\n\n        results.append({\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T15:13:10.871513Z","iopub.execute_input":"2025-11-09T15:13:10.871908Z","iopub.status.idle":"2025-11-09T15:13:10.888386Z","shell.execute_reply.started":"2025-11-09T15:13:10.871878Z","shell.execute_reply":"2025-11-09T15:13:10.887041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === Saliency example for CNN1D encoder ===\ndef saliency_map(model, sample_input, target_label, device=DEVICE):\n    # sample_input: tensor shape (1, C, T)\n    model.eval()\n    x = sample_input.to(device).float().requires_grad_(True)\n    out = model(x)  # adapt if model returns (logits, attn)\n    if isinstance(out, tuple):\n        logits = out[0]\n    else:\n        logits = out\n    score = logits[0, target_label]\n    model.zero_grad()\n    score.backward()\n    sal = x.grad.abs().detach().cpu().numpy()  # (1,C,T)\n    channel_importance = sal.mean(axis=2).squeeze()\n    return sal.squeeze(), channel_importance\n\n# Usage example:\n# sample = next(iter(loaders['val']))\n# s, ch_imp = saliency_map(model.enc_imu, sample['imu'][0:1], target_label=pred_label)\n# plot ch_imp\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:49:26.789248Z","iopub.execute_input":"2025-11-09T14:49:26.789719Z","iopub.status.idle":"2025-11-09T14:49:26.797959Z","shell.execute_reply.started":"2025-11-09T14:49:26.789675Z","shell.execute_reply":"2025-11-09T14:49:26.796941Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8) Kiểm soát overfit: Dropout & LayerNorm\n\n- Mục tiêu: so sánh đường cong **train/val loss** và **macro-F1** khi:\n  1. Không dùng dropout, không dùng layer norm.\n  2. Có dropout (0.5) + layer norm.\n\n- Thí nghiệm nhanh với backbone `IMU_CNN1D`.\n- Kỳ vọng: khi có Dropout + LayerNorm, mô hình ít bị overfit hơn, đường cong train–val sát nhau hơn.\n","metadata":{}},{"cell_type":"code","source":"# %% [regularization]\nif HAS_TORCH:\n    def quick_run_imu_cnn1d(dropout, use_ln):\n        cfg2 = CONFIG.copy()\n        cfg2['DROPOUT'] = dropout\n        cfg2['USE_LAYER_NORM'] = use_ln\n        model = ModelIMU('CNN1D', in_ch=IMU_CH, n_classes=N_CLASSES, cfg=cfg2)\n        model, hist = fit_model(model, loaders, cfg2, tag=f'IMU_CNN1D_dp{dropout}_ln{use_ln}')\n        return pd.DataFrame(hist)\n\n    \n    # Chạy 2 cấu hình: không regularization vs có regularization\n    h1 = quick_run_imu_cnn1d(dropout=0.0, use_ln=False)\n    h2 = quick_run_imu_cnn1d(dropout=0.5, use_ln=True)\n\n    # Vẽ loss curves\n    plt.figure()\n    plt.plot(h1['epoch'], h1['train_loss'], label='train_loss no-reg')\n    plt.plot(h1['epoch'], h1['val_loss'], label='val_loss no-reg')\n    plt.plot(h2['epoch'], h2['train_loss'], label='train_loss reg')\n    plt.plot(h2['epoch'], h2['val_loss'], label='val_loss reg')\n    plt.title('Regularization: Loss Curves')\n    plt.legend(); plt.tight_layout(); plt.show()\n\n    # Vẽ macro-F1 curves\n    plt.figure()\n    plt.plot(h1['epoch'], h1['train_f1'], label='train_f1 no-reg')\n    plt.plot(h1['epoch'], h1['val_f1'], label='val_f1 no-reg')\n    plt.plot(h2['epoch'], h2['train_f1'], label='train_f1 reg')\n    plt.plot(h2['epoch'], h2['val_f1'], label='val_f1 reg')\n    plt.title('Regularization: Macro-F1 Curves')\n    plt.legend(); plt.tight_layout(); plt.show()\nelse:\n    print('Chưa có PyTorch – bỏ qua kiểm tra regularization')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:49:32.837454Z","iopub.execute_input":"2025-11-09T14:49:32.837815Z","iopub.status.idle":"2025-11-09T14:49:36.334904Z","shell.execute_reply.started":"2025-11-09T14:49:32.837790Z","shell.execute_reply":"2025-11-09T14:49:36.333883Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9) Giải thích mô hình (Explainability)\n\n### 9.1 Saliency map\n- **IMU (1D)**: tính gradient của logit theo input → heatmap `channels × time`.\n- **ToF (2D)**: tính gradient theo input ảnh, lấy trung bình theo thời gian → heatmap không gian.\n\n### 9.2 Attention weights\n- Xây một encoder IMU có AttentionPooling1D.\n- Huấn luyện nhanh vài epoch.\n- Vẽ biểu đồ attention theo thời gian để minh hoạ timestep quan trọng.\n\n> Lưu ý: phần này mang tính minh hoạ trực quan, không bắt buộc phải huấn luyện kỹ.\n","metadata":{}},{"cell_type":"code","source":"# %% [explainability]\nif HAS_TORCH:\n    # ---- Mô hình nhỏ với Attention ----\n    class IMUWithAttention(nn.Module):\n        def __init__(self, in_ch, n_classes, cfg):\n            super().__init__()\n            self.conv = nn.Conv1d(in_ch, 64, kernel_size=5, padding=2)\n            self.proj = nn.Linear(64, 64)\n            self.attn = AttentionPooling1D(64)\n            self.head = ClassifierHead(64, n_classes, dropout=cfg['DROPOUT'])\n        def forward(self, x):\n            h = x.transpose(1, 2)\n            h = F.relu(self.conv(h))        # [B, 64, T]\n            h = h.transpose(1, 2)           # [B, T, 64]\n            h2 = self.proj(h)\n            ctx, alpha = self.attn(h2)\n            return self.head(ctx), alpha\n\n    # Saliency 1D cho IMU\n    batch = next(iter(loaders['val']))\n    x = batch['imu'].to(DEVICE).float()\n    x.requires_grad_(True)\n    model_plain = ModelIMU('CNN1D', in_ch=IMU_CH, n_classes=N_CLASSES, cfg=CONFIG).to(DEVICE)\n    model_plain.eval()\n    logits = model_plain(x)\n    cls = logits.argmax(dim=1)\n    sel = logits[range(x.size(0)), cls].sum()\n    sel.backward()\n    sal = x.grad.detach().abs().mean(dim=0).cpu().numpy()  # [T, C]\n    plt.figure()\n    plt.imshow(sal.T, aspect='auto')\n    plt.title('IMU Saliency (channels × time)')\n    plt.xlabel('time'); plt.ylabel('channels')\n    plt.tight_layout(); plt.show()\n\n    # Saliency 2D cho ToF\n    x2 = batch['tof'].to(DEVICE).float().unsqueeze(2)\n    x2.requires_grad_(True)\n    model_tof = ModelToF(n_classes=N_CLASSES, cfg=CONFIG).to(DEVICE)\n    model_tof.eval()\n    logits2 = model_tof(x2)\n    cls2 = logits2.argmax(dim=1)\n    sel2 = logits2[range(x2.size(0)), cls2].sum()\n    sel2.backward()\n    grad = x2.grad.detach().abs().mean(dim=1).squeeze(1).cpu().numpy()  # [B, H, W]\n    plt.figure()\n    plt.imshow(grad[0], interpolation='nearest')\n    plt.title('ToF Saliency (spatial) - sample 0')\n    plt.tight_layout(); plt.show()\n\n    # Attention weights minh hoạ\n    model_attn = IMUWithAttention(IMU_CH, N_CLASSES, CONFIG).to(DEVICE)\n    crit = nn.CrossEntropyLoss()\n    opt = Adam(model_attn.parameters(), lr=CONFIG['LR'])\n    for _ in range(2):  # train nhanh 2 epoch demo\n        model_attn.train()\n        for b in loaders['train']:\n            x_train = b['imu'].to(DEVICE).float()\n            y_train = b['y'].to(DEVICE)\n            opt.zero_grad()\n            logits, _ = model_attn(x_train)\n            loss = crit(logits, y_train)\n            loss.backward()\n            opt.step()\n\n    model_attn.eval()\n    with torch.no_grad():\n        _, alpha = model_attn(x)\n    a = alpha[0].detach().cpu().numpy()\n    plt.figure()\n    plt.plot(np.arange(a.shape[0]), a)\n    plt.title('Attention weights theo thời gian (IMU sample 0)')\n    plt.xlabel('time'); plt.ylabel('weight')\n    plt.tight_layout(); plt.show()\nelse:\n    print('Chưa có PyTorch – bỏ qua explainability')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T14:50:37.240262Z","iopub.execute_input":"2025-11-09T14:50:37.240663Z","iopub.status.idle":"2025-11-09T14:50:38.551937Z","shell.execute_reply.started":"2025-11-09T14:50:37.240637Z","shell.execute_reply":"2025-11-09T14:50:38.550644Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 10) Kết luận & Ghi chú cuối\n\n- Sau khi chạy các thí nghiệm, kết quả **macro-F1** trên tập test sẽ được lưu trong bảng `dl_results.csv` (trong thư mục `/mnt/data`).\n- Bảng này có thể ghép so sánh trực tiếp với kết quả **baseline classical ML** ở Notebook 1 để minh chứng:\n  - **Deep Learning** (CNN/LSTM/GRU/BiLSTM, Multimodal) thường vượt trội hơn baseline (LogReg, RF, XGB...).\n- **Regularization** (Dropout, LayerNorm) giúp mô hình bớt overfit, đặc biệt trên dữ liệu nhỏ.\n- **Explainability** (saliency map, attention) cho phép hiểu rõ:\n  - Cảm biến nào (channel) đóng vai trò quan trọng.\n  - Thời điểm nào trong chuỗi tín hiệu có ảnh hưởng mạnh đến dự đoán.\n\n👉 Đây là bước quan trọng để chứng minh tính ưu việt của mô hình deep learning, đồng thời bổ sung thêm insight khoa học khi phân tích dữ liệu đa cảm biến.\n","metadata":{}},{"cell_type":"code","source":"# %% [summary_table]\nimport os, json, pandas as pd\nfrom glob import glob\nfrom pathlib import Path\n\nNOTEBOOK_NAME = \"Notebook 2 – Deep Learning: Mô hình chuỗi\"\n\n# Tự động tìm tất cả file metrics_*.json trong thư mục hiện tại\nrecords = []\nfor fpath in sorted(glob(\"metrics_*.json\")):\n    tag = Path(fpath).stem.replace(\"metrics_\", \"\")\n    with open(fpath, \"r\") as f:\n        data = json.load(f)\n\n    # Lấy dữ liệu từ file metrics\n    binary_f1 = data.get(\"binary_f1\")\n    macro_f1 = data.get(\"macro_f1\")\n    final_score = None\n    if binary_f1 is not None and macro_f1 is not None:\n        final_score = (binary_f1 + macro_f1) / 2\n\n    # Các thông tin khác (nếu có)\n    val_acc = data.get(\"val_acc\")\n    train_time = data.get(\"train_time\")\n    inference_time = data.get(\"inference_time\")\n    params_M = round(data.get(\"params_M\", 0), 3) if \"params_M\" in data else None\n    features_used = data.get(\"features_used\", \"-\")\n    window_size = data.get(\"window_size\", 64)\n    optimizer = data.get(\"optimizer\", \"Adam\")\n    notes = data.get(\"notes\", \"\")\n\n    records.append({\n        \"model_name\": tag,\n        \"notebook\": NOTEBOOK_NAME,\n        \"features_used\": features_used,\n        \"window_size\": window_size,\n        \"optimizer/solver\": optimizer,\n        \"params (M)\": params_M,\n        \"Binary\": binary_f1,\n        \"Macro\": macro_f1,\n        \"Final Score\": final_score,\n        \"val_acc\": val_acc,\n        \"train_time\": train_time,\n        \"inference_time\": inference_time,\n        \"notes\": notes\n    })\n\n# Gộp thành bảng tổng hợp\ndf_summary = pd.DataFrame(records)\ndf_summary = df_summary.sort_values(\"Final Score\", ascending=False)\n\n# Hiển thị kết quả đẹp\nprint(\"=== Tổng hợp kết quả huấn luyện các mô hình Deep Learning ===\")\ndisplay(df_summary.round(4))\n\n# (Tùy chọn) Lưu bảng này để đính kèm báo cáo\ndf_summary.to_csv(\"summary_notebook2_dl.csv\", index=False)\nprint(\"✅ Saved summary_notebook2_dl.csv\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-09T15:04:22.211341Z","iopub.execute_input":"2025-11-09T15:04:22.211801Z","iopub.status.idle":"2025-11-09T15:04:22.238634Z","shell.execute_reply.started":"2025-11-09T15:04:22.211767Z","shell.execute_reply":"2025-11-09T15:04:22.237469Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}