From ffd836fcea55d5dacc7e640d5527a709d1ef8567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A3=AE?= Date: Fri, 6 Feb 2026 17:47:49 +0900 Subject: [PATCH] Add improvement notes for optimized script --- docs/brain_lever_competition_improvements.md | 336 +++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 docs/brain_lever_competition_improvements.md diff --git a/docs/brain_lever_competition_improvements.md b/docs/brain_lever_competition_improvements.md new file mode 100644 index 0000000..37951a7 --- /dev/null +++ b/docs/brain_lever_competition_improvements.md @@ -0,0 +1,336 @@ +# 改善案(脳活動からレバー位置予測) + +以下は、提示されたベースライン実装を前提にした改善アイデアのまとめです。実装方針を具体化しやすいように、追加の特徴量設計やクロスバリデーション、LightGBMの学習設定改善などを整理しています。 + +## 1. 特徴量エンジニアリングの拡張 + +### 1-1. 時系列の統計量 +現在はラグと差分、セッション内Zスコアのみですが、**一定ウィンドウの統計量**を追加すると安定しやすいです。 + +- 直近 `k` ステップの **平均・標準偏差・最小/最大** +- **指数移動平均 (EMA)** +- **絶対値差分**(速度のような指標) + +### 1-2. 周波数領域・帯域情報(簡易) +短いウィンドウで **移動平均を2種**(例: 3ステップ平均・10ステップ平均)入れるだけでも、低周波/高周波の成分を分けた特徴量として役立ちます。 + +### 1-3. セッション単位の集約特徴 +各 `sample_id` 内での +- **平均/分散** +- **最大-最小** +- **トレンド傾向(時間 vs 平均値の回帰係数)** +などを、セッション全体特徴として行全体に付与する方法もあります。 + +## 2. モデルと学習設定の改善 + +### 2-1. LightGBMのearly stopping +現在は `early_stopping(100)` を使っていますが、 +`min_data_in_leaf` や `max_depth` を調整すると過学習が減ることがあります。 + +### 2-2. レギュラライズ +LightGBMの `reg_alpha` や `reg_lambda` を追加して +安定化させるのが有効です。 + +### 2-3. Ridge/LGBMのアンサンブル +Ridgeの線形予測とLGBMの非線形予測を **平均アンサンブル**すると、 +MSEが安定しやすくなります。 + +## 3. クロスバリデーション設計 +GroupKFoldは妥当ですが、`sample_id` が「実験セッション」であるなら +**GroupKFold固定**は必須。 +さらに、**最後の一部時間を各セッション内の検証区間とする時系列CV**も試す価値があります。 + +## 4. 改善例コード(抜粋) + +以下は、現行コードの構造を保ったまま追加できる改善例です。 + +```python +def add_rolling_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", window=5): + df = df.sort_values([group_col, "time"]).copy() + rolled = ( + df.groupby(group_col)[numeric_cols] + .rolling(window=window, min_periods=1) + .agg(["mean", "std", "min", "max"]) + .reset_index(level=0, drop=True) + ) + rolled.columns = [f"{c}_roll{window}_{stat}" for c, stat in rolled.columns] + return rolled + +def add_ema_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", span=10): + df = df.sort_values([group_col, "time"]).copy() + ema = df.groupby(group_col)[numeric_cols].apply(lambda g: g.ewm(span=span).mean()) + ema = ema.reset_index(level=0, drop=True) + ema.columns = [f"{c}_ema{span}" for c in numeric_cols] + return ema + +def prepare_features(train: pd.DataFrame, test: pd.DataFrame): + y = train["lever"].astype(float) + + base_train = train.drop(columns=["lever", "id"]) + base_test = test.drop(columns=["id"]) + + numeric_cols = base_train.select_dtypes(include="number").columns.tolist() + cat_cols = [c for c in base_train.columns if c not in numeric_cols and c != "sample_id"] + + train_feat = add_time_series_features(base_train, numeric_cols) + test_feat = add_time_series_features(base_test, numeric_cols) + + train_z = add_session_zscore(base_train, numeric_cols) + test_z = add_session_zscore(base_test, numeric_cols) + + train_diff = add_diff_features(base_train, numeric_cols) + test_diff = add_diff_features(base_test, numeric_cols) + + # 追加: rolling & EMA + train_roll = add_rolling_features(base_train, numeric_cols, window=5) + test_roll = add_rolling_features(base_test, numeric_cols, window=5) + + train_ema = add_ema_features(base_train, numeric_cols, span=10) + test_ema = add_ema_features(base_test, numeric_cols, span=10) + + train_feat = pd.concat([train_feat, train_z, train_diff, train_roll, train_ema], axis=1) + test_feat = pd.concat([test_feat, test_z, test_diff, test_roll, test_ema], axis=1) + + X_train = train_feat.drop(columns=["sample_id"]) + X_test = test_feat.drop(columns=["sample_id"]) + + if cat_cols: + X_train = pd.get_dummies(X_train, columns=cat_cols, dummy_na=False) + X_test = pd.get_dummies(X_test, columns=cat_cols, dummy_na=False) + + X_train, X_test = X_train.align(X_test, join="left", axis=1, fill_value=0) + X_train = X_train.fillna(0) + X_test = X_test.fillna(0) + + return X_train, y, X_test +``` + +## 5. 追加のチューニング候補 +- `num_leaves`, `min_data_in_leaf`, `max_depth` を探索 +- `learning_rate` を小さめにして `n_estimators` を増やす(高精度狙い) +- `subsample` / `colsample_bytree` で正則化を強める + +## 6. 改善を適用した“最終コード”例 +「実際に置き換えるとどうなるか」を明確にするため、上記の改善(rolling/EMA/正則化・early stoppingなど)を反映した**全体例**を掲載します。パスやカラム名は手元のデータに合わせてください。 + +```python +import pandas as pd +from pathlib import Path +from sklearn.model_selection import GroupKFold +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import StandardScaler +from sklearn.linear_model import Ridge +from sklearn.metrics import mean_squared_error +import lightgbm as lgb + + +def load_data(base: Path): + train = pd.read_csv(base / "train.csv") + test = pd.read_csv(base / "test.csv") + sample_sub = pd.read_csv(base / "sample_submission.csv") + return train, test, sample_sub + + +def add_time_series_features(df: pd.DataFrame, numeric_cols, group_col="sample_id"): + df = df.sort_values([group_col, "time"]).copy() + for lag in (1, 2, 3): + lagged = df.groupby(group_col)[numeric_cols].shift(lag) + lagged.columns = [f"{c}_lag{lag}" for c in numeric_cols] + df = pd.concat([df, lagged], axis=1) + return df + + +def add_session_zscore(df: pd.DataFrame, numeric_cols, group_col="sample_id"): + grouped = df.groupby(group_col)[numeric_cols] + mean = grouped.transform("mean") + std = grouped.transform("std").replace(0, 1) + z = (df[numeric_cols] - mean) / std + z.columns = [f"{c}_z" for c in numeric_cols] + return z + + +def add_diff_features(df: pd.DataFrame, numeric_cols, group_col="sample_id"): + df = df.sort_values([group_col, "time"]).copy() + diff = df.groupby(group_col)[numeric_cols].diff(1) + diff.columns = [f"{c}_diff1" for c in numeric_cols] + return diff + + +def add_rolling_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", window=5): + df = df.sort_values([group_col, "time"]).copy() + rolled = ( + df.groupby(group_col)[numeric_cols] + .rolling(window=window, min_periods=1) + .agg(["mean", "std", "min", "max"]) + .reset_index(level=0, drop=True) + ) + rolled.columns = [f"{c}_roll{window}_{stat}" for c, stat in rolled.columns] + return rolled + + +def add_ema_features(df: pd.DataFrame, numeric_cols, group_col="sample_id", span=10): + df = df.sort_values([group_col, "time"]).copy() + ema = df.groupby(group_col)[numeric_cols].apply(lambda g: g.ewm(span=span).mean()) + ema = ema.reset_index(level=0, drop=True) + ema.columns = [f"{c}_ema{span}" for c in numeric_cols] + return ema + + +def prepare_features(train: pd.DataFrame, test: pd.DataFrame): + y = train["lever"].astype(float) + + base_train = train.drop(columns=["lever", "id"]) + base_test = test.drop(columns=["id"]) + + numeric_cols = base_train.select_dtypes(include="number").columns.tolist() + cat_cols = [c for c in base_train.columns if c not in numeric_cols and c != "sample_id"] + + train_feat = add_time_series_features(base_train, numeric_cols) + test_feat = add_time_series_features(base_test, numeric_cols) + + train_z = add_session_zscore(base_train, numeric_cols) + test_z = add_session_zscore(base_test, numeric_cols) + + train_diff = add_diff_features(base_train, numeric_cols) + test_diff = add_diff_features(base_test, numeric_cols) + + train_roll = add_rolling_features(base_train, numeric_cols, window=5) + test_roll = add_rolling_features(base_test, numeric_cols, window=5) + + train_ema = add_ema_features(base_train, numeric_cols, span=10) + test_ema = add_ema_features(base_test, numeric_cols, span=10) + + train_feat = pd.concat([train_feat, train_z, train_diff, train_roll, train_ema], axis=1) + test_feat = pd.concat([test_feat, test_z, test_diff, test_roll, test_ema], axis=1) + + X_train = train_feat.drop(columns=["sample_id"]) + X_test = test_feat.drop(columns=["sample_id"]) + + if cat_cols: + X_train = pd.get_dummies(X_train, columns=cat_cols, dummy_na=False) + X_test = pd.get_dummies(X_test, columns=cat_cols, dummy_na=False) + + X_train, X_test = X_train.align(X_test, join="left", axis=1, fill_value=0) + X_train = X_train.fillna(0) + X_test = X_test.fillna(0) + return X_train, y, X_test + + +def cv_score(X, y, groups, model_type="lgbm"): + cv = GroupKFold(n_splits=5) + scores = [] + + for fold, (tr_idx, va_idx) in enumerate(cv.split(X, y, groups=groups), start=1): + X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx] + y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx] + + if model_type == "ridge": + model = make_pipeline(StandardScaler(with_mean=False), Ridge(alpha=1.0)) + model.fit(X_tr, y_tr) + pred = model.predict(X_va) + else: + model = lgb.LGBMRegressor( + n_estimators=2000, + learning_rate=0.03, + num_leaves=63, + max_depth=-1, + min_data_in_leaf=80, + subsample=0.8, + colsample_bytree=0.8, + reg_alpha=0.1, + reg_lambda=0.1, + random_state=42, + n_jobs=-1, + ) + model.fit( + X_tr, + y_tr, + eval_set=[(X_va, y_va)], + eval_metric="mse", + callbacks=[lgb.early_stopping(200, verbose=False)], + ) + pred = model.predict(X_va, num_iteration=model.best_iteration_) + + mse = mean_squared_error(y_va, pred) + scores.append(mse) + print(f"fold {fold} mse: {mse:.6f}") + + print(f"mean mse: {sum(scores)/len(scores):.6f}") + + +def train_and_predict(X, y, X_test, model_type="lgbm"): + if model_type == "ridge": + model = make_pipeline(StandardScaler(with_mean=False), Ridge(alpha=1.0)) + model.fit(X, y) + return model.predict(X_test) + + model = lgb.LGBMRegressor( + n_estimators=2000, + learning_rate=0.03, + num_leaves=63, + max_depth=-1, + min_data_in_leaf=80, + subsample=0.8, + colsample_bytree=0.8, + reg_alpha=0.1, + reg_lambda=0.1, + random_state=42, + n_jobs=-1, + ) + model.fit(X, y) + return model.predict(X_test) + + +def main(): + base = Path(r"c:\Users\hashi\Downloads\JOAI") + train, test, sample_sub = load_data(base) + X, y, X_test = prepare_features(train, test) + + groups = train["sample_id"] + model_type = "lgbm" + cv_score(X, y, groups, model_type=model_type) + + test_pred = train_and_predict(X, y, X_test, model_type=model_type) + submission = sample_sub.copy() + submission["lever"] = test_pred + submission.to_csv(base / "submission_baseline.csv", index=False) + print("saved: submission_baseline.csv") + + +if __name__ == "__main__": + main() +``` + +## 7. 参考: ユーザー提示の最適化スクリプトに対する改善ポイント +以下は、提示された「最適化版」スクリプトに対する改善点の整理です。コード全体の方向性は良いものの、**再現性・汎化性・計算コスト**の観点で修正すると精度が安定しやすくなります。 + +### 7-1. データリーク/評価設計の見直し +- `time_holdout_split` は **未来のデータが学習に混ざらない**ようにする点では良いですが、`day_n` と `time` の順序だけで切ると **マウスIDやセッションの分布が偏る**可能性があります。 + - `GroupKFold(sample_id)` を基本にしつつ、**最後の時間区間を検証**にする「時系列+グループ」の折衷設計を検討してください。 +- `group_mouse` は **マウスIDで分割**できる点は良いですが、分布が大きく変わるためスコアが不安定になりやすいです。 + - 指標のブレが大きい場合は `group_sample` を優先し、補助的に `group_mouse` を使うのが安全です。 + +### 7-2. 特徴量の過多と冗長性 +- 現状は「平均・標準偏差・最大/最小」「左右差」「機能領域平均」「学習進捗」「時系列短期平均」などが大量に入り、**冗長な特徴量が多い**です。 + - LightGBMなど木系は冗長でも耐えますが、**不要特徴が過学習と計算コストを増やす**ため、重要度で削減すると良いです。 +- `SSp_ll_l_enhanced` などの「2倍強調」は **データドリブンでなく経験則**なので、まずは通常特徴として扱い、必要なら後で重み付けを検討しましょう。 + +### 7-3. 時系列特徴の扱い +- `rolling(window=3)` などの単純移動平均だけでなく、**差分・EMA・ラグ**の追加は有効です。 +- ただし `sample_id` 内の時系列特徴を増やしすぎると **リークに近い情報**を取りやすくなるため、 + - `lag1/2/3`, `diff1`, `EMA(10)` 程度から始めるのが安全です。 + +### 7-4. モデルの重複と計算コスト +- XGBoost / LightGBM / RandomForest の **3モデルを常に回す設計**は重いです。 + - まず LightGBM を主軸にし、スコアが詰まったら追加モデルを試す方が効率的です。 +- アンサンブル平均は安定性が増す一方で、**CV設計が不十分だと評価が過大に見える**ため注意が必要です。 + +### 7-5. 数値安定性・前処理 +- `inf` の置換は良いですが、**NaN補完は変換前に統一**した方が安定します。 +- `mouse_id_encoded` を数値特徴として扱うのは有効ですが、**カテゴリ特徴として扱う**(One-Hot / Target Encoding)も有効です。 + +### 7-6. 改善の方向性(推奨) +- **特徴量削減**: 重要度で上位 200〜500 程度に絞る。 +- **シンプルな時系列特徴**: `lag + diff + EMA` 程度に抑える。 +- **モデルは LGBM 中心**: まず `num_leaves` と `min_data_in_leaf` を調整。 +- **CV安定化**: `GroupKFold(sample_id)` を基準に、補助で `time_holdout`。