阶段 4 · 学会创造

视觉 tokenizer:把一张图
变成一串「词」

上一章《一致性模型与蒸馏》把扩散采样压到一步,但那仍是连续向量——这一章换成离散的「词」。 GPT 之所以能写文章,是因为文字天生是「词」——每个词都能反复用。 图像不是——一张 256×256 的图,颜色值就有近 20 万个,每个值只是一个颜色。 让语言模型生成图片,第一步是 造一套「视觉词汇表」:每 8×8 像素一个编号,256×256 的图变成 32×32 = 1024 个编号。

1

一堆颜色值,没有一个「词」

比什么文本图像
基本单位词 / 子词,天然的离散符号 像素,0~255 的 256 档整数
词表现成的(几万到十几万) 没有——得自己造
「下一个」是什么意思在几万个候选里挑一个的概率分布 也能做成 256 类分类(PixelCNN 就是逐个像素分类的模型),但一个像素只是一个颜色,没有可复用的意思
序列长度几十到几千 一张 512×512 图 = 26 万个像素,太长了

所以这一章要做两件事。① 压缩——不能一个像素一个 token; 要把一块图(前面按 8×8,图更大时用 16×16)压成一个 token,让序列长度降到可训练的规模。 ② 换成有「意思」的词——每个 token 代表一种局部花样(一段边、一块纹理), 能像词一样反复用,而不是一个孤立的颜色值。VQ-VAE 就是同时做到这两件事的那个结构。

🎯 类比

像把一首钢琴曲变成乐谱。
原始音频是一串采样点——每个点只是一个瞬间的响度,没有「音符」这种能反复用的单位。
乐谱是离散的——「C、E、G」是从有限的音符表里挑出来的。
一旦变成乐谱,就能用语言模型那套「预测下一个音符」的办法来作曲了。
视觉 tokenizer 就是给图像写乐谱的那个东西。

互动 · 连续值切成有限档,到底丢掉了什么

档越少,「下一个」越好预测,但误差越大——这就是离散化那笔交易。

2

三个部件:编码 → 查字典 → 解码

部件输入 → 输出在做什么
编码器 Encoder 256×256×3 → 32×32×64 把图压成 1024 个 64 维的连续向量(每 8×8 一块)
码本 Codebook 1024 个向量 → 1024 个整数 这就是「视觉词汇表」:512 个可学习的向量(这章默认按 VQ-VAE 原文的 512; DALL·E 这类大模型用到 8192)。每个连续向量换成离它最近的那个码本向量
解码器 Decoder 32×32×64 → 256×256×3 把量化后的向量还原成图像
一张图 → 1024 个整数(每个在 0 ~ 511 之间)
现在这串整数就和一句话的 token 序列一模一样了

关键在「最近邻」这三个字:编码器输出的向量是连续的,它落在 64 维空间里的任意位置; 码本是有限个固定点。量化 = 把这个连续向量「吸附」到离它最近的那个码本点上。 于是无穷多种可能,被压缩成了 512 种。

互动 · 一块切多大,序列就有多长

块越大 → token 越少、越好训,但每一块里的细节也丢得越多。

3

亲手做一次最近邻量化

演示只画了 16 个白点;真实码本想开多大,看任务。VQ-VAE 原文用 512,DALL·E 用到 8192。 拖动橙色圆点(代表编码器输出的一个向量), 本章会真的算出它到每一个码本向量的欧氏距离,然后挑最近的那个。

互动 · 真实计算欧氏距离与量化误差

💡 注意底色的分区

背景那些浅色的块是真实算出来的Voronoi 分区(离哪个白点最近,就归哪一块)—— 每个格子都被算了一遍「离哪个码本点最近」,按结果上色。
它告诉你一件重要的事:每个码本点「负责」的是一整片区域,不是一个点。 落在同一片区域里的所有向量,最后都会变成同一个编号—— 这就是量化的信息损失从哪来的。

4

怎么让「取最近」这件事可导

VQ-VAE 有一个看起来很致命的问题:argmin(挑出让距离最小的那个编号)是不可导的。 找最近邻这个操作,它的梯度在数学上处处是 0(除了跳变点)—— 反向传播到这里就断了,编码器收不到任何训练信号。

❌ 直接量化 zq = ek,其中 k = argmin ‖ze − ej‖

argmin 是「离散选择」,导数几乎处处为 0。梯度传不回编码器。
✅ 直通估计器 Straight-Through 前向:zq = ek(真的用量化值)
反向:假装 zq 等于 ze,把解码器传来的梯度直接抄给编码器

一行代码:z = z_e + (z_q - z_e).detach()
.detach() 是 PyTorch 里「停止梯度」的写法:前向照常算,反向当它是常数,不往它身上传梯度。
所以这行前向得到的就是 zq(真正量化后的值);反向看,它是 ze 加上一个当成常数的量, 对 ze 求导就是 1——等于把解码器传来的梯度原样抄给了编码器。
💡 完整的损失函数有三项

