A VQ-VAE, short for vector-quantized variational autoencoder, is an autoencoder whose code is a short list of integers. An autoencoder is a pair of networks. The encoder compresses the input into a code, and the decoder rebuilds the input from that code (§32.1). In a VQ-VAE the encoder turns an image into a grid of vectors. Each vector is replaced by the closest entry of a learned table of vectors, called the codebook, and only that entry's number is kept. A 256×256 photograph can become a 32×32 grid of 1,024 integers, each one of 8,192 possible values.
That list of integers is why this section matters. A Transformer reads and writes tokens from a fixed vocabulary, and a grid of real-valued pixels has no vocabulary. Once an image is a sequence of code numbers, the next-token training that builds a language model can also generate images. The same idea turns speech into tokens for the audio models of §37.3. Most autoregressive image generators and many multimodal models start from a tokenizer of this kind, so interviewers for generative and multimodal teams expect you to know how one is trained.
Discrete codes also avoid a failure of the ordinary VAE. A VAE trains an autoencoder as a generative model (§32.3). Its loss adds a reconstruction error to a charge for how much information the code carries about the input. §32.4 showed that when the decoder is strong enough to model the data by itself, the cheapest solution can be a code that carries nothing. That failure is called posterior collapse. In a VQ-VAE the charge for the code is a constant fixed by the architecture, so the optimizer gains nothing by emptying the code.
The model comes from van den Oord, Vinyals & Kavukcuoglu (2017). We build it in seven steps. We find the nearest code with one matrix product. We read the model as a VAE to see what its code costs. We carry a gradient through a choice that has no derivative, and we train the codebook as a running k-means. We keep every code in use. We meet a second way through a discrete choice, which samples the code from a smoothed categorical distribution. Finally we fit a model over the integers, which turns the autoencoder into a generator.
Snap each vector to its nearest code
Start with the smallest case. The section comes back to it several times.
Now picture every point of the plane. Each point has one nearest code. The set of points nearest to code k is called its Voronoi cell, and the cells of all the codes tile the plane. Every encoder output in a cell is replaced by that cell's code. So the decoder learns only which cell the encoder output fell in, one of K possibilities, and the information that passes is at most log2K bits per vector.
In plain words, the encoder proposes a vector at each position, the quantizer rounds it to the nearest of K stored prototypes, and the decoder works only from the prototypes. Finding k⋆ is a nearest-neighbor search, the problem of finding the stored vector closest to a query that vector databases solve in §7.4. Reading out ck⋆ is a row lookup by index, exactly what an embedding layer does (§1.2). §1.10 already used such a codebook to turn image patches and sound into tokens. This section shows how to train one inside a network.
Finding the nearest code with one matrix product
A training batch holds n encoder outputs, every position of every image flattened into one list, stacked as the rows of Z∈Rn×dz. A Python loop over the rows is far too slow. Instead we expand the squared distance, as in §1.3,
∥z−ck∥2=∥z∥2−2z⊤ck+∥ck∥2.
In plain words, a squared distance is the two squared lengths minus twice the dot product. For all pairs at once, the dot products form one matrix product ZCvq⊤∈Rn×K, the same way Exercise 0.1b computed all pairwise distances. The first term is the same for every code, so it cannot change which code is nearest and can be dropped. The last term cannot be dropped.
Taking the largest dot product z⊤ck alone is a classic bug. It favors codes with large norms, because a long code has a large dot product with every vector that points its way. It gives the right answer only when every code has the same norm. That is the normalized lookup we will meet later as a cure for unused codes. The next cell checks both claims.
Python · cell 1
The loop and the matrix product agree, and the matrix product is many times faster. The dot-product shortcut picks the nearest code for only about 1 percent of the vectors once code norms differ. At scale, memory becomes the problem. A million vectors against 16,384 codes in float32 is a distance matrix of 106×16,384×4 bytes, about 65.5 GB. Production code therefore processes the batch in chunks and keeps only the argmin of each row. A chunk of 8,192 rows takes about 537 MB.
Core
Nearest-code assignment without a loop
Write vq_assign(Z, C). The input Z has shape (n, d) and holds encoder outputs, and C has shape (K, d) and holds the codebook. Return idx, an integer array of shape (n,) with the index of each row's nearest code in Euclidean distance, and Zq = C[idx]. Use the distance expansion and one matrix product, with no Python loop over rows. Break ties toward the smaller index, which np.argmin does for you. The test codebook has codes of very different norms, so the dot-product shortcut fails.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
VQ-VAE is a VAE whose posterior is a point
A VQ-VAE trains on the same objective as a VAE, for one particular choice of the encoder's distribution and of the prior. Recall that objective from §32.2. A latent variable model is trained by maximizing the evidence lower bound (ELBO), a lower bound on the log-likelihood logpθ(x),
In plain words, the first term rewards reconstructing x from codes drawn from the encoder's distribution qϕ, called the approximate posterior. The second term charges for how far that distribution sits from the prior p, a fixed distribution over codes. The KL charge is the cost of the information the code carries.
In a VQ-VAE the latent at one position is an index k∈[K]. The encoder's posterior puts all its mass on the nearest code k⋆, a one-hot distribution. During training the prior is uniform, p(k)=1/K. Here is the KL with real numbers before the general case.
Take K=4 and suppose code 3 wins. Then q=(0,0,1,0) and p=(41,41,41,41). Only the winning term of the KL sum is nonzero, since 0log0=0, so the KL is 1⋅log1/41=log4≈1.386 nats, exactly 2 bits. Had code 1 won, the answer would be the same. The general case is the same computation,
With T positions and independent uniform priors the KL is TlogK. For a 32×32 grid with K=512 that is 1,024×9=9,216 bits, the same for every image and every setting of the parameters. The reconstruction term simplifies too, since the expectation over a one-hot qϕ has only one outcome.
∂ Proof of the VQ-VAE bound
The marginal likelihood sums over all KT index sequences, pθ(x)=∑kpθ(x∣k)pω(k). Every term is nonnegative, so the sum is at least its single term at k=k⋆(x). Taking logs gives the bound. For the second statement, put the one-hot q into Eq[logpθ(x∣k)]−DKL(q∥pω). The expectation is logpθ(x∣k⋆), and the KL is −logpω(k⋆) because only one term survives. ∎
In plain words, the cost of describing x is the cost of its code under the prior plus the cost of the details the decoder still has to supply. The bound is tight when the decoder gives almost no probability to x from any other code. Van den Oord et al. argue that a converged decoder behaves this way, since it was only ever trained on k⋆.
Three consequences follow, and each one returns later in the section.
There is nothing for the KL to collapse to. In §32.4 the optimizer could shrink the KL to zero by making the encoder's distribution equal the prior. The price was a code that says nothing. Here the KL is the constant TlogK, and its gradient is zero, so the objective never rewards ignoring the code. It does not force the decoder to use the code either. But a decoder that ignores zq pays more reconstruction loss and gains nothing. That is the sense in which VQ-VAE avoids posterior collapse.
The rate is fixed by the architecture, and usage decides how much of it is spent. In §32.4 the rate was the average KL term, the information the code is charged for. Here it is logK per position, whatever the parameters. The code k⋆ is a deterministic function of x, so the information it actually carries depends only on how evenly the codes are used. The next theorem makes this exact.
Learning the prior is a second stage of maximizing the same bound. With the encoder and decoder frozen, the only term that depends on ω is logpω(k⋆(x)). Maximizing the bound over ω is maximum-likelihood training of a model over index sequences. We do that in the last part of the section.
The next result uses two quantities from §0.5. The entropyH(p)=−∑kpklogpk of a distribution is its average surprise, at most logK for K outcomes. The mutual informationI(x;k⋆) is how much knowing the code reduces uncertainty about the input.
∂ Proof that a deterministic code carries its usage entropy
Write I(x;k⋆)=H(k⋆)−H(k⋆∣x). The second term is zero because k⋆ is a function of x, so knowing x leaves no uncertainty about it. The distribution of k⋆ is pˉ, so H(k⋆)=H(pˉ). Gibbs' inequality (§0.5), the fact that a KL divergence is never negative, gives H(pˉ)≤logK with equality only at the uniform distribution. For the split, DKL(pˉ∥uniform)=∑kpˉklog(Kpˉk)=logK−H(pˉ), and the two pieces add to logK. ∎
In plain words, a VQ code can carry up to log2K bits per position, but it carries only as many as the entropy of its usage. A codebook of 16,384 entries in which only 200 are used, each equally often, carries log2200≈7.6 bits per position, not 14. That is why codebook usage gets so much attention below. Unused codes are the discrete form of posterior collapse, since both waste the capacity the code was given. An interviewer who asks why VQ-VAE does not suffer from posterior collapse usually follows up with what it suffers from instead, and this is the answer.
Gradients through a nearest-neighbor lookup
Training needs the gradient of the loss with respect to the encoder's parameters, and the path from the loss to the encoder runs through zq=ck⋆(ze). Go back to the three-code example. Move ze from (1.2,0.3) to (1.25,0.3) and the nearest code is still c2, so zq does not change at all. Inside a Voronoi cell the derivative of zq is zero, and on a cell boundary zq jumps, so there is no derivative. The true gradient of the reconstruction loss with respect to the encoder is zero almost everywhere, and backpropagation would deliver nothing.
§0.3 met the same problem with rounding. Its answer was the straight-through estimator (Bengio, Léonard & Courville 2013), which runs the true operation in the forward pass and pretends it was the identity in the backward pass. §0.9 packaged it as a custom autograd function. VQ-VAE applies it to the whole vector.
In plain words, the decoder computes with the code, and the encoder is told what the decoder wanted the code to do. The decoder is fed zqst, so its forward pass is exactly the quantized one. Two side effects are easy to miss. First, the codebook receives no gradient from the reconstruction loss at all, because zq appears only inside the stop-gradient. Second, the encoder's gradient is computed at the wrong place, at zq rather than at ze. The next theorem says how wrong that is.
It needs one term. A gradient is M-Lipschitz if moving the input by a distance r changes the gradient by at most Mr, so M measures how fast the gradient can turn.
∂ Proof of the straight-through approximation
(1) By the chain rule, ∇zeLrec(ze+δ)=∇Lrec(ze+δ)=∇Lrec(zq), since δ is held constant. (2) The Lipschitz condition says ∥∇Lrec(a)−∇Lrec(b)∥≤M∥a−b∥. Take a=zq and b=ze. (3) Inside a cell zq(ze) is constant, so its derivative is zero. ∎
In plain words, the straight-through estimator pretends the encoder output already sits at its code. The pretense is good exactly when the encoder output is close to its code, and the error shrinks in proportion to the distance. Huh et al. (2023) call this difference the gradient gap.
A linear decoder does not remove the error. For Lrec(z)=∥Wz+b−x∥2 the gradient is 2W⊤(Wz+b−x), so the straight-through gradient is off by exactly 2W⊤Wδ. The error vanishes when the quantization error is zero, or when the loss is linear in z, so that its gradient is the same at zq and at ze.
Python · cell 2
Once the spacing is small, the relative error falls in proportion to it, 0.237, 0.095, and 0.019 at spacings 0.25, 0.1, and 0.02, as part 2 of the theorem predicts. With coarse codes the straight-through gradient often points somewhere else entirely, with an average cosine of 0.43 at spacing 2. So encoder outputs should stay close to their codes. That is the job of one of two extra losses, which come next.
The codebook loss and the commitment loss
The straight-through estimator leaves two things untrained. The codebook gets no gradient from reconstruction, so something must pull each code toward the encoder outputs it represents. And nothing ties the encoder outputs to the codes. The VQ-VAE paper notes that the latent space has no natural scale, so encoder outputs can drift and grow if the codes do not keep up. The quantization error, which bounds the straight-through error, then grows with them. Each problem gets its own squared-distance term, and the stop-gradients decide which side of the distance each term moves.
In plain words, the decoder learns to reconstruct from codes. Each code moves toward the encoder outputs it wins. Each encoder output is asked, more gently, to stay near the code it chose. Without the stop-gradients the last two terms would merge into one, (1+βcommit)∥ze−ck⋆∥2, pulling codes and encoder outputs toward each other equally hard. Splitting it lets the codebook follow the data at full strength while the encoder, whose outputs carry the information, is only nudged.
The constant TlogK from the bound is dropped, since it has no gradient. In practice the two VQ terms are averaged rather than summed. The paper averages them over the T positions. DeepMind's Sonnet library takes a mean over every element, and the reconstruction term is often a mean squared error. The scaling matters, because βcommit only means something relative to the size of the reconstruction loss, as the paper itself notes.
With the loss fixed, we can write down exactly what each parameter receives. This is the derivation an interviewer asks for, and the stop-gradients make it short.
∂ Proof of the VQ-VAE gradients
For ze,i, the reconstruction term contributes gi through the identity Jacobian of zqst. The codebook term contributes nothing, because ze,i sits inside sg. The commitment term contributes βcommit⋅2(ze,i−cki). For ck, only the codebook terms of the rows with ki=k contain it outside a stop-gradient, and each contributes 2(ck−ze,i). Summing them gives 2nkck−2∑ize,i=2nk(ck−zˉk). ∎
The codebook gradient is a sum over every row that picked the code. That is a scatter-add, the backward pass of any row lookup, as §1.2 proved. The same NumPy trap applies. dC[idx] += rows keeps only one of several rows with the same index, while np.add.at(dC, idx, rows) adds them all.
The next cell checks these numbers.
Python · cell 3
Interview
The VQ-VAE backward pass, with repeated codes
Write vq_vae_grads(z_e, C, g, beta_commit). The input z_e of shape (n, d) is a batch of encoder outputs, and C of shape (K, d) is the codebook. The input g = dL_rec/dz_q, also of shape (n, d), is the gradient the decoder sends back to its input. Assign each row to its nearest code. Then return (dz_e, dC), the gradients with respect to z_e and C of
using the straight-through estimator. The tests compare your answer with finite differences of the function that each parameter actually sees, on a batch in which three vectors share one code. This is the coding version of the most common VQ interview question.
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Carrying the gradient across with a rotation
The straight-through estimator moves the gradient from zq to ze without changing it. The decoder sees only zq, so for a given loss every encoder output in a cell gets the same gradient, wherever in the cell it sits. Fifty et al. (2024) proposed a different rule, the rotation trick. It carries the gradient across with the rotation that turns ze onto the direction of zq, so the gradient keeps its angle to the vector. This is a 2024 refinement. A first reading can skim to the widget below and come back for the details.
Here is a tiny case. Let ze=(1,0) and its code zq=(0,2), a quarter turn away and twice as long. Suppose the decoder's gradient is g=(0,1), pointing along zq, so a descent step would shrink zq. The straight-through estimator hands ze the same (0,1), which pushes it sideways. The rotation trick turns g back by the quarter turn and scales it by the length ratio 2, giving (2,0). That points along ze, so the step shrinks ze, which is what the decoder asked of zq.
In the tiny case, r=(1,1)/2, and the formula gives R=(01−10), the quarter turn. Then sR⊤g=2(1,0)=(2,0), as claimed.
∂ Why this R is the rotation, as two Householder reflections
A Householder reflection Hu=I−2uu⊤ for a unit vector u mirrors space across the hyperplane orthogonal to u. It is symmetric and orthogonal, with determinant −1. Write c=z^e⊤z^q, so ∥z^e+z^q∥2=2(1+c) and r⊤z^e=r⊤z^q=(1+c)/2(1+c)=(1+c)/2.
First reflect across the hyperplane orthogonal to r. Since 2r(r⊤z^e)=(z^e+z^q)(1+c)/(1+c)=z^e+z^q, we get Hrz^e=−z^q. Then reflect across the hyperplane orthogonal to z^q, which sends −z^q to z^q. The product Hz^qHr is orthogonal with determinant (−1)2=1, so it is a rotation, and it maps z^e to z^q. A vector orthogonal to both z^e and z^q is orthogonal to r as well, so both reflections leave it alone.
Expanding the product,
Hz^qHr=I−2rr⊤−2z^qz^q⊤+4(z^q⊤r)z^qr⊤,
and the last term is 4(1+c)/2z^q(z^e+z^q)⊤/2(1+c)=2z^qz^e⊤+2z^qz^q⊤. Two terms cancel, leaving R=I−2rr⊤+2z^qz^e⊤. Applying it, or its transpose, to a vector costs two dot products and needs no dz×dz matrix. The formula fails only when z^q=−z^e. There r is undefined, and any rotation by π in a plane containing z^e will do.
The construction says how to compute the rotation. The reason to want it is one property that the straight-through estimator lacks.
∂ Proof that the angle is preserved
R is orthogonal, so ∥sR⊤g∥=s∥g∥. And (sR⊤g)⊤ze=sg⊤Rze=s∥ze∥g⊤z^q. Dividing by the two norms, s∥g∥ and ∥ze∥, gives cos(sR⊤g,ze)=g⊤z^q/∥g∥=cos(g,zq). ∎
In plain words, if the decoder wants zq to grow, the rotation trick asks ze to grow along its own direction. The straight-through estimator would instead ask it to move in a fixed direction, which may point partly sideways for a vector near the edge of the cell. When ze and zq point the same way, R=I, and the trick reduces to the straight-through estimator scaled by s.
In the widget the decoder's loss is a tilted bowl with its lowest point at the target square. The dashed arrow is the true downhill step at the encoder output, the step an unquantized autoencoder would take.
What gradient does the encoder actually receive through a quantizer?
Drag the ring, which is an encoder output, or the target square. Ovals mark equal loss, and patches are the codes’ cells.
Loading interactive figure…
Drag ze toward the edge of its cell. The straight-through step stays the same everywhere in the cell, and it parts ways with the dashed step. Switch to the rotation trick. The step now turns through the angle between the two lines from the origin, so it keeps its angle to the vector. That is a different goal from matching the dashed step, and as you drag, the step lands nearer to it or farther from it. Then go back to straight-through and switch to the whole plane. Every point of a cell gets the same arrow, the checkerboard pattern that Fifty et al. draw, and the rotation trick breaks that pattern.
Research
The rotation trick, forward matrix and batched backward
rotation_matrix(e, q) returns the d×d matrix sR, where s=∥q∥/∥e∥ and R is the rotation that takes e^ to q^ in their plane and fixes every vector orthogonal to both, so that sRe=q.
rotation_trick_backward(z_e, z_q, g) takes three arrays of shape (n, d) and returns the encoder gradients sR⊤g, row by row, in O(nd) work and without forming any d×d matrix.
Both must stay finite when q points exactly opposite to e. In that case any rotation by π in a plane containing e^ is acceptable. The tests check the mapping, that R is a rotation (orthogonal with determinant 1) that moves only the plane of e and q, the preserved angle, and the batched form against the matrix and for speed.
import numpy as np
def rotation_matrix(e, q):
"""The rotation-trick Jacobian s R for one pair, with s R e = q and R a rotation (d >= 2)."""
return np.eye(len(e)) # placeholder: the straight-through Jacobian
def rotation_trick_backward(z_e, z_q, g):
"""Rows of z_e, z_q, g are (n, d). Returns s R^T g for every row without forming R."""
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Learning the codebook as online k-means
Look again at the codebook gradient, 2nk(ck−zˉk). It points from the mean of the code's vectors to the code. Hold the assignments fixed, and the codebook loss ∑i:ki=k∥ze,i−ck∥2 is a sum of squared distances, which is smallest when the code sits at the mean, ck=zˉk.
Assigning every vector to its nearest code and then moving each code to the mean of its vectors is one round of Lloyd's algorithm for k-means. §7.4 used the same algorithm to build its search indexes. So VQ-VAE learns its codebook by k-means, with two differences. The data, the encoder outputs, move while it runs. And they arrive one minibatch at a time.
A plain gradient step with learning rate η gives ck←(1−2ηnk)ck+2ηnkzˉk. That is a step toward the mean, but its size is proportional to the count nk. With η=0.01, a code that won 4 vectors moves 8 percent of the way to their mean, and a code that won 40 moves 80 percent. Popular codes jump to their data while rarely chosen codes crawl, and a code with nk=0 does not move at all. Appendix A.1 of the VQ-VAE paper proposes an update whose step is normalized by the count.
Here is one step with numbers, first for a rarely chosen code and then for a popular one.
That equal treatment is no accident. Over many batches the EMA computes exactly the k-means mean of the vectors each code has won, with recent batches weighted more.
∂ Proof that the EMA update is online k-means
Unrolling the recursions gives Nk(t)=γtNk(0)+(1−γ)∑sγt−snk(s) and mk(t)=γtNk(0)ck(0)+(1−γ)∑sγt−s∑ize,i. The loss Jk(t) is a weighted sum of squared distances from c to a set of points. The old code has weight γtNk(0), and each assigned vector of batch s has weight (1−γ)γt−s. Setting its gradient to zero shows that its minimizer is the weighted mean of those points. The numerator of that mean is mk(t) and the denominator is Nk(t).
For the last statement, substitute mkold=Nkoldckold into one update. This gives cknew=(1−w)ckold+wzˉk with w=(1−γ)nk/(γNkold+(1−γ)nk). In steady state Nk≈nk, so w≈1−γ. ∎
In plain words, each code sits at the mean of every vector it has won over roughly the last 1/(1−γ) batches, 100 batches for γ=0.99, with recent batches weighted more. That is what k-means would compute if the encoder stopped moving, and the forgetting is what lets the codebook follow an encoder that keeps moving. A code that stops winning keeps its position, because its sum and its count decay together. Practitioners believe the EMA update is more stable than the codebook loss, and the simulation below agrees. An interviewer will expect you to derive it from the k-means mean.
Core
The EMA codebook update with Laplace smoothing
Write ema_codebook_update(N, m, Z, idx, gamma, eps). The state is the EMA counts N of shape (K,) and the EMA sums m of shape (K, d). The batch is Z of shape (n, d), with its code indices idx of shape (n,). Return (N_new, m_new, C_new), where
Nk←γNk+(1−γ)nk and mk←γmk+(1−γ)∑i:ki=kzi, and
the codebook is ck=mk/N~k with the smoothed counts N~k=(Nk+ϵ)n/(n+Kϵ), where n=∑jNj after the update.
Return the unsmoothed N_new, since the smoothing is only for the division, as in Sonnet. One code in the tests has never been used, so its count is zero.
import numpy as np
def ema_codebook_update(N, m, Z, idx, gamma=0.99, eps=1e-5):
"""N: (K,) EMA counts, m: (K, d) EMA sums, Z: (n, d) encoder outputs, idx: (n,) their codes.
Returns (N_new, m_new, C_new). N_new is stored unsmoothed; only the division is smoothed."""
N_new = gamma * N
m_new = gamma * m
return N_new, m_new, m_new / N_new[:, None]
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Codebook collapse and how to prevent it
Everything so far trains only the codes that win. A code with no assigned vectors gets exactly zero gradient, and the EMA leaves it where it is. So a code that starts far from the encoder outputs, or that the encoder outputs drift away from, is never chosen and never updated. Meanwhile the live codes move toward the data and their cells grow, covering the space where the dead codes might have won. This is codebook collapse, also called index collapse. By the usage theorem above, it directly lowers the information the code can carry.
Here are both numbers for a small codebook.
Measure both over many vectors, not over one batch. A batch of n vectors can show at most n distinct codes. With K=16,384 and a batch of 4,096 positions, the per-batch usage can never exceed 25 percent, even for a perfect codebook. LlamaGen, an image generator built on the Llama language-model architecture (Sun et al. 2024), counts usage over a queue of the last 65,536 assignments.
Python · cell 4
Python · cell 5
Data on eight blobs is gentle. In a 256-dimensional latent space with 16,384 codes, the chance that a randomly placed code lands near any encoder output is far smaller. That is why collapse gets worse as K and dz grow, as the measurements below show.
Why do codebooks collapse, and what keeps every code alive?
Sixteen codes learn eight blobs of data. Each shaded cell holds the points nearest one code.
Loading interactive figure…
Pick a starting position for the codes, an update rule, and whether to restart dead codes, then press Train. Dead codes are drawn hollow. Start far away with no restarts and watch most codes stay hollow. Then turn on restarts, or start the codes on the data.
Each fix attacks one of three causes. The codes may start in a bad place, codes that lose may never get a gradient, or the geometry may leave most codes far from the data.
Start the codes on the data. Initialize the codebook by running k-means on a sample of encoder outputs. Łańcucki et al. (2020) went further. During early training they periodically re-estimated the whole codebook on recent encoder outputs with k-means++, a version of k-means whose starting centers are spread apart. They also added batch normalization before the quantizer and gave the codebook its own, larger learning rate.
Restart dead codes. Whenever a code's average usage falls below a threshold, move it to a random encoder output from the current batch, as the Jukebox music model did (Dhariwal et al. 2020). It costs nothing at inference and guarantees every code gets data.
Look up codes in a small, normalized space. Project each encoder output to a low-dimensional lookup space, and ℓ2-normalize both the projection and the codes. The squared distance then depends only on the cosine similarity. In a few dimensions codes and data overlap far more easily. Yu et al. (2021) introduced this in ViT-VQGAN, projecting from 768 dimensions down to 32 or 8.
Update the whole codebook at once. Write the codebook as CvqW, with a frozen random Cvq and a learned shared matrix W∈Rdz×dz. Then every gradient moves every code (Zhu et al. 2024, SimVQ). A related method freezes 100,000 codes, the k-means centers of image features from a pretrained CLIP encoder, and trains only a linear projection of them. It reports usage above 99 percent (Zhu et al. 2024, VQGAN-LC). Huh et al. (2023) propose a related shared affine reparameterization of the codes.
Fix the gradient. The rotation trick above changes how the encoder's gradient crosses the quantizer, and can raise usage.
Give up the learned codebook. Finite scalar quantization and lookup-free quantization, the subject of §32.6, round each channel to a fixed grid, so there is no codebook to collapse.
Interview
Codebook health, and dead-code restarts
Write two utilities that a VQ training loop needs.
codebook_stats(idx, K) returns the usage fraction (codes used at least once, divided by K) and the perplexity eH(pˉ), with the entropy in nats and 0log0=0.
restart_dead_codes(C, counts, Z, rng, threshold=1.0) returns a copy of C in which every code whose count is below threshold is replaced by a randomly chosen row of Z, the current encoder outputs. Draw the rows with the generator rng.
Live codes must not change, C must not be modified in place, and no two restarted codes may receive the same row. You may assume Z has at least as many rows as there are dead codes.
import numpy as np
def codebook_stats(idx, K):
"""idx: (n,) code indices in [0, K). Returns (usage fraction, perplexity e^H in nats)."""
raise NotImplementedError
def restart_dead_codes(C, counts, Z, rng, threshold=1.0):
"""Replace every code with count below threshold by a distinct random row of Z."""
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
Discrete codes without the straight-through estimator
The straight-through estimator is not the only way through a discrete choice. The other way keeps a real probability for each of the K codes and samples the code from a smoothed version of that distribution, which has a gradient. DALL-E, OpenAI's first text-to-image model, tokenized its images this way, with a discrete VAE (dVAE) (Ramesh et al. 2021).
Let ℓ∈RK be the encoder's logits at one position, scores whose softmax gives the probability of each code. The smoothing rests on the Gumbel-max trick of §0.4. Draw independent standard Gumbel noise gk=−log(−logUk), with each Uk uniform on (0,1). Here gk is noise, not a gradient. Then argmaxk(ℓk+gk) is an exact sample from softmax(ℓ). The argmax has no gradient, so the relaxation replaces it by a softmax with a temperature. This is the Gumbel-softmax, found independently by Jang, Gu & Poole (2017) and, as the Concrete distribution, by Maddison, Mnih & Teh (2017).
The general recipe is the same for any logits and any temperature.
In plain words, add the right noise to the logits, then take a soft argmax instead of a hard one. The noise is drawn independently of ℓ, so this is the reparameterization trick of §32.3, applied to a relaxed categorical variable. The temperature trades bias against variance. A high τ gives smooth samples, unlike the one-hot codes the model will see at test time, so the gradient is biased but steady. A low τ gives nearly one-hot samples and a nearly unbiased gradient with high variance. The cell shows the argmax frequencies staying exact while the samples sharpen.
Python · cell 6
At τ=5 the average sample is close to uniform, even though its argmax is distributed exactly as (0.6,0.3,0.1). At τ=0.1 the samples are almost one-hot, with a largest entry of 0.963 on average, and their mean is the softmax itself.
The dVAE puts this relaxation inside the ELBO. At each of 32×32 positions the encoder outputs K=8,192 logits. The decoder's first layer is a 1×1 convolution, a matrix applied at every position, whose columns are the code vectors. It turns the relaxed one-hot y into ∑kykck, a weighted average of code vectors. The KL to the uniform prior is now a genuine, input-dependent term, DKL(qϕ(⋅∣x)∥uniform)=logK−H(qϕ(⋅∣x)) per position.
Ramesh et al. annealed τ from 1 to 1/16 over the first 150,000 updates with a cosine schedule, and report that a linear schedule usually diverged. They raised the KL weight from 0 to 6.6 over the first 5,000 updates. To tokenize images for the Transformer, they took the argmax of the encoder's logits with no noise. The 1,024 tokens of a 256×256 image shrank the Transformer's context by a factor of 196,608/1,024=192 compared with raw pixel values.
The dVAE's codes found a second use. BEiT pretrained Vision Transformers by masking image patches and predicting their DALL-E dVAE tokens (Bao et al. 2021), as §28.5 describes.
Core
Gumbel-softmax samples, soft and hard
Write gumbel_softmax(logits, tau, rng, hard=False) for logits of shape (n, K). Draw independent standard Gumbel noise g=−log(−logU) with U uniform from rng.random. Return one relaxed sample per row, softmax((ℓ+g)/τ), computed stably. With hard=True, return instead the one-hot vector of each row's argmax, which is the forward value of the straight-through Gumbel estimator. The tests check that the argmax frequencies match softmax(ℓ) at two temperatures. That catches two common bugs, dividing only the logits by τ and a wrong sign in the noise.
import numpy as np
def gumbel_softmax(logits, tau, rng, hard=False):
"""logits: (n, K). One relaxed sample per row: softmax((logits + g) / tau), g standard Gumbel.
With hard=True return the one-hot of its argmax instead."""
raise NotImplementedError
Tests, hints, verified solutions, and AI help, with Pro at $7.99 a month.
A prior over codes turns the autoencoder into a generator
A trained VQ-VAE can encode and decode, but it cannot yet generate. During training the prior was uniform and independent across positions. Sampling T independent uniform indices gives garbage, because real index grids are highly structured. A sky code is usually next to another sky code.
The VQ-VAE bound already says what to do. With the encoder and decoder frozen, maximize logpω(k⋆(x)) over the training set. Put the T positions in an order, raster order for an image (left to right, then top to bottom), and factor the prior by the chain rule,
pω(k1,…,kT)=t=1∏Tpω(kt∣k1,…,kt−1).
In plain words, the probability of a whole grid is the probability of each code given the codes before it. Train any autoregressive model with next-token cross-entropy over the K classes, exactly as a GPT is trained on text (§6.2). To generate, sample one index at a time with the methods of §6.4, then run the decoder once.
The prior is also where the bits go. Its average cross-entropy on held-out code grids, in bits, is the length an ideal compressor would need to store a grid using that prior. It replaces the Tlog2K of the uniform prior in the bound.
Here is the whole pipeline at toy scale. The images are 8×8 bars, with every row equal or every column equal. The tokenizer cuts each image into a 4×4 grid of 2×2 patches. It quantizes the patches with a codebook of K=6, learned by ten rounds of Lloyd's algorithm, each one an assignment with assign_vec from earlier followed by an average. That is exactly a VQ-VAE whose encoder and decoder are the identity on patches. The prior predicts each token from all earlier ones with a separate softmax regression per position, a tiny stand-in for a Transformer.
Python · cell 7
Python · cell 8
Python · cell 9
At real scale the original VQ-VAE used a PixelCNN prior for images. A PixelCNN is a convolutional network that predicts each position from the ones above and to the left (van den Oord et al. 2016). For audio it used a WaveNet, the same kind of network for a sequence of sound samples (van den Oord et al. 2016). Since 2021 the prior is almost always a Transformer, in DALL-E, VQGAN, Parti, LlamaGen, and others, as §34.4 describes. The early-fusion multimodal models of §31.6 put image tokens and text tokens in one sequence, and they read and write exactly these image tokens. Neural audio codecs turn speech into the same kind of tokens (§37.3).
How much does a grid of codes actually store? The numbers below are worth being able to compute on a whiteboard.
Bits are not the whole story. A code learned without any labels can also line up with units that people name.
Where this breaks
Four limits push the field beyond the plain VQ-VAE.
Biased gradients. The straight-through error is bounded by a multiple of the quantization error, so a coarse codebook trains its encoder with the wrong gradient.
Collapse at scale. Codebooks collapse more readily as K and dz grow, and the fixes above are patches on that dynamic.
The size of the codebook is a trade-off with the prior. More codes give more bits per token and better reconstructions. But the prior faces a larger softmax and sees each code less often. In LlamaGen's ablation, reconstruction stopped improving past 16,384 codes. Reconstruction FID was 3.02, 2.91, 2.19, and 2.26 for 4,096, 8,192, 16,384, and 32,768 codes (Table 2).
Blur. Squared-error reconstruction from a few codes is blurry, because the best squared-error guess is the average of all plausible images (§32.1).
The next section adds perceptual and adversarial losses and replaces the learned codebook with quantizers that cannot collapse.
In PyTorch
A production quantizer keeps the codebook in buffers updated by the EMA rule, uses the one-line straight-through estimator z_e + (z_q - z_e).detach(), tracks usage, and restarts dead codes. The block trains a tiny linear encoder and decoder through it on 2-D blobs and checks that the encoder receives a gradient. It then shows PyTorch's built-in straight-through Gumbel-softmax.
🔥 In PyTorchfor a machine with PyTorch
The table compares the four ways of training a discrete code that this chapter covers.
gradient of ∥sg[ze]−c∥2, step size proportional to usage
running means, online k-means
gradient of the ELBO through the soft lookup
fixed grid, nothing to learn
extra losses
codebook and commitment
commitment
KL to the prior, weight β
none (LFQ adds an entropy penalty)
codebook usage
prone to collapse
better, still needs restarts or good initialization
good with a large KL weight
near full by construction
used by
VQ-VAE, VQGAN
VQ-VAE-2, Jukebox, SoundStream
DALL-E, BEiT's tokenizer
MAGVIT-v2 (LFQ), MaskGIT and UViM with FSQ
Read more
van den Oord, Vinyals & Kavukcuoglu (2017). Neural Discrete Representation Learning. The VQ-VAE paper. Section 3 gives the one-hot posterior, the constant KL, the straight-through estimator, and the three-term loss, and appendix A.1 the EMA update.
Ramesh et al. (2021). Zero-Shot Text-to-Image Generation. Section 2.1 and appendix A describe the dVAE, its temperature and KL schedules, and the surprising effect of a large KL weight.
🎬 Vector-Quantized Variational Autoencoders (VQ-VAEs) · DeepBeanAn animated walk through nearest-code quantization, the straight-through gradient, and the codebook and commitment losses, a good picture before the derivations.🎬 VQ-VAEs: Neural Discrete Representation Learning | Paper + PyTorch Code Explained · Aleksa Gordić - The AI EpiphanyReads the VQ-VAE paper section by section and then walks through a PyTorch implementation, including the EMA codebook and the PixelCNN prior.🎬 Categorical Reparameterization with Gumbel-Softmax & The Concrete Distribution · Bayesian Deep Learning Workshop NIPS 2016The authors of the two Gumbel-softmax papers present the relaxation, its temperature, and the straight-through variant that DALL-E's dVAE later used.