Вывод градиентов для слоёв наглядно

Урок выводит градиенты двухслойной сети и показывает, как ошибка передаётся из выходного слоя в скрытый.

Ключ к многослойному backprop — величина дельта (delta): «сколько ошибки приходится на данный нейрон». Дельта выходного слоя порождает дельты скрытых слоёв.

Сеть, которую дифференцируем

Возьмём сеть 2 → 2 → 1 с sigmoid. Введём обозначения: h — активации скрытого слоя, out — выход, y — цель. Потеря — MSE одного примера: L = (out - y)^2. Нам нужны градиенты по весам обоих слоёв.

Шаг 1: дельта выходного нейрона

Для выходного нейрона перемножаем две локальные производные: d_out = (out - y) * out * (1 - out). Здесь (out - y) — из MSE (множитель 2 поглощаем в lr), а out*(1-out) — производная sigmoid. Эта дельта — «сигнал ошибки» выходного нейрона.

Шаг 2: градиенты весов выходного слоя

Производная потери по весу выходного слоя = дельта выхода × активация входящего скрытого нейрона: dL/dW2_j = d_out * h_j. По смещению: dL/db2 = d_out. Это прямое следствие правила dz/dw = вход.

Шаг 3: передаём ошибку в скрытый слой

Каждый скрытый нейрон j получает долю ошибки пропорционально весу, которым он соединён с выходом, и умножает её на свою производную активации: d_h_j = d_out * W2_j * h_j * (1 - h_j). Это и есть «распространение ошибки назад» — ответственность размазывается по скрытым нейронам.

Шаг 4: градиенты весов скрытого слоя

Дальше всё как на выходном слое: dL/dW1_jk = d_h_j * x_k, dL/db1_j = d_h_j.

Один шаг backprop в коде

import math
def sigmoid(z): return 1.0 / (1.0 + math.exp(-z))

x, y = [1.0, 0.0], 1.0
W1 = [[0.3, -0.2], [0.5, 0.4]]   # 2 скрытых нейрона
b1 = [0.0, 0.0]
W2 = [0.6, -0.7]                 # выходной нейрон
b2 = 0.1

# forward, запоминаем активации
h = [sigmoid(W1[j][0]*x[0] + W1[j][1]*x[1] + b1[j]) for j in range(2)]
out = sigmoid(W2[0]*h[0] + W2[1]*h[1] + b2)
print("Выход:", round(out, 4), " Потеря:", round((out - y)**2, 4))

# backward
d_out = (out - y) * out * (1 - out)
gW2 = [d_out * h[0], d_out * h[1]]
d_h = [d_out * W2[j] * h[j] * (1 - h[j]) for j in range(2)]
gW1 = [[d_h[j]*x[0], d_h[j]*x[1]] for j in range(2)]

print("Дельта выхода:", round(d_out, 4))
print("Градиенты W2:", [round(g, 4) for g in gW2])
print("Дельты скрытых:", [round(d, 4) for d in d_h])
print("Градиенты W1:", [[round(g,4) for g in row] for row in gW1])

Вывод:

Выход: 0.5022  Потеря: 0.2478
Дельта выхода: -0.1244
Градиенты W2: [-0.0715, -0.0775]
Дельты скрытых: [-0.0183, 0.0205]
Градиенты W1: [[-0.0183, -0.0], [0.0205, 0.0]]

Заметьте: градиенты по второму входу скрытого слоя нулевые, потому что x[1] = 0 — этот вход не повлиял на выход, и менять связанные с ним веса смысла нет. Backprop это «понимает» автоматически.

Главная идея в одной фразе

Дельта старшего слоя, помноженная на веса, становится основой дельты предыдущего слоя. Так за один обратный проход мы получаем градиенты всех весов — именно этого мы добивались.

Итог

  • Дельта нейрона = сигнал ошибки × производная его активации.
  • Градиент веса = дельта нейрона-приёмника × активация нейрона-источника.
  • Дельты скрытого слоя получаются из дельты выходного через веса между ними.
Проверьте себя
1. Что такое дельта (delta) нейрона в backprop?
AЕго вес
BСигнал ошибки, приходящийся на этот нейрон (× производную активации)
CЕго активация
DСкорость обучения нейрона
2. Как градиент веса связан с дельтой и активациями?
AГрадиент = дельта приёмника × активация источника
BГрадиент = дельта × скорость обучения
CГрадиент = активация / дельта
DГрадиент = сумма всех весов
3. Почему в примере градиенты по второму входу скрытого слоя нулевые?
AОшибка в коде
BПотому что x[1] = 0, и этот вход не влиял на выход
Csigmoid обнулила их
DТак всегда у второго входа
Поддержать проект