Фаза 01 · урок 08
Оптимизация: семейство градиентного спуска
Цель урока: У вас есть функция потерь. Она сообщает, насколько ошибается ваша модель. У вас есть градиенты. Они показывают, в каком направлении функция потерь становится хуже. Теперь нужна стратегия движения вниз.
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Что означает оптимизация
- Градиентный спуск (обычный)
- Скорость обучения: самый важный гиперпараметр
- SGD, полный пакет и мини-пакет
- Момент: шар, катящийся вниз по склону
- Adam: адаптивные скорости обучения
- Расписания скорости обучения
- Выпуклые и невыпуклые функции
- Визуализация ландшафта функции потерь
- Создайте это
- Шаг 1: Определите тестовую функцию
- Шаг 2: Обычный градиентный спуск
- Шаг 3: SGD с моментом
- Шаг 4: Adam
- Шаг 5: Запустите и сравните
- Используйте это
- Внедрите это
- Упражнения
- Ключевые термины
- Дополнительные материалы
Обучение нейронной сети — это не что иное, как поиск дна долины.
Тип: Практика Язык: Python Предварительные требования: Фаза 1, уроки 04–05 («Производные, градиенты») Время: около 75 минут
Цели обучения
- Реализовать с нуля обычный градиентный спуск, SGD с моментом и Adam.
- Сравнить сходимость оптимизаторов на функции Розенброка и объяснить, почему Adam адаптирует скорость обучения для каждого веса.
- Различать выпуклые и невыпуклые ландшафты функции потерь и объяснять роль седловых точек в пространствах высокой размерности.
- Настраивать расписания скорости обучения (ступенчатое затухание, косинусный отжиг, разогрев) для устойчивости обучения.
Проблема
У вас есть функция потерь. Она сообщает, насколько ошибается ваша модель. У вас есть градиенты. Они показывают, в каком направлении функция потерь становится хуже. Теперь нужна стратегия движения вниз.
Наивный подход прост: двигайтесь в направлении, противоположном градиенту. Масштабируйте шаг некоторым числом, называемым скоростью обучения. Повторяйте. Это градиентный спуск, и он работает. Но у слова «работает» есть оговорки. Слишком большая скорость обучения — и вы перелетите через долину, отскакивая между её стенками. Слишком маленькая — и будете ползти к ответу тысячи ненужных шагов. Попадёте в седловую точку — и остановитесь, хотя минимума ещё не нашли.
Каждый оптимизатор в глубоком обучении отвечает на один вопрос: как добраться до дна долины быстрее и надёжнее?
Концепция
Что означает оптимизация
Оптимизация — это поиск входных значений, которые минимизируют (или максимизируют) функцию. В машинном обучении этой функцией является функция потерь. Входами служат веса модели. Обучение — это оптимизация.
minimize L(w) where:
L = loss function
w = model weights (could be millions of parameters)
Градиентный спуск (обычный)
Это простейший оптимизатор. Вычислите градиент функции потерь по каждому весу. Сдвиньте каждый вес в направлении, противоположном его градиенту. Масштабируйте шаг скоростью обучения.
w = w - lr * gradient
Это весь алгоритм. Одна строка.
Скорость обучения: самый важный гиперпараметр
Скорость обучения управляет размером шага. От неё зависит всё, что связано со сходимостью.
Формулы для правильной скорости обучения не существует. Её находят экспериментально. Типичные начальные значения: 0.001 для Adam, 0.01 для SGD с моментом.
SGD, полный пакет и мини-пакет
Обычный градиентный спуск вычисляет градиент по всему набору данных, прежде чем сделать один шаг. Это называется пакетным градиентным спуском (batch gradient descent). Он устойчив, но медленен.
Стохастический градиентный спуск (stochastic gradient descent, SGD) вычисляет градиент по одному случайному примеру и сразу делает шаг. Он шумный, но быстрый.
Градиентный спуск по мини-пакету — это компромисс. Вычислите градиент по небольшому пакету (32, 64, 128, 256 примеров), затем сделайте шаг. Именно так поступают на практике.
| Вариант | Размер пакета | Качество градиента | Скорость шага | Шум |
|---|---|---|---|---|
| Пакетный GD | Весь набор данных | Точное | Медленная | Нет |
| SGD | 1 пример | Очень шумное | Быстрая | Высокий |
| Мини-пакет | 32–256 | Хорошая оценка | Сбалансированная | Умеренный |
Шум SGD и мини-пакетов — не ошибка. Он помогает выходить из неглубоких локальных минимумов и седловых точек.
Момент: шар, катящийся вниз по склону
Обычный градиентный спуск учитывает только текущий градиент. Если градиент идёт зигзагом (что часто бывает в узких долинах), продвижение получается медленным. Метод с моментом исправляет это, накапливая прошлые градиенты в слагаемом скорости.
v = beta * v + gradient
w = w - lr * v
Аналогия: шар, катящийся вниз по склону. Он не останавливается и не начинает движение заново на каждой неровности. Он набирает скорость в устойчивых направлениях и подавляет колебания.
beta (обычно 0.9) определяет, какой объём истории сохранять. Более высокое beta означает больший момент и более плавные траектории, но более медленную реакцию на смену направления.
Adam: адаптивные скорости обучения
Разным весам нужны разные скорости обучения. Вес, который редко получает большие градиенты, должен делать более крупные шаги, когда это наконец происходит. Вес, который постоянно получает огромные градиенты, должен делать более мелкие шаги.
Adam (Adaptive Moment Estimation) отслеживает для каждого веса две величины:
- Первый момент (m): скользящее среднее градиентов (как момент).
- Второй момент (v): скользящее среднее квадратов градиента (величина градиента).
m = beta1 * m + (1 - beta1) * gradient
v = beta2 * v + (1 - beta2) * gradient^2
m_hat = m / (1 - beta1^t) bias correction
v_hat = v / (1 - beta2^t) bias correction
w = w - lr * m_hat / (sqrt(v_hat) + epsilon)
Деление на sqrt(v_hat) — ключевая идея. Веса с большими градиентами делятся на большое число (малый эффективный шаг). Веса с малыми градиентами делятся на малое число (большой эффективный шаг). Каждый вес получает собственную адаптивную скорость обучения.
Гиперпараметры по умолчанию: lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8. Эти значения по умолчанию хорошо работают для большинства задач.
Расписания скорости обучения
Фиксированная скорость обучения — это компромисс. В начале обучения нужны большие шаги для быстрого продвижения. В конце обучения нужны маленькие шаги для тонкой настройки вблизи минимума.
Распространённые расписания:
| Расписание | Формула | Сценарий применения |
|---|---|---|
| Ступенчатое затухание | lr = lr * factor every N epochs | Простое ручное управление |
| Экспоненциальное затухание | lr = lr_0 * decay^t | Плавное уменьшение |
| Косинусный отжиг | lr = lr_min + 0.5 * (lr_max - lr_min) * (1 + cos(pi * t / T)) | Трансформеры, современное обучение |
| Разогрев + затухание | Линейное нарастание, затем затухание | Большие модели, предотвращение ранней нестабильности |
Выпуклые и невыпуклые функции
У выпуклой функции один минимум. Градиентный спуск всегда его находит. Квадратичная функция f(x) = x^2 выпукла.
Функции потерь нейронных сетей невыпуклы. У них много локальных минимумов, седловых точек и плоских областей.
На практике локальные минимумы в высокоразмерных нейронных сетях редко являются проблемой. Значения функции потерь у большинства локальных минимумов близки к глобальному минимуму. Настоящее препятствие — седловые точки (плоские в одних направлениях и искривлённые в других). Момент и шум мини-пакетов помогают выходить из них.
Визуализация ландшафта функции потерь
Функция потерь — это функция всех весов. Для модели с 1 миллионом весов ландшафт функции потерь находится в пространстве размерности 1 000 001. Мы визуализируем его, выбирая два случайных направления в пространстве весов и строя график потерь вдоль этих направлений; в результате получается двумерная поверхность.
Острые минимумы плохо обобщаются. Плоские минимумы обобщаются хорошо. Это одна из причин, по которым SGD с моментом часто превосходит Adam по итоговой точности на тесте: его шум не даёт закрепиться в острых минимумах.
gradient-descent
Создайте это
Шаг 1: Определите тестовую функцию
Функция Розенброка — классическая задача для проверки оптимизации. Её минимум находится в точке (1, 1) внутри узкой изогнутой долины, которую легко найти, но трудно пройти вдоль неё.
f(x, y) = (1 - x)^2 + 100 * (y - x^2)^2
def rosenbrock(params):
x, y = params
return (1 - x) ** 2 + 100 * (y - x ** 2) ** 2
def rosenbrock_gradient(params):
x, y = params
df_dx = -2 * (1 - x) + 200 * (y - x ** 2) * (-2 * x)
df_dy = 200 * (y - x ** 2)
return [df_dx, df_dy]
Шаг 2: Обычный градиентный спуск
class GradientDescent:
def __init__(self, lr=0.001):
self.lr = lr
def step(self, params, grads):
return [p - self.lr * g for p, g in zip(params, grads)]
Шаг 3: SGD с моментом
class SGDMomentum:
def __init__(self, lr=0.001, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.velocity = None
def step(self, params, grads):
if self.velocity is None:
self.velocity = [0.0] * len(params)
self.velocity = [
self.momentum * v + g
for v, g in zip(self.velocity, grads)
]
return [p - self.lr * v for p, v in zip(params, self.velocity)]
Шаг 4: Adam
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.epsilon = epsilon
self.m = None
self.v = None
self.t = 0
def step(self, params, grads):
if self.m is None:
self.m = [0.0] * len(params)
self.v = [0.0] * len(params)
self.t += 1
self.m = [
self.beta1 * m + (1 - self.beta1) * g
for m, g in zip(self.m, grads)
]
self.v = [
self.beta2 * v + (1 - self.beta2) * g ** 2
for v, g in zip(self.v, grads)
]
m_hat = [m / (1 - self.beta1 ** self.t) for m in self.m]
v_hat = [v / (1 - self.beta2 ** self.t) for v in self.v]
return [
p - self.lr * mh / (vh ** 0.5 + self.epsilon)
for p, mh, vh in zip(params, m_hat, v_hat)
]
Шаг 5: Запустите и сравните
def optimize(optimizer, func, grad_func, start, steps=5000):
params = list(start)
history = [params[:]]
for _ in range(steps):
grads = grad_func(params)
params = optimizer.step(params, grads)
history.append(params[:])
return history
start = [-1.0, 1.0]
gd_history = optimize(GradientDescent(lr=0.0005), rosenbrock, rosenbrock_gradient, start)
sgd_history = optimize(SGDMomentum(lr=0.0001, momentum=0.9), rosenbrock, rosenbrock_gradient, start)
adam_history = optimize(Adam(lr=0.01), rosenbrock, rosenbrock_gradient, start)
for name, history in [("GD", gd_history), ("SGD+M", sgd_history), ("Adam", adam_history)]:
final = history[-1]
loss = rosenbrock(final)
print(f"{name:6s} -> x={final[0]:.6f}, y={final[1]:.6f}, loss={loss:.8f}")
Ожидаемый вывод: Adam сходится быстрее всего. SGD с моментом следует более плавной траекторией. Обычный GD медленно движется по узкой долине.
Используйте это
На практике используйте оптимизаторы PyTorch или JAX. Они поддерживают группы параметров, затухание весов, отсечение градиента и ускорение на GPU.
import torch
model = torch.nn.Linear(784, 10)
sgd = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
adam = torch.optim.Adam(model.parameters(), lr=0.001)
adamw = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(adam, T_max=100)
Практические правила:
- Начните с Adam (lr=0.001). Он работает для большинства задач без настройки.
- Перейдите на SGD с моментом (lr=0.01, momentum=0.9), если нужна лучшая итоговая точность и вы можете потратить больше времени на настройку.
- Используйте AdamW (Adam с развязанным затуханием весов) для трансформеров.
- Всегда используйте расписание скорости обучения для запусков обучения дольше нескольких эпох.
- Если обучение нестабильно, уменьшите скорость обучения. Если обучение идёт слишком медленно, увеличьте её.
Внедрите это
Этот урок создаёт промпт для выбора подходящего оптимизатора. См. outputs/prompt-optimizer-guide.md.
Созданные здесь классы оптимизаторов снова появятся в фазе 3, когда мы будем обучать нейронную сеть с нуля.
Упражнения
-
Перебор скорости обучения. Запустите обычный градиентный спуск на функции Розенброка со скоростями обучения [0.0001, 0.0005, 0.001, 0.005, 0.01]. Постройте график или выведите итоговую потерю после 5000 шагов для каждой из них. Найдите наибольшую скорость обучения, при которой метод всё ещё сходится.
-
Сравнение момента. Запустите SGD со значениями момента [0.0, 0.5, 0.9, 0.99] на функции Розенброка. Отслеживайте потерю на каждом шаге. При каком значении момента сходимость самая быстрая? При каком возникает перелёт?
-
Выход из седловой точки. Определите функцию
f(x, y) = x^2 - y^2(седловая точка находится в начале координат). Начните в (0.01, 0.01). Сравните поведение обычного GD, SGD с моментом и Adam. Какой метод выходит из седловой точки? -
Реализуйте затухание скорости обучения. Добавьте в класс GradientDescent расписание экспоненциального затухания:
lr = lr_0 * 0.999^step. Сравните сходимость с затуханием и без него на функции Розенброка.
Ключевые термины
| Термин | Как обычно говорят | Что это на самом деле означает |
|---|---|---|
| Градиентный спуск | «Идите вниз по склону» | Обновляйте веса, вычитая градиент, масштабированный скоростью обучения. Самый базовый оптимизатор. |
| Скорость обучения | «Размер шага» | Скаляр, задающий, насколько далеко каждое обновление сдвигает веса. Слишком большое значение вызывает расхождение. Слишком малое тратит вычисления впустую. |
| Момент | «Продолжайте катиться» | Накапливайте прошлые градиенты в векторе скорости. Подавляет колебания и ускоряет движение в устойчивых направлениях. |
| SGD | «Случайная выборка» | Стохастический градиентный спуск. Вычисляйте градиент по случайному подмножеству вместо полного набора данных. На практике почти всегда означает SGD по мини-пакетам. |
| Мини-пакет | «Порция данных» | Небольшое подмножество обучающих данных (32–256 примеров), используемое для оценки градиента. Балансирует скорость и точность градиента. |
| Adam | «Оптимизатор по умолчанию» | Adaptive Moment Estimation. Отслеживает для каждого веса скользящие средние градиентов и квадратов градиентов, чтобы назначить каждому весу собственную скорость обучения. |
| Коррекция смещения | «Исправить холодный старт» | Первый и второй моменты Adam инициализируются нулями. Коррекция смещения делит на (1 - beta^t), чтобы компенсировать это на ранних шагах. |
| Расписание скорости обучения | «Меняйте lr со временем» | Функция, которая корректирует скорость обучения в ходе обучения. Большие шаги в начале, маленькие — в конце. |
| Выпуклая функция | «Одна долина» | Функция, у которой любой локальный минимум является глобальным. Градиентный спуск всегда его находит. Функции потерь нейронных сетей не выпуклы. |
| Седловая точка | «Плоско, но не минимум» | Точка с нулевым градиентом, которая является минимумом в одних направлениях и максимумом в других. Распространена в высоких размерностях. |
| Ландшафт функции потерь | «Рельеф» | Функция потерь, построенная над пространством весов. Визуализируется срезом вдоль двух случайных направлений. |
| Сходимость | «Добраться до цели» | Оптимизатор достиг точки, в которой дальнейшие шаги не дают значимого уменьшения функции потерь. |
Дополнительные материалы
- Sebastian Ruder: Обзор алгоритмов оптимизации градиентного спуска — подробный обзор всех основных оптимизаторов
- Почему момент действительно работает (Distill) — интерактивная визуализация динамики момента
- Adam: метод стохастической оптимизации (Kingma & Ba, 2014) — оригинальная статья об Adam, короткая и доступная
- Визуализация ландшафта функции потерь нейронных сетей (Li et al., 2018) — статья, показавшая различие между острыми и плоскими минимумами
Источник: Optimization — оригинал Навигация: назад: 01.07 — Теорема Байеса и статистическое мышление · Фаза 1 — Математические основы · Полный каталог · далее: 01.09 — Теория информации: энтропия и KL-дивергенция.