Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

歴史をたどってディープラーニングを学ぶ 第十一回 VGGをC#で実装して学ぶ

こんにちは、ニューラルネット老人こと糟谷勇児です。
今回はVGGを自作して学ぼうと思います。しかし、ニューラルネットガチ勢からすると、ディープラーニングのフレームワークでいいですし、ゆるふわ勢もフレームワークでいいので、自作はどういう層に響くのでしょうか。
まあ、あまり気にせず今回もやっていきましょう。

VGG概要

VGGはオックスフォード大学のVGG(Visual Geometry Group)というチームが開発したニューラルネットです。

VGGは2014年のこちらの論文にて紹介されています。
https://arxiv.org/pdf/1409.1556.pdf

2014年ということは、もう現代と6年差ですね。いくらDogYearと言っても、6歳と言ったら犬でも散歩大好きな年齢です。老人でもなくなってきました。
実際VGGは現在でも結構使われています。先日オンライン参加した2020年5月のPRMUの研究会でもいくつかの発表でVGG16の学習済みモデルを使用していました。
VGGというとVGG16,19の学習済みモデルがよく使用されているのですが、元の論文では11層のA、13層のBも紹介されています。
11層ならなんか作れそうですよね。

f:id:kasuya_ug:20200927231245p:plain
"Very Deep Convolutional Networks for Large-Scale Image Recognition"より引用

ちなみに、11層のAを最初に学習して、それを初期値としてより層の深いVGGを学習するとのことです。

VGGの特徴

VGGは基本的にはAlexNetに近いのですが、いくつか違うところがあります。

  • コンボリューション層のフィルタのサイズが3×3のみで構成されている
  • コンボリューション層の次にはReLUによる活性化関数を挟む

AlexNetはいろいろなサイズのフィルタを使うので、高速化が難しく、大きいサイズのフィルタが精度低下の原因にもなっていたようです。
また、AlexNetは複数回のコンボリューションを行いますが、その後に活性化関数を挟みません。VGGはコンボリューションのあとに活性化関数をかませるので、早い段階から不要な特徴部分を選別する能力があると考えられます。

ちょうど前回、3×3のフィルタをSIMDで高速化したので、5×5を使うのはやめたいなと思っていて渡りに船というわけです。それでは早速実装していきましょう。

活性化層の実装

今までもReLUは使っていましたが全結合層とセットだったので、今回はReLUをコンボリューション層の後で使えるようにします。
実装としては二つのやり方があります。活性化層(Activation Layer)を新たに作るか、コンボリューション層の中で活性化関数をかけられるようにするかです。今回は活性化層を作る方式としました。理由としては、コンボリューション層と活性化関数を切り離して考えられるので実装が疎結合になるためです。
今後、活性化関数のパラメーターも学習できるようになるなど汎用性も高いですしね。

念のため、活性化層のバックプロパゲーションの数式を計算しておきます。
活性化層はニューロンの結合が一対一なので、下層の同じ位置のニューロンの出力のみに依存するため、微分の連鎖律のΣが消えて、バックプロパゲーションが簡単に計算できます。
f:id:kasuya_ug:20201002230646p:plain


この結果は以前お世話になったこちらのブログの数式と一致したので大丈夫でしょう。

数式で書き下す Convolutional Neural Networks (CNN) - Yusuke Sugomori's Blog


