からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

1.3:ニューラルネットワークの学習【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、1.3節「ニューラルネットワークの学習」の内容です。全結合のニューラルネットワークを構成する基本的なレイヤの逆伝播を説明して、各レイヤをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

1.3.1 損失関数

・Softmax関数

 Softmax関数とは、最終層のAffineレイヤの出力(スコア)$\mathbf{s} = (s_1, s_2, \cdots, s_H)$に対して、次の式の計算により活性化を行う関数です。ここで$H$は最終層のニューロン数です。

$$ y_k = \frac{ \exp(s_k) }{ \sum_{k'=1}^H \exp(s_{k'}) } \tag{1.6} $$

 Softmax関数の出力$\mathbf{y} = (y_1, y_2, \cdots, y_H)$は

$$ 0 \leq y_k \leq 1,\ \sum_{k=1}^H y_k = 1 $$

となることから、スコア$\mathbf{s}$を確率値に変換したと解釈できます。この変換(活性化)を正規化と呼びます。詳しくは「3.5:出力層の設計」を参照ください。

 ではSoftmax関数を関数として実装します。

# Softmax関数の実装
def softmax(x):
    # データの形状により条件分岐
    if x.ndim == 2: # 2次元配列のとき
        x = x - x.max(axis=1, keepdims=True) # オーバーフロー対策
        x = np.exp(x) # 式(1.6)の分子
        x /= x.sum(axis=1, keepdims=True) # 式(1.6)
    if x.ndim == 1: # 1次元配列のとき
        x = x - np.max(x) # オーバーフロー対策
        x = np.exp(x) / np.sum(np.exp(x)) # 式(1.6)
    
    return x


 試してみましょう。

# 仮のスコアを生成
s = np.arange(-5, 5)
print(s)

# Softmax関数による正規化
y = softmax(s)
print(np.round(y, 3))
print(np.sum(y))
[-5 -4 -3 -2 -1  0  1  2  3  4]
[0.    0.    0.001 0.002 0.004 0.012 0.031 0.086 0.233 0.632]
1.0

 大小関係を保ったまま0以上1以下の値に変換され、またその和が1になっています。

 次は正規化された出力$\mathbf{y}$を用いて、推論の損失を求めます。

・交差エントロピー誤差

 交差エントロピー誤差は、Softmax関数の出力$\mathbf{y}$とone-hotベクトル(one-hot表現)の教師ラベル$\mathbf{t} = (t_1, t_2, \cdots, t_H)$を用いて、次の式で求めます。

$$ L = - \sum_{k=1}^H t_k \log y_k \tag{1.7} $$


 バッチデータを扱う場合は

$$ L = - \frac{1}{N} \sum_{i=1}^N \sum_{k=1}^H t_{ik} \log y_{ik} \tag{1.8} $$

となります。各データに対して式(1.7)の計算を行い、それをミニバッチ全体で和をとり、バッチサイズで割ります。つまりデータごとの損失の平均と言えます。詳しくは「4.2:損失関数」を参照ください。

 では交差エントロピー誤差を関数として実装します。

# 交差エントロピー誤差の実装
def cross_entropy_error(y, t):
    # 2次元配列に変形
    if y.ndim == 1: # 1次元配列のとき
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)
    
    # 正解ラベルのインデックスに変換
    if t.size == y.size: # one-hotベクトルのとき
        t = t.argmax(axis=1)
    
    # バッチサイズを取得
    batch_size = y.shape[0]
    
    # 交差エントロピー誤差を計算
    return - np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size # 式(1.8)

 1e-7は$\frac{1}{10^7}$のことで、0除算とならないようにするための微小な値です。

 試してみましょう。教師ラベルもNumPy配列である必要があります。

# 正解のインデックスを指定
k = 1

# 仮の教師ラベルを生成
t = np.zeros_like(y)
t[k] = 1
print(t)

# 交差エントロピー誤差を計算
L = cross_entropy_error(y, t)
print(L)
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
8.45815829637557

 全てのデータで和をとるので、損失はスカラになります。

 Softmaxレイヤと損失関数レイヤを合わせて、Softmax with Lossレイヤとして扱います。このレイヤの実装は、1.3.5.3項で逆伝播と共に行います。

