はじまり
MNIST(手書き数字)分類器を自作するお話です。C++でやります。
私はAI関連企業に勤めていますが、私の担当はアプリケーション部分、すなわち、製品のうちAI以外の、画面描画やユーザーインターフェースを主に開発しています。したがって、私自身は機械学習素人であり、決してガチ勢などではありません。しかし、隣のシマが気になるのです。私の席から5mくらいしか離れていないところで、機械学習ガチ勢がAIモデル開発をしていたり、大量の画像にアノテーション作業をしていたりします。隣の部署が作ったAIエンジンをアプリケーションに組み込む作業もしますが、AIエンジン内部については私は何もわかりません。
アプリケーションエンジニアな私でも、AI関連企業勤務なので、ガチ勢は無理でも、機械学習の隅っこをちょっとかじってみるくらいはしてみたくなるものです。というわけで、オライリーの赤い本「ゼロから作る Deep Learning」を見ながら、自分で手を動かしてみることにしました。
やったことは、パーセプトロンから誤差逆伝播法までです。本に書いてあるプログラムを入力すれば、あるいは、それらが収められたリポジトリをクローンすれば、動かすだけなら難しくはありません。本のプログラムはPythonで書かれており、主にNumPyライブラリによる行列演算を利用して実装されています。ですが、速度は遅くてもいいから、行列のドット積から復習も兼ねて理解したかったので、既成の行列演算ライブラリは使わないことにしました。そして、Pythonは私の得意言語ではありませんので、使い慣れているC++を使用することにしました。
まずは本を読む
「ゼロから作る Deep Learning」は機械学習の基礎を習得するのに最良の本だと思いました。他の本では専門用語が出てきて、その定義や数式が書いてあって、申し訳程度のサンプルプログラムが掲載されていておしまい、みたいな本が多いですが、この赤い本は、なぜ動くのか、どういう仕組みなのか、が懇切丁寧に書かれていて、大変理解の助けになりました。たとえば、パーセプトロンとは何かについては、論理演算のゲート回路(OR, AND, NAND, XOR)を取り上げて解説されているのがとてもわかりやすいです。これなら機械学習素人のアプリケーションエンジニアでも理解できます。伝播についての説明は、演算グラフを用いた解説が秀逸だと思いました。
学習済みデータから手書き数字の分類をする
MNIST1
https://github.com/soramimi/mnist1
機械学習をほとんど理解していない状態から、学習する処理を実装するのは無理です。
まずは、Pythonのサンプルプログラムを途中まで実行して、学習済みデータ(各層のウェイトとバイアスの行列)をファイルに落として、予め用意した28x28の画像を推論させました。
行列の扱いに慣れていない数学苦手マンはrowとcolumnがこんがらがってきますので、自分で実装して慣れるしかないと思います。
学習処理をC++に移植する
MNIST2
https://github.com/soramimi/mnist2
本の第3章に掲載されている関数ひとつひとつをC++に書き直して、学習処理を実装しました。自作行列演算クラスをちょっと改良しました。MNISTの配布データセットを読みこむ処理を追加しました。
層を意識したリファクタリング
MNIST3
https://github.com/soramimi/mnist3
Affineレイヤ、Sigmoidレイヤ、Softmaxレイヤの各クラスを作りました。
ネットワークは二層パーセプトロンで、第0層(入力)は784、第1層(隠れ層)は50、第2層(出力)は10です。コードを抜粋します。
int input = 28 * 28;
int hidden = 50;
int output = 10;
affine_layer1.make(input, hidden);
affine_layer2.make(hidden, output);
順伝播する処理です。
Matrix a1 = affine_layer1.forward(x);
Matrix z1 = sigmoid_layer.forward(a1);
Matrix a2 = affine_layer2.forward(z1);
Matrix y = softmax_layer.forward(a2);
逆伝播の処理です。
Matrix dy = softmax_layer.backward(y);
Matrix dz1 = affine_layer2.backward(dy);
Matrix da1 = sigmoid_layer.backward(dz1);
affine_layer1.backward(da1);
層を積み重ねてネットワークを構築するための改造
MNIST4
https://github.com/soramimi/mnist4
レイヤのリストを用意しておき、
std::vector<std::shared_ptr<AbstractLayer>> layers;
このリストにレイヤを突っ込んでいくだけで、ネットワークが構築できるようにしました。
addAffineLayer(input, hidden, Rand);
addSigmoidLayer();
addAffineLayer(hidden, output, Rand);
addSoftmaxLayer();
(Randというのは、重み値の初期化をするために乱数を生成するラムダ式です)
このようにリスト化したことで、以前までは forward
や backward
をいちいち呼び出していたのを、ループで記述できるようになりました。
// 順伝播
for (auto &p : layers) {
y = p->forward(y);
}
// 逆伝播
for (auto it = layers.rbegin(); it != layers.rend(); it++) {
auto &p = *it;
y = p->backward(y);
}
このタイミングで、行列クラスをスマートポインタ化しました。(なんとなくstd::shared_ptr
のかわりに、自前実装のスマートポインタで作りましたが、そこは追求しないでください)
おしまい
とりあえずここまでです。
ただの二層パーセプトロンです。ぜんぜん深くない低速機械学習を実装することができました。この次の課題は畳み込みでしょう。そうなってくると、CPUだけでは処理が重くなりそうなので、CUDAかOpenCLを利用してGPUを活用する必要も出てきそうです。
C++で書かれた軽量機械学習ライブラリとしては**tiny-dnn**が知られています。私のはそんなに良く出来ていませんが、オライリーの赤い本を理解する助けくらいにはなるのではと思います。
依存ライブラリは無く、標準C/C++ライブラリだけで動きますので、とりあえず気軽に機械学習に入門してみたいという方の参考になれば幸いです。
にわか 似非 機械学習エンジニア、というか、アプリケーションエンジニアが作った、機械学習への挑戦の第一歩ということでお許しください。