Snyder et al. (ICASSP 2018) · Wan et al. (arXiv:1710.10467) · Desplanques et al. (arXiv:2005.07143)

Whose Voice Is This?

Three products — a bank's phone line, a meeting recorder, a voice cloner — all reduce to the same artifact: one fixed-length vector that says who and nothing about what.

Prerequisites: a dot product and what a mean and a standard deviation are. Spectrograms, pooling, margins, PLDA, EER and clustering are built from zero.
10
Chapters
7
Interactive Sims
192
Dims Per Voice
0.87%
ECAPA Vox1-O EER

Chapter 0: Whose Voice Is This

Three teams in three different buildings are shipping three different products this quarter.

The first team runs a bank's phone line. A caller says "it's me, I'd like to move some money," and the system has eight seconds of speech to decide whether the voice on the line belongs to the person who opened the account four years ago. Wrong in one direction and a customer is locked out of their own money. Wrong in the other direction and someone else empties it.

The second team builds a meeting recorder. Forty-five minutes of audio come in, nobody enrolled, nobody said their name, and the transcript has to come out attributed: Speaker 1 said this, Speaker 2 said that. There is no list of candidates. The system does not even know how many people were in the room.

The third team is making an audiobook. The author recorded thirty seconds of a chapter introduction and then got laryngitis. The product needs the remaining eleven hours to sound like her.

Verification. Diarization. Cloning. Three teams, three roadmaps, three sets of metrics — and one shared artifact underneath all of them, which is the subject of this lesson.

A speaker embedding is a fixed-length vector that answers "who is speaking" and refuses to answer anything else. Feed it eight seconds of a phone call or eleven minutes of a lecture; it returns the same number of floats. Feed it the same person reading two completely different sentences and the two vectors land almost on top of each other. Feed it two different people reading the identical sentence and the two vectors land far apart. Everything in this lesson is machinery for making that last pair of sentences simultaneously true.

Why you cannot just train a classifier

The obvious first design is a classifier: one output per enrolled user, softmax, argmax, done. Spend ten seconds with it and it collapses.

Your bank has 40 million customers. That is a softmax over 40 million classes, which is a final weight matrix of shape (40,000,000 × 512) — roughly 20.5 billion parameters, 82 gigabytes at fp32, in the last layer alone. But size is the least of it. The real problem is that the class list changes every minute. Someone opens an account at 3am. A gradient step is not an acceptable part of a signup flow.

And the meeting recorder makes the failure total: its "classes" are four strangers who will never be seen again, whose number is unknown, and who cannot be enumerated in advance even in principle.

Product questionClassifier answerWhat it actually needs
Is this caller the account holder?Needs a row for that account holder, trainedA comparison against a stored vector
Which of my 40M customers is this?A 20-billion-parameter output layer that is stale by lunchtimeA nearest-neighbour search over 40M stored vectors
Who spoke when in this meeting?Impossible — no classes existClustering of vectors with unknown k
Make the TTS sound like herMeaningless — a class index is not a control signalA vector to condition a generator on

Read the right-hand column. Compare, search, cluster, condition. Not one of those is a classification. All four are operations on a geometry. That is the shift: stop asking the network for an answer and start asking it for a coordinate.

The scaffolding gets thrown away. Nearly every model in this lesson is trained as a classifier over a few thousand training speakers — and then the classification layer is deleted before deployment. The output was never the point. The point was to force the layer below the output to arrange voices usefully. We train a model to answer a question we do not care about, so that it learns a representation we do.

The contract, stated precisely

Write it as a function. Let x be an utterance — a variable-length sequence of audio samples. We want an encoder f such that:

f(x) ∈ Rd   for every x, regardless of length

and such that for a fixed similarity function s (a dot product, near enough), the following holds for speakers who were never in the training set:

s( f(x1), f(x2) ) is large when x1, x2 are the same person
s( f(x1), f(x3) ) is small when they are different people

That last clause — never in the training set — is the whole difficulty, and it is worth naming why. A classifier is evaluated on new examples of old classes. A speaker embedding is evaluated on new classes entirely. The VoxCeleb2 training set has 5,994 speakers; the VoxCeleb1 test set has 40, and none of them appear in training. We are asking a model to generalise along an axis it was never scored on.

Classifier generalisation
New photo of a cat → still "cat". The class was in training. The model has seen 10,000 cats.
↓ a much stronger demand
Embedding generalisation
A voice the model has never heard, from a language it was never trained on, over a codec that did not exist at training time → still lands next to that same voice tomorrow.

Four invariances, and one thing that must survive

An embedding is defined as much by what it throws away as by what it keeps. Here is the full list of what must be discarded, and it is longer than people expect.

Nuisance factorConcrete instanceWhy it is hard to discard
ContentEnrolled saying "my voice is my password", tested saying "move two thousand dollars"Phonemes dominate the spectrum. The loudest structure in the signal is the words
DurationEnrolled on 30 s, tested on 2 sShort utterances give fewer phonemes, so the estimate is noisier — and noisier in a way that is systematically biased, not just random
ChannelEnrolled on a studio mic, tested over a GSM codec through a car speakerphoneChannel effects are multiplicative in the spectrum and can be larger than speaker effects
SessionSame person, with a cold, three years later, shouting over trafficThe speaker's own voice genuinely changed. There is no clean target
LanguageEnrolled in Cantonese, tested in EnglishPhoneme inventories differ, so the two utterances do not even span the same acoustic territory
Speaker identityVocal tract length, glottal source, habitual pitch, formant trajectories, timing habitsThis is the one thing that must survive. It is entangled with all five rows above

Notice that the first five rows are not noise in the statistical sense. They are structured, high-energy, and correlated with identity. A person's habitual choice of words is a bit of an identity cue; a person's usual phone is correlated with who they are. Which means an embedding that is too good at exploiting shortcuts will look wonderful on your test set and fall over in production, when the same speaker calls from a different handset. Chapter 6 will make this failure measurable.

What one vector actually costs

Let us make this physical, because the numbers are pleasant and they explain why the whole field settled on this design.

Take four seconds of speech sampled at 16 kHz. That is 4 × 16,000 = 64,000 samples. As 16-bit PCM it is 128 kB. Now run it through the encoder we will build in Chapter 4 and out comes a 192-dimensional float vector: 192 × 4 = 768 bytes.

64,000 samples → 192 floats   =   333× compression, and the ratio grows with duration

Enrol 40 million customers and the entire biometric database is 40,000,000 × 768 bytes = 30.7 GB — a single machine. Identification against all of them is one matrix product: a (40,000,000 × 192) matrix times a 192-vector is 40,000,000 × 192 × 2 ≈ 15.4 GFLOPs, which a mid-range GPU finishes in well under a second. The entire "which of 40 million people is this" problem is one matmul. That is the payoff for insisting on a fixed-length vector with a linear similarity.

Design consequence, stated early. The similarity has to be cheap and fixed — a dot product — because everything downstream is built on doing it billions of times. If scoring required running a neural network on each pair, none of the four products above would exist in their current form. Chapter 2's PLDA backend is the historical exception, and Chapter 5 explains exactly why the field eventually got rid of it.

Scoring by hand, once, so nothing is mysterious

Before any architecture, do the arithmetic on toy vectors. Suppose our embeddings are four-dimensional (they will be 192 later; four is enough to see it). The enrolled template for Ava is:

a = (3, 4, 0, 0)

Its length is √(32 + 42) = √25 = 5, so the unit vector is â = (0.6, 0.8, 0, 0). Now three test utterances arrive.

Test 1 — Ava again, different sentence: b = (4, 3, 0, 0), length 5, unit (0.8, 0.6, 0, 0).

cos(a, b) = 0.6×0.8 + 0.8×0.6 + 0 + 0 = 0.48 + 0.48 = 0.96

Test 2 — an obvious impostor: c = (0, 0, 5, 0), unit (0, 0, 1, 0).

cos(a, c) = 0.6×0 + 0.8×0 + 0×1 + 0 = 0.00

Test 3 — a harder impostor, someone with a similar voice: d = (1, 4, 3, 0). Its length is √(1 + 16 + 9) = √26 = 5.099, so the unit vector is (0.1961, 0.7845, 0.5883, 0).

cos(a, d) = 0.6×0.1961 + 0.8×0.7845 = 0.1177 + 0.6276 = 0.7453

Now set a decision threshold. At θ = 0.5 we accept tests 1 and 3 and reject test 2 — which means we just let an impostor into Ava's account. Raise θ to 0.8 and we correctly reject d, but we would also have rejected a genuine Ava utterance that happened to score 0.78 on a noisy line.

The entire discipline lives in that paragraph. There is no threshold that is right. There is only a threshold that trades one kind of wrongness for another, and the exchange rate depends on whether you are guarding a bank or unlocking a smart speaker. Chapter 6 turns this into two numbers — equal error rate and minimum detection cost — and shows, with hand arithmetic, that they can disagree about where to stand by a factor of six.

One vector, four consumers

Here is the map of the whole field, drawn once. Every product in this lesson is a different operation applied to the same vector.

Verification (1:1)
Score f(xtest) against one stored template. Compare to a threshold. Output: accept / reject. Metrics: EER, minDCF (Ch 6).
Identification (1:N)
Score against N stored templates, take the argmax — optionally with an "unknown" reject option. One matmul. Metric: top-1 accuracy.
Diarization (0:unknown)
Cut the recording into windows, embed each, cluster with unknown k, stitch the labels back onto the timeline. Metric: DER (Ch 7).
Conditioning
Hand the vector to a TTS or voice-conversion decoder as a control input; it synthesises in that voice. Metric: speaker similarity — scored by the same embedder (Ch 8).

That last row deserves a moment, because it is where the field gets uncomfortable. The vector that guards the bank account is the same vector that steers the cloner. A better embedding makes verification more accurate and cloning more convincing, simultaneously, and there is no way to have one without the other. Chapter 8 is entirely about that sentence.

The three papers, and what each one actually contributed

This lesson is built on three papers that, between them, define the modern recipe. It is worth separating what each changed, because they changed different layers of the same stack.

PaperYearThe single ideaWhich layer it moved
x-vector
Snyder et al.
2018A time-delay network with statistics pooling, trained to classify speakers, plus aggressive noise and reverberation augmentationThe encoder. Replaced the generative i-vector front end with a discriminative one
GE2E
Wan et al.
2018Train directly on the comparison: batches of N speakers × M utterances, scored against speaker centroids, with the own-utterance excludedThe objective. Removed the classification head entirely
ECAPA-TDNN
Desplanques et al.
2020Channel attention, multi-scale Res2Net blocks, multi-layer aggregation and attentive statistics pooling — on top of an angular-margin lossThe encoder again, four ways at once. The result is still the field's default in 2026

They are not a sequence of refutations. x-vectors established the shape (frame network → pooling → embedding); GE2E questioned whether the classification objective was necessary; ECAPA accepted a classification-shaped objective again but with an angular margin, and poured its effort into the encoder. The modern default — ECAPA-TDNN trained with additive angular margin softmax, scored with plain cosine — is a synthesis of all three lines of thinking.

What to hold on to through the next nine chapters. Every architecture here has the same three-part skeleton: (1) a frame-level network that produces one vector per 10 ms, (2) a pooling operation that collapses the whole time axis into one vector, (3) a training objective that shapes the geometry of that vector. Papers differ in which of the three they improve. Chapter 1 argues that part (2) — the boring-looking one — is where the speaker abstraction actually happens.

Where we are going

Chapters 1–2 — build the shape
The signal chain with every tensor shape named → why pooling is the abstraction → x-vectors and statistics pooling, hand-derived → PLDA in plain words
Chapters 3–5 — shape the geometry
GE2E's centroid trick, worked to a number on a 2×2 batch → ECAPA's four upgrades with an honest ablation reading → angular margins, imported from face recognition
Chapters 6–9 — deploy and defend
EER and minDCF derived and hand-computed → diarization and the overlap problem → what happens when the cloner learns to speak → the map of everything

What a "speaker" physically is

Before designing anything, it is worth knowing that speaker identity is a real physical quantity and not a statistical convenience. Speech production has two independent stages, and this source-filter split is the reason the whole enterprise works.

The source is the glottis — the vocal folds — opening and closing periodically to chop the airflow from the lungs into a buzz. Its rate is the fundamental frequency F₀: typically 85–180 Hz for adult male voices, 165–255 Hz for adult female voices.

The filter is everything above the glottis: the pharynx, mouth and nasal cavities, which resonate at frequencies determined by their geometry. Those resonances are the formants, and to a first approximation the vocal tract is a tube closed at one end (the glottis) and open at the other (the lips), whose resonances are the odd quarter-wavelengths:

Fn = (2n − 1) · c / (4L),   c ≈ 343 m/s

Work it for an adult with a 17.5 cm vocal tract:

F₁ = 343 / (4 × 0.175) = 343 / 0.700 = 490 Hz
F₂ = 3 × 490 = 1,470 Hz   F₃ = 5 × 490 = 2,450 Hz

Now a 14.0 cm tract — a shorter person, or a child:

F₁ = 343 / 0.560 = 612.5 Hz   F₂ = 1,837.5 Hz   F₃ = 3,062.5 Hz

The tract is 17.5/14.0 = 1.25 times shorter, and every formant moved up by exactly that factor. Uniform scaling — which is the single most important structural fact about voice identity, and it has a consequence you can see in Chapter 1's front end.

A uniform frequency scaling is a constant shift on a logarithmic axis. Multiplying every formant by 1.25 moves each one by 12·log2(1.25) = 3.86 semitones, the same distance for all of them. That is why the Mel scale — which is roughly logarithmic above 1 kHz — is not merely a nod to perception: it turns the dominant speaker-difference into a translation of the feature pattern rather than a warp of it. Translations are exactly what a convolutional network handles gracefully. The classical technique that exploits this explicitly is vocal tract length normalisation, which literally slides the filterbank.

So the quantities available to an embedding are: formant positions and their trajectories (anatomy), F₀ and its variability (source), the spectral tilt and breathiness of the glottal pulse shape, nasality (whether the velum is habitually open), and timing habits. Anatomy is stable over years. F₀ moves with emotion, health and effort. A good embedding leans on the first and treats the second with suspicion — which is why systems that lean hard on pitch fail whenever a user has a cold.

Text-dependent and text-independent

One more axis before we build anything, because the two settings have genuinely different failure modes and this lesson's papers sit on both sides of it.

Text-dependentText-independent
What the user saysA fixed phrase — "OK Google", or a prompted digit stringAnything
Utterance length0.5–2 s2 s to minutes
Content variationNone — so it need not be discardedTotal — the dominant nuisance
Accuracy at short durationsGood — the phrase is a controlled experimentPoor — a 1 s clip may contain four phonemes
Vulnerable to replaySeverely — the attacker knows the phraseLess so, if the prompt is random
Which paper hereGE2E's original deployment (keyword-triggered assistants)x-vector, ECAPA, all VoxCeleb numbers

The two settings even prefer different loss functions, and Chapter 3 will show exactly why: the contrast variant of the GE2E loss suits the text-dependent case and the softmax variant suits the text-independent one, for a reason visible in the arithmetic.

The deployment protocol, in code

To make the shape of the system concrete before we build any of it, here is the entire deployment surface of a verification system. Fifteen lines, and everything else in this lesson is about making encoder good.

# ── enrolment: run once, when the user signs up ──
def enrol(utterances):                # 3–5 clips, ideally different sessions
    embs = [normalize(encoder(u)) for u in utterances]   # each (192,)
    template = normalize(np.mean(embs, axis=0))     # (192,) — the centroid
    return template                                  # 768 bytes in the database

# ── verification: run on every login attempt ──
def verify(template, test_audio, theta):
    e = normalize(encoder(test_audio))            # (192,)
    score = float(template @ e)                    # cosine, since both are unit
    return score >= theta, score

# ── identification over N enrolled users: one matmul ──
def identify(gallery, test_audio):             # gallery: (N, 192)
    e = normalize(encoder(test_audio))
    scores = gallery @ e                          # (N,) — 40M rows is ~15 GFLOPs
    return int(scores.argmax()), float(scores.max())

Three things to notice. Enrolment averages several clips, which is the same centroid idea Chapter 3 puts inside the training loss — the training-time comparison and the deployment-time comparison should have the same shape, and here they do. The database stores a vector, not audio, which is both a privacy improvement and, as Chapter 8 argues, a new liability. And theta is passed in from outside, because choosing it is a business decision that Chapter 6 makes explicit.

Why is a 40-million-way softmax classifier the wrong design for a bank's voice verification system — the deepest reason, not just the memory cost?

Chapter 1: From Frames to One Vector

A microphone gives you a list of numbers: air pressure, sampled sixteen thousand times a second. A verification system needs 192 numbers that mean "Ava". This chapter walks every step between those two, naming every tensor shape, and then stops on the one step that most treatments skip past — because that step is where the speaker actually appears.

Stage 1: the waveform

Sound is a pressure wave. A microphone converts pressure to voltage, an ADC samples that voltage at a fixed rate, and you get an array of floats. For speech, 16 kHz is the near-universal choice.

Why 16,000? The Nyquist–Shannon theorem says a sample rate of R can represent frequencies up to R/2. At 16 kHz that is 8 kHz, and human speech puts almost all of its intelligibility below 8 kHz. (Telephony historically used 8 kHz sampling — a 4 kHz ceiling — which is why "s" and "f" are so easy to confuse on a bad phone line: the energy that distinguishes them lives at 4–8 kHz and the phone threw it away.)

So four seconds of speech is:

x ∈ R64,000   (4 s × 16,000 samples/s)

Could we feed those 64,000 raw numbers to a network? People have — SincNet learns band-pass filters directly on the waveform, and modern self-supervised models like WavLM start from raw audio. But every system in this lesson starts from a spectrogram instead, and the reason is not laziness.

Why not raw waveform. Two recordings of the same utterance, identical to the ear, can be sample-for-sample almost uncorrelated — shift one by 3 samples (0.19 ms, utterly inaudible) and every sample value changes. Phase is perceptually near-irrelevant and numerically enormous. A magnitude spectrogram throws phase away, which is exactly the right first act of destruction: it makes representations of perceptually-identical signals numerically identical.

Stage 2: framing

Speech is not stationary over four seconds — but it is roughly stationary over 25 milliseconds, because the vocal tract cannot reshape itself faster than that. So we cut the signal into short overlapping windows.

The standard settings, used by all three papers: window 25 ms, hop 10 ms. In samples at 16 kHz:

window = 0.025 × 16,000 = 400 samples  ·  hop = 0.010 × 16,000 = 160 samples

Overlap is 400 − 160 = 240 samples, or 60%. The number of frames from 64,000 samples:

T = 1 + ⌊(64,000 − 400) / 160⌋ = 1 + ⌊63,600 / 160⌋ = 1 + 397 = 398

Sanity check that number a second way: with a 10 ms hop you get 100 frames per second, so four seconds is about 400 frames. It is 398 and not 400 because the first window needs 400 samples before it can start and the last one needs 400 samples before it can end — the two-frame shortfall is the edge effect.

Each window is multiplied by a Hamming or Hann taper before the transform. Without the taper, chopping the signal at the window edge creates a discontinuity, and a discontinuity in the time domain is broadband energy in the frequency domain — the artefact is called spectral leakage, and it smears every frame with a false noise floor.

Stage 3: the spectrum, and then the Mel spectrum

Each 400-sample window goes through an FFT (zero-padded to 512 points, the next power of two). A real 512-point FFT gives 512/2 + 1 = 257 independent magnitude bins, each of width 16,000/512 = 31.25 Hz, evenly spaced from 0 to 8,000 Hz.

