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())