Zenn
👨‍💻

Kaggle WSDM Cup 参加記録

に公開

はじめに

データアナリティクスラボ株式会社です。

今回はデータソリューション事業部のメンバーでKaggleの「WSDM Cup - Multilingual Chatbot Arena」に参加しましたので、その取り組みや上位解法についてご紹介いたします。

参加メンバー

  • 宮澤:広告業界のクライアント様とのマーケティングにかかわる分析業務、金融業外のクライアント様とのAIにかかわる研究業務に従事。業務外ではKaggleやatmaCupなどに参加。
  • 力岡:金融、製造、医療などの分野において、データ分析、モデル開発、AIコンサルティング、およびサービス開発に従事。
  • 平野:金融分野の分析を行っており、社内では生成AIの研究活動を実施。

課題について

概要

今回の課題は、Chatbot Arenaにおける2つのLLMの回答で、ユーザー(人間)がどちらを好むかを正確に予測するというタスクでした。こちらはLMSYS - Chatbot Arena Human Preference Predictionsの続編となっていますが、違いとしては多言語を含むデータであるという点になります。また、今回のコンペはWSDM 2025の併設コンペとなっています。

データ

コンペではtrainデータセットが与えられていました。形式は非常にシンプルです。

  • prompt :ユーザー(人間)の質問や指示のテキスト。
  • response_[a/b] :2つのLLMの回答テキスト。
  • winner :どちらのLLMの回答を好むか。
  • model_[a/b] :responseを生成したLLMのモデル名。
  • language :言語情報。


https://www.kaggle.com/competitions/wsdm-cup-multilingual-chatbot-arena/data

評価指標

単純なAccuracyが評価指標でした。LMSYSというコンペの時のlog lossとは違った指標となっています。

制約

テストデータのリークを防ぐため、2月初旬までが提出期間であり、その後3月までかけて実際に収集されたデータで予測を行い、最終的な順位が決まります。

また、Notebookコンペであったため時間の制約もあります。GPUの場合は提出フェーズでは約10,000件のデータを4.75時間以内に処理を完了させ、テストフェーズでは約25,000件(以下)のデータを12時間以内に処理を完了させる必要があります。

結果

35位/950チームで銀メダルを獲得しました!

取り組み

今回はチームメンバーでそれぞれモデルを学習させ、終盤にアンサンブルする形で進めました。以下では各自取り組んだ学習や手法について紹介します。

Qwen2.5-14B-Instruct(宮澤)

宮澤は、Qwen2.5モデルを使って学習をしました。Qwenを採用した理由は、他のLLMコンペでもよく使われており、汎用的な性能が高いことはもちろん、各種ライブラリでのサポートもされており使いやすいことが挙げられます。

データについて

コンペで与えられていたtrain.parquet を使用しました。train, valid, testに分割しましたが、その際に回答LLMと言語情報の分布ができるだけ等しくなることが望ましいと考え、model_a + language で文字列を作成して、これをstratify_keyとして層化分割しました。

モデルサイズの検証

まずはモデルサイズと精度は相関するということを仮定して、できるだけ大きなモデルを使うことを試みました。検証したのはQwen/Qwen2.5-14B-Instruct-AWQQwen/Qwen2.5-32B-Instruct-GPTQ-Int4 です。可能であれば32Bを使った方がよいだろうと考えていました。検証およびサブミット時の推論には全てvllmを用いました。

結果として、トークン長の上限を4,096としたときに32Bモデルは精度は高いと思われましたが、制限時間内に処理が終わらずタイムアウトエラーになったためこの時点では不採用としました。

Model Local test score LB score
Qwen2.5-14B-Instruct-AWQ 0.5966 0.601
Qwen2.5-32B-Instruct-GPTQ-Int4 0.6255 Time Out

プロンプトエンジニアリング

まずはベースラインとして追加学習のない状態でのスクリプトを作成しました。その際にプロンプトエンジニアリングによる精度向上もできるだろうと考えていたため、いくつか検証をしてみました。イメージですがプロンプトは以下のように与えました。

