{"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":12819390,"sourceType":"datasetVersion","datasetId":8106486},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## CMI - Detect Behavior with Sensor Data","metadata":{}},{"cell_type":"code","source":"# Full Modified Code: Subject-Aware Validation Split with GroupKFold\n\nimport os, json, joblib, numpy as np, pandas as pd\nfrom pathlib import Path\nimport warnings\nwarnings.filterwarnings(\"ignore\")\n\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\n\nfrom tensorflow.keras.utils import Sequence, to_categorical, pad_sequences\nfrom tensorflow.keras.models import Model, load_model\nfrom tensorflow.keras.layers import (\n    Input, Conv1D, BatchNormalization, Activation, add, MaxPooling1D, Dropout,\n    Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Multiply, Reshape,\n    Lambda, Concatenate\n)\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom tensorflow.keras import backend as K\nimport tensorflow as tf\n\n# Configuration\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nEXPORT_DIR = Path(\"./\")\nBATCH_SIZE = 64\nPAD_PERCENTILE = 95\nLR_INIT = 1e-3\nWD = 3e-3\nMIXUP_ALPHA = 0.4\nEPOCHS = 160\nPATIENCE = 40\nN_SPLITS = 5\n# Toggle training vs inference\nTRAIN = False\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi-models\")\n\n# Tensor Manipulations\n\ndef time_sum(x): return K.sum(x, axis=1)\n\ndef squeeze_last_axis(x): return tf.squeeze(x, axis=-1)\n\ndef expand_last_axis(x): return tf.expand_dims(x, axis=-1)\n\ndef se_block(x, reduction=8):\n    ch = x.shape[-1]\n    se = GlobalAveragePooling1D()(x)\n    se = Dense(ch // reduction, activation='relu')(se)\n    se = Dense(ch, activation='sigmoid')(se)\n    se = Reshape((1, ch))(se)\n    return Multiply()([x, se])\n\ndef residual_se_cnn_block(x, filters, kernel_size, pool_size=2, drop=0.3, wd=1e-4):\n    shortcut = x\n    for _ in range(2):\n        x = Conv1D(filters, kernel_size, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x); x = Activation('relu')(x)\n    x = se_block(x)\n    if shortcut.shape[-1] != filters:\n        shortcut = Conv1D(filters, 1, padding='same', use_bias=False, kernel_regularizer=l2(wd))(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n    x = add([x, shortcut]); x = Activation('relu')(x)\n    x = MaxPooling1D(pool_size)(x); x = Dropout(drop)(x)\n    return x\n\ndef attention_layer(inputs):\n    score = Dense(1, activation='tanh')(inputs)\n    score = Lambda(squeeze_last_axis)(score)\n    weights = Activation('softmax')(score)\n    weights = Lambda(expand_last_axis)(weights)\n    context = Multiply()([inputs, weights])\n    context = Lambda(time_sum)(context)\n    return context\n\n# Data preparation\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list, scaler: StandardScaler):\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(mat).astype('float32')\n\nclass MixupGenerator(Sequence):\n    def __init__(self, X, y, batch_size, alpha=0.2):\n        self.X, self.y = X, y\n        self.batch = batch_size\n        self.alpha = alpha\n        self.indices = np.arange(len(X))\n    def __len__(self): return int(np.ceil(len(self.X) / self.batch))\n    def __getitem__(self, i):\n        idx = self.indices[i*self.batch:(i+1)*self.batch]\n        Xb, yb = self.X[idx], self.y[idx]\n        lam = np.random.beta(self.alpha, self.alpha)\n        perm = np.random.permutation(len(Xb))\n        X_mix = lam * Xb + (1-lam) * Xb[perm]\n        y_mix = lam * yb + (1-lam) * yb[perm]\n        return X_mix, y_mix\n    def on_epoch_end(self): np.random.shuffle(self.indices)\n\n# Model builder\ndef build_two_branch_model(pad_len, imu_dim, tof_dim, n_classes, wd=1e-4):\n    inp = Input(shape=(pad_len, imu_dim+tof_dim))\n    imu = Lambda(lambda t: t[:, :, :imu_dim])(inp)\n    tof = Lambda(lambda t: t[:, :, imu_dim:])(inp)\n\n    x1 = residual_se_cnn_block(imu, 64, 3, drop=0.3, wd=wd)\n    x1 = residual_se_cnn_block(x1, 128, 5, drop=0.3, wd=wd)\n\n    x2 = Conv1D(64, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(tof)\n    x2 = BatchNormalization()(x2); x2 = Activation('relu')(x2)\n    x2 = MaxPooling1D(2)(x2); x2 = Dropout(0.3)(x2)\n    x2 = Conv1D(128, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x2)\n    x2 = BatchNormalization()(x2); x2 = Activation('relu')(x2)\n    x2 = MaxPooling1D(2)(x2); x2 = Dropout(0.3)(x2)\n\n    merged = Concatenate()([x1, x2])\n    x = Bidirectional(LSTM(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    x = Dropout(0.4)(x)\n    x = attention_layer(x)\n\n    for units, drop in [(256, 0.5), (128, 0.3)]:\n        x = Dense(units, use_bias=False, kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x); x = Activation('relu')(x); x = Dropout(drop)(x)\n\n    out = Dense(n_classes, activation='softmax', kernel_regularizer=l2(wd))(x)\n    return Model(inp, out)\n\nif TRAIN:\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n    le = LabelEncoder(); df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n\n    meta_cols = {'gesture', 'gesture_int', 'sequence_type', 'behavior', 'orientation',\n                 'row_id', 'subject', 'phase', 'sequence_id', 'sequence_counter'}\n    feature_cols = [c for c in df.columns if c not in meta_cols]\n    imu_cols  = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n    tof_cols  = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n\n    scaler = StandardScaler().fit(df[feature_cols].ffill().bfill().fillna(0).values)\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n\n    seq_gp = df.groupby('sequence_id')\n    X_list, y_list, lens, subjects = [], [], [], []\n    for seq_id, seq in seq_gp:\n        X_list.append(preprocess_sequence(seq, feature_cols, scaler))\n        y_list.append(seq['gesture_int'].iloc[0])\n        lens.append(len(X_list[-1]))\n        subjects.append(seq['subject'].iloc[0])\n\n    pad_len = int(np.percentile(lens, PAD_PERCENTILE))\n    np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(feature_cols))\n\n    X = pad_sequences(X_list, maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n    y = to_categorical(y_list, num_classes=len(le.classes_))\n    subjects = np.array(subjects)\n\n    # Group-aware KFold split\n    gkf = GroupKFold(n_splits=N_SPLITS)\n    val_scores = []\n    for fold, (train_idx, val_idx) in enumerate(gkf.split(X, y, groups=subjects)):\n        print(f\"\\n--- FOLD {fold+1}/{N_SPLITS} ---\")\n        X_tr, X_val = X[train_idx], X[val_idx]\n        y_tr, y_val = y[train_idx], y[val_idx]\n        y_list_tr = y_tr.argmax(1)\n\n        cw_vals = compute_class_weight('balanced', classes=np.arange(len(le.classes_)), y=y_list_tr)\n        class_weight = dict(enumerate(cw_vals))\n\n        model = build_two_branch_model(pad_len, len(imu_cols), len(tof_cols), len(le.classes_), wd=WD)\n        steps = len(X_tr) // BATCH_SIZE\n        lr_sched = tf.keras.optimizers.schedules.CosineDecayRestarts(LR_INIT, first_decay_steps=5*steps)\n        model.compile(optimizer=Adam(lr_sched),\n                      loss=tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.1),\n                      metrics=['accuracy'])\n\n        train_gen = MixupGenerator(X_tr, y_tr, batch_size=BATCH_SIZE, alpha=MIXUP_ALPHA)\n        cb = EarlyStopping(patience=PATIENCE, restore_best_weights=True, verbose=1)\n        model.fit(train_gen, epochs=EPOCHS, validation_data=(X_val, y_val),\n                  class_weight=class_weight, callbacks=[cb], verbose=1)\n\n        model.save(EXPORT_DIR / f\"gesture_model_fold_{fold+1}.h5\")\n        print(f\"✔ Fold {fold+1} model saved.\")\n\n        # quick metric\n        from cmi_2025_metric_copy_for_import import CompetitionMetric\n        preds = model.predict(X_val).argmax(1)\n        true  = y_val.argmax(1)\n        h_f1 = CompetitionMetric().calculate_hierarchical_f1(\n            pd.DataFrame({'gesture': le.classes_[true]}),\n            pd.DataFrame({'gesture': le.classes_[preds]})\n        )\n        print(\"Hold‑out H‑F1 =\", round(h_f1, 4))\n        val_scores.append(h_f1)\n\n    # Average validation score\n    avg_h_f1 = np.mean(val_scores)\n    print(f\"\\nAverage Hold‑out H‑F1 over {N_SPLITS} folds: {round(avg_h_f1, 4)}\")\n\nelse:\n  print(\"▶ INFERENCE MODE – loading artefacts …\")\n  feature_cols   = np.load(PRETRAINED_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n  pad_len        = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\n  scaler         = joblib.load(PRETRAINED_DIR / \"scaler.pkl\")\n  gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n  \n  imu_cols = [c for c in feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\n  tof_cols = [c for c in feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n\n# Load all trained fold models for ensembling\ntf.keras.backend.clear_session()\nmodels = []\nfor fold in range(1, N_SPLITS+1):\n    model_path = PRETRAINED_DIR / f\"gesture_model_fold_{fold}.h5\"\n    m = load_model(model_path, compile=False, custom_objects={\n        'time_sum': time_sum,\n        'squeeze_last_axis': squeeze_last_axis,\n        'expand_last_axis': expand_last_axis,\n        'se_block': se_block,\n        'residual_se_cnn_block': residual_se_cnn_block,\n        'attention_layer': attention_layer,\n    })\n    models.append(m)\nprint(f\"Loaded {len(models)} models for ensembling.\")\n\n# Ensemble predict function updated for Kaggle evaluation\n# Accepts both sequence and demographics as required by CMIInferenceServer\n# demographics is unused but required by signature\ndef predict(sequence, demographics) -> str:\n    \"\"\"Predict gesture for a single sequence using ensemble of fold models.\n    CMIInferenceServer passes sequence as a Polars DataFrame, so convert to pandas.\"\"\"\n    # Convert Polars to pandas if needed\n    if hasattr(sequence, 'to_pandas'):\n        df_seq = sequence.to_pandas()\n    else:\n        df_seq = sequence\n    # Preprocess\n    mat = preprocess_sequence(df_seq, feature_cols, scaler)\n    pad = pad_sequences([mat], maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n    # Ensemble predictions\n    preds = np.array([m.predict(pad, verbose=0) for m in models])\n    mean_pred = preds.mean(axis=0)\n    idx = int(mean_pred.argmax(1)[0])\n    return str(gesture_classes[idx])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kaggle_evaluation.cmi_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-06T08:45:27.327802Z","iopub.execute_input":"2025-06-06T08:45:27.327979Z","iopub.status.idle":"2025-06-06T08:45:28.530263Z","shell.execute_reply.started":"2025-06-06T08:45:27.327964Z","shell.execute_reply":"2025-06-06T08:45:28.529559Z"}},"outputs":[],"execution_count":null}]}