Machine Learning Academy · Lektion

Problemet med försvinnande gradienter över djupa tidssteg

Ni kommer att observera exploderande och försvinnande gradienter i ett djupt RNN genom loggning av gradientnormer och förstå varför långa sekvenser gör träningen instabil.

Lektion 2 av 413 steg

Problemet med försvinnande gradienter över djupa tidssteg är en gratis lektion i Machine Learning Academy på CoddyKit. Detta är lektion 2 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Machine Learning Academy, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Machine Learning Academy innehåller totalt 4 lektioner.

Gradienter måste färdas genom tiden

För att lära sig långsiktiga beroenden i en sekvens måste gradienter från förlusten vid det sista tidssteget färdas bakåt genom varje tidssteg för att uppdatera parametrarna som bearbetade de tidiga indata. För en sekvens med längden T innebär det att samma viktmatris W_hh multipliceras med sig själv T gånger under BPTT. Denna upprepade multiplikation är grundorsaken till både försvinnande gradienter (exponentiell avtagning) och exploderande gradienter (exponentiell tillväxt).

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

Försvinnande gradient: den matematiska grundorsaken

Under BPTT innefattar gradienten av förlusten med avseende på det dolda tillståndet vid tidssteg t produkten av Jacobianmatriserna för h med avseende på h vid varje steg från t till T. Jacobianen vid varje steg innefattar diag(f'(h_t)) * W_hh, där f' är derivatan av aktiveringsfunktionen. För tanh är f' begränsad till 1, och typiska slumpmässiga vikter har en spektral radie mindre än 1 — därför driver produkten av T matriser gradienterna till noll exponentiellt snabbt när T ökar.

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

Att observera försvinnande gradienter experimentellt

Ni kan observera försvinnande gradienter direkt genom att logga gradientnormen vid varje tidssteg under BPTT. Registrera bakåtkrokar på det dolda tillståndet vid varje RNN-steg för att fånga gradienternas storlek. I ett vanligt RNN med 50 steg är gradienten vid tidssteg 1 vanligtvis 1e-6 eller mindre — i praktiken noll — vilket innebär att de första tokenen i en sekvens nästan inte påverkar modellparametrarna. Modellen kan inte lära sig att subjektet i början av en lång mening bestämmer verbet i slutet.

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

Exploderande gradienter: den andra ytterligheten

Exploderande gradienter uppstår när den spektrala radien för W_hh överstiger 1 — gradientnormerna växer exponentiellt med sekvensens längd. Symptomen är NaN-värden i förlusten eller parametrar som uppdateras till oändlighet. Till skillnad från försvinnande gradienter (som leder till obemärkt misslyckad inlärning) kraschar exploderande gradienter träningen på ett synligt sätt. Den vanliga lösningen är gradientklippning: skala om gradientvektorn så att den maximala L2-normen är 1.0 före optimeringssteget. Detta förhindrar katastrofala uppdateringar utan att ta bort gradientsignalen.

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 av gradientnormer över lager

En praktisk felsökningsteknik är att logga gradientnormerna för alla parametrar efter varje bakåtpassering och plotta dem under träningen. För vanliga RNN:er uppvisar den rekurrenta viktmatrisen W_hh vanligtvis mycket mindre gradienter än indatavikten W_xh, vilket bekräftar att långväga information inte når de tidigare parametrarna. Denna visualisering visar ofta att endast de sista tidsstegen bidrar meningsfullt till inlärningen, vilket motiverar ett byte till grindade arkitekturer.

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

Varför tanh förvärrar försvinnandet

Aktiveringsfunktionen tanh är begränsad till intervallet −1 till 1 och har derivatan 1 - tanh^2(x). När indata är stor (mättad) närmar sig derivatan 0, vilket minskar gradienten till nästan noll vid det steget. Multiplikation av många derivator nära noll under BPTT förstärker problemet med försvinnande gradienter. ReLU har derivatan 1 för positiva indata (ingen mättning), vilket underlättar gradientflödet i framåtriktade nätverk, men i RNN:er dominerar den upprepade multiplikationen av W_hh fortfarande och kan orsaka explosioner 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!

Trunkerad BPTT: en praktisk lösning

Trunkerad BPTT begränsar gradientpropageringen till ett fast fönster med K tidssteg i stället för hela sekvensens längd. Gradienter propageras K steg bakåt, varefter det dolda tillståndet kopplas loss från beräkningsgrafen (och blir en konstant). Detta förhindrar minnesproblem och gradientexplosioner för mycket långa sekvenser (ljud och textkorpusar), men innebär att beroenden som sträcker sig över mer än K steg inte lärs in. K=20-50 är vanligt vid språkmodellering med vanliga 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')

Utmaningen med långväga beroenden

Beakta meningen: 'The trophy that the man won was big.' Verbet 'was' måste kongruera med 'trophy', inte med 'man'. Det kräver att information om 'trophy' bärs över 5 ord till den plats där 'was' förekommer. Ett vanligt RNN som tränas med BPTT kan i princip inte göra detta tillförlitligt för luckor på mer än 5–10 token. Detta är den centrala begränsningen som motiverade utvecklingen av LSTM (1997) och senare Transformer (2017), som båda har uttryckliga mekanismer för att bevara information över långa avstånd.

# 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')

Initialiseringstrick för vikter i RNN:er

Flera initialiseringstrick förbättrar träningen av vanliga RNN:er på sekvenser med måttlig längd. Om W_hh initialiseras som en ortogonal matris (med en spektral radie på exakt 1) förhindras tidigt försvinnande eller exploderande. En skip-anslutning från indata direkt till utdata kringgår flera matrismultiplikationer. Initialisering med identitetsmatris för W_hh med ReLU-aktivering (IRNN) har visat sig matcha LSTM på vissa uppgifter, vilket bevisar att enbart initialisering delvis kan hantera problemet med försvinnande 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

Varför LSTM uppfanns

Problemet med försvinnande gradienter i RNN:er dokumenterades av Hochreiter 1991. Hans lösning, nätverket Long Short-Term Memory (LSTM), introducerades 1997 och ersätter det enda dolda tillståndet med ett celltillstånd som skyddas av grindar. Celltillståndet flödar genom tiden med endast additiva förändringar (inte multiplikativa), vilket skapar en gradientmotorväg som gör att gradienter kan flöda bakåt på obestämd tid utan att försvinna. Denna enda arkitektoniska innovation möjliggjorde praktisk träning av sekvenser med beroenden på över 100 tidssteg.

# 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')

Jämförelse av träningsstabilitet hos RNN och LSTM

Skillnaden i träningsstabilitet mellan vanliga RNN:er och LSTM blir dramatisk för sekvenser som är längre än 20–30 tidssteg. I den klassiska kopieringsuppgiften (att återskapa indatasekvensen efter en lång fördröjning) misslyckas vanliga RNN:er helt vid fördröjningar över 10 steg, medan LSTM lyckas med fördröjningar på över 100 steg. Detta praktiska riktmärke visar konkret att problemet med försvinnande gradienter i grunden begränsar vanliga RNN:er och att LSTM:s arkitektoniska lösning är nödvändig för verklig 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}')

