📖

【コンペ体験記】松尾研究所 社内コンペ「DS Dojo」で1位と2位を取った話

2024/12/04に公開

はじめに

初めまして、株式会社松尾研究所でインターンをさせていただいている佐藤大地と申します。
本記事は、松尾研究所 Advent Calendar 2024の記事です。

松尾研究所内で開催された社内コンペ(DS Dojo)に参加し、第1回では2位を、第2回では1位を取れたので、その解法や体験記のようなものをまとめて1つの記事にしてみました。

DS Dojoとは

DS Dojoとは、今回私が参加した社内コンペの名称です。社外の方でも条件を満たせば参加できるのですが、簡単のために社内コンペだと考えていただきたいです。第1回は2024年4月、第2回は2024年8月に開催され、私は両方参加しました。DS Dojoの位置付けについては、第1回で1位を取られたからあげさんの記事に書かれているので、そちらをご覧いただきたいです。

https://zenn.dev/mkj/articles/03a188902ea4ea

それでは早速各コンペの具体的な話に移ります。

DS Dojo #1

概要

コンペのタスクは 「3種類のLLMが生成した文章から、LLMの種類を当てること」 でした。ですので基本的にはテキスト分類を行います。
タスクについて詳しく説明しますと、とあるプロンプトを

  • 0: GPT-3.5
  • 1: Claude-Haiku
  • 2: Gemini

の3種類のLLMにそれぞれ入力して生成された文章から、それがどのLLMから生成されたものなのかをLLMのラベル(0, 1, 2)で予測するタスクでした。

評価指標はマクロF1-Scoreです。

私は中盤までソロ参加していましたが、途中で知人とチームを組みました。

データ

参加者には学習データ(train.csv)とテストデータ(test.csv)が与えられました。csvのカラムとその説明は以下の通りです。

カラム名 説明
id 一意な識別子
prompt モデルへ与えたテキスト
text モデルから生成されたテキスト
label 正解ラベル(学習データのみ存在)

解法

解法の大筋は、テキスト分類でよく用いられる、deberta-v3-largeとLightGBMのスタッキングです。1段目としてdeberta-v3-largeのFine-tuningを行い、その予測値やメタデータを2段目のLightGBMに入力して学習するという構成です。

詳細な解法についてはこれから詳しく説明します。

1段目

deberta-v3-largeにはtextカラムのみを入力しました。promptカラムの情報を使うという選択肢もありますが、prompt自体はLLMの出力そのものではないため、そこまで有益な情報ではないと判断し、textカラムだけを入力しました。

これだけでもLBスコアは0.835とそこそこ高かったです。

2段目

LightGBMへの入力は以下の通りです。思いつく限りで割と簡単に実装できる情報を入れまくったという感じです。

  1. deberta-v3-largeの出力値
  2. textカラムの文字数
  3. promptカラムの文字数
  4. \frac{\text{textカラムの文字数}}{\text{promptカラムの文字数}}
  5. textカラムの単語数
  6. promptカラムの単語数
  7. \frac{\text{textカラムの単語数}}{\text{promptカラムの単語数}}
  8. textカラムの文章数
  9. promptカラムの文章数
  10. \frac{\text{textカラムの文章数}}{\text{promptカラムの文章数}}
  11. 同一promptの中でのtextカラムの文字数の最大値,平均値,最小値,標準偏差
  12. 同一promptの中でのtextカラムの単語数の最大値,平均値,最小値,標準偏差
  13. 同一promptの中でのtextカラムの文章数の最大値,平均値,最小値,標準偏差
  14. TF-IDF(TfidfVectorizer)で生成した文章ベクトル(ngramの範囲: 1~4)

コンペの穴を突くような形にはなりますが、train.csvとtest.csvを観察すると、とある1つのpromptに対して3種類のLLMの出力が必ず揃っていたため、同じpromptに対してそれぞれのLLMがどういう出力をするのかを比較できる状態のデータとなっていました。そのため、4, 7, 10, 11, 12, 13の特徴量を作成することで、LLMが長い文章を生成しがちなのか、それとも短い文書を生成しがちなのかを比較できるようにしました。

14については、何かしら文章ベクトルを入れてみたいなと思い作成しました。DNNを用いた文章の意味理解は1段目のdebertaである程度済んでいると思い、ここでは単語ベースの文章ベクトルが得られるTF-IDFを用いました。ここで作成するngramの範囲は1~4が一番ベストでした。これ以上ngramの範囲を大きくすると過学習しました。

他の文章ベクトル手法は試しておらず、RAGでよく聞くBM25やsentence-transformerを使ってみるのもありかなと思います。

後処理

一番スコアに効いたのは、とある後処理でした。

上の方でも述べましたが、今回与えられたデータでは、とあるpromptに対し、必ず3種類のLLMの出力が揃っていることが特徴でした。