あなたの役割はユーザーからの質問と2つのLLMの回答を見て、よりよいと思われる回答モデルを判定することです。

質問:{prompt}

モデルAの回答:{response_a}

モデルBの回答:{response_b}

どちらがよりよい回答でしょうか?

改良バージョンではこれに対して「〇〇という観点で判定しなさい」といった箇条書きでの評価項目など、詳細な指示プロンプトを追加しています。 結果として、以下のようにスコア向上が見られました。

Model Local test score LB score
Version 1 0.5966 0.601
Version 2 0.6093 0.605
Version 3 0.6139 0.614

量子化の検証

Qwen2.5では公式からAWQ量子化モデルが公開されていますが、前回参加したコンペの解法ではauto-roundを用いた時の精度が高いことが述べられていたため、こちらも検証しました。Qwen/Qwen2.5-14B-Instruct-AWQQwen/Qwen2.5-14B-Instruct をauto-roundで4bit量子化したモデルとの比較をしています。結果として、auto-roundを使ったモデルでの精度向上が見られたため、こちらを採用することとしました。auto-roundではgptqとawqを選択できますが、awqではgptqと比較して推論時間が長かったためgptqを採用しました。

Model Local test score LB score
AWQ 0.6139 0.614
auto-round (gptq) 0.6185 0.622

ファインチューニング

追加学習なしではこれ以上のスコアの向上が難しかったため、ファインチューニングを実施しました。基本的にはWSDM Cupのコンペデータとして付与されていた学習データ(分割したもの)を使用しました。他に使用できそうなデータとしてはlmsys/chatbot_arena_conversations があったため、ここからマルチターンデータを除き、winnertieであるデータ(引き分けデータ)を除き、WSDMデータと重複を削除してNo. 3のデータを作りました。

1, 2の学習推移を見ていると、validationのlossとaccuracyが僅かですが最後まで改善傾向にあったため、学習量を増加することでまだ改善の余地があると考えました。そのため以下表のNo.3では(コンペ終盤であったこともあり)分割データのうちtrain, testを全て学習データに含めてしまい、さらにresponse_[a/b](およびwinner)を入れ替えたデータを追加して全体を2倍のデータ量に増やして学習を行いました。

結果としてはデータ量を増強させたNo.3の精度が最も高い結果となりました。なお、モデルは全てQwen/Qwen2.5-14B-Instruct をLoRAで学習し、その後auto-roundで4bit量子化を行いました。

No Data Local test score LB score
1 WSDMデータセット train 0.6942 0.688
2 LMSYSデータセット 0.6809 0.675
3 WSDMデータセット train test, LMSYSデータセット - 0.692

学習はLoRAを用いて行なっています。学習設定は全て同じものを使いました。トークンの最大長が長かったためメモリエラーに苦しみつつ、やむなくバッチサイズを1にしました。

  • lora_rank = 32
  • lora_alpha = 64
  • num_train_epochs=1
  • per_device_train_batch_size=1
  • gradient_accumulation_steps=16
  • learning_rate=5e-5

実装上で工夫した点としては、validationの際にlossだけではなくaccuracyを計算するようにcompute_metricsをカスタムして評価の推移を監視していたことです。 CausalLMモデルにおいては予測されるトークンと正解のトークンが一致しているかどうかで正解率を計算することになります。しかし、普通にlogitを取得して最大値を持つトークンをデコードしようとすると、全時点数 * 語彙数分のlogit を取得することになり、メモリが圧迫されてエラーになってしまいました。そこで、preprocess_logits_for_metrics を使って各時点においてlogitsが最大のトークンだけを取得するようにしてことでメモリ使用を大幅に削減でき、エラーなくaccuracyの計算ができました。また、詳細な理由はわかりませんがlabels(教師ラベル)のトークンの位置とpreds(予測ラベル)のトークンの位置では、非マスクつまり損失計算したいトークンの位置が1つずれていることがわかりました。そのため、以下の処理ではマスクを1つ分ずらす処理をしています。(おそらく文頭トークンなどの処理によるもの?)

