本周学习《动手学深度学习》第3章线性神经网络。我先从零实现线性回归,再用PyTorch高级API写一遍;随后读取Fashion-MNIST,分别完成softmax回归的从零实现和简洁实现。相比只会调用模型,这次重点是理解训练循环里每一步的作用。

这篇复盘主要记录

  • 线性回归中的模型、平方损失和SGD分别做什么
  • 从零实现与PyTorch简洁实现的对应关系
  • softmax怎样把十个分数变成类别概率
  • Fashion-MNIST训练结果和仍未解决的问题

一、这一章的两类问题

线性回归回答“有多少”,例如根据面积和房龄估计房价;softmax回归回答“是哪一类”,例如判断一张服装图片是T恤、裤子还是鞋。两者输出不同,但训练主线几乎一样:

读取一小批数据 → 模型给出预测 → 损失衡量差距 → 反向传播计算梯度 → 优化器更新参数。

二、先造一份知道答案的数据

书中先用真实参数w=[2,-3.4]、b=4.2生成1000条人工数据,并加入少量随机噪声。这样训练结束后,可以直接检查模型有没有把原参数找回来。

true_w = torch.tensor([2.0, -3.4])
true_b = 4.2
features = torch.normal(0, 1, (1000, 2))
labels = features @ true_w + true_b
labels += torch.normal(0, 0.01, labels.shape)
labels = labels.reshape((-1, 1))

可以把两个特征想成房屋面积和房龄,权重表示这两个因素各自会怎样影响价格,偏置则是模型的基础起点。

三、为什么要小批量读取

训练时不把60000张图片一次塞给模型,也不必每看一张就更新一次。小批量是在计算效率和更新频率之间取一个折中。生活中有点像批作业:一次拿一小摞,批完就根据这一摞发现的问题调整判断,再继续下一摞。

for start in range(0, len(features), batch_size):
    batch_indices = torch.tensor(indices[start:start + batch_size])
    yield features[batch_indices], labels[batch_indices]

四、线性回归从零实现

从零版只使用张量与自动求导。模型负责计算Xw+b,平方损失负责衡量预测与真实值的距离,SGD沿着梯度反方向把参数挪动一小步。

for X, y in data_iter(batch_size, features, labels):
    loss = squared_loss(linreg(X, w, b), y)
    loss.sum().backward()
    sgd([w, b], learning_rate, batch_size)

3轮以后,平均损失降到约0.00005,学到的参数为:

真实权重: [ 2.0000, -3.4000]
学到权重: [ 1.9997, -3.3995]
真实偏置: 4.2
学到偏置: 4.1995

参数没有一模一样,是因为数据里故意加入了噪声。不过误差已经很小,说明训练循环确实在工作。

五、简洁实现并没有换掉原理

从零实现PyTorch简洁实现作用
data_iterDataLoader按小批量读取数据
X @ w + bnn.Linear建立线性模型
squared_lossnn.MSELoss计算预测误差
sgdtorch.optim.SGD根据梯度更新参数

高级API减少了重复代码,但“预测、损失、反向传播、更新”四个步骤没有改变。从零实现的价值,就是知道这些封装里面大致在做什么。

六、Fashion-MNIST是什么

Fashion-MNIST包含60000张训练图片和10000张测试图片,共10种服装类别。每张图片是28×28的灰度图,所以一批256张图片的形状为:

torch.Size([256, 1, 28, 28])

256:一批图片数量
1:灰度通道数
28、28:图片高度和宽度

softmax线性模型会先把每张图片展平成784个像素,再输出10个类别分数。

七、softmax到底做了什么

线性层输出的10个数字可能为负,也不保证总和为1,不能直接当概率。softmax先取指数让结果都为正,再除以同一行的总和:

softmax(X) = exp(X) / sum(exp(X))

可以把它理解成把10个候选类别的“原始支持分”换算成总和为1的支持率。概率最大的类别就是最终预测。

八、交叉熵为什么关注正确类别

如果真实标签是“短靴”,交叉熵主要检查模型给“短靴”的概率。正确类别概率越接近1,损失越小;如果模型很自信地给错,损失会明显增大。

def cross_entropy(probabilities, labels):
    true_probabilities = probabilities[range(len(probabilities)), labels]
    return -torch.log(true_probabilities)

九、两种softmax实现的真实结果

softmax回归从零实现和PyTorch简洁实现的损失与测试准确率曲线
图1:两种实现的损失下降趋势基本一致,测试准确率最终都在83%左右。
实现第10轮损失训练准确率测试准确率
从零实现0.447484.7%83.4%
PyTorch简洁实现0.447284.8%82.9%

两次训练的初始参数和数据打乱顺序不完全相同,因此准确率不会逐位一致。它们的损失与准确率走势接近,说明从零版的主要计算逻辑是正确的。

十、与PDF代码不同的地方

十一、目前还不熟的地方

  1. 数值稳定性。从零版直接计算指数,输入过大时可能上溢,书中也提醒这只是教学实现。
  2. 交叉熵推导。会写代码,也知道正确类别概率越低损失越大,但从最大似然到交叉熵的公式还需要再推一遍。
  3. 错误分析。目前只看整体准确率,还没有用混淆矩阵检查衬衫、T恤和套衫等相似类别。

十二、下周计划

  1. 01
    多层感知机

    理解隐藏层、ReLU和非线性表达能力。

  2. 02
    两种实现

    分别完成多层感知机的从零实现和PyTorch简洁实现。

  3. 03
    模型选择

    学习训练误差、泛化误差、过拟合与欠拟合。

  4. 04
    正则化

    开始权重衰减和dropout实验。

十三、本周最值得保留的认识

模型损失梯度更新评估

模型先给出预测,损失把“错多少”变成一个数字,反向传播计算每个参数应该往哪里改,优化器完成更新,最后用测试集评估效果。线性回归、softmax回归乃至后面更深的网络,都会反复使用这条主线。