脳からの知識蒸留(Distilling the Knowledge from a Brain) – 結果 –

前回からの続き。脳からの知識蒸留を目指し実験を行った。目的は効率的なハンドラベリングであり、今回のPoCでは生体情報をDeep Learningの蒸留と同じ方法、ソフトターゲットの設定で活用できるか?を検証した。

解いた問題と前提

データセット

脳からの知識蒸留を目指すため、前回作成したツールを用いて約330枚のバラの写真に対するハンドラベリングを行った。ハンドラベリング時に取得したデータは次の通り。

  • 病気の区分(黒星病・うどん粉病・健康)
  • 病気の進行度(軽症・中程度・重症)
  • 脳波(集中度を利用)
  • 分類にかかった時間(集中度の平均化のために使用)

元データはバラの病気診断サイト用に収集したもので、黒星病・うどん粉病・健康が1/3ずつとなるよう調整し、ハンドラベリングを実施した(各クラスのデータ数は同じ)。進行度は軽症が半分、中程度以上が半分な感じだが、データセット内の進行度の割合は調整していない。

モデル・学習の概要

今回は3クラス(黒星病・うどん粉病・健康)分類問題を(Convolution層+Pooling層)×2+分類用の層×2なCNNで解くシンプルな問題設定・モデルとした。転移学習や事前学習は行っていない。

脳からの知識蒸留が有効かを確認するため、下記4つのデータで学習し結果を比較した。

  1. 病気区分のラベルのみを用いた学習(普通の学習)
  2. 病気区分のラベルと進行度を併用した学習。病気進行度が高いほど病気区分ラベルの確信度が高くなるようにした。
  3. 病気区分のラベルと脳波を併用した学習。集中度が低いほど病気区分ラベルの確信度が高くなるようにした。(難しく考えなくても分類できたと言う意図[1])
  4. 病気区分のラベルと進行度と脳波を併用した学習。2.と3.の掛け算。

データセットを学習用75%・評価用25%に分割し、2エポック後の評価用データに対する正解率を比較した。学習データ・評価データに含まれる写真は4条件すべてで同一である(4条件でデータ分割による有利不利は生じていない)。loss関数として1.ではcategorical_crossentropyを、2.-4.ではkullback_leibler_divergenceを用いた。これは、2.-4.の正解データが教師の出力(本件では人間の確信度に相当する分布)でありバイナリ値ではない為である。

結果とまとめ

結果は次の通りであった。驚くべきことに[2]、Distilling the Knowledge from a Brainには効果があった。

  1. 通常の学習:正解率 37%
  2. 進行度の併用:正解率 37%
  3. 脳波の併用:正解率 41%
  4. 進行度+脳波の併用:正解率 49%

結果の解釈は難しいが、正解ラベル以外の情報(特に脳波)にも意味がありそうな感じである。データ数が少なく、そもそもの正解率が低いので何ともいえない感もあるので、今後データ数を増やして再度実験を行ってみたいところ。以下、硬い感じのまとめ。

AIが流行るにつれてハンドラベリングの重要性も上がっている[3]。本PoCではハンドラベリング時に脳波を測定し、それをモデル学習時に使用することで学習の効率化が出来る事がわかった。今後のラベリング作業では脳波を測定することがスタンダードになるだろう[4]。分類時の脳波付きデータセットが広く公開されることを期待する[5]。そのようなデータセットのもと、Distilling the Knowledge from a Brainの活用や脳波予測タスクをマルチタスクの1つとして解く学習によって、他のタスクの精度が上がっていくと推測される[6]。

(硬いまとめはここまで。個人的な思い的な考察はその他に続く。)

脚注

[1] この仮定は相当怪しい。

[2] こんな雑な問題設定・解き方で差が出るとは思わなかったが、複数回実行しても結果がほぼ同じであった。同じモデルにtrainを繰り返していないか確認したり、1.-4.の学習順番を変えてみたりもしたが同じ結果だった。びっくり。観測者効果的なもので脳波が変わったのだろうか?それはそれでびっくりだが。

[3] これはたぶん本当。実務では大きな課題。

[4] 脳波計測がスタンダードにはならないだろうが、取りやすい生体データが併用される可能性は感じた。特に心拍とか視線とか。

[5] 欲しい人がいれば今回のデータを公開してもよいかなーと思いつつ、雑にやったところを綺麗にするのが面倒なので、お蔵入りになりそうな予感がしている。

[6] 個人的にマルチタスクへの適用に可能性を感じている(参考論文「One Model To Learn Them All」)が、良い感じのデータが無いので試せていない。暇があったらやるかも。

Read more »

脳からの知識蒸留(Distilling the Knowledge from a Brain) – 準備-

知識の蒸留(Knowledge Distillation)とは?

Deep learningの世界では知識の蒸留(Knowledge Distillation)が行われている。蒸留というと非常にかっこよい響きなのだが、やっているのは「大きなモデル」を用いて「小さなモデル」を「効率的に学習・構築」することである。

バラの病気診断モデルは以前紹介したように次の手順で構築した。

  • データ(写真)を集めて、ラベル(病気の有無など)を人が設定する(ハンドラベリング)。
  • ハンドラベリングしたデータを用いて、教師あり学習を利用し、植物の葉が病気か否かを判別する多値分類モデルを構築する。

上記で構築したバラの病気診断モデルはInception V3をベースにしておりネットワークの規模が大きい(ネットワーク規模の情報)。すなわち、高精度だが低速度である。バラの病気診断モデルをスマホに展開したい場合、若干精度を落としてでもネットワーク規模が小さく高速なモデル(例えばMobileNet)を使いたくなる。普通はデータだけ再利用してモデルは学習しなおすというプロセスが必要となるが、蒸留を用いると効率的な構築が可能となる。ざっくりとした仕組みは次の通りである。

  1. 病気診断モデル(Inception V3 / 先生)に学習用画像データを入力し、その画像に対する「健康・黒星病・うどん粉病・その他カビ系の病気」の4カテゴリの確率を得る。
  2. 病気診断モデル(Inception V3 / 先生)が出した4カテゴリの確率情報も用いて病気診断モデル(MobileNet / 生徒)を学習する。

