目录
1 模型的搭建
1.1 模型定义的三要素
- 首先,必须继承 nn.Module 这个类,要让 PyTorch 知道这个类是一个 Module。
- 其次,在__init__(self)中设置好需要的“组件"(如 conv、pooling、Linear、BatchNorm等)。
- 最后,在 forward(self, x)中用定义好的“组件”进行组装,就像搭积木,把网络结构搭建出来,这样一个模型就定义好了
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool1 = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.pool2 = nn.MaxPool2d(2, 2)
self.fc1 = nn.Linear(16 * 5 * 5, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
#第一行是初始化,往后定义了一系列组件,如由 Conv2d 构成的 conv1,有 MaxPool2d构成的 poo1l。
#当这些组件定义好之后,就可以定义 forward()函数,用来搭建网络结构,
def forward(self, x):
x = self.pool1(F.relu(self.conv1(x)))
x = self.pool2(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
#x为模型的输入,第一行表示,x 经过 conv1,然后经过激活函数 relu,再经过 pool1 操作;
#第二行于第一行一样;第三行,表示将 x 进行 reshape,为了后面做为全连接层的输入;第四,第五行的操
#作都一样,先经过全连接层 fc,然后经过 relu;第六行,模型的最终输出是 fc3 输出。
至此,一个模型定义完毕,接着就可以在后面进行使用。
1.2 nn.Sequetial
torch.nn.Sequential
其实就是
Sequential
容器,该容器将一系列操作按先后顺序给包起来,方便重复使用,
例如
Resnet
中有很多重复的
block
,就可以用
Sequential
容器把重复的地方包起来。
下面为resnet34的代码:
#coding:utf8
from .BasicModule import BasicModule
from torch import nn
from torch.nn import functional as F
class ResidualBlock(nn.Module):
'''
实现子module: Residual Block
'''
def __init__(self, inchannel, outchannel, stride=1, shortcut=None):
super(ResidualBlock, self).__init__()
self.left = nn.Sequential(
nn.Conv2d(inchannel, outchannel, 3, stride, 1, bias=False),
nn.BatchNorm2d(outchannel),
nn.ReLU(inplace=True),
nn.Conv2d(outchannel, outchannel, 3, 1, 1, bias=False),
nn.BatchNorm2d(outchannel) )
self.right = shortcut
def forward(self, x):
out = self.left(x)
residual = x if self.right is None else self.right(x)
out += residual
return F.relu(out)
class ResNet34(BasicModule):
'''
实现主module:ResNet34
ResNet34包含多个layer,每个layer又包含多个Residual block
用子module来实现Residual block,用_make_layer函数来实现layer
'''
def __init__(self, num_classes=2):
super(ResNet34, self).__init__()
self.model_name = 'resnet34'
# 前几层: 图像转换
self.pre = nn.Sequential(
nn.Conv2d(3, 64, 7, 2, 3, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.MaxPool2d(3, 2, 1))
# 重复的layer,分别有3,4,6,3个residual block
self.layer1 = self._make_layer( 64, 128, 3)
self.layer2 = self._make_layer( 128, 256, 4, stride=2)
self.layer3 = self._make_layer( 256, 512, 6, stride=2)
self.layer4 = self._make_layer( 512, 512, 3, stride=2)
#分类用的全连接
self.fc = nn.Linear(512, num_classes)
def _make_layer(self, inchannel, outchannel, block_num, stride=1):
'''
构建layer,包含多个residual block
'''
shortcut = nn.Sequential(
nn.Conv2d(inchannel,outchannel,1,stride, bias=False),
nn.BatchNorm2d(outchannel))
layers = []
layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut))
for i in range(1, block_num):
layers.append(ResidualBlock(outchannel, outchannel))
return nn.Sequential(*layers)
def forward(self, x):
x = self.pre(x)
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
x = self.layer4(x)
x = F.avg_pool2d(x, 7)
x = x.view(x.size(0), -1)
return self.fc(x)
小结:
模型的定义就是先
继承
,再
构建组件
,最后
组装
。
其中基本组件可从
torch.nn
中获取,或者从
torch.nn.functional
中获取,同时为了方便重复使用组件,可以使用
Sequential
容器将一系列组件包起来,最后在
forward()
函数中将
这些组件组装成你的模型
2 权值初始化的⼗种⽅法
上一小节介绍了模型定义的方法,模型定义完成后,通常我们还需要对权值进行初始
化,才能开始训练。
初始化方法会直接影响到模型的收敛与否,在本小节,将介绍如何对模型进行初始
化。
2.1 权值初始化流程
总共两步,
第一步,先设定什么层用什么初始化方法,初始化方法在
torch.nn.init
中给出;
第二步,实例化一个模型之后,执行该函数,即可完成初始化
# 定义权值初始化
def initialize_weights(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
torch.nn.init.xavier_normal(m.weight.data)
if m.bias is not None:
m.bias.data.zero_()
elif isinstance(m, nn.BatchNorm2d):
m.weight.data.fill_(1)
m.bias.data.zero_()
elif isinstance(m, nn.Linear):
torch.nn.init.normal(m.weight.data, 0, 0.01)
m.bias.data.zero_()
这段代码基本流程是这样,先从
self.modules()
中遍历每一层,然后判断各层属于什么类型,例如,是否是
nn.Conv2d
、
nn.BatchNorm2d
、
nn.Linear
等,然后根据不同类型的
层,设定不同的权值初始化方法,例如,
Xavier
,
kaiming
,
normal_
,
uniform_
等。
Ps: kaiming
也称之为
MSRA
初始化,当年何恺明还在微软亚洲研究院,因而得名。
来看看第一行代码中的
self.modules()
,源码在
torch/nn/modules/module.py
中 。
def modules(self):
for name, module in self.named_modules():
yield module
功能是:
Returns an iterator over all modules in the network.
能依次返回模型中的各层,
例如:
接着,判断
m
的类型,属于什么类型,可以看到当前
m
属于
Conv2d
类型,则进行如下初始化:
以上代码表示采用
torch.nn.init.xavier_normal
方法对该层的
weight
进行初始化,并判断是否存在偏置
(bias)
,若存在,将
bias
初始化为全
0
。
这样,该层就初始化完毕,参照以上流程,不断遍历模型的每一层,最终完成模型的
初始化。
2.2 常用初始化方法
PyTorch
在
torch.nn.init
中ᨀ供了常用的初始化方法函数,这里简单介绍,方便查询使用。
介绍分两部分:
1. Xavier
,
kaiming
系列;
2.
其他方法分布
Xavier
初始化方法,论文在《
Understanding the difficulty of training deep feedforward neural networks
》
公式推导是从
“
方差一致性
”
出发,初始化的分布有均匀分布和正态分布两种。
1. Xavier 均匀分布
torch.nn.init.xavier_uniform_(tensor, gain=1)
xavier
初始化方法中服从均匀分布
U(−a,a)
,分布的参数
a = gain * sqrt(6/fan_in+fan_out)
,这里有一个
gain
,增益的大小是依据激活函数类型来设定
eg
:
nn.init.xavier_uniform_(w, gain=nn.init.calculate_gain('relu'))
PS
:上述初始化方法,也称为
Glorot initialization
2. Xavier 正态分布
torch.nn.init.xavier_normal_
(
tensor
,
gain=1
)
xavier
初始化方法中服从正态分布,
mean=0,std = gain * sqrt(2/fan_in + fan_out)
kaiming
初始化方法,论文在《
Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification
》,公式推导同样从
“
方差一致性
”
出法,
kaiming
是针对
xavier
初始化方法在
relu
这一类激活函数表现不佳而ᨀ出的改进,详细可以参看论文。
3. kaiming 均匀分布
torch.nn.init.kaiming_uniform_
(
tensor
,
a=0
,
mode='fan_in'
,
nonlinearity='leaky_relu'
)
此为均匀分布,
U
~(
-bound, bound
)
, bound = sqrt(6/(1+a^2)*fan_in)
其中,
a
为激活函数的负半轴的斜率,
relu
是
0。mode-
可选为
fan_in
或
fan_out, fan_in
使正向传播时,方差一致
; fan_out
使反向传播时,方差一致
nonlinearity-
可选
relu
和
leaky_relu
,默认值为 。
leaky_relu
nn
.
init
.
kaiming_uniform_(w, mode
=
'fan_in'
, nonlinearity
=
'relu'
)
4. kaiming 正态分布
torch.nn.init.kaiming_normal_
(
tensor
,
a=0
,
mode='fan_in'
,
nonlinearity='leaky_relu'
)
此为
0
均值的正态分布,
N
~
(0,std)
,其中
std = sqrt(2/(1+a^2)*fan_in)
其中,
a
为激活函数的负半轴的斜率,
relu
是
0
mode-
可选为
fan_in
或
fan_out, fan_in
使正向传播时,方差一致
;fan_out
使反向传播时,方差一致
nonlinearity-
可选
relu
和
leaky_relu
,默认值为 。
leaky_relu
nn
.
init
.
kaiming_normal_(w, mode
=
'fan_out'
, nonlinearity
=
'relu'
)
5. 均匀分布初始化
torch.nn.init.uniform_
(
tensor
,
a=0
,
b=1
)
使值服从均匀分布
U(a,b)
6. 正态分布初始化
torch.nn.init.normal_
(
tensor
,
mean=0
,
std=1
)
使值服从正态分布
N(mean, std)
,默认值为
0
,
1
7. 常数初始化
torch.nn.init.constant_
(
tensor
,
val
)
使值为常数
val
nn
.
init
.
constant_(w,
0.3
)
8. 单位矩阵初始化
torch.nn.init.eye_
(
tensor
)
将二维
tensor
初始化为单位矩阵(
the identity matrix
)
9. 正交初始化
torch.nn.init.orthogonal_
(
tensor
,
gain=1
)
使得
tensor
是正交的,论文
:
Exact solutions to the nonlinear dynamics of learning in deep linear neural networks” - Saxe, A. et al. (2013)
10. 稀疏初始化
torch.nn.init.sparse_
(
tensor
,
sparsity
,
std=0.01
)
从正态分布
N
~(
0. std
)中进行稀疏化,使每一个
column
有一部分为
0
sparsity-
每一个
column
稀疏的比例,即为
0
的比例
nn
.
init
.
sparse_(w, sparsity
=
0.1
)
11. 计算增益
torch.nn.init.calculate_gain
(
nonlinearity
,
param=None
)
2.3 权值初始化杂谈
1.
从代码中发现,即使不进行初始化,我们模型的权值也不为空,而是有值的,这些值是
在什么时候赋的呢?
其实,在创建网络实例的过程中
,
一旦调用
nn.Conv2d
的时候就会有对权值进行初始化
Conv2d
是继承
_ConvNd
,初始化赋值是在
_ConvNd
当中的
self.weight = Parameter(torch.Tensor(out_channels, in_channels // groups, *kernel_size))
这些值是创建一个
Tensor
时得到的,是一些很小的随机数
2.
按需定义初始化方法,例如:
if isinstance(m, nn.Conv2d):
n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
m.weight.data.normal_(0, math.sqrt(2. / n))
3 模型 Finetune
上一小节,介绍了模型权值初始化,以及
PyTorch
自带的权值初始化方法函数。我们知道一个良好的权值初始化,可以使收敛速度加快,甚至可以获得更好的精度。而在实际
应用中,我们通常采用一个已经训练模型的模型的权值参数作为我们模型的初始化参数,
也称之为
Finetune
,更宽泛的称之为迁移学习。迁移学习中的
Finetune
技术,本质上就是 让我们新构建的模型,拥有一个较好的权值初始值。
finetune
权值初始化三步曲,
finetune
就相当于给模型进行初始化,其流程共用三步:
第一步:保存模型,拥有一个预训练模型;
第二步:加载模型,把预训练模型中的权值取出来;
第三步:初始化,将权值对应的
“
放
”
到新模型中
3.1Finetune 之权值初始化
在进行
finetune
之前我们需要拥有一个模型或者是模型参数,因此需要了解如何保存模型。官方文档中介绍了两种保存模型的方法,一种是保存整个模型,另外一种是仅保存
模型参数(官方推荐用这种方法),这里采用官方推荐的方法
第一步:保存模型参数
若拥有模型参数,可跳过这一步。
假设创建了一个
net = Net()
,并且经过训练,通过以下方式保存:
torch.save(net.state_dict(), 'net_params.pkl')
第二步:加载模型
进行三步曲中的第二步,加载模型,这里只是加载模型的参数:
pretrained_dict = torch.load('net_params.pkl')
第三步:初始化
进行三步曲中的第三步,将取到的权值,对应的放到新模型中:
- 首先我们创建新模型,并且获取新模型的参数字典 net_state_dict:
- net = Net() # 创建 net
- net_state_dict = net.state_dict() # 获取已创建 net 的 state_dict
- 接着将 pretrained_dict 里不属于 net_state_dict 的键剔除掉: pretrained_dict_1 = {k: v for k, v in pretrained_dict.items() if k in net_state_dict}
- 然后,用预训练模型的参数字典 对 新模型的参数字典 net_state_dict 进行更新: net_state_dict.update(pretrained_dict_1)
- 最后,将更新了参数的字典 “放”回到网络中: net.load_state_dict(net_state_dict)
这样,利用预训练模型参数对新模型的权值进行初始化过程就做完了。
采用
finetune
的训练过程中,有时候希望前面层的学习率低一些,改变不要太大,而后面的全连接层的学习率相对大一些。这时就需要对不同的层设置不同的学习率,下面就介绍如何为不同层配置不同的学习率。
3.2 不同层设置不同的学习率
在利用
pre-trained model
的参数做初始化之后,我们可能想让
fc
层更新相对快一些,而希望前面的权值更新小一些,这就可以通过为不同的层设置不同的学习率来达到此目的。
为不同层设置不同的学习率,主要通过优化器对多个参数组进行设置不同的参数。所以,只需要将原始的参数组,划分成两个,甚至更多的参数组,然后分别进行设置学习率。
这里将原始参数
“
切分
”
成
fc3
层参数和其余参数,为
fc3
层设置更大的学习率。
ignored_params = list(map(id, net.fc3.parameters())) # 返回的是 parameters 的 内存地址
base_params = filter(lambda p: id(p) not in ignored_params, net.parameters()) # 返回 base params 的 内存地址
optimizer = optim.SGD([
{'params': base_params},
{'params': net.fc3.parameters(), 'lr': 0.001*10}], 0.001, momentum=0.9, weight_decay=1e-4)
第一行
+
第二行的意思就是,将
fc3
层的参数
net.fc3.parameters()
从原始参数net.parameters()
中剥离出来
base_params
就是剥离了
fc3
层的参数的其余参数,然后在优化器中为
fc3
层的参数单独设定学习率。
optimizer = optim.SGD(......)
这里的意思就是
base_params
中的层,用
0.001, momentum=0.9,weight_decay=1e-4
fc3
层设定学习率为:
0.001*10
3.3 补充:
挑选出特定的层的机制是利用内存地址作为过滤条件,将需要单独设定的那部分参数,从总的参数中剔除。
base_params 是一个 list,每个元素是一个 Parameter 类
net.fc3.parameters()
是一个
<generator object parameters>
ignored_params = list(map(id, net.fc3.parameters()))
net.fc3.parameters()
是一个
<generator object parameters at 0x11b63bf00>
所以迭代的返回其中的
parameter
,这里有
weight
和
bias
最终返回
weight
和
bias
所在内存的地址