- Обучать любую модель через интерфейс
fit,predict,scoreи правильно разбивать данные - Оценивать регрессию через MSE, RMSE и R², а классификацию — через accuracy, precision, recall и F1
- Распознавать переобучение дерева решений и вычислять неоднородность Джини
- Оценивать модель без утечки с помощью кросс-валидации и конвейера
В уроке «Основные понятия машинного обучения» мы писали градиентный спуск вручную. На практике никто не начинает каждый раз с нуля: библиотека scikit-learn предлагает десятки проверенных моделей, инструменты подготовки данных и метрики — всё с одним простым интерфейсом. В этом уроке ты будешь предсказывать цены квартир, определять виды цветков и, главное, научишься правильно оценивать модель.
Единый интерфейс: fit, predict, score
Данные всегда имеют один вид: матрица признаков X (n примеров × d признаков) и вектор целевой переменной y (длины n). Каждая модель (в scikit-learn — estimator) знает три метода: fit(X, y) обучает, predict(X) предсказывает, score(X, y) измеряет качество. Гиперпараметры передают в конструктор (DecisionTreeClassifier(max_depth=3)), а имена обученных параметров оканчиваются подчёркиванием: coef_, intercept_. Объекты, преобразующие данные (трансформеры), имеют методы fit и transform.
| Класс | Задача | Идея |
|---|---|---|
LinearRegression | регрессия | ŷ = w · x + b, минимум квадратичной ошибки |
LogisticRegression | классификация | линейная оценка превращается в вероятность сигмоидой (softmax) |
DecisionTreeClassifier | классификация | дерево вопросов «да / нет» |
KNeighborsClassifier | классификация | голосование k ближайших соседей |
StandardScaler | трансформер | превращает каждый признак в z-оценку |
Разбиение и линейная регрессия
train_test_split случайно делит данные на обучающую и тестовую части: test_size=0.25 — четверть уходит в тест, random_state делает разбиение воспроизводимым, а в классификации stratify=y сохраняет одинаковые доли классов в обеих частях. Ниже синтетические данные о 200 квартирах созданы по правилу y = 1,5 · площадь + 12 · комнаты + 20 + шум (цена в тысячах манатов); модель должна найти эти коэффициенты, не зная их.
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error, r2_score
rng = np.random.default_rng(0)
area = rng.uniform(40, 140, 200)
rooms = rng.integers(1, 5, 200)
X = np.column_stack([area, rooms])
y = 1.5 * area + 12 * rooms + 20 + rng.normal(0, 10, 200)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
model = LinearRegression().fit(X_train, y_train)
print('coef:', model.coef_.round(2), 'intercept:', round(model.intercept_, 2))
pred = model.predict(X_test)
mse = mean_squared_error(y_test, pred)
print(f'MSE = {mse:.1f}, RMSE = {np.sqrt(mse):.2f}, R2 = {r2_score(y_test, pred):.3f}')
print(X_train.shape, X_test.shape)
print(model.predict([[80, 3]]).round(1))▸ Ожидаемый результат
coef: [ 1.48 11.06] intercept: 23.97 MSE = 95.4, RMSE = 9.76, R2 = 0.960 (150, 2) (50, 2) [175.4]
- yᵢ, ŷᵢистинное значение и предсказание
- RMSEтипичная ошибка в единицах целевой переменной (здесь тысячи манатов)
- R²доля объяснённой вариации: 1 — идеально, 0 — уровень модели, всегда предсказывающей среднее
Модель нашла коэффициенты 1,48 и 11,06 — близко к истинным 1,5 и 12. RMSE ≈ 9,8 — это уровень добавленного шума (σ = 10): модель полностью уловила сигнал, а шум предсказать невозможно.
Классификация: логистическая регрессия и дерево решений
Логистическая регрессия превращает линейную оценку z = w · x + b в вероятность от 0 до 1 с помощью сигмоиды; для более чем двух классов используют её обобщение — softmax. Знаменитый набор iris (классический пример в статистике с 1936 года) содержит 4 измерения 150 цветков 3 видов и входит в scikit-learn — интернет не нужен.
- σ(z)сигмоида: переводит любое число в интервал (0; 1), σ(0) = 0,5
- pвероятность принадлежности примера к положительному классу; при p ≥ 0,5 предсказывается «1»
import numpy as np
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix
X, y = load_iris(return_X_y=True)
print(X.shape, np.bincount(y))
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, stratify=y, random_state=9)
clf = LogisticRegression(max_iter=1000).fit(X_train, y_train)
pred = clf.predict(X_test)
print('accuracy:', round(accuracy_score(y_test, pred), 3))
print(confusion_matrix(y_test, pred))
print(clf.predict_proba(X_test[:2]).round(2))▸ Ожидаемый результат
(150, 4) [50 50 50] accuracy: 0.956 [[15 0 0] [ 0 14 1] [ 0 1 14]] [[0. 0.1 0.9] [0. 0.2 0.8]]
predict_proba даёт вероятность каждого класса.Дерево решений шаг за шагом делит данные вопросами вроде «длина лепестка < 2,45 см?». На каждом шаге выбирается вопрос, после которого группы получаются как можно более «чистыми»; чистоту измеряет неоднородность Джини. Если дерево не ограничивать, оно растёт, пока не запомнит каждый обучающий пример.
- pₖдоля класса k в узле
- G0 — узел чистый (один класс); для двух классов максимум 0,5
- nL, nRчисло примеров в левом и правом дочерних узлах
В узле 10 примеров: 6 класса A и 4 класса B. Вопрос делит их на левую группу (5 A, 0 B) и правую (1 A, 4 B). Найди неоднородность Джини до и после разбиения.
Показать решениеСкрыть решение
Слева только A → GL = 0.
Справа: GR = 1 − (0,2² + 0,8²) = 1 − (0,04 + 0,64) = 0,32.
После: Gsplit = 5/10 · 0 + 5/10 · 0,32 = 0,16.
Неоднородность уменьшилась на 0,48 − 0,16 = 0,32; дерево выбирает из всех возможных вопросов тот, что даёт наибольшее уменьшение.
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
X, y = make_moons(n_samples=400, noise=0.3, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
for depth in [1, 3, 5, None]:
tree = DecisionTreeClassifier(max_depth=depth, random_state=0).fit(X_train, y_train)
train_acc = tree.score(X_train, y_train)
test_acc = tree.score(X_test, y_test)
print(f'max_depth={str(depth):>4}: train {train_acc:.3f}, test {test_acc:.3f}, leaves {tree.get_n_leaves()}')▸ Ожидаемый результат
max_depth= 1: train 0.814, test 0.817, leaves 2 max_depth= 3: train 0.889, test 0.908, leaves 7 max_depth= 5: train 0.907, test 0.892, leaves 15 max_depth=None: train 1.000, test 0.883, leaves 38
make_moons создаёт зашумлённые синтетические данные в виде двух вложенных «полумесяцев». Глубина 1 — недообучение; 3 — лучший результат на тесте; неограниченное дерево запоминает 100% обучающей выборки, а на тесте становится хуже.Метрики помимо accuracy
Для бинарной классификации матрица ошибок scikit-learn устроена так: первая строка — [TN, FP], вторая — [FN, TP]. Здесь TP — верно найденные положительные, FP — ложные тревоги, FN — пропущенные положительные, TN — верно отклонённые отрицательные. В примере со спам-фильтром «1» означает спам:
- Precisionкакая доля названных спамом писем действительно спам
- Recallкакую долю всего спама удалось поймать
- F1гармоническое среднее precision и recall
- Nобщее число примеров
from sklearn.metrics import (accuracy_score, confusion_matrix, f1_score,
precision_score, recall_score)
y_true = [1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0]
y_pred = [1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0]
print(confusion_matrix(y_true, y_pred))
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()
print('TP, FP, FN, TN =', tp, fp, fn, tn)
print('accuracy :', round(accuracy_score(y_true, y_pred), 3))
print('precision:', round(precision_score(y_true, y_pred), 3))
print('recall :', round(recall_score(y_true, y_pred), 3))
print('F1 :', round(f1_score(y_true, y_pred), 3))▸ Ожидаемый результат
[[5 2] [1 4]] TP, FP, FN, TN = 4 2 1 5 accuracy : 0.75 precision: 0.667 recall : 0.8 F1 : 0.727
Скрининговый тест проверяет 1000 человек, из них 10 больны. Модель находит 8 больных (TP = 8, FN = 2) и даёт 40 ложных тревог (FP = 40, TN = 950). Найди accuracy, precision и recall и сравни с моделью, которая говорит «все здоровы».
Показать решениеСкрыть решение
Precision = 8 / (8 + 40) ≈ 0,167 — настоящая лишь каждая шестая тревога.
Recall = 8 / (8 + 2) = 0,8 — найдено 80% больных.
Модель «все здоровы»: accuracy = 990 / 1000 = 0,99 (выше!), но recall = 0 — не найден ни один больной.
Вывод: на несбалансированных данных accuracy обманчива; смотри на recall, precision и F1.
Кросс-валидация и конвейеры
Результат одного тестового разбиения зависит от удачи: другой random_state может дать другое число. k-кратная кросс-валидация делит данные на k частей, обучает модель k раз — каждый раз одна часть служит тестом — и усредняет k результатов. Разброс результатов показывает, насколько надёжна оценка.
- kчисло блоков, обычно 5 или 10
- scoreᵢрезультат, когда i-й блок служит тестом
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
X, y = load_iris(return_X_y=True)
scores = cross_val_score(LogisticRegression(max_iter=1000), X, y, cv=5)
print(scores.round(3))
print(f'mean = {scores.mean():.3f}, std = {scores.std():.3f}')▸ Ожидаемый результат
[0.967 1. 0.933 0.967 1. ] mean = 0.973, std = 0.025
Конвейер (Pipeline) объединяет шаги подготовки и модель в один estimator. При кросс-валидации он в каждом блоке обучает (fit) масштабировщик только на обучающей части этого блока — информация из тестовой части не утекает в модель. Для KNN, основанного на расстояниях, масштабирование решающе: в наборе wine один признак лежит примерно от 11 до 15, другой — от 278 до 1680, и второй полностью «поглощает» расстояние.
from sklearn.datasets import load_wine
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
X, y = load_wine(return_X_y=True)
print(X.shape, X.min(axis=0).round(1)[[0, 12]], X.max(axis=0).round(1)[[0, 12]])
raw = cross_val_score(KNeighborsClassifier(), X, y, cv=5).mean()
pipe = make_pipeline(StandardScaler(), KNeighborsClassifier())
scaled = cross_val_score(pipe, X, y, cv=5).mean()
print(f'without scaling: {raw:.3f}')
print(f'with scaling: {scaled:.3f}')
print([name for name, _ in pipe.steps])▸ Ожидаемый результат
(178, 13) [ 11. 278.] [ 14.8 1680. ] without scaling: 0.691 with scaling: 0.949 ['standardscaler', 'kneighborsclassifier']
Раздели данные iris с test_size=0.3, stratify=y, random_state=1. Обучи DecisionTreeClassifier(max_depth=3, random_state=0). В первой строке выведи accuracy на тесте (2 знака после запятой), затем матрицу ошибок.
from sklearn.datasets import load_iris
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
X, y = load_iris(return_X_y=True)
# split, train, predict, print accuracy and the confusion matrix▸ Ожидаемый результат
0.98 [[15 0 0] [ 0 15 0] [ 0 1 14]]
Для заданных истинных и предсказанных меток выведи на отдельных строках accuracy, precision, recall и F1, каждое с округлением до 3 знаков. Затем проверь вручную: сколько здесь TP, FP и FN?
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
y_true = [0, 1, 1, 0, 1, 0, 1, 1, 0, 0]
y_pred = [0, 1, 0, 0, 1, 1, 1, 0, 0, 0]
# print accuracy, precision, recall and F1 (3 decimals each)▸ Ожидаемый результат
0.7 0.75 0.6 0.667
Главное
- У всех моделей один интерфейс:
fit,predict,score; обученные параметры оканчиваются на_. train_test_split(..., random_state=..., stratify=y)даёт воспроизводимое сбалансированное разбиение.- Регрессия: MSE, RMSE, R²; классификация: accuracy, precision, recall, F1 и матрица ошибок.
- На несбалансированных данных accuracy обманчива; большой разрыв между обучением и тестом — признак переобучения.
- Кросс-валидация даёт более надёжную оценку; конвейер предотвращает утечку, обучая подготовку только на обучающей части каждого блока.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.