0%

深度学习算法(一):simple NN(前馈神经网络的正反向推导)

本文的公式不存在次方的说法,所以看见上标,不要想成是次方。 对于权重的表示问题,请看博客,但是由于是以前的学习笔记,不保证完全正确。 如果想了解为什么梯度下降要对w和b求导,可以看这篇建议边看边写,否则思维跟不上。

前言

参考文章 以如下神经网络架构为例。参考文章中使用了一个2 2 2的神经网络架构,但是现实中神经网络架构不会这么整整齐齐。所以还是使用了略复杂的架构,此外原文中未对bias(偏差)更新。另外原文也没有实现向量化后的计算。虽然在后面的代码写了,但是由于代码太长了,有一种代码我给出来了,你们自己去看的感觉。说实话没多少注释,都没看的欲望(╬ ̄皿 ̄)。然后她所使用的符号让我不太习惯,因为看吴恩达以及李宏毅老师使用的符号都是\(w^l_{ji}\ a^l_i\)等等,所以自己重新推导一遍,并且使用了数学公式,而不是截图,更好看一点。 带参数的前馈神经网络模版 解释一下最下面的神经元,这个神经元初始化为1,也就是意味着1 * b = b。输入值为1,一个偏差乘1还是偏差本身。

关于函数选用

本文所有激活函数选择sigmoid函数,代价函数选择binary_crossentropy。

一些约定

本文所有的输入值,激活值,输出值都是列向量

初始化数据以及正向传播

初始化数据

此处初始化各层的权重值,偏差。由于是演示,所以顺便把输入层也初始化了。 设 \[ \begin{cases} x_1 = a^0_1 = 0.55, x_2 = a^0_2 = 0.72\\ y_1 = 0.60, y_2 = 0.54\\ \end{cases}\\ \begin{cases} w^1_{11}=0.4236548, w^1_{12}=0.64589411\quad|\quad w^1_{21}=0.43758721, w^1_{22}=0.891773\quad|\quad w^1_{31}=0.96366276, w^1_{32}=0.38344152\\ b^1_1=0.79172504, b^1_2=0.52889492, b^1_3=0.56804456\\ w^2_{11}=0.92559664, w^2_{12}=0.07103606, w^2_{13}=0.0871293\quad|\quad w^2_{21}=0.0202184, w^2_{22}=0.83261985, w^2_{23}=0.77815675\\ b^2_1=0.87001215, b^2_2=0.97861834\\ \end{cases} \] 不用多看,反正也用不到几次。。。

正向传播

对于正向传播,应该是很熟悉了,所以我直接一次写完,不做过多解释。

输入层到隐藏层

\[ z^1_1 = w^1_{11} * a^0_1 + w^1_{12} * a^0_2 + 1 * b^1_1\\ z^1_2 = w^1_{21} * a^0_1 + w^1_{22} * a^0_2 + 1 * b^1_2\\ z^1_3 = w^1_{31} * a^0_1 + w^1_{32} * a^0_2 + 1 * b^1_3\\ \] 带入sigmoid函数中,以下开始省略bias乘的1: \[ a^1_1 = \sigma{(z^1_1)}\\ a^1_2 = \sigma{(z^1_2)}\\ a^1_3 = \sigma{(z^1_3)}\\ \]

隐藏层到输出层

\[ z^2_1 = w^2_{11} * a^1_1 + w^2_{12} * a^1_2 + w^2_{13} * a^1_3 + b^2_1\\ z^2_2 = w^2_{21} * a^1_1 + w^2_{22} * a^1_2 + w^2_{13} * a^1_3 + b^2_2\\ \] 带入sigmoid函数中: \[ a^2_1 = \sigma{(z^2_1)}\\ a^2_2 = \sigma{(z^2_2)}\\ \]

计算代价

