ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

推荐系统评估指标解析:AUC与GAUC的原理、计算与实践

推荐系统评估指标解析:AUC与GAUC的原理、计算与实践 1. 从一次推荐模型评估说起大概在两三年前我第一次独立负责一个推荐系统的排序模型迭代。当时线上指标涨得不错离线评估用的一直是AUC看起来光鲜亮丽——训练集AUC逼近0.85验证集也有0.82左右。结果上线之后业务方的反馈却很平淡“你们这个模型好像也没什么感觉。”后来复盘的时候一位老哥点了我一句你光看AUC但AUC衡量的是全局排序质量。推荐场景里用户看到的是自己的列表不是所有人的列表。一句话点醒了我——这就是GAUC存在的意义。简单说AUC站在“所有人”的视角看正样本能不能排到负样本前面GAUC则回到“每个用户”的视角先看同一个用户内部模型能不能把他的喜欢排在不喜欢前面再按用户加权平均。一个是全局排序一个是用户内排序两种视角直接决定了评估结果能不能反映线上真实体验。这篇文章想把这两件事彻底讲透。不管你是刚入门推荐系统的新人还是已经跑过不少离线实验的工程师只要你需要评估排序模型AUC和GAUC这笔账一定要算清楚。我会从定义、计算方式、代码实现到踩坑经历把这套东西完整梳理一遍。2. AUC先聊透它到底在衡量什么2.1 AUC不是什么“准确率”很多刚接触排序模型的人会把AUC和准确率混在一起。准确率关心的是“你判对了几成”AUC关心的则是“你把好东西排前面的能力”。一句话理解AUC随机抽一个正样本随机抽一个负样本模型把正样本的分数排在负样本前面的概率。这句话不是辅助理解的口诀而是AUC的数学定义。AUC就是ROC曲线下的面积而ROC曲线的本质就是在不同阈值下把真正例率和假正例率画成一条曲线。面积越大说明正样本的分数分布整体靠前模型排序能力越强。打一个生活化的比方假设你手上有两组卡片红色代表用户会点击的内容蓝色代表不会点击的内容。AUC就是在问——我随机抽一张红卡、一张蓝卡系统能不能大概率把红卡排在蓝卡前面。如果这个概率是0.5说明模型完全瞎猜如果是1.0说明排序完美无误。这个定义非常重要因为它直接引出了AUC的一个特性AUC不依赖具体分数只依赖排序关系。同一个AUC值可以是分数区间0到1得到的也可以是分数区间100到200得到的只要排序不变AUC就不变。这也是为什么AUC能天然屏蔽掉sigmoid输出层的尺度影响。2.2 AUC的计算逻辑与代码实现AUC的计算方式有好几种最常见的包括按照分数排序后统计每个正样本前面有多少负样本算一个比例。用rank sum公式直接计算速度快且等价于Mann-Whitney U统计量。用sklearn的roc_auc_score一步到位。rank sum公式是这样的AUC (sum(rank_pos) - n_pos * (n_pos 1) / 2) / (n_pos * n_neg)其中rank_pos是所有正样本在按分数升序排列后的序号之和。这个公式理解起来稍微绕一点但它揭示了一个核心事实——AUC本质上就是个排序指标跟具体分数值无关。Python代码实现也不复杂这里我直接贴一个不依赖sklearn的版本方便你理解底层逻辑import numpy as np def compute_auc(labels, scores): 手工实现 AUC 计算 :param labels: 0/1标签数组 :param scores: 模型输出分数数组 :return: AUC值 data list(zip(scores, labels)) # 按分数从低到高排序 data.sort(keylambda x: x[0]) rank 0 pos_cnt 0 neg_cnt 0 rank_sum 0 # 处理相同分数的情况取平均rank i 0 n len(data) while i n: j i # 找到分数相同的连续区间 while j n and data[j][0] data[i][0]: j 1 # 计算这个区间的平均rankrank从1开始 avg_rank (i 1 j) / 2 for k in range(i, j): if data[k][1] 1: rank_sum avg_rank pos_cnt 1 else: neg_cnt 1 i j if pos_cnt 0 or neg_cnt 0: return 0.5 auc (rank_sum - pos_cnt * (pos_cnt 1) / 2) / (pos_cnt * neg_cnt) return auc注意代码里处理了同分的情况用平均rank而不是直接按顺序排。这一点在实际数据里很重要因为模型输出的分数经常会出现大量相同值尤其是特征离散化程度比较高的时候rank不加修正会带来偏差。不过实际工作中更多时候还是直接用sklearnfrom sklearn.metrics import roc_auc_score auc roc_auc_score(y_true, y_pred)一行搞定快、稳、经过了无数项目的验证。但是直接用sklearn之前我强烈建议你先理解上面的手工版本。不是说你以后要手写AUC而是只有亲手把排序逻辑捋一遍你才会真正明白AUC的边界在哪——它不在乎用户是谁不在乎场景是什么心里只有“正样本”和“负样本”两种身份。2.3 全局AUC视角下的三个盲区AUC作为全局指标在推荐系统场景下至少有三个明显盲区。我一个个说这些全是实际项目里踩过的坑。第一个盲区跨用户不可比。假设用户A非常活跃一天看了300个视频点了10个用户B很少打开APP看了5个点了4个。AUC在计算时把所有样本混在一起排序相当于默认“用户A的负样本”和“用户B的正样本”之间存在可比性。但实际做推荐的时候这两个样本根本没有出现在同一个列表里说谁排在谁前面毫无意义。更直白地讲同一个分数0.7对用户A可能算高对用户B可能算低因为两个用户的历史行为分布完全不同。第二个盲区个性化被平均掉。AUC对正样本的偏置非常不敏感。什么意思想象一个极端场景模型对用户A的排序很一般AUC只有0.6但用户A样本量巨大模型对用户B排序很好AUC有0.9但样本量很小。全局AUC算出来可能是0.83看起来不错实际上核心用户A的体验并不好。这种“部分用户体验拖垮整体感受”的问题全局视角根本看不出来。第三个盲区样本不平衡的影响。推荐场景正样本通常是“点击”或“转化”占比可能只有百分之几甚至千分之几。AUC对类别不平衡不敏感这是它的优点也是缺点。不敏感意味着正样本再少也能算出稳定的指标但正样本少也意味着AUC主要被负样本的排序质量主导模型可能把大量精力花在了“负样本内部排序”上而这部分对用户体验的影响非常有限。这三个盲区不是AUC本身的错而是使用场景的错配。AUC是为二分类评估设计的推荐排序本质上也是二分类但加了“用户”这个维度之后问题就从“能不能分开正负”变成了“能不能在一个用户内部把正样本顶上去”后者才是推荐系统真正关心的。3. GAUC把用户拉回评估视野3.1 GAUC的定义与直觉GAUC全称Group AUC也叫用户级AUC核心思想一句话先在每个用户内部计算AUC再对所有用户的AUC做加权平均。公式长这样GAUC (Σ_i w_i * AUC_i) / Σ_i w_i其中i是第i个用户AUC_i是用户i内部的AUCw_i是用户i的权重通常用曝光数或点击数。回到之前说的全局视角盲区GAUC的优势就非常清晰了既然跨用户的排序比较没有意义那我就不比较。每个用户只看自己的列表模型有没有把“喜欢的”排在“不喜欢的”前面这个能力才是推荐系统提升体验的关键。权重选择是有讲究的。最常用的是曝光数因为曝光数反映了用户在所有用户中的“体量”比例。你花了很多计算资源给用户A推荐了100个物品他的体验权重自然应该比只看了5个物品的用户B高。也有用点击数的那更偏向“高活用户/高兴趣用户”的视角业务上一般看具体目标来定。我在实际项目里用过曝光数也用过点击数两者GAUC趋势基本一致但曝光数更稳定——因为点击数少的用户内部AUC方差很大加权时会引入噪声。3.2 为什么GAUC更适合推荐排序场景说个具体的例子。假设你有两位用户用户A曝光10个物品点击了2个物品2、物品8实际点击标签是[0,1,0,0,0,0,0,1,0,0]。用户B曝光10个物品点击了5个实际点击标签是[1,1,0,1,0,1,0,1,0,1]。如果模型A给用户A预测的分数能轻松把物品2和物品8排到前面用户A的AUC可能是0.9。模型B给用户B预测的分数可能只有0.65。按曝光加权GAUC两人曝光相同就是(0.9 0.65) / 2 0.775。但全局AUC怎么算20个样本混在一起排序错位可能发生在不同用户之间算出来的AUC数值看起来可能比0.775高也可能低但关键问题在于——哪怕全局AUC很高你都说不清楚它到底是因为“用户A的排序变好了”还是“用户B的正样本恰好排到了用户A的负样本前面”。后者对线上推荐毫无意义因为用户A和用户B看到的是完全不同的两个页面。GAUC的另一个优势是它对样本不平衡更敏感、更“真实”。线上推荐列表里一个用户看到的曝光样本中绝大多数是不会点击的。GAUC直接在用户内部计算天然把这个不平衡因素纳入了评估——它关心的就是“这个用户那么多不点的东西里模型能不能把他唯一点的那一个顶上去”。这个场景和线上用户真实感受完全一致。3.3 GAUC计算的关键细节GAUC实现本身不复杂但有几个细节值得单独拿出来说因为做不好直接影响结果可信度。第一个细节AUC计算需要正负样本同时存在。如果某个用户只有一个正样本其他全是负样本公式可以正常算。但如果用户只有负样本、没有正样本AUC分母为0这个用户常规计算会报错。实际处理方式是跳过该用户不参与GAUC计算。但这里有个权衡——你跳过了大量“未点击用户”只算了“有点击用户的GAUC”加权时要不要把这些用户的曝光也算进分母我在实际项目里的做法是跳过没有正样本的用户只统计有点击用户的曝光权重。这样算出来的GAUC衡量的是“有点击用户群体上的排序质量”而没点击的用户并不存在排序需求——因为他们没有正样本也就谈不上排序好坏。这个口径需要在团队内保持一致否则每次实验对比的基数不同结论就会被污染。第二个细节加权逻辑。曝光数和点击数我都用过两者的差异在用户行为极不均匀时会被放大。比如头部用户贡献了80%的曝光按曝光加权时GAUC几乎完全被头部用户主导。如果你的模型优化目标是所有用户全面体验可能需要考虑截断或平滑如果目标是“把大盘体验做好”那头部用户主导也没什么问题。这里没有标准答案取决于业务目标但你要意识到这个选择的影响。第三个细节样本数量极少的用户AUC方差很大。用户内部只有2个曝光、1个点击AUC要么是0要么是1稳定性很差。用曝光加权时这种用户权重低影响不大。但如果你的数据里大量用户都是低曝光GAUC的方差会上升实验对比时可能看不出置信差异。这时候可以考虑加一个最小曝光阈值比如只统计曝光大于等于5的用户让指标更稳定。4. 完整实验代码与实操演示4.1 准备一份推荐场景模拟数据理论说再多不如跑一遍代码。这里我用Python构造一份模拟的推荐曝光数据包含4个字段用户ID、物品ID、真实标签点击为1、模型预测分数。import pandas as pd import numpy as np np.random.seed(42) users [] items [] labels [] scores [] user_ids [u1, u2, u3, u4] base_click_rates { u1: 0.1, u2: 0.3, u3: 0.05, u4: 0.2 } for uid in user_ids: # 每个用户曝光数不同 n_expo np.random.randint(8, 20) for j in range(n_expo): users.append(uid) items.append(fitem_{uid}_{j}) # 生成真实标签 p base_click_rates[uid] label 1 if np.random.random() p else 0 labels.append(label) # 生成模型分数这里刻意让分数和标签存在正相关 if label 1: score 0.5 0.4 * np.random.random() else: score np.random.random() scores.append(score) df pd.DataFrame({ user_id: users, item_id: items, label: labels, score: scores }) print(df.groupby(user_id)[label].agg([sum, count]))这里构造的思路是不同用户有不同的点击习惯用户内部点击概率不同但同时模型分数和标签之间存在正相关也就是模型有一定的排序能力。这样模拟出的数据比较接近真实推荐场景。4.2 用Pandas实现GAUC计算不依赖任何高级框架直接用Pandas就能把GAUC算出来。思路很简单按用户分组每个组内算AUC再用曝光数加权汇总。from sklearn.metrics import roc_auc_score def compute_gauc(df, uid_coluser_id, label_collabel, score_colscore, weight_colNone): 计算 GAUC :param df: 包含用户ID、标签、分数的DataFrame :param weight_col: 权重列默认None则使用组内样本数作为权重 :return: GAUC值 if weight_col is None: # 默认用样本数作为权重即曝光数 df df.copy() df[_w] 1 weight_col _w user_aucs [] user_weights [] for uid, group in df.groupby(uid_col): # 该用户内部正样本或负样本缺失时跳过 if group[label_col].nunique() 2: continue auc roc_auc_score(group[label_col], group[score_col]) weight group[weight_col].sum() user_aucs.append(auc) user_weights.append(weight) if len(user_aucs) 0: return 0.5 gauc np.average(user_aucs, weightsuser_weights) return gauc # 计算GAUC默认按曝光数加权 gauc compute_gauc(df) print(fGAUC {gauc:.4f})这段代码看着简单但包含了一个非常关键的处理逻辑每个用户组内做nunique() 2的判断剔除掉只有正样本或只有负样本的用户。没有这个判断代码直接报错或者算出一个错误的0.5。实际跑一下这份模拟数据你会发现不同用户的内部AUC差异很大。有的用户可能0.9以上有的只有0.6左右。加权平均之后得到的GAUC会比直接算全局AUC更贴近线上“每个用户感受”的平均水平。4.3 手工实现分组AUC避免过度依赖sklearn有些场景下数据量非常庞大——千万级曝光、百万级用户如果每个用户都调用一次roc_auc_score光是函数调用开销就够受的。而且分组后很多用户组样本量很小没必要走复杂的ROC计算逻辑。这时候可以手工实现一个轻量版分用户AUC。核心公式还是之前的rank sum只是加上了分组计算def compute_auc_by_group(labels, scores): 计算单个用户组内的AUC手工实现避免sklearn开销 n_pos int(np.sum(labels)) n_neg len(labels) - n_pos if n_pos 0 or n_neg 0: return None # 按分数排序返回排序后的索引 order np.argsort(scores) sorted_labels labels[order] # 计算每个样本的rank从1开始 ranks np.empty(len(labels), dtypenp.float64) i 0 n len(labels) while i n: j i while j n and np.isclose(scores[order[j]], scores[order[i]]): j 1 avg_rank (i 1 j) / 2 ranks[order[i:j]] avg_rank i j rank_sum np.sum(ranks[sorted_labels 1]) auc (rank_sum - n_pos * (n_pos 1) / 2) / (n_pos * n_neg) return auc def compute_gauc_fast(df, uid_coluser_id, label_collabel, score_colscore): 高效GAUC计算按用户分组预先转为numpy数组再循环 total_weight 0.0 weighted_sum 0.0 for uid, group in df.groupby(uid_col): labels group[label_col].to_numpy() scores group[score_col].to_numpy() auc compute_auc_by_group(labels, scores) if auc is None: continue weight len(group) weighted_sum auc * weight total_weight weight return weighted_sum / total_weight if total_weight 0 else 0.5这种做法的好处很明显把sklearn的调用开销省掉了同时rank计算逻辑里手动处理了同分问题。千万级数据下性能差距非常明显。不过这版代码是纯Python循环如果你有几十万用户还是建议用Pandas的groupby配合向量化操作或者直接用Spark SQL的窗口函数来做。4.4 GAUC和NDCG的关系聊GAUC很容易带出一个近亲指标NDCG。两者都在强调“位置”但角度不同。GAUC关注的是“用户内正负样本的排序正确率”NDCG关注的是“排序列表里越靠前的位置排到相关内容的收益越大”。GAUC不区分位置权重它只负责判断正样本是否在负样本前面NDCG则通过折损系数让排名第1的收益大于排名第10的收益。实际推荐场景里位置越靠前越重要NDCG其实更贴近业务直觉。但GAUC的优点是计算简单、直观而且和AUC共享同一套统计理论体系团队沟通成本低。我见过不少团队用GAUC作为主指标、NDCG作为辅助指标两者一起看基本能覆盖排序质量的两个维度。4.5 加权方式的进阶处理前面讲了最常用的曝光加权实际工程里还可以做改进。比如用时间的衰减权重一个用户过去的点击和现在的点击重要性应该不同。再比如对曝光次数做对数变换避免头部用户权重过大。我在一个内容推荐项目里用过这样的变体df[weight] np.log1p(df[expo_cnt]) def compute_gauc_custom(df, uid_coluser_id, label_collabel, score_colscore, weight_colweight): user_aucs [] user_weights [] for uid, group in df.groupby(uid_col): if group[label_col].nunique() 2: continue auc roc_auc_score(group[label_col], group[score_col]) # 组内权重可以是聚合值也可以是曝光数 weight group[weight_col].iloc[0] # 假设每个用户的weight是固定的 user_aucs.append(auc) user_weights.append(weight) return np.average(user_aucs, weightsuser_weights) if user_weights else 0.5对数压缩权重会让中尾部用户的声音变大整体GAUC更稳健不会因为头部用户的行为波动而剧烈震荡。但也要注意压缩权重会改变实验灵敏度——如果你希望指标对头部用户的体验变化更敏感就不要压缩具体看业务阶段和优化目标来权衡。5. 实际落地中的典型问题与排查方法5.1 用户内正样本太少AUC不稳定怎么办这是GAUC落地时最常遇到的问题。大多数用户的行为是稀疏的一个用户可能只看过5个内容点过1个。算单个用户AUC时样本量太小随机波动极大。我的建议是加一个用户维度的最小样本过滤像这样def compute_gauc_with_threshold(df, uid_coluser_id, label_collabel, score_colscore, min_expo5, min_pos1): user_aucs [] user_weights [] for uid, group in df.groupby(uid_col): n_expo len(group) n_pos group[label_col].sum() if n_expo min_expo or n_pos min_pos: continue if group[label_col].nunique() 2: continue auc roc_auc_score(group[label_col], group[score_col]) user_aucs.append(auc) user_weights.append(n_expo) return np.average(user_aucs, weightsuser_weights) if user_weights else 0.5min_expo设5、min_pos设1过滤掉极低置信度的用户组GAUC的方差会明显下降。缺点也很明显——过滤掉的用户虽然单个权重低但数量庞大累计起来可能占整体曝光一定比例。所以设置阈值前要统计一下被过滤用户占总曝光的比例如果超过10%就要反思样本构造是否有问题而不是一味调高阈值。5.2 GAUC和线上指标对不上先检查数据口径GAUC和线上AB实验指标对不上的时候十个里有八个是数据口径问题。最常见的是正样本定义不一致。离线GAUC里你把“点击”当正样本线上实验看“点击率”或“人均点击”没问题。但如果线上看的是“转化率”而离线正样本用的是“点击”那GAUC和线上指标天然存在偏置对不上很正常。另一个常见问题是样本范围不一致。离线评估用的是“曝光样本”线上也是曝光口径没问题。但有的团队离线把“仅曝光但未点击”的样本过滤掉了只保留点击和转化样本这样算出来的GAUC不再是排序质量的体现而是“点击后转化概率”的排序质量跟线上点击率指标完全不是一回事。第三个问题是时间窗口。模型预测用的特征都是基于历史行为的离线评估时如果用了未来信息比如用t1时刻的点击量做特征评估t时刻的排序质量GAUC会虚高。这点在构造训练集和测试集时就要严格按时间切分别出现数据泄漏。5.3 GAUC和AUC一起看怎么解读涨跌实际迭代模型时我养成了一个习惯每个实验同时看AUC和GAUC两列一起对比比单看一个指标能看出更多信息。如果AUC涨了GAUC也涨了说明模型在全局排序和用户内排序两个维度上都变好了这是最理想的情况可以直接上线。如果AUC涨了但GAUC没涨甚至跌了说明模型可能学到了“跨用户之间的差异模式”比如学会了识别“高活跃用户容易点击”这种全局规律但对单个用户内部的个性化排序帮助不大。这种模型上线后业务方大概率没感觉因为它做的事情更像“人群分层”而不是“个性化推荐”。如果AUC没涨GAUC涨了那就是典型的“用户内排序提升”模型它可能通过引入用户历史行为序列特征更好地捕捉了个体偏好差异但整体正负样本分布没有显著变化。这种模型值得重点关注因为它往往能在不改变大盘统计指标的情况下显著提升用户体验。反过来如果GAUC跌了但AUC涨了就要警惕模型是不是过拟合了用户群体特征或者训练数据分布。这种模型上线后可能出现“头部用户指标上升、尾部用户指标下降”的割裂现象长期看对平台生态不利。5.4 关于negative sampling的补充推荐场景经常要做负采样来减小训练规模但负采样比例直接影响AUC和GAUC的数值。这里有个常见的坑负采样之后AUC会偏高因为负样本变少了、排序任务变简单了但GAUC受影响的规律不太一样因为它是在用户内计算的如果采样是全局随机采样每个用户内部的负样本密度可能变化不一致。我自己踩过的坑是训练时负采样比例设为1:5离线评估AUC从0.82涨到了0.9但GAUC只涨了一丁点。后来细想AUC的提升主要来自负样本变少后“正样本跨用户排序更靠前”而用户内正负样本的相对关系没变GAUC自然不动。所以如果你要对比不同模型的GAUC最稳妥的做法是统一评估口径用全量曝光样本做评估或者至少保证负采样比例一致。否则模型能力没变光靠采样就能把AUC或者GAUC“做”上去这种指标提升没有实际意义。5.5 常见问题速查表问题可能原因排查方向GAUC比AUC低很多用户间分数分布差异大检查模型是否过度依赖全局统计特征GAUC比AUC高用户内差异明显、跨用户差异小正常说明个性化能力强GAUC波动大用户样本少、权重不均匀增加最小曝光阈值或平滑权重GAUC上涨但线上无提升评估口径和数据泄漏问题检查特征是否用了未来信息AUC上涨但GAUC下跌模型在学人群分层而非个性化引入更多用户行为序列特征直接调用roc_auc_score报错组内只有单一类别过滤nunique() 2的用户组多天GAUC不可比数据量级或用户构成变化统一评估窗口和权重口径这个表基本覆盖了我在实际项目里遇到过的所有典型情况。每一条背后都有一个真实的排障经历不是拍脑袋写出来的。6. 一点个人体会和补充聊到这儿AUC和GAUC的核心内容基本讲完了。最后分享一下我自己做评估指标这件事的体会。我刚接触排序模型那阵子特别喜欢盯着AUC数字看从0.80涨到0.81都能开心半天。后来发现指标和业务感受的背离往往不是因为模型不行而是因为评估视角选错了。AUC回答的是“这套排序在所有样本上是否合理”但产品经理在乎的是“这个用户打开APP之后看到的前几个内容是不是他想看的”。后一个问题恰恰是GAUC的视角。所以现在我做评估方案时第一件事不是选模型而是先想清楚看什么指标。业务目标是提高人均点击那GAUC比AUC优先级高业务目标是提高整体流量分发效率全局排序质量也值得关注。两者不矛盾但主次要分明。另外想补充一点GAUC虽然好但它也不是银弹。它仍然是个离线的、基于历史数据的评估指标无法覆盖超新鲜度、多样性、探索等在线因素。指标只能帮你筛掉明显不行的模型真正的判断还是得上线实验和业务方一起看数据、看用户反馈。如果你正在搭建一套评估体系我的建议是AUC和GAUC都算ADCG也可以加进来但不要只看一个数做决策。把指标组合起来看配合具体的业务场景去解读才能避免“模型指标涨了但线上没感觉”的尴尬。希望这篇内容能帮你把AUC和GAUC这层窗户纸捅破。真正吃透这两个指标之后再看推荐排序模型的评估应该会清晰很多。
返回列表