误差反向传播(Back-propagation, BP)算法的出现是神经网络发展的重大突破,也是现在众多深度学习训练方法的基础。该方法会计算神经网络中损失函数对各参数的梯度,配合优化方法更新参数,降低损失函数。
BP本来只指损失函数对参数的梯度通过网络反向流动的过程,但现在也常被理解成神经网络整个的训练方法,由误差传播、参数更新两个环节循环迭代组成。
本文将以最基础的全连接深度前馈网络为例,详细展示Back-propagation的全过程,并以Numpy进行实现。
通常我们以神经元来计量“层”,但本文将权重抽象为“层”,个人认为这样更有助于反向传播的理解和代码的编写。如上图所示的网络就被抽象为两个中间层、一个输出层的结构。
简而言之,神经网络的训练过程中,前向传播和反向传播交替进行,如下图所示:前向传播通过训练数据和权重参数计算输出结果;反向传播通过导数链式法则计算损失函数对各参数的梯度,并根据梯度进行参数的更新,这一点是重点,会在后文详叙。
图2 前向传播&反向传播
- 前向传播
每层中前向传播的过程如下所示,很简单的矩阵运算。我们将权重作为层,中间层和输出层均可用Layer类来表示,只是对应的激活函数不同。如图2所示,每一层的输入和输出都是 ℎ ,且前一层的输出是后一层的输入。
# * 表示element-wise乘积,· 表示矩阵乘积
class Layer:
'''中间层类'''
self.W # (input_dim, output_dim)
self.b # (1, output_dim)
self.activate(a) = sigmoid(a)/tanh(a)/ReLU(a)/Softmax(a)
def forward(self, input_data): # input_data: (1, input_dim)
'''单个样本的前向传播'''
input_data · self.W + self.b = a # a: (1, output_dim)
h = self.activate(a) # h: (1, output_dim)
return h
2. 反向传播
损失对参数梯度的反向传播可以被这样直观解释:由A到传播B,即由 ∂L/∂A 得到 ∂L/∂B ,由导数链式法则 ∂L/∂B=(∂L/∂A)⋅(∂A/∂B) 实现。所以神经网络的BP就是通过链式法则求出 L对所有参数梯度的过程。
如上图示例,输入 x,经过网络的参数 w,b ,得到一系列中间结果 a,ℎ 。 a 表示通过权重和偏置的结果,还未经过激活函数, ℎ 表示经过激活函数后的结果。灰色框内表示 L 对各中间计算结果的梯度,这些梯度的反向传播有两类:
- 由 ℎ 到 a,通过激活函数,如右上角
# * 表示element-wise乘积,· 表示矩阵乘积
class Output_layer(Layer):
'''属性和forward方法继承Layer类'''
def backward(input_grad):
'''输出层backward方法'''
'''单个样本的反向传播'''
a_grad = input_grad # (1, output_dim)
b_grad = a_grad # (1, output_dim)
W_grad = (input_data.T) · a_grad # (input_dim, output_dim)
self.b -= learning_rate * b_grad
self.W -= learning_rate * W_grad
return a_grad · (self.W).T # (1, input_dim)
3. Batch 批量计算
除非用随机梯度下降,否则每次用以训练的样本都是整个batch计算的,损失函数 L 则是整个batch中样本得到损失的均值。
在计算中会以向量化的方式增加运算效率,用batch_size表示批的规模,代码可更改为:
# * 表示element-wise乘积,· 表示矩阵乘积
class Layer:
'''中间层类'''
def forward(self, input_data): # input_data: (batch_size, input_dim)
'''batch_size个样本的前向传播'''
input_data · self.W + self.b = a # a: (1, output_dim)
h = self.activate(a) # h: (1, output_dim)
return h
def backward(input_grad): # input_grad: (batch_size, output_dim)
'''batch_size个样本的反向传播'''
a_grad = input_grad * activate’(a) # (batch_size, output_dim)
b_grad = a_grad.mean(axis=0) # (1, output_dim)
W_grad = (a_grad.reshape(batch_size,1,output_dim)
* input_data.reshape(batch_size,input_dim,1)).mean(axis=0)
# (input_dim, output_dim)
self.b -= lr * b_grad
self.W -= lr * W_grad
return a_grad · (self.W).T # output_grad: (batch_size, input_dim)
class Output_layer(Layer):
'''输出层类:属性和forward方法继承Layer类'''
def backward(input_grad): # input_grad: (batch_size, output_dim)
'''输出层backward方法'''
'''batch_size个样本的反向传播'''
a_grad = input_grad # (batch_size, output_dim)
b_grad = a_grad.mean(axis=0) # (1, output_dim)
W_grad = (a_grad.reshape(batch_size,1,output_dim)
* input_data.reshape(batch_size,input_dim,1)).mean(axis=0)
# (input_dim, output_dim)
self.b -= learning_rate * b_grad
self.W -= learning_rate * W_grad
return a_grad · (self.W).T # output_grad: (batch_size, input_dim)
这里比较易错的地方是什么时候求均值,对 ∂L/∂a 求均值还是对 ∂L/∂a 求均值:梯度在中间结果 a,ℎ 上都不需要求均值,对参数 w,b 的梯度时才需要求均值。
4. 代码
模拟一个三层神经网络的训练