第5 章
若干经典CNN 预训练模型及其迁移方法 
顾名思义,预训练模型(Pre-trainingModel)是在训练数据充足的数据集上训练出来的
性能优越的大模型,可以为下游任务提供支持。大规模的CNN 预训练模型具有强大的特
征抽取能力和表达能力,对解决复杂问题具有明显的优势。但是,训练大的预训练模型需要
具备一定的条件,比如,需要带标注的大数据和大算力的支撑。对大规模数据的标注,其本
身就是一件耗时的工程,而且大规模数据的获取也是一件不容易的事情;大算力的构建往往
只有那些大的专业公司才能完成。因此,为了解决一个小问题,而去训练一个大模型是不现
实的。但是,我们可以利用那些已经训练好了的且已经公开发布的模型(预训练模型)来解
决我们面临的问题,这就涉及预训练模型的迁移和微调方法。通过迁移和微调,我们可以
“站在巨人的肩膀上”去解决问题,从而达到事半功倍的效果。
5.1 一个使用VGG16的图像识别程序
在例4.3中,我们从“零”开始编写了一个识别猫、狗图像的程序。在本节中,以VGG16 
为基础,编写一个待学习参数非常少的图像识别程序,而且用的训练数据也很少,主要目的
是让读者对已有预训练模型的使用和微调方法有一个初步的了解。
5.1.1 程序代码
在下面例子中,通过对预训练模型VGG16进行微调,构建一个能够识别猫狗图像的深
度神经网络。该网络需要学习的参数比较少,使用的训练数据也很少,但效果更佳。
【例5.1】 以VGG16作为预训练模型,通过微调,创建一个能够识别猫狗图像的深度
神经网络。
本例的任务与例4.3的任务一样,都是识别猫和狗的图像。不同的是,本例使用了预训
练模型———VGG16,这样使用的训练数据就相对少得多。在本例中,训练图像位于./data/ 
catdog/training_set2目录下,猫和狗的图像各1000张,共有2000张图像作为训练数据,它
们都是从./data/catdog/training_set目录中随机抽取,但测试集不变(与例4.3一样,位于./ 
data/catdog/test_set目录下,一共有2023张)。
本程序首先导入VGG16,然后冻结参数并修改模型的部分结构,以适合本例的任务,最
后进行训练和测试。程序的全部代码如下: 
from torchvision import datasets, transforms, models 
import torch 
import torch.nn as nn

1 20 深度学习理论与应用 
from torch.utils.data import DataLoader,Dataset 
from PIL import Image 
import os 
import time 
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 
#------------------------------------- 
transform = transforms.Compose([ 
transforms.Resize((224,224)), #调整图像大小为(224,224) 
transforms.ToTensor(), #转换为张量
]) 
class cat_dog_dataset(Dataset): 
def __init__(self, dir): 
self.dir = dir 
self.files = os.listdir(dir) 
def __len__(self): #需要重写该方法,返回数据集大小 
return len(self.files) 
def __getitem__(self, idx): 
file = self.files[idx] 
fn = os.path.join(self.dir, file) 
img = Image.open(fn).convert('RGB') 
img = transform(img) #调整图像形状为(3,224,224),并转换为张量 
img = img.reshape(-1,224,224) 
y = 0 if 'cat' in file else 1 #构造图像的类别 
return img,y 
#============================================= 
batch_size = 20 
train_dir = './data/catdog/training_set2' #训练集所在的目录
test_dir ='./data/catdog/test_set' #测试集所在的目录
train_dataset = cat_dog_dataset(train_dir) #创建数据集
train_loader = DataLoader(dataset=train_dataset, #打包 
batch_size=batch_size, 
shuffle=True) 
test_dataset = cat_dog_dataset(test_dir) 
test_loader = DataLoader(dataset=test_dataset, 
batch_size=batch_size, 
shuffle=True) 
print('训练集大小: ',len(train_loader.dataset)) 
print('测试集大小: ',len(test_loader.dataset)) 
#================================ 
cat_dog_vgg16 = models.vgg16(pretrained=True).to(device) 
for i,param in enumerate(cat_dog_vgg16.parameters()): 
param.requires_grad = False #冻结cat_dog_vgg16 中已有的所有参数
cat_dog_vgg16.classifier[3]= nn.Linear(4096,1024) #其参数默认是可学习的
cat_dog_vgg16.classifier[6]= nn.Linear(1024,2) #其参数默认是可学习的
cat_dog_vgg16.train() 
cat_dog_vgg16 = cat_dog_vgg16.to(device) 
optimizer = torch.optim.SGD(cat_dog_vgg16.parameters(), lr=0.01, momentum=0.9) 
start=time.time() #开始计时
cat_dog_vgg16.train() 
for epoch in range(10): #执行10 代