コンボリューション層の計算には、1ピクセルの出力当たり(下層の画像の平面数)×(フィルタのピクセル数)回の掛け算が必要になります。一方で、活性化層は出力する画像のピクセル数回の活性化関数の計算で出力を算出できます。下層の平面数が50で3×3のフィルタとすれば、単純計算で活性化層の計算量はコンボリューション層の計算量の450分の1となり、あまり全体の速度には関係ないということになります。
なので安心して数式通り実装してよいでしょう。100行もかからずに実装出来ます。

    public class ActivationLayer : Layer
    {
        public NeuralImage inputs;
        public override double e { get; set; }
        private activeFunction activeFunc;
        private differentialActiveFunction differentialFunc;
        private FUNC_TYPE funcType;

       public ActivationLayer(int underWidth, int underHeight, int underPlane, FUNC_TYPE funcType = FUNC_TYPE.ReLU)
        {
            this.dEdyNext = new double[underPlane][,];
            this.underPlane = underPlane;
            this.underWidth = underWidth;
            this.underHeight = underHeight;
            this.funcType = funcType;
            this.nextHeight = underHeight;
            this.nextWidth = underWidth;
            this.nextPlane = underPlane;

            outputs = new NeuralImage(nextPlane, nextWidth, nextHeight);
            if (funcType == FUNC_TYPE.Sigmoid)
            {
                activeFunc = NeuralFunc.Sigmoid;
                differentialFunc = NeuralFunc.Dsigmoid;
            }
            else if (funcType == FUNC_TYPE.LeakyReLU)
            {
                activeFunc = NeuralFunc.LeakyReLU;
                differentialFunc = NeuralFunc.DLeakyReLU;
            }
            else
            {
                activeFunc = NeuralFunc.ReLU;
                differentialFunc = NeuralFunc.DReLU;
            }
        }

        public override Layer Copy()
        {
            return new ActivationLayer(underWidth, underHeight, underPlane, funcType);
        }

        public override void MergeDeltaWeights(List<Layer> layers)
        {
        }

        public override NeuralImage CalcOutput(NeuralImage input)
        {
            inputs = input;
            return input.ApplyFunction(activeFunc, 0);
        }

        public override void CalcBackPropagation(NeuralImage dEdy)
        {
            for(int p = 0; p< underPlane; p++)
            {
                dEdyNext[p] = new double[underHeight, underWidth];
                for(int y = 0; y < underHeight; y++)
                {
                    for(int x =0; x < underWidth; x++)
                    {
                        dEdyNext[p][y, x] = dEdy.data[p][y, x] * differentialFunc(inputs.data[p][y, x], 0);
                    }
                }
            }
        }

        public override void updateWeight()
        {
        }
    }

VGGの実装

これで、VGGを作るパーツは整いました。早速、いつものCifar10の画像に使っていきたいのですが、ここで課題があります。VGGは5回のMaxPooling層を持っています。それぞれで画像の縦横が1/2のサイズになっていくので、五回行うと32×32の画素数のCifar10は1ピクセルまで小さくなってしまいます。

Cifar10にVGGを使用する方法としては、画像を拡大するというのもあります。ただ、画像を大きくしても情報量が増えているわけではないため、本末転倒感があります。
そこで今回は、MaxPoolingが2回になるようにVGGの上のほうの層を削りました。
具体的にはこんな感じ。

  1. コンボリューション層 フィルタ数20 サイズ 3×3 ストライド1
  2. 活性化層(ReLU)
  3. MaxPooling層  サイズ3×3 ストライド2
  4. コンボリューション層 フィルタ数20 サイズ 3×3 ストライド1
  5. 活性化層(ReLU)
  6. MaxPooling層  サイズ3×3 ストライド2
  7. コンボリューション層 フィルタ数20 サイズ 3×3 ストライド1
  8. 活性化層(ReLU)
  9. 全結合層(ReLU) ニューロン数120
  10. 全結合層(ReLU) ニューロン数84
  11. ソフトマックス層 ニューロン数2

VGG5とでも呼びますかね。

実験

では実験していきます。まず、コンボリューション層のフィルタ数を20にしてみました。これを学習してみると、前回のAlexNetの91.6%を上回る91.8%で、しかも今回は安定して高精度です。

f:id:kasuya_ug:20201004231507p:plain
VGGの精度と二乗誤差の推移

さらにフィルタの数を5ずつ増やして25のとき、30のときと学習させてみます。
92.0%まで精度が上がりました。これはすごい。

f:id:kasuya_ug:20201004231929p:plain
フィルタ数と精度

終わりに

VGGは単純な構造ですが、かなりすごいことが分かりました。もしかしたら自作勢ははじめはVGGを目指して作っていくのがいいのかもしれません。
速度も速く、3時間ぐらいで150エポックの学習ができました。

次回は2014年の技術、Global Average Poolingについて学んでいこうと思います。



▼本シリーズのほかの記事はこちら

buildersbox.corp-sansan.com

© Sansan, Inc.