ですので、通常のテキスト分類モデルの予測値をそのまま採用すると、同じpromptに対するLLMの出力A, B, Cに対して、

  • AはGeminiの出力
  • BもGeminiの出力
  • CはClaudeの出力

というように予測値の被りが現れるという問題がありました(イメージは下画像を参照)。本来は、A, B, Cに対する予測では、3種類のLLMが1回ずつ登場する必要があります。

そのため、そのような予測値の被りが起きないような後処理を適用することでスコアを一気に上げることができました。

具体的には、もし予測値の被りが起きたら、予測値が被ったサンプルの中で、全く予測されていない種類のLLMに対する確率値(確信度)が最も高いサンプルに対して、全く予測されていないLLMのラベルを予測値として与えました。文章だと伝わりづらいので、下の画像をご覧いただければご理解いただけるかと思います。

このテクニックは、学習データとテストデータがまとまって与えられているからこそ可能な、コンペの穴を突くような手法であることは理解していますが、モデルの予測値を観察することの重要性を認識できるよい経験でした。1サンプルだけが与えられ、それに対して予測するとなるとこの手法は使えません。このアイデアはチームメンバーが気づいてくれたものなのでとても感謝しています。

結果

Public LBでは割と余裕で1位だったのですが...

Shake Downして2位になってしまいました。しかも僅差で負けたので余計に悔しいです。

Shake Downした原因は2つあります。

1つ目は「Trust Your CV」を守りきれなかったことです。

当初私が個人で参加していたときは、スタッキングの1段目と2段目で同じデータ分割方法を用いてcv=5の交差検証を実装していたのですが、チームマージをして、他メンバーのコードを融合するときに、どうしても分割方法を揃えて実装するのが時間的に難しく、そのため多少リークしていてCVスコアが1段目までしか算出できないコードを書いてしまいました。そのため、後処理まで終えた最終的なCVスコアを出せず、コンペ終盤はLBスコアを見てsubmissionを選んでしまいました。

CVスコアも最後まで計算し、リークもない理想的なNotebookも書いてはあったのですが、それはLBスコアが悪く、CVスコアとLBスコアの相関が取れなかったため、そのNotebookを活用する選択はできませんでした。

Trust Your CVという言葉はよく聞きますし、重々承知していたつもりではあったのですが、甘えがしっかりとShake Downという結果に繋がったので、これは大きな反省点です。

Shake Downしたもう1つの原因は、submissionの選択に多様性を持たせていなかったことです。
このコンペでは、最終サブは2個選択できました。

よくある最終サブの選び方は

  • CVスコアが最も良い堅実なサブ
  • CVスコアは良くないけれども、LBスコアがとてもよいチャレンジングなサブ

の2つを選択し、サブに多様性を持たせて保険をかけるという戦略だと思います。

しかし、弊チームはほぼ同じ解法のsubmissionを2つ選んでしまい、Private LBでは両方とも似たような低いスコアを取ってしまいました。

負け惜しみではあるんですが、CVスコアも最後まで計算し、リークもない理想的なNotebookを提出できていれば優勝できていたので、それをsubmissionのうちの1つに入れておけば良かったなと後悔しています。

参考までに、1位のチームの解法は下2つの記事で紹介されています。1位のチームはデータドリブンなアプローチを取られているのでとても理想的だと感じました。私みたいにとりあえずモデルにデータをぶち込んでしまおうという考えはよくないですね...反省です。
1位はLightGBMだけを使った解法であり、NLPコンペなのにDNNを使っていなくても高いスコアを達成できるという点が驚きでした。もちろんLightGBMだけの解法がどれだけ有効なのかはタスクによりますが、LightGBMだけでも戦えるようなNLPのタスクがあるということを知ることができたのはとても良かったです。

https://zenn.dev/mkj/articles/fe662c205e4158

https://zenn.dev/mkj/articles/03a188902ea4ea

DS Dojo # 2

前回の負けが悔しかったので、4ヶ月後に行われた、第2回のDS Dojoにソロで参加しました。その時期はあまり時間が取れなかったため、ソロで空き時間にコンペに取り組むというスタイルで参加しました。

概要

コンペのタスクは、 「Spotifyのデータから、その曲をコンペホストがお気に入り曲に登録しているかを予測する」 というものでした。

コンペあるあるの話として、実はネットのどこかにテストデータの正解データが存在していて、それがコンペ開催中に明らかになり、コンペホストがデータを作り直すというケースがあると思います。しかし、今回のタスクでは、コンペホストのスマホを盗まない限り正解データを知ることは絶対できず、そういう点で安全なコンペ設計だなと思いました。

与えられたデータは基本的にテーブルデータ(ただし一部系列データを含む)で、それを用いて0か1のラベル(target)を予測する2値分類を解きます。