第5 章 若干经典CNN 预训练模型及其迁移方法1 21 
ep_loss=0 
for i,(x,y) in enumerate(train_loader): 
x, y = x.to(device),y.to(device) 
pre_y = cat_dog_vgg16(x) 
loss = nn.CrossEntropyLoss()(pre_y, y.long()) #使用交叉熵损失函数 
ep_loss += loss*x.size(0) #loss 是损失函数的平均值,故要乘以样本数量 
optimizer.zero_grad() 
loss.backward() 
optimizer.step() 
print('第%d 轮循环中,损失函数的平均值为: %.4f'\ 
%(epoch+1,(ep_loss/len(train_loader.dataset)))) 
end = time.time() #计时结束
print('训练时间为: %.1f 秒'%(end-start)) 
#============================================= 
correct = 0 
cat_dog_vgg16.eval() 
with torch.no_grad(): 
for i, (x, y) in enumerate(train_loader): #计算在训练集上的准确率 
x, y = x.to(device), y.to(device) 
pre_y = cat_dog_vgg16(x) 
pre_y = torch.argmax(pre_y, dim=1) 
t = (pre_y == y).long().sum() 
correct += t 
t = 1.*correct/len(train_loader.dataset) 
print('1. 网络模型在训练集上的准确率: {:.2f}%'\ 
.format(100*t.item())) 
correct = 0 
with torch.no_grad(): 
for i, (x, y) in enumerate(test_loader): #计算在测试集上的准确率 
x, y = x.to(device), y.to(device) 
pre_y = cat_dog_vgg16(x) 
pre_y = torch.argmax(pre_y, dim=1) 
t = (pre_y == y).long().sum() 
correct += t 
t = 1.*correct/len(test_loader.dataset) 
print('2. 网络模型在测试集上的准确率: {:.2f}%'\ 
.format(100*t.item())) 
执行上述代码,输出结果(部分)如下: 
… … 
第9 轮循环中,损失函数的平均值为: 0.0460 
第10 轮循环中,损失函数的平均值为: 0.0553 
训练时间为: 86.4 秒
1. 网络模型在训练集上的准确率: 99.70% 
2. 网络模型在测试集上的准确率: 96.69% 
可见,与例4.3相比,该程序的训练数据少了,运行的代数也少了,但准确率却大幅上升
了。显然,这得益于预训练模型VGG16的功劳,是站在VGG16这个“巨人肩膀”上的结果。