def preprocess_logits_for_metrics(logits, labels):
    """
    モデルの出力logitsから、次元を縮小して予測トークンを抽出する。
    """
    if isinstance(logits, tuple):
        logits = logits[0]  # tupleの場合は最初の要素を使用
    return logits.argmax(dim=-1)  # 予測トークンを取得
def compute_metrics(eval_preds):
    """
    メトリクス(accuracy)を計算するための関数。
    """

    preds, labels = eval_preds

    if isinstance(preds, tuple):
        preds = preds[0]  # tupleの場合は最初の要素を使用

    # -100 を無視し、有効なラベルの位置を取得
    valid_labels_mask = labels != -100
    valid_labels = labels[valid_labels_mask]

    # valid_labels_mask の一つ前の位置を取得(predの方が位置が一つずれているため)
    shifted_mask = np.roll(valid_labels_mask, shift=-1, axis=1)
    shifted_mask[:, -1] = False  # 最後の列をマスクする
    aligned_preds = preds[shifted_mask]

    # processor.decode を使用して全データをデコード
    decoded_preds = [processor.decode([pred], skip_special_tokens=True) for pred in aligned_preds]
    decoded_labels = [processor.decode([label], skip_special_tokens=True) for label in valid_labels]

    # メトリクスの計算(全データを使用)
    accuracy = np.mean([pred.strip() == label.strip() for pred, label in zip(decoded_preds, decoded_labels)])
    return {"accuracy": round(accuracy, 4)}

また、補足として、今回は分類タスクでしたが(ClassificationのHeadを追加して学習するという発想がなく)CausalLMモデルとして普通にトークン予測をするように学習していました。これが原因かは定かではないですが、vllmのMultipleChoiceLogitsProcessorを使って予測トークンの対数確率を取得すると、決まった値しか取らない(離散的な値になってしまう)という事象が発生していました。

トークン切り詰め方法

データによってユーザーの質問や回答の長さにばらつきがあり、非常に長いものも含まれていたため、処理時間の制約を考えると、こうした長いテキストは何かしらの方法で切り詰める必要がありました。実際にprompt + response_a + response_b の文字列に対して Qwen2.5のトークナイザーでトークン数をカウントした場合、以下のような分布になりました。これを見ると、多くは2,500以下程度ですが、右側に裾の長い分布であるため、それよりもはるかに長いテキストが存在することがわかります。

これを踏まえて考えるべきこととしては、モデルの処理する上限トークン数をいくつに設定するかということです。vllmではmax_model_len でそれを設定します。この分布を見る限り、指示プロンプトのことも考えると3,000以上程度にしておくとデータのうちの大部分を切り詰めずに処理できると考えられます。 とはいえ一部でトークン切り詰めが必要であるため、その切り詰め方法を考えました。

この処理方法はあまり検証する時間がなかったため、以下のルールで決めてしまいました。手法はLost In the Middleから着想を得ています。

  • 上限トークン数を超える場合はresponse_[a/b]の中央から等しいトークン数を切り詰める。
  • ただしreponseが過度に切り取られることは好ましくないため、80%は残すものとする。
  • 必要な残り切り詰め数はpromptの中央から切り詰めるものとする。

この切り詰め方法を使って上限を2,048 / 4,096 / 8,192にした時の結果が以下です。学習データの分布から確認されたように、2,048まで切り詰めてしまうと多くのデータから情報が失われてスコアが下がることがわかります。

Tokens Local test score LB score
max = 2,048 - 0.685
max = 4,096 - 0.691
max = 8,192 - 0.692

ただし、個人的に危惧していたこととしては、昨今の推論スケーリングの傾向から、2月から3月にかけて取得されるテストデータのresponseの平均トークン長が、それ以前に取得されているものよりも長くなるのではないかということでした。ここに対処する術と時間がなかったため、このような不安を抱えながらも上記の手法で上限4,096トークンとしたままサブミットしました。

Gemma-2-9B-it(力岡)

