☰
排序模型实战:从GBDT+LR到深度学习的设计与优化
2026/9/28 17:44:24 网站建设 项目流程

1. 排序模型到底在解决什么问题

排序模型这四个字,听起来像是推荐系统或者搜索引擎的专属名词,但实际上它的应用范围远比大多数人想象的宽。电商里搜索"运动鞋"之后那一列结果怎么排、短视频信息流里下一条推什么、招聘平台上简历和岗位怎么匹配、甚至外卖App里商家列表的先后顺序,背后都是排序模型在干活。说白了,排序模型的核心任务就一句话:给定一个用户和一个候选集合,把最可能让用户满意的那些item挑出来,并且按满意度从高到低排好序。

这件事看起来简单,做起来极其复杂。因为"满意度"这个东西没法直接测量,你只能通过用户的行为去间接推断——点击了、停留了多久、收藏了、下单了、划走了、举报了,每一种行为都携带不同的信号强度。排序模型要做的就是把这些信号整合起来,对每一个候选item打出一个分数,然后按分数排列。这个分数不需要有绝对意义,只要保证相对顺序正确就行,这也是排序模型和回归模型、分类模型在目标上的本质区别。

我刚开始接触排序模型的时候,最容易犯的一个错误就是把它当成一个普通的二分类问题来做——预测用户会不会点击。这么做不是不行,但效果往往差强人意。原因在于,排序是一个相对问题,用户面对的是一屏结果,他是在几个候选之间做选择,而不是独立判断每个item的好坏。你单独预测每个item的点击率,然后按点击率排序,理论上可行,但实际中会遇到位置偏差、样本选择偏差等一系列问题。所以后来才有了pairwise、listwise这些专门为排序设计的损失函数和训练方式。

这篇文章适合谁看?如果你正在做推荐系统、搜索、广告相关的项目,或者你在准备机器学习相关的面试,再或者你只是好奇"为什么我搜出来的东西顺序是这样的",那这篇内容应该能给你一些实在的参考。我会从整体设计思路讲到具体实现细节,再把我自己踩过的坑和排查问题的经验都倒出来,尽量让你看完就能上手。

2. 排序模型的整体设计思路与方案选型

2.1 从业务目标倒推技术方案

做排序模型最忌讳的一件事就是上来就选模型、调参数,而不去想业务目标是什么。不同的业务目标会导致完全不同的技术选型。比如电商搜索的排序,核心指标可能是GMV(成交额),那你在设计模型的时候就要把转化率、客单价这些因素考虑进去;而内容信息流的排序,核心指标可能是用户停留时长或者互动率,那模型优化的方向就完全不一样。

我一般会先问三个问题:第一,这个排序场景下用户的核心行为是什么?是点击、购买、还是停留?第二,不同行为之间的优先级怎么定?比如点击和购买,显然购买更重要,但点击数据量远大于购买数据,怎么平衡?第三,有没有硬性约束?比如某些内容必须置顶、某些商品不能出现在前几位,这些约束必须在排序之后单独处理,不能指望模型自己学会。

这三个问题想清楚了,你才能决定用什么样的模型结构、什么样的损失函数、什么样的特征体系。我见过太多团队一上来就搞深度学习模型,结果发现业务目标都没定义清楚,最后模型效果没法评估,项目直接烂尾。

2.2 排序模型的三代演进路线

从技术演进的视角看,排序模型大致经历了三个阶段,每个阶段解决的核心问题不一样。

第一代是线性模型时代,代表是LR(逻辑回归)加上大量的手工特征交叉。这个阶段的核心思路是:既然线性模型学不会特征之间的交互,那我就人工把交叉特征构造出来。比如"用户性别=女 且 商品类目=美妆"这个组合特征,人工构造好之后喂给LR。这种做法可解释性强,工程上也好维护,但问题也很明显——特征工程的工作量巨大,而且很多高阶交叉根本没法穷举。

第二代是树模型和因子分解机时代,代表是GBDT+LR、FM、FFM。GBDT负责自动做特征交叉和离散化,LR负责最终的融合。FM和FFM则是通过隐向量的方式来自动学习二阶交叉特征,大大减少了手工工作量。这个阶段我在实际项目里用得最多的是GBDT+LR的组合,效果稳定,训练速度也快,特别适合中小规模的排序场景。

