{"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":"gpu","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":"# 1D CNN IMU only \n\nThis codes is adapted from: https://www.kaggle.com/code/richolson/cmi-2025-1d-cnn-imu-only-baseline.\n","metadata":{}},{"cell_type":"markdown","source":"# Load Libraries and Set Up\n\nSet a random seed: This makes sure that every time we run the code, we get the same result — useful for testing and fairness.","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-03T17:10:25.546084Z","iopub.execute_input":"2025-08-03T17:10:25.546335Z","iopub.status.idle":"2025-08-03T17:10:39.543438Z","shell.execute_reply.started":"2025-08-03T17:10:25.546313Z","shell.execute_reply":"2025-08-03T17:10:39.542676Z"}},"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-03T17:10:39.544475Z","iopub.execute_input":"2025-08-03T17:10:39.544939Z","iopub.status.idle":"2025-08-03T17:11:09.482261Z","shell.execute_reply.started":"2025-08-03T17:10:39.544920Z","shell.execute_reply":"2025-08-03T17:11:09.481631Z"}},"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)\n\nIn this step, we convert the gesture labels (like \"Hair Pulling\", \"Wave Hello\", etc.) from words into numbers, because machine learning models can only understand numbers — not text.\n\nWe use something called a LabelEncoder to:\n\n- Assign each gesture a unique number (e.g., \"Above ear - pull hair\" → 0, \"Eyelash - pull hair\" → 4)\n\n- Replace the text labels in the dataset with these numbers\n\nSave the list of class names (for use later during prediction, so we can turn numbers back into gesture names)\n\nFinally, we print out the mapping so we know which number means which gesture.\n\nThis step is super important, because the model will output numbers like 0, 1, 2 — and without this mapping, we wouldn’t know what those numbers mean!","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-03T17:11:09.482940Z","iopub.execute_input":"2025-08-03T17:11:09.483142Z","iopub.status.idle":"2025-08-03T17:11:09.583771Z","shell.execute_reply.started":"2025-08-03T17:11:09.483124Z","shell.execute_reply":"2025-08-03T17:11:09.583212Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 50% of Hidden Test Set is IMU-only!\n\nIn the test set, **50% of sequences only include IMU data** — the other sensors (thermopile and TOF) are missing.\n\nBut in our training set, we found that **only 1.2% of the data is IMU-only**.\n\nThis means our model might **not learn well from IMU-only data** if we train on all data together.\n\n*It's important to check this early. It tells us that we may want to train a separate model just using IMU features, or use other tricks to make the model more robust.*","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 sequence 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-03T17:11:09.585181Z","iopub.execute_input":"2025-08-03T17:11:09.585400Z","iopub.status.idle":"2025-08-03T17:11:11.100818Z","shell.execute_reply.started":"2025-08-03T17:11:09.585383Z","shell.execute_reply":"2025-08-03T17:11:11.100128Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Define Feature Columns (IMU-only)\n\n* In this section, we **select the sensor features we want to use** to train our model.\n\n- We are **training only with IMU data**, so we drop all thermopile (`thm_`) and time-of-flight (`tof_`) columns.\n- We also remove other columns that are **not useful for prediction**, like labels and metadata (`gesture`, `subject`, `sequence_id`, etc.).\n- What’s left are **7 IMU features**:\n  - `acc_x`, `acc_y`, `acc_z`: acceleration signals\n  - `rot_w`, `rot_x`, `rot_y`, `rot_z`: rotation/orientation signals\n\nThis step makes sure the model only sees **data that will also exist in the test set** — so we don’t “cheat” by using extra information not available during testing.","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-03T17:11:11.101576Z","iopub.execute_input":"2025-08-03T17:11:11.101895Z","iopub.status.idle":"2025-08-03T17:11:11.107475Z","shell.execute_reply.started":"2025-08-03T17:11:11.101844Z","shell.execute_reply":"2025-08-03T17:11:11.106817Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Check for Missing Values\n\nBefore training a model, we need to make sure the input data is clean.  \nThis step checks whether any of the selected feature columns (our IMU data) are missing values (NaNs).\n\nMissing values can cause problems during model training, because models expect complete numeric input, not blanks.\n\nIf we find missing values, we’ll handle them later.  \nThis is a basic but important step in any machine learning pipeline.\n","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-03T17:11:11.108213Z","iopub.execute_input":"2025-08-03T17:11:11.108489Z","iopub.status.idle":"2025-08-03T17:11:11.139081Z","shell.execute_reply.started":"2025-08-03T17:11:11.108463Z","shell.execute_reply":"2025-08-03T17:11:11.138518Z"}},"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-03T17:11:11.139672Z","iopub.execute_input":"2025-08-03T17:11:11.139875Z","iopub.status.idle":"2025-08-03T17:11:11.143983Z","shell.execute_reply.started":"2025-08-03T17:11:11.139857Z","shell.execute_reply":"2025-08-03T17:11:11.143240Z"}},"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-03T17:11:11.144596Z","iopub.execute_input":"2025-08-03T17:11:11.144868Z","iopub.status.idle":"2025-08-03T17:11:29.481361Z","shell.execute_reply.started":"2025-08-03T17:11:11.144847Z","shell.execute_reply":"2025-08-03T17:11:29.480736Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"X.shape # tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T17:11:29.482200Z","iopub.execute_input":"2025-08-03T17:11:29.482377Z","iopub.status.idle":"2025-08-03T17:11:29.487941Z","shell.execute_reply.started":"2025-08-03T17:11:29.482363Z","shell.execute_reply":"2025-08-03T17:11:29.487253Z"}},"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":"markdown","source":"labels: 0, 1, 2, 3, 4, 5...17","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-03T17:11:29.490106Z","iopub.execute_input":"2025-08-03T17:11:29.490312Z","iopub.status.idle":"2025-08-03T17:11:29.538198Z","shell.execute_reply.started":"2025-08-03T17:11:29.490297Z","shell.execute_reply":"2025-08-03T17:11:29.537476Z"}},"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) #42","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T17:11:29.538926Z","iopub.execute_input":"2025-08-03T17:11:29.539149Z","iopub.status.idle":"2025-08-03T17:11:29.548820Z","shell.execute_reply.started":"2025-08-03T17:11:29.539126Z","shell.execute_reply":"2025-08-03T17:11:29.548142Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Build, compile, and train 1D CNN model\n\nThis CNN model has 5 similar blocks.  \nEach block looks at motion patterns, cleans the data, and keeps only the important parts.  \nAs we go deeper from Block 1 to Block 5, the model learns more complex features.\n\nAfter that, the Dense layers act like decision-makers — they take all the features and figure out which gesture it is.\n\nSo in short:\n- Blocks 1–5 → Find patterns in motion data  \n- Dense layers → Use those patterns to predict the gesture","metadata":{}},{"cell_type":"code","source":"%%time\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau\nfrom tensorflow.keras.layers import BatchNormalization\n\n# Build 1D CNN model\n\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(),  # Alternative to MaxPooling1D + Flatten\n    Dropout(0.5),\n    \n    # Larger 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 model with categorical crossentropy loss (for one-hot labels)\nmodel.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])\nmodel.summary()\n\n# Define early stopping to prevent overfitting and restore best model\nearly_stopping = EarlyStopping(\n    monitor='val_accuracy',           \n    patience=10,\n    restore_best_weights=True,\n    verbose=1\n)\n\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_accuracy',      \n    factor=0.7,                 \n    patience=3,                  \n    min_lr=1e-7,                 \n    verbose=1\n)\n\n# Train model using explicitly split validation set (80/20 held out)\nprint(\"Training model...\")\nmodel.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]\n)\n\nmodel.save(\"gesture_cnn_model.h5\")\nprint(\"Training complete.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T17:11:29.549847Z","iopub.execute_input":"2025-08-03T17:11:29.550495Z","iopub.status.idle":"2025-08-03T17:15:24.562785Z","shell.execute_reply.started":"2025-08-03T17:11:29.550468Z","shell.execute_reply":"2025-08-03T17:15:24.562129Z"}},"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-03T17:15:24.563622Z","iopub.execute_input":"2025-08-03T17:15:24.564052Z","iopub.status.idle":"2025-08-03T17:15:26.735449Z","shell.execute_reply.started":"2025-08-03T17:15:24.564025Z","shell.execute_reply":"2025-08-03T17:15:26.734797Z"}},"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    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-03T17:15:26.736138Z","iopub.execute_input":"2025-08-03T17:15:26.736323Z","iopub.status.idle":"2025-08-03T17:15:29.581055Z","shell.execute_reply.started":"2025-08-03T17:15:26.736309Z","shell.execute_reply":"2025-08-03T17:15:29.580243Z"}},"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-03T17:15:29.581928Z","iopub.execute_input":"2025-08-03T17:15:29.582152Z","iopub.status.idle":"2025-08-03T17:15:29.694901Z","shell.execute_reply.started":"2025-08-03T17:15:29.582134Z","shell.execute_reply":"2025-08-03T17:15:29.694256Z"}},"outputs":[],"execution_count":null}]}