力岡は、gemma2-9b-itを利用して学習を行いました。具体的には4bit量子化版の、unsloth/gemma-2-9b-it-bnb-4bitを利用しました。gemma2を採用した理由は、前回のコンペ(LMSYS)において上位陣の採用率が高く、少ない計算リソースで高精度かつ高速に動作するためです。

学習環境

計算環境には、Google Colab ProのA100 GPUを使用しました。提出モデルの学習に最終的に要した時間は約80時間で、金額に換算すると約1万円がかかる計算となります。

ファインチューニング

ファインチューニングは、①LMSYSコンペの学習データ、②LMSYSで使用されていた追加データ、③WSDMコンペの学習データの順番で学習を行いました。LLMを分類問題として扱うために、Gemma2ForSequenceClassificationを利用し、勝者がmodel_amodel_bのどちらであるかの確率値を出力するように学習しました。

学習プロンプト

以下の形式のプロンプトを使用しました。こちらについては、特に工夫などは行っていません。

<prompt>: {prompt}

<response_a>: {response_a}

<response_b>: {response_b}

LMSYSデータの処理と学習

LMSYSデータには、model_amodel_bのどちらかが勝者となるラベルに加えて、引き分け(Tie)のラベルも含まれていました。しかし、実験の結果、引き分けのデータを含めるとテストスコアがわずかに低下する傾向が見られたため、最終的に引き分けのデータをすべて除外し、勝敗が明確なデータのみを学習に用いることにしました。ここでは、約6万件のデータを用いて学習を行われています。

WSDMデータの処理と学習

WSDMコンペの学習データに対しては、データ拡張とアンサンブル手法を適用することで、モデルの精度向上を図りました。ここでは、約5万件のデータで学習が行われています。

1. model_amodel_bresponseを入れ替えて2回学習

ファインチューニングにおけるプロンプト形式では、単純にmodel_amodel_bの出力結果を直列に学習させているため、回答の順序や回答パターンに偏る可能性があります。そのため、データを拡張する形でabを入れ替えたバージョンも作成し、合計2回学習を行いました。結果として、精度には大きな変化は見られませんでしたが、過学習の兆候もなかったため、この方法を採用しました。

2. 4-fold分割によるLoRAアダプタのアンサンブル

WSDM データを使用する際、4-foldの分割を行い、それぞれのfoldでLoRAアダプタを学習しました。最終的には、4つの異なるLoRAアダプタを加重平均でアンサンブルすることで、単一モデルよりも安定した性能を目指しました。加重平均の計算には、以下のコードを利用しています。この手法も大幅な精度向上は見られませんでしたが、汎化性能の向上を期待して採用しました。

d1 = load_file("gemma-2-9b-it-bnb-4bit_lora_exp019-fold0/lora/adapter_model.safetensors")
d2 = load_file("gemma-2-9b-it-bnb-4bit_lora_exp019-fold1/lora/adapter_model.safetensors")
d3 = load_file("gemma-2-9b-it-bnb-4bit_lora_exp019-fold2/lora/adapter_model.safetensors")
d4 = load_file("gemma-2-9b-it-bnb-4bit_lora_exp019-fold3/lora/adapter_model.safetensors")

d = {}
for k, v in d1.items():
    v = d1[k] + d2[k] + d3[k] + d4[k]
    v = v / 4.
    d[k] = v

save_file(d, "lora_merge/gemma-2-9b-it-bnb-4bit_lora_exp019/adapter_model.safetensors")

各学習段階におけるテストスコアとPublic LBの関係性は以下の通りです。

学習段階 テストスコア Public LB
LMSYSコンペの学習データ 0.6524 -
LMSYSで使用されていた追加データ 0.6507 -
WSDMコンペの学習データ 0.6910, 0.6927, 0.6890, 0.6939 0.685
WSDMコンペの学習データ(response入れ替え) 0.6939, 0.6927, 0.6916, 0.6921 0.688

トークン切り詰め処理

上記の宮澤の処理と同様で中央の文章を切り落とす処理を行いました。promptの最大token数を402、responseの最大token数を702、max_token_lengthを1,900として、トークンの切り詰めを実施しました。

