← All posts

Speech generation from the inside

Post 2 of 8

Residual vector quantization: turning sound into a few small numbers

Posts in this series · 8

The audio-basics post ended with a promise: Mimi squeezes one second of speech down to 400 numbers. Even that is not the end. Models like Moshi want something smaller still: whole numbers, picked from a fixed menu, the way a language model picks words from its vocabulary.

The trick that turns a frame of sound into a few whole numbers is called residual vector quantization, or RVQ. The Pocket TTS post and the Mimi post each give it one formula and one paragraph. This post slows down and builds it from nothing: what a codebook is, why we take leftovers, how the codebooks learn, the ways they break, and how SoundStream, EnCodec, DAC and Mimi each use it. Every example uses points on a flat page (two numbers each) and codebooks with four entries, so you can check all of it with a pencil.

What a codec has to do

A neural audio codec has three parts.

Why bother with whole numbers? Two reasons. Whole numbers are cheap to send: if you and a friend share the same menu, you only need to say “number 7”. And a language model can predict them the same way it predicts the next word. The cost is that a menu has only so many items, so something is always lost. The whole art is losing as little as possible with as few numbers as possible.

A box of paint colours

Think of a paint-by-number kit. The kit has a small box of paints, each with a number on the lid. The picture is covered in little regions, and each region is labelled with the number of the paint that comes closest to the real colour. You never mix a new colour. You just point at the closest one in the box.

That is vector quantization, or VQ:

A plain list of numbers like a colour (red, green, blue) or a frame from the encoder is a vector. We will use vectors with just two numbers, so each one is a dot on a page: (3,1.5)(3, 1.5) means “3 to the right, 1.5 up”.

A small picture repainted using only the closest colour from a small box of paintsThe real coloursPainted from the box (number = paint sent)
Paints in the box
Figure 1. Paint by number. Every cell sends only the number of its closest paint. Each box of paints comes from k-means on this picture’s colours (see sorting buttons into piles).

Finding the nearest entry

How do we decide which entry is “closest”? We measure the straight-line distance between two dots, the one a ruler would give. For a frame zz and a codebook with KK entries c1,…,cKc_1, \dots, c_K, the chosen index is

k=arg⁡min⁡j∥z−cj∥2k = \arg\min_{j} \lVert z - c_j \rVert^2

and the quantized frame is z^=ck\hat z = c_k.

What each symbol means:

Intuition: try every paint against the region and keep the one that differs least.

Worked example. Take the codebook with K=4K = 4 entries

c1=(4,0)c_1 = (4, 0), c2=(0,4)c_2 = (0, 4), c3=(−4,0)c_3 = (-4, 0), c4=(0,−4)c_4 = (0, -4)

and the frame z=(3,1.5)z = (3, 1.5).

entryz−cjz - c_jsquared length
c1=(4,0)c_1 = (4, 0)(−1,1.5)(-1, 1.5)1+2.25=1 + 2.25 = 3.25
c2=(0,4)c_2 = (0, 4)(3,−2.5)(3, -2.5)9+6.25=15.259 + 6.25 = 15.25
c3=(−4,0)c_3 = (-4, 0)(7,1.5)(7, 1.5)49+2.25=51.2549 + 2.25 = 51.25
c4=(0,−4)c_4 = (0, -4)(3,5.5)(3, 5.5)9+30.25=39.259 + 30.25 = 39.25

The smallest is 3.25, so k=1k = 1 and z^=(4,0)\hat z = (4, 0). We send one number, “1”. The error left over is z−z^=(−1,1.5)z - \hat z = (-1, 1.5), with squared length 3.25.

How much is one index worth? A codebook with KK entries needs log⁡2K\log_2 K bits to name an entry. (log⁡2K\log_2 K asks “how many times do I double 1 to reach KK?”) With K=4K = 4, that is log⁡24=\log_2 4 = 2 bits: the four indices are 00, 01, 10, 11. (Bits are explained in the audio-basics post.) Real codecs use K=1,024K = 1{,}024 (10 bits) or K=2,048K = 2{,}048 (11 bits).

In a real codec zz has hundreds of numbers instead of two, but the recipe is the same: square the differences, add them up, keep the smallest.

Why one codebook is not enough

The error of 3.25 above is big. The obvious fix is a bigger box of paints: more entries means some entry lands closer to every frame. But the box grows terribly fast.

