Fəsil 01 · 8 dəq oxu

Neural network-lər necə öyrənir

Neural network-lər necə öyrənir

Bu kitabda danışacağımız hər şey bir neçə sadə mexanizm üzərində qurulub. Milyardlarla parametri olan etmək, retrieval üçün model öyrətmək, diffusion model-ə yeni conditioning əlavə etmək — hamısı eyni loop-dur: proqnoz ver, səhvi ölç, səhvi azaldacaq istiqamətdə parametrləri bir az dəyiş. Bu fəsil həmin loop-u hissələrə ayırır. Məqsəd riyazi isbatlar deyil, training-də nəsə səhv gedəndə nəyə baxmalı olduğunu bilməkdir: loss niyə düşmür, niyə birdən NaN olur, model niyə əla, yeni data-da isə zəifdir.

Tensor-lar: hər şey ədəd massividir

gördüyü yeganə şey ədədlərdir. Şəkil piksel dəyərlərindən ibarət cədvəl, mətn id-lərinin ardıcıllığı, səs isə amplituda ardıcıllığıdır. Bu ədədlər tensor-larda saxlanılır. Tensor çoxölçülü massivdir: scalar 0 ölçülü, vektor 1 ölçülü, matris 2 ölçülü tensor-dur. Rəngli şəkillərdən ibarət batch isə 4 ölçülüdür: [batch, kanal, hündürlük, en].

Praktikada tensor-un iki xassəsini daim izləməlisiniz:

  • shape — hər ölçünün uzunluğu. Training kodundakı xətaların böyük hissəsi shape uyğunsuzluğudur.
  • dtype — ədədlərin necə saxlandığı: float32, bfloat16, int64. Bu seçim yaddaşı və sürəti birbaşa müəyyən edir. Sonrakı fəsillərdə ona qayıdacağıq.

GPU-ların bu sahədə hakim olmasının səbəbi sadədir: hesablamalarının demək olar hamısı böyük tensor-lar üzərində matris vurmasıdır, GPU isə minlərlə belə əməliyyatı paralel icra etmək üçün qurulub.

Model parametrləşdirilmiş funksiyadır

Ən sadə formada model girişi çıxışa çevirən funksiyadır və onun davranışı parametr-lərlə () müəyyən olunur:

y_pred = f(x; θ)

Burada x giriş, θ parametrlər toplusu, y_pred isə proqnozdur. Bir linear layer y = W·x + b hesablayır: W matrisi, b isə bias vektorudur. Bir neçə linear layer-i ardıcıl düzsəniz yenə də linear funksiya alırsınız, buna görə layer-lərin arasına qeyri-xətti activation funksiyası qoyulur: ReLU (max(0, z)), GELU və ya SiLU. Müasir model-lərdə ən çox GELU və SiLU istifadə olunur.

Ən sadə beləcə alınır: linear → activation → linear → activation → ... Belə layer-lərdən kifayət qədər olarsa, network demək olar istənilən funksiyanı approximate edə bilər. Lakin bunun mümkün olması o demək deyil ki, düzgün parametrləri tapmaq asandır. Training məhz bu problemlə məşğuldur.

Loss: səhvi bir ədədlə ifadə etmək

Parametrləri yaxşılaşdırmaq üçün əvvəlcə "yaxşı" nə deməkdir, onu ölçmək lazımdır. proqnozla düzgün cavabı müqayisə edib bir scalar qaytarır: nə qədər kiçikdirsə, bir o qədər yaxşıdır.

Mean squared error (MSE) regression üçün standartdır, yəni ev qiyməti və ya temperatur kimi davamlı dəyəri proqnozlaşdırarkən istifadə olunur:

MSE = (1/N) · Σ (y_pred − y_true)²

Kvadrata salmaq böyük səhvləri daha ağır cəzalandırır.

üçün istifadə olunur və əsas loss-udur. Model hər class üçün xam bal (logit) qaytarır. Softmax bu balları cəmi 1 olan ehtimallara çevirir. isə düzgün class-a verilən ehtimalın mənfi logarifmidir:

p = softmax(logits)
loss = −log(p[düzgün_class])

Model düzgün cavaba 0.9 ehtimal versə, loss ≈ 0.105 olur. 0.01 versə, loss ≈ 4.6 olur. Əmin olub səhv etmək çox baha başa gəlir. training-də "class" lüğətdəki biridir. Növbəti proqnozlaşdırmaq on minlərlə class-lı məsələsidir və onun loss-u .

Loss sizin həqiqətən istədiyiniz şeyin proxy-sidir. Model loss-u optimallaşdırır, niyyətinizi yox. Bu ayrılıq kitab boyu dəfələrlə qarşımıza çıxacaq.

Gradient descent: aşağı enmək

Loss-u parametrlərin funksiyası kimi təsəvvür edin: milyonlarla ölçüdə dağlıq relyef. Məqsəd bu relyefin aşağı nöqtəsini tapmaqdır. Gradient hər parametr üzrə loss-un nə qədər sürətlə dəyişdiyini göstərən vektordur və ən dik yüksəliş istiqamətini göstərir. Deməli, onun əksinə addım atmaq lazımdır:

θ ← θ − η · ∇L(θ)

η learning rate-dir, yəni addımın ölçüsü. Kiçik learning rate ilə training yavaş gedir və pis yerdə ilişib qala bilir. Böyük learning rate ilə isə model minimumun üstündən tullanır, loss salınır və ya partlayaraq NaN olur.

Bütün dataset üzərində gradient hesablamaq çox bahadır, buna görə hər addımda kiçik təsadüfi alt çoxluq, yəni mini-batch götürülür. Buna () deyilir. Gradient-in təxmini küylü olur, amma ucuzdur. Bu küy hətta faydalıdır, çünki model-in kəskin, dar minimumlara ilişməsinin qarşısını alır.

Backpropagation: gradient-i necə hesablamalı

Milyardlarla parametr üçün gradient-i ayrıca hesablamaq mümkün deyil. bunu səmərəli edir. Forward pass zamanı hər əməliyyat computational graph-da qeyd olunur: hansı tensor hansından hansı əməliyyatla alındı. Backward pass zamanı chain rule tətbiq edilərək loss-dan geriyə doğru hərəkət olunur:

dL/dx = dL/dy · dy/dx

Hər node yalnız öz lokal törəməsini bilməlidir. Gradient-lər çıxışdan girişə doğru vurulub ötürülür və bir forward, bir backward pass ilə bütün parametrlərin gradient-i əldə olunur.

PyTorch və digər framework-lər bunu avtomatik edir, bu mexanizmə autograd deyilir. Bunu bilməyin iki praktik nəticəsi var:

  • Yaddaş. Backward pass üçün forward zamanı aralıq nəticələr (activation-lar) saxlanılmalıdır. Böyük model-lərdə yaddaşı çox vaxt deyil, məhz activation-lar doldurur.
  • Gradient axını. Gradient onlarla layer boyu vurularaq ötürüldüyü üçün sıfıra yaxınlaşa (vanishing) və ya partlaya (exploding) bilər. Dərin network-lərin öyrənə bilməsi üçün aşağıda gördüyümüz texnikalar lazımdır.

Optimizer-lər: SGD-dən AdamW-yə

Sadə hər parametrə eyni learning rate tətbiq edir. Bunun üzərində bir neçə təkmilləşdirmə qurulub.

Momentum keçmiş gradient-lərin hərəkətli ortalamasını saxlayır. ağır top kimi hərəkət edir: küyü hamarlayır və dar dərələrdə yan-yana salınmaq əvəzinə irəliləyir.

Adam hər parametr üçün iki hərəkətli ortalama saxlayır: gradient-in özünün (momentum) və kvadratının. Kvadratın ortalamasına bölmək hər parametrə öz effektiv learning rate-ini verir. Nadir yenilənən parametrlər daha böyük, küylü olanlar daha kiçik addım atır.

AdamW Adam-ın decay-i düzgün tətbiq edən variantıdır və bu gün training üçün de facto standartdır. Bunun bir qiyməti var: AdamW hər parametr üçün iki əlavə ədəd saxlayır. 7 milyard parametrli model-i AdamW ilə train etmək üçün yalnız state-i üçün təxminən 56 GB yaddaş lazımdır (float32-də 7B × 2 × 4 byte). fəsillərində LoRA kimi texnikaların niyə meydana çıxdığını bu rəqəm izah edəcək.

Yaddaşa qənaət edən alternativlər (8-bit , Adafactor) və daha yeni (məsələn, Muon) də var, amma başlanğıc nöqtə AdamW-dir.

Learning rate schedule, batch size və gradient clipping

Learning rate training boyu sabit qalmır. Ən geniş yayılmış sxem belədir:

  • Warmup — ilk bir neçə yüz və ya min addımda learning rate sıfıra yaxın dəyərdən hədəf dəyərə qədər xətti artır. Başlanğıcda Adam-ın statistikaları hələ etibarsızdır və böyük addımlar model-i qeyri-stabil bölgəyə ata bilər.
  • Cosine decay — sonra learning rate cosine əyrisi boyunca tədricən kiçik dəyərə enir. Sonda kiçik addımlar model-in minimumun dibinə yerləşməsinə imkan verir.

Bəzi müasir reseptləri "warmup-stable-decay" sxemini istifadə edir: uzun müddət sabit learning rate, sonda qısa və kəskin enmə. Üstünlüyü odur ki, training-i istənilən nöqtədə davam etdirmək və ya dayandırmaq asan olur.

Batch size gradient təxmininin nə qədər küylü olduğunu müəyyən edir. Böyük batch daha dəqiq gradient verir və GPU-nu daha səmərəli doldurur, amma bir addımın qiyməti artır və bir nöqtədən sonra əlavə fayda azalır. Batch yaddaşa sığmayanda gradient accumulation istifadə olunur: bir neçə kiçik batch-ın gradient-i toplanır, sonra bir dəfə addım atılır.

