先别改模型权重——SemIf 决策漂移先查输入分布

文章导读
看到 SemIf 的决策结果漂移,第一反应去调权重或重训,通常不是最省时间的一步。SemIf 的输出由输入、模型、运行环境三者共同决定,输入分布先变了,同一套权重在新输入上给出不同决策,是很正常的事,这时改权重等于在一个还在移动的靶子上调参。更稳的顺序是先做输入侧对照:漂移前后的请求样本、对应的 SemIf 输出记录、以及可观察的输入统计特征。如果输入侧能解释漂移,模型侧就不用动;解释不了,再往依
📋 目录
  1. A 收集漂移前后的输入样本和对应 SemIf 输出记录
  2. B 用简单统计或可视化对比输入长度、词汇分布、字段缺失
  3. C 固定输入分布后重跑 SemIf 观察输出是否稳定
  4. D 若输入变化解释不了漂移再检查运行环境与依赖版本
  5. E 输出归因结论和下一步验证命令
A A

看到 SemIf 的决策结果漂移,第一反应去调权重或重训,通常不是最省时间的一步。SemIf 的输出由输入、模型、运行环境三者共同决定,输入分布先变了,同一套权重在新输入上给出不同决策,是很正常的事,这时改权重等于在一个还在移动的靶子上调参。更稳的顺序是先做输入侧对照:漂移前后的请求样本、对应的 SemIf 输出记录、以及可观察的输入统计特征。如果输入侧能解释漂移,模型侧就不用动;解释不了,再往依赖版本和运行环境查。

SemIf 决策漂移建议先数据后模型:抽漂移前后的输入样本与输出记录做对照,比对输入长度、词汇分布和字段缺失,再把输入固定住重跑。若固定输入下输出稳定,问题多半在输入侧;若仍漂移,再查依赖版本、运行参数和调用路径。这套顺序只是把成本低、可回退的一步放在前面,不能保证定位到唯一原因,多因素同时变化时需要一次只改一个变量。

收集漂移前后的输入样本和对应 SemIf 输出记录

先建立对照数据,才能确认漂移到底发生在哪些请求上、是不是集中在某个流量来源或某个字段形态。建议从 SemIf 调用入口附近的结构化日志里抽,漂移前后各取一段,尽量选同一时段、同一来源的请求,不要只取高峰期或只取灰度流量,否则对比结论会被采样偏差带偏。

需要落下来的字段至少包括:

  • request_id:用于把输入和输出对齐,缺了它后面没法回放。
  • ts:请求时间,用来切分漂移前后窗口。
  • input_hash:输入正文的哈希,用来判断两次跑的输入是否真的相同。
  • input_len:输入长度(字符数或 token 数,二者取其一并保持一致)。
  • input_snippet:输入摘要,截取前 100~200 字,便于人工看形态。
  • sem_if_label 与 sem_if_score:SemIf 的输出摘要,即决策标签和分数。
  • branch / route:实际走到的分支,用来判断漂移是否改变了调用路径。

如果现有日志没有这些字段,可以先在调用入口补一段结构化日志,字段名按你们现有的日志体系替换:

{
  "request_id": "...",
  "ts": "<ISO8601 时间>",
  "input_hash": "sha1(text)",
  "input_len": 128,
  "input_snippet": "text[:120]",
  "sem_if_label": "...",
  "sem_if_score": 0.0,
  "branch": "..."
}

日志量大的话,先按 request_id 抽样,两侧各取几百条即可,重点是把「哪一类请求漂了」这件事看出来,不是把全量都拉下来。

用简单统计或可视化对比输入长度、词汇分布、字段缺失

这一步只判断输入侧有没有可观察的变化,不需要复杂模型。用 pandas 跑一遍分位数和缺失率就够了:

import pandas as pd

before = pd.read_json("semif_before.jsonl", lines=True)
after  = pd.read_json("semif_after.jsonl",  lines=True)

for name, df in [("before", before), ("after", after)]:
    print(name, "len p50/p95:", df["input_len"].quantile([.5, .95]).to_dict())
    print(name, "missing:", df.isna().mean().round(3).to_dict())
    print(name, "label dist:", df["sem_if_label"].value_counts(normalize=True).round(3).to_dict())

