ベイズの定理

ベイズの定理と言えば、赤玉白玉の問題が定番ですがせっかく勉強するので競馬を題材にやってみました。

ベイズの定理の公式

よく見かけるけど、よう分からんという式ですね。。

事象Aを観測したあとで事象Bが発生する確率は


\displaystyle P(B | A) = \frac{P(A | B) P(B)}{P(A)}

と表すことができます。

事象Aに関するデータを観測することで、事象Bの確率が P(B)から P(B|A) へ、より尤もらしい値へと更新される ということを意味しています。

ここで、

  • P(B): 事前確率
  • P(B|A): 事後確率
  • P(A|B): 尤度(ゆうど)

と呼びます。

P(A) は実際には B が取りうるすべての値に対しての P(A | B) * P(B) に関する積分であり、


\displaystyle P(B | A) = \frac{P(A | B) P(B)}{P(A)} = \frac{P(A | B) P(B)}{\sum_{{B}}P(A|B)P(B)}

となります。

P(A)と書くとシンプルに見えますが、実際の計算は大変です。。

その他、

  • P(A, B) = P(A | B) * P(B) = P(B | A) * P(A): 同時確率
  • P(A): 周辺尤度

と言うようです。

また、より学問的な世界では A, Bではなく DもしくはX(データ), θ(分布パラメータ) という表現をするようです。


\displaystyle P(\theta | X) = \frac{P(X | \theta) P(\theta)}{P(X)}

ただ、θ という見慣れない記号を見るだけで急に難しく感じ引いてしまうのは私だけではないでしょう。。

問題設定

では問題設定してみます。

問題

  • 厩舎は成績によってAクラス、Bクラス、Cクラスに分類される
  • 厩舎に所属する馬が2頭連続で勝利した場合、それらの馬がAクラス厩舎所属である確率は?

せっかくなのでリアルな数字でやってみたいと思います。

netkeibaの調教師リーディングのページから 厩舎ごとの勝利数を取得し、勝利数の分布をプロットしてみます。

厩舎の成績データを取得するコードはこんな感じです。(python2)

# encoding: utf-8
from bs4 import BeautifulSoup
import urllib2
import chardet
import traceback
import pickle


def fetchPage(url):
    req = urllib2.Request(url)
    res = urllib2.urlopen(req)
    body = res.read()
    guess_enc = chardet.detect(body)
    try:
        unicode_html = body.decode(guess_enc['encoding'])
    except UnicodeDecodeError:
        print url, guess_enc
        print traceback.format_exc()
        unicode_html = None

    if not unicode_html:
        try:
            unicode_html = body.decode('shift_jisx0213')
            print 'Decoding in shift_jisx0213 is successful.'
        except UnicodeDecodeError:
            print traceback.format_exc()
            unicode_html = body

    return unicode_html


def getTrainerResult(html, offset=0):
    results = []
    soup = BeautifulSoup(html, "html.parser")
    trainer_leading_table = soup.select("table.race_table_01 tr")
    for i, row in enumerate(trainer_leading_table[2:]):
        # print row
        data = row.select("td")
        name = unicode(data[1].string)
        stable = data[2].string
        win_count = int(data[4].string)
        second_place_count = int(data[5].string)
        third_place_count = int(data[6].string)
        unplaced_count = int(data[7].string)
        grade_race_win = int(data[9].string)
        stakes_race_win = int(data[11].string)
        not_stakes_race_win = int(data[13].string)
        if stable != u'栗東' and stable != u'美浦':
            continue
        result = {'no': i + offset,
                  'name': name,
                  'win_count': win_count,
                  'second_place_count': second_place_count,
                  'third_place_count': third_place_count,
                  'unplaced_count': unplaced_count,
                  'grade_race_win': grade_race_win,
                  'stakes_race_win': stakes_race_win,
                  'not_stakes_race_win': not_stakes_race_win}
        results.append(result)
    return results