1.3.2-4

 微分の基礎については「4.3:数値微分」、勾配については「4.4:勾配」、解析的な微分とチェインルール(連鎖率)については「5.2:連鎖率」を参照ください。
 また加算ノードと乗算ノードについては「5.4:単純なレイヤの実装」を参照ください。

1.3.4.5 MatMulノード

・順伝播の確認

 MatMulレイヤの順伝播は、行列の積のことです。1.1.4項と同じ内容なので、簡単に確認します。

 MatMulノードの順伝播の入力$\mathbf{x}$、重み$\mathbf{W}$を

$$ \mathbf{x} = \begin{pmatrix} x_2 & x_2 & \cdots & x_D \end{pmatrix} ,\ \mathbf{W} = \begin{pmatrix} w_{11} & w_{12} & \cdots & w_{1H} \\ w_{21} & w_{22} & \cdots & w_{2H} \\ \vdots & \vdots & \ddots & \vdots \\ w_{D2} & w_{D2} & \cdots & w_{DH} \end{pmatrix} $$

とすると、順伝播の出力$\mathbf{y}$は

$$ \begin{aligned} \mathbf{y} &= \mathbf{x} \mathbf{W} \\ &= \begin{pmatrix} x_1 & x_2 & \cdots & x_D \end{pmatrix} \begin{pmatrix} w_{11} & w_{12} & \cdots & w_{1H} \\ w_{21} & w_{22} & \cdots & w_{2H} \\ \vdots & \vdots & \ddots & \vdots \\ w_{D1} & w_{D2} & \cdots & w_{DH} \end{pmatrix} \\ &= \begin{pmatrix} \sum_{i=1}^D x_i w_{i1} & \sum_{i=1}^D x_i w_{i2} & \cdots & \sum_{i=1}^D x_i w_{iH} \end{pmatrix} \\ &= \begin{pmatrix} y_1 & y_2 & \cdots & y_H \end{pmatrix} \end{aligned} $$

となります。
 入力の$i$番目の要素$x_i$は、出力$\mathbf{y}$の全ての要素に影響していることが分かります。つまり$x_i$は、次のレイヤの$H$個のニューロン全てに伝播しています。これはRepeatノードの順伝播と言えます。またこのことは図1-7からも分かります。

・逆伝播の確認

 順伝播の入力$\mathbf{x}$に関する勾配$\frac{\partial L}{\partial \mathbf{x}}$は、$\mathbf{x}$と同じ形状なので

$$ \frac{\partial L}{\partial \mathbf{x}} = \begin{pmatrix} \frac{\partial L}{\partial x_1} & \frac{\partial L}{\partial x_2} & \cdots & \frac{\partial L}{\partial x_D} \end{pmatrix} $$

です。これがMatMulレイヤの逆伝播の出力になります。

 この勾配の$i$番目の要素(微分)$\frac{\partial L}{\partial x_i}$を考えます。$x_i$が$y_1$から$y_H$のニューロンに(順)伝播していることから、$x_i$のニューロンには$\frac{\partial L}{\partial y_1}$から$\frac{\partial L}{\partial y_H}$が(逆)伝播してきます。よってRepeatノードの逆伝播より、全ての和

$$ \begin{align} \frac{\partial L}{\partial x_i} &= \frac{\partial L}{\partial y_1} \frac{\partial y_1}{\partial x_i} + \frac{\partial L}{\partial y_2} \frac{\partial y_2}{\partial x_i} + \cdots + \frac{\partial L}{\partial y_H} \frac{\partial y_H}{\partial x_i} \\ &= \sum_{j=1}^H \frac{\partial L}{\partial y_j} \frac{\partial y_j}{\partial x_i} \tag{1.12} \end{align} $$

となります。
 また

$$ y_j = \sum_{i=1}^D x_i w_{ij} $$

なので、順伝播の入力に関する出力の微分は

$$ \frac{\partial y_j}{\partial x_i} = w_{ij} $$

となります。
 従って、MatMulレイヤの$i$番目のニューロンの逆伝播の出力は

