pytorch官方文档:Transfer Learning tutorial
cs231n关于Transfer Learning的文档:Transfer Learning-cs231n
迁移学习(Transfer Learning)
迁移学习是深度学习中很重要的一点。本身是指将已经训练好的模型参数迁移到新的模型。在实际情况中,很少会有人从头训练一个卷积神经网络。原因主要有两个,一是没有足够大的数据集来训练好一个网络;二是没有那么多精力和资源来重新测试一个网络。因此,人们会先在已有的大型数据集上,如ImageNet上训练,之后再使用训练好的模型来完成自己的任务,提高效率。
迁移学习主要有以下两种情景:1、将卷积网络作为固定的特征提取器。网络在已有大型数据集训练好后,去掉最后的全连接层,将剩下的部分作为固定的特征提取器,在自己的数据集上只训练分类器。
2、微调卷积网络。网络预训练好后,不仅替换掉最后的全连接层,而且在新的数据集上训练时,依旧对整个网络进行反传,微调参数。
微调(fine-tune)的注意事项:
选取微调形式的两个重要因素:新数据集的大小(size)和相似性(与预训练的数据集相比)。
牢记卷积网络在提取特征时,前面的层所提取的更具一般性,后面的层更加具体,更倾向于原始的数据集(more original-dataset-specific)。
四个基本原则:1、新数据集小而且相似时,不建议进行微调,以防止过拟合。最好是只训练线性分类器。
2、新数据集大而且相似时,因为有足够的数据,所以可以对整个网络进行训练微调。
3、新数据集小而且不相似时,因为数据集不相似,所以最好不要在网络最后面的层训练分类器,最好在网络更前一点来对SVM分类器进行训练(it might work better to train the SVM classifier from activations somewhere earlier in the network)。
4、新数据集大而且不相似时,因为数据集足够大,所以有充分的信心可以训练好。[数据集大就是可以随心所欲...]
此外,注意不要随意更改预训练网络的结构,学习率最好选择一个较小的开始。
以上是关于迁移学习的一些知识,预训练的模型在pytorch中有很多,接下来主要讲解如何使用VGG16_bn,行文的结构主要按照官方的文档,记录自己不懂和比较重要的地方。大家各取所需,互相学习指教。
想直接看重点的,可以直接看模型预训练部分。
Load data
在pytorch中,预训练的模型对输入的图片有最适的要求。图片的形状应为(3 x H x W),高和宽至少是224。需要使用mean = [0.485, 0.456, 0.406] 和 std = [0.229, 0.224, 0.225]进行标准化。
主要使用了这个包,transforms提供了对图片的基本操作,以下是涉及到具体函数。
Compose():将不同的transforms操作组合在一起。参数以列表形式存储。
(transforms)
RandomResizedCrop():将给定的图片以随机的大小和宽高比裁剪,并且调整大小为给定的大小。
(size, scale=(0.08, 1.0), ratio=(0.75, 1.3333333333333333), interpolation=2)
参数: size:输出的图片大小
scale:裁剪的大小范围,默认是原图的0.08-1.0
ratio: 才见到宽高比,默认是原图的3/4-4/3
interpolation:插值,默认是PIL.Image.BILINEAR
RandomHorizontalFlip():随机水平翻转给定的图片,默认以0.5的概率移动,也就是有50%的概率移动或者不移动。
(p=0.5)
Normalize():将一个tensor image根据给出的均值和标准差进行标准化。会对每一个通道进行标准化。
input[channel]=(input[channel])-mean[channel])/std[channel]
(mean, std)
参数: mean:均值,列表形式,给出各个通道的均值。 如[0.485, 0.456, 0.406]
std: 标准差,列表形式,给出各个通道的标准差。如[0.229, 0.224, 0.225]
Visualize a few images
目的是更好的理解对数据的增益变换(第一步的操作)。
难以理解的地方是numpy中的transpose操作。官方文档为:transpose
transpose()在二维矩阵时就是正常意义的转置,但是在高维的时候,就不是很容易理解。
(a,axes=None)
参数: a:输入的array
axes: 进行变换的轴顺序。
对于tensor,使用transpose(a, argsort(axes))。通常是(1,0,2)
感觉transpose的axes与shape是对应的,或者说是通过shape的变换更容易理解。
如x的shape是(2,3,4),此时的轴顺序为(0,1,2),x' = ((1,0,2)), 那么x'的shape变成了(3,2,4)。
在理解上,x为2个3 x 4的矩阵,当axes变为(1,0,2),相当于变成了3个2 x 4的矩阵,具体变换操作是第一个矩阵的第一行和和第二个矩阵的第一行组成新的一号矩阵 ,以此类推组成了新的三个矩阵。
如果是((0,2,1)),则变成了2个4 x 3矩阵,此时对于每个矩阵都是进行了转置操作。
clip():给定一个区间,对于输入的array如果在区间外,那么值变为区间的边界值(靠近的一侧),如给定区间为[0, 1],那么0.23经过clip()函数之后,变为0.
(a, a_min, a_max, out=None)
参数:a:输入
a_min:最小值,可以是标量,也可以是数组(此时是与输入取值对应进行比较),或者是none
a_max:最大值,同样可以是标量、数组或者none
out:输出
文档中的函数实现了将标准化的图片重新恢复到原来的图片,对于为什么transpose((1,2,0)),应该是相对于()而言。
.make_grid():实现图片的网格化输出。
.make_grid(tensor, nrow=8, padding=2, normalize=False, range=None, scale_each=False, pad_value=0
参数: tensor: 输出。tensor或者list,tensor应为(B x C x H x W)4维;list应为大小相同的图片
nrow: 每行中的图片数量,默认是8,最终的grid形状是(batch / nrow[行数], nrow[列数])
padding: 填充 默认是2
normalize: 标准化,默认是false,将图片减去最小的像素值并除以最大的像素值,使图片的像素范围为0-1,
range:与normalize配合使用,形式为元组(min,max),默认是从tensor中计算得到。
scale_each: 为ture,则分别缩放每一个图像,计算每一个图片的(min,max),而不是从所有图片(一个batch)中计算出(min,max), 默认是false
pad_value: 填充的像素值,默认是0
一般也就第一个参数会用到,其他都选择默认就行。
接下来才是重头戏:
模型预训练
在官方文档中,是按照训练模型(保存参数)-> 参数可视化 -> 具体的两种方式fine-tune和fixed feature extractor。
但是觉得不是很好理解,所以按照个人理解来讲解。、
(一)首先是讲解几个用到的函数:
state_dict(): 返回一个包含模型所有状态的字典,具体包括参数和持续的缓冲(persistent buffers,不知道怎么翻译,如果有好的翻译请留言我,谢谢)
state_dict(destination=None, prefix='', keep_vars=False)
参数: destination: 通过源码来看,其是最终的输出,默认是none,意思是从没有开始,不建议更改默认值。
prefix: 前缀,放在parameter和buffer前,默认是空,不建议修改
keep_vars: 默认是false,不建议修改,否则会影响param的赋值。
load_state_dict(): 加载预训练模型的参数。
load_state_dict(state_dict, strict=True)
参数: state_dict: 字典形式,包含预训练好的模型参数
strict: 默认为true,要求预训练模型的参数的key与新模型的参数key要一致。
(二)讲解两种主要的保存模型和加载模型的方法。Serialization semantics、
1、官方推荐方法:只保存模型参数
(the_model.state_dict(), PATH)
只加载模型参数
-
the_model = TheModelClass(*args, **kwargs)
-
the_model.load_state_dict((PATH))
2、保存和加载整个模型
(the_model, PATH)
加载模型
the_model = (PATH)
关于文件存储的后缀,
pth和pkl文件,自己通过cPickle测试是都一样,感觉pytorch官方使用pth文件更多一点,所以倾向于使用pth文件。
(三) 如何使用VGG16_bn预训练模型
官方源码:vgg16_bn
1、如何修改预训练模型的结构
要想修改预训练模型的结构,需要从源码知道模型设计时各部分名称。以VGG为例,从源码可以知道分为两大部分:
和。所以,需要修改最后的全连接部分时,代码如下:
-
vgg_16_bn = models.vgg16_bn(pretrained=True)
-
vgg_16_bn.classifier = ((512 * 7 * 7, 4096),
-
(True),
-
(),
-
(4096, 4096),
-
(True),
-
())
这样就可以修改最后的全连接层。
2、只加载预训练模型的参数,并与自己的网络结构对应参数匹配
这里需要用到.model_zoo.load_url()函数。使用方法如下
pretrain_state_dict = model_zoo.load_url('/models/vgg16_bn')
从而得到预训练参数。
-
# 得到自己网络模型的参数、
-
model_dict = model.state_dict()
-
# 将pretrained中与自己模型不匹配的参数去掉
-
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict}
-
# 更新自己的model_dict
-
model_dict.update(pretrained_dict)
-
# 加载参数
-
model.load_state_dict(model_dict)
因为,参数的键就是层的名字,因此自己的新模型中修改的层需要与预训练模型的对应层名字不一样,才能只加载自己想要的参数。
3、微调和固定参数不变
微调,个人觉得就是正常训练,不需要做其他的限制,一直训练得到自己想要的结果。
固定参数不变,需要保证各层的梯度不再回传,从而参数不变。需要保证参数的requires_grad == False,具体如下:
-
for param in model_conv.parameters():
-
param.requires_grad = False
写在最后,以上就是自己使用预训练模型的一些记录,想要将预训练模型作为自己网络结构的一部分的话,需要在def __init__(self)中,添加代码:
self.vgg_16_bn = models.vgg16_bn(pretrained=True)
这样就可以使用,当然还需要对最后的全连接进行改动。此外,初始化的时候,def __init__(self)的参数除了self还可以加其他参数。