leading_results = []
for i in range(4):
    page = i + 1
    html = fetchPage("http://db.netkeiba.com/?pid=trainer_leading&year=2017&page=%d" % page)
    # print(html)
    results = getTrainerResult(html, offset=len(leading_results))
    # print(results)
    leading_results += results

print(leading_results)
print(len(leading_results))

with open("trainer_leading.pkl", "w") as f:
    pickle.dump(leading_results, f)

成績のヒストグラムをプロットすると次のようになりました。(X軸:勝利数、Y軸:頻度)

f:id:sanshonoki:20171227094444p:plain

このヒストグラムを見て次のように3つのカテゴリに分けます。

  • Aクラス: 25勝以上 (35厩舎)
  • Bクラス: 11勝以上25勝未満 (110厩舎)
  • Cクラス: 10勝以下 (51厩舎)

それぞれのクラスでの平均勝率は計算して以下のようになりました。

  • Aクラスの勝率: 11.8%
  • Bクラスの勝率: 6.7%
  • Cクラスの勝率: 3.3%

求めるものは 事後確率 P(Stable=A | win,win) となります。

ベイズの定理を使って事後確率を計算する

事前確率を計算する

まず、事前確率を求めます。

厩舎の分布から計算します。

P(Stable=A) = 35 / (35 + 110 + 51) = 18%
P(Stable=B) = 110 / (35 + 110 + 51) = 56%
P(Stable=C) = 51 / (35 + 110 + 51) = 26%

尤度を計算する

次に、尤度を計算します。

各クラスでの勝率は

P(win | Stable=A) = 11.8%
P(win | Stable=B) = 6.7%
P(win | Stable=C) = 3.3%

なので、

2勝するという確率は

P(win,win | Stable=A) = 0.118 * 0.118 = 0.013923999999999999
P(win,win | Stable=B) = 0.067 * 0.067 = 0.004489000000000001
P(win,win | Stable=C) = 0.033 * 0.033 = 0.0010890000000000001

となります。

P(win, win)を計算する

とりうる厩舎クラスに対して同時確率を積分します。

P(win,win) = P(Stable=A) * P(win,win | Stable=A) 
                      + P(Stable=B) * P(win,win | Stable=B) 
                      + P(Stable=C) * P(win,win | Stable=C)
    = 0.005303300000000001

事後確率を計算する

ベイズの定理に従って事後確率を計算します。

P(Stable=A | win,win) = P(win,win | Stable=A) * P(Stable=A) / P(win,win)
    = 0.013923999999999999 * 0.18 / 0.005303300000000001
    = 0.47259630795919505

約47%となりました。

何も情報がない状態では ある馬が Aクラスの厩舎の所属馬であるかどうかは 厩舎数の分布から見積もった 11.8% ということになりますが 勝利というデータを観測することによってその確率は 47% に高まります。

1勝の場合

2勝ではなく1勝だったときの事後確率 P(win | Stable=A) は

P(win | Stable=A) = 11.8% = 0.118
P(win | Stable=B) = 6.7% = 0.067
P(win | Stable=C) = 3.3% = 0.033

P(win) = P(Stable=A) * P(win | Stable=A) + P(Stable=B) * P(win | Stable=B) + P(Stable=C) * P(win | Stable=C)
    = 0.06734000000000001

P(Stable=A | win) = P(win | Stable=A) * P(Stable=A) / P(win) 
    = 0.2988416988416988

となり、約30% となります。

勝利が続くことでAクラス厩舎である確率がより高まることが分かります。(30% → 47%)

分母は無視することが多い(らしい)

P(win, win) は 積分した結果であり P(Stable=A) を変数として含まないので分母は無視して計算することが多いようです。

実際に計算してみます。

分子のみを計算し、比率を求めます。

P(win,win | Stable=A) * P(Stable=A) = 0.013923999999999999 * 0.18 = 0.00250632
P(win,win | Stable=B) * P(Stable=B) = 0.004489000000000001 * 0.56 = 0.0025138400000000007
P(win,win | Stable=C) * P(Stable=C) = 0.0010890000000000001 * 0.26 = 0.00028314000000000003

