返回 AICAP-180
B14 · Day 134外部 ground-truth AML eval

loader 与去偏

Day 133 把 AMLworld 行结构校验干净了,但有一根刺没拔:Is_Laundering 极度不平衡(<0.1% 正例)。今天做的 loader 与去偏(debiasing)就是拔这根刺——用流式读取避免一次性载入大文件,用分层平衡抽样(stratified balanced sampling)把均衡子集喂给裁判,让召回率和置信区间可解释。

阶段: B14 · 外部 ground-truth AML eval(Day 131-140) 标签: #stratified-sampling #class-imbalance #streaming #reproducibility

今日导引(由浅入深)

Day 133 把 AMLworld 行结构校验干净了,但有一根刺没拔:Is_Laundering 极度不平衡(<0.1% 正例)。今天做的 loader 与去偏(debiasing)就是拔这根刺——用流式读取避免一次性载入大文件,用分层平衡抽样(stratified balanced sampling)把均衡子集喂给裁判,让召回率和置信区间可解释。

它在 B14 的位置是「把干净行变成均衡评测集」,是 Day 135 LLM judge 校准和 Day 136 混淆矩阵的直接输入。如果跳过去偏,Day 136 算出的 recall 会因为正例太少而方差爆炸、毫无意义。

放到 B1→B18 能力曲线上:这是「评测集工程」的核心一环,对应数据科学里处理不平衡数据的标准纪律,也是 AI Solutions Architect 在「评测可信度」上的硬功夫。最小可判定产出:写 src/aml/amlworldLoader.ts,固定 seed 抽 N≈300(约 150 正 / 150 负)导出 balanced_300.json,loader 单测断言正负各 150±5、seed 可复现。

1. 机理精读

不平衡为什么毁掉评测。 Is_Laundering 正例 <0.1%,若直接随机抽 N=300 裁判,期望正例约 300 × 0.001 = 0.3 条——recall 的分母 TP+FN 几乎为 0,要么算出 NaN,要么单条样本的对错就把 recall 从 0 翻到 1,方差爆炸、毫无统计意义。

更隐蔽的是多数类(normal)会把整个评测淹没:一个「全判 normal」的裁判在原始分布上 accuracy ≈ 99.9%,却 recall = 0——这正是 Day 136 强调「不平衡场景别只报 accuracy」的根因。accuracy 在极端不平衡下是个谎报指标。

对策:分层平衡抽样。Is_Laundering 分层(正例一层、负例一层),各层独立抽样,凑成 N≈300 的均衡子集(约 150 正 / 150 负)。这样做的收益:

  1. recall 分母稳定(~150 个真正例),单点估计可靠;
  2. Day 137 的 bootstrap CI 更窄、更可解释——正例越多,区间越紧;
  3. 裁判不会被多数类带偏——judge 看到的正负比例 1:1,不会因为「反正大多是 normal」而退化成常判 normal。

代价是:均衡子集的先验偏离真实分布——所以在均衡集上测出的是「条件召回/精度」,FPR 和 precision 的绝对值需结合真实基率(base rate)换算后才能谈业务产能,这一点要在报告里诚实标注,不能把均衡集上的 precision 当真实环境的 precision。

为什么固定 seed。 eval 必须可复现。抽样用伪随机,固定 seed(如 mulberry32(seed))保证「同一份 CSV + 同一 seed → 同一份 balanced_300.json」。可复现性是 2026 eval-rigor 共识的硬要求,也是「别人能验证你的数字」的前提(与 first-party / 可外部验证原则一致——一个不可复现的评测集,等于没有评测集)。

为什么流式读取。 AMLworld HI-Small 可能数百万行,一次性 readFileSync + split('\n') 会爆内存。流式(line-by-line stream)逐行读、逐行校验、按层做蓄水池抽样(reservoir sampling),内存占用与文件大小解耦——无论文件 100MB 还是 2GB,常驻内存只有两个 ~150 容量的桶。这是工程纪律,不是可选项。

关键边界:去偏 ≠ 过采样。 我们做的是抽样平衡(从真实正/负例里各抽固定数量,对多数类做下采样),不是合成过采样(如 SMOTE 用 k 近邻插值造新正例)。

SMOTE 会引入数据集里不存在的合成样本,污染外部效度——评测集必须全是真实带标样本,否则又退回到「测自己造的数据」的循环陷阱(Day 131 的病又回来了)。所以 eval 集严禁 SMOTE / 合成过采样,只许下采样多数类。(训练阶段可酌情用 SMOTE,但 eval 阶段绝对禁止——这是两个不同的纪律。)

