Python 数据科学手册 5.7 支持向量机

2022-12-01 17:26:39 浏览数 (2)

5.7 支持向量机

支持向量机(SVM)是一种特别强大且灵活的监督算法,用于分类和回归。 在本节中,我们将探索支持向量机背后的直觉,及其在分类问题中的应用。

我们以标准导入开始:

代码语言:javascript复制
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# use seaborn plotting defaults
import seaborn as sns; sns.set()

支持向量机的动机

作为贝叶斯分类讨论的一部分(见朴素贝叶斯分类),我们学习了一个简单模型,它描述每个底层类的分布,并使用这些生成模型,依概率确定新的点的标签。 这是生成分类的一个例子。 这里我们将考虑区分性分类:我们不对每个类进行建模,只需找到一条或两条直线(在两个维度上),或者流形(在多个维度上),将类彼此划分。

作为一个例子,考虑分类任务的简单情况,其中两个类别的点是良好分隔的:

代码语言:javascript复制
from sklearn.datasets.samples_generator import make_blobs
X, y = make_blobs(n_samples=50, centers=2,
                  random_state=0, cluster_std=0.60)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn');

线性判别分类器将尝试绘制分离两组数据的直线,从而创建分类模型。 对于这里所示的二维数据,这是我们可以手动完成的任务。 但是立刻我们看到一个问题:有两个以上的可能的分界线可以完美地区分两个类!

我们可以画出如下:

线性判别分类器尝试绘制分离两组数据的直线,从而创建分类模型。 对于这里所示的二维数据,这是我们可以手动完成的任务。 但是我们立刻看到一个问题:有两个以上的可能的分界线,可以完美地区分两个类!

我们可以这样绘制:

代码语言:javascript复制
xfit = np.linspace(-1, 3.5)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plt.plot([0.6], [2.1], 'x', color='red', markeredgewidth=2, markersize=10)

for m, b in [(1, 0.65), (0.5, 1.6), (-0.2, 2.9)]:
    plt.plot(xfit, m * xfit   b, '-k')

plt.xlim(-1, 3.5);

这些是三个非常不同的分隔直线,然而,这些分隔直线能够完全区分这些样例。 根据你的选择,为新数据点(例如,该图中由“X”标记的数据点)分配不同的标签! 显然,我们简单的直觉,“在分类之间划线”是不够的,我们需要进一步思考。

支持向量机:间距最大化

支持向量机提供了一种改进方法。 直觉是这样的:我们并非在分类之间,简单绘制一个零宽度的直线,而是画出边距为一定宽度的直线,直到最近的点。 这是一个例子:

代码语言:javascript复制
xfit = np.linspace(-1, 3.5)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')

for m, b, d in [(1, 0.65, 0.33), (0.5, 1.6, 0.55), (-0.2, 2.9, 0.2)]:
    yfit = m * xfit   b
    plt.plot(xfit, yfit, '-k')
    plt.fill_between(xfit, yfit - d, yfit   d, edgecolor='none',
                     color='#AAAAAA', alpha=0.4)

plt.xlim(-1, 3.5);

在支持向量机中,边距最大化的直线是我们将选择的最优模型。 支持向量机是这种最大边距估计器的一个例子。

拟合支持向量机

我们来看看这个数据的实际结果:我们将使用 Scikit-Learn 的支持向量分类器,对这些数据训练 SVM 模型。 目前,我们将使用一个线性核并将C参数设置为一个非常大的数值(我们之后深入讨论这些参数的含义)。

