本文讲述了如何使用迁移学习来对图片分类任务训练一个卷积神经网络。关于更多的迁移学习可以查看cs231n notes。
关于这些笔记:
实际上,人们通常不会从头训练一整个卷积神经网络(从随机初始化权重开始),因为通常并没有足够大的数据集。相反,更常见的做法是在一个非常大的数据集上(例如ImageNet数据集,有着1000个类别的120万的图片)训练一个卷积网络,然后将这个网络作为想要的任务的初始化权重或者固定特征提取器(fixed feature extractor)。
以下有两种主要的迁移学习的应用场景:
- 微调卷积网络(finetuning the convnet):我们从一个预训练的网络模型开始我们的训练任务,而不是随机初始化权重的方式。
- 将卷积网络作为固定特征提取器(ConvNet as fixed feature extractor):在这里,我们将冻结(freeze,固定权重)除了网络最后一层的全连接层的所有网络部分。将最后一层的全连接层替换为以随机权重初始化的层,然后只对新的一层进行训练。
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 0x7f590ec129d0>
加载数据
我们将使用torchvision和torch.utils.data包来加载数据集。
本文要解决的任务是训练一个分类模型,此模型用来分类蚂蚁和蜜蜂。对于这两个类别,我们大约有120张训练图像;75个验证图像。相对于从头开始训练,通常来说这是一个可以泛化(generalize)的非常小的数据集。所以我们将使用迁移学习,我们应该能够很合理的泛化模型。
这个数据集是ImageNet的非常小的一个子集。
从此连接下载数据集,并且解压到当前目录。
# 对于训练集,使用数据增强和规范化
# 对于验证集,只使用规范化
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/"
images_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ["train", "val"]}
dataloaders = {x: torch.utils.data.DataLoader(images_datasets[x], batch_size=4, shuffle=True, num_workers=4) for x in ["train", "val"]}
dataset_sizes = {x: len(images_datasets[x]) for x in ['train', 'val']}
class_names = images_datasets["train"].classes
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
绘制部分图像
为了搞清楚数据增强做了什么事情,我们可以绘制出几张训练图像。
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
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"]))
# 对于一个批量,产生一个网格
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])
训练模型
现在,让我们编写一个训练模型的通用函数。在这里,我们将阐述:
- scheduling the learning rate
- saving the best model
在接下来的部分,scheduler
是torch.optim.lr_scheduler
的一个LR scheduler对象。
def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
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}/{num_epochs - 1}')
print('-' * 10)
# 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}')
# deep copy the model
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() - 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
绘制模型预测
对于一少部分的图片,显示预测的通用函数。
def visualize_model(model, num_images=6):
was_training = model.training
model.eval()
images_so_far = 0
fig = plt.figure()
with torch.no_grad():
for i, (inputs, labels) in enumerate(dataloaders['val']):
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
for j in range(inputs.size()[0]):
images_so_far += 1
ax = plt.subplot(num_images//2, 2, images_so_far)
ax.axis('off')
ax.set_title(f'predicted: {class_names[preds[j]]}')
imshow(inputs.cpu().data[j])
if images_so_far == num_images:
model.train(mode=was_training)
return
model.train(mode=was_training)
微调卷积网络
加载一个预训练模型并且重置最后的全连接层。
model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features
# Here the size of each output sample is set to 2.
# Alternatively, it can be generalized to nn.Linear(num_ftrs, len(class_names)).
model_ft.fc = nn.Linear(num_ftrs, 2)
model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()
# Observe that all parameters are being optimized
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
训练并验证
在CPU上执行时间大概在15-25分钟,而在GPU上,大概只需要不到1分钟。
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler, num_epochs=25)
Epoch 0/24
----------
train Loss: 0.6200 Acc: 0.7049
val Loss: 0.1974 Acc: 0.9281
Epoch 1/24
----------
train Loss: 0.3805 Acc: 0.8238
val Loss: 0.2273 Acc: 0.9085
Epoch 2/24
----------
train Loss: 0.6180 Acc: 0.7541
val Loss: 0.3575 Acc: 0.8301
Epoch 3/24
----------
train Loss: 0.6691 Acc: 0.7213
val Loss: 0.7067 Acc: 0.7320
Epoch 4/24
----------
train Loss: 0.5769 Acc: 0.7787
val Loss: 0.3549 Acc: 0.8758
Epoch 5/24
----------
train Loss: 0.5283 Acc: 0.8074
val Loss: 0.4467 Acc: 0.8693
Epoch 6/24
----------
train Loss: 0.5416 Acc: 0.8115
val Loss: 0.4135 Acc: 0.8758
Epoch 7/24
----------
train Loss: 0.5169 Acc: 0.8320
val Loss: 0.2594 Acc: 0.9150
Epoch 8/24
----------
train Loss: 0.3956 Acc: 0.8443
val Loss: 0.2633 Acc: 0.8889
Epoch 9/24
----------
train Loss: 0.3106 Acc: 0.8811
val Loss: 0.2324 Acc: 0.9150
Epoch 10/24
----------
train Loss: 0.3582 Acc: 0.8361
val Loss: 0.2172 Acc: 0.9150
Epoch 11/24
----------
train Loss: 0.3057 Acc: 0.8811
val Loss: 0.2832 Acc: 0.9085
Epoch 12/24
----------
train Loss: 0.2628 Acc: 0.8893
val Loss: 0.2257 Acc: 0.9216
Epoch 13/24
----------
train Loss: 0.2785 Acc: 0.8730
val Loss: 0.2305 Acc: 0.9281
Epoch 14/24
----------
train Loss: 0.2999 Acc: 0.8648
val Loss: 0.2338 Acc: 0.9281
Epoch 15/24
----------
train Loss: 0.1689 Acc: 0.9426
val Loss: 0.2332 Acc: 0.9216
Epoch 16/24
----------
train Loss: 0.2660 Acc: 0.9057
val Loss: 0.2629 Acc: 0.9150
Epoch 17/24
----------
train Loss: 0.3769 Acc: 0.8074
val Loss: 0.2362 Acc: 0.9281
Epoch 18/24
----------
train Loss: 0.3248 Acc: 0.8607
val Loss: 0.2255 Acc: 0.9216
Epoch 19/24
----------
train Loss: 0.2859 Acc: 0.8893
val Loss: 0.2545 Acc: 0.9150
Epoch 20/24
----------
train Loss: 0.2435 Acc: 0.8852
val Loss: 0.2538 Acc: 0.9150
Epoch 21/24
----------
train Loss: 0.3553 Acc: 0.8525
val Loss: 0.2828 Acc: 0.9085
Epoch 22/24
----------
train Loss: 0.3092 Acc: 0.8770
val Loss: 0.2246 Acc: 0.9216
Epoch 23/24
----------
train Loss: 0.2671 Acc: 0.8934
val Loss: 0.2437 Acc: 0.9216
Epoch 24/24
----------
train Loss: 0.1983 Acc: 0.9262
val Loss: 0.2293 Acc: 0.9281
Training complete in 8m 22s
Best val Acc: 0.928105
visualize_model(model_ft)
将ConvNet作为固定特征提取器
接下来,我们要冻结网络中除最后一层的所有部分。我们需要设置requires_grad=False
来冻结网络中的参数,以便于在backward()
中梯度不被计算。
你可以从此文档中查看更多。
model_conv = torchvision.models.resnet18(pretrained=True)
for param in model_conv.parameters():
param.requires_grad = False
# Parameters of newly constructed modules have requires_grad=True by default
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, 2)
model_conv = model_conv.to(device)
criterion = nn.CrossEntropyLoss()
# Observe that only parameters of final layer are being optimized as
# opposed to before.
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)
训练和验证
在CPU上训练,这会耗与之前场景下一半的时间。这是因为对于网络中的大部分参数,已经不需要被计算梯度。但是,前向传播仍然是需要进行的。
model_conv = train_model(model_conv, criterion, optimizer_conv, exp_lr_scheduler, num_epochs=25)
Epoch 0/24
----------
train Loss: 0.6645 Acc: 0.6557
val Loss: 0.2080 Acc: 0.9477
Epoch 1/24
----------
train Loss: 0.4321 Acc: 0.7787
val Loss: 0.2543 Acc: 0.8889
Epoch 2/24
----------
train Loss: 0.4560 Acc: 0.8320
val Loss: 0.3580 Acc: 0.8301
Epoch 3/24
----------
train Loss: 0.4300 Acc: 0.8197
val Loss: 0.1768 Acc: 0.9542
Epoch 4/24
----------
train Loss: 0.4507 Acc: 0.8074
val Loss: 0.1854 Acc: 0.9412
Epoch 5/24
----------
train Loss: 0.3498 Acc: 0.8525
val Loss: 0.1786 Acc: 0.9477
Epoch 6/24
----------
train Loss: 0.4869 Acc: 0.8033
val Loss: 0.3503 Acc: 0.8562
Epoch 7/24
----------
train Loss: 0.3607 Acc: 0.8484
val Loss: 0.1843 Acc: 0.9477
Epoch 8/24
----------
train Loss: 0.3784 Acc: 0.8238
val Loss: 0.1927 Acc: 0.9412
Epoch 9/24
----------
train Loss: 0.2795 Acc: 0.8811
val Loss: 0.1895 Acc: 0.9346
Epoch 10/24
----------
train Loss: 0.3200 Acc: 0.8811
val Loss: 0.1843 Acc: 0.9477
Epoch 11/24
----------
train Loss: 0.3509 Acc: 0.8607
val Loss: 0.1780 Acc: 0.9477
Epoch 12/24
----------
train Loss: 0.3471 Acc: 0.8279
val Loss: 0.1747 Acc: 0.9412
Epoch 13/24
----------
train Loss: 0.3467 Acc: 0.8320
val Loss: 0.2249 Acc: 0.9150
Epoch 14/24
----------
train Loss: 0.3171 Acc: 0.8484
val Loss: 0.1845 Acc: 0.9477
Epoch 15/24
----------
train Loss: 0.3456 Acc: 0.8279
val Loss: 0.1745 Acc: 0.9412
Epoch 16/24
----------
train Loss: 0.3599 Acc: 0.8525
val Loss: 0.1960 Acc: 0.9477
Epoch 17/24
----------
train Loss: 0.2593 Acc: 0.8934
val Loss: 0.1894 Acc: 0.9477
Epoch 18/24
----------
train Loss: 0.3548 Acc: 0.8361
val Loss: 0.1900 Acc: 0.9346
Epoch 19/24
----------
train Loss: 0.3291 Acc: 0.8361
val Loss: 0.1977 Acc: 0.9412
Epoch 20/24
----------
train Loss: 0.2964 Acc: 0.8402
val Loss: 0.1920 Acc: 0.9412
Epoch 21/24
----------
train Loss: 0.3526 Acc: 0.8607
val Loss: 0.2000 Acc: 0.9281
Epoch 22/24
----------
train Loss: 0.3836 Acc: 0.7992
val Loss: 0.1915 Acc: 0.9412
Epoch 23/24
----------
train Loss: 0.3399 Acc: 0.8566
val Loss: 0.1821 Acc: 0.9346
Epoch 24/24
----------
train Loss: 0.3259 Acc: 0.8402
val Loss: 0.1718 Acc: 0.9477
Training complete in 4m 1s
Best val Acc: 0.954248
visualize_model(model_conv)
plt.ioff()
plt.show()
下一步阅读
如果你希望阅读更多的迁移学习的应用,可以查看Quantized Transfer Learning for Computer Vision Tutorial。