学習する(改良3:「どの情報をどう使うか」をモデル自身に学習させる仕組みを追加)

学習

今回のコード改良では、CLS+学習窓+MoE、つまり従来のLSTM+SetTransformer構成に対して、「どの情報をどう使うか」をモデル自身に学習させる仕組みを追加しました。これにより、人間があらかじめ決め打ちした窓幅や重み付けに依存せず、状況に応じた柔軟な表現が可能になります。

改良は大きく4ステップに分かれています。


① SetTransformer の拡張(レースCLSトークン対応)

従来の SetTransformer は馬ごとの特徴ベクトルをそのまま自己注意で処理していました。
今回の改良では、**レース全体を表す特殊トークン(CLSトークン)**を先頭に追加。これにより、各馬の特徴に加えて、レース全体の文脈を学習的に共有できるようになりました。

イメージすると「このレースは全体的にハイペース気味」「芝の状態が全馬に影響」などの共通要素を抽出できる教師なしのまとめ役を追加した感じです。


② 学習窓プーリング(TemporalAttentionPool)

これまで履歴を扱う際は「直近3走」「直近5走」といった固定窓の平均/分散を特徴量にしていました。
しかし、実際には「直近1走だけが強く効く馬」や「10走前の特定条件が重要」など、固定窓では表現できないケースがあります。

そこで、LSTMが出力する時系列埋め込みに対して TemporalAttentionPool を導入。
これは各時刻に対する重みを学習し、「どの走が重要か」をモデル自身に選ばせる仕組みです。


③ Mixture-of-Experts (MoE) ゲート

特徴表現の変換を1本のMLPで処理するのではなく、**複数の「専門家」ネットワーク(Expert)**を用意しました。
各馬・各レースごとにゲートが動的に重みを割り振り、最適な組み合わせを選択します。

これにより「この馬は距離適性が効くからExpert Aを強めに」「このレースは斤量差が重要だからExpert Bを選ぶ」といった柔軟な思考が可能になり、モデルが状況ごとに思考回路を切り替えるイメージに近づきます。


④ モデル構築部の整理

従来はモデルを作った後に set_transformer=... で差し替える形を取っていました。
これをやめて、HybridRaceModelWithEmbedding のコンストラクタ引数で直接 SetTransformer の設定を渡すよう変更しました。

これにより、コードがシンプルになり、拡張(CLSトークンON/OFF、MoE使用有無、学習窓使用有無)を1か所で制御できるようになりました。


改良の効果まとめ

  • レース全体の文脈を共有できる(CLSトークン)
  • 重要な走歴をモデルが選べる(学習窓プーリング)
  • 状況ごとに異なる視点を切り替えられる(MoEゲート)
  • コードの拡張性と可読性を向上(SetTransformer設定の統合)

結果として、単純な固定窓や平均ベースの特徴量に頼るのではなく、
モデル自身が「どの情報を重視するか」を学習的に決定できる構造に進化しました。

差し替え①:SetTransformer(レースCLSトークン対応)

既存の class SetTransformer(nn.Module): ...まるごと置き換え

class SetTransformer(nn.Module):
    def __init__(self, input_dim, hidden_dim=16, num_heads=2, num_blocks=1,
                 dropout_p=0.3, use_race_token=True):
        super().__init__()
        self.encoder_linear = nn.Linear(input_dim, hidden_dim)
        self.encoder_relu = nn.ReLU()
        self.encoder_ln = nn.LayerNorm(hidden_dim)
        self.encoder_dropout = nn.Dropout(p=dropout_p)

        self.blocks = nn.ModuleList([
            nn.TransformerEncoderLayer(
                d_model=hidden_dim,
                nhead=num_heads,
                batch_first=True,
                dropout=dropout_p
            )
            for _ in range(num_blocks)
        ])

        self.use_race_token = use_race_token
        if self.use_race_token:
            self.race_token = nn.Parameter(torch.zeros(1, 1, hidden_dim))
            nn.init.normal_(self.race_token, std=0.02)

        self.output = nn.Sequential(
            nn.Linear(hidden_dim, 32),
            nn.ReLU(),
            nn.Dropout(p=dropout_p),
            nn.Linear(32, 1)
        )

    def forward(self, x, key_padding_mask=None):
        squeeze_back = False
        if x.dim() == 2:
            x = x.unsqueeze(0)
            squeeze_back = True
            if key_padding_mask is not None and key_padding_mask.dim() == 1:
                key_padding_mask = key_padding_mask.unsqueeze(0)

        B, S, D = x.shape
        x = self.encoder_linear(x)
        x = self.encoder_relu(x)
        x = self.encoder_dropout(x)
        x = self.encoder_ln(x)

        if self.use_race_token:
            token = self.race_token.repeat(B, 1, 1)  # [B,1,D]
            x = torch.cat([token, x], dim=1)         # [B,1+S,D]
            if key_padding_mask is not None:
                pad = torch.zeros((B, 1), dtype=key_padding_mask.dtype,
                                  device=key_padding_mask.device)
                key_padding_mask = torch.cat([pad, key_padding_mask], dim=1)

        for block in self.blocks:
            x = block(x, src_key_padding_mask=key_padding_mask)

        if self.use_race_token:
            x = x[:, 1:, :]  # 文脈提供のみ。出力は馬トークンのみ

        out = self.output(x).squeeze(-1)  # [B,S]
        if squeeze_back:
            out = out.squeeze(0)
        return out