1 22 深度学习理论与应用
5.1.2 代码解释
本例主要是导入了一个预训练模型———VGG16,创建实例cat_dog_vgg16,以代替在例
4.3中创建的实例model_CatDog,其他部分代码基本相同。相关代码说明如下: 
(1)通过下面语句从模型库models中导入已经训练好的模型VGG16。 
cat_dog_vgg16 = models.vgg16(pretrained=True) 
其中,pretrained=True表示要下载已训练好的所有参数。如果pretrained=False,则
表示不下载这些参数,而使用随机方法初始化所有参数。这相当于只使用模型VGG16的
结构,而不要其训练过的参数。显然,一般情况下pretrained=True。
如果想导入VGGNet的另一个家族成员———VGG19,则用下列语句即可。 
cat_dog_vgg19 = models.vgg19(pretrained=True) 
注意,此处的cat_dog_vgg16就是相当于例4.3中的model_CatDog,都是已经创建好的
实例。因此,在本例中可以不再创建一个类。
(2)使用下列语句冻结刚创建的模型cat_dog_vgg16的参数。 
for i,param in enumerate(cat_dog_vgg16.parameters()): 
param.requires_grad = False #冻结cat_dog_vgg16 的所有参数
如果一个参数的requires_grad属性值设置为False,则该参数在训练过程中是不能被
更新的,因而称为“冻结”。由于VGG16中的参数都是训练过的,且已被实践证明是可行
的,因而就不需要再训练了,而且VGG16中的参数量巨大,一般也没有条件来训练它们。
用下列代码,可以查看模型中各层参数张量是否可以被训练。 
for layer in cat_dog_vgg16.named_modules(): 
t = list(layer[1].parameters()) 
if len(t) == 0: #如果当前层没有训练参数,则len(t) = 0 
continue 
L = [] 
for param in layer[1].parameters(): 
L.append(param.requires_grad) 
print(layer[0], ' ------------> ',L) 
#True 表示相应参数张量可训练,False 表示不可以
(3)对模型cat_dog_vgg16进行微调,改为适合本例识别任务的网络结构。先用下列
语句打印出cat_dog_vgg16的层次结构: 
print(cat_dog_vgg16) 
结果如图5-1所示。从图5-1中可以看出,该网络有1000个输出,而本程序只需要两个
输出,因而至少需要更改最后一层网络的输出结构。作为例子,本例修改最后面的两个全连
接层,即修改下面这两层: 
(3):Linear(in_features=4096,out_features=4096,bias=True) 
(6):Linear(in_features=4096,out_features=1000,bias=True) 
修改后的VGG16结构如下:

第 
5 
章 若干经典CNN 
预训练模型及其迁移方法123 


图5-
1 
VGG16 
结构的层次图


1 24 深度学习理论与应用 
使用的修改代码如下: 
cat_dog_vgg16.classifier[3]= nn.Linear(4096,1024) #其参数默认是可学习的
cat_dog_vgg16.classifier[6]= nn.Linear(1024,2) #其参数默认是可学习的
注意,只有这两层发生改变,且其参数也是默认可学习的(即这两层参数的requires_ 
grad属性值默认为True),而其他网络层都保持不变,它们的参数已被冻结。
(4)在加载数据时,以./data/catdog/training_set2目录下的图像文件作为训练数据,训
练的代数改为10代。
除了上述改变外,数据加载方法、模型训练方法和测试方法等其他部分与例4.3的
相同。
5.2 经典卷积神经网络的结构
上一节已经见证了预训练模型VGG16的魅力。本节将介绍包括VGG16在内的若干
经典预训练模型的结构,一方面可以为今后模型结构设计提供参考,另一方面也可以为更好
地通过微调方法利用这些模型作准备。
5.2.1 卷积神经网络的发展过程
神经网络的出现可以追溯到1943年。当年,心理学家WarrenMcCulloch和数理逻辑
学家WalterPitts首先提出了人工神经网络的概念,并给出了人工神经元的数学模型,从此
开启了人工神经网络研究的时代。1957年,美国神经学家FrankRosenblatt成功地在IBM 
704机上完成了感知器的仿真,并于1960年实现了手写英文字母的识别。1974年,Paul 
Werbos在其博士论文中首次提出后向传播(Backpropagation,BP)思想来修正网络参数的
方法,这是BP算法的雏形。但在当时由于人工智能正处于发展的低谷,这项工作并没有引
起足够的重视。1986年,在Meclelland和Rumelhart等的努力下,BP算法被进一步发展, 
并逐步引起广泛关注,被大量应用于神经网络训练任务当中。BP算法的主要贡献在于,提
出一种基于梯度信息的参数修正算法,为神经网络的训练提供了一种非常成功的参数训练
方法。目前,正在盛行的深度学习中各种网络模型也均采用1986年提出的BP算法。
最早的卷积神经网络是由LannYeCun等于1998年提出来的,这就是LeNet。LeNet 
主要用于识别手写数字图像,由两个卷积层和两个池化层组成,结构比较简单,但它是最早
达到实用水平的神经网络。如今,真正掀起深度学习风暴的是LeNet的加宽版——— 
AlexNet。AlexNet是于2012年由Hinton的学生KrizhevskyAlex提出来的,并在当年的
ImageNet视觉挑战赛(ImageNetLargeScaleVisualRecognitionChallenge,ILSVRC)上以
巨大的优势获得冠军。相比于以往战绩,AlexNet大幅降低了图像识别错误率,它的出现标
志着深度学习时代的来临。
2014年,GoogLeNet和VGG同时诞生。GoogLeNet是当年的ILSVRC冠军,通过设
计和开发Inception模块,使得模型的参数大幅减少。VGG则继续加深网络,通过扩展网络
的深度来获取性能的提升。
2015年,残差神经网络ResNet诞生,并在当年获得ILSVRC冠军。ResNet旨在解决
网络因深度增加而出现性能退化的问题,它提供了一种构造大深度卷积网络的技术和方法。

