
做推荐系统的同学应该都有体会模型结构决定上限但真正把上限托起来、把下限兜住的往往是特征。一个推荐模型能不能在业务上持续产出增量比拼的往往不是换了个什么结构、调了个什么Loss而是特征系统的完整度和精细化程度。我最近把我们这套推荐系统的特征体系完整梳理了一遍从用户行为日志、物品内容数据到上下文环境、业务存量数据逐层拆解后统计下来一共沉淀了230多个可用的特征。这篇就围绕这套特征系统讲讲这些特征从哪来、怎么组织、怎么加工、怎么上线以及真正跑起来之后踩过的坑。先说清楚这套特征体系是一个典型的电商场景推荐系统兼顾了首页信息流推荐、相关推荐和搜索排序这几个业务。不同的业务场景复用同一套特征底座但不同场景会开启不同的特征子集。所以下面讲到的230多个特征不是每一个在每一次请求里都会被用到而是整个系统里所有可用特征的总盘子。理解了这个前提再往下看就不会觉得数量夸张了。1. 特征全景图230多个特征是从哪个维度长出来的我先按特征的性质和产出链路把整套特征体系分成六个大的特征组。这个分组方式不是按业务线分的而是按特征在建模过程中扮演的角色分的方便后续做特征管理、血缘追踪和实验对比。1.1 六组特征的整体构成推荐的本质是预估在给定的上下文里把什么样的物品展示给什么样的用户能带来最理想的反馈。所以特征组的第一层分法就是按主体来切用户侧特征、物品侧特征、上下文特征。这三类是最基础的模型如果没有这三类信息几乎无法判断谁在什么环境下看什么东西。剩下三组是我在实际建模中吃过亏之后逐渐补上的交叉特征、序列特征和统计特征。交叉特征解决的是单看用户和单看物品都不够两者放在一起信息量才足够大的问题序列特征解决的是行为发生的先后顺序里隐藏着兴趣演化的方向统计特征则是把长周期、大范围的群体行为压缩成可以学习的稠密信号。这六组加起来构成了230多个特征的完整盘子。1.2 特征来源渠道数据从哪儿来源头上就三类日志数据、内容数据和业务数据。日志数据是用户行为埋点产生的包括曝光、点击、停留时长、加购、下单、收藏、分享这些动作内容数据是物品本身的属性包括类目、品牌、价格、标题、图片、详情页标签等业务数据则是存量的事实数据比如库存状态、是否新品、是否参加促销、运营后台打的商品分层标签等。三者的产出周期差异很大。日志数据是最高频的也是实时特征的主要来源内容数据相对稳定往往通过离线批处理按小时或按天更新业务数据则是介于两者之间促销状态这种可能在一天的某个时刻突然变化。所以特征系统里必须同时支撑离线链路和实时链路两种链路之间还要有一致性校验这个我在第4章详细讲。2. 六组特征详解每个特征组的构造逻辑与业务含义这一章把六组特征逐一拆开结合具体的特征实例讲清楚每个特征为什么存在、怎么计算、在模型里起什么作用。2.1 用户特征组从长期画像到实时意图用户特征组是整套系统里数量最多的一组统计下来大概占了70多个。它又可以按时间粒度分成三个子层静态画像、近N天统计、实时行为。静态画像层主要是注册信息包括user_id、性别、年龄段、城市等级、会员等级、注册天数。这些特征更新频率低但很重要因为冷启动阶段模型基本只能靠这些信号。注册天数是我很看重的特征它体现了用户的生命周期阶段注册1天的新客和注册500天的老客行为模式和风险偏好差别很大。会员等级则直接反映用户在平台上的价值分层高等级用户对价格的敏感度明显偏低。近N天统计层是用户特征的主力覆盖近1天、近3天、近7天、近14天、近30天这几个窗口每个窗口下都有一组行为统计量count类点击次数、去重物品数、加购次数、下单次数、收藏次数、搜索次数ratio类点击转加购率、加购转下单率、下单支付率、搜索后点击率消费偏好类近30天平均成交价、近30天优惠券使用率、类目集中度熵类目集中度熵我补充解释一下它是用信息熵把用户近一段时间点击类目的分布压缩成一个数值。熵值越接近0说明用户点击全部集中在同一个类目里兴趣非常聚焦熵值越大说明用户什么都看兴趣分散。这个特征在召回阶段和排序阶段都有用排序阶段可以帮模型区分我这个用户是一个深度垂类用户还是泛兴趣用户进而影响对物品类目的打分倾向。实时行为层则聚焦当前这次请求之前的极短时间窗内的行为比如近1分钟点击目标、当前session已经浏览的类目列表、当前小时内的搜索词序列。这类特征对捕捉用户当下的即时意图至关重要。用户可能过去30天的偏好一直是美妆但此刻他连续点了三条数码3C的内容那么当前session的实时意图明显已经切换了模型必须通过实时特征感知到这种漂移并做出反应。2.2 物品特征组内容质量、生命周期与价格带物品特征组大概40多个核心是回答一个问题这个物品本身值不值得被推荐。最基础的是属性类特征item_id、类目id、品牌id、价格、上下架时间、商家id、店铺评分等。item_id看起来是一个纯粹的ID但它经过embedding之后能承载这个物品的历史行为信息是物品特征组里单特征信息量最大的一个。类目id和品牌id也是embedding重点。然后是质量类特征包括物品维度的点击率、转化率、收藏率、好评率、浏览量、销量。这些统计量要在合适的窗口下计算并且要剔除用户自发搜索带来的流量否则会出现严重的偏差。我实际使用的有近7天ctr、近30天cvr、累积好评率、近7天销量环比等。质量类特征有一个明显的用处它让模型不必要从头学习什么是好物品直接给一个先验信号尤其对长尾物品的预估帮助很大。物品生命周期状态也很关键是否新品、是否临期品、是否季节性商品、库存状态。新品没有历史统计量模型容易预估偏低所以必须单独给一个is_new特征并且在训练阶段对正样本做加权让模型知道新品虽然曝光少但一旦曝光表现不差。季节性商品则存在明显的周期性比如夏季防晒霜的转化集中在5月到8月这类信息通过月份交叉特征来体现比纯时间戳要好理解得多。价格带是物品特征里容易被忽略但实际影响很大的特征。同样是连衣裙99元和999元面对的用户群体完全不同。我把价格做了分桶变成价格带等级同时保留原价的连续值。分桶的目的是减少对异常值的敏感连续值是保留模型自己学非线性关系的空间。2.3 上下文特征组推荐发生的环境信息上下文特征组数量不多大概15个左右但直接影响排序候选集的选择。因为它刻画的是什么时候、什么地方、什么条件下用户发起了这次请求。时间维度上有小时、星期几、是否周末、是否节假日、距离最近节假日的天数、季节。小时这个特征虽然看起来简单但很多场景下是强信号——通勤时段用户偏好短平快的内容和商品晚上则更愿意逛。距离最近节假日的天数这个特征是我后来加的因为大促前一周用户的行为模式会发生明显变化搜索和比价意图增强这个特征帮助模型学到大促前的流速变化。空间维度上有用户所在城市、城市级别、GPS所在的商圈信息、经纬度。空间特征不是所有场景都需要但对本地生活类的物品效果显著。用户在某个商圈附近看到本地门店商品的推荐点击率和到店转化会明显好于随机推荐。设备与环境维度包括设备类型、操作系统、网络环境wifi还是移动网络、App版本。这些特征在很多文章里被归为弱特征但实际跑下来它们对模型稳定性的贡献不小。App版本可以反映用户是否升级到了最近版本新版用户一般行为路径有变化网络环境影响视频和图片类物品的加载体验wifi用户的粘性明显更高。2.4 交叉特征组把用户和物品放在同一张桌上交叉特征组是我认为投入产出比最高的一组特征数量大约30个每一个都值得单独立项去细抠。用户与物品的交叉特征核心是用户对这个物品及其所属类目、品牌、价格带的历史亲密度。比如用户最近7天点击过这个类目的次数、用户历史上对同品牌物品的下单次数、用户近30天购买过的平均价格带与该物品价格带的差距、用户是否点击过该物品的相似款。这些特征的本质是把用户偏好和物品属性对齐当模型同时看到用户画像偏向低价带和物品价格偏低这个对齐关系时它就不需要自己费力去拟合这个交互了。物品与物品的交叉特征主要服务相关推荐场景。包括候选物品与用户最近一次点击物品的类目相似度、品牌相似度、价格带相似度、标签重合度。这些特征通过离线计算好的物品相似度矩阵来提供。还有一类时间与物品的交叉物品最近一次被曝光给该用户的时间间隔、该用户在近7天对这个类目的点击时间分布。时间间隔特性能捕捉用户之前见过这个物品没点这次再见到要不要点的微妙信号。如果用户两天前曝光没点今天又出现模型可能判断不够吸引但如果用户刚刚加购了同款今天再推那可能是跨品类联动的有效推荐。2.5 序列特征组行为顺序里藏着的意图方向序列特征组大概20多个核心思想是行为不仅有有没有的区别还有先后顺序的区别。最直接的序列特征是用户近N次点击序列的统计量最近一次点击与当前时间的间隔、最近两次点击之间的时间间隔、当前session内点击步数、序列长度、点击序列中的类目转移次数。这些特征在信息流推荐里帮助模型判断用户当前正处于探索期还是收束期。探索期的用户刚刚进入App到处看意图不明收束期的用户已经连续点击多个同类目物品此时推荐同类目物品的命中率显著提升。还有序列embedding类特征把用户近N次点击的item_id序列通过模型内的序列encoder产出向量再和候选物品embedding做交互。这个特征线的设计不追求复杂的Transformer结构简单的Mean-pooling在大部分业务场景下的收益就足够好复杂的结构在长序列上反而容易过拟合。序列特征组的计算链路比较重。每次请求都要拉取用户最近的行为序列做截断、排序、padding再喂给模型。如果序列长度阈值设得太大线上延迟和存储压力都会上来设得太小又丢失了足够的历史上下文。我目前用的是最长截断到50个行为超过的部分按时间倒序保留最近50条。2.6 统计特征组群体行为的压缩信号统计特征组是数量上最容易被低估的一组我最终统计下来大概50多个。把这一类单独成组的原因是它不是直接从用户或物品的数据里取出来的而是需要经过多一层聚合计算。最典型的包括用户在全平台近7天点击过的物品中被其他用户点击的分布情况候选物品在相似用户群体中的转化率类目的热门程度排名变化。这一类特征的价值在于引入了群体行为和横向对比。单看一个物品点击率0.3%可能无法判断好坏但如果在同品类的均值是0.15%、中位数0.12%时0.3%就是一个很强的信号。模型直接学习绝对数值比较困难但给出相对于基线水平的倍数偏移学起来就容易很多。还有一类是滑窗聚合特征比如物品近1小时、近3小时、近24小时的曝光量、点击量、加购量。这些特征在做实时热门推荐时尤其有用一个物品如果近1小时的点击量暴涨说明有可能是爆款潜力股要把它及时推到推荐池里。为了保证计算效率这一类特征一般用Flink这样的实时计算引擎来维护不能离线算完再推。统计特征最大的风险是方差和时滞。小时级别的窗口方差很大一个物品可能在某个小时突然涌进来大量流量比如被外部渠道引爆了此时如果直接把数据喂给模型会导致排序剧烈波动。我通常会加一层平滑用EWMA指数加权移动平均来衰减历史值平滑系数控制在0.7左右既能及时响应近期变化又不会因为单点突变造成抖动。3. 特征加工的工业化流程从原始日志到可建模特征有了特征清单接下来的问题是如何稳定地批量生产。特征加工不是简单的SQL取数里面涉及清洗、编码、归一化、分桶、embedding和血缘管理一系列环节。这一章讲我在生产环境里的完整加工链路。3.1 数据清洗与异常值处理脏数据是模型的天敌特征加工的起点是清洗。日志数据里的脏数据主要有三类机器人点击、爬虫流量和测试流量。我们通过IP异常检测、行为频次阈值和用户标识校验来过滤比如一个用户在一秒内点击了20个物品基本可以断定是异常流量这种样本进入训练集就是纯噪声。缺失值和异常值也有固定策略。对稀疏的ID类特征比如类目id、品牌id缺失时填一个统一的UNKNOWN占位符对数值类特征比如价格、销量缺失时用全局中位数填充不用均值因为均值对长尾分布太敏感。异常值的处理上用户行为计数类特征要做截断比如点击次数超过99分位数的值一律压到99分位数避免个别极端用户主导模型参数。这里我特别提醒一个坑清洗规则必须是离线训练和线上推理共用同一套代码。曾经发生过线上过滤规则更新了但离线训练数据没同步导致模型在线上看到的特征分布和训练时对不上上线后CTR掉了快5个点。排查了两天才找到根因。现在清洗逻辑全部收敛到一个公共库离线训练和在线推理都做统一调用。3.2 特征编码方案ID类、数值类、序列类怎么喂给模型编码方案直接决定模型能学到什么。ID类特征我统一走embeddingembedding维度一般取特征值数量的开4次方再乘个系数稀疏类目用64维高频品牌用32维长尾ID类用16维。低频值的处理很讲究出现次数少于5次的ID会被归拢到UNKNOWN桶里否则embedding表会越来越大低频值的学习也不充分只会产生噪声。数值类特征的工作量反而比想象中大。原始数值直接喂给深度模型容易让梯度不稳定尤其价格、销量这种量级跨度大的特征。我实际用下来最有效的组合是log1p变换做一次非线性压缩然后做分位数分桶桶的边界按训练集的分位数从0.01到0.99打20个点。分桶后的特征用embedding编码模型可以学到桶之间的顺序关系。连续值本人也保留一路两路一起进模型没有冲突。序列类特征的编码相对重但价值也明显。用户近N次点击的item序列先做长度截断和倒序排列形成一个固定长度的item序列向量序列再输入浅层Transformer或者GRU。序列编码的输出用来和候选物品做Target-Attention交互这样模型就能知道用户刚刚看的几个物品里和当前候选物品相关的占比有多少。3.3 特征交叉与高阶加工让特征之间产生化学反应基础特征之间加交叉是特征系统从能用到好用的关键一步。高阶加工我常用的方式是数值交叉把用户近30天的平均成交价和候选物品价格相减得到价格差特征这个特征比单独给两个价格更能表达价格匹配度类目交叉把用户最近一次点击的类目和候选物品类目做相等映射得到一个布尔匹配特征聚合交叉用户在目标类目下的点击次数除以用户在全部类目的点击次数得到类目偏好浓度时序交叉用户最近一次在该类目发生行为距离现在的天数表达偏好时效性这些交叉特征的设计原则是每个特征在业务上都能用一句话解释清楚如果解释不清说明这个特征更多是噪声不值得加。模型可以学出复杂的交互但前提是我们有足够多的数据。在数据没那么充裕的场景手工交叉特征仍然占有重要位置。4. 特征上线与实时性设计离线训练和线上服务必须对齐特征系统在离线挖好不算完真正难的是让它稳定地在线上跑起来并且在训练和推理之间保持一致性。这一章讲上线链路的设计和我在一致性问题上付出的学费。4.1 离线特征与实时特征的协同策略离线特征和实时特征承担的分工完全不同。离线特征以天为粒度更新服务于用户长期偏好类模型这类特征稳定性好、可解释性强适合做基座实时特征以秒或分钟为粒度更新服务于用户瞬时意图类模型波动大但响应快。两者协同的关键是避免重复造轮子。同一个信息不要既放在离线特征里又放在实时特征里否则模型会出现特征权重分配混乱。我的策略是近1天以上的行为统计全部走离线近1小时以内的行为统计全部走实时中间1小时到24小时这个区间在绝大多数场景下对推荐效果的影响相对有限可以合并到离线统计的当日累计值里或者直接忽略这样架构上省很多事。实时特征链路的延迟目标是P99不超过200ms。每次请求到来时用户最近的实时行为从Redis这类内存存储里拉取和离线特征拼接后一起进模型。为了避免实时存储挂掉影响推荐主链路实时特征读取失败时的fallback是直接置默认值而不是把请求打回保证用户体验优先。4.2 训练和推理一致性的保障线上日志回放机制特征一致性是推荐系统上线过程中最容易踩的暗坑。如果训练时用的用户近7天点击次数是截止到训练样本当日零点计算的线上推理时却用了截止到当前时刻的实时计数特征定义就不一致了模型会学到错误的规律。我最终采用的是日志回放机制来保障一致性。线上每一次请求的特征输入都会完整地记录下来落到日志里。离线训练时对每个样本使用的是从该样本发生时刻往过去回溯的特征快照而不是用今天的计算值。这套机制部署初期投入比较大需要把特征计算引擎做到底层支持按时间快照查询但跑通之后一劳永逸再也不需要担心特征穿越和前后不一致的问题。另外还有一道防线是特征监控。每天对线上采样的特征值和离线计算的特征值做分布K-L散度对比超过阈值自动告警。曾有次实时计算集群升级后出现了短暂的窗口边界错位问题导致近1小时的点击量算成了近2小时线上CTR出现5%的回落。靠监控告警在半小时内定位到了这个特征偏差避免了大盘长时间受损。4.3 特征平台化管理注册、血缘、权限与版本特征数量过了200之后如果还靠手工配置会变成一场灾难。我们逐步把特征管理收敛到一个内部特征平台上每个特征除了计算逻辑之外还必须登记以下元信息业务定义、适用场景、上线日期、责任人、血缘来源、数据质量等级。血缘来源记录了这个特征从哪张原始表、哪些过滤条件、哪些计算步骤加工而来方便任何时间回溯。特征版本管理也是平台能力的一部分。特征计算逻辑的变更一律走版本发布流程不直接原地修改。训练任务和线上模型都显式指定自己使用的特征版本号保证灰度切换时线上模型不会因为特征逻辑突然变更而失去对照基准。5. 230特征系统的实践复盘调试、优化与踩坑回顾最后这部分聊实践。特征系统不是堆数量230多个特征是反复迭代、增删、实验后形成的规模。这一章记录我在整个过程中反复用到的调试思路和一些值得长期记住的教训。5.1 特征覆盖率问题上线前必须自查的那张表每个特征在线上都存在可能拿不到值的情况。年龄未知、城市未知、新品没有历史统计量、冷启动用户没有行为记录。特征上线前必须做覆盖率评估低于阈值的特征要决定怎么办。我的经验是覆盖率低于99%的特征如果业务意义非至关重要干脆不加覆盖率在99%到99.9%的要设置合理解释的默认值极端重要的是默认值的选择必须基于业务理解不能简单填0。打个比方如果用户没有近7天点击记录直接填0确实没问题但如果用户是因为长时间未登录呢此时填0会让模型把他当成没有兴趣的活跃用户更合理的默认值应该是未知模式的静默状态这种时候可以加一个布尔特征标记该统计是否有效让模型自行学习。5.2 特征穿越与信息泄漏最隐蔽的效果虚高特征穿越是离线训练效果很好、上线效果大跌的最常见原因。我排查过的最隐蔽的一例某个物品当天是否参与秒杀活动的特征在实际业务里活动规则是当天下午2点才生效的但数据表里当天0点就已经标好了活动状态。训练时模型看到参与秒杀的物品转化率很高学到了下午2点前的转化率也会高上线后模型在上午大量推荐秒杀品用户并不买账。这个坑提醒我几件事所有特征都要做延时校验确认特征生效时刻是否晚于样本的实际行为时刻常用滞后N天的策略做特征对齐定期做特征与目标的反事实验证。特征穿越带来的离线指标虚高往往高达20%以上如果你的离线A/B很漂亮但线上纹丝不动优先检查有没有穿越。5.3 特征相关性分析与冗余清理做减法反而涨效果到了200多个特征之后我开始主动做减法。特征之间相关性太高时不仅不会带来增量还会让模型权重分散、训练变慢甚至带来过拟合。我用每种特征与目标变量的互信息、特征两两相关性矩阵来辅助做筛减把互信息趋近于0且业务解释不清晰的特征逐步下线。有一次清理了十几个和物品价格强相关的特征只保留价格带分层和价格差A/B实验显示点击率反而提升了0.8个百分点。原因在于模型把大量参数浪费在了冗余的重复信息上清理之后它把注意力放到了真正有区分度的序列特征上整体泛化能力反而好了。5.4 特征工程和模型结构的博弈深度模型还需要手工特征吗这个问题几乎每隔一段时间就会被拿出来讨论一次。我的观点很明确特征工程不是被模型替代了而是换了一种存在形态。深度模型确实能自动学习特征组合但它能学得好有两个前提——足够多的数据和足够强的信号表达。在很多中长尾场景数据稀疏模型学习效率低于手工特征的先验知识注入。实际项目中的做法是手工特征重点投在那些模型不太容易从原始输入里自己学出来的业务逻辑上比如价格带匹配度、类目偏好浓度、活动敏感度模型自学习的部分则保留在embedding和序列交互上。两者配合比我之前做过的任何只靠自动特征或只靠手工特征的版本效果都要更好。5.5 特征迭代的工程建议小步快跑灰度验证迭代特征系统最重要的工程习惯是小步快跑。每次不要同时上线一大批特征那样即便整体效果涨了也说不清楚是哪个特征贡献的后续维护无从下手。我通常一批只上3到5个强关联特征跑一个完整的灰度周期用分层实验平台评估每个特征的边际贡献。实验评估不只看点击率、转化率这样的核心业务指标还要盯每个特征维度上的供需结构变化比如新老用户的比例、热门物品和长尾物品的曝光占比。有些特征虽然提升了整体点击率但把流量高度集中到了头部热门物品上长尾供给被挤兑这种特征就需要考虑加约束来平衡。6. 特征系统的下一步演进与我的实操心得这套230的特征体系到目前为止稳定服务了多种推荐场景但从工程建设的角度看仍然有不少可以继续演进的方向。最后分享一下我个人的判断和长期沉淀下来的实操心得。6.1 记忆与泛化的再平衡场景化特征子系统的建设方向目前这套特征系统是一套大而全的底座不同业务场景通过开闭特征子集来适配。但更进一步的做法是为每个核心场景维护一组场景专属特征。比如首页信息流更依赖实时意图特征和序列特征相关推荐更依赖物品相似度交叉特征搜索排序则更依赖文本相关性特征。场景专属特征不建议在底座上无脑堆单独建一套更清晰。目标是将特征系统的扩展方式从不断加新特征逐步过渡到给不同场景配置不同特征组合。6.2 长期反复测试后的几条经验第一特征不是越多越好能上线且能持续证明增量才算有效特征。我给自己定了一条铁律新增特征如果在两个完整灰度周期内没能带来至少0.3个百分点的核心业务指标提升就主动下线不恋战。第二特征系统的可诊断性比特征本身更重要。一个特征价值再高如果线上出问题时排查链路太长运营成本同样会拖垮团队。所以日志回放、数据血缘、特征监控这三件事一定要在特征数量还没爆发的时候就开始做迟了再补成本高到难以承受。第三关于深度学习模型会不会让特征工程师无所事事我在多个项目上的体会是模型越复杂特征质量问题被放大的比例也越大。特征工程师不会消失但工作重心会从手工造特征转向设计特征生产链路、保障特征质量、发掘特征之间的高阶组合。能做好这几件事的人在任何一家公司都很难被替代。最后再说一个细节任何特征系统的迭代都不要忽略了业务可解释性这一关。推荐效果上去了业务同事一定有各种追问——为什么某个品类的曝光突然降了为什么某个用户反复看到已购同款。如果我们的特征系统无法回答这些为什么那么再漂亮的离线指标也会在跨团队协作中消耗殆尽。给每个重要特征都留一句人话解释这钱花得值得。