{"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"},{"sourceId":13242446,"sourceType":"datasetVersion","datasetId":8390817}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nimport os\n\n\n# Load preprocessed data (replace with your dataset path if needed)\ntrain_df = pd.read_pickle(\"//kaggle/input/processed-data/processed_train.pkl\")\ntest_df = pd.read_pickle(\"/kaggle/input/processed-data/processed_test.pkl\")\n\n# Convert all numeric columns to float32 to save memory\nnumeric_cols_train = train_df.select_dtypes(include=['float64', 'int64']).columns\ntrain_df[numeric_cols_train] = train_df[numeric_cols_train].astype('float32')\n\nnumeric_cols_test = test_df.select_dtypes(include=['float64', 'int64']).columns\ntest_df[numeric_cols_test] = test_df[numeric_cols_test].astype('float32')\n\nprint(\"Train shape:\", train_df.shape)\nprint(\"Test shape:\", test_df.shape)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:22:04.932387Z","iopub.execute_input":"2025-11-22T09:22:04.932673Z","iopub.status.idle":"2025-11-22T09:22:19.683781Z","shell.execute_reply.started":"2025-11-22T09:22:04.932645Z","shell.execute_reply":"2025-11-22T09:22:19.682814Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2. Prepare Data for Time-Series Modeling\n\n\nFor time-series models, we need to organize the data into a sequence format where each sequence is a sliding window of sensor data.\n\nTime Series Windowing: This step involves converting the data into sequences of fixed lengths (e.g., window size = 50, 100, 200) and ensuring that the sequence data is aligned with the target (behavior).","metadata":{}},{"cell_type":"code","source":"print(\"Train columns:\", train_df.columns.tolist())\nprint(\"Test columns:\", test_df.columns.tolist())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:22:19.687152Z","iopub.execute_input":"2025-11-22T09:22:19.687460Z","iopub.status.idle":"2025-11-22T09:22:19.692860Z","shell.execute_reply.started":"2025-11-22T09:22:19.687431Z","shell.execute_reply":"2025-11-22T09:22:19.691973Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile nb3_models.py\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch import Tensor\n\n# ---------- 1. Vanilla Transformer Encoder ----------\nclass TransformerEncoderModel(nn.Module):\n    def __init__(self, in_channels, num_classes, d_model=128, nhead=4, num_layers=2, dim_feedforward=256, dropout=0.1):\n        super().__init__()\n        self.input_proj = nn.Linear(in_channels, d_model)\n        encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead,\n                                                   dim_feedforward=dim_feedforward, dropout=dropout,\n                                                   batch_first=True)\n        self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)\n        self.classifier = nn.Sequential(\n            nn.LayerNorm(d_model),\n            nn.Linear(d_model, num_classes)\n        )\n    def forward(self, x: Tensor) -> Tensor:\n        x = self.input_proj(x)\n        x = self.encoder(x)\n        x = x.mean(dim=1)\n        return self.classifier(x)\n\n# ---------- 2. Time Series Transformer (TST) ----------\nclass TSTModel(nn.Module):\n    def __init__(self, in_channels, num_classes, d_model=128, nhead=4, num_layers=2,\n                 dim_feedforward=256, dropout=0.1):\n        super().__init__()\n        self.embed = nn.Linear(in_channels, d_model)\n        self.pos_embed = nn.Parameter(torch.zeros(1, 1000, d_model))\n        self.encoder = nn.TransformerEncoder(\n            nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout, batch_first=True),\n            num_layers=num_layers\n        )\n        self.cls_token = nn.Parameter(torch.zeros(1, 1, d_model))\n        self.norm = nn.LayerNorm(d_model)\n        self.head = nn.Linear(d_model, num_classes)\n        nn.init.trunc_normal_(self.pos_embed, std=0.02)\n        nn.init.trunc_normal_(self.cls_token, std=0.02)\n\n    def forward(self, x):\n        B, T, C = x.shape\n        cls_tokens = self.cls_token.expand(B, -1, -1)\n        x = torch.cat((cls_tokens, self.embed(x)), dim=1)\n        pos = self.pos_embed[:, :x.size(1)]\n        x = x + pos\n        x = self.encoder(x)\n        x = self.norm(x[:, 0])\n        return self.head(x)\n\n# ---------- 3. InformerLite ----------\nclass ProbSparseAttention(nn.Module):\n    def __init__(self, topk=64):\n        super().__init__()\n        self.topk = topk\n    def forward(self, q, k, v):\n        scores = torch.matmul(q, k.transpose(-2, -1)) / q.size(-1)**0.5\n        topk = min(self.topk, scores.size(-1))\n        topk_idx = scores.topk(topk, dim=-1).indices\n        mask = torch.zeros_like(scores)\n        mask.scatter_(-1, topk_idx, 1.0)\n        scores = scores * mask - 1e9 * (1 - mask)\n        attn = torch.softmax(scores, dim=-1)\n        return torch.matmul(attn, v)\n\nclass InformerLite(nn.Module):\n    def __init__(self, in_channels, num_classes, d_model=128, nhead=4,\n                 num_layers=2, dim_feedforward=256, dropout=0.1, topk=64):\n        super().__init__()\n        self.input_proj = nn.Linear(in_channels, d_model)\n        self.attn = ProbSparseAttention(topk)\n        self.layers = nn.ModuleList([\n            nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward, dropout, batch_first=True)\n            for _ in range(num_layers)\n        ])\n        self.fc = nn.Sequential(nn.LayerNorm(d_model), nn.Linear(d_model, num_classes))\n\n    def forward(self, x):\n        x = self.input_proj(x)\n        for layer in self.layers:\n            attn_out = self.attn(x, x, x)\n            x = layer(attn_out)\n        x = x.mean(1)\n        return self.fc(x)\n\n# ---------- 4. Temporal Convolutional Network (TCN) ----------\nclass Chomp1d(nn.Module):\n    def __init__(self, chomp_size): super().__init__(); self.chomp_size = chomp_size\n    def forward(self, x): return x[:, :, :-self.chomp_size].contiguous()\n\nclass TemporalBlock(nn.Module):\n    def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2):\n        super().__init__()\n        self.conv1 = nn.Conv1d(n_inputs, n_outputs, kernel_size, stride=stride, padding=padding, dilation=dilation)\n        self.chomp1 = Chomp1d(padding)\n        self.relu1 = nn.ReLU()\n        self.dropout1 = nn.Dropout(dropout)\n\n        self.conv2 = nn.Conv1d(n_outputs, n_outputs, kernel_size, stride=stride, padding=padding, dilation=dilation)\n        self.chomp2 = Chomp1d(padding)\n        self.relu2 = nn.ReLU()\n        self.dropout2 = nn.Dropout(dropout)\n\n        self.net = nn.Sequential(self.conv1, self.chomp1, self.relu1, self.dropout1,\n                                 self.conv2, self.chomp2, self.relu2, self.dropout2)\n        self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None\n        self.relu = nn.ReLU()\n        self.init_weights()\n\n    def init_weights(self):\n        self.conv1.weight.data.normal_(0, 0.01)\n        self.conv2.weight.data.normal_(0, 0.01)\n        if self.downsample is not None:\n            self.downsample.weight.data.normal_(0, 0.01)\n\n    def forward(self, x):\n        out = self.net(x)\n        res = x if self.downsample is None else self.downsample(x)\n        return self.relu(out + res)\n\nclass TCN(nn.Module):\n    def __init__(self, in_channels, num_classes, levels=4, channels=64, kernel_size=3, dropout=0.2):\n        super().__init__()\n        layers = []\n        num_inputs = in_channels\n        for i in range(levels):\n            dilation = 2 ** i\n            padding = (kernel_size - 1) * dilation\n            layers.append(TemporalBlock(num_inputs, channels, kernel_size, stride=1,\n                                        dilation=dilation, padding=padding, dropout=dropout))\n            num_inputs = channels\n        self.network = nn.Sequential(*layers)\n        self.fc = nn.Linear(channels, num_classes)\n\n    def forward(self, x):\n        x = x.transpose(1, 2)     # [B, C, T]\n        out = self.network(x)\n        out = out.mean(-1)\n        return self.fc(out)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:22:19.694980Z","iopub.execute_input":"2025-11-22T09:22:19.695285Z","iopub.status.idle":"2025-11-22T09:22:19.713979Z","shell.execute_reply.started":"2025-11-22T09:22:19.695263Z","shell.execute_reply":"2025-11-22T09:22:19.713116Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Group per sequence -> [N, T, C]\ndef build_sequences(df: pd.DataFrame, seq_col: str, target_col: str, feature_cols: List[str],\n                    window_size: int) -> Tuple[np.ndarray, np.ndarray, LabelEncoder]:\n    grouped, labels = [], []\n    for seq, g in df.groupby(seq_col):\n        X_seq = g[feature_cols].to_numpy(dtype=np.float32)  # [Ti, C]\n        labels.append(g[target_col].iloc[0])  # Target is grouped gesture\n        # pad/crop to window_size\n        if len(X_seq) >= window_size:\n            grouped.append(X_seq[:window_size])\n        else:\n            pad = window_size - len(X_seq)\n            grouped.append(np.pad(X_seq, ((0,pad),(0,0)), mode=\"constant\"))\n    X = np.stack(grouped)                              # [N, T, C]\n    le = LabelEncoder()\n    y = le.fit_transform(np.asarray(labels).astype(str))  # Labels as grouped gestures\n    return X, y, le","metadata":{}},{"cell_type":"code","source":"# ================== NB3 HARNESS (chuẩn schema) ==================\nimport os, time, math, re, json, warnings\nimport pandas as pd\nfrom dataclasses import dataclass, asdict\nfrom typing import List, Dict, Any, Optional, Tuple\nimport uuid\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader, RandomSampler, SequentialSampler\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.model_selection import train_test_split\n\nfrom sklearn.metrics import f1_score, accuracy_score, confusion_matrix\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport numpy as np\n\n\nwarnings.filterwarnings(\"ignore\", category=UserWarning)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ---------- 1) Dataset ----------\nclass ArrayTSDataset(Dataset):\n    def __init__(self, X, y):\n        self.X = torch.tensor(X, dtype=torch.float32)\n        self.y = torch.tensor(y, dtype=torch.long)\n    def __len__(self): return len(self.X)\n    def __getitem__(self, i): return self.X[i], self.y[i]\n\n# ---------- 2) Feature selector ----------\n# Feature selector\ndef select_feature_cols(df: pd.DataFrame, features_used: str, target_col: str, seq_col: str) -> List[str]:\n    # Tùy vào schema của bạn, chỉnh prefix dưới đây cho phù hợp\n    imu_prefixes = (\"acc_\", \"gyro_\", \"rot_\", \"mag_\")\n    tof_prefixes = (\"tof_\",)\n    thm_prefixes = (\"thm_\", \"temp_\", \"thermo_\")\n\n    def pick(prefixes):\n        return [c for c in df.columns\n                if c not in [target_col, seq_col]\n                and df[c].dtype in [\"float32\",\"float64\",\"int32\",\"int64\"]\n                and any(c.startswith(p) for p in prefixes)]\n\n    if features_used.lower() == \"imu\":\n        cols = pick(imu_prefixes)\n    elif features_used.lower() == \"tof\":\n        cols = pick(tof_prefixes)\n    elif features_used.lower() == \"thermo\":\n        cols = pick(thm_prefixes)\n    elif features_used.lower() in [\"full\",\"all\",\"fusion\"]:\n        cols = [c for c in df.columns if c not in [target_col, seq_col] and df[c].dtype in [\"float32\",\"float64\",\"int32\",\"int64\"]]\n    else:\n        # fallback: dùng tất cả numeric\n        cols = [c for c in df.columns if c not in [target_col, seq_col] and df[c].dtype in [\"float32\",\"float64\",\"int32\",\"int64\"]]\n    if len(cols) == 0:\n        raise ValueError(f\"Không tìm thấy cột feature phù hợp cho '{features_used}'.\")\n    return cols\n\n\n\n# ---------- 3) Group per sequence -> [N, T, C] ----------\n# Group per sequence -> [N, T, C]\ndef build_sequences(df: pd.DataFrame, seq_col: str, target_col: str, feature_cols: List[str],\n                    window_size: int) -> Tuple[np.ndarray, np.ndarray, LabelEncoder]:\n    grouped, labels = [], []\n    for seq, g in df.groupby(seq_col):\n        X_seq = g[feature_cols].to_numpy(dtype=np.float32)  # [Ti, C]\n        labels.append(g[target_col].iloc[0])  # Target is grouped gesture_grouped_id (0-8)\n        # pad/crop to window_size\n        if len(X_seq) >= window_size:\n            grouped.append(X_seq[:window_size])\n        else:\n            pad = window_size - len(X_seq)\n            grouped.append(np.pad(X_seq, ((0,pad),(0,0)), mode=\"constant\"))\n    X = np.stack(grouped)\n\n    # ❗ SỬA LỖI: Dùng ID đã encode sẵn (0-8) và trả về LabelEncoder toàn cục\n    y = np.asarray(labels).astype(np.int64)\n    global le_gesture_grouped\n    \n    return X, y, le_gesture_grouped\n# =========================\n# 1) Group gesture cho TRAIN\n# =========================\n\n# Gesture mapping (8 target + 10 non-target)\nGESTURE_MAPPER = {\n    \"Above ear - pull hair\": 0,\n    \"Cheek - pinch skin\": 1,\n    \"Eyebrow - pull hair\": 2,\n    \"Eyelash - pull hair\": 3,\n    \"Forehead - pull hairline\": 4,\n    \"Forehead - scratch\": 5,\n    \"Neck - pinch skin\": 6,\n    \"Neck - scratch\": 7,\n\n    \"Drink from bottle/cup\": 8,\n    \"Feel around in tray and pull out an object\": 9,\n    \"Glasses on/off\": 10,\n    \"Pinch knee/leg skin\": 11,\n    \"Pull air toward your face\": 12,\n    \"Scratch knee/leg skin\": 13,\n    \"Text on phone\": 14,\n    \"Wave hello\": 15,\n    \"Write name in air\": 16,\n    \"Write name on leg\": 17,\n}\n\nTARGET_LABELS = [k for k,v in GESTURE_MAPPER.items() if v <= 7]\n\ndef to_grouped_label(name):\n    return name if name in TARGET_LABELS else \"non_target\"\n\n# ❗ chỉ áp dụng cho train_df, KHÔNG áp dụng test_df\ntrain_df[\"gesture_grouped\"] = train_df[\"gesture\"].apply(to_grouped_label)\n\nfrom sklearn.preprocessing import LabelEncoder\n\nle_gesture_grouped = LabelEncoder()\ntrain_df[\"gesture_grouped_id\"] = le_gesture_grouped.fit_transform(train_df[\"gesture_grouped\"])\n# CLASS_NAMES dùng cho evaluate()\nCLASS_NAMES = list(le_gesture_grouped.classes_)\n\nprint(CLASS_NAMES, len(CLASS_NAMES))\n\nprint(\"Grouped labels:\", le_gesture_grouped.classes_)\n\n\n@torch.no_grad()\ndef evaluate(model, loader, class_names=None, plot_cm=False, save_path=None) -> Dict[str, float]:\n    \"\"\"\n    Evaluate model with F1, accuracy, and optional confusion matrix visualization.\n    Compatible with both binary and multiclass tasks.\n    \"\"\"\n    model.eval()\n    y_true, y_pred = [], []\n    for xb, yb in loader:\n        xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n        preds = model(xb).argmax(1)\n        y_true.extend(yb.cpu().numpy())\n        y_pred.extend(preds.cpu().numpy())\n\n    y_true = np.array(y_true)\n    y_pred = np.array(y_pred)\n    n_classes = len(np.unique(y_true))\n\n    # --- Compute F1 scores ---\n    if n_classes == 2:\n        bin_f1 = f1_score(y_true, y_pred, pos_label=1)\n        macro = f1_score(y_true, y_pred, average=\"macro\")\n    else:\n        bin_f1 = f1_score(y_true, y_pred, average=\"micro\")  # dùng micro-F1 cho multiclass\n        macro = f1_score(y_true, y_pred, average=\"macro\")\n    acc = accuracy_score(y_true, y_pred)\n    final = (bin_f1 + macro) / 2\n\n    # --- Confusion Matrix ---\n    cm = confusion_matrix(y_true, y_pred)\n    labels = class_names if class_names else np.arange(n_classes)\n\n    if plot_cm:\n        plt.figure(figsize=(6, 5))\n        sns.heatmap(cm, annot=True, fmt=\"d\", cmap=\"Blues\",\n                    xticklabels=labels, yticklabels=labels)\n        plt.title(\"Confusion Matrix\")\n        plt.xlabel(\"Predicted\")\n        plt.ylabel(\"True\")\n        plt.tight_layout()\n        if save_path:\n            plt.savefig(save_path, dpi=300, bbox_inches=\"tight\")\n            print(f\"📁 Saved confusion matrix to {save_path}\")\n        plt.show()\n\n    return {\n        \"Binary F1\": float(bin_f1),\n        \"Macro F1\": float(macro),\n        \"Final Score\": float(final),\n        \"val_acc\": float(acc),\n        \"confusion_matrix\": cm\n    }\n\n# ---------- 5) Train (mini-protocol ready) ----------\ndef train_one(model, train_loader, val_loader, optimizer, epochs: int, grad_accum: int = 1, amp: bool = True):\n    model.to(DEVICE)\n    amp_enabled = (amp and DEVICE.type == \"cuda\")\n    scaler = torch.cuda.amp.GradScaler(enabled=amp_enabled)\n    criterion = nn.CrossEntropyLoss()\n    total_train_time = 0.0\n    if DEVICE.type == \"cuda\":\n        torch.cuda.reset_peak_memory_stats(DEVICE)\n    best = {\"Final Score\": -1, \"epoch\": 0}\n    epoch_logs = []\n    for ep in range(epochs):\n        model.train()\n        t0 = time.time()\n        optimizer.zero_grad(set_to_none=True)\n        for step, (xb, yb) in enumerate(train_loader):\n            xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n            if torch.__version__.startswith(\"2.1\") or torch.__version__.startswith(\"2.2\"):\n                autocast_ctx = torch.amp.autocast(\"cuda\", enabled=amp_enabled)\n            else:\n                autocast_ctx = torch.cuda.amp.autocast(enabled=amp_enabled)\n\n            with autocast_ctx:\n                logits = model(xb)\n                loss = criterion(logits, yb) / grad_accum\n\n            scaler.scale(loss).backward()\n            if (step + 1) % grad_accum == 0:\n                scaler.step(optimizer); scaler.update(); optimizer.zero_grad(set_to_none=True)\n        total_train_time += (time.time() - t0)\n\n        # --- Evaluate sau mỗi epoch ---\n        metrics = evaluate(model, val_loader, class_names=CLASS_NAMES, plot_cm=False)\n        epoch_logs.append({\n            \"epoch\": ep + 1,\n            **{k: round(v, 4) for k, v in metrics.items() if k in [\"Binary F1\", \"Macro F1\", \"Final Score\", \"val_acc\"]}\n        })\n\n\n        if metrics[\"Final Score\"] > best[\"Final Score\"]:\n            best = metrics\n        # --- Cập nhật best nếu tốt hơn ---\n        if metrics[\"Final Score\"] > best[\"Final Score\"]:\n            best = metrics\n            best[\"epoch\"] = ep + 1\n\n        # --- In chi tiết sau mỗi epoch ---\n        print(f\"Epoch {ep+1}/{epochs} | \"\n              f\"Loss={loss.item():.4f} | \"\n              f\"Binary F1={metrics['Binary F1']:.3f} | \"\n              f\"Macro F1={metrics['Macro F1']:.3f} | \"\n              f\"Final={metrics['Final Score']:.3f} | \"\n              f\"Acc={metrics['val_acc']:.3f}\")\n\n    # --- Sau khi train xong: in tóm tắt best ---\n    best_epoch = best.get(\"epoch\", epochs)\n    print(f\"\\n🏆 Best Epoch: {best_epoch} | Final Score={best['Final Score']:.3f} | \"\n      f\"Acc={best['val_acc']:.3f}\")\n\n    \n    \n    evaluate(\n        model, val_loader,\n        class_names=CLASS_NAMES,\n        plot_cm=True,\n        save_path=f\"/kaggle/working/confmat_{model.__class__.__name__}_final.png\"\n    )\n    # inference time / sample (trên 1 batch val)\n    model.eval()\n    xb, _ = next(iter(val_loader))\n    xb = xb.to(DEVICE)\n    if DEVICE.type == \"cuda\": torch.cuda.synchronize()\n    t1 = time.time(); _ = model(xb)\n    if DEVICE.type == \"cuda\": torch.cuda.synchronize()\n    infer_ms = (time.time() - t1) / xb.size(0) * 1000\n\n    peak_gb = (torch.cuda.max_memory_allocated(DEVICE) / (1024**3)) if DEVICE.type == \"cuda\" else 0.0\n    params_m = sum(p.numel() for p in model.parameters() if p.requires_grad) / 1e6\n    return best, total_train_time, infer_ms, params_m, peak_gb, epoch_logs\n\n# ---------- 6) Logging theo đúng schema ----------\nSCHEMA_COLS = [\n    \"model_name\",\"runtime_id\",\"notebook\",\"features_used\",\"window_size\",\n    \"optimizer/solver\",\"params (M)\",\"Binary F1\",\"Macro F1\",\"Final Score\",\n    \"val_acc\",\"train_time\",\"inference_time\",\"notes\"\n]\n\ndef append_result(row: Dict[str, Any], csv_path: str = \"/kaggle/working/transformer_results.csv\"):\n    df_row = pd.DataFrame([row], columns=SCHEMA_COLS)\n    if os.path.exists(csv_path):\n        df = pd.read_csv(csv_path)\n        df = pd.concat([df, df_row], ignore_index=True)\n    else:\n        df = df_row\n    df.to_csv(csv_path, index=False)\n    print(f\"✅ Logged to {csv_path}\")\n\n# ---------- 7) Length sweep {50,100,200} ----------\ndef run_length_sweep(train_df: pd.DataFrame,\n                     model_builder,            # hàm: (in_channels, num_classes) -> nn.Module\n                     model_name: str,\n                     notebook_name: str,\n                     features_used: str,       # \"IMU\" | \"ToF\" | \"Thermo\" | \"Full\"\n                     seq_col: str,             # ví dụ \"sequence_counter\"\n                     target_col: str,          # ví dụ \"behavior\"\n                     lengths: List[int] = [50,100,200],\n                     optimizer_name: str = \"AdamW\",\n                     lr: float = 1e-3,\n                     epochs: int = 5,\n                     batch_size: int = 64,\n                     grad_accum: int = 1,\n                     amp: bool = True,\n                     runtime_prefix: str = \"mp01\",\n                     results_csv: str = \"/kaggle/working/informerlite_results.csv\",\n                     max_train_sequences: Optional[int] = 1000,   # mini-protocol cap\n                     max_val_sequences: Optional[int] = 300):     # mini-protocol cap\n\n    feat_cols_full = select_feature_cols(train_df, features_used, target_col, seq_col)\n\n    for L in lengths:\n        print(f\"\\n===== Sequence length = {L} | features={features_used} =====\")\n        # Build sequences\n        X, y, le = build_sequences(train_df[[seq_col, target_col] + feat_cols_full], seq_col, target_col, feat_cols_full, L)\n        C = X.shape[2]\n        num_classes = len(CLASS_NAMES)\n        print(f\"[DEBUG CHECK] Giá trị num_classes hiện tại: {num_classes}\")\n\n        # Split & cap for mini-protocol\n        X_tr, X_va, y_tr, y_va = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)\n        if max_train_sequences: X_tr, y_tr = X_tr[:max_train_sequences], y_tr[:max_train_sequences]\n        if max_val_sequences:   X_va, y_va = X_va[:max_val_sequences], y_va[:max_val_sequences]\n\n        train_loader = DataLoader(ArrayTSDataset(X_tr, y_tr), batch_size=batch_size, sampler=RandomSampler(ArrayTSDataset(X_tr, y_tr)))\n        val_loader   = DataLoader(ArrayTSDataset(X_va, y_va), batch_size=batch_size, sampler=SequentialSampler(ArrayTSDataset(X_va, y_va)))\n\n        # Model + Optim\n        model = model_builder(C, num_classes)\n        if optimizer_name.lower() == \"adam\":\n            optimizer = torch.optim.Adam(model.parameters(), lr=lr)\n        elif optimizer_name.lower() == \"sgd\":\n            optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0.9)\n        else:\n            optimizer = torch.optim.AdamW(model.parameters(), lr=lr)\n\n        # Train\n        best, train_time_s, infer_ms, params_m, peak_gb, epoch_logs = train_one(model, train_loader, val_loader, optimizer,\n                                                                    epochs=epochs, grad_accum=grad_accum, amp=amp)\n\n# --- DEBUG: Kiểm tra số lớp ĐẦU RA của mô hình vừa huấn luyện ---\n# Giả sử lớp đầu ra cuối cùng của bạn là self.fc\n        try:\n    # Lấy shape của weight (num_classes, input_dim)\n            output_shape = model.fc[-1].weight.shape \n            num_classes_trained = output_shape[0]\n            print(f\"\\n[DEBUG] Số lớp đầu ra của mô hình VỪA HUẤN LUYỆN là: {num_classes_trained}\")\n            if num_classes_trained != 9:\n                print(\"🚨 CẢNH BÁO: Mô hình VỪA HUẤN LUYỆN có số lớp KHÔNG PHẢI 9. Quay lại kiểm tra MODEL_BUILDER.\")\n        # SỬA LỖI: Dùng biến train_time_s thay vì total_train_time\n                return best, train_time_s, infer_ms, params_m, peak_gb, epoch_logs \n        except AttributeError:\n            print(\"[DEBUG] Không tìm thấy lớp .fc[-1] trong mô hình. Cần chỉnh sửa đường dẫn lớp đầu ra.\")\n# -------------------------------------------------------------\n# -------------------------------------------------------------\n        # 1) Lưu feature cols\n        np.save(\"/kaggle/working/feature_cols.npy\", np.array(feat_cols_full, dtype=object))\n\n# 2) Lưu danh sách gesture classes\n        np.save(\"/kaggle/working/gesture_classes.npy\", le_gesture_grouped.classes_)\n\n\n# 3) Lưu sequence length (window_size)\n        np.save(\"/kaggle/working/sequence_maxlen.npy\", np.array([L]))\n\n# 4) Lưu scaler nếu có\n        if 'scaler' in globals() and scaler is not None:\n            joblib.dump(scaler, \"/kaggle/working/scaler.pkl\")\n\n# 5) Lưu checkpoint model theo window-size\n        ckpt_path = f\"/kaggle/working/{model_name}_L{L}.pth\"\n        torch.save(model.state_dict(), ckpt_path)\n        print(f\"💾 Saved checkpoint to {ckpt_path}\")\n\n\n        # Log 1 dòng theo schema\n        row = {\n            \"model_name\": model_name,\n            \"runtime_id\": f\"{runtime_prefix}_{model_name}_L{L}_{uuid.uuid4().hex[:4]}\",\n            \"notebook\": notebook_name,\n            \"features_used\": features_used,\n            \"window_size\": L,\n            \"optimizer/solver\": optimizer_name,\n            \"params (M)\": round(params_m, 3),\n            \"Binary F1\": round(best[\"Binary F1\"], 3),\n            \"Macro F1\": round(best[\"Macro F1\"], 3),\n            \"Final Score\": round(best[\"Final Score\"], 3),\n            \"val_acc\": round(best[\"val_acc\"], 3),\n            \"train_time\": f\"{train_time_s:.2f}s\",\n            \"inference_time\": f\"{infer_ms:.2f} ms/sample\",\n            \"notes\": f\"mini-protocol; peak_mem={peak_gb:.2f}GB; grad_accum={grad_accum}; amp={'on' if (amp and DEVICE.type=='cuda') else 'off'}\"\n        }\n        append_result(row, results_csv)\n# ================== /NB3 HARNESS ==================\n# ================== 8) Evaluate on IMU-only and Full splits ==================\ndef run_full_evaluation(\n    train_df: pd.DataFrame,\n    model_builder,\n    model_name: str,\n    notebook_name: str,\n    seq_col: str = \"sequence_counter\",\n    target_col: str = \"gesture_grouped_id\",\n    lengths: List[int] = [50,100,200],\n    optimizer_name: str = \"AdamW\",\n    lr: float = 1e-3,\n    epochs: int = 5,\n    batch_size: int = 64,\n    grad_accum: int = 1,\n    amp: bool = True,\n    runtime_prefix: str = \"mp01\",\n    results_csv: str = \"/kaggle/working/informerlite_results.csv\",\n    max_train_sequences: Optional[int] = 1000,\n    max_val_sequences: Optional[int] = 300,\n):\n    \"\"\"Run both IMU-only and Full splits (Task 6).\"\"\"\n    print(\"\\n===============================\")\n    print(f\"🏁 Starting full evaluation for {model_name}\")\n    print(\"===============================\")\n\n    for feature_mode in [\"IMU\", \"Full\"]:\n        print(f\"\\n### Running {feature_mode}-only mode ###\")\n        run_length_sweep(\n            train_df=train_df,\n            model_builder=model_builder,\n            model_name=model_name,\n            notebook_name=notebook_name,\n            features_used=feature_mode,\n            seq_col=seq_col,\n            target_col=target_col,\n            lengths=lengths,\n            optimizer_name=optimizer_name,\n            lr=lr,\n            epochs=epochs,\n            batch_size=batch_size,\n            grad_accum=grad_accum,\n            amp=amp,\n            runtime_prefix=f\"{runtime_prefix}_{feature_mode}\",\n            results_csv=results_csv,\n            max_train_sequences=max_train_sequences,\n            max_val_sequences=max_val_sequences,\n        )\n\n    print(\"\\n✅ Completed both IMU and Full runs. Results appended to CSV.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:22:19.714984Z","iopub.execute_input":"2025-11-22T09:22:19.715241Z","iopub.status.idle":"2025-11-22T09:22:20.737899Z","shell.execute_reply.started":"2025-11-22T09:22:19.715221Z","shell.execute_reply":"2025-11-22T09:22:20.736968Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nfrom sklearn.metrics import f1_score\n\nclass ParticipantVisibleError(Exception):\n    \"\"\"Errors raised here will be shown directly to the competitor.\"\"\"\n    pass\n\nclass CompetitionMetric:\n    \"\"\"Hierarchical macro F1 for the CMI 2025 challenge.\"\"\"\n    def __init__(self):\n        self.target_gestures = [\n            'Above ear - pull hair',\n            'Cheek - pinch skin',\n            'Eyebrow - pull hair',\n            'Eyelash - pull hair',\n            'Forehead - pull hairline',\n            'Forehead - scratch',\n            'Neck - pinch skin',\n            'Neck - scratch',\n        ]\n        self.non_target_gestures = [\n            'Write name on leg',\n            'Wave hello',\n            'Glasses on/off',\n            'Text on phone',\n            'Write name in air',\n            'Feel around in tray and pull out an object',\n            'Scratch knee/leg skin',\n            'Pull air toward your face',\n            'Drink from bottle/cup',\n            'Pinch knee/leg skin'\n        ]\n        self.all_classes = self.target_gestures + self.non_target_gestures\n\n    def calculate_hierarchical_f1(self, sol: pd.DataFrame, sub: pd.DataFrame) -> float:\n        # Validate gestures\n        invalid_types = {i for i in sub['gesture_grouped'].unique() if i not in self.all_classes}\n        if invalid_types:\n            raise ParticipantVisibleError(f\"Invalid gesture values in submission: {invalid_types}\")\n\n        # Compute binary F1 (Target vs Non-Target)\n        y_true_bin = sol['gesture_grouped'].isin(self.target_gestures).values\n        y_pred_bin = sub['gesture_grouped'].isin(self.target_gestures).values\n        f1_binary = f1_score(\n            y_true_bin,\n            y_pred_bin,\n            pos_label=True,\n            zero_division=0,\n            average='binary'\n        )\n\n        # Build multi-class labels for gestures\n        y_true_mc = sol['gesture_grouped'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub['gesture_grouped'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n\n        # Compute macro F1 over all gesture classes\n        f1_macro = f1_score(\n            y_true_mc,\n            y_pred_mc,\n            average='macro',\n            zero_division=0\n        )\n\n        return 0.5 * f1_binary + 0.5 * f1_macro\n\n\ndef score(solution: pd.DataFrame, submission: pd.DataFrame, row_id_column_name: str) -> float:\n    \"\"\"\n    Compute hierarchical macro F1 for the CMI 2025 challenge.\n\n    Expected input:\n      - solution and submission as pandas.DataFrame\n      - Column 'sequence_id': unique identifier for each sequence\n      - 'gesture_grouped': one of the eight target gestures or \"Non-Target\"\n    \"\"\"\n    # Validate required columns\n    for col in (row_id_column_name, 'gesture_grouped'):\n        if col not in solution.columns:\n            raise ParticipantVisibleError(f\"Solution file missing required column: '{col}'\")\n        if col not in submission.columns:\n            raise ParticipantVisibleError(f\"Submission file missing required column: '{col}'\")\n\n    metric = CompetitionMetric()\n    return metric.calculate_hierarchical_f1(solution, submission)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:22:20.738878Z","iopub.execute_input":"2025-11-22T09:22:20.739323Z","iopub.status.idle":"2025-11-22T09:22:20.749869Z","shell.execute_reply.started":"2025-11-22T09:22:20.739290Z","shell.execute_reply":"2025-11-22T09:22:20.749019Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from nb3_models import InformerLite\n\ndef MODEL_BUILDER(in_channels, num_classes):\n    return InformerLite(in_channels, num_classes, d_model=128, nhead=4, num_layers=2,\n                        dim_feedforward=256, dropout=0.1, topk=64)\n\nMODEL_NAME = \"InformerLite\"\nNOTEBOOK_NAME = \"NB3-3 InformerLite\"\n\nrun_full_evaluation(train_df, MODEL_BUILDER, MODEL_NAME, NOTEBOOK_NAME,\n                    seq_col=\"sequence_counter\", target_col=\"gesture_grouped_id\",\n                    lengths=[50,100,200], optimizer_name=\"AdamW\", lr=1e-3,\n                    epochs=5, batch_size=64, grad_accum=1, amp=True,\n                    runtime_prefix=\"mp01\", results_csv=\"/kaggle/working/informerlite_results.csv\",\n                    max_train_sequences=1000, max_val_sequences=300)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:22:20.750825Z","iopub.execute_input":"2025-11-22T09:22:20.751155Z","iopub.status.idle":"2025-11-22T09:25:26.821395Z","shell.execute_reply.started":"2025-11-22T09:22:20.751124Z","shell.execute_reply":"2025-11-22T09:25:26.820561Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\n# --- Đường dẫn đến file kết quả ---\ncsv_path = \"/kaggle/working/informerlite_results.csv\"\n\n# --- Đọc và sắp xếp ---\ndf = pd.read_csv(csv_path)\ndf = df.sort_values(by=[\"model_name\", \"features_used\", \"window_size\"]).reset_index(drop=True)\n\n# --- Chọn cột hiển thị ---\ncols_to_show = [\n    \"model_name\", \"features_used\", \"window_size\",\n    \"Binary F1\", \"Macro F1\", \"Final Score\",\n    \"val_acc\", \"params (M)\", \"train_time\", \"inference_time\"\n]\n\n# --- In toàn bộ bảng ---\nprint(\"\\n==============================================\")\nprint(\"📊 NB3 Full Results (All Window Sizes)\")\nprint(\"==============================================\\n\")\nprint(df[cols_to_show].to_string(index=False))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:25:26.822348Z","iopub.execute_input":"2025-11-22T09:25:26.822648Z","iopub.status.idle":"2025-11-22T09:25:26.845563Z","shell.execute_reply.started":"2025-11-22T09:25:26.822604Z","shell.execute_reply":"2025-11-22T09:25:26.844672Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n\nimport os\nfrom IPython.display import Image, display\n\n# --- Lặp qua từng dòng kết quả ---\nfor _, row in df.iterrows():\n    model_name = row[\"model_name\"]\n    feature = row[\"features_used\"]\n    window = int(row[\"window_size\"])\n\n    cm_path = f\"/kaggle/working/confmat_{model_name}_{feature}_L{window}.png\"\n\n    print(f\"\\n🔹 {model_name} | {feature} | window={window}\")\n    if os.path.exists(cm_path):\n        display(Image(cm_path))\n    else:\n        print(f\"⚠️ Confusion matrix not found at {cm_path}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:25:26.846456Z","iopub.execute_input":"2025-11-22T09:25:26.846758Z","iopub.status.idle":"2025-11-22T09:25:26.854788Z","shell.execute_reply.started":"2025-11-22T09:25:26.846721Z","shell.execute_reply":"2025-11-22T09:25:26.853845Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ======================================================\n# 🔥 CREATE SUBMISSION.PARQUET FROM YOUR TRAINED MODEL\n# ======================================================\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport joblib\nimport torch\nimport torch.nn.functional as F\n\n# Load artefacts\nFEATURE_COLS = np.load(\"/kaggle/working/feature_cols.npy\", allow_pickle=True).tolist()\nGESTURE_CLASSES = np.load(\"/kaggle/working/gesture_classes.npy\", allow_pickle=True).tolist()\nMAXLEN = int(np.load(\"/kaggle/working/sequence_maxlen.npy\")[0])\n\nscaler = None\nif os.path.exists(\"/kaggle/working/scaler.pkl\"):\n    scaler = joblib.load(\"/kaggle/working/scaler.pkl\")\n\n# Load best checkpoint (thay model_name + L cho đúng file bạn lưu)\nCHECKPOINT = \"/kaggle/working/InformerLite_L100.pth\"\n\nmodel = MODEL_BUILDER(len(FEATURE_COLS), len(GESTURE_CLASSES))\nstate = torch.load(CHECKPOINT, map_location=\"cpu\")\nmodel.load_state_dict(state, strict=True)\nprint(state[\"fc.1.weight\"].shape)\n\n\nprint(\"Loaded checkpoint:\", CHECKPOINT)\n\nmodel.eval()\n\n# ----------------- PREPROCESS FUNCTION -----------------\n\ndef preprocess_sequence(df):\n    # Tạo DataFrame mới chỉ chứa các cột FEATURE_COLS\n    # Dùng reindex để đảm bảo tất cả FEATURE_COLS đều có mặt. \n    # Những cột có sẵn sẽ giữ nguyên giá trị; những cột thiếu sẽ được điền 0 (fill_value=0).\n    seq_df = df.reindex(columns=FEATURE_COLS, fill_value=0).astype(\"float32\")\n    \n    # Lúc này, seq_df đã có đầy đủ các cột và không bị fragmented.\n    X = seq_df.values\n\n    if scaler:\n        X = scaler.transform(X)\n    \n    # Pad/truncate\n    T = X.shape[0]\n    if T >= MAXLEN:\n        X = X[:MAXLEN]\n    else:\n        pad = np.zeros((MAXLEN - T, X.shape[1]), dtype=np.float32)\n        X = np.vstack([X, pad])\n    return X\n\n# ----------------- LOAD TEST DATA -----------------\ntest_df = pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\").to_pandas()\ndemo_df = pl.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\").to_pandas()\n\nrecords = []\nfor seq, g in test_df.groupby(\"sequence_id\"):\n    seq_pd = g  # đã là pandas rồi\n    X = preprocess_sequence(seq_pd)\n\n    X_tensor = torch.tensor(X).unsqueeze(0)\n\n    with torch.no_grad():\n        logits = model(X_tensor)\n        pred = logits.argmax(dim=1).item()\n\n    gesture = GESTURE_CLASSES[pred]\n    records.append((seq, gesture))\n\n# ----------------- CREATE SUBMISSION -----------------\ndf_sub = pd.DataFrame(records, columns=[\"sequence_id\", \"gesture\"])  # Ensure the column is 'gesture'\n\n# ----------------- VERIFY THE STRUCTURE -----------------\nprint(\"First few rows of the submission data:\")\nprint(df_sub.head())  # Verify the structure and the values in 'gesture' column\n\n# Check if 'gesture' contains valid gesture classes\nif df_sub[\"gesture\"].isin(GESTURE_CLASSES).all():\n    print(\"✅ All gesture values are valid.\")\nelse:\n    print(\"❌ There are invalid gesture values in the 'gesture' column.\")\n\n# Check the number of rows and columns\nprint(f\"Number of rows: {df_sub.shape[0]}, Number of columns: {df_sub.shape[1]}\")\nprint(f\"Column names: {df_sub.columns.tolist()}\")\n\n# ----------------- SAVE TO PARQUET -----------------\ndf_sub.to_parquet(\"/kaggle/working/submission.parquet\", index=False)\n\nprint(\"✅ DONE! Saved /kaggle/working/submission.parquet\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:25:26.855662Z","iopub.execute_input":"2025-11-22T09:25:26.855934Z","iopub.status.idle":"2025-11-22T09:25:27.970377Z","shell.execute_reply.started":"2025-11-22T09:25:26.855905Z","shell.execute_reply":"2025-11-22T09:25:27.969379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(CHECKPOINT)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:25:27.971366Z","iopub.execute_input":"2025-11-22T09:25:27.971692Z","iopub.status.idle":"2025-11-22T09:25:27.976488Z","shell.execute_reply.started":"2025-11-22T09:25:27.971670Z","shell.execute_reply":"2025-11-22T09:25:27.975281Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#!rm -f /kaggle/working/*.pth\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:25:27.977448Z","iopub.execute_input":"2025-11-22T09:25:27.977709Z","iopub.status.idle":"2025-11-22T09:25:27.996418Z","shell.execute_reply.started":"2025-11-22T09:25:27.977682Z","shell.execute_reply":"2025-11-22T09:25:27.995474Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\n\nstate = torch.load(\"/kaggle/working/InformerLite_L100.pth\", map_location=\"cpu\")\nprint(state[\"fc.1.weight\"].shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-22T09:25:27.999289Z","iopub.execute_input":"2025-11-22T09:25:27.999548Z","iopub.status.idle":"2025-11-22T09:25:28.022411Z","shell.execute_reply.started":"2025-11-22T09:25:27.999527Z","shell.execute_reply":"2025-11-22T09:25:28.021409Z"}},"outputs":[],"execution_count":null}]}