代码语言:javascript复制
from sklearn.svm import SVC # "Support vector classifier"
model = SVC(kernel='linear', C=1E10)
model.fit(X, y)
代码语言:javascript复制
SVC(C=10000000000.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape=None, degree=3, gamma='auto', kernel='linear',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

为了更好展现这里发生的事情,让我们创建一个辅助函数,为我们绘制 SVM 的决策边界。

代码语言:javascript复制
def plot_svc_decision_function(model, ax=None, plot_support=True):
    """Plot the decision function for a 2D SVC"""
    if ax is None:
        ax = plt.gca()
    xlim = ax.get_xlim()
    ylim = ax.get_ylim()

    # create grid to evaluate model
    x = np.linspace(xlim[0], xlim[1], 30)
    y = np.linspace(ylim[0], ylim[1], 30)
    Y, X = np.meshgrid(y, x)
    xy = np.vstack([X.ravel(), Y.ravel()]).T
    P = model.decision_function(xy).reshape(X.shape)

    # plot decision boundary and margins
    ax.contour(X, Y, P, colors='k',
               levels=[-1, 0, 1], alpha=0.5,
               linestyles=['--', '-', '--'])

    # plot support vectors
    if plot_support:
        ax.scatter(model.support_vectors_[:, 0],
                   model.support_vectors_[:, 1],
                   s=300, linewidth=1, facecolors='none');
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
代码语言:javascript复制
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(model);

这是最大化两组点之间的间距的分界线。 请注意,一些训练点只是碰到了边缘:它们由该图中的黑色圆圈表示。 这些点是这种拟合的关键要素,被称为支持向量,并提供了算法的名称。 在 Scikit-Learn 中,这些点存储在分类器的support_vectors_属性中:

代码语言:javascript复制
model.support_vectors_
代码语言:javascript复制
array([[ 0.44359863,  3.11530945],
       [ 2.33812285,  3.43116792],
       [ 2.06156753,  1.96918596]])

这个分类器的成功的关键在于,为了拟合,只有支持向量的位置是重要的;任何远离边距的点,都不会影响拟合。 从技术上讲,这是因为这些要点不用于拟合模型的损失函数,所以只要不超过边距,它们的位置和数值就不重要了。

我们可以看到这一点,例如,如果我们绘制该数据集的前 60 个点和前120个点获得的模型:

代码语言:javascript复制
def plot_svm(N=10, ax=None):
    X, y = make_blobs(n_samples=200, centers=2,
                      random_state=0, cluster_std=0.60)
    X = X[:N]
    y = y[:N]
    model = SVC(kernel='linear', C=1E10)
    model.fit(X, y)

    ax = ax or plt.gca()
    ax.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
    ax.set_xlim(-1, 4)
    ax.set_ylim(-1, 6)
    plot_svc_decision_function(model, ax)

fig, ax = plt.subplots(1, 2, figsize=(16, 6))
fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)
for axi, N in zip(ax, [60, 120]):
    plot_svm(N, axi)
    axi.set_title('N = {0}'.format(N))

在左图中,我们看到了 60 个训练点的模型和支持向量。 在右图中,我们将训练点数量翻了一番,但是模型没有改变:左图的三个支持向量仍然是右图的支持向量。 远程点的确切行为的这种不敏感性,是 SVM 模型的优点之一。

如果你正在运行这个笔记,可以使用 IPython 的交互式小部件,以交互方式查看 SVM 模型的此功能:

代码语言:javascript复制
from ipywidgets import interact, fixed
interact(plot_svm, N=[10, 200], ax=fixed(None));

超越支持向量机:核

SVM 与核结合在一起,就会变得非常强大。 之前,我们已经看到了一个核的版本,就是“线性回归”中的基函数。 在那里,我们将数据投影到更高维空间中,由多项式和高斯基函数定义,从而能够将线性分类器用于非线性关系。

在 SVM 模型中,我们可以使用相同想法的一个版本。 为了阐述核的动机,我们来看一些不是线性分离的数据:

代码语言:javascript复制
from sklearn.datasets.samples_generator import make_circles
X, y = make_circles(100, factor=.1, noise=.1)

clf = SVC(kernel='linear').fit(X, y)

plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf, plot_support=False);

很明显,这些数据不可能线性分隔。 但是,我们可以从线性回归中的基函数回归中吸取经验,并考虑如何将数据投影到更高的维度,使得线性分隔就足够了。 例如,我们可以使用的一个简单的投影是径向基函数,中心是中间那一堆点:

代码语言:javascript复制
r = np.exp(-(X ** 2).sum(1))

我们可以使用三维图形来显示这个额外的数据维度 - 如果你正在运行笔记本,则可以使用滑块来旋转图形:

代码语言:javascript复制
from mpl_toolkits import mplot3d

def plot_3D(elev=30, azim=30, X=X, y=y):
    ax = plt.subplot(projection='3d')
    ax.scatter3D(X[:, 0], X[:, 1], r, c=y, s=50, cmap='autumn')
    ax.view_init(elev=elev, azim=azim)
    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_zlabel('r')

interact(plot_3D, elev=[-90, 90], azip=(-180, 180),
         X=fixed(X), y=fixed(y));

我们可以看到,使用这个附加维度,通过在r = 0.7处绘制分离平面,数据可以线性分离。

在这里,我们必须选择并仔细调整我们的预测:如果我们没有将径向基函数置于正确的位置,我们就不会看到这样清晰的线性可分离结果。一般来说,做出这样的选择的需求是一个问题:我们想以某种方式自动找到最佳的基函数来使用。

为此,一个策略是计算以数据集中每个点为中心的基函数,并使 SVM 算法筛选出结果。这种类型的基函数变换被称为核变换,因为它基于每对点之间的相似关系(或核)。

