Обобщение, переобучение, регуляризация
Почему модель, выучившая train наизусть, проваливается на новых данных, и как это лечить — bias–variance, кривые обучения, L2, ранняя остановка, кросс-валидация и утечки данных.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Представьте: вы дообучаете Qwen на 500 вопросах службы поддержки. Ставите 10 эпох, loss на train падает до 0.05. Вы довольны, пока не задаёте вопрос, которого не было в данных. Модель отвечает хуже, чем до дообучения, зато обучающие ответы воспроизводит слово в слово. Она запомнила, но не научилась.
Цель машинного обучения — обобщение, обобщающая способностьобобщение, обобщающая способностьEN generalization: хорошо работать на данных, которых модель не видела. Ошибка на train — лишь подсказка, и часто обманчивая. Сегодня разберём, почему так происходит, как это диагностировать и чем лечить. Эти инструменты понадобятся и при обучении LLM: дообучение на маленьких данных переобучается очень легко, а бенчмарки, «протёкшие» в обучающий корпус, дают красивые и бессмысленные цифры.
Недообучение и переобучение
Заголовок раздела «Недообучение и переобучение»Возьмём задачу, где ответ знаем мы, но не модель: . Разберём запись.
- — синус, школьная функция-«волна». Вам не нужно помнить тригонометрию: важна только форма. Пока идёт от 0 до 1, делает ровно одну волну: поднимается от 0 до 1, опускается до −1 и возвращается в 0. Прямая такую кривую заведомо не повторит — это нам и нужно.
- («эпсилон») — случайный шум, своя небольшая случайная добавка к каждой точке. Его стандартное отклонениестандартное отклонениеEN standard deviationКорень из дисперсии; измеряется в тех же единицах, что и сама величина. (урок 0.3.2) равно 0.2: типичное отклонение точки от волны — около 0.2. Дисперсия шума — это нижняя граница MSE: шум не предскажет никакая модель (как в практике прошлого урока).
Дадим модели 12 обучающих точек и будем подгонять к ним полиномы разных степеней. Качество проверим на 1000 новых точек.
Полином степени — это , у него параметр. Удобный приём: составим матрицу признаков со столбцами . Например, при точке соответствует строка , и предсказание равно . Это обычная взвешенная сумма признаков, как у прямой из прошлого урока, только признаков больше. Поэтому говорят, что модель линейна по весам, (матрица на вектор — урок 1.1.2), хотя по она кривая.
Веса подбирает метод наименьших квадратов — минимизация MSE на train. У этой задачи есть готовая формула
, где — обратная матрица из
урока 1.1.3. Откуда эта формула берётся, разберём в
уроке 2.2.1, а пока это одна строка np.polyfit(x, y, d).
| Степень | Параметров | MSE на train | MSE на test | Самый большой коэффициент |
|---|---|---|---|---|
| 0 | 1 | 0.3072 | 0.565 | 0.02 |
| 1 | 2 | 0.1540 | 0.267 | 1.4 |
| 3 | 4 | 0.0131 | 0.049 | 33 |
| 5 | 6 | 0.0125 | 0.048 | 56 |
| 9 | 10 | 0.0035 | 1.850 | 241 533 |
| 11 | 12 | 0.0000 | 143 039 | 2 790 689 858 |
Три режима видны сразу:
- Степени 0–1 — недообучениенедообучениеEN underfitting. Прямая не может изогнуться в синусоиду. Ошибка велика и на train, и на test. Модели не хватает выразительности.
- Степени 3–5 — норма. Ошибка на test ≈ 0.05 при шуме . Лучше почти невозможно.
- Степени 9–11 — переобучениепереобучениеEN overfitting. На train почти ноль, на test катастрофа. У полинома 11-й степени 12 коэффициентов на 12 точек: он проходит через каждую точку, включая шум. Чтобы так извиваться, ему нужны коэффициенты в миллиарды.
Разность «ошибка на test − ошибка на train» называют разрывом обобщения (generalization gap). Растущий разрыв — главный симптом переобучения.
Смещение и разброс
Заголовок раздела «Смещение и разброс»Почему простая модель ошибается иначе, чем сложная? Представьте, что мы много раз собираем новые 20 обучающих точек (и положения точек, и шум каждый раз другие) и каждый раз обучаем модель заново. Получим много немного разных кривых.
- Смещение (bias) — насколько средняя из этих кривых промахивается мимо истины. Прямая промахивается всегда, сколько данных ни дай: это систематическая ошибка.
- Разброс (variance) — насколько кривые отличаются друг от друга. Полином 9-й степени на каждой выборке выдаёт что-то своё: он подстраивается под случайный шум.
Аналогия с дартсом: смещение — все дротики кучно легли, но в стороне от центра. Разброс — дротики вокруг центра, но разлетелись по всей мишени.
Для квадратичной ошибки это разложение можно записать точно. Формулу ниже не нужно запоминать: достаточно понимать, что ошибка складывается из трёх частей. Значки — из урока 1.3.1: — математическое ожиданиематематическое ожидание (матожидание)EN expected value (expectation)E[X] = Σ x·p(x) — среднее значение «в долгую», центр тяжести распределения. (среднее по всем случайным исходам), — дисперсиядисперсияEN varianceVar[X] = E[(X − E[X])²] — средний квадрат отклонения от матожидания. (средний квадрат отклонения от среднего). Пусть , где — истинная зависимость (у нас синус), шум в среднем равен нулю () и имеет дисперсию , а — модель, обученная на случайной выборке . Тогда в каждой точке
Что здесь написано. — среднее по всем возможным обучающим выборкам. — «средняя кривая» из нашего мысленного эксперимента. Слева — средний квадрат ошибки на новом примере. Справа три слагаемых: насколько средняя кривая промахивается мимо истины , насколько отдельные кривые разбросаны вокруг средней, и шум, который не предскажет никто.
Посчитаем честно: 2000 раз сгенерируем по 20 точек, обучим полиномы и усредним слагаемые по сетке из 101 значения от 0 до 1. Например, у кубического полинома выходит .
| Степень | Смещение² | Разброс | Сумма + шум 0.04 |
|---|---|---|---|
| 0 | 0.495 | 0.027 | 0.562 |
| 1 | 0.205 | 0.032 | 0.276 |
| 3 | 0.0055 | 0.017 | 0.063 |
| 5 | 0.0001 | 0.15 | 0.19 |
| 7 | 0.04 | 75 | 75 |
С ростом сложности смещение падает, а разброс растёт. Минимум суммы — посередине, у степени 3. Это и есть компромисс смещения и разбросакомпромисс смещения и разбросаEN bias–variance tradeoff. Если нарисовать ошибку на новых данных в зависимости от сложности модели, получится кривая, похожая на букву U: слева высокая из-за смещения, справа — из-за разброса, внизу — золотая середина.
Разброс у степени 7 огромен, но почти весь (96%) его дают 1% самых неудачных выборок: на краю отрезка не оказалось точек, и полином там «взрывается». Поэтому от запуска к запуску это число меняется в разы. Те же взорвавшиеся выборки сдвигают и «среднюю кривую» — отсюда смещение 0.04 у степени 7 вместо почти нуля у степени 5. У степени 9 разброс уходит в десятки и сотни тысяч.
Кривые обучения
Заголовок раздела «Кривые обучения»Как понять, что именно происходит с вашей моделью? Нарисовать кривые обучениякривая обученияEN learning curve. Их два вида.
Ошибка в зависимости от размера выборки. Медианы по 200 повторам для того же синуса (в каждом повторе — новая обучающая выборка и 1000 новых тестовых точек):
| Обучающих точек | Степень 1: train / test | Степень 9: train / test |
|---|---|---|
| 15 | 0.193 / 0.261 | 0.013 / 7.047 |
| 30 | 0.217 / 0.253 | 0.027 / 0.082 |
| 100 | 0.233 / 0.240 | 0.036 / 0.045 |
| 1000 | 0.235 / 0.236 | 0.039 / 0.040 |
У прямой обе кривые быстро сходятся к ≈ 0.236 и дальше стоят. Это предел для прямой: 0.196 — промах лучшей возможной прямой мимо синусоиды (смещение²) плюс 0.04 шума. Больше данных не поможет — нужна модель сложнее. У полинома 9-й степени огромный разрыв при малых данных, но он закрывается с ростом выборки. Здесь больше данных — лучшее лекарство. Именно поэтому большие модели требуют больших корпусов.
Ошибка в зависимости от эпохи. При итеративном обучении (градиентный спуск, нейросети) модель улучшается шаг за шагом. ЭпохаэпохаEN epochОдин полный проход по обучающим данным. LLM на претрейне часто не делают и одной эпохи. — один полный проход по всем обучающим данным (урок 1.4.1). После каждой эпохи считают loss на train и на validation:
- оба падают — учим дальше;
- train падает, validation остановился и пошёл вверх — началось переобучение, пора останавливаться;
- train почти не падает — проблема в оптимизации (слишком большой или маленький learning rate, неудачные начальные значения весов, баг в коде) или модель слишком проста.
В схеме ниже встретится ещё одна причина плохого validation — сдвиг распределения: новые данные устроены иначе, чем обучающие. Например, модель учили на отзывах о фильмах, а проверяют на отзывах о ресторанах.
flowchart TD A["Loss на train большой?"] -->|да| B["Модель слишком простая<br/>или плохая оптимизация"] B --> B1["Сложнее модель, больше признаков;<br/>проверить lr и код"] A -->|нет| C["Loss на validation большой?"] C -->|да| D["Переобучение<br/>или сдвиг распределения"] D --> D1["Больше данных, регуляризация,<br/>ранняя остановка, проще модель"] C -->|нет| E["Готово — проверяем на test один раз"]
L2-регуляризация
Заголовок раздела «L2-регуляризация»Переобученный полином выдаёт себя гигантскими коэффициентами. Идея регуляризациирегуляризацияEN regularization: штрафовать модель за сложность прямо в функции потерь. Самый популярный штраф — квадрат нормы весов:
Значок — длина (норма) вектора из урока 1.1.1, а квадрат длины — просто сумма квадратов его элементов. Поэтому первое слагаемое — знакомая сумма квадратов ошибок: — вектор ошибок на всех примерах. Второе — сумма квадратов весов , умноженная на число («лямбда»). При штрафа нет; чем больше , тем дороже модели обходятся большие веса.
Пример. Веса , сумма квадратов ошибок равна 1.5, . Штраф: , и . Другие веса ошибаются чуть сильнее, сумма квадратов ошибок 2.0, зато штраф всего , и . С регуляризацией выигрывает вторая, более «скромная» модель.
Для линейной модели решение по-прежнему находится формулой: к из метода наименьших квадратов добавляется . Здесь — единичная матрицаединичная матрицаEN identity matrixКвадратная матрица I с единицами на диагонали и нулями вне её; Ix = x. В NumPy — np.eye(n). из урока 1.1.2 (единицы на диагонали, нули вне её), так что означает «прибавить к каждому диагональному элементу». Это гребневая регрессия (ridge), L2-регуляризациягребневая регрессия (ridge), L2-регуляризацияEN ridge regressionШтраф λ‖w‖² к функции потерь. В нейросетях та же идея называется weight decay. (ridge):
Добавка заодно делает матрицу обратимой, даже когда признаков больше, чем примеров (подробнее — в уроке 2.2.1). Вот полином 9-й степени на тех же 12 точках:
| MSE на train | MSE на test | ||
|---|---|---|---|
| 0 | 0.0035 | 1.850 | 361 661 |
| 0.0085 | 0.074 | 137 | |
| 0.0139 | 0.052 | 20 | |
| 0.0228 | 0.077 | 10.8 | |
| 0.0436 | 0.118 | 4.6 | |
| 1 | 0.1682 | 0.338 | 0.6 |
Ошибка на train с ростом только растёт: модель теряет свободу. Ошибка на test описывает ту же U-кривую: — переобучение, — недообучение, оптимум около . Крошечный штраф уже уменьшил ошибку на test в 25 раз. Число — гиперпараметргиперпараметрEN hyperparameterНастройка, которую не учат градиентом, а выбирают по валидации — степень полинома, λ, learning rate, число слоёв.: градиентом его не найти, его выбирают по валидации.
Ещё три взгляда на тот же штраф:
- Градиент. Производная равна . Каждый шаг спуска сначала умножает веса на , а потом делает обычный шаг (, «эта», — learning rate из урока 1.4.1). Например, при и веса на каждом шаге умножаются на . Веса «затухают» — отсюда название weight decayзатухание весов (weight decay)EN weight decayНа каждом шаге веса умножаются на (1 − ηλ) — «притягиваются» к нулю. Для обычного SGD равносильно L2-регуляризации, для Adam — нет. («затухание весов»). В AdamW из урока 1.4.2 это затухание отделено от адаптивного шага. При предобучении GPT-3 и Llama брали weight decay = 0.1.
- Вероятность (для любознательных, можно пропустить). L2-штраф — это априорное убеждение «до всяких данных», что веса небольшие: априорное распределение (prior) весов — нормальное с центром в нуле. Обучение с ним — уже не чистый метод максимального правдоподобия из урока 1.3.2, а поиск самой вероятной модели с учётом prior по правилу Байеса из урока 1.3.1 (MAP-оценка).
- Практика. Свободный член обычно не штрафуют: сдвиг всех ответов на константу сложности не добавляет. В задании ниже для простоты штрафуются все коэффициенты, включая . У L1-штрафа (сумма модулей вместо суммы квадратов) другое свойство: он обнуляет часть весов, то есть отбирает признаки. Dropout — ещё один регуляризатор нейросетей, о нём в уроке 3.4.2.
Ранняя остановка
Заголовок раздела «Ранняя остановка»У итеративного обучения есть бесплатный регуляризатор — время. Веса стартуют с малых значений и растут по мере обучения. Если остановиться вовремя, они не успеют подстроиться под шум. Для линейных моделей можно показать, что ранняя остановкаранняя остановкаEN early stopping градиентного спуска действует почти как L2-штраф.
Алгоритм:
- После каждой эпохи считаем loss на валидации.
- Если он лучший за всё время — сохраняем веса (чекпоинт).
- Если эпох подряд (patience) улучшений нет — останавливаемся и откатываемся к лучшему чекпоинту.
У LLM та же логика. Чекпоинты сохраняют регулярно, а при SFT главный рычаг — число эпох: на небольших датасетах обычно хватает одной-трёх. Код — в практике ниже.
Кросс-валидация
Заголовок раздела «Кросс-валидация»Валидационная выборка из 20 примеров даёт шумную оценку: повезёт с разбиением — модель выглядит лучше, не повезёт — хуже. Когда данных мало, используют кросс-валидация, перекрёстная проверкакросс-валидация, перекрёстная проверкаEN cross-validationСамый частый вариант — k-кратная кросс-валидация (k-fold cross-validation). по k блокам (k-fold):
- Перемешать данные и разрезать на равных частей (обычно или 10).
- Для каждой части : обучить модель на остальных частях, посчитать ошибку на части .
- Оценка качества: .
Каждый пример ровно один раз побывал в валидации, и все данные поработали на обучение. Цена — обучений вместо одного. Выбираем степень полинома по 5-fold CV на 30 точках:
| Степень | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| MSE на train | 0.494 | 0.287 | 0.284 | 0.031 | 0.031 | 0.025 | 0.025 | 0.024 | 0.023 | 0.023 |
| 5-fold CV | 0.532 | 0.376 | 0.425 | 0.049 | 0.049 | 0.039 | 0.047 | 0.042 | 0.052 | 0.170 |
| Test (1000 точек) | 0.547 | 0.252 | 0.255 | 0.046 | 0.046 | 0.045 | 0.045 | 0.045 | 0.048 | 0.051 |
Ошибка на train монотонно падает и выбрала бы степень 9. CV уверенно отбрасывает степени 0–2 и 9, а степени 3–7 для неё почти равны: разница в пределах шума. В таких случаях часто берут самую простую модель из почти равных. После выбора гиперпараметра модель обучают заново на всех данных.
Варианты: стратифицированная CV сохраняет доли классов в каждой части; групповая не разрывает группы (все снимки пациента в одной части); для временных рядов валидация всегда идёт после обучающего отрезка. Когда какую брать и насколько неверная схема завышает качество — в уроке 2.4.4. Для больших нейросетей CV слишком дорога, поэтому берут одну большую валидационную выборку.
Утечки данных
Заголовок раздела «Утечки данных»утечка данныхутечка данныхEN data leakage — ситуация, когда в обучение или выбор модели просачивается информация, которой не будет при реальном применении. Результат всегда один: оценка лучше, чем реальность. Типичные источники:
- Тест внутри train. Дубликаты и почти-дубликаты: одна и та же фотография в разном размере, один текст с разной разметкой.
- Предобработка по всем данным. Среднее для нормализации, отбор признаков, заполнение пропусков, PCA посчитали до разбиения. Самый коварный вид — демонстрация ниже.
- Утечка целевой переменной. Признак, который знает ответ. Предсказываем уход клиента, а среди признаков — «дата расторжения договора». На тесте блестяще, в продакшене бесполезно.
- Время. Случайное разбиение временного ряда: модель учится на завтрашних данных, чтобы предсказать вчерашние.
- Группы. Один пользователь в train и в test: модель узнаёт пользователя, а не закономерность.
Насколько это опасно? Возьмём 100 объектов, 5000 признаков чистого шума и случайные метки. Предсказывать нечего, честная точность — 50%, как у монетки. Отберём 20 признаков, сильнее всего коррелирующих с меткой, и обучим простой классификатор. Два новых слова:
- Корреляция признака с меткой — число, которое показывает, насколько признак «ходит вместе» с меткой: у объектов с меткой 1 он в среднем заметно больше (или меньше), чем у объектов с меткой 0. Если связи нет, корреляция около нуля. В коде мы считаем её модуль, то есть силу связи без учёта направления.
- Классификатор «ближайший центр» — самый простой из возможных. Усредняем точки каждого класса и получаем два «центра», а новый объект относим к классу, чей центр ближе. Похожую идею вы ещё встретите в k-means (урок 2.3.2).
Код длинный, но читать его можно по комментариям: важны две строки, отмеченные стрелкой ←.
import numpy as np
rng = np.random.default_rng(0)n, p, top = 100, 5000, 20 # 100 объектов, 5000 признаков, отбираем 20 лучшихX = rng.normal(size=(n, p)) # 5000 признаков чистого шума: таблица 100 × 5000y = rng.integers(0, 2, size=n) # случайные метки 0/1: предсказывать НЕЧЕГО
def select(X, y, top): """Индексы признаков, сильнее всего коррелирующих с меткой.""" Xc = (X - X.mean(0)) / X.std(0) # стандартизуем каждый столбец (среднее 0, std 1) corr = np.abs(Xc.T @ (y - y.mean())) / len(y) # сила связи каждого признака с меткой: 5000 чисел return np.argsort(corr)[-top:] # argsort упорядочивает по возрастанию → берём 20 последних
def nearest_centroid(X_tr, y_tr, X_te): """Классификатор «ближайший центр».""" c0, c1 = X_tr[y_tr == 0].mean(0), X_tr[y_tr == 1].mean(0) # центры классов 0 и 1 (маски — урок 0.3.1) d0 = ((X_te - c0) ** 2).sum(1) # квадрат расстояния каждого объекта до центра 0 d1 = ((X_te - c1) ** 2).sum(1) # ... и до центра 1 return (d1 < d0).astype(int) # ближе к центру 1 → класс 1, иначе 0
def cv_accuracy(leaky, k=5): """Точность по 5-fold кросс-валидации (см. раздел выше).""" folds = np.array_split(rng.permutation(n), k) # перемешанные номера, разрезанные на 5 частей по 20 feats_all = select(X, y, top) # ← отбор по ВСЕМ данным, включая валидацию (утечка) accs = [] for i in range(k): va = folds[i] # часть i — валидация tr = np.concatenate([folds[j] for j in range(k) if j != i]) # остальные четыре — train feats = feats_all if leaky else select(X[tr], y[tr], top) # ← честно: отбираем только по train pred = nearest_centroid(X[tr][:, feats], y[tr], X[va][:, feats]) accs.append((pred == y[va]).mean()) # доля верных ответов на валидации return np.mean(accs)
print(f"с утечкой: accuracy = {cv_accuracy(leaky=True):.2f}")print(f"честно: accuracy = {cv_accuracy(leaky=False):.2f}")с утечкой: accuracy = 0.88честно: accuracy = 0.4488% на чистом шуме. Среди 5000 случайных признаков всегда найдутся 20, которые случайно совпали с метками
на этих 100 объектах, включая валидационные. Если отбирать признаки внутри каждого фолда, точность честно падает
до уровня монетки. Правило: всё, что обучается, включая предобработку, обучается только на train. В scikit-learn
для этого есть Pipeline (урок 2.4.4).
Практика
Заголовок раздела «Практика»Соберите полиномиальную регрессию с L2-регуляризацией и выбор степени по валидации. В заготовке четыре функции
с # TODO: mse, poly_features, fit_ridge и select_degree (функция predict уже готова). Делайте их по одной
в этом порядке и после каждой нажимайте «Запустить». Когда всё будет готово, кнопка напечатает ошибки на train и val
для степеней 0, 1, 3, 5, 9 и лучшую степень по валидации среди 0–9. Посмотрите, где начинается переобучение.
Новая для вас функция NumPy — np.linalg.solve(A, b): она находит вектор , для которого . Это то же
самое, что , но считается точнее и быстрее, чем через обратную матрицу.
import numpy as np
def poly_features(x, degree):
"""Матрица [1, x, x², …, x^degree] формы (len(x), degree + 1).
Пример: poly_features([2, 3], 2) → [[1, 2, 4], [1, 3, 9]]
(строка — одна точка, столбец j — x в степени j).
"""
# TODO: ваш код. Шаг 1: x = np.asarray(x, dtype=float) — на вход может прийти список.
# Шаг 2: все степени сразу через broadcasting (урок 0.3.2).
...
def fit_ridge(x, y, degree, lam=0.0):
"""Коэффициенты полинома, минимизирующие ||Xw − y||² + lam·||w||².
Решение в замкнутой форме: w = (XᵀX + lam·I)⁻¹ Xᵀy, где X = poly_features(x, degree).
При lam = 0 это обычный метод наименьших квадратов.
Пригодятся: np.eye(m) — единичная матрица m × m;
np.linalg.solve(A, b) — решение уравнения A·w = b (то же, что inv(A) @ b, но точнее).
"""
# TODO: ваш код
...
def predict(x, w):
"""Значения полинома с коэффициентами w в точках x (уже готово)."""
return poly_features(x, len(w) - 1) @ w
def mse(y, y_hat):
"""Среднеквадратичная ошибка: среднее от (y − y_hat)². Верните float.
Пример: mse([1, 2, 3], [1, 2, 5]) → (0 + 0 + 4) / 3 ≈ 1.3333
"""
# TODO: ваш код
...
def select_degree(x_tr, y_tr, x_val, y_val, degrees, lam=0.0):
"""Обучите полином каждой степени из degrees на train и верните ту степень,
у которой ошибка на ВАЛИДАЦИИ минимальна.
План: для каждой степени d — w = fit_ridge(x_tr, y_tr, d, lam), ошибка mse(y_val, predict(x_val, w));
затем верните ту d, у которой ошибка наименьшая (не номер в списке, а саму степень).
"""
# TODO: ваш код
...
# «Запустить»: train- и val-ошибка для разных степеней
rng = np.random.default_rng(0)
x_tr = rng.uniform(0, 1, 15)
y_tr = np.sin(2 * np.pi * x_tr) + rng.normal(0, 0.2, 15)
x_val = rng.uniform(0, 1, 100)
y_val = np.sin(2 * np.pi * x_val) + rng.normal(0, 0.2, 100)
if poly_features([1.0], 1) is None or fit_ridge(x_tr, y_tr, 1) is None or mse([0.0], [0.0]) is None:
print("Допишите poly_features, fit_ridge и mse — тогда здесь появится таблица ошибок.")
else:
for d in [0, 1, 3, 5, 9]:
w = fit_ridge(x_tr, y_tr, d)
print(f"степень {d}: train {mse(y_tr, predict(x_tr, w)):.4f} val {mse(y_val, predict(x_val, w)):.4f}")
best = select_degree(x_tr, y_tr, x_val, y_val, list(range(10)))
print("лучшая степень по валидации:", best if best is not None else "допишите select_degree")
После решения поэкспериментируйте: вызовите fit_ridge(x_tr, y_tr, 9, lam) для из списка
[0, 1e-8, 1e-6, 1e-4, 1e-2, 1] и найдите лучшее значение по валидации. Данные в задании другие (15 точек), поэтому
точного совпадения с таблицей урока не ждите. Но картина та же: при ошибка на val почти в 20 раз
больше, чем при крошечном штрафе , а при модель уже недообучена.
Ранняя остановка на PyTorch
Заголовок раздела «Ранняя остановка на PyTorch»Запустите локально. Сеть на 66 тысяч параметров учится на 20 точках, шум со стандартным отклонением 0.3, то есть
неустранимая ошибка . PyTorch и нейросети подробно разберём на ступени 3, а сейчас относитесь к сети
как к «чёрному ящику» с настраиваемыми весами и следите только за логикой цикла: шаг обучения, замер на валидации,
чекпоинт, счётчик терпения. Одна эпоха здесь — один шаг спуска по всем 20 точкам сразу. Строки, которые делают
шаг обучения (zero_grad, backward, step), — стандартное заклинание PyTorch: вы встречали его в
уроке 1.4.1, а разберёте до винтика в уроке 3.3.2.
import copyimport torchfrom torch import nn
torch.set_num_threads(1) # для крошечной сети один поток быстрееtorch.manual_seed(2) # сид: при каждом запуске одни и те же «случайные» числаf = lambda x: torch.sin(2 * torch.pi * x) # истинная зависимость — синусx_tr = torch.rand(20, 1); y_tr = f(x_tr) + 0.3 * torch.randn(20, 1) # 20 обучающих точек с шумомx_va = torch.rand(200, 1); y_va = f(x_va) + 0.3 * torch.randn(200, 1) # 200 валидационных
# «Чёрный ящик»: нейросеть из трёх линейных слоёв (ступень 3), ~66 тысяч весовmodel = nn.Sequential(nn.Linear(1, 256), nn.ReLU(), nn.Linear(256, 256), nn.ReLU(), nn.Linear(256, 1))opt = torch.optim.Adam(model.parameters(), lr=1e-3) # оптимизатор Adam из урока 1.4.2loss_fn = nn.MSELoss() # функция потерь — MSE
best_val, best_epoch, best_state = float("inf"), 0, None # inf — «бесконечность»: любой loss будет лучшеpatience, bad = 1000, 0 # сколько эпох терпим без улучшения; счётчик неудачных эпохfor epoch in range(1, 3001): model.train() # режим обучения opt.zero_grad() # стереть градиенты с прошлого шага loss = loss_fn(model(x_tr), y_tr) # предсказания на train и их MSE loss.backward() # посчитать градиенты по всем весам opt.step() # сделать шаг спуска
model.eval() # режим оценки with torch.no_grad(): # при замере градиенты не нужны val = loss_fn(model(x_va), y_va).item() # .item() превращает тензор из одного числа в float if val < best_val: # новый рекорд — запоминаем веса best_val, best_epoch, bad = val, epoch, 0 best_state = copy.deepcopy(model.state_dict()) # копия всех весов — это и есть чекпоинт else: bad += 1 # ещё одна эпоха без улучшения if epoch in (1, 50, 100, 150) or epoch % 250 == 0: # печатаем выборочно print(f"эпоха {epoch:4d}: train {loss.item():.3f} val {val:.3f}") if bad >= patience: # терпение кончилось — останавливаемся print(f"стоп на эпохе {epoch}: лучшая была {best_epoch} (val {best_val:.3f})") break
model.load_state_dict(best_state) # откатываемся к лучшим весамэпоха 1: train 0.558 val 0.604эпоха 50: train 0.131 val 0.185эпоха 100: train 0.052 val 0.113эпоха 150: train 0.046 val 0.102эпоха 250: train 0.045 val 0.103эпоха 500: train 0.041 val 0.107эпоха 750: train 0.036 val 0.111эпоха 1000: train 0.031 val 0.124стоп на эпохе 1138: лучшая была 138 (val 0.102)Первые полторы сотни эпох падают оба loss: модель выучивает саму синусоиду. Дальше train продолжает снижаться
до 0.031 — это ниже уровня шума 0.09. Модель заучивает шум конкретных 20 точек, и val loss растёт.
Лучшей была 138-я эпоха, к ней мы и откатились. Числа на другой версии PyTorch могут немного отличаться, картина — нет. Попробуйте
добавить weight_decay=1e-2 в torch.optim.AdamW вместо Adam и сравните.
Проверь себя
Заголовок раздела «Проверь себя»- Цель — обобщение. Недообучение: обе ошибки высоки. Переобучение: train низкая, validation высокая.
- Ожидаемая ошибка = смещение² + разброс + шум. Сложность модели переливает одно в другое. Большие нейросети живут за пределами классической U-кривой (двойной спуск).
- Кривые обучения подсказывают лекарство: при высоком смещении — сложнее модель, при высоком разбросе — больше данных и регуляризация.
- L2 / weight decay, ранняя остановка и кросс-валидация — базовый набор против переобучения. , степень и число эпох выбирают по валидации.
- Утечки делают оценку красивой и ложной: предобработку учим только на train, а у LLM следим за загрязнением бенчмарков.
Дальше: модуль 2.2 — линейная регрессия, логистическая регрессия и метрики качества с нуля. А классические алгоритмы — kNN, деревья, бустинг — ждут в модуле 2.3.