第三代是深度学习时代,代表是Wide&Deep、DeepFM、DIN、DIEN这些模型。深度学习最大的优势是能端到端地学习高阶特征交叉,而且可以很方便地引入序列特征、注意力机制。但代价是模型复杂度高、训练成本大、线上 serving 的延迟也更高。我的经验是,如果你的候选集规模在几百到几千,QPS要求不是特别高,深度学习模型值得一试;但如果候选集上万、QPS要求几千甚至上万,那还是得在模型复杂度和推理速度之间做权衡。

2.3 为什么我推荐从GBDT+LR起步

虽然现在深度学习排序模型很火,但我依然建议刚接触排序模型的人从GBDT+LR开始。原因有三:第一,这个方案的工程链路清晰,从特征工程到模型训练到线上部署,每个环节你都能看得见摸得着,不像深度学习模型那样像个黑盒;第二,GBDT+LR的效果在很多场景下并不比深度学习差太多,尤其是当你的特征体系还不够丰富的时候;第三,这个方案对数据量的要求相对较低,几百万条样本就能训出一个可用的模型,而深度学习模型往往需要上千万甚至上亿的样本才能发挥优势。

具体来说,GBDT+LR的流程是这样的:先用GBDT在原始特征上训练,然后把GBDT每个叶子节点的输出作为一个新的特征,所有叶子节点的输出拼接成一个稀疏向量,再把这个向量喂给LR做最终训练。GBDT在这里的作用是自动做特征交叉和离散化,LR的作用是给这些交叉特征分配权重。这个方案的精妙之处在于,GBDT的树结构天然地捕捉了特征之间的非线性关系,而LR又保证了最终输出的概率校准性。

3. 核心细节解析与实操要点

3.1 样本构造:排序模型的地基

排序模型的样本构造和普通分类模型有本质区别。普通分类模型一条样本就是一个独立的样本,但排序模型的样本往往是以组的形式出现的——同一个请求下的所有候选item构成一个组,组内的样本之间有相互竞争的关系。

我刚开始做的时候没注意这一点,直接把所有曝光日志打平当成独立样本训练,结果模型学出来的分数在不同请求之间没有可比性。后来才明白,排序模型必须保留组结构,尤其是在用pairwise或listwise损失的时候。

具体操作上,我一般会这样构造样本:每个请求ID对应一个组,组内包含该请求下所有被曝光的item,每个item标注用户是否点击、是否转化等行为。正样本是有点击或转化的item,负样本是曝光未点击的item。这里有个坑:曝光未点击的item不一定真的是负样本,用户可能只是没看到那个位置,或者看到了但不感兴趣。所以有些团队会做"未曝光采样",把那些没被曝光但可能相关的item也作为负样本,这样能缓解位置偏差的问题。

还有一个细节是样本的时间窗口。排序模型对时效性很敏感,用户昨天的兴趣和今天的兴趣可能完全不同。我一般会用最近7到14天的数据来训练,太老的数据反而会引入噪声。如果业务变化快,比如电商大促期间,那时间窗口还要进一步缩短。

3.2 特征体系:决定排序效果上限的关键

特征工程是排序模型里最耗时间但也最出效果的部分。我习惯把特征分成四大类:用户侧特征、item侧特征、上下文特征、交叉特征。

用户侧特征包括用户ID、年龄、性别、历史行为统计(比如过去7天点击了多少次、购买了多少次)、兴趣标签等。item侧特征包括item ID、类目、价格、历史CTR、历史转化率等。上下文特征包括时间、地点、设备、网络环境等。交叉特征则是用户侧和item侧的组合,比如"用户对某个类目的历史点击率"。

这里我要特别强调一点:历史统计特征一定要做时间窗口的切分。比如"过去1天点击次数"、"过去7天点击次数"、"过去30天点击次数",这三个特征携带的信息完全不同。过去1天的反映短期兴趣,过去30天的反映长期偏好。如果你只用一个"历史点击次数",模型就没法区分短期和长期兴趣。

另外,特征的时间对齐极其重要。你在构造训练样本的时候,特征只能使用样本发生时间之前的数据,绝对不能用到未来的信息。这个坑我踩过——有一次不小心把item的未来曝光量作为特征,离线AUC高得离谱,上线之后效果一塌糊涂。后来排查了半天才发现是特征穿越了。

3.3 损失函数选择:Pointwise、Pairwise还是Listwise

排序模型的损失函数有三种主流选择,每种都有各自的适用场景。

Pointwise是把排序当成回归或分类问题,对每个item独立预测一个分数。优点是实现简单,缺点是忽略了item之间的相对关系。我一般只在候选集很小、或者只需要粗排的场景下用pointwise。

