迁移学习简介与实战

  迁移学习(transfer learning)通俗来说,就是运用已有的知识来学习新的知识,用成语来说就是举一反三。很多场景下,直接对目标从头开始学习成本太高,这时候我们可以运用已有的知识辅助尽快得学习新知识。本文主要对迁移学习进行了介绍,提供了一些资料,最后在实战中利用迁移学习实现了图像分类。

简介

  迁移学习(transfer learning)通俗来说,就是运用已有的知识来学习新的知识,用成语来说就是举一反三。很多场景下,直接对目标从头开始学习成本太高,这时候我们可以运用已有的知识辅助尽快得学习新知识。
  比如已经会下中国象棋,就可以类比着来学习国际象棋。又比如计算机视觉领域,我们训练一个超大的神经网络是需要耗费大量的时间,同时对硬件也有一定的要求;但是如果我们使用已经训练好的深层卷积网络,把网络参数做为初始值直接训练我们的目标数据,或者固定卷积层作为固定的特征提取器,仅重新训练最后一层用于分类的全连接层,就相当于我们在一个全局相对较优的位置来对我们的目标数据进行训练。这样不仅速度快,而且可以获得较为不错的分类效果。迁移学习实际上是很系统的一个领域,如果想要对迁移学习有一个较为完整的认识,可以阅读经典综述论文:A survey on transfer learning。另外,感谢jindongwang@github的博文,这里非常全面得搜集了关于迁移学习的知识面,非常值得学习!

深度学习中的迁移学习

  下面我们以迁移学习在计算机视觉领域的应用为例,介绍一下常用的操作。在实际中,面对大数据集和深度神经网络,非常少的同学会选择使用随机初始化的网络参数来训练整个卷积网络。通常,我们会使用在超大数据集如ImageNet上预训练好的卷积层网络ConvNet,然后把它作为网络初始值进行fine-tune,或者作为固定的特征提取器然后训练最后一层全连接层,来应用到我们感兴趣的任务当中。
  常见的迁移学习策略有如下三种:

  • A.把卷积层作为固定的特征提取器:使用一个在ImageNet上与训练好的卷积网络,移除最后一层全连接层(因为ImageNet上有1000个图片分类),然后按照实际情况重新训练最后一层;
  • B.微调卷积层:这个方法不仅移除了最后一层全连接层进行重训练,同时把预训练的卷积层作为初始值加入到反向传播过程中进行fine-tune微调;
  • C.预训练整个网络:可能花费2-3周在多卡GPU上训练ImageNet网络,然后将最后的checkpoints文件共享出来;

  那么,在实际应用场景中,我们应该如何决定使用那种策略呢?这就涉及到几个因素了,最重要的两个因素是:1.新任务的数据集的大小;新任务数据集与预训练使用的数据集的相似程度(如与ImageNet数据集内容相似,亦或非常不同如显微镜图)。以下是四种场景以及几条指导原则:

  • 新数据集数量较小且与原始数据集内容相似:这种情况下,微调预训练好的卷积层不是一个好的选择,因为这样容易出现过拟合现象,因为这两个数据集内容相似,我们可以直接使用预训练好的卷积提取处高级特征。因此,更为合适的方法是按照实际任务在最后一层卷积层训练一个线性分类模型如lr;
  • 新数据集数量较大且与原始数据集内容相似:相比上一个情景,这个数据集数量更大,所以我们可以微调整个卷积层参数,不用担心过拟合问题;
  • 新数据集数量较小且与原始数据集内容非常不同:一方面为数据集小,因此适合在最后一层训练一个线性分类器;另一方面因为数据集内容相差较大,这又不适合只训练最后一层分类器,这个是一个为难的情况。最好的建议是,不适用整个卷积网络,而是在卷积网络选择较浅层的一个位置作为输出,训练一个svm分类器;
  • 新数据集数量较大且与原始数据集内容非常不同:因为我们有足够的数据,这种情况就建议微调整个网络参数;

pyTorch实战

  接下来我们通过实战来加深对迁移学习的了解。这里我们的目标是对一个数量较小的数据集进行分类,总共有两个类别的图片,分别是蚂蚁(ants:124张)和蜜蜂(bees:121张)。可以看到这个场景符合上述的第1种场景。实战中,我们使用预训练好的resnet18网络,对最后一层全连接层进行相应改造,然后进行训练。我们这里使用了pyTorch。
  数据下载:https://download.pytorch.org/tutorial/hymenoptera_data.zip

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch
import torch.nn as nn
import torch.optim as optim