以字母J记为代价函数的名称,最后一个表达式为最简版: \[ \begin{align} J & = -[(y_1 * \log(a^2_1) + (1 - y_1) * \log(1 - a^2_1) + (y_2 * \log(a^2_2) + (1 - y_2) * \log(1 - a^2_2)]\\ J & = -\Sigma^2_{i = 1}{[(y_i * \log(a^2_i) + (1 - y_i) * \log(1 - a^2_i)]}\\ J & = -\Sigma{[(y * \log(a^2) + (1 - y) * \log(1 - a^2)]}\\ \end{align} \]

向量化

上述的表达式全部是一个一个列出来的,如果使用向量来表示乘积那就方便很多。可以看到下面只用了五行就写完了上面正向传播的所有步骤。

如果无法理解这一步那就是不会线性代数的问题,线性代数不在此文的介绍范围之内。

\[ \begin{align} z^1 & = w^1 * a^0 + b^1 & \text{输入层到隐藏层}\\ a^1 & = \sigma{(z^1)} & \text{带入隐藏层的激活函数}\\ z^2 & = w^2 * a^1 + b^2 & \text{隐藏层到输出层}\\ a^2 & = \sigma{(z^2)} & \text{带入输出层的激活函数}\\ J & = -\Sigma{[(y * \log(a^2) + (1 - y) * \log(1 - a^2)]} & \text{计算代价}\\ \end{align} \]

上述表达式代码实现

最后几节有神经网络numpy实现的全部代码,可以直接跳过本节看下一节,这里的代码只是给出一个直观的理解,可以自己运行看看。 受到keras以及万物皆对象的启发,首先建立一个神经元对象

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
class SimpleNN:
def __init__(self, units, activation='sigmoid'):
self.units = units
self.activation = activation
self.hyperparameters = dict()
# W, b, A_prev的导数
self.grads = dict()

def _init_hyperparameters(self, shape):
"""
初始化超参数,在神经网络中权重值不能初始化为0,偏差可以
:param shape: 神经元的形状,(units, input_shape)
:return: hyperparameters,该神经元的超参数。可以通过hyperparameters['W']、hyperparameters['b']取值
"""
# 由于是演示,所以使用了随机初始化
W = np.random.rand(*shape)
b = np.random.rand(shape[0], 1)
return W, b

def build(self, input_shape):
"""
构造神经元,目前只执行初始化超参数的步骤
:param input_shape:
:return:
"""
W, b = self._init_hyperparameters(shape=(self.units, input_shape))
self.hyperparameters['W'] = W
self.hyperparameters['b'] = b
为方便起见,将大部分的函数都放入Model中,下面给出所有的代码
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import numpy as np
from simple_neural_network.activation_function import *
from simple_neural_network.cost_function import *
np.random.seed(0)


class SimpleNN:
def __init__(self, units, activation='sigmoid'):
self.units = units
self.activation = activation

def _init_hyperparameters(self, shape):
"""
初始化超参数,在神经网络中权重值不能初始化为0,偏差可以
:param shape: 神经元的形状,(units, input_shape)
:return: hyperparameters,该神经元的超参数。可以通过hyperparameters['W']、hyperparameters['b']取值
"""
# 用于是演示,所以使用了随机初始化
hyperparameters = {
'W': np.random.rand(*shape),
'b': np.random.rand(shape[0], 1)
}
return hyperparameters

def build(self, input_shape):
"""
构造神经元,目前只执行初始化超参数的步骤
:param input_shape:
:return:
"""
self.hyperparameters = self._init_hyperparameters(shape=(self.units, input_shape))


class Model:
def __init__(self):
# 全部的神经元,并且根据神经元数量计算神经网络架构的层数,在计算时需要减1因为输入层不算入神经网络层数
self.neurons = list()
# 按顺序缓存A, (Z, W, b),由于输入层不需要任何缓存,所以放入None填充此位置。方便根据索引取值
self.value_caches = [None]
# 代价函数
self.cost = ''

def add(self, neuron):
"""
在模型中添加神经元
:param neuron:
:return:
"""
self.neurons.append(neuron)

def linear_forward(self, A_prev, W, b):
"""
正向传播,线性运算:Z = W * A + b
:param A_prev: 前一层的激活值
:param W: 权重值
:param b: 偏差
:return:
Z: 运算结果
"""
Z = np.dot(W, A_prev) + b
cache = A_prev, (Z, W, b)
self.value_caches.append(cache)
return Z

def nonlinear_forward(self, A_prev, W, b, activation):
"""
进入激活函数进行非线性计算
:param A_prev:
:param W:
:param b:
:param activation:
:return:
"""
Z = self.linear_forward(A_prev, W, b)
if activation == 'sigmoid':
return sigmoid(Z)
elif activation == 'relu':
return relu(Z)

def deep_forward(self, X):
"""
:param X: 输入值
:return:
A: 最后一层的运算结果,也就是输出层的激活值
"""
# 计算神经网络层数,减1是为了去掉输入层,众所周知输入层不需要进行计算
L = len(self.neurons) - 1
A = X
# 循环整个神经网络,进行正向传播,从1开始,因为索引0是输入层
for l in range(1, L + 1):
# 根据索引获取神经元实例
neuron = self.neurons[l]
A_prev = A
W = neuron.hyperparameters['W']
b = neuron.hyperparameters['b']
A = self.nonlinear_forward(A_prev, W, b, neuron.activation)
return A

def compile(self):
pass

def fit(self, X, Y, epochs=1):
# 将输入层的神经元添加进去
self.neurons.insert(0, SimpleNN(len(X)))
# 初始化神经元的超参数
for i, n in enumerate(self.neurons[1:]):
input_shape = self.neurons[i].units
n.build(input_shape)

# 开始反向传播
AL = self.deep_forward(X)
激活函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np


def sigmoid(z):
return 1 / (1 + np.exp(-z))


def relu(z):
return z * (z > 0)


def relu_backward(dA, Z):
dZ = np.array(dA, copy=True) # just converting dz to a correct object.

dZ[Z <= 0] = 0
return dZ


def sigmoid_backward(dA, Z):
s = sigmoid(Z)
return dA * s * (1 - s)
测试一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 输入值的大小
input_size = 2
# 输出值的大小
output_size = 2
# 方便书写,截断小数
X0 = np.round(np.random.rand(input_size, 1), 2)
Y0 = np.round(np.random.rand(output_size, 1),2)

# 该模型为2 3 2架构
model = Model()
model.add(SimpleNN(3))
model.add(SimpleNN(output_size))
model.compile()
model.fit(X0, Y0)
print(model.value_caches[0])

反向传播

说是说反向传播,实际上整个流程就是在链式求导。如果把这点想通了,整个神经网络的难点就只在向量化上了。一定要理解为什么整个流程只是在做链式求导的问题,在这里我并不是随便一提。

为了便于查找,把之前的图再放这。 带参数的前馈神经网络模版

首先更新输出层的权重值(W)以及偏差值(b)

梯度下降公式大家应该都知道:\(W = W - \alpha * grad\)。其中的grad实际上就是W的导数,参考。 可以对照上图观察,输出层的权重值分别为: \[ \begin{pmatrix} w^2_{11}&w^2_{12}&w^2_{13}\\ w^2_{21}&w^2_{22}&w^2_{23}\\ \end{pmatrix} \] 所以我们需要分别求: \[ \begin{pmatrix} \frac{\partial J}{\partial w^2_{11}} & \frac{\partial J}{\partial w^2_{12}} & \frac{\partial J}{\partial w^2_{13}}\\ \frac{\partial J}{\partial w^2_{21}} & \frac{\partial J}{\partial w^2_{22}} & \frac{\partial J}{\partial w^2_{23}}\\ \end{pmatrix} \tag{3.1.1}\label{3.1.1} \]

求矩阵中第一个w的导数并更新w

先求\(\frac{\partial J}{w^2_{11}}\),我们知道这个神经网络的代价的表达式是 \[ J = -[(y_1 * \log(a^2_1) + (1 - y_1) * \log(1 - a^2_1) + (y_2 * \log(a^2_2) + (1 - y_2) * \log(1 - a^2_2)] \] 为了方便对照我将隐藏层到输出层的正向传播的步骤也写在下面: \[ \begin{align} z^2_1 & = w^2_{11} * a^1_1 + w^2_{12} * a^1_2 + w^2_{13} * a^1_3 + b^2_1\\ z^2_2 & = w^2_{21} * a^1_1 + w^2_{22} * a^1_2 + w^2_{13} * a^1_3 + b^2_2\\ a^2_1 & = \sigma{(z^2_1)} = \frac{1}{1 - e^{-z^2_1}}\\ a^2_2 & = \sigma{(z^2_2)} = \frac{1}{1 - e^{-z^2_2}}\\ \end{align} \] 根据链式求导法则得: \[ \frac{\partial J}{\partial w^2_{11}} = \frac{\partial J}{\partial a^2_1} * \frac{\partial a^2_1}{\partial z^2_1} * \frac{\partial z^2_1}{\partial w^2_{11}} \] 我们将其拆解,一步一步地求: \[ \begin{align} \frac{\partial J}{\partial a^2_1} & = -[(\frac{y_1}{a^2_1} + \frac{1 - y_1}{a^2_1 - 1}) + 0] & \text{首先对a求导,此步如果你不会微积分会有疑惑} \tag{3.1.2}\label{3.1.2}\\ \frac{\partial a^2_1}{\partial z^2_1} & = (a^2_1) * (1 - a^2_1) & \text{这是对sigmoid函数的求导,百度一下求导过程}\\ \frac{\partial J}{\partial z^2_1} & = \frac{\partial J}{\partial a^2_1} * \frac{\partial a^2_1}{\partial z^2_1} & \text{其次对z求导}\\ & = \frac{\partial J}{\partial a^2_1} * (a^2_1) * (1 - a^2_1) \tag{3.1.3}\label{3.1.3}\\ \frac{\partial z^2_1}{\partial w^2_{11}} & = a^1_1\\ \frac{\partial J}{\partial w^2_{11}} & = \frac{\partial J}{\partial z^2_1} * \frac{\partial z^2_1}{\partial w^2_{11}} & \text{最后对w求导} \tag{3.1.4}\label{3.1.4}\\ & = \frac{\partial J}{\partial z^2_1} * a^1_1\\ \frac{\partial J}{\partial w^2_{11}} & = -[(\frac{y_1}{a^2_1} + \frac{1 - y_1}{a^2_1 - 1}) + 0] * (a^2_1) * (1 - a^2_1) * a^1_1 & \text{整合在一起}\\ \end{align} \] 其中\([(\frac{y_1}{a^2_1} + \frac{1 - y_1}{a^2_1 - 1}) + 0] * (a^2_1) * (1 - a^2_1)\)实际上是可以化简的,化简为\(a^2_1 - y_1\),同时去掉了负号,所以 \[ \frac{\partial J}{\partial w^2_{11}} = (a^2_1 - y_1) * a^1_1 \] 我们将数值带入其中,之前的正向传播已经得到了所有激活值。 \(\frac{\partial J}{\partial w^2_{11}} = (0.85220348 - 0.60) * 0.81604509 = 0.20580941153491322\)\(w^2_{11}\)更新, \[ w^2_{11} = w^2_{11} - \alpha * \frac{\partial J}{\partial w^2_{11}} \] 学习速率\(\alpha\)选1,经过简单的运算,\(w^2_{11} = 0.92559664 - 1 * 0.20580941153491322 = 0.7197872284650868\)

如果细心点就会发现,\(\frac{\partial J}{\partial w^2_{11}}\)其实就等于这层的z的导数乘上前一层的激活值a。如果没发现也没关系,下面向量化这节会做一个总结。

求所有w的导数

同理可以求出所有的导数 \[ \begin{pmatrix} \frac{\partial J}{\partial w^2_{11}} & \frac{\partial J}{\partial w^2_{12}} & \frac{\partial J}{\partial w^2_{13}}\\ \frac{\partial J}{\partial w^2_{21}} & \frac{\partial J}{\partial w^2_{22}} & \frac{\partial J}{\partial w^2_{23}}\\ \end{pmatrix} \tag{\ref{3.1.1}} \]

向量化

上面只求了一个w的导数,虽然其他的w的求导都是类似操作,但是真要算起来,对于自己没去算过的人,可能花一天都没有办法将其用向量化表示。 求导是十分简单的,但是向量化可能会有点问题。问题的主要来源是想偷懒。对于这种问题,最好得到解决办法是暴力破解,即求出所有的w的导数,然后再将其向量化。

首先观察上述公式\(\ref{3.1.4}\)\[ \frac{\partial J}{\partial w^2_{11}} = \frac{\partial J}{\partial z^2_1} * \frac{\partial z^2_1}{\partial w^2_{11}} \] 它由两部分组成,一个是\(\frac{\partial J}{\partial z^2_1}\),第二部分是\(\frac{\partial z^2_1}{\partial w^2_{11}}\),如果你自己求过导就会发现其实\(\frac{\partial z^2_1}{\partial w^2_{11}} = a^1_1\)。为了方便你们观察,我列出所有式子: \[ \begin{align} \frac{\partial J}{\partial w^2_{11}} = \frac{\partial J}{\partial z^2_1} * a^1_1 \quad \frac{\partial J}{\partial w^2_{12}} = \frac{\partial J}{\partial z^2_1} * a^1_2 \quad \frac{\partial J}{\partial w^2_{13}} = \frac{\partial J}{\partial z^2_1} * a^1_3\\ \frac{\partial J}{\partial w^2_{21}} = \frac{\partial J}{\partial z^2_2} * a^1_1 \quad \frac{\partial J}{\partial w^2_{22}} = \frac{\partial J}{\partial z^2_2} * a^1_2 \quad \frac{\partial J}{\partial w^2_{23}} = \frac{\partial J}{\partial z^2_2} * a^1_3\\ \end{align} \] 可能到这你有点烦躁了,因为表达式实在太多了。没关系,下方蓝色的note会给出总结,直接一步求解完毕。 有没有发现,里面有一半是重复的元素?我们可以将它们组成向量得到: \[ \eqref{3.1.1} \begin{pmatrix} \frac{\partial J}{\partial w^2_{11}} & \frac{\partial J}{\partial w^2_{12}} & \frac{\partial J}{\partial w^2_{13}}\\ \frac{\partial J}{\partial w^2_{21}} & \frac{\partial J}{\partial w^2_{22}} & \frac{\partial J}{\partial w^2_{23}}\\ \end{pmatrix} = \begin{pmatrix} \frac{\partial J}{\partial z^2_1}\\ \frac{\partial J}{\partial z^2_2}\\ \end{pmatrix} * \begin{pmatrix} a^1_1 & a^1_2 & a^1_3 \end{pmatrix} \tag{3.1.5} \] 公式3.1.5和上面那六个表达式实际上计算的东西是一样的。进一步缩写为 \[ \frac{\partial J}{\partial w^2} = \frac{\partial J}{\partial z^2} * (a^1)^T \tag{3.1.6} \] 这里加了一个T代表转置,实际上我们所有的输入值,激活值,输出值都是列向量。

总结一下,在这里\(\frac{\partial J}{\partial w}\)的步骤为:求权重值所在层的z的导数\(\frac{\partial J}{\partial z}\)再乘上前一层的激活值。这是对一个w求导所做的运算,而对一整个W矩阵求导那就是公式3.1.6的那个向量化操作。但是观察公式3.1.6发现,其实求一个w和求一个W矩阵并无区别,无非是将数字相乘改为向量(矩阵)相乘。 另外,其实这对神经网络中每一层的操作都是一样。如果不信可以自己算一下。所以以后理解的时候,可以用这种方式理解,加快理解速度。

更新偏差

偏差比权重简单很多。 \[ \begin{align} \frac{\partial J}{\partial a^2_1} & = -[(\frac{y_1}{a^2_1} + \frac{1 - y_1}{a^2_1 - 1}) + 0] \tag{\ref{3.1.2}}\\ \frac{\partial J}{\partial z^2_1} & = \frac{\partial J}{\partial a^2_1} * \frac{\partial a^2_1}{\partial z^2_1}\\ & = \frac{\partial J}{\partial a^2_1} * (a^2_1) * (1 - a^2_1) \tag{\ref{3.1.3}}\\ \frac{\partial J}{\partial b^2_1} & = \frac{\partial J}{\partial z^2_1} * \frac{\partial z^2_1}{\partial b^2_1} \\ & = \frac{\partial J}{\partial z^2_1}\\ \frac{\partial J}{\partial b^2_1} & = -[(\frac{y_1}{a^2_1} + \frac{1 - y_1}{a^2_1 - 1}) + 0] * (a^2_1) * (1 - a^2_1)\\ \end{align} \] 可以看到 \[ \frac{\partial J}{\partial b^2_1} = \frac{\partial J}{\partial z^2_1} \] 所以偏差的向量化比较简单: \[ \begin{pmatrix} \frac{\partial J}{\partial b^2_1}\\ \frac{\partial J}{\partial b^2_2}\\ \end{pmatrix} = \begin{pmatrix} \frac{\partial J}{\partial z^2_1}\\ \frac{\partial J}{\partial z^2_2}\\ \end{pmatrix} \] ### 整理 整理一下上一波的求导过程。目标是求得\(\frac{\partial J}{w^2_{11}}\),但是上面我并没有一步求导到底,相反我将每一步都写出来了,这是有原因的。因为\(\frac{\partial J}{\partial z^2_1}\)会在前一层对w求导时使用,所以在代码上当然需要保存副本。而\(\frac{\partial J}{w^2_{11}}\)已经在这次的反向传播中使用过了,它的价值也算是用完了。 在首先更新输出层的权重值(W)以及偏差值(b)略有瑕疵的步骤(也就是上述所有步骤)是:

  1. 求出\(\frac{\partial J}{\partial a^2}\)
  2. 进一步求出\(\frac{\partial J}{\partial z^2}\)
  3. 分别求出\(\frac{\partial J}{\partial w^2}\ \frac{\partial J}{\partial b^2}\)

根据上面三步,我们可以观察出,如果需要求出一层的\(\frac{\partial J}{\partial w^2}\ \frac{\partial J}{\partial b^2}\)步骤3),需要求出同一层\(\frac{\partial J}{\partial a^2}\ \frac{\partial J}{\partial z^2}\)步骤1和2)。所以如果我们需要求出前一层\(\frac{\partial J}{\partial w^1}\ \frac{\partial J}{\partial b^1}\),必须先求出前一层的a的导数\(\frac{\partial J}{\partial a^1}\ \frac{\partial J}{\partial z^1}\),而由于公式\(z^2_1 = w^2_{11} * a^1_1 + w^2_{12} * a^1_2 + w^2_{13} * a^1_3 + b^2_1\),可以观察到上述步骤2对z求导之后其实拥有三个选项: 1. 求w的导数(步骤3) 2. 求b的导数(步骤3) 3. 求上一层a的导数

所以正确的步骤是: 1. 求出\(\frac{\partial J}{\partial a^2}\)不变) 2. 进一步求出\(\frac{\partial J}{\partial z^2}\)不变) 3. 分别求出\(\frac{\partial J}{\partial w^2}\ \frac{\partial J}{\partial b^2}\)不变) 4. 最后求出前一层的\(\frac{\partial J}{\partial a^1}\),准备下一步的计算。

也就是说,我们在一层中进行求导,需要分别求4个参数的导数,即当前层的a,w,b以及前一层的a的导数。

更新隐藏层的权重值以及偏差值

由于上述步骤太多,来回滑动网页略繁琐,我再次把图放出来,以供参考。 带参数的前馈神经网络模版 隐藏层的更新与输出层略微不同,由于看公式不太形象,可以看上面的图。观察发现,隐藏层的某一个神经元链接着输出层的所有神经元。所以隐藏层的神经元的误差其实来源于与它相连接的输出层的神经元。 根据链式求导法则,我们知道:一个函数对一个变量求导,如果有多条路径可以到达该变量,那么就需要对每条路径都求导,最后将结果相加。转换成数学公式就跟下面公式3.2.1的求导过程一样。

对第一个w求导

我们按照上一节《整理》的四个步骤来做,先求出a的导数: \[ \begin{align} \frac{\partial J}{\partial a^1_1} & = \frac{\partial J}{\partial a^2_1} * \frac{\partial a^2_1}{\partial a^1_1} + \frac{\partial J}{\partial a^2_2} * \frac{\partial a^2_2}{\partial a^1_1} & \text{输出层两个神经元均要求导再相加}\\ & = \frac{\partial J}{\partial z^2_1} * \frac{\partial z^2_1}{\partial a^1_1} + \frac{\partial J}{\partial z^2_2} * \frac{\partial z^2_1}{\partial a^1_1} & \text{之前求过z的导数,为了方便书写用它替换}\\ & = \frac{\partial J}{\partial z^2_1} * w^2_{11} + \frac{\partial J}{\partial z^2_2} * w^2_{21} \tag{3.2.1}\label{3.2.1}\\ \end{align} \]

我们可以观察到隐藏层的a的导数\(\frac{\partial J}{\partial a^1_1}\)实际上就是输出层的z的导数\(\frac{\partial J}{\partial z^2_1}\)乘上与之相连的输出层的神经元的w。 一般化之后就是:除了输出层,其他所有层的a的导数都是后一层z的导数乘上后一层的w。因为输出层的a的导数是通过代价函数求的。

所以下一步就是求z的导数: \[ \begin{align} \frac{\partial J}{\partial z^1_1} & = \frac{\partial J}{\partial z^2_1} * \frac{\partial z^2_1}{\partial a^1_1} * \frac{\partial a^1_1}{\partial z^1_1} + \frac{\partial J}{\partial z^2_2} * \frac{\partial z^2_1}{\partial a^1_1} * \frac{\partial a^1_1}{\partial z^1_1}\\ & = \frac{\partial J}{\partial z^2_1} * w^2_{11} * \frac{\partial a^1_1}{\partial z^1_1} + \frac{\partial J}{\partial z^2_2} * w^2_{21} * \frac{\partial a^1_1}{\partial z^1_1}\\ & = \frac{\partial J}{\partial a^1_1} * \frac{\partial a^1_1}{\partial z^1_1} & \text{这里a的导数参考}\ref{3.2.1}\\ \frac{\partial a^1_1}{\partial z^1_1} & = a^1_1 * (1 - a^1_1) & \text{对sigmoid函数求导,前面已经说过了}\\ \end{align} \] 最后求出w的导数 \[ \begin{align} \frac{\partial J}{\partial w^1_{11}} & = \frac{\partial J}{\partial a^1_1} * \frac{\partial a^1_1}{\partial z^1_1} * \frac{\partial z^1_1}{\partial w^1_{11}}\\ & = \frac{\partial J}{\partial z^1_1} * \frac{\partial z^1_1}{\partial w^1_{11}}\\ & = \frac{\partial J}{\partial z^1_1} * a^0_1 \end{align} \]

向量化

你肯定已经想把它向量化了。先列出所有的表达式。 \[ \frac{\partial J}{\partial w^1_{11}} = \frac{\partial J}{\partial z^1_1} * a^0_1\\ \frac{\partial J}{\partial w^1_{12}} = \frac{\partial J}{\partial z^1_1} * a^0_2\\ \frac{\partial J}{\partial w^1_{21}} = \frac{\partial J}{\partial z^1_2} * a^0_1\\ \frac{\partial J}{\partial w^1_{22}} = \frac{\partial J}{\partial z^1_2} * a^0_2\\ \frac{\partial J}{\partial w^1_{31}} = \frac{\partial J}{\partial z^1_3} * a^0_1\\ \frac{\partial J}{\partial w^1_{32}} = \frac{\partial J}{\partial z^1_3} * a^0_2\\ \] 可以发现这其实跟上面的向量化步骤一模一样: \[ \begin{align} \begin{pmatrix} \frac{\partial J}{\partial w^1_{11}} & \frac{\partial J}{\partial w^1_{12}}\\ \frac{\partial J}{\partial w^1_{21}} & \frac{\partial J}{\partial w^1_{22}}\\ \frac{\partial J}{\partial w^1_{31}} & \frac{\partial J}{\partial w^1_{32}}\\ \end{pmatrix} & = \begin{pmatrix} \frac{\partial J}{\partial z^1_1}\\ \frac{\partial J}{\partial z^1_2}\\ \frac{\partial J}{\partial z^1_3}\\ \end{pmatrix} * \begin{pmatrix} a^0_1 & a^0_2 \end{pmatrix} \\ \frac{\partial J}{\partial w^1} & = \frac{\partial J}{\partial z^1} * (a^0)^T \end{align} \]

对偏差求导

这一步更是简单,直接给结果了。 \[ \begin{align} \begin{pmatrix} \frac{\partial J}{\partial b^1_1}\\ \frac{\partial J}{\partial b^1_2}\\ \frac{\partial J}{\partial b^1_3}\\ \end{pmatrix} & = \begin{pmatrix} \frac{\partial J}{\partial z^1_1}\\ \frac{\partial J}{\partial z^1_2}\\ \frac{\partial J}{\partial z^1_3}\\ \end{pmatrix}\\ \frac{\partial J}{\partial b^1} & = \frac{\partial J}{\partial z^1} \end{align} \]

注意点

上述步骤看起来没什么问题,但是在实际编程中会有很大问题。在向量化的时候,我直接使用了\(\frac{\partial J}{\partial z^1}\),但是问题就是\(\frac{\partial J}{\partial z^1}\)的向量化我直接跳过了。要向量化\(\frac{\partial J}{\partial z^1}\),实际上得先向量化\(\frac{\partial J}{\partial a^1}\)。观察表达式\(\ref{3.2.1}\),先给出所有的式子: \[ \frac{\partial J}{\partial a^1_1} = \frac{\partial J}{\partial z^2_1} * w^2_{11} + \frac{\partial J}{\partial z^2_2} * w^2_{21}\\ \frac{\partial J}{\partial a^1_2} = \frac{\partial J}{\partial z^2_1} * w^2_{12} + \frac{\partial J}{\partial z^2_2} * w^2_{22}\\ \frac{\partial J}{\partial a^1_3} = \frac{\partial J}{\partial z^2_1} * w^2_{13} + \frac{\partial J}{\partial z^2_2} * w^2_{23}\\ \]

向量化

\[ \begin{pmatrix} \frac{\partial J}{\partial a^1_1}\\ \frac{\partial J}{\partial a^1_2}\\ \frac{\partial J}{\partial a^1_3}\\ \end{pmatrix} = \begin{pmatrix} w^2_{11} & w^2_{12} & w^2_{13}\\ w^2_{21} & w^2_{22} & w^2_{23}\\ \end{pmatrix}^T * \begin{pmatrix} \frac{\partial J}{\partial z^2_1} & \frac{\partial J}{\partial z^2_2} \end{pmatrix} \]

总结

在反向传播中,每一层都只需要重复如下几步:

  1. 求出\(\frac{\partial J}{\partial a^2}\)
  2. 进一步求出\(\frac{\partial J}{\partial z^2}\)
  3. 分别求出\(\frac{\partial J}{\partial w^2}\ \frac{\partial J}{\partial b^2}\)
  4. 最后求出前一层的\(\frac{\partial J}{\partial a^1}\),准备下一步的计算。此步骤的向量化操作在注意点

代码

使用numpy实现一个简单的神经网络

注意事项

在做反向传播代码时,验算了很多遍,发现公式推导没有问题,但是梯度却一直在上升,心态都炸了。 最后发现,用了大半年的crossentropy在最前面居然要加上一个“-”号。以前由于是偷懒,在求导的时候一般不加负号,在求完导之后再补上。然后由于写习惯了,导致我忘记crossentropy居然是有负号的。

撒花

在第二节有一步是初始化数据,但是全篇都没用几个地方用到。是因为数据测试起来太麻烦了,我需要在代码里一步一步分析神经网络的计算过程,从而获得数据。以后再补充吧。