1.迁移学习入门
什么是迁移学习:在深度神经网络算法的引用过程中,如果我们面对的是数据规模较大的问题,那么在搭建好深度神经网络模型后,我们势必要花费大量的算力和时间去训练模型和优化参数,最后耗费了这么多资源得到的模型却只能解决这一个问题,性价比较低。如果训练得到的模型能够解决同一类问题,那么模型的性价比就会提高很多,这就促使使用迁移模型解决同一类问题的方法出现。我们通过对一个训练好的模型进行细微调整,就能将其应用到相似的问题中,最后还能取得很好的效果;另外,对于原始数据较少的问题,我们也能够通过采用迁移模型进行有效的解决,所以,如果能够选取合适的迁移学习方法,则会对解决我们所面临的的问题有很大的帮助。
如果我们选择使用卷积神经网络模型解决猫狗分类问题,需要首先搭建模型,然后不断对模型进行训练,使其预测猫和狗的图片的准确性达到要求的阈值,在此过程中会消耗大量的时间在参数优化和模型训练上。之后面临类似分类问题时,可以直接使用以前已经得到的模型和模型的参数并稍加改动来满足新的需求。不过,对迁移模型需要进行重新训练,这是因为最后分类的对象发生了变化,但是重新训练的时间和搭建全新的模型进行训练的时间相比很少。通过迁移学习可以节省大量的时间和精力,而且最终得到的结果不会太差,这就是迁移学习的优势和特点。
需要注意的是,迁移学习过程中有时会导致迁移模型出现负迁移,可以将其理解为模型泛化能力恶化。加入我们将迁移学习用于解决两个毫不相关的问题,则极有可能使最后迁移得到的模型出现负迁移。
2.数据集处理
使用的数据集来自Kaggle网站上的“Dogs vs. Cats”竞赛项目,可以通过网络免费下载这些数据集。在这个数据集的训练数据集中一共有25000张猫和狗的图片,测试集中有12500张图片。
2.1 验证数据集和测试数据集
在实践中,我们不会直接使用测试数据集对搭建的模型进行训练和优化,而是在训练数据集中划出一部分作为验证集,来评估在每个批次的训练后模型的泛化能力。这样做的原因是如果我们使用测试数据集进行模型的训练和优化,那么模型最终会对测试数据集产生拟合倾向,换言之,我们的模型只有在对测试数据集中图片的类别进行预测时才有极强的准确率,而在对测试数据集以外的图片类别进行预测时会出现非常多的错误,这样的模型缺少泛化能力。所以,为了防止这种情况的出现,我们会把测试数据集从模型的训练和优化过程中隔离出来,只在每轮训练结束后使用。如果模型对验证数据集和测试数据集的预测同时具备高准确率和低损失值,就基本说明模型的参数优化是成功的,模型将具备极强的泛化能力,在本章的实践中我们分别从训练数据集的猫狗图片中各抽出2500张图片构成一个具有5000张图片的验证数据集。
2.2 数据预览
在划分好数据集后,可以进行数据预览。
import torch
import torchvision
from torchvision import datasets, transforms
import os
import matplotlib.pyplot as plt
import time
%matplotlib inline
以上代码中先导入必要的包,新增加os包和time包,os包集成了一些对文件路径和目录进行操作的类,time包主要是一些和时间相关的方法。
在获取全部数据集后,我们可以对数据进行简单分类。新建一个名为DogsVSCats的文件夹,在该文件夹下新建一个名为train和valid的子文件夹,在子文件夹下面再分别新建一个名为cat和dog的文件夹,最后将数据集中对应部分的数据放到对应名字的文件夹中,之后就可以进行数据的载入了。
data_dir = "DogsVSCats"
data_transform = {x:transforms.Compose([transforms.Scale([64,64]),
transforms.ToTensor()]) for x in ["train","valid"]}
image_datasets = {x:datasets.ImageFolder(root = os.path.join(data_dir,x)),
transform = data_transform[x] for x in ["train","valid"]}
dataloader = {x:torch.utils.data.DataLoader(dataset = image_datasets[x],
batch_size = 16,
shuffle = True)
for x in ["train","valid"]}
在进行数据的载入时我们使用torch.transforms中的Scale类将原始图片的大小统一缩放至64*64。在以上代码中对数据的变换和导入都使用了字典的形式,因为我们需要分别对训练数据集和验证数据集的数据载入方法进行简单定义,所以使用字典可以简化代码,也方便之后进行相应的调用和操作。
os.path.join来自os包,它的作用是将输入参数中的两个名字拼接成一个完整的文件路径。其他常用os.path类方法如下:
**(1)os.path.dirname:**用于返回一个目录的目录名,输入参数为文件的目录。
**(2)os.path.exists:**用于测试输入参数指定的文件是否存在
**(3)os.path.isdir:**用于测试输入参数是否是目录名
**(4)os.path.isfile:**用于测试输入参数是否是一个文件
**(5)os.path.samefile:**用于测试两个输入的路径参数是否指向同一个文件
**(6)os.path.split:**用于对输入参数中的目录名进行分割,返回一个元组,该元组由目录名和文件名组成
接下来获取一个批次的数据并进行数据预览和分析:
X_example, y_example - next(iter(dataloader["train"]))
以上代码通过next和iter迭代操作获取一个批次的装载数据,不过因为受到我们之前定义的batch_size值得影响,这一批次数据只有16张图片,所以X_example的长度也全部是16.可以通过打印这两个变量来确认。打印代码如下:
print(u"X_example 个数{}".format(len(X_example)))
print(u"y_example 个数{}".format(len(y_example)))
其中,X_example是Tensor数据类型的变量,因为做了图片大小的缩放变换,所以现在图片的大小全是64*64了,X_example的维度就是(16,3,64,64),16代表批次中有16张图片;3代表色彩通道数;64代表图片宽度和高度。
y_example也是Tensor数据类型的变量,不过其中元素全是0和1。这是因为在进行数据装载时已经对dog文件夹和cat文件夹下的内容进行了独热编码,所以这时的0和1不仅是每张图片的标签,还分别对应猫的图片和狗的图片。我们可以简单做一个打印输出,来验证这个独热编码的对应关系:
index_classes = image_datasets["train"].class_to_idx
print(index_classes)
输出结果如下:
{'cat':0,'dog':1}
这样就很明显了,猫的图片标签和狗的图片标签被独热编码后分别被数字化了,相较于使用文字作为图片的标签而言,使用0和1也可以让之后的计算方便很多。不过,为了增加之后绘制的图像标签的可识别性,我们还需要通过image_datasets[“train”].classes将原始标签的结果存储在名为example_classes的变量中。代码如下:
example_classes = image_datasets["train"].classes
print(example_classes)
example_classes其实是一个列表,而且在这个列表中只有两个元素,分别是dog和cat。我们是用Matplotlib对一个批次的图片进行绘制,具体代码如下:
img = torchvision.utils.make_grid(X_example)
img = img.numpy().transpose([1,2,0])
print([example_classes[i] for i in y_example])
plt.imshow(img)
plt.show()
3.模型搭建和参数优化
本章会先基于一个简化的VGGNet架构搭建卷积神经网络模型并进行模型训练和参数优化,然后迁移一个完整的VGG16架构的卷积神经网络模型,最后迁移一个ResNet50架构的卷积神经网络模型,并对比这三个模型在预测结果上的准确性和在泛化能力上的差异。
3.1 自定义VGGNet
首先基于VGG16架构来搭建一个简化版的VGGNet模型,要求输入图片大小为6464,而在标准的VGG16架构模型中输入图片大小为224224的;同时简化版模型删除了VGG16最后的三个卷积层和池化层,也改变了全连接层中的连接参数,这一系列的改变都是为了减少整个模型参与训练的参数数量。简化版模型的搭建代码如下:
class Models(torch.nn.Module):
def __init__(self):
super(Models, self).__init__()
self.Conv = torch.nn.Sequential(
torch.nn.Conv2d(3,64,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.Conv2d(64,64,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size = 2, stride =2),
torch.nn.Conv2d(64,128,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.Conv2d(128,128,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(kernel_size = 2, stride =2),
torch.nn.Conv2d(128,256,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.Conv2d(256,256,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.Conv2d(256,256,kernel_size =3,stride =1,padding =1),
torch.nn.ReLU()
torch.nn.MaxPool2d(kernel_size = 2, stride =2),
torch.nn.Conv2d(256,512,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.Conv2d(512,512,kernel_size = 3,stride = 1,padding = 1),
torch.nn.ReLU(),
torch.nn.Conv2d(512,512,kernel_size =3,stride =1,padding =1),
torch.nn.ReLU()
torch.nn.MaxPool2d(kernel_size = 2, stride =2),
)
self.Classes = torch.nn.Sequential(
torch.nn.Linear(4*4*512,1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(1024,1024),
torch.nn.ReLU(),
torch.nn.Dropout(p=0.5),
torch.nn.Linear(1024,2)
)
def forward(self,input):
x = self.Conv(input)
x = x.view(-1,4*4*512)
x = self.Classes(x)
return x
搭建好模型后,通过print对搭建的模型进行打印输出来显示模型中的细节,打印输出的代码如下:
model = Models()
print(model)
然后,定义好模型的损失函数和对参数进行优化的优化函数,代码如下:
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),lr = 0.00001)
epoch_n = 10
time_open = time.time()
for epoch in range(epoch_n):
print("Epoch:{}/{}".format(epoch,epoch_n-1))
print("-"*10)
for phase in ["train","valid"]:
if phase == "train":
print("Training...")
model.train(True)
else:
print("Validing...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch, data in enumerate(dataloader[phase],1):
x,y = data
x,y = Variable(x),Variable(y)
y_pred = model(x)
_,pred = torch.max(y_pred.data,1)
optimizer.zero_grad()
loss = loss_f(y_pred,y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.data
running_corrects += torch.sum(pred==y.data)
if batch%500 == 0 and phase =="train":
print("Batch {}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch, 100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{}:Loss:{:.4f} ACC:{:.4f}%".format(phase, epoch_loss,epoch_acc))
time_end = time.time() - time_open
print(time_end)
在代码中优化函数使用的是Adam,损失函数使用的是交叉熵,训练次数总共是10次。由于使用计算机的CPU进行计算,所以整个过程非常耗时。接下来我们对源代码进行调整,将需要计算的参数全部迁移至GPU上,这个过程非常简单方便,只需要重新对部分参数进行类型转换就可以了。在此之前,我们先确认GPU硬件是否可用,代码如下:
print(torch.cuda.is_available())
Use_gpu = torch.cuda.is_available()
打印结果是True则说明GPU已经具备了被使用的全部条件,如果遇到False。则说明显卡暂时不支持,如果是驱动存在问题,则最简单的方法是将显卡驱动升级到最新版本。
在完成对模型训练过程中的参数迁移后,新的训练代码如下:
if Use_gpu:
model = model.cuda()
epoch_n = 10
time_open = time.time()
for epoch in range(epoch_n):
print("Epoch {}/{}".format(epoch,epoch_n))
print("-"*10)
for phase in ["train","valid"]:
if phase == "train":
print("Train...")
model.train(True)
else:
print("Valid...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch,data in enumerate(dataloader[phase],1):
X,y = data
if Use_gpu:
X,y = Variable(X.cuda()),Variable(y.cuda())
else:
X,y = Variable(X),Variable(y)
y_pred = model(X)
_,pred = torch.max(y_pred.data,1)
optimizer.zero_grad()
loss = loss_f(y_pred,y)
if phase =="train":
loss.backward()
optimizer.step()
running_loss += loss.data
running_corrects += torch.sum(pred==y.data)
if batch%500 == 0 and phase == "train":
print()
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} ACC:{:.4f}%".format(phase,epoch_loss,epoch_acc))
time_end = time.time()-time_open
print(time_end)
以上代码中,model==model.cuda()和X, y = Variable(X.cuda()),Variable(y.cuda())就是参与迁移至GPU的具体代码。
到目前为止,我们构建的卷积神经网络模型已经具备了较高的预测准确率,接下来引入迁移学习模型。
3.2 迁移VGG16
接下来是迁移学习的具体实施过程,首先需要下载已经具备最优参数的模型,这需要对我们之前使用的model = Models()代码部分进行替换,因为我们不需要再自己搭建和定义训练的模型了,而是通过代码自动下载模型并直接调用,具体代码如下:
model = models.vgg16(prepare = True)
在以上代码中,我们指定进行下载的模型是VGG16,并通过设置prepare = True中的值为True,来实现下载的模型附带了已经优化好的模型参数。这样,迁移学习的第一步就完成了,如果想要查看迁移模型的细节,可以通过print将其打印输出。
下面进行迁移学习的第二步,对迁移过来的模型进行调整。尽管迁移学习要求我们需要解决的问题之间最好具有很强的相似性,但是每个问题对最后输出的结果会有不一样的要求,而承担整个模型输出分类工作的是卷积神经网络模型中的全连接层,所以在迁移学习的过程中调整最多的也是全连接层部分。其基本思路是冻结卷积神经网络中全连接层之前的全部网络层次,让这些被冻结的网络层次中的参数在模型的训练过程中不进行梯度更新,能够被优化的参数仅仅是没有被冻结的全连接层的全部参数。
下面看具体的代码。首先,迁移过来的VGG16架构模型在最后输出的结果是1000个,而我们的问题中只需要两个输出结果,所以全连接层必须进行调整。模型调整的具体代码如下:
for param in model.parameters():
param.requires_grad = False
model.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))
if Use_gpu:
model = model.cuda()
cost = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.classifier.parameters(),lr=0.00001)
首先,对原模型中的参数进行遍历操作,将参数中的param.requires_grad全部设置为False,这样对应的参数将不计算梯度,也不会进行梯度更新了,这就是之前说的冻结操作。然后,定义新的全连接层结构并重新赋值给model.classifier。完成了新的全连接层定义后,全连接层中的param.requires_grad参数被默认重置为True,所以不需要再次遍历参数来进行解冻操作。损失函数的loss值依然使用交叉熵进行计算,但是在优化函数中负责优化的参数变成了全连接层中的所有参数,即对model.classifier.parameters这部分参数进行优化。在调整完模型的结构之后,我们可以通过打印输出对比其与模型没有进行调整前有什么不同。可以看出最大的不同就是模型的最后一部分全连接层发生了变化。
下面是对VGG16结构的卷积神经网络模型进行迁移学习的完整代码:
import torch
import torchvision
from torchvision import datasets, models, transforms
import os
from torch.autograd import Variable
import matplotlib.pyplot as plt
import time
%matplotlib inline
data_dir = "DogsVSCats"
data_transform = {x:transforms.Compose([transforms.Scale([224,224]),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])]) for x in ["train","valid"]}
image_datasets = {x:datasets.ImageFolder(root = os.path.join(data_dir,x),
transform = data_transform[x]) for x in ["train","valid"]}
dataloader = {x:torch.utils.data.DataLoader(dataset = image_datasets[x],
batch_size = 16,
shuffle = True) for x in ["train","valid"]}
X_example,y_example = next(iter(dataloader["train"]))
example_classes = image_datasets["train"].classes
index_classes = image_datasets["train"].class_to_index
model = models.vgg16(pretrained=True)
Use_gpu = torch.cuda.is_available()
for param in model.parameters():
param.requires_grad = False
model.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))
if Use_gpu:
model = model.cuda()
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.classifier.parameters(), lr = 0.00001)
epoch_n = 5
time_open = time.time()
for epoch in range(epoch_n):
print()
print("-"*10)
for phase in ["train","valid"]:
if phase == "train":
print("Train...")
model.train(True)
else:
print("Valid...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch,data in enumerate(dataloader[phase],1):
X,y = data
if Use_gpu:
X,y = Variable(X.cuda(),Variable(y.cuda())
else:
X,y = Variable(X),Variable(y)
y_pred = model(X)
_,pred = torch.max(y_pred.data,1)
optimizer.zero_grad()
loss = loss_f(y_pred,y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.data
running_corrects += torch.sum(pred==y.data)
if batch%500 == 0 and phase == "train":
print("Batch {}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch,100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} ACC:{:.4f}%".format(phase,epoch_loss,epoch_acc))
time_end = time.time()-time_open
print(time_end)
3.3 迁移ResNet50
接下来对ResNet架构中的ResNet50模型进行迁移,进行迁移的代码为model = models.resnet50(pretrained = True)。和迁移vgg16模型类似,在代码中使用resnet50对vgg16进行替换就完成了对应模型的迁移。之后需要对resnet50的全连接层部分进行调整:
for param in model.parameters():
param.requires_grad = False
model.fc = torch.nn.Linear(2048,2)
因为resnet50中全连接层只有一层,所以对代码的调整非常简单。对resnet50进行迁移学习的完整代码如下:
import torch
import torchvision
from torchvision import datasets, models, transforms
import os
from torch.autograd import Variable
import matplotlib.pyplot as plt
import time
%matplotlib inline
data_dir = "DogsVSCats"
data_transform = {x:transforms.Compose([transforms.Scale([224,224]),
transforms.ToTensor(),
transforms.Normalize(mean=[0.5,0.5,0.5],std=[0.5,0.5,0.5])]) for x in ["train","valid"]}
image_datasets = {x:datasets.ImageFolder(root = os.path.join(data_dir,x),
transform = data_transform[x]) for x in ["train","valid"]}
dataloader = {x:torch.utils.data.DataLoader(dataset = image_datasets[x],
batch_size = 16,
shuffle = True) for x in ["train","valid"]}
X_example,y_example = next(iter(dataloader["train"]))
example_classes = image_datasets["train"].classes
index_classes = image_datasets["train"].class_to_index
model = models.resnet50(pretrained=True)
Use_gpu = torch.cuda.is_available()
for param in model.parameters():
param.requires_grad = False
model.fc = toech.nn.Linear(2048,2)
if Use_gpu:
model = model.cuda()
loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.fc.parameters(), lr = 0.00001)
epoch_n = 5
time_open = time.time()
for epoch in range(epoch_n):
print()
print("-"*10)
for phase in ["train","valid"]:
if phase == "train":
print("Train...")
model.train(True)
else:
print("Valid...")
model.train(False)
running_loss = 0.0
running_corrects = 0
for batch,data in enumerate(dataloader[phase],1):
X,y = data
if Use_gpu:
X,y = Variable(X.cuda(),Variable(y.cuda())
else:
X,y = Variable(X),Variable(y)
y_pred = model(X)
_,pred = torch.max(y_pred.data,1)
optimizer.zero_grad()
loss = loss_f(y_pred,y)
if phase == "train":
loss.backward()
optimizer.step()
running_loss += loss.data
running_corrects += torch.sum(pred==y.data)
if batch%500 == 0 and phase == "train":
print("Batch {}, Train Loss:{:.4f}, Train ACC:{:.4f}".format(batch, running_loss/batch,100*running_corrects/(16*batch)))
epoch_loss = running_loss*16/len(image_datasets[phase])
epoch_acc = 100*running_corrects/len(image_datasets[phase])
print("{} Loss:{:.4f} ACC:{:.4f}%".format(phase,epoch_loss,epoch_acc))
time_end = time.time()-time_open
print(time_end)
3.4 小结
可以发现GPU在深度学习的计算优化过程中效率明显高于CPU;迁移学习非常强大,能快速解决同类问题,对类似的问题不用再从头到尾对模型的全部参数进行优化。