① 重建损失 ‖x − x̂‖²(这个记号读作「平方误差」)——解码器要还原得像。
② 码本损失 ‖sg[ze] − ek‖²——让码本向量朝编码器输出靠。 sg 是 stop-gradient(停止梯度):前向照常算,反向当它是常数,所以这一项只推码本、不推编码器。
③ 承诺损失 β‖ze − sg[ek]‖²——让编码器输出朝码本靠,别乱跑。 这里的 β 是这一项的权重(不是别的章那个 β),原文取 0.25。
②拉码本、③拉编码器,两头往中间靠。

为什么反向「假装 zq 等于 ze」不会把编码器带歪?因为承诺损失一直在把 ze 往最近的码上拉, 两者本来就靠得很近;把一个当另一个用,方向和大小差不了多少。

M

数学 · 一个向量怎么变成编号

上面两节说「查最近邻」「梯度从旁边抄过去」。这两件事加起来只有三个字母, 但中间有一步最容易卡住:「挑最近的那个」这个动作,梯度是过不去的。 先把这一整步画出来——图看懂了,公式就只剩下抄一遍。

动画 · 前向走一遍、反向再走回来(距离和梯度都是当场算的)

互动 · 公式里每个符号,在图上都是哪一块

悬停公式里的字母——上面那张图里对应的那块会亮(线越粗 = 梯度越大); ∂L/∂z_q 读作「损失对量化向量的导数」。卡上 16 个码是缩小演示,实际可以是 512、8192。

🎯 用「图书馆还书」理解这一步

你手上有一本书,但图书馆只认书架上的编号。于是你一本一本比过去, 找到和你手上这本最像的那一册,把它的编号抄下来。
抄下来的编号 = k。后面的人拿到的是一串数字,不是你那本书。
现在的问题是:如果后面的人说「这编号不对」,你怎么知道该改哪本书? 编号和书之间没有连续的关系——这就是 argmin 不可导。 直通估计器的做法是:装作编号就是书本身,把批评原样转给你。

🎬 自己验一遍

把上面的滑块从 0° 拖到 360°,让 z 在码本上走一圈。
看那个 k:它不会连续地变,而是隔一段突然跳一下。 跳的那一下,就是 z 从这个码的「地盘」走进了隔壁那个码的地盘—— 微型图解里那 16 根柱子,最短的那根换人了。

5

码本坍塌:大部分码从没被用过

这是 VQ-VAE 最著名的失败模式。想象一下:你造了 512 个词的词汇表, 训练完之后发现很多词根本分不到样本——它们从没被选中过。 原因是编码器发现「反正只用那几个码就够了」, 于是把所有输出都挤到少数几个码附近,形成正反馈:用得越多 → 梯度越多 → 越会被选中。

互动 · 码本利用率监视器

被用到的码
—
死码
—
困惑度 perplexity
—

困惑度:这些码「等效」有多少个在被均匀使用。 512 个码全都一样常用,困惑度就是 512;只用一个码,困惑度就是 1。 它比「被碰过的码的比例」更能说明问题:只用了 8 个码、但 8 个都很平均,困惑度就是 8(而不是 512)。
(公式是 exp(熵):exp 就是「e 的几次方」;熵衡量使用分布有多分散,均匀时最大,挤到一个码上时为零。)
所以论文里汇报的通常是困惑度——它衡量「有效词汇量」。 它低一定不好(大量码白占参数);高只说明码都用上了,不保证画得好:画质还要看重建损失那一头。

修法怎么做效果
码本重启 Codebook Restart 监控每个码的使用次数,太久没用就把它的向量重置成一个随机编码器输出 最直接有效。把死码「复活」成有用区域
EMA(指数滑动平均)更新 不用梯度更新码本,而是用指数滑动平均统计每个码对应样本的均值 比梯度更稳。VQ-VAE-2 的默认做法
减小码本 码本开小一点(比如 8192 改 512) 治标。相当于承认「用不到那么多词」
换掉量化方式 用 FSQ / LFQ(有限标量量化 / 无查找量化)——不需要码本,直接对每个维度做舍入 从结构上消灭坍塌问题。见谱系表

重启为什么不会马上又死?因为重置是扔到一个真实数据点上;下一批数据里总会有向量离它最近,它就会被用上、被拉着学。

互动 · 困惑度在量什么:有效词汇量

左边均匀 = 每个码都在干活;右边长尾 = 大多数码是摆设。困惑度就落在两者之间。 (演示用 64 个码;真实码本可以开到 512、8192。)

6

拿到整数之后,图像变成一门语言

