图像没有天然的离散单元,「视觉 Tokenizer」因此分裂成两条目标不同的路线:离散化把图像压成整数 id、与文本共享一张词表,代价是重建损失与码本坍缩;连续注入跳过离散化,直接把 patch embedding 投影进 LLM 的隐空间,代价是图像不可被自回归地「生成」。选哪条,取决于模型要不要产出图像。
两条路线的分水岭
| 维度 | 离散化(VQ 系) | 连续注入(projector 系) |
|---|---|---|
| 输出 | 整数 token id | 隐空间向量 |
| 能否进词表 | 能,扩 embedding 表即可 | 不能,绕过 embedding 查表 |
| 图像生成 | 支持,与文本同一套 next-token 目标 | 不支持(需外挂 diffusion) |
| 信息损失 | 量化误差不可逆 | 仅受 projector 容量限制 |
| 训练难度 | 高:码本坍缩、梯度不可导 | 低:端到端可导 |
| 代表 | VQGAN、MAGVIT-v2、Chameleon | CLIP-ViT + MLP(LLaVA)、Perceiver Resampler(Flamingo) |
判据很干脆:理解型 VLM 走连续注入,统一生成模型走离散化。前者只需图像信息尽量无损地进入注意力;后者要求图像可以被采样出来,而自回归采样只在有限字典上定义。两者如何在同一张词表里共存,见 多模态统一词表。
离散化路线
码本量化
VQ-VAE 的结构是「编码器 → 最近邻查表 → 解码器」。编码器把 图像下采样成 的特征图,每个位置的向量 替换为码本 中最近的一项:
就是这个 patch 的 token id,一张图变成 个整数。argmin 不可导,靠直通估计(straight-through estimator)把解码器的梯度原样搬给编码器:
import torch
import torch.nn.functional as F
def vq(z_e, codebook, beta=0.25):
"""z_e: (N, D) 编码器输出;codebook: (K, D)。返回量化结果、token id、辅助损失。"""
# 展开的平方欧氏距离,避免 (N, K, D) 的显式广播
d = (z_e.pow(2).sum(1, keepdim=True)
- 2 * z_e @ codebook.t()
+ codebook.pow(2).sum(1))
idx = d.argmin(1) # 最近邻下标 = 图像 token id
z_q = codebook[idx]
# 前向用量化值,反向让梯度绕过 argmin 直达 z_e
z_q_st = z_e + (z_q - z_e).detach()
codebook_loss = F.mse_loss(z_q, z_e.detach()) # 拉码本靠近编码器输出
commit_loss = F.mse_loss(z_e, z_q.detach()) # 拉编码器靠近码本
return z_q_st, idx, codebook_loss + beta * commit_loss
z_e = torch.randn(64, 8)
codebook = torch.randn(512, 8)
z_q, idx, loss = vq(z_e, codebook)
print(idx[:8].tolist(), round(loss.item(), 3), idx.unique().numel(), "/ 512 被用到")VQGAN 在此之上换掉重建目标:L2 像素损失让解码器输出模糊,于是加入 patch 判别器的对抗损失 + LPIPS 感知损失。同样的压缩率下纹理锐度显著提升,这是 VQGAN 取代 VQ-VAE 成为图像 token 化事实标准的直接原因。
lookup-free 量化
码本一大,最近邻检索与码本梯度都开始拖累训练,LFQ(lookup-free quantization,MAGVIT-v2)索性取消 embedding 表:把通道数压到 ,每维独立二值化,隐式码本就是 ,规模 。
def lfq(z_e):
"""每维独立符号量化;token id 由 d 个比特拼成,无需任何检索。"""
q = torch.sign(z_e)
q = torch.where(q == 0, torch.ones_like(q), q) # sign(0) 归入 +1
bits = (q > 0).long()
idx = (bits * (2 ** torch.arange(z_e.shape[-1]))).sum(-1)
return z_e + (q - z_e).detach(), idx
z_e = torch.randn(4, 18)
_, idx = lfq(z_e)
print(idx.tolist(), "词表规模 =", 2 ** 18) # 262144,无 embedding 参数好处是三重的:无检索开销、无码本参数、词表可以轻松推到 而利用率不塌。代价是失去了「相似 token 对应相近向量」的几何结构,需要熵正则(最大化边缘熵、最小化条件熵)把比特分布压平。后续的 BSQ 等变体把二值点约束到球面上,进一步稳住尺度。
码本坍缩与 EMA
坍缩指大部分码本项永不被选中——初始化时离数据流形远的项拿不到梯度,于是永远不动,实际词表退化成几十项,重建质量被硬顶住。诊断指标是码本利用率与困惑度 ,健康的 VQGAN 利用率应接近 100%。
常用对策:
- EMA 更新:不对码本求梯度,改用指数移动平均维护每项的聚类中心与计数,、、,典型 。本质是在线 k-means,比梯度下降稳得多
- 死码复活:计数长期低于阈值的码本项,直接用当前 batch 里的随机编码器输出重新初始化
- 低维 + L2 归一化码本:ViT-VQGAN 把码本维度压到 8–32 并做单位化,距离退化为余弦相似度,缓解高维空间里「所有距离都差不多」的问题
- 提高 commitment 权重 :抑制编码器输出漂离码本的速度
连续注入路线
理解型 VLM 不需要 id。ViT patch embedding 本身就是一次线性 tokenization: 的 patch 展平后过一个线性层,得到序列向量。视觉编码器(CLIP / SigLIP ViT)输出的 patch 特征经一个projector 对齐到 LLM 的 ,再与文本 embedding 拼接成一条序列。
三种主流 projector:
| 方案 | 机制 | 输出 token 数 |
|---|---|---|
| 线性 / MLP(LLaVA) | 两层 MLP 逐 patch 映射 | 与 patch 数相等,信息最全 |
| Q-Former(BLIP-2) | 固定数量可学习 query 对视觉特征做 cross-attention | 固定 32,压缩率极高 |
| Perceiver Resampler(Flamingo) | latent query + cross-attention,支持变长/多帧输入 | 固定 64,帧数无关 |
MLP 保真但 token 多,query 类方案把序列长度钉成常数、代价是丢弃细节(OCR 与小目标任务上退化明显)。实践趋势是保留逐 patch 映射,转而在空间维度做无损压缩:pixel shuffle 把 邻域沿通道拼接,token 数降到 1/4 而不经过信息瓶颈。
图像 token 在序列里的位置由特殊标记划定,<image> 占位符在模板展开时被替换成整段视觉 embedding,机制与 特殊 Token 与 Chat Template 中的保留 token 一致。
分辨率的平方开销
两条路线共享同一个硬约束:token 数随边长平方增长。
| 分辨率 | patch 14 | 叠加 2×2 pixel shuffle |
|---|---|---|
| 224² | 256 | 64 |
| 448² | 1024 | 256 |
| 896² | 4096 | 1024 |
| 1568² | 12544 | 3136 |
高分辨率因此双重昂贵:prefill 的注意力是 token 数的平方,一张 896² 的图就能吃掉万级上下文——远超一段长文本,而文本侧一个汉字才占一到两个 token(对比见 Tokenizer 概述)。缓解手段按代价递增排列:
- 固定低分辨率 + tiling:把大图切成若干 336² 子图各自编码,再拼上一张全局缩略图(LLaVA-1.6 的 AnyRes)。简单,但子图之间缺乏全局注意力
- 原生动态分辨率:ViT 用 2D 旋转位置编码支持任意尺寸输入,配 token merge,token 数随实际内容伸缩(Qwen2-VL 一系)
- token 剪枝:按注意力得分丢弃冗余 patch,通常剪掉 50% 以上对多数任务几乎无损
- 视频:再乘上帧数,必须做时间维下采样或 3D 因果 tokenizer(MAGVIT-v2 的时空联合量化)
对 LLM 侧的连带影响是 embedding 表:离散路线要把 量级的视觉码本并入词表,参数量按 线性膨胀,权衡方式与 词表设计与裁剪 中的 vocab size 取舍同源。两条路线在工业模型中的实际选型与演进脉络,可对照既有综述。