Werkplaats / De werkplaats
In jouw eigen tempo
← Multimodaal & generatief
MODULE 03 · LES 11

Som transcriptiekansen over CTC-paden

Onderscheid framepaden en transcripties en zie waarom greedy niet altijd de hoogste reekskans vindt.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je voegt opeenvolgende labels samen vóór je blanks verwijdert.
  • Je somt begrensde uitlijningskansen en onderscheidt het beste pad van de waarschijnlijkste transcriptie.

Een lang uitgesproken klank levert meer audioframes op dan een korte. Toch wil je voor beide dezelfde letter of hetzelfde klanklabel terugkrijgen. Een herkenner heeft daarom een regel nodig om een lange rij framekeuzes om te zetten naar een kortere transcriptie.

Connectionist temporal classification, kortweg CTC, voegt daarvoor een extra klasse toe: blank. Blank betekent dat er op dat frame geen nieuw label wordt uitgestuurd. In onze voorbeelden is 0 blank, 1 het oefenlabel A en 2 het oefenlabel B.

De modeluitvoer blijft per frame een kansverdeling over alle klassen. Eerst begrijpen we hoe één gekozen klassepad een labelreeks wordt. Daarna tellen we de kansen op van verschillende paden die dezelfde transcriptie opleveren.

1. Herhalingen samenvoegen vraagt een vaste volgorde

Denk aan een aanwijzer die meerdere frames op A blijft staan. Dat is één aanhoudend label. Pas wanneer hij via blank terug naar A gaat, moet er een tweede A in de transcriptie verschijnen.

Voeg daarom eerst opeenvolgende gelijke padlabels samen en verwijder daarna de blanks. Bij het volgende pad blijven twee A's behouden, doordat er een blank tussen staat. De laatste twee B's horen bij één aanhoudend label.

path = [1,1,0,1,2,2]
print(ctc_collapse(path))
# [1, 1, 2]

Blanks vooraf verwijderen zou de twee A-groepen ten onrechte aan elkaar plakken. Je ziet de volgorde terug in dit kleine verschil: drie opeenvolgende A-frames geven één A, maar A-blank-A geeft twee A's.

print(ctc_collapse([1,1,1]))
print(ctc_collapse([1,0,1]))
# [1]
# [1, 1]

Maak ctc_collapse zonder de oorspronkelijke lijst of array te wijzigen. Een volledig blank pad levert een lege transcriptie. Blank is ook geen gewone spatie: een echte spatie zou, wanneer je die wilt herkennen, een apart betekenisvol label zijn.

01
CHECKPOINTVoeg herhalingen samen vóór blankverwijdering

2. Het waarschijnlijkste pad hoeft niet de beste reeks te geven

Greedy-decoding kiest op ieder frame de hoogste klassescore en past daarna collapse toe. Omdat softmax de volgorde van de logits bewaart, mag argmax rechtstreeks op de logits. Bij gelijke maxima kiezen we de eerste klasse.

Neem twee frames die beide blankkans 0,6 en A-kans 0,4 hebben. We kiezen logits als logaritmen van die kansen; softmax levert dan dezelfde verdeling terug. Greedy kiest tweemaal blank en geeft dus geen label.

logits = np.log([[.6,.4],[.6,.4]])
print(ctc_greedy(logits))
# []

Maar één transcriptie kan langs meerdere paden ontstaan. De vier mogelijkheden zijn hier volledig met de hand uit te schrijven. Pad AA wordt na samenvoegen één A; het stelt in dit voorbeeld dus geen dubbele A voor.

| Pad | Na collapse | Padkans | |---|---|---:| | blank, blank | leeg | 0,36 | | blank, A | A | 0,24 | | A, blank | A | 0,24 | | A, A | A | 0,16 |

De drie A-paden hebben samen kans 0,64. Dat is groter dan de kans 0,36 van de lege reeks. Greedy is snel, maar dit voorbeeld bewijst dat hij niet altijd de waarschijnlijkste transcriptie vindt.

VERKEN HET ZELFEén beste pad is niet altijd de beste transcriptie

