神经网络学习_反向传播与梯度下降(2)

神经网络学习_反向传播与梯度下降(2)

前言

本节数学推导较多,然而是必经之路。

前向计算

我们从一个简单的神经元模型开始:

从输入到输出的计算过程是这样的: \[ \begin{align} &Z=\sum_{i=1}^{3}W_iX_i+b\\ &A=Activation(Z) \end{align} \] 神经元数量一多,整体的计算就显得特别复杂,因此我们采用矩阵乘法来简化计算: \[ \begin{bmatrix} W_1 & W_2 & W_3 \end{bmatrix} \begin{bmatrix} X_1 \\ X_2 \\ X_3 \end{bmatrix} +b =\sum_{i=1}^{3}W_iX_i+b \]

左行右列,对应相乘。

对于单层两个神经元:

第一个: \[ \begin{bmatrix} W_{11} & W_{21} & W_{31} \end{bmatrix} \begin{bmatrix} X_1 \\ X_2 \\ X_3 \end{bmatrix} +b_1 =\sum_{i=1}^{3}W_{i1}X_i+b_1 \] 第二个: \[ \begin{bmatrix} W_{12} & W_{22} & W_{32} \end{bmatrix} \begin{bmatrix} X_1 \\ X_2 \\ X_3 \end{bmatrix} +b_2 =\sum_{i=1}^{3}W_{i2}X_i+b_2 \]

把两个结合到一起: \[ \begin{bmatrix} W_{11} & W_{21} & W_{31} \\ W_{12} & W_{22} & W_{32} \end{bmatrix} \begin{bmatrix} X_1 \\ X_2 \\ X_3 \end{bmatrix} + \begin{bmatrix} b_1 \\ b_2 \end{bmatrix} = \begin{bmatrix} \sum_{i=1}^{3}W_{i1}X_i+b_1 \\ \sum_{i=1}^{3}W_{i2}X_i+b_2 \end{bmatrix} \]

\(X\) 表示 \(\begin{bmatrix}X_1 \\ X_2 \\ X_3\end{bmatrix}\)\(B\) 表示 \(\begin{bmatrix}b_1 \\b_2\end{bmatrix}\)\(Z\) 表示 \(\begin{bmatrix}\sum_{i=1}^{3}W_{i1}X_i+b_1 \\\sum_{i=1}^{3}W_{i2}X_i+b_2\end{bmatrix}\)

我们一般用 \(W_{ij}\) 表示矩阵 \(W\)\(i\) 行第 \(j\) 列的元素。

但是(6)式中,\(W_{ij}\) 表示的意思是:前一层的第 \(i\) 个神经元与后一层的第 \(j\) 个神经元之间的权重。

为保持下标一致,我们用 \(W\) 表示 \(\begin{bmatrix}W_{11} & W_{12} \\ W_{21} & W_{22} \\ W_{31} & W_{32}\end{bmatrix}\)\(W\) 的转置 \(W^\top\) 表示 \(\begin{bmatrix}W_{11} & W_{21} & W_{31} \\W_{12} & W_{22} & W_{32}\end{bmatrix}\) \[ \Rightarrow Z=W^\top X+B \]

注意观察这些矩阵的行数与列数。

进而可以推广到含有 \(n^{(l)}\) 个神经元的第 \(l\) 层的前向计算:

符号 说明
\(l\) \(\mathbb{N}^* \cup \{0\}\)。层号,第 \(0\) 层表示输入层。
\(n^{(l)}\) \(\mathbb{N}^*\)。第 \(l\) 层的神经元数量。
\(Y^{(l-1)}\) \(\mathbb{R}^{n^{(l-1)}\times1}\)。第 \(l-1\) 层的输出矩阵,同时也是第 \(l\) 层的输入矩阵。
\(W^{(l)}\) \(\mathbb{R}^{n^{(l-1)}\times n^{(l)}}\)。第 \(l-1\) 层与第 \(l\) 层之间的权重矩阵
\(B^{(l)}\) \(\mathbb{R}^{n^{(l)}\times1}\)。第 \(l\) 层的偏置(Bios)矩阵
\(Act()\) 激活函数

\[ \begin{align} &Z^{(l)}=(W^{(l)})^\top Y^{(l-1)}+B^{(l)}\\ &Y^{(l)}=Act(Z^{(l)}) \end{align} \]

