{"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":"gpu","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":12588831,"sourceType":"datasetVersion","datasetId":7950864},{"sourceId":12590510,"sourceType":"datasetVersion","datasetId":7952010},{"sourceId":12592179,"sourceType":"datasetVersion","datasetId":7953103},{"sourceId":12630560,"sourceType":"datasetVersion","datasetId":7980830},{"sourceId":12636801,"sourceType":"datasetVersion","datasetId":7985167},{"sourceId":12636942,"sourceType":"datasetVersion","datasetId":7985269},{"sourceId":12647763,"sourceType":"datasetVersion","datasetId":7992801},{"sourceId":12647929,"sourceType":"datasetVersion","datasetId":7987806},{"sourceId":12648558,"sourceType":"datasetVersion","datasetId":7993282},{"sourceId":12650996,"sourceType":"datasetVersion","datasetId":7740030},{"sourceId":12664339,"sourceType":"datasetVersion","datasetId":8003056},{"sourceId":12676243,"sourceType":"datasetVersion","datasetId":8010151},{"sourceId":12678488,"sourceType":"datasetVersion","datasetId":8011745},{"sourceId":12695247,"sourceType":"datasetVersion","datasetId":8016810},{"sourceId":12732843,"sourceType":"datasetVersion","datasetId":8048271},{"sourceId":12778018,"sourceType":"datasetVersion","datasetId":8053494},{"sourceId":466189,"sourceType":"modelInstanceVersion","modelInstanceId":376120,"modelId":396836},{"sourceId":470148,"sourceType":"modelInstanceVersion","modelInstanceId":379274,"modelId":399247},{"sourceId":483414,"sourceType":"modelInstanceVersion","modelInstanceId":386604,"modelId":405728},{"sourceId":485741,"sourceType":"modelInstanceVersion","modelInstanceId":387598,"modelId":406646},{"sourceId":487006,"sourceType":"modelInstanceVersion","modelInstanceId":388389,"modelId":407336},{"sourceId":492845,"sourceType":"modelInstanceVersion","modelInstanceId":391738,"modelId":410431},{"sourceId":494164,"sourceType":"modelInstanceVersion","modelInstanceId":392549,"modelId":411204},{"sourceId":494167,"sourceType":"modelInstanceVersion","modelInstanceId":392549,"modelId":411204},{"sourceId":494177,"sourceType":"modelInstanceVersion","modelInstanceId":392549,"modelId":411204}],"dockerImageVersionId":31090,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# %% [1] Install & Imports & Utils\n!pip install -q pywavelets\n\nimport os, gc, math, time, glob, shutil, random, pickle, logging, threading\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport polars as pl\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.nn.utils.rnn import pad_sequence\n\nfrom sklearn.model_selection import StratifiedGroupKFold\nfrom sklearn.preprocessing import StandardScaler, LabelEncoder\n\nfrom tqdm import tqdm\nfrom scipy.spatial.transform import Rotation as R\nfrom scipy.signal import butter, filtfilt\nimport pywt\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts\n\n# Repro\ndef set_seed(seed: int = 42):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(\"Device:\", DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T02:15:30.897498Z","iopub.execute_input":"2025-08-16T02:15:30.897998Z","iopub.status.idle":"2025-08-16T02:15:40.140063Z","shell.execute_reply.started":"2025-08-16T02:15:30.897971Z","shell.execute_reply":"2025-08-16T02:15:40.139323Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [2] Metric & Scaler & Dataset (feature engineering含む)\nfrom sklearn.metrics import f1_score\n\nclass ParticipantVisibleError(Exception):\n    pass\n\nclass CompetitionMetric:\n    \"\"\"Hierarchical macro F1 for the CMI 2025 challenge.\"\"\"\n    def __init__(self):\n        self.target_gestures = [\n            'Above ear - pull hair',\n            'Cheek - pinch skin',\n            'Eyebrow - pull hair',\n            'Eyelash - pull hair',\n            'Forehead - pull hairline',\n            'Forehead - scratch',\n            'Neck - pinch skin',\n            'Neck - scratch',\n        ]\n        self.non_target_gestures = [\n            'Write name on leg',\n            'Wave hello',\n            'Glasses on/off',\n            'Text on phone',\n            'Write name in air',\n            'Feel around in tray and pull out an object',\n            'Scratch knee/leg skin',\n            'Pull air toward your face',\n            'Drink from bottle/cup',\n            'Pinch knee/leg skin'\n        ]\n        self.all_classes = self.target_gestures + self.non_target_gestures\n\n    def calculate_hierarchical_f1(self, sol: pd.DataFrame, sub: pd.DataFrame):\n        invalid_types = {i for i in sub['gesture'].unique() if i not in self.all_classes}\n        if invalid_types:\n            raise ParticipantVisibleError(f\"Invalid gesture values in submission: {invalid_types}\")\n        y_true_bin = sol['gesture'].isin(self.target_gestures).values\n        y_pred_bin = sub['gesture'].isin(self.target_gestures).values\n        f1_binary = f1_score(y_true_bin, y_pred_bin, pos_label=True, zero_division=0, average='binary')\n        y_true_mc = sol['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        y_pred_mc = sub['gesture'].apply(lambda x: x if x in self.target_gestures else 'non_target')\n        f1_macro = f1_score(y_true_mc, y_pred_mc, average='macro', zero_division=0)\n        return 0.5 * f1_binary + 0.5 * f1_macro, f1_binary, f1_macro\n\n\nclass ToFScaler:\n    \"\"\"-1(無信号)→固定値, 0(故障)→固定値, それ以外は標準化（センサー横一律平均/分散）\"\"\"\n    def __init__(self):\n        self.means_full = np.zeros(320)\n        self.stds_full  = np.ones(320)\n        self.col_indices = {s: ((s-1)*64, s*64) for s in range(1, 6)}\n\n    def fit(self, X_tof):\n        tof_values = X_tof.values if isinstance(X_tof, pd.DataFrame) else X_tof\n        if tof_values.shape[1] != 320:\n            raise ValueError(f\"TOF列は320想定、実際は{tof_values.shape[1]}\")\n        tof = tof_values.reshape(-1, 5, 64)\n        for sensor in range(5):\n            valid = (tof[:, sensor, :] != -1) & (tof[:, sensor, :] != 0)\n            v = tof[:, sensor, :][valid]\n            m = v.mean() if len(v) > 0 else 0.0\n            s = v.std()  if len(v) > 0 else 1.0\n            s = 1.0 if s < 1e-7 else s\n            st, ed = self.col_indices[sensor+1]\n            self.means_full[st:ed] = m\n            self.stds_full[st:ed]  = s\n        return self\n\n    def transform(self, X_tof):\n        arr = X_tof.values.copy() if isinstance(X_tof, pd.DataFrame) else X_tof.copy()\n        valid = (arr != -1) & (arr != 0)\n        no_sig = (arr == -1)\n        failure = (arr == 0)\n        out = np.zeros_like(arr, dtype=float)\n        idx = np.where(valid)[1]\n        out[valid] = (arr[valid] - self.means_full[idx]) / self.stds_full[idx]\n        out[no_sig] = 10.0\n        out[failure] = -10.0\n        if isinstance(X_tof, pd.DataFrame):\n            return pd.DataFrame(out, columns=X_tof.columns, index=X_tof.index)\n        return out\n\n\nclass GestureDataset(Dataset):\n    def __init__(self, sequence_dir, label_dir, metadata_df, max_len=None,\n                 is_train=False, scale_path=None, use_augmentation=False,\n                 drift_std=0.01, drift_max=0.05):\n        self.sequence_dir = sequence_dir\n        self.label_dir = label_dir\n        self.metadata = metadata_df.reset_index(drop=True)\n        self.sequence_ids = self.metadata['sequence_id'].tolist()\n        self.max_len = max_len\n        self.is_train = is_train\n        self.scale_path = scale_path\n        self.use_augmentation = use_augmentation and is_train\n        self.drift_std = drift_std\n        self.drift_max = drift_max\n\n        # 定数\n        self.tof_dim = 320\n\n        # ラベルエンコーダ\n        self.gesture_encoder = LabelEncoder()\n        self.gesture_encoder.fit(self.metadata['gesture'])\n\n        # スケーラ（IMU+THM、DEMO、TOF）\n        self.non_tof_scaler = StandardScaler()  # IMU+THM 用\n        self.demo_scaler    = StandardScaler()  # DEMO(7) 用\n        self.tof_scaler     = ToFScaler()       # TOF(320) 用\n\n        # シーケンス読み込み（raw -> FE済みの feature 行列の list）\n        self.sequence_data = self._load_sequences()\n\n        # スケーラの fit/保存 or 読み込み → 正規化キャッシュ\n        if scale_path is not None:\n            if is_train:\n                self._fit_scalers()\n                self._save_scalers(scale_path)\n                self._normalize_and_cache_sequences()\n            else:\n                with open(os.path.join(scale_path, 'train_scalers.pkl'), 'rb') as f:\n                    obj = pickle.load(f)\n                # 後方互換：2タプル(旧) or 3タプル(新)\n                if isinstance(obj, tuple) and len(obj) == 3:\n                    self.non_tof_scaler, self.tof_scaler, self.demo_scaler = obj\n                elif isinstance(obj, tuple) and len(obj) == 2:\n                    self.non_tof_scaler, self.tof_scaler = obj\n                    self.demo_scaler = self._identity_scaler(7)  # DEMOは恒等スケール\n                else:\n                    raise ValueError(\"train_scalers.pkl is not a (2|3)-tuple.\")\n                self._normalize_and_cache_sequences()\n        else:\n            # スケールしない（そのまま FE 結果を保持）\n            self.cached_sequences = self.sequence_data.copy()\n\n        print(f\"Dataset ready: {len(self)} sequences\")\n        lens = [len(s) for s in self.cached_sequences]\n        if len(lens) > 0:\n            print(f\"len mean={np.mean(lens):.1f}, max={np.max(lens)}\")\n\n    # ===== Utils (filters & wavelet) =====\n    def butter_lowpass(self, data, cutoff, fs, order=4):\n        b, a = butter(order, cutoff/(0.5*fs), btype='low');  return filtfilt(b, a, data)\n    def butter_highpass(self, data, cutoff, fs, order=4):\n        b, a = butter(order, cutoff/(0.5*fs), btype='high'); return filtfilt(b, a, data)\n    def butter_bandpass(self, data, lowcut, highcut, fs, order=4):\n        b, a = butter(order, [lowcut/(0.5*fs), highcut/(0.5*fs)], btype='band'); return filtfilt(b, a, data)\n    def wavelet_denoise(self, data, wavelet='db4', level=1):\n        coeffs = pywt.wavedec(data, wavelet, mode='symmetric')\n        coeffs[1:] = [np.zeros_like(v) for v in coeffs[1:]]\n        return pywt.waverec(coeffs, wavelet, mode='symmetric')[:len(data)]\n\n    # ===== IMU 補助 =====\n    def _remove_gravity_from_acc(self, acc_values, quat_values):\n        T = acc_values.shape[0]\n        out = np.zeros_like(acc_values)\n        g_world = np.array([0, 0, 9.81])\n        for i in range(T):\n            q = quat_values[i]\n            if np.all(np.isnan(q)) or np.all(np.isclose(q, 0)):\n                out[i] = acc_values[i]; continue\n            try:\n                rot = R.from_quat(q)\n                g_sensor = rot.apply(g_world, inverse=True)\n                out[i] = acc_values[i] - g_sensor\n            except ValueError:\n                out[i] = acc_values[i]\n        return out\n\n    def _calculate_angular_velocity_from_quat(self, quat_values, time_delta=1/10):\n        T = quat_values.shape[0]\n        ang = np.zeros((T, 3))\n        for i in range(T-1):\n            q1, q2 = quat_values[i], quat_values[i+1]\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                continue\n            try:\n                r1, r2 = R.from_quat(q1), R.from_quat(q2)\n                delta = r1.inv() * r2\n                ang[i] = delta.as_rotvec() / time_delta\n            except ValueError:\n                pass\n        return ang\n\n    def _calculate_angular_distance(self, quat_values):\n        T = quat_values.shape[0]\n        ang = np.zeros(T)\n        for i in range(T-1):\n            q1, q2 = quat_values[i], quat_values[i+1]\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                ang[i] = 0; continue\n            try:\n                r1, r2 = R.from_quat(q1), R.from_quat(q2)\n                angle = np.linalg.norm((r1.inv()*r2).as_rotvec())\n                ang[i] = angle\n            except ValueError:\n                ang[i] = 0\n        return ang\n\n    # ===== Feature Engineering =====\n    def _feature_engineering(self, seq):\n        \"\"\"\n        RAW seq 形式の想定:\n          [acc_x, acc_y, acc_z, rot_w, rot_x, rot_y, rot_z, thm(5), tof(320), demo(7)]\n        返却featuresは（いったん）:\n          [IMU派生(36), thm(5), tof(320), demo(7)]\n        ※ 後段の正規化で [IMU+THM][DEMO][TOF] に並べ替えてキャッシュします\n        \"\"\"\n        fs = 50\n        imu = seq[:, :7]         # acc(3) + rot(wxyz)(4)\n        acc = imu[:, :3]\n        rot = imu[:, 3:7]        # wxyz\n\n        acc_lp = np.stack([self.butter_lowpass(acc[:,i], 2, fs) for i in range(3)], axis=1)\n        acc_hp = np.stack([self.butter_highpass(acc[:,i], 0.5, fs) for i in range(3)], axis=1)\n        acc_bp = np.stack([self.butter_bandpass(acc[:,i], 0.5, 5, fs) for i in range(3)], axis=1)\n        acc_wv = np.stack([self.wavelet_denoise(acc[:,i], 'db4', 1) for i in range(3)], axis=1)\n\n        acc_mag = np.sqrt((acc**2).sum(1))\n        rot_angle = 2 * np.arccos(np.clip(rot[:,0], -1, 1))\n        acc_mag_jerk = np.diff(acc_mag, prepend=acc_mag[0])\n        rot_angle_vel = np.diff(rot_angle, prepend=rot_angle[0])\n        linear_acc = self._remove_gravity_from_acc(acc, rot[:, [1,2,3,0]])  # x,y,z,w\n        linear_acc_mag = np.sqrt((linear_acc**2).sum(1))\n        linear_acc_mag_jerk = np.diff(linear_acc_mag, prepend=linear_acc_mag[0])\n        angular_vel = self._calculate_angular_velocity_from_quat(rot[:, [1,2,3,0]])\n        angular_distance = self._calculate_angular_distance(rot[:, [1,2,3,0]])\n\n        # 36ch の IMU派生特徴\n        new_imu = np.concatenate([\n            acc, \n            seq[:, 3:7],          # 生 rot(wxyz)\n            rot,                  # rot 再掲（合計で +4ch）\n            acc_mag[:,None], rot_angle[:,None],\n            acc_mag_jerk[:,None], rot_angle_vel[:,None],\n            linear_acc, linear_acc_mag[:,None], linear_acc_mag_jerk[:,None],\n            angular_vel, angular_distance[:,None],\n            acc_lp, acc_hp, acc_bp, acc_wv,\n        ], axis=1)  # -> 36\n\n        # thm(5)+tof(320)+demo(7)\n        other = seq[:, 7:]\n        features = np.concatenate([new_imu, other], axis=1)\n        return features  # 返却時点では [IMU(36), thm(5), tof(320), demo(7)]\n\n    # ===== IO / scaler =====\n    def _load_sequences(self):\n        out = []\n        for sid in tqdm(self.sequence_ids, desc=\"Load seq\", leave=False):\n            subject = self.metadata.loc[self.metadata['sequence_id']==sid, 'subject'].iloc[0]\n            p = os.path.join(self.sequence_dir, str(subject), f\"{sid}.npy\")\n            if os.path.exists(p):\n                raw = np.load(p)\n                feat = self._feature_engineering(raw)\n                out.append(feat)\n        return out\n\n    def _identity_scaler(self, n):\n        s = StandardScaler()\n        s.mean_ = np.zeros(n, dtype=float)\n        s.var_ = np.ones(n, dtype=float)\n        s.scale_ = np.ones(n, dtype=float)\n        s.n_features_in_ = n\n        s.n_samples_seen_ = np.array([1], dtype=np.int64)\n        return s\n\n    def _fit_scalers(self):\n        \"\"\"\n        FE後 features の並びは [IMU(36), thm(5), tof(320), demo(7)] を想定。\n        ここで DEMO を独立スケールし、キャッシュ時に [IMU+THM][DEMO][TOF] に組み直す。\n        \"\"\"\n        all_seq = np.vstack(self.sequence_data)\n        D = all_seq.shape[1]\n        demo = all_seq[:, -7:]                                   # (.., 7)\n        tof  = all_seq[:, D - (self.tof_dim + 7) : D - 7]        # (.., 320)\n        imu_thm = all_seq[:, : D - (self.tof_dim + 7)]           # (.., 36+5)\n\n        self.non_tof_scaler.fit(imu_thm)\n        self.demo_scaler.fit(demo)\n        self.tof_scaler.fit(tof)\n\n    def _save_scalers(self, scale_path):\n        Path(scale_path).mkdir(parents=True, exist_ok=True)\n        with open(os.path.join(scale_path, 'train_scalers.pkl'), 'wb') as f:\n            # 3タプル保存（後方互換のためTOFを2番目に保持）\n            pickle.dump((self.non_tof_scaler, self.tof_scaler, self.demo_scaler), f)\n\n    def _normalize_and_cache_sequences(self):\n        \"\"\"\n        features([IMU, thm, tof, demo]) → 正規化 → 最終的に\n        [IMU+THM][DEMO][TOF] の順にキャッシュ（以降の __getitem__ の分割ロジックと一致）\n        \"\"\"\n        self.cached_sequences = []\n        for seq in tqdm(self.sequence_data, desc=\"Normalize\", leave=False):\n            D = seq.shape[1]\n            demo = seq[:, -7:]\n            tof  = seq[:, D - (self.tof_dim + 7) : D - 7]\n            imu_thm = seq[:, : D - (self.tof_dim + 7)]\n\n            n_imu_thm = self.non_tof_scaler.transform(imu_thm)\n            n_demo    = self.demo_scaler.transform(demo)\n            n_tof     = self.tof_scaler.transform(tof)\n\n            # 最終構成を [IMU+THM][DEMO][TOF] に統一\n            self.cached_sequences.append(\n                np.concatenate([n_imu_thm, n_demo, n_tof], axis=1)\n            )\n        self.sequence_data = None  # free\n\n    # ===== Augment (train only) =====\n    def _jitter(self, x, sigma=0.05):\n        return x + np.random.normal(0., sigma, size=x.shape)\n\n    def _time_mask(self, x, max_len=20):\n        L = x.shape[0]\n        m = np.random.randint(1, max_len+1)\n        st = np.random.randint(0, max(1, L-m))\n        x[st:st+m] = 0\n        return x\n\n    def _feature_mask(self, x, max_k=15):\n        C = x.shape[1]\n        k = np.random.randint(1, max_k+1)\n        idx = np.random.choice(C, k, replace=False)\n        x[:, idx] = 0\n        return x\n\n    def _motion_drift(self, imu_part):\n        T = imu_part.shape[0]\n        drift = np.cumsum(np.random.normal(scale=self.drift_std, size=(T,1)), axis=0)\n        drift = np.clip(drift, -self.drift_max, self.drift_max)\n        # 物理チャネル例（accや一部の派生に疑似ドリフト）\n        imu_part[:, 0:3]   += drift   # acc\n        imu_part[:, 10:13] += drift\n        imu_part[:, 15:18] += drift\n        return imu_part\n\n    def _apply_augmentations(self, x):\n        \"\"\"\n        x は正規化後・キャッシュ後の配列で [IMU+THM][DEMO7][TOF320]\n        DEMO 列は保護（統計量を壊さない）\n        \"\"\"\n        D = x.shape[1]\n        demo_st = D - self.tof_dim - 7\n        demo_ed = D - self.tof_dim\n        # DEMO 以外の列インデックス\n        aug_cols = np.r_[0:demo_st, demo_ed:D]\n\n        if np.random.rand() < 0.7:\n            noise = np.zeros_like(x)\n            noise[:, aug_cols] = np.random.normal(0., 0.05, size=(x.shape[0], len(aug_cols)))\n            x = x + noise\n        if np.random.rand() < 0.5:\n            L = x.shape[0]\n            m = np.random.randint(1, 21)\n            st = np.random.randint(0, max(1, L-m))\n            x[st:st+m, aug_cols] = 0\n        if np.random.rand() < 0.5:\n            C = len(aug_cols)\n            k = np.random.randint(1, 16)\n            idx = np.random.choice(aug_cols, k, replace=False)\n            x[:, idx] = 0\n        if np.random.rand() < 0.5:\n            # IMU派生を主対象に疑似ドリフト（先頭36chがIMU派生）\n            imu_end = 36\n            imu = x[:, :imu_end]\n            oth = x[:, imu_end:]\n            imu = self._motion_drift(imu)\n            x = np.concatenate([imu, oth], axis=1)\n        return x\n\n    # ===== PyTorch Dataset =====\n    def __len__(self):\n        return len(self.sequence_ids)\n\n    def __getitem__(self, idx):\n        \"\"\"\n        ここでは [IMU+THM][DEMO][TOF] を前提に分割する。\n          - TOF: 末尾320列\n          - DEMO: TOF直前の7列\n          - IMU+THM: それ以前（IMU=先頭~(末尾-7-5)、THM=その直前5列）\n        \"\"\"\n        seq = self.cached_sequences[idx].copy()\n        if self.use_augmentation:\n            seq = self._apply_augmentations(seq)\n\n        # 長さ制限\n        if self.max_len and len(seq) > self.max_len:\n            seq = seq[-self.max_len:]\n\n        # 分割\n        D = seq.shape[1]\n        tof = seq[:, -self.tof_dim:]                   # (T, 320)\n        imu_thm_demo = seq[:, :-self.tof_dim]          # (T, *)\n        demo = imu_thm_demo[0, -7:]                    # (7,)\n        imu_thm = imu_thm_demo[:, :-7]\n        thm  = imu_thm[:, -5:]                         # (T, 5)\n        imu  = imu_thm[:, :-5]                         # (T, 36)\n        thm_tof = np.concatenate([thm, tof], axis=1)   # (T, 325)\n\n        # フェーズラベル\n        sid = self.sequence_ids[idx]\n        subject = self.metadata.loc[self.metadata['sequence_id']==sid, 'subject'].iloc[0]\n        p_lab = os.path.join(self.label_dir, str(subject), f\"{sid}.npy\")\n        phase_labels = np.load(p_lab).astype(np.float32)\n        if self.max_len and len(phase_labels) > len(seq):\n            phase_labels = phase_labels[-len(seq):]\n\n        # クラスラベル\n        meta = self.metadata.iloc[idx]\n        y = self.gesture_encoder.transform([meta['gesture']])[0]\n\n        return {\n            'imu': torch.tensor(imu, dtype=torch.float32),\n            'thm_tof': torch.tensor(thm_tof, dtype=torch.float32),\n            'demo': torch.tensor(demo, dtype=torch.float32),\n            'phase_labels': torch.tensor(phase_labels, dtype=torch.long),\n            'length': len(imu),\n            'sequence_id': sid,\n            'gesture_labels': torch.tensor(y, dtype=torch.long),\n        }\n\ndef collate_fn(batch):\n    batch.sort(key=lambda x: x['length'], reverse=True)\n    imu = [b['imu'] for b in batch]\n    thm_tof = [b['thm_tof'] for b in batch]\n    demo = torch.stack([b['demo'] for b in batch])\n    phase = [b['phase_labels'] for b in batch]\n    lengths = torch.tensor([b['length'] for b in batch], dtype=torch.long)\n    seq_ids = [b['sequence_id'] for b in batch]\n    y = torch.stack([b['gesture_labels'] for b in batch])\n\n    imu_p = pad_sequence(imu, batch_first=True, padding_value=0)\n    thm_tof_p = pad_sequence(thm_tof, batch_first=True, padding_value=0)\n    phase_p = pad_sequence(phase, batch_first=True, padding_value=3)\n    mask = (torch.arange(imu_p.size(1))[None, :] < lengths[:, None]).float()\n\n    return {'imu': imu_p, 'thm_tof': thm_tof_p, 'demo': demo,\n            'phase_labels': phase_p, 'mask': mask, 'lengths': lengths,\n            'gesture_labels': y, 'sequence_ids': seq_ids}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T02:15:40.141574Z","iopub.execute_input":"2025-08-16T02:15:40.141894Z","iopub.status.idle":"2025-08-16T02:15:40.189253Z","shell.execute_reply.started":"2025-08-16T02:15:40.141874Z","shell.execute_reply":"2025-08-16T02:15:40.188640Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [3] Model (二段ヘッド) + Blocks\nclass FiLM(nn.Module):\n    def __init__(self, feat_dim: int, cond_dim: int, hidden: int = 64):\n        super().__init__()\n        self.net = nn.Sequential(\n            nn.Linear(cond_dim, hidden), nn.SiLU(inplace=True),\n            nn.Linear(hidden, feat_dim * 2)\n        )\n    def forward(self, feat, cond):               # feat: (B, D), cond: (B, C)\n        gamma, beta = self.net(cond).chunk(2, dim=-1)\n        return feat * (1 + torch.tanh(gamma)) + beta\n\nclass CoordAttention(nn.Module):\n    def __init__(self, channels, reduction=8):\n        super().__init__()\n        self.mid = max(8, channels // reduction)\n        self.comp = nn.Sequential(\n            nn.Conv1d(channels, self.mid, 1, bias=False),\n            nn.BatchNorm1d(self.mid),\n            nn.SiLU(inplace=True)\n        )\n        self.time_conv = nn.Conv1d(1, 1, kernel_size=5, padding=2, bias=False)\n        self.channel_conv = nn.Conv1d(self.mid, channels, kernel_size=1, bias=False)\n        self.sig = nn.Sigmoid()\n\n    def forward(self, x):  # x: (B, C, T)\n        f = self.comp(x)                   # (B, mid, T)\n        t_attn = self.sig(self.time_conv(f.mean(1, keepdim=True)))   # (B,1,T)\n        c_attn = self.sig(self.channel_conv(f.mean(2, keepdim=True)))# (B,C,1)\n        return x * t_attn * c_attn\n\nclass ResidualCNNBlock(nn.Module):\n    def __init__(self, in_channels, out_channels, kernel_size, Model,\n                 reduction=8, pool_size=2, dropout=0.3, **_):  # ★ 余剰キーワードを受け流す\n        super().__init__()\n        self.conv1 = nn.Conv1d(in_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn1 = nn.BatchNorm1d(out_channels)\n\n        self.conv2 = nn.Conv1d(out_channels, out_channels, kernel_size,\n                               padding=kernel_size//2, bias=False)\n        self.bn2 = nn.BatchNorm1d(out_channels)\n\n        self.attention = Model(out_channels, reduction)\n\n        self.shortcut = nn.Sequential()\n        if in_channels != out_channels:\n            self.shortcut = nn.Sequential(\n                nn.Conv1d(in_channels, out_channels, 1, bias=False),\n                nn.BatchNorm1d(out_channels)\n            )\n\n        self.pool = nn.MaxPool1d(pool_size)\n        self.dropout = nn.Dropout(dropout)\n\n    def forward(self, x):\n        sc = self.shortcut(x)\n        y = F.relu(self.bn1(self.conv1(x)))\n        y = self.bn2(self.conv2(y))\n        y = self.attention(y)\n        y = F.relu(y + sc)\n        y = self.pool(y)\n        y = self.dropout(y)\n        return y\n\nclass MLPAttention(nn.Module):\n    def __init__(self, d):\n        super().__init__()\n        self.attn = nn.Sequential(nn.Linear(d, d//8), nn.SiLU(inplace=True), nn.Linear(d//8, 1))\n    def forward(self, x):  # (B,T,C)\n        w = F.softmax(self.attn(x), dim=1)  # (B,T,1)\n        return (x * w).sum(1)\n\nclass DilatedTCNBlock(nn.Module):\n    def __init__(self, channels=128, kernel_size=3, dilations=(1,2,4), dropout=0.1):\n        super().__init__()\n        layers = []\n        for d in dilations:\n            pad = (kernel_size - 1) * d // 2  # 長さ保持\n            layers += [\n                nn.Conv1d(channels, channels, kernel_size,\n                          padding=pad, dilation=d, bias=False),\n                nn.BatchNorm1d(channels),\n                nn.SiLU(inplace=True),\n                nn.Dropout(dropout),\n            ]\n        self.net = nn.Sequential(*layers)\n        self.proj = nn.Identity()  # チャネル不変なので恒等\n    def forward(self, x):  # x: (B,C,T)\n        y = self.net(x)\n        return y + self.proj(x)  # 残差\n\nclass IMUOnlyHierModel(nn.Module):\n    def __init__(self, imu_dim, tof_dim, n_classes, weight_decay=1e-4):\n        super().__init__()\n        self.imu_dim = imu_dim\n        self.tof_dim = tof_dim\n        self.n_classes = n_classes\n        self.weight_decay = weight_decay\n\n        # --- IMU branch: CNN 2段 ---\n        self.imu_block1 = ResidualCNNBlock(imu_dim, 64, 3, dropout=0.3, Model=CoordAttention, weight_decay=weight_decay)\n        self.imu_block2 = ResidualCNNBlock(64, 128, 5, dropout=0.3, Model=CoordAttention, weight_decay=weight_decay)\n\n        # --- ★ 追加: 軽量TCN（長さ保持） ---\n        self.tcn = DilatedTCNBlock(channels=128, kernel_size=3, dilations=(1,2,4), dropout=0.1)\n\n        # --- RNN + Attention ---\n        self.bigru = nn.GRU(128, 128, bidirectional=True, batch_first=True)\n        self.gru_dropout = nn.Dropout(0.4)\n        self.mlp_attention = MLPAttention(256)\n\n        # --- demographics(7) で feat(256) をFiLM ---\n        self.film = FiLM(feat_dim=256, cond_dim=7)\n\n        # --- Head ---\n        self.dense1 = nn.Linear(256, 256, bias=False); self.bn_dense1 = nn.BatchNorm1d(256); self.drop1 = nn.Dropout(0.5)\n        self.dense2 = nn.Linear(256, 128, bias=False); self.bn_dense2 = nn.BatchNorm1d(128); self.drop2 = nn.Dropout(0.3)\n        self.classifier = nn.Linear(128, n_classes)\n\n    def forward(self, imu, thm_tof, demo):\n        x = imu.transpose(1, 2)        # (B, C, T)\n        x = self.imu_block1(x)         # (B, 64, T/2)\n        x = self.imu_block2(x)         # (B,128, T/4)\n        x = self.tcn(x)                # (B,128, T/4)\n        x = x.transpose(1, 2)          # (B, T/4, 128)\n\n        x, _ = self.bigru(x)           # (B, T/4, 256)\n        x = self.gru_dropout(x)\n        feat = self.mlp_attention(x)   # (B, 256)\n\n         # --- FiLM で条件付け ---\n        demo = demo.float()                        # (B,7)\n        feat = self.film(feat, demo)\n\n        h = F.relu(self.bn_dense1(self.dense1(feat))); h = self.drop1(h)\n        h = F.relu(self.bn_dense2(self.dense2(h)));    h = self.drop2(h)\n        logits = self.classifier(h)\n        return logits","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T02:15:40.190047Z","iopub.execute_input":"2025-08-16T02:15:40.190220Z","iopub.status.idle":"2025-08-16T02:15:40.219474Z","shell.execute_reply.started":"2025-08-16T02:15:40.190206Z","shell.execute_reply":"2025-08-16T02:15:40.218962Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [4] Losses (Logit-Adjusted CE, hierarchical loss) & train_one_epoch\n\n# ==== HierLoss toggle ====\nUSE_HIER = False   # ← 階層ロスを使わない\nHIER_ALPHA = 0.0   # 一応ゼロで固定\nHIER_BETA  = 0.0\n\nclass LogitAdjustedCE(nn.Module):\n    def __init__(self, class_priors: torch.Tensor, tau: float = 1.0):\n        super().__init__()\n        self.register_buffer('bias', (-tau * torch.log(class_priors.clamp_min(1e-6))).float())\n    def forward(self, logits, target):\n        return F.cross_entropy(logits + self.bias.to(logits.device), target)\n\ndef hierarchical_loss(logits, bin_logit, target, target_mask, la_ce, alpha=0.5, beta=0.5):\n    # まず通常の多クラス損失\n    loss_mc = la_ce(logits, target)\n\n    # ★ ここを追加：alpha=beta=0 のときは即返す（余計な計算をしない）\n    if (alpha <= 0) and (beta <= 0):\n        return loss_mc\n\n    device = logits.device\n    tmask = target_mask.to(device)\n    y_bin = tmask[target].float()\n    loss_bin = F.binary_cross_entropy_with_logits(bin_logit.squeeze(-1), y_bin)\n\n    p = F.softmax(logits, dim=1)\n    p_target_sum = p[:, tmask].sum(dim=1)\n    p_bin = torch.sigmoid(bin_logit.squeeze(-1))\n    loss_cons = F.mse_loss(p_bin, p_target_sum)\n\n    return loss_mc + alpha*loss_bin + beta*loss_cons\n\ndef bin_logit_from_logits(logits: torch.Tensor, target_mask: torch.Tensor) -> torch.Tensor:\n    \"\"\"\n    logits: (B, C)\n    target_mask: (C,) bool  — True が targetクラス\n    返り値: (B, 1) の二値ロジット（target vs non-target）\n    \"\"\"\n    device = logits.device\n    tmask = target_mask.to(device)\n    if tmask.dtype != torch.bool:\n        tmask = tmask.bool()\n\n    B, C = logits.shape\n    n_t  = int(tmask.sum().item())\n    n_nt = C - n_t\n\n    # 通常経路：target群とnon-target群のlog-sum-exp差 = log-oddsに一致\n    if n_t > 0 and n_nt > 0:\n        lse_t  = torch.logsumexp(logits[:, tmask], dim=1)   # (B,)\n        lse_nt = torch.logsumexp(logits[:, ~tmask], dim=1)  # (B,)\n        bl = lse_t - lse_nt                                  # (B,)\n    else:\n        # 念のための保険（全クラスが片側に寄る異常系）：確率からlogit再構成\n        p  = torch.softmax(logits, dim=1)                   # (B, C)\n        p_t = p[:, tmask].sum(dim=1) if n_t > 0 else torch.zeros(B, device=device)\n        p_t = p_t.clamp(1e-6, 1-1e-6)\n        bl = torch.log(p_t / (1 - p_t))                     # (B,)\n\n    return bl.unsqueeze(-1)  # (B,1)\n\n\ndef train_one_epoch(\n    model: torch.nn.Module,\n    dataloader: torch.utils.data.DataLoader,\n    optimizer: torch.optim.Optimizer,\n    device: torch.device,\n    epoch: int,\n    total_epochs: int,\n    la_ce: torch.nn.Module,\n    target_mask_tensor: torch.Tensor,\n    warmup_epochs: int = 3,\n    eta_min: float = 1e-6,\n    grad_clip: float = 1.0,\n    scheduler=None,  # ★ 追加：外部スケジューラ（例：CosineAnnealingWarmRestarts）\n):\n    \"\"\"\n    scheduler を渡した場合：スケジューラ側でLR管理（各バッチ後に step）。\n    渡さない場合：従来どおり warmup + cosine で手動更新。\n    \"\"\"\n    model.train()\n    running_loss = 0.0\n    preds_all, t_all = [], []\n\n    base_lr = optimizer.defaults.get('lr', optimizer.param_groups[0]['lr'])\n    num_batches = max(1, len(dataloader))\n\n    for bi, batch in enumerate(tqdm(dataloader, desc=f\"Epoch {epoch+1} [Train]\", leave=False)):\n        # ---------- LR 更新 ----------\n        if scheduler is None:\n            # 手動: warmup + cosine\n            cur_iter = epoch * num_batches + bi\n            cur_frac = cur_iter / max(1, num_batches)\n            if cur_frac < warmup_epochs:\n                lr = base_lr * (0.001 + (cur_frac / max(1e-12, warmup_epochs)) * 0.999)\n            else:\n                num = (cur_frac - warmup_epochs)\n                den = max(1e-12, (total_epochs - warmup_epochs))\n                cos = 0.5 * (1 + math.cos(math.pi * (num / den)))\n                lr = eta_min + (base_lr - eta_min) * cos\n            for pg in optimizer.param_groups:\n                pg['lr'] = lr\n        # scheduler ありの場合は optimizer の lr は scheduler が管理（ここでは変更しない）\n\n        # ---------- forward ----------\n        imu = batch['imu'].to(device)\n        thm_tof = batch['thm_tof'].to(device)\n        demo = batch['demo'].to(device)\n        y = batch['gesture_labels'].to(device)\n\n        optimizer.zero_grad(set_to_none=True)\n\n        out = model(imu, thm_tof, demo)\n        if isinstance(out, tuple) and len(out) == 2:\n            logits, bin_logit = out\n        else:\n            logits = out\n            # モデルに二値ヘッドが無い場合のフォールバック（target_mask_tensor から作る）\n            bin_logit = bin_logit_from_logits(logits, target_mask_tensor)\n\n        # ---------- loss / backward ----------\n        loss = hierarchical_loss(logits, bin_logit, y, target_mask_tensor, la_ce, alpha=0.5, beta=0.5)\n        loss.backward()\n        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=grad_clip)\n        optimizer.step()\n\n        # ---------- scheduler step（スケジューラ使用時のみ） ----------\n        if scheduler is not None:\n            # CosineAnnealingWarmRestarts などは fractional epoch で step するのが定石\n            scheduler.step(epoch + bi / num_batches)\n\n        running_loss += float(loss.item())\n        with torch.no_grad():\n            preds_all.extend(logits.argmax(1).detach().cpu().tolist())\n            t_all.extend(y.detach().cpu().tolist())\n\n    avg_loss = running_loss / max(1, len(dataloader))\n    inv = dataloader.dataset.gesture_encoder.inverse_transform\n    f1, f1_bin, f1_mac = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': inv(np.array(t_all, dtype=int))}),\n        pd.DataFrame({'gesture': inv(np.array(preds_all, dtype=int))})\n    )\n    return avg_loss, f1, f1_bin, f1_mac","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T02:15:40.221073Z","iopub.execute_input":"2025-08-16T02:15:40.221269Z","iopub.status.idle":"2025-08-16T02:15:40.243163Z","shell.execute_reply.started":"2025-08-16T02:15:40.221255Z","shell.execute_reply":"2025-08-16T02:15:40.242460Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [5] Multi-crop validate, crops helper\ndef make_crops(x: np.ndarray, L=256, K=5):\n    T = len(x)\n    if T <= L: return [x[-L:]]\n    starts = np.linspace(0, T-L, num=K, dtype=int)\n    return [x[s:s+L] for s in starts]\n\n\n# ---- TTA (time-shift + jitter) ----\nTTA_SHIFTS = (-16, 0, +16)   # 0.32秒相当（50Hz想定）\nTTA_SIGMA  = 0.01            # IMUへの微小ノイズ\nUSE_TTA_INFER = True   # 推論でTTAを使わない\n\n\ndef shift_pad(x: np.ndarray, shift: int) -> np.ndarray:\n    \"\"\"時系列(T,C)をゼロパディングで時間シフト（循環しない）\"\"\"\n    T, C = x.shape\n    y = np.zeros_like(x)\n    if shift == 0:\n        return x\n    if shift > 0:\n        y[shift:] = x[:T-shift]\n    else:\n        s = -shift\n        y[:T-s] = x[s:]\n    return y\n\ndef tta_variants_per_crop(imu_crop: np.ndarray, thm_tof_crop: np.ndarray):\n    \"\"\"各cropからTTA 3本（±16, 0）を生成。JitterはIMUのみ.\"\"\"\n    outs = []\n    for sh in TTA_SHIFTS:\n        imu_s  = shift_pad(imu_crop, sh)\n        thm_s  = shift_pad(thm_tof_crop, sh)\n        # 微小Jitter（IMUのみ）\n        if TTA_SIGMA > 0:\n            imu_s  = imu_s + np.random.normal(0.0, TTA_SIGMA, imu_s.shape).astype(imu_s.dtype)\n        outs.append((imu_s, thm_s))\n    return outs\n\ndef validate(model, dataloader, device, temps=None):\n    model.eval()\n    val_preds, val_targets = [], []\n    Tmc = float(temps.get('T_mc', 1.0)) if temps is not None else 1.0\n\n    with torch.no_grad():\n        for batch in tqdm(dataloader, desc=\"[Val]\", leave=False):\n            imu_p = batch['imu'].cpu().numpy()\n            thmtof_p = batch['thm_tof'].cpu().numpy()\n            demo = batch['demo'].to(device)\n            lengths = batch['lengths'].cpu().numpy()\n            labels = batch['gesture_labels'].cpu().numpy()\n\n            for i in range(len(lengths)):\n                T = lengths[i]\n                imu_np = imu_p[i, :T, :]\n                thm_np = thmtof_p[i, :T, :]\n                imu_cs = make_crops(imu_np, L=256, K=5)\n                thm_cs = make_crops(thm_np, L=256, K=5)\n\n                # --- cropごとに TTAの logits を平均 ---\n                crop_logits = []\n                for ic, tc in zip(imu_cs, thm_cs):\n                    tta_logits = []\n                    for (ic2, tc2) in tta_variants_per_crop(ic, tc):\n                        it = torch.tensor(ic2, dtype=torch.float32).unsqueeze(0).to(device)\n                        tt = torch.tensor(tc2, dtype=torch.float32).unsqueeze(0).to(device)\n                        dt = demo[i].unsqueeze(0)\n                        out = model(it, tt, dt)\n                        logits = out[0] if isinstance(out, tuple) else out\n                        tta_logits.append(logits.cpu())\n                    # TTA logits 平均\n                    logits_mean = torch.stack(tta_logits).mean(0)  # (1,C)\n                    crop_logits.append(logits_mean)\n\n                # --- crop 平均 → 温度 → softmax ---\n                logits_cropmean = torch.stack(crop_logits).mean(0)       # (1,C)\n                logits_cropmean = logits_cropmean / max(Tmc, 1e-3)\n                prob = F.softmax(logits_cropmean, dim=1).cpu()\n\n                val_preds.append(int(prob.argmax(1).item()))\n                val_targets.append(int(labels[i]))\n\n    inv = dataloader.dataset.gesture_encoder.inverse_transform\n    f1, f1_bin, f1_mac = CompetitionMetric().calculate_hierarchical_f1(\n        pd.DataFrame({'gesture': inv(np.array(val_targets, dtype=int))}),\n        pd.DataFrame({'gesture': inv(np.array(val_preds,   dtype=int))})\n    )\n    return 0.0, f1, f1_bin, f1_mac","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T02:15:40.244022Z","iopub.execute_input":"2025-08-16T02:15:40.244410Z","iopub.status.idle":"2025-08-16T02:15:40.263407Z","shell.execute_reply.started":"2025-08-16T02:15:40.244385Z","shell.execute_reply":"2025-08-16T02:15:40.262731Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [6] Inference helpers: load models+scalers+temps, temperature calibration\ndef load_models_and_scalers(models_dir, model_class, n_classes, device):\n    model_paths = sorted(glob.glob(os.path.join(models_dir, \"model_fold*_best.pth\")))\n    if not model_paths:\n        raise FileNotFoundError(f\"No models found in {models_dir}\")\n    models, scalers, temps = [], [], []\n    for p in model_paths:\n        k = os.path.basename(p).split('_best.pth')[0].split('fold')[-1]\n        model = model_class(36, 325, n_classes).to(device)\n        model.load_state_dict(torch.load(p, map_location=device, weights_only=False))\n        model.eval()\n        models.append(model)\n        with open(os.path.join(models_dir, f\"scaler_fold{k}.pkl\"), 'rb') as f:\n            scalers.append(pickle.load(f))  # (non_tof_scaler, tof_scaler)\n        t_path = os.path.join(models_dir, f\"temp_fold{k}.pkl\")\n        if os.path.exists(t_path):\n            with open(t_path, 'rb') as f:\n                temps.append(pickle.load(f))  # {'T_mc': float, 'T_bin': float}\n        else:\n            temps.append({'T_mc': 1.0, 'T_bin': 1.0})\n    return models, scalers, temps\n\ndef calibrate_temperature(\n    model,\n    val_loader,\n    device,\n    init_T: float = 1.0,\n    max_iter: int = 20,\n    max_samples: int = 2000,   # バリデーションから使う上限サンプル数\n):\n    \"\"\"\n    温度スケーリング（多クラス側のみ）を安定・高速に実行。\n    - モデル出力logitsを事前にno_gradでキャッシュ → 逆伝播はTのみに流れる\n    - LBFGSでTを更新（logits/TでCE最小化）\n    \"\"\"\n    model.eval()\n\n    # 1) 検証logitsをキャッシュ（no_grad & detach）\n    cached_logits, cached_targets, seen = [], [], 0\n    with torch.no_grad():\n        for batch in val_loader:\n            if seen >= max_samples:\n                break\n            imu = batch['imu'].to(device)\n            thm_tof = batch['thm_tof'].to(device)\n            demo = batch['demo'].to(device)\n            y = batch['gesture_labels'].to(device)\n\n            out = model(imu, thm_tof, demo)\n            logits = out[0] if isinstance(out, tuple) else out\n            cached_logits.append(logits.detach())  # ← detach が重要\n            cached_targets.append(y.detach())\n            seen += y.size(0)\n\n    if not cached_logits:\n        # データが無い場合はスキップ\n        return 1.0\n\n    logits_cat = torch.cat(cached_logits, 0).to(device)\n    targets_cat = torch.cat(cached_targets, 0).to(device)\n\n    # 2) Tのみ最適化\n    T = torch.tensor([init_T], dtype=torch.float32, device=device, requires_grad=True)\n    opt = torch.optim.LBFGS([T], lr=0.1, max_iter=max_iter, line_search_fn='strong_wolfe')\n\n    def closure():\n        opt.zero_grad()\n        # Tの暴走防止（微小値クリップ）\n        T_pos = T.clamp_min(1e-3)\n        loss = F.cross_entropy(logits_cat / T_pos, targets_cat)\n        loss.backward()  # 勾配はTのみに流れる（logitsは定数）\n        return loss\n\n    opt.step(closure)\n    return float(T.detach().cpu().item())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T02:15:40.264073Z","iopub.execute_input":"2025-08-16T02:15:40.264347Z","iopub.status.idle":"2025-08-16T02:15:40.283778Z","shell.execute_reply.started":"2025-08-16T02:15:40.264320Z","shell.execute_reply":"2025-08-16T02:15:40.283261Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [7] Train (5-fold) with fold別scaler保存 & 温度スケーリング保存\nTRAIN = False  # Kaggle提出ノートブックでは False に\n\nif TRAIN:\n    # Logging\n    Path(\"logs\").mkdir(exist_ok=True)\n    logging.basicConfig(\n        filename=\"logs/imutrain.log\",\n        level=logging.INFO,\n        format=\"%(asctime)s [%(levelname)s] %(message)s\",\n        datefmt=\"%Y-%m-%d %H:%M:%S\",\n    )\n\n    metadata_path = '/kaggle/input/cmi-data/data/train_labels.csv'\n    sequence_dir  = '/kaggle/input/cmi-data/data/train_sequences'\n    label_dir     = '/kaggle/input/cmi-data/data/phase_labels'\n\n    NUM_FOLDS = 5\n    SEED = 2\n    BATCH_SIZE = 32\n    MAX_LEN = 256\n    LR = 1e-3\n    LR_MIN = 1e-6\n    EPOCHS = 100\n    EARLY_STOP = 30  # ← 伸ばしたい場合は 50 に（任意）\n    MODEL_DIR = \"IMUMODEL\"\n    NUM_WORKERS = 4\n    WARMUP_E = 3\n    SCALE_ROOT = \"CMI_data/data/scalers\"\n\n    Path(MODEL_DIR).mkdir(parents=True, exist_ok=True)\n    set_seed(SEED)\n    device = DEVICE\n\n    meta = pd.read_csv(metadata_path)\n    sgkf = StratifiedGroupKFold(n_splits=NUM_FOLDS, shuffle=True, random_state=SEED)\n\n    labels_all = meta['gesture'].values\n    seqs_all   = meta['sequence_id'].tolist()\n    subs_all   = meta['subject'].tolist()\n\n    all_fold_f1 = []\n\n    for fold, (tr_idx, va_idx) in enumerate(sgkf.split(seqs_all, labels_all, subs_all), start=1):\n        print(f\"\\n=== Fold {fold}/{NUM_FOLDS} ===\")\n        tr_meta = meta.iloc[tr_idx].reset_index(drop=True)\n        va_meta = meta.iloc[va_idx].reset_index(drop=True)\n\n        scale_path = f'{SCALE_ROOT}/fold{fold}'\n        tr_ds = GestureDataset(sequence_dir, label_dir, tr_meta, max_len=MAX_LEN, is_train=True,  scale_path=scale_path, use_augmentation=True)\n        va_ds = GestureDataset(sequence_dir, label_dir, va_meta, max_len=MAX_LEN, is_train=False, scale_path=scale_path)\n\n        tr_loader = DataLoader(tr_ds, batch_size=BATCH_SIZE, shuffle=True,  collate_fn=collate_fn,\n                               num_workers=NUM_WORKERS, pin_memory=True, drop_last=True)\n        va_loader = DataLoader(va_ds, batch_size=BATCH_SIZE, shuffle=False, collate_fn=collate_fn,\n                               num_workers=NUM_WORKERS, pin_memory=True, drop_last=False)\n\n        n_classes = len(tr_ds.gesture_encoder.classes_)\n        model = IMUOnlyHierModel(imu_dim=36, tof_dim=325, n_classes=n_classes).to(device)\n        optimizer = torch.optim.Adam(model.parameters(), lr=LR, weight_decay=1e-4)\n\n        # ★ 追加：CosineAnnealingWarmRestarts を fold ごとに作成\n        #   - T_0 は最初の周期（例: 20epoch）\n        #   - もっと頻繁に再スタートさせたいなら T_0=10 も有効\n        scheduler = CosineAnnealingWarmRestarts(\n            optimizer, T_0=20, T_mult=1, eta_min=LR_MIN\n        )  # ★\n\n        # prior & target mask\n        train_idx_labels = tr_ds.gesture_encoder.transform(tr_meta['gesture'])\n        counts = np.bincount(train_idx_labels, minlength=n_classes)\n        priors = torch.tensor(counts / counts.sum(), dtype=torch.float32)\n        la_ce  = LogitAdjustedCE(priors, tau=1.0)\n\n        target_gestures = CompetitionMetric().target_gestures\n        all_classes = list(tr_ds.gesture_encoder.classes_)\n        target_idx = [all_classes.index(g) for g in target_gestures]\n        target_mask_tensor = torch.zeros(n_classes, dtype=torch.bool)\n        target_mask_tensor[target_idx] = True\n\n        best_f1 = -1.0\n        no_improve = 0\n\n        for epoch in range(EPOCHS):\n            tr_loss, tr_f1, tr_f1b, tr_f1m = train_one_epoch(\n                model, tr_loader, optimizer, device, epoch, EPOCHS,\n                la_ce, target_mask_tensor, warmup_epochs=WARMUP_E, eta_min=LR_MIN,\n                scheduler=scheduler  # ★ 追加：scheduler を渡す\n            )\n            va_loss, va_f1, va_f1b, va_f1m = validate(model, va_loader, device, temps=None)\n\n            print(f\"Fold{fold} Ep{epoch+1:03d} | \"\n                  f\"LR={optimizer.param_groups[0]['lr']:.6f} | \"\n                  f\"Train loss={tr_loss:.4f} F1={tr_f1:.4f} | \"\n                  f\"Val F1={va_f1:.4f}\")\n\n            if va_f1 > best_f1:\n                best_f1 = va_f1\n                no_improve = 0\n                best_path = os.path.join(MODEL_DIR, f\"model_fold{fold}_best.pth\")\n                torch.save(model.state_dict(), best_path)\n                # fold scaler をモデル横へコピー\n                shutil.copyfile(os.path.join(scale_path, 'train_scalers.pkl'),\n                                os.path.join(MODEL_DIR, f\"scaler_fold{fold}.pkl\"))\n            else:\n                no_improve += 1\n\n            # last also saved\n            torch.save(model.state_dict(), os.path.join(MODEL_DIR, f\"model_fold{fold}_last.pth\"))\n\n            if no_improve >= EARLY_STOP:\n                print(f\"Early stopping at epoch {epoch+1}\")\n                break\n\n        # 温度スケーリング（ベスト重みで校正）\n        model.load_state_dict(torch.load(os.path.join(MODEL_DIR, f\"model_fold{fold}_best.pth\"),\n                                         map_location=device, weights_only=False))\n        model.eval()\n        T_mc = calibrate_temperature(model, va_loader, device)\n        with open(os.path.join(MODEL_DIR, f\"temp_fold{fold}.pkl\"), 'wb') as f:\n            pickle.dump({'T_mc': T_mc, 'T_bin': 1.0}, f)\n        print(f\"[Fold {fold}] Calibrated T_mc={T_mc:.3f}\")\n\n        all_fold_f1.append(best_f1)\n        # cleanup\n        del model, optimizer, tr_loader, va_loader, tr_ds, va_ds, scheduler  # ★ scheduler も解放\n        gc.collect(); torch.cuda.empty_cache(); time.sleep(1)\n\n    if all_fold_f1:\n        print(\"\\n===== CV Result =====\")\n        for i, s in enumerate(all_fold_f1, 1):\n            print(f\"Fold {i}: {s:.4f}\")\n        print(f\"Mean F1: {np.mean(all_fold_f1):.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:48:59.029143Z","iopub.execute_input":"2025-08-16T06:48:59.029982Z","iopub.status.idle":"2025-08-16T06:48:59.046138Z","shell.execute_reply.started":"2025-08-16T06:48:59.029954Z","shell.execute_reply":"2025-08-16T06:48:59.045463Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [8] Inference module (fold別scaler+温度+マルチクロップ) — 安定版\nimport os, glob, re, pickle, threading\nimport numpy as np\nimport polars as pl\nimport pandas as pd\nimport torch\nimport torch.nn.functional as F\nfrom sklearn.preprocessing import LabelEncoder\n\n# ==== Globals ====\nIMU_MODELS = []\nFOLD_SCALERS = []  # list of tuples: (non_tof_scaler, tof_scaler)\nFOLD_TEMPS = []    # list of dicts:  {'T_mc': float, 'T_bin': float}\nGESTURE_CLASSES = None\nTRAIN_DS = None    # FE-only dummy (uses _feature_engineering of your class)\nINITIALIZED = False\nINIT_LOCK = threading.Lock()\n\n# モデル/スケーラ配置先（提出ノートでは Add Data の実パスに上書き）\nIMU_MODELS_DIR = \"/kaggle/input/imumodel\"  # 例: /kaggle/input/<your-dataset-slug>\nDEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# ==== Columns ====\nTIME_SERIES_FEATURES = [\n    'acc_x','acc_y','acc_z','rot_w','rot_x','rot_y','rot_z',\n    'thm_1','thm_2','thm_3','thm_4','thm_5'\n]\nTOF_FEATURES = [f'tof_{sensor}_v{pixel}' for sensor in range(1,6) for pixel in range(64)]\nSTATIC_FEATURES = ['adult_child','age','sex','handedness','height_cm','shoulder_to_wrist_cm','elbow_to_wrist_cm']\nIMU_FEATS = ['acc_x','acc_y','acc_z','rot_w','rot_x','rot_y','rot_z']\nTHM_FEATS = ['thm_1','thm_2','thm_3','thm_4','thm_5']\n\n# コンペの全クラス名（フォールバック用）\nCOMP_ALL_CLASSES = [\n    'Above ear - pull hair', 'Cheek - pinch skin', 'Eyebrow - pull hair',\n    'Eyelash - pull hair', 'Forehead - pull hairline', 'Forehead - scratch',\n    'Neck - pinch skin', 'Neck - scratch',\n    'Write name on leg', 'Wave hello', 'Glasses on/off', 'Text on phone',\n    'Write name in air', 'Feel around in tray and pull out an object',\n    'Scratch knee/leg skin', 'Pull air toward your face', 'Drink from bottle/cup',\n    'Pinch knee/leg skin'\n]\n\nclass _IdentityScaler:\n    def transform(self, X):\n        return X\n\n# ==== Interpolation (train-pipeline consistent) ====\ndef interpolate_thm_column(col):\n    s = pd.Series(col)\n    return s.interpolate('linear', limit_direction='both').fillna(0).values\n\ndef interpolate_imu_column(col):\n    s = pd.Series(col)\n    return s.ffill().bfill().fillna(0).values\n\ndef interpolate_tof_column(col):\n    s = pd.Series(col)\n    return s.ffill().bfill().fillna(0).values\n\ndef interpolate_sequence_polars(sequence, imu_features, thm_features, tof_features):\n    df = sequence.to_pandas()\n    for f in imu_features:\n        if f in df.columns:\n            df[f] = interpolate_imu_column(df[f])\n    for f in thm_features:\n        if f in df.columns:\n            df[f] = interpolate_thm_column(df[f])\n    for f in tof_features:\n        if f in df.columns:\n            df[f] = interpolate_tof_column(df[f])\n    return pl.from_pandas(df)\n\n# ==== Utility: 推論時のクラス数とラベル順 ====\ndef infer_num_classes_from_ckpt(models_dir: str) -> int:\n    \"\"\"checkpointの分類ヘッドの出力次元からクラス数を推定\"\"\"\n    # 最初に見つかったモデルでOK\n    paths = sorted(glob.glob(os.path.join(models_dir, \"model_fold*_best.pth\")))\n    if not paths:\n        raise FileNotFoundError(f\"No model files found under {models_dir}\")\n    state = torch.load(paths[0], map_location='cpu', weights_only=False)\n    # ヘッド候補のキー\n    head_keys = [\n        'cls_head.weight', 'classifier.weight', 'head.weight', \n        'fc.weight', 'final.weight'\n    ]\n    for k in head_keys:\n        if k in state:\n            return state[k].shape[0]\n    # 次善策：weightで out_features を探す\n    for k, v in state.items():\n        if k.endswith('.weight') and v.ndim == 2:\n            # 最も小さな out_features をクラス数候補に（ヘッドは通常最小）\n            # ただし 1や2 の場合は除外\n            out_features = v.shape[0]\n            if 3 <= out_features <= 64:\n                return out_features\n    raise RuntimeError(\"Could not infer number of classes from checkpoint\")\n\ndef ensure_label_encoder(imu_models_dir: str, n_classes: int) -> LabelEncoder:\n    \"\"\"LabelEncoderを用意（classes.npy優先／無ければコンペのクラス名を辞書順）\"\"\"\n    le = LabelEncoder()\n    classes_npy = os.path.join(imu_models_dir, \"classes.npy\")\n    if os.path.exists(classes_npy):\n        classes = np.load(classes_npy, allow_pickle=True)\n        if len(classes) != n_classes:\n            print(f\"[warn] classes.npy has {len(classes)} classes, but checkpoint expects {n_classes}. \"\n                  f\"Proceeding with classes.npy; verify mapping.\")\n        le.classes_ = np.array(list(classes))\n        return le\n    # フォールバック：コンペの全クラス名を辞書順に\n    classes = sorted(COMP_ALL_CLASSES)\n    if len(classes) != n_classes:\n        raise RuntimeError(\n            f\"Fallback classes length {len(classes)} != inferred {n_classes}. \"\n            f\"Please include classes.npy in your weights dataset.\"\n        )\n    le.fit(classes)\n    return le\n\n# ==== Loader (models + scalers + temps) ====\ndef load_models_and_scalers(models_dir, model_class, n_classes, device):\n    model_paths = sorted(glob.glob(os.path.join(models_dir, \"model_fold*_best.pth\")))\n    if not model_paths:\n        raise FileNotFoundError(\n            f\"No model files found under {models_dir}. \"\n            f\"Attach your weights dataset and ensure files like model_fold1_best.pth exist.\"\n        )\n    models, scalers, temps = [], [], []\n    for p in model_paths:\n        m = re.search(r\"model_fold(\\d+)_best\\.pth$\", os.path.basename(p))\n        if not m:\n            continue\n        k = int(m.group(1))\n        # 1) model（クラス数は推定済みの n_classes を使用）\n        model = model_class(imu_dim=36, tof_dim=325, n_classes=n_classes).to(device)\n        state = torch.load(p, map_location=device, weights_only=False)\n        model.load_state_dict(state)\n        model.eval()\n        models.append(model)\n        # 2) scaler\n        sc_path = os.path.join(models_dir, f\"scaler_fold{k}.pkl\")\n        if not os.path.exists(sc_path):\n            raise FileNotFoundError(f\"Missing scaler for fold{k}: {sc_path}\")\n        # load_models_and_scalers(...):\n        with open(sc_path, \"rb\") as f:\n            sc_obj = pickle.load(f)\n            if isinstance(sc_obj, tuple) and len(sc_obj) == 3:\n                scalers.append(sc_obj)  # (non_tof, tof, demo)\n            else:\n                # 後方互換（旧: (non_tof, tof)）— demo は non_tof と同じ基準に\n                non_tof, tof = sc_obj\n                from sklearn.preprocessing import StandardScaler\n                dummy = StandardScaler()\n                dummy.mean_ = np.zeros(7); dummy.scale_ = np.ones(7); dummy.n_features_in_ = 7\n                scalers.append((non_tof, tof, dummy))\n\n        # 3) temperature (optional)\n        tp_path = os.path.join(models_dir, f\"temp_fold{k}.pkl\")\n        if os.path.exists(tp_path):\n            with open(tp_path, \"rb\") as f:\n                temps.append(pickle.load(f))\n        else:\n            temps.append({'T_mc': 1.0, 'T_bin': 1.0})\n    return models, scalers, temps\n\n# ==== Multi-crop fallback ====\nif 'make_crops' not in globals():\n    def make_crops(x, L=256, K=5):\n        \"\"\"\n        x: (T, C)\n        均等にK箇所の開始位置をサンプリングし、長さLで切り出し（不足はゼロパディング）。\n        \"\"\"\n        T, C = x.shape\n        crops = []\n        if T <= L:\n            pad = np.zeros((L - T, C), dtype=x.dtype)\n            crops = [np.vstack([x, pad])]\n        else:\n            starts = np.linspace(0, T - L, num=K, dtype=int)\n            for s in starts:\n                crops.append(x[s:s+L])\n        return crops\n\n# ==== Lightweight init ====\ndef init_infer_module(\n    imu_models_dir=IMU_MODELS_DIR,\n    metadata_path='/kaggle/input/cmi-detect-behavior-with-sensor-data/train_labels.csv',\n    model_class=None  # 例: IMUOnlyHierModel を渡す\n):\n    \"\"\"\n    - 学習データのロード/fitは一切行わない\n    - GestureDataset.__init__を呼ばず、FE用のダミーを作って _feature_engineering を流用\n    - クラス数は checkpoint から推定。classes.npy があれば順序も再現。\n    \"\"\"\n    from __main__ import GestureDataset  # あなたの定義を参照\n    global IMU_MODELS, FOLD_SCALERS, FOLD_TEMPS, GESTURE_CLASSES, TRAIN_DS, INITIALIZED\n\n    if INITIALIZED:\n        return\n    with INIT_LOCK:\n        if INITIALIZED:\n            return\n        print(\"Initializing inference module...\")\n\n        # 0) 推論クラス数を checkpoint から推定\n        n_classes = infer_num_classes_from_ckpt(imu_models_dir)\n\n        # 1) FE専用の軽量インスタンス（__init__は呼ばない）\n        TRAIN_DS = GestureDataset.__new__(GestureDataset)\n        TRAIN_DS.tof_dim = 320  # 定数\n        TRAIN_DS.gesture_encoder = ensure_label_encoder(imu_models_dir, n_classes)\n        GESTURE_CLASSES = n_classes\n\n        # 2) モデルクラスの選択（Hier→普通の順で探す）\n        if model_class is None:\n            try:\n                from __main__ import IMUOnlyHierModel as DefaultModelClass\n            except Exception:\n                from __main__ import IMUOnlyModel as DefaultModelClass\n            model_class = DefaultModelClass\n\n        # 3) モデル/スケーラ/温度をロード\n        models, scalers, temps = load_models_and_scalers(\n            imu_models_dir, model_class, n_classes=GESTURE_CLASSES, device=DEVICE\n        )\n        IMU_MODELS[:] = models\n        FOLD_SCALERS[:] = scalers\n        FOLD_TEMPS[:] = temps\n\n        print(f\"Inference init: models={len(IMU_MODELS)} · classes={GESTURE_CLASSES}\")\n        INITIALIZED = True\n\n# ==== Predict ====\ndef predict(sequence: pl.DataFrame, demographics: pl.DataFrame) -> str:\n    try:\n        init_infer_module()  # 必要なら model_class=IMUOnlyHierModel を明示\n\n        # 0) 必須列チェック（不足なら即エラーで場所特定）\n        required = IMU_FEATS + THM_FEATS + TOF_FEATURES\n        miss = [c for c in required if c not in sequence.columns]\n        if miss:\n            raise RuntimeError(f\"Missing columns in sequence: {miss[:10]} ... (total {len(miss)})\")\n        miss_d = [c for c in STATIC_FEATURES if c not in demographics.columns]\n        if miss_d:\n            raise RuntimeError(f\"Missing columns in demographics: {miss_d}\")\n\n        # 1) 補間\n        sequence = interpolate_sequence_polars(sequence, IMU_FEATS, THM_FEATS, TOF_FEATURES)\n        demographics = demographics.fill_null(0)\n        sequence = sequence.select(IMU_FEATS + THM_FEATS + TOF_FEATURES)\n        demographics = demographics.select(STATIC_FEATURES)\n\n        # 2) 特徴生成（1回のみ）\n        seq_np = sequence.to_numpy().astype(np.float32)\n        demo_np = demographics.to_numpy().astype(np.float32)[0]\n        full_seq = np.concatenate([seq_np, np.tile(demo_np, (len(seq_np), 1))], axis=1)\n\n        # 学習時と同じ関数を使う（GestureDataset のメソッド）\n        base_features = TRAIN_DS._feature_engineering(full_seq)\n\n        # 3) foldごとに正規化→マルチクロップ→温度→確率算出（ロジット平均版）\n                # 3) foldごとに正規化→マルチクロップ→温度→ロジット算出（logits平均）\n        all_fold_logits = []\n\n        with torch.no_grad():\n            for (model, scaler_pack, temps) in zip(IMU_MODELS, FOLD_SCALERS, FOLD_TEMPS):\n                # --- スケーラの取り出し（3タプル/2タプル両対応）\n                if isinstance(scaler_pack, tuple):\n                    if len(scaler_pack) == 3:\n                        non_tof_scaler, tof_scaler, demo_scaler = scaler_pack\n                    elif len(scaler_pack) == 2:\n                        non_tof_scaler, tof_scaler = scaler_pack\n                        demo_scaler = _IdentityScaler()  # 旧フォーマット互換\n                    else:\n                        raise RuntimeError(f\"Unexpected scaler tuple length: {len(scaler_pack)}\")\n                else:\n                    raise RuntimeError(\"Scaler pack must be a tuple.\")\n\n                # --- base_features: [IMU(36), THM(5), TOF(320), DEMO(7)]\n                D = base_features.shape[1]\n                tof  = base_features[:, D - TRAIN_DS.tof_dim - 7 : D - 7]   # 320\n                demo = base_features[:, -7:]                                 # 7\n                imu_thm = base_features[:, : D - (TRAIN_DS.tof_dim + 7)]     # 36+5\n\n                # --- スケール適用\n                n_imu_thm = non_tof_scaler.transform(imu_thm)\n                n_demo    = demo_scaler.transform(demo)\n                n_tof     = tof_scaler.transform(tof)\n\n                # --- 学習と同じ最終並びに統一: [IMU+THM][DEMO][TOF]\n                norm_features = np.concatenate([n_imu_thm, n_demo, n_tof], axis=1)\n\n                # --- モデル入力の分割（学習と同じロジック）\n                tof_features   = norm_features[:, -TRAIN_DS.tof_dim:]        # 末尾320\n                imu_thm_demo   = norm_features[:, :-TRAIN_DS.tof_dim]        # 残り\n                demo_vec       = imu_thm_demo[0, -7:]                        # DEMO 7\n                thm            = imu_thm_demo[:, :-7][:, -5:]                # THM 5\n                imu            = imu_thm_demo[:, :-7][:, :-5]                # IMU 36\n                thm_tof        = np.concatenate([thm, tof_features], axis=1) # 325\n\n                # --- マルチクロップ\n                imu_cs = make_crops(imu, L=256, K=5)\n                thm_cs = make_crops(thm_tof, L=256, K=5)\n\n                # --- cropごとに TTA ロジットを平均\n                crop_logits = []\n                for ic, tc in zip(imu_cs, thm_cs):\n                    tta_logits = []\n                    for (ic2, tc2) in tta_variants_per_crop(ic, tc):\n                        it = torch.tensor(ic2, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n                        tt = torch.tensor(tc2, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n                        dt = torch.tensor(demo_vec, dtype=torch.float32).unsqueeze(0).to(DEVICE)\n                        out = model(it, tt, dt)\n                        logits = out[0] if isinstance(out, tuple) else out\n                        tta_logits.append(logits.cpu())\n                    crop_logits.append(torch.stack(tta_logits).mean(0))  # (1,C)\n\n                # --- crop 平均 → 温度\n                fold_logits = torch.stack(crop_logits).mean(0)  # (1,C)\n                Tmc = float(temps.get('T_mc', 1.0))\n                fold_logits = fold_logits / max(Tmc, 1e-3)\n\n                all_fold_logits.append(fold_logits)\n\n        # --- 全foldのログイット平均 → softmax 1回\n        logits_total = torch.stack(all_fold_logits).mean(0)  # (1, C)\n        avg_prob = F.softmax(logits_total, dim=1)\n\n        \n        pred_idx = int(avg_prob.argmax(dim=1).item())\n        pred_label = TRAIN_DS.gesture_encoder.inverse_transform([pred_idx])[0]\n        return str(pred_label)\n\n\n    except Exception as e:\n        # Gateway でも見えるように stderr へ詳細を出す\n        import sys, traceback\n        print(\"PREDICT_ERROR:\", e, file=sys.stderr)\n        print(traceback.format_exc(), file=sys.stderr)\n        raise\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-08-16T06:57:53.564871Z","iopub.execute_input":"2025-08-16T06:57:53.565559Z","iopub.status.idle":"2025-08-16T06:57:53.602497Z","shell.execute_reply.started":"2025-08-16T06:57:53.565532Z","shell.execute_reply":"2025-08-16T06:57:53.601805Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# %% [9] Kaggle Inference Server\nimport kaggle_evaluation.cmi_inference_server\n\ninference_server = kaggle_evaluation.cmi_inference_server.CMIInferenceServer(predict)\n\nif os.getenv('KAGGLE_IS_COMPETITION_RERUN'):\n    inference_server.serve()\nelse:\n    # ローカルゲートウェイ（デバッグ用）\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-16T06:57:56.114043Z","iopub.execute_input":"2025-08-16T06:57:56.114783Z","iopub.status.idle":"2025-08-16T06:57:57.689101Z","shell.execute_reply.started":"2025-08-16T06:57:56.114757Z","shell.execute_reply":"2025-08-16T06:57:57.688420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}