Ieder frame kiest blank of A. Bekijk alle paden, voeg eerst herhalingen samen en verwijder daarna blanks. Tel pas dan de kansen per transcriptie op.

Alle paden; het waarschijnlijkste pad is gemarkeerd
PadNa collapsePadkans
0 0leeg0,36
0 1A0,24
1 0A0,24
1 1A0,16
De som van alle paden die dezelfde transcriptie geven
TranscriptieGesommeerde kans
leeg0,36
A0,64
Greedy-transcriptie
leeg
Hoogste reekskans
A

Hier verschillen de uitkomsten: greedy kiest het beste afzonderlijke pad, maar meerdere andere paden steunen samen een waarschijnlijkere transcriptie. Voor AA zijn minstens drie frames nodig: A, blank, A.

0 betekent blank, 1 betekent A. Blank is geen spatie. De framekansen zijn hier op ieder tijdstip gelijk; de padkans is het product van de gekozen framekansen.

02
CHECKPOINTDecodeer het beste klassepad

3. Som kansen van uitlijningen, geen logits

Een klassepad π kiest op ieder tijdstip t één klasse. Als p de modelkansen zijn, is zijn padkans het product van de gekozen framekansen. CTC gebruikt deze factorisatie gegeven de modelinvoer x.

P(π|x)=∏t=1Tpt,πt

Voor de kans van doelreeks l tel je alle passende padkansen op. Het pijltje onder de som betekent dat het pad na collapse precies l moet opleveren. Een hoog scorend maar verkeerd pad telt dus niet mee.

P(l|x)=∑π↦lP(π|x)

Maak ctc_enumerated_log_probability met de gegeven stabiele logkansen. Som passende paden met logaddexp, zodat kleine producten niet voortijdig verdwijnen. Exp van de geretourneerde logkans maakt ons handantwoord weer zichtbaar.

log_p = ctc_enumerated_log_probability(
    logits,[1])
print(round(float(np.exp(log_p)),2))
# 0.64

Begrens enumeratie tot 4096 paden: het aantal groeit als C tot de macht T. Een dubbele A heeft minstens drie frames nodig, omdat een blank de twee A's moet scheiden. De validator weigert een onmogelijk kort doel voordat je paden doorloopt.

03
CHECKPOINTSom alle toegestane uitlijningen

Wat neem je mee?

  • De volgorde van samenvoegen en blankverwijdering bepaalt het antwoord.
  • Een transcriptiekans telt meerdere mogelijke uitlijningen bij elkaar op.
  • Greedy is een benadering; volledige enumeratie groeit te snel voor lange audio.

Hierna bereken je dezelfde doelskans met een compacte dynamische tabel.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPBereken CTC-uitlijningen en gradiënten
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Voeg herhalingen samen vóór blankverwijdering

Schrijf ctc_collapse(path,blank=0). Voeg eerst opeenvolgende gelijke padlabels samen en verwijder daarna blanks. Geef een nieuwe lijst Python-intlabels. Pad: gehele 1D-array/lijst van lengte 0–64 met IDs 0–7; lege vector mag. Blank is Python-int 0–7, geen bool. Gebruik de gegeven validator, behoud de invoer en geef bij ongeldige invoer ValueError.

Een hint

Werk eerst het handvoorbeeld uit; controleer daarna de vormen, grenzen en het behoud van de invoer.

02
Decodeer het beste klassepad

Schrijf ctc_greedy(logits,blank=0). Neem per frame argmax, eerste maximum bij ties; gebruik ctc_collapse met de gegeven blank. Geef een nieuwe labellijst. Logits voldoen aan de gegeven validator: reële eindige T×C-matrix, T 1–64, C 2–8, waarden [-100,100]. Blank is een geldige Python-intklasse, geen bool. Behoud de invoer; ongeldige invoer geeft ValueError. Dit benadert de beste labelreeks en garandeert niet de hoogste gesommeerde reekskans.

Een hint

Werk eerst het handvoorbeeld uit; controleer daarna de vormen, grenzen en het behoud van de invoer.

