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

    • 第六章 支持向量机
      • 6.1 什么是支持向量机
        • 6.1.1 线性SVM
        • 6.1.2 函数间隔和几何间隔
        • 6.1.3 最大间隔分离超平面
        • 6.1.4 支持向量和间隔边界
        • 6.1.4 学习的对偶算法
      • 6.2 线性支持向量机与软间隔最大化
        • 6.2.1 线性支持向量机
        • 6.2.2 学习的对偶算法
        • 6.2.3 支持向量
        • 6.2.4 合页损失函数
        • 6.2.5 编程求解线性SVM
        • 6.2.6 简化版SMO算法
      • 6.3 非线性支持向量机与核函数
        • 6.3.1 核技巧
        • 6.3.2 正定核
        • 6.3.3 常用核函数
        • 6.3.4 非线性支持向量分类机
        • 6.3.5 编程实现非线性SVM
      • 6.4 序列最小最优化算法(SMO算法)
        • 6.4.1 两个变量二次规划的求解方法
        • 6.4.2 变量的选择方法
        • 6.4.3 SMO算法步骤
        • 6.4.4 SMO算法优化
        • 6.4.5 完整版SMO算法
      • 6.5 使用sklearn构建SVM分类器
        • 6.5.1 Sklearn.svm.SVC
        • 6.5.2 编写代码
      • 6.6 SVM的优缺点

第六章 支持向量机

6.1 什么是支持向量机

支持向量机(Support Vector Machines)是目前被认为最好的现成的算法之一

在很久以前的情人节,大侠要去救他的爱人,但魔鬼和他玩了一个游戏。

魔鬼在桌子上似乎有规律放了两种颜色的球,说:“你用一根棍分开它们?要求:尽量在放更多球之后,仍然适用。”
这里写图片描述

于是大侠这样放,干的不错?

这里写图片描述
然后魔鬼,又在桌上放了更多的球,似乎有一个球站错了阵营。
这里写图片描述

SVM就是试图把棍放在最佳位置,好让在棍的两边有尽可能大的间隙。
这里写图片描述

现在即使魔鬼放了更多的球,棍仍然是一个好的分界线。

这里写图片描述

然后,在SVM 工具箱中有另一个更加重要的 trick。 魔鬼看到大侠已经学会了一个trick,于是魔鬼给了大侠一个新的挑战。
这里写图片描述

现在,大侠没有棍可以很好帮他分开两种球了,现在怎么办呢?当然像所有武侠片中一样大侠桌子一拍,球飞到空中。然后,凭借大侠的轻功,大侠抓起一张纸,插到了两种球的中间。
这里写图片描述

现在,从魔鬼的角度看这些球,这些球看起来像是被一条曲线分开了。
这里写图片描述

再之后,无聊的大人们,把这些球叫做 「data」,把棍子 叫做 「classifier」, 最大间隙trick 叫做「optimization」, 拍桌子叫做「kernelling」, 那张纸叫做「hyperplane」。

图片来源:Support Vector Machines explained well

当数据为线性可分的时候,也就是可以用一根棍子将两种小球分开的时候,只要将棍子放在让小球距离棍子的距离最大化的位置即可,寻找该最大间隔的过程就叫做最优化。

但是一般的数据是线性不可分的,所以要将其转化到高维空间去,用一张纸将其进行分类,空间转化就是需要核函数,用于切分小球的纸就是超平面。

什么是SVM:

分类作为数据挖掘领域中一项非常重要的任务,它的目的是学会一个分类函数或分类模型(或者叫做分类器),而支持向量机本身便是一种监督式学习的方法(至于具体什么是监督学习与非监督学习,请参见此系列Machine L&Data Mining第一篇),它广泛的应用于统计分类以及回归分析中。

支持向量机(SVM)是90年代中期发展起来的基于统计学习理论的一种机器学习方法,通过寻求结构化风险最小来提高学习机泛化能力,实现经验风险和置信范围的最小化,从而达到在统计样本量较少的情况下,亦能获得良好统计规律的目的。

通俗来讲,它是一种二类分类模型,其基本模型定义为特征空间上的间隔最大的线性分类器,即支持向量机的学习策略便是间隔最大化,最终可转化为一个凸二次规划问题的求解。

6.1.1 线性SVM

线性可分的二分类问题:

这里写图片描述

上图中红色和蓝色分别表示不同的两个类别,数据为线性可分,但是将其分开的直线不止一条,(b)(c)分别给出了不同的方法。黑色的实现为“决策面”,每个决策面对应一个线性分类器,两者的性能是有差距的。

这里写图片描述

决策面不同的情况下,添加一个红色的点,显然(b)仍然能够很好的分类,但是(c)已经分类错误了,所以决策面(b)优于(c)。

如何选择较好的决策面:

