在机器学习系统从实验走向生产的过程中,特征计算与特征复用逐渐成为工程瓶颈。同一个特征可能被多个模型重复计算,离线训练与在线推理的特征口径不一致,训练数据中混入未来信息导致模型效果虚高。Feast通过特征存储的抽象来解决这些问题,它把原始数据源、特征计算逻辑和存储位置统一管理起来,并对外提供一致的离线与在线取数接口。其中,特征服务是Feast面向模型消费方的核心入口,而时间点正确性则是保证训练数据可信度的关键机制。

一、Feast的核心概念与特征服务定位
要理解特征服务,先要明确Feast中的几个基础对象。实体描述业务中的主体,例如用户、商品或交易。数据源指向原始数据的存储位置,可以是离线数据仓库中的表,也可以是在线数据库或流式数据源。特征视图是Feast最核心的抽象,它描述了一组特征如何从数据源计算得到,并声明了该特征视图所关联的实体、时间戳字段以及特征列。离线存储通常用于保存历史特征值,支持大规模训练数据生成;在线存储则面向低延迟推理,保存最新的特征值。
特征服务则是一组特征视图的逻辑集合,它不存储任何数据,而是定义了一个可供模型训练和在线推理统一使用的特征列表。在没有特征服务时,模型团队往往需要分别处理来自多个特征视图的特征,每次训练和推理都要手工组装特征向量,很容易出现特征遗漏或顺序不一致的问题。特征服务把这种组合关系固化下来,模型消费方只需要引用一个服务名称,就能稳定地获取所需特征。
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
from datetime import timedelta
# 定义实体
user = Entity(name="user_id", join_keys=["user_id"])
# 定义数据源
user_stats_source = FileSource(
path="data/user_stats.parquet",
timestamp_field="event_timestamp",
)
# 定义特征视图
user_stats_fv = FeatureView(
name="user_stats",
entities=[user],
ttl=timedelta(days=30),
schema=[
Field(name="total_orders", dtype=Int64),
Field(name="avg_order_value", dtype=Float32),
],
source=user_stats_source,
online=True,
)
上面的代码定义了一个用户实体和一个针对用户统计信息的特征视图。特征视图中的ttl参数表示特征值在多长时间内有效,这与在线特征的新鲜度有关,但时间点正确性则主要依赖时间戳字段和实体连接逻辑。特征服务将在这些特征视图之上进行组合。
二、Feature Service的定义与在线离线使用
定义特征服务非常简单,只需要列出需要包含的特征视图以及要从每个视图中选取的特征字段。Feast允许跨多个特征视图组合特征,即使这些特征视图关联的实体不完全相同,只要在取数时能够通过实体DataFrame进行关联即可。一个典型的特征服务定义如下。
from feast import FeatureService
user_profile_service = FeatureService(
name="user_profile_service",
features=[
user_stats_fv[["total_orders", "avg_order_value"]],
# 可以继续添加其他特征视图的特征
],
)
特征服务定义完成后,可以通过Feast的应用命令将对象注册到特征存储中。在线推理时,使用get_online_features接口传入实体键值,即可获得由在线存储返回的最新特征向量。该接口只返回特征当前值,不关心历史时间点,因此延迟很低,适合线上服务调用。
from feast import FeatureStore
store = FeatureStore(repo_path=".")
online_features = store.get_online_features(
features=user_profile_service,
entity_rows=[
{"user_id": 1001},
{"user_id": 1002},
],
).to_dict()
print(online_features)
离线训练则需要使用get_historical_features接口。该接口接收一个包含实体键和事件时间戳的实体DataFrame,返回在事件时间点之前有效的特征值。这里的实体DataFrame通常对应训练样本,每一行表示一条带有标签和事件时间的样本记录。Feast会根据事件时间戳从离线存储中检索符合条件的特征值,并按照实体键进行连接,最终生成训练数据集。
import pandas as pd
from feast import FeatureStore
store = FeatureStore(repo_path=".")
entity_df = pd.DataFrame({
"user_id": [1001, 1002, 1003],
"event_timestamp": pd.to_datetime([
"2024-01-10 10:00:00",
"2024-01-11 15:30:00",
"2024-01-12 08:00:00",
]),
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=user_profile_service,
).to_df()
print(training_df.head())
从上面的流程可以看出,特征服务把在线和离线两个场景的取数逻辑统一到了同一个定义中。模型训练时使用的特征组合与线上推理完全一致,避免了特征口径漂移。但真正让离线训练数据具备可信度的,是Feast在历史数据检索中强制执行的时间点连接机制。
三、时间点正确性:Feast如何实现Point-in-Time Join
时间点正确性要求训练样本中的特征值必须来自该样本事件发生之前已经存在的数据。如果特征值包含了事件发生之后的信息,就会造成数据泄漏。例如,用用户昨天的购买金额来预测用户今天是否流失是合理的,但如果用今天的购买金额去预测今天的流失标签,模型就会学到未来信息,离线评估时表现很好,但线上无法复现。时间点连接正是为了解决这个问题而设计。
Feast在执行get_historical_features时,会读取实体DataFrame中的event_timestamp列,并将其作为每个样本的时间边界。对于每一个特征视图,Feast会使用实体键和时间戳字段进行连接,连接条件可以简化为:特征记录的时间戳小于等于样本的事件时间戳,并且满足特征视图的TTL约束。通过这样的过滤,只有事件时间之前生成的特征值才会被保留下来。如果同一实体在事件时间之前有多条特征记录,Feast会选择最新的一条,因为最新记录更接近事件发生时刻,通常具有更高的信息价值。
下面的SQL片段展示了时间点连接的逻辑。实际实现中Feast会生成复杂的SQL或Spark查询,但核心条件类似。
SELECT
e.user_id,
e.event_timestamp,
f.total_orders,
f.avg_order_value
FROM entity_df e
LEFT JOIN user_stats_fv f
ON e.user_id = f.user_id
AND f.event_timestamp <= e.event_timestamp
AND f.event_timestamp >= e.event_timestamp - INTERVAL '30 days'
可以看到,f.event_timestamp <= e.event_timestamp保证了特征值不晚于事件时间,而f.event_timestamp >= e.event_timestamp减去TTL则限制了特征值的有效期。如果一个特征在30天前生成,而样本事件发生在今天,那么该特征可能已经过期,不满足时间点连接的条件。这种机制同时兼顾了正确性和特征新鲜度。
需要强调的是,时间点正确性依赖特征视图正确声明时间戳字段。如果在定义特征视图时没有指定timestamp_field,Feast无法判断特征值的事件时间,时间点连接就失去了依据。因此,在设计特征管道时,必须明确区分事件时间与处理时间,并将事件时间作为特征记录的时间戳写入数据源。
四、端到端实践:构建特征服务并验证时间点正确性
下面通过一个完整的例子说明如何从原始数据开始构建特征服务并验证时间点正确性。假设有一个用户交易明细表,包含用户ID、交易金额和交易时间。我们希望构建用户累计交易额和近30天交易次数两个特征,并通过特征服务提供给流失预测模型。
首先准备数据源并注册实体和特征视图。这里使用Parquet文件作为离线数据源,后续可以替换为数据仓库表。特征视图需要包含两个特征列,并且指定timestamp_field为交易时间。在线存储可以使用Feast的本地SQLite实现,生产环境通常替换为Redis或DynamoDB。
from feast import Entity, FeatureView, Field, FileSource
from feast.types import Float32, Int64
from datetime import timedelta
user = Entity(name="user_id", join_keys=["user_id"])
transactions_source = FileSource(
path="data/transactions.parquet",
timestamp_field="transaction_timestamp",
)
user_transactions_fv = FeatureView(
name="user_transactions",
entities=[user],
ttl=timedelta(days=30),
schema=[
Field(name="total_spend_30d", dtype=Float32),
Field(name="txn_count_30d", dtype=Int64),
],
source=transactions_source,
online=True,
)
from feast import FeatureService
user_txn_service = FeatureService(
name="user_txn_service",
features=[user_transactions_fv[["total_spend_30d", "txn_count_30d"]]],
)
应用注册后,Feast会创建离线存储表和在线存储表。如果特征值需要从原始交易明细中聚合得到,可以通过Feast的批处理作业或外部调度系统预计算特征值后写入离线存储和在线存储。对于这个例子,我们假设特征已经提前计算好并写入到对应的表中。
接下来构造实体DataFrame,模拟三条训练样本。每条样本包含用户ID和事件时间戳,事件时间戳代表标签发生的时间。调用get_historical_features后,检查返回的特征值是否只使用了事件时间之前的数据。
import pandas as pd
from feast import FeatureStore
store = FeatureStore(repo_path=".")
entity_df = pd.DataFrame({
"user_id": [1001, 1001, 1002],
"event_timestamp": pd.to_datetime([
"2024-01-15 12:00:00",
"2024-01-20 12:00:00",
"2024-01-18 09:00:00",
]),
})
training_df = store.get_historical_features(
entity_df=entity_df,
features=user_txn_service,
).to_df()
print(training_df[["user_id", "event_timestamp", "total_spend_30d", "txn_count_30d"]])
如果用户1001在1月14日发生了一笔交易,在1月16日又发生了一笔交易,而样本事件时间是1月15日,那么返回的总交易额只应包含1月14日及之前的交易,不能包含1月16日的交易。这就是时间点正确性在端到端流程中的体现。验证时可以手动查询底层数据,确认返回的特征值与预期一致。
五、常见陷阱与最佳实践
时间点正确性看似简单,但在实际落地中非常容易出错。一个常见的陷阱是使用数据写入特征存储的时间作为时间戳,而不是业务事件发生的时间。如果特征计算任务延迟运行,处理时间会晚于事件时间,虽然不会造成未来数据泄漏,但可能导致特征值在事件时间之后才可用,训练样本中出现空值或过期值。更严重的是,如果某条特征记录的事件时间被错误地设置成未来时间,时间点连接就会把未来信息带入训练数据。
另一个容易被忽视的问题是时区。事件时间戳来自不同系统时,可能存在时区不一致的情况。如果实体DataFrame中的event_timestamp使用UTC时间,而特征视图中的timestamp字段使用本地时间,两者的比较就会偏移若干小时。统一时区规范是保证时间点正确性的基础要求。此外,迟到数据也需要特别处理。流式场景下,事件可能在发生数小时甚至数天后才到达特征管道,如果直接覆盖历史特征值,可能影响已经生成的训练数据。建议为特征视图保留事件时间字段,并设置合理的TTL和回填策略。
最佳实践可以总结为四点。第一,始终为特征视图声明事件时间戳,并保证该时间戳来自业务事件本身,而不是数据处理时间。第二,在生成训练数据时,实体DataFrame中的event_timestamp要与标签时间严格一致,不要使用预测时间或导出时间。第三,定期回填历史特征,避免在线存储与离线存储出现不一致。第四,对关键特征进行时间点正确性抽样验证,建立自动化测试,防止数据管道变更引入泄漏。
Feast通过特征服务统一了特征的在线与离线消费方式,并通过时间点连接在引擎层面约束了训练数据的正确性。将这些机制理解透彻,才能在构建特征平台时真正发挥Feast的价值,避免模型训练中的数据泄漏问题。