🔴 Сложный ⏱️ 60 минут

Градиентный спуск

📋 Содержание урока

Градиентный спуск 🏔️

Открой почти любой учебный ноутбук по машинному обучению — от простенькой линейной регрессии на игрушечных данных до кода, которым тренируют модели уровня GPT, — и почти в каждом ты найдёшь одну и ту же строчку, повторённую тысячи или миллионы раз подряд: посчитай градиент функции потерь по весам, сдвинь веса чуть-чуть против градиента. Это и есть градиентный спуск — не один из многих методов обучения моделей, а буквально тот единственный алгоритм, вокруг которого построено практически всё современное машинное обучение. Линейная регрессия, логистическая регрессия, метод опорных векторов, градиентный бустинг, свёрточные сети, трансформеры — все они обучаются одним и тем же способом: маленькими шагами против направления, в котором ошибка растёт быстрее всего.

Ты уже встречал этот алгоритм в университетском курсе (урок 212), где мы строго вывели формулу шага через неравенство Коши-Буняковского и доказали, что антиградиент указывает направление наискорейшего убывания функции. Там разговор был математическим: откуда берётся формула и почему она вообще работает. Здесь разговор другой — инженерный. Тебе не нужно каждый раз доказывать теорему, чтобы обучить модель, но тебе критически важно понимать, как этот алгоритм ведёт себя на практике: почему обучение иногда «взрывается» и функция потерь улетает в бесконечность, почему иногда обучение тянется часами без видимого прогресса, почему одна и та же архитектура нейросети иногда обучается до разного качества в зависимости от того, как её проинициализировали, и какие рычаги у тебя есть, чтобы всем этим управлять.

Если тебе придётся выбрать один-единственный гиперпараметр, с настройки которого начинается обучение практически любой модели, это будет скорость обучения — learning rate, тот самый коэффициент $\eta$ в формуле шага. Слишком маленький — и ты будешь ждать сходимости часами там, где могло бы хватить минут. Слишком большой — и модель вместо обучения устроит истерику: ошибка будет скакать вверх-вниз или вовсе улетит в бесконечность уже на первых итерациях. Именно поэтому в реальной практике настройка learning rate почти всегда идёт первым пунктом в списке гиперпараметров, которые пробуют перебирать, и именно поэтому сегодняшний урок целиком посвящён тому, чтобы ты видел эту зависимость не абстрактно, а на конкретных числах, которые сам прогонишь через несколько итераций.

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

История

Идея градиентного спуска, как ты уже знаешь из университетского курса, восходит к 1847 году, когда французский математик Огюстен Коши предложил метод наискорейшего спуска для решения систем уравнений, возникавших в задачах небесной механики. Почти полтора столетия этот метод оставался красивой теоретической идеей без практического применения: делать вручную даже пару десятков итераций для сколько-нибудь содержательной задачи было немыслимо трудоёмко, а без вычислительных машин о тысячах и миллионах итераций, которые сегодня прогоняются за секунды на видеокарте, не было и речи.

Настоящая вторая жизнь градиентного спуска в контексте обучения моделей началась в середине XX века вместе с ранними работами по нейронным сетям. В 1957 году Фрэнк Розенблатт предложил персептрон и правило обучения для него, а десятилетия спустя, в 1986 году, Дэвид Румельхарт, Джеффри Хинтон и Рональд Уильямс опубликовали статью, популяризировавшую алгоритм обратного распространения ошибки (backpropagation) — эффективный способ вычислить градиент функции потерь по каждому весу многослойной сети. Backpropagation сам по себе не является отдельным методом оптимизации: это способ посчитать градиент, а не способ им воспользоваться — саму работу по обновлению весов на основе посчитанного градиента всё так же делает градиентный спуск. Именно эта комбинация — обратное распространение для вычисления градиента плюс градиентный спуск для обновления весов — на десятилетия стала стандартной парой инструментов для обучения нейросетей.

Взрывной рост популярности метода пришёлся на 2010-е годы вместе с революцией глубокого обучения: доступность видеокарт (GPU), огромные размеченные датасеты и модификации базового алгоритма — Adam (2014), RMSProp, SGD с моментом, которые ты изучишь подробнее позже в курсе, — превратили градиентный спуск из инструмента для обучения простых моделей в рабочую лошадку, которая тренирует сети с миллиардами и триллионами параметров. Забавно, что метод, придуманный для уточнения орбит планет по неточным телескопическим наблюдениям XIX века, спустя без малого два столетия учит нейросети писать код, распознавать речь и вести диалог — при этом сама формула шага не изменилась ни на йоту с 1847 года.

Формула шага и скорость обучения

Интуиция

Представь себя туристом на склоне горы в густом тумане: карты нет, видимости нет, есть только ощущение уклона почвы под ногами прямо там, где ты стоишь. Разумная стратегия — почувствовать, в какую сторону земля уходит вниз круче всего, сделать шаг именно туда, снова оценить уклон, снова шагнуть. Вопрос, от которого зависит буквально всё, — насколько большой шаг делать. Сделаешь слишком робкий шажок в пять сантиметров — доберёшься до подножия горы к следующему тысячелетию. Сделаешь гигантский прыжок в двадцать метров — рискуешь перепрыгнуть через долину и оказаться на соседнем склоне, причём выше, чем был исходно.

Именно эту интуицию формализует скорость обучения $\eta$ (learning rate) — коэффициент, который определяет длину шага при каждом обновлении весов.

Формула

Формула шага градиентного спуска. Чтобы минимизировать функцию потерь $L(w)$ по весам модели $w$, начиная с некоторого начального приближения $w^{(0)}$, повторяют обновление

$$w^{(t+1)} = w^{(t)} - \eta\,\nabla L\!\left(w^{(t)}\right)$$

