Частные производные и градиент
Функции многих переменных
У настоящей модели не один параметр, а много. Даже у прямой их два: потеря L(w, b) зависит от наклона w и сдвига b. Функцию двух переменных можно нарисовать как поверхность (рельеф местности) или как карту, где цвет — высота.
import numpy as np
import vis
areas = np.array([32, 38, 45, 50, 56, 63, 70, 78, 85, 95], dtype=float)
prices = np.array([6.8, 7.9, 9.1, 10.4, 11.0, 12.9, 13.8, 15.6, 16.5, 19.2])
x = (areas - areas.mean()) / areas.std() # нормированная площадь — так поверхность нагляднее
ws = np.linspace(0, 6, 25)
bs = np.linspace(8, 16, 25)
L = np.array([[((w * x + b - prices) ** 2).mean() for w in ws] for b in bs])
vis.heatmap(np.log(L)[::-1], xlabels=[f"{w:.1f}" if i % 6 == 0 else "" for i, w in enumerate(ws)],
ylabels=[f"{b:.1f}" if i % 6 == 0 else "" for i, b in enumerate(bs[::-1])],
title="ln L(w, b): по горизонтали w, по вертикали b", show_values=False, cell=14)
Тёмная «чаша» — область, где потеря минимальна. Обучение — это поиск дна этой чаши. С двумя параметрами её видно глазами, но с миллионом — нет. Нужен способ понять, куда идти, находясь в одной точке.
Частные производные
Частная производная \frac{\partial f}{\partial x} — обычная производная по x, когда все остальные переменные заморожены (считаются константами). Геометрически — наклон поверхности, если идти строго вдоль оси x.
Пример: f(x, y) = x^2 y + 3y.
- \frac{\partial f}{\partial x} = 2xy — y как константа-множитель;
- \frac{\partial f}{\partial y} = x^2 + 3 — x^2 как константа-множитель.
f = lambda x, y: x ** 2 * y + 3 * y
x, y, h = 2.0, 5.0, 1e-6
print("∂f/∂x ≈", (f(x + h, y) - f(x - h, y)) / (2 * h), " формула 2xy =", 2 * x * y)
print("∂f/∂y ≈", (f(x, y + h) - f(x, y - h)) / (2 * h), " формула x² + 3 =", x ** 2 + 3)
Градиент
Градиент — вектор из всех частных производных:
Главное свойство: градиент указывает направление самого быстрого роста функции, а его длина — насколько крут подъём. Значит, -\nabla f (антиградиент) — направление самого быстрого спуска.
Почему? Линейное приближение для функции многих переменных:
Изменение функции — скалярное произведение градиента на шаг d. При шаге фиксированной длины оно максимально, когда d сонаправлен с \nabla f (косинус равен 1), и минимально — когда противоположен. Перпендикулярно градиенту функция не меняется: это направление вдоль линии уровня.
import numpy as np
import vis
f = lambda p: p[0] ** 2 + 3 * p[1] ** 2 # вытянутая чаша
grad = lambda p: np.array([2 * p[0], 6 * p[1]])
p = np.array([2.0, 1.0])
g = grad(p)
print("градиент в (2, 1):", g)
for angle in range(0, 360, 45):
d = np.array([np.cos(np.radians(angle)), np.sin(np.radians(angle))]) * 0.01
print(f"шаг под углом {angle:3}°: изменение f = {f(p + d) - f(p):+.4f}")
u = -g / np.linalg.norm(g)
print("шаг по антиградиенту: ", f"{f(p + 0.01 * u) - f(p):+.4f} ← самое сильное убывание")
Градиент функции потерь
Для линейной модели \hat y = X\theta и потери L = \frac{1}{N}\sum_i (\hat y_i - y_i)^2 частная производная по параметру \theta_j по правилу цепочки:
Все d таких сумм — это одно матричное умножение:
Прочитать формулу можно так: ошибки X\theta - y «раздаются» обратно параметрам пропорционально признакам, которые в них участвовали. Признак, который был большим у объектов с большой ошибкой, получает большую долю градиента.
import numpy as np
areas = np.array([32, 38, 45, 50, 56, 63, 70, 78, 85, 95], dtype=float)
prices = np.array([6.8, 7.9, 9.1, 10.4, 11.0, 12.9, 13.8, 15.6, 16.5, 19.2])
X = np.c_[(areas - areas.mean()) / areas.std(), np.ones(10)]
theta = np.array([1.0, 5.0])
grad = 2 / len(X) * X.T @ (X @ theta - prices)
print("градиент по формуле:", grad)
L = lambda t: ((X @ t - prices) ** 2).mean()
h = 1e-6
num = np.array([(L(theta + e) - L(theta - e)) / (2 * h) for e in np.eye(2) * h])
print("численный градиент: ", num)
Первый градиентный спуск по двум параметрам
Повторяем: посчитать градиент → шагнуть против него → снова.
import numpy as np
import vis
areas = np.array([32, 38, 45, 50, 56, 63, 70, 78, 85, 95], dtype=float)
prices = np.array([6.8, 7.9, 9.1, 10.4, 11.0, 12.9, 13.8, 15.6, 16.5, 19.2])
X = np.c_[(areas - areas.mean()) / areas.std(), np.ones(10)]
theta = np.array([0.5, 9.0])
lr = 0.1
path, losses = [theta.copy()], []
for step in range(40):
err = X @ theta - prices
losses.append((err ** 2).mean())
theta = theta - lr * 2 / len(X) * X.T @ err
path.append(theta.copy())
print(f"нашли: w = {theta[0]:.3f}, b = {theta[1]:.3f}, MSE = {losses[-1]:.4f}")
print("точное решение:", np.linalg.lstsq(X, prices, rcond=None)[0].round(3))
vis.line(losses, title="Потери падают с каждым шагом", xlabel="шаг", ylabel="MSE", logy=True)
path = np.array(path)
vis.line(path[:, 0], path[:, 1], title="Путь параметров (w, b)", xlabel="w", ylabel="b", points=path[::4])
Спуск нашёл то же решение, что и нормальное уравнение, но ни разу не обращал матрицу. И этот способ работает для любой дифференцируемой модели — в том числе для нейросети.
Якобиан
Если функция возвращает не число, а вектор (как слой нейросети: вектор → вектор), её производная — матрица частных производных, якобиан: J_{ij} = \frac{\partial y_i}{\partial x_j}. Для линейного слоя y = Wx якобиан равен самой матрице W. Явно якобианы почти никогда не строят — в следующем модуле увидим, как обходиться без них.
Итог
- Частная производная — производная по одной переменной при замороженных остальных.
- Градиент \nabla f — вектор частных производных; указывает направление быстрейшего роста; f(x + d) \approx f(x) + \nabla f \cdot d.
- Градиент потерь имеет форму параметров: у каждого веса — своя частная производная.
- Для MSE линейной модели: \nabla_\theta L = \frac{2}{N}X^T(X\theta - y).
- Градиентный спуск: \theta \leftarrow \theta - \eta\nabla L.
Практика решено 0 из 6
Найдите \frac{\partial f}{\partial y} для f(x, y) = x^2 y + 3y в точке (2, 5).
Найдите градиент f(x, y) = x^2 + 3xy в точке (1, 2). Ответ: два числа через запятую.
В каком направлении функция растёт быстрее всего?
У модели 1 000 000 параметров, функция потерь — одно число. Какая форма у градиента потерь по параметрам?
Численный градиент. Напишите numerical_gradient(f, x, h=1e-5): f принимает NumPy-вектор
и возвращает число, x — вектор. Верните вектор той же формы, где i-я компонента —
центральная разность по i-й координате (меняйте только её на ±h).
Какие тесты проверяют код (2)
f = lambda v: v[0] ** 2 + 3 * v[0] * v[1]
g = numerical_gradient(f, np.array([1.0, 2.0]))
assert np.allclose(g, [8, 3], atol=1e-6), f"Ожидалось (8, 3), получено {g}"
A = np.array([[2.0, 1], [1, 3]])
f = lambda v: v @ A @ v
x = np.array([0.5, -1.0])
assert np.allclose(numerical_gradient(f, x), 2 * A @ x, atol=1e-6)
import numpy as np
def numerical_gradient(f, x, h=1e-5):
x = x.astype(float)
grad = np.zeros_like(x)
for i in range(len(x)):
e = np.zeros_like(x)
e[i] = h
grad[i] = (f(x + e) - f(x - e)) / (2 * h)
return grad
Градиент MSE. Линейная модель \hat y = X\theta, где X — матрица (N, d), theta — вектор (d,).
Потеря L = \frac{1}{N}\|X\theta - y\|^2. Её градиент:
Напишите mse_grad(X, y, theta) (без циклов) и gd_step(X, y, theta, lr) — один шаг
градиентного спуска, возвращающий новый вектор параметров.
Какие тесты проверяют код (2)
rng = np.random.default_rng(0)
X, y, th = rng.normal(size=(20, 3)), rng.normal(size=20), rng.normal(size=3)
L = lambda t: ((X @ t - y) ** 2).mean()
num = np.array([(L(th + e) - L(th - e)) / 2e-6 for e in np.eye(3) * 1e-6])
assert np.allclose(mse_grad(X, y, th), num, atol=1e-6), "Градиент не совпадает с численным"
rng = np.random.default_rng(1)
X = np.c_[rng.normal(size=50), np.ones(50)]
y = X @ np.array([3.0, -1.0])
th = np.zeros(2)
for _ in range(500):
th = gd_step(X, y, th, 0.1)
assert np.allclose(th, [3, -1], atol=1e-3), f"После 500 шагов должно получиться (3, −1), получено {th}"
import numpy as np
def mse_grad(X, y, theta):
return 2 / len(X) * X.T @ (X @ theta - y)
def gd_step(X, y, theta, lr):
return theta - lr * mse_grad(X, y, theta)
Войдите, чтобы отмечать пройденные уроки.