{"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":12718424,"sourceType":"datasetVersion","datasetId":8016261}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport gc\nimport numpy as np\nimport pandas as pd\nimport random\nimport warnings\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport joblib\n\n# Import all necessary Keras components\nimport tensorflow as tf\nfrom tensorflow.keras.models import Model\nfrom tensorflow.keras.layers import Input, Conv1D, BatchNormalization, Activation, Dense, Dropout, Concatenate, GlobalAveragePooling1D, GaussianNoise, add, Multiply, Reshape, MaxPooling1D, GRU, Bidirectional, Attention\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau\nfrom tensorflow.keras.utils import to_categorical, pad_sequences, Sequence\nfrom tensorflow.keras.losses import CategoricalCrossentropy\n\n# Import all necessary Sklearn components\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom scipy.spatial.transform import Rotation as R\nfrom scipy.fft import rfft\n\nwarnings.filterwarnings(\"ignore\")\n\n# ===================================================================================\n# Configuration\n# ===================================================================================\nclass CFG:\n    # Set the mode here: 'TRAIN' or 'INFERENCE'\n    MODE = 'INFERENCE' \n\n    # --- Paths ---\n    OUTPUT_DIR = Path(\"/kaggle/working/\")\n    MODEL_INPUT_DIR = Path(\"/kaggle/input/cmi-detection-training-output/\") \n\n    # --- General ---\n    SEED = 42\n    \n    # --- Training Params ---\n    BATCH_SIZE = 64\n    EPOCHS = 200\n    LR = 5e-4\n    PATIENCE = 35 \n    LR_PATIENCE = 10\n    LR_FACTOR = 0.5\n    PAD_LEN_PERCENTILE = 95\n    MIXUP_ALPHA = 0.4\n    LABEL_SMOOTHING = 0.1\n    \n    # --- NEW: Inference Param ---\n    TTA_STEPS = 5 # Number of augmentations for Test-Time Augmentation\n\ndef seed_everything(seed): os.environ['PYTHONHASHSEED'] = str(seed); random.seed(seed); np.random.seed(seed); tf.random.set_seed(seed)\nseed_everything(CFG.SEED)\n\n# ===================================================================================\n# Augmentation Function (Needed in both modes)\n# ===================================================================================\ndef _augment_sequence(sequence):\n    # Magnitude Scaling\n    scale = np.random.uniform(0.9, 1.1)\n    sequence = sequence * scale\n    # Time Shifting\n    shift = np.random.randint(-10, 10)\n    sequence = np.roll(sequence, shift, axis=0)\n    return sequence\n\n# ===================================================================================\n# Feature Engineering (Needed in both modes)\n# ===================================================================================\ndef calculate_angular_velocity(quat_values, time_delta=1/50):\n    angular_vel = np.zeros((len(quat_values), 3));\n    for i in range(len(quat_values) - 1):\n        q_t, q_t_plus_dt = quat_values[i], quat_values[i+1];\n        if np.any(np.isnan(q_t)) or np.any(np.isnan(q_t_plus_dt)): continue\n        try: rot_t = R.from_quat(q_t); rot_t_plus_dt = R.from_quat(q_t_plus_dt); delta_rot = rot_t.inv() * rot_t_plus_dt; angular_vel[i, :] = delta_rot.as_rotvec() / time_delta\n        except: pass\n    return angular_vel\ndef remove_gravity(df_group):\n    acc = df_group[['acc_x', 'acc_y', 'acc_z']].values; quat = df_group[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    linear_accel = np.zeros_like(acc); gravity = np.array([0, 0, 9.81])\n    for i in range(len(acc)):\n        try: rotation = R.from_quat(quat[i]); gravity_sensor = rotation.apply(gravity, inverse=True); linear_accel[i, :] = acc[i, :] - gravity_sensor\n        except: linear_accel[i, :] = acc[i, :]\n    return linear_accel\ndef engineer_features(df):\n    processed = [];\n    for _, group in tqdm(df.groupby('sequence_id'), desc=\"Engineering Features\", leave=False, bar_format='{l_bar}{bar:10}{r_bar}'):\n        g = group.copy(); linear_accel = remove_gravity(g); g['linear_acc_x'], g['linear_acc_y'], g['linear_acc_z'] = linear_accel.T; angular_vel = calculate_angular_velocity(g[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values); g['angular_vel_x'], g['angular_vel_y'], g['angular_vel_z'] = angular_vel.T\n        for axis in ['x', 'y', 'z']: g[f'linear_acc_{axis}_jerk'] = g[f'linear_acc_{axis}'].diff().fillna(0)\n        g['tof_neg1_pct'] = (g[[c for c in g.columns if c.startswith('tof_') and '_v' in c]] == -1).sum(axis=1) / 320.0; motion_features = ['acc_x', 'acc_y', 'acc_z', 'linear_acc_x', 'linear_acc_y', 'linear_acc_z', 'angular_vel_x', 'angular_vel_y', 'angular_vel_z']\n        for col in motion_features: g[f'{col}_std_roll5'] = g[col].rolling(window=5, min_periods=1).std().fillna(0); g[f'{col}_skew_roll5'] = g[col].rolling(window=5, min_periods=1).skew().fillna(0)\n        for col in motion_features:\n            fft_vals = np.abs(rfft(g[col].values))\n            if len(fft_vals) > 4: top_freq_indices = np.argsort(fft_vals[1:])[-3:] + 1; g[f'{col}_fft_mag1'] = fft_vals[top_freq_indices[-1]] if len(top_freq_indices) > 0 else 0; g[f'{col}_fft_mag2'] = fft_vals[top_freq_indices[-2]] if len(top_freq_indices) > 1 else 0; g[f'{col}_fft_mag3'] = fft_vals[top_freq_indices[-3]] if len(top_freq_indices) > 2 else 0\n            else: g[f'{col}_fft_mag1'], g[f'{col}_fft_mag2'], g[f'{col}_fft_mag3'] = 0, 0, 0\n        processed.append(g)\n    return pd.concat(processed).fillna(0)\n\n# ===================================================================================\n# Model Architectures & Data Generators\n# ===================================================================================\nclass MixupGenerator(Sequence):\n    def __init__(self, X, y, batch_size, alpha=0.2, shuffle=True):\n        self.X_imu, self.X_thm_tof = X[0], X[1]; self.y, self.batch_size, self.alpha, self.shuffle = y, batch_size, alpha, shuffle; self.indices = np.arange(len(self.y)); self.on_epoch_end()\n    def __len__(self): return int(np.floor(len(self.y) / self.batch_size))\n    def __getitem__(self, index):\n        indices = self.indices[index*self.batch_size:(index+1)*self.batch_size]; X_imu_aug = np.array([_augment_sequence(x) for x in self.X_imu[indices]]); X_thm_tof_aug = np.array([_augment_sequence(x) for x in self.X_thm_tof[indices]]); lam = np.random.beta(self.alpha, self.alpha); perm = np.random.permutation(len(indices)); X_imu_mix = lam * X_imu_aug + (1 - lam) * X_imu_aug[perm]; X_thm_tof_mix = lam * X_thm_tof_aug + (1 - lam) * X_thm_tof_aug[perm]; y_mix = lam * self.y[indices] + (1 - lam) * self.y[indices[perm]]; return (X_imu_mix, X_thm_tof_mix), y_mix\n    def on_epoch_end(self):\n        if self.shuffle: np.random.shuffle(self.indices)\nclass MixupGeneratorIMU(Sequence):\n    def __init__(self, X, y, batch_size, alpha=0.2, shuffle=True):\n        self.X, self.y, self.batch_size, self.alpha, self.shuffle = X, y, batch_size, alpha, shuffle; self.indices = np.arange(len(self.y)); self.on_epoch_end()\n    def __len__(self): return int(np.floor(len(self.y) / self.batch_size))\n    def __getitem__(self, index):\n        indices = self.indices[index*self.batch_size:(index+1)*self.batch_size]; X_aug = np.array([_augment_sequence(x) for x in self.X[indices]]); lam = np.random.beta(self.alpha, self.alpha); perm = np.random.permutation(len(indices)); X_mix = lam * X_aug + (1 - lam) * X_aug[perm]; y_mix = lam * self.y[indices] + (1 - lam) * self.y[indices[perm]]; return X_mix, y_mix\n    def on_epoch_end(self):\n        if self.shuffle: np.random.shuffle(self.indices)\ndef se_block(x, reduction=8):\n    channels = x.shape[-1]; se = GlobalAveragePooling1D()(x); se = Dense(channels // reduction, activation='relu')(se); se = Dense(channels, activation='sigmoid')(se); return Multiply()([x, Reshape((1, channels))(se)])\ndef residual_se_cnn_block(x, filters, kernel_size, wd, pool_size=2, drop=0.4):\n    shortcut = x; x = Conv1D(filters, kernel_size, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x); x = BatchNormalization()(x); x = Activation('relu')(x); x = Conv1D(filters, kernel_size, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x); x = BatchNormalization()(x); x = Activation('relu')(x); x = se_block(x)\n    if shortcut.shape[-1] != filters: shortcut = Conv1D(filters, 1, padding='same', use_bias=False, kernel_regularizer=l2(wd))(shortcut)\n    x = add([x, shortcut]); x = Activation('relu')(x); x = MaxPooling1D(pool_size)(x); x = Dropout(drop)(x); return x\ndef build_slim_model(pad_len, imu_dim, thm_tof_dim, n_classes, wd=5e-4):\n    inp_imu = Input(shape=(pad_len, imu_dim), name='imu_input'); x1 = Dense(64, activation='relu')(inp_imu); x1 = GaussianNoise(0.02)(x1); x1 = residual_se_cnn_block(x1, 32, 5, wd=wd); x1 = residual_se_cnn_block(x1, 64, 3, wd=wd); x1 = Bidirectional(GRU(64, return_sequences=True, kernel_regularizer=l2(wd)))(x1); attention_out = Attention()([x1, x1]); x1 = GlobalAveragePooling1D()(attention_out)\n    inp_thm_tof = Input(shape=(pad_len, thm_tof_dim), name='thm_tof_input'); x2 = Dense(32, activation='relu')(inp_thm_tof); x2 = GaussianNoise(0.02)(x2); x2 = residual_se_cnn_block(x2, 32, 5, wd=wd); x2 = GlobalAveragePooling1D()(x2)\n    merged = Concatenate()([x1, x2]); x = Dense(128, activation='relu', kernel_regularizer=l2(wd))(merged); x = Dropout(0.5)(x); out = Dense(n_classes, activation='softmax')(x); return Model(inputs=[inp_imu, inp_thm_tof], outputs=out)\ndef build_imu_only_slim_model(pad_len, imu_dim, n_classes, wd=5e-4):\n    inp_imu = Input(shape=(pad_len, imu_dim), name='imu_input'); x1 = Dense(64, activation='relu')(inp_imu); x1 = GaussianNoise(0.02)(x1); x1 = residual_se_cnn_block(x1, 32, 5, wd=wd); x1 = residual_se_cnn_block(x1, 64, 3, wd=wd); x1 = Bidirectional(GRU(64, return_sequences=True, kernel_regularizer=l2(wd)))(x1); attention_out = Attention()([x1, x1]); x1 = GlobalAveragePooling1D()(attention_out)\n    x = Dense(128, activation='relu', kernel_regularizer=l2(wd))(x1); x = Dropout(0.5)(x); out = Dense(n_classes, activation='softmax')(x); return Model(inputs=inp_imu, outputs=out)\n\n###################################################################################\n# --- MAIN EXECUTION BLOCK ---\n###################################################################################\nif __name__ == \"__main__\":\n    \n    if CFG.MODE == 'TRAIN':\n        print(\"Script is in TRAIN mode. Please re-run this script in a separate notebook to generate artifacts.\")\n        # The training code would go here. It remains unchanged from the previous version.\n    \n    else: # INFERENCE MODE\n        print(\"Running in INFERENCE mode.\")\n        if os.path.exists('/kaggle/input/cmi-detect-behavior-with-sensor-data/test.csv'):\n            import kaggle_evaluation.cmi_inference_server\n        else: # Mock server for local testing\n            class MockInferenceServer:\n                def __init__(self, pred_func): print(\"Mock server initialized.\")\n                def serve(self): print(\"Mock server `serve()` called.\")\n                def run_local_gateway(self, data_paths): print(f\"Mock server `run_local_gateway` called.\")\n            kaggle_evaluation = type('Kaggle', (), {'cmi_inference_server': type('CMI', (), {'CMIInferenceServer': MockInferenceServer})})()\n\n        print(\"Loading artifacts...\"); LE = joblib.load(CFG.MODEL_INPUT_DIR / 'label_encoder.joblib'); PAD_LEN = joblib.load(CFG.MODEL_INPUT_DIR / 'pad_len.joblib'); ADV_IMU_COLS = joblib.load(CFG.MODEL_INPUT_DIR / 'adv_imu_cols.joblib'); THM_TOF_COLS = joblib.load(CFG.MODEL_INPUT_DIR / 'thm_tof_cols.joblib'); SCALER_IMU = joblib.load(CFG.MODEL_INPUT_DIR / 'scaler_imu.joblib'); SCALER_THM_TOF = joblib.load(CFG.MODEL_INPUT_DIR / 'scaler_thm_tof.joblib'); N_CLASSES = len(LE.classes_)\n        print(\"Loading models...\"); MODEL_FULL = build_slim_model(PAD_LEN, len(ADV_IMU_COLS), len(THM_TOF_COLS), N_CLASSES); MODEL_FULL.load_weights(CFG.MODEL_INPUT_DIR / 'full_model.weights.h5'); MODEL_IMU_ONLY = build_imu_only_slim_model(PAD_LEN, len(ADV_IMU_COLS), N_CLASSES); MODEL_IMU_ONLY.load_weights(CFG.MODEL_INPUT_DIR / 'imu_only_model.weights.h5'); print(\"Artifacts loaded.\")\n\n        def predict(sequence_df_polars, demographics_df_polars):\n            sequence_df = sequence_df_polars.to_pandas()\n            demographics_df = demographics_df_polars.to_pandas()\n            sequence_df = pd.merge(sequence_df, demographics_df, on='subject', how='left')\n            sensor_cols_inf = [c for c in sequence_df.columns if c.startswith(('acc_', 'rot_', 'thm_', 'tof_'))]\n            for col in sensor_cols_inf: sequence_df[col] = pd.to_numeric(sequence_df[col], errors='coerce')\n            sequence_df['sequence_id'] = 0; processed_df = engineer_features(sequence_df)\n            is_imu_only = processed_df[THM_TOF_COLS].isnull().all().all()\n            \n            # --- Store predictions from each TTA step ---\n            tta_preds = []\n            \n            # --- Extract original data once ---\n            original_imu_data = processed_df[ADV_IMU_COLS].ffill().bfill().fillna(0).values\n            if not is_imu_only:\n                original_thm_tof_data = processed_df[THM_TOF_COLS].ffill().bfill().fillna(0).values\n            \n            for i in range(CFG.TTA_STEPS):\n                # Use original data on the first step, augment on subsequent steps\n                imu_data = _augment_sequence(np.copy(original_imu_data)) if i > 0 else original_imu_data\n                \n                if is_imu_only:\n                    scaled_data = SCALER_IMU.transform(imu_data)\n                    padded_data = pad_sequences([scaled_data], maxlen=PAD_LEN, dtype='float32')\n                    preds = MODEL_IMU_ONLY.predict_on_batch(padded_data)\n                else:\n                    thm_tof_data = _augment_sequence(np.copy(original_thm_tof_data)) if i > 0 else original_thm_tof_data\n                    scaled_imu = SCALER_IMU.transform(imu_data)\n                    scaled_thm_tof = SCALER_THM_TOF.transform(thm_tof_data)\n                    padded_imu = pad_sequences([scaled_imu], maxlen=PAD_LEN, dtype='float32')\n                    padded_thm_tof = pad_sequences([scaled_thm_tof], maxlen=PAD_LEN, dtype='float32')\n                    preds = MODEL_FULL.predict_on_batch([padded_imu, padded_thm_tof])\n                \n                tta_preds.append(preds)\n            \n            # Average the predictions across all TTA steps\n            final_preds = np.mean(tta_preds, axis=0)\n            \n            predicted_class_index = np.argmax(final_preds, axis=1)[0]\n            predicted_class_name = LE.inverse_transform([predicted_class_index])[0]\n            \n            return predicted_class_name\n\n        print(\"Setting up Kaggle inference server...\")\n        inference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n        \n        if os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n            inference_server.serve()\n        else:\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-11T17:12:53.019060Z","iopub.execute_input":"2025-08-11T17:12:53.020337Z","iopub.status.idle":"2025-08-11T17:12:58.331058Z","shell.execute_reply.started":"2025-08-11T17:12:53.020300Z","shell.execute_reply":"2025-08-11T17:12:58.329910Z"}},"outputs":[],"execution_count":null}]}