资讯动态

从Softmax回归到多层感知机:分类任务核心机制与工程实践

发布时间:2026/9/20 8:00:05 来源:尧图企业网站定制
1. 从线性回归到Softmax回归分类问题的第一道门槛1.1 为什么分类问题不能直接套用线性回归很多刚接触《动手学深度学习》的朋友学到第24集左右会碰到一个坎前面线性回归那套“预测一个连续值”的思路刚摸熟突然就要做分类了。分类的输出是“猫、狗、鸟”这种离散标签你拿线性回归去拟合输出可能是3.7或者-1.2这怎么解释没法解释。我当初在这里卡了很久后来想明白一件事分类问题的本质是估计每个类别的概率。比如一张图模型告诉你“猫的概率0.7狗的概率0.2鸟的概率0.1”这个输出才有意义。而线性回归的输出是实数域上的任意值它天然不具备“概率分布”的约束——不能保证非负也不能保证加起来等于1。所以Softmax回归登场了。它做的事情其实很朴素先算线性输出再通过Softmax函数把一堆实数“压”成一个合法的概率分布。这个“压”的过程就是整个分类模型的核心机制之一。1.2 Softmax函数的数学直觉与手算过程Softmax的公式看起来简单$$\hat{y}_j \frac{\exp(o_j)}{\sum_k \exp(o_k)}$$其中 $o_j$ 是第 $j$ 个类别的线性输出logit。我第一次看到这个公式的时候觉得“就这”但真正手算过几遍之后才发现里面的门道。假设三个类别的logit分别是 $o [2.0, 1.0, 0.1]$我们来手算一遍先取指数$e^{2.0} \approx 7.389$$e^{1.0} \approx 2.718$$e^{0.1} \approx 1.105$求和$7.389 2.718 1.105 11.212$归一化$\hat{y} [0.659, 0.242, 0.099]$三个数加起来正好是1而且最大的logit对应最大的概率。这就是Softmax的核心作用保序 归一化。它不改变原来logit之间的大小关系但把输出变成了合法的概率分布。注意Softmax对logit的绝对大小不敏感只对相对差异敏感。如果你把所有logit同时加10Softmax的输出完全不变。这个性质在数值计算里非常关键后面讲数值稳定性的时候会用到。1.3 交叉熵损失为什么不用MSE分类问题用交叉熵而不是均方误差MSE这是很多人第一个“知其然不知其所以然”的点。我一开始也觉得MSE不是挺好吗为什么要换原因在于梯度行为。假设真实标签是类别0Softmax输出 $\hat{y}_0 0.1$预测得很差。如果用MSE损失是 $(1-0.1)^2 0.81$梯度里会带一个Softmax导数项 $\hat{y}_0(1-\hat{y}_0)$当预测值接近0或1的时候这个导数会变得非常小导致梯度消失学不动。交叉熵损失的形式是$$L -\sum_j y_j \log \hat{y}_j$$对于one-hot标签它简化为 $L -\log \hat{y}_{\text{正确类别}}$。预测正确类别的概率越低损失越大而且梯度形式极其干净——对logit的梯度就是 $\hat{y}_j - y_j$也就是“预测概率减去真实概率”。这个梯度不会因为预测值饱和而消失学习效率高得多。我实测过同样的网络结构MNIST上交叉熵通常比MSE快2到3倍收敛。这不是玄学是梯度性质决定的。1.4 从零实现Softmax回归的关键细节《动手学深度学习》在这一部分安排了从零实现的环节我觉得这是全书最值得动手敲一遍的章节之一。几个容易踩坑的点第一数值稳定性。直接算 $\exp(o_j)$ 当 $o_j$ 很大时会溢出。标准做法是先减去最大值$o_j - \max(o)$。数学上等价但数值上安全得多。这个技巧在后续所有涉及Softmax的地方都要用。第二批量计算。不要用for循环一个个样本算要用矩阵运算。假设批量大小是256类别数是10logit矩阵形状是 $(256, 10)$Softmax沿着维度1做一行代码搞定。第三交叉熵的实现。不要先算Softmax再算log而是用log-sum-exp技巧合并计算避免中间结果的精度损失。PyTorch的CrossEntropyLoss内部就是这么做的它接收的是原始logit而不是Softmax后的概率。# 数值稳定的Softmax实现 def softmax(X): X_exp torch.exp(X - X.max(dim1, keepdimTrue).values) partition X_exp.sum(dim1, keepdimTrue) return X_exp / partition这段代码我建议每个人都手敲一遍不要直接调库。理解了底层后面调库的时候才知道它在帮你做什么。2. 多层感知机打破线性模型的表达瓶颈2.1 为什么需要隐藏层Softmax回归本质上还是一个线性分类器——它只能画出线性的决策边界。对于XOR这种经典的非线性可分问题线性模型无论怎么训练都无能为力。这不是训练不够久的问题是模型表达能力的天花板。多层感知机MLP的思路是在输入和输出之间插入一个或多个隐藏层每个隐藏层后面接一个非线性激活函数。这样一来网络就具备了逼近任意连续函数的能力万能近似定理。注意关键词是“非线性”——如果你插入隐藏层但不加激活函数多层线性变换叠加还是线性变换等于白加。我见过不少初学者犯这个错误加了隐藏层忘了激活函数然后困惑为什么模型效果和单层一样。这个坑很隐蔽因为代码能跑通loss也在降就是效果上不去。2.2 激活函数的选择ReLU为什么成了默认选项《动手学深度学习》里介绍了Sigmoid、Tanh、ReLU三种激活函数。实际项目中ReLU及其变体几乎是默认选择原因有三计算简单ReLU就是 $\max(0, x)$没有指数运算前向和反向都极快。梯度不饱和Sigmoid和Tanh在输入很大或很小时梯度趋近于0深层网络里会导致梯度消失。ReLU在正半轴梯度恒为1梯度能顺畅回传。稀疏激活负半轴输出为0相当于一部分神经元被“关掉”带来一定的正则化效果。但ReLU也有自己的问题负半轴梯度为0如果某个神经元一直落在负半轴它就“死”了再也更新不了。这就是著名的“Dead ReLU”问题。实践中可以通过减小学习率、使用LeakyReLU或ELU来缓解。激活函数表达式优点缺点Sigmoid$1/(1e^{-x})$输出在(0,1)适合概率梯度消失严重Tanh$(e^x-e^{-x})/(e^xe^{-x})$零中心梯度消失ReLU$\max(0,x)$计算快不饱和Dead ReLU2.3 隐藏层大小的选择经验隐藏层神经元数量怎么定这个问题没有标准答案但有一些经验法则可以参考。太小了模型欠拟合学不到复杂的模式太大了参数量爆炸容易过拟合训练也慢。我通常的做法是先从一个中等大小开始比如256或512观察训练集和验证集的loss曲线。如果训练loss远低于验证loss说明过拟合了要么减小隐藏层要么加正则化如果两个loss都居高不下说明欠拟合可以增大隐藏层或加层。在Fashion-MNIST这个数据集上784维输入10类输出一个隐藏层256个神经元通常能到88%左右的准确率两个隐藏层256128能到89%左右。再往上加收益递减明显。2.4 从零实现MLP参数初始化的重要性从零实现MLP的时候参数初始化是一个容易被忽视但影响巨大的环节。如果全部初始化为0所有神经元的输出和梯度都一样对称性无法打破网络永远学不到东西。如果初始化太大激活值会爆炸太小信号会逐层衰减。常用的初始化方法Xavier初始化适用于Sigmoid和Tanh方差与输入输出维度相关。He初始化适用于ReLU方差是Xavier的两倍因为ReLU把一半的激活值置零了。PyTorch默认的初始化通常够用但如果你自己从零写一定要显式初始化。我试过用标准正态分布初始化一个5层MLP训练loss直接变成NaN换成He初始化后立刻正常。# He初始化示例 def init_weights(m): if isinstance(m, nn.Linear): nn.init.kaiming_normal_(m.weight, nonlinearityrelu) nn.init.zeros_(m.bias)3. Dropout与正则化让模型学会“不依赖”3.1 Dropout的工作原理Dropout是深度学习中最重要的正则化技术之一思路极其简单训练时随机“丢弃”一部分神经元的输出置零测试时使用全部神经元但把输出按丢弃概率缩放。为什么这能防止过拟合一种直观解释是Dropout让网络不能依赖任何一个特定的神经元因为那个神经元随时可能被丢掉。这迫使网络学习更加鲁棒的特征表示相当于在训练大量不同的子网络并做集成。我第一次理解Dropout的时候觉得“这不就是随机噪声吗”。但仔细想想它和普通噪声不一样——它是结构性的、乘性的噪声直接作用于神经元的激活值效果比加性噪声强得多。3.2 训练与测试阶段的差异Dropout最容易被搞错的地方是训练和测试阶段的行为不一致。训练时以概率 $p$ 丢弃神经元被保留的神经元输出不缩放或者按 $1/(1-p)$ 缩放取决于实现。测试时不丢弃任何神经元但需要把输出乘以 $(1-p)$ 来保持期望一致。PyTorch的nn.Dropout已经处理好了这一切你只需要在训练前调model.train()测试前调model.eval()。但如果你自己从零实现一定要记住这个差异。我见过有人测试时忘了缩放准确率直接掉了5个百分点排查了半天才发现是Dropout的问题。提示推理阶段一定要调用model.eval()否则Dropout和BatchNorm都会用训练模式的行为结果完全不对。这是新手最常犯的错误之一。3.3 Dropout概率的选择Dropout概率 $p$ 怎么选常见默认值是0.5但这不是万能答案。对于大型全连接层0.5通常效果不错。对于卷积层因为参数共享本身就有正则化效果Dropout概率通常设得更小0.1到0.3甚至不用。对于非常小的网络Dropout可能反而有害因为模型本身容量就不够再丢就学不动了。我的经验是先从0.2到0.5之间试观察验证集loss。如果验证loss比训练loss高很多说明过拟合严重可以增大Dropout如果两个loss差不多但都很高说明欠拟合应该减小Dropout或者去掉。3.4 动态Dropout一个值得尝试的变体热词里提到了“动态dropout”这是一个有意思的方向。传统Dropout的概率是固定的动态Dropout则根据训练进度或神经元的重要性调整丢弃概率。一种简单实现是训练初期用较小的Dropout概率让网络充分学习后期逐渐增大加强正则化。这背后的逻辑是早期网络还没学到东西丢太多会导致欠拟合后期网络开始过拟合了加大Dropout正好抑制。我试过线性递增的方案从0.1线性增到0.5在Fashion-MNIST上比固定0.5的验证准确率高了约0.3个百分点。提升不大但确实有效。不过要注意动态Dropout增加了超参数调优的复杂度如果时间有限固定概率也够用。4. 交叉熵的深入理解与常见困惑4.1 原始GAN公式中的交叉熵为什么没有负号这是一个在社区里被反复问到的经典问题。原始GAN的判别器损失是$$\max_D \mathbb{E}{x\sim p{data}}[\log D(x)] \mathbb{E}_{z}[\log(1-D(G(z)))]$$而标准交叉熵是带负号的。为什么这里没有关键在于优化方向不同。交叉熵损失通常是最小化所以带负号GAN的判别器目标是最大化判别准确率所以写成最大化形式自然没有负号。如果你把GAN的判别器损失改写成最小化形式负号就出现了$$\min_D -\mathbb{E}{x\sim p{data}}[\log D(x)] - \mathbb{E}_{z}[\log(1-D(G(z)))]$$所以不是“没有负号”而是“优化目标的方向决定了符号”。理解这一点GAN的损失函数就不再神秘了。4.2 BCE与交叉熵的关系二分类交叉熵BCE和多分类交叉熵本质上是同一个东西只是BCE是二分类的特例。对于二分类Softmax退化为Sigmoid交叉熵退化为$$L -[y\log\hat{y} (1-y)\log(1-\hat{y})]$$这就是BCE。PyTorch里BCELoss和CrossEntropyLoss的区别在于BCELoss期望输入已经过SigmoidCrossEntropyLoss期望输入是原始logit并内部做Softmax。用错了会导致数值不稳定或结果错误。我建议二分类也用CrossEntropyLoss把输出维度设为2这样数值稳定性由库保证不容易出错。4.3 交叉熵的梯度推导交叉熵配合Softmax的梯度推导是深度学习里最优雅的结果之一。设logit为 $o$Softmax输出为 $\hat{y}$真实标签为 $y$one-hot则$$\frac{\partial L}{\partial o_j} \hat{y}_j - y_j$$推导过程用到了Softmax的雅可比矩阵和链式法则但最终结果极其简洁。这个梯度的含义很直观如果预测概率高于真实概率梯度为正参数往减小该logit的方向更新反之亦然。这个简洁的梯度形式是交叉熵在分类任务中占统治地位的根本原因。它让优化过程稳定、高效不会因为预测饱和而停滞。5. 多层感知机实现服装分类的完整流程5.1 数据准备与预处理Fashion-MNIST是《动手学深度学习》里用来替代MNIST的服装分类数据集10个类别60000张训练图10000张测试图每张28x28灰度图。预处理的关键步骤展平把28x28的图展成784维向量因为MLP接收的是向量而不是图像。归一化把像素值从[0,255]缩放到[0,1]或标准化到均值0方差1。归一化能加速收敛这是实测有效的。批量化用DataLoader按batch取数据batch size通常设64到256。transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ])5.2 模型定义与训练循环一个典型的两层MLPnet nn.Sequential( nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, 128), nn.ReLU(), nn.Dropout(0.2), nn.Linear(128, 10) )训练循环的要点优化器用Adam或SGDMomentum学习率从0.001开始试。每个epoch记录训练loss和验证准确率。用model.train()和model.eval()切换模式。我实测下来这个结构在Fashion-MNIST上训练20个epoch能到89%左右的测试准确率。如果加数据增强随机水平翻转、随机裁剪能再提升1到2个百分点。5.3 训练过程中的监控与调优训练过程中要盯住几个信号训练loss不降学习率太小或模型有问题。训练loss降但验证loss不降过拟合加正则化或减模型容量。loss震荡严重学习率太大减小或加学习率衰减。验证准确率突然掉可能是学习率过大导致跳出了好的区域。我习惯每5个epoch打印一次验证准确率同时保存验证准确率最高的模型权重。这样即使后面过拟合了也能拿到最好的那个版本。6. 常见问题与排查技巧实录6.1 损失变成NaN怎么办这是训练中最常见的问题之一。原因通常有三类学习率太大梯度爆炸参数飞到无穷。解决方法是减小学习率或加梯度裁剪。log(0)问题交叉熵里如果预测概率是0log(0)是负无穷。用数值稳定的实现可以避免。输入数据有异常值比如未归一化的数据里有极大的值。检查数据预处理。我遇到过一次NaN排查了半天发现是数据里有几个像素值是255但没归一化导致第一层输出爆炸。归一化之后立刻正常。6.2 准确率上不去怎么排查准确率卡在一个值上不去按以下顺序排查模型容量够不够加层或加宽试试。训练够不够久loss还在降就继续训。学习率合不合适试试学习率扫描。数据有没有问题标签对不对有没有脏数据。正则化是不是太强Dropout或权重衰减太大。6.3 常见问题速查表问题可能原因解决方法loss为NaN学习率过大/数据未归一化减小学习率/归一化数据验证准确率远低于训练过拟合加Dropout/权重衰减/数据增强训练loss不降学习率过小/模型太简单增大学习率/增加模型容量测试时准确率骤降忘了model.eval()推理前调用eval()Dead ReLU学习率过大/初始化不当减小学习率/换LeakyReLU6.4 几个独家避坑技巧技巧一调试时先用一个很小的子集比如1000张图过拟合。如果模型连1000张图都过拟合不了说明模型或训练流程有问题不用浪费时间在全量数据上。技巧二保存训练过程中的随机种子。深度学习有随机性同样的代码两次跑结果可能差1到2个百分点。固定种子能让实验可复现。技巧三学习率用余弦退火或阶梯衰减通常比固定学习率效果好。我习惯用CosineAnnealingLR从0.01降到0.0001效果稳定。技巧四如果时间允许用K折交叉验证来评估模型。单次划分的验证集可能不够代表性K折能给出更可靠的估计。7. 从集合24到50的知识脉络梳理7.1 核心概念之间的逻辑关系从第24集到第50集内容围绕“从线性到非线性、从回归到分类、从欠拟合到过拟合”这条主线展开。Softmax回归解决了分类的输出表示问题交叉熵解决了分类的损失函数问题多层感知机解决了非线性表达能力问题Dropout解决了过拟合问题。这四个点串起来构成了深度学习分类任务的基础框架。理解这条脉络比记住每个公式更重要。因为后续的卷积网络、循环网络、注意力机制本质上都是在这个框架上做扩展——换更强的特征提取器换更适合序列的损失函数换更高效的正则化方法。7.2 这些知识在实际项目中的应用这些基础概念在实际项目中的价值远超“入门知识”的定位。我做过一个文本分类项目数据量不大几千条用TF-IDF加逻辑回归只能到82%准确率。换成两层MLP加Dropout直接到88%。再后来加了预训练词向量到91%。每一步提升都对应着这里讲的一个概念MLP带来非线性Dropout抑制过拟合预训练带来更好的输入表示。所以不要觉得这些是“基础”就跳过。恰恰是这些基础决定了你后面能走多远。我见过太多人直接上Transformer结果连交叉熵和Softmax的关系都说不清楚调参全靠试效率极低。7.3 后续学习方向的建议学完这部分之后建议的下一步卷积神经网络处理图像数据的标配理解卷积、池化、感受野。批量归一化加速训练、稳定梯度的利器和Dropout配合使用。优化器进阶Adam、RMSProp、学习率调度策略。正则化进阶权重衰减、数据增强、早停。我个人在实际操作中的体会是基础打牢之后学新东西的速度会快很多。因为新东西往往是旧概念的组合或变体你有了框架往里填就行。反过来如果基础不牢每学一个新模型都像从零开始效率极低。最后分享一个小技巧学完每个概念后尝试用三句话向别人解释它——是什么、为什么需要它、怎么用。如果你能说清楚说明真懂了如果说不清楚回去再看一遍。这个方法帮我省了很多“以为自己懂了”的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价