第 
5 
章 若干经典CNN 
预训练模型及其迁移方法125 

2019年,谷歌公司开发了一种以效率著称的深度神经网络———EficientNet。
EficientNet仍然是至今为止最好的图像识别网络之一。

5.2.2 
AlexNet 
网络
在结构上,AlexNet要比LeNe复杂得多,它由5个卷积层、3个最大池化层、2个归一
化层和3个全连接层组成。

在第一层(卷积层1)中,输入图像的尺寸为227×227×3,采用11×11卷积核,设置的
输出通道数为96 、步长为4,因而在该层输出时,特征图的大小为(227-11)/4+1=55,输出
特征图的形状为(55×55×96 )。

在第二层(池化层1)中,输入的特征图就是上一层的输出,其尺寸为227×227×3,该
层采用3×3池化核,步长为2,因而输出特征图的尺寸为(55-3)/2+1=27,从而该层输出
特征图的形状为27×27×96(池化层不改变通道数)。

其他层输出的特征图的形状变化可以依此类推,具体操作和输出特征图的形状变化如
表5-1所示。

表5-
1 
AlexNet网络的层次结构

网络层输入形状操作(等效操作) 输出形状特征图大小计算当前层中的参数量
卷积层1 227×227×311×11卷积核,输出通
道数为96,步长为4 55×55×96 (227-11)/4+ 
1=55 
96×3×11×11+96 
=34944 
池化层1 55×55×96 3×3池化核,步长为2 27×27×96 (55-3)/2+1 
=27 0 
归一化层0 
卷积层2 27×27×96 
5×5卷积核,输出通道
数为256,步长为1,填
充为2 
27×27×256(27-5+2× 
2)/1+1=27 
256×96×3×3+ 
256=221440 
池化层2 27×27×2563×3池化核,步长为2 13×13×256(27-3)/2+1 
=27 0 
归一化层0 
卷积层3 13×13×256 
3×3卷积核,输出通道
数为384,步长为1,填
充为1 
13×13×384(13-3+2× 
1)/1+1=13 
384×256×3×3+ 
384=885120 
卷积层4 13×13×384 
3×3卷积核,输出通道
数为384,步长为1,填
充为1 
13×13×384(13-3+2× 
1)/1+1=13 
384×384×3×3+ 
384=1327488 
卷积层5 13×13×384 
3×3卷积核,输出通道
数为256,步长为1,填
充为1 
13×13×256(13-3+2× 
1)/1+1=13 
256×384×3×3+ 
256=884992 
池化层3 13×13×2563×3池化核,步长为2 6×6×256 (13-3)/2+1 
=6 0 


126
深度学习理论与应用

续表

网络层输入形状操作(等效操作) 输出形状特征图大小计算当前层中的参数量
扁平化6×6×256 将特征图向量化9216 0 
全连接层1 9216 全连接4096 9216×4096+4096 
=37752832 
全连接层2 4096 全连接4096 4096×4096+4096 
=16781312 
全连接层3 4096 全连接1000 4096×1000+1000 
=4097000 

按照第2章介绍的方法,我们可以计算AlexNet的参数总量为61975936,即AlexNet 
有六千多万个参数需要优化。

5.2.3 
VGGNet 
网络
VGGNet是牛津大学Simonyan等提出的一种深度神经网络结构,其中比较常用的结
构是VGG16,其次是VGG19 。作为一个例子,下面主要介绍VGG16 网络的层次结构和
特点。

VGG16 有13 个卷积层和3个全连接层,这些都是带有待优化参数的网络层,共16 个
网络,因而称为VGG16 。VGG16 网络的层次结构如表5-2所示。

