迁移学习教程

译者:@Sylvester

校对者:@Archie Yu

作者: Sasank Chilamkurthy

这个教程将教你如何使用迁移学习训练你的网络. 你可以在 cs231n 笔记 中 阅读更多有关迁移学习的信息.

引用自该笔记,

事实上, 很少有人从头(随机初始化)开始训练一个卷积网络, 因为拥有一个足够大的数据库是比较少见的. 替代的是, 通常会从一个大的数据集(例如 ImageNet, 包含120万的图片和1000个分类)预训练一个卷积网络, 然后将这个卷积网络作为初始化的网络, 或者是感兴趣任务的固定的特征提取器.

如下是两种主要的迁移学习的使用场景:

  • 微调卷积网络: 取代随机初始化网络, 我们从一个预训练的网络初始化, 比如从 imagenet 1000 数据集预训练的网络. 其余的训练就像往常一样.
  • 卷积网络作为固定的特征提取器: 在这里, 我们固定网络中的所有权重, 最后的全连接层除外. 最后的全连接层被新的随机权重替换, 并且, 只有这一层是被训练的.
  1. # License: BSD
  2. # Author: Sasank Chilamkurthy
  3. from __future__ import print_function, division
  4. import torch
  5. import torch.nn as nn
  6. import torch.optim as optim
  7. from torch.optim import lr_scheduler
  8. from torch.autograd import Variable
  9. import numpy as np
  10. import torchvision
  11. from torchvision import datasets, models, transforms
  12. import matplotlib.pyplot as plt
  13. import time
  14. import os
  15. import copy
  16. plt.ion() # interactive mode

加载数据

我们用 torchvision 和 torch.utils.data 包加载数据.

我们今天要解决的问题是, 训练一个可以区分 ants (蚂蚁) 和 bees (蜜蜂) 的模型. 用于训练的 ants 和 bees 图片各120张. 每一类用于验证的图片各75张. 通常, 如果从头开始训练, 这个非常小的数据集不足以进行泛化. 但是, 因为我们使用迁移学习, 应该可以取得很好的泛化效果.

这个数据集是一个非常小的 imagenet 子集

注解:

这里 <[https://download.pytorch.org/tutorial/hymenoptera_data.zip](https://download.pytorch.org/tutorial/hymenoptera_data.zip)>_ 下载数据, 然后解压到当前目录.

  1. # 训练要做数据增强和数据标准化
  2. # 验证只做数据标准化
  3. data_transforms = {
  4. 'train': transforms.Compose([
  5. transforms.RandomSizedCrop(224),
  6. transforms.RandomHorizontalFlip(),
  7. transforms.ToTensor(),
  8. transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
  9. ]),
  10. 'val': transforms.Compose([
  11. transforms.Scale(256),
  12. transforms.CenterCrop(224),
  13. transforms.ToTensor(),
  14. transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
  15. ]),
  16. }
  17. data_dir = 'hymenoptera_data'
  18. image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
  19. data_transforms[x])
  20. for x in ['train', 'val']}
  21. dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
  22. shuffle=True, num_workers=4)
  23. for x in ['train', 'val']}
  24. dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
  25. class_names = image_datasets['train'].classes
  26. use_gpu = torch.cuda.is_available()

显示一些图片

让我们显示一些训练中的图片, 以便了解数据增强.

  1. def imshow(inp, title=None):
  2. """Imshow for Tensor."""
  3. inp = inp.numpy().transpose((1, 2, 0))
  4. mean = np.array([0.485, 0.456, 0.406])
  5. std = np.array([0.229, 0.224, 0.225])
  6. inp = std * inp + mean
  7. inp = np.clip(inp, 0, 1)
  8. plt.imshow(inp)
  9. if title is not None:
  10. plt.title(title)
  11. plt.pause(0.001) # 暂停一会, 让 plots 更新
  12. # 获得一批训练数据
  13. inputs, classes = next(iter(dataloaders['train']))
  14. # 从这批数据生成一个方格
  15. out = torchvision.utils.make_grid(inputs)
  16. imshow(out, title=[class_names[x] for x in classes])

训练模型

现在, 让我们写一个通用的函数来训练模型. 这里, 我们将会举例说明:

  • 调度学习率
  • 保存最佳的学习模型

