Машинное обучение звучит загадочно, но идея за ним на удивление простая, и сегодня мы разберём её с нуля — на бытовых примерах, без формул в первых абзацах. В прошлом уроке мы научились держать данные в руках: X — матрица признаков, а y обозначает вектор ответов. Теперь сделаем следующий шаг: поймём, что вообще значит «модель учится», какие бывают задачи, обучим первую настоящую модель (метод k-ближайших соседей) и встретим самое важное правило всего курса — про честную проверку.
Урок длинный и неспешный. Мы не просто запустим model.fit() в две строки — мы увидим глазами, как модель разрезает плоскость на классы, как параметр меняет её поведение, и почему проверка на тех же данных, на которых учились, — это самообман.
Чем ML отличается от обычной программы
В обычном программировании мы сами пишем правила. Хотим отличить спам от не-спама — пишем: «если в письме есть слово „выигрыш“ и три восклицательных знака — спам». Работает, пока спамеры не начнут писать «вы-и-грыш» или «в1игрыш». Тогда мы дописываем ещё правило. И ещё. Беда в том, что таких правил тысячи, они противоречат друг другу, устаревают за неделю, и поддерживать их вручную невозможно.
Машинное обучение переворачивает подход. Мы не пишем правила сами, а показываем компьютеру много примеров («вот это спам, а это — нет») и даём ему самому найти закономерность.
Обычный код: данные + правила → ответ. Машинное обучение: данные + ответы → правила.
Эти «правила», которые модель вывела сама из примеров, и есть результат обучения. В случае со спамом мы скармливаем модели тысячи писем с метками «спам / не спам», а она сама выясняет, какие слова, длины, отправители и сочетания признаков характерны для спама. Когда правил слишком много, чтобы прописать руками, — это территория ML.
Объекты, признаки, ответы
Любая задача ML описывается тремя словами. Мы уже встретили их в прошлом уроке — закрепим и свяжем с обозначениями.
- Объект — то, про что делаем предсказание: квартира, письмо, фото, клиент банка.
- Признаки () — то, что про объект знаем: для квартиры это площадь, этаж, расстояние до центра. Это столбцы матрицы
X. - Ответ, или таргет () — то, что хотим предсказать: цена квартиры, «спам / не спам».
Набор объектов с известными ответами — обучающая выборка. На ней модель учится. Цель — построить модель (обозначим её — просто имя функции, как в школе): она по признакам нового объекта выдаёт предсказание. Предсказание принято писать с «крышечкой» — («игрек с крышкой»), чтобы на письме отличать его от настоящего ответа .
Вся остальная теория курса — это разные способы устроить функцию и разные способы её обучить. Но рамка всегда одна: на вход признаки, на выход предсказание.
Два главных типа задач
Почти всё, с чем столкнёшься в начале, делится на два типа — по тому, какой ответ нужен.
Регрессия — ответ это число на непрерывной шкале: цена квартиры, завтрашняя температура, время в приложении. Ответом может быть 5.3, или 5.31, или 5.312 — любая точка на шкале.
Классификация — ответ это класс из заранее известного списка: спам / не спам; кошка / собака / птица; вернёт клиент кредит или нет. Промежуточных значений нет — только одна из категорий.
Посмотрим на оба типа на настоящих данных. Слева — регрессия: облако точек и непрерывный тренд, по которому для любого признака можно прочитать число. Справа — классификация: два класса (кружки и квадраты), которые нужно разделить границей.
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(1)
# регрессия: ответ — число
xr = rng.uniform(0, 10, size=40)
yr = 2 + 1.3 * xr + rng.normal(0, 1.4, size=40)
# классификация: ответ — класс (два облака точек)
cA = rng.normal([2.2, 2.4], 0.7, size=(30, 2))
cB = rng.normal([4.4, 4.6], 0.7, size=(30, 2))
fig, ax = plt.subplots(1, 2)
ax[0].scatter(xr, yr); ax[0].set_title("Регрессия → число")
ax[1].scatter(cA[:,0], cA[:,1]); ax[1].scatter(cB[:,0], cB[:,1], marker="s")
ax[1].set_title("Классификация → класс")
plt.show()
Простое правило: число — регрессия, категория — классификация. Этого различия хватит надолго.
Есть и третий большой тип — обучение без учителя (когда правильных ответов нет, и модель сама ищет структуру, например группирует похожие объекты в кластеры). До него дойдём позже; пока сосредоточимся на классификации.
Первая модель: «спроси у соседей» (k-NN)
Давай уже что-нибудь обучим. Самая интуитивная модель называется метод k-ближайших соседей (k-Nearest Neighbors, k-NN), и идея в ней буквально бытовая.
Хочешь понять, понравится ли человеку новый фильм? Найди несколько людей с похожими вкусами (ближайших «соседей») и посмотри, как оценили фильм они. Большинство сказало «да» — скорее всего, понравится и ему. Хочешь оценить квартиру? Посмотри на несколько похожих по площади и району и возьми их среднюю цену. Это и есть k-NN.
Как измерить «похожесть»
Чтобы алгоритм мог искать «похожих», нужно формализовать похожесть. Представь каждый объект как точку в пространстве: его признаки — это координаты. Квартира с признаками [площадь=50, этаж=4] превращается в точку (50, 4). Тогда похожие объекты стоят рядом, а «ближайшие соседи» — это буквально ближайшие точки по обычному (евклидову) расстоянию:
Здесь и — два объекта, а обозначают их первые признаки, и так далее по всем признакам. Под корнем — сумма квадратов разностей по каждой координате; это та же теорема Пифагора, просто в измерениях. В коде это одна строка:
import numpy as np
a = np.array([1.0, 2.0])
b = np.array([4.0, 6.0])
print(np.sqrt(((a - b) ** 2).sum())) # → 5.0
(Разности и , квадраты и , сумма , корень — классический египетский треугольник.)
Чтобы классифицировать новый объект, k-NN находит ближайших к нему точек обучающей выборки и устраивает голосование: какого класса среди соседей больше — такой ответ и выдаём.
На картинке новый объект — ромб в центре; берём 3 ближайших точки, и раз кругов среди них больше, чем квадратов (2 против 1), относим объект к классу «круг».
Обучаем k-NN на ирисах
Соберём это в коде. Возьмём встроенный учебный датасет про ирисы — три вида цветков, для каждого измерены длина и ширина чашелистика и лепестка (4 признака):
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
X, y = load_iris(return_X_y=True) # X — 4 признака, y — вид цветка (0/1/2)
print(X.shape, X[0]) # → (150, 4) [5.1 3.5 1.4 0.2]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y) # про разбиение — ниже
model = KNeighborsClassifier(n_neighbors=3) # спрашиваем 3 ближайших соседа
model.fit(X_train, y_train) # «обучение» k-NN = запомнить данные
print(model.score(X_test, y_test)) # → 1.0 — доля верных ответов на тесте
Точность на тесте — 1.0 (100% угаданных видов): ирисы хорошо разделяются по размерам лепестков, поэтому даже простейшая модель справляется идеально. Обрати внимание на странность: у k-NN обучение — это буквально «запомнить обучающую выборку». Никаких коэффициентов он не подбирает. Вся работа происходит в момент предсказания, когда он считает расстояния до всех запомненных точек и ищет ближайших. Такие модели называют ленивыми (lazy): они откладывают всю работу на момент запроса.
Граница решений: как k-NN видит мир
Самое наглядное в k-NN — это граница решений: линия (или поверхность), по которой модель разделяет классы. Возьмём только два признака ириса (длину и ширину лепестка), обучим k-NN и закрасим каждую точку плоскости тем цветом, какой класс модель в ней предсказывает:
from matplotlib.colors import ListedColormap
X2 = X[:, 2:4] # длина и ширина лепестка
model = KNeighborsClassifier(n_neighbors=5).fit(X2, y)
# сетка точек по всей плоскости
xx, yy = np.meshgrid(np.arange(0.5, 7.5, 0.02), np.arange(-0.2, 2.8, 0.02))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.contourf(xx, yy, Z, alpha=0.8) # цветные зоны = предсказания
plt.scatter(X2[:,0], X2[:,1], c=y, edgecolor="k")
plt.xlabel("длина лепестка, см"); plt.ylabel("ширина лепестка, см")
plt.show()
Вот что значит «модель что-то выучила»: она разбила плоскость на зоны, и для любой новой точки ответ — это цвет зоны, в которую она попала. Граница между зонами — там, где голоса соседей делятся пополам. Заметь, что граница у k-NN извилистая, не прямая: она в точности следует за тем, как разбросаны обучающие точки.
Параметры и гиперпараметры
Число соседей (n_neighbors) мы задаём сами до обучения — это гиперпараметр. Хорошая аналогия: гиперпараметры — как ручки на духовке, которые ты выставляешь до готовки (температуру, время). А параметры — то, что «само получается» в процессе обучения из данных. У k-NN параметры — это просто запомненные точки, а у моделей дальше в курсе это числа-коэффициенты (веса), которые модель подбирает сама.
Как влияет на границу? Сравним два крайних случая на искусственных данных с шумом:
for k in [1, 25]:
m = KNeighborsClassifier(n_neighbors=k).fit(X, y)
# ... строим границу решений, как выше ...
- Маленькое (например, 1) — модель слушает одного-единственного ближайшего соседа и цепляется за случайные выбросы в данных (их называют шумом). Граница получается рваной, с «островками» вокруг отдельных точек. Это переобучение.
- Большое — модель усредняет слишком многих соседей, граница чрезмерно сглаживается, теряются настоящие детали. Это недообучение.
Правильное подбирают, проверяя модель на отложенных данных. И тут возникает важнейшая идея всего ML.
Честная проверка: train / test
Мы обучили модель и хотим понять, насколько она хороша. Логично измерить, сколько ответов она угадывает. Но есть ловушка.
Если проверять модель на тех же данных, на которых она обучалась, она «жульничает» — она их уже видела. С k-NN это особенно наглядно: при ближайший сосед каждой обучающей точки — она сама (расстояние 0), поэтому на обучающих данных k-NN угадывает ровно 100% и полностью нас обманывает.
Это как готовиться к экзамену по задачнику с ответами (train), а проверять себя на тех же задачах. Угадать заученное — не то же самое, что понять. Честная проверка — это новые задачи на самом экзамене (test).
Решение — разделить данные на две части до всякого обучения:
- train (обучающая, 70–80%) — на ней модель учится;
- test (тестовая, 20–30%) — модель её никогда не видела, по ней меряем качество.
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, # 20% в тест; random_state фиксирует разбиение
stratify=y # сохранить пропорции классов в train и test
)
random_state=42 — любое фиксированное число: оно делает случайное разбиение воспроизводимым, чтобы у тебя и у меня получились одинаковые train и test (само число значения не имеет, а 42 используют просто по традиции). А stratify=y следит, чтобы доли классов в train и test совпадали с исходными: ирисы в датасете идут подряд по видам, и без стратификации в тест мог бы попасть перекос (скажем, почти один вид).
Видим переобучение на графике
Теперь главное наблюдение урока. Переберём от 1 до 25 и для каждого нарисуем точность отдельно на train и на test:
ks = range(1, 26)
train_acc, test_acc = [], []
for k in ks:
m = KNeighborsClassifier(n_neighbors=k).fit(X_train, y_train)
train_acc.append(m.score(X_train, y_train))
test_acc.append(m.score(X_test, y_test))
plt.plot(list(ks), train_acc, label="train")
plt.plot(list(ks), test_acc, label="test")
plt.xlabel("число соседей k"); plt.ylabel("доля верных ответов")
plt.legend(); plt.show()
Смотри на расхождение кривых. При train-точность равна 1.0 — модель идеально «помнит» обучение, но test-кривая ниже: это и есть переобучение в чистом виде. В середине диапазона test держится высоко — там и живёт хорошее . На большом обе кривые начинают проседать — недообучение. Запомни этот силуэт: разрыв «train высоко, test низко» — главный признак переобучения, и мы будем встречать его весь курс.
Главное правило всего курса. Качество модели оценивается только на данных, которых она не видела при обучении. Это «честная проверка», и обходить её нельзя ни при каких обстоятельствах.
Итог урока
- ML находит правила сам по примерам, а не получает их от программиста: «данные + ответы → правила».
- Любая задача — это объекты, их признаки и ответы ; предсказание модели пишут как .
- Число на выходе — регрессия, категория — классификация (есть ещё обучение без учителя).
- k-NN — первая модель: «найди ближайших соседей по расстоянию и проголосуй»; обучение у неё = просто запомнить данные.
- Граница решений — то, как модель разбивает пространство на классы; у k-NN она извилистая и зависит от .
- Гиперпараметры (как ) мы задаём сами до обучения; параметры модель подбирает по данным сама.
- Маленькое → переобучение (рваная граница), большое → недообучение (пересглаженная).
- Качество меряем только на тесте; разрыв «train высоко, test низко» — признак переобучения.
В следующем уроке построим модель — линейную регрессию, которая не запоминает данные целиком, а выводит из них компактный закон в виде формулы.
Домашнее задание
Задания идут от простых к сложным. 🧠 — теория, 💻 — код, 🔮 — «что будет, если…». Сначала попробуй сам, потом открой разбор.
- 🧠 Приведи три задачи из жизни и определи для каждой: регрессия или классификация? Что объект, признаки, ответ?
Разбор
Примеры: (а) предсказать завтрашнюю температуру — регрессия, объект = день, признаки = сегодняшняя погода/влажность/сезон, ответ = число градусов. (б) распознать, есть ли на фото кошка — классификация, объект = фото, признаки = пиксели, ответ = да/нет. (в) предсказать время доставки заказа — регрессия, объект = заказ, признаки = расстояние/загрузка/погода, ответ = минуты. Ключ: если ответ — число на шкале, регрессия; если категория, классификация.
- 🧠 Объясни своими словами разницу «данные + правила → ответ» и «данные + ответы → правила» на примере спама.
Разбор
В обычном коде мы пишем правила («если есть слово выигрыш — спам») и применяем их к письму, чтобы получить ответ. В ML мы даём модели письма вместе с готовыми ответами (спам/не спам), а модель сама выводит правила, которые мы не формулировали. Преимущество: когда признаков спама тысячи и они меняются, человек не успевает писать правила вручную, а модель переучивается на новых данных.
- 🧠 В k-NN с модель даёт ровно 100% на обучающих данных. Почему это не значит, что она хорошая?
Разбор
Потому что для каждой обучающей точки её ближайший сосед — это она сама (расстояние 0), значит ответ всегда совпадает с истинным. Это измерение на данных, которые модель «видела», — она их просто помнит, а не понимает закономерность. Честная оценка — на тесте, где новой точке соседями служат другие объекты. Там k=1 обычно слабее, чем k=5–10.
- 💻 Возьми пример с ирисами из урока, обучи k-NN с k=3 и посчитай точность на тесте. Получилось выше 0.9?
Разбор
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
X, y = load_iris(return_X_y=True)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
m = KNeighborsClassifier(n_neighbors=3).fit(Xtr, ytr)
print(m.score(Xte, yte)) # → 1.0Да, с таким разбиением получается 1.0 — ирисы хорошо разделимы. На других данных так красиво бывает редко.
- 🔮 Перебери от 1 до 20 и построй график точности на тесте. При каком лучше? Что происходит на краях диапазона?
Разбор
Код — как в разделе про переобучение. На малом (1–2) test обычно чуть ниже из-за чувствительности к шуму; на большом (ближе к 20) начинает падать из-за пересглаживания. Лучшие значения — в середине (для ирисов часто 3–7). Точные числа зависят от random_state, но силуэт «провал на краях, плато в середине» устойчив.
- 🧠 Чем гиперпараметр отличается от параметра? Приведи по примеру каждого.
Разбор
Гиперпараметр мы задаём сами до обучения, он не выводится из данных: пример — число соседей в k-NN. Параметр модель подбирает сама в процессе обучения из данных: у k-NN это запомненные обучающие точки, у линейной регрессии (следующий урок) — коэффициенты . Гиперпараметры настраивают по тесту/валидации, параметры — автоматически на train.
- 🔮 Почему k-NN сильно замедляется на больших данных? (Подумай, что он делает в момент предсказания.)
Разбор
Чтобы классифицировать один новый объект, k-NN считает расстояние до всех запомненных обучающих точек, а потом ищет среди них ближайших. Если в обучении миллион объектов — миллион вычислений расстояний на каждый запрос. Обучение у k-NN мгновенное (просто запомнить), но предсказание дорогое и растёт линейно с размером данных. Поэтому на больших данных он медленный, и применяют ускорители (KD-деревья) или другие модели.