《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机

第5章 支持向量机

支持向量机(SVM)是个非常强大并且有多种功能的机器学习模型,能够做线性或者非线性的分类,回归,甚至异常值检测。机器学习领域中最为流行的模型之一,是任何学习机器学习的人必备的工具。SVM 特别适合应用于复杂但中小规模数据集的分类问题。


线性支持向量机分类

SVM 的基本思想能够用一些图片来解释得很好。左边的图显示了三种可能的线性分类器的判定边界。其中用虚线表示的线性模型判定边界很差,甚至不能正确地划分类别。另外两个线性模型在这个数据集表现的很好,但是它们的判定边界很靠近样本点,在新的数据上可能不会表现的很好。相比之下,右边图中 SVM 分类器的判定边界实线,不仅分开了两种类别,而且还尽可能地远离了最靠近的训练数据点。可以认为 SVM 分类器在两种类别之间保持了一条尽可能宽敞的街道(图中平行的虚线),其被称为最大间隔分类。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第1张图片

我们注意到添加更多的样本点在“街道”外并不会影响到判定边界,因为判定边界是由位于“街道”边缘的样本点确定的,这些样本点被称为“支持向量”(图中被圆圈圈起来的点)

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第2张图片

软间隔分类

如果我们严格地规定所有的数据都不在“街道”上,都在正确地两边,称为硬间隔分类,硬间隔分类有两个问题,第一,只对线性可分的数据起作用,第二,对异常点敏感。下图显示了只有一个异常点的鸢尾花数据集:左边的图中很难找到硬间隔,右边的图中判定边界和我们之前在图 5-1 中没有异常点的判定边界非常不一样,它很难一般化。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第3张图片

为了避免上述的问题,我们更倾向于使用更加软性的模型。目的在保持“街道”尽可能大和避免间隔违规(例如:数据点出现在“街道”中央或者甚至在错误的一边)之间找到一个良好的平衡。这就是软间隔分类。

在 Scikit-Learn 库的 SVM 类,可以用C超参数(惩罚系数)来控制这种平衡:较小的C会导致更宽的“街道”,但更多的间隔违规。图 5-4 显示了在非线性可分隔的数据集上,两个软间隔SVM分类器的判定边界。左边图中,使用了较大的C值,导致更少的间隔违规,但是间隔较小。右边的图,使用了较小的C值,间隔变大了,但是许多数据点出现在了“街道”上。然而,第二个分类器似乎泛化地更好:事实上,在这个训练数据集上减少了预测错误,因为实际上大部分的间隔违规点出现在了判定边界正确的一侧。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第4张图片

  • 如果 SVM 模型过拟合,可以尝试通过减小超参数C去调整。

以下的 Scikit-Learn 代码加载了内置的鸢尾花(Iris)数据集,缩放特征,并训练一个线性 SVM 模型(使用LinearSVC类,超参数C=1,hinge 损失函数)来检测 Virginica 鸢尾花。

import numpy as np 
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)] # petal length, petal width 
y = (iris["target"] == 2).astype(np.float64) # Iris-Virginica

svm_clf = Pipeline([
    ("scaler", StandardScaler()), 
    ("linear_svc", LinearSVC(C=1, loss="hinge")), 
    ])

svm_clf.fit(X, y)
>>>svm_clf.predict([[5.5, 1.7]])
array([ 1.])

铰链损失(Hinge Loss):主要用于支持向量机(SVM) 中; Hinge loss 的叫法来源于其损失函数的图形,为一个折线,通用的函数表达式为:

L(mi)=max(0,1mi(w)) L ( m i ) = m a x ( 0 , 1 − m i ( w ) )

表示如果被正确分类,损失是0,否则损失就是 1mi(w) 1 − m i ( w ) 。SVM 的损失函数可以看作是 L2-norm 和 Hinge loss 之和。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第5张图片

作为一种选择,可以在 SVC 类,使用SVC(kernel=”linear”, C=1),但是它比较慢,尤其在较大的训练集上,所以一般不被推荐。另一个选择是使用SGDClassifier类,即SGDClassifier(loss=”hinge”, alpha=1/(m*C))。它应用了随机梯度下降来训练一个线性 SVM 分类器。尽管它不会和LinearSVC一样快速收敛,但是对于处理那些不适合放在内存的大数据集是非常有用的,或者处理在线分类任务同样有用。

LinearSVC要使偏置项规范化,首先应该集中训练集减去它的平均数。如果使用了StandardScaler,那么它会自动处理。此外,确保设置loss参数为hinge,因为它不是默认值。最后,为了得到更好的效果,需要将dual参数设置为False,除非特征数比样本量多.

