Gradientní sestup a zpětná propagace chyby: Trénink neuronových sítí od základů v CUDA/C++
Tento článek je čtvrtou částí hlubokého ponoru do světa neuronových sítí, kde krok za krokem budujeme architekturu Transformer, počínaje těmi nejzákladnějšími komponentami v CUDA. V tomto díle přecházíme od implementace dopředného průchodu ke klíčové fázi – tréninku modelu. Rozebereme si fundamentální mechanismy gradientního sestupu (Gradient Descent) a zpětné propagace chyby (Backpropagation), implementujeme je na nízké úrovni s použitím C++ a CUDA, abychom naučili naši neuronovou síť rozpoznávat ručně psané číslice z datasetu MNIST. Cílem není jen používat hotové knihovny, ale do hloubky pochopit, jak tyto složité výpočetní procesy fungují „pod kapotou“, což je kriticky důležité pro hluboké porozumění moderním modelům strojového učení a umělé inteligence.
Základy tréninku neuronových sítí: Gradientní sestup
Poté, co jsme v předchozí části úspěšně implementovali dopředný průchod, je náš model schopen provádět predikce. Na počátku, bez tréninku, však budou tyto predikce náhodné. Aby model začal poskytovat správné výsledky, je nutné upravit jeho vnitřní parametry (váhy a bias) tak, aby se minimalizovala chyba mezi predikcemi a skutečnými štítky. Právě k tomuto účelu se používají algoritmy jako gradientní sestup a zpětná propagace chyby.
Představte si, že se nacházíte na vrcholu hory v husté mlze a vaším úkolem je sestoupit do nejnižšího bodu údolí. Nevidíte celou krajinu, ale cítíte sklon pod nohama. Gradient je vektor ukazující směr největšího stoupání. Abyste sestoupili, musíte se pohybovat v opačném směru než gradient. V kontextu neuronových sítí je „hora“ povrchem chybové funkce (nebo ztrátové funkce) a „údolí“ je jejím globálním minimem. Cílem gradientního sestupu je najít toto minimum iterativním upravováním vah sítě ve směru opačném ke gradientu chybové funkce vzhledem k těmto vahám.
Zpětná propagace chyby (Backpropagation) je algoritmus, který umožňuje efektivně vypočítat tyto gradienty. Funguje tak, že chybu přenáší z výstupní vrstvy sítě zpět k vstupní, čímž rozděluje odpovědnost za chybu mezi všechny váhy. Matematicky je to realizováno pomocí řetězového pravidla (Chain Rule) derivace, které umožňuje vypočítat, jak změna každé váhy ovlivňuje celkovou chybu modelu.
Ztrátová funkce: Křížová entropie
Pro měření „špatnosti“ predikcí modelu se používá ztrátová funkce. V klasifikačních úlohách, jako je rozpoznávání ručně psaných číslic MNIST, je standardní volbou křížová entropie (Cross-Entropy Loss). Umožňuje kvantitativně vyhodnotit rozdíl mezi distribucí pravděpodobností předpovězenou modelem a skutečnou distribucí (kde pouze správná třída má pravděpodobnost 1 a ostatní 0). Čím větší je tento rozdíl, tím vyšší je hodnota ztrátové funkce.

Kde:
-  — počet tříd (pro MNIST je to 10).
-  — skutečná hodnota (1 pro správnou třídu, 0 pro ostatní).
-  — predikce modelu (pravděpodobnost získaná po vrstvě Softmax).
Jelikož se trénink obvykle provádí na dávkách (batchích) dat, nikoli na jednotlivých příkladech, celková ztrátová funkce pro dávku se zprůměruje podle její velikosti ( — velikost dávky):

Níže je uveden kód v C++ pro výpočet křížové entropie na CPU, což zjednodušuje ladění a porozumění:
float cross_entropy_loss_cpu(
const Tensor& predictions, const std::vector<int>& labels
) {
int batch_size = predictions.shape()[0];
int num_classes = predictions.shape()[1];
float loss = 0.0f;
for (int i = 0; i < batch_size; i++) {
for (int j = 0; j < num_classes; j++) {
float prob = predictions.get({i, j});
float target = (labels[i] == j) ? 1.0f : 0.0f;
loss += -std::log(prob) * target;
}
}
return loss / batch_size;
}
Zpětná propagace a řetězové pravidlo
Abychom minimalizovali ztrátovou funkci , musíme vědět, jak se její hodnota mění při malé změně každé váhy v síti. To je právě gradient ztrátové funkce vzhledem k vahám. Například pro váhu  první vrstvy potřebujeme najít .
Architektura naší sítě, vyvinutá v předchozích článcích, vypadá následovně:
- První lineární vrstva: 
- Aktivace (ReLU): 
- Druhá lineární vrstva: 
- Výstup (Softmax + Loss): 
Pro výpočet gradientu  aplikujeme řetězové pravidlo (Chain Rule):

