8-2 模型微调

61 阅读4分钟

一. 介绍

当训练样本数量有限时,可以采用微调(迁移学习)的方式,将从源数据集(通常比目标数据集大得多)学习的知识迁移到目标数据集上,从而更有效的识别当前任务的目标,同时提升模型的泛化性。

二. 注意要点

2.1 数据分布的一致性

微调时的数据应尽量保持相似的分布特征,或者进行相应的预处理使其适配。例如对图像数据要保证通道顺序、尺寸范围、归一化方式等与预训练时一致,否则可能导致模型效果不佳甚至无法正常收敛。

2.2 模型修改与学习率选择

微调时不同层的参数更新往往需要不同的学习率。

一般来说,对于预训练模型中冻结后又解冻的层或者新添加的层,可以适当设置较大一点的学习率,以帮助它们快速适应新任务。

对于那些保留预训练权重且不希望过多变动的层,使用较小的学习率,避免破坏已学到的良好特征。像前面提到的对参数分组设置不同学习率的策略就是常见的做法。

三. 代码实现

3.1 导入依赖库

%matplotlib inline
import os
import torch
import torch.nn as nn
import torchvision
from d2l import torch as d2l

3.2 获取数据集

下载数据

此处的download_extract函数主要实现下载并解压。

d2l.DATA_HUB['hotdog']=(d2l.DATA_URL + 'hotdog.zip','fba480ffa8aa7e0febbb511d181409f899b9baa5')
data_dir = d2l.download_extract('hotdog',folder="data/hotdog")

读取数据

采用 ImageFolder 的方法读取数据,该方法主要是适用于以文件夹分类不同样本的数据集

具体规则是:

  • 主目录(如 data/hotdog/train)下包含多个子文件夹,每个子文件夹的名称即为该类别的标签
  • 每个子文件夹中存放属于该类别的所有图像文件。

image.png

train_imgs = torchvision.datasets.ImageFolder(os.path.join("data/hotdog", 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join("data/hotdog", 'test'))

查看数据样本

hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i-1][0] for i in range(8)]
d2l.show_images(hotdogs+not_hotdogs,2,8,scale=1.4)

image.png

数据预处理

在 resnet18 中,主要采用的 224*224 的图像,且进行了通道标准化处理(目的是减少梯度消失和协变量偏移,即减少每一层输入分布的变化。使得网络参数更新更平滑),所以在这里,我们对香肠数据集也进行同样的数据处理。

normalize = torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406],std=[0.229, 0.224, 0.225])

train_augs = torchvision.transforms.Compose([
    torchvision.transforms.RandomResizedCrop(224),
    torchvision.transforms.RandomHorizontalFlip(),
    torchvision.transforms.ToTensor(),
    normalize,
])

test_augs = torchvision.transforms.Compose([
    torchvision.transforms.Resize(256),
    torchvision.transforms.CenterCrop(224),
    torchvision.transforms.ToTensor(),
])

注:对于 mean 和 std 是预训练模型(如 ResNet、VGG 等)在 ImageNet 数据集上计算得到的均值和标准差

3.3 定义和初始化模型

下载原模型以及模型参数

pretrained_net = torchvision.models.resnet18(pretrained=True)

查看该模型的全连接层(Fully Connected Layer)

pretrained_net.fc

发现输出是:

Linear(in_features=512, out_features=1000, bias=True) 改输出意味着从 224*224*3 的图片经过卷积和池化后,化简为 512 的向量作为输入,同时输出为 1000 个类中的一个。

而本实验是二分类问题,所以只需要设置输出为 2 即可

finetuned_net = torchvision.models.resnet18(pretrained=True)
finetuned_net.fc = nn.Linear(finetuned_net.fc.in_features, 2)
# Xavier 均匀分布初始化新声明的输出层的参数
nn.init.xavier_uniform_(finetuned_net.fc.weight)

3.4 微调模型

下面的代码和上篇文章思路差不多,主要区别在于对于不同的层,这里主要是其他层和全连接层,使用了不同的学习率

def train_fine_tuning(net,learning_rate,batch_size=128,num_epochs=5,param_group=True):
    train_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(os.path.join("data/hotdog", 'train'),transform=train_augs),batch_size=batch_size,shuffle=True)
    test_iter = torch.utils.data.DataLoader(torchvision.datasets.ImageFolder(os.path.join("data/hotdog", 'test'),transform=test_augs),batch_size=batch_size)
    devices = [torch.device("cuda:0" if torch.cuda.is_available() else "cpu")]
    # 交叉熵损失,是分类问题最常用的损失函数之一
    loss=nn.CrossEntropyLoss(reduction='none')
    if param_group:
        params_1x = [param for name, param in net.named_parameters() if name not in ['fc.weight', 'fc.bias']]
        fc_params = list(net.fc.parameters())
        trainer = torch.optim.SGD([{'params': params_1x},{'params':fc_params, 'lr': learning_rate*10},],lr=learning_rate,weight_decay=0.001)
    else:
        trainer = torch.optim.SGD(net.parameters(),lr=learning_rate,weight_decay=0.001)
    d2l.train_ch13(net,train_iter,test_iter,loss,trainer,num_epochs,devices)

注意:这里的优化器和之前的 Adm 都是经常使用的,它们的主要区别在于:

  1. SGD通常需要人工精心选择一个合适的固定学习率
  2. Adam能够根据每个参数的梯度情况自适应地调整学习率,不同参数有不同的学习率
train_fine_tuning(finetuned_net,5e-5)