qinghuan
← 回到知识探索

AI 直觉 / 动手实验

输入变了,模型就一定变差了吗?

给固定规则换两批模拟数据,分别检查输入分布和后来拿到的正确答案,做一份不会越界下结论的漂移报告。

展开本文目录
  1. 下载完整练习包
  2. 先把三个问题分开问
  3. 比较之前,先固定尺子
  4. 两个反例,比一张警报图更清楚
  5. 把无标签阶段和有标签阶段写进不同字段
  6. 警告出现后,下一步查什么
  7. 一份可信的报告,需要保留哪些边界
  8. 记住这些要点
  9. 别把例子用过头
  10. 自己试一小步
  11. 查原始资料
开始前,会这些就够了
  • 能计算比例与平均值
  • 会运行 Python 列表、函数和断言
  • 知道评估正确率需要真实标签

图片分类服务今天收到的暗色图片突然变多,监测页亮了警告。能立即说模型失效吗?反过来,图片亮度分布与昨天一样,能说预测仍然可靠吗?这两个判断都缺了一步:输入长什么样,与预测是否答对,需要分别检查。

本篇在已有分类指标知识上补一条上线后的检查路线。我们不用真实图片和神经网络,而用一个固定数字规则制造两个可核对的反例,交付输入统计和延迟标签结果分开的 JSON 报告。代码中的百分比只是人工构造数据的算术结果。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本;制作时实测 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • drift_report.py:完整示例
  • README.md:环境、范围与独立练习
  • observed.stdout.txt:制作时的观察记录
  • verification.json:对应源码的验证范围
  • SHA256SUMS.txt:文件内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B drift_report.py

解压后在含示例文件的文件夹打开 PowerShell,按命令运行。先预测结果,再与当次输出对照。 程序会在当前目录写入教学报告,建议使用独立练习文件夹。 教学数据与标准库公式,不代表 PyTorch 执行、训练效果或真实模型准确率。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

先把三个问题分开问

第一个问题是“来了什么样的输入”。例如原先一百张图片里有五十张偏暗,今天有八十张偏暗。输入各类所占的比例发生变化,可以记为输入分布变化。它值得调查,却没有告诉我们这三十张新增的暗图是否更难,或模型是否能正确识别。

第二个问题是“同样输入现在对应什么答案”。假设工单长度没变,但公司修改了紧急工单的业务定义,原来无需加急的情形现在需要加急。这时输入直方图可能保持原样,输入与标签的关系却变了。第三个问题才是“当前规则对这批记录答对多少”。需要标签或其他可靠验收结果才能直接计算。

真实项目里三种变化可能同时出现,也可能互相抵消。本实验特意把它们拆开;它展示的是判断边界,并不声称仅凭一个输入字段就能给所有变化分类。

比较之前,先固定尺子

把数值想成一张图片的简化亮度,范围先约定为零到十。我们提前固定四个区间:[0,2.5)、[2.5,5)、[5,7.5)、[7.5,10],最后一个包含十。另设低于范围、高于范围和缺失三个格子。新旧窗口必须用同一套边界,否则“每格各占多少”就失去共同含义。

程序先数每格数量,再除以当前窗口的总记录数。因此一百条与两百条记录也能比较比例,但数量仍要保留:十条记录里多来一条,与一万条里多来一条的波动尺度不同。这里采用每格比例差的绝对值之和再除以二,称为总变差距离;零表示这些格子的比例完全相同,一表示完全落在不重叠的格子里。

这个距离是对已经分箱的分布计算的。它忽略同一格子内部的变化,也不检查多个字段之间的关系。我们不设通用报警线,不计算显著性,不把零距离写成“两批原始数据完全一致”。

为什么最后除以二?比例总和都是一;某些格子增加的份量,必然来自其他格子减少的份量。取绝对值后,加和把这次转移的两边都数了一遍。参考的四格各占四分之一,新窗口前两格各占十分之四、后两格各占十分之一,正好有十分之三的份量换了格子。这是确定的算术解释,不是对未知总体做统计推断。

还要区分组成和总量:若各格数量都翻倍,比例和距离完全不变,但服务要处理的请求可能翻倍。输入组成监测不能替代流量、延迟或容量监测;这也是报告保留原始计数而不只输出一个距离的原因。

两个反例,比一张警报图更清楚

参考窗口有一百条记录,数值一、三、七、九各二十五条。固定预测规则是:数值至少五就预测一,否则预测零。第一组新数据把一和三各改为四十条,七和九各改为十条;正确答案仍然遵守原规则。分布距离变成0.3,但所有记录仍能被固定规则答对。这里只能说明“出现输入变化”不必然导致质量下降。

