开个新坑,虽然之前大概扫过一遍但是几乎是没学懂的,这次再回锅肉一下,主要是写得好理解一些方便自己记忆理解

3 线性神经网络

线性回归

  1. 要素

    • 要求什么? 线性模型:权重w+偏置b -> y=wx+by =w^{\top}x+b
    • 怎么监督? 损失函数:量化目标的实际值与预测值之间的差距
      • 损失函数为什么通常使用均方误差:根据极大似然估计法,参数w和b的最优值是使整个数据集的似然最大的值。在高斯噪声的假设下,最小化均方误差等价于对线性模型的极大似然估计
    • 一步到位?解析解:w=(XX)1Xyw^* =(X^\top X)^{-1}X^\top y 只有简单模型才有
    • 求解过程? 随机梯度下降:小批量的平均损失关于模型参数的导数(梯度)将梯度乘以一个预先确定的正数η,并从当前参数的值中减掉
      • 初始化参数
      • 重复以下训练,直到完成
        • 计算梯度g←(w,b)1BiBl(x(i),y(i),w,b)∂_{(w,b)} \frac{1}{|B|} \sum_{i∈B}l(x^{(i)},y^{(i)},w,b)
        • 更新参数(w,b)(w,b)ηg(w,b)←(w,b)−ηg
    • 学习后做什么?预测
  2. 简洁实现

    • 生成数据集 features, labels = d2l.synthetic_data(true_w, true_b, 1000) features是X,labels是已经加过噪声的y

    • 读取数据集

      1
      2
      3
      4
      5
      6
      def load_array(data_arrays, batch_size, is_train=True): #@save
      """构造一个PyTorch数据迭代器"""
      dataset = data.TensorDataset(*data_arrays)#按样本→对应的数据集对象一条条
      return data.DataLoader(dataset, batch_size, shuffle=is_train) #从dataset打乱拿batch_size个
      batch_size = 10
      data_iter = load_array((features, labels), batch_size)
    • 定义模型

      • 在PyTorch中,全连接层在Linear类中定义。值得注意的是,我们将两个参数传递到nn.Linear中。第一个指定输入特征形状,即2,第二个指定输出特征形状,输出特征形状为单个标量,因此为1。

      • 我们首先定义一个模型变量net,它是一个Sequential类的实例。Sequential类将多 个层串联在一起。当给定输入数据时,Sequential实例将数据传入到第一层,然后将第一层的输出作为第二层的输入,以此类推。在下面的例子中,我们的模型只包含一个层,因此实际上不需要Sequential。但是由 于以后几乎所有的模型都是多层的,在这里使用Sequential会让你熟悉“标准的流水线”。

        1
        2
        fron torch import nn
        net = nn.Sequential(nn.Linear(2, 1))
    • 初始化模型参数:通过net[0]选择网络中的第一个图层,然后使用weight.data和bias.data方法访问参数(不管几维对同类所有)。我们还可以使用 替换方法normal_和fill_来重写参数值

      1
      2
      3
      """net[0]对应sequential的层数结构选择"""
      net[0].weight.data.normal_(0, 0.01) #normal是正态随机
      net[0].bias.data.fill_(0) #fill是填固定的
    • 定义损失函数:计算均方误差使用的是MSELoss类,也称为平方L2范数。默认情况下,它返回所有样本损失的平均值。 loss = nn.MSELoss()

    • 定义优化算法: 小批量随机梯度下降算法是一种优化神经网络的标准工具,PyTorch在optim模块中实现了该算法的许多变种。当我们实例化一个SGD实例时,我们要指定优化的参数*(可通过net.parameters()从我们的模型中获得)* 以及优化算法所需的超参数字典。小批量随机梯度下降只需要设置lr值,这里设置为0.03。trainer = torch.optim.SGD(net.parameters(), lr=0.03)

    • 训练:在每个迭代周期里,我们将完整遍历一次数据集(train_data),不停地从中获取一个小批量的输 入和相应的标签。对于每一个小批量,我们会进行以下步骤: ①通过调用net(X)生成预测并计算损失l(前向传播)②通过进行反向传播来计算梯度 ③通过调用优化器来更新模型参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      num_epochs = 3
      for epoch in range(num_epochs): # 整个数据集训练3遍
      for X, y in data_iter: # 每次取一个batch
      l = loss(net(X), y) # ① 预测,并计算预测误差
      trainer.zero_grad() # ② 清除上一次的梯度
      l.backward() # ③ 根据loss计算w、b的梯度
      trainer.step() # ④ 根据梯度更新w、b
      l = loss(net(features), labels) # ⑤ 用全部数据检查当前模型的loss
      print(f'epoch {epoch + 1}, loss {l:f}')

