1 实验3-1-基于 VGG19 实现图像分类
1.1 实验目的
掌握卷积神经网络的设计原理,掌握卷积神经网络的使用方法,能够使用 Python 语言实现VGG19网络模型对给定的输入图像进行分类。具体包括:
- 加深对深度卷积神经网络中卷积层、最大池化层等基本单元的理解。
- 利用Python语言实现VGG19的前向传播计算,加深对VGG19网络结构的理解,为后续风格迁移中使用 VGG19 网络计算风格损失奠定基础。
- 在第2.1节实验的基础上将三层神经网络扩展为 VGG19 网络,加深对神经网络工程 实现中基本模块演变的理解,为后续建立更复杂的综合实验(如风格迁移)奠定基础。
1.2 实验要求
为验证实验的正确性,选择如图3.9所示猫咪的图像进行分类测试。该猫咪图像的真实 类别为 tabby cat,对应 ImageNet 数据集类别编号的 281.实验结果将该图像的类别编号判 断为 281。通过查询 ImageNet 数据集类别编号对应的具体类别,编号 281 对应 tabby cat,说明利用 VGG19 网络判断得到了正确的图像类别。
• 60分 标准:给定卷积层和池化层的前向传播输入矩阵和参数值,可以得到正确的前向传播输出矩阵。 • 80分 标准:建立VGG19网络后,给定VGG19的网络参数值和输入图像,可以得到正确的pool5层输出结果。 • 100分 标准:建立VGG19网络后,给定VGG19的网络参数值和输入图像,可以得到正确的Softmax层输出结果和正确的图像分类结果。
1.3 实验步骤及心得收获
根据实验指导书,本次实验需要完成数据加载模块、基本单元模块、网络结构模块、网络推断模块。 主要要完成的是VGG19模型的各个层的计算方法、模型的结构以及加载模型函数。
# file: vgg_cpu.py
if __name__ == '__main__':
vgg = VGG19()
vgg.build_model()
vgg.init_model()
vgg.load_model()
vgg.load_image('cat1.jpg')
vgg.evaluate()
1.3.1 全连接层、ReLU层、Softmax层和Loss
1)全连接层
假定全连接层的输入是m维特征x(行向量),输出是n维特征v,那么权重为W,大小为m*n,偏置b是n维。
那么全连接层的前向传播计算如下:
在进行完一轮正向传播后,需要更新每一层的参数,使模型在训练集上向损失下降的方向前进。函数的方向导数和梯度公式如下:
其中
是一个单位向量。易证,当 方向与梯度 方向相同时, 取得最大值,即梯度方向是函数变化率最大的方向,换言之,梯度的反方向是函数下降的最快方向。因此,在训练参数时,这里使用小批量梯度下降法。
对于每一个全连接层需要更新的参数有
和 ,需要求损失函数对他们的梯度,根据链式求导的法则,依赖于损失函数对于这一层输出的梯度,也需要向前一层传播损失函数对前一层输出(本层输入)的偏导数。
更新参数时防止更新太大跳过最小值,需要加入学习率lr:
向下层传递参数:
2)激活函数ReLU
所有的全连接层,从输入到输出都是线性的,如果不引入非线性的元素,那么多个全连接层进行组合最后计算的结果还是线性的。那么一个多层的网络在多次训练之后,依然等价于一个一层的线性模型,这样就拜拜增加了计算量,浪费大量的内存和时间。 假设有m维的输入x,经过n、k维的全连接层h1、h2,最后输出的结果是y,那么由以下公式(13)和(14)推导可以得到(15),证明了上述的结论:
常用的激活函数有sigmoid、tanh、ReLU等,这里使用的是ReLU:
70
对于激活函数ReLU的反向传播函数:
3)Softmax层
神经网络预测出来的值可以是空间内的任意值,但是对于一个n-分类问题来说,可能的取值只有n种。Softmax的作用就是将预测出来的值进行归一化,得到每一个分类的概率,选取概率最大的类作为分类的结果。假定输入n维的v,则输出就是归一化后n维的y:
由指数函数
的性质可以知道, 的值越大,则 的取值越大。softmax函数利用这样的性质可以凸显出取值大的值,并抑制取值小的数。
4)Loss函数
softmax函数通常与交叉熵函数一起使用,因此这里使用了交叉熵作为损失函数:
Loss和Softmax结合后,得到的反向传播函数如下:
1.3.2 卷积层、池化层、Flatten
1)卷积层
卷积可以理解成为函数 经过旋转和平移 距离后与函数 重叠部分函数值乘积的和。因为实现了参数共享,因此在卷积层所用的权重和偏置的参数较少,此外卷积还具有具有平移不变性,即使图片中的目标位置平移到图片另一个地方,卷积神经网络仍然能很好地识别出这个目标,输出的结果也和未平移之前是一致的。
为了保证卷积之后的有效输出尺寸与输入尺寸一致,首先要对卷积层的输入做边界扩充,即在输入特征的上下左右边界都增加 行, 列的 0 。
# 边界填充
height = self.input.shape[2] + self.padding*2
width = self.input.shape[3] + self.padding*2
self.input_pad = np.zeros([
self.input.shape[0], self.input.shape[1],
height, width])
tmp=self.padding
self.input_pad[:,:,tmp:tmp+self.input.shape[2],
tmp:tmp+self.input.shape[3]] = self.input
然后根据实验手册提供的卷积层正向传播如下公式,编写对应的代码:
# 正向传播
self.output[idxn, idxc, idxh, idxw] = np.sum(
self.weight[:, :, :, idxc]*self.input_pad[
idxn, :, idxh*self.stride:idxh*self.stride
+self.kernel_size, idxw*self.stride:idxw\
*self.stride+self.kernel_size])
+self.bias[idxc]
2)池化层
池化可以看作对数据核心信息进行下采样操作,对于卷积神经网络模型,若无池化层,则相邻层之问的信息传递与抽取完全依靠卷积核的大小。若想提升相邻层的信息抽取覆盖范围,则只能增加核的尺度或在相邻层之间引人卷积尺度相同的邻接层。这必然引人更多的可变参数。参数增多必然增加模型过拟合风险,同时,也需要付出更多的训练代价。经过池化下采样操作之后,再用原尺寸的卷积核进行层间信息抽取时,相当于增加了卷核函数的视野,却并未引人新的学习参数。
池化层的正向传播公式如下:
对应的代码如下:
# Maxpooling 正向传播
self.output[idxn, idxc, idxh, idxw] = np.max(
self.input[idxn,idxc,idxh*self.stride:idxh*
self.stride+self.kernel_size,
idxw*self.stride:idxw*self.stride
+self.kernel_size])
3)Flatten层
扁平化层的作用就是将多个维度压缩成一维,将特征变成一个向量,使其能够使用线性的函数进行特征提取,并且最终生成分类,此层中所需要完成的工作只有矩阵的转置,根据提示使用如下代码即可完成:
# matconvnet feature map dim:[N,height,width,channel]
# ours feature map dim: [N, channel, height, width]
# 转换 input 维度顺序
self.input = np.transpose(input, [0, 2, 3, 1])
1.3.3 VGG19的结构
VGG19的结构如下图中的 E 所示:

因此,只需要按照这些层的类定义初始化类对象,并且将他们放入VGG19类的一个字典中就能完成VGG19的搭建了:
# 用字典保存各层结构
self.layers = {}
# 卷积层
ConvolutionalLayer(kernel_size, channel_in, channel_out, padding, stride)
# 最大池化层
MaxPoolingLayer(kernel_size, stride)
# ReLU层
ReLULayer()
# 扁平化层
FlattenLayer(input_shape, output_shape)
# 全连接层
FullyConnectedLayer(num_input, num_output)
# Softmax层和交叉熵损失函数
SoftmaxLossLayer()
填写完整后的代码如下图所示:

1.3.4 模型加载和前向传播推断
这一部分实验指导书已经给出了具体的实现方法,具体要做的就是把保存的numpy参数读取到模型中,并转置为正确的形式;而前向传播只需要按照定义的顺序,将输入的参数与模型字典中的各层参数进行运算即可。

1.4 实验结果
前向传播输出矩阵 | Pool5层输出结果 | 图像分类结果 | 图像分类概率 |
---|---|---|---|
正确 | 正确 | 281 | 0.4933 |
2 实验3-2-基于 DLP 平台实现图像分类
2.1 实验目的
巩固卷积神经网络的设计原理,能够使用 pycnml 库提供的 Python 接口将 VGG19 网络模型移植到 DLP 上,实现图像分类。具体包括:
- 使用 pycnml 库实现卷积、ReLU 等基本网络模块。
- 使用提供的 pycnml 库实现 VGG19 网络。
- 分析评估 DLP 和 CPU 运行 VGG19 进行图像分 类的性能。
2.2 实验要求
本实验仍然选择图3.9所示猫咪的图像进行分类测试,该猫咪图像的真实类别为 tabby cat,对应ImageNet数据集类别编号的281。若实验结果将该图像的类别编号判断为281,则 可认为判断正确。性能评判标准为预测猫咪类别时 VGG19 网络 forward 函数运行的时间。
• 100分标准:使用pycnml搭建VGG19网络,给定VGG19的网络参数值和输入图像,可以得到正确的Softmax 层输出结果和正确的图像分类结果。
2.3 实验步骤及心得收获
这一节的实验,主要是要求调用DLP平台的pycnml库来实现VGG19神经网络的正向推断,这个实验的要点是要掌握pycnml库的使用方法。
整体的实验步骤与实验3.1类似,但是不需要手动实现各个层的计算了。
# file: vgg19_demo.py
if __name__ == '__main__':
vgg = VGG19()
vgg.build_model()
vgg.load_model()
vgg.evaluate('file_list')
深度学习编程库 pycnml,通过调用 DLP 上 CNML 库中的高性能算子实现了全连接层、 卷积层、池化层、ReLU 激活层、Softmax 损失层等常用的网络层的基本功能,并提供了常用网络层的 Python 接口。使用pycnml库搭建VGG19模型需要使用到的函数如下所示:
import pycnml
self.net = pycnml.CnmlNet(16)
# 卷积层
self.net.createConvLayer(name, kernel_size, channel_in, channel_out, padding, stride,params)
# 池化层
self.net.createPoolingLayer(name, kernel_size, stride)
# ReLU层
self.net.createReLuLayer(name)
#扁平化层
self.net.createFlattenLayer(name, input_shape, output_shape)
# 全连接层
self.net.createMlpLayer(name, num_output, params)
# softmax层
self.net.createSoftmaxLayer(name, 1)
剩下需要填写的只有加载模型和加载图片的函数了,根据提示进行相应的转置运算即可:
# TODO:调整权重形状
# matconvnet: weights dim [height, width, in_channel, out_channel]
# ours: weights dim [out_channel, in_channel, height, width]
weight = weight.transpose( [3,2,0,1]).flatten().astype(np.float)
# input dim [N, channel, height, width]
# TODO:调整输入数据的形状
input_image = input_image.transpose( [0, 3, 1, 2]).astype(np.float)
input_data = input_image.flatten()
2.4 实验结果
图像分类耗时 | 图像分类结果 | 图像分类概率 |
---|---|---|
0.0218 | 281 | 0.4790 |
3 实验3-3-非实时图像风格迁移
3.1 实验目的
掌握深度学习的训练方法,能够使用 Python 语言基于 VGG19 网络模型实现非实时图 像风格迁移。具体包括:
- 加深对卷积神经网络的理解,利用 VGG19 模型进行图像特征提取。
- 使用 Python 语言实现风格迁移中相关风格和内容损失函数的计算,加深对非实时 风格迁移的理解。
- 使用 Python 语言实现非实时风格迁移中迭代求解风格化图像的完整流程,为后续 实现实时风格迁移并建立更复杂的综合实验奠定基础。
3.2 实验要求
• 60 分标准: 正确实现利用四重循环计算卷积层和池化层的前向传播和反向传播过程。给定卷积层和最大池化层的前向传播和反向传播输入矩阵和参数值,可以得到正确的前向传播输出矩阵和反向传播输出梯度。同时分别给出卷积层和最大池化层正确的前向传播和反向传播时间。 • 80 分标准: 正确实现内容损失函数和风格损失函数的计算,计算得出风格迁移后的图片。给定生成图像、目标内容图像和目标风格图像,可以计算得到正确的内容损失值和风格损失值;可以得到正确的内容损失和风格损失对生成图像的更新梯度,生成风格迁移后的图像。 • 100 分标准: 对第3.3节实验中介绍的卷积层和池化层的实现中使用的四重循环进行改进,提升计算速度。给定卷积层和池化层的前向传播和反向传播输入矩阵和参数值,可以得到正确的前向传播输出矩阵和反向传播输出梯度。同时分别给出卷积层和池化层正确的前向传播和反向传播时间和对应的加速比。
3.3 实验步骤及心得收获
非实时图像风格迁移,这里使用的是利用卷积神经网络实现的方法,使用了三个卷积神经网络,如下图所示。首先输人内容图像 ,和风格图像 ,而训练网络的目标,就是尽可能地将内容图像赋予风格图像的风格特征,将内容图像和风格图像分别经过 CNN 生成各自的特征图,组成内容特征集 P 和风格特征集 A。
同时初始化一张噪声图像,作为原始图像,通过 CNN 生成的特征图构成内容特征和风格特征,然后由这些特征计算目标损失函数。通过优化损失两数来调整原始图像,使内容特征与内容图像的内容接近、风格特征与风格图像的风格特征接近,经过多轮反复调整,可以使得中间图像在内容上与内容图像一致,在风格上与风格图像一致。

