Faza 03 · lecția 09

Scheme ale ratei de învățare și warmup

Scopul lecției: Rata de învățare este cel mai important hiperparametru. Nu arhitectura. Nu dimensiunea setului de date. Nu funcția de activare. Rata de învățare. Dacă nu reglați nimic altceva, reglați-o pe aceasta.

Versiunea curentă AlexBred.com: primele 100 de lecții ale programului în limba română.

Curs
AI Engineering from Scratch
Fază
Fundamentele învățării profunde
Lectură
19 min.
Verificat
Cuprinsul lecției
  1. Obiective de învățare
  2. Problema
  3. Conceptul
  4. Rată de învățare constantă
  5. Descreștere în trepte
  6. Amortizare cosinusoidală
  7. Warmup: de ce porniți cu o valoare mică
  8. Warmup liniar urmat de descreștere cosinusoidală
  9. Politica 1cycle
  10. Formele schemelor
  11. Diagramă decizională
  12. Valori reale din modele publicate
  13. Construiți
  14. Pasul 1: funcțiile schemelor
  15. Pasul 2: vizualizarea tuturor schemelor
  16. Pasul 3: rețeaua pentru antrenare
  17. Pasul 4: compararea tuturor schemelor
  18. Pasul 5: LR prea mare sau prea mică
  19. Folosiți
  20. Livrați
  21. Exerciții
  22. Termeni-cheie
  23. Lecturi suplimentare

Rata de învățare este cel mai important hiperparametru. Nu arhitectura. Nu dimensiunea setului de date. Nu funcția de activare. Rata de învățare. Dacă nu reglați nimic altceva, reglați-o pe aceasta.

Tip: Construire Limbaje: Python Cerințe preliminare: Lecția 03.06 — Optimizatori; lecția 03.08 — Inițializarea ponderilor Durată: ~90 de minute

Obiective de învățare

  • Să implementați de la zero scheme cu rată de învățare constantă, descreștere în trepte, amortizare cosinusoidală, warmup urmat de cosinus și 1cycle.
  • Să demonstrați cele trei moduri de eșec ale alegerii ratei de învățare: divergența când este prea mare, stagnarea când este prea mică și oscilația când nu descrește.
  • Să explicați de ce warmup-ul este folosit cu optimizatorii bazați pe Adam și cum poate stabiliza începutul antrenării.
  • Să comparați viteza de convergență a celor cinci scheme pe aceeași sarcină și să o alegeți pe cea potrivită unui buget de antrenare dat.

Problema

Stabiliți rata de învățare la 0,1. Antrenarea diverge — pierderea ajunge la infinit în trei pași. Stabiliți-o la 0,0001. Antrenarea înaintează foarte lent — după 100 de epoci, modelul abia s-a îndepărtat de starea aleatoare. Stabiliți-o la 0,01. Antrenarea funcționează 50 de epoci, apoi pierderea oscilează în jurul unui minim la care nu poate ajunge, deoarece pașii sunt prea mari.

Rata de învățare optimă nu trebuie să fie constantă. La început sunt utile adesea salturi mari, pentru a avansa rapid. Spre sfârșitul antrenării sunt utile salturi mici, pentru stabilizarea în apropierea unui minim. Diferența dintre un model cu acuratețe de 90% și unul cu 95% poate proveni uneori numai din schemă.

Multe modele importante folosesc o schemă a ratei de învățare. Parametrii nu sunt aleși arbitrar, ci sunt de regulă rezultatul unor experimente costisitoare cu hiperparametri.

Notă tehnică a traducerii: Afirmația originală despre „fiecare model important publicat în ultimii trei ani” nu poate fi susținută universal, iar valorile sale pentru Llama 3 și GPT-3 sunt greșite. Raportul Llama 3 indică pentru modelul 405B o rată maximă de 8e-5, 8.000 de pași de warmup și o descreștere cosinusoidală până la 8e-7 în 1.200.000 de pași; modelul a fost preantrenat pe 15,6 trilioane de tokenuri. Lucrarea GPT-3 indică pentru modelul 175B o rată de 6e-5, warmup liniar pe 375 de milioane de tokenuri și descreștere cosinusoidală până la 10% din valoare pe parcursul a 260 de miliarde de tokenuri.