$$ \frac{\partial L}{\partial x_i} = \sum_{j=1}^H \frac{\partial L}{\partial y_j} w_{ij} \tag{1.13} $$

になります。

 $i = 1, 2, \cdots, D$についても同様に解けるので

$$ \frac{\partial L}{\partial \mathbf{x}} = \begin{pmatrix} \sum_{j=1}^H \frac{\partial L}{\partial y_j} w_{1j} & \sum_{j=1}^H \frac{\partial L}{\partial y_j} w_{2j} & \cdots & \sum_{j=1}^H \frac{\partial L}{\partial y_j} w_{Dj} \end{pmatrix} $$

となります。
 またこの式は、次のように行列の積に分解できます。

$$ \begin{align} \frac{\partial L}{\partial \mathbf{x}} &= \begin{pmatrix} \frac{\partial L}{\partial y_1} & \frac{\partial L}{\partial y_2} & \cdots & \frac{\partial L}{\partial y_H} \end{pmatrix} \begin{pmatrix} w_{11} & w_{21} & \cdots & w_{H1} \\ w_{12} & w_{22} & \cdots & w_{H2} \\ \vdots & \vdots & \ddots & \vdots \\ w_{1D} & w_{2D} & \cdots & w_{HD} \end{pmatrix} \\ &= \frac{\partial L}{\partial \mathbf{y}} \mathbf{W}^{\mathrm{T}} \tag{1.14} \end{align} $$

 ここで$\mathbf{W}^{\mathrm{T}}$は、$\mathbf{W}$の転置行列です。

 さてこのことを利用してえいやっとすると、MatMulレイヤの逆伝播(各変数の勾配)は

$$ \begin{aligned} \frac{\partial L}{\partial \mathbf{x}} &= \frac{\partial L}{\partial \mathbf{y}} \mathbf{W}^{\mathrm{T}} \\ \frac{\partial L}{\partial \mathbf{W}} &= \mathbf{x}^{\mathrm{T}} \frac{\partial L}{\partial \mathbf{y}} \end{aligned} $$

で計算できます。これはバッチデータであっても成り立ちます。詳しくは「Affineレイヤの逆伝播」を参照ください。

 ではMatMulレイヤをクラスとして実装します。

# MatMulレイヤの実装
class MatMul:
    # 初期化メソッドの定義
    def __init__(self, W):
        self.params = [W] # パラメータ
        self.grads = [np.zeros_like(W)] # 勾配
        self.x = None # 入力
    
    # 順伝播メソッドの定義
    def forward(self, x):
        # 重みを取得
        W, = self.params
        
        # 行列の積を計算
        out = np.dot(x, W)
        
        # 入力を保存
        self.x = x
        
        return out
    
    # 逆伝播メソッドの定義
    def  backward(self, dout):
        # 重みを取得
        W, = self.params
        
        # 勾配を計算
        dx = np.dot(dout, W.T)
        dW = np.dot(self.x.T, dout)
        
        # 勾配を格納
        self.grads[0][...] = dW
        
        return dx

 [...]を使ったコピーについては、本を読んでください。

 W, = self.paramsの処理について確認します。インスタンス変数paramsには、NumPy配列(重み)を1つの要素として格納しています。,を付けて受け取ることで、このリストの要素(配列)を取り出せます。

# NumPy配列を格納したリストを作成
lt = [np.array([0, 1, 2])]
print(lt.__class__)
print(lt)

# NumPy配列として要素を抽出
arr, = lt
print(arr.__class__)
print(arr)
<class 'list'>
[array([0, 1, 2])]
<class 'numpy.ndarray'>
[0 1 2]


 実装したクラスを試してみましょう。入力と重みを生成して、インスタンスを作成します。

# バッチサイズを指定
N = 10

# このレイヤのニューロン数を指定
D = 15

# 次のレイヤのニューロン数を指定
H = 5

# 入力を生成
x = np.random.randn(N, D)
print(x.shape)

# 重みを生成
W = np.random.randn(D, H)
print(W.shape)

# インスタンスを作成
matmal_layer = MatMul(W)
(10, 15)
(15, 5)

 順伝播の入力xの形状を$N \times D$、重みWの形状を$D \times H$とします。

 順伝播と逆伝播のメソッドを実行します。

