PyTorch计算机视觉4——迁移学习Transfer Learning详解与实现0. 前言1. 迁移学习基本概念2. 迁移学习的重要性3. ImageNet4. 迁移学习流程5. 使用 ResNet 进行迁移学习小结系列链接0. 前言在计算机视觉项目中设计合适的卷积神经网络 (Convolutional Neural Network, CNN) 模型并非易事ResNet9模型架构是针对低分辨率图像数据集最简洁高效的模型之一。对于高分辨率图像数据集则需要采用更强大的模型。现有许多基于数百万张图像训练得到的优秀预训练模型可供利用因此我们无需重复造轮子。迁移学习 (Transfer Learning) 正是运用这些预训练模型处理新数据集的图像分类或其他计算机视觉任务的有效方法。1. 迁移学习基本概念迁移学习 (Transfer Learning) 指利用已经学习好的模型在新任务上具有良好的表现和推广能力的机器学习技术能够将通用数据集上的模型学习迁移到特定数据集中。通常用于执行迁移学习的预训练模型在数百万张图像(通用大型数据集)上进行训练然后使用特定感兴趣数据集微调预训练模型。2. 迁移学习的重要性假设我们需要处理道路图像并根据图像包含的对象进行分类而从零开始构建、训练模型可能会因为图像的数量不足而难以学习到数据集中的各种变化对8,000张图像进行训练比在2000张图像上进行训练的模型准确率更高。在ImageNet上训练的预训练模型能够很好的解决该问题在对ImageNet数据集进行训练期间模型已经学习了很多与交通相关的特征例如汽车、道路、树木和人等。因为模型已经学习了大量通用特征因此利用已经训练好的模型能够更快和更准确的训练新模型只需要将预训练模型提取到的特征用于新模型的训练就可以得到适用于目标任务的性能优异的新模型。3. ImageNetImageNet是一个大规模图像数据集该数据集包含了超过1400万张标注图像覆盖了1万多个分类标签。ImageNet数据集被广泛用于计算机视觉领域中包括自动驾驶、智能监控、医学影像分析等。ImageNet 挑战赛由ImageNet项目组于2010年开始举办是计算机视觉领域的顶尖比赛之一。在本节中将使用在ImageNet数据集上预训练的深度神经网络构建迁移学习模型。4. 迁移学习流程迁移学习的一般流程如下归一化输入图像使用与预训练模型训练期间相同的均值和标准差进行归一化获取在大型数据集上进行预训练模型的架构与模型权重丢弃预训练模型的最后几层将截断的预训练模型连接到一个或多个新初始化的神经网络层并确保最后一层的神经元与需要预测的类别数(输出)相同确保预训练模型的权重不可更新(即在反向传播期间冻结预训练模型参数)但新初始化的神经网络层权重是可训练的。因为预训练模型权重已经使用大型数据集进行了很好的训练因此可以利用从大型模型中学习到的特征而无需对预训练模型进行训练而只需要利用小数据集训练新初始化的神经网络层更新可训练参数拟合模型我们已经了解了如何实现迁移学习接下来我们介绍预训练卷积神经网络架构ResNet并使用迁移学习将预训练模型应用于蔬菜图像分类任务。5. 使用 ResNet 进行迁移学习运行以下代码可以看到超过百种预训练模型包括resnet18、resnet34、resnet50、resnet101、resnet152以及vgg16等。这些预训练模型原本针对224×224尺寸图像设计本节将改造并使用resnet18、resnet50和resnet152模型进行蔬菜图像分类。importtorchvisionimporttorchvision.modelsdir(models)蔬菜图像分类项目包含三个部分第一部分负责导入数据集、构建数据加载器并展示样本图像及其标签。从 Kaggle 下载的zip文件大小约为560MB解压至VegetableImgs文件夹后可见三个子文件夹train、test和validation每个文件夹下包含15个以蔬菜种类命名的二级子文件夹。通过使用torchvision的ImageFolder工具将这些图像打包成三个数据集运行train_dataset.classes可以看到这15个类别的名称列表Bean(豆类)、Bitter_Gourd(苦瓜)、Bottle_Gourd(瓶瓜)、Brinjal(茄子)、Broccoli(西兰花)、Cabbage(卷心菜)、Capsicum(辣椒)、Carrot(胡萝卜)、Cauliflower(花椰菜)、Cucumber(黄瓜)、Papaya(木瓜)、Potato(土豆)、Pumpkin(南瓜)、Radish(萝卜)和Tomato(西红柿)。由于原始图像尺寸不一致且多数为224×224分辨率为节省训练时间我们将统一调整为128×128分辨率。importtorch;importtorch.nnasnn;fromtorch.utils.dataimportDataLoaderimporttorch.optim.lr_scheduleraslr_schedulerfromtorchvision.utilsimportmake_grid;fromtorchvisionimportmodelsfromtorchvision.datasetsimportImageFolderfromtorchvisionimporttransformsasT;frommatplotlibimportpyplotaspltimportnumpyasnp;importpandasaspd;fromtqdmimporttqdm,trange n_epochs10img_size(224,224);img_channels3batch_size64lr1e-2data_path./VegetableImgs/将三个数据集分别载入批大小为64的数据加载器下图展示了训练图像的数据样本其中部分图像因RandomVerticalFlip增强处理呈现倒置效果。第二部分包含预训练模型、损失函数、优化器及学习率调度器的配置。最后部分则用于模型训练及结果可视化。train_datasetImageFolder(rootdata_pathtrain/,transformT.Compose([T.Resize(img_size),T.RandomVerticalFlip(0.5),T.RandomHorizontalFlip(p0.5),T.ToTensor(),T.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])]))val_datasetImageFolder(rootdata_pathvalidation/,transformT.Compose([T.Resize(img_size),T.ToTensor(),T.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])]))test_datasetImageFolder(rootdata_pathtest/,transformT.Compose([T.Resize(img_size),T.ToTensor(),T.Normalize([0.5,0.5,0.5],[0.5,0.5,0.5])]))classestrain_dataset.classes;n_classlen(classes)s_p_eint(len(train_dataset)/batch_size);n_sampless_p_e*batch_size train_dataloaderDataLoader(train_dataset,batch_sizebatch_size,shuffleTrue,num_workers4,pin_memoryTrue,drop_lastTrue)val_dataloaderDataLoader(val_dataset,batch_sizebatch_size,shuffleFalse,num_workers4,pin_memoryTrue,drop_lastTrue)test_dataloaderDataLoader(test_dataset,batch_sizebatch_size,shuffleFalse,num_workers4,pin_memoryTrue,drop_lastTrue)n_batchlen(train_dataloader)forimgs,labelsintrain_dataloader:print(imgs.shape,\nlables,labels);breakdefdenorm(img_tensors):# Shift image pixel values to [0,1]returnimg_tensors*0.50.5defshow_imgs(images):fig,axplt.subplots(figsize(16,10))inputsmake_grid(denorm(images[:16]),nrow8)ax.imshow(inputs.permute(1,2,0))ax.set(xticks[],yticks[])show_imgs(imgs)默认情况下torchvision提供的所有预训练模型都适用于包含1000个类别的图像数据集。如果执行print(model)会发现resnet18模型的最后一层是一个全连接层nn.Linear(1024, 1000, biasFalse)。该层原本为包含1000个类别的ImageNet数据集设计因此我们需要针对仅含15个类别的蔬菜图像数据集修改最后一层参数。修改完成后运行print(model)会看到最终的全连接层已变为nn.Linear(1024,15, biasFalse)。我们还可以通过pretrainedTrue选项导入优化后的预训练模型参数。尝试False选项会发现分类准确度较低。如果一些代码前的#符号代码将冻结预训练模型中的所有参数。由于最后一层全连接层已被修改此时仅最后一层的参数会在模型训练过程中更新。但这种情况下的分类准确率并不理想。更新所有预训练的resnet18模型的参数时测试数据加载器的分类准确率可达约98%如下图所示。若使用resnet50和resnet101模型准确率会有小幅提升但训练时间将分别延长至两倍和三倍。modelmodels.resnet18(pretrainedTrue)print(model)model.eval()forparaminmodel.parameters():param.requires_gradFalsenum_ftrsmodel.fc.in_features model.fcnn.Linear(num_ftrs,n_class)modelmodel.cuda()criterionnn.CrossEntropyLoss(reductionsum)optimizertorch.optim.SGD(model.parameters(),lrlr)schedulerlr_scheduler.OneCycleLR(optimizer,max_lrlr,steps_per_epochs_p_e,epochsn_epochs,pct_start0.4)deftraining(my_dataloader):total_loss0.0;n_correct0.0n_sampleslen(my_dataloader.dataset)model.train()forimages,labelsinmy_dataloader:labelslabels.cuda(non_blockingTrue)outputsmodel(images.cuda(non_blockingTrue))predictionstorch.argmax(outputs,dim1)n_correcttorch.sum(predictionslabels).item()losscriterion(outputs,labels)total_lossloss.item()loss.backward()nn.utils.clip_grad_value_(model.parameters(),clip_value0.1)optimizer.step()scheduler.step()optimizer.zero_grad()returntotal_loss/n_samples,100*n_correct/n_samplesdefevaluation(my_dataloader):n_sampleslen(my_dataloader.dataset)withtorch.no_grad():total_loss0.0;n_correct0model.eval()forimages,labelsinmy_dataloader:labelslabels.cuda(non_blockingTrue)outputsmodel(images.cuda(non_blockingTrue))predictionstorch.argmax(outputs,dim1)n_correcttorch.sum(predictionslabels).item()losscriterion(outputs,labels)total_lossloss.item()returntotal_loss/n_samples,100*n_correct/n_samplesdeffitting(epochs):dfpd.DataFrame(np.empty([epochs,5]),indexnp.arange(epochs),columns[loss_train,acc_train,loss_val,acc_val,lr])progress_bartrange(epochs)foriinprogress_bar:df.iloc[i,0],df.iloc[i,1]training(train_dataloader)df.iloc[i,2],df.iloc[i,3]evaluation(val_dataloader)df.iloc[i,4]optimizer.param_groups[0][lr]progress_bar.set_description(train_loss%.5f%df.iloc[i,0])progress_bar.set_postfix({train_acc:df.iloc[i,1],val_acc:df.iloc[i,3]})returndf train_historyfitting(n_epochs)dftrain_history fig,axplt.subplots(1,3,figsize(12,3),sharexTrue)df.plot(axax[0],y[1,3],style[r-,b-d])df.plot(axax[1],y[0,2],style[r-,b-d])df.plot(axax[2],y[4],style[r-])foriinrange(3):ax[i].set_xlabel(epoch)ax[i].grid(whichmajor,axisboth,colorg,linestyle:)ax[0].set_ylabel(accuracy(%))ax[2].ticklabel_format(stylesci,axisy,scilimits(0,0))对测试数据集中指定索引的单个图像进行分类并输出该图像以供验证分类结果。loss,accuracyevaluation(test_dataloader)print(test dataloader accuracy (%),accuracy)n1300img,labeltest_dataset[n]defpredict_image(img,model):print(img.shape,img.shape)ximg.unsqueeze(0).cuda()print(img.unsqueeze(0).shape,x.shape)ymodel(x)predstorch.argmax(y,dim1)returntrain_dataset.classes[preds[0].item()]print(Prediction is: {0}; Label is {1}.format(predict_image(img,model),train_dataset.classes[label]))plt.imshow(denorm(img).permute(1,2,0))通过以下ResNet18模型的结构解析将理解预训练ResNet模型内部包含哪些代码。其中名为basic的函数包含两个卷积层该函数的输入为4D张量输出同样为4D张量但其通道数和分辨率可能与输入不同。# Simple ResNet18 ---------------------------------------------------------------importtorch.nnasnn;fromtorchsummaryimportsummarydefbasic(in_channels,out_channels,stride1):returnnn.Sequential(nn.Conv2d(in_channels,out_channels,kernel_size3,stridestride,padding1,biasFalse),nn.BatchNorm2d(out_channels),nn.ReLU(inplaceTrue),nn.Conv2d(out_channels,out_channels,kernel_size3,stride1,padding1,biasFalse),nn.BatchNorm2d(out_channels))classId_Block(nn.Module):def__init__(self,in_channels,out_channels,stride1):super().__init__()self.basic_blockbasic(in_channels,out_channels,stridestride)self.relunn.ReLU()defforward(self,x):returnself.relu(xself.basic_block(x))classRs_Block(nn.Module):def__init__(self,in_channels,out_channels,stride2):super().__init__()self.basic_blockbasic(in_channels,out_channels,stride2)self.shortcutnn.Sequential(nn.Conv2d(in_channels,out_channels,kernel_size1,stridestride,padding0,biasFalse),nn.BatchNorm2d(out_channels))self.relunn.ReLU()defforward(self,x):returnself.relu(self.shortcut(x)self.basic_block(x))classResNet(nn.Module):def__init__(self,image_channels3,num_classes10):super().__init__()self.netnn.Sequential(nn.Conv2d(in_channelsimage_channels,out_channels64,kernel_size7,stride2,padding3,biasFalse),nn.BatchNorm2d(64),nn.ReLU(),# shape -1 x 64 x 112 x 112nn.MaxPool2d(kernel_size3,stride2,padding1),Id_Block(64,64,1),Id_Block(64,64,1),Rs_Block(64,128,2),Id_Block(128,128,1),Rs_Block(128,256,2),Id_Block(256,256,1),Rs_Block(256,512,2),Id_Block(512,512,1),nn.AdaptiveAvgPool2d(1),# shape -1 x 64 x 1 x 1nn.Flatten(),nn.Linear(512,num_classes))defforward(self,imgs):#imgs.shape -1 x 3 x 224 x 224returnself.net(imgs)#ioutput.shape -1 x n_classesmodelResNet().cuda()summary(model,(3,224,224))#print details of the model Params: 42.65MB定义名为Id_Block的类包含两个分支其基础分支的输入与输出保持相同维度另一分支则直接输出输入张量本身。两个分支的输出会直接相加作为该类的最终输出。而命名为Rs_Block的类同样具有双分支结构与输入张量维度相比其基础分支(步长2)输出的通道数和分辨率会发生改变另一个名为shortcut的分支通过核大小为1的卷积层对输入张量进行变换从而使第二分支的输出维度能与基础分支输出维度匹配。这些基础模块共同构成了ResNet18模型的完整架构。ResNet18模型包含17个卷积层和1个全连接层这正是其命名为ResNet18的原因。运行代码p_list [p.numel() for p in model.parameters()]; print(sum(p_list), , p_list)将得到resnet18模型参数总量(11181642个)及各层参数数量分布。若切换至其他预训练模型并查看参数数量会发现resnet50模型的参数总量达到23528522个。通过运行以下代码我们可以打印出所有参数名称及其维度输出的首行可见名为conv1.weight的参数其维度为torch.Size([64, 3, 7, 7])这就是ResNet18模型第一个卷积层中卷积操作的权重张量。forname,parameterinmodel.named_parameters():print(name,\t\t,parameter.shape)该卷积权重张量可视为包含64个样本、3个通道、分辨率为7×7的图像批次。通过后续代码可可视化这批图像此时权重张量的每个元素值都被归一化至[0,1]区间。下图可视化了来自经过参数优化的预训练ResNet18模型可以看到层数更深的ResNet模型能够提取图像更精细的特征模式信息。fig,axplt.subplots(figsize(7,7))wmodel.conv1.weight.cpu()min_wtorch.min(w)w1(-1/(2*min_w))*w0.5img_gridmake_grid(w1,nrow8,padding1)ax.imshow(img_grid.permute(1,2,0))小结本节介绍了迁移学习的概念、重要性及实现流程并基于ResNet预训练模型完成了蔬菜图像分类任务。迁移学习通过利用在ImageNet等大型数据集上预训练的模型权重有效解决了小样本数据集训练困难的问题。实验表明微调resnet18模型在测试集上可达约98%的分类准确率。最后通过可视化模型参数展示了预训练模型各层提取的特征模式加深了对深度网络内部机制的理解。系列链接PyTorch计算机视觉1——计算机视觉的数学工具PyTorch计算机视觉2——神经网络模型训练与PyTorch基础PyTorch计算机视觉3——卷积神经网络CNN详解与实现