求解原问题(primal problem)和对偶问题(dual problem)。求解原问题使用的是TRON的优化算法,对偶问题使用的是Coordinate Descent优化算法。总的来说,两个算法的优化效率都较高,但还是有各自更加擅长的场景。对于样本量不大,但是维度特别高的场景,如文本分类,更适合对偶问题求解,因为由于样本量小,计算出来的Kernel Matrix也不大,后面的优化也比较方便。而如果求解原问题,则求导的过程中要频繁对高维的特征矩阵进行计算,如果特征比较稀疏的话,那么就会多做很多无意义的计算,影响优化的效率。相反,当样本数非常多,而特征维度不高时,如果采用求解对偶问题,则由于Kernel Matrix过大,求解并不方便。反倒是求解原问题更加容易。


非线性支持向量机分类

尽管线性 SVM 分类器在许多案例上表现得出乎意料的好,但是很多数据集并不是线性可分的。一种处理非线性数据集方法是增加更多的特征,例如多项式特征;在某些情况下可以变成线性可分的数据。在】左图中,它只有一个特征x1的简单的数据集,该数据集不是线性可分的。但是如果增加了第二个特征 x2=(x1)^2,产生的 2D 数据集就能很好的线性可分。

为了实施这个想法,通过 Scikit-Learn,可以创建一个流水线(Pipeline)去包含多项式特征(PolynomialFeatures)变换(在 121 页的“Polynomial Regression”中讨论),然后一个StandardScaler和LinearSVC。在卫星数据集(moons datasets)测试一下效果。

from sklearn.datasets import make_moons
X, y = make_moons(n_samples=100, noise=0.15, random_state=42)

def plot_dataset(X, y, axes):
    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(r"$x_1$", fontsize=20)
    plt.ylabel(r"$x_2$", fontsize=20, rotation=0)

plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第6张图片

from sklearn.datasets import make_moons 
from sklearn.pipeline import Pipeline 
from sklearn.preprocessing import PolynomialFeatures

polynomial_svm_clf = Pipeline([
    ("poly_features", PolynomialFeatures(degree=3)), 
    ("scaler", StandardScaler()), 
    ("svm_clf", LinearSVC(C=10, loss="hinge")) ])

polynomial_svm_clf.fit(X, y)

查看分类结果

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

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第7张图片

多项式核

添加多项式特征很容易实现,不仅仅在 SVM,在各种机器学习算法都有不错的表现,但是低次数的多项式不能处理非常复杂的数据集,而高次数的多项式却产生了大量的特征,会使模型变得慢。

幸运的是,当使用 SVM 时,可以运用一个被称为“核技巧”(kernel trick)的神奇数学技巧。它可以取得就像添加了许多多项式,甚至有高次数的多项式,一样好的结果。所以不会大量特征导致的组合爆炸,因为并没有增加任何特征。这个技巧可以用 SVC 类来实现。让我们在卫星数据集测试一下效果。

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)

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)

plt.figure(figsize=(11, 4))