这种策略的潜在问题 - 将N个点投影到N个维度 - 就是随着N增长,它的计算开销可能会变得非常大。然而,由于一个被称为核技巧的简洁的小过程,内核转换数据上的拟合可以隐式完成,也就是说,不需要为核投影构建完全的N维数据表示!这个核技巧内置在 SVM 中,也是该方法如此强大的原因之一。

在 Scikit-Learn 中,我们可以通过使用kernel模型超参数,将线性核更改为 RBF(径向基函数)核来应用核化 SVM:

代码语言:javascript复制
clf = SVC(kernel='rbf', C=1E6)
clf.fit(X, y)
代码语言:javascript复制
SVC(C=1000000.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape=None, degree=3, gamma='auto', kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False)
代码语言:javascript复制
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
plot_svc_decision_function(clf)
plt.scatter(clf.support_vectors_[:, 0], clf.support_vectors_[:, 1],
            s=300, lw=1, facecolors='none');

使用这个核化的 SVM,我们得到了合适的非线性决策边界。这个核的转换策略,通常用在机器学习中,将线性方法快速调整为非线性方法,尤其是可以使用核技巧的模型。

调整 SVM:软边距

我们迄今为止的讨论集中在非常干净的数据集,其中存在完美的决策边界。 但是如果你的数据有一定的重叠呢? 例如,你可能拥有如下数据:

代码语言:javascript复制
X, y = make_blobs(n_samples=100, centers=2,
                  random_state=0, cluster_std=1.2)
plt.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn');

为了处理这种情况,SVM 实现了软化因子,即“软化”边距:也就是说,如果允许更好的匹配,它允许某些点进入边距。 边缘的硬度由调整参数控制,通常称为C。 对于非常大的C,边距是硬的,点不能进入。 对于较小的C,边缘较软,可以扩展并包含一些点。

下图显示了参数的变化C,如何影响最终拟合,通过软化边缘:

代码语言:javascript复制
X, y = make_blobs(n_samples=100, centers=2,
                  random_state=0, cluster_std=0.8)

fig, ax = plt.subplots(1, 2, figsize=(16, 6))
fig.subplots_adjust(left=0.0625, right=0.95, wspace=0.1)

for axi, C in zip(ax, [10.0, 0.1]):
    model = SVC(kernel='linear', C=C).fit(X, y)
    axi.scatter(X[:, 0], X[:, 1], c=y, s=50, cmap='autumn')
    plot_svc_decision_function(model, axi)
    axi.scatter(model.support_vectors_[:, 0],
                model.support_vectors_[:, 1],
                s=300, lw=1, facecolors='none');
    axi.set_title('C = {0:.1f}'.format(C), size=14)

参数C的最佳值将取决于你的数据集,并应使用交叉验证或类似的过程进行调整(请参阅超参数和模型验证)。

示例:人脸识别

作为支持向量机的一个例子,我们来看看人脸识别问题。 我们将使用 Wild 数据集中的标记人脸,其中包含数千张各种公众人物的整理照片。 数据集的获取器内置于 Scikit-Learn中:

代码语言:javascript复制
from sklearn.datasets import fetch_lfw_people
faces = fetch_lfw_people(min_faces_per_person=60)
print(faces.target_names)
print(faces.images.shape)
代码语言:javascript复制
['Ariel Sharon' 'Colin Powell' 'Donald Rumsfeld' 'George W Bush'
 'Gerhard Schroeder' 'Hugo Chavez' 'Junichiro Koizumi' 'Tony Blair']
(1348, 62, 47)

让我们绘制这些人脸来看看我们要处理什么:

代码语言:javascript复制
fig, ax = plt.subplots(3, 5)
for i, axi in enumerate(ax.flat):
    axi.imshow(faces.images[i], cmap='bone')
    axi.set(xticks=[], yticks=[],
            xlabel=faces.target_names[faces.target[i]])

每个图像包含[62×47]或近 3,000 个像素。 我们可以简单地使用每个像素值作为特征,但是通常使用某种预处理器,来提取更有意义的特征更有效;在这里,我们将使用主成分分析(参见主成分分析)来提取150个基本成分,并扔给我们的支持向量机分类器。 我们可以将预处理器和分类器打包成单个管道,来最直接地执行此操作:

代码语言:javascript复制
from sklearn.svm import SVC
from sklearn.decomposition import RandomizedPCA
from sklearn.pipeline import make_pipeline

pca = RandomizedPCA(n_components=150, whiten=True, random_state=42)
svc = SVC(kernel='rbf', class_weight='balanced')
model = make_pipeline(pca, svc)

出于测试我们的分类器输出的目的,我们将数据分割成训练集和测试集。

代码语言:javascript复制
from sklearn.cross_validation import train_test_split
Xtrain, Xtest, ytrain, ytest = train_test_split(faces.data, faces.target,
                                                random_state=42)

