BQML DNNで競馬予測してみる

BQMLでDNNとXGBoostとAutoML Tablesが使えるようになるというニュースがありDNNを試してみました。
なんとなくでそれなりにいい感じにできちゃいます。
https://cloud.google.com/bigquery-ml/docs/release-notes

#データを準備する
説明変数と目的変数が1行に含まれているデータをBQに投入する。
私は趣味で集めていた競馬データを使ってみた。
競馬データを集めたい場合は以下の記事が大変参考になる。
[データ収集からディープラーニングまで全て行って競馬の予測をしてみた](https://qiita.com/kami634/items/55e49dad76396d808bf5)

競馬の場合、以下を横持ちにしているデータを準備する。

- 説明変数：レースの距離、会場、性齢、斤量、過去レースの結果、該当距離の成績、、等
- 目的変数：着順（3着以内になるか否か）

明らかな連続値は数値型にして、あとは文字型にしておけばいい感じに学習してくれる。
（本当はもう少し丁寧に前処理した方が良いのだろうが、、）

#モデルを作成する

```sql
CREATE OR REPLACE MODEL <モデル名>
OPTIONS(MODEL_TYPE='DNN_CLASSIFIER',
        ACTIVATION_FN = 'RELU',
        AUTO_CLASS_WEIGHTS = TRUE,
        BATCH_SIZE = 2048,
        DROPOUT = 0.1,
        EARLY_STOP = TRUE,
        HIDDEN_UNITS = [128, 128, 128],
        INPUT_LABEL_COLS = ['目的変数カラム'],
        LEARN_RATE=0.001,
        MAX_ITERATIONS = 500,
        OPTIMIZER = 'ADAM')
AS SELECT  
  <説明変数カラム>,
  <目的変数カラム>
FROM
  <table name>
```

大体1時間〜2時間くらいでモデルは作成できた。

##各パラメータについて
上記のモデル作成の例は、公式ドキュメントに乗っているパラメータほぼそのままだが、
これでもそれなりのものができるっぽい

### MODEL_TYPE
モデルのタイプを回帰（気温等の連続値の予測）か、分類（天気が晴れなのか雨なのか等のカテゴリ値）か選択する

```
MODEL_TYPE = { 'DNN_CLASSIFIER' | 'DNN_REGRESSOR' }
```

競馬の着順（3着以内になるか否か）はカテゴリ値の分類なので、DNN_CLASSIFIERにする

###ACTIVATION_FN
活性化関数（ニューロンが発火するための計算式）を選択する


```
ACTIVATION_FN =  { 'RELU' | 'RELU6' | 'CRELU' | 'ELU' | 'SELU' | 'SIGMOID' | 'TANH' }
```

（どれ使えばいいんだ。。）

以下[Wikipedia](https://ja.wikipedia.org/wiki/%E6%B4%BB%E6%80%A7%E5%8C%96%E9%96%A2%E6%95%B0)より
> ReLU（ランプ関数）
2011年、Xavier Glorot らは隠れ層の活性化関数としてmax(0, x) を使った方がtanhやソフトプラスよりも改善するということを発表した。一般的にはこれはランプ関数と呼ばれるが、ニューラルネットワークの世界ではReLU (英: Rectified Linear Unit, Rectifier、正規化線形関数）と呼ばれる。Yann LeCunやジェフリー・ヒントンらが雑誌ネイチャーに書いた論文では、2015年5月現在これが最善であるとしている。発想としては区分線形関数を使った1次スプライン補間である。線形補間を参照。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209773/c1001bac-f3ce-e2df-404d-82a86b8329a1.png)

2015年5月時点では最善だったっぽいので、とりあえずRELUにしてみる。

###AUTO_CLASS_WEIGHTS
分類モデルのときだけ有効なパラメータ。
不均衡データ（例えば分類したいラベルにAとBの2種類があってAが90%みたいなとき）を分類するときにいい感じに重み付けしてくれるらしい機能。
今回の競馬分類はそれなりに不均衡データになると思うので、TRUEにする。
デフォルトはFalse。

[機械学習における不均衡データの扱い方](https://qiita.com/r-takahama/items/631a59953fc20ceaf5d9#:~:text=%E4%B8%8D%E5%9D%87%E8%A1%A1%E3%83%87%E3%83%BC%E3%82%BF%E3%81%A8%E3%81%AF,%E3%81%97%E3%81%BE%E3%81%86%E5%8F%AF%E8%83%BD%E6%80%A7%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)

```
AUTO_CLASS_WEIGHTS = { TRUE | FALSE }
```

###BATCH_SIZE
DNNではミニバッチ勾配降下法というトレーニングデータをサブセットに分けて最適なパラメータを見つける手法が使われるようだが、そのときのサブセットのデータサイズ。

[機械学習／ディープラーニングにおけるバッチサイズ、イテレーション数、エポック数の決め方](https://qiita.com/kenta1984/items/bad75a37d552510e4682)

今回のデータの件数は100万件くらいなので、とりあえず2048でやってみる。

```
BATCH_SIZE = int64_value
```

###DROPOUT
過学習を防止するために、学習を進める際に一定の確率でノードを無視する率。
少し調べてみると、0.5か0.1が多そう。BQML公式ドキュメントの例では0.1になってたので、とりあえず0.1でやってみる。

[Dropout：ディープラーニングの火付け役、単純な方法で過学習を防ぐ](https://deepage.net/deep_learning/2016/10/17/deeplearning_dropout.html)

```
DROPOUT = float64_value
```

###EARLY_STOP
学習の結果あまり改善がなかったときに、学習をストップさせるかどうか。
過学習防止や余計な計算リソースを使わないようにする。
デフォルトはTRUE。

```
EARLY_STOP = { TRUE | FALSE }
```

###HIDDEN_UNITS
DNNの隠れ層における、層の数とユニットの数を指定。
[隠れ層と隠れニューロンはいくつにするべきなのか？](https://webbigdata.jp/ai/post-1491)

（しかし適切な数がよくわからない。。）

BQML公式ドキュメントの例にならって、[128, 128, 128]にしてみる。

```
HIDDEN_UNITS = int_array
```

###INPUT_LABEL_COLS
目的変数カラムを指定する

```
INPUT_LABEL_COLS = string_array
```

###LEARN_RATE
学習のたびにどれだけ重み付けパラメータを更新するかという率。
高すぎるとうまく学習しなかったり、小さすぎると収束まで時間がかかりすぎる。

（どのあたりが妥当なのだろうか。。）

BQML公式ドキュメントの例にならって、0.001にしてみる。

```
LEARN_RATE = float64_value
```

###MAX_ITERATIONS 
最大イテレーション回数。
イテレーション回数はバッチサイズに対して、何回学習すると元のデータセット件数になるかという回数。
約100万件のデータに対してバッチサイズを2048にした場合は、500が適切っぽいのかな。

[機械学習／ディープラーニングにおけるバッチサイズ、イテレーション数、エポック数の決め方](https://qiita.com/kenta1984/items/bad75a37d552510e4682)

```
MAX_ITERATIONS = int64_value
```

###OPTIMIZER 

[【2020決定版】スーパーわかりやすい最適化アルゴリズム -損失関数からAdamとニュートン法-](https://qiita.com/omiita/items/1735c1d048fe5f611f80)

>「損失をゼロにする」というゴールをなるべく効率よく到達してくれるものが最適化アルゴリズム です。



>Adamの正体はモーメンタムとRMSPropの良いとこどり 

（良いとこどりとは素晴らしい。とりあえずAdamにしよう）

```
OPTIMIZER =  { 'ADAGRAD' | 'ADAM' | 'FTRL' | 'RMSPROP' | 'SGD' }
```

#モデルの評価
以下のように、説明変数カラム全部と目的変数カラムをもつWith句を作成し、
ml.evaluate関数にモデル名とWith句のテーブル名を渡す

```sql
with eval_table
as
(
select 
  <説明変数カラム>,
  <目的変数カラム>
FROM
  <table name>
)
select
 *
from
ml.evaluate(MODEL `<モデル名>`, table eval_table)
```

##結果
以下のような結果が返ってくる

| precision | recall | accuracy | f1_score | log_loss | roc_auc |  
|--:|--:|--:|--:|--:|--:|--:|
|0.3089887640449438 | 0.7638888888888888 | 0.572736520854527 | 0.44 | 0.6899435430022666 | 0.6968951048951049 |

[【入門者向け】機械学習の分類問題評価指標解説(正解率・適合率・再現率など)](https://qiita.com/FukuharaYohei/items/be89a99c53586fa4e2e4)
[機械学習でLog Lossとは何か](https://qiita.com/exp/items/1c6c9a3fae2d97bfa0c7)
[【ROC曲線とAUC】機械学習の評価指標についての基礎講座](https://www.randpy.tokyo/entry/roc_auc)

どの指標を重視するのかは作るモデルごとに異なるみたいです。
（今回のモデルだと何を重視したらよいのか、、）

- precision（正解率）：正予測の正答率。今回の場合だと、3着以内と予想した馬が実際に3着以内になる確率
- recall（再現率）：正に対する正答率。今回の場合だと、実際3着以内になった馬の中で、3着以内と予想された確率
- accuracy（正解率）：全予測正答率。今回の場合だと全予測の中で、実際に3着以内予想と3着以内外の正解率
- f1_score：適合率と再現率の調和平均（適合率と再現率のバランスを見るらしい）
- log_loss：間違ったラベルを高い確率で出しちゃうと高くなるらしい
- roc_auc：不均衡データを考慮した指標。高いほどいいっぽい

#予測する
以下のように、説明変数カラム全部をもつWith句を作成し、ml.predict関数にモデル名とWith句のテーブル名を渡す

予測された分類結果は「predicted_目的変数カラム」の項目になる（予測項目がuriageならpredicted_uriageになる）
確率をもつ項目は「predicted_目的変数カラム_probs」になる（これはネストされた項目なので、UNNESTすることで扱いやすくなる）

```sql
with pred_table
as
(
select 
  <説明変数カラム>
FROM
  <table name>
)
select
  ~,
  predicted_目的変数カラム, /* 分類結果 */
  data.prob /*分類確率*/
from
  ml.predict(MODEL `<モデル名>`, table pred_table),
  unnest(predicted_目的変数カラム_probs) as data
where 
  data.label=1 /* 3着以内に入る確率だけ出す */
```

##2020 年宝塚記念を予測するとこんな感じ
先週（2020/6/28）のG1宝塚記念を予測してみた。
並びは3着以内確率上位順。
1着は外しているが何となくいい線いってそうな気がする。
（ちなみにこのレース3連単 **183,870円!** です）

|レース名|馬番|馬名|3着内確率|
|:---------- |------|------|------:|
|宝塚記念|5|サートゥルナーリア|0.82017|
|宝塚記念|16|クロノジェネシス|0.79715|
|宝塚記念|14|キセキ|0.75275|
|宝塚記念|12|モズベッロ|0.70728|
|宝塚記念|11|ラッキーライラック|0.69796|
|宝塚記念|10|メイショウテンゲン|0.66159|
|宝塚記念|1|トーセンカンビーナ|0.56264|
|宝塚記念|3|グローリーヴェイズ|0.55474|
|宝塚記念|7|ワグネリアン|0.52002|
|宝塚記念|18|ブラストワンピース|0.48493|
|宝塚記念|8|レッドジェニアル|0.48320|
|宝塚記念|15|スティッフェリオ|0.33708|
|宝塚記念|6|トーセンスーリヤ|0.33540|
|宝塚記念|13|ダンビュライト|0.32290|
|宝塚記念|17|カデナ|0.3186|
|宝塚記念|2|ペルシアンナイト|0.27348|
|宝塚記念|9|アドマイヤアルバ|0.14271|
|宝塚記念|4|アフリカンゴールド|0.09236|


#料金
[BigQuery ML の料金](https://cloud.google.com/bigquery-ml/pricing?hl=ja)によると、
CREATE MODELは毎月10GBの無料枠があり、それ以降は$250.00 per TBかかるようである。

しかし以下の画面のように実行見積もりは150MBだけど、実際は87GB使っちゃいましたみたいなパターンはどのような扱いになるのかイマイチよくわからない。。
（今の所BQMLの請求きてないので、見積もりのほうが優先されるのか。。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209773/5e799c57-3545-a7a4-9262-b02c46dd392e.png)

#その他参考
[BigQuery MLにAutoML Tables、XGBoost、DNNが来たのでおさらい](https://qiita.com/na0/items/a236a613cbf957dab503)

[The CREATE MODEL statement for Deep Neural Network (DNN) models](https://cloud.google.com/bigquery-ml/docs/reference/standard-sql/bigqueryml-syntax-create-dnn-models)