资讯动态

从像素到软Token:多模态视觉输入工程实践与Token预算控制

发布时间:2026/10/26 13:15:59 来源:尧图企业网站定制
1. 从像素到语义视觉输入为什么需要“软 Token”1.1 一个被低估的工程问题很多人第一次接触多模态模型时脑子里想的都是“模型怎么理解图片”。但真正落到工程实现上第一个卡住你的问题往往特别朴素一张照片、一段视频到底是怎么被塞进模型里的你手里有一张 1920×1080 的 JPG文件大小 800KB。模型不认识 JPG它只认识数字。于是你要做的第一件事是解码——把压缩的像素流还原成 RGB 三通道矩阵。这一步用 Pillow 或 OpenCV 一行代码就能搞定但接下来才是真正的分水岭这个 1920×1080×3 的张量直接展平就是 622 万个数字。如果每个数字都当成一个 token 喂给 Transformer光是这一张图就能把上下文窗口撑爆。所以业界几乎不会这么做。主流做法是把图像切成小块Patch每块压成一个向量再通过投影层映射到语言模型的嵌入空间。这些被映射后的向量就是所谓的软 TokenSoft Token。它和文本 Token 最大的区别在于文本 Token 来自离散词表是查表得到的软 Token 是连续向量是算出来的。这个区别决定了后面所有的工程取舍。我写这篇东西的起因是最近帮一个团队排查他们多模态检索服务的内存暴涨问题。追到最后发现根子就在视觉软 Token 的数量估算上——他们按“一张图 256 个 Token”做的容量规划实际跑起来单帧到了 1000 多视频一进来直接雪崩。这类坑文档里不会写只有踩过才知道。1.2 这篇文章适合谁看如果你正在做下面任何一件事这篇内容应该对你有用搭建多模态 RAG需要把图片、视频帧入库做向量检索用 ComfyUI 之类的工具做“照片替换视频人物”这类任务想搞清楚帧是怎么被处理的做视频理解、视频摘要需要控制单帧的 Token 预算单纯好奇“模型看图和看字有什么不一样”。我会从像素解码一路讲到软 Token 的生成、压缩、批处理中间穿插 ffmpeg 逐帧导出、照片批量处理、常见内存问题排查这些实操环节。不堆公式重点讲清楚每一步为什么这么做、参数怎么定、哪里容易翻车。2. 视觉输入的完整链路拆解2.1 从文件到张量解码这一步的隐藏成本一张照片进入处理管线第一步永远是解码。这一步看起来无脑但有几个细节直接影响到后面的 Token 数量和质量。分辨率决定上限但不是越高越好。假设你原图是 4000×3000直接送进模型切 Patch 之后 Token 数量会非常夸张。主流视觉编码器比如 ViT 系列通常工作在 224×224 或 336×336 这样的固定输入尺寸上。所以工程上一般会先把图缩放到编码器的目标分辨率。这里有个反直觉的点缩放不是简单地把图变小而是要先做等比缩放再裁剪或填充否则长宽比一变物体形状就扭曲了模型学到的空间关系全乱。我一般用这样的策略短边缩放到目标尺寸长边按比例缩放后中心裁剪。这样既保住了主体又不会引入形变。用 Pillow 写大概是这样from PIL import Image def resize_and_crop(img, target336): w, h img.size scale target / min(w, h) new_w, new_h int(w * scale), int(h * scale) img img.resize((new_w, new_h), Image.BICUBIC) left (new_w - target) // 2 top (new_h - target) // 2 return img.crop((left, top, left target, top target))色彩空间也要统一。大部分预训练视觉模型是在 RGB 上训练的如果你拿到的是 CMYK 的印刷图或者带 Alpha 通道的 PNG得先转成 RGB。带透明通道的图如果不处理直接转张量会多出一个通道维度对不上报错信息还特别隐晦。归一化参数别自己拍脑袋。每个预训练模型都有自己的均值和标准差比如 ImageNet 的 mean[0.485, 0.456, 0.406]。用错了归一化参数模型不是不能用而是效果会莫名其妙地差一截你还找不到原因。这个坑我见过太多次了。2.2 Patch 切分软 Token 数量的第一决定因素图像变成张量之后下一步是切 Patch。这一步是软 Token 数量的直接决定因素值得单独拎出来讲。假设输入是 336×336×3Patch 大小是 14×14那么切出来的 Patch 数量是 (336/14) × (336/14) 24 × 24 576 个。每个 Patch 展平后是 14×14×3 588 维经过一个线性投影层映射到模型的隐藏维度比如 768就得到 576 个软 Token。这里的关键参数就是Patch 大小。Patch 越小Token 越多细节保留越好但计算量和内存占用呈平方级增长。Patch 越大Token 越少速度快但细粒度信息丢失严重。Patch 大小336×336 输入的 Token 数细节保留计算开销适用场景8×81764高很高精细识别、OCR14×14576中中通用视觉理解16×16441中低较低分类、粗粒度检索32×32121低低快速预览、大规模批处理我个人的经验是做检索用 14×14 起步做分类可以放宽到 16×16只有涉及文字识别或精细缺陷检测才值得上 8×8。因为 Token 数量直接决定了你后面向量库的存储成本和检索延迟不是越多越好。还有一个容易被忽略的点动态分辨率。现在不少模型支持按原图长宽比动态切 Patch比如一张 640×480 的图可能切成 40×30 个 Patch。这样做的好处是不浪费 Token 在填充区域上但代价是不同图片的 Token 数量不一致批处理时要做 padding反而增加了工程复杂度。要不要用取决于你的场景是“单张精细处理”还是“大批量吞吐”。2.3 投影层从图像特征到语言空间的桥梁Patch 切完、展平、加位置编码之后得到的还是一堆“图像特征”。这些特征和语言模型的词嵌入不在同一个空间里直接混在一起喂给模型效果会很差。所以中间需要一个投影层Projection Layer把图像特征映射到语言模型的嵌入维度。这个投影层的结构不同模型差别很大。早期的一些方案用一个简单的线性层后来发现线性层表达能力不够就换成了 MLP通常是两层中间带激活函数。再后来有些模型用 Q-Former 这类结构先用一组可学习的 Query 向量去“抽取”图像信息把几百个 Patch Token 压缩成几十个 Query Token。这个压缩过程非常关键。举个例子576 个 Patch Token 经过 Q-Former 压缩成 64 个 Token意味着你的视觉 Token 预算直接降到原来的九分之一。代价是信息有损但对于“图生文”这类任务损失的那部分细节往往本来也用不上。提示如果你在做多模态 RAG投影层的选择会直接影响检索质量。用线性投影的模型视觉 Token 和文本 Token 的对齐程度通常不如用对比学习训练的模型。选模型时别只看参数量要看它的视觉-语言对齐是怎么做的。3. 视频帧当软 Token 遇上时间维度3.1 视频不是“很多张照片”那么简单很多人第一次处理视频输入时会本能地把它当成“一堆照片”。逻辑上没错但工程上完全不是一回事。一张照片产生 576 个软 Token一段 10 秒、30fps 的视频有 300 帧如果每帧都处理就是 172800 个 Token。这个数量级别说推理了光是显存就装不下。所以视频处理的核心矛盾是时间维度带来的信息增量和 Token 预算的硬约束之间的冲突。解决思路无非两条要么减少帧数抽帧要么减少每帧的 Token 数降分辨率或增大 Patch。实际工程中往往是两条一起用。3.2 抽帧策略关键帧、均匀采样与场景切分抽帧是视频处理的第一道闸门。抽得好用十分之一的帧数保住九成的信息抽得差要么漏掉关键动作要么塞进一堆重复画面。均匀采样是最简单的策略每隔 N 帧取一帧。比如 30fps 的视频每 15 帧取一帧等效于 2fps。这个策略适合画面变化平缓的场景比如监控、讲座录像。但它的问题也很明显快速运动的片段会丢帧静止片段又采了太多冗余帧。关键帧提取依赖视频编码本身的信息。H.264/H.265 这类编码格式里I 帧是完整编码的帧P 帧和 B 帧是差分编码的。用 ffmpeg 可以直接导出 I 帧ffmpeg -i input.mp4 -vf selecteq(pict_type,I) -vsync vfr keyframe_%04d.jpg这样导出的帧数量通常远少于总帧数而且天然对应画面变化较大的时刻。但要注意I 帧的间隔是编码器决定的不一定符合你的语义需求。场景切分是更语义化的做法计算相邻帧的直方图差异或感知哈希差异超过阈值就认为发生了场景切换取切换点作为关键帧。这个方法对“照片替换视频人物”这类任务特别有用因为人物替换通常发生在特定镜头里按场景切分能保证每个镜头至少有一帧被采样到。我一般会组合使用先用场景切分把视频切成若干片段每个片段内再均匀采样 1-2 帧。这样既控制了总量又保证了覆盖度。3.3 逐帧导出实操ffmpeg 参数怎么调说到逐帧导出ffmpeg 是绕不开的工具。但它的参数多到让人头大我挑几个实际会用到的讲。先看视频基本信息ffprobe -v error -select_streams v:0 -show_entries streamwidth,height,r_frame_rate,nb_frames,duration -of defaultnoprint_wrappers1 input.mp4这条命令会输出分辨率、帧率、总帧数、时长。帧率这个字段要特别注意它经常是30000/1001这种分数形式实际是 29.97fps不是 30fps。如果你按 30 算长时间视频会累积出明显误差。逐帧导出ffmpeg -i input.mp4 -q:v 2 frames/%06d.jpg-q:v 2控制 JPEG 质量范围 1-31数字越小质量越高。做视觉 Token 提取的话我建议用 2-3因为后续还要缩放原图质量太差会放大误差。如果只要特定时间段ffmpeg -ss 00:01:30 -i input.mp4 -t 10 -q:v 2 frames/%06d.jpg-ss放在-i前面是快速定位关键帧级别放在后面是精确到帧但速度慢。做批量处理时快速定位通常够用。注意导出几千帧 JPG 会占用大量磁盘空间。1920×1080 的 JPG 大约 300-500KB10000 帧就是 3-5GB。如果只是做 Token 提取建议导出时就缩放加上-vf scale336:336能省下大量空间和后续处理时间。3.4 视频帧的 Token 预算控制假设你已经抽出了 100 帧每帧 576 个 Token总共 57600 个 Token。这个量级对于大多数语言模型来说还是太大。接下来要做的是进一步压缩。空间压缩把每帧的分辨率降下来或者增大 Patch。336×336 降到 224×224Token 数从 576 降到 256直接砍掉一半多。时间压缩把相邻几帧的特征做池化平均或最大合并成一个 Token。比如每 4 帧合并成 1 帧的特征100 帧就变成 25 帧。层级压缩先用一个轻量编码器提取每帧特征再用一个时间维度的 Transformer 做时序建模最后只保留一个全局表示。这种做法在视频分类里很常见但会丢失时间定位能力不适合需要“第几秒发生了什么”的任务。实际选哪种取决于你的下游任务。做视频摘要时间压缩够用做动作定位空间压缩更安全做视频检索层级压缩效率最高。4. 批量处理与工程化落地4.1 批量照片处理的文件管理做视觉 Token 提取很少只处理一张图。批量处理时文件命名和目录结构如果一开始没设计好后面会非常痛苦。我习惯的目录结构是这样的project/ raw/ # 原始文件只读 decoded/ # 解码后的中间结果 tokens/ # 软 Token 输出 logs/ # 处理日志原始文件永远不动所有处理结果写到新目录。这样出问题时可以随时重跑不用怕污染源数据。文件命名上保留原始文件名中的关键信息很重要。比如从视频导出的帧命名成videoA_frame_000123.jpg比000123.jpg好得多因为前者能追溯来源。批量重命名可以用 Python 的pathlibfrom pathlib import Path for i, p in enumerate(sorted(Path(frames).glob(*.jpg))): new_name fvideoA_frame_{i:06d}.jpg p.rename(p.with_name(new_name))这里用sorted()保证顺序稳定用零填充保证字典序和数字序一致。别小看这个细节我见过因为文件名排序错乱导致帧顺序颠倒的事故。4.2 批处理的显存与内存管理批量提取 Token 时最常见的翻车方式是一次性把所有图加载进内存。1000 张 1920×1080 的图解码后每张约 6MB加起来就是 6GB还没算模型本身的占用。正确做法是用生成器逐张加载、逐张处理、逐张释放def process_batch(paths, model, batch_size16): batch [] for p in paths: img load_and_preprocess(p) batch.append(img) if len(batch) batch_size: yield model.encode(batch) batch [] if batch: yield model.encode(batch)batch_size的选择要看显存。我的经验值是336×336 输入、ViT-Base 级别的模型16GB 显存可以跑到 32-64如果模型更大或者输入分辨率更高降到 8-16。宁可 batch 小一点多跑几轮也不要为了跑满显存导致 OOM 中断因为中断后重跑的成本远高于多跑几轮。还有一个内存泄漏的常见来源没有及时释放中间张量。PyTorch 的自动求导会保留计算图做推理时一定要包在torch.no_grad()里否则显存会随着处理数量线性增长跑几百张就爆了。4.3 存储与检索软 Token 怎么存软 Token 提取出来之后通常不会原样存几百维的浮点向量而是做进一步处理。如果是做检索一般会再经过一个投影头降到 128-768 维然后归一化存进向量库。归一化这一步很重要因为余弦相似度是检索里最常用的度量而归一化后的向量点积就等于余弦相似度能省一次计算。存储格式上小规模可以用 numpy 的.npy大规模建议用 FAISS 或类似的向量索引。FAISS 的IndexFlatIP适合精确检索IndexIVFFlat适合大规模近似检索。选哪个取决于你的数据量和延迟要求。数据规模推荐方案检索方式延迟量级 10万numpy 暴力检索精确毫秒级10万-100万FAISS IVF近似毫秒级 100万FAISS IVF PQ近似亚毫秒级提示软 Token 和文本 Token 的向量空间是否对齐直接决定了跨模态检索的效果。如果你的模型不是用对比学习训练的别指望图像向量和文本向量能直接算相似度中间需要额外的对齐步骤。5. 常见问题与排查实录5.1 Token 数量对不上从预期到实际的偏差排查问题现象按公式算出来一张图应该是 576 个 Token实际跑出来是 577 或 580。排查思路多出来的 Token 通常是CLS Token和位置编码相关的特殊 Token。很多视觉编码器会在 Patch Token 前面加一个 CLS Token 用于分类这个 Token 也会进入后续处理。另外有些实现会加寄存器 TokenRegister Token来稳定训练。这些都要算进预算里。解决方法不要自己算直接打印模型输出的 shape。以实际 shape 为准做容量规划。5.2 视频帧提取后顺序错乱问题现象导出的帧按文件名排序后播放顺序不对。排查思路ffmpeg 默认的%d格式不补零frame_1.jpg、frame_10.jpg、frame_2.jpg按字典序排会变成 1、10、2。另外如果用了-vsync vfr帧率不固定时间戳信息可能丢失。解决方法用%06d补零保证字典序和数字序一致。如果需要保留时间戳导出时用-frame_pts 1文件名会带上帧序号。5.3 显存溢出批处理时的 OOM 排查问题现象处理到第 N 张图时突然 OOM前面都正常。排查思路按顺序检查这几个点——是否包了torch.no_grad()是否有变量在循环外累积比如把每张图的特征 append 到一个 list 里忘了清是否有图片尺寸异常大比如混进了一张 8000×6000 的图。解决方法加一个尺寸检查超过阈值的先缩放再处理用del显式删除中间变量用torch.cuda.empty_cache()定期清理缓存但别在循环里每张都调有性能开销。5.4 常见问题速查表问题可能原因排查方法解决方向Token 数比预期多CLS/Register Token打印输出 shape按实际 shape 规划帧顺序错乱文件名未补零检查文件名用 %06d 格式显存线性增长未禁用梯度检查 no_grad包 no_grad检索效果差向量未归一化检查向量模长做 L2 归一化图像形变缩放未保持长宽比检查预处理等比缩放裁剪颜色异常色彩空间未转换检查通道数统一转 RGB5.5 几个我踩过的坑坑一以为分辨率越高越好。早期做检索时我把原图直接缩放到 512×512 提取特征结果检索质量反而不如 336×336。原因是模型训练时的分辨率就是 336推理时给更大的输入位置编码外推会引入噪声。推理分辨率尽量贴近训练分辨率这是铁律。坑二忽略 EXIF 旋转信息。手机拍的照片经常带 EXIF Orientation 标记Pillow 默认不应用这个旋转。结果就是竖拍的照片在模型眼里是横的人物识别全乱。用ImageOps.exif_transpose()处理一下就好但不知道这个坑的人会排查很久。坑三视频抽帧后没做去重。静止镜头里抽出的帧几乎一模一样全送进模型既浪费算力又稀释了有效信息。后来我加了一步感知哈希去重相邻帧哈希差异小于阈值的直接跳过Token 总量降了 40%检索效果没掉。坑四批量处理时没做失败隔离。一批 1000 张图里有一张损坏整个批次崩掉前面 999 张白跑。后来改成每张图单独 try-except失败的记录到日志里跳过整体吞吐稳定多了。6. 从软 Token 到实际应用几个落地场景6.1 多模态检索以图搜图与以文搜图软 Token 最直接的应用就是检索。把图库里的每张图提取成向量存起来查询时把查询图或查询文本也转成向量算相似度取 Top-K。这里有个工程细节图库向量的更新策略。如果图库是静态的一次性建好索引就行如果图库持续增长要考虑索引的重建频率。FAISS 的 IVF 索引在数据量变化较大时聚类中心会失效需要定期重建。我的做法是数据量增长超过 20% 就重建一次重建期间用旧索引顶着。6.2 视频理解从帧 Token 到事件描述视频帧的软 Token 提取出来之后可以接一个时序模型做事件描述。比如输入一段监控视频输出“有人在门口停留了 30 秒后离开”。这个场景的关键是时间定位。如果 Token 压缩得太狠模型知道“发生了什么”但不知道“什么时候发生的”。所以做这类任务时我建议保留每帧的时间戳信息在 Token 序列里插入时间编码让模型能对齐时间。6.3 内容审核批量图片的快速筛查批量图片审核是软 Token 的另一个高频场景。核心诉求是吞吐量不是精度。这时候可以把 Patch 调大、分辨率调低用最小的 Token 预算跑一个轻量分类头先筛出可疑的再对可疑的做精细处理。这种“粗筛精查”的两级架构比一上来就用大模型全量处理吞吐量能差一个数量级。6.4 照片修复与增强中的 Token 视角照片修复模型比如超分、去噪、上色虽然不直接输出软 Token但它们的编码器-解码器结构里编码器输出的特征图本质上就是一组空间 Token。理解软 Token 的生成逻辑有助于你判断一个修复模型的能力边界——比如它的感受野有多大、能保留多少高频细节。实际用的时候我一般会先看模型的输入分辨率。如果模型是按 256×256 训练的你给它 1024×1024 的图它要么内部缩放丢失细节要么分块处理块间不连续。知道这一点就不会对修复效果有不切实际的期待。7. 一些参数选择的经验值最后分享几个我在实际项目里总结的参数经验值供参考。这些不是标准答案但能帮你少走弯路。图像预处理短边缩放到 336长边等比缩放后中心裁剪。JPEG 质量用 90 以上避免二次压缩损失。Patch 大小检索任务 14×14分类任务 16×16OCR 任务 8×8。没有特殊需求就用 14×14。视频抽帧先场景切分每个场景均匀取 2-4 帧。总帧数控制在 100 帧以内超过就做时间池化。批处理 batch_size16GB 显存、336 输入、ViT-Base用 32。显存翻倍就翻倍但别超过 128收益递减。向量维度检索用 256-768 维存储和精度比较平衡。低于 128 维精度掉得明显高于 1024 维存储成本不划算。归一化所有用于检索的向量都做 L2 归一化用内积代替余弦相似度省一次计算。这些数字不是拍脑袋来的是反复调出来的。但你的场景和我的不一样该调还得调。关键是理解每个参数背后的权衡而不是照抄。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