External Memory

プログラミング周辺知識の備忘録メイン

畳み込みニューラルネットワーク VGGNetとResNet + α

VGGNetとResNetはILSVRC competitionで優秀な畳み込みネットワークをだったもの。
それぞれ2014年準優勝、2015年優勝。

VGGNet

https://arxiv.org/pdf/1409.1556.pdf

3*3、または1*1の小さなフィルターサイズconv層を積層した深いCNNで16-19layer構成である。3*3で小さいというのはAlexNetの11*11 convなどと比較してのことだろう。


基本的な全体的な構造として、input layerおよびpooling layer(max pooling)間にconv層を連続して2-3層積層しており、pooling layerごとにチャンネル数は2倍、最後はFully-Connected(FC) 3層、softmax層である。

これは3*3のconv2層および3層は、それぞれ5*5、7*7のフィルターと空間的に同じ効果であるという考えからくる。3*3conv3層は7*7conv1層よりも非線形ReLU層の数の違いによりより弁別的で、3*3conv3層に置換した場合パラメータ数を55%ほど減らせるため正則化を適用しているとみなすことが出来るという利点があると述べている。

1*1conv層はNetwork in Networkでも述べられているように、
線形変換と非線形ReLU関数が導入されるのでよりネットワークが非線形性を増大させる効果がある。
この文献中では1*1conv層追加挿入で精度向上の効果はあったが、同じネットワーク構造で比較して1*1conv層を3*3conv層に置き換えた方が精度が良かったようである。



実際の学習時には、深いネットワークで学習するとき最初の4つのconv層と最後の3つのfully-connected層の重みの初期化には、浅いモデル(Table 1 A)で学習した重みパラメータを利用し、学習時に学習率を減らさずパラメータの変更を許している。


19 layer構成に対し、単一モデルでILSVRC-2012 datasetの分類でtop1 error 24.4%、top5 error 7.1%である。
この時学習時において画像サイズは384pixelから256-512pixelの範囲でscale jitteringされ、
テスト時は256,384,512の3種類でスケール変更して行われている。
このとき画像サイズに柔軟に対応するため、
FC層は7*7conv層と1*1conv層に置き換えられ(完全畳み込みネットワークFCN)、
最後にGlobal Average Poolingが用いられている。
FCNは計算効率や精度の面から画像をcropするよりは都合がよい。


またイメージサイズを256や386固定でrescaleして事前学習するなど学習速度にいろいろ工夫があるが、
"four NVIDIA Titan Black GPUs, training a single net took 2-3 weeks depending on the architecture."
とある。


ResNet

https://arxiv.org/pdf/1512.03385.pdf

深いネットワークモデルにおけるtraining accuracyの劣化(trainingの劣化なので過学習が原因ではない)の解決には、追加された層がidentity mappingであり、他の層が浅いモデルのコピーであるという考え方を用いる。


ここでは"shortcut connections"がIdentity Mappingを行う。
ショートカットの概念はHighway Networkのgating functionsにもあり、深いモデルに対してうまく機能している。
畳み込みネットワークの構造例3 All-CNN,Highway Network - External Memory

gating functionsと比べると、パラメータフリーである点と常にresidual functionsが機能している点で異なっている。よってHighwayでは100layer以上では精度が得られていない。


追加された層がIdentity Mappingとして構築された場合、深いモデルは浅いモデルよりエラーが大きくなることはないはずだが、ショートカットなしの場合は非線形層がIdentity Mappingを困難にしているようである。

実際Identity Mappingが最適となれば、Identity Mappingに近づくために非線形層の重みはゼロに向かう。よって最適関数がIdentity Mappingに近い場合は、Identity Mappingを参照にして摂動が見つかりやすくなる。
この考えよりIdentity Mappingにショートカットが用いられるのである。


residual mappingとショートカットのIdentity Mappingは以下のように表される。
\mathbf{y}=\mathcal{F}(\mathbf{x},{W_i})+\mathbf{x}
layerを2つショートカットするなら、
\mathcal{F}=W_2\sigma(W_1\mathbf{x})
となる。


層間で次元が変わる場合(poolingやstraide 2以上のconv)、ゼロパディングかもしくは以下の式(liner projection shortcuts)で次元を変える。
\mathbf{y}=\mathcal{F}(\mathbf{x},{W_i})+W_s\mathbf{x}


この文献においては、ゼロパディングとProjection Shortcutsで比較するとわずかにProjection Shortcutsのほうが良い性能が得られており、
また次元変更時のみでなくすべてのショートカットでProjection Shortcutsを用いることでさらに性能が向上している結果が得られているようだが、
著者はその差はわずかと判断して次元変更時のみProjection Shortcutsを適用している。