P(Stable=A | win,win) = P(win,win | Stable=A) * P(Stable=A) / 
                                     (P(win,win | Stable=A) * P(Stable=A) 
                                   + P(win,win | Stable=B) * P(Stable=B)
                                   + P(win,win | Stable=C) * P(Stable=C)) 
    = 0.47259630795919505

分母も使って計算したときと同じ47%になりました!

AzureのGPUインスタンスを立てる

Azureのアカウントを持っているのでAzureでもGPUインスタンスを立ててみました。

fast.ai に Azure用GPUインスタンスセットアップのドキュメントスクリプトが用意されていたので基本的には楽チンにセットアップができます。

が一部ハマったのでメモ

install-gpu-azure.sh
# 8行目あたり
# sudo apt-get -y install cuda
sudo apt-get -y install cuda-8-0 # http://forums.fast.ai/t/setup-problems-azure/5879

# 22行目あたり
# pip install theano
pip install theano==0.9.0

CUDA8.0を明示的に指定しないとCUDAの最新版(CUDA9.0)がインストールされて次のエラーが出ちゃいました。

Exception: The nvidia driver version installed with this OS does not give good results for reduction.Installing the nvidia driver available on the same download page as the cuda package will fix the problem: http://developer.nvidia.com/cuda-downloads

Theanoも同様にバージョン指定してやる必要がありました。 バージョンを指定しないと 1.0.0 がインストールされて実行時に次のエラーが出ます。

ValueError: You are tring to use the old GPU back-end. It was removed from Theano. Use device=cuda* now. See https://github.com/Theano/Theano/wiki/Converting-to-the-new-gpu-back-end%28gpuarray%29 for more information.

1.0.0用の設定や修正をすれば動くようになると思いますが触らぬ神に祟りなしということで0.9.0を入れることとしました。

AWSと比べて

スペック比較

Azure (NC6) AWS (p2.xlarge)
vCPU 6 4
RAM (GiB) 56 61
GPU 1 (Tesla K80) 1 (Tesla K80)
Price ¥110.57/hour 0.900 USD/hour

https://azure.microsoft.com/ja-jp/pricing/details/virtual-machines/windows/ https://aws.amazon.com/jp/ec2/instance-types/p2/

ほぼ同等のスペックのはずなのですが学習を実行させるとなぜかAzureが遅かったです。。(後述)

課金体系

ちょっと前まではAWSが時間単位での課金だったので圧倒的にAzureに軍配が上がっていましたが

jp.techcrunch.com

の記事にあるようにAWSが10/2から秒単位での課金になったようです。

Azureは分単位ですが趣味の勉強の範囲では分単位か秒単位かはあまり気になりません。 でも、ビジネスとしてプロダクトで使うときは気にするんだろうなと思います。

CNMEM

http://forums.fast.ai/t/performance-issues-in-azure-800-seconds-for-lesson-1/2815/3 にも書いてあるようになぜか学習にかかる時間が遅い・・・ ╮(•ω•)╭

[global]
device = gpu
floatX = float32
[lib]
cnmem = 0.95

な感じで .theanorc を編集し、CNMEMを有効にするとAWSとほぼ同等の速度になりました。

Lesson1の The punchline: state of the art custom model in 7 lines of code の学習時間の比較
Azure (NC6) AWS (p2.xlarge)
CNMEM disabled 1107 sec 772 sec
CNMEM enabled 95% 686 sec 647 sec

AzureはVMのディスクタイプでSSDを選ぶとGPUインスタンスであるNC6インスタンスが選べず、 GPUを使いたい場合はHDDしか選択できないのですがそのせいなんでしょうか??

毎回IPアドレスが変わる

https://blogs.technet.microsoft.com/mskk-cloudos/2016/04/06/azure-ip/

