{"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":"# Detect Behavior with Sensor Data – CNN + Bi-LSTM + Demographics\n# ------------------------------------------------------------------\n# This is a minimally-intrusive revision of your original notebook.\n# The only functional addition is that the seven demographic/anthro-\n# pometric columns from train_demographics.csv are merged onto every\n# row of the sensor frame and treated as extra numeric channels.\n# Nothing else in the pipeline changes, so you can reuse previous\n# hyper-parameters and checkpoints if desired.\n\nimport 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 Sequential, load_model\nfrom tensorflow.keras.layers import (\n    Conv1D, MaxPooling1D, Flatten, Dense, Dropout, BatchNormalization,\n    LSTM, Bidirectional, GlobalAveragePooling1D\n)\nfrom tensorflow.keras.utils import to_categorical\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import ReduceLROnPlateau, EarlyStopping\nimport tensorflow as tf\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server  # noqa: F401   | Kaggle runner hook\n\nprint(\"Imports loaded\")\n\n# ------------------------------------------------------------------\n# 1.  LOAD TRAIN SENSOR DATA + DEMOGRAPHICS\n# ------------------------------------------------------------------\nprint(\"Loading sensor dataset …\")\nroot = \"/kaggle/input/cmi-detect-behavior-with-sensor-data\"\n\ndf = pd.read_csv(f\"{root}/train.csv\")\nprint(f\"Loaded {len(df):,} rows of sensor frames\")\n\n# --- NEW: merge participant demographics on the key `subject` --------\nprint(\"Merging demographic attributes …\")\ndemographics = pd.read_csv(f\"{root}/train_demographics.csv\")\ndf = df.merge(demographics, on=\"subject\", how=\"left\")\n\n# ------------------------------------------------------------------\n# 2.  LABEL-ENCODE GESTURE TARGET\n# ------------------------------------------------------------------\nlabel_encoder = LabelEncoder()\ndf[\"gesture\"] = label_encoder.fit_transform(df[\"gesture\"].astype(str))\nnp.save(\"gesture_classes.npy\", label_encoder.classes_)\n\nprint(\"Gesture label mapping:\")\nfor idx, lab in enumerate(label_encoder.classes_):\n    print(f\"  {idx}: {lab}\")\n\n# ------------------------------------------------------------------\n# 3.  FEATURE LIST CONSTRUCTION\n# ------------------------------------------------------------------\n# Optionally skip thermal/TOF values → set to False to use them.\n\ndrop_thermal_and_tof = False\n\nexcluded_cols = {\n    \"gesture\", \"sequence_type\", \"behavior\", \"orientation\",  # train-only targets\n    \"row_id\", \"subject\", \"phase\",                            # meta\n    \"sequence_id\", \"sequence_counter\"                         # ids\n}\n\nthermal_tof_cols = [c for c in df.columns if c.startswith((\"thm_\", \"tof_\"))]\n\nif drop_thermal_and_tof:\n    excluded_cols.update(thermal_tof_cols)\n    print(f\"Ignoring {len(thermal_tof_cols)} thermopile/TOF channels → set drop_thermal_and_tof=False to use them.\")\n\n# --- NEW: demographic numeric columns --------------------------------\ndemographic_cols = [\n    \"adult_child\", \"age\", \"sex\", \"handedness\",\n    \"height_cm\", \"shoulder_to_wrist_cm\", \"elbow_to_wrist_cm\",\n]\n\n# Combine sensor + demographic feature list\nfeature_cols = [c for c in df.columns if c not in excluded_cols]\nprint(f\"Using {len(feature_cols)} feature columns for training, including demographics:\")\nprint(sorted(feature_cols)[:15], \"…\")\n\n# Check missing values\nnan_total = df[feature_cols].isna().sum().sum()\nprint(f\"Total NaNs inside feature matrix: {nan_total:,}\")\n\n# ------------------------------------------------------------------\n# 4.  SEQUENCE BUILDING HELPERS\n# ------------------------------------------------------------------\n\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_columns: list[str]) -> np.ndarray:\n    \"\"\"Fill→scale a *single* sequence dataframe and return float32 numpy.\"\"\"\n    data = df_seq[feature_columns].copy()\n    data = data.ffill().bfill().fillna(0.0)\n    scaled = StandardScaler().fit_transform(data)   # per-sequence scaler (unchanged)\n    return scaled.astype(\"float32\")\n\nprint(\"Constructing padded tensor dataset …\")\nseq_groups = df.groupby(\"sequence_id\")\n\nX, seq_lengths = [], []\nfor i, (_, seq) in enumerate(seq_groups):\n    if i and i % 500 == 0:\n        print(f\"  processed {i} sequences …\")\n    arr = preprocess_sequence(seq, feature_cols)\n    X.append(arr)\n    seq_lengths.append(arr.shape[0])\n\npad_len = int(np.percentile(seq_lengths, 90))\nprint(f\"90th-percentile length = {pad_len} → fixed pad length chosen\")\nnp.save(\"sequence_maxlen.npy\", pad_len)\n\nX = pad_sequences(X, maxlen=pad_len, dtype=\"float32\", padding=\"post\", truncating=\"post\")\n\ny = seq_groups[\"gesture\"].first().values\nnum_classes = len(np.unique(y))\ny = to_categorical(y, num_classes=num_classes)\n\n# ------------------------------------------------------------------\n# 5.  TRAIN/VAL SPLIT & MODEL\n# ------------------------------------------------------------------\nX_train, X_val, y_train, y_val = train_test_split(\n    X, y, test_size=0.20, random_state=42, stratify=y\n)\n\nprint(\"Building CNN-BiLSTM model …\")\nmodel = Sequential([\n    Conv1D(64, 3, activation=\"relu\", input_shape=(pad_len, X_train.shape[2])),\n    BatchNormalization(),\n    Conv1D(64, 3, activation=\"relu\"),\n    MaxPooling1D(2),\n    Dropout(0.30),\n\n    Conv1D(128, 5, activation=\"relu\"),\n    BatchNormalization(),\n    Conv1D(128, 5, activation=\"relu\"),\n    MaxPooling1D(2),\n    Dropout(0.30),\n\n    Conv1D(256, 7, activation=\"relu\"),\n    BatchNormalization(),\n    Conv1D(256, 7, activation=\"relu\"),\n    MaxPooling1D(2),\n    Dropout(0.40),\n\n    Bidirectional(LSTM(128, return_sequences=True)),\n    Dropout(0.40),\n\n    GlobalAveragePooling1D(),\n\n    Dense(512, activation=\"relu\"),\n    BatchNormalization(),\n    Dropout(0.50),\n    Dense(256, activation=\"relu\"),\n    Dropout(0.30),\n\n    Dense(num_classes, activation=\"softmax\"),\n])\n\nmodel.compile(optimizer=Adam(1e-3), loss=\"categorical_crossentropy\", metrics=[\"accuracy\"])\nmodel.summary()\n\nprint(\"Training …\")\ncallbacks = [\n    ReduceLROnPlateau(patience=4, factor=0.5, verbose=1),\n    EarlyStopping(patience=8, restore_best_weights=True, verbose=1)\n]\nmodel.fit(X_train, y_train, epochs=60, batch_size=128,\n          validation_data=(X_val, y_val), callbacks=callbacks)\n\nmodel.save(\"gesture_cnn_model.h5\")\nprint(\"Training complete; model saved → gesture_cnn_model.h5\")\n\n# ------------------------------------------------------------------\n# 6.  LOCAL VALIDATION METRIC\n# ------------------------------------------------------------------\nprint(\"Computing validation hierarchical-F1 …\")\nfrom cmi_2025_metric_copy_for_import import CompetitionMetric  # local helper\n\nprobs_val = model.predict(X_val, verbose=0)\nlabels_val_pred = np.argmax(probs_val, axis=1)\nlabels_val_true = np.argmax(y_val, axis=1)\n\ncls = np.load(\"gesture_classes.npy\", allow_pickle=True)\nval_pred_df = pd.DataFrame({\"gesture\": [cls[i] for i in labels_val_pred]})\nval_true_df = pd.DataFrame({\"gesture\": [cls[i] for i in labels_val_true]})\n\nmetric = CompetitionMetric()\nscore = metric.calculate_hierarchical_f1(val_true_df, val_pred_df)\nprint(f\"Estimated public-LB score on held-out fold: {score:.4f}\")\n\n# ------------------------------------------------------------------\n# 7.  INFERENCE HELPER\n# ------------------------------------------------------------------\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"Kaggle inference signature: returns predicted gesture string.\"\"\"\n    seq_df = sequence.to_pandas()\n    demo_df = demographics.to_pandas()\n    seq_df = seq_df.merge(demo_df, on=\"subject\", how=\"left\")\n\n    arr = preprocess_sequence(seq_df, feature_cols)\n    maxlen = int(np.load(\"sequence_maxlen.npy\"))\n    padded = pad_sequences([arr], maxlen=maxlen, dtype=\"float32\", padding=\"post\", truncating=\"post\")\n\n    mdl = load_model(\"gesture_cnn_model.h5\")\n    probs = mdl.predict(padded, verbose=0)\n    idx = int(np.argmax(probs, axis=1)[0])\n    classes = np.load(\"gesture_classes.npy\", allow_pickle=True)\n    return str(classes[idx])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-01T19:28:56.639619Z","iopub.execute_input":"2025-06-01T19:28:56.639915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 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-01T19:28:51.482180Z","iopub.status.idle":"2025-06-01T19:28:51.482465Z","shell.execute_reply.started":"2025-06-01T19:28:51.482293Z","shell.execute_reply":"2025-06-01T19:28:51.482307Z"}},"outputs":[],"execution_count":null}]}