またResNetではpooling layerの代わりにstride 2のconvolution layerを用いている。
これはAll-CNNでも見られた手法である。


ネットワーク構造はVGGnetsを参考にしているようである。
文献中Table 1からわかるように全体のlayerの数によってショートカット距離や畳み込みのフィルターサイズ、チャンネル数が違っている。
チャンネル数は基本的にはoutputのサイズが1/2になればチャンネル数を2倍にしている。つまりstride 2のconvolution layer後のチャンネル数が2倍である。

(追記9/26:活性化関数の前にBatch Normalization(BN)を行っている。identity mappingと足し合わせた後reluを適用しているが、この場合BNは足し合わせる前にresidualのみにおいて行われる。)

50 layer以上積層した時に見られる構造をBottleneck Architecturesと呼んでおり、
これは精度向上ではなく経済的な問題でBottleneck選択しているようである(ボトルネックでないほうが精度は出るようである)。
また最後にGlobal Average Poolingを用いている。
(追記9/26:global average pooling後に1*1特徴マップのチャンネル数が所望のラベル分類数となるようにfully-connected layerを追加している。これはFCNの性質を壊さない。)


ImageNet 2012 classification datasetにおいて、152 layer ResNetで単一モデルtop-5 validation error は 4.49%、
6つの異なる深さのアンサンブルモデルではtest setにおけるtop-5 errorは3.57%である。


また、CIFAR10に対しては110層モデルで6.43%のerror率である。
1202層を積層しても7.93%error率で大幅に性能が劣化していない。
ここで110layerにおける学習時にウォーミングアップとして初期学習率0.01でerror率80%以下にした後、学習率を0.1に上げている。恐らく初期の学習が学習率0.1ではうまく進まなかったのだろう。


しかしCIFAR10でパッチサイズ128の64000stepは20layerでも個人的には計算時間的に難ありのように感じる…。

GoogLeNet Inception-v4, Inception-ResNet

https://arxiv.org/pdf/1602.07261.pdf

inceptionモデルは全体的な構造を維持した状態で、サブネットワークの調整を重ねに重ねたような複雑な構造をしているように見える。
文献に記載されているように、大きくアーキテクチャーを変更することに関して慎重だったらしく、単純化されず複雑化したという記載がある。
実際文献中に図示されているサブネットワーク構造を見ると、他のニューラルネットワークと比べて非常に複雑であるように見える。

この文献だけでは、どのような考え方でこのような構造になったのかの詳しい記載はなかった。

ILSVRC 2012 classificationでのInception-v4, Inception-ResNetのアンサンブルのtop5エラー率は3.08%で性能自体はかなりよく見える。


目についた記述としては、
Inception-ResNetではResNetの非常に深いモデルにおいて学習初期にニューロンが死ぬ問題を、residualにファクターを掛けることで回避している。
つまり
\mathbf{y}=\mathcal{F}(\mathbf{x},{W_i})+factor*\mathbf{x}
ということ。
ここでResNetでの初期学習の不安定性について言及している。

Network in NetworkでCIFAR-10分類

畳み込みニューラルネットワークにはいろいろなバリエーションがある。
その一つとしてNetwork in Networkを用いてCIFAR-10の分類を行った。
元文献ではerror率8.8%ほどまで達成している。
Network in Networkは以前書き下した以下のようなものである。
畳み込みネットワーク(CNN)の構造例2-Network in NetworkとMaxout networks - External Memory

元文献は
http://arxiv.org/abs/1312.4400


Network in Networkはmaxoutの派生のようなものである。
conv層の後にパーセプトロン層を複数、次いでpooling層からなるmlplayerと呼ばれる単位の構造からなっている。
今回実際に分類に用いる構造はmlplayer 3層である。
ネットワークの構成は論文中にはmaxoutに従ったとあるので、maxoutコードを参考にした。
https://github.com/lisa-lab/pylearn2/tree/master/pylearn2/scripts/papers/maxout

mlplayer中身の構成はパーセプトロン2層にpooling層(max_pooling window size 3 stride 2)、パーセプトロン層はカーネルサイズ1*1のconv層に等価である(文献ではパーセプトロン3層)。

最後のmlplayerはpooling層の代わりにglobal average poolingを用いた。
global average poolingはカーネルサイズが特徴マップサイズのaverage poolingと等価である。

