はじめに
「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。
この記事は、5.5.1項「ReLUレイヤ」の内容です。ReLUレイヤをPythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
5.5.1 ReLUレイヤの実装
ReLU関数の順伝播と逆伝播の計算を行うReLUレイヤを実装します。ReLU関数(順伝播)については「3.2.7:ReLU関数の実装【ゼロつく1のノート(実装)】 - からっぽのしょこ」を参照してください。
利用するライブラリを読み込みます。
# 5.5.1項で利用するライブラリ import numpy as np import matplotlib.pyplot as plt
・数式の確認
まずは、ReLUレイヤの定義式を確認します。
順伝播では、入力を$\mathbf{X} = (x_{0,0}, \cdots, x_{N-1,H-1})$、出力を$\mathbf{Y} = (y_{0,0}, \cdots, y_{N-1,H-1})$として、行列の各項に対してReLU関数の計算をします。$N$はバッチサイズ(1試行当たりのデータ数)、$H$は中間層(次の層)のニューロン数です。また、Pythonのインデックスに合わせて添字を0から割り当てています。
順伝播の各入力(ReLU関数の入力)$x_{n,h}$が、0より大きいときは$x_{n,h}$をそのまま出力し、0以下のときは0を出力します。
逆伝播では、「逆伝播の入力$\frac{\partial L}{\partial y_{n,h}}$」と「ReLUレイヤの微分$\frac{\partial y_{n,h}}{\partial x_{n,h}}$」の積$\frac{\partial L}{\partial x_{n,h}} = \frac{\partial L}{\partial y_{n,h}} \frac{\partial y_{n,h}}{\partial x_{n,h}}$を求めます。
$\frac{\partial L}{\partial x_{n,h}}$は、順伝播の入力$x_{n,k}$に関する損失$L$の微分であり、逆伝播の出力です。詳しくは「5.2:連鎖率【ゼロつく1のノート(数学)】 - からっぽのしょこ」を参照してください。
$\frac{\partial L}{\partial y_{n,h}}$は、順伝播の出力$y_{n,k}$に関する損失$L$の微分です。これは、次のレイヤの逆伝播で計算されるため、このレイヤでは計算しません。
$\frac{\partial y_{n,h}}{\partial x_{n,h}}$は、ReLU関数の入力$x_{n,h}$に関する出力$y_{n,h}$の微分であり、次の式で計算します。
順伝播の入力$x_{n,h}$が、0より大きいときは$x_{n,h}$を$x_{n,h}$で微分するので1になり、0以下のときは0を$x_{n,h}$で微分するので0になります。
順伝播において$x_{n,h}$に1か0を掛ける乗算ノードと捉えると、逆伝播ではひっくり返した値を出力しているのが分かります(5.3.2項)。
逆伝播の計算においても順伝播の入力が影響してます。
したがって、逆伝播の出力$\frac{\partial L}{\partial x_{n,h}}$は、次の式で計算できます。
以上が、ReLUレイヤで行う計算です。
・処理の確認
次に、ReLUレイヤで行う処理を確認します。
・順伝播の計算
3.2.7項では、np.maximum()
を使ってReLU関数を実装しました。ReLUレイヤでは、順伝播の入力と0との大小関係を逆伝播でも利用します。そこで、入力の各項が0以下かどうかの情報をmask
として保存しておき、順伝播と逆伝播の計算に用います。
順伝播の入力$\mathbf{X}$を作成します。
# (仮の)順伝播の入力を作成 x = np.array([[1.0, -0.5], [0.0, 3.0]]) print(x)
[[ 1. -0.5]
[ 0. 3. ]]
ここでは簡単に、$2 \times 2$の2次元配列とします。
x
に関して、値が0以下の要素の情報をmask
として保存します。
# 0以下の要素の情報を保存 mask = (x <= 0) print(mask)
[[False True]
[ True False]]
比較演算子<=
を使って、値が0
以下の要素を調べます。条件に合う要素(と同じ位置の要素)はTrue
、合わない要素はFalse
となります。
入力x
を複製してy
とします。
# 順伝播の入力を複製 y = x.copy() print(y)
[[ 1. -0.5]
[ 0. 3. ]]
mask
を添字として使うとTrue
の要素のみを取り出せます。
# 0以下の要素を抽出 print(y[mask])
[-0.5 0. ]
この機能を利用して、True
(0以下)の要素のみに0
を代入します。
# 0以下の要素を0に置換 y[mask] = 0 print(y)
[[1. 0.]
[0. 3.]]
順伝播の出力が得られました。0以上の要素はそのままの値を持ち、0以下の要素は0となります。y
を次のレイヤ(Affineレイヤ)に入力します。
ここまでは、順伝播の処理を確認しました。続いて、逆伝播の処理を確認します。
・逆伝播の計算
逆伝播の入力$\frac{\partial L}{\partial \mathbf{Y}}$を作成します。
# (仮の)逆伝播の入力を作成 dy = np.ones_like(y) print(dy)
[[1. 1.]
[1. 1.]]
$\frac{\partial L}{\partial \mathbf{Y}}$は$\mathbf{Y}$と同じ形状です。ここでは簡単に、全ての要素を1
とします。
順伝播において0以下だった要素に0
を代入します。
# 0以下だった要素を0に置換 dy[mask] = 0 print(dy)
[[1. 0.]
[0. 1.]]
順伝播において0以上だった要素はそのままの値を持ち、0以下の要素は0となりました。
変数を複製します。
# 変数を複製 dx = dy print(dx)
[[1. 0.]
[0. 1.]]
逆伝播の出力が得られました。dx
を前のレイヤ(Affineレイヤ)に入力します。
以上がReLUレイヤで行う処理です。
・実装
処理の確認ができたので、ReLUレイヤをクラスとして実装します。
# ReLUレイヤの実装 class Relu: # 初期化メソッド def __init__(self): # 順伝播の入力における0以下の要素の情報を初期化 self.mask = None # 順伝播メソッド def forward(self, x): # 0以下の要素の情報を保存 self.mask = (x <= 0) # 順伝播の入力を複製 out = x.copy() # 0以下の要素を0に置換 out[self.mask] = 0 return out # 逆伝播メソッド def backward(self, dout): # 順伝播時に0以下だった要素を0に置換 dout[self.mask] = 0 # 複製 dx = dout return dx
順伝播の計算時に(順伝播メソッドの実行時に)、順伝播の入力x
(の各項)と0
との大小関係をインスタンス変数mask
として保存します。
実装したクラスを試してみましょう。
Relu
クラスのインスタンスを作成します。
# ReLUレイヤのインスタンスを作成
layer = Relu()
順伝播の入力を作成して、順伝播の計算をします。
# (仮の)順伝播の入力を作成 x = np.array([[1.0, -0.5], [0.0, 3.0]]) print(x) # 順伝播を計算 y = layer.forward(x) print(y)
[[ 1. -0.5]
[ 0. 3. ]]
[[1. 0.]
[0. 3.]]
逆伝播の入力を作成して、逆伝播の計算をします。
# (仮の)逆伝播の入力を作成 dy = np.ones_like(y) print(dy) # 逆伝播を計算 dx = layer.backward(dy) print(dx)
[[1. 1.]
[1. 1.]]
[[1. 0.]
[0. 1.]]
ReLUレイヤを実装できました。
・グラフの確認
最後に、ReLUレイヤの順伝播と逆伝播をグラフで確認します。
Relu
クラスのインスタンスを作成します。
# ReLUレイヤのインスタンスを作成
layer = Relu()
作図用に順伝播の各入力$x$がとり得る値を作成して、順伝播を計算します。
# 作図用の順伝播の入力を作成 x_vals = np.arange(-3.0, 3.0, 0.01) # 順伝播を計算 y_vals = layer.forward(x_vals) print(np.round(y_vals[:5], 2)) # 前から5つ print(np.round(y_vals[-5:], 2)) # 後から5つ
[0. 0. 0. 0. 0.]
[2.95 2.96 2.97 2.98 2.99]
順伝播のグラフを作成します。
# 順伝播のグラフを作成 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, y_vals, label='forward') # 折れ線グラフ plt.xlabel('x') # x軸ラベル plt.ylabel('y') # y軸ラベル plt.title('ReLU Layer', fontsize=20) # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
当然ですが、ReLU関数のグラフになります。
作図用の逆伝播の入力を作成して、逆伝播を計算します。
# 作図用の逆伝播の入力を作成 dy_vals = np.ones_like(y_vals) # 逆伝播を計算 dx_vals = layer.backward(dy_vals) print(np.round(dx_vals[:5], 2)) # 前から5つ print(np.round(dx_vals[-5:], 2)) # 後から5つ
[0. 0. 0. 0. 0.]
[1. 1. 1. 1. 1.]
作図用の逆伝播の入力は全ての要素(点)が1
です。
逆伝播のグラフを作成します。
# 逆伝播のグラフを作成 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, dx_vals, label='backward') # 折れ線グラフ plt.xlabel('x') # x軸ラベル plt.ylabel('dx') # y軸ラベル plt.title('ReLU Layer', fontsize=20) # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
$x \leq 0$のとき$\frac{\partial y}{\partial x} = 0$、$x > 0$のとき$\frac{\partial y}{\partial x} = 1$です。(ステップ関数の形になったのに深い意味はないよね?)
順伝播と逆伝播のグラフを重ねて描画します。
# ReLUレイヤのグラフを作成 plt.figure(figsize=(8, 6)) # 図の設定 plt.plot(x_vals, y_vals, label='forward') # ReLU関数 plt.plot(x_vals, dx_vals, label='backward') # 勾配 plt.xlabel('x') # x軸ラベル plt.ylabel('y, dx') # y軸ラベル plt.title('ReLU Layer', fontsize=20) # タイトル plt.legend() # 凡例 plt.grid() # グリッド線 plt.show()
$x < 0$の範囲では、順伝播のグラフ(青線)は$y = 0$で変化しないので(傾きが0なので)、逆伝播のグラフ(オレンジ線)は$\frac{\partial y}{\partial x} = 0$なのが分かります。逆に$x > 0$の範囲では、$y = x$なので(傾きが1なので)、$\frac{\partial y}{\partial x} = 1$なのが分かります。
以上で、ReLUレイヤを実装できました。次は、Sigmoidレイヤを実装します。
参考文献
- 斎藤康毅『ゼロから作るDeep Learning』オライリー・ジャパン,2016年.
- サポートページ:Sigmoidレイヤのコード(https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/common/layers.py)
おわりに
順調ですか?
- 2021.09.19:加筆修正しました。その際に記事を分割しました。
【次節の内容】