Trebuie să înțelegeți schemele deoarece valorile implicite nu se vor potrivi neapărat problemei dumneavoastră. Când ajustați fin un model preantrenat, schema potrivită diferă de cea folosită la antrenarea de la zero. Când măriți dimensiunea lotului, poate trebui modificată și perioada de warmup. Când antrenarea se întrerupe la pasul 10.000, trebuie să puteți determina dacă problema provine din schemă sau din altă cauză.

Conceptul

Rată de învățare constantă

Cea mai simplă abordare. Alegeți o valoare și folosiți-o la fiecare pas.

lr(t) = lr_0

Este rareori optimă. Fie este prea mare spre sfârșitul antrenării și provoacă oscilații în jurul minimului, fie este prea mică la început și irosește calculul pe pași minusculi. Poate fi suficientă pentru modele mici și depanare. Este o alegere slabă pentru multe antrenări de lungă durată.

Descreștere în trepte

Abordarea clasică din epoca ResNet. Reduceți rata de învățare cu un factor, de obicei de 10 ori, la epoci fixe.

lr(t) = lr_0 * gamma^(floor(epoch / step_size))

Cu gamma = 0.1 și step_size = 30, rata scade de 10 ori la fiecare 30 de epoci. O rețetă ResNet-50 răspândită folosește lr=0.1 și reduceri de 10 ori la epocile 30, 60 și 90.

Notă tehnică a traducerii: Articolul ResNet original nu fixează pentru ImageNet bornele 30, 60 și 90; el descrie împărțirea ratei la 10 când eroarea se plafonează. Succesiunea 30/60/90 este o rețetă ulterioară frecventă, nu configurația documentată în lucrarea ResNet inițială.

Problema este că punctele optime de descreștere depind de setul de date și de arhitectură. Dacă treceți la altă problemă, trebuie să reglați din nou momentele reducerilor. Tranzițiile sunt abrupte și pot modifica brusc dinamica pierderii.

Amortizare cosinusoidală

Rata descrește lin de la valoarea maximă la una minimă, urmând o curbă cosinusoidală:

lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T))

Unde t este pasul curent, iar T este numărul total de pași.

La t=0, termenul cosinus este 1, deci lr = lr_max. La t=T, termenul cosinus este -1, deci lr = lr_min. Descreșterea este lentă la început, accelerează la mijloc și încetinește din nou spre final.

Aceasta este o alegere implicită frecventă pentru antrenările moderne. Pe lângă lr_max și lr_min, trebuie stabilită și durata T. Forma cosinusoidală menține pași substanțiali în partea centrală a antrenării și îi reduce lin spre final, dar nu este garantat că va fi optimă pentru orice problemă.

Warmup: de ce porniți cu o valoare mică

Adam și alți optimizatori adaptivi mențin estimări mobile ale primului și celui de-al doilea moment al gradientului. La pasul 0, aceste estimări sunt inițializate cu zero. Actualizările timpurii pot avea o scară instabilă în anumite configurații; dacă rata de învățare este mare în această perioadă, modelul poate face pași prea mari și slab orientați.

Warmup-ul poate reduce acest risc. Porniți cu o rată de învățare foarte mică, adesea lr_max / warmup_steps sau chiar zero, și măriți-o liniar până la lr_max pe parcursul primilor N pași. Astfel, optimizatorul și dinamica rețelei evoluează treptat înainte de atingerea ratei complete.

lr(t) = lr_max * (t / warmup_steps)     pentru t < warmup_steps

Notă tehnică a traducerii: Adam aplică bias correction estimărilor momentelor, deci nu este corect să spunem pur și simplu că primele actualizări se bazează pe „statistici inutile”. Warmup-ul nu este obligatoriu pentru orice utilizare a lui Adam; utilitatea și durata lui depind de scara actualizărilor, parametrizare, arhitectură, dimensiunea lotului și rata țintă. O analiză dedicată atribuie instabilitatea timpurie mai ales magnitudinii termenului de actualizare, nu doar inițializării cu zero a momentelor.

