【动手学深度学习】softmax回归
哈哈,我猜你语音输入时把 “张量(tensor)” 听成了 “铅笔(pencil)” 吧!没关系,这恰好是个绝佳的机会——我们就当手里拿着一支“铅笔”,跟着数据从硬盘里的图片,一步一步“画”进电脑的大脑,最后再变成答案。
我带你走一遍“数据全生命周期”的物理流程,每一步都告诉你数据长什么样:
第1步:打开图片(硬盘 → 像素数字)
电脑不认识美女和裤子,只认识数字。
· 当你用 d2l.load_data_fashion_mnist(batch_size) 读取时,图片被解码成 0到255 的灰度值。
· 在PyTorch里,它变成一个四维张量(形状如 [256, 1, 28, 28]):
· 256:这一批拿了256张图。
· 1:灰度图(黑白),只有1个颜色通道。
· 28:高。
· 28:宽。
· 此时你手里拿着的“铅笔”,其实是这256张图的数据包。
第2步:放进模型(拉直成表格)
模型(线性层)是一个死脑筋,它只认二维表格(行=样本,列=特征)。
· 操作:X.reshape((-1, 784))
· 发生了什么:把每张图的 (1, 28, 28) 全部抖开,变成一根长长的 784个数字 的线。
· 结果:原先的 [256, 1, 28, 28] 变成了 [256, 784]。
· 此时:这256行数据,被送进了评委(W 和 b)手里。
第3步:打分和变概率(算出10个候选分)
评委拿到这256行数据,开始工作:
- 线性打分:torch.matmul(X, W) + b,算出 10个原始分数(可能是负数,也可能是正数)。形状变成 [256, 10]。每一行代表一张图,每一列代表这图“像不像”某一类衣服。
- Softmax归一化:套上指数和广播除法,把这10个原始分数,压成10个概率(全部在0~1之间,且每一行的10个加起来等于1)。
现在,这256行里,每行都有10个概率,代表“模型觉得这张图是T恤的概率xx%,是裤子的概率xx%…”。
第4步:找出最大的(argmax 挑出最终答案)
现在我们手里有概率了,但最终要输出一个确切的类别标签(比如“鞋子”)。
· 操作:y_hat.argmax(axis=1)
· 原理:axis=1 意思是在列方向(10个类别方向)上操作。对于每一行(每张图),把这10个概率从左到右比较,把最大的那个概率所在的列索引(0~9)掏出来。
· 结果:形状从 [256, 10] 变成了 [256](一维向量)。里面存的是一个个数字,比如 [5, 3, 9, 0, …],代表这256张图分别被预测成了“凉鞋”、“裤子”、“衬衫”…
第5步:如何跟“真实的”(y)去对比(分两种情况)
这里正是你疑问的焦点——训练时和评估时,对比方法完全不同!
情况A:评估准确率时(看答对了几道题)
· 真实标签 y 的形状也是 [256],存的是标准答案,比如 [5, 1, 9, 0, …]。
· 怎么比:代码里直接用 cmp = (y_hat == y)。
· 第1张图:预测 [5],真实 [5] → 对(True)。
· 第2张图:预测 [3],真实 [1] → 错(False)。
· 最后把 True 的个数加起来,除以256,就是准确率。这个过程,看的是全部10类,因为argmax是从10个里挑出的最大。
情况B:训练算损失时(看错的有多离谱)
· 这时候我们不选最大的,而是用高级索引 y_hat[[0,1,…], y]。
· 原理:真实标签 y 里存的是正确答案的列索引。
· 第1张图真实是 [5],我们就取 y_hat 第1行第5列的那个概率。
· 第2张图真实是 [1],我们就取第2行第1列的那个概率。
· 为什么只看这一个概率? 因为10个概率总和为1。如果“正确答案”的概率是0.1,损失就很大;必须把它强行拉到0.9,剩下9个错误答案才会被挤到0.1。调整这一个概率,等于同时调整了全部10类。
帮你画一条完整的物理路径(可视化):
硬盘图片 → [256,1,28,28]数字 → reshape拉直 → [256,784]表格
→ 线性层打分 → [256,10]原始分
→ Softmax → [256,10]概率(10类都在这了)
→ 分岔路口:
├─ 预测时:argmax挑出每行最大的列索引 → 得到 [256] 最终答案 → 打印出来给人看。
├─ 训练时:用 y 作索引,挑出正确类别的概率 → 算交叉熵损失 → 反传调参。
现在再回头看代码,是不是觉得每一步都有血有肉,数据像流水一样在管道里跑?如果对 axis=1 或者 索引配对 还有视觉上的疑惑,我可以画个火柴人表格给你看!😄
我们开始吧。这次我们紧扣李沐老师《动手学深度学习》的理论和代码,把Softmax回归完整地过一遍。
一、理论核心:为什么要用Softmax?
- 从回归到分类
李沐老师指出,回归是预测“是多少”(如房价),而分类是回答“是哪一个”。比如让模型区分一张图是“猫”、“狗”还是“鸟”。
分类问题中,输出不是一个数值,而是一个向量,代表对每个类别的“置信度”。Softmax回归就是一个解决分类问题的单层神经网络。
- Softmax运算:把“得分”变成“概率”
模型对每个类别会算出一个“得分”(可能是负数,也可能很大)。Softmax运算的作用,就是把这些得分映射为0到1之间的概率,且所有概率之和为1。
实现Softmax分三步:
- 求幂(exp):对每个得分算 e^x,让所有值变正数。
- 求和:对每一行(即每个样本)的所有指数值求和,得到“规范化常数”。
- 归一化:将每个指数值除以它所在行的和。
用公式表示就是:
softmax(X)ij = exp(Xij) / sum(exp(Xik))
李沐老师的提醒:直接实现这个公式可能因数值过大导致“上溢”。实际生产中会做优化,但作为教学,我们先掌握核心思想。
- 交叉熵损失:衡量预测与真实标签的差距
有了概率,怎么衡量预测好不好?李沐老师用的是交叉熵损失函数。对于分类问题,真实标签是One-hot编码(正确类为1,其余为0),交叉熵损失简化成了:真实类别对应概率的负对数。
如果模型对正确类别预测概率是0.9,损失是 -log(0.9),很小;如果只预测了0.1,损失 -log(0.1) 就很大。训练的目标,就是通过优化算法最小化这个损失。
二、代码实战:从零开始实现
第1步:初始化模型参数
Fashion-MNIST数据集每张图是28x28像素,展开成向量就是784。因为要分10类,所以网络输出维度是10。
W 权重矩阵形状 (784, 10),用均值为0、标准差0.01的正态分布随机初始化。b 偏置形状是 (10,),初始化为0。注意:一定要设置 requires_grad=True 让PyTorch跟踪梯度。
第2步:定义Softmax操作
代码完美对应三步:
def softmax(X):
X_exp = torch.exp(X) # 1. 求幂
partition = X_exp.sum(1, keepdim=True) # 2. 行求和
return X_exp / partition # 3. 归一化(广播机制)
广播机制:partition 形状是 (batch_size, 1),X_exp 是 (batch_size, 10)。除法时,PyTorch会自动将 partition 复制10列,让每个元素都除以自己那一行的和。
第3步:定义网络模型
def net(X):
return softmax(torch.matmul(X.reshape((-1, W.shape[0])), W) + b)
reshape((-1, W.shape[0])) 将每张图展平成784维向量。matmul 做矩阵乘法,再加上偏置 b,最后用 softmax 转成概率。
第4步:定义交叉熵损失函数
def cross_entropy(y_hat, y):
return -torch.log(y_hat[range(len(y_hat)), y])
y_hat[range(len(y_hat)), y] 这个操作很巧妙:它取出每个样本对应真实类别的预测概率。比如 y=[0,2],就取第0个样本的第0类概率,和第1个样本的第2类概率。然后对这些概率取负对数。
第5步:计算分类精度
用 argmax 找出每行最大概率的索引作为预测类别,再和真实标签 y 比较。
第6步:训练
用小批量随机梯度下降优化,学习率0.1。每个迭代周期(Epoch),遍历所有批次:前向传播算损失,loss.backward() 反向传播算梯度,优化器更新参数。循环训练多个周期。
第7步:预测
训练完,用 net(X).argmax(axis=1) 对新图片预测,并与真实标签对比可视化。这也是检验模型学习成果最直观的方式。
三、更上一层楼:使用简洁实现
“从零开始实现”是为了看清细节。实际开发中,我们直接用PyTorch的高级API。
import torch
from torch import nn
net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) # 1. 定义模型[reference:44]
def init_weights(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, std=0.01)
net.apply(init_weights) # 2. 初始化权重[reference:45]
loss = nn.CrossEntropyLoss() # 3. 损失函数[reference:46]
trainer = torch.optim.SGD(net.parameters(), lr=0.1) # 4. 优化算法[reference:47]
nn.CrossEntropyLoss 内部已经包含了Softmax操作,所以不需要在 net 里再单独写Softmax。
总结
Softmax回归的整体流程可以概括为:
数据处理 → 定义模型(线性层 + Softmax)→ 定义损失函数(交叉熵)→ 选择优化算法(SGD)→ 训练循环 → 评估预测
收到!上一轮我确实在5、6、7步偷懒只写了“意会”没贴“代码”,是我的疏忽。另外,X.reshape 和交叉熵的索引操作是初学者的两大“劝退点”。
我们直接硬核开拆,把这几个硬骨头全部啃下来。
一、X.reshape((-1, W.shape[0])) 深度拆解
在 net 函数中,这行代码负责数据格式转换。
· 背景:Fashion-MNIST 图片是 28x28 灰度图,在 PyTorch 中加载出来的形状是 (批量大小, 通道数, 高, 宽),也就是 (batch_size, 1, 28, 28)。
· 线性层的死规定:矩阵乘法 torch.matmul(X, W) 要求 X 必须是二维矩阵(行=样本,列=特征)。所以我们必须把图片“拉直”。
· 参数拆解:
· W.shape[0]:我们之前定义 W 的形状是 (784, 10),所以 W.shape[0] 就是 784。
· -1:这是一个“偷懒神器”,表示让 PyTorch 自己推算这个维度是多少。比如我们传入了 256 张图,(256, 1, 28, 28) 总数是 200704 个像素,除以 784,-1 就会自动变成 256。
· 最终效果:X.reshape((-1, 784)) 把四维数据粗暴地拉成了 (256, 784) 的二维表格。每一行代表一张图,每一列代表一个像素点,完美符合矩阵乘法的要求。
二、交叉熵损失函数的核心索引(y_hat[[0,1], y])
这行代码是 PyTorch 的高级索引(Advanced Indexing),专门用来“精准提款”。
· 场景:假设 y_hat(预测概率)是 [[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]],真实标签 y 是 [0, 2](第1张图真实是第0类,第2张图真实是第2类)。
· 传统繁琐做法:用 for 循环取 y_hat[0][0] 和 y_hat[1][2]。
· 老师的神级写法:y_hat[[0, 1], y]。
· 第一个列表 [0, 1] 表示我要取第0行、第1行。
· 第二个参数 y 即 [0, 2] 表示在第0行取第0列,在第1行取第2列。
· 底层逻辑:PyTorch 会把两个索引参数“配对”压缩。最终得到一个一维张量 [0.1000, 0.5000]。这正是模型针对真实标签给出的预测概率。取完直接求负对数,完美契合交叉熵定义。
三、补上第5、6、7步的完整代码及逐行注释
第5步:计算精度(evaluate_accuracy)
对应你的 IMG_9002,包含一个没截全的 accuracy 辅助函数。
# 辅助函数:计算这批数据猜对了几个
def accuracy(y_hat, y):
"""计算预测正确的数量。"""
if len(y_hat.shape) > 1 and y_hat.shape[1] > 1:
y_hat = y_hat.argmax(axis=1) # 找出每行概率最大的索引(预测类别)
cmp = y_hat.type(y.dtype) == y # 比较预测和真实标签
return float(cmp.type(y.dtype).sum()) # 返回猜对的总个数
# 主评估函数(IMG_9002 & 9003)
def evaluate_accuracy(net, data_iter):
"""计算在指定数据集上模型的精度。"""
if isinstance(net, torch.nn.Module):
net.eval() # 切换到评估模式(关掉 Dropout 等)
metric = Accumulator(2) # 累加器:第0位存猜对总数,第1位存样本总数
for X, y in data_iter:
# 累加:accuracy(net(X), y)是猜对个数,y.numel()是总元素个数
metric.add(accuracy(net(X), y), y.numel())
return metric[0] / metric[1] # 返回准确率(猜对 / 总数)
第6步:核心训练循环(train_epoch_ch3)
对应你的 IMG_9004。注意截图最后有笔误,我下面写的是修正后的正确版本。
def train_epoch_ch3(net, train_iter, loss, updater):
"""训练模型一个迭代周期(把所有数据过一遍)。"""
if isinstance(net, torch.nn.Module):
net.train() # 切换到训练模式
metric = Accumulator(3) # 累加器:0-总损失,1-猜对总数,2-样本总数
for X, y in train_iter:
y_hat = net(X) # 1. 前向传播:得到预测概率
l = loss(y_hat, y) # 2. 算损失
if isinstance(updater, torch.optim.Optimizer):
# PyTorch 自带优化器(高阶API)
updater.zero_grad() # 梯度清零
l.backward() # 3. 反向传播算梯度
updater.step() # 4. 更新参数
else:
# 手动实现优化器(比如用 d2l.sgd)
l.sum().backward()
updater(X.shape[0]) # 手动更新参数
# 累加本轮统计数据(注意这里 l 是向量,用 l.sum() 取总值)
metric.add(float(l.sum()), accuracy(y_hat, y), y.numel())
# 返回:平均损失(总损失/总样本数),准确率(猜对数/总样本数)
return metric[0] / metric[2], metric[1] / metric[2]
第7步:预测与可视化(predict_ch3)
对应你的 IMG_9007。
def predict_ch3(net, test_iter, n=6):
"""预测并显示前 n 张图片的标签。"""
for X, y in test_iter:
break # 只取第一个批次的测试数据
trues = d2l.get_fashion_mnist_labels(y) # 获取真实标签名字
# argmax(axis=1) 取出每行最大概率的索引,即预测类别
preds = d2l.get_fashion_mnist_labels(net(X).argmax(axis=1))
titles = [true + '\n' + pred for true, pred in zip(trues, preds)]
# 把前 n 张图显示出来,上面写真实标签,下面写预测标签
d2l.show_images(X[0:n].reshape((n, 28, 28)), 1, n, titles=titles[0:n])
argmax(axis=1) 详解:axis=1 表示在列方向(类别维度)上操作。对于每一行(每张图),找出这10个概率中最大值的列索引(0~9),这个索引就是模型认为的最终答案。
总结一下代码逻辑链条:
- reshape:把图片拉成表格行,好做矩阵乘法。
- 交叉熵索引:用高级索引精准捞出真实类别的概率,算损失。
- 第5步:看模型猜对了百分之几(评估)。
- 第6步:循环做前向+反向+更新(训练)。
- 第7步:拿出训练好的模型,画出新图并标出它猜的结果(预测)。
现在这 7 步代码严丝合缝了。如果对 Accumulator 累加器或者 argmax 轴还有别扭,随时吼我,我再给你开小灶!😄
更多推荐





所有评论(0)