もっと、頭を良くさせたいと思い、以下の改良を加えてみました!
- 枠番を復活 & レース内正規化特徴(A)
- 順位の切り詰めをやめる(フルランク学習)
- 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損失」は学習時の話。推論処理自体は変更不要です。
バグ修正
また、推論時コードで「天気」のところは「天候」の間違いでしたので、訂正してください。すみません。


コメント