资讯中心

025、视觉Token化与图像编码:ViT的Patch Embedding与实时感知权衡

📅 2026/8/19 16:53:18
025、视觉Token化与图像编码:ViT的Patch Embedding与实时感知权衡
025、视觉Token化与图像编码ViT的Patch Embedding与实时感知权衡调试机器人抓取时遇到过这么个怪问题模型在仿真里百发百中一上真机就变成“盲人摸象”后来定位到是视觉编码器的延迟抖动——图像输入到动作输出的端到端延迟在80ms到200ms之间随机跳变而仿真里这个数字稳定在50ms。查到最后问题出在ViT的Patch Embedding上不是模型算得慢而是我把图像Resize和Patch切分的耗时算漏了。这期就聊聊视觉Token化这件事以及它在实时机器人系统里到底该怎么权衡。先说ViT的Patch Embedding到底在干什么。一张224×224的RGB图像切成16×16的patch每个patch是16×16×3768维的向量经过一个线性投影变成768维的embedding再加上位置编码这就是Transformer的输入序列。听起来简单但这里藏着两个坑一个是patch size的选择直接决定了序列长度另一个是图像预处理resize、归一化、数据类型转换的耗时往往被忽略而这两件事在机器人实时感知里都是要命的。先看patch size。16×16是ViT-Base的默认配置224×224输入得到196个token。但机器人视觉输入往往不是224×224可能是640×480的深度图或者1280×720的RGB-D融合图。如果你直接resize到224×224会丢失大量空间细节——抓取场景里一个螺丝刀和一根铅笔在224分辨率下可能就糊成一片了。我见过有人为了省计算把输入降到112×112结果抓取精度直接掉了15个点。反过来如果你保持高分辨率输入比如448×448patch size还是16那token数就变成784个Transformer的计算量是O(n²)的序列长度翻4倍计算量翻16倍这在机器人上根本跑不动。所以这里有个关键权衡patch size和输入分辨率要一起调。我常用的做法是固定token数比如保持196个token不变那么输入分辨率从224提到448patch size就要从16提到32。32×32的patch在448分辨率下每个patch覆盖的实际物理区域和224分辨率下16×16是一样的但计算量不变。这个思路在RT-2和Octo这类VLA模型里很常见它们用固定token数来保证Transformer的计算开销可控同时通过调整patch size来适配不同分辨率的传感器输入。但别高兴太早patch size变大带来的问题是空间粒度变粗。32×32的patch在448分辨率下每个patch对应原图32×32像素如果目标物体只有20×20像素那它可能完全落在一个patch里位置信息就丢了。这时候需要靠位置编码来补救但位置编码是学习出来的它对小目标的区分能力有限。我的经验是对于抓取任务目标物体通常占据图像中心区域patch size 32还能接受但对于精细操作比如插拔连接器patch size超过16就很容易翻车。再说图像预处理。这里踩过一个大坑用OpenCV读图后直接转成torch tensor然后丢给ViT。看起来没问题但OpenCV读出来是HWC格式uint8类型而ViT的Patch Embedding层期望的是BCHW、float32、归一化到[0,1]或[-1,1]。这个转换过程如果写在数据加载器里还好但如果你在实时推理循环里做每次都要执行HWC→CHW、uint8→float32、除以255、减均值除方差这些操作加起来可能要5-10ms。听起来不多但如果你用30Hz的相机每帧预算只有33ms这10ms就是30%的开销。更隐蔽的是Resize操作。ViT的输入尺寸是固定的但相机分辨率是固定的所以你必须resize。OpenCV的INTER_LINEAR插值在CPU上跑640×480→224×224大概要2-3ms但如果你用GPU上的torchvision.transforms.Resize第一次调用会触发CUDA kernel编译可能要几百毫秒的延迟。这个延迟在仿真里不会出现因为仿真环境通常已经帮你处理好了但真机上第一次推理就会卡一下。解决办法是预热在模型加载后先跑一次假的输入把CUDA kernel编译好然后再进入实时循环。还有一个容易被忽略的点图像归一化的均值和方差。ViT预训练模型用的是ImageNet的均值和方差但机器人相机的图像分布和ImageNet差很远——深度图、红外图、低光照RGB直接用ImageNet的统计量会让特征分布偏移。我见过有人直接用CLIP的预处理结果模型在真机上完全失效因为CLIP的归一化是针对自然图像的。正确做法是统计你自己数据集的均值和方差或者干脆用BatchNorm在训练时自适应。但如果你用的是预训练ViT做特征提取器那就得重新校准归一化参数否则特征分布不对下游的action head也会跟着错。实时感知的权衡还涉及一个更根本的问题视觉Token化应该放在哪里做。在VLA模型里ViT通常是作为视觉编码器输出token序列给LLM。但LLM的输入序列长度有限比如7B模型通常支持2048个token你不可能把784个视觉token全塞进去还得留空间给文本指令和动作token。所以实际做法是要么用更少的视觉token比如用32×32的patch得到49个token要么用注意力池化attention pooling把196个token压缩成32个。Perceiver和Q-Former就是干这个的但它们的计算开销也不小在实时系统里要慎重。我自己的经验是对于大多数机器人操作任务196个视觉token已经够用了不需要压缩。但如果你要同时输入多视角图像比如左右目、第三视角那token数就会爆炸。这时候可以考虑用共享的ViT编码多视角然后拼接token序列但要注意位置编码要区分视角——否则模型分不清哪个token来自哪个相机。这个坑我踩过用绝对位置编码会导致模型把不同视角的相同位置混淆后来改成视角相关的可学习位置编码才解决。最后说说实时性的实际调优。如果你用TensorRT或者ONNX Runtime部署ViTPatch Embedding层通常会被融合进一个卷积层——因为16×16的patch切分加线性投影本质上就是一个kernel size16、stride16的卷积。这个优化能省掉显式的patch切分和拼接操作减少内存拷贝。但要注意如果你在PyTorch里用unfold或者view来切patch导出到ONNX时可能会生成低效的图结构导致推理变慢。我的建议是直接用Conv2d实现Patch Embedding这样导出和部署都顺畅。还有一个细节图像输入的数据类型。机器人相机通常输出8-bit的RGB但深度图可能是16-bit。如果你把深度图直接当成RGB输入ViT那深度值的范围比如0-10000mm和RGB的0-255完全不在一个量级归一化后深度信息会被压缩到很窄的区间模型学不到深度特征。正确做法是把深度图单独编码或者做log变换压缩动态范围再和RGB拼接。这个我在做抓取时深有体会直接用原始深度图模型在近距离抓取时经常误判距离log变换后就好了很多。回到开头的那个延迟抖动问题。后来我定位到除了Resize和归一化的耗时还有一个隐藏的元凶PyTorch的DataLoader在实时推理时如果用了多进程每个进程都要拷贝一份模型权重导致显存带宽竞争推理延迟就抖动了。解决办法是推理时不用DataLoader直接手动把图像从相机驱动拷贝到GPU用torch.cuda.stream来异步处理这样延迟就稳定了。总结一下我踩过的坑和调优经验供你参考。第一patch size和输入分辨率要一起设计固定token数是个好策略但要注意小目标的感知能力。第二图像预处理的耗时不能忽略尤其是Resize和归一化要在实时循环外做好预热和异步处理。第三归一化参数要用自己数据集的统计量别迷信ImageNet。第四Patch Embedding用Conv2d实现部署时更高效。第五多视角输入要处理位置编码的视角区分。第六深度图要单独编码别和RGB混在一起。最后说点个人看法。视觉Token化这件事在VLA模型里看似只是前处理但它的设计直接决定了模型能感知到什么、感知得多快。很多人在调模型结构、调loss却忽略了视觉入口的细节结果模型在仿真里跑得飞起一上真机就拉胯。我的建议是在搭建VLA系统时先把视觉编码器的延迟和特征质量单独测一遍用你自己的数据、你自己的相机、你自己的推理框架别用ImageNet的benchmark来评估。视觉入口稳了后面的动作生成才有意义。