{"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":12794968,"sourceType":"datasetVersion","datasetId":8089438}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =========================\n# CMI Inference - Template\n# =========================\nimport os\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport joblib\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# --------- 1) PATHS: sửa theo tên dataset output của bạn ----------\nCKPT_DIR = \"/kaggle/input/cmi-two-branch-bigr-u-checkpoints\"  # <-- đổi cho đúng\nRAW_DIR  = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\n\nCKPT_FILE = \"best_checkpoint.pth\"  # hoặc \"gesture_two_branch_mixup_pytorch.pth\"\nSCALER_FILE = \"scaler.pkl\"\nFEATS_FILE = \"feature_cols.npy\"\nMAXLEN_FILE = \"sequence_maxlen.npy\"\nCLASSES_FILE = \"gesture_classes.npy\"\n\n# --------- 2) MODEL CLASS: dán NGUYÊN si class dùng khi train ----------\n# Ví dụ nếu bạn dùng TwoBranchHARModel hoặc TwoBranchModel => dán lại ở đây.\n# (BẮT BUỘC class phải khớp kiến trúc với checkpoint)\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\n\n# --- Residual IMU block: phải có conv_block, se_block, shortcut_conv ---\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        hidden = max(1, channels // reduction)\n        self.fc1 = nn.Linear(channels, hidden)\n        self.fc2 = nn.Linear(hidden, channels)\n    def forward(self, x):                     # x: (B,C,T)\n        s = x.mean(dim=2)                     # (B,C)\n        s = F.relu(self.fc1(s), inplace=True)\n        s = torch.sigmoid(self.fc2(s)).unsqueeze(2)  # (B,C,1)\n        return x * s\n\nclass ResidualSECNNBlock(nn.Module):\n    \"\"\"\n    PHẢI ĐÚNG TÊN: conv_block, se_block, shortcut_conv\n    \"\"\"\n    def __init__(self, in_ch, out_ch, k=3, drop=0.1):\n        super().__init__()\n        pad = k // 2\n        self.conv_block = nn.Sequential(             # <- tên này phải đúng\n            nn.Conv1d(in_ch, out_ch, k, padding=pad, bias=False),  # [0]\n            nn.BatchNorm1d(out_ch),                                  # [1]\n            nn.ReLU(inplace=True),                                   # [2]\n            nn.Conv1d(out_ch, out_ch, k, padding=pad, bias=False),   # [3]\n            nn.BatchNorm1d(out_ch),                                  # [4]\n        )\n        self.se_block = SEBlock(out_ch, reduction=8) # <- đúng tên\n        if in_ch != out_ch:\n            self.shortcut_conv = nn.Sequential(      # <- đúng tên\n                nn.Conv1d(in_ch, out_ch, 1, bias=False),\n                nn.BatchNorm1d(out_ch),\n            )\n        else:\n            self.shortcut_conv = nn.Identity()\n\n        self.pool  = nn.MaxPool1d(2)\n        self.drop  = nn.Dropout(drop)\n\n    def forward(self, x):\n        sc = self.shortcut_conv(x)\n        y  = self.conv_block(x)\n        y  = self.se_block(y)\n        y  = F.relu(y + sc, inplace=True)\n        y  = self.pool(y)\n        y  = self.drop(y)\n        return y\n\n# --- ToF block: PHẢI đặt tên \"block\" để khớp 'tof_branch_blockX.block.*' ---\nclass SimpleCNNBlock(nn.Module):\n    def __init__(self, in_filters, out_filters, kernel_size=3, pool_size=2, drop=0.2):\n        super().__init__()\n        pad = kernel_size // 2\n        self.block = nn.Sequential(                          # <- đúng tên\n            nn.Conv1d(in_filters, out_filters, kernel_size, padding=pad, bias=False),\n            nn.BatchNorm1d(out_filters),\n            nn.ReLU(inplace=True),\n            nn.MaxPool1d(pool_size),\n            nn.Dropout(drop)\n        )\n    def forward(self, x):\n        return self.block(x)\n\n# --- Attention: PHẢI có thuộc tính 'score_dense' ---\nclass AttentionLayer(nn.Module):\n    def __init__(self, input_dim):\n        super().__init__()\n        self.score_dense = nn.Linear(input_dim, 1)           # <- đúng tên\n    def forward(self, x):     # x: (B,T,D)\n        s = torch.tanh(self.score_dense(x)).squeeze(2)       # (B,T)\n        a = torch.softmax(s, dim=1).unsqueeze(2)             # (B,T,1)\n        return (x * a).sum(1)                                # (B,D)\n\nclass TwoBranchModel(nn.Module):\n    def __init__(self, pad_len, imu_dim, tof_dim, n_classes):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.tof_dim = tof_dim\n\n        # IMU branch: tên block phải trùng\n        self.imu_branch_block1 = ResidualSECNNBlock(imu_dim, 64, 3, drop=0.1)\n        self.imu_branch_block2 = ResidualSECNNBlock(64, 128, 5, drop=0.1)\n\n        # ToF branch: tên block phải trùng\n        self.tof_branch_block1 = SimpleCNNBlock(tof_dim, 64, drop=0.2)\n        self.tof_branch_block2 = SimpleCNNBlock(64, 128, drop=0.2)\n\n        merged_cnn_features = 128 + 128\n        self.recurrent_layer = nn.LSTM(\n            input_size=merged_cnn_features,\n            hidden_size=128,\n            bidirectional=True,\n            batch_first=True\n        )\n\n        self.attention_layer = AttentionLayer(input_dim=128*2)\n\n        self.classifier_head = nn.Sequential(  # checkpoint có 'classifier_head.*'\n            nn.Linear(256, 256, bias=False),   # [0]\n            nn.BatchNorm1d(256),               # [1]\n            nn.ReLU(inplace=True),             # [2]\n            nn.Dropout(0.5),                   # [3]\n            nn.Linear(256, 128, bias=False),   # [4]\n            nn.BatchNorm1d(128),               # [5]\n            nn.ReLU(inplace=True),             # [6]\n            nn.Dropout(0.3),                   # [7]\n        )\n        self.output_layer = nn.Linear(128, n_classes)\n\n    def forward(self, x):  # x: (B, pad_len, F)\n        imu = x[:, :, :self.imu_dim]\n        tof = x[:, :, self.imu_dim:]\n        imu = imu.permute(0, 2, 1)   # (B,C,T)\n        tof = tof.permute(0, 2, 1)\n\n        x1 = self.imu_branch_block1(imu)   # (B,64,T/2)\n        x1 = self.imu_branch_block2(x1)    # (B,128,T/4)\n\n        x2 = self.tof_branch_block1(tof)   # (B,64,T/2)\n        x2 = self.tof_branch_block2(x2)    # (B,128,T/4)\n\n        # concat theo channel\n        merged = torch.cat([x1, x2], dim=1)   # (B,256,T/4)\n        merged = merged.permute(0, 2, 1)      # (B,T/4,256)\n\n        rnn_out, _ = self.recurrent_layer(merged)  # (B,T/4,256)\n        att = self.attention_layer(rnn_out)        # (B,256)\n        h = self.classifier_head(att)              # (B,128)\n        logits = self.output_layer(h)              # (B,n_classes)\n        return logits\n\n\n# --------- 3) LOAD ARTEFACTS + MODEL ----------\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nfeature_cols = np.load(os.path.join(CKPT_DIR, FEATS_FILE), allow_pickle=True).tolist()\npad_len      = int(np.load(os.path.join(CKPT_DIR, MAXLEN_FILE)))\nclasses      = np.load(os.path.join(CKPT_DIR, CLASSES_FILE), allow_pickle=True)\nscaler       = joblib.load(os.path.join(CKPT_DIR, SCALER_FILE))\n\nimu_dim  = len([c for c in feature_cols if c.startswith(('acc_', 'rot_'))])\nttf_dim  = len(feature_cols) - imu_dim\nnum_cls  = len(classes)\n\nmodel = TwoBranchModel(\n    pad_len=pad_len,\n    imu_dim=imu_dim,\n    tof_dim=ttf_dim,\n    n_classes=num_cls\n).to(device)\n\n\nckpt_path = os.path.join(CKPT_DIR, CKPT_FILE)\n\n# PyTorch 2.6: cần weights_only=False nếu checkpoint là dict pickled\nckpt_path = os.path.join(CKPT_DIR, CKPT_FILE)  # ví dụ 'best_checkpoint.pth'\nckpt = torch.load(ckpt_path, map_location='cpu', weights_only=False)\nstate = ckpt['model_state_dict'] if isinstance(ckpt, dict) and 'model_state_dict' in ckpt else ckpt\n# nếu từng torch.compile\nstate = {k.replace('_orig_mod.', ''): v for k, v in state.items()}\n# trước đây:\n# model.load_state_dict(state, strict=True)\n\n# thay bằng:\nmissing, unexpected = model.load_state_dict(state, strict=False)\nif missing:\n    print(\"Missing keys:\", missing)\nif unexpected:\n    print(\"Unexpected keys:\", unexpected)\n\nmodel.eval()\n\n\n\n\n# ====== (A) Rebuild engineered features exactly like train ======\ndef add_engineered_features(seq_df: pd.DataFrame) -> pd.DataFrame:\n    df = seq_df.copy()\n\n    # IMU engineered features (an toàn khi thiếu cột)\n    for col in ['acc_x','acc_y','acc_z','rot_w']:\n        if col not in df.columns:\n            df[col] = 0.0\n\n    # acc_mag, rot_angle\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'].astype(float), -1, 1))\n\n    # group theo sequence_id nếu có, nếu không thì diff theo index\n    if 'sequence_id' in df.columns:\n        df['acc_mag_jerk'] = df.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n        df['rot_angle_vel'] = df.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n    else:\n        df['acc_mag_jerk'] = df['acc_mag'].diff().fillna(0)\n        df['rot_angle_vel'] = df['rot_angle'].diff().fillna(0)\n\n    # ToF aggregates: mean/std/min/max trên 64 pixel cho từng cảm biến 1..5\n    # Nếu không có đủ cột tof_*_v* thì bỏ qua silently (để tránh crash sample lạ)\n    # ToF aggregates: mean/std/min/max trên 64 pixel cho từng cảm biến 1..5\n    for i in range(1, 6):\n        pix_cols = [f\"tof_{i}_v{p}\" for p in range(64)]\n        if not any(col in df.columns for col in pix_cols):\n        # Không có cột ToF -> tạo cột 0\n            df[f'tof_{i}_mean'] = 0.0\n            df[f'tof_{i}_std']  = 0.0\n            df[f'tof_{i}_min']  = 0.0\n            df[f'tof_{i}_max']  = 0.0\n            continue\n\n    # Lấy những cột tồn tại; nếu thiếu vài cột vẫn chạy được\n        exist_cols = [c for c in pix_cols if c in df.columns]\n        block = df[exist_cols].replace(-1, np.nan).to_numpy(dtype=float)  # (T, K<=64)\n\n    # Tính với NaN-safe; sau đó NaN -> 0 để tránh warning & giữ shape ổn định\n        with np.errstate(all='ignore'):\n            m = np.nanmean(block, axis=1)\n            s = np.nanstd(block,  axis=1)  # ddof=0 mặc định\n            mn = np.nanmin(block,  axis=1)\n            mx = np.nanmax(block,  axis=1)\n\n        m  = np.nan_to_num(m,  nan=0.0)\n        s  = np.nan_to_num(s,  nan=0.0)\n        mn = np.nan_to_num(mn, nan=0.0)\n        mx = np.nan_to_num(mx, nan=0.0)\n\n        df[f'tof_{i}_mean'] = m\n        df[f'tof_{i}_std']  = s\n        df[f'tof_{i}_min']  = mn\n        df[f'tof_{i}_max']  = mx\n\n\n    return df\n\n# ====== (B) Preprocess khớp TwoBranchModel: (1, pad_len, F) ======\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences as keras_pad_sequences\n\ndef preprocess_sequence_two_branch(seq_df: pd.DataFrame) -> torch.Tensor:\n    # 1) rebuild engineered features\n    df_eng = add_engineered_features(seq_df)\n\n    # 2) đảm bảo đủ cột theo feature_cols.npy (thứ tự PHẢI khớp)\n    missing = [c for c in feature_cols if c not in df_eng.columns]\n    if missing:\n        # tạo cột trống cho mọi cột còn thiếu (trường hợp thm_* v.v. vắng)\n        for c in missing:\n            df_eng[c] = 0.0\n\n    # 3) chọn và sắp xếp đúng thứ tự\n    X = df_eng[feature_cols].ffill().bfill().fillna(0).to_numpy(dtype=float)   # (T, F)\n\n    # 4) scale theo scaler đã fit khi train\n    X = scaler.transform(X)\n\n    # 5) pad/truncate về pad_len\n    Xpad = keras_pad_sequences([X], maxlen=pad_len, dtype='float32',\n                               padding='post', truncating='post')[0]           # (pad_len, F)\n\n    # 6) tensor cho TwoBranchModel: (1, pad_len, F)\n    return torch.from_numpy(Xpad).unsqueeze(0).float()\n\n# ====== (C) predict() cho Inference Server ======\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    seq_df = sequence.to_pandas()\n\n    # nếu lúc train bạn KHÔNG merge demographics -> không cần merge ở đây\n    # nếu có merge demographics khi train, bạn phải merge tương tự tại đây.\n\n    x = preprocess_sequence_two_branch(seq_df).to(device)\n    with torch.no_grad():\n        logits = model(x)\n        idx = int(torch.argmax(logits, dim=1).item())\n    return str(classes[idx])\n\n\n# --------- 5) START CMI INFERENCE SERVER ----------\nimport kaggle_evaluation.cmi_inference_server\n\nserver = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    server.serve()\nelse:\n    server.run_local_gateway(\n        data_paths=(\n            f\"{RAW_DIR}/test.csv\",\n            f\"{RAW_DIR}/test_demographics.csv\",\n        )\n    )\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-18T08:46:23.960651Z","iopub.execute_input":"2025-08-18T08:46:23.961052Z","iopub.status.idle":"2025-08-18T08:46:25.166358Z","shell.execute_reply.started":"2025-08-18T08:46:23.961028Z","shell.execute_reply":"2025-08-18T08:46:25.165394Z"}},"outputs":[],"execution_count":null}]}