Softmax回归

  1. 基本原理: Softmax 回归是一种用于多分类问题的单层神经网络。对于同一个输入样本,输出层中的每个神经元分别通过一次线性计算得到一个类别得分(logit),每个得分对应一个类别。随后,将所有类别得分一起输入 Softmax 函数,将其转换为总和为 1 的概率分布,最终选择概率最大的类别作为预测结果。

    ../_images/softmaxreg.svg

    • Softmax 函数: y^=softmax(o) 其中 y^j=exp(oj)kexp(ok)\hat{\mathbf{y}}=\operatorname{softmax}(\mathbf{o}) \quad \text { 其中 } \quad \hat{y}_{j}=\frac{\exp \left(o_{j}\right)}{\sum_{k} \exp \left(o_{k}\right)} softmax运算不会改变未规范化的预测之间的大小次序,只会确定分配给每个类别的概率。

    • 损失函数: 交叉熵损失

      • 最大似然:模型给真实分配类别的概率尽可能大

      • 对整个数据集,假设各样本相互独立,则希望所有样本真实类别对应概率的乘积最大。为了将概率的连乘转化为更方便计算的求和,对似然取对数。机器学习通常将优化问题写成“最小化损失”,因此在对数似然前加负号L=jyjlogy^jL=−\sum_j y_jlog\hat y_j

    • 计算梯度&更新参数:

      • 梯度结论: Loj=y^jyj\frac{\partial L}{\partial o_j} = \hat{y}_j - y_j

      • 链式法则:Loss 并不直接依赖某个 ww,而是 ww 先影响 oooo 再影响 Loss,因此沿着这条路径把各段的导数相乘,就能得到 ww 对 Loss 的最终影响。

        LwA1=LoAoAwA1=(y^AyA)x1\frac{\partial L}{\partial w_{A1}}=\frac{\partial L}{\partial o_A}\frac{\partial o_A}{\partial w_{A1}}=(\hat{y}_A-y_A)x_1

        LbA=LoAoAbA=y^AyA\frac{\partial L}{\partial b_{A}}=\frac{\partial L}{\partial o_A}\frac{\partial o_A}{\partial b_{A}}=\hat{y}_A-y_A

      • 参数更新与之前相同

  2. 代码实现

    • 定义&初始化模型参数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      # 定义 Softmax 回归网络
      net = nn.Sequential(
      nn.Flatten(), # 将每张 1×28×28 的图片展平成 784 维向量
      nn.Linear(784, 10) # 全连接层:784个输入特征 → 10个类别的logits
      )

      # 定义参数初始化函数
      def init_weights(m):
      # 判断当前遍历到的模块是否为全连接层
      if type(m) == nn.Linear:
      # 将该全连接层的所有权重w初始化为
      # 均值为0、标准差为0.01的正态分布随机数
      nn.init.normal_(m.weight, mean=0, std=0.01)

      # 遍历net中的各个模块,并对每个模块调用init_weights
      # Flatten不满足if条件,不处理
      # Linear满足if条件,因此其weight会被重新初始化
      net.apply(init_weights)
    • 损失函数:loss = nn.CrossEntropyLoss(reduction='none')

    • 优化算法(通用):trainer = torch.optim.SGD(net.parameters(), lr=0.1)

    • 训练

      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
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      def train_epoch_ch3(net, train_iter, loss, trainer):
      """使用PyTorch训练模型一个epoch"""

      # 切换到训练模式
      net.train()

      # 三个累加器:loss总和、预测正确数、样本总数
      metric = Accumulator(3)

      # 遍历所有batch
      for X, y in train_iter:
      # 前向传播并计算每个样本的loss
      y_hat = net(X)
      l = loss(y_hat, y)

      # 清梯度 → 反向传播计算梯度 → 更新参数
      trainer.zero_grad()
      l.mean().backward()
      trainer.step()

      # 累计loss总和、预测正确数和样本数
      metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())

      # 返回整个epoch的平均loss和训练准确率
      return metric[0] / metric[2], metric[1] / metric[2]

      def train_ch3(net, train_iter, test_iter, loss, num_epochs, optimizer):
      """训练模型多个epoch"""

      # 创建动态绘图工具:绘制训练loss、训练准确率和测试准确率
      animator = Animator(xlabel='epoch', xlim=[1, num_epochs], ylim=[0.3, 0.9],
      legend=['train loss', 'train acc', 'test acc'])

      # 训练num_epochs轮
      for epoch in range(num_epochs):
      # 训练一个epoch,返回(平均训练loss, 训练准确率)
      train_metrics = train_epoch_ch3(net, train_iter, loss, optimizer)

      # 在测试集上计算当前模型的准确率
      test_acc = evaluate_accuracy(net, test_iter)

      # 添加当前epoch的数据:(train loss, train acc, test acc)
      animator.add(epoch + 1, train_metrics + (test_acc,))

      # 取最后一个epoch的训练loss和训练准确率
      train_loss, train_acc = train_metrics

      # 检查最终训练结果是否达到预期
      assert train_loss < 0.5, train_loss
      assert 0.7 < train_acc <= 1, train_acc
      assert 0.7 < test_acc <= 1, test_acc

      num_epochs = 10
      train_ch3(net, train_iter, test_iter, loss, num_epochs, optimizer)