在保证决策面方向不变且不会出现错分样本的情况下移动决策面,会在原来的决策面两侧找到两个极限位置(越过该位置就会产生错分现象),如虚线所示。

虚线的位置由决策面的方向和距离原决策面最近的几个样本的位置决定。而这两条平行虚线正中间的分界线就是在保持当前决策面方向不变的前提下的最优决策面。

两条虚线之间的垂直距离就是这个最优决策面对应的分类间隔。显然每一个可能把数据集正确分开的方向都有一个最优决策面(有些方向无论如何移动决策面的位置也不可能将两类样本完全分开),而不同方向的最优决策面的分类间隔通常是不同的,那个具有“最大间隔”的决策面就是SVM要寻找的最优解。

而这个真正的最优解对应的两侧虚线所穿过的样本点,就是SVM中的支持样本点,称为”支持向量”。

学习的目标是在特征空间中找到一个分离超平面,能将实例分到不同的类中。

分离超平面的方程: wx+b=0  w ⋅ x + b = 0

方程由法向量 w  w 和截距 b  b 决定,可以用 (w,b)  ( w , b ) 来表示。
这里写图片描述
分离超平面将特征空间划分为两部分,一部分为正类,一部分为负类,法向量指向的一侧为正类,另一侧为负类。

这里写图片描述

6.1.2 函数间隔和几何间隔

机器学习实战(六)——支持向量机_第1张图片

机器学习实战(六)——支持向量机_第2张图片

机器学习实战(六)——支持向量机_第3张图片

备注:

这里写图片描述这里写图片描述

上图7.1中有A,B,C三个点,表示3个实例,均在分离超平面的正类一侧,预测他们的类,点A距离超平面较远,若预测该类为正类,就比较确信为正确的,点C距离分离超平面较近,不是很确信。

函数间隔(function margin):

一般来说,一个点距离分离超平面的远近可以表示分类预测的确信程度,在超平面 wx+b=0  w ⋅ x + b = 0 确定的情况下, |wx+b|  | w ⋅ x + b | 能够相对的表示点x距离超平面的远近,而 wx+b  w ⋅ x + b 的符号与类标记y的符号是否一致能够表示分类是否正确,所以可以量 y(wx+b)  y ( w ⋅ x + b ) 来表示分离的正确性和确信度。

这里写图片描述

从函数间隔变为几何间隔:

虽然函数间隔可以表示分类预测的正确性即确信度,但是选择分离超平面时,只有函数间隔远远不够,因为只要成比例的改变 w  w b  b ,例如将它们改变为 2w  2 w 2b  2 b ,超平面并没有改变( wx+b=0  w ∗ x + b = 0 ,右边为0,不会因为系数而改变),但是函数间隔( y(wx+b)  y ( w ∗ x + b ) )却变为原来的2倍。

对分离超平面的法向量 w  w 加某些约束,如规范化, ||w||=1  | | w | | = 1 ,使得间隔是确定的,此时函数间隔变为几何间隔。

这里写图片描述

上图给出了超平面 (w,b)  ( w , b ) 和其法向量 w  w ,点 A  A 表示某一实例 x i   x i ,其类标记为 y i =+1  y i = + 1 ,点 A  A 与超平面的距离由线段 AB  A B 给出,记作 y i   y i

y i =w||w|| x i +b||w||   y i = w | | w | | ⋅ x i + b | | w | |

其中, ||w||  | | w | | w  w L 2   L 2 范数,如果点 A  A 在超平面的负一侧,即 y i =1  y i = − 1 ,则:

y i =(w||w|| b||w|| )  y i = − ( w | | w | | ⋅ b | | w | | )

一般情况,当样本点 (x i ,y i )  ( x i , y i ) 被超平面 (w,b)  ( w , b ) 正确分类时,点 x i   x i 与超平面 (w,b)  ( w , b ) 的距离是:

γ i =y i (w||w|| x i +b||w|| )  γ i = y i ( w | | w | | ⋅ x i + b | | w | | )

几何间隔的概念:
这里写图片描述

6.1.3 最大间隔分离超平面

这里写图片描述

最大间隔分离超平面,即为下面的约束最优化问题:

这里写图片描述

γ=1  γ = 1 ,最大化 1||w||   1 | | w | | ,和最小化 12 ||w|| 2   1 2 | | w | | 2 是等价的,所以变为:

这里写图片描述

这就变为一个凸二次规划问题,求出上式的解 w    w ∗ b    b ∗ ,就可以得到最大间隔分离超平面 w  x+b    w ∗ ⋅ x + b ∗ 及分类决策函数 f(x)=sign(w  x+b  )  f ( x ) = s i g n ( w ∗ ⋅ x + b ∗ ) ,即线性可分支持向量机模型。

总结:

这里写图片描述

线性可分训练数据集的最大间隔分离超平面是存在且唯一的

