{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":242954653,"sourceType":"kernelVersion"},{"sourceId":427830,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":348766,"modelId":370022}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# Three‑Branch Human‑Activity‑Recognition Pipeline (IMU + Thermopile + TOF  + SE‑CNN + BiLSTM + Attention)\n# upvote for me if you like it and before copy <<>>\n# I want to find some teammates (responsible). If you want, contact me.\n\nimport os\nimport gc\nimport json\nimport joblib\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.pipeline import Pipeline\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.decomposition import PCA\nfrom sklearn.tree import DecisionTreeClassifier\nfrom sklearn.svm import SVC\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, Conv2D, Conv3D, BatchNormalization, Activation, add, MaxPooling1D, MaxPooling2D, MaxPooling3D, Dropout,\n    Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Multiply, Reshape,\n    Lambda, Concatenate, InputLayer, Flatten, Cropping1D, Cropping2D, RandomRotation, RandomZoom\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\nimport polars as pl\nimport matplotlib.pyplot as plt\nimport keras\n# (Competition metric will only be imported when TRAINing)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:08:59.852550Z","iopub.execute_input":"2025-06-07T17:08:59.852823Z","iopub.status.idle":"2025-06-07T17:09:17.908978Z","shell.execute_reply.started":"2025-06-07T17:08:59.852801Z","shell.execute_reply":"2025-06-07T17:09:17.908166Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------- CONFIG ---------------------------------\nTRAIN = False\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi/keras/default/1\")  # used when TRAIN=False\n# RAW_DIR = Path(\"../kaggle\")\n# PRETRAINED_DIR = Path(\"../kaggle\")  # used when TRAIN=False\n\nEXPORT_DIR = Path(\"./\")                                    # artefacts will be saved here\nBATCH_SIZE = 128\nPAD_PERCENTILE = 90\nLR_INIT = 1e-3\nWD = 1e-4\nMIXUP_ALPHA = 0.2\nEPOCHS = 160\nPATIENCE = 20\nTOF_LIMIT = 100\n\nprint(\"▶ imports ready · tensorflow\", tf.__version__)\n\n# -------------------------- 1. UTILITIES ------------------------------\n\ndef time_sum(x):\n    return K.sum(x, axis=1)\n\ndef squeeze_last_axis(x):\n    return tf.squeeze(x, axis=-1)\n\ndef expand_last_axis(x):\n    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,\n                   kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x)\n        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,\n                          kernel_regularizer=l2(wd))(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n    x = add([x, shortcut])\n    x = Activation('relu')(x)\n    x = MaxPooling1D(pool_size)(x)\n    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","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:17.910430Z","iopub.execute_input":"2025-06-07T17:09:17.911051Z","iopub.status.idle":"2025-06-07T17:09:17.922580Z","shell.execute_reply.started":"2025-06-07T17:09:17.911023Z","shell.execute_reply":"2025-06-07T17:09:17.921691Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------- 2. DATA HELPERS ------------------------------\n\ndef preprocess_all(\n    df: pd.DataFrame,\n    df_demog: pd.DataFrame,\n    feature_cols: list[str],\n    imu_cols: list[str],\n    tof_cols: list[str],\n    thm_cols: list[str]):\n\n    # cleanup imu\n    df[imu_cols] = df[imu_cols].fillna(0)\n\n    # cleanup tof\n    df[tof_cols] = df[tof_cols].replace(-1, 0)\n    df[tof_cols] = df[tof_cols].fillna(0)\n    df[tof_cols] = df[tof_cols].where(df[tof_cols] < TOF_LIMIT, TOF_LIMIT)\n\n    # cleanup thm\n    df[thm_cols] = df[thm_cols].fillna(0)\n    thm_cols2 = thm_cols.copy()\n    thm_cols2.append('subject')\n    thm_cols2.append('sequence_id')\n    df[thm_cols] = df[thm_cols2].groupby(['subject', 'sequence_id'])[thm_cols].diff()\n    df[thm_cols] = df[thm_cols].fillna(0)\n\n    # cleanup demog and add duration\n    # get the durations of each gesture\n    df_gesture_duration = df.groupby(['sequence_id']).size().reset_index(name='duration')\n    df = df.merge(df_gesture_duration, on=\"sequence_id\", how=\"right\")\n    df = df.merge(df_demog, on=\"subject\", how=\"right\")\n    return df\n\ndef preprocess_sequence(\n    df_seq: pd.DataFrame,\n    df_demog: pd.DataFrame,\n    feature_cols: list[str],\n    imu_cols: list[str],\n    tof_cols: list[str],\n    thm_cols: list[str]):\n\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return mat\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):\n        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):\n        np.random.shuffle(self.indices)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:17.923587Z","iopub.execute_input":"2025-06-07T17:09:17.923854Z","iopub.status.idle":"2025-06-07T17:09:17.960623Z","shell.execute_reply.started":"2025-06-07T17:09:17.923812Z","shell.execute_reply":"2025-06-07T17:09:17.959616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def tof_camera_cnn_block(x):\n#    x = RandomRotation(factor=(-0.2, 0.2))(x)\n    x = Conv2D(32, (3, 3), input_shape=(8, 8), padding='same', strides=1, activation='relu', use_bias=True)(x)\n    x = MaxPooling2D(pool_size=(2, 2))(x)\n    x = Dropout(0.4)(x)\n    x = Conv2D(32, (3, 3), input_shape=(8, 8), padding='same', strides=1, activation='relu')(x)\n    x = MaxPooling2D(pool_size=(2, 2))(x)\n    x = Dropout(0.4)(x)\n    x = Flatten()(x)\n    x = Dense(3200, activation='relu')(x)\n    x = Reshape((25, 128))(x)\n    return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:17.962794Z","iopub.execute_input":"2025-06-07T17:09:17.963161Z","iopub.status.idle":"2025-06-07T17:09:17.984173Z","shell.execute_reply.started":"2025-06-07T17:09:17.963130Z","shell.execute_reply":"2025-06-07T17:09:17.983331Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------- 3. MODEL DEFINITION --------------------------\n\ndef build_four_branch_model(pad_len, imu_dim, tof_dim, thm_dim, demog_dim, gesture_duration_dim, n_classes, wd=1e-4):\n    print(f\"imu_dim {imu_dim}, tof_dim {tof_dim}, thm_dim {thm_dim}, demog_dim {demog_dim}, gesture_duration_dim {gesture_duration_dim}\")\n    inp = Input(shape=(pad_len, imu_dim+tof_dim+thm_dim+demog_dim+gesture_duration_dim))\n\n    # imu values\n    imu = Lambda(lambda t: t[:, :, :imu_dim])(inp)\n\n    # tof values\n    tof = Lambda(lambda t: t[:, :, imu_dim:imu_dim+tof_dim])(inp)\n\n    # tof camera 0 values\n    tof0 = Lambda(lambda t: t[:, :, 0:64])(tof)\n    tof0_reshape = Reshape((-1, 8, 8))(tof0)\n\n    # tof camera 1 values\n    tof1 = Lambda(lambda t: t[:, :, 64:128])(tof)\n    tof1_reshape = Reshape((-1, 8, 8))(tof1)\n\n    # tof camera 2 values\n    tof2 = Lambda(lambda t: t[:, :, 128:192])(tof)\n    tof2_reshape = Reshape((-1, 8, 8))(tof2)\n\n    # tof camera 3 values\n    tof3 = Lambda(lambda t: t[:, :, 192:256])(tof)\n    tof3_reshape = Reshape((-1, 8, 8))(tof3)\n\n    # tof camera 4 values\n    tof4 = Lambda(lambda t: t[:, :, 256:320])(tof)\n    tof4_reshape = Reshape((-1, 8, 8))(tof4)\n\n    # thm values\n    thm = Lambda(lambda t: t[:, :, imu_dim+tof_dim:imu_dim+tof_dim+thm_dim])(inp)\n\n    # demog values\n    # at this point we combine the gesture durations into the demographics for model processing\n    demog = Lambda(lambda t: t[:, :, imu_dim+tof_dim+thm_dim:])(inp)\n\n    # IMU deep branch\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    # TOF lighter branch\n    # all are treated as if there are 5 seperate tof cameras\n    x2_0 = tof_camera_cnn_block(tof0_reshape)\n    x2_1 = tof_camera_cnn_block(tof1_reshape)\n    x2_2 = tof_camera_cnn_block(tof2_reshape)\n    x2_3 = tof_camera_cnn_block(tof3_reshape)\n    x2_4 = tof_camera_cnn_block(tof4_reshape)\n    \n    # Thermal lighter branch\n    x3 = Conv1D(64, 3, data_format=\"channels_last\", padding='same', use_bias=False, kernel_regularizer=l2(wd))(thm)\n    x3 = BatchNormalization()(x3);\n    x3 = Activation('relu')(x3)\n    x3 = MaxPooling1D(2)(x3)\n    x3 = Dropout(0.4)(x3)\n    x3 = Conv1D(128, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x3)\n    x3 = BatchNormalization()(x3)\n    x3 = Activation('relu')(x3)\n    x3 = MaxPooling1D(2)(x3)\n    x3 = Dropout(0.4)(x3)\n\n    # Demographics and gesture duration\n    # only use the first row since the rest are the same\n    x4 = Cropping1D((0, pad_len - 1))(demog)\n    x4 = Dense(64, activation=\"relu\")(x4)\n    x4 = Dropout(0.4)(x4)\n    x4 = Dense(3200, activation='relu')(x4)\n    x4 = Reshape((25, 128))(x4)\n\n    merged1 = Concatenate()([x1, x3, x4])\n    merged2 = Concatenate()([x2_0, x2_1, x2_2, x2_3, x2_4])\n    merged = Concatenate()([merged1, merged2])\n\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)\n        x = Dropout(drop)(x)\n\n    out = Dense(n_classes, activation='softmax', kernel_regularizer=l2(wd))(x)\n    return Model(inp, out)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:17.985224Z","iopub.execute_input":"2025-06-07T17:09:17.985529Z","iopub.status.idle":"2025-06-07T17:09:18.006071Z","shell.execute_reply.started":"2025-06-07T17:09:17.985506Z","shell.execute_reply":"2025-06-07T17:09:18.005185Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------- 4.a LOAD DATASET -----------------------------\n\nif TRAIN:\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n    df_demog = pd.read_csv(RAW_DIR / \"train_demographics.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.006937Z","iopub.execute_input":"2025-06-07T17:09:18.007250Z","iopub.status.idle":"2025-06-07T17:09:18.025255Z","shell.execute_reply.started":"2025-06-07T17:09:18.007228Z","shell.execute_reply":"2025-06-07T17:09:18.024284Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------- 4. TRAINING PHASE ----------------------------\n\nif TRAIN:\n    # label encoding\n    le = LabelEncoder(); df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n\n    # feature list\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    demog_cols = ['handedness', 'shoulder_to_wrist_cm', 'elbow_to_wrist_cm']\n    gesture_duration_cols = ['duration']\n    imu_cols  = [c for c in feature_cols if (c.startswith('acc_') or c.startswith('rot_'))]\n    tof_cols  = [c for c in feature_cols if c.startswith('tof_')]\n    thm_cols  = [c for c in feature_cols if c.startswith('thm_')]\n    feature_cols = feature_cols + demog_cols + gesture_duration_cols\n    \n    print(f\"  IMU {len(imu_cols)} | TOF {len(tof_cols)} | THM {len(thm_cols)} | demog {len(demog_cols)} | gesture_duration {len(gesture_duration_cols)} | total {len(feature_cols)} features\")    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.026184Z","iopub.execute_input":"2025-06-07T17:09:18.026514Z","iopub.status.idle":"2025-06-07T17:09:18.045969Z","shell.execute_reply.started":"2025-06-07T17:09:18.026484Z","shell.execute_reply":"2025-06-07T17:09:18.045038Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    # build sequences\n    # remove unneeded demographic columns\n    demog_remove = ['adult_child', 'age', 'sex', 'height_cm']\n    df_demog.drop(columns=demog_remove, inplace=True)\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.047172Z","iopub.execute_input":"2025-06-07T17:09:18.047498Z","iopub.status.idle":"2025-06-07T17:09:18.061399Z","shell.execute_reply.started":"2025-06-07T17:09:18.047469Z","shell.execute_reply":"2025-06-07T17:09:18.060508Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    X_list, y_list, lens = [], [], []\n    index = 0\n    df = preprocess_all(\n        df,\n        df_demog,\n        feature_cols,\n        imu_cols,\n        tof_cols,\n        thm_cols)\n    seq_gp = df.groupby('sequence_id')\n    for seq_id, seq in seq_gp:\n        mat = preprocess_sequence(\n            seq,\n            df_demog,\n            feature_cols,\n            imu_cols,\n            tof_cols,\n            thm_cols)\n        X_list.append(mat)\n        y_list.append(seq['gesture_int'].iloc[0])\n        lens.append(len(mat))\n        if index % 500 == 0:\n            print(index)\n        index = index + 1\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\n    # split\n    X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y_list)\n\n    # apply the global scaler to the training set\n    s1 = len(X_tr)\n    s2 = len(X_tr[0])\n    s3 = len(X_tr[0][0])\n    X_tr = X_tr.reshape(-1, s3)\n    scaler = StandardScaler().fit(X_tr)\n    X_tr = scaler.transform(X_tr)\n    X_tr = X_tr.reshape(s1, s2, s3)\n\n    # save the scaler values\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n    \n    # apply the scaler to the X_val\n    s1 = len(X_val)\n    s2 = len(X_val[0])\n    s3 = len(X_val[0][0])\n    X_val = X_val.reshape(-1, s3)\n    X_val = scaler.transform(X_val)\n    X_val = X_val.reshape(s1, s2, s3)\n    \n    # class weights\n    cw_vals = compute_class_weight('balanced', classes=np.arange(len(le.classes_)), y=y_list)\n    class_weight = dict(enumerate(cw_vals))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.062613Z","iopub.execute_input":"2025-06-07T17:09:18.062960Z","iopub.status.idle":"2025-06-07T17:09:18.080710Z","shell.execute_reply.started":"2025-06-07T17:09:18.062936Z","shell.execute_reply":"2025-06-07T17:09:18.079922Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    gb = df.groupby('sequence_id')\n    index = 0\n    for seq_id, seq in gb:\n        gesture = seq['gesture'].values[0]\n        if gesture == \"Forehead - scratch\":\n            print(f\"index {index}, seq {seq_id}, gesture {gesture}\")\n        index = index + 1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.083930Z","iopub.execute_input":"2025-06-07T17:09:18.084254Z","iopub.status.idle":"2025-06-07T17:09:18.101743Z","shell.execute_reply.started":"2025-06-07T17:09:18.084230Z","shell.execute_reply":"2025-06-07T17:09:18.100917Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    # Generate a sample signal (e.g., a sine wave)\n    fs = 1  # Sampling frequency\n    duration = 2  # Duration in seconds\n    t = np.linspace(0, duration, int(fs * duration), endpoint=False)\n\n    signal = np.array([arr[2] for arr in X[54]])\n\n    # Create a Keras input layer\n    inputs = keras.layers.Input(shape=(None, 1))\n\n    # Perform FFT using a lambda layer\n    fft_layer = keras.layers.Lambda(lambda v: tf.signal.fft(tf.cast(v, tf.complex64)))(inputs)\n\n    # Create a Keras model\n    model = keras.Model(inputs=inputs, outputs=fft_layer)\n\n    # Reshape the signal for the model\n    signal_reshaped = signal.reshape(1, len(signal), 1)\n\n    # Get the FFT output\n    fft_output = model.predict(signal_reshaped)\n\n    # Calculate the magnitude spectrum\n    magnitude_spectrum = np.abs(fft_output[0, :, 0])\n\n    # Calculate the frequency axis\n    freq = np.fft.fftfreq(len(signal), 1/fs)\n\n    # Plot the magnitude spectrum\n    plt.plot(freq, magnitude_spectrum)\n    plt.title('FFT Magnitude Spectrum')\n    plt.xlabel('Frequency (Hz)')\n    plt.ylabel('Magnitude')\n    plt.xlim(0, fs/2) # Display only the positive frequencies\n    plt.grid(True)\n    plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.103090Z","iopub.execute_input":"2025-06-07T17:09:18.103426Z","iopub.status.idle":"2025-06-07T17:09:18.120510Z","shell.execute_reply.started":"2025-06-07T17:09:18.103404Z","shell.execute_reply":"2025-06-07T17:09:18.119649Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    # model\n    model = build_four_branch_model(pad_len, len(imu_cols), len(tof_cols), len(thm_cols), len(demog_cols), len(gesture_duration_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.05),\n                  metrics=['accuracy'])\n    model.summary()\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    history = 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 / \"gesture_four_branch_mixup.h5\")\n    print(\"✔ Training done – artefacts saved in\", EXPORT_DIR)\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    print(\"Hold‑out H‑F1 =\", round(h_f1, 4))\n\nelse:\n    print(\"▶ INFERENCE MODE – loading artefacts from\", PRETRAINED_DIR)\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    custom_objs = {\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    model = load_model(PRETRAINED_DIR / \"gesture_four_branch_mixup.h5\",\n                       compile=False, custom_objects=custom_objs)\n    print(\"  model, scaler, pads loaded – ready for evaluation\")","metadata":{"scrolled":true,"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:18.121428Z","iopub.execute_input":"2025-06-07T17:09:18.121714Z","iopub.status.idle":"2025-06-07T17:09:20.713008Z","shell.execute_reply.started":"2025-06-07T17:09:18.121695Z","shell.execute_reply":"2025-06-07T17:09:20.712155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    # Plotting the training and validation loss\n    plt.figure(figsize=(10, 5))\n    plt.plot(history.history['loss'], label='Training Loss')\n    plt.plot(history.history['val_loss'], label='Validation Loss')\n    plt.title('Training and Validation Loss')\n    plt.xlabel('Epochs')\n    plt.ylabel('Loss')\n    plt.legend()\n    plt.show()\n\n    # Plotting the training and validation accuracy\n    plt.figure(figsize=(10, 5))\n    plt.plot(history.history['accuracy'], label='Training Accuracy')\n    plt.plot(history.history['val_accuracy'], label='Validation Accuracy')\n    plt.title('Training and Validation Accuracy')\n    plt.xlabel('Epochs')\n    plt.ylabel('Accuracy')\n    plt.legend()\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:20.713917Z","iopub.execute_input":"2025-06-07T17:09:20.714142Z","iopub.status.idle":"2025-06-07T17:09:20.720617Z","shell.execute_reply.started":"2025-06-07T17:09:20.714123Z","shell.execute_reply":"2025-06-07T17:09:20.719669Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ----------------------- 5. KAGGLE predict() --------------------------\n\n# make sure gesture_classes exists in both modes\nif TRAIN:\n    gesture_classes = le.classes_\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    global gesture_classes\n    if gesture_classes is None:\n        gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n\n    df_seq = sequence.to_pandas()\n    df_demog = demographics.to_pandas()\n    \n    # remove unneeded demographic columns\n    demog_remove = ['adult_child', 'age', 'sex', 'height_cm']\n    df_demog.drop(columns=demog_remove, inplace=True)\n\n    imu_cols  = [c for c in feature_cols if (c.startswith('acc_') or c.startswith('rot_'))]\n    tof_cols  = [c for c in feature_cols if c.startswith('tof_')]\n    thm_cols  = [c for c in feature_cols if c.startswith('thm_')]\n\n    df_seq = preprocess_all(\n        df_seq,\n        df_demog,\n        feature_cols,\n        imu_cols,\n        tof_cols,\n        thm_cols)\n\n    mat = preprocess_sequence(\n        df_seq,\n        df_demog,\n        feature_cols,\n        imu_cols,\n        tof_cols,\n        thm_cols)\n\n    pad = pad_sequences([mat], maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n    # scale\n    pad[0] = scaler.transform(pad[0]).astype('float32')\n    idx = int(model.predict(pad, verbose=0).argmax(1)[0])\n    return str(gesture_classes[idx])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-07T17:09:20.721624Z","iopub.execute_input":"2025-06-07T17:09:20.721983Z","iopub.status.idle":"2025-06-07T17:09:20.771006Z","shell.execute_reply.started":"2025-06-07T17:09:20.721964Z","shell.execute_reply":"2025-06-07T17:09:20.770236Z"}},"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-07T17:09:20.772109Z","iopub.execute_input":"2025-06-07T17:09:20.772366Z","iopub.status.idle":"2025-06-07T17:09:24.693026Z","shell.execute_reply.started":"2025-06-07T17:09:20.772347Z","shell.execute_reply":"2025-06-07T17:09:24.692053Z"}},"outputs":[],"execution_count":null}]}