損失関数の変更

損失関数を変更することで精度が向上するかを確認するために、損失関数をCrossEntropyLossBCEWithLogitsLossに変えて試しました。結果として、大きな精度変化は見られなかったため、最終的にデフォルト設定であるCrossEntropyLossを用いて学習を行いました。

今回の実装にはあまり関係ないのですが、損失関数の設定で注意すべき点として、SequenceClassificationで確率値を学習、出力するためにnum_labels=1を設定すると、以下のGitHubリンクにあるように、デフォルトの損失関数がMSELossになります。

https://github.com/huggingface/transformers/blob/7ae6f070044b0171a71f3269613bf02fd9fca6f2/src/transformers/models/bert/modeling_bert.py#L1564-L1575

MSELossは回帰問題向けの損失関数であり、分類問題にはあまり適していません。そのため、独自にCustomTrainerクラスを作成し、CrossEntropyLossBCEWithLogitsLossを適用する必要があります。以下にその実装例を示します。

class CustomTrainer(Trainer):
    def compute_loss(self, model, inputs, return_outputs=False):
        labels = inputs.get("labels")
        outputs = model(**inputs)
        logits = outputs.logits

        loss_fct = BCEWithLogitsLoss()
        loss = loss_fct(logits.view(-1), labels.view(-1))
        return (loss, outputs) if return_outputs else loss

分類ヘッドの初期化

LMSYSの2thの解法に記載があるのですが、XXXForSequenceClassificationヘッドの初期化では初期の反復で大きな損失が発生するため、Customのヘッドで再初期化すると良いとありました。詳細の理由は不明ですが、恐らく事前学習済みモデルの初期値 or ランダム設定の初期値では学習が不安定になるのではないかと考えています。