● パブリックIPアドレスの値を決めることはできない 「パブリックIPアドレス」ではIPアドレスの値そのものを利用者が決めることはできません。

とあるので固定IPは無理のようです。

ただ、DNSを設定することはでき

http://[YourDnsName].eastus.cloudapp.azure.com:8888/というようなURLで jupyter notebookを開けます。

DNSの設定
  1. リソースからパブリックIPアドレスを選択
  2. 左側にある 構成 をクリック
  3. DNSラベルをつける

ディスクサイズが小さい

デフォルトでは30GBになります。 一見、十分そうですがfast.aiの課題をやっているとすぐに枯渇しちゃいます。 dogscatsの次のデータセットのstate-farmをダウンロードして展開しようとしたら既に100%になってしまいました。。

ディスクサイズの増やし方

超簡単です。

  1. リソースからディスクを選ぶ
  2. サイズを変更し、保存する (シャットダウンした状態で)
  3. VMを再起動する

Step by Step: how to resize a Linux VM OS disk in Azure (ARM) – Cloud Solution Architect

CentOSとか他のOSだと作業がいろいろとあるようですがubuntuだとこれだけです。 ubuntu素敵。( •ᴗ•)

Jupyter Notebookを印刷するときにレスペーパー化する

fast.aiの Practical Deep Learning for Coders は Jupyter Notebookで教材が提供されています。 コードは紙に出力したい派なのでipynbファイルを紙に印刷してそれを見ながら勉強しています。

ただ、

以下のようなTraining時の長い出力がダラダラと含まれていたりで

f:id:sanshonoki:20171115045932p:plain:w400

俯瞰できずに読みにくいし、印刷ページも無駄に増えて何か勿体ない ! ><

というのがあり、印刷時に不必要な部分をカットできるツールを作りました。

github.com

具体的には Trainingの出力のうち、最初と最後のepochの出力だけを残して後はカットします。

このツールを使うと、先ほどのページが

f:id:sanshonoki:20171115050218p:plain:w400

というような感じになり、とてもスッキリ〜 ( •ᴗ•)

MNISTのノートブック、mnist.ipynb が 何と 13ページ → 8ページ に! w

印刷代も30円節約できます。。

いきなりHerokuにRailsアプリがデプロイできなくなった..

密かに運営している POG集計サービス「Poger mailing service」の中で行っているスクレイピング処理が今日から誤動作し始めちゃいました。。

これはhtmlの出力仕様が変わったに違いない! (当たり)

急いで不具合を修正して、

Herokuに git push heroku master でいつものようにデプロイしたら

Unable to negotiate a key exchange method

というエラーが出てデプロイできなくなってしまった..! Oh, my god...

何だコレ?

エラーメッセージで検索してみると 公式ページに 「弱い暗号化アルゴリズムは使えなくした」とある。

https://devcenter.heroku.com/changelog-items/1311

We strengthened the SSH transport for our Git service by disabling the following algorithms:

解決法

https://stackoverflow.com/questions/46834011/suddenly-cant-push-to-heroku-unable-to-negotiate-key-exchange-method にあるように

$ brew install openssh

で解決 !!

ふぅ、焦った。。

スマートビームレーザーがすごい

超小型レーザープロジェクター Smart BEAM Laser を買いました。

f:id:sanshonoki:20171105074603j:plain

これがめっちゃよい

焦点調整不要! 電源ケーブル不要! 無線接続で投影可能! ƪ(•◡•ƪ)"

画質はそこそこですが他の方のレビューでもかなり評判高いです。

無線接続の仕方

公式のFAQページでも書いてありますが iOSAndroid で 使用モードが異なる ので注意! (モード切り替えは電源ボタンを2回連続押し)

少しハマってしまいました。。

分かりにくいですね..

iOS (iPad)
  • Media Sharing モード にする
  • WiFi は SMARTBEAMLASER-○○ につないでおく
  • 起動したアプリから AirPlay する
