{"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":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom tensorflow.keras.models import Model, load_model\nfrom tensorflow.keras.layers import (\n    Input, Conv1D, BatchNormalization, Activation, add,\n    MaxPooling1D, Dropout, Bidirectional, LSTM,\n    Dense, GlobalAveragePooling1D, Flatten, Permute,\n    multiply, Lambda, RepeatVector\n)\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping\nfrom tensorflow.keras.regularizers import l2\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\nfrom tensorflow.keras import backend as K\ndef time_sum(x):\n    return K.sum(x, axis=1)\n\nprint(\"Imports loaded\")\n\n# --------------------------------------------------\n# 1. Load and preprocess dataset\n# --------------------------------------------------\nprint(\"Loading dataset...\")\ndf = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\nprint(f\"Loaded {len(df)} rows.\")\n\nlabel_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\nprint(\"Gesture label mapping:\")\nfor idx, label in enumerate(label_encoder.classes_):\n    print(f\"  {idx}: {label}\")\n\n# Check IMU-only sequences\ndef check_for_imu_only_seqs():\n    thermal_tof_cols = [col for col in df.columns if col.startswith('thm_') or col.startswith('tof_')]\n    imu_only_flags = df[thermal_tof_cols].isna().groupby(df['sequence_id']).all().all(axis=1)\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    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()\n\nexcluded_cols = {\n    'gesture', 'sequence_type', 'behavior', 'orientation',  # train-only\n    'row_id', 'subject', 'phase',  # metadata\n    'sequence_id', 'sequence_counter'  # identifiers\n}\n\n# If True, drop thermal and tof columns\ndrop_thermal_and_tof = True\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)\n\n# Check for missing values\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.\")\n\ndef 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\n\n# 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\n# Determine padding length\nmax_len_percentile = 90\nminlen = min(seq_lengths)\navglen = int(np.mean(seq_lengths))\npad_len_to_use = int(np.percentile(seq_lengths, max_len_percentile))\nprint(f\"Sequence length stats - Min: {minlen}, Avg: {avglen}, {max_len_percentile}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')\n\n# Get labels (first gesture per sequence)\ny = df.groupby('sequence_id')['gesture'].first().values\n\nprint(\"Integer labels (first 4):\", y[:4])\nnum_classes = len(np.unique(y))\ny = to_categorical(y, num_classes=num_classes)\nprint(\"One-hot labels (first 4):\", y[:4])\n\n# Split into train/validation\nX_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)\n\n# --------------------------------------------------\n# 2. Build improved HAR model (Residual CNN + BiLSTM + Attention + Regularization)\n# --------------------------------------------------\ndef residual_cnn_block(x, filters, kernel_size, pool_size=2, dropout_rate=0.3, wd=1e-4):\n    \"\"\"\n    Residual block: Conv1D -> BN -> ReLU -> Conv1D -> BN -> Add skip -> ReLU -> MaxPool -> Dropout\n    \"\"\"\n    shortcut = x  # skip connection\n\n    # First Conv1D\n    x = Conv1D(\n        filters,\n        kernel_size,\n        padding='same',\n        kernel_regularizer=l2(wd),\n        use_bias=False\n    )(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    # Second Conv1D\n    x = Conv1D(\n        filters,\n        kernel_size,\n        padding='same',\n        kernel_regularizer=l2(wd),\n        use_bias=False\n    )(x)\n    x = BatchNormalization()(x)\n\n    # If channels don't match, adjust shortcut\n    if shortcut.shape[-1] != filters:\n        shortcut = Conv1D(\n            filters,\n            kernel_size=1,\n            padding='same',\n            kernel_regularizer=l2(wd),\n            use_bias=False\n        )(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n\n    # Add skip connection\n    x = add([x, shortcut])\n    x = Activation('relu')(x)\n\n    # Pooling + Dropout\n    x = MaxPooling1D(pool_size=pool_size)(x)\n    x = Dropout(dropout_rate)(x)\n    return x\n\ndef attention_layer(inputs):\n    \"\"\"\n    Simple attention mechanism over time dimension.\n    inputs: (batch, timesteps, features)\n    returns context vector of shape (batch, features)\n    \"\"\"\n    # Compute attention scores\n    score_dense = Dense(1, activation='tanh')(inputs)       # shape = (batch, timesteps, 1)\n    score_flat = Flatten()(score_dense)                      # shape = (batch, timesteps)\n    attention_weights = Activation('softmax')(score_flat)    # shape = (batch, timesteps)\n\n    # Expand dims to match features\n    attention_weights = RepeatVector(inputs.shape[-1])(attention_weights)  # (batch, features, timesteps)\n    attention_weights = Permute((2, 1))(attention_weights)                # (batch, timesteps, features)\n\n    # Weighted sum\n    weighted_sequence = multiply([inputs, attention_weights])              # (batch, timesteps, features)\n\n    # ----- Specify output_shape for Lambda to avoid inference error -----\n    # Reduce sum over time axis → (batch, features)\n    context_vector = Lambda(\n        time_sum,\n        output_shape=lambda inp_shape: (inp_shape[0], inp_shape[2]),\n        name=\"time_sum\"\n    )(weighted_sequence)\n\n    return context_vector\n\ndef build_improved_har_model(input_shape, num_classes, weight_decay=1e-4):\n    \"\"\"\n    Build a HAR model with:\n    - Residual CNN multi-scale blocks (kernel sizes 3,5,7)\n    - Bidirectional LSTM\n    - Attention layer\n    - Dense layers with L2 regularization and Dropout\n    \"\"\"\n    inputs = Input(shape=input_shape)  # e.g. (pad_len_to_use, num_channels)\n    x = inputs\n\n    # Residual CNN Block 1: kernel_size = 3\n    x = residual_cnn_block(\n        x,\n        filters=64,\n        kernel_size=3,\n        pool_size=2,\n        dropout_rate=0.3,\n        wd=weight_decay\n    )\n\n    # Residual CNN Block 2: kernel_size = 5\n    x = residual_cnn_block(\n        x,\n        filters=128,\n        kernel_size=5,\n        pool_size=2,\n        dropout_rate=0.3,\n        wd=weight_decay\n    )\n\n    # Residual CNN Block 3: kernel_size = 7\n    x = residual_cnn_block(\n        x,\n        filters=256,\n        kernel_size=7,\n        pool_size=2,\n        dropout_rate=0.4,\n        wd=weight_decay\n    )\n\n    # Bidirectional LSTM\n    x = Bidirectional(\n        LSTM(\n            units=128,\n            return_sequences=True,\n            kernel_regularizer=l2(weight_decay),\n            recurrent_regularizer=l2(weight_decay),\n            bias_regularizer=l2(weight_decay)\n        )\n    )(x)\n    x = Dropout(0.4)(x)\n\n    # Attention over LSTM outputs\n    x = attention_layer(x)  # shape = (batch, 128*2)\n\n    # Dense Layers with BatchNorm, Dropout, L2\n    x = Dense(\n        units=256,\n        activation=None,\n        kernel_regularizer=l2(weight_decay),\n        use_bias=False\n    )(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n    x = Dropout(0.5)(x)\n\n    x = Dense(\n        units=128,\n        activation=None,\n        kernel_regularizer=l2(weight_decay),\n        use_bias=False\n    )(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n    x = Dropout(0.3)(x)\n\n    outputs = Dense(\n        units=num_classes,\n        activation='softmax',\n        kernel_regularizer=l2(weight_decay)\n    )(x)\n\n    model = Model(inputs=inputs, outputs=outputs)\n    return model\n\nprint(\"Building improved model...\")\ninput_shape = (X_train.shape[1], X_train.shape[2])  # (pad_len_to_use, num_channels)\nmodel = build_improved_har_model(input_shape, num_classes, weight_decay=1e-4)\n\n# Compile model with Adam optimizer\nmodel.compile(\n    optimizer=Adam(learning_rate=1e-3),\n    loss='categorical_crossentropy',\n    metrics=['accuracy']\n)\nmodel.summary()\n\n# --------------------------------------------------\n# 3. Train the model\n# --------------------------------------------------\nprint(\"Training model...\")\ncallbacks = [\n    ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=5, verbose=1),\n    EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True, verbose=1)\n]\n\nmodel.fit(\n    X_train,\n    y_train,\n    validation_data=(X_val, y_val),\n    epochs=60,\n    batch_size=128,\n    callbacks=callbacks,\n    verbose=1\n)\nmodel.save(\"gesture_har_model_improved.h5\")\nprint(\"Training complete.\")\n\n# --------------------------------------------------\n# 4. Evaluate on validation set with custom metric\n# --------------------------------------------------\nfrom cmi_2025_metric_copy_for_import import CompetitionMetric\n\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\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\nval_submission = pd.DataFrame({'gesture': val_pred_labels})\nval_solution = pd.DataFrame({'gesture': val_true_labels})\n\nmetric = CompetitionMetric()\nscore = metric.calculate_hierarchical_f1(val_solution, val_submission)\nprint(f\"Estimated leaderboard (val) score: {score:.4f}\")\n\n# --------------------------------------------------\n# 5. Inference function for Kaggle evaluation\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    maxlen = int(np.load(\"sequence_maxlen.npy\"))\n    padded = pad_sequences([processed], maxlen=maxlen, dtype='float32',\n                           padding='post', truncating='post')\n\n    model = load_model(\n        \"gesture_har_model_improved.h5\",\n        compile=False,\n        custom_objects={'time_sum': time_sum}\n    )\n\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 str(gesture_classes[predicted_index])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T09:48:16.778708Z","iopub.execute_input":"2025-06-01T09:48:16.779215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"inference_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-01T09:47:33.970847Z","iopub.execute_input":"2025-06-01T09:47:33.971648Z","iopub.status.idle":"2025-06-01T09:47:34.640866Z","shell.execute_reply.started":"2025-06-01T09:47:33.971621Z","shell.execute_reply":"2025-06-01T09:47:34.639841Z"}},"outputs":[],"execution_count":null},{"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-01T09:41:58.262636Z","iopub.status.idle":"2025-06-01T09:41:58.262854Z","shell.execute_reply.started":"2025-06-01T09:41:58.262750Z","shell.execute_reply":"2025-06-01T09:41:58.262758Z"}},"outputs":[],"execution_count":null}]}