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
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
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_48_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_65_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. ネットワークの重み初期化には違いがあるか。それは重要ですか。