ニューラルネットワーク=形式ニューロンを層状に並べたもの

fadis氏:こんにちは、松林です。今日はNNEFの話をします。

ニューラルネットワークという関数があります。ニューラルネットワークは、形式ニューロンと呼ばれるものをたくさんつなげたものです。形式ニューロンは、(スライドを示して)図のようにたくさんの入力を受け取って、それに重みをかけて総和を取り、活性化関数に通して出力します。ニューラルネットワークは、これを層状に並べたものです。

図のように、左から来た値と右の形式ニューロンのすべての組み合わせが接続されている層を全結合層と言います。

個々の形式ニューロンが重みを持っているので、ニューラルネットワーク全体は大量の重みを持つことになります。

ニューラルネットワークをアプリケーションに組み込んで使いたい

(スライドを示して)こんなふうに相関がありそうだけど関係がよくわからないデータがある時に、とりあえずニューラルネットワークを当てはめます。ニューラルネットワークは、層が十分な数重なっている時に任意の関数を近似できる性質があるため、入出力の関係を探る問題は、入出力の関係を再現できる重みを探す最適化問題になります。

適切な重みを探す作業を学習と言います。学習を行うためのフレームワークはいくつか開発されています。

フレームワークを使って学習ができたら、そのニューラルネットワークをアプリケーションに組み込んで使いたくなります。ところが、学習結果の保存形式はフレームワークの数だけあります。アプリケーションがこれらに個別に対応するのは面倒です。

NNEF(Neural Network Exchange Format)の概要

画像の場合、JPEG形式のようなアプリケーション間で交換できる共通の形式がありますが、ニューラルネットワークにはこういうものはないのでしょうか? あります。それが今日紹介するNNEF(Neural Network Exchange Format)です。

学習結果を再現するために保存しなければならないものは、いくつかあります。ニューラルネットワークがどのような層で構成されているか、各層の設定、さらに各層の重みと、その値がどのようにファイルに記録されているかです。

NNEFは、同じディレクトリに置かれた複数のファイルで学習結果を表現します。ネットワークの構成は、人間が読み書きしやすいテキスト形式で、重みのデータは、素早くパースできるバイナリ形式で記録されています。

ニューラルネットワークは一般的にGPUなどのアクセラレータで処理されますが、重みが別ファイルに分割されているので、ファイルのパースを始める前にデータをGPUに送り始められます。(スライドを示して)ネットワークの定義は以下のように、NNEFのバージョンとネットワークの名前から始まっています。

オレンジで示した範囲に登場するバッファのうち、dataは入力値が入ってくるバッファで、probは出力値が吐かれるバッファです。

VGG16のcaffemodelをNNEFに変換したものを読んでみる

今回は例としてVGG16を扱います。VGG16は画像を識別するために作られたニューラルネットワークです。NNEFの公式で学習済みのVGG16のcaffemodelをNNEFに変換したものが配布されているので、これを読んでいきます。

緑で示した部分が入力値のバッファの定義、紫で示した部分が出力値のバッファの定義です。externalは、外から入ってくるデータがあることを表すオペレータです。形状に10,3,224,224が指定されていますが、これは224×224ピクセルのRGBの画像が10枚まとめて入ってくる予定という意味です。

(スライドを示して)ここにあるlinearは、行列とベクトルの積を求めるオペレータです。なぜ行列とベクトルの積が出てくるのかというと、全結合層は行列とベクトルの積で表せるからです。xとyが図のような関係にある時、各ニューロンの重みを並べて行列にすると、全結合層は行列とベクトルの積を求めて活性化関数に通すという計算になります。

linearの後ろにあるのは活性化関数です。linearして活性化関数に通しているので、(スライドを示して)この部分が全結合層に相当することがわかります。

softmaxは、出力の総和が1になる活性化関数です。これもlinearとペアになっているので、全結合層が2段重なっていることがわかります。

variableは、ファイルから読んだ内容を使うというオペレータです。この行の意味はfc7_blob2.datというファイルから読んだ4,096要素のベクトルをvariable_29と呼ぶというものです。

次の行は、4,096×4,096の行列を読んで、variable_28と呼んでいます。これらのvariableがlinearの入力になっているので、まとめると図のような計算が行われることになります。

今見たのはVGGの一番後ろの部分です。

NNEFで畳み込みを行う方法

VGGは画像を扱うニューラルネットワークなので、上のほうには画像処理向きの層が置かれています。画像処理では、画像から特徴を取り出すためにしばしばフィルタが用いられます。このフィルタの係数を重みとすることで、適切なフィルタを学習で獲得しようというのが畳み込み層です。

NNEFで畳み込みを行うにはconvを使います。

convの引数は入力画像、フィルタ、バイアス。続いてフィルタが入力画像を読む間隔を指定するdilation、入力画像の周囲に枠を追加するpadding、フィルタが何ピクセルずつ移動するかを表すstrideです。

入力画像が3チャネル、出力画像が4チャネルの場合、それぞれのチャネルの組み合わせに対するフィルタが必要になるため、フィルタは3×3×3×4のテンソルになります。

VGGでは、R、G、Bの3チャネルの画像が入力されたあと、最初の畳み込み層で64チャネルに変換され、次の畳み込み層では64チャネルから64チャネルに変換されます。

