import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import mglearn
import warnings
warnings.filterwarnings(action='ignore', module='scipy', message='^internal gelsd')
%matplotlib inline
Когато искаме да определим колко добре се справя даден модел може да ползваме различни методи за оценяването му. Няма универсална формула – всичко зависи от конкретните данни и целта ни с тях.
Ще разгледаме няколко различни подхода.
Подходите за оценка на регресия и класификация са различни.
Ще започнем с регресията и ще минем към класификацията.
Ще ползваме следните нотации (примери в контекста на определяне на цени на апартаменти).
Още Mean Squared Error (MSE). Това е средно аритметичното на грешките в предвижданията:
$$ \operatorname {MSE} = \frac{1}{n} \sum_{i=1}^{n} \big( \hat{y}_i - y_i \big) ^ 2 $$
Обърнете внимание, че $\hat{y}_i - y_i$ е грешката за $i$-тия елемент (разликата между търсената стойност и какво казва модела).
Когато ползваме линейна регресия (без регуляризация), алгоритъма намира коефициенти $a_0, a_1, \ldots, a_n$, които минимизират тази функция.
Нека да видим пример. Първо, малко данни:
frame = pd.DataFrame({
'y': [1, 3, 5, 10, 24, 55, 77],
'ŷ': [1, 2, 5, 10, 30, 50, 100],
})
frame['correct'] = frame['y'] == frame['ŷ']
frame['difference'] = frame['ŷ'] - frame['y']
frame['squared_error'] = frame.difference**2
frame
frame.squared_error.mean()
Въпрос:
Кой е най-добрият константен предиктор $c$ за горния сет $y$?
"Константен предиктор" ще рече $c = \hat{y}_1 = \hat{y}_2 = \ldots = \hat{y}_n$.
Нека пробваме да го намерим по "инженерен метод" (без математика). Просто ще пробваме числата между -100 и 100.
xs = pd.np.linspace(-100, 100)
ys = [((frame.y - constant) ** 2).mean() for constant in xs]
plt.plot(xs, ys)
plt.grid();
Тази функция изглежда изпъкнала с минимум в constant = 25
.
Интересно, но това е и средното-аритметично на $y$:
frame.y.mean()
Нека да наречем това Constant Model Mean Square Error (CMMSE). Т.е.:
$$ \operatorname{CMMSE} = \frac{1}{n} \sum_{i=1}^{n} \big( \bar{y} - y_i \big)^2 $$
Където $\bar{y}$ е средното-аритметично, т.е.:
$$ \bar{y} = \frac{1}{n} \sum_{i=1}^{n} y_i $$
Най-популярната оценка при регресия е $R^2$. Тя се измерва с:
$$ \operatorname{R^2} = 1 - \frac{\operatorname{MSE}}{\operatorname{CMMSE}} $$
Или по-подробно:
$$ \operatorname{R^2} = 1 - \frac{\frac{1}{n} \sum_{i=1}^n \big( \hat{y}_i - y_i \big)^2}{\frac{1}{n} \sum_{i=1}^n \big( \bar{y} - y_i \big)^2} $$
Обърнете внимание, че:
Накратко, baseline-а на $R^2$ е средното аритметично и оценява на нула. Всичко по-добре е положително число, като максималния резултат е 0. Всичко по-лошо е отрицателно число.
score
на линейната регресия връща $R^2$ score. Това най-полезния score за регресия в повечето практически ситуации.
Да илюстрираме това с малък есперимент. Ще вземем следните данни:
from sklearn.datasets import make_regression
X, y = make_regression(n_features=1, n_samples=100, noise=15.0, random_state=0)
plt.scatter(X, y);
Да натренираме линейна регресия:
from sklearn.linear_model import LinearRegression
model = LinearRegression().fit(X, y)
model.score(X, y)
Видяхме какво връща score
. Нека пробваме горната формула:
1 - ((model.predict(X) - y) ** 2).mean() / ((y.mean() - y) ** 2).mean()
Получаваме същия резултат.
Може да видим и отрицателна стойност с много грешен модел (например винаги отговор 100):
from sklearn.dummy import DummyRegressor
model = DummyRegressor(strategy='constant', constant=100).fit(X, y)
model.score(X, y)
При оптималния константен модел имаме $R^2 = 0$:
model = DummyRegressor(strategy='constant', constant=y.mean()).fit(X, y)
model.score(X, y)
$$ \operatorname{MAE} = \frac{1}{N} \sum_{i=1}^N \big| \hat y_i - y_i \big| $$
Забележете, че константия модел, оптимизиращ MSE не е същия като този, който оптимизира MAE.
xs = np.linspace(-10, 30, 400)
ys = [(constant - frame.y).abs().mean() for constant in xs]
plt.plot(xs, ys)
plt.grid();
scikit-learn ви дава функции за тези метрики:
from sklearn.metrics import mean_absolute_error, r2_score, mean_squared_error
X, y = make_regression(n_features=1, n_samples=100, noise=15.0, random_state=0)
model = LinearRegression().fit(X, y)
y_pred = model.predict(X)
print("mean square error: {}".format(mean_squared_error(y, y_pred)))
print("mean absolute error: {}".format(mean_absolute_error(y, y_pred)))
print("R² score: {}".format(r2_score(y, y_pred)))
Може и да изчислим MSE на логаритмите на $y$ и $\hat{y}$:
$$ \operatorname{MSLE} = \frac{1}{n} \sum_{i=1}^{n} \big( \ln(1 + y_i) - \ln(1 + \hat{y}_i) \big) $$
Тази функция е по-подходяща при експоненциално нарастващи dataset-и.
scikit-learn ви я дава на готово.
from sklearn.metrics import mean_squared_log_error
Класификацията ползва малко по-различни методи за оценка. Нека ги разгледаме.
Най-простата метрика е за колко точки предвиждаме правилно:
$$ \operatorname{Accuracy} = \frac{1}{n} \sum_{i=1}^n \big[ \hat{y}_i = y_i \big] $$
Където $\big[ \hat{y}_i = y_i \big]$ е 1 при правилно предвиждане и 0 при грешно.
Показва колко често моделът предсказва верен клас.
Това прави score
при класификациите в scikit-learn:
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, stratify=cancer.target, random_state=42)
model = LogisticRegression(C=1.0).fit(X_train, y_train)
model.score(X_test, y_test)
Тази метрика е много неподходяща за данни, в които единия клас има много повече представители от другия. Нека да разгледаме какво се случва.
Ще ползваме друг интересен dataset – digits.
from sklearn.datasets import load_digits
digits = load_digits()
fig, axes = plt.subplots(5, 20, figsize=(10, 5), subplot_kw={'xticks': (), 'yticks': ()})
for ax, img in zip(axes.ravel(), digits.images):
ax.imshow(img)
plt.subplots_adjust(wspace=1, hspace=0, top=.5, bottom=0)
Всяка "данна" е просто 8x8 картинка, представена като масив от пиксели:
digits.data[0]
Ако се загледате внимателно, може да видите нулата по-долу:
digits.data[0].reshape(8, 8)
Нека да създадем dataset в който ще се опитваме да отгатваме само 9ките.
y = digits.target == 9
X_train, X_test, y_train, y_test = train_test_split(digits.data, y, random_state=0, stratify=y)
Какво ли прави логистичната регресия:
logistic = LogisticRegression().fit(X_train, y_train)
logistic.score(X_test, y_test)
96% не е никак зле!
Или?
Да видим какво ще направи един константен модел:
from sklearn.dummy import DummyClassifier
dummy = DummyClassifier(strategy='constant', constant=0).fit(X_train, y_train)
dummy.score(X_test, y_test)
Явно може и по-лесно да постигнем 90% точност.
Представете си, че имаме рядко заболяване – имат го само 1% от хората. Може да постигнем 99% точност просто като винаги отговаряме "пациента е здрав". Това не е много добър модел.
За да се справим с това ползваме малко по-различни метрики.
Първата интересна графика е confusion matrix. За да я начертаем ни трябват 4 числа:
Обърнете внимание, че при бинарната класификация често наричаме единия клас "позитивен" ($y = 1$). Това може да е "спам" в мейлите, "злокачествен" при туморите и "девятка" при числата.
Confusion matrix е просто следната таблица:
mglearn.plots.plot_binary_confusion_matrix()
scikit-learn има функция, която дава метриките на confusion matrix:
from sklearn.metrics import confusion_matrix
confusion_matrix(y_test, dummy.predict(X_test))
Така по-лесно може да се ориентираме в резултата и да видим, че има проблем.
Логистичната регресия се справя по-добре:
confusion_matrix(y_test, logistic.predict(X_test))
Хората се радват на шарено. Нека си направим функция за чертане на confusion matrix:
import itertools
def plot_confusion_matrix(y_true, y_pred, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues,
figsize=(9, 7)):
matrix = confusion_matrix(y_true, y_pred)
if normalize:
matrix = matrix.astype('float') / matrix.sum(axis=1)[:, np.newaxis]
plt.figure(figsize=figsize)
plt.imshow(matrix, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = matrix.max() / 2.
for i, j in itertools.product(range(matrix.shape[0]), range(matrix.shape[1])):
plt.text(j, i, format(matrix[i, j], fmt),
horizontalalignment="center",
size=int((figsize[0] / 10) * 38),
color="white" if matrix[i, j] > thresh else "black")
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
plot_confusion_matrix(y_test, logistic.predict(X_test), ['not nine', 'nine'])
Така числата са малко трудни за разбиране. Затова имаме normalize=True
:
plot_confusion_matrix(y_test, logistic.predict(X_test), ['not nine', 'nine'], normalize=True)
Виждаме, че познаваме not-nine в 98% от случаите и nine в 89%.
Как ли се справя глупавия ни модел?
plot_confusion_matrix(y_test, dummy.predict(X_test), ['not nine', 'nine'])
Тук ясно се вижда проблема.
Като цяло, оптималната матрица е с положителни числа по основния диагонал и нули извън него.
Precision е метрика, която се опитва да намали False Positives:
$$ \operatorname{Precision} = \frac{\operatorname{TP}}{\operatorname{TP} + \operatorname{FP}} $$
Тя е, важна когато искаме да сме сигурни в позитивното си предвиждане.
Представете си, че правим система за автоматично бомбардиране на терористи. Алгоритъма преценява дали даден човек е терорист, и ако е такъв, го бомбардираме. Искаме да минимизираме (или отстраним) цивилните жертви – за нас е много важно да сме сигурни в оценката на алгоритъма, когато предвиди "терорист". Съответно, търсим алгоритъм с добър precision.
Аналогичен случай е спама – ако алгоритъма е решил, че определ имейл е спам, искаме да сме много сигурни в това, преди да го изтрием. Предпотичаме имейлите на кантар да не са спам, пред това да изтрием нещо важно.
Recall е аналогична метрика:
$$ \operatorname{Recall} = \frac{\operatorname{TP}}{\operatorname{TP} + \operatorname{FN}} $$
Тя е важна, когато искаме да сме много сигурни, че ще хванем всички позитивни данни.
Добър пример за това са туморите. Цената на false positive е различна от тази на false negative. При false positive ще кажем на здрав пациент, че може да е болен. Това ще струва допълнителни изследвания и малко емоционални мъки. При false negative може да пропуснем да направим лечение и да застрашим живота на пациента. В този случай предпочитаме алгоритъма ни да има висок recall.
Лесен начин да помним двете метрики е TP
разделено на сбора на реда или колоната:
plot_confusion_matrix(y_test, logistic.predict(X_test), ['not nine', 'nine'], figsize=(5, 4))
Precision е 395 разделено на сбора на първата колона, докато recall е 395 разделено на сбора на първия ред.
Ако метрика която взема и двете числа предвид, може да ползваме $f_1$ score. Това е хармоничната средна стойност на двете:
$$ f_1 = 2 \cdot \frac{\operatorname{precision} \cdot \operatorname{recall}}{\operatorname{precision} + \operatorname{recall}} $$
from sklearn.metrics import f1_score
f1_score(y_test, logistic.predict(X_test))
scikit-learn може да ни даде всички метрики.
from sklearn.metrics import classification_report
print(classification_report(y_test, logistic.predict(X_test), target_names=['not nine', 'nine']))
Нека да пробваме с още един dummy класификатор (с магически random state):
dummy = DummyClassifier(random_state=7260, strategy='stratified').fit(X_train, y_train)
Ако гледаме само score
, няма да видим твърде драматична разлика:
print("Dummy score: {}".format(dummy.score(X_test, y_test)))
print("Logistic score: {}".format(logistic.score(X_test, y_test)))
Разликите си личат в пълния report:
print("Dummy Classifier:\n\n", classification_report(y_test, dummy.predict(X_test), target_names=['not nine', 'nine']))
print("Logistic Regression:\n\n", classification_report(y_test, logistic.predict(X_test), target_names=['not nine', 'nine']))
Повечето класификационни модели имат още една хватка – могат да дават увереност колко са убедени в модела си. Нека разгледаме един синтетичен пирмер.
from sklearn.datasets import make_circles
X, y = make_circles(noise=0.25, factor=0.5, random_state=1)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
Да видим как изглежда този dataset:
mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^')
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o');
Да го пробваме с GradientBoostingClasifier
(от семейство decision trees):
from sklearn.ensemble import GradientBoostingClassifier
gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train);
Нека да начертаем границата:
mglearn.tools.plot_2d_separator(gbrt, X, alpha=.4, fill=True, cm=mglearn.cm2)
mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^')
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o');
Повечето класификационни модели имат функция predict_proba
, която показва вероятността на всеки клас (според класификатора):
gbrt.predict_proba(X_test[:6])
Първото число е вероятността за първия клас, второто – за втория. Може да видим, че модела е по-убеден за едни точки, отколкото за други.
Също така, сборът на двете е 1 (до floating point точност).
Моделите имат и decision_function
, който е числото зад тези неща.
Стойността на decision_function
зависи от конкретния модел и обикновено е нещо абстрактно. Вероятността е функция на нейния резултат.
gbrt.decision_function(X_test[:6])
Нека си направим таблица:
frame = pd.DataFrame(
np.hstack((
gbrt.predict_proba(X_test).round(4),
gbrt.decision_function(X_test).reshape(-1, 1).round(4),
gbrt.predict(X_test).reshape(-1, 1),
)),
columns=['blue %', 'red %', 'decision function', 'answer']
)
frame['color'] = np.array(['blue', 'red'])[frame['answer'].astype(int)]
frame
Тук виждаме няколко неща:
Нека наречтаем това на графика:
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
mglearn.tools.plot_2d_separator(gbrt, X, ax=axes[0], alpha=.4, fill=True, cm=mglearn.cm2)
scores_image = mglearn.tools.plot_2d_scores(gbrt, X, ax=axes[1], alpha=.4, cm=mglearn.ReBl)
for ax in axes:
mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^', ax=ax)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o', ax=ax)
ax.set_xlabel("Feature 0")
ax.set_ylabel("Feature 1")
cbar = plt.colorbar(scores_image, ax=axes.tolist())
axes[0].legend(["Test class 0", "Test class 1", "Train class 0", "Train class 1"], ncol=4, loc=(.1, 1.1));
Фонът във втората диаграма показва стойността на decision функцията. Може да съобразим, че алгоритъма сравнява с някакъв treshold и така определя класа. В този конретен случай изглежда, че сравнява с 0.
Бихме могли да го тестваме:
np.all(gbrt.predict(X_test) == (gbrt.decision_function(X_test) > 0))
Ако ползваме decision_function
вместо predict
може да наклоним класификатора в определена посока:
print("df > 0: {}".format(gbrt.predict(X_test).sum()))
print("df > 4: {}".format((gbrt.decision_function(X_test) > 4).sum()))
print("df > -4: {}".format((gbrt.decision_function(X_test) > -4).sum()))
Може да начертаем decision boundary при различните варианти:
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
for (ax, threshold) in zip(axes, [0, 4, -4]):
mglearn.tools.plot_2d_separator(gbrt, X, ax=ax, alpha=.4, threshold=threshold, fill=True, cm=mglearn.cm2)
mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^', ax=ax)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o', ax=ax)
ax.set_title("df > {}".format(threshold))
Виждаме, че при промяна на стойноста, с която сравняваме, започваме да предпочитаме единия клас пред другия.
Гледайки горната таблица, при > 4 имаме увереност над 98%. Така бихме могли да правим trade-off между decision и recall.
print("Report at df > 0:\n")
print(classification_report(y_test, gbrt.predict(X_test)))
print("Report at df > -4:\n")
print(classification_report(y_test, gbrt.decision_function(X_test) > -4))
Може да видим, че за позитивня клас (1) сме увеличили recall-а от 0.81 до 0.94 за сметка на всичко останало. Понякога това може да желано.
Всичко това ни е нуждо да обясним финалните метрики за класификация.
За видим precision-recall curve, нека да започнем с друг dataset:
from mglearn.datasets import make_blobs
X, y = make_blobs(n_samples=(4000, 500), centers=2, cluster_std=[7.0, 2], random_state=22)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
Нека да видим как изглежда:
plt.figure(figsize=(10, 10))
mglearn.discrete_scatter(X_test[:, 0], X_test[:, 1], y_test, markers='^')
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, markers='o');
Ще тренираме друг класификатор (SVC
), който няма да обясняваме подробно. Просто може да знаем, че е класификатор:
from sklearn.svm import SVC
svc = SVC(gamma=.05).fit(X_train, y_train)
Може да видим report с default-ния treshold:
print(classification_report(y_test, svc.predict(X_test)))
Интересно е да видим как ще променят recall и precision, ако променяме treshold-а. Може да начертаем графика с precision_recall_curve
:
from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test, svc.decision_function(X_test))
close_zero = np.argmin(np.abs(thresholds))
plt.plot(precision[close_zero], recall[close_zero], 'o', markersize=10, label='threshold zero', fillstyle='none', c='k', mew=2)
plt.plot(precision, recall, label='precision recall curve')
plt.xlabel('Precision')
plt.ylabel('Recall');
На тази графика виждаме:
Различните модели ще имат различна крива. Да пробваме с друг:
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=100, random_state=0, max_features=2)
forest.fit(X_train, y_train)
precision_rf, recall_rf, tresholds_rf = precision_recall_curve(y_test, forest.predict_proba(X_test)[:, 1])
close_default_rf = np.argmin(np.abs(tresholds_rf - 0.5))
plt.plot(precision_rf, recall_rf, label='rf')
plt.plot(precision_rf[close_default_rf], recall_rf[close_default_rf], '^', c='k', markersize=10, label='treshold 0.5 rf', fillstyle='none', mew=2)
plt.xlabel('Precision')
plt.xlabel('Recall');
Може да видим и двете заедно на една графика:
plt.plot(precision, recall, label='svc')
plt.plot(precision_rf, recall_rf, label='rf')
plt.plot(precision[close_zero], recall[close_zero], 'o', markersize=10, label='treshold zero svc', fillstyle='none', c='k', mew=2)
plt.plot(precision_rf[close_default_rf], recall_rf[close_default_rf], '^', c='k', markersize=10, label='treshold 0.5 rf', fillstyle='none', mew=2)
plt.xlabel('Precision')
plt.xlabel('Recall')
plt.legend(loc='best');
От графиката се вижда, че support vector машината се оправя по-добре в средната точка, но random forest-а се справя по-добре с крайностите. Това не се вижда в $f_1$ score-а:
print("f₁ for SVC: {}".format(f1_score(y_test, svc.predict(X_test))))
print("f₁ for Random Forest: {}".format(f1_score(y_test, forest.predict(X_test))))
Друга метрика е площта под кривата. scikit-learn има функция и за това:
from sklearn.metrics import average_precision_score
print("AUC for SVC: {}".format(average_precision_score(y_test, svc.decision_function(X_test))))
print("AUC for Random Forest: {}".format(average_precision_score(y_test, forest.predict_proba(X_test)[:, 1])))
ROC е друга метрика, която може да погледнем. Вместо recall и precision съпоставя две други метрики – false positive rate и true positive rate:
$$ \operatorname{TPR} = \frac{\operatorname{TP}}{\operatorname{TP}+\operatorname{FN}} $$
$$ \operatorname{FPR} = \frac{\operatorname{FP}}{\operatorname{FP}+\operatorname{TN}} $$
Интуитивно:
Може да си начертаем графика:
from sklearn.metrics import roc_curve
fpr, tpr, tresholds = roc_curve(y_test, svc.decision_function(X_test))
plt.plot(fpr, tpr, label="ROC Curve")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
close_zero = np.argmin(np.abs(tresholds))
plt.plot(fpr[close_zero], tpr[close_zero], 'o', fillstyle='none', c='k', markersize=10);
Идеалната крива е максимално близка до горния ляв ъгъл. От графиката може да видим, че след един момент нататък (около 0.9), може да увеличим recall само на цената на голямо увеличение на FPR.
Може да направим същото за Random Forest класификатора:
fpr_rf, tpr_rf, tresholds_rf = roc_curve(y_test, forest.predict_proba(X_test)[:, 1])
plt.plot(fpr_rf, tpr_rf, label="ROC Curve")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
close_default_rf = np.argmin(np.abs(tresholds_rf - 0.5))
plt.plot(fpr_rf[close_default_rf], tpr_rf[close_default_rf], 'o', fillstyle='none', c='k', markersize=10);
Или и двете заедно:
plt.plot(fpr, tpr, label="svm")
plt.plot(fpr[close_zero], tpr[close_zero], 'o', fillstyle='none', c='k', markersize=10)
plt.plot(fpr_rf, tpr_rf, label="random forest")
plt.plot(fpr_rf[close_default_rf], tpr_rf[close_default_rf], '^', fillstyle='none', c='k', markersize=10);
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.legend(loc='best');
Отново, имаме метрика събираща лицето под кривата:
from sklearn.metrics import roc_auc_score
print("AUC for SVC: {}".format(roc_auc_score(y_test, svc.decision_function(X_test))))
print("AUC for Random Forest: {}".format(roc_auc_score(y_test, forest.predict_proba(X_test)[:, 1])))
При двоична класификация:
$$ \operatorname{LogLoss} = - \frac {1}{n} \sum_{i=1}^n \Big( y_i \ln(p_i) + (1 - y_i) \ln(1 - p_i) \Big) $$
Където:
Пример:
Предсказваме клас 1 с вероятност 0.9 и истинския клас е 1:
$$ \operatorname{LogLoss} = - \big ( 1 * log(0.9) + (1 - 1) * log(1 - 0.9) \big ) = $$
$$ = - \big (-0.105 + 0 \big ) = 0.105 $$
Предсказваме клас 1 с вероятност 0.9 и истинския клас е 0:
$$ \operatorname{LogLoss} = - \big ( 0 * log(0.9) + (1 - 0) * log(1 - 0.9) \big ) = $$
$$ = - \big ( 0 - 2.3 \big ) = 2.3 $$
При много класове:
$$ \operatorname{LogLoss} = - \frac {1}{n} \sum_{i=1}^n \sum_{j=1}^l \sigma_{ij}\ln(p_{ij} ) $$
Където:
from sklearn.metrics import log_loss, accuracy_score
def plot_log_loss():
plt.figure(figsize=(16, 4))
plt.subplot(1,2,1)
plt.title('Клас 1')
x = pd.np.linspace(0, 1)
loss_log = [log_loss([1], [x], labels=[0, 1]) for x in x]
plt.plot(x, loss_log); plt.grid();
plt.plot(x, 1 -x);
plt.ylim(-0.1, 8)
plt.ylabel('Натрупана грешка за 1 семпъл')
plt.xlabel('Разлика във вероятността от истинския клас')
plt.subplot(1,2,2)
plt.title('Клас 0')
x = pd.np.linspace(0, 1)
loss_log = [log_loss([0], [x], labels=[0, 1]) for x in x]
plt.ylim(-0.1, 8)
plt.plot(x, loss_log); plt.grid();
plt.plot(x, x);
plt.xlabel('Разлика във вероятността от истинския клас')
plt.legend(['LogLoss', 'Абсолютна разлика'])
plot_log_loss()
Вижда се, че тази функция наказва модел, който е много уверен в грешните неща. Оптимизирането й води до модел, е по-консервативен във вероятностите си.
Това е функцията, която се оптимизира при логистична регресия.
Най-добрия константен модел:
Настройваме вероятностите за всеки клас да са равни на съответните честоти на класа.
Dataset:
Вероятностите са [0.1, 0.9] за двата класа.
predict_proba
връща вероятности, които модела е определил. Това може да не са реалните вероятности, обаче.
Наричаме един модел "калибриран" когато двете съвпадат. Калибрирането на модели е сложна тема, която може и да разгледаме по-натам. Засега може да се пробвате със следния paper:
Recall и precision са само част от голям набор от метрики, базирани на TP, FP, TN и FN. В machine learning те се ползват най-често, но има и други. Ако ви е любопитно, wikipedia има много добра статия по въпроса:
Да се тренира и тества модел върху едни и същи данни се счита за грешка. Достатъчно сложен модел може да научи перфектно резултата за всеки вход за данните с които се тренира. Това не означава, че ще генерализира добре.
В sklearn
можем да използваме функцията train_test_split
за бързо разделяне на данните на две части:
X_train
и y_train
X_test
и y_test
train_test_split
прави и случайно разбъркване на данните.
from sklearn.datasets import load_iris
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, test_size=0.4, random_state=0)
print("Train shape:", X_train.shape, y_train.shape)
print("Test shape: ", X_test.shape, y_test.shape)
Имаме следната ситуация:
alpha
, max_depth
, n_features
, и т.н.) се появява шанс за овърфит.За да се реши този проблем се прави още едно разделяне на данните, което се нарича "Валидационнен сет".
На практика данните се разделят на 3 части:
Или на картинка:
Може да ползваме cross (кръстосана) валидация.
Или на картинка:
Може да ползваме cross_val_score
за да направи тази операция:
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
logistic = LogisticRegression()
scores = cross_val_score(logistic, iris.data, iris.target, cv=10, scoring='accuracy')
print(scores)
print("Mean: {}".format(scores.mean()))
print("Std: {}".format(scores.std()))
Обърнете внимание, че:
train_test_split
.cross_val_score
запазва пропорциите на класовете в двете множествата. Параметърът cv
може да приема следните стойности:
None
използва три-кратно крос валидиране.int
- k-кратно крос валидиране, където k е подаденото число.StratifiedKFold
.(train, validation)
множестваИма друга функция, cross_validate
която дава повече информация:
from sklearn.model_selection import cross_validate
scoring = ['accuracy', 'precision_macro', 'recall_macro', 'f1_macro']
logistic = LogisticRegression()
scores = cross_validate(logistic, iris.data, iris.target, scoring=scoring, cv=5, return_train_score=False)
pd.DataFrame(scores)
Ако търсим средни стойности, може да се пробваме с describe
:
pd.DataFrame(scores).describe()
Частния случай $k = n$ се нарича leave one out. В него правим по едно трениране за всеки елемент:
from sklearn.model_selection import LeaveOneOut
X = np.array([1, 2, 3, 4])
y = np.array([0.1, 0.2, 0.3, 0.4])
leave_one_out = LeaveOneOut()
for train_idx, test_idx in leave_one_out.split(X):
print("* train: {} {} test: {} {}".format(X[train_idx], y[train_idx], X[test_idx], y[test_idx]))
Може да ползваме LeaveOneOut
за оценка по следния начин:
logistic = LogisticRegression()
scores = cross_val_score(logistic, iris.data, iris.target, cv=LeaveOneOut())
print(scores.mean())
print(scores)
Има и cross_val_predict
, който прави предвиждания:
from sklearn.model_selection import cross_val_predict
logistic = LogisticRegression()
y_pred = cross_val_predict(logistic, iris.data, iris.target, cv=LeaveOneOut())
print(y_pred)
print(iris.target)
print((y_pred == iris.target).mean())
Да отбележим още веднъж, че прип класификация, fold-овете генерирани от крос валидация трябва да запазвата пропорциите на класовете. Важно е всеки клас да бъде представен равномерно във всеки fold:
mglearn.plots.plot_stratified_cross_validation();
cross_val_score
сам се сеща да го направи при класификация, но поняга може да се наложи да ползвате StratifiedKFold
.
KFold
и ShuffleSplit
, предполагат, че пробите са независими и идентично разпределени.TimeSeriesSplit
.from sklearn.model_selection import TimeSeriesSplit
X = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
series_split = TimeSeriesSplit(n_splits=3, max_train_size=5)
for train_idx, test_idx in series_split.split(X):
print('* train: {} test: {}'.format(X[train_idx], X[test_idx]))