Skip to content

深度学习

动手学深度学习

  • 线性回归恰好是一个在整个域中只有一个最小值的学习问题。 但是对像深度神经网络这样复杂的模型来说,损失平面上通常包含多个最小值。 深度学习实践者很少会去花费大力气寻找这样一组参数,使得在训练集上的损失达到最小。

多层感知机

  • 单层的神经网络只能处理线性关系(线性回归模型),通过添加隐藏层可以处理更加普遍的函数关系类型
    • 这种架构就成为多层感知机 MLP
    • image.png|400
  • 为了让多层结构真正提升模型的表示能力(而不是只能表示线性的仿射关系),对每个隐藏单元应用非线性的激活函数(如ReLU、Sigmoid),从而阻止多层感知机退化成线性模型 \(\mathbf{H}=\sigma(\mathbf{X}\mathbf{W}^{(1)}+\mathbf{b}^{(1)})\)
    • ReLU: 修正线性单元 \(\mathrm{ReLU}(x)=\max(x,0)\)。它求导表现得特别好:要么让参数消失,要么让参数通过。这使得优化表现得更好,并且ReLU减轻了困扰以往神经网络的梯度消失问题。
    • sigmoid:机爱你个 R 上的输入映射到 \((0,1)\)\(\mathrm{sigmoid}(x)=\frac1{1+\exp(-x)}.\)在隐藏层中已经较少使用, 它在大部分时候被更简单、更容易训练的ReLU所取代。
    • tanh:将输入压缩到 \((-1,1)\)\(\tanh(x)=\frac{1-\exp(-2x)}{1+\exp(-2x)}\)
  • 过使用更深的网络,可以更容易地逼近许多函数

实现

# 创建多层网络
net = nn.Sequential(nn.Flatten(),
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10))

def init_weights(m):
    if type(m) == nn.Linear:
        nn.init.normal_(m.weight, std=0.01)

net.apply(init_weights);

batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