SoundStream, Google’s 2021 codec, makes the problem concrete. It writes 75 frames a second, and at a bitrate of 6,000 bits per second (6 kbps, kilobits per second) each frame gets 6000/75=6000 / 75 = 80 bits. One codebook spending all 80 bits needs 2802^{80} entries, about a million million million million. No computer can store that, let alone search it each frame. SoundStream’s authors call it “obviously unfeasible”.

We need a way to spend many bits without building one giant box. That is where leftovers come in.

Quantize what is left over

Think of giving someone directions to a café. First you name a big landmark: “go to the train station”. That gets them close, but not there. Then a correction: “from the station, walk two blocks north”. Then a smaller one: “it is the third door on the left”. Each instruction only has to fix what the previous one left wrong, so each one can come from a short list.

RVQ does exactly this with codebooks. The error left after one codebook is called the residual (from “residue”, what remains). A second codebook quantizes the residual. A third quantizes what the second left over, and so on:

r0=z,ki=arg⁡min⁡j∥ri−1−cj(i)∥2,ri=ri−1−cki(i)r_0 = z, \qquad k_i = \arg\min_j \lVert r_{i-1} - c^{(i)}_j \rVert^2, \qquad r_i = r_{i-1} - c^{(i)}_{k_i} z^=∑i=1Qcki(i)\hat z = \sum_{i=1}^{Q} c^{(i)}_{k_i}

What each symbol means:

Intuition: each stage zooms in on the mistake of the stage before. The first codebook paints in big strokes; later ones paint in finer and finer touch-ups. That is why the entries of later codebooks end up smaller after training.

Worked example. Keep z=(3,1.5)z = (3, 1.5) and stage 1’s codebook from the last section. Add two smaller codebooks, each half the size of the one before:

Stage 1 picks (4,0)(4, 0), as we found. Residual r1=(−1,1.5)r_1 = (-1, 1.5), squared length 3.25.

Stage 2 compares r1=(−1,1.5)r_1 = (-1, 1.5) with its four entries:

entryr1−cr_1 - csquared length
(1,1)(1, 1)(−2,0.5)(-2, 0.5)4.254.25
(−1,1)(-1, 1)(0,0.5)(0, 0.5)0.25
(−1,−1)(-1, -1)(0,2.5)(0, 2.5)6.256.25
(1,−1)(1, -1)(−2,2.5)(-2, 2.5)10.2510.25

It picks entry 2, (−1,1)(-1, 1). Residual r2=(0,0.5)r_2 = (0, 0.5), squared length 0.25.

Stage 3 compares r2=(0,0.5)r_2 = (0, 0.5) with its entries; (0,0.5)(0, 0.5) itself is in the list, distance 0. It picks entry 2. Residual r3=(0,0)r_3 = (0, 0).

Rebuild: z^=(4,0)+(−1,1)+(0,0.5)=(3,1.5)\hat z = (4, 0) + (-1, 1) + (0, 0.5) = (3, 1.5). Exactly zz.

The squared error fell 11.25 → 3.25 → 0.25 → 0 (11.25 is 32+1.523^2 + 1.5^2, the error of sending nothing at all). We sent three indices, 1, 2, 2, at 2 bits each: 6 bits in all, and we stored only 3×4=3 \times 4 = 12 entries. One codebook with 6 bits would need 26=2^6 = 64 entries to offer the same number of choices. With 80 bits, the gap becomes 8×1,024=8,1928 \times 1{,}024 = 8{,}192 stored entries against 2802^{80}. Real frames are almost never rebuilt exactly; this toy was picked so the numbers come out clean.

Three codebooks each fixing what the previous one missed, with the view zooming in on the leftoverStage by stage● the frame z (drag it)◆ entries of the current codebook→ entries chosen so far, tip to tail
Stages
Figure 3. Directions, then corrections. Each stage zooms in on what the last one missed. Defaults reproduce the worked example: errors 11.25, 3.25, 0.25, 0 and indices 1, 2, 2. Drag z anywhere.

Two consequences we will use later:

Counting bits

Once the codebooks are fixed, the bitrate is pure arithmetic:

R=f×Q×log⁡2KR = f \times Q \times \log_2 K

What each symbol means:

Intuition: bits per second = frames per second, times indices per frame, times bits per index.

