{"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":"credit goes to this notebook and author\n\nhttps://www.kaggle.com/code/richolson/cmi-2025-1d-cnn-imu-only-baseline\n\nnew changed  here learning rate is changed exponential value","metadata":{}},{"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.","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\nfrom tensorflow.keras.utils import to_categorical\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-01T23:05:59.338594Z","iopub.execute_input":"2025-06-01T23:05:59.33892Z","iopub.status.idle":"2025-06-01T23:06:13.099566Z","shell.execute_reply.started":"2025-06-01T23:05:59.338884Z","shell.execute_reply":"2025-06-01T23:06:13.098888Z"}},"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-01T23:06:13.100668Z","iopub.execute_input":"2025-06-01T23:06:13.101099Z","iopub.status.idle":"2025-06-01T23:06:42.406749Z","shell.execute_reply.started":"2025-06-01T23:06:13.10108Z","shell.execute_reply":"2025-06-01T23:06:42.405974Z"}},"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-01T23:06:42.407416Z","iopub.execute_input":"2025-06-01T23:06:42.407626Z","iopub.status.idle":"2025-06-01T23:06:42.506501Z","shell.execute_reply.started":"2025-06-01T23:06:42.40761Z","shell.execute_reply":"2025-06-01T23:06:42.505777Z"}},"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-01T23:06:42.507244Z","iopub.execute_input":"2025-06-01T23:06:42.507502Z","iopub.status.idle":"2025-06-01T23:06:44.19635Z","shell.execute_reply.started":"2025-06-01T23:06:42.507463Z","shell.execute_reply":"2025-06-01T23:06:44.195752Z"}},"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-01T23:06:44.198111Z","iopub.execute_input":"2025-06-01T23:06:44.1983Z","iopub.status.idle":"2025-06-01T23:06:44.203909Z","shell.execute_reply.started":"2025-06-01T23:06:44.198286Z","shell.execute_reply":"2025-06-01T23:06:44.203192Z"}},"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-01T23:06:44.204575Z","iopub.execute_input":"2025-06-01T23:06:44.204808Z","iopub.status.idle":"2025-06-01T23:06:44.239443Z","shell.execute_reply.started":"2025-06-01T23:06:44.204792Z","shell.execute_reply":"2025-06-01T23:06:44.238703Z"}},"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-01T23:06:44.240136Z","iopub.execute_input":"2025-06-01T23:06:44.240321Z","iopub.status.idle":"2025-06-01T23:06:44.244958Z","shell.execute_reply.started":"2025-06-01T23:06:44.240306Z","shell.execute_reply":"2025-06-01T23:06:44.244189Z"}},"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-01T23:06:44.245712Z","iopub.execute_input":"2025-06-01T23:06:44.245999Z","iopub.status.idle":"2025-06-01T23:07:02.672846Z","shell.execute_reply.started":"2025-06-01T23:06:44.245975Z","shell.execute_reply":"2025-06-01T23:07:02.672057Z"}},"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-01T23:07:02.673861Z","iopub.execute_input":"2025-06-01T23:07:02.674079Z","iopub.status.idle":"2025-06-01T23:07:02.71754Z","shell.execute_reply.started":"2025-06-01T23:07:02.674062Z","shell.execute_reply":"2025-06-01T23:07:02.716876Z"}},"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-01T23:07:02.718348Z","iopub.execute_input":"2025-06-01T23:07:02.718646Z","iopub.status.idle":"2025-06-01T23:07:02.728236Z","shell.execute_reply.started":"2025-06-01T23:07:02.718617Z","shell.execute_reply":"2025-06-01T23:07:02.72766Z"}},"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\nmodel = Sequential([\n    # Block 1\n    Conv1D(filters=256, 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=384, kernel_size=5, activation='relu'),\n    BatchNormalization(),\n    MaxPooling1D(pool_size=2),\n    Dropout(0.3),\n\n    # Block 3\n    Conv1D(filters=512, kernel_size=3, activation='relu'),\n    BatchNormalization(),\n    MaxPooling1D(pool_size=2),\n    Dropout(0.4),\n\n    # Flatten and Dense\n    Flatten(),\n    Dense(512, activation='relu'),\n    Dropout(0.5),\n    Dense(256, activation='relu'),\n    Dropout(0.4),\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',      # Watch validation loss\n    patience=15,              # Stop if no improvement after 5 epochs\n    restore_best_weights=True,\n    verbose=1\n)\n\nreduce_lr = ReduceLROnPlateau(\n    monitor='val_accuracy',\n    factor=0.5,\n    patience=5,\n    min_lr=1e-8,\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=100,\n    batch_size=128,\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-01T23:08:24.377292Z","iopub.execute_input":"2025-06-01T23:08:24.37767Z","iopub.status.idle":"2025-06-01T23:09:40.983418Z","shell.execute_reply.started":"2025-06-01T23:08:24.377647Z","shell.execute_reply":"2025-06-01T23:09:40.982605Z"}},"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-01T23:09:46.271173Z","iopub.execute_input":"2025-06-01T23:09:46.271458Z","iopub.status.idle":"2025-06-01T23:09:47.292797Z","shell.execute_reply.started":"2025-06-01T23:09:46.271437Z","shell.execute_reply":"2025-06-01T23:09:47.292132Z"}},"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":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_seq = sequence.to_pandas()\n    processed = preprocess_sequence(df_seq, feature_cols)\n    maxlen = int(np.load(\"sequence_maxlen.npy\"))  # ensure consistent shape\n    padded = pad_sequences([processed], maxlen=maxlen, dtype='float32', padding='post', truncating='post')\n    model = load_model(\"gesture_cnn_model.h5\")\n    prediction = model.predict(padded, verbose=0)\n    predicted_index = np.argmax(prediction, axis=1)[0]\n    gesture_classes = np.load(\"gesture_classes.npy\", allow_pickle=True)\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-01T23:10:05.758456Z","iopub.execute_input":"2025-06-01T23:10:05.758755Z","iopub.status.idle":"2025-06-01T23:10:08.075843Z","shell.execute_reply.started":"2025-06-01T23:10:05.758736Z","shell.execute_reply":"2025-06-01T23:10:08.075219Z"}},"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-01T23:10:10.832356Z","iopub.execute_input":"2025-06-01T23:10:10.83324Z","iopub.status.idle":"2025-06-01T23:10:11.551219Z","shell.execute_reply.started":"2025-06-01T23:10:10.833205Z","shell.execute_reply":"2025-06-01T23:10:11.550543Z"}},"outputs":[],"execution_count":null}]}