Pairwise是把排序当成一个二分类问题,但样本是成对的——一个正样本和一个负样本组成一对,模型要学习的是正样本的分数应该高于负样本。代表算法是BPR和RankNet。Pairwise的好处是直接优化了相对顺序,更贴近排序的本质。但缺点是样本对的数量是O(n^2)级别的,如果候选集很大,样本对会爆炸。

Listwise是把整个候选列表作为一个整体来优化,代表算法是ListNet和LambdaRank。Listwise理论上最优,因为它直接优化了列表级别的指标(比如NDCG),但实现复杂度也最高。我在实际项目中用得最多的是pairwise,因为它在效果和复杂度之间取得了比较好的平衡。

这里给一个我常用的pairwise损失实现思路:对于每个请求,取一个正样本和一个负样本组成一对,计算两个样本的分数差,然后用sigmoid函数把分数差映射成概率,最后用交叉熵损失来训练。关键代码如下:

import torch import torch.nn as nn class PairwiseLoss(nn.Module): def __init__(self): super().__init__() self.sigmoid = nn.Sigmoid() def forward(self, pos_score, neg_score): # pos_score: 正样本的预测分数 # neg_score: 负样本的预测分数 diff = pos_score - neg_score prob = self.sigmoid(diff) # 目标是让prob接近1,即正样本分数高于负样本 loss = -torch.log(prob + 1e-10).mean() return loss

这个实现很简单,但效果很稳。注意这里加了一个极小值1e-10防止log(0)的情况。

3.4 评估指标:离线看什么,线上看什么

排序模型的评估指标分离线 and 线上两套。离线常用的有AUC、GAUC、NDCG、MAP、MRR。线上则看CTR、CVR、人均点击次数、停留时长这些业务指标。

AUC是最常用的离线指标,但它有个致命缺陷:AUC是全局的,不区分请求。也就是说,AUC高不代表每个请求内部的排序都好。所以我更推荐用GAUC(Group AUC),也就是按请求分组计算AUC然后加权平均。GAUC能更好地反映模型在每个请求内部的排序能力。

NDCG(Normalized Discounted Cumulative Gain)是另一个我常用的指标,它考虑了位置的影响——排在前面的item权重更高。NDCG的计算公式是:

NDCG@K = DCG@K / IDCG@K

其中DCG@K = Σ(rel_i / log2(i+1)),rel_i是第i个位置的相关性分数,IDCG@K是理想情况下的DCG。这个指标特别适合评估Top-K排序的质量。

线上评估就简单直接得多,主要看业务指标的变化。但要注意,线上评估一定要做A/B实验,而且实验周期要足够长,至少覆盖一个完整的用户行为周期(比如一周)。我见过太多团队A/B实验只跑了一天就下结论,结果第二天指标就反转了。

4. 实操过程与核心环节实现

4.1 数据准备与特征工程实操

假设我们现在要做一个电商搜索的排序模型,候选集是搜索"运动鞋"之后返回的500个商品。第一步是准备数据。

原始数据一般包含三张表:曝光日志表(记录每个请求下曝光了哪些item)、用户行为表(记录用户的点击、购买等行为)、item属性表(记录item的类目、价格等信息)。我一般会先用SQL把这些表join起来,生成一张宽表,每一行是一个"请求-item"对,包含用户特征、item特征、上下文特征和label。

特征工程阶段,我会重点构造以下几类特征:

  • 用户历史统计特征:过去1/7/30天的点击次数、购买次数、点击率、购买率,按类目、品牌、价格区间分别统计。
  • item历史统计特征:过去1/7/30天的曝光量、点击量、点击率、转化率。
  • 用户-item交叉特征:用户对该item所属类目的历史点击率、用户对该品牌的购买次数、用户历史购买价格均值与item价格的差值。
  • 上下文特征:请求时间的小时、星期几、是否节假日、用户所在城市等级。

这里有个实操技巧:对于连续值特征,我一般会做分桶离散化。比如价格,我会分成0-50、50-100、100-200、200-500、500+这几个桶。离散化的好处是能捕捉非线性关系,而且对异常值更鲁棒。分桶的边界可以通过等频分桶或者业务经验来确定。

4.2 模型训练与调参实录

数据准备好之后,就可以开始训练了。我用的是LightGBM+LR的方案,LightGBM负责特征交叉,LR负责最终融合。

LightGBM的训练参数我一般这样设置:

