鲁智深
2022/12/05阅读:145主题:自定义主题1
R 机器学习 朴素贝叶斯和SVM
前言
在 《Machine Learning with R, tidyverse, and mlr》
一书的第六章—— 朴素贝叶斯(Naive Bayes)和支持向量机(Support Vector Machines,SVM)中,上期文章小编主要学习了关于朴素贝叶斯的基本内容,主要涉及 Baye’s rule
。本期小编将继续学习支持向量机(SVM),SVM 是机器学习中经常出现的一个基本算法,在凸优化课程中常被用作讲解的例子。在本书中,主要介绍基于 mlr
包的 SVM 算法的使用方法。

假设你想预测你的老板是否心情愉悦(二元分类),在接下来的几周内,你将记录你在办公桌上玩游戏的时间、为公司赚的钱以及第二天老板的心情是否愉悦。你将使用 SVM 算法构建一个分类器来帮助你决定在某一天你是否需要避开你的老板。
SVM 算法将学习一个线性超平面,将你的老板心情好的日子和心情不好的日子分开,并在数据中增加一个额外的维数来找到最佳超平面。
1. SVM 简介
1.1 线性可分数据的 SVM

Fig 1 所示是基于你在办公桌玩游戏的时间和为公司赚的钱得到的你老板心情好坏的结果。SVM 算法找到一个最优的线性超平面来分离类。对于类是完全线性可分的问题,可能有许多不同的超平面,它们在分离训练数据中的类方面做得一样好。
-
最优超平面是使其周围的边界(
margin
)最大化的超平面(可推广到更多未知的数据)。 -
边界是超平面周围接触最少实例的距离。
-
数据中接触边界的实例为支持向量(
support vector
)。
超平面是比数据中变量的维数少一个的曲面。对于二维特征空间(如 Fig 1 中的示例),超平面只是一条直线。对于三维特征空间,超平面是一个曲面。很难在四维或更多维的特征空间中描绘超平面,但原理是相同的:它们是穿过特征空间的表面。

支持向量是训练集中最重要的实例,因为它们定义了类之间的边界。不仅如此,算法学习的超平面完全依赖于支持向量的位置,而不是训练集中的其他实例。 如 Fig 2 所示,如果我们移动其中一个支持向量的位置,那么超平面的位置也会有所移动。然而,如果移动了一个非支持向量的实例,就不会对超平面产生任何影响。
1.2 非完全可分类的 SVM
如上所述中,类是完全可分的,这样就可以清楚地展示如何选择超平面的位置来最大化边界。但是如果这些类不能完全可分呢?在没有边界的情况下,算法如何找到一个超平面呢?
支持向量机的原始公式通常使用硬边界(hard margin
)。如果 SVM 使用硬边界,那么任何实例都不允许落在边界之内。这意味着,如果类不是完全可分的,那么算法将失效。这就使得硬边界 SVM 只能处理简单的分类问题。因此,SVM 的一个拓展为 软边界 SVM(soft-margin SVM
),在软边界 SVM 中,算法仍然学习到最好地分离类的超平面,但允许实例落在其边界内。
软边界 SVM 算法仍然试图找到最好地分离类的超平面,但如果实例在其边界内,则会受到惩罚。在边界内的实例的惩罚有多严重是由控制边界“硬”或“软”的超参数控制的。边界越硬,在边界内的实例就越少,超平面依赖的支持向量就越少。边界越软,其内部的实例就越多,超平面依赖于更多的支持向量。如果我们的边界太硬,可能会过度拟合决策边界附近的噪声,而如果我们的边界太软,可能会欠拟合数据,并学习到一个分离类效果不好的决策边界。
1.3 非线性可分数据的 SVM
SVM 算法强大的原因之一是它可以为数据添加额外的维度,找到一种线性的方法来分离非线性数据。

Fig 3 中使用两个预测变量,类是不可线性分离的。SVM 算法为数据增加了额外的维度,这样一个线性超平面就可以在这个新的高维空间中分离类,我们可以把它想象成特征空间的变形或拉伸。这个额外的维度被称为核(kernel
)。
算法使用一种称为核函数(kernel function
)的数据数学转换找到这个新核。有许多核函数可供选择,每个核函数对数据应用不同的转换,适合于在不同情况下寻找线性决策边界。Fig 4 展示了一些常见的核函数可以分离非线性可分数据的例子,其中包括:
-
linear kernel (相当于没有核) -
polynomial kernel -
Gaussian radial basis kernel -
sigmoid kernel

