从Softmax回归到多层感知机:分类任务核心机制与工程实践
2026/9/20 7:59:57 网站建设 项目流程

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同时加10,Softmax的输出完全不变。这个性质在数值计算里非常关键,后面讲数值稳定性的时候会用到。

1.3 交叉熵损失:为什么不用MSE

分类问题用交叉熵而不是均方误差(MSE),这是很多人第一个“知其然不知其所以然”的点。我一开始也觉得,MSE不是挺好吗,为什么要换?

原因在于梯度行为。假设真实标签是类别0,Softmax输出 $\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,类别数是10,logit矩阵形状是 $(256, 10)$,Softmax沿着维度1做,一行代码搞定。

第三,交叉熵的实现。不要先算Softmax再算log,而是用log-sum-exp技巧合并计算,避免中间结果的精度损失。PyTorch的CrossEntropyLoss内部就是这么做的,它接收的是原始logit而不是Softmax后的概率。

# 数值稳定的Softmax实现 def softmax(X): X_exp = torch.exp(X - X.max(dim=1, keepdim=True).values) partition = X_exp.sum(dim=1, keepdim=True) 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/(1+e^{-x})$输出在(0,1),适合概率梯度消失严重
Tanh$(e^x-e^{-x})/(e^x+e^{-x})$零中心梯度消失
ReLU$\max(0,x)$计算快,不饱和Dead ReLU

2.3 隐藏层大小的选择经验

隐藏层神经元数量怎么定?这个问题没有标准答案,但有一些经验法则可以参考。

太小了,模型欠拟合,学不到复杂的模式;太大了,参数量爆炸,容易过拟合,训练也慢。我通常的做法是:先从一个中等大小开始(比如256或512),观察训练集和验证集的loss曲线。如果训练loss远低于验证loss,说明过拟合了,要么减小隐藏层,要么加正则化;如果两个loss都居高不下,说明欠拟合,可以增大隐藏层或加层。

在Fashion-MNIST这个数据集上,784维输入,10类输出,一个隐藏层256个神经元通常能到88%左右的准确率,两个隐藏层(256+128)能到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, nonlinearity='relu') 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里BCELossCrossEntropyLoss的区别在于:BCELoss期望输入已经过Sigmoid,CrossEntropyLoss期望输入是原始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 模型定义与训练循环

一个典型的两层MLP:

net = 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或SGD+Momentum,学习率从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)问题:交叉熵里如果预测概率是0,log(0)是负无穷。用数值稳定的实现可以避免。
  • 输入数据有异常值:比如未归一化的数据里有极大的值。检查数据预处理。

我遇到过一次NaN,排查了半天发现是数据里有几个像素值是255但没归一化,导致第一层输出爆炸。归一化之后立刻正常。

6.2 准确率上不去怎么排查

准确率卡在一个值上不去,按以下顺序排查:

  1. 模型容量够不够:加层或加宽试试。
  2. 训练够不够久:loss还在降就继续训。
  3. 学习率合不合适:试试学习率扫描。
  4. 数据有没有问题:标签对不对,有没有脏数据。
  5. 正则化是不是太强:Dropout或权重衰减太大。

6.3 常见问题速查表

问题可能原因解决方法
loss为NaN学习率过大/数据未归一化减小学习率/归一化数据
验证准确率远低于训练过拟合加Dropout/权重衰减/数据增强
训练loss不降学习率过小/模型太简单增大学习率/增加模型容量
测试时准确率骤降忘了model.eval()推理前调用eval()
Dead ReLU学习率过大/初始化不当减小学习率/换LeakyReLU

6.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、学习率调度策略。
  • 正则化进阶:权重衰减、数据增强、早停。

我个人在实际操作中的体会是,基础打牢之后,学新东西的速度会快很多。因为新东西往往是旧概念的组合或变体,你有了框架,往里填就行。反过来,如果基础不牢,每学一个新模型都像从零开始,效率极低。

最后分享一个小技巧:学完每个概念后,尝试用三句话向别人解释它——是什么、为什么需要它、怎么用。如果你能说清楚,说明真懂了;如果说不清楚,回去再看一遍。这个方法帮我省了很多“以为自己懂了”的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询