Дополнительные упражнения: производные и градиенты

⏱ 45 мин · 🧩 заданий: 10

Разобранный пример: градиент softmax + кросс-энтропии

Это самая важная производная в классификации, выведем её. Логиты z, вероятности p = \text{softmax}(z), правильный класс y:

L = -\ln p_y = -z_y + \ln\sum_j e^{z_j}

Производная по логиту z_k:

  • первое слагаемое -z_y даёт -1, если k = y, и 0 иначе;
  • второе по правилу цепочки: \frac{1}{\sum_j e^{z_j}} \cdot e^{z_k} = p_k.

Итог:

\frac{\partial L}{\partial z_k} = p_k - [k = y] \qquad\Longleftrightarrow\qquad \nabla_z L = p - \text{onehot}(y)

Смысл очевиден: каждый логит толкается пропорционально ошибке в его вероятности. Для правильного класса p_y - 1 < 0 — логит растёт; для неправильных p_k > 0 — логиты уменьшаются, и сильнее те, кому модель зря дала большую вероятность. Для батча из N объектов градиент среднего — то же, делённое на N.

import numpy as np

z = np.array([2.0, 1.0, 0.1])
y = 1
p = np.exp(z - z.max()); p /= p.sum()
grad = p - np.eye(3)[y]
print("вероятности:", p.round(3))
print("градиент:   ", grad.round(3))

def loss(z):
    z = z - z.max()
    return -(z[y] - np.log(np.exp(z).sum()))
num = np.array([(loss(z + e) - loss(z - e)) / 2e-6 for e in np.eye(3) * 1e-6])
print("численно:   ", num.round(3))

Ту же форму «предсказание минус ответ» имеет градиент MSE по предсказанию (2(\hat y - y)) и градиент бинарной кросс-энтропии с сигмоидой по логиту (p - y). Это не совпадение: такие пары «функция выхода + потеря» специально подобраны, чтобы градиент был простым и не затухал.

Разобранный пример: градиентный спуск в овраге

Посмотрим, как спуск ведёт себя на функции Розенброка из последнего задания. Шаг приходится брать маленьким: по одному направлению функция очень крутая (стенки оврага), и большой шаг перебрасывает через овраг. А вдоль дна она почти плоская, поэтому с маленьким шагом движение медленное. Эту проблему решают оптимизаторы с инерцией и адаптивным шагом (Momentum, Adam) — им будет посвящён отдельный урок.

import numpy as np
import vis

f = lambda p: (1 - p[0]) ** 2 + 100 * (p[1] - p[0] ** 2) ** 2
grad = lambda p: np.array([-2 * (1 - p[0]) - 400 * p[0] * (p[1] - p[0] ** 2), 200 * (p[1] - p[0] ** 2)])

for lr in (0.001, 0.0015, 0.003):
    p = np.array([-1.0, 1.0])
    for step in range(3000):
        p = p - lr * grad(p)
        if not np.all(np.isfinite(p)) or np.abs(p).max() > 1e6:
            print(f"lr = {lr}: разошёлся на шаге {step}")
            break
    else:
        print(f"lr = {lr}: через 3000 шагов p = {p.round(3)}, f = {f(p):.5f}")

p = np.array([-1.0, 1.0]); path = [p]
for _ in range(5000):
    p = p - 0.001 * grad(p); path.append(p)
path = np.array(path)
vis.line(path[:, 0], path[:, 1], title="Путь спуска: быстро на дно оврага, потом медленно вдоль него", xlabel="x", ylabel="y", points=path[::250])

Шпаргалка по модулю

Понятие Формула
Производная f'(x) = \lim_{h\to 0}\frac{f(x+h) - f(x)}{h}, f(x + \Delta) \approx f(x) + f'(x)\Delta
Численно \frac{f(x + h) - f(x - h)}{2h}
Правило цепочки (f(g(x)))' = f'(g(x))\,g'(x)
Таблица (x^n)' = nx^{n-1}, (e^x)' = e^x, (\ln x)' = 1/x
Сигмоида \sigma' = \sigma(1 - \sigma), максимум 0.25
tanh, ReLU 1 - \tanh^2; [x > 0]
Градиент \nabla f = (\partial f/\partial x_1, \dots) — направление быстрейшего роста
MSE линейной модели \nabla_\theta = \frac{2}{N}X^T(X\theta - y)
Softmax p_k = e^{z_k} / \sum_j e^{z_j} (вычитайте максимум)
Кросс-энтропия L = -\ln p_y, \nabla_z L = p - \text{onehot}(y)
Бинарная КЭ \nabla_w = \frac1N X^T(p - y)
Градиентный спуск \theta \leftarrow \theta - \eta \nabla L

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

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

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

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

Найдите производную f(x) = \sqrt{x} в точке x = 4.

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

