Pytorch深度学习【十】

数值稳定性

  • 神经网络的梯度
    • 考虑有d层神经网络(t代表层数)
      h t = f t ( h t − 1 ) \bm{h}^t = f_t(\bm{h^{t-1}}) ht=ft(ht1)
      y = ℓ ∘ f d ∘ . . . ∘ f 1 ( x ) y = \ell\circ f_d \circ ... \circ f_1(x) y=fd...f1(x)

    • 计算损失 ℓ \ell 关于参数 W t \bm{W_t} Wt的梯度
      ∂ ℓ ∂ W t = ∂ ℓ ∂ h d ∂ h d ∂ h d − 1 . . . ∂ h t + 1 ∂ h t ∂ h t ∂ W t \frac{\partial\ell}{\partial\bm{W}^t}=\frac{\partial\ell}{\partial\bm{h}^d}\frac{\partial\bm{h}^d}{\partial\bm{h}^{d-1}}...\frac{\partial\bm{h}^{t+1}}{\partial\bm{h}^{t}}\frac{\partial\bm{h}^{t}}{\partial\bm{W}^{t}} Wt=hdhd1hd...htht+1Wtht

    • 梯度更新的逻辑就是每一层梯度累乘后作为梯度再进行更新操作,然而上述推导过程其实是d-t次矩阵乘法,这就会极大概率的产生两个问题。

  • 两个常见问题
    • 梯度爆炸—梯度大于1—指数增长
    • 梯度消失—梯度小于1—衰减
  • 例子:MLP
    • 加入如下MLP— σ \sigma σ是激活函数
      f t ( h t − 1 ) = σ ( W t h t − 1 ) f_t(\bm{h}^{t-1})=\sigma(\bm{W}^t\bm{h}^{t-1}) ft(ht1)=σ(Wtht1)
      ∂ h t ∂ h t − 1 = d i a g ( σ ′ ( W t h t − 1 ) ) ( W t ) \frac{\partial\bm{h}^t}{\partial\bm{h}^{t-1}}=diag(\sigma^{'}(\bm{W}^t\bm{h}^{t-1}))(\bm{W}^t) ht1ht=diag(σ(Wtht1))(Wt)
      ∏ i = t d − 1 ∂ h i + 1 ∂ h i = ∏ i = t d − 1 d i a g ( σ ′ ( W i h i − 1 ) ) ( W i ) \prod_{i=t}^{d-1} \frac{\partial\bm{h}^{i+1}}{\partial\bm{h}^{i}}=\prod_{i=t}^{d-1}diag(\sigma^{'}(\bm{W}^i\bm{h}^{i-1}))(\bm{W}^i) i=td1hihi+1=i=td1diag(σ(Wihi1))(Wi)
  • 梯度爆炸的问题
    • 值超出值域(infinity)
      • 对于16位浮点数尤为严重
    • 对学习率敏感—因为我们的梯度下降其实是学习率和梯度相乘后进行修正
      • 如果lr过大—大参数值—更大的梯度
      • 如果lr过小—训练无法进行
      • 我们可能需要不断的调整学习率
  • 梯度消失的问题
    • 梯度值变成0
      • 对16位浮点数尤为严重
    • 训练没有进展
      • 不管如何选择学习率
    • 对于底部层尤为严重
      • 仅仅顶部层训练的好
      • 无法让神经网络更深
        • 因为更深网络,当底部拿到梯度时,由于反向传播的原因,梯度已经很小无法进行有效更新
  • 总结
    • 当数值太大太小都会导致数值问题
    • 常发生在深度模型中,因为其会对n个数累乘

初始化和激活函数的选择

  • 如何让训练更加稳定
    • 乘法变加法
    • 归一化
    • 合理的权重初始和激活函数— w w w σ \sigma σ对梯度有很大影响
  • 权重初始化
    • 在合理值区间里随机初始化参数
    • 训练开始时候数值很不稳定
      • 远离最优解损失表面会比较复杂
      • 最优解附近表面会比较平
以下将进行两种论证,我们都采用正向推理进行推导反向同理
  • 将每层的输出和梯度都看做随机变量
  • 让它们都均值方差都保持一致—核心目的
  • 即为我们的目的是
    • 正向 E [ h i t ] = 0 \mathbb{E}[h_i^t]=0 E[hit]=0 & V a r [ h i t ] = a Var[h_i^t]=a Var[hit]=a
    • 反向 E [ α ℓ α h i t ] = 0 \mathbb{E}[\frac{\alpha\ell}{\alpha h_i^t}]=0 E[αhitα]=0 & V a r [ α ℓ α h i t ] = b Var[\frac{\alpha\ell}{\alpha h_i^t}]=b Var[αhitα]=b
    • a 和 b都是常数
  • 做以下论证来证明选择何种初始化方法更好
    • w i , j t w_{i,j}^t wi,jt是独立分布,因此 E [ w i , j t ] = 0 \mathbb{E}[w_{i,j}^t]=0 E[wi,jt]=0, V a r [ w i , j t ] = γ t Var[w_{i,j}^t]=\gamma_t Var[wi,jt]=γt
    • 假设没有激活函数 h t = W t h t − 1 \bm{h}^t=\bm{W}^t\bm{h}^{t-1} ht=Wtht1,这里 W t ∈ R n t ∗ n t − 1 \bm{W}^t\in\mathbb{R}^{n_t*n_{t-1}} WtRntnt1
      E [ h i t ] = E [ ∑ j w i , j t h j t − 1 ] = ∑ j E [ w i , j t ] E [ h j t − 1 ] = 0 \mathbb{E}[h_i^t]=\mathbb{E}[\sum_jw_{i,j}^th_j^{t-1}]=\sum_j\mathbb{E}[w_{i,j}^t]\mathbb{E}[h_j^{t-1}]=0 E[hit]=E[jwi,jthjt1]=jE[wi,jt]E[hjt1]=0
      V a r [ h i t ] = E [ ( h i t ) 2 ] − E [ ( h i t ) ] 2 = E [ ( ∑ j w i , j t h j t − 1 ) 2 ] = n t − 1 γ t V a r [ h j t − 1 ] Var[h_i^t]=\mathbb{E}[(h_i^t)^2]- \mathbb{E}[(h_i^t)]^2=\mathbb{E}[(\sum_jw_{i,j}^th_j^{t-1})^2]=n_{t-1}\gamma_tVar[h_j^{t-1}] Var[hit]=E[(hit)2]E[(hit)]2=E[(jwi,jthjt1)2]=nt1γtVar[hjt1]
    • 上面的推导不难看出为了保证每层的均值和方差具有一致性我们要满足 n t − 1 γ t = 1 n_{t-1}\gamma_t=1 nt1γt=1 & n t γ t = 1 n_{t}\gamma_t=1 ntγt=1
    • 因此我们采用Xavier进行初始化
    • 当前层输入输出大小分别为 n t − 1 n_{t-1} nt1 n t n_{t} nt
      • 正态分布 ( 0 , 2 / ( n t − 1 + n t ) ) (0, \sqrt{2/(n_{t-1}+n_t)}) (0,2/(nt1+nt) )
      • 均匀分布 ( − 6 / ( n t − 1 + n t ) , 6 / ( n t − 1 + n t ) ) (-\sqrt{6/(n_{t-1}+n_t)}, \sqrt{6/(n_{t-1}+n_t)}) (6/(nt1+nt) ,6/(nt1+nt) )
  • 做以下论证来证明选择何种激活函数可以更好
    • 假设 σ ( x ) = α x + β \sigma(x) = \alpha x + \beta σ(x)=αx+β
    • 当前层的输出表达式为 h ′ = W t h t − 1 a n d h ′ = σ ( h ′ ) \bm{h}^{'}=\bm{W}^t\bm{h}^{t-1}\quad and\quad\bm{h}^{'}=\sigma({\bm{h}^{'}}) h=Wtht1andh=σ(h)
    • 进行对期望和方差的如下推导
      E [ h i t ] = E [ α h i ′ + β ] = β \mathbb{E}[h_i^t]=\mathbb{E}[\mathbb{\alpha h_i^{'}}+\beta]=\beta E[hit]=E[αhi+β]=β
      V a r [ h i t ] = E [ ( h i t ) 2 ] − E [ ( h i t ) ] 2 = E [ ( α h i ′ + β ) 2 ] − β 2 = E [ α 2 ( h i ′ ) + 2 α β h i ′ + β 2 ] − β 2 = α V a r [ h i ′ ] \begin{align*} Var[h_i^t] &= \mathbb{E}[(h_i^t)^2]- \mathbb{E}[(h_i^t)]^2\\ &=\mathbb{E}[(\alpha h_i^{'}+\beta)^2]-\beta^2 \\ &=\mathbb{E}[\alpha^2(h_i^{'})+2\alpha\beta h_i^{'}+\beta^2]-\beta^2\\ &=\alpha Var[h_i^{'}] \end{align*} Var[hit]=E[(hit)2]E[(hit)]2=E[(αhi+β)2]β2=E[α2(hi)+2αβhi+β2]β2=αVar[hi]
    • 针对上面的推导,我们不难理解,为了使得激活函数不改变我们输入输出方差的话显然 β = 0 \beta=0 β=0 & α = 1 \alpha = 1 α=1
    • 因此我们希望我们的激活函数是一次函数形最大特征是过原点
    • 通过对常见三个激活函数进行泰勒展开
      • sigmoid(x)不过原点而tanh(x)和relu(x)过原点
      • 显然relu(x)最受欢迎因为它展开后几乎为一次函数最大限度满足了数据稳定性
  • 总结
    • 合理的权重初始值和激活函数的选取可以提升数值稳定性即目的是每一层的输出和每一层的梯度是一定稳定范围内的随机变量

模型构造的基本知识

  • 层和块
# 顺序块---自定义重写Sequential的实现---我们可以更多的自定义模型中的操作
#!pip install torchsummary
from torchsummary import summary
class MySequential(nn.Module):
  def __init__(self, *args):
    super().__init__()
    for block in args:
      self._modules[block] = block
  def forward(self, X):
    for block in self._modules.values():
      X = block(X)
    return X
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
summary(net, input_size=(1, 20))
# 在正向传播函数中执行代码
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):
    print(f"input shape: {X.shape}")  # 打印输入的形状
    X = self.linear(X)
    X = F.relu(torch.mm(X, self.rand_weight) + 1)
    X = self.linear(X)
    while X.abs().sum() > 1:
      X /= 2
    print(f"output shape: {X.shape}")  # 打印输出的形状
    return X.sum()
net = FixedHiddenMLP()
net(X)
# 混合搭配各种组合块的方法
class NewMySequential(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))
net = nn.Sequential(NewMySequential(), nn.Linear(16, 20), FixedHiddenMLP())
net(X)
print(net)
  • 参数管理
print(net[2].state_dict()) # 可以直接查看当前层的所有参数
print(net[2].bias) # 查看最后一层的偏移
print(net[2].bias.data) # 直接访问
print(net[2].weight.grad) # 访问当前梯度
print(*[(name, param.shape) for name, param in net(0).named_parameters()]) 
# 访问net(0)中的所有参数 * 表示打印出列表的每个数据
print(*[(name, param.shape) for name, param in net.named_parameters()]) 
# 访问net中的所有参数 * 表示打印出列表的每个数据
  • 内置初始化
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]
  • 对某些块应用不同的初始化方法
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)
# 对于一个module来说不管是整体或者是单个层其实都是可以使用初始化方法的
net[0].apply(xavier) # 第一个层用xavier进行初始化 
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
# 当然也可以自定义初始化
  • 参数固定—某些层,我们希望参数形成对比因此共用某些层的参数
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)
# 这样无论如何训练shared永远不会被更新参数
  • 自定义层
# 自定义一个无参数的层
import torch
import torch.nn.functional as F
from torch import nn
class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer()) # 可以将层直接加入
# 自定义一个带参数的层
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)
linear = MyLinear(5, 3)
linear.weight
# 使用自定义层直接执行正向传播计算
linear(torch.rand(2, 5))
# 使用自定义层构建模型
net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))
  • 带有参数的层重点关注设计self.weight和self.bias
  • 读写文件
# 加载和保存张量
import torch
from torch import nn
from torch.nn import functional as F
x = torch.arange(4)
torch.save(x, 'x-file')
x2 = torch.load('x-file')
x2
# 存储一个张量列表,然后读回内存
y = torch.zeros(4)
torch.save([x, y], 'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
# 写入或读取从字符串映射到张量的字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
# 加载保存模型参数
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)
# 将模型的参数存储为一个叫做“mlp.params”的文件
torch.save(net.state_dict(), 'mlp.params')
# 实例化了原始多层感知机模型的一个备份。 直接读取文件中存储的参数
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval()
Y_clone = clone(X)
Y_clone == Y
# 验证迁移后模型是否正常
  • pytorch需要注意一点,那就是不可以直接保存模型,只能保存权重,这就需要我们在移植过程中进行模型的再次定义

你可能感兴趣的:(深度学习,pytorch)