{"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":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12451091,"sourceType":"datasetVersion","datasetId":7854327},{"sourceId":506255,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":401943,"modelId":419939}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom scipy.interpolate import CubicSpline\nimport numpy as np\nimport pickle\nfrom keras.src.utils import pad_sequences, to_categorical\nimport joblib\nimport polars as pl\nfrom tqdm import tqdm\nfrom sklearn.utils import compute_class_weight\nimport os\nfrom scipy.spatial.transform import Rotation as R\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\nimport kaggle_evaluation.cmi_inference_server","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:33:43.970409Z","iopub.execute_input":"2025-08-06T02:33:43.971029Z","iopub.status.idle":"2025-08-06T02:34:05.822649Z","shell.execute_reply.started":"2025-08-06T02:33:43.970988Z","shell.execute_reply":"2025-08-06T02:34:05.821648Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#train_df = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv') #(574945, 341)\n#train_demo = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:05.824043Z","iopub.execute_input":"2025-08-06T02:34:05.824572Z","iopub.status.idle":"2025-08-06T02:34:05.829140Z","shell.execute_reply.started":"2025-08-06T02:34:05.824544Z","shell.execute_reply":"2025-08-06T02:34:05.827970Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"TRAIN = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:05.830912Z","iopub.execute_input":"2025-08-06T02:34:05.831214Z","iopub.status.idle":"2025-08-06T02:34:05.855107Z","shell.execute_reply.started":"2025-08-06T02:34:05.831190Z","shell.execute_reply":"2025-08-06T02:34:05.854131Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#classes_count = train_df['gesture'].value_counts()\n#plt.bar(classes_count.index, classes_count.values)\n#plt.xlabel('Class')\n#plt.ylabel('Count')\n#plt.xticks(rotation=90)\n#plt.tight_layout() \n#plt.title('Class Distribution')\n#plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:05.857315Z","iopub.execute_input":"2025-08-06T02:34:05.857624Z","iopub.status.idle":"2025-08-06T02:34:05.876086Z","shell.execute_reply.started":"2025-08-06T02:34:05.857601Z","shell.execute_reply":"2025-08-06T02:34:05.875056Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Class Distribution\n\nThe bar chart below shows the distribution of different action classes within a dataset. The y-axis represents the count of samples for each class, and the x-axis lists the specific actions observed.\n\n## Observations\n\n- **\"Text on phone\"**, **\"Eyebrow - pull hair\"**, and **\"Neck - pinch skin\"** are also among the most common classes.\n- The least frequent class is **\"Pinch knee/leg skin\"**.\n- There is a significant class imbalance.\n\nWe will have to handle the imbalenced dataset\n","metadata":{}},{"cell_type":"code","source":"class Prepdata:\n    def __init__(self, input_file: pd.DataFrame, input_demo: pd.DataFrame, augment_data: bool = False, augment_factor: int = 1, limit_factor=None, pad_percentile=75, calc_rotation_features=True):\n        self.df = input_file.copy()\n        self.demo = input_demo.copy()\n        self.augment_data = augment_data\n        self.augment_factor = augment_factor\n        self.pad_percentile = pad_percentile\n        self.calc_rotation_features = calc_rotation_features\n\n\n        if limit_factor:\n            self.df = self.df.head(limit_factor)\n        \n        self.excluded_cols = {\n            'behavior', 'orientation',  \n            'row_id', 'subject', 'phase', 'sequence_counter' \n        }\n\n        self.is_training_mode = 'sequence_type' in self.df.columns and 'gesture' in self.df.columns\n\n        if self.is_training_mode:\n            self.targets = self.df[['sequence_id', 'sequence_type', 'gesture']].drop_duplicates()\n            self.df = self.df.drop(['sequence_type', 'gesture'], axis=1)\n        else:\n            self.targets = None\n\n        self.df = self.df.merge(self.demo, on='subject')\n\n        \n        self.df = self.clean_dataset(self.df)\n\n        self.IMU_FEATURES = ['acc_x', 'acc_y', 'acc_z', 'rot_w', 'rot_x', 'rot_y', 'rot_z']\n        self.BASE_TOF = [col for col in self.df.columns.to_list() if col.startswith('tof_')]\n\n\n        if self.calc_rotation_features:\n            self.df = self.calculate_enhanced_rotation_features(self.df)\n            self.ROTATION_FEATURES = ['rot_angle', 'acc_mag', 'lin_acc_x', 'lin_acc_y' ,'lin_acc_z', 'linear_acc_mag', 'ang_vel_x', 'ang_vel_y', 'ang_vel_z', 'angular_dist']\n            self.TOF_FEATURES = self.BASE_TOF + ['tof_mean', 'tof_std']\n            self.ALL_FEATURES = self.IMU_FEATURES + self.ROTATION_FEATURES + self.TOF_FEATURES \n        else:\n            self.ROTATION_FEATURES = []\n            self.TOF_FEATURES = self.BASE_TOF\n            self.ALL_FEATURES = self.IMU_FEATURES + self.TOF_FEATURES\n        \n        self.gesture_encoder = LabelEncoder()\n        self.sequence_type_encoder = LabelEncoder()\n        self.scaler = StandardScaler()\n        \n        if self.is_training_mode:\n            self.fit_scaler()\n\n\n\n    def remove_gravity_from_acc(self, acc_data, rot_data):\n        if isinstance(acc_data, pd.DataFrame):\n            acc_values = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n        else:\n            acc_values = acc_data\n\n        if isinstance(rot_data, pd.DataFrame):\n            quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n        else:\n            quat_values = rot_data\n\n        num_samples = acc_values.shape[0]\n        linear_accel = np.zeros_like(acc_values)\n        \n        gravity_world = np.array([0, 0, 9.81])\n\n        for i in range(num_samples):\n            if np.all(np.isnan(quat_values[i])) or np.all(np.isclose(quat_values[i], 0)):\n                linear_accel[i, :] = acc_values[i, :] \n                continue\n\n            try:\n                rotation = R.from_quat(quat_values[i])\n                gravity_sensor_frame = rotation.apply(gravity_world, inverse=True)\n                linear_accel[i, :] = acc_values[i, :] - gravity_sensor_frame\n            except ValueError:\n                linear_accel[i, :] = acc_values[i, :]\n                \n        return linear_accel\n\n    def calculate_angular_velocity_from_quat(self, rot_data, time_delta=1/200):\n        if isinstance(rot_data, pd.DataFrame):\n            quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n        else:\n            quat_values = rot_data\n\n        num_samples = quat_values.shape[0]\n        angular_vel = np.zeros((num_samples, 3))\n\n        for i in range(num_samples - 1):\n            q_t = quat_values[i]\n            q_t_plus_dt = quat_values[i+1]\n\n            if np.all(np.isnan(q_t)) or np.all(np.isclose(q_t, 0)) or np.all(np.isnan(q_t_plus_dt)) or np.all(np.isclose(q_t_plus_dt, 0)):\n                continue\n\n            try:\n                rot_t = R.from_quat(q_t)\n                rot_t_plus_dt = R.from_quat(q_t_plus_dt)\n                delta_rot = rot_t.inv() * rot_t_plus_dt\n                angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n            except ValueError:\n                pass\n                \n        return angular_vel\n\n    def calculate_angular_distance(self, rot_data):\n        if isinstance(rot_data, pd.DataFrame):\n            quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n        else:\n            quat_values = rot_data\n\n        num_samples = quat_values.shape[0]\n        angular_dist = np.zeros(num_samples)\n\n        for i in range(num_samples - 1):\n            q1 = quat_values[i]\n            q2 = quat_values[i+1]\n\n            if np.all(np.isnan(q1)) or np.all(np.isclose(q1, 0)) or \\\n            np.all(np.isnan(q2)) or np.all(np.isclose(q2, 0)):\n                angular_dist[i] = 0\n                continue\n            try:\n                r1 = R.from_quat(q1)\n                r2 = R.from_quat(q2)\n                relative_rotation = r1.inv() * r2\n                angle = np.linalg.norm(relative_rotation.as_rotvec())\n                angular_dist[i] = angle\n            except ValueError:\n                angular_dist[i] = 0\n                \n        return angular_dist\n\n    def calculate_enhanced_rotation_features(self, input_df: pd.DataFrame):\n        input_df = input_df.copy()\n        \n        input_df['acc_mag'] = np.sqrt(input_df['acc_x'] ** 2 + input_df['acc_y'] ** 2 + input_df['acc_z'] **2)\n        input_df['rot_angle'] = 2 * np.arccos(np.clip(np.abs(input_df['rot_w']), 0, 1))\n        \n        acc_data = input_df[['acc_x', 'acc_y', 'acc_z']].values\n        rot_data = input_df[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n        \n        linear_accel = self.remove_gravity_from_acc(acc_data, rot_data)\n        input_df['lin_acc_x'] = linear_accel[:, 0]\n        input_df['lin_acc_y'] = linear_accel[:, 1]\n        input_df['lin_acc_z'] = linear_accel[:, 2]\n        input_df['linear_acc_mag'] = np.sqrt(linear_accel[:, 0]**2 + linear_accel[:, 1]**2 + linear_accel[:, 2]**2)\n        \n        if self.calc_rotation_features:\n            angular_vel = self.calculate_angular_velocity_from_quat(rot_data)\n            input_df['ang_vel_x'] = angular_vel[:, 0]\n            input_df['ang_vel_y'] = angular_vel[:, 1]\n            input_df['ang_vel_z'] = angular_vel[:, 2]\n            \n            angular_dist =self. calculate_angular_distance(rot_data)\n            input_df['angular_dist'] = angular_dist\n        \n        input_df['tof_mean'] = np.mean(input_df[self.BASE_TOF], axis=1)\n        input_df['tof_std'] = np.std(input_df[self.BASE_TOF], axis=1)\n        \n        return input_df\n   \n    def fit_scaler(self):\n        cleaned_df = self.clean_dataset(self.df)\n        feature_data = cleaned_df[self.ALL_FEATURES].values\n        self.scaler.fit(feature_data)\n        print(f\"StandardScaler fitted on {len(self.ALL_FEATURES)} features\")\n    \n    def time_warp_series(self, input_data, sigma=0.2, knot=4):\n        T = input_data.shape[0]\n        orig_steps = np.arange(T)\n\n        warp_factors = np.random.normal(loc=1.0, scale=sigma, size=(knot,))\n        warp_steps = np.linspace(0, T-1, num=knot)\n        warp_curve = CubicSpline(warp_steps, warp_factors)(orig_steps)\n\n        warped_time = np.cumsum(warp_curve)\n        warped_time = warped_time / warped_time[-1] * (T - 1)\n\n        warped_series = np.zeros_like(input_data)\n        for d in range(input_data.shape[1]):\n            cs = CubicSpline(orig_steps, input_data[:, d])\n            warped_series[:, d] = cs(warped_time)\n\n        return warped_series\n    \n    def augment_sequence(self, scaled_data):\n        augmented_sequences = []\n        \n        if np.isnan(scaled_data).sum() == 0:\n            for _ in range(self.augment_factor):\n                warped_data = self.time_warp_series(scaled_data)\n                augmented_sequences.append(warped_data)\n        \n        return augmented_sequences\n    \n    def clean_dataset(self, df: pd.DataFrame):\n        cols_to_drop = [col for col in self.excluded_cols if col in df.columns]\n        df = df.drop(columns=cols_to_drop)\n        df = df.replace(-1.0, np.nan)\n        df = df.ffill().bfill().fillna(0)\n        return df\n\n    def encode_targets(self, input_targets):\n        if input_targets is None:\n            return None, {}, {}\n        \n        encoded_targets = input_targets.copy()\n        \n        encoded_targets['gesture'] = self.gesture_encoder.fit_transform(input_targets['gesture'])\n        gesture_2_id = dict(zip(self.gesture_encoder.classes_, \n                               self.gesture_encoder.transform(self.gesture_encoder.classes_)))\n        \n        encoded_targets['sequence_type'] = self.sequence_type_encoder.fit_transform(input_targets['sequence_type'])\n        sequence_type_2_id = dict(zip(self.sequence_type_encoder.classes_, \n                                    self.sequence_type_encoder.transform(self.sequence_type_encoder.classes_)))\n        \n        return encoded_targets, gesture_2_id, sequence_type_2_id\n    \n    def preprocess_sequence(self, sequence_data):\n        feature_data = sequence_data[self.ALL_FEATURES].values\n        scaled_data = self.scaler.transform(feature_data)\n        return scaled_data\n    \n    def prepare_data(self):\n        cleaned_df = self.df\n        X_sequences, Y, sequence_lengths = [], [], []\n        \n        if self.is_training_mode:\n            encoded_targets, gesture_2_id, sequence_type_2_id = self.encode_targets(self.targets)\n            \n            for seq_id, group in tqdm(cleaned_df.groupby('sequence_id'), desc='Preprocessing data w/ scaling & augmentation: '):\n                \n                scaled_sequence = self.preprocess_sequence(group)\n                \n                if scaled_sequence is not None:\n                    X_sequences.append(scaled_sequence)\n                    sequence_lengths.append(len(scaled_sequence))\n                    \n                    target_gesture = encoded_targets[encoded_targets['sequence_id'] == seq_id]\n                    if not target_gesture.empty:\n                        gesture_label = target_gesture['gesture'].iloc[0]\n                        Y.append(gesture_label)\n                        \n                        if self.augment_data:\n                            augmented_sequences = self.augment_sequence(scaled_sequence)\n                            for aug_seq in augmented_sequences:\n                                X_sequences.append(aug_seq)\n                                sequence_lengths.append(len(aug_seq))\n                                Y.append(gesture_label)\n                    else:\n                        print(f'Found no target for sequence_id: {seq_id}')\n        else:\n            encoded_targets, gesture_2_id, sequence_type_2_id = None, {}, {}\n            \n            for seq_id, group in tqdm(cleaned_df.groupby('sequence_id'), desc='Preprocessing inference data: '):\n                \n                scaled_sequence = self.preprocess_sequence(group)\n                \n                if scaled_sequence is not None:\n                    X_sequences.append(scaled_sequence)\n                    sequence_lengths.append(len(scaled_sequence))\n\n        if sequence_lengths:\n            pad_length = int(np.percentile(sequence_lengths, self.pad_percentile))\n            print(f\"Padding sequences to length: {pad_length} ({self.pad_percentile}th percentile)\")\n            \n            X_padded = pad_sequences(X_sequences, maxlen=pad_length, padding='post', truncating='post', dtype='float32')\n        else:\n            X_padded = np.array([])\n            pad_length = 0\n\n        if self.is_training_mode and Y:\n            n_classes = len(gesture_2_id)\n            Y_categorical = to_categorical(Y, num_classes=n_classes)\n            print(f\"Labels converted to categorical format with {n_classes} classes\")\n        else:\n            Y_categorical = np.array([])\n        return X_padded, Y_categorical, gesture_2_id, sequence_type_2_id, pad_length\n    \n    def save_preprocessing_artifacts(self, export_dir=\"./\"):\n        if self.is_training_mode:\n            joblib.dump(self.scaler, f\"{export_dir}/scaler.pkl\")\n\n            with open(f\"{export_dir}/gesture_map.pkl\", \"wb\") as f:\n                pickle.dump(gesture_2_id, f)\n            \n            with open(f\"{export_dir}/gesture_encoder.pkl\", \"wb\") as f:\n                pickle.dump(self.gesture_encoder, f)\n            \n            with open(f\"{export_dir}/sequence_type_encoder.pkl\", \"wb\") as f:\n                pickle.dump(self.sequence_type_encoder, f)\n            \n            np.save(f\"{export_dir}/feature_cols.npy\", np.array(self.ALL_FEATURES))\n            \n            print(f\"Preprocessing artifacts saved to {export_dir}\")\n\n    def load_preprocessing_artifacts(self, export_dir=\"/kaggle/input/decoding-environment/\"):\n        if not self.is_training_mode:\n            self.scaler = joblib.load(f\"{export_dir}/scaler.pkl\")\n            \n            with open(f\"{export_dir}/gesture_encoder.pkl\", \"rb\") as f:\n                self.gesture_encoder = pickle.load(f)\n            \n            with open(f\"{export_dir}/sequence_type_encoder.pkl\", \"rb\") as f:\n                self.sequence_type_encoder = pickle.load(f)\n            \n            print(f\"Preprocessing artifacts loaded from {export_dir}\")\n\n\nif TRAIN:     \n    prep = Prepdata(train_df, train_demo,  augment_data=True, augment_factor=1)\n    X_padded, Y_categorical, gesture_2_id, sequence_type_2_id, pad_length = prep.prepare_data()\n\n    np.save(\"X_padded_array.npy\", X_padded)\n    np.save(\"Y_categorical_array.npy\", Y_categorical)\n\n\n    prep.save_preprocessing_artifacts()\n\n    with open(\"gesture_map.pkl\", \"rb\") as f:\n        gesture_2_id = pickle.load(f)\n\n\n    predicted_class = np.argmax(Y_categorical, axis=1)\n    classes = np.unique(predicted_class)\n    class_weights = compute_class_weight(class_weight='balanced', classes=classes, y=predicted_class)\n\n    class_weight_dict = {int(class_num): float(weight) for class_num, weight in zip(classes, class_weights)}\n\n    \n    X_train, X_temp, Y_train, Y_temp = train_test_split(X_padded, Y_categorical, random_state=42, test_size=0.2)\n\n    X_test, X_val, Y_test, Y_val = train_test_split(X_temp, Y_temp, test_size=0.2)\n\n    \n    Y_train = Y_train.squeeze() \n    Y_test = Y_test.squeeze() \n    \n    print(f\"\\nFinal shapes:\")\n    print(f\"X_train: {X_train.shape}\")\n    print(f\"Y_train: {Y_train.shape}\")\n    print(f\"X_test: {X_test.shape}\")\n    print(f\"Y_test: {Y_test.shape}\")\n    \n    n_classes = len(gesture_2_id)\n    print(f\"Number of classes: {n_classes}\")\n\n    print(np.var(X_train))\n    print(np.var(X_test))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:05.877200Z","iopub.execute_input":"2025-08-06T02:34:05.877963Z","iopub.status.idle":"2025-08-06T02:34:05.928850Z","shell.execute_reply.started":"2025-08-06T02:34:05.877914Z","shell.execute_reply":"2025-08-06T02:34:05.927276Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import tensorflow as tf\nimport keras\nfrom keras.src.models import Model\nfrom tensorflow.keras.utils import Sequence\nfrom keras.src.layers import (\n    Input, Conv1D, BatchNormalization, Activation, add, MaxPooling1D, Dropout,\n    Bidirectional, LSTM, GlobalAveragePooling1D, Dense, Multiply, Reshape,\n    Lambda, Concatenate, GRU, GaussianNoise, Layer\n)\nfrom keras.src.callbacks import EarlyStopping, ReduceLROnPlateau    , ModelCheckpoint\nfrom tensorflow.keras.regularizers import l2\nfrom sklearn.model_selection import StratifiedKFold\nfrom sklearn.metrics import accuracy_score, f1_score\nfrom keras.src import backend as K\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:05.930642Z","iopub.execute_input":"2025-08-06T02:34:05.931057Z","iopub.status.idle":"2025-08-06T02:34:06.041356Z","shell.execute_reply.started":"2025-08-06T02:34:05.931027Z","shell.execute_reply":"2025-08-06T02:34:06.040540Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"EPOCHS = 50\nBATCH_SIZE = 32\nIMU_DIM = 17\nTOF_DIM = 322\nn_classes = 18","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:06.042752Z","iopub.execute_input":"2025-08-06T02:34:06.043111Z","iopub.status.idle":"2025-08-06T02:34:06.047801Z","shell.execute_reply.started":"2025-08-06T02:34:06.043082Z","shell.execute_reply":"2025-08-06T02:34:06.046889Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@keras.saving.register_keras_serializable()\nclass ComputeSlope(Layer):\n    def __init__(self, **kwargs):\n        super(ComputeSlope, self).__init__(**kwargs)\n    \n    def call(self, x):\n        time_steps = tf.shape(x)[1]\n        time_indices = tf.range(time_steps, dtype=tf.float32)\n        time_indices = tf.reshape(time_indices, [1, -1, 1])\n        \n        n = tf.cast(time_steps, tf.float32)\n        sum_t = tf.reduce_sum(time_indices, axis=1, keepdims=True)\n        sum_x = tf.reduce_sum(x, axis=1, keepdims=True)\n        sum_tx = tf.reduce_sum(x * time_indices, axis=1, keepdims=True)\n        sum_t2 = tf.reduce_sum(time_indices * time_indices, axis=1, keepdims=True)\n        \n        numerator = n * sum_tx - sum_t * sum_x\n        denominator = n * sum_t2 - sum_t * sum_t\n        slope = tf.where(tf.abs(denominator) > 1e-8, numerator / denominator, tf.zeros_like(numerator))\n        \n        return slope\n    \n    def get_config(self):\n        return super().get_config()\n\ndef time_sum(x):\n    return tf.reduce_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, output_shape=lambda input_shape: (input_shape[0], input_shape[1]))(score)\n    weights = Activation('softmax')(score)\n    weights = Lambda(expand_last_axis, output_shape=lambda input_shape: (input_shape[0], input_shape[1], 1))(weights)\n    context = Multiply()([inputs, weights])\n    context = Lambda(time_sum, output_shape=lambda input_shape: (input_shape[0], input_shape[2]))(context)\n    return context\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    \n    def __len__(self):\n        return int(np.ceil(len(self.X) / self.batch))\n    \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    \n    def on_epoch_end(self):\n        np.random.shuffle(self.indices)\n\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\ndef build_two_branch_model(imu_dim, tof_dim, n_classes, wd=1e-4):\n    inp = Input(shape=(None, 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.1, wd=wd)\n    x1 = residual_se_cnn_block(x1, 128, 5, drop=0.1, 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.2)(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.2)(x2)\n\n    merged = Concatenate()([x1, x2])\n\n    xa = Bidirectional(LSTM(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    xb = Bidirectional(GRU(128, return_sequences=True, kernel_regularizer=l2(wd)))(merged)\n    xc = GaussianNoise(0.09)(merged)\n    xc = Dense(16, activation='elu')(xc)\n    \n    x = Concatenate()([xa, xb, xc])\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)\n\nif TRAIN:\n    model = build_two_branch_model(IMU_DIM, TOF_DIM, n_classes)\n    model.summary()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:06.048797Z","iopub.execute_input":"2025-08-06T02:34:06.049140Z","iopub.status.idle":"2025-08-06T02:34:06.087519Z","shell.execute_reply.started":"2025-08-06T02:34:06.049096Z","shell.execute_reply":"2025-08-06T02:34:06.086417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"saving_path = \"fold_models\"\nn_folds = 3\nn_ensemble_models = 2\nif TRAIN:\n    if not os.path.exists(saving_path):\n        os.makedirs(saving_path)\n\n    stk = StratifiedKFold(n_folds)\n\n    Y_train_indicies = np.argmax(Y_train, axis=1)\n\n    fold_predictions = []\n    fold_models = []\n    fold_scores = []\n    print(\"\\nStarting Training...\")\n\n\n    for i, (train_idx, test_idx) in enumerate(stk.split(X_train, Y_train_indicies)):\n        X_fold_train, Y_fold_train = X_train[train_idx], Y_train[train_idx]\n        X_fold_test, Y_fold_test = X_train[test_idx], Y_train[test_idx]\n\n        print(f\"Fold {i + 1} - Train: {X_fold_train.shape[0]}, Val: {X_fold_test.shape[0]}\")\n\n\n        fold_ensemble_models = []\n        fold_ensemble_predictions = []\n\n        for model_idx in range(n_ensemble_models):\n            print(f\"\\nTraining model {model_idx + 1}/{n_ensemble_models} for fold {i + 1}\")\n\n            model = build_two_branch_model(IMU_DIM, TOF_DIM, n_classes)\n            model.compile(optimizer='adam',loss='categorical_crossentropy',metrics=['accuracy'])\n\n            callbacks = [\n                EarlyStopping(monitor='val_loss', patience=8, restore_best_weights=True),\n                ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=4, min_lr=1e-6),\n                ModelCheckpoint(f'{saving_path}/model_fold_{i}_ensemble_{model_idx}.keras', \n                              monitor='val_loss', save_best_only=True, save_weights_only=False)\n            ]\n\n            history = model.fit(\n                X_fold_train, Y_fold_train,\n                epochs=EPOCHS,\n                validation_data=(X_fold_test, Y_fold_test),\n                callbacks=callbacks,\n                class_weight=class_weight_dict,\n                verbose=1\n            )\n\n            model.load_weights(f'{saving_path}/model_fold_{i}_ensemble_{model_idx}.keras')\n            fold_pred = model.predict(X_fold_test, verbose=0)\n            fold_ensemble_predictions.append(fold_pred)\n            fold_ensemble_models.append(model)\n    \n        ensemble_pred = np.mean(fold_ensemble_predictions, axis=0)\n        fold_predictions.append(ensemble_pred)\n        fold_models.append(fold_ensemble_models)\n\n        fold_score = accuracy_score(np.argmax(Y_fold_test, axis=1), np.argmax(ensemble_pred, axis=1))\n        fold_scores.append(fold_score)\n        print(f\"Fold {i + 1} ensemble accuracy: {fold_score:.4f}\")\n\n\n    print(f\"\\nCross-validation scores: {fold_scores}\")\n    print(f\"Mean CV score: {np.mean(fold_scores):.4f} (+/- {np.std(fold_scores) * 2:.4f})\")\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:06.088602Z","iopub.execute_input":"2025-08-06T02:34:06.089083Z","iopub.status.idle":"2025-08-06T02:34:06.114019Z","shell.execute_reply.started":"2025-08-06T02:34:06.088941Z","shell.execute_reply":"2025-08-06T02:34:06.112986Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!zip -r /kaggle/working/fold_models.zip /kaggle/working/fold_models","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:06.116300Z","iopub.execute_input":"2025-08-06T02:34:06.116640Z","iopub.status.idle":"2025-08-06T02:34:06.272429Z","shell.execute_reply.started":"2025-08-06T02:34:06.116611Z","shell.execute_reply":"2025-08-06T02:34:06.271186Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report, confusion_matrix\n\nif TRAIN:\n   test_predictions = []\n   \n   for fold_idx in range(n_folds):\n       fold_ensemble_predictions = []\n       \n       for model_idx in range(n_ensemble_models):\n           model = build_two_branch_model(IMU_DIM, TOF_DIM, n_classes)\n           model.load_weights(f'{saving_path}/model_fold_{fold_idx}_ensemble_{model_idx}.keras')\n           \n           pred = model.predict(X_test, verbose=0)\n           fold_ensemble_predictions.append(pred)\n       \n       fold_ensemble_pred = np.mean(fold_ensemble_predictions, axis=0)\n       test_predictions.append(fold_ensemble_pred)\n   \n   final_predictions = np.mean(test_predictions, axis=0)\n   predicted_classes = np.argmax(final_predictions, axis=1)\n\n   true_labels = np.argmax(Y_test, axis=1)\n   print(\"classificaiton report: \\n\", classification_report(true_labels, predicted_classes))\n\n   print(\"\\nconfusion matrix: \", confusion_matrix(true_labels, predicted_classes))\n   \n\n\n   \n#    print(f\"X_test shape: {X_test.shape}\")\n#    print(f\"Test predictions shape: {final_predictions.shape}\")\n#    print(f\"Predicted classes: {predicted_classes}\")\n\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-06T02:34:06.273712Z","iopub.execute_input":"2025-08-06T02:34:06.274042Z","iopub.status.idle":"2025-08-06T02:34:06.282504Z","shell.execute_reply.started":"2025-08-06T02:34:06.274002Z","shell.execute_reply":"2025-08-06T02:34:06.281403Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"PREPROCESSING_ARTIFACTS = None\n\ndef load_artifacts_once(artifacts_dir=\"/kaggle/input/decoding-environment/\"):\n    global PREPROCESSING_ARTIFACTS\n    \n    if PREPROCESSING_ARTIFACTS is None:\n        PREPROCESSING_ARTIFACTS = {}\n        \n        PREPROCESSING_ARTIFACTS['scaler'] = joblib.load(f\"{artifacts_dir}/scaler.pkl\")\n        \n        with open(f\"{artifacts_dir}/gesture_encoder.pkl\", \"rb\") as f:\n            PREPROCESSING_ARTIFACTS['gesture_encoder'] = pickle.load(f)\n            \n        with open(f\"{artifacts_dir}/sequence_type_encoder.pkl\", \"rb\") as f:\n            PREPROCESSING_ARTIFACTS['sequence_type_encoder'] = pickle.load(f)\n        \n        PREPROCESSING_ARTIFACTS['feature_cols'] = np.load(f\"{artifacts_dir}/feature_cols.npy\")\n        \n        gesture_encoder = PREPROCESSING_ARTIFACTS['gesture_encoder']\n        PREPROCESSING_ARTIFACTS['gesture_2_id'] = dict(zip(\n            gesture_encoder.classes_, \n            gesture_encoder.transform(gesture_encoder.classes_)\n        ))\n        PREPROCESSING_ARTIFACTS['id_2_gesture'] = {v: k for k, v in PREPROCESSING_ARTIFACTS['gesture_2_id'].items()}\n        \n        print(f\"Gestures: {PREPROCESSING_ARTIFACTS['gesture_2_id']}\")\n        print(f\"Expected features: {len(PREPROCESSING_ARTIFACTS['feature_cols'])}\")\n    \n    return PREPROCESSING_ARTIFACTS\n\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    try:\n        artifacts = load_artifacts_once()\n        \n        sequence_pd = sequence.to_pandas()\n        demographics_pd = demographics.to_pandas()\n        \n        prep = Prepdata(sequence_pd, demographics_pd, augment_data=False, augment_factor=1, calc_rotation_features=True)\n        \n        prep.load_preprocessing_artifacts()\n        X_padded, _, _, _, pad_length = prep.prepare_data()\n        \n        if len(X_padded) == 0 or X_padded.size == 0:\n            fallback_gesture = list(artifacts['gesture_2_id'].keys())[0]\n            print(f\"Empty data, returning fallback: {fallback_gesture}\")\n            return fallback_gesture\n        \n        X_padded = np.array(X_padded, dtype=np.float32)\n        if len(X_padded.shape) == 2:\n            X_padded = np.expand_dims(X_padded, axis=0)\n        \n        print(f\"Input shape for prediction: {X_padded.shape}\")\n        \n        all_predictions = []\n\n        for fold_idx in range(n_folds):\n            fold_predictions = []\n            \n            for model_idx in range(n_ensemble_models):\n                try:\n                    ensemble_model = build_two_branch_model(IMU_DIM, TOF_DIM, n_classes)\n\n                    model_path = f'/kaggle/input/model_test_1/keras/default/1/model_fold_{fold_idx}_ensemble_{model_idx}.keras'\n                    ensemble_model.load_weights(model_path)\n                    \n                    pred = ensemble_model.predict(X_padded, verbose=0)\n                    fold_predictions.append(pred)\n                    \n                except Exception as e:\n                    print(f\"Warning: Could not load model fold_{fold_idx}_ensemble_{model_idx}: {e}\")\n                    continue\n            \n            if fold_predictions:\n                fold_ensemble_pred = np.mean(fold_predictions, axis=0)\n                all_predictions.append(fold_ensemble_pred)\n        \n        if not all_predictions:\n            print(\"Warning: No ensemble models could be loaded, using fallback\")\n            fallback_gesture = list(artifacts['gesture_2_id'].keys())[0]\n            return fallback_gesture\n        \n        final_prediction = np.mean(all_predictions, axis=0)\n        \n        if len(final_prediction.shape) > 1:\n            predicted_class_idx = np.argmax(final_prediction[0])\n        else:\n            predicted_class_idx = np.argmax(final_prediction)\n        \n        predicted_gesture = artifacts['id_2_gesture'][predicted_class_idx]\n        \n        print(f\"Predicted gesture: {predicted_gesture} (from {len(all_predictions)} folds)\")\n        return predicted_gesture\n        \n    except Exception as e:\n        print(f\"Error in prediction: {e}\")\n        import traceback\n        traceback.print_exc()\n        \n        try:\n            artifacts = load_artifacts_once()\n            fallback = list(artifacts['gesture_2_id'].keys())[0]\n        except:\n            fallback = \"unknown\"\n        \n        print(f\"Returning fallback gesture: {fallback}\")\n        return fallback\n\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-06T02:34:06.283670Z","iopub.execute_input":"2025-08-06T02:34:06.283968Z","iopub.status.idle":"2025-08-06T02:34:39.897736Z","shell.execute_reply.started":"2025-08-06T02:34:06.283942Z","shell.execute_reply":"2025-08-06T02:34:39.896757Z"}},"outputs":[],"execution_count":null}]}