%load_ext d2lbook.tab
tab.interact_select(['mxnet', 'pytorch', 'tensorflow', 'jax'])

5.2. 多層パーセプトロンの実装

多層パーセプトロン(MLP)の実装は、単純な線形モデルに比べてそれほど複雑ではない。概念上の重要な違いは、複数の層を連結して用いる点にある。

from d2l import torch as d2l
import torch
from torch import nn
from d2l import mxnet as d2l
from mxnet import np, 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

5.2.1. ゼロからの実装

まず、この種のネットワークをゼロから実装する。

5.2.1.1. モデルパラメータの初期化

Fashion-MNIST には 10 個のクラスがあり、各画像は \(28 \times 28 = 784\) 個のグレースケール画素値からなる格子で表される。これまでと同様、ここでは画素間の空間構造をいったん無視し、784 個の入力特徴量と 10 クラスをもつ分類データセットとして扱う。まずは、1 つの隠れ層と 256 個の隠れユニットをもつ MLP を実装する。層の数と幅はいずれも調整可能であり、これらはハイパーパラメータである。一般に、層の幅は 2 の大きな冪で割り切れる値に選ぶことが多い。これは、ハードウェアにおけるメモリ割り当てやアドレス指定の仕組みの都合上、計算効率がよいからである。

ここでも、パラメータは複数のテンソルで表す。各層ごとに、1 つの重み行列と 1 つのバイアスベクトルを保持する必要がある。いつものように、これらのパラメータに対する損失の勾配を格納するためのメモリも確保する。

以下のコードでは nn.Parameter を用いて、クラス属性を autograd によって追跡されるパラメータとして自動的に登録する(2.5 章)。

class MLPScratch(d2l.Classifier):
    def __init__(self, num_inputs, num_outputs, num_hiddens, lr, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens) * sigma)
        self.b1 = nn.Parameter(torch.zeros(num_hiddens))
        self.W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs) * sigma)
        self.b2 = nn.Parameter(torch.zeros(num_outputs))
class MLPScratch(d2l.Classifier):
    def __init__(self, num_inputs, num_outputs, num_hiddens, lr, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.W1 = np.random.randn(num_inputs, num_hiddens) * sigma
        self.b1 = np.zeros(num_hiddens)
        self.W2 = np.random.randn(num_hiddens, num_outputs) * sigma
        self.b2 = np.zeros(num_outputs)
        for param in self.get_scratch_params():
            param.attach_grad()
class MLPScratch(d2l.Classifier):
    num_inputs: int
    num_outputs: int
    num_hiddens: int
    lr: float
    sigma: float = 0.01

    def setup(self):
        self.W1 = self.param('W1', nn.initializers.normal(self.sigma),
                             (self.num_inputs, self.num_hiddens))
        self.b1 = self.param('b1', nn.initializers.zeros, self.num_hiddens)
        self.W2 = self.param('W2', nn.initializers.normal(self.sigma),
                             (self.num_hiddens, self.num_outputs))
        self.b2 = self.param('b2', nn.initializers.zeros, self.num_outputs)
class MLPScratch(d2l.Classifier):
    def __init__(self, num_inputs, num_outputs, num_hiddens, lr, sigma=0.01):
        super().__init__()
        self.save_hyperparameters()
        self.W1 = tf.Variable(
            tf.random.normal((num_inputs, num_hiddens)) * sigma)
        self.b1 = tf.Variable(tf.zeros(num_hiddens))
        self.W2 = tf.Variable(
            tf.random.normal((num_hiddens, num_outputs)) * sigma)
        self.b2 = tf.Variable(tf.zeros(num_outputs))

5.2.1.2. モデル

全体の動作を確実に理解するため、組み込みの relu 関数を直接呼び出すのではなく、ReLU 活性化関数を自分で実装する。

def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)
def relu(X):
    return np.maximum(X, 0)
def relu(X):
    return jnp.maximum(X, 0)
def relu(X):
    return tf.math.maximum(X, 0)

空間構造を無視するので、各 2 次元画像を reshape して長さ num_inputs の平坦なベクトルに変換する。最後に、わずか数行のコードで モデルを実装する。フレームワーク組み込みの autograd を使うため、これで十分である。

@d2l.add_to_class(MLPScratch)
def forward(self, X):
    X = d2l.reshape(X, (-1, self.num_inputs))
    H = relu(d2l.matmul(X, self.W1) + self.b1)
    return d2l.matmul(H, self.W2) + self.b2

5.2.1.3. 学習

幸い、MLP の学習ループはソフトマックス回帰の場合とまったく同じである。 モデル、データ、トレーナーを定義し、最後にモデルとデータに対して fit メソッドを呼び出す。

model = MLPScratch(num_inputs=784, num_outputs=10, num_hiddens=256, lr=0.1)
data = d2l.FashionMNIST(batch_size=256)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)
../_images/output_mlp-implementation_161c92_49_0.svg

5.2.2. 簡潔な実装

予想どおり、高水準 API を使えば、MLP はさらに簡潔に実装できる。