下面函数中, scheduler 参数是 torch.optim.lr_scheduler 中的 LR scheduler 对象.

  1. def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
  2. since = time.time()
  3. best_model_wts = copy.deepcopy(model.state_dict())
  4. best_acc = 0.0
  5. for epoch in range(num_epochs):
  6. print('Epoch {}/{}'.format(epoch, num_epochs - 1))
  7. print('-' * 10)
  8. # 每一个迭代都有训练和验证阶段
  9. for phase in ['train', 'val']:
  10. if phase == 'train':
  11. scheduler.step()
  12. model.train(True) # 设置 model 为训练 (training) 模式
  13. else:
  14. model.train(False) # 设置 model 为评估 (evaluate) 模式
  15. running_loss = 0.0
  16. running_corrects = 0
  17. # 遍历数据
  18. for data in dataloaders[phase]:
  19. # 获取输入
  20. inputs, labels = data
  21. # 用 Variable 包装输入数据
  22. if use_gpu:
  23. inputs = Variable(inputs.cuda())
  24. labels = Variable(labels.cuda())
  25. else:
  26. inputs, labels = Variable(inputs), Variable(labels)
  27. # 设置梯度参数为 0
  28. optimizer.zero_grad()
  29. # 正向传递
  30. outputs = model(inputs)
  31. _, preds = torch.max(outputs.data, 1)
  32. loss = criterion(outputs, labels)
  33. # 如果是训练阶段, 向后传递和优化
  34. if phase == 'train':
  35. loss.backward()
  36. optimizer.step()
  37. # 统计
  38. running_loss += loss.data[0] * inputs.size(0)
  39. running_corrects += torch.sum(preds == labels.data)
  40. epoch_loss = running_loss / dataset_sizes[phase]
  41. epoch_acc = running_corrects / dataset_sizes[phase]
  42. print('{} Loss: {:.4f} Acc: {:.4f}'.format(
  43. phase, epoch_loss, epoch_acc))
  44. # 深拷贝 model
  45. if phase == 'val' and epoch_acc > best_acc:
  46. best_acc = epoch_acc
  47. best_model_wts = copy.deepcopy(model.state_dict())
  48. print()
  49. time_elapsed = time.time() - since
  50. print('Training complete in {:.0f}m {:.0f}s'.format(
  51. time_elapsed // 60, time_elapsed % 60))
  52. print('Best val Acc: {:4f}'.format(best_acc))
  53. # 加载最佳模型的权重
  54. model.load_state_dict(best_model_wts)
  55. return model

显示模型的预测结果

写一个处理少量图片, 并显示预测结果的通用函数

  1. def visualize_model(model, num_images=6):
  2. images_so_far = 0
  3. fig = plt.figure()
  4. for i, data in enumerate(dataloaders['val']):
  5. inputs, labels = data
  6. if use_gpu:
  7. inputs, labels = Variable(inputs.cuda()), Variable(labels.cuda())
  8. else:
  9. inputs, labels = Variable(inputs), Variable(labels)
  10. outputs = model(inputs)
  11. _, preds = torch.max(outputs.data, 1)
  12. for j in range(inputs.size()[0]):
  13. images_so_far += 1
  14. ax = plt.subplot(num_images//2, 2, images_so_far)
  15. ax.axis('off')
  16. ax.set_title('predicted: {}'.format(class_names[preds[j]]))
  17. imshow(inputs.cpu().data[j])
  18. if images_so_far == num_images:
  19. return

调整卷积网络

加载一个预训练的网络, 并重置最后一个全连接层.

  1. model_ft = models.resnet18(pretrained=True)
  2. num_ftrs = model_ft.fc.in_features
  3. model_ft.fc = nn.Linear(num_ftrs, 2)
  4. if use_gpu:
  5. model_ft = model_ft.cuda()
  6. criterion = nn.CrossEntropyLoss()
  7. # 如你所见, 所有参数都将被优化
  8. optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
  9. # 每 7 个迭代, 让 LR 衰减 0.1 因素
  10. exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

训练和评估

如果使用 CPU, 这将花费 15-25 分钟. 但使用 GPU 的话, 需要的时间将少于1分钟.

  1. model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
  2. num_epochs=25)
  1. visualize_model(model_ft)

卷积网络作为固定的特征提取器

这里, 我们固定网络中除最后一层外的所有权重. 为了固定这些参数, 我们需要设置 requires_grad == False , 然后在 backward() 中就不会计算梯度.

你可以在 这里 阅读更多相关信息.

  1. model_conv = torchvision.models.resnet18(pretrained=True)
  2. for param in model_conv.parameters():
  3. param.requires_grad = False
  4. # 新构建的 module 的参数中, 默认设置了 requires_grad=True.
  5. num_ftrs = model_conv.fc.in_features
  6. model_conv.fc = nn.Linear(num_ftrs, 2)
  7. if use_gpu:
  8. model_conv = model_conv.cuda()
  9. criterion = nn.CrossEntropyLoss()
  10. # 如你所见, 和我们前面提出的一样, 只有最后一层的参数被优化.
  11. optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
  12. # 每 7 个迭代, 让 LR 衰减 0.1 因素
  13. exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)

训练和评估

在使用 CPU 的情况下, 和前一个方案相比, 这将花费的时间是它的一半. 期望中, 网络的大部分是不需要计算梯度的. 前向传递依然要计算梯度.

  1. model_conv = train_model(model_conv, criterion, optimizer_conv,
  2. exp_lr_scheduler, num_epochs=25)
  1. visualize_model(model_conv)
  2. plt.ioff()
  3. plt.show()