O euristică întâlnită este ca warmup-ul să ocupe 1–5% din pașii de antrenare, însă aceasta nu este o regulă universală. Llama 3 405B a folosit 8.000 de pași, iar GPT-3 a folosit 375 de milioane de tokenuri de warmup.

Warmup liniar urmat de descreștere cosinusoidală

O combinație modernă frecventă: rata crește liniar, apoi descrește cosinusoidal.

if t < warmup_steps:
    lr(t) = lr_max * (t / warmup_steps)
else:
    progress = (t - warmup_steps) / (total_steps - warmup_steps)
    lr(t) = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * progress))

Această combinație apare în numeroase antrenări de transformatoare, inclusiv în variante din familiile Llama și GPT. Warmup-ul poate preveni instabilitatea inițială, iar descreșterea cosinusoidală micșorează lin pașii spre final. Rețeta exactă diferă însă între modele și etape de antrenare.

Politica 1cycle

Observația lui Leslie Smith, publicată inițial în 2017 și ulterior în forma finală: măriți rata de învățare de la o valoare mică la una mare în prima parte a antrenării, apoi reduceți-o în partea rămasă. Pare contraintuitiv — de ce ați mări rata de învățare pe parcurs?

Teoria propusă este că o rată mare de învățare acționează ca regularizare prin modificarea traiectoriei de optimizare. Modelul explorează o regiune mai largă a peisajului pierderii în etapa de creștere, iar etapa de descreștere rafinează soluția găsită.

Faza 1 (de la 0 la T/2):    lr crește de la lr_max/25 la lr_max
Faza 2 (de la T/2 la T):    lr scade de la lr_max la lr_max/10000

1cycle poate antrena mai repede decât amortizarea cosinusoidală în unele probleme și la același buget de calcul. Compromisul este că trebuie să cunoașteți dinainte numărul total de pași.

Notă tehnică a traducerii: Împărțirea exactă în două jumătăți și factorii de mai sus reprezintă o variantă simplificată, nu definiția universală a 1cycle. De exemplu, OneCycleLR din PyTorch folosește implicit 30% din ciclu pentru creștere, amortizare cosinusoidală, variația inversă a momentumului, initial_lr = max_lr/25 și min_lr = initial_lr/10000. Performanța mai rapidă nu este garantată pe orice sarcină.

Formele schemelor

Диаграмма к уроку «Scheme ale ratei de învățare și warmup»

Diagramă decizională

Диаграмма к уроку «Scheme ale ratei de învățare și warmup»

Notă tehnică a traducerii: Diagrama oferă euristici, nu reguli generale. Bugetul exprimat numai prin durată nu determină schema optimă, 1cycle nu garantează cea mai rapidă convergență, iar proporția de warmup și raportul lr_min/lr_max trebuie validate pentru configurația concretă.

Valori reale din modele publicate

Диаграмма к уроку «Scheme ale ratei de învățare și warmup»

lr-schedule

Construiți

Pasul 1: funcțiile schemelor

Fiecare funcție primește pasul curent și întoarce rata de învățare pentru acel pas.

import math


def constant_schedule(step, lr=0.01, **kwargs):
    return lr


def step_decay_schedule(step, lr=0.1, step_size=100, gamma=0.1, **kwargs):
    return lr * (gamma ** (step // step_size))


def cosine_schedule(step, lr=0.01, total_steps=1000, lr_min=1e-5, **kwargs):
    if step >= total_steps:
        return lr_min
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * step / total_steps))


def warmup_cosine_schedule(step, lr=0.01, total_steps=1000, warmup_steps=100, lr_min=1e-5, **kwargs):
    if total_steps <= warmup_steps:
        return lr * (step / max(warmup_steps, 1))
    if step < warmup_steps:
        return lr * step / warmup_steps
    progress = (step - warmup_steps) / (total_steps - warmup_steps)
    return lr_min + 0.5 * (lr - lr_min) * (1 + math.cos(math.pi * progress))


