我们开始吧。这次我们紧扣李沐老师《动手学深度学习》的理论和代码,把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操作
代码完美对应三步:
defsoftmax(X):X_exp=torch.exp(X)# 1. 求幂partition=X_exp.sum(1,keepdim=True)# 2. 行求和returnX_exp/partition# 3. 归一化(广播机制)广播机制:partition 形状是 (batch_size, 1),X_exp 是 (batch_size, 10)。除法时,PyTorch会自动将 partition 复制10列,让每个元素都除以自己那一行的和。
第3步:定义网络模型
defnet(X):returnsoftmax(torch.matmul(X.reshape((-1,W.shape[0])),W)+b)reshape((-1, W.shape[0])) 将每张图展平成784维向量。matmul 做矩阵乘法,再加上偏置 b,最后用 softmax 转成概率。
第4步:定义交叉熵损失函数
defcross_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。
importtorchfromtorchimportnn net=nn.Sequential(nn.Flatten(),nn.Linear(784,10))# 1. 定义模型[reference:44]definit_weights(m):iftype(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 辅助函数。
# 辅助函数:计算这批数据猜对了几个defaccuracy(y_hat,y):"""计算预测正确的数量。"""iflen(y_hat.shape)>1andy_hat.shape[1]>1:y_hat=y_hat.argmax(axis=1)# 找出每行概率最大的索引(预测类别)cmp=y_hat.type(y.dtype)==y# 比较预测和真实标签returnfloat(cmp.type(y.dtype).sum())# 返回猜对的总个数# 主评估函数(IMG_9002 & 9003)defevaluate_accuracy(net,data_iter):"""计算在指定数据集上模型的精度。"""ifisinstance(net,torch.nn.Module):net.eval()# 切换到评估模式(关掉 Dropout 等)metric=Accumulator(2)# 累加器:第0位存猜对总数,第1位存样本总数forX,yindata_iter:# 累加:accuracy(net(X), y)是猜对个数,y.numel()是总元素个数metric.add(accuracy(net(X),y),y.numel())returnmetric[0]/metric[1]# 返回准确率(猜对 / 总数)第6步:核心训练循环(train_epoch_ch3)
对应你的 IMG_9004。注意截图最后有笔误,我下面写的是修正后的正确版本。
deftrain_epoch_ch3(net,train_iter,loss,updater):"""训练模型一个迭代周期(把所有数据过一遍)。"""ifisinstance(net,torch.nn.Module):net.train()# 切换到训练模式metric=Accumulator(3)# 累加器:0-总损失,1-猜对总数,2-样本总数forX,yintrain_iter:y_hat=net(X)# 1. 前向传播:得到预测概率l=loss(y_hat,y)# 2. 算损失ifisinstance(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())# 返回:平均损失(总损失/总样本数),准确率(猜对数/总样本数)returnmetric[0]/metric[2],metric[1]/metric[2]第7步:预测与可视化(predict_ch3)
对应你的 IMG_9007。
defpredict_ch3(net,test_iter,n=6):"""预测并显示前 n 张图片的标签。"""forX,yintest_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'+predfortrue,predinzip(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 轴还有别扭,随时吼我,我再给你开小灶!😄