Правило цепочки

⏱ 35 мин · 🧩 заданий: 7

Композиция и её производная

Нейросеть — композиция функций: вход → слой → активация → слой → … → потеря. Нам нужна производная потери по каждому весу, а вес спрятан глубоко внутри. Как продифференцировать f(g(x))?

Правило цепочки:

\big(f(g(x))\big)' = f'(g(x)) \cdot g'(x)

Или в обозначениях Лейбница, где u = g(x), y = f(u):

\frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx}

Производные звеньев перемножаются.

Интуиция: шестерёнки

Если шестерёнка u крутится в 3 раза быстрее x, а y — в 2 раза быстрее u, то y крутится в 2 \cdot 3 = 6 раз быстрее x. Каждое звено передаёт изменение дальше, умножая его на свой «коэффициент передачи» — локальную производную.

x ──(×3)──▶ u = 3x ──(×2)──▶ y = 2u dy/dx = 2 · 3 = 6

Пример

f(x) = (3x + 1)^2. Внутренняя функция u = 3x + 1, внешняя y = u^2:

\frac{dy}{du} = 2u, \quad \frac{du}{dx} = 3 \quad\Rightarrow\quad f'(x) = 2(3x + 1) \cdot 3 = 6(3x + 1)
f = lambda x: (3 * x + 1) ** 2
df = lambda x: 6 * (3 * x + 1)

for x in (0.0, 1.0, -2.0):
    num = (f(x + 1e-6) - f(x - 1e-6)) / 2e-6
    print(f"x = {x}: по формуле {df(x)}, численно {num:.6f}")

Ещё примеры:

  • e^{2x}: \;(e^u)' \cdot (2x)' = e^{2x} \cdot 2;
  • \ln(x^2 + 1): \;\frac{1}{x^2 + 1} \cdot 2x;
  • \sigma(wx + b) по w: \;\sigma'(wx + b) \cdot x = \sigma(1 - \sigma) \cdot x.

Длинные цепочки

Звеньев может быть сколько угодно — производные всё равно перемножаются:

y = f_3(f_2(f_1(x))) \quad\Rightarrow\quad \frac{dy}{dx} = f_3' \cdot f_2' \cdot f_1'
import vis

vis.graph(
    {"x": "x", "u1": "u₁ = f₁(x)", "u2": "u₂ = f₂(u₁)", "y": "y = f₃(u₂)"},
    [("x", "u1"), ("u1", "u2"), ("u2", "y")],
    title="dy/dx = f₃′(u₂) · f₂′(u₁) · f₁′(x)",
)

Чтобы посчитать такое произведение, нужно знать значения в каждом звене (u_1, u_2) — они нужны для локальных производных. Поэтому вычисления делают в два прохода:

  1. Прямой проход (forward): идём от x к y, вычисляем и запоминаем все промежуточные значения.
  2. Обратный проход (backward): идём от y назад к x, перемножая локальные производные.

Обратный проход и называется обратным распространением ошибки (backpropagation). Почему именно назад, а не вперёд, — станет ясно в уроке о графе вычислений: так за один проход получаются производные по всем параметрам сразу.

Разбор: производная потерь нейрона

Возьмём нейрон с одним входом и квадратичной потерей:

z = wx + b, \qquad p = \sigma(z), \qquad L = (p - y)^2

Хотим \frac{\partial L}{\partial w} и \frac{\partial L}{\partial b}.

w ─┐ x ─┼─▶ z = w·x + b ──▶ p = σ(z) ──▶ L = (p − y)² b ─┘

Прямой проход при w = 0.5, b = -0.2, x = 2, y = 1:

  • z = 0.5 \cdot 2 - 0.2 = 0.8
  • p = \sigma(0.8) \approx 0.690
  • L = (0.690 - 1)^2 \approx 0.096

Обратный проход — локальные производные от конца к началу:

Звено Локальная производная Значение
L = (p - y)^2 \frac{\partial L}{\partial p} = 2(p - y) -0.620
p = \sigma(z) \frac{dp}{dz} = p(1 - p) 0.214
z = wx + b \frac{\partial z}{\partial w} = x, \frac{\partial z}{\partial b} = 1 2 и 1

Перемножаем:

\frac{\partial L}{\partial z} = -0.620 \cdot 0.214 \approx -0.133, \qquad \frac{\partial L}{\partial w} = -0.133 \cdot 2 \approx -0.265, \qquad \frac{\partial L}{\partial b} = -0.133

Обе производные отрицательны: увеличение w и b уменьшит потерю. И правда — нейрон выдал 0.69 вместо 1, ему нужно «загореться» сильнее.

import math

w, b, x, y = 0.5, -0.2, 2.0, 1.0

# прямой проход
z = w * x + b
p = 1 / (1 + math.exp(-z))
L = (p - y) ** 2
print(f"z = {z:.3f}, p = {p:.3f}, L = {L:.4f}")

# обратный проход
dL_dp = 2 * (p - y)
dp_dz = p * (1 - p)
dL_dz = dL_dp * dp_dz
dL_dw, dL_db = dL_dz * x, dL_dz * 1
print(f"∂L/∂w = {dL_dw:.4f}, ∂L/∂b = {dL_db:.4f}")

# проверка численной производной
loss = lambda w, b: (1 / (1 + math.exp(-(w * x + b))) - y) ** 2
h = 1e-6
print(f"численно: {(loss(w + h, b) - loss(w - h, b)) / (2 * h):.4f}, {(loss(w, b + h) - loss(w, b - h)) / (2 * h):.4f}")