Každý člen v tomto řetězci představuje lokální gradient, který ukazuje, jak výstup dané vrstvy nebo funkce ovlivňuje její vstup. Zpětná propagace začíná výpočtem gradientu ztrátové funkce vzhledem k výstupům poslední vrstvy a poté postupně předává tento gradient zpět přes všechny vrstvy sítě.
Detailní odvození gradientů pro Softmax a křížovou entropii
Začněme od samého konce řetězce – derivací ztrátové funkce  vzhledem k výstupům druhé lineární vrstvy  po aplikaci Softmax. Toto je jeden z nejdůležitějších a nejčastěji používaných gradientů. Připomeňme si, jak vypadá Softmax pro každý prvek  ve vektoru :

Kde  je výstup druhé lineární vrstvy (logity) a  je výsledek Softmaxu (pravděpodobnosti). Gradient  lze rozložit podle řetězového pravidla:

Nejprve najdeme derivaci ztrátové funkce  vzhledem k :

Nyní přejdeme k derivaci Softmaxu vzhledem k . Zde bude potřeba pravidlo pro derivaci podílu. Vzorec Softmaxu: 
Pokud , derivace je rovna:

Pokud , derivace je rovna:

Pomocí Kroneckerova symbolu  lze tyto dva případy sjednotit:

Kombinací derivací křížové entropie a Softmaxu získáme:

Další zjednodušení, s přihlédnutím k tomu, že součet skutečných pravděpodobností  je vždy roven 1, vede k překvapivě jednoduchému a elegantnímu vzorci:

To znamená, že gradient ztrátové funkce vzhledem k logitům (výstupům před Softmaxem) je jednoduše rozdíl mezi předpovězenými pravděpodobnostmi a skutečnými štítky. Tento výsledek je základním kamenem pro implementaci zpětné propagace chyby v klasifikačních modelech.
Implementace gradientu v C++
Použitím odvozeného vzorce můžeme implementovat výpočet gradientu pro poslední vrstvu naší neuronové sítě. Tento gradient bude následně použit k aktualizaci vah, čímž se model posune směrem k menší chybě. Zde je, jak to vypadá v kódu C++:
auto grad = Tensor::create_zeros({ batch_size, num_classes });
for (int i = 0; i < batch_size; ++i) {
for (int j = 0; j < num_classes; ++j) {
float prob = fc2.output->get({i, j});
float target = (targets[i] == j) ? 1.0f : 0.0f;
float gradient = (prob - target) / batch_size; // Dělení na batch_size pro zprůměrování
grad->set({i, j}, gradient);
}
}
Tento kód vypočítá gradient chyby pro každý prvek v dávce a poté jej zprůměruje podle velikosti dávky. Výsledný tenzor grad obsahuje informace o tom, o kolik je třeba změnit logity , aby se chyba snížila. Dále bude tento gradient propagován přes předchozí vrstvy sítě, což umožní korigovat váhy , ,  a  s použitím odpovídajících lokálních derivací.
Co je důležité:
- Gradientní sestup — je iterační optimalizační metoda, která upravuje parametry modelu ve směru opačném ke gradientu ztrátové funkce, aby nalezla její minimum.
- Zpětná propagace chyby (Backpropagation) — algoritmus, který využívá řetězové pravidlo pro efektivní výpočet gradientů ztrátové funkce vzhledem ke všem vahám neuronové sítě.
- Křížová entropie — standardní ztrátová funkce pro klasifikační úlohy, která měří rozdíl mezi předpovězenými a skutečnými distribucemi pravděpodobností.
- Derivace Softmax + Křížová entropie vzhledem k logitům se zjednodušuje na rozdíl mezi předpovězenými pravděpodobnostmi a skutečnými štítky, což výrazně zjednodušuje implementaci.
- Implementace od základů v C++/CUDA umožňuje hluboké porozumění vnitřním mechanismům fungování modelů, jako je Transformer, bez použití vysokoúrovňových abstrakcí.
— Editorial Team
Zatím žádné komentáře.