Module 27 · Part VII · Beyond Text

Recognition: OCR and Speech With CTC

PP-OCR and SVTR, the Alignment Problem, the CTC Objective and Its Recursion, Speech Recognition, and Error Rates

By the end you can train a sequence recognizer without alignments, and compute CTC by hand.

Builds onModule 4·Module 6·
Check yourself ▸

Before you start, can you already…

  • ☐train a tiny MLP end to end, diagnose overfitting, and read an experiment's result critically. (Module 4)
  • ☐compute attention by hand, prove why scores are divided by √d_k, and explain what multiple heads add. (Module 6)

If any is shaky, the linked module teaches it.

Open in Colab
3 Hours·Elective·Specialized·0/5

Reading text from images and transcribing speech share a shape: a long sequence of frames (pixel columns or audio slices) in, a shorter sequence of characters or words out, and no labels saying which frames belong to which character. This module covers a production OCR system built on a vision transformer, the loss that trains without alignments, the same idea in speech, and how recognizers are scored.

27.1 PP-OCR and SVTR

PP-OCR is Baidu's open-source optical character recognition (OCR) system, widely used on servers and small devices. A detector finds each text line, and a recognizer reads one cropped line at a time. The recognizer, SVTR ("Single Visual model for scene Text Recognition", Du et al., 2022), is a vision transformer alone, with no language model. Like ViT (section 25.2) it attends over patches, but a text line is short and very wide and must become a left-to-right sequence, so SVTR shrinks the height step by step to a single row of vectors.

Architecture Flowchart
Diagram: Cropped text line image, H x W x 3 (e.g. 32 x 320 x 3); Patch embedding: each vector covers 4 x 4 pixels -> 8 x 80 grid; Stage 1: local and global mixing blocks (8 x 80); Patch merging: halve the height, keep the width (4 x 80); Stage 2: local and global mixing blocks (4 x 80); Patch merging: halve the height again (2 x 80); Stage 3: mixing blocks (2 x 80); Combining: pool the height to 1 -> a sequence of 80 vectors; Linear layer: one character distribution per position, trained with CTC; Recognized text: 'Attention Is All You Need'

In words. A 32 by 320 crop becomes an 8 by 80 patch grid; two merges halve the height, and pooling squeezes it to one row of 80 vectors. A linear layer gives each a character distribution, and CTC (section 27.2) turns the 80 guesses into a transcript.

Figure 27.1
SVTR squeezes the height, keeps the width

The shape of the tensor at each stage of SVTR’s recognizer for a 32 × 320 text-line crop.

Each drawn cell is one patch vector (the input crop is drawn at patch resolution for comparison). The final 80 vectors each give one character distribution for CTC.
Predict, then try

A 32 × 320 crop becomes an 8 × 80 grid of patches. Before stepping through the stages, predict how many output positions are left at the end, and why the width is never reduced.

In words. The tensor's shape through SVTR, each stage drawn as a labelled grid: crop, eight-by-eighty patches, two height-halving merges, and a final row of eighty vectors.

Key Ideas in SVTR

  1. Local and global mixing. Characters are small strokes (a bar, a loop, a dot), but words need context along the line. Local mixing attends within a small window to pick up strokes; global mixing attends across the line to relate characters.
  2. Shrinking the height, not the width. After the patch embedding, SVTR never reduces the width of its 1:101:10 input, so neighboring letters stay in separate positions: 80 for a 320-pixel line, plenty for one line's characters.

27.2 The Alignment Problem and the BlankRun this section

The model produces TT outputs, one per frame (an image column position or an audio slice), for a target of U<TU < T labels: say T=80T = 80 frames for U=15U = 15 characters. Nobody labels "the letter a spans frames 12 to 15".

Connectionist Temporal Classification (CTC) (Graves et al., 2006) adds one output symbol, the blank ("no character here"), defines a rule turning frame-by-frame outputs into a transcript, and trains the model to make the correct transcript likely, summed over every frame-by-frame output that produces it.

The Collapsing Operator B\mathcal{B}

An alignment (or path) π=(π1,…,πT)\pi = (\pi_1, \dots, \pi_T) has one character or blank per frame. The collapsing operator B\mathcal{B} turns it into a label sequence in two steps, in order:

  1. Merge runs of the same symbol into one: .
  2. Delete every blank: (a,ϵ,b)→(a,b)(a, \epsilon, b) \to (a, b).

For example, both (c,c,ϵ,a,t)(c, c, \epsilon, a, t) and (ϵ,c,ϵ,a,a,t)(\epsilon, c, \epsilon, a, a, t) collapse to "cat".

