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

Bereken CTC-uitlijningen en gradiënten

Bereken stabiele voorwaartse en achterwaartse logmassa, posteriors en echte CTC-logitgradiënten.

100 min2 checkpoints
DIT GA JE ONTDEKKEN
  • Je somt uitlijningen met een begrensde dynamische tabel en correcte skipvoorwaarden.
  • Je gebruikt de juiste beta-conventie voor posteriors en gradiënt p min q.

Een reeks van 64 frames met maar twee mogelijke klassen heeft al meer klassepaden dan je zinvol kunt uitschrijven. Toch wil je de kans van één bekende transcriptie berekenen en daarop leren. Dat lukt door gedeelde tussenberekeningen één keer te bewaren.

Denk aan routes door een raster. Veel routes komen op hetzelfde punt uit en hebben vanaf daar dezelfde vervolgmogelijkheden. CTC telt hun massa op in een dynamische tabel, zonder iedere volledige route apart te onthouden.

We gebruiken weer blankkans 0,6 en A-kans 0,4 op twee frames, met doel A. Dezelfde berekening die eerder 0,64 gaf, wordt nu een voorbeeld voor de tabel. Later gebruiken we de tussenwaarden ook om echte trainingsgradiënten te bepalen.

1. Houd bij hoeveel van het doel is bereikt

Plaats blanks vóór, tussen en na de doellabels. Voor doel A wordt de uitgebreide toestandrij [blank,A,blank]. Voor U doellabels zijn er zo S=2U+1 toestanden; verschillende blankposities blijven afzonderlijke toestanden.

logits = np.log([[.6,.4],[.6,.4]])
forward = ctc_forward(logits,[1])
print(forward['expanded'].tolist())
# [0, 1, 0]

Je mag in dezelfde toestand blijven of één toestand vooruitgaan. Een sprong van twee mag alleen naar een niet-blank label dat verschilt van het label twee posities terug. Die laatste regel voorkomt dat een dubbele A zonder tussengelegen blank ongemerkt één A wordt.

De code bewaart logmassa's in alpha; noem zo'n waarde a op frame t en toestand s. I bevat de toegestane ouderwaarden van het vorige frame, en c_s is het klasselabel van deze toestand. Logsumexp, afgekort LSE, telt massa's op terwijl ze in logruimte blijven.

at,s=lnpt,cs+LSE(It,s)

Op het eerste frame mag alleen de eerste blank of het eerste doellabel beginnen. Andere cellen krijgen −∞: logaritme van nul massa, dus onbereikbaar. Dat is een geldige tabelwaarde en geen foutieve modelinvoer.

for row in np.exp(forward['alpha']):
    print(row.round(3).tolist())
# [0.6, 0.4, 0.0]
# [0.36, 0.4, 0.24]

Aan het einde mag het doel met of zonder laatste blank zijn afgerond. Daarom tel je de laatste twee toestandsmassa's op: 0,4+0,24=0,64. Een leeg doel heeft maar één toestand, waarvoor alleen die laatste cel telt.

Maak ctc_forward met de gegeven invoerhelpers en zelfstandige uitvoerarrays. Een gewone som van loggetallen zou producten in plaats van kanssommen opleveren. Gebruik daarom steeds logaddexp voor samenkomende ouders.

VERKEN HET ZELFTel uitlijningen samen zonder ieder pad uit te schrijven

Volg de toestanden van een gegeven transcriptie. Vergelijk de voorwaartse logmassa met de posterior: die tweede kijkt ook naar de rest van de audio.

Uitgebreid doel: _ A _. De positie s onderscheidt verschillende toestanden met hetzelfde label; _ staat voor blank.

Toestanden op frame 1; logmassa en posterior zijn verschillende grootheden
sLabellog αPosterior
0blank-0,5110,375
1A-0,9160,625
2blank−∞0
Klassen op frame 1: gradiënt = framekans − klasseposterior
KlasseKans pPosterior qp − q
blank0,60,3750,225
A0,40,625-0,225
Logkans gehele transcriptie
-0,446
Verlies −logkans
0,446

De eindkans gebruikt op het laatste frame toestanden s=1 en s=2. Blanks en eventuele herhalingen worden verwerkt in de toestanden, niet achteraf uit de som gegooid.

De transcriptie is hier gegeven om haar kans en trainingsgradiënt te berekenen. Dit is geen decoder voor de onbekende beste reeks. −∞ betekent een onbereikbare toestand; beta sluit de huidige emissie uit.

01
CHECKPOINTVul de voorwaartse logmassatabel

Bij 64 frames en zestien doellabels zijn er 33 toestanden, dus 2112 tabelcellen. De volgende berekening blijft daardoor klein, ook wanneer het aantal volledige klassepaden al onpraktisch groot is. Aangrenzende gelijke labels blijven hun verplichte tussenblank houden.

large = ctc_forward(
    np.zeros((64,2)),[1]*16)
print(large['alpha'].shape)
# (64, 33)

2. Verbind de doelskans aan een trainingssignaal

Noem de totale logdoelkans L. Het verlies voor één transcriptie is −L, de negatieve logkans of negative log-likelihood (NLL). Voor ons voorbeeld is dat ongeveer 0,446; het is geen gemiddelde per frame.

De achterwaartse tabel beta bewaart logmassa van alle toegestane vervolgen. Onze beta sluit de huidige emissie uit, terwijl alpha die juist bevat. Daardoor wordt de kans op toestand s, gegeven het doel, met de volgende formule berekend.

qt,s=exp(at,s+bt,s−L)

Tel toestanden met hetzelfde klasselabel bij elkaar op tot klasseposterior Q. Die verdeling vertelt welke klasse op ieder frame bij de bekende transcriptie past, over alle mogelijke uitlijningen heen. De gradiënt naar de logits is vervolgens de modelkans p min Q.

