你好!祝你求职顺利。这段代码非常经典,“手写 AUC”是推荐系统、计算广告以及机器学习算法工程师面试中的绝对高频考点。
面试官让你写这段代码,不仅仅是为了看你会不会写循环,主要是想考察你对 AUC 物理意义的理解以及时间复杂度优化的能力。
下面我结合面试场景,为你通俗易懂地逐行拆解这段代码。
💡 核心前提:AUC 的物理意义是什么?
在讲解代码前,你必须记住一句话(面试时直接这么答):
“AUC 的本质是:随机抽取一个正样本和一个负样本,正样本的预测得分大于负样本预测得分的概率。”
所以,计算 AUC 其实就是在计算:正样本得分 > 负样本得分的“对数”,然后除以总对数。
📝 逐行代码拆解
1. 准备阶段:把连续分数变成“分桶”
def my_auc(labels, preds, n_bins=100000):neg_his = [0]*n_bins # 负样本直方图(桶)pos_his = [0]*n_bins # 正样本直方图(桶)n_pos = 0 # 正样本总数n_neg = 0 # 负样本总数
- 解释:这里使用了直方图法(分桶法)。通常的概率预测值在
0.0到1.0之间。我们将这个区间切分成 100,000 个小桶。pos_his和neg_his就像两排柜子,用来分别记录各个分数段里有多少个正样本和负样本。
2. 装桶阶段:统计各个分数段的样本分布
for i in range(len(labels)):cur = int(preds[i]*n_bins) # 核心:计算当前样本该进哪个桶if labels[i] == 0:neg_his[cur] += 1 # 负样本进对应的负样本桶n_neg += 1else:pos_his[cur] += 1 # 正样本进对应的正样本桶n_pos += 1
- 解释:遍历所有样本。
int(preds[i]*n_bins)的作用是将小数分数映射到整数桶编号上。例如,预测分数为0.8,就会被放进第80000号桶(0.8 * 100000)。通过这个循环,我们统计出了每个极其微小的分数段内,正样本和负样本的数量,同时也得到了正负样本的总数。
3. 计算组合总数(分母)
total = n_neg * n_pos
- 解释:所有的正样本和负样本可以组合成多少对?当然是相乘。这就是我们最终算概率的分母。
4. 核心计算阶段:统计“正样本分数 > 负样本分数”的对数(分子)
sum_neg = 0 # 累计遇到的负样本数量sum_auc = 0 # 累计符合条件的“正>负”样本对数(分子)for i in range(n_bins):# 统计当前桶里的正样本,能和之前/当前的负样本组成多少符合条件的对sum_auc += (sum_neg*pos_his[i] + pos_his[i]*neg_his[i]*0.5)# 将当前桶的负样本数量,加入到累计负样本中,供后面更高分数的正样本使用sum_neg += neg_his[i]
- 解释:这是整段代码最精妙的地方。我们从低分(0号桶)到高分(99999号桶)开始遍历。
sum_neg * pos_his[i]:因为我们是从低分往高分遍历,所以sum_neg代表分数严格比当前桶低的所有负样本总数。当前桶里如果有正样本(pos_his[i]个),那么这些正样本的分数一定大于这sum_neg个负样本。所以构成了sum_neg * pos_his[i]对正确的组合。pos_his[i] * neg_his[i] * 0.5:如果正样本和负样本落在了同一个桶里(分数极其接近),谁大谁小呢?我们近似认为它们互有胜负,也就是有 50% 的概率正样本排在前面,所以算作半对,即乘以0.5。sum_neg += neg_his[i]:处理完当前桶后,把当前桶里的负样本也加到sum_neg里,因为对于下一个更高分数的桶来说,当前桶的负样本也是属于“分数比较低”的。
5. 返回结果
return sum_auc/total
- 解释:将符合条件的对数(分子)除以总对数(分母),得到最终的 AUC 概率值。
🌟 面试加分项(一定要主动和面试官提)
如果面试官问:“为什么要用分桶法来写,而不直接用排序?”你可以这样回答:
- 时间复杂度的跨越:如果是传统的方法(把所有样本按照预测分数排序,然后计算 Rank 值),排序的时间复杂度是 \(O(N \log N)\)。而这种“分桶/直方图法”,无论有多少个样本,都只需要遍历两次数组(一次装桶,一次算分),时间复杂度被硬生生降到了 \(O(N)\)。
- 空间复杂度:由于桶的数量
n_bins是固定的常量(如 100,000),因此空间复杂度是 \(O(1)\)。 - 工业界实用性:在实际的搜推广业务中,测试集动辄几亿、几十亿个请求(\(N\) 极大)。如果是 \(O(N \log N)\) 的排序算法,集群根本跑不动。由于机器预估的分数精度通常也就是几位小数,使用 \(O(N)\) 的分桶法(比如切分10万个桶)能以极低的性能开销得到极其近似的真实 AUC 值,这在工业界非常有价值。
(小提示:截图中第 21 行和第 22 行的测试数据里,labels 是 8 个元素,但 preds 是 9 个元素,两边长度不一致跑起来会报错,你在自己电脑上测试时记得补齐哦。)
