从零开始的全连接神经网络中的反向传播
从零开始的全连接神经网络中的反向传播
使用 Python 和 NumPy 构建您自己的全连接神经网络的指南。
A dog. Image from 今日医学新闻
温柔的介绍
在巴甫洛夫进行的一项著名研究中,狗只要听到铃声就会受到治疗。最初,狗并没有被令人费解的铃声打扰——它们摇着尾巴做自己的事情。但是当他们发现食物几乎总是伴随着铃声的响起时,他们的大脑将铃声与传入的食物联系起来。结果,每当狗听到铃响时,它们就会蜂拥而至,唾液滴下,准备吃东西。
狗的条件行为是由于 反向传播 在他们自己的神经网络中。用一种过于简单的方式来解释,比如函数 y=f(x) 决定了狗的行为,其中 x 是狗的兴奋剂输入(光、声、触),y 是狗的输出行为(走路、坐,汪)。反向传播训练狗将 x=bell ringing 映射到 y=eat,而最初 x=bell ringing 将指向 y=什么也不做。
现在我们知道什么是反向传播,我们可以使用这种做法来训练将输入 X 映射到所需 Y 的机器算法。在这篇文章中,我将数字识别作为我们希望机器学习的任务,其中输入 X是 28x28 的图像,Y 是图像对应的数字 (0-9)。
前向传播
要了解反向传播,必须了解正向传播并定义一些变量。我们首先定义网络的大 X 和大 Y。
Uppercase X and Y refer to the inputs and outputs of the entire network, while lowercase x and y are the inputs and outputs of a single layer.
在前向传播过程中,每一层接受一个输入 x = 上一层的输出,进行加权和计算,并将 y = 当前层的输出输出到下一层。随着层向上传播(朝向输出层),网络的最终输出 Y 将等于输出层的输出 y。
现在已经了解了前向传播的大图,让我们深入研究单层内的加权和计算。除了输入 x 和输出 y,还有 4 个其他类型的变量:
对于单个神经元,它的权重与该层的输入数量一样多。它还将有一个偏置项 b 和一个指定的激活函数 g(sigmoid、tanh、relu……)。神经元将使用其权重和偏置来计算其输入的偏置加权和,并对其应用激活函数以找到神经元的输出 y。
Weighted sum calculation of a single neuron.
你可能会问,毕竟有一个激活函数有什么意义呢?具有激活函数会增加网络的非线性。没有它,多层网络将崩溃为一层,从而显着降低其复杂性。所以最后一步,a=g(z),并不是没有原因的。
现在我们有了前向传播的数学公式,让我们对其进行编码。
DenseLayer 类:
def __init__(self, num_neurons, num_inputs, activation_func):
self.num_neurons = num_neurons
self.num_inputs = num_inputs
self.activation_func = 激活函数
self.x = np.zeros(num_inputs)
self.z = np.zeros(num_neurons)
self.w = np.random.randn(num_neurons, num_inputs) * 0.01
self.b = np.random.randn(num_neurons) * 0.01
self.dw = np.zeros((num_neurons, num_inputs))
self.db = np.zeros(num_neurons)
def forward_propagation(self, x):
# x 是一个 numpy 维度数组 (#inputs)
self.x = x # 将 x 存储为 back prop
对于范围内的神经元数(self.num_neurons):
z = 0
对于范围内的 weight_num(self.num_inputs):
z += self.w[neuron_num][weight_num] * x[weight_num]
z += self.b[neuron_num]
self.z[neuron_num] = z # 存储 z 作为 back prop
a = 激活(self.z,self.activation_func)
返回一个
在网络上进行前向传播:
# network 是一个包含所有层的列表
对于索引,枚举(网络)中的层:
如果索引 == 0:
输出 = layer.forward_propagation(x)
别的:
输出= layer.forward_propagation(输出)
output... # 是网络的最终输出
损失和成本函数
现在我们已经获得了 输出
对于网络,我们希望有一种方法来告诉网络是否需要它的输出。这就是损失函数和成本函数发挥作用的地方。
我们首先定义损失函数。
The cross-entropy loss function, also known as logistic loss.
损失函数测量单个训练样本的输出偏离程度,但我们不希望网络仅从一个训练样本中学习,因此我们取所有训练样本的平均损失,并将其称为成本函数。
You may also see the cost function being named as C(x), or E(x), but I like the letter J.
那么我们想用成本函数做什么呢?我们希望将其最小化。如果你回忆一下微积分,我们可以计算导数来解决优化问题。如果我们反复减去当前导数的一部分,我们可以轻轻地滑下斜坡,当导数达到 0 时,我们知道我们正在降落在某种最小值上。
Using derivatives to find global minima.
这就是反向传播和梯度下降背后的重要思想。找到正确的 ∂J/∂w 和 ∂J/∂b 并更新参数,直到梯度达到接近 0 的位置。
Gradient descent is like walking down a hill. Image from 易艾.
反向传播
顾名思义,反向传播向后传播(朝向输入层)。这背后的原因很直观:您希望对对输出有更直接影响的层进行更大的更新,因此您从输出层开始。
回想一下,成本函数是
计算 J 关于输出层的 ŷ = y 的导数
The uppercase L refers to the output layer, while lowercase l refers to the rest of the layers.
如果您自己进行微积分工作,您可能会注意到导数缺少标量 1/m。我故意省略了这个术语,因为这个标量的影响会被学习率(我们稍后会谈到)减轻,所以现在把它放在这里没有意义。
现在已经计算了输出层的∂J/∂a,我们可以向下传播这个导数(朝向输入层),其中每一层将使用输入的∂J/∂a来计算∂J/∂w,∂J/下一层的∂b和∂J/∂a。
这是反向传播的大图。现在的问题是,给定当前层的∂J/∂a,我们如何找到下一层的∂J/∂w、∂J/∂b和∂J/∂a?答案是更多的微积分。
回忆一下前向传播的公式
如果我们对这些公式进行微积分,我们可以找到我们想要的导数。最容易找到的导数是∂J/∂z。
Using chain rule.
有了∂J/∂z,我们可以更容易地计算其他导数
最后,下一层的∂J/∂a,以便我们可以向下传播网络。
现在我们有了所有的公式,我们可以将它们组装成代码。
def back_propagation(self, da):
# da 是一个 numpy 维度数组 (#neurons)
da_prev = np.zeros(self.num_inputs)
对于范围内的神经元数(self.num_neurons):
dz = da[neuron_num] * activation_prime(self.z[neuron_num],
self.activation_func)
对于范围内的 weight_num(self.num_inputs):
self.dw[neuron_num][weight_num] = dz *
self.x[weight_num]
da_prev[weight_num] += dz * self.w[neuron_num]
[weight_num]
self.db[neuron_num] = dz
返回 da_prev
沿网络反向传播:
# 前向传播...
# 输出层的∂J/∂a
da = np.divide(yhat[:, sample_num] - y[:, sample_num], (1 - yhat[:, sample_num]) * y[:, sample_num] + epsilon)
对于反向层(网络):
da = layer.back_propagation(da)
Epsilon 是一个非常小的数字,可以防止除以 0。我使用 epsilon = 1e-8。
梯度下降
现在我们已经计算了导数∂J/∂w和∂J/∂b,我们可以做梯度下降了。
Alpha is the learning rate, a scalar for gradient descent.
# 反向传播...
对于网络中的层:
layer.w = layer.w - alpha * layer.dw
layer.b = layer.b - alpha * layer.db
如果我们运行前向传播、反向传播和梯度下降,我们将只更新一次参数 w 和 b,并实现一步梯度下降。做多步梯度下降后,代价函数值会逐渐减小,如果我们映射代价函数,它可能看起来像这样:
My cost function. Image generated by Matplotlib.
结语
反向传播本质上是你现在正在做的学习过程。为了确保更好的学习体验,我建议您编写自己的神经网络。如果你这样做了,你可能会发现一件事真的很烦人——训练时间。
在我的电脑上,训练 1000 步大约需要 10 分钟。那太长了。在我的下一篇文章中,我们将看到矢量化如何将训练时间减少到几秒钟。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明