評価指標はPR-AUC(Average Precision Score)です。

データ

Spotifyのお気に入りリストから作成されたデータが与えられました。そこにはさまざまな曲の情報が入っていますが、コンペホストのお気に入りリストに入っている曲のアーティストから、アーティストの全楽曲情報を取得してデータが作成されました。そのため、コンペホストがお気に入りリストに入れていないアーティストの曲は全く与えられません。また、target0の中には、聴いていない楽曲も含まれていないということにもなります。つまり、target0であるような曲の中には、「聴いてみたけどお気に入り登録しなかった曲」と「一度も聴いたことがないのでお気に入り登録していない曲」が混ざっていることになります。このような条件下で作成された学習データとテストデータが与えられました。

与えられたデータの詳細について以下説明します。

train.csv/test.csv

カラム名 説明
album_name 対象trackが収録されているアルバム名
artists 対象trackのアーティスト名
album_release_date 対象trackが収録されているアルバムが発売された年月日
album_total_tracks 対象trackが収録されているアルバムに収録されている曲数
album_type album or single or compilation
available_markets spotifyで公開されている国数
duration_s 対象trackの長さ
track_id 対象trackのID。music_feature内のcsvと紐づいている。
track_name 対象trackの名前
track_popularity 対象trackの人気度。0-100の整数。
music_danceability テンポ、リズムの安定性、ビートの強さなどの組み合わせによって踊りやすいかどうかの指標。0-1のfloat型で、1に近いほどenergyが踊りやすい。|
music_energy 対象trackのintensityやactivityなどから算出される指標。デスメタルなどenergyが高いとされている。0-1のfloat型で、1に近いほどenergyが高い。
music_key 対象trackのキー。C=0などマッピングがされれている。0-11の整数。
music_mode 対象trackのmodality。0 or 1の値で、メジャーが1、マイナーが0。
music_loudness 対象trackの物理的な強さ。-60dB ~ 0dBのfloat型。
music_speechiness 対象track内でのスピーチ度合い。HIPHOPなどは値が高い。0-1のfloat型で、1に近いほどspeechinessが高い。
music_acousticness 対象trackのアコースティック度合い。0-1のfloat型で、1に近いほどacousticnessが高い。
music_instrumentalness 対象trackのインストゥルメンタル度合い。0-1のfloat型で、1に近いほどinstrumentalnessが高い。(≒ボーカルが存在しない)
music_liveness 対象trackがライブ収録かどうか。0.8以上であればライブである可能性が高い。
music_valence 対象trackのポジティブ度合い。1に近いほどポジティブ。
music_tempo 対象trackのBPM
music_time_signature 対象trackの拍子記号
target 対象trackがお気に入りに登録されているかどうか(0 or 1)

music_featureフォルダ

このフォルダには{track_id}.csvが大量にあり、そのtrackに関する系列データが入っています。
特定のスパンごとの音楽関連の情報が入っており、曲そのものが入っているわけではないです。

カラム名 説明
start 音色とハーモニーが比較的均一なEntityの開始時間
loudness 物理的な強さ。-60dB ~ 0dBのfloat(秒単位)
tempo BPM(秒単位)
key キー(秒単位)
mode modality(秒単位)
time_signature 拍子記号(秒単位で取得)
loudness_max_time 最大loudnessの時間(ms単位)
loudness_max Entity内での最大loudness(ms単位)
pitches_1-12 12音階のそれぞれの強さ。最も強い音階が1。(ms単位)
timble_1-12 異なるタイプの楽器や声を区別するための、音符や歌の質。0を中心とした12個のベクトル(ms単位)

解法

一部系列データはありますが、与えられたデータはテーブルデータだと捉え、LightGBMベースで解き、特徴量エンジニアリングをひたすら頑張るという方針を取りました。

データ構造の特徴として、1対多のリレーションが複数あり、これを意識してバリデーションを組んだり特徴量エンジニアリングをしたりする必要がありました。

train.csv/test.csvでは以下のようなリレーションがあります。

  • 1つのアーティスト(artist)と複数のアルバム(albums)が対応している
  • 1つのアルバム(album)と複数の曲(tracks)が対応している

また、1つの曲(track)にはmusic_featureという系列データが用意されており、系列データは複数レコードから成るため、これも1対多のリレーションだと言えます。

train.csvとtest.csvを観察すると、同一アーティストの曲がtrain.csvとtest.csvに分かれて入っていることはありませんでした。これはリーク防止のためだと考えられます。
ですので、これに従ってStratifiedGroupKFoldを採用しました。

