Machine Learning Academy · Lektion

Problemet med forsvindende gradienter over dybe tidstrin

De lærende observerer eksploderende og forsvindende gradienter i en dyb RNN gennem logning af gradientnormer og forstår, hvorfor lange sekvenser gør træningen ustabil.

Lektion 2 af 413 trin

Problemet med forsvindende gradienter over dybe tidstrin er en gratis Machine Learning Academy-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Machine Learning Academy, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Machine Learning Academy-kurset indeholder 4 lektioner i alt.

Gradienter skal bevæge sig gennem tiden

For at lære af afhængigheder over lange afstande i en sekvens skal gradienter fra losset ved det sidste tidssteg bevæge sig baglæns gennem hvert tidssteg for at opdatere de parametre, der behandlede de tidlige input. For en sekvens med længden T betyder det, at den samme vægtmatrix W_hh multipliceres med sig selv T gange under BPTT. Denne gentagne multiplikation er årsagen til både forsvindende gradienter (eksponentielt henfald) og eksploderende gradienter (eksponentiel vækst).

import torch

# Conceptual illustration of gradient travel through T steps
# Gradient = dL/dh_T * (W_hh)^T * ...

# If W_hh has spectral radius < 1:
W_small = torch.eye(4) * 0.9
print('W^10 max value:', (W_small @ W_small @ W_small @
      W_small @ W_small @ W_small @
      W_small @ W_small @ W_small @ W_small).abs().max().item())
# -> very small: gradient vanishes

# If W_hh has spectral radius > 1:
W_big = torch.eye(4) * 1.1
print('W^10 max value:', (W_big ** 10).abs().max().item())
# -> very large: gradient explodes

Forsvindende gradienter: Den matematiske årsag