Найдите производную f(x) = \sigma(3x) в точке x = 0 (\sigma — сигмоида).

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

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

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

Чему равна \frac{\partial}{\partial w_2} от L = (w_1 x_1 + w_2 x_2 - y)^2 при w = (1, 2), x = (3, 1), y = 4?

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

Найдите градиент f(x, y) = e^{x} \sin y + y^2 в точке (0, 0). Два числа через запятую.

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

Логиты z = [2, 0, 0], правильный класс — 0. Какой знак у ∂L/∂z₀ для кросс-энтропии?

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

Градиент кросс-энтропии по логитам. Для одного объекта с логитами z (вектор длины C) и правильным классом y потеря L = -\ln \text{softmax}(z)_y. Её градиент поразительно прост:

\frac{\partial L}{\partial z} = \text{softmax}(z) - \text{onehot}(y)

Напишите ce_grad(z, y) по этой формуле (вычисляйте softmax устойчиво). Тесты сверят результат с численным градиентом.

Какие тесты проверяют код (2)
def loss(z, y):
    z = z - z.max()
    return -(z[y] - np.log(np.exp(z).sum()))
rng = np.random.default_rng(0)
for _ in range(5):
    z, y = rng.normal(size=4), int(rng.integers(4))
    num = np.array([(loss(z + e, y) - loss(z - e, y)) / 2e-6 for e in np.eye(4) * 1e-6])
    assert np.allclose(ce_grad(z.copy(), y), num, atol=1e-6), f"Не совпадает с численным градиентом при z={z.round(2)}, y={y}"

assert abs(ce_grad(np.array([1.0, 2, 3]), 0).sum()) < 1e-12, "Сумма компонент градиента всегда 0"
Задание 9 Программа на Python

Логистическая регрессия. Модель для двух классов: p_i = \sigma(x_i \cdot w + b), потеря — бинарная кросс-энтропия L = -\frac{1}{N}\sum_i \big[y_i \ln p_i + (1 - y_i)\ln(1 - p_i)\big]. Её градиенты:

\nabla_w L = \frac{1}{N} X^T (p - y), \qquad \frac{\partial L}{\partial b} = \frac{1}{N}\sum_i (p_i - y_i)

Напишите logreg_grads(X, y, w, b) → (grad_w, grad_b) и train_logreg(X, y, lr=0.5, steps=500), которая стартует с нулей и возвращает (w, b). Без циклов по объектам.

Какие тесты проверяют код (2)
rng = np.random.default_rng(0)
X, y = rng.normal(size=(30, 2)), (rng.random(30) > 0.5).astype(float)
w, b = rng.normal(size=2), 0.3
def L(w, b):
    p = 1 / (1 + np.exp(-(X @ w + b)))
    return -(y * np.log(p) + (1 - y) * np.log(1 - p)).mean()
gw, gb = logreg_grads(X, y, w, b)
num = np.array([(L(w + e, b) - L(w - e, b)) / 2e-6 for e in np.eye(2) * 1e-6])
assert np.allclose(gw, num, atol=1e-6), "grad_w не совпадает с численным"
assert abs(gb - (L(w, b + 1e-6) - L(w, b - 1e-6)) / 2e-6) < 1e-6, "grad_b не совпадает с численным"

import nanotorch as nt
X, y = nt.make_blobs(200, centers=((-2, -1), (2, 1)), std=1.0, seed=1)
w, b = train_logreg(X, y.astype(float))
acc = ((X @ w + b > 0) == (y == 1)).mean()
assert acc > 0.95, f"Точность на хорошо разделимых данных должна быть > 95%, получено {acc:.1%}"

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

Спуск по «банану». Функция Розенброка f(x, y) = (1 - x)^2 + 100(y - x^2)^2 — классический трудный тест для оптимизаторов: минимум в точке (1, 1) лежит на дне узкого изогнутого оврага.

Напишите rosen_grad(p) — аналитический градиент (вектор из двух чисел) и descend(p0, lr, steps) — градиентный спуск, возвращающий конечную точку. Тест запустит спуск из (−1, 1) с lr = 0.001 на 20 000 шагов и проверит, что вы рядом с минимумом.

Какие тесты проверяют код (2)
f = lambda p: (1 - p[0]) ** 2 + 100 * (p[1] - p[0] ** 2) ** 2
for p in ([0.0, 0.0], [-1.0, 2.0], [1.5, 1.0]):
    p = np.array(p)
    num = np.array([(f(p + e) - f(p - e)) / 2e-6 for e in np.eye(2) * 1e-6])
    assert np.allclose(rosen_grad(p), num, atol=1e-4), f"Градиент в {p} неверен"

p = descend([-1.0, 1.0], 0.001, 20000)
assert np.linalg.norm(p - 1) < 0.05, f"Ожидали точку около (1, 1), получили {p.round(3)}"

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