import lightgbm as lgb params = { 'objective': 'binary', 'metric': 'auc', 'boosting_type': 'gbdt', 'num_leaves': 64, 'learning_rate': 0.05, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'max_depth': 6, 'min_data_in_leaf': 100, 'lambda_l2': 1.0, 'verbose': -1 } train_data = lgb.Dataset(X_train, label=y_train) valid_data = lgb.Dataset(X_valid, label=y_valid, reference=train_data) model = lgb.train( params, train_data, num_boost_round=500, valid_sets=[valid_data], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)] )

这里有几个参数值得说明。num_leaves=64和max_depth=6是为了控制模型复杂度,防止过拟合。min_data_in_leaf=100保证每个叶子节点至少有100条样本,避免学出太细碎的规则。learning_rate=0.05配合num_boost_round=500和早停,是比较稳妥的组合。

LightGBM训练完之后,我把每棵树的叶子节点输出作为新特征,拼接成一个稀疏向量,然后用LR训练。LR的训练我用的是sklearn的LogisticRegression,C=0.1做正则化。

这里有个细节:LightGBM的叶子节点编号需要做one-hot编码。比如第1棵树有64个叶子,那这64个叶子就对应64个one-hot特征。所有树的叶子特征拼接起来,维度可能上万。这个稀疏向量直接喂给LR就行,LR对稀疏特征的处理效率很高。

4.3 线上部署与性能优化

模型训练好之后,下一步是部署到线上。排序模型的线上 serving 有两个核心要求:低延迟和高吞吐。

低延迟方面,我一般会把LightGBM和LR的推理逻辑用C++重写,或者用ONNX Runtime来加速。LightGBM的推理其实很快,主要瓶颈在特征获取上。如果特征需要实时从多个数据源拉取,那延迟就很难控制。我的做法是:把特征分成实时特征和离线特征,离线特征提前算好存到KV存储里,线上直接查;实时特征只保留最关键的几个,比如用户最近一次点击的item ID。

高吞吐方面,我一般会用批量推理的方式。把同一个请求下的所有候选item组成一个batch,一次性推理完,而不是一个一个来。这样能充分利用CPU的向量化能力,吞吐量能提升好几倍。

还有一个工程上的坑:特征版本管理。线上模型用的特征必须和训练时的特征完全一致,包括特征的顺序、类型、缺失值处理方式。我见过因为特征顺序搞错导致线上效果暴跌的案例。所以我现在都会在模型文件里附带一份特征配置文件,线上加载模型的时候同时加载特征配置,确保一致性。

5. 常见问题与排查技巧实录

5.1 离线指标好但线上效果差

这是排序模型最常见的问题,没有之一。原因通常有以下几种:

特征穿越:训练时用了未来的信息。比如你用了item的未来曝光量作为特征,离线AUC会虚高,但线上根本拿不到这个特征。排查方法是逐特征检查时间对齐,确保每个特征的计算时间都早于样本时间。

样本选择偏差:训练样本只用了曝光过的item,但线上需要对所有候选item排序。曝光过的item往往是经过上一版模型筛选的,分布和全量候选集不一样。解决方法是在训练时加入未曝光样本,或者用IPS(Inverse Propensity Scoring)做纠偏。

位置偏差:用户倾向于点击排在前面的item,导致前面的item正样本率天然更高。模型学到的可能是"位置"而不是"相关性"。解决方法是在训练时把位置作为一个特征,但在线上推理时把位置特征置为同一个值,这样模型就不会依赖位置信息。

线上线下特征不一致:这个最隐蔽,也最致命。排查方法是做特征一致性校验,把线上实时计算的特征和离线计算的特征做对比,看看有没有差异。

5.2 模型分数分布异常

有时候你会发现模型输出的分数都集中在一个很窄的区间里,比如都在0.4到0.6之间。这种情况通常是因为特征区分度不够或者模型欠拟合。

如果是特征区分度不够,那就要检查特征工程是不是做得太粗糙。比如用户历史点击次数这个特征,如果大部分用户都是0,那这个特征就没什么区分度。可以考虑做更细粒度的统计,或者引入更多特征。

如果是模型欠拟合,那可以尝试增加模型复杂度,比如增加LightGBM的树数量、增大num_leaves,或者增加特征数量。

还有一种可能是样本不均衡。如果正样本比例极低(比如低于1%),模型可能会倾向于预测一个接近先验概率的分数。解决方法是对正样本做上采样,或者用focal loss之类的损失函数。

5.3 线上延迟过高

