动手学深度学习 4.2 模型参数的访问、初始化和共享
前言
从零开始学习ai文章系列计划是个人在《动手学深度学习》和《磨菇书》两本书的学习中的个人笔记,文章也会以课本中的章节分开,即每个章节一片笔记。我会尽量的把主要内容以及遇到的难点进行记录与解决,如果哪里有错误的欢迎指正。或者不清晰的可以直接查看原文部分。
《动手学深度学习》原文(课本):https://tangshusen.me/Dive-into-DL-PyTorch/#/
《动手学深度学习》代码:https://github.com/ShusenTang/Dive-into-DL-PyTorch
(由于有时候公式太多,可能会直接贴图片)
在3.3节(线性回归的简洁实现)中,我们通过init模块来初始化模型的参数。我们也介绍了访问模型参数的简单方法。本节将深入讲解如何访问和初始化模型参数,以及如何在多个层之间共享同一份模型参数。
我们先定义一个与上一节中相同的含单隐藏层的多层感知机。我们依然使用默认方式初始化它的参数,并做一次前向计算。与之前不同的是,在这里我们从nn中导入了init模块,它包含了多种模型初始化方法。
1 | import torch |
1 访问模型参数
对于Sequential实例中含模型参数的层,我们可以通过Module类的parameters()或者named_parameters方法来访问所有参数(以迭代器的形式返回),后者除了返回参数Tensor外还会返回其名字。

两种用法简单举例:
1 | for param in net.parameters(): |
下面,访问多层感知机net的所有参数:
1 | print(type(net.named_parameters())) |

可见返回的名字自动加上了层数的索引作为前缀。
我们再来访问net中单层的参数。对于使用Sequential类构造的神经网络,我们可以通过方括号[ ] 来访问网络的任一层。索引0表示隐藏层为Sequential实例最先添加的层。
1 | for name, param in net[0].named_parameters(): |

因为这里是单层的所以没有了层数索引的前缀。另外返回的param的类型为torch.nn.parameter.Parameter,其实这是Tensor的子类,和Tensor不同的是如果一个Tensor是Parameter,那么它会自动被添加到模型的参数列表里,来看下面这个例子。
1 | class MyModel(nn.Module): |

上面的代码中weight1在参数列表中但是weight2却没在参数列表中。
因为Parameter是Tensor,即Tensor拥有的属性它都有,比如可以根据data来访问参数数值,用grad来访问参数梯度。
1 | weight_0 = list(net[0].parameters())[0] |

2 初始化模型参数
PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,但我们经常需要使用其他方法来初始化权重。
PyTorch的init模块里提供了多种预设的初始化方法。
继续使用一开始的网络进行举例
1 | import torch |
在下面的例子中,我们将权重参数初始化成均值为0、标准差为0.01的正态分布随机数,并依然将偏差参数清零。
1 | for name, param in net.named_parameters(): |

下面使用常数来初始化权重参数。
1 | for name, param in net.named_parameters(): |

3 自定义初始化方法
先来看看PyTorch是怎么实现这些初始化方法的,例如torch.nn.init.normal_:
1 | def normal_(tensor, mean=0, std=1): |
可以看到这就是一个inplace改变Tensor值的函数,而且这个过程是不记录梯度的。 类似的我们来实现一个自定义的初始化方法。
在下面的例子里,我们令权重有一半概率初始化为0,有另一半概率初始化为[−10,−5]和[5,10]两个区间里均匀分布的随机数。
即图中的tensor对tensor内容的每个数做相同的计算赋值。
1 |
|
tensor.uniform_(-10, 10) 将每个数均匀分布初始化为[-10,10]内的数
(tensor.abs() >= 5) 如果该数的绝对值大于等于5,则该括号内的值为1否则为0
4 共享模型参数
在有些情况下,我们希望在多个层之间共享模型参数。4.1.3节提到了如何共享模型参数: Module类的forward函数里多次调用同一个层。此外,如果我们传入Sequential的模块是同一个Module实例的话参数也是共享的,下面来看一个例子:
1 | linear = nn.Linear(1, 1, bias=False) |

在内存中,这两个线性层其实一个对象:
1 | print(id(net[0]) == id(net[1])) |

因为模型参数里包含了梯度,所以在反向传播计算时,这些共享的参数的梯度是累加的:
简单说,PyTorch 反向传播时,若某个参数 w 在多个路径中使用,则其 .grad 的计算会改变成多次计算。
1 | linear = nn.Linear(1, 1, bias=False) |

上面原本课程中的例子。我们假设该2层线性层都是分离的,即为下图的 linear1 和 linear2 .

我们能看到我们算出了y=9的值。
此时反向传播,计算权重梯度如下

倘若共享w的话,前向传播会变成如下

其反向传播,更新w变成如下。

成功计算出程序输出中的6。
倘若我们使用3层共享的linear。那么根据以上公式,我们会得出w的梯度是\(\frac {dy} {dw}=\frac {d} {dw}(w^3x)=3w^2=27\)

重新计算
1 | x = torch.ones(1, 1) |

符合预期。
因为参数梯度叠加的问题,我估计应该不会在训练中使用共享层,或者说在训练中锁定共享层的参数,不进行修改。即对共享层
param.requires_grad = False 。