一旦图像变成了整数序列,所有为语言模型造的工具就立刻能用了—— 包括那个最简单的目标:预测下一个。

从图到「一句话」的完整流程

① 切块压缩256×256 图 → 32×32 = 1024 个连续向量
② 量化每个向量 → 一个整数编号(0~511)
③ 自回归生成把 1024 个编号按光栅顺序排成一列,用 GPT 那套预测下一个
④ 解码生成的编号查码本、过解码器,还原成图像

把 2D 的图拉成 1D 序列时,必须选一个顺序。最常用的是光栅顺序 (从左到右、从上到下)。
但这相当于告诉模型:「同一行的右边」比「下一行的左边」更近—— 这是一个不太自然的先验(先验 = 做决定前就假定好的东西)。 后来的工作试过各种顺序(Hilbert 曲线、随机排列), 但大多数还是用光栅顺序,因为它最简单,而且效果够好。

互动 · 2D 的图,到底是怎么被拉成 1D 的

同一张图,换一种走法就是另一条序列——而模型只看得见那条序列。

模型年份做法意义
DALL·E 12021 离散 VAE(先把潜变量量化成离散码的自编码器)得到 1024 个 token,用 GPT-3 那一类自回归 Transformer 生成 证明了「图像可以当成一门语言来生成」
Parti2022 同样的路线,但把 Transformer 放大到 200 亿参数 证明这条路也能 scale,但被扩散模型盖过了风头
VQGAN2021 在 VQ-VAE 上加一个对抗损失(GAN 里判别器那份)和感知损失(拿预训练网络的特征比对),让重建更清晰 当时自回归方法里的最好水平;至今仍是很多模型的视觉编码器
Chameleon / Emu32024 用同一个 Transformer 处理文本 token 和视觉 token,真正统一 回到「一个架构统治一切」的路线。能同时看图、写文、生成图
VILA-U2024 视觉 token 用对比学习对齐到文字空间,两类 token 走同一个自回归目标 少了一层翻译,理解与生成在同一个空间里发生
7

量化方法一路怎么走过来

方法怎么量化优点缺点
VQ-VAE
2017
单层码本,最近邻查找 开山之作 码本容易坍塌(后续文献报告的),重建质量一般
VQ-VAE-2 多个分辨率层级,各有一个码本 重建质量大幅提升(能生成 1024×1024 人脸) 层级管理复杂
VQGAN VQ-VAE + 对抗损失 + 感知损失 重建锐利,成为事实标准 引入 GAN 就引入了不稳定
RQ-VAE 残差量化:先量一次,再对残差(这次没量准的那部分)量一次…… 同样的码本大小下精度高很多 序列变长(一个向量变多个 token)
FSQ
Finite Scalar Quantization
干脆扔掉码本:把每个维度直接舍入到有限的几档 结构上就不会坍塌;实现极简;不需要任何辅助损失 能表示的组合数 = 各维档数相乘,只能按这种乘积来定,不好随意设成 8192 这样的数
LFQ
Lookup-Free Quantization
用一个隐式的二值码本 训练稳定,比 VQ 少调几项(省掉了码本损失,仍要承诺损失) 相对新,生态少
连续 latent(扩散路线) 不量化,直接用连续向量 信息损失最小,重建质量最高 不能直接做「预测下一个」,通常配扩散模型

这张表其实是两条路线的分岔。离散路线(VQ 系列 → Chameleon):能接自回归,和语言模型统一,但量化天生丢信息。 连续路线(VAE → Stable Diffusion 的潜空间):保留信息最多,但只能配扩散/流匹配。 分岔就从「要不要量化」开始,它决定后面必须用哪种生成范式。

互动 · 残差量化:一级不够,就把剩下的再量一次

拖级数:误差掉得很快,但序列也跟着变长——谱系表里 RQ 那一行就是这个权衡。

8

四个绕不过去的问题

问题具体表现什么时候真的会痛 → 换什么
① 量化必然丢信息 把一个连续向量吸附到最近的码本点,误差一定存在; 这个误差会成为生成质量的天花板,解码器再强也补不回来 细节要求高的场景(小字、纹理)→ 残差量化(RQ),或干脆走连续 latent + 扩散
② 码本大小是个死旋钮 开小了表达力不够,开大了大量码没人用、出现坍塌; 它是结构超参数,训练完不能改 数据集或任务变了也不能调 → FSQ(没有要学的码本,每维直接舍入)
③ 序列太长 文本一个句子才几十个 token,一张 512×512 图要 1024 个(16×16 一块), 高分辨率更是上万;而自回归是串行的 要生成高分辨率图或实时出图 → 多尺度 / 分层 token(先用很少的 token 画出整张图的粗稿,再逐层加细节,每层只补一点),或换成扩散(不串行猜)
④ 顺序是个不自然的先验 光栅顺序把 2D 结构压成 1D,等于告诉模型「同一行的右边比下一行的左边更近」 图上关系跨行跨列时(长距离依赖)→ 换顺序(Hilbert 曲线等)或扩散