Worked example (toy): f=10f = 10 frames a second, Q=3Q = 3, K=4K = 4 gives 10×3×2=10 \times 3 \times 2 = 60 bits per second.

Worked examples (real codecs, all from the papers listed in Sources):

codecffQQKKRR
SoundStream at 6 kbps7581,02475×8×10=75 \times 8 \times 10 = 6,000
EnCodec at 24 kHz, 6 kbps7581,0246,000
EnCodec at 24 kHz, 24 kbps75321,02424,000
DAC at 44.1 kHz8691,02486×9×10=7,74086 \times 9 \times 10 = 7{,}740, which the paper calls 8 kbps
Mimi in Moshi12.582,04812.5×8×11=12.5 \times 8 \times 11 = 1,100

For comparison, the raw 24 kHz recording at 16 bits a sample is 24,000×16=24{,}000 \times 16 = 384,000 bits per second. Mimi’s tokens are about 350 times smaller.

Mimi wins mostly on ff: it writes 6 times fewer frames than EnCodec, which matters even more for a language model than for a phone line, because every frame is a step the model must predict.

One second of codec tokens and the bitrate they add up toOne second of tokens: a column per frame, a cell per codebook0 s1 s
Preset
Figure 4. Counting bits. Frames per second, times codebooks, times bits per index. Preset values from the papers listed in Sources.

How codebooks learn

So far the codebooks were handed to us. In a real codec they are learned from data, together with the encoder and decoder. The goal: place the entries where the frames actually are.

Sorting buttons into piles

Pour a jar of buttons on a table and make four piles, one per colour. You put a few piles down, drop each button on the pile it looks most like, then move each pile’s label to the average colour of the buttons on it. Repeat, and the labels settle.

That is k-means clustering:

  1. Assign. Each frame goes to its nearest entry (the lookup from before).
  2. Update. Each entry moves to the average of the frames assigned to it:
cj←1nj∑x∈Sjxc_j \leftarrow \frac{1}{n_j} \sum_{x \in S_j} x

What each symbol means:

Intuition: each paint moves to the middle of the regions that chose it.

Worked example. Entry c1c_1 won three frames: (2,0)(2, 0), (4,2)(4, 2) and (3,1)(3, 1). The sum is (9,3)(9, 3), n1=3n_1 = 3, so c1c_1 moves to (3,1)(3, 1). Repeat assign and update until nothing moves.

SoundStream uses k-means only to start: it runs k-means on the first training batch and uses the centres as the initial codebook, so the entries begin near real frames instead of at random spots.

Codebook entries moving to the middle of the points they winStepTotal squared errorEntries in usePerplexity (worth this many)
Run
Start entries
Update
Figure 2. Sorting buttons into piles. Assign, average, repeat. Switch the update to EMA to see entries take smaller steps, or start them in the corner to make dead codes.

A running average instead of a full redo

During training, frames arrive in small batches and the encoder keeps changing, so redoing k-means every step is wasteful. Instead each entry keeps a running tally and drifts a little each step. This is the exponential moving average, or EMA, update (SoundStream and EnCodec both use it, following VQ-VAE-2, the vector-quantized variational autoencoder, version 2):

Nj←γNj+(1−γ) nj,mj←γmj+(1−γ)∑x∈Sjx,cj=mjNjN_j \leftarrow \gamma N_j + (1 - \gamma)\, n_j, \qquad m_j \leftarrow \gamma m_j + (1 - \gamma) \sum_{x \in S_j} x, \qquad c_j = \frac{m_j}{N_j}

What each symbol means:

Intuition: instead of jumping to this batch’s middle, the entry takes a small step toward it, so one odd batch cannot throw it around.

Worked example (with γ=0.5\gamma = 0.5 so the numbers stay small). Entry c1c_1 has N1=2N_1 = 2 and m1=(4,0)m_1 = (4, 0), so it sits at (2,0)(2, 0). This batch it wins the three frames above: n1=3n_1 = 3, sum (9,3)(9, 3).

Plain k-means would jump to (3,1)(3, 1); EMA moves part of the way, from (2,0)(2, 0) to (2.6,0.6)(2.6, 0.6).

Note that the EMA update does not use gradients at all. The encoder still needs gradients, which raises a problem we meet below.

Paints nobody uses

