6.1 パラメータの更新

「ゼロから作るDeep Learning」 6章1節の内容を学習しながら、Ivoryライブラリでoptimizerモジュールを作成していきます。オプティマイザーの基底クラスであるOptimizerクラスは、学習するべきパラメータのリストparamsと学習率を属性に持ちます。(なお、OptimizerクラスおよびそのサブクラスはPython 3.7で導入されたデータクラスですが、以下の定義では@dataclassの記述は省略されています。)

Code 6.1 Optimizerクラス

class Optimizer:
    learning_rate: float = 0.01
    variables: List[Variable] = field(default_factory=list, repr=False)
    name: str = ""

    def __post_init__(self):
        if not self.name:
            self.name = self.__class__.__name__

    def set_variables(self, variables: Iterable[Variable]):
        self.variables = list(variables)
        self.init()

    def set_model(self, model: Model):
        self.set_variables(model.weight_variables)

    def init(self):
        pass

    def update(self):
        pass

以下に、各々の更新手法の計算式と「ゼロから作るDeep Learning」を参考にして実装したOptimizerのサブクラスをまとめます。

6.1.2 SGD

\mathbf{W} \leftarrow \mathbf{W} - \eta\frac{\partial L}{\partial \mathbf{W}}

Code 6.2 SGDクラス

class SGD(Optimizer):
    def update(self):
        for variable in self.variables:
            variable.data -= self.learning_rate * variable.grad

6.1.4 Momentum

\mathbf{v} \leftarrow \alpha\mathbf{v} - \eta\frac{\partial L}{\partial \mathbf{W}}
\mathbf{W} \leftarrow \mathbf{W} + \mathbf{v}

Code 6.3 Momentumクラス

class Momentum(Optimizer):
    momentum: float = 0.9
    v: List[Data] = field(init=False)

    def init(self):
        self.v = [np.zeros_like(variable.data) for variable in self.variables]

    def update(self):
        for k, variable in enumerate(self.variables):
            self.v[k] = self.momentum * self.v[k] - self.learning_rate * variable.grad
            variable.data += self.v[k]

6.1.5 AdaGrad

\mathbf{h} \leftarrow \mathbf{h} + \frac{\partial L}{\partial \mathbf{W}}\odot\frac{\partial L}{\partial \mathbf{W}}
\mathbf{W} \leftarrow \mathbf{W} - \eta\frac1{\sqrt{\mathbf{h}}}\frac{\partial L}{\partial \mathbf{W}}

Code 6.4 AdaGradクラス

class AdaGrad(Optimizer):
    h: List[Data] = field(init=False)

    def init(self):
        self.h = [np.zeros_like(variable.data) for variable in self.variables]

    def update(self):
        for k, variable in enumerate(self.variables):
            self.h[k] += variable.grad * variable.grad
            sqrt = np.sqrt(self.h[k]) + 1e-7
            variable.data -= self.learning_rate * variable.grad / sqrt

6.1.6 Adam

Code 6.5 Adamクラス

class Adam(Optimizer):
    """Adam (http://arxiv.org/abs/1412.6980v8)"""

    learning_rate: float = 0.001
    beta1: float = 0.9
    beta2: float = 0.999
    iteration: int = field(init=False)
    m: List[Data] = field(init=False)
    v: List[Data] = field(init=False)

    def init(self):
        self.iteration = 0
        self.m = [np.zeros_like(variable.data) for variable in self.variables]
        self.v = [np.zeros_like(variable.data) for variable in self.variables]

    def update(self):
        self.iteration += 1
        lr, it = self.learning_rate, self.iteration
        lr *= np.sqrt(1 - self.beta2 ** it) / (1 - self.beta1 ** it)

        for k, variable in enumerate(self.variables):
            self.m[k] += (1 - self.beta1) * (variable.grad - self.m[k])
            self.v[k] += (1 - self.beta2) * (variable.grad ** 2 - self.v[k])
            variable.data -= lr * self.m[k] / (np.sqrt(self.v[k]) + 1e-7)

6.1.7 どの更新手法を用いるか?

ch06/optimizer_compare_naive.pyを参考にして、上述の更新手法を比較してみます。例題となる関数の定義です。

import numpy as np

def f(x):
    return x[0] ** 2 / 20 + x[1] ** 2

def df(x):
    return np.array([x[0] / 10, 2 * x[1]])

[23] 2019-06-12 17:35:30 (31.4ms) python3 (809ms)

オプティマイザーを引数としてとり、パラメータの更新ごとに結果をイールドするジェネレータを定義します。例題の関数に合わせて2次元ベクトルのパラメータを使います。

