{"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,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":12030638,"sourceType":"datasetVersion","datasetId":7569492},{"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 sklearn.utils.class_weight import compute_class_weight\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    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.utils import to_categorical, Sequence\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences\nfrom tensorflow.keras import backend as K\nimport tensorflow as tf\nimport polars as pl\nimport kaggle_evaluation.cmi_inference_server\n\nprint(\"Imports loaded\")\n\n# ----------------------------------------------------------------------\n# 1. Utility functions\n# ----------------------------------------------------------------------\n\ndef time_sum(x):\n    \"\"\"Reduce-sum over the time axis (axis=1).\"\"\"\n    return K.sum(x, axis=1)\n\ndef squeeze_last_axis(x):\n    \"\"\"Squeeze the last axis: (batch, timesteps, 1) -> (batch, timesteps).\"\"\"\n    return tf.squeeze(x, axis=-1)\n\ndef expand_last_axis(x):\n    \"\"\"Expand dims at last axis: (batch, timesteps) -> (batch, timesteps, 1).\"\"\"\n    return tf.expand_dims(x, axis=-1)\n\ndef se_block(x, reduction=8):\n    \"\"\"Squeeze-and-Excitation block.\"\"\"\n    channels = x.shape[-1]\n    se = GlobalAveragePooling1D()(x)                           # (batch, channels)\n    se = Dense(channels // reduction, activation='relu')(se)\n    se = Dense(channels, activation='sigmoid')(se)             # (batch, channels)\n    se = Reshape((1, channels))(se)                             # (batch, 1, channels)\n    return Multiply()([x, se])                                  # scale channels\n\ndef residual_se_cnn_block(x, filters, kernel_size, pool_size=2, dropout_rate=0.3, wd=1e-4):\n    \"\"\"\n    Residual CNN block with SE:\n      - Conv1D -> BN -> ReLU -> Conv1D -> BN\n      - SE block\n      - Add skip connection -> ReLU -> MaxPool -> Dropout\n    \"\"\"\n    shortcut = x\n\n    # First Conv\n    x = Conv1D(filters, kernel_size, padding='same',\n               kernel_regularizer=l2(wd), use_bias=False)(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n\n    # Second Conv\n    x = Conv1D(filters, kernel_size, padding='same',\n               kernel_regularizer=l2(wd), use_bias=False)(x)\n    x = BatchNormalization()(x)\n\n    # SE block\n    x = se_block(x, reduction=8)\n\n    # Adjust shortcut if channel dims differ\n    if shortcut.shape[-1] != filters:\n        shortcut = Conv1D(filters, 1, padding='same',\n                          kernel_regularizer=l2(wd), use_bias=False)(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n\n    # Add skip + activation\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 over time:\n      - Dense(1, tanh) -> softmax weights -> weighted sum\n    \"\"\"\n    score_dense = Dense(1, activation='tanh')(inputs)  # (batch, timesteps, 1)\n\n    # Squeeze last axis: (batch, timesteps,1) -> (batch, timesteps)\n    score_flat = Lambda(\n        squeeze_last_axis,\n        output_shape=lambda s: (s[0], s[1])\n    )(score_dense)\n\n    attention_weights = Activation('softmax')(score_flat)  # (batch, timesteps)\n\n    # Expand dims: (batch, timesteps) -> (batch, timesteps, 1)\n    attention_weights_expanded = Lambda(\n        expand_last_axis,\n        output_shape=lambda s: (s[0], s[1], 1)\n    )(attention_weights)\n\n    weighted_seq = Multiply()([inputs, attention_weights_expanded])  # (batch, timesteps, features)\n\n    # Sum over time axis to get context: (batch, timesteps, features) -> (batch, features)\n    context = Lambda(\n        time_sum,\n        output_shape=lambda s: (s[0], s[2])\n    )(weighted_seq)\n\n    return context\n\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list, scaler: StandardScaler) -> np.ndarray:\n    \"\"\"\n    - Forward/backward fill missing\n    - Scale using provided scaler\n    \"\"\"\n    data = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(data)\n\ndef mixup_data(X, y, alpha=0.2):\n    \"\"\"Return mixed inputs, pairs of targets, and lambda.\"\"\"\n    batch_size = X.shape[0]\n    lam = np.random.beta(alpha, alpha)\n    indices = np.random.permutation(batch_size)\n    X_mix = lam * X + (1 - lam) * X[indices]\n    y_mix = lam * y + (1 - lam) * y[indices]\n    return X_mix, y_mix\n\nclass MixupGenerator(Sequence):\n    def __init__(self, X, y, batch_size=128, alpha=0.2, shuffle=True):\n        self.X, self.y = X, y\n        self.batch_size = batch_size\n        self.alpha = alpha\n        self.shuffle = shuffle\n        self.indices = np.arange(len(self.X))\n    \n    def __len__(self):\n        return int(np.ceil(len(self.X) / self.batch_size))\n    \n    def __getitem__(self, idx):\n        batch_idx = self.indices[idx * self.batch_size : (idx + 1) * self.batch_size]\n        X_batch = self.X[batch_idx].copy()\n        y_batch = self.y[batch_idx].copy()\n        X_mix, y_mix = mixup_data(X_batch, y_batch, self.alpha)\n        return X_mix, y_mix\n    \n    def on_epoch_end(self):\n        if self.shuffle:\n            np.random.shuffle(self.indices)\n\n# ----------------------------------------------------------------------\n# 2. Load & preprocess data\n# ----------------------------------------------------------------------\n\nprint(\"Loading dataset...\")\ndf = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\nprint(f\"Total rows: {len(df)}\")\n\n# Encode labels\nlabel_encoder = LabelEncoder()\ndf['gesture'] = label_encoder.fit_transform(df['gesture'].astype(str))\nnp.save('gesture_classes.npy', label_encoder.classes_)  # for inference\n\n# Exclude metadata\nexcluded_cols = {\n    'gesture', 'sequence_type', 'behavior', 'orientation',\n    'row_id', 'subject', 'phase',\n    'sequence_id', 'sequence_counter'\n}\nall_feature_cols = [c for c in df.columns if c not in excluded_cols]\n\n# Split feature columns into IMU vs. TOF/Thermal\nimu_cols = [c for c in all_feature_cols if not (c.startswith('thm_') or c.startswith('tof_'))]\ntof_thm_cols = [c for c in all_feature_cols if c.startswith('thm_') or c.startswith('tof_')]\n\n# Reorder so that IMU features come first\nfeature_cols = imu_cols + tof_thm_cols\nprint(f\"IMU features: {len(imu_cols)}, TOF/Thermal features: {len(tof_thm_cols)}\")\nprint(f\"Total features used: {len(feature_cols)}\")\n\n# Check for missing values\nnan_counts = df[feature_cols].isna().sum()\nprint(\"Total NaNs in features:\", nan_counts.sum())\n\n# Build a global scaler on all training data (IMU+TOF/Thermal)\nprint(\"Fitting StandardScaler on all data...\")\nall_values = df[feature_cols].ffill().bfill().fillna(0).values\nglobal_scaler = StandardScaler().fit(all_values)\n\n# Group sequences and build padded arrays\nsequences = df.groupby('sequence_id')\nX_list = []\nlengths = []\ny_list = []\n\nprint(\"Building sequences (global scaling)...\")\nfor i, (seq_id, seq) in enumerate(sequences):\n    seq_data = seq[feature_cols]\n    scaled = global_scaler.transform(seq_data.ffill().bfill().fillna(0).values)\n    X_list.append(scaled)\n    lengths.append(scaled.shape[0])\n    y_list.append(seq['gesture'].iloc[0])\n    if i % 500 == 0:\n        print(f\"  Processed {i} sequences...\")\n\n# Determine pad length (90th percentile)\npad_len = int(np.percentile(lengths, 90))\nprint(f\"Pad/truncate all sequences to length {pad_len} (90th percentile).\")\nnp.save(\"sequence_maxlen.npy\", pad_len)\n\n# Pad/truncate\nX = pad_sequences(X_list, maxlen=pad_len, dtype='float32', padding='post', truncating='post')\ny = np.array(y_list)\n\n# One-hot encode labels\nnum_classes = len(np.unique(y))\ny_cat = to_categorical(y, num_classes=num_classes)\n\n# Train/validation split (stratify by label)\nX_train, X_val, y_train, y_val = train_test_split(\n    X, y_cat, test_size=0.2, random_state=42, stratify=y\n)\n\nprint(\"Shapes:\", X_train.shape, X_val.shape, y_train.shape, y_val.shape)\n\n# Compute class weights for imbalance\nlabels_train = np.argmax(y_train, axis=1)\nclass_weights_values = compute_class_weight('balanced', classes=np.unique(labels_train), y=labels_train)\nclass_weight = dict(enumerate(class_weights_values))\n\n# ----------------------------------------------------------------------\n# 3. Build model (Two-branch: IMU + TOF/Thermal, SE-CNN, BiLSTM, Attention, Dense)\n# ----------------------------------------------------------------------\n\ndef build_two_branch_model(input_shape, num_classes, imu_dim, tof_thm_dim, wd=1e-4):\n    inputs = Input(shape=input_shape)  # (pad_len, total_features)\n    \n    # Split branches\n    x_imu = Lambda(lambda x: x[:, :, :imu_dim], name='imu_slice')(inputs)\n    x_ttf = Lambda(lambda x: x[:, :, imu_dim:], name='tof_thm_slice')(inputs)\n    \n    # IMU branch: 2 Residual SE-CNN blocks\n    b1 = residual_se_cnn_block(x_imu, filters=64, kernel_size=3, pool_size=2, dropout_rate=0.3, wd=wd)\n    b1 = residual_se_cnn_block(b1, filters=128, kernel_size=5, pool_size=2, dropout_rate=0.3, wd=wd)\n    \n    # TOF/Thermal branch: 2 small Conv1D blocks\n    b2 = Conv1D(64, 3, padding='same', kernel_regularizer=l2(wd), use_bias=False)(x_ttf)\n    b2 = BatchNormalization()(b2)\n    b2 = Activation('relu')(b2)\n    b2 = MaxPooling1D(2)(b2)\n    b2 = Dropout(0.3)(b2)\n    \n    b2 = Conv1D(128, 3, padding='same', kernel_regularizer=l2(wd), use_bias=False)(b2)\n    b2 = BatchNormalization()(b2)\n    b2 = Activation('relu')(b2)\n    b2 = MaxPooling1D(2)(b2)\n    b2 = Dropout(0.3)(b2)\n    \n    # Concatenate branches\n    merged = Concatenate(axis=-1, name='concat_branches')([b1, b2])\n    \n    # BiLSTM for temporal dependencies\n    x = Bidirectional(\n        LSTM(128, return_sequences=True,\n             kernel_regularizer=l2(wd),\n             recurrent_regularizer=l2(wd),\n             bias_regularizer=l2(wd))\n    )(merged)\n    x = Dropout(0.4)(x)\n    \n    # Attention layer\n    x = attention_layer(x)  # (batch, 256)\n    \n    # Dense head with regularization\n    x = Dense(256, activation=None, kernel_regularizer=l2(wd), use_bias=False)(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n    x = Dropout(0.5)(x)\n    \n    x = Dense(128, activation=None, kernel_regularizer=l2(wd), use_bias=False)(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n    x = Dropout(0.3)(x)\n    \n    outputs = Dense(num_classes, activation='softmax', kernel_regularizer=l2(wd), name='output')(x)\n    \n    return Model(inputs=inputs, outputs=outputs, name='TwoBranch_HAR_Model')\n\ninput_shape = (pad_len, len(feature_cols))\nimu_dim = len(imu_cols)\ntof_thm_dim = len(tof_thm_cols)\nmodel = build_two_branch_model(input_shape, num_classes, imu_dim, tof_thm_dim, wd=1e-4)\n\n# ----------------------------------------------------------------------\n# 4. Compile with learning rate schedule & label smoothing\n# ----------------------------------------------------------------------\n\nsteps_per_epoch = len(X_train) // 128\nlr_schedule = tf.keras.optimizers.schedules.CosineDecayRestarts(\n    initial_learning_rate=1e-3,\n    first_decay_steps=5 * steps_per_epoch,\n    t_mul=2.0,\n    m_mul=0.8\n)\noptimizer = Adam(learning_rate=lr_schedule)\n\nloss_fn = tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.05)\n\nmodel.compile(optimizer=optimizer, loss=loss_fn, metrics=['accuracy'])\nmodel.summary()\n\n# ----------------------------------------------------------------------\n# 5. Training with Mixup\n# ----------------------------------------------------------------------\nTRAIN = False\nif TRAIN:\n    batch_size = 128\n    train_gen = MixupGenerator(X_train, y_train, batch_size=batch_size, alpha=0.2, shuffle=True)\n    \n    callbacks = [\n        EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True, verbose=1)\n    ]\n    \n    model.fit(\n        train_gen,\n        validation_data=(X_val, y_val),\n        epochs=70,\n        class_weight=class_weight,\n        callbacks=callbacks,\n        verbose=1\n    )\n    \n    model.save(\"gesture_two_branch_mixup.h5\")\n    print(\"Model training complete and saved as gesture_two_branch_mixup.h5\")\n    \n    # ----------------------------------------------------------------------\n    # 6. Evaluate on validation with custom metric\n    # ----------------------------------------------------------------------\n    \n    from cmi_2025_metric_copy_for_import import CompetitionMetric\n    \n    y_val_pred_probs = model.predict(X_val, verbose=0)\n    y_val_pred = np.argmax(y_val_pred_probs, axis=1)\n    y_val_true = np.argmax(y_val, axis=1)\n    \n    gesture_classes = np.load(\"/kaggle/input/pretrained-model/gesture_classes.npy\", allow_pickle=True)\n    val_pred_labels = pd.Series(y_val_pred).map(lambda i: gesture_classes[i])\n    val_true_labels = pd.Series(y_val_true).map(lambda i: gesture_classes[i])\n    \n    val_submission = pd.DataFrame({'gesture': val_pred_labels})\n    val_solution = pd.DataFrame({'gesture': val_true_labels})\n    \n    metric = CompetitionMetric()\n    score = metric.calculate_hierarchical_f1(val_solution, val_submission)\n    print(f\"Validation hierarchical F1 score: {score:.4f}\")\n    \n    # ----------------------------------------------------------------------\n    # 7. Inference function for Kaggle evaluation\n    # ----------------------------------------------------------------------\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_seq = sequence.to_pandas()\n    processed = preprocess_sequence(df_seq, feature_cols, global_scaler)\n    maxlen = int(np.load(\"/kaggle/input/pretrained-model/sequence_maxlen.npy\"))\n    padded = pad_sequences([processed], maxlen=maxlen, dtype='float32',\n                           padding='post', truncating='post')\n    model = load_model(\n        \"/kaggle/input/pretrained-model/gesture_two_branch_mixup.h5\",\n        compile=False,\n        custom_objects={\n            'time_sum': time_sum,\n            'squeeze_last_axis': squeeze_last_axis,\n            'expand_last_axis': expand_last_axis }\n    )\n    prediction = model.predict(padded, verbose=0)\n    pred_idx = np.argmax(prediction, axis=1)[0]\n    return str(gesture_classes[pred_idx])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-02T01:45:15.133844Z","iopub.execute_input":"2025-06-02T01:45:15.134327Z","iopub.status.idle":"2025-06-02T01:46:05.841682Z","shell.execute_reply.started":"2025-06-02T01:45:15.134303Z","shell.execute_reply":"2025-06-02T01:46:05.841063Z"}},"outputs":[],"execution_count":null},{"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-02T01:46:39.648223Z","iopub.execute_input":"2025-06-02T01:46:39.648821Z","iopub.status.idle":"2025-06-02T01:46:40.643622Z","shell.execute_reply.started":"2025-06-02T01:46:39.648790Z","shell.execute_reply":"2025-06-02T01:46:40.642872Z"}},"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-02T01:46:08.511195Z","iopub.execute_input":"2025-06-02T01:46:08.511486Z","iopub.status.idle":"2025-06-02T01:46:12.148874Z","shell.execute_reply.started":"2025-06-02T01:46:08.511460Z","shell.execute_reply":"2025-06-02T01:46:12.148206Z"}},"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-02T01:46:12.150151Z","iopub.execute_input":"2025-06-02T01:46:12.150374Z","iopub.status.idle":"2025-06-02T01:46:13.214733Z","shell.execute_reply.started":"2025-06-02T01:46:12.150356Z","shell.execute_reply":"2025-06-02T01:46:13.213985Z"}},"outputs":[],"execution_count":null}]}