现在的数据集越来越大,都是大模型的训练,参数都早已超过亿级,面对如此大的训练集,绝大部分用户的硬件配置达不到,那有没有一种方法让这些训练好的大型数据集的参数,迁移到自己的一个目标训练数据集当中呢?比如使用最广泛的图像数据集ImageNet,超过1000万张的图像和1000个分类,这些耗费大量时间人力物力而训练出来的参数,为我所用?
答案是肯定的,就是接下来说的微调(fine tuning),顾名思义就是细微的调节将这个已训练好的模型参数迁移过来,或者说复制(不是完全拷贝,故有点区别,所以叫微调)过来,最后再对自己的模型进行训练。
比如说我们的一个数据集是想找出图片中的热狗,但ImageNet数据集的图像大多于此无关,那迁移过来的参数有用吗?有用,因为在训练ImageNet中抽取的特征,如:边缘、纹理、形状等,对于识别物体都有同样的效果。
如何从源数据集迁移到目标数据集呢?方法就是将除了输出层的其余层的参数复制(做了微调)到目标数据集的除了输出层的其余层。而对于目标数据集的输出层,我们随机初始化该层的模型参数,然后将整个目标数据集重新训练一遍即可。
我们下载热狗数据集,来识别图像中的热狗的一个示例:
import d2lzh as d2l
from mxnet import gluon,init,nd
from mxnet.gluon import data as gdata,loss as gloss,model_zoo
from mxnet.gluon import utils as gutils
import os
import zipfile
#下载热狗数据集(如果超时等下载不了,就直接手动下载再解压)
#解压之后是hotdog目录,里面是train和test目录,分别都有hotdog和not-hotdog目录,存放热狗和非热狗(和热狗长的像的,比如香蕉之类)的图像
data_dir='../data'
fname=gutils.download('https://apache-mxnet.s3-accelerate.amazonaws.com/gluon/dataset/hotdog.zip')
with zipfile.ZipFile(fname,'r') as z:
z.extractall(data_dir)
当然如果遇到权限错误,比如我的是C盘存放,使用管理员权限的命令行执行即可。
PermissionError: [WinError 5] 拒绝访问。: '..\\data\\hotdog'
数据集下载下来了,我们先来显示正类图像(热狗)和负类图像(非热狗),熟悉下这个数据集,代码如下:
train_imgs=gdata.vision.ImageFolderDataset(os.path.join(data_dir,'hotdog/train'))
test_imgs=gdata.vision.ImageFolderDataset(os.path.join(data_dir,'hotdog/test'))
#print(len(train_imgs),len(test_imgs))#2000,800
#print(train_imgs[0])#..., 0)(高,宽,通道)和标签值,0是热狗,1是非热狗
#print(train_imgs.items[0])#('../data\\hotdog/train\\hotdog\\0.png', 0)
hotdogs=[train_imgs[i][0] for i in range(8)]
not_hotdogs=[test_imgs[-i][0] for i in range(8)]
d2l.show_images(hotdogs+not_hotdogs,2,8,scale=1.5)
d2l.plt.show()
从画布中显示的图像可以看出,都是些大小和宽高比都不一样的热狗与非热狗图。
在训练时,先从图像中裁剪出随机大小和随机高宽比的一块区域,然后将该区域缩放到高宽为224像素的输入。测试时,我们将图像的高宽缩放到256像素,然后从中裁剪出高宽为224的中心区域作为输入。此外,我们对颜色通道做标准化,就是每个数值减去所有数值的均值,再除以标准差。
# 对颜色通道做标准化处理
normalize = gdata.vision.transforms.Normalize(
[0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 均值,方差
train_augs = gdata.vision.transforms.Compose([gdata.vision.transforms.RandomResizedCrop(224), gdata.vision.transforms.RandomFlipLeftRight(),
gdata.vision.transforms.ToTensor(), normalize])
test_augs = gdata.vision.transforms.Compose([gdata.vision.transforms.Resize(256), gdata.vision.transforms.CenterCrop(224),
gdata.vision.transforms.ToTensor(), normalize])
使用ImageNet数据集上预训练的ResNet-18作为源模型。预训练模型的参数,将下载到:C:\Users\Tony\.mxnet\models里面的resnet18_v2-8aacf80f.params参数文件
pretrained_net = model_zoo.vision.resnet18_v2(pretrained=True)
#包含两个成员变量:features和output
#打印output输出层看下,输出1000类的全连接层
print(pretrained_net.features)#Dense(512 -> 1000, linear)
接下来新建目标模型,其定义和预训练的源模型一样,只不过将最后的输出数修改为目标数据集的类别数,因为features中的模型参数是已经在ImageNet数据集上预训练得到的,已经足够好,所以只需要使用较小的学习率来微调这些参数。对于output中的模型参数我们采用随机初始化,一般需要更大的学习率(10倍学习率)从头训练。
finetune_net=model_zoo.vision.resnet18_v2(classes=2)
finetune_net.features=pretrained_net.features
finetune_net.output.initialize(init.Xavier())
finetune_net.output.collect_params().setattr('lr_mult',10)
定义一个使用微调的训练函数,便于多次调用:
def train_fine_tuning(net,learning_rate,batch_size=128,num_epochs=5):
train_iter=gdata.DataLoader(train_imgs.transform_first(train_augs),batch_size,shuffle=True)
test_iter=gdata.DataLoader(test_imgs.transform_first(test_augs),batch_size)
ctx=d2l.try_all_gpus()
net.collect_params().reset_ctx(ctx)
net.hybridize()
loss=gloss.SoftmaxCrossEntropyLoss()
trainer=gluon.Trainer(net.collect_params(),'sgd',{'learning_rate':learning_rate,'wd':0.001})
d2l.train(train_iter,test_iter,net,loss,trainer,ctx,num_epochs)
我们以小的学习率0.01微调获得预训练模型参数,然后以10倍学习率从头训练目标模型的输出层参数,当然本人配置不是很好,批处理大小就弄小点,不然内存溢出。
train_fine_tuning(finetune_net,0.01,32)
'''
(pygpu) C:\Users\Tony>python p.py
training on [gpu(0)]
epoch 1, loss 1.5428, train acc 0.815, test acc 0.594, time 28.9 sec
epoch 2, loss 0.7645, train acc 0.864, test acc 0.921, time 24.9 sec
epoch 3, loss 0.5352, train acc 0.882, test acc 0.921, time 24.8 sec
epoch 4, loss 0.4242, train acc 0.882, test acc 0.774, time 24.8 sec
epoch 5, loss 0.3898, train acc 0.893, test acc 0.917, time 24.9 sec
'''
作为对比,定义一个相同模型,但是将它所有模型参数都是初始化为随机值,由于需要从头训练,学习率大点0.1。
scratch_net=model_zoo.vision.resnet18_v2(classes=2)
scratch_net.initialize(init=init.Xavier())
train_fine_tuning(scratch_net,0.1,32)
'''
(pygpu) C:\Users\Tony>python p.py
training on [gpu(0)]
epoch 1, loss 0.5667, train acc 0.750, test acc 0.818, time 27.9 sec
epoch 2, loss 0.4731, train acc 0.795, test acc 0.824, time 25.0 sec
epoch 3, loss 0.4109, train acc 0.818, test acc 0.854, time 24.9 sec
epoch 4, loss 0.3790, train acc 0.828, test acc 0.812, time 24.9 sec
epoch 5, loss 0.4080, train acc 0.826, test acc 0.853, time 24.9 sec
'''
如果不微调,直接使用源模型参数,将会怎么样呢?
finetune_net = model_zoo.vision.resnet18_v2(classes=2)
finetune_net.features=pretrained_net.features
finetune_net.features.collect_params().setattr('grad_req', 'null')
finetune_net.output.initialize(init.Xavier())
finetune_net.output.collect_params().setattr('lr_mult', 10)
测试几次的效果,loss效果比较差,精度还好,不稳定性要大点,可能是小数据集容易过拟合的原因,不知道伙伴们测试的效果如何?
我们也可以将微调得到的最终参数保存起来
finetune_net.collect_params().save('hotdog.params')
然后我们加载保存的参数看下:
mynet=model_zoo.vision.resnet18_v2(classes=2)
mynet.collect_params().load('hotdog.params')
train_fine_tuning(mynet, 0.01, 32)
出现如下错误:
AssertionError: Parameter 'resnetv22_batchnorm0_gamma' is missing in file 'hotdog.params', which contains parameters: 'resnetv20_batchnorm0_gamma', 'resnetv20_batchnorm0_beta', 'resnetv20_batchnorm0_running_mean', ..., 'resnetv20_batchnorm2_running_mean', 'resnetv20_batchnorm2_running_var', 'resnetv21_dense0_weight', 'resnetv21_dense0_bias'. Please make sure source and target networks have the same prefix.
很明显里面的前缀名不一致,这个就是没有固定前缀,在训练模型的时候都是动态递增的前缀,所以我们需要固定前缀,这样在加载的时候就指定前缀就好了
训练的时候,指定前缀:
pretrained_net = model_zoo.vision.resnet18_v2(pretrained=True,prefix='res_')
finetune_net = model_zoo.vision.resnet18_v2(classes=2,prefix='res_')
finetune_net.features=pretrained_net.features
finetune_net.output.initialize(init.Xavier())
finetune_net.output.collect_params().setattr('lr_mult', 10)
加载的时候,同样指定前缀即可:
mynet=model_zoo.vision.resnet18_v2(classes=2,prefix='res_')
mynet.collect_params().load('hotdog.params')
#print(mynet)
然后看下效果:
train_fine_tuning(mynet, 0.01, 32)
'''
(pygpu) C:\Users\Tony>python p.py
training on [gpu(0)]
epoch 1, loss 0.1788, train acc 0.933, test acc 0.939, time 28.0 sec
epoch 2, loss 0.1303, train acc 0.947, test acc 0.926, time 25.7 sec
epoch 3, loss 0.1398, train acc 0.948, test acc 0.943, time 25.7 sec
epoch 4, loss 0.1236, train acc 0.953, test acc 0.939, time 25.1 sec
epoch 5, loss 0.1157, train acc 0.955, test acc 0.941, time 25.1 sec
'''
目前的输出层我们使用的是随机初始化,现在我们将ImageNet数据集中的热狗这个类的权重参数应用到我们的初始化里面来,看下有什么效果:
weight=pretrained_net.output.weight
hotdog_w=nd.split(weight.data(),1000,axis=0)[713]
output_init=nd.concat(hotdog_w,-hotdog_w,dim=0)#将热狗的权重分一个负类的代表非热狗
print(output_init)
'''
[[-0.07650785 0.02459255 0.00455526 ... -0.06427797 -0.01825024
-0.02214353]
[ 0.07650785 -0.02459255 -0.00455526 ... 0.06427797 0.01825024
0.02214353]]
'''
finetune_net.output.initialize(init.Constant(output_init))#加载ImageNet里的热狗权重参数(增加一个负类)
finetune_net.output.collect_params().setattr('lr_mult', 10)
train_fine_tuning(finetune_net, 0.01, 32)
'''
training on [gpu(0)]
[12:41:37] c:\jenkins\workspace\mxnet-tag\mxnet\src\operator\nn\cudnn\./cudnn_algoreg-inl.h:97: Running performance tests to find the best convolution algorithm, this can take a while... (set the environment variable MXNET_CUDNN_AUTOTUNE_DEFAULT to 0 to disable)
epoch 1, loss 1.3655, train acc 0.804, test acc 0.818, time 27.9 sec
epoch 2, loss 0.6536, train acc 0.875, test acc 0.902, time 25.0 sec
epoch 3, loss 0.5011, train acc 0.885, test acc 0.934, time 25.1 sec
epoch 4, loss 0.3031, train acc 0.913, test acc 0.922, time 25.0 sec
epoch 5, loss 0.2167, train acc 0.917, test acc 0.924, time 24.9 sec
'''