%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from sklearn import linear_model, svm
from sklearn import metrics
from sklearn.datasets import fetch_openml
from keras import models, layers, utils
import os
База данных MNIST из рукописных цифр - один из наиболее известных наборов данных. Мы возьмем множество изображений размером 28x28 и соответствующие им номера с ресурса https://www.openml.org/, используя функцию fetch_openml из sklearn. Поскольку API ещё сырое, возникла проблема с кешированием и пришлось делать его вручную (внизу).
def load_mnist():
if not os.path.isfile('mnist_X.npy'):
X,y = mnist = fetch_openml('mnist_784', cache=False, return_X_y=True)
np.save('mnist_X',X)
np.save('mnist_y',y)
else:
X = np.load('mnist_X.npy')
y = np.load('mnist_y.npy')
y = np.array([int(yy) for yy in y]) # метки оказались строковыми, преобразуем эти строки в числа.
return X,y
X, y = load_mnist()
Всего 70000 цифр, они представлены в виде массивов серых пикселей (числа от 0 до 255) размером 784 (расплющенный 28x28). Набор данных не отсортирован.
print(X.shape, y.shape)
print(y[:10])
print(X[0,::40]) # выведем каждый 40й пиксель
Matplotlib может отображать такие матрицы пикселей, но желательно указать, что нам нужна черно-белая интерпретация через аргумент cmap.
plt.imshow(X[2].reshape(28,28), cmap=plt.cm.gray_r)
В ходе семинара стало понятно, что нормализация изображения в диапазон 0..1 позволяет обеспечить лучшую сходимость некоторых моделей.
X /= 256
Для скорости будем использовать только часть данных. 3000 цифр для тренировки и 500 для проверки.
X_train, y_train = X[:3000], y[:3000]
X_test, y_test = X[3000:3500], y[3000:3500]
В качестве первой модели попробуем логистическую регрессию, которая опирается на линейное разделение данных.
log_r = linear_model.LogisticRegression(verbose=1)
log_r.fit(X_train, y_train)
Пример предсказаний (сверху) и истинных цифр (снизу)
y_pred = log_r.predict(X_test)
print(y_pred[:20])
print(y_test[:20])
Замерим долю правильных ответов (accuracy) в обучающей выборке.
print(metrics.accuracy_score(y_test, y_pred))
В общем случая, доля правильных ответов не является надежной метрикой для классификаторов, особенно если речь идёт о несбалансированных наборах данных. Например, если один класс встречается в 99% случаев, а другой в 1%, то классификатор, который всегда назначает первый класс, по этой метрики будет иметь 0.99.
Более детальной метрикой является точность (precision) и полнота (recall). Они замеряются отдельно для каждого класса. Введем следующие обозначения:
$TP$ (True Positive) --- количество экземпляров, которым классификатор корректно назначил этот класс.
$FP$ (False Positive) --- количество экземпляров, которым классификатор ошибочно назначил этот класс.
$TN$ (True Negative) --- количество экземпляров, которым классификатор корректно не назначил этот класс.
$FN$ (False Negative) --- количество экземпляров, которым классификатор ошибочно не назначил этот класс.
Тогда точность равна пропорции правильных назначений класса к общему числу назначений этого класса: \begin{equation*} precision = \frac{TP}{TP + FP} \end{equation*} Полнота равна пропорции правильных назначений класса к общему числу экземпляров этого класса. \begin{equation*} recall = \frac{TP}{TP + FN} \end{equation*}
Точность и полнота активно используются как метрики в поисковых системах. В этом контексте точность - это пропорция релевантных документов в поисковой выдаче, а полнота - это пропорция релевантных, попавших в выдачу, к общему числу релевантных документов.
Часто их объединяют в F1-меру - среднее гармоническое этих метрик: \begin{equation*} F1 = \frac{2 * precision * recall}{precision + recall} \end{equation*}
print(metrics.classification_report(y_test, y_pred))
Можно заметить, что цифру 8 обладает наименьшей полнотой (т.е. классификатор достаточно часто ошибочно не идентифицирует эту цифру), а цифра 5 обладает наименьшей точностью (модель часто ошибочно ее назначает).
Матрица ошибок - это матрица, элемент которой $M_{ij}$ равен числу экземпляров класса $i$, которые классификатор пометил как класс $j$.
print(metrics.confusion_matrix(y_test, y_pred))
В целом, здесь нет сильно выделяющихся значений, максимум цифра 5 была перепутана с цифрой 1 3 раза, также как цифра 8 с цифрой 5.
Отметим, что имея эту матрицу можно вычислить точность и полноту: \begin{equation*} precision_i = \frac{M_{ii}}{\sum_j M_{ji}} \end{equation*} т.е. пропорция диагонального элемента к сумме $i$-го столбца. \begin{equation*} recall_i = \frac{M_{ii}}{\sum_j M_{ij}} \end{equation*} т.е. пропорция диагонального элемента к сумме $i$-ой строки.
M = metrics.confusion_matrix(y_test, y_pred)
print("Precision ", np.round(M.diagonal() / M.sum(axis=0), 2))
print("Recall ", np.round(M.diagonal() / M.sum(axis=1), 2))
Рассмотрим примеры перепутываемых цифр, например, 8, перепутанную с 5.
digit = X_test[(y_test == 8) & (y_pred == 5)][0]
plt.imshow(digit.reshape(28,28), cmap=plt.cm.gray_r)
9 c 1
digit = X_test[(y_test == 9) & (y_pred == 1)][0]
plt.imshow(digit.reshape(28,28), cmap=plt.cm.gray_r)
Попробуем также другие классификаторы, в частности SVM с RBF-ядром. В данном случае необходимо искать подходящее значение параметра $\gamma$, которое даст необходимый баланс между обобщаемостью и гибкостью модели (см. лекцию). Туториал будет обновлен под автоматический поиск позднее, но лучшее найденное значение на семинаре было 0.04.
svc = svm.SVC(gamma = 0.04)
# X -= X.mean(axis=0)
svc.fit(X_train, y_train)
y_pred = svc.predict(X_test)
print(metrics.accuracy_score(y_test, y_pred))
print(metrics.classification_report(y_test, y_pred))
print(metrics.confusion_matrix(y_test, y_pred))
Напоследок, натренируем нейронную сеть, используя библиотеку Keras. Данная сеть имеет два скрытых слоя по 300 и 200 элементов и $relu(x) = max(x,0)$ активациями. $softmax$ слой используется для получения распределения вероятностей, также как в мультиномиальной логистической регрессии. keras.utils.to_categorical используется для преобразования y_train в one-hot кодировки (аналогично OneHotEncoder в туториале про pandas).
nn = models.Sequential()
nn.add(layers.Dense(units=300,activation='relu', input_dim=784))
nn.add(layers.Dense(units=200,activation='relu'))
nn.add(layers.Dense(units=10, activation='softmax'))
y_train_oh = utils.to_categorical(y_train, 10)
nn.compile(optimizer='adagrad', loss='categorical_crossentropy')
nn.fit(X_train,y_train_oh,batch_size=64,epochs=40,verbose=2)
y_pred = nn.predict(X_test)
y_pred = y_pred.argmax(axis=1)
print(y_pred)
print(metrics.classification_report(y_test, y_pred))
print(metrics.accuracy_score(y_test, y_pred))