不写脚本也可以:把两份 jsonl 导出成 CSV,用电子表格做三件事——对 input_len 列算中位数和第 95 百分位、对关键结构化字段做透视表看取值占比、对每一列算空值比例。词汇分布可以先用粗粒度做法,取高频词或关键枚举值的前若干项做占比对比。

结果怎么读:input_len 的第 95 百分位明显右移、某个字段的缺失比例上升、出现了此前没有的枚举值或新增说法,都说明输入分布发生了变化。反过来,如果两次的分布几乎重合,输入侧就解释不了这次漂移,可以往下走。样本量小的时候,中位数差几个点不要当成结论。

固定输入分布后重跑 SemIf 观察输出是否稳定

把输入固定住,就排除了输入变化这一项,剩下的差异只能来自模型或环境。做法是拿同一份固化后的输入文件,在尽量不变的条件下重复跑若干次,输出分别落盘:

先别改模型权重——SemIf 决策漂移先查输入分布
for i in $(seq 1 5); do
  <your_semif_run_cmd> \
    `--input` fixed_inputs.jsonl \
    `--out`   runs/out_${i}.jsonl \
    `--seed`  ${i}
done

# 对比两次输出是否一致
jq -S . runs/out_1.jsonl > /tmp/a.json
jq -S . runs/out_2.jsonl > /tmp/b.json
diff /tmp/a.json /tmp/b.json | head

替换项:<your_semif_run_cmd> 换成你们现有的调用入口,CLI 离线脚本或本地服务请求都行;如果是服务形式,就把同一批请求按相同顺序重放并记录返回。fixed_inputs.jsonl 用上一步抽出的、内容固定的输入,注意不要中途改预处理逻辑。

两种结果的含义不同:同一输入多次输出不一致,说明模型或环境本身带随机性或版本差异;同一输入输出一致、但和漂移前的决策不同,那就要回到输入侧,再比一次那批输入的具体差异。

若输入变化解释不了漂移再检查运行环境与依赖版本

到这一步才把范围从数据侧扩到环境侧,需要并排对照的项目通常是这几类:

  • 依赖版本:SemIf 本体、推理框架、tokenizer、数值计算库是否升级或降级。
  • 运行参数:随机种子、采样开关、batch size、最大长度与截断策略、超时重试设置。
  • 调用路径:入口服务版本、预处理中间件、特征拼装顺序、是否命中缓存、灰度开关位置。
  • 运行时:机器型号、驱动版本、线程数、是否走了不同的量化或加速路径。

最省事的做法是导出两份环境清单再 diff:

python -m pip freeze > env_after.txt
diff env_before.txt env_after.txt

需要留意的是,缓存命中差异和灰度开关切换经常伪装成「模型漂移」,跑固定输入之前先确认这两项在漂移前后是否一致,否则后面的对照都不干净。

输出归因结论和下一步验证命令

排查结果要落成一段可执行的结论,而不是停在「可能是输入变了」这种猜测上。可以用下面这个模板填空:

  • 现象:在 <时间段/流量来源> 上,SemIf 的 <标签/分支> 出现变化。
  • 输入侧:长度、词汇、缺失字段 <有/无> 可观察变化,具体为 ……
  • 固定输入重跑:同一输入下输出 <一致/不一致>。
  • 环境侧:依赖与运行参数 <有/无> 差异,具体为 ……
  • 归因:当前证据支持 <输入侧变化 / 环境差异 / 尚不能确定>。
  • 下一步:<动作>,验证方式 <命令或对比维度>。

填完结论后再跑一条可重复的验证命令,把同一批固定输入在当前环境下重跑一次,和基线对比:

<your_semif_run_cmd> `--input` fixed_inputs.jsonl \
  `--out` runs/recheck_$(date +%s).jsonl

jq -r '[.request_id, .sem_if_label] | @tsv' runs/recheck_*.jsonl \
  | sort | uniq -c | head

这条命令的价值在于可重复:换一台机器、换一个版本再跑,只要输入文件不变,结果差异就能归到环境差异上。边界也要说清楚——这套流程只能把输入侧排除或坐实,不保证一次定位到唯一原因;如果输入和环境同时变了,需要一次只改一个变量重跑,否则结论仍然是不确定的。