data augmentationを行って、画像サイズは32から24に変わっているので結局構成は

conv6*6 - (conv1*1)*2 - max_pooling - (dropout) -
conv4*4 - (conv1*1)*2 - max_pooling - (dropout) -
conv3*3 - (conv1*1)*2 - global_ave_pool

conv x*x のx*xはカーネルサイズである。チャンネル数はmlplayerごとにそれぞれ96,192,192。

optimizerはadamで学習率は基本は0.001固定、weight decayの正則化による重み自由度制限は行っていない(元文献では行っている)。
また、ハイパーパラメータの最適化も行っていない。
dropoutはmlplayer層間に挿入しているが、今回はdropoutなしの方が分類精度は良かった。

構造構築の部分だけコードを抜粋すれば以下のようになる。

def inference(data,istrain):
    
    model = Conv_net(0.001,10,istrain)
    model.conv2d(6,6,3,96,"conv1-1",inpt=data)
    model.conv2d(1,1,96,96,"conv1-2")
    model.conv2d(1,1,96,96,"conv1-3")
    model.max_pool(ksize=[1,3,3,1])
    #model.drop_out(0.5)
    model.conv2d(4,4,96,192,"conv2-1")
    model.conv2d(1,1,192,192,"conv2-2")
    model.conv2d(1,1,192,192,"conv2-3")
    model.max_pool(ksize=[1,3,3,1])
    #model.drop_out(0.5)
    model.conv2d(3,3,192,192,"conv3-1")
    model.conv2d(1,1,192,192,"conv3-2")
    model.conv2d(1,1,192,10,"conv3-3")
    
    model.avg_pool(ksize=[1,6,6,1],strides=[1,6,6,1])
    model.reshape([-1,10])

    return model

当初、出力のほとんどがゼロで学習が進まなかったので、
・二つ目のpooling層はmax→averageに変更
・学習率変更
・バイアス初期値ゼロから少しずらし
をそれぞれ行うことでゼロ勾配を避けることができた。
結局バイアス初期値をずらすやり方が安定でハイパーパラメータ選択を自由に行えたので特別なことがなければ今後はこれを利用する。

以下の出力は10 epoch刻み100 epochまでで正答率が最大のものである。

  epoch         loss     accuracy
------------------------------------
second pooling layer "average pooling"
    100       1.6152       0.7947

learning rate 0.0001 
    100       1.4743       0.5413

bias init 0.1
     90       0.5381       0.8157

dropoutなし + bias init 0.1
     50       0.5829       0.8067
     90       0.5251       0.8306

前回行ったCIFAR-10サンプルコードで正答率は50 epochで正答率80%弱なので最適化なしでも若干学習速度が速い。
とはいえ期待したような性能を引き出すことはできなかったが。
空いた時間があるときにプログラムを動かして、90%辺りを目指して最適化を試みることにする。

あとは学習時間がかなり長いので(100epochで12h)、訓練データをきちんと分けて再学習が出来るようにしたほうが良かったということと、1-5 epoch程度の学習でハイパーパラメータのある程度狙いをつけられるようにしたい。

畳み込みニューラルネットワークのプログラム作成とCIFAR10分類

いろいろな畳み込みニューラルネットワークの構造を試すために、たたき台となるようなプログラムを作成した。
主にCIFAR-10の分類に使用すると思う。

作成したとはいっても、下記URL tensorflow CIFAR-10用のサンプルコードとネットワーク構造はほぼ同じで、重み、バイアス初期化やoptimizerを少し変更している。
models/tutorials/image/cifar10 at master · tensorflow/models · GitHub


このサンプルコードでは以下のような手法を使っている。

  • overlap pooling

pooling操作時にウィンドウサイズに対してスライド距離が小さい場合、それぞれのウィンドウ同士が重なる。
サンプルの場合ウィンドウサイズは3、スライド距離は2である。

  • data argumentation

データを増やすため、データの入力に対してランダムに反転、切り抜き抽出、正規化、輝度・コントラスト調整を行っている。

  • local response normalization

AlexNetでも使用されているlocal_response_normalizationは名前の通り、空間位置ごとの特徴マップ間活性化値の正規化である。
局所的なコントラストの違いを調整するような働きをすると思われるので自然画像のようなサンプルに適しているだろうと思う。
local_response_normalizationはAlexNetでは畳み込み層の直後だが、
サンプルコードでは最初のnormalizationはpooling層の直後だったので今回はこちらに従った。個人的にはpooling層の手前に配置するほうが、pooling層をうまく使えているような気がするが。

  • stepに対して学習率を減衰させる勾配降下法

