はじめに
どーも@HirokiNakaharaです。何かの縁があって書くことにしたので、今別件のプロジェクトで使っている軽ーい機械学習を使って手書き文字認識器をIntel CycloneV SoCに実装してみます。年末はどうも忙しいようなので、
- Google colaboratory (無料の機械学習向けPyThon環境)
- Scikit-learn (機械学習ライブラリ)
- OpenCL (お手軽にFPGAを実装できる高位合成言語)
を使ってちゃっちゃっと作ってみました(記事公開の当日夜中1時から設計開始。どんだけギリギリなの。。)。今回のプロジェクト一式は私のgithubにあげておきますのでご参照ください。
Decision Tree (決定木)とは
機械学習のモデルは特に決めることはないのですが、今回は短期間(これを書いているのは夜中の1時、カレンダーの公開時間は7時なのであと6時間!)で作る必要があるので、簡単にコード生成器が作れるシンプルなモデルをチョイスしました。とは言ってもそこそこの精度は欲しいのでロジスティック回帰や線形結合モデルはパス。そこで決定木を使うことにしました(単に今やってるプロジェクトで慣れていると言うのもありますが)。
決定木とはデータの特徴点(説明変数)に対して学習によって得られたルールに従って木を根から辿っていくことで予測する機械学習の一種です。
メリットは
- 特徴点によって分類するので誤判定したときの原因が特定できる(説明可能性な機械学習の一種)
- スケールが異なる特徴点をまとめて扱える(通常のモデルは正規化等の前処理が必要)
があげられる一方、デメリットは
- 分類性のはそれほど高くない
- 過学習を良く起こす
- 線形性のあるデータに適していない
ことが知られています。決定木の学習は様々ありますが、最もデータを分離できる説明変数を選択することを再起的に繰り返していく方法が有名でしょうか。
Google Colaboratoryを使った決定木の学習
Decision Treeは良く知られているアルゴリズムであり、scikit-learnという機械学習ライブラリに収録されているので、これを使いましょう。幸い、scikit-learnには有名な学習データを読み込むAPIがありますので、これを使って手書き文字の一種であるMNISTデータセットを使用します。今回は無料なオンライン機械学習環境を提供してくれるGoogle colaboratoryで学習して、OpenCL用のカーネルを自動生成するスクリプトを作成しましょう。
Google検索等で「google colab」と入力してColaboratoryを開き→ノートブックを開く、を選択し、GitHub→HirokiNakaharaと入力→HirokiNakahara/IntelAdventCalender2020を選択します。するとMNIST_decision_tree.ipynbノートブックを選択できるようになります(上図)。
あとはノートブックの各セルを実行すればOKです。以下、主要な部分の解説です。
from sklearn import tree
# 決定木によるモデル構築
clf_dt = tree.DecisionTreeClassifier(criterion='gini', splitter='best', max_depth = 4)
clf_dt = clf_dt.fit(x_train, y_train)
scikit-learnをインポートしてdecision treeを読み込みます。max_depthは深さを制限するパラメータです。これを深くしても良いですが、合成に時間がかかるので認識精度とのトレードオフになります。試しに
clf_dt = tree.DecisionTreeClassifier(criterion='gini', splitter='best')
とすればかなり深い決定木が学習されます(が、訓練データに過適合してしまって、テストデータの精度はそれほど上がりません)。
決定木は乱数によってモデルを学習した場合、その形が異なることが多い、すなわち分散が大きいことが知られています。そこで、決定木を複数並べてアンサンブルモデルを構築することで高精度なランダムフォレストやその発展系であるBoostingというモデルがあります(が、今は夜中の2時(公開まであと5時間、、)なので、時間がありませんでした、、、誰か実装して、、)。
#Intel OpenCL用のカーネル自動生成
学習済みの決定木モデルからOpenCL用のカーネルコードを自動生成してみます。決定木は根から説明変数を評価しながらトレースするモデルなので、各ノード毎にif文による分岐で記述することができます。そこで、学習済み決定木の内部パラメータ(あるノードで参照する説明変数と、ジャンプ先のノードの番地)にアクセスしてif文に置き換えて生成するコードを作成しました。
Intel OpenCL はホストCPU(今回はDE10-nanoをターゲットにしたのでARMプロセッサ)とFPGA部分を分離して記述するので、別途ホストCPU向けのコードが必要です。今回はホストコードは手で設計して、学習によって変更が生じるカーネルコードは自動生成する方法にしました。
学習した決定木の内部パラメータにアクセスするにはPythonコードでは
def get_code(tree, feature_names, function_name="decision_tree"):
left = tree.tree_.children_left
right = tree.tree_.children_right
threshold = tree.tree_.threshold
features = [feature_names[i] for i in tree.tree_.feature]
value = tree.tree_.value
でできますので
// thresholdが葉を意味するマジックコード以外
label hogehoge:
if ( features < threshold){
goto left;
} else {
goto right;
}
// thresholdが葉を意味するマジックコードの場合
return value;
のようにコードを生成して、再起的にleft, rightにたどればOKです。詳細はColaboratoryのノードブックをみてください。
ノートブックのセルを全て実行すると
- bench_input.h
- dt_mnist.cl
が生成されます。これをダウンロードしましょう。ファイルにマウスカーソルを当てて「:」をクリックするとダウンロードメニューが出てきます。
DE10-nano Cyclone V SoCボードのセットアップ
今回はDE10-nano(Cyclone V SoC搭載)ボードを使いました。入手しやすいこと(その気になったらAmazonで翌日配達!)、値段が安いこと、サンプルが豊富であることから選択しました。
ボードの環境セットアップは開発元のTerasic社のドキュメントが参考になります。Terasic社は割とトラブルが少なくサンプルプログラムも豊富なので好んで使っています。
OpenCLでの合成→実行
あとはOpenCLで合成するだけです。ここまで来ると簡単ですね〜。高位合成ありがたやです。
$ aoc device/dt_mnist.cl -o bin/dt_mnist.aocx -board=de10nano -c -v --report
でカーネルをコンパイルします。-c, -v, --reportオプションをつけると合成中のプロセスが表示されたり、ハードウェア量の見積もりができます。ここで、敢えて次のステップの合成には進まず、OpenCLのコードのリファクタリングに戻った方が手早く設計が終わるポイントです。今回ですと決定木のサイズはFPGAのリソース、速度、認識精度に影響するのでGoogle colaboratoryに戻って再学習しつつ、カーネルコードの修正を繰り返すことになります。このステップの手戻りをいかに短縮するかが短期間設計のポイントです。そこで、今回はボトルネックになるkernelのコード記述部分を自動生成することにして期間短縮を狙いました。
ハードウェアができるまで30分くらいでした(筆者の環境で)。うーん、深夜の3時を過ぎました。あと4時間経過するとカレンダーが公開です。。とはいえ、ここまで設計しておけばあとはホストコードを
$ make
でコンパイルすればOK。で、生成したbin/hostとbin/dt_mnist.aocxをDE10-nano FPGAボードに転送して実行するだけでした。簡単!
まとめ
なんとか2時間ちょいでMNISTを学習してFPGAにポーティングできたよ〜
高位合成すげーね。公開の朝早起きして作ってもOKだったじゃん(Adventカレンダーを書いている人に頭が上がりません、、)
Google Colabratory様にも頭が上がりません。あとscikit-leranとか。random forestやるとあっという間に精度上がりますよ。
宣伝
ベンチャーを立ち上げましたTokyo Artisan Intelligence社といいます。こんな超絶特急設計なんて案件はまずありませんが、ちゃっちゃとMachine Learningを使ったプロダクトを作って社会実装しておりますので、興味があったらwantedlyで人材募集していますのでお越しください!
それでは良いお年を〜