返回机制实验室

W01 · S01—S07 · 总 Day 9197

历史数据:当时发生,不等于当时已知

用迟到、回填与过期特征理解训练数据泄漏。

作者准备的学习示例 · 不计真实学习进度 · 不代表生产 / GPU / 真机结果

在仓库根目录运行;只输出合成示例,不写文件、不访问网络

npm run learning:p2 -- w01
跳到完整源码 ↓

核心问题

假设在第 10 分钟判断一个案例。数据库后来补录了一条“第 5 分钟发生”的信息。今天回放第 10 分钟的决策时,可不可以用它?答案取决于你要重建的是最终事实,还是决策者当时可见的信息。二者都可能有用,却不能混在同一份训练或评估输入中。

对应 S01~S07。先读事件时间、数据契约和 point-in-time 日课,再运行本例。目标是建立时间语义,不是搭建特征平台。

1. 两条时间轴与一个选择规则

eventAt 表示业务事件或特征的有效时间;availableAt 表示这个版本已经可供决策系统使用的时间。后者不一定等于原始日志的入库时间:如果还要做清洗、聚合或上线发布,真正可用会更晚。

本例在决策时刻 t 选择满足下列条件的最新记录:实体匹配、eventAt <= tavailableAt <= tt - eventAt <= TTL。先按事件时间倒序,再按可见时间倒序;完全相同则按 ID 确定顺序。该 tie-break 只是让示例可重复,真实冲突需要业务版本规则。

TTL 是相对历史决策时刻计算,不是相对今天。过期值应返回缺失状态,不自动当零。零可能是一个真实观察值;把缺失填成零会改变特征含义,也会影响下游模型。

2. 手算一遍输入

ID事件时刻可见时刻第 10 分钟能否看到
original5620
late-correction51580不能,是后来纠正
future-event121295不能,事件尚未发生
other-entity88100不能,属于另一个实体

只过滤事件时间,会选到数值 80。它没有来自“未来事件”,却来自“未来知识”,同样可能造成训练泄漏。加上可见时间约束后,第 10 分钟只能选 20。到第 30 分钟,TTL 为 10 时这些记录均过期,输出 null

npm run learning:p2 -- w01

先比较 eventTimeOnlyasKnownThen,再看 expired。代码不训练模型,因此“消除了这条回填泄漏”不能写成“模型已达到无泄漏或高质量”。标签构造、跨实体关联、数据切分仍可能存在其他泄漏。

3. 改一个变量,观察一个因果关系

late-correction.availableAt 从 15 改为 9,其他值不动。现在它在第 10 分钟之前已可见,预期会被选择。这个变化来自可见性,而不是模型变聪明。恢复原值后,把 TTL 从 10 改为 4,原始值也应过期。

兴趣延伸:给一次决策分别保留 eventCutoffknowledgeCutoff。前者决定讨论哪个业务时刻,后者决定用截至何时的知识重建历史。这样可以同时表达“当时知道什么”和“今天如何纠正历史”,而不覆盖旧记录。此处只是双时态设计练习,不要求写数据库。

4. 课程与官方材料

  • Feast Point-in-time joins:读 TTL 与 created timestamp 的说明。其默认事件时间匹配与可见时间限制不是一回事;本例显式要求后者。不要把教学字段名当成所有存储都支持的 API。
  • FSDL 2022:选 Data Management,想清楚来源、清洗、标注和版本怎样进入数据生命周期;不需要完成整套数据作业。

5. 向 AGI 与具身智能延伸

AGI 相关研究中的记忆回放同样需要区分“事件发生”和“Agent 获取信息”的时刻,否则可能高估规划与预测。具身场景还会增加传感器时钟、传输延迟和坐标变换版本。这里的分钟级表格只能说明信息可见性,不能证明处理了机器人实时同步。

可记三句:为什么回填会泄漏;缺失和零有什么差别;自己的案例中 available time 应由哪个环节定义。

配套日课:按需要补充理论

本实验贯穿一周,不要求一天做完。

可运行源码

src/learning/ai-systems/w01-point-in-time.ts · 构建时直接读取源文件,避免讲义代码与实现各自漂移。

export interface Feature {
  id: string
  entity: string
  eventAt: number
  availableAt: number
  value: number
}

// Minutes on a synthetic clock. availableAt means visible to the decision system.
export const features: Feature[] = [
  { id: 'original', entity: 'case-a', eventAt: 5, availableAt: 6, value: 20 },
  { id: 'late-correction', entity: 'case-a', eventAt: 5, availableAt: 15, value: 80 },
  { id: 'future-event', entity: 'case-a', eventAt: 12, availableAt: 12, value: 95 },
  { id: 'other-entity', entity: 'case-b', eventAt: 8, availableAt: 8, value: 100 },
]

export function historicalFeature(rows: Feature[], entity: string, at: number, ttl = 10) {
  if (ttl < 0) throw new Error('TTL must be non-negative')
  return rows.filter(row => row.entity === entity && row.eventAt <= at
    && row.availableAt <= at && at - row.eventAt <= ttl)
    .sort((a, b) => b.eventAt - a.eventAt || b.availableAt - a.availableAt || a.id.localeCompare(b.id))[0]
}

export function run() {
  const at = 10
  const eventTimeOnly = features.filter(row => row.entity === 'case-a' && row.eventAt <= at)
    .sort((a, b) => b.eventAt - a.eventAt || b.availableAt - a.availableAt)[0]
  return {
    clock: 'synthetic minutes', features, decisionAt: at,
    eventTimeOnly, asKnownThen: historicalFeature(features, 'case-a', at),
    expired: historicalFeature(features, 'case-a', 30) ?? null,
    lesson: 'At minute 10, the correction available at minute 15 must not be visible.',
  }
}