Занятие 11
Лабораторное занятие 11¶
Введение в "машинное обучение"¶
Машинное обучение (ML) — это наука, изучающая алгоритмы, автоматически улучшающиеся благодаря опыту. © Яндекс.Образование
Основная задача ML: подобрать алгоритм (модель), которая решала бы соотвествующую задачу на определенном наборе данных. Часто набор данных ограничен и не содержит всех возможных вариаций, но в этом и состоит "магия" машинного обучения — иметь возможность ответить на поставленный вопрос почти для любого набора данных при настройке алгоритма на ограниченных данных
Среди задач ML можно выделить следующие (первые две мы рассмотрим в данной лабораторной работе):
- Задачи регрессии (определение числового параметра по совокупности характеристик объекта)
- Задачи классификации (определение принадлежности объекта к опрределенному классу)
- Задача класстеризации (разбиение выборки на непересекающиеся группы)
- Задача поиска аномалий
- и т.д.
Задачей регрессии может быть предсказание цены на недвижимость при наличии информации о расположении недвижимости, доступности транспорта и т.д.
Задача классификации — определение наличия болезни у пациента по его физическим параметрам.
Для обучения алгоритмов используется несколько основных подходов:
- Обучение с учителем/supervised learning (предоставляется набор размеченных данных)
- Обучение без учителя/unsupervised learning (набор данных не содержит информации о классах, целевых значениях)
- Обучение с подкреплением/reinforcement learning (алгоритм обучается на данных за "награду")
В этой лабораторной работе мы познакомимся с ML на языке Python с помощью библиотеки scikit-learn sklearn.
Библиотека scikit-learn содержит множество моделей. На этом семинаре мы познакомимся с моделями для обучения с учителем: KNeighborsClassifier и DecisionTreeClassifier.
Выполните ячейку ниже для установки библиотеки scikit-learn.
%pip install scikit-learn
Еще нам сегодня понадобится библиотека для красивой отрисовки и получения части данных: seaborn.
%pip install seaborn
Метод 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
Принадлежность точек внутренности окружности радиуса 1 будем считать за принадлежность к некоторому нулевому классу.
Утверждение выше можем сформулировать иначе: нулевая гипотеза нашей модели утверждает, что точка находится внутри окружности радиуса 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");
По распредеелниям можно точно утверждать, что выборка является сбалансрованной (одинаковое количество точек внутри окружности радиуса 1 и вне этой окружности). Координаты точек в окружности скорее всего будут распределены более плотно вокруг координат 0 (обратите внимание на гистограммы во второй группе графиков)
Построим коррреляционную матрицу:
correlation_df = df.corr()
sns.heatmap(correlation_df, vmin=-1, vmax=1, annot=True, cmap="RdBu_r")
plt.figure()
sns.heatmap(correlation_df[np.abs(correlation_df) > 0.75], vmin=-1, vmax=1, annot=True, cmap="RdBu_r");
По виду матрицы нельзя сказать о наличии корреляций между параметрами.
Разбиение данных на выборки¶
Для обучения модели требуется набор данных. Пускай он каким-то образом материализовался у нас. Как убедиться, что полученная модель после обучения является качественной? Как измерить уровень качества обученной модели.
Для этого набор данных разделяется на тренировочную и тестовую выборки (и еще более редко на третью выборку — валидационную).
Для разделения на выборки можно написать свои функции. НО давайте воспользуемся уже готовой функцией train_test_split из подмодуля sklearn.model_selection.
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 (в некотором понимании аналог seed из подмодуля numpy.random).
Упражнение 1. Изобразите на двумерной плоскости тренировочную и тестовую выборки (с помощью разных цветов) и не забудьте отметить принадлежность точки к определенному классу (с помощью разной формы facecolor="None", edgecolor="C1").
# Вставьте ваш код
plt.legend()
plt.xlabel(r"$x_0$")
plt.ylabel(r"$x_1$")
plt.tight_layout()
Теперь же обратися к классу KNeighborsClassifier из sklearn.neighbors. Он реализует метод 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}} $$
Картинка ниже визуализирует смысл матрицы несоответствия.
Дадим несколько интерпретаций для выше указанных TP, TN, FP, FN.
- $\rm True\ Positive$:
- Была правильно определена принадлежность рассматриваемого объекта к нулевому классу;
- Была принята основная (нулевая) гипотеза ($H_0$), при условии ее верности.
- $\rm True\ Negative$:
- Была правильно определена не принадлежность рассматриваемого объекта к нулевому классу;
- Была отклонена основная (нулевая) гипотеза ($H_0$), при условии ее ложности.
- $\rm False\ Positive$:
- Была определена принадлежность рассматриваемого объекта к нулевому классу, хотя объект не относится к нему;
- Была отклонена основная (нулевая) гипотеза ($H_0$), при условии ее верности.
- $\rm False\ Negative$:
- Была определена не принадлежность рассматриваемого объекта к нулевому классу, хотя объект относится к нулевому классу;
- Была принята основная (нулевая) гипотеза ($H_0$), при условии ее ложности.
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
# Вставьте ваш код сюда
Попробуем построить зависимость частоты 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!
Оптимизация параметров модели kNN¶
Давайте попробуем оптимизировать количество соседей.
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 в данной задаче при фиксированном значении n_neighbors=3.
precisions = {}
# Вставьте ваш код сюда
precisions
Преобразование данных, альтернативное решение¶
С двумя координатами работать привычно, но задача обладает симметрией.
Попробуем построить альтернативное решение с использованием информации о расстоянии от центра координат до соответствующей точки.
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$. Сравните результат с предыдущей моделью, которая обучается на одном параметре.
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[["x0", "x1"]].to_numpy()
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()}")
Искусственный набор данных¶
Если с примером про окружность все понятно, то можно приступить к набору данных с большим количеством признаков. Сгенерируем такие псевдоданные с помощью 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, hue="target")
correlation_df = df.corr()
plt.figure()
sns.heatmap(correlation_df, vmin=-1, vmax=1, annot=True);
По виду корреляционной матрицы можно сказать, что часть признаков сильно корррелирует (или антикоррелирует) между собой.
Это поведение вполне очевидно, ведь для создания данных мы указали:
n_repeated=1часть признаков повторяется;n_redundant=1часть признаков является избыточной для описания данных.
Избыточные признаки откидывают из анализа, корреляции между признаками стараются избегать. Их можно откинуть или же регуляризировать (об этом позже).
Задания¶
Задание 1. Попробуйте создать модель классификации методом kNN и обучить ее с коррелированными данными (dataset_corr) и без (dataset_uncorr). Попробуйте проанализировать влияние коррелированных признаков на качество модели.
from sklearn.datasets import make_classification
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import precision_score, recall_score
X_corr, y_corr = make_classification(
n_samples=1000,
n_features=3,
n_classes=2,
n_redundant=1,
random_state=2,
)
df_corr = pd.DataFrame(np.vstack((X_corr.T, y_corr)).T, columns=["x0", "x1", "x2", "target"])
X_uncorr, y_uncorr = make_classification(
n_samples=1000,
n_features=3,
n_classes=2,
n_redundant=0,
random_state=2,
)
df_uncorr = pd.DataFrame(np.vstack((X_uncorr.T, y_uncorr)).T, columns=["x0", "x1", "x2", "target"])
# Ваш код для моделей на коррелированных и некоррелированных данных
Задание 2. Проведите анализ набора данных breast cancer. Обучите модели kNN. Проанализируйте влияние признаков на качество моделей.
from sklearn.datasets import load_breast_cancer
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import precision_score, recall_score
from pandas import DataFrame
dataset = load_breast_cancer(as_frame=True)
df: DataFrame = dataset.frame
print(dataset.DESCR)
df.head()
# Ваш код для реализации модели и анализа данных
Дополнительные материалы¶
Ниже содержится информация о деревьях решений. Это популярный алгоритм классификации данных. Предлагается читателю ознакомится с этим алгоритмом в качестве факультатива.
Дерево решений¶
В программировании есть понятие дерева. Это структура, которая состоит из узлов. У узла может быть родитель и наследники. Если у узла нет родителя, то этот узел является корневым. Если у узла нет наследников, то этот узел является листом.
Среди деревьев выбеляют класс бинарных деревьев. Узлы таких деревьев содержат максимум двух наследников
В каждом узле дерева можно разместить условие на какой-нибудь признак. По проверке первого условия решается куда далее "направлять" признаки. Процесс длится до момента спуска в определенный лист. И уже по листу решается принадлежность набора признаков определенному классу.
Деревья могут использоваться и в задаче регрессии. В таком случае они подгоняют искомую зависимость кусочно-константной функцией.
Вся работа с деревьями содержиться в подмодуле 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 узлов.