![f7507a9763db6c4df23c557aede60754.png](https://img-blog.csdnimg.cn/img_convert/f7507a9763db6c4df23c557aede60754.png)
那么好,我们今天来看一看优化器和参数更新
在进入这一部分之前,我觉得有必要说明自动求导和反向传播的数学定义以及在编程中实现的原理, 在网上找到了这一篇文章觉得很不错,
【Autograd】深入理解BP与自动求导okcd00.oschina.io![c342c83d299af60752f15a830f2183e6.png](https://img-blog.csdnimg.cn/img_convert/c342c83d299af60752f15a830f2183e6.png)
前文中说到,backward会求出loss的导数,这里补充一个上一篇文章中没有提到的点:
在engine求导过程中,所有的节点都会依次求出导数,对于之前的参数层,则会求出梯度向量(或者说Jacobian矩阵,因为目标函数是一个标量),这是一个关于参数的一阶导数矩阵(自变量为输入的数据,输出为图的输出)。 然后计算出hessian矩阵(的一部分,完整的hessian矩阵无法被计算并表述),这是一个关于参数的二阶偏导数矩阵,得到的数值将会被放进Variable.grad.data中,接下来,我们回到python。
参数获取(nn.Module.parameters()
)
parameters方法会获得当前的参数列表,它将是一个迭代器,包含了你定义的模块中的所有的参数, 而print parmeters对象则会给你返回字符串样式的提示
In[2]: model.parameters
Out[2]:
<bound method Module.parameters of Net(
(conv1): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(20, 50, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=800, out_features=500, bias=True)
(fc2): Linear(in_features=500, out_features=10, bias=True)
)>
In[3]: model.parameters()
Out[3]: <generator object Module.parameters at 0x0000019F23B80678>
具体实现逻辑如下:
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
def _named_members(self, get_members_fn, prefix='', recurse=True):
r"""Helper method for yielding various names + members of modules."""
memo = set()
modules = self.named_modules(prefix=prefix) if recurse else [(prefix, self)]
for module_prefix, module in modules:
members = get_members_fn(module)
for k, v in members:
if v is None or v in memo:
continue
memo.add(v)
name = module_prefix + ('.' if module_prefix else '') + k
yield name, v
逻辑很简单,之前也有一个add_modules方法,这里我没有贴出来,这里完成了模型的参数注册,说到参数,我觉得可以多说一句, conv的源代码中有专门对参数的描写,而且也有专门的Parameter类,这里附一下conv的parameters实现和类的实现
if transposed:
self.weight = Parameter(torch.Tensor(
in_channels, out_channels // groups, *kernel_size))
else:
self.weight = Parameter(torch.Tensor(
out_channels, in_channels // groups, *kernel_size))
if bias:
self.bias = Parameter(torch.Tensor(out_channels))
else:
self.register_parameter('bias', None)
self.reset_parameters()
# 可以看到,这里的参数使用了Parameter类,我们自己写一些操作需要参数的时候,可以直接使用这个类,免去了注册之类的麻烦。
class Parameter(torch.Tensor):
def __new__(cls, data=None, requires_grad=True):
if data is None:
data = torch.Tensor()
return torch.Tensor._make_subclass(cls, data, requires_grad)
def __deepcopy__(self, memo):
if id(self) in memo:
return memo[id(self)]
else:
result = type(self)(self.data.clone(), self.requires_grad)
memo[id(self)] = result
return result
def __repr__(self):
return 'Parameter containing:n' + super(Parameter, self).__repr__()
def __reduce_ex__(self, proto):
# See Note [Don't serialize hooks]
return (
torch._utils._rebuild_parameter,
(self.data, self.requires_grad, OrderedDict())
)
# 这个类的实现逻辑还是非常pythonic的,全部使用的魔术方法(运算符重载),虽然说逻辑并不难,就是注册了一些需要求导的张量罢了。
ok,我们现在知道优化器里面输入的是什么了,接下来,我们说一会数学:
参数更新的数学原理
首先,需要明确的是,pt的求导是对实值执行求导而不是对表达式进行求导,这一点的差别会产生误差的问题,这是为了动态性而做的牺牲,不过,这样也带来了一个好处, 在计算参数应该下降的数值时,可以直接用
值得一提的是: 有很多人都说同样一个网络,pt和tf训练的结果有区别,其实这里就是导致区别的一个点。另外还有一个点则是初始参数的问题,不过并不是主要的。
mnist中,优化器用的是SGD,这里面也包括了动量之类的一系列的东西,具体公式再官网文档里面有写,这里就不在赘述了,计算之后的参数会直接更新在之前的参数向量 中,到这里,一次迭代就全部完成了。
CUDA
接下来说一点额外的把,就是cuda和cudnn,关于并行计算的,其实在前面的forward计算中,pt会直接调用c的代码,如果你在之前指定过model.cuda(),那么这个时候就会 直接调用cuda或者cudnn的代码。
我们之前写pt的c或者cuda代码的时候,通常都是转换成numpy或者py的列表,然后通过swig转换为c的数据类型,不过,看了源码发现,pt有自己的cuda tensor实现,可以直接include相关的头文件,会省事很多,不过这个是一个大坑,后一篇文章再讲吧,我们下期再见吧。