机器学习(六)——支持向量机

一、支持向量机的一般原理

支持向量机跟逻辑回归比较像。可以说,支持向量机是逻辑回归的一种优化或者扩展。因此,虽然说支持向量机既可以处理分类问题,也可以处理回归问题,但是它一般还是主要用于分类问题。而且其本质是处理二元分类问题。
支持向量机的理念也非常简单,从它的别名“大间距分类器”我们也能窥知一二。简单点说,支持向量机就是找到这么一个(决策)边界(或者理解成分界线,专业点说,叫超平面),既能将两个分类进行完美的区分,又能使得该边界距离两个分类之间的间距达到最大。(因为只有间距最大,才更具有鲁棒性,分类器的效果才最好)。


那么如何找到这个“超平面”呢?

现在我们先回到逻辑回归。在逻辑回归中,因为数据要分成两类,我们是用0,1来进行的两类区分。而且为了最后使得的值能落在0,1之间,我们引入了逻辑函数,并且给g(z)赋予了一个意义,即他代表了分类结果为1时的概率。函数值越靠近1,代表分类结果为1的可能性越大。函数值越靠近0,代表分类结果为0的可能性越大。此外,只和有关,,那么,而g(z)只是用来映射,真实的类别决定权还是在于。再者,当时,,反之。如果我们只从出发,希望模型达到的目标就是让训练数据中y=1的特征,而是y=0的特征。Logistic回归就是要学习得到,使得正例的特征远大于0,负例的特征远小于0,而且要在全部训练实例上达到这个目标。

在svm中,我们对上述逻辑回归稍微做一点点调整。首先,我们还是讲数据分为两类,但是我们用-1,1来进行区分。因此,我们希望引入一个新的g(z),能够使得映射后的值落在[-1,1] 之间。即: 。注意,这个新的映射函数g(z) 是一个分段函数,而不是之前在逻辑回归中的连续函数。换言之,在进行分类的时候,遇到一个新的数据点x,将x代入f(x) 中,如果f(x)小于0则将x的类别赋为-1,如果f(x)大于0则将x的类别赋为1。

现在,我们假设已经找到了这个超平面,该超平面我们用 来表示。根据前面的介绍,我们知道一定会满足如下几点:
1)在分类为1的所有样本中,一定存在一个点,其距离超平面的距离最近。我们设为;
2)在分类为-1的所有样本中,一定存在一个点,其距离超平面的距离最近。我们设为;
3)将带入到上面的式子中,得到的,一定大于0。且其他所有分类为1的样本点,都有;
4)同理,将带入到上面的式子中,得到的,一定小于0。且其他所有分类为-1的样本点,都有;
5)对于或者到超平面f(x) 的函数距离,无论是多少,都不会影响超平面的存在。即,如果成比例的改变w和b,函数距离也会成比例改变,但是,对于我们要求解的超平面是不会有影响的;
6)根据点到直线的距离公式,我们可以知道到超平面f(x) 的几何距离为:。(是一样的),其中;

综合以上,
1)为了去掉绝对值,我们可以将或者到超平面f(x) 的函数距离改写为:。
2)根据5)我们可以知道,无论为多少,都不会影响超平面的优化,因此,为了后面便于计算,我们可以假定。(当然,你也可以假定为任何值,但是如上所述,都不会影响超平面的优化。既然如此,为啥我们不简单点,假定为1呢?)
3)根据svm的思想,我们最终得到svm的目标函数:即,求解 的最大值,也就等价于最小化(1/2只是为了后面求导方便,对结果没有影响)。同时还需要满足一个约束,即

所以,SVM的目标函数为:

关于如何求解上述目标函数,细节就不在这里谈了。可以回顾一下运筹学中所教的方法。网上也有相关的文章介绍,有兴趣的同学可以去参考。

这里在啰嗦几句:
我们知道:两个向量的内积,就是一个向量在另外一个向量上的投影长度,乘以另外那个向量的长度(范式)。(不知道的同学,直接记住就好)。
根据此特点,,其中,就是在w上的投影长度。那么原来的约束就可以变为:,或则。 注意,这里我们省略了b,不影响实际的求解结果。
我们要求||w||的最小值,我们就需要让越大越好。我们通过旋转超平面的角度,直到找到一个超平面,能够满足最大,也就是在w上的投影长度最大,就是我们要找的超平面。 这从几何意义上也解释了上述目标函数的由来。

二、软间隔

在实际应用中,完全线性可分的样本是很少的,如果遇到了不能够完全线性可分的样本,我们应该怎么办?比如下面这个:


于是我们就有了软间隔,相比于硬间隔的苛刻条件,我们允许个别样本点出现在间隔带里面,比如:

我们允许部分样本点不满足约束条件:
根据我们在运筹学中学到的方法,我们可以引入一个松弛变量 ,令 ,即

