4. 特征存储与版本管理

特征存储(Feature Store)是特征工程体系中的核心基础设施,它解决了特征在生产环境中的共享、复用、一致性和版本追溯问题。在特征回放对比场景中,特征存储不仅需要保存特征值,还必须记录特征随时间的演变过程,即特征快照与版本标签。

4.1 特征仓库的核心概念

特征仓库是一个专门用于管理机器学习特征的系统层组件,它统一了特征的定义、存储、服务和发现流程。与传统的特征文件存储相比,特征仓库提供了以下关键能力:

能力维度 传统方式(CSV/Parquet) 特征仓库(Feast/自定义)
特征定义 分散在 Notebook 或脚本中 集中式特征注册表(Registry)
数据一致性 训练/推理可能使用不同版本 在线/离线特征自动对齐
时间回溯 需要手动管理时间分区 内置时间点查询(Point-in-Time Join)
版本管理 依赖文件名或 Git 记录 特征视图版本 + 时间戳标签
服务方式 批量导出后加载 低延迟在线服务 + 批量导出
关键认知:特征仓库的核心价值在于将特征从"数据工件"提升为"一等公民"——每个特征都有明确的元数据、时间范围和版本标识,这使得回放对比实验可以在精确的时间点上获取一致的特征快照。

4.2 使用 Feast 管理特征快照

Feast 是目前最成熟的开源特征仓库方案,它通过特征视图(Feature View)和数据源(Data Source)的组合来管理特征快照。每个特征视图可以关联一个时间戳字段,Feast 利用该字段实现时间点查询。

4.2.1 特征视图与时间戳定义

from datetime import datetime, timedelta
from feast import FeatureView, Field, FileSource
from feast.types import Float32, Int64, String

# 定义数据源,指定时间戳列
transaction_source = FileSource(
    path="s3://feature-bucket/transactions/",
    timestamp_field="event_timestamp",       # 事件时间戳
    created_timestamp_column="created_at",   # 记录创建时间
)

# 定义特征视图,设置 TTL 和版本标签
transaction_features = FeatureView(
    name="transaction_rolling_stats",
    entities=["user_id"],
    ttl=timedelta(days=90),                  # 特征有效期
    schema=[
        Field(name="avg_7d_amount", dtype=Float32),
        Field(name="count_30d_txns", dtype=Int64),
        Field(name="last_category", dtype=String),
    ],
    source=transaction_source,
    tags={"version": "v2.1", "team": "risk"}, # 版本标签
)
实践建议:在特征回放场景中,timestamp_field 应使用业务事件时间(如交易发生时间),而非数据加载时间。这能确保回放时获取的是"当时可见"的特征状态,避免未来信息泄露。

4.2.2 时间点查询与快照获取

Feast 的 get_historical_features 方法接受一个包含实体 ID 和查询时间戳的 DataFrame,自动为每个时间点匹配该时刻有效的特征快照:

import pandas as pd
from feast import FeatureStore

store = FeatureStore(repo_path="./feature_repo")

# 构造回放查询:每个样本需要指定查询时间点
query_df = pd.DataFrame({
    "user_id": ["U001", "U002", "U001"],
    "event_timestamp": [
        pd.Timestamp("2024-06-01 12:00:00"),
        pd.Timestamp("2024-06-02 15:30:00"),
        pd.Timestamp("2024-06-10 09:00:00"),
    ],
})

# 获取历史特征快照
feature_df = store.get_historical_features(
    entity_df=query_df,
    features=[
        "transaction_rolling_stats:avg_7d_amount",
        "transaction_rolling_stats:count_30d_txns",
    ],
).to_df()

print(feature_df.head())

4.3 自定义特征快照存储方案

当 Feast 等开源方案无法满足特定需求(如私有云环境、自定义存储引擎、特殊版本策略)时,可以构建轻量级自定义方案。核心设计包括三个层次:

  1. 存储层:使用 Delta Lake / Iceberg 等支持时间旅行的表格式,或按时间分区存储 Parquet 文件。
  2. 元数据层:记录每个特征快照的版本标签、生效时间、计算时间、特征 Schema。
  3. 查询层:提供按实体 + 时间戳获取快照的 API。

4.3.1 快照存储结构设计

# 特征快照目录结构示例
feature_snapshots/
  └── user_features/
      ├── version=v2.1/
      │   ├── snapshot_date=2024-06-01/
      │   │   └── data.parquet
      │   ├── snapshot_date=2024-06-02/
      │   │   └── data.parquet
      │   └── _snapshot_meta.json
      └── version=v2.2/
          ├── snapshot_date=2024-06-10/
          │   └── data.parquet
          └── _snapshot_meta.json
# _snapshot_meta.json 元数据示例
{
  "feature_view": "user_features",
  "version": "v2.1",
  "created_at": "2024-06-01T02:00:00Z",
  "valid_from": "2024-06-01T00:00:00Z",
  "valid_to": "2024-06-10T00:00:00Z",
  "features": ["avg_7d_amount", "count_30d_txns"],
  "computation_hash": "a1b2c3d4e5f6",
  "source_data_range": {
    "start": "2024-05-01",
    "end": "2024-05-31"
  }
}

4.3.2 快照查询实现

import pandas as pd
from pathlib import Path
import json