収束を速めるために350 epochごとに学習率を0.1倍にまで減衰させている。
指数関数的減衰を使用している。350 epochで0.1倍に減衰する。

プログラム

実際に作成したプログラムは以下の通りである。
CIFAR-10データのinputに関するところは、サンプルコードと内容は同じなので省略。

import tensorflow as tf
import numpy as np
import os


NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN = 50000
NUM_EXAMPLES_PER_EPOCH_FOR_EVAL = 10000

class Conv_net():
    
    def __init__(self,l_rate,num_label,istrain):
        self.l_rate = l_rate
        self.num_label = num_label
        self.istrain = istrain
        
    def conv2d(self,p_height,p_width,ch,next_ch,name,strides=[1,1,1,1],inpt=None):
        
        W = tf.Variable(tf.truncated_normal([p_height, p_width, ch, next_ch],
                            stddev=1.0 / tf.sqrt(float(p_height * p_width))),name=name+"weight")
        b = tf.Variable(tf.zeros([next_ch]),name=name+"bias")
        
        if inpt == None:
            self.y = tf.nn.relu(tf.nn.conv2d(self.y, W, strides=strides, padding='SAME')+ b)
        else:
            self.y = tf.nn.relu(tf.nn.conv2d(inpt, W, strides=strides, padding='SAME')+ b)

        
    def max_pool(self,ksize=[1,2,2,1],strides=[1,2,2,1]):
        self.y = tf.nn.max_pool(self.y, ksize=ksize,
            strides=strides, padding='SAME')
    
    def lrn(self,depth_radius,bias,alpha,beta):
        self.y = tf.nn.local_response_normalization(self.y,depth_radius=depth_radius,
                                                    bias=bias,alpha=alpha,beta=beta)
    
    def fully_connect(self,unit,next_unit,name):
        W = tf.Variable(tf.truncated_normal([unit, next_unit],
                        stddev=1.0 / tf.sqrt(float(unit))),name=name+"weight")
        b = tf.Variable(tf.zeros([next_unit]),name=name+"weight")
        self.y = tf.reshape(self.y, [-1, unit])
        self.y = tf.nn.relu(tf.matmul(self.y, W)+ b)
    
    def drop_out(self,rate):
            self.y = tf.layers.dropout(self.y, tf.constant(rate),training = self.istrain)
            
    def output(self,unit,name):
        W = tf.Variable(tf.truncated_normal([unit, self.num_label],
                    stddev=1.0 / tf.sqrt(float(unit))),name=name+"weight")
        b = tf.Variable(tf.zeros([self.num_label]),name=name+"bias")

        self.y = tf.matmul(self.y, W)+ b
        
    def train(self,epoch,labels,data_length,batch_size):
        
        cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels = labels,logits = self.y))
        train_op = tf.train.AdamOptimizer(self.l_rate).minimize(cross_entropy)
        
        saver = tf.train.Saver(max_to_keep=11)
        sess = tf.InteractiveSession()
        tf.global_variables_initializer().run()

        tf.train.start_queue_runners(sess=sess)
        for i in range(epoch*data_length//batch_size):
            _,loss = sess.run([train_op,cross_entropy])
            
            
            if i % (data_length*epoch//(batch_size*10)) == 0:
                print("{0:>7}{1:>13.4f}".format(i*batch_size//data_length,loss))
                saver.save(sess,"tmp/cifar10",global_step=i*batch_size//data_length)   
                
        print("{0:>7}{1:>13.4f}".format(epoch,sess.run(cross_entropy)))
        saver.save(sess,"tmp/cifar10",global_step=epoch)   
        
        
        return cross_entropy
    
    def print_accurency(self,p_epoch,labels,batch_size):
        cross_entropy = tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(labels = labels,logits = self.y))
        top_k_op = tf.nn.in_top_k(self.y, labels, 1)
        
        num_iter = int(NUM_EXAMPLES_PER_EPOCH_FOR_EVAL / batch_size)
        true_count = 0
        total_loss = 0
        total_sample_count = num_iter * batch_size
        step = 0
        while step < num_iter:
            loss,predictions = sess.run([cross_entropy,top_k_op])
            true_count += np.sum(predictions)
            total_loss += loss
            step += 1
        precision = true_count / total_sample_count
        loss = total_loss / num_iter
        print("{0:>7}{1:>13.4f}{2:>13.4f}".format(p_epoch,loss,precision))

#----一部省略------

def inference(data,istrain):
    model = Conv_net(0.001,10,istrain)
    model.conv2d(5,5,3,64,"conv1",inpt=data)
    model.max_pool(ksize=[1,3,3,1])
    model.lrn(4,1.0,0.001/9.0,0.75)
    model.conv2d(5,5,64,64,"conv2")
    model.lrn(4,1.0,0.001/9.0,0.75)
    model.max_pool(ksize=[1,3,3,1])
    model.fully_connect(36*64,384,"fully1")
    #model.drop_out(0.5)
    model.fully_connect(384,192,"fully2")
    #model.drop_out(0.5)
    model.output(192,"output")
    
    return model
    
if __name__ == '__main__':
    #CIFAR-10
    epoch = 50
    
    train_data, train_label = distorted_inputs("cifar-10-batches-bin",50,
                                               img_size = 24,crop=True,flip=True,
                                               brightness=True,contrast=True)
    print("{0:>7}{1:>13}".format("epoch","train_loss"))
    print("-------------------------------------")
    train_cifar10 = inference(train_data,True)
    train_cifar10.train(epoch,train_label,NUM_EXAMPLES_PER_EPOCH_FOR_TRAIN,50)
    
    
    print("{0:>7}{1:>13}{2:>13}".format("epoch","loss","accuracy"))
    print("------------------------------------")   
       
    for i in range(0,epoch+1,epoch//10):
        
        tf.reset_default_graph()
        
        test_data, test_labels = inputs(True,"cifar-10-batches-bin",50,img_size = 24)
        eval_cifar10 = inference(test_data,False)
        
        sess = tf.InteractiveSession()
        saver = tf.train.Saver()
        saver.restore(sess,"tmp/cifar10-{}".format(i))
        
        tf.train.start_queue_runners(sess=sess)
               
        eval_cifar10.print_accurency(i,test_labels,50)
        


sparse_softmax_cross_entropy_with_logits関数に関して、入力の形式が示している通りそれぞれのラベルが離散的でが確率に対して排他的なら使用可能である。
MNISTやCIFAR-10などのよくある離散的な分類問題では問題なく使えるだろう。
softmax_cross_entropy_with_logitsは排他的でなくても使える。
何故sparseと名付けているのかよくわからないが、排他的な確率前提の出力なら比較的にスパースっぽい気もする。

tf.nn.in_top_k関数はターゲットがpredictionのtop kにあるかによってbool値のtensorを返す。k=1の場合は、予測の最大値とlabelが一致していればTrueを返す。

CIFAR-10 学習結果

プログラムを実行して、CIFAR-10分類精度は以下のようになる。
上記の手法を使用したときの精度の差異を比較できるようにした。。
baseのネットワーク構造は単純なCNN (conv + maxpooling)*2 + 全結合層*2 である。

  epoch         loss     accuracy
------------------------------------
base
     45       3.0144       0.6869

base + dropout
     50       1.3974       0.7237

base + local_response_normalization
     45       2.8104       0.6932

base + overlap pooling
     30       2.3325       0.7137

base + data argumentation + overlap pooling
     50       0.6110       0.7947

base + data argumentation + overlap pooling + local_response_normalization
     50       0.6139       0.7880

baseの実行時にepochに対してlossが徐々に低下したため全結合層にdropoutしたものを付け加えた。

local_response_normalization適用時に、
train_lossが他の系よりかなり小さかったのに対し、testデータに対するlossは大きかった。原因としてコントラスト調整で過学習気味になりやすいというのはすぐ思いあたるが、AlexNetでは過学習の抑制に苦労したらしいので、このあたりの影響もあったのだろうか?

そこで過学習抑制のためのdata argumentationと組み合わせてlocal_response_normalizationを適用させたが、逆に精度は落ちた。
後で調べてみると、local_response_normalizationはあまり効果がなくてあまり使用されていないようである。
CS231n Convolutional Neural Networks for Visual Recognition


data argumentationを適用したものに関しては、epoch 50程度ではまだ精度は飽和していないようだった。ちなみにサンプルコードでは100 epoch程度以上で飽和するようなので、学習量は足りてない。これに関しては、別の検証時に行うことにする。


data argumentationを適用する前の入力データ量は32*32*3であり、適用後は24*24*3である。Core i5-7600 CPUでepoch 50の学習時間は32サイズで3時間弱ほどかかり、local_response_normalizationを適用するとさらに倍近くの時間がかかる。
24サイズでは時間短縮できるが、大抵のCNNはかなり深いので学習時間の問題もそれなりに考えなければならない。