Write greedy and beam-search CTC decoders from scratch, including length normalisation. Use when you need help with skill ctc decoder.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add anubhavg-icpl/vibe --skill skill-ctc-decoder --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Skill Ctc Decoder?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-skill-ctc-decoder)More formats (shields.io, HTML) on the badges page.
---
name: skill-ctc-decoder
description: Write greedy and beam-search CTC decoders from scratch, including length normalisation. Use when you need help with skill ctc decoder.
license: CC-BY-NC-SA-4.0
phase: 4
lesson: 19
metadata:
version: 1.0.0
tags: [ocr, ctc, decoding, sequence-models]
---
# CTC Decoder
Produce two decoding routines for CTC outputs: greedy (fast) and beam (better on noisy inputs).
## When to use
- Running OCR inference on custom CRNN outputs.
- Benchmarking a pretrained OCR model against different decoders.
- Implementing a simple beam search without pulling in ctcdecode.
## Inputs
- `log_probs`: (T, N, C) log-softmax over vocab (index 0 = blank by convention).
- `vocab`: list of C characters.
- `beam_width` (beam only): typically 5-10.
## Greedy decoder
```python
def greedy_ctc_decode(log_probs, vocab, blank=0):
preds = log_probs.argmax(dim=-1).transpose(0, 1).cpu().tolist()
out = []
for seq in preds:
decoded = []
prev = None
for idx in seq:
if idx != prev and idx != blank:
decoded.append(vocab[idx])
prev = idx
out.append("".join(decoded))
return out
```
## Beam search decoder
```python
import heapq
import math
def beam_ctc_decode(log_probs, vocab, beam_width=5, blank=0):
T, N, C = log_probs.shape
lp = log_probs.cpu()
results = []
for n in range(N):
beams = {("",): (0.0, -math.inf)} # (prefix_tuple) -> (p_blank, p_nonblank)
for t in range(T):
logits_t = lp[t, n]
new_beams = {}
for prefix, (p_b, p_nb) in beams.items():
for c in range(C):
p = logits_t[c].item()
if c == blank:
nb = p_b + p
nnb = p_nb + p
upd = new_beams.get(prefix, (-math.inf, -math.inf))
new_beams[prefix] = (
_logsumexp(upd[0], _logsumexp(nb, nnb)),
upd[1],
)
else:
last = prefix[-1] if prefix else ""
char = vocab[c]
if char == last:
# Case 1: stay on same prefix (collapse from p_nb)
upd = new_beams.get(prefix, (-math.inf, -math.inf))
new_beams[prefix] = (upd[0], _logsumexp(upd[1], p_nb + p))
# Case 2: extend prefix via blank-separated repeat ("a_a" -> "aa")
new_prefix = prefix + (char,)
upd = new_beams.get(new_prefix, (-math.inf, -math.inf))
new_beams[new_prefix] = (upd[0], _logsumexp(upd[1], p_b + p))
else:
new_prefix = prefix + (char,)
upd = new_beams.get(new_prefix, (-math.inf, -math.inf))
nb = _logsumexp(p_b, p_nb) + p
new_beams[new_prefix] = (upd[0], _logsumexp(upd[1], nb))
beams = dict(heapq.nlargest(
beam_width,
new_beams.items(),
key=lambda kv: _logsumexp(kv[1][0], kv[1][1]),
))
best = max(beams.items(), key=lambda kv: _logsumexp(kv[1][0], kv[1][1]))[0]
results.append("".join(best))
return results
def _logsumexp(a, b):
if a == -math.inf: return b
if b == -math.inf: return a
m = max(a, b)
return m + math.log(math.exp(a - m) + math.exp(b - m))
```
## Rules
- The blank index in CTC is 0 by convention in PyTorch's `nn.CTCLoss`.
- Beam search improves accuracy on low-confidence inputs; on clean inputs the improvement is <1% CER.
- Never prune the beam below 5; the accuracy-latency trade flattens below that.
- When running beam search inside a tight latency budget, drop to greedy; the quality hit is small on most production OCR data.
- For large vocabularies (CJK with 3000+ characters), switch to `ctcdecode` (C++) instead of the pure Python version above; the Python beam quickly becomes the bottleneck.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!