尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CTR校准:推荐系统概率预估失真的数学修正与工程实践

CTR校准:推荐系统概率预估失真的数学修正与工程实践 1. 项目概述为什么CTR校准是推荐系统的“定盘星”在推荐系统这个行当里干了十几年我见过太多团队把模型AUC、线上AB测试的CTR提升当作终极目标吭哧吭哧优化模型结构、引入新特征结果上线后预估的CTR和真实的CTR对不上要么高得离谱要么低得可怜。这就像你用一个不准的秤去称黄金买卖双方都得亏。CTR校准就是给这个“秤”做一次精准的标定确保模型输出的概率值能真实反映用户点击的可能性。这不是锦上添花而是决定推荐系统能否健康、可持续运营的“定盘星”。简单来说CTR校准要解决的核心问题是模型预估的CTRpCTR分布与线上真实观察到的CTR分布不一致。一个未经校准的模型可能因为训练数据分布比如负样本下采样、模型结构如深度模型的复杂度或损失函数如交叉熵的优化目标等原因导致其输出的概率值在数值意义上“失真”。校准的目标就是通过一个后处理映射函数将失真的pCTR调整到与真实CTR一致的水平。这直接关系到后续的排序公平性、出价策略在广告场景、以及资源分配的效率。无论你是做信息流推荐、电商推荐还是广告投放只要你的业务依赖CTR预估做决策校准就是你绕不开的一环。2. 校准的核心原理从“失真”到“保真”的数学映射要理解校准我们得先弄明白模型预估为什么会“失真”。最常见的原因莫过于负样本下采样。在实际业务中曝光未点击的负样本量往往是点击正样本的几十甚至上百倍。为了训练效率我们通常会对负样本进行随机下采样比如只保留10%的负样本。这直接改变了训练数据的先验分布原始数据中正样本的比例先验CTR假设是1%下采样后训练集中的正样本比例可能变成了10%。模型在这个“扭曲”的分布上学习其输出的概率值自然就偏高了。校准的本质就是学习一个映射函数f: p - p其中p是模型原始的预估概率p是校准后的概率。这个函数需要满足对于任意一个校准后的概率值p在真实线上环境中属于该概率区间的样本其真实的点击率应该约等于p。最经典的评估指标是校准曲线Calibration Curve或可靠性图Reliability Diagram将预测概率区间等分如[0, 0.1), [0.1, 0.2), ...计算每个区间内样本的平均预测概率x轴和平均真实标签y轴。一条完美的校准曲线应该是一条45度的对角线。注意校准不改变模型的排序能力即AUC不变。它只调整概率值的绝对大小不改变样本间的相对顺序。一个好的校准方法是在保持模型区分度Discrimination不变的前提下提升其概率预测的准确性Calibration。2.1 理论基础从贝叶斯视角看下采样校准从概率论的角度下采样校准有坚实的贝叶斯基础。设原始空间的正样本先验为π下采样后训练集中的正样本先验为π_s下采样率为α即保留负样本的比例。根据贝叶斯公式可以推导出校准公式p_calibrated (p * α) / (p * α (1 - p))其中p是模型在下采样数据上预估的概率。这个公式就是著名的普拉特缩放Platt Scaling在线性情况下的特例它假设下采样操作是均匀随机的。但在实际中下采样可能不是完全均匀的或者失真的原因不止下采样一种因此我们需要更通用的、数据驱动的方法。3. 主流CTR校准方法全解析与实操对比市面上校准方法很多但归根结底可以分为参数化方法和非参数化方法。参数化方法假设映射函数有特定的形式如线性、S型通过优化少量参数来拟合非参数化方法则更灵活通常基于分箱或平滑技术。3.1 参数化方法简单高效的“标尺”1. 普拉特缩放Platt Scaling这是最经典、应用最广的参数化方法。它假设校准函数是一个逻辑回归Logistic Regression函数p 1 / (1 exp(-(A * logit(p) B)))其中logit(p) log(p / (1 - p))A和B是需要学习的参数。实操要点需要用一个独立的验证集绝不能是训练集来学习参数A和B。这个验证集的数据分布应尽可能与线上测试分布一致。Python实现使用sklearnfrom sklearn.linear_model import LogisticRegression import numpy as np def platt_scaling(predictions, labels): predictions: 模型在验证集上的原始预测概率 (n_samples,) labels: 验证集的真实标签 (n_samples,) 返回: 学习到的校准函数 # 将预测概率转换为logit值为避免log(0)加一个极小值 logits np.log(predictions / (1 - predictions 1e-12)).reshape(-1, 1) lr LogisticRegression(C1e10, solverlbfgs) # 设置很大的C防止正则化影响 lr.fit(logits, labels) # 校准函数: p sigmoid(A * logit(p) B) A lr.coef_[0][0] B lr.intercept_[0] def calibrate(p): logit_p np.log(p / (1 - p 1e-12)) calibrated_logit A * logit_p B return 1 / (1 np.exp(-calibrated_logit)) return calibrate适用场景适用于失真模式相对简单、单调的情况特别是主要由均匀下采样引起的问题。计算量小易于上线。2. 温度缩放Temperature Scaling这是Platt Scaling的一个特例常用于校准神经网络模型。它只学习一个参数T温度p softmax(logits / T)对于二分类可以简化为对sigmoid输出的调整。T 1会使概率分布更平缓降低过高的置信度T 1则会使分布更尖锐。实操心得温度缩放对于校准深度模型的“过度自信”特别有效。我们通常在模型输出logits的验证集上通过最小化负对数似然NLL来优化T。T1表示不校准。3.2 非参数化方法灵活精准的“曲线板”1. 等宽分箱法Isotonic Regression保序回归是一种非常强大的非参数校准方法。它不假设校准函数的具体形式只要求函数是单调递增的。其核心思想是将预测概率排序后找到一条最优的、单调的非递减曲线来拟合真实标签。实操要点将验证集的预测概率p作为输入特征真实标签y作为目标。调用sklearn.isotonic.IsotonicRegression进行拟合。它会自动学习一个分段常数函数。应用时用学习到的transform函数映射新概率。Python实现from sklearn.isotonic import IsotonicRegression def isotonic_calibration(predictions, labels): ir IsotonicRegression(out_of_boundsclip) # clip将超出训练范围的预测值限制在边界 ir.fit(predictions, labels) return ir # 直接返回拟合好的模型用于转换 # 使用 calibrator isotonic_calibration(val_preds, val_labels) calibrated_probs calibrator.transform(test_preds)优势与陷阱优势非常灵活能拟合复杂的失真模式校准效果通常优于参数化方法。陷阱容易过拟合特别是当验证集数据量不足时学到的分段函数可能在线上出现不稳定的跳变。out_of_bounds参数的处理需要谨慎通常选择clip。2. 贝叶斯分箱平均法Bayesian Binning into Quantiles, BBQ这是一种更稳健的分箱方法。它不像等宽分箱那样固定箱体边界而是考虑多种可能的分箱方式并对它们的校准结果进行贝叶斯模型平均从而得到更平滑、更可靠的校准曲线。虽然实现比等宽分箱复杂但在数据噪声大或验证集较小时稳定性更好。3.3 方法对比与选型指南方法原理优点缺点适用场景普拉特缩放学习逻辑回归参数进行线性logit变换简单、高效、不易过拟合、参数少只能拟合单调的S型失真对复杂模式乏力均匀下采样导致的失真需要快速上线温度缩放调整softmax/sigmoid的温度参数T极简单参数专为神经网络设计只能进行全局缩放调整能力有限深度模型输出过度自信/不自信等宽分箱拟合单调的非参数分段常数函数非常灵活能拟合任意单调失真容易过拟合对边界外预测处理敏感验证集充足失真模式复杂未知BBQ贝叶斯模型平均多种分箱结果稳健抗过拟合结果平滑计算复杂实现难度较高数据有噪声验证集规模中等对稳定性要求高选型心法我的经验是从简到繁用数据说话。首先尝试普拉特缩放它是个不错的基线。如果校准曲线显示失真不是简单的S型而是有更复杂的波动再考虑等宽分箱。对于深度模型可以先试温度缩放。永远记住要用一个独立的测试集来评估校准效果而不仅仅是验证集。4. 完整校准流程与Python实战理论说再多不如动手跑一遍。下面我以一个模拟的下采样场景展示从数据准备、模型训练、到校准评估的完整流程。我们会对比校准前后的效果。4.1 步骤一模拟业务数据与下采样import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.calibration import calibration_curve, CalibratedClassifierCV import matplotlib.pyplot as plt # 1. 生成模拟数据 np.random.seed(42) n_samples 100000 # 假设有3个特征 X np.random.randn(n_samples, 3) # 用逻辑函数生成真实概率并加入一些非线性 true_logit 0.5 * X[:, 0] 0.3 * X[:, 1]**2 - 0.4 * X[:, 2] true_proba 1 / (1 np.exp(-true_logit)) # 根据真实概率生成点击标签 y (np.random.rand(n_samples) true_proba).astype(int) print(f原始数据正样本比例: {y.mean():.4f}) # 2. 模拟负样本下采样 (保留10%的负样本) pos_idx np.where(y 1)[0] neg_idx np.where(y 0)[0] downsample_rate 0.1 sampled_neg_idx np.random.choice(neg_idx, sizeint(len(neg_idx) * downsample_rate), replaceFalse) sampled_idx np.concatenate([pos_idx, sampled_neg_idx]) np.random.shuffle(sampled_idx) X_sampled X[sampled_idx] y_sampled y[sampled_idx] print(f下采样后数据正样本比例: {y_sampled.mean():.4f}) print(f下采样后数据量: {len(X_sampled)})4.2 步骤二在下采样数据上训练模型# 划分训练集和验证集用于校准 X_train, X_val, y_train, y_val train_test_split(X_sampled, y_sampled, test_size0.3, random_state42) # 再划分一个测试集保持原始分布用于最终评估 X_full_train, X_test, y_full_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 训练一个简单的逻辑回归模型在下采样数据上 model LogisticRegression(max_iter1000) model.fit(X_train, y_train) # 获取预测概率 train_preds model.predict_proba(X_train)[:, 1] val_preds model.predict_proba(X_val)[:, 1] # 注意测试集预测是用同一个模型但输入是原始分布的X_test test_preds_raw model.predict_proba(X_test)[:, 1]4.3 步骤三应用普拉特缩放进行校准# 使用验证集学习普拉特缩放参数 from sklearn.linear_model import LogisticRegression as LR_Platt def learn_platt_scaler(val_preds, val_labels): 学习并返回一个校准函数 logits np.log(val_preds / (1 - val_preds 1e-12)).reshape(-1, 1) # 使用逻辑回归但这里目标就是验证集标签 lr_platt LR_Platt(C1e10, solverlbfgs, max_iter1000) lr_platt.fit(logits, val_labels) A lr_platt.coef_[0][0] B lr_platt.intercept_[0] print(f学习到的普拉特参数: A{A:.4f}, B{B:.4f}) def calibrate_fn(p): p np.clip(p, 1e-12, 1-1e-12) # 避免数值问题 logit_p np.log(p / (1 - p)) calibrated_logit A * logit_p B return 1 / (1 np.exp(-calibrated_logit)) return calibrate_fn platt_calibrate learn_platt_scaler(val_preds, y_val) test_preds_calibrated platt_calibrate(test_preds_raw)4.4 步骤四评估校准效果def evaluate_calibration(y_true, pred_probs, method_name, n_bins10): 计算并绘制校准曲线和评估指标 fraction_of_positives, mean_predicted_value calibration_curve(y_true, pred_probs, n_binsn_bins) # 绘制校准曲线 plt.plot(mean_predicted_value, fraction_of_positives, s-, labelf{method_name}) # 绘制理想对角线 plt.plot([0, 1], [0, 1], k:, labelPerfectly calibrated) # 计算ECE (Expected Calibration Error) bin_edges np.linspace(0., 1. 1e-8, n_bins 1) bin_indices np.digitize(pred_probs, bin_edges) - 1 bin_indices np.clip(bin_indices, 0, n_bins - 1) ece 0.0 for bin_idx in range(n_bins): mask bin_indices bin_idx if np.sum(mask) 0: bin_probs pred_probs[mask] bin_labels y_true[mask] avg_pred np.mean(bin_probs) avg_true np.mean(bin_labels) ece np.abs(avg_pred - avg_true) * len(bin_probs) ece / len(y_true) # 计算对数损失 from sklearn.metrics import log_loss ll log_loss(y_true, pred_probs) print(f{method_name} - ECE: {ece:.6f}, Log Loss: {ll:.6f}) return ece, ll # 评估校准前后 plt.figure(figsize(8, 8)) plt.subplot(2, 2, 1) ece_raw, ll_raw evaluate_calibration(y_test, test_preds_raw, Raw Predictions) plt.title(Calibration Curve (Raw)) plt.subplot(2, 2, 2) ece_cal, ll_cal evaluate_calibration(y_test, test_preds_calibrated, Platt Calibrated) plt.title(Calibration Curve (Platt Calibrated)) # 绘制概率分布对比 plt.subplot(2, 2, 3) plt.hist(test_preds_raw, bins50, alpha0.7, labelRaw, densityTrue) plt.xlabel(Predicted Probability) plt.ylabel(Density) plt.title(Distribution of Raw Predictions) plt.legend() plt.subplot(2, 2, 4) plt.hist(test_preds_calibrated, bins50, alpha0.7, colororange, labelCalibrated, densityTrue) plt.xlabel(Predicted Probability) plt.ylabel(Density) plt.title(Distribution of Calibrated Predictions) plt.legend() plt.tight_layout() plt.show()运行这段代码你会清晰地看到校准前的预测概率分布集中在高位因为下采样导致模型高估校准曲线严重偏离对角线。而经过普拉特缩放后概率分布被“拉回”到更合理的范围校准曲线也明显更贴近对角线。ECE期望校准误差和Log Loss对数损失通常都会有显著下降。5. 线上部署与持续监控的实战要点校准不是一劳永逸的“离线实验”。模型会迭代数据分布会漂移概念漂移校准函数也必须跟着变。5.1 校准模型的更新策略定期更新建立校准模型的定期重训 pipeline例如每天或每周使用最近一段时间如过去7天的线上曝光-点击日志作为验证集重新学习校准参数。触发式更新监控校准指标如每日ECE。当指标恶化超过一定阈值例如ECE上升50%时自动触发校准模型的重训。AB测试框架集成在新的模型版本上线进行AB测试时必须为其单独训练校准函数。切勿使用旧模型的校准器因为不同模型结构的失真特性可能完全不同。5.2 校准模块的线上部署校准时一个简单的函数调用但部署时有讲究位置校准应作为模型服务Model Serving的一部分在模型推理后立即执行。确保线上服务的校准函数版本与离线评估时使用的完全一致。性能普拉特缩放几个浮点运算和等宽分箱查表法开销极低通常不会成为性能瓶颈。对于分箱法可以将分箱边界和映射值预加载到内存中实现O(1)复杂度的查找。代码一致性强烈建议将校准函数的训练和应用代码封装成统一的库确保离线训练和线上服务使用同一套代码避免因实现差异引入bug。5.3 监控与报警设计你需要为校准建立专门的监控面板核心指标每日/实时ECE直接衡量校准好坏。校准曲线对比图每天将线上观测到的校准曲线与理想对角线、历史曲线对比直观发现漂移。预测概率分布监控pCTR分布的均值、分位数如90分位是否有剧烈波动。报警规则ECE连续N小时超过阈值。校准曲线在关键概率区间如0.3-0.7业务决策敏感区偏离历史基线过大。pCTR均值发生突变可能由校准函数失效或数据管道问题导致。6. 避坑指南校准实践中常见的“雷区”踩过不少坑这里总结几个最容易出问题的地方坑1使用训练集进行校准这是最致命的错误。校准的目的是纠正模型在“未知数据”上的偏差你必须使用模型未见过的、独立同分布的验证集来学习校准函数。用训练集校准等于让考试题目和复习资料一模一样完全失去了评估意义。坑2忽略数据分布的一致性校准验证集的数据分布必须与线上服务时模型面对的数据分布一致。例如如果你用白天数据训练的校准器应用到夜间流量上效果可能很差。如果线上有多个流量通道如主feed、相关推荐它们的CTR先验可能不同需要考虑为每个通道单独校准或引入通道特征。坑3校准器过拟合尤其是使用等宽分箱这类非参数方法时如果验证集数据量小学到的分段函数会非常“崎岖”在线上遇到验证集未覆盖的概率值时映射可能不稳定。解决方法使用更大的验证集、采用贝叶斯平均如BBQ、或者在分箱后对映射值进行平滑处理如滑动平均。坑4校准后AUC下降理论上单调校准不应改变AUC。如果AUC下降了说明你的校准过程可能不是单调的或者在校准过程中意外地使用了标签信息对样本进行了重排序。请检查你的校准实现确保映射函数是单调递增的并且校准过程只改变概率值不改变样本ID的顺序。坑5忘记校准截距项在广告等需要基于pCTR进行出价的场景校准不仅要保证曲线形状正确还要保证概率的绝对数值准确。例如如果你的模型整体高估了2倍那么即使校准曲线是完美的对角线你的出价也会是实际价值的两倍。这时你需要关注校准函数在p0.5这样的点上的输出是否合理。有时需要结合业务先验CTR对校准函数进行微调。校准是推荐系统从“实验室精度”走向“商业实用”的关键一步。它不像搞一个花哨的模型结构那样引人注目但却是确保系统稳定、公平、可信的基石。每次模型迭代都别忘了问一句“这次你校准了吗”
返回列表