深度学习之微调及代码实现

微调概念

微调(fine-tuning)是深度学习特别是计算机视觉领域中最重要的一个技术。微调也叫迁移学习Transfer Learning。

他的核心思想是说就像人类学习一样,先掌握一些大量的基础知识,之后再想要掌握别的方面的知识的时候,就只需要再学习一点点数据就可以了。把这段话类比在人工智能中,其实就是说模型先在一个很大的数据集上进行了训练,得到一个预训练好的模型,之后再把这个模型迁移到别的任务中的时候,只需要学习一个相对来说不那么大甚至很小的数据集,就可以在该任务上取得比较好的效果。与此同时,其他领域的其他任务可能正好只能构造出来一个很小的数据集,通过应用微调技术,这个数据集比较小的问题就迎刃而解了。

模型的网络架构可以简单的分成两个部分,如下图所示,一个部分是靠近数据集开始的第1层一直到最后第L-1层,这部分是用于进行数据的特征抽取的,并且此特征抽取是可学习的;另一个部分是最后的输出层,比如如果说分类任务的话,这一层可能就是一个线性层/全连接层,可能还加了一个softmax回归。网络架构的特征抽取部分越底层提取的是数据越抽象的特征,越顶层提取的是更加语义化的特征。因此在目标数据集上训练目标模型的时候,首先网络架构要同预训练的模型的一样,比如都使用resnet18模型,然后在模型权重初始化的时候,将特征抽取部分的模型权重全部都copy预训练模型的,而最后面输出层的权重可采用随机初始化来从头训练,因为目标数据集的labels很大可能是同源数据集不一样的,因此输出层不能copy。而在训练的时候,因为Loss、grad等是从顶层开始往下传的,所以最后一层的权重更新也比较迅速,能比较快的就训练好,而下面的层的权重在Loss、grad传过来的时候只需要进行细微的调整即可,这也是微调的地方所在。

image-20250122120033882

这样通过微调的技术,就能够使得预训练好的模型在目标数据集上微调一下参数,就可以达到一个很好的效果,这也是深度学习能够迅速的在工业界应用的一个前提所在。

核心代码

利用深度学习框架PyTorch,其中有一些预训练好的模型的结构以及参数可供我们选取使用,官方文档位于:Models and pre-trained weights — Torchvision 0.20 documentation

以ResNet18模型为例,在教程中作者使用了pretrained这一参数来表示模型是否使用已经预训练好的参数,但我查阅了目前的官方文档之后发现官方有说明pretrained参数现已弃用,使用该参数会发出警告,因此我们可以将相关代码修改成如下这样,可以实现同样对的效果:

# pretrained_net = torchvision.models.resnet18(pretrained=True)
pretrained_net = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT)
print(pretrained_net.fc)
# Linear(in_features=512, out_features=1000, bias=True)

此模型是在ImageNet数据集上预训练的,该数据集有1000个类别,因此最后的线性层out_features=1000

将此作为一个预训练模型去微调训练一个只有2个类别的热狗数据集,模型设置的核心代码如下:

finetune_net = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT)
# 因为目标数据集——热狗数据集只有2个类别
finetune_net.fc = nn.Linear(in_features=finetune_net.fc.in_features, out_features=2)
# 只随机初始化最后一层线性层的权重参数
nn.init.xavier_uniform_(finetune_net.fc.weight)

为了进行比较,定义了一个相同的模型,但是将其所有模型参数初始化为随机值,整个模型需要从头开始训练。

# 为了进行比较,所有模型参数初始化为随机值 从头开始训练
scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)

完整代码

# export PYTHONIOENCODING=utf-8
import os
import torch
import torchvision
from torch import nn
from d2l import torch as d2l
import matplotlib.pyplot as plt
# 设置 Matplotlib 后端
import matplotlib

matplotlib.use('TkAgg')

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

# ImageFolder 是一个方便的数据加载工具,用于从一个目录中加载分类任务的图像数据
# 使用 os.path.join 将 data_dir 和 'train' 拼接成一个完整的文件路径
train_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'train'))
test_imgs = torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'))

hotdogs = [train_imgs[i][0] for i in range(8)]
not_hotdogs = [train_imgs[-i - 1][0] for i in range(8)]

# 在PyCharm中显示图像
# 使用 d2l 的 show_images 方法显示图像
# d2l.show_images(hotdogs + not_hotdogs, num_rows=2, num_cols=8, scale=1.2)
# 确保显示图像
# plt.show()

# 使用RGB通道的均值和标准差,以标准化每个通道
# 因为预训练模型在ImageNet数据集上训练的时候做了这样的处理 所以迁移训练的时候也需要做
normalize = torchvision.transforms.Normalize(
    # 第一个表示RGB三通道的mean均值, 第二个表示RGB三通道的std标准差
    [0.485, 0.456, 0.406], [0.229, 0.224, 0.225]
)