对于一个给定的问题,核函数的类型不是从数据中学到的,而是要事先指定。所以,核函数的选择是一个类别超参数(categorical hyperparameter
),因此,选择性能最好的核函数的最佳方法是使用超参数调优。
1.4 SVM 算法的超参数
当构建一个支持向量机时,我们需要调优很多超参数,再加上训练单个模型的成本可能比较高,这使得训练一个性能最佳的 SVM 需要相当长的时间(可使用并行运算,后面有提到)。
SVM 算法有相当多的超参数需要调优,但需要考虑的最重要的是:
-
核(Fig 4) -
度超参数( degree hyperparameter
),控制多项式核的决策边界的“弯曲度”,多项式的次数越高,可以学习到的决策边界就越弯曲和复杂,但这有可能对训练集造成过拟合(Fig 4) -
cost 或 C 超参数( cost or C hyperparameter
),控制边界的“硬”或“软”程度(Fig 5) -
伽马超参数( gamma hyperparameter
),它控制实例对决策边界位置的影响程度(Fig 5)
低 cost 告诉算法,在边界内有更多的实例是可以接受的,并将导致更宽的边界不太受类边界附近的局部差异的影响。高 cost 对在边界内的实例施加了更严厉的惩罚,并将导致更窄的边界,更容易受到类边界附近的局部差异的影响。
伽马超参数控制每个实例对超平面位置的影响,除线性核外,所有核函数都使用这个超参数。伽玛值越大,每个实例就越引人注意,决策边界的颗粒度越大(可能导致过拟合),伽玛值越小,每个实例就越不引人注意,决策边界的颗粒度越小(可能导致欠拟合)。

1.5 多分类 SVM
到目前为止,以上所述都是两个类的情况,是因为 SVM 算法天生就倾向于分离两个类。但是,当有两个以上的类时,不是创建单个 SVM,而是创建多个模型,并让它们进行竞争以预测新数据最可能的类,有两种方式:
-
one versus all -
one versus one
在 one versus all
中,将根据类的数量创建多个 SVM 模型。每个 SVM 模型都描述了一个最好地将一个类从所有类中分离出来的超平面。
在 one versus one
中,为每一对类创建一个 SVM 模型。每个 SVM 模型描述一个超平面,该超平面最好地将一个类与另一个类分开,忽略数据中其他类。

one versus all
和 one versus one
实现多分类 SVM
实际上,这两种方法的性能差别不大。尽管训练了更多的模型(超过三个类),但 one versus one
的计算成本有时比 one versus all
的计算成本要低。这是因为,尽管我们训练的模型更多,但训练集更小(因为被忽略的情况)。mlr 调用的 SVM 算法的实现采用了 one versus one
的方法。
如果在预测一个新实例时没有明确的获胜类别,可使用一种叫做 Platt Scaling
的技术。该技术获取实例与每个超平面的距离,并使用逻辑函数将其转换为概率。过程如下(Fig 7):
-
对于每一个超平面(无论是我们使用 one versus one
还是one versus all
):
-
测量每个实例与超平面的距离, -
使用 logistic 函数将这些距离转换为概率;
-
将新数据分类为具有最高的概率的超平面的类。

