# 计算机视觉的量化迁移学习教程（beta）

> 原文：<https://pytorch.org/tutorials/intermediate/quantized_transfer_learning_tutorial.html>

小费

为了充分利用本教程，我们建议使用此 [Colab 版本](https://colab.research.google.com/github/pytorch/tutorials/blob/gh-pages/_downloads/quantized_transfer_learning_tutorial.ipynb)。 这将使您可以尝试以下信息。

**作者**： [Zafar Takhirov](https://github.com/z-a-f)

**由**审核： [Raghuraman Krishnamoorthi](https://github.com/raghuramank100)

**编辑**：[Jessica Lin](https://github.com/jlin27)

本教程以 [Sasank Chilamkurthy](https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html) 编写的原始 [PyTorch 迁移学习](https://pytorch.org/tutorials/beginner/transfer_learning_tutorial.html)教程为基础。

迁移学习是指利用预训练的模型应用于不同数据集的技术。 使用迁移学习的主要方法有两种：

1.  **作为固定特征提取器的 ConvNet**：在这里，您[“冻结”](https://arxiv.org/abs/1706.04983)网络中所有参数的权重，除了最后几层（又称“头部”，通常是全连接层）。 将这些最后一层替换为使用随机权重初始化的新层，并且仅训练这些层。
2.  **ConvNet 的微调**：使用随机训练的网络初始化模型，而不是随机初始化，然后像往常一样使用不同的数据集进行训练。 通常，如果输出数量不同，则在网络中也会更换头部（或头部的一部分）。 这种方法通常将学习率设置为较小的值。 这样做是因为已经对网络进行了训练，并且只需进行较小的更改即可将其“微调”到新的数据集。

您还可以结合以上两种方法：首先，可以冻结特征提取器，并训练头部。 之后，您可以解冻特征提取器（或其一部分），将学习率设置为较小的值，然后继续进行训练。

在本部分中，您将使用第一种方法-使用量化模型提取特征。

## 第 0 部分，先决条件

在深入学习迁移学习之前，让我们回顾一下“先决条件”，例如安装和数据加载/可视化。

```py
# Imports
import copy
import matplotlib.pyplot as plt
import numpy as np
import os
import time

plt.ion()

```

### 安装每夜构建

因为您将使用 PyTorch 的 Beta 部分，所以建议安装最新版本的`torch`和`torchvision`。 [您可以在这里找到有关本地安装的最新说明](https://pytorch.org/get-started/locally/)。 例如，要在没有 GPU 支持的情况下进行安装：

```py
pip install numpy
pip install --pre torch torchvision -f https://download.pytorch.org/whl/nightly/cpu/torch_nightly.html
# For CUDA support use https://download.pytorch.org/whl/nightly/cu101/torch_nightly.html

```

### 加载数据

注意

本部分与原始的迁移学习教程相同。

我们将使用`torchvision`和`torch.utils.data`包加载数据。

您今天要解决的问题是从图像中对**蚂蚁**和**蜜蜂**进行分类。 该数据集包含约 120 张针对蚂蚁和蜜蜂的训练图像。 每个类别有 75 个验证图像。 可以认为这是一个很小的数据集。 但是，由于我们正在使用迁移学习，因此我们应该能够很好地进行概括。

*此数据集是 imagenet 的很小子集。*

注意

从[此处](https://download.pytorch.org/tutorial/hymenoptera_data.zip)下载数据，并将其提取到`data`目录。

```py
import torch
from torchvision import transforms, datasets

# Data augmentation and normalization for training
# Just normalization for validation
data_transforms = {
    'train': transforms.Compose([
        transforms.Resize(224),
        transforms.RandomCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(224),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

data_dir = '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=16,
                                              shuffle=True, num_workers=8)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

```

### 可视化一些图像

让我们可视化一些训练图像，以了解数据扩充。

```py
import torchvision

def imshow(inp, title=None, ax=None, figsize=(5, 5)):
  """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)
  if ax is None:
    fig, ax = plt.subplots(1, figsize=figsize)
  ax.imshow(inp)
  ax.set_xticks([])
  ax.set_yticks([])
  if title is not None:
    ax.set_title(title)

# Get a batch of training data
inputs, classes = next(iter(dataloaders['train']))

# Make a grid from batch
out = torchvision.utils.make_grid(inputs, nrow=4)

fig, ax = plt.subplots(1, figsize=(10, 10))
imshow(out, title=[class_names[x] for x in classes], ax=ax)

```

### 模型训练的支持函数

以下是模型训练的通用函数。 此函数也：

*   安排学习率
*   保存最佳模型

```py
def train_model(model, criterion, optimizer, scheduler, num_epochs=25, device='cpu'):
  """
  Support function for model training.

  Args:
    model: Model to be trained
    criterion: Optimization criterion (loss)
    optimizer: Optimizer to use for training
    scheduler: Instance of ``torch.optim.lr_scheduler``
    num_epochs: Number of epochs
    device: Device to run the training on. Must be 'cpu' or 'cuda'
  """
  since = time.time()

  best_model_wts = copy.deepcopy(model.state_dict())
  best_acc = 0.0

  for epoch in range(num_epochs):
    print('Epoch {}/{}'.format(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('{} Loss: {:.4f} Acc: {:.4f}'.format(
        phase, epoch_loss, epoch_acc))

      # 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('Training complete in {:.0f}m {:.0f}s'.format(
    time_elapsed // 60, time_elapsed % 60))
  print('Best val Acc: {:4f}'.format(best_acc))

  # load best model weights
  model.load_state_dict(best_model_wts)
  return model

```

### 可视化模型预测的支持函数

通用函数，显示一些图像的预测

```py
def visualize_model(model, rows=3, cols=3):
  was_training = model.training
  model.eval()
  current_row = current_col = 0
  fig, ax = plt.subplots(rows, cols, figsize=(cols*2, rows*2))

  with torch.no_grad():
    for idx, (imgs, lbls) in enumerate(dataloaders['val']):
      imgs = imgs.cpu()
      lbls = lbls.cpu()

      outputs = model(imgs)
      _, preds = torch.max(outputs, 1)

      for jdx in range(imgs.size()[0]):
        imshow(imgs.data[jdx], ax=ax[current_row, current_col])
        ax[current_row, current_col].axis('off')
        ax[current_row, current_col].set_title('predicted: {}'.format(class_names[preds[jdx]]))

        current_col += 1
        if current_col >= cols:
          current_row += 1
          current_col = 0
        if current_row >= rows:
          model.train(mode=was_training)
          return
    model.train(mode=was_training)

```

## 第 1 部分，基于量化特征提取器训练自定义分类器

在本节中，您将使用“冻结”量化特征提取器，并在其顶部训练自定义分类器头。 与浮点模型不同，您无需为量化模型设置`require_grad = False`，因为它没有可训练的参数。 请参阅[文档](https://pytorch.org/docs/stable/quantization.html)了解更多详细信息。

加载预训练的模型：在本练习中，您将使用 [ResNet-18](https://pytorch.org/hub/pytorch_vision_resnet/) 。

```py
import torchvision.models.quantization as models

# You will need the number of filters in the `fc` for future use.
# 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_fe = models.resnet18(pretrained=True, progress=True, quantize=True)
num_ftrs = model_fe.fc.in_features

```

此时，您需要修改预训练模型。 该模型在开始和结束时都有量化/去量化块。 但是，由于只使用特征提取器，因此反量化层必须在线性层（头部）之前移动。 最简单的方法是将模型包装在`nn.Sequential`模块中。

第一步是在 ResNet 模型中隔离特征提取器。 尽管在本示例中，您被责成使用`fc`以外的所有层作为特征提取器，但实际上，您可以根据需要选择任意数量的零件。 如果您也想替换一些卷积层，这将很有用。

注意

将特征提取器与量化模型的其余部分分开时，必须手动将量化器/去量化器放置在要保持量化的部分的开头和结尾。

下面的函数创建一个带有自定义头部的模型。

```py
from torch import nn

def create_combined_model(model_fe):
  # Step 1\. Isolate the feature extractor.
  model_fe_features = nn.Sequential(
    model_fe.quant,  # Quantize the input
    model_fe.conv1,
    model_fe.bn1,
    model_fe.relu,
    model_fe.maxpool,
    model_fe.layer1,
    model_fe.layer2,
    model_fe.layer3,
    model_fe.layer4,
    model_fe.avgpool,
    model_fe.dequant,  # Dequantize the output
  )

  # Step 2\. Create a new "head"
  new_head = nn.Sequential(
    nn.Dropout(p=0.5),
    nn.Linear(num_ftrs, 2),
  )

  # Step 3\. Combine, and don't forget the quant stubs.
  new_model = nn.Sequential(
    model_fe_features,
    nn.Flatten(1),
    new_head,
  )
  return new_model

```

警告

当前，量化模型只能在 CPU 上运行。 但是，可以将模型的未量化部分发送到 GPU。

```py
import torch.optim as optim
new_model = create_combined_model(model_fe)
new_model = new_model.to('cpu')

criterion = nn.CrossEntropyLoss()

# Note that we are only training the head.
optimizer_ft = optim.SGD(new_model.parameters(), lr=0.01, momentum=0.9)

# Decay LR by a factor of 0.1 every 7 epochs
exp_lr_scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)

```

### 训练和评估

此步骤在 CPU 上大约需要 15-25 分钟。 由于量化模型只能在 CPU 上运行，因此您不能在 GPU 上运行训练。

```py
new_model = train_model(new_model, criterion, optimizer_ft, exp_lr_scheduler,
                        num_epochs=25, device='cpu')

visualize_model(new_model)
plt.tight_layout()

```

## 第 2 部分，微调量化模型

在这一部分中，我们将微调用于迁移学习的特征提取器，并对特征提取器进行量化。 请注意，在第 1 部分和第 2 部分中，特征提取器都是量化的。 不同之处在于，在第 1 部分中，我们使用了预训练的量化模型。 在这一部分中，我们将在对感兴趣的数据集进行微调之后创建一个量化的特征提取器，因此这是一种在具有量化优势的同时通过迁移学习获得更好的准确率的方法。 请注意，在我们的特定示例中，训练集非常小（120 张图像），因此微调整个模型的好处并不明显。 但是，此处显示的过程将提高使用较大数据集进行传递学习的准确率。

预训练特征提取器必须是可量化的。 为确保其可量化，请执行以下步骤：

> 1.  使用 `torch.quantization.fuse_modules` 熔断 `(Conv, BN, ReLU)` ， `(Conv, BN)` 和 `(Conv, ReLU)` 。
> 2.  将特征提取器与自定义头部连接。 这需要对特征提取器的输出进行反量化。
> 3.  在特征提取器中的适当位置插入伪量化模块，以模拟训练期间的量化。

对于步骤（1），我们使用`torchvision/models/quantization`中的模型，这些模型具有成员方法`fuse_model`。 此函数将所有`conv`，`bn`和`relu`模块融合在一起。 对于自定义模型，这需要使用模块列表调用`torch.quantization.fuse_modules` API 进行手动融合。

步骤（2）由上一节中使用的`create_combined_model`函数执行。

步骤（3）通过使用`torch.quantization.prepare_qat`来实现，它会插入伪量化模块。

在步骤（4）中，您可以开始“微调”模型，然后将其转换为完全量化的版本（步骤 5）。

要将微调模型转换为量化模型，可以调用`torch.quantization.convert`函数（在我们的情况下，仅对特征提取器进行量化）。

注意

由于随机初始化，您的结果可能与本教程中显示的结果不同。

```py
# notice quantize=False model = models.resnet18(pretrained=True, progress=True, quantize=False) num_ftrs = model.fc.in_features

# Step 1 model.train() model.fuse_model() # Step 2 model_ft = create_combined_model(model) model_ft[0].qconfig = torch.quantization.default_qat_qconfig # Use default QAT configuration # Step 3 model_ft = torch.quantization.prepare_qat(model_ft, inplace=True)
```

### 微调模型

在当前教程中，整个模型都经过了微调。 通常，这将导致更高的精度。 但是，由于此处使用的训练集很小，最终导致我们过度适应了训练集。

步骤 4.微调模型

```py
for param in model_ft.parameters():
  param.requires_grad = True

model_ft.to(device)  # We can fine-tune on GPU if available

criterion = nn.CrossEntropyLoss()

# Note that we are training everything, so the learning rate is lower
# Notice the smaller learning rate
optimizer_ft = optim.SGD(model_ft.parameters(), lr=1e-3, momentum=0.9, weight_decay=0.1)

# Decay LR by a factor of 0.3 every several epochs
exp_lr_scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=5, gamma=0.3)

model_ft_tuned = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
                             num_epochs=25, device=device)

```

步骤 5.转换为量化模型

```py
from torch.quantization import convert
model_ft_tuned.cpu()

model_quantized_and_trained = convert(model_ft_tuned, inplace=False)

```

让我们看看量化模型在几张图像上的表现

```py
visualize_model(model_quantized_and_trained)

plt.ioff()
plt.tight_layout()
plt.show()

```