からっぽのしょこ

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

5.5.1:ReLUレイヤの実装【ゼロつく1のノート(実装)】

はじめに

 「プログラミング」初学者のための『ゼロから作るDeep Learning』攻略ノートです。『ゼロつくシリーズ』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 関数やクラスとして実装される処理の塊を細かく分解して、1つずつ実行結果を見ながら処理の意図を確認していきます。

 この記事は、5.5.1項「ReLUレイヤ」の内容です。ReLUレイヤをPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

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から割り当てています。

$$ \begin{align} y_{n,h} = \begin{cases} x_{n,h} &\quad (x_{n,h} > 0) \\ 0 &\quad (x_{n,h} \leq 0) \end{cases} \tag{5.7} \end{align} $$

 順伝播の各入力(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}$の微分であり、次の式で計算します。

$$ \begin{align} \frac{\partial y_{n,h}}{\partial x_{n,h}} = \begin{cases} 1 &\quad (x_{n,h} > 0) \\ 0 &\quad (x_{n,h} \leq 0) \end{cases} \tag{5.8} \end{align} $$

 順伝播の入力$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}}$は、次の式で計算できます。

$$ \begin{aligned} \frac{\partial L}{\partial x_{n,h}} &= \frac{\partial L}{\partial y_{n,h}} \frac{\partial y_{n,h}}{\partial x_{n,h}} \\ &= \begin{cases} \frac{\partial L}{\partial y_{n,h}} &\quad (x_{n,h} > 0) \\ 0 &\quad (x_{n,h} \leq 0) \end{cases} \end{aligned} $$

 以上が、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レイヤの順伝播のグラフ

 当然ですが、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()

ReLUレイヤの逆伝播のグラフ

 $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()

ReLUレイヤのグラフ

 $x < 0$の範囲では、順伝播のグラフ(青線)は$y = 0$で変化しないので(傾きが0なので)、逆伝播のグラフ(オレンジ線)は$\frac{\partial y}{\partial x} = 0$なのが分かります。逆に$x > 0$の範囲では、$y = x$なので(傾きが1なので)、$\frac{\partial y}{\partial x} = 1$なのが分かります。

 以上で、ReLUレイヤを実装できました。次は、Sigmoidレイヤを実装します。

参考文献

おわりに

 順調ですか?

  • 2021.09.19:加筆修正しました。その際に記事を分割しました。


【次節の内容】

www.anarchive-beta.com