KelpNetでVGG
今回は、KelpNetでVGG(をスケールダウンしたもの)を実装していきます。VGGは、2014年のILSVRCという画像認識の大会で2位(1位はGoogleNetです)になったネットワークであり、代表的な畳み込みニューラルネットワークの1つです。
今回の記事は、ニューラルネットワークの基礎+CNN、KelpNetの概要を理解している方を対象にしています。この記事を読み終わるころには、「VGGの概要」「VGGの特徴」「CIFAR-10とKelpNetでの使い方」「KelpNetでのVGGの実装方法」が分かるようになっており、順番もこの説明で行っています。
ニューラルネットワークの基礎・KelpNetの概要は、それぞれ以下の記事で紹介しています。
また、今回用いたコードは全てこちらにあります。
VGGの概要
VGGは、3×3の畳み込み・最大値プーリング・全結合層から構成されるとてもシンプルな構造のネットワークです。この、「3×3の小さなフィルタのみ用いていること」がVGGの大きな特徴です。VGGは、224×224のRGB画像を入力として、1000クラスの分類を学習することが出来ます。
下図はVGGの概要です。畳み込み層の上の数字は、出力するチャンネルの「縦のサイズ × 横のサイズ × 枚数」となっております。また、全結合層上の数字はユニット数となっています。各畳み込み層では幅1のパディングを行うことでチャンネルのサイズを変えずに、プーリング層でチャンネルのサイズを半分にしています。
VGGの特徴
VGGの畳み込み層は、3×3のサイズのフィルタのみを用いています。**3×3という小さなフィルタの畳み込み層を積み重ねることによって、5×5や7×7など大きなフィルタの畳み込み層と同様の受容野を持つことが出来ます。受容野とは、入力を行う領域のことです。
下図のように3×3の畳み込み層を2つ積み重ねることによって、5×5の畳み込み層の受容野と同じ大きさの受容野を持つことが出来ます。同様に、3つ積み重ねることによって7×7畳み込み層の受容野と同じ大きさの受容野を持つことが出来ます。VGGでは、従来5×5や7×7のフィルタを用いた畳み込み層を全て以下のように3×3に分解して用いています。**
このように、5×5や7×7ではなく3×3の畳み込みを行っているのには以下の2つの理由があります。
- 活性化関数(ReLU)を組み込む回数を増やすことによって、特徴をより区別しやすくする。
- パラメータ数(重みの数)を減らし、正則化をかける。
5×5の畳み込みの代わりに3×3の畳み込みを2回、もしくは7×7の畳み込みの代わりに3×3の畳み込みを3回行うことが出来ます。また、その過程で同時に活性化関数(ReLU)にかけることが可能になります。このように、3×3の畳み込みを行うことによって活性化関数にかける回数が増え、特徴判断がより段階的に行えるようになります。これによって、画像の特徴をより区別しやすくなるようにしています。
また、大きなフィルタを用いるよりも小さなフィルタを用いて段階的に畳み込みを行うことで、パラメータ数(重みの数)を減らすことが出来ます。例として、チャンネル数を入力出力ともにCと固定した場合を考えてみます。7×7の畳み込みを1回行う場合、重みの数はとなります。また、3×3の畳み込みを3回行う場合、重みの数はとなります。これにより、約45%のパラメータ数の削減が出来ていることがわかります。
- C: 入出力のチャンネル数
- 7×7の畳み込みを1回行う場合のパラメータ数
- 3×3の畳み込みを3回行う場合のパラメータ数
CIFAR-10
今回は、VGGでCIFAR-10というデータセットを使って学習を行います。CIFAR-10とは、以下のようなデータセットです。
- 32x32のRGB画像
- 訓練データが50000枚、テストデータが10000枚
- airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truckの10クラス
KelpNetでCIFAR-10
KelpNetでは、CIFAR-10を簡単に扱うことが出来るように、CIFAR-10用のデータローダーが用意されています。ここでは、それらの導入と使用方法について説明します。KelpNetの導入の際と似たようなことを行いますので、こちらのKelpNetの導入の記事も参考になるかもしれません。こちらでは画像を用いてより詳しく解説されています。
まず、クローンしていない方はこちらからKelpNetをクローンします。
git clone https://github.com/harujoh/KelpNet.git
クローン出来たら、VisualStudioでKelpNetのソリューションファイルであるKelpNet/KelpNet.slnを開きます。次に、開いたKelpNetのソリューションをビルドします。すると、KelpNet/KelpNetTester/bin/Debug/ に複数DLLファイルが作成されていると思います。ここで、DLLとはダイナミックリンクライブラリの略であり、プログラムの実行時にリンクされるライブラリのことです。
このDLLファイルのうち、CIFARLoader.dllとTestDataManager.dll(入れてない方はKelpNet.dllとCloo.dllも)を、VisualStudioで作成したプロジェクトフォルダ(おすすめはコンソールアプリ .NET Frameworkのバージョン4.5です)にコピーしてください。
次に、VisualStudioのソリューションエクスプローラーの参照を右クリックし、参照の追加を選択します。すると参照マネージャーが出てくるので、左側の参照タブをクリックした後に右下の参照をクリックすると、参照するファイルの選択というウィンドウが出てきます。そこで、先ほどのdllファイルをそれぞれ追加してください。(ここで、まだ追加していない方は、アセンブリタグからSystem.Drawingも追加してください。)
次に、KelpNet/KelpNetTester/TestData/にあるCifarData.csとTestDataSet.csを自分のプロジェクトフォルダに置きます。このとき、それぞれのコードのnamespaceをKelpNetTester.TestDataから自分のプロジェクトのnamespaceに変更してください。これで、KelpNetでCIFAR-10を使用する準備は終了です。
KelpNetでVGG (mini version)
それでは、KelpNetでVGGを実装していきます。今回は、より多くの方が学習を実行できるように、VGGをスケールダウンしたモデルを実装します。
今回の学習は、私のマシンではGPUで約5時間かかりました。CPUの場合は10倍以上の時間がかかると思いますので、可能であればGPUでの学習をお勧めします。
モデルの定義
先ほど説明したVGGをスケールダウンするために、上図のように最初の4層分の畳み込み層を削除し、フィルタサイズ・ユニット数を1/4にしています。
またKelpNetの場合、誤差関数のSoftmaxCrossEntropyにSoftmax関数が内包されているため、出力層の活性化関数としてFunctionStackの末尾にSoftmax関数を追加する必要はありません。
//ネットワークの構成を FunctionStack に書き連ねる FunctionStack nn = new FunctionStack ( /* 最初の4層の畳み込み層は使用しない new Convolution2D (3, 64, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (64, 64, 3, pad: 1, gpuEnable: true), new ReLU (), new MaxPooling(2, 2, gpuEnable: true), new Convolution2D (64, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (128, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new MaxPooling(2, 2, gpuEnable: true), */ // (3, 32, 32)のサイズ画像を入力 new Convolution2D (3, 64, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (64, 64, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (64, 64, 3, pad: 1, gpuEnable: true), new ReLU (), new MaxPooling (2, 2, gpuEnable: true), // (64, 16, 16) new Convolution2D (64, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (128, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (128, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new MaxPooling (2, 2, gpuEnable: true), // (128, 8, 8) new Convolution2D (128, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (128, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new Convolution2D (128, 128, 3, pad: 1, gpuEnable: true), new ReLU (), new MaxPooling (2, 2, gpuEnable: true), // (128, 4, 4) new Linear (128 * 4 * 4, 1024, gpuEnable: true), new ReLU (), new Dropout (0.5), new Linear (1024, 1024, gpuEnable: true), new ReLU (), new Dropout (0.5), new Linear (1024, 10, gpuEnable: true) ); //optimizerを宣言 nn.SetOptimizer (new Adam ());
学習部分
今回は、10epoch分の学習を行います。new CifarData()
によってCIFAR-10の訓練・テストデータを用意します。次に、cifarData.GetRandomXSet(BATCH_DATA_COUNT)
によって指定したバッチサイズ分のデータを訓練データから取得します。最後に、Trainer.Train (nn, datasetX.Data, datasetX.Label, new SoftmaxCrossEntropy ())
によって取得した訓練データを用いて学習を行っています。
// CIFAR-10の訓練・テストデータを取得 CifarData cifarData = new CifarData (); // 全てのデータを何回分使うか for (int epoch = 1; epoch < 10; epoch++) { // 何回バッチを実行するか for (int i = 1; i < TRAIN_DATA_COUNT + 1; i++) { //訓練データからランダムにデータを取得 TestDataSet datasetX = cifarData.GetRandomXSet (BATCH_DATA_COUNT); // 取得したデータを用いて学習を行う Trainer.Train (nn, datasetX.Data, datasetX.Label, new SoftmaxCrossEntropy ()); } }
テスト
最後に、テストを行います。cifarData.GetRandomYSet (TEACH_DATA_COUNT)
によって指定した数のテストデータを取得し、Trainer.Accuracy (nn, datasetY.Data, datasetY.Label)
によって取得したデータから認識率を求めています。
//テストデータからランダムにデータを取得 TestDataSet datasetY = cifarData.GetRandomYSet (TEACH_DATA_COUNT); //テストを実行 Real accuracy = Trainer.Accuracy (nn, datasetY.Data, datasetY.Label); Console.WriteLine ("accuracy " + accuracy);
学習結果
学習は、以下のような結果となりました。epochが進むにつれて認識率 (Accuracy) が上がっていることが分かります。