Snabbtest

Testa Er förståelse av begreppen inom Machine Learning with Python från den här lektionen.

Sammanfattning av lektionen

I den här lektionen lärde Ni Er att försvinnande gradienter uppstår när upprepad multiplikation med W_hh (med en spektral radie < 1) driver gradienterna exponentiellt mot noll i långa sekvenser, att exploderande gradienter uppstår när den spektrala radien är > 1 och åtgärdas med gradientklippning, samt att LSTM uppfanns specifikt för att lösa problemet med försvinnande gradienter genom en additiv uppdatering av celltillståndet som skapar en gradientmotorväg. Härnäst undersöker vi LSTM-cellens arkitektur i detalj.

Gratis att börja

Lär dig Python med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
30
Lektioner
120

Vanliga frågor

Är lektionen ”Problemet med försvinnande gradienter över djupa tidssteg” gratis?

Ja – hela texten till ”Problemet med försvinnande gradienter över djupa tidssteg” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Machine Learning Academy, kan Ni uppgradera till CoddyKit PRO. Kursen i Machine Learning Academy innehåller totalt 4 lektioner.

Vad lär jag mig i ”Problemet med försvinnande gradienter över djupa tidssteg”?

Ni kommer att observera exploderande och försvinnande gradienter i ett djupt RNN genom loggning av gradientnormer och förstå varför långa sekvenser gör träningen instabil. Ni övar på Machine Learning Academy med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Machine Learning Academy?

Du behöver inga förkunskaper. Utbildningen i Machine Learning Academy på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.

Hur lång tid tar lektionen ”Problemet med försvinnande gradienter över djupa tidssteg”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Machine Learning Academy-lektionen?

Ja. Varje Machine Learning Academy-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Vanliga RNN: Dolt tillstånd och sekvensutrullning
  2. Problemet med försvinnande gradienter över djupa tidssteg
  3. LSTM-cell: Inmatnings-, glömske- och utdatagater
  4. Sekvens till ett värde: Sentimentanalys med en LSTM
← Tillbaka till Machine Learning Academy