Android
  • Media Mirroring モード にする
  • WiFiはONにする (無線機器につながっていなくてもok)
  • Miracast する (使い方は機種によって大分違う。マニュアルを見るべし)
  • 4GやWiFiつないだ状態でも使える

Android だと 4GやWiFiつないだ状態でもミラーリングができるので YouTubeなどオンラインコンテンツも投影できます。Google Play等で購入したビデオは再生するのにインターネット接続が必要なのでこれはありがたいです ( •ᴗ•)

台形補正

アプリストアから「SmartBeamLaser」アプリをインストールすると台形補正ができます。 これで上下方向±15°くらいは補正できます。補正できるレンジは大きくなく、斜めの投影は補正できないのでおまけの域は出ませんがないよりはいいかなと思います。

f:id:sanshonoki:20171102215322p:plain:w250

アプリは Media Sharingモードじゃないと本体に繋がらないので Android のときは切り替えが必要でやや面倒です。

あと、メニュー画面を見ても分かるようにアプリがあると音量もコントロールできます。

Amazonの星の評価...

今回、Amazonで買ったのですがショップから評価依頼が来てビックリ。

一般的なAmazonの評価基準につきましては、 良いと思われる場合は、星5つ 普通と思われる場合は、星4つ 悪いと感じられた場合は、星3つ 非常に悪い場合には、星2もしくは1が、一般的なAmazonでの評価と判断されます。

え、普通が 4 なんですか?!

マジ?

星3つが普通だと思っているたくさんの人がいてここに星4つを普通だとする人が混ざってくると、みんなが「普通」だと思っているものが 3.0ではなく 3.0 + α と評価がかさ上げされてしまう... ╰(゚x゚​)╯

Amazon、恐ろしい。。

fast.aiでTensorFlowをバックエンドとして使う

fast.ai はKerasのバックエンドとしてTheanoを前提としていますがTensorFlowも使うことができます。

~/.keras/keras.json

{
    "image_dim_ordering": "tf",
    "epsilon": 1e-07,
    "floatx": "float32",
    "backend": "tensorflow"
}

とすればTensorFlowになり、特にエラーもなくJupyter Notebookを実行できます。

が、

これだけではうまくいきません。。

なぜか、学習したときの精度が低くなっちゃう.. ><

Lesson1のThe punchline: state of the art custom model in 7 lines of codeを実行すると、

Theano TensorFlow
val_acc: 0.9825 val_acc: 0.9190

となります。。

原因

VGG16モデルの転移学習用の重みデータ vgg16.h5vgg16_bn.h5 が Theano用になっているからです。

KerasのWikiページ Converting convolution kernels from Theano to TensorFlow and vice versa · fchollet/keras Wiki · GitHub に重みファイルの変換の方法が記載されていますが

どうやらKeras v2用のコードのようでfast.aiで使用するKeras v1だとエラーになります。

Keras v1での重み変換

ここのコードを参考にしてうまく変換できました。:-)

実装したものはこちら gist.github.com

このコードで作った vgg16.tf.h5vgg16.h5 の代わりに使うようにして TensorFlow バックエンドで学習を実行すると、

Theano TensorFlow(重み変換前) TensorFlow(重み変換後)
val_acc: 0.9825 val_acc: 0.9190 val_acc: 0.9840

期待通りの精度が出るようになりました。( •ᴗ•)

utils.py の修正とバックエンド切り替えスクリプト

このままでは Theano と TensorFlow のバックエンドを頻繁に変えたいときに面倒なので utils.py を少し修正します。

== 46行目あたり ==

if K.backend() == 'theano':
    import theano
    from theano import shared, tensor as T
    from theano.tensor.nnet import conv2d, nnet
    from theano.tensor.signal import pool

のように修正し、バックエンドが Theano のときだけ Theanoのライブラリを読み込むようにします。

さらに、次のようなシェルスクリプトを作ると

switch_keras_backend.sh

#!/bin/bash