# 順伝播の計算
y = matmal_layer.forward(x)
print(y.shape)

# 逆伝播の入力を生成
dout = np.random.randn(*y.shape)
print(dout.shape)

# 逆伝播の計算
dx = matmal_layer.backward(dout)
print(dx.shape)

# 重みに関する勾配を取得
dW, = matmal_layer.grads
print(dW.shape)
(10, 5)
(10, 5)
(10, 15)
(15, 5)

 順伝播の出力yと逆伝播の入力doutは$N \times H$に、逆伝播の出力dxx、重みに関する勾配dWWと同じ形状になることを確認できました。

 ここまでは、基本的な計算に関するノードの順伝播と逆伝播の処理を確認しました。次項からは、レイヤ(関数)レベルでの順伝播と逆伝播の確認と実装を行っていきます。

1.3.5 勾配の導出と逆伝播の実装

1.3.5.1 Sigmoidレイヤ

 シグモイド関数の順伝播は、式(1.5)でした。逆伝播は、次の式になります。

$$ \frac{\partial y}{\partial x} = y (1 - y) \tag{1.15} $$

 順伝播の出力$y$のみを用いて計算できます。
 この式の導出については「シグモイド関数の逆伝播」を、実装については「5.5:活性化レイヤの実装」参照ください。

 ではSigmoidレイヤをクラスとして実装します。逆伝播の計算に順伝播の出力outを用いるため、インスタンス変数として保存しておきます。

# Sigmoidレイヤの実装
class Sigmoid:
    # 初期化メソッドの定義
    def __init__(self):
        self.params = [] # パラメータ
        self.grads = [] # 勾配
        self.out = None # 順伝播の出力
    
    # 順伝播メソッドの定義
    def forward(self, x):
        out = 1 / (1 + np.exp(-x)) # 式(1.5)
        
        # 出力を保存
        self.out = out
        return out
    
    # 逆伝播メソッドの定義
    def backward(self, dout):
        dx = dout * (1.0 - self.out) * self.out # 式(1.15)
        return dx


 実装したクラスを試してみましょう。インスタンスを作成して、前項の出力yはバイアスを省略したAffineレイヤの出力と言えるので、yを順伝播メソッドに渡します。

# インスタンスを作成
sigmoid_layer = Sigmoid()

# 順伝播の計算
a = sigmoid_layer.forward(y)
print(np.round(a, 3))
print(a.shape)

# 逆伝播の入力を生成
dout = np.random.randn(*a.shape)

# 逆伝播の計算
da = sigmoid_layer.backward(dout)
print(da.shape)
[[0.051 0.89  0.632 0.988 0.219]
 [0.139 0.216 0.978 0.569 1.   ]
 [0.987 0.994 0.926 0.871 0.004]
 [0.714 0.85  0.147 0.001 0.991]
 [0.745 0.998 0.965 0.014 0.984]
 [0.754 0.947 0.928 0.558 0.808]
 [0.032 0.779 0.07  0.858 0.731]
 [0.135 0.897 0.984 0.204 0.988]
 [1.    0.282 0.085 0.286 0.999]
 [0.924 0.    0.893 0.991 0.627]]
(10, 5)
(10, 5)

 0から1の値に変換されています。また順伝播でも逆伝播でも形状は変わりません。

 Sigmoidレイヤを実装できました。次項では、Sigmoidレイヤとセットで用いるAffineレイヤを実装します。

1.3.5.2 Affineレイヤ

 Affineレイヤの順伝播は、1.2.1項で確認しました。また入力$\mathbf{x}$と重み$\mathbf{W}$に関する勾配は、(加算ノードは逆伝播に影響しないので)MatMulノードの逆伝播と同じ式になります。

 バイアス$\mathbf{b} = (b_1, b_2, \cdots, b_H)$に関する勾配$\frac{\partial L}{\partial \mathbf{b}} = (\frac{\partial L}{\partial b_1}, \frac{\partial L}{\partial b_2}, \cdots, \frac{\partial L}{\partial b_H})$は、Repeatノードの逆伝播より