где $\eta > 0$ — скорость обучения (learning rate), гиперпараметр, который задаётся заранее (или адаптируется по ходу обучения, о чём мы поговорим отдельно). При достаточно малом $\eta$ каждый шаг гарантированно уменьшает $L$ (это строго доказано в уроке 212 через линейное приближение Тейлора); при слишком большом $\eta$ шаг может «перепрыгнуть» минимум и увеличить, а не уменьшить значение функции потерь.

Разбор примеров

Пример 1 (слишком маленький learning rate). Рассмотрим простую задачу линейной регрессии без свободного члена: три наблюдения $x = (1, 2, 3)$, целевые значения $y = (3, 5, 6)$, модель $\hat y = wx$. Функция потерь — среднеквадратичная ошибка $L(w) = \frac13\sum_i (wx_i - y_i)^2$. Её градиент (в одномерном случае — просто производная):

$$L'(w) = \frac23\sum_i (wx_i - y_i)\,x_i = \frac{28w - 62}{3}$$

Точный минимум этой функции (его легко найти, приравняв $L'(w)=0$) находится в точке $w^\* = 62/28 \approx 2{,}2143$, а минимальное значение ошибки $L_{\min} \approx 0{,}4524$. Стартуем из $w^{(0)}=0$ со слишком осторожной скоростью обучения $\eta = 0{,}01$:

Шаг $\nabla L(w)$ $w$ $L(w)$
$0$ $0{,}0000$ $23{,}3333$
$1$ $-20{,}6667$ $0{,}2067$ $19{,}2615$
$2$ $-18{,}7378$ $0{,}3940$ $15{,}9143$
$3$ $-16{,}9889$ $0{,}5639$ $13{,}1628$
$4$ $-15{,}4033$ $0{,}7180$ $10{,}9009$
$5$ $-13{,}9656$ $0{,}8576$ $9{,}0415$
$6$ $-12{,}6622$ $0{,}9842$ $7{,}5131$

После шести шагов вес $w$ прошёл лишь меньше половины пути от $0$ до оптимума $2{,}2143$, а ошибка снизилась всего в три раза (с $23{,}33$ до $7{,}51$), тогда как теоретический минимум — $0{,}4524$. Метод неуклонно движется в правильную сторону, но такими темпами до окрестности минимума потребуются ещё десятки итераций.

Пример 2 (удачный learning rate). Та же самая функция, тот же старт $w^{(0)}=0$, но $\eta = 0{,}1$:

Шаг $\nabla L(w)$ $w$ $L(w)$
$0$ $0{,}0000$ $23{,}3333$
$1$ $-20{,}6667$ $2{,}0667$ $0{,}5541$
$2$ $-1{,}3778$ $2{,}2044$ $0{,}4528$
$3$ $-0{,}0919$ $2{,}2136$ $0{,}4524$
$4$ $-0{,}0061$ $2{,}2142$ $0{,}4524$
$5$ $-0{,}0004$ $2{,}2143$ $0{,}4524$

Уже после первого шага ошибка упала с $23{,}33$ до $0{,}55$ — метод почти долетел до минимума одним прыжком, а дальше лишь чуть-чуть подправляет положение, пока градиент не станет практически нулевым. К пятому шагу значения $w$ и $L$ совпадают с теоретическим оптимумом до четвёртого знака после запятой. Разница с предыдущим примером колоссальна: при в десять раз большей скорости обучения понадобилось не десятки, а всего пять шагов.

Пример 3 (слишком большой learning rate — расходимость). Та же функция, тот же старт, но $\eta = 0{,}35$:

Шаг $\nabla L(w)$ $w$ $L(w)$
$0$ $0{,}0000$ $23{,}3333$
$1$ $-20{,}6667$ $7{,}2333$ $118{,}0096$
$2$ $46{,}8444$ $-9{,}1622$ $604{,}4354$
$3$ $-106{,}1807$ $28{,}0010$ $3103{,}5829$
$4$ $240{,}6763$ $-56{,}2357$ $15943{,}6475$
$5$ $-545{,}5331$ $134{,}7009$ $81913{,}0460$
$6$ $1236{,}5416$ $-298{,}0887$ $420849{,}1554$

Обрати внимание на два симптома катастрофы одновременно: значение $w$ знакопеременно скачет (положительное, отрицательное, снова положительное), а ошибка вместо убывания растёт экспоненциально — с $23{,}33$ до почти полумиллиона всего за шесть шагов. Каждый следующий шаг «перепрыгивает» минимум настолько далеко за его пределы, что градиент там оказывается ещё больше исходного, и процесс раскручивается сам себя, как снежный ком. Если бы ты наблюдал такую картину на графике обучения реальной модели, это выглядело бы как функция потерь, стремительно улетающая в NaN через несколько итераций, — классический, мгновенно узнаваемый симптом слишком высокой скорости обучения.

Почему это важно

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

Пакетный градиентный спуск и цикл обучения модели

Интуиция

До сих пор в примерах мы работали с крошечными игрушечными датасетами из трёх-четырёх точек, и градиент функции потерь считался сразу по всем этим точкам за один шаг. Это и есть суть пакетного градиентного спуска (batch gradient descent): на каждой итерации градиент вычисляется усреднением по абсолютно всем примерам обучающего датасета, и только после этого происходит одно обновление весов. Если представить обучение как спуск с горы, то пакетный градиентный спуск — это турист, который перед каждым единственным шагом собирает мнение о наклоне склона со всех точек, которые он вообще способен ощупать (то есть со всего датасета), усредняет их и только после этого делает шаг, максимально уверенный в выбранном направлении.

Формула

Пакетный градиентный спуск. Пусть функция потерь на датасете из $N$ примеров имеет вид среднего по всем примерам $L(w) = \frac1N \sum_{i=1}^N \ell_i(w)$, где $\ell_i(w)$ — потеря на одном обучающем примере (например, квадрат ошибки для регрессии). Тогда градиент по всему датасету равен среднему из градиентов по каждому отдельному примеру

$$\nabla L(w) = \frac1N \sum_{i=1}^N \nabla \ell_i(w)$$

и шаг пакетного градиентного спуска обновляет веса ровно один раз за проход по всему датасету (эта единица измерения называется эпохой):

$$w^{(t+1)} = w^{(t)} - \eta\,\frac1N \sum_{i=1}^N \nabla \ell_i\!\left(w^{(t)}\right)$$

Разбор примеров

Пример 1 (полная трассировка обучения линейной регрессии). Возьмём чуть более реалистичную задачу: четыре наблюдения $x = (1, 2, 3, 4)$, цели $y = (3, 5, 7, 10)$, модель с двумя параметрами $\hat y = wx + b$ (вес и свободный член). Функция потерь — среднеквадратичная ошибка (MSE) по всем четырём точкам сразу: $L(w,b) = \frac14\sum_i (wx_i+b-y_i)^2$. Точное решение (его можно получить и напрямую, через нормальные уравнения) — $w^\*=2{,}3$, $b^\*=0{,}5$, $L_{\min}=0{,}075$. Стартуем из $(w^{(0)}, b^{(0)}) = (0,0)$ с $\eta = 0{,}1$ и прогоняем семь полных проходов по датасету — семь эпох пакетного градиентного спуска:

Эпоха $\partial L/\partial w$ $\partial L/\partial b$ $w$ $b$ $L$
$0$ $0{,}0000$ $0{,}0000$ $45{,}7500$
$1$ $-37{,}0000$ $-12{,}5000$ $3{,}7000$ $1{,}2500$ $20{,}5875$
$2$ $24{,}7500$ $8{,}5000$ $1{,}2250$ $0{,}4000$ $9{,}2897$
$3$ $-16{,}6250$ $-5{,}5750$ $2{,}8875$ $0{,}9575$ $4{,}2169$
$4$ $11{,}1000$ $3{,}8525$ $1{,}7775$ $0{,}5723$ $1{,}9390$
$5$ $-7{,}4763$ $-2{,}4680$ $2{,}5251$ $0{,}8191$ $0{,}9160$
$6$ $4{,}9721$ $1{,}7637$ $2{,}0279$ $0{,}6427$ $0{,}4565$
$7$ $-3{,}3679$ $-1{,}0751$ $2{,}3647$ $0{,}7502$ $0{,}2499$

Обрати внимание на зигзагообразный характер траектории: значения $w$ прыгают то выше, то ниже оптимального $2{,}3$ (сначала $3{,}70$, потом $1{,}23$, потом $2{,}89$...), а знак градиента чередуется на каждом шаге. При этом ошибка $L$ всё равно неуклонно убывает: $45{,}75 \to 20{,}59 \to 9{,}29 \to \dots \to 0{,}25$. Эта колебательная, но сходящаяся траектория — совершенно типичное поведение для функций потерь с «оврагами» (когда чувствительность к разным параметрам сильно различается, как здесь — $w$ умножается на признак с разбросом значений от $1$ до $4$, а $b$ — на константу): при выбранной скорости обучения метод слегка «перескакивает» узкое дно оврага туда-сюда, прежде чем окончательно устаканиться. Это ровно та ситуация, где помогает масштабирование признаков или адаптивные методы вроде Adam — но об этом подробнее в следующих уроках курса.

Пример 2 (почему пакетный спуск дорогой на практике). Представь, что датасет состоит не из четырёх, а из десяти миллионов примеров — вполне типичный размер для промышленной задачи. Чтобы сделать один-единственный шаг обновления весов, пакетному градиентному спуску нужно сначала вычислить предсказание модели и градиент потерь на всех десяти миллионах примеров, а уже потом усреднить результат и обновить веса. Если для сходимости нужно, скажем, тысяча таких шагов (тысяча полных эпох), общее число обращений к отдельным примерам данных составит $10^7 \times 10^3 = 10^{10}$ — десять миллиардов. Каждый шаг при этом крайне «информативно насыщенный» (он основан на полном датасете и потому даёт максимально точное направление), но и крайне медленный и памятеёмкий: чтобы посчитать градиент по всем десяти миллионам примеров сразу, их приходится держать в памяти (или как минимум последовательно прогонять) целиком. Именно эта дороговизна одного шага — прямая мотивация следующей темы курса, стохастического градиентного спуска, где градиент оценивается не по всему датасету, а по небольшой случайной подвыборке, что делает каждый отдельный шаг гораздо дешевле ценой некоторого шума в направлении.

Пример 3 (полный цикл обучения на псевдокоде). Вот как пакетный градиентный спуск из примера 1 выглядит в виде цикла обучения, максимально близкого к реальному коду на Python/NumPy — именно такой цикл (пусть и спрятанный внутри .fit() или optimizer.step()) выполняется при обучении подавляющего большинства моделей машинного обучения:

import numpy as np

# Данные примера 1: x = (1,2,3,4), y = (3,5,7,10)
X = np.array([1.0, 2.0, 3.0, 4.0])
y = np.array([3.0, 5.0, 7.0, 10.0])
n = len(X)

w, b = 0.0, 0.0          # начальное приближение
eta = 0.1                # скорость обучения
n_epochs = 7              # число полных проходов по датасету
tol = 1e-6                # критерий остановки по изменению loss
prev_loss = float("inf")

for epoch in range(1, n_epochs + 1):
    pred = w * X + b
    error = pred - y

    loss = np.mean(error ** 2)
    grad_w = (2 / n) * np.sum(error * X)
    grad_b = (2 / n) * np.sum(error)

    w -= eta * grad_w
    b -= eta * grad_b

    print(f"epoch={epoch} loss={loss:.4f} w={w:.4f} b={b:.4f}")

    if abs(prev_loss - loss) < tol:
        print("Сходимость по изменению loss, останавливаемся")
        break
    prev_loss = loss

Заметь ключевую структуру, которая повторяется практически в любом обучающем цикле, от sklearn до PyTorch: посчитать предсказание модели, посчитать ошибку и значение функции потерь, посчитать градиент функции потерь по параметрам, обновить параметры на шаг против градиента, проверить критерий остановки. В библиотеках вроде PyTorch эти строчки скрыты за вызовами loss.backward() (автоматическое вычисление градиента через тот же принцип обратного распространения) и optimizer.step() (собственно шаг $w \leftarrow w - \eta \nabla L$), но математическая суть цикла — ровно та, что выписана здесь явными формулами NumPy.

Почему это важно

Пакетный градиентный спуск — это теоретическая база, на которой строятся все практические варианты метода. Понимание того, что каждый его шаг требует полного прохода по датасету, объясняет сразу две вещи: почему обучение на больших датасетах — вычислительно дорогая задача, требующая GPU и распределённых вычислений, и почему на практике почти никогда не используется «чистый» пакетный вариант, а вместо него — его более дешёвые модификации, о которых пойдёт речь в следующем уроке. Но при этом сама механика — вычислить градиент, сделать шаг против него, повторить — остаётся неизменной ядром абсолютно любого цикла обучения, каким бы конкретным вариантом градиентного спуска он ни пользовался.

Сходимость: выпуклые функции против невыпуклых

Интуиция

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

Формула

Гарантия сходимости на выпуклых функциях. Если $L(w)$ выпукла и дифференцируема, то по теореме из урока про выпуклые функции любая точка $w^\*$, в которой $\nabla L(w^\*) = 0$, является глобальным минимумом. Значит, любая точка, к которой сходится градиентный спуск на выпуклой функции потерь (при достаточно малом и, возможно, убывающем $\eta$), гарантированно оказывается наилучшим возможным решением — независимо от точки старта.

Отсутствие такой гарантии на невыпуклых функциях. Если $L(w)$ невыпукла, критические точки, где $\nabla L(w) = 0$, могут быть локальными минимумами (не глобальными), локальными максимумами или седловыми точками (где гессиан имеет собственные значения разных знаков, как в уроке про выпуклые функции). Результат градиентного спуска в этом случае зависит от точки старта и деталей траектории, и математической гарантии оптимальности результата не существует.

Разбор примеров

Пример 1 (выпуклая функция — гарантия срабатывает). Возьмём тот же пример линейной регрессии из предыдущего раздела: $L(w,b) = \frac14\sum_i (wx_i+b-y_i)^2$ — это в точности функция вида $\frac{1}{2n}\|y-Xw\|^2$ из урока про выпуклые функции, и там строго доказано, что гессиан такой функции равен $\frac1n X^\top X$ и всегда положительно полуопределён, какой бы ни была матрица данных $X$. Значит, эта функция потерь выпукла всегда, для абсолютно любого датасета. Неважно, стартуешь ли ты из $(0,0)$, как в примере из предыдущего раздела, или из какой-нибудь дикой точки вроде $(1000, -500)$ — при достаточно аккуратной скорости обучения градиентный спуск гарантированно приведёт к одному и тому же ответу $(w^\*, b^\*) = (2{,}3;\ 0{,}5)$. Это ровно то свойство, которое делает линейную регрессию «предсказуемо обучаемой» моделью: результат sklearn.linear_model.LinearRegression не зависит от того, каким методом внутри его считать — прямым решением нормальных уравнений или градиентным спуском.

Пример 2 (невыпуклая функция — два разных локальных минимума). Рассмотрим функцию потерь $L(w) = w^4 - 4w^3 + 2w^2$ — она осознанно невыпукла: у неё три критические точки, $w=0$ (локальный минимум, $L=0$), $w \approx 0{,}382$ (локальный максимум, $L \approx 0{,}090$) и $w \approx 2{,}618$ (глобальный минимум, $L \approx -11{,}090$). Запустим градиентный спуск с одинаковой скоростью обучения $\eta = 0{,}02$, но из двух разных стартовых точек.

Старт $w^{(0)} = -1{,}0$:

Шаг $L'(w)$ $w$ $L(w)$
$1$ $-20{,}0000$ $-0{,}6000$ $1{,}7136$
$2$ $-7{,}5840$ $-0{,}4483$ $0{,}8028$
$4$ $-3{,}1395$ $-0{,}2942$ $0{,}2825$
$6$ $-1{,}7896$ $-0{,}2121$ $0{,}1301$
$8$ $-1{,}1632$ $-0{,}1603$ $0{,}0685$

Старт $w^{(0)} = 4{,}0$:

Шаг $L'(w)$ $w$ $L(w)$
$1$ $80{,}0000$ $2{,}4000$ $-10{,}5984$
$2$ $-4{,}2240$ $2{,}4845$ $-10{,}8964$
$4$ $-1{,}7050$ $2{,}5744$ $-11{,}0684$
$6$ $-0{,}5494$ $2{,}6051$ $-11{,}0882$
$8$ $-0{,}1618$ $2{,}6143$ $-11{,}0900$

Один и тот же алгоритм, одна и та же функция потерь, одна и та же скорость обучения — но старт из $-1{,}0$ плавно сползает к плохому локальному минимуму ($L \to 0$), а старт из $4{,}0$ находит глобальный минимум ($L \to -11{,}09$), почти в двенадцать раз лучше. Это не гипотетическая ситуация, а миниатюрная модель ровно того, что происходит при обучении нейросети: две одинаковые архитектуры с разной случайной инициализацией весов вполне ожидаемо сходятся к разному итоговому качеству, и никакой математической гарантии, что обе попадут в одинаково хорошее решение, не существует.

Пример 3 (седловая точка — «плато, а потом обвал»). Рассмотрим функцию $f(w_1, w_2) = w_1^2 - w_2^2$ — классический пример седла: минимум вдоль оси $w_1$, максимум вдоль оси $w_2$. Стартуем очень близко к седловой точке $(0,0)$, но не точно в ней — из $(3;\ 0{,}01)$, с $\eta = 0{,}1$. Обновление разделяется на два независимых геометрических множителя: $w_1^{(t)} = 3 \cdot 0{,}8^t$ (множитель $1-2\eta=0{,}8$, направление затухает) и $w_2^{(t)} = 0{,}01 \cdot 1{,}2^t$ (множитель $1+2\eta=1{,}2$, направление растёт).

Итерация $w_1$ $w_2$ $f(w_1,w_2)$
$0$ $3{,}0000$ $0{,}0100$ $8{,}9999$
$7$ $0{,}6291$ $0{,}0358$ $0{,}3945$
$10$ $0{,}3221$ $0{,}0619$ $0{,}0999$
$15$ $0{,}1056$ $0{,}1541$ $-0{,}0126$
$20$ $0{,}0346$ $0{,}3834$ $-0{,}1458$
$25$ $0{,}0113$ $0{,}9540$ $-0{,}9099$
$30$ $0{,}0037$ $2{,}3738$ $-5{,}6347$
$35$ $0{,}0012$ $5{,}9067$ $-34{,}8889$
$40$ $0{,}0004$ $14{,}6977$ $-216{,}0228$

Присмотрись к первым десяти итерациям: значение $w_2$ едва заметно растёт ($0{,}0100 \to 0{,}0619$), и на первый взгляд кажется, что процесс уверенно сходится по обеим координатам — $w_1$ быстро уменьшается, а $w_2$ выглядит почти постоянным. Это и есть коварство седловой точки: слабая, но экспоненциально растущая компонента вдоль неустойчивого направления какое-то время остаётся практически незаметной (в реальном обучении нейросети график функции потерь в это время выглядит как «плато» — почти не двигающаяся горизонтальная линия), а затем, когда $w_2$ вырастает достаточно, чтобы её вклад стал заметен, поведение резко меняется — функция обрывается вниз без какого-либо предела. Именно так на практике описывают обучение глубоких сетей: подолгу «залипает» рядом с седловой точкой, а затем внезапно, после долгого затишья, функция потерь резко проваливается.

Почему это важно

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

Расписания скорости обучения и критерии остановки

Интуиция

Фиксированная на всё время обучения скорость $\eta$ — не всегда лучший выбор: в начале обучения, когда точка далеко от минимума, выгодно двигаться крупными шагами, а ближе к минимуму — мелкими, чтобы не «перепрыгивать» его туда-сюда, как ты видел в примере с оврагом выше. Расписание скорости обучения (learning rate schedule) — это правило, по которому $\eta$ постепенно уменьшается по ходу обучения. А раз обучение — процесс итеративный, у него обязательно должен быть и явный сигнал, когда его пора завершить, — критерий остановки.

Формула

Основные расписания скорости обучения. Ступенчатое затухание (step decay): $\eta_t = \eta_0 \cdot \gamma^{\lfloor t/s \rfloor}$ — скорость обучения умножается на коэффициент $\gamma \in (0,1)$ каждые $s$ эпох.

Экспоненциальное затухание: $\eta_t = \eta_0 \cdot e^{-kt}$ — плавное, непрерывное уменьшение с параметром скорости затухания $k$.

Косинусное затухание (cosine annealing): $\eta_t = \eta_{\min} + \tfrac12(\eta_{\max}-\eta_{\min})\bigl(1+\cos(\pi t/T)\bigr)$ — плавно спускается от $\eta_{\max}$ до $\eta_{\min}$ по половине косинусоиды за $T$ шагов.

Основные критерии остановки обучения. Остановка по изменению функции потерь: $|L^{(t)} - L^{(t-1)}| < \varepsilon$. Остановка по норме градиента: $\|\nabla L(w^{(t)})\| < \varepsilon$. Ограничение числа итераций: $t \ge t_{\max}$ (страхует от бесконечного цикла, если два предыдущих критерия так и не сработали).

Разбор примеров

Пример 1 (ступенчатое затухание). Пусть $\eta_0 = 0{,}1$, коэффициент затухания $\gamma = 0{,}5$, шаг расписания $s = 10$ эпох. На эпохе $t=25$: $\eta_{25} = 0{,}1 \cdot 0{,}5^{\lfloor 25/10 \rfloor} = 0{,}1 \cdot 0{,}5^2 = 0{,}025$ — скорость обучения уже дважды уменьшилась вдвое (на эпохах 10 и 20) и составляет четверть от исходной. Такая схема типична для обучения сверточных сетей на изображениях: несколько десятков эпох с относительно крупным шагом для быстрого начального прогресса, а затем несколько плановых снижений для аккуратной финальной подстройки весов.

Пример 2 (экспоненциальное затухание). Пусть $\eta_0 = 0{,}1$, $k = 0{,}01$. На шаге $t=50$: $\eta_{50} = 0{,}1 \cdot e^{-0{,}01 \cdot 50} = 0{,}1 \cdot e^{-0{,}5} \approx 0{,}1 \cdot 0{,}6065 \approx 0{,}0607$. В отличие от ступенчатого расписания, здесь скорость обучения меняется плавно на каждом шаге, а не скачками, — это устраняет резкие «изломы» в поведении обучения на границах ступеней.

Пример 3 (критерий остановки по градиенту и по изменению функции потерь на реальной трассировке). Вернись к примеру 2 из раздела про скорость обучения — сходящемуся спуску с $\eta = 0{,}1$ на функции $L(w) = \frac13\sum(wx_i-y_i)^2$. Возьмём критерий остановки по изменению функции потерь с порогом $\varepsilon = 0{,}001$: между шагом $3$ ($L=0{,}4524$) и шагом $4$ ($L=0{,}4524$) разница по модулю меньше $0{,}0001$ — уже на четвёртом шаге критерий срабатывает, и дальнейшие итерации (пятый, шестой шаг с изменением в четвёртом знаке после запятой) действительно оказались бы избыточной тратой вычислений. Если бы вместо этого использовался критерий по норме градиента с тем же порогом $\varepsilon=0{,}001$, обучение остановилось бы чуть позже — на шаге $4$, где $|\nabla L| = 0{,}0061 > 0{,}001$, ещё не сработал бы, а на шаге $5$, где $|\nabla L| = 0{,}0004 < 0{,}001$, сработал бы. Два разных критерия дают близкий, но не абсолютно идентичный момент остановки — и это нормально: они измеряют разные, хотя и тесно связанные, аспекты сходимости.

Почему это важно

Ни одно реальное обучение модели не запускается «навсегда»: без разумного критерия остановки ты либо потратишь вычислительные ресурсы на тысячи бесполезных итераций после того, как модель фактически уже сошлась, либо, что хуже, рискуешь пройти дальше точки, где начинается переобучение (об этом подробнее — в темах курса про регуляризацию). А расписания скорости обучения — это способ одновременно получить быстрый прогресс в начале обучения и аккуратную, стабильную сходимость в конце, без необходимости вручную подбирать единственное компромиссное значение $\eta$ на всё время обучения.

Практика: 30 заданий

Базовые задания (1–10)

Задание 1: Функция потерь $L(w) = 2w^2$, старт $w_0=3$, скорость обучения $\eta=0{,}1$. Найти $w_1$ после одного шага.


Задание 2: Найти градиент функции потерь $L(w_1,w_2)=w_1^2+3w_2^2$ в точке $(2,-1)$.


Задание 3: Функция потерь $L(w)=w^2$, старт $w_0=6$, скорость обучения $\eta=0{,}05$. Найти $w_1$.


Задание 4 (машинное обучение): Один обучающий пример: $x=2$, $y=10$, модель $\hat y=wx$ (без свободного члена), старт $w_0=0$, $\eta=0{,}05$. Найти $w_1$ после одного шага градиентного спуска по $L(w)=(wx-y)^2$.


Задание 5 (машинное обучение): Пакетный градиент по двум примерам $x=(1,3)$, $y=(2,8)$, модель $\hat y=wx$, точка $w=1$. Найти $\nabla L(1)$ для $L(w)=\frac12\sum_i(wx_i-y_i)^2$.


Задание 6: Норма градиента на текущем шаге $\|\nabla L\|=0{,}0007$, порог критерия остановки $\varepsilon=0{,}001$. Нужно ли останавливать обучение?


Задание 7: На соседних эпохах $L_{\text{prev}}=2{,}503$, $L_{\text{new}}=2{,}501$, порог $\varepsilon=0{,}01$. Сработает ли критерий остановки по изменению функции потерь?


Задание 8: Определить диапазон значений $\eta$, при которых градиентный спуск для функции $L(w)=2w^2$ сходится (старт из любой ненулевой точки).


Задание 9 (машинное обучение): Датасет из $N=1\,000\,000$ примеров, пакетный градиентный спуск делает $T=200$ шагов (эпох) до сходимости. Сколько всего обращений к отдельным примерам данных потребуется за всё обучение?


Задание 10: Для функции $L(w)=w^2$ скорость обучения выбрана равной $\eta=1{,}2$. Что произойдёт с последовательностью $w^{(t)}$?

Средние задания (11–20)

Задание 11: Функция потерь $L(w)=2w^2$, старт $w_0=5$, $\eta=0{,}2$. Найти $w_1$ и $w_2$.


Задание 12 (машинное обучение): Модель $\hat y=wx+b$ с одним обучающим примером $x=1$, $y=5$, текущая точка $(w,b)=(1,1)$, $\eta=0{,}1$. Найти новую точку после одного шага для $L(w,b)=(wx+b-y)^2$.


Задание 13: Для функции из раздела про learning rate ($L(w)=\frac13\sum(wx_i-y_i)^2$, $x=(1,2,3)$, $y=(3,5,6)$), старт $w_0=0$: после одного шага при $\eta=0{,}01$ получаем $L\approx19{,}26$, а при $\eta=0{,}1$ — $L\approx0{,}55$. Объяснить разницу.


Задание 14: Найти критические точки функции $L(w)=w^3-3w$ и классифицировать их (минимум/максимум) через вторую производную.


Задание 15 (машинное обучение): Объяснить, почему при увеличении размера пакета (batch), по которому усредняется градиент, оценка направления градиента становится более устойчивой (менее «шумной»).


Задание 16: Расписание ступенчатого затухания: $\eta_0=0{,}1$, $\gamma=0{,}5$, $s=10$. Найти $\eta$ на эпохе $t=25$.


Задание 17: Экспоненциальное затухание: $\eta_0=0{,}1$, $k=0{,}01$. Найти $\eta$ на шаге $t=50$.


Задание 18 (машинное обучение): Модель обучалась $500$ эпох, но функция потерь на обучающей выборке перестала заметно уменьшаться уже после $200$-й эпохи. Какой критерий остановки стоило использовать, чтобы не тратить лишние $300$ эпох?


Задание 19: Сколько итераций потребуется, чтобы норма градиента для функции $L(w)=2w^2$ при $\eta=0{,}1$ уменьшилась в $1000$ раз по сравнению с начальной?


Задание 20: Косинусное затухание: $\eta_{\min}=0{,}001$, $\eta_{\max}=0{,}1$, $T=100$. Найти $\eta$ на шаге $t=50$ (ровно середина расписания).

Продвинутые задания (21–30)

Задание 21 (машинное обучение): Полная трассировка: модель $\hat y=wx+b$, данные $x=(1,2)$, $y=(4,7)$, старт $(0,0)$, $\eta=0{,}2$. Найти точку после трёх шагов пакетного градиентного спуска.


Задание 22: Используя теорему из урока про выпуклые функции, объяснить, почему точка, в которой градиентный спуск для MSE линейной регрессии останавливается с $\nabla L\approx0$, гарантированно является наилучшим возможным решением.


Задание 23: Для функции $f(w_1,w_2)=\frac12w_1^2-\frac14w_2^2$ найти множители одного шага градиентного спуска по каждой координате при $\eta=0{,}5$ и определить, какое направление устойчиво, а какое — нет.


Задание 24: Возле седловой точки $w_1=w_1^{(0)}$, $w_2=0{,}001$, $\eta=0{,}05$, неустойчивый множитель $(1+2\eta)=1{,}1$. На каком шаге $w_2$ впервые превысит $1$?


Задание 25 (машинное обучение): Датасет $N=10^7$ примеров, пакетному градиентному спуску требуется $T=1000$ шагов до сходимости. Оценить порядок общего числа обращений к отдельным примерам.


Задание 26: Сравнить сходимость $L_1(w)=w^2$ и $L_2(w)=100w^2$ при одинаковой $\eta=0{,}05$.


Задание 27 (машинное обучение): При обучении логистической регрессии ошибка на обучающей выборке продолжает падать, но ошибка на валидационной выборке начинает расти после 30-й эпохи. Какой критерий остановки здесь уместен?


Задание 28: Для функции $L(w)=w^4-4w^3+2w^2$, старт $w_0=1{,}5$, $\eta=0{,}02$. Определить (без полной трассировки, по расположению точки относительно локального максимума $w\approx0{,}382$), к какому минимуму сойдётся метод, а затем сверить с точным расчётом первых двух шагов.


Задание 29: Объяснить, почему невозможно определить заранее, не проведя вычислений (или не изучив форму функции), к какому из двух локальных минимумов сойдётся градиентный спуск на невыпуклой функции, зная только общий вид функции, но не конкретную стартовую точку.


Задание 30 (машинное обучение): При обучении нейросети функция потерь не убывает плавно, а колеблется рывками вверх-вниз на протяжении всего обучения, не показывая явной тенденции к сходимости. Предложить два-три конкретных действия для диагностики и исправления ситуации.

Частые ошибки

Разберём типичные ошибки, которые встречаются практически у каждого, кто впервые настраивает обучение модели градиентным спуском.

  • Выбирают скорость обучения «на глаз», без пробных запусков. Как показано в разделе про скорость обучения, разница между $\eta=0{,}01$, $\eta=0{,}1$ и $\eta=0{,}35$ на одной и той же задаче — это разница между «слишком медленно», «отлично» и «полная расходимость». Единственно верный практический подход — попробовать несколько порядков величины ($0{,}001$, $0{,}01$, $0{,}1$, $1$) и посмотреть на график функции потерь, а не угадывать одно значение заранее.

  • Не замечают признаков расходимости на ранней стадии. Если функция потерь растёт (а не падает) уже на первых нескольких итерациях, или в логах появляется NaN, — это почти всегда означает слишком большую скорость обучения, а не «сложную задачу» или «плохие данные». Первое действие в такой ситуации — резко уменьшить $\eta$, а не менять архитектуру модели.

  • Путают критерий остановки по числу итераций с критерием по сходимости. Ограничение max_iter — это страховка от бесконечного цикла, а не показатель того, что модель действительно обучилась. Модель может «не успеть» сойтись за отведённое число итераций (тогда нужно либо увеличить лимит, либо увеличить скорость обучения), а может, наоборот, сойтись задолго до достижения лимита, впустую тратя вычислительное время на дополнительные эпохи без прогресса.

  • Ожидают от невыпуклой модели той же гарантии, что и от выпуклой. Единственный запуск обучения нейросети с одной случайной инициализацией не даёт никаких математических гарантий оптимальности результата — в отличие от линейной или логистической регрессии. Разумная практика — несколько запусков с разной инициализацией и отбор лучшего по валидационной метрике, а не слепая вера в единственный результат.

  • Используют одну и ту же скорость обучения для признаков совершенно разного масштаба. Как показано в задании 26, если один признак принимает значения порядка единиц, а другой — порядка тысяч, одна и та же скорость обучения может быть подходящей для одной координаты и катастрофической для другой. Нормализация признаков (приведение к одному масштабу) перед обучением — стандартная практика именно по этой причине.

  • Путают знак в формуле шага. Как и в университетском курсе, ошибка $w_{new}=w+\eta\nabla L(w)$ вместо $w_{new}=w-\eta\nabla L(w)$ превращает спуск в подъём — модель будет двигаться в сторону увеличения ошибки. Признак такой ошибки — функция потерь стабильно растёт от эпохи к эпохе, причём предсказуемо, без хаотичных скачков (в отличие от расходимости из-за слишком большого $\eta$).

Главное запомнить

  • Формула шага градиентного спуска $w_{new}=w-\eta\nabla L(w)$ — ядро обучения подавляющего большинства моделей машинного обучения, от линейной регрессии до огромных нейросетей; строгий вывод этой формулы разобран в университетском уроке 212.

  • Скорость обучения $\eta$ — критический гиперпараметр: слишком маленькая делает обучение неоправданно медленным, слишком большая приводит к колебаниям или явной расходимости — обе крайности наглядно видны в численных трассировках этого урока.

  • Пакетный градиентный спуск вычисляет градиент по всему обучающему датасету на каждом шаге, что даёт максимально точное направление движения, но делает каждый отдельный шаг вычислительно дорогим на больших датасетах.

  • Полный цикл обучения модели — это повторяющаяся последовательность из четырёх действий: посчитать предсказание, посчитать функцию потерь и градиент, обновить веса на шаг против градиента, проверить критерий остановки.

  • На выпуклых функциях потерь (MSE линейной регрессии, кросс-энтропия логистической регрессии) градиентный спуск гарантированно сходится к глобальному минимуму — эта гарантия строго следует из теоремы урока про выпуклые функции.

  • На невыпуклых функциях потерь (нейросети) такой гарантии нет: результат зависит от точки старта и может застрять в локальном минимуме или надолго задержаться рядом с седловой точкой, что явно видно в численном примере с «плато, а затем обвалом».

  • Расписания скорости обучения (ступенчатое, экспоненциальное, косинусное затухание) постепенно уменьшают $\eta$ по ходу обучения, сочетая быстрый прогресс в начале с аккуратной сходимостью в конце.

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

  • Метод Коши 1847 года, разработанный для задач небесной механики, спустя почти два столетия без изменения самой формулы шага стал главным инструментом обучения современных нейросетей.

Связь с темами курса

Этот урок — прямое практическое продолжение университетского урока 212, где формула шага $w_{new}=w-\eta\nabla L(w)$ была строго выведена через неравенство Коши-Буняковского и доказано, что антиградиент указывает направление наискорейшего локального убывания функции. Здесь мы взяли эту доказанную формулу как данность и сосредоточились на вопросах, которые встают уже после вывода: как выбрать скорость обучения на практике, как выглядит полный цикл обучения модели и какие инструменты управления сходимостью существуют.

Гарантия сходимости на выпуклых функциях потерь напрямую опирается на теорему из урока 278 про выпуклые функции: любой локальный минимум выпуклой функции автоматически является глобальным. Именно эта теорема — причина, по которой линейная и логистическая регрессия называются «просто оптимизируемыми» моделями, а обучение нейросети — принципиально другая, менее предсказуемая задача, где встречаются локальные минимумы и седловые точки, подробно разобранные с точки зрения гессиана в том же уроке 278 и в университетском уроке 213 про экстремумы функций нескольких переменных.

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

Интересные факты

  • Строка кода optimizer.step(), которая встречается в буквально любом обучающем скрипте на PyTorch, скрывает за собой ровно ту же формулу $w_{new}=w-\eta\nabla L(w)$, которую Коши записал в 1847 году для задач небесной механики — почти два столетия спустя формула не изменилась ни на йоту, изменились лишь скорость вычислений и масштаб задач.

  • В обучении моделей размером в миллиарды параметров один-единственный шаг пакетного градиентного спуска по всему датасету может стоить реальных денег — десятки или сотни долларов вычислительного времени на облачных GPU-кластерах, — что делает выбор эффективного варианта градиентного спуска не только математическим, но и вполне бюджетным вопросом.

  • Явление «плато, а затем внезапный обвал функции потерь», которое ты видел в численном примере с седловой точкой, — задокументированный и хорошо изученный в машинном обучении феномен: исследования показывают, что в пространствах с миллионами параметров подавляющее большинство критических точек невыпуклой функции потерь — это не минимумы и не максимумы, а именно седловые точки, вблизи которых градиентный спуск может «застревать» на заметное число итераций.

  • Расписание скорости обучения с «тёплыми перезапусками» (warm restarts), при котором $\eta$ периодически резко возвращается к высокому значению после долгого затухания, используется именно для того, чтобы намеренно «выбить» траекторию обучения из плоского участка рядом с седловой точкой или неглубоким локальным минимумом, дав ей шанс найти решение получше.

Лайфхаки

  • Прежде чем перебирать значения скорости обучения линейно (0,1; 0,2; 0,3...), перебирай их по порядкам величины (0,0001; 0,001; 0,01; 0,1; 1) — влияние $\eta$ на сходимость нелинейно, и правильный порядок величины обычно важнее точного числа внутри него.

  • Если после первых нескольких итераций функция потерь растёт, а не падает, — не трать время на анализ архитектуры модели или качества данных, сразу уменьши скорость обучения в 5–10 раз и перезапусти: в подавляющем большинстве случаев причина именно в этом.

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

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

  • Для невыпуклых моделей (нейросетей) не полагайся на единственный запуск обучения: запусти обучение несколько раз с разной случайной инициализацией и выбирай лучший результат по валидационной метрике — это простой и надёжный способ частично компенсировать отсутствие теоретической гарантии сходимости к глобальному минимуму.

  • Начинай с чуть большей скорости обучения, чем кажется безопасной, и подключай расписание затухания, а не выбирай сразу маленькое фиксированное значение — так ты получаешь быстрый прогресс в начале обучения и не жертвуешь итоговым качеством в конце, когда $\eta$ уже успеет снизиться.

Если тебе нужно выбрать ровно один алгоритм, который стоит понимать до мельчайших практических деталей, чтобы уверенно чувствовать себя в машинном обучении, — это градиентный спуск. Линейная регрессия, логистическая регрессия, метод опорных векторов, градиентный бустинг, свёрточные сети, трансформеры — за конкретными названиями моделей и архитектур почти всегда стоит один и тот же цикл: посчитать градиент, сделать шаг против него, повторить. Формула проста до неприличия — вычесть из весов градиент, умноженный на маленькое число, — но за этой простотой стоит вся инженерная культура выбора скорости обучения, расписаний затухания, критериев остановки и понимания того, когда можно доверять единственному запуску, а когда нужно перезапускать обучение снова и снова. Освоив это по-настоящему, ты будешь понимать, что происходит внутри optimizer.step(), не как магию, а как конкретный, предсказуемый и управляемый процесс — а это ровно то умение, которое отличает специалиста, который умеет обучать модели, от специалиста, который умеет их чинить, когда они не обучаются.

Понял тему? Закрепи в боте! 🚀

Попрактикуйся на задачах и получи персональные рекомендации от AI

💪 Начать тренировку
💬 Есть вопрос? Спроси бота!