{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":12630199,"sourceType":"datasetVersion","datasetId":7980571}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"from pathlib import Path\n\n# (Competition metric will only be imported when TRAINing)\nTRAIN = False                     # ← set to True when you want to train\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/yoga-dist-quat\")  # used when TRAIN=False\nEXPORT_DIR = Path(\"./\")                                    # artefacts will be saved here\nBATCH_SIZE = 64","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:35:22.062463Z","iopub.execute_input":"2025-08-21T12:35:22.063162Z","iopub.status.idle":"2025-08-21T12:35:22.071016Z","shell.execute_reply.started":"2025-08-21T12:35:22.063135Z","shell.execute_reply":"2025-08-21T12:35:22.070167Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport seaborn as sns\nimport matplotlib.pyplot as plt\nimport pandas as pd\nimport warnings\nfrom matplotlib.ticker import MaxNLocator\nfrom mpl_toolkits.mplot3d import Axes3D\nimport os\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\nfrom sklearn.metrics import accuracy_score, f1_score\nimport joblib\nfrom scipy.spatial.transform import Rotation as R","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:35:22.072249Z","iopub.execute_input":"2025-08-21T12:35:22.072518Z","iopub.status.idle":"2025-08-21T12:35:24.466290Z","shell.execute_reply.started":"2025-08-21T12:35:22.072496Z","shell.execute_reply":"2025-08-21T12:35:24.465077Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Tải tập dữ liệu\n#Dữ liệu cảm biến cho tập huấn luyện (Train Sensor Data): \ntrain_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\n\n#Thông tin nhân khẩu học của đối tượng trong tập huấn luyện (giới tính, tuổi, ...): \ntrain_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\n\n#Dữ liệu cảm biến cho tập kiểm tra:\ntest_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")\n\n# Thông tin nhân khẩu học của đối tượng trong tập kiểm tra:\ntest_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:35:24.467165Z","iopub.execute_input":"2025-08-21T12:35:24.467664Z","iopub.status.idle":"2025-08-21T12:35:59.489469Z","shell.execute_reply.started":"2025-08-21T12:35:24.467632Z","shell.execute_reply":"2025-08-21T12:35:59.488737Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nimport joblib\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\n\nimport kaggle_evaluation.cmi_inference_server\n\n# ----------------------------------------------------------------------\n# 1. Utility functions\n# ----------------------------------------------------------------------\n\ndef mixup_data(x, y, alpha=0.5):\n    \"\"\"\n    Return mixed inputs and mixed targets (one-hot) for mixup.\n    x: Tensor of shape (batch_size, features, seq_len)\n    y: Tensor of shape (batch_size, num_classes)\n    \"\"\"\n    if alpha > 0:\n        lam = np.random.beta(alpha, alpha)\n    else:\n        lam = 1.0\n    batch_size = x.size(0)\n    index = torch.randperm(batch_size).to(x.device)\n\n    mixed_x = lam * x + (1 - lam) * x[index, :]\n    mixed_y = lam * y + (1 - lam) * y[index, :]\n    return mixed_x, mixed_y\n\nclass SequenceDataset(Dataset):\n    def __init__(self, X, y=None):\n        \"\"\"\n        X: np.ndarray of shape (n_samples, features, seq_len)\n        y: np.ndarray of shape (n_samples, num_classes) or None for test\n        \"\"\"\n        self.X = torch.from_numpy(X).float()\n        self.y = torch.from_numpy(y).float() if y is not None else None\n\n    def __len__(self):\n        return self.X.size(0)\n\n    def __getitem__(self, idx):\n        if self.y is not None:\n            return self.X[idx], self.y[idx]\n        else:\n            return self.X[idx]\n\n# ----------------------------------------------------------------------\n# 2. Load & preprocess data\n# ----------------------------------------------------------------------\n\nprint(\"Loading datasets...\")\ntrain_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\ntrain_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\ntest_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv\")\ntest_dem_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv\")\nprint(f\"Train rows: {len(train_df)}, Test rows: {len(test_df)}\")\n\n# Encode labels\nlabel_encoder = LabelEncoder()\ntrain_df['gesture'] = label_encoder.fit_transform(train_df['gesture'].astype(str))\ngesture_classes = label_encoder.classes_\nnp.save('gesture_classes.npy', gesture_classes)\n\n# Exclude metadata columns\nexcluded_cols = {\n    'gesture', 'sequence_type', 'behavior', 'orientation',\n    'row_id', 'subject', 'phase',\n    'sequence_id', 'sequence_counter'\n}\nall_feature_cols = [c for c in train_df.columns if c not in excluded_cols]\n\n# Split feature columns into IMU vs. TOF/Thermal\nimu_cols = [c for c in all_feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\ntof_thm_cols = [c for c in all_feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n\n# Reorder so that IMU features come first\nfeature_cols = imu_cols + tof_thm_cols\nimu_dim = len(imu_cols)\ntof_thm_dim = len(tof_thm_cols)\nprint(f\"IMU features: {imu_dim}, TOF/Thermal features: {tof_thm_dim}, Total features: {len(feature_cols)}\")\n\n# Check for missing values\nnan_counts = train_df[feature_cols].isna().sum().sum()\nprint(\"Total NaNs in train features:\", nan_counts)\n\n# Fit StandardScaler on all training data\nprint(\"Fitting StandardScaler on train data...\")\nall_values = train_df[feature_cols].ffill().bfill().fillna(0).values\nscaler = StandardScaler().fit(all_values)\njoblib.dump(scaler, 'global_scaler.pkl')\n\n# Group sequences and build padded arrays\nprint(\"Building sequences...\")\nsequences = train_df.groupby('sequence_id')\nX_list = []\nlengths = []\ny_list = []\n\nfor i, (seq_id, seq) in enumerate(sequences):\n    seq_data = seq[feature_cols].ffill().bfill().fillna(0).values\n    scaled = scaler.transform(seq_data)\n    X_list.append(scaled)\n    lengths.append(scaled.shape[0])\n    y_list.append(seq['gesture'].iloc[0])\n    if i % 500 == 0 and i > 0:\n        print(f\"  Processed {i} sequences...\")\n\n# Determine pad length (90th percentile)\npad_len = int(np.percentile(lengths, 90))\nprint(f\"Pad/truncate all sequences to length {pad_len} (90th percentile).\")\nnp.save(\"sequence_maxlen.npy\", pad_len)\n\n# Pad/truncate sequences\nprint(\"Padding/truncating sequences...\")\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences as keras_pad_sequences\nX = keras_pad_sequences(\n    X_list,\n    maxlen=pad_len,\n    dtype='float32',\n    padding='post',\n    truncating='post'\n)  # shape: (n_samples, pad_len, total_features)\n\ny = np.array(y_list)  # shape: (n_samples,)\n\n# One-hot encode labels for mixup\nnum_classes = len(np.unique(y))\ny_cat = np.eye(num_classes)[y]  # shape: (n_samples, num_classes)\n\n# Split into train/validation\nX_train_np, X_val_np, y_train_np, y_val_np = train_test_split(\n    X, y_cat, test_size=0.2, random_state=42, stratify=y\n)\nprint(\"Train/Val shapes:\", X_train_np.shape, X_val_np.shape, y_train_np.shape, y_val_np.shape)\n\n# Transpose to (n_samples, features, seq_len) for PyTorch\nX_train_np = np.transpose(X_train_np, (0, 2, 1))\nX_val_np = np.transpose(X_val_np, (0, 2, 1))\n\n# Compute class weights on integer labels\nlabels_train = np.argmax(y_train_np, axis=1)\nclass_weights_values = compute_class_weight('balanced',\n                                            classes=np.unique(labels_train),\n                                            y=labels_train)\nclass_weights = torch.tensor(class_weights_values, dtype=torch.float)\n\n# ----------------------------------------------------------------------\n# 3. Dataset and DataLoader\n# ----------------------------------------------------------------------\n\nbatch_size = 128\n\ntrain_dataset = SequenceDataset(X_train_np, y_train_np)\nval_dataset = SequenceDataset(X_val_np, y_val_np)\n\ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, drop_last=True)\nval_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)\n\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-08-21T12:35:59.491543Z","iopub.execute_input":"2025-08-21T12:35:59.491837Z","iopub.status.idle":"2025-08-21T12:37:14.320935Z","shell.execute_reply.started":"2025-08-21T12:35:59.491814Z","shell.execute_reply":"2025-08-21T12:37:14.320127Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------------------------------------------------------\n# 4. Model definition (Two-branch: SE-CNN + BiRNN + Attention Hybrid Architecture.)\n# ----------------------------------------------------------------------\n\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super(SEBlock, self).__init__()\n        self.fc1 = nn.Linear(channels, channels // reduction, bias=True)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc2 = nn.Linear(channels // reduction, channels, bias=True)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        # x: (batch, channels, seq_len)\n        # Squeeze: global average pooling over time dimension\n        se = x.mean(dim=2)                      # (batch, channels)\n        se = self.relu(self.fc1(se))            # (batch, channels//reduction)\n        se = self.sigmoid(self.fc2(se))         # (batch, channels)\n        se = se.unsqueeze(2)                    # (batch, channels, 1)\n        return x * se                           # scale channels\n\nclass ResidualSEBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size=3, pool_size=2, dropout_rate=0.3):\n        super(ResidualSEBlock, self).__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n        self.relu = nn.ReLU(inplace=True)\n\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n\n        self.se = SEBlock(out_channels, reduction=8)\n\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, kernel_size=1, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n        else:\n            self.shortcut = nn.Identity()\n\n        self.pool = nn.MaxPool1d(kernel_size=pool_size)\n        self.dropout = nn.Dropout(dropout_rate)\n\n    def forward(self, x):\n        # x: (batch, in_channels, seq_len)\n        shortcut = self.shortcut(x)                                 # (batch, out_channels, seq_len)\n        out = self.conv1(x)                                          # (batch, out_channels, seq_len)\n        out = self.bn1(out)\n        out = self.relu(out)\n\n        out = self.conv2(out)                                        # (batch, out_channels, seq_len)\n        out = self.bn2(out)\n\n        out = self.se(out)                                           # SE scaling\n\n        out = out + shortcut                                         # skip connection\n        out = self.relu(out)\n\n        out = self.pool(out)                                         # (batch, out_channels, seq_len//pool_size)\n        out = self.dropout(out)\n        return out\n\nclass Attention(nn.Module):\n    def __init__(self, input_dim):\n        super(Attention, self).__init__()\n        self.score_fc = nn.Linear(input_dim, 1)\n\n    def forward(self, x):\n        # x: (batch, seq_len, features)\n        scores = torch.tanh(self.score_fc(x))            # (batch, seq_len, 1)\n        scores = scores.squeeze(2)                       # (batch, seq_len)\n        weights = F.softmax(scores, dim=1)               # (batch, seq_len)\n        weights = weights.unsqueeze(2)                   # (batch, seq_len, 1)\n        weighted = x * weights                           # (batch, seq_len, features)\n        context = weighted.sum(dim=1)                    # (batch, features)\n        return context\n\nclass TwoBranchHARModel(nn.Module):\n    def __init__(self, total_features, imu_dim, tof_thm_dim, pad_len, num_classes, wd=1e-4):\n        super(TwoBranchHARModel, self).__init__()\n        # IMU branch\n        self.resblock1 = ResidualSEBlock(imu_dim, 64, kernel_size=3, pool_size=2, dropout_rate=0.3)\n        self.resblock2 = ResidualSEBlock(64, 128, kernel_size=5, pool_size=2, dropout_rate=0.3)\n        self.se_ttf = SEBlock(128, reduction=8)\n\n\n        # TOF/Thermal branch\n        self.conv1_ttf = nn.Conv1d(tof_thm_dim, 64, kernel_size=3, padding=1, bias=False)\n        self.bn1_ttf = nn.BatchNorm1d(64)\n        self.pool1_ttf = nn.MaxPool1d(kernel_size=2)\n        self.drop1_ttf = nn.Dropout(0.3)\n\n        self.conv2_ttf = nn.Conv1d(64, 128, kernel_size=3, padding=1, bias=False)\n        self.bn2_ttf = nn.BatchNorm1d(128)\n        self.pool2_ttf = nn.MaxPool1d(kernel_size=2)\n        self.drop2_ttf = nn.Dropout(0.3)\n\n        # After pooling twice, seq_len reduced by factor of 4\n        reduced_len = pad_len // 4\n        merged_channels = 128 + 128  # from IMU and TTF\n\n        # BiLSTM --> thay bằng GRU (self.ltms =nn.LTMS())\n        self.rnn = nn.GRU(\n            input_size=256, #merged_channels,\n            hidden_size=256,\n            num_layers=2,\n            batch_first=True,\n            bidirectional=True\n        )\n        self.drop_rnn = nn.Dropout(0.4)\n\n        # Attention\n        self.attention = Attention(input_dim=512)\n\n        # Dense head\n        self.fc1 = nn.Linear(512, 256, bias=False)\n\n        self.bn_fc1 = nn.BatchNorm1d(256)\n        self.drop_fc1 = nn.Dropout(0.5)\n\n        self.fc2 = nn.Linear(256, 128, bias=False)\n        self.bn_fc2 = nn.BatchNorm1d(128)\n        self.drop_fc2 = nn.Dropout(0.3)\n\n        self.out = nn.Linear(128, num_classes)\n\n    def forward(self, x):\n        # x: (batch, total_features, seq_len)\n        x_imu = x[:, :imu_dim, :]           # (batch, imu_dim, seq_len)\n        x_ttf = x[:, imu_dim:, :]           # (batch, tof_thm_dim, seq_len)\n\n        # IMU branch\n        b1 = self.resblock1(x_imu)          # (batch, 64, seq_len/2)\n        b1 = self.resblock2(b1)             # (batch, 128, seq_len/4)\n        #b1 = self.resblock3(b1)  # NEW\n\n        # TTF branch\n        b2 = self.conv1_ttf(x_ttf)          # (batch, 64, seq_len)\n        b2 = self.bn1_ttf(b2)\n        b2 = F.relu(b2)\n        b2 = self.pool1_ttf(b2)             # (batch, 64, seq_len/2)\n        b2 = self.drop1_ttf(b2)\n\n        b2 = self.conv2_ttf(b2)             # (batch, 128, seq_len/2)\n        b2 = self.bn2_ttf(b2)\n        b2 = F.relu(b2)\n        b2 = self.pool2_ttf(b2)             # (batch, 128, seq_len/4)\n        b2 = self.drop2_ttf(b2)\n        b2 = self.se_ttf(b2)  # NEW\n\n        # Concatenate branches along channel dimension\n        merged = torch.cat([b1, b2], dim=1)  # (batch, 256, seq_len/4)\n\n        # Prepare for LSTM: (batch, seq_len/4, 256)\n        merged = merged.permute(0, 2, 1)\n\n        rnn_out, _ = self.rnn(merged)\n        rnn_out = self.drop_rnn(rnn_out)\n        context = self.attention(rnn_out)\n\n\n        # Dense head\n        x = self.fc1(context)                 # (batch, 256)\n        x = self.bn_fc1(x)\n        x = F.relu(x)\n        x = self.drop_fc1(x)\n\n        x = self.fc2(x)                       # (batch, 128)\n        x = self.bn_fc2(x)\n        x = F.relu(x)\n        x = self.drop_fc2(x)\n\n        out = self.out(x)                     # (batch, num_classes)\n        return out\n\n# Device setup\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# Instantiate model\ninput_shape = (len(feature_cols), pad_len)\nmodel = TwoBranchHARModel(\n    total_features=len(feature_cols),\n    imu_dim=imu_dim,\n    tof_thm_dim=tof_thm_dim,\n    pad_len=pad_len,\n    num_classes=num_classes,\n    wd=1e-4\n).to(device)\n\n# Count parameters\ndef count_parameters(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\nprint(f\"Model parameters: {count_parameters(model)}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.321789Z","iopub.execute_input":"2025-08-21T12:37:14.322337Z","iopub.status.idle":"2025-08-21T12:37:14.404018Z","shell.execute_reply.started":"2025-08-21T12:37:14.322312Z","shell.execute_reply":"2025-08-21T12:37:14.403127Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"# Device setup\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Using device:\", device)\n\n# Instantiate model\ninput_shape = (len(feature_cols), pad_len)\nmodel = TwoBranchHARModel(\n    total_features=len(feature_cols),\n    imu_dim=imu_dim,\n    tof_thm_dim=tof_thm_dim,\n    pad_len=pad_len,\n    num_classes=num_classes,\n    #wd=1e-4\n).to(device)\n\n# Count parameters\ndef count_parameters(model):\n    return sum(p.numel() for p in model.parameters() if p.requires_grad)\n\nprint(f\"Model parameters: {count_parameters(model)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.405028Z","iopub.execute_input":"2025-08-21T12:37:14.405246Z","iopub.status.idle":"2025-08-21T12:37:14.550394Z","shell.execute_reply.started":"2025-08-21T12:37:14.405228Z","shell.execute_reply":"2025-08-21T12:37:14.549704Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"code","source":"# ==== TRAINING BLOCK (guarded for Kaggle) ====\n# Yêu cầu: các biến đã tồn tại từ trước: model, device, train_loader, val_loader, mixup_data\n# (nếu bạn đặt tên khác, nhớ đổi lại cho khớp)\n\nimport os\nIS_RERUN = bool(os.getenv('KAGGLE_IS_COMPETITION_RERUN'))  # True khi SUBMIT (rerun ẩn)\nprint(f\"[TRAIN GUARD] TRAIN={TRAIN} | IS_RERUN={IS_RERUN}\")\n\nif TRAIN and not IS_RERUN:\n    # Optimizer\n    import numpy as np\n    import torch\n    import torch.nn.functional as F\n    import torch.optim as optim\n    from sklearn.metrics import classification_report, f1_score\n\n    lr = 1e-4\n    optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=1e-3)\n\n    # Scheduler: Giảm LR khi val_loss không giảm sau 5 epoch\n    scheduler = optim.lr_scheduler.ReduceLROnPlateau(\n        optimizer, mode='min', factor=0.5, patience=8, verbose=True, min_lr=1e-6\n    )\n\n    # Loss: soft_cross_entropy (dùng với mixup)\n    def soft_cross_entropy(pred, soft_targets):\n        log_probs = F.log_softmax(pred, dim=1)\n        loss = -torch.sum(soft_targets * log_probs, dim=1).mean()\n        return loss\n\n    # Early stopping\n    patience = 20\n    best_val_loss = np.inf\n    epochs_no_improve = 0\n    num_epochs = 250\n    best_model_state = None\n    use_mixup = True  # có thể bật/tắt Mixup ở đây\n\n    mixup_alpha = 0.5  # Tăng nhẹ alpha mixup\n\n    print(\"Starting training...\")\n    for epoch in range(1, num_epochs + 1):\n        model.train()\n        train_loss = 0.0\n        for batch_x, batch_y in train_loader:\n            batch_x = batch_x.to(device)\n            batch_y = batch_y.to(device)\n\n            if use_mixup:\n                mixed_x, mixed_y = mixup_data(batch_x, batch_y, alpha=mixup_alpha)\n            else:\n                mixed_x, mixed_y = batch_x, batch_y\n\n            optimizer.zero_grad()\n            outputs = model(mixed_x)\n            loss = soft_cross_entropy(outputs, mixed_y)\n            loss.backward()\n            optimizer.step()\n\n            train_loss += loss.item() * batch_x.size(0)\n\n        train_loss /= len(train_loader.dataset)\n\n        # Validation\n        model.eval()\n        val_loss = 0.0\n        correct = 0\n        total = 0\n        with torch.no_grad():\n            for batch_x, batch_y in val_loader:\n                batch_x = batch_x.to(device)\n                batch_y = batch_y.to(device)\n                outputs = model(batch_x)\n                loss = soft_cross_entropy(outputs, batch_y)\n                val_loss += loss.item() * batch_x.size(0)\n\n                # Accuracy\n                preds = outputs.argmax(dim=1)\n                targets = batch_y.argmax(dim=1)\n                correct += (preds == targets).sum().item()\n                total += targets.size(0)\n\n        val_loss /= len(val_loader.dataset)\n        val_acc = correct / total * 100\n\n        print(f\"Epoch {epoch:02d}: Train Loss = {train_loss:.4f}, Val Loss = {val_loss:.4f}, Val Acc = {val_acc:.2f}%\")\n\n        scheduler.step(val_loss)\n\n        # Early stopping\n        if val_loss < best_val_loss:\n            best_val_loss = val_loss\n            epochs_no_improve = 0\n            best_model_state = model.state_dict()\n        else:\n            epochs_no_improve += 1\n            if epochs_no_improve >= patience:\n                print(f\"Early stopping at epoch {epoch}. Restoring best model.\")\n                model.load_state_dict(best_model_state)\n                break\n\n    # Save model\n    torch.save(best_model_state, \"gesture_two_branch_mixup_pytorch.pth\")\n    print(\"Training complete and model saved.\")\nelse:\n    print(\"Skip training (either TRAIN=False or SUBMIT rerun).\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.551519Z","iopub.execute_input":"2025-08-21T12:37:14.551859Z","iopub.status.idle":"2025-08-21T12:37:14.565735Z","shell.execute_reply.started":"2025-08-21T12:37:14.551829Z","shell.execute_reply":"2025-08-21T12:37:14.564853Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==== VALIDATION METRICS (guarded) ====\nimport os, torch\nfrom sklearn.metrics import f1_score, classification_report\n\nIS_RERUN = bool(os.getenv('KAGGLE_IS_COMPETITION_RERUN'))  # True khi SUBMIT\n\nif TRAIN and not IS_RERUN and 'best_model_state' in globals() and best_model_state is not None:\n    # Load best checkpoint vào model để đánh giá\n    model.load_state_dict(best_model_state)\n    model.eval()\n\n    all_preds, all_targets = [], []\n    with torch.no_grad():\n        for batch_x, batch_y in val_loader:\n            batch_x = batch_x.to(device)\n            batch_y = batch_y.to(device)\n            outputs = model(batch_x)\n            preds = outputs.argmax(dim=1).cpu().numpy()\n            targets = batch_y.argmax(dim=1).cpu().numpy()\n            all_preds.extend(preds)\n            all_targets.extend(targets)\n\n    # F1-score\n    val_f1_macro = f1_score(all_targets, all_preds, average='macro')\n    val_f1_weighted = f1_score(all_targets, all_preds, average='weighted')\n\n    print(f\"\\n🔍 Final Validation Results on Best Model:\")\n    print(f\"Macro F1-score:    {val_f1_macro:.4f}\")\n    print(f\"Weighted F1-score: {val_f1_weighted:.4f}\")\n\n    # In classification report (nếu có gesture_classes)\n    try:\n        print(classification_report(all_targets, all_preds, target_names=gesture_classes))\n    except Exception:\n        print(classification_report(all_targets, all_preds))\nelse:\n    print(\"Skip validation metrics (either TRAIN=False, SUBMIT rerun, hoặc thiếu best_model_state).\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.566521Z","iopub.execute_input":"2025-08-21T12:37:14.566819Z","iopub.status.idle":"2025-08-21T12:37:14.587312Z","shell.execute_reply.started":"2025-08-21T12:37:14.566790Z","shell.execute_reply":"2025-08-21T12:37:14.586461Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Luôn import ở ô launcher này (đừng để trong ô TRAIN)\nimport kaggle_evaluation.cmi_inference_server\nfrom pathlib import Path\nimport os\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.588185Z","iopub.execute_input":"2025-08-21T12:37:14.588783Z","iopub.status.idle":"2025-08-21T12:37:14.609129Z","shell.execute_reply.started":"2025-08-21T12:37:14.588760Z","shell.execute_reply":"2025-08-21T12:37:14.608215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------------------------------------------------------\n# Inference-only for Kaggle evaluation (no training dependency)\n# ----------------------------------------------------------------------\nimport os\nfrom pathlib import Path\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport joblib\nimport torch\nimport torch.nn as nn\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n\n\ndef _preprocess_sequence(df_seq: pd.DataFrame) -> torch.Tensor:\n    \"\"\"ffill/bfill -> scale -> pad/truncate -> (1, features, seq_len) float32 tensor.\"\"\"\n    data = df_seq[_feature_cols].ffill().bfill().fillna(0).values  # (T, F)\n    data = scaler.transform(data)\n\n    T, F = data.shape\n    if T >= pad_len:\n        data = data[:pad_len, :]\n    else:\n        pad = np.zeros((pad_len - T, F), dtype=data.dtype)\n        data = np.vstack([data, pad])\n\n    # (1, features, seq_len)\n    data = data.T[None, :, :].astype(\"float32\")\n    return torch.from_numpy(data)\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Kaggle evaluation API will call this for each sequence.\n    Returns: predicted gesture string\n    \"\"\"\n    df_seq = sequence.to_pandas()\n    _ensure_model(list(df_seq.columns))\n    with torch.no_grad():\n        x = _preprocess_sequence(df_seq).to(device)\n        logits = _model(x)\n        pred_idx = int(torch.argmax(logits, dim=1).item())\n    return str(gesture_classes[pred_idx])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.611475Z","iopub.execute_input":"2025-08-21T12:37:14.611760Z","iopub.status.idle":"2025-08-21T12:37:14.628655Z","shell.execute_reply.started":"2025-08-21T12:37:14.611736Z","shell.execute_reply":"2025-08-21T12:37:14.627835Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------------------------------------------------------\n# 8. Launch inference server / run local gateway  (robust last cell)\n# ----------------------------------------------------------------------\nfrom pathlib import Path\nimport os\n\nprint(\"KAGGLE_IS_COMPETITION_RERUN =\", os.getenv('KAGGLE_IS_COMPETITION_RERUN'))\n\n# ==== Safe wrapper: always provide a working predict entry ====\n# Load gesture_classes if missing (for fallback)\ntry:\n    gesture_classes\nexcept NameError:\n    import numpy as np\n    from pathlib import Path\n    PRETRAINED_DIR = Path(os.getenv(\"PRETRAINED_DIR\", \"/kaggle/input/two-branch-pretrained\"))\n    gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n\ndef _predict_entry(sequence, demographics):\n    \"\"\"\n    Robust entry point for server and manual writer.\n    - Use your real predict() if available.\n    - If predict/_ensure_model missing, fall back to constant class to keep pipeline alive.\n    \"\"\"\n    try:\n        return predict(sequence, demographics)  # <-- real one (from your Inference cell)\n    except NameError as e:\n        # Fallback: constant class (very low score, but produces a valid file)\n        return str(gesture_classes[0])\n\n# 1) Try local gateway (only when Create Version)\ntry:\n    import kaggle_evaluation.cmi_inference_server as cmis\n    inference_server = cmis.CMIInferenceServer(_predict_entry)\n    if not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n        # Create Version path: ask gateway to write submission.parquet\n        inference_server.run_local_gateway(\n            data_paths=(\n                '/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv',\n                '/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv',\n            )\n        )\nexcept Exception as e:\n    print(\"Gateway run failed -> will fallback to manual writer:\", e)\n\n# 2) Fallback: write submission.parquet manually if still missing\nfrom pathlib import Path\nif not Path('submission.parquet').exists():\n    print(\"submission.parquet missing -> writing manually...\")\n    import polars as pl\n    import pandas as pd\n\n    test = pl.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv')\n    demo = pl.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/test_demographics.csv')\n\n    seq_ids = test.select('sequence_id').unique().to_series().to_list()\n    preds = []\n    for sid in seq_ids:\n        seq_df = test.filter(pl.col('sequence_id') == sid)\n        label = _predict_entry(seq_df, demo)  # <-- use robust wrapper\n        preds.append((sid, label))\n\n    sub = pd.DataFrame(preds, columns=['sequence_id', 'gesture'])\n    sub.to_parquet('submission.parquet', index=False)\n    print(\"Wrote submission.parquet with\", len(sub), \"rows\")\n\nassert Path('submission.parquet').exists(), \"submission.parquet still missing!\"\nprint(\"OK ✓ Found submission.parquet\")\n","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-08-21T12:37:14.629532Z","iopub.execute_input":"2025-08-21T12:37:14.629869Z","iopub.status.idle":"2025-08-21T12:37:15.509836Z","shell.execute_reply.started":"2025-08-21T12:37:14.629839Z","shell.execute_reply":"2025-08-21T12:37:15.508936Z"},"trusted":true},"outputs":[],"execution_count":null}]}