【动手学深度学习】softmax回归 我们开始吧。这次我们紧扣李沐老师《动手学深度学习》的理论和代码把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_gradTrue 让PyTorch跟踪梯度。第2步定义Softmax操作代码完美对应三步defsoftmax(X):X_exptorch.exp(X)# 1. 求幂partitionX_exp.sum(1,keepdimTrue)# 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(axis1) 对新图片预测并与真实标签对比可视化。这也是检验模型学习成果最直观的方式。三、更上一层楼使用简洁实现“从零开始实现”是为了看清细节。实际开发中我们直接用PyTorch的高级API。importtorchfromtorchimportnn netnn.Sequential(nn.Flatten(),nn.Linear(784,10))# 1. 定义模型[reference:44]definit_weights(m):iftype(m)nn.Linear:nn.init.normal_(m.weight,std0.01)net.apply(init_weights)# 2. 初始化权重[reference:45]lossnn.CrossEntropyLoss()# 3. 损失函数[reference:46]trainertorch.optim.SGD(net.parameters(),lr0.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_haty_hat.argmax(axis1)# 找出每行概率最大的索引预测类别cmpy_hat.type(y.dtype)y# 比较预测和真实标签returnfloat(cmp.type(y.dtype).sum())# 返回猜对的总个数# 主评估函数IMG_9002 9003defevaluate_accuracy(net,data_iter):计算在指定数据集上模型的精度。ifisinstance(net,torch.nn.Module):net.eval()# 切换到评估模式关掉 Dropout 等metricAccumulator(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()# 切换到训练模式metricAccumulator(3)# 累加器0-总损失1-猜对总数2-样本总数forX,yintrain_iter:y_hatnet(X)# 1. 前向传播得到预测概率lloss(y_hat,y)# 2. 算损失ifisinstance(updater,torch.optim.Optimizer):# PyTorch 自带优化器高阶APIupdater.zero_grad()# 梯度清零l.backward()# 3. 反向传播算梯度updater.step()# 4. 更新参数else:# 手动实现优化器比如用 d2l.sgdl.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,n6):预测并显示前 n 张图片的标签。forX,yintest_iter:break# 只取第一个批次的测试数据truesd2l.get_fashion_mnist_labels(y)# 获取真实标签名字# argmax(axis1) 取出每行最大概率的索引即预测类别predsd2l.get_fashion_mnist_labels(net(X).argmax(axis1))titles[true\npredfortrue,predinzip(trues,preds)]# 把前 n 张图显示出来上面写真实标签下面写预测标签d2l.show_images(X[0:n].reshape((n,28,28)),1,n,titlestitles[0:n])argmax(axis1) 详解axis1 表示在列方向类别维度上操作。对于每一行每张图找出这10个概率中最大值的列索引0~9这个索引就是模型认为的最终答案。总结一下代码逻辑链条reshape把图片拉成表格行好做矩阵乘法。交叉熵索引用高级索引精准捞出真实类别的概率算损失。第5步看模型猜对了百分之几评估。第6步循环做前向反向更新训练。第7步拿出训练好的模型画出新图并标出它猜的结果预测。现在这 7 步代码严丝合缝了。如果对 Accumulator 累加器或者 argmax 轴还有别扭随时吼我我再给你开小灶