Целите днес:
import numpy as np
import mglearn
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import eli5.sklearn
import warnings
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import make_moons, load_boston, load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, PolynomialFeatures, MinMaxScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso, LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.neural_network import MLPClassifier
warnings.filterwarnings(action='ignore', module='scipy', message='^internal gelsd')
%matplotlib inline
Обикновено ни интересува колко добре се справя един модел с нови данни, които не е виждал. Тъй като нямаме добър начин да измерим това (освен да го пуснем в продукция за няколко месаца и да видим), използваме друг подход – разделяме данните на тренировъчни и тестови, тренираме модела с тренировъчните и проверяваме колко добре се справя върху тестовите. Ползваме тестовите данни като proxy метрика за генерализацията.
Ако модела не се справя оптимално, обикновено говорим за две неща:
Често си ще говорим са "сложност" на моделите:
Много разбираем класификатор:
plt.figure(figsize=(10, 6))
mglearn.plot_knn_classification.plot_knn_classification(n_neighbors=3)
Параметърът $k$ се явява форма на регуляризация. Колкото по-висок е, толкова "по-прост" ще бъде модела (ще открие по-проста зависимост между данните).
def plot_knn_parameter():
X, y = mglearn.datasets.make_forge()
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
for n_neighbors, ax in zip([1, 3, 9], axes):
clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X, y)
mglearn.plots.plot_2d_separator(clf, X, fill=True, eps=0.5, ax=ax, alpha=.4)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y, ax=ax)
ax.set_title("{} neighbour(s)".format(n_neighbors))
plot_knn_parameter()
make_moons
който да моделира този казус.make_moons
създава два полумесеца, вложени един в друг.
X, y = make_moons(1000, noise=0, random_state=0)
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=1, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=1, s=60);
Може да си представите как реката минава между тях.
make_moons
има параметър noise
който въвежда шум. Например:
X, y = make_moons(1000, noise=0.1, random_state=0)
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=1, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=1, s=60);
X, y = make_moons(1000, noise=0.2, random_state=0)
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=1, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=1, s=60);
X, y = make_moons(1000, noise=0.3, random_state=0)
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=1, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=1, s=60);
X, y = make_moons(1000, noise=0.4, random_state=0)
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=1, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=1, s=60);
Ще ползваме такъв шум 0.4 за нашия пример.
Нека си създадем dataset начисто и да го разделим на тренировъчен и тестов.
X, y = make_moons(1500, noise=0.4, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
Да видим как изглежда отново:
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=0.4, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=0.4, s=60);
Нека да видим как се държи kNN с няколко различни стойности за k:
for k in [1, 7, 21, 501]:
clf = KNeighborsClassifier(n_neighbors=k)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
print("k = {:3}, train score = {:1.5f}, test score = {:1.5f}".format(k, train_score, test_score))
Нека всъщност начертаем графика. Ще ползваме следната функция:
def plot_knn_scores(ks):
train_scores = []
test_scores = []
for k in ks:
clf = KNeighborsClassifier(n_neighbors=k).fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
train_scores.append(train_score)
test_scores.append(test_score)
plt.figure(figsize=(10, 6))
plt.plot(ks, train_scores, color='blue', label='train score')
plt.plot(ks, test_scores, color='green', label='test score')
plt.legend()
Така изглеждат нещата за $k \in [1, 10)$:
plot_knn_scores(range(1, 10))
Нека да видим $k \in [1, 100)$:
plot_knn_scores(range(1, 100, 2))
И финално, $k \in [1, 1130)$:
plot_knn_scores(range(1, 1130, 10))
Тук виждаме следните неща:
От графиката може да видим, че при $k = 1$ има overfitting, докато при по-големи $k$ ($k > 400$) започва да има underfitting.
Търсим някакъв $k$, който да има оптимален train score (например $k = 351$ се приближава).
Друг начин да осмислим резултата е като начертаем decision boundary. За да стане това, просто оцветяваме всяка точка от картата с цвета, който класификатора ще предвиди.
За целта ползваме следните функции:
def plot_decision_boundary(classifier, alpha=1):
plt.figure(figsize=(10, 6))
eps = X.std() / 2.
ax = plt.gca()
x_min, x_max = X[:, 0].min() - eps, X[:, 0].max() + eps
y_min, y_max = X[:, 1].min() - eps, X[:, 1].max() + eps
xx = np.linspace(x_min, x_max, 700)
yy = np.linspace(y_min, y_max, 700)
X1, X2 = np.meshgrid(xx, yy)
X_grid = np.c_[X1.ravel(), X2.ravel()]
cmap = matplotlib.colors.ListedColormap(['red', 'blue'])
decision_values = classifier.predict(X_grid)
ax.imshow(decision_values.reshape(X1.shape), extent=(x_min, x_max, y_min, y_max),
aspect='auto', origin='lower', alpha=alpha, cmap=cmap)
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
ax.set_xticks(())
ax.set_yticks(())
def summarize_knn(k):
classifier = KNeighborsClassifier(n_neighbors=k)
classifier.fit(X_train, y_train)
print("K = {}".format(k))
print("train score: {}".format(classifier.score(X_train, y_train)))
print("test score: {}".format(classifier.score(X_test, y_test)))
plot_decision_boundary(classifier)
Ето как изглеждат нещата при $k = 1$.
summarize_knn(1)
Тук виждаме доста научен шум и малко острови.
Увеличавайки $k$, научения шум намалява и островите стават по-малки:
summarize_knn(3)
summarize_knn(9)
summarize_knn(57)
При $k = 151$ започваме да виждаме очакваните резултати:
summarize_knn(151)
Ако прекалим обаче, модела не научава добре границата:
summarize_knn(501)
Може и да начертаем как се променят train и test scores при размера на входните данни ($n$).
def plot_knn_with_different_datasets():
percentages = range(10, 100, 1)
test_scores = []
train_scores = []
for percent in percentages:
size = int(len(X_train) * ((percent + 1) / 100.0))
X_subset = X_train[:size, :]
y_subset = y_train[:size]
clf = KNeighborsClassifier(n_neighbors=123)
clf.fit(X_subset, y_subset)
train_score = clf.score(X_subset, y_subset)
test_score = clf.score(X_test, y_test)
train_scores.append(train_score)
test_scores.append(test_score)
plt.figure(figsize=(10, 6))
plt.plot(percentages, train_scores, color='blue', label='train score')
plt.plot(percentages, test_scores, color='green', label='test score')
plt.legend()
plot_knn_with_different_datasets()
Виждаме, че колкото повече данни имаме, толкова по-добре се справят train и test score.
Един извод, който може да си извадите, е че колкото повече данни имаме, толкова по-малко склонен е алгоритъма към overfitting.
Нека да разгледаме boston dataset-а и да се опитаме да приложим линейна регресия с различни регуларизации.
Първо, нека да погледнем данните:
boston = load_boston()
Тук подмолно въвеждаме една библиотека (pandas
) която прави работата с таблици по-лесна:
frame = pd.DataFrame(boston.data, columns=boston.feature_names)
frame.head()
Да видим какво бяха отделните колони:
print(boston.DESCR[253:1222])
Нека пробваме да натренираме проста линейна регресия:
X, y = boston.data, boston.target
X_train, X_test, y_train, y_test = train_test_split(X, y)
model = LinearRegression()
model.fit(X_train, y_train)
print("train score:", model.score(X_train, y_train))
print("test score: ", model.score(X_test, y_test))
Тук всякаш нямаме overfitting. Но пък може би ще успеем да постигнем резултат ако помасажираме входните данни.
Един подход, който често дава добри резултати е да добавим степенни на feature-ите. Например, ако имаме два feature – $x_1$ и $x_2$, да добавим и $x_1^2$, $x_1 x_2$ и $x_2^2$ като нови feature-и.
В scikit learn има клас, който може да направи това:
polynomial = PolynomialFeatures(degree=2, include_bias=True, interaction_only=False)
data = np.array([[2.0, 5.0],
[3.0, 4.0]])
polynomial.fit_transform(data)
Друга обработка на данните, която ще направим, е скалиране. За целта имаме StandardScaler()
, който изважда средно аритметичното и дели на стандартното отклонение. Това (условно) разпределя всички данни в интервала $(-4, -4)$.
frame[['TAX']].describe()
transformed = StandardScaler().fit_transform(frame[['TAX']])
pd.DataFrame(transformed).describe()
Ще прекараме оригиналния dataset през тези трансформации:
data = boston.data
data = StandardScaler().fit_transform(data)
data = PolynomialFeatures(degree=2, include_bias=False).fit_transform(data)
Да видим формата на оригиналните данни и на трансформираните:
print("original data shape: ", boston.data.shape)
print("transformed data shape:", data.shape)
Нека си разделим данните на тренировъчни и тестови:
X, y = data, boston.target
X_train, X_test, y_train, y_test = train_test_split(data, boston.target, random_state=0)
Преди да тренираме модел, нека си припомним трите вида линейна регресия:
LinearRegression
– най-простата възможна, без регуляризация.Ridge
– вариант с L2 регуляризация, която кара всички коефициенти да се опитват да бъдат малкиLasso
– вариант с L1 регуларизация, която кара някои коефициенти да бъдат точно 0Нека първо да пробваме с Ridge
:
model = Ridge().fit(X_train, y_train)
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
print("train:", train_score)
print("test: ", test_score)
Тук има стабилен overfit.
Регуляризацията се контролира с един параметър alpha
. Колкото е по-голям, толкова по-силна е регуляризацията (т.е. модела се опитва да е по-прост). Нека да начертаем train и test score при различни стойности на alpha
.
def plot_ridge_learning_rate():
alphas = np.linspace(0.00001, 200, 5000)
train_scores = []
test_scores = []
for alpha in alphas:
model = Ridge(alpha=alpha).fit(X_train, y_train)
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
train_scores.append(train_score)
test_scores.append(test_score)
plt.figure(figsize=(10, 6))
plt.plot(alphas, train_scores, color='blue', label='train score')
plt.plot(alphas, test_scores, color='green', label='test score')
plt.legend()
plot_ridge_learning_rate()
Отново виждаме тендецния train score да намалява монотонно, докато test score първо да расте и после да намалява. Тук оптимален резултат се вижда някъде около $\alpha = 25$.
Нека сега пробваме с Lasso
:
model = Lasso().fit(X_train, y_train)
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
features_used = np.sum(model.coef_ != 0)
print("train: ", train_score)
print("test: ", test_score)
print("features used: {}/{}".format(features_used, len(model.coef_)))
Тук отново изглежда, че имаме overfit. Също, забележете, че модела ползва само 11 от всичките 104 feature-а.
Пак ще начертаем графика:
def plot_lasso_learning_rate():
alphas = np.linspace(0.0001, 0.5, 1000)
train_scores = []
test_scores = []
features_used = []
for alpha in alphas:
model = Lasso(alpha=alpha, random_state=0, max_iter=1_000_000).fit(X_train, y_train)
train_score = model.score(X_train, y_train)
test_score = model.score(X_test, y_test)
features = np.sum(model.coef_ != 0)
train_scores.append(train_score)
test_scores.append(test_score)
features_used.append(features)
fig, axes = plt.subplots(2, 1, figsize=(12, 10))
axes = axes.ravel()
axes[0].plot(alphas, train_scores, color='blue', label='train score')
axes[0].plot(alphas, test_scores, color='green', label='test score')
axes[0].legend()
axes[1].plot(alphas, features_used, label='features used')
axes[1].legend();
plot_lasso_learning_rate()
Отново се наблюдава същата тенденция. Има стойности на alpha
, за които резултата е оптимален (около $\alpha=0.4$).
Въпреки това, има голяма разлика между train score и test score. Това може да се дължи на няколко неща:
Регуляризацията може да намали overfit-а при малко данни. Ето пример:
plt.figure(figsize=(10, 6))
mglearn.plots.plot_ridge_n_samples()
Тук се вижда, че Ridge
се справя по-добре при по-малък training set. Линейната регресия без регуляризация има нужда от почти 400 точки за да научи нещо, докато Ridge
се оправя (някак) и с по-малко.
Като забавен aside – може да ползваме линейна регресия да по какъв начин различните feature допринасят на крайния резултат. Например, може да натренираме линейна регресия за апартаментите в Бостън и да видим всеки feature как допринася на цената.
Първо ще настренираме линейна регресия:
data = StandardScaler().fit_transform(boston.data)
X_train, X_test, y_train, y_test = train_test_split(data, boston.target)
linear = LinearRegression()
linear.fit(X_train, y_train)
print("train:", linear.score(X_train, y_train))
print("test: ", linear.score(X_test, y_test))
След това може да ползваме една библиотека, eli5
(от Explain it Like I'm 5) за да видим стойностите на коефициентите:
eli5.sklearn.explain_weights.explain_linear_regressor_weights(linear, feature_names=boston.feature_names)
Може да видим че RAD (достъпа до магистрали) вдига цената на апартамента, докато DIS (дистанцията от employment център) я сваля.
print(boston.DESCR[253:1222])
Нека just for fun да се върнем към първоначалния dataset и да пробваме с логистична регресия и полиномни feature-и.
X, y = make_moons(1000, noise=0.5, random_state=0)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
red = X[y == 0]
blue = X[y == 1]
plt.figure(figsize=(10, 6))
plt.scatter(red[:, 0], red[:, 1], color='red', alpha=0.4, s=60)
plt.scatter(blue[:, 0], blue[:, 1], color='blue', alpha=0.4, s=60);
Нека натренираме логистична регресия и да покажем decision boundary-то:
reg = LogisticRegression().fit(X_train, y_train)
print("train:", reg.score(X_train, y_train))
print("test: ", reg.score(X_test, y_test))
plot_decision_boundary(reg)
Неизненадващо, логистичната регресия търси права, която да разделя нашия град на две и това не дава добър резултат.
Нека да пробваме да видим какво става с полиноми от по-висока степен и различна регуляризация:
def plot_logistic_moons(X, y, degree=1, C=1):
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
pipeline = make_pipeline(
PolynomialFeatures(degree=degree, interaction_only=False, include_bias=False),
LogisticRegression(C=C)
)
classifier = pipeline.fit(X_train, y_train)
print("train:", classifier.score(X_train, y_train))
print("test: ", classifier.score(X_test, y_test))
plt.figure(figsize=(10, 6))
eps = X.std() / 2.
ax = plt.gca()
x_min, x_max = X[:, 0].min() - eps, X[:, 0].max() + eps
y_min, y_max = X[:, 1].min() - eps, X[:, 1].max() + eps
xx = np.linspace(x_min, x_max, 700)
yy = np.linspace(y_min, y_max, 700)
X1, X2 = np.meshgrid(xx, yy)
X_grid = np.c_[X1.ravel(), X2.ravel()]
cmap = matplotlib.colors.ListedColormap(['red', 'blue'])
decision_values = classifier.predict(X_grid)
ax.imshow(decision_values.reshape(X1.shape), extent=(x_min, x_max, y_min, y_max),
aspect='auto', origin='lower', alpha=1, cmap=cmap)
ax.set_xlim(x_min, x_max)
ax.set_ylim(y_min, y_max)
ax.set_xticks(())
ax.set_yticks(())
При втора степен не получаваме добри резултати.
plot_logistic_moons(X, y, degree=2, C=1)
При трета степен почти получаваме нещо разумно:
plot_logistic_moons(X, y, degree=3, C=10)
Ако продължим нагоре, нещата стават все по-шантави:
plot_logistic_moons(X, y, degree=4, C=1)
plot_logistic_moons(X, y, degree=6, C=0.1)
plot_logistic_moons(X, y, degree=7, C=100)
Колкото и да се мъчим по този начин, едва ли ще получим добър модел с логистичната регресия.
Извод: не всеки модел е подходящ за всеки dataset.
И ей така за идеята, нека натренираме и една невронна мрежа:
mlp = MLPClassifier(solver='lbfgs', random_state=0, alpha=0.3, max_iter=1000)
mlp.fit(X_train, y_train)
print("train:", mlp.score(X_train, y_train))
print("test: ", mlp.score(X_test, y_test))
plot_decision_boundary(mlp)
Накратко между overfit/underfit и данни, сложност и feature може да се обобщи така:
по-малко | повече | |
---|---|---|
сложност на модела | underfit | overfit |
данни | overfit | underfit? |
feature-и | underfit | overfit |
Тази таблица добре обобщава интуицията.
Струва си да се отбележи, че повече данни обикновено винаги е по-добре и не непременно ще доведе до underfit – обикновено води просто до по-добри резултати