6.1.4 支持向量和间隔边界

支持向量:

在线性可分情况下,训练数据集的样本点中与分离超平面距离最近的样本点的实例称为支持向量,支持向量是使约束条件等号成立的点,即:

y i (w×x i +b)1=0  y i ( w × x i + b ) − 1 = 0

y i =+1  y i = + 1 的正例点,支持向量在超平面:

H 1 :w×x i +b=1  H 1 : w × x i + b = 1

y i =+1  y i = + 1 的正例点,支持向量在超平面:

H 2 :w×x i +b=1  H 2 : w × x i + b = 1

如下图所示,在 H 1   H 1 H 2   H 2 上的点就是支持向量。

这里写图片描述

间隔边界:

H 1   H 1 H 2   H 2 平行,且没有实例点落在它们中间,分离超平面与它们平行且位于中央,间隔即为 H 1   H 1 H 2   H 2 直接的距离,依赖于分离超平面的法向量 w  w ,等于 2||w||   2 | | w | | H 1   H 1 H 2   H 2 称为间隔边界。

分离超平面是由支持向量决定的,其他实例点并不起作用,移动支持向量将会改变所求平面,移动其他实例点所求平面不会改变。

由于支持向量在确定分离超平面中起着决定性作用,所以将这种分离模型称为支持向量机。

支持向量的个数一般很少,所以支持向量机由很少的“重要的”训练样本确定。

这里写图片描述

6.1.4 学习的对偶算法

为了求解线性可分支持向量机的最优化问题,将它作为原始最优化问题,应用拉格朗日对偶性,通过求解对偶问题得到原始问题的最优解,这就是线性可分支持向量机的对偶算法。

对偶算法的优点:

1)对偶问题往往更容易求解
2)自然引入核函数,进而推广到非线性分类问题

首先,我们先要从宏观的视野上了解一下拉格朗日对偶问题出现的原因和背景。

我们知道我们要求解的是最小化问题,所以一个直观的想法是如果我能够构造一个函数,使得该函数在可行解区域内与原目标函数完全一致,而在可行解区域外的数值非常大,甚至是无穷大,那么这个没有约束条件的新目标函数的优化问题就与原来有约束条件的原始目标函数的优化问题是等价的问题。这就是使用拉格朗日方程的目的,它将约束条件放到目标函数中,从而将有约束优化问题转换为无约束优化问题。

随后,人们又发现,使用拉格朗日获得的函数,使用求导的方法求解依然困难。进而,需要对问题再进行一次转换,即使用一个数学技巧:拉格朗日对偶。

所以,显而易见的是,我们在拉格朗日优化我们的问题这个道路上,需要进行下面二个步骤:

  • 将有约束的原始目标函数转换为无约束的新构造的拉格朗日目标函数
  • 使用拉格朗日对偶性,将不易求解的优化问题转化为易求解的优化

而求解这个对偶学习问题,可以分为三个步骤:首先要让 L(w,b,α)  L ( w , b , α ) 关于 w  w b  b 最小化,然后求对α的极大,最后利用SMO算法求解对偶问题中的拉格朗日乘子。

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

  • KKT条件

假设一个最优化模型能够表示成下列标准形式:

这里写图片描述
KKT条件的全称是Karush-Kuhn-Tucker条件,KKT条件是说最优值条件必须满足以下条件:
1)条件一:经过拉格朗日函数处理之后的新目标函数L(w,b,α)对α求导为零:
2)条件二: h(x)=0  h ( x ) = 0
3)条件三: αg(x)=0  α ∗ g ( x ) = 0

对于我们的优化问题:
这里写图片描述

上述优化问题满足三个条件,故满足了凸优化问题和KKT条件。

示例:

这里写图片描述

这里写图片描述

这里写图片描述

6.2 线性支持向量机与软间隔最大化

6.2.1 线性支持向量机

线性可分问题的支持向量机学习方法对线性不可分训练数据是不适用的,因为上述方法在那个的不等式约束并不成立。

如何将其扩展到线性不可分问题:

修改硬间隔最大化,使其成为软间隔最大化。

假设给定一个特征空间上的训练数据集:

T={(X 1 ,Y 1 ),(X 2 ,Y 2 ),...,(x N ,y N )}  T = { ( X 1 , Y 1 ) , ( X 2 , Y 2 ) , . . . , ( x N , y N ) }
其中, xiχ=R n ,y i Y={+1,1},i=1,2,...,N  x − i ∈ χ = R n , y i ∈ Y = { + 1 , − 1 } , i = 1 , 2 , . . . , N x i   x i 为第i个特征向量, y i   y i x i   x i 的类标记,假设训练数据集是线性不可分的,通常情况是,训练数据中有一些特异点(outlier),将这些特异点去除后,剩下的大部分样本点组成的几何是线性可分的。

