Постановка задачи машинного обучения
Три составляющие
Любая задача обучения с учителем формулируется одинаково — от линейной регрессии до ChatGPT.
1. Данные. Обучающая выборка из N пар «объект — ответ»:
x_i — вектор признаков (площадь и этаж; 64 пикселя; токены текста), y_i — правильный ответ (цена; цифра; следующее слово).
2. Модель. Семейство функций f_\theta(x) с параметрами \theta. Выбирая архитектуру (прямая, нейросеть из 3 слоёв, трансформер), мы выбираем семейство; конкретная функция определяется значениями параметров.
3. Функция потерь \ell(\hat y, y) — штраф за предсказание \hat y при правильном ответе y. Для регрессии — квадрат ошибки, для классификации — кросс-энтропия (мы вывели обе из правдоподобия).
Обучение — поиск параметров, минимизирующих среднюю потерю на выборке, эмпирический риск:
Так машинное обучение сводится к оптимизации: найти минимум функции L(\theta), где \theta — миллионы чисел. Главный инструмент для этого — градиентный спуск (следующий урок).
import numpy as np
# данные
X = np.array([32, 38, 45, 50, 56, 63, 70, 78, 85, 95], dtype=float)
y = np.array([6.8, 7.9, 9.1, 10.4, 11.0, 12.9, 13.8, 15.6, 16.5, 19.2])
# модель: семейство прямых с параметрами θ = (w, b)
def model(x, theta):
w, b = theta
return w * x + b
# функция потерь и эмпирический риск
loss = lambda pred, target: (pred - target) ** 2
risk = lambda theta: loss(model(X, theta), y).mean()
for theta in [(0.1, 5), (0.2, 0.5), (0.19, 0.3)]:
print(f"θ = {theta}: риск = {risk(theta):.4f}")
Главное: качество на новых данных
Минимизировать ошибку на обучающих данных легко: можно просто запомнить все ответы. Модель «если вход совпадает с одним из обучающих — верни его ответ» имеет нулевую ошибку на выборке и бесполезна на новых объектах.
Нам нужно обобщение: хорошая работа на данных, которых модель не видела. Поэтому данные делят на части:
| Выборка | Доля | Для чего |
|---|---|---|
| Обучающая (train) | ~60–80% | подбор параметров градиентным спуском |
| Валидационная (val) | ~10–20% | выбор гиперпараметров, архитектуры, момента остановки |
| Тестовая (test) | ~10–20% | одна финальная честная оценка |
Не подглядывайте в тест
Каждый раз, когда вы смотрите на тестовую ошибку и что-то меняете, тест превращается в ещё одну валидацию, и итоговая оценка становится завышенной. Тестовая выборка используется один раз — в самом конце.
import numpy as np
import vis
rng = np.random.default_rng(0)
x = np.sort(rng.uniform(-3, 3, 30))
y = np.sin(x) + rng.normal(0, 0.25, 30)
idx = rng.permutation(30)
tr, te = idx[:20], idx[20:]
grid = np.linspace(-3, 3, 200)
curves = {}
for deg in (1, 3, 15):
coef = np.polyfit(x[tr], y[tr], deg)
err_tr = ((np.polyval(coef, x[tr]) - y[tr]) ** 2).mean()
err_te = ((np.polyval(coef, x[te]) - y[te]) ** 2).mean()
print(f"многочлен степени {deg:2}: ошибка train = {err_tr:.3f}, test = {err_te:.3f}")
curves[f"степень {deg}"] = np.clip(np.polyval(coef, grid), -2, 2)
vis.line(grid, curves, title="Простая, подходящая и слишком сложная модель", xlabel="x")
vis.scatter(x[tr], y[tr], title="Обучающие точки")
- Степень 1 — недообучение: модель слишком простая, плохо и на обучении, и на тесте.
- Степень 3 — то, что нужно.
- Степень 15 — переобучение: идеально проходит через обучающие точки, но между ними выделывает дикие петли, и ошибка на тесте огромна.
Борьбе с переобучением посвящён целый урок в модуле о нейросетях.
Параметры и гиперпараметры
- Параметры \theta подбирает алгоритм обучения: веса и сдвиги.
- Гиперпараметры выбирает человек (или перебор по валидации): архитектура, число слоёв и нейронов, шаг обучения, размер батча, число эпох, сила регуляризации.
Типичный цикл работы: выбрать гиперпараметры → обучить на train → оценить на val → поменять гиперпараметры → … → лучшую модель один раз оценить на test.
Подготовка признаков
Признаки бывают в очень разных масштабах: площадь — десятки, цена в рублях — миллионы, доля — от 0 до 1. Градиентный спуск плохо работает, когда масштабы сильно различаются (поверхность потерь становится вытянутым оврагом — это мы видели на функции Розенброка). Поэтому признаки стандартизируют: вычитают среднее и делят на стандартное отклонение.
Утечка данных
Среднее и отклонение считают только по обучающей выборке, а затем применяют те же числа к валидации и тесту. Если считать их по всем данным, информация из теста «просочится» в обучение.
Базовое решение (baseline)
Прежде чем радоваться результату, сравните его с тупейшим решением:
- регрессия — всегда предсказывать среднее значение y по обучающей выборке;
- классификация — всегда предсказывать самый частый класс;
- случайное угадывание.
Модель, которая не обгоняет baseline, ничему не научилась. Для цифр случайное угадывание даёт 10%, а кросс-энтропия случайной модели — \ln 10 \approx 2.30.
import numpy as np
import ml_data
X, y = ml_data.load_digits()
values, counts = np.unique(y, return_counts=True)
print("самый частый класс:", values[counts.argmax()], f"— baseline-точность {counts.max() / len(y):.1%}")
print(f"кросс-энтропия равномерного угадывания: ln 10 = {np.log(10):.3f}")
Итог
- Задача обучения: данные D, семейство моделей f_\theta, функция потерь \ell; обучение — минимизация эмпирического риска \frac1N\sum \ell(f_\theta(x_i), y_i).
- Цель — обобщение на новые данные: train для параметров, val для гиперпараметров, test — один раз в конце.
- Недообучение — модель слишком проста; переобучение — запоминает выборку.
- Признаки стандартизируют по статистикам обучающей выборки; результат сравнивают с baseline.
Практика решено 0 из 6
Что это — параметр модели (подбирается обучением) или гиперпараметр (выбирает человек)?
| параметр | гиперпараметр | |
|---|---|---|
| Веса линейного слоя | ||
| Шаг обучения (learning rate) | ||
| Число слоёв нейросети | ||
| Сдвиг (bias) нейрона | ||
| Размер батча | ||
| Число эпох обучения |
Модель показала ошибку 0.1% на обучающей выборке и 35% на тестовой. Что произошло?
Для чего нужна валидационная выборка, если уже есть тестовая?
В задаче классификации 95% писем — не спам. Модель говорит «не спам» всегда. Какая у неё точность (accuracy)?
Разбиение данных. Напишите split(X, y, val=0.2, test=0.2, seed=0): перемешайте индексы
(np.random.default_rng(seed).permutation(len(X))) и разбейте данные на три части:
первые round(test·N) перемешанных индексов — тест, следующие round(val·N) — валидация,
остальное — обучение. Верните (X_train, y_train, X_val, y_val, X_test, y_test).
Какие тесты проверяют код (1)
X = np.arange(100).reshape(50, 2); y = np.arange(50)
Xtr, ytr, Xva, yva, Xte, yte = split(X, y)
assert (len(Xtr), len(Xva), len(Xte)) == (30, 10, 10), f"Размеры {len(Xtr), len(Xva), len(Xte)}, ожидалось (30, 10, 10)"
assert sorted(np.concatenate([ytr, yva, yte]).tolist()) == list(range(50)), "Каждый объект ровно в одной части"
assert np.array_equal(Xtr[:, 0] // 2, ytr), "Признаки и ответы должны остаться в парах"
assert not np.array_equal(ytr, np.arange(30)), "Данные нужно перемешать"
import numpy as np
def split(X, y, val=0.2, test=0.2, seed=0):
idx = np.random.default_rng(seed).permutation(len(X))
n_test, n_val = round(test * len(X)), round(val * len(X))
te, va, tr = idx[:n_test], idx[n_test:n_test + n_val], idx[n_test + n_val:]
return X[tr], y[tr], X[va], y[va], X[te], y[te]
Нормализация без утечки. Напишите класс Standardizer с методами fit(X) (запоминает
среднее и стандартное отклонение каждого столбца в атрибутах mean и std) и
transform(X) (возвращает (X - mean) / std). Статистики считаются только на обучающих данных
и потом применяются к любым другим.
Какие тесты проверяют код (1)
tr = np.array([[1.0, 100], [3, 300]]) s = Standardizer().fit(tr) assert np.allclose(s.mean, [2, 200]) and np.allclose(s.std, [1, 100]) assert np.allclose(s.transform(tr), [[-1, -1], [1, 1]]) assert np.allclose(s.transform(np.array([[5.0, 200]])), [[3, 0]]), "Новые данные — со статистиками обучающей выборки"
import numpy as np
class Standardizer:
def fit(self, X):
self.mean = X.mean(axis=0)
self.std = X.std(axis=0)
return self
def transform(self, X):
return (X - self.mean) / self.std
Войдите, чтобы отмечать пройденные уроки.