图里的内容你自己看得一清二楚,模型却答得像没看过图,这种现象通常只有两条来路:一是图在送进模型前已经被预处理改过(裁切、等比缩到很小的长边、强制拉伸),关键区域消失或糊掉;二是对话模板里承载图片的那段标记没有正确落到最终输入里,模型收到的其实只有文字。两种表现都像“没看图”,但取证方式不一样,可以先分别留证据,再决定改哪一处。
先存两张图:原图和由送入张量还原出来的图,肉眼比对有没有被裁掉或缩糊;再打印张量的取值范围与通道顺序,确认归一化没有把像素压成异常值;然后把同一张图换一套对话模板重跑,并打印两次拼接后的输入,看图片标记是否还在。如果连只含一行文字的图都答错,优先查模板与输入拼接;如果文字图答对、复杂图答错,再回头调分辨率与裁切。单张简单图只能做定性判断。
把原图和送进模型的张量分别存成图片对照
这一步只回答一个问题:模型实际拿到的像素,和你看到的原图是不是同一张内容。做法是在预处理返回之后、batch 拼接之前,把张量另存一份转成 PNG,此时看到的才是模型真正读到的内容。
import torch
from torchvision.utils import save_image
def dump_input_tensor(t, path):
x = t.detach().float().cpu()
if x.dim() == 4:
x = x[0] # 取 batch 中的第一张
mean = torch.tensor([0.5, 0.5, 0.5]).view(3, 1, 1) # 与预处理保持一致
std = torch.tensor([0.5, 0.5, 0.5]).view(3, 1, 1)
x = (x * std + mean).clamp(0, 1)
save_image(x, path)如果通道顺序不是 RGB、或者张量布局还是 HWC,先转成 (C,H,W) 的 RGB 再存,否则还原图颜色本身就是错的,容易误判成预处理问题。
缩放与裁切配置通常集中在这几个字段,名字按你实际使用的仓库替换:
- image_size / max_pixels / min_pixels:控制长边或总像素上限;
- do_resize / shortest_edge:等比缩放还是强制拉伸;
- do_center_crop / crop_size:是否居中裁切、裁多大;
- patch_size:视觉侧切 patch 的粒度,间接决定小字还看不看得清。
验证方式是把原图和还原图并排看三件事:边缘有没有被切掉、文字或小目标有没有糊成一片、宽高比有没有被拉伸。只要出现裁切或明显缩糊,就先归到预处理侧,记下当时的配置值再调整。
核对预处理里的归一化与通道顺序
尺寸对不代表数值对。均值方差用错、少了除以 255 这一步、通道从 RGB 变成 BGR,都会让模型看到一张亮度和颜色都不对的图,常见表现是能说出大致场景,但答非所问。
归一化参数一般和图像处理器定义写在一起,字段名类似 image_mean、image_std、do_normalize、rescale_factor;通道顺序则取决于读图库——PIL 读出来是 RGB,OpenCV 默认是 BGR。找到这两处之后,打印送进模型的张量做核对:
x = batch['pixel_values']
print(x.dtype, x.shape) # 期望 float32/float16 与 (B, C, H, W)
print('min/max/mean:', float(x.min()), float(x.max()), float(x.mean()))
print('per-channel mean:', [float(c.mean()) for c in x[0]])取值范围可以先按这几档对照:只做除以 255 的缩放,张量大致落在 [0, 1];mean=std=0.5 的归一化,大致落在 [-1, 1];ImageNet 常用均值方差,单张图多数落在 -2 到 2 附近。打印出来是 0 到 255、成百上千的数值、或者三个通道均值明显异常时,优先怀疑归一化没生效或通道顺序反了。红蓝互换是最直观的通道顺序证据。
换一套对话模板重跑同一张图
模板问题的典型症状是:日志里确实有图片张量,回答却像在答一道纯文字题。原因通常是图片占位符在拼接时被丢掉、数量对不上,或者被放到了指令之后,模型拿到的是“一张图 + 一段没提到图的文字”。
切换方式有两种:在构造 messages 的地方把 template 参数换成另一套;或者直接绕开 apply_chat_template,手工拼接字符串。两种方式都用同一张图、同一句提问,其余参数保持不变。
text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
print(repr(text)) # 看图片占位符是否出现、位置在哪打印拼接文本后重点看三处:占位符有没有出现、出现次数是否等于图片数量、位置是否在用户提问之前。不同模板对占位符的写法不一样,有的用固定特殊 token,有的靠 processor 按字符串替换;换模板时必须确认新模板的占位符和新 processor 的替换规则一致,否则模型收到的就是纯文本输入。
然后把两次回答并列记录,不要只看哪次更顺眼:
- 模板 A、默认预处理:原样抄下实际输出,标注是否提到图中元素;
- 模板 B、同一张图:原样抄下实际输出,标注是否提到图中元素。
如果两套模板的输出差别只在措辞,说明模板不是主因;如果其中一套明显开始描述图片内容,那么问题就落在图片占位符与拼接链路上。
用单张只含一行文字的图做最小验证
复杂图很难分清“没看懂”还是“没看到”。做一张白底黑字、只有一行短文字的图,答案可以被人工判定,能把范围压到最小。
- 测试图内容:一张方形白底图,居中一行黑色大字,例如 MAGE-1234,文字不要贴边,字号要保证缩到目标分辨率后仍然可辨认;
- 期望回答:回答中明确包含图上那串字符;
- 实际回答:分别在默认配置、以及修正后的预处理与模板下各跑一次,把原始输出抄进记录里。
据此可以划定初步边界:
- 文字图答对、复杂图答错——图片链路是通的,重点回到分辨率与裁切,例如调大 max_pixels、减少激进缩放;
- 文字图答错,且打印出的拼接文本里没有图片占位符——先修模板与输入拼接;
- 文字图答错,但拼接文本和还原图都正常——回到归一化、通道顺序、模型加载方式继续查;
- 三种都不成立——说明还有别的环节在改动输入,需要结合具体仓库代码逐段定位。
结论边界要说清楚:单张行文字图只能证明图片有没有被正确送进模型,不能用来评价模型对复杂图表、密集文字或小目标的识别能力,也不要因为这一张图通过就认为分辨率问题已经解决。