这里写图片描述

这里写图片描述

线性支持向量机定义:

给定的线性不可分的训练数据集,通过求解凸二次规划问题,即软间隔最大化问题(7.32)~(7.34),得到的分离超平面为:

w  x+b  =0  w ∗ ⋅ x + b ∗ = 0

以及相应的分类决策函数:

f(x)=sign(w  x+b  )  f ( x ) = s i g n ( w ∗ ⋅ x + b ∗ )

称为线性支持向量机

6.2.2 学习的对偶算法

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

步骤(2)中,对任一适合条件 0<α  j <C  0 < α j ∗ < C α  j   α j ∗ 都可以求出 b    b ∗ ,但是由于问题(7.32)~(7.34)对 b  b 的解不唯一,所以实际计算时可以取在所有符合条件的样本点上的平均值。

6.2.3 支持向量

这里写图片描述

6.2.4 合页损失函数

这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述

6.2.5 编程求解线性SVM

可视化数据集

import matplotlib.pylab as plt
import numpy as np

""" 函数说明:读取数据 """


# readlines() 自动将文件内容分析成一个行的列表,该列表可以由 Python 的 for... in...结构进行处理
# readlines()一次读取整个文件,readline() 每次只读取一行,通常比 .readlines() 慢得多

def loadDataSet(fileName):
    dataMat = []; labelMat = []
    fr = open(fileName)
    for line in fr.readlines():                                     #逐行读取,滤除空格等
        lineArr = line.strip().split('\t')
        dataMat.append([float(lineArr[0]), float(lineArr[1])])      #添加数据
        labelMat.append(float(lineArr[2]))                          #添加标签
    return dataMat,labelMat


""" 函数说明:数据可视化 """


def showDataSet(dataMat, labelMat):
    data_plus = []  # 正样本
    data_minus = []  # 负样本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np = np.array(data_plus)  # 转换为numpy矩阵
    data_minus_np = np.array(data_minus)  # 转换为numpy矩阵
    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1])  # 正样本散点图
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1])  # 负样本散点图
    plt.show()


if __name__ == '__main__':
    dataMat, labelMat = loadDataSet('testSet.txt')
    showDataSet(dataMat, labelMat)

结果:
这里写图片描述

6.2.6 简化版SMO算法

from time import sleep
import matplotlib.pylab as plt
import numpy as np
import random
import types

""" 函数说明:读取数据 """


def loadDataSet(fileName):
    dataMat = []
    labelMat = []
    fr = open(fileName)
    for line in fr.readlines():  # 逐行读取,滤除空格等
        lineArr = line.strip().split('\t')
        dataMat.append([float(lineArr[0]), float(lineArr[1])])  # 添加数据
        labelMat.append(float(lineArr[2]))  # 添加标签
    return dataMat, labelMat


""" 函数说明:随机选择alpha Parameters: i:alpha m:alpha参数个数 """


def selectJrand(i, m):
    j = i
    # 选择一个不等于i的j
    while (j == i):
        j = int(random.uniform(0, m))
    return j


""" 函数说明:修剪alpha Parameters: aj:alpha的值 H:alpha上限 L:alpha下限 Returns: aj:alpha的值 """


def clipAlpha(aj, H, L):
    if aj > H:
        aj = H
    if L > aj:
        aj = L
    return aj


""" 函数说明:简化版SMO算法 Parameters: dataMatIn:数据矩阵 classLabels:数据标签 C:松弛变量 toler:容错率 maxIter:最大迭代次数 """


