ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SigLIP-HD:不加大图像,也能让视觉编码器看清细节

SigLIP-HD:不加大图像,也能让视觉编码器看清细节 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、EagleCLIP 擅长图文对应、纯视觉模型擅长细节理论上互补但实测收益有限甚至出现负增益。第三条也是最主流的一条直接加大输入分辨率从固定放大到保留原生分辨率稳步换来更强的 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-336pxDocVQA 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.py的forward里完成代码结构几乎和上面这段话一一对应先算学生在 512² 上的特征再对每个教师尺度分别做切图推理、插值回 32×32、堆叠取平均最后把学生特征和教师平均特征一起送进对齐损失。可以边读边对照第 3.1 节图 2 的两条分支代码里的每一步都能在框架图上找到位置。# siglip_hd/train/meta_arch.pydefforward(self,img_stu,img_tea):# 学生吃 512² 图取指定层的隐藏态作为特征stu_featself.student(img_stu,output_hidden_statesTrue).hidden_states[self.cfg.student.select_layer]H_feat_stuint(math.sqrt(stu_feat.shape[-2]))# 32# 教师对每个尺度512, 1024分别推理tea_feat_list[]fortea_img_sizeinself.tea_img_sizes:img_tea_localF.interpolate(img_tea,(tea_img_size,)*2,modebilinear,align_cornersTrue)tea_featself._infer_teacher(img_tea_local)# 把高分辨率特征插值回 32×32和学生对齐tea_featF.interpolate(tea_feat,(H_feat_stu,)*2,modebilinear,align_cornersTrue)tea_feattea_feat.reshape(tea_feat.shape[0],tea_feat.shape[1],-1).permute(0,2,1)tea_feat_list.append(tea_feat)tea_feat_avgtorch.stack(tea_feat_list).mean(dim0)# 插值 平均lossself.criterion(stu_feat,tea_feat_avg)returnloss这段代码把第 2 节的探路结论落成了工程。self.tea_img_sizes来自配置里的[512, 1024]循环里对每个尺度先把输入图 resize 到目标大小、再调_infer_teacher做切图推理拿回的特征统一插值到 32×32这样两个尺度的特征形状完全一致。最后torch.stack(...).mean(dim0)就是「插值平均」——注意它平均的是已经对齐到同一形状的特征所以输出维度和学生一模一样这正是省掉投影头的关键。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×24 块每块单独过一遍教师再把 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)(Hbase​s⋅Hbase​​)2s2,s∈{1,2,3,…}其中s ss是相对基础尺度的倍率H base 512 H_{\text{base}}512Hbase​512。s 2 s2s21024²要 4 次前向s 3 s3s31536²要 9 次s 4 s4s42048²要 16 次。这个平方增长就是「多尺度不能无限加」的算力根源——每多加一档尺度教师的前向开销不是线性而是平方级往上翻而第 2.2 节又证明了收益早就饱和了所以两尺度是性价比的甜点区。4.2 代码透视_infer_teacher的切图与拼回切图-拼回这段逻辑是整个教师分支里最绕的部分值得逐行看。核心是用torch.chunk在高、宽两个维度把大图切成网格小块摊平成一个大 batch 一次性过教师再用reshape permute把每块吐出来的 token 按原始空间位置拼回一张完整的高分辨率特征图。绕点全在维度顺序上下面的注释会标出每一步张量形状怎么变。# siglip_hd/train/meta_arch.pytorch.no_grad()def_infer_teacher(self,img):B,Himg.shape[0],img.shape[2]assertH%self.tea_base_size0grid_numH//self.tea_base_size# 1024 // 512 2# 先按高、再按宽切成 grid_num × grid_num 个 512² 小块chunks_heightimg.chunk(grid_num,dim2)grids[chunk.chunk(grid_num,dim3)forchunkinchunks_height]grids_flat[gridforbatch_chunkingridsforgridinbatch_chunk]img_gridtorch.stack(grids_flat,dim1)img_gridimg_grid.reshape(-1,3,self.tea_base_size,self.tea_base_size)# 所有小块一次性过教师冻结、no_gradtea_feat(self.teacher(img_grid,output_hidden_statesTrue).hidden_states[self.cfg.teacher.select_layer].detach())# 把每块的 token 按原始空间位置拼回完整特征图N_len,Cint(math.sqrt(tea_feat.shape[-2])),tea_feat.shape[-1]tea_feattea_feat.reshape(B,grid_num,grid_num,N_len,N_len,C)tea_feattea_feat.permute(0,5,1,3,2,4)tea_feattea_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_grad加detach双重冻结确保教师只当特征生成器拼回靠那个精心设计的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​​Fs−Ft​,Lcos​1−∥Fs∥∥Ft∥Fs⋅Ft​,Lsmooth​{0.5(Fs−Ft)2∣Fs−Ft∣−0.5​∣Fs−Ft∣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_typel1):super().__init__()self.patch_typepatch_typedefforward(self,pred,target):# pred / target: (B, N, C)targettarget.to(pred.dtype)ifself.patch_typel1:returnF.l1_loss(pred,target)elifself.patch_typesmooth_l1:returnF.smooth_l1_loss(pred,target)elifself.patch_typecosine:return(1.0-F.cosine_similarity(pred,target,dim-1)).mean()else:raiseValueError(fUnknown 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_stu和img_tea两个张量送进 batch。这样保证学生和教师看的是像素级完全对应的同一内容只是分辨率不同蒸馏对齐才有意义。# siglip_hd/data/dataset.pyself.transform_stuSiglipImageProcessor.from_pretrained(google/siglip2-so400m-patch16-512)self.transform_stu.size{height:cfg.student.img.size,width:cfg.student.img.size}# 512tea_max_img_sizemax(omegaconf.OmegaConf.to_object(cfg.teacher.img.size))self.transform_teaSiglipImageProcessor.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:imgImage.open(self.img_paths[idx]).convert(RGB)imgImageOps.exif_transpose(img)img_stuself.transform_stu.preprocess(img,return_tensorspt)[pixel_values][0]img_teaself.transform_tea.preprocess(img,return_tensorspt)[pixel_values][0]breakexceptException:idxrandom.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」。…详情请参照古月居
返回列表