Platt Scaling
来获得对于每个超平面的概率
当我们对新的未知数据进行分类时,使用三个“s”形曲线中的每一个将新数据和超平面的距离转换为概率,最高的概率即所属的类。
2.建立 SVM 模型
本节将学习如何建立 SVM 模型并同时调优多个超参数。
假设你对垃圾邮件十分反感,因为这一定程度上会影响工作效率,所以你决定对几个月来收到的邮件进行特征提取,这些特征包括感叹号的数量和某些单词的使用频率等。根据这些数据(spam dataset
),我们便可以做一个 SVM 分类器,可以用做垃圾邮件过滤器。
加载包:
library(mlr)
library(tidyverse)
2.1 加载和探索 spam 数据集
该数据集位于 kernlab
包中,加载数据集并将其转化为 tibble
形式。
data(spam, package = "kernlab")
spamTib <- as_tibble(spam)
spamTib
# A tibble: 4,601 × 58
# make address all num3d our over remove internet order mail receive
# <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
# 1 0 0.64 0.64 0 0.32 0 0 0 0 0 0
# 2 0.21 0.28 0.5 0 0.14 0.28 0.21 0.07 0 0.94 0.21
# 3 0.06 0 0.71 0 1.23 0.19 0.19 0.12 0.64 0.25 0.38
# 4 0 0 0 0 0.63 0 0.31 0.63 0.31 0.63 0.31
# 5 0 0 0 0 0.63 0 0.31 0.63 0.31 0.63 0.31
# 6 0 0 0 0 1.85 0 0 1.85 0 0 0
# 7 0 0 0 0 1.92 0 0 0 0 0.64 0.96
# 8 0 0 0 0 1.88 0 0 1.88 0 0 0
# 9 0.15 0 0.46 0 0.61 0 0.3 0 0.92 0.76 0.76
# 10 0.06 0.12 0.77 0 0.19 0.32 0.38 0 0.06 0 0
# … with 4,591 more rows, and 47 more variables: will <dbl>, people <dbl>,
# report <dbl>, addresses <dbl>, free <dbl>, business <dbl>, email <dbl>,
# you <dbl>, credit <dbl>, your <dbl>, font <dbl>, num000 <dbl>, money <dbl>,
# hp <dbl>, hpl <dbl>, george <dbl>, num650 <dbl>, lab <dbl>, labs <dbl>,
# telnet <dbl>, num857 <dbl>, data <dbl>, num415 <dbl>, num85 <dbl>,
# technology <dbl>, num1999 <dbl>, parts <dbl>, pm <dbl>, direct <dbl>,
# cs <dbl>, meeting <dbl>, original <dbl>, project <dbl>, re <dbl>, …
该数据集有 4601 封电子邮件和从电子邮件中提取的 58 个变量。我们的目标是训练一个模型,它可以使用这些变量中的信息来预测一个新电子邮件是否是垃圾邮件。
除了表示邮件是否为垃圾邮件的 type
为因子类型外,所有的变量都是连续的,因为 SVM 算法无法处理分类预测。
2.2 调优超参数
定义任务和学习者,将 classif.svm
作为 makeLearner()
的参数,以指定将使用 SVM。
spamTask <- makeClassifTask(data = spamTib, target = "type")# 定义任务
svm <- makeLearner("classif.svm")#定义学习者
2.2.1 函数 getParamSet()
在训练模型之前,需要调整我们的超参数。要找出哪些超参数可用于对算法进行调优,只需使用函数 getParamSet()
。
getParamSet("classif.svm")
# Type Def Constr Req Tunable
#cost numeric 1 0 to Inf Y TRUE
#kernel discrete radial [lin,poly,rad,sig] - TRUE
#degree integer 3 1 to Inf Y TRUE
#gamma numeric - 0 to Inf Y TRUE
#scale logicalvector TRUE - - TRUE
上面的输出结果中做了简化,只保留了较为重要的几列:
-
行名是超参数的名称; -
Type
: 该超参数的类型:numeric, integer, discrete or logical values; -
Def
: 该超参数默认值; -
Constr
: 定义该超参数的约束,可以是一组特定值,也可以是可接受值的范围; -
Req
: 定义学习者是否需要该超参数; -
Tunable
: 逻辑值,定义是否可以对该超参数进行调优。
需要调优的最重要的超参数是: the kernel,the cost,the degree,the gamma
。
2.2.2 函数 makeParamSet()
使用函数 makeParamSet()
来定义我们希望调优的超参数空间。对于 makeParamSet()
函数,提供定义希望调优的每个超参数所需的信息,以列分隔。
-
kernel
超参数接受离散值(核函数的名称),因此使用makeDiscreteParam()
函数将其值定义为我们创建的核函数向量; -
degree
超参数接受整数值,因此使用makeIntegerParam()
函数并定义希望调优的上下界; -
cost
和gamma
超参数接受任何numeric
,因此使用makeNumericParam()
函数并定义希望调优的上下界。
kernels <- c("polynomial", "radial", "sigmoid")#创建核函数向量
svmParamSpace <- makeParamSet(#定义超参数空间
makeDiscreteParam("kernel", values = kernels),
makeIntegerParam("degree", lower = 1, upper = 3),
makeNumericParam("cost", lower = 0.1, upper = 10),
makeNumericParam("gamma", lower = 0.1, 10))
2.2.3 随机搜索 (Random Search)
之前在对 KNN 算法调优的时候,使用了网格搜索算法(Grid Search
):尝试定义的超参数空间的每个组合,并找到性能最佳的组合。只要指定一个合理的超参数空间进行搜索,网格搜索就总能找到性能最佳的超参数。但是对于上文我们定义的 SVM 超参数空间来说,假如选择步长 0.1,那么cost
和 gamma
超参数将有 100 个值,另外两个超参数各有 3 个,要在此参数空间上执行网格搜索,需要对模型进行 90,000 次训练,会大大增加时间和计算成本。
因此,我们可以使用随机搜索的方式,其主要过程如下:
-
随机选择超参数值的组合; -
使用交叉验证来训练和评估使用这些超参数值的模型; -
记录模型的性能指标(通常是分类任务的平均错分误差); -
尽可能多地重复步骤 1 到 3; -
选择提供最佳性能模型的超参数值组合。
与网格搜索不同,随机搜索不能保证找到超参数值的最佳集合。然而,通过足够的迭代,通常可以找到性能良好的超参数组合。
接下来,使用函数 makeTuneControlRandom()
来定义随机搜索,用 maxit
参数告诉函数随机搜索过程的迭代次数。事实上,应尝试将 maxit
设置为计算预算所允许的最高值。
randSearch <- makeTuneControlRandom(maxit = 20)#随机搜索,迭代次数为20
cvForTuning <- makeResampleDesc("Holdout", split = 2/3)#hold-out 交叉验证
2.2.4 并行运算
为了并行运算该调优过程,减少代码运行时间,将调优代码放入 parallelMap
包中函数 parallelStartSocket()
和 parallelStop()
之间。并调用函数 tuneParams()
调优:
library(parallelMap)
library(parallel)
parallelStartSocket(cpus = detectCores())
tunedSvmPars <- tuneParams("classif.svm", #学习者
task = spamTask,#任务
resampling = cvForTuning,#交叉验证
par.set = svmParamSpace,#超参数空间
control = randSearch)#搜索方式
parallelStop()
输出调优结果值:
tunedSvmPars
#Tune result:
#Op. pars: kernel=polynomial; degree=1; cost=8.11; gamma=6.82
#mmce.test.mean=0.0730117
或:
tunedSvmPars$x
#$kernel
#[1] "polynomial"
#$degree
#[1] 1
#$cost
#[1] 8.114902
#$gamma
#[1] 6.815739
可以看到,一阶多项式核函数(等价于线性核函数), cost
为 8.11,gamma
为 6.82,给出了性能最好的模型。
2.3 用调优的超参数训练模型
使用函数 setHyperPars()
可以将学习者与一组预定义的超参数值组合在一起。第一个参数是想要使用的学习者,parw .vals
参数是包含调优的超参数值的对象。
tunedSvm <- setHyperPars(makeLearner("classif.svm"),
par.vals = tunedSvmPars$x)
tunedSvmModel <- train(tunedSvm, spamTask)#训练模型
3. 交叉验证 SVM 模型
上文中已经使用调优的超参数构建了模型。在本节中,我们将交叉验证所建模型,以估计模型在新的未知数据上的性能。
交叉验证整个模型构建过程是很重要的,这意味着在模型构建过程中任何依赖数据的步骤(例如超参数调优)都需要包含在交叉验证中。
要在交叉验证中包含超参数调优,我们需要使用 wrapper
函数,它将学习者和超参数调优过程打包在一起。
由于 mlr
将使用嵌套交叉验证(在内部循环中执行超参数调优,并将成功的值组合传递给外部循环),因此首先使用函数 makeResamplDesc()
定义外部交叉验证策略。在本例中,外部循环选择了 3-fold 交叉验证。对于内部循环,使用之前定义的 cvForTuning
。
接下来,使用函数 makeTuneWrapper()
来封装学习者。
outer <- makeResampleDesc("CV", iters = 3)#定义外部交叉验证
svmWrapper <- makeTuneWrapper("classif.svm", #定义 wrapped learner
resampling = cvForTuning,
par.set = svmParamSpace,
control = randSearch)
parallelStartSocket(cpus = detectCores())
cvWithTuning <- resample(svmWrapper, spamTask, resampling = outer)#运行嵌套交叉验证
parallelStop()
输出交叉验证结果:
cvWithTuning
#Resample Result
#Task: spamTib
#Learner: classif.svm.tuned
#Aggr perf: mmce.test.mean=0.0688973
#Runtime: 26.9628
可以看到,模型正确地将 1 - 0.069 = 0.931 = 93.1% 的电子邮件划分为垃圾邮件或非垃圾邮件。
小编有话说
本期关于 SVM 算法涉及的内容较多,包括多个超参数调优、随机搜索和并行运算等内容,小编已经收获满满,不知道读者们呢?下期,小编将学习 《Machine Learning with R, tidyverse, and mlr》
一书的第七章:新的分类算法——决策树,敬请期待!
作者介绍