RandomForestでPOGの賞金予測をする

結論から書けば玉砕でした。。

そりぁそうですよ

全兄弟で同じ厩舎でも一方はG1馬、他方は未勝利馬ということもあるわけですから。

数千サンプルのデータと二十次元程度の特徴量から予測するなんておこがましかったわけです。

いちおうトライしたのでやったことを書いておこうと思います。

データ

先日実装したnetkeibaスクレイパーを使って現3歳馬のデータを抽出しました。

  • 2018/5/3時点での皐月賞後が終わったあとデータ
  • 5962頭から 地方調教師に所属する馬を除いて 全3923頭

他に調教師、生産者、種牡馬、母父、母馬のデータも抽出し、結合して使います

特徴量ベクトル(説明変数)

調教師、生産者、種牡馬、母父、母馬のデータを結合し、 19次元のベクトルを入力として用います。

  • debut_weight:デビュー戦の体重
  • birth_date_from_beginning_of_year:1/1を基準にした誕生日までの日数
  • crop_win_count:母馬の産駒の合計勝利数
  • crop_grade_horse_count:母馬の産駒の重賞馬の合計頭数
  • crop_grade_win_count:母馬の産駒の重賞勝ちの合計数
  • win_count_trainer:調教師の勝利数
  • prize_trainer:調教師の獲得賞金額
  • win_count_breeder:生産者の勝利数
  • prize_breeder:生産者の獲得賞金額
  • win_ratio:種牡馬の勝率
  • earning_index:種牡馬のearning index
  • prize_sire:種牡馬の獲得賞金額
  • win_ratio_bms:母父の勝率
  • earning_index_bms:母父のearning index
  • sex_セ、sex_牝、sex_牡: 性別(sexをone-hot encoding)
  • stable_trainer_栗東、stable_trainer_美浦: 所属(stable_trainerをone-hot encoding)

今年デビュー予定馬のデビュー時体重のデータを得るのは難しいかもしれませんが 大型とか小型とかある程度はPOG本から情報仕入れられるのと予測に役に立ちそうなファクターな気がしたので入れています。

目的変数

もちろん獲得賞金額になりますがlogをとって対数スケールにしました。

使った機械学習アルゴリズム

手っ取り早い RandomForest です。 パラメータチューニングなしで学習させました。

学習データと検証データの比率は 7:3 でランダムに分割しました

学習結果

学習データでの予測結果のグラフです (logスケールなので軸の目盛りは0-10です)

f:id:sanshonoki:20180523223843p:plain

R2スコアは 0.83です

このグラフを見ると淡い期待が。。

競馬で言えば、最後の直線に入り「そのまま、そのまま」と叫ぶのに似た気持ちです

検証データでの予測結果

f:id:sanshonoki:20180523223919p:plain

R2スコアは 0.07 ...

全部正解だったときのスコアが1.0、ランダムに当てずっぽうに答えたときのスコアが0.0なので非常に辛い結果です

心の目で見るとわずかに右上がりの傾向が見えないこともないけど?(強がり)

Feature Importance

夢は潰え終戦しました

が、最後にFeature Importanceを出力し、各特徴量の学習への寄与度を見てみます。

f:id:sanshonoki:20180523225001p:plain

上位8位の特徴量
  1. 調教師の獲得賞金額
  2. 1/1を基準にした誕生日までの日数
  3. 母馬の産駒の合計勝利数
  4. デビュー戦の体重
  5. 母父のearning index
  6. 生産者の獲得賞金額
  7. 種牡馬の勝率
  8. 母父の勝率

これはPOGをやったことある人ならうなずける結果ではないでしょうか

このデータを頭に入れてドラフト会議に臨みたいと思います

2018-2019年度POG用の2歳馬リストを作る

先日作ったスクリプトで自分でも2歳馬リストが作れることが分かったので作ってみました。