最后,我们可以使用网格搜索的交叉验证来探索参数的组合。 这里我们将调整C(控制边缘硬度)和gamma(其控制径向基函数核的大小),并确定最佳模型:

代码语言:javascript复制
from sklearn.grid_search import GridSearchCV
param_grid = {'svc__C': [1, 5, 10, 50],
              'svc__gamma': [0.0001, 0.0005, 0.001, 0.005]}
grid = GridSearchCV(model, param_grid)

%time grid.fit(Xtrain, ytrain)
print(grid.best_params_)
代码语言:javascript复制
CPU times: user 47.8 s, sys: 4.08 s, total: 51.8 s
Wall time: 26 s
{'svc__gamma': 0.001, 'svc__C': 10}

最优值落在我们网格中间;如果他们落在边缘,我们需要扩大网格,来确保我们找到了真正的最优值。

现在有了这种交叉验证的模型,我们可以预测测试数据的标签,该模型还没有看到:

代码语言:javascript复制
model = grid.best_estimator_
yfit = model.predict(Xtest)

让我们看一看一些测试图像,以及它们的预测值。

代码语言:javascript复制
fig, ax = plt.subplots(4, 6)
for i, axi in enumerate(ax.flat):
    axi.imshow(Xtest[i].reshape(62, 47), cmap='bone')
    axi.set(xticks=[], yticks=[])
    axi.set_ylabel(faces.target_names[yfit[i]].split()[-1],
                   color='black' if yfit[i] == ytest[i] else 'red')
fig.suptitle('Predicted Names; Incorrect Labels in Red', size=14);

在这个小样本中,我们的最佳估计器只错误标记一个人脸(底部的行中,布什的脸错误标记为布莱尔)。 我们可以使用分类报告更好了解我们的估计器的表现,该分类报告按标签列出了恢复统计量:

代码语言:javascript复制
from sklearn.metrics import classification_report
print(classification_report(ytest, yfit,
                            target_names=faces.target_names))
代码语言:javascript复制
                   precision    recall  f1-score   support

     Ariel Sharon       0.65      0.73      0.69        15
     Colin Powell       0.81      0.87      0.84        68
  Donald Rumsfeld       0.75      0.87      0.81        31
    George W Bush       0.93      0.83      0.88       126
Gerhard Schroeder       0.86      0.78      0.82        23
      Hugo Chavez       0.93      0.70      0.80        20
Junichiro Koizumi       0.80      1.00      0.89        12
       Tony Blair       0.83      0.93      0.88        42

      avg / total       0.85      0.85      0.85       337

我们也可以展示这些分类之间的混淆矩阵:

代码语言:javascript复制
from sklearn.metrics import confusion_matrix
mat = confusion_matrix(ytest, yfit)
sns.heatmap(mat.T, square=True, annot=True, fmt='d', cbar=False,
            xticklabels=faces.target_names,
            yticklabels=faces.target_names)
plt.xlabel('true label')
plt.ylabel('predicted label');

这有助于我们了解哪些标签可能被估算器混淆。

对于真实的人脸识别任务,其中照片不会被预先裁剪成漂亮的网格,人脸分类方案的唯一区别是特征选择:您需要使用更复杂的算法来查找人脸, 并提取独立于像素的特征。 对于这种应用,一个很好的选择是使用 OpenCV,除了别的以外,它包括用于一般图像的,以及专用于人脸的现代化特征提取工具。

支持向量机总结

我们在这里看到了支持向量机背后的原则的简单直观的介绍。这些方法是强大的分类方法,原因有很多:

  • 他们依赖相对较少的支持向量,意味着它们是非常紧凑的模型,并且占用很少的内存。
  • 一旦训练了模型,预测阶段非常快。
  • 因为它们仅受边缘附近的点的影响,它们适用于高维数据,甚至维度大于样本的数据,这对于其他算法来说是一个挑战。
  • 内核方法的集成使得它们非常通用,能够适应许多类型的数据。

然而,SVM也有几个缺点:

  • 在最差的情况下,样本数N的复杂度为O(N^3),对于高效的实现,是O(N^2)。对于大量的训练样本,这种计算成本可能令人望而却步。
  • 结果强烈依赖于软化参数C的合适选择。这必须通过交叉验证仔细选择,随着数据集增大,开销也增大。
  • 结果没有直接的概率解释。这可以通过内部交叉验证来估计(参见SVC的概率参数),但这种额外的估计是昂贵的。

考虑到这些特性,一般来说,只要其他更简单,更快,并且不需要调优的方法不足以满足我的需求,我一般只会考虑 SVM。然而,如果你投入了足够的 CPU 周期,使用 SVM 训练和验证你的数据,这个方法有很好的效果。

0 人点赞