开始前,会这些就够了
- 知道像素由 RGB 数字组成
- 能计算平均值并看懂列表
- 了解模型输入与输出类别编号是不同东西
- 先读图片如何变成 RGB 数字,再把通道、坐标与缩放规则纳入固定样例。
本地脚本预测正常,换成网页上传后却像在乱猜。权重文件没有改变,输入形状也对得上,问题还可能藏在哪里?模型接到的是转换后的数字;读图片的通道顺序、缩放方式和最后展示的类别名称,都可能在换入口时悄悄改变。
本篇继续已有像素入门和 FashionMNIST 练习,专门检查“同一内容从两个入口进入,是否变成同样的模型输入”。产物是一份输入约定、一个手算小样例和三个可触发的错误。全程不用图片库,不训练模型,不报告识别准确率。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本;制作时实测 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- preprocess_contract.py:完整示例
- README.md:环境、范围与独立练习
- observed.stdout.txt:制作时的观察记录
- verification.json:对应源码的验证范围
- SHA256SUMS.txt:文件内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B preprocess_contract.py解压后在含示例文件的文件夹打开 PowerShell,按命令运行。先预测结果,再与当次输出对照。 程序会在当前目录写入教学报告,建议使用独立练习文件夹。 教学数据与标准库公式,不代表 PyTorch 执行、训练效果或真实模型准确率。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
模型文件之外,还经过了哪些步骤
一次图片预测可以拆成:文件解码、颜色处理、调整尺寸或裁剪、数值转换、模型计算、类别名称查表。即使模型参数完全相同,前面产生不同数字,或后面用错名称表,用户看到的答案仍可能变化。所以“文件加载成功”只是其中一个检查点。
我们把入口双方共同遵守的规则写成一份约定:形状、维度顺序、颜色通道、输入范围、缩放除数、特征顺序和类别名称顺序。真实项目还要记权重版本、尺寸调整方法与预处理版本。这里的约定只是普通 JSON 数据,不是某个框架自带的万能格式。
训练阶段可能故意随机翻转或裁剪来增加变化,线上推理也可能另有明确策略。需要一致的是模型要求的含义、确定步骤与版本配套关系;不是把所有训练随机增强原封不动搬到每次预测里。
先挑一张看得清每个数字的小图
本例的图片只有两行两列,每个像素按红、绿、蓝排列。左上是[255,0,0],右上是[128,64,0],左下是[64,128,255],右下是[0,0,128]。这个不对称样例刻意让红蓝不相同;若只用全灰图,交换红蓝后数值可能完全一样,错误就被测试材料遮住了。
为了让运算能手算,我们分别求四个像素的红、绿、蓝平均值,再除以255。三个通道的总和分别是447、192、383,分母都是4×255。把这三个分数直接写成预期值,形成固定对照样例,也叫黄金样例。预期值不能再次调用同一段待检查代码生成,否则同一个错误可能同时出现在两边。
这里输出的是三个平均颜色特征,已经丢掉位置和形状信息。它不是实际识图网络的标准输入,也不会识别衣服。我们只借它检查数据含义是否跨入口保持一致。
维度顺序同样要写清。这个列表是先行、再列、最后通道,叫HWC;TorchVision张量常见约定则是先通道、再行、再列,叫CHW。把十二个数字重新标成另一种形状,不等于把同一个像素的通道搬到正确位置。迁移代码应明确改变哪一轴,不能只因为元素总数相同就认为转换正确。
形状正确,仍然可能错在哪里
第一种错误是把红蓝交换。三个输出仍都是零到一之间的小数,长度也仍为三,所以形状和范围断言都能通过;黄金样例却能看到第一项与第三项换了位置。第二种错误是只转浮点数而没有除以255,数字仍可计算,但已经不在约定范围内。两类错误需要不同检查,不能只留一个“没有报错”。
第三种错误在输出之后:类别编号一原来对应 above_rule,展示层却把名称表倒过来。模型分数和编号一点没变,屏幕上的文字已经反了。名称表应随模型版本保存,不能按字母排序或临时从另一个数据集拿一份替代。
脚本的固定打分是两倍红均值减蓝均值再减0.4,按正负给出两个编号。系数是为了显露通道错误而人工选择的,分数不是概率,也不是训练得到的参数。例子里交换通道会跨过零线;真实模型未必每次都改最终类别,因此也要比较中间输入和分数。
运行一次正常路径,再让错误主动暴露
代码给两条正常路径使用同一预处理函数,同时拿手算值核对结果。然后分别制造交换通道、遗漏缩放和类别名称倒序三种问题,要求检查器能指出异常。还补了尺寸不对、像素越界和非有限值的拒绝案例;断言通过代表这些预先声明的例子被正确处理。
同一函数只能减少两份实现逐渐分叉的机会,不能证明函数自己没有错误,所以手算对照仍有价值。脚本不读取实际 JPEG 或 PNG;若项目问题发生在文件解码、透明通道、旋转方向或缩放插值,必须再加入对应的真实文件样例才有证据。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Original 2x2 RGB teaching fixture; not an image decoder or trained classifier."""
import json
import math
from pathlib import Path
CONTRACT = {"version": "toy-rgb-v1", "shape": [2, 2, 3], "layout": "HWC",
"channels": ["R", "G", "B"], "input_range": [0, 255],
"scale_divisor": 255, "feature_order": ["mean_R", "mean_G", "mean_B"],
"class_names": ["below_rule", "above_rule"]}
IMAGE = [[[255, 0, 0], [128, 64, 0]], [[64, 128, 255], [0, 0, 128]]]
EXPECTED = [447 / 1020, 192 / 1020, 383 / 1020] # Independent hand-counted channel sums.
def validate_image(image):
if not isinstance(image, list) or len(image) != 2:
raise ValueError("expected two rows")
for row in image:
if not isinstance(row, list) or len(row) != 2:
raise ValueError("expected two pixels per row")
for pixel in row:
if not isinstance(pixel, list) or len(pixel) != 3:
raise ValueError("expected RGB triple")
if any(type(value) is not int or not 0 <= value <= 255 for value in pixel):
raise ValueError("expected uint8-like integers")
def preprocess(image, channel_order=(0, 1, 2), divisor=255):
validate_image(image)
return [sum(pixel[c] for row in image for pixel in row) / (4 * divisor)
for c in channel_order]
def validate_features(features):
if len(features) != 3 or any(not math.isfinite(x) or not 0 <= x <= 1 for x in features):
raise ValueError("three finite features in [0,1] required")
def golden_check(features):
validate_features(features)
if not all(math.isclose(a, b, rel_tol=0, abs_tol=1e-12) for a, b in zip(features, EXPECTED)):
raise ValueError("golden feature mismatch")
def validate_contract(actual):
if actual != CONTRACT:
raise ValueError("preprocessing or class metadata differs from the saved contract")
def toy_score(features):
# Fixed arithmetic chosen to expose a mismatch, not learned weights or probability.
return 2 * features[0] - features[2] - 0.4
def must_reject(action):
try:
action()
except ValueError:
return
raise AssertionError("bad contract accepted")
training_path = preprocess(IMAGE)
serving_path = preprocess(IMAGE)
golden_check(training_path)
golden_check(serving_path)
assert training_path == serving_path
swapped = preprocess(IMAGE, channel_order=(2, 1, 0))
unscaled = preprocess(IMAGE, divisor=1)
validate_features(swapped) # Shape and range alone do not notice RGB/BGR confusion.
must_reject(lambda: golden_check(swapped))
must_reject(lambda: validate_features(unscaled))
assert toy_score(training_path) > 0 > toy_score(swapped)
class_id = int(toy_score(training_path) >= 0)
correct_name = CONTRACT["class_names"][class_id]
wrong_order = list(reversed(CONTRACT["class_names"]))
wrong_name = wrong_order[class_id]
assert correct_name != wrong_name
validate_contract(dict(CONTRACT))
must_reject(lambda: validate_contract({**CONTRACT, "class_names": wrong_order}))
must_reject(lambda: validate_image([[[0, 0, 0]]]))
must_reject(lambda: validate_image([[[256, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]))
must_reject(lambda: validate_image([[[float("nan"), 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]))
report = {"scope": "synthetic RGB fixture and fixed arithmetic, no image library or training",
"contract": CONTRACT, "golden_image": IMAGE, "expected_features": EXPECTED,
"correct": {"features": training_path, "score": toy_score(training_path), "class": correct_name},
"rgb_bgr_error": {"features": swapped, "score": toy_score(swapped), "range_check_passes": True},
"missing_scale_error": {"features": unscaled, "range_check_passes": False},
"class_order_error": {"unchanged_class_id": class_id, "wrong_name": wrong_name}}
Path("preprocess-report.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print("correct_features", [round(x, 6) for x in training_path])
print("scores", round(toy_score(training_path), 6), round(toy_score(swapped), 6))
print("same_class_id_different_name", correct_name, wrong_name)
print("PASS: golden fixture and three mismatch checks; preprocess-report.json saved")
怎样运行:保存为 preprocess_contract.py,在自己的空练习目录运行 python preprocess_contract.py。只用 Python 3 标准库,不下载数据或安装依赖。 2026-10-13 已用 3.14.0 执行这份示例并通过自带断言;下方为当次输出。教学数据与标准库公式,不代表 PyTorch 执行、训练效果或真实模型准确率。
运行后对照结果
correct_features [0.438235, 0.188235, 0.37549]
scores 0.10098 -0.087255
same_class_id_different_name above_rule below_rule
PASS: golden fixture and three mismatch checks; preprocess-report.json saved换回真实框架时,比较到哪一层
先选一小组固定原始文件,让训练代码中的确定性处理分支与服务入口各走一次,保存模型真正接收到的张量。核对形状、类型、通道、范围与若干关键位置,再按预先选定的绝对和相对容差比较所有元素。差异应从最早出现的一步调查,避免只在最终类别处看见结果变了。
TorchVision 的官方说明指出,推理转换与具体模型、变体甚至权重版本相关,提供 weights.transforms() 和配套类别元数据。阅读示例时可以学习“把转换和名称与权重绑在一起”的方式,但本篇没有加载这些权重。也不要把某个示例的均值与方差机械套到所有模型上。
若两种图片后端或精度产生很小数值差异,应解释容差依据。精确哈希适合检查原始文件或明确的版本描述;浮点张量更适合数值比较。把容差越放越宽直到通过,会让本来要发现的偏差消失。
交付一个别人能重复检查的入口
保存输入约定、原始小样例、独立预期结果、当前实际输出和负例日志。每次修改处理代码或替换权重,重新检查这些材料,并说明哪些变化是故意的。对于没有覆盖的解码或图像尺寸行为,直接写出范围,不用“测试通过”包办所有输入。
这份实验最重要的产物不是一次预测,而是一个能主动拒绝错误含义的检查器。它补上“参数已保存”到“线上输入与训练约定相符”之间的一段证据;真实准确率是否改变,还需要另外设计数据集和评估。
如果错误已经发生,保留同一原始输入在每一步的少量对照数据,比立即重新训练更容易定位。先修复已确认的约定差异,再重新执行正常与负例,最后才回到真实数据评估;不要靠改变模型阈值来补偿一个还没解释清楚的颜色或单位错误。
记住这些要点
- 形状相同不代表通道和数值含义相同。
- 固定小样例的预期值要独立于待检查代码。
- 权重、预处理规则和类别名称应配套管理。
- 正常路径通过之外,还要证明已声明的错误能被发现。
别把例子用过头
- 只检查能否加载权重或输入维度。
- 用全灰、全零或对称图片检查通道顺序。
- 把转成浮点数等同于缩放到零至一。
- 最终类别没变就忽略输入或分数差异。
自己试一小步
如果宽和高被交换,当前的两行两列样例能有效发现吗?
- 自拟一张长宽不同且颜色不对称的小图,写出约定。
- 先决定需要保存哪些位置或特征,才能发现宽高交换。
- 分别实现正确入口和一个故意交换轴的入口。
- 记录哪项断言发现错误,并列出仍未覆盖的图片处理步骤。
做到这些,就算完成
- 至少一项预期结果由手算或独立方法得到。
- 正常路径通过,交换轴的路径被预期断言拒绝。
- 没有只靠修改错误信息或预期值使测试通过。
- 提交脚本、约定、输入样例和实际日志,不声称验证了真实识图准确率。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- TorchVision 0.24:图像转换约定 (新标签页)↗
读 Supported input types and conventions 与 Dtype and expected value range:CHW维度约定、整数与浮点图像范围、
ToDtype转换范围;本文HWC列表是自定义输入,不能混用。 - TorchVision 0.24:预训练权重、转换与类别元数据 (新标签页)↗
读 Using the pre-trained models 与 Classification示例:
weights.transforms()、不同权重的处理要求和weights.meta['categories'] 对应输出编号。 - scikit-learn:不一致的预处理 (新标签页)↗
只读 Inconsistent preprocessing:训练使用的确定转换需要在后续数据上使用,Pipeline帮助保持处理流程;不把随机训练增强与线上处理要求混为一谈。