Softmax 是什么

Softmax封面

你用输入法打字时,敲下拼音 hello,候选栏会弹出几个词:「hello」「哈罗」「喝了」。输入法不是随便排的——它给每个候选算了一个「可能性」,可能性高的排前面。

但模型最初算出来的不是「可能性」,而是一堆原始得分:hello 得 2.0 分,哈罗得 1.0 分,喝了得 0.1 分。这些分数有正有负、大小没上限,看着就头疼——「2.0 分」到底意味着多确信?

Softmax 要解决的就是这件事:把一组任意大小的原始得分,变成一组加起来等于 1 的概率,让你一眼看出模型倾向哪个答案。

为什么需要它:原始得分的三个毛病

神经网络输出层吐出来的一组数(叫 logits,原始得分)有三个毛病,直接拿给用户没法用:

  • 没上下界:可能是 -100,也可能是 1000,你不知道「2.0」算高还是低
  • 互相不可比:第一个数 2.0、第二个 1.0,差不代表第一个的可能性是第二个的两倍
  • 加起来没意义:2.0 + 1.0 + 0.1 = 3.1,这个 3.1 啥也说明不了

我们需要的是一组像概率一样的数:每个都在 0 到 1 之间,且全部加起来正好等于 1(「100% 的可能性分给这几个候选」)。这就是 softmax 干的事。

公式直觉

给定一组原始得分 z1,z2,,zKz_1, z_2, \ldots, z_K(共 KK 个,对应 KK 个候选类别),第 ii 个类别经过 softmax 后的概率是:

softmax(zi)=ezij=1Kezj \text{softmax}(z_i) = \frac{e^{z_i}}{\sum_{j=1}^{K} e^{z_j}}

逐项看这个式子在做什么:

  • ziz_i:第 ii 个类别的原始得分(logit),由网络上一层算出来
  • ee:自然对数的底(约 2.718),指数函数 exe^x 的底数
  • ezie^{z_i}:把 ziz_i 套进指数函数 exe^x。指数函数有个好性质——任何实数进去都出来正数,这就把「有正有负」的得分全变成了正数
  • j=1Kezj\sum_{j=1}^{K} e^{z_j}:所有 KK 个指数化后的得分加起来,当作分母
  • jj:求和指标,遍历 1 到 KK(即依次取每个类别)
  • 整个分式:每个指数得分除以总和,得到一个占比

整个过程像把 KK 个候选的得分「按比例分蛋糕」——先把每个得分都过一遍指数函数(变正、且放大差距),再用总和去归一化,每个候选分到一块,所有块加起来正好一整块。

至于为什么非用指数 exe^x 不可,是因为它有两个好处:① 把负数也变成正数(保证概率非负);② 它增长很快,会放大差距——原本 2.0 和 1.0 只差一倍,指数化后变成 7.39 和 2.72,差快三倍,模型更确信的那个会被进一步突出。

指数化放大得分差距示意图

图 1:指数化如何放大得分差距——同样的 2.0 与 1.0,过一遍 eˣ 后,比例从 2 : 1 被拉大到约 2.7 : 1。

计算示例:三个数字走一遍

就用开头输入法那三个候选的得分 z=[2.0, 1.0, 0.1]z = [2.0,\ 1.0,\ 0.1](hello / 哈罗 / 喝了)实地算一遍:

  1. 指数化(分子):
    • e2.07.389e^{2.0} \approx 7.389
    • e1.02.718e^{1.0} \approx 2.718
    • e0.11.105e^{0.1} \approx 1.105
  2. 求和(分母):7.389+2.718+1.105=11.2127.389 + 2.718 + 1.105 = 11.212
  3. 归一化(每个除以分母):
    • p1=7.389/11.2120.659p_1 = 7.389 / 11.212 \approx 0.659
    • p2=2.718/11.2120.242p_2 = 2.718 / 11.212 \approx 0.242
    • p3=1.105/11.2120.099p_3 = 1.105 / 11.212 \approx 0.099

自检:三个概率 0.659+0.242+0.099=1.0000.659 + 0.242 + 0.099 = 1.000,和为 1,全在 0–1 之间;且原本得分最高的 2.0 对应最大的概率 0.659,顺序没乱。

回头看:模型对第一个候选有约 66% 的把握,第二个约 24%,第三个约 10%——这下人能看懂了。

和 argmax、sigmoid 是什么关系

这三个函数经常被搞混,其实各管一摊:

函数输出用在哪可导吗
softmax一组概率(和为 1)多分类输出层(训练时)
argmax一个序号(最大值的位置)推理时挑最终答案
sigmoid一组概率(每个独立,和不要求为 1)二分类 / 多标签分类

先解释一下表里的「可导」,它决定了谁能用来训练。训练模型靠的是「微调一点点参数,看误差怎么变」,这要求函数平滑、能算变化率(也就是「可导」);softmax 是一条平滑的曲线,满足要求,所以能参与训练。而 argmax 是「只挑最大的、其余一刀切归零」的硬操作,拐角太陡、没法算变化率,所以只在推理时用、不参与训练。

