〜LambdaRank編〜ココンの情報をいつでも、どこでも。〜LambdaRank編〜ツイートAI戦略室の坂本です。元はといえば忘年会の余興から始まった競馬予想AIですが、ブログ記事のアクセス数も多く、予想以上に注目されている感じです。実は以前のモノは、所詮余興というノリで作ったAIなので、色々と不完全な部分もあったのですが、方々からリクエストがあったので、もう少しだけきちんと考えて予想AIを作り直すことにしました(本当は次の忘年会までネタとして取っておきたかったのですが・・・)。ここで紹介する競馬予想AIのソースコード全体は、例によってGitHub(https://github.com/cocon-ai-group/turf-tipster2/)で公開しています。記事中で紹介するのはコードの断片のみなので、全体を見ながら記事を読む方が、より理解が容易となるでしょう。学習データの用意LightGBMによる学習オッズ計算LambdaRankとは前回では、独自に定義した損失関数を使ってニューラルネットワークを学習させることで、「馬の名前から強さベクトルを作成する」ニューラルネットワークを作成しました。実はこのような、教師データから序列を求める機械学習はランキング学習と呼ばれ、様々なアルゴリズムが存在しています。代表的にはRankNet・NNRank、LambdaRankなどがあり、いずれも損失関数の形を工夫することで、「強さベクトル」に相当する数値を出力させることが出来ます。中でもLambdaRank(https://dl.acm.org/citation.cfm?doid=1571941.1572021)は、マイクロソフトの研究者が考案して公開したアルゴリズムで、(おそらく)検索エンジンBingのランキングシステムのベースとなっているであろう、優れたアルゴリズムです。このLambdaRankを、こちらでも紹介した決定木を使用したアルゴリズムに対して適用させたモノを、LambdaMART(https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/MSR-TR-2010-82.pdf)と呼び、ニューラルネットワークを使用した機械学習よりも軽量で、大きなデータに対して適用することが容易になっています。このように色々と優れたアルゴリズムがある以上、それを使用して競走馬の序列を学習させれば、前回と同じ発想による競馬予想AIが出来るだろう、ということで、今回はLambdaRank/LambdaMARTによる競馬予想AIの作成について解説します。ちなみにランキング学習のアルゴリズムを使用する以上、ここでは前回と同様に、「強い馬は上の順位に位置することが多いだろう」という前提に立って競馬予想AIを作成します。全て結果となる順位のみから学習することになるので、追い込み馬や先行馬などの脚質、馬同士の相性などについては考慮されません。前回指摘したように、「勝つときは大勝ちするが負けるときは大負け」のような馬については前提の対象外になります。そのため実際の競馬レースを本当に正しくモデル化出来るのか、という疑問もありますが、大量のデータを学習させることでどの程度の予測が可能か、まずは試してみたいと思います。LambdaRankによる学習の基本まずは競馬予想AIに必要な、機械学習の部分について解説しようと思います。手始めに、前回同様の小さなデータ(馬と騎手の名前、競馬場の名前、馬場の種類、天気、距離)のみを学習させる例について考えてみましょう。RambdaRankを使う場合、学習データの単位はランキングされる一つ一つの要素です。ここでは、レース一つではなく、レース内に出走する馬の一頭一頭が、学習データの一単位となります。学習ターゲットはランキングの数値で、ここではレース結果の着順が学習ターゲットに当たります。まくって勝っただとか先行逃げ切りレースだったとかは学習しません(というかデータが無いです)。通常の回帰とは異なっている箇所として、RambdaRankを使用する場合に必要な「Queryデータ」というデータが存在する点が挙げられます。このQueryデータは、ランキング学習におけるランキングの単位(学習データの単位ではなく)を表すデータで、そのランキングに含まれている学習データの数を表すデータとなります。上の図では、2レースで7頭の馬が学習データとなるため、学習データの数は7個で、それぞれ6次元で7×6サイズの配列が、学習データのサイズとなります。Queryデータは2レース分なので2個のデータを持つ一次元配列で、それぞれの値は4と3になります。Queryデータの合計は学習データの数と一致しなければなりません。ここでは馬と騎手の名前の他に、4つのパラメーターのみをレースのメタデータとして追加していますが、きちんとスクレイピングさえしてやれば、他にも気温や湿度や風向きや前日の株価や太陽黒点の数など、色々なデータを追加することも出来ます。したデータの選別が今回の手法でのAI作成では重要で、AIの個性をもたらず原因となる訳なので、このプログラムを元に独自の競馬予想AIを作成したいという方は、色々とデータの取り扱いについて考えてみると面白いでしょう。学習データの用意それでは実際のアルゴリズムとプログラムコードについて解説します。競馬レースのデータについては前回と同様、JRAのデータサービスであるJRA-VAN(https://jra-van.jp/)等から入手します。今回も、前回用意したデータと共通のフォーマットを使用します。レース情報には、レースのメターデータと、出走馬情報のリストが含まれています。race_train.csv新潟日報賞|新潟|芝|1400|曇|600:230_160_980:410:1210:490_6730_3340:2700:22900:102190|20170812,1|アポロノシンザン|津村明秀,2|ビップライブリー|大野拓弥,3|ディアマイダーリン|田中勝春,4|ラプソディーア|吉田豊,5|ルグランパントル|柴田善臣,6|メイショウメイゲツ|江田照男,7|ネオスターダム|内田博幸,8|ピンストライプ|M.デム,9|ナイトフォックス|丸田恭介,10|テンテマリ|丸山元気,11|エリーティアラ|北村宏司,12|クリノタカラチャン|野中悠太,13|スズカシャーマン|伊藤工真,14|スズカアーサー|武士沢友,15|オヒア|小崎綾也,16|マリオーロ|吉田隼人,17|フレンドスイート|柴田大知,18|ディープジュエリー|石橋脩このCSVファイルを読み込むのは、以下の関数です。内容は簡単なモノで、CSVの各カラムから全てのデータを取り出して引数のリストへ追加するのと、全ての馬の名前、騎手の名前、競馬場の名前、馬場の種類、天気の種類をラベル化するためにリストアップしておくものになります。3:all_horse_name.append(result[1])all_jockey_name.append(result[2])上記関数でデータを読み込んだら、全ての馬の名前、騎手の名前、競馬場の名前、馬場の種類、天気の種類について以下のようにLabelEncoderを作成しておきます。LabelEncoder().fit(all_tenki_str)そうして、各レースのデータを分解して数値データ化するために、以下の関数を作成しました。この関数は、レース情報のリストを引数に取り、同じく引数で与えられるリストに、分解したデータを追加してゆきます。学習データのターゲットは、レースの着順となりますが、ここではmax_positionで最大値を指定することで、「ある一定以下の下位馬は全部同じ扱い」にしています。重要なのが、ソースコード中にある、「ターゲットの順序をシャッフルする」という行です。レース結果から作成されるCSVデータは着順に並んでいるのですが、そのまま学習させてしまうと、LightGBMは「枠順の上の馬ほどランクが高い」という風に学習をしてしまうのです!(実際作ってみて、このような挙動をするとは予想外でした。RambdaRankの特性なのかLightGBMの特性なのかは解りませんが、前回のようにニューラルネットワークでバッチサイズ方向にデータを並べる場合は起こりえない嵌まりポイントです)そこでレース毎に枠順をランダムに並び替えることで、枠順は順位とは関係ないという事を学習させています。(本来は出走時の枠順で並べるべきでしょうね。target:all_races_rank.append(tgt[0])all_races_target.append(tgt[1])ソースコードの細々とした箇所の解説は省きますが、こうしてレースのデータをRambdaRankで学習可能なデータ形式で読み込めば、後は機械学習を行うだけとなります。学習データとなる、馬と騎手の名前、競馬場の名前、馬場の種類、天気、距離のうち、距離を除くデータはカテゴリカルなデータ、つまり連続量ではなくどれか一つを選択するタイプのデータです。いった場合、機械学習アルゴリズムではOne-Hotベクトルとして、種類の数だけのサイズで対象の次元のみが1となるベクトルデータを用意することで精度の向上が図れます。馬の名前について言えば、馬に相当する数値(1とか2とか)は一つの値として表現できますが、学習させるときには馬の数と同じ次元数を持つベクトル(馬が全7頭の場合:1は[1,0,0,0,0,0,0]に、2は[0,1,0,0,0,0,0]になる)で馬を表現する事になります。そのようなベクトルは手動で作成しても良いのですが、幸いにLightGBMでは「categorical_feature」オプションを使用することで、「そのデータはカテゴリカルなデータだ」と教えてやることが出来ます。「categorical_feature」オプションで指定されたデータは、内部でOne-Hotベクトルとして扱われることになります。「categorical_feature」オプションを指定して、RambdaRankでLightGBMを学習させるコードは、以下の部分になります。max_position」は学習の際の評価の方法を指定します。「metric」オプションは評価の損失関数で「ndcg_eval_at」は評価する対象を(ここでは1着から3着までを評価の対象に)指定します。「max_position」はランキングに含まれる最大の値を指定します(最大のQueryデータ値とは異なります)。結果は回帰と同じく「predict」を使用して求めます。predictを呼び出す際にもQueryデータは必要となります。下のコードでは、1レースのみの予想を求めているので、長さ1の配列に出走馬の数を入れてpredict時のQueryデータとしています。dst[dst_ind結果に対して呼び出している「norm_racedata」は、求められたスコアを正規化するものですが、特に無くても構わないです。ここでは後で異なる(LightGBM以外の)アルゴリズムとアンサンブルするために全ての結果を正規化するようにしていますが、正規化を行わなくてもそれほど違いは出ないようです。dataオッズ計算さて、RambdaRankによる予想は、あくまで「レース結果の予想」であって、「馬券の予想」ではないことに注意してください。馬券を買う場合はオッズを見ながら買うことになるので、オッズで重み付けをおこない、Q学習などの手法を採るべきなのでしょうが、ここではそのような学習は行っていません。それでも、実際に馬券を買っていくら帰ってくるのかは気になるところなので、過去のレースのオッズ情報を元に、どの馬券でどのくらいのリターンが期待できるか、計算してみます。レース情報からオッズ情報を読み込む関数です。race_oddsこの関数はほぼ問題ないでしょう。レース情報を取得するプログラムで作成した並び順が正しく再現されていればそれで良いです。次に実際のリターンを計算する部分です。[レース数%d]\n'%len(all_races_query_test))df_outfile.write('\t単勝\t複勝\t複勝(1枚)\t馬連\tワイド\tワイド(1枚)\t馬単\t三連複\t三連単\n')df_outfile.write('オッズ:'+'\t'.join(list(map(str,ret_score_r)))+'\n')df_outfile.write('当り数:'+'\t'.join(list(map(str,ret_hitnum)))+'\n')リターンの計算は上のように、テスト用データに対する予想を行い、結果とオッズ情報から平均オッズとあたり数を計算しています。学習データとテスト用データを、2000レースと500レース分用意して、AIの予想精度を見てみましょう。2残念ながら馬券で儲けを期待できるほどの精度は出ていませんが、単勝馬券の控除率が80%であることと、予想に使うデータの種類が少ない(馬と騎手の名前、競馬場の名前、馬場の種類、天気、距離しか見ていない!)ことを考えれば、それなりではないでしょうか。データの量を増やすことで、さらに予想精度を改善することが出来ます。データの種類を増やして、予想精度を向上させる部分については、後編で解説をします。2020」参加レポート広報の馬場です。