競馬予測システムの構築とMLOps実践(したかった)

本稿は[Brainpad Advent Calendar 2023](https://qiita.com/advent-calendar/2023/brainpad)の記事ですが，記事内容は筆者のプライベートな活動・見解に基づくものであることご了承ください．
遅刻投稿＆[ネタ被り記事](https://qiita.com/_tamiya/items/16786a4364584e0fc0a9)です，申し訳ありません🙇

## 1. はじめに

元々2021年頃から気が向いた時駆動で競馬予測を散発的に取り組んでいました．散発的とは予測したいレースがあれば，それに間に合うようにデータ取得と予測モデル構築をし出走前に予測結果を基に馬券を買うといった流れを繰り返すことを指します．
カジュアルに機械学習でギャンブルを楽しむ分には，これでも遊べるので問題ありませんでしたが，近年エンジニアリングやシステムアーキテクチャに対する個人的な関心が高まったことと昨年のアドカレで社内の競馬AI勢の活動に触発されてシステム構築とMLOps実践にトライしてみました．

## 2. 要件定義

なぜ競馬予測システムを構築したいのか？その目的は結局**儲けたい**に行き着くでしょう．
この目的を達成するためには**確度の高いレース予測**を**継続的**に出力し**最適な意思決定**をする必要がありますね．うーむ，かなりヘビーですが個人開発だし夢ぐらい見させて欲しいの精神でいきます．
目的を実現するための上記の構成要素をwhatツリーで分解してみると以下の通りでしょうか．
  
  
![目的からのhowツリー.drawio-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/1ee9fed4-4f1a-829e-3a77-0510584d4433.png)
  
  
Step1として**継続的**に焦点を当ててデータ取得～予測まで実行する仕組みを設計・実装しました．

## 3. 設計・実装

システム構成図は次のとおり

![システム構成図.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/f187dbe7-9567-f439-021b-8a74c4bccd66.png)
  
  
### 3.1. Webクローラー

某競馬サイトよりクローリングを定期実行し出馬表・レース結果・競走馬プロフィールを取得しデータレイクに格納していく．データを取得する方法については数多の記事が既に存在するので，ここでは割愛させていただきます．取得するデータ区分名と保存先のバケット名は次の表のとおり．

  |バケット名                       |データ名          |更新タイミング    |説明                                                                     |
|-------------------------------|-----------------|----------------|-------------------------------------------------------------------------|
|keiba-db-manual             |出走馬            |-               |競走馬の所属や血統に関するデータ                                              |
|keiba-db-race-cards-meta-*  |出走表メタデータ    |各レース開始10分前|出走表の諸条件                            |
|keiba-db-race-cards-*       |出走表            |各レース開始10分前|レースに出馬する競走馬のテーブルデータ        |
|keiba-db-race-results-meta-*|レース結果メタデータ|毎週水曜09:00    |レース結果の諸条件                        |
|keiba-db-race-results-*     |レース結果         |毎週水曜09:00    |レース結果が付与された競走馬のテーブルデータ|

出走表及びそのメタデータについてはレース開催日早朝に出走表を10分前に取得するためのタスクを`CloudTasks`に格納しています．
また取得したデータは下図のようにGCSの各バケットに格納されます．
![データレイク・出走表.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/a84827e5-742c-f768-0adb-e73f25c88c03.png)

※ 参考: `CloudRun`上のWebクローラーのメトリクス
![CloudRun上のWebクローラーのメトリクス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/10b253e8-b550-283e-9e2b-8168f24ec7a3.png)


### 3.2. ELT

データレイクに格納したデータを整形しDWHに格納していきます．
データの更新頻度がレース開催日において10～30分あたりとなる出走表とそのメタデータについては`CloudFunctions`上にELT実行関数を実装し，週あたりに1回の頻度で更新するレース結果とそのメタデータについては`BigQueryDataTransfer`を利用しています．
更新頻度が日単位の粒度であれば`BigQueryDataTransfer`で統一できたのだが，やむを得ずこのような構成となりました．

`BigQueryDataTransfer`は指定した時間に`GCS`上のデータを取得しBigQueryに格納することができます．
![BigQueryDataTransfer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/73c4cd77-c03f-297b-8229-96897fa615ea.png)

整形されたデータは下図のように`BigQuery`上にて利用でき，これらを用いてドメイン知識理解・予測モデル構築に使用しています．
![DWH・BigQuery.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/5a10bf5a-8072-8d04-a3f8-9c19bdb62a90.png)

※ 参考: `CloudFunctions`上の出走表を対象としたELT関数のメトリクス
![ELT・CloudFunctions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/0650e86b-dd54-f9b0-6b3d-0111db779fb4.png)

### 3.3. 学習〜評価

ローカルマシン下にてDWHからデータを呼び出し予測モデルを構築・評価を行っています．予測モデルについては`jupyter notebook`にてプロトタイピングを行い予測モデルを固めたpickleファイルを出力，それを予測モデル用のバゲットにアップロードすることで予測の準備が完了となります．

※ 参考: ローカルマシン上での予測モデルのプロトタイピング
![学習と評価.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592574/86f03ec3-31ed-d517-486f-888efb492de4.png)

### 3.4. 予測～通知

Webクローラー部と同じように`CloudScheduler`で発火し直前に取得した予測用データとGCS上の予測モデルにて予測結果を出力し，Slackに予測結果を通知する予測実行関数を`CloudFunctions`上に**実装中**となっています．

## 4. まとめ

年内に実装しきりたかったですが途中で力尽きてしまいました．一応競馬予測のシステム構成に関する記事はあまり見かけていなかったので，少しばかりではありますが差別化できたとは思っています．
来年はこの仕組みを運用しつつ学習～予測あたりも`VertexAI`を活用するなどアップデートを図っていきたい所存です．読んでいただきありがとうございました．