支持向量机(SVM)是一种有监督学习算法,主要用于解决数据的分类问题。它尝试找到一个最优的超平面,该超平面能最大化两个类别之间的边界(也称为“决策面”)。
- 线性模型与线性特征
线性模型在低维空间中可能非常受限,因为线和平面的灵活性有限。有一种方法可以让线性模型更加灵活,就是添加更多的特征——举个例子,添加输入特征的交互项或多项式。
X, y = make_blobs(centers=4, random_state=8)
y = y % 2
# 第一个参数是x值 第二个参数是y值 第三个参数是标签
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
plt.show()
plt.clf()
上述代码显示特征0与特征1的一种类别分布,显然在这个二维平面中很难通过一条直线进行类别的划分。
现在我们对输入特征进行扩展,比如说添加第二个特征的平方(feature1 ** 2)作为一个新特征。现在我们将每个数据点表示为三维点(feature0,feature1,feature1 **2),而不是二维点(feature0,feature1)。然后在数据的新表示中,用线性模型(三维空间中的平面)将这两个类别分开。
代码如下:
X, y = make_blobs(centers=4, random_state=8)
y = y % 2
# hstack: 在数组的横向(行)方向上拼接两个或更多的数组, 也就是在原来的基础上增加列
# 选取数组X的所有行(':'),以及从第二列开始的所有列(1:)
X_new = np.hstack([X, X[:, 1:] ** 2])
# 显示线性决策边界
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# 设置仰角和方位角
ax.view_init(elev=-152, azim=-26)
linear_svm_3d = LinearSVC().fit(X_new, y)
# 从训练好的SVM模型中获取系数和截距。'coef'是系数,'intercept'是截距
coef, intercept = linear_svm_3d.coef_.ravel(), linear_svm_3d.intercept_
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)
# "使用meshgrid函数根据上面生成的x和y坐标数组生成网格坐标。这将产生一个二维网格,每个网格点都有一对(x, y)坐标。"
XX, YY = np.meshgrid(xx, yy)
# 根据系数和截距计算出每个网格点的z坐标
ZZ = (coef[0] * XX + coef[1] * YY + intercept) / -coef[2]
# 使用plot_surface方法绘制3D曲面图。参数rstride和cstride分别控制了网格在x和y方向上的采样率。alpha参数控制了曲面的透明度。
ax.plot_surface(XX, YY, ZZ, rstride=8, cstride=8, alpha=0.3)
# 创建一个掩码,用于标记y值为0的数据点。
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)
ax.set_xlabel('feature0')
ax.set_ylabel('feature1')
ax.set_zlabel('feature1 **2')
plt.show()
- 理解SVM
在训练过程中,SVM学习每个训练数据点对于表示两个类别之间的决策边界的重要性。但是通常真正重要的训练点是位于类别之间边界上的那些点(支持向量)。要对新样本进行预测,需要测量它与每个支持向量之间的距离。分类决策是基于它与支持向量之间的距离以及在训练过程中学到的支持向量重要性(保存在SVC的dual_coef中)来做出 的。数据点之间的距离由高斯核给出:
Krbf(x1,x2)=exp(-γ ||x1-x2||^2)
||x1-x2||表示欧式距离,γ(gamma)是控制高斯核宽度的函数。下面举个例子看一个svm是如何分类的,在这个例子中,SVM给出了非常平滑且非线性(不是直线)的边界
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)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
# 支持向量
sv = svm.support_vectors_
# 支持向量的类别标签由dual_coef_的正负号给出
sv_labels = svm.dual_coef_.ravel() > 0
mglearn.discrete_scatter(sv[:, 0], sv[:, 1], sv_labels, s=15, c='g', markeredgewidth=3)
plt.show()
我们调节两个参数:C参数和gamma参数,看一下上述分类的效果:
fig, axes = plt.subplots(3, 3, figsize=(15, 10))
for ax, C in zip(axes, [-1, 0, 3]):
# gamma越小,高斯核的半径越大,决策边界越平滑。
# 大的gamma值则会生成更为复杂的模型,决策边界更关注单个点。
for a, gamma in zip(ax, range(-1, 2)):
# plot_svm函数会取数据,并进行点的展示。函数内部会将C与gamma分别作为10的指数,图片会展示出来参数
mglearn.plots.plot_svm(log_C=C, log_gamma=gamma, ax=a)
axes[0, 0].legend(["class 0", "class 1", "sv class 0", "sv class 1"], ncol=4, loc=(.9, 1.2))
plt.xlabel("Feature 0")
plt.ylabel("Feature 1")
plt.show()
从左到右,我们将参数gamma的值从0.1增加到10。gamma较小,说明高斯核的半径较大,许多点都被看作比较靠近。这一点可以在图中看出:左侧的图决策边界非常平滑,越向右的图决策边界更关注单个点。小的gamma值表示决策边界变化很慢,生成的是复杂度较低的模型,而大的gamma值则会生成更为复杂的模型。
从上到下,我们将参数C的值从0.1增加到1000。与线性模型相同,C值很小,说明模型非常受限,每个数据点的影响范围都有限。你可以看到,左上角的图中,决策边界看起来几乎是线性的,误分类的点对边界几乎没有任何影响。再看左下角的图,增大C之后这些点对模型的影响变大,使得决策边界发生弯曲来将这些点正确分类。
- SVM数据缩放
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target,
random_state=0)
svc = SVC(gamma="auto")
svc.fit(X_train, y_train)
print("Accuracy on training set:{:.2f}".format(svc.score(X_train, y_train)))
print("Accuracy on test set:{:.2f}".format(svc.score(X_train, y_train)))
Accuracy on training set:1.00 Accuracy on test set:0.63
这个模型在训练集上的分数十分完美,但在测试集上的精度只有63%,存在相当严重的过拟合。虽然SVM的表现通常都很好,但它对参数的设定和数据的缩放非常敏感。我们来看一下每个特征的最小值和最大值,它们绘制在对数坐标上:
plt.plot(X_train.min(axis=0), 'o', label='min')
plt.plot(X_train.max(axis=0), '^', label='max')
plt.legend(loc=4)
plt.xlabel("Feature index")
plt.ylabel("Feature magnitude")
plt.yscale("log")
plt.show()
从这张图中,我们可以确定数据集的特征具有完全不同的数量级。这对其他模型来说(比如线性模型)可能是小问题,但对核SVM却有极大影响。为了解决这个问题,一种方法就是对每个特征进行缩放,使其大致都位于同一范围。核SVM常用的缩放方法就是将所有特征缩放到0和1之间。
min_on_training = X_train.min(axis=0)
range_on_training = (X_train - min_on_training).max(axis=0)
X_train_scaled = (X_train - min_on_training) / range_on_training
print("Minimum for each feature\n{}".format(X_train_scaled.min(axis=0)))
print("Maximum for each feature\n{}".format(X_train_scaled.max(axis=0)))
svc = SVC(gamma="auto")
svc.fit(X_train_scaled, y_train)
print("Accuracy on training set:{:.3f}".format(svc.score(X_train_scaled, y_train)))
X_test_scaled = (X_test - min_on_training) / range_on_training
print("Accuracy on test set:{:.3f}".format(svc.score(X_test_scaled, y_test)))
Minimum for each feature [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.] Maximum for each feature [1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.] Accuracy on training set:0.948 Accuracy on test set:0.951
从上述程序的执行结果来看,数据缩放的作用很大!实际上模型现在处于欠拟合的状态,因为训练集和测试集的性能非常接近,但还没有接近100%的精度。gamma和C控制的都是模型复杂度,较大的值都对应更为复杂的模型,我们可以尝试增大C或gamma来拟合更为复杂的模型。