Under BPTT involverer lossens gradient med hensyn til den skjulte tilstand ved tidssteg t produktet af Jacobimatricerne for h med hensyn til h ved hvert trin fra t til T. Jacobimatricen ved hvert trin indeholder diag(f'(h_t)) * W_hh, hvor f' er den afledte af aktiveringsfunktionen. For tanh er f' begrænset af 1, og typiske tilfældige vægte har en spektral radius på under 1 — derfor driver dette produkt af T matricer gradienterne eksponentielt hurtigt mod nul med T.

import torch

# Track gradient norm through BPTT
def simulate_bptt_gradient(T, weight_scale=0.9):
    W = torch.eye(8) * weight_scale
    grad = torch.ones(8)   # gradient at final timestep
    norms = [grad.norm().item()]
    for t in range(T):
        grad = W.T @ grad  # one BPTT step
        norms.append(grad.norm().item())
    return norms

norms = simulate_bptt_gradient(T=20)
print('Gradient norms over 20 steps:')
print([f'{n:.4f}' for n in norms[::5]])
# Decreases from 2.83 -> nearly 0 after 20 steps

Observation af forsvindende gradienter i praksis

Du kan observere forsvindende gradienter direkte ved at logge gradientnormen ved hvert tidssteg under BPTT. Registrer hooks til baglæns gennemløb på den skjulte tilstand ved hvert RNN-trin for at opfange gradienternes størrelser. I et almindeligt RNN med 50 trin vil gradienten ved tidssteg 1 typisk være 1e-6 eller mindre — reelt nul — hvilket betyder, at de første tokens i en sekvens næsten ikke påvirker modellens parametre. Modellen kan ikke lære, at subjektet i begyndelsen af en lang sætning bestemmer verbet i slutningen.

import torch
import torch.nn as nn

rnn = nn.RNN(4, 8, batch_first=True)
X = torch.randn(1, 30, 4, requires_grad=True)

output, h_n = rnn(X)
loss = output[:, -1, :].sum()   # loss at last timestep
loss.backward()

# Gradient with respect to early inputs
if X.grad is not None:
    per_step_grads = X.grad.abs().mean(dim=-1)
    print('Gradient norms per timestep (first 5 vs last 5):')
    print(per_step_grads[0, :5].tolist())    # early: tiny
    print(per_step_grads[0, -5:].tolist())   # late: larger

Eksploderende gradienter: Den anden yderlighed

Eksploderende gradienter opstår, når den spektrale radius for W_hh overstiger 1 — gradientnormerne vokser eksponentielt med sekvenslængden. Symptomet er NaN-værdier for loss eller parametre, der opdateres til uendelig. I modsætning til forsvindende gradienter (som medfører et ubemærket træningssvigt) får eksploderende gradienter træningen til at gå synligt ned. Den almindelige løsning er gradientbegrænsning: omberegn gradientvektoren, så den har en maksimal L2-norm på 1.0 før optimeringstrinnet. Det forhindrer katastrofale opdateringer uden at fjerne gradientsignalet.

import torch
import torch.nn as nn
import torch.optim as optim

rnn = nn.RNN(4, 8, batch_first=True)
optimizer = optim.SGD(rnn.parameters(), lr=0.1)

X = torch.randn(2, 50, 4)   # 50-step sequence
output, _ = rnn(X)
loss = output.sum()
loss.backward()

# Check gradient norm before clipping
total_norm = 0
for p in rnn.parameters():
    if p.grad is not None:
        total_norm += p.grad.data.norm(2) ** 2
total_norm = total_norm ** 0.5
print(f'Gradient norm before clip: {total_norm:.2f}')

# Clip to max_norm=1.0
nn.utils.clip_grad_norm_(rnn.parameters(), max_norm=1.0)
optimizer.step()

Visualisering af gradientnormer på tværs af lag

En praktisk teknik til fejlfinding er at logge gradientnormerne for alle parametre efter hvert baglæns gennemløb og plotte dem under træningen. For almindelige RNN'er viser den rekurrente vægtmatrix W_hh typisk meget mindre gradienter end inputvægten W_xh, hvilket bekræfter, at information over lange afstande ikke når frem til de tidligere parametre. Denne visualisering afslører ofte, at kun de sidste få tidssteg bidrager meningsfuldt til læringen, hvilket motiverer skiftet til arkitekturer med gating.

import torch
import torch.nn as nn

rnn = nn.RNN(4, 8, batch_first=True, num_layers=1)
X = torch.randn(1, 20, 4)
out, _ = rnn(X)
out.sum().backward()

print('Gradient norms per parameter:')
for name, p in rnn.named_parameters():
    if p.grad is not None:
        norm = p.grad.norm().item()
        print(f'  {name}: {norm:.6f}')
# weight_ih_l0 (input weights): larger
# weight_hh_l0 (recurrent weights): often much smaller

Hvorfor tanh forværrer forsvindende gradienter

Aktiveringsfunktionen tanh er begrænset til mellem -1 og 1 og har den afledte 1 - tanh^2(x). Når inputtet er stort (mættet), nærmer den afledte sig 0 — og reducerer gradienten til næsten nul ved det pågældende trin. Multiplikation af mange afledte tæt på nul under BPTT forstærker problemet med forsvindende gradienter. ReLU har den afledte 1 for positive input (ingen mætning), hvilket hjælper gradientflowet i fremadrettede netværk, men i RNN'er dominerer den gentagne multiplikation med W_hh stadig og kan forårsage eksplosioner med ReLU.

import torch

# Tanh derivative: 1 - tanh(x)^2
x = torch.linspace(-4, 4, 9)
tanh_x = torch.tanh(x)
tanh_deriv = 1 - tanh_x ** 2

print('x:         ', x.tolist())
print('tanh(x):   ', [f'{v:.2f}' for v in tanh_x.tolist()])
print('tanh_deriv:', [f'{v:.2f}' for v in tanh_deriv.tolist()])
# At x=+/-3: deriv ~0.01 -- 100x smaller than at x=0
# Multiplied over 20 steps: 0.01^20 = 10^-40!

Afkortet BPTT: En praktisk løsning

Afkortet BPTT begrænser gradientpropageringen til et fast vindue på K tidssteg i stedet for hele sekvensens længde. Gradienterne propagere​s K trin bagud, hvorefter den skjulte tilstand frakobles beregningsgrafen (og bliver en konstant). Det forhindrer hukommelsesproblemer og gradienteksplosioner for meget lange sekvenser (lyd og tekstkorpusser), men betyder, at afhængigheder, der strækker sig over mere end K trin, ikke læres. K=20-50 er typisk ved sprogmodellering med almindelige RNN'er.

import torch
import torch.nn as nn

rnn = nn.RNN(4, 8, batch_first=True)
batch_size = 4
h = torch.zeros(1, batch_size, 8)  # initial hidden state

# Process a 200-step sequence in chunks of 20
full_sequence = torch.randn(batch_size, 200, 4)

for chunk_start in range(0, 200, 20):
    chunk = full_sequence[:, chunk_start:chunk_start+20, :]
    out, h = rnn(chunk, h.detach())  # detach: stop grad here
    loss = out.sum()
    loss.backward()
    print(f'Chunk {chunk_start}-{chunk_start+20}: done')

Udfordringen ved afhængigheder over lange afstande

Overvej sætningen: 'The trophy that the man won was big.' Verbet 'was' skal stemme overens med 'trophy', ikke med 'man'. Det kræver, at oplysningerne om 'trophy' bevares over 5 ord, indtil 'was' forekommer. Et almindeligt RNN, der trænes med BPTT, kan grundlæggende ikke gøre dette pålideligt ved mellemrum på over 5-10 tokens. Dette er den centrale begrænsning, der førte til udviklingen af LSTM (1997) og senere Transformer-modeller (2017), som begge har eksplicitte mekanismer til at bevare information over lange afstande.

# Classic long-range dependency examples:
examples = [
    'The trophy ... man ... was [big/big] -- which subject?',
    'The cat ... [sat/sat] -- past vs present?',
    'The key [was/were] -- singular subject far away'
]

for ex in examples:
    print('Example:', ex)

# Vanilla RNN performance on long-range deps:
print('\nVanishing gradient effect on learning:')
for gap in [1, 5, 10, 20, 50]:
    ability = 'easy' if gap < 5 else ('hard' if gap < 20 else 'nearly impossible')
    print(f'  {gap}-step gap: {ability} for vanilla RNN')

Initialiseringstricks til RNN'er

Flere initialiseringstricks forbedrer træningen af almindelige RNN'er på sekvenser af moderat længde. Initialisering af W_hh som en ortogonal matrix (spektral radius præcis 1) forhindrer tidlig forsvinden eller eksplosion. En genvejstilslutning fra input direkte til output omgår flere matrixmultiplikationer. Initialisering med identitetsmatrix af W_hh sammen med ReLU-aktivering (IRNN) har vist sig at kunne matche LSTM på visse opgaver, hvilket beviser, at initialisering alene delvist kan afhjælpe problemet med forsvindende gradienter.

import torch
import torch.nn as nn

rnn = nn.RNN(4, 8, batch_first=True)

# Orthogonal init for hidden-to-hidden weights
nn.init.orthogonal_(rnn.weight_hh_l0)

# Identity init (IRNN) for W_hh
nn.init.eye_(rnn.weight_hh_l0)  # identity matrix

print('Spectral radius after orthogonal init:')
eigvals = torch.linalg.eigvals(rnn.weight_hh_l0)
print(eigvals.abs().max().item())  # should be ~1.0

Hvorfor LSTM blev opfundet

Problemet med forsvindende gradienter i RNN'er blev dokumenteret af Hochreiter i 1991. Hans løsning, netværket Long Short-Term Memory (LSTM), der blev introduceret i 1997, erstatter den enkelte skjulte tilstand med en celletilstand, der beskyttes af gates. Celletilstanden bevæger sig gennem tiden med kun additive ændringer (ikke multiplikative), hvilket skaber en gradientmotorvej, der gør det muligt for gradienter at bevæge sig baglæns i det uendelige uden at forsvinde. Denne ene arkitektoniske nyskabelse gjorde praktisk træning af sekvenser med afhængigheder på over 100 tidssteg mulig.

# The core difference between RNN and LSTM gradient flow:

# Vanilla RNN: h_t = tanh(W_hh * h_{t-1} + W_xh * x_t)
# Gradient must pass through tanh and W_hh MULTIPLICATIVELY
# -> vanishes after ~10 steps

# LSTM: c_t = f_t * c_{t-1} + i_t * g_t
# Cell state c_t is updated ADDITIVELY
# Forget gate f_t can be near 1 (keep everything)
# -> gradient flows back cleanly

print('LSTM key insight: additive cell state update')
print('Gradient highway: constant error carousel')
print('Forget gate f_t controls information retention')

Sammenligning af stabiliteten ved RNN- og LSTM-træning

Forskellen i træningsstabilitet mellem almindelige RNN'er og LSTM'er bliver markant for sekvenser, der er længere end 20-30 tidssteg. På den klassiske kopieringsopgave (gengiv inputsekvensen efter en lang forsinkelse) mislykkes almindelige RNN'er fuldstændigt ved forsinkelser på over 10 trin, mens LSTM'er lykkes ved forsinkelser på over 100 trin. Dette praktiske benchmark demonstrerer konkret, at problemet med forsvindende gradienter grundlæggende begrænser almindelige RNN'er, og at LSTM's arkitektoniske løsning er nødvendig til reel sekvensmodellering.

import torch
import torch.nn as nn

# Compare RNN vs LSTM on a 30-step sequence
models = {
    'RNN':  nn.RNN(4, 16, batch_first=True),
    'LSTM': nn.LSTM(4, 16, batch_first=True)
}

X = torch.randn(8, 30, 4)  # 30-step sequence

for name, model in models.items():
    out, _ = model(X)
    loss = out.sum()
    loss.backward()
    # Check gradient of first input vs last input
    total_grad_norm = sum(
        p.grad.norm().item() for p in model.parameters()
        if p.grad is not None
    )
    print(f'{name} total grad norm: {total_grad_norm:.4f}')

Hurtigt tjek

Test din forståelse af begreberne inden for Machine Learning med Python fra denne lektion.

Opsummering af lektionen

I denne lektion har du lært, at forsvindende gradienter opstår, når gentagen multiplikation med W_hh (med en spektral radius < 1) driver gradienterne eksponentielt mod nul i lange sekvenser, at eksploderende gradienter opstår, når den spektrale radius er > 1, og løses med gradientbegrænsning, samt at LSTM blev opfundet specifikt for at løse problemet med forsvindende gradienter gennem en additiv opdatering af celletilstanden, der skaber en gradientmotorvej. Dernæst undersøger vi LSTM-cellens arkitektur i detaljer.

Gratis at komme i gang

Lær Python med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
30
Lektioner
120

Ofte stillede spørgsmål

Er lektionen “Problemet med forsvindende gradienter over dybe tidstrin” gratis?

Ja — hele teksten til “Problemet med forsvindende gradienter over dybe tidstrin” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Machine Learning Academy-kurset, skal du opgradere til CoddyKit PRO. Machine Learning Academy-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Problemet med forsvindende gradienter over dybe tidstrin”?

De lærende observerer eksploderende og forsvindende gradienter i en dyb RNN gennem logning af gradientnormer og forstår, hvorfor lange sekvenser gør træningen ustabil. Du øver dig i Machine Learning Academy med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Machine Learning Academy?

Der kræves ingen tidligere erfaring. Machine Learning Academy på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.

Hvor lang tid tager lektionen “Problemet med forsvindende gradienter over dybe tidstrin”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Machine Learning Academy-lektion?

Ja. Alle Machine Learning Academy-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Vanilla-RNN'er: Hidden state og sekvensudrulning
  2. Problemet med forsvindende gradienter over dybe tidstrin
  3. LSTM-cellen: Input-, forget- og output gates
  4. Sekvens-til-én: Sentimentanalyse med en LSTM
← Tilbage til Machine Learning Academy