普通は健康か否かという0/1の情報で学習するが、高精度モデルが診断した確率にも有用な情報が含まれているので、それを利用しようというアイデアである。実際に蒸留は効果があり、モデル圧縮をする場合によく用いられている。詳細はIntelの記事が良くまとまっていて、kerasを用いた実装例も載っている。

脳からの知識の蒸留(Distilling the Knowledge from a Brain)

バラの病気診断サイトを作ったときに時間がかかったのは、病気か否かのラベリングである。一般的に人間によるラベリングは高コストであり、しかも、間違いが含まれている。ハンドラベリングをしたことがある人ならわかると思うが、現実のデータには判定に困る画像も多い。加えて判定基準は人によって異なっている。同症状の画像に異なるラベルが貼られていることは少なくない。

ハンドラベリングは教師である人間から、生徒であるDeep Learningな各モデルへの情報伝達に他ならない。ということは蒸留の仕組みも利用できるはずである。人間が下した0/1の判断だけではなく、それに付随する判断確率の情報を用いれば高速・高精度なモデルが構築できるに違いない。問題は判断確率の分布情報をどうやって得るかであるが、近年のテクノロジーによって解決できる。すなわち脳波を計れば良い[1]。

脳波の測定とラベリング

今回、脳波の測定はNeuroSkyのMindWave Mobile 2で行った。このデバイスを用いてハンドラベリング時の脳波をはかり、その値をラベルの確度の一つとして利用する。MindWave Mobile2は1.5万円くらいで購入でき、python+thinkgearを用いてデータの取得が可能である(serialではなくpyserialが必要なことに注意)。なお、本件はMacで実施したのでWindowsではやり方が異なる可能性がある。

ライブラリはpipから導入できる。

pip install thinkgear
pip install pyserial

ライブラリ導入後、pythonを用いて

device = '/dev/tty.MindWaveMobile-SerialPo'
tg = ThinkGearProtocol(device)
for pkt in tg.get_packets():
  for d in pkt:
    if isinstance(d, ThinkGearAttentionData):
      val = d.value
      now = datetime.datetime.now()
      print("{},{}".format(now, val))

と言う感じでデータが取得できる(上記の例では脳波というよりは集中度をとっている)

今回は下画像のようなラベリング用簡易WEBアプリに脳波取得ロジックを組み込み、ラベリング実施時の脳波(特に集中度)を計測・保存した。ハンドラベリング時には0/1な病気判定だけでなく、その病気の進行度も選べるようになっており、迷いが生じると脳波に現れる[2]。モデル構築の学習時に脳波情報を併用することで、Deep Learning部の収束が早くなれば、脳からの知識蒸留ができた[3]と言えるのではないか。

実験の結果は次ページで報告。

脚注

[1] 別に脳波である必要は無い。むしろ、脳波は計測が難しく、このような用途には適していない。

[2] 実際のところ現れたと言えば現れたが本当に脳波なのかはかなり疑問である。前述の通り、心拍とか血圧とか目線とか判断までの時間とか脳波よりも測りやすくて効果がありそうな指標は多数存在する。

[3] 本件が誇大広告であることは認識している。が、Deep Learning関係の研究論文から著しく外れた言葉使いはしていない(と思う)ので激しい突っ込みは勘弁してください。

AI診断の信頼性:XAI(Explainable Artificial Intelligence)とLIME

これまで3回にわたって、AIで植物の病気を診断するサイトを作った時の話をまとめた。「その他」の所に「作ったモデルにイマイチ自信ないわー」と書き続けた気がするので、(正しく)Deep Learningな人工知能(AI)を構築する難しさと、その対応について記事にしてみた。

Deep Learningを使用したモデルはイマイチ信頼できない

ラノベのタイトルみたいだなーと思いつつ・・・。色々な場所で言われている通り、Deep Learningを使用した判別モデルが実際のところ何を行っているかを知るのは簡単ではない。AIに限らずだが、モデル作成を長くやっているとクロスバリデーションで高精度なモデルが現実世界では全く使えない状況に遭遇する。特に、ドメイン知識から考えてモデルに納得感が無い場合、「評価設計が間違えている」「リーク情報(leakage) を拾っている」「データのバイアスが影響している」事が原因で、現実問題に対して有効でない事が多い。納得感はまっとうなモデルを作る上でとても重要である。私は何をやっているかわからない(説明可能性がない、納得感のない)モデルを用いることに恐怖感を感じるし、それを信頼して使うことは出来ない。(そーいうこともあり、でぃーぷらーにんぐは、しょうじき、しごとではあんまつかいたくない。(画像が対象ならいろいろ確認しつつ使うけど・・・。))

Deep Learningを説明する取り組み

Deep Learningのような複雑なモデルを説明する研究が進んでいる。アプローチとしては、Deep Learningの各層がどのような入力に強く反応するかを調べる方法が有名で、Feature Visualizationに詳しい。その他の手法として、入力を変化させながらモデル出力の変化を見る方法がある。前々回紹介した「“Why Should I Trust You?”Explaining the Predictions of Any Classifier」で提案されたLIME(Local Interpretable Model-Agnostic Explanations)は、入力近傍の動きを探ることでモデルの説明を行う方法である。ブラックボックスなモデルであっても、妥当な動きが見られれば信頼できる、少なくとも、信頼を得る材料にはなるという考え方である。

LIMEを試してみた

LIMEを行うソフトウェアは

pip install lime

でインストールできる。LIMEはモデル構築手法を選ばず適用できるが、scikit-learnやkerasを使っていると利用しやすい。植物の病気診断サイトはkerasを用いたモデル構築を行っており、limeのチュートリアルに沿って診断根拠を把握することができる。

下の画像は典型的な黒星病の葉であり、植物の病気診断サイトでは99%の確率で黒星病であると判定される。

これは正しい判断であるのだが、正しく画像を判定して黒星病と診断しているのか、たまたま黒星病と判定しているのか、何らかのリーク情報を拾っているのかはパッと見はわからない。LIMEを利用すると、AIがどこに反応しているかある程度把握できる。LIMEを利用した結果は下記のようなものとなる。(画像の大きさはモデルの前提にあわせて変更している。)

