Как да използвам PyTorch?

За да използваш PyTorch ефективно, ти трябват три неща: правилна инсталация (CPU/GPU), ясна „рамка“ за работа с tensors и устройства (CPU/CUDA/ROCm) и стабилен тренировъчен цикъл (данни → модел → loss → backward → optimizer). PyTorch е най-силен, когато пишеш „нормален“ Python, но измерваш всичко като инженер: скорост, памет и качество. По-долу е практичен път от нулата до първи работещ модел, плюс навици за по-сериозни проекти.

Въведение

PyTorch е deep learning framework, който е удобен за учене и мощен за реални системи. Той работи с tensors (многомерни масиви), автоматични градиенти (autograd) и модулен начин за дефиниране на модели (nn.Module). В 2.x линията PyTorch добави torch.compile, което може да оптимизира изпълнението (особено на GPU) без да пренаписваш модела.

Какво ще научиш:

  • как да инсталираш PyTorch правилно
  • как да местиш tensors/модел между CPU и GPU
  • как се пише training loop, който не „лъже“
  • как да запазваш/зареждаш модели и да правиш inference
  • как да ускоряваш и дебъгваш

Ако не можеш да повториш резултата си (seed, версии, данни), не си „тренирал модел“, а си направил демо.

Стъпка 1: Подготви среда (venv/conda) и базов проект

Направи си чиста среда, за да не се бориш със зависимости:

  • създай venv или conda env
  • сложи си requirements.txt или pyproject.toml
  • добави минимална структура:
    • train.py
    • data.py
    • model.py
    • eval.py

Това ти спестява хаос още на втория ден.

Стъпка 2: Инсталирай правилния build (CPU, CUDA или ROCm)

Най-сигурният начин е да ползваш selector-а на официалния сайт и да копираш командата за твоята ОС и compute платформа.

Практически сценарии:

  • CPU-only (за учене и прототипи): най-лесен, най-малко зависимости.
  • CUDA build (NVIDIA GPU): избираш CUDA версията, която съответства на драйвърите/системата.
  • ROCm build (AMD GPU): избираш поддържана ROCm версия.

Мини-чек след инсталация:

  • импортира ли се torch без грешки?
  • вижда ли GPU?

Стъпка 3: Провери инсталацията и устройството (device)

Пусни:

import torch
print(torch.__version__)
print('cuda available:', torch.cuda.is_available())
if torch.cuda.is_available():
    print('gpu:', torch.cuda.get_device_name(0))

После тествай tensor:

x = torch.rand(5, 3)
print(x.shape, x.dtype, x.device)

Основна идея: почти всичко „живее“ или на CPU, или на GPU. Ако входът е на GPU, моделът трябва да е на GPU.

Полезен шаблон:

device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = model.to(device)
X = X.to(device)

Стъпка 4: Научи минималния „език“ на PyTorch (tensors + shapes)

Това са навиците, които спестяват дни дебъг:

  • мисли в shape (напр. batch x features или batch x channels x height x width)
  • проверявай dtype (float32, float16, bfloat16)
  • знаи къде е tensor-ът: x.device

Чести операции:

  • reshape/view за промяна на размери
  • permute за размяна на оси
  • cat/stack за слепване

Пример:

x = torch.randn(32, 100)       # batch=32
w = torch.randn(100, 10)
logits = x @ w                 # (32, 10)

Стъпка 5: Разбери autograd: loss → backward() → optimizer

autograd пази граф на операциите и може да сметне градиенти.

Минимумът:

  • дефинираш loss
  • извикваш loss.backward()
  • optimizer прави step()

Важно:

  • винаги нулирай градиентите: optimizer.zero_grad(set_to_none=True)
  • в evaluation използвай torch.no_grad()

Стъпка 6: Дефинирай модел с nn.Module (и валидирай входа)

Стандартният подход:

  • дефинираш __init__ със слоеве
  • дефинираш forward

Пример (MLP):

import torch
import torch.nn as nn

class MLP(nn.Module):
    def __init__(self, in_dim=100, hidden=256, out_dim=10):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, out_dim)
        )

    def forward(self, x):
        return self.net(x)

Стабилният модел започва със стабилен вход: валидирай размерите и типовете още в началото.

Полезен трик: още в forward понякога е ок да сложиш assert-и при разработка (после ги махаш).

Стъпка 7: Подготви данни с Dataset и DataLoader

Dataset описва как взимаш пример по индекс, а DataLoader управлява batching, shuffle и паралелно зареждане.

Скелет:

from torch.utils.data import Dataset, DataLoader

class MyDataset(Dataset):
    def __init__(self):
        self.X = ...
        self.y = ...

    def __len__(self):
        return len(self.X)

    def __getitem__(self, idx):
        return self.X[idx], self.y[idx]

loader = DataLoader(MyDataset(), batch_size=64, shuffle=True, num_workers=2)

Съвети:

  • започни с num_workers=0, после увеличи
  • за GPU обучение често помага pin_memory=True
  • ако данните са изображения, разгледай torchvision.datasets + transforms

Стъпка 8: Напиши training loop, който е коректен (train/eval + метрики)

Минимален, но реалистичен цикъл:

import torch
import torch.nn as nn

device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = MLP().to(device)
opt = torch.optim.AdamW(model.parameters(), lr=3e-4)
loss_fn = nn.CrossEntropyLoss()

for epoch in range(5):
    model.train()
    total_loss = 0.0
    for X, y in loader:
        X, y = X.to(device), y.to(device)
        opt.zero_grad(set_to_none=True)
        logits = model(X)
        loss = loss_fn(logits, y)
        loss.backward()
        opt.step()
        total_loss += loss.item()

    model.eval()
    with torch.no_grad():
        # сметни метрики на validation
        pass
    print('epoch', epoch, 'loss', total_loss/len(loader))

