图像没有天然的离散单元,「视觉 Tokenizer」因此分裂成两条目标不同的路线:离散化把图像压成整数 id、与文本共享一张词表,代价是重建损失与码本坍缩;连续注入跳过离散化,直接把 patch embedding 投影进 LLM 的隐空间,代价是图像不可被自回归地「生成」。选哪条,取决于模型要不要产出图像。

两条路线的分水岭

维度离散化(VQ 系)连续注入(projector 系)
输出整数 token id隐空间向量
能否进词表能,扩 embedding 表即可不能,绕过 embedding 查表
图像生成支持,与文本同一套 next-token 目标不支持(需外挂 diffusion)
信息损失量化误差不可逆仅受 projector 容量限制
训练难度高:码本坍缩、梯度不可导低:端到端可导
代表VQGAN、MAGVIT-v2、ChameleonCLIP-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²25664
448²1024256
896²40961024
1568²125443136

高分辨率因此双重昂贵: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 取舍同源。两条路线在工业模型中的实际选型与演进脉络,可对照既有综述。