SigLIP-HD:不加大图像,也能让视觉编码器看清细节
2026/9/2 15:28:30 网站建设 项目流程

0. 简介

SigLIP-HD 面向多模态大模型(MLLM)里的视觉编码环节,处理的是一个被主流路线绕过去的矛盾:想让模型看清文档、图表、场景文字这类细粒度内容,几乎所有工作都在往「喂更大的图」上加码,代价是切图、多次前向、成倍膨胀的视觉 token 和额外的 token 压缩模块。它没有继续在分辨率上堆料,而是提出一套由细到粗的监督(fine-to-coarse supervision):让一个冻结的教师在多尺度图像(512²+1024²)上算出高质量特征,再逼一个学生在中等分辨率 512² 的单张图上把这套特征学过来。方法建立在目前很强的SigLIP 2-So400m/16-512px编码器上,训练只在 4.5M 张原始图像上跑,不用任何人工或合成的细粒度标注。从实验看,最值得关注的是它在完全相同的推理预算下把 DocVQA 从 56.0 提到 59.6、ChartQA 从 61.6 提到 65.2、HRBench 从 43.5 提到 48.3,且能作为即插即用的替换件直接换进现有 SigLIP 2 流水线。下面结合论文与仓库代码,重点拆解三件事:好特征到底长什么样、由细到粗的监督怎么落地成训练循环、以及为什么最简单的 L1 损失反而赢了。代码已经在 Github开源了


1. 为什么又要在「分辨率」这件事上折腾

1.1 人眼能在中等分辨率读懂,AI 为什么不行

先看一个反差鲜明的事实。把一张原生 1722px 高的文档图缩到 512px,字确实糊了,但人类依然能准确读出上面写了什么。换句话说,人的视觉系统在中等分辨率下就已经具备细粒度感知能力,不需要凑到原生大图才看得清。可当前的多模态大模型走的却是相反的路:早期工作(LLaVA 系列)粗暴地把图缩到 336² 这种固定低分辨率,近期的 Qwen2.5-VL、LLaVA-OneVision 则干脆保留原生分辨率。前者看不清细节,后者算力开销巨大。SigLIP-HD 的出发点就是这句反问——在简单加大图像之前,我们真的把模型在标准分辨率下的感知潜力榨干了吗?

1.2 现有三条路线,每条都有硬伤

提升 MLLM 视觉表征,业界大致有三条路,每条都不便宜。第一条是从头预训练更强的视觉编码器(DINOv2、Perception Encoder),靠更好的算法、更多数据、更大模型堆出来,动辄百万 GPU 小时、十亿级数据,绝大多数研究者玩不起。第二条是多编码器融合(Cambrian-1、Eagle),CLIP 擅长图文对应、纯视觉模型擅长细节,理论上互补,但实测收益有限,甚至出现负增益。第三条也是最主流的一条,直接加大输入分辨率,从固定放大到保留原生分辨率,稳步换来更强的 OCR 能力。这里的关键是,第三条虽然有效,却把复杂度全压在了推理侧:图必须被切成小块(tile)去匹配预训练分辨率,一次前向变成多次前向,视觉 token 成倍膨胀,还得再挂 resampler、pixel unshuffle 这类压缩模块给大模型减负,整个框架越叠越重。SigLIP-HD 的核心判断是:这条路走反了方向——与其在推理时加大图,不如在训练时把大图的知识蒸馏进标准分辨率的特征里。


2. 什么才是「好特征」:动手前先做四组探路实验

2.1 选谁当底座:SigLIP 2-So400m/16-512px

方法建立在SigLIP 2的 So400m/16-512px 版本上。这个模型 429M 参数,吃一张 512² 的图,在 patch size 16 下吐出 32²=1024 个视觉 token。作者先做了一组横向对比:在 LLaVA-1.5-7B 协议下,SigLIP 2 的这个 512px 版本在 DocVQA、TextVQA 这类 OCR 场景上明显强于 CLIP-L/14-336px(DocVQA 22.4 对 32.2),主要就赢在分辨率更大。所以后续实验主要用它当底座。这里要厘清的是,选底座不是拍脑袋,而是先确认「哪个现成编码器在目标场景上已经够强」,再在它上面做增量。

2.2 用几个尺度算「教师特征」最划算