上の画像では黒星病と判定するにあたって注目した部分を黄色で囲んでいる。画像中、左部分と中央部分は黒星病診断で重要となる黒の斑点を診断根拠して提示しており、納得感がある。一方で右上のバラのトゲ部分を診断根拠している点には違和感がある。これは「黒星病の写真にトゲが写っている画像が多く、診断にあたりトゲに注目するモデルができている」「黒星病を(トゲがある)バラ特有の病気だと認識した」などと解釈できる。LIMEの結果は人が解釈する必要があり、それにはドメイン知識とモデル(データセット含む)の知識が必要となる。本件ではデータセットのほとんどがバラの写真であることから、前者の疑いが強い。

上記は「画像は健康な葉か?」を診断した結果であり、緑の部分は「この葉が健康であると診断する場合、ポジティブな要素として用いる部分」、赤の部分は「この葉が健康であると診断する場合、ネガティブな要素として用いる部分」を示す。画像上部の緑一色の部分が「健康そう」とした根拠、黒星病の特徴である黒の斑点が「健康でなさそう」という判断根拠とされている。この点は納得感があるが、地面部分も「健康でなさそう」という判断根拠とされていて、違和感がある。「地面の画像とともに健康な葉が提示されている画像が少なく、黒の斑点以外に注目してしまう」「茶色の地面を”枯れている”と誤認識した」などの理由が考えられる。

LIMEを利用することで、AIがどのように診断しているかをある程度可視化することができ、納得感が無い場合はどのように修正するかを判断できる。本件ではおそらくデータセットに問題がある。一応気をつかって撮影したはずのデータセットを用いていても、このような問題を内包している。きちんとしたAI構築は簡単ではない。(コードを書いて実行するだけなら簡単だけど・・・。)

XAIの重要性

説明可能なAIをXAI(Explainable Artificial Intelligence)と呼ぶ。前述のLIMEはXAIを実現するための1手法としても位置づけられる(完璧とは言いがたいが)。XAIは、AIが実務で使用されるにつれ重要となっていくテクノロジーであり、私は2つの側面があると考えている。

  1. モデル構築者の不安を解消するテクノロジー
  2. 社会にAIが受け入れられるためのテクノロジー

1.の観点は前述の通りで、ある程度説明性がないと、構築したAIが現実でうまく動くのか不安で仕方ない私のような人を手助けするモノである。(が、この点を重視する人はほぼいない)

2.の観点は重要である(というか本来的な意味はこっちの方である)。これに関連し、モデルの判断について「説明を受ける権利(right to explanation)」があるとする考え方もあり、GDPR2018でも話題になった。GDPRにおいて「説明を受ける権利」が存在しているかは議論がある(左記が整理された論文その日本語の解説)が、この手の権利が今後重視されることは間違いない。

公平性、FADM(Fairness Aware Data Mining)

公平性の観点からも判断理由の説明は重要である。例えば、「この家を買うと、収入に比べて、借入額が多すぎます。そのため、あなたにはお金を貸しません。」という判断には納得感がある。ある意味、相手のことを思い「無理な返済計画の後、破産しないよう」判断しているとも言える。一方で「あなたの名前は怪しいので、お金を貸しません。」という判断には納得できないだろう。不利に判断される名前が、ある地域に特有のものであれば、差別に繋がる判断として大問題となる可能性すらある。

Deep Learningだ、AIだ、人工知能だ、とテクノロジーを使って何らかの判断をするのは良いのだが、データセットや手法に依存して、差別的なモデルが作成される可能性がある。このようなモデルが運用されると、差別を助長する方向で判断がなされ、その拡大につながりかねない。クロスバリデーションで確認した、統計分析した、など理由があっても、現実世界で「差別的な判断」は受け入れられないだろう。

前述のお金を貸す例でいうと、差別は下記のように広がる。

  1. 特定の名前の人(複数)がお金を返さなかったデータが存在する状況下で
  2. 何も考えずに、名前も判断材料とする「AI」を作り、運用すると
  3. 「AI」は特定の名前の人に不利な結果を返すため、その名前の人に提示される金利は上がり
  4. 特定の名前の人の破産率は上がって、1.に戻る

名前には地域や生まれ年(=年齢)が反映されており年収と関連する。「名前」を入れることで精度が上がる「AI」は作成可能だが(現時点でも)実務屋としては「これはダメです」と言わないといけないんだろーなーと思っている。書いていても思うが、実際の関連性も怪しいし。

現時点では微妙としても、GDPRのような規制が進んでいくと、差別的な判断が法的・社会的に問題となるだろう。現在、持てるデータをすべてつっこみ作られた作成者すら何が起きているかよくわからない「AI」が増えている。知らず知らずのうちに差別的な「AI」を運用し、その結果大炎上する可能性は高い。公平性・説明可能性は今後重要性を増していく。

公平性はFADM(Fairness Aware Data Mining)といったキーワードで研究が進んでいるが決定打となるテクノロジーは存在しない。いくつかの手法が提案されているが、差別の定義が難しかったり、計算コストが高かったりと運用が難しい。一方で、今でも(人間の判断でも)差別が行われている可能性はある。差別とは何か?が定式化され、数学的に差別がないと保証された方法ができれば、世の中が良くなるかもしれない。人工知能ブームでいろんな議論が盛り上がっているし、企業によってはお金もあるしで、この手の研究が進めばよいなーと思う。(結局、結論は前回と同じ)

WEBアプリ構築(keras+uwsgi+bottle)編+まとめ&AI構築の悩み(3/3)