まずはベースラインとして、train.csv/test.csvにあるほぼ全てのカラムをそのままモデルに入れました。このとき、質的変数(album_type)はOne-Hotエンコーディングし、文字列やIDが入ったカラムは削除しました。このモデルの特徴量の重要度を調べた結果、曲の人気度を表す(track_popularity)の重要度がダントツで高くなりました。人気が高い曲ほどコンペホストがお気に入り登録している可能性は高いので、この結果は当然だと思いました。その後は、track_popularityを中心に特徴量エンジニアリングを実施しました。作成した有効な特徴量は以下の通りです。

  • 同一アーティストの中でその曲が何番目に人気か(人気曲ランキングのようなもの)
  • 同一アルバムの中でその曲が何番目に人気か(人気曲ランキングのようなもの)
  • 同一アーティストでの人気度の合計(アーティストの人気度を意味する)
  • 同一アルバムでの人気度の合計(アルバムの人気度を意味する)
  • アーティストの人気度におけるその曲の人気度の割合
  • アルバムの人気度におけるその曲の人気度の割合
  • アーティストの人気度におけるそのアルバムの人気度の割合
  • アルバムがリリースされた年
  • アルバムがリリースされた月
  • アルバムがリリースされた日
  • アーティストごとの曲の人気度の最小値, 最大値, 標準偏差, 平均値
  • アーティストごとの曲の人気曲ランキングの最大値, 標準偏差, 平均値
  • アルバムごとの曲の人気度の最小値, 最大値, 標準偏差, 平均値
  • アルバムごとの曲の人気曲ランキングの最大値, 標準偏差, 平均値

この中で特に有効だった特徴量は、

  • 同一アーティストの中でその曲が何番目に人気か
  • 同一アルバムの中でその曲が何番目に人気か

の2つです。いずれも人気曲ランキングを意味する特徴量になります。

このコンペのデータは、コンペホストがお気に入り登録していない曲のアーティストの曲は全くデータに入っていないということが特徴です。そのため、たとえ世間であまり人気がないアーティストであっても、そのアーティストの曲がtrain.csv/test.csvに含まれているということは、そのアーティストの中で最も人気な曲をコンペホストがお気に入り登録している可能性が高いです。別の言い方をすると、絶対的な人気度(track_popularity)は低いが、マイナーなアーティストが今回のデータに含まれている場合、そのアーティスト内で最も人気度が高い曲はコンペホストがお気に入り登録している可能性が高いです。そのように考え、人気度という絶対値ではなくそれをアーティスト内やアルバム内でランキング化した相対的な評価値を特徴量として生成しました。それが

  • 同一アーティストの中でその曲が何番目に人気か
  • 同一アルバムの中でその曲が何番目に人気か

でして、実際にこれらの特徴量は重要度がかなり高くなっていたので、仮説は合っていたと言えそうです。

ただ、不可解な点として、楽曲の長さを示す(duration_s)も同じくらい重要度が高かったのですが、これがなぜなのか理解できず、そのためこの特徴量に関しては有効な特徴量エンジニアリングができませんでした。これは、今回のデータの中に極端に曲の長さが短いまたは長い曲があり、そのような曲はほとんど似たような正解ラベルであったため、決定木の早い段階の分岐でduration_sが使われていただけなのではないかと推測しています。

music_featureフォルダの情報に関しては、音楽的情報がそのままお気に入り登録の有無につながるとは考えず、当初は用いない考えでした。しかし、コンペ途中でコンペホストが公開したNotebookでは、music_featureの中からいくつか有効な特徴量を生成できていたようなので、それを参考に以下2つの特徴量を追加しました。

  • tempoの最大値
  • loudness_maxの最大値

music_featureに関しては他にも多くの特徴量があったのですが、機械的に大量に特徴量を作成すると特徴量の数が膨大になり、精度がかなり悪くなりました。そのため、コンペホストの公開Notebookを参考にして、筋の良さそうな特徴量を1つ1つ加えて実験するという方針を取りました。

結果

以上のように特徴量エンジニアリングに注力することで、Public LBでは2位、Private LBでは1位を取ることができました。Public LBで1位の方はかなりShake Downしていていたため、Public LBにoverfitしていたようです。私はPublic LBスコアとCVスコアの相関は取れており、Trust Your CVを徹底していたため、無難に安定してPrivate LBで高いスコアを出せたのだと考えています。

Public LBの結果

Private LBの結果

もっと時間があれば、xgboostやcatboostといった他のGBDTとアンサンブルしたり、optunaでハイパラチューニングしたりしたかったなと思っています。

ひとまず1位を取れたので素直に嬉しいです。

さいごに

長い長い記事を最後まで読んでいただき、本当にありがとうございます。
久しぶりにコンペに参加し、コンペってやっぱり楽しいなと再認識できました。
Kaggleなどのコンペは2~3ヶ月と長いので、DS Dojoのような1ヶ月のコンペはとても参加しやすく、非常にありがたいです。
第3回のDS Dojoが開催されたら、また参加したいと思います!(2連覇したい)

松尾研究所テックブログ

Discussion