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 language model-i fine-tune etmək, retrieval üçün embedding 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ə training data-da əla, yeni data-da isə zəifdir.
Tensor-lar: hər şey ədəd massividir
Neural network-ün gördüyü yeganə şey ədədlərdir. Şəkil piksel dəyərlərindən ibarət cədvəl, mətn token 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: neural network 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ə (weight-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 weight 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ə neural network 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. Loss function 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.
Cross-entropy classification üçün istifadə olunur və language model-lərin əsas loss-udur. Model hər class üçün xam bal (logit) qaytarır. Softmax bu balları cəmi 1 olan ehtimallara çevirir. Cross-entropy 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. Language model training-də "class" lüğətdəki token-lərdən biridir. Növbəti token-i proqnozlaşdırmaq on minlərlə class-lı classification məsələsidir və onun loss-u cross-entropy-dir.
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 stochastic gradient descent (SGD) 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. Backpropagation 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 weight-lər 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ə SGD 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. Optimizer 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 weight decay-i düzgün tətbiq edən variantıdır və bu gün transformer 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 optimizer state-i üçün təxminən 56 GB yaddaş lazımdır (float32-də 7B × 2 × 4 byte). Fine-tuning 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 optimizer-lər, Adafactor) və daha yeni optimizer-lər (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 pretraining 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 training data-nı əzbərləmək deyil, görmədiyi data üzərində yaxşı işləməkdir, yəni generalization. Data-nı həmişə ən azı iki hissəyə bölün: training set və model-in heç vaxt öyrənmədiyi validation set. İki loss əyrisini birlikdə izləyin:
- İkisi də düşürsə, model öyrənir.
- Training loss düşür, validation loss isə qalxırsa, bu overfitting-dir: model training nümunələrinin xüsusiyyətlərini əzbərləyir.
- İkisi də yüksək qalırsa, bu underfitting-dir: model çox kiçikdir, learning rate səhvdir və ya data-da bir problem var.
Overfitting-ə qarşı əsas alətlər:
- Daha çox və daha müxtəlif data. Çox vaxt ən təsirli həll budur.
- Weight decay — hər addımda weight-ləri bir az sıfıra doğru çəkir. Model daha sadə həllərə üstünlük verir.
- Dropout — 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 language model-lərin pretraining-ində dropout ç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ə fine-tuning zamanı isə dropout yenidən faydalı olur.
- Early stopping — validation loss ən aşağı olduğu nöqtədəki checkpoint-i saxlamaq.
Initialization və normalization
Weight-lərin 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 weight-ləri 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. Transformer-lərdə 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 transformer daxilində görəcəyik.
Minimal training loop
Bütün parçaları birləşdirək. Aşağıdakı kod kiçik classifier-i 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ə validation set ü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
- 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.
- Training set-dən cəmi 500 nümunə götürün və model-i uzun müddət train edin. Overfitting-in başladığı addımı tapın, sonra weight decay və dropout əlavə edib bu nöqtənin necə dəyişdiyini ölçün.
- Hesablayın: 1.5 milyard parametrli model-i float32-də AdamW ilə train etmək üçün weight-lər, gradient-lər və optimizer 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?