合并隐藏层
如果模型中只通过单个仿射变换将输入直接映射到输出,这其中的“线性”意味着单调假设:特征任何增大都有可能导致模型输出增大(若权重为正)或者模型输出减少(为负)
对于感知机,它无法拟合 XOR 函数,只能产生线性分割面
为克服线性模型的限制,可将许多连接层堆叠在一起,每一层都输出到上面的层,直到生成最后的输出。可将前L−1 层看作“表示”,将最后一层看作“线性预测器”,该架构称为多层感知机(multilayer perceptron,简称 MLP)
多层感知机解决了感知机 XOR 问题,但相较于 SVM:①需要选择多个超参数;②收敛较难;③SVM数学性更好一些
例如下图的多层感知机(4输入3输出,隐藏层有5个隐藏单元,层数为2且均为全连接层),每个输入都会影响隐藏层中每个神经元,同理,隐藏层中每个神经元又会影响输出层中的每个神经元。

其中,隐藏层的大小为超参数。
“一层”通常指可学习的权重的一层,h 则是在激活函数之后的
多隐藏层
为构建更通用的多层感知机,从而产生更有表达能力的模型,可以堆叠多个上述的隐藏层。使用更深(注意不是更广)的网络能够更容易地逼近许多函数。
此时,超参数:隐藏层数、每层隐藏层的大小
如下:
h1=σ(W1x+b1)h2=σ(W2h1+b2)h3=σ(W3h2+b3)o=W4h3+b4

激活函数——从线性到非线性
为发挥多层结构的潜力,需要在仿射变换后对每个隐藏单元应用非线性的激活函数σ,其输出值称为激活值,从而保证多层感知机不会退化为线性模型,如下:
H=σ(XW(1)+b(1))O=XW(2)+b(2)
它们将输入信号转换为输出的可微运算
大多数激活函数为非线性,主要为了避免层数的“塌陷”
常用的激活函数包括 ReLU 函数、sigmod 函数、tanh 函数
最常用的即是 线性整流单元(Rectified linear unit, ReLU),实现简单且在各种预测任务中表现良好。
ReLU(x)=max(x,0)
也就说,ReLU 函数将正元素保留,所有负元素都被置为0

当输⼊为负时,ReLU 函数的导数为0,而当输⼊为正时,ReLU 函数的导数为1。注意,当输⼊值精确等于0时,ReLU 函数不可导。在此时,我们默认使⽤左侧的导数,即当输⼊为0时导数为0。
使用 ReLU 的原因:求导表现良好,要么使参数消失、要么使参数通过;优化表现更好,减轻神经网络的梯度消失问题(见下文)
使用 sigmod 函数,指数运算开销较大,在CPU上进行一次指数运算,其开销相当于100次乘法运算。
梯度爆炸与梯度消失
考虑具有d 层的神经网络,在计算损失函数l 的梯度Wt 时,可以观察到共有d−t 个矩阵进行相乘:
∂Wt∂l=∂hd∂l∂hd−1∂hd...∂ht∂ht+1∂Wt∂ht
梯度爆炸
梯度爆炸(gradient exploding)问题:参数更新过大,破坏了模型的稳定收敛
导致的问题:
梯度消失
梯度消失(gradient vanishing)问题:参数更新过小,在每次更新时几乎不会移动,导致无法学习
常见原因:

梯度是累乘回传的,因此靠近数据的层得到的梯度会变得很小
每层线性运算之后的激活函数σ —— Sigmoid 函数。如下图,当 Sigmoid 函数输入比较大或比较小时,其梯度就会消失。当反向传播通过许多层时,除非Sigmoid 函数输入接近于0,否则整个乘积的梯度可能会消失。

由此,才会选择更加稳定的 ReLU 系列函数
导致的问题:
- 梯度值将趋近于为0的渐变:对于16位浮点数,当梯度值小于 $2^{-24} \approx 5.96\times 10^{-8} $ 即为0
- 训练没有进展:无论如何选择学习率
- 底层训练基本无效:只有顶层训练会有效。将网络变得更深,可能并没有更好的效果
稳定模型训练
目标:确保渐变值在适当的范围内,如[10−6,103]
方法:
- 改变神经网络框架结构(乘法变为加法),如 ResNet,LSTM
- 批量归一化,渐变修剪
- 适当的权重初始化以及激活函数
PyTorch 神经网络的块
更多复杂的神经网络结构,是层组的重复模式组成。
对于神经网络的块,它可以描述单个层、由多个层组成的组件或者整个模型本身。
多个层被组合成块,再使用块来抽象,能够将一些块组合成更大的组件。