Правила за здрав разум:

  • model.train() включва dropout/batchnorm поведение
  • model.eval() + torch.no_grad() за стабилна оценка
  • пази метрики по епохи (loss, accuracy) и гледай тренд

Стъпка 9: Запази модел и направи inference

Най-чистият навик в PyTorch е да пазиш state_dict:

torch.save(model.state_dict(), 'model.pt')

И да зареждаш така:

model = MLP()
model.load_state_dict(torch.load('model.pt', map_location='cpu'))
model.eval()

За inference:

  • model.eval()
  • torch.no_grad()
  • стабилен препроцесинг и постпроцесинг

Стъпка 10: Ускорение (mixed precision и torch.compile)

Mixed precision

На много GPU-и mixed precision дава голям speedup. Подходът е:

  • torch.cuda.amp.autocast() за forward
  • GradScaler за стабилни градиенти при fp16

torch.compile

В PyTorch 2.x можеш да компилираш модела с:

model = torch.compile(model)

Документацията описва, че torch.compile използва стека TorchDynamo/TorchInductor и може да ускорява тренировка и inference без промяна на модела. Важно е да знаеш, че поддръжката на най-новите версии на Python може да изостава, затова за production избирай поддържан Python и тествай на реални данни.

Първо направи модела правилен, после го прави бърз. Оптимизация върху грешна задача е загуба на време.

Стъпка 11: Дебъг и стабилизирай обучението

Когато нещо „не учи“:

  • провери данните (labels, клас дисбаланс, normalization)
  • провери learning rate (често е твърде голям)
  • провери дали loss намалява на малък subset (overfit test)
  • логвай градиенти и активирания, ако има NaN/inf

Полезен тест: вземи 128 примера и виж дали моделът може да overfit-не. Ако не може, проблемът е в модела/данните/кода.

Съвети за по-добри резултати

  • Ползвай AdamW като базов optimizer и започни с малък learning rate.
  • Винаги отделяй validation set и не „пипай“ теста до края.
  • Запиши seed и версии (PyTorch, CUDA) за повторяемост.
  • Измервай speed: време за епоха, GPU utilization, memory.
  • Ако имаш NaN/inf, провери mixed precision, learning rate и данните.

Чести грешки, които да избягваш

  • Смесване на устройства: част от tensor-ите са на CPU, моделът е на GPU.
  • Грешни shape-ове (особено при CNN/Transformer входове).
  • Липса на model.eval() при оценка, което влошава метриките.
  • Нулиране на градиентите в грешния момент.
  • Липса на логване: не знаеш кога и защо се „чупи“ обучението.

Източници и полезни линкове (проверени 2025–2026)

Как да направиш проекта повторяем (reproducible)

Повторяемостта е разликата между „случайно се получи“ и инженерство. Минимален чеклист:

  • фиксирай seed за Python/NumPy/PyTorch
  • логвай версии (PyTorch, CUDA/ROCm, драйвър)
  • записвай конфигурацията на експеримента (batch size, lr, архитектура)
  • отделяй данните на train/val/test по стабилен начин

Пример за seed (не гарантира 100% детерминизъм, но помага):

import random
import numpy as np
import torch

seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
    torch.cuda.manual_seed_all(seed)

Checkpointing: запазвай „най-добрия“ модел, не само последния

Добра практика е да пазиш checkpoint при най-добра validation метрика. Така не губиш време, ако последните епохи започнат да overfit-ват:

best_acc = -1.0
for epoch in range(num_epochs):
    # train ...
    val_acc = ...
    if val_acc > best_acc:
        best_acc = val_acc
        torch.save({
            "epoch": epoch,
            "model": model.state_dict(),
            "opt": opt.state_dict(),
            "best_acc": best_acc,
        }, "checkpoint.pt")

Как да избегнеш OOM (out-of-memory) на GPU

Ако ти свършва GPU паметта:

  • намали batch size
  • включи mixed precision
  • ползвай gradient accumulation (симулираш голям batch)
  • изключи излишни tensor-и от графа (внимавай с detach() и no_grad() при оценка)
  • профилирай dtype (float32 vs float16/bfloat16)

Практичен подход: първо направи базовия модел да работи стабилно на малък batch, после увеличавай и оптимизирай.

Transfer learning: започни от pretrained модел

В практиката често не тренираш „от нула“. Вместо това взимаш pretrained backbone (например за изображения или текст), замразяваш част от слоевете и дообучаваш само последните. Това дава по-добро качество с по-малко данни и по-малко време. Типичен подход:

  • зареди pretrained модел от подходяща библиотека
  • замрази параметрите: p.requires_grad = False
  • добави нов „head“ (класификатор)
  • тренирай само head-а, после размрази част от backbone-а за finetune

Learning rate scheduler (когато обучението „засече“)

Ако loss спре да пада, scheduler често помага. В PyTorch имаш torch.optim.lr_scheduler с готови стратегии (step, cosine, reduce-on-plateau). Важно: логвай текущия learning rate, за да знаеш какво се случва и защо.

Кога да използваш „високо ниво“ библиотека (напр. Lightning)

Когато вече разбираш базовия training loop, high-level библиотека може да ти спести boilerplate (логване, чекпойнти, distributed training). Ползвай я, ако имаш стандартен supervised pipeline и искаш бърза итерация. Ако правиш нетипичен алгоритъм или много custom логика, остани на „чист“ PyTorch, докато стабилизираш идеята.

Бележка: Започни с малък модел, измервай, и итерай системно.