{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":13242446,"sourceType":"datasetVersion","datasetId":8390817}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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-10-15T13:34:25.667574Z","iopub.execute_input":"2025-10-15T13:34:25.668529Z","iopub.status.idle":"2025-10-15T13:34:34.572886Z","shell.execute_reply.started":"2025-10-15T13:34:25.668499Z","shell.execute_reply":"2025-10-15T13:34:34.572101Z"}},"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-10-15T13:34:34.574237Z","iopub.execute_input":"2025-10-15T13:34:34.574580Z","iopub.status.idle":"2025-10-15T13:34:34.579986Z","shell.execute_reply.started":"2025-10-15T13:34:34.574559Z","shell.execute_reply":"2025-10-15T13:34:34.578988Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================\n# Imports & Config\n# ==========================\nimport os, time, math, random, gc\nimport numpy as np\nimport pandas as pd\nimport torch, torch.nn as nn, torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom sklearn.preprocessing import LabelEncoder\nfrom sklearn.metrics import f1_score, accuracy_score\n\nSEED = 42\nrandom.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nOPTIMIZER_NAME = \"AdamW\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.580809Z","iopub.execute_input":"2025-10-15T13:34:34.581028Z","iopub.status.idle":"2025-10-15T13:34:34.598311Z","shell.execute_reply.started":"2025-10-15T13:34:34.581012Z","shell.execute_reply":"2025-10-15T13:34:34.597511Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================\n# Utilities\n# ==========================\ndef infer_grid_shape(n_channels):\n    r = int(round(math.sqrt(n_channels)))\n    return (r, r) if r * r == n_channels else (8, 8)\n\ndef grid_adjacency(H, W, mode='8n', weight='uniform'):\n    N = H * W\n    A = np.zeros((N, N), dtype=np.float32)\n    def idx(r, c): return r * W + c\n    nbrs = [(-1, 0), (1, 0), (0, -1), (0, 1)]\n    if mode == '8n': nbrs += [(-1, -1), (-1, 1), (1, -1), (1, 1)]\n    for r in range(H):\n        for c in range(W):\n            i = idx(r, c)\n            for dr, dc in nbrs:\n                rr, cc = r + dr, c + dc\n                if 0 <= rr < H and 0 <= cc < W:\n                    j = idx(rr, cc)\n                    if weight == 'uniform':\n                        A[i, j] = 1.0\n                    else:\n                        d = math.sqrt(dr**2 + dc**2)\n                        A[i, j] = 1.0 / (d + 1e-6)\n    A += np.eye(N)\n    D = A.sum(axis=1)\n    D_inv_sqrt = 1.0 / np.sqrt(D + 1e-8)\n    A_hat = (A * D_inv_sqrt[:, None]) * D_inv_sqrt[None, :]\n    return torch.from_numpy(A_hat)\n\ndef add_mask_channel(x, sentinel=-1.0):\n    val = np.where(x == sentinel, 0, x)\n    mask = (~np.isclose(x, sentinel)).astype(np.float32)\n    return np.stack([val, mask], axis=-1)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.600309Z","iopub.execute_input":"2025-10-15T13:34:34.600637Z","iopub.status.idle":"2025-10-15T13:34:34.614432Z","shell.execute_reply.started":"2025-10-15T13:34:34.600619Z","shell.execute_reply":"2025-10-15T13:34:34.613640Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==========================\n# Dataset\n# ==========================\nclass ToFDataset(Dataset):\n    def __init__(self, X, y):\n        self.X = X.astype(np.float32)\n        self.y = y.astype(np.int64)\n    def __len__(self): return len(self.X)\n    def __getitem__(self, i):\n        x = np.transpose(self.X[i], (0, 3, 1, 2))  # [T, C, H, W]\n        return torch.from_numpy(x), int(self.y[i])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.615118Z","iopub.execute_input":"2025-10-15T13:34:34.615310Z","iopub.status.idle":"2025-10-15T13:34:34.634357Z","shell.execute_reply.started":"2025-10-15T13:34:34.615295Z","shell.execute_reply":"2025-10-15T13:34:34.633581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GraphConv(torch.nn.Module):\n    def __init__(self, in_dim, out_dim):\n        super().__init__()\n        self.lin = torch.nn.Linear(in_dim, out_dim, bias=False)\n    def forward(self, X, A):\n        return torch.matmul(A, self.lin(X))\n\nclass SimpleGCN(torch.nn.Module):\n    def __init__(self, H, W, in_ch, nclass):\n        super().__init__()\n        self.N = H * W\n        self.gc1 = GraphConv(in_ch, 32)\n        self.gc2 = GraphConv(32, 64)\n        self.fc = torch.nn.Linear(64, nclass)\n\n    def forward(self, x, A):\n        # x: (B, T, H, W, C)\n        B, T, H, W, C = x.shape\n        x = x.reshape(B, T, H * W, C)\n        outs = []\n        for t in range(T):\n            h = F.relu(self.gc1(x[:, t], A))\n            h = F.relu(self.gc2(h, A))\n            pooled = h.mean(1)\n            outs.append(pooled)\n        out = torch.stack(outs, 1).mean(1)\n        return self.fc(out)\n\nclass SimpleGAT(torch.nn.Module):\n    def __init__(self, H, W, in_ch, nclass, heads=4):\n        super().__init__()\n        self.N = H * W\n        self.fc1 = torch.nn.Linear(in_ch, 32)\n        self.attn = torch.nn.MultiheadAttention(32, num_heads=heads, batch_first=True)\n        self.fc2 = torch.nn.Linear(32, nclass)\n\n    def forward(self, x, A=None):  # A không dùng, nhưng giữ để tương thích\n        B, T, H, W, C = x.shape\n        x = x.reshape(B, T, H * W, C)\n        outs = []\n        for t in range(T):\n            h = F.relu(self.fc1(x[:, t]))\n            attn_out, _ = self.attn(h, h, h)\n            pooled = attn_out.mean(1)\n            outs.append(pooled)\n        out = torch.stack(outs, 1).mean(1)\n        return self.fc2(out)\n\nclass CNN2D(torch.nn.Module):\n    def __init__(self, nclass, in_ch):\n        super().__init__()\n        self.frame = torch.nn.Sequential(\n            torch.nn.Conv2d(in_ch, 32, 3, padding=1),\n            torch.nn.ReLU(),\n            torch.nn.MaxPool2d(2),\n            torch.nn.Conv2d(32, 64, 3, padding=1),\n            torch.nn.ReLU(),\n            torch.nn.AdaptiveAvgPool2d(1)\n        )\n        self.temporal = torch.nn.LSTM(64, 64, batch_first=True)\n        self.fc = torch.nn.Linear(64, nclass)\n\n    def forward(self, x):\n        # x: (B, T, H, W, C)\n        B, T, H, W, C = x.shape\n        feats = []\n        for t in range(T):\n            xt = x[:, t].permute(0, 3, 1, 2)  # -> (B, C, H, W)\n            feat = self.frame(xt).flatten(1)\n            feats.append(feat)\n        z = torch.stack(feats, 1)\n        z, _ = self.temporal(z)\n        z = z.mean(1)\n        return self.fc(z)\n\nclass CNN3D(torch.nn.Module):\n    def __init__(self, nclass, in_ch):\n        super().__init__()\n        self.net = torch.nn.Sequential(\n            torch.nn.Conv3d(in_ch, 16, 3, padding=1),\n            torch.nn.ReLU(),\n            torch.nn.MaxPool3d((2,2,2)),\n            torch.nn.Conv3d(16, 32, 3, padding=1),\n            torch.nn.ReLU(),\n            torch.nn.AdaptiveAvgPool3d(1)\n        )\n        self.fc = torch.nn.Linear(32, nclass)\n\n    def forward(self, x):\n        # x: (B, T, H, W, C)\n        x = x.permute(0, 4, 1, 2, 3)  # -> (B, C, T, H, W)\n        z = self.net(x).flatten(1)\n        return self.fc(z)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.635328Z","iopub.execute_input":"2025-10-15T13:34:34.635656Z","iopub.status.idle":"2025-10-15T13:34:34.655069Z","shell.execute_reply.started":"2025-10-15T13:34:34.635636Z","shell.execute_reply":"2025-10-15T13:34:34.654193Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\n# Giả lập dữ liệu (bạn thay bằng dữ liệu thực nếu có)\nN, T, H, W, C = 100, 10, 8, 8, 3   # 100 mẫu, 10 khung, 8x8, 3 kênh\nX = np.random.rand(N, T, H, W, C).astype(np.float32)\ny = np.random.randint(0, 2, size=(N,))  # ví dụ 2 lớp (nhị phân)\n\n# Chia train/val\nX_train, X_val = X[:80], X[80:]\ny_train, y_val = y[:80], y[80:]\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.655939Z","iopub.execute_input":"2025-10-15T13:34:34.656150Z","iopub.status.idle":"2025-10-15T13:34:34.677067Z","shell.execute_reply.started":"2025-10-15T13:34:34.656135Z","shell.execute_reply":"2025-10-15T13:34:34.676422Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.preprocessing import StandardScaler\nimport numpy as np\n\nX_train_reshaped = X_train.reshape(-1, X_train.shape[-1])\nX_val_reshaped = X_val.reshape(-1, X_val.shape[-1])\n\nscaler = StandardScaler().fit(X_train_reshaped)\nX_train = scaler.transform(X_train_reshaped).reshape(X_train.shape)\nX_val = scaler.transform(X_val_reshaped).reshape(X_val.shape)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.677876Z","iopub.execute_input":"2025-10-15T13:34:34.678047Z","iopub.status.idle":"2025-10-15T13:34:34.727306Z","shell.execute_reply.started":"2025-10-15T13:34:34.678034Z","shell.execute_reply":"2025-10-15T13:34:34.726578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_experiments(X_train, y_train, X_val, y_val, H=8, W=8, mask_channel=None, epochs=80):\n    import torch, torch.nn as nn, torch.nn.functional as F, numpy as np, time, pandas as pd\n    from sklearn.metrics import f1_score, accuracy_score\n\n    DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    n_classes = len(np.unique(y_train))\n    in_channels = X_train.shape[-1]\n\n    # === Helper: adjacency 8-neighbor ===\n    def build_adjacency(H, W):\n        A = torch.zeros(H*W, H*W)\n        for i in range(H):\n            for j in range(W):\n                idx = i * W + j\n                for di in [-1, 0, 1]:\n                    for dj in [-1, 0, 1]:\n                        ni, nj = i+di, j+dj\n                        if 0 <= ni < H and 0 <= nj < W:\n                            n_idx = ni * W + nj\n                            A[idx, n_idx] = 1\n        D_inv = torch.diag(1.0 / (A.sum(1) + 1e-5))\n        return (D_inv @ A).to(DEVICE)\n\n    A_hat = build_adjacency(H, W)\n\n    # === DataLoader ===\n    bs = 32\n    dl_tr = torch.utils.data.DataLoader(\n        torch.utils.data.TensorDataset(torch.tensor(X_train, dtype=torch.float32),\n                                       torch.tensor(y_train, dtype=torch.long)),\n        batch_size=bs, shuffle=True)\n    dl_va = torch.utils.data.DataLoader(\n        torch.utils.data.TensorDataset(torch.tensor(X_val, dtype=torch.float32),\n                                       torch.tensor(y_val, dtype=torch.long)),\n        batch_size=bs, shuffle=False)\n\n    # === Models ===\n    models = {\n        \"GCN\": lambda: SimpleGCN(H, W, in_ch=in_channels, nclass=n_classes),\n        \"GAT\": lambda: SimpleGAT(H, W, in_ch=in_channels, nclass=n_classes),\n        \"CNN2D\": lambda: CNN2D(nclass=n_classes, in_ch=in_channels),\n        \"CNN3D\": lambda: CNN3D(nclass=n_classes, in_ch=in_channels),\n    }\n\n    # === Evaluate ===\n    @torch.no_grad()\n    def evaluate(model, dl, A=None):\n        model.eval()\n        y_true, y_pred = [], []\n        for xb, yb in dl:\n            xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n            logits = model(xb, A) if A is not None else model(xb)\n            y_pred += logits.argmax(1).cpu().tolist()\n            y_true += yb.cpu().tolist()\n        macro = f1_score(y_true, y_pred, average='macro')\n        binary = f1_score(y_true, y_pred, average='binary') if len(set(y_true)) == 2 else 0\n        acc = accuracy_score(y_true, y_pred)\n        return binary, macro, acc\n\n    results = []\n\n    # === Train each model ===\n    for name, fn in models.items():\n        print(f\"\\n▶ Training {name} ...\")\n        model = fn().to(DEVICE)\n        opt = torch.optim.AdamW(model.parameters(), lr=5e-4, weight_decay=1e-4)\n        scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=epochs)\n        params_m = sum(p.numel() for p in model.parameters()) / 1e6\n\n        best_macro = 0\n        patience = 0\n        start = time.time()\n\n        for ep in range(1, epochs+1):\n            model.train()\n            for xb, yb in dl_tr:\n                xb, yb = xb.to(DEVICE), yb.to(DEVICE)\n                opt.zero_grad()\n                logits = model(xb, A_hat) if name in [\"GCN\", \"GAT\"] else model(xb)\n                loss = F.cross_entropy(logits, yb)\n                loss.backward()\n                opt.step()\n            scheduler.step()\n\n            bin_f1, mac_f1, acc = evaluate(model, dl_va, A_hat if name in [\"GCN\",\"GAT\"] else None)\n            if ep % 5 == 0:\n                print(f\"Epoch {ep}/{epochs}: loss={loss.item():.3f}, Macro={mac_f1:.3f}, Acc={acc:.3f}\")\n            if mac_f1 > best_macro:\n                best_macro = mac_f1\n                patience = 0\n            else:\n                patience += 1\n                if patience > 10:\n                    print(f\"⏹ Early stop at epoch {ep}\")\n                    break\n\n        train_time = time.time() - start\n        start_inf = time.time()\n        evaluate(model, dl_va, A_hat if name in [\"GCN\",\"GAT\"] else None)\n        inf_time = (time.time() - start_inf) / len(dl_va.dataset)\n\n        bin_f1, mac_f1, acc = evaluate(model, dl_va, A_hat if name in [\"GCN\",\"GAT\"] else None)\n        results.append({\n            \"features_used\": \"ToF_Grid\",\n            \"window_size\": 10,\n            \"optimizer/solver\": \"AdamW+Cosine\",\n            \"params (M)\": round(params_m, 4),\n            \"Binary\": round(bin_f1, 4),\n            \"Macro\": round(mac_f1, 4),\n            \"Final Score\": round(mac_f1/2, 4),\n            \"val_acc\": round(acc, 4),\n            \"train_time\": round(train_time, 2),\n            \"inference_time\": round(inf_time, 4),\n        })\n\n    df = pd.DataFrame(results)\n    print(\"\\n=== RESULTS ===\")\n    print(df)\n    return df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.728152Z","iopub.execute_input":"2025-10-15T13:34:34.728424Z","iopub.status.idle":"2025-10-15T13:34:34.746091Z","shell.execute_reply.started":"2025-10-15T13:34:34.728401Z","shell.execute_reply":"2025-10-15T13:34:34.745112Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"report = run_experiments(X_train, y_train, X_val, y_val, H=8, W=8, epochs=80)\nreport.to_csv(\"graph_spatial_results.csv\", index=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-15T13:34:34.748009Z","iopub.execute_input":"2025-10-15T13:34:34.748502Z","iopub.status.idle":"2025-10-15T13:34:41.303611Z","shell.execute_reply.started":"2025-10-15T13:34:34.748484Z","shell.execute_reply":"2025-10-15T13:34:41.302970Z"}},"outputs":[],"execution_count":null}]}