在PyTorch中,块由类(class)表示。
顺序块
在线性回归模型中,我们通过实例化 nn.Sequential 来构建模型。该 nn.Sequential 定义了一种特殊的 Module (即在 PyTorch 中表示一个块的类),它维护了一个由 Module 组成的有序列表。层的执行顺序,是作为参数进行传递的。
1 2 3 4 5 6 7 8 9
| import torch from torch import nn from torch.nn import functional as F
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) X = torch.rand(2, 20) net(X)
|
1 2 3 4
| tensor([[-0.1277, -0.0921, -0.0567, 0.0012, -0.1954, -0.0261, 0.1818, -0.0811, 0.0870, -0.2461], [-0.0374, -0.0249, -0.0311, -0.0926, -0.1382, -0.1261, 0.2243, -0.1285, 0.0920, -0.3513]], grad_fn=<AddmmBackward>)
|
自定义块
自定义块能够更加灵活地定义参数,以及如何进行正向传播(执行控制流,或者执行自定义的数学运算)
实现自定义块需要提高下列方法:
- 自己的构造方法:
__init__(),根据需要初始化模型参数 - 正向传播方法:
forward(),其中将输入数据作为参数。通过正向传播方法来生成输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| class MLP(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(20, 256) self.out = nn.Linear(256, 10) def forward(self, X): return self.out( F.relu( self.hidden(X) ) ) net = MLP() net(X)
|
1 2 3 4
| tensor([[ 0.0847, 0.1138, 0.1566, -0.0616, 0.1425, -0.1011, 0.0108, 0.1416, -0.0965, -0.0415], [ 0.0932, 0.0636, 0.2682, -0.0951, 0.0942, -0.0673, -0.0293, 0.1489, -0.1779, -0.0966]], grad_fn=<AddmmBackward>)
|
自定义带参数的层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class MyLinear(nn.Module): def __init__(self, in_units, units): super().__init__() self.weight = nn.Parameter(torch.randn(in_units, units)) self.bias = nn.Parameter(torch.randn(units, )) def forward(self, X): linear = torch.matmul(X, self.weight.data) + self.bias.data return F.relu(linear)
dense = MyLinear(5, 3) print(dense.weight)
print(dense(torch.rand(2, 5))) net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1)) net(torch.rand(2, 64))
|
1 2 3 4 5 6 7 8 9 10
| Parameter containing: tensor([[ 1.3052, 0.0695, -0.0955], [-1.0995, -0.9732, 0.4702], [ 0.1498, -0.6493, 1.1616], [ 0.3355, 0.7083, 0.7779], [-1.1136, -0.4356, -0.4151]], requires_grad=True) tensor([[0.0000, 0.0000, 0.0000], [0.7157, 0.0000, 0.0000]]) tensor([[6.6546], [0.0000]])
|
PyTorch 模型参数管理
参数管理包括以下三个内容:
- 访问参数,用于调试、诊断和可视化
- 参数初始化
- 在不同模型组件之间共享参数
参数访问
对于 nn.Sequential 类定义的模型,可通过索引来访问模型的任意层(类似于列表)
首先定义模型:
1 2 3
| net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1)) X = torch.rand(size=(2, 4)) net(X)
|
访问模型的参数,其中每个参数都表示为参数(Parameter) 类的一个实例,注意是一个复合的对象,包含数值本身、梯度及额外信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| print(net[2].state_dict())
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
print(net[2].weight.grad == None)
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
|
从嵌套块中收集参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def littleblock(): return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 4), nn.ReLU())
def bigblock(): net = nn.Sequential() for i in range(4): net.add_module(f'my block {i}', littleblock()) return net
rgnet = nn.Sequential(bigblock(), nn.Linear(4, 1)) rgnet(X) print(rgnet)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| Sequential( (0): Sequential( (my block 0): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) (my block 1): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) (my block 2): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) (my block 3): Sequential( (0): Linear(in_features=4, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=4, bias=True) (3): ReLU() ) ) (1): Linear(in_features=4, out_features=1, bias=True) )
|
参数初始化
默认情况下,PyTorch会根据一个范围(根据输入和输出维度计算)均匀地初始化权重和偏置矩阵
而 nn.init 模块提供了多种预置初始化方法
(一)内置初始化:例如下列代码,调用内置初始化器,对所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。
TIPS:诸如 normal_ 中的下划线 _ ,一般指原地替换操作
均匀分布:服从U(a,b)
均匀分布,即相同长度间隔的分布概率是等可能的。它由两个参数a 和b 来定义,分别表示数轴上的最小值和最大值,可写为U(a,b)
1
| torch.nn.init.uniform_(tensor, a=0, b=1)
|
高斯分布:服从N(mean,std)
1
| torch.nn.init.normal_(tensor, mean=0, std=1)
|
初始化常数(不推荐)
1
| torch.nn.init.constant_(tensor, val)
|
Xavier
1 2
| torch.nn.init.xavier_uniform_(tensor, gain=1) torch.nn.init.xavier_normal_(tensor, gain=1)
|
(二)自定义初始化:
直接设置参数:
1 2
| net[0].weight.data[:] += 233 net[1].weight.data[0, 0] = 42
|
自定义函数应用至 net 中:例如,使用以下分布为任意权重参数w 来定义初始化函数:
w∼⎩⎨⎧U(5,10)0U(−10,−5)with probability 0.25with probability 0with probability 0.25
1 2 3 4 5 6 7
| def my_init(m): if type(m) == nn.Linear: nn.init.uniform_(m.weight, -10, 10) m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init) net[0].weight[:2]
|
1 2
| tensor([[ 5.2321, -5.7124, -5.1867, -0.0000], [-8.0589, -7.5297, 6.9211, -0.0000]], grad_fn=<SliceBackward>)
|
参数绑定
若希望在多个层之间能够共享参数,可以如下操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| shared = nn.Linear(8, 8) net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), shared, nn.ReLU(), shared, nn.ReLU(), nn.Linear(8, 1)) net(X)
print(net[2].weight.data[0] == net[4].weight.data[0]) net[2].weight.data[0, 0] = 100
print(net[2].weight.data[0] == net[4].weight.data[0])
|
1 2
| tensor([True, True, True, True, True, True, True, True]) tensor([True, True, True, True, True, True, True, True])
|
可以观察到第二层与第三层的参数是绑定(共享)的,注意,它们共享的是同一个对象(张量)
只有当创建的实例对象,并放在不同的地方,才会共享参数
因此,它们不仅值相等,梯度也相等
由此,在反向传播期间,第二个隐藏层和第三个隐藏层的梯度会累加在一起
PyTorch 读写文件
加载和保存张量
对于单个张量,可调用 load 和 save 方法分别读写它们
1 2 3 4
| x = torch.arange(4) torch.save(x, './x-file') x2 = torch.load("./x-file") x2
|
当然,可存储一个张量列表,还可将其读回内存
1 2 3 4
| y = torch.zeros(4) torch.save([x, y], './x-file') x2, y2 = torch.load('./x-file') (x2, y2)
|
此外,还可以写入或读入 从字符串映射到张量 的字典
1 2 3
| mydict = {'x': x, 'y': y} torch.save(mydict, 'mydict') mydict2 = torch.load('mydict')
|
加载和保存模型参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class MLP(nn.Module): def __init__(self): super().__init__() self.hidden = nn.Linear(20, 256) self.output = nn.Linear(256, 10) def forward(self, x): return self.output(F.relu(self.hidden(x))) net = MLP() X = torch.randn(size = (2, 20)) Y = net(X)
torch.save(net.state_dict(), 'mlp.params')
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
|
1 2 3 4
| MLP( (hidden): Linear(in_features=20, out_features=256, bias=True) (output): Linear(in_features=256, out_features=10, bias=True) )
|