def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
    # 转换为numpy的mat存储
    dataMatrix = np.mat(dataMatIn)
    labelMat = np.mat(classLabels).transpose()
    # 初始化b参数,统计dataMatrix的维度
    b = 0;
    m, n = np.shape(dataMatrix)
    # 初始化alpha参数,设为0
    alphas = np.mat(np.zeros((m, 1)))
    # 初始化迭代次数
    iter_num = 0
    # 最多迭代matIter次
    while (iter_num < maxIter):
        alphaPairsChanged = 0
        for i in range(m):
            # 步骤1:计算误差Ei
            fXi = float(np.multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[i, :].T)) + b
            Ei = fXi - float(labelMat[i])
            # 优化alpha,设定一定的容错率。
            if ((labelMat[i] * Ei < -toler) and (alphas[i] < C)) or ((labelMat[i] * Ei > toler) and (alphas[i] > 0)):
                # 随机选择另一个与alpha_i成对优化的alpha_j
                j = selectJrand(i, m)
                # 步骤1:计算误差Ej
                fXj = float(np.multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[j, :].T)) + b
                Ej = fXj - float(labelMat[j])
                # 保存更新前的aplpha值,使用深拷贝
                alphaIold = alphas[i].copy()
                alphaJold = alphas[j].copy()
                # 步骤2:计算上下界L和H
                if (labelMat[i] != labelMat[j]):
                    L = max(0, alphas[j] - alphas[i])
                    H = min(C, C + alphas[j] - alphas[i])
                else:
                    L = max(0, alphas[j] + alphas[i] - C)
                    H = min(C, alphas[j] + alphas[i])
                if L == H: print("L==H"); continue
                # 步骤3:计算eta
                eta = 2.0 * dataMatrix[i, :] * dataMatrix[j, :].T - dataMatrix[i, :] * dataMatrix[i, :].T -\
                      dataMatrix[j,:] * dataMatrix[j, :].T
                if eta >= 0: print("eta>=0"); continue
                # 步骤4:更新alpha_j
                alphas[j] -= labelMat[j] * (Ei - Ej) / eta
                # 步骤5:修剪alpha_j
                alphas[j] = clipAlpha(alphas[j], H, L)
                if (abs(alphas[j] - alphaJold) < 0.00001): print("alpha_j变化太小"); continue
                # 步骤6:更新alpha_i
                alphas[i] += labelMat[j] * labelMat[i] * (alphaJold - alphas[j])
                # 步骤7:更新b_1和b_2
                b1 = b - Ei - labelMat[i] * (alphas[i] - alphaIold) * dataMatrix[i, :] * dataMatrix[i, :].T - labelMat[
                    j] * (alphas[j] - alphaJold) * dataMatrix[i, :] * dataMatrix[j, :].T
                b2 = b - Ej - labelMat[i] * (alphas[i] - alphaIold) * dataMatrix[i, :] * dataMatrix[j, :].T - labelMat[
                    j] * (alphas[j] - alphaJold) * dataMatrix[j, :] * dataMatrix[j, :].T
                # 步骤8:根据b_1和b_2更新b
                if (0 < alphas[i]) and (C > alphas[i]):
                    b = b1
                elif (0 < alphas[j]) and (C > alphas[j]):
                    b = b2
                else:
                    b = (b1 + b2) / 2.0
                # 统计优化次数
                alphaPairsChanged += 1
                # 打印统计信息
                print("第%d次迭代 样本:%d, alpha优化次数:%d" % (iter_num, i, alphaPairsChanged))
        # 更新迭代次数
        if (alphaPairsChanged == 0):
            iter_num += 1
        else:
            iter_num = 0
        print("迭代次数: %d" % iter_num)
    return b, alphas


""" 函数说明:分类结果可视化 """


def showClassifer(dataMat, w, b):
    # 绘制样本点
    data_plus = []  # 正样本
    data_minus = []  # 负样本
    for i in range(len(dataMat)):
        if labelMat[i] > 0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np = np.array(data_plus)  # 转换为numpy矩阵
    data_minus_np = np.array(data_minus)  # 转换为numpy矩阵
    plt.scatter(np.transpose(data_plus_np)[0], np.transpose(data_plus_np)[1], s=30,alpha=0.7)  # 正样本散点图
    plt.scatter(np.transpose(data_minus_np)[0], np.transpose(data_minus_np)[1], s=30,alpha=0.7)  # 负样本散点图
    # 绘制直线
    x1 = max(dataMat)[0]
    x2 = min(dataMat)[0]
    a1, a2 = w
    b = float(b)
    a1 = float(a1[0])
    a2 = float(a2[0])
    y1, y2 = (-b - a1 * x1) / a2, (-b - a1 * x2) / a2
    plt.plot([x1, x2], [y1, y2])
    # 找出支持向量点
    for i, alpha in enumerate(alphas):
        if abs(alpha) > 0:
            x, y = dataMat[i]
            plt.scatter([x], [y], s=150, c='none', alpha=0.7, linewidth=1.5, edgecolor='red')
    plt.show()

""" 函数说明:计算w """


def get_w(dataMat, labelMat, alphas):
    alphas, dataMat, labelMat = np.array(alphas), np.array(dataMat), np.array(labelMat)
    w = np.dot((np.tile(labelMat.reshape(1, -1).T, (1, 2)) * dataMat).T, alphas)
    return w.tolist()


if __name__ == '__main__':
    dataMat, labelMat = loadDataSet('testSet.txt')
    b, alphas = smoSimple(dataMat, labelMat, 0.6, 0.001, 40)
    w = get_w(dataMat, labelMat, alphas)
    showClassifer(dataMat, w, b)

结果:

这里写图片描述

蓝色实线是所求分类器,红色圈表示支持向量机

6.3 非线性支持向量机与核函数

当分类问题是非线性的时候,可以使用非线性支持向量机,其主要特点是利用核技巧。

6.3.1 核技巧

一、非线性分类问题

这里写图片描述

这里写图片描述

非线性问题的解决方法:通过非线性变换,将非线性问题变为线性问题