方法
  1. 必要なデータをスクリプトで取得

    $ python get_horse_data.py --age 2 -o pog2018_1.csv --include_no_debut
    $ python get_horse_additional_data.py -i pog2018_1.csv -o pog2018_2.csv
    
  2. 2つのファイルをマージする

     import pandas as pd
    
     df1 = pd.read_csv("pog2018_1.csv")
     df2 = pd.read_csv("pog2018_2.csv")
    
     df = pd.merge(df1, df2, on='id')
    
     # 不要なカラムを削除
     df.drop(['birth_year', 'trainer_id', 'owner_id', 'breeder_id', 'prize', 'name_y', 'race_result'], axis=1, inplace=True)
     df.drop(df.loc[:, df.columns.str.contains('^Unnamed')], inplace=True, axis=1)
    
     df.to_csv("pog2018_list.csv")
    
出力項目
  • id
  • 馬名
  • 性別
  • 厩舎
  • 母父
  • オーナー
  • 生産者
  • 毛色
  • 生年月日
  • セリ価格
  • 近親馬

以下のサイトで2歳馬リストがダウンロードできますがセルフで作ってみたいという方はやってみてください。。 (スクリプトの拡張大歓迎)

fast.aiのPractical Deep Learning For Coders, Part 1 (2018 edition)

fast.aiのPractical Deep Learning For Coders, Part 1 (2018 edition) を細々と続けていましたがようやく一通り終えた...! というところまでいきました。

7 weeksのコースなので2倍ぐらいの時間がかかっちゃった計算になります。。