$$ \frac{\partial L}{\partial b_k} = \sum_{i=1}^N \frac{\partial L}{\partial y_{ik}} $$

となります。
 逆伝播の導出については「バッチ版Affineレイヤの逆伝播」を、実装については「5.6.2:バッチ版Affineレイヤ」を参照ください。

ではAffineレイヤをクラスとして実装します。

 重みとバイアスに関する勾配は、それぞれ同じ形状の全ての要素が0の変数を作成して、リストに格納し、インスタンス変数として保持します。順伝播時に引数に指定する入力xは、逆伝播の計算にも用いるため、これもインスタンス変数として保存します。

# Affineレイヤの実装
class Affine:
    # 初期化メソッドの定義
    def __init__(self, W, b):
        self.params = [W, b] # パラメータ
        self.grads = [np.zeros_like(W), np.zeros_like(b)] # 勾配
        self.x = None # 入力
    
    # 順伝播メソッドの定義
    def forward(self, x):
        # パラメータを取得
        W, b = self.params
        
        # 重み付き和を計算
        out = np.dot(x, W) + b # 式(1.4)
        
        # 入力を保存
        self.x = x
        
        return out
    
    # 逆伝播メソッドの定義
    def backward(self, dout):
        # パラメータを取得
        W, b = self.params
        
        # 勾配を計算:(1.3.4.5項)
        dx = np.dot(dout, W.T)
        dW = np.dot(self.x.T, dout)
        db = np.sum(dout, axis=0)
        
        # 勾配を保存
        self.grads[0][...] = dW
        self.grads[1][...] = db
        return dx


 1.3.4.5項と同様に、実装したクラスを試してみましょう。まずは順伝播に必要な変数を生成して、各レイヤのインスタンスを作成します。

# バッチサイズを指定
N = 10

# このレイヤのニューロン数を指定
D = 15

# 次のレイヤのニューロン数を指定
H = 5

# 入力を生成
x = np.random.randn(N, D)
print(x.shape)

# 重みを生成
W = np.random.randn(D, H)
print(W.shape)

# バイアスを生成
b = np.random.randn(H)
print(b.shape)

# Affineレイヤのインスタンスを作成
affine_layer = Affine(W, b)

# Sigmoidレイヤのインスタンスを作成
sigmoid_layer = Sigmoid()
(10, 15)
(15, 5)
(5,)


 順伝播メソッドを実行します。

# Affineレイヤの順伝播の計算
h = affine_layer.forward(x)
print(h.shape)

# Sigmoidレイヤの順伝播の計算
a = sigmoid_layer.forward(h)
print(a.shape)
(10, 5)
(10, 5)


 逆伝播の入力を生成して、逆伝播メソッドを実行します。

# 逆伝播の入力を生成
dout = np.random.randn(*y.shape)
print(dout.shape)

# Sigmoidレイヤの逆伝播の計算
dout = sigmoid_layer.backward(dout)
print(dout.shape)

# Affineレイヤの逆伝播の計算
dx = affine_layer.backward(dout)
print(dx.shape)

# 重みとバイアスに関する勾配を取得
dW, db = affine_layer.grads
print(dW.shape)
print(db.shape)
(10, 5)
(10, 5)
(10, 15)
(15, 5)
(5,)

 それぞれ対応する変数が同じ形状になっていることを確認できました。

 Affineレイヤを実装できました。次項では、最終層の活性化レイヤと損失関数レイヤを合わせて実装します。

1.3.5.3 Softmax with Lossレイヤ

 Softmax関数と損失関数(交差エントロピー誤差)の順伝播は、1.3.1項で確認しました。またこの二つのレイヤを合わせてSoftmax with Lossレイヤとして扱います。
 これは2つ合わせたレイヤの逆伝播が

$$ \frac{\partial L}{\partial a_{ik}} = \frac{1}{N} ( y_{ik} - t_{ik} ) $$

