古语有言:“它山之石可以攻玉”,迁移学习就是这么一种思想,将在其他训练集上训练好的神经网络迁移到目标任务上。自打迁移学习的思想提出后,在工业实践上,就很少有人会从头开始(随机初始化)训练网络。
1 为什么需要迁移学习
相比于从零开始训练模型,迁移学习的优越性主要体现在一下几方面:
-
加速收敛。由于已经在一些数据集上进行训练,网络已经习得一些模式,再在新数据集上训练时,权重变化较小,经过少数几轮训练即可快速看到效果,甚至不需要训练,直接使用。
-
降低对数据标注以及数据量的要求。数据标注是一件费时费力的事情,大部分的深度学习任务其实都没有能力去获取足够标注数量的数据集。通过迁移学习,可以在较少量数据集上去的更加显著的效果。
-
降低对机器性能的要求和损耗。大部分机构都没有足够条件去使用大量GPU机器去训练,但网络上目前已经有许多大公司开源的预训练模型,这些预训练模型已经在一些通用的数据集上进行训练。
2 迁移学习方案
我们把提前训练好的模型称为预训练模型,将预训练模型运用到新的任务上,主要有一下几种方案:
- 将预训练模型当做特征提取器。
以计算机视觉为例,假设先通过ImageNet数据集训练好了一个卷积网络。注意,ImageNet数据集包含1000个分类,也就是说,这个训练好的卷积网络最后的全连接层输出为1000维向量,但是我们目标人为大概率不是一千个分类,所以,在进行迁移时,我们需要删除最后一个全连接层,而保留之前的所有的隐藏层并迁移到新的网络中。在新的网络中,迁移过来的隐藏层并不会进行反向传播训练,只作为特征提取器。在迁移过来的隐藏层后,可以添加新的、符合目标任务需求的全连接层,甚至可以接SVM等等。
- 微调(fine-tuning)。
微调是说,迁移过来的隐藏层也同样参与反向传播训练,更新权重。这里,我们可以更加个性化地设置对全部隐藏层进行训练,亦或者指定某部分层进行训练。
3 迁移学习的几种应用场景
- 新数据集很小,与原始数据集类似。
由于数据集很小,进行微调并不是一个好方法,容易过拟合,切由于数据集相似度搞,最好的方案是删除神经网络的最后全连接层,添加一个新的全连接层,与新数据集中的类别数量相匹配,并随机化设置新的全连接层的权重,冻结预先训练过的网络中的所有权重,训练该网络以更新新连接层的权重。
- 新数据集很大,与原始数据集类似。
由于新的数据集足够大,出现过拟合情况可能性不大,所以删掉最后的全连接层,并替换成与新数据集中的类别数量相匹配的层级,随机地初始化新的完全连接层的权重,使用预先训练过的权重初始化迁移部分网络的权重,重新训练整个神经网络(包括迁移过来部分网络和新的全连接层)。
- 新数据集很小,但与原始数据集有很大不同。
由于数据集非常不同且数据集规模小,因此最好从网络顶部训练分类器,其中包含更多特定于数据集的特征。将全连接层,以及与全连接层毗邻的部分高层级特征提取网络删除,然后向剩下的预先训练过的层级添加新的全连接层,并与新数据集的类别数量相匹配,随机化设置新的全连接层的权重,冻结预先训练过的网络中的所有权重。
- 新数据集很大,与原始数据集有很大不同。
由于数据集非常大,重新训练也并无不可。但是,在实践中,使用预训练模型中的权重进行初始化通常仍然是很有帮助的。在这种情况下,我们将有足够的数据和信心来微调整个网络。此时,我们只需要删掉最后的完全连接层,并替换成与新数据集中的类别数量相匹配的层级,并使用随机初始化的权重重新训练网络(全连接部分)。
4 迁移学习实例
pytorch官方本身就提供有很多预训练模型,可以很方便的拿来使用。接下来,我们直接加载pytorch中的resnet18网络模型,并使用预训练权重进行初始化,对含有200多张的“蚂蚁/蜂蜜”二分类数据集进行训练。
from __future__ import print_function, division
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import torch.backends.cudnn as cudnn
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy
cudnn.benchmark = True
plt.ion() # interactive mode
<matplotlib.pyplot._IonContext at 0x7faf0bf9c730>
加载数据:
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
data_dir = './hymenoptera_data'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
data_transforms[x])
for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
shuffle=True, num_workers=4)
for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
加载到GPU中:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
device
device(type='cuda', index=0)
查看一下数据:
def imshow(inp, title=None):
"""Imshow for Tensor."""
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001)
inputs, classes = next(iter(dataloaders['train']))
# Make a grid from batch
out = torchvision.utils.make_grid(inputs)
class_names = image_datasets['train'].classes
imshow(out, title=[class_names[x] for x in classes])
定义训练函数:
def train_model(model, criterion, optimizer, scheduler, num_epochs=10):
since = time.time()
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0
for epoch in range(num_epochs):
print(f'Epoch {epoch+1}/{num_epochs}: ', end='')
# Each epoch has a training and validation phase
for phase in ['train', 'val']:
if phase == 'train':
model.train() # Set model to training mode
else:
model.eval() # Set model to evaluate mode
running_loss = 0.0
running_corrects = 0
# Iterate over data.
for inputs, labels in dataloaders[phase]:
inputs = inputs.to(device)
labels = labels.to(device)
# zero the parameter gradients
optimizer.zero_grad()
# forward
# track history if only in train
with torch.set_grad_enabled(phase == 'train'):
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
loss = criterion(outputs, labels)
# backward + optimize only if in training phase
if phase == 'train':
loss.backward()
optimizer.step()
# statistics
running_loss += loss.item() * inputs.size(0)
running_corrects += torch.sum(preds == labels.data)
if phase == 'train':
scheduler.step()
epoch_loss = running_loss / dataset_sizes[phase]
epoch_acc = running_corrects.double() / dataset_sizes[phase]
print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f} ', end='' if phase=='train' else '\n')
# deep copy the model
if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())
time_elapsed = time.time() - since
print(f'Training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s')
print(f'Best val Acc: {best_acc:4f}')
# load best model weights
model.load_state_dict(best_model_wts)
return model
model_ft = models.resnet18(weights=False) # 定义模型
model_ft.load_state_dict(torch.load('./model/resnet18-f37072fd.pth')) # 加载权重
<All keys matched successfully>
weights为True时,将会从网络中自动加载预训练好的权重,但由于网络原因,自动加载会很慢,所以设置为False,然后通过load_state_dict方法加载本地下载好的权重。
# model_ft = models.resnet18(pretrained=True)
# 提取出预训练模型中,最后的全连接层
num_ftrs = model_ft.fc.in_features
# 定义一个二分类的全连接层,并替换原来的全连接层
model_ft.fc = nn.Linear(num_ftrs, 2)
model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()
# 所有参数都要进行更新
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
num_epochs=10)
Epoch 1/10: train Loss: 0.2739 Acc: 0.8852 val Loss: 0.2041 Acc: 0.9216 Epoch 2/10: train Loss: 0.2986 Acc: 0.8770 val Loss: 0.1896 Acc: 0.9281 Epoch 3/10: train Loss: 0.3275 Acc: 0.8607 val Loss: 0.1806 Acc: 0.9150 Epoch 4/10: train Loss: 0.4008 Acc: 0.8074 val Loss: 0.2174 Acc: 0.9085 Epoch 5/10: train Loss: 0.3790 Acc: 0.8156 val Loss: 0.2254 Acc: 0.9085 Epoch 6/10: train Loss: 0.2306 Acc: 0.9016 val Loss: 0.1927 Acc: 0.9150 Epoch 7/10: train Loss: 0.2859 Acc: 0.8689 val Loss: 0.1935 Acc: 0.9281 Epoch 8/10: train Loss: 0.2926 Acc: 0.9016 val Loss: 0.1830 Acc: 0.9216 Epoch 9/10: train Loss: 0.3015 Acc: 0.8607 val Loss: 0.1743 Acc: 0.9216 Epoch 10/10: train Loss: 0.2726 Acc: 0.8566 val Loss: 0.2281 Acc: 0.9085 Training complete in 0m 20s Best val Acc: 0.928105
可见,通过迁移学习,模型经过短短几次迭代,就已经获得了非常不错的准确率。