互动 · 「序列太长」到底有多长

自回归是串行的:生成一张 512×512 图,要老老实实猜一千多次「下一个」。

⚠️ 但它今天的重要性反而在上升

2024 年之后,「视觉 tokenizer + 统一 Transformer」这条路线重新热了起来 (Chameleon、Emu3、VILA-U)。原因不是它生成质量更好——扩散模型在画质上仍然领先—— 而是:
① 它能让「理解和生成」用同一套东西。扩散模型擅长画,但不擅长「看着图回答问题」。
② 训练目标极简。就是「预测下一个」,不需要第二套数学。
③ 多模态天然统一。文本、图像、音频都变成 token 排在同一个序列里就行。

9

小结

它对应哪条线 ④ 学习即压缩——而且是这条线里最字面的一次: 它真的在给图像造一本字典,然后把每一张图都写成「字典里的编号」。
一句话 视觉 tokenizer 本质上是在给图像造一门只有 512 个字的语言—— 每一张图都必须用这 512 个字写出来。这一步换来了「可以预测下一个」, 代价是这门语言写不出来的细节,就永远丢了。
它牺牲了什么 牺牲了细节。码本有限,每个向量只能落到最近的码上,总会留一点误差; 码本开大误差会小,但要预测的字表也变大、更难学,还更容易出现没人用的码—— 所以这是一个取舍,不是调一下就能消失的问题。 解码器再厉害,也补不回没被记下来的那部分。
🎬 自己验一遍

回到第 3 节那张图。用鼠标拖动那个橙色圆点,让它从白点附近走到两个白点之间的空隙上。 看右边的 d = …… 那个读数——它会从接近 0 涨到 0.13 以上; 同时下面「量化结果 = k」那个编号,会在你跨过分界线的一瞬间跳一下。

它撞上了哪几条暗线

上面「④ 学习即压缩」是《第一性原理》那章列的七条主线之一;下面是这一章撞上的三条暗线(暗线编号和主线不是一套)。

暗线这一章的回答
B 什么被牺牲了 用细节换「可预测」。量化误差消不掉,只能取舍:码本开大误差小, 但字表变大更难学、更容易有没人用的码;再加上把 2D 拉成 1D 时选的那个光栅顺序, 等于额外告诉模型「同一行的右边比下一行的左边更近」。
换来的是:图像终于有了词表,可以用交叉熵目标训练了。
D 跑在什么上 查码本这一步几乎不做计算,时间全花在把码本从显存读出来。 最近邻查找 = 拿一个 64 维向量和 512 个向量逐一比距离。查码本每读一个数只做一次比较, 读得多、算得少,时间就耗在读上;码本太大,放不进 GPU 上那块又小又快的缓存, 只能每次从显存里慢慢读。
反过来,编码器和解码器是普通卷积,时间花在算上。 所以同一个模型里,最贵的那一步恰恰是看起来最「便宜」的查表—— 这也是 FSQ 干脆想把码本整个扔掉的原因之一。 完整的账见 《硬件与算力账本》。
F 违背了哪个直觉 「梯度必须有定义才能训练」是错的。 argmin 的导数处处为 0,按教科书这根本不能训; 但直通估计器前向用真值、反向假装自己是恒等映射,照样训得出来。
这一章还有第二个反直觉:码本不用梯度下降、改用 EMA 统计更新,反而更稳。 两件事合起来说明:能训练的不是「可导」,而是「信号能传回去」。
🎯 前后钩子

它接住了上一章的什么:《一致性模型与蒸馏》把采样压到一步,但仍是连续向量。这一章换成离散的「词」—— 更早的《自编码器》和 《VAE 变分自编码器》是零件,只多一步「查字典」。

它给下一章留了什么:下一章 《生成模型家族图谱》 要把八章学到的路线摆到同一张桌子上比。这一章给出的是其中「离散 → 自回归」那一格的全部底牌—— 包括它为什么赢不到最后,以及为什么 2024 年之后又回来了。

一句话带走视觉 tokenizer

任务只有两个:把图压成一串「视觉词」(好做「预测下一个」),同时把序列压短(好训练)。 三步是编码 → 查码本取最近邻 → 解码;argmin 不可导,所以靠直通估计器把梯度抄过去。
最大的坑是码本坍塌——大部分码没人用;修法是码本重启 / EMA 更新,或者干脆学 FSQ 把码本扔掉。
它和扩散模型代表两条分岔:离散 → 自回归,连续 → 扩散; 2024 年之后「统一 Transformer」又把它推回台前,因为理解与生成能共用一个模型。

10

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式