微调概念
微调(fine-tuning)是深度学习特别是计算机视觉领域中最重要的一个技术。微调也叫迁移学习Transfer Learning。
他的核心思想是说就像人类学习一样,先掌握一些大量的基础知识,之后再想要掌握别的方面的知识的时候,就只需要再学习一点点数据就可以了。把这段话类比在人工智能中,其实就是说模型先在一个很大的数据集上进行了训练,得到一个预训练好的模型,之后再把这个模型迁移到别的任务中的时候,只需要学习一个相对来说不那么大甚至很小的数据集,就可以在该任务上取得比较好的效果。与此同时,其他领域的其他任务可能正好只能构造出来一个很小的数据集,通过应用微调技术,这个数据集比较小的问题就迎刃而解了。
模型的网络架构可以简单的分成两个部分,如下图所示,一个部分是靠近数据集开始的第1层一直到最后第L-1层,这部分是用于进行数据的特征抽取的,并且此特征抽取是可学习的;另一个部分是最后的输出层,比如如果说分类任务的话,这一层可能就是一个线性层/全连接层,可能还加了一个softmax
回归。网络架构的特征抽取部分越底层提取的是数据越抽象的特征,越顶层提取的是更加语义化的特征。因此在目标数据集上训练目标模型的时候,首先网络架构要同预训练的模型的一样,比如都使用resnet18模型,然后在模型权重初始化的时候,将特征抽取部分的模型权重全部都copy预训练模型的,而最后面输出层的权重可采用随机初始化来从头训练,因为目标数据集的labels很大可能是同源数据集不一样的,因此输出层不能copy。而在训练的时候,因为Loss、grad等是从顶层开始往下传的,所以最后一层的权重更新也比较迅速,能比较快的就训练好,而下面的层的权重在Loss、grad传过来的时候只需要进行细微的调整即可,这也是微调的地方所在。
这样通过微调的技术,就能够使得预训练好的模型在目标数据集上微调一下参数,就可以达到一个很好的效果,这也是深度学习能够迅速的在工业界应用的一个前提所在。
核心代码
利用深度学习框架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这种写法,在上面的代码中可以看到,一个运行结果如下图所示:
可以看出,比起参数随机初始化的从头开始训练而言,用微调技术训练的模型对热狗数据集的分类效果更好。