排序模型的延迟主要花在特征获取和模型推理上。如果延迟过高,可以按以下顺序排查:

排查项可能原因解决方案
特征获取实时特征太多,或者KV存储查询慢减少实时特征数量,增加缓存
模型推理模型太大,或者没有做批量推理模型剪枝、量化,或者改批量推理
网络传输特征和模型分散在不同机器上把特征和模型部署在同一台机器上
并发竞争多个请求同时查询同一个特征加本地缓存,或者做请求合并

我自己的经验是,80%的延迟问题都出在特征获取上。所以优化延迟的第一步永远是精简特征,把那些重要性低、获取成本高的特征砍掉。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
离线AUC高,线上CTR低特征穿越或样本偏差检查特征时间对齐,对比线上线下特征分布修正特征计算逻辑,加入未曝光样本
模型分数区分度低特征区分度不够或欠拟合检查特征分布,看是否大部分值相同增加特征,或增加模型复杂度
线上延迟高特征获取慢或模型推理慢打点统计各环节耗时精简特征,批量推理,加缓存
模型效果随时间下降数据分布漂移监控特征分布和模型分数分布定期重新训练,加入时间衰减权重
某些item永远排不上去特征覆盖不足检查这些item的特征是否缺失补充item侧特征,或做冷启动处理

5.5 我踩过的三个坑

第一个坑是特征穿越。有一次我做了一个"item过去7天点击率"的特征,离线AUC提升了3个点,我特别高兴。结果上线之后效果反而下降了。排查了半天才发现,我在计算这个特征的时候,用的是包含样本当天的数据,也就是说样本当天的点击也计入了过去7天的统计里。这导致模型在训练时看到了未来信息,离线指标虚高。后来我把特征计算的时间窗口改成"样本时间之前7天",问题就解决了。

第二个坑是样本不均衡。有一个场景正样本率只有0.5%,我直接拿全量样本训练,结果模型输出的分数全部集中在0.005左右,完全没有区分度。后来我对正样本做了10倍上采样,同时调整了LR的class_weight,模型才正常起来。

第三个坑是线上线下特征不一致。线上用的是Java计算特征,离线用的是Python,两边对缺失值的处理方式不一样——Java把缺失值填了0,Python填了-1。结果线上效果比离线差了5个点。后来我统一了缺失值的处理逻辑,问题才解决。这件事之后,我养成了一个习惯:任何特征的上线,都必须做线上线下一致性校验,抽样1000条样本,对比两边计算出来的特征值,完全一致才能上线。

6. 排序模型的扩展方向与个人体会

排序模型做到一定程度之后,你会发现单纯的排序已经不够用了。因为排序的前提是有一个候选集,而候选集的质量直接决定了排序的天花板。所以现在越来越多的团队把排序和召回、重排放在一起做端到端的优化。比如用双塔模型做召回,用深度学习模型做排序,再用MMR之类的算法做重排保证多样性。

另一个方向是多目标排序。用户的行为是多元的,有点击、有购买、有收藏、有分享,单一目标的排序模型很难同时优化所有这些行为。所以现在很多团队在做多目标模型,比如MMOE、PLE这些结构,同时预测多个行为,然后在线上根据业务需求调整不同目标的权重。

还有一个方向是实时排序。传统的排序模型是离线训练、线上推理,模型更新频率可能是天级甚至周级。但用户的兴趣是实时变化的,所以现在有些团队在做在线学习,模型能够实时吸收用户的反馈,分钟级甚至秒级更新。这个方向的技术挑战很大,但效果提升也很明显。

我个人在实际操作中的体会是,排序模型的效果提升从来不是靠单一的技术突破,而是靠数据、特征、模型、工程四个方面的协同优化。你很难说哪个方面最重要,因为任何一个方面拖后腿,整体效果都上不去。我见过特征做得很好但工程跟不上导致延迟太高没法上线的,也见过工程很牛但特征粗糙导致效果一般的。所以做排序模型,一定要有全局视角,不能只盯着模型结构调来调去。

最后分享一个小技巧:每次上线新模型之前,一定要做小流量实验。不要一上来就全量,哪怕你离线指标再好。因为线上环境太复杂了,总会有你意想不到的问题。小流量实验能帮你把风险控制在可接受范围内,而且能给你一个真实的线上效果评估。我现在的习惯是,新模型先上1%流量,观察一天,没问题再逐步扩到5%、10%、50%,最后全量。这个过程虽然慢,但稳。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询