学習する(改良4:枠の位置バイアスを素直に学習させ、順位情報を削らず、レース全体の確率モデル(Plackett–Luce)で“勝ち切る力”を直接最適化する。

バグ修正

もっと、頭を良くさせたいと思い、以下の改良を加えてみました!

  1. 枠番を復活 & レース内正規化特徴(A)
  2. 順位の切り詰めをやめる(フルランク学習)
  3. Plackett–Luce 損失(B)の追加

1) 枠番を復活 & レース内正規化特徴(A)

置き換える場所

  • セクション「# 不要列削除」の行を置き換え枠番を削除しない)。
  • その後、「✅ 特徴量セットを構築」の直後〜final_drop を定義する前追記

置き換えコード(不要列)

# ✅ 枠番は残す
data.drop(columns=["レース名", "タイプ", "着差", "賞金", "馬主ID", "左右", "クラス"], inplace=True)

追記コード(レース内での相対化)

# === 枠番のレース内相対特徴(A) ===
data["枠番"] = pd.to_numeric(data["枠番"], errors="coerce")
med_by_race = data.groupby("レースID", sort=False)["枠番"].transform("median")
data["枠番"] = data["枠番"].fillna(med_by_race).fillna(0).astype("float32")

race_grp = data.groupby("レースID", sort=False)

# 内枠ほど有利という一般的傾向を“順位化”で表現
data["枠番_rank_in_race"] = race_grp["枠番"].rank(method="dense", ascending=True).astype("float32")

# レース内標準化(平均0・分散1)。定義不能は0埋め
mu = race_grp["枠番_rank_in_race"].transform("mean")
sd = race_grp["枠番_rank_in_race"].transform("std").replace(0, np.nan)
data["枠番_norm_in_race"] = ((data["枠番_rank_in_race"] - mu) / sd).fillna(0).astype("float32")

# 平均からのズレ量(符号付き)
data["枠番_diff_from_mean"] = (data["枠番_rank_in_race"] - mu).fillna(0).astype("float32")
# =====================================

メモ:final_drop にこれら新列(枠番, 枠番_rank_in_race, 枠番_norm_in_race, 枠番_diff_from_mean)を入れないよう注意。


2) 順位の切り詰めをやめる(フルランク学習)

置き換える場所

  • セクション「# 馬ごとの時系列シーケンス化…」の for ループ中、y_list を作る2箇所置き換え
  • セクション「# train__model開始」の class_weights_tensor 作成部分置き換え(クラス数を自動追従)。

置き換えコード(時系列の y 作成)

# --- n >= S のとき ---
# 置き換え前: y_seq = np.minimum(y[idx_end], 6)
y_seq = y[idx_end]                 # ← 切り詰めない
y_list.append(y_seq - 1)           # 0始まりへ

# --- n < S のとき ---
# 置き換え前: y_list.append(np.array([min(y_last, 6) - 1], dtype=np.int64))
y_list.append(np.array([y_last - 1], dtype=np.int64))

置き換えコード(クラス重みベクトル)

# 置き換え前は [0..5] 固定長だったはず。最大クラスに自動追従へ。
class_weights_dict = compute_class_weights(y_train)
max_cls = int(y_train.max())  # 0始まりの最大クラス
class_weights_tensor = torch.tensor(
    [class_weights_dict.get(i, 1.0) for i in range(max_cls + 1)],
    dtype=torch.float32
).to("cuda" if torch.cuda.is_available() else "cpu")

メモ:PL/リストワイズ系は順位の距離情報を活かせるので、切り詰めない方が相性◎。


3) Plackett–Luce 損失(B)の追加

追加・置き換える場所

  • セクション「損失関数」群(weighted_pairwise_ranking_loss など)のすぐ下関数を追加
  • セクション「train_racewise_regression_model」内の学習ループで、PL を合算

追加コード(損失関数)

def plackett_luce_loss(scores: torch.Tensor, ranks: torch.Tensor) -> torch.Tensor:
    """
    ranks: 1(最上位) .. K(大きいほど下位)。同着は最小順位共有でOK。
    大きい scores ほど強いと仮定。上位からの逐次softmax尤度の負ログを計算。
    """
    order = torch.argsort(ranks, dim=0)  # 昇順 = 1位,2位,...
    s = scores[order]
    loss = s.new_zeros(())
    n = s.size(0)
    for t in range(n):
        denom = torch.logsumexp(s[t:], dim=0)
        loss = loss - (s[t] - denom)
    return loss / max(1, n)

置き換えコード(学習ループで合算)

学習ループの with autocast_ctx: ブロック内、1レース分ごとに PL を計算・合算します。

# 係数(目安: 0.5〜2.0 をスイープ)
lambda_top1 = 8.0
lambda_ndcg = 3.0
lambda_pl   = 1.0   # ← 追加

with autocast_ctx:
    preds = model(X_pack, cat_pack, mask=mask_pack)  # [B,S]
    valid_mask = ~mask_pack

    loss_pair_list, loss_top1_list, loss_ndcg3_list = [], [], []
    loss_pl_list = []  # ← 追加

    for r_valid, p_r, y_r, h_r in zip(
        valid_mask, preds, y_pack, hist_pack if hist_pack is not None else [None]*preds.size(0)
    ):
        if r_valid.sum() < 2:
            continue

        p_r = p_r[r_valid]
        y_r = y_r[r_valid]

        # 順位を 1..K に正規化(既存関数のままでOK)
        y_r, _ = normalize_targets_for_ranking(y_r)

        lp     = weighted_pairwise_ranking_loss(p_r, y_r)
        lt1    = soft_top1_loss(p_r, y_r, class_weights=class_weights)
        lndcg3 = hard_ndcg3_loss_torch(p_r, y_r)
        lpl    = plackett_luce_loss(p_r, y_r)  # ← 追加

        loss_pair_list.append(lp)
        loss_top1_list.append(lt1)
        loss_ndcg3_list.append(lndcg3)
        loss_pl_list.append(lpl)  # ← 追加

    if not loss_pair_list:
        continue

    # ✅ 合算に PL を追加
    loss = (
        torch.stack(loss_pair_list).mean()
        + lambda_top1 * torch.stack(loss_top1_list).mean()
        + lambda_ndcg * torch.stack(loss_ndcg3_list).mean()
        + lambda_pl   * torch.stack(loss_pl_list).mean()  # ← 追加
    )

メモ:モデル出力は「大きいほど上位」の向きで実装している想定です(既存のランキング損失と整合)。

なお、推論時には、枠番については同様に変更が必要ですが、「順位の切り詰め廃止」「PL損失」学習時の話。推論処理自体は変更不要です。

バグ修正

また、推論時コードで「天気」のところは「天候」の間違いでしたので、訂正してください。すみません。

コメント

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