反向传播

我们来看一个简单的三层神经网络。其中,输入层只负责将输入数据传递给隐藏层。

符号 说明
\(X\) \(\mathbb{R}^{3\times1}\)。输入层的输入。
\(W^{(1)}\) \(\mathbb{R}^{3\times2}\)。输入层与隐藏层之间的权重矩阵。
\(B^{(1)}\) \(\mathbb{R}^{2\times1}\)。隐藏层的偏置矩阵。
\(Y^{(1)}\) \(\mathbb{R}^{2\times1}\)。隐藏层的输出。
\(W^{(2)}\) \(\mathbb{R}^{2\times1}\)。隐藏层与输出层之间的权重矩阵。
\(B^{(2)}\) \(\mathbb{R}^{1\times1}\)。输出层的偏置矩阵。
\(\widehat Y\) \(\mathbb{R}^{1\times1}\)。输出层的输出。
\(Y\) \(\mathbb{R}^{1\times1}\)。准确值或标签值。
\(Loss(y,\hat y)\) 损失函数。实际输出与标签值之间的“差距”。
\(Act(Z)\) 激活函数。
\(\lambda\) 学习率。

当我们前向计算出实际输出 \(\widehat Y\) 后,需要与标签值 \(Y\) 比较,看二者差距是多少。因此一次前向计算后能够得到一个“差距”:\(Loss(Y,\widehat Y)\)

我们的目标很明确,就是使得这个 \(Loss(Y,\widehat Y)\) 尽可能地小。根据前面的梯度下降法,问题就转变成了:\(Loss(Y,\widehat Y)\)的极小值点

考虑到神经网络中可调整的变量就是各个权重 \(W^{(l)}\) 和偏置 \(B^{(l)}\),我们先以 \(W^{(2)}_{11}\) 为例:

简单的例子

目标

改变 \(w^{(2)}_{11}\) 使得 \(Loss(Y,\widehat Y)\) 取得极小值。

方法

\[ w^{(2)'}_{11}=w^{(2)}_{11}-\lambda \nabla Loss(Y,\widehat Y) \]

计算

梯度的计算: \[ \nabla Loss(Y,\widehat Y)=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{w^{(2)}_{11}}} \] 求导的链式法则

  • \({Loss(Y,\widehat Y)}\)\(\widehat Y\) 的函数;

  • \(\widehat Y=Act(Z^{(2)})\)

  • \(Z^{(2)}=(W^{(2)})^\top Y^{(1)}+B^{(2)}=\sum_{i=1}^2w^{(2)}_{i1}y^{(1)}_{i1}+b^{(2)}_{11}\)

\[ \begin{align} \frac{\partial{Loss(Y,\widehat Y)}}{\partial{w^{(2)}_{11}}} =&\frac{\partial{Loss(Y,\widehat Y)}}{\partial{\widehat Y}}\cdot\frac{\partial{\widehat Y}}{\partial{Z^{(2)}}}\cdot\frac{\partial{Z^{(2)}}} {\partial{w^{(2)}_{11}}}\\ =&\frac{\partial{Loss(Y,\widehat Y)}}{\partial{\widehat Y}}\cdot\frac{\partial{Act(Z^{(2)})}}{\partial{Z^{(2)}}}\cdot \begin{bmatrix}y^{(1)}_{11}\end{bmatrix} \end{align} \] 因此: \[ w^{(2)'}_{11}=w^{(2)}_{11}-\lambda\cdot\frac{\partial{Loss(Y,\widehat Y)}}{\partial{\widehat Y}}\cdot\frac{\partial{Act(Z^{(2)})}}{\partial{Z^{(2)}}}\cdot \begin{bmatrix}y^{(1)}_{11}\end{bmatrix} \]

规定:

符号 说明
\(Loss(y,\widehat y)=\frac{1}{2}(y-\widehat y)^2\) 平方损失函数
\(Act(x)=\frac{1}{1+e^{-x}}\) Sigmoid 函数

则: \[ \frac{\partial{Loss(y,\widehat y)}}{\partial{\widehat y}}=-(y-\widehat y) \]

\[ \frac{\partial{Act(x)}}{\partial{x}}=Act(x)\cdot(1-Act(x)) \]

\[ \therefore w^{(2)'}_{11}=w^{(2)}_{11}+\lambda\cdot(Y-\widehat Y)\cdot Act(Z^{(2)})\cdot[1-Act(Z^{(2)})]\cdot\begin{bmatrix}y^{(1)}_{11}\end{bmatrix} \]

转化为矩阵形式: \[ W^{(2)'}=W^{(2)}+\lambda\cdot(Y-\widehat Y)\cdot Act(Z^{(2)})[1-Act(Z^{(2)})]\cdot Y^{(1)} \]

\(Y\in\mathbb{R}^{1\times1}\)

\(\widehat Y\in\mathbb{R}^{1\times1}\)

\(Z^{(2)}\in\mathbb{R}^{1\times1}\)

\(Act(Z^{(2)})=\begin{bmatrix}Act(z^{(2)}_{11})\end{bmatrix}\in\mathbb{R}^{1\times1}\)

\(Y^{(1)}\in\mathbb{R}^{2\times1}\)

通用的公式更为复杂,但这里并不涉及与之相关的矩阵求导。

又一个简单的例子

上一个例子中,我们得到了根据输出层的损失 \(Loss(Y,\widehat Y)\) 修改 \(W^{(2)}\) 的方法。

在这个例子中,我们将看到,输出层的损失 \(Loss(Y,\widehat Y)\) 是如何被输入层与隐藏层之间的权重 \(W^{(1)}\) 影响的。

需要注意的是: \[ Loss(Y,\widehat Y)\\ \widehat Y=Act(Z^{(2)})=Act((W^{(2)})^\top Y^{(1)}+B^{(2)})\\ Y^{(1)}=Act(Z^{(1)})=Act((W^{(1)})^\top X+B^{(1)}) \] 也就是说,\(Loss\) 并不是直接由 \(W^{(1)}\) 的计算结果得来的,它们中间还隔了一层。

计算如下: \[ \nabla Loss(Y,\widehat Y)=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{W^{(1)}}}=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{}Z^{(1)}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}} \]

\[ \because\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(1)}}}=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot\frac{\partial{Z^{(2)}}}{\partial{Y^{(1)}}}\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}} \]

