8.2. ブロックを用いたネットワーク(VGG)¶
AlexNet は、深い CNN が良い結果を達成できるという実証的証拠を示したが、その後の研究者が新しいネットワークを設計する際の一般的なひな形は提示しませんでした。以下の節では、深層ネットワークの設計によく用いられるいくつかのヒューリスティックな概念を紹介する。
この分野の進歩は、チップ設計における VLSI(very large scale integration)の進歩を思わせる。そこでは、エンジニアはトランジスタの配置から論理要素へ、さらに論理ブロックへと移っていった (Mead, 1980)。同様に、ニューラルネットワークアーキテクチャの設計も徐々に抽象化が進み、研究者は個々のニューロンから層全体へ、そして現在では層の反復パターンであるブロックへと考えるようになった。さらに10年後の現在では、研究者が学習済みのモデル全体を別の、とはいえ関連するタスクに転用するところまで進んでいる。このような大規模な事前学習済みモデルは、通常 foundation models と呼ばれる (Bommasani et al., 2021)。
話をネットワーク設計に戻そう。ブロックを用いるという考え方は、オックスフォード大学の Visual Geometry Group(VGG)による、同名の VGG ネットワークで最初に現れた (Simonyan and Zisserman, 2014)。ループとサブルーチンを使えば、現代のどの深層学習フレームワークでも、こうした反復構造をコードで簡単に実装できる。
from d2l import torch as d2l
import torch
from torch import nn
from d2l import mxnet as d2l
from mxnet import np, npx, init
from mxnet.gluon import nn
npx.set_np()
from d2l import jax as d2l
from flax import linen as nn
import jax
import tensorflow as tf
from d2l import tensorflow as d2l
8.2.1. VGG ブロック¶
VGG に戻ると、VGG ブロックは、パディング 1 の \(3\times3\)
カーネルを持つ畳み込みの 系列 に続いて、ストライド 2 の
\(2 \times 2\) max-pooling
層(各ブロック後に高さと幅を半分にする)から構成される。以下のコードでは、1
つの VGG ブロックを実装する vgg_block という関数を定義する。
以下の関数は 2 つの引数を取り、畳み込み層の数 num_convs
と出力チャネル数 num_channels に対応する。
def vgg_block(num_convs, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.LazyConv2d(out_channels, kernel_size=3, padding=1))
layers.append(nn.ReLU())
layers.append(nn.MaxPool2d(kernel_size=2,stride=2))
return nn.Sequential(*layers)
def vgg_block(num_convs, num_channels):
blk = nn.Sequential()
for _ in range(num_convs):
blk.add(nn.Conv2D(num_channels, kernel_size=3,
padding=1, activation='relu'))
blk.add(nn.MaxPool2D(pool_size=2, strides=2))
return blk
def vgg_block(num_convs, out_channels):
layers = []
for _ in range(num_convs):
layers.append(nn.Conv(out_channels, kernel_size=(3, 3), padding=(1, 1)))
layers.append(nn.relu)
layers.append(lambda x: nn.max_pool(x, window_shape=(2, 2), strides=(2, 2)))
return nn.Sequential(layers)
def vgg_block(num_convs, num_channels):
blk = tf.keras.models.Sequential()
for _ in range(num_convs):
blk.add(
tf.keras.layers.Conv2D(num_channels, kernel_size=3,
padding='same', activation='relu'))
blk.add(tf.keras.layers.MaxPool2D(pool_size=2, strides=2))
return blk
8.2.2. VGG ネットワーク¶
図 8.2.1 AlexNet から VGG へ。重要な違いは、VGG が層のブロックから構成されるのに対し、AlexNet の層はすべて個別に設計されている点である。¶
vgg_block
関数でも定義済み)を順番に接続したものである。この畳み込みのグループ化は、具体的な演算の選択は大きく変化してきたものの、過去10年ほとんど変わらずに残ってきたパターンである。arch はタプルのリスト(ブロックごとに 1
つ)で構成され、各タプルは 2
つの値、すなわち畳み込み層の数と出力チャネル数を含む。これはちょうど
vgg_block 関数を呼び出すために必要な引数である。したがって、VGG
は単一の具体的なモデルというより、ネットワークの ファミリー
を定義している。特定のネットワークを構築するには、arch
を順にたどってブロックを組み立てればよいだけである。class VGG(d2l.Classifier):
def __init__(self, arch, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(num_classes))
self.net.initialize(init.Xavier())
if tab.selected('pytorch'):
conv_blks = []
for (num_convs, out_channels) in arch:
conv_blks.append(vgg_block(num_convs, out_channels))
self.net = nn.Sequential(
*conv_blks, nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
if tab.selected('tensorflow'):
self.net = tf.keras.models.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(
tf.keras.models.Sequential([
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(4096, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(4096, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(num_classes)]))
class VGG(d2l.Classifier):
def __init__(self, arch, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(num_classes))
self.net.initialize(init.Xavier())
if tab.selected('pytorch'):
conv_blks = []
for (num_convs, out_channels) in arch:
conv_blks.append(vgg_block(num_convs, out_channels))
self.net = nn.Sequential(
*conv_blks, nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
if tab.selected('tensorflow'):
self.net = tf.keras.models.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(
tf.keras.models.Sequential([
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(4096, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(4096, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(num_classes)]))
class VGG(d2l.Classifier):
arch: list
lr: float = 0.1
num_classes: int = 10
training: bool = True
def setup(self):
conv_blks = []
for (num_convs, out_channels) in self.arch:
conv_blks.append(vgg_block(num_convs, out_channels))
self.net = nn.Sequential([
*conv_blks,
lambda x: x.reshape((x.shape[0], -1)), # flatten
nn.Dense(4096), nn.relu,
nn.Dropout(0.5, deterministic=not self.training),
nn.Dense(4096), nn.relu,
nn.Dropout(0.5, deterministic=not self.training),
nn.Dense(self.num_classes)])
class VGG(d2l.Classifier):
def __init__(self, arch, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(4096, activation='relu'), nn.Dropout(0.5),
nn.Dense(num_classes))
self.net.initialize(init.Xavier())
if tab.selected('pytorch'):
conv_blks = []
for (num_convs, out_channels) in arch:
conv_blks.append(vgg_block(num_convs, out_channels))
self.net = nn.Sequential(
*conv_blks, nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
if tab.selected('tensorflow'):
self.net = tf.keras.models.Sequential()
for (num_convs, num_channels) in arch:
self.net.add(vgg_block(num_convs, num_channels))
self.net.add(
tf.keras.models.Sequential([
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(4096, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(4096, activation='relu'),
tf.keras.layers.Dropout(0.5),
tf.keras.layers.Dense(num_classes)]))
元の VGG ネットワークは 5 つの畳み込みブロックからなり、そのうち最初の 2 つはそれぞれ 1 層の畳み込み層を持ち、残りの 3 つはそれぞれ 2 層の畳み込み層を含みる。最初のブロックの出力チャネル数は 64 で、その後の各ブロックでは出力チャネル数が 2 倍ずつ増え、512 に達するまで続く。このネットワークは 8 層の畳み込み層と 3 層の全結合層を使うため、しばしば VGG-11 と呼ばれる。
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))).layer_summary(
(1, 1, 224, 224))
Sequential output shape: torch.Size([1, 64, 112, 112])
Sequential output shape: torch.Size([1, 128, 56, 56])
Sequential output shape: torch.Size([1, 256, 28, 28])
Sequential output shape: torch.Size([1, 512, 14, 14])
Sequential output shape: torch.Size([1, 512, 7, 7])
Flatten output shape: torch.Size([1, 25088])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 4096])
ReLU output shape: torch.Size([1, 4096])
Dropout output shape: torch.Size([1, 4096])
Linear output shape: torch.Size([1, 10])
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))).layer_summary(
(1, 1, 224, 224))
Sequential output shape: (1, 64, 112, 112)
Sequential output shape: (1, 128, 56, 56)
Sequential output shape: (1, 256, 28, 28)
Sequential output shape: (1, 512, 14, 14)
Sequential output shape: (1, 512, 7, 7)
Dense output shape: (1, 4096)
Dropout output shape: (1, 4096)
Dense output shape: (1, 4096)
Dropout output shape: (1, 4096)
Dense output shape: (1, 10)
[07:38:07] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512)),
training=False).layer_summary((1, 224, 224, 1))
Sequential output shape: (1, 112, 112, 64)
Sequential output shape: (1, 56, 56, 128)
Sequential output shape: (1, 28, 28, 256)
Sequential output shape: (1, 14, 14, 512)
Sequential output shape: (1, 7, 7, 512)
function output shape: (1, 25088)
Dense output shape: (1, 4096)
custom_jvp output shape: (1, 4096)
Dropout output shape: (1, 4096)
Dense output shape: (1, 4096)
custom_jvp output shape: (1, 4096)
Dropout output shape: (1, 4096)
Dense output shape: (1, 10)
VGG(arch=((1, 64), (1, 128), (2, 256), (2, 512), (2, 512))).layer_summary(
(1, 224, 224, 1))
Sequential output shape: (1, 112, 112, 64)
Sequential output shape: (1, 56, 56, 128)
Sequential output shape: (1, 28, 28, 256)
Sequential output shape: (1, 14, 14, 512)
Sequential output shape: (1, 7, 7, 512)
Sequential output shape: (1, 10)
8.2.3. 学習¶
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
if tab.selected('pytorch'):
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
if tab.selected('pytorch'):
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
if tab.selected('pytorch'):
model.apply_init([next(iter(data.get_dataloader(True)))[0]], d2l.init_cnn)
trainer.fit(model, data)
trainer = d2l.Trainer(max_epochs=10)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
with d2l.try_gpu():
model = VGG(arch=((1, 16), (1, 32), (2, 64), (2, 128), (2, 128)), lr=0.01)
trainer.fit(model, data)
8.2.4. 要約¶
VGG は、真に現代的な畳み込みニューラルネットワークの最初のものだと主張できるかもしれない。AlexNet は、深層学習を大規模で有効にする多くの構成要素を導入したが、複数の畳み込みからなるブロックや、深くて狭いネットワークを好む設計など、重要な性質を導入したのは VGG だと言えるだろう。また、実際に同じパラメータ化を持つモデルのファミリー全体である最初のネットワークでもあり、実践者に対して複雑さと速度の間で十分なトレードオフを与える。ここは、現代の深層学習フレームワークが真価を発揮する場でもある。もはやネットワークを指定するために XML の設定ファイルを生成する必要はなく、単純な Python コードでそのようなネットワークを組み立てればよいのである。
より最近では、ParNet (Goyal et al., 2021) が、多数の並列計算を用いることで、はるかに浅いアーキテクチャでも競争力のある性能を達成できることを示した。これは刺激的な進展であり、将来のアーキテクチャ設計に影響を与えることが期待される。とはいえ、この章の残りでは、過去10年の科学的進歩の道筋に従うことにする。
8.2.5. 演習¶
AlexNet と比べると、VGG は計算の面でかなり遅く、GPU メモリもより多く必要とする。
AlexNet と VGG に必要なパラメータ数を比較せよ。
畳み込み層と全結合層で使われる浮動小数点演算回数を比較せよ。
全結合層によって生じる計算コストをどのように削減できるか。
ネットワークの各層に対応する次元を表示すると、ネットワークは 11 層あるにもかかわらず、8 つのブロック(に加えていくつかの補助変換)に関する情報しか見えない。残りの 3 層はどこへ行ったのだろうか。
VGG 論文 (Simonyan and Zisserman, 2014) の表 1 を用いて、VGG-16 や VGG-19 などの他の一般的なモデルを構成せよ。
Fashion-MNIST の解像度を \(28 \times 28\) から \(224 \times 224\) へ 8 倍にアップサンプリングするのは非常に無駄である。ネットワークアーキテクチャと解像度変換を変更して、たとえば入力を 56 または 84 次元にしてみよ。ネットワークの精度を下げずにそれを実現できるか。ダウンサンプリングの前により多くの非線形性を加える方法については、VGG 論文 (Simonyan and Zisserman, 2014) を参照せよ。