とシンプルな式になるためです。ここで$N$はバッチサイズ、$y_{ik}$は(最終層の)Affineレイヤの出力、$t_{ik}$は教師ラベルです。$t_{ik}$は、正解ラベルであれば1、それ以外であれば0をとります。
 つまり各要素$y_{ik}$について、正解の要素であれば1を引いてから、正解でなければそのままバッチサイズで割った値を次のレイヤに伝播します。
 この式の導出は「5.6.3:ソフトマックス関数と交差エントロピー誤差の逆伝播」を、実装については「5.6.3:Softmax-with-Lossレイヤ」を参照ください。

 ではSoftmax with Lossレイヤをクラスとして実装します。

 このレイヤの逆伝播の入力doutは、$\frac{\partial L}{\partial L} = 1$のことです。他のレイヤの連鎖率においてもこの項は含まれていますが、値が1のため省略されています。

# Softmax with Lossレイヤの実装
class SoftmaxWithLoss:
    # 初期化メソッドの定義
    def __init__(self):
        self.params = [] # パラメータ
        self.grads = [] # 勾配
        self.y = None # Softmaxレイヤの出力
        self.t = None # 教師ラベル
    
    # 順伝播メソッドの定義
    def forward(self, x, t):
        self.t = t # 教師ラベル
        self.y = softmax(x) # 式(1.6)
        
        # 正解インデックスに変換
        if self.t.size == self.y.size: # one-hotベクトルのとき
            self.t = self.t.argmax(axis=1)
        
        # 交差エントロピー誤差を計算
        loss = cross_entropy_error(self.y, self.t) # 式(1.8)
        return loss
    
    # 逆伝播メソッドの定義
    def backward(self, dout=1):
        # バッチサイズを取得
        batch_size = self.t.shape[0]
        
        # 順伝播の入力をコピー
        dx = self.y.copy()
        
        # 逆伝播の計算:(1.3.5.3項)
        dx[np.arange(batch_size), self.t] -= 1 # 正解ラベルのみ1を引く
        dx *= dout
        dx = dx / batch_size
        return dx


 逆伝播の処理では、次のような操作によって正解ラベルの要素のみ計算しています。

# バッチサイズを指定
N = 5

# クラス(最終レイヤのニューロン)数を指定
H = 5

# 正解ラベルのインデックスを指定
t = np.array([0, 1, 2, 3, 4])

# 0からバッチサイズ未満の整数列を生成
row_idx = np.arange(N)

# 全ての要素が0の配列を作成
dx = np.zeros((N, H))

# 該当する要素から1を引く
dx[row_idx, t] -= 1
print(dx)
[[-1.  0.  0.  0.  0.]
 [ 0. -1.  0.  0.  0.]
 [ 0.  0. -1.  0.  0.]
 [ 0.  0.  0. -1.  0.]
 [ 0.  0.  0.  0. -1.]]


 実装してクラスを試してみましょう。

 各データのサイズを指定してAffineレイヤの変数を生成して、また各レイヤのインスタンスを作成します。

# バッチサイズ,1つ前のニューロン数,クラス数を指定
N, D, H = 10, 15, 5

# Affineレイヤの入力を生成
x = np.random.randn(N, D)
print(x.shape)

# 重みを生成
W = np.random.randn(D, H)
print(W.shape)

# バイアスを生成
b = np.random.randn(H)
print(b.shape)

# Affineレイヤのインスタンスを作成
affine_layer = Affine(W, b)

# Softmax with Lossレイヤのインスタンスを作成
loss_layer = SoftmaxWithLoss()
(10, 15)
(15, 5)
(5,)


 Affineレイヤの順伝播メソッドを実行し、対応する教師ラベルを生成します。最後にSoftmax with Lossレイヤの順伝播メソッドを実行します。

# Affineレイヤの順伝播の計算
y = affine_layer.forward(x)
print(y.shape)

# 教師ラベルを生成
t = np.zeros_like(y)
k = np.random.randint(0, H, N) # 正解ラベル
t[np.arange(N), k] = 1
print(t)
print(y.shape)

# Softmax with Lossレイヤの順伝播の計算
L = loss_layer.forward(y, t)
print(L)
(10, 5)
[[0. 0. 1. 0. 0.]
 [0. 0. 0. 1. 0.]
 [0. 1. 0. 0. 0.]
 [0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1.]
 [0. 0. 1. 0. 0.]
 [0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 1.]
 [0. 1. 0. 0. 0.]
 [1. 0. 0. 0. 0.]]