\[ \therefore \begin{align} \nabla Loss(Y,\widehat Y)&=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{W^{(1)}}}\\ &=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot\frac{\partial{Z^{(2)}}}{\partial{Y^{(1)}}}\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}}\\ &=\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot (W^{(2)})^\top\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}} \end{align} \]

注意到:

\(\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\) 可以看成最终损失对于第 \(2\) 层(输出层)激活前的值的敏感度,而 \(\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot W^{(2)}\) 将敏感度按照权重分配到了第 \(2\) 层(隐藏层)的激活后输出 \(Y^{(1)}\) 上。

其中,第 \(2\) 层共有 \(n\) 个神经元,第 \(3\) 层(输出层)共有 \(q\) 个神经元。\(W^{(2)}\in\mathbb{R}^{n\times q},\;Loss\in\mathbb{R}^{q\times1},\;Z^{(2)}\in\mathbb{R}^{q\times1}\)。则(26)式按照权重将最终损失的敏感度分配到第 \(2\) 层。

针对我们选取的激活函数和损失函数,具体形式推导如下: \[ \begin{align} W^{(1)'} &=W^{(1)}-\lambda\nabla Loss(Y,\widehat Y)\\ &=W^{(1)}+\lambda\cdot\frac{\partial{Loss(Y,\widehat Y)}}{\partial{Z^{(2)}}}\cdot (W^{(2)})^\top\cdot\frac{\partial{Y^{(1)}}}{\partial{Z^{(1)}}}\cdot\frac{\partial{Z^{(1)}}}{\partial{W^{(1)}}} \end{align} \] 带入具体函数的偏导数,得到: \[ W^{(1)'}=W^{(1)}+\lambda\cdot(Y-\widehat Y)\cdot(W^{(2)})^\top\cdot Act(Z^{(1)})[1-Act(Z^{(1)})]\cdot X \] 这就是我们的三层神经网络中,输入层与隐藏层之间的权重矩阵的调整的公式。

上面的推导并没有很好地显示出反向传播的含义。更为专业地推导可以参见CMU等的公开课。