Evenly spaced is wrong for hearing. The ear's frequency resolution is roughly logarithmic: the difference between 100 Hz and 200 Hz is enormous perceptually, while 7,000 Hz and 7,100 Hz are indistinguishable. The Mel scale encodes that:

m(f) = 2595 · log10(1 + f/700)

Work an example. For a bank of 80 Mel filters spanning 20 Hz to 7,600 Hz:

m(20) = 2595 · log10(1 + 0.02857) = 2595 × 0.012234 = 31.75 mel
m(7600) = 2595 · log10(1 + 10.857) = 2595 × 1.074 = 2787.0 mel

The span is 2787.0 − 31.75 = 2755.2 mel. With 80 filters we need 82 boundary points, hence 81 equal gaps of 2755.2/81 = 34.02 mel each. Convert the first two gaps back to Hz using the inverse, f(m) = 700(10m/2595 − 1):

FilterCentre (mel)Centre (Hz)Approx. width (Hz)
1 (lowest)65.841.5≈ 22
20712.1605≈ 60
401392.51,510≈ 120
80 (highest)2753.07,405≈ 400

The highest filter is roughly 18 times wider than the lowest. That single fact is the whole point of the Mel warp: it spends resolution where the ear spends it, and where the speaker-discriminating information about vocal-tract length and glottal source happens to live.

Finally, take the logarithm of each filter's energy. Loudness is perceived logarithmically, and — more usefully — a multiplicative channel effect (a microphone that boosts the highs by 6 dB) becomes an additive offset in the log domain, which a network can subtract. That is why the standard feature is log-Mel filterbank energies, or "fbanks".

X ∈ RF×T = R80 × 398   (80 Mel bins, 398 frames)

We went from 64,000 numbers to 80 × 398 = 31,840. A 2× reduction, and every surviving number is perceptually meaningful.

The signal chain, with live shapes

Every stage from pressure wave to embedding, with the tensor shape recomputed as you change the utterance duration, the hop and the Mel-bin count. Watch the only column that never moves: the last one.

Duration (s) 4
Hop (ms) 10
Mel bins 80

Drag the duration slider from 1 second to 20 and watch every intermediate shape grow twenty-fold while the final vector stays at 192. That invariance is not a convenience. It is the product requirement from Chapter 0, and exactly one operation in the whole chain delivers it.

Stage 4: the frame-level network

The fbank matrix goes into a network that produces one vector per frame. Architecturally this can be a stack of dilated 1-D convolutions (x-vector, ECAPA), a recurrent network (GE2E's LSTM), or a transformer (WavLM-based systems). What they share is the output shape:

H ∈ RC×T   —  C channels per frame, still T frames

For ECAPA-TDNN with C = 1536 after aggregation, that is 1536 × 398 = 611,328 numbers. We have gone up, not down. The frame network's job is not compression; it is to re-describe each 10 ms slice in terms that make the speaker separable — and to give each frame a wide enough view of its neighbours to notice things like formant trajectories, which no single 25 ms window can contain.

Realization detail worth internalising. A frame at index t in H does not summarise the 25 ms at time t. It summarises a receptive field of neighbouring frames — 15 frames (about 165 ms) in the x-vector, about 23 frames (roughly 245 ms) in ECAPA. That is the difference between seeing a slice of a vowel and seeing a whole syllable, and it is why "time-delay neural network" was the right name for this family in 1989 and still is.

Stage 5: pooling — where the speaker actually appears

Now the pivotal step. We have H of shape (C × T), where T depends on the utterance. We need a vector whose size does not. So we collapse the time axis:

h ∈ RC×T  →  pool over t  →  v ∈ RC'  (no T anywhere)

Stated that way it sounds like plumbing — a reshape to make the shapes line up. It is not. It is the most consequential line in the architecture, and here is why.

Pooling is permutation-invariant. Averaging over t gives the same answer whichever order the frames arrive in. So the pooled vector cannot represent sequence. It cannot know that "cat" came before "dog", or that a rising pitch contour turned into a falling one. Shuffle the frames of an utterance and the pooled vector is bit-identical.

For a speech recognition system that would be a catastrophe — word order is the entire signal. For a speaker recognition system it is precisely the point.

The pooling step is the speaker abstraction. Speaker identity is a property of the distribution of frames, not of their order. Ava's vocal tract shapes every frame she produces, in every order, in every sentence. The words are the ordering; the speaker is the distribution. Pooling is a projection that keeps distributional properties and destroys ordering — which is to say, it keeps who and destroys what. Everything before pooling is feature extraction. Everything after it is geometry. The disentangling itself happens right here, in an operation with no parameters.

That reframing pays off immediately. It tells you what to pool: statistics of the frame distribution. The mean is the first one. The x-vector paper's contribution — Chapter 2 — was noticing that the mean alone is not enough, and adding the standard deviation. Attentive pooling — Chapter 4 — is the observation that not all frames deserve an equal vote.

It also tells you the cost. Pooling is why a clip-level speaker embedding cannot tell you when someone spoke, which is why diarization in Chapter 7 has to slice the recording into short windows first and embed each one separately. And it is why an embedding cannot represent two simultaneous speakers as anything but a blend — the overlap problem, also Chapter 7.

Stage 6: the embedding layer, and the head we throw away

The pooled vector goes through one or two fully-connected layers, and the output of the first of them is the embedding. For ECAPA:

StageShape (4 s input)NumbersDepends on T?
Waveform(64,000,)64,000Yes
Log-Mel fbank(80, 398)31,840Yes
Frame network output(1536, 398)611,328Yes
Attentive stats pooling(3072,)3,072No
FC + batch norm → embedding(192,)192No
AAM-softmax head (training only)(5994,)5,994No

Read the "depends on T" column. It flips exactly once, at pooling. Everything above that line is a variable-length representation of a signal; everything below it is a fixed-size description of a person.

The last row is deleted at deployment. The AAM-softmax head has 5,994 × 192 = 1,150,848 parameters — about 7.8% of a C=1024 ECAPA's 14.7M — and every one of them is discarded once training ends. They existed only to apply pressure to the 192 numbers above them.

Inline concept check — answer before reading on. Why is the pooled vector 3,072 rather than 1,536?  …  Because statistics pooling emits two statistics per channel: the mean over time and the standard deviation over time, concatenated. 1536 means followed by 1536 standard deviations. Chapter 2 derives both by hand and shows the exact case where dropping the standard deviations makes two different speakers collide.

The shapes, once more, as code

The whole chain in twenty lines, so the shapes are unambiguous. This is PyTorch-flavoured pseudocode — every line corresponds to a row of the table above.

import torch, torchaudio

wav, sr = torchaudio.load("ava.wav")     # wav: (1, 64000), sr = 16000

fbank = torchaudio.compliance.kaldi.fbank(     # log-Mel filterbank energies
    wav, num_mel_bins=80,
    frame_length=25.0, frame_shift=10.0,
    sample_frequency=16000)                    # fbank: (398, 80)  [T, F]

x = fbank.transpose(0, 1).unsqueeze(0)         # x: (1, 80, 398)   [B, F, T]
x = x - x.mean(dim=2, keepdim=True)          # cepstral mean norm — kills the channel

h = frame_network(x)                           # h: (1, 1536, 398)  [B, C, T]

mu    = h.mean(dim=2)                        # mu:  (1, 1536)   — T is gone
sigma = h.std(dim=2)                         # sigma: (1, 1536)
pooled = torch.cat([mu, sigma], dim=1)        # pooled: (1, 3072)

emb = fc(pooled)                               # emb: (1, 192) ← the speaker embedding
emb = torch.nn.functional.normalize(emb, dim=1) # unit length: cosine == dot product

# training only — deleted at deployment:
logits = aam_softmax_head(emb, labels)         # logits: (1, 5994)

Two lines in there are quietly load-bearing. x - x.mean(dim=2) is cepstral mean normalisation: subtracting each Mel bin's average over the utterance removes any constant multiplicative colouring of the channel, because a constant gain in the linear domain is a constant offset in the log domain. It is three characters of code that does more for channel robustness than most architectural choices.

And normalize(emb) makes the embedding unit-length, so the dot product is the cosine. After that line, similarity is one instruction, and the geometry the training objective shaped is the geometry the deployment scorer reads. Chapter 5 shows how much depends on those two being the same.

What each of the three papers puts in each slot

Slotx-vector (2018)GE2E (2018)ECAPA-TDNN (2020)
Features24-dim fbank40-dim fbank80-dim fbank / MFCC
Frame network5 TDNN layers, 15-frame context3-layer LSTM, 768 cells, projection 2563 SE-Res2Blocks + multi-layer aggregation
PoolingMean + std (statistics pooling)Last frame only — the LSTM's final stateAttentive statistics pooling, per channel
Embedding512-dim256-dim, L2-normalised192-dim
ObjectiveSoftmax over training speakersGE2E loss on centroids — no headAAM-softmax, s=30, m=0.2
Scoring backendLDA + length-norm + PLDACosineCosine

Look at the pooling row. GE2E's LSTM takes the final hidden state, which is not permutation-invariant — the recurrence smuggles order back in. It works, because an LSTM run over four seconds of speech mostly ends up summarising rather than remembering. But it is the one design in this table that fights the Chapter 1 thesis rather than embracing it, and it is not what the field kept.

The uncertainty tradeoff, quantified

Chapter 1 chose a 25 ms window without justifying the number. It is the resolution of a genuine tradeoff, and the arithmetic explains why nobody has moved off it in thirty years.

A window of duration Δt gives a frequency resolution of roughly Δf ≈ 1/Δt. You cannot improve one without ruining the other — this is the time-frequency uncertainty principle, and it is a property of the Fourier transform, not of any implementation.

WindowDurationΔf ≈ 1/ΔtF₀ periods inside it at 120 HzVerdict for speaker recognition
128 samples8 ms125 Hz0.96Cannot resolve harmonics at all — less than one glottal cycle fits
256 samples16 ms62.5 Hz1.92Onsets razor sharp, pitch structure smeared
400 samples25 ms40 Hz3.0Three glottal periods — enough to see harmonic structure, short enough to be quasi-stationary
1024 samples64 ms15.6 Hz7.7Beautiful harmonics, but the vocal tract has moved during the window — formants smear across phonemes

The bolded row is the compromise everyone lands on: three glottal periods. Fewer and you cannot see the source; more and the filter has changed while you were looking. Twenty-five milliseconds is where those two constraints meet for adult human speech, which is why the number is a constant of the field rather than a hyperparameter anyone tunes.

Why the logarithm, with numbers

Chapter 1 said "take the log" and gave two reasons. Both deserve arithmetic, because the second one is doing real work in every deployed system.

Reason one: dynamic range. Speech energy across a single utterance spans roughly 60 dB from a stressed vowel to a quiet fricative. In linear power that is a ratio of 106. Feed that to a network with any normalisation scheme and the fricatives are numerically invisible — they round to zero against the vowels. In the log domain the same span is 60 units, comfortably handled.

Reason two: channels become additive. Suppose a microphone boosts everything above 2 kHz by 6 dB. In the linear domain, the observed spectrum is the true one multiplied pointwise by a channel response:

Y(f) = H(f) · X(f)  →   log Y(f) = log H(f) + log X(f)

A 6 dB boost is a linear factor of 106/20 = 1.995 ≈ 2. In natural log that is ln(1.995) = 0.691, added to every affected bin, identically, for the whole recording. So:

CMVN:  x̃f,t = xf,t − (1/T) ∑τ xf,τ

Subtracting each bin's own mean over time removes that 0.691 exactly, along with any other constant colouring of the channel. Two recordings of the same person on wildly different microphones become numerically similar after one subtraction. Cepstral mean normalisation is the highest-value three characters in the whole pipeline, and it works only because the log turned a multiplication into an addition.

What CMVN cannot fix, and why the augmentation of Chapter 2 is still needed. The derivation assumed H(f) is constant over the utterance. Reverberation is not: it is a convolution in time, so it smears energy across frames and its effect on the log spectrum depends on what was said just before. Additive noise is worse still — log(X + N) is not log X plus anything. So CMVN handles the linear, time-invariant part of the channel for free, and everything else has to be learned from augmented data.

Two more front-end details that are not decoration

Pre-emphasis. Before framing, most pipelines apply a one-tap high-pass filter, y[n] = x[n] − 0.97·x[n−1]. The glottal source has a spectral tilt of roughly −12 dB per octave and lip radiation adds +6 dB per octave, leaving a net −6 dB per octave slope in the speech spectrum. Pre-emphasis approximately flattens it, so the higher formants — F₃ and F₄, which carry a great deal of speaker-specific information — are not numerically dwarfed by F₁.

The floor. log(0) is negative infinity, and a filterbank energy really can be zero in digital silence. Every implementation adds a floor, either log(x + ε) with ε around 10−6, or a hard clamp. Get this wrong and a single silent frame produces a NaN that propagates through pooling and destroys the whole utterance's embedding — a failure that is invisible in a research benchmark of curated clips and immediate in production, where silence is everywhere.

What the frame network is actually for

One last idea before pooling, because it is easy to read "frame network" as "a stack of layers" and miss the point.

A single 25 ms window contains a snapshot of the vocal tract. But a great deal of speaker identity lives in how the tract moves — the speed and target of formant transitions, the sharpness of a stop release, the coordination between voicing onset and articulation. None of that is present in any single frame, and all of it is present in a 150 ms neighbourhood.

One frame (25 ms)
The spectral shape right now. Says which vowel-ish configuration the tract is in, and roughly how long the tract is.
↓ a 15-frame receptive field adds…
A syllable-scale window (165 ms)
Trajectories. How fast F₂ slides from a consonant into the following vowel, whether voicing starts before or after the release — motor habits that are far harder to fake than a static timbre.
↓ and pooling turns that into…
A distribution over trajectory-descriptors
Not "she made this transition" but "her transitions look like this, on average, with this much variability". Identity as a statistical fingerprint of motor behaviour.

Read that chain twice. It explains why every architecture in this lesson widens the frame-level receptive field — Chapter 4's ECAPA gets to 245 ms and gives every intermediate scale as well — and it explains what statistics pooling is summarising: not raw spectra, but the distribution of a speaker's articulatory habits.

Shuffling the frames of an utterance leaves a mean-pooled embedding bit-identical. Why is that a feature rather than a bug for speaker recognition?

Chapter 2: x-vectors and Statistics Pooling

In 2018 David Snyder and colleagues at Johns Hopkins published a paper with an unglamorous title — "X-Vectors: Robust DNN Embeddings for Speaker Recognition" — and ended a decade in which the state of the art had been a generative model. The architecture is small enough to hold in your head, and every choice in it is defensible from first principles. We will build it layer by layer, derive its pooling by hand on a five-frame toy, and then explain its scoring backend in words rather than matrix algebra.

What it replaced, in one paragraph

Before x-vectors, the field ran on i-vectors (Dehak et al., 2011). The idea: model the distribution of all speech frames with a giant Gaussian mixture — the "universal background model", typically 2,048 components — then, for a given utterance, note how the mixture's means have to shift to explain that particular recording. Stack all those shifts into one enormous supervector, and assume the shifts live near a low-dimensional linear subspace. The utterance's coordinates in that subspace are the i-vector, usually 400 or 600 dimensions.

It is a beautiful piece of engineering and it has one structural weakness: nothing in the training objective ever mentions speaker identity. The UBM is fit by maximum likelihood on unlabelled audio; the subspace is fit to explain variance. Speaker labels only enter afterwards, at the scoring stage. So the representation is optimised to describe speech, and then you hope description implies discrimination.

The x-vector thesis. Put the speaker labels into the representation learner, not just the scorer. Train a network to answer "which of these 5,000 people is talking" and take the hidden layer. The features stop being a description of the audio and become, by construction, whatever separates people.

The architecture, layer by layer

Here is Table 1 of the paper, expanded so every number is derivable. Input is 24-dimensional filterbank features, mean-normalised over a sliding window of up to 3 seconds.

LayerContextIn → outParamsWhat it sees
frame1{t−2 … t+2}120 → 51261,9525 frames = 65 ms
frame2{t−2, t, t+2}1536 → 512787,4569 frames = 105 ms
frame3{t−3, t, t+3}1536 → 512787,45615 frames = 165 ms
frame4{t}512 → 512262,65615 frames
frame5{t}512 → 1500769,50015 frames
stats poolingall T frames1500×T → 30000The whole utterance
segment63000 → 5121,536,512← the x-vector
segment7512 → 512262,656
softmax512 → N speakers512NDeleted at deployment

Two arithmetic points worth checking yourself. The input width of frame1 is 120 because it splices five consecutive 24-dimensional frames: 5 × 24 = 120. The input width of frame2 is 1536 because it splices three of frame1's 512-dimensional outputs: 3 × 512 = 1536.

And the receptive field. Frame1 spans 5 input frames. Frame2 reaches ±2 of frame1's outputs, each of which already spans 5, so its span is 5 + 2×2 = 9. Frame3 reaches ±3 of those, so 9 + 2×3 = 15. Frame4 and frame5 are pointwise, so the span stops at 15. At a 10 ms hop:

15 frames × 10 ms + 15 ms of window overhang = 165 ms

165 milliseconds is roughly the length of a syllable. That is a deliberate scale: long enough to contain a formant transition (the glide from a consonant into a vowel, which is heavily shaped by vocal-tract geometry), short enough not to be dominated by which word was said.

Why "time-delay" and not just "convolution". A TDNN layer is a 1-D convolution — the term predates the modern vocabulary (Waibel et al., 1989). The one thing the older framing gets right is the dilation: frame2 looks at {t−2, t, t+2}, not {t−1, t, t+1}. It skips. Skipping widens the receptive field geometrically instead of linearly while keeping the parameter count fixed at three taps. Chapter 4 shows ECAPA taking that trick considerably further.

Statistics pooling, derived by hand

Now the layer with zero parameters that does the most work. Take a toy: T = 5 frames of a C = 3 channel activation. (Real numbers are 1500 × 398; three and five are enough to see the mechanism.)

Framech 1ch 2ch 3
t = 1201
t = 2413
t = 3025
t = 4611
t = 5310

Means, channel by channel:

μ1 = (2+4+0+6+3)/5 = 15/5 = 3.000
μ2 = (0+1+2+1+1)/5 = 5/5 = 1.000
μ3 = (1+3+5+1+0)/5 = 10/5 = 2.000

Standard deviations. Take channel 1. Deviations from the mean of 3: (−1, +1, −3, +3, 0). Squares: (1, 1, 9, 9, 0), summing to 20. Divide by T = 5:

σ12 = 20/5 = 4.000  →  σ1 = 2.000

Channel 2: deviations (−1, 0, +1, 0, 0), squares (1, 0, 1, 0, 0) summing to 2, so σ22 = 0.400 and σ2 = 0.6325.

Channel 3: deviations (−1, +1, +3, −1, −2), squares (1, 1, 9, 1, 4) summing to 16, so σ32 = 3.200 and σ3 = 1.7889.

Concatenate means then standard deviations:

v = [ 3.000, 1.000, 2.000  |  2.000, 0.6325, 1.7889 ] ∈ R6

Fifteen numbers became six, and — the part that matters — five frames became six numbers, and fifty frames would also have become six numbers. In the real network, 1500 × T becomes 3000, for any T.

Two experiments on that toy that make the design obvious

Experiment 1: reverse time. Feed the frames in the order t = 5, 4, 3, 2, 1. Channel 1 becomes (3, 6, 0, 4, 2). Sum: still 15. Mean: still 3.000. Deviations: (0, +3, −3, +1, −1), squares (0, 9, 9, 1, 1) — sum still 20, so σ is still 2.000. Every number in v is unchanged.

That is Chapter 1's thesis with arithmetic attached. Reversing the utterance destroys every word in it and leaves the speaker vector untouched, because the pool reads the distribution and not the sequence.

Experiment 2: delete the standard deviations. Suppose we kept only the means. Consider two speakers whose channel-1 activations over five frames are:

Speaker A: (1, 1, 1, 1, 1)  →  μ = 1.000
Speaker B: (−1, 3, 1, −1, 3)  →  μ = 5/5 = 1.000

Identical means. Now the standard deviations. A has zero deviation, so σA = 0. B has deviations (−2, +2, 0, −2, +2), squares (4, 4, 0, 4, 4) summing to 16, so σB2 = 3.200 and σB = 1.7889.

Mean-only pooling collapses these two speakers to the same point. Mean+std separates them by 1.7889 in that coordinate. And this is not a contrived case. Channel 1 might be responding to high-frequency fricative energy. Speaker A produces it steadily; speaker B produces it in bursts. That difference — the variability of a feature over an utterance, not its average — is a real and highly speaker-specific property, covering things like breathiness, pitch range, and the sharpness of consonant attacks. The mean averages it away by definition. That is why the standard deviation is in there, and it is the single most-copied detail of the x-vector paper.
Statistics pooling visualiser

A frame-level activation map on the left, the pooled vector below it. Switch between two utterances from the same speaker and one from a different speaker, then hit "reverse time" and watch the pooled bars refuse to move. Turn off the standard deviations and watch two different speakers collide.

Utterance:

Two readings to take away. The same speaker's two sentences have visibly different activation maps — different words, different phonemes — and nearly identical pooled vectors. The different speaker's map may look superficially similar in places and pools somewhere else entirely. Pooling is doing the work that the words were hiding.

The augmentation, which is really the paper's contribution

Here is the part people forget: the x-vector paper's headline claim is not about the architecture. It is that data augmentation helps discriminative embeddings far more than it helps generative ones. The recipe:

AugmentationSourceHow it is appliedWhat invariance it teaches
BabbleMUSAN speechSum 3–7 speakers at 13–20 dB SNRIgnore background talkers — the hardest noise, because it looks like speech
MusicMUSAN musicOne track at 5–15 dB SNRIgnore harmonic non-speech structure
NoiseMUSAN noiseAdditive at 0–15 dB SNR, once per secondIgnore stationary and impulsive backgrounds
ReverberationSimulated room impulse responsesConvolve the clean signal with an RIRIgnore the room — the channel effect that is convolutional, not additive

The asymmetry the paper measures: augmentation applied to the PLDA backend helps both i-vectors and x-vectors a little; augmentation applied to the embedding training data helps x-vectors substantially and does not apply to i-vectors at all, because the i-vector extractor is not trained with labels and cannot exploit "these two clips are the same speaker in different rooms". A discriminative front end can be told what to ignore. A generative one cannot.

Concept + realization. Augmentation here is not a regulariser in the usual "prevent overfitting" sense. It is a way to hand-write invariances into the training signal. Adding a room impulse response produces a pair of clips that a human calls "the same person" and a spectrogram calls "very different", and the label forces the network to agree with the human. If you ever wonder why a research embedding falls apart on your call-centre audio, check whether its training set contained your channel — that is usually the whole story.

The backend: PLDA in plain words

x-vectors are not scored with a cosine. They are scored with PLDA — probabilistic linear discriminant analysis — and this is worth explaining properly, because the reason it exists tells you something about what the objective did and did not do.

The generative story PLDA tells is one sentence: every embedding is a speaker point plus a channel wobble.

x = μ + F·h + ε,   h ~ N(0, I),   ε ~ N(0, Σ)

In words: μ is the global average voice; h is a latent identity, drawn once per person; F says how identities spread out; and ε is everything that changes between recordings of the same person — room, mic, mood, cold. Two recordings of Ava share one h and get two independent draws of ε.

Scoring is then a likelihood ratio, and this is the part worth internalising:

score(x1, x2) = log  P(x1, x2 | one shared h) − log P(x1 | h1) P(x2 | h2)

Read it as a question a detective asks: which story explains this pair better — one person recorded twice, or two people recorded once each? That is not a distance. Two vectors can be far apart and still score highly, if the direction they differ in is a direction that Σ says varies a lot between sessions. And two vectors can be close and score badly if they are close in a direction that everybody's voice occupies.

This is the key intuition and it is worth a second reading. Cosine similarity treats every direction as equally meaningful. PLDA has learned which directions carry identity (F) and which carry nuisance (Σ), and discounts the nuisance directions before comparing. If x-vector training had already produced a space where identity directions and nuisance directions were separated, PLDA would be unnecessary. It was necessary. That is a measurement of what the plain-softmax objective failed to do — and Chapter 5 is about the objective that finally did it.

The full deployment pipeline, in order, because every step matters and skipping one costs real accuracy:

1. Centre
Subtract the mean x-vector of the evaluation domain. Removes a domain-wide offset that would otherwise inflate every similarity.
2. LDA to ~150 dims
Project onto directions that maximise between-speaker over within-speaker variance. Discards roughly 70% of the coordinates as nuisance.
3. Length-normalise
Scale each vector to unit length. Makes the heavy-tailed real distribution look more Gaussian, which is what PLDA assumes.
4. PLDA log-likelihood ratio
The detective's question above. Output is an unbounded real number, not a similarity in [−1, 1].

Every one of those four steps needs data from the target domain to estimate. That is PLDA's practical cost: it is a second training procedure, with a second data requirement, on top of the network. Move to a new domain and your embeddings still work but your backend does not.

What x-vectors got right, and the two things they left open

EstablishedLeft open
Frame network → pooling → embedding is the right skeleton. Every system since uses itThe frame network is shallow (5 layers) with a fixed 15-frame view. Chapter 4 widens and deepens it
Mean + standard deviation is the right minimum poolingEvery frame votes equally — including silence. Chapter 4 fixes this with attention
Augmentation is not optional— still true, still the highest-leverage knob
Discriminative beats generative for the front endThe objective (plain softmax) does not shape the metric, so a separate PLDA backend is needed. Chapter 5 removes it

Those two open items are exactly what the next three chapters close, from two different directions: GE2E attacks the objective by training on comparisons directly, and ECAPA attacks the encoder while adopting an angular-margin objective. Both roads end at cosine scoring with no backend.

How big the i-vector machinery actually was

It is worth pricing the thing x-vectors replaced, because the comparison explains why the field switched so fast once the discriminative version worked at all.

A typical i-vector front end used a universal background model with 2,048 Gaussian components over 60-dimensional features (20 MFCCs plus deltas and double-deltas). The supervector of all component means is:

2,048 × 60 = 122,880 dimensions

The total variability matrix T projects that down to a 400-dimensional i-vector, so T alone is:

122,880 × 400 = 49,152,000 parameters

Forty-nine million parameters — more than three times an ECAPA-TDNN at C = 1024 — estimated by expectation-maximisation, with no speaker label ever entering the objective. And extraction at test time was not a forward pass: it required accumulating Baum-Welch statistics against all 2,048 components and then solving a 400 × 400 linear system per utterance.

The switch was not only about accuracy. A neural front end replaced a per-utterance linear solve with a single forward pass, replaced an EM training loop with SGD, and — the decisive part — made the representation trainable on labelled data and augmentable. When a new method is simultaneously more accurate, faster at inference, easier to train and able to absorb data augmentation, adoption is not a close call.

The augmentation, priced in decibels

Chapter 2's table said "13–20 dB SNR" without unpacking it. Signal-to-noise ratio in decibels is:

SNRdB = 10 · log10( Psignal / Pnoise )

So 20 dB means the speech carries 100 times the power of the noise, 10 dB means 10 times, and 0 dB means they are equal. To mix at a target SNR you measure both powers and scale the noise:

scale = √( Psignal / (Pnoise · 10SNR/10) )

Work one: speech power 0.01, noise power 0.04, target SNR 13 dB. Then 101.3 = 19.95, so scale = √(0.01 / (0.04 × 19.95)) = √(0.01/0.798) = √0.01253 = 0.1119. Multiply the noise by 0.1119 and add.

Notice the ranges the paper chose. Babble is mixed at 13–20 dB — comparatively gentle — while general noise goes down to 0 dB, where speech and noise are equally loud. That asymmetry is deliberate: babble is competing speech, so mixing it too loudly produces clips whose "correct" speaker label is genuinely ambiguous, and training on a wrong label is worse than not training at all. Augmentation has to stay on the right side of the line where the label is still true.

The whole encoder, in code

The x-vector is small enough to write out completely. This is the architecture of the table above, with the shapes annotated.

import torch, torch.nn as nn

class TDNNLayer(nn.Module):
    """A dilated 1-D conv. context={-2,0,2} is kernel 3 with dilation 2."""
    def __init__(self, cin, cout, k, dilation):
        super().__init__()
        self.conv = nn.Conv1d(cin, cout, k, dilation=dilation)
        self.act, self.bn = nn.ReLU(), nn.BatchNorm1d(cout)
    def forward(self, x):                 # x: (B, cin, T)
        return self.bn(self.act(self.conv(x)))   # (B, cout, T')

class XVector(nn.Module):
    def __init__(self, feat_dim=24, n_spk=5994):
        super().__init__()
        self.frames = nn.Sequential(
            TDNNLayer(feat_dim, 512, k=5, dilation=1),   # context {-2..2}
            TDNNLayer(512, 512, k=3, dilation=2),        # context {-2,0,2}
            TDNNLayer(512, 512, k=3, dilation=3),        # context {-3,0,3}
            TDNNLayer(512, 512, k=1, dilation=1),
            TDNNLayer(512, 1500, k=1, dilation=1))
        self.seg6 = nn.Linear(3000, 512)      # ← the x-vector comes from here
        self.seg7 = nn.Linear(512, 512)
        self.head = nn.Linear(512, n_spk)      # deleted at deployment

    def forward(self, x, return_emb=False):    # x: (B, 24, T)
        h = self.frames(x)                       # (B, 1500, T-14)
        mu = h.mean(dim=2)                     # (B, 1500)
        sd = h.std(dim=2, unbiased=False)      # (B, 1500)
        stats = torch.cat([mu, sd], dim=1)      # (B, 3000)  ← T is gone
        emb = self.seg6(stats)                   # (B, 512)   ← the x-vector
        if return_emb: return emb
        return self.head(torch.relu(self.seg7(torch.relu(emb))))

Two details in there are the kind that cost a day when you get them wrong. unbiased=False divides by T rather than T−1, matching the derivation above and, more importantly, staying finite when a padded segment has T = 1. And the embedding is taken before the non-linearity of segment 6 — the paper's own choice, and it matters, because the ReLU that follows would clip every negative coordinate and destroy exactly the sign structure a cosine or PLDA score reads.

Why length normalisation is not cosmetic

The third step of the PLDA pipeline — scale every vector to unit length — looks like tidiness. It is a distributional repair, and the reason is worth stating because it recurs in Chapter 5.

PLDA assumes the within-speaker and between-speaker distributions are Gaussian. Real x-vector distributions are not: they are heavy-tailed, because a fraction of utterances are short, noisy, or contain a cough, and those land far from the centre. A heavy tail wrecks a Gaussian model's covariance estimate, and the covariance is the thing PLDA uses to decide which directions are nuisance.

Projecting onto the unit sphere bounds every vector's distance from the origin, which truncates the tail by construction. The empirical result — established well before x-vectors, in the i-vector era — is a large and free accuracy gain. And it hints at the eventual answer: if the natural home of these representations is a sphere, perhaps the training objective should live on the sphere too. That is Chapter 5.

Statistics pooling concatenates the mean and the standard deviation over time. What exactly does the standard deviation add that the mean cannot express?

Chapter 3: GE2E — Training on the Comparison

The x-vector is trained to answer a question nobody will ever ask it: which of these 5,000 people is this? At deployment it is asked a completely different question: are these two clips the same person? There is a leap of faith in the middle, and Chapter 2 measured the size of it — a whole second model, PLDA, exists to repair the mismatch.

In late 2017 Li Wan, Quan Wang, Alan Papir and Ignacio Lopez Moreno at Google published "Generalized End-to-End Loss for Speaker Verification" with a simple proposal: stop leaping. Train on the comparison itself.

The batch is the idea

Almost everything distinctive about GE2E is in how a batch is constructed. Instead of sampling utterances independently, you sample a grid:

N speakers × M utterances each = N·M utterances per batch
(the paper uses N = 64, M = 10 → 640 utterances)

Every utterance goes through the encoder — a 3-layer LSTM with 768 cells and a 256-unit projection, taking the final frame's output — and is L2-normalised. Call the result eji: the embedding of speaker j's utterance i.

Now compute, for each speaker, a centroid:

ck = (1/M) ∑m=1..M ekm

and build a similarity matrix of every utterance against every centroid:

Sji,k = w · cos(eji, ck) + b,   w > 0

with w and b learned, initialised to (10, −5). The matrix has N·M rows and N columns — 640 × 64 in the paper's configuration. Each row asks: which of these 64 people does this clip belong with?

Why a centroid rather than another utterance. The obvious alternative — and what the older TE2E and triplet-loss methods did — is to compare an utterance against one other utterance. But a single utterance is a noisy estimate of a speaker: it has particular words, particular background noise, a particular mood. A centroid of M utterances averages that away. Comparing against a centroid means comparing against the speaker rather than against one recording of the speaker, which is exactly the enrolment procedure the system will use at deployment. The training-time comparison and the test-time comparison finally have the same shape.

The exclusion trick, which is not optional

There is a hole in the definition above and it is fatal. When we score utterance eji against its own speaker's centroid cj, that utterance is one of the M terms inside the centroid. So the model can win by cheating: push every embedding to a fixed point and the centroid follows it there, similarity 1.0, loss 0, and the embedding carries no information at all.

GE2E's fix is to exclude the utterance being scored:

cj(−i) = (1 / (M − 1)) ∑m ≠ i ejm

and use that whenever k = j, while using the full centroid for k ≠ j. Now the target is genuinely held out: the utterance must be close to the other recordings of the same person, none of which it contributed to. The paper is explicit that this "makes training stable and helps avoid trivial solutions".

You have seen this trick before. It is leave-one-out cross-validation, applied inside a loss function, at every step. Any time a target is computed from a set that includes the prediction, there is a degenerate solution waiting. BYOL's stop-gradient, SimSiam's asymmetry and GE2E's exclusion are three answers to the same hazard.

The two losses

Given a row of the similarity matrix, GE2E offers two ways to turn it into a scalar.

Softmax variant — treat the row as logits over N speakers and apply cross-entropy with the true speaker as target:

L(eji) = −Sji,j + log ∑k=1..N exp(Sji,k)

Contrast variant — push the true similarity toward 1 and the single worst impostor toward 0, through a sigmoid σ:

L(eji) = 1 − σ(Sji,j) + maxk ≠ j σ(Sji,k)

The total loss is the sum over all N·M rows. These behave differently and the difference matters, so we will now compute both, twice, on a batch small enough to do in your head.

A 2 × 2 batch, worked to a number

Take N = 2 speakers, M = 2 utterances each, and pretend embeddings are two-dimensional unit vectors so we can name them by angle. (In two dimensions, cos(e, c) is just the cosine of the angle between them, which makes every step checkable by hand.)

Case A — a well-trained batch. Speaker A's two utterances sit at 0° and 50°. Speaker B's sit at 130° and 170°.

Because M = 2, the exclusion rule is simple: the own-speaker centroid for utterance i is just the other utterance (one term, so no averaging needed). The other speaker's full centroid is the mean of its two unit vectors, which points at the bisecting angle — 150° for B, 25° for A.

Row 1, utterance A₁ at 0°:

cos(A₁, cA(−1)) = cos(0° − 50°) = cos 50° = 0.6428
cos(A₁, cB) = cos(0° − 150°) = cos 150° = −0.8660

Apply the scale and bias with the paper's initial values w = 10, b = −5:

SA₁,A = 10(0.6428) − 5 = 6.428 − 5 = 1.428
SA₁,B = 10(−0.8660) − 5 = −8.660 − 5 = −13.660

Softmax loss for this row. Exponentiate: e1.428 = 4.1706, and e−13.660 = 0.00000117. Sum = 4.17060.

L = −1.428 + log(4.17060) = −1.428 + 1.42800 = 0.0000003

Essentially zero. Do the same for the other three rows — A₂ at 50° scores cos 50° = 0.6428 against its partner at 0° and cos 100° = −0.1736 against cB; B₁ at 130° scores cos 40° = 0.7660 against its partner and cos 105° = −0.2588 against cA; B₂ at 170° scores cos 40° = 0.7660 and cos 145° = −0.8192:

RowS (own)S (other)Softmax loss
A₁1.428−13.6600.0000003
A₂1.428−6.7360.000285
B₁2.660−7.5880.0000354
B₂2.660−13.1920.0000001
Total LG0.00032

Check row A₂ yourself: e1.428 = 4.1706, e−6.736 = 0.0011884, sum 4.17179, log = 1.428285, minus 1.428 = 0.000285. Good.

Case B — a batch the model gets wrong. Now suppose the encoder is undertrained and speaker B's utterances land at 20° and 40°, right between speaker A's two at 0° and 50°. B's centroid is at 30°.

cos(A₁, cA(−1)) = cos 50° = 0.6428  →  S = 1.428
cos(A₁, cB) = cos 30° = 0.8660  →  S = 8.660 − 5 = 3.660

The impostor now scores higher than the true speaker. Softmax loss:

e1.428 = 4.1706,  e3.660 = 38.859,  sum = 43.030
L = −1.428 + log(43.030) = −1.428 + 3.7619 = 2.334

Two point three three four, against 0.0000003 for the good row — seven million times larger. Cross-entropy is not gentle about being confidently wrong.

Now the contrast variant on the same two rows, with σ(z) = 1/(1 + e−z):

σ(1.428) = 1/(1 + 0.2398) = 0.8065
σ(3.660) = 1/(1 + 0.02573) = 0.9749
σ(−13.660) = 0.00000117
RowSoftmax lossContrast loss
Case A (good)0.00000031 − 0.8065 + 0.0000012 = 0.1935
Case B (bad)2.3341 − 0.8065 + 0.9749 = 1.1684
Read the first row of that table carefully — it is the whole distinction. On the well-separated batch the softmax loss has effectively vanished (3×10−7) but the contrast loss is still 0.1935 and still pushing. Softmax is relative: it only cares that the true similarity beats the impostors, and once it does by a wide margin there is nothing left to want. Contrast is absolute: it wants σ(Sown) to reach 1, which means it keeps compacting genuine pairs long after they are separable. The paper reports softmax works better for text-independent verification, contrast better for text-dependent — and the reason is visible right here.
GE2E similarity-matrix explorer

Left: embeddings on the unit circle, grouped by speaker, with centroids marked. Right: the similarity matrix S = w·cos + b, one row per utterance, one column per speaker centroid, with the per-row losses. Tighten the within-speaker spread and watch the diagonal light up. Then turn off self-exclusion and watch the model discover the cheat.

Within-speaker spread 18°
Scale w 10
Bias b -5.0

What w and b are actually for

The scale w and bias b look like cosmetic add-ons. They are not, and the sliders above make the argument. A cosine lives in [−1, 1]. Softmax over a row of cosines can never be confident: the best possible logit gap is 2, and with N = 64 speakers the probability of the correct class is capped at

e1 / (e1 + 63·e−1) = 2.71828 / (2.71828 + 23.177) = 0.1050

— a loss of −log(0.1050) = 2.254 that no encoder can ever reduce. The model would be permanently pinned in a regime where gradients look like it is failing even when the embedding is perfect. Multiplying by w = 10 gives logits in [−10, 10] and restores the dynamic range. The bias b shifts the operating point so that the sigmoid in the contrast variant is not saturated at initialisation.

The paper constrains w > 0 during training. That constraint is load-bearing: if w were allowed to go negative, the model could satisfy the loss by making genuine pairs dissimilar and flipping the sign, which is a valid optimum of the loss and a useless embedding.

MultiReader — the practical trick worth stealing

GE2E ships a second idea that gets less attention and is arguably more useful day to day. Google had a large dataset of "OK Google" utterances and a small one for a new keyword. Fine-tuning on the small set overfits; pooling the sets lets the large one dominate. MultiReader instead computes the loss on both and combines:

L = Lsmall + α · Llarge

The paper frames α as a regularisation weight: the large dataset acts as a prior that keeps the model honest while the small one steers it. It generalises to K datasets with K weights, and it is the standard answer whenever you have one big generic corpus and one small in-domain one — which, in speaker verification, is always.

What GE2E claims, and what it actually established

The reported headline: more than 10% relative reduction in EER against the previous TE2E system, with training time cut by about 60%. Both come from the same source — a batch of N×M produces N·M×N comparisons rather than N·M/2 pairs, so each gradient step is informed by far more comparisons at essentially the same forward cost.

What GE2E fixedWhat it did not
Train-test mismatch: the training comparison is now the deployment comparison, against a centroid, as at enrolmentThe encoder is a modest LSTM; the field moved on to much stronger frame networks (Chapter 4)
No classification head, so no fixed speaker list is baked into the modelBatch construction is now a constraint: you need M utterances of each of N speakers per step, which is a real data-loader burden
Negatives come free from batch structure — no mining, unlike triplet lossThe hardest negatives are the ones not in your batch. Large N helps and costs memory
Cosine scoring works directly — no PLDA backendAngular margins (Chapter 5) turned out to give the same benefit inside a classification-shaped loss, which is easier to batch

The honest historical verdict: the field did not keep GE2E's exact loss for text-independent verification — AAM-softmax won that contest. But it kept GE2E's encoder, in an unexpected place. Google's SV2TTS paper took a GE2E-trained speaker encoder and used it to condition a Tacotron 2 synthesiser on a target voice, which is the direct ancestor of every zero-shot voice cloning system in use today. The loss designed to verify voices became the machinery that copies them. Chapter 8 returns to that irony with the seriousness it deserves.

Counting the comparisons, which is where the speedup comes from

GE2E's claim of a 60% training-time reduction sounds like an engineering detail. It is a direct consequence of the batch shape, and the arithmetic makes it obvious.

Triplet loss (the FaceNet approach, and TE2E's ancestor) consumes a batch of B utterances as B/3 triplets. Each triplet yields exactly one comparison-with-a-margin. With B = 640:

640 / 3 ≈ 213 triplets → 213 comparisons per gradient step

GE2E consumes the same 640 forward passes as a 64 × 10 grid and scores every utterance against every centroid:

N·M × N = 640 × 64 = 40,960 comparisons per gradient step

A factor of 192, for the same encoder cost. The forward passes dominate the compute; the similarity matrix is one (640 × 256)(256 × 64) product, about 21 MFLOPs, which is nothing beside 640 LSTM passes. You get two orders of magnitude more supervision essentially free.

The general lesson, which shows up everywhere in metric learning. Comparisons are cheap; encoder forward passes are expensive. So a good contrastive objective is one that extracts the maximum number of informative comparisons from a fixed number of forward passes. This is the same accounting that makes CLIP's in-batch cross-entropy better than a pairwise loss, and the same reason contrastive methods keep pushing batch sizes up. GE2E adds one twist the others lack: because it compares to centroids rather than to individual utterances, it also cuts the noise in each comparison by averaging M recordings.

What the gradient actually does

Take the softmax variant and differentiate. For a row with similarities S = (S₁, …, SN) and true speaker j, write p = softmax(S). Then, exactly as in ordinary cross-entropy:

∂L / ∂Sk = pk − [k = j]

So the gradient with respect to the true speaker's similarity is pj − 1, which is negative — push Sj up. For every other speaker it is pk, which is positive — push Sk down, and in proportion to how much probability mass that impostor currently holds.

Work it on Case B from above, where S = (1.428, 3.660). The softmax is:

e1.428 = 4.1706,  e3.660 = 38.859,  sum 43.030
p = (0.0969, 0.9031)
∂L/∂Sown = 0.0969 − 1 = −0.9031   ∂L/∂Simpostor = +0.9031

Compare Case A, where S = (1.428, −13.660) and p = (0.99999972, 0.00000028):

∂L/∂Sown = −0.00000028   ∂L/∂Simpostor = +0.00000028
Automatic hard-negative mining, for free. The gradient on each impostor is exactly its softmax probability. Impostors the model already considers implausible receive essentially no gradient; the one it is currently confusing receives almost all of it. Triplet loss needs an explicit mining stage to achieve this — semi-hard negative selection, an entire sub-literature of heuristics — and the softmax over a batch gets it as an identity of the derivative. This is the single strongest argument for softmax-shaped contrastive losses, and it is why the field ended up back at a classification-shaped objective in Chapter 5.

Then, since Sk = w·cos(e, ck) + b, the chain rule pushes that scalar through the cosine into the embedding. The gradient on e has a component along cj (pulling the utterance toward its own speaker's centroid) minus a weighted sum of components along the impostor centroids — and because the cosine's derivative is orthogonal to e itself, the update is purely rotational. Length never changes. The whole optimisation lives on the sphere, which is exactly the geometry Chapter 5 formalises.

The loss in code

import torch, torch.nn.functional as F

def ge2e_loss(emb, w, b):
    """emb: (N, M, D) — L2-normalised. w: positive scalar. b: scalar."""
    N, M, D = emb.shape
    centroids = emb.mean(dim=1)                       # (N, D)  full centroids

    # leave-one-out centroid for the own-speaker column:
    # (sum − self) / (M − 1), computed without a loop
    excl = (emb.sum(dim=1, keepdim=True) - emb) / (M - 1)   # (N, M, D)

    e = F.normalize(emb, dim=2)
    c = F.normalize(centroids, dim=1)                   # (N, D)
    S = torch.einsum('nmd,kd->nmk', e, c)               # (N, M, N) all cosines

    # overwrite the diagonal (own-speaker) entries with the excluded version
    own = F.cosine_similarity(e, F.normalize(excl, dim=2), dim=2)  # (N, M)
    idx = torch.arange(N, device=emb.device)
    S[idx, :, idx] = own

    S = w.clamp(min=1e-6) * S + b                        # w > 0 is enforced here
    labels = idx.unsqueeze(1).expand(N, M).reshape(-1)
    return F.cross_entropy(S.reshape(N * M, N), labels)

The one line worth staring at is excl. Subtracting each embedding from its own speaker's sum and dividing by M−1 computes all N·M leave-one-out centroids in a single vectorised expression — no loop, no masking, no gather. And w.clamp(min=1e-6) is the positivity constraint from the paper, enforced in the one place it cannot be forgotten.

Why the two variants split along the text-dependent line

Return to the table where Case A scored 0.0000003 under softmax and 0.1935 under contrast. That gap is the entire explanation, and it maps cleanly onto Chapter 0's text-dependent axis.

Softmax variantContrast variant
What it wantsThe true similarity to beat the impostorsThe true similarity to reach 1, and the worst impostor to reach 0
Behaviour once separatedStops pushing — gradient decays to zeroKeeps compacting
Sees how many impostorsAll N − 1, weighted by probabilityExactly one — the hardest
SuitsText-independent, where many impostors are plausible and the ranking is what mattersText-dependent, where the phrase is fixed, the space is tight, and you want absolute compactness
Failure modeUnder-trains once the batch is easy — needs large N to keep finding hard negativesOver-focuses on one negative, which may be an annotation error

The pattern generalises well past this paper. A relative objective produces a good ranking and a badly calibrated absolute scale; an absolute objective produces compact clusters and can over-fit its single hardest example. Chapter 5's angular margin is, read in this light, a way of getting the softmax's automatic hard-negative weighting and an absolute compactness target, by moving the target rather than changing the loss shape.

Why does GE2E exclude the current utterance from its own speaker's centroid?

Chapter 4: ECAPA-TDNN — Four Upgrades at Once

In 2020 Brecht Desplanques, Jenthe Thienpondt and Kris Demuynck at Ghent published an architecture whose acronym is an inventory of its own contents: Emphasized Channel Attention, Propagation and Aggregation in TDNN. It kept the x-vector skeleton exactly — frame network, pooling, embedding — and improved four things inside it. Six years later it is still the default speaker encoder in SpeechBrain, WeSpeaker, NeMo and most production systems.

This chapter takes the four upgrades one at a time, says what each one buys with actual arithmetic, and then reads the ablation honestly — including the part where the components turn out not to add up.

Upgrade 1: channel attention (squeeze-and-excitation)

Start with the problem. A TDNN layer's frame output at time t is built from a 15-frame window. It has no idea what is happening two seconds away. But whether a given channel's activation is informative often depends on global context: if the whole clip is drowning in babble, the channels that respond to high-frequency detail should be trusted less; if the clip is clean and close-miked, they should be trusted more.

The squeeze-excitation block (Hu et al., 2018, imported from vision) gives a local convolution a global thermostat. Two steps.

Squeeze — average each channel over the entire utterance, producing one number per channel:

zc = (1/T) ∑t=1..T hc,t   z ∈ RC

Excite — push z through a small bottleneck MLP and a sigmoid to get a gain per channel, then multiply:

s = σ( W2 · f(W1 z + b1) + b2 ),   s ∈ (0,1)C
h′c,t = sc · hc,t

The bottleneck is small — ECAPA uses 128 units for C = 512 or 1024. Parameter cost for C = 1024: 1024×128 + 128×1024 = 262,144, about 1.8% of the model. Cheap.

What the squeeze actually buys, stated precisely. The mean over t is taken over every frame in the utterance. So the SE block's receptive field is infinite — it sees the whole recording — while the convolution it modulates still sees only 15 or 23 frames. That is a strange and powerful asymmetry: unlimited context for scaling, limited context for mixing. It costs almost nothing because a mean is not a mixing operation. The lesson generalises far beyond audio: if you need global information but cannot afford global attention, use global information to gate rather than to combine.

And notice the family resemblance. The SE squeeze is a mean over time. The pooling layer in Chapter 2 is a mean over time. ECAPA computes a global mean at three places in the frame network before computing it once more at pooling. The architecture is, in a real sense, mostly about when and how to average over the time axis.

Upgrade 2: Res2Net — many receptive fields inside one layer

An x-vector's frame network has one receptive field per layer: 5, then 9, then 15 frames. But speaker cues live at wildly different time scales at once. Glottal buzz is a property of a 10 ms slice. Formant transitions take 50–100 ms. Speaking rhythm and prosodic habits take half a second. A single fixed window is a compromise across all three.

Res2Net (Gao et al., 2019) gets several scales out of one layer by splitting the channels and chaining the splits. With C channels and scale s = 8, you cut the input into 8 groups of C/8 channels and process them like this:

y1 = x1  (passed through untouched)
yi = Ki( xi + yi−1 )  for i = 2 … 8

Each Ki is a dilated 3-tap convolution. Group 2 passes through one convolution. Group 3 passes through the sum of its own input and group 2's output, so it has effectively been through two convolutions. Group 8 has been through seven.

Work out the receptive fields. A 3-tap convolution with dilation d extends the receptive field by (3−1)·d = 2d frames. For the first ECAPA block, d = 2:

GroupConvolutions traversedReceptive field (frames)In time (10 ms hop)Speech structure at that scale
10110 msA single glottal period region — raw timbre
21550 msSteady portion of a vowel
32990 msA consonant-vowel transition
5417170 msA syllable
8729290 msA short word, or a prosodic gesture

The general formula is RFi = 1 + (i − 1)·2d, so for d = 2 group 8 gives 1 + 7×4 = 29 frames. One layer, receptive fields from 10 ms to 290 ms, simultaneously. That is the multi-scale claim made concrete.

Now the parameter cost, which is the surprise. Res2Net with C = 512 and s = 8 has 64 channels per group and 7 convolutions of 64 → 64 with 3 taps:

7 × 64 × 64 × 3 = 86,016 parameters

A plain 3-tap convolution over all 512 channels would cost:

512 × 512 × 3 = 786,432 parameters
Res2Net delivers five different receptive fields for 9.1× fewer parameters than one fixed receptive field. 786,432 ÷ 86,016 = 9.14. The saving comes from never mixing all 512 channels at once — each convolution only ever sees 64 channels in and 64 out. The 1×1 convolutions that bracket the block do the cross-channel mixing at 3× lower cost because they have one tap instead of three. This is the same accounting that makes depthwise-separable convolutions work in MobileNet, applied along a different axis.

ECAPA stacks three of these SE-Res2Blocks with dilations 2, 3 and 4. Total frame-network receptive field, counting the initial 5-tap convolution:

5 + 2(2) + 2(3) + 2(4) = 5 + 4 + 6 + 8 = 23 frames ≈ 245 ms

Against the x-vector's 15 frames (165 ms). About 1.5× wider at the top — and, crucially, with every intermediate scale available rather than only the widest.

Res2Net multi-scale + the SE thermostat

One output frame, and the input frames each Res2Net group can see. Change the dilation and the scale and watch the nested brackets grow. Toggle the SE block to see the one path whose context is the entire utterance.

Dilation d 2
Scale s (groups) 8

Upgrade 3: multi-layer feature aggregation

In an x-vector, only the last frame layer feeds the pooling. Everything the earlier layers computed reaches pooling only in whatever form the later layers chose to preserve. ECAPA instead concatenates the outputs of all three SE-Res2Blocks and runs a 1×1 convolution over the stack:

concat(h(1), h(2), h(3)) ∈ R3C × T  →  1×1 conv  →  R1536 × T

The paper's argument, which the vision literature had already made about feature pyramids: shallow layers hold fine spectral detail that deep layers have abstracted away, and speaker identity lives at both levels. The "propagation and aggregation" in the acronym refers to this plus the residual connections that let each block also add to a running sum.

Concretely, for C = 512, the concatenation is 1536 channels wide and the 1×1 conv maps 1536 → 1536, costing 1536×1536 = 2.36M parameters — the single largest block in the C=512 model. It is not free. Whether it is worth its weight is exactly the kind of question the ablation should answer, and Chapter 4's honest reading is coming in three sections.

Upgrade 4: attentive statistics pooling, per channel

Return to statistics pooling from Chapter 2. Every frame votes equally — including the 40% of a typical recording that is silence, breath, or a door closing. Attentive statistics pooling (Okabe et al., 2018) learns the votes.

A small network scores each frame, softmax over time turns the scores into weights, and the statistics become weighted:

et = vT f(W ht + b) + k   αt = exp(et) / ∑τ exp(eτ)
μ̃ = ∑t αt ht   σ̃ = √( ∑t αt ht ⊙ ht − μ̃ ⊙ μ̃ )

Work it on the Chapter 2 toy. Channel 1 was (2, 4, 0, 6, 3), which pooled to μ = 3.000 and σ = 2.000 with equal weights of 0.2 each. Now suppose the attention network judges frames 1 and 3 to be near-silence and assigns α = (0.05, 0.25, 0.05, 0.50, 0.15) — which sums to 1.00, as it must.

μ̃ = 0.05(2) + 0.25(4) + 0.05(0) + 0.50(6) + 0.15(3)
   = 0.10 + 1.00 + 0.00 + 3.00 + 0.45 = 4.550

For the standard deviation, use the weighted second moment minus the square of the weighted mean:

∑αtht2 = 0.05(4) + 0.25(16) + 0.05(0) + 0.50(36) + 0.15(9)
   = 0.20 + 4.00 + 0.00 + 18.00 + 1.35 = 23.550
σ̃2 = 23.550 − 4.5502 = 23.550 − 20.7025 = 2.8475
σ̃ = 1.6875

Compare: unweighted (3.000, 2.000), attentive (4.550, 1.6875). Down-weighting the two frames the network judged uninformative moved the mean up by more than half a standard deviation and tightened the dispersion. If frames 1 and 3 really were silence, the second pair is a much better description of the speaker.

ECAPA extends this in two ways beyond the 2018 formulation. First, the attention is channel-dependent — a separate α per channel per frame, not one shared weight per frame — because a frame can be informative about a speaker's pitch while being useless about their fricatives. Second, the attention network's input is the frame concatenated with the global context (the mean and standard deviation over the whole utterance, broadcast back to every frame), so a frame is judged relative to the recording it sits in rather than in isolation. That last detail is the same idea as the SE squeeze, applied at pooling.

The full architecture, with shapes

StageOutput shape (C = 1024, T = 398)Params
Input MFCC / fbank(80, 398)
Conv1D k=5, d=1 + ReLU + BN(1024, 398)0.41M
SE-Res2Block, d = 2(1024, 398)≈ 1.4M
SE-Res2Block, d = 3(1024, 398)≈ 1.4M
SE-Res2Block, d = 4(1024, 398)≈ 1.4M
MFA: concat 3 blocks → 1×1 conv(1536, 398)4.72M
Attentive stats pooling(3072,)0.4M
FC + BN → embedding(192,)0.59M
AAM-softmax head (training only)(5994,)1.15M
Total, C = 1024≈ 14.7M
Total, C = 512≈ 6.2M

Fourteen million parameters. For context, that is roughly one seven-hundredth of a modern 10B language model, and it runs comfortably in real time on a phone CPU. Speaker embedding is one of the few remaining corners of deep learning where the useful model is small.

The results, and the honest ablation reading

Trained on VoxCeleb2's development set — 5,994 speakers — and evaluated on the three VoxCeleb1 trial lists:

SystemVox1-O EERVox1-E EERVox1-H EERVox1-O minDCF
x-vector style baseline (E-TDNN)≈ 1.49%≈ 1.61%≈ 2.69%≈ 0.16
ECAPA-TDNN, C = 5121.01%1.24%2.32%0.127
ECAPA-TDNN, C = 10240.87%1.12%2.12%0.107

Vox1-O is the original 40-speaker test list (about 37.6k trials). Vox1-E is the extended list over all 1,251 VoxCeleb1 speakers (about 580k trials). Vox1-H is the hard list, where every impostor trial pairs speakers of the same nationality and gender — which is why its error rate is roughly double. Always ask which list a number came from; the same system can look twice as good by quoting Vox1-O.

Now the ablation, and here is where a lesson should be careful. The paper removes one component at a time from the C = 512 model. The full model scores 1.01% EER. Every single-component removal lands in a band roughly between 1.1% and 1.2%.

How to read an ablation like that honestly. Each component is worth on the order of a tenth of an EER point on its own — call it a 10–20% relative degradation. But 1.01 is not 1.20 minus four separate tenths: the contributions are not additive and the paper does not claim they are. Channel attention and attentive pooling are both mechanisms for "use global context to decide what matters", so removing one leaves the other partly covering for it. The correct summary is: this is an architecture of four mutually-reinforcing ideas whose combined effect (roughly 1.49% → 1.01%, a 32% relative reduction) exceeds any one of them, and whose individual credit assignment is genuinely ambiguous. Anyone quoting a precise per-component delta as an independent quantity is over-reading the experiment.

The second honest note: ECAPA was trained with AAM-softmax and scored with a plain cosine, while the x-vector baseline used plain softmax and a PLDA backend. The comparison therefore bundles an architecture change with an objective change. The objective is the subject of Chapter 5, and it is doing a substantial share of the work in that table.

What ECAPA did not solve

LimitationWhich design decision causes itWhat the field did next
Short utterances (under 2 s) degrade sharplyPooling estimates statistics from few frames — the estimator's variance grows as 1/TLength-aware training, uncertainty-propagating pooling, xi-vectors
Overlapping speakers produce a blend, not two vectorsPooling is a single average over all frames, and averaging two identities gives a thirdTarget-speaker VAD, end-to-end neural diarization (Chapter 7)
Domain shift (new language, new codec) still hurtsInvariances come from augmentation, and you cannot augment what you did not anticipateLarge self-supervised front ends (WavLM), domain adversarial training
The embedding is not interpretable or editable192 entangled dimensions with no imposed structureMostly unsolved — and it becomes a real problem in Chapter 8

The SE-Res2Block, in code

All four upgrades except the aggregation live inside one repeated block. Writing it out removes any remaining ambiguity about the ordering.

class SERes2Block(nn.Module):
    def __init__(self, C, k=3, dilation=2, scale=8, se_bottleneck=128):
        super().__init__()
        self.scale, self.width = scale, C // scale       # 512 // 8 = 64
        self.pre  = nn.Conv1d(C, C, 1)                    # 1×1 — mixes channels
        self.convs = nn.ModuleList([                     # scale − 1 = 7 convs
            nn.Conv1d(self.width, self.width, k,
                      dilation=dilation, padding=dilation * (k - 1) // 2)
            for _ in range(scale - 1)])
        self.post = nn.Conv1d(C, C, 1)
        self.se   = nn.Sequential(                       # squeeze-excitation
            nn.Linear(C, se_bottleneck), nn.ReLU(),
            nn.Linear(se_bottleneck, C), nn.Sigmoid())

    def forward(self, x):                              # x: (B, C, T)
        res = x
        h = torch.relu(self.pre(x))
        parts = torch.split(h, self.width, dim=1)        # 8 × (B, 64, T)
        out, prev = [parts[0]], None                    # group 1 passes through
        for i, conv in enumerate(self.convs):
            inp = parts[i + 1] if prev is None else parts[i + 1] + prev
            prev = torch.relu(conv(inp))                 # RF grows with i
            out.append(prev)
        h = torch.relu(self.post(torch.cat(out, dim=1)))

        z = h.mean(dim=2)                              # SQUEEZE: (B, C) over ALL T
        h = h * self.se(z).unsqueeze(2)                 # EXCITE: per-channel gain
        return h + res                                  # residual — the "propagation"

Three lines carry the chapter. torch.split plus the accumulating prev is the entire Res2Net mechanism — there is no special layer, only a loop that feeds each group the previous group's output. h.mean(dim=2) is the squeeze, and it is the only place in the block where information crosses the whole utterance. And h + res is what lets the multi-layer aggregation later see a running sum rather than only the final transformation.

Where the compute goes

ECAPA is cheap, and it is worth knowing which parts are cheap. Take C = 1024 and T = 398 frames (four seconds).

ComponentMultiply-accumulatesShare
Initial conv, k=5: 80 × 1024 × 5 × 398163 M3.3%
Per block — the two 1×1 convs: 2 × 10242 × 398835 M
Per block — the Res2Net convs: 7 × 1282 × 3 × 398137 M
Three SE-Res2Blocks total2.92 G58.5%
MFA 1×1 conv: 3072 × 1536 × 3981.88 G37.7%
Attention + pooling + FC< 30 M0.6%
Total per 4-second utterance≈ 5.0 G100%

The multi-layer aggregation is the single most expensive layer in the network — a 1×1 convolution across 3,072 input channels at every one of 398 frames. That is a real cost for a component whose individual ablation contribution is on the order of a tenth of an EER point, and it is a reasonable first thing to shrink when you need a smaller model.

Notice also that the Res2Net convolutions are only 137 M of each block's 971 M — about 14%. The multi-scale machinery that the paper is named after is nearly free; the expensive part is the 1×1 channel mixing that surrounds it. This is the same accounting as depthwise-separable convolutions in vision: once you factor the spatial and channel operations apart, the spatial one becomes negligible.

The training recipe, which is half the result

Reproducing ECAPA's numbers requires the recipe, not just the architecture. Every item below appears in the paper or its reference implementations, and skipping any of them costs measurable accuracy.

IngredientSettingWhy it matters
Crops2 s random crops during trainingForces the pooling layer to work with few frames, which is the deployment condition. Training only on long clips produces a model that collapses on short ones
AugmentationMUSAN noise/music/babble + simulated room impulse responses, as in Chapter 2Still the largest single lever. Inherited unchanged from the x-vector paper
SpecAugmentRandom masking of time and frequency bands in the fbankPrevents the model from depending on any single Mel band — a cheap channel-robustness prior
LossAAM-softmax, s = 30, m = 0.2, with margin warm-upChapter 5. Starting at the full margin from random init often fails to converge
ScheduleCyclical learning rate, weight decay on weights onlyStandard, but the cyclical schedule is specified in the paper and reproductions that use a flat schedule land noticeably worse
Large-margin fine-tuneA short final phase on 3–6 s crops with a larger margin and a tiny learning rateA consistent 10–20% relative win. Almost universal in competitive systems and almost never in the architecture diagram
ScoringCosine + AS-Norm (Chapter 6)Worth as much as several architectural components combined
The uncomfortable reading of that table. If you ablated the recipe with the same rigour the paper ablates the architecture, several of the rows above would show larger effects than any of the four named upgrades. That is not a criticism of the paper — it is honest about its recipe — but it is a warning about how architecture papers get read. The block diagram is the part that gets copied into slides; the crop length and the score normalisation are the parts that decide whether your reproduction lands at 0.9% or 1.6%.

Why the attention needs global context

One detail of the attentive pooling deserves its own argument, because it is easy to dismiss as an implementation flourish.

ECAPA feeds the attention network each frame concatenated with the utterance-level mean and standard deviation. Without that, the attention scores a frame in isolation: it can learn "loud frames are informative" or "frames with high-frequency energy are informative", both of which are rules about absolute levels.

Absolute levels are exactly what changes between recordings. A frame that is loud in a quiet studio recording and a frame that is loud in a noisy car have completely different informativeness, and an attention network that cannot see the recording's overall statistics has no way to tell them apart. Give it the global mean and standard deviation and the rule it can learn becomes relative: this frame is unusually clean for this recording. Relative rules transfer across channels; absolute ones do not.

That is the same argument, in a third place, as the SE block's squeeze and the CMVN of Chapter 1: a local decision made without global context is a decision about a level, and levels are the nuisance. Three components, three global means, one idea.

The squeeze step of an SE block averages each channel over the entire utterance. Why is that a big deal in a network built from 3-tap convolutions?

Chapter 5: The Margin Arrives from the Face World

Chapter 2 left a loose end. x-vectors are trained with plain softmax and then need PLDA, a whole second model, to be scored well. Chapter 3 showed one escape: change the loss so that it trains on comparisons directly. This chapter is the other escape, and it is the one the field actually took.

It did not come from speech. It came from face recognition, where the identical problem — open-set biometric matching with a cosine at test time — had been attacked for five years by a series of papers with escalating names: FaceNet, SphereFace, CosFace, ArcFace. ECAPA-TDNN's loss is ArcFace's loss, unchanged, applied to voices.

Why plain softmax produces a bad metric

Start with the diagnosis. The softmax logit for class j is wjTx + bj. Write the dot product in polar form:

wjTx = ‖wj‖ · ‖x‖ · cosθj

Three quantities determine the logit: the weight norm, the feature norm, and the angle. Only the third one is about identity. The other two are free parameters the optimiser will happily abuse.

And it does abuse them. Two concrete pathologies, both observed in practice:

Pathology 1 — frequent classes grow longer weight vectors. If speaker 17 has 800 training utterances and speaker 4,022 has 40, the optimiser can lower total loss by simply making ‖w17‖ larger. Nothing about speaker 17's direction improved; the class just got louder. At test time, when you delete the head and compare embeddings by cosine, all of that effort is invisible — you kept only the angles.

Pathology 2 — easy examples inflate their feature norm. A clean, long, close-miked utterance gets a large ‖x‖, which makes its logit large, which makes its loss small — without its angle ever having improved. The loss is satisfiable by being loud rather than by being correctly oriented.

The mismatch, in one sentence. Training optimises a quantity that depends on norms and angles; deployment reads a quantity that depends on angles alone. Every unit of progress the optimiser makes on norms is progress on a metric nobody will ever use. PLDA exists to clean up after this. The margin losses fix it at the source.

Step one: normalise everything

The fix begins by removing the two nuisance quantities. Set the bias to zero, constrain every class weight to unit length, and normalise the feature to a fixed length s:

bj = 0,   ‖wj‖ = 1,   ‖x‖ = s  →   logitj = s · cosθj

Now the only thing the loss can improve is the angle between the embedding and its class direction. Training and testing finally measure the same thing.

But this creates a new problem, and it is severe enough to be worth computing. With logits confined to s·[−1, 1], can the softmax ever be confident? Take s = 1 and the best possible embedding: the target class at cos = 1 and all 5,993 others at cos ≈ 0.

ptarget = e1 / (e1 + 5993 · e0) = 2.71828 / 5995.72 = 0.000453
L = −log(0.000453) = 7.699

Chance level is log(5994) = 8.699. So a perfect embedding reduces the loss by 1.0 out of 8.7. There is almost no gradient signal distinguishing a great embedding from a mediocre one. Now set s = 30:

ptarget = e30 / (e30 + 5993) = 1.0686×1013 / (1.0686×1013 + 5993) ≈ 1 − 5.6×10−10
L ≈ 5.6×10−10
The scale s is not a tuning knob, it is a feasibility requirement. Without it, normalised softmax cannot express confidence and therefore cannot be optimised. s = 30 (ECAPA's choice; ArcFace uses 64 for faces with far more classes) restores the dynamic range that normalisation destroyed. If you ever change the number of classes by an order of magnitude, revisit s — the required scale grows roughly with log(C).

Step two: add the margin

Normalising alone gives a loss that is happy as soon as the target class merely wins. But "merely wins" is a terrible standard for an open-set problem: at test time the impostor will be someone who was not in the training set, possibly closer than any training impostor was. We want the target angle to be small with room to spare.

Additive angular margin softmax (AAM-softmax, ArcFace) does this by penalising the target class's angle before the softmax:

L = −log   exp(s·cos(θy + m)) / [ exp(s·cos(θy + m)) + ∑j≠y exp(s·cosθj) ]

Only the target term gets the margin. The model is scored as if its embedding were m radians further from its own class than it actually is, so to satisfy the loss it must get m radians closer than it otherwise would.

ECAPA uses m = 0.2 radians. In degrees: 0.2 × 180/π = 11.459°. Hold on to that number, we are about to use it three times.

The margin, worked in degrees

Set up a concrete situation. An embedding sits 40° from its own speaker's class direction, and the nearest impostor class sits at 55°. Scale s = 30.

Without a margin:

cos 40° = 0.76604 → logit 22.981
cos 55° = 0.57358 → logit 17.207
gap = 5.774

Since only two classes matter here, the probability of the correct class is a logistic of the gap:

p = 1/(1 + e−5.774) = 1/(1 + 0.003107) = 0.99690
L = −log(0.99690) = 0.003105

The loss is essentially zero. The optimiser has no reason to improve this embedding further, even though 40° is not a comfortable margin against an unseen impostor.

With m = 0.2 rad = 11.459°: the target angle is treated as 40° + 11.459° = 51.459°.

cos 51.459° = 0.62305 → logit 18.692
cos 55° = 0.57358 → logit 17.207 (unchanged)
gap = 1.485
p = 1/(1 + e−1.485) = 1/(1 + 0.22651) = 0.81532
L = −log(0.81532) = 0.20418

The loss went from 0.0031 to 0.2042 — 66 times larger — for an embedding that did not change at all. That is a gradient the optimiser will act on.

How much closer must it get? Solve for the angle that restores the original loss. We need the gap back at 5.774, so cos(θy + m) must reach 0.57358 + 5.774/30 = 0.57358 + 0.19247 = 0.76605, which is cos 40.00°. Therefore:

θy + 11.459° = 40.00°  →  θy = 28.54°
The margin buys exactly m degrees of angular clearance, and the algebra says so exactly. 40° − 28.54° = 11.46° = m. This is not an approximation or an empirical observation — it falls straight out of the substitution. Every embedding is pulled into a cone m radians tighter around its class centre than plain normalised softmax would have demanded. That tighter cone is the whole product: it is margin against impostors the training set never contained.

The decision boundary moves too, and in the same amount. Plain softmax puts the boundary between classes y and j where cosθy = cosθj, i.e. θy = θj. AAM puts it where cos(θy + m) = cosθj, i.e. θy = θj − m. The boundary is shoved m radians toward the target class, leaving a strip of angle that belongs to neither.

Angular margin geometry

Class directions on the unit circle with one embedding. Move the embedding's angle and the margin, and watch the two losses (plain normalised softmax versus AAM) diverge. The shaded wedge is the margin: the angular territory the loss refuses to accept even though the embedding is technically on the right side.

Angle to own class 40°
Margin m (rad) 0.20
Scale s 30

Push the margin slider to 0.6 rad and watch the loss stay enormous even when the embedding is nearly perfect. That is the practical failure mode: too large a margin makes the loss unsatisfiable from a random initialisation, and training either diverges or collapses. The standard remedy is a margin warm-up — start at m = 0 and ramp to the target over the first few epochs — which nearly every production recipe uses and nearly every paper mentions only in a footnote.

The family tree, and what each member changed

LossOriginTarget logitWhere the margin acts
Softmax‖w‖‖x‖cosθ + bNowhere. Norms are free to absorb the loss
Triplet (FaceNet, 2015)FacesNot a classifier — d(a,p) + α < d(a,n)In Euclidean distance. Requires hard-negative mining
SphereFace / A-softmax (2017)Facess·cos(m·θy)Multiplicative on the angle. Non-monotone, needs careful handling
CosFace / AM-softmax (2018)Facess·(cosθy − m)Additive on the cosine. Simple, stable — but the angular effect varies with θ
ArcFace / AAM (2019)Facess·cos(θy + m)Additive on the angle. The margin is a constant number of degrees everywhere on the sphere

The reason ArcFace won, and the reason ECAPA uses it: with CosFace, subtracting a fixed m from the cosine translates into different angular penalties depending on where you are — near θ = 0 the cosine is flat, so a cosine margin of 0.35 costs a lot of degrees; near θ = 90° the cosine is steep, so the same 0.35 costs few. ArcFace's margin is uniform in the quantity that actually gets measured at test time. Geometry that matches the metric.

Bridge worth naming explicitly. Faces and voices are the same mathematical problem wearing different clothes: an open-set biometric where enrolment is one example, the gallery changes daily, and the test-time comparison is a cosine. That structural identity is why a loss invented for LFW and MegaFace transferred to VoxCeleb without a single modification. The same family now trains text embedding models, image retrieval models and re-identification models — anywhere the deployed operation is nearest-neighbour search rather than classification. If your model will be searched rather than asked, you probably want an angular margin.

Why this kills PLDA

Return to Chapter 2's diagnosis. PLDA existed because the x-vector space had identity directions and nuisance directions mixed together, and something had to learn which was which. AAM-softmax trains the encoder so that:

Every utterance of a speaker
is pulled into a cone of angular radius (roughly) θmax − m around that speaker's class direction — all channels, all sentences, all sessions.
↓ which means
Within-speaker variation is angularly small
by construction, and between-speaker separation is angularly large by the same construction — the two things PLDA had to estimate from data.
↓ therefore
Cosine is already the right scorer
No backend, no domain-specific estimation, no second training procedure. Score = dot product of two unit vectors. Chapter 0's "one matmul" becomes literally true.

This is the single most practically important consequence in the lesson. It is why a modern speaker verification system is one ONNX file and a dot product, and why the four-step centre-LDA-lengthnorm-PLDA pipeline of 2018 has quietly disappeared from production stacks. The honest caveat: PLDA still helps under severe domain shift, where the training-time cone assumption breaks — and adaptive score normalisation (AS-Norm) remains widely used for exactly that reason.

The loss in code, and the trap inside it

class AAMSoftmax(nn.Module):
    def __init__(self, emb_dim=192, n_spk=5994, s=30.0, m=0.2):
        super().__init__()
        self.W = nn.Parameter(torch.randn(n_spk, emb_dim))
        self.s, self.m = s, m
        self.cos_m, self.sin_m = math.cos(m), math.sin(m)
        self.th  = math.cos(math.pi - m)          # where θ + m crosses π
        self.mm  = math.sin(math.pi - m) * m      # the linear fallback slope

    def forward(self, emb, labels):              # emb: (B, 192)
        x = F.normalize(emb, dim=1)                # ‖x‖ = 1
        W = F.normalize(self.W, dim=1)             # ‖w_j‖ = 1
        cos = x @ W.t()                           # (B, n_spk) — pure cosines
        sin = torch.sqrt((1.0 - cos ** 2).clamp(0, 1))

        # cos(θ + m) = cosθ·cos m − sinθ·sin m — no arccos needed
        cos_m = cos * self.cos_m - sin * self.sin_m

        # θ + m > π: cos(θ+m) turns back UP, so the "penalty" would reward.
        # Fall back to a monotone linear surrogate past that point.
        cos_m = torch.where(cos > self.th, cos_m, cos - self.mm)

        one_hot = F.one_hot(labels, cos.size(1)).float()
        logits = self.s * (one_hot * cos_m + (1 - one_hot) * cos)
        return F.cross_entropy(logits, labels)

The torch.where line is the trap, and it is worth understanding rather than copying. The margin is applied as cos(θ + m), and the cosine is monotonically decreasing only on [0, π]. If an embedding is already almost opposite its class direction — θ close to π, which happens for a few examples early in training — then θ + m exceeds π, the cosine starts increasing again, and the "penalty" becomes a reward. The loss would then actively push those examples further away.

Work the boundary. With m = 0.2, the crossing is at θ = π − 0.2 = 2.9416 rad = 168.5°, where cosθ = −0.9801. So any embedding whose cosine to its own class is below −0.9801 falls into the bad region. The standard fix — ArcFace's, reproduced above — replaces the trigonometric penalty with a linear one, cosθ − m·sin(π−m), which is monotone everywhere and agrees with the correct expression at the boundary.

Also note that the implementation never calls arccos. The angle-addition identity cos(θ+m) = cosθ·cos m − sinθ·sin m gets the same result from the cosine alone, which matters because arccos has infinite derivative at ±1 and would produce NaN gradients for any embedding that lands exactly on a class direction.

How much room is there on the sphere?

A margin only makes sense if the space can hold all the classes at that separation. It is worth a rough count, because the answer explains why 192 dimensions is enough and why the margin cannot simply be raised.

Two class directions separated by at least an angle δ occupy non-overlapping spherical caps of angular radius δ/2. The fraction of a d-sphere's surface covered by one such cap scales, for small caps in high dimension, like sind−1(δ/2). So the number of classes that fit is bounded roughly by:

Nmax ≈ 1 / sind−1(δ/2)

Put d = 192 and a modest required separation of δ = 20°, so δ/2 = 10° and sin(10°) = 0.1736:

Nmax ≈ (1/0.1736)191 = 5.76191 — astronomically large
Dimension 192 is not remotely the binding constraint. The sphere in 192 dimensions has room for more mutually-separated directions than there are atoms in the observable universe. Whatever limits speaker verification, it is not geometric capacity. It is that the encoder cannot map every recording of a person to the same direction — within-speaker variation, not between-speaker crowding, is the whole problem. This is why embeddings kept shrinking (512 → 256 → 192) with no loss: the extra coordinates were never doing anything, and fewer of them means less room for nuisance to hide in.

Which reframes the margin. It is not fighting for space. It is forcing the encoder to reduce within-speaker spread, by refusing to accept a solution where a speaker's recordings merely land on the right side of a boundary. And that is why a margin that is too large does not "run out of room" — it becomes unsatisfiable because the encoder cannot compress the genuine session-to-session variation of a human voice below some floor, and asking it to do so just breaks training.

Two failure modes worth knowing

Label noise. VoxCeleb is built from YouTube, and a fraction of its clips are mislabelled — a different person in the same video, or a segment where the celebrity is not the one speaking. Under a plain softmax a wrong label is one bad example among millions. Under a margin loss it is much worse: the margin says "get much closer to this class than you would otherwise need to", so the loss on the mislabelled example is enormous and the gradient it produces is correspondingly large. Margin losses amplify label noise by construction.

The published fix is sub-centre ArcFace: give each class K sub-centres (K = 3 typically), take the maximum cosine over them, and let outliers cluster on a sub-centre of their own rather than dragging the main one. The pragmatic fix is data cleaning — the "cleaned" VoxCeleb trial lists exist for exactly this reason.

Margin warm-up. At random initialisation, embeddings are near-orthogonal to every class direction, so θ is around 90° for everything and cos(θ+m) is negative for the true class. The loss is then large and nearly flat across classes, and with a full margin from step zero the model frequently fails to escape. Ramping m from 0 to 0.2 over the first few epochs solves it. Every production recipe does this; most papers mention it in one clause, if at all.

Under AAM-softmax with s = 30 and m = 0.2 rad, an embedding at 40° from its class direction faces a nearest impostor class at 55°. The plain-softmax loss is 0.0031; the AAM loss is 0.204. How much closer must the embedding get to restore the original loss value, and why is that number what it is?

Chapter 6: EER, minDCF, and Where to Stand

Every number quoted so far — 1.01%, 0.87%, 2.12% — is an equal error rate. This chapter derives what that means from zero, computes one by hand on ten trials, then shows that the operating point EER picks is, for a bank, catastrophically wrong — and computes the alternative by hand too.

The trial, and the two ways to be wrong

A verification system does not classify. It scores a trial: a pair consisting of an enrolment and a test utterance, plus a hidden truth about whether they are the same person. Vocabulary, which is worth fixing because the literature uses several sets of words for the same two mistakes:

Trial typeTruthSystem says acceptSystem says reject
Target (genuine)Same speakerCorrectMiss = false rejection. Rate: Pmiss, FRR
Non-target (impostor)Different speakersFalse alarm = false acceptance. Rate: Pfa, FARCorrect

The system emits a real-valued score. A threshold θ converts it into a decision: accept if score ≥ θ. Both error rates are therefore functions of θ, and they move in opposite directions:

Pmiss(θ) = fraction of target trials scoring below θ  —  increases with θ
Pfa(θ) = fraction of non-target trials scoring at or above θ  —  decreases with θ

Raise the threshold and you reject more impostors and more customers. Lower it and you admit more of both. There is no θ that reduces both. That trade-off is the system's quality: a better embedding is one where the two score distributions overlap less, so that every threshold is a better deal than it was.

Why a single accuracy number cannot describe this. Accuracy depends on how many impostor trials you chose to include, which is an arbitrary property of the evaluation list, not of the system. Make the trial list 99.9% non-target and a system that rejects everything scores 99.9%. Pmiss and Pfa are each computed within their own class, so they are invariant to the mix. Every metric in this chapter is a way of summarising the (Pmiss, Pfa) curve into one number, and every one of them loses something.

Equal error rate, hand-computed

The EER is the single point where Pmiss(θ) = Pfa(θ), and the reported number is that shared value. Let us compute one exactly on a set small enough to enumerate. Ten trials — five target, five non-target — with these cosine scores:

Target scores (same speaker)Non-target scores (impostors)
0.82, 0.74, 0.61, 0.55, 0.310.66, 0.48, 0.35, 0.22, 0.10

Notice the interleaving: one impostor (0.66) outscores three genuine trials, and one genuine trial (0.31) is beaten by two impostors. That overlap is the system's error. Now sweep θ and count.

θTargets acceptedPmissNon-targets acceptedPfa
0.850 of 51.000 of 50.00
0.801 of 50.800 of 50.00
0.702 of 50.600 of 50.00
0.652 of 50.601 of 50.20
0.554 of 50.201 of 50.20
0.454 of 50.202 of 50.40
0.305 of 50.003 of 50.60

Check the bolded row by hand. At θ = 0.55, the target scores at or above it are 0.82, 0.74, 0.61 and 0.55 — four out of five, so one miss, Pmiss = 1/5 = 0.20. The non-target scores at or above 0.55: only 0.66 — one out of five, so Pfa = 1/5 = 0.20. The two rates are equal.

EER = 20%, achieved for any θ in (0.48, 0.55]

(The crossing is an interval rather than a point because scores are discrete. With 37,611 trials, as on VoxCeleb1-O, the curve is fine-grained enough that the crossing is essentially a point, and standard tools interpolate.)

Twenty percent is a terrible system, which is deliberate — the numbers had to be hand-checkable. A real ECAPA at 0.87% would need roughly 115 target trials before it misses one.

Why EER is the wrong place to stand

EER is a fine summary and a bad operating point, because it silently assumes the two errors cost the same. For a bank, a false acceptance is a fraud loss and a regulatory event; a false rejection is a mildly annoyed customer who tries again. The costs might differ by a factor of a thousand. And there is a second asymmetry: almost every trial is an impostor. Fraud attempts are rare, so the population is overwhelmingly genuine, or — in the attack-oriented framing NIST uses — the prior probability of a target trial is low.

The detection cost function puts both asymmetries in one expression:

DCF(θ) = Cmiss · Ptarget · Pmiss(θ)  +  Cfa · (1 − Ptarget) · Pfa(θ)

Read the terms: each error rate is multiplied by how much that error costs and by how often that kind of trial occurs. It is an expected cost per trial. Divide by the cost of the best trivial system so that 1.0 means "no better than always saying no":

DCFnorm(θ) = DCF(θ) / min( Cmiss·Ptarget,  Cfa·(1−Ptarget) )

minDCF is the minimum of that over all thresholds — the cost you would pay if you had picked the threshold perfectly. The VoxCeleb convention is Ptarget = 0.01 with both costs at 1; NIST SRE has used 0.01, 0.005 and (with Cmiss = 10) 0.001.

minDCF on the same ten trials, by hand

Use Ptarget = 0.05, Cmiss = Cfa = 1. The normaliser is min(0.05, 0.95) = 0.05, so:

DCFnorm(θ) = (0.05·Pmiss + 0.95·Pfa) / 0.05 = Pmiss + 19·Pfa

A false alarm is now worth nineteen misses. Recompute the sweep:

θPmissPfaDCFnorm = Pmiss + 19 Pfa
0.851.000.001.00
0.800.800.000.80
0.700.600.000.60 ← minimum
0.650.600.200.60 + 3.80 = 4.40
0.55 (the EER point)0.200.200.20 + 3.80 = 4.00
0.450.200.400.20 + 7.60 = 7.80
0.300.000.600.00 + 11.40 = 11.40
Look at what just happened. minDCF = 0.60, at θ = 0.70. The EER threshold of 0.55 gives a normalised cost of 4.00 — nearly seven times worse, and four times worse than a system that simply rejects every trial (which by construction scores 1.00). A system tuned to its equal error rate, deployed at a bank's operating prior, would be worse than useless. Same scores, same system, same day. Only the question changed.

This is why papers report both numbers and why the two can rank systems differently. EER measures the overall separation of the two distributions. minDCF measures the quality of the tail — how far you can push the threshold up before genuine users start falling out. A system with beautiful average separation and a fat impostor tail wins on EER and loses on minDCF. When you read "0.87% EER / 0.107 minDCF", the second number is the one your security team cares about.

EER and minDCF threshold playground

Two score distributions — targets in teal, impostors in red — with a movable threshold. Below: Pmiss and Pfa as functions of the threshold, their crossing (the EER), and the normalised DCF curve with its own, different minimum. Change the operating prior and watch the two optimal thresholds pull apart.

Threshold 0.50
System quality good
Ptarget:

The DET curve, and why it is not a ROC

Plot Pmiss against Pfa as θ sweeps and you get a detection error tradeoff curve. It differs from a ROC curve in two ways that both matter.

First, both axes are errors, so lower-left is better and the curve descends. Second — the important one — both axes are on a normal deviate scale: the position of a point at rate p is Φ−1(p), the inverse of the standard normal CDF. Under the (roughly true) assumption that target and non-target scores are Gaussian with equal variance, this warping turns the DET curve into a straight line. Systems become comparable by slope and intercept rather than by squinting at curvature, and the interesting low-error region gets stretched out instead of being crushed into a corner.

Error rate pΦ−1(p)Visual effect
0.500.000Centre of the axis
0.10−1.282
0.01−2.326Still comfortably on the plot
0.001−3.090Where a linear axis would have crushed everything to zero

The EER is where the DET curve crosses the diagonal Pmiss = Pfa. minDCF is where a line of a particular slope (set by the cost ratio) is tangent to the curve. Two different geometric constructions on the same curve — which is exactly why they can pick different systems.

Score normalisation: the fix nobody mentions in the abstract

One practical problem breaks the whole framework: a single global threshold assumes all speakers' scores are on the same scale. They are not. Some voices are generic — they score moderately high against everybody, so their impostor trials cluster near the threshold. Some are distinctive and score near zero against everyone.

Adaptive score normalisation (AS-Norm) fixes this per trial. Keep a cohort of a few thousand impostor utterances. For the enrolment vector e, score it against the cohort, take the top-K most similar (K around 300), and compute that subset's mean μe and standard deviation σe. Do the same for the test vector t. Then:

s′(e,t) = ½ [ (s(e,t) − μe)/σe  +  (s(e,t) − μt)/σt ]

In words: instead of asking "how similar are these two?", ask "how unusually similar are these two, compared with how similar this voice usually is to strangers?" It is a z-score against a personalised impostor distribution. In practice AS-Norm is worth 10–20% relative on minDCF, costs a few thousand extra dot products per trial, and appears in essentially every competitive VoxCeleb submission while rarely making it into an architecture diagram.

Concept + realization. Every one of these corrections — length normalisation, PLDA, AS-Norm, per-domain calibration — exists because the raw output of a neural network is a score, not a probability, and scores are only comparable within the context they were produced. Calibration is not a footnote to a verification system; it is the layer that turns a ranking into a decision. If you are building one of these, budget as much time for the scoring pipeline as for the model.

What to distrust when reading numbers

ReportedQuestion to askWhy it can move the number
"1.01% EER on VoxCeleb"Vox1-O, Vox1-E, or Vox1-H?Vox1-H roughly doubles the EER for the same system — same gender, same nationality impostors
"EER 0.87%"Cleaned trial list or the original?The original VoxCeleb1 list contains label errors; the cleaned list is the standard
"State of the art"Score normalisation? Test-time augmentation? Fusion of several models?AS-Norm plus a fusion of four systems can beat a single model by 30% relative and is not the same claim
Any EER at allWhat is the minDCF?An EER-optimised system can be worse than useless at an operational prior, as the hand computation above shows
Utterance-level EERHow long are the test utterances?Below 2 seconds, error rates for the same model can triple. Duration is a hidden axis in every table

How many trials do you need to trust an EER?

Here is a question almost nobody asks about a reported error rate, and the arithmetic is elementary enough that there is no excuse.

An EER of 1% measured on Ntar target trials is an estimate of a proportion. The standard error of a proportion p from n independent samples is:

SE = √( p(1 − p) / n )

VoxCeleb1-O has roughly 18,800 target trials. For p = 0.01:

SE = √( 0.01 × 0.99 / 18,800 ) = √(5.27 × 10−7) = 0.000726 = 0.073%

So a 95% interval is roughly ±1.96 × 0.073% = ±0.14 percentage points. A system reported at 0.87% and one reported at 0.95% are, on that list, statistically indistinguishable — and papers routinely present such a gap as an improvement.

Now run it the other way. To resolve a difference of 0.05 percentage points at 1% EER with any confidence you would need the standard error below about 0.025%, requiring:

n = p(1 − p) / SE2 = 0.0099 / (0.00025)2 = 158,400 target trials

Which is roughly why the extended list Vox1-E, with about 580,000 trials, exists. It is not merely "more data" — it is the resolution needed to distinguish modern systems at all.

The caveat that makes this optimistic. Trials are not independent. The same 40 speakers appear in thousands of trials each, so an unlucky speaker — one with a hoarse recording, or an unusual accent for the training distribution — correlates errors across many trials at once. The effective sample size is closer to the number of distinct speakers than the number of trials. Which means the honest error bars are wider than the formula above, and the right reflex when reading a table of EERs separated by hundredths of a point is scepticism, not ranking.

Duration: the hidden axis in every table

Every EER in this lesson was measured on VoxCeleb utterances averaging around 8 seconds. Production audio is often much shorter, and the degradation is not gentle.

The mechanism is the pooling estimator again. Statistics pooling estimates a mean from T frames, and the standard error of a mean falls as 1/√T. Going from 8 s (about 800 frames) to 1 s (about 100 frames):

√(800/100) = √8 = 2.83× more noise in every pooled coordinate

And a second, larger effect stacks on top: with 100 frames, which phonemes occurred stops averaging out. A one-second clip might contain four phonemes; a different one-second clip from the same speaker contains four different phonemes; and the pooled statistics of those two are genuinely different for content reasons, not identity reasons. Short-duration error rates are dominated by content leakage, which is why training on short crops (Chapter 4's recipe) helps far more than any architectural change.

Test durationApprox. framesPooling noise (relative)What usually happens to the EER
8 s (VoxCeleb typical)8001.0×The number in the paper
4 s4001.41×Modest degradation
2 s2002.0×Noticeably worse; short-crop training starts to matter a lot
1 s1002.83×Often several times the published EER — content leakage dominates

Calibration: turning a score into a decision you can defend

EER and minDCF both assume you can find the right threshold. In deployment you must choose one before seeing the test data, and a raw cosine gives no principled way to do so. Calibration is the missing step.

The goal is to map the raw score to a log-likelihood ratio:

ℓ(x) = log  p(x | same speaker) / p(x | different speakers)

because a log-likelihood ratio composes with a prior by simple addition. Once you have one, the Bayes-optimal threshold is fixed by the costs and the prior alone, with no reference to any dataset:

θ = log   [ Cfa · (1 − Ptarget) ] / [ Cmiss · Ptarget ]

Work it for the bank of Chapter 6: Ptarget = 0.01, Cmiss = 1, Cfa = 10.

θ = log( 10 × 0.99 / (1 × 0.01) ) = log(990) = 6.898

Accept only when the calibrated score exceeds 6.898 nats — that is, when the evidence for "same speaker" is about a thousand times stronger than the evidence against. No threshold sweep, no held-out set at deployment time, and if the fraud team changes Cfa to 50 you recompute the threshold in one line rather than re-running an evaluation.

Getting there in practice is usually one line of logistic regression on a held-out development set: fit s′ = a·s + b to maximise the likelihood of the development labels. Two parameters. The metric that measures whether it worked is Cllr, the cost of log-likelihood-ratio, which is the average information loss of your scores against a perfectly calibrated oracle — and unlike EER, it is sensitive to how wrong a confident wrong answer was, not merely to whether it crossed a line.

Why this is the most underrated part of the stack. A well-calibrated mediocre system is deployable: you can reason about it, set thresholds from business costs, and combine it with other evidence. An uncalibrated excellent system is a research artefact — every threshold decision is a guess, and every domain change silently invalidates it. If you build one of these, the two lines of logistic regression at the end will do more for the product than the last 20% of architecture work.
On the ten-trial toy set, the EER is 20% at θ = 0.55, but the minimum normalised DCF at Ptarget = 0.05 is 0.60 at θ = 0.70 — while the EER threshold gives a normalised cost of 4.00. What is the correct conclusion?

Chapter 7: Diarization — Who Spoke When

Verification has an enrolment. Diarization has nothing: forty-five minutes of a meeting, an unknown number of participants, no labelled examples of any of them, and a required output of the form speaker A from 00:03 to 00:19, speaker B from 00:19 to 00:31. It is the purest possible test of an embedding space, because there is no way to compensate with a good backend — if the geometry is wrong, the clustering is wrong.

The classical pipeline, with arithmetic

1. Voice activity detection
Find the regions containing speech at all. Errors here become "missed speech" and "false alarm speech" in the final metric, and no downstream stage can repair them.
2. Uniform segmentation
Chop the speech into short overlapping windows — typically 1.5 s long with a 0.75 s shift — regardless of who is talking.
3. Embed every window
One ECAPA forward pass per window. This is the step where Chapters 1–5 get used.
4. Cluster with unknown k
Agglomerative clustering with a stopping threshold, or spectral clustering with the eigengap heuristic to choose k.
5. Resegment and smooth
A Bayesian HMM (VBx) or similar refines the boundaries at frame resolution, because 0.75 s windows cannot place a turn change precisely.

Put numbers on step 2. A ten-minute meeting with 600 seconds of actual speech, windowed at 1.5 s with a 0.75 s shift:

windows = ⌊(600 − 1.5) / 0.75⌋ + 1 = ⌊798⌋ + 1 = 799

799 embeddings of 192 dimensions each: 153,408 floats, about 600 kB. The affinity matrix of all pairwise cosines is 799 × 799 = 638,401 entries — computed as one (799×192)(192×799) matrix product, roughly 245 MFLOPs. Nothing here is expensive. The whole difficulty is in choosing the window length and the clustering threshold.

The window-length dilemma

This is the central design tension of the classical pipeline and it has no good resolution.

Window lengthEmbedding qualityBoundary precisionPurity
3.0 sGood — many frames, stable statisticsPoor — a turn change is located to within 3 sPoor — long windows straddle turn changes
1.5 sAcceptable — the usual compromiseModerateModerate
0.5 sPoor — roughly 50 frames, so the pooled statistics are noisy and content-dependentGoodGood when it works

The mechanism behind the first column is worth naming, because it is the same estimator argument in every field. Statistics pooling estimates a mean and a standard deviation from T frames. The standard error of a mean estimated from T samples falls as 1/√T. Going from a 3 s window (about 300 frames) to a 0.5 s window (about 50 frames) multiplies the noise in the pooled statistics by √(300/50) = √6 = 2.45×. And the extra error is not benign random jitter: with only 50 frames, which phonemes happened to occur becomes a first-order effect, so short-window embeddings drift toward content and away from identity.

The dilemma, stated plainly. Short windows give precise timing and unreliable identity. Long windows give reliable identity and imprecise timing. Both errors land in the same metric. Every classical diarizer is a particular compromise on this axis, and the resegmentation stage exists purely to buy back the timing precision that long windows gave away.

Clustering with an unknown number of speakers

Agglomerative hierarchical clustering (AHC) is the simple answer: start with every window as its own cluster, repeatedly merge the two most similar clusters, and stop when the best available similarity falls below a threshold τ. The number of clusters at the stopping point is your speaker count.

The whole system's behaviour hinges on τ, and it fails in both directions. Set it too high and one speaker's windows — recorded across a shift in position, volume, or emotion — split into two clusters, so your two-person interview reports four speakers. Set it too low and two similar-sounding participants merge into one. Because τ is a fixed number applied to a similarity whose scale drifts with recording conditions, a threshold tuned on one corpus rarely transfers to another. AS-Norm from Chapter 6 helps here too, for exactly the same reason.

Spectral clustering is the more robust alternative. Build the affinity matrix A of pairwise similarities, refine it (row-wise thresholding, symmetrisation), form the normalised Laplacian, and take its eigenvalues. The eigengap heuristic picks k as the index of the largest jump between consecutive eigenvalues — the intuition being that a graph with k well-separated components has k near-zero eigenvalues followed by a gap. Then run k-means on the first k eigenvectors.

The eigengap is more stable than a similarity threshold because it reads the structure of the affinity matrix rather than the absolute value of any entry, and structure survives the scale drift that kills τ.

Diarization clustering demo

Top: a meeting timeline, coloured by the system's decision. Bottom: the window embeddings in two dimensions, with true speakers as shapes and predicted clusters as colours. Move the merge threshold and watch the speaker count fall from over-segmentation, through correct, into merged. Then turn on overlapped speech and watch the points that belong nowhere.

Merge threshold 0.55
Window length 1.5 s
True speakers:

Diarization error rate, computed

DER is the total duration of erroneous audio divided by the total duration of speech. Three error types are summed:

DER = ( Tmiss + Tfalse alarm + Tconfusion ) / Tspeech

Missed speech is time that was speech and got labelled as silence (or, in overlap, a second speaker who was never emitted). False alarm is silence labelled as speech. Confusion is speech attributed to the wrong speaker after the optimal one-to-one mapping between predicted and reference labels — found with the Hungarian algorithm, because label identities are arbitrary and "speaker 1" in your output may be "speaker 3" in the reference.

Worked example. A ten-minute meeting with 600 s of speech. The system misses 30 s, hallucinates 12 s of speech in silence, and attributes 48 s to the wrong participant:

DER = (30 + 12 + 48) / 600 = 90 / 600 = 15.0%

Now score the overlap. Suppose 8% of the speech time — 48 s — has two people talking at once, and our system, like every clustering-based diarizer, emits exactly one label per instant. Every overlapped second is a missed second for the second speaker:

DERwith overlap = (30 + 48 + 12 + 48) / 600 = 138 / 600 = 23.0%

The same system, the same audio, the same output — and the error rate goes from 15.0% to 23.0% purely by deciding to score the hard part. Overlap rates of 5–15% are typical in real meetings and much higher in casual conversation.

The two conventions that quietly halve published DERs. First, a collar — usually 0.25 s on each side of every reference boundary — is excluded from scoring, on the grounds that human annotators cannot place a turn change more precisely than that. Reasonable, and it removes exactly the region where a windowed system is weakest. Second, overlap is often ignored entirely. A DER quoted with a 0.25 s collar and no overlap scoring can be less than half the DER of the same system scored fully. Always ask which convention a number used, the way you would ask which VoxCeleb list an EER came from.

The overlap problem, and why it is structural

Overlap is not an edge case that better engineering will clean up. It is a direct consequence of Chapter 1's design.

Take a 1.5 s window in which Ava and Ben are both talking. The frame network sees a mixture. Pooling averages over all its frames. The resulting embedding is, roughly, a point between Ava's cluster and Ben's cluster — and here is the fatal part: the average of two identities is a perfectly plausible third identity. The embedding space has no way to signal "this is a blend". A point between two clusters looks exactly like a person whose voice sits between them.

Say it precisely. The pooled representation is a single point, so it can encode "who" only if there is exactly one "who". A one-vector-per-window architecture cannot represent two simultaneous speakers even in principle, no matter how good the encoder is. This is not a training problem or a data problem. It is the representation's expressive limit, and it is the direct price of the permutation-invariant pooling that made everything else work.

Which is why the modern answers all abandon the "embed then cluster" shape:

ApproachHow it handles overlapWhat it costs
EEND — end-to-end neural diarizationOutputs a per-speaker binary activity at every frame, so two speakers can both be active. Trained with permutation-invariant loss over label orderingsNeeds a fixed maximum speaker count (or an attractor mechanism to grow it), and needs simulated mixtures for training
TS-VAD — target-speaker VADGiven candidate speaker embeddings, predicts each one's activity independently, so overlaps are simply two positivesNeeds an initial diarization to produce the candidate embeddings — usually the classical pipeline, run first
Separation firstRun speech separation, then diarize each separated streamSeparation artefacts corrupt the embeddings, and stream-to-speaker assignment is its own problem
Hybrid (VBx + overlap detector)Classical pipeline, plus a dedicated detector that assigns a second label in detected overlap regionsTwo systems to tune; the overlap detector is itself error-prone

Note what EEND gives up to gain overlap handling: it is no longer producing a reusable speaker embedding at all. It produces activity curves for the speakers in this recording. You cannot take an EEND output and look someone up in a database. The embedding's weakness on overlap and its strength as a portable identity are two views of the same design decision.

Spectral clustering, worked on a four-window toy

The eigengap heuristic is usually stated and rarely demonstrated. It is small enough to do by hand, and doing it once makes the method stop feeling like magic.

Take four windows: two from speaker A, two from speaker B, with cosine similarities that reflect that structure. Here is the affinity matrix (self-similarities zeroed, as is standard):

w₁w₂w₃w₄
w₁00.90.10.1
w₂0.900.10.1
w₃0.10.100.9
w₄0.10.10.90

The degree of each node is the sum of its row: 0.9 + 0.1 + 0.1 = 1.1, identically for all four. That uniformity makes the normalised Laplacian easy: with D = 1.1·I,

Lsym = I − D−1/2 A D−1/2 = I − A/1.1

So the eigenvalues of L are 1 − λA/1.1, where λA are A's eigenvalues. By symmetry, A's eigenvectors are the four sign patterns:

EigenvectorPatternλAλL = 1 − λA/1.1
v₁(+1, +1, +1, +1)0.9 + 0.1 + 0.1 = 1.10.000
v₂(+1, +1, −1, −1)0.9 − 0.1 − 0.1 = 0.70.364
v₃(+1, −1, +1, −1)−0.9 + 0.1 − 0.1 = −0.91.818
v₄(+1, −1, −1, +1)−0.9 − 0.1 + 0.1 = −0.91.818

Verify one row. For v₂ = (1, 1, −1, −1), the first component of A·v is 0(1) + 0.9(1) + 0.1(−1) + 0.1(−1) = 0.9 − 0.2 = 0.7, and 0.7 = 0.7 × 1, so λ = 0.7. Good.

Now sort the Laplacian eigenvalues ascending: 0.000, 0.364, 1.818, 1.818. The gaps between consecutive values are 0.364, 1.454, 0.000. The largest gap sits after the second eigenvalue, so the eigengap heuristic reports k = 2. Correct.

And the eigenvector that produced the decision, v₂ = (+1, +1, −1, −1), is the clustering: the sign pattern separates the two speakers. Running k-means on the first k eigenvectors is a way of reading that structure out when the pattern is less clean than this toy.

Why the eigengap beats a similarity threshold. Notice what we never used: the absolute value 0.9. If every similarity were scaled down — a noisier recording, a mismatched channel, an embedding whose cosines simply run lower — the eigenvalues would change but the gap structure would survive, because the Laplacian is normalised by degree. A hard threshold τ would have failed. Spectral clustering reads the shape of the affinity matrix, and shape is what transfers across recordings.

The pipeline in code

import numpy as np
from scipy.linalg import eigh

def diarize(audio, vad, encoder, win=1.5, shift=0.75, sr=16000):
    segments = vad(audio)                              # [(start, end), ...] speech only
    windows = []
    for (a, b) in segments:
        t = a
        while t + win <= b:
            windows.append((t, t + win)); t += shift
    E = np.stack([encoder(audio[int(a*sr):int(b*sr)]) for a, b in windows])
    E /= np.linalg.norm(E, axis=1, keepdims=True)     # (Nw, 192), unit

    A = E @ E.T                                        # (Nw, Nw) cosine affinity
    A = np.maximum(A, 0); np.fill_diagonal(A, 0)       # standard refinement
    d = A.sum(1) + 1e-9
    L = np.eye(len(A)) - (A / np.sqrt(np.outer(d, d)))  # normalised Laplacian

    vals, vecs = eigh(L)
    gaps = np.diff(vals[:10])
    k = int(np.argmax(gaps)) + 1                        # eigengap → speaker count

    U = vecs[:, :k]
    U /= np.linalg.norm(U, axis=1, keepdims=True) + 1e-9
    labels = kmeans(U, k)                              # (Nw,)
    return list(zip(windows, labels))                     # → RTTM after smoothing

The line that decides everything is k = argmax(gaps) + 1, and note that it searches only the first ten eigenvalues. Without that cap, numerical noise in the tail routinely produces a spurious maximum and the system reports forty speakers in a two-person interview. Capping the search at a plausible maximum speaker count is not a hack; it is a prior, and it should be set from what you know about the recording.

Two more metrics, because DER hides things

MetricWhat it measuresWhat it hides
DERFraction of speech time that is wrong, summed over miss, false alarm and confusionIt is time-weighted, so a system can look excellent by getting the dominant speaker right and mangling everyone who spoke briefly
JER (Jaccard error rate)Averages an error rate per speaker rather than per second, so every participant counts equallyLess intuitive; a single very short speaker can dominate it
Speaker counting accuracyDid the system get k right, ignoring everything elseNothing about boundary quality — but it is often the number a downstream product actually depends on

The DER-hides-short-speakers problem is worth a concrete case. In a meeting where one person talks for 80% of the time, a system that labels everything as that one speaker achieves a DER of 20% before doing any work at all — better than many real systems on hard audio. JER exists because that number is a lie, and any evaluation of a meeting product should report both.

Online diarization, where the whole design breaks

Everything above assumed the recording is finished. A live captioning product cannot wait: it must label each second as it arrives, and that breaks the pipeline in three specific places.

Clustering needs the future
Agglomerative clustering and eigengap both look at the whole affinity matrix. Online, you must assign each new window to an existing cluster or open a new one, with no ability to revise — a much harder decision made with far less evidence.
Speaker count grows
Person four joins at minute twelve. Every threshold that was correct for three speakers may be wrong for four, and there is no going back to relabel the first twelve minutes.
Latency versus window length
A 1.5 s window costs at least 1.5 s of latency before that window can be labelled at all. Shorten it for responsiveness and the embedding degrades exactly as the estimator argument above predicts.

The practical answers are all forms of buying back information: keep a rolling buffer and permit retroactive relabelling within it, maintain running per-speaker centroids that update as evidence accumulates, and accept a fixed lag of a few seconds as the price of a usable error rate. This is the same tradeoff, in a different costume, as every other streaming inference problem on this site.

Why can a clustering-based diarizer not represent two people speaking simultaneously, and what does that tell you about the pooling layer?

Chapter 8: The Lock and the Key Are the Same Vector

Chapter 0 listed four consumers of a speaker embedding. Three of them defend an account. The fourth builds a voice. This chapter is about what happens when the fourth consumer gets good, and it opens with the sentence that the field spent a decade trying not to say out loud.

A perfect speaker embedding must accept a perfect clone. The embedding is defined as a function of the audio that captures speaker identity and discards everything else. If a synthesiser produces audio whose speaker-identity content matches Ava's, then by definition the embedding maps it near Ava's template. Accepting it is not a bug in the model. It is the model doing exactly what it was specified to do. No amount of training makes a verification model reject a signal that is, by its own criterion, correct.

This is not a hypothetical. Google's SV2TTS paper (2018) took a speaker encoder trained with the GE2E loss from Chapter 3 — the same loss, the same architecture — froze it, and used its 256-dimensional output to condition a Tacotron 2 synthesiser. Give it five seconds of a stranger's speech and it generates arbitrary new sentences in that voice. The verification model became the cloning model's steering wheel, with no modification.

The attack surface, from cheapest to hardest

AttackWhat the attacker needsEffortWhat it defeats
ImpersonationA talented mimicHuman skillWeak systems only. Humans are poor at matching vocal-tract geometry
ReplayA recording of the target and a loudspeakerMinutesAny text-dependent system with a fixed passphrase. The oldest and still most common real-world attack
Text-to-speechTarget audio + a zero-shot TTS modelSeconds of reference audioText-dependent and text-independent systems. Generates arbitrary content
Voice conversionTarget audio + the attacker speaking liveReal-time capableEverything above, plus challenge-response, because the attacker can answer live
Adversarial perturbationQuery access to the verification modelOptimisation loopThe embedding directly — an inaudible perturbation moves the vector where you want it

Read the "effort" column top to bottom. It used to be that cloning a voice required hours of clean studio recordings of the target and a day of fine-tuning. VALL-E (2023) conditions on a three-second acoustic prompt and generates in that voice with no fine-tuning at all. YourTTS reaches zero-shot multilingual cloning from similarly short references. The reference audio requirement collapsed from hours to seconds, which moved the attack from "targeted, expensive" to "anyone who has left a voicemail".

Why the embedding cannot defend itself

Consider what a countermeasure would have to detect. The verification embedding was trained, on purpose, to be invariant to channel, to content, to duration, and to session. A synthetic utterance differs from a real one in exactly those dimensions plus a few subtle artefacts. Every invariance we trained in is an invariance the attacker inherits.

What the embedding measures
Vocal-tract resonances, glottal source characteristics, habitual formant targets — the slow, distributional properties that survive pooling.
↓ and a modern vocoder reproduces
Exactly those properties
because they are what a listener perceives as "sounding like her", and matching human perception is the synthesiser's entire training objective.
↓ leaving
Only the residue as evidence
Micro-artefacts in phase, unnaturally smooth pitch contours, missing breath noise, vocoder fingerprints in the high band — none of which the verification model was ever asked to notice.

So the defence has to be a separate model asking a different question. Verification asks "is this Ava?". The countermeasure asks "was this produced by a human vocal tract at all?". Those are different classifiers with different features, and stacking them is called a tandem system.

The tandem, with arithmetic

Two systems in series, and a genuine user must pass both. Suppose:

ComponentOn genuine usersOn cloned attacks
ASV (the speaker verifier)rejects 2% — Pmiss = 0.02accepts 85% — a good clone passes easily
CM (the countermeasure)rejects 3% — Pfa,bonafide = 0.03accepts 5% — catches 19 of 20

Attack acceptance requires passing both, and the two decisions are roughly independent:

P(clone accepted) = 0.85 × 0.05 = 0.0425  (down from 0.85 — a 20× improvement)

Genuine acceptance also requires passing both:

P(genuine accepted) = (1 − 0.02) × (1 − 0.03) = 0.98 × 0.97 = 0.9506
Pmiss, tandem = 1 − 0.9506 = 4.94%  (up from 2.00%)

The countermeasure cut spoof acceptance by a factor of twenty and nearly two and a half times the rate at which real customers get locked out. That trade is the entire subject of the anti-spoofing literature, and it is why the community adopted t-DCF — the tandem detection cost function — which scores the combined system with costs for all three outcomes (miss a genuine user, accept an impostor, accept a spoof) rather than scoring the countermeasure alone.

Why scoring the countermeasure alone is meaningless. A countermeasure that rejects every utterance has a spoof-detection EER of 0% and makes the whole system unusable. A countermeasure with a slightly worse EER but a much better operating point on genuine speech may be strictly better in deployment. t-DCF exists because the two components are not independently optimisable — the same reason Chapter 6 argued that a calibration-free EER is only half a result. Any biometric metric that scores one stage of a pipeline in isolation will mislead you about the pipeline.

t-DCF, written out

The tandem cost function generalises Chapter 6's DCF to three trial types instead of two. A trial can now be a genuine target, a zero-effort impostor (a different real person), or a spoof. Each has a prior and each mistake has a cost:

t-DCF(θCM) = C₁·PmissCM(θ) + C₂·PfaCM(θ)

where C₁ and C₂ are constants that already fold in the fixed verifier's behaviour and the three priors and costs. Read what that structure says: the countermeasure is being scored through the verifier it sits in front of. C₂ contains the factor Pfa,spoofASV — the rate at which the verifier accepts spoofs it is shown — so a countermeasure's false-accept rate matters exactly in proportion to how vulnerable the verifier already was.

Two consequences fall straight out. If the verifier happened to reject a particular attack anyway (Pfa,spoofASV near zero for that attack), the countermeasure's mistakes on it cost almost nothing — so a countermeasure should spend its capacity on the attacks that do get through. And a countermeasure that rejects everything drives PmissCM to 1, so C₁ dominates and the cost is maximal, which is precisely the degenerate case a standalone EER would have rewarded with a perfect score.

Read the ASVspoof evaluation design as a lesson in benchmark construction. The training set contains a handful of attack algorithms; the evaluation set contains different ones, deliberately withheld. That single decision converts the benchmark from "can you detect these six vocoders" into "does your detector generalise to a vocoder that did not exist when you trained" — which is the only question a deployed countermeasure ever faces. Every benchmark that shares generators between train and test measures the wrong thing, and reports numbers that will not survive contact with production.

What a countermeasure actually looks for

SignalWhy synthesis gets it wrongHow long it stays useful
High-band artefacts (above 6 kHz)Vocoders allocate capacity where perception is, and perception is low-band. The top octave is where the seams showWeakens every generation. Modern codec-based models are much cleaner up there
Phase inconsistencyMany vocoders reconstruct phase approximately (or from a magnitude spectrogram, where it was thrown away). Real speech has physically consistent phaseWeakening. Diffusion and codec decoders model waveforms directly
Prosody that is too smoothReal pitch contours have micro-jitter and shimmer from an imperfect biological oscillator; models regress toward the meanWeakening fast — explicit jitter modelling is now standard
Missing breath, lip smacks, swallowsTraining data is often cleaned of exactly these, so models never learn to produce themWeakening — and trivially fixable by an attacker who splices them in
Channel and liveness evidenceReal speech arrives through a real room and a real microphone. Replay adds a second recording chain; injected audio has no acoustic path at allDurable — it is a physical constraint, not a modelling gap
Challenge-response"Please say 4-8-1-9" cannot be answered by a pre-rendered clipDurable against replay and offline TTS; defeated by real-time voice conversion

The pattern is stark. Every artefact-based signal has a shelf life, because each one names a specific weakness that the next generation of synthesisers will fix — often as a side effect of trying to sound better, not of trying to evade detection. Only the signals grounded in physics (there must be an acoustic path) or in interaction (the response must be produced now, in reply to something unpredictable) resist that erosion.

The generalisation failure, measured. The ASVspoof challenges are structured so that the evaluation set contains attack algorithms absent from the training set, and the results are consistent across editions: countermeasures that reach very low error rates on known attacks degrade by large factors on unseen ones. This is the defining problem of the field, and it is structural. A countermeasure is trained to recognise the artefacts of the synthesisers that existed when the dataset was built. The attacker's model is, by construction, newer. You are always classifying tomorrow's generator with yesterday's discriminator.

The irrevocability problem

Passwords have a property biometrics do not: you can change them. If a database of 40 million speaker embeddings leaks, nobody can be issued a new voice.

And embeddings are not opaque. They were built to be geometrically meaningful, which means a leaked template supports:

Mitigations exist and none of them are free. Cancellable biometrics apply a secret, user-specific transform before storage so a leak yields a template that only works with one key — but the transform must preserve enough geometry to keep the comparison accurate, and every bit it preserves is a bit an attacker can exploit. On-device matching never transmits the template at all, which is a real improvement and moves the problem to device security. Fuzzy vaults and homomorphic matching are cryptographically appealing and, at scale, expensive.

The design principle to carry out of this chapter. Treat a voice as an identifier, not an authenticator. An identifier answers "which account is this likely to be" and narrows a search cheaply. An authenticator answers "is this person authorised", and for that, a signal that anyone can record from across a room and reproduce in three seconds is not sufficient on its own. Voice is an excellent first factor and a poor only factor — and the better the embedding gets, the more true that becomes, because the same quality that improves verification improves cloning.

Adversarial perturbation: attacking the vector directly

The attacks above try to sound like the target. There is a cheaper attack that does not bother: perturb the audio so the embedding lands where you want, regardless of what it sounds like.

The optimisation is the one every adversarial-example paper writes. Given a starting utterance x (the attacker's own voice) and a target template t, find a perturbation δ that maximises the score while staying imperceptible:

maxδ   cos( f(x + δ), t )   subject to  ‖δ‖ ≤ ε

Put a number on the constraint. Audio is commonly represented in [−1, 1], and an ε of 0.002 is roughly 54 dB below full scale — well under the noise floor of any real recording chain. A gradient ascent of a few hundred steps against a white-box model routinely moves the cosine from near zero to well above any deployed threshold, with the result still sounding exactly like the attacker.

Why this attack is philosophically different from cloning. A clone exploits the embedding working correctly — it genuinely carries the target's identity cues. An adversarial example exploits the embedding failing: the perturbed audio does not sound like the target to any human, yet the model insists it does. The first is a specification problem with no model-side fix. The second is a robustness problem, and robustness problems do have partial fixes — adversarial training, input randomisation, ensembling, and simply not exposing the score as a continuous value to an attacker who can query you a thousand times. Rate-limiting the API is, unglamorously, one of the more effective countermeasures in this entire chapter.

The black-box version is slower but real: an attacker who receives a numeric score per attempt can run a gradient-free search. Returning only accept/reject, and rate-limiting attempts per account, removes most of the signal that search needs. Design the interface, not only the model.

Replay, and the one thing physics gives you for free

Replay deserves separate treatment because it is the attack that actually happens at scale, and because it is the one where the defence rests on something an attacker cannot optimise away.

A replayed utterance has been through the recording chain twice: once when the attacker captured it, once when the microphone captured the loudspeaker. Each pass convolves the signal with a channel, and channels compose:

y = x ∗ hroom1 ∗ hmic1 ∗ hspeaker ∗ hroom2 ∗ hmic2

Loudspeakers are the weak link. A phone speaker rolls off sharply below about 300 Hz — it physically cannot move enough air to reproduce a 100 Hz fundamental — and adds measurable harmonic distortion when driven loudly. So a replayed recording carries a signature in the low band and in the harmonic structure that is a property of hardware, not of any generative model.

That is why replay detection generalises better than synthesis detection: the attacker would have to build a physically better loudspeaker, not train a better network. It is also why the ASVspoof challenges keep a separate physical-access track — the two problems have genuinely different structure, even though both are "someone is not who they claim".

A threat model you can actually plan against

AdversaryWhat they haveRealistic defenceResidual risk
OpportunistA recording from a voicemail or a videoRandom-prompt challenge-response; replay detectionLow — a pre-recorded clip cannot answer a new prompt
Funded attackerZero-shot TTS on a few seconds of target audioCountermeasure model plus liveness; second factor for high-value actionsModerate and rising — the countermeasure decays as generators improve
Real-time converterLive voice conversion, answering prompts as themselvesChallenge-response fails here. Needs channel-level and behavioural signals, plus a genuinely independent factorHigh — this is the case the field has not solved
White-box adversaryModel weights or a scoring oracleRate limiting, binary decisions only, adversarial training, model rotationModerate — largely an interface-design problem
Insider / breachThe template databaseOn-device matching, per-user secret transforms, encryption at restPermanent — a leaked voice cannot be reissued

Read the last row and then read Chapter 0's enrolment code again. template = normalize(np.mean(embs, axis=0)) writes an unchangeable biometric to a database. That line is thirty characters and it is the largest liability in the entire system.

Where this connects on this site

The generation side of this story is covered in its own lessons, and reading them next to this chapter is the point:

Why is "train the speaker verification model to reject synthetic speech" not a coherent fix?

Chapter 9: Connections and Cheat Sheet

Nine chapters ago we started with three teams in three buildings. Here is what the shared artifact underneath them turned out to be, what the field has done with it since 2020, and where it sits in the rest of this site.

The one-paragraph summary of each paper

x-vector — Snyder et al., 2018
Put speaker labels into the representation learner. Five TDNN layers, statistics pooling (mean and standard deviation over time), 512-dim embedding, heavy noise and reverberation augmentation, PLDA backend. Established the skeleton every later system uses.
GE2E — Wan et al., 2018
Train on the comparison, not on a proxy classification. Batches of N speakers × M utterances, scored against centroids with the current utterance excluded, learnable scale and bias. No classification head. Its encoder became the conditioner for zero-shot voice cloning.
ECAPA-TDNN — Desplanques et al., 2020
Four upgrades to the encoder — SE channel attention, Res2Net multi-scale blocks, multi-layer aggregation, channel-dependent attentive statistics pooling — on top of an angular-margin loss. 192-dim embedding, cosine scoring, no backend. Still the default.

The cheat sheet

QuantityValueWhere it came from
Sample rate / window / hop16 kHz / 25 ms / 10 msCh 1 — 100 frames per second
Frames from 4 s of audio398Ch 1 — 1 + ⌊(64000 − 400)/160⌋
Mel bins24 (x-vector) / 40 (GE2E) / 80 (ECAPA)Ch 1–4
x-vector receptive field15 frames = 165 msCh 2 — 5 + 4 + 6
ECAPA receptive field23 frames ≈ 245 ms, with Res2Net scales from 10 to 290 msCh 4 — 5 + 4 + 6 + 8
Statistics pooling output2× channels (mean & std concatenated)Ch 2 — 1500→3000, 1536→3072
Embedding dimension512 (x-vec) / 256 (GE2E) / 192 (ECAPA)Ch 1 table
GE2E batchN = 64 speakers × M = 10 utterancesCh 3
GE2E scale / bias initw = 10, b = −5, with w constrained positiveCh 3
AAM-softmax hyperparameterss = 30, m = 0.2 rad = 11.46°Ch 5
Res2Net receptive field, group i1 + (i − 1)·2d framesCh 4
Res2Net parameter saving9.1× versus a full 3-tap conv (86,016 vs 786,432)Ch 4
Training corpusVoxCeleb2 dev — 5,994 speakersCh 4–5
ECAPA C=1024 results0.87% / 1.12% / 2.12% EER on Vox1-O / E / HCh 4
Model size6.2M (C=512) / 14.7M (C=1024) parametersCh 4
EERThe rate where Pmiss = PfaCh 6 — hand-computed as 20% on ten trials
Normalised DCF(CmissPtarPmiss + Cfa(1−Ptar)Pfa) / min(CmissPtar, Cfa(1−Ptar))Ch 6 — 1.0 means "no better than always rejecting"
Diarization windows1.5 s length, 0.75 s shiftCh 7 — 799 windows from 600 s of speech
DER(miss + false alarm + confusion) / total speechCh 7 — 15.0% ignoring overlap, 23.0% scoring it

What changed after 2020

Line of workThe ideaWhy it matters
ResNet speaker encoders (r-vector)Treat the spectrogram as an image and use a 2-D ResNet-34 or ResNet-101 instead of a 1-D TDNNCompetitive with or better than ECAPA at higher compute. The two families have traded places on VoxCeleb leaderboards for years
Self-supervised front ends (WavLM, wav2vec 2.0)Replace hand-designed fbanks with representations pretrained on tens of thousands of hours of unlabelled speech, then attach an ECAPA headThe largest single accuracy jump since ECAPA, and the biggest gain is on hard and cross-domain conditions
TitaNet, ReDimNet and friends1-D depthwise-separable convolutions with squeeze-excitation; dimensionality reshaping between 1-D and 2-D viewsBetter accuracy-per-parameter, which matters for on-device deployment
Large-margin fine-tuningAfter normal training, fine-tune briefly on longer crops with a larger margin and a low learning rateA consistent free win of roughly 10–20% relative. Almost universal in competitive systems, almost never in the headline architecture description
End-to-end diarization (EEND, TS-VAD)Predict per-speaker activity directly, with permutation-invariant trainingThe only family that handles overlapped speech — at the cost of no longer producing a portable embedding (Ch 7)
Speaker-conditioned generationThe embedding as a control input to TTS and voice conversion, and increasingly replaced by raw acoustic promptsTurned the verification stack into the cloning stack, which is Chapter 8's whole problem
The structural observation across all six rows. Nothing in that table changes the skeleton. It is still frame network → pooling → embedding → angular-margin objective. Six years of progress went into better frame networks, better pretraining and better fine-tuning schedules — not into a different shape. The shape was settled by the x-vector paper in 2018 and has held, which is unusual and worth noticing. When a skeleton survives that long, it is usually because it encodes a real constraint of the problem — here, that speaker identity is a distributional property of frames and therefore something you pool for.

Where this sits in the rest of the site

Read nextWhy
CLAPThe same two-tower contrastive machinery on audio and text. Compare its symmetric cross-entropy over a batch with GE2E's centroid loss — both manufacture negatives from batch structure, and both learn a space you score with a dot product
Audio representationsThe front end of Chapter 1 in much more depth — windows, Mel warping, and what each transform destroys on purpose
Similarity metricsWhy cosine and not Euclidean, what normalisation buys, and how the choice of metric constrains the training objective
Contrastive learningThe general theory behind Chapter 3: in-batch negatives, temperature, collapse, and why leave-one-out targets keep appearing
Metric designChapter 6's argument generalised — how to choose an operating point when the two errors have different costs, in any detection system
Self-supervised speechWhere modern speaker encoders now get their front ends, and why a WavLM feature beats an 80-dim fbank
TTS architectures and VALL-EThe other side of Chapter 8 — how the vector becomes a voice
Voice turn-taking and Streaming speechDiarization's real-time cousins — who is talking, decided now, with a latency budget
Embedding securityInversion, linkage and leakage for embeddings in general. Voice is the case where "you cannot change it" bites hardest

A practical decision guide

If you are…Do this
Building verification todayTake a pretrained ECAPA-TDNN (SpeechBrain or WeSpeaker), score with cosine, add AS-Norm, and calibrate on your domain. Do not train from scratch unless your domain is genuinely unusual
Getting bad accuracy in production but good accuracy on VoxCelebCheck the duration distribution and the channel first. Almost always one of those two, and almost never the architecture
Choosing a thresholdNever use the EER threshold unless the two errors genuinely cost the same. Estimate your prior and your costs, compute DCF, and pick its minimum — then recalibrate quarterly
Doing diarizationStart with pyannote or a VBx-style pipeline. If your audio has real conversational overlap, budget for an overlap-aware stage from day one rather than treating it as a later refinement
Using voice for authenticationTreat it as one factor. Add a liveness or challenge-response layer, expect the artefact-based countermeasure to decay, and plan the incident response for a template leak before you store the first template
Training your ownCopy the augmentation recipe before you copy the architecture — MUSAN plus room impulse responses is worth more than any block you could design. Then add large-margin fine-tuning at the end

Build it yourself, in the order that will not waste your time

If you want to internalise this lesson by writing code rather than reading, here is the order that keeps every step verifiable against a number you already have.

StepBuildCheck it against
1The front end: framing, Mel filterbank, log, CMVNThe frame-count arithmetic — 4 s at 16 kHz with a 10 ms hop must give exactly 398 frames
2Statistics pooling, standaloneThe Chapter 2 toy: five frames of three channels must pool to (3, 1, 2 | 2, 0.6325, 1.7889), and reversing the frames must change nothing
3The x-vector TDNN stackParameter counts in the Chapter 2 table, and a receptive field of exactly 15 frames
4AAM-softmaxThe Chapter 5 worked example: 40° against 55° at s = 30 gives losses of 0.0031 and 0.2042
5The GE2E lossThe Chapter 3 batch: a row with cosines 0.6428 and 0.8660 at w = 10, b = −5 must give a loss of 2.334
6EER and minDCF scorersThe Chapter 6 ten-trial set: EER 20% at θ = 0.55, minDCF 0.60 at θ = 0.70 for Ptarget = 0.05
7Only now: the ECAPA blocksRes2Net group 8 at d = 2 must see 29 frames, and the block must have 9.1× fewer conv parameters than a full 3-tap
8The augmentation pipelineMixing at a target SNR: speech power 0.01, noise power 0.04, 13 dB gives a noise scale of 0.1119

The ordering is deliberate. Steps 1, 2, 4, 5 and 6 each have a closed-form check in this lesson, so a bug announces itself immediately. The architecture — the part that feels like the real work — is step 7, because a wrong ECAPA block trains to a slightly worse number and tells you nothing, whereas a wrong pooling layer fails a three-line assertion.

Ten numbers worth memorising

#NumberWhat it is
125 ms / 10 msWindow and hop. Three glottal periods, 100 frames per second
2398Frames in 4 seconds. The arithmetic every shape derives from
3Pooling's output width, because mean and standard deviation are both kept
4192ECAPA's embedding dimension. 768 bytes per person
515 vs 23 framesx-vector's receptive field versus ECAPA's — 165 ms against 245 ms
6s = 30, m = 0.2AAM-softmax. The margin is 11.46°, and it buys exactly that much clearance
75,994VoxCeleb2 dev speakers. Chance loss is ln 5994 = 8.699
80.87%ECAPA C=1024 on Vox1-O — and 2.12% on Vox1-H, which is the number to quote
91.5 s / 0.75 sDiarization window and shift. 799 windows from 600 s of speech
101.0The normalised DCF of a system that rejects everything. Anything above it is worse than useless

The sentence to keep

Pooling is where the speaker appears. Everything before it is signal processing that keeps the words. Everything after it is geometry that has already lost them. The order of the frames carries what was said; the distribution of the frames carries who said it — and a permutation-invariant average is the operation that separates the two, with no parameters and no training. Every architecture in this lesson is an argument about what to compute before that average, how to weight it, and what shape to force the result into. The average itself has never changed.

References

  1. Snyder, D., Garcia-Romero, D., Sell, G., Povey, D., Khudanpur, S. "X-Vectors: Robust DNN Embeddings for Speaker Recognition." ICASSP 2018. PDF
  2. Wan, L., Wang, Q., Papir, A., Moreno, I. L. "Generalized End-to-End Loss for Speaker Verification." ICASSP 2018. arXiv:1710.10467
  3. Desplanques, B., Thienpondt, J., Demuynck, K. "ECAPA-TDNN: Emphasized Channel Attention, Propagation and Aggregation in TDNN Based Speaker Verification." Interspeech 2020. arXiv:2005.07143
  4. Dehak, N., Kenny, P., Dehak, R., Dumouchel, P., Ouellet, P. "Front-End Factor Analysis for Speaker Verification." IEEE TASLP, 2011.
  5. Prince, S. J. D., Elder, J. H. "Probabilistic Linear Discriminant Analysis for Inferences About Identity." ICCV 2007.
  6. Variani, E., Lei, X., McDermott, E., Moreno, I. L., Gonzalez-Dominguez, J. "Deep Neural Networks for Small Footprint Text-Dependent Speaker Verification." ICASSP 2014.
  7. Okabe, K., Koshinaka, T., Shinoda, K. "Attentive Statistics Pooling for Deep Speaker Embedding." Interspeech 2018. arXiv:1803.10963
  8. Hu, J., Shen, L., Sun, G. "Squeeze-and-Excitation Networks." CVPR 2018. arXiv:1709.01507
  9. Gao, S., Cheng, M.-M., Zhao, K., Zhang, X.-Y., Yang, M.-H., Torr, P. "Res2Net: A New Multi-scale Backbone Architecture." IEEE TPAMI, 2021. arXiv:1904.01169
  10. Deng, J., Guo, J., Xue, N., Zafeiriou, S. "ArcFace: Additive Angular Margin Loss for Deep Face Recognition." CVPR 2019. arXiv:1801.07698
  11. Schroff, F., Kalenichenko, D., Philbin, J. "FaceNet: A Unified Embedding for Face Recognition and Clustering." CVPR 2015. arXiv:1503.03832
  12. Wang, H., Wang, Y., Zhou, Z., Ji, X., Gong, D., Zhou, J., Li, Z., Liu, W. "CosFace: Large Margin Cosine Loss for Deep Face Recognition." CVPR 2018. arXiv:1801.09414
  13. Nagrani, A., Chung, J. S., Zisserman, A. "VoxCeleb: A Large-Scale Speaker Identification Dataset." Interspeech 2017. arXiv:1706.08612
  14. Chung, J. S., Nagrani, A., Zisserman, A. "VoxCeleb2: Deep Speaker Recognition." Interspeech 2018. arXiv:1806.05622
  15. Snyder, D., Chen, G., Povey, D. "MUSAN: A Music, Speech, and Noise Corpus." 2015. arXiv:1510.08484
  16. Jia, Y., Zhang, Y., Weiss, R. J., Wang, Q., Shen, J., Ren, F., Chen, Z., Nguyen, P., Pang, R., Moreno, I. L., Wu, Y. "Transfer Learning from Speaker Verification to Multispeaker Text-To-Speech Synthesis." NeurIPS 2018. arXiv:1806.04558
  17. Wang, C., Chen, S., Wu, Y., et al. "Neural Codec Language Models are Zero-Shot Text to Speech Synthesizers" (VALL-E). 2023. arXiv:2301.02111
  18. Casanova, E., Weber, J., Shulby, C., Junior, A. C., Gölge, E., Ponti, M. A. "YourTTS: Towards Zero-Shot Multi-Speaker TTS and Zero-Shot Voice Conversion." ICML 2022. arXiv:2112.02418
  19. Fujita, Y., Kanda, N., Horiguchi, S., Xue, Y., Nagamatsu, K., Watanabe, S. "End-to-End Neural Speaker Diarization with Self-Attention." ASRU 2019. arXiv:1909.06247
  20. Medennikov, I., Korenevsky, M., Prisyach, T., et al. "Target-Speaker Voice Activity Detection: a Novel Approach for Multi-Speaker Diarization in a Dinner Party Scenario." Interspeech 2020. arXiv:2005.07272
  21. Landini, F., Profant, J., Diez, M., Burget, L. "Bayesian HMM Clustering of x-vector Sequences (VBx) in Speaker Diarization." Computer Speech & Language, 2022. arXiv:2012.14952
  22. Bredin, H., et al. "pyannote.audio: Neural Building Blocks for Speaker Diarization." ICASSP 2020. arXiv:1911.01255
  23. Todisco, M., Wang, X., Vestman, V., et al. "ASVspoof 2019: Future Horizons in Spoofed and Fake Audio Detection." Interspeech 2019. arXiv:1904.05441
  24. Kinnunen, T., Lee, K. A., Delgado, H., et al. "t-DCF: a Detection Cost Function for the Tandem Assessment of Spoofing Countermeasures and Automatic Speaker Verification." Odyssey 2018. arXiv:1804.09618
  25. Chen, S., Wang, C., Chen, Z., et al. "WavLM: Large-Scale Self-Supervised Pre-Training for Full Stack Speech Processing." IEEE JSTSP, 2022. arXiv:2110.13900
  26. Koluguri, N. R., Park, T., Ginsburg, B. "TitaNet: Neural Model for Speaker Representation with 1D Depth-wise Separable Convolutions and Global Context." ICASSP 2022. arXiv:2110.04410
  27. Ravanelli, M., Bengio, Y. "Speaker Recognition from Raw Waveform with SincNet." SLT 2018. arXiv:1808.00158
Six years of progress after ECAPA-TDNN left the skeleton — frame network, pooling, embedding, angular-margin objective — unchanged. What does that most likely indicate?