さらにブログの筆もなかなか進まず.. (;´Д`)

良かった点は

  • v1(2017 edition)の復習ができた
  • 特にBatchNormalization、Resnetについて理解が深まった
  • 新しいテクニック SGDR (stochastic gradient descent with restarts) 、TTA (test time augmentation) を知れた

一方で残念だったところは

  • fast.aiのライブラリでかなり抽象化されているのでPyTorchでのコーディング力がついた感なし
  • 正直、v1(2017 edition)以上にforumに頼らないと進めない/分からないことがある

コーディング力をつけるためにはfast.aiのライブラリの中身をちゃんと読んでそこで何をやっているのか詳しく追っていく必要がありそうです。。

あとは発展途上ということもあって情報不足であったり動かないコードも多くforumにはかなりお世話になりました。 この辺が改善されると全受講者の無駄?な時間が減ると思います。

ということで、躓いた箇所や参考にしたforum情報をまとめておきたいと思います。

(注:本家のコードは随時更新されているので既に役に立たなくなっている情報もあるかもしれません)

躓いた箇所や参考になったforum記事のまとめ

lesson1
lesson1-vgg
lesson1-breeds
lesson2-image_models
lesson3-rossman
lesson4-imdb
lesson6-rnn
lesson6-sgd
  • ffmpegのインストール
  • Gradient Descent - Classificationのセクションで accuracyが上がらない
    • 以下のコードに修正したらaccuracyが上がるようになった

        # loss = nll(y_hat,y)
        loss = -1.0 * nll(y_hat, y) # loss must be a positive value.
      
lesson7-cifar10
planet_cv
nasnet
nlp
lang-model
  • データセットが見つからない
  • いろいろとエラーが出る..!
    • 以下のように修正したらとりあえず動いた

        #md = LanguageModelData(PATH, TEXT, **FILES, bs=bs, bptt=bptt, min_freq=10) # This does not work.
        md = LanguageModelData.from_text_files(PATH, TEXT, **FILES, bs=bs, bptt=bptt, min_freq=10)
      
        #learner = md.get_model(SGD_Momentum(0.7), bs, em_sz, nh, nl) # This does not work.
        learner = md.get_model(SGD_Momentum(0.7), em_sz, nh, nl)
      
        #learner.fit(10, 1, wds=1e-6, reg_fn=reg_fn, clip=clip) # fit() got multiple values for keyword argument 'reg_fn'
        learner.reg_fn = reg_fn
        learner.clip = clip
        learner.fit(10, 1, wds=1e-6)
      
cifar10-simplenet
  • ImageClassifierData.from_csv でエラー
    • とりあえず以下のコードにすれば動く

        # data = ImageClassifierData.from_csv(PATH, 'train', PATH/'train.csv', tfms=tfms, bs=bs)
        data = ImageClassifierData.from_paths(PATH, val_name='test', tfms=tfms, bs=bs)
      


これらの情報やその他メモを詰め込んだ自分の作業用ノートブックはこちらです。 github.com

netkeibaのデータのスクレイパー

今週末は桜花賞。いよいよクラシックシーズンの開幕です。

ということは、、

来年度のPOGの足音も聞こえてきたってことになります。

今年こそは機械学習を使って一人勝ち!!

その第一歩としてnetkeibaの各種ランキングのスクレイパーを実装しました。

github.com

取得できるデータは

  • ○歳馬の賞金順
  • ○年度の調教師リーディング
  • ○年度の生産者リーディング
  • ○年度の馬主リーディング
  • ○年度の騎手リーディング

あと、これらのページを補完する情報として

  • 各馬の生年月日、セリ取引価格、通算成績
    f:id:sanshonoki:20180403220124p:plain:w300
  • 母馬の繁殖成績
    f:id:sanshonoki:20180403220020p:plain:w300

も抽出できるようにしました。これらをCSV形式で出力します。

抽出処理自体はBeautifulSoup4を使ってゴリゴリやっていて特段に工夫した点はないですがやや手こずったところもあったのでメモを残します。

競走馬検索のページング

リーディング情報ではGETで http://db.netkeiba.com/?pid=trainer_leading&year=2017&page=2 というようにpage番号のクエリパラメータをつければ簡単に2ページ目以降の情報が簡単に取得できますが競走馬検索ではpageのパラメータがなくこのままでは1ページ目以外取得できません。

取得方法
  1. 最初の検索結果のレスポンスのhtmlにserialが埋め込まれているのでこのデータを取得

     <input type="hidden" name="serial" value="a:19:{s:3:&quot;pid&quot;;s:10:&quot;horse_list&quot;;s:4:&quot;word&quot;;s:0:&quot;&quot;;s:4:&quot;sire&quot;;s:0:&quot;&quot;;s:5:&quot;keito&quot;;s:0:&quot;&quot;;s:4:&quot;mare&quot;;s:0:&quot;&quot;;s:3:&quot;bms&quot;;s:0:&quot;&quot;;s:7:&quot;trainer&quot;;s:0:&quot;&quot;;s:5:&quot;owner&quot;;s:0:&quot;&quot;;s:7:&quot;breeder&quot;;s:0:&quot;&quot;;s:9:&quot;under_age&quot;;s:1:&quot;4&quot;;s:8:&quot;over_age&quot;;s:1:&quot;4&quot;;s:9:&quot;prize_min&quot;;s:0:&quot;&quot;;s:9:&quot;prize_max&quot;;s:0:&quot;&quot;;s:4:&quot;sort&quot;;s:5:&quot;prize&quot;;s:4:&quot;list&quot;;s:3:&quot;100&quot;;s:9:&quot;style_dir&quot;;s:17:&quot;style/netkeiba.ja&quot;;s:13:&quot;template_file&quot;;s:15:&quot;horse_list.html&quot;;s:9:&quot;style_url&quot;;s:18:&quot;/style/netkeiba.ja&quot;;s:6:&quot;search&quot;;s:14:&quot;年齢[4歳~4歳]&quot;;}" />
    

    のような感じで初回の検索条件がエンコードされています

  2. 2ページ目以降はこのserialの値とpageをPOSTパラメータとしてリクエストする

コードとしては以下のようになります。

def getSerial(html):
    soup = BeautifulSoup(html, "html.parser")
    serial = soup.select('input[name="serial"]')

def getPageBySerial(serial, page=2):
    url = 'http://db.netkeiba.com'
    params = {
        'pid': 'horse_list',
        'sort_key': 'prize',
        'sort_type': 'desc',
        'page': page,
        'serial': serial.encode('euc-jp')}
    res = requests.post(url, data=params)
    res.encoding = res.apparent_encoding
    html = res.text
    return html

serial は euc-jpでエンコードしないとうまくいかないのが注意点です。(馬名で検索するときも同様)

謎の挙動。。

一部の○○IIという馬が検索できない

IIがついていてもサンデースマイルIIのように正常に検索できる馬がいる一方でスノーフレークII等、IIがつく数頭の馬で完全一致で検索すると「馬名[スノーフレークII]、年齢[無指定~無指定]では見つかりませんでした。」となってしまいます。

対策

IIを除いた「スノーフレーク」で検索すると正常に検索できるのでIIがつく場合はIIを取り除いて部分一致で検索するようにしました。 ただし、この場合、2頭以上にヒットした場合に以下のように複数の候補が出て来るのでその中から名前が完全に一致する馬のIDを抽出します。

f:id:sanshonoki:20180403220955p:plain

Debit Or Creditで検索すると 「この db.netkeiba.com ページが見つかりません」という結果が返ってくる

普通なら検索でヒットしなかった場合、「馬名[コンナウマイナイヨ]、年齢[2歳~無指定]では見つかりませんでした。」というようにページ自体は表示されるのですがこの馬だけは404のエラーレスポンスだけが返ってきてブラウザが出力する404エラーページが表示されます..。これはサーバーサイドのバグなのでしょうか。。

対策

最後の単語を省いてDebit Orで検索すると404エラーとならずにDebit Or Creditの検索結果がちゃんと返ってきました。 スペースを含む馬名でうまく結果が得られなかった場合は最後の単語を除いて部分一致で検索するように実装しています。

何頭かの候補が返ってきた場合は、○○IIの場合と同じく検索結果の中から名前が一致する馬を探します。


準備は整いました。何か予測できるように頑張らないと。。

lang_model-arxiv.ipynbで必要となるarxiv.csvとall_arxiv.pickleを取得する方法

ニッチすぎてこれが役に立つ人は果たして何人いるのだろう。。という記事ですが少し苦労したのでメモです。

何かと言うと、細々と続けている MOOC講座 fast.ai の part1 v2の lang_model-arxiv.ipynb でソースとなるファイルが見つからない問題です。

問題となる箇所

3つ目のセル

df_mb = pd.read_csv(f'{PATH}arxiv.csv')
df_all = pd.read_pickle(f'{PATH}all_arxiv.pickle')

jeremyがフォーラムのスレッドで次のようにコメントしてますが

You can download an arxiv dataset using this project: https://hackernoon.com/building-brundage-bot-10252facf3d1

このリンクに飛んで探しても all_arxiv.pickle は見つかりません..。

なぜなら、、

このページにリンクが張ってあるリポジトリのコードを自分で実行して作成する必要があるからです。。

arxiv.csvの取得

all_arxiv.pickleの前にまずarxiv.csvから

こちらは https://github.com/amauboussin/arxiv-twitterbot からリンクが張ってあるbrundage_bot.csvをダウンロードしてarxiv.csvにリネームすれば良さそうです

all_arxiv.pickleの取得

続いてall_arxiv.pickleです。 以下の手順で作成できました

  1. 必要なもの

    • python2
  2. プロジェクトを git clone する

     $ git clone https://github.com/amauboussin/arxiv-twitterbot.git
    
  3. 依存ライブラリのインストール

     $ conda install pandas
     $ conda install requests
     $ pip install feedparser
    
  4. job.pyの修正

    tweetに関する箇所は不要なのでコメントアウトします

     from time import sleep
    
     from get_arxiv import check_for_update, update_arxiv
     # from tweet_papers import tweet_day
    
     # if check_for_update():
     #     update_arxiv()
     #     sleep(2)
     #     tweet_day(dry_run=False)
     update_arxiv()
    
  5. get_arxiv.pyの修正

    pklファイルが元から存在している前提だったり、ループが終わらなかったり(?!)、ちょこちょこと修正する必要がありました :-)

    gist.github.com

  6. job.pyを実行する

    時間がかかりますが辛抱強く待つとall_arxive.pickleが作成されます

    $ python job.py
    

これで一応 lang_model-arxiv.ipynbは最後まで動かせました。 私が作成したall_arxive.pickleはここに置いてあるので面倒くさい方は使ってください

Azureマシンのjupyterをローカルから開けるようにする

Azureで仮想マシンを起動すると毎回パブリックIPアドレスが変わるのでsshするときとても面倒です。

さらに、Azureマシン上でjupyter notebookを立ち上げたときも毎回tokenの値が違うのでこれをコピペでブラウザに貼り付け、localhostの部分を自分のIPアドレスDNS名に置き換えるという作業がとてもとても面倒です。

あぁもう面倒すぎる...

ということで、重い腰を上げてAzure CLIを使ってIPアドレスを意識せずにsshログインしたり、jupyterのtokenつきURLをローカルマシン上で開けるようにできるシェルスクリプトを作りました。

github.com

準備

https://github.com/tanakatsu/azure-cli-wrapper-scripts のファイルをダウンロードしておきます

  1. Azureのマシン名やsshキーのパスなどを設定

    azure-env.shを編集して自分の設定に置き換えます

    export vmName=YourVmName
    export resourceGroup=YourResourceName
    export privateKeyPath=PathToPrivateKey # ~/.ssh/your_azure_private_key
    export jupyterRootDir=. # relative path from home directory
    
  2. Azure CLIのインストール

    公式ページに方法が書いてありますが Macの場合は brew update && brew install azure-cli で一発でインストールできます

  3. az loginでログインしておく

     To sign in, use a web browser to open the page https://aka.ms/devicelogin and enter the code GDGSSH8M3 to authenticate. 
    

    のようなレスポンスが返ってくるので指示通り、https://aka.ms/deviceloginを開いて コードを入力します。「デバイスMicrosoft Azure Cross-platform Command Line Interface アプリケーションにサインインしました。...」という画面が表示されればokです

使い方の例

# 環境変数とエイリアスのセットアップ
source azure-env.sh
source azure-alias.sh

# VMを起動する
azure-start

# リモートマシンのjupyter notebookをローカルマシンから立ち上げる
azure-start-jupyter

起動したjupyter notebookをローカルマシンのブラウザで開くときは別のターミナルを使います。

## in another terminal

# 環境変数とエイリアスのセットアップ
source azure-env.sh
source azure-alias.sh

# IPアドレスを取得
azure-ip

# jupyter notebookをブラウザで開く
./azure-open-jupyter.sh

これで毎回変わるIPアドレスを意識することなくローカルマシン上での操作だけでjupyter notebookを使えるようになります (◍•ᴗ•◍)

シャットダウンは

# 終了し割り当てを解除する
azure-stop

Azure CLIaz vm stopだとマシン停止だけで割り当て解除まではされないようで az vm deallocateも同時に行っています。 管理画面で停止ボタンを押すと割り当ての解除までやってくれるのでCLIでも割り当て解除までやってほしいです。。(stopだけだと課金が続いちゃいます ><)

シェルスクリプトの中でやっていること

jupyter notebookをリモートで起動する

今まで知らなかったのですが sshコマンドでは ssh接続時に直接コマンドを実行することもできるようです。

例えば、ssh ユーザー名@ipアドレス ls とすればリモートマシンのホームディレクトリのファイルのリストが返ってきます。

これを利用してjupyter notebookを起動します。

具体的なコードはこちら

alias azure-start-jupyter='ssh -i ${privateKeyPath} -o "StrictHostKeyChecking no" ubuntu@${instanceId} "export PATH=~/anaconda3/bin:$PATH;source activate fastai;cd ${jupyterRootDir};jupyter notebook"'

sshを使ってリモートのコマンドを実行したとき.bashrcは実行されないようでその中でやっているPATHの設定等などは明示的にやってあげる必要があります。

リモートで起動したjupyter notebookのtokenを取得する

jupyter notebook listコマンドで tokenつきのURLが出力されるのでこれを sshコマンドを使ってリモートマシン上で実行します。 あとは sed やら awk やらでローカルのブラウザで開くためのURLを構築します。

具体的なコードはこちら

list=`ssh -i ${privateKeyPath} ubuntu@${instanceId} 'export PATH=~/anaconda3/bin:$PATH;source activate fastai;jupyter notebook list'`
url=`echo $list | grep http | sed -e "s/localhost/${instanceId}/g" | awk '{print $4}'`
echo $url
open $url


sshコマンドのいろいろな使い方に関しては以下の記事が大変参考になりました。 orebibou.com

CUDA9.1の環境でTensorFlowをインストールする

fast.aiのpart1 v2は基本的に PyTorch を使って進めることになりますが Keras+TensorFlow用の notebook(keras_lesson1.ipynb)が1つだけあります。

これも動かしたる! と思ってTensorFlowのパッケージをインストールしようとしたところ、、

part1 v2用の環境はCUDAのバージョンが9.1となっており、Anacondaやpipを使ってGPU対応のTensorFlowを一発でインストールができないことが分かりました。。(CUDA8.0用にビルドされているため)

が、先人の記事を発見し何とかインストールできました。(๑˃̵ᴗ˂̵)و

deep-rikei.hatenadiary.jp

自分の記事の中で書きたいことはただ1つ。

bazel は 0.7.0 を使おう

(↑の記事の中で推奨とされている通り。最新版はダメ、絶対)

というのも、最初は 「p3インスタンス(V100)上でCUDA+CUDNN+Tensorflowを動かすのが大変だったのできろく。 - 焼肉が食べたい」 の記事を発見し、これを参考に作業してたのですが

bazelのインストールでそのまま真似したら

sudo apt-get -y install openjdk-8-jdk
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update && sudo apt-get install oracle-java8-installer
echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list
curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -
sudo apt-get update && sudo apt-get install bazel
sudo apt-get upgrade bazel

の手順で bazel の 0.9.0がインストールされていて、そのまま進めると TensorFlowのビルド時に大量のエラーメッセージ... ><

記事ではその後、TensorFlowのconfigureスクリプトの実行ログで

$ ./configure
You have bazel 0.7.0 installed.

となっているのでその時だときっと bazel 0.7.0 がインストールされたのでしょう。。

CUDAとcudnnをインストールした以降の手順のメモ

fast.aiの環境構築が済んでいるとCUDAとcudnnは既にインストールされているので 先のid:syakai-jinさんの記事の (16) 以降をやればよいです。

  1. bazelのインストール (0.7.0じゃないとダメ。0.9.0だとビルド時にエラーが出た)

     $ wget https://github.com/bazelbuild/bazel/releases/download/0.7.0/bazel-0.7.0-installer-linux-x86_64.sh
     $ chmod +x bazel-0.7.0-installer-linux-x86_64.sh
     $ ./bazel-0.7.0-installer-linux-x86_64.sh
     $ bazel version
    

    bazel のバージョンが 0.7.0 であることを確認しましょう

  2. export PATH=~/bin:$PATH を.bashrcに追加

  3. 必要なパッケージをインストール

    $ sudo apt-get -y install libcupti-dev

  4. TensorFlowのソースコードを取ってきます

     $ git clone -b v1.4.0 https://github.com/tensorflow/tensorflow
     $ cd tensorflow
     $ ./configure
    
    • Do you wish to build TensorFlow with CUDA support? [y/N]: => Yes
    • Please specify the CUDA SDK version you want to use, e.g. 7.0. [Leave empty to default to CUDA 8.0]: => 9.1
    • Please specify the cuDNN version you want to use. [Leave empty to default to cuDNN 6.0]: => 7.0.5

    これら以外は No と ENTER(デフォルトのまま)でよい

    CUDAのバージョンは ls /usr/local/cuda*、cndnnのバージョン確認は ls /usr/local/cuda/lib64/libcudnn.so.* で確認できます

  5. シンボリックリンクを作る

     $ sudo ln -s /usr/local/cuda/include/crt/math_functions.hpp /usr/local/cuda/include/math_functions.hpp
    

    これをしないとファイルがないというビルドエラーになる

  6. ビルド

     $ bazel build -c opt --copt=-march="haswell" --config=cuda //tensorflow/tools/pip_package:build_pip_package
    
  7. パッケージを作成

     $ bazel-bin/tensorflow/tools/pip_package/build_pip_package /tmp/tensorflow_pkg`
    
  8. パッケージをインストール

     $ pip install /tmp/tensorflow_pkg/tensorflow-1.4.0-cp36-cp36m-linux_x86_64.whl`
    

あとは keras と h5py をインストールしましょう。

$ pip install keras
$ conda install h5py # これをインストールしないとweightファイルのload時にエラーが出る