if [ $# -ne 1 ]; then
  echo "ERROR: Wrong arguments"
  echo "Usage: switch_keras_backend.sh th|tf"
  exit 1
fi

if [ $1 != 'th' ] && [ $1 != 'tf' ]; then
  echo "ERROR: Wrong parameter. Parameter must be 'th' or 'tf'."
  echo "Usage: switch_keras_backend.sh th|tf"
  exit 1
fi

backend=$1
if [ $backend = 'th' ]; then
  echo "Use theano as backend"
elif [ $backend = 'tf' ]; then
  echo "Use tensorflow as backend"
fi

cd ~/.keras
ln -nsf keras.$backend.json keras.json

cd ~/.keras/models
ln -nsf vgg16.$backend.h5 vgg16.h5
ln -nsf vgg16_bn.$backend.h5 vgg16_bn.h5

echo 'Done!'

重みファイルも簡単に切り替えができます。

vgg16.h5keras.json はそれぞれのバックエンド毎に用意したファイルへのシンボリックリンクにしておく必要あり)

Theanoのインストール

fast.ai の Practical Deep Learning For Coders, Part 1 は Keras v1(バックエンドはTheano)を使用します。

なので、早速 Ubuntu14.04に Theanoをインストールしていきます。

インストール

公式ページ に書いてある通り、以下の1行で楽々インストール

$ conda install theano pygpu

.theanorcに手こずった..

しかし、やはりというか設定関係で少し苦労..。

ubuntu14.04 theanoとかgpgpu環境構築メモ - 備忘録とか日常とか を参考にして

cnmen = True としたら ValueError: could not convert string to float: True というエラーが出た。。

どうやら cnmem = True とするのはTheano 0.7までのときの設定のようで Theano 0.8からは cnmem = 1 のように float型の値を入れるらしい。

https://github.com/Theano/Theano/issues/4170

ところが、cnmem = 1 としたら先のエラーは解消したもののconvolutionを使った学習実行時に

RuntimeError: ('The following error happened while compiling the node', GpuDnnSoftmax{tensor_format='bc01', mode='channel', algo='log'}(GpuContiguous.0), '\n', 'could not create cuDNN handle: CUDNN_STATUS_INTERNAL_ERROR')

という新たなエラーが...! (。ŏ﹏ŏ)

いろいろ試してみると cnmem = 0〜0.9 までだったら問題ないことが分かりました。

公式ページの http://deeplearning.net/software/theano_versions/0.9.X/library/config.html#config.config.lib.cnmem を読む限りは

cnmem = 1cnmem = 0.95 でも設定としては大丈夫そうなのだが..

CNMeM

cnmem = 0 (CNMeM is disabled)がよいのか? cnmem = 0.9 (CNMeM is enabled with initial size: 90.0% of memory)がよいのか?

公式ページによると

Controls the use of CNMeM (a faster CUDA memory allocator). Applies to the old GPU backend CUDA backend up to Theano release 0.8.

This could cause memory fragmentation. So if you have a memory error while using CNMeM, try to allocate more memory at the start or disable it. If you try this, report your result on :reftheano-dev.

とあり、

  • CNMeMを使うとCUDAのメモリ割り当てが早くなる
  • ただし、メモリの断片化を引き起こす可能性がある

とのことです。

自分の環境(GeForce GTX 950)でLesson1の「The punchline: state of the art custom model in 7 lines of code」 のコードを使って比較してみたところ

cnmem = 0 (CNMeM disabled) cnmem = 0.9 (CNMeM enabled)
約806s / epoch 約785s / epoch

と 実際に CNMeMを有効にしたほうが速くなりました。:-)

Making Theano Faster with CuDNN and CNMeM on Windows 10 – Ankivil の記事でも CNMeM + CuDNN が速いという報告になっています。(赤い実線に注目)

https://ankivil.com/wp-content/uploads/2016/08/cuddn_cnmem_combined_graph_vgg16.png (図は https://ankivil.com/wp-content/uploads/2016/08/cuddn_cnmem_combined_graph_vgg16.png から引用)