Gradient clipping gradient vektorunun norm-u həddi keçəndə onu kiçildir (adətən norm 1.0-a qədər). Bu, nadir rast gəlinən böyük gradient-in bütün training-i pozmasının qarşısını alan ucuz sığortadır.

Overfitting və regularization

Model-in məqsədi əzbərləmək deyil, görmədiyi data üzərində yaxşı işləməkdir, yəni . Data-nı həmişə ən azı iki hissəyə bölün: və model-in heç vaxt öyrənmədiyi . İki loss əyrisini birlikdə izləyin:

  • İkisi də düşürsə, model öyrənir.
  • Training loss düşür, validation loss isə qalxırsa, bu -dir: model training nümunələrinin xüsusiyyətlərini əzbərləyir.
  • İkisi də yüksək qalırsa, bu -dir: model çox kiçikdir, learning rate səhvdir və ya data-da bir problem var.

qarşı əsas alətlər:

  • Daha çox və daha müxtəlif data. Çox vaxt ən təsirli həll budur.
  • decay — hər addımda bir az sıfıra doğru çəkir. Model daha sadə həllərə üstünlük verir.
  • — training zamanı activation-ların təsadüfi hissəsini sıfırlayır. Model tək bir yola güvənə bilmir. Böyük çox vaxt söndürülür, çünki data o qədər çoxdur ki, model hər nümunəni bir dəfə görür. Kiçik dataset-lərdə zamanı isə yenidən faydalı olur.
  • — validation loss ən aşağı olduğu nöqtədəki checkpoint-i saxlamaq.

Initialization və normalization

başlanğıc dəyərləri təsadüfi seçilir, amma miqyası vacibdir. Çox böyük olsa, activation-lar layer-dən layer-ə böyüyür və partlayır. Çox kiçik olsa, siqnal sönür. Xavier və Kaiming kimi initialization sxemləri hər layer-in çıxışında activation-ların təxminən eyni miqyasda qalması üçün girişlərin sayına görə miqyaslayır.

Normalization layer-ləri bu stabilliyi training boyu qoruyur: activation-ları sıfır ortalamaya və vahid miqyasa gətirir, sonra isə öyrənilən parametrlərlə yenidən miqyaslayır. LayerNorm və onun sadələşdirilmiş variantı RMSNorm istifadə olunur. Bunlara əlavə olaraq residual connection (x + layer(x)) gradient-ə layer-ləri yan keçən birbaşa yol açır. Yüz layer-lik network-lərin ümumiyyətlə öyrənə bilməsinin əsas səbəbi residual connection-dır. Üçüncü fəsildə hər ikisini daxilində görəcəyik.

Minimal training loop

Bütün parçaları birləşdirək. Aşağıdakı kod kiçik train edir. Strukturu milyardlıq model-lərin training loop-u ilə eynidir, fərq yalnız miqyasdadır:

import torch
from torch import nn

model = nn.Sequential(
    nn.Linear(784, 256), nn.GELU(),
    nn.Linear(256, 10),
)
loss_fn = nn.CrossEntropyLoss()
opt = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
sched = torch.optim.lr_scheduler.OneCycleLR(opt, max_lr=1e-3, total_steps=10_000)

for step, (x, y) in enumerate(train_loader):
    logits = model(x)                 # forward pass
    loss = loss_fn(logits, y)         # səhvi ölç
    opt.zero_grad()                   # köhnə gradient-ləri sil
    loss.backward()                   # backpropagation
    nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    opt.step()                        # parametrləri yenilə
    sched.step()                      # learning rate-i yenilə
    if step % 500 == 0:
        print(step, loss.item(), evaluate(model, val_loader))

OneCycleLR warmup ilə cosine decay-i birləşdirir. evaluate isə üzərində loss-u və ya accuracy-ni hesablayan funksiyadır.

Training-də problem olanda ilk addımlar həmişə eynidir. Əvvəlcə model-in bir kiçik batch-ı əzbərləyə bildiyini yoxlayın: əzbərləyə bilmirsə, bug kodda və ya data-dadır, hiperparametrlərdə yox. Sonra loss əyrilərinə, gradient norm-una və learning rate-ə baxın. Loss birdən NaN olursa, adətən səbəb learning rate-in çox böyük olması, gradient partlaması və ya data-da səhv dəyərdir.

Tapşırıqlar

  1. Yuxarıdakı loop-u MNIST kimi kiçik dataset üzərində işə salın. Sonra learning rate-i 10 dəfə artırın və 10 dəfə azaldın. Hər üç halda training və validation loss əyrilərini bir qrafikdə çəkin və fərqi izah edin.
  2. cəmi 500 nümunə götürün və model-i uzun müddət train edin. başladığı addımı tapın, sonra decay və əlavə edib bu nöqtənin necə dəyişdiyini ölçün.
  3. Hesablayın: 1.5 milyard parametrli model-i float32-də AdamW ilə train etmək üçün , gradient-lər və state-i birlikdə nə qədər yaddaş tutur? Activation-lar bu hesaba niyə daxil deyil və onlar nədən asılıdır?