ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

多标签Jaccard优化:从代理损失到凸校准维度的工程解读

2026/9/4 20:20:50 拓冰建站 浏览量
多标签Jaccard优化:从代理损失到凸校准维度的工程解读 很多做过多标签分类的开发者都有一种直觉分类器训练时用交叉熵评估时看 Jaccard两个数字之间经常“对不上”。训练损失一直接近 0线上 Jaccard 却迟迟上不去或者线下调阈值能让 Jaccard 涨 3 个点一换测试集又吐回去。我在读到Exponential Convex Calibration Dimension for the Multi-Label Jaccard Measure这类理论主题时最大的感受是我们缺的不是更复杂的模型而是对“代理损失、决策阈值、评估指标三者之间关系”的系统化理解。本文尽量用较少的公式把多标签 Jaccard 度量、凸校准维度、指数型凸代理损失这些概念串成一条线并给出可运行的 Python 代码。适合正在做多标签分类工程、想读理论论文但需要中文铺垫的读者。读完后你会明白 Jaccard 为什么难优化、代理损失为什么需要“校准”以及从论文标题回到代码层面时应该关注哪些变量。1. 背景与核心概念1.1 多标签分类任务与 Jaccard 度量多标签分类在业务中非常常见。一张商品图可能同时命中“连衣裙”和“夏季新品”一篇文章可能同时属于“科技”和“财经”一个工单可能同时被标记为“网络故障”和“售后投诉”。这些任务的共同特点是每个样本的标签数量不固定标签之间也不互斥。多标签分类的目标是给每个样本预测一个标签集合。评估这种预测集合的好坏Jaccard 是一种非常自然的做法。Jaccard 的计算逻辑来自集合论衡量的是两个集合的交集与并集之间的比例。如果某个样本的真实标签集合是 Y预测标签集合是 Ŷ那么 Jaccard 指标可以写成Jaccard |Y ∩ Ŷ| / |Y ∪ Ŷ|分子是两个集合共有的标签数量分母是“只要出现在其中一个集合里的标签数量”。如果真实标签和预测标签完全相同Jaccard 等于 1如果两者完全没有重合部分Jaccard 等于 0。实际应用中如果两个集合都为空通常会约定 Jaccard 为 1。这个约定会影响后面对代码和损失函数的设计值得单独注意。在多标签论文和竞赛中常见做法是把每个样本的 Jaccard 计算出来再求平均。也就是说所有样本的 Jaccard 分数取均值作为一种整体指标。部分场景也会把“多标签 Jaccard”定义成基于所有样本合并后的混淆矩阵来计算也就是全局 Jaccard。本文不涉及两者之间的争论只讨论按样本平均的情况因为这是最常用、也最能体现单样本决策难度的设置。1.2 一个直观的计算样例看几个具体例子会更容易理解。假设某个多标签数据集一共有 4 个候选标签编号为 1 到 4。真实标签和预测标签分别如下表所示真实标签 Y预测标签 Ŷ交集并集Jaccard{1, 2, 3}{1, 2}230.667{1, 2, 3}{1, 4}140.250{1}{2, 3}030.000{1, 2}{1, 2}221.000空集空集001.000约定其中第四行是最好的完整命中情况。第五行需要特别说明在集合相似度计算中两个空集合的交集和并集都是空集分母为 0无法直接相除。如果某类任务允许“真实标签为空”并且模型也预测为空那么从业务上看这个样本应该得满分否则模型永远不敢预测空集。因此工程上通常把这种情况约定为 1.0。1.3 直接用准确率可能不合适很多初学者会问多标签分类为什么不用准确率准确率虽然直观但它把“大多数标签都预测对了”和“个别标签预测错了”视作完全不同的两类情况。例如真实标签是 {1, 2, 3}模型预测 {1, 2}标签级别的准确率可能达到 2/3 或接近 1取决于是否把负类也计入。问题是这种指标并没有直接回答业务关心的问题预测出来的集合和真实集合有多像。Jaccard 有一个优点它对“预测过多”和“预测过少”都同样敏感。还是以 {1, 2, 3} 为例预测 {1, 2} 会扣分预测 {1, 2, 3, 4} 同样会扣分。因为并集变大了比值就会下降。这种对称惩罚比单纯看召回率或精确率更接近集合级评估的直觉。因此当业务方要求“把要打的标签尽可能完整地打出来同时不要乱打太多无关标签”时Jaccard 通常比准确率更能反映需求。2. 为什么直接优化 Jaccard 行不通2.1 Jaccard 天然是离散的、不光滑的机器学习模型最终输出的不是离散标签而是连续分数。例如一个模型可能输出标签 10.91 标签 20.45 标签 30.30要得到预测集合常见做法是设置一个阈值例如只把分数大于 0.5 的标签选进来。问题在于Jaccard 是在“选进来之后”的离散集合上计算的。这个从连续分数到离散集合的映射存在一个跳变只要某一标签的分数从 0.49 变成 0.51预测集合就变了Jaccard 可能从 0.3 跳到 1.0。这种阶梯状、不可导的指标很难直接用梯度下降优化。理论上你可以把预测集合本身视作一个变量来搜索但候选集合数量是 2^L。如果候选标签 L20就已经有上百万个候选集合如果 L100枚举几乎不可行。所以直接用 Jaccard 作为损失函数很快就会遇到计算和优化双重障碍。2.2 Jaccard 不可分解二分类交叉熵、平方误差这类损失可以拆分成单个标签上的损失之和。也就是说每个标签的梯度可以独立计算互相不干扰。Jaccard 做不到这一点。因为 Jaccard 的分子是集合交集分母是集合并集两者都依赖“整组标签同时被选择”的情况。举个最简单的例子真实标签集合是 {1, 2}。模型把标签 1 的分数排得再高也不能单独决定 Jaccard 大小它还要看标签 2 是否被选上以及是否存在其他被误选进去的标签。如果模型多选了一个标签 3这个额外的误报会让分母变大从而影响标签 1 和标签 2 带来的收益。标签与标签之间“绑定”在一起这正是 Jaccard 不适合简单拆分成多个二分类问题的根本原因。2.3 最优决策规则与“固定阈值”不完全等价有些论文会把多标签问题做得很简单先为每个标签训练一个二分类器再用 0.5 作为阈值。这种做法从工程上能跑通但它隐含一个假设每个标签的校准概率都能直接决定集合内是否包含该标签。现实情况往往不是这样。给定一组标签分数例如标签 A0.80标签 B0.79如果固定阈值取 0.5两个标签都会被选进预测集合。但假设这个样本的真实标签只有一个那么无论选 A 还是 B预测集合都会包含一个假阳性标签。此时最优决策可能是“只取分数最高的那个标签”而不是“把大于阈值的标签全部取出”。这说明在 Jaccard 这种集合级指标下标签之间会竞争有限预算。这也是为什么单纯让每个标签的预测概率更准并不等于 Jaccard 更高。模型需要学习的是整体排序和集合规模之间的配合。这种配合关系很难由固定阈值规则自动完成。3. 凸校准维度与代理损失的关系3.1 代理损失的“校准”是什么既然 Jaccard 不能直接作为损失函数优化研究者通常会设计一个代理损失。这个代理损失是连续的、可微的最好还是凸的然后希望在模型训练完成后代理损失取到较小值的同时真实关心的 Jaccard 也能取得较高分数。但“希望”不等于一定成立。更正式的研究方式是问如果样本无限多、模型能力无限强当模型在某个代理损失上达到全局最优时它是否一定会在真实指标上也达到全局最优如果答案是肯定的就说这个代理损失关于目标指标是校准的也常被称为一致性。用更口语的话说代理损失只是“替身”。替身要可靠不能只是在当前数据集上看起来相关而是要求在理论层面保证最优化二者不会南辕北辙。交叉熵对于普通 0/1 损失具有这种校准性质所以二分类里用交叉熵很少出错。但 Jaccard 这种集合级指标不存在这么简单的替身。3.2 校准维度是个怎样的概念校准维度想衡量的是为了从一个连续评分函数恢复到最优离散预测至少需要引入多少“决策自由度”。二分类问题可以这样理解模型输出一个连续分数 f(x)最优预测是看分数是否为正值。这里的决策自由度很小基本可以看作一个符号判断也就是一维阈值判断。换成 top-K 分类问题时模型输出 L 个分数最优预测通常从分数最高的 K 个标签里产生此时额外多了一个 K 的决策。如果每个样本的最优 K 都固定决策规则依然很简洁但真实数据中每个样本的真实标签数可能不同最优 K 就会随样本变化。多标签 Jaccard 比 top-K 更复杂。Jaccard 的惩罚机制不仅决定“选几个”还决定“哪些不能漏、哪些不能多”。理论上最优预测集合可能并不严格等价于“分数最高的那 K 个标签”因为类别不平衡、标签相关性、阈值位置都会影响最终集合。Jaccard 对应的最优决策规则可能需要更多维度的信息才能描述清楚。凸校准维度这个概念就是为了量化这种“额外自由度”而提出的。需要说明的是这里的“维度”不是指输入特征维度也不是神经网络隐层神经元数量而是决策规则本身的自由度。它关心的是如果代理损失是一个凸函数为了保证代理损失最小化结果能映射回真实 Jaccard 最优结果这个凸代理损失至少需要多复杂的决策空间。3.3 Exponential Convex Calibration Dimension 想表达什么看到Exponential Convex Calibration Dimension for the Multi-Label Jaccard Measure这个标题时可以先不急着把它当作某个 API 或算法名。它更像一条理论研究线索研究者在讨论对于多标签 Jaccard 指标如果使用指数型的凸代理损失那么校准维度到底会是多少以及这种维度是否可以控制在一个可处理的范围。Exponential 通常让人联想到指数函数例如exp(-z)这种形式有几个很好的性质它是凸函数、光滑、处处可导并且对分类错误样本会施加较大的梯度压力。把指数损失作为代理损失在很多二分类模型中被证明可以产生一种提升式的加权训练效果。因此当标题中出现 Exponential 时可以猜测研究者讨论的是这类带指数表达式的凸代理损失。不过我不建议在没看到原文时把 Exponential 的含义写死。它既可能指指数损失函数族也可能指证明过程中出现的指数级校准维度上界。更稳妥的理解是这篇文章的核心问题在于 Jaccard 度量下凸代理损失的整体结构如何决定最优决策的复杂性。本文后续代码不实现完整证明而是帮助建立这种损失的几何和计算直觉。4. 环境准备与基础代码4.1 环境与安装本文示例以 Python 为主需要 numpy。如果你希望直接调用 sklearn 里的 Jaccard 计算函数还需要安装 scikit-learn。安装命令如下pip install numpy scikit-learn版本不需要刻意追求最新。通常来说Python 3.9 及以上、numpy 1.20 以上、scikit-learn 1.0 以上的环境都可以正常运行。如果你用的是旧版本请留意jaccard_score函数里average参数在不同版本中的默认行为可能存在差异。建议代码文件结构如下multi_label_jaccard/ ├── jaccard_utils.py └── demo_sweep.py其中jaccard_utils.py放公共计算函数demo_sweep.py放阈值扫描实验。保持函数独立有利于后续扩展也方便直接复制到实际项目中使用。4.2 单样本 Jaccard 计算函数先从最简单的场景开始一个样本的真实标签和预测标签都用 Python 集合表示。计算 Jaccard 可以直接写成下面这样。# 文件路径jaccard_utils.py def jaccard_on_set(y_true, y_pred): 接收两个标签集合返回单样本 Jaccard 分数。 如果两个集合都为空按惯例返回 1.0。 if not y_true and not y_pred: return 1.0 inter len(y_true y_pred) union len(y_true | y_pred) if union 0: return 1.0 return inter / union if __name__ __main__: print(jaccard_on_set({1, 2, 3}, {1, 2})) # 0.666... print(jaccard_on_set({1, 2, 3}, {4})) # 0.0 print(jaccard_on_set(set(), set())) # 1.0这段代码是整个实验的基础。需要注意两个集合都为空的情况非常特殊不能直接执行除法否则会出现ZeroDivisionError。从业务角度也需要仔细约定空预测命中空真实标签到底应该算 0 还是算 1。大部分文档和论文实现中这种情况下 Jaccard 会被设为 1因为它代表完全一致。4.3 批量预测中的向量化 Jaccard实际建模时标签通常以 0/1 矩阵形式出现。每个样本用长度为 L 的向量表示其中 1 表示该样本包含这个标签。假设有三个样本、六个候选标签真实标签和模型分数如下import numpy as np y_true np.array([ [1, 1, 0, 0, 0, 0], [0, 1, 1, 1, 0, 0], [0, 0, 0, 1, 1, 1], ]) scores np.array([ [0.7, 0.6, 0.5, 0.4, 0.3, 0.2], [0.2, 0.9, 0.8, 0.7, 0.6, 0.1], [0.1, 0.2, 0.3, 0.5, 0.6, 0.9], ])我们可以按 top-K 规则生成预测集合取每个样本分数最高的 K 个标签作为预测集合。这样做的意义是假设模型只负责输出一个可信排序最终集合规模由外部参数 K 控制。代码如下def average_jaccard_at_k(y_true, scores, k): 按 top-K 规则生成预测集合并计算平均 Jaccard。 n_samples, n_labels y_true.shape total_jaccard 0.0 for i in range(n_samples): pred np.zeros(n_labels, dtypeint) if k 0: top_k_indices np.argsort(scores[i])[::-1][:k] pred[top_k_indices] 1 intersection int(np.sum(y_true[i] pred)) union int(np.sum((y_true[i] | pred))) if union 0: total_jaccard 1.0 else: total_jaccard intersection / union return total_jaccard / n_samples for k in range(0, 7): avg average_jaccard_at_k(y_true, scores, k) print(fk{k}, average_jaccard{avg:.4f})在这组手工构造的数据上不同 K 的结果会非常不同。直观的预期是K 很小时模型会漏掉一部分真实标签Jaccard 偏低K 等于标签总数时预测集合会包含很多误报标签Jaccard 同样偏低。因此一定存在某个居中的 K使得平均 Jaccard 最高。你可以把这类结果理解为一个简单结论Jaccard 的好坏不只看排序质量还看集合规模如何选择。4.4 使用 sklearn 快速计算如果你的数据已经是 0/1 矩阵并且想快速和 sklearn 官方实现作对比可以直接使用jaccard_score。这里必须指定averagesamples表示对每个样本先算 Jaccard再取平均。from sklearn.metrics import jaccard_score y_pred np.array([ [1, 1, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1], ]) # averagesamples 表示先算每个样本的 Jaccard再对所有样本取平均 score j