不管是在机器学习的哪一个领域中,迁移学习的作用都不可小觑。在工业生产中使用迁移学习的方法,可以使得一个模型稍加修改就可以应对多种类似的问题。节约了极大的时间成本。在这个小项目中,我们将迁移VGG16网络来训练Kaggle的猫与狗数据集。目标是能让我们迁移学习出来的网络可以高效的识别训练和测试集中的猫与狗。数据集可以直接移步到kaggle的官方网站上下载。传送门
迁移学习的理论知识在这里就不在多赘述了。那么如何可以在代码中实现迁移学习呢?为了让一个网络可以用于与它原本任务相似的任务上,我们通常会对网络进行一些小修改。迁移学习所学习的就是我们更改的部分。所以我们在代码实现的时候,应该将没有修改的地方冻结,这样就不会影响没有修改的网络部分的参数。
接下来就直接进入代码环节。首先这个实现是使用的pytorch。另外非常建议使用GPU来训练这个网络,因为CPU的训练速度实在是太慢了!!首先先对数据集的文件位置做一些处理,将测试集和训练集分来放入一个新的文件夹中,建议直接在当前目录下新建一个文件夹,然后将测试和训练数据集放入这个文件夹中。个人将测试和训练文件夹的名字分别改成了 ‘test_sets’ 和 ‘training_sets’。如下如所示:
然后对文件进行加载。
首先先导入必要的辅助包:
from torchvision import transforms,models,datasets
import os
import torch
from torch.autograd import Variable
import numpy as np
os模块会用于合并文件夹路径。在加载之前,首先应该了解两点。pytorch是将图片以张量的形式读入,所以,读入的图片使用张量表示。其次标准的VGG16网络对输入照片的尺寸是有规定的,图片必须以224*224的大小进入网络,不然会出现网络输出与标签的维度不同的错误。
知晓这两点之后,我们首先先规定好读入图片的格式以及大小:
transforms = {
x:transforms.Compose([transforms.Resize((224,224)),
transforms.ToTensor()]) for x in['training_sets','test_sets']}
使用字典的方式让我们可以更容易的使用测试集与训练集。上述代码相当于同时为两个文件夹下图片,定义好了读入的大小和形式。但是这个时候并没有任何的图片被读入,这是提前定义好的。
在定义好了读入文件的一些属性之后,就是真正开始读取图片的时候了:
image_dataset = {
x:datasets.ImageFolder(root=os.path.join(path,x),
transform=transforms[x]) for x in ['training_sets','test_sets']}
在这一句代码完成之后,python就已经找到了存放这些图片的文件夹,并将它们以我们想要的形式读入。
我们可以看看image_dataset 到底是什么类别:
print(type(image_dataset))
'dict'>'
在这个字典中,每一个键对应的值都是一个迭代器。因为图片很多不可能将图片全部读入放入内存中。我们可以读出第一章图片:
x,y = next(iter(image_dataset['training_sets']))
这里x存储的就是第一章图片的张量,y使其对应的标签,是猫还是狗。我们可以将读取出来的第一张照片使用OpenCV读出:
x,y = next(iter(image_dataset['training_sets']))
x = x.numpy().reshape(x.shape[1],x.shape[2],x.shape[0])
cv2.imshow('first_img:',x)
cv2.waitKey()
因为x在这里的类型的tensor,如果想使用CV2来说进行绘图,首先应该将tensor转换成ndarray。除此之外,还要注意x的形状,x原本的形状是(通道数,宽,高),但是CV2想要的是(宽,高,通道数),对这些进行一点修改,然后我们可以得到以下的图片:
这就可以说明数据集的导入已经成功了。但是我们知道训练网络不能一次性将所有的训练集都送入到网络中,我们需要将数据分成一个一个的batch。所有的batch也将有迭代器的形式进行存储。我们们先提前定义好一个batch_size:
batch_size_ = 16
然后在定义迭代器:
dataloader = {
x:torch.utils.data.DataLoader(dataset=image_dataset[x],
batch_size=batch_size_,
shuffle=True) for x in ['training_sets','test_sets']}
这样一来所有的图片就被分成了很多个大小为16的批。数据准备工作完成之后,我们就可以对网络进行设计了。首先我们先下载VGG16网络和已经训练好的参数。
VGG = models.vgg16(pretrained=True)
观察VGG16网络的结构可以发现,它的卷积层不需要做任何的修改,主要是修改它的全连接层,因为原本的VGG16网络它是用于1000个类别的分类,但是我们这里的任务目标就只用两种。所以我们需要重写VGG16网络的全连接部分。但是在做这个之前,我么需要冻结网络的参数,保证其他的网络参数不会改变。
冻结网络参数:
for param in VGG.parameters():
param.requires_grad = False
可以看出冻结网络参数的意思就是不在对其进行更新,因为我们不在对它求梯度了。然后就是重写VGG16的全连接层,将网络变成一个适合于2分类的问题。
VGG.classifier = torch.nn.Sequential(
torch.nn.Linear(25088,4096),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(4096,4096),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(4096,2)
)
‘classifier’ 是原生的VGG16网络中对全连接成的命名,因为全连接层的作用就是分类。可以发现唯一的改动就是将最后的输出个数从1000变成了2。改动之后的部分自动解除冻结,所以我们不需要手动解冻。
接下来就是对网络进行训练了。首先先将模型迁移至GPU,
if use_GPU:
VGG = VGG.cuda()
然后设置损失函数和优化器,这里使用‘adam’作为优化器,交叉熵作为损失函数:
cost = torch.nn.CrossEntropyLoss()
otpimizer = torch.optim.Adam(VGG.parameters(),lr=0.00001)
然后就是正式的训练阶段:
定义10个epoch:
n_epoch = 10
模型只在使用训练集的时候进行训练,在使用验证集的时候不训练。
for epoch in range(n_epoch):
print(f'epoch{epoch}/{n_epoch}')
batch_cost = 0.0 #用于记录每一个batch的损失
batch_correct = 0 #用于记录每一个batch预测对了几张图片
for state in ['training_sets','test_sets']: #训练和验证交替进行
if state =='training_sets':
print('training...')
VGG.train(True)
else:
print('validating...')
VGG.train(False)
for batch,data in enumerate(dataloader[state],1):
x,y = data #读取出图片以及标签
x,y = Variable(x.cuda()),Variable(y.cuda()) #将变量迁移至GPU
y_pred = VGG(x) #开始前向传播
_,pred = torch.max(y_pred,1) #最高概率的预测
loss = cost(y_pred,y) #计算损失
otpimizer.zero_grad() #每一个batch的梯度清零,不然梯度会一直累积
if state =='training_sets': #只在训练状态下,进行反向传播以及梯度更新
loss.backward()
otpimizer.step()
#print out batch loss and corrects per batch
batch_cost += loss.data
batch_correct += torch.sum(pred==y.data)
print(f'batch:{batch},loss:{loss},acc:{100*batch_correct/(batch_size_*batch)}')
迁移VGG16网络来完成猫与狗分类的效果如下图:
可以看出迁移VGG16网络来学习分类任务的效果是非常好的。
迁移学习的价值就在这里,他可以将一些十分优秀的网络迁移到当前面临的问题上来,但是迁移学习可能会出现负迁移的情况。这种情况发生有可能说明,迁移的网络和当前的任务类型差距太大,网络不适合当前的任务。这时候需要重新选择一个合适的网络进行迁移。