举例说明:
假设二维平面x-y上存在若干点,其中点集A服从{x,y|x^2+y^2=1},点集B服从{x,y|x^2+y^2=9},那么这些点在二维平面上的分布是这样的:
这里写图片描述

蓝色的是点集A,红色的是点集B,他们在xy平面上并不能线性可分,即用一条直线分割( 虽然肉眼是可以识别的) 。采用映射(x,y)->(x,y,x^2+y^2)后,在三维空间的点的分布为:
这里写图片描述

这里写图片描述

用线性分类方法求解非线性问题分为两步:

1)使用一个变换将原空间的数据映射到新空间
2)在新空间用线性分类学习方法从训练数据中学习分类模型

二、核函数的定义

χ  χ 为输入空间,又设 H  H 为特征空间(希尔伯特空间),如果存在一个从 χ  χ H  H 的映射: ϕ(x)=χH  ϕ ( x ) = χ → H

使得所有的 x,zχ  x , z ∈ χ ,函数 K(x,z)  K ( x , z ) 满足条件: K(x,z)=ϕ(x)ϕ(z)  K ( x , z ) = ϕ ( x ) ⋅ ϕ ( z )

K(x,z)  K ( x , z ) 称为核函数, ϕ(x)  ϕ ( x ) 称为映射函数。

这里写图片描述

三、核技巧在支持向量机中的应用
这里写图片描述

这里写图片描述

6.3.2 正定核

通常所说的核函数就是正定核函数

这里写图片描述

6.3.3 常用核函数

这里写图片描述

6.3.4 非线性支持向量分类机

利用核技巧可以将线性分类的学习方法应用到非线性分类问题中去,将线性支持向量机扩展到非线性支持向量机,只需将线性支持向量机对偶形式的内积换成核函数即可

非线性支持向量机:
从非线性分类训练集,通过核函数与软间隔最大化,或凸二次规划(7.95)~(7.97)学习得到的分类决策函数:

这里写图片描述

称为非线性支持向量, K(x,z)  K ( x , z ) 是正定核函数。

非线性支持向量机学习算法:
这里写图片描述

6.3.5 编程实现非线性SVM

接下来,我们将使用testSetRBF.txttestSetRBF2.txt进行实验,前者作为训练集,后者作为测试集。

import matplotlib.pylab as plt
import numpy as np

def loadDataSet(fileName):
    """ :param fileName: :return: dataMat, labelMat """
    dataMat = []
    labelMat = []
    fr = open(fileName)
    for line in fr.readlines():  # 逐行读取,滤除空格等
        lineArr = line.strip().split('\t')
        dataMat.append([float(lineArr[0]), float(lineArr[1])])  # 添加数据
        labelMat.append(float(lineArr[2]))  # 添加标签
    return dataMat, labelMat


def showDataSet(dataMat,labelMat):
    """ 数据可视化 :param dataMat: 数据矩阵 :param labelMat: 数据标签 :return: 无 """
    #正样本
    data_plus=[]
    #负样本
    data_minus=[]

    for i in range(len(dataMat)):
        if labelMat[i]>0:
            data_plus.append(dataMat[i])
        else:
            data_minus.append(dataMat[i])
    data_plus_np=np.array(data_plus)
    data_minus_np=np.array(data_minus)
    #正样本散点图
    plt.scatter(np.transpose(data_plus_np)[0],np.transpose(data_plus_np)[1])
    #负样本散点图
    plt.scatter(np.transpose(data_minus_np)[0],np.transpose(data_minus_np)[1])

    plt.show()

if __name__=='__main__':
    dataArr,labelArr=loadDataSet('testSetRBF.txt')
    showDataSet(dataArr,labelArr)


结果:
这里写图片描述

6.4 序列最小最优化算法(SMO算法)

支持向量机问题可以转化为求解凸二次规划问题,这样的问题具有全局最优解,并且有许多算法可以用于这个问题的求解,但是当训练样本容量很大时,这些算法往往变得非常低效,以至于无法使用。

1996年,John Platt发布了一个称为SMO的强大算法,用于训练SVM。SM表示序列最小化(Sequential Minimal Optimizaion)。Platt的SMO算法是将大优化问题分解为多个小优化问题来求解的。这些小优化问题往往很容易求解,并且对它们进行顺序求解的结果与将它们作为整体来求解的结果完全一致的。在结果完全相同的同时,SMO算法的求解时间短很多。

SMO算法的目标是求出一系列alpha和b,一旦求出了这些alpha,就很容易计算出权重向量w并得到分隔超平面。

SMO算法的工作原理是:每次循环中选择两个alpha进行优化处理。一旦找到了一对合适的alpha,那么就增大其中一个同时减小另一个。这里所谓的”合适”就是指两个alpha必须符合以下两个条件,条件之一就是两个alpha必须要在间隔边界之外,而且第二个条件则是这两个alpha还没有进进行过区间化处理或者不在边界上。