class CustomFeatureStore:
    def __init__(self, base_path: str):
        self.base_path = Path(base_path)

    def get_snapshot(self, feature_view: str, version: str,
                     entity_ids: list, timestamp: pd.Timestamp):
        """获取指定时间点的特征快照"""
        # 1. 定位版本目录
        version_path = self.base_path / feature_view / f"version={version}"

        # 2. 加载元数据,确定有效时间范围
        with open(version_path / "_snapshot_meta.json") as f:
            meta = json.load(f)

        valid_from = pd.Timestamp(meta["valid_from"])
        valid_to = pd.Timestamp(meta["valid_to"])

        if not (valid_from <= timestamp < valid_to):
            raise ValueError(f"Timestamp {timestamp} out of version range")

        # 3. 找到最近的快照日期
        snapshot_date = timestamp.strftime("%Y-%m-%d")
        snapshot_path = version_path / f"snapshot_date={snapshot_date}"

        if not snapshot_path.exists():
            # 回退到最近可用快照
            snapshot_path = self._find_nearest_snapshot(version_path, timestamp)

        # 4. 加载数据并过滤实体
        df = pd.read_parquet(snapshot_path / "data.parquet")
        return df[df["entity_id"].isin(entity_ids)]

    def _find_nearest_snapshot(self, version_path, timestamp):
        """查找不超过目标时间戳的最近快照"""
        snapshots = sorted(version_path.glob("snapshot_date=*"))
        target_str = timestamp.strftime("%Y-%m-%d")
        for snap in reversed(snapshots):
            if snap.name.split("=")[1] <= target_str:
                return snap
        return snapshots[0]  # 兜底返回最早快照
注意:自定义方案需要自行处理快照的原子性写入、并发读取、垃圾回收等工程问题。对于生产环境,建议优先评估 Feast 等成熟方案,仅在确实无法满足需求时采用自定义实现。

4.4 特征时间戳与版本标签设计

特征版本管理是回放对比实验准确性的基石。一个设计良好的版本标签系统需要同时表达三个时间维度:

  • 事件时间(Event Time):特征对应的业务事件发生时间,用于时间点匹配。
  • 快照时间(Snapshot Time):特征值被计算并存储的时间,反映特征的新鲜度。
  • 版本时间(Version Time):特征定义或计算逻辑变更的时间,用于版本追溯。

4.4.1 版本标签命名规范

标签类型 格式示例 说明
语义版本 v2.1.0 主版本.次版本.补丁,适用于特征逻辑变更
日期版本 2024Q2_v1 按季度/月份迭代,适用于周期性重算
实验标签 exp_rolling_window_7d 实验性特征,便于 A/B 对比
哈希版本 sha256:a1b2c3d4 基于特征计算逻辑的哈希值,自动检测变更

4.4.2 版本标签在回放中的应用

在特征回放对比实验中,版本标签用于精确控制每个时间点应该使用哪个特征版本:

# 回放对比实验:比较 v2.0 和 v2.1 版本的特征效果
experiment_config = {
    "experiment_id": "exp_004",
    "feature_view": "user_features",
    "versions": {
        "baseline": {
            "version_tag": "v2.0",
            "valid_range": ("2024-01-01", "2024-06-30"),
            "description": "原始 7 天滚动窗口"
        },
        "candidate": {
            "version_tag": "v2.1",
            "valid_range": ("2024-03-01", "2024-06-30"),
            "description": "优化后的 14 天加权窗口"
        }
    },
    "overlap_period": ("2024-03-01", "2024-06-30"),  # 两个版本都有效的重叠期
    "query_timestamps": pd.date_range("2024-03-01", "2024-06-30", freq="7D")
}

# 在重叠期内分别获取两个版本的特征快照
for version_info in experiment_config["versions"].values():
    snapshot = store.get_snapshot(
        feature_view=experiment_config["feature_view"],
        version=version_info["version_tag"],
        entity_ids=["U001", "U002"],
        timestamp=pd.Timestamp("2024-04-15")
    )
    # 进行回放对比分析...
最佳实践:建议在特征仓库中维护一个版本映射表(Version Registry),记录每个版本标签的生效时间范围、变更原因、关联代码提交 ID。这样在回放对比时,可以快速定位"某个时间点应该使用哪个版本",并追溯版本变更的上下文。

4.5 特征存储架构总览

下图展示了特征仓库在特征回放对比流程中的核心位置,以及各组件之间的数据流关系:

特征仓库架构与回放对比数据流 数据源 原始事件日志 业务数据库 特征仓库核心 特征注册表 快照存储 版本管理器 时间点查询 消费端 回放对比实验 在线推理服务 特征计算 快照服务 版本标签与时间戳设计 • 事件时间(Event Time):特征对应的业务发生时刻,用于 Point-in-Time Join • 快照时间(Snapshot Time):特征值被计算并持久化的时刻,反映数据新鲜度 • 版本时间(Version Time):特征定义/逻辑变更的时刻,用于版本追溯 • 版本标签格式:语义版本(v2.1.0)| 日期版本(2024Q2)| 实验标签(exp_7d) • 回放对比时,通过版本标签 + 时间戳精确锁定特征快照

4.6 版本管理策略对比

根据团队规模和业务复杂度,可以选择不同的版本管理策略:

策略 适用场景 优点 缺点
全量快照 小团队、特征数量少 实现简单,回放速度快 存储成本高,冗余大
增量快照 大规模特征、频繁更新 存储效率高,支持细粒度回溯 查询时需要合并,复杂度高
双版本并行 A/B 实验、模型对比 支持在线切换,对比直观 维护两套存储,资源消耗翻倍
按需计算 特征逻辑频繁变更 无需预存快照,始终使用最新逻辑 回放速度慢,依赖上游数据可用性
推荐组合:对于特征回放对比场景,建议采用"全量快照 + 双版本并行"的组合策略。对核心特征保留每日全量快照(TTL 90 天),在实验期间同时维护基线版本和候选版本,确保回放对比的准确性和可重复性。