{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","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":12622598,"sourceType":"datasetVersion","datasetId":7975186}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nfrom pathlib import Path\n\n\n# Giả sử bạn đã add dataset với đường dẫn là /kaggle/input/eda-cleaned-data\ndf_path = \"/kaggle/input/eda-data\"\n\ntrain_df = pd.read_parquet(f\"{df_path}/train_cleaned.parquet\")\ntrain_dem_df = pd.read_parquet(f\"{df_path}/train_demographics.parquet\")\ntest_df = pd.read_parquet(f\"{df_path}/test_cleaned.parquet\")\ntest_dem_df = pd.read_parquet(f\"{df_path}/test_demographics.parquet\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:02.715613Z","iopub.execute_input":"2025-08-03T00:40:02.715911Z","iopub.status.idle":"2025-08-03T00:40:06.496879Z","shell.execute_reply.started":"2025-08-03T00:40:02.715888Z","shell.execute_reply":"2025-08-03T00:40:06.496143Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"USE_PROCESSED_DATA = True  # ← chuyển thành False nếu muốn dùng data gốc\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nEDA_DIR = Path(\"/kaggle/input/eda-data\")  # ← Add Dataset chứa file .parquet\nimport pandas as pd\n\nif USE_PROCESSED_DATA:\n    print(\"📦 Loading EDA-processed data...\")\n    train_df = pd.read_parquet(EDA_DIR / \"train_cleaned.parquet\")\n    train_dem_df = pd.read_parquet(EDA_DIR / \"train_demographics.parquet\")\n    test_df = pd.read_parquet(EDA_DIR / \"test_cleaned.parquet\")\n    test_dem_df = pd.read_parquet(EDA_DIR / \"test_demographics.parquet\")\nelse:\n    print(\"📦 Loading RAW data...\")\n    train_df = pd.read_csv(RAW_DIR / \"train.csv\")\n    train_dem_df = pd.read_csv(RAW_DIR / \"train_demographics.csv\")\n    test_df = pd.read_csv(RAW_DIR / \"test.csv\")\n    test_dem_df = pd.read_csv(RAW_DIR / \"test_demographics.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:06.498183Z","iopub.execute_input":"2025-08-03T00:40:06.498590Z","iopub.status.idle":"2025-08-03T00:40:08.563487Z","shell.execute_reply.started":"2025-08-03T00:40:06.498561Z","shell.execute_reply":"2025-08-03T00:40:08.562599Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, json, joblib, numpy as np, pandas as pd\nfrom pathlib import Path\nimport warnings \nwarnings.filterwarnings(\"ignore\")\n\n\nfrom sklearn.model_selection import train_test_split\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,AdamW\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom tensorflow.keras import backend as K\nimport tensorflow as tf\nimport polars as pl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:08.564514Z","iopub.execute_input":"2025-08-03T00:40:08.564789Z","iopub.status.idle":"2025-08-03T00:40:25.390222Z","shell.execute_reply.started":"2025-08-03T00:40:08.564767Z","shell.execute_reply":"2025-08-03T00:40:25.389437Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# (Competition metric will only be imported when TRAINing)\nTRAIN = False                     # ← set to True when you want to train\nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi025-trained-model\")  # used when TRAIN=False\nEXPORT_DIR = Path(\"./\")                                    # artefacts will be saved here\nBATCH_SIZE = 64\nPAD_PERCENTILE = 95\nLR_INIT = 1e-3\nWD = 3e-4\nMIXUP_ALPHA = 0.4\nEPOCHS = 160\nPATIENCE = 40\n\n\nprint(\"▶ imports ready · tensorflow\", tf.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:25.392567Z","iopub.execute_input":"2025-08-03T00:40:25.393491Z","iopub.status.idle":"2025-08-03T00:40:25.398450Z","shell.execute_reply.started":"2025-08-03T00:40:25.393465Z","shell.execute_reply":"2025-08-03T00:40:25.397554Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras.layers import *\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.regularizers import l2\nimport tensorflow.keras.backend as K\nfrom keras.layers import Input, Dense, Dropout, GlobalMaxPooling1D, Flatten\nfrom keras.models import Model\n\n# --- SE Block (Squeeze-and-Excitation Block) ---\ndef enhanced_se_block(x, reduction=8):\n    ch = x.shape[-1]\n    se1 = GlobalMaxPooling1D()(x)\n    se2 = GlobalAveragePooling1D()(x)\n    se = Concatenate()([se1, se2])\n    \n    se = Dense(max(4, ch // reduction), activation='swish')(se)\n    se = Dense(ch, activation='sigmoid')(se)\n    se = Reshape((1, ch))(se)\n    return Multiply()([x, se])\n\n# --- MS-TCN Block (Multiscale Temporal Convolutional Network) ---\ndef ms_tcn_block(x, filters, dilation_rates=[1, 2, 4, 8], wd=1e-4):\n    branches = []\n    for rate in dilation_rates:\n        b = Conv1D(filters, 3, padding='same', dilation_rate=rate, \n                   use_bias=False, kernel_regularizer=l2(wd))(x)\n        b = BatchNormalization()(b)\n        b = Activation('swish')(b)\n        branches.append(b)\n    out = Concatenate(axis=-1)(branches)\n    out = Conv1D(filters, 1, use_bias=False, kernel_regularizer=l2(wd))(out)\n    out = BatchNormalization()(out)\n    return Activation('swish')(out)\n\n# --- Cross Attention Fusion ---\ndef cross_attention_fusion(x1, x2, filters, wd=1e-4):\n    q1 = Conv1D(filters//2, 1, use_bias=False, kernel_regularizer=l2(wd))(x1)\n    k1 = Conv1D(filters//2, 1, use_bias=False, kernel_regularizer=l2(wd))(x2)\n    v1 = Conv1D(filters, 1, use_bias=False, kernel_regularizer=l2(wd))(x2)\n\n    def compute_attention(inputs):\n        q, k, v = inputs\n        scores = tf.matmul(q, k, transpose_b=True)\n        scores = scores / tf.math.sqrt(tf.cast(tf.shape(k)[-1], tf.float32))\n        weights = tf.nn.softmax(scores, axis=-1)\n        return tf.matmul(weights, v)\n\n    attn1 = Lambda(compute_attention)([q1, k1, v1])\n\n    q2 = Conv1D(filters//2, 1, use_bias=False, kernel_regularizer=l2(wd))(x2)\n    k2 = Conv1D(filters//2, 1, use_bias=False, kernel_regularizer=l2(wd))(x1)\n    v2 = Conv1D(filters, 1, use_bias=False, kernel_regularizer=l2(wd))(x1)\n\n    attn2 = Lambda(compute_attention)([q2, k2, v2])\n\n    fused = Concatenate()([attn1, attn2])\n    fused = Conv1D(filters, 1, use_bias=False, kernel_regularizer=l2(wd))(fused)\n    return BatchNormalization()(fused)\n\nclass TimeAlign(layers.Layer):\n    def __init__(self, target_steps, **kwargs):\n        super().__init__(**kwargs)\n        self.target_steps = target_steps\n\n    def call(self, x):\n        current_steps = tf.shape(x)[1]\n        diff = self.target_steps - current_steps\n\n        def pad_fn():\n            paddings = [[0, 0], [0, diff], [0, 0]]\n            return tf.pad(x, paddings)\n\n        def crop_fn():\n            return x[:, :self.target_steps, :]\n\n        return tf.cond(diff > 0, pad_fn, crop_fn)\n\n    def get_config(self):\n        return {'target_steps': self.target_steps}\n\ndef pyramid_fusion(imu_features, tof_features, target_steps, wd=1e-4):\n    fusion_layers = []\n\n    for imu, tof in zip(imu_features, tof_features):\n        imu = TimeAlign(target_steps)(imu)\n        tof = TimeAlign(target_steps)(tof)\n\n        fused = cross_attention_fusion(imu, tof, 128, wd)\n        fused = enhanced_se_block(fused)\n\n        fusion_layers.append(fused)\n\n    fusion_output = Concatenate(axis=-1)(fusion_layers)\n    return fusion_output\n\nclass AdaptiveSampling(Layer):\n    def __init__(self, input_steps, output_steps, **kwargs):\n        super().__init__(**kwargs)\n        self.input_steps = input_steps\n        self.output_steps = output_steps\n\n    def build(self, input_shape):\n        self.kernel = self.add_weight(\n            name=\"sampling_kernel\",\n            shape=(self.input_steps, self.output_steps),\n            initializer=\"glorot_uniform\",\n            trainable=True,\n        )\n\n    def call(self, inputs):\n        weights = tf.nn.softmax(self.kernel, axis=-1)\n        return tf.einsum('btc,ts->bsc', inputs, weights)\n\n    def compute_output_shape(self, input_shape):\n        return (input_shape[0], self.output_steps, input_shape[2])\n\n    def get_config(self):\n        config = super().get_config()\n        config.update({\"input_steps\": self.input_steps, \"output_steps\": self.output_steps})\n        return config\n\ndef build_enhanced_model(pad_len, imu_dim, tof_dim, n_classes, wd):\n    imu_input = Input(shape=(pad_len, imu_dim), name='imu_input')\n    tof_input = Input(shape=(pad_len, tof_dim), name='tof_input')\n\n    x1 = Conv1D(64, 3, padding='same', activation='relu', kernel_regularizer=l2(wd))(imu_input)\n    x1 = Dropout(0.2)(x1)\n    x1 = Conv1D(64, 3, padding='same', activation='relu', kernel_regularizer=l2(wd))(x1)\n\n    x2 = Conv1D(64, 3, padding='same', activation='relu', kernel_regularizer=l2(wd))(tof_input)\n    x2 = Dropout(0.2)(x2)\n    x2 = Conv1D(64, 3, padding='same', activation='relu', kernel_regularizer=l2(wd))(x2)\n\n    fused = pyramid_fusion([x1], [x2], pad_len, wd)\n    fused = AdaptiveSampling(input_steps=pad_len, output_steps=128)(fused)\n\n\n    context = GlobalMaxPooling1D()(fused)\n\n    x = context\n    x = Dense(256, activation='swish')(x)\n    x = Dropout(0.5)(x)\n    x = Dense(128, activation='swish')(x)\n    x = Dropout(0.3)(x)\n    output = Dense(n_classes, activation='softmax')(x)\n\n    return Model(inputs=[imu_input, tof_input], outputs=output)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:25.399449Z","iopub.execute_input":"2025-08-03T00:40:25.399729Z","iopub.status.idle":"2025-08-03T00:40:25.431539Z","shell.execute_reply.started":"2025-08-03T00:40:25.399704Z","shell.execute_reply":"2025-08-03T00:40:25.430607Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Utility Functions","metadata":{}},{"cell_type":"code","source":"import tensorflow as tf\nfrom tensorflow.keras import layers, models\nfrom tensorflow.keras import backend as K\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.layers import *\nfrom tensorflow.keras.models import Model\n\n# --- Tensor Manipulation Helper Functions ---\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\n# --- SE Block (Squeeze-and-Excitation Block) ---\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\n# --- Multi-Scale CNN Branch ---\ndef multi_scale_cnn_branch(x, filters=64, wd=1e-4):\n    convs = []\n    for ks in [3, 5, 7, 11]:\n        conv = Conv1D(filters, ks, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x)\n        conv = BatchNormalization()(conv)\n        conv = Activation('relu')(conv)\n        convs.append(conv)\n    out = Concatenate()(convs)\n    return out\n\n# --- ResNet + SE Block ---\ndef resnet_se_block(x, filters, wd=1e-4):\n    shortcut = x\n    x = Conv1D(filters, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x)\n    x = BatchNormalization()(x)\n    x = Activation('relu')(x)\n    x = Conv1D(filters, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x)\n    x = BatchNormalization()(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])\n    x = Activation('relu')(x)\n    return x\n\n# --- Attention Layer ---\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# --- TimeAlign Layer (for padding/truncating sequences to a fixed length) ---\nclass TimeAlign(Layer):\n    def __init__(self, target_steps, **kwargs):\n        super().__init__(**kwargs)\n        self.target_steps = target_steps\n\n    def call(self, x):\n        current_steps = tf.shape(x)[1]\n        diff = self.target_steps - current_steps\n\n        def pad_fn():\n            paddings = [[0, 0], [0, diff], [0, 0]]\n            return tf.pad(x, paddings)\n\n        def crop_fn():\n            return x[:, :self.target_steps, :]\n\n        return tf.cond(diff > 0, pad_fn, crop_fn)\n\n    def get_config(self):\n        return {'target_steps': self.target_steps}\n\n# --- Building the Two-Branch Model ---\ndef build_enhanced_model(pad_len, imu_dim, tof_dim, n_classes, wd=1e-4):\n    inp = Input(shape=(pad_len, imu_dim + tof_dim), name='main_input')\n\n    imu = Lambda(lambda t: t[:, :, :imu_dim])(inp)\n    tof = Lambda(lambda t: t[:, :, imu_dim:])(inp)\n\n    imu_features = []\n    x1 = imu\n    for i in range(3):\n        x1 = ms_tcn_block(x1, filters=64*(i+1), wd=wd)\n        x1 = enhanced_se_block(x1)\n        imu_features.append(x1)\n        x1 = Conv1D(x1.shape[-1], 3, strides=2, padding='same', kernel_regularizer=l2(wd))(x1)\n        x1 = Dropout(0.2 + 0.1*i)(x1)\n\n    tof_features = []\n    x2 = tof\n    for i in range(3):\n        x2 = ms_tcn_block(x2, filters=32*(i+1), wd=wd)\n        x2 = enhanced_se_block(x2)\n        tof_features.append(x2)\n        x2 = Conv1D(x2.shape[-1], 3, strides=2, padding='same', kernel_regularizer=l2(wd))(x2)\n        x2 = Dropout(0.2 + 0.1*i)(x2)\n\n    fused = pyramid_fusion(imu_features, tof_features, pad_len, wd)\n    fused = AdaptiveSampling(input_steps=pad_len, output_steps=128)(fused)\n\n\n    x = Bidirectional(GRU(128, return_sequences=True, kernel_regularizer=l2(wd)))(fused)\n    x = MultiHeadAttention(num_heads=4, key_dim=32, kernel_regularizer=l2(wd))(x, x)\n    context = GlobalMaxPooling1D()(x)\n\n    x = context\n    x = Dense(256, activation='swish')(x)\n    x = Dropout(0.5)(x)\n    x = Dense(128, activation='swish')(x)\n    x = Dropout(0.3)(x)\n    out = Dense(n_classes, activation='softmax', kernel_regularizer=l2(wd))(x)\n\n    model = Model(inputs=inp, outputs=out)\n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:25.432620Z","iopub.execute_input":"2025-08-03T00:40:25.433404Z","iopub.status.idle":"2025-08-03T00:40:25.456207Z","shell.execute_reply.started":"2025-08-03T00:40:25.433373Z","shell.execute_reply":"2025-08-03T00:40:25.455378Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Helpers","metadata":{}},{"cell_type":"markdown","source":"## Model Definition - Two Branch Architecture","metadata":{}},{"cell_type":"code","source":"# Normalizes and cleans the time series sequence. \n\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list[str], scaler: StandardScaler):\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(mat).astype('float32')\n\n# MixUp the data argumentation in order to regularize the neural network. \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)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:25.457026Z","iopub.execute_input":"2025-08-03T00:40:25.457269Z","iopub.status.idle":"2025-08-03T00:40:25.478153Z","shell.execute_reply.started":"2025-08-03T00:40:25.457250Z","shell.execute_reply":"2025-08-03T00:40:25.477075Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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    # --- IMU 多尺度 CNN + ResNet + SE ---\n    x1 = multi_scale_cnn_branch(imu, filters=64, wd=wd)\n    x1 = resnet_se_block(x1, filters=128, wd=wd)\n    x1 = MaxPooling1D(pool_size=2)(x1)\n    x1 = Dropout(0.3)(x1)\n    x1 = MaxPooling1D(pool_size=2)(x1)\n\n    # --- TOF 分支保持原样 ---\n    x2 = multi_scale_cnn_branch(tof, filters=64, wd=wd)\n    x2 = resnet_se_block(x2, filters=128, wd=wd)\n    x2 = MaxPooling1D(pool_size=2)(x2)\n    x2 = Dropout(0.3)(x2)\n    x2 = MaxPooling1D(pool_size=2)(x2)\n\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    # --- 全连接层 ---\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-08-03T00:40:25.479221Z","iopub.execute_input":"2025-08-03T00:40:25.479596Z","iopub.status.idle":"2025-08-03T00:40:25.494164Z","shell.execute_reply.started":"2025-08-03T00:40:25.479566Z","shell.execute_reply":"2025-08-03T00:40:25.493277Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training / Inference Pipeline","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n\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\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    print(f\"  IMU {len(imu_cols)} | TOF/THM {len(tof_cols)} | total {len(feature_cols)} features\")\n\n    # global scaler\n    scaler = StandardScaler().fit(df[feature_cols].ffill().bfill().fillna(0).values)\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n\n    # build sequences\n    seq_gp = df.groupby('sequence_id')\n    X_list, y_list, lens = [], [], []\n    for seq_id, seq in seq_gp:\n        mat = preprocess_sequence(seq, feature_cols, scaler)\n        X_list.append(mat)\n        y_list.append(seq['gesture_int'].iloc[0])\n        lens.append(len(mat))\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    # 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))\n    \n    # model\n    model = build_enhanced_model(pad_len=pad_len,imu_dim=len(imu_cols),tof_dim=len(tof_cols),n_classes=len(le.classes_),wd=WD)\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 / \"gesture_two_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    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    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        'multi_scale_cnn_branch': multi_scale_cnn_branch,\n        'resnet_se_block':resnet_se_block,\n        'attention_layer': attention_layer,\n    }\n    model = load_model(PRETRAINED_DIR / \"gesture_two_branch_mixup.h5\",\n                       compile=False, custom_objects=custom_objs)\n    print(\"  model, scaler, pads loaded – ready for evaluation\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T00:40:25.495101Z","iopub.execute_input":"2025-08-03T00:40:25.495360Z","iopub.status.idle":"2025-08-03T03:10:05.764203Z","shell.execute_reply.started":"2025-08-03T00:40:25.495342Z","shell.execute_reply":"2025-08-03T03:10:05.761298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import f1_score, classification_report\n\n# Tính lại F1-score trên validation set tốt nhất\nmodel.set_weights(best_model_state)\nmodel.eval()\n\nall_preds = []\nall_targets = []\n\nwith torch.no_grad():\n    for batch_x, batch_y in val_loader:\n        batch_x = batch_x.to(device)\n        batch_y = batch_y.to(device)\n        outputs = model(batch_x)\n        preds = outputs.argmax(dim=1).cpu().numpy()\n        targets = batch_y.argmax(dim=1).cpu().numpy()\n        all_preds.extend(preds)\n        all_targets.extend(targets)\n\n# F1-score\nval_f1_macro = f1_score(all_targets, all_preds, average='macro')\nval_f1_weighted = f1_score(all_targets, all_preds, average='weighted')\n\nprint(f\"\\n🔍 Final Validation Results on Best Model:\")\nprint(f\"Macro F1-score:    {val_f1_macro:.4f}\")\nprint(f\"Weighted F1-score: {val_f1_weighted:.4f}\")\n\n# Nếu bạn có gesture_classes (label names), in bảng chi tiết:\nprint(classification_report(all_targets, all_preds, target_names=gesture_classes))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T03:10:05.765810Z","iopub.status.idle":"2025-08-03T03:10:05.766195Z","shell.execute_reply.started":"2025-08-03T03:10:05.766041Z","shell.execute_reply":"2025-08-03T03:10:05.766058Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Predicting","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom tensorflow.keras.preprocessing.sequence import pad_sequences\nfrom sklearn.preprocessing import LabelEncoder\nimport joblib\n\n# Ensure the model and gesture_classes are only loaded once\ngesture_classes = None\nmodel = None\nscaler = None\npad_len = None\n\ndef load_model_and_resources():\n    global model, gesture_classes, scaler, pad_len\n    if model is None:\n        # Load the pretrained model\n        model = tf.keras.models.load_model(PRETRAINED_DIR / \"gesture_two_branch_mixup.h5\")\n        print(\"Model loaded.\")\n        \n    if gesture_classes is None:\n        # Load gesture classes\n        gesture_classes = np.load(PRETRAINED_DIR / \"gesture_classes.npy\", allow_pickle=True)\n        print(\"Gesture classes loaded.\")\n        \n    if scaler is None:\n        # Load scaler\n        scaler = joblib.load(PRETRAINED_DIR / \"scaler.pkl\")\n        print(\"Scaler loaded.\")\n        \n    if pad_len is None:\n        # Load padding length\n        pad_len = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\n        print(\"Padding length loaded.\")\n\n# Call this function at the start of the script\nload_model_and_resources()\n\ndef preprocess_sequence(df_seq: pd.DataFrame, feature_cols: list[str], scaler: object) -> np.ndarray:\n    # Ensure all missing values are handled, and the sequence is scaled properly\n    mat = df_seq[feature_cols].ffill().bfill().fillna(0).values\n    return scaler.transform(mat).astype('float32')\n\ndef predict(sequence: pd.DataFrame, demographics: pd.DataFrame) -> str:\n    # Ensure model and resources are loaded\n    load_model_and_resources()\n\n    # Convert the sequence to pandas if it's a polars dataframe\n    df_seq = sequence.to_pandas()\n\n    # Preprocess sequence and pad it\n    mat = preprocess_sequence(df_seq, feature_cols, scaler)\n    pad = pad_sequences([mat], maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n\n    # Make prediction and retrieve the class\n    pred_idx = int(model.predict(pad, verbose=0).argmax(1)[0])\n    return str(gesture_classes[pred_idx])\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-03T03:10:05.768742Z","iopub.status.idle":"2025-08-03T03:10:05.769009Z","shell.execute_reply.started":"2025-08-03T03:10:05.768891Z","shell.execute_reply":"2025-08-03T03:10:05.768902Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Integration with Kaggle Evaluation","metadata":{}},{"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-08-03T03:10:05.770029Z","iopub.status.idle":"2025-08-03T03:10:05.770515Z","shell.execute_reply.started":"2025-08-03T03:10:05.770266Z","shell.execute_reply":"2025-08-03T03:10:05.770285Z"}},"outputs":[],"execution_count":null}]}