03
Som alle toegestane uitlijningen

Schrijf ctc_enumerated_log_probability(logits,labels,blank=0). Gebruik de gegeven stabiele log-softmax/labelvalidatie uit _ctc_setup. Enumereer alle C**T klassepaden, kies paden die na ctc_collapse exact labels geven, en som hun logkansen met logaddexp. Geef een Python-floatlogkans. Weiger C**T>4096 vóór enumeratie. Doellabels hebben lengte 0–16, zijn gehele geldige klassen zonder blank; T moet ten minste U plus het aantal aangrenzende herhalingen zijn. Behoud invoer; ongeldige invoer geeft ValueError.

Een hint

Werk eerst het handvoorbeeld uit; controleer daarna de vormen, grenzen en het behoud van de invoer.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

import itertools

# Jouw functies

def ctc_collapse(path,blank=0):
    path=_ctc_ids(path,64);blank=_ctc_int(blank,0,7);result=[];previous=None
    for value in path:
        label=int(value)
        if label!=blank and label!=previous:result.append(label)
        previous=label
    return result

def ctc_greedy(logits,blank=0):
    logits=_ctc_logits(logits);blank=_ctc_int(blank,0,logits.shape[1]-1)
    return ctc_collapse(np.argmax(logits,axis=1),blank)

def ctc_enumerated_log_probability(logits,labels,blank=0):
    log_probs,expanded,_=_ctc_setup(logits,labels,blank);target=expanded[1::2].tolist();times,classes=log_probs.shape
    if classes**times>4096:raise ValueError('Enumeratie is beperkt tot 4096 paden')
    total=-np.inf
    for path in itertools.product(range(classes),repeat=times):
        if ctc_collapse(path,blank)==target:
            total=float(np.logaddexp(total,sum(float(log_probs[t,label]) for t,label in enumerate(path))))
    return float(total)

# Gegeven helpers — eerdere methoden en invoergrenzen

def _ctc_int(value,minimum,maximum):
    if type(value) is not int or not minimum<=value<=maximum:raise ValueError('Geheel getal buiten de oefengrens')
    return value

def _ctc_logits(value):
    raw=np.asarray(value)
    if raw.ndim!=2 or raw.dtype.kind not in 'iuf' or not 1<=raw.shape[0]<=64 or not 2<=raw.shape[1]<=8:
        raise ValueError('Een matrix met 1–64 tijden en 2–8 klassen vereist')
    result=raw.astype(float,copy=True)
    if not np.isfinite(result).all() or np.max(np.abs(result))>100:raise ValueError('Eindige logits binnen [-100,100] vereist')
    return result

def _ctc_ids(value,maximum_length):
    raw=np.asarray(value)
    if raw.ndim!=1 or len(raw)>maximum_length or (raw.size and raw.dtype.kind not in 'iu'):
        raise ValueError('Een begrensde vector met gehele labelindices vereist')
    if raw.size and (np.any(raw<0) or np.any(raw>7)):raise ValueError('Labelindex buiten [0,7]')
    return raw.astype(int,copy=True)

def _ctc_setup(logits,labels,blank):
    logits=_ctc_logits(logits);labels=_ctc_ids(labels,16);blank=_ctc_int(blank,0,logits.shape[1]-1)
    if np.any(labels>=logits.shape[1]) or np.any(labels==blank):raise ValueError('Doellabels zijn geldige klassen zonder blank')
    minimum=len(labels)+int(np.sum(labels[1:]==labels[:-1]))
    if len(logits)<minimum:raise ValueError('Te weinig tijdstappen voor deze doelreeks')
    shifted=logits-logits.max(axis=1,keepdims=True);log_probs=shifted-np.log(np.exp(shifted).sum(axis=1,keepdims=True))
    expanded=np.full(2*len(labels)+1,blank,dtype=int);expanded[1::2]=labels
    skip=np.zeros(len(expanded),dtype=bool)
    if len(expanded)>2:skip[2:]=(expanded[2:]!=blank)&(expanded[2:]!=expanded[:-2])
    return log_probs,expanded,skip

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.