任务描述
本关任务:实现神经网络模型的前向传播。
相关知识
为了完成本关任务,你需要掌握:
神经网络前向传播的原理;
计算图;
神经网络前向传播的实现。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第 5.1 章节的内容。
神经网络的前向传播
在之前的实训中,我们简单学习了神经网络的前向传播。神经网络网络是由多个神经网络层堆叠而成的模型。神经网络的前向传播就是按照神经网络层的堆叠顺序,将前驱网络层的输出作为后继网络层的输入,通过网络层的运算规则计算对应的输出。在之前实训中,我们曾以一个简单的三层神经网络为例,这里我们简单做一个回顾。
图1
图1 简单三层神经网络
假设这个神经网络的三层分别为f
1
(x;W
1
), f
2
(x;W
2
), f
3
(x;W
3
),每层之后的激活函数为g
1
(x), g
2
(x), g
3
(x)。网络训练使用的损失函数为L(x,t),其中x表示网络的输出,t表示目标。那么这个网络的计算过程可以表示为:
y
1
z
1
y
2
z
2
y
3
z
3
=f
1
(x;W
1
)
=g
1
(y
1
)
=f
2
(z
1
;W
2
)
=g
2
(y
2
)
=f
3
(z
2
;W
3
)
=g
3
(y
3
)
这里z
3
就是神经网络模型的输出。结合损失函数,可以得到网络模型的损失值:
l=L(z
3
,t)
计算图
这里我们希望进一步引入计算图的概念。到目前为止,我们一直讲神经网络是网络层的堆叠。但是,这里希望强调的是,这种堆叠结构并不一定是线性堆叠的结构,对于一些复杂的神经网络模型,通常有多个输入和多个输出,网络模型的中间结果也会互相调用。但是,网络层的计算一定是有序的,不能有环状依赖的存在。这样,就形成了一个 DAG(有向无环图)的结构。这样的图结构就叫做神经网络的计算图。
计算图中的节点包含两种,一种是数据节点,一种是计算节点。顾名思义,数据节点的作用是存储数据,网络的输入、网络层的参数、网络层的中间计算结果、网络层的计算结果等都存储在数据节点中。而计算节点,就是把若干个数据节点作为输入,进行某种运算,再将结果输出到另一个数据节点中。也就是说,数据节点和计算节点是间隔分布的。在构造计算节点时,通常会使用一些非常基础的计算作为一个节点,如矩阵乘法、加法、卷积等,例如全连接层会被拆分成矩阵乘法和加法两个操作。下图展示了一个使用 sigmoid 激活函数的全连接层对应的计算图。
图1
图2 使用sigmoid激活函数的全连接层对应的计算图
神经网络前向传播的实现
在本实训中,基于之前我们一步一步实现的网络层,需要你自己定义一个小型的卷积神经网络,并实现其前向传播。这里要求实现一个名为 TinyNet 的小型卷积神经网络模型。TinyNet 包含 7 层,输入是一个形状为(B,3,8,8)的numpy.array;第一层是一个输出通道为 6、卷积核大小为 3、步长为 1、填充为 1 的卷积层;第二层是 ReLU 激活;第三层是一个池化核大小为 2、步长为 2、填充为 0 的最大值池化层,该池化层将特征图大小变为4×4;第四层是一个输出通道为 12、卷积核大小为 3、步长为 1、填充为 1 的卷积层;第五层是 ReLU 激活;第六层是一个池化核大小为 2、步长为 1、填充为 0 的最大值池化层,该池化层将特征图大小变为2×2;第 7 层是一个全连接层,有 10 个输出神经元。最后,网络使用SoftmaxWithLoss作为损失函数。整个网络的结构如下表所示:
序号 类型 参数 输出特征图大小
0 输入 (B, 3, 8, 8) -
1 卷积层 输出通道6,卷积核大小3x3,步长1,填充0 (B, 6, 8, 8)
2 激活函数 ReLU (B, 6, 8, 8)
3 池化层 池化窗口2x2,步长2,填充0 (B, 6, 4, 4)
4 卷积层 输出通道12,卷积核大小3x3,步长1,填充0 (B, 12, 4, 4)
5 激活函数 ReLU (B, 12, 4, 4)
6 池化层 池化窗口2x2,步长2,填充0 (B, 12, 2, 2)
7 全连接层 输出神经元10 (B, 10)
8 损失函数 SoftmaxWithLoss -
实训已经预先定义了一个TinyNet类。该类的构造函数接受 6 个参数:W_conv1和b_conv1对应第一个卷积层的权重和偏置,W_conv2和b_conv2对应第二个卷积层的权重和偏置,W_fc和b_fc对应全连接层的权重和偏置。你需要在该类的构造函数中,定义网络中的各个层。实训已经提供了各个网络层的定义与实现,与之前实训中的定义完全相同,你可以直接使用。之后,你需要在前向传播函数forward(x, t)中实现TinyNet的前向传播,并返回全连接层的输出以及 loss 函数的值(按照此顺序)。
编程要求
根据提示,在右侧编辑器中 Begin 和 End 之间补充代码,实现上述 TinyNet 的定义和前向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:
平台会随机产生输入x、目标t以及三组权重和偏置,然后根据你的实现代码,创建一个TinyNet类的实例,然后利用该实例进行前向传播计算。你的答案将与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过 1e-5 即可。
开始你的任务吧,祝你成功!
import numpy
from layers import Convolution, Relu, FullyConnected, MaxPool, SoftmaxWithLoss
class TinyNet:
def __init__(self, W_conv1, b_conv1, W_conv2, b_conv2, W_fc, b_fc):
########## Begin ##########
self.conv1 = Convolution(W_conv1, b_conv1, stride=1, pad=1)
self.relu1 = Relu()
self.pool1 = MaxPool(2, 2, stride=2, pad=0)
self.conv2 = Convolution(W_conv2, b_conv2, stride=1, pad=1)
self.relu2 = Relu()
self.pool2 = MaxPool(2, 2, stride=2, pad=0)
self.fc = FullyConnected(W_fc, b_fc)
self.loss = SoftmaxWithLoss()
########## End ##########
def forward(self, x, t):
########## Begin ##########
x = self.conv1.forward(x)
x = self.relu1.forward(x)
x = self.pool1.forward(x)
x = self.conv2.forward(x)
x = self.relu2.forward(x)
x = self.pool2.forward(x)
x = self.fc.forward(x)
loss = self.loss.forward(x, t)
return x, loss
########## End ##########
任务描述
本关任务:实现神经网络模型的反向传播。
相关知识
为了完成本关任务,你需要掌握:
神经网络反向传播的原理;
计算图上的反向传播;
神经网络反向传播的实现。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第 5 章的内容。
神经网络的反向传播
在之前的实训中,我们简单学习了神经网络的反向传播。神经网络的反向传播就是按照神经网络层的堆叠顺序的逆顺序,将后继网络层的输入的梯度作为前驱网络层反向传播的输入,通过网络层的反向传播运算规则,计算对应的输入的梯度和参数的梯度。在之前实训中,我们曾以一个简单的三层神经网络为例,这里我们简单做一个回顾。
图1
图1 简单三层神经网络
假设这个神经网络的三层分别为f
1
(x;W
1
), f
2
(x;W
2
), f
3
(x;W
3
),每层之后的激活函数为g
1
(x), g
2
(x), g
3
(x)。网络训练使用的损失函数为L(x,t),其中x表示网络的输出,t表示目标。那么这个网络的计算过程可以表示为:
y
1
z
1
y
2
z
2
y
3
z
3
=f
1
(x;W
1
)
=g
1
(y
1
)
=f
2
(z
1
;W
2
)
=g
2
(y
2
)
=f
3
(z
2
;W
3
)
=g
3
(y
3
)l=L(z
3
,t)
之后,对第三层进行反向传播,按照相同的方法,可以对之前的网络层进行推导:
∂y
3
∂l
∂W
3
∂l
∂z
2
∂l
=
∂z
3
∂l
⋅
∂y
3
∂z
3
∂z
3
∂l
⋅
∂y
3
∂g
3
(y
3
)
=
∂y
3
∂l
⋅
∂W
3
∂y
3
∂y
3
∂l
⋅
∂W
3
∂f
3
(z
2
;W
3
)
=
∂y
3
∂l
⋅
∂z
2
∂y
3
∂y
3
∂l
⋅
∂z
2
∂f
3
(z
2
;W
3
)
计算图
这里我们希望进一步引入计算图的反向传播。在上一关中,我们学习了神经网络的计算图把每个网络层拆解成一系列的元操作,这些元操作对应计算节点,所有的中间结果对应数据节点,这些节点按照计算顺序形成一个 DAG 的结构。
在反向传播时,对于有多个输入的层,对于每个输入的反向传播可能会不同。一个典型的例子就是矩阵乘法算子 y=x
T
W,通过前面我们对全连接层的学习,我们知道对 x 和对 W 的反向传播计算是不同的,此时在计算图中矩阵乘法算子的反向传播就需要拆成两个算子,变成两个计算节点。下图展示了一个使用 sigmoid 激活函数的全连接层对应的前向和反向传播的计算图。
图1
图2 使用sigmoid激活函数的全连接层对应的前向和反向传播的计算图
神经网络反向传播的实现
实训拓展了在之前的实训定义的TinyNet类,实训已经给出了forward(x, t)的实现,并针对反向传播的需要对其进行了一定的修改。你需要实现该类的反向传播函数backward()。你需要将构造函数中的W_conv1、b_conv1、W_conv2、b_conv2、W_fc、b_fc的梯度按照顺序返回。
编程要求
根据提示,在右侧编辑器中 Begin 和 End 之间补充代码,实现上述 TinyNet 的定义和前向传播。
测试说明
平台会对你编写的代码进行测试,测试方法为:
平台会随机产生输入x、目标t以及三组权重和偏置,然后根据你的实现代码,创建一个TinyNet类的实例,然后利用该实例进行前向传播计算,再进行反向传播的计算。你的答案将与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过 1e-5 即可。
import numpy
from layers import Convolution, Relu, FullyConnected, MaxPool, SoftmaxWithLoss
class TinyNet:
def __init__(self, W_conv1, b_conv1, W_conv2, b_conv2, W_fc, b_fc):
self.conv1 = Convolution(W_conv1, b_conv1, stride=1, pad=1)
self.relu1 = Relu()
self.pool1 = MaxPool(2, 2, stride=2, pad=0)
self.conv2 = Convolution(W_conv2, b_conv2, stride=1, pad=1)
self.relu2 = Relu()
self.pool2 = MaxPool(2, 2, stride=2, pad=0)
self.fc = FullyConnected(W_fc, b_fc)
self.loss = SoftmaxWithLoss()
def forward(self, x, t):
x = self.conv1.forward(x)
x = self.relu1.forward(x)
x = self.pool1.forward(x)
x = self.conv2.forward(x)
x = self.relu2.forward(x)
x = self.pool2.forward(x)
x = self.fc.forward(x)
loss = self.loss.forward(x, t)
return x, loss
def backward(self):
########## Begin ##########
dx = self.loss.backward()
dx = self.fc.backward(dx)
dx = self.pool2.backward(dx)
dx = self.relu2.backward(dx)
dx = self.conv2.backward(dx)
dx = self.pool1.backward(dx)
dx = self.relu1.backward(dx)
dx = self.conv1.backward(dx)
########## End ##########
return self.conv1.dW, self.conv1.db, self.conv2.dW, self.conv2.db, self.fc.dW, self.fc.db
任务描述
本关任务:实现神经网络的梯度下降训练。
相关知识
为了完成本关任务,你需要掌握:梯度下降训练的原理。
本实训内容可参考《深度学习入门——基于 Python 的理论与实现》一书中第 6 章的内容。
神经网络的训练
神经网络是一类非常典型的非凸模型,对与非凸函数进行优化的问题是非凸优化问题,而解决非凸优化问题的最常用的方法就是梯度下降。在之前的实训中,我们学习过梯度和梯度下降法的概念,这里做一个简单的回顾。梯度是函数值上升最快的参数变化方向,通常来说,这也是函数值下降最快的参数变化方向的负方向。如果我们能够求得每个参数的梯度∂l/∂w,那么我们就可以令所有的参数沿着其负梯度方向前进一小步,得到一组新的参数。这就是梯度下降法的基本思想,这一小步的距离叫做学习率η。参数更新的过程可以用下面公式表示:
w
i
′
=w
i
−η⋅
∂w
i
∂l
如果这一过程延续足够长的时间,我们就可以期望模型能够收敛到一个足够好的位置(局部最优)。对于非凸优化问题,我们通常不期望模型能够收敛到全局最优,而只是期望模型能够收敛到一个足够好的局部最优。这个过程可以用下图表示:
图1
图1 梯度下降法
随机梯度下降
神经网络模型的训练离不开数据。在之前的实训中,我们可以看到,损失函数值的计算只与当前的 batch 有关。在使用梯度下降时,一种可行的方法是对于所有的训练数据,计算损失函数值,进而计算梯度,更新权重。但是,这样存在一个问题,就是每次更新需要的计算量非常大。目前,用来训练神经网络的数据集非常巨大,对整个数据集计算损失再进行更新效率非常低,因此,我们引入随机梯度下降。随机梯度下降的思想是,每次从训练数据中随机取出若干个,构成一个 batch,每次只对这一个 batch 计算损失和计算梯度,进而更新权重。数学上可以证明,随机梯度下降也可以保证网络的收敛。
通常,随机梯度下降在采样训练数据时并不是完全随机采样的,而是先将整个数据集随机排序,然后从头开始依次取。按照这样的方式将整个数据集里的数据全都选取了一遍叫做一个 epoch,每次取的叫一个 batch 或者一个 iteration。
欠拟合与过拟合
机器学习模型在训练时还有另外一个重要的问题,那就是欠拟合(underfit)与过拟合(overfit)。在本质上这是一个数据集与模型拟合能力相匹配的问题。模型的参数越多,模型越复杂,那么模型拟合数据的能力就越强。但是,如果数据比较简单,那么用一个过分强大的模型来拟合这个数据集会造成模型“记住”了每个训练样本,而不是从训练数据中挖掘出共性,从而造成过拟合;而如果数据非常复杂,而我们使用了一个过分简单的模型,那么模型就难以挖掘到数据背后的模式,从而造成欠拟合。下图展示了欠拟合与过拟合。图中的样本是从一个二次曲线上采样下来的,如果我们用一个线性函数来拟合,那么会造成欠拟合;而如果我们用一个高次函数来拟合,就会造成过拟合。这个高次函数经过了所有的样本点,但明显这不是我们想要的那个。
图1
图2 欠拟合、过拟合和拟合
那么欠拟合与过拟合要怎么解决呢?对于欠拟合,我们通常采用的方法是设计更复杂、拟合能力更强的神经网络;而对于过拟合,我们通常采用的方法是正则化(regularization)。而通常采用的正则化方法就是 L2 正则化。L2 正则化的基本思想是在 loss 中加入一个正则化项,这个正则化项是模型中的每个参数的 2 范数,即:
L=L(x,t)+λ⋅∑
2
1
w
i
2
通过最小化这个总损失,可以使得每个参数尽量小,从而抑制过拟合。λ是正则化系数,通常称为 weight decay,常用值为 1e-5。值得注意的是,正则化项只与参数本身有关,与模型的输入以及样本的标签都没有关系,因此,正则化项不需要显式的放在损失函数中计算,而是可以在更新参数的时候直接加到参数对应的梯度中。
随机梯度下降的实现
在本实训,你将对之前定义的 TinyNet,实现一次随机梯度下降的迭代。具体来说,你要实现train_one_iter函数,该函数接受 9 个参数:TinyNet 的三组权重和偏置、这个 iteration 的输入数据x、标签t、学习率learning_rate和正则化系数weight_decay。在该函数中,你要先构建一个TinyNet实例,然后先进行前向传播,再进行反向传播,最后对模型参数进行更新,最后把更新后的参数按照输入顺序返回。
编程要求
根据提示,在右侧编辑器 Begin 和 End 之间补充代码,实现随机梯度下降的训练。
测试说明
平台会对你编写的代码进行测试,测试方法为:
平台会随机产生输入x、目标t以及三组权重和偏置,并制定学习率和正则化系数,然后根据你的实现调用train_one_iter函数。你的答案将与标准答案进行比较。因为浮点数的计算可能会有误差,因此只要你的答案与标准答案之间的误差不超过 1e-5 即可。
import numpy
from layers import Convolution, Relu, FullyConnected, MaxPool, SoftmaxWithLoss
class TinyNet:
def __init__(self, W_conv1, b_conv1, W_conv2, b_conv2, W_fc, b_fc):
self.conv1 = Convolution(W_conv1, b_conv1, stride=1, pad=1)
self.relu1 = Relu()
self.pool1 = MaxPool(2, 2, stride=2, pad=0)
self.conv2 = Convolution(W_conv2, b_conv2, stride=1, pad=1)
self.relu2 = Relu()
self.pool2 = MaxPool(2, 2, stride=2, pad=0)
self.fc = FullyConnected(W_fc, b_fc)
self.loss = SoftmaxWithLoss()
def forward(self, x, t):
x = self.conv1.forward(x)
x = self.relu1.forward(x)
x = self.pool1.forward(x)
x = self.conv2.forward(x)
x = self.relu2.forward(x)
x = self.pool2.forward(x)
x = self.fc.forward(x)
loss = self.loss.forward(x, t)
return x, loss
def backward(self):
dx = self.loss.backward()
dx = self.fc.backward(dx)
dx = self.pool2.backward(dx)
dx = self.relu2.backward(dx)
dx = self.conv2.backward(dx)
dx = self.pool1.backward(dx)
dx = self.relu1.backward(dx)
dx = self.conv1.backward(dx)
return self.conv1.dW, self.conv1.db, self.conv2.dW, self.conv2.db, self.fc.dW, self.fc.db
def train_one_iter(W_conv1, b_conv1, W_conv2, b_conv2, W_fc, b_fc, x, t, learning_rate):
network = TinyNet(W_conv1, b_conv1, W_conv2, b_conv2, W_fc, b_fc)
out, loss = network.forward(x, t)
dW_conv1, db_conv1, dW_conv2, db_conv2, dW_fc, db_fc = network.backward()
########## Begin ##########
new_W_conv1 = W_conv1 - dW_conv1 * learning_rate
new_b_conv1 = b_conv1 - db_conv1 * learning_rate
new_W_conv2 = W_conv2 - dW_conv2 * learning_rate
new_b_conv2 = b_conv2 - db_conv2 * learning_rate
new_W_fc = W_fc - dW_fc * learning_rate
new_b_fc = b_fc - db_fc * learning_rate
########## End ##########
return new_W_conv1, new_b_conv1, new_W_conv2, new_b_conv2, new_W_fc, new_b_fc