在设计由细到粗的监督之前,必须先回答一个前置问题:要蒸馏的那份高质量特征,到底该用几个图像尺度算出来?作者把 SigLIP 2-512px 喂进不同的多尺度组合——单 512²、512²+1024²、512²+1024²+1536²、再加 2048²、以及单独的 1024²、1536²。为了公平,高分辨率特征都插值回 32² token,多尺度特征取平均,保证所有设置的 token 数完全一致。

结论很清楚:在 token 数相同的前提下,引入高分辨率图(512² → 512²+1024²)能提升 12 个 benchmark 里的 10 个,OCR 和图表任务尤其明显。但收益会饱和——继续加到 2048² 那一档反而变差,说明盲目加大分辨率是次优的。另一个发现是单独一个高分辨率视图普遍不如多尺度视图,这印证了全局视野不可或缺。这里值得注意,MMBench 这类偏语义的 benchmark 甚至不喜欢细粒度特征,这也解释了后面为什么要坚持保留基础尺度。

2.3 高分辨率图怎么推理、多尺度特征怎么融合

还有两个工程细节需要探路。其一,一张 1024² 的大图怎么喂给 512px 的模型?作者对比了三种:不重叠滑窗、半重叠滑窗、以及直接插值位置编码。结果有点反直觉——半重叠滑窗(在密集预测任务里通常更好)在这里反而掉点,插值位置编码也不如滑窗。作者推测是重叠区域 token 分布不一致、或位置编码冲突所致。最终最佳实践仍是不重叠滑窗其二,多尺度特征怎么融合?对比了「插值+平均」「插值+通道拼接」「pixel unshuffle+拼接」三种,最简单的插值+平均反而最好。这个结论直接决定了后面训练框架的形态:平均之后特征维度和基础尺度完全一样,省掉了后续对齐所需的投影头。

一句话理解:探路实验给出三条硬结论——用 512²+1024² 两个尺度、不重叠滑窗切图、插值后取平均。这三条后面会原封不动搬进训练框架。


3. 整体框架:一路推理造教师,一路训练学学生

3.1 两分支结构与输入输出

SigLIP-HD 的框架简单到可以一句话说清:一个冻结的教师分支产出高质量特征,一个可训练的学生分支去逼近它。教师和学生共享同一份 SigLIP 2 预训练权重,架构、输入输出完全一致,中间不加任何投影模块。学生吃一张 512² 的图,产出 32² 个 token,记它的特征图为F s F^{s}Fs;教师则在多尺度图上算出融合后的高质量目标特征F t F^{t}Ft。优化目标就是在 patch 级别把F s F^{s}Fs对齐到F t F^{t}Ft

整个前向过程在meta_arch.pyforward里完成,代码结构几乎和上面这段话一一对应:先算学生在 512² 上的特征,再对每个教师尺度分别做切图推理、插值回 32×32、堆叠取平均,最后把学生特征和教师平均特征一起送进对齐损失。可以边读边对照第 3.1 节图 2 的两条分支,代码里的每一步都能在框架图上找到位置。

# siglip_hd/train/meta_arch.pydefforward(self,img_stu,img_tea):# 学生:吃 512² 图,取指定层的隐藏态作为特征stu_feat=self.student(img_stu,output_hidden_states=True).hidden_states[self.cfg.student.select_layer]H_feat_stu=int(math.sqrt(stu_feat.shape[-2]))# 32# 教师:对每个尺度(512, 1024)分别推理tea_feat_list=[]fortea_img_sizeinself.tea_img_sizes:img_tea_local=F.interpolate(img_tea,(tea_img_size,)*2,mode="bilinear",align_corners=True)tea_feat=self._infer_teacher(img_tea_local)# 把高分辨率特征插值回 32×32,和学生对齐tea_feat=F.interpolate(tea_feat,(H_feat_stu,)*2,mode="bilinear",align_corners=True)tea_feat=tea_feat.reshape(tea_feat.shape[0],tea_feat.shape[1],-1).permute(0,2,1)tea_feat_list.append(tea_feat)tea_feat_avg=torch.stack(tea_feat_list).mean(dim=0)# 插值 + 平均loss=self.criterion(stu_feat,tea_feat_avg)returnloss

