9.2 殘差向量量化(RVQ, Residual Vector Quantization)(2018-2023)#
一、向量量化的基本架構#
向量量化(Vector Quantization, VQ)的想法來自訊號處理與通訊理論:把連續向量空間用有限個「代表點」離散化,再用代表點的索引來代替原始向量。碼本是一個矩陣 \(C \in \mathbb{R}^{K \times D}\) ,含 \(K\) 個碼字,每個碼字 \(\mathbf{c}_k \in \mathbb{R}^{D}\) 與潛在空間同維。給定一個潛在向量 \(\mathbf{z}\) ,量化就是找最近的碼字:
這個最近鄰查找(nearest neighbor search, NNS)是整個方案的瓶頸所在:它不可導,因此梯度無法從解碼端流回碼本。求解可以用暴力比對,也可以用乘積量化(Product Quantization)把碼本分解以加速。
最近鄰查找在幾何上等於把潛在空間切成 \(K\) 個 Voronoi 胞元,每個胞元內的輸入都會被替換成同一個碼字。這個觀點立刻解釋了兩件事。第一,量化誤差的形態是「有界的方向性扭曲」,而不是獨立的隨機噪聲:同一胞元內的所有輸入都被拉向同一個代表點,因此誤差之間高度相關,會形成方向一致的失真。第二,誤差的下界由胞元的直徑決定,而不是由碼字數量決定;只要某個方向上的輸入分佈很寬,即使碼本很大,那個方向上的胞元依然很長。這就是「碼本更大不一定更好、資料分佈的各向異性才是關鍵」的來源。
在聲音領域,向量量化的另一個源頭是說話人聲碼(speech codec)的經典架構。Gray 與 Neuhoff 建立的「先做 LPC 分析,再用向量量化量化 LPC 參數」的兩段式設計,曾是低比特率語音編碼的主流。其教訓是:若第一段分析本身是有損的,第二段量化再精確也救不回來。這條教訓在神經編解碼器裡以更精緻的形式再現——潛在表示的品質決定了量化誤差的動態範圍。
二、殘差向量量化#
SoundStream(Zeghidour 等人,2021)與 EnCodec(Défossez 等人,2022)採用的殘差向量量化(Residual VQ, RVQ),是把單一巨大碼本拆成多級串聯的量化器。定義殘差序列:
其中 \(q_k(\cdot)\) 是第 \(k\) 級的最近鄰量化器,輸出第 \(k\) 級選中的碼字。第 \(K\) 級之後,總量化結果為各級之和:
flowchart TB
Z["潛在向量 z ∈ R^D"] --> R1["殘差 r1 = z"]
R1 --> Q1["第 1 級量化<br/>最近鄰查找<br/>q1(r1)"]
Q1 --> R2["殘差 r2 = r1 − q1(r1)"]
R2 --> Q2["第 2 級量化<br/>q2(r2)"]
Q2 --> R3["殘差 r3 = r2 − q2(r2)"]
R3 --> QD["... 第 K 級 ..."]
QD --> SUM["總重建 ẑ = Σ qk(rk)"]
R3 -. "殘差能量急遽衰減" .-> DEC["剩餘能量 ≈ 0"]
Q1 -.-> C1["碼本索引 k1"]
Q2 -.-> C2["碼本索引 k2"]
QD -.-> CK["碼本索引 kK"]關鍵在於每一級只負責前一級留下的殘差。第一級碼本的作用是把 \(\mathbf{z}\) 拉進它覆蓋的某個粗略區域;第二級碼本在該區域內做精細修正;第三級再修正第二級的誤差。於是每一級的碼本都能用很少的條目覆蓋廣闊的範圍,因為它只需要分辨「差異」而非「絕對值」。
這裡的查找有一個容易被忽略的性質:它是逐級貪婪的。第 2 級只能在第 1 級已選定的結果上做修正,不能為了讓整體重建誤差更小而回頭推翻第 1 級的選擇。嚴格而言,全域最優的 RVQ 應該最小化總殘差 \(\left\| \mathbf{z} - \sum_k q_k \right\|\) ,而貪婪解最小化的是逐級的局部誤差。實測中這個差距通常很小,因為第一級的貢獻佔了總誤差的大部分;但在第一級碼本覆蓋不足時,貪婪策略的次優性就會變得明顯——這也是「第一級碼本務必覆蓋完整分佈」這條經驗法則的來源。
三、共享碼本與參數量#
一個自然的問題是:每一級是否該有自己的碼本?如果每級都有獨立的 \(K\) 個條目,參數量就是 \(K\) 倍。EnCodec 的做法是多級共用同一組碼本:每一級都從同一個大小為 1024 的碼本 \(C \in \mathbb{R}^{1024 \times 128}\) 中挑選條目,但各級的查找是在不同的殘差空間上進行的。
這個設計的巧妙之處在於碼本條目會被「分層複用」。第 1 級需要覆蓋整個潛在空間的分佈,第 2 級覆蓋的是第 1 級殘差的分佈,而殘差的分佈天然更集中、動態範圍更小。同樣的 1024 個碼字,在殘差空間裡能覆蓋得比在原始空間裡更細密。
把參數量算清楚:單一共享碼本含 \(K D\) 個參數, \(K = 1024\) 、 \(D = 128\) 時是 131072 個,佔 EnCodec 編解碼器總參數的比例只有個位數百分比。若每一級都配獨立碼本,參數量就變成 \(N K D\) ,8 級時超過一百萬,逼近整個編解碼器的規模,而重建品質的提升遠不足以支撐這份代價。共享的另一個好處是語意層級的一致性:因為所有級別共用同一個碼字空間,不同級別選到的「形狀」彼此可比較,模型在第一級學到的粗分類可以直接複用到後續級別的細化上。
從參數效率看,共享碼本的代價幾乎為零:單一碼本只有 13 萬個參數,相比數百萬級的卷積編解碼器可以忽略。真正的成本在於每幀要輸出 \(K\) 個索引。
四、碼本容量與比特率的關係推導#
把碼本大小與比特率放在一起算,就能看清 RVQ 的設計意圖。設每一級碼本含 \(K\) 個條目,則每幀每一級的索引需要 \(\log_2 K\) 個比特; \(K = 1024\) 時恰好是 10 個比特。若堆疊 \(N\) 級,每幀的索引總長度是 \(N \log_2 K\) 個比特,再乘上幀率 \(f\) 就得到比特率:
代入 EnCodec 的數字: \(f = 75\) 幀每秒、 \(N = 8\) 級、 \(K = 1024\) ,得到 \(R = 75 \times 8 \times 10 = 6000\) bps,也就是 6 kbps。若只送前 4 級則是 3 kbps,送滿 16 級就是 12 kbps。
flowchart LR
A["前 2 級<br/>1.5 kbps"] --> B["前 4 級<br/>3 kbps"]
B --> C["8 級<br/>6 kbps"]
C --> D["16 級<br/>12 kbps"]
D --> E["32 級<br/>24 kbps"]這條公式解釋了為什麼「級數」是比特率最靈敏的旋鈕:每增加一級就線性地多出 \(f \log_2 K\) 比特,儲存與傳輸成本同步上升,而重建品質的邊際增益卻迅速遞減。它也解釋了為什麼碼本大小很少被大幅調高:把 \(K\) 從 1024 升到 4096 會讓每幀每級的索引從 10 比特漲到 12 比特,總碼率增加兩成,參數量卻從 13 萬暴增到 52 萬。
反過來說, \(K\) 與 \(N\) 的乘積關係還決定了等效組合數: \(K^{N}\) 是理論上可區分的編碼組合數。 \(K = 1024\) 、 \(N = 8\) 時是 \(2^{80}\) ,遠大於一段 10 秒音訊可能的內容數;這正是殘差量化能在如此小的碼本上支撐高碼率的根本原因。
五、殘差能量衰減與分層的精細到粗糙分解#
殘差量化的分層結構可以用能量來刻畫。定義第 \(k\) 級殘差的能量佔比:
實測中 \(\rho_1\) 往往落在 0.7 到 0.9 之間,代表第一級碼本單獨就攔下了絕大部分的結構; \(\rho_2\) 通常掉到 0.05 以下,之後每級的衰減更陡。這條衰減曲線正是「精細到粗糙」分解的定量版本:第一級碼本負責的是頻譜能量的大範圍輪廓——基頻落在哪個區間、哪幾個共振峰突出——它的每個碼字覆蓋一大片區域,因此條目數不多也能定位到正確的鄰域;第二級之後處理的是「相對於該區域的偏移量」,動態範圍小得多,同樣數量的碼字能覆蓋得更細緻。末級殘差幾乎是各向同性的細微噪聲,與錄音的麥克風底噪、量化本身引入的隨機誤差混雜在一起,這部分資訊對聽覺的重要性遠低於前幾級——這正是多級設計能把有限比特花在刀刃上的原因。
換句話說,殘差量化不是把一個整數拆成若干位元,而是把一個連續的重建誤差拆成若干個階層性的近似誤差。第一級近似的是「結構在哪裡」,第二級近似的是「結構偏了多少」,第三級近似的是「偏得有多不規則」。每一層的問題都比上一層更局部,因此所需的碼本容量也小得多。
六、最近鄰查找的實作#
最近鄰查找的複雜度是 RVQ 中最容易被低估的成本。每一幀每一級都要在 \(K\) 個條目中找最近者,暴力比對的代價是 \(O(KD)\) 次乘加; \(N\) 級合計就是 \(O(NKD)\) 。以 \(K = 1024\) 、 \(D = 128\) 、 \(N = 8\) 計,每幀光是查找就要百萬次量級的距離運算,在 75 幀每秒下是每秒數千萬次。因此實務上有兩條路。
第一條是用矩陣乘積把距離計算批次化。展開:
其中 \(\left\| \mathbf{z} \right\|_2^2\) 對所有碼字相同,而 \(\left\| \mathbf{c}_k \right\|_2^2\) 對每個碼字固定,可以預先算好並快取,於是查找退化為一次矩陣乘法加上一個 argmax,在 GPU 上可以極度有效地平行化。第二條是用近似搜尋換取速度:FAISS 風格的 IVF(Inverted File)先把碼本空間切成若干 Voronoi 胞元,推理時只在少數候選胞元內精確比對;PQ(Product Quantization)則把 \(D\) 維碼字拆成若干子空間各自用小碼本編碼,把比對維數降下來。
還有一個容易被忽略的實作重點:碼本在推論時是常駐快取的靜態資料。它的體積是 \(K \times D \times 4\) 位元組,1024 乘 128 乘 4 只有 512 KB,可以完全放進 GPU 的快取記憶體甚至 SRAM。於是實際瓶頸往往不是算力而是頻寬:每一幀都要從快取讀出 \(N\) 份碼字矩陣。正是這個特性,讓「所有級別共用同一組碼本」變得格外划算——所有查找都指向同一塊記憶體。
七、為什麼殘差量化勝過單一大型碼本#
第一個理由是容量的指數成長。若單一碼本要達到同等解析度,需要約 \(K^{N}\) 個條目( \(N\) 為潛在維度),這在實務上完全不可行。RVQ 用 \(K\) 級串聯、每級 1024 條目,理論等效容量是 \(1024^{K}\) 的組合數,但只有 \(1024 \times K\) 個參數。
第二個理由是比特率的逐級細化。單一碼本若要支援 1.5 kbps 與 24 kbps 兩種操作點,就需要兩套不同大小的碼本,或者設計複雜的碼本間距機制。RVQ 則天然支援截斷:只用前 \(k\) 級,就得到一個較粗糙但合法的重建;用滿 \(K\) 級,得到高品質重建。這就是「可變比特率」的免費午餐——同一組權重,同一串解碼流程,只是送進解碼器的索引數不同。
第三個理由是殘差能量的超指數衰減。實測中第一級殘差的能量往往佔總能量的一大部分,而第二級之後衰減非常快,因為高階殘差接近各向同性的細微噪聲,而碼本只需要分辨它的少數主要方向。
第四個理由是分層語意。第一級碼本索引大致對應「聲學事件類型」這種粗略類別,後續級別則越來越像「該類別內的細微變化」。這種結構對後續的 token 建模非常友善:語言模型可以先預測粗類別,再細化。
八、Token 序列的形狀#
flowchart LR
subgraph T["時間維度"]
F1["幀 1<br/>k1 k2 k3 k4 k5 k6 k7 k8"]
F2["幀 2<br/>k1 k2 k3 k4 k5 k6 k7 k8"]
F3["幀 3<br/>k1 k2 k3 k4 k5 k6 k7 k8"]
end
F1 --> O["展平後的 token 序列<br/>t1 t2 ... t8 t9 t10 ..."]
F2 --> O
F3 --> O
O --> LM["自迴歸語言模型"]一個 RVQ 編解碼器產生的不是一維 token 串,而是時間 × 級數的二維矩陣。以 EnCodec 為例:24 kHz、幀長 32 個樣本,幀率 75 Hz;每幀潛在維度 128;8 級 RVQ,每級碼本 1024。於是每幀輸出 8 個索引,每秒 600 個 token。若每個索引以 10 比特編碼,就是 6 kbps。
這個二維結構在進入語言模型時有兩種處理方式:展平(把整個矩陣按時間優先展成單一序列)最簡單,但丟失了「跨級別的依賴」結構;交錯多碼本預測(interleaved multi-codebook prediction,MusicGen 採用的方式)則把不同級別交錯排列,並讓後面幾級能條件於前面幾級已預測的結果,藉此壓縮序列長度並提升一致性。
九、量化誤差如何傳遞到下游語言模型#
量化誤差不僅停留在編解碼器內部,它會以「token 序列上的噪聲」形式傳到語言模型裡,這是神經 codec 與傳統 codec 的一個本質差異。傳統 codec 的比特流裡每一位都承載真實的訊號資訊,錯一位就是錯一位;神經 codec 的 token 則是一個離散化後的近似,其序列滿足某個機率分佈,語言模型學的是這個分佈的轉移。
殘差量化讓這個問題變得可分解:第 1 級 token 若錯了,錯的是「粗結構」,也就是音素類型或基頻區間就選錯了,這類錯誤會沿著時間軸傳播,並且可能被語言模型的上下文修正;第 5 級 token 若錯了,錯的是「細節」,模型即使學得很好,解碼回來也只是高頻略有差異。於是各級 token 在下游的可學性並不相同:越前面的級別資訊量越大,也越需要模型投入容量;後面的級別則接近雜訊,可以用更弱的建模能力處理。這正是交錯多碼本預測在設計上必須小心安排各級上下文的原因。
另一個方向的反應是讓語言模型學會容忍噪聲。當量化誤差本身是隨機的,模型在足夠資料量下會把這個噪聲吸收成「背景分佈」的一部分:它學到的不是「沒有噪聲的序列」,而是「帶著這份量化噪聲的序列」。這解釋了一個有趣的現象:若換用另一個碼本(或另一個位元率)的 token 序列去做零樣本轉移,品質會明顯下降,因為模型吸收的是特定噪聲的統計特性,而不是抽象的聲學內容。
十、與連續潛在表示的對比#
為什麼不能讓自迴歸模型直接建模連續的潛在特徵?三個理由。
其一,離散化是自迴歸訓練的前提。交叉熵損失要求每一步的輸出是一個機率分佈;對連續向量做最大概然估計,得到的是高斯混合之類的形式,取樣時又難以保證分佈的一致性與可重現性。離散化成有限符號集之後,這一切自動成立。
其二,語言模型的容量分配以 token 為單位。一條十萬 token 的序列可以被任何架構的自迴歸 Transformer 直接消費;一條十萬連續向量序列則需要專門設計的連續潛在變數模型,而那類模型的訓練、取樣與工程複雜度都高得多,也難以沿用文字語言模型的全部成果。
其三,離散化讓量化誤差變成可控且可治理的。若直接回歸連續特徵,模型面對的是沒有明確類別邊界的連續目標,細微的量化差異會被回歸誤差吸收;一旦離散化,量化誤差的形態就是明確的離散替換,訓練時可以用承諾損失與 EMA 直接治理。代價則是離散化本身帶來的資訊損失——這是本章整個設計要權衡的核心。
十一、SoundStream 與 EnCodec 的設計對照#
| 參數 | SoundStream(2021) | EnCodec(2022) |
|---|---|---|
| 取樣率 | 24 kHz | 24 kHz |
| 幀長 | 320 樣本(75 幀/秒) | 32 樣本(75 幀/秒) |
| 潛在維度 | 128 | 128 |
| RVQ 級數 | 8 到 32(可變) | 8(另有 16、32 的擴充) |
| 每級碼本大小 | 1024 | 1024 |
| 碼本共享 | 是 | 是 |
| 碼率區間 | 0.9 ~ 6 kbps | 1.5 ~ 24 kbps |
| 訓練資料 | 約 60 小時多樣音訊 | 320 小時高品質音訊 |
EnCodec 特別值得注意的一點是它使用了因果分塊的訓練策略:把長音訊切成 1 秒左右的片段訓練,推理時再以固定大小的滑動視窗處理,這讓它能在流式情境下運作。相較之下 SoundStream 則支援因果與非因果兩種模型。
另一個差異在於 EnCodec 的潛在幀長更短(32 而非 320),意味著它保留了更細的時間解析度,代價是潛在序列更長、運算更重。幀長的選擇,反映了「時間解析度」與「序列長度」之間同樣的取捨。
十二、從編解碼器到語言模型的過渡難題#
RVQ 把連續音訊變成離散序列,看似打開了大門,但從編解碼器走到語言模型還有兩道牆。
第一道牆是解碼器的能力邊界。解碼器只能重建它訓練過的音域:EnCodec 在音樂上表現優異,在演講上則需要另一個模型。而語言模型一旦在 EnCodec token 上訓練,它學到的分布就受限於解碼器能表達的聲音集合。任何它想生成但解碼器未曾學會的特徵,都無法被還原。
第二道牆是token 序列仍然不是文字。它是連續的時間序列,長度極大,且相鄰 token 之間沒有顯式的語意邊界。文字有詞根、詞綴、空白邊界這種天然的離散結構;音訊 token 則完全沒有。第 10 章要處理的正是這個問題:如何把一個長而稠密的連續 token 序列,交給自迴歸模型而不在時間與序列長度上爆炸。
這裡也埋下第 9.3 節的伏筆:RVQ 之所以能在碼本只有 1024 條目的情況下支撐 24 kbps 的碼率,完全依賴各級碼本的高利用率。若有大量碼字長期閒置,等效容量會遠低於理論值,語言模型的建模效率也隨之崩潰。