hirohirohirohirosのブログ

地方国立大学に通う情報系学部4年

ゼロから作るDeep Learning 3 ステップ55~ステップ58 まとめ dezeroでVGG16を実装する

hirohirohirohiros.hatenablog.com
 dezeroで遂に画像認識に長けたモデルであるCNNとその代表VGG16を実装します!

ステップ55, 56

CNNの復習

 CNNは画像に関連するタスクによく用いられます.CNNの解説はゼロから作るDeep Learning1で詳細な解説がなされているので,ここでは簡単に復習をします.

畳み込み演算

 CNNも今まで作ってきたレイヤの組み合わせです.CNNには畳み込み層とよばれる層があります.これは,画像処理で言うところのフィルタ演算のことです.
 2×2の入力データに対して,同様に2×2のフィルタを用意します.このフィルタの各辺は入力データ以下のサイズです.畳み込み演算は,入力データに対してフィルタの窓を一定間隔で移動させながら適用させます.入力データとフィルタの一致する要素を乗算し,その和を求めます.フィルタの移動する間隔をストライドと言います.一つずつずらすときストライドは1,二つずつずらすときストライドは2になります.

パディング

 フィルタの大きさと,ストライドの大きさによって,入力データを畳み込み演算すると,出力データは入力データより小さくなってしまいます.それだと問題になることがあります.なぜなら,より深いネットワークで何回も畳み込み演算をすると,どんどんデータが小さくなってしまい,いずれ畳み込み演算が出来なくなってしまうからです.
 このため,出力データが小さくならないように工夫する必要があります.それがパディングです.パディングは入力データの周辺に0の値を埋め込みます.こうすることで,n×nのデータを(n+1)×(n+1)のデータにしてやります.こうすると,畳み込み演算で入力データが小さくなっても,出力データは本来の入力データと同じサイズに維持されます.いくつ,周囲にデータを埋め込むかのサイズをパディングで指定します.

出力サイズの計算式

 ストライドを大きくすると出力サイズは小さくなり,パディングを大きくすると出力サイズは大きくなります.つまり,出力サイズは入力サイズだけでなく,ストライドやパディングによっても決まると言うことです.出力サイズを求める式は \frac{入力データサイズ + パディング*2 - カーネルサイズ}{ストライド} + 1となります.

プーリング層

 CNNは畳み込み層とプーリング層を組み合わせます.プーリング層とは,データサイズを小さくする演算です.フィルタのサイズを決め,そのフィルタに対応する入力データの値を見ます.Maxプーリングなら,そのフィルタの範囲の入力データの中で最も大きい値のみを出力データにするという処理をします.最も大きい値のみを出力データとするので,それ以外のデータは失われることから出力データのサイズは入力データより小さくなります.
 なぜ畳み込み層ではデータを小さくしないようにパディングという処理を追加したのに,プーリング層というサイズを小さくする層を使うのかというと,微小な位置変化に対してロバストにするためです.入力データの微少な位置変化,値が一つズレたようなデータに対して,プーリング層により,Maxの値のみを返すので,同じ結果を返します.画像の位置が少しずれただけで,内容が変わると言うことはないので,画像の位置がずれても同じ結果を返すような役割をプーリング層は果たしています.

ステップ58

VGG16

 VGGは2014年のコンペで準優勝したモデルで,CNNを使ったモデルです.何層もの畳み込みを行い,途中でプーリングを挟みます.特徴としては,畳み込み層のフィルタサイズは3×3であること,畳み込み層のチャンネル数はプーリングを行うたび2倍になること,全血豪壮ではDropoutを使うこと,活性化関数はReluを使うこと,があります.
 複雑そうなモデルですが,今まで作り上げてきたdezeroを組み合わせるだけで,簡単に構築できます.コード量は膨大ですが,似たような記述の繰り返しで,そこまで難しいことをやっているわけではないことが分かります.

class VGG16(Model):
    def __init__(self, pretrained=False):
        super().__init__()
        self.conv1_1 = L.Conv2d(64, kernel_size=3, stride=1, pad=1)
        self.conv1_2 = L.Conv2d(64, kernel_size=3, stride=1, pad=1)
        self.conv2_1 = L.Conv2d(128, kernel_size=3, stride=1, pad=1)
        self.conv2_2 = L.Conv2d(128, kernel_size=3, stride=1, pad=1)
        self.conv3_1 = L.Conv2d(256, kernel_size=3, stride=1, pad=1)
        self.conv3_2 = L.Conv2d(256, kernel_size=3, stride=1, pad=1)
        self.conv3_3 = L.Conv2d(256, kernel_size=3, stride=1, pad=1)
        self.conv4_1 = L.Conv2d(512, kernel_size=3, stride=1, pad=1)
        self.conv4_2 = L.Conv2d(512, kernel_size=3, stride=1, pad=1)
        self.conv4_3 = L.Conv2d(512, kernel_size=3, stride=1, pad=1)
        self.conv5_1 = L.Conv2d(512, kernel_size=3, stride=1, pad=1)
        self.conv5_2 = L.Conv2d(512, kernel_size=3, stride=1, pad=1)
        self.conv5_3 = L.Conv2d(512, kernel_size=3, stride=1, pad=1)
        self.fc6 = L.Linear(4096)
        self.fc7 = L.Linear(4096)
        self.fc8 = L.Linear(1000)
        
    def forward(self, x):
        x = F.relu(self.conv1_1(x))
        x = F.relu(self.conv1_2(x))
        x = F.pooling(x, 2, 2)
        x = F.relu(self.conv2_1(x))
        x = F.relu(self.conv2_2(x))
        x = F.pooling(x, 2, 2)
        x = F.relu(self.conv3_1(x))
        x = F.relu(self.conv3_2(x))
        x = F.relu(self.conv3_3(x))
        x = F.pooling(x, 2, 2)
        x = F.relu(self.conv4_1(x))
        x = F.relu(self.conv4_2(x))
        x = F.relu(self.conv4_3(x))
        x = F.pooling(x, 2, 2)
        x = F.relu(self.conv5_1(x))
        x = F.relu(self.conv5_2(x))
        x = F.relu(self.conv5_3(x))
        x = F.pooling(x, 2, 2)
        x = F.reshape(x, (x.shape[0], -1))
        x = F.dropout(F.relu(self.fc6(x)))
        x = F.dropout(F.relu(self.fc7(x)))
        x = self.fc8(x)
        return x

畳み込み層を終えるごとに,活性化関数Reluをはさみ,2,3回に1回プーリング層を挟みます.それを挟むとチャンネル数が2倍になっていることが分かります.最後の全層結合層の出力サイズは1000であることも分かります.
 dezeroでVGG16を実装することが出来ました!同じ要領で,ResNetなど他の画像認識モデルも実装出来ることが分かります.