任务描述
本关任务:实现全连接层的反向传播。
相关知识
为了完成本关任务,你需要掌握:
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第5章的内容。
神经网络的反向传播
在之前的实训中,我们学习了神经网络通过反向传播来计算每个参数的梯度,同时反向传播的核心思想是求导的链式法则,即:
∂x∂l=∂f(x)∂l⋅∂x∂f(x)
那么,给定一个神经网络,反向传播是如何进行的呢?这里我们以一个三层神经网络为例,来讲解神经网络的反向传播。下图展示了这个简单神经网络的结构。
图1 简单三层神经网络
首先,我们引入一个记号f(x;W),来表示输入为x、参数为W的一个网络层。假设这个神经网络的三层分别为f1(x;W1), f2(x;W2), f3(x;W3),每层之后的激活函数为g1(x), g2(x), g3(x)。网络训练使用的损失函数为L(x,t),其中x表示网络的输出,t表示目标。那么这个网络的计算过程可以表示为:
y1z1y2z2y3z3l=f1(x;W1)=g1(y1)=f2(z1;W2)=g2(y2)=f3(z2;W3)=g3(y3)=L(z3,t)
在进行反向传播时,首先我们对损失函数进行反向传播:
∂z3∂l=∂z3∂L(z3,t)
之后,对第三层进行反向传播,按照相同的方法,可以对之前的网络层进行推导:
∂y3∂l∂W3∂l∂z2∂l=∂z3∂l⋅∂y3∂z3=∂z3∂l⋅∂y3∂g3(y3)=∂y3∂l⋅∂W3∂y3=∂y3∂l⋅∂W3∂f3(z2;W3)=∂y3∂l⋅∂z2∂y3=∂y3∂l⋅∂z2∂f3(z2;W3)
通过上面的推导可以看到,对于一个特定的网络层,只要其输出的梯度已知,那么其参数和输入的梯度只与该层的计算有关,而与之前和之后的网络层是什么没有关系。因此,只要对每个网络层都定义好其前向传播和反向传播的计算,那么神经网络就可以计算每个参数的梯度。在这个过程中,神经网络的计算形成了计算图,这里不做详细展开,感兴趣的同学可以参考教材第5.1−5.2章节的内容。
全连接层的反向传播
下面,我们来学习全连接层的反向传播。首先,回顾一下全连接层的前向传播。一个包含N个输入神经元,M个输出神经元的全连接层包含两组参数:权重W∈RN×M和偏置b∈RM,其输入可以看作是一个N维的(列)向量x∈RN,此时全连接层的前向传播的计算可以表示为:
y=xTW+b
先假设根据链式法则,已知∂l/∂y,按照全连接层的定义,其应该是一个M维的(列)向量。根据矩阵计算求导法则,可以得到:
∂b∂l∂W∂l∂x∂l=∂y∂l⋅∂b∂y=∂y∂l=∂y∂l⋅∂W∂y=x×(∂y∂l)T=∂y∂l⋅∂x∂y=W×∂y∂l
全连接层反向传播的实现
实训拓展了在之前的实训定义的FullyConnected
类,实训已经给出了forward(x)
的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)
,dout
是损失函数相对于全连接层输出的梯度,即之前公式中的∂y∂l,是一个形状为(B,M)的numpy.ndarray
,M是全连接层的输出通道数。在前向传播时,因为全连接层的输入的形状可以有所变化,并记录在了self.original_x_shape
中,因此在计算完成后,请把输入的梯度还原为原来的形状。全连接层的输入记录在了self.x
中。在反向传播的过程中,请将self.W
和self.b
的梯度分别存储在self.dW
和self.db
中,并将x
的梯度返回。
编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现全连接层的反向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入x
、权重W
、偏置b
和输出梯度dout
,然后根据你的实现代码,创建一个FullyConnected
类的实例,然后利用该实例先进行前向传播计算,再进行反向传播计算。你的答案将并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过10−5即可。
样例输入:
W:
[[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6]]
b:
[0.1, 0.2, 0.3]
x:
[[1, 2],
[3, 4]]
dout:
[[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6]]
则对应的梯度为:
dx:
[[0.14 0.32]
[0.32 0.77]]
dW:
[[1.3 1.7 2.1]
[1.8 2.4 3. ]]
db:
[0.5 0.7 0.9]
上述结果有四舍五入的误差,你可以忽略。
开始你的任务吧,祝你成功!
import numpy as np
class FullyConnected:
def __init__(self, W, b):
r'''
全连接层的初始化。
Parameter:
- W: numpy.array, (D_in, D_out)
- b: numpy.array, (D_out)
'''
self.W = W
self.b = b
self.x = None
self.original_x_shape = None
self.dW = None
self.db = None
def forward(self, x):
r'''
全连接层的前向传播。
Parameter:
- x: numpy.array, (B, d1, d2, ..., dk)
Return:
- y: numpy.array, (B, M)
'''
self.original_x_shape = x.shape
x = x.reshape(x.shape[0], -1)
self.x = x
out = np.dot(self.x, self.W) + self.b
return out
def backward(self, dout):
r'''
全连接层的反向传播
Parameter:
- dout: numpy.array, (B, M)
Return:
- dx: numpy.array, (B, d1, d2, ..., dk) 与self.original_x_shape形状相同
另外,还需计算以下结果:
- self.dW: numpy.array, (N, M) 与self.W形状相同
- self.db: numpy.array, (M,)
'''
########## Begin ##########
dx = np.dot(dout, self.W.T)
self.dW = np.dot(self.x.T, dout)
self.db = np.sum(dout, axis=0)
dx = dx.reshape(*self.original_x_shape)
return dx
########## End ##########
第2关:实现常用激活函数的反向传播
任务描述
本关任务:实现常用激活函数的反向传播。
相关知识
为了完成本关任务,你需要掌握:常用激活函数的反向传播。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第5.5章节的内容。
常用激活函数的反向传播
在上一关中,我们学习了全连接层的反向传播。在这一关中,我们更进一步,学习常用激活函数的反向传播。跟之前的实训一样,我们主要关注 sigmoid 和 ReLU 这两个激活函数。
1. sigmoid激活函数
在之前的实训中,我们学习了 sigmoid 激活函数的前向传播:
y=sigmoid(x)=1/(1+e−x)
因为激活函数都是逐元素进行计算的,因此激活函数的反向传播只需要根据函数求导法则求解即可:
∂x∂y=(1−y)y
对于 sigmoid 激活函数,可以得到:
∂x∂l=∂y∂l⋅∂x∂y=∂y∂l⋅(1−y)y
2. ReLU激活函数
在之前的实训中,我们学习了 ReLU 激活函数的前向传播:
y=ReLU(x)=max(0,x)
可以看到,ReLU 激活函数存在不可导点x=0,不能直接求导。此时,我们在该点处使用 ReLU 的次梯度:
∂x∂y(0)=0
关于次梯度的概念,这里不做深入的介绍,感兴趣的同学可以阅读相关书籍。在引入次梯度之后,ReLU 激活函数的梯度可以完整的表示为:
∂x∂y={0,x≤01,x>0
再结合之前的反向传播公式,就可以得到 ReLU 激活函数的反向传播计算方法。
常用激活函数反向传播的实现
对于 sigmoid 激活函数,实训拓展了在之前的实训定义的Sigmoid
类,实训已经给出了forward(x)
的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)
,dout
是损失函数相对于 Sigmoid 输出的梯度,即之前公式中的∂y∂l,是一个形状与输入x
相同的numpy.ndarray
。前向传播的输出记录在了self.out
中,以方便你进行反向传播的计算。backward(dout)
函数需要返回输入x
的梯度。
对于 ReLU 激活函数,实训拓展了在之前的实训定义的ReLU
类,实训已经给出了forward(x)
的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward(dout)
,dout
是损失函数相对于 Sigmoid 输出的梯度,即之前公式中的∂y∂l,是一个形状与输入x
相同的numpy.ndarray
。self.mask
记录了前向传播时每个输入是否小于等于 0,以方便你进行方向传播的计算。backward(dout)
函数需要返回输入x
的梯度。
编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现上述激活函数的反向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:平台会随机产生输入x
和输出梯度dout
,然后根据你的实现创建一个Sigmoid
/ReLU
类的实例,然后利用该实例先进行前向传播计算,再进行反向传播计算。你的答案将并与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过10−5即可。
样例输入:
# 对于sigmoid激活函数:
x:
[[-1, 0, 1]]
dout:
[[1, 2, 3]]
#对于ReLU激活函数:
x:
[[-1, 0, 1]]
dout:
[[1, 2, 3]]
则对应的梯度为:
# 对于sigmoid激活函数:
dx:
[[0.20, 0.50, 0.59]]
#对于ReLU激活函数:
dx:
[0, 2, 3]
上述结果有四舍五入的误差,你可以忽略。
开始你的任务吧,祝你成功!
import numpy as np
class Sigmoid:
def __init__(self):
self.out = None
def forward(self, x):
r'''
Sigmoid激活函数的前向传播。
Parameter:
- x: numpy.array, (B, d1, d2, ..., dk)
Return:
- y: numpy.array, (B, d1, d2, ..., dk)
'''
out = 1. / (1. + np.exp(-x))
self.out = out
return out
def backward(self, dout):
r'''
sigmoid的反向传播
Parameter:
- dout: numpy.array, (B, d1, d2, ..., dk)
Return:
- dx: numpy.array, (B, d1, d2, ..., dk)
'''
########## Begin ##########
dx = dout * (1.0 - self.out) * self.out
return dx
########## End ##########
class Relu:
def __init__(self):
self.mask = None
def forward(self, x):
r'''
ReLU激活函数的前向传播。
Parameter:
- x: numpy.array, (B, d1, d2, ..., dk)
Return:
- y: numpy.array, (B, d1, d2, ..., dk)
'''
self.mask = (x <= 0)
out = x.copy()
out[self.mask] = 0
return out
def backward(self, dout):
r'''
relu的反向传播
Parameter:
- dout: numpy.array, (B, d1, d2, ..., dk)
Return:
- dx: numpy.array, (B, d1, d2, ..., dk)
'''
########## Begin ##########
dout[self.mask] = 0
dx = dout
return dx
########## End ##########