增加软间隔后我们的优化目标变成了:
这里的C,就跟之前过拟合中的 一样,是一个惩罚因子。如果C设的很大, 就会很小,就相当于是硬间隔,要求完全线形可分。反之,如果C设得比较小, 就会相对较大,此时将会允许有跟多的样本点不满足约束条件。

三、核函数

还有一些数据没法直接用一条直线分开,这类数据叫做非线性可分数据。比如下面这个图,一维数据红点和蓝点位于同一条线上,我们是找不到分界点将红点和蓝点分开的。


对于这种问题,我们也是有办法解决的,如果我们用函数 来映射这几个数据,增加一个维度,将一维数据转换为二维数据,红点就变成(-1, 1)和(1, 1),而蓝点变成(-3, 9)和(3, 9)。这样红点和蓝点就变成线性可分的,可以很容易地找到决策边界。

所以可以将低维空间中的非线性可分离数据,映射到高维空间,就可能得到线性可分离数据。
这就如同我们在逻辑回归中提到的一样,我们可以使用高级数的多项式模型来解决无法用直线进行分隔的分类问题:

为了获得上图所示的判定边界,我们的模型可能是 的形式。

但是这里有个问题:原来的特征是低维的,我们人为的构造了一些特征,让其变成高维的了。那到底要如何构造特征,才能确保我们到了高维后,一定线形可分呢? 换句话说,按上面的例子,你如何知道要变成,就可以线形可分了?为什么不是其他的形式?

注意,这里跟我们之前学的PCA恰好相反。pca是降维,而我们这里是要通过对特征的一些组合变换,构造出一些新的特征。通过人为的增加特征的维度来让原来线形不可分变为线形可分。

我们知道,如果我们要引入高阶特征,我们的维度将会呈指数增加,而且我们也不能保证我们引入了高阶特征后,一定会线形可分。那如何构造新的特征呢?这里我们介绍一个新的构造特征的思路:即引入某种函数,这些函数用来计算当前训练样本与给定标记之间的相似度。如果我们给定了5个标记,那么就会构造出5个新的特征。这些变换过的特征,代表当前所有训练样本,与这5个标记之间的相似程度。这就是核函数的思想。

那么,如何选定标记呢? 一个最简单的做法,即是将训练集的所有样本作为标记。一条样本代表一个标记,有m个样本,就有m个标记。那么转换后的特征就有m个(实际是m+1个,我们这里不考虑截距项)。我们要做的事情,就是用交叉验证或者测试集或者直接再用训练集来计算出m个特征的参数。

例如,对于高斯核函数,假设我们选定了m个特征,有n个训练样本,那么高斯核函数就是用下面这个公式来计算相似度:

当然,计算相似度有很多种方法,你也可以使用线形核函数:

或者多项式核函数:

其原理都是类似,只是计算的方法不同而已。

从数学角度上来说,回顾上面内容,我们的任务是找出合适的参数w,b,使得分割超平面间距最大,且能正确对数据进行分类。间距最大是我们的优化目标。正确对数据进行分类是约束条件。即在满足约束条件的前提下,求解的最小值。

为了去掉约束条件,便于我们求解,我们使用拉格朗日乘子法。其方法是引入非负系数α来作为约束条件的权重:

对这个函数求偏导,并令偏导数为0得到:
对w求偏导: =>
对b求偏导: =>
将 代入拉格朗日函数,并考虑约束条件,求对偶问题,得到:

求出后,带入目标函数,即可求出w和b。
在上面的式子中, 都是实数, 和 是特征向量。这种向量的点积运算,我们就定义为核函数。它的物理含义是衡量两个向量的相似性。即: 。就是相似性函数。核函数的作用就是接收低维空间的输入向量,能够计算出在高维空间里的向量点积。

四、支持向量机的R的实现

在R中进行支持向量机的模型预测比较简单,可以直接使用e1071包的svm函数即可实现。

#导入数据,预测化学品的生物降解
bdf <- read.table("biodeg.csv", sep=";", quote="\"")
head(bdf, n = 3) #查看数据,有41个特征。第42列包含输出,不可生物降解为NRB,可生物降解为RB
bdf$V42 <- factor(bdf$V42) #将第42列因子化
levels(bdf$V42)<-c(0,1)  #将第42列变为0,1


#区分测试集和训练集
library(caret)
library(tidyverse)
set.seed(23419002)
bdf_sampling_vector <- createDataPartition(bdf$V42, p = 0.80, list = FALSE)
bdf_train <- bdf[bdf_sampling_vector,]
bdf_test <- bdf[-bdf_sampling_vector,]