Here is how codebooks break. Suppose one paint in the box is a colour no region is close to. It never wins a lookup, so its pile is always empty, so the update never moves it, so it never wins. It is stuck forever. Such an entry is a dead code. When many entries die, the codebook has effectively shrunk: a 1,024-entry codebook that only uses 100 entries delivers about log⁡2100≈6.6\log_2 100 \approx 6.6 bits instead of 10. This is codebook collapse.

A useful health number is the codebook usage (how many entries win at least once over many frames), or its smoother cousin, perplexity:

PPL=2H,H=−∑j=1Kpjlog⁡2pj\text{PPL} = 2^{H}, \qquad H = -\sum_{j=1}^{K} p_j \log_2 p_j

What each symbol means:

Intuition: perplexity counts the entries that are pulling their weight.

Worked example. K=4K = 4. If all four are used equally, each pj=0.25p_j = 0.25, log⁡20.25=−2\log_2 0.25 = -2, so H=4×0.25×2=2H = 4 \times 0.25 \times 2 = 2 bits and PPL=4\text{PPL} = 4. If two entries are dead and the other two split the frames, p=(0.5,0.5,0,0)p = (0.5, 0.5, 0, 0), H=2×0.5×1=1H = 2 \times 0.5 \times 1 = 1 bit and PPL=2\text{PPL} = 2. Half the box is wasted.

Three fixes, each used by a codec in this post.

Fix one: random restarts

If a paint is never used, throw it out and refill the pot with a colour copied from the picture. SoundStream tracks the EMA of each entry’s assignment count (decay 0.99) and, when it falls below 2, replaces the entry with a frame sampled at random from the current batch. EnCodec does the same: unused entries are replaced with a candidate from the current batch. A copied frame is, by construction, near real data, so it starts winning lookups at once.

Fix two: look up in a small space

In hundreds of dimensions, most of the space is empty, and an entry placed at random is far from everything. DAC (the Descript Audio Codec, 2023) factorizes the codes: before the lookup it projects each vector down to just 8 numbers with a small learned map, does the nearest-neighbour search there, and projects the winner back up. In 8 dimensions the frames fill the space much more densely, so entries are close to something and stay alive. The bits are unchanged: the index still names one of 1,024 entries.

Fix three: compare directions, not lengths

DAC also l2-normalizes both the frame and every entry before the lookup: it rescales each to length 1 (“l2” is the ruler length from before). Then every point sits on a circle (in 2-D) or a sphere, and the nearest entry is simply the one pointing in the most similar direction. In the DAC paper’s words, this “converts euclidean distance to cosine similarity”.

∥u−v∥2=2−2 u⋅vwhen ∥u∥=∥v∥=1\lVert u - v \rVert^2 = 2 - 2\, u \cdot v \quad \text{when } \lVert u \rVert = \lVert v \rVert = 1

What each symbol means:

Intuition: on a circle, being close and pointing the same way are the same thing. A lone entry can no longer hide far away “out in the corner”, because there are no corners: every entry is on the same circle as the data.

Worked example. Frame z=(3,4)z = (3, 4) has length 9+16=5\sqrt{9 + 16} = 5, so normalized u=(0.6,0.8)u = (0.6, 0.8). Entry (0,10)(0, 10) normalizes to v=(0,1)v = (0, 1). Then u⋅v=0.8u \cdot v = 0.8 and ∥u−v∥2=0.62+0.22=0.40\lVert u - v \rVert^2 = 0.6^2 + 0.2^2 = 0.40, which matches 2−2×0.8=0.402 - 2 \times 0.8 = 0.40. Before normalizing, (0,10)(0, 10) was far from (3,4)(3, 4) (squared distance 45); after, it is a near neighbour.

Codebook entries that never win stay stuck, unless they are restarted on real dataSmoothed wins per batch, N_j
Training
Random restarts
Decay γ
Figure 5. Paints nobody uses. EMA updates on batches of 32 points. An entry that never wins never moves, unless we refill it from the data. The threshold of 2 is SoundStream’s.
Normalizing points and entries onto a circle so that nearest means most similar directionPoints won by each entryThe outlined point z = (3, 4) against entry 4
Lookup
Figure 6. Compare directions, not lengths. On the circle there are no far corners for an entry to hide in. The readout checks the worked example: 45 before, 0.40 after.

The straight-through trick

