输出层要回答的只有一个问题:这属于哪一类。 回归用 1 个数不激活,二分类用 sigmoid,多分类用 softmax,多标签还是 sigmoid。 softmax 里那三行代码是数值需要,不是数学需要:logits 是 1000、1001、1002 时, 直接算 exp 得到三个 nan,先减最大值才得到 0.09、0.2447、0.6652。 多标签时三个概率加起来能到 2.39——这不是错,因为一张图可以同时是猫和狗。
前四节把「怎么从像素里抽特征」讲完了,这一节讲「怎么把特征变成答案」。 输出层是唯一一处「必须跟着任务走」的地方。 隐层可以无脑用 ReLU,输出层不行——选错就是把模型用错了。 这一节还有一个很实用的知识点:softmax 的数值实现。 它只有三行代码,但少写一行就会在你的模型跑到第几万轮时突然全变成 nan。
一、四种任务,四种输出层
| 任务 | 输出个数 | 最后一层 | 损失 |
|---|---|---|---|
| 回归 | 1 | 不激活 | 平方误差 |
| 二分类 | 1 | sigmoid | 二元交叉熵 |
| 多分类 | 类别数 | softmax | 交叉熵 |
| 多标签 | 类别数 | sigmoid | 各类二元交叉熵之和 |
| 这张表是「配对的」,不能拆开随便组合。 | |||
| softmax 配交叉熵、sigmoid 配二元交叉熵,这两对里的导数都能约掉,梯度干净。 | |||
| 乱配的话梯度里会多一项激活函数的导数,训练会慢很多——第 2.6 节量过,差 51 倍。 | |||
| 判断用 softmax 还是 sigmoid,只问一句:这些类互斥吗? | |||
| 互斥(一张图只能是 0 到 9 中的一个数字)就用 softmax; | |||
| 不互斥(一张图里可以同时有猫和狗)就用 sigmoid。 |
二、softmax 在做什么
它把一组实数变成一组加起来等于 1 的正数。 做法是:先各自取指数,再除以总和。
softmax(z) = exp(z) / sum(exp(z))
取指数的目的是「放大差距」:大的更正、小的被压下去。 除以总和是为了「归一化」:结果可以被当成概率。 实测 logits 是 2.0、1.0、0.1 时,概率是 0.659、0.2424、0.0986,和是 1.000000。 注意它保持大小顺序,但不是「各自独立压缩」——改一个 logit,三类的概率都会变。
三、那三行代码为什么必须有
logits 是 1000、1001、1002 的时候:
| 写法 | 结果 |
|---|---|
| 直接 exp 再归一化 | nan, nan, nan |
| 先减最大值再 exp | 0.09, 0.2447, 0.6652 |
| exp(1000) 是 10 的 434 次方,早超出双精度浮点的上限(约 1.8 乘 10 的 308 次方)。 | |
| 溢出成无穷大,无穷大除以无穷大就是 nan。 | |
| 减去最大值之后,指数最大是 0,最小是负 2,绝不溢出。 | |
| 而「分子分母同时除以同一个数」不改变比值,所以数学上完全等价。 | |
| 这就是所有框架里 softmax 都写成那三行的原因。 | |
| 它不是为了省事,是为了数值稳定——而这个问题只有在你训练到几十万轮、 | |
| logit 涨到几百的时候才会暴露。 |
四、多标签为什么不能用 softmax
三类「各自是不是」,logits 是 2.2、1.4、0.8: sigmoid 之后是 0.9002、0.8022、0.69,加起来 2.3924。 超过 1 是对的。 一张图里同时有猫和狗,两个概率都该高;三个类都命中,加起来接近 3 也合理。 softmax 会强行把它们压成「三选一」,等于告诉模型「这三个里只能有一个对」。 这个假设在互斥分类里是对的,在多标签里是错的。 所以「输出层怎么选」这件事,本质上是在说「我对标签的假设是什么」。
五、交叉熵只看正确那一类
| 正确类的概率 | 损失 |
|---|---|
| 0.9 | 0.1054 |
| 0.6 | 0.5108 |
| 0.3 | 1.2040 |
| 0.1 | 2.3026 |
| 它不看其它类的概率怎么分配。 | |
| 十类里正确类概率 0.9、其余摊 0.1,和正确类 0.9、其余全给第二名,损失是一样的。 | |
| 这是交叉熵的一个「特点」也是一个「缺点」:它不惩罚「把错误的类分得很细」。 | |
| 所以有些任务会额外加「标签平滑」,把目标从 1 改成 0.9,逼模型别太自信。 |
六、动手 Demo:softmax 的三行代码是数值需要,不是数学需要
这段代码先看 softmax 把一组 logits 变成概率,再演示 logits 很大时直接算会溢出; 然后比较多标签场景下 sigmoid 和 softmax 的差别,最后列出交叉熵在不同概率下的值。 重点看两处。第三段 nan 对 0.09、0.2447、0.6652;第四段三个 sigmoid 概率加起来 2.3924。
# -*- coding: utf-8 -*-
import numpy as np
def softmax_naive(S):
E = np.exp(S)
return E / E.sum(axis=-1, keepdims=True)
def softmax_stable(S):
S = S - S.max(axis=-1, keepdims=True)
E = np.exp(S)
return E / E.sum(axis=-1, keepdims=True)
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-np.clip(z, -60.0, 60.0)))
print('[1] 输出层要回答的问题只有一个:这属于哪一类')
print(' 三种任务,三种输出层:')
print(' 回归 1 个数,不激活,损失用平方误差')
print(' 二分类 1 个数过 sigmoid,损失用二元交叉熵')
print(' 多分类 类别数个数过 softmax,损失用交叉熵')
print(' 多标签 类别数个数过 sigmoid(每类独立判断是不是)')
print('')
print('[2] softmax 就一句话:把一组实数变成加起来等于 1 的概率')
S = np.array([2.0, 1.0, 0.1])
P = softmax_stable(S)
print(' logits = %s' % np.round(S, 2).tolist())
print(' 概率 = %s,和 = %.6f' % (np.round(P, 4).tolist(), P.sum()))
print(' 它保持大小顺序:logits 最大的那一类概率也最大')
print(' 但它不是「各自独立压缩」,改一个 logit 会影响所有类的概率')
print('')
print('[3] 直接算 exp 会溢出,所以实现里要先减最大值')
big = np.array([1000.0, 1001.0, 1002.0])
with np.errstate(over='ignore', invalid='ignore'):
naive = softmax_naive(big)
stable = softmax_stable(big)
print(' logits = %s' % big.tolist())
print(' 直接 exp 的结果:%s' % np.round(naive, 4).tolist())
print(' 先减最大值再 exp:%s' % np.round(stable, 4).tolist())
print('')
print(' exp(1000) 早就超出双精度浮点的范围了,除以无穷大得到的是 nan。')
print(' 减去最大值之后指数最大是 0,绝不会溢出;而减掉同一个常数不改变比值。')
print(' 这就是所有框架里 softmax 都写成那三行的原因——不是数学需要,是数值需要。')
print('')
print('[4] 多标签时不能用 softmax')
z = np.array([2.2, 1.4, 0.8])
p_multi = sigmoid(z)
print(' 三类「各自是不是」:sigmoid 之后是 %s,和 = %.4f'
% (np.round(p_multi, 4).tolist(), p_multi.sum()))
print(' 三个都不小,加起来超过 1——这在一张图里同时有猫和狗时是合理的。')
print(' 用 softmax 就会强行把它们压成「三选一」,那是错的。')
print(' 所以选输出层要先问一句:这些类互斥吗?')
print('')
print('[5] 交叉熵只看正确那一类的概率')
for p in (0.9, 0.6, 0.3, 0.1):
print(' 正确类概率 %.1f -> 损失 %.4f' % (p, -np.log(p)))
print('')
print(' 概率 0.9 时损失 0.105,0.6 时 0.511,0.1 时 2.303。')
print(' 它对「很自信但错了」罚得极重,这就是上一章讲过的那个性质。')
print(' 注意它不看其它类的概率怎么分配——只要正确那一类够高,损失就小。')
实跑输出:
--------------------------------------------------------------------
输出层:softmax 的三行代码是数值需要,不是数学需要
--------------------------------------------------------------------
[1] 输出层要回答的问题只有一个:这属于哪一类
三种任务,三种输出层:
回归 1 个数,不激活,损失用平方误差
二分类 1 个数过 sigmoid,损失用二元交叉熵
多分类 类别数个数过 softmax,损失用交叉熵
多标签 类别数个数过 sigmoid(每类独立判断是不是)
[2] softmax 就一句话:把一组实数变成加起来等于 1 的概率
logits = [2.0, 1.0, 0.1]
概率 = [0.659, 0.2424, 0.0986],和 = 1.000000
它保持大小顺序:logits 最大的那一类概率也最大
但它不是「各自独立压缩」,改一个 logit 会影响所有类的概率
[3] 直接算 exp 会溢出,所以实现里要先减最大值
logits = [1000.0, 1001.0, 1002.0]
直接 exp 的结果:[nan, nan, nan]
先减最大值再 exp:[0.09, 0.2447, 0.6652]
exp(1000) 早就超出双精度浮点的范围了,除以无穷大得到的是 nan。
减去最大值之后指数最大是 0,绝不会溢出;而减掉同一个常数不改变比值。
这就是所有框架里 softmax 都写成那三行的原因——不是数学需要,是数值需要。
[4] 多标签时不能用 softmax
三类「各自是不是」:sigmoid 之后是 [0.9002, 0.8022, 0.69],和 = 2.3924
三个都不小,加起来超过 1——这在一张图里同时有猫和狗时是合理的。
用 softmax 就会强行把它们压成「三选一」,那是错的。
所以选输出层要先问一句:这些类互斥吗?
[5] 交叉熵只看正确那一类的概率
正确类概率 0.9 -> 损失 0.1054
正确类概率 0.6 -> 损失 0.5108
正确类概率 0.3 -> 损失 1.2040
正确类概率 0.1 -> 损失 2.3026
概率 0.9 时损失 0.105,0.6 时 0.511,0.1 时 2.303。
它对「很自信但错了」罚得极重,这就是上一章讲过的那个性质。
注意它不看其它类的概率怎么分配——只要正确那一类够高,损失就小。
第二段的 1.000000 是这一节的第一个要点:softmax 的输出一定归一。
它保持大小顺序,所以 argmax 不变——分类时只看谁最大,其实不需要真的归一化。
归一化的价值在于「能被当成概率」,才能算交叉熵、才能做阈值判断。
第三段那三个 nan 是全节最有实用价值的地方。
它说明「数学上等价的写法」在浮点世界里不等价。
你写的 softmax 如果没有减最大值,小模型跑几百轮看不出问题,大模型跑几天才炸。
第四段 2.3924 这个数容易被当成 bug。
它不是——它是「多标签」和「多分类」的区别在数值上的表现。
判断标准是「这些类互斥吗」,不是「加起来是不是 1」。
第五段那张表说明交叉熵只关心正确类。
所以「准确率」和「交叉熵」是两件事:准确率只看 argmax 对不对,交叉熵还看对了多少。
代码里 softmax_naive 是故意留着的反面例子,softmax_stable 才是该用的写法。
np.errstate 那两行只是为了让警告不刷屏——在真实代码里你会想知道它溢出了。
30 秒实验
- 把
big改成 100、101、102,重跑。直接算也能得到正确结果,因为 exp(100) 还在浮点范围内。 所以「减最大值」这一步在 logit 小的时候看不出差别,这正是它容易被漏掉的原因。 - 把
z改成 0、0、0,重跑第四段。三个 sigmoid 都是 0.5,加起来 1.5。 即使 logits 全相等,多标签的三个概率也不归一——这是 sigmoid 的性质。 - 把第五段的概率改成 0.99、0.999,重跑。损失会到 0.01 和 0.001。 但永远不会是 0——因为 -ln(1) 才是 0,而概率到不了 1。
常见误区
- 「softmax 输出的是概率,所以可以做阈值判断」。 它可以当概率用,但多分类里各类是互相竞争的,阈值判断更适合多标签(sigmoid)。 多分类通常直接取最大那一类。
- 「多标签时概率加起来超过 1 是 bug」。 不是 bug。每类独立判断「是不是」,加起来可以是任何值。 实测三个类的 sigmoid 输出和是 2.3924。
- 「softmax 减最大值只是为了效率」。 是为了数值稳定。不减的话 exp 会溢出成无穷大,结果变成 nan。 logits 到几百的时候一定会遇到。
- 「回归问题也可以套 softmax」。 softmax 的输出一定在 0 到 1 之间且归一,回归的目标是任意实数。 用它做回归等于把答案限制在概率单纯形上。
自测三问(点开对答案)
1. 四种任务分别该用什么输出层? —— 回归用 1 个数不激活配平方误差;二分类用 1 个数过 sigmoid 配二元交叉熵; 多分类用类别数过 softmax 配交叉熵;多标签用类别数过 sigmoid,各类独立算二元交叉熵。 选 softmax 还是 sigmoid 只问一句:这些类互斥吗? 2. 为什么 softmax 的实现要先减掉最大值? —— 因为 exp 增长极快,logits 到 1000 时直接算会溢出成无穷大,除以无穷大得到 nan。 减去最大值之后指数最大是 0,绝不溢出;数学上分子分母同时缩放,比值不变。 实测 logits 为 1000、1001、1002 时,不稳定的写法全是 nan。 3. 交叉熵只看正确类的概率,会带来什么问题? —— 它不惩罚「把错误的类分得很细」。正确类 0.9,其余怎么分配损失都一样。 所以模型可能对错误类别给出很自信的排序,却没有任何代价。标签平滑就是用来缓解这个问题的。到这里,卷积网络的每个部件都拆完了。 剩下最后一件事:把这些部件串起来,用梯度真正训练一遍。 卷积核的梯度怎么算?池化的梯度往哪传?下一节把这两件事对账。
- ☐ (本节完,下一节 3.6 神经网络的训练方法)