{"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":"# 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-06-09T15:48:11.005788Z","iopub.execute_input":"2025-06-09T15:48:11.006007Z","iopub.status.idle":"2025-06-09T15:48:11.024031Z","shell.execute_reply.started":"2025-06-09T15:48:11.005991Z","shell.execute_reply":"2025-06-09T15:48:11.023218Z"}},"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-06-09T15:48:11.025593Z","iopub.execute_input":"2025-06-09T15:48:11.025824Z","iopub.status.idle":"2025-06-09T15:48:30.379486Z","shell.execute_reply.started":"2025-06-09T15:48:11.025801Z","shell.execute_reply":"2025-06-09T15:48:30.378474Z"}},"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-06-09T15:48:30.380276Z","iopub.execute_input":"2025-06-09T15:48:30.380556Z","iopub.status.idle":"2025-06-09T15:48:30.488866Z","shell.execute_reply.started":"2025-06-09T15:48:30.380535Z","shell.execute_reply":"2025-06-09T15:48:30.487877Z"}},"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 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-06-09T15:48:30.489854Z","iopub.execute_input":"2025-06-09T15:48:30.490178Z","iopub.status.idle":"2025-06-09T15:48:32.071871Z","shell.execute_reply.started":"2025-06-09T15:48:30.490148Z","shell.execute_reply":"2025-06-09T15:48:32.070956Z"}},"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-06-09T15:48:32.072933Z","iopub.execute_input":"2025-06-09T15:48:32.073264Z","iopub.status.idle":"2025-06-09T15:48:32.079621Z","shell.execute_reply.started":"2025-06-09T15:48:32.073243Z","shell.execute_reply":"2025-06-09T15:48:32.078709Z"}},"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-06-09T15:48:32.080452Z","iopub.execute_input":"2025-06-09T15:48:32.080686Z","iopub.status.idle":"2025-06-09T15:48:32.117657Z","shell.execute_reply.started":"2025-06-09T15:48:32.080659Z","shell.execute_reply":"2025-06-09T15:48:32.116936Z"}},"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-06-09T15:48:32.120775Z","iopub.execute_input":"2025-06-09T15:48:32.120999Z","iopub.status.idle":"2025-06-09T15:48:32.125320Z","shell.execute_reply.started":"2025-06-09T15:48:32.120981Z","shell.execute_reply":"2025-06-09T15:48:32.124449Z"}},"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-06-09T15:48:32.126201Z","iopub.execute_input":"2025-06-09T15:48:32.126876Z","iopub.status.idle":"2025-06-09T15:48:53.404673Z","shell.execute_reply.started":"2025-06-09T15:48:32.126855Z","shell.execute_reply":"2025-06-09T15:48:53.403685Z"}},"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-06-09T15:48:53.406305Z","iopub.execute_input":"2025-06-09T15:48:53.406597Z","iopub.status.idle":"2025-06-09T15:48:53.453360Z","shell.execute_reply.started":"2025-06-09T15:48:53.406577Z","shell.execute_reply":"2025-06-09T15:48:53.452423Z"}},"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-06-09T15:48:53.454336Z","iopub.execute_input":"2025-06-09T15:48:53.454973Z","iopub.status.idle":"2025-06-09T15:48:53.465606Z","shell.execute_reply.started":"2025-06-09T15:48:53.454940Z","shell.execute_reply":"2025-06-09T15:48:53.464905Z"}},"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 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-06-09T15:48:53.466371Z","iopub.execute_input":"2025-06-09T15:48:53.466613Z","iopub.status.idle":"2025-06-09T15:52:45.942825Z","shell.execute_reply.started":"2025-06-09T15:48:53.466588Z","shell.execute_reply":"2025-06-09T15:52:45.941724Z"}},"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-06-09T15:52:52.687453Z","iopub.execute_input":"2025-06-09T15:52:52.688287Z","iopub.status.idle":"2025-06-09T15:52:54.046970Z","shell.execute_reply.started":"2025-06-09T15:52:52.688258Z","shell.execute_reply":"2025-06-09T15:52:54.046256Z"}},"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-06-09T15:52:57.289246Z","iopub.execute_input":"2025-06-09T15:52:57.289965Z","iopub.status.idle":"2025-06-09T15:52:59.609837Z","shell.execute_reply.started":"2025-06-09T15:52:57.289937Z","shell.execute_reply":"2025-06-09T15:52:59.609181Z"}},"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-06-09T15:53:01.827627Z","iopub.execute_input":"2025-06-09T15:53:01.828477Z","iopub.status.idle":"2025-06-09T15:53:01.953051Z","shell.execute_reply.started":"2025-06-09T15:53:01.828450Z","shell.execute_reply":"2025-06-09T15:53:01.952162Z"}},"outputs":[],"execution_count":null}]}