前回、前々回から引き続き、植物の写真から病気を判別するサイト(http://www.plant-check.jp/)を作ったときのまとめ。今回はWEBサイトを構築したときのまとめ。

WEBサイトの概要

リンクにあるとおり、WEBサイトはシンプルな作りとなっている。大きなユースケースは下記の2つ。そしてやることはほとんど同じ。

  1. ユーザがサイトの「写真を撮影」ボタンを押すと、自分のスマホから写真を撮ることが出来る。次に「病気をチェック」ボタンを押すと、取った写真の植物が病気か判定される。
  2. ユーザがサイトの「写真を選択」ボタンを押すと、自分のPC/スマホから写真を選択することが出来る。次に「病気をチェック」ボタンを押すと、選んだ写真の植物が病気か判定される。

HTML部分

スマホ経由で写真を取らせるにはcapture=”camera”を使えばOK。

 <input type="file" accept="image/*" capture="camera" id="camera" style="display:none;" name="upload" />

それ以外の部分として、labelタグを使ってボタンを隠したり、若干JavaScriptでボタンの活性・非活性を切り替えたりしているが特殊なことはやっていない。

サーバ部分

サーバ部分の構成は次の通りで、こちらも特殊な構成ではない。

  • sakuraのVPS(ubuntu 16.04 LTS)
  • Docker
  • nginx
  • uwsgi
  • Pythonのアプリ
    • bottleを利用してモデル部分(keras + tensorflow)とWEBアプリ部分をつなげた。

uwsgiとnginxのつなぎはSocketを使用した。(nginx側 uwsgi_pass unix:/path_to/uwsgi.sock、uwsgi側 socket = /path_to/uwsgi.sock )良くわからない場合は、公式サイトのサンプルを見ながら書くのが一番だった記憶がある。

Pythonのアプリ部分はbottleを用いて書いた。毎回毎回モデルをロードするアプローチはさすがに富豪過ぎるので、起動時に判別モデル(植物か否か、病気か否か)をロードしてそれを使いまわすようにしている。

一点はまった点としてuwsgi.confでthreads = 1、enable-threads = falseとしないと動作しなかった。当時forumを確認した感じではkerasのpredict処理がマルチスレッドに対応していないのが原因だったと記憶している(が、モデル読み込み時にフリーズしたような記憶もある)。それと、bottleの制約のようだが、巨大ファイルをアップロードされた場合の対策(chunkごとに読みこんで、一定以上のサイズの場合は処理を停止)を自分で書かないといけないのが面倒だった。

まとめ

モデル構築部分のコード行数は約100行(×2)、サーバ部分のコード行数は約200行だった。「AI(人工知能)が病気を診断!」というサイトがこの程度の行数で書けてしまうのは、便利なライブラリが整備されたおかげで、作者に感謝である。もっとも、モデル構築部分はノウハウ含めて自動化されていくはずで、大手各社もAutoML、Azure Machine Learning、・・・と言った名前でそのような環境をリリースしている。

AIやら人工知能やら何やらの主戦場は、結局何するの?という方向か、ほんまに使えるの?という方向になる気がしていて、クロスバリデーションベースのモデル精度を競っても無意味な時代になりつつあるんだろうなーと思う。(ということで私は自動化大歓迎、その他の思いはその他へ)

今後、情熱が復活すれば他の病気への対応を行う予定。もう少し実験したいことがあるので、コードを公開するのはその後になる見込み。

Read more »

モデル構築(keras/tensorflow+InceptionV3+Data augmentation)編(2/3)

前回から引き続き、植物の写真から病気を判別するサイト(http://www.plant-check.jp/)を作ったときのまとめ。

モデル構築の流れ

前回書いたとおり、今回、(1)植物の葉かそれ以外かを判別する2値分類モデル+(2)植物の葉が病気か否かを判別する多値分類モデルを作成した。構築方法は(1)、(2)ともに同様でkeras, tensorflowを用いて転移学習を行った。モデル構築の流れは下記の通り。バリバリのDeep Learningなので、人工知能(AI)を利用し、植物の病気判定を行った!と言っていいはず(大事なことなので2回目)。

  1. 写真をラベルごと((1)は「植物の葉/それ以外」、(2)は「健康な葉/黒星病/うどん粉病/その他カビ系の病気」)ごとに別のディレクトリに格納する。
  2. kerasを使ってImageNetを学習したInception V3をロードする。
  3. ロードしたモデルの一部(今回は250層以降)を学習可能に設定する。
  4. 問題(2値 or 多値)に応じて、Inecption V3の後段のネットワークを設定する。
  5. 写真データを学習用と検証に分ける。
  6. 写真データをdata augmentationするよう設定する。
  7. 学習、検証する。

モデル構築のコード

上記1.~7.のフローはkeras (+ tensorflow)を用いると簡単に実装できる。2値分類モデルにおける、2.~4.は下記のように実装可能。多値の場合は最終層を「predictions = Dense(クラス数, activation=”softmax”)(x)」てな感じに変えて「loss = “categorical_crossentropy”」とすればよい。ざっくりいうとニューラルネットワークの前段として学習済みのネットワークを用い特徴量抽出等を再利用(=転移学習)し、本件で必要な分類を行うネットワークを追加している。

base_model = InceptionV3(weights='imagenet', include_top=False)
x = base_model.output
x = GlobalAveragePooling2D()(x)
x = Dense(1024, activation="relu")(x)
x = Dropout(0.5)(x)
x = Dense(256, activation="relu")(x)
predictions = Dense(1, activation="sigmoid")(x)

model = Model(inputs=base_model.input, outputs=predictions)

for layer in model.layers[:249]:
   layer.trainable = False
for layer in model.layers[249:]:
   layer.trainable = True

model.compile(loss = "binary_crossentropy", optimizer = optimizers.SGD(lr=0.0001, momentum=0.9), metrics=["accuracy"])

Data augmentationは画像を変形(回転、移動、縮小、拡大などなど)させながらデータを増やして学習する方法である。画像を変形させることで、1つの画像から複数のパターンを生み出す。日本語だとデータ拡張とか呼ばれている(はず)。写真の撮られ方に依存する差異等が吸収できるので、未知データに対する性能の向上に効果がある(と私は思っている)。モデル適用時にも変形させながら何パターンか適用し、その平均を取ると性能向上効果がある(こともある)が、こっちをデータ拡張と言うかはよくわからない。

kerasだと「ImageDataGenerator」を使って簡単に書ける。多値の場合は「class_mode = “categorical”」に変更すればよい。学習データと検証データのsplitを事前に行っていれば、同じようにtest_generatorをかける。

train_datagen = ImageDataGenerator(
    rescale = 1./255,
    horizontal_flip = True,
    fill_mode = "nearest",
    zoom_range = 0.3,
    width_shift_range = 0.3,
    height_shift_range=0.3,
    rotation_range=90)
train_generator = train_datagen.flow_from_directory(
    train_data_dir,
    target_size = (img_height, img_width),
    batch_size = batch_size,
    class_mode = "binary")

学習は下記のように行えばよく、チェックポイントごとにモデルが保存され、精度に応じて自動でストップされる。nvidia-dockerのコンテナでGPU版のtensorflowを入れていれば、GPUを用いた学習が行われる。とても便利。

checkpoint = ModelCheckpoint("/保存用ディレクトリ/モデル名_{epoch:02d}.h5", monitor='val_acc', verbose=1, save_best_only=True, save_weights_only=False, mode='auto', period=1)
early = EarlyStopping(monitor='val_acc', min_delta=0, patience=10, verbose=1, mode='auto')

model.fit_generator(
    train_generator,
    samples_per_epoch = nb_train_samples,
    epochs = epochs,
    validation_data = validation_generator,
    nb_val_samples = nb_validation_samples,
    callbacks = [checkpoint, early])

今まで紹介したコードは、だいたいkerasのsampleコードをベースにしている。自分でやってみたい方はkerasのチュートリアル(https://keras.io/ja/内のリンクから飛べる)を読むのがお勧めである。

モデル構築のポイント

コード自体は簡単に書け、実行してみると検証データでの精度も良いと見える結果が得られる。が、実際に重要なのは「未知データに対する予測性能」で、思ったとおりの結果にならないことがある。これは学習・検証データの関連が強すぎるから(=独立じゃないから)で、学習結果を評価する上で重要なポイントとなる。この手の問題への対応はとっても難しい。私は人工知能やらAIやらと呼ばれるモノの中には、正しく評価されていない、過大評価なヤツも多いと思っている。

Deep Learningを使った場合(特に本件のような非常に複雑なネットワークを組んでいる場合)、「黒星病の特徴である黒の斑点を見て、黒星病と判断している」など「AIの判定に納得感があるか」はわかりにくい。検証すると、「AIはバラが植わっている鉢の色に注目して病気だと判別する」場合もある(詳しくは後述*1)。

このような動きはモデルの説明可能性の文脈でよく話題になっていて、たとえばKDD 2016の「“Why Should I Trust You?”Explaining the Predictions of Any Classifier」に詳しい。論文中の対応案は、まずまず良く動くが、速度面など使い勝手はイマイチという印象を受けた。説明可能性は重要な分野だが、現時点で決定打となる対応策は存在しない。

本件では写真を撮ったのが自分自身ということもあり、背景や鉢が注目点とならないよう気を使っている。具体的には病気の葉と健康な葉それぞれを同じ木・背景で撮影する、様々なパターンを混ぜるなど、変な場所に注目されないようにデータを作成している。

モデルの学習時には完全に未知のデータで検証がされるようにし、また、手動でAIが間違いやすいパターンの解析・チェックを行っている。割と丁寧に判別モデルを作っているが、それでも、「未知データに対する判別能力」は「検証結果として数学的に求められた能力」ほど高くはないんだろうなーと思っている。

Read more »

植物の病気をDeep Learningで判別  概要+準備編(1/3)

植物の写真から病気を判別するサイト(http://www.plant-check.jp/)を作ってみた。
サイトは2017年8月くらいに作ったのだが、そのメモがてら、やったことをまとめてみる。
使用した技術は最近の流行をふまえ、わりと本格的。バリバリのDeep Learningなので、人工知能(AI)を利用し、植物の病気判定を行った!と言っていいはず。

  • 病気判定に使用してたのは(1)植物の葉かそれ以外かを判別する2値分類モデル+(2)植物の葉が病気か否かを判別する多値分類モデル。
  • 2つのモデルともにDeep Learningを活用。具体的にはInception V3(ImageNet)を転移学習させる形で作成。
  • ソフトウェアはPythonで作成。Deep Learning部分はkeras + tensorflow、WEB部分はnginx + uwsgi + bottleで構成。
  • インフラ部分にnvidia-docker(keras + tensorflowでの学習部分)とdocker(WEB部分と判別モデル利用)を使用。サーバはsakuraのVPS(ubuntu 16.04 LTS)。
  • 学習データにはa) 自分で集めたバラの葉っぱ画像(健康な葉と病気の葉をハンドラベリング)とb)SUN Database(http://groups.csail.mit.edu/vision/SUN/)を使用。
    • (1)植物の葉かそれ以外かを判別するモデルにはa) + b)を利用
    • (2)植物の葉が病気か否かを判別するモデルにはa)のハンドラベリング結果を利用

