Softmax在YOLOv8中只做一件事——把16个Bin的分数变成概率
一句话总结:Softmax 在 YOLOv8 中只用于 DFL 解码——把 16 个 bin 的原始分数变成概率分布(加起来 = 1),然后才能加权平均算出坐标。分类分支用的是 Sigmoid,不是 Softmax。
目录
- 16-Softmax在YOLOv8中只做一件事——把16个Bin的分数变成概率
- 目录
- Softmax 在哪出场
- 为什么需要 Softmax——原始分数不能当概率用
- 代码演示:16 个 bin 的完整 Softmax 过程
- 可视化:Softmax 前后对比
- Softmax 的两个关键特性
- 特性 1:赢家通吃
- 特性 2:概率总和 = 1
- 为什么分类分支用 Sigmoid 而不是 Softmax
- 自测
- 一句话记住
Softmax 在哪出场
YOLOv8 内部用到 Softmax 的只有一处——DFL 解码链:
DFL 解码链:
16 个 bin 的原始分数 Softmax 后(16 个概率)
[2.0, 5.0, 1.0, 0.5, …] → [0.046, 0.927, 0.017, 0.010, …]
↑ ↑
正负无穷都行 加起来 = 1.0
↓
加权平均(Conv(dfl))
↓
偏移量 → Sub → Div → 像素坐标
分类分支(80 个类别)用的是 Sigmoid,不是 Softmax。
为什么需要 Softmax——原始分数不能当概率用
模型输出的 16 个 bin 值是原始分数,没有范围限制:
原始分数(16 个 bin):
[2.0, 5.0, 1.0, 0.5, 0.2, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
加起来 = 8.8 ← 不是 100%,不能叫"投票"
Softmax 的任务就是把"打分"变成"投票"——让 16 个值变成合法的概率分布。
代码演示:16 个 bin 的完整 Softmax 过程
import numpy as np
import matplotlib.pyplot as plt
# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# ============================================================
# 第1步:给定 16 个 bin 的原始分数(模拟 YOLOv8 的 DFL 输出)
# ============================================================
# 这是一组典型的 DFL 输出:bin₁ 的分数最高,说明偏移量大概率在 0.5 附近
raw_scores = np.array([2.0, 5.0, 1.0, 0.5, 0.2, 0.1,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0])
print("=" * 60)
print("第1步:原始分数(16 个 bin)")
print("=" * 60)
for i, s in enumerate(raw_scores):
bar = "█" * int(s * 5)
print(f" bin_{i:2d} (偏移 {i*0.5:.1f}): {s:6.2f} {bar}")
# ============================================================
# 第2步:Softmax 计算
# ============================================================
def softmax(x):
"""Softmax 函数:e^x / sum(e^x)"""
e_x = np.exp(x – np.max(x)) # 减去最大值防止数值溢出
return e_x / e_x.sum()
probs = softmax(raw_scores)
print("\\n" + "=" * 60)
print("第2步:Softmax 后(16 个概率)")
print("=" * 60)
print(f" 概率总和: {probs.sum():.6f} ← 验证:加起来必须等于 1")
print()
for i, p in enumerate(probs):
bar = "█" * int(p * 100)
print(f" bin_{i:2d} (偏移 {i*0.5:.1f}): {p:.4f} ({p*100:5.1f}%) {bar}")
# ============================================================
# 第3步:加权平均 → 偏移量
# ============================================================
# Conv(dfl) 的权重 = [0, 1, 2, …, 15]
dfl_weights = np.arange(16, dtype=np.float32)
offset = np.dot(probs, dfl_weights)
print("\\n" + "=" * 60)
print("第3步:加权平均 → 偏移量")
print("=" * 60)
print(f" 权重: [0, 1, 2, …, 15]")
print(f" 计算: sum(prob_i × i) for i=0..15")
print(f" 偏移量 = {offset:.4f} 个特征图像素")
print(f" 含义: 框的边界离格子左上角约 {offset:.2f} 个特征图像素")
# ============================================================
# 第4步:对比——如果不用 Softmax 直接加权平均
# ============================================================
# 如果直接用原始分数加权平均(不归一化)
raw_offset = np.dot(raw_scores, dfl_weights) / raw_scores.sum()
print(f"\\n 如果不用 Softmax(直接加权平均): {raw_offset:.4f}")
print(f" 用 Softmax(正确做法): {offset:.4f}")
print(f" ← Softmax 保证了概率的合法性")
# ============================================================
# 可视化
# ============================================================
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 图1:原始分数
ax1 = axes[0, 0]
colors_raw = ['#d4e6f1' if s < 1 else '#85c1e9' if s < 3 else '#2e86c1' for s in raw_scores]
bars1 = ax1.bar(range(16), raw_scores, color=colors_raw, edgecolor='white', linewidth=0.5)
ax1.set_title('原始分数(Softmax 前)', fontsize=14, fontweight='bold')
ax1.set_xlabel('Bin 编号')
ax1.set_ylabel('原始分数')
ax1.set_xticks(range(16))
ax1.set_xticklabels([f'{i}\\n({i*0.5})' for i in range(16)])
ax1.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
for i, (bar, s) in enumerate(zip(bars1, raw_scores)):
if s > 0.5:
ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1,
f'{s:.1f}', ha='center', va='bottom', fontsize=8, fontweight='bold')
ax1.set_ylim(0, max(raw_scores) * 1.2)
# 图2:概率分布(Softmax 后)
ax2 = axes[0, 1]
colors_prob = ['#fadbd8' if p < 0.05 else '#f1948a' if p < 0.3 else '#e74c3c' for p in probs]
bars2 = ax2.bar(range(16), probs, color=colors_prob, edgecolor='white', linewidth=0.5)
ax2.set_title('概率分布(Softmax 后)', fontsize=14, fontweight='bold')
ax2.set_xlabel('Bin 编号')
ax2.set_ylabel('概率')
ax2.set_xticks(range(16))
ax2.set_xticklabels([f'{i}\\n({i*0.5})' for i in range(16)])
for i, (bar, p) in enumerate(zip(bars2, probs)):
if p > 0.01:
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.005,
f'{p:.3f}', ha='center', va='bottom', fontsize=8, fontweight='bold')
ax2.set_ylim(0, max(probs) * 1.2)
# 图3:加权平均过程
ax3 = axes[1, 0]
bin_offsets = np.arange(16) * 0.5 # 每个 bin 对应的实际偏移量
ax3.bar(bin_offsets, probs, width=0.4, color='#e74c3c', alpha=0.7, edgecolor='white', label='概率')
ax3.axvline(x=offset * 0.5, color='#2e86c1', linewidth=3, linestyle='–',
label=f'加权平均 = {offset*0.5:.2f} 特征图像素')
ax3.set_title('加权平均——框偏移量的由来', fontsize=14, fontweight='bold')
ax3.set_xlabel('偏移量(特征图像素)')
ax3.set_ylabel('概率')
ax3.legend(fontsize=10)
ax3.grid(axis='y', alpha=0.3)
# 图4:对比 Sigmoid vs Softmax
ax4 = axes[1, 1]
# 模拟分类分支的 6 个类别用 Sigmoid
cls_scores = np.array([9.5, 0.1, 0.2, 0.3, 0.0, 0.1])
cls_sigmoid = 1 / (1 + np.exp(–cls_scores))
x_pos = np.arange(6)
ax4.bar(x_pos, cls_sigmoid, color='#27ae60', alpha=0.7, edgecolor='white', label='Sigmoid(分类)')
ax4.axhline(y=1.0, color='gray', linestyle=':', alpha=0.5)
ax4.set_title('Sigmoid 用于分类分支(80 个类别)', fontsize=14, fontweight='bold')
ax4.set_xlabel('类别')
ax4.set_ylabel('置信度')
ax4.set_xticks(x_pos)
ax4.set_xticklabels(['person', 'bicycle', 'car', 'cat', 'dog', '…'])
ax4.set_ylim(0, 1.1)
for i, (bar, v) in enumerate(zip(ax4.patches, cls_sigmoid)):
ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
f'{v:.3f}', ha='center', va='bottom', fontsize=9, fontweight='bold')
ax4.legend(fontsize=10)
plt.suptitle('Softmax 在 YOLOv8 中的作用——DFL 解码链', fontsize=16, fontweight='bold', y=1.01)
plt.tight_layout()
plt.savefig('softmax_in_yolov8.png', dpi=150, bbox_inches='tight')
plt.show()
print("\\n" + "=" * 60)
print("总结")
print("=" * 60)
print(f" 原始分数 (bin₁): 5.0 → Softmax → 概率: {probs[1]:.4f} ({probs[1]*100:.1f}%)")
print(f" 原始分数 (bin₀): 2.0 → Softmax → 概率: {probs[0]:.4f} ({probs[0]*100:.1f}%)")
print(f" 偏移量 = {offset:.4f} 特征图像素")
print(f" 原图坐标 = {offset:.4f} × stride")

