{"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":12139340,"sourceType":"datasetVersion","datasetId":7645099},{"sourceId":12274546,"sourceType":"datasetVersion","datasetId":7713851},{"sourceId":12293285,"sourceType":"datasetVersion","datasetId":7748073},{"sourceId":12308152,"sourceType":"datasetVersion","datasetId":7744442},{"sourceId":12349092,"sourceType":"datasetVersion","datasetId":7779616},{"sourceId":242954653,"sourceType":"kernelVersion"},{"sourceId":246893721,"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},"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=1337)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# (Competition metric will only be imported when TRAINing)\nTRAIN = False                \nDEBUG_GATE = False  # <-- [新機能] デバッグモードを有効化\n                     \nRAW_DIR = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data\")\nPRETRAINED_DIR = Path(\"/kaggle/input/cmi-pretrained-model-v8\")\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 = 5\nMASKING_PROB = 0.25 # <-- [新機能] 学習時にTOF/THMをマスクする確率\nGATE_LOSS_WEIGHT = 0.2\n\nprint(\"▶ imports ready · tensorflow\", tf.__version__)","metadata":{"trusted":true},"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; quat_values = rot_data[['rot_x', 'rot_y', 'rot_z', 'rot_w']].values\n    linear_accel = np.zeros_like(acc_values); 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])): linear_accel[i, :] = acc_values[i, :]; continue\n        try: rotation = R.from_quat(quat_values[i]); gravity_sensor_frame = rotation.apply(gravity_world, inverse=True); linear_accel[i, :] = acc_values[i, :] - gravity_sensor_frame\n        except (ValueError, IndexError): 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; 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: 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 (ValueError, IndexError): 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; 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: r1, r2 = R.from_quat(q1), R.from_quat(q2); relative_rotation = r1.inv() * r2; angular_dist[i] = np.linalg.norm(relative_rotation.as_rotvec())\n        except (ValueError, IndexError): pass\n    return angular_dist","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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]; se = GlobalAveragePooling1D()(x); se = Dense(ch // reduction, activation='relu')(se); se = Dense(ch, activation='sigmoid')(se); se = Reshape((1, ch))(se); 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): x = Conv1D(filters, kernel_size, padding='same', use_bias=False, kernel_regularizer=l2(wd))(x); x = BatchNormalization()(x); x = Activation('relu')(x)\n    x = se_block(x)\n    if shortcut.shape[-1] != filters: shortcut = Conv1D(filters, 1, padding='same', use_bias=False, kernel_regularizer=l2(wd))(shortcut); shortcut = BatchNormalization()(shortcut)\n    x = add([x, shortcut]); x = Activation('relu')(x); x = MaxPooling1D(pool_size)(x); x = Dropout(drop)(x); return x\n    \ndef attention_layer(inputs):\n    score = Dense(1, activation='tanh')(inputs); score = Lambda(squeeze_last_axis)(score); weights = Activation('softmax')(score); weights = Lambda(expand_last_axis)(weights); context = Multiply()([inputs, weights]); context = Lambda(time_sum)(context); return context","metadata":{"trusted":true},"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, self.batch, self.imu_dim, self.class_weight, self.alpha, self.masking_prob = X, y, batch_size, imu_dim, class_weight, alpha, masking_prob; 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]; Xb, yb = self.X[idx].copy(), self.y[idx].copy()\n        sample_weights = np.ones(len(Xb), dtype='float32')\n        if self.class_weight: sample_weights = np.array([self.class_weight.get(i, 1.0) for i in yb.argmax(axis=1)])\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: Xb[i, :, self.imu_dim:] = 0; gate_target[i] = 0.0\n        if self.alpha > 0:\n            lam, perm = np.random.beta(self.alpha, self.alpha), np.random.permutation(len(Xb))\n            X_mix, y_mix = lam*Xb + (1-lam)*Xb[perm], lam*yb + (1-lam)*yb[perm]\n            gate_target_mix, sample_weights_mix = lam*gate_target + (1-lam)*gate_target[perm], 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        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},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def 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)); imu = Lambda(lambda t: t[:, :, :imu_dim])(inp); tof = Lambda(lambda t: t[:, :, imu_dim:])(inp)\n    x1 = residual_se_cnn_block(imu, 64, 3, drop=0.1, wd=wd); x1 = residual_se_cnn_block(x1, 128, 5, drop=0.1, wd=wd)\n    x2_base = Conv1D(64, 3, padding='same', use_bias=False, kernel_regularizer=l2(wd))(tof); 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); 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); x2_base = MaxPooling1D(2)(x2_base); x2_base = Dropout(0.2)(x2_base)\n    gate_input = GlobalAveragePooling1D()(tof); gate_input = Dense(16, activation='relu')(gate_input); gate = Dense(1, activation='sigmoid', name='tof_gate')(gate_input); x2 = Multiply()([x2_base, gate])\n    merged = Concatenate()([x1, x2]); x = Bidirectional(LSTM(256, return_sequences=True, kernel_regularizer=l2(wd)))(merged); x = Dropout(0.45)(x); x = attention_layer(x)\n    for units, drop in [(512, 0.5), (128, 0.3)]: x = Dense(units, use_bias=False, kernel_regularizer=l2(wd))(x); x = BatchNormalization()(x); x = Activation('relu')(x); x = Dropout(drop)(x)\n    out = Dense(n_classes, activation='softmax', name='main_output', kernel_regularizer=l2(wd))(x)\n    return Model(inputs=inp, outputs=[out, gate])","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if TRAIN:\n    print(\"▶ TRAIN MODE – preparing data...\")\n    df = pd.read_csv(RAW_DIR / \"train.csv\")\n    train_dem_df = pd.read_csv(RAW_DIR / \"train_demographics.csv\"); df = pd.merge(df, train_dem_df, on='subject', how='left')\n    le = LabelEncoder(); df['gesture_int'] = le.fit_transform(df['gesture']); np.save(EXPORT_DIR / \"gesture_classes.npy\", le.classes_)\n    \n    print(\"  Calculating all 1D features...\")\n    df_groups = df.groupby('sequence_id')\n    linear_accel_list = [pd.DataFrame(remove_gravity_from_acc(g,g), columns=['linear_acc_x','linear_acc_y','linear_acc_z'], index=g.index) for _,g in df_groups]\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    angular_vel_list = [pd.DataFrame(calculate_angular_velocity_from_quat(g), columns=['angular_vel_x','angular_vel_y','angular_vel_z'], index=g.index) for _,g in df_groups]\n    df = pd.concat([df, pd.concat(angular_vel_list)], axis=1)\n    angular_dist_list = [pd.DataFrame(calculate_angular_distance(g), columns=['angular_distance'], index=g.index) for _,g in df_groups]\n    df = pd.concat([df, pd.concat(angular_dist_list)], axis=1)\n    cols_for_stats = ['linear_acc_mag', 'linear_acc_mag_jerk', 'angular_distance']\n    for col in cols_for_stats: df[f'{col}_skew'], df[f'{col}_kurt'] = df.groupby('sequence_id')[col].transform('skew'), df.groupby('sequence_id')[col].transform(pd.Series.kurtosis)\n    \n    tof_mean_cols = [f'tof_{i}_mean' for i in range(1, 6)]\n    temp_tof_df = pd.DataFrame()\n    for i in range(1, 6):\n        pixel_cols = [f\"tof_{i}_v{p}\" for p in range(64)]; temp_tof_df[f'tof_{i}_mean'] = df[pixel_cols].replace(-1, np.nan).mean(axis=1)\n    df['tof_std_across_sensors'] = temp_tof_df[tof_mean_cols].std(axis=1); df['tof_range_across_sensors'] = temp_tof_df[tof_mean_cols].max(axis=1) - temp_tof_df[tof_mean_cols].min(axis=1)\n    thm_cols = [f'thm_{i}' for i in range(1, 6)]; df['thm_std_across_sensors'] = df[thm_cols].std(axis=1); df['thm_range_across_sensors'] = df[thm_cols].max(axis=1) - df[thm_cols].min(axis=1)\n\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_stats = [f'{c}_{s}' for c in cols_for_stats for s in ['skew', 'kurt']]\n    imu_cols = list(dict.fromkeys(imu_cols_base + imu_engineered + imu_stats))\n    \n    tof_aggregated_cols = []; [tof_aggregated_cols.extend([f'tof_{i}_{s}' for s in ['mean','std','min','max']]) for i in range(1,6)]\n    cross_sensor_cols = ['tof_std_across_sensors', 'tof_range_across_sensors', 'thm_std_across_sensors', 'thm_range_across_sensors']\n    tof_thm_cols = thm_cols + tof_aggregated_cols + cross_sensor_cols\n    \n    final_feature_cols = imu_cols + tof_thm_cols\n    imu_dim_final, tof_thm_dim_final = len(imu_cols), len(tof_thm_cols)\n    print(f\"  IMU {imu_dim_final} | TOF/THM {tof_thm_dim_final} | total {len(final_feature_cols)} features\")\n    # --- [修正点] ファイル名を統一 ---\n    np.save(EXPORT_DIR / \"final_feature_cols.npy\", np.array(final_feature_cols))\n\n    print(\"  Building sequences...\")\n    seq_gp = df.groupby('sequence_id')\n    X_list, y_list_int, groups_list, lens = [], [], [], []\n    for _, 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.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]); groups_list.append(seq_df_copy['subject'].iloc[0]); lens.append(len(seq_df_copy))\n\n    print(\"  Fitting Scaler and padding sequences...\")\n    scaler = StandardScaler().fit(np.concatenate(X_list, axis=0)); joblib.dump(scaler, EXPORT_DIR / \"scaler.pkl\")\n    pad_len = int(np.percentile(lens, PAD_PERCENTILE)); np.save(EXPORT_DIR / \"sequence_maxlen.npy\", pad_len)\n    X = pad_sequences([scaler.transform(x) for x in X_list], maxlen=pad_len, padding='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...\")\n    sgkf = StratifiedGroupKFold(n_splits=N_SPLITS, shuffle=True, random_state=42)\n    oof_preds = np.zeros_like(y, dtype='float32')\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_dim_final, len(le.classes_), wd=WD)\n        model.compile(optimizer=Adam(LR_INIT),\n                      loss={'main_output': 'categorical_crossentropy', 'tof_gate': 'binary_crossentropy'},\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, 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, 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        # --- [修正点] ファイル名を統一 ---\n        model.save(EXPORT_DIR / f\"final_model_fold_{fold}.h5\")\n        preds_val, _ = model.predict(X_val); oof_preds[val_idx] = preds_val\n\n    print(\"\\n✔ Training done.\")\n    try:\n        from cmi_2025_metric_copy_for_import import CompetitionMetric\n        print(f\"Overall OOF H‑F1 Score = {CompetitionMetric().calculate_hierarchical_f1(pd.DataFrame({'gesture': le.classes_[y.argmax(1)]}), pd.DataFrame({'gesture': le.classes_[oof_preds.argmax(1)]})):.4f}\")\n    except ImportError: print(\"Could not calculate OOF score. `cmi_2025_metric_copy_for_import.py` not found.\")\n\nelse:\n    print(\"▶ INFERENCE MODE – loading artefacts for HYBRID ENSEMBLE...\")\n    # --- モデル群A（あなたの新しく学習したモデル）の読み込み ---\n    print(\"  Loading your 5-Fold Gated Models...\")\n    YOUR_MODELS_DIR = Path(\"/kaggle/input/cmi-pretrained-model-v8\") # <-- あなたの成果物データセットのパス\n    final_feature_cols = np.load(YOUR_MODELS_DIR / \"final_feature_cols.npy\", allow_pickle=True).tolist()\n    pad_len = int(np.load(YOUR_MODELS_DIR / \"sequence_maxlen.npy\"))\n    scaler = joblib.load(YOUR_MODELS_DIR / \"scaler.pkl\")\n    gesture_classes = np.load(YOUR_MODELS_DIR / \"gesture_classes.npy\", allow_pickle=True)\n    custom_objs_A = {'time_sum': time_sum, 'squeeze_last_axis': squeeze_last_axis, 'expand_last_axis': expand_last_axis, 'se_block': se_block, 'residual_se_cnn_block': residual_se_cnn_block, 'attention_layer': attention_layer}\n    models_A = [load_model(YOUR_MODELS_DIR / f\"final_model_fold_{f}.h5\", compile=False, custom_objects=custom_objs_A) for f in range(N_SPLITS)]\n    print(f\"  {len(models_A)} of your models loaded successfully.\")\n\n    # --- モデル群B（公開されているシンプルモデル）の読み込み ---\n    print(\"\\n  Loading Public Simple Model...\")\n    PUBLIC_MODEL_DIR = Path(\"/kaggle/input/lb-0-78-quaternions-tf-bilstm-gru-attention\")\n    final_feature_cols_B = np.load(PUBLIC_MODEL_DIR / \"feature_cols.npy\", allow_pickle=True).tolist()\n    pad_len_B = int(np.load(PUBLIC_MODEL_DIR / \"sequence_maxlen.npy\"))\n    scaler_B = joblib.load(PUBLIC_MODEL_DIR / \"scaler.pkl\")\n    custom_objs_B = {'time_sum': time_sum, 'squeeze_last_axis': squeeze_last_axis, 'expand_last_axis': expand_last_axis, 'se_block': se_block, 'residual_se_cnn_block': residual_se_cnn_block, 'attention_layer': attention_layer}\n    model_B = load_model(PUBLIC_MODEL_DIR / \"gesture_two_branch_mixup.h5\", compile=False, custom_objects=custom_objs_B)\n    print(\"  Public model loaded successfully.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    predictions = []\n    # --- 1. モデル群A（あなたのモデル）の予測 ---\n    df_seq_A = sequence.to_pandas()\n    # 物理FE + センサー間FE\n    linear_accel=remove_gravity_from_acc(df_seq_A,df_seq_A);df_seq_A['linear_acc_x'],df_seq_A['linear_acc_y'],df_seq_A['linear_acc_z']=linear_accel[:,0],linear_accel[:,1],linear_accel[:,2]\n    df_seq_A['linear_acc_mag']=np.sqrt(df_seq_A['linear_acc_x']**2+df_seq_A['linear_acc_y']**2+df_seq_A['linear_acc_z']**2);df_seq_A['linear_acc_mag_jerk']=df_seq_A['linear_acc_mag'].diff().fillna(0)\n    angular_vel=calculate_angular_velocity_from_quat(df_seq_A);df_seq_A['angular_vel_x'],df_seq_A['angular_vel_y'],df_seq_A['angular_vel_z']=angular_vel[:,0],angular_vel[:,1],angular_vel[:,2]\n    df_seq_A['angular_distance']=calculate_angular_distance(df_seq_A)\n    cols_for_stats=['linear_acc_mag','linear_acc_mag_jerk','angular_distance'];\n    for col in cols_for_stats:df_seq_A[f'{col}_skew'],df_seq_A[f'{col}_kurt']=df_seq_A[col].skew(),df_seq_A[col].kurtosis()\n    for i in range(1,6):\n        pixel_cols=[f\"tof_{i}_v{p}\" for p in range(64)];tof_data=df_seq_A[pixel_cols].replace(-1,np.nan)\n        df_seq_A[f'tof_{i}_mean'],df_seq_A[f'tof_{i}_std'],df_seq_A[f'tof_{i}_min'],df_seq_A[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    tof_mean_cols=[f'tof_{i}_mean' for i in range(1,6)];df_seq_A['tof_std_across_sensors']=df_seq_A[tof_mean_cols].std(axis=1);df_seq_A['tof_range_across_sensors']=df_seq_A[tof_mean_cols].max(axis=1)-df_seq_A[tof_mean_cols].min(axis=1)\n    thm_cols=[f'thm_{i}' for i in range(1,6)];df_seq_A['thm_std_across_sensors']=df_seq_A[thm_cols].std(axis=1);df_seq_A['thm_range_across_sensors']=df_seq_A[thm_cols].max(axis=1)-df_seq_A[thm_cols].min(axis=1)\n    mat_unscaled_A = df_seq_A[final_feature_cols].ffill().bfill().fillna(0).values.astype('float32')\n    mat_scaled_A = scaler.transform(mat_unscaled_A)\n    pad_input_A = pad_sequences([mat_scaled_A], maxlen=pad_len, padding='post', dtype='float32')\n    for model in models_A: pred_A, _ = model.predict(pad_input_A, verbose=0); predictions.append(pred_A.argmax())\n\n    # --- 2. モデル群B（公開モデル）の予測 ---\n    df_seq_B = sequence.to_pandas()\n    df_seq_B['acc_mag']=np.sqrt(df_seq_B['acc_x']**2+df_seq_B['acc_y']**2+df_seq_B['acc_z']**2);df_seq_B['rot_angle']=2*np.arccos(df_seq_B['rot_w'].clip(-1,1))\n    df_seq_B['acc_mag_jerk']=df_seq_B['acc_mag'].diff().fillna(0);df_seq_B['rot_angle_vel']=df_seq_B['rot_angle'].diff().fillna(0)\n    linear_accel_B=remove_gravity_from_acc(df_seq_B,df_seq_B);df_seq_B['linear_acc_x'],df_seq_B['linear_acc_y'],df_seq_B['linear_acc_z']=linear_accel_B[:,0],linear_accel_B[:,1],linear_accel_B[:,2]\n    df_seq_B['linear_acc_mag']=np.sqrt(df_seq_B['linear_acc_x']**2+df_seq_B['linear_acc_y']**2+df_seq_B['linear_acc_z']**2);df_seq_B['linear_acc_mag_jerk']=df_seq_B['linear_acc_mag'].diff().fillna(0)\n    angular_vel_B=calculate_angular_velocity_from_quat(df_seq_B);df_seq_B['angular_vel_x'],df_seq_B['angular_vel_y'],df_seq_B['angular_vel_z']=angular_vel_B[:,0],angular_vel_B[:,1],angular_vel_B[:,2]\n    df_seq_B['angular_distance']=calculate_angular_distance(df_seq_B)\n    for i in range(1,6):\n        pixel_cols=[f\"tof_{i}_v{p}\" for p in range(64)];tof_data=df_seq_B[pixel_cols].replace(-1,np.nan)\n        df_seq_B[f'tof_{i}_mean'],df_seq_B[f'tof_{i}_std'],df_seq_B[f'tof_{i}_min'],df_seq_B[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    mat_unscaled_B = df_seq_B[final_feature_cols_B].ffill().bfill().fillna(0).values.astype('float32')\n    mat_scaled_B = scaler_B.transform(mat_unscaled_B)\n    pad_input_B = pad_sequences([mat_scaled_B], maxlen=pad_len_B, padding='post', dtype='float32')\n    pred_B = model_B.predict(pad_input_B, verbose=0); predictions.append(pred_B.argmax())\n    \n    # --- 3. 多数決による最終決定 ---\n    final_idx = max(set(predictions), key=predictions.count)\n    return str(gesture_classes[final_idx])","metadata":{"trusted":true},"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},"outputs":[],"execution_count":null}]}