(10, 5)
3.322577465420487

 これで損失(交差エントロピー誤差)が求まりました。

 続いて逆伝播メソッドを実行します。

# 逆伝播の入力
dout = 1

# Softmax with Lossレイヤの逆伝播の計算
dout = loss_layer.backward(dout)
print(dout.shape)

# Affineレイヤの逆伝播の計算
dout = affine_layer.backward(dout)
print(dout.shape)
(10, 5)
(10, 15)


 以上でニューラルネットワークを構成する基本パーツを実装できました。これで誤差逆伝播法による勾配を求められます。次は勾配を用いてパラメータを更新する処理を実装します。

1.3.6 重みの更新

 勾配$\frac{\partial L}{\partial \mathbf{W}}$を用いて、次の式によりパラメータを更新します。

$$ \mathbf{W} \leftarrow \mathbf{W} - \eta \frac{\partial L}{\partial \mathbf{W}} \tag{1.16} $$

 ここで$\eta$は学習係数(学習率)で、更新の幅を調整する項です。元のパラメータ$\mathbf{W}$から$\eta$で割り引いた勾配を引いた値を新たなパラメータとします。$\leftarrow$は、右辺の計算によって左辺の$\mathbf{W}$を更新することを表現しています。
 この手法を勾配降下法(SGD)と言います。詳しい内容やSGDを発展させた手法については「6.1節の記事」を参照ください。

 ではSGDをクラスとして実装します。

# SGDの実装
class SGD:
    # インスタンスメソッドの定義
    def __init__(self, lr=0.01):
        self.lr = lr # 学習率
    
    # 更新メソッド
    def update(self, params, grads):
        # パラメータごとに更新
        for i in range(len(params)):
            params[i] -= self.lr * grads[i] # 式(1.16)


 実装したクラスを試してみましょう。前項のAffineレイヤのインスタンスに保存されているパラメータparamsと勾配gradsをそのまま用います。

# 最適化手法のインスタンスを作成
optimizer = SGD(lr=0.01)

# パラメータを更新
optimizer.update(affine_layer.params, affine_layer.grads)

# パラメータを取得
W, b = affine_layer.params
print(np.round(W, 2))
print(W.shape)
print(np.round(b, 2))
print(b.shape)
[[ 0.71  2.15 -0.75 -0.58 -0.43]
 [-0.23 -1.74  1.47 -0.51 -0.53]
 [ 1.67 -0.74 -1.8  -0.26  2.36]
 [-0.63 -0.19 -1.49  0.88 -2.23]
 [-0.96  0.19  1.88 -0.32  0.05]
 [-0.64  0.68  0.54  0.2   1.02]
 [-1.86 -1.04  1.32  1.98 -0.69]
 [-1.56  0.49  1.77  0.4   1.05]
 [ 1.31  1.03  0.81 -0.77  1.56]
 [-0.47  1.9  -1.03 -0.94 -1.36]
 [ 0.53  0.15 -0.14 -0.64  2.31]
 [-1.6   0.4   0.57  0.58  0.39]
 [ 0.23 -1.97  0.94 -0.81 -0.61]
 [-0.68  0.29  1.83  0.11  1.42]
 [ 0.25  0.17  0.72  0.71  0.16]]
(15, 5)
[-1.08 -0.41 -0.6   1.67  1.6 ]
(5,)

 乱数によって生成したデータを使っているので、この例の値を見ること自体に意味はありませんが、このような処理によってパラメータを更新します。

 次節では、ここまでに実装したレイヤを用いて2層のニューラルネットワークを実装し、また実際にデータを使って学習を行います。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 1章は1巻の復習なので、サクサク進められてます。実装方法が1巻と少し変わっていますが、その恩恵はまだ感じられていません。いつ頃利いてくるのかな。

 1巻については、かなり細かく噛み砕いた解説記事にしました。2巻の記事のレベルではまだ理解しきれないという方は、是非リンクしている記事も読んでみてください!

【次節の内容】

https://www.anarchive-beta.com/entry/2020/08/30/190000www.anarchive-beta.com