这里写图片描述

6.4.1 两个变量二次规划的求解方法

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

6.4.2 变量的选择方法

这里写图片描述

这里写图片描述

这里写图片描述

6.4.3 SMO算法步骤

这里写图片描述

这里写图片描述

1)步骤一:计算误差:
这里写图片描述

2)步骤二:计算上下界L和H:
这里写图片描述

3)步骤三:计算 η  η
这里写图片描述

4)步骤四:更新 α j   α j

这里写图片描述

5)步骤五:根据取值范围修剪 α j   α j
这里写图片描述

6)步骤六:更新 α i   α i
这里写图片描述

7)步骤七:更新 b 1   b 1 b 2   b 2
这里写图片描述

8)步骤八:根据 b 1   b 1 b 2   b 2 更新 b  b
这里写图片描述

6.4.4 SMO算法优化

启发选择方式:

在几百个点组成的小规模数据集上,简化版SMO算法的运行是没有什么问题的,但是在更大的数据集上的运行速度就会变慢。简化版SMO算法的第二个α的选择是随机的,针对这一问题,我们可以使用启发式选择第二个α值,来达到优化效果。

下面这两个公式想必已经不再陌生:

这里写图片描述

在实现SMO算法的时候,先计算η,再更新a_j。为了加快第二个α_j乘子的迭代速度,需要让直线的斜率增大,对于α_j的更新公式,其中η值没有什么文章可做,于是只能令:

这里写图片描述

因此,我们可以明确自己的优化方法了:

  • 最外层循环,首先在样本中选择违反KKT条件的一个乘子作为最外层循环,然后用”启发式选择”选择另外一个乘子并进行这两个乘子的优化
  • 在非边界乘子中寻找使得|E_i - E_j|最大的样本
  • 如果没有找到,则从整个样本中随机选择一个样本

6.4.5 完整版SMO算法

完整版Platt SMO算法是通过一个外循环来选择违反KKT条件的一个乘子,并且其选择过程会在这两种方式之间进行交替:

  • 在所有数据集上进行单遍扫描
  • 在非边界α中实现单遍扫描

非边界α指的就是那些不等于边界0或C的α值,并且跳过那些已知的不会改变的α值。所以我们要先建立这些α的列表,用于才能出α的更新状态。

在选择第一个α值后,算法会通过”启发选择方式”选择第二个α值。

6.5 使用sklearn构建SVM分类器

在第一篇文章中,我们使用了kNN进行手写数字识别。它的缺点是存储空间大,因为要保留所有的训练样本,如果你的老板让你节约这个内存空间,并达到相同的识别效果,甚至更好。那这个时候,我们就要可以使用SVM了,因为它只需要保留支持向量即可,而且能获得可比的效果。

6.5.1 Sklearn.svm.SVC

官方手册

sklearn.svm模块提供了很多模型,其中svm.SVC是基于libsvm实现的。

这里写图片描述

class sklearn.svm.SVC(C=1.0, kernel=’rbf’, degree=3, gamma=’auto’, coef0=0.0, shrinking=True, probability=False, tol=0.001, cache_size=200, class_weight=None, verbose=False, max_iter=-1, decision_function_shape=’ovr’, random_state=None)

参数说明如下:

  • C:惩罚项,float类型,可选参数,默认为1.0,C越大,即对分错样本的惩罚程度越大,因此在训练样本中准确率越高,但是泛化能力降低,也就是对测试数据的分类准确率降低。相反,减小C的话,容许训练样本中有一些误分类错误样本,泛化能力强。对于训练样本带有噪声的情况,一般采用后者,把训练样本集中错误分类的样本作为噪声。
  • kernel:核函数类型,str类型,默认为’rbf’。可选参数为:

    • ’linear’:线性核函数
    • ‘poly’:多项式核函数
    • ‘rbf’:径像核函数/高斯核
    • ‘sigmod’:sigmod核函数
    • ‘precomputed’:核矩阵
    • precomputed表示自己提前计算好核函数矩阵,这时候算法内部就不再用核函数去计算核矩阵,而是直接用你给的核矩阵,核矩阵需要为n*n的。
  • degree:多项式核函数的阶数,int类型,可选参数,默认为3。这个参数只对多项式核函数有用,是指多项式核函数的阶数n,如果给的核函数参数是其他核函数,则会自动忽略该参数。

  • gamma:核函数系数,float类型,可选参数,默认为auto。只对’rbf’ ,’poly’ ,’sigmod’有效。如果gamma为auto,代表其值为样本特征数的倒数,即1/n_features。
  • coef0:核函数中的独立项,float类型,可选参数,默认为0.0。只有对’poly’ 和,’sigmod’核函数有用,是指其中的参数c。
  • probability:是否启用概率估计,bool类型,可选参数,默认为False,这必须在调用fit()之前启用,并且会fit()方法速度变慢。
  • shrinking:是否采用启发式收缩方式,bool类型,可选参数,默认为True。
  • tol:svm停止训练的误差精度,float类型,可选参数,默认为1e^-3。
  • cache_size:内存大小,float类型,可选参数,默认为200。指定训练所需要的内存,以MB为单位,默认为200MB。
  • class_weight:类别权重,dict类型或str类型,可选参数,默认为None。给每个类别分别设置不同的惩罚参数C,如果没有给,则会给所有类别都给C=1,即前面参数指出的参数C。如果给定参数’balance’,则使用y的值自动调整与输入数据中的类频率成反比的权重。
  • verbose:是否启用详细输出,bool类型,默认为False,此设置利用libsvm中的每个进程运行时设置,如果启用,可能无法在多线程上下文中正常工作。一般情况都设为False,不用管它。
  • max_iter:最大迭代次数,int类型,默认为-1,表示不限制。
  • decision_function_shape:决策函数类型,可选参数’ovo’和’ovr’,默认为’ovr’。’ovo’表示one vs one,’ovr’表示one vs rest。
  • random_state:数据洗牌时的种子值,int类型,可选参数,默认为None。伪随机数发生器的种子,在混洗数据时用于概率估计。

