{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Let's try using a 1D CNN!\n\nA friend told me they are good for dealing with time-series data like this.\n\n### 50% of Hidden Test Set is IMU-only!\n* \"Half of the hidden-test sequences are recorded with IMU only; the thermopile (thm_) and time-of-flight (tof__v*) columns are still present but contain null values for those sequences.\"\n* https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/data\n\nEasy approach: **Just use the IMU data.**\n\nAlso ignoring demographics data for now.\n\n*Friendly Reminder: If re-using large parts of this work in a public notebook - **please credit where you found the code**.*","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport random\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom tensorflow.keras.models import Sequential, load_model\nfrom tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout, GlobalMaxPooling1D, BatchNormalization\nfrom tensorflow.keras.utils import to_categorical\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\n\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences\nimport tensorflow as tf\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\n\n# Set global seed for reproducibility\nSEED = 42\nos.environ['PYTHONHASHSEED'] = str(SEED)\nrandom.seed(SEED)\nnp.random.seed(SEED)\ntf.random.set_seed(SEED)\n\nprint(\"Imports loaded\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:32:27.532984Z","iopub.execute_input":"2025-08-23T10:32:27.533191Z","iopub.status.idle":"2025-08-23T10:32:44.441763Z","shell.execute_reply.started":"2025-08-23T10:32:27.533164Z","shell.execute_reply":"2025-08-23T10:32:44.440953Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Load Train Data","metadata":{}},{"cell_type":"code","source":"# Load the dataset\nprint(\"Loading dataset...\")\ndf = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\nprint(f\"Loaded {len(df)} rows.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:33:12.964775Z","iopub.execute_input":"2025-08-23T10:33:12.965337Z","iopub.status.idle":"2025-08-23T10:33:42.621606Z","shell.execute_reply.started":"2025-08-23T10:33:12.965301Z","shell.execute_reply":"2025-08-23T10:33:42.620821Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Encode gesture (our target)\n* Save encodings to file (in case we want to use model in another notebook)","metadata":{}},{"cell_type":"code","source":"label_encoder = LabelEncoder()\ndf['gesture'] = label_encoder.fit_transform(df['gesture'].astype(str))\n\n# Save class names for inference\nnp.save('gesture_classes.npy', label_encoder.classes_)\n\n# Print class label mapping\nprint(\"Gesture label mapping:\")\nfor idx, label in enumerate(label_encoder.classes_):\n    print(f\"  {idx}: {label}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:42:55.194014Z","iopub.execute_input":"2025-08-23T10:42:55.194640Z","iopub.status.idle":"2025-08-23T10:42:55.298858Z","shell.execute_reply.started":"2025-08-23T10:42:55.194613Z","shell.execute_reply":"2025-08-23T10:42:55.298057Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 50% of Hidden Test Set is IMU-only!\n* \"Half of the hidden-test sequences are recorded with IMU only; the thermopile (thm_) and time-of-flight (tof__v*) columns are still present but contain null values for those sequences.\"\n* https://www.kaggle.com/competitions/cmi-detect-behavior-with-sensor-data/data\n* Is our train data like that?  **NO!**","metadata":{}},{"cell_type":"code","source":"print(\"Checking for IMU-only sequences...\")\n\ndef check_for_imu_only_seqs():\n    # Identify thermopile and TOF columns\n    thermal_tof_cols = [col for col in df.columns if col.startswith('thm_') or col.startswith('tof_')]\n    \n    # Group by seq|uence and check if all thm_/tof_ values are null\n    imu_only_flags = df[thermal_tof_cols].isna().groupby(df['sequence_id']).all().all(axis=1)\n    \n    # Report statistics\n    total_sequences = df['sequence_id'].nunique()\n    imu_only_count = imu_only_flags.sum()\n    imu_only_pct = (imu_only_count / total_sequences) * 100\n    \n    print(f\"Total sequences: {total_sequences}\")\n    print(f\"IMU-only sequences (all thm_/tof_ null): {imu_only_count} ({imu_only_pct:.1f}%)\")\n\ncheck_for_imu_only_seqs()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:42:59.200706Z","iopub.execute_input":"2025-08-23T10:42:59.201003Z","iopub.status.idle":"2025-08-23T10:43:00.902993Z","shell.execute_reply.started":"2025-08-23T10:42:59.200981Z","shell.execute_reply":"2025-08-23T10:43:00.902142Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Define Feature Columns (IMU-only)\n* Exclude train-only data","metadata":{}},{"cell_type":"code","source":"excluded_cols = {\n    'gesture', 'sequence_type', 'behavior', 'orientation',  # train-only\n    'row_id', 'subject', 'phase',  # metadata\n    'sequence_id', 'sequence_counter'  # identifiers\n}\n\n# Setting this true makes model ignore thermal and tof data\ndrop_thermal_and_tof = True\n\nif drop_thermal_and_tof:\n    thermal_tof_cols = [col for col in df.columns if col.startswith('thm_') or col.startswith('tof_')]\n    excluded_cols.update(thermal_tof_cols)\n    print(f\"Ignoring {len(thermal_tof_cols)} thermopile / time-of-flight columns.\")\n\n# Select numeric feature columns\nfeature_cols = [col for col in df.columns if col not in excluded_cols]\nprint(f\"Using {len(feature_cols)} numeric feature columns for training:\")\nprint(feature_cols)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:04.254503Z","iopub.execute_input":"2025-08-23T10:43:04.255034Z","iopub.status.idle":"2025-08-23T10:43:04.260477Z","shell.execute_reply.started":"2025-08-23T10:43:04.255012Z","shell.execute_reply":"2025-08-23T10:43:04.259616Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Check for Missing Values\n* Yup - there are a bunch - we'll take care of these...","metadata":{}},{"cell_type":"code","source":"# Check for NaNs in selected feature columns\nnan_counts = df[feature_cols].isna().sum()\ntotal_nans = nan_counts.sum()\nprint(f\"\\nTotal missing values in feature columns: {total_nans}\")\nif total_nans > 0:\n    print(\"Columns with missing values:\")\n    print(nan_counts[nan_counts > 0])\nelse:\n    print(\"No missing values found in feature columns.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:06.071177Z","iopub.execute_input":"2025-08-23T10:43:06.071740Z","iopub.status.idle":"2025-08-23T10:43:06.095906Z","shell.execute_reply.started":"2025-08-23T10:43:06.071715Z","shell.execute_reply":"2025-08-23T10:43:06.095300Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Pre-process data: scale and simple impute\n* Fill missing values: forward-fill, then back-fill, then fill remaining with 0\n* Scale features to zero mean and unit variance\n* We call this function as part of building input sequences and doing inference","metadata":{}},{"cell_type":"code","source":"def preprocess_sequence(df_sequence: pd.DataFrame, feature_cols: list) -> np.ndarray:\n    data = df_sequence[feature_cols].copy()\n    data = data.ffill().bfill().fillna(0)\n    scaled = StandardScaler().fit_transform(data)\n    return scaled","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:08.057577Z","iopub.execute_input":"2025-08-23T10:43:08.057856Z","iopub.status.idle":"2025-08-23T10:43:08.062182Z","shell.execute_reply.started":"2025-08-23T10:43:08.057835Z","shell.execute_reply":"2025-08-23T10:43:08.061385Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Build input sequences\n* All sequences must be the same length to train the model\n* Pad or truncate to a length that covers 90% of sequence lengths\n* This defines the fixed input size for our 1D CNN","metadata":{}},{"cell_type":"code","source":"# Build sequences\nsequence_ids = df['sequence_id'].unique()\nsequences = df.groupby('sequence_id')\n\nX = []\nseq_lengths = []\n\nprint(\"Building sequences...\")\nfor i, (seq_id, seq) in enumerate(sequences):\n    if i % 500 == 0:\n        print(f\"Processing sequence {i}...\")\n    processed = preprocess_sequence(seq, feature_cols)\n    X.append(processed)\n    seq_lengths.append(processed.shape[0])\n\nmax_len_perentile = 90\n\n# Report sequence length stats\nminlen = min(seq_lengths)\navglen = int(np.mean(seq_lengths))\npad_len_to_use = int(np.percentile(seq_lengths, max_len_perentile))  \nprint(f\"Sequence length stats - Min: {minlen}, Avg: {avglen}, {max_len_perentile}th percentile: {pad_len_to_use}\")\nprint(f\"Padding / truncating all sequences to fixed length {pad_len_to_use}...\")\n\nnp.save(\"sequence_maxlen.npy\", pad_len_to_use)  # Save for inference\n\n# Pad/truncate to fixed length\nX = pad_sequences(X, maxlen=pad_len_to_use, dtype='float32', padding='post', truncating='post')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:09.826868Z","iopub.execute_input":"2025-08-23T10:43:09.827498Z","iopub.status.idle":"2025-08-23T10:43:28.238764Z","shell.execute_reply.started":"2025-08-23T10:43:09.827472Z","shell.execute_reply":"2025-08-23T10:43:28.238169Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Prepare target labels as one-hot vectors\n* Use the first gesture label from each sequence as its target class (already converted to integer earlier)\n* Convert integer class labels to one-hot vectors for training (required for categorical cross-entropy)\n* The number of unique labels defines the model's output layer size","metadata":{}},{"cell_type":"code","source":"# Use groupby to get the first gesture per sequence (already integer-encoded)\ny = df.groupby('sequence_id')['gesture'].first().values\n\nprint(\"Integer labels:\", y[:4])\n\n# Convert to one-hot vectors\nnum_classes = len(np.unique(y))\ny = to_categorical(y, num_classes=num_classes)\n\nprint(\"After one-hot encoding:\", y[:4])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:32.631355Z","iopub.execute_input":"2025-08-23T10:43:32.632095Z","iopub.status.idle":"2025-08-23T10:43:32.678685Z","shell.execute_reply.started":"2025-08-23T10:43:32.632066Z","shell.execute_reply":"2025-08-23T10:43:32.677887Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train / test split","metadata":{}},{"cell_type":"code","source":"X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:34.801548Z","iopub.execute_input":"2025-08-23T10:43:34.801821Z","iopub.status.idle":"2025-08-23T10:43:34.812340Z","shell.execute_reply.started":"2025-08-23T10:43:34.801802Z","shell.execute_reply":"2025-08-23T10:43:34.811650Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Build, compile, and train 1D CNN model\n* Use Conv1D layers to extract temporal patterns from sensor sequences\n* Each Conv1D layer uses:\n  - `kernel_size` to define the number of time steps it looks at (temporal window)\n  - `filters` to define how many distinct patterns it tries to learn at each layer\n* Apply max pooling and dropout for regularization and dimensionality reduction\n* Flatten and pass through dense layers for final classification\n* Save trained model to disk for inference","metadata":{}},{"cell_type":"code","source":"%%time\nfrom tensorflow.keras.callbacks import Callback, EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.layers import BatchNormalization\nfrom tensorflow.keras.models import Sequential\nfrom tensorflow.keras.layers import Conv1D, MaxPooling1D, GlobalMaxPooling1D, Dropout, Dense\nfrom sklearn.metrics import f1_score\nimport numpy as np\n\n# ===== Custom Callback to track Binary F1, Macro F1, Overall Score =====\nclass F1Metrics(Callback):\n    def __init__(self, X_val, y_val):\n        super(F1Metrics, self).__init__()\n        self.X_val = X_val\n        self.y_val = y_val\n        self.history = []\n\n    def on_epoch_end(self, epoch, logs=None):\n        if logs is None:\n            logs = {}\n\n        # True labels (int)\n        y_true = np.argmax(self.y_val, axis=1)\n        # Predictions\n        y_pred = np.argmax(self.model.predict(self.X_val, verbose=0), axis=1)\n\n        # Binary mapping: BFRB vs Non-BFRB (class 0 = Non-BFRB, >0 = BFRB)\n        y_true_binary = (y_true > 0).astype(int)\n        y_pred_binary = (y_pred > 0).astype(int)\n\n        # Compute metrics\n        binary_f1 = f1_score(y_true_binary, y_pred_binary, average=\"binary\")\n        macro_f1  = f1_score(y_true, y_pred, average=\"macro\")\n        overall   = (binary_f1 + macro_f1) / 2\n\n        # Add to logs so Keras shows them in training history\n        logs[\"val_binary_f1\"] = binary_f1\n        logs[\"val_macro_f1\"] = macro_f1\n        logs[\"val_overall_score\"] = overall\n\n        # Save history for later\n        self.history.append({\n            \"epoch\": epoch+1,\n            \"binary_f1\": binary_f1,\n            \"macro_f1\": macro_f1,\n            \"overall\": overall\n        })\n\n    def on_train_end(self, logs=None):\n        # Final evaluation after training ends\n        y_true = np.argmax(self.y_val, axis=1)\n        y_pred = np.argmax(self.model.predict(self.X_val, verbose=0), axis=1)\n\n        y_true_binary = (y_true > 0).astype(int)\n        y_pred_binary = (y_pred > 0).astype(int)\n\n        binary_f1 = f1_score(y_true_binary, y_pred_binary, average=\"binary\")\n        macro_f1  = f1_score(y_true, y_pred, average=\"macro\")\n        overall   = (binary_f1 + macro_f1) / 2\n\n        print(\"\\n===== FINAL VALIDATION METRICS =====\")\n        print(f\"Binary F1 (BFRB vs Non-BFRB): {binary_f1:.4f}\")\n        print(f\"Macro F1 (multi-class):       {macro_f1:.4f}\")\n        print(f\"Overall Score (CMI metric):   {overall:.4f}\")\n\n\n# ===== Build 1D CNN model =====\nmodel = Sequential([\n    # Block 1 \n    Conv1D(filters=512, kernel_size=7, activation='relu', input_shape=(X_train.shape[1], X_train.shape[2])),\n    BatchNormalization(),\n    MaxPooling1D(pool_size=2),\n    Dropout(0.3),\n    \n    # Block 2 \n    Conv1D(filters=768, kernel_size=5, activation='relu'),\n    BatchNormalization(),\n    MaxPooling1D(pool_size=2),\n    Dropout(0.3),\n    \n    # Block 3\n    Conv1D(filters=1024, kernel_size=3, activation='relu'),\n    BatchNormalization(),\n    MaxPooling1D(pool_size=2),\n    Dropout(0.4),\n    \n    # Block 4\n    Conv1D(filters=1536, kernel_size=3, activation='relu'),\n    BatchNormalization(),\n    MaxPooling1D(pool_size=2),\n    Dropout(0.4),\n    \n    # Block 5\n    Conv1D(filters=2048, kernel_size=3, activation='relu'),\n    BatchNormalization(),\n    GlobalMaxPooling1D(),\n    Dropout(0.5),\n    \n    # Dense layers\n    Dense(2048, activation='relu'),\n    Dropout(0.5),\n    Dense(1024, activation='relu'),\n    Dropout(0.4),\n    Dense(512, activation='relu'),\n    Dropout(0.3),\n    \n    # Output\n    Dense(num_classes, activation='softmax')\n])\n\n# Compile\nmodel.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])\nmodel.summary()\n\n# Callbacks\nearly_stopping = EarlyStopping(\n    monitor='val_accuracy', patience=10,\n    restore_best_weights=True, verbose=1\n)\n\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_accuracy', factor=0.7,\n    patience=3, min_lr=1e-7, verbose=1\n)\n\nf1_metrics = F1Metrics(X_val, y_val)\n\n# Training\nprint(\"Training model...\")\nhistory = model.fit(\n    X_train, y_train,\n    epochs=150,\n    batch_size=64,\n    validation_data=(X_val, y_val),\n    callbacks=[early_stopping, reduce_lr, f1_metrics],\n    verbose=1\n)\n\nmodel.save(\"gesture_cnn_model.h5\")\nprint(\"Training complete.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:43:52.330277Z","iopub.execute_input":"2025-08-23T10:43:52.331024Z","iopub.status.idle":"2025-08-23T10:48:20.537731Z","shell.execute_reply.started":"2025-08-23T10:43:52.330992Z","shell.execute_reply":"2025-08-23T10:48:20.537101Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Use Competition Metric for LB Estimate\n* We import: https://www.kaggle.com/code/richolson/cmi-2025-metric-copy-for-import\n* Which is a copy of: https://www.kaggle.com/code/metric/cmi-2025","metadata":{}},{"cell_type":"code","source":"from cmi_2025_metric_copy_for_import import CompetitionMetric\n\n# Get predicted labels for the validation set\nprint(\"Predicting on validation set...\")\ny_val_pred_probs = model.predict(X_val, verbose=0)\ny_val_pred = np.argmax(y_val_pred_probs, axis=1)\ny_val_true = np.argmax(y_val, axis=1)\n\n# Map integer labels back to gesture strings\ngesture_classes = np.load(\"gesture_classes.npy\", allow_pickle=True)\nval_pred_labels = pd.Series(y_val_pred).map(lambda i: gesture_classes[i])\nval_true_labels = pd.Series(y_val_true).map(lambda i: gesture_classes[i])\n\n# Build DataFrames for the metric\nval_submission = pd.DataFrame({'gesture': val_pred_labels})\nval_solution = pd.DataFrame({'gesture': val_true_labels})\n\n# Run competition metric\nmetric = CompetitionMetric()\nscore = metric.calculate_hierarchical_f1(val_solution, val_submission)\nprint(f\"Estimated leaderboard (val) score: {score:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:54:02.197930Z","iopub.execute_input":"2025-08-23T10:54:02.198218Z","iopub.status.idle":"2025-08-23T10:54:03.735297Z","shell.execute_reply.started":"2025-08-23T10:54:02.198199Z","shell.execute_reply":"2025-08-23T10:54:03.734499Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Predict function for inference server\n* Runs same preprocess_sequence function on inference data as we did for training\n* Pads / truncates to sequences to same length as for training\n* Loads trained model and predicts gesture class\n* Maps predicted index back to original gesture label","metadata":{}},{"cell_type":"code","source":"# loading model and setup outside of predict function to reduce overhead\nmodel = load_model(\"gesture_cnn_model.h5\")\nmaxlen = int(np.load(\"sequence_maxlen.npy\"))  # ensure consistent shape\ngesture_classes = np.load(\"gesture_classes.npy\", allow_pickle=True)\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_seq = sequence.to_pandas()\n    \n    processed = preprocess_sequence(df_seq, feature_cols)\n    padded = pad_sequences([processed], maxlen=maxlen, dtype='float32', padding='post', truncating='post')\n    prediction = model.predict(padded, verbose=0)\n    predicted_index = np.argmax(prediction, axis=1)[0]\n    return gesture_classes[predicted_index]\n\n# Launch inference server\ninference_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-23T10:54:06.831009Z","iopub.execute_input":"2025-08-23T10:54:06.831510Z","iopub.status.idle":"2025-08-23T10:54:09.688229Z","shell.execute_reply.started":"2025-08-23T10:54:06.831486Z","shell.execute_reply":"2025-08-23T10:54:09.687608Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Verify prediction function","metadata":{}},{"cell_type":"code","source":"# Manual test (only runs outside Kaggle gateway)\nif not os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    print(\"\\nRunning manual test...\")\n    test_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv')\n    sample_seq_id = test_df['sequence_id'].unique()[0]\n    test_seq = test_df[test_df['sequence_id'] == sample_seq_id]\n    prediction = predict(pl.DataFrame(test_seq), None)\n    print(f\"Manual prediction result for sequence_id {sample_seq_id}: {prediction}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-23T10:54:17.438171Z","iopub.execute_input":"2025-08-23T10:54:17.438811Z","iopub.status.idle":"2025-08-23T10:54:17.552504Z","shell.execute_reply.started":"2025-08-23T10:54:17.438788Z","shell.execute_reply":"2025-08-23T10:54:17.551812Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}