plt.subplot(121)
plot_predictions(poly_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.title(r"$d=3, r=1, C=5$", fontsize=18)

plt.subplot(122)
plot_predictions(poly100_kernel_svm_clf, [-1.5, 2.5, -1, 1.5])
plot_dataset(X, y, [-1.5, 2.5, -1, 1.5])
plt.title(r"$d=10, r=100, C=5$", fontsize=18)
plt.show()

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第8张图片

左图使用3阶的多项式核训练了一个 SVM 分类器。右图是使用了10阶的多项式核 SVM 分类器。很明显,如果的模型过拟合,可以减小多项式核的阶数。相反的,如果是欠拟合,可以尝试增大它。超参数coef0控制了高阶多项式与低阶多项式对模型的影响。

通用的方法是用网格搜索去找到最优超参数。首先进行非常粗略的网格搜索一般会很快,然后在找到的最佳值进行更细的网格搜索。对每个超参数的作用有一个很好的理解可以帮助在正确的超参数空间找到合适的值。

增加相似特征

另一种解决非线性问题的方法是使用相似函数(similarity funtion)计算每个样本与特定地标(landmark)的相似度。例如,让我们来看看前面讨论过的一维数据集,并在x1=-2和x1=1之间增加两个地标。接下来,我们定义一个相似函数,即高斯径向基函数(Gaussian Radial Basis Function,RBF),设置γ = 0.3。

公式 5-1 RBF

ϕγ(x,)=exp(γ|x|2) ϕ γ ( x , ℓ ) = e x p ( − γ | x − ℓ | 2 )

它是个从 0 到 1 的钟型函数,值为 0 的离地标很远,值为 1 的在地标上。现在我们准备计算新特征。例如,我们看一下样本x1=-1:它距离第一个地标距离是 1,距离第二个地标是 2。因此它的新特征为x2=exp(-0.3 × (1^2))≈0.74和x3=exp(-0.3 × (2^2))≈0.30。右边的图显示了特征转换后的数据集(删除了原始特征),现在是线性可分了。

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第9张图片

高斯 RBF 核

就像多项式特征法一样,相似特征法对各种机器学习算法同样也有不错的表现。但是在所有额外特征上的计算成本可能很高,特别是在大规模的训练集上。然而,“核” 技巧再一次显现了它在 SVM 上的神奇之处:高斯核可以获得同样好的结果成为可能,就像在相似特征法添加了许多相似特征一样,但事实上,并不需要在RBF添加它们。我们使用 SVC 类的高斯 RBF 核来检验一下。

rbf_kernel_svm_clf = Pipeline((
        ("scaler", StandardScaler()),
        ("svm_clf", SVC(kernel="rbf", gamma=5, C=0.001))
    ))
rbf_kernel_svm_clf.fit(X, y)

《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第10张图片
上图显示了用不同的超参数gamma (γ)和C训练的模型。增大γ使钟型曲线更窄,导致每个样本的影响范围变得更小:即判定边界最终变得更不规则,在单个样本周围环绕。相反的,较小的γ值使钟型曲线更宽,样本有更大的影响范围,判定边界最终则更加平滑。所以γ是可调整的超参数:如果模型过拟合,应该减小γ值,若欠拟合,则增大γ(与超参数C相似)。

还有其他的核函数,但很少使用。例如,一些核函数是专门用于特定的数据结构。在对文本文档或者 DNA 序列进行分类时,有时会使用字符串核(String kernels)(例如,使用 SSK 核(string subsequence kernel)或者基于编辑距离(Levenshtein distance)的核函数)。

这么多可供选择的核函数,如何决定使用哪一个?
一般来说,应该先尝试线性核函数(记住LinearSVC比SVC(kernel=”linear”)要快得多),尤其是当训练集很大或者有大量的特征的情况下。如果训练集不太大,也可以尝试高斯径向基核(Gaussian RBF Kernel),它在大多数情况下都很有效。如果有空闲的时间和计算能力,还可以使用交叉验证和网格搜索来试验其他的核函数,特别是有专门用于的训练集数据结构的核函数。

计算复杂性

LinearSVC类基于liblinear库,它实现了线性 SVM 的优化算法。它并不支持核技巧,但是它样本和特征的数量几乎是线性的:训练时间复杂度大约为O(m × n)。

如果你要非常高的精度,这个算法需要花费更多时间。这是由容差值超参数ϵ(在 Scikit-learn 称为tol)控制的。大多数分类任务中,使用默认容差值的效果是已经可以满足一般要求。

SVC 类基于libsvm库,它实现了支持核技巧的算法。训练时间复杂度通常介于O(m^2 × n)和O(m^3 × n)之间。不幸的是,这意味着当训练样本变大时,它将变得极其慢(例如,成千上万个样本)。这个算法对于复杂但小型或中等数量的数据集表现是完美的。然而,它能对特征数量很好的缩放,尤其对稀疏特征来说(sparse features)。在这个情况下,算法对每个样本的非零特征的平均数量进行大概的缩放。
《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第11张图片


SVM 回归

正如我们之前提到的,SVM 算法应用广泛:不仅仅支持线性和非线性的分类任务,还支持线性和非线性的回归任务。技巧在于逆转我们的目标:限制间隔违规的情况下,不是试图在两个类别之间找到尽可能大的“街道”(即间隔)。SVM 回归任务是限制间隔的情况下,尽量放置更多的样本在“街道”上。“街道”的宽度由超参数ϵ控制。下图显示了在一些随机生成的线性数据上,两个线性 SVM 回归模型的训练情况。一个有较大的间隔(ϵ=1.5),另一个间隔较小(ϵ=0.5)。
《Hands-On Machine Learning with Scikit-Learn & TensorFlow》读书笔记 第五章 支持向量机_第12张图片

添加更多的数据样本在间隔之内并不会影响模型的预测,因此,这个模型认为是不敏感的(ϵ-insensitive)。

你可以使用 Scikit-Learn 的LinearSVR类去实现线性 SVM 回归。下面的代码产生的模型在图 5-10 左图(训练数据需要被中心化和标准化)

from sklearn.svm import LinearSVR
svm_reg = LinearSVR(epsilon=1.5)
svm_reg.fit(X, y)

处理非线性回归任务,你可以使用核化的 SVM 模型。


你可能感兴趣的:(hands-on,ML,with,Sklearn&TF)