そのため最初の畳み込み層のフィルタは3×3×3×64のテンソルになっています。(スライドを示して)このテンソルを利用するこの部分が最初の畳み込み層で、次の3×3×64×64のテンソルをフィルタとして使うこの部分が、2つ目の畳み込み層です。

2つ目の畳み込み層の後にはmax_poolが続いています。

Max Pooling層は、フィルタの範囲内で最大の値を出力画像に吐きます。引数のpaddingは入力画像に枠を設定するもので、sizeはフィルタの大きさ、strideはフィルタがピクセルずつ移動するかを指定します。strideが2なので、画像は半分のサイズに縮小されます。

(スライドを示して)今見たのはVGGのこの部分です。

7×7×512の画像を一直線のベクトルに変換

ここから先は、同じような層が続いているのですが、この部分で7×7×512の画像を一直線のベクトルに変換する必要があります。

reshapeは、バッファの内容の解釈を変更するオペレータです。ここでは、画像だったものを一次元のベクトルに変換しています。ファイルから重みなどの値を読んで、畳み込みとMax Poolingをひたすら繰り返し、最後に全結合層を2段経てVGGの完成です。

各種オペレータの紹介

NNEFには、VGGで使った以外にもさまざまなオペレータがあります。constantは、値が一定のバッファを作ります。deconvは逆畳み込みを行います。boxはフィルタの範囲内の総和を出力画像に書きます。argmax_poolはフィルタの範囲内で、最大の値を持つ要素の値ではなく、インデックスを出力に吐きます。sampleはargmax_poolなどで得たインデックスを使って、インデックスで他のテンソルから値を拾ってきます。

reduceは特定の軸方向のすべての値を集約する演算を行い、テンソルの回数を1つ下げます。splitはテンソルを特定の軸方向に分割して、concatは逆に2つのテンソルをくっつけます。padはテンソルに枠を付けます。convでも枠を付けることはできますが、convの演算を行わずに枠だけを付けたい場合に使うのがpadです。

gatherは特定の軸方向の値の中から1つをインデックスで選び、castはテンソルの要素の型を変換します。matmulは行列と行列の積を計算して、transposeは行列を転置します。

VGGで使ったRectified Linear Unitとsoftmax以外にもいろいろな活性化関数が用意されています。プーリングもmax_pool以外のものがいくつか用意されています。l1_normalizationのような正規化を行うオペレータも用意されています。その他、関心領域プーリングや画像のリサイズのためのオペレータも備わっています。

自作のライブラリをNNEFに対応させるための実装

うちではGCTというライブラリを作っています。これは、GPUを使うアプリケーションがよく行う処理を簡潔に書けるようにするためのものです。ニューラルネットワークの評価はよく行う処理なので、NNEFを渡したらGPUでシュッと実行できるようにしたいところです。

(スライドを示して)差し当たって、VGGを動かすために必要なオペレータは、この8つです。このうち3つは実際にGPUで何かを実行するものではないので、残りの5つを実装していきます。

畳み込みです。入力用のバッファ、フィルタのバッファ、出力用のバッファ、バイアス用のバッファを受け取り、フィルタサイズなどのパラメーターで特殊化できるようにしておきます。出力画像の1ピクセルを1スレッドが担当します。複数のスレッドが同じフィルタを使うので、共有メモリにフィルタをロードします。入力値とフィルタをかけてバイアスを足して出力に書きます。

linearを実装します。でかい行列とベクトルの積をたくさんのスレッドで計算するには、水平加算を使います。行列の1要素につき1スレッドで乗算を行い、水平加算で総和を取ります。

Max Poolingを実装します。畳み込みと同じようにフィルタサイズなどで特殊化をできるようにしておいて、フィルタの範囲内で最大の値を出力に書きます。

Rectified Linear Unitを実装します。この活性化関数は入力値と0のうち、大きいほうを出力するだけです。

softmaxを実装します。softmaxには入力値のexponentialの総和が必要です。そのため、共有メモリで値を共有できる最大のスレッド数である1,024スレッドの中で計算を行います。入力値のexponential÷入力値のexponentialの総和を求めます。

NNEFの公式ライブラリにNNEFのパーサが含まれているので、これを使ってNNEFをパースします。

NNEFを読み込んで、必要なファイルの内容をGPUに送ります。各層の処理をするパイプラインを作り、バッファを結び付けます。コマンドバッファにメモリバリアとパイプラインの実行を積みます。

画像に前処理をしてGPUに送ります。評価は画像1枚で行うので、バッチサイズを1に変更します。このモデルは画像を受け取ると、それが何かを表すIDを返すように学習がされています。それぞれのIDが何を表しているのかの対応表をImageNetから拾ってきます。

カルボナーラをビルドして写真を撮ります。この写真をGPUにおいてVGGを実行すると「この物体はカルボナーラである」という結果が返ってきます。レモンの画像を投げると「この物体はレモンである」という結果が返ってきます。ドライバーを投げると「この物体はドライバーである」という結果が返ってきます。

まとめ

まとめに入りましょう。NNEFは学習済みのニューラルネットワークをエクスポートするためのファイル形式です。グラフ定義部分はテキスト形式なので、人間が直接読み書きできます。ご清聴ありがとうございました。