追記②:学習窓プーリング&MoE(新規クラス2つ)

これらは新規追加SetTransformer の直後あたりに貼ってください。

class TemporalAttentionPool(nn.Module):
    """ 時間方向の重みを学習して加重平均(学習窓) """
    def __init__(self, d):
        super().__init__()
        self.q = nn.Parameter(torch.randn(d))

    def forward(self, H):  # H: [N, T, d]
        a = torch.matmul(H, self.q) / (H.size(-1) ** 0.5)  # [N,T]
        a = torch.softmax(a, dim=1)
        return torch.sum(H * a.unsqueeze(-1), dim=1)       # [N,d]


class MoEGate(nn.Module):
    """ Mixture-of-Experts: 複数の変換をゲートで重み付け合成 """
    def __init__(self, d, n_experts=3, hidden=64):
        super().__init__()
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(d, hidden), nn.ReLU(),
                nn.Linear(hidden, d)
            ) for _ in range(n_experts)
        ])
        self.gate = nn.Sequential(
            nn.Linear(d, hidden), nn.ReLU(),
            nn.Linear(hidden, n_experts)
        )

    def forward(self, h):  # [B,S,d]
        g = torch.softmax(self.gate(h), dim=-1)  # [B,S,E]
        outs = [exp(h) for exp in self.experts]  # E個の [B,S,d]
        stacked = torch.stack(outs, dim=-1)      # [B,S,d,E]
        return torch.sum(stacked * g.unsqueeze(-2), dim=-1)  # [B,S,d]

差し替え③:HybridRaceModelWithEmbedding(学習窓+MoEを内蔵)

既存の class HybridRaceModelWithEmbedding(...):まるごと置き換え

class HybridRaceModelWithEmbedding(nn.Module):
    def __init__(self, input_dim, sequence_length, lstm_hidden,
                 embedding_input_dims=None, embedding_output_dims=None,
                 set_hidden_dim=32, set_heads=2, set_blocks=1, set_dropout=0.3,
                 use_temporal_attention=True, use_moe=True, moe_experts=3, moe_hidden=64,
                 use_race_token=True):
        super().__init__()
        self.sequence_length = sequence_length
        self.embedding_layers = nn.ModuleDict()
        self.embedding_dims = 0

        if embedding_input_dims and embedding_output_dims:
            for col in embedding_input_dims:
                self.embedding_layers[col] = nn.Embedding(
                    num_embeddings=int(embedding_input_dims[col]),
                    embedding_dim=int(embedding_output_dims[col])
                )
                self.embedding_dims += int(embedding_output_dims[col])

        self.lstm = nn.LSTM(
            input_size=input_dim + self.embedding_dims,
            hidden_size=lstm_hidden,
            batch_first=True
        )
        self.dropout = nn.Dropout(p=0.3)

        self.use_temporal_attention = use_temporal_attention
        if self.use_temporal_attention:
            self.temp_pool = TemporalAttentionPool(lstm_hidden)

        self.use_moe = use_moe
        if self.use_moe:
            self.moe = MoEGate(d=lstm_hidden, n_experts=moe_experts, hidden=moe_hidden)

        self.set_transformer = SetTransformer(
            input_dim=lstm_hidden,
            hidden_dim=set_hidden_dim,
            num_heads=set_heads,
            num_blocks=set_blocks,
            dropout_p=set_dropout,
            use_race_token=use_race_token
        )

    def forward(self, x_num, x_cat_dict, mask=None):
        squeeze_b = False
        if x_num.dim() == 3:  # [S,T,D] -> [1,S,T,D]
            x_num = x_num.unsqueeze(0)
            squeeze_b = True

        B, S, T, D = x_num.shape
        device = x_num.device

        emb_list = []
        for col, x_cat in x_cat_dict.items():
            if x_cat.dim() == 2:  # [S,T] -> [1,S,T]
                x_cat = x_cat.unsqueeze(0)
            x_cat = x_cat.to(device)
            emb = self.embedding_layers[col](x_cat)  # [B,S,T,emb_d]
            emb_list.append(emb)

        x = torch.cat([x_num] + emb_list, dim=-1) if emb_list else x_num  # [B,S,T,D+emb]
        x = x.view(B * S, T, -1)  # [B*S,T,F]

        H, _ = self.lstm(x)       # [B*S,T,lstm_hidden]
        H = self.dropout(H)

        if self.use_temporal_attention:
            h = self.temp_pool(H)     # [B*S,d]
        else:
            h = H[:, -1, :]           # 最終時刻

        h = h.view(B, S, -1)          # [B,S,d]

        if self.use_moe:
            h = self.moe(h)           # [B,S,d]

        key_padding_mask = None if mask is None else (mask if mask.dim() == 2 else mask.unsqueeze(0))
        out = self.set_transformer(h, key_padding_mask=key_padding_mask)  # [B,S]
        if squeeze_b:
            out = out.squeeze(0)
        return out

