8.1. 畳み込み深層ニューラルネットワーク(AlexNet)¶
CNNは、LeNet の導入 (LeCun et al., 1995) 以降、 コンピュータビジョンおよび機械学習のコミュニティではよく知られていたが、 すぐにこの分野を席巻したわけではない。 LeNet は初期の小規模データセットでは良好な結果を示したが、 より大規模で現実的なデータセットに対して CNN を訓練する性能と実現可能性は、まだ確立されていなかった。 実際、1990年代初頭から2012年の画期的な結果 (Krizhevsky et al., 2012) までの長い期間の多くにおいて、 ニューラルネットワークは、カーネル法 (Schölkopf and Smola, 2002)、アンサンブル法 (Freund and Schapire, 1996)、 構造化推定 (Taskar et al., 2004) などの他の機械学習手法にしばしば劣っていた。
コンピュータビジョンに関しては、この比較は必ずしも正確ではない。 つまり、畳み込みネットワークへの入力は 生の、あるいは(たとえば中心化のような)軽い前処理を施した画素値から成るが、実務家が生の画素を従来のモデルにそのまま入力することはない。 代わりに、典型的なコンピュータビジョンのパイプラインは、 SIFT (Lowe, 2004)、SURF (Bay et al., 2006)、visual words の bag (Sivic and Zisserman, 2003) などの特徴抽出パイプラインを手作業で設計するものであった。 特徴を 学習する のではなく、特徴は 作り込まれて いた。 進歩の大半は、一方では特徴抽出に関するより巧妙なアイデア、他方では幾何学 (Hartley and Zisserman, 2000) に対する深い洞察によってもたらされた。学習アルゴリズムは、しばしば後回しにされていた。
1990年代にはいくつかのニューラルネットワーク用アクセラレータが利用可能であったが、 深い多チャネル・多層の CNN を多数のパラメータで構成するには、まだ十分な性能がなかった。 たとえば、1999年の NVIDIA GeForce 256 は、 ゲーム以外の処理に対する意味のあるプログラミング基盤もないまま、 加算や乗算などの浮動小数点演算を1秒あたり最大4億8千万回(MFLOPS)しか処理できなかった。 今日のアクセラレータは、1デバイスあたり1000 TFLOPs を超える性能を発揮できる。 さらに、データセットもまだ比較的小規模であった。60,000枚の低解像度 \(28 \times 28\) ピクセル画像に対する OCR は、非常に難しい課題と見なされていた。 これらの障害に加えて、ニューラルネットワークを訓練するための重要な工夫、 すなわちパラメータ初期化のヒューリスティクス (Glorot and Bengio, 2010)、 確率的勾配降下法の巧妙な変種 (Kingma and Ba, 2014)、 非飽和活性化関数 (Nair and Hinton, 2010)、 効果的な正則化手法 (Srivastava et al., 2014) なども、まだ欠けていた。
したがって、エンドツーエンド(画素から分類まで)のシステムを訓練するのではなく、 古典的なパイプラインは次のようなものであった。
興味深いデータセットを入手する。初期の頃は、これらのデータセットには高価なセンサーが必要でした。たとえば、1994年の Apple QuickTake 100 は、わずか $1000 で、最大8枚の画像を保存できる、驚くべき 0.3 メガピクセル(VGA)解像度を備えていました。
光学、幾何学、その他の解析的手法、そして時には幸運な大学院生による偶然の発見に基づく手作りの特徴を用いてデータセットを前処理する。
SIFT(scale-invariant feature transform) (Lowe, 2004)、SURF(speeded up robust features) (Bay et al., 2006)、あるいは他の多くの手調整されたパイプラインのような標準的な特徴抽出器にデータを通す。OpenCV は今日でも SIFT 抽出器を提供している!
得られた表現を、好みの分類器、たいていは線形モデルかカーネル法に投入して分類器を訓練する。
機械学習研究者に話を聞けば、 彼らは機械学習が重要で美しいものだと答えるだろう。 さまざまな分類器の性質は洗練された理論によって証明され (Boucheron et al., 2005)、凸最適化 (Boyd and Vandenberghe, 2004) はそれらを得るための主流となっていた。 機械学習分野は活気に満ち、厳密で、きわめて有用であった。しかし、 コンピュータビジョン研究者に話を聞けば、 まったく異なる話が返ってきただろう。 彼らが語る画像認識の不都合な真実は、 新しい学習アルゴリズムではなく、特徴、幾何学 (Hartley and Zisserman, 2000, Hartley and Kahl, 2009)、そして工学が進歩を牽引していたということである。 コンピュータビジョン研究者は、 少し大きい、あるいは少しきれいなデータセット、 あるいは少し改善された特徴抽出パイプラインのほうが、 どんな学習アルゴリズムよりも最終的な精度にずっと大きく影響すると、もっともな理由で考えていた。
from d2l import torch as d2l
import torch
from torch import nn
from d2l import mxnet as d2l
from mxnet import np, init, npx
from mxnet.gluon import nn
npx.set_np()
from d2l import jax as d2l
from flax import linen as nn
import jax
from jax import numpy as jnp
from d2l import tensorflow as d2l
import tensorflow as tf
8.1.1. 表現学習¶
状況を別の言い方で表すなら、 パイプラインで最も重要な部分は表現であった。 そして2012年までは、その表現は主として機械的に計算されていた。 実際、新しい特徴関数の設計、結果の改善、手法の記述は、 論文の中でいずれも大きな位置を占めていた。 SIFT (Lowe, 2004)、 SURF (Bay et al., 2006)、 HOG(histograms of oriented gradient) (Dalal and Triggs, 2005)、 visual words の bag (Sivic and Zisserman, 2003)、 および同様の特徴抽出器が主流であった。
Yann LeCun、Geoff Hinton、Yoshua Bengio、 Andrew Ng、Shun-ichi Amari、Juergen Schmidhuber などを含む別の研究者グループは、 異なる構想を持っていた。 彼らは、特徴そのものが学習されるべきだと考えていた。 さらに、特徴が十分に複雑であるためには、 複数の共同学習された層から階層的に構成され、 各層が学習可能なパラメータを持つべきだと考えていた。 画像の場合、最下層は動物の視覚系が入力を処理する仕組みに倣って、 エッジ、色、テクスチャを検出するようになるかもしれない。 特に、sparse coding (Olshausen and Field, 1996) によって得られるような視覚特徴の自動設計は、 現代の CNN が登場するまで未解決の課題であった。 画像データから特徴を自動生成するというアイデアが大きな支持を得たのは、 Dean et al. (2012), Le (2013) になってからであった。
最初の現代的な CNN (Krizhevsky et al., 2012) は、 その発明者の一人である Alex Krizhevsky にちなんで AlexNet と名付けられ、 LeNet に対する主として進化的な改良でした。 これは2012年の ImageNet コンペティションで優れた性能を達成した。
図 8.1.1 AlexNet の第1層で学習された画像フィルタ。再現図は Krizhevsky et al. (2012) より。¶
興味深いことに、ネットワークの最下層では、 モデルは従来のフィルタに似た特徴抽出器を学習した。 図 8.1.1 は低レベルの画像記述子を示している。 ネットワークのより高い層は、これらの表現を基にして、 目、鼻、草の葉などのより大きな構造を表現するかもしれない。 さらに高い層では、人、飛行機、犬、フリスビーのような 物体全体を表現するかもしれない。 最終的には、最後の隠れ状態が画像の内容を要約したコンパクトな表現を学習し、 異なるカテゴリに属するデータを容易に分離できるようになる。
AlexNet(2012)とその前身である LeNet(1995)は、多くのアーキテクチャ要素を共有している。では、なぜこれほど長い時間がかかったのだろうか? 重要な違いは、過去20年間で利用可能なデータ量と計算能力が大幅に増加したことであった。そのため AlexNet ははるかに大規模であり、1995年に利用可能だった CPU と比べて、より多くのデータで、より高速な GPU 上で訓練された。
8.1.1.1. 欠けていた要素: データ¶
多数の層を持つ深層モデルは、 従来手法(たとえば線形法やカーネル法)に基づく凸最適化を大きく上回る段階に入るために、 大量のデータを必要とする。 しかし、コンピュータのストレージ容量の制約、 (画像)センサーの相対的な高コスト、 そして1990年代における研究予算の比較的厳しさのため、 多くの研究は小さなデータセットに依存していた。 多くの論文は UCI のデータセット集に依拠しており、 その多くは、低解像度で撮影され、しばしば人工的にきれいな背景を持つ、 数百枚、あるいは(せいぜい)数千枚の画像しか含んでいなかった。
2009年、ImageNet データセットが公開され (Deng et al., 2009)、 1000の異なるカテゴリの物体からそれぞれ1000例、合計100万例からモデルを学習するという課題を研究者に突きつけた。 カテゴリ自体は WordNet (Miller, 1995) における最も一般的な名詞ノードに基づいていた。 ImageNet チームは Google Image Search を使って各カテゴリの大規模な候補集合を事前にふるい分け、 Amazon Mechanical Turk のクラウドソーシング・パイプラインを用いて、 各画像が対応するカテゴリに属するかどうかを確認した。 この規模は前例がなく、他のデータセットを1桁以上上回っていた (たとえば CIFAR-100 は 60,000 枚の画像である)。 もう一つの特徴は、画像が 80 million-sized の TinyImages データセット (Torralba et al., 2008) のような \(32 \times 32\) ピクセルのサムネイルとは異なり、 比較的高解像度の \(224 \times 224\) ピクセルであったことである。 これにより、より高次の特徴を形成できた。 これに対応するコンペティションである ImageNet Large Scale Visual Recognition Challenge (Russakovsky et al., 2015) は、コンピュータビジョンと機械学習研究を前進させ、 研究者に、学術界がそれまで考えていたよりも大きな規模で、どのモデルが最良の性能を示すのかを突き止めるよう挑んだ。今日では、1000万枚の画像を扱うことも前例のないことではない。たとえば、公開されている YFCC-100M データセットには、なんと1億枚の画像が含まれている。同時に、医療画像のように特定の視覚タスクでは、十分な高品質のデータを得ることが依然として非常に困難な場合がある。 LAION-5B (Schuhmann et al., 2022) のような最大級の視覚データセットには、追加のメタデータを伴う数十億枚の画像が含まれている。
8.1.1.2. 欠けていた要素: ハードウェア¶
深層学習モデルは計算資源を大量に消費する。 訓練には数百エポックかかることがあり、各反復では 計算コストの高い線形代数演算を多数の層に通してデータを流す必要がある。 これが、1990年代から2000年代初頭にかけて、 より効率的に最適化された凸目的関数に基づく単純なアルゴリズムが好まれた主な理由の一つである。
グラフィックス処理装置(GPU)は、深層学習を実用的にするうえで ゲームチェンジャーであることが証明された。 これらのチップはもともと、コンピュータゲーム向けのグラフィックス処理を高速化するために開発されていた。 特に、多くのコンピュータグラフィックス処理に必要な、高スループットの \(4 \times 4\) 行列—ベクトル積に最適化されていた。 幸いなことに、その数学は 畳み込み層の計算に必要なものと驚くほど似ている。 その頃、NVIDIA と ATI は GPU を一般計算向けに最適化し始めており (Fernando, 2004)、 それらを general-purpose GPUs(GPGPUs)として売り出すまでになっていた。
直感を得るために、現代のマイクロプロセッサ(CPU)のコアを考えてみよう。 各コアはかなり高性能で、高いクロック周波数で動作し、 大きなキャッシュ(L3 で数メガバイトに達することもある)を備えている。 各コアは、分岐予測器、深いパイプライン、専用実行ユニット、投機実行、 そしてその他多くの付加機能を備え、 幅広い命令を実行するのに適している。 これにより、複雑な制御フローを持つ多種多様なプログラムを実行できる。 しかし、この見かけ上の強みは、同時に弱点でもある。 汎用コアは構築コストが非常に高いのである。制御フローの多い汎用コードには優れている。 そのためには、実際に計算が行われる ALU(arithmetic logical unit)だけでなく、 前述の付加機能すべて、さらに メモリインターフェース、コア間のキャッシュ制御、高速相互接続などにも 多くのチップ面積が必要になる。CPU は、 専用ハードウェアと比べると、単一のタスクに関しては相対的に不得手である。 現代のノートパソコンは 4–8 コアを備え、 ハイエンドサーバーでさえソケットあたり 64 コアを超えることはまれである。 単純に、費用対効果がよくないからである。
これに対して、GPU は数千の小さな処理要素から構成されることがある(NIVIDA の最新の Ampere チップは最大 6912 CUDA コアを備えます)。 それらはしばしば、より大きなグループ(NVIDIA では warp と呼ぶ)にまとめられる。 詳細は NVIDIA、AMD、ARM、その他のチップベンダーで多少異なる。各コアは比較的弱く、 約 1GHz のクロック周波数で動作するが、 そのようなコアの総数が、GPU を CPU より桁違いに高速にしている。 たとえば、NVIDIA の最近の Ampere A100 GPU は、特殊な16ビット精度(BFLOAT16)の行列—行列乗算で1チップあたり300 TFLOPs超を提供し、 より汎用的な浮動小数点演算(FP32)では最大20 TFLOPsを実現する。 一方、CPU の浮動小数点性能は 1 TFLOPs を超えることはめったにない。たとえば、Amazon の Graviton 3 は16ビット精度演算でピーク 2 TFLOPs に達し、これは Apple の M1 プロセッサの GPU 性能に近い値である。
GPU が FLOPs の観点で CPU よりはるかに高速である理由は多くある。 第一に、消費電力はクロック周波数に対して 二乗的 に増加する傾向がある。 したがって、4倍高速に動作する CPU コア1個の電力予算で(典型的な値です)、 速度を \(\frac{1}{4}\) に落とした GPU コアを16個使えば、 \(16 \times \frac{1}{4} = 4\) 倍の性能が得られる。 第二に、GPU コアははるかに単純であり (実際、長い間、汎用コードを実行することすら できなかった)、 そのためエネルギー効率が高いのである。たとえば、(i) 投機的実行を通常はサポートせず、(ii) 各処理要素を個別にプログラムすることは通常できず、(iii) コアごとのキャッシュははるかに小さい傾向がある。 最後に、深層学習の多くの演算は高いメモリ帯域幅を必要とする。 この点でも GPU は優れており、多くの CPU より少なくとも10倍広いバスを備えている。
2012年に戻ろう。大きな突破口は、 Alex Krizhevsky と Ilya Sutskever が GPU 上で動作する深い CNN を実装したときに訪れた。 彼らは、CNN における計算上のボトルネックである 畳み込みと行列乗算が、いずれもハードウェアで並列化できる演算であることに気づいた。 3GB のメモリを持つ 2枚の NVIDIA GTX 580 を用い、 それぞれが 1.5 TFLOPs を実行可能であったため(10年後でも多くの CPU にとっては依然として難題であった)、 彼らは高速な畳み込みを実装した。 cuda-convnet のコードは十分に優れており、 その後数年間にわたって業界標準となり、 深層学習ブームの最初の数年間を支えた。
8.1.2. AlexNet¶
8層 CNN を採用した AlexNet は、 ImageNet Large Scale Visual Recognition Challenge 2012 で 大差をつけて優勝した (Russakovsky et al., 2013)。 このネットワークは、学習によって得られる特徴が手作業で設計された特徴を超えうることを、 コンピュータビジョンにおいて初めて示し、従来のパラダイムを打ち破った。
AlexNet と LeNet のアーキテクチャは、 図 8.1.2 が示すように、驚くほど似ている。 ここでは、2012年当時にモデルを2つの小さな GPU に収めるために必要だった いくつかの設計上の癖を取り除いた、やや簡略化した AlexNet を示す。
図 8.1.2 LeNet(左)から AlexNet(右)へ。¶
AlexNet と LeNet の間には、重要な違いもある。 第一に、AlexNet は比較的小さな LeNet-5 よりもはるかに深い。 AlexNet は8層から成り、5つの畳み込み層、 2つの全結合隠れ層、1つの全結合出力層を含む。 第二に、AlexNet は活性化関数として sigmoid ではなく ReLU を用いた。以下で詳細を見ていこう。
8.1.2.1. アーキテクチャ¶
AlexNet の第1層では、畳み込みウィンドウの形状は \(11\times11\) である。 ImageNet の画像は MNIST の画像よりも縦横ともに8倍大きいため、 ImageNet データ中の物体は、より多くの画素とより豊かな視覚的詳細を占める傾向がある。 したがって、物体を捉えるにはより大きな畳み込みウィンドウが必要である。 第2層の畳み込みウィンドウの形状は \(5\times5\) に縮小され、その後に \(3\times3\) が続く。 さらに、第1、第2、第5の畳み込み層の後には、 ウィンドウ形状 \(3\times3\)、ストライド 2 の最大プーリング層が追加される。 また、AlexNet の畳み込みチャネル数は LeNet の10倍である。
最後の畳み込み層の後には、4096出力を持つ非常に大きな全結合層が2つある。 これらの層だけでほぼ1GBのモデルパラメータを必要とする。 初期の GPU はメモリが限られていたため、 元の AlexNet ではデュアルデータストリーム設計が採用され、 2つの GPU がそれぞれモデルの半分だけを保存・計算する役割を担っていた。 幸いなことに、現在は GPU メモリが比較的豊富なので、 最近ではモデルを GPU 間で分割する必要はほとんどない (この点で、ここでの AlexNet モデルは元論文と異なる)。
8.1.2.2. 活性化関数¶
さらに、AlexNet は sigmoid 活性化関数を、より単純な ReLU 活性化関数に変更した。第一に、ReLU 活性化関数の計算はより単純である。たとえば、sigmoid 活性化関数にある指数演算がない。 第二に、ReLU 活性化関数は、異なるパラメータ初期化法を用いる場合にモデル訓練を容易にする。これは、sigmoid 活性化関数の出力が 0 または 1 に非常に近いとき、その領域の勾配はほぼ 0 となり、逆伝播がモデルパラメータの一部を更新し続けられなくなるためである。対照的に、ReLU 活性化関数の正の区間における勾配は常に 1 である(5.1.2 章)。したがって、モデルパラメータが適切に初期化されていない場合、sigmoid 関数では正の区間で勾配がほぼ 0 になり、モデルを効果的に訓練できない可能性がある。
8.1.2.3. 容量制御と前処理¶
AlexNet は dropout(5.6 章)によって 全結合層のモデル複雑度を制御する一方、 LeNet は重み減衰のみを使用する。 さらにデータを増強するために、AlexNet の訓練ループでは、 反転、切り抜き、色の変化など、 多くの画像拡張が追加された。 これによりモデルはより頑健になり、実質的なサンプルサイズの増加によって過学習が抑えられる。 このような前処理手順の詳細なレビューについては Buslaev et al. (2020) を参照せよ。
class AlexNet(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Sequential()
self.net.add(
nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
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'):
self.net = nn.Sequential(
nn.LazyConv2d(96, kernel_size=11, stride=4, padding=1),
nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2),
nn.LazyConv2d(256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
nn.LazyConv2d(256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2), nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(p=0.5),
nn.LazyLinear(4096), nn.ReLU(),nn.Dropout(p=0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
if tab.selected('tensorflow'):
self.net = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=96, kernel_size=11, strides=4,
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Conv2D(filters=256, kernel_size=5, padding='same',
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.Conv2D(filters=256, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
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 AlexNet(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Sequential()
self.net.add(
nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
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'):
self.net = nn.Sequential(
nn.LazyConv2d(96, kernel_size=11, stride=4, padding=1),
nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2),
nn.LazyConv2d(256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
nn.LazyConv2d(256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2), nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(p=0.5),
nn.LazyLinear(4096), nn.ReLU(),nn.Dropout(p=0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
if tab.selected('tensorflow'):
self.net = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=96, kernel_size=11, strides=4,
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Conv2D(filters=256, kernel_size=5, padding='same',
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.Conv2D(filters=256, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
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 AlexNet(d2l.Classifier):
lr: float = 0.1
num_classes: int = 10
training: bool = True
def setup(self):
self.net = nn.Sequential([
nn.Conv(features=96, kernel_size=(11, 11), strides=4, padding=1),
nn.relu,
lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2)),
nn.Conv(features=256, kernel_size=(5, 5)),
nn.relu,
lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2)),
nn.Conv(features=384, kernel_size=(3, 3)), nn.relu,
nn.Conv(features=384, kernel_size=(3, 3)), nn.relu,
nn.Conv(features=256, kernel_size=(3, 3)), nn.relu,
lambda x: nn.max_pool(x, window_shape=(3, 3), strides=(2, 2)),
lambda x: x.reshape((x.shape[0], -1)), # flatten
nn.Dense(features=4096),
nn.relu,
nn.Dropout(0.5, deterministic=not self.training),
nn.Dense(features=4096),
nn.relu,
nn.Dropout(0.5, deterministic=not self.training),
nn.Dense(features=self.num_classes)
])
class AlexNet(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
if tab.selected('mxnet'):
self.net = nn.Sequential()
self.net.add(
nn.Conv2D(96, kernel_size=11, strides=4, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Conv2D(256, kernel_size=5, padding=2, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(384, kernel_size=3, padding=1, activation='relu'),
nn.Conv2D(256, kernel_size=3, padding=1, activation='relu'),
nn.MaxPool2D(pool_size=3, strides=2),
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'):
self.net = nn.Sequential(
nn.LazyConv2d(96, kernel_size=11, stride=4, padding=1),
nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2),
nn.LazyConv2d(256, kernel_size=5, padding=2), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2),
nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
nn.LazyConv2d(384, kernel_size=3, padding=1), nn.ReLU(),
nn.LazyConv2d(256, kernel_size=3, padding=1), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2), nn.Flatten(),
nn.LazyLinear(4096), nn.ReLU(), nn.Dropout(p=0.5),
nn.LazyLinear(4096), nn.ReLU(),nn.Dropout(p=0.5),
nn.LazyLinear(num_classes))
self.net.apply(d2l.init_cnn)
if tab.selected('tensorflow'):
self.net = tf.keras.models.Sequential([
tf.keras.layers.Conv2D(filters=96, kernel_size=11, strides=4,
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Conv2D(filters=256, kernel_size=5, padding='same',
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.Conv2D(filters=384, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.Conv2D(filters=256, kernel_size=3, padding='same',
activation='relu'),
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
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)])
高さと幅がともに224の単一チャネルのデータ例を構成し各層の出力形状を観察するため、 図 8.1.2 の AlexNet アーキテクチャに対応させる。
AlexNet().layer_summary((1, 1, 224, 224))
Conv2d output shape: torch.Size([1, 96, 54, 54])
ReLU output shape: torch.Size([1, 96, 54, 54])
MaxPool2d output shape: torch.Size([1, 96, 26, 26])
Conv2d output shape: torch.Size([1, 256, 26, 26])
ReLU output shape: torch.Size([1, 256, 26, 26])
MaxPool2d output shape: torch.Size([1, 256, 12, 12])
Conv2d output shape: torch.Size([1, 384, 12, 12])
ReLU output shape: torch.Size([1, 384, 12, 12])
Conv2d output shape: torch.Size([1, 384, 12, 12])
ReLU output shape: torch.Size([1, 384, 12, 12])
Conv2d output shape: torch.Size([1, 256, 12, 12])
ReLU output shape: torch.Size([1, 256, 12, 12])
MaxPool2d output shape: torch.Size([1, 256, 5, 5])
Flatten output shape: torch.Size([1, 6400])
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])
AlexNet().layer_summary((1, 1, 224, 224))
Conv2D output shape: (1, 96, 54, 54)
MaxPool2D output shape: (1, 96, 26, 26)
Conv2D output shape: (1, 256, 26, 26)
MaxPool2D output shape: (1, 256, 12, 12)
Conv2D output shape: (1, 384, 12, 12)
Conv2D output shape: (1, 384, 12, 12)
Conv2D output shape: (1, 256, 12, 12)
MaxPool2D output shape: (1, 256, 5, 5)
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:35:39] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
AlexNet(training=False).layer_summary((1, 224, 224, 1))
Conv output shape: (1, 54, 54, 96)
custom_jvp output shape: (1, 54, 54, 96)
function output shape: (1, 26, 26, 96)
Conv output shape: (1, 26, 26, 256)
custom_jvp output shape: (1, 26, 26, 256)
function output shape: (1, 12, 12, 256)
Conv output shape: (1, 12, 12, 384)
custom_jvp output shape: (1, 12, 12, 384)
Conv output shape: (1, 12, 12, 384)
custom_jvp output shape: (1, 12, 12, 384)
Conv output shape: (1, 12, 12, 256)
custom_jvp output shape: (1, 12, 12, 256)
function output shape: (1, 5, 5, 256)
function output shape: (1, 6400)
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)
AlexNet().layer_summary((1, 224, 224, 1))
Conv2D output shape: (1, 54, 54, 96)
MaxPooling2D output shape: (1, 26, 26, 96)
Conv2D output shape: (1, 26, 26, 256)
MaxPooling2D output shape: (1, 12, 12, 256)
Conv2D output shape: (1, 12, 12, 384)
Conv2D output shape: (1, 12, 12, 384)
Conv2D output shape: (1, 12, 12, 256)
MaxPooling2D output shape: (1, 5, 5, 256)
Flatten output shape: (1, 6400)
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)
8.1.3. 訓練¶
AlexNet は Krizhevsky et al. (2012) で ImageNet
上で訓練されたが、 ここでは Fashion-MNIST を使う。 ImageNet
モデルを収束まで訓練するには、現代の GPU
でも数時間から数日かかることがあるからである。 AlexNet を Fashion-MNIST
に直接適用する際の問題の一つは、 その
画像の解像度が低い(\(28 \times 28\) ピクセル) ImageNet
の画像よりも。 ことである。
これを動作させるために、\(224 \times 224\)
にアップサンプリングする。
これは、情報を増やさずに計算複雑度を単に増やすだけなので、一般には賢明な方法ではない。それでも、AlexNet
のアーキテクチャに忠実であるためにここではそうする。 このリサイズは
d2l.FashionMNIST コンストラクタの resize 引数で行う。
これで、AlexNet の訓練を開始できる。 7.6 章 の LeNet と比べると、 ここでの主な変更点は、より小さな学習率の使用と、 より深く広いネットワーク、より高い画像解像度、そしてより高コストな畳み込みによる、はるかに遅い訓練である。
model = AlexNet(lr=0.01)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
trainer.fit(model, data)
model = AlexNet(lr=0.01)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
trainer.fit(model, data)
model = AlexNet(lr=0.01)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
trainer.fit(model, data)
trainer = d2l.Trainer(max_epochs=10)
data = d2l.FashionMNIST(batch_size=128, resize=(224, 224))
with d2l.try_gpu():
model = AlexNet(lr=0.01)
trainer.fit(model, data)
8.1.4. 議論¶
AlexNet の構造は LeNet と驚くほど似ているが、精度向上(dropout)と訓練のしやすさ(ReLU)の両面でいくつかの重要な改良が加えられている。同様に驚くべきなのは、深層学習ツールの進歩の大きさである。2012年には数か月かかった作業が、今ではどの現代的フレームワークでも十数行のコードで実現できる。
アーキテクチャを見直すと、AlexNet には効率面で弱点があることがわかる。最後の2つの隠れ層は、それぞれサイズ \(6400 \times 4096\) と \(4096 \times 4096\) の行列を必要とする。これは 164 MB のメモリと 81 MFLOPs の計算に相当し、どちらも、特に携帯電話のような小型デバイスでは無視できない負担である。これが、AlexNet が後続の節で扱う、はるかに効果的なアーキテクチャに取って代わられた理由の一つである。それでも、これは現在使われている浅いネットワークから深いネットワークへの重要な一歩である。実験では、パラメータ数が訓練データ量をはるかに上回っているにもかかわらず(最後の2層だけで 4000万以上のパラメータがあり、6万枚の画像からなるデータセットで訓練している)、過学習はほとんど見られない。訓練損失と検証損失は、訓練を通して事実上同一である。これは、現代の深層ネットワーク設計に本質的な dropout などの正則化が改善されたためである。
AlexNet の実装は LeNet より数行多いだけに見えるが、この概念的変化を受け入れ、その優れた実験結果を活用するまでに、学術界は長い年月を要した。これも、効率的な計算ツールが欠けていたためである。当時は DistBelief (Dean et al., 2012) も Caffe (Jia et al., 2014) も存在せず、Theano (Bergstra et al., 2010) もまだ多くの特徴を欠いていた。状況を劇的に変えたのは、TensorFlow (Abadi et al., 2016) の登場であった。
8.1.5. 演習¶
上の議論を踏まえて、AlexNet の計算特性を分析しなさい。
畳み込み層と全結合層それぞれのメモリ使用量を計算しなさい。どちらが支配的か。
畳み込み層と全結合層の計算コストを計算しなさい。
メモリ(読み書き帯域幅、レイテンシ、サイズ)は計算にどのように影響するか。訓練と推論でその影響に違いはあるか。
あなたはチップ設計者であり、計算とメモリ帯域幅のトレードオフを考えなければならない。たとえば、より高速なチップはより多くの電力を消費し、場合によってはより大きなチップ面積を必要とする。より多いメモリ帯域幅は、より多くのピンと制御ロジックを必要とし、やはり面積を増やす。どのように最適化するか。
なぜエンジニアはもはや AlexNet の性能ベンチマークを報告しないのか。
AlexNet の訓練でエポック数を増やしてみなさい。LeNet と比べて、結果はどう異なるか。なぜか。
AlexNet は Fashion-MNIST データセットには複雑すぎるかもしれない。特に初期画像の解像度が低いためである。
訓練を速くしつつ、精度が大きく低下しないようにモデルを単純化してみなさい。
\(28 \times 28\) 画像に直接適用できる、より良いモデルを設計しなさい。
バッチサイズを変更し、スループット(images/s)、精度、GPU メモリの変化を観察しなさい。
LeNet-5 に dropout と ReLU を適用しなさい。改善するか。画像に内在する不変性を利用するために前処理を行うことで、さらに改善できるか。
AlexNet を過学習させることはできるか。訓練を破綻させるには、どの特徴を取り除くか、あるいは変更する必要があるか。