第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主要导入