from ivory.core.variable import Variable

def train(optimizer):
    x = Variable((2,))  # 2次元ベクトルのパラメータ
    x.data = np.array([-7.0, 2.0])
    optimizer.set_variables([x])

    for i in range(30):
        yield x.data.copy()  # inplaceで値が更新されるため、コピー値を返す。
        x.grad = df(x.data)
        optimizer.update()  # ここで、x.gradに従って、x.dataが更新される。

[24] 2019-06-12 17:35:30 (15.6ms) python3 (825ms)

訓練と可視化を行います。

import matplotlib.pyplot as plt

from ivory.core.optimizer import SGD, AdaGrad, Adam, Momentum

optimizers = [
    SGD(learning_rate=0.95),
    Momentum(learning_rate=0.1),
    AdaGrad(learning_rate=1.5),
    Adam(learning_rate=0.3),
]
outs = [np.array(list(train(optimizer))) for optimizer in optimizers]

x = np.arange(-8, 8, 0.01)
y = np.arange(-3, 3, 0.01)
xs, ys = np.meshgrid(x, y)
zs = f([xs, ys])
zs[zs > 7] = 0

for k, out in enumerate(outs):
    plt.subplot(2, 2, k + 1)
    plt.contour(xs, ys, zs)
    plt.plot(out[:, 0], out[:, 1], "o-", color="red")
    plt.plot(0, 0, "+")
    name = optimizers[k].name
    plt.title(name)

plt.tight_layout()
plt.show()

[26] 2019-06-12 17:35:30 (844ms) python3 (1.79s)

image/png

6.1.8 MNISTデータセットによる更新手法の比較

ch06/optimizer_compare_mnist.pyおよびcommon/multi_layer_net.pyを参考にして、上述の更新手法を比較してみます。

データセットを準備します。

from ivory.datasets.mnist import load_dataset

data = load_dataset(train_only=True)  # 今回は訓練データのみ使う
data.batch_size = 128
data.epochs = -1
data.random = True
data

[27] 2019-06-12 17:35:31 (1.72s) python3 (3.51s)

mnist_train(batch_size=128, epochs=-1, len=468, column=0, size=(60000,))

ネットワークを構築します。

from ivory.core.model import sequential

net = [
    ("input", 784),
    (4, "affine", 100, "relu"),
    ("affine", 10, "softmax_cross_entropy"),
]
model = sequential(net)
model.layers

[28] 2019-06-12 17:35:32 (31.2ms) python3 (3.54s)

[<Affine('Affine.1', (784, 100)) at 0x1f56a714320>,
 <Relu('Relu.1', (100,)) at 0x1f56a7145c0>,
 <Affine('Affine.2', (100, 100)) at 0x1f56a714668>,
 <Relu('Relu.2', (100,)) at 0x1f56a714828>,
 <Affine('Affine.3', (100, 100)) at 0x1f56a714908>,
 <Relu('Relu.3', (100,)) at 0x1f56a714ac8>,
 <Affine('Affine.4', (100, 100)) at 0x1f56a714ba8>,
 <Relu('Relu.4', (100,)) at 0x1f56a714d68>,
 <Affine('Affine.5', (100, 10)) at 0x1f56a714e48>,
 <SoftmaxCrossEntropy('SoftmaxCrossEntropy.1', (10,)) at 0x1f56a71e048>]

訓練を行うジェネレータ関数を定義します。

def train(optimizer):  # type: ignore
    print(optimizer.name)
    for variable in model.weight_variables:  # 重みを初期化
        variable.data = variable.init()
    optimizer.set_model(model)

    for _, (x, t) in zip(range(2000), data):
        model.set_data(x, t)
        model.forward()
        yield model.loss
        model.backward()
        optimizer.update()

[29] 2019-06-12 17:35:32 (15.6ms) python3 (3.56s)

訓練を実行します。

optimizers = [SGD(), Momentum(), AdaGrad(), Adam()]
result = [list(train(optimizer)) for optimizer in optimizers]

[30] 2019-06-12 17:35:32 (30.9s) python3 (34.4s)

SGD
Momentum
AdaGrad
Adam

可視化を行います。

from scipy.signal import savgol_filter

markers = ["o", "x", "s", "D"]
for k, x in enumerate(savgol_filter(result, 9, 3)):
    plt.plot(x, marker=markers[k], markevery=100, label=optimizers[k].name)
plt.xlabel("iterations")
plt.ylabel("loss")
plt.xlim(0, 2000)
plt.ylim(0, 1)
plt.legend()
plt.show()

[31] 2019-06-12 17:36:03 (1.22s) python3 (35.6s)

image/png