def one_cycle_schedule(step, lr=0.01, total_steps=1000, **kwargs):
    mid = max(total_steps // 2, 1)
    if step < mid:
        return (lr / 25) + (lr - lr / 25) * step / mid
    else:
        progress = (step - mid) / max(total_steps - mid, 1)
        return lr * (1 - progress) + (lr / 10000) * progress

Pasul 2: vizualizarea tuturor schemelor

Afișați o diagramă textuală care arată evoluția fiecărei scheme pe parcursul antrenării.

def visualize_schedule(name, schedule_fn, total_steps=500, **kwargs):
    steps = list(range(0, total_steps, total_steps // 20))
    if total_steps - 1 not in steps:
        steps.append(total_steps - 1)

    lrs = [schedule_fn(s, total_steps=total_steps, **kwargs) for s in steps]
    max_lr = max(lrs) if max(lrs) > 0 else 1.0

    print(f"\n{name}:")
    for s, lr_val in zip(steps, lrs):
        bar_len = int(lr_val / max_lr * 40)
        bar = "#" * bar_len
        print(f"  Step {s:4d}: lr={lr_val:.6f} {bar}")

Pasul 3: rețeaua pentru antrenare

O rețea simplă cu două straturi pe setul de date circular, la fel ca în lecțiile anterioare, însă acum variem schema.

import random


def sigmoid(x):
    x = max(-500, min(500, x))
    return 1.0 / (1.0 + math.exp(-x))


def relu(x):
    return max(0.0, x)


def relu_deriv(x):
    return 1.0 if x > 0 else 0.0


def make_circle_data(n=200, seed=42):
    random.seed(seed)
    data = []
    for _ in range(n):
        x = random.uniform(-2, 2)
        y = random.uniform(-2, 2)
        label = 1.0 if x * x + y * y < 1.5 else 0.0
        data.append(([x, y], label))
    return data


def train_with_schedule(schedule_fn, schedule_name, data, epochs=300, base_lr=0.05, **kwargs):
    random.seed(0)
    hidden_size = 8
    total_steps = epochs * len(data)

    std = math.sqrt(2.0 / 2)
    w1 = [[random.gauss(0, std) for _ in range(2)] for _ in range(hidden_size)]
    b1 = [0.0] * hidden_size
    w2 = [random.gauss(0, std) for _ in range(hidden_size)]
    b2 = 0.0

    step = 0
    epoch_losses = []

    for epoch in range(epochs):
        total_loss = 0
        correct = 0

        for x, target in data:
            lr = schedule_fn(step, lr=base_lr, total_steps=total_steps, **kwargs)

            z1 = []
            h = []
            for i in range(hidden_size):
                z = w1[i][0] * x[0] + w1[i][1] * x[1] + b1[i]
                z1.append(z)
                h.append(relu(z))

            z2 = sum(w2[i] * h[i] for i in range(hidden_size)) + b2
            out = sigmoid(z2)

            error = out - target
            d_out = error * out * (1 - out)

            for i in range(hidden_size):
                d_h = d_out * w2[i] * relu_deriv(z1[i])
                w2[i] -= lr * d_out * h[i]
                for j in range(2):
                    w1[i][j] -= lr * d_h * x[j]
                b1[i] -= lr * d_h
            b2 -= lr * d_out

            total_loss += (out - target) ** 2
            if (out >= 0.5) == (target >= 0.5):
                correct += 1
            step += 1

        avg_loss = total_loss / len(data)
        accuracy = correct / len(data) * 100
        epoch_losses.append(avg_loss)

    return epoch_losses

Notă tehnică a traducerii: Experimentul folosește actualizări SGD simple, nu Adam, deci nu testează afirmația despre warmup și statisticile unui optimizator adaptiv. În plus, d_out este gradientul pierderii 0.5 * (out - target) ** 2, în timp ce valoarea raportată omite factorul 0.5; aceasta schimbă numai scara gradientului față de pierderea afișată, nu direcția lui.

Notă tehnică a traducerii: std = sqrt(2/2) este calculat pentru primul strat, cu fan_in = 2, dar este reutilizat pentru w2, al cărui fan_in este hidden_size = 8. Astfel, comparația dintre scheme poate fi influențată și de inițializarea prea largă a celei de-a doua proiecții; o inițializare He potrivită pentru ea ar folosi sqrt(2/hidden_size).

Pasul 4: compararea tuturor schemelor

Antrenați aceeași rețea cu fiecare schemă și comparați pierderea finală și comportamentul convergenței.

def compare_schedules(data):
    configs = [
        ("Constant", constant_schedule, {}),
        ("Step Decay", step_decay_schedule, {"step_size": 15000, "gamma": 0.1}),
        ("Cosine", cosine_schedule, {"lr_min": 1e-5}),
        ("Warmup+Cosine", warmup_cosine_schedule, {"warmup_steps": 3000, "lr_min": 1e-5}),
        ("1cycle", one_cycle_schedule, {}),
    ]

    print(f"\n{'Schedule':<20} {'Start Loss':>12} {'Mid Loss':>12} {'End Loss':>12} {'Best Loss':>12}")
    print("-" * 70)

    for name, schedule_fn, extra_kwargs in configs:
        losses = train_with_schedule(schedule_fn, name, data, epochs=300, base_lr=0.05, **extra_kwargs)
        mid_idx = len(losses) // 2
        best = min(losses)
        print(f"{name:<20} {losses[0]:>12.6f} {losses[mid_idx]:>12.6f} {losses[-1]:>12.6f} {best:>12.6f}")

Notă tehnică a traducerii: Codul compară numai pierderile de antrenare la sfârșit de epocă. Fără un set de validare, rulări repetate și o măsură definită a timpului sau numărului de pași până la un prag, el nu demonstrează generalizarea și nici viteza de convergență în sens general.

Pasul 5: LR prea mare sau prea mică

Demonstrați cele trei moduri de eșec: o rată prea mare produce divergență, una prea mică înaintează foarte lent, iar una potrivită permite convergența.

def lr_sensitivity(data):
    learning_rates = [1.0, 0.1, 0.01, 0.001, 0.0001]

    print("\nLR Sensitivity (constant schedule, 100 epochs):")
    print(f"  {'LR':>10} {'Start Loss':>12} {'End Loss':>12} {'Status':>15}")
    print("  " + "-" * 52)

    for lr in learning_rates:
        losses = train_with_schedule(constant_schedule, f"lr={lr}", data, epochs=100, base_lr=lr)
        start = losses[0]
        end = losses[-1]

        if end > start or math.isnan(end) or end > 1.0:
            status = "DIVERGED"
        elif end > start * 0.9:
            status = "BARELY MOVED"
        elif end < 0.15:
            status = "CONVERGED"
        else:
            status = "LEARNING"

        end_str = f"{end:.6f}" if not math.isnan(end) else "NaN"
        print(f"  {lr:>10.4f} {start:>12.6f} {end_str:>12} {status:>15}")

Notă tehnică a traducerii: Etichetele de stare sunt euristici specifice acestui exemplu. Condiția end > start nu demonstrează matematic divergența, iar pragul end < 0.15 nu definește universal convergența.

Folosiți

PyTorch oferă planificatoare în torch.optim.lr_scheduler:

import torch
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR, OneCycleLR, StepLR

model = nn.Sequential(nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 1))
optimizer = optim.Adam(model.parameters(), lr=3e-4)

scheduler = CosineAnnealingLR(optimizer, T_max=1000, eta_min=1e-5)

for step in range(1000):
    loss = train_step(model, optimizer)
    scheduler.step()

Notă tehnică a traducerii: scheduler.step() trebuie apelat după optimizer.step(); exemplul este corect numai dacă train_step execută actualizarea optimizatorului. T_max numără apelurile planificatorului, așadar unitatea sa este pasul sau epoca în funcție de locul apelului. Fragmentul este doar ilustrativ și nu poate rula autonom: lipsesc importul sau definiția pentru nn și definiția funcției train_step.

Pentru warmup urmat de cosinus, folosiți un planificator lambda sau get_cosine_schedule_with_warmup din Hugging Face:

from transformers import get_cosine_schedule_with_warmup

scheduler = get_cosine_schedule_with_warmup(
    optimizer,
    num_warmup_steps=2000,
    num_training_steps=100000,
)

Funcția Hugging Face este utilizată în multe scripturi de ajustare fină pentru modele din familiile Llama și GPT. Ca punct de pornire, puteți încerca warmup urmat de cosinus, cu warmup de 3–5% din numărul total de pași, dar trebuie să validați alegerea pe arhitectura, optimizatorul, dimensiunea lotului și bugetul concret; nu există o configurație garantată pentru toate problemele.

Livrați

Această lecție produce:

  • outputs/prompt-lr-schedule-advisor.md — un prompt care recomandă schema și hiperparametrii potriviți ai ratei de învățare pentru configurația de antrenare.

Exerciții

  1. Implementați descreșterea exponențială: lr(t) = lr_0 * gamma^t, unde gamma = 0.999. Comparați-o cu amortizarea cosinusoidală pe setul de date circular.

  2. Implementați testul intervalului ratei de învățare al lui Leslie Smith: antrenați câteva sute de pași, mărind exponențial LR de la 1e-7 la 1. Reprezentați grafic pierderea în funcție de LR. Valoarea maximă potrivită se află chiar înainte ca pierderea să înceapă să crească.

  3. Antrenați cu warmup urmat de cosinus, dar variați lungimea warmup-ului: 0%, 1%, 5%, 10% și 20% din numărul total de pași. Găsiți intervalul în care antrenarea este cea mai stabilă.

  4. Implementați amortizarea cosinusoidală cu reporniri calde (SGDR): readuceți rata de învățare la lr_max la fiecare T pași și reluați descreșterea. Comparați-o cu schema cosinusoidală standard într-o antrenare mai lungă.

  5. Construiți un „chirurg al schemei” care monitorizează pierderea în timpul antrenării, trece automat de la warmup la cosinus când pierderea se stabilizează și reduce lr dacă pierderea stagnează prea mult.

Termeni-cheie

Termen Ce spun oamenii Ce înseamnă de fapt
Rată de învățare „Cât de repede învață modelul” Scalarul care înmulțește gradientul pentru a determina mărimea actualizării parametrilor
Schemă „Modificați LR în timp” Funcție care asociază fiecărui pas de antrenare o rată de învățare și urmărește îmbunătățirea convergenței
Warmup „Porniți cu o LR mică” Creșterea liniară a LR de la o valoare apropiată de zero la valoarea-țintă în primii N pași, pentru a stabiliza începutul optimizării
Amortizare cosinusoidală „Descreștere lină a LR” Reducerea LR pe o curbă cosinusoidală de la lr_max la lr_min pe parcursul antrenării
Descreștere în trepte „Reduceți LR la borne” Înmulțirea LR cu un factor, de obicei 0,1, la intervale fixe de epoci
Politica 1cycle „În sus, apoi în jos” Metoda lui Leslie Smith de a crește și apoi a reduce LR într-un singur ciclu, asociată cu super-convergența în anumite configurații
Testul intervalului LR „Găsiți cea mai bună rată de învățare” Antrenare scurtă cu LR în creștere, pentru a identifica regiunea în care pierderea începe să diverge
Cosinus cu reporniri calde „Reinițializați și repetați” Readucerea periodică a LR la lr_max, urmată de o nouă descreștere cosinusoidală (SGDR)
Eta min „Pragul inferior pentru LR” Rata minimă de învățare până la care descrește schema
Rată maximă de învățare „LR maximă” Cea mai mare LR atinsă în timpul antrenării, de regulă după warmup

Lecturi suplimentare

  • Loshchilov și Hutter, „SGDR: Stochastic Gradient Descent with Warm Restarts” (2017) — a introdus amortizarea cosinusoidală și repornirile calde.
  • Smith și Topin, „Super-Convergence: Very Fast Training of Neural Networks Using Large Learning Rates” (2018) — lucrarea despre politica 1cycle.
  • Touvron și colaboratorii, „Llama 2: Open Foundation and Fine-Tuned Chat Models” (2023) — documentează o schemă cu warmup și cosinus folosită la scară mare.
  • Goyal și colaboratorii, „Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour” (2017) — regula de scalare liniară și warmup-ul pentru antrenarea cu loturi mari.

Sursă: Originalul în limba engleză

Navigare: ← Lecția 03.08 — Inițializarea ponderilor · Faza 3 — Fundamentele învățării profunde · Lecția 03.10 — Construiți-vă propriul mini-framework → · Catalog complet