这段代码把第 2 节的探路结论落成了工程。self.tea_img_sizes来自配置里的[512, 1024],循环里对每个尺度先把输入图 resize 到目标大小、再调_infer_teacher做切图推理,拿回的特征统一插值到 32×32,这样两个尺度的特征形状完全一致。最后torch.stack(...).mean(dim=0)就是「插值+平均」——注意它平均的是已经对齐到同一形状的特征,所以输出维度和学生一模一样,这正是省掉投影头的关键。

3.2 训练与推理的关键不对称

这套框架最值得玩味的是它的不对称性:教师看的是多尺度大图,学生看的是单张中等图;教师只在训练时出现,推理时彻底消失。训练结束后,学生就是最终交付的 SigLIP-HD 编码器,它的结构和 IO 与原始 SigLIP 2 完全相同。这意味着部署时用户不需要改任何切图、多次前向、token 压缩的逻辑,只要把 vision tower 的 checkpoint 路径从 SigLIP 2 换成 SigLIP-HD,就能白嫖到更强的感知能力。这里的关键是,所有额外的多尺度计算都被隔离在训练阶段,推理预算一分钱没多花。


4. 教师分支拆解:切图、推理、拼回、平均

4.1 为什么必须切图而不是直接吃大图

教师要在 1024² 上产出特征,但 SigLIP 2 是在 512² 上预训练的。直接把 1024² 喂进去,要么得插值位置编码(第 2.3 节已证明会掉点),要么就得切图。SigLIP-HD 选了切图:把 1024² 的图按 512² 的窗口不重叠地切成 2×2=4 块,每块单独过一遍教师,再把 4 块特征按空间位置拼回一张完整的高分辨率特征图。这样每一块都落在模型熟悉的 512² 分辨率上,位置编码不失真,代价是一张 1024² 图要跑 4 次前向。这也是作者不加更多尺度的原因——加个 3× 尺度就要多跑 9 次前向,性价比急剧下降。切图前向的次数随尺度倍率平方增长,可以写成:

N forward ( s ) = ( s ⋅ H base H base ) 2 = s 2 , s ∈ { 1 , 2 , 3 , … } N_{\text{forward}}(s) = \Bigl(\tfrac{s \cdot H_{\text{base}}}{H_{\text{base}}}\Bigr)^2 = s^2, \qquad s \in \{1, 2, 3, \dots\}Nforward(s)=(HbasesHbase)2=s2,s{1,2,3,}

其中s ss是相对基础尺度的倍率,H base = 512 H_{\text{base}}=512Hbase=512s = 2 s=2s=2(1024²)要 4 次前向,s = 3 s=3s=3(1536²)要 9 次,s = 4 s=4s=4(2048²)要 16 次。这个平方增长就是「多尺度不能无限加」的算力根源——每多加一档尺度,教师的前向开销不是线性而是平方级往上翻,而第 2.2 节又证明了收益早就饱和了,所以两尺度是性价比的甜点区。

4.2 代码透视:_infer_teacher的切图与拼回

切图-拼回这段逻辑是整个教师分支里最绕的部分,值得逐行看。核心是用torch.chunk在高、宽两个维度把大图切成网格小块,摊平成一个大 batch 一次性过教师,再用reshape + permute把每块吐出来的 token 按原始空间位置拼回一张完整的高分辨率特征图。绕点全在维度顺序上,下面的注释会标出每一步张量形状怎么变。

# siglip_hd/train/meta_arch.py@torch.no_grad()def_infer_teacher(self,img):B,H=img.shape[0],img.shape[2]assertH%self.tea_base_size==0grid_num=H//self.tea_base_size# 1024 // 512 = 2# 先按高、再按宽切成 grid_num × grid_num 个 512² 小块chunks_height=img.chunk(grid_num,dim=2)grids=[chunk.chunk(grid_num,dim=3)forchunkinchunks_height]grids_flat=[gridforbatch_chunkingridsforgridinbatch_chunk]img_grid=torch.stack(grids_flat,dim=1)img_grid=img_grid.reshape(-1,3,self.tea_base_size,self.tea_base_size)# 所有小块一次性过教师(冻结、no_grad)tea_feat=(self.teacher(img_grid,output_hidden_states=True).hidden_states[self.cfg.teacher.select_layer].detach())# 把每块的 token 按原始空间位置拼回完整特征图N_len,C=int(math.sqrt(tea_feat.shape[-2])),tea_feat.shape[-1]tea_feat=tea_feat.reshape(B,grid_num,grid_num,N_len,N_len,C)tea_feat=tea_feat.permute(0,5,1,3,2,4)tea_feat=tea_feat.reshape(B,C,grid_num*N_len,grid_num*N_len)returntea_feat

