基于Scikit-Learn、Keras和TensorFlow2支持向量机(Support Vector Machine)
支持向量机(SVM)是非常强大的机器学习算法,支持线性和非线性的分类任务、回归任务,甚至可以完成离群值检测任务。SVM是机器学习最受欢迎的算法之一,特别适合于比较复杂的中小型数据集上进行建模,如果数据集比较大SVM就显得比较吃力。
SVM的核心思想是:生成一条决策边界,使得决策边界离最近点的距离越远越好。
0. 导入所需的库
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
%matplotlib inline
import sklearn
for i in (np, mpl, sklearn):
print(i.__name__,": ",i.__version__,sep="")
输出:
numpy: 1.17.4
matplotlib: 3.1.2
sklearn: 0.21.3
1. SVM线性分类任务
from sklearn.svm import SVC
from sklearn import datasets
iris = datasets.load_iris()
X = iris["data"][:, (2, 3)] # 花瓣长度和宽度
y = iris["target"]
setosa_or_versicolor = (y == 0) | (y == 1)
X = X[setosa_or_versicolor]
y = y[setosa_or_versicolor]
# SVM Classifier model
svm_clf = SVC(kernel="linear", C=float("inf"))
svm_clf.fit(X, y)
输出:
SVC(C=inf, cache_size=200, class_weight=None, coef0=0.0,
decision_function_shape='ovr', degree=3, gamma='auto_deprecated',
kernel='linear', max_iter=-1, probability=False, random_state=None,
shrinking=True, tol=0.001, verbose=False)
x0 = np.linspace(0, 5.5, 200)
pred_1 = 5*x0 - 20
pred_2 = x0 - 1.8
pred_3 = 0.1 * x0 + 0.5
def plot_svc_decision_boundary(svm_clf, xmin, xmax):
w = svm_clf.coef_[0]
b = svm_clf.intercept_[0]
# At the decision boundary, w0*x0 + w1*x1 + b = 0
# => x1 = -w0/w1 * x0 - b/w1
x0 = np.linspace(xmin, xmax, 200)
decision_boundary = -w[0]/w[1] * x0 - b/w[1]
margin = 1/w[1]
gutter_up = decision_boundary + margin
gutter_down = decision_boundary - margin
svs = svm_clf.support_vectors_
plt.scatter(svs[:, 0], svs[:, 1], s=180, facecolors='#FFAAAA')
plt.plot(x0, decision_boundary, "k-", linewidth=2)
plt.plot(x0, gutter_up, "k--", linewidth=2)
plt.plot(x0, gutter_down, "k--", linewidth=2)
fig, axes = plt.subplots(ncols=2, figsize=(12,5), sharey=True)
plt.sca(axes[0])
plt.plot(x0, pred_1, "r--", linewidth=2)
plt.plot(x0, pred_2, "g-", linewidth=2)
plt.plot(x0, pred_3, "g-", linewidth=2)
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs", label="Iris versicolor")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo", label="Iris setosa")
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.legend(loc="upper left", fontsize=14)
plt.axis([0, 5.5, 0, 2])
plt.sca(axes[1])
plot_svc_decision_boundary(svm_clf, 0, 5.5)
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo")
plt.xlabel("Petal length", fontsize=14)
plt.axis([0, 5.5, 0, 2])
plt.tight_layout()
plt.show()
输出:
从上图可以看出,两个类别的鸢尾花是线性可分的,即通过一条直线就可以将两种鸢尾花分开。
左图:两条绿色实线能将两个类别正确地分开,但是红色虚线分类是错误的,没有很好地把两类分开。
右图:黑色实线将两类很好地分开,这正是SVM模型所产生的决策边界。这条实现不仅将两个类别正确地分开了,而且这条实线尽可能离最近的点距离最远。
SVM模型中决策边界由间隔边界上的点决定,加入非间隔边界上的数样本不会对决策边界产生影响,而这些间隔边界上的点(样本)组成的向量叫支持向量。
注意:SVM对特征归一化特别敏感,因此使用SVM算法训练模型前一定要对数据进行归一化预处理。未进行归一化和归一化后SVM模型的差别如下图所示:
Xs = np.array([[1, 50], [5, 20], [3, 80], [5, 60]]).astype(np.float64)
ys = np.array([0, 0, 1, 1])
svm_clf = SVC(kernel="linear", C=100) # 参数C:正则化系数
svm_clf.fit(Xs, ys)
plt.figure(figsize=(12,5))
plt.subplot(121)
plt.plot(Xs[:, 0][ys==1], Xs[:, 1][ys==1], "bo")
plt.plot(Xs[:, 0][ys==0], Xs[:, 1][ys==0], "ms")
plot_svc_decision_boundary(svm_clf, 0, 6)
plt.xlabel("$x_0$", fontsize=20)
plt.ylabel("$x_1$ ", fontsize=20, rotation=0)
plt.title("Unscaled", fontsize=16)
plt.axis([0, 6, 0, 90])
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(Xs)
svm_clf.fit(X_scaled, ys)
plt.subplot(122)
plt.plot(X_scaled[:, 0][ys==1], X_scaled[:, 1][ys==1], "bo")
plt.plot(X_scaled[:, 0][ys==0], X_scaled[:, 1][ys==0], "ms")
plot_svc_decision_boundary(svm_clf, -2, 2)
plt.xlabel("$x_0$", fontsize=20)
plt.ylabel("$x'_1$ ", fontsize=20, rotation=0)
plt.title("Scaled", fontsize=16)
plt.axis([-2, 2, -2, 2])
plt.tight_layout()
plt.show()
输出:
2. 软间隔分类(Soft Margin Classification)
硬间隔分类:严格要求所有样本都必须在间隔之外。这时就有两个问题:一是这种模型只能用于严格线性可分的数据,二是这种模型对离群值异常敏感。
软间隔分类:容忍一定数量的样本位于分类间隔中,这样可以增加模型的泛化能力和模型的应用场景。
X_outliers = np.array([[3.4, 1.3], [3.2, 0.8]])
y_outliers = np.array([0, 0])
Xo1 = np.concatenate([X, X_outliers[:1]], axis=0)
yo1 = np.concatenate([y, y_outliers[:1]], axis=0)
Xo2 = np.concatenate([X, X_outliers[1:]], axis=0)
yo2 = np.concatenate([y, y_outliers[1:]], axis=0)
svm_clf2 = SVC(kernel="linear", C=10**9)
svm_clf2.fit(Xo2, yo2)
fig, axes = plt.subplots(ncols=2, figsize=(12,5), sharey=True)
plt.sca(axes[0])
plt.plot(Xo1[:, 0][yo1==1], Xo1[:, 1][yo1==1], "bs")
plt.plot(Xo1[:, 0][yo1==0], Xo1[:, 1][yo1==0], "yo")
plt.text(0.3, 1.0, "Impossible!", fontsize=24, color="red")
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.annotate("Outlier",
xy=(X_outliers[0][0], X_outliers[0][1]),
xytext=(2.5, 1.7),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.1),
fontsize=16,
)
plt.axis([0, 5.5, 0, 2])
plt.sca(axes[1])
plt.plot(Xo2[:, 0][yo2==1], Xo2[:, 1][yo2==1], "bs")
plt.plot(Xo2[:, 0][yo2==0], Xo2[:, 1][yo2==0], "yo")
plot_svc_decision_boundary(svm_clf2, 0, 5.5)
plt.xlabel("Petal length", fontsize=14)
plt.annotate("Outlier",
xy=(X_outliers[1][0], X_outliers[1][1]),
xytext=(3.2, 0.08),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.1),
fontsize=16,
)
plt.axis([0, 5.5, 0, 2])
plt.tight_layout()
plt.show()
输出:
如上所示,左图中有一个黄色离群点,此时就无法用硬间隔SVM进行分类。而右图中也存在一个黄色离群点,但幸好可以用硬间隔SVM分类,但是与之前的图进行对比发现,决策间隔变小了很多很多,也就是说模型的泛化能力下降很多。
以上这种存在离群值的数据是日常实际应用是更常见的情况,而解决这种问题软间隔SVM分类就比较拿手。
from sklearn import datasets
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import LinearSVC
iris = datasets.load_iris()
X = iris["data"][:,(2,3)] # 选择花瓣长度和宽度属性
y = (iris["target"]==2).astype(np.float64)
svm_clf = Pipeline([
("scaler",StandardScaler()),
("linear_svc",LinearSVC(C=1,loss="hinge",random_state=42))
])
svm_clf.fit(X,y)
输出:
Pipeline(memory=None,
steps=[('scaler',
StandardScaler(copy=True, with_mean=True, with_std=True)),
('linear_svc',
LinearSVC(C=1, class_weight=None, dual=True,
fit_intercept=True, intercept_scaling=1,
loss='hinge', max_iter=1000, multi_class='ovr',
penalty='l2', random_state=42, tol=0.0001,
verbose=0))],
verbose=False)
svm_clf.predict([[5.5,1.7]])
输出:
array([1.])
与逻辑回归分类器不一样,SVM只输出最终的类别,不会输出属于每个类别的概率。
注意:LinearSVC同样会对偏置项进行正则化,因此需要对训练集先做减平均值的操作,这种操作由StandardScaler自动完成。同时需要注意需要将超参数loss的值设置为hinge。最后为了达到更好的性能和效果,需要将超参数dual设置为False,除非训练集中特征数目大于训练样本数。
scaler = StandardScaler()
svm_clf1 = LinearSVC(C=1, loss="hinge", random_state=42)
svm_clf2 = LinearSVC(C=100, loss="hinge", random_state=42)
scaled_svm_clf1 = Pipeline([
("scaler", scaler),
("linear_svc", svm_clf1),
])
scaled_svm_clf2 = Pipeline([
("scaler", scaler),
("linear_svc", svm_clf2),
])
scaled_svm_clf1.fit(X, y)
scaled_svm_clf2.fit(X, y)
输出:
Pipeline(memory=None,
steps=[('scaler',
StandardScaler(copy=True, with_mean=True, with_std=True)),
('linear_svc',
LinearSVC(C=100, class_weight=None, dual=True,
fit_intercept=True, intercept_scaling=1,
loss='hinge', max_iter=1000, multi_class='ovr',
penalty='l2', random_state=42, tol=0.0001,
verbose=0))],
verbose=False)
# Convert to unscaled parameters
b1 = svm_clf1.decision_function([-scaler.mean_ / scaler.scale_])
b2 = svm_clf2.decision_function([-scaler.mean_ / scaler.scale_])
w1 = svm_clf1.coef_[0] / scaler.scale_
w2 = svm_clf2.coef_[0] / scaler.scale_
svm_clf1.intercept_ = np.array([b1])
svm_clf2.intercept_ = np.array([b2])
svm_clf1.coef_ = np.array([w1])
svm_clf2.coef_ = np.array([w2])
# Find support vectors (LinearSVC does not do this automatically)
t = y * 2 - 1
support_vectors_idx1 = (t * (X.dot(w1) + b1) < 1).ravel()
support_vectors_idx2 = (t * (X.dot(w2) + b2) < 1).ravel()
svm_clf1.support_vectors_ = X[support_vectors_idx1]
svm_clf2.support_vectors_ = X[support_vectors_idx2]
fig, axes = plt.subplots(ncols=2, figsize=(12,5), sharey=True)
plt.sca(axes[0])
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "g^", label="Iris virginica")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "bs", label="Iris versicolor")
plot_svc_decision_boundary(svm_clf1, 4, 5.9)
plt.xlabel("Petal length", fontsize=14)
plt.ylabel("Petal width", fontsize=14)
plt.legend(loc="upper left", fontsize=14)
plt.title("$C = {}$".format(svm_clf1.C), fontsize=16)
plt.axis([4, 5.9, 0.8, 2.8])
plt.sca(axes[1])
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "g^")
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "bs")
plot_svc_decision_boundary(svm_clf2, 4, 5.99)
plt.xlabel("Petal length", fontsize=14)
plt.title("$C = {}$".format(svm_clf2.C), fontsize=16)
plt.axis([4, 5.9, 0.8, 2.8])
plt.tight_layout()
plt.show()
输出:
sklearn超参数C指定了这种容忍的程度,C值越小,容忍度越大,反之越小。注意:如果SVM过拟合,可以通过减小C值调整模型。
3. SVM非线性分类任务
虽然SVM线性分类器效果很好,但是实际应用中有些数据往往是线性不可分的。解决线性不可分问题的办法之一是添加更多的特征,例如添加多项式特征。
3.1 SVM解决非线性可分问题
X1D = np.linspace(-4, 4, 9).reshape(-1, 1)
X2D = np.c_[X1D, X1D**2]
y = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0])
plt.figure(figsize=(12, 5))
plt.subplot(121)
plt.grid(True, which='both')
plt.axhline(y=0, color='k')
plt.plot(X1D[:, 0][y==0], np.zeros(4), "bs")
plt.plot(X1D[:, 0][y==1], np.zeros(5), "g^")
plt.gca().get_yaxis().set_ticks([])
plt.xlabel(r"$x_1$", fontsize=20)
plt.axis([-4.5, 4.5, -0.2, 0.2])
plt.subplot(122)
plt.grid(True, which='both')
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
plt.plot(X2D[:, 0][y==0], X2D[:, 1][y==0], "bs")
plt.plot(X2D[:, 0][y==1], X2D[:, 1][y==1], "g^")
plt.xlabel(r"$x_1$", fontsize=20)
plt.ylabel(r"$x_2$ ", fontsize=20, rotation=0)
plt.gca().get_yaxis().set_ticks([0, 4, 8, 12, 16])
plt.plot([-4.5, 4.5], [6.5, 6.5], "r--", linewidth=3)
plt.axis([-4.5, 4.5, -1, 17])
plt.subplots_adjust(right=1)
plt.tight_layout()
plt.show()
输出:
观察上面左图,数据只有一个特征x1,分别有绿色和蓝色两类点,可以看出无法用一条直线将它们分开。
如果增加一个特征,即x1的平方,此时再观察右图就可以发现这两类点可以用一条直线分开。
感觉增加多项式特征在解决线性不可分问题还是很有用的,现在尝试将其运用在moon数据集上:
from sklearn.datasets import make_moons
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import PolynomialFeatures
X,y = make_moons(n_samples=100, noise=0.15, random_state=42)
def plot_dataset(X,y,axes):
#plt.figure(figsize=(8,5))
plt.plot(X[:,0][y==0],X[:,1][y==0],"bs")
plt.plot(X[:,0][y==1],X[:,1][y==1],"g^")
plt.axis(axes)
plt.grid(True,which="both")
plt.xlabel("$x_1$",fontsize=20)
plt.ylabel("$x_2$",fontsize=20,rotation=0)
plot_dataset(X,y,[-1.5,2.5,-1,1.5])
plt.show()
输出:
polynomial_svm_clf = Pipeline([
("poly_features",PolynomialFeatures(degree=3)),
("scaler",StandardScaler()),
("svm_clf",LinearSVC(C=10,loss="hinge", random_state=42))
])
polynomial_svm_clf.fit(X,y)
输出:
Pipeline(memory=None,
steps=[('poly_features',
PolynomialFeatures(degree=3, include_bias=True,
interaction_only=False, order='C')),
('scaler',
StandardScaler(copy=True, with_mean=True, with_std=True)),
('svm_clf',
LinearSVC(C=10, class_weight=None, dual=True,
fit_intercept=True, intercept_scaling=1,
loss='hinge', max_iter=1000, multi_class='ovr',
penalty='l2', random_state=42, tol=0.0001,
verbose=0))],
verbose=False)
def plot_predictions(clf, axes):
x0s = np.linspace(axes[0],axes[1],100)
x1s = np.linspace(axes[2],axes[3],100)
x0, x1 = np.meshgrid(x0s, x1s)
X = np.c_[x0.ravel(),x1.ravel()]
y_pred = clf.predict(X).reshape(x0.shape)
y_decision = clf.decision_function(X).reshape(x0.shape)
plt.contourf(x0,x1,y_pred,cmap=plt.cm.brg, alpha=0.2)
plt.contourf(x0,x1,y_decision, cmap=plt.cm.brg,alpha=0.1)
plot_predictions(polynomial_svm_clf, [-1.5,2.5,-1,1.5])
plot_dataset(X,y,[-1.5,2.5,-1,1.5])
plt.show()
输出:
从上述输出结果可以看出,线性不可分的二分类问题经过添加多项式特征后变得可分了。
3.2 多项式核
添加多项式特征的方法比较简单,而且在很多机器学习算法中都可以使用。但是如果多项式次数不够高,则无法处理比较复杂的数据,如果次数太高,则会产生大量的特征从而导致模型训练会很慢很慢。
然而为了解决如上的矛盾,SVM中存在kernel trick(核技巧)的方法,核技巧可以达到添加高次多项式的效果,但不会真正添加到数据中,所以也就不存在维数爆炸的现象。
from sklearn.svm import SVC
poly_kernel_svm_clf = Pipeline([
("scaler",StandardScaler()),
("svm_clf",SVC(kernel="poly",degree=3,coef0=1,C=5))
])
poly_kernel_svm_clf.fit(X,y)
输出:
Pipeline(memory=None,
steps=[('scaler',
StandardScaler(copy=True, with_mean=True, with_std=True)),
('svm_clf',
SVC(C=5, cache_size=200, class_weight=None, coef0=1,
decision_function_shape='ovr', degree=3,
gamma='auto_deprecated', kernel='poly', max_iter=-1,
probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False))],
verbose=False)
poly100_kernel_svm_clf = Pipeline([
("scaler",StandardScaler()),
("svm_clf",SVC(kernel="poly",degree=10,coef0=100,C=5))
])
poly100_kernel_svm_clf.fit(X,y)
输出:
Pipeline(memory=None,
steps=[('scaler',
StandardScaler(copy=True, with_mean=True, with_std=True)),
('svm_clf',
SVC(C=5, cache_size=200, class_weight=None, coef0=100,
decision_function_shape='ovr', degree=10,
gamma='auto_deprecated', kernel='poly', max_iter=-1,
probability=False, random_state=None, shrinking=True,
tol=0.001, verbose=False))],
verbose=False)
fig, axes = plt.subplots(ncols=2,figsize=(12,5),sharey=True)
plt.sca(axes[0])
plot_predictions(poly_kernel_svm_clf,[-1.5,2.45,-1,1.5])
plot_dataset(X,y,[-1.5,2.4,-1,1.5])
plt.title(r"$d=3, r=1, C=5$",fontsize=18)
plt.sca(axes[1])
plot_predictions(poly100_kernel_svm_clf, [-1.5,2.45,-1,1.5])
plot_dataset(X,y,[-1.5,2.45,-1,1.5])
plt.title(r"$d=10, r=100, C=5$",fontsize=18)
plt.ylabel("")
plt.tight_layout()
plt.show()
输出:
如上图所示,左边是利用SVM三次多项式核训练的模型效果,右边是10次多项式核模型。观察发现,如果模型过拟合,就需要降低degree的值,反之如果欠拟合,就需要增大degree的值。
超参数coef0控制模型受高次多项式和低次多项式影响的程度。
3.3 相似特征
上面介绍了解决线性不可分问题的解决方法之一,即添加多项式特征。
解决线性不可分问题另一个方法是添加利用相似函数计算的特征。 相似函数计算样本与特定目标相似的程度。
def gaussian_rbf(x, landmark, gamma): # 高斯径向基函数
return np.exp(-gamma * np.linalg.norm(x - landmark, axis=1)**2)
gamma = 0.3
x1s = np.linspace(-4.5, 4.5, 200).reshape(-1, 1)
x2s = gaussian_rbf(x1s, -2, gamma)
x3s = gaussian_rbf(x1s, 1, gamma)
XK = np.c_[gaussian_rbf(X1D, -2, gamma), gaussian_rbf(X1D, 1, gamma)]
yk = np.array([0, 0, 1, 1, 1, 1, 1, 0, 0])
plt.figure(figsize=(12, 5))
plt.subplot(121)
plt.grid(True, which='both')
plt.axhline(y=0, color='k')
plt.scatter(x=[-2, 1], y=[0, 0], s=150, alpha=0.5, c="red")
plt.plot(X1D[:, 0][yk==0], np.zeros(4), "bs")
plt.plot(X1D[:, 0][yk==1], np.zeros(5), "g^")
plt.plot(x1s, x2s, "g--")
plt.plot(x1s, x3s, "b:")
plt.gca().get_yaxis().set_ticks([0, 0.25, 0.5, 0.75, 1])
plt.xlabel(r"$x_1$", fontsize=20)
plt.ylabel(r"Similarity", fontsize=14)
plt.annotate(r'$\mathbf{x}$',
xy=(X1D[3, 0], 0),
xytext=(-0.5, 0.20),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.1),
fontsize=18,
)
plt.text(-2, 0.9, "$x_2$", ha="center", fontsize=20)
plt.text(1, 0.9, "$x_3$", ha="center", fontsize=20)
plt.axis([-4.5, 4.5, -0.1, 1.1])
plt.subplot(122)
plt.grid(True, which='both')
plt.axhline(y=0, color='k')
plt.axvline(x=0, color='k')
plt.plot(XK[:, 0][yk==0], XK[:, 1][yk==0], "bs")
plt.plot(XK[:, 0][yk==1], XK[:, 1][yk==1], "g^")
plt.xlabel(r"$x_2$", fontsize=20)
plt.ylabel(r"$x_3$ ", fontsize=20, rotation=0)
plt.annotate(r'$\phi\left(\mathbf{x}\right)$',
xy=(XK[3, 0], XK[3, 1]),
xytext=(0.65, 0.50),
ha="center",
arrowprops=dict(facecolor='black', shrink=0.1),
fontsize=18,
)
plt.plot([-0.1, 1.1], [0.57, -0.1], "r--", linewidth=3)
plt.axis([-0.1, 1.1, -0.1, 1.1])
plt.subplots_adjust(right=1)
plt.tight_layout()
plt.show()
输出:
上图左图所示,我们分别在x=-2和x=1处添加两个landmarks。
此时,定义相似函数为高斯径向基函数(Gaussian Radial Basis Function,RBF),