三种平衡策略的取舍对照

策略做法外部效度eval 可用
下采样多数类从真实负例抽 ~150保留(全真实样本)✓(本任务选此)
过采样少数类(复制)真实正例重复采样到 150部分保留(无新样本但有重复偏置)△(CI 会被重复样本压窄、虚假自信)
合成过采样(SMOTE)k 近邻插值造新正例破坏(引入不存在的样本)✗(污染外部效度)

本任务选「下采样多数类」:负例够多(~500 万),抽 150 不会样本枯竭;正例 ~5000 也足够抽 150 而无需重复。只有当正例少到抽不满 150(如 <150 真正例)时,才需考虑放宽到全量正例 + 等量负例的非对称设计——AMLworld HI-Small 不会触发这个边界。

2. 推导 / 手算 / 代码走读

注意:src/aml/amlworldLoader.ts 当前尚未创建(本日新建),下面是要建的设计;其下游 src/aml/groundTruthEval.tsbinaryEval/binaryEvalWithCI)是真实既有代码(已 Read 确认)。

要新建的 amlworldLoader.ts 设计要点:

  1. 流式读取:用 Node 的 readline/stream 逐行读 CSV,避免 readFileSync 全量载入;逐行交给 Day 133 的 validateRows/zod schema 校验,跳过或报告脏行。
  2. 分层蓄水池抽样:维护两个桶(正例、负例),用固定 seed 的 RNG 做蓄水池抽样(reservoir sampling),各抽 ~150,单遍流式即可,无需全量驻留。蓄水池抽样保证每个样本被选概率均等,即便不知道总行数也成立。
  3. 固定 seed RNG:复用测试里已验证的 mulberry32(seed) 模式(见 src/aml/__tests__/groundTruthEval.test.ts 第 4-12 行的 mulberry32 实现:a = seed >>> 0 起步,每次更新 aMath.imul 混合)——确定性、可注入、可复现。
  4. 导出格式:写出 balanced_300.json,每条形如 { ...AmlworldRow, label }labelisLaundering 派生:1→可疑类、0→'normal'),供 Day 135 judge 与 Day 136 binaryEval 直接消费。
  5. 可审计:保留原始 from/to/amount 字段,落盘时记录来源文件 + seed + 抽样数 + 时间戳,便于复现与审计。