表5-
2 
VGG16 
网络的层次结构

网络层输入形状操作(等效操作) 输出形状特征图大小计算当前层中的参数量
卷积层1 
卷积层2 
(3,224,224) 
(64,224,224)
3×3 卷积核,输出通
道数为64 
3×3 卷积核,输出通
道数为64 
(64,224,224)
(64,224,224) 
224-3+2×1+ 
1=224 
同上
64×3×3×3+64 
=1792 
64×64×3×3+64 
=36928 
池化层1 (64,224,224)2×2 池化核,步长为2(64,112,112)224/2=112 0 
卷积层3 (64,112,112)3×3 卷积核,输出通
道数为128 (128,112,112)112-3+2×1+ 
1=112 
128×64×3×3+ 
128=73856 
卷积层4 (128,112,112)3×3 卷积核,输出通
道数为128 (128,112,112)同上128×128×3×3+ 
128=147584 
池化层2 (128,112,112)2×2 池化核,步长为2(128,56,56) 112/2=56 0 
卷积层5 (128,56,56) 3×3 卷积核,输出通
道数为256 (256,56,56) 56-3+2×1+1 
=56 
256×128×3×3+ 
256=295168 
卷积层6 (256,56,56) 3×3 卷积核,输出通
道数为256 (256,56,56) 同上256×256×3×3+ 
256=590080 
卷积层7 (256,56,56) 3×3 卷积核,输出通
道数为256 (256,56,56) 同上256×256×3×3+ 
256=590080 
池化层3 (256,56,56) 2×2 池化核,步长为2(256,28,28) 56/2=28 0 


第5 章 若干经典CNN 预训练模型及其迁移方法1 27 
续表
网络层输入形状操作(等效操作) 输出形状特征图大小计算当前层中的参数量
卷积层8 (256,28,28) 3×3 卷积核,输出通
道数为512 (512,28,28) 28-3+2×1+1 
=28 
512×256×3×3+ 
512=1180160 
卷积层9 (512,28,28) 3×3 卷积核,输出通
道数为512 (512,28,28) 同上512×512×3×3+ 
512=2359808 
卷积层10 (512,28,28) 3×3 卷积核,输出通
道数为512 (512,28,28) 同上512×512×3×3+ 
512=2359808 
池化层4 (512,28,28) 2×2池化核,步长为2 (512,14,14) 28/2=14 0 
卷积层11 (512,14,14) 3×3 卷积核,输出通
道数为512 (512,14,14) 14-3+2×1+1 
=14 
512×512×3×3+ 
512=2359808 
卷积层12 (512,14,14) 3×3 卷积核,输出通
道数为512 (512,14,14) 同上512×512×3×3+ 
512=2359808 
卷积层13 (512,14,14) 3×3 卷积核,输出通
道数为512 (512,14,14) 同上512×512×3×3+ 
512=2359808 
池化层5 (512,14,14) 2×2池化核,步长为2 (512,7,7) 14/2=7 0 
全连接层17×7×512= 
25088 全连接4096 25088×4096+4096 
=102764544 
全连接层24096 全连接4096 4096×4096+4096 
=16781312 
全连接层34096 全连接1000 4096×1000+1000 
=4097000 
Softmax层1000 计算概率分布1000 0 
从表5-2中可以看出,VGG16全部采用3×3卷积核(步长均为1)和2×2池化核(步
长均为2),在卷积时均填充数为1(即填充1个0圈)。AlexNet采用大的卷积核,以扩大其
感受野,因此层次不需要很高。与AlexNet相比,VGG16采用小卷积核和小池化核,各层
的参数不多,但堆叠了13层3×3卷积核。底层卷积核的感受野确实不大,但高层的感受野
同样很大,而且层与层之间的非线性映射可以提高对底层特征学习的抽象能力。总体而言, 
AlexNet显得“矮胖”,宽度大;VGG16则比较“高瘦”,深度大,VGG16参数总量为138357 
544,是AlexNet两倍多,其性能当然也比AlexNet好得多。
注意,在图5-1所示的VGG16的结构中,第37行所示的网络层是第一个全连接层。该
层要求输入张量的最后一维的大小必须为25088。然而,VGG16可以接收不同尺寸图像的
输入,从而卷积网络部分会产生不同尺寸的特征图(第33行所表示的网络层的输出)。那
么,VGG16是如何把不同尺寸的特征图都转换为最后一维的大小为25088的张量呢? 这
主要依赖于第33行所示的自适应平均池化层。该层对应的代码如下: 
nn.AdaptiveAvgPool2d(output_size=(7, 7)) 
其作用是,对输入该层的特征图,不管图像尺寸为多少,其输出特征图的尺寸永远为

