Занятие 12
Лабораторное занятие 12¶
Введение в "машинное обучение"¶
Машинное обучение (ML) — это наука, изучающая алгоритмы, автоматически улучшающиеся благодаря опыту. © Яндекс.Образование
Основная задача ML: подобрать алгоритм (модель), которая решала бы соотвествующую задачу на определенном наборе данных. Часто набор данных ограничен и не содержит всех возможных вариаций, но в этом и состоит "магия" машинного обучения — иметь возможность ответить на поставленный вопрос почти для любого набора данных при настройке алгоритма на ограниченных данных
Среди задач ML можно выделить следующие (первые две мы рассмотрим в данной лабораторной работе):
- Задачи регрессии (определение числового параметра по совокупности характеристик объекта)
- Задачи классификации (определение принадлежности объекта к опрределенному классу)
- Задача класстеризации (разбиение выборки на непересекающиеся группы)
- Задача поиска аномалий
- и т.д.
Задачей регрессии может быть предсказание цены на недвижимость при наличии информации о расположении недвижимости, доступности транспорта и т.д.
Задача классификации — определение наличия болезни у пациента по его физическим параметрам.
Для обучения алгоритмов используется несколько основных подходов:
- Обучение с учителем/supervised learning (предоставляется набор размеченных данных)
- Обучение без учителя/unsupervised learning (набор данных не содержит информации о классах, целевых значениях)
- Обучение с подкреплением/reinforcement learning (алгоритм обучается на данных за "награду")
В этой лабораторной работе мы познакомимся с ML на языке Python с помощью библиотеки scikit-learn sklearn.
Библиотека scikit-learn содержит множество моделей. На этом семинаре мы познакомимся с моделями для обучения с учителем: KNeighborsClassifier и DecisionTreeClassifier
Метод k-ближайших соседей¶
Рассмотрим модель k-ближайших соседей (k-nearest neighbors kNN). Интуитивное понимание модели — для выбранного набора данных можно вычислить расстояние между точками. Принадлежность точки к определенному классу определяется "голосованием" k ближайших соседей. Чаще всего k нечетное (почему же?)
Анимация для объяснения метода kNN: ссылка
Здесь оставим наглядную картинку
Расстояние между точками признаков можно считать различными способами: как в евклидовом пространстве, так и косинусоидальной метрикой. Выбор метрики зависит от решаемой задачи.
Метод kNN можно использовать для решения задач регрессии и классфификации. На текущий момент рассмотрим ее использование для задачи бинарной (всего два возможных состояния) классфикации на примере искуственных данных.
Сгенерируем пары точек на двумерной плоскости
import numpy as np
np.random.seed(1)
n_samples = 1000
X = np.random.rand(n_samples, 2) * 2.5 - 1.25
Определим такие точки, которые лежат внутри окружности радиуса 1
y = (X**2).sum(axis=1) < 1
Отобразим получившиеся данные
from matplotlib import pyplot as plt
plt.figure(figsize=(5, 4))
plt.scatter(X[y, 0], X[y, 1], label="in circle")
plt.scatter(X[~y, 0], X[~y, 1], label="out of circle")
plt.legend()
plt.xlabel(r"$x_0$")
plt.ylabel(r"$x_1$")
plt.tight_layout()
Составим из набора точек и меток таблицу
import pandas as pd
df = pd.DataFrame(np.vstack((X.T, y)).T, columns=["x0", "x1", "target"])
df.head()
Каждая строка данных описывается 2 признаками. Каждой строке данных из переменной X соответствует свое значение из y.
Попробуем нарисовать рраспределения признаков
import seaborn as sns
sns.pairplot(df);
sns.pairplot(df, hue="target");
По распредеелниям можно точно утверждать, что выборка является сбалансрованной (одинаковое количество точек в кругу и вне). Координаты точек в окружности скорее всего будут распределены более узко вокруг 0 (обратите внимание на гистограммы во второй группе графиков)
Построим коррреляционную матрицу:
correlation_df = df.corr()
sns.heatmap(correlation_df, vmin=-1, vmax=1, annot=True)
plt.figure()
sns.heatmap(correlation_df[np.abs(correlation_df) > 0.75], vmin=-1, vmax=1, annot=True);
По виду матрицы нельзя сказать о наличии корреляций между параметрами.
Разбиение данных на выборки¶
Стоит задаться вопросом: а как проверить качество модели после обучения?
Для этого набор данных разделяется на тренировочную и тестовую выборки (и еще более редко на третью выборку — валидационную)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
Мы зафиксировали процесс разделения набора данных на тренировочную и тестовую выборки с помощью параметра random_state.
Упражнение 1. Изобразите на двумерной плоскости тренировочную и тестовую выборки (с помощью разных цветов) и не забудьте отметить принадлежность точки к определенному классу (с помощью разной формы facecolor="None").
# Вставьте ваш код
plt.legend()
plt.xlabel(r"$x_0$")
plt.ylabel(r"$x_1$")
plt.tight_layout()
Теперь же обратися к классу KNeighborsClassifier. Он реализует метод kNN
from sklearn.neighbors import KNeighborsClassifier
model_knn = KNeighborsClassifier(n_neighbors=5, metric="euclidean")
У модели есть параметр n_neighbors. Он отвечает за количество соседей, которые используется при оценке параметра.
Еще интересным параметром является metric. Он определяет каким образом вычисляется расстояние между точками. Приведем несколько определений для метрики расстояния:
euclidean$$ \rho(x, y) = \sqrt{\sum\limits_{i = 1}^{N}\left(x_i - y_i\right)^2}$$manhattan$$ \rho(x, y) = \sum\limits_{i = 1}^{N}|x_i - y_i|$$minkowski$$ \rho(x, y) = \left(\sum\limits_{i = 1}^{N}\left(x_i - y_i\right)^p\right)^{1 / p}, p \in (0, 1)$$
Попробуем обучить нашу модель
model_knn.fit(X=X_train, y=y_train)
Теперь можно предсказать значение целевого параметра на тренировочной выборке данных
y_pred = model_knn.predict(X_test)
Определим качество работы нашей модели. Это можно сделать с помощью confusion_matrix и accuracy_score
confusion_matrixвозвращает матрицу несоотвествия (матрицу ошибок); значение в $ij$-элементе соотвествует количеству элементов, которые пренадлежат группе $i$ и были отмечены как элементы группы $j$. Для случая бинарной классификации: $$\begin{pmatrix} \text{True positive}\ (\mathrm{TP}) & \text{False positive}\ \mathrm{(FP)} \\ \text{False negative}\ \mathrm{(FN)} & \text{True negative}\ (\mathrm{TN}) \\ \end{pmatrix}$$accuracy_scoreсоотвествует доле правильно предсказаных меток $$ \text{accuracy score} = \dfrac{\mathrm{TP} + \mathrm{TN}}{\mathrm{TP} + \mathrm{TN} + \mathrm{FP} + \mathrm{FN}} $$
from sklearn.metrics import accuracy_score, confusion_matrix
knn_accuracy = accuracy_score(y_true=y_test, y_pred=y_pred)
knn_confussion_matrix = confusion_matrix(y_true=y_test, y_pred=y_pred)
print(f"Accuracy of kNN: {knn_accuracy}")
print(f"Confussion matrix of kNN:\n {knn_confussion_matrix}")
Дополнительно вводятся метрики качества precision и recall
precisionдоля релевантных объектов среди извлеченных $$ \mathrm{precision} = \dfrac{\mathrm{TP}}{\mathrm{TP} + \mathrm{FP}} $$recallдоля релевантных объектов, которые были извлечены $$ \mathrm{recall} = \dfrac{\mathrm{TP}}{\mathrm{TP} + \mathrm{FN}} $$
Упражнение 2. Вычислите precision и recall для вышеопределенной модели
from sklearn.metrics import precision_score, recall_score
precision_score(y_true=y_test, y_pred=y_pred), recall_score(y_true=y_test, y_pred=y_pred),
Попробуем построить зависимость частоты TP от FP.
from sklearn.metrics import roc_curve
fpr, tpr, treshold = roc_curve(y_true=y_test, y_score=model_knn.predict_proba(X_test)[:, 1])
plt.scatter(fpr, tpr, facecolors="none", edgecolors="C1")
plt.step(fpr, tpr)
plt.plot([0, 1], [0, 1], linestyle="--", color="black")
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC curve')
plt.tight_layout()
Мы получили очень хорошее значение точности для нашей модели. Можем ли мы получить больше? Конечно, если откинем метод kNN и просто начертим окружность радиуса 1!
Давайте попробуем оптимизировать количество соседей
accuracies_knn = []
for n_neighbors in range(1, 20):
model_knn = KNeighborsClassifier(n_neighbors=n_neighbors, metric="euclidean")
model_knn.fit(X_train, y_train)
accuracies_knn.append(accuracy_score(model_knn.predict(X_test), y_test))
plt.plot(range(1, 20), accuracies_knn, linestyle="--", color="black")
plt.scatter(range(1, 20), accuracies_knn)
plt.xlim((0, 21))
plt.xlabel("N neighbors")
plt.ylabel("accuracy")
plt.tight_layout()
По графику можно сказать: 3 соседа является оптимальным выбором.
Упражнение 2. Попробуйте найти оптимальный выбор параметра metric для класса KNeighborsClassifier в данной задаче
Давайте попробуем проанализировать данные с использованием только одного признака: расстояние от центра до выбранной точки
df["r"] = df["x0"]**2 + df["x1"]**2
X = df["r"].to_numpy().reshape((-1, 1))
y = df["target"].to_numpy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
model_knn = KNeighborsClassifier(n_neighbors=3, metric="euclidean")
model_knn.fit(X=X_train, y=y_train)
y_pred = model_knn.predict(X_test)
knn_accuracy = accuracy_score(y_true=y_test, y_pred=y_pred)
knn_confussion_matrix = confusion_matrix(y_true=y_test, y_pred=y_pred)
print(f"Accuracy of kNN: {knn_accuracy}")
print(f"Confussion matrix of kNN:\n {knn_confussion_matrix}")
Получили идеальный классификатор. Но всегда ли он будет таким идеальным? Можем это проверить с помощью механизма кросс-валидации
from sklearn.model_selection import cross_validate
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, random_state=None, shuffle=True)
X = df["r"].to_numpy().reshape((-1, 1))
y = df["target"].to_numpy()
model_cv = KNeighborsClassifier(n_neighbors=3, metric="euclidean")
cv_res = cross_validate(
model_cv,
X,
y,
scoring="accuracy",
cv=kf,
)
print(f"Test accuracy are {cv_res['test_score']}")
print(f"Mean accuracy = {cv_res['test_score'].mean()}")
Упражнение 3. Проведите кросс-валидацию для модели kNN, которая обучается на паре параметров $x_0$ и $x_1$. Сравните результат с предыдущей моделью, которая обучается на одном параметре
Если с примером про окружность все понятно, то можно приступить к набору данных с большим количеством признаков. Сгенерируем такие псевдоданные с помощью make_classification
from sklearn.datasets import make_classification
X, y = make_classification(
n_samples=1000,
n_features=6,
n_classes=2,
n_repeated=1,
n_redundant=1,
random_state=2,
)
df = pd.DataFrame(np.vstack((X.T, y)).T, columns=["x0", "x1", "x2", "x3", "x4", "x5", "target"])
df.head()
Построим распределение признаков и корреляционную матрицу
sns.pairplot(df)
correlation_df = df.corr()
plt.figure()
sns.heatmap(correlation_df, vmin=-1, vmax=1, annot=True);
По виду корреляционной матрицы совместным распределениям можно сказать, что часть признаков сильно корррелирует (или антикоррелирует) между собой.
Это поведение вполне очевидно, ведь для создания данных мы указали:
n_repeated=1часть признаков повторяетсяn_redundant=1часть признаков является избыточной для описания данных
Избыточные признаки откидывают из анализа, корреляции между признаками стараются избегать. Их можно откинуть или же регуляризировать (об этом позже)
Задание 1. Попробуйте создать модель классификации методом kNN и обучить ее с коррелированными данными и без. Попробуйте проанализировать влияние коррелированных признаков на качество модели.
Для улучшения обучения модели иногда приходится нормализовать признаки. Под этим имеют в виду процедуру приведения признаков к нормальному распределению
- Для каждого признака вычисляется среднее и вычитается из всех признаков одновременно
- После смещения признаки масшатабируются делением на разность максимального-минимального значения
Эту процедуру уже реализует класс Normalizer, но частично эту процедуру можно провести с помощью MinMaxScaler.
Задание 3. Реализуйте два различных подхода к препроцессингу признаков и обучите модель kNN снова. Как это отразиться использование трансформированных признаков на качестве модели?
from sklearn.preprocessing import MinMaxScaler, Normalizer
Перейдем к еще одному методу решения проблемы классификации
Дерево решений¶
В программировании есть понятие дерева. Это структура, которая состоит из узлов. У узла может быть родитель и наследники. Если у узла нет родителя, то этот узел является корневым. Если у узла нет наследников, то этот узел является листом.
Среди деревьев выбеляют класс бинарных деревьев. Узлы таких деревьев содержат максимум двух наследников
В каждом узле дерева можно разместить условие на какой-нибудь признак. По проверке первого условия решается куда далее "направлять" признаки. Процесс длится до момента спуска в определенный лист. И уже по листу решается принадлежность набора признаков определенному классу.
Деревья могут использоваться и в задаче регрессии. В таком случае они подгоняют искомую зависимость кусочно-константной функцией.
Вся работа с деревьями содержиться в подмодуле tree
Набор данных ирисы¶
Попробуем воспользоваться деревом решений для классификации объектов. В качестве данных возьмем iris:
sepal_length: длина чашелистикаsepal_width: ширина чашелистикаpetal_length: длина лепесткаpetal_width: ширина лепесткаspecies: вид ириса
df = sns.load_dataset("iris")
df.head()
Отдельно выдилим названия признаков и целевого параметра
feature_names = df.columns[:-1]
target_name = df.columns[-1]
Посмотрим на совместное распределение признаков
sns.pairplot(df, hue="species")
plt.figure()
correlation_df = df[feature_names].corr()
sns.heatmap(correlation_df, vmin=-1, vmax=1, annot=True);
Теперь мы готовы к построению модели
from sklearn import tree
model_tree = tree.DecisionTreeClassifier()
X = df[feature_names].to_numpy()
y = df[target_name].to_numpy()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)
model_tree.fit(X_train, y_train)
Оценим качество модели
from sklearn.metrics import accuracy_score, confusion_matrix
print(accuracy_score(y_true=y_test, y_pred=model_tree.predict(X_test)))
confusion_matrix(y_test, model_tree.predict(X_test))
Дерево решений показывает отличный результат.
Визуализация дерева решений¶
Давайте посмотрим на внутренность дерева. Дерево решений можно визуализировать в текстовом формате
print(tree.export_text(model_tree))
Деревья решений можно визуализировать в графическом формате
tree.plot_tree(model_tree);
Деревья решений легко переобучить. Например, если задать их слишком "глубокими"
model_tree = tree.DecisionTreeClassifier(max_features=1)
model_tree.fit(X_train, y_train)
accuracy_score(y_test, model_tree.predict(X_test)), confusion_matrix(y_test, model_tree.predict(X_test))
tree.plot_tree(model_tree);
Глубоким деревьям не доверяют. Они имеют потенциал к переобучению. Классическая глубина дерева решения составляет от 3 до 5 узлов
Задание 2. В качестве задачи на классификацию возьмем классический набор данных Титаника. Вам предоставляется информация о пассажирах затонувшего лайнера. Попробуйте определить влияние параметров на целевую метрику "survived".
- Определите наиболее влияющие параметры
- Воспользуйтесь алгоритмом дерева решений
- Заставьте дерево переобучиться
- Решите задачу методом kNN
df = sns.load_dataset("titanic")
df.head()
Задание 3. Проведите анализ набора данных breast cancer. Обучите модели kNN и дерева решений. Проанализируйте влияние признаков на качество моделей
from sklearn.datasets import load_breast_cancer
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pandas import DataFrame
dataset = load_breast_cancer(as_frame=True)
df: DataFrame = dataset.frame
print(dataset.DESCR)
df.head()