Перейти к содержанию
Educora
Университет25 мин35 / 42

Машинное обучение в scikit-learn

Единый интерфейс scikit-learn: разбиение данных, линейная и логистическая регрессия, дерево решений, MSE, R², accuracy, матрица ошибок, precision и recall, кросс-валидация и конвейеры, защищающие от утечки данных.

Проверь себя
В этом уроке ты узнаешь
  • Обучать любую модель через интерфейс 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 + шум (цена в тысячах манатов); модель должна найти эти коэффициенты, не зная их.

Python
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]
MSE = (1/n) · ∑ (yᵢ − ŷᵢ)² RMSE = √MSE R² = 1 − ∑ (yᵢ − ŷᵢ)² / ∑ (yᵢ − ȳ)²MSE = (1/n) · ∑ (yᵢ − ŷᵢ)² RMSE = √MSE R² = 1 − ∑ (yᵢ − ŷᵢ)² / ∑ (yᵢ − ȳ)²
где:
  • 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 — интернет не нужен.

p = σ(z) = 1 / (1 + e^(−z)), z = w · x + bp = σ(z) = 1 / (1 + e^(−z)), z = w · x + b
где:
  • σ(z)сигмоида: переводит любое число в интервал (0; 1), σ(0) = 0,5
  • pвероятность принадлежности примера к положительному классу; при p ≥ 0,5 предсказывается «1»
Python
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]]
В матрице ошибок строки — истинные классы, столбцы — предсказанные: один versicolor принят за virginica, а один virginica — за versicolor. predict_proba даёт вероятность каждого класса.

Дерево решений шаг за шагом делит данные вопросами вроде «длина лепестка < 2,45 см?». На каждом шаге выбирается вопрос, после которого группы получаются как можно более «чистыми»; чистоту измеряет неоднородность Джини. Если дерево не ограничивать, оно растёт, пока не запомнит каждый обучающий пример.

G = 1 − ∑ₖ pₖ² Gsplit = (nL / n) · GL + (nR / n) · GRG = 1 − ∑ₖ pₖ² Gsplit = (nL / n) · GL + (nR / n) · GR
где:
  • pₖдоля класса k в узле
  • G0 — узел чистый (один класс); для двух классов максимум 0,5
  • nL, nRчисло примеров в левом и правом дочерних узлах
Пример 1: насколько хорошо разбиение?

В узле 10 примеров: 6 класса A и 4 класса B. Вопрос делит их на левую группу (5 A, 0 B) и правую (1 A, 4 B). Найди неоднородность Джини до и после разбиения.

Показать решение
До: G = 1 − (0,6² + 0,4²) = 1 − (0,36 + 0,16) = 0,48.
Слева только 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; дерево выбирает из всех возможных вопросов тот, что даёт наибольшее уменьшение.
Python
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» означает спам:

Accuracy = (TP + TN) / N Precision = TP / (TP + FP) Recall = TP / (TP + FN) F1 = 2 · P · R / (P + R)Accuracy = (TP + TN) / N Precision = TP / (TP + FP) Recall = TP / (TP + FN) F1 = 2 · P · R / (P + R)
где:
  • Precisionкакая доля названных спамом писем действительно спам
  • Recallкакую долю всего спама удалось поймать
  • F1гармоническое среднее precision и recall
  • Nобщее число примеров
Python
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
Пример 2: парадокс accuracy

Скрининговый тест проверяет 1000 человек, из них 10 больны. Модель находит 8 больных (TP = 8, FN = 2) и даёт 40 ложных тревог (FP = 40, TN = 950). Найди accuracy, precision и recall и сравни с моделью, которая говорит «все здоровы».

Показать решение
Accuracy = (8 + 950) / 1000 = 0,958.
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 результатов. Разброс результатов показывает, насколько надёжна оценка.

CV = (1/k) · ∑ᵢ₌₁ᵏ scoreᵢCV = (1/k) · ∑ᵢ₌₁ᵏ scoreᵢ
где:
  • kчисло блоков, обычно 5 или 10
  • scoreᵢрезультат, когда i-й блок служит тестом
Python
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, и второй полностью «поглощает» расстояние.

Python
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 знака после запятой), затем матрицу ошибок.

Задание · Python
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?

Задание · Python
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.

1 / 10
У модели accuracy на обучении 1,00, а на тесте 0,72. Что это значит?