第二组新数据完全复制参考输入,却把正确答案改为相反规则。监测器看到的数值分布没有任何差异,距离为零;后来拿到标签时,会发现原预测全部错误。这里只能说明“没有发现输入变化”不足以证明质量稳定。这个极端翻转是为了把反例讲清,不是现实中标签通常怎样变化的预测。

两组都没有更新规则。若边改模型边换数据,就无法用这份小实验单独解释输入和标签各自的影响。保持预测函数固定,是让每个结论有明确依据的关键。

把无标签阶段和有标签阶段写进不同字段

下面脚本的第一阶段只接收输入值,输出记录数、分箱数量、比例和距离,并把质量状态固定写成 unknown_without_labels。第二阶段才使用自拟标签计算正确率,写入另一个字段。这样报告不会因为有一条漂移警告,就自动填上一项从未测量的质量结论。

程序还检查空窗口、非有限数字和字符串数字。缺失值保留为独立格子,但玩具预测函数没有缺失值处理规则,因此不能悄悄把缺失当零后继续报正确率。下载后先读清这些拒绝条件,再运行并保存自己实际得到的报告。

展开收起代码 · 小实验python · 97 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Original, deterministic teaching simulation; no learned model or external data."""
import json
import math
from pathlib import Path

BUCKETS = ("below_0", "0_to_2.5", "2.5_to_5", "5_to_7.5", "7.5_to_10", "above_10", "missing")


def bucket(value):
    if value is None:
        return "missing"
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
        raise ValueError("feature must be finite numeric or None")
    if value < 0:
        return "below_0"
    if value < 2.5:
        return "0_to_2.5"
    if value < 5:
        return "2.5_to_5"
    if value < 7.5:
        return "5_to_7.5"
    if value <= 10:
        return "7.5_to_10"
    return "above_10"


def summarize(values):
    if not values:
        raise ValueError("empty observation window")
    counts = dict.fromkeys(BUCKETS, 0)
    for value in values:
        counts[bucket(value)] += 1
    return {"count": len(values), "counts": counts,
            "proportions": {name: count / len(values) for name, count in counts.items()}}


def total_variation(left, right):
    return sum(abs(left[name] - right[name]) for name in BUCKETS) / 2


def accuracy(values, labels):
    if (not values or len(values) != len(labels)
            or any(type(label) is not int or label not in (0, 1) for label in labels)):
        raise ValueError("aligned binary labels required")
    for value in values:
        bucket(value)  # Enforce the same feature contract as the input-only monitor.
    if any(value is None for value in values):
        raise ValueError("toy predictor has no missing-value rule")
    return sum(int(value >= 5) == label for value, label in zip(values, labels)) / len(values)


def must_reject(action):
    try:
        action()
    except ValueError:
        return
    raise AssertionError("invalid input accepted")


reference = [1] * 25 + [3] * 25 + [7] * 25 + [9] * 25
changed_mix = [1] * 40 + [3] * 40 + [7] * 10 + [9] * 10
changed_rule = list(reference)
windows = {"reference": reference, "changed_mix": changed_mix, "changed_rule": changed_rule}
baseline = summarize(reference)
monitor = {}
for name, values in windows.items():
    summary = summarize(values)
    monitor[name] = {**summary,
                     "input_tv": total_variation(baseline["proportions"], summary["proportions"]),
                     "quality_status": "unknown_without_labels"}

# The monitoring stage above never receives these labels.
labels = {"reference": [int(x >= 5) for x in reference],
          "changed_mix": [int(x >= 5) for x in changed_mix],
          "changed_rule": [int(x < 5) for x in changed_rule]}
delayed_quality = {name: accuracy(values, labels[name]) for name, values in windows.items()}
assert math.isclose(monitor["changed_mix"]["input_tv"], 0.3)
assert monitor["changed_rule"]["input_tv"] == 0
assert delayed_quality == {"reference": 1.0, "changed_mix": 1.0, "changed_rule": 0.0}
assert all(item["quality_status"] == "unknown_without_labels" for item in monitor.values())
assert summarize([None, -1, 11])["counts"]["missing"] == 1
assert [bucket(x) for x in (0, 2.5, 5, 7.5, 10)] == [
    "0_to_2.5", "2.5_to_5", "5_to_7.5", "7.5_to_10", "7.5_to_10"]
must_reject(lambda: summarize([]))
must_reject(lambda: summarize([float("nan")]))
must_reject(lambda: summarize([float("inf")]))
must_reject(lambda: summarize(["3"]))
must_reject(lambda: accuracy([float("nan")], [0]))
must_reject(lambda: accuracy([1], [False]))
must_reject(lambda: accuracy([1], [0.0]))
report = {"scope": "synthetic scalar rule; no training; no production-quality claim",
          "bin_policy": "fixed in advance; left closed, right open except final bin includes 10",
          "monitor_without_labels": monitor, "quality_after_labels_arrive": delayed_quality}
Path("drift-report.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
for name in windows:
    print(name, "input_tv=", round(monitor[name]["input_tv"], 3), "later_accuracy=", delayed_quality[name])
print("PASS: drift and quality are separate; drift-report.json saved")

怎样运行:保存为 drift_report.py,在自己的空练习目录运行 python drift_report.py。只用 Python 3 标准库,不下载数据或安装依赖。 2026-10-13 已用 3.14.0 执行这份示例并通过自带断言;下方为当次输出。教学数据与标准库公式,不代表 PyTorch 执行、训练效果或真实模型准确率。

运行后对照结果
reference input_tv= 0.0 later_accuracy= 1.0
changed_mix input_tv= 0.3 later_accuracy= 1.0
changed_rule input_tv= 0.0 later_accuracy= 0.0
PASS: drift and quality are separate; drift-report.json saved

警告出现后,下一步查什么

先查数据链路:采样时间窗是否一致、字段单位是否改变、缺失值是否突然增多、图片转换是否升级。假如亮度从零到一改成零到二百五十五,重训模型并不能替代修复单位错误。再按实际业务分组查看变化来自哪个来源、设备或时段,保留数量,避免总体比例遮住局部情况。

若输入变化是真实业务变化,就安排取样标注或等待标签回流,用事先约定的指标检查当前表现。标签往往来得不齐:只拿到处理快的工单,不一定代表所有工单。报告应记清标签覆盖率、观察窗口与未到齐的部分,不能把暂时没有答案的记录都算成成功。

TFDV 文档把数据统计比较与告警阈值写成可配置流程;Google 的监测教程另外要求观察实际效果和关键子群。本篇把二者接成一个调查顺序,脚本并没有实现 TFDV,也没有自动重新训练或上线模型。

一份可信的报告,需要保留哪些边界

交付时至少保存参考数据版本、当前窗口范围、字段含义、分箱边界、各格数量、距离,以及标签是否可用。输入报告可以较早生成;质量报告需要另记标签来源和完成程度。若更换参考窗口,标注变更日期和理由,不要为了让警告消失临时选择一批更相似的数据。

本次可执行产物只覆盖一个数值字段和一个确定规则,没有抽样误差估计、多字段联合检测、图像表示或真实业务损失。它的价值是让“看到了变化”“知道变化有害”和“已经修复”成为三种不同的报告状态。后续接入真实系统时,每一项状态都需要新增证据。

记住这些要点

  • 输入分布变化与质量下降不是同一个命题。
  • 无标签监测应明确报告质量未知。
  • 固定分箱和参考窗口,保存数量,才能解释距离。
  • 真实质量检查还要核对标签覆盖率和业务切片。

别把例子用过头

  • 把漂移距离当作错误率或置信度。
  • 新旧窗口各自重新分箱后直接比较。
  • 输入直方图一样,就宣称模型没有退化。
  • 为消除告警自动更新参考或重训,未查数据故障。

自己试一小步

如果同一个分箱里的数值整体移动,现有监测会不会漏掉它?

  1. 保留原参考与边界,自拟一批只在各自格子内部移动的数值。
  2. 运行当前监测,先记录自己预测的结果,再保存实际输出。
  3. 增加一种能补充观察格子内部变化的统计,但不要删除原报告。
  4. 为有标签与无标签两种情况分别写一段结论。

做到这些,就算完成

  • 原始与新增统计使用同一批记录,并保留样本数。
  • 解释新统计能发现什么、仍会遗漏什么。
  • 没有标签时不填写准确率。
  • 提交修改脚本、报告和至少一个监测仍不能识别的反例。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

  • TensorFlow Data Validation:漂移检测与训练/服务差异 (新标签页)↗TensorFlow 官方文档 · 英文

    只读 Training-Serving Skew Detection 与 Drift Detection:数据窗口比较、统计距离、阈值需要领域知识;本篇自写总变差距离并非该工具的内置实现复刻。

    2026-10-13 核对在线正文;教程可免费阅读,无须安装 TensorFlow。

  • Monitoring pipelines:模型上线后怎样监测 (新标签页)↗Google for Developers · 英文

    读 Write a data schema、Check metrics for important data slices、Use real-world metrics 和 Check for training-serving skew;支持把数据检查与实际效果分开。

    2026-10-13 核对在线正文;网页标注更新日期为2025-10-16,未运行页面练习。

  • Failing Loudly:检测数据集变化的原始研究 (新标签页)↗Rabanser、Günnemann 与 Lipton · 英文

    本次读摘要:作者分别研究变化检测、代表变化的样例与变化是否有害。用于拓展阅读,不据此声称本文小距离方法优于其他检测器。

    2026-10-13 核对作者、摘要与v4版本信息;本次未复现实验,也未据摘要转述论文全部结论。