我在不少入门讨论区里见过同一个问题:sigmoid 和 softmax 长得那么像,为什么中间层用的是 sigmoid,输出层却是 softmax?直接拿 sigmoid 当输出层行不行?这个问题如果只看结论,你可能会背出“sigmoid 用于二分类,softmax 用于多分类”,但真的从 MLP 一路看到 LeNet 时,你会发现事情远没有这么简单:LeNet 的卷积中间层明明在用 sigmoid 做非线性,输出层却要换成 softmax,两者到底分工在哪里?
这篇文章就围绕这个“换人”的过程展开。我会从 MLP 的结构缺陷讲起,把 LeNet 当作一个完整标本,掰开来看 sigmoid 和 softmax 各自解决什么问题,以及为什么输出层非要它们不可。适合刚学完神经网络基础、正准备上手卷积网络的读者,也适合那些会用框架但一直没搞懂损失函数和输出层配合关系的人。
1. 为什么拿 LeNet 当标本:从 MLP 到卷积结构的第一个完整样本
要理解 sigmoid 和 softmax 的分工,最好的方式不是单独看数学公式,而是放进一个真实网络里看它们的位置。LeNet 恰好是这样一个标本:它足够小,结构足够经典,而且把“中间层用 sigmoid 压缩特征,输出层用 softmax 给答案”这件事展现得清清楚楚。
1.1 MLP 到 LeNet 的跨越:问题不是“网络变深了”,而是“特征提取方式变了”
先回顾一下 MLP。一个典型的多层感知机,输入是一个向量,中间若干个全连接层,最后接一个输出层。全连接意味着每个输入节点和下一层的每个节点都有连接,参数数量随节点数量平方增长。处理图像时,一张 32×32 的三通道图拉直就是 3072 维,第一层隐层如果放 256 个神经元,光这一层就是接近 78 万个参数,而且这种拉直操作把图像的二维空间结构完全丢掉了。
LeNet 的突破在于它不再把图像当作一维向量,而是用卷积核在二维平面上滑动,提取局部特征。一个卷积核只看一个小邻域,多个卷积核叠加起来就能识别边缘、纹理、局部形状。参数数量大幅下降,同时特征的位置信息被保留下来。这个思路直接影响了后来几乎所有视觉网络。
但要注意,卷积、池化只是在改变“特征怎么提取”,网络最后仍然需要一个决策模块。LeNet 的做法是:卷积特征图经过若干轮提取之后,展平成一维向量,再接两层全连接,最后输出 10 个类别的分数。也就是说,网络的前半段负责“看懂图像”,后半段的全连接和输出层负责“给出判断”。sigmoid 出现在前半段和中间段,softmax 住在最后一步。
1.2 LeNet 输出端的设计:为什么卷积网络也要靠“全连接 + softmax”收尾
LeNet-5 的经典结构大致是:输入 32×32 灰度图,经过第一个卷积层得到 6 张特征图,然后经过一个“带 sigmoid 的下采样层”(其实就是带可学习参数的池化),再经过第二个卷积层得到 16 张特征图,再下采样,最后展平,接两个全连接层,输出 10 个数字对应的分数。
这里有个容易忽略的细节:早期 LeNet-5 的输出层并非我们现在常用的 softmax 层,而是某种基于径向基函数的距离度量层。但在后来的复现和教学中,大家普遍把输出层简化为“全连接层 + softmax + 交叉熵损失”,因为这种做法更直观、更好训练,也能解释清楚输出层激活函数的意义。这篇文章讨论的就是这个简化后依然成立的经典模式。
你可能会问:既然卷积网络已经完成了特征提取,为什么最后不直接用每个类别的得分做判断?得分不是越高越好吗?问题在于“得分”这个东西在训练时没法直接告诉网络“差了多少”。两个类别的得分差是 0.3 还是 3.0,对权重更新来说应该有不同的含义。可如果得分是任意实数,这个差值本身没有清晰的概率含义。softmax 就是用来把这些得分转换成“可信度分布”的,这事我们后面细说。
2. sigmoid 在这里不是“压缩”,而是神经元的“模拟开关”
很多教材喜欢说 sigmoid 的作用是“把输入压缩到 0 到 1 之间”。这句话没错,但它掩盖了真正重要的东西:sigmoid 不是一个数值转换器,而是一个可微分的、带饱和区的决策开关。理解这一点,你才能真正明白为什么 LeNet 会把它放在卷积和池化的中间。
2.1 没有非线性,堆多少层都白搭:线性叠加的数学事实
先从最根本的问题说起:神经网络为什么要激活函数?如果不是为了“压缩”,那目的是什么?
假设你只有一个线性层,输出是y = Wx + b。如果你堆两层线性层,第二层是y2 = W2(W1x + b1) + b2,展开之后依然是y2 = (W2W1)x + (W2b1 + b2)。看,两个线性变换叠加,本质上还是一个线性变换,根本没有引入新的表达能力。你堆一百层,和只堆一层的表达能力完全一样。
非线性的作用就在这里体现:没有它,网络无论多深都只能拟合线性关系,连异或问题都解决不了。sigmoid 这个 S 型曲线把线性变换的输出做了一个非常柔和、可导的“弯折”,每弯一次,网络就能表示更复杂的函数形状。LeNet 里每一轮特征提取之后,都需要这个弯折来增强表达力。
2.2 sigmoid 的曲线直觉:三个区间的不同手感
sigmoid 的公式是σ(z) = 1 / (1 + exp(-z)),它的输出永远在 0 到 1 之间,而且是对称的:σ(0)正好是 0.5。这条曲线可以分成三个有代表性的区间:
输入 z 远小于 0 时,输出贴近 0,神经元处于“关闭”状态;输入 z 远大于 0 时,输出贴近 1,神经元处于“开启”状态。这两种状态之间有一个过渡带,大约在 -4 到 4 之间,在这个区间里输出对输入的变化最敏感,导数值最大。
用开关来类比最合适:sigmoid 不是一个非开即闭的数字开关,而是一个带有“渐亮”过程的模拟开关。灯光从熄灭到亮起不是瞬间完成的,中间那段就是 sigmoid 的过渡区。网络在训练时,恰恰需要这段渐变区间来微调“打开的力度”,如果它是一个硬开关,梯度根本传不过去,训练也就无从谈起。
2.3 LeNet 中 sigmoid 被用在哪里:不只是卷积后的装饰
回到 LeNet 的具体结构。现在很多人复现 LeNet 时,会在卷积层之后用 ReLU,但最早的 LeNet-5 在下采样层里用的是 sigmoid。具体来说,它的下采样层会先对特征图的一个小邻域求和,再乘一个可学习系数、加一个偏置,然后过 sigmoid 做非线性压缩。
这个设计在当时很自然:卷积抽取局部特征,下采样降低分辨率,sigmoid 在降采样后把特征值限制在一个温和的范围内,避免特征值无限制地增长。从现代视角看,这种非线性的位置有些“偏弱”,因为真正强的非线性来自层间的激活函数。但无论位置怎么变,核心思想是一致的:每一轮特征变换之后,都要有一个可导的非线性函数来增加表达能力。
2.4 sigmoid 带来的“梯度饱和”:LeNet 时代能忍,后面忍不了
sigmoid 有一个无法回避的缺点:饱和区的梯度几乎为零。输入一旦到了 10 以上,sigmoid 的输出无限接近 1,导数却已经小到可以忽略。这意味着网络反传时,这个神经元收到的误差信号会被“截断”,权重几乎得不到更新。
在 LeNet 这种层数不多的网络里,这个问题还不算致命,因为网络浅,梯度经过几层衰减后还能传回来。但后续网络越堆越深,sigmoid 的饱和问题被无限放大,这也是后来 ReLU 这类分段线性激活函数取代它的直接原因。你可以这样理解:sigmoid 做“决策开关”的直觉是很好的,但它这个开关在两端太钝了。LeNet 用它是因为当时没有更好的选择,而它承担的职责——给线性变换引入非线性——是所有激活函数共同的使命。
3. softmax 的真相:它不是归一化工具,而是“概率投票器”
如果说 sigmoid 是神经元的开关,那 softmax 就是整个网络最后一场“投票”。它把输出层的一堆原始得分,转换为一个和为 1 的概率分布。但如果你只是把它当作“归一化”,那又把事情看浅了。softmax 最核心的细节在于那个exp,它决定了这张票怎么投。
3.1 先把 softmax 的公式拆到不能再拆
softmax 的公式长这样:
softmax(z)_i = exp(z_i) / Σ_j exp(z_j)
其中z是输出层神经元的原始得分(也叫 logits),i是当前类别。分母是所有类别得分取 exp 之后的和。显然输出所有分量都是正数,而且总和为 1,所以它可以被解释为“这个输入属于各类别的概率”。
我见过很多初学者问一个问题:如果只是为了“总和为 1”,为什么不用最简单的归一化,就是每个数除以所有数的和?比如得分是 [1, 2, 3],直接除以 6 不就得到概率了吗?
这个问题问得很好,答案藏在 exp 里,而且 exp 至少带来了三个好处。
3.2 为什么必须用 exp:放大差异、保证正数、让梯度计算顺滑
第一个好处是“保证正数”。神经网络输出的得分可能是负的,比如 [-1, 2, -0.5]。如果直接做除法,负得分会得到负概率,这在概率意义上完全没有意义。指数函数天然把负值映射成正数,这个障碍直接绕过去了。
第二个好处是“放大差异”。exp 是一个单调递增的凸函数,它对更大的输入给予不成比例的放大。得分 [1, 2, 3],对应的 exp 是 [2.718, 7.389, 20.085],比值从 1:2:3 变成了约 1:2.72:7.39。这意味着最大的那个得分被进一步“凸显”出来。直觉上这就像一场投票计票时,给第一名的选票额外加了权重,让赢者更突出。
第三个好处是“让梯度变得干净”。稍后讲交叉熵时你会看到,softmax 和交叉熵组合在一起时,梯度形式极其简洁,甚至不需要手工计算分母的联合作用。这是神经网络设计里少有的“天然配合”,靠着这个组合,输出层和损失函数之间的信息传递变得异常顺畅。
3.3 数值稳定性:为什么所有框架里都要减最大值
很多人在推 softmax 公式时,会在实现层面遇到一个大坑:logits 的数值稍微大一点,exp 就可能爆炸。比如得分是 [100, 101, 102],exp(102)已经是一个天文数字,直接算会溢出。
解决办法非常巧妙:给所有得分同时减去一个常数 C,最常见的是减去最大值。因为 softmax 的分子分母同时除以exp(C),结果不变:
softmax(z)_i = exp(z_i - C) / Σ_j exp(z_j - C)
取 C 为最大值后,最大的指数项变成exp(0)=1,其余项都小于等于 1,计算完全稳定。这个技巧在 PyTorch 的CrossEntropyLoss里是默认实现的,但如果你自己写 softmax 或是做量化推理,一定要记得这一步。这是那种“不踩一次不知道痛”的坑。
3.4 sigmoid 和 softmax 的关系:二分类的 softmax 就是 sigmoid
你可能已经隐约感觉到 sigmoid 和 softmax 之间有某种联系。确实如此:对于一个只有两个类别的分类问题,如果你对两个 logits 做 softmax,然后取第一个类别的概率,化简到最后你会发现它恰好等于对一个 logit 差值做 sigmoid。
推导大致是这样的:设两个类别的 logits 为z_1和z_2,那么
p_1 = exp(z_1) / (exp(z_1) + exp(z_2))
分子分母同时除以exp(z_1),得到p_1 = 1 / (1 + exp(z_2 - z_1)) = sigmoid(z_1 - z_2)。
这个关系说明一个事实:sigmoid 本质上就是“两个类别的 softmax”。它并不像很多人觉得的那样和 softmax 是平行概念,而是后者在二分类情形下的退化形式。多分类时你只能用 softmax,二分类时你用 sigmoid 还是 softmax,在数学上最终等价,只是 sigmoid 写法更省参数。
4. 输出层为什么要它们:最后一公里决定了网络学不学得动
前面两章分别讲清了 sigmoid 和 softmax 的职责,但还有一个关键问题没回答:为什么输出层“非要”它们?网络前面已经有那么多非线性激活了,输出层直接给原始得分不行吗?
答案是:原始得分不是不能用,但用它会导致训练信号非常难设计。输出层是损失函数和网络之间的唯一通道,这最后一公里如果设计不当,前面的所有特征提取都白搭。
4.1 分类任务的本质:输出层给出的是“可信度分布”,不是“分数”
先想清楚分类任务要什么。手写数字识别,给一张图,网络要判断它是 0 到 9 中的哪一个。最自然的表达方式是:输出一个 10 维向量,每一维代表“属于这一类别的概率”,最终取概率最大的那个类别作为预测结果。
这个“概率”视角很重要。如果输出层直接给得分 [5.2, -1.3, 4.8, ...],我们只能说“第一个类别的得分更高”,但高多少算高?如果两类的得分只差 0.1,网络其实并不确定;如果差了 8,网络就很有把握。得分本身没有语义边界,训练时我们很难定义一个合理的损失函数来利用这种“模糊的自信度”。
softmax 把得分转换成概率后,一切都变得可以度量。五个类别的概率为 0.9 和五个类别概率都接近 0.2,明显代表了截然不同的置信度。这种可度量的不确定性,正是把输出层变成“分布”的核心动机。
4.2 交叉熵的梯度为什么干净:softmax + 交叉熵的误差信号
损失的常用选择是交叉熵。对单个样本,如果它的真实类别是c,预测概率是p_1, ..., p_K,交叉熵就是:
L = -log(p_c)
当网络对正确类别给出的概率是 0.9 时,损失很小;概率只有 0.1 时,损失很大。这和我们对“错误程度”的直觉一致。
现在看最精彩的部分:如果网络的输出层是“全连接层 + softmax”,且损失函数用交叉熵,那么输出层每个神经元关于得分z_i的梯度是多少?最终你会得到一个非常干净的式子:
∂L/∂z_i = p_i - y_i
其中y_i是真实标签的 one-hot 编码:当i等于真实类别时取 1,否则取 0。也就是说,梯度就是“预测概率和真实标签之间的差值”。预测高了,梯度为正,权重被往下拉;预测低了,梯度为负,权重被往上抬。误差信号直接就是预测和真实的偏差,没有任何复杂的系数缠绕。
这个性质是输出层用 softmax 的最大理由。如果你不用 softmax,只拿原始得分配合别的损失,梯度形式会繁琐得多,训练稳定性也会更差。可以说,softmax + 交叉熵是概率输出和梯度传导这对需求共同逼出来的最优解。
4.3 输出层的选型对照:什么时候用 sigmoid、什么时候用 softmax
实际任务里,输出层的选型其实有一套非常明确的规则。下面这张表可以当作速查:
| 任务类型 | 输出层结构 | 损失函数例子 | 说明 |
|---|---|---|---|
| 二分类 | 1 个神经元 + sigmoid | 二分类交叉熵 | 输出代表“属于正类的概率” |
| 单标签多分类 | K 个神经元 + softmax | 多分类交叉熵 | 所有输出概率和为 1 |
| 多标签分类 | K 个神经元,各自独立 sigmoid | 逐个二分类交叉熵之和 | 每个标签独立判断,概率和不为 1 |
| 回归任务 | 不接激活或恒等输出 | 均方误差 MSE | 目标是连续值,不需要概率化 |
重点说下多标签场景。很多人会犯错,以为多标签就是“多个类别的 softmax”,其实完全不是。多标签任务里,一张图可以同时属于“天空”和“白天”,这两个标签并不互斥。如果硬用 softmax,模型会被迫在“天空”和“白天”之间做非此即彼的选择,这会直接破坏标签之间的独立性。正确做法是每个输出神经元各自过 sigmoid,各自判断“这个标签是否成立”。
4.4 回归到 LeNet:它的输出层设计如何体现这些原则
再看 LeNet 的输出端:10 个输出神经元,代表 0 到 9 十个类别,后面接 softmax 得到概率分布,配合多分类交叉熵训练。这是一个典型的“单标签多分类”范式。
你可能会注意一个问题:LeNet 中间层用 sigmoid,输出层用 softmax,那中间层的 sigmoid 会不会和输出层的 softmax 冲突?不会。中间层的 sigmoid 是在“特征空间”里做非线性变换,它的输出不要求概率归一化,只要求增加表达力。输出层的 softmax 是在“决策空间”里做概率化,它只关心 10 个候选者谁更可信。两者服务的层次不同,职责完全不重叠。理解这一点,是彻底搞懂这篇标题的关键。
5. 实际训练里的手感差异:这两个输出函数的坑和技巧
前面讲的都是原理层面的内容。但真正到了训练网络的时候,你会发现 sigmoid 和 softmax 的坑一个接一个。下面这些经验是我在实际调试网络时积累下来的,希望对你有帮助。
5.1 直接在输出层用 sigmoid 会发生什么
有人会想:既然 sigmoid 也是输出 0 到 1 的值,那多分类任务里直接给 10 个输出各自过 sigmoid 行不行?技术上当然可以,但结果会很别扭。10 个 sigmoid 输出每个都在 0 到 1 之间,但它们加起来不等于 1。你怎么解释这些值?它们是概率吗?每个类别独立判断?可分类任务要求“这张图只能属于一个数字”,10 个独立概率加在一起超过 1,模型就无法表达“各候选之间的排他性”。
训练时你还会发现另一个问题:没有 softmax 的概率竞争机制,交叉熵的梯度就不再有p_i - y_i那么干净的形态,输出层的误差信号会变得浑浊。震荡、收敛慢、最终精度上不去,都是可能的后果。所以我的建议很简单:单标签多分类任务,输出层老老实实用 softmax,不要自作聪明。
5.2 logits、温度参数与置信度控制
还有一个容易被忽视的细节:softmax 的输入是 logits,也就是未经激活的原始得分。很多框架里的CrossEntropyLoss会直接吞原始 logits,内部帮你做 softmax。这意味着你在搭建网络时,输出层前面不要自己提前加激活函数,把全连接层的原始输出直接交给损失函数就行。
如果在其他场合需要手工控制概率分布的“锐利程度”,你可以引入温度参数 T,把 softmax 改成softmax(z / T)。T 越小,概率分布越尖锐,最大概率越突出;T 越大,分布越平滑,各类别概率越接近。这个技巧在知识蒸馏里很常用,如果你只做常规分类,记住这个参数的存在即可。我调试时偶尔会用较高的温度来观察模型对不同类别的“犹豫程度”,这比直接看 logits 更直观。
5.3 多标签分类的正确姿势:每个输出各自 sigmoid
上面表格里提到过,这里单独展开。我碰到过一个实际案例:某任务要同时判断图片中是否存在多个物体,一共有 5 个候选物体类别。最初实现的人给 5 个输出接了一层 softmax,结果训练收敛很慢,而且模型总是倾向于只给出一个判断。后来改成 5 个独立的 sigmoid,每个输出配二进制交叉熵损失,效果立刻改善。
原因很简单:softmax 强制五个输出和为 1,这在“多个物体可以同时存在”的任务里是错误先验。sigmoid 则完全没有这种约束,每个标签独立判断,网络可以自由地同时打开多个输出。所以,选 sigmoid 还是 softmax,本质上是在问一个问题:你的任务标签之间是互斥还是独立?互斥用 softmax,独立用 sigmoid。
5.4 一个快速验证的小实验:把 softmax 换成 sigmoid 看损失曲线
如果你想亲自体会两者差异,我建议做一个只需要几分钟的小实验。取一个公开的手写数字识别小数据集,搭建一个最简单的两层网络:输入层、一个隐藏层(比如 64 个神经元,sigmoid 激活)、一个 10 维输出层。
第一次实验:输出层接 softmax,配合交叉熵。观察训练损失的下降曲线,通常很平滑,最终准确率也比较稳定。第二次实验:把输出层换成 10 个独立 sigmoid,配上每个维度的二分类交叉熵(即把多分类强行当成多标签来训练)。你会发现损失下降变慢,最终准确率会略低于 softmax 版本,而且模型的预测表现会出现“多个输出同时高”的怪现象。
这个实验不需要跑很多 epoch,几十个回合就能看出差别。通过亲手对比,你会比背一百遍公式都更清楚为什么输出层非要 softmax 不可。
说回到开头的那个问题:sigmoid 和 softmax 长得像,是因为它们本质上是同一个家族——把实数映射到有界的概率区间。但它们在网络中的角色完全不同:sigmoid 是特征空间里的非线性开关,softmax 是决策空间里的概率投票器。一个是过程工具,一个是终极表达。理解了这层分工,再去看 LeNet 乃至后续的各种分类网络,你会发现输出层的设计其实一直都在沿用一个非常稳定的范式:中间靠非线性激活增强表达,最后靠 softmax 把得分变概率,再配合交叉熵把误差干净地传回去。
我自己在最初上手神经网络时,曾经花了很多时间纠结输出层的函数形式,其实不如多做几次上面那样的替换实验来得快。如果你也在这个阶段,建议找个手写数字识别的小网络,把输出层的激活函数来回换几次,亲眼看看损失曲线的变化,比任何理论解释都有效。