反向传播法是神经网络的基础了,但是很多人在学的时候总是会遇到一些问题,或者说看书上一堆推导公式感觉很复杂,其实仔细看,就是一个链式求导法则反复用。本篇会以最详细的方式为大家讲解反向传播法,也会有简单的反向传播代码实现,咱们别急,等我慢慢道来。
目录
- 1.前向传播
- 2.反向传播
- 3.代码实现简单的反向传播
1.前向传播
首先我们来看一张简单的两层神经网络图(自己制作的,有点丑)
我先给小伙伴们解释一下图中参数的意思
和是输入层和隐藏层的偏置值(也可以先不看偏置值),下面推导默认使用偏置值,只不过b用表示的,这里面就是b,默认为1,
,……是输入的n个特征值,,……是隐藏层经过激活函数后的m个输出值,……是输出层经过激活函数后的个预测值
表示从输入层第i个神经元到隐藏层第j个神经元的权重
表示从隐藏层第j个神经元到输出层第k个神经元的权重
了解了这些参数的意思,我们先简单讲解前向传播,这个对小伙伴们来说应该很简单吧!
(1)输入层---->隐藏层:
(2)隐藏层---->输出层:
上面公式中:
和相当于线性回归()的输出结果,是激活函数,这里我们使用激活函数,也就是,和都是隐藏层和输出层经过激活函数后的结果,最后得到的就是正向传播最后得到的预测值。
2.反向传播
接下来我们先定义一个损失函数,最简单也是大家最熟悉的均方差损失函数,也就是,这里的就是真实值(标签值),就是上面正向传播得到的预测值,是为了求导方便,不影响最后的损失对结果的度量,那么这里的
又因为:
那么最终的就等价于:
得到的完整式子后,我们的目标肯定是越小越好,这就需要优化,用的最多的当然就是梯度下降法了,不停地迭代更新,直到得到最合适的值使得最小,由梯度下降法的参数更新公式,可以得到:
其中是学习率,自己设定,那么:
我们令:
我们把称作学习信号,代入式子:
现在我们来看式子:
将上面两个式子代回:
得到了最后的学习信号表达式,就可以代回梯度下降参数更新公式里面了:
接下来求,这个比上面的更简单,因为我们知道偏置的权重系数是1,所以就有:
再更新偏置值:
这个时候两层的权重和偏置就更新了一次,小伙伴们要要使用其他激活函数或者损失函数,只需要修改对应位置激活函数的导函数和损失函数的导函数就可以了,我们也可以发现参数更新的速度(收敛速度)跟学习率有关系,所以lr初始值尽量从小开始设置,上面的推导过程写得很详细,第一次看可能会比较昏,不妨动手跟着算一遍就会很清楚了,矩阵的反向传播也是同样的,过程差不多,大家可以自行推导,下面我将用python代码实现反向传播过程
3.代码实现简单的反向传播
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-1 * x))
def d_sigmoid(x):
s = sigmoid(x)
return s * (np.ones(s.shape) - s)
def mean_square_loss(s, y):
return np.sum(np.square(s - y) / 2)
def d_mean_square_loss(s, y):
return s - y
def forward(W1, W2, b1, b2, X, y):
# 输入层到隐藏层
y1 = np.matmul(X, W1) + b1 # [2, 3]
z1 = sigmoid(y1) # [2, 3]
# 隐藏层到输出层
y2 = np.matmul(z1, W2) + b2 # [2, 2]
z2 = sigmoid(y2) # [2, 2]
# 求均方差损失
loss = mean_square_loss(z2, y)
return y1, z1, y2, z2, loss
def backward_update(epochs, lr=0.01):
# 随便创的数据和权重,偏置值,小伙伴们也可以使用numpy的ranodm()进行随机初始化
X = np.array([[0.6, 0.1], [0.3, 0.6]])
y = np.array([0, 1])
W1 = np.array([[0.4, 0.3, 0.6], [0.3, 0.4, 0.2]])
b1 = np.array([0.4, 0.1, 0.2])
W2 = np.array([[0.2, 0.3], [0.3, 0.4], [0.5, 0.3]])
b2 = np.array([0.1, 0.2])
# 先进行一次前向传播
y1, z1, y2, z2, loss = forward(W1, W2, b1, b2, X, y)
for i in range(epochs):
# 求得隐藏层的学习信号(损失函数导数乘激活函数导数)
ds2 = d_mean_square_loss(z2, y) * d_sigmoid(y2)
# 根据上面推导结果式子(2.4不看学习率)--->(学习信号乘隐藏层z1的输出结果),注意形状需要转置
dW2 = np.matmul(z1.T, ds2)
# 对隐藏层的偏置梯度求和(式子2.6),注意是对列求和
db2 = np.sum(ds2, axis=0)
# 式子(2.5)前两个元素相乘
dx = np.matmul(ds2, W2.T)
# 对照式子(2.3)
ds1 = d_sigmoid(y1) * dx
# 式子(2.5)
dW1 = np.matmul(X.T, ds1)
# 对隐藏层的偏置梯度求和(式子2.7),注意是对列求和
db1 = np.sum(ds1, axis=0)
# 参数更新
W1 = W1 - lr * dW1
b1 = b1 - lr * db1
W2 = W2 - lr * dW2
b2 = b2 - lr * db2
y1, z1, y2, z2, loss = forward(W1, W2, b1, b2, X, y)
# 每隔100次打印一次损失
if i % 100 == 0:
print('第%d批次' % (i / 100))
print('当前损失为:{:.4f}'.format(loss))
print(z2)
# sigmoid激活函数将结果大于0.5的值分为正类,小于0.5的值分为负类
z2[z2 > 0.5] = 1
z2[z2 < 0.5] = 0
print(z2)
if __name__ == '__main__':
backward_update(epochs=50001, lr=0.01)