1 28 深度学习理论与应用
7×7(批量大小和通道数不变,通道数为512)。这样,经过扁平化后得到输入全连接网络层
的维度大小为7×7×512=25088。也就是说,自适应平均池化层保证了VGG16可以接收
不同尺寸图像的输入,而不需改变网络的结构。读者也可以在自己构建的模型中使用自适
应平均池化层,以使得网络可以接收不同尺寸图像的输入。
5.2.4 GoogLeNet 网络与1×1 卷积核
一般来说,如果一个网络越宽(卷积核数量增加)、越深(深度增加),那么它的参数就越
多,就能解决越复杂的问题。但带来的问题也是明显的:一是在训练数据有限的情况下,容
易造成过拟合,无法真正解决问题;二是极大地增加计算量,需要更强的算力支撑。自然地, 
在保持同样宽度和高度的情况下,如何尽可能地减少网络参数的个数呢? 而这就是
GoogLeNet要解决的主要问题。为此,GoogLeNet使用了许多关键技术,其中很重要的技
术就是设计了1×1卷积核。下面先看看1×1卷积核的作用。
从nn.Conv2d()函数看,1×1卷积核对应的函数如下: 
nn.Conv2d(in_channels, out_channels, (1, 1)) 
其中,默认步长为1,无填充。
假设输入特征图的高和宽分别为H 和W ,则在此卷积核作用下输出特征图的高和宽
分别为H -1+1=H 和W -1+1=W 。也就是说,在1×1卷积核作用下,卷积后特征图的
高和宽均保持不变。但根据卷积的定义,特征图中各个元素是各通道上对应元素的加权和, 
因此输出特征图对输入特征图进行了一种线性变换。重要的是,虽然in_channels是由输入
特征图确定的,但out_channels可以根据需要自由设置。如果设置结果是out_channels< 
in_channels,那么这种设置是对输入特征图的压缩,可以理解为降维;如果out_channels> 
in_channels,那么这种设置是对输入特征图的扩张,可以理解为升维。也就是说,1×1卷积
可以起到升维和降维的作用,同时也对输入特征图进行了一种线性加权变换。简单地理解, 
1×1卷积是通过线性变换改变特征图的通道数,从而起到升维和降维的作用。
GoogLeNet这个名字可以理解为Google+LeNet,意指是谷歌公司在LeNet的基础上
发展出来的。GoogLeNet有两个特点,一个是GoogLeNet由9个称为Inception的模块构
成,另一个是有3个softmax输出层。下面先介绍第一个特点。
Inception模块经过了几个版本演进,分别是原始版本、v1、v2和v3。Inception原始版
本和Inceptionv1的结构分别如图5-2(a)和图5-2(b)所示。原始版本是由并列的3个卷积
层和1个池化层构成,分别是:1×1卷积层、3×3卷积层、5×5卷积层和3×3最大池化层。
每个卷积层设置有多个卷积核,卷积核个数即为该卷积层的输出通道数,池化层的通道数保
持不变。在Inception模块中,所有这些通道被叠加在一起作为该Inception模块的输出通
道。之所以用不同大小的卷积核,是因为这些不同尺寸的卷积核可以提取不同粒度的特征, 
实现多尺度特征提取,目的是充分利用不同粒度的特征,这是GoogLeNet的创新之一。
我们注意到,GoogLeNet并没有使用原始版本的Inception模块,而是使用了如图5-2(b) 
所示的带降维的Inceptionv1。实际上,后者是前者的改进版本,改进的结果体现在减少了
神经网络的参数量,从而提高运行效率。与原始版本相比,带降维的Inceptionv1主要导入