The encoder, decoder and (in DAC) the codebooks are trained by gradient descent: run the codec, measure how wrong the rebuilt sound is with a loss (one number, bigger is worse), and nudge every weight a little in the direction that shrinks the loss. The direction is the gradient, and it is passed backward from the loss through each step, a process called backpropagation.

The lookup breaks that chain. Picking the nearest entry is a jump: move zz a tiny bit and the choice usually does not change at all, so the output does not move, and the gradient through the lookup is zero almost everywhere. The encoder would never learn.

The fix is the straight-through estimator: on the way forward use the quantized z^\hat z; on the way backward pretend the quantizer was not there and hand the decoder’s gradient straight to zz. In code it is one line:

zq=z+sg⁡(z^−z)z_q = z + \operatorname{sg}(\hat z - z)

What each symbol means:

Intuition: forward, z+(z^−z)=z^z + (\hat z - z) = \hat z, so the decoder sees the quantized frame. Backward, the stop-gradient part counts as a constant, so zqz_q behaves like zz and the gradient flows to the encoder as if quantization were the identity. EnCodec describes it exactly this way: gradients are computed “as if the quantization step was the identity function during the backward phase”.

Worked example. z=(3,1.5)z = (3, 1.5), z^=(4,0)\hat z = (4, 0). Forward: zq=(3,1.5)+(1,−1.5)=(4,0)z_q = (3, 1.5) + (1, -1.5) = (4, 0). Suppose the decoder’s loss says “the first number should go down by 0.2”: gradient (0.2,0)(0.2, 0) at zqz_q. Straight-through copies it to zz unchanged, (0.2,0)(0.2, 0), and the encoder learns to push its first number down. The gradient is a guess (it ignores that a big enough move would switch entries), but it points the right way often enough to train.

A gradient stopping at the quantizer, or jumping over it with the straight-through trick
Straight-through
Commitment loss
Figure 7. The straight-through trick. Forward uses the rounded value; backward pretends the rounding was not there. Toy numbers from the worked example; press “Run one step”.

Keeping the encoder close

Straight-through gives the encoder a direction, but nothing stops it from wandering away from the codebook, which makes the rounding error large and the borrowed gradient a poor guess. Two extra loss terms, from the VQ-VAE paper, pull the two sides together:

Lvq=∥sg⁡(z)−z^∥2+β ∥z−sg⁡(z^)∥2L_{\text{vq}} = \lVert \operatorname{sg}(z) - \hat z \rVert^2 + \beta\, \lVert z - \operatorname{sg}(\hat z) \rVert^2

What each symbol means:

Intuition: the paint moves toward the region and the region is asked to commit to a paint, so they meet in the middle instead of chasing each other.

Worked example. z=(3,1.5)z = (3, 1.5), z^=(4,0)\hat z = (4, 0), squared distance 3.25 from earlier. Codebook loss =3.25= 3.25; commitment loss =0.25×3.25=0.8125= 0.25 \times 3.25 = 0.8125; total Lvq=4.0625L_{\text{vq}} = 4.0625. Its gradient on zz from the commitment term is 2β(z−z^)=0.5×(−1,1.5)=(−0.5,0.75)2 \beta (z - \hat z) = 0.5 \times (-1, 1.5) = (-0.5, 0.75); a gradient-descent step goes the opposite way, (0.5,−0.75)(0.5, -0.75) times the step size, which points from zz toward (4,0)(4, 0).

In RVQ each stage gets its own copy of these terms, applied to its own residual.

One model, many bitrates

A phone on a bad connection wants fewer bits; a music studio wants more. Training a separate codec for every bitrate is expensive. SoundStream’s answer is quantizer dropout: for each training example, pick a number nqn_q at random, uniformly between 1 and QQ, and use only the first nqn_q codebooks. (“Dropout” is the usual name for randomly switching parts of a network off during training.)

Because the decoder sometimes gets only stage 1, it must make good sound from stage 1 alone; because it sometimes gets all stages, it must use the fine corrections too. At test time you pick nqn_q to choose a bitrate, with no retraining. SoundStream trains one model at 18 kbps and runs it from 3 to 18 kbps; at 9 and 12 kbps the scalable model even scored marginally better than models trained for that bitrate only, which the authors read as dropout acting as a regularizer.