差し替え④:モデル構築部(train__model開始のところ)

model_self_attn = HybridRaceModelWithEmbedding(... から emodel = ... までを置き換え
以降の学習ループはそのままでOKです。set_transformer=... の上書き行は不要になります。

print("✅ train__model開始")
input_dim = X_train_seq.shape[2] 

class_weights_dict = compute_class_weights(y_train)
class_weights_tensor = torch.tensor(
    [class_weights_dict.get(i, 1.0) for i in range(6)],
    dtype=torch.float32
).to("cuda" if torch.cuda.is_available() else "cpu")

# Self-Attn側(軽め設定)
model_self_attn = HybridRaceModelWithEmbedding(
    input_dim=input_dim,
    sequence_length=sequence_length,
    lstm_hidden=64,
    embedding_input_dims=embedding_input_dims,
    embedding_output_dims=embedding_output_dims,
    set_hidden_dim=16, set_heads=1, set_blocks=1, set_dropout=0.1,
    use_temporal_attention=True,   # ✅ 学習窓ON
    use_moe=True, moe_experts=3,   # ✅ MoE ON
    use_race_token=True            # ✅ レースCLSトークンON
)

# Set-Transformer側(厚め設定)
model_set_transformer = HybridRaceModelWithEmbedding(
    input_dim=input_dim,
    sequence_length=sequence_length,
    lstm_hidden=64,
    embedding_input_dims=embedding_input_dims,
    embedding_output_dims=embedding_output_dims,
    set_hidden_dim=32, set_heads=2, set_blocks=2, set_dropout=0.4,
    use_temporal_attention=True,
    use_moe=True, moe_experts=3,
    use_race_token=True
)

# Stacking Ensemble(既存どおり)
emodel = StackingEnsembleRaceModel(
    model_self_attn=model_self_attn,
    model_set_transformer=model_set_transformer,
    hidden_dim=32
).to(device)

評価


🚀 Training started...
Epoch 1: total=3855.7766 | top1_prob=0.737 | top1_acc=0.824 | ndcg3=0.962 | ⏱️ 303.77s
[Epoch 0] LR[0]: 0.000500
Epoch 2: total=4424.7383 | top1_prob=0.654 | top1_acc=0.586 | ndcg3=0.913 | ⏱️ 302.00s
[Epoch 1] LR[0]: 0.000500
Epoch 3: total=4180.6662 | top1_prob=0.709 | top1_acc=0.727 | ndcg3=0.943 | ⏱️ 300.98s
[Epoch 2] LR[0]: 0.000500
Epoch 4: total=4380.6302 | top1_prob=0.655 | top1_acc=0.763 | ndcg3=0.943 | ⏱️ 301.56s
⛔ Early stopping: 改善停止(best=0.9499)
✅ transformer_model 保存済み
✅ final_feature_names_seq 保存済み
✅ final_feature_names_seq 保存済み
[auto-batch] OK with batch_size=4096

📊 Baseline NDCG@3: 0.7117

🚀 Numeric PFI: 69 features × 5 repeats
✅ done: 距離  ΔNDCG@3 = +0.0037 ± 0.0035  (149.8s)
✅ done: 馬番  ΔNDCG@3 = +0.0042 ± 0.0013  (151.7s)
✅ done: 斤量  ΔNDCG@3 = +0.0039 ± 0.0009  (150.9s)
✅ done: 馬体重  ΔNDCG@3 = +0.0032 ± 0.0020  (150.8s)
✅ done: 体重増減  ΔNDCG@3 = +0.0046 ± 0.0011  (154.0s)
✅ done: 日数差  ΔNDCG@3 = +0.0017 ± 0.0021  (155.3s)
✅ done: 日数差_norm  ΔNDCG@3 = -0.0024 ± 0.0005  (155.3s)
✅ done: 斤量_馬体重比  ΔNDCG@3 = -0.0027 ± 0.0001  (157.3s)
✅ done: 距離_ma3_馬  ΔNDCG@3 = +0.0015 ± 0.0028  (157.7s)
✅ done: 距離_sd3_馬  ΔNDCG@3 = +0.0031 ± 0.0017  (158.3s)
✅ done: 距離_diff_vs_馬ma3  ΔNDCG@3 = +0.0049 ± 0.0017  (184.8s)
✅ done: 距離_z_vs_馬ma3  ΔNDCG@3 = +0.0078 ± 0.0016  (159.5s)
✅ done: 距離_ma5_馬  ΔNDCG@3 = +0.0032 ± 0.0017  (157.4s)
✅ done: 距離_sd5_馬  ΔNDCG@3 = +0.0020 ± 0.0025  (160.6s)
✅ done: 距離_diff_vs_馬ma5  ΔNDCG@3 = +0.0055 ± 0.0016  (160.6s)
✅ done: 距離_z_vs_馬ma5  ΔNDCG@3 = +0.0028 ± 0.0013  (160.2s)
✅ done: 距離_ma7_馬  ΔNDCG@3 = +0.0028 ± 0.0012  (160.1s)
✅ done: 距離_sd7_馬  ΔNDCG@3 = +0.0053 ± 0.0018  (158.6s)
✅ done: 距離_diff_vs_馬ma7  ΔNDCG@3 = +0.0006 ± 0.0011  (159.8s)
✅ done: 距離_z_vs_馬ma7  ΔNDCG@3 = -0.0012 ± 0.0009  (153.3s)
✅ done: 斤量_ma3_馬  ΔNDCG@3 = -0.0009 ± 0.0015  (155.5s)
✅ done: 斤量_sd3_馬  ΔNDCG@3 = -0.0021 ± 0.0012  (158.9s)
✅ done: 斤量_diff_vs_馬ma3  ΔNDCG@3 = -0.0018 ± 0.0011  (159.5s)
✅ done: 斤量_z_vs_馬ma3  ΔNDCG@3 = -0.0009 ± 0.0008  (159.4s)
✅ done: 斤量_ma5_馬  ΔNDCG@3 = -0.0012 ± 0.0010  (159.2s)
✅ done: 斤量_sd5_馬  ΔNDCG@3 = -0.0021 ± 0.0009  (159.8s)
✅ done: 斤量_diff_vs_馬ma5  ΔNDCG@3 = -0.0016 ± 0.0003  (160.0s)
✅ done: 斤量_z_vs_馬ma5  ΔNDCG@3 = -0.0004 ± 0.0014  (159.0s)
✅ done: 斤量_ma7_馬  ΔNDCG@3 = -0.0019 ± 0.0013  (156.6s)
✅ done: 斤量_sd7_馬  ΔNDCG@3 = -0.0036 ± 0.0018  (158.1s)
✅ done: 斤量_diff_vs_馬ma7  ΔNDCG@3 = -0.0012 ± 0.0007  (158.2s)
✅ done: 斤量_z_vs_馬ma7  ΔNDCG@3 = -0.0025 ± 0.0017  (159.4s)
✅ done: 距離_mean_クラス過去_ma3  ΔNDCG@3 = +0.0019 ± 0.0020  (160.3s)
✅ done: 距離_diff_vs_クラス_ma3  ΔNDCG@3 = +0.0007 ± 0.0012  (193.8s)
✅ done: 距離_mean_クラス過去_ma5  ΔNDCG@3 = +0.0017 ± 0.0019  (158.6s)
✅ done: 距離_diff_vs_クラス_ma5  ΔNDCG@3 = +0.0012 ± 0.0018  (160.7s)
✅ done: 距離_mean_クラス過去_ma7  ΔNDCG@3 = +0.0014 ± 0.0018  (161.4s)
✅ done: 距離_diff_vs_クラス_ma7  ΔNDCG@3 = +0.0019 ± 0.0043  (161.1s)
✅ done: 斤量_mean_クラス過去_ma3  ΔNDCG@3 = +0.0029 ± 0.0008  (159.6s)
✅ done: 斤量_diff_vs_クラス_ma3  ΔNDCG@3 = +0.0013 ± 0.0018  (157.0s)
✅ done: 斤量_mean_クラス過去_ma5  ΔNDCG@3 = +0.0013 ± 0.0016  (155.4s)
✅ done: 斤量_diff_vs_クラス_ma5  ΔNDCG@3 = +0.0034 ± 0.0007  (158.5s)
✅ done: 斤量_mean_クラス過去_ma7  ΔNDCG@3 = +0.0031 ± 0.0008  (164.3s)
✅ done: 斤量_diff_vs_クラス_ma7  ΔNDCG@3 = +0.0026 ± 0.0013  (153.5s)
✅ done: 馬体重_diff_prev  ΔNDCG@3 = +0.0016 ± 0.0011  (159.8s)
✅ done: 馬体重_pct_prev  ΔNDCG@3 = +0.0031 ± 0.0002  (140.6s)
✅ done: 馬体重_ma5_馬  ΔNDCG@3 = +0.0017 ± 0.0014  (143.6s)
✅ done: 馬体重_sd5_馬  ΔNDCG@3 = +0.0003 ± 0.0012  (162.8s)
✅ done: 馬体重_z_vs_馬ma5  ΔNDCG@3 = -0.0003 ± 0.0005  (154.8s)
✅ done: 馬体重_diff_prev_ma2  ΔNDCG@3 = +0.0006 ± 0.0020  (159.1s)
✅ done: 馬体重_diff_prev_ma3  ΔNDCG@3 = +0.0007 ± 0.0015  (160.9s)
✅ done: 馬体重_diff_prev_ma5  ΔNDCG@3 = +0.0000 ± 0.0005  (157.1s)
✅ done: 好成績  ΔNDCG@3 = -0.0003 ± 0.0004  (155.1s)
✅ done: 距離カテゴリフラグ  ΔNDCG@3 = -0.0000 ± 0.0008  (158.0s)
✅ done: 脚質フラグ  ΔNDCG@3 = -0.0003 ± 0.0008  (162.0s)
✅ done: 馬場フラグ  ΔNDCG@3 = +0.0006 ± 0.0012  (158.2s)
✅ done: 芝ダフラグ  ΔNDCG@3 = +0.0001 ± 0.0009  (186.2s)
✅ done: 出走数  ΔNDCG@3 = +0.0143 ± 0.0019  (160.0s)
✅ done: 勝利数  ΔNDCG@3 = +0.0165 ± 0.0028  (161.1s)
✅ done: 騎手勝率  ΔNDCG@3 = +0.0127 ± 0.0003  (162.8s)
✅ done: 過去走破速度  ΔNDCG@3 = +0.0118 ± 0.0009  (162.3s)
✅ done: 過去人気  ΔNDCG@3 = +0.0142 ± 0.0033  (162.6s)
✅ done: 過去着順  ΔNDCG@3 = +0.0146 ± 0.0014  (159.3s)
✅ done: 過去上り  ΔNDCG@3 = +0.0155 ± 0.0014  (161.4s)
✅ done: 履歴長_norm  ΔNDCG@3 = +0.0150 ± 0.0008  (157.4s)
✅ done: 騎手-馬コンビ勝率  ΔNDCG@3 = +0.0142 ± 0.0008  (156.7s)
✅ done: 相対斤量差  ΔNDCG@3 = +0.0148 ± 0.0009  (160.4s)
✅ done: 相対騎手勝率差  ΔNDCG@3 = +0.0156 ± 0.0004  (163.5s)
✅ done: 相対過去速度  ΔNDCG@3 = +0.0155 ± 0.0008  (164.0s)

🚀 Categorical PFI: 19 features × 5 repeats
✅ done: 競馬場  ΔNDCG@3 = +0.0149 ± 0.0013  (160.8s)
✅ done: 芝ダ  ΔNDCG@3 = +0.0126 ± 0.0000  (164.4s)
✅ done: 馬場  ΔNDCG@3 = +0.0126 ± 0.0015  (160.0s)
✅ done: 性  ΔNDCG@3 = +0.0125 ± 0.0010  (157.1s)
✅ done: 年齢層  ΔNDCG@3 = +0.0116 ± 0.0009  (162.4s)
✅ done: 距離カテゴリ  ΔNDCG@3 = +0.0125 ± 0.0014  (163.1s)
✅ done: 距離Cx芝ダ  ΔNDCG@3 = +0.0132 ± 0.0013  (163.9s)
✅ done: 天候x馬場  ΔNDCG@3 = +0.0147 ± 0.0011  (162.2s)
✅ done: 競馬場x芝ダ  ΔNDCG@3 = +0.0135 ± 0.0012  (164.0s)
✅ done: 年齢層x芝ダ  ΔNDCG@3 = +0.0143 ± 0.0010  (175.3s)
✅ done: 性x距離カテゴリ  ΔNDCG@3 = +0.0130 ± 0.0012  (161.7s)
✅ done: 血統キー  ΔNDCG@3 = +0.0146 ± 0.0018  (160.6s)
✅ done: 血統得意距離  ΔNDCG@3 = +0.0123 ± 0.0008  (157.6s)
✅ done: 血統得意脚質  ΔNDCG@3 = +0.0119 ± 0.0014  (161.3s)
✅ done: 血統得意馬場  ΔNDCG@3 = +0.0123 ± 0.0002  (160.4s)
✅ done: 血統得意芝ダ  ΔNDCG@3 = +0.0124 ± 0.0000  (161.7s)
✅ done: 脚質過去  ΔNDCG@3 = +0.0132 ± 0.0015  (159.5s)
✅ done: 騎手ID  ΔNDCG@3 = +0.0130 ± 0.0018  (160.1s)
✅ done: 調教師ID  ΔNDCG@3 = +0.0139 ± 0.0016  (166.1s)

📋 数値特徴量の影響(NDCG@3 低下量: 平均±SD):
  勝利数                  drop = +0.0165 ± 0.0028 ⬆️
  相対騎手勝率差              drop = +0.0156 ± 0.0004 ⬆️
  相対過去速度               drop = +0.0155 ± 0.0008 ⬆️
  過去上り                 drop = +0.0155 ± 0.0014 ⬆️
  履歴長_norm             drop = +0.0150 ± 0.0008 ⬆️
  相対斤量差                drop = +0.0148 ± 0.0009 ⬆️
  過去着順                 drop = +0.0146 ± 0.0014 ⬆️
  出走数                  drop = +0.0143 ± 0.0019 ⬆️
  騎手-馬コンビ勝率            drop = +0.0142 ± 0.0008 ⬆️
  過去人気                 drop = +0.0142 ± 0.0033 ⬆️
  騎手勝率                 drop = +0.0127 ± 0.0003 ⬆️
  過去走破速度               drop = +0.0118 ± 0.0009 ⬆️
  距離_z_vs_馬ma3         drop = +0.0078 ± 0.0016 ⬆️
  距離_diff_vs_馬ma5      drop = +0.0055 ± 0.0016 ⬆️
  距離_sd7_馬             drop = +0.0053 ± 0.0018 ⬆️
  距離_diff_vs_馬ma3      drop = +0.0049 ± 0.0017 ⬆️
  体重増減                 drop = +0.0046 ± 0.0011 ⬆️
  馬番                   drop = +0.0042 ± 0.0013 ⬆️
  斤量                   drop = +0.0039 ± 0.0009 ⬆️
  距離                   drop = +0.0037 ± 0.0035 ⬆️
  斤量_diff_vs_クラス_ma5   drop = +0.0034 ± 0.0007 ⬆️
  馬体重                  drop = +0.0032 ± 0.0020 ⬆️
  距離_ma5_馬             drop = +0.0032 ± 0.0017 ⬆️
  斤量_mean_クラス過去_ma7    drop = +0.0031 ± 0.0008 ⬆️
  距離_sd3_馬             drop = +0.0031 ± 0.0017 ⬆️
  馬体重_pct_prev         drop = +0.0031 ± 0.0002 ⬆️
  斤量_mean_クラス過去_ma3    drop = +0.0029 ± 0.0008 ⬆️
  距離_z_vs_馬ma5         drop = +0.0028 ± 0.0013 ⬆️
  距離_ma7_馬             drop = +0.0028 ± 0.0012 ⬆️
  斤量_diff_vs_クラス_ma7   drop = +0.0026 ± 0.0013 ⬆️
  距離_sd5_馬             drop = +0.0020 ± 0.0025 ⬆️
  距離_mean_クラス過去_ma3    drop = +0.0019 ± 0.0020 ⬆️
  距離_diff_vs_クラス_ma7   drop = +0.0019 ± 0.0043 ⬆️
  馬体重_ma5_馬            drop = +0.0017 ± 0.0014 ⬆️
  日数差                  drop = +0.0017 ± 0.0021 ⬆️
  距離_mean_クラス過去_ma5    drop = +0.0017 ± 0.0019 ⬆️
  馬体重_diff_prev        drop = +0.0016 ± 0.0011 ⬆️
  距離_ma3_馬             drop = +0.0015 ± 0.0028 ⬆️
  距離_mean_クラス過去_ma7    drop = +0.0014 ± 0.0018 ⬆️
  斤量_diff_vs_クラス_ma3   drop = +0.0013 ± 0.0018 ⬆️
  斤量_mean_クラス過去_ma5    drop = +0.0013 ± 0.0016 ⬆️
  距離_diff_vs_クラス_ma5   drop = +0.0012 ± 0.0018 ⬆️
  馬体重_diff_prev_ma3    drop = +0.0007 ± 0.0015 ⬆️
  距離_diff_vs_クラス_ma3   drop = +0.0007 ± 0.0012 ⬆️
  馬場フラグ                drop = +0.0006 ± 0.0012 ⬆️
  距離_diff_vs_馬ma7      drop = +0.0006 ± 0.0011 ⬆️
  馬体重_diff_prev_ma2    drop = +0.0006 ± 0.0020 ⬆️
  馬体重_sd5_馬            drop = +0.0003 ± 0.0012 ⬆️
  芝ダフラグ                drop = +0.0001 ± 0.0009 ⬆️
  馬体重_diff_prev_ma5    drop = +0.0000 ± 0.0005 ⬆️
  距離カテゴリフラグ            drop = -0.0000 ± 0.0008 ⬇️
  脚質フラグ                drop = -0.0003 ± 0.0008 ⬇️
  好成績                  drop = -0.0003 ± 0.0004 ⬇️
  馬体重_z_vs_馬ma5        drop = -0.0003 ± 0.0005 ⬇️
  斤量_z_vs_馬ma5         drop = -0.0004 ± 0.0014 ⬇️
  斤量_ma3_馬             drop = -0.0009 ± 0.0015 ⬇️
  斤量_z_vs_馬ma3         drop = -0.0009 ± 0.0008 ⬇️
  斤量_ma5_馬             drop = -0.0012 ± 0.0010 ⬇️
  斤量_diff_vs_馬ma7      drop = -0.0012 ± 0.0007 ⬇️
  距離_z_vs_馬ma7         drop = -0.0012 ± 0.0009 ⬇️
  斤量_diff_vs_馬ma5      drop = -0.0016 ± 0.0003 ⬇️
  斤量_diff_vs_馬ma3      drop = -0.0018 ± 0.0011 ⬇️
  斤量_ma7_馬             drop = -0.0019 ± 0.0013 ⬇️
  斤量_sd5_馬             drop = -0.0021 ± 0.0009 ⬇️
  斤量_sd3_馬             drop = -0.0021 ± 0.0012 ⬇️
  日数差_norm             drop = -0.0024 ± 0.0005 ⬇️
  斤量_z_vs_馬ma7         drop = -0.0025 ± 0.0017 ⬇️
  斤量_馬体重比              drop = -0.0027 ± 0.0001 ⬇️
  斤量_sd7_馬             drop = -0.0036 ± 0.0018 ⬇️

📋 カテゴリ特徴量の影響(NDCG@3 低下量: 平均±SD):
  競馬場                  drop = +0.0149 ± 0.0013 ⬆️
  天候x馬場                drop = +0.0147 ± 0.0011 ⬆️
  血統キー                 drop = +0.0146 ± 0.0018 ⬆️
  年齢層x芝ダ               drop = +0.0143 ± 0.0010 ⬆️
  調教師ID                drop = +0.0139 ± 0.0016 ⬆️
  競馬場x芝ダ               drop = +0.0135 ± 0.0012 ⬆️
  脚質過去                 drop = +0.0132 ± 0.0015 ⬆️
  距離Cx芝ダ               drop = +0.0132 ± 0.0013 ⬆️
  性x距離カテゴリ             drop = +0.0130 ± 0.0012 ⬆️
  騎手ID                 drop = +0.0130 ± 0.0018 ⬆️
  馬場                   drop = +0.0126 ± 0.0015 ⬆️
  芝ダ                   drop = +0.0126 ± 0.0000 ⬆️
  距離カテゴリ               drop = +0.0125 ± 0.0014 ⬆️
  性                    drop = +0.0125 ± 0.0010 ⬆️
  血統得意芝ダ               drop = +0.0124 ± 0.0000 ⬆️
  血統得意距離               drop = +0.0123 ± 0.0008 ⬆️
  血統得意馬場               drop = +0.0123 ± 0.0002 ⬆️
  血統得意脚質               drop = +0.0119 ± 0.0014 ⬆️
  年齢層                  drop = +0.0116 ± 0.0009 ⬆️

✅ PFI (NDCG@3 × 5回平均, レース内S軸シャッフル, 馬ID除外) 完了

CLS+学習窓+MoE を導入した結果どうなったか?

今回の実験では、レースCLSトークン・TemporalAttentionPool(学習窓)・MoEゲートを導入し、モデルに「どの特徴をどの程度使うか」を自動で学習させる構造に改良しました。その結果、前回のモデルと比べて明確に良くなった点と悪くなった点が見えてきました。


改善された点 ✨

1. 特徴の活用が整理された

PFIの結果から、前回は多くの特徴をシャッフルしても性能が**むしろ上がる(ΔNDCGがマイナス)**という現象が目立っていました。これは「特徴がノイズとして働いていた」ことを意味します。

今回のモデルでは逆に、多くの特徴で**シャッフルすると性能が落ちる(ΔNDCGがプラス)**方向に改善。特に以下が顕著でした:

  • 相対指標(相対騎手勝率差・相対過去速度・相対斤量差)
  • 騎手や出走数の蓄積知(騎手勝率・騎手-馬コンビ勝率・勝利数・出走数)
  • 過去成績系の要約(過去着順・過去上り・過去人気)

→ 競馬的に納得感のある特徴が効くようになり、解釈可能性が大きく向上しました。

2. 距離・斤量の扱いが洗練

  • 距離系では 短期(3〜5走)の差分や標準化指標が有効に。
  • 斤量系では固定窓の平均や分散がマイナス寄与となり、相対斤量差のほうが重要になった。

これは、CLSやMoEが「固定窓の平均に頼らず、必要な履歴や相対化を選んで使っている」ことを示唆します。


悪化した点 ⚠️

1. Baseline NDCG が低下

  • 前回: 0.9184
  • 今回: 0.7117
    と、大きく低下しています。
    ただし、このBaselineはPFI計算用の「全特徴をそのまま使った基準値」なので、学習設定や評価方法が変わった可能性もあります。

もし条件が同一なら、モデルの表現力を高めた代わりに学習が安定しきらず、基礎性能が落ちたと考えられます。


どうすればもっと良くなるか? 🚀

  1. 学習安定化の工夫
    • EarlyStoppingが早く効いてしまっているので、Learning Rate Scheduler や SWA (Stochastic Weight Averaging) を導入して「落ち込み→持ち直し」を滑らかにする。
    • Dropout率や正則化を見直して過学習を抑制。
  2. 相対特徴のさらなる拡張
    • 今回有効だった「相対騎手勝率差」「相対過去速度」を広げ、例えば「相対上り」「相対人気」「相対馬体重増減」なども導入。
  3. 窓の設計をシンプル化
    • 斤量の固定窓特徴は寄与が低いので、削減してモデルを軽量化 → 学習効率UPとノイズ低減が期待できる。
  4. カテゴリ交差の強化
    • 競馬場×芝ダ天候×馬場 が効いているので、さらに「騎手×競馬場」「調教師×距離カテゴリ」などを追加して、CLSトークンで文脈を掴みやすくする。

まとめ

  • 改善点: 特徴の活用が整理され、相対系・蓄積知・過去成績の要約がしっかり効くようになった。
  • 悪化点: Baseline NDCGが大きく低下(条件差か学習安定性の問題)。
  • 次の一手: 学習安定化+相対特徴拡張+不要特徴の削減で、今回の改良を「高精度かつ安定」に仕上げる。

コメント

タイトルとURLをコピーしました