这段代码的精髓在最后那个permute(0, 5, 1, 3, 2, 4)。切完之后特征的维度是(B, 行块, 列块, 块内行, 块内列, C),直接 reshape 会把「块」和「块内」的顺序搞乱,拼出来的特征图是错位的。permute先把通道 C 提到前面,再交替排列「行块-块内行」和「列块-块内列」,这样 reshape 成(B, C, grid_num×N_len, grid_num×N_len)时,空间位置才是连续正确的。这里的关键是整个方法用@torch.no_grad().detach()双重保证教师不参与梯度——教师只是个特征生成器,一分梯度都不该流回去。

难点提示(切图拼回是怎么回事):可以把它想成拼图。1024² 的大图被裁成 2×2 四块拼图,每块单独让「熟悉 512² 的教师」看清楚、记下特征,再按原来的位置摆回桌面拼成完整画面。permute就是确保你没把左上角的拼图块摆到右下角去。

4.3 工程细节:teacher 与 student 共享同一份预训练权重

一个容易忽略的细节是,MetaArch初始化时教师和学生都从cfg.teacher.arch/cfg.student.arch加载,而配置里两者都是google/siglip2-so400m-patch16-512。也就是说,教师和学生的起点是同一个模型,区别只在教师被冻结、看多尺度大图,学生可训练、看单张中图。这和传统知识蒸馏很不一样——传统蒸馏往往是大教师教小学生,这里是「同一个模型的多尺度集成版」教「它自己的单尺度版」。作者把这种做法归纳为一句话:不依赖任何外部模型的知识,纯粹释放当前模型自身的潜力。


4.4 小结:教师分支的三步一循环

回头看整个教师分支,逻辑其实收敛成一个稳定的三步循环:切图、推理、拼回,外面再套一层「多尺度取平均」。切图保证每一块都落在模型熟悉的 512² 上、位置编码不失真;推理阶段用no_graddetach双重冻结,确保教师只当特征生成器;拼回靠那个精心设计的permute把 token 摆回正确空间位置。这三步对每个尺度各跑一遍,再把插值到同一形状的特征沿尺度维度平均,就得到了学生要模仿的目标。这里的关键是,整条链路没有任何可训练参数、没有投影头、没有额外损失,纯粹是「拿现成的模型把大图看仔细,再压成标准分辨率的特征」,这也是它能即插即用的根本原因。

5. 学生分支与对齐损失:最严格的 L1 反而赢了

5.1 特征对齐的三种候选损失

学生要学教师,就得有个「学得像不像」的度量。业界给过好几种选择:EVA 用余弦相似度损失,AM-RADIO 用余弦相似度加 smooth L1 的组合。SigLIP-HD 把这些和最朴素的 L1 损失一起对比,发现三者平均表现非常接近,都超过了 SigLIP 2 基线,但最简单也最严格的 L1 略微胜出,于是选它作最终损失。核心问题在于:余弦相似度只约束方向、不约束模长,smooth L1 在小误差处会变软,而 L1 对每一个 patch、每一个通道的数值偏差都同等严格地惩罚——对「精确复刻教师特征」这个目标而言,越严格越好。把三种损失写在一起对比就一目了然:

