使用预训练模型进行微调最常见的方法是使用同预训练模型相同的网络结构,只更改最后输出层,在不同的数据上进行微调。除此之外,我们经常需要自己搭建部分网络来适应特殊的任务,下面具体说明如果使用Mxnet的symbol接口实现各种操作,symbol接口实现方式很久之前测试过,不知道现在是否有变动,最近一直使用Gluon。
1. 加载预训练模型修改输出层
(1)symbol接口实现
symbol接口加载模型如下所示,0表示epoch索引:
sym_vgg, arg_params_vgg, aux_params_vgg=mx.model.load_checkpoint('vgg', 0)
其中sym保存着VGG的网络结构,arg保存着模型参数,aux保存着辅助状态(貌似是训练时的trainer),我们可以根据sym,定义一个函数修改后面几层,以匹配我们的数据集输出维度,返回新的sym_new:
def get_fine_tune_model(symbol, num_classes, layer_name = 'pool5'):
all_layers = symbol.get_internals() #get_internals()得到symbol中所有内部节点
#得到原始pool5中的输出,可以理解为将pool5之前的层保存下来,之后的层舍弃,重新搭建
pool5_new = all_layers[layer_name+'_output']
conv6_new = mx.symbol.Convolution(data = pool5_new,...)
...
output_new = mx.symbol.SoftmaxOutput(data = fc_new, name = 'softmax')
return output_new
然后通过调用这个函数便可以得到新的sym:
sym_new = get_fine_tune_model(sym, num_classes)
然后便可以使用arg_params, aux_params去初始化sym_new,由于mxnet的初始化按照layer的名字匹配,所以无论是修改的结构还是修改了通道数,layer的名字一定要重新命名,否则会在初始化的时候出错。由于现在新的网络结构和arg_params中的名字不匹配,也就是网络结构中有的层,在arg_params并没有提供初始化参数,所以需要设置fit函数中的allow_missing=True,意思为允许不提供初始化参数,使用fit中的initializer将对应层随机初始化。
devs = [mx.gpu(2),mx.gpu(3)]
mod = mx.mod.Module(symbol=symbol, context=devs)
mod.fit(train, val,
num_epoch=200,
arg_params=arg_params,
aux_params=aux_params,
allow_missing=True,
batch_end_callback = mx.callback.Speedometer(batch_size, 10),
kvstore='device',
optimizer='sgd',
optimizer_params={'learning_rate':0.001,'momentum': 0.9},
initializer=mx.init.Xavier(rnd_type='gaussian', factor_type="in", magnitude=0.2),
eval_metric='acc',
epoch_end_callback=checkpoint)
(2)Gluon接口实现
首先load Gluoncv中model_zoo提供的imagenet预训练模型:
model_name = 'resnet50_v1d'
finetune_net = get_model(model_name, pretrained=True)
将最后的fc层改成16类输出:
with finetune_net.name_scope():
finetune_net.fc = nn.Dense(16)
随机初始化fc层并reset到指定显卡上:
ctx = [mx.gpu(3)]
finetune_net.fc.initialize(init.Xavier(), ctx = ctx)
finetune_net.collect_params().reset_ctx(ctx)
注意model_zoo中不同的模型最后输出层定义不同,有的是name是fc,有的name是output,需要注意。比如mobilenet1.0就定义为output,所以如果想修改mobilenet1.0的输出,就需要如下:
model_name = 'mobilenet1.0'
finetune_net = get_model(model_name, pretrained=True)
with finetune_net.name_scope():
finetune_net.output = nn.Dense(16)
finetune_net.output.initialize(init.Xavier(), ctx = ctx)
还要注意,model_zoo中有些模型最后输出不是一个简单的普通层,而是多个层组长的HybridSequential,所以可以按照原始网络的样子重新写个output,最终赋值过去:
net = get_model('mobilenetv3_large', pretrained=True)
output = nn.HybridSequential()
output.add(nn.Conv2D(16, kernel_size=(1, 1)),
nn.Flatten())
with net.name_scope():
net.output = output
net.output.initialize(mx.init.MSRAPrelu(), ctx=ctx)
所以最后没有固定的样子,按照原始网络的样子搭建就好,原始网络可以直接print。
2. 固定部分参数,调整学习率
(1)symbol接口实现
常见的固定参数的方式有两种,一种是将对应层的学习率调到0,一种是利用框架提供的接口,指定特定层固定。本节主要以框架提供接口的方式,实现固定特定的层,而对于调整学习率的方式,会给出可能的实现方式,有待验证。
主要参考mxnet提供的module API,函数参数包括symbol以及配置参数,其中参数fixed_param_names为指定固定参数的层,举例如下:
fixed_param_prefix = ['conv1_1_weight', 'conv1_1_bias']
mod=mx.mod.Module(symbol = symbol, context = devs, fixed_param_names = layer_name)
mod.fit(train, val, ...)
以上代码实现将模型中名字为conv1_1的卷积层的权值和偏置固定。
通过将学习率设置为0的方式应该也可以,不过可能由于仍然有梯度回传,所以速度不如直接按照上面指定固定的层速度快:
mod._optimizer.set_lr_mult({'conv1_bias' : 0, 'conv1_weight' : 0})
(2)Gluon接口实现
在得到net之后,可以直接靠索引等方式指明特定层,实现方便:
net.features.collect_params().setattr('lr_mult', 10) #set this layers lr_new=lr*10
net.features[0:4].collect_params().setattr('lr_mult', 0.1) #set this layers lr_new=lr*0.1
net.features.collect_params().setattr('grad_req', 'null') #fix params