没写清楚或者我理解不对的查看官方文档源码最为靠谱
目录
class torch.Tensor的反向传播函数
backward(gradient=None,retain_graph=None,creat_graph=False)
解析backward( )自动微分--自定义层
扩展autograd:适用于完全自己定义运算的层
直接定义函数并封装 (重要)
梯度校验gradcheck
detach( )和detach_( )
grad
is_leaf
神经网络工具torch.nn
torch.nn.Module
add_module(name, module)
modules( ) 和named_modules( )
children( )和named_children( )
cpu(device_id=None)和cuda(device_id=None)
eval()和train()
forward(*input)
parameters( )和named_parameters( )
to(*args,**kwargs)
zero_grad()
torch.nn.Parameter
FC层源码例证
查看学习参数_parameter
torch.nn.Sequential(* args)
torch.nn.ModuleList(Modules=None)
优化器torch.optim
构建优化器
自定义配置参数
不同module设置不同参数
不同层设置不同参数(待分析)
调整学习率
step()
zero_grad()
暂时只写部分常用的,结合网上部分理解进行测试和分析
计算当前tensor对各叶子结点的梯度。通过构建的计算图按照链式法则求导,如果tensor为标量,那么backward无需输入参数,非标量需要输入等维度权重矩阵,计算图在用完后会被废弃。主要会关注一下gradient,另外两个参数一般不用。
注意:此函数会累积叶子结点的梯度,调用函数之前需要清零叶子结点梯度;如果用了优化器,直接清零优化器就行,如下:
x.grad.data.zero_() #单个叶tensor
model.zero_grad() #整个模型梯度参数清零
optimizer.zero_grad() #当optimizer=optim.Optimizer(model.parameters())时,两者等效
如果tensor是标量无需传递参数,自动对叶子结点求导,但如果是一个矩阵,如loss=[loss1,loss2],需要传入一个和其相同尺寸的矩阵,计算loss矩阵各元素对叶子结点的倒导数。该矩阵元素可以全为1,也能设置不同的权重。如下:
loss.backward(torch.tensor([[1.0,1.0,1.0,1.0]]))
loss.backward(torch.tensor([[0.1,1.0,10.0,0.001]]))
loss.backward(torch.ones_like(n))
打开Tensor类的backward()属性查看源码,发现这部分的反向求导实际是假的,内部只有一个函数:torch.autograd.backward(self, gradient, retain_graph, create_graph),这才是自动求导的本体。再进入这个函数查看那源码,好了,看不懂.....
再看看怎么自定义自己的层:
这种层的运算无法通过torch基本运算实现,因此autograd无法追踪,需要自己计算反向传播函数,并进行相关的定义。
一般使用的nn.Module是nn.functional经过封装得到的,如nn.Conv2d继承了nn.Module但是内部torch.nn.function.conv2d,通过封装的Parameter将学习参数传入function进行学习。Function类本身是没有学习参数的(不像Module),只是单纯的接受输入输出,因此用torch.autograd.function自定义层,需要自己指定function的forward和backward函数,前向传播接受输入返回输出;反向传播接受输出的梯度返回输入的梯度。
查看Function类的代码:
class Function(object):
def forward(self, *input):
raise NotImplementedError
def backward(self, *grad_output):
raise NotImplementedError
注意:torch.autograd.Function和torch.nn.functional不一样,后者是用于进一步封装一些常用的Module而设计的(当然用户也可以根据自己的需要进行如卷积函数的卷积方式等的调整),前者则是用于完全重新定义自己的层。这里的比较很清楚:https://zhuanlan.zhihu.com/p/27783097
关于backward和forward进一步说明(结合下面的代码)
传入forward的参数已经有了requires_grad的标志(可学习的通过Parameter设置了);
形参可以有默认参数;
必须返回tensor,可以返回多个tensor
forward返回了几个值,这里除了ctx以外就还要传入几个形参;
forward 除ctx外有几个形参,backward就要返回几个tensor,并且grad和原传入的叶节点一一对应
ctx作为内部参数在前向反向传播中协调:
ctx.save_for_backward保存反向传播需要用到的参数;
ctx.saved_tensors读取参数
没有学习参数的层
这种层只用Function定义即可,没必要用Module封装,差别不大。(为了统一好看也可以封装).注意:Function定义没有构造函数__init__.
自定义ReLU函数(规范写法新式类,使用静态修饰器,尽量不要使用定义类-实例化-调用的路线):
import torch
from torch.autograd import Function
'后来看了下底层代码,ctx是默认参数,input不是,可以为任意字符串变量,只是习惯都这么写的而已'
class MyReLU(Function):
@staticmethod '静态修饰器,用该方法的类无需实例化即可调用'
def forward(ctx, input): 'ctx是默认参数,相当于self;第二个默认参数input,是输入数据'
ctx.save_for_backward(input) '为反向传播存储变量'
return x.clamp(min=0)
@staticmethod
def backward(ctx, grad_output):
x, = ctx.saved_tensors
grad_x = grad_output.clone()
grad_x[input < 0] = 0
return grad_x
'注意此处的调用方法'
a=torch.randn(2,3)
b=MyReLU.apply(a)
print(a)
print(b)
tensor([[ 0.5061, 0.1917, -0.0556],
[ 0.5597, -0.0638, 0.2077]])
tensor([[0.5061, 0.1917, 0.0000],
[0.5597, 0.0000, 0.2077]])
如果想要用Module封装一下(可以作为范式参考,不过module是自己写的,不知道规不规范,不过能用):
import torch
import torch.nn as nn
from torch.autograd import Function
class MyReLU(Function):
@staticmethod
def forward(ctx, input):
ctx.save_for_backward(input)
return input.clamp(min=0)
@staticmethod
def backward(ctx, grad_output):
input, = ctx.saved_tensors
grad_x = grad_output.clone()
grad_x[input < 0] = 0
return grad_x
class ReLU(nn.Module):
def __init__(self):
super(ReLU, self).__init__() 'super继承'
pass 'relu没啥初始化参数,就不用了'
def forward(self, input): '前向传播输入input数据'
return MyReLU.apply(input) '新式类apply调用function'
a=torch.randn(2,3)
relu=ReLU() '实例化Module'
print(a)
print(relu(a)) '隐式调用前向传播'
tensor([[-0.8195, -0.7637, -0.2855],
[ 3.0632, 1.1802, -0.3376]])
tensor([[0.0000, 0.0000, 0.0000],
[3.0632, 1.1802, 0.0000]])
有学习参数的层
有学习参数的层,在Functiuon部分实现相同,仅仅是Module封装的区别,在构造函数部分除了正常的非学习参数外,还要用Parameter类定义学习参数。
第一个例子(只看怎么封装module,function实现不推荐):
本来想找官方pytorch1.0的linear层的源码分析,结果发现他内层用的是torch.nn.functional而不是torch.autofrad.Function,并且里面有用jit很复杂,所以我用的0.4.0的源码:
首先是内层linear函数实现,实现了FC层运算就行,挺简单的:
def linear(input, weight, bias=None):
r"""
Applies a linear transformation to the incoming data: :math:`y = xA^T + b`.
"""
if input.dim() == 2 and bias is not None:
# fused op is marginally faster
return torch.addmm(bias, input, weight.t())
output = input.matmul(weight.t()) '矩阵转置乘法'
if bias is not None:
output += bias '加上偏置'
return output
注意这个地方和前面的有区别!这里的内层函数实现是直接定义函数功能的,没有继承autograd的Function定义前向反向传播,那么调用module的fake backward()时,这里的内层函数其实没有实现backwaard,它怎么反向传播的?不懂.....所以,自己写层的底层函数时,推荐继承torch.autograd.Function!
然后是外部Module的封装如下,在构造函数中初始化所有参数,用Parameter封装学习参数,便于喂给优化器等:
class Linear(Module):
'''
Examples::
>>> m = nn.Linear(20, 30)
>>> input = torch.randn(128, 20)
>>> output = m(input)
'''
def __init__(self, in_features, out_features, bias=True): '传入实例化参数'
super(Linear, self).__init__()
self.in_features = in_features '非学习参数'
self.out_features = out_features
self.weight = Parameter(torch.Tensor(out_features, in_features)) 'Parameter封装学习参数'
if bias:
self.bias = Parameter(torch.Tensor(out_features)) 'Parameter封装学习参数'
else:
self.register_parameter('bias', None)
self.reset_parameters()
...
def forward(self, input): '前向传播隐式调用方法,输入input'
return F.linear(input, self.weight, self.bias)
...
第二个例子(模版):
上面的FC层底层有点问题没懂,所以这里用Function标准化实现一个自定义线性层,可作为模版改动。
'''
定义函数实现:
1.继承自torch.autograd.Function
2.@staticmethod
3.第一个是ctx,第二个是input,bias可选参数
4.定义forword和backward
'''
class LinearFunction(Function):
@staticmethod
def forward(ctx, input, weight, bias=None):
ctx.save_for_backward(input, weight, bias) '为反向传播存储数据'
output = input.mm(weight.t()) '实现运算'
if bias is not None:
output += bias.unsqueeze(0).expand_as(output)
return output
@staticmethod
def backward(ctx, grad_output):
input, weight, bias = ctx.saved_tensors
grad_input = grad_weight = grad_bias = None
'ctx.needs_input_grad存放的是requires_grad的boolean,用于检查,确保健壮性,可以不要'
if ctx.needs_input_grad[0]:
grad_input = grad_output.mm(weight)
if ctx.needs_input_grad[1]:
grad_weight = grad_output.t().mm(input)
if bias is not None and ctx.needs_input_grad[2]:
grad_bias = grad_output.sum(0).squeeze(0)
return grad_input, grad_weight, grad_bias
'''
Module封装实现:
1.构造函数输入
2.设置属性变量,对于学习参数用Parameter封装
3.forward通过函数类的apply方法调用前向计算
'''
class Linear(nn.Module):
def __init__(self, input_features, output_features, bias=True):
super(Linear, self).__init__()
self.input_features = input_features
self.output_features = output_features
self.weight = nn.Parameter(torch.Tensor(output_features, input_features))
if bias:
self.bias = nn.Parameter(torch.Tensor(output_features))
else:
# You should always register all possible parameters, but the
# optional ones can be None if you want.
self.register_parameter('bias', None)
# Not a very smart way to initialize weights
self.weight.data.uniform_(-0.1, 0.1) '初始化'
if bias is not None:
self.bias.data.uniform_(-0.1, 0.1)
def forward(self, input):
# See the autograd section for explanation of what happens here.
return LinearFunction.apply(input, self.weight, self.bias)
a=torch.randn(3,10)
linear=Linear(10,5)
out=linear(a)
反向求导可以这么测试:
a=torch.randn(3,10)
a.requires_grad_(True) '因为默认的a是没有求导的,所以需要手动改'
linear=Linear(10,5)
out=linear(a)
# print(out)
out.backward(torch.ones_like(out)) '矩阵求导需要传入权值矩阵'
tensor([[ 0.0568, 0.2460, -0.0683, 0.0210, 0.0054, -0.1439, 0.0289, -0.1342,
-0.0350, -0.0637],
[ 0.0568, 0.2460, -0.0683, 0.0210, 0.0054, -0.1439, 0.0289, -0.1342,
-0.0350, -0.0637],
[ 0.0568, 0.2460, -0.0683, 0.0210, 0.0054, -0.1439, 0.0289, -0.1342,
-0.0350, -0.0637]])
如果要实现的运算可以通过torch的基本运算实现,那么直接自定义一个函数,实现运算即可,也可用module封装,作为forward函数,自动求导实现backward。
结论是:如果使用torch基本运算实现,即使在自定义的函数中也能被追踪记录求导,而不用自己去继承Function写反向传播!
下面是线性层的对比实验:
可以看出,固定随机数种子后,无论是采用Function还是自己定义的一个乘法加法实现的低配线性层,都能求得相同的结果,因此,推荐简单定义运算,使用基础函数运算实现复杂函数,利用自动微分求解反向传播
torch.manual_seed(2019)
class LinearFunction(Function):
@staticmethod
def forward(ctx, input, weight, bias=None):
ctx.save_for_backward(input, weight, bias)
output = input.mm(weight.t())
if bias is not None:
output += bias.unsqueeze(0).expand_as(output)
return output
@staticmethod
def backward(ctx, grad_output):
input, weight, bias = ctx.saved_tensors
grad_input = grad_weight = grad_bias = None
if ctx.needs_input_grad[0]:
grad_input = grad_output.mm(weight)
if ctx.needs_input_grad[1]:
grad_weight = grad_output.t().mm(input)
if bias is not None and ctx.needs_input_grad[2]:
grad_bias = grad_output.sum(0).squeeze(0)
return grad_input, grad_weight, grad_bias
'测试函数'
def linear_func(input,weight,bias):
output = input.mm(weight.t())
output += bias.unsqueeze(0).expand_as(output)
return output
class Linear(nn.Module):
def __init__(self, input_features, output_features, bias=True):
super(Linear, self).__init__()
self.input_features = input_features
self.output_features = output_features
self.weight = nn.Parameter(torch.Tensor(output_features, input_features))
if bias:
self.bias = nn.Parameter(torch.Tensor(output_features))
else:
self.register_parameter('bias', None)
self.weight.data.uniform_(-0.1, 0.1)
if bias is not None:
self.bias.data.uniform_(-0.1, 0.1)
# def forward(self, input):
# return LinearFunction.apply(input, self.weight, self.bias)
def forward(self,input):
return linear_func(input,self.weight ,self.bias)
a=torch.ones(3,10)
a.requires_grad_(True)
linear=Linear(10,5)
out=linear(a)
out.backward(torch.ones_like(out))
print(a.grad)
写完程序用这个来验证反向求导计算公式对不对,该函数通过数值逼近的方法进行验证
detach()截断计算流,从计算图中分离一个张量,并且不会被追踪求导,返回的张量requires_grad=False。需要注意的是新张量和原来的是共享内存的,使用inplace函数可以修改参数,但是这样会使backward求导报错(.data方法就不会,出了问题很难查找)
detach()_可以将变量从计算图中分离出来,作为新的叶节点,设置grad_fn=None,同样requires_grad=False.
用处很多,以类似finetune为例,计算部分的梯度参数截断计算图:
'y=A(x), z=B(y) 求B中参数的梯度,不求A中参数的梯度'
# 第一种方法
y = A(x)
z = B(y.detach())
z.backward()
# 第二种方法
'这种好理解'
y = A(x)
y.detach_()
z = B(y)
z.backward()
'截断了y取出来作为叶子结点,破坏了原来的序惯模型结构(实际上没有搭建出来),'
'由于叶节点是不求导的,因而梯度被从截断处阻断了无法向前传播更新,完成只更新后面的层,冻结前面的层'
注意:但是如果你也想用y来对A进行反向求导,就只能用第一种方法。因为第二种方法已经将A的输出给 detach(分离)了。
该属性默认为None,会在首次调用backward 计算导数时生成tensor存放梯度。后面的梯度累加也存放于此。
判断一个节点是否为叶子结点。叶子结点是由用户创建,并且不依赖于其他变量,需要求导的参数,其grad_fn参数为None。
这里说一下计算图的概念。计算图中包含算子(函数)和变量,计算图用于记录算子和变量之间关系。计算图的最终计算目标是根节点,由用户自行创建不依赖于其他变量的变量时叶子结点,利用链式法则可以很容易求得各叶子结点的梯度。那么对于通过函数计算得到的变量,有一个grad_fn属性会记录记录其反向传播函数
叶子结点的理解:
>>> a = torch.rand(10, requires_grad=True)
>>> a.is_leaf
True
>>> b = torch.rand(10, requires_grad=True).cuda()
>>> b.is_leaf
False
# b was created by the operation that cast a cpu Tensor into a cuda Tensor
>>> c = torch.rand(10, requires_grad=True) + 2
>>> c.is_leaf
False
# c was created by the addition operation
>>> d = torch.rand(10).cuda()
>>> d.is_leaf
True
这个类是素有模块或网络的基类,所有的层都必须继承这个类。
源码初始化包含以下部分:
def __init__(self):
self._backend = thnn_backend
self._parameters = OrderedDict() #存放学习参数
self._buffers = OrderedDict()
self._backward_hooks = OrderedDict() #几个钩子
self._forward_hooks = OrderedDict()
self._forward_pre_hooks = OrderedDict()
self._state_dict_hooks = OrderedDict()
self._load_state_dict_pre_hooks = OrderedDict()
self._modules = OrderedDict() #存放该处添加的子模块
self.training = True #训练/检测标志,针对BN、Dropout层
Module也可以包含其它Modules
,允许使用树结构嵌入他们。也可以将子模块赋值给模型属性(一般搭建复杂网络还是采用序惯模型和ModuleList比较好)。当调用.cuda()时会将该模型和其子模型都放到GPU上(Tensor转为cuda类型)。
下面展开他的一些常用方法:
将一个 child module
添加到当前 module
。 被添加的module
可以通过自定义的name属性来获取。
查看源码会发现,name属性不能为空,并且最好设置不一样的name,便于检索。(重复的name不会添加)
import torch.nn as nn
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.add_module("conv", nn.Conv2d(10, 20, 4))
#self.conv = nn.Conv2d(10, 20, 4) 和上面这个增加module的方式等价
model = Model()
print(model.conv)
Conv2d(10, 20, kernel_size=(4, 4), stride=(1, 1))
modules( )返回一个包含当前模型所有模块的迭代器,遍历获取模块信息;
named_modules()返回迭代器,包含全部自定义的name属性和模块信息(推荐)。
查看named_modules()源码:
def named_modules(self, memo=None, prefix=''):
if memo is None:
memo = set()
if self not in memo:
memo.add(self)
yield prefix, self
for name, module in self._modules.items():
if module is None:
continue
submodule_prefix = prefix + ('.' if prefix else '') + name
for m in module.named_modules(memo, submodule_prefix):
yield m
不难发现,named_modules()在yield返回生成器时,少返回name属性就能实现modules(),他也确实是这么干的。而named_modules()获取模块信息是通过遍历self._modules.items()实现,也就是当前模型的_modules属性,其中以字典的形式存放子模块,通过name可以访问value也就是模块信息。但其实_modules只有子模块信息,因此还需要制作全部模块信息。
使用例子如下:
import torch.nn as nn
class Model(nn.Module):
def __init__(self):
super(Model, self).__init__()
self.add_module("conv", nn.Conv2d(10, 20, 4))
self.add_module("conv1", nn.Conv2d(20 ,10, 4))
model = Model()
print(' children:')
for sub_module in model.children():
print(sub_module)
print(' modules:')
for sub_module in model.named_modules():
print(sub_module)
('', Model(
(conv): Conv2d(10, 20, kernel_size=(4, 4), stride=(1, 1))
(conv1): Conv2d(20, 10, kernel_size=(4, 4), stride=(1, 1))
))
('conv', Conv2d(10, 20, kernel_size=(4, 4), stride=(1, 1)))
('conv1', Conv2d(20, 10, kernel_size=(4, 4), stride=(1, 1)))
注意: 子模块中重复的模块不会重复打印!!!(主要是后面的children()有影响,modules的第一个全部模型还是会有的)如下:
l = nn.Linear(2, 2)
net = nn.Sequential(l, l)
for idx, m in enumerate(net.modules()):
print(idx, ':', m)
0 : Sequential(
(0): Linear(in_features=2, out_features=2, bias=True)
(1): Linear(in_features=2, out_features=2, bias=True)
)
1 : Linear(in_features=2, out_features=2, bias=True)
与上面的modules区别在于:children()返回的模块信息不包含当前自己,仅有子模块。
源码就不用看了,和named_modules类似,还更简单。
named_children()多返回一个name属性。
cpu()方法将所有的模型参数(parameters
)和buffers
复制到CPU;cuda()则是转移到GPU,可以指定设备号
eval()和train()
将模型设置为验证或者训练模式,这对于dropout和BN层等在不同阶段行为不同的层,含有以上层时务必记得在训练和检测时设置不同的模式。
可以看下源码:
#删掉了注释文档
def train(self, mode=True):
self.training = mode
for module in self.children():
module.train(mode)
return self
def eval(self):
return self.train(False)
不难发现,其实是用到了nn.Module的training属性,train()方法遍历模型的子模块,将所有模块的training置为True,不区分层;eval()方法则是调用了train()方法,只是标志位改为False。
计算前向传播。查看源码会发现,这部分是空的,因为当前模型的前向传播方式是由所有子模块共同决定的,此处需要自定义实现forward( )函数,指定前向传播方式。所有自定义的模型子类在其对应的这个位置也要实现自己的forward函数。
调用方式不建议显式调用,model.forward(input)而是采用:model(input)的形式隐式调用。
两个的区别不再赘述。named_parameters( )的name命名规则有两种:默认阿拉伯数字;按嵌套递进命名‘ . ’连接。在https://blog.csdn.net/mingqi1996/article/details/85549172中分析nn.Module部分有讲到。
作用是返回含有可学习参数的迭代器(没有不可学参数!),可遍历获取数据;会在optimizer设置时需要取出。
查看源码加深理解:
def parameters(self, recurse=True):
for name, param in self.named_parameters(recurse=recurse):
yield param
def named_parameters(self, prefix='', recurse=True):
gen = self._named_members(
lambda module: module._parameters.items(),
prefix=prefix, recurse=recurse)
for elem in gen:
yield elem
named_parameters( )还是基本实现代码,派生了parameters( )方法。这里看似返回的生成器只包含一个参数,但是在这两个函数的上一行定义的_named_members实际上提取了name,v两个参数,分别是名称和数据。调用方法是读取Module的_parameters属性,里面存放了当前模块定义的可学习参数(不含添加层的参数,那些参数在子modules的_parameters里),存储形式是有序字典。通过字典迭代器得到所有键值对。
有必要介绍一下常用的字典遍历方式:正常字典遍历得到的是key,采用items方法:dict.items()返回一个可迭代对象,迭代两个元素分别是key和value。如:
person={'name':'lizhong','age':'26','city':'BeiJing','blog':'www.jb51.net'}
for x,y in person.items():
print(x,y)
name lizhong
age 26
city BeiJing
blog www.jb51.net
有三种用法:
to
(device=None, dtype=None, non_blocking=False)
to
(dtype, non_blocking=False)
to
(tensor, non_blocking=False)
着重注意第一种,可以将模型加载到GPU上,如:model.to( 'gpu:0' ).train( )
将module
中的所有模型参数的梯度设置为0.一般有两种方法:
model.zero_grad()
optimizer.zero_grad() # 当optimizer=optim.Optimizer(model.parameters())时,两者等效
查看源码:
def zero_grad(self):
for p in self.parameters():
if p.grad is not None:
p.grad.detach_() #截断计算图取回梯度
p.grad.zero_() #inplace修改梯度
是通过遍历所有parameter,取回可学习参数tensor的梯度,置零完成的。
为了在自定义网络中添加自己设置的可学习参数,需要用到torch.nn.Parameter类。
Parameters是Tensor的子类,和后者的区别是,在用它创建对象时会自定设置requirse_grad=True。一般将其与Module一起用,如果将Parameters对象赋值给Module的属性(如自定义继承自nn.Module的线性层,self.weight定义其权值矩阵),那么该参数会自动加到Module的参数列表中(也就是说会出现在parameters()迭代器中,可以访问,用于optimizer设置进行这些参数的学习,访问方式下面会说)
注意:如果在当前网络中加入子模块(如卷积),子模块带有可学习参数(卷积核),子模块的学习参数是不会在当前模块的_parameters中出现的
以PyTorch的Linear层API为例,在官方文档查看该层的源码,只用关注__init__方法:
import math
import torch
from torch.nn.parameter import Parameter #注意从torch.nn.parameter导入基类Parameter
from .. import functional as F
from .. import init
from .module import Module
from ..._jit_internal import weak_module, weak_script_method
[docs]@weak_module
class Linear(Module): #继承自nn.Module
__constants__ = ['bias']
def __init__(self, in_features, out_features, bias=True):
super(Linear, self).__init__()
self.in_features = in_features #输入输出维度是非学习参数,直接赋值
self.out_features = out_features
#来了,权值矩阵是可学习参数,使用Parameter类封装,在Module下回自动传递给Module的_parameters属性,
#可以使用parameters()方法调用
self.weight = Parameter(torch.Tensor(out_features, in_features))
if bias:
self.bias = Parameter(torch.Tensor(out_features))
else:
self.register_parameter('bias', None)
self.reset_parameters()
def reset_parameters(self):
init.kaiming_uniform_(self.weight, a=math.sqrt(5))
if self.bias is not None:
fan_in, _ = init._calculate_fan_in_and_fan_out(self.weight)
bound = 1 / math.sqrt(fan_in)
init.uniform_(self.bias, -bound, bound)
@weak_script_method
def forward(self, input):
return F.linear(input, self.weight, self.bias)
def extra_repr(self):
return 'in_features={}, out_features={}, bias={}'.format(
self.in_features, self.out_features, self.bias is not None
)
注释分析见代码,这里提出注意事项:
测试代码为:
import torch as t
from torch import nn
class Linear(nn.Module): # 自定义层一般都继承nn.Module
def __init__(self, in_features, out_features, s): #类实例化传入的参数
super(Linear, self).__init__() # 一般都这么写,第一个参数是自定义的类名;等价于nn.Module.__init__(self)
self.w = nn.Parameter(t.randn(in_features, out_features))
self.b = nn.Parameter(t.randn(out_features))
self.s = t.Tensor(s)
def forward(self, x):
x = x.mm(self.w) # 矩阵乘x*w
return x + self.b.expand_as(x) #x*w+b(广播扩充到x的维度)
layer = Linear(4,3,3)
p=layer.parameters()
for i in p:
print(i)
Parameter containing:
tensor([[ 1.4464, 0.6851, 1.5925],
[-1.1364, 0.7490, 0.2329],
[-0.8073, 0.6702, 0.2039],
[-1.3104, -1.0499, -1.7284]], requires_grad=True)
Parameter containing:
tensor([ 0.1890, -1.5098, 0.0855], requires_grad=True) #没有s
以上面的自定义FC层进行三种方法的举例:
#类方法的字典索引
print('layer.b:{}'.format(layer.b))
print('layer.s:{}'.format(layer.s))
layer.b:Parameter containing:
tensor([ 1.0731, -1.1797, 0.1393], requires_grad=True)
layer.s:tensor([1.3116e-42, 0.0000e+00, 1.3116e-42]) #可以看出,打印的Parameter是有标识的,而不可学习参数则没有
#parameters()方法
params=layer.parameters()
for param in params:
print(param)
#结果
Parameter containing:
tensor([[ 0.7136, -1.0395, 0.2689],
[-1.9080, -1.0661, -1.5544],
[-0.1325, 0.6200, 0.0422],
[ 0.8645, 0.5063, -0.8369]], requires_grad=True)
Parameter containing:
tensor([ 0.2010, -0.3477, 1.9097], requires_grad=True) #只索引学习参数
#named_parameters()方法
params=layer.named_parameters()
for name,param in params:
print(name,' : ',param)
#结果
w : Parameter containing:
tensor([[-0.7323, -0.5557, 1.8584],
[ 1.2272, -0.7652, 1.7013],
[ 1.2756, -0.2243, -0.5890],
[-0.0723, -0.1321, -0.2032]], requires_grad=True)
b : Parameter containing:
tensor([ 1.3658, 1.0532, -1.1596], requires_grad=True) #多了一个name参数,为自定义的属性名
注意:一般用于遍历会使用返回生成器的parameters()或者named_parameters(),这里推荐使用后者,后者会返回一个name属性标注参数的名字(self自定义的属性名)。(查看了二者的源代码发现,parameters()是基于named_parameters()实现的,不同之处仅在于前者只yield返回了一个属性,而后者返回了两个)
一个时序容器,modules 会以他们传入的顺序被添加到容器中。用的很多,例子随便写就行了:
model = nn.Sequential(
nn.Conv2d(1,20,5),
nn.ReLU(),
nn.Conv2d(20,64,5),
nn.ReLU()
)
推荐方式:上述添加方式的modules的name属性是按照默认的阿拉伯数字编码的,大型网络不便于层级分类和操作,推荐先实例化一个空的sequential容器,再采用add_module()的方式逐个添加层,自定义name。
下面看看他的源码(很多定义的方法比较高级,虽然常用但是目前还没接触,暂时看不懂先不管了),列出看得明白的部分并分析如下:
'可以看出不仅能接受module作为输入,还能接受有序字典输入;
其实内部实现添加层还是用的add_module,但由于此处无法指定name,故选择循环的编号作为name'
def __init__(self, *args):
super(Sequential, self).__init__()
if len(args) == 1 and isinstance(args[0], OrderedDict):
for key, module in args[0].items():
self.add_module(key, module)
else:
for idx, module in enumerate(args):
self.add_module(str(idx), module)
...
'len方法可以查看模型的层数目'
def __len__(self):
return len(self._modules)
...
'这个容器是定义了forward方法的!通过循环,按照放入的顺序进行依次参数计算传播'
def forward(self, input):
for module in self._modules.values():
input = module(input)
return input
注意:容器是定义了forward方法的!通过循环,按照放入的顺序进行依次参数计算传播。forward方法是固定的,一般不改。可以用于构造模块化的网络层,比如ResNet的残差模块,卷积+BN等基本单元,yolov3也有借鉴这样的写法。
ModuleList类允许存储module为列表,可以像python list一样被索引,也可作为迭代器遍历其中的modules。和Sequential的主要区别在于ModuleList没有forward 方法,因此内部层没有连接。可以认为:ModuleList就是用于迭代的。
注意:ModuleList没有定义forward方法,不能直接给输入,只是用来迭代的;在真正定义的整体模型中,再自己定义ModuleList中各层的前向传播方式。(一般直接输入输出就行,但是有short cut、自定义层等特殊情况会夸层处理之类的)
其与python list有着高度类似,添加模型和索引、遍历方法相同(只定义了这三种方法,没有python list的pop等一堆操作,别乱用):
举个例子:
'这里存入的是三个层,也可以是嵌套的module,如sequential'
modellist = nn.ModuleList([nn.Linear(3,4), nn.ReLU(), nn.Linear(4,2)])
input = t.randn(1, 3)
'由于缺少forward函数,遍历对每个子模块设置前向传播方法--等效于sequential'
for model in modellist:
input = model(input)
' 下面会报错,因为modellist没有实现forward方法'
output = modelist(input)
使用ModuleList而不是list的原因:ModuleList是Module的子类,因此使用它时元素可以自动识别为子moddule,而list等不具有该特点,其下module的参数不能被封装到主module的parameter而无法学习。使用方法参考yolov3的Darknet模型,只是在其构造函数内初始化了ModuleList如下:
self.hyperparams, self.module_list = create_modules(self.module_defs)
在这个列表容器内的module(net-block)就都能像正常直接定义添加的卷积等层一样,识别其下子模块的可学习参数,封装到对应的parameter下(python的list当然不行)。
借鉴yolov3的构建模型方式:
yolov3-pytorch的实现和注释放在这里,在model.py中查看Darknet模型:https://github.com/ming71/yolov3-pytorch-annotation/blob/master/models.py
PyTorch将深度学习中常用的优化方法全部封装在torch.optim中,optim文件夹下有12个文件,包括1个核心的父类(optimizer)、1个辅助类(lr_scheduler)以及10个常用优化算法的实现类。optim中内置的常用算法包括adadelta、adam、adagrad、adamax、asgd、lbfgs、rprop、rmsprop、sgd、sparse_adam。所有的优化方法都是继承基类optim.Optimizer,并实现了自己的优化步骤。
不同的算法就没必要一个个看了,只关注通用的句法和实现。
构建Optimizer需要传入包含了需要优化的参数(必须都是Tensor对象)的可迭代对象(生成器、字典),关于这个生成器的结构,之前没讨论过,实际是Parameter类型,包含数据和求导标志位,数据是存放参数的tensor,分析可见:https://blog.csdn.net/mingqi1996/article/details/85549172#%E4%BC%98%E5%8C%96%E5%99%A8Optimizer。然后,你可以设置optimizer的参数选项,如学习率,权重衰减等。简单的SGD优化器例子:
'传入的是model.parameters(),前面分析过,该方法可以调出model的可学习参数,返回方式是一个可迭代的生成器'
optimizer = optim.SGD(model.parameters(), lr = 0.01, momentum=0.9)
注意: 如果要通过.cuda( ) 将模型移到GPU ,在为其构建优化器之前执行此操作。.cuda()之后的模型的参数将是与调用之前的对象不同的对象。(一般先把模型放到GPU,下一步紧接着就构建optimizer)
Optimizer
支持为每个参数单独设置选项(在finetune中经常用到)。若想这么做,不要直接传入iterable的生成器,而是传入iterable的dict。每一个dict都分别定 义了一组参数,并且包含一个param
键,这个键对应参数的列表。其他的键应该optimizer所接受的其他参数的关键字相匹配,并且会被用于对这组参数的优化。
如下是对LeNet的特征提取和分类部分分别设置不同的学习率:
# 如果对某个参数不指定学习率,就使用最外层的默认学习率
optimizer =optim.SGD([
{'params': net.features.parameters()}, # 学习率为1e-5
{'params': net.classifier.parameters(), 'lr': 1e-2}
], lr=1e-5)
optimizer
这种调用方式的原理分析,需要分析源码,首先看看基类Optimizer,所有其他类都是在其上实现的,并且继承了他的一些变量,下面是其构造函数:
def __init__(self, params, defaults):
self.defaults = defaults
'params是可迭代对象,内部应为tensor或dict'
if isinstance(params, torch.Tensor):
raise TypeError("params argument given to the optimizer should be "
"an iterable of Tensors or dicts, but got " +
torch.typename(params))
self.state = defaultdict(dict)
self.param_groups = [] '该列表注意一下,存放所有可迭代参数list(后面会内嵌dict)'
param_groups = list(params) 'list化全部可学习参数tensor,这里内嵌tensor'
if len(param_groups) == 0:
raise ValueError("optimizer got an empty parameter list")
'这里很关键:查看list的一个元素,如果输入的不是字典,输入也就是一个全部学习参数的生成器
那么给这个list内嵌的tensor转为dict,为原来的tensor加一个key为params(重要,通过这个来读取学习参数,和lr等key一样))'
if not isinstance(param_groups[0], dict):
param_groups = [{'params': param_groups}]
'源码解读:通过下面遍历,实现param_groups属性列表内每个子元素的完全封装,遍历后每个元素均包含key:params,lr,decay等,以及对应的value'
for param_group in param_groups:
self.add_param_group(param_group)
以上面的SGD源码为例,在其上进行注释:
class SGD(Optimizer):
'构造函数传入Parameter或字典的可迭代对象、学习率、动量等'
def __init__(self, params, lr=required, momentum=0, dampening=0,
weight_decay=0, nesterov=False):
if lr is not required and lr < 0.0:
raise ValueError("Invalid learning rate: {}".format(lr))
if momentum < 0.0:
raise ValueError("Invalid momentum value: {}".format(momentum))
if weight_decay < 0.0:
raise ValueError("Invalid weight_decay value: {}".format(weight_decay))
'将参数设置选项进行封装到defaults字典内'
defaults = dict(lr=lr, momentum=momentum, dampening=dampening,
weight_decay=weight_decay, nesterov=nesterov)
if nesterov and (momentum <= 0 or dampening != 0):
raise ValueError("Nesterov momentum requires a momentum and zero dampening")
super(SGD, self).__init__(params, defaults)
...
'单步执行方法'
def step(self, closure=None):
loss = None
if closure is not None:
loss = closure()
'经过封装,param_groups list每个元素均包含完整一套的default设置,以及自己的param,通过k-v索引,下面就是各default对象索引赋值'
for group in self.param_groups:
weight_decay = group['weight_decay']
momentum = group['momentum']
dampening = group['dampening']
nesterov = group['nesterov']
'遍历当前params的所有tensor,实现SGD'
for p in group['params']:
if p.grad is None:
continue
d_p = p.grad.data
if weight_decay != 0:
d_p.add_(weight_decay, p.data)
if momentum != 0:
param_state = self.state[p]
if 'momentum_buffer' not in param_state:
buf = param_state['momentum_buffer'] = torch.zeros_like(p.data)
buf.mul_(momentum).add_(d_p)
else:
buf = param_state['momentum_buffer']
buf.mul_(momentum).add_(1 - dampening, d_p)
if nesterov:
d_p = d_p.add(momentum, buf)
else:
d_p = buf
p.data.add_(-group['lr'], d_p)
return loss
总结来说,基类Optimizer有一个属性param_groups,以列表形式嵌套多个字典,每个字典包含一个学习的层/module的全部信息,以键值对的形式独立存储了params,momentum,lr等信息,为不同层的设置提供了可能性。回到最初的设置方法,参数传递的是一个内嵌dict的list,并且制定了param和lr的key,形式也就不奇怪了。
补充:
为了筛选出需要求导的参数进一步提高程序健壮性,还能在优化器参数param传递时,按照requires_grad再筛一次不求导的参数,如下用正则表达式实现:
optimizer = torch.optim.SGD(filter(lambda x: x.requires_grad, model.parameters()), lr=lr0, momentum=.9)
使用id()函数和正则表达式实现:
# 只为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[0], net.classifier[3]])
special_layers_params = list(map(id, special_layers.parameters()))
base_params = filter(lambda p: id(p) not in special_layers_params,
net.parameters())
optimizer = t.optim.SGD([
{'params': base_params},
{'params': special_layers.parameters(), 'lr': 0.01}
], lr=0.001 )
另一种形式 :
net = net()
lr = 0.001
'只有一个层'
conv5_params = list(map(id, net.conv5.parameters()))
base_params = filter(lambda p: id(p) not in conv5_params,
net.parameters())
optimizer = torch.optim.SGD([
{'params': base_params},
{'params': net.conv5.parameters(), 'lr': lr * 100},
, lr=lr, momentum=0.9)
'多个层'
conv5_params = list(map(id, net.conv5.parameters()))
conv4_params = list(map(id, net.conv4.parameters()))
base_params = filter(lambda p: id(p) not in conv5_params + conv4_params,
net.parameters())
optimizer = torch.optim.SGD([
{'params': base_params},
{'params': net.conv5.parameters(), 'lr': lr * 100},
{'params': net.conv4.parameters(), 'lr': lr * 100},
, lr=lr, momentum=0.9)
主要有两种做法。一种是修改optimizer.param_groups中对应的学习率,另一种是新建优化器。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况。
'方法1: 调整学习率, 手动decay, 保存动量'
for param_group in optimizer.param_groups:
param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
'方法2: 调整学习率,新建一个optimizer'
old_lr = 0.1
optimizer1 =optim.SGD([
{'params': net.features.parameters()},
{'params': net.classifier.parameters(), 'lr': old_lr*0.1}
], lr=1e-5)
执行单步优化并更新参数。基类的step方法是不定义的,一是因为都在各种派生优化算法实现,二是也可以提供给用户自己实现自定义优化方法。实现略,但是记得循环调用。
优化器存储的tensor梯度清零。每次优化之后一般都清掉,避免累加,除非是特殊需求(如batch),可以再step()之后立刻执行,免得忘了。