小小的总结

回顾一下我们对于激活函数和损失函数的设定:

符号 说明
\(Loss(y,\widehat y)=\frac{1}{2}(y-\widehat y)^2\) 平方损失函数
\(Act(x)=\frac{1}{1+e^{-x}}\) Sigmoid 函数

通过上面两个例子,我们得到了三层神经网络中,在梯度下降算法下,两个权重矩阵如何进行调整: \[ W^{(2)'}=W^{(2)}+\lambda\cdot(Y-\widehat Y)\cdot Act(Z^{(2)})[1-Act(Z^{(2)})]\cdot Y^{(1)} \]

\[ W^{(1)'}=W^{(1)}+\lambda\cdot\Big[(Y-\widehat Y)\cdot(W^{(2)})^\top\Big]\cdot Act(Z^{(1)})[1-Act(Z^{(1)})]\cdot X \]

接下来便是代码实现了。

代码实现

上一节中,我们定义了神经网络类,但是还没有写它的方法。下面就添加进训练及查询的方法。需要注意的是,数学公式很简洁(虽然笔者的很丑陋。。。),实现起来很复杂。因为你不但需要考虑 Python 中的数据类型,还要考虑各种可能对神经网络的效果有很大影响的细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 定义神经网络类
class neuralNetwork:

# 初始化神经网络
def __init__(self, InputNodes, HiddenNodes, OutputNodes, LearningRate):
# 设置输入层、隐藏层、输出层感知机数量
self.in_nodes = InputNodes
self.hid_nodes = HiddenNodes
self.out_nodes = OutputNodes
# 学习率
self.LR = LearningRate
# 权重矩阵W_ih, W_ho表示Input到Hidden,Hidden到Output的权重矩阵
# 形式如下:
# w_11 w_12
# w_21 w_22
# w_ij表示上一层第i结点到下一层第j结点的权重值
self.W_ih = np.random.normal(0.0, pow(self.hid_nodes, -0.5), (self.in_nodes, self.hid_nodes))
self.W_ho = np.random.normal(0.0, pow(self.out_nodes, -0.5), (self.hid_nodes, self.out_nodes))

# 激活函数为sigmoid函数
self.activate_func = lambda x:scipy.special.expit(x)
self.inv_activate_func = lambda x: scipy.special.logit(x)


# 训练神经网络,输入+目标输出
def train(self, inputs_list, targets_list):
# 将列表输入转换为矩阵
inputs = np.array(inputs_list, ndmin = 2).T
targets = np.array(targets_list, ndmin = 2).T
# 计算隐藏层输入: W_ih·inputs
hidden_inputs = np.dot(self.W_ih.T, inputs)
# 计算隐藏层输出
hidden_outputs = self.activate_func(hidden_inputs)

# 计算输出层输入: W_ho·hidden_outputs
final_inputs = np.dot(self.W_ho.T, hidden_outputs)
# 计算输出层输出
final_outputs = self.activate_func(final_inputs)

# 输出层结点误差errors,误差函数为误差平方和sum(errors^2)
output_errors = targets - final_outputs
# 隐藏层节点误差,通过权重分配
hidden_errors = np.dot(self.W_ho, output_errors)

# 更新隐藏层与输出层之间的权重
self.W_ho += (self.LR * np.dot((output_errors * final_outputs * (1.0 - final_outputs)), hidden_outputs.T)).T

# print((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)).shape)
# 更新输出层与隐藏层之间的权重
self.W_ih += (self.LR * np.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), inputs.T)).T


# 查询函数,训练完后进行验证
def query(self, inputs_list):
# 将列表输入转为矩阵
inputs = np.array(inputs_list, ndmin = 2).T

# 计算隐藏层输入
hidden_inputs = np.dot(self.W_ih.T, inputs)
# 计算隐藏层输出
hidden_outputs = self.activate_func(hidden_inputs)

# 计算输出层输入
final_inputs = np.dot(self.W_ho.T, hidden_outputs)
# 计算输出层输出
final_outputs = self.activate_func(final_inputs)

return final_outputs

后记

不得不说,自己推导真累。。。

前后纠结了两天,补了补矩阵运算😱(我已经忘得差不多了),看了看还没学的矩阵求导(一知半解)