文章目录
正向传播和反向传播
正向传播
-
正向传播(forward propagation或forward pass)指的是:按顺序(从输入层到输出层)计算和存储 神经网络中每层的结果。
-
计算图
反向传播
-
反向传播指的是计算神经网络参数梯度的方法。该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。
-
在正向传播的计算图中单隐藏层简单网络的参数是 W ( 1 ) W^{(1)} W(1) 和 W ( 2 ) W^{(2)} W(2) 。反向传播的目的是计算梯度 ∂ J / ∂ W ( 1 ) \partial J/\partial W^{(1)} ∂J/∂W(1)和 ∂ J / ∂ W ( 2 ) \partial J/\partial W^{(2)} ∂J/∂W(2)。为此,应用链式法则,依次计算每个中间变量和参数的梯度。计算的顺序与正向传播中执行的顺序相反,因为需要从计算图的结果开始,并朝着参数的方向计算。
训练神经网络
- 在训练神经网络时,正向传播和后向传播相互依赖。正向传播沿着依赖的方向遍历计算图并计算其路径上的所有变量。然后将这些用于反向传播,计算顺序与计算图的相反。
- 反向传播复用正向传播中存储的中间值,以避免重复计算。带来的影响之一是需要保留正向传播计算的中间值,直到反向传播完成。这也是训练比单纯的预测需要更多的内存(显存)的原因之一。此外,这些中间值的大小与网络层的数量和批量的大小大致成正比。因此,使用更大的批量来训练更深层次的网络更容易导致内存(显存)不足(out of memory)错误。
数值稳定性和模型初始化
梯度爆炸和梯度消失
-
不稳定梯度使优化算法面临一些问题。 一是 梯度爆炸(gradient exploding)问题:参数更新过大,破坏了模型的稳定收敛; 二是梯度消失(gradient vanishing)问题:参数更新过小,在每次更新时几乎不会移动,导致无法学习。
-
梯度爆炸问题
-
值超出值域(infinity),对于16为浮点数尤为严重(数值区间6e-5~6e4)
-
对学习率敏感
如果学习率太大->大参数值->更大的梯度
如果学习率太小->训练无进展
可能需要在训练过程不断调整学习率
-
-
梯度消失问题
- 梯度值变为0(对16位浮点数尤为严重)
- 不管如何选择学习率,训练都没有进展
- 对于底部层尤为严重,仅仅顶部层训练的较好,因此无法让神经网络更深。
导致梯度消失问题的一个常见的原因是跟在每层的线性运算之后的激活函数σ。当它的输入很大或是很小时,sigmoid函数的梯度都会消失。此外,当反向传播通过许多层时,除非在刚刚好的地方sigmoid函数的输入接近于零,否则整个乘积的梯度可能会消失。因此,更稳定的ReLU系列函数已经成为默认选择。
参数初始化
解决梯度消失和梯度爆炸的一种方式使进行初始化。
-
默认初始化
使用正态分布来初始化权重值
-
Xavier初始化
-
满足 ( n i n + n o u t ) σ 2 / 2 = 1 (n_{in}+n_{out})\sigma^{2}/2=1 (nin+nout)σ2/2=1或等价于 σ = 2 / ( n i n + n o u t ) \sigma=\sqrt{2/(n_{in}+n_{out})} σ=2/(nin+nout) 。其中 n i n n_{in} nin和 n o u t n_{out} nout 为该层输入和输出的维度。
-
通常,Xavier初始化从均值为零,方差 σ 2 = 2 / ( n i n + n o u t ) \sigma^{2}=\sqrt{2/(n_{in}+n_{out})} σ2=2/(nin+nout) 的高斯分布中采样权重。
-
Xavier初始化表明,对于每一层,输出的方差不受输入维度的影响,任何梯度的方差不受输出维度的影响。
-
层和块
自定义块
-
每个块必须实现的基本功能
- 将输入数据作为其正向传播函数的参数
- 通过正向传播函数来生成输出。输出的形状可能与输入的形状不同。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。
- 存储和访问正向传播计算所需的参数
- 根据需要初始化模型参数。
-
代码实现
class MLP(nn.Module): # 用模型参数声明层。这里声明两个全连接的层 def __init__(self): # 调用`MLP`的父类`Block`的构造函数来执行必要的初始化。 # 这样,在类实例化时也可以指定其他函数参数数 super().__init__() self.hidden = nn.Linear(20, 256) # 隐藏层 self.out = nn.Linear(256, 10) # 输出层 # 定义模型的正向传播,即如何根据输入`X`返回所需的模型输出 def forward(self, X): # 这里使用ReLU的函数版本,其在nn.functional模块中定义。 return self.out(F.relu(self.hidden(X)))
顺序块
-
代码实现
class MySequential(nn.Module): def __init__(self, *args): super().__init__() for block in args: # 这里,`block`是`Module`子类的一个实例。把它保存在'Module'类的成员变量 # `_children` 中。`block`的类型是OrderedDict。 self._modules[block] = block def forward(self, X): # OrderedDict保证了按照成员添加的顺序遍历它们 for block in self._modules.values(): X = block(X) return X
In:
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) net(X)
Out:
tensor([[ 0.0820, -0.0776, -0.0659, -0.0075, -0.1185, -0.0005, 0.1571, -0.1571, -0.0343, -0.3000], [ 0.1122, -0.0706, -0.0532, 0.0521, -0.2315, 0.0262, -0.0751, 0.0285, -0.1170, -0.1475]], grad_fn=<AddmmBackward>)
在正向传播函数中执行代码
-
代码实现
class FixedHiddenMLP(nn.Module): def __init__(self): super().__init__() # 不计算梯度的随机权重参数。因此其在训练期间保持不变。 self.rand_weight = torch.rand((20, 20), requires_grad=False) self.linear = nn.Linear(20, 20) def forward(self, X): X = self.linear(X) # 使用创建的常量参数以及`relu`和`dot`函数。 X = F.relu(torch.mm(X, self.rand_weight) + 1) # 复用全连接层。这相当于两个全连接层共享参数。 X = self.linear(X) # 控制流 while X.abs().sum() > 1: X /= 2 return X.sum()
In:
net = FixedHiddenMLP() net(X)
Out:
tensor(-0.0919, grad_fn=<SumBackward0>)
混合搭配各种组合块
-
代码实现
In:
class NestMLP(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU()) self.linear = nn.Linear(32, 16) def forward(self, X): return self.linear(self.net(X)) chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP()) chimera(X)
Out:
tensor(-0.4179, grad_fn=<SumBackward0>)
-
小结
- 层也是块。
- 一个块可以由许多层组成。
- 一个块可以由许多块组成。
- 块可以包含代码。
- 块负责大量的内部处理,包括参数初始化和反向传播。
- 层和块的顺序连接由
Sequential
块处理。
参数管理
参数访问
In:
import torchfrom torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))X = torch.rand(size=(2, 4))net(X)
Out:
tensor([[-0.3242],
[-0.3097]], grad_fn=<AddmmBackward>)
In:
print(net[2].state_dict())
Out:
OrderedDict([('weight', tensor([[ 0.1175, -0.1409, 0.2194, -0.0502, 0.2908, 0.0575, -0.1581, 0.3334]])), ('bias', tensor([-0.3429]))])
-
目标参数
In:
print(type(net[2].bias)) # nn.Linear(8, 1)层bias参数的类型 print(net[2].bias) print(net[2].bias.data) # nn.Linear(8, 1)层bias参数的值
Out:
<class 'torch.nn.parameter.Parameter'>Parameter containing:tensor([-0.3429], requires_grad=True)tensor([-0.3429])
In:
net[2].weight.grad == None
Out:
True
-
一次性访问所有参数
In:
print(*[(name, param.shape) for name, param in net[0].named_parameters()])print(*[(name, param.shape) for name, param in net.named_parameters()])
Out:
('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))
In:
net.state_dict()['2.bias'].data
Out:
tensor([-0.3429])
-
从嵌套块收集函数
In:
def block1(): return nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 4), nn.ReLU())def block2(): net = nn.Sequential() for i in range(4): # 在这里嵌套 net.add_module(f'block {i}', block1()) return netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))rgnet(X)
Out:
tensor([[0.2171], [0.2172]], grad_fn=<AddmmBackward>)
In:
print(rgnet)
Out:
Sequential( (0): Sequential( (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() ) (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() ) (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() ) (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))
In:
rgnet[0][1][0].bias.data #访问第一个主块的第二个子块的第一层的bias参数的值
Out:
tensor([ 0.1753, 0.0331, -0.2145, -0.3924, 0.3921, -0.4297, 0.3446, 0.1276])
参数初始化
-
内置初始化
-
将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。
In:
def init_normal(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, mean=0, std=0.01) nn.init.zeros_(m.bias)net.apply(init_normal)net[0].weight.data[0], net[0].bias.data[0]
Out:
(tensor([-8.2716e-04, 8.8105e-03, -7.6907e-03, 9.2172e-05]), tensor(0.))
-
将所有参数初始化为给定的常数(比如1)
In:
def init_constant(m): if type(m) == nn.Linear: nn.init.constant_(m.weight, 1) nn.init.zeros_(m.bias)net.apply(init_constant)net[0].weight.data[0], net[0].bias.data[0]
Out:
(tensor([1., 1., 1., 1.]), tensor(0.))
-
可以对某些块应用不同的初始化方法。例如,使用Xavier初始化方法初始化第一层,然后第二层初始化为常量值42。
In:
def xavier(m): if type(m) == nn.Linear: nn.init.xavier_uniform_(m.weight)def init_42(m): if type(m) == nn.Linear: nn.init.constant_(m.weight, 42)net[0].apply(xavier)net[2].apply(init_42)print(net[0].weight.data[0])print(net[2].weight.data)
Out:
tensor([ 0.1015, -0.5771, 0.4569, -0.2264])tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
-
-
自定义初始化
使用以下的分布为任意权重参数ww定义初始化方法:
In:
def my_init(m): if type(m) == nn.Linear: print( "Init", *[(name, param.shape) for name, param in m.named_parameters()][0]) nn.init.uniform_(m.weight, -10, 10) m.weight.data *= m.weight.data.abs() >= 5net.apply(my_init)net[0].weight[:2]
Out:
Init weight torch.Size([8, 4])Init weight torch.Size([1, 8])tensor([[-0.0000, -9.8019, 7.4454, -0.0000], [ 8.8228, 0.0000, 0.0000, 9.0327]], grad_fn=<SliceBackward>)
始终可以直接设置参数:
In:
net[0].weight.data[:] += 1 net[0].weight.data[0, 0] = 42 net[0].weight.data[0]
Out:
tensor([42.0000, -8.8019, 8.4454, 1.0000])
参数绑定
In:
# 给共享层一个名称,以便可以引用它的参数。
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])
Out:
tensor([True,