model = Gemma2ForSequenceClassification.from_pretrained(
    CFG.model_name,
    num_labels=2,
    torch_dtype=torch.bfloat16,
    device_map="auto",
)
hdim = model.config.hidden_size
model.score = torch.nn.Sequential(
    torch.nn.Dropout(0.1),
    torch.nn.Linear(hdim, hdim // 2),
    torch.nn.Dropout(0.1),
    torch.nn.GELU(),
    torch.nn.Linear(hdim // 2, 2),
).bfloat16().to(CFG.device)

実際に分類ヘッド以外の条件を変えずに同じデータで学習させた場合、以下のように初期値や最終値のLossも低くなることが分かりました。(exp009:HEAD適用前、exp016:HEAD適用後)

試したけどうまくいかなかったこと

  • 蒸留のためのQwen2.5-32Bファインチューニング

    • Qwen2.5-32BやQwen2.5-72Bのファインチューニングを試みましたが、Lossがなかなか下がらず、最終的に断念しました。CausalLM形式での学習ではある程度うまくいく傾向も見られましたが、計算リソースの制約もあり、途中で中断しました。
  • gemma2-9b-itのファインチューニング

    • gemma2-9b-itについても、量子化前のモデルを用いたファインチューニングを試みましたが、こちらも同様にLossの改善が見られず、断念しました。量子化の前後で学習挙動に大きな差はないと考えていましたが、原因の特定ができなかったので、中断しました。

機械学習モデル(平野)

上記2つのLLMモデルがベンチマークとして高いスコアを記録している一方で、比較的少ない特徴量と処理時間で最低限のスコアを挙げている非LM系の機械学習モデルを使用した手法が公開されていたため、非LM系の機械学習モデルも組み合わせられないかと調査しました。(※最終的に優位な使い方を見つけられませんでした。)

モデル

公開ノートブックとして、決定木・XGBoost・LightGBM系のモデルを利用した解法がLBスコア0.6近辺を記録していたため、処理時間やスコアをおよびLLMモデルの予測結果と補完度合い(後述)を考慮して今回はXGBoostを使用しました。

非LM系モデル VS LLMモデルの予測結果対応

LLM(Gemma)モデルの予測結果正誤と非LM(XGBoost)モデルの予測結果正誤でクロス集計を行うことで、LLMモデルと非LMモデルの予測互換関係を把握しました。

特化モデルの作成

①LLM系が苦手としているデータ(=LLMの推論スコアが0や1から遠い0.5付近のデータ)のみで学習させたモデル

②LLMの予測正誤0/1をラベルとして(LLMが間違えてしまうデータの傾向を)非LMモデルで学習させたモデル

⇒①②ともにCV上昇に効果なし

アンサンブルモデルとして部分的に利用

LLMモデルの苦手なデータ(予測値が0.5付近)のデータを対象に上記アンサンブルの最終スコアが0や1に近い(0.3以下or0.7以上)のデータのみアンサンブルモデルのスコアを採用しました。

⇒こちらもCV上昇せず

アンサンブル

重みづけアンサンブル

最終的に上記で作成したQwen14BとGemma9Bの2つをベースモデルとして重みづけアンサンブルを行いました。

処理時間の都合上、片方のモデル①で100%のデータを予測し、もう片方のモデル②ではモデル①では曖昧な予測となったスコア0.5付近のX%のデータを予測し、2つのスコアを重みづけしてアンサンブルを行いました。

モデル① モデル② 推論割合① 推論割合② スコア重み① スコア重み② LBスコア
Gemma9B Qwen14B 100% 50% 0.5 0.5 0.697
Gemma9B Qwen14B 100% 50% 0.3 0.7 0.696
Qwen14B Gemma9B 100% 50% 0.3 0.7 0.701
Qwen14B Gemma9B 100% 35% 0.7 0.3 0.698
Qwen14B Gemma9B 100% 35% 0.5 0.5 0.699
Qwen14B Gemma9B 100% 35% 0.3 0.7 0.701
Qwen14B Gemma9B 100% 40% 0.3 0.7 0.701

最終的に、最下部の「Qwen14Bですべて予測したのち、予測確率が0.5付近の40%のデータをGemma9Bで推論して3:7で加重平均」というアンサンブルがPublic LBで最もスコアが高かったため、こちらを提出しました。

動的重みづけアンサンブル

Qwen14Bモデルは0.5から離れる(0に近いもしくは1に近い)とGemma9Bをやや上回る精度であることがわかっていました。そこで、Qwen14Bの予測確率が0.5に近い部分ではGemma9Bの予測を重視するという重みづけを動的に行うことを試みました。説明は割愛しますが、実装としては以下のように、それぞれのモデルの重みをどれくらいにするかは決定的に制約を付けたうえで、その範囲で加重平均を行うようにしていました。

# qwen のスコアの最大値と最小値を求める
qwen_max = result_df["qwen_score"].max()
qwen_min = result_df["qwen_score"].min()

base = 0.5

# 正方向と負方向のズレのどちらが大きいかで分母を決定
denom = max(qwen_max - base, base - qwen_min)
# 分母がゼロにならないようにチェック
if denom == 0:
    denom = 1

def weighted_score(row, base=base, denom=denom):
    gemma = row["gemma_score"]
    qwen  = row["qwen_score"]

    if pd.notna(gemma) and pd.notna(qwen):
        # 基準値からのズレの大きさを正規化する
        delta = abs(qwen - base) / denom
        # delta の上限を 1 にクランプする
        delta = min(delta, 1)
        
        # delta = 0 のときは qwen:gemma = 0:1、delta = 1 のときは qwen:gemma = 0.6:0.4 とする
        w_qwen = delta * 0.6    # qwen の重み(0〜0.6の間)
        w_gemma = 1 - w_qwen    # gemma の重み(1〜0.4の間)
        
        return gemma * w_gemma + qwen * w_qwen

    elif pd.notna(gemma):
        return gemma

    elif pd.notna(qwen):
        return qwen

    else:
        return None

# DataFrame に対して関数を適用して final_score 列を作成
result_df["final_score"] = result_df.apply(weighted_score, axis=1)
result_df.head()

上位解法

2nd 3rd 4th 5th(1) 5th(2) 5th(3) 6th 7th

利用モデル

上位にランクインしたモデルの組み合わせは以下の通りです。GemmaやQwenをベースに予測するチームが多かったです。また、ほとんどのチームが複数モデルのアンサンブルを活用していました。

順位 ベースモデル
2nd Gemma2-9B, ArmoRM-Llama3-8B-v0.1
3rd Qwen2.5-14B-Instruct, Phi4
4th Qwen2.5
5th Qwen2.5-14B-Instruct
6th Gemma2-9B, Qwen2.5-14B
7th Gemma2-9B, Qwen2.5-14B-Instruct, Qwen2.5-32B-Instruct, Mistral-Small-24B-Instruct-2501

手法や工夫

プロンプト切り捨て

プロンプトが長いと推論時間に間に合わないため、一部を省略することで効率的に処理を行う。

  • 5th: 中間部分を切り捨てて省略記号に置き換え

疑似ラベリング(Pseudo Labeling)

疑似ラベリングとは、学習済みのモデルを用いて未ラベルのデータに対して推論を行い、確度の高いものをラベル付きデータとして再利用する手法。

  • 2nd,5th,6th,7th: LMSYS3位のモデルが生成したデータを活用し、APIで応答を生成。

蒸留(Knowledge Distillation)

知識蒸留は、大規模な教師モデルの知識を、小規模な生徒モデルに伝達する手法で、モデル圧縮や効率化のために利用されます。基本的には70B級のモデルで学習させた結果を用いて、7B級のモデルを学習させるなどが行われます。

  • 3rd:Qwen2.5-14B のlogitを使用した自己蒸留でQwen2.5-72B の蒸留と同等のCV達成。
  • 4th: Athene-v2-chat + nvidia/Llama-3.1-Nemotron-70B-Instruct-HF + Qwen2.5-72B-Instructを微調整データセットで個別にトレーニングし、ソフトラベルを生成。
  • 6th: Llama3.3-70B + Qwen2.5-72BをWSDM+LMSYSで学習させて、 Gemma2-9Bおよび Qwen2.5-14Bに蒸留。損失関数は、KL ダイバージェンス損失、クロスエントロピー損失、および 2つの損失の均等加重平均を使用。

推論モデルの利用

  • 3rd, 4th, 5th: AutoModelForSequenceClassificationは vLLM で利用できないため、AutoModelForCasualLMを活用。特定のトークン(例: model_a, model_b)のロジットを取得して出力を決定。

TTA、アンサンブル

response_a, reponse_bの順番を変えて、元のモデルの推論確率と平均をとったり、複数のモデルを組み合わせて精度を向上させる。

  • 2nd: Gemma2-9BでPABを、ArmoRM-Llama3-8B-v0.1でPBAを予測し、2.5:1の比率で重み付け
  • 3rd: Qwen2.5-14B-Instructで一部をTTA、Phi4で短文を重点処理し、結果をアンサンブル
  • 7th: Gemma2-9Bで全体を採点し、不確実な上位 15%はQwen2.5-32B-Instruct(提出物1), Mistral-Small-24B-Instruct-2501(提出物2)を利用。次の 35%はQwen2.5-14B-Instructで予測し、アンサンブル。

言語予測

プロンプトの言語を推測することで適切な処理を実現する。

  • 3rd: fasttextを利用してプロンプトの言語を推測。

おわりに

今回はシンプルな課題でしたが、人間の曖昧な評価を予測することや多言語であるため読み取りができないという点に難しさがありました。モデルごとにメンバーで役割を分担して最終的にアンサンブルする取り組みは効果があったと思います。上位解法を見ると、膨大な計算リソースを必要とする蒸留だけではなく、自己蒸留によるノイズラベルのクリーニングが効果的だったことがわかりました。これはData Centricなアプローチであると捉えられ、実際の業務にも役立てられる考え方であると思いました。
今回銀メダルを獲得することができたため、次は金メダルを目指して頑張りたいと思います!

DAL Tech Blog

Discussion

ログインするとコメントできます