gt,k=pt,k−Qt,k

In ons voorbeeld loopt op het eerste frame 0,40 van de totale doelmassa 0,64 via A: dat geeft 0,625. Q is op beide frames [0,375;0,625], dus de logitgradiënt is [0,225;−0,225]. Een stap tegen deze gradiënt verlaagt de voorkeur voor blank en verhoogt die voor A.

backward = ctc_backward(logits,[1])
print(round(backward['loss'],3))
for row in backward['gradient']:
    print(row.round(3).tolist())
# 0.446
# [0.225, -0.225]
# [0.225, -0.225]

Maak ctc_backward met dezelfde skipregels, nu voor de opvolgers. De doeltranscriptie is nodig om het trainingssignaal te bepalen. Tijdens voorspellen gebruik je alleen de audio en het model; een posterior op basis van het juiste antwoord hoort daar niet bij.

02
CHECKPOINTBereken posteriors en echte CTC-gradiënten

Wat neem je mee?

  • De tabel berekent een gegeven doelskans met T×S cellen, zonder alle paden uit te schrijven.
  • Alpha en beta moeten precies afspreken welke emissie ze meetellen.
  • Het CTC-trainingssignaal leert uit gehele transcripties zonder vooraf gegeven framegrenzen.

Deze doelsom zoekt nog niet automatisch de beste transcriptie. Het bouwproject gebruikt de eerder geleerde greedy-decoder en meet daarna de echte reeksfouten.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-29.
DE VOLGENDE STAPTrain en toets een eigen CTC-framehoofd
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 / 2
01
Vul de voorwaartse logmassatabel

Schrijf ctc_forward(logits,labels,blank=0) zonder padenumeratie. Gebruik _ctc_setup voor log_probs, expanded=[blank,l1,blank,...] en het skipmasker. Initialiseer alpha[0,0] en zo nodig alpha[0,1] met de corresponderende logemissie; overige cellen -inf. Combineer ouders s/s-1 en alleen bij toegestane skip s-2 via logaddexp; voeg de huidige logemissie toe. Eindkans is de logsom van de laatste twee toestanden, of alleen toestand 0 bij leeg doel. Geef log_probability(float),alpha(T,S),expanded(S integer),log_probs(T,C),probabilities(T,C). Alle oorspronkelijke oefengrenzen blijven gelden; behoud invoer en geef zelfstandige arrays. Ongeldige invoer geeft ValueError.

Een hint

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

02
Bereken posteriors en echte CTC-gradiënten

Schrijf ctc_backward(logits,labels,blank=0). Gebruik ctc_forward; beta sluit de huidige emissie uit. Eindig met logmassa 0 in de laatste twee toestanden (één bij leeg doel), elders -inf. Loop terug met emissies op t+1 en toegestane opvolgers s/s+1/s+2. State_posterior=exp(alpha+beta-log_probability); tel alle toestanden met hetzelfde klasselabel op in class_posterior. Geef beta(T,S),state_posterior(T,S),class_posterior(T,C),loss=-log_probability(float),gradient=probabilities-class_posterior(T,C). -inf is geldig in alpha/beta, niet in logits; behoud de invoer en geef zelfstandige arrays. Gebruik de bestaande grenzen; 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

# Jouw functies

def ctc_forward(logits,labels,blank=0):
    log_probs,expanded,skip=_ctc_setup(logits,labels,blank);times=len(log_probs);states=len(expanded)
    alpha=np.full((times,states),-np.inf);alpha[0,0]=log_probs[0,blank]
    if states>1:alpha[0,1]=log_probs[0,expanded[1]]
    for time in range(1,times):
        previous=alpha[time-1];incoming=np.logaddexp(previous,np.concatenate([[-np.inf],previous[:-1]]))
        if states>2:
            jump=np.concatenate([[-np.inf,-np.inf],previous[:-2]])
            incoming=np.where(skip,np.logaddexp(incoming,jump),incoming)
        alpha[time]=incoming+log_probs[time,expanded]
    log_probability=float(alpha[-1,0] if states==1 else np.logaddexp(alpha[-1,-1],alpha[-1,-2]))
    return {'log_probability':log_probability,'alpha':alpha,'expanded':expanded,'log_probs':log_probs,'probabilities':np.exp(log_probs)}

def ctc_backward(logits,labels,blank=0):
    forward=ctc_forward(logits,labels,blank);log_probs=forward['log_probs'];expanded=forward['expanded'];times=len(log_probs);states=len(expanded)
    # Beta excludes this time's emission; alpha includes it. No division by a tiny current probability.
    beta=np.full((times,states),-np.inf);beta[-1,-1]=0.
    if states>1:beta[-1,-2]=0.
    skip=np.zeros(states,dtype=bool)
    if states>2:skip[:-2]=(expanded[2:]!=blank)&(expanded[2:]!=expanded[:-2])
    for time in range(times-2,-1,-1):
        outgoing=log_probs[time+1,expanded]+beta[time+1]
        value=np.logaddexp(outgoing,np.concatenate([outgoing[1:],[-np.inf]]))
        if states>2:
            jump=np.concatenate([outgoing[2:],[-np.inf,-np.inf]])
            value=np.where(skip,np.logaddexp(value,jump),value)
        beta[time]=value
    state_posterior=np.exp(forward['alpha']+beta-forward['log_probability']);class_posterior=np.zeros_like(log_probs)
    for state,label in enumerate(expanded):class_posterior[:,label]+=state_posterior[:,state]
    gradient=forward['probabilities']-class_posterior
    return {'beta':beta,'state_posterior':state_posterior,'class_posterior':class_posterior,
            'loss':-forward['log_probability'],'gradient':gradient}

# 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.