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
参数设置为一个非常大的数值(我们之后深入讨论这些参数的含义)。
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_
属性中:
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:
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
,如何影响最终拟合,通过软化边缘:
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个基本成分,并扔给我们的支持向量机分类器。 我们可以将预处理器和分类器打包成单个管道,来最直接地执行此操作:
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
(其控制径向基函数核的大小),并确定最佳模型:
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 训练和验证你的数据,这个方法有很好的效果。