跳至主要内容

R 中的支持向量机

本教程将帮助您从高层次理解 SVM 的工作原理,并使用 R 来实现。
· 12分钟 阅读

使用 AI 探索

在机器学习中,支持向量机是一类带有相关学习算法的监督学习模型,用于分析数据并进行分类与回归。然而,它们主要用于分类问题。本教程将从高层次理解 SVM 的工作原理,然后使用 R 来实现。我将更注重直觉而非形式化推导。也就是说,我们会尽量跳过数学推导,转而建立对其工作原理的直观认知。

支持向量机算法

线性数据

通过一个简单示例最容易理解支持向量机的基础与工作方式。设想我们有两个标签:red 和 blue,数据包含两个特征:x 和 y。我们希望有一个分类器,给定一组 (x,y) 坐标,就能判断它是 red 还是 blue。将已带标签的训练数据绘制在平面上:

graph

支持向量机会基于这些数据点输出能够最好地区分两类标签的超平面(在二维中就是一条直线)。这条线就是决策边界:落在它一侧的我们分类为 blue,另一侧则为 red。

hyperplane

但什么才是最好的超平面?对 SVM 来说,是能够最大化两类标签间隔的那一个。换句话说:到每个标签中最近样本点的距离最大的超平面(在此情形下是一条直线)。

hyperplane

非线性数据

上面的示例很容易,因为数据显然是线性可分的——我们可以画一条直线将 red 和 blue 分开。但现实中往往没那么简单。看看这个例子:

Non-Linear Data

很明显不存在线性的决策边界(单条直线来分隔两类)。不过,向量显然被很好地分隔开了,看起来应该不难将它们区分。

我们的做法是:增加第三个维度。到目前为止,我们有两个维度:$x$ 和 $y$。我们创建一个新的 z 维度,并约定用一种对我们有利的方式来计算它:$z = x² + y²$(您会注意到这就是圆的方程)。

这会得到一个三维空间。对该空间取一个切片,看起来如下:

Non-Linear Data

SVM 能在这里做什么?我们来看看:

Non-Linear Data

太好了!注意既然我们现在处于三维空间,超平面就是与 $x$ 轴平行、位于某个 $z$(比如 $z = 1$)处的一个平面。

剩下的就是把它映射回二维:

Non-Linear Data

搞定!我们的决策边界是半径为 1 的圆,它用 SVM 将两类标签分开。

核技巧

在上述示例中,我们通过巧妙地将空间映射到更高维来对非线性数据进行分类。然而,计算这种变换可能会非常耗费计算资源:可能会有很多新维度,每个维度都涉及复杂计算。要对数据集中每个向量都进行这样的操作工作量很大,因此如果能找到更省事的办法就更好了。

诀窍在这里:SVM 并不需要实际的向量来发挥魔力,它只需要它们之间的点积即可。这意味着我们可以绕过新维度的昂贵计算!我们可以这样做:

  • 设想我们想要的新空间:

equation

  • 弄清楚在该空间中的点积长什么样:

equation

  • 让 SVM 照常运行,但使用这个新的点积——我们把它称为核函数。

这就是著名的核技巧,通过扩展特征空间来容纳类别之间的非线性边界。用于分隔非线性数据的常见核类型包括多项式核、径向基核和线性核(与支持向量分类器相同)。简单来说,这些核将数据变换为能够通过线性超平面的空间,从而实现分类。

优缺点

我们来看一下 SVM 的一些优缺点:

优点

  • 高维表现:SVM 在高维空间中依然有效,尤其适用于文档分类与情感分析等维度极大的场景。

  • 内存高效:由于在实际进行新样本分类时只使用训练集中一小部分支持向量,决策时只需存储并计算这些点。

  • 灵活多变:类别分隔往往高度非线性。能够应用不同核函数为决策边界提供了极大灵活性,从而提升分类表现。

缺点

  • 核参数选择:SVM 对核参数的选择非常敏感。在每个对象的特征数超过训练样本数量的情况下,SVM 可能表现不佳。直观地看,如果高维特征空间远大于样本数,那么可用于支撑最优线性超平面的有效支持向量更少,随着新样本加入,分类性能会变差。

  • 非概率化:分类器通过将样本置于分类超平面的两侧来工作,因此对类别归属没有直接的概率解释。不过,一个可用的衡量指标是新点距离决策边界的远近,以此评估分类的“把握”。

R 中的支持向量机

线性 SVM 分类器

我们先在二维中生成一些数据,并让两类之间稍微分开一些。设置随机种子后,创建一个矩阵 x,在两个变量上各有 20 个观测、两类、服从正态分布。然后创建变量 y,取值为 -1 或 1,每类各 10 个。当 y = 1 时,将两个坐标上的均值从 0 移到 1。最后绘制数据,并根据响应对点进行着色。点型 19 会生成清晰可见的大圆点,按响应是 1 还是 -1 分别编码为蓝色或红色。