运行结果:
============================================================
第1步:原始分数(16 个 bin)
============================================================
bin_ 0 (偏移 0.0): 2.00 ██████████
bin_ 1 (偏移 0.5): 5.00 █████████████████████████
bin_ 2 (偏移 1.0): 1.00 █████
bin_ 3 (偏移 1.5): 0.50 ██
bin_ 4 (偏移 2.0): 0.20 █
bin_ 5 (偏移 2.5): 0.10
bin_ 6 (偏移 3.0): 0.00
bin_ 7 (偏移 3.5): 0.00
bin_ 8 (偏移 4.0): 0.00
bin_ 9 (偏移 4.5): 0.00
bin_10 (偏移 5.0): 0.00
bin_11 (偏移 5.5): 0.00
bin_12 (偏移 6.0): 0.00
bin_13 (偏移 6.5): 0.00
bin_14 (偏移 7.0): 0.00
bin_15 (偏移 7.5): 0.00
============================================================
第2步:Softmax 后(16 个概率)
============================================================
概率总和: 1.000000 ← 验证:加起来必须等于 1
bin_ 0 (偏移 0.0): 0.0428 ( 4.3%) ████
bin_ 1 (偏移 0.5): 0.8604 ( 86.0%) ██████████████████████████████████████████████████████████████████████████████████████
bin_ 2 (偏移 1.0): 0.0158 ( 1.6%) █
bin_ 3 (偏移 1.5): 0.0096 ( 1.0%)
bin_ 4 (偏移 2.0): 0.0071 ( 0.7%)
bin_ 5 (偏移 2.5): 0.0064 ( 0.6%)
bin_ 6 (偏移 3.0): 0.0058 ( 0.6%)
bin_ 7 (偏移 3.5): 0.0058 ( 0.6%)
bin_ 8 (偏移 4.0): 0.0058 ( 0.6%)
bin_ 9 (偏移 4.5): 0.0058 ( 0.6%)
bin_10 (偏移 5.0): 0.0058 ( 0.6%)
bin_11 (偏移 5.5): 0.0058 ( 0.6%)
bin_12 (偏移 6.0): 0.0058 ( 0.6%)
bin_13 (偏移 6.5): 0.0058 ( 0.6%)
bin_14 (偏移 7.0): 0.0058 ( 0.6%)
bin_15 (偏移 7.5): 0.0058 ( 0.6%)
============================================================
第3步:加权平均 → 偏移量
============================================================
权重: [0, 1, 2, …, 15]
计算: sum(prob_i × i) for i=0..15
偏移量 = 1.5896 个特征图像素
含义: 框的边界离格子左上角约 1.59 个特征图像素
如果不用 Softmax(直接加权平均): 1.1136
用 Softmax(正确做法): 1.5896
← Softmax 保证了概率的合法性
可视化:Softmax 前后对比
运行代码后生成 4 张图:
左上图:原始分数——bin₁ 一枝独秀(5.0),其他 bin 也有一定分数。
右上图:Softmax 后的概率——bin₁ 占 92.7%,其他 bin 被压到很低。e^x 的放大效应让最大值"赢家通吃"。
左下图:加权平均过程——每个 bin 的概率(红色柱)乘以对应的偏移量,加权平均线(蓝色虚线)就是最终偏移量。
右下图:分类分支用 Sigmoid——每个类别独立算置信度,不互斥(person 0.999 的同时 cat 也可以 0.574)。
Softmax 的两个关键特性
特性 1:赢家通吃
Softmax 的 e^x 会让大的值更大、小的值更小:
原始分数: [2.0, 5.0, 1.0, 0.5]
e^2.0 e^5.0 e^1.0 e^0.5
=7.39 =148.4 =2.72 =1.65
Softmax: [0.046, 0.927, 0.017, 0.010]
↑ ↑
被压低了 被放大了
特性 2:概率总和 = 1
Softmax 保证了 16 个 bin 的概率加起来等于 1,这是概率分布的基本要求。只有这样,加权平均才有数学意义。
为什么分类分支用 Sigmoid 而不是 Softmax
| 激活函数 | Softmax | Sigmoid |
| 通道数 | 16 个 bin | 80 个类别 |
| 互斥性 | 互斥(偏移量只有 1 个正确答案) | 不互斥(一只猫可以同时是 cat 和 animal) |
| 总和 | = 1 | 各自独立,不要求和 |
分类分支(Sigmoid):
person: 0.9997 ← 可以同时很高
cat: 0.5743 ← 不互斥
dog: 0.0201
DFL 分支(Softmax):
bin₀: 0.0462
bin₁: 0.9267 ← 互斥,加起来 = 1
bin₂: 0.0170
bin₃: 0.0103
自测
Q1:Softmax 在 YOLOv8 中出现在哪?
点击查看答案
只出现在 DFL 解码链中。分类分支用的是 Sigmoid。
Q2:为什么 DFL 需要 Softmax?
点击查看答案
模型的原始输出是任意范围的分数,不能直接加权平均。Softmax 把分数变成概率分布(加起来 = 1),然后每个 bin 的概率乘以权重求和,得到合法的加权平均值。
Q3:Softmax 的 e^x 会导致什么现象?
点击查看答案
“赢家通吃”——大的值被放大,小的值被压低。比如原始分数 [2.0, 5.0],Softmax 后变成 [0.046, 0.927],差距从 3 倍变成了 20 倍。
Q4:分类分支为什么不用 Softmax?
点击查看答案
分类分支的 80 个类别不互斥——一只猫可以同时是 cat 和 animal。Softmax 强制总和为 1,会迫使模型"选一个",不适合多标签场景。Sigmoid 每个类别独立输出 0~1,允许多个类别同时为高。
一句话记住
Softmax 把 16 个 bin 的原始分数变成概率分布(加起来 = 1),让加权平均有数学意义。e^x 的放大效应让高分 bin 赢家通吃,最终偏移量由最高分的 bin 主导。
