こんにちはニューラルネット老人こと糟谷勇児です。
2012年の技術に追いついたので現在は9年差、犬の年齢に例えると52歳ぐらいなので、そろそろ老人を脱却できるかもしれません。
www.ipet-ins.com
今回はAlexNetをもう少し深堀りしつつ、今後の予定を考えていきます。
データオーギュメンテーションの導入
前回はAlexNetにデータオーギュメンテーションなしで学習させてみたところ、Le-Netと同じ7層のネットワークより精度が低いという結果になってしまいました。
そこで、今回はデータオーギュメンテーションを入れて学習してみます。
データオーギュメンテーションはこちらと同じ方式でやっています。
buildersbox.corp-sansan.com
こちらのグラフがデータオーギュメンテーションありの場合です。
最大91.6%まで精度が上がりました。今までは90.6%が最大だったので、これはポテンシャルを感じる!
とはいえ、そこまで行ったのは一瞬だけ、たまたまかもしれません。もう少し調査したいところですね。
しかし遅い
前回、AlexNetを学習させるのに土日かかると書きましたが、データオーギュメンテーションでデータを2倍に増やすと、1エポックの学習(全データを1回学習)に2倍の時間がかかることになります。
これでは土日月火と必要になりますね。
それはそうと、DeepLearningのある生活は潤いがあります。
私の作ったC#のライブラリでは5エポックの学習ごとにslackに精度と二乗誤差が通知されます。
「前回からそろそろ3時間経つからそろそろ次の精度来るかな」とか、朝起きたら、「お、進んでるな」なんてslackを見るのは朝顔の花が咲くところを観察しているようで楽しいですね。
趣はあるのですが、いろいろな検証をしたいとなるとやはり遅すぎるので高速化したくなってきます。
AlexNet以降のDeepLearning
AlexNetの後にはVGGやGoogLeNet、ResNetなどが控えています。
VGGは学習済みモデルが公開されていて、便利なため、直近の研究会でも画像認識を使うけど画像認識が主題ではない研究でよく使われているのを見かけます。ResNetは以前ブログに書いたロゴの抽出でも使用しているものです。
日本史に例えると、AlexNetで文明開化して明治時代に突入し、その後の近現代史にVGGやGoogLeNetやResNetが位置されているという感じでしょうか。
さて、これらの新しいニューラルネットワークの構造には共通点があります。
それは基本的に3×3以下でストライド1のフィルタしかコンボリューション層で使用されないということです。
と言うのも5×5のフィルタを計算するより、3×3を二層重ねたものを計算する方が高速かつ、活性化層を挟んでデータをフィルタリングできるので、精度も高まるということのようです。
つまり、これからの学習においては3×3ストライド1のフィルタに全力投球すればいいということになります。
3×3のフィルタを高速化する
SIMD(single instruction, multiple data)の活用を実施します。C#ではVectorというクラスがあり、これに詰めて内積を取ったり、スカラー積を取ったりすると最大4回の計算を1度にやってくれるのです。
docs.microsoft.com
ただし、計算に使用できる数値型は単精度(float)に限られます。
以前、全体をfloatにしたときは速度がそんなに変わらなかったのとdoubleのほうが丸め誤差が少なくなるので、結局doubleに戻していました。また全体をfloatに書き直すのは結構大変なので、今回は3×3のフィルターを使用するコンボリューション層のクラスを別途作って前後で変換するようにしてみます。併せて配列からのデータの取り出しをポインターに変更します。
C#では配列からデータを取り出そうとすると、配列の境界チェックが行われるのですが、ポインターを使うとそれを行わずにデータにアクセスできます。
フィルタのコードはこんな感じ。
public unsafe float[,] ApplyFilterOneV3x3(Vector3[] filter, int planeIndex) { var newWidth = width - 2;//widthは元画像の横幅 var newHeight = height - 2;//heightは元画像の縦幅 var stateData = new float[newHeight, newWidth]; var v1 = new Vector3(); var v2 = new Vector3(); var v3 = new Vector3(); fixed(float* dataPlane = &data[planeIndex][0, 0]) { for (var ypos = 0; ypos < newHeight; ypos++) { for (var xpos = 0; xpos < newWidth; xpos++) { //画像データをvectorに詰める var ptr = ypos * width + xpos; v1.X = dataPlane[ptr++]; v1.Y = dataPlane[ptr++]; v1.Z = dataPlane[ptr]; ptr = (ypos + 1) * width + xpos; v2.X = dataPlane[ptr++]; v2.Y = dataPlane[ptr++]; v2.Z = dataPlane[ptr]; ptr = (ypos + 2) * width + xpos; v3.X = dataPlane[ptr++]; v3.Y = dataPlane[ptr++]; v3.Z = dataPlane[ptr]; // 内積を計算して合計を取る stateData[ypos, xpos] = Vector3.Dot(v1, filter[0]) + Vector3.Dot(v2, filter[1]) + Vector3.Dot(v3, filter[2]); } } } return stateData; }
読みにくいですね。Vector3は使いまわしたほうが若干早いのでnewせずに3行かけてデータを入れてます。3×3のフィルタに限定しているのでVector3×3個でいいのですが、5×5だとVector4を7個とかにするのがいいんですかね。今後3×3しか出てこないと信じて深追いしないことにします。
同様にバックプロパゲーションの計算もVector3とポインターを使用したプログラムに書き換えます。
そうすると、全体で約二倍速くなりました。
これでデータオーギュメンテーションでデータを二倍にしても土日で計算が終わるはずです。
デバッグが大変
さて、ポインターを使用すると圧倒的に可読性が下がるのでデバッグは大変なのですが、うまく単体テストを使うことでデバッグしやすくなります。今までの汎用的なコンボリューション層のプログラムと今回作成のプログラムでほぼ同じ動作をするはずなので、そのようなテストを作成して、テストが落ちなくなるように修正していきます。
急に最先端のプログラムを作ろうとすると難しいのですが、3層パーセプトロンから初めて、N層に拡張し、コンボリューション層やプーリング層などを選べるように拡張してきたので、新しく作ったものの一部が以前作ったものと同じ動作をするか確認していくと比較的プログラムが楽に作れます。
今後
速くなったのでいろいろ検証していきたいですね。特に気になっている点としては、AlexNetが後段でpooling層なしの3連のコンボリューション層を持っているところです。ここがLe-Netとの大きな違いなのですが、その役割について私はあまりよくわかっていません。ここのフィルタやそこを通る画像を可視化してその役割を知れたらなとか思っています。
とはいえそれはたぶん、かなり時間がかかる作業になるため、先にVGGに行ってみたい欲求もあります。VGGはAlexNetと異なり、コンボリューション層の後に活性化層を持っていて、こまめに要らないデータをそぎ落としていくスタイルで、これは以前紹介したHAAR like特徴量とAdaboostを使用した手法に近く、こっちのほうが精度出そう感は伝わってきます。
また、さらに高速化を進めるために、学習方法の変更も考えたいですね。
現在では二乗誤差よりクロスエントロピー誤差が一般的に使用されているようですし、確率的勾配法もいろいろ拡張されたものが提案されています。AlexNetに実装されている慣性を用いた学習も実装していないですし。
DeepLearningはやることがいっぱいで飽きないですね。
それではまた。
▼本シリーズのほかの記事はこちら