{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12274546,"sourceType":"datasetVersion","datasetId":7713851},{"sourceId":242954653,"sourceType":"kernelVersion"}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os, json, joblib, numpy as np, pandas as pd\nfrom pathlib import Path\nimport warnings \nwarnings.filterwarnings(\"ignore\")\n\nfrom scipy.spatial.transform import Rotation as R\n\nfrom sklearn.model_selection import StratifiedGroupKFold\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, GRU, GaussianNoise\n)\nfrom tensorflow.keras.regularizers import l2\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping\nfrom tensorflow.keras import backend as K\nimport tensorflow as tf\nimport polars as pl\n\nimport matplotlib.pyplot as plt","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:10.162128Z","iopub.execute_input":"2025-06-25T00:36:10.162375Z","iopub.status.idle":"2025-06-25T00:36:27.990912Z","shell.execute_reply.started":"2025-06-25T00:36:10.162346Z","shell.execute_reply":"2025-06-25T00:36:27.99031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import random\ndef seed_everything(seed):\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    random.seed(seed)\n    np.random.seed(seed)\n    tf.random.set_seed(seed)\n    tf.experimental.numpy.random.seed(seed)\n    os.environ['TF_CUDNN_DETERMINISTIC'] = '1'\n    os.environ['TF_DETERMINISTIC_OPS'] = '1'\nseed_everything(seed=42)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:27.992537Z","iopub.execute_input":"2025-06-25T00:36:27.993219Z","iopub.status.idle":"2025-06-25T00:36:27.998097Z","shell.execute_reply.started":"2025-06-25T00:36:27.993197Z","shell.execute_reply":"2025-06-25T00:36:27.997294Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# (Competition metric will only be imported when TRAINing)\nTRAIN = True              \nDEBUG_GATE = False  # <-- [新機能] デバッグモードを有効化\n                     \nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi-pretrained-model-v5\")\nEXPORT_DIR = Path(\"./\")\nBATCH_SIZE = 64\nPAD_PERCENTILE = 95\nLR_INIT = 5e-4\nWD = 3e-3\nMIXUP_ALPHA = 0.4\nEPOCHS = 160\nPATIENCE = 40\nN_SPLITS = 10\nMASKING_PROB = 0.25 # <-- [新機能] 学習時にTOF/THMをマスクする確率\nGATE_LOSS_WEIGHT = 0.2\n\nprint(\"▶ imports ready · tensorflow\", tf.__version__)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:27.999512Z","iopub.execute_input":"2025-06-25T00:36:27.99978Z","iopub.status.idle":"2025-06-25T00:36:28.030768Z","shell.execute_reply.started":"2025-06-25T00:36:27.999758Z","shell.execute_reply":"2025-06-25T00:36:28.029932Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def remove_gravity_from_acc(acc_data, rot_data):\n    acc_values = acc_data[['acc_x', 'acc_y', 'acc_z']].values\n    quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    linear_accel = np.zeros_like(acc_values)\n    gravity_world = np.array([0, 0, 9.81])\n    for i in range(len(acc_values)):\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        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    return linear_accel\n\ndef calculate_angular_velocity_from_quat(rot_data, time_delta=1/200):\n    quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\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.all(np.isnan(q_t)) or np.all(np.isnan(q_t_plus_dt)): continue\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: pass\n    return angular_vel\n\ndef calculate_angular_distance(rot_data):\n    quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    angular_dist = np.zeros(len(quat_values))\n    for i in range(len(quat_values) - 1):\n        q1, q2 = quat_values[i], quat_values[i+1]\n        if np.all(np.isnan(q1)) or np.all(np.isnan(q2)): continue\n        try:\n            r1, r2 = R.from_quat(q1), R.from_quat(q2)\n            relative_rotation = r1.inv() * r2\n            angular_dist[i] = np.linalg.norm(relative_rotation.as_rotvec())\n        except ValueError: pass\n    return angular_dist","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:28.031554Z","iopub.execute_input":"2025-06-25T00:36:28.031792Z","iopub.status.idle":"2025-06-25T00:36:28.042673Z","shell.execute_reply.started":"2025-06-25T00:36:28.031774Z","shell.execute_reply":"2025-06-25T00:36:28.041885Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Tensor Manipulations\ndef time_sum(x): return K.sum(x, axis=1)\ndef squeeze_last_axis(x): return tf.squeeze(x, axis=-1)\ndef expand_last_axis(x): return tf.expand_dims(x, axis=-1)\n\ndef se_block(x, reduction=8):\n    ch = x.shape[-1]\n    se = GlobalAveragePooling1D()(x)\n    se = Dense(ch // reduction, activation='relu')(se)\n    se = Dense(ch, activation='sigmoid')(se)\n    se = Reshape((1, ch))(se)\n    return Multiply()([x, se])\n\ndef residual_se_cnn_block(x, filters, kernel_size, pool_size=2, drop=0.3, wd=1e-4):\n    shortcut = x\n    for _ in range(2):\n        x = Conv1D(filters, kernel_size, padding='same', use_bias=False,\n                   kernel_regularizer=l2(wd))(x)\n        x = BatchNormalization()(x)\n        x = Activation('relu')(x)\n    x = se_block(x)\n    if shortcut.shape[-1] != filters:\n        shortcut = Conv1D(filters, 1, padding='same', use_bias=False,\n                          kernel_regularizer=l2(wd))(shortcut)\n        shortcut = BatchNormalization()(shortcut)\n    x = add([x, shortcut])\n    x = Activation('relu')(x)\n    x = MaxPooling1D(pool_size)(x)\n    x = Dropout(drop)(x)\n    return x\n\ndef attention_layer(inputs):\n    score = Dense(1, activation='tanh')(inputs)\n    score = Lambda(squeeze_last_axis)(score)\n    weights = Activation('softmax')(score)\n    weights = Lambda(expand_last_axis)(weights)\n    context = Multiply()([inputs, weights])\n    context = Lambda(time_sum)(context)\n    return context","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:28.043496Z","iopub.execute_input":"2025-06-25T00:36:28.043698Z","iopub.status.idle":"2025-06-25T00:36:28.061043Z","shell.execute_reply.started":"2025-06-25T00:36:28.043682Z","shell.execute_reply":"2025-06-25T00:36:28.060355Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class GatedMixupGenerator(Sequence):\n    def __init__(self, X, y, batch_size, imu_dim, class_weight=None, alpha=0.2, masking_prob=0.0):\n        self.X, self.y = X, y\n        self.batch = batch_size\n        self.imu_dim = imu_dim\n        self.class_weight = class_weight\n        self.alpha = alpha\n        self.masking_prob = masking_prob\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].copy(), self.y[idx].copy()\n        \n        # サンプルごとの重みを計算\n        sample_weights = np.ones(len(Xb), dtype='float32')\n        if self.class_weight:\n            y_integers = yb.argmax(axis=1)\n            sample_weights = np.array([self.class_weight[i] for i in y_integers])\n        \n        gate_target = np.ones(len(Xb), dtype='float32')\n        if self.masking_prob > 0:\n            for i in range(len(Xb)):\n                if np.random.rand() < self.masking_prob:\n                    Xb[i, :, self.imu_dim:] = 0\n                    gate_target[i] = 0.0\n\n        if self.alpha > 0:\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            gate_target_mix = lam * gate_target + (1 - lam) * gate_target[perm]\n            sample_weights_mix = lam * sample_weights + (1 - lam) * sample_weights[perm]\n            return X_mix, {'main_output': y_mix, 'tof_gate': gate_target_mix}, sample_weights_mix\n\n        return Xb, {'main_output': yb, 'tof_gate': gate_target}, sample_weights\n\n    def on_epoch_end(self):\n        np.random.shuffle(self.indices)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:28.06169Z","iopub.execute_input":"2025-06-25T00:36:28.061944Z","iopub.status.idle":"2025-06-25T00:36:28.076271Z","shell.execute_reply.started":"2025-06-25T00:36:28.061918Z","shell.execute_reply":"2025-06-25T00:36:28.075489Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- [変更点] 複数の出力を持ち、補助損失に対応したモデル ---\ndef build_gated_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    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_base = Conv1D(64, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(tof)\n    x2_base = BatchNormalization()(x2_base); x2_base = Activation('relu')(x2_base)\n    x2_base = MaxPooling1D(2)(x2_base); x2_base = Dropout(0.2)(x2_base)\n    x2_base = Conv1D(128, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x2_base)\n    x2_base = BatchNormalization()(x2_base); x2_base = Activation('relu')(x2_base)\n    x2_base = MaxPooling1D(2)(x2_base); x2_base = Dropout(0.2)(x2_base)\n    \n    gate_input = GlobalAveragePooling1D()(tof)\n    gate_input = Dense(16, activation='relu')(gate_input)\n    # 補助損失の計算対象となるため、レイヤーに名前を付ける\n    gate = Dense(1, activation='sigmoid', name='tof_gate')(gate_input)\n    \n    x2 = Multiply()([x2_base, gate])\n\n    merged = Concatenate()([x1, x2])\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    x = Concatenate()([xa, xb, xc])\n    x = Dropout(0.4)(x)\n    x = attention_layer(x)\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', name='main_output', kernel_regularizer=l2(wd))(x)\n    \n    # 2つの出力を持つモデルを定義\n    return Model(inputs=inp, outputs=[out, gate])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:28.078377Z","iopub.execute_input":"2025-06-25T00:36:28.078578Z","iopub.status.idle":"2025-06-25T00:36:28.096389Z","shell.execute_reply.started":"2025-06-25T00:36:28.078562Z","shell.execute_reply":"2025-06-25T00:36:28.095647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# if DEBUG_GATE and not TRAIN:\n#     print(\"▶ GATE DEBUG MODE – preparing data to analyze trained models...\")\n    \n#     # --- 1. 学習時と全く同じデータ準備プロセスを実行 ---\n#     df = pd.read_csv(RAW_DIR / \"train.csv\")\n#     train_dem_df = pd.read_csv(RAW_DIR / \"train_demographics.csv\")\n#     df = pd.merge(df, train_dem_df, on='subject', how='left')\n#     le = LabelEncoder()\n#     df['gesture_int'] = le.fit_transform(df['gesture'])\n#     print(\"  Calculating engineered features...\")\n#     df['acc_mag'] = np.sqrt(df['acc_x']**2 + df['acc_y']**2 + df['acc_z']**2)\n#     df['rot_angle'] = 2 * np.arccos(df['rot_w'].clip(-1, 1))\n#     df['acc_mag_jerk'] = df.groupby('sequence_id')['acc_mag'].diff().fillna(0)\n#     df['rot_angle_vel'] = df.groupby('sequence_id')['rot_angle'].diff().fillna(0)\n#     cols_for_stats = ['acc_mag', 'rot_angle', 'acc_mag_jerk', 'rot_angle_vel']\n#     for col in cols_for_stats:\n#         df[f'{col}_skew'] = df.groupby('sequence_id')[col].transform('skew')\n#         df[f'{col}_kurt'] = df.groupby('sequence_id')[col].transform(pd.Series.kurtosis)\n    \n#     # --- [修正点] 成果物の読み込み元から特徴量リストを取得 ---\n#     final_feature_cols = np.load(PRETRAINED_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n    \n#     print(\"  Building sequences...\")\n#     seq_gp = df.groupby('sequence_id')\n#     X_list_unscaled, y_list_int, groups_list, lens = [], [], [], []\n#     for seq_id, seq_df in seq_gp:\n#         seq_df_copy = seq_df.copy()\n#         for i in range(1, 6):\n#             pixel_cols_tof = [f\"tof_{i}_v{p}\" for p in range(64)]\n#             tof_sensor_data = seq_df_copy[pixel_cols_tof].replace(-1, np.nan)\n#             seq_df_copy[f'tof_{i}_mean'] = tof_sensor_data.mean(axis=1)\n#             seq_df_copy[f'tof_{i}_std']  = tof_sensor_data.std(axis=1)\n#             seq_df_copy[f'tof_{i}_min']  = tof_sensor_data.min(axis=1)\n#             seq_df_copy[f'tof_{i}_max']  = tof_sensor_data.max(axis=1)\n#         mat_unscaled = seq_df_copy[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32')\n#         X_list_unscaled.append(mat_unscaled)\n#         y_list_int.append(seq_df_copy['gesture_int'].iloc[0])\n#         groups_list.append(seq_df_copy['subject'].iloc[0])\n#         lens.append(len(mat_unscaled))\n\n#     print(\"  Loading scaler and padding sequences...\")\n#     # --- [修正点] 読み込み元を PRETRAINED_DIR に変更 ---\n#     scaler = joblib.load(PRETRAINED_DIR / \"scaler.pkl\") \n#     pad_len = int(np.load(PRETRAINED_DIR / \"sequence_maxlen.npy\"))\n    \n#     X_scaled_list = [scaler.transform(x_seq) for x_seq in X_list_unscaled]\n#     del X_list_unscaled\n#     X = pad_sequences(X_scaled_list, maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n#     del X_scaled_list\n#     y_stratify = np.array(y_list_int)\n#     groups = np.array(groups_list)\n\n#     # --- 2. CV分割を再現し、各フォールドのモデルを分析 ---\n#     sgkf = StratifiedGroupKFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n#     custom_objs = {\n#         'time_sum': time_sum, 'squeeze_last_axis': squeeze_last_axis, 'expand_last_axis': expand_last_axis,\n#         'se_block': se_block, 'residual_se_cnn_block': residual_se_cnn_block, 'attention_layer': attention_layer,\n#     }\n\n#     for fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y_stratify, groups)):\n#         print(f\"\\n===== Analyzing FOLD {fold+1}/{N_SPLITS} =====\")\n#         X_val = X[val_idx]\n        \n#         # --- [修正点] 読み込み元を PRETRAINED_DIR に変更 ---\n#         model_path = PRETRAINED_DIR / f\"gesture_model_fold_{fold}.h5\"\n#         if not model_path.exists():\n#             print(f\"Model file not found: {model_path}. Skipping this fold.\")\n#             continue\n#         model = load_model(model_path, compile=False, custom_objects=custom_objs)\n\n#         if 'tof_gate' not in [layer.name for layer in model.layers]:\n#             print(\"Error: 'tof_gate' layer not found in the model.\")\n#             break\n#         gate_output = model.get_layer('tof_gate').output\n#         debug_model = Model(inputs=model.input, outputs=[model.output, gate_output])\n\n#         print(\"  Predicting on validation data to get gate values...\")\n#         _, gate_values = debug_model.predict(X_val, batch_size=64) # BATCH_SIZE\n#         gate_values = gate_values.flatten()\n\n#         print(f\"  Gate Value Stats: Mean={np.mean(gate_values):.4f}, Std={np.std(gate_values):.4f}, Min={np.min(gate_values):.4f}, Max={np.max(gate_values):.4f}\")\n\n#         plt.figure(figsize=(10, 5))\n#         plt.hist(gate_values, bins=50, range=(0, 1), color='skyblue', edgecolor='black')\n#         plt.title(f\"Fold {fold+1} Gate Value Distribution\", fontsize=16)\n#         plt.xlabel(\"Gate Value (0 = TOF/THM Off, 1 = TOF/THM On)\", fontsize=12)\n#         plt.ylabel(\"Frequency\", fontsize=12)\n#         plt.grid(axis='y', alpha=0.75)\n#         plt.show()\n    \n#     print(\"\\n✔ Gate analysis finished.\")\n\nif TRAIN:\n    print(\"▶ TRAIN MODE – loading dataset …\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n    train_dem_df = pd.read_csv(RAW_DIR / \"train_demographics.csv\")\n    df = pd.merge(df, train_dem_df, on='subject', how='left')\n    le = LabelEncoder()\n    df['gesture_int'] = le.fit_transform(df['gesture'])\n    np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n    \n    # --- [変更点] 物理FEの導入 ---\n    print(\"  Removing gravity and calculating linear acceleration features...\")\n    linear_accel_list = [pd.DataFrame(remove_gravity_from_acc(group[['acc_x', 'acc_y', 'acc_z']], group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]), columns=['linear_acc_x', 'linear_acc_y', 'linear_acc_z'], index=group.index) for _, group in df.groupby('sequence_id')]\n    df = pd.concat([df, pd.concat(linear_accel_list)], axis=1)\n    df['linear_acc_mag'] = np.sqrt(df['linear_acc_x']**2 + df['linear_acc_y']**2 + df['linear_acc_z']**2)\n    df['linear_acc_mag_jerk'] = df.groupby('sequence_id')['linear_acc_mag'].diff().fillna(0)\n\n    print(\"  Calculating angular velocity and distance from quaternions...\")\n    angular_vel_list = [pd.DataFrame(calculate_angular_velocity_from_quat(group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]), columns=['angular_vel_x', 'angular_vel_y', 'angular_vel_z'], index=group.index) for _, group in df.groupby('sequence_id')]\n    df = pd.concat([df, pd.concat(angular_vel_list)], axis=1)\n    angular_dist_list = [pd.DataFrame(calculate_angular_distance(group[['rot_x', 'rot_y', 'rot_z', 'rot_w']]), columns=['angular_distance'], index=group.index) for _, group in df.groupby('sequence_id')]\n    df = pd.concat([df, pd.concat(angular_dist_list)], axis=1)\n\n    # --- [変更点] 物理FEを反映した特徴量リスト ---\n    imu_cols_base = ['linear_acc_x', 'linear_acc_y', 'linear_acc_z'] + [c for c in df.columns if c.startswith('rot_')]\n    imu_engineered = ['linear_acc_mag', 'linear_acc_mag_jerk', 'angular_vel_x', 'angular_vel_y', 'angular_vel_z', 'angular_distance']\n    imu_cols = list(dict.fromkeys(imu_cols_base + imu_engineered))\n    \n    thm_cols_original = [c for c in df.columns if c.startswith('thm_')]\n    tof_aggregated_cols_template = []\n    for i in range(1, 6): tof_aggregated_cols_template.extend([f'tof_{i}_mean', f'tof_{i}_std', f'tof_{i}_min', f'tof_{i}_max'])\n\n    final_feature_cols = imu_cols + thm_cols_original + tof_aggregated_cols_template\n    imu_dim_final = len(imu_cols)\n    tof_thm_aggregated_dim_final = len(thm_cols_original) + len(tof_aggregated_cols_template)\n    \n    print(f\"  IMU (phys-based) {imu_dim_final} | THM + Aggregated TOF {tof_thm_aggregated_dim_final} | total {len(final_feature_cols)} features\")\n    np.save(EXPORT_DIR / \"feature_cols.npy\", np.array(final_feature_cols))\n\n    print(\"  Building sequences...\")\n    seq_gp = df.groupby('sequence_id') \n    X_list_unscaled, y_list_int, groups_list, lens = [], [], [], [] \n    for seq_id, seq_df in seq_gp:\n        seq_df_copy = seq_df.copy()\n        for i in range(1, 6):\n            pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]; tof_data = seq_df_copy[pixel_cols].replace(-1, np.nan)\n            seq_df_copy[f'tof_{i}_mean'], seq_df_copy[f'tof_{i}_std'], seq_df_copy[f'tof_{i}_min'], seq_df_copy[f'tof_{i}_max'] = tof_data.mean(axis=1), tof_data.std(axis=1), tof_data.min(axis=1), tof_data.max(axis=1)\n        X_list_unscaled.append(seq_df_copy[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32'))\n        y_list_int.append(seq_df_copy['gesture_int'].iloc[0])\n        groups_list.append(seq_df_copy['subject'].iloc[0])\n        lens.append(len(seq_df_copy))\n\n    print(\"  Fitting StandardScaler...\")\n    all_steps_concatenated = np.concatenate(X_list_unscaled, axis=0)\n    scaler = StandardScaler().fit(all_steps_concatenated)\n    joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n    \n    print(\"  Scaling and padding sequences...\")\n    X_scaled_list = [scaler.transform(x_seq) for x_seq in X_list_unscaled]\n    pad_len = int(np.percentile(lens, PAD_PERCENTILE)); np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    X = pad_sequences(X_scaled_list, maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n    y_stratify, groups, y = np.array(y_list_int), np.array(groups_list), to_categorical(y_list_int, num_classes=len(le.classes_))\n\n    print(\"  Starting training with Stratified Group K-Fold CV...\")\n    sgkf = StratifiedGroupKFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n    oof_preds = np.zeros_like(y, dtype='float32')\n    \n    for fold, (train_idx, val_idx) in enumerate(sgkf.split(X, y_stratify, groups)):\n        print(f\"\\n===== FOLD {fold+1}/{N_SPLITS} =====\")\n        X_tr, X_val, y_tr, y_val = X[train_idx], X[val_idx], y[train_idx], y[val_idx]\n        model = build_gated_two_branch_model(pad_len, imu_dim_final, tof_thm_aggregated_dim_final, len(le.classes_), wd=WD)\n        model.compile(optimizer=Adam(LR_INIT),\n                      loss={'main_output': tf.keras.losses.CategoricalCrossentropy(label_smoothing=0.1), 'tof_gate': tf.keras.losses.BinaryCrossentropy()},\n                      loss_weights={'main_output': 1.0, 'tof_gate': GATE_LOSS_WEIGHT},\n                      metrics={'main_output': 'accuracy'})\n        class_weight_dict = dict(enumerate(compute_class_weight('balanced', classes=np.arange(len(le.classes_)), y=y_tr.argmax(1))))\n        train_gen = GatedMixupGenerator(X_tr, y_tr, batch_size=BATCH_SIZE, imu_dim=imu_dim_final, class_weight=class_weight_dict, alpha=MIXUP_ALPHA, masking_prob=MASKING_PROB)\n        val_gen = GatedMixupGenerator(X_val, y_val, batch_size=BATCH_SIZE, imu_dim=imu_dim_final)\n        cb = EarlyStopping(patience=PATIENCE, restore_best_weights=True, verbose=1, monitor='val_main_output_accuracy', mode='max')\n        model.fit(train_gen, epochs=EPOCHS, validation_data=val_gen, callbacks=[cb], verbose=1)\n        model.save(EXPORT_DIR / f\"gesture_model_fold_{fold}.h5\")\n        preds_val, _ = model.predict(X_val); oof_preds[val_idx] = preds_val\n\n    # (OOFスコア計算部分はcmi_2025_metric_copy_for_import.pyがあれば有効化)\n    print(\"\\n✔ Training done.\")\n    \n    # 評価指標ファイルをインポートしてOOFスコアを計算\n    from cmi_2025_metric_copy_for_import import CompetitionMetric\n    true_oof_int = y.argmax(1)\n    pred_oof_int = oof_preds.argmax(1)\n    \n    h_f1_oof = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': le.classes_[true_oof_int]}),\n        pd.DataFrame({'gesture': le.classes_[pred_oof_int]}))\n    print(f\"Overall OOF H‑F1 Score = {h_f1_oof:.4f}\")\n    \nelse:\n    print(\"▶ INFERENCE MODE – loading artefacts from\", PRETRAINED_DIR)\n    final_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    # Note: imu_dim_final and tof_thm_aggregated_dim_final are not needed for inference here\n    # as the predict function works on the full feature set.\n\n    custom_objs = {\n        'time_sum': time_sum, 'squeeze_last_axis': squeeze_last_axis, 'expand_last_axis': expand_last_axis,\n        'se_block': se_block, 'residual_se_cnn_block': residual_se_cnn_block, 'attention_layer': attention_layer,\n    }\n    \n    models = []\n    print(f\"  Loading {N_SPLITS} models for ensemble inference...\")\n    for fold in range(N_SPLITS):\n        # --- [重要] 推論時にはカスタムオブジェクトに`build_gated_two_branch_model`は不要 ---\n        # 保存されたモデルはレイヤーの構造を保持しているため、カスタムレイヤー/関数のみ渡せばよい\n        model_path = PRETRAINED_DIR / f\"gesture_model_fold_{fold}.h5\"\n        model = load_model(model_path, compile=False, custom_objects=custom_objs)\n        models.append(model)\n    print(\"  Models, scaler, feature_cols, pad_len loaded – ready for evaluation\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:28.097335Z","iopub.execute_input":"2025-06-25T00:36:28.097629Z","iopub.status.idle":"2025-06-25T00:36:34.201919Z","shell.execute_reply.started":"2025-06-25T00:36:28.097602Z","shell.execute_reply":"2025-06-25T00:36:34.201117Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    df_seq = sequence.to_pandas()\n    # --- [変更点] 推論時にも物理FEを完全に再現 ---\n    linear_accel = remove_gravity_from_acc(df_seq, df_seq)\n    df_seq['linear_acc_x'], df_seq['linear_acc_y'], df_seq['linear_acc_z'] = linear_accel[:, 0], linear_accel[:, 1], linear_accel[:, 2]\n    df_seq['linear_acc_mag'] = np.sqrt(df_seq['linear_acc_x']**2 + df_seq['linear_acc_y']**2 + df_seq['linear_acc_z']**2)\n    df_seq['linear_acc_mag_jerk'] = df_seq['linear_acc_mag'].diff().fillna(0)\n    angular_vel = calculate_angular_velocity_from_quat(df_seq)\n    df_seq['angular_vel_x'], df_seq['angular_vel_y'], df_seq['angular_vel_z'] = angular_vel[:, 0], angular_vel[:, 1], angular_vel[:, 2]\n    df_seq['angular_distance'] = calculate_angular_distance(df_seq)\n    \n    for i in range(1, 6):\n        pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]; tof_data = df_seq[pixel_cols].replace(-1, np.nan)\n        df_seq[f'tof_{i}_mean'], df_seq[f'tof_{i}_std'], df_seq[f'tof_{i}_min'], df_seq[f'tof_{i}_max'] = tof_data.mean(axis=1), tof_data.std(axis=1), tof_data.min(axis=1), tof_data.max(axis=1)\n        \n    mat_unscaled = df_seq[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32')\n    mat_scaled = scaler.transform(mat_unscaled)\n    pad_input = pad_sequences([mat_scaled], maxlen=pad_len, padding='post', truncating='post', dtype='float32')\n    \n    all_preds = [model.predict(pad_input, verbose=0)[0] for model in models] # 主出力のみ取得\n    avg_pred = np.mean(all_preds, axis=0)\n    return str(gesture_classes[avg_pred.argmax()])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-25T00:36:34.20276Z","iopub.execute_input":"2025-06-25T00:36:34.203053Z","iopub.status.idle":"2025-06-25T00:36:34.21143Z","shell.execute_reply.started":"2025-06-25T00:36:34.203027Z","shell.execute_reply":"2025-06-25T00:36:34.210797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if not TRAIN:\n    import kaggle_evaluation.cmi_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-06-25T00:36:34.21227Z","iopub.execute_input":"2025-06-25T00:36:34.212584Z","iopub.status.idle":"2025-06-25T00:36:42.646169Z","shell.execute_reply.started":"2025-06-25T00:36:34.212564Z","shell.execute_reply":"2025-06-25T00:36:42.645315Z"}},"outputs":[],"execution_count":null}]}