三者的关系:

  • softmax + argmax 是流水线:训练时用 softmax 得到概率,推理(预测)时再在概率上套 argmax,挑出概率最大的那个类别作为最终答案(argmax 直接吃原始得分也行,但概率更直观)。

  • softmax 是 sigmoid 的「多分类升级版」:当只有 2 个类别时,softmax 和 sigmoid 数学上等价;类别一多,sigmoid 会「各算各的」(每个类别独立给一个 0–1 的概率,互相不约束,加起来可能 >1),softmax 则强制它们分一块蛋糕(和为 1)。所以:

    • 互斥的多分类(一张图只能是猫鸟)→ softmax
    • 多标签分类(一张图可以同时有猫狗)→ sigmoid,每个标签独立判断
softmax、sigmoid、argmax 三者对比示意图

图 2:softmax / sigmoid / argmax 一图分清——softmax 强制和为 1(互斥分类)、sigmoid 各算各的(多标签,和可超 1)、argmax 只挑最大者(其余归零)。

典型应用:多分类输出层的「最后一棒」

softmax 最经典的舞台是多分类模型的输出层。以手写数字识别(0–9 共 10 类)为例:

  1. 网络前面几十层把图片特征提取成一串向量
  2. 最后一层是全连接层,输出 10 个原始得分(logits),对应 0–9
  3. softmax 把这 10 个得分转成 10 个概率,每个代表「这张图是该数字的可能性」
  4. 训练时,这组概率再喂给一种叫交叉熵(cross-entropy)的函数算误差、更新模型(交叉熵是另一个概念,专门配合 softmax 用,本文不展开,你只要知道它负责「拿概率和真实标签比,算出误差」即可)
  5. 预测时,argmax 从 10 个概率里挑最大的,作为识别结果

正因为 softmax 和交叉熵总是一起用,PyTorch 干脆把两步合成了一个函数 nn.CrossEntropyLoss——用的时候模型最后一层不需要再加 softmax(这是个常见坑,下面代码里会点明)。

softmax 在多分类输出层的流水线示意图

图 3:softmax 在多分类输出层的位置——把上一层的 logits 转成概率,训练时配合交叉熵算误差,推理时由 argmax 挑出最大概率作为答案。

完整代码

这段代码分三步走:①建一个吐 logits 的小网络 → ②训练一步(用 CrossEntropyLoss 算误差,它内部已含 softmax)→ ③推理时手动套一次 softmax 看概率、用 argmax 出预测。带着这个结构读下面的逐行注释:

python
import torch
import torch.nn as nn

# 一个最小的多分类网络(3 个类别)
class Classifier(nn.Module):
    def __init__(self, in_dim, num_classes):
        super().__init__()
        # 全连接层:输出 num_classes 个 logits(原始得分)
        self.fc = nn.Linear(in_dim, num_classes)

    def forward(self, x):
        # 关键坑:这里【故意不加 softmax】!
        # 因为 nn.CrossEntropyLoss 内部已经先做 softmax 再算交叉熵,
        # 如果这里再加一次 softmax,等于做了两次,模型会学坏。
        return self.fc(x)

torch.manual_seed(42)  # 固定随机种子,让每次跑结果一样(可复现)

model = Classifier(in_dim=4, num_classes=3)
criterion = nn.CrossEntropyLoss()           # 内部自带 softmax + 交叉熵
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

# 假数据:2 个样本,每个 4 维特征
x = torch.randn(2, 4)
# 假标签:样本 0 属于类别 1,样本 1 属于类别 0(类别编号从 0 起)
y = torch.tensor([1, 0])

# —— 第②步:训练一步 ——
logits = model(x)                # 前向:得到原始得分(这步故意不加 softmax)
loss = criterion(logits, y)      # 关键:criterion 内部先 softmax 再算交叉熵,所以上面不能重复加
loss.backward(); optimizer.step()  # 反向传播更新参数(标准训练循环,省略 zero_grad 等样板)
print(f"训练损失: {loss.item():.4f}")

# —— 推理时想看"概率",手动套一次 softmax ——
with torch.no_grad():
    probs = torch.softmax(model(x), dim=1)   # dim=1 表示按行归一化(每行和为 1)
    print(f"每个样本属于各类的概率:\n{probs}")
    print(f"每行求和(应≈1): {probs.sum(dim=1)}")
    preds = probs.argmax(dim=1)              # argmax 挑概率最大的类别作为预测
    print(f"预测类别: {preds.tolist()}")

运行后你会看到:probs 每行的三个数加起来约等于 1,preds 给出每个样本的预测类别编号——这就是 softmax + argmax 的完整工作流。

小结

一句话:softmax 把一组原始得分变成一组加起来为 1 的概率,让模型的输出从「一堆看不懂的数」变成「我有多确信每个候选」——它是多分类任务输出层的标配,和交叉熵是黄金搭档。下次你用输入法,看到候选栏把「hello」稳稳排在最前,背后多半就有 softmax 在把一堆原始得分,悄悄变成那组你看到的概率排序。

参考资料

  1. Softmax Regression — Dive into Deep Learning(李沐等,经典教材) https://d2l.ai/chapter_linear-classification/softmax-regression.html
  2. Softmax Activation Function: Everything You Need to Know — Pinecone(含 softmax/sigmoid/argmax 对比) https://www.pinecone.io/learn/softmax-activation/
  3. Softmax Function Definition — DeepAI Machine Learning Glossary https://deepai.org/machine-learning-glossary-and-terms/softmax-layer