5.5 激活函数层的实现

现在,我们将计算图的思路应用到神经网络中。这里,我们把构成神经网络的层实现为一个类。先来实现激活函数的 ReLU 层和 Sigmoid 层。

5.5.1 ReLU层

激活函数 ReLU(Rectified Linear Unit)由下式(5.7)表示。

5.5 激活函数层的实现 - 图1

通过式(5.7),可以求出 y 关于 x 的导数,如式(5.8)所示。

5.5 激活函数层的实现 - 图2

在式(5.8)中,如果正向传播时的输入 x 大于 0,则反向传播会将上游的值原封不动地传给下游。反过来,如果正向传播时的 x 小于等于 0,则反向传播中传给下游的信号将停在此处。用计算图表示的话,如图 5-18 所示。

现在我们来实现 ReLU 层。在神经网络的层的实现中,一般假定 forward()backward() 的参数是 NumPy 数组。另外,实现 ReLU 层的源代码在 common/layers.py 中。

5.5 激活函数层的实现 - 图3

图 5-18 ReLU 层的计算图

  1. class Relu:
  2. def __init__(self):
  3. self.mask = None
  4. def forward(self, x):
  5. self.mask = (x <= 0)
  6. out = x.copy()
  7. out[self.mask] = 0
  8. return out
  9. def backward(self, dout):
  10. dout[self.mask] = 0
  11. dx = dout
  12. return dx

Relu 类有实例变量 mask。这个变量 mask 是由 True/False 构成的 NumPy 数组,它会把正向传播时的输入 x 的元素中小于等于 0 的地方保存为 True,其他地方(大于 0 的元素)保存为 False。如下例所示,mask 变量保存了由 True/False 构成的 NumPy 数组。

  1. >>> x = np.array( [[1.0, -0.5], [-2.0, 3.0]] )
  2. >>> print(x)
  3. [[ 1. -0.5]
  4. [-2. 3. ]]
  5. >>> mask = (x <= 0)
  6. >>> print(mask)
  7. [[False True]
  8. [ True False]]

如图 5-18 所示,如果正向传播时的输入值小于等于 0,则反向传播的值为 0。因此,反向传播中会使用正向传播时保存的 mask,将从上游传来的 doutmask 中的元素为 True 的地方设为 0。

5.5 激活函数层的实现 - 图4 ReLU 层的作用就像电路中的开关一样。正向传播时,有电流通过的话,就将开关设为 ON;没有电流通过的话,就将开关设为 OFF。反向传播时,开关为 ON 的话,电流会直接通过;开关为 OFF 的话,则不会有电流通过。

5.5.2 Sigmoid 层

接下来,我们来实现 sigmoid 函数。sigmoid 函数由式(5.9)表示。

y=\frac{1}{1+\exp(-x)}\quad\quad\quad\quad\quad(5.9)

用计算图表示式(5.9)的话,则如图 5-19 所示。

5.5 激活函数层的实现 - 图6

图 5-19 sigmoid 层的计算图(仅正向传播)

图 5-19 中,除了“×”和“+”节点外,还出现了新的“exp”和“/”节点。“exp”节点会进行 y = exp(x) 的计算,“/”节点会进行 y=\frac{1}{x} 的计算。

如图 5-19 所示,式(5.9)的计算由局部计算的传播构成。下面我们就来进行图 5-19 的计算图的反向传播。这里,作为总结,我们来依次看一下反向传播的流程。

步骤 1

“/”节点表示 y=\frac{1}{x},它的导数可以解析性地表示为下式。

\begin{aligned}\frac{\partial y}{\partial x}&=-\frac{1}{x^2}\\&=-y^2\end{aligned}\quad\quad\quad\quad\quad(5.10)

根据式(5.10),反向传播时,会将上游的值乘以 -y^2(正向传播的输出的平方乘以 -1 后的值)后,再传给下游。计算图如下所示。

5.5 激活函数层的实现 - 图11

步骤 2

“+”节点将上游的值原封不动地传给下游。计算图如下所示。

5.5 激活函数层的实现 - 图12

步骤 3

“exp”节点表示 y = exp(x),它的导数由下式表示。

\frac{\partial y}{\partial x}=\exp(x)\quad\quad\quad\quad\quad(5.11)

计算图中,上游的值乘以正向传播时的输出(这个例子中是 exp(-x))后,再传给下游。

5.5 激活函数层的实现 - 图14

步骤 4

“×”节点将正向传播时的值翻转后做乘法运算。因此,这里要乘以 -1。

5.5 激活函数层的实现 - 图15

图 5-20 Sigmoid 层的计算图

根据上述内容,图 5-20 的计算图可以进行 Sigmoid 层的反向传播。从图 5-20 的结果可知,反向传播的输出为 \frac{\partial L}{\partial y}y^2\exp(-x),这个值会传播给下游的节点。这里要注意,\frac{\partial L}{\partial y}y^2\exp(-x) 这个值只根据正向传播时的输入 x 和输出 y 就可以算出来。因此,图 5-20 的计算图可以画成图 5-21 的集约化的“sigmoid”节点。

5.5 激活函数层的实现 - 图18

图 5-21 Sigmoid 层的计算图(简洁版)

图 5-20 的计算图和简洁版的图 5-21 的计算图的计算结果是相同的,但是,简洁版的计算图可以省略反向传播中的计算过程,因此计算效率更高。此外,通过对节点进行集约化,可以不用在意 Sigmoid 层中琐碎的细节,而只需要专注它的输入和输出,这一点也很重要。

另外,\frac{\partial L}{\partial y}y^2\exp(-x) 可以进一步整理如下。

\begin{aligned}\frac{\partial L}{\partial y}y^2\exp(-x)&=\frac{\partial L}{\partial y}\frac{1}{(1+\exp(-x))^2}\exp(-x)\\&=\frac{\partial L}{\partial y}\frac{1}{1+\exp(-x)}\frac{\exp(-x)}{1+\exp(-x)}\\&=\frac{\partial L}{\partial y}y(1-y)\end{aligned}\quad\quad\quad\quad\quad(5.12)

因此,图 5-21 所表示的 Sigmoid 层的反向传播,只根据正向传播的输出就能计算出来。

5.5 激活函数层的实现 - 图21

图 5-22 Sigmoid 层的计算图:可以根据正向传播的输出 \boldsymbol{y} 计算反向传播

现在,我们用 Python 实现 Sigmoid 层。参考图 5-22,可以像下面这样实现(实现的代码在 common/layers.py 中)。

  1. class Sigmoid:
  2. def __init__(self):
  3. self.out = None
  4. def forward(self, x):
  5. out = 1 / (1 + np.exp(-x))
  6. self.out = out
  7. return out
  8. def backward(self, dout):
  9. dx = dout * (1.0 - self.out) * self.out
  10. return dx

这个实现中,正向传播时将输出保存在了实例变量 out 中。然后,反向传播时,使用该变量 out 进行计算。