特征工程一致性:离线与在线特征计算逻辑对齐、特征存储与访问模式差异
特征工程一致性,说白了就是「离线训练时怎么算特征,线上推理时也得一模一样」。听起来简单对吧?但我见过太多团队在这上面栽跟头。模型离线 AUC 0.85,上线直接掉到 0.72,查来查去,最后发现是特征计算逻辑没对齐。
我个人习惯把这个问题拆成两个层面来看:计算逻辑的对齐和存储访问的差异。前者是「算得对不对」,后者是「拿得快不快」。咱们一个一个说。
计算逻辑对齐:同一个特征,两种命运
先讲个真实案例。我之前维护过一个推荐系统,离线用 Spark 做特征工程,线上用 Java 写了个特征计算服务。有个特征是「用户过去 1 小时点击某类商品的次数」。离线代码里,时间窗口是用 unix_timestamp() - 3600 算的;线上代码里,用的是 System.currentTimeMillis() - 3600000。看起来一样对吧?但问题出在时区上——Spark 集群用的是 UTC,线上服务器用的是 Asia/Shanghai。差了 8 个小时,特征值全偏了。
嗯,这里要注意:计算逻辑对齐不是「看起来一样」,而是「执行结果完全一致」。我总结了三个最容易出问题的点:
- 时间处理:统一时区、统一精度(秒级还是毫秒级)、统一窗口边界(左闭右开还是左开右闭)
- 聚合函数:count、sum、avg 这些基础函数在不同语言里行为一致,但像 percentile、median 这种,实现方式可能不同
- 缺失值处理:离线可能直接 drop,线上可能填充默认值——这俩结果能一样才怪
核心原则:离线与在线必须共享同一份特征计算代码。要么用同一个函数库(比如把 Python 特征函数封装成服务),要么用同一个表达式引擎(比如 ONNX、PMML)。不要「离线写一套,线上重写一套」。
特征存储与访问模式差异
离线训练时,特征是从 Hive 表或者 Parquet 文件里批量读出来的。一次读几千万行,慢点没关系,反正跑几个小时都行。但线上不一样——每次请求只查一个用户的特征,延迟要求毫秒级。
这就引出了存储和访问模式的根本差异。我习惯用一张表来对比:
| 维度 | 离线训练 | 在线推理 |
|---|---|---|
| 数据量 | 全量历史数据,TB 级 | 单条或批量,KB 级 |
| 延迟要求 | 分钟到小时级 | 毫秒级 |
| 存储介质 | HDFS、Hive、Parquet | Redis、内存、本地缓存 |
| 访问模式 | 批量扫描 + 全表扫描 | 点查 + 批量点查 |
| 数据更新 | 每日/每小时全量覆盖 | 实时增量更新 |
你想想看,离线用 Hive 存特征,线上用 Redis 查特征,这中间怎么保证数据一致?我曾经踩过一个坑:离线特征每天凌晨 2 点更新,线上 Redis 也是凌晨 2 点刷新。但有一次 Hive 任务延迟了 30 分钟,Redis 按时刷新了旧数据,结果线上用了前一天的特征,模型预测全乱套了。
避坑指南:我曾经因为离线在线特征更新不同步,导致线上模型「失忆」了整整 6 个小时。后来我强制要求:线上特征缓存必须等离线特征就绪后才能刷新,中间加一个「版本号校验」的环节。版本号对不上,宁可返回旧特征,也不返回错误特征。
特征对齐的三种常见方案
在实际项目中,我见过三种主流的对齐方案。每种都有适用场景,没有银弹。
- 代码复用方案:把特征计算逻辑封装成公共库,离线在线都调用同一个 jar 包或 Python 包。适合特征逻辑复杂、团队规模大的场景。
- 特征存储方案:离线算好特征后,写入统一的特征存储(比如 Feature Store),线上直接从存储里读。适合特征相对稳定、更新频率不高的场景。
- 实时计算方案:离线在线都用同一套实时计算引擎(比如 Flink),离线回放历史数据,线上处理实时数据。适合需要秒级特征更新的场景。
我个人比较推荐第二种方案,也就是 Feature Store 的思路。原因很简单——它把「计算」和「存储」解耦了。离线只管算,算完存进去;线上只管读,读出来直接用。中间只要保证存储的一致性就行。
小技巧:如果你暂时没有 Feature Store,可以用一个简单的「特征快照表」来过渡。离线每天生成一张特征宽表,线上从这张表的 Redis 副本里读。虽然不够实时,但至少能保证离线在线用的是同一份数据。
一张图看懂特征工程一致性
下面这张图展示了离线与在线特征工程的核心流程,以及一致性需要关注的几个关键节点:
实践中的几个关键检查点
说了这么多理论,最后给几个实操层面的检查点。每次做特征工程一致性对齐时,我都会拿着这个清单过一遍:
- 代码是否共享:离线在线用的是同一个特征函数吗?如果不是,差异在哪?
- 时间窗口是否一致:离线用「过去 1 小时」,线上也是「过去 1 小时」吗?边界怎么处理的?
- 特征存储是否同步:离线写入特征存储后,线上多久能读到?有没有版本号机制?
- 缺失值是否统一:离线训练时缺失值怎么处理的?线上遇到缺失值会怎么处理?
- 特征名称是否一致:这个听起来简单,但我见过离线叫
user_click_cnt_1h,线上叫click_cnt_1h,模型加载时直接报错。
一句话总结:特征工程一致性不是「差不多就行」,而是「必须一模一样」。计算逻辑、存储介质、访问模式,每一个环节都要对齐。别等到线上效果差了再回头查——那时候你已经浪费了好几个迭代周期了。