set.seed(10111)
x = matrix(rnorm(40), 20, 2)
y = rep(c(-1, 1), c(10, 10))
x[y == 1,] = x[y == 1,] + 1
plot(x, col = y + 3, pch = 19)

现在加载包含 svm 函数的 e1071 包(如果尚未安装请先安装)。

library(e1071)

接着您将创建一个数据框,并将 y 转为因子变量。然后在该数据框上调用 svm,将 y 作为响应变量,其他变量作为自变量。数据框会把矩阵 x 拆成两列,命名为 x1 和 x2。我们告知 SVM 使用线性核,调参中的 cost 为 10,且 scale 为 false。在此示例中,我们不对变量进行标准化。

dat = data.frame(x, y = as.factor(y))
svmfit = svm(y ~ ., data = dat, kernel = "linear", cost = 10, scale = FALSE)
print(svmfit)

打印 svmfit 会得到其摘要。可以看到支持向量的数量为 6——它们是靠近边界或在边界错误一侧的点。

SVM 提供了绘图函数来展示决策边界,如下所示。颜色似乎不太可控。它也有些反常规,因为它将 x2 放在横轴、x1 放在纵轴。

plot(svmfit, dat)
SVM

我们来试着自己绘图。首先要创建覆盖整个定义域的 x1 和 x2 的网格(晶格)。为此,编写一个名为 make.grid 的函数。它接收数据矩阵 x,以及参数 n,表示每个方向上的取点数。这里我们使用 75 x 75 的网格。

在该函数中,使用apply 函数获取 x 中各变量的取值范围。然后对 x1 和 x2 分别用 seq 函数从最小值到最大值生成长度为 n 的序列。此时,x1 与 x2 各有 75 个在各自坐标轴上均匀分布的取值。最后使用 expand.grid 函数将 x1 与 x2 组合成网格。

make.grid = function(x, n = 75) {
  grange = apply(x, 2, range)
  x1 = seq(from = grange[1,1], to = grange[2,1], length = n)
  x2 = seq(from = grange[1,2], to = grange[2,2], length = n)
  expand.grid(X1 = x1, X2 = x2)
}

现在可以对 x 应用 make.grid 函数。我们查看网格中前 1 到 10 行的取值。

xgrid = make.grid(x)
xgrid[1:10,]

可以看到,网格优先遍历第一个坐标,同时固定第二个坐标。

有了网格后,我们将对网格中每个点进行预测。使用新数据 xgrid 调用 predict,将响应命名为 ygrid。随后绘制并根据分类结果给点着色,使决策边界清晰。我们也把原始点通过 points 函数加到图上。

svmfit 有一个名为 index 的组件,它指明哪些点是支持向量。再次使用 points 将它们也标注出来。

ygrid = predict(svmfit, xgrid)
plot(xgrid, col = c("red","blue")[as.numeric(ygrid)], pch = 20, cex = .2)
points(x, col = y + 3, pch = 19)
points(x[svmfit$index,], pch = 5, cex = 2)
plot

如图所示,被方框圈出的点靠近决策边界,并在确定该边界时起到关键作用。

遗憾的是,svm 函数并不算太“友好”,您需要做些工作才能取回线性系数。这大概是因为这只对线性核有意义,而该函数更通用。因此我们用一个公式来更高效地提取系数。我们提取线性系数 beta 和 beta0。

beta = drop(t(svmfit$coefs)%*%x[svmfit$index,])
beta0 = svmfit$rho

现在可以在网格上重新绘图,然后把原始点(包括支持向量)加回去。接着使用这些系数按如下简单方程绘制决策边界:

根据该方程,计算出决策边界的斜率和截距。然后用 abline 函数并传入这两个参数。随后的两次 abline 分别表示决策边界的上、下边距。

plot(xgrid, col = c("red", "blue")[as.numeric(ygrid)], pch = 20, cex = .2)
points(x, col = y + 3, pch = 19)
points(x[svmfit$index,], pch = 5, cex = 2)
abline(beta0 / beta[2], -beta[1] / beta[2])
abline((beta0 - 1) / beta[2], -beta[1] / beta[2], lty = 2)
abline((beta0 + 1) / beta[2], -beta[1] / beta[2], lty = 2)
plot

可以清楚地看到,一些支持向量恰好位于边距上,而另一些位于边距内。

非线性 SVM 分类器

上一节介绍了线性 SVM。现在转向 SVM 的非线性版本。我们来看《统计学习要素》中的一个示例:在二维空间中,决策边界是非线性的。我们将使用带核的支持向量机来学习该边界。