暂退法

  • 希望模型深度挖掘特征,将权重分散到许多特征中,而不是过于依赖少数潜在的虚假关联
  • 线性模型没有考虑到特征之间的交互作用,对于每个特征,线性模型必须指定正的或负的权重,而忽略其他特征。
  • 泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡
  • 线性模型
    • 很高的偏差:它们只能表示一小类函数
    • 方差很低:它们在不同的随机数据样本上可以得出相似的结果。
  • 深度神经网络:不局限于单独查看每个特征,而是学习特征之间的交互

  • 在标准暂退法正则化中,通过按保留(未丢弃)的节点的分数进行规范化来消除每一层的偏差

    • 每个中间活性值 \(h\) 以暂退概率 \(p\) 由随机变量 \(h`\) 替代
    • \(h^{\prime}=\begin{cases}0&\text{概率为 }p\\\frac h{1-p}&\text{其他情况}&\end{cases}\)
      def dropout_layer(X, dropout):
          assert 0 <= dropout <= 1
          # 在本情况中,所有元素都被丢弃
          if dropout == 1:
              return torch.zeros_like(X)
          # 在本情况中,所有元素都被保留
          if dropout == 0:
              return X
          mask = (torch.rand(X.shape) > dropout).float()
          return mask * X / (1.0 - dropout)
      
  • 只需在每个全连接层之后添加一个 Dropout 层,将暂退概率作为唯一的参数传递给它的构造函数。在训练时,Dropout 层将根据指定的暂退概率随机丢弃上一层的输出。在测试时,Dropout 层仅传递数据(测试时通常不进行丢弃)。
    net = nn.Sequential(nn.Flatten(),
            nn.Linear(784, 256),
            nn.ReLU(),
            # 在第一个全连接层之后添加一个dropout层
            nn.Dropout(dropout1),
            nn.Linear(256, 256),
            nn.ReLU(),
            # 在第二个全连接层之后添加一个dropout层
            nn.Dropout(dropout2),
            nn.Linear(256, 10))
    

计算图与传播

前向传播
  • 从输入层到输出层按顺序来计算和存储神经网络中每层的结果
反向传播
  • 计算神经网络采纳数梯度的方法,根据微积分中的链式规则,按早相反的顺序从输出层到输入层遍历网络,存储了计算某些参数梯度时所需的任何中间变量

数值稳定性和模型初始化

  • 神经网络的结构如下:每一层都是上一层输出和本层参数为变量的函数 \(\mathbb{H}^{(l)}=f(\mathbb{H}^{(l-1)};\mathbb{W}^{(l)})\)
    • 那么网络的输出就可以表示为 \(\mathbb{H}^{(L)}=f(f(\cdots f(\mathbb{X};\mathbb{W}^{(1)})\cdots;\mathbb{W}^{(L-1)});\mathbb{W}^{(L)})\)
    • \(l\) 参数的更新就依赖梯度 \(\frac{\partial\mathcal{L}}{\partial\mathbb{W}^{(l)}}=\frac{\partial\mathcal{L}}{\partial\mathbb{H}^{(L)}}\cdot\prod_{i=l}^{L-1}\frac{\partial\mathbb{H}^{(i+1)}}{\partial\mathbb{H}^{(i)}}\)
  • 梯度是一个累积乘积,每一层的梯度都依赖于前面所有层的梯度,通过反向传播算法计算梯度时,梯度会随着层数增加而出现指数性衰减或增长
    • 如果每一层的梯度的值非常小或非常大,那么它们的乘积会导致梯度整体上指数性地减小或增大。
  • 梯度消失
    • 当每一层的梯度都小于 1,就可能导致梯度消失
    • 当sigmoid函数的输入很大或是很小时,它的梯度都会消失,因此更稳定的ReLU系列函数已经成为从业者的默认选择
    • 靠近输入层的参数更新非常缓慢:因为梯度接近 0,导致这些层的权重几乎不会变化,模型无法有效学习深层特征。
    • 训练时间变长或停止:模型可能无法收敛。
  • 梯度爆炸
    • 如果权重初始化得过大,或者激活函数(如 ReLU)在大范围值上表现不稳定,那么每一层的梯度值会变大。
    • 如果网络的权重在初始化时值很大,正向传播中的激活值会不断增大,反向传播的梯度也会随之增大,导致梯度爆炸。
    • 参数更新过大:导致模型的损失函数值变得不稳定,甚至出现 NaN。
    • 优化难以收敛:爆炸的梯度会破坏优化算法的稳定性。
参数初始化
  • 假设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元,如果我们在隐藏层的所有单元的参数初始化时,给定相同的值,这就会导致一种对称性
    • 由于隐藏单元的输出完全相同,网络的行为就好像隐藏层只有一个单元,失去了隐藏单元的多样性。
    • 可以通过随机初始化和暂退法正则化来解决
  • 更好的初始化方案:Xavier 初始化
    • 通过控制权重的方差,确保前向传播和反向传播中方差的稳定性,从而减轻梯度消失或梯度爆炸问题。

环境和分布偏移

  • 协变量偏移:输入分布 \(P(X)\) 发生变化,但分布条件 \(P(Y|X)\) 和标签分布 \(P(Y)\) 不变
  • 为训练集中的每个样本赋予权重,来调整训练分布使其更加接近测试分布 \(w(X)=\frac{P_{\mathrm{target}}(X)}{P_{\mathrm{source}}(X)}\)

    • 这个系数可以通过混合输入输出用对数几率回归进行计算
  • 标签偏移:标签分布发生变化,但分布条件和输入分布不变

    • 在训练数据上构造混淆矩阵(行表示预测结果类型,列表示实际标签,用于表示模型预测结果)
    • 在测试数据集上做预测
    • 结合混淆矩阵反推测试数据的真实标签分布
    • 利用估计的真实标签分布结合训练数据集调整训练数据集的权重
  • 概念偏移:条件分布本身发生变化,其他条件不变

  • 使用新数据更新现有的网络权重,而不是从头开始训练。

深度学习计算

层和块

  • 块:可以描述单个层、由多个层组成的组件或整个模型本身
    • image.png|500
  • 自定义块 (下面实现一个顺序块)
    class MySequential(nn.Module):
        def __init__(self, *args):
            super().__init__()
            for idx, module in enumerate(args):
                # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
                # 变量_modules中。_module的类型是OrderedDict
                self._modules[str(idx)] = module
    
        def forward(self, X):
            # OrderedDict保证了按照成员添加的顺序遍历它们
            for block in self._modules.values():
                X = block(X)
            return X
    
    # 使用创建的网络
    net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
    net(X)
    
  • 自定义层
    # 无参数的层
    class CenteredLayer(nn.Block):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
    
        def forward(self, X):
            return X - X.mean()
    # 有参数的层
    class MyDense(nn.Block):
        def __init__(self, units, in_units, **kwargs):
            super().__init__(**kwargs)
            self.weight = self.params.get('weight', shape=(in_units, units))
            self.bias = self.params.get('bias', shape=(units,))
    
        def forward(self, x):
            linear = np.dot(x, self.weight.data(ctx=x.ctx)) + self.bias.data(
                ctx=x.ctx)
            return npx.relu(linear)
    

参数管理

  • 获取模型一层的参数 net[2].state_dict()
    • OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894, 0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', tensor([0.0887]))])
  • 一次性访问所有采纳数
    print(*[(name, param.shape) for name, param in net[0].named_parameters()])
    print(*[(name, param.shape) for name, param in net.named_parameters()])
    
  • 通过下标访问 net.state_dict()['2.bias'].data
  • 对具有复杂层级的块也可以嵌套访问 rgnet[0][1][0].bias.data
参数初始化
  • 正态分布初始化 nn.init.normal_(m.weight, mean=0, std=0.01)
  • 初始化为 0 nn.init.normal_(m.weight, mean=0, std=0.01)
  • 自定义初始化
    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() >= 5
    
    net.apply(my_init)
    net[0].weight[:2]
    
延后初始化
  • 使得在没有指定输入维度等信息时就可以先建立网络
    • 直到数据第一次通过模型传递时,框架才会动态地推断出每个层的大小
  • 延后初始化使框架能够自动推断参数形状,使修改模型架构变得容易

读写文件

  • 实现将模型保存到文件中
  • 加载和保存张量
    x = torch.arange(4)
    torch.save(x, 'x-file')
    x2 = torch.load('x-file')
    # 存储一个列表(同理也可以存储字典等)
    torch.save([x, y],'x-files')
    x2, y2 = torch.load('x-files')
    
  • 加载和保存模型参数
    torch.save(net.state_dict(), 'mlp.params')
    clone.load_state_dict(torch.load('mlp.params'))
    

GPU

  • 获取 GPU 的数目 torch.cuda.device_count()
  • 查看张量所在的位置x.device
  • 将张量存储在 GPU 上 X = torch.ones(2, 3, device=try_gpu())
    • 指定存储的 GPU torch.rand(2, 3, device=try_gpu(1))
  • 进行张量运算之前,要决定在哪里执行这个操作(将其他数据移动过去, 否则找不到数据会发生异常)
    • 将数据复制到 GPU 1 上 Z = X.cuda(1)
    • 之后就可以进行计算
  • 指定神经网络模型运行到 GPU 上 net = net.to(device=try_gpu())

卷积神经网络 CNN

  • 强大的、为处理图像数据而设计的神经网络
  • 图片中有太多的像素点,传统的机器学习算法无法处理如此海量的参数
  • 卷积神经网络正是将空间不变性的这一概念系统化,从而基于这个模型使用较少的参数来学习有用的表示。
  • 平移不变性:不管检测对象出现在图像中的哪个位置,神经网络的前面几层应该对相同的图像区域具有相似的反应,即为“平移不变性”。(即如果输入图像中的某个对象从左侧移动到了右侧,模型应该能够识别到这是同一个对象,而不因为其位置的改变对输出产生完全不同的影响)
  • 局部性:神经网络的前面几层应该只探索输入图像中的局部区域,而不过度在意图像中相隔较远区域的关系,这就是“局部性”原则。最终可以聚合这些局部特征,以在整个图像级别进行预测。
    • 这个原则与现实中图像的性质相符:一个像素的特征通常与其周围的像素紧密相关,而与距离较远的像素关系较弱。
  • 输入的图像表示为矩阵 \(X\),形状为 \((H, W)\)

    • 隐藏层中的表示也都为相同形状的二维矩阵
    • 因此对于全连接层每层的参数可以使用一个四阶张量来表示
    • \(W(h,w,h,w)\) 表示连接输入图像中位置 \((h,w)\) 的像素与隐藏表示中位置 \((h,w)\) 的像素权重 \(\mathbf{H}(h^{\prime},w^{\prime})=\sum_{h=1}^H\sum_{w=1}^W\mathbf{X}(h,w)\cdot\mathbf{W}(h^{\prime},w^{\prime},h,w)+\mathbf{b}(h^{\prime},w^{\prime}),\)
    • 传统的多层感知机全连接具有太多的参数,而且并不具有平移不变形,难以处理平移
  • 卷积操作通过一个小的权重窗口(卷积核)对图像的局部区域进行加权求和。

  • 每一个输出像素值只依赖于输入图像的局部像素区域,而不是整个图像 \(\mathbf{H}(i,j)=\sum_{m,n}\mathbf{X}(i+m,j+n)\cdot\mathbf{K}(m,n)\) 其中 \(K\) 表示卷积核的权重
    • 卷积操作中的权重在整个输入图像中共享。无论输入图像的某个特征出现在左上角还是右下角,卷积核都能对该特征进行相同的加权计算
    • 当输入图像中的对象发生平移时,卷积操作产生的隐藏表示也会随之平移,而不会改变表示的内容。
    • 有点:参数数量少,平移不变形,捕获局部特征