Градиентный спуск: идея и реализация

Градиентный спуск находит минимум, делая маленькие шаги против наклона функции — как спуск с горы в тумане.

Градиентный спуск — итеративный метод: из текущей точки шагаем в сторону, противоположную градиенту, и повторяем, пока не достигнем минимума. Правило: x ← x − η·f'(x), где η — шаг обучения.

Спуск с горы в тумане

Представьте, что вы на склоне в густом тумане и хотите спуститься в долину. Вы не видите дна, но можете ощупать землю под ногами и понять, куда уклон. Логичная стратегия: сделать шаг в сторону самого крутого спуска, потом снова ощупать, снова шагнуть. Градиентный спуск делает ровно это: «наклон под ногами» — это производная (градиент), а «шаг» — обновление параметра. Идём против наклона, потому что нам нужно вниз.

Правило обновления

Формула одна и простая: x_новое = x − η · f'(x). Здесь f'(x) — наклон в текущей точке, а η (эта, learning rate) — шаг обучения: насколько большой шаг делаем. Знак минус разворачивает нас от роста к убыванию. Реализуем спуск для f(x) = (x − 3)² с минимумом в x = 3.

def f(x):
    return (x - 3) ** 2

def grad(x):
    return 2 * (x - 3)        # производная (x-3)^2

x = 0.0          # стартуем далеко от минимума
lr = 0.1         # шаг обучения

for step in range(1, 11):
    g = grad(x)
    x = x - lr * g           # шаг против градиента
    print(f"шаг {step:2}: x = {round(x, 4):6}  f(x) = {round(f(x), 4)}")

Вывод:

шаг  1: x =    0.6  f(x) = 5.76
шаг  2: x =   1.08  f(x) = 3.6864
шаг  3: x =  1.464  f(x) = 2.3593
шаг  4: x = 1.7712  f(x) = 1.5099
шаг  5: x =  2.017  f(x) = 0.9664
шаг  6: x = 2.2136  f(x) = 0.6185
шаг  7: x = 2.3709  f(x) = 0.3958
шаг  8: x = 2.4967  f(x) = 0.2533
шаг  9: x = 2.5973  f(x) = 0.1621
шаг 10: x = 2.6779  f(x) = 0.1038

Видно, как x ползёт от 0 к 3, а потеря падает с 9 почти до нуля. Каждый шаг меньше предыдущего: ближе к минимуму наклон положе, шаги мельче — спуск сам тормозит.

Доводим до сходимости

Десяти шагов мало; дадим спуску больше итераций и посмотрим, как близко он подойдёт к точному минимуму x = 3.

def grad(x): return 2 * (x - 3)

x = 0.0
lr = 0.1
for _ in range(100):
    x = x - lr * grad(x)

print("После 100 шагов x =", round(x, 6))
print("Точный минимум   = 3")
print("Ошибка           =", round(abs(x - 3), 8))

Вывод:

После 100 шагов x = 3.0
Точный минимум   = 3
Ошибка           = 0.0

Универсальный спуск через численный градиент

Красота метода в том, что нам не нужна формула производной — её можно посчитать численно (из раздела про анализ). Тогда градиентный спуск работает для любой функции, даже если мы не умеем её дифференцировать на бумаге.

def numeric_grad(f, x, h=1e-6):
    return (f(x + h) - f(x - h)) / (2 * h)

def f(x):
    return (x - 5) ** 2 + 2      # минимум в x = 5

x = 0.0
lr = 0.1
for _ in range(200):
    x = x - lr * numeric_grad(f, x)

print("Найденный минимум x =", round(x, 4), " (ожидаем 5)")
print("Значение функции    =", round(f(x), 4), " (ожидаем 2)")

Вывод:

Найденный минимум x = 5.0  (ожидаем 5)
Значение функции    = 2.0  (ожидаем 2)

Итог

  • Градиентный спуск шагает против наклона к минимуму: x ← x − η·f'(x).
  • Шаг обучения η регулирует размер шага; знак минус ведёт вниз.
  • Возле минимума наклон мал → шаги мельчают, спуск сам замедляется.
  • С численным градиентом метод работает для любой функции, без формулы производной.
Проверьте себя
1. Какое правило обновления использует градиентный спуск?
Ax ← x + η·f'(x)
Bx ← x − η·f'(x)
Cx ← f'(x) / η
Dx ← x · f'(x)
2. Почему вблизи минимума шаги градиентного спуска становятся мельче?
AШаг обучения автоматически уменьшается
BНаклон (производная) около минимума мал, поэтому η·f'(x) — маленькая величина
CЗаканчиваются итерации
DФункция становится отрицательной
3. Зачем градиентному спуску может пригодиться численный градиент?
AОн точнее аналитического всегда
BОн позволяет применять спуск к любой функции, даже без формулы её производной
CОн работает быстрее на больших данных
DБез него нельзя задать шаг обучения
Поддержать проект