L L1 = ∣ F s − F t ∣ , L cos = 1 − F s ⋅ F t ∥ F s ∥ ∥ F t ∥ , L smooth = { 0.5 ( F s − F t ) 2 ∣ F s − F t ∣ < 1 ∣ F s − F t ∣ − 0.5 otherwise \mathcal{L}_{\text{L1}} = \bigl| F^{s} - F^{t} \bigr|, \quad \mathcal{L}_{\text{cos}} = 1 - \frac{F^{s}\cdot F^{t}}{\|F^{s}\|\,\|F^{t}\|}, \quad \mathcal{L}_{\text{smooth}} = \begin{cases} 0.5\,(F^{s}-F^{t})^2 & |F^{s}-F^{t}|<1 \\ |F^{s}-F^{t}|-0.5 & \text{otherwise} \end{cases}LL1=FsFt,Lcos=1FsFtFsFt,Lsmooth={0.5(FsFt)2FsFt0.5FsFt<1otherwise

余弦损失分母做了归一化、只看方向而丢掉了模长信息,smooth L1 在误差小于 1 时退化成平方项、对小误差的惩罚明显变软,只有 L1 对任意大小的误差都保持线性、同等严格的惩罚。正是这份「不打折扣」让它在特征复刻任务上略胜一筹,消融里 L1 拿到 54.6,比余弦的 54.3 和余弦+smooth L1 的 54.2 都高。

难点提示(三种损失差在哪):想象老师批改抄写作业。余弦损失像只看「字的形状对不对」,不管写得深浅;smooth L1 像对小错睁一只眼闭一只眼,错得离谱才重罚;L1 则是一笔一划对照标准答案,差多少扣多少分。要让学生一模一样地复刻教师,最较真的 L1 反而最合适。

5.2 代码透视:一个极简的FeatAlignLoss

对应的损失实现短到几乎没有多余逻辑,一个nn.Module把 L1、smooth L1、余弦三种损失都收进来,用配置里的patch_type字符串切换,默认就是l1。这种写法的好处是消融实验时只改一行配置就能切换损失函数,不必动模型代码,也方便别人一键复现论文里的三种损失对比。代码越薄,越说明方法的强度来自设计而非工程堆料。

# siglip_hd/loss/feat_align.pyclassFeatAlignLoss(nn.Module):def__init__(self,patch_type="l1"):super().__init__()self.patch_type=patch_typedefforward(self,pred,target):# pred / target: (B, N, C)target=target.to(pred.dtype)ifself.patch_type=="l1":returnF.l1_loss(pred,target)elifself.patch_type=="smooth_l1":returnF.smooth_l1_loss(pred,target)elifself.patch_type=="cosine":return(1.0-F.cosine_similarity(pred,target,dim=-1)).mean()else:raiseValueError(f"Unknown patch_type:{self.patch_type}")

这段代码把target先转成和pred一样的 dtype,是为了配合 BFloat16 训练——教师在 no_grad 下算出来的特征可能和学生的自动混合精度类型不完全一致。默认走l1分支,直接调F.l1_loss,对(B, N, C)三个维度全部求平均绝对误差。这里值得注意,损失是在 patch 级别(N 个 token)逐个算的,不是把整图特征拉平成一个向量比对——patch 级监督才能逼学生把每个空间位置的细节都学到位,这也是它区别于 CLIPSelf 那种区域级粗监督的地方。

5.3 数据预处理:学生图与教师图的分流

一个常被忽略的工程点是,学生和教师吃的其实是同一张原图的两种预处理版本。数据集里用两个SiglipImageProcessor,一个把图 resize 到学生尺寸 512²,另一个 resize 到教师的最大尺寸 1024²,同一张图同时产出img_stuimg_tea两个张量送进 batch。这样保证学生和教师看的是像素级完全对应的同一内容,只是分辨率不同,蒸馏对齐才有意义。

# siglip_hd/data/dataset.pyself.transform_stu=SiglipImageProcessor.from_pretrained("google/siglip2-so400m-patch16-512")self.transform_stu.size={"height":cfg.student.img.size,"width":cfg.student.img.size}# 512tea_max_img_size=max(omegaconf.OmegaConf.to_object(cfg.teacher.img.size))self.transform_tea=SiglipImageProcessor.from_pretrained("google/siglip2-so400m-patch16-512")self.transform_tea.size={"height":tea_max_img_size,"width":tea_max_img_size}# 1024def__getitem__(self,idx):whileTrue:try:img=Image.open(self.img_paths[idx]).convert("RGB")img=ImageOps.exif_transpose(img)img_stu=self.transform_stu.preprocess(img,return_tensors="pt")["pixel_values"][0]img_tea=self.transform_tea.preprocess(img,return_tensors="pt")["pixel_values"][0]breakexceptException:idx=random.choice(range(len(self.img_paths)))return{"img_stu":img_stu,"img_tea":img_tea,"img_path":img_path}

这里的关键是教师图只 resize 到最大尺度 1024²,中间尺度(512²)在forward里由F.interpolate现场下采样得到,不必在数据侧存多份。那个while True+ 随机换 idx 的写法是针对 4.5M 原始图像里难免的坏图——读失败就随机抓另一张顶上,保证 dataloader 不会因为个别损坏文件卡死。数据是纯图像、无任何标注,这也印证了论文说的「purely exploring cheap raw images for self-improvement」。

…详情请参照古月居

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询