{"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"}],"dockerImageVersionId":31040,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:48:42.636063Z","iopub.execute_input":"2025-07-02T17:48:42.636384Z","iopub.status.idle":"2025-07-02T17:48:42.647178Z","shell.execute_reply.started":"2025-07-02T17:48:42.636359Z","shell.execute_reply":"2025-07-02T17:48:42.646298Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport polars as pl\nimport tensorflow as tf\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.metrics import f1_score\nimport os\nimport threading\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:48:47.671549Z","iopub.execute_input":"2025-07-02T17:48:47.672081Z","iopub.status.idle":"2025-07-02T17:48:47.677819Z","shell.execute_reply.started":"2025-07-02T17:48:47.672043Z","shell.execute_reply":"2025-07-02T17:48:47.676878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"try:\n    import kaggle_evaluation.cmi_inference_server\nexcept ImportError:\n    # Mock the class if the library is not available locally for testing\n    class MockCMIInferenceServer:\n        def __init__(self, predict_fn):\n            self.predict_fn = predict_fn\n        def serve(self):\n            print(\"Running in mock server mode. `serve()` called.\")\n        def run_local_gateway(self, data_paths):\n            print(\"Running local gateway with mock server.\")\n            # This mock gateway will not actually loop through sequences.\n            # It's just to ensure the server setup code runs without error.\n            print(\"Local gateway mock run complete.\")\n\n    kaggle_evaluation = type('KaggleEvaluation', (), {})()\n    kaggle_evaluation.cmi_inference_server = type('CMIInferenceServer', (), {'CMIInferenceServer': MockCMIInferenceServer})()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:48:51.579816Z","iopub.execute_input":"2025-07-02T17:48:51.580179Z","iopub.status.idle":"2025-07-02T17:48:51.586794Z","shell.execute_reply.started":"2025-07-02T17:48:51.580153Z","shell.execute_reply":"2025-07-02T17:48:51.585672Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Define constants\nNUM_IMU = 6  # 3 accel + 3 rotation\nNUM_THERM = 5\nNUM_TOF = 5\nNUM_CLASSES = 9  # 8 BFRB-like + 1 non_target\nSEQUENCE_LENGTH = 200  # Adjust based on dataset exploration\nBATCH_SIZE = 32\nEPOCHS = 50","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:48:55.861975Z","iopub.execute_input":"2025-07-02T17:48:55.862355Z","iopub.status.idle":"2025-07-02T17:48:55.867437Z","shell.execute_reply.started":"2025-07-02T17:48:55.862329Z","shell.execute_reply":"2025-07-02T17:48:55.866391Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Gesture mapping (for multiclass)\nGESTURE_MAP = {\n    'above_ear_pull_hair': 0,\n    'forehead_pull_hairline': 1,\n    'forehead_scratch': 2,\n    'eyebrow_pull_hair': 3,\n    'eyelash_pull_hair': 4,\n    'neck_pinch_skin': 5,\n    'neck_scratch': 6,\n    'cheek_pinch_skin': 7,\n    'non_target': 8\n}\nNUM_CLASSES = len(GESTURE_MAP)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:49:23.177188Z","iopub.execute_input":"2025-07-02T17:49:23.177519Z","iopub.status.idle":"2025-07-02T17:49:23.183626Z","shell.execute_reply.started":"2025-07-02T17:49:23.177497Z","shell.execute_reply":"2025-07-02T17:49:23.182434Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Global Variables for Trained Artifacts ---\n# These will be populated by the one-time training process.\nFULL_MODEL = None\nIMU_MODEL = None\nSCALER_FULL = None\nSCALER_IMU = None\nREVERSE_GESTURE_MAP = {}\nMODELS_TRAINED_FLAG = threading.Event()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:49:49.240281Z","iopub.execute_input":"2025-07-02T17:49:49.240590Z","iopub.status.idle":"2025-07-02T17:49:49.245612Z","shell.execute_reply.started":"2025-07-02T17:49:49.240567Z","shell.execute_reply":"2025-07-02T17:49:49.244671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- Main One-Time Training and Server Setup ---\ndef main():\n    \"\"\"\n    This function handles the one-time model training. It's called only once.\n    All helper functions for training are nested inside to prevent scope issues.\n    \"\"\"\n    global FULL_MODEL, IMU_MODEL, SCALER_FULL, SCALER_IMU, REVERSE_GESTURE_MAP\n    \n    # --- NESTED HELPER FUNCTIONS FOR TRAINING ---\n    def normalize_gesture(gesture):\n        if not isinstance(gesture, str): return 'non_target'\n        normalized = gesture.lower().replace(' - ', '_').replace(' ', '_').replace('/', '_')\n        return 'non_target' if normalized not in GESTURE_MAP else normalized\n\n    def preprocess_training_data(df, is_full_sensor=True):\n        df = df.copy()\n        df['gesture_normalized'] = df['gesture'].apply(normalize_gesture)\n        sensor_cols = ['acc_x', 'acc_y', 'acc_z', 'rot_x', 'rot_y', 'rot_z']\n        if is_full_sensor:\n            sensor_cols += [f'thm_{i}' for i in range(1, 6)]\n            for i in range(1, 6):\n                tof_cols = [f'tof_{i}_v{j}' for j in range(64) if f'tof_{i}_v{j}' in df.columns]\n                if tof_cols: df[f'tof_{i}'] = df[tof_cols].mean(axis=1)\n            sensor_cols += [f'tof_{i}' for i in range(1, 6) if f'tof_{i}' in df.columns]\n        \n        nan_seq_ids = df[df[sensor_cols].isnull().any(axis=1)]['sequence_id'].unique()\n        df = df[~df['sequence_id'].isin(nan_seq_ids)]\n        \n        sequences, labels, valid_seq_ids = [], [], []\n        for seq_id, group in df.groupby('sequence_id'):\n            X = group[sensor_cols].values\n            if len(X) > SEQUENCE_LENGTH: X = X[:SEQUENCE_LENGTH]\n            elif len(X) < SEQUENCE_LENGTH: X = np.pad(X, ((0, SEQUENCE_LENGTH - len(X)), (0, 0)), mode='constant')\n            sequences.append(X)\n            labels.append(GESTURE_MAP[group['gesture_normalized'].iloc[0]])\n            valid_seq_ids.append(seq_id)\n            \n        if not sequences: return np.array([]), np.array([]), StandardScaler(), []\n        \n        X_out = np.array(sequences)\n        y_multi = np.array(labels)\n        scaler = StandardScaler()\n        X_out = scaler.fit_transform(X_out.reshape(-1, X_out.shape[-1])).reshape(X_out.shape)\n        \n        return X_out, y_multi, scaler, valid_seq_ids\n\n    def build_cnn_model(input_shape, num_classes=NUM_CLASSES):\n        inputs = tf.keras.Input(shape=input_shape)\n        x = tf.keras.layers.Conv1D(64, 3, activation='relu', padding='same')(inputs)\n        x = tf.keras.layers.BatchNormalization()(x)\n        x = tf.keras.layers.MaxPooling1D(2)(x)\n        x = tf.keras.layers.Conv1D(128, 3, activation='relu', padding='same')(x)\n        x = tf.keras.layers.BatchNormalization()(x)\n        x = tf.keras.layers.MaxPooling1D(2)(x)\n        x = tf.keras.layers.Conv1D(256, 3, activation='relu', padding='same')(x)\n        x = tf.keras.layers.BatchNormalization()(x)\n        x = tf.keras.layers.GlobalAveragePooling1D()(x)\n        x = tf.keras.layers.Dense(128, activation='relu')(x)\n        x = tf.keras.layers.Dropout(0.3)(x)\n        binary_output = tf.keras.layers.Dense(1, activation='sigmoid', name='binary')(x)\n        multi_output = tf.keras.layers.Dense(num_classes, activation='softmax', name='multi')(x)\n        return tf.keras.Model(inputs, [binary_output, multi_output])\n\n    def train_model(X_train, y_train, X_val, y_val, input_shape):\n        model = build_cnn_model(input_shape)\n        model.compile(optimizer='adam',\n                      loss={'binary': 'binary_crossentropy', 'multi': 'sparse_categorical_crossentropy'},\n                      metrics={'binary': 'accuracy', 'multi': 'accuracy'})\n        y_binary_train = (y_train != GESTURE_MAP['non_target']).astype(int)\n        y_binary_val = (y_val != GESTURE_MAP['non_target']).astype(int)\n        model.fit(X_train, {'binary': y_binary_train, 'multi': y_train},\n                  batch_size=BATCH_SIZE, epochs=EPOCHS,\n                  validation_data=(X_val, {'binary': y_binary_val, 'multi': y_val}),\n                  callbacks=[tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5, restore_best_weights=True)])\n        return model\n\n    # --- TRAINING EXECUTION ---\n    try:\n        df_train = pd.read_csv('/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv')\n    except FileNotFoundError:\n        print(\"Training file not found. Skipping training.\")\n        MODELS_TRAINED_FLAG.set()\n        return\n\n    print(\"--- Starting One-Time Model Training ---\")\n    \n    # Create the reverse map from normalized name to original name\n    original_gestures = df_train['gesture'].dropna().unique()\n    REVERSE_GESTURE_MAP = {normalize_gesture(g): g for g in original_gestures}\n    \n    print(\"Preprocessing full-sensor data...\")\n    X_full, y_multi_full, scaler_full, valid_seqs = preprocess_training_data(df_train, is_full_sensor=True)\n    \n    df_train_filtered = df_train[df_train['sequence_id'].isin(valid_seqs)].copy()\n    \n    print(\"\\nPreprocessing IMU-only data...\")\n    X_imu, y_multi_imu, scaler_imu, _ = preprocess_training_data(df_train_filtered, is_full_sensor=False)\n\n    assert len(X_full) == len(X_imu) and np.array_equal(y_multi_full, y_multi_imu)\n\n    print(f\"\\nSplitting {len(X_full)} sequences into training and validation sets...\")\n    X_full_train, X_full_val, X_imu_train, X_imu_val, y_train, y_val = train_test_split(\n        X_full, X_imu, y_multi_full, test_size=0.2, random_state=42, stratify=y_multi_full)\n\n    print(\"\\n--- Training Full Sensor Model ---\")\n    FULL_MODEL = train_model(X_full_train, y_train, X_full_val, y_val, X_full_train.shape[1:])\n    SCALER_FULL = scaler_full\n\n    print(\"\\n--- Training IMU-Only Model ---\")\n    IMU_MODEL = train_model(X_imu_train, y_train, X_imu_val, y_val, X_imu_train.shape[1:])\n    SCALER_IMU = scaler_imu\n    \n    print(\"\\n--- Models and scalers are trained and ready for inference. ---\")\n    MODELS_TRAINED_FLAG.set() # Signal that training is complete\n\n# --- Prediction Function for the Kaggle API ---\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    \"\"\"\n    Predicts the gesture for a single sequence DataFrame.\n    This function will be called repeatedly by the evaluation server.\n    \"\"\"\n    # This ensures training happens only on the first call and other calls wait.\n    if not MODELS_TRAINED_FLAG.is_set():\n        MODELS_TRAINED_FLAG.wait()\n\n    df_test = sequence.to_pandas()\n    df_columns = df_test.columns.tolist()\n    is_full_sensor = any('thm' in col.lower() or 'tof' in col.lower() for col in df_columns)\n    \n    model = FULL_MODEL if is_full_sensor else IMU_MODEL\n    scaler = SCALER_FULL if is_full_sensor else SCALER_IMU\n\n    if model is None or scaler is None:\n        return 'non_target'\n\n    sensor_cols = ['acc_x', 'acc_y', 'acc_z', 'rot_x', 'rot_y', 'rot_z']\n    if is_full_sensor:\n        sensor_cols += [f'thm_{i}' for i in range(1, 6)]\n        for i in range(1, 6):\n            tof_cols = [f'tof_{i}_v{j}' for j in range(64) if f'tof_{i}_v{j}' in df_columns]\n            if tof_cols: df_test[f'tof_{i}'] = df_test[tof_cols].mean(axis=1)\n        sensor_cols += [f'tof_{i}' for i in range(1, 6) if f'tof_{i}' in df_test.columns]\n\n    if not all(col in df_test.columns for col in sensor_cols):\n        return 'non_target'\n\n    X = df_test[sensor_cols].values\n    if np.any(np.isnan(X)): X = np.nan_to_num(X, nan=0.0)\n    \n    if len(X) > SEQUENCE_LENGTH: X = X[:SEQUENCE_LENGTH]\n    elif len(X) < SEQUENCE_LENGTH: X = np.pad(X, ((0, SEQUENCE_LENGTH - len(X)), (0, 0)), mode='constant')\n    \n    X_scaled = scaler.transform(X.reshape(-1, X.shape[-1])).reshape(1, SEQUENCE_LENGTH, X.shape[-1])\n    \n    _, multi_pred = model.predict(X_scaled, verbose=0)\n    multi_pred = np.nan_to_num(multi_pred, nan=0.0)\n    gesture_idx = np.argmax(multi_pred, axis=1)[0]\n    \n    # Look up the normalized gesture name\n    normalized_gesture = [k for k, v in GESTURE_MAP.items() if v == gesture_idx][0]\n    \n    # Convert back to the original gesture string for submission\n    original_gesture = REVERSE_GESTURE_MAP.get(normalized_gesture, 'non_target')\n    \n    return original_gesture\n\n# Start the training in a separate thread to allow the server to start immediately.\ntraining_thread = threading.Thread(target=main)\ntraining_thread.start()\n\n# Set up the server with the globally-scoped predict function.\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\n# Start the server to listen for prediction requests.\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    # This local gateway is for basic testing and may not fully replicate the environment.\n    print(\"\\nStarting local gateway for basic testing...\")\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    )\n\n# Clean up the training thread\ntraining_thread.join()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-07-02T17:53:19.650410Z","iopub.execute_input":"2025-07-02T17:53:19.650725Z","iopub.status.idle":"2025-07-02T18:00:24.130417Z","shell.execute_reply.started":"2025-07-02T17:53:19.650701Z","shell.execute_reply":"2025-07-02T18:00:24.129036Z"}},"outputs":[],"execution_count":null}]}