Вывод градиентов для слоёв наглядно
Урок выводит градиенты двухслойной сети и показывает, как ошибка передаётся из выходного слоя в скрытый.
Ключ к многослойному 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 это «понимает» автоматически.
Главная идея в одной фразе
Дельта старшего слоя, помноженная на веса, становится основой дельты предыдущего слоя. Так за один обратный проход мы получаем градиенты всех весов — именно этого мы добивались.
Итог
- Дельта нейрона = сигнал ошибки × производная его активации.
- Градиент веса = дельта нейрона-приёмника × активация нейрона-источника.
- Дельты скрытого слоя получаются из дельты выходного через веса между ними.