3. 数据准备与特征工程基础

在 Databricks 上做模型生命周期管理,数据准备不是“把 CSV 读进来”这么简单。训练数据的可追溯性、特征的一致性、版本的可回滚性,直接决定了模型能否被复现、能否被审计、能否安全上线。本节围绕 Delta Lake 的存储能力、特征表(Feature Table)的组织方式、数据版本控制手段,以及训练/测试集划分策略展开,目标是让读者能够独立完成一套可复现的训练数据准备流程。

一、为什么用 Delta Lake 管理训练数据

Delta Lake 是构建在 Parquet 之上的开放表格式,它在数据文件之外维护一份事务日志(_delta_log),记录每次写入的元数据、schema 变更和提交信息。对训练数据而言,这带来三个关键能力:

  • ACID 事务:并发写入不会产生半成品文件,读取方要么看到旧版本,要么看到新版本,不会读到中间状态。
  • 时间旅行(Time Travel):通过 VERSION AS OF 或 TIMESTAMP AS OF 可以读取任意历史版本,这是训练数据可复现的基础。
  • Schema 强制与演进:写入时校验 schema,避免上游字段类型漂移悄悄污染训练集;需要新增特征列时可用 mergeSchema 显式演进。

一个典型的训练数据表可以这样建立。假设我们有一批用户行为日志,需要落成 Delta 表:

CREATE TABLE IF NOT EXISTS ml.training.user_events (
  user_id      STRING,
  event_time   TIMESTAMP,
  event_type   STRING,
  amount       DOUBLE,
  label        INT
)
USING DELTA
PARTITIONED BY (DATE(event_time))
LOCATION 'abfss://ml@storage.dfs.core.windows.net/training/user_events';

按日期分区的好处是:训练时若只取最近 90 天数据,Delta 的谓词下推会直接跳过无关分区,扫描量大幅下降。写入时建议使用 MERGE INTO 做幂等更新,而不是简单 INSERT,避免重复跑批导致样本重复。

二、特征表的创建与组织

特征表是把“原始数据 → 特征”的加工结果固化下来的表。它的核心价值在于:训练和推理使用同一份特征定义,避免训练/服务偏差(training-serving skew)。在 Databricks 中,特征表通常由 Feature Store 管理,但底层仍然是 Delta 表。

创建特征表时,需要明确三件事:主键(通常是 entity id + 时间戳)、特征列、以及特征的新鲜度要求。下面示例把用户近 7 天的交易聚合特征写入特征表:

CREATE OR REPLACE TABLE ml.features.user_txn_features AS
SELECT
  user_id,
  window_end AS feature_ts,
  COUNT(*)                       AS txn_cnt_7d,
  SUM(amount)                    AS txn_amt_7d,
  AVG(amount)                    AS txn_avg_7d,
  MAX(event_time)                AS last_txn_time
FROM (
  SELECT *,
         window(event_time, '7 days').end AS window_end
  FROM ml.training.user_events
  WHERE event_time >= current_timestamp() - INTERVAL 7 DAYS
)
GROUP BY user_id, window_end;

这里 feature_ts 是特征的时间戳,它必须严格早于标签发生的时间,否则会造成标签泄漏。特征表建好后,建议再建一张 feature_spec 元数据表,记录每个特征的含义、计算逻辑、负责人和更新频率,方便后续排查。

三、数据版本控制

Delta Lake 的版本控制是“表级”的,每次写入产生一个新版本号。训练时固定版本号,就能保证数据可复现。例如:

SELECT * FROM ml.features.user_txn_features VERSION AS OF 12;

如果只记得时间,也可以用时间戳:

SELECT * FROM ml.features.user_txn_features
TIMESTAMP AS OF '2024-06-01T00:00:00Z';

实践中建议在训练脚本里显式记录两个信息:特征表版本号和训练数据快照版本号,写入模型注册表的描述字段。这样当线上模型出现问题时,可以快速定位到当时用的数据版本。另外,Delta 的 DESCRIBE HISTORY 可以查看版本历史,RESTORE 可以回滚到指定版本,但回滚要谨慎,因为它会改变当前表状态。

四、训练/测试集划分策略

划分策略取决于数据的时间特性。对于时序数据,随机划分会导致未来信息泄漏到训练集,必须按时间切分。常见做法是:

  • 时间切分:用较早的数据训练,较晚的数据测试。例如前 80% 时间窗口训练,后 20% 测试。
  • 滚动窗口验证:在训练集内部再做多个时间窗口的交叉验证,评估模型在不同时间段的稳定性。
  • 分层抽样:如果标签极度不平衡,可在时间切分基础上对正负样本做分层,保证测试集有足够正例。

下面是一个按时间切分的示例,假设以 2024-05-01 为切分点:

CREATE OR REPLACE TEMP VIEW train_set AS
SELECT * FROM ml.features.user_txn_features
WHERE feature_ts < '2024-05-01';

CREATE OR REPLACE TEMP VIEW test_set AS
SELECT * FROM ml.features.user_txn_features
WHERE feature_ts >= '2024-05-01';

划分完成后,务必检查训练集和测试集的标签分布、特征分布是否一致。如果测试集正例比例远低于训练集,说明切分点选得不好,需要调整。此外,测试集应当“冻结”,在模型定稿前不要反复用它调参,否则测试集就变成了验证集。

建议在训练脚本中把数据版本、切分时间点、随机种子统一记录到 MLflow 的 run 参数里,形成完整的实验追溯链。
Delta Lake 训练数据准备与版本控制链路 原始事件日志 user_events (Delta) 特征加工 窗口聚合 / 编码 特征表 user_txn_features 版本控制 VERSION AS OF 时间 → 训练集(feature_ts < 2024-05-01) 用于拟合模型参数 测试集(≥ 2024-05-01) 冻结,仅用于最终评估 切分点 训练数据快照记录(写入 MLflow Run) • 特征表版本号:VERSION 12 • 切分时间点:2024-05-01T00:00:00Z • 随机种子:42 • 标签分布:train 8.2% / test 8.5%

最后强调一点:数据准备不是一次性工作。随着上游数据源变化、特征逻辑调整,训练数据表会不断产生新版本。把版本号、切分策略、特征定义都纳入模型注册表的元数据,才能让“这个模型是用什么数据训出来的”这个问题有确定答案。这也是 Databricks 模型生命周期管理区别于普通脚本训练的核心价值。