# шаг градиентного спуска
lr = 1.0
w2, b2 = w - lr * dL_dw, b - lr * dL_db
print(f"после шага: L = {loss(w2, b2):.4f} (было {L:.4f})")

Один шаг против градиента — и потеря уменьшилась. Повторите это сотни раз для всех объектов — и нейрон обучится. Именно это вы сделаете в заданиях.

Затухающие градиенты

В глубокой сети с сигмоидой в цепочке много множителей \sigma'(z) \le 0.25. Произведение двадцати таких чисел — меньше 10^{-12}: до первых слоёв градиент почти не доходит, и они не учатся. Это проблема затухающих градиентов. Обратная беда — взрывающиеся градиенты, когда множители больше 1.

import numpy as np
import vis

depth = np.arange(1, 31)
vis.line(depth, {"сигмоида (множитель 0.25)": 0.25 ** depth, "tanh (до 1, обычно ~0.6)": 0.6 ** depth, "ReLU (1 у активных)": np.ones(30)},
         title="Масштаб градиента после n слоёв", xlabel="число слоёв", logy=True)

Поэтому в современных сетях используют ReLU и её родственников (у них производная 1), аккуратную инициализацию весов, нормализацию (BatchNorm, LayerNorm) и остаточные связи. Всем этим темам будут посвящены отдельные уроки.

Итог

  • Правило цепочки: производная композиции — произведение производных звеньев, \frac{dy}{dx} = \frac{dy}{du}\frac{du}{dx}.
  • Для вычисления нужны промежуточные значения: сначала прямой проход, затем обратный — это backpropagation.
  • Производная потерь нейрона: \frac{\partial L}{\partial w} = 2(p - y) \cdot p(1-p) \cdot x.
  • Длинные цепочки множителей меньше 1 приводят к затухающим градиентам.

Практика решено 0 из 7

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Ввод ответа

Найдите производную f(x) = (3x + 1)^2 в точке x = 1.

Задание 2 Ввод ответа

Найдите производную f(x) = e^{2x} в точке x = 0.

Задание 3 Ввод ответа

Найдите производную f(x) = \ln(x^2 + 1) в точке x = 1.

Задание 4 Один вариант ответа

В цепочке L(\sigma(z(w))) известно: \frac{\partial L}{\partial \sigma} = 0.4, \frac{d\sigma}{dz} = 0.25, \frac{dz}{dw} = 3. Чему равна \frac{dL}{dw}?

Задание 5 Один вариант ответа

Сеть из 20 слоёв с сигмоидой. Производная сигмоиды не больше 0.25. Что примерно происходит с градиентом первого слоя?

Задание 6 Программа на Python

Градиент нейрона. Нейрон с одним входом: z = wx + b, p = \sigma(z), потеря L = (p - y)^2. Напишите neuron_grads(w, b, x, y), возвращающую пару \left(\frac{\partial L}{\partial w}, \frac{\partial L}{\partial b}\right), по правилу цепочки:

\frac{\partial L}{\partial w} = \underbrace{2(p - y)}_{\partial L / \partial p} \cdot \underbrace{p(1 - p)}_{dp/dz} \cdot \underbrace{x}_{\partial z / \partial w}, \qquad \frac{\partial L}{\partial b} = 2(p - y) \cdot p(1 - p) \cdot 1
Какие тесты проверяют код (1)
def loss(w, b, x, y):
    p = 1 / (1 + math.exp(-(w * x + b)))
    return (p - y) ** 2
h = 1e-6
for w, b, x, y in [(0.5, -0.2, 2.0, 1.0), (-1.0, 0.3, 0.5, 0.0), (2.0, 1.0, -1.5, 1.0)]:
    gw, gb = neuron_grads(w, b, x, y)
    nw = (loss(w + h, b, x, y) - loss(w - h, b, x, y)) / (2 * h)
    nb = (loss(w, b + h, x, y) - loss(w, b - h, x, y)) / (2 * h)
    assert abs(gw - nw) < 1e-6, f"∂L/∂w неверно для {(w, b, x, y)}: {gw} vs численно {nw}"
    assert abs(gb - nb) < 1e-6, f"∂L/∂b неверно для {(w, b, x, y)}: {gb} vs численно {nb}"

Задание 7 Программа на Python

Обучаем нейрон. Используя neuron_grads из прошлого задания (скопируйте её), напишите train_neuron(data, lr=1.0, epochs=200): начните с w = 0, b = 0, и epochs раз пройдите по всем парам (x, y) из data, делая шаг w -= lr * gw; b -= lr * gb после каждой пары. Верните (w, b).

Данные: «x > 1 → 1, иначе 0». Обученный нейрон должен правильно классифицировать все точки (σ(wx + b) > 0.5 ⇔ y = 1).

Какие тесты проверяют код (1)
data = [(-1.0, 0), (0.0, 0), (0.5, 0), (0.8, 0), (1.3, 1), (1.6, 1), (2.0, 1), (3.0, 1)]
w, b = train_neuron(data)
sig = lambda z: 1 / (1 + math.exp(-z))
wrong = [(x, y) for x, y in data if (sig(w * x + b) > 0.5) != (y == 1)]
assert not wrong, f"Ошибки на точках {wrong}; w={w:.3f}, b={b:.3f}"
assert w > 0, "Вес должен стать положительным: чем больше x, тем ближе к классу 1"

Войдите, чтобы отмечать пройденные уроки.