Производная: скорость изменения
Скорость изменения
Вы едете на машине. Путь — функция времени s(t). Спидометр показывает скорость — насколько быстро меняется путь прямо сейчас. Это и есть производная: s'(t).
В ML вместо пути — функция потерь L(w), а вместо времени — параметр модели w. Производная L'(w) отвечает на главный вопрос обучения: если чуть-чуть увеличить вес, ошибка вырастет или упадёт — и насколько сильно?
Определение
Сдвинем вход на крошечное h и посмотрим, во сколько раз изменился выход:
Дробь — наклон прямой через две близкие точки графика. Когда h \to 0, эта прямая превращается в касательную, а производная — в её наклон.
import numpy as np
import vis
f = lambda x: x ** 2
x0 = 1.5
for h in (1, 0.1, 0.01, 0.001, 1e-6):
print(f"h = {h:<8} наклон секущей = {(f(x0 + h) - f(x0)) / h:.6f}")
print("точная производная 2·x₀ =", 2 * x0)
x = np.linspace(-1, 3, 100)
slope = 2 * x0
vis.line(x, {"f(x) = x²": f(x), "касательная в x₀ = 1.5": f(x0) + slope * (x - x0)},
title="Производная — наклон касательной", xlabel="x", marker=(x0, f(x0)))
Касательная — лучшее линейное приближение функции рядом с точкой:
Эта формула — суть всего обучения: зная производную, мы предсказываем, как изменится ошибка при малом шаге.
Численная производная
Даже не зная формулы, производную можно посчитать: взять маленькое h и вычислить дробь. Точнее работает центральная разность:
import math
def derivative(f, x, h=1e-5):
return (f(x + h) - f(x - h)) / (2 * h)
print(derivative(lambda x: x ** 3, 2)) # ≈ 12
print(derivative(math.exp, 0)) # ≈ 1
print(derivative(math.log, 10)) # ≈ 0.1
Численная производная медленная (два вычисления функции на каждый параметр — для сети с миллионом весов это два миллиона прогонов) и неточная. Поэтому сети обучают с аналитическими производными, а численную используют для проверки: когда мы напишем свой движок автоматического дифференцирования, будем сверять его с численной производной — это называется gradient check.
Правила дифференцирования
| Правило | Формула |
|---|---|
| Константа | (c)' = 0 |
| Сумма | (f + g)' = f' + g' |
| Множитель | (c \cdot f)' = c \cdot f' |
| Произведение | (f g)' = f' g + f g' |
| Частное | \left(\frac{f}{g}\right)' = \frac{f'g - fg'}{g^2} |
| Композиция | (f(g(x)))' = f'(g(x)) \cdot g'(x) — правило цепочки, ему посвящён следующий урок |
Таблица производных
| f(x) | f'(x) |
|---|---|
| x^n | n x^{n - 1} |
| e^x | e^x — экспонента не меняется! |
| \ln x | 1/x |
| \sin x, \cos x | \cos x, -\sin x |
| \sigma(x) = \frac{1}{1 + e^{-x}} | \sigma(x)(1 - \sigma(x)) |
| \tanh x | 1 - \tanh^2 x |
| \text{ReLU}(x) = \max(0, x) | 1 при x > 0, 0 при x < 0 |
Пример: f(x) = 3x^2 - 4x + 1. По правилам суммы и множителя f'(x) = 3 \cdot 2x - 4 = 6x - 4.
Производные для нейросетей
Квадрат ошибки. Модель предсказала p, правильный ответ y, потеря L = (p - y)^2. Производная по p: L' = 2(p - y). Если предсказали больше, чем нужно (p > y), производная положительна: уменьшите p. Чем больше ошибка — тем больше производная.
Сигмоида. Её производная красиво выражается через неё саму: \sigma' = \sigma(1 - \sigma). Максимум — 0.25 в нуле, а при больших |z| производная почти ноль. Это важно: если нейрон с сигмоидой «насытился», градиент через него почти не проходит.
ReLU. Производная — 1 там, где нейрон активен, и 0, где нет. В нуле производной формально нет (излом), на практике берут 0.
import numpy as np
import vis
z = np.linspace(-6, 6, 300)
s = 1 / (1 + np.exp(-z))
vis.line(z, {"σ(z)": s, "σ′(z) = σ(1 − σ)": s * (1 - s)}, title="Сигмоида и её производная", xlabel="z")
vis.line(z, {"ReLU(z)": np.maximum(0, z), "ReLU′(z)": (z > 0).astype(float)}, title="ReLU и её производная", xlabel="z")
Знак производной и минимум
- f'(x) > 0 — функция растёт: чтобы уменьшить её, двигайтесь влево.
- f'(x) < 0 — функция убывает: двигайтесь вправо.
- f'(x) = 0 — плоское место: минимум, максимум или «полочка».
То есть, чтобы уменьшить функцию, двигайтесь против знака производной: x \leftarrow x - \eta f'(x) с маленьким шагом \eta. Это градиентный спуск в одну строку, и он сработает даже там, где уравнение f'(x) = 0 не решить на бумаге.
import numpy as np
import vis
f = lambda x: x ** 4 - 3 * x ** 2 + x
df = lambda x: 4 * x ** 3 - 6 * x + 1
x = np.linspace(-2.2, 2.2, 300)
vis.line(x, {"f(x)": f(x), "f′(x)": df(x)}, title="Минимумы f там, где f′ пересекает ноль снизу вверх", xlabel="x")
w = 2.0
for step in range(30):
w -= 0.02 * df(w) # шаг против производной
print(f"нашли минимум: x = {w:.4f}, f = {f(w):.4f}, f′ = {df(w):.6f}")
Обратите внимание: у этой функции два минимума, и спуск из точки 2.0 нашёл ближайший (локальный), а не самый глубокий. Об этом мы ещё поговорим.
Итог
- Производная f'(x) — скорость изменения функции и наклон касательной: f(x + \Delta) \approx f(x) + f'(x)\Delta.
- Численная производная \frac{f(x+h) - f(x-h)}{2h} медленная, но годится для проверки.
- Правила: сумма, множитель, произведение, частное; таблица: x^n, e^x, \ln x, \sigma, \tanh, ReLU.
- Чтобы уменьшить функцию, двигайтесь против знака производной.
Практика решено 0 из 6
Найдите производную f(x) = x^3 в точке x = 2.
Найдите производную f(x) = 5x^2 - 3x + 7 в точке x = 1.
В точке x_0 производная функции потерь L'(x_0) = -4. Куда нужно сдвинуть x, чтобы потери уменьшились?
Сопоставьте функцию и её производную.
| e^x | 1/x | 2x | \sigma(x)(1 - \sigma(x)) | 0 или 1 | |
|---|---|---|---|---|---|
| x^2 | |||||
| e^x | |||||
| \ln x | |||||
| \sigma(x) (сигмоида) | |||||
| \text{ReLU}(x), при x \ne 0 |
Численная производная. Напишите derivative(f, x, h=1e-5) — производную функции f в точке x
по центральной разности: \dfrac{f(x + h) - f(x - h)}{2h}.
Какие тесты проверяют код (2)
assert abs(derivative(lambda x: x ** 3, 2) - 12) < 1e-6 import math assert abs(derivative(math.exp, 1.0) - math.e) < 1e-6 assert abs(derivative(math.log, 4.0) - 0.25) < 1e-6 assert abs(derivative(math.sin, 0.0) - 1) < 1e-6
def derivative(f, x, h=1e-5):
return (f(x + h) - f(x - h)) / (2 * h)
Производные функций активации. Напишите аналитические (по формулам) производные:
d_sigmoid(z)— \sigma(z)(1 - \sigma(z));d_tanh(z)— 1 - \tanh^2 z (можноmath.tanh);d_relu(z)— 1 при z > 0, иначе 0.
Тесты сравнят их с численной производной.
Какие тесты проверяют код (3)
sig = lambda z: 1 / (1 + math.exp(-z))
num = lambda f, x: (f(x + 1e-6) - f(x - 1e-6)) / 2e-6
for z in (-3, -0.5, 0, 0.7, 4):
assert abs(d_sigmoid(z) - num(sig, z)) < 1e-6, f"d_sigmoid({z}) неверно"
assert abs(d_tanh(z) - num(math.tanh, z)) < 1e-6, f"d_tanh({z}) неверно"
assert d_relu(2.5) == 1 and d_relu(-1) == 0
assert abs(d_sigmoid(0) - 0.25) < 1e-12, "Максимум производной сигмоиды — 0.25 в нуле"
import math
def sigmoid(z):
return 1 / (1 + math.exp(-z))
def d_sigmoid(z):
s = sigmoid(z)
return s * (1 - s)
def d_tanh(z):
return 1 - math.tanh(z) ** 2
def d_relu(z):
return 1.0 if z > 0 else 0.0
Войдите, чтобы отмечать пройденные уроки.