首先,从这个 URL 直接下载教材示例所用的数据。数据是混合并模拟生成的。然后查看其列名。

load(file = "ESL.mixture.rda")
names(ESL.mixture)

目前,训练数据为 x 和 y。我们之前已经创建过 x 和 y,因此先删除它们,以便加载这份新数据。

rm(x, y)
attach(ESL.mixture)

这些数据同样是二维的。我们先画出来看看。

plot(x, col = y + 1)
Non-Linear SVM Classifier

数据似乎有不少重叠,但也能看出其结构的特殊性。现在我们创建一个以 y 为响应的数据框,并将其转为因子。随后拟合一个采用径向核、cost 为 5 的 SVM。

dat = data.frame(y = factor(y), x)
fit = svm(factor(y) ~ ., data = dat, scale = FALSE, kernel = "radial", cost = 5)

接下来创建网格并进行预测。该数据实际上自带网格点。如果查看列表中的名称摘要,会发现有两个变量 px1 和 px2,分别是每个变量的网格取值。我们用 expand.grid 创建取值网格,然后在网格上的每个取值点进行分类预测。

xgrid = expand.grid(X1 = px1, X2 = px2)
ygrid = predict(fit, xgrid)

最后,我们根据决策边界为点着色并绘制。可以看到决策边界是非线性的。我们也把数据点画在图中,看看它们位于何处。

plot(xgrid, col = as.numeric(ygrid), pch = 20, cex = .2)
points(x, col = y + 1, pch = 19)
Non-Linear SVM Classifier

总体上,决策边界很好地追随了数据的分布,但方式是高度非线性的。

我们看看是否还能进一步改进该图:让预测函数在每个网格点产生实际的函数估计。特别地,我们希望用 contour 函数绘制出给出决策边界的等高线。数据框中还有一个名为 prob 的变量,它是在网格点上数据属于类别 1 的真实概率。如果绘制其 0.5 等高线,就得到贝叶斯决策边界,即理论上能达到的最佳边界。

首先,在网格上对拟合结果进行预测。将 decision values 设为 TRUE,因为我们需要实际的函数值而不仅是分类结果。它会作为属性返回实际分类值,我们需要提取名为 decision 的那个属性。

接着,按前面的步骤创建网格、进行预测并绘制点。

然后使用 contour 函数。它需要两个网格序列、一个函数,以及 level 和 add 两个参数。我们需要将函数组织成一个矩阵,尺寸为 px1 和 px2 的长度(分别为 69 和 99)。设置 level 为 0 并将其添加到图中。这样就能看到该等高线追随决策边界,这是在二维中绘制非线性决策边界的便捷方式。

最后,加上真实边界,即概率的等高线。该等高线的水平为 0.5,这就是以概率意义定义的决策边界(也称为贝叶斯决策边界)。

func = predict(fit, xgrid, decision.values = TRUE)
func = attributes(func)$decision

xgrid = expand.grid(X1 = px1, X2 = px2)
ygrid = predict(fit, xgrid)
plot(xgrid, col = as.numeric(ygrid), pch = 20, cex = .2)
points(x, col = y + 1, pch = 19)

contour(px1, px2, matrix(func, 69, 99), level = 0, add = TRUE)
contour(px1, px2, matrix(func, 69, 99), level = 0.5, add = TRUE, col = "blue", lwd = 2)
Non-Linear SVM Classifier

结果表明,您的非线性 SVM 已经非常接近贝叶斯决策边界。

结论

回顾一下,支持向量机是一类监督式分类器,目标是将特征空间划分为两个或更多组。其方法是基于已知类别标签,找到能够最佳分隔这些组的方式:

  • 在较简单的情况下,分隔“边界”是线性的,在高维空间中表现为用直线(或平面)将各组分开。
  • 在更复杂的情况下(用直线或平面无法很好分隔各组),SVM 能够进行非线性划分,这是通过核函数实现的。
  • 最终,这使得 SVM 成为非常精细而强大的分类器,但通常的代价是容易发生过拟合。

我个人认为,当各组分隔明显时,SVM 是非常出色的分类器;当数据是非线性可分时,它也同样表现优异。您可以自行变换数据以实现线性可分,或者让 SVM 直接将数据变换并线性地分隔两类——这正是使用 SVM 的主要理由之一:您无需亲自对非线性数据做变换。SVM 的一个不足在于其“黑箱”属性。使用核来分隔非线性数据使其难以(甚至不可能)解释。理解 SVM 将为您的分类提供一种替代于 GLM 和决策树的方法。希望本教程能让您对 SVM 的全貌有更广阔的认识,并更好地理解这种方法。

如果您想进一步学习 R,请参加 DataCamp 的机器学习工具箱课程。

欢迎查看我们的使用 Scikit-learn 的支持向量机教程。

主题