{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"isSourceIdPinned":false,"sourceType":"competition"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport json\nimport joblib\nimport numpy as np\nimport pandas as pd\nimport random\nfrom pathlib import Path\nimport warnings\nimport shutil\nwarnings.filterwarnings(\"ignore\")\n\n# Scikit-learn\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\n# PyTorch\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\nfrom torch.optim.lr_scheduler import LambdaLR\n\nimport polars as pl\n\n# Set Seed\ndef seed_everything(seed):\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\nseed_everything(seed=42)\n\n# Configuration\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nEXPORT_DIR = Path(\"./\")                 \n\nBATCH_SIZE = 64\nPAD_PERCENTILE = 95\nLR_INIT = 5e-4\nWD = 3e-3\nMIXUP_ALPHA = 0.4\nEPOCHS = 160\nPATIENCE = 40\n\nprint(\"▶ imports ready · pytorch\", torch.__version__)\n\n# Utility Functions\ndef time_sum(x): return torch.sum(x, dim=1)\ndef squeeze_last_axis(x): return x.squeeze(-1)\ndef expand_last_axis(x): return x.unsqueeze(-1)\n\nclass SEBlock(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super(SEBlock, self).__init__()\n        self.avg_pool = nn.AdaptiveAvgPool1d(1)\n        self.fc1 = nn.Linear(channels, channels // reduction, bias=False)\n        self.relu = nn.ReLU(inplace=True)\n        self.fc2 = nn.Linear(channels // reduction, channels, bias=False)\n        self.sigmoid = nn.Sigmoid()\n\n    def forward(self, x):\n        b, c, _ = x.size()\n        se = self.avg_pool(x).view(b, c)\n        se = self.fc1(se)\n        se = self.relu(se)\n        se = self.fc2(se)\n        se = self.sigmoid(se).view(b, c, 1)\n        return x * se\n\nclass ResidualSECNNBlock(nn.Module):\n    def __init__(self, in_filters, out_filters, kernel_size, pool_size=2, drop=0.3):\n        super(ResidualSECNNBlock, self).__init__()\n        self.conv_block = nn.Sequential(\n            nn.Conv1d(in_filters, out_filters, kernel_size, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters), nn.ReLU(inplace=True),\n            nn.Conv1d(out_filters, out_filters, kernel_size, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters), nn.ReLU(inplace=True)\n        )\n        self.se_block = SEBlock(out_filters)\n        self.shortcut_conv = nn.Sequential(\n            nn.Conv1d(in_filters, out_filters, 1, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters)\n        ) if in_filters != out_filters else None\n        self.max_pool = nn.MaxPool1d(pool_size)\n        self.dropout = nn.Dropout(drop)\n\n    def forward(self, x):\n        shortcut = self.shortcut_conv(x) if self.shortcut_conv else x\n        x = self.conv_block(x)\n        x = self.se_block(x)\n        x = x + shortcut\n        x = F.relu(x)\n        x = self.max_pool(x)\n        x = self.dropout(x)\n        return x\n\nclass AttentionLayer(nn.Module):\n    def __init__(self, input_dim):\n        super(AttentionLayer, self).__init__()\n        self.score_dense = nn.Linear(input_dim, 1)\n    def forward(self, inputs):\n        score = self.score_dense(inputs)\n        score = torch.tanh(score)\n        score = squeeze_last_axis(score)\n        weights = F.softmax(score, dim=1)\n        weights = expand_last_axis(weights)\n        context = inputs * weights\n        context = time_sum(context)\n        return context\n\nclass MixupDataset(Dataset):\n    def __init__(self, X: torch.Tensor, y: torch.Tensor, alpha: float = 0.2):\n        self.X, self.y, self.alpha = X, y, alpha\n    def __len__(self): return len(self.X)\n    def __getitem__(self, idx: int):\n        x1, y1 = self.X[idx], self.y[idx]\n        rand_idx = random.randint(0, len(self.X) - 1)\n        x2, y2 = self.X[rand_idx], self.y[rand_idx]\n        lam = np.random.beta(self.alpha, self.alpha)\n        x_mixed = lam * x1 + (1 - lam) * x2\n        y_mixed = lam * y1 + (1 - lam) * y2\n        return x_mixed, y_mixed\n\nclass SimpleCNNBlock(nn.Module):\n    def __init__(self, in_filters, out_filters, kernel_size=3, pool_size=2, drop=0.2):\n        super(SimpleCNNBlock, self).__init__()\n        self.block = nn.Sequential(\n            nn.Conv1d(in_filters, out_filters, kernel_size, padding='same', bias=False),\n            nn.BatchNorm1d(out_filters), nn.ReLU(inplace=True),\n            nn.MaxPool1d(pool_size), nn.Dropout(drop)\n        )\n    def forward(self, x): return self.block(x)\n\nclass TwoBranchModel(nn.Module):\n    def __init__(self, pad_len, imu_dim, tof_dim, n_classes):\n        super(TwoBranchModel, self).__init__()\n        self.imu_dim, self.tof_dim = imu_dim, tof_dim\n        self.imu_branch = nn.Sequential(\n            ResidualSECNNBlock(imu_dim, 64, 3, drop=0.1),\n            ResidualSECNNBlock(64, 128, 5, drop=0.1)\n        )\n        self.tof_branch = nn.Sequential(\n            SimpleCNNBlock(tof_dim, 64, drop=0.2),\n            SimpleCNNBlock(64, 128, drop=0.2)\n        )\n        self.recurrent_layer = nn.LSTM(256, 128, bidirectional=True, batch_first=True)\n        self.attention_layer = AttentionLayer(128 * 2)\n        self.classifier_head = nn.Sequential(\n            nn.Linear(128 * 2, 256, bias=False), nn.BatchNorm1d(256),\n            nn.ReLU(inplace=True), nn.Dropout(0.5),\n            nn.Linear(256, 128, bias=False), nn.BatchNorm1d(128),\n            nn.ReLU(inplace=True), nn.Dropout(0.3)\n        )\n        self.output_layer = nn.Linear(128, n_classes)\n\n    def forward(self, x):\n        imu = x[:, :, :self.imu_dim].permute(0, 2, 1)\n        tof = x[:, :, self.imu_dim:].permute(0, 2, 1)\n        x1 = self.imu_branch(imu)\n        x2 = self.tof_branch(tof)\n        merged = torch.cat([x1, x2], dim=1).permute(0, 2, 1)\n        recurrent_out, _ = self.recurrent_layer(merged)\n        attention_out = self.attention_layer(recurrent_out)\n        classified = self.classifier_head(attention_out)\n        return self.output_layer(classified)\n\ndef save_checkpoint(state, is_best, directory, filename=\"latest_checkpoint.pth\", best_filename=\"best_checkpoint.pth\"):\n    filepath = directory / filename\n    torch.save(state, filepath)\n    if is_best:\n        shutil.copyfile(filepath, directory / best_filename)\n\n# This global dict will hold the artifacts\nartifacts = {}\n\ndef fe_and_prepare_data(is_train):\n    global artifacts\n    if is_train:\n        if EXPORT_DIR.exists() and any(EXPORT_DIR.iterdir()):\n            print(f\"--- Cleaning up existing files in {EXPORT_DIR} ---\")\n            for item in EXPORT_DIR.iterdir():\n                if item.is_file(): item.unlink()\n                elif item.is_dir(): shutil.rmtree(item)\n        EXPORT_DIR.mkdir(exist_ok=True)\n        \n        print(\"▶ Loading and preparing training data...\")\n        df = pd.read_csv(RAW_DIR / \"train.csv\")\n        le = LabelEncoder()\n        df['gesture_int'] = le.fit_transform(df['gesture'])\n        np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n        artifacts['gesture_classes'] = le.classes_\n    else:\n        # In inference, we don't have a big CSV, so this part is handled in predict\n        return\n\n    print(\"  Calculating engineered features...\")\n    # 1. Basic Kinematics\n    df['acc_mag'] = np.sqrt(df['acc_x']**2 + df['acc_y']**2 + df['acc_z']**2)\n    df['rot_angle'] = 2 * np.arccos(df['rot_w'].clip(-1, 1))\n\n    # 2. Advanced Time-Derivative Features\n    for col in ['acc_x', 'acc_y', 'acc_z', 'acc_mag', 'rot_x', 'rot_y', 'rot_z', 'rot_angle']:\n        df[f'{col}_vel'] = df.groupby('sequence_id')[col].diff().fillna(0)\n        df[f'{col}_acc'] = df.groupby('sequence_id')[f'{col}_vel'].diff().fillna(0)\n\n    # 3. Rolling Window Features\n    window_sizes = [5, 10]\n    feature_cols_for_rolling = ['acc_mag', 'acc_mag_vel', 'acc_mag_acc']\n    for window in window_sizes:\n        for col in feature_cols_for_rolling:\n            df[f'{col}_roll_mean_{window}'] = df.groupby('sequence_id')[col].rolling(window, min_periods=1, center=True).mean().reset_index(level=0, drop=True)\n            df[f'{col}_roll_std_{window}'] = df.groupby('sequence_id')[col].rolling(window, min_periods=1, center=True).std().reset_index(level=0, drop=True)\n\n    print(\"  Calculating ToF features with vectorized NumPy...\")\n    tof_pixel_cols = [f\"tof_{i}_v{p}\" for i in range(1, 6) for p in range(64)]\n    tof_data_np = df[tof_pixel_cols].replace(-1, np.nan).to_numpy()\n    reshaped_tof = tof_data_np.reshape(len(df), 5, 64)\n    with warnings.catch_warnings():\n        warnings.simplefilter('ignore', category=RuntimeWarning)\n        mean_vals, std_vals = np.nanmean(reshaped_tof, axis=2), np.nanstd(reshaped_tof, axis=2)\n        min_vals, max_vals = np.nanmin(reshaped_tof, axis=2), np.nanmax(reshaped_tof, axis=2)\n    tof_agg_cols = []\n    for i in range(1, 6):\n        df[f'tof_{i}_mean'], df[f'tof_{i}_std'] = mean_vals[:, i-1], std_vals[:, i-1]\n        df[f'tof_{i}_min'], df[f'tof_{i}_max'] = min_vals[:, i-1], max_vals[:, i-1]\n        tof_agg_cols.extend([f'tof_{i}_mean', f'tof_{i}_std', f'tof_{i}_min', f'tof_{i}_max'])\n    \n    imu_cols = [c for c in df.columns if c.startswith(('acc_', 'rot_'))]\n    thm_cols = [c for c in df.columns if c.startswith('thm_')]\n    final_feature_cols = imu_cols + thm_cols + tof_agg_cols\n    artifacts['final_feature_cols'] = final_feature_cols\n    artifacts['imu_dim_final'] = len(imu_cols)\n    artifacts['tof_thm_aggregated_dim_final'] = len(thm_cols) + len(tof_agg_cols)\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(final_feature_cols))\n\n    print(\"  Building, scaling, and padding sequences...\")\n    X_list, y_list, lens = [], [], []\n    for _, seq_df in df.groupby('sequence_id'):\n        X_list.append(seq_df[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32'))\n        y_list.append(seq_df['gesture_int'].iloc[0])\n        lens.append(len(seq_df))\n        \n    feature_scaler = StandardScaler().fit(np.concatenate(X_list, axis=0))\n    joblib.dump(feature_scaler, EXPORT_DIR / \"scaler.pkl\")\n    artifacts['feature_scaler'] = feature_scaler\n    X_scaled_list = [feature_scaler.transform(x) for x in X_list]\n    \n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    artifacts['pad_len'] = pad_len\n    \n    X_padded = np.zeros((len(X_scaled_list), pad_len, len(final_feature_cols)), dtype='float32')\n    for i, seq in enumerate(X_scaled_list):\n        X_padded[i, :min(len(seq), pad_len)] = seq[:min(len(seq), pad_len)]\n    y_np = np.array(y_list)\n    y_one_hot = F.one_hot(torch.from_numpy(y_np), num_classes=len(artifacts['gesture_classes'])).float().numpy()\n    \n    X_tr, X_val, y_tr_oh, y_val_oh, y_tr_int, _ = train_test_split(\n        X_padded, y_one_hot, y_np, test_size=0.1, random_state=82, stratify=y_np\n    )\n    \n    artifacts['train_loader'] = DataLoader(MixupDataset(torch.from_numpy(X_tr), torch.from_numpy(y_tr_oh), alpha=MIXUP_ALPHA), batch_size=BATCH_SIZE, shuffle=True, num_workers=os.cpu_count()//2 or 1, pin_memory=True)\n    artifacts['val_loader'] = DataLoader(torch.utils.data.TensorDataset(torch.from_numpy(X_val), torch.from_numpy(y_val_oh)), batch_size=BATCH_SIZE, shuffle=False, num_workers=os.cpu_count()//2 or 1, pin_memory=True)\n    \n    cw_vals = compute_class_weight('balanced', classes=np.arange(len(artifacts['gesture_classes'])), y=y_tr_int)\n    artifacts['class_weight'] = torch.from_numpy(cw_vals).float()\n\n# --- Main Execution Block ---\n\n# 1. Always train the model first\nprint(\"--- Starting Training Phase ---\")\nfe_and_prepare_data(is_train=True)\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"Using device: {device}\")\n\nclass_weight = artifacts['class_weight'].to(device)\nmodel = TwoBranchModel(\n    artifacts['pad_len'], artifacts['imu_dim_final'], \n    artifacts['tof_thm_aggregated_dim_final'], len(artifacts['gesture_classes'])\n)\nmodel.to(device)\noptimizer = optim.Adam(model.parameters(), lr=LR_INIT, weight_decay=WD)\nlr_scheduler = LambdaLR(optimizer, lambda step: 0.5 * (1 + np.cos(np.pi * step / (EPOCHS * len(artifacts['train_loader'])))))\ngrad_scaler = torch.cuda.amp.GradScaler(enabled=(device.type == 'cuda'))\n\nstart_epoch, best_val_accuracy, epochs_no_improve = 0, -1.0, 0\n\nprint(\"Skipping torch.compile() due to GPU hardware incompatibility.\")\n\nprint(\"  Starting model training...\")\nfor epoch in range(start_epoch, EPOCHS):\n    model.train()\n    running_loss = 0\n    for data, targets_one_hot in artifacts['train_loader']:\n        data, targets_one_hot = data.to(device), targets_one_hot.to(device)\n        targets_idx = torch.argmax(targets_one_hot, dim=1)\n        with torch.cuda.amp.autocast(enabled=(device.type == 'cuda')):\n            outputs = model(data)\n            smoothed_targets = targets_one_hot * (1 - 0.1) + (0.1 / len(artifacts['gesture_classes']))\n            per_sample_loss = F.cross_entropy(outputs, smoothed_targets, reduction='none')\n            loss = (per_sample_loss * class_weight[targets_idx]).mean()\n        optimizer.zero_grad(set_to_none=True)\n        grad_scaler.scale(loss).backward()\n        grad_scaler.step(optimizer)\n        grad_scaler.update()\n        lr_scheduler.step()\n        running_loss += loss.item() * data.size(0)\n    \n    epoch_loss = running_loss / len(artifacts['train_loader'].dataset)\n\n    model.eval()\n    val_loss, val_correct, val_total = 0.0, 0, 0\n    with torch.no_grad():\n        for data, targets_one_hot in artifacts['val_loader']:\n            data, targets_one_hot = data.to(device), targets_one_hot.to(device)\n            targets_idx = torch.argmax(targets_one_hot, dim=1)\n            with torch.cuda.amp.autocast(enabled=(device.type == 'cuda')):\n                outputs = model(data)\n                v_loss = F.cross_entropy(outputs, targets_idx, weight=class_weight)\n            val_loss += v_loss.item() * data.size(0)\n            _, predicted = torch.max(outputs.data, 1)\n            val_total += targets_idx.size(0)\n            val_correct += (predicted == targets_idx).sum().item()\n    \n    val_epoch_loss = val_loss / val_total\n    val_epoch_accuracy = val_correct / val_total\n\n    print(f\"E {epoch+1}/{EPOCHS} | L: {epoch_loss:.4f} | VL: {val_epoch_loss:.4f} VAcc: {val_epoch_accuracy:.4f}\")\n\n    is_best = val_epoch_accuracy > best_val_accuracy\n    if is_best:\n        best_val_accuracy = val_epoch_accuracy\n        epochs_no_improve = 0\n        print(f\"  New best val acc: {best_val_accuracy:.4f}. Saving model.\")\n        model_state = model.state_dict()\n        checkpoint_state = {\n            'epoch': epoch, 'model_state_dict': model_state,\n            'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': lr_scheduler.state_dict(),\n            'scaler_state_dict': grad_scaler.state_dict(), 'best_val_accuracy': best_val_accuracy\n        }\n        save_checkpoint(checkpoint_state, is_best=True, directory=EXPORT_DIR)\n    else:\n        epochs_no_improve += 1\n\n    if epochs_no_improve >= PATIENCE:\n        print(f\"Early stopping after {PATIENCE} epochs without improvement.\")\n        break\nprint(\"✔ Training done.\")\n\n# --- Fallback save ---\n# Always save the latest checkpoint at the end of training, regardless of performance.\n# This ensures that a model file is always available for submission.\nprint(\"Saving final model checkpoint...\")\nfinal_model_state = model.state_dict()\nfinal_checkpoint_state = {\n    'epoch': epoch, 'model_state_dict': final_model_state,\n    'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': lr_scheduler.state_dict(),\n    'scaler_state_dict': grad_scaler.state_dict(), 'best_val_accuracy': best_val_accuracy\n}\nsave_checkpoint(final_checkpoint_state, is_best=False, directory=EXPORT_DIR, filename=\"latest_checkpoint.pth\")\n# Also save it as best_checkpoint.pth if no best model was ever saved\nif not (EXPORT_DIR / \"best_checkpoint.pth\").exists():\n    print(\"No best model was saved during training. Saving last model as best_checkpoint.pth.\")\n    save_checkpoint(final_checkpoint_state, is_best=True, directory=EXPORT_DIR)\n\nprint(\"\\n--- Training Script Finished, Preparing Submission ---\")\n\n\n# 2. Immediately load the trained model and prepare for inference\nprint(\"\\n--- Preparing for Submission Phase ---\")\nprint(\"  Loading artifacts from training output...\")\n# The artifacts dictionary is already populated from the training phase.\n# We just need to load the model.\n\nprint(\"  Loading best model from training...\")\n# The device is already defined from the training phase.\ninference_model = TwoBranchModel(\n    artifacts['pad_len'], artifacts['imu_dim_final'], \n    artifacts['tof_thm_aggregated_dim_final'], len(artifacts['gesture_classes'])\n)\n# In PyTorch >= 2.6, weights_only defaults to True. We must set it to False \n# to load the full checkpoint which includes optimizer state and other python objects.\ncheckpoint = torch.load(EXPORT_DIR / \"best_checkpoint.pth\", map_location=device, weights_only=False)\ninference_model.load_state_dict(checkpoint['model_state_dict'])\ninference_model.to(device)\ninference_model.eval()\nprint(\"✔ Artifacts loaded. Ready for evaluation.\")\n\n\n# 3. Define the prediction function using the loaded model\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"Predicts a gesture from a sequence, applying the same feature engineering as in training.\"\"\"\n    df_seq = sequence.to_pandas()\n    \n    # --- Feature Engineering (must be identical to training) ---\n    df_seq['acc_mag'] = np.sqrt(df_seq['acc_x']**2 + df_seq['acc_y']**2 + df_seq['acc_z']**2)\n    df_seq['rot_angle'] = 2 * np.arccos(df_seq['rot_w'].clip(-1, 1))\n    for col in ['acc_x', 'acc_y', 'acc_z', 'acc_mag', 'rot_x', 'rot_y', 'rot_z', 'rot_angle']:\n        df_seq[f'{col}_vel'] = df_seq[col].diff().fillna(0)\n        df_seq[f'{col}_acc'] = df_seq[f'{col}_vel'].diff().fillna(0)\n\n    window_sizes = [5, 10]\n    feature_cols_for_rolling = ['acc_mag', 'acc_mag_vel', 'acc_mag_acc']\n    for window in window_sizes:\n        for col in feature_cols_for_rolling:\n            df_seq[f'{col}_roll_mean_{window}'] = df_seq[col].rolling(window, min_periods=1, center=True).mean()\n            df_seq[f'{col}_roll_std_{window}'] = df_seq[col].rolling(window, min_periods=1, center=True).std()\n\n    tof_pixel_cols = [f\"tof_{i}_v{p}\" for i in range(1, 6) for p in range(64)]\n    tof_data_np = df_seq[tof_pixel_cols].replace(-1, np.nan).to_numpy()\n    reshaped_tof = tof_data_np.reshape(len(df_seq), 5, 64)\n    with warnings.catch_warnings():\n        warnings.simplefilter('ignore', category=RuntimeWarning)\n        mean_vals, std_vals = np.nanmean(reshaped_tof, axis=2), np.nanstd(reshaped_tof, axis=2)\n        min_vals, max_vals = np.nanmin(reshaped_tof, axis=2), np.nanmax(reshaped_tof, axis=2)\n    for i in range(1, 6):\n        df_seq[f'tof_{i}_mean'], df_seq[f'tof_{i}_std'] = mean_vals[:, i-1], std_vals[:, i-1]\n        df_seq[f'tof_{i}_min'], df_seq[f'tof_{i}_max'] = min_vals[:, i-1], max_vals[:, i-1]\n    \n    # --- Final Processing ---\n    mat_unscaled = df_seq[artifacts['final_feature_cols']].ffill().bfill().fillna(0).values.astype('float32')\n    mat_scaled = artifacts['feature_scaler'].transform(mat_unscaled)\n    padded_sequence = np.zeros((artifacts['pad_len'], len(artifacts['final_feature_cols'])), dtype='float32')\n    seq_len = min(len(mat_scaled), artifacts['pad_len'])\n    padded_sequence[:seq_len] = mat_scaled[:seq_len]\n    model_input = torch.from_numpy(padded_sequence).float().unsqueeze(0).to(device)\n\n    with torch.no_grad():\n        outputs = inference_model(model_input)\n        predicted_idx = torch.argmax(outputs, dim=1).item()\n        \n    return str(artifacts['gesture_classes'][predicted_idx])\n\n\n# 4. Start the inference server for submission\nprint(\"\\n--- Starting Inference Server for Submission ---\")\ntry:\n    import kaggle_evaluation.cmi_inference_server\n    # The official submission environment uses KAGGLE_IS_COMPETITION_RERUN.\n    # We run the server if this script is executed in that environment.\n    # As requested, removing the compatibility check and forcing the submission server to start.\n    print(\"Starting the inference server for submission.\")\n    inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n    inference_server.serve()\n    print(\"--- Server finished ---\")\nexcept ImportError:\n    print(\"Could not import Kaggle environment, skipping server.\")\nexcept Exception as e:\n    print(f\"An error occurred during submission server setup: {e}\")\n\nprint(\"\\n--- Submission Script Finished ---\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}