The others follow suit: EnCodec picks a number of codebooks that is a multiple of 4 to cover 1.5, 3, 6, 12 and 24 kbps at 24 kHz; DAC applies quantizer dropout with probability 0.5 (half the examples keep all codebooks), which the paper found kept quality at full bitrate better.

Worked example, using the toy from the residual section: with nq=1n_q = 1 the rebuild is (4,0)(4, 0), squared error 3.25 at 2 bits; nq=2n_q = 2 gives (3,1)(3, 1), error 0.25 at 4 bits; nq=3n_q = 3 gives (3,1.5)(3, 1.5), error 0 at 6 bits. Each extra stage buys a smaller fix, which is why cutting from the end degrades gracefully.

Training with a random number of codebook stages, and what each prefix of stages rebuildsTraining examples: each rolls how many of the 8 stages it may useHow often each stage got trainedAt test time, keep the first n_q stages of the toy frame (3, 1.5)
Training rule
Roll
Figure 8. One model, many bitrates. Stage 1 is trained on every example and later stages less often, so every prefix learns to sound good. Rules from SoundStream (uniform) and DAC (p = 0.5).

How real codecs use it

SoundStream (Google, 2021). 24 kHz audio, 75 frames a second, codebooks of 1,024 entries, 8 quantizers at 6 kbps. EMA codebook updates, k-means start on the first batch, dead-code restarts below a count of 2, and the quantizer dropout above. At a fixed 6 kbps the paper compares 8 codebooks of 1,024, 16 of 32, and 80 of 2; quality (ViSQOL, a computer estimate of listening quality) falls only slightly, 4.01, 3.98, 3.92, which shows how much of the job deep RVQ can do with tiny codebooks.

EnCodec (Meta, 2022). Same frame rate at 24 kHz, 75 frames a second (150 at 48 kHz). Codebooks of 1,024 entries, up to 32 at 24 kHz. EMA updates with decay 0.99, unused entries replaced from the batch, straight-through gradients and a commitment loss. VALL-E builds its speech generator on EnCodec at 8 codebooks (Pocket TTS post).

DAC (Descript, 2023). 44.1 kHz audio, 86 frames a second, 9 codebooks of 1,024 entries for about 8 kbps. Instead of EMA it learns the codebooks by gradient with the codebook and commitment losses, and fights collapse with the 8-dimensional factorized lookup and l2 normalization.

Mimi (Kyutai, 2024, the codec inside Moshi). 24 kHz audio, 12.5 frames a second, 8 codebooks of 2,048 entries, 1.1 kbps. Two twists:

The Mimi post follows the frame through the rest of the network, and explains why the Pocket TTS variant drops the quantizer for a continuous latent.

Mimi's semantic quantizer running beside a seven-stage acoustic quantizer, and the token grid they produceOne second of Mimi tokens: 12.5 columns, one row per codebook
Quantizer
Figure 9. Mimi’s split quantizer. One token for what was said, seven for how it sounded (Moshi paper, Sec 3.3.2). The slider drops acoustic stages and updates the bitrate.

Try it yourself

All in the figures above; nothing to install. Every figure uses a fixed random seed, so you will see the same numbers I did.

  1. Figure 2, k-means. Keep 4 entries and 60 points and press “Step slowly” a few times: each step recolours the points, then slides the diamonds to the middle of their piles, and usage stays 4 of 4. Now choose “Corner” and press “Run 20 steps”: one entry never wins a point and never moves, so usage stays at 3 of 4.
  2. Figure 3, residuals. Press “Reset to (3, 1.5)” and “Next stage” three times. The table must read 11.25, 3.25, 0.25, 0 with indices 1, 2, 2. Then drag the red dot to about (2, 2) and step again: the error no longer reaches zero, because these tiny codebooks cannot spell every point.
  3. Figure 4, bitrate. Pick the Mimi preset, then raise the codebooks from 8 to 32: the rate goes from 1,100 to 4,400 bits per second. Then pick SoundStream 6 kbps and read 6,000.
  4. Figure 5, dead codes. With restarts off, press Run: only 3 of 8 entries ever win points, and perplexity sits near 2.3. Switch restarts on and press Run: after 7 restarts all 8 of 8 win, and perplexity climbs to about 7.
  5. Figure 8, dropout. Roll 1,000 examples under each rule and compare the bars with the red lines: stage 1 is always trained, stage 8 only one time in eight under the uniform rule, and more than half the time under DAC’s rule.

What to take away

Sources