{"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":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## 🔴 **Import**","metadata":{}},{"cell_type":"code","source":"import os\nimport random\nfrom tqdm.auto import tqdm\nfrom collections import Counter\nfrom dataclasses import dataclass\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nsns.set()\n\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.base import BaseEstimator, TransformerMixin\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\n\nimport torch\nimport torch.nn as nn\nfrom torch.nn import functional as F\nfrom torch.nn.utils.rnn import pad_sequence\nfrom torch.utils.data import Dataset, DataLoader\n\nfrom torchmetrics.aggregation import MeanMetric\n\nfrom cmi_2025_metric_copy_for_import import CompetitionMetric\nimport kaggle_evaluation.cmi_inference_server","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:39.283986Z","iopub.execute_input":"2025-08-19T02:00:39.284214Z","iopub.status.idle":"2025-08-19T02:00:53.725037Z","shell.execute_reply.started":"2025-08-19T02:00:39.284191Z","shell.execute_reply":"2025-08-19T02:00:53.724332Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 **Functions**","metadata":{}},{"cell_type":"markdown","source":"### 🟠 **Utils**","metadata":{}},{"cell_type":"code","source":"def calc_parameters(model, scale=1e6):\n    return sum([p.numel() for p in model.parameters() if p.requires_grad]) / scale","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:53.725891Z","iopub.execute_input":"2025-08-19T02:00:53.726318Z","iopub.status.idle":"2025-08-19T02:00:53.730124Z","shell.execute_reply.started":"2025-08-19T02:00:53.726298Z","shell.execute_reply":"2025-08-19T02:00:53.729380Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🟠 **Train**","metadata":{}},{"cell_type":"code","source":"def train_one_epoch(model, train_loader, loss_fn, optimizer, scheduler=None, epoch=None):\n    model.train()  # Set the model to training mode\n    loss_train = MeanMetric()  # Initialize a metric to track average training loss\n    all_preds, all_targets = [], []  # Lists to collect predictions and targets for accuracy\n\n    # Create a progress bar for the training loop\n    with tqdm(train_loader, unit='batch') as tepoch:\n        for inputs, targets in tepoch:\n            if epoch is not None:\n                tepoch.set_description(f'Epoch {epoch}')  # Show current epoch in progress bar\n\n            inputs = inputs.to(cfg.device)   # Move inputs to device (CPU/GPU)\n            targets = targets.to(cfg.device) # Move targets to device\n\n            logits = model(inputs)  # Forward pass to get raw model outputs\n\n            loss = loss_fn(logits, targets)  # Compute loss between predictions and ground truth\n\n            loss.backward()  # Backpropagation to compute gradients\n            \n            optimizer.step()  # Update model parameters\n            optimizer.zero_grad()  # Clear gradients for the next iteration\n\n            if scheduler is not None:\n                scheduler.step()  # Update learning rate if scheduler is provided\n\n            loss_train.update(loss.item(), inputs.shape[0])  # Update running average of loss\n\n            # Display the current average loss in the progress bar\n            tepoch.set_postfix(loss=loss_train.compute().item())\n\n            # Save predictions and ground truth labels for accuracy calculation\n            all_preds.extend(logits.argmax(dim=1).cpu().numpy())\n            all_targets.extend(targets.cpu().numpy())\n\n    # Compute hierarchical F1 score using the competition's custom metric\n    metric_train = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': label_encoder.classes_[all_targets]}),\n        pd.DataFrame({'gesture': label_encoder.classes_[all_preds]})\n    )\n\n    return loss_train.compute().item(), metric_train  # Return average loss and F1 for the epoch","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:53.732064Z","iopub.execute_input":"2025-08-19T02:00:53.732289Z","iopub.status.idle":"2025-08-19T02:00:53.752618Z","shell.execute_reply.started":"2025-08-19T02:00:53.732274Z","shell.execute_reply":"2025-08-19T02:00:53.751984Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🟠 **Evaluate**","metadata":{}},{"cell_type":"code","source":"def evaluate(model, valid_loader):\n    model.eval()  # Set the model to evaluation mode (disables dropout, etc.)\n    loss_eval = MeanMetric()  # Initialize a metric to track average validation loss\n    all_preds, all_targets = [], []  # Lists to collect predictions and targets for accuracy\n\n    with torch.inference_mode():  # Disable gradient computation for faster inference and less memory usage\n        for inputs, targets in valid_loader:\n            inputs = inputs.to(cfg.device)   # Move inputs to device (CPU/GPU)\n            targets = targets.to(cfg.device) # Move targets to device\n\n            logits = model(inputs)  # Forward pass to get model outputs\n\n            loss = F.cross_entropy(logits, targets)  # Compute cross-entropy loss\n            loss_eval.update(loss.item(), inputs.shape[0])  # Update running average of loss\n\n            # Save predicted classes and ground truth labels\n            all_preds.extend(logits.argmax(dim=1).cpu().numpy())\n            all_targets.extend(targets.cpu().numpy())\n\n    # Compute hierarchical F1 score using the competition's custom metric\n    metric_eval = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': label_encoder.classes_[all_targets]}),\n        pd.DataFrame({'gesture': label_encoder.classes_[all_preds]})\n    )\n\n    return loss_eval.compute().item(), metric_eval  # Return average validation loss and F1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:53.753306Z","iopub.execute_input":"2025-08-19T02:00:53.753534Z","iopub.status.idle":"2025-08-19T02:00:53.771998Z","shell.execute_reply.started":"2025-08-19T02:00:53.753513Z","shell.execute_reply":"2025-08-19T02:00:53.771320Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 **Args**","metadata":{}},{"cell_type":"code","source":"@dataclass\nclass Config:\n    data_dir: str\n    device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n    input_channels: int = 7\n    num_classes: int = 18","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:53.772746Z","iopub.execute_input":"2025-08-19T02:00:53.772951Z","iopub.status.idle":"2025-08-19T02:00:53.867680Z","shell.execute_reply.started":"2025-08-19T02:00:53.772928Z","shell.execute_reply":"2025-08-19T02:00:53.866637Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"cfg = Config(\n    data_dir = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\n)\n\ncfg","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:53.868507Z","iopub.execute_input":"2025-08-19T02:00:53.868769Z","iopub.status.idle":"2025-08-19T02:00:53.887731Z","shell.execute_reply.started":"2025-08-19T02:00:53.868741Z","shell.execute_reply":"2025-08-19T02:00:53.887178Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 **Dataset**","metadata":{}},{"cell_type":"markdown","source":"### 🟠 **Load Data**","metadata":{}},{"cell_type":"code","source":"df_train = pd.read_csv(f\"{cfg.data_dir}/train.csv\")\ndf_train.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:00:53.888549Z","iopub.execute_input":"2025-08-19T02:00:53.888841Z","iopub.status.idle":"2025-08-19T02:01:22.690136Z","shell.execute_reply.started":"2025-08-19T02:00:53.888807Z","shell.execute_reply":"2025-08-19T02:01:22.689359Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🟠 **Preprocess**","metadata":{}},{"cell_type":"code","source":"class SensorImputer(BaseEstimator, TransformerMixin):\n    def __init__(self, method='median', thm_threshold=10.0, selected_sensors=None):\n        \"\"\"\n        method: 'mean' or 'median'\n        thm_threshold: threshold below which thermal values are considered invalid\n        selected_sensors: list of sensor types to include, e.g., ['acc', 'rot']\n        \"\"\"\n        self.method = method\n        self.thm_threshold = thm_threshold\n        self.selected_sensors = selected_sensors  # ['acc', 'rot', 'thm', 'tof']\n        self.acc_columns = []\n        self.rot_columns = []\n        self.thm_columns = []\n        self.tof_columns = []\n        self.column_stats = {}\n\n    def fit(self, X, y=None):\n        # Detect columns\n        self.acc_columns = [col for col in X.columns if col.startswith(\"acc_\")]\n        self.rot_columns = [col for col in X.columns if col.startswith(\"rot_\")]\n        self.thm_columns = [col for col in X.columns if col.startswith(\"thm_\")]\n        self.tof_columns = [col for col in X.columns if col.startswith(\"tof_\")]\n\n        # Determine which columns to keep based on selected_sensors\n        all_groups = {\n            'acc': self.acc_columns,\n            'rot': self.rot_columns,\n            'thm': self.thm_columns,\n            'tof': self.tof_columns\n        }\n\n        if self.selected_sensors is not None:\n            self.sensor_columns = []\n            for sensor in self.selected_sensors:\n                self.sensor_columns += all_groups.get(sensor, [])\n        else:\n            self.sensor_columns = sum(all_groups.values(), [])\n\n        # Compute statistics for selected sensors\n        for col in self.sensor_columns:\n            values = X[col].copy()\n            if col in self.thm_columns:\n                values[values < self.thm_threshold] = np.nan\n            if col in self.tof_columns:\n                values = values[values != -1]\n            if self.method == 'mean':\n                self.column_stats[col] = values.mean()\n            elif self.method == 'median':\n                self.column_stats[col] = values.median()\n            else:\n                raise ValueError(\"method must be either 'mean' or 'median'\")\n\n        return self\n\n    def transform(self, X):\n        X = X.copy()\n\n        # Preprocessing for thermal\n        if 'thm' in self.selected_sensors or self.selected_sensors is None:\n            for col in self.thm_columns:\n                if col in self.sensor_columns:\n                    X.loc[X[col] < self.thm_threshold, col] = np.nan\n\n        for col in self.sensor_columns:\n            if col in self.tof_columns:\n                X[col] = X[col].replace(-1, np.nan)\n            X[col] = X[col].fillna(self.column_stats[col])\n\n        return X[self.sensor_columns]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:22.691101Z","iopub.execute_input":"2025-08-19T02:01:22.691466Z","iopub.status.idle":"2025-08-19T02:01:22.700698Z","shell.execute_reply.started":"2025-08-19T02:01:22.691439Z","shell.execute_reply":"2025-08-19T02:01:22.699954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build the full pipeline\npipeline = Pipeline([\n    ('imputer', SensorImputer(method='median', thm_threshold=15.0, selected_sensors=['acc', 'rot'])),\n    ('scaler', StandardScaler()),\n])\npipeline\n\n# Apply pipeline to full data\nX_processed = pipeline.fit_transform(df_train)\nX_processed.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:22.702976Z","iopub.execute_input":"2025-08-19T02:01:22.703254Z","iopub.status.idle":"2025-08-19T02:01:23.477056Z","shell.execute_reply.started":"2025-08-19T02:01:22.703227Z","shell.execute_reply":"2025-08-19T02:01:23.476286Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Add back sequence_id so we can group\nfeature_columns = pipeline['imputer'].sensor_columns\ndf_processed = pd.DataFrame(X_processed, columns=feature_columns)\ndf_processed[\"sequence_id\"] = df_train[\"sequence_id\"].values\ndf_processed['gesture'] = df_train['gesture'].values\n\ndf_processed","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:23.477814Z","iopub.execute_input":"2025-08-19T02:01:23.478021Z","iopub.status.idle":"2025-08-19T02:01:23.509459Z","shell.execute_reply.started":"2025-08-19T02:01:23.478004Z","shell.execute_reply":"2025-08-19T02:01:23.508782Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Group by sequence\ngrouped = df_processed.groupby('sequence_id')\ntotal_seqs = grouped.ngroups\n\n# Build tensors\nX = []\nfor seq_id, seq in tqdm(grouped, total=total_seqs, desc=\"Building sequences\"):\n    seq = torch.tensor(seq[feature_columns].values, dtype=torch.float32)\n    X.append(seq)\n\nprint(len(X), X[0].shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:23.510133Z","iopub.execute_input":"2025-08-19T02:01:23.510422Z","iopub.status.idle":"2025-08-19T02:01:26.686808Z","shell.execute_reply.started":"2025-08-19T02:01:23.510400Z","shell.execute_reply":"2025-08-19T02:01:26.686123Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🟠 **Build Labels**","metadata":{}},{"cell_type":"code","source":"label_encoder = LabelEncoder()\ndf_processed['gesture'] = label_encoder.fit_transform(df_processed['gesture'])\n\n# Create sequence-level labels by aggregating gesture per sequence\nlabels = (\n    df_processed.groupby(\"sequence_id\")[\"gesture\"]\n    .agg(lambda x: x.mode()[0])  # Use mode in case of repeated labels\n)\n\n# Convert the label Series to a PyTorch LongTensor\ny = torch.LongTensor(labels.values)\n\nprint(y, y.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:26.687777Z","iopub.execute_input":"2025-08-19T02:01:26.688030Z","iopub.status.idle":"2025-08-19T02:01:27.514485Z","shell.execute_reply.started":"2025-08-19T02:01:26.688012Z","shell.execute_reply":"2025-08-19T02:01:27.513673Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🟠 **Build Custom Dataset**","metadata":{}},{"cell_type":"code","source":"class CMIDataset(Dataset):\n    \"\"\"IMU dataset with optional preprocessing and on‑the‑fly augmentation.\n    \"\"\"\n\n    def __init__(self, seqs, labels=None, transform=None):\n        self.seqs = seqs\n        self.labels = labels\n        self.transform = transform\n\n    def __getitem__(self, idx):\n        x = self.seqs[idx]\n\n        # Transform\n        if self.transform is not None:\n            x = self.transform(x)\n        \n        if self.labels is not None:\n            y = self.labels[idx]\n            return x, y\n        \n        return x\n\n    def __len__(self):\n        return len(self.seqs)\n\n    def __repr__(self):\n        return \"CMIDataset\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:27.515228Z","iopub.execute_input":"2025-08-19T02:01:27.515488Z","iopub.status.idle":"2025-08-19T02:01:27.520492Z","shell.execute_reply.started":"2025-08-19T02:01:27.515470Z","shell.execute_reply":"2025-08-19T02:01:27.519697Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def pad_collate(batch):\n    \"\"\"Pad variable‑length sequences to the max length in batch.\"\"\"\n    if isinstance(batch[0], tuple):\n        xs, ys = zip(*batch)\n    else:\n        xs, ys = batch, None\n\n    xs_padded = pad_sequence(xs, batch_first=True).transpose(1, 2)  # (B, C, T_max)\n        \n    if ys is None:\n        return xs_padded\n    \n    return xs_padded, torch.as_tensor(ys)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:27.521321Z","iopub.execute_input":"2025-08-19T02:01:27.521561Z","iopub.status.idle":"2025-08-19T02:01:27.537555Z","shell.execute_reply.started":"2025-08-19T02:01:27.521528Z","shell.execute_reply":"2025-08-19T02:01:27.536972Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 **Model**","metadata":{}},{"cell_type":"code","source":"class ConvBlock1D(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size=3, pool_kernel=2):\n        super().__init__()\n        self.conv = nn.Conv1d(in_channels, out_channels, kernel_size=kernel_size, padding=1)\n        self.bn = nn.BatchNorm1d(out_channels)\n        self.act = nn.ReLU()\n        self.pool = nn.MaxPool1d(kernel_size=pool_kernel)\n\n    def forward(self, x):\n        x = self.conv(x)\n        x = self.bn(x)\n        x = self.act(x)\n        x = self.pool(x)\n        return x\n\n\nclass TimeSeriesCNN(nn.Module):\n    def __init__(self, input_channels=1, num_classes=1, conv_channels=[32, 64, 128], dropout=0.3):\n        super().__init__()\n        \n        self.feature_extractor = nn.Sequential()\n        in_ch = input_channels\n        for i, out_ch in enumerate(conv_channels):\n            self.feature_extractor.add_module(f'convblock_{i}', ConvBlock1D(in_ch, out_ch))\n            in_ch = out_ch  # update for next layer\n        \n        self.global_pool = nn.AdaptiveAvgPool1d(1)\n\n        self.dropout = nn.Dropout(dropout)\n        self.fc1 = nn.Linear(conv_channels[-1], 64)\n        self.fc2 = nn.Linear(64, num_classes)\n\n    def forward(self, x):\n        # x shape: (batch_size, channels, time_steps)\n        x = self.feature_extractor(x)\n        x = self.global_pool(x)      # shape: (B, C, 1)\n        x = x.squeeze(-1)            # shape: (B, C)\n\n        x = self.dropout(x)\n        x = F.relu(self.fc1(x))\n        x = self.fc2(x)\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:27.538182Z","iopub.execute_input":"2025-08-19T02:01:27.538404Z","iopub.status.idle":"2025-08-19T02:01:27.553486Z","shell.execute_reply.started":"2025-08-19T02:01:27.538389Z","shell.execute_reply":"2025-08-19T02:01:27.552743Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 **Train**","metadata":{}},{"cell_type":"code","source":"# Split the sequence list and labels together\nX_train, X_valid, y_train, y_valid = train_test_split(\n    X, y, test_size=0.2, random_state=42, stratify=labels\n)\n\ntrain_set = CMIDataset(X_train, y_train, transform=None)\n\nvalid_set = CMIDataset(X_valid, y_valid, transform=None)\n\ntrain_loader = DataLoader(train_set, batch_size=16, shuffle=True, collate_fn=pad_collate)\n\nvalid_loader = DataLoader(valid_set, batch_size=16, shuffle=False, collate_fn=pad_collate)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:27.554184Z","iopub.execute_input":"2025-08-19T02:01:27.554430Z","iopub.status.idle":"2025-08-19T02:01:27.582505Z","shell.execute_reply.started":"2025-08-19T02:01:27.554415Z","shell.execute_reply":"2025-08-19T02:01:27.581694Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model = TimeSeriesCNN(cfg.input_channels, cfg.num_classes, conv_channels=[32, 64, 128]).to(cfg.device)\ncalc_parameters(model, scale=1e3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:27.583357Z","iopub.execute_input":"2025-08-19T02:01:27.583564Z","iopub.status.idle":"2025-08-19T02:01:27.760420Z","shell.execute_reply.started":"2025-08-19T02:01:27.583548Z","shell.execute_reply":"2025-08-19T02:01:27.759689Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# loss check\n\nx_batch, y_batch = next(iter(train_loader))\nprint(x_batch.shape, y_batch.shape, end=\"\\n\\n\")\n\nwith torch.no_grad():\n    logits = model(x_batch.to(cfg.device))\n\nloss0 = F.cross_entropy(logits, y_batch.to(cfg.device))\nprint(f\"Initial Loss: {loss0} | Expected Loss: {-np.log(1/18)}\") ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:27.761267Z","iopub.execute_input":"2025-08-19T02:01:27.761445Z","iopub.status.idle":"2025-08-19T02:01:28.362823Z","shell.execute_reply.started":"2025-08-19T02:01:27.761430Z","shell.execute_reply":"2025-08-19T02:01:28.362145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"counts = Counter(y_train.tolist())\nclass_counts = torch.tensor([counts.get(i, 0) for i in range(cfg.num_classes)], dtype=torch.float32)\n\nclass_weights = 1.0 / (class_counts + 1e-8)  # Inverse frequency\nclass_weights = class_weights / class_weights.sum()  # Normalize to keep loss scale stable (optional but recommended)\n\nloss_fn = nn.CrossEntropyLoss(weight=class_weights).to(cfg.device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:28.363622Z","iopub.execute_input":"2025-08-19T02:01:28.363892Z","iopub.status.idle":"2025-08-19T02:01:28.370422Z","shell.execute_reply.started":"2025-08-19T02:01:28.363867Z","shell.execute_reply":"2025-08-19T02:01:28.369837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"learning_rate   = 3e-4\nweight_decay    = 0.01\nuse_fused       = True\n\n# Optimizer\noptimizer = torch.optim.AdamW(\n    model.parameters(),\n    lr=learning_rate,\n    weight_decay=weight_decay,\n    fused=use_fused\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:28.371066Z","iopub.execute_input":"2025-08-19T02:01:28.371269Z","iopub.status.idle":"2025-08-19T02:01:28.383068Z","shell.execute_reply.started":"2025-08-19T02:01:28.371254Z","shell.execute_reply":"2025-08-19T02:01:28.382509Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_epochs = 100\nsteps_per_epoch = len(train_loader)\n\n# OneCycleLR scheduler: warm-up + cosine annealing\nscheduler = torch.optim.lr_scheduler.OneCycleLR(\n    optimizer,\n    max_lr=learning_rate,              # Peak learning rate after warm-up\n    epochs=num_epochs,                 # Total number of epochs (no +1)\n    steps_per_epoch=steps_per_epoch,   # Number of optimizer steps per epoch\n    pct_start=0.05,                    # % of total steps used for warm-up\n    anneal_strategy='cos',             # Use cosine annealing after warm-up\n    div_factor=25,                     # initial_lr = max_lr / div_factor\n    final_div_factor=1e9               # final_lr  = max_lr / final_div_factor\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:28.383894Z","iopub.execute_input":"2025-08-19T02:01:28.384076Z","iopub.status.idle":"2025-08-19T02:01:28.396595Z","shell.execute_reply.started":"2025-08-19T02:01:28.384063Z","shell.execute_reply":"2025-08-19T02:01:28.395898Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"loss_train_hist = [loss0.item()]\nloss_valid_hist = [loss0.item()]\n\nmetric_train_hist = [0]\nmetric_valid_hist = [0]\n\nbest_loss_valid = torch.inf\nepoch_counter = 0\n\ncheckpoint_path = \"best-model.pt\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:28.397395Z","iopub.execute_input":"2025-08-19T02:01:28.397626Z","iopub.status.idle":"2025-08-19T02:01:28.409443Z","shell.execute_reply.started":"2025-08-19T02:01:28.397611Z","shell.execute_reply":"2025-08-19T02:01:28.408722Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"torch.manual_seed(1337)\n\nfor epoch in range(1, num_epochs+1):\n    # Train\n    loss_train, metric_train = train_one_epoch(model, train_loader, loss_fn, optimizer, scheduler, epoch)\n    print(f'🔸 Train: Loss={loss_train:.4} | Metric={metric_train:.4}')\n\n    # Validation\n    loss_valid, metric_valid = evaluate(model, valid_loader)\n    print(f'🔹 Valid: Loss={loss_valid:.4} | Metric={metric_valid:.4}')\n\n    loss_train_hist.append(loss_train)\n    loss_valid_hist.append(loss_valid)\n\n    metric_train_hist.append(metric_train)\n    metric_valid_hist.append(metric_valid)\n\n    #  Save best model\n    if loss_valid < best_loss_valid:\n        best_loss_valid = loss_valid\n        torch.save(model.state_dict(), checkpoint_path)\n        print(f\"✅ Saved new best model (epoch {epoch})\")\n\n    print()\n    \n    epoch_counter += 1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:01:28.410238Z","iopub.execute_input":"2025-08-19T02:01:28.410458Z","iopub.status.idle":"2025-08-19T02:04:26.062033Z","shell.execute_reply.started":"2025-08-19T02:01:28.410434Z","shell.execute_reply":"2025-08-19T02:04:26.061204Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🟠 **Plot**","metadata":{}},{"cell_type":"code","source":"n = len(loss_train_hist)\n\nplt.figure(figsize=(8, 6))\n\nplt.plot(range(n), loss_train_hist, 'r-', label='Train')\nplt.plot(range(n), loss_valid_hist, 'b-', label='Validation')\n\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.grid(True)\nplt.legend();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:04:26.062972Z","iopub.execute_input":"2025-08-19T02:04:26.063262Z","iopub.status.idle":"2025-08-19T02:04:26.375424Z","shell.execute_reply.started":"2025-08-19T02:04:26.063244Z","shell.execute_reply":"2025-08-19T02:04:26.374645Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.figure(figsize=(8, 6))\n\nplt.plot(range(n), metric_train_hist, 'r-', label='Train')\nplt.plot(range(n), metric_valid_hist, 'b-', label='Validation')\n\nplt.xlabel('Epoch')\nplt.ylabel('Metric')\nplt.grid(True)\nplt.legend();","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:04:26.376260Z","iopub.execute_input":"2025-08-19T02:04:26.376528Z","iopub.status.idle":"2025-08-19T02:04:26.619233Z","shell.execute_reply.started":"2025-08-19T02:04:26.376510Z","shell.execute_reply":"2025-08-19T02:04:26.618405Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==== EVAL CELL: tổng hợp mọi metric vào 1 ô ====\nimport numpy as np\nimport torch\nfrom sklearn.metrics import (\n    accuracy_score, balanced_accuracy_score,\n    f1_score, precision_score, recall_score,\n    classification_report, confusion_matrix\n)\n\n# 1) Device + classes (tên lớp theo đúng thứ tự encoder)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nif \"label_encoder\" not in globals():\n    raise RuntimeError(\"Chưa thấy label_encoder. Hãy đảm bảo bạn đã fit LabelEncoder trước ô này.\")\nclasses = list(label_encoder.classes_)\nnum_classes = len(classes)\n\n# 2) Đánh giá trên valid_loader\nmodel.to(device)\nmodel.eval()\n\nall_targets, all_preds = [], []\nwith torch.no_grad():\n    for batch in valid_loader:\n        # batch có thể là (X, y) hoặc dict; đổi tuỳ Dataset của bạn\n        if isinstance(batch, (tuple, list)) and len(batch) == 2:\n            batch_x, batch_y = batch\n        elif isinstance(batch, dict):\n            batch_x, batch_y = batch[\"x\"], batch[\"y\"]\n        else:\n            raise RuntimeError(\"Không nhận diện được cấu trúc batch từ valid_loader.\")\n\n        batch_x = batch_x.to(device)\n        # y có thể là LongTensor (class index) hoặc one-hot\n        if isinstance(batch_y, torch.Tensor):\n            if batch_y.dtype in (torch.int64, torch.long):\n                y_true = batch_y.cpu().numpy()\n            else:\n                y_true = batch_y.argmax(dim=1).cpu().numpy()\n        else:\n            # phòng trường hợp dataset trả numpy\n            y_true = np.asarray(batch_y)\n            if y_true.ndim > 1:  # one-hot\n                y_true = y_true.argmax(axis=1)\n\n        logits = model(batch_x)\n        y_pred = logits.argmax(dim=1).cpu().numpy()\n\n        all_targets.append(y_true)\n        all_preds.append(y_pred)\n\nall_targets = np.concatenate(all_targets)\nall_preds   = np.concatenate(all_preds)\n\n# 3) Metrics chung\nacc           = accuracy_score(all_targets, all_preds)\nbacc          = balanced_accuracy_score(all_targets, all_preds)\nf1_macro      = f1_score(all_targets, all_preds, average=\"macro\")\nf1_micro      = f1_score(all_targets, all_preds, average=\"micro\")\nf1_weighted   = f1_score(all_targets, all_preds, average=\"weighted\")\nprecision_macro = precision_score(all_targets, all_preds, average=\"macro\", zero_division=0)\nrecall_macro    = recall_score(all_targets, all_preds, average=\"macro\", zero_division=0)\n\nprint(\"📊 Validation Results\")\nprint(\"-----------------------------\")\nprint(f\"Accuracy            : {acc:.4f}\")\nprint(f\"Balanced Accuracy   : {bacc:.4f}\")\nprint(f\"F1 Macro            : {f1_macro:.4f}\")\nprint(f\"F1 Micro            : {f1_micro:.4f}\")\nprint(f\"F1 Weighted         : {f1_weighted:.4f}\")\nprint(f\"Precision (Macro)   : {precision_macro:.4f}\")\nprint(f\"Recall (Macro)      : {recall_macro:.4f}\")\n\n# 4) (Tuỳ chọn) nếu bạn có danh sách nhãn BFRB -> tính thêm Binary F1 (BFRB vs non-BFRB)\nif \"bfrb_gestures\" in globals():\n    try:\n        bfrb_indices = label_encoder.transform(bfrb_gestures)\n        bin_true = np.isin(all_targets, bfrb_indices).astype(int)\n        bin_pred = np.isin(all_preds,   bfrb_indices).astype(int)\n        f1_binary = f1_score(bin_true, bin_pred, zero_division=0)\n        final_metric = 0.5 * (f1_binary + f1_macro)\n        print(f\"F1 Binary (BFRB vs non-BFRB): {f1_binary:.4f}\")\n        print(f\"Final metric (avg Binary & Macro): {final_metric:.4f}\")\n    except Exception as e:\n        print(f\"[Note] Không tính được F1 Binary vì lỗi: {e}\")\n\n# 5) Classification report (per class)\nprint(\"\\n🔎 Classification report (per class):\")\nprint(classification_report(all_targets, all_preds, target_names=classes, digits=4))\n\n# 6) Confusion matrix (in số)\ncm = confusion_matrix(all_targets, all_preds, labels=np.arange(num_classes))\nprint(\"Confusion matrix (rows=true, cols=pred):\")\nprint(cm)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:23:10.834941Z","iopub.execute_input":"2025-08-19T02:23:10.835289Z","iopub.status.idle":"2025-08-19T02:23:10.986937Z","shell.execute_reply.started":"2025-08-19T02:23:10.835266Z","shell.execute_reply":"2025-08-19T02:23:10.986319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ===== TRAIN ARTIFACT SNAPSHOT (safe to paste in your TRAIN notebook) =====\nimport os, joblib, numpy as np, pandas as pd\nfrom sklearn.preprocessing import LabelEncoder\n\n# 0) Bảo đảm có train_df; nếu chưa, tự load\nif 'train_df' not in globals():\n    train_df = pd.read_csv(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\n\n# 1) CLASSES (thứ tự nhãn đúng với model)\nif 'label_encoder' in globals():\n    classes = list(label_encoder.classes_)\nelse:\n    _le = LabelEncoder()\n    _ = _le.fit_transform(train_df['gesture'].astype(str))\n    classes = list(_le.classes_)\n    label_encoder = _le  # lưu lại để dùng tiếp\n\n# 2) FEATURE COLS (đúng thứ tự đã train: IMU trước, rồi THM/ToF + engineered features nếu có)\nimu_cols = ['acc_x','acc_y','acc_z','rot_w','rot_x','rot_y','rot_z']\n\n# ToF/Thermal “raw” (nếu bạn không drop pixel ToF)\nthm_tof_cols = [c for c in train_df.columns if c.startswith('thm_') or c.startswith('tof_')]\n\n# Nếu bạn có engineered features, liệt kê vào đây theo đúng thứ tự bạn đã feed vào model:\nextra_feats = []\n# ví dụ (bật những dòng bạn thực sự đã dùng khi train):\n# extra_feats += ['acc_mag','rot_angle','acc_mag_jerk','rot_angle_vel']\n# extra_feats += [f\"tof_{i}_{stat}\" for i in range(1,6) for stat in ['mean','std','min','max']]\n\nif 'feature_cols' in globals():\n    # Tôn trọng danh sách đã có sẵn (nếu bạn tạo trước đó trong notebook)\n    feature_cols = list(feature_cols)\nelse:\n    # Mặc định: IMU trước, rồi extra_feats (nếu có), rồi THM/ToF\n    feature_cols = imu_cols + extra_feats + thm_tof_cols\n\n# 3) MAXLEN (độ dài pad/truncate)\nif 'pad_len' in globals():\n    maxlen = int(pad_len)\nelif 'maxlen' in globals():\n    maxlen = int(maxlen)\nelse:\n    # Tự tính theo p90 nếu chưa có\n    seq_lengths = train_df.groupby('sequence_id').size().to_numpy()\n    maxlen = int(np.percentile(seq_lengths, 90))\n\n# 4) SCALER (tùy chọn)\nscaler = globals().get('scaler', None)  # nếu bạn đã fit scaler trong train, nó sẽ có ở đây\n\n# 5) SAVE\nos.makedirs(\"outputs\", exist_ok=True)\nnp.save(\"outputs/feature_cols.npy\", np.array(feature_cols, dtype=object))\nnp.save(\"outputs/classes.npy\",     np.array(classes, dtype=object))\nnp.save(\"outputs/maxlen.npy\",      np.array([maxlen], dtype=np.int32))\nif scaler is not None:\n    joblib.dump(scaler, \"outputs/scaler.pkl\")\n\nprint(\"✅ Saved artifacts to ./outputs\")\nprint(f\" - feature_cols.npy  ({len(feature_cols)} features)\")\nprint(f\" - classes.npy       ({len(classes)} classes)\")\nprint(f\" - maxlen.npy        ({maxlen})\")\nprint(f\" - scaler.pkl        ({'yes' if scaler is not None else 'no'})\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:23:16.755956Z","iopub.execute_input":"2025-08-19T02:23:16.756271Z","iopub.status.idle":"2025-08-19T02:23:16.767295Z","shell.execute_reply.started":"2025-08-19T02:23:16.756246Z","shell.execute_reply":"2025-08-19T02:23:16.766443Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔴 **Predict (Kaggle Evaluation)**","metadata":{}},{"cell_type":"code","source":"model = TimeSeriesCNN(cfg.input_channels, cfg.num_classes, conv_channels=[32, 64, 128]).to(cfg.device)\nstate_dict = torch.load(\"best-model.pt\", map_location=cfg.device)\nmodel.load_state_dict(state_dict)\nmodel.eval()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:23:21.820682Z","iopub.execute_input":"2025-08-19T02:23:21.820981Z","iopub.status.idle":"2025-08-19T02:23:21.838805Z","shell.execute_reply.started":"2025-08-19T02:23:21.820961Z","shell.execute_reply":"2025-08-19T02:23:21.838204Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_seq = sequence.to_pandas()\n    # Preprocess\n    data = pipeline.transform(df_seq)\n    data = torch.tensor(data.T, dtype=torch.float32).unsqueeze(0)\n    with torch.inference_mode():\n        logits = model(data.to(cfg.device))\n    cls = torch.argmax(logits, keepdims=True).cpu().numpy()\n    cls = label_encoder.inverse_transform(cls[0])\n    return cls[0]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:23:25.599814Z","iopub.execute_input":"2025-08-19T02:23:25.600092Z","iopub.status.idle":"2025-08-19T02:23:25.605073Z","shell.execute_reply.started":"2025-08-19T02:23:25.600072Z","shell.execute_reply":"2025-08-19T02:23:25.604516Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\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    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-19T02:23:28.093614Z","iopub.execute_input":"2025-08-19T02:23:28.093878Z","iopub.status.idle":"2025-08-19T02:23:29.509593Z","shell.execute_reply.started":"2025-08-19T02:23:28.093848Z","shell.execute_reply":"2025-08-19T02:23:29.508808Z"}},"outputs":[],"execution_count":null}]}