#训练模型。支持向量机的包为e1071
library(e1071)
#用支持向量机来进行模型构建。kernel核函数,此处为线性核函数;cost代表损失函数的值,即我们之前讲到的C
model_lin<-svm(V42~.,data=bdf_train,kernel="linear",cost=10)
model_lin$fitted #查看训练后的结果

mean(bdf_train[,42] == model_lin$fitted) #计算训练后的模型准确度,可以看到,在训练集上有87.9%的准确率。
table(actual = bdf_train[,42],predictions = model_lin$fitted) #查看混淆矩阵

test_predictions <- predict(model_lin,bdf_test[,1:41])  #在测试集上进行预测
mean(bdf_test[,42] == test_predictions) #查看测试集的准确度。可以看到测试集上的准确度达到了91.9%

这里我们有个问题。cost到底取多少算合适呢?所以我们面临一个参数调优的问题。参数调优,可以手工来进行,也可也自动来进行。
先看手工方式:

#定义一函数,用于计算不同cost下的准确度
getAccuraciesOfLinearSVM<-function(cost) {
  #根据给定参数cost,来拟合模型
  model_lin<-svm(V42~.,data=bdf_train,kernel="linear",cost=cost)
  #计算训练集的准确度。signif用于数据截取函数,digits表示保留3位小数 
  train_accuracy <- signif(mean(bdf_train[,42] == model_lin$fitted), digits = 3)
  test_predictions <- predict(model_lin,bdf_test[,1:41]) #预测测试集
  test_accuracy <- signif(mean(bdf_test[,42] == test_predictions), digits = 3) #计算测试集准确度
  return(list(training=train_accuracy, test=test_accuracy))
}

#手工取c为0.01, 0.1, 1, 10, 100, 1000这几个数,观察效果
cost <- c(0.01, 0.1, 1, 10, 100, 1000)
linearPerformances <- sapply(cost, getAccuraciesOfLinearSVM)
colnames(linearPerformances) <- cost #给输出结果取个列名
linearPerformances

当然,我们还可以使用caret包中的tune来自动完成上述工作。

#读取数据(信贷数据)
german_raw <- read.table("german.data", quote="\"")

#重新命名列名
names(german_raw) <- c("checking", "duration", "creditHistory",
                       "purpose", "credit", "savings", "employment",
                       "installmentRate", "personal", "debtors", "presentResidence", "property", "age", "otherPlans", "housing", "existingBankCredits", "job", "dependents", "telephone", "foreign", "risk")
library(caret)
#将一些因子变量变为虚拟变量,同时将risk因子化,且变为0和1(之前是1和2)
dummies <- dummyVars(risk ~ ., data = german_raw)
german<- data.frame(predict(dummies, newdata = german_raw), risk=factor((german_raw$risk-1)))

set.seed(977)
german_sampling_vector <- createDataPartition(german$risk, p = 0.80, list = FALSE)
german_train <- german[german_sampling_vector,]
german_test <- german[-german_sampling_vector,]

#这里增加了权重。在信贷问题上,如果把一个高风险客户识别为低风险客户,对银行有可能造成较大损失
#反之则不然。因此这里要对分类结果增加权重。这里我们把分类为低风险客户设置为5
#分类为高风险客户设置为1,因此分到低风险客户的惩罚项是高风险客户的5倍
#因此我们结果会偏向于尽可能的被分为高风险客户
class_weights <- c(1,5)
names(class_weights) <- c("0","1")
class_weights

set.seed(2423)
#使用tune进行自动调参
german_radial_tune <- tune(svm,risk~.,data=german_train, kernel="radial", 
                           ranges = list(cost=c(0.01,0.1,1,10,100), 
                                         gamma=c(0.01,0.05,0.1,0.5,1)),
                           class.weights = class_weights)
#查看调参后的最优参数组合
german_radial_tune$best.parameters
#在最优参数组合下的性能
german_radial_tune$best.performance

#根据调参得到的最优模型在测试集上进行预测,可以看到,在测试集上有75%的准确率
german_model <- german_radial_tune$best.model
test_predictions <- predict(german_model,german_test[,1:61])
mean(test_predictions == german_test[,62])
table(predicted = test_predictions, actual = german_test[,62])

set.seed(2423)
#不设权重的结果
german_radial_tune_unbiased <- tune(svm,risk~.,data=german_train, kernel="radial", 
                                    ranges = list(cost=c(0.01,0.1,1,10,100), 
                                                  gamma=c(0.01,0.05,0.1,0.5,1)))
german_radial_tune_unbiased$best.parameters
german_radial_tune_unbiased$best.performance

【参考文献】
支持向量机通俗导论
支持向量机(SVM)是什么意思?
【机器学习】支持向量机 SVM
支持向量机(SVM)——原理篇
支持向量机(SVM)介绍
SVM支持向量机原理及核函数

你可能感兴趣的:(机器学习(六)——支持向量机)