from torch.optim import lr_scheduler
from torch.autograd import Variable
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 对训练样本和验证样本图片进行预处理,包括裁剪、旋转、标准化
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.RandomResizedCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])
])
}

加载数据,下面路径自行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data_dir = '/mypar/Data/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']}
class_names = image_datasets['train'].classes

use_gpu = torch.cuda.is_available()
1
2
3
4
5
6
7
8
9
def imshow(inp, title=None):
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
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.show()

看一眼数据集

1
2
3
inputs, classes = next(iter(dataloaders['train']))
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])

png

训练模型,使用了scheduler对step进行控制,保存最好的模型参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def train_model(model, criterion, optimizer, scheduler,
num_epochs=25):
begin = time.time()
best_model_wts = copy.deepcopy(model.state_dict())
best_acc = 0.0

for epoch in range(num_epochs):
print('epoch: {}'.format(epoch))
print('-' * 10)

for phase in ['train', 'val']:
if phase == 'train':
scheduler.step()
model.train(True)
else:
model.train(False)

total_loss = 0.0
correct = 0

for inputs, labels in dataloaders[phase]:
if use_gpu:
inputs = Variable(inputs.cuda())
labels = Variable(labels.cuda())
else:
inputs = Variable(inputs)
labels = Variable(labels)

optimizer.zero_grad()
outputs = model(inputs)
_, preds = torch.max(outputs.data, 1)
loss = criterion(outputs, labels)

if phase == 'train':
loss.backward()
optimizer.step()

total_loss += loss.data[0] * inputs.size(0)
correct += torch.sum(preds == labels.data)

epoch_loss = total_loss / dataset_sizes[phase]
epoch_acc = correct / dataset_sizes[phase]

print('{} loss: {:.4f}, acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))

if phase == 'val' and epoch_acc > best_acc:
best_acc = epoch_acc
best_model_wts = copy.deepcopy(model.state_dict())

print('')

time_elapsed = time.time() - begin
print('Training complete in {:.0f}m {:.0f}s'.format(
time_elapsed // 60, time_elapsed % 60))
print('Best val Acc: {:4f}'.format(best_acc))

model.load_state_dict(best_model_wts)
return model

加载resnet18模型,并且通过设置requires_grad为False来固定中间的网络参数,同时改造最后一层全连接层,输出设置为2

1
2
3
4
5
6
model_conv = torchvision.models.resnet18(pretrained=True)
for param in model_conv.parameters():
param.requires_grad = False

num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, 2)
1
2
3
4
5
6
if use_gpu:
model_conv = model_conv.cuda()

criterion = nn.CrossEntropyLoss()
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)
1
2
model_conv = train_model(model_conv, criterion, optimizer_conv,
exp_lr_scheduler, num_epochs=10)
epoch: 0
----------
train loss: 0.5931, acc: 0.6434
val loss: 0.3232, acc: 0.8562

epoch: 1
----------
train loss: 0.6324, acc: 0.7049
val loss: 0.4907, acc: 0.7843

epoch: 2
----------
train loss: 0.4494, acc: 0.8115
val loss: 0.2656, acc: 0.8758

epoch: 3
----------
train loss: 0.3670, acc: 0.8566
val loss: 0.2530, acc: 0.9020

epoch: 4
----------
train loss: 0.3793, acc: 0.8279
val loss: 0.2208, acc: 0.9216

epoch: 5
----------
train loss: 0.5154, acc: 0.8402
val loss: 0.2366, acc: 0.9150

epoch: 6
----------
train loss: 0.4312, acc: 0.8320
val loss: 0.3153, acc: 0.8693

epoch: 7
----------
train loss: 0.5664, acc: 0.7541
val loss: 0.2647, acc: 0.8824

epoch: 8
----------
train loss: 0.3585, acc: 0.8197
val loss: 0.2251, acc: 0.9020

epoch: 9
----------
train loss: 0.3170, acc: 0.8648
val loss: 0.2423, acc: 0.8954

Training complete in 0m 9s
Best val Acc: 0.921569

如果需要对整个网络进行fine-tune,那么可以参考如下代码对上面相应部分进行替换

1
2
3
4
5
6
7
8
9
10
model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)

if use_gpu:
model_ft = model_ft.cuda()

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)

enjoy it!

参考资料:http://cs231n.github.io/transfer-learning/

0%