下游对接(真实代码走读,src/aml/groundTruthEval.ts

  • balanced_300.json 里每条提取出 {label, predicted} 后喂给 binaryEval(items, 'normal')(第 38 行)。该函数逐条判定 actualPos = i.label !== normalLabelpredPos = i.predicted !== normalLabel,累加 tp/fp/tn/fn(第 43-50 行),返回 recall/precision/fpr/f1/n(第 51-61 行)。
  • binaryEvalWithCI(第 65 行)再做 bootstrap CI(默认 bootstrap=2000,可注入 rng)。
  • 所以 loader 的产物必须让 label 字段和 binaryEvalnormalLabel='normal' 约定对齐——负例的 label 必须正好是字符串 'normal',否则会被当成阳性类。

手算:为什么 150/150 而非 30/270。 设裁判真召回 r=0.8。

  • N_pos=150 时,recall 标准误 ≈ √(r(1-r)/150) = √(0.16/150) ≈ 0.033;
  • N_pos=30 时,≈ √(0.16/30) ≈ 0.073。

正例越多、recall 区间越窄——这就是把正例抽到 ~150 的统计理由。150/150 在「区间够窄」和「裁判 token 成本可控」之间取平衡。

手算:均衡集 precision 为何不能直接外推(base-rate 换算)。 这是 Day 134 必须诚实标注的核心陷阱。设裁判 recall=0.8、FPR=0.1:

  • 均衡集(150 正/150 负)上,precision = TP/(TP+FP) = (0.8×150)/(0.8×150 + 0.1×150) = 120/135 ≈ 0.89——看起来很好。
  • 但真实分布正例率仅 0.1%(10000 笔里 10 正 / 9990 负),同样 recall=0.8、FPR=0.1 时:TP=0.8×10=8,FP=0.1×9990=999,真实 precision = 8/(8+999) ≈ 0.008

同一个裁判,均衡集 precision 0.89、真实环境 0.8%——相差百倍。所以报告里 precision/FPR 的绝对值必须标「均衡集条件下」,谈业务产能(每天人工要复核多少误报)时要用真实 base rate 重算。recall 不受 base rate 影响(分母只含真正例),可以直接报;precision/FPR 的绝对值则强依赖基率。这也解释了为什么 AML 业务偏好高 recall、容忍高 FP(Day 136 主题)——在极端不平衡下,precision 天然被基率压低,硬追 precision 会牺牲 recall。

3. 今日实战

  1. 新建 src/aml/amlworldLoader.ts:CSV 流式读取(readline)+ 按 Is_Laundering 分层 + 固定 seed 的平衡抽样,导出 balanced_300.json(约 150 正 / 150 负)。
  2. 复用 groundTruthEval.test.ts 里的 mulberry32(seed) 作为可注入 RNG,保证可复现。
  3. 新增 loader 单测:先对合成 CSV fixture(造若干正/负行)跑绿,断言:(a)正负各 150±5;(b)同一 seed 两次抽样结果一致(可复现);(c)流式不全量载入(验证逻辑路径不依赖总行数)。
  4. 对真实 AMLworld 跑一次,落盘 balanced_300.json(git-ignore 大数据,只提交统计/来源记录)——标「待数据」。

4. 今日实测 / 产出

  • 状态待数据(下载公开集)
  • 产出(待数据后):落盘 balanced_300.json + loader 单测绿(正负各 150±5)。
  • 可先做的部分:流式 loader + 抽样逻辑可先对合成 CSV fixture 写测试跑绿(不 gated,今天即可完成代码 + 合成测试)。
  • 诚实标注:对真实 AMLworld 的 300 子集落盘需数据下载后产出,标「待数据」。src/aml/amlworldLoader.ts本日新建文件(当前仓库中尚不存在,已确认)。

5. 常见误区 / 陷阱

  • 直接随机抽样不分层:300 条里几乎无正例,recall 失真——必须按 Is_Laundering 分层。
  • 用 SMOTE / 合成过采样平衡 eval 集:会污染外部效度,把真值评测退化成测合成数据;eval 集只许下采样真实样本(训练可用,eval 禁用)。
  • 忘记固定 seed:抽样不可复现,别人无法验证你的数字,违反可外部验证原则。
  • 把均衡子集的 precision/FPR 绝对值当真实业务值:均衡先验偏离真实基率,需结合 base rate 换算后才谈产能,报告里要标注。
  • readFileSync 全量载入大 CSV:内存爆炸——必须流式 + 蓄水池抽样。

6. 学习资源(每条带 YYYY-MM)

  • Anthropic《Demystifying evals》(2026-01)——eval 集需控制类别分布、报告可复现的方法论。
  • IBM AMLworld dataset card, Kaggle (2024-04)——Is_Laundering 分布与规模。
  • He & Garcia《Learning from Imbalanced Data》(IEEE TKDE, 2009-09)——不平衡数据抽样与去偏经典综述(概念打底,非主线)。
  • Vitter《Random Sampling with a Reservoir》(ACM TOMS, 1985-03)——蓄水池抽样原始算法(单遍流式抽样的理论基础)。
  • 本仓代码:src/aml/groundTruthEval.ts(下游 binaryEval/binaryEvalWithCI,已 Read)、src/aml/__tests__/groundTruthEval.test.tsmulberry32 可复现 RNG 范式,已 Read)。

SOTA检查 (2026-06 更新)

  • 当前主流:流式 + 分层抽样是不平衡数据评测的标准做法,无替代。
  • 可复现性:固定 seed 的可复现性要求与 2026 eval-rigor 共识一致。
  • 是否仍 SOTA:是。下采样多数类 + 固定 seed 仍是 eval 集构建首选。
  • 过时黑名单:避免用 SMOTE 等合成过采样做 eval 集——会污染外部效度(训练时可酌情用,eval 时禁用);避免不固定 seed 的一次性抽样。
  • 下次复查点:Day 135 judge 校准前,确认 balanced_300.jsonlabel 字段与 binaryEvalnormalLabel='normal' 约定一致(负例 label 必须精确为 'normal')。

衔接

  • 昨天:Day 133 — schema 对齐(AMLworld 行映射到 AmlCase、运行时列校验)。
  • 今天:流式读 + 按 Is_Laundering 分层平衡抽样,固定 seed 导出 balanced_300.json(约 150 正/150 负)。
  • 明天:Day 135 — LLM judge 校准(用 NAMED deepseek-v4-flashIs_Laundering 做二分类,30 条手验子集算一致率)。