5.2.2.1. モデル

ソフトマックス回帰の簡潔な実装(4.5 章)と比べると、違いは、以前は 1 つ だけ追加していた全結合層を、ここでは 2 つ 追加する点だけである。1 つ目が隠れ層であり、2 つ目が出力層である。

class MLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential(nn.Flatten(), nn.LazyLinear(num_hiddens),
                                 nn.ReLU(), nn.LazyLinear(num_outputs))
class MLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = nn.Sequential()
        self.net.add(nn.Dense(num_hiddens, activation='relu'),
                     nn.Dense(num_outputs))
        self.net.initialize()
class MLP(d2l.Classifier):
    num_outputs: int
    num_hiddens: int
    lr: float

    @nn.compact
    def __call__(self, X):
        X = X.reshape((X.shape[0], -1))  # Flatten
        X = nn.Dense(self.num_hiddens)(X)
        X = nn.relu(X)
        X = nn.Dense(self.num_outputs)(X)
        return X
class MLP(d2l.Classifier):
    def __init__(self, num_outputs, num_hiddens, lr):
        super().__init__()
        self.save_hyperparameters()
        self.net = tf.keras.models.Sequential([
            tf.keras.layers.Flatten(),
            tf.keras.layers.Dense(num_hiddens, activation='relu'),
            tf.keras.layers.Dense(num_outputs)])

以前は、モデルのパラメータを使って入力を変換する forward メソッドを定義していた。これらの操作は本質的にパイプラインである。すなわち、入力を受け取り、変換(たとえば、重みとの行列積の後にバイアスを加える)を適用し、その変換の出力を次の変換への入力として繰り返し用いる。しかし、ここでは forward メソッドが定義されていないことに気づいたかもしれない。実際には、MLPModule クラス(3.2.2 章)から forward メソッドを継承し、単に self.net(X)X は入力)を呼び出す。そして self.netSequential クラスによって変換の列として定義されている。Sequential クラスは順伝播の過程を抽象化し、変換そのものに集中できるようにする。Sequential クラスの動作については、6.1.2 章 でさらに説明する。

5.2.2.2. 学習

学習ループ は、ソフトマックス回帰を実装したときとまったく同じである。このモジュール性により、モデルアーキテクチャに関する事項と、それ以外の考慮事項を分離できる。

model = MLP(num_outputs=10, num_hiddens=256, lr=0.1)
trainer.fit(model, data)
../_images/output_mlp-implementation_161c92_66_0.svg

5.2.3. まとめ

深層ネットワークの設計に慣れてくると、1 層のネットワークを複数層へ拡張することは、それほど大きな困難ではなくなる。特に、学習アルゴリズムやデータローダーはそのまま再利用できる。ただし、MLP をゼロから実装するのは依然として煩雑である。モデルパラメータに名前を付けて管理しなければならないため、モデルの拡張が難しくなる。たとえば、42 層目と 43 層目の間に別の層を挿入したいとしよう。その場合、順番に名前を付け直さない限り、42b 層のような不自然な名前を導入することになりかねない。さらに、ネットワークをゼロから実装すると、フレームワークが有効な性能最適化を施すこともはるかに難しくなる。

それでも、完全結合の深層ネットワークがニューラルネットワークモデリングの第一選択であった 1980 年代後半の最先端には到達したことになる。次の概念的な段階では、画像を扱う。その前に、統計の基礎と、モデルを効率よく計算する方法に関する詳細をいくつか復習する必要がある。

5.2.4. 演習

  1. 隠れユニット数 num_hiddens を変え、その値がモデルの精度にどう影響するかをプロットせよ。このハイパーパラメータの最適な値は何か。

  2. 隠れ層を追加し、結果にどう影響するかを調べよ。

  3. 1 個のニューロンしかない隠れ層を挿入するのが悪い考えであるのはなぜか。何がうまくいかなくなる可能性があるか。

  4. 学習率を変えると結果はどう変わるか。他のすべてのパラメータを固定したとき、どの学習率が最良の結果を与えるか。エポック数とどう関係するか。

  5. 学習率、エポック数、隠れ層の数、各隠れ層の隠れユニット数を含む、すべてのハイパーパラメータを同時に最適化せよ。

    1. それらすべてを最適化したとき、得られる最良の結果は何か。

    2. 複数のハイパーパラメータを扱うのがはるかに難しいのはなぜか。

    3. 複数のパラメータを同時に最適化するための効率的な戦略を説明せよ。

  6. より難しい問題に対して、フレームワーク版とゼロからの実装の速度を比較せよ。ネットワークの複雑さによってどう変わるか。

  7. 適切に整列した行列と整列していない行列について、テンソル–行列積の速度を測定せよ。たとえば、次元が 1024、1025、1026、1028、1032 の行列で試せ。

    1. GPU と CPU の間でどう変わるか。

    2. CPU と GPU のメモリバス幅を調べよ。

  8. さまざまな活性化関数を試せ。どれが最もよく機能するか。

  9. ネットワークの重み初期化の違いは結果に影響するか。重要か。