{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":98446,"databundleVersionId":11748273,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nimport pandas as pd \nimport os\nimport ast, random, re\nfrom tqdm.notebook import tqdm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchmetrics.classification import MulticlassF1Score\nfrom tqdm import tqdm\n\n\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:38:22.315534Z","iopub.execute_input":"2025-06-30T11:38:22.315794Z","iopub.status.idle":"2025-06-30T11:38:37.021074Z","shell.execute_reply.started":"2025-06-30T11:38:22.315771Z","shell.execute_reply":"2025-06-30T11:38:37.020236Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Nhét LODGO LOSO, nhét khử nhiễu","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:38:37.021834Z","iopub.execute_input":"2025-06-30T11:38:37.022234Z","iopub.status.idle":"2025-06-30T11:38:37.025822Z","shell.execute_reply.started":"2025-06-30T11:38:37.022216Z","shell.execute_reply":"2025-06-30T11:38:37.025112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ------------------- Cấu hình chung -----------------------\nclass CFG:\n    seed          = 42\n    # device        = torch.device(\"xla\" if torch.cuda.is_available() else \"cpu\")\n    device        = torch.device(\"cuda\")\n\n    data_path     = \"/kaggle/input/2nd-wear-dataset-challenge\"\n    test_path     = \"/kaggle/input/2nd-wear-dataset-challenge/test.csv\"\n\n    window_size   = 50   # 1 giây @ 50 Hz\n    stride        = 25\n\n    # ViT-LSTM\n    vit_dim       = 256\n    \n    vit_heads     = 32\n    heads         = 32\n\n    mlp_ratio     = 4.0 \n    vit_depth     = 16\n    vit_mlp_ratio = 4.0\n    vit_dropout   = 0.3\n    dropout        = 0.3\n    \n    patch_size    = 5\n    num_blocks    = 8\n    \n    lstm_hidden   = 256\n    lstm_layers   = 4\n    lstm_dropout  = 0.3\n\n    num_classes   = 19\n    batch_size    = 256\n    epochs        = 200\n    lr            = 1e-3\n\n    \ndef seed_everything(seed: int = 42):\n    random.seed(seed)\n    np.random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark     = False\n\nseed_everything(CFG.seed)\nprint(f\"Using device: {CFG.device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:38:37.027554Z","iopub.execute_input":"2025-06-30T11:38:37.027786Z","iopub.status.idle":"2025-06-30T11:38:37.067595Z","shell.execute_reply.started":"2025-06-30T11:38:37.027769Z","shell.execute_reply":"2025-06-30T11:38:37.066789Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pd.options.mode.use_inf_as_na = True       \nACC_RE = re.compile(r'_(acc_[xyz])$')  \n\nlabel_map = {\n    'null': 0, 'jogging': 1, 'jogging (rotating arms)': 2, 'jogging (skipping)': 3,\n    'jogging (sidesteps)': 4, 'jogging (butt-kicks)': 5, 'stretching (triceps)': 6,\n    'stretching (lunging)': 7, 'stretching (shoulders)': 8, 'stretching (hamstrings)': 9,\n    'stretching (lumbar rotation)': 10, 'push-ups': 11, 'push-ups (complex)': 12,\n    'sit-ups': 13, 'sit-ups (complex)': 14, 'burpees': 15, 'lunges': 16,\n    'lunges (complex)': 17, 'bench-dips': 18\n}\n\ndef load_one_train(path: str) -> pd.DataFrame:\n    df = pd.read_csv(\n        path,\n        dtype={'label': 'string'},\n        low_memory=False\n    )\n\n    # ---- ép nhãn ----\n    if 'label' in df.columns:\n        df['label'] = (df['label'].str.strip().str.lower()\n                        .map(label_map).astype('Int8', errors='ignore'))\n\n    acc_cols = [c for c in df.columns if ACC_RE.search(c)]\n    df[acc_cols] = df[acc_cols].astype(np.float32)\n\n\n    if acc_cols:\n        df[acc_cols] = (\n            df.groupby('sbj_id', group_keys=False)[acc_cols]\n              .apply(lambda g: g.interpolate(method='linear',\n                                              limit_direction='both'))\n        )\n\n    return df\n\n# --------- load TRAIN ----------\ntrain_dir   = os.path.join(CFG.data_path, \"train\")\ntrain_files = [f for f in os.listdir(train_dir) if f.endswith('.csv')]\n\ndf_train_raw = pd.concat(\n    [load_one_train(os.path.join(train_dir, f)) for f in tqdm(train_files, desc=\"Loading train files\")],\n    ignore_index=True\n)\n\n# --------- load TEST ----------\ndf_test_raw = pd.read_csv(\n    CFG.test_path,\n    converters={'x_axis': ast.literal_eval,\n                'y_axis': ast.literal_eval,\n                'z_axis': ast.literal_eval}\n)\n\nprint(\"Train shape:\", df_train_raw.shape, \"| Test shape:\", df_test_raw.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:38:37.068559Z","iopub.execute_input":"2025-06-30T11:38:37.068838Z","iopub.status.idle":"2025-06-30T11:39:20.453601Z","shell.execute_reply.started":"2025-06-30T11:38:37.068813Z","shell.execute_reply":"2025-06-30T11:39:20.452868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Testing!!!\n\n\ndf_test = df_test_raw.copy()\ndf_test['window_idx'] = df_test.groupby(['sbj_id','sensor_location']).cumcount()\n\n# pivot sang wide format\ndf_wide = df_test.pivot(\n    index=['sbj_id','window_idx'],\n    columns='sensor_location',\n    values=['x_axis','y_axis','z_axis']\n)\ndf_wide.columns = [f\"{axis}_{loc}\" for axis,loc in df_wide.columns]\ndf_wide = df_wide.reset_index()\n\nchannels = []\nfor loc in ['right_arm','left_arm','right_leg','left_leg']:\n    for axis in ['x_axis','y_axis','z_axis']:\n        channels.append(f\"{axis}_{loc}\")\n\nX_test = np.stack(\n    [ np.stack(df_wide[ch].tolist(), axis=0) for ch in channels ],\n    axis=1\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:39:20.454277Z","iopub.execute_input":"2025-06-30T11:39:20.454529Z","iopub.status.idle":"2025-06-30T11:39:21.257035Z","shell.execute_reply.started":"2025-06-30T11:39:20.454503Z","shell.execute_reply":"2025-06-30T11:39:21.256480Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df_train_raw['label'] = (\n    df_train_raw['label']\n      .fillna(0)\n      .astype('Int8')\n)\n\n\nvals = df_train_raw['label'].dropna().unique()\ndf_train_raw['label'] = df_train_raw['label'].fillna(0)\n\ndf_train_raw['label'] = df_train_raw['label'].astype(int)\n\nprint(\"Tổng cộng\", len(vals), \"giá trị:\")\nprint(*sorted(vals), sep=\"\\n\")\n\nprint(\"Train data shape:\", df_train_raw.shape)\nprint(\"Test  data shape:\", df_test_raw.shape)\ndisplay(df_train_raw)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:39:21.257765Z","iopub.execute_input":"2025-06-30T11:39:21.257977Z","iopub.status.idle":"2025-06-30T11:39:21.340650Z","shell.execute_reply.started":"2025-06-30T11:39:21.257960Z","shell.execute_reply":"2025-06-30T11:39:21.340014Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"SENSOR_LOCATIONS = ['right_arm', 'left_arm', 'right_leg', 'left_leg']\n\ndef create_location_specific_windows(df_raw, win_size, stride):\n    all_windows = []\n    \n    for sbj_id, group in tqdm(df_raw.groupby('sbj_id'), desc=\"Creating windows\"):\n        labels = group['label'].values\n        \n        # Tạo dữ liệu cho từng vị trí\n        location_data = {}\n        for loc in SENSOR_LOCATIONS:\n            cols = [f'{loc}_acc_x', f'{loc}_acc_y', f'{loc}_acc_z']\n            location_data[loc] = group[cols].values.T # Shape (3, N)\n            \n        n_samples = len(group)\n        for start in range(0, n_samples - win_size + 1, stride):\n            end = start + win_size\n            \n            # Lấy nhãn cho window (lấy nhãn cuối cùng)\n            window_label = labels[end-1]\n            \n            # Tạo một window cho mỗi vị trí\n            for loc in SENSOR_LOCATIONS:\n                window_tensor = location_data[loc][:, start:end].astype(np.float32)\n                all_windows.append({\n                    'tensor': window_tensor,\n                    'label': window_label,\n                    'sensor_location': loc,\n                    'sbj_id': sbj_id\n                })\n                \n    return pd.DataFrame(all_windows)\n\ndf_windows = create_location_specific_windows(df_train_raw, CFG.window_size, CFG.stride)\n\nprint(f\"Total windows created: {len(df_windows)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:39:21.341414Z","iopub.execute_input":"2025-06-30T11:39:21.341732Z","iopub.status.idle":"2025-06-30T11:39:24.351337Z","shell.execute_reply.started":"2025-06-30T11:39:21.341699Z","shell.execute_reply":"2025-06-30T11:39:24.350500Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nfrom scipy.interpolate import CubicSpline\n\ndef augment_jitter(x: np.ndarray, sigma: float = 0.05) -> np.ndarray:\n    noise = np.random.normal(loc=0.0, scale=sigma, size=x.shape)\n    return (x + noise).astype(x.dtype)\n\n\ndef augment_scale(x: np.ndarray, scale_range: tuple[float, float] = (0.9, 1.1)) -> np.ndarray:\n    factor = np.random.uniform(scale_range[0], scale_range[1])\n    return (x * factor).astype(x.dtype)\n\n\ndef augment_rotation(x: np.ndarray) -> np.ndarray:\n    if x.shape[0] != 3:\n        raise ValueError(f\"augment_rotation expects x.shape[0] == 3, got {x.shape[0]}\")\n    \n    # random unit axis\n    axis = np.random.randn(3).astype(x.dtype)\n    axis /= np.linalg.norm(axis) + 1e-8\n    \n    # random angle in [-π, π]\n    angle = np.random.uniform(-np.pi, np.pi)\n    \n    # skew-symmetric cross-product matrix K\n    K = np.array([\n        [0,        -axis[2],  axis[1]],\n        [axis[2],   0,       -axis[0]],\n        [-axis[1],  axis[0],  0      ]\n    ], dtype=x.dtype)\n    \n    # Rodrigues' rotation formula\n    R = (\n        np.eye(3, dtype=x.dtype) +\n        np.sin(angle) * K +\n        (1 - np.cos(angle)) * (K @ K)\n    )\n    \n    return R @ x  # shape (3, T)\n\n\ndef augment_time_warp(x: np.ndarray, sigma: float = 0.2, n_knots: int = 4) -> np.ndarray:\n    C, T = x.shape\n    # control points evenly spaced in [0, T-1]\n    knots = np.linspace(0, T - 1, n_knots)\n    # warp factors around 1.0\n    factors = np.random.normal(loc=1.0, scale=sigma, size=n_knots)\n    spline = CubicSpline(knots, factors)\n    \n    # evaluate warp curve at each time step\n    warp_curve = spline(np.arange(T)).astype(x.dtype)\n    \n    # broadcast and apply\n    return (x * warp_curve).astype(x.dtype)\n\n\ndef augment_timenet(x: np.ndarray, max_warp: float = 0.2) -> np.ndarray:\n    C, T = x.shape\n    # sample warp factor\n    warp = np.random.uniform(1 - max_warp, 1 + max_warp)\n    new_T = max(5, int(round(T * warp)))\n    \n    # original and target time indices\n    orig_idx = np.arange(T)\n    target_idx = np.linspace(0, T - 1, new_T)\n    \n    # vectorized per-channel interpolation\n    stretched = np.vstack([\n        np.interp(target_idx, orig_idx, x_channel)\n        for x_channel in x\n    ]).astype(x.dtype)  # shape (C, new_T)\n    \n    # prepare output and fill\n    out = np.zeros((C, T), dtype=x.dtype)\n    if new_T >= T:\n        out[:] = stretched[:, :T]\n    else:\n        out[:, :new_T] = stretched\n        # pad remainder with last value of each channel\n        out[:, new_T:] = stretched[:, -1, np.newaxis]\n    \n    return out\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T11:39:24.352206Z","iopub.execute_input":"2025-06-30T11:39:24.352511Z","iopub.status.idle":"2025-06-30T11:39:24.363473Z","shell.execute_reply.started":"2025-06-30T11:39:24.352486Z","shell.execute_reply":"2025-06-30T11:39:24.362736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class WearDataset(Dataset):\n    def __init__(self, dataframe, mean, std, is_train=True, has_label=True):\n        self.df = dataframe.reset_index(drop=True)\n        self.mean = mean\n        self.std = std\n        self.is_train = is_train\n        self.has_label = has_label\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        x = row['tensor'] # Shape (3, 50)\n        # x_np = row['tensor'].astype(np.float32)\n\n        if self.is_train:\n            if random.random() < 0.4:\n                x = augment_jitter(x)\n            if random.random() < 0.6:\n                x = augment_scale(x)\n            if random.random() < 0.6:\n                x = augment_rotation(x)\n            if random.random() < 0.5:\n                x = augment_timenet(x)\n            # if random.random() < 0.3:\n            #     x = augment_time_warp(x)\n        x = torch.from_numpy(x.copy()).to(self.mean.device)\n        x = (x - self.mean) / (self.std + 1e-8)\n\n        # x = (x - self.mean) / self.std\n        \n        # label = row['label'] if self.has_label else -1\n        # return x, torch.tensor(label, dtype=torch.long)\n\n        if self.has_label and 'label' in row:\n            label = int(row['label'])\n        else:\n            label = 0\n        return x, torch.tensor(label, dtype=torch.long)\ndef make_loader(df, mean, std, is_train, has_label=True, shuffle=False, batch_size=CFG.batch_size):\n    ds = WearDataset(df, mean, std, is_train=is_train, has_label=has_label)\n    return DataLoader(ds,\n                      batch_size=batch_size,\n                      shuffle=shuffle,\n                      num_workers=8 if torch.cuda.is_available() else 0,\n                      persistent_workers=True if torch.cuda.is_available() else False,\n                      pin_memory=True,\n                      drop_last=shuffle)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:21:24.595736Z","iopub.execute_input":"2025-06-30T12:21:24.596327Z","iopub.status.idle":"2025-06-30T12:21:24.604682Z","shell.execute_reply.started":"2025-06-30T12:21:24.596297Z","shell.execute_reply":"2025-06-30T12:21:24.603957Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\n\nclass PatchEmbed1D(nn.Module):\n    \"\"\"\n    Splits a 1D signal into non-overlapping patches and projects them to an embedding dimension.\n    This acts as the first CNN layer.\n    Input:  x of shape (B, C, T)\n    Output: tensor of shape (B, L, D) where L = T // patch_size\n    \"\"\"\n    def __init__(self, in_ch: int, dim: int, patch: int):\n        super().__init__()\n        self.proj = nn.Conv1d(\n            in_channels=in_ch,\n            out_channels=dim,\n            kernel_size=patch,\n            stride=patch,\n            bias=True\n        )\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n        x = self.proj(x)\n        return x.permute(0, 2, 1)\n\n\nclass LocationSpecificModel(nn.Module):\n\n    def __init__(self, cfg: CFG, in_channels: int = 3):\n        super().__init__()\n        num_patches = cfg.window_size // cfg.patch_size\n\n        self.patch = PatchEmbed1D(in_ch=in_channels, dim=cfg.vit_dim, patch=cfg.patch_size)\n        self.pos_emb = nn.Parameter(torch.zeros(1, num_patches, cfg.vit_dim))\n        nn.init.trunc_normal_(self.pos_emb, std=.02)\n\n        self.lstm = nn.LSTM(\n            input_size=cfg.vit_dim,\n            hidden_size=cfg.lstm_hidden,\n            num_layers=cfg.lstm_layers,\n            dropout=cfg.lstm_dropout if cfg.lstm_layers > 1 else 0.0,\n            batch_first=True,\n            bidirectional=True\n        )\n\n        self.attn_pool = nn.Sequential(\n            nn.Linear(cfg.lstm_hidden * 2, cfg.lstm_hidden * 2),\n            nn.Tanh(),\n            nn.Linear(cfg.lstm_hidden * 2, 1),\n            nn.Softmax(dim=1)\n        )\n\n        merge_conv_out_dim = 256\n        self.merge_conv = nn.Conv1d(\n            in_channels=cfg.lstm_hidden * 2,\n            out_channels=merge_conv_out_dim,\n            kernel_size=1,  # Different from patch_size\n            bias=True\n        )\n\n        self.head = nn.Sequential(\n            nn.LayerNorm(merge_conv_out_dim),\n            nn.GELU(),\n            nn.Dropout(0.3),\n            nn.Linear(merge_conv_out_dim, cfg.num_classes)\n        )\n\n    def forward(self, x: torch.Tensor) -> torch.Tensor:\n\n        x = self.patch(x)\n        x = x + self.pos_emb\n\n        lstm_out, _ = self.lstm(x)\n\n        attn_weights = self.attn_pool(lstm_out)\n        pooled = torch.sum(lstm_out * attn_weights, dim=1)\n\n        pooled_reshaped = pooled.unsqueeze(2)\n        \n        merged_out = self.merge_conv(pooled_reshaped)\n        \n        merged_flat = merged_out.squeeze(2)\n        \n        return self.head(merged_flat)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T13:31:53.360564Z","iopub.execute_input":"2025-06-30T13:31:53.360866Z","iopub.status.idle":"2025-06-30T13:31:53.375068Z","shell.execute_reply.started":"2025-06-30T13:31:53.360846Z","shell.execute_reply":"2025-06-30T13:31:53.374284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_epoch(model, loader, optim, loss_fn, meter, device):\n    is_train = optim is not None\n    model.train() if is_train else model.eval()\n    \n    running_loss = 0.0\n    \n    pbar = tqdm(loader, leave=False)\n    for x, y in pbar:\n        x, y = x.to(device), y.to(device)\n        \n        with torch.set_grad_enabled(is_train):\n            logits = model(x)\n            loss = loss_fn(logits, y)\n            \n            if is_train:\n                optim.zero_grad()\n                loss.backward()\n                optim.step()\n        \n        running_loss += loss.item() * x.size(0)\n        meter.update(logits.softmax(-1), y)\n        \n    epoch_loss = running_loss / len(loader.dataset)\n    epoch_f1 = meter.compute().item()\n    meter.reset()\n    \n    return epoch_loss, epoch_f1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T13:31:53.376093Z","iopub.execute_input":"2025-06-30T13:31:53.376298Z","iopub.status.idle":"2025-06-30T13:31:53.394086Z","shell.execute_reply.started":"2025-06-30T13:31:53.376283Z","shell.execute_reply":"2025-06-30T13:31:53.393334Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# TESTING !!!!\nmodels = {}\nmeans = {}\nstds = {}\n\n\nfor loc in SENSOR_LOCATIONS:\n    print(f\"===== Training model for {loc} =====\")\n    \n    # Lọc dữ liệu cho vị trí hiện tại\n    df_loc = df_windows[df_windows['sensor_location'] == loc].copy()\n    \n    all_tensors = np.stack(df_loc['tensor'].values) # Shape (N, 3, 50)\n    mu  = all_tensors.mean(axis=(0, 2))            # (3,) \n    sig = all_tensors.std(axis=(0, 2)) + 1e-8\n\n    mean = torch.from_numpy(mu).float().unsqueeze(1)\n    std  = torch.from_numpy(sig).float().unsqueeze(1)\n\n    means[loc] = mean\n    stds[loc] = std\n    \n    # Chia dữ liệu\n    all_sbj_ids = df_loc['sbj_id'].unique()\n    train_sbj_ids = np.random.choice(all_sbj_ids, size=int(len(all_sbj_ids)*0.7), replace=False)\n    val_sbj_ids = np.setdiff1d(all_sbj_ids, train_sbj_ids)\n    \n    df_train_loc = df_loc[df_loc['sbj_id'].isin(train_sbj_ids)]\n    df_val_loc = df_loc[df_loc['sbj_id'].isin(val_sbj_ids)]\n    \n    train_loader = make_loader(df_train_loc, mean, std, is_train=True, shuffle=True)\n    val_loader = make_loader(df_val_loc, mean, std, is_train=False, shuffle=False)\n    \n\n    model = LocationSpecificModel(CFG, in_channels=3).to(CFG.device)\n    optimizer = torch.optim.Adam(model.parameters(), lr=CFG.lr, weight_decay=1e-5)\n    loss_fn = nn.CrossEntropyLoss()\n    meter = MulticlassF1Score(num_classes=CFG.num_classes, average='macro').to(CFG.device)\n\n\n    best_f1   = -1.0\n    wait_cnt  = 0 \n    patience  = 14\n    for epoch in range(CFG.epochs):\n        train_loss, train_f1 = run_epoch(model, train_loader, optimizer, loss_fn,\n                                         meter, CFG.device)\n    \n        val_loss,   val_f1   = run_epoch(model, val_loader,   None,      loss_fn,\n                                         meter, CFG.device)\n    \n    \n        print(f\"Epoch {epoch+1}/{CFG.epochs} │ \"\n              f\"Train Loss: {train_loss:.4f}, F1: {train_f1:.4f} │ \"\n              f\"Val Loss: {val_loss:.4f},   F1: {val_f1:.4f}\")\n    \n        if val_f1 > best_f1:\n            best_f1  = val_f1\n            wait_cnt = 0                         \n            torch.save(model.state_dict(), f\"best_model_{loc}.pth\")\n            print(f\"  ↳ New best for {loc}: F1 = {best_f1:.4f}  (model saved)\")\n        else:                   \n            wait_cnt += 1\n            if wait_cnt >= patience:\n                print(f\"Early stopping at epoch {epoch+1} (patience reached).\")\n                break\n    # ------------------------------------\n    \n    # nạp lại trọng số tốt nhất trước khi đánh giá / lưu\n    model.load_state_dict(torch.load(f\"best_model_{loc}.pth\"))\n    models[loc] = model.eval()\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T13:31:53.478542Z","iopub.execute_input":"2025-06-30T13:31:53.478982Z","iopub.status.idle":"2025-06-30T14:42:27.765127Z","shell.execute_reply.started":"2025-06-30T13:31:53.478965Z","shell.execute_reply":"2025-06-30T14:42:27.764097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix\n\n\nclass_names = [name for name, _ in sorted(label_map.items(), key=lambda item: item[1])]\n\nall_true_labels = []\nall_predictions = []\n\n\n\ndef plot_confusion_matrix(y_true, y_pred, class_names, title='Confusion Matrix'):\n    cm = confusion_matrix(y_true, y_pred)\n    \n    plt.figure(figsize=(12, 10))\n    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', \n                xticklabels=class_names, yticklabels=class_names)\n    plt.title(title, fontsize=16)\n    plt.ylabel('True Label')\n    plt.xlabel('Predicted Label')\n    plt.show()\n\n    \n\nprint(\"------------------------------------------------------------------\")\n\nseed_everything(CFG.seed) \n\nfor loc in SENSOR_LOCATIONS:\n    print(f\"\\n===== For sensor: {loc.upper()} =====\")\n    \n    model = LocationSpecificModel(CFG, in_channels=3)\n    model.load_state_dict(torch.load(f\"best_model_{loc}.pth\"))\n    model.to(CFG.device)\n    model.eval()\n    \n    mean = means[loc]\n    std = stds[loc]\n    \n    df_loc = df_windows[df_windows['sensor_location'] == loc].copy()\n    all_sbj_ids = df_loc['sbj_id'].unique()\n    train_sbj_ids = np.random.choice(all_sbj_ids, size=int(len(all_sbj_ids)*0.8), replace=False)\n    val_sbj_ids = np.setdiff1d(all_sbj_ids, train_sbj_ids)\n    df_val_loc = df_loc[df_loc['sbj_id'].isin(val_sbj_ids)]\n\n    val_loader = make_loader(df_val_loc, mean, std, is_train=False, shuffle=False)\n    \n    loc_true_labels = []\n    loc_predictions = []\n    \n    with torch.no_grad():\n        for x_batch, y_batch in tqdm(val_loader, desc=f\"Dự đoán trên tập Val ({loc})\"):\n            x_batch = x_batch.to(CFG.device)\n            logits = model(x_batch)\n            preds = logits.argmax(1)\n            \n            loc_true_labels.extend(y_batch.cpu().numpy())\n            loc_predictions.extend(preds.cpu().numpy())\n            \n    all_true_labels.extend(loc_true_labels)\n    all_predictions.extend(loc_predictions)\n            \n    plot_confusion_matrix(loc_true_labels, loc_predictions, class_names, \n                          title=f'{loc.replace(\"_\", \" \").title()}')\n\n\nprint(\"\\n------------------------------------------------------------------\")\nprint(\"===== All sensor =====\")\nplot_confusion_matrix(all_true_labels, all_predictions, class_names, \n                      title='All sensor')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T14:42:27.767393Z","iopub.execute_input":"2025-06-30T14:42:27.767651Z","iopub.status.idle":"2025-06-30T14:42:45.381049Z","shell.execute_reply.started":"2025-06-30T14:42:27.767611Z","shell.execute_reply":"2025-06-30T14:42:45.380025Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"predictions = []\nfor _, row in tqdm(df_test_raw.iterrows(),\n                   total=len(df_test_raw), desc=\"Predicting\"):\n\n    loc    = row['sensor_location']\n    model  = models[loc]\n    mean   = means[loc].float()          # (3,1)\n    std    = stds[loc].float()\n\n    x_test   = np.stack([row['x_axis'],\n                       row['y_axis'],\n                       row['z_axis']], dtype=np.float32)\n\n    x      = torch.from_numpy(x_test).unsqueeze(0)        # (1,3,50)\n    x      = (x - mean) / (std + 1e-8)\n    x      = x.to(CFG.device, dtype=torch.float32)\n\n    with torch.no_grad():\n        pred = model(x).argmax(1).item()\n    predictions.append(pred)\n\ndf_submission = pd.DataFrame({'id': df_test_raw['id'], 'target_value': predictions})\ndf_submission.to_csv('submission.csv', index=False)\nprint(\"Submission file created successfully!\")\ndf_submission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T14:42:45.382320Z","iopub.execute_input":"2025-06-30T14:42:45.382855Z","iopub.status.idle":"2025-06-30T14:44:43.589810Z","shell.execute_reply.started":"2025-06-30T14:42:45.382823Z","shell.execute_reply":"2025-06-30T14:44:43.589235Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(model)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T14:44:43.591052Z","iopub.execute_input":"2025-06-30T14:44:43.591259Z","iopub.status.idle":"2025-06-30T14:44:43.595602Z","shell.execute_reply.started":"2025-06-30T14:44:43.591244Z","shell.execute_reply":"2025-06-30T14:44:43.594823Z"}},"outputs":[],"execution_count":null}]}