AUC 与 GAUC:从全局排序到用户内排序的理解

张开发
2026/4/6 21:52:27 15 分钟阅读

分享文章

AUC 与 GAUC:从全局排序到用户内排序的理解
一、为什么需要 AUC评估一个推荐模型最直觉的想法是看准确率——预测对了多少条。但准确率有一个致命缺陷它高度依赖正负样本的比例。在推荐场景中用户点击的内容往往只占曝光内容的百分之几样本极度不平衡一个把所有样本都预测为不点击的模型准确率也能高达 95% 以上。这样的指标毫无意义。AUC 的出现解决了这个问题。其概率含义直接而深刻从正样本中随机抽取一个从负样本中随机抽取一个模型给正样本打出更高分数的概率。这与样本比例无关它衡量的是模型的排序能力而非绝对的预测准确性。在数学上AUC 等价于 Wilcoxon-Mann-Whitney 统计量即正确排序的正负样本对数除以全部正负样本对数同分时每对贡献 0.5。二、全局 AUC 的内在假设与局限全局 AUC 的计算方式是把所有用户的所有样本混在一起统一在分数轴上排列然后统计正确排序的正负样本对。这里隐含着一个重要假设——任何一个正样本都可以与任何一个负样本配对无论它们来自谁。这个假设在某些场景下是合理的例如二元分类任务样本之间确实没有归属关系。但在推荐系统中这个假设与业务目标产生了根本性的偏差。推荐系统真正关心的问题是对于某一个具体的用户模型能不能把他感兴趣的内容排在他不感兴趣的内容之前。用户 A 喜欢的内容得分高于用户 B 不喜欢的内容这件事在业务上没有任何意义但全局 AUC 会将其计入正确排序对拉高分数。更严重的问题在于全局 AUC 会被高活跃用户主导。一个日活达数千次的高频用户贡献了大量样本他的排序质量在全局 AUC 中占据极高权重。而大量普通用户每次只有几条曝光他们的排序体验被彻底淹没。模型可能对少数高活跃用户拟合极好却对大多数普通用户表现糟糕但全局 AUC 依然很高给出了虚假的好信号。三、GAUC 的诞生背景GAUCGroup AUC正是为解决上述局限而提出的。它的核心思想极为简洁先把样本按用户分组在每个用户内部独立计算 AUC再将各用户的 AUC 加权平均得到最终的 GAUC。这一改变带来了本质上的不同。正负样本配对的边界从全局收缩到了用户内跨用户的正负对被完全丢弃。GAUC 问的问题从模型对任意正负样本能否正确排序变成了对每一个用户模型能否在他的曝光列表内正确排序。前者是统计意义上的区分能力后者是业务意义上的个性化排序能力。两者貌似相近实则差异可以非常显著。一个典型的场景是某用户的历史行为较为小众模型对他的预测分数整体偏低但用户内部的相对顺序是准确的。全局 AUC 会因为他的正样本分数低于其他用户的负样本而惩罚模型GAUC 则只看他自己内部的排序给出客观评价。GAUC 因此更能反映模型在每个用户视角下的真实排序质量。四、两种加权方式的业务含义GAUC 的加权策略是一个容易被忽视但至关重要的设计选择直接影响指标对齐的业务目标。曝光量加权是最常用的方式每个用户的权重等于其曝光样本数。这意味着曝光多的用户在最终 GAUC 中占据更高比重模型需要在高流量用户身上表现好才能获得高 GAUC。从平台整体收益的角度看这与业务逻辑是对齐的——曝光量大的用户贡献了更多的点击和转化机会他们的排序体验直接影响平台的整体效率指标如点击率、GMV。因此如果业务目标是优化平台总体转化曝光加权 GAUC 是更合适的选择。均匀加权则给每个用户相同的权重不论其曝光量多少。这意味着一个只有 5 条曝光的长尾用户与一个有 500 条曝光的高活跃用户对最终指标的贡献相同。这种加权方式传达的业务信号是每一个用户的体验同等重要。对于强调用户公平性、长尾用户留存或新用户冷启动效果的产品阶段均匀加权 GAUC 是更能反映真实目标的指标。例如在评估冷启动策略时关注的正是那些曝光稀少的新用户能否得到合理的推荐排序曝光加权反而会稀释这部分信号。需要特别指出的是即使使用曝光量加权GAUC 也与全局 AUC 不等价两者之间存在不可消除的差距。原因在于权重只决定了每条样本的投票分量而正负对的配对边界——用户内 vs. 全局——是根本性的结构差异无法通过调整权重来弥合。全局 AUC 包含跨用户的正负对GAUC 永远不包含这是两个指标统计对象的本质不同。五、实践中需要注意的陷阱理解了 GAUC 的原理在工程实现和指标使用中还有几点值得关注。首先是同分Tie的处理。当同一用户内两个样本的预测分数完全相同时不能简单地将其计为正确或错误排序正确做法是贡献 0.5。这个细节在代码实现中容易被忽略导致 GAUC 略微偏高。其次是稀疏用户的过滤。若某用户在一次评估窗口内只有正样本或只有负样本该用户的 AUC 无法定义应当从计算中排除。更进一步单个正负对的用户1 正 1 负计算出的 AUC 只有 0 或 1是极端噪声在数据量足够时建议设置最低样本阈值如至少 2 正 2 负来过滤。最后GAUC 与全局 AUC 描述的是模型的不同侧面二者互为补充。全局 AUC 衡量模型的整体区分能力GAUC 衡量模型在每个用户视角下的个性化排序能力。在 A/B 实验中两者都应关注只看全局 AUC 可能遗漏个性化体验的退步只看 GAUC 可能遗漏整体排序能力的变化。六、小结AUC 与 GAUC 本质上都在回答同一个问题——模型有没有把正样本排在负样本前面——区别在于谁和谁比。全局 AUC 允许跨用户配对GAUC 严格限制在用户内部。这一边界的改变使 GAUC 从一个统计意义上的区分能力指标转变为一个真正对齐推荐系统业务目标的评估工具。而加权策略的选择则进一步决定了 GAUC 对齐的是平台整体效率还是用户个体公平需要结合具体业务阶段做出判断。理解这些层次才能在模型评估时真正用对指标而不是被指标数字表面的好看所迷惑。AUC代码import numpy as np def auc_rank(q_list, label): q_list np.array(q_list) label np.array(label) rank_index np.argsort(q_list) q_list_ranked q_list[rank_index] label_ranked label[rank_index] total_pos np.sum(label_ranked 1) total_neg np.sum(label_ranked 0) l, n 0, len(label_ranked) cum_neg, cum_pos 0, 0 while l n: r l while r n and q_list_ranked[l] q_list_ranked[r]: r 1 group_neg np.sum(label_ranked[l:r] 0) group_pos np.sum(label_ranked[l:r] 1) cum_pos group_pos * cum_neg group_pos * group_neg * 0.5 cum_neg group_neg l r return cum_pos / (total_pos * total_neg) q [0.1, 0.9, 0.2, 0.8, 1, 0.2, 0.3, 0.8] label [0, 1, 0, 0, 1, 0, 0, 1] auc auc_rank(q, label) print(fauc:{auc})GAUC代码import numpy as np from collections import defaultdict def _auc_single_user(user_scores, user_labels): 单用户 AUC处理 tie同分算 0.5 order np.argsort(user_scores) sorted_scores user_scores[order] sorted_labels user_labels[order] n_pos np.sum(sorted_labels 1) n_neg np.sum(sorted_labels 0) cum_neg 0 correct_pairs 0.0 l, n 0, len(sorted_labels) while l n: r l # 找到同分的一组 while r n and sorted_scores[r] sorted_scores[l]: r 1 # [l, r) 为同分组 group_pos np.sum(sorted_labels[l:r] 1) group_neg np.sum(sorted_labels[l:r] 0) # 同分组内正负对算 0.5组前负样本算完全正确 correct_pairs group_pos * cum_neg group_pos * group_neg * 0.5 cum_neg group_neg l r return correct_pairs / (n_pos * n_neg) def gauc_rank(user_ids, labels, scores, weight_typeimpression): # 去掉细节GAUC 的本质只有两步 # GAUC Σ(用户i的AUC × 用户i的权重) / Σ(用户i的权重) user_ids np.asarray(user_ids) labels np.asarray(labels) scores np.asarray(scores) user_sample_dict defaultdict(list) for idx, uid in enumerate(user_ids): user_sample_dict[uid].append(idx) user_auc_dict {} total_weighted_auc 0.0 total_weight 0.0 for uid, indices in user_sample_dict.items(): user_labels labels[indices] user_scores scores[indices] n_pos np.sum(user_labels 1) n_neg np.sum(user_labels 0) if n_pos 0 or n_neg 0: continue user_auc _auc_single_user(user_scores, user_labels) user_auc_dict[uid] user_auc weight len(indices) if weight_type impression else \ 1.0 if weight_type uniform else \ (_ for _ in ()).throw(ValueError(fUnknown weight_type: {weight_type})) total_weighted_auc user_auc * weight total_weight weight gauc total_weighted_auc / total_weight if total_weight 0 else 0.5 print(fgauc: {gauc:.4f}) for uid, auc in user_auc_dict.items(): n len(user_sample_dict[uid]) w n if weight_type impression else 1.0 print(f user{uid} auc{auc:.4f} weight{w}) return gauc, user_auc_dict label [0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0] q [0.1, 0.9, 0.2, 0.8, 1, 0.2, 0.3, 0.9, 0.7, 0.9, 0.7] user_id [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3 ] gauc_rank(user_id, label, q, weight_typeimpression)

更多文章