6.5.2 编写代码

import numpy as np
import operator
from os import listdir
from sklearn.svm import SVC


def img2Vector(filename):
    """ 将32*32的二进制图像转换为1*1024的向量 :param filename: 文件名 :return: 返回的二进制图像的1*1024向量 """
    # 创建1*1024零向量
    returnVect = np.zeros((1, 1024))
    # 打开文件
    fr = open(filename)
    # 按行读取
    for i in range(32):
        # 读取一行数据
        lineStr = fr.readline()
        # 每一行的前32个元素依次添加到returnVect中
        for j in range(32):
            returnVect[0, 32 * i + j] = int(lineStr[j])
    # 返回转换后的1*1024向量
    return returnVect


def handwritingClassTest():
    """ 手写数字分类测试 :return: 无 """
    # 测试集的Labels
    hwLabels = []
    # 返回trainingDigits目录下的文件名
    trainingFileList = listdir('E:/python/machine learning in action/My Code/chap 06/trainingDigits')
    # 返回文件夹下文件的个数
    m = len(trainingFileList)
    # 初始化训练的Mat矩阵,测试集
    trainingMat = np.zeros((m, 1024))
    # 从文件名中解析出训练的类别
    for i in range(m):
        # 获得文件的名字
        fileNameStr = trainingFileList[i]
        # 获得分类的数字
        classNumber = int(fileNameStr.split('_')[0])
        # 将获得的类别添加到hwlabels中
        hwLabels.append(classNumber)
        # 将每个文件的1*1024数据存储到trainingMat矩阵中
        trainingMat[i, :] = img2Vector('E:/python/machine learning in action/My Code/chap 06/trainingDigits/%s' % (fileNameStr))

    clf = SVC(C=200, kernel='rbf')
    clf.fit(trainingMat, hwLabels)
    # 返回testDigits目录下的文件列表
    testFileList = listdir('testDigits')
    # 错误检测技术
    errorCount = 0.0
    # 测试数据的数量
    mTest = len(testFileList)
    # 从文件中解析出测试集的类别并进行分类测试
    for i in range(mTest):
        fileNameStr = testFileList[i]
        classNumber = int(fileNameStr.split('_')[0])
        # 获得测试集的1*1024向量,用于训练
        vectorUnderTest = img2Vector(
            'E:/python/machine learning in action/My Code/chap 06/testDigits/%s' % (fileNameStr))
        # 获得预测结果
        classfierResult = clf.predict(vectorUnderTest)
        print("分类返回结果为 %d \t 真实结果为%d " % (classfierResult, classNumber))

        if (classfierResult != classNumber):
            errorCount += 1.0
    print("总共错了%d个数据 \n 错误率为%f%%" % (errorCount, errorCount / mTest * 100))


if __name__ == '__main__':
    handwritingClassTest()

结果:

这里写图片描述

6.6 SVM的优缺点

优点:

  • 可用于线性/非线性分类,也可以用于回归,泛化错误率低,也就是说具有良好的学习能力,且学到的结果具有很好的推广性。
  • 可以解决小样本情况下的机器学习问题,可以解决高维问题,可以避免神经网络结构选择和局部极小点问题。
  • SVM是最好的现成的分类器,现成是指不加修改可直接使用。并且能够得到较低的错误率,SVM可以对训练集之外的数据点做很好的分类决策。

缺点:

  • 对参数调节和和函数的选择敏感。

你可能感兴趣的:(机器学习实战)