# 一些图像增广操作
train_augs = torchvision.transforms.Compose([
    # 因为ImageNet的图像的尺寸是224*224,所以需要Resize到这个尺寸
    torchvision.transforms.RandomResizedCrop(224),
    torchvision.transforms.RandomHorizontalFlip(),
    torchvision.transforms.ToTensor(),
    normalize])

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

# 注意:官方文档中有说明pretrained参数现已弃用,使用该参数会发出警告
pretrained_net = torchvision.models.resnet18(pretrained=True)
print(pretrained_net.fc)
# Linear(in_features=512, out_features=1000, bias=True)
# pretrained_net = torchvision.models.resnet18(weights=torchvision.models.ResNet18_Weights.DEFAULT)
# print(pretrained_net.fc)

finetune_net = torchvision.models.resnet18(pretrained=True)
# finetune_net = torchvision.models.resnet18(weights=ResNet18_Weights)
# 因为目标数据集——热狗数据集只有2个类别
finetune_net.fc = nn.Linear(in_features=finetune_net.fc.in_features, out_features=2)
# 只随机初始化最后一层线性层的权重参数
nn.init.xavier_uniform_(finetune_net.fc.weight)

def train_batch_ch13(net, X, y, loss, trainer, devices):
    if isinstance(X, list):
        X = [x.to(devices[0]) for x in X]
    else:
        X = X.to(devices[0])
    y = y.to(devices[0])
    net.train()
    trainer.zero_grad()
    pred = net(X)
    # 根据后面loss的定义:loss = nn.CrossEntropyLoss(reduction='none')
    # 这里的l计算会得到一个与输入批次大小(batch size)相同长度的张量,其中每个元素对应一个样本的损失
    l = loss(pred, y)
    # 所以这里需要进行.sum()求和
    l.sum().backward()
    trainer.step()
    train_loss_sum = l.sum()
    train_acc_sum = d2l.accuracy(pred, y)
    return train_loss_sum, train_acc_sum

def train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
               devices=d2l.try_all_gpus()):
    timer, num_batchs = d2l.Timer(), len(train_iter)
    net = nn.DataParallel(net, device_ids=devices).to(devices[0])
    for epoch in range(num_epochs):
        print("第" + str(epoch + 1) + "轮开始训练了......")
        metric = d2l.Accumulator(4)
        for i, (features, labels) in enumerate(train_iter):
            timer.start()
            l, acc = train_batch_ch13(
                net, features, labels, loss, trainer, devices)
            # labels.shape[0] 就是当前批次的大小,也即样本数
            # 如果 labels 是一个一维向量(比如分类任务中的标签) labels.numel() 就是批次中的样本数量
            metric.add(l, acc, labels.shape[0], labels.numel())
            timer.stop()
    test_acc = d2l.evaluate_accuracy_gpu(net, test_iter)
    print(f'loss {metric[0] / metric[2]:.3f}, train acc '
          f'{metric[1] / metric[3]:.3f}, test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec on '
          f'{str(devices)}')


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_dir, 'train'), transform=train_augs),
        batch_size=batch_size, shuffle=True)
    test_iter = torch.utils.data.DataLoader(
        torchvision.datasets.ImageFolder(os.path.join(data_dir, 'test'), transform=test_augs),
        batch_size=batch_size)

    devices = [d2l.try_gpu(i) for i in range(4)]
    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"]]
        # 意思就是最后一层参数更新的学习率是前面所有层的10倍
        trainer = torch.optim.SGD([{'params': params_1x},
                                   {'params': net.fc.parameters(),
                                    '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)
    train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs, devices)


# 最后一层的学习率是5e-4, 其他层是5e-5
train_fine_tuning(finetune_net, 5e-5, num_epochs=3)

# 为了进行比较,所有模型参数初始化为随机值 从头开始训练
scratch_net = torchvision.models.resnet18()
scratch_net.fc = nn.Linear(scratch_net.fc.in_features, 2)
# 所有层的学习率都是5e-3
train_fine_tuning(scratch_net, 5e-3, param_group=False, num_epochs=8)

此代码我在实验室服务器上运行成功了,但是因为实验室服务器的环境配置中torchvision的版本比较低,还不支持前面ResNet18_Weights的这种写法,因此我又写回了pretrained=True这种写法,在上面的代码中可以看到,一个运行结果如下图所示:

image-20250122162518268

可以看出,比起参数随机初始化的从头开始训练而言,用微调技术训练的模型对热狗数据集的分类效果更好。

参考

13.2. 微调 — 动手学深度学习 2.0.0 documentation

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值