開発環境の準備

開発環境としてDockerを利用したコンテナを2つ+リリース用のコンテナを1つ作成した。

  1. GPUを用いた学習用としてtensorflow + kerasが動作するコンテナ
    • ホストとなるUbuntu 16.04にnVidia公式ドライバをインストールした後、nvidia-dockerを導入して構築。
    • 安定版のgcr.io/tensorflow/tensorflow:latest-gpuをベースにしてkerasを追加で入れた。
    • (困ったときは公式サイトを見るのが一番だった。)
  2. 学習したモデルを用いたWEBアプリを開発するためのコンテナ
    • ホストは1.と同じ。pipからkeras, tensorflow, bottle, uwsgiを入れた。
    • 開発用なのでjupyterも導入。
  3. WEBアプリをリリースする環境のためのコンテナ
    • 2.との違いはjupyterなど余計なソフトウェアを省いている点だけ。

データの準備

本件で作成したモデルの概要とモデル構築に利用するデータは次のとおり。

  1. 植物の葉かそれ以外かを判別する2値分類モデル
    • バラの葉の画像データを正例、SUN Database(http://groups.csail.mit.edu/vision/SUN/)の植物以外のカテゴリを負例とする2値分類モデル。
    • Imagenetで学習したInception V3(keras付属)を利用。
    • 前半250層を固定。Inception V3の後に判別層を追加する形でネットワークを構成(詳細は次回記載)、学習させた。
    • いわゆる転移学習とかfine tuningとか呼ばれる手法。
    • 使ったデータは約16000枚。(data augmentation前)
  2. 植物の葉が病気か否かを判別する多値分類モデル
    • バラの葉の画像データを「健康」「黒星病」「うどん粉病」「その他カビ系の病気」の4カテゴリに分類する多値分類モデル。カテゴリ分けは自分でとったバラの写真・動画(画像に一定フレームごとに画像として切り出し)をハンドラベリングした。
    • 利用した学習手法は1.と同様。
    • 使ったデータは約1800枚。(data augmentation前)

バラの葉の画像データは夏休みに都内の公園をめぐって自分で撮影した。モデルに投入していないデータ(バラ以外、対象病気以外)も含めて3000枚以上撮影。機械学習のコード書くより大変だった(が良い経験になった(詳しくは次回))

Read more »

ドキュメントとソースコードの対応2(word2vecの利用)

先週に引き続きドキュメントとソースコードの対応付けについて実験してみた。

手順は下記の通り。前回からの改良(?)としてword2vecで日本語→英語の変換を行ってみた。

  1. 英語と日本語のマニュアルからword2vecで使用できる形式で単語を抜き出す。
    • 形態素解析のライブラリにはMecabを利用。
    • 2.のword2vecの処理で使うためのデータ。
  2. PostgreSQLのソース内にあるhtml、c、hファイルを対象にファイル内に存在する単語を抜き出す。
    1. 単語が日本語であった場合はword2vec的に類似度の高い単語TOP10から距離に応じて英単語をランダムに選ぶ。
      • ライブラリにはgensim(http://radimrehurek.com/gensim/index.html)を利用。
  3. 2.で抜き出したデータに対し、TF-IDF用の辞書(単語と出現頻度)を作る。
  4. 2.で抜き出したデータと3.を使って、それぞれのファイルに含まれる単語に対してTF-IDFで重み付けを行う。
  5. データ内(PostgreSQLのソース内)にあるhtmlファイルに対してコサイン距離の近いc、hファイルを一覧化する。

結果は↓の通り。前回に比べてコサイン類似度の値自体は上がっているものの適切なソースファイルを引けているかというとやや疑問。とはいえ何もやらないよりはマシかも(?)

詳細な結果とソースは以下のURLをご参照。

--../data/postgresql-9.3.4/doc/src/sgml/html/app-createuser.html--
[   (   '../data/postgresql-9.3.4/src/bin/scripts/createuser.c',
        0.5312760078128166),
    (   '../data/postgresql-9.3.4/src/backend/commands/user.c',
        0.49654701095797743),
    (   '../data/postgresql-9.3.4/src/bin/pg_dump/pg_dumpall.c',
        0.4521924409520057),
    (   '../data/postgresql-9.3.4/src/include/commands/user.h',
        0.4458741678013434),
    (   '../data/postgresql-9.3.4/src/include/catalog/pg_authid.h',
        0.4366612559172716)]

--../data/postgresql-9.3.4/html-ja/app-createuser.html--
[   (   '../data/postgresql-9.3.4/src/bin/scripts/createuser.c',
        0.27458665377082037),
    (   '../data/postgresql-9.3.4/src/bin/psql/tab-complete.c',
        0.2411847340696424),
    (   '../data/postgresql-9.3.4/src/backend/commands/user.c',
        0.20269999574142247),
    ('../data/postgresql-9.3.4/src/include/utils/acl.h', 0.16197452745649446),
    (   '../data/postgresql-9.3.4/src/bin/pg_dump/pg_backup.h',
        0.16130232695577076)]
--../data/postgresql-9.3.4/doc/src/sgml/html/indexes-intro.html--
[   (   '../data/postgresql-9.3.4/src/backend/commands/indexcmds.c',
        0.5887593627491802),
    ('../data/postgresql-9.3.4/src/include/utils/rel.h', 0.5847988052787634),
    (   '../data/postgresql-9.3.4/src/backend/optimizer/path/indxpath.c',
        0.5736868945937168),
    (   '../data/postgresql-9.3.4/src/backend/catalog/index.c',
        0.5695396670614502),
    (   '../data/postgresql-9.3.4/src/backend/optimizer/util/plancat.c',
        0.5669650556082131)]

--../data/postgresql-9.3.4/html-ja/indexes-intro.html--
[   (   '../data/postgresql-9.3.4/src/backend/access/rmgrdesc/spgdesc.c',
        0.6065874266691939),
    (   '../data/postgresql-9.3.4/src/backend/access/transam/rmgr.c',
        0.6055142588080052),
    (   '../data/postgresql-9.3.4/contrib/pg_xlogdump/rmgrdesc.c',
        0.6055142588080052),
    (   '../data/postgresql-9.3.4/src/backend/access/spgist/spgquadtreeproc.c',
        0.6054292723362433),
    (   '../data/postgresql-9.3.4/src/include/access/rmgrlist.h',
        0.6054292723362433)]

ドキュメントとソースコードの対応

ソフトウェア開発においてドキュメントとソースコードの対応を取り続けることは想像以上に難しい。開発当初は綺麗に対応していたドキュメントとソースが年を追うごとに乖離していき、それぞれ別物として成長していくことはよくある光景である。そのような中、ドキュメントに対応するソースが何であるか一覧化できるそりゅ~しょんが強く求められている・・・ような気がする。

ざっくり言うとドキュメントとソースコードの紐付けが自動でできるかどうか気になったのでやってみた。(ここにはひさしぶりの投稿)

条件は下記の通り。

  • Postgresql9.3.4のマニュアルに対応するソースを表示する。
  • マニュアル、ソースそれぞれについて出てくる単語を抽出し、TF-IDFで重み付けを行い、コサイン類似度の大きなものを関連しているとみなす。

結果、英語版のマニュアルとソースの対応はそこそこの精度で取れるが、日本語版のマニュアルに対する対応はイマイチだった。ソース中に日本語が入っているとは思えない(=キーとなる情報が少ない)ので納得のいく結果ではある。

下記はcreateuserとindex-introの例。[]内には–・・・.html(マニュアル)–に対して類似度が大きなソース上位5つが入っている。()内の”の中がソースコードの名称でその後の数字がコサイン類似度。なお、htmlディレクトリ以下にあるのが英語版で、html-ja以下にあるのが日本語版となっている。

--../data/postgresql-9.3.4/doc/src/sgml/html/app-createuser.html--
[   (   '../data/postgresql-9.3.4/src/bin/scripts/createuser.c',
        0.5315036131957114),
    (   '../data/postgresql-9.3.4/src/backend/commands/user.c',
        0.49634873213501424),
    (   '../data/postgresql-9.3.4/src/bin/pg_dump/pg_dumpall.c',
        0.45199746892518794),
    (   '../data/postgresql-9.3.4/src/include/commands/user.h',
        0.4456889634461926),
    (   '../data/postgresql-9.3.4/src/include/catalog/pg_authid.h',
        0.436479878362433)]

--../data/postgresql-9.3.4/html-ja/app-createuser.html--
[   (   '../data/postgresql-9.3.4/src/bin/scripts/createuser.c',
        0.24660267524744783),
    (   '../data/postgresql-9.3.4/src/bin/psql/tab-complete.c',
        0.21638658446826328),
    (   '../data/postgresql-9.3.4/src/backend/commands/user.c',
        0.18185439320172517),
    (   '../data/postgresql-9.3.4/src/backend/catalog/objectaddress.c',
        0.17654887868713476),
    (   '../data/postgresql-9.3.4/src/backend/parser/gram.c',
        0.15892817112702087)]
--../data/postgresql-9.3.4/doc/src/sgml/html/indexes-intro.html--
[   (   '../data/postgresql-9.3.4/src/backend/commands/indexcmds.c',
        0.5887593627491802),
    ('../data/postgresql-9.3.4/src/include/utils/rel.h', 0.5847988052787634),
    (   '../data/postgresql-9.3.4/src/backend/optimizer/path/indxpath.c',
        0.5736868945937168),
    (   '../data/postgresql-9.3.4/src/backend/catalog/index.c',
        0.5695396670614502),
    (   '../data/postgresql-9.3.4/src/backend/optimizer/util/plancat.c',
        0.5669650556082131)]

--../data/postgresql-9.3.4/html-ja/indexes-intro.html--
[   (   '../data/postgresql-9.3.4/src/backend/lib/stringinfo.c',
        0.09184457364491616),
    (   '../data/postgresql-9.3.4/src/interfaces/ecpg/test/expected/sql-insupd.c',
        0.0886416204237675),
    (   '../data/postgresql-9.3.4/src/test/examples/testlibpq3.c',
        0.08837346944289427),
    (   '../data/postgresql-9.3.4/src/backend/access/heap/visibilitymap.c',
        0.08815864541352804),
    (   '../data/postgresql-9.3.4/src/interfaces/ecpg/test/expected/sql-indicators.c',
        0.08800994862677734)]

詳細な結果はhttp://staka.jp/doc-src/result.txtの通り。なんとなくだが、英語版マニュアルとソースコードはそれなりに紐付いているように見える。日本語のコメントが豊富にあるソースとであれば日本語版もうまく行くような気がしないでもない。

ソースコードはhttps://github.com/s-taka/doc-srcにおいておいた。基本的にPythonで書いていて、日本語マニュアルの形態素解析はMeCabで行っている。最近流行のword2vecの利用は今後の課題。

補記

前回同様、前段の補記。ドキュメントとソースが乖離していくのがよくある光景なのは事実だが、ドキュメントとソースが(ざっくりと)紐付いてうれしい人がどれだけいるかは謎。ドキュメント→ソース→コミットログ→作成者とたどっていけるのはある程度便利かもしれないが。本当は、ドキュメントとソースの乖離が分かるとうれしい人は多そうだけど、自然言語処理のアプローチでそれを見つけるのは至難の技だろーなーと思う。

 

GPGPUで精度保証付き計算

金融の計算において、計算精度は超重要である。精度が低ければ価格付けに失敗し大損するかもしれない。

CUDAには、__fadd_rn(x, y)、__fadd_rz(x, y)、__dadd_rd(x, y)、__dadd_ru(x, y)、のように四則演算で丸めの方向を指定可能な関数が用意されている。数学関数の誤差についての情報も与えられておりGPGPUで精度保証計算を行うことが可能である。

前回同様、ブラックショールズモデルでのオプションプライシングを例に精度保証付き計算の実験を行った。条件は下記の通り。

  • 計測対象のオプション明細は1024万件。パラメータは乱数により発生させる。
  • プレミアムを算出。(デルタ、ガンマはめんどーなので省略した。)
  • 計算はすべてdouble型で行う。(単精度版は後半)
  • Amazon EC2のクラスタ GPU クアドラプル エクストララージを使用。GPUはFermi M2050を1つ使用。CPUは1コアのみ使用。コンパイルは全部CUDAでやっている。(CPUには不利な条件だが、CPUとの計算時間は比較しない。)
  • 四則演算については丸めの方向を指定しつつ区間演算で精度保証を行う。
  • 数学関数については誤算情報を加味しつつ区間を設定し精度保証を行う。(わりとてきーにやっている。)

結果は下記の通り。数字の順はGPUで精度保証計算をしたプレミアムの下界、CPUでの計算結果、GPUでの計算結果、GPUで精度保証計算をしたプレミアムの上界となっている。最後の時間は、CPUでの計算時間、GPUへのデータ転送と初期化にかかった時間、GPUで精度保証付き計算をしたときにかかった時間(結果の転送時間込み)、GPUでふつーに計算したときにかかった時間(結果の転送時間込み)。

GPU lower < CPU | GPU < GPU upper
PREMIUM[0]: 0.405499973123749 < 0.405499973123820 | 0.405499973123821 < 0.405499973123893
PREMIUM[1024000]: 4.029060162124779 < 4.029060162124900 | 4.029060162124893 < 4.029060162125027
PREMIUM[2048000]: 0.733045301526717 < 0.733045301526774 | 0.733045301526773 < 0.733045301526825
PREMIUM[3072000]: 1.977617064175732 < 1.977617064175853 | 1.977617064175839 < 1.977617064175931
PREMIUM[4096000]: 0.162532300560706 < 0.162532300560737 | 0.162532300560734 < 0.162532300560762
PREMIUM[5120000]: 0.767333091428844 < 0.767333091428927 | 0.767333091428925 < 0.767333091428991
PREMIUM[6144000]: 8.282037131422769 < 8.282037131422967 | 8.282037131422962 < 8.282037131423138
PREMIUM[7168000]: 0.020357967912149 < 0.020357967912157 | 0.020357967912157 < 0.020357967912164
PREMIUM[8192000]: 0.442812778153888 < 0.442812778153947 | 0.442812778153947 < 0.442812778154016
PREMIUM[9216000]: 0.727449524196905 < 0.727449524196972 | 0.727449524196975 < 0.727449524197043 

CPU time: 2779.697021 (ms)
GPU Initial time: 2082.956055 (ms)
GPU Processing time 1: 442.318848 (ms)
GPU Processing time 2: 89.383057 (ms)

GPUで精度保証付きの計算を行うために普通の5倍くらいの時間がかかる。単精度で計算すると下記のようになる。

GPU lower < CPU | GPU < GPU upper
PREMIUM[0]: 0.39829 < 0.39859 | 0.39859 < 0.39882
PREMIUM[1024000]: 4.04288 < 4.04349 | 4.04350 < 4.04403
PREMIUM[2048000]: 0.72358 < 0.72385 | 0.72385 < 0.72404
PREMIUM[3072000]: 1.98723 < 1.98767 | 1.98767 < 1.98803
PREMIUM[4096000]: 0.16034 < 0.16050 | 0.16050 < 0.16059
PREMIUM[5120000]: 0.77226 < 0.77262 | 0.77262 < 0.77288
PREMIUM[6144000]: 8.28720 < 8.28772 | 8.28772 < 8.28814
PREMIUM[7168000]: 0.02022 < 0.02033 | 0.02033 < 0.02036
PREMIUM[8192000]: 0.44575 < 0.44603 | 0.44603 < 0.44622
PREMIUM[9216000]: 0.72577 < 0.72608 | 0.72608 < 0.72630
CPU time: 8085.477051 (ms)
GPU Initial time: 2080.006104 (ms)
GPU Processing time 1: 200.956787 (ms)
GPU Processing time 2: 45.681152 (ms)

この例についてはCPUについては単精度の方が倍精度より遅い。理由としては最適化(特にSSEなのかFPUなのか)やらコンパイラやらCPUの特性やらメモリやらの話題があるがここでは省略する。

単精度の場合も普通の計算とくらべて5倍くらいの時間がかかっている。計算量もそうだが分岐命令が追加されたペナルティが大きいものと思われる。

単精度の方は精度保証有り無しの時間の比率が変わるかを調べる事が主目的であるため、四則演算以外の関数に関わる精度保証を非常に荒く行っている。上界、下界の差が広いのはそのためである。

以上の結果から、ブラックショールズモデルでのオプションプライシングで精度保証付き計算を行うためには通常の5倍程度の計算時間が必要であることがわかる。

補記

最初に計算精度が超重要とか書いたけれども、そもそもブラックショールズモデルではボラティリティスマイルや金利なんかの期間構造を無視してしまっているので、そのモデルに起因する差異のほうが数値計算上の誤差より大きい。なので、精度保証をやる意味はあんまり無(ry・・・いやすこしはあるかm・・・まぁちょっとは・・・

現実問題での上界下界を求めたければ正のバイアスがかかる式と負のバイアスがかかる式で挟み撃ちーという感じでやる必要がある。(アメリカンオプションのプライシングで見かける。とはいえ、そこまでやっても実際問題モデル誤差は残る。。。)

それはおいといて計算時間が5倍というのはそんなもんかなという印象。上界、下界のために最低2倍の計算とメモリが必要で、かつ、割り算の場合は正負の場合分けが必要なため分岐が入ってしまう。

精度保証付き数値計算は面白い考え方だと思うが、イマイチ流行っていない気がしなくも無い。コンパイラオプションなりVMの設定なりで気軽に使える時代が来たら利用する機会はありそうだが。。。

 

 

Amazon EC2 GPUインスタンス(Fermi)で金融の計算

先日(といってもかなり昔)にセットアップしたAmazonEC2のGPUインスタンスでFermiの実験してみた。(正確には下調べ。)

比較条件は以下の通り。お題は金融の世界でよく使われるブラックショールズモデルでのオプションプライシング。

  • 計測対象のオプション明細は512万件。パラメータは乱数により発生させる。
  • プレミアム、デルタ、ガンマを算出。
  • 計測はすべてdouble型。標準正規分布の累積分布関数についてもdouble型の精度のある近似式を用いる。
  • Amazon EC2のクラスタ GPU クアドラプル エクストララージを使用。GPUはFermi M2050を1つ使用。CPUは33.5ECUをフルで使用。(全力ならGPUは2つ使える。が、今回はGPU間の分散はやっていない。)
  • GPUのコンパイルなどはCUDA 3.2、CPUの方はGCC(4.1)+openMP。

結果は以下の通り。

  • CPUから見える場所に明細がある状態から計算完了(CPUから見える場所への転送完了)だとGPUが2000msecに対して、CPUは300msecくらい。(1コアのみCPUを使うと3300msecくらい)
  • GPU側に明細を転送した後からだとGPUは350msecくらいで計算を終え、計算結果をCPUから見える場所に転送できる。

ブラックショールズは計算負荷がかなり軽く、メモリ転送やデバイスの初期化に時間がかかるGPUには不利な条件。256万件だとGPU側の計算時間が「明細転送後計算開始~計算結果をCPU側にコピー完了」で230msecなので、GPUはまだ本気が出せていないよーな感じ(それを言うと、CPUも拡張命令使ってないので本気は出せていない)。

結論としてはGPU化するにはあんまり向いていない倍精度なブラックショールズ式の計算において、てきとーにコードを書いた場合でも計算部分(片道の転送時間は含む)ならば1 GPUでCPU 8コア(2つのクアッドコアNehalem)に相当する計算能力が出せるっぽい。素晴らしい。

ちなみに計算誤差は512万件分、「delta_diff += fabs( delta_cpu[i] –  delta_gpu[i]);」という感じで足しても

PREMIUM: DIFF = 0.000000026168368
DELTA: DIFF = 0.000000000065476
GAMMA: DIFF = 0.000000000001220

程度。なお、512万件の合算値はPREMIUM:52703033, DELTA:2487382, GAMMA:69438くらい。

個別でみるとほぼdoubleの限界のくらいまであっている。誤差部分はGPUの積和演算利用とか、CPUは実は微妙に拡張倍精度演算をすることがあるとか(最適化やSSEなど細かい話題次第ですが)、非正規化数の存在とかあるんだけど、どこが影響しているのかは調べていない。

むかーし試してみていたときよりもdouble型の能力・性能がかなり上がっている印象。相当向いていない処理にもかかわらずまずまずの結果だった。もうちょっと遊んでみる予定。

# NVIDIA的にはGPGPUに力を入れているようだが、新コアのKeplerが出たらAmazonも入れてくれるんだろうか?