{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31089,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"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-07-30T15:15:18.175655Z","iopub.execute_input":"2025-07-30T15:15:18.175920Z","iopub.status.idle":"2025-07-30T15:15:31.109058Z","shell.execute_reply.started":"2025-07-30T15:15:18.175901Z","shell.execute_reply":"2025-07-30T15:15:31.108490Z"}},"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-07-30T15:15:31.110103Z","iopub.execute_input":"2025-07-30T15:15:31.110508Z","iopub.status.idle":"2025-07-30T15:15:31.114976Z","shell.execute_reply.started":"2025-07-30T15:15:31.110482Z","shell.execute_reply":"2025-07-30T15:15:31.114266Z"}},"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-07-30T15:15:31.115649Z","iopub.execute_input":"2025-07-30T15:15:31.115884Z","iopub.status.idle":"2025-07-30T15:15:31.133843Z","shell.execute_reply.started":"2025-07-30T15:15:31.115867Z","shell.execute_reply":"2025-07-30T15:15:31.133144Z"}},"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-07-30T15:15:33.230985Z","iopub.execute_input":"2025-07-30T15:15:33.231253Z","iopub.status.idle":"2025-07-30T15:15:33.237289Z","shell.execute_reply.started":"2025-07-30T15:15:33.231233Z","shell.execute_reply":"2025-07-30T15:15:33.236495Z"}},"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-07-30T15:15:40.586873Z","iopub.execute_input":"2025-07-30T15:15:40.587138Z","iopub.status.idle":"2025-07-30T15:15:40.664288Z","shell.execute_reply.started":"2025-07-30T15:15:40.587116Z","shell.execute_reply":"2025-07-30T15:15:40.663557Z"}},"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-07-30T15:16:13.700872Z","iopub.execute_input":"2025-07-30T15:16:13.701408Z","iopub.status.idle":"2025-07-30T15:16:13.706673Z","shell.execute_reply.started":"2025-07-30T15:16:13.701377Z","shell.execute_reply":"2025-07-30T15:16:13.706121Z"}},"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-07-30T15:16:16.589076Z","iopub.execute_input":"2025-07-30T15:16:16.589470Z","iopub.status.idle":"2025-07-30T15:16:44.575127Z","shell.execute_reply.started":"2025-07-30T15:16:16.589437Z","shell.execute_reply":"2025-07-30T15:16:44.574413Z"}},"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-07-30T15:16:44.576479Z","iopub.execute_input":"2025-07-30T15:16:44.576738Z","iopub.status.idle":"2025-07-30T15:16:44.585775Z","shell.execute_reply.started":"2025-07-30T15:16:44.576720Z","shell.execute_reply":"2025-07-30T15:16:44.585137Z"}},"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-07-30T15:16:44.586563Z","iopub.execute_input":"2025-07-30T15:16:44.586919Z","iopub.status.idle":"2025-07-30T15:16:45.317399Z","shell.execute_reply.started":"2025-07-30T15:16:44.586896Z","shell.execute_reply":"2025-07-30T15:16:45.316632Z"}},"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-07-30T15:16:45.318979Z","iopub.execute_input":"2025-07-30T15:16:45.319202Z","iopub.status.idle":"2025-07-30T15:16:45.347858Z","shell.execute_reply.started":"2025-07-30T15:16:45.319186Z","shell.execute_reply":"2025-07-30T15:16:45.347210Z"}},"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-07-30T15:16:45.348775Z","iopub.execute_input":"2025-07-30T15:16:45.349160Z","iopub.status.idle":"2025-07-30T15:16:48.309753Z","shell.execute_reply.started":"2025-07-30T15:16:45.349136Z","shell.execute_reply":"2025-07-30T15:16:48.308993Z"}},"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-07-30T15:16:48.310545Z","iopub.execute_input":"2025-07-30T15:16:48.310811Z","iopub.status.idle":"2025-07-30T15:16:49.125798Z","shell.execute_reply.started":"2025-07-30T15:16:48.310793Z","shell.execute_reply":"2025-07-30T15:16:49.125042Z"}},"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-07-30T15:16:54.982092Z","iopub.execute_input":"2025-07-30T15:16:54.982398Z","iopub.status.idle":"2025-07-30T15:16:54.988009Z","shell.execute_reply.started":"2025-07-30T15:16:54.982348Z","shell.execute_reply":"2025-07-30T15:16:54.987414Z"}},"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-07-30T15:17:06.500707Z","iopub.execute_input":"2025-07-30T15:17:06.500985Z","iopub.status.idle":"2025-07-30T15:17:06.505586Z","shell.execute_reply.started":"2025-07-30T15:17:06.500965Z","shell.execute_reply":"2025-07-30T15:17:06.504862Z"}},"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-07-30T15:17:24.990008Z","iopub.execute_input":"2025-07-30T15:17:24.990527Z","iopub.status.idle":"2025-07-30T15:17:24.997788Z","shell.execute_reply.started":"2025-07-30T15:17:24.990504Z","shell.execute_reply":"2025-07-30T15:17:24.997049Z"}},"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-07-30T15:17:50.931083Z","iopub.execute_input":"2025-07-30T15:17:50.931376Z","iopub.status.idle":"2025-07-30T15:17:50.947342Z","shell.execute_reply.started":"2025-07-30T15:17:50.931338Z","shell.execute_reply":"2025-07-30T15:17:50.946649Z"}},"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-07-30T15:18:04.385645Z","iopub.execute_input":"2025-07-30T15:18:04.386232Z","iopub.status.idle":"2025-07-30T15:18:04.551279Z","shell.execute_reply.started":"2025-07-30T15:18:04.386210Z","shell.execute_reply":"2025-07-30T15:18:04.550693Z"}},"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-07-30T15:18:25.195354Z","iopub.execute_input":"2025-07-30T15:18:25.195954Z","iopub.status.idle":"2025-07-30T15:18:25.803047Z","shell.execute_reply.started":"2025-07-30T15:18:25.195934Z","shell.execute_reply":"2025-07-30T15:18:25.802235Z"}},"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-07-30T15:18:32.432786Z","iopub.execute_input":"2025-07-30T15:18:32.433372Z","iopub.status.idle":"2025-07-30T15:18:32.439400Z","shell.execute_reply.started":"2025-07-30T15:18:32.433336Z","shell.execute_reply":"2025-07-30T15:18:32.438876Z"}},"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-07-30T15:18:37.639961Z","iopub.execute_input":"2025-07-30T15:18:37.640648Z","iopub.status.idle":"2025-07-30T15:18:37.644898Z","shell.execute_reply.started":"2025-07-30T15:18:37.640626Z","shell.execute_reply":"2025-07-30T15:18:37.644167Z"}},"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-07-30T15:18:39.006351Z","iopub.execute_input":"2025-07-30T15:18:39.007048Z","iopub.status.idle":"2025-07-30T15:18:39.011075Z","shell.execute_reply.started":"2025-07-30T15:18:39.007027Z","shell.execute_reply":"2025-07-30T15:18:39.010471Z"}},"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-07-30T15:18:43.124222Z","iopub.execute_input":"2025-07-30T15:18:43.124527Z","iopub.status.idle":"2025-07-30T15:18:43.129044Z","shell.execute_reply.started":"2025-07-30T15:18:43.124504Z","shell.execute_reply":"2025-07-30T15:18:43.128308Z"}},"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-07-30T15:19:09.860173Z","iopub.execute_input":"2025-07-30T15:19:09.860545Z","iopub.status.idle":"2025-07-30T15:22:00.891727Z","shell.execute_reply.started":"2025-07-30T15:19:09.860522Z","shell.execute_reply":"2025-07-30T15:22:00.891112Z"}},"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-07-30T15:22:27.402547Z","iopub.execute_input":"2025-07-30T15:22:27.402827Z","iopub.status.idle":"2025-07-30T15:22:27.702542Z","shell.execute_reply.started":"2025-07-30T15:22:27.402807Z","shell.execute_reply":"2025-07-30T15:22:27.701769Z"}},"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-07-30T15:23:26.043522Z","iopub.execute_input":"2025-07-30T15:23:26.044066Z","iopub.status.idle":"2025-07-30T15:23:26.275113Z","shell.execute_reply.started":"2025-07-30T15:23:26.044043Z","shell.execute_reply":"2025-07-30T15:23:26.274405Z"}},"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-07-30T15:24:40.856420Z","iopub.execute_input":"2025-07-30T15:24:40.856939Z","iopub.status.idle":"2025-07-30T15:24:40.872124Z","shell.execute_reply.started":"2025-07-30T15:24:40.856916Z","shell.execute_reply":"2025-07-30T15:24:40.871408Z"}},"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-07-30T15:24:44.839875Z","iopub.execute_input":"2025-07-30T15:24:44.840353Z","iopub.status.idle":"2025-07-30T15:24:44.844928Z","shell.execute_reply.started":"2025-07-30T15:24:44.840333Z","shell.execute_reply":"2025-07-30T15:24:44.844097Z"}},"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-07-30T15:24:46.116852Z","iopub.execute_input":"2025-07-30T15:24:46.117583Z","iopub.status.idle":"2025-07-30T15:24:47.479722Z","shell.execute_reply.started":"2025-07-30T15:24:46.117559Z","shell.execute_reply":"2025-07-30T15:24:47.479059Z"}},"outputs":[],"execution_count":null}]}