实验使用了经典的VGG19模型的卷积层和池化层,在前面的实验中已经手动实现了,因此该实验的重点是完成内容损失函数和风格损失函数。
1)内容损失函数
内容损失函数是原始图像和内容图像的内容特征的欧氏距离,其中 n ∈ [1, N]、 c ∈ [1,C]、 h ∈ [1, H]、 w ∈ [1,W] 均为整数,用于表示特征图上的位置:
反向传播的过程中,用 Loss 对 F 求偏导,结果如下列公式所示:
2)风格损失函数
图像风格特征利用图像每一层第
个和第 个特征进行内积运算得到
同时,风格的损失函数利用的是各层风格损失之和,因此每一层的风格损失,和风格损失的总和如下所示:
3)整体损失
而整体的损失函数就是内容损失和风格损失按照一定的比例进行求和:
当
的比例接近 1 时,生成的图像就会保留越多的内容,而 接近 0 时,生成的图像就会吸收更多的风格特征。
4)Adam 优化器
如公式(10)和公式(11)所示,一般的梯度下降法都是保持固定的学习率,这样很有可能造成学习率太大下降过快、跳出最小值范围,或学习率过大陷入局部极小值、迭代速度慢。而 Adam 优化器利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率,因此收敛速度更快,训练过程也更加平稳。
一般设置
, 。
#class AdamOptimizer
def update(self, input, grad):
# TODO: 补全参数更新过程
self.step += 1
self.mt = self.beta1 * self.mt
+ (1 - self.beta1) * grad
self.vt = self.beta2 * self.vt
+ (1 - self.beta2) * grad * grad
mt_hat = self.mt / (1 - self.beta1
** self.step)
vt_hat = self.vt / (1 - self.beta2
** self.step)
# TODO: 利用梯度的一阶矩和二阶矩的无偏估计更新风格迁移图像
output = input - self.lr * mt_hat
/ (vt_hat ** 0.5 + self.eps)
return output
5)网络结构
非实时图像风格迁移的总体网络结构如下图所示,按照这个结构补齐VGG19的初始化函数即可。

补全代码后,如下图所示:

6) 实验的优化——Img2col
实验中要求对卷积核进行优化,普通的实现是使用4重for循环,所以卷积的计算非常慢。快速卷积方法有:img2col,Winograd,FFT。其中Img2col是一种比较好理解的算法,基本思想是将卷积核相乘转化为矩阵相称,很多库已经对矩阵相乘进行了优化,因此速度要比for循环相乘快很多。
img2col的原理如下图所示:
参考《人工智能的数学基础》一书,对该过程有更清晰的认识:
3.4 实验结果
前向传播时间 | 反向传播时间 | 卷积层损失值 | 池化层损失值 |
---|---|---|---|
107.356071 ms | 107.356071 ms | 0.000000% | 0.000000% |
内容损失值(前向/反向) | 风格损失值(前向/反向) |
---|---|
['0.000000% , 0.000000%'] | ['0.025137% , 0.000000%'] |
前向传播加速比 | 反向传播加速比 |
---|---|
0.729672 | 1.057222 |
训练结果 | 内容参考图 | 风格参考图 |
---|---|---|
![]() | ![]() | ![]() |