The order makes double letters possible: (c,c,a,t)→(c, c, a, t) \to "cat" but (c,ϵ,c,a,t)→(c, \epsilon, c, a, t) \to "ccat". Adjacent identical symbols always merge, so a double letter needs a blank between, and without blanks "book" could never be written. Hence a minimum length: one frame per label plus one per double letter, so "cat" needs T≥3T \ge 3 and "book" T≥5T \ge 5.

Architecture Flowchart
Diagram: Alignment 1: [c, c, ε, a, t]; Collapsing operator B; Alignment 2: [ε, c, a, a, t]; Alignment 3: [c, ε, a, ε, t]; Target word: 'cat'

In words. Three alignments collapse to the same word; the loss adds up the probability of every alignment that does.

27.3 The CTC Objective and Its RecursionRun this section

With ykty_k^t the probability of symbol kk (blank included) at frame tt, and frames independent given the input x\mathbf{x}, an alignment's probability is the product over frames, and the transcript's is the sum over alignments that collapse to it:

P(l∣x)=∑π∈B−1(l)P(π∣x)=∑π∈B−1(l)∏t=1TyπttP(\mathbf{l} \mid \mathbf{x}) = \sum_{\pi \in \mathcal{B}^{-1}(\mathbf{l})} P(\pi \mid \mathbf{x}) = \sum_{\pi \in \mathcal{B}^{-1}(\mathbf{l})} \prod_{t=1}^T y_{\pi_t}^t

B−1(l)\mathcal{B}^{-1}(\mathbf{l}) is the set of alignments that collapse to l\mathbf{l}. The loss is the negative log-likelihood (section 3.4):

LCTC=−ln⁡P(l∣x)\mathcal{L}_{\text{CTC}} = -\ln P(\mathbf{l} \mid \mathbf{x})

There are KTK^T paths for KK symbols, so CTC computes the sum by dynamic programming, filling a table of partial sums so each shared piece is computed once: the forward-backward algorithm. The forward half:

  1. Put a blank before, between and after the labels. For "cat" this gives ϵ c ϵ a ϵ t ϵ\epsilon\, c\, \epsilon\, a\, \epsilon\, t\, \epsilon, which has 2∣l∣+1=72|\mathbf{l}| + 1 = 7 positions.
  2. Let be the total probability of all partial alignments that cover frames 1 to tt and end at position ss of that extended sequence.
  3. To get αt(s)\alpha_t(s), add up the ways to arrive: stay at ss, step from s−1s-1, or jump from s−2s-2 over a blank. The jump is allowed only when the labels on either side of that blank differ, which is the double-letter rule again. Multiply the sum by yty^t for the symbol at ss.
  4. At the end, add αT\alpha_T at the last two positions (the final label, and the final blank).
Figure 27.3
Filling in the CTC table, one frame at a time

Rows: the word with a blank before, between and after its letters. Columns: frames. Each cell adds what flows in from the same row, the row above, or two rows above (skipping a blank between different letters), times this frame’s probability for that row’s symbol.

Per-frame probabilities are invented but sum to 1 over the symbols ε, c, a, t, o, k, b. The probability of the word is the sum of the last two cells of the last column.
Predict, then try

Fill in the table for "cat" over 5 frames. Before stepping, predict which cells can never be reached, whatever the probabilities.

In words. The CTC table for a short word, rows the word with blanks inserted and columns the frames. Each step fills a column from the same row, the row above, or two above when the skip is allowed, times the frame's probability for that symbol. The last two cells of the last column sum to the word's probability.

The table costs instead of exponential. The backward half runs the same recursion from the end, and together they give the gradient.

Measured (lab 7, experiments/ctc_alignment.py). Two checks. First, for every TT from 3 to 8, listing every path by brute force and adding up the ones that collapse to "cat" gives exactly the same probability as the recursion, to the last bit. Second, counting those paths separately shows how fast they multiply: 1,7,84,462,1716,50051, 7, 84, 462, 1716, 5005 of them at T=3,4,6,8,10,12T = 3, 4, 6, 8, 10, 12, out of 4T4^T possible paths in total. At T=100T = 100 with ∣l∣=20|\mathbf{l}| = 20, brute force would list 10134.210^{134.2} paths, while the recursion fills in 4,1004{,}100 table entries.

27.4 The Same Idea in Speech

Speech is cut into overlapping frames, typically 25 milliseconds every 10, each a vector (often a mel-spectrogram slice, section 28.1). A minute of speech is 6,000 frames and perhaps 150 words, with no word boundaries labelled.

CTC was used for speech first. A CTC acoustic model is an encoder, now usually a transformer or a convolution-augmented Conformer, with a linear layer giving a distribution over characters or subword tokens, plus blank, per frame, trained with section 27.3's loss. With frames independent given the input, it decodes fast and can stream, emitting text while the speaker talks.

That independence is also its weakness: it cannot use its own output so far, so it has no built-in language model. Transducers (RNN-T) add a small prediction network over the output so far; encoder-decoder models (section 8.3) such as Whisper attend over the whole audio and write autoregressively, more accurate but hard to stream. Many production systems train a CTC loss alongside an attention decoder to get both.

27.5 Measuring Recognition: Error RatesRun this section

The edit distance is the fewest single-symbol substitutions SS, deletions DD and insertions II turning the output into the reference transcript. Divided by the reference length NN:

ER=S+D+IN\text{ER} = \frac{S + D + I}{N}

Over characters it is the character error rate (CER), usual for OCR; over words, the word error rate (WER), usual for speech. One wrong letter is one character error but a whole word error, so WER is typically several times CER, and insertions let either exceed 100%.

Both texts are first normalized for case, punctuation, spacing and number formats, so "3" for "three" is not penalized unless it matters. As with benchmark prompts (section 13.2), the rules change the score, so error rates are comparable only under the same rules.

Browser lab

Character and word error rates

Edit distance by dynamic programming, the same table-filling idea as CTC’s recursion, computed over characters and over words. Section 27.5 predicts that the word error rate is several times the character error rate on the same output, and that normalization changes both.

Exercises

Check

What does the CTC blank symbol mean, and why is it needed even for a word with no repeated letters?

Apply

Collapse the alignment (ϵ,b,b,ϵ,o,o,ϵ,o,k)(\epsilon, b, b, \epsilon, o, o, \epsilon, o, k) with the CTC rule.

Apply

What is the minimum number of frames needed to output "letter"?

Apply

A reference has 10 words. The output substitutes one word, drops one, and adds one extra word. What is the WER?

Extend

Why can a CTC model stream its transcript while the speaker is still talking, while an encoder-decoder model like Whisper usually cannot?

Core Academic References

  • Du et al. (2022): SVTR: Scene Text Recognition with a Single Visual Model. IJCAI 2022. arXiv:2205.00159
  • Du et al. (2020): PP-OCR: A Practical Ultra Lightweight OCR System. arXiv:2009.09941
  • Graves et al. (2006): Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks. ICML 2006.
  • Graves (2012): Sequence Transduction with Recurrent Neural Networks (RNN-T). arXiv:1211.3711
  • Gulati et al. (2020): Conformer: Convolution-augmented Transformer for Speech Recognition. Interspeech 2020. arXiv:2005.08100
  • Radford et al., OpenAI (2023): Robust Speech Recognition via Large-Scale Weak Supervision (Whisper). ICML 2023. arXiv:2212.04356
  • Levenshtein (1966): Binary Codes Capable of Correcting Deletions, Insertions, and Reversals. Soviet Physics Doklady, 10(8): 707–710.

Measured labs

These labs test what this module says by running real code. Each card states the idea, what the experiment found, and what it does not prove. The numbers come from experiments/results.json, the output of running experiments/run_all.sh on the tiny-transformers reference code at version 137f7ef. The project checks fail if a measured number in the module text stops matching that file.

Lab 7 — CTC, and why it needs a blank token

What the module says

Section 27.2 defines CTC's collapse rule B\mathcal{B}: merge repeated labels, then delete blanks ε, so (c,c,ϵ,a,t)(c, c, \epsilon, a, t) becomes "cat". It says a dynamic-programming recursion adds up the probability of every alignment in about T⋅∣l∣T\cdot|\mathbf{l}| steps (TT frames, ∣l∣|\mathbf{l}| labels) instead of listing exponentially many alignments one by one.

What the experiment found

Listing every alignment by brute force and running the recursion give exactly the same probability for T=3T = 3 to 88 frames. As section 27.2 explains, the blank also separates repeats: (c,ϵ,c,a,t)(c, \epsilon, c, a, t) collapses to "ccat", not "cat". Without a blank between them, two identical labels in a row would always merge, so a word with a double letter could never be produced. At T=100T = 100 frames and 20 labels, brute force would need 10^134.2 paths; the recursion uses 4,100 states.

(c, c, ε, a, t) collapses to
cat
(c, ε, c, a, t) collapses to
ccat

the blank keeps the two c's apart

possible alignments at T=12T = 12 frames
16,777,200

of which 5,005 collapse to "cat"

brute force = recursion at T=8T = 8
0.00704956 = 0.00704956

probability of "cat"

T=100T = 100 frames, 20 labels
4,100 states vs 10^134.2 paths

What this does not prove

Brute force only works on short sequences; that is why the check stops at 8 frames, and it is the point of the comparison. Residual vector quantization (section 28.3) is not measured, because nothing in the reference repository implements it.

Run it yourself

bash
cd tiny-transformers && .venv/bin/python experiments/ctc_alignment.py

The code being tested