import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
import mglearn
from IPython.display import display
%matplotlib inline
SVM е особен алгоритъм, който не се обяснява добре с малко математика.
Математиката е интересна, но не е тривиална за обясняване. Ако искате да прочетете повече, тук има един хубав материал от Andrew Ng:
http://cs229.stanford.edu/notes/cs229-notes3.pdf
Ще се опитаме да създадем неформална интуиция отгоре-отгоре.
Има няколко разновидности. Ще погледнем първо линейните:
Пример за линеен SVM, намиращ хипер-равнина (права в 2D):
Алгоритъма се опитва да оптимизира margin-а (разстоянието между пунктираните линии).
За класификация, може да ползвате LinearSVC
:
from sklearn.svm import LinearSVC
X, y = mglearn.datasets.make_forge()
model = LinearSVC().fit(X, y)
mglearn.plots.plot_2d_separator(model, X, fill=False, eps=0.5, alpha=.7)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y);
Има регуляризационен параметър C
, който е обратно пропорционален на колко регуляризация искаме – ниски стойности на C
предизвикват голяма резуляризация и обратното.
mglearn.plots.plot_linear_svc_regularization()
В горните диаграми се вижда, че голяма регуляризация (малко C
) поставя акцента върху клъстерите от точки, докато ниската регуляризация (голямо C
) се опитва да класифицира правилно всяка една точка.
Преди да видим нелинейни kernel-и, нека да си припомним ограниченията на линейните модели и как може да се заобиколят.
Да започнем със нелинеен dataset:
from sklearn.datasets import make_blobs
X, y = make_blobs(centers=4, random_state=8)
y = y % 2
mglearn.discrete_scatter(X[:, 0], X[:, 1], y);
Да видим какво ще направи линеен модел:
model = LinearSVC().fit(X, y)
mglearn.plots.plot_2d_separator(model, X)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y);
Как изглежда успеваемостта на train set-а?
model.score(X, y)
Слабо. Да пробваме да добавим нов feature, който е квадрата на един от съществуващите. Т.е., започваме с:
X[:5]
И добавяме $x_2^2$ като трети feature:
X_new = np.hstack([X, X[:, 1:] ** 2])
X_new[:5]
Нека да начертаем новите данни:
from mpl_toolkits.mplot3d import Axes3D, axes3d
figure = plt.figure()
ax = Axes3D(figure, elev=-152, azim=-26)
mask = (y == 0)
ax.scatter(X_new[mask, 0], X_new[mask, 1], X_new[mask, 2], c='b', cmap=mglearn.cm2, s=60)
ax.scatter(X_new[~mask, 0], X_new[~mask, 1], X_new[~mask, 2], c='r', marker='^', cmap=mglearn.cm2, s=60);
Това изглежда като разделимо с равнина. Да пробваме да я натренираме LinearSVM
:
model = LinearSVC().fit(X_new, y)
model.score(X_new, y)
Явно наистина може да разделим тези точки с равнина!
Ако избухнем с малко код, може и дори да я начертаем:
coef, intercept = model.coef_.ravel(), model.intercept_
figure = plt.figure()
ax = Axes3D(figure, elev=-152, azim=-26)
xx = np.linspace(X_new[:, 0].min() - 2, X_new[:, 0].max() + 2, 50)
yy = np.linspace(X_new[:, 1].min() - 2, X_new[:, 1].max() + 2, 50)
XX, YY = np.meshgrid(xx, yy)
ZZ = (coef[0] * XX + coef[1] * YY + intercept) / -coef[2]
ax.plot_surface(XX, YY, ZZ, rstride=8, cstride=8, alpha=0.3)
ax.scatter(X_new[mask, 0], X_new[mask, 1], X_new[mask, 2], c='b', cmap=mglearn.cm2, s=60)
ax.scatter(X_new[~mask, 0], X_new[~mask, 1], X_new[~mask, 2], c='r', marker='^', cmap=mglearn.cm2, s=60);
Може и да погледнем това "отгоре":
ZZ = YY ** 2
dec = model.decision_function(np.c_[XX.ravel(), YY.ravel(), ZZ.ravel()])
plt.contourf(XX, YY, dec.reshape(XX.shape), levels=[dec.min(), 0, dec.max()], cmap=mglearn.cm2, alpha=0.5)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y);
Накратко и доста нетчно, ако ползвате gaussian kernel, алгоритъма сам може да намери подходящи полиномни feature-и. И да класифицира правилно.
За целта ползвате SVC
със kernel='rbf'
, което е и стойността по подразбиране. RBF идва от Radial Basis Function.
Зад това има доста математика, в която дори няма да си помисляме да навлизаме.
from sklearn.svm import SVC
model = SVC().fit(X, y)
model.score(X, y)
Резултата е същия (намери се пълно разделение), без да се налага да познаваме какви полиномни feature-и ни трябват.
Има два интересни параметъра – gamma
и C
.
За да ги илюстрираме, ще разгледаме един друг синтетичен dataset.
X, y = mglearn.tools.make_handcrafted_dataset()
svm = SVC(kernel='rbf', C=10, gamma=0.1).fit(X, y)
mglearn.plots.plot_2d_separator(svm, X, eps=.5)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y);
fig, axes = plt.subplots(3, 3, figsize=(15, 10))
for ax, C in zip(axes, [-1, 0, 3]):
for a, gamma in zip(ax, range(-1, 2)):
mglearn.plots.plot_svm(log_C=C, log_gamma=gamma, ax=a)
Без математиката е трудно за обяснение. Най-близкото до което може да стигнем е:
C
е стандартния регуляризационен параметърgamma
е обратно пропорционална на широчината на gaussian kernel-а (хъхъ). Висока gamma ще създава по-комплексни decision boundary-ита.SVM са много чувствителни на мащаба на данните. Преди да го илсютрираме, нека първо разгледаме инструментите за мащабиране.
Този клас позволява да промените мащаба на дадени данни. Например:
from sklearn.preprocessing import MinMaxScaler
data = [[100], [120], [170], [250], [300]]
scaler = MinMaxScaler(feature_range=(0, 1))
scaler.fit(data)
scaler.transform(data)
Може да видите параметрите, с които той оперира:
print("scale: {}".format(scaler.scale_))
print("min: {}".format(scaler.min_))
print("data min: {}".format(scaler.data_min_))
print("data max: {}".format(scaler.data_max_))
Веднъж като сте fit
-нали на едни данни, може да ползвате transform
за следващите.
scaler.transform([[150], [200], [250]])
Обърнете внимание, че правим един fit
и много transform
-и.
MinMaxScaler
ще скалира и данни извън първоначалния range:
scaler.transform([[0], [400]])
Има и още един метод, fit_transform
, който прави fit
+ transform
:
data = [[100, -10], [120, 5], [170, 2]]
MinMaxScaler().fit_transform(data)
MinMaxScaler
работи върху всички колони. Ако искате да обработите само някои, има и функция:
from sklearn.preprocessing import minmax_scale
minmax_scale([[1], [2], [4]])
Може директно да я ползвате и за DataFrame
:
frame = pd.DataFrame({
'name': ['Edward', 'Bella', 'Jacob'],
'age': [17, 18, 16]
}, columns=['name', 'age'])
frame
frame[['age']] = minmax_scale(frame[['age']])
frame
Подобен механизъм, който скалира до средна стойност (mean) 0 и дисперсия (variance) 1.
from sklearn.preprocessing import StandardScaler
data = np.array([0.0, 1.0, 3.0, 7.0, 42.0, 100.0, 2.0]).reshape(-1, 1)
StandardScaler().fit_transform(data)
Разбира се, има и функция:
from sklearn.preprocessing import scale
scale([0.0, 7.0, 12.0, 80.0])
Всичко до тук е част от по-голяма абстракция – трансформери (трансформатори?).
В scikit-learn има класове, чиято цел е да трансформират данните. Те се характеризират със fit
и transform
методи:
fit
се вика веднъж за да "настрои" трансформацията.transform
може да се вика много пъти след като вече има трансформация.fit_transform
прави и двете.В някои специални случаи, клас ще има само fit_transform
.
Може да си имплементираме собствени трансформатори:
from sklearn.base import TransformerMixin
class Logarithmizer(TransformerMixin):
def transform(self, X, *_):
return np.log(X)
def fit(self, *_):
return self
data = [0.5, 1, 2.71828182, 7.38905609, 10]
Logarithmizer().fit_transform(data)
Imputer
е трасформер, който попълва липсващите данни. Има три стратегии, контролирани с параметър strategy
:
mean
– средно аритметичноmedian
– медианаmost_frequent
– модаfrom sklearn.preprocessing import Imputer
values = np.array([1.0, 5.0, np.nan, 1.0, 2.0, np.nan]).reshape(-1, 1)
Imputer(strategy='most_frequent').fit_transform(values)
OneHotEncoder
е трансформерLabelEncoder
същоLabelBinarizer
, който е комбинация от горните двеТрансформер, който дава полиномни фийчъри.
from sklearn.preprocessing import PolynomialFeatures
data = [[1, 2],
[3, 4],
[6, 2]]
PolynomialFeatures().fit_transform(data)
degrees=2
– максимална степен на полиномитеinclude_bias=True
– константен featureinteraction_only=False
– не включва членове, където има степен, по-голяма от първа (без $x^2$, $y^3$ или $x^2y$)Support Vector Machine-ите са чувствителни на мащабиране на feature-ите. Например:
from sklearn.svm import SVC
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
scaler = StandardScaler().fit(X_train)
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
unscaled_score = SVC().fit(X_train, y_train).score(X_test, y_test)
scaled_score = SVC().fit(X_train_scaled, y_train).score(X_test_scaled, y_test)
print("Unscaled score: {:.2f}".format(unscaled_score))
print("Scaled score: {:.2f}".format(scaled_score))
Това важи и за LinearSVC
, Лъчо:
from sklearn.svm import LinearSVC
unscaled_score = LinearSVC(random_state=42).fit(X_train, y_train).score(X_test, y_test)
scaled_score = LinearSVC(random_state=42).fit(X_train_scaled, y_train).score(X_test_scaled, y_test)
print("Unscaled score: {:.2f}".format(unscaled_score))
print("Scaled score: {:.2f}".format(scaled_score))
Pipeline
-а позволява да се наредят няколко стъпки една след друга.
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
('scale', StandardScaler()),
('svm', LinearSVC()),
])
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
pipeline.fit(X_train, y_train)
pipeline.score(X_test, y_test)
Всяка стъпка преди последната трябва да е трансформер (да има fit
и transform
). Последната може да е и класификатор (fit
, predict
и score
).
Имената в аргументите на pipeline са полезни понякога заради други операции, които може да прилагаме. Ще разгледаме това малко по-късно.
В по-лесния случай може да ползваме make_pipeline
.
from sklearn.pipeline import make_pipeline
pipeline = make_pipeline(StandardScaler(), LinearSVC())
pipeline.fit(X_train, y_train)
pipeline.score(X_test, y_test)
Може да видим, че make_pipeline
прави същото, но използва имената на класовете с малки букви:
pipeline
Вече сме видяли няколко класа, които ни помагат с търсенето:
GridSearchCV
– пробва всички комбинации от параметриRandomizedSearchCV
– пробва няколко sample-а от всички комбинации с параметриТе работят добре и с pipelines. Нека видим пример:
from sklearn.model_selection import GridSearchCV
from sklearn.datasets import load_boston
from sklearn.linear_model import Ridge
boston = load_boston()
X, y = boston.data, boston.target
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
pipeline = Pipeline([
('scaler', StandardScaler()),
('polynomials', PolynomialFeatures()),
('ridge', Ridge())
])
grid = {
'polynomials__degree': [1, 2, 3],
'ridge__alpha': [0.001, 0.01, 1, 10, 100],
}
search = GridSearchCV(pipeline, param_grid=grid, cv=5)
Обърнете внимане подаваме параметрите при pipeline – имаме името на стъпката (напр. polynomials
), последвано от две подчертавки (__
), след което имаме името на параметъра (degrees
).
search.fit(X_train, y_train);
search.best_params_
Може да видим всички резултати:
pd.DataFrame(search.cv_results_)
Вероятно ще е по-добре да си ги начертаем графично:
plt.matshow(search.cv_results_['mean_test_score'].reshape(3, -1), vmin=0, cmap="viridis")
plt.xlabel("ridge__alpha")
plt.ylabel("polynomials__degree")
plt.xticks(range(len(grid['ridge__alpha'])), grid['ridge__alpha'])
plt.yticks(range(len(grid['polynomials__degree'])), grid['polynomials__degree'])
plt.colorbar();
От графикта се вижда, че сме подбрали обхвата на параметрите добре. Ако максимума бе в ръб или ъгъл, щеше да има смисъл да погледнем други параметри.
Може да търсим и различни комбинации за даден модел – например да ползваме едни параметри при kernel=rbf
и други при kernel=linear
.
from sklearn.datasets import load_iris
iris = load_iris()
grid = [
{'kernel': ['rbf'],
'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000],
'gamma': [0.001, 0.01, 0.1, 1, 10, 100]},
{'kernel': ['linear'],
'C': [0.001, 0.01, 0.1, 1, 10, 100]}
]
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=0)
search = GridSearchCV(SVC(), grid, cv=5)
search.fit(X_train, y_train)
print(search.best_params_)
print(search.best_score_)
Може дори да направим GridSearch
в който заменяме стъпките от pipeline-а – например, може да търсим няколко комбинации от класификатори и scaler-и.
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import RobustScaler
pipe = Pipeline([('preprocessing', StandardScaler()), ('classifier', SVC())])
grid = [
{
'classifier': [SVC()],
'preprocessing': [RobustScaler(), StandardScaler(), MinMaxScaler()],
'classifier__gamma': [0.001, 0.01, 0.1, 1, 10, 100],
'classifier__C': [0.001, 0.01, 0.1, 1, 10, 100]
},
{
'classifier': [RandomForestClassifier(n_estimators=100)],
'preprocessing': [None],
'classifier__max_features': [1, 2, 3]
}
]
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
search = GridSearchCV(pipe, grid, cv=5)
search.fit(X_train, y_train)
print("Best params:\n{}\n".format(search.best_params_))
print("Best cross-validation score: {:.2f}".format(search.best_score_))
Нека видим още един интересен проблем. Ще се пробваме със същия dataset, но този път с cross validation.
from sklearn.model_selection import cross_val_score
cancer = load_breast_cancer()
X, y = scale(cancer.data), cancer.target
score = cross_val_score(LinearSVC(), X, y, cv=3)
print(score)
print(score.mean())
Какъв е проблема в горния код?
Данните се мащабират преди train-test split-а, направен от крос валидацията. По този начин изтича информация от тестовия сет към тренирането. Това няма да се случи с реални данни, обаче – те ще идват с мащаб, който алгоритъма няма да знае. По-реалистично е да мащабираме само тестовите данни и да оставим възможността тестовия сет да излиза настрани.
Това може да се случи като дадем pipeline
-а на cross_val_score
– така той ще прави отделно мащабиране за всяко разделение, което ще е по-реален индикатор на точността.
X, y = cancer.data, cancer.target
pipeline = make_pipeline(StandardScaler(), LinearSVC())
score = cross_val_score(pipeline, X, y, cv=3)
print(score)
print(score.mean())
Може да видим, че с първия fold имаме по-лош (и по-реалистичен) резултат.
Ето визуализация на грешния подход:
mglearn.plots.plot_improper_processing()
Ето как нещата трябва да изглеждат наистина:
mglearn.plots.plot_proper_processing()
Това е интересен клас, който помага за preprocessing-а на данни. Нека видим банален пример – искаме да извлечем едновременно полиномни feature-и и скалиран feature.
data = np.array([1.0, 5.0, 2.0, 7.0]).reshape(-1, 1)
PolynomialFeatures().fit_transform(data)
StandardScaler().fit_transform(data)
Бихме могли просто да конкатенираме двете матрици:
np.concatenate([
PolynomialFeatures().fit_transform(data),
StandardScaler().fit_transform(data)
], axis=1)
Има по-добър начин:
from sklearn.pipeline import FeatureUnion
union = FeatureUnion([
('polynomials', PolynomialFeatures()),
('scaled', StandardScaler()),
])
union.fit_transform(data)
Разбира се, аргументите на FeatureUnion
могат да бъдат и Pipeline
обекти. Например, бихме могли да прекараме стойностите MinMaxScaler
преди да ги дадем на PolynomialFeatures
:
union = FeatureUnion([
('polynomials', Pipeline([
('minmax', MinMaxScaler()),
('polynomials', PolynomialFeatures())
])),
('scaled', StandardScaler()),
])
union.fit_transform(data)
С тези механизми може да си дефинираме нагледно preprocessing-а на данни.
Нека започнем с един познат dataset:
titanic = pd.read_csv('data/titanic/train.csv')
titanic.head(5)
Ще си дефинираме трансформер, който взема само една колона:
from sklearn.base import BaseEstimator, TransformerMixin
class ItemSelector(BaseEstimator, TransformerMixin):
def __init__(self, key):
self.key = key
def fit(self, x, y=None):
return self
def transform(self, data_dict):
return data_dict[[self.key]]
Да видим дали работи:
ItemSelector('Age').fit_transform(titanic).head(10)
Пушка!
Сега може да построим нещо с FeatureUnion
:
union = FeatureUnion([
('age', Pipeline([
('select', ItemSelector('Age')),
('imputer', Imputer(strategy='mean')),
('scaler', StandardScaler()),
]))
])
union.fit_transform(titanic)[:10]
Чудесно!
Нека сега пробваме друго:
from sklearn.preprocessing import LabelBinarizer
union = FeatureUnion([
('gender', Pipeline([
('select', ItemSelector('Sex')),
('imputer', Imputer(strategy='most_frequent')),
('encoder', LabelBinarizer()),
]))
])
try:
union.fit_transform(titanic)
except Exception as e:
print(e)
Не стана.
И Imputer
и LabelBinarizer
не работят добре в Pipeline. Нека си направим собствени.
class LabelBinarizerPipelineFriendly(LabelBinarizer):
def fit(self, X, y=None):
super().fit(X)
def transform(self, X, y=None):
return super().transform(X)
def fit_transform(self, X, y=None):
return super().fit(X).transform(X)
class StringImputer(TransformerMixin):
def fit(self, X, *_):
self.modes = X.mode().iloc[0]
return self
def transform(self, X, y=None):
return X.fillna(self.modes)
union = FeatureUnion([
('gender', Pipeline([
('select', ItemSelector('Sex')),
('imputer', StringImputer()),
('encoder', LabelBinarizerPipelineFriendly()),
]))
])
union.fit_transform(titanic)[:5]
И сега всичко наведнъж:
model = Pipeline([
('union', FeatureUnion([
('age', Pipeline([
('select', ItemSelector('Age')),
('imputer', Imputer(strategy='mean')),
('scaler', StandardScaler()),
])),
('gender', Pipeline([
('select', ItemSelector('Sex')),
('imputer', StringImputer()),
('encoder', LabelBinarizerPipelineFriendly()),
])),
('embarked', Pipeline([
('select', ItemSelector('Embarked')),
('imputer', StringImputer()),
('encoder', LabelBinarizerPipelineFriendly()),
])),
('sibsp', Pipeline([
('select', ItemSelector('SibSp')),
('scaler', StandardScaler()),
])),
('parch', Pipeline([
('select', ItemSelector('Parch')),
('scaler', StandardScaler()),
])),
])),
('svc', SVC())
])
scores = cross_val_score(model, titanic, titanic['Survived'])
print(scores)
print(scores.mean())