机器学习实战刻意练习-- Task 5 支持向量机

Week 3

支持向量机(Support Vector Machine,SVM)

下面介绍支持向量机,首先我们提出几个问题:

什么是SVM?

首先,支持向量机不是一种机器,而是一种机器学习算法。

  1. SVM - Support Vector Machine ,俗称支持向量机,是一种 supervised learning
    (监督学习)算法,属于 classification(分类)的范畴。
  2. 在数据挖掘的应用中,与 unsupervised learning(无监督学习)Clustering(聚类)相对应和区别。
  3. 广泛应用于 Machine Learning(机器学习)Computer Vision(计算机视觉,装逼一点说,就是 cv)Data Mining(数据挖掘)当中。
“ Machine(机)” 是什么?

Classification Machine (分类器),这个没什么好说的。也可以理解为算法,机器学习领域里面常常用 “机” 也就是 machine 这个字表示算法。

“支持向量” 又是什么?

通俗理解: support vector (支持向量)的意思就是 数据集中的某些点,位置比较特殊。比如 x+y-2=0 这条直线,直线上面区域 x+y-2>0 的全是 A 类,下面的 x+y-2<0 的全是 B 类,我们找这条直线的时候,一般就看聚集在一起的两类数据,他们各自的最边缘位置的点,也就是最靠近划分直线的那几个点,而其他点对这条直线的最终位置的确定起不了作用,所以我们称这些点为 “支持向量”。

另一个说法是maximum margin (最大间隔)上的这些点就叫 “支持向量”,我想补充的是为啥这些点就叫 “支持向量” ,因为最后的 classification machine(分类器)的表达式里只含有这些 “支持向量” 的信息,而与其他数据点无关,如下公式:
gs
在这个表达式中,只有支持向量的系数 adas 不等于 0 。
机器学习实战刻意练习-- Task 5 支持向量机_第1张图片
如上图,支持向量就是上面用框框圈住的圆。

我们再补充几个相关的基本概念:

linearly separable (线性可分): 如上图中的两组数据,它们之间已经分的足够开了,因此很容易就可以在图中画出一条直线将两组数据点分开。在这种情况下,这组数据就被称为线性可分数据。

separating hyperplane(分隔超平面): 上述将数据集分隔开来的直线称为分隔超平面。

hyperplane(超平面): 在上面给出的例子中,由于数据点都在二维平面上,所以此时分隔超平面就只是一条直线。但是,如果所给的数据集是三维的,那么此时用来分隔数据的就是一个平面。显而易见,更高纬度的情况可以依此类推。如果数据是 1024 维的,那么就需要一个 1023 维的对象来对数据进行分隔。这个 1023 维的对象被称为超平面,也就是分类的决策边界。分布在超平面一侧的所有数据都属于某个类别,而分布在另一侧的所有数据则属于另一个类别。

margin(间隔): 我们希望能通过上述的方式来构建分类器,即如果数据点离决策边界越远,那么其最后的预测结果也就越可信。既然这样,我们希望找到离分隔超平面最近的点,确保它们离分隔面的距离尽可能远。这里所说的点到分隔面的距离就是"间隔"。我们希望间隔尽可能地大,这是因为如果我们犯错或者在有限数据上训练分类器的话,我们希望分类器尽可能健壮。

支持向量(support vector) : 就是上面所说的离分隔超平面最近的那些点。

分类器 : 分类器就是给定一个样本的数据,判定这个样本属于哪个类别的算法。例如在股票涨跌预测中,我们认为前一天的交易量和收盘价对于第二天的涨跌是有影响的,那么分类器就是通过样本的交易量和收盘价预测第二天的涨跌情况的算法。

特征 : 在分类问题中,输入到分类器中的数据叫做特征。以上面的股票涨跌预测问题为例,特征就是前一天的交易量和收盘价。

线性分类器 : 线性分类器是分类器中的一种,就是判定分类结果的根据是通过特征的线性组合得到的,不能通过特征的非线性运算结果作为判定根据。还以上面的股票涨跌预测问题为例,判断的依据只能是前一天的交易量和收盘价的线性组合,不能将交易量和收盘价进行开方,平方等运算。

SVM算法的原理

可以看看这几个博客都讲得很详细…(其实这一块我也模模糊糊,后期还要再重新学习巩固这一部分)
https://blog.csdn.net/qq_35992440/article/details/80987664
https://blog.csdn.net/b285795298/article/details/81977271
https://blog.csdn.net/c406495762/article/details/78072313

SVM 开发流程

收集数据:可以使用任意方法。
准备数据:需要数值型数据。
分析数据:有助于可视化分隔超平面。
训练算法:SVM的大部分时间都源自训练,该过程主要实现两个参数的调优。
测试算法:十分简单的计算过程就可以实现。
使用算法:几乎所有分类问题都可以使用SVM,值得一提的是,SVM本身是一个二类分类器,对多类问题应用SVM需要对代码做一些修改。

SVM 算法特点

优点:泛化(由具体的、个别的扩大为一般的,就是说:模型训练完后的新样本)错误率低,计算开销不大,结果易理解。
缺点:对参数调节和核函数的选择敏感,原始分类器不加修改仅适合于处理二分类问题。
使用数据类型:数值型和标称型数据。

SMO高效优化算法

序列最小优化(Sequential Minimal Optimization, SMO)

  • SMO目标:求出一系列 alpha 和 b,一旦求出 alpha,就很容易计算出权重向量 w 并得到分隔超平面。
  • SMO思想:是将大优化问题分解为多个小优化问题来求解的。
  • SMO原理:每次循环选择两个 alpha 进行优化处理,一旦找出一对合适的 alpha,那么就增大一个同时减少一个。

这里指的合适必须要符合一定的条件:

  1. 这两个 alpha 必须要在间隔边界之外
  2. 这两个 alpha 还没有进行过区间化处理或者不在边界上。

之所以要同时改变2个 alpha;原因是我们有一个约束条件: (\sum_{i=1}^{m} a_i·label_i=0);如果只是修改一个 alpha,很可能导致约束条件失效。

SMO伪代码如下:

创建一个 alpha 向量并将其初始化为0向量
当迭代次数小于最大迭代次数时(外循环)
    对数据集中的每个数据向量(内循环):
        如果该数据向量可以被优化
            随机选择另外一个数据向量
            同时优化这两个向量
            如果两个向量都不能被优化,退出内循环
    如果所有向量都没被优化,增加迭代数目,继续下一次循环

应用简化版SMO算法处理小规模数据集

# 收集数据
'''
文本文件格式:

3.542485	1.977398	-1
3.018896	2.556416	-1
7.551510	-1.580030	1
2.114999	-0.004466	-1
8.127113	1.274372	1
'''

# 代码实现
# 并建立一个svmMLiA.py文件,将下列代码输进文件中!
from numpy import *
import matplotlib.pyplot as plt

# SMO算法中的辅助函数
def loadDataSet(fileName):
    """
    对文件进行逐行解析,从而得到第行的类标签和整个特征矩阵
    Args:
        fileName 文件名
    Returns:
        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]))
    fr.close()
    return dataMat, labelMat

def selectJrand(i, m):
    """
    随机选择一个整数
    Args:
        i  第一个alpha的下标
        m  所有alpha的数目
    Returns:
        j  返回一个不为i的随机数,在0~m之间的整数值
    """
    j = i
    while j == i:
        j = int(random.uniform(0, m))
    return j

def clipAlpha(aj, H, L):
    """clipAlpha(调整aj的值,使aj处于 L<=aj<=H)
    Args:
        aj  目标值
        H   最大值
        L   最小值
    Returns:
        aj  目标值
    """
    if aj > H:
        aj = H
    if L > aj:
        aj = L
    return aj

# 运行命令
>>> import svmMLiA
>>> dataArr, labelArr = svmMLiA.loadDataSet('testSet.txt')
>>> labelArr
[-1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, 
-1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 
1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0]
>>> 

# 简化版SMO算法
def smoSimple(dataMatIn, classLabels, C, toler, maxIter):
    """smoSimple
    Args:
        dataMatIn    数据集
        classLabels  类别标签
        C   松弛变量(常量值),允许有些数据点可以处于分隔面的错误一侧。
            控制最大化间隔和保证大部分的函数间隔小于1.0这两个目标的权重。
            可以通过调节该参数达到不同的结果。
        toler   容错率(是指在某个体系中能减小一些因素或选择对某个系统产生不稳定的概率。)
        maxIter 退出前最大的循环次数
    Returns:
        b       模型的常量值
        alphas  拉格朗日乘子
    """
    dataMatrix = mat(dataMatIn)
    # 矩阵转置 和 .T 一样的功能
    labelMat = mat(classLabels).transpose()
    m, n = shape(dataMatrix)

    # 初始化 b和alphas(alpha有点类似权重值。)
    b = 0
    alphas = mat(zeros((m, 1)))

    # 没有任何alpha改变的情况下遍历数据的次数
    iter = 0
    while (iter < maxIter):
        # w = calcWs(alphas, dataMatIn, classLabels)
        # print("w:", w)

        # 记录alpha是否已经进行优化,每次循环时设为0,然后再对整个集合顺序遍历
        alphaPairsChanged = 0
        for i in range(m):
            # print 'alphas=', alphas
            # print 'labelMat=', labelMat
            # print 'multiply(alphas, labelMat)=', multiply(alphas, labelMat)
            # 我们预测的类别 y = w^Tx[i]+b; 其中因为 w = Σ(1~n) a[n]*lable[n]*x[n]
            fXi = float(multiply(alphas, labelMat).T*(dataMatrix*dataMatrix[i, :].T)) + b
            # 预测结果与真实结果比对,计算误差Ei
            Ei = fXi - float(labelMat[i])

            # 约束条件 (KKT条件是解决最优化问题的时用到的一种方法。我们这里提到的最优化问题通常是指对于给定的某一函数,求其在指定作用域上的全局最小值)
            # 0<=alphas[i]<=C,但由于0和C是边界值,我们无法进行优化,因为需要增加一个alphas和降低一个alphas。
            # 表示发生错误的概率:labelMat[i]*Ei 如果超出了 toler, 才需要优化。至于正负号,我们考虑绝对值就对了。
            '''
            # 检验训练样本(xi, yi)是否满足KKT条件
            yi*f(i) >= 1 and alpha = 0 (outside the boundary)
            yi*f(i) == 1 and 0
            if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)):

                # 如果满足优化的条件,我们就随机选取非i的一个点,进行优化比较
                j = selectJrand(i, m)
                # 预测j的结果
                fXj = float(multiply(alphas, labelMat).T*(dataMatrix*dataMatrix[j, :].T)) + b
                Ej = fXj - float(labelMat[j])
                alphaIold = alphas[i].copy()
                alphaJold = alphas[j].copy()

                # L和H用于将alphas[j]调整到0-C之间。如果L==H,就不做任何改变,直接执行continue语句
                # labelMat[i] != labelMat[j] 表示异侧,就相减,否则是同侧,就相加。
                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

                # eta是alphas[j]的最优修改量,如果eta==0,需要退出for循环的当前迭代过程
                # 参考《统计学习方法》李航-P125~P128<序列最小最优化算法>
                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

                # 计算出一个新的alphas[j]值
                alphas[j] -= labelMat[j]*(Ei - Ej)/eta
                # 并使用辅助函数,以及L和H对其进行调整
                alphas[j] = clipAlpha(alphas[j], H, L)
                # 检查alpha[j]是否只是轻微的改变,如果是的话,就退出for循环。
                if (abs(alphas[j] - alphaJold) < 0.00001):
                    print("j not moving enough")
                    continue
                # 然后alphas[i]和alphas[j]同样进行改变,虽然改变的大小一样,但是改变的方向正好相反
                alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])
                # 在对alpha[i], alpha[j] 进行优化之后,给这两个alpha值设置一个常数b。
                # w= Σ[1~n] ai*yi*xi => b = yj- Σ[1~n] ai*yi(xi*xj)
                # 所以:  b1 - b = (y1-y) - Σ[1~n] yi*(a1-a)*(xi*x1)
                # 为什么减2遍? 因为是 减去Σ[1~n],正好2个变量i和j,所以减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
                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("iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))
        # 在for循环外,检查alpha值是否做了更新,如果在更新则将iter设为0后继续运行程序
        # 知道更新完毕后,iter次循环无变化,才推出循环。
        if (alphaPairsChanged == 0):
            iter += 1
        else:
            iter = 0
        print("iteration number: %d" % iter)
    return b, alphas
    
# 运行命令
>>> import svmMLiA
>>> dataArr, labelArr = svmMLiA.loadDataSet('testSet.txt')
>>> b, alphas = svmMLiA.smoSimple(dataArr, labelArr, 0.6, 0.001, 40)
# 输出类似如下结果:(实际上很长很长,等他输出完得花半分钟多)
...
j not moving enough
iteration number: 8
iter: 8 i:29, pairs changed 1
iteration number: 0
...
# 上述运行过程需要一段时间才能收敛。运行结束之后我们可以对其进行观察。
>>> b
matrix([[-3.83803613]])
# 我们可以直接观察alpha矩阵本身,但是其中零元素太多。为了观察大于0的元素的数量,如下运行命令:
>>> alphas[alphas>0]
matrix([[0.12746714, 0.24133515, 0.36880229]])
# 为了得到支持向量的个数,输入:
>>> shape(alphas[alphas>0])
(1, 3)
# 为了了解哪些数据点是支持向量,输入:
>>> for i in range(100):
	if alphas[i] > 0.0:
		print(dataArr[i], labelArr[i])

		
[4.658191, 3.507396] -1.0
[3.457096, -0.082216] -1.0
[6.080573, 0.418886] 1.0
>>> 

# 在原始数据集上对这些支持向量画圈(作图)
def calcWs(alphas, dataArr, classLabels):
    """
    基于alpha计算w值
    Args:
        alphas        拉格朗日乘子
        dataArr       feature数据集
        classLabels   目标变量数据集
    Returns:
        wc  回归系数
    """
    X = mat(dataArr)
    labelMat = mat(classLabels).transpose()
    m, n = shape(X)
    w = zeros((n, 1))
    for i in range(m):
        w += multiply(alphas[i] * labelMat[i], X[i, :].T)
    return w
    
def plotfig_SVM(xMat, yMat, ws, b, alphas):
    """
    参考地址:
       http://blog.csdn.net/maoersong/article/details/24315633
       http://www.cnblogs.com/JustForCS/p/5283489.html
       http://blog.csdn.net/kkxgx/article/details/6951959
    """

    xMat = mat(xMat)
    yMat = mat(yMat)

    # b原来是矩阵,先转为数组类型后其数组大小为(1,1),所以后面加[0],变为(1,)
    b = array(b)[0]
    fig = plt.figure()
    ax = fig.add_subplot(111)

    # 注意flatten的用法
    ax.scatter(xMat[:, 0].flatten().A[0], xMat[:, 1].flatten().A[0])

    # x最大值,最小值根据原数据集dataArr[:, 0]的大小而定
    x = arange(-1.0, 10.0, 0.1)

    # 根据x.w + b = 0 得到,其式子展开为w0.x1 + w1.x2 + b = 0, x2就是y值
    y = (-b-ws[0, 0]*x)/ws[1, 0]
    ax.plot(x, y)

    for i in range(shape(yMat[0, :])[1]):
        if yMat[0, i] > 0:
            ax.plot(xMat[i, 0], xMat[i, 1], 'cx')
        else:
            ax.plot(xMat[i, 0], xMat[i, 1], 'kp')

    # 找到支持向量,并在图中标红
    for i in range(100):
        if alphas[i] > 0.0:
            ax.plot(xMat[i, 0], xMat[i, 1], 'ro')
    plt.show()

# 运行命令
>>> svmMLiA.calcWs(alphas, dataArr, labelArr)
array([[ 0.8143644 ],
       [-0.27233233]])
>>> ws = calcWs(alphas, dataArr, labelArr)
>>> plotfig_SVM(dataArr, labelArr, ws, b, alphas)
>>> 
# 图像如下:

机器学习实战刻意练习-- Task 5 支持向量机_第2张图片

利用完整 Platt SMO 算法加速优化

  • 在优化过程中,唯一的不同就是选择alpha的方式。
  • Platt SMO算法是通过一个外循环来选择第一个alpha值的,并且其选择过程会在两种方式之间进行交替:一种方式是在所有数据集上进行单遍扫描,另一种方式则是在非边界alpha中实现单遍扫描。而所谓非边界alpha指的就是那些不等于边界0或C的alpha值。
  • 对整个数据集的扫描相当容易,而实现非边界alpha值的扫描时,首先需要建立这些alpha值的列表,然后再对这个表进行遍历。同时,该步骤会跳过那些已知的不会改变的alpha值。 在选择第一个alpha值后,算法会通过一个内循环来选择第二个alpha值。
  • 在优化过程中,会通过最大化步长的方式来获得第二个alpha值。在简化版SMO算法中,我们会在选择 j 之后计算错误率Ej。但在这里,我们会建立一个全局的缓存用于保存误差值,并从中选择使得步长或者说 Ei - Ej 大的alpha值。
# 利用完整 Platt SMO 算法加速优化
# 下面的程序中包含1个用于清理代码的数据结构和3个用于对E进行缓存的辅助函数。
# 将其输入进文件中。

# 误差缓存
class optStruct:
    def __init__(self, dataMatIn, classLabels, C, toler):  # Initialize the structure with the parameters
        self.X = dataMatIn
        self.labelMat = classLabels
        self.C = C
        self.tol = toler
        self.m = shape(dataMatIn)[0]
        self.alphas = mat(zeros((self.m, 1)))
        self.b = 0
        self.eCache = mat(zeros((self.m, 2)))  # first column is valid flag

# 内循环的启发式方法
def calcEk(oS, k):
    """calcEk(求 Ek误差:预测值-真实值的差)
    该过程在完整版的SMO算法中陪出现次数较多,因此将其单独作为一个方法
    Args:
        oS  optStruct对象
        k   具体的某一行
    Returns:
        Ek  预测结果与真实结果比对,计算误差Ek
    """
    fXk = float(multiply(oS.alphas, oS.labelMat).T * (oS.X * oS.X[k, :].T)) + oS.b
    Ek = fXk - float(oS.labelMat[k])
    return Ek

# 选择具有最大的步长的 j
def selectJ(i, oS, Ei):  # this is the second choice -heurstic, and calcs Ej
    """selectJ(返回最优的j和Ej)
    内循环的启发式方法。
    选择第二个(内循环)alpha的alpha值
    这里的目标是选择合适的第二个alpha值以保证每次优化中采用最大步长。
    该函数的误差与第一个alpha值Ei和下标i有关。
    Args:
        i   具体的第i一行
        oS  optStruct对象
        Ei  预测结果与真实结果比对,计算误差Ei
    Returns:
        j  随机选出的第j一行
        Ej 预测结果与真实结果比对,计算误差Ej
    """
    maxK = -1
    maxDeltaE = 0
    Ej = 0
    # 首先将输入值Ei在缓存中设置成为有效的。这里的有效意味着它已经计算好了。
    oS.eCache[i] = [1, Ei]

    # print 'oS.eCache[%s]=%s' % (i, oS.eCache[i])
    # print 'oS.eCache[:, 0].A=%s' % oS.eCache[:, 0].A.T
    # """
    # # 返回非0的:行列值
    # nonzero(oS.eCache[:, 0].A)= (
    #     行: array([ 0,  2,  4,  5,  8, 10, 17, 18, 20, 21, 23, 25, 26, 29, 30, 39, 46,52, 54, 55, 62, 69, 70, 76, 79, 82, 94, 97]), 
    #     列: array([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])
    # )
    # """
    # print 'nonzero(oS.eCache[:, 0].A)=', nonzero(oS.eCache[:, 0].A)
    # # 取行的list
    # print 'nonzero(oS.eCache[:, 0].A)[0]=', nonzero(oS.eCache[:, 0].A)[0]
    # 非零E值的行的list列表,所对应的alpha值
    validEcacheList = nonzero(oS.eCache[:, 0].A)[0]
    if (len(validEcacheList)) > 1:
        for k in validEcacheList:  # 在所有的值上进行循环,并选择其中使得改变最大的那个值
            if k == i:
                continue  # don't calc for i, waste of time

            # 求 Ek误差:预测值-真实值的差
            Ek = calcEk(oS, k)
            deltaE = abs(Ei - Ek)
            if (deltaE > maxDeltaE):
                maxK = k
                maxDeltaE = deltaE
                Ej = Ek
        return maxK, Ej
    else:  # 如果是第一次循环,则随机选择一个alpha值
        j = selectJrand(i, oS.m)

        # 求 Ek误差:预测值-真实值的差
        Ej = calcEk(oS, j)
    return j, Ej

def updateEk(oS, k):  # after any alpha has changed update the new value in the cache
    """updateEk(计算误差值并存入缓存中。)
    在对alpha值进行优化之后会用到这个值。
    Args:
        oS  optStruct对象
        k   某一列的行号
    """

    # 求 误差:预测值-真实值的差
    Ek = calcEk(oS, k)
    oS.eCache[k] = [1, Ek]

# 完整的 Platt SMO 算法中的优化例程
def innerL(i, oS):
    """innerL
    内循环代码
    Args:
        i   具体的某一行
        oS  optStruct对象
    Returns:
        0   找不到最优的值
        1   找到了最优的值,并且oS.Cache到缓存中
    """

    # 求 Ek误差:预测值-真实值的差
    Ei = calcEk(oS, i)

    # 约束条件 (KKT条件是解决最优化问题的时用到的一种方法。我们这里提到的最优化问题通常是指对于给定的某一函数,求其在指定作用域上的全局最小值)
    # 0<=alphas[i]<=C,但由于0和C是边界值,我们无法进行优化,因为需要增加一个alphas和降低一个alphas。
    # 表示发生错误的概率:labelMat[i]*Ei 如果超出了 toler, 才需要优化。至于正负号,我们考虑绝对值就对了。
    '''
    # 检验训练样本(xi, yi)是否满足KKT条件
    yi*f(i) >= 1 and alpha = 0 (outside the boundary)
    yi*f(i) == 1 and 0
    if ((oS.labelMat[i] * Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i] * Ei > oS.tol) and (oS.alphas[i] > 0)):
        # 选择最大的误差对应的j进行优化。效果更明显
        j, Ej = selectJ(i, oS, Ei)
        alphaIold = oS.alphas[i].copy()
        alphaJold = oS.alphas[j].copy()

        # L和H用于将alphas[j]调整到0-C之间。如果L==H,就不做任何改变,直接return 0
        if (oS.labelMat[i] != oS.labelMat[j]):
            L = max(0, oS.alphas[j] - oS.alphas[i])
            H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
        else:
            L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
            H = min(oS.C, oS.alphas[j] + oS.alphas[i])
        if L == H:
            print("L==H")
            return 0

        # eta是alphas[j]的最优修改量,如果eta==0,需要退出for循环的当前迭代过程
        # 参考《统计学习方法》李航-P125~P128<序列最小最优化算法>
        eta = 2.0 * oS.X[i, :] * oS.X[j, :].T - oS.X[i, :] * oS.X[i, :].T - oS.X[j, :] * oS.X[j, :].T
        if eta >= 0:
            print("eta>=0")
            return 0

        # 计算出一个新的alphas[j]值
        oS.alphas[j] -= oS.labelMat[j] * (Ei - Ej) / eta
        # 并使用辅助函数,以及L和H对其进行调整
        oS.alphas[j] = clipAlpha(oS.alphas[j], H, L)
        # 更新误差缓存
        updateEk(oS, j)

        # 检查alpha[j]是否只是轻微的改变,如果是的话,就退出for循环。
        if (abs(oS.alphas[j] - alphaJold) < 0.00001):
            print("j not moving enough")
            return 0

        # 然后alphas[i]和alphas[j]同样进行改变,虽然改变的大小一样,但是改变的方向正好相反
        oS.alphas[i] += oS.labelMat[j] * oS.labelMat[i] * (alphaJold - oS.alphas[j])
        # 更新误差缓存
        updateEk(oS, i)

        # 在对alpha[i], alpha[j] 进行优化之后,给这两个alpha值设置一个常数b。
        # w= Σ[1~n] ai*yi*xi => b = yj Σ[1~n] ai*yi(xi*xj)
        # 所以:  b1 - b = (y1-y) - Σ[1~n] yi*(a1-a)*(xi*x1)
        # 为什么减2遍? 因为是 减去Σ[1~n],正好2个变量i和j,所以减2遍
        b1 = oS.b - Ei - oS.labelMat[i] * (oS.alphas[i] - alphaIold) * oS.X[i, :] * oS.X[i, :].T - oS.labelMat[j] * (oS.alphas[j] - alphaJold) * oS.X[i, :] * oS.X[j, :].T
        b2 = oS.b - Ej - oS.labelMat[i] * (oS.alphas[i] - alphaIold) * oS.X[i, :] * oS.X[j, :].T - oS.labelMat[j] * (oS.alphas[j] - alphaJold) * oS.X[j, :] * oS.X[j, :].T
        if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]):
            oS.b = b1
        elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]):
            oS.b = b2
        else:
            oS.b = (b1 + b2) / 2.0
        return 1
    else:
        return 0

# 完整版 Platt SMO 的外循环代码
def smoP(dataMatIn, classLabels, C, toler, maxIter):
    """
    完整SMO算法外循环,与smoSimple有些类似,但这里的循环退出条件更多一些
    Args:
        dataMatIn    数据集
        classLabels  类别标签
        C   松弛变量(常量值),允许有些数据点可以处于分隔面的错误一侧。
            控制最大化间隔和保证大部分的函数间隔小于1.0这两个目标的权重。
            可以通过调节该参数达到不同的结果。
        toler   容错率
        maxIter 退出前最大的循环次数
    Returns:
        b       模型的常量值
        alphas  拉格朗日乘子
    """

    # 创建一个 optStruct 对象
    oS = optStruct(mat(dataMatIn), mat(classLabels).transpose(), C, toler)
    iter = 0
    entireSet = True
    alphaPairsChanged = 0

    # 循环遍历:循环maxIter次 并且 (alphaPairsChanged存在可以改变 or 所有行遍历一遍)
    # 循环迭代结束 或者 循环遍历所有alpha后,alphaPairs还是没变化
    while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
        alphaPairsChanged = 0

        #  当entireSet=true or 非边界alpha对没有了;就开始寻找 alpha对,然后决定是否要进行else。
        if entireSet:
            # 在数据集上遍历所有可能的alpha
            for i in range(oS.m):
                # 是否存在alpha对,存在就+1
                alphaPairsChanged += innerL(i, oS)
                print("fullSet, iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))
            iter += 1
        # 对已存在 alpha对,选出非边界的alpha值,进行优化。
        else:
            # 遍历所有的非边界alpha值,也就是不在边界0或C上的值。
            nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
            for i in nonBoundIs:
                alphaPairsChanged += innerL(i, oS)
                print("non-bound, iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))
            iter += 1

        # 如果找到alpha对,就优化非边界alpha值,否则,就重新进行寻找,如果寻找一遍 遍历所有的行还是没找到,就退出循环。
        if entireSet:
            entireSet = False  # toggle entire set loop
        elif (alphaPairsChanged == 0):
            entireSet = True
        print("iteration number: %d" % iter)
    return oS.b, oS.alphas

# 运行命令
>>> import svmMLiA
>>> dataArr, labelArr = svmMLiA.loadDataSet('testSet.txt')
>>> b, alphas = svmMLiA.smoP(dataArr, labelArr, 0.6, 0.001, 40)
# 输出类似以下程序:
...
non-bound, iter: 2 i:17, pairs changed 0
non-bound, iter: 2 i:23, pairs changed 0
j not moving enough
non-bound, iter: 2 i:54, pairs changed 0
non-bound, iter: 2 i:55, pairs changed 0
iteration number: 3
j not moving enough
fullSet, iter: 3 i:0, pairs changed 0
fullSet, iter: 3 i:1, pairs changed 0
fullSet, iter: 3 i:2, pairs changed 0
...
# 做出图像比较
>>> import svmMLiA
>>> dataArr, labelArr = svmMLiA.loadDataSet('testSet.txt')
>>> b, alphas = svmMLiA.smoP(dataArr, labelArr, 0.6, 0.001, 40)
iteration number: 1
iteration number: 2
iteration number: 3
>>> ws = calcWs(alphas, dataArr, labelArr)
>>> plotfig_SVM(dataArr, labelArr, ws, b, alphas)
>>> 

机器学习实战刻意练习-- Task 5 支持向量机_第3张图片

# 刚才我们花了大量的时间来计算那些alpha值,但是如何利用它们进行分类呢?
# 我们首先必须基于alpha值得到超平面,这也包括了 w 的运算。
# 下面的这一个小函数是用来实现上述任务的:
def calcWs(alphas, dataArr, classLabels):
    """
    基于alpha计算w值
    Args:
        alphas        拉格朗日乘子
        dataArr       feature数据集
        classLabels   目标变量数据集
    Returns:
        wc  回归系数
    """
    X = mat(dataArr)
    labelMat = mat(classLabels).transpose()
    m, n = shape(X)
    w = zeros((n, 1))
    for i in range(m):
        w += multiply(alphas[i] * labelMat[i], X[i, :].T)
    return w

# 运行下列命令:
>>> import svmMLiA
>>> dataArr, labelArr = svmMLiA.loadDataSet('testSet.txt')
>>> b, alphas = svmMLiA.smoP(dataArr, labelArr, 0.6, 0.001, 40)
# 输出类似下列程序:\
...
L==H
fullSet, iter: 0 i:3, pairs changed 1
fullSet, iter: 0 i:4, pairs changed 2
fullSet, iter: 0 i:5, pairs changed 2
fullSet, iter: 0 i:6, pairs changed 2
j not moving enough
fullSet, iter: 0 i:7, pairs changed 2
L==H
fullSet, iter: 0 i:8, pairs changed 2
fullSet, iter: 0 i:9, pairs changed 2
...
>>> ws = svmMLiA.calcWs(alphas, dataArr, labelArr)
>>> ws
array([[ 0.65307162],
       [-0.17196128]])
# 对数据进行分类处理,比如对第一个数据点分类:
>>> datMat = mat(dataArr)
>>> datMat[0] * mat(ws) + b
matrix([[-0.92555695]])
# 如果该值大于0,则其属于1类;如果该值小于0,则其属于-1类;
# 对于数据点0,则得到的类别标签应该是-1。
# 下面检查其分类结果的正确性:
>>> labelArr[0]
-1.0
>>> datMat[2] * mat(ws) + b
matrix([[2.30436336]])
>>> labelArr[2]
1.0
>>> datMat[1] * mat(ws) + b
matrix([[-1.36706674]])
>>> labelArr[1]
-1.0

在复杂数据上应用核函数

  • 对于线性可分的情况,效果明显
  • 下面我们要使用一种称为“核函数”(kernel)的工具将数据转化成易于分类器理解的形式

利用核函数将数据映射到高维空间

  • 使用核函数:可以将数据从某个特征空间到另一个特征空间的映射。(通常情况下:这种映射会将低维特征空间映射到高维空间。)
  • 可以把核函数想象成一个包装器(wrapper)或者是接口(interface),它能将数据从某个很难处理的形式转换成为另一个较容易处理的形式。
  • 经过空间转换后:低维需要解决的非线性问题,就变成了高维需要解决的线性问题。
  • SVM 优化特别好的地方,在于所有的运算都可以写成内积(inner product: 是指2个向量相乘,得到单个标量 或者数值);内积替换成核函数的方式被称为核技巧(kernel trick)或者核"变电"(kernel substation)
  • 核函数并不仅仅应用于支持向量机,很多其他的机器学习算法也都用到核函数。最流行的核函数:径向基函数(radial basis function)

径向基核函数

  • 径向基函数是 SVM 中常用的一个核函数。径向基函数是一个采用向量作为自变量的函数,能够基于向量距离运算输出一个标量。这个距离可以是从<0,0>向量或者其他向量开始计算的距离。
  • 接下来,我们将会使用到径向基函数的高斯版本,其具体公式为:
    sssss
    其中,aaaaa是用户定义的用于确定到达率(reach)或者说函数值跌落到0的速度参数。
  • 上述高斯核函数将数据从其特征空间映射到更高维的空间,具体来说这里是映射到一个无穷维的空间。
  • 高斯核函数只是一个常用的核函数,使用者并不需要确切地理解数据到底是如何表现的,而且使用高斯核函数还会得到一个理想的结果。
  • 在上面的例子中,数据点基本上都在一个圆内。对于这个例子,我们可以直接检查原始数据,并意识到只要度量数据点到圆心的距离即可。然而,如果碰到了一个不是这种形式的新数据集,那么我们就会陷入困境。在该数据集上,使用高斯核函数可以得到很好的结果。当然,该函数也可以用于许多其他的数据集,并且也能得到低错误率的结果。
  • 下面我们进行代码的实现:
# 首先我们在 svmMLiA.py 文件中添加函数并进行修改,那么我们就可以在已有代码中使用核函数
# 首先在文件中加入 kernelTrans() 函数,然后对 optStruct 类进行修改:
class optStruct:
    """
    建立的数据结构来保存所有的重要值
    """
    def __init__(self, dataMatIn, classLabels, C, toler, kTup):
        """
        Args:
            dataMatIn    数据集
            classLabels  类别标签
            C   松弛变量(常量值),允许有些数据点可以处于分隔面的错误一侧。
                控制最大化间隔和保证大部分的函数间隔小于1.0这两个目标的权重。
                可以通过调节该参数达到不同的结果。
            toler   容错率
            kTup    包含核函数信息的元组
        """

        self.X = dataMatIn
        self.labelMat = classLabels
        self.C = C
        self.tol = toler

        # 数据的行数
        self.m = shape(dataMatIn)[0]
        self.alphas = mat(zeros((self.m, 1)))
        self.b = 0

        # 误差缓存,第一列给出的是eCache是否有效的标志位,第二列给出的是实际的E值。
        self.eCache = mat(zeros((self.m, 2)))

        # m行m列的矩阵
        self.K = mat(zeros((self.m, self.m)))
        for i in range(self.m):
            self.K[:, i] = kernelTrans(self.X, self.X[i, :], kTup)

# 核转换函数
def kernelTrans(X, A, kTup):  # calc the kernel or transform data to a higher dimensional space
    """
    核转换函数
    Args:
        X     dataMatIn数据集
        A     dataMatIn数据集的第i行的数据
        kTup  核函数的信息
    Returns:
    """
    m, n = shape(X)
    K = mat(zeros((m, 1)))
    if kTup[0] == 'lin':
        # linear kernel:   m*n * n*1 = m*1
        K = X * A.T
    elif kTup[0] == 'rbf':
        for j in range(m):
            deltaRow = X[j, :] - A
            K[j] = deltaRow * deltaRow.T
        # 径向基函数的高斯版本
        K = exp(K / (-1 * kTup[1] ** 2))  # divide in NumPy is element-wise not matrix like Matlab
    else:
        raise NameError('Houston We Have a Problem -- That Kernel is not recognized')
    return K

# 为了使用核函数,先期的两个函数 innerL() 和 calcEk() 的代码需要修改,如下:
def innerL(i, oS):
    """innerL
    内循环代码
    Args:
        i   具体的某一行
        oS  optStruct对象
    Returns:
        0   找不到最优的值
        1   找到了最优的值,并且oS.Cache到缓存中
    """

    # 求 Ek误差:预测值-真实值的差
    Ei = calcEk(oS, i)

    # 约束条件 (KKT条件是解决最优化问题的时用到的一种方法。我们这里提到的最优化问题通常是指对于给定的某一函数,求其在指定作用域上的全局最小值)
    # 0<=alphas[i]<=C,但由于0和C是边界值,我们无法进行优化,因为需要增加一个alphas和降低一个alphas。
    # 表示发生错误的概率:labelMat[i]*Ei 如果超出了 toler, 才需要优化。至于正负号,我们考虑绝对值就对了。
    '''
    # 检验训练样本(xi, yi)是否满足KKT条件
    yi*f(i) >= 1 and alpha = 0 (outside the boundary)
    yi*f(i) == 1 and 0
    if ((oS.labelMat[i] * Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i] * Ei > oS.tol) and (oS.alphas[i] > 0)):
        # 选择最大的误差对应的j进行优化。效果更明显
        j, Ej = selectJ(i, oS, Ei)
        alphaIold = oS.alphas[i].copy()
        alphaJold = oS.alphas[j].copy()

        # L和H用于将alphas[j]调整到0-C之间。如果L==H,就不做任何改变,直接return 0
        if (oS.labelMat[i] != oS.labelMat[j]):
            L = max(0, oS.alphas[j] - oS.alphas[i])
            H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])
        else:
            L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)
            H = min(oS.C, oS.alphas[j] + oS.alphas[i])
        if L == H:
            # print("L==H")
            return 0

        # eta是alphas[j]的最优修改量,如果eta==0,需要退出for循环的当前迭代过程
        # 参考《统计学习方法》李航-P125~P128<序列最小最优化算法>
        eta = 2.0 * oS.K[i, j] - oS.K[i, i] - oS.K[j, j]  # changed for kernel
        if eta >= 0:
            print("eta>=0")
            return 0

        # 计算出一个新的alphas[j]值
        oS.alphas[j] -= oS.labelMat[j] * (Ei - Ej) / eta
        # 并使用辅助函数,以及L和H对其进行调整
        oS.alphas[j] = clipAlpha(oS.alphas[j], H, L)
        # 更新误差缓存
        updateEk(oS, j)

        # 检查alpha[j]是否只是轻微的改变,如果是的话,就退出for循环。
        if (abs(oS.alphas[j] - alphaJold) < 0.00001):
            # print("j not moving enough")
            return 0

        # 然后alphas[i]和alphas[j]同样进行改变,虽然改变的大小一样,但是改变的方向正好相反
        oS.alphas[i] += oS.labelMat[j] * oS.labelMat[i] * (alphaJold - oS.alphas[j])
        # 更新误差缓存
        updateEk(oS, i)

        # 在对alpha[i], alpha[j] 进行优化之后,给这两个alpha值设置一个常数b。
        # w= Σ[1~n] ai*yi*xi => b = yi- Σ[1~n] ai*yi(xi*xj)
        # 所以:  b1 - b = (y1-y) - Σ[1~n] yi*(a1-a)*(xi*x1)
        # 为什么减2遍? 因为是 减去Σ[1~n],正好2个变量i和j,所以减2遍
        b1 = oS.b - Ei - oS.labelMat[i] * (oS.alphas[i] - alphaIold) * oS.K[i, i] - oS.labelMat[j] * (oS.alphas[j] - alphaJold) * oS.K[i, j]
        b2 = oS.b - Ej - oS.labelMat[i] * (oS.alphas[i] - alphaIold) * oS.K[i, j] - oS.labelMat[j] * (oS.alphas[j] - alphaJold) * oS.K[j, j]
        if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]):
            oS.b = b1
        elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]):
            oS.b = b2
        else:
            oS.b = (b1 + b2) / 2.0
        return 1
    else:
        return 0

def calcEk(oS, k):
    """calcEk(求 Ek误差:预测值-真实值的差)
    该过程在完整版的SMO算法中陪出现次数较多,因此将其单独作为一个方法
    Args:
        oS  optStruct对象
        k   具体的某一行
    Returns:
        Ek  预测结果与真实结果比对,计算误差Ek
    """
    fXk = float(multiply(oS.alphas, oS.labelMat).T * oS.K[:, k] + oS.b)
    Ek = fXk - float(oS.labelMat[k])
    return Ek

在测试中使用核函数

  • 下面我们将构建一个对下图的数据点进行有效分类的分类器,该分类器使用了径向基核函数。

机器学习实战刻意练习-- Task 5 支持向量机_第4张图片

  • 首先,我们需要确定它的大小,然后利用该核函数构建出一个分类器。
  • 下面为测试函数,也加入到文件中:
# 利用核函数进行分类的径向基测试函数
def testRbf(k1=1.3):
    dataArr, labelArr = loadDataSet('testSetRBF.txt')
    b, alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, ('rbf', k1))  # C=200 important
    datMat = mat(dataArr)
    labelMat = mat(labelArr).transpose()
    svInd = nonzero(alphas.A > 0)[0]
    sVs = datMat[svInd]  # get matrix of only support vectors
    labelSV = labelMat[svInd]
    print("there are %d Support Vectors" % shape(sVs)[0])
    m, n = shape(datMat)
    errorCount = 0
    for i in range(m):
        kernelEval = kernelTrans(sVs, datMat[i, :], ('rbf', k1))

        # 和这个svm-simple类似: fXi = float(multiply(alphas, labelMat).T*(dataMatrix*dataMatrix[i, :].T)) + b
        predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
        if sign(predict) != sign(labelArr[i]):
            errorCount += 1
    print("the training error rate is: %f" % (float(errorCount) / m))

    dataArr, labelArr = loadDataSet('testSetRBF2.txt')
    errorCount = 0
    datMat = mat(dataArr)
    labelMat = mat(labelArr).transpose()
    m, n = shape(datMat)
    for i in range(m):
        kernelEval = kernelTrans(sVs, datMat[i, :], ('rbf', k1))
        predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
        if sign(predict) != sign(labelArr[i]):
            errorCount += 1
    print("the test error rate is: %f" % (float(errorCount) / m))
    
'''
上述代码只有一个可选的输入参数,该输入参数是高斯径向基函数中的一个用户定义变量。整个代码主要是由以前定义的函数集合构成的。

首先,程序从文件中读入数据集,然后在该数据集上运行Platt SMO算法,其中核函数的类型为'rbf'。 

优化过程结束后,在后面的矩阵数学运算中建立了数据的矩阵副本,并且找出那些非零alpha值,从而得到所需要的支持向量;
同时,也就得到了这些支持向量和alpha的类别标签值。
这些值仅仅是需要分类的值。
 
整个代码中最重要的是for循环开始的那两行,它们给出了如何利用核函数进行分类。
首先利用结构初始化方法中使用过的kernelTrans()函数,得到转换后的数据。
然后,再用其与前面的alpha及类别标签值求积。
其中需要特别注意的另一件事是,在这几行代码中,是如何做到只需要支持向量数据就可以进行分类的。除此之外,其他数据都可以直接舍弃。 

与第一个for循环相比,第二个for循环仅仅只有数据集不同,后者采用的是测试数据集。
读者可以比较不同的设置在测试集和训练集上表现出的性能。
'''
# 运行命令:
>>> import svmMLiA
>>> import importlib
>>> importlib.reload(svmMLiA)
<module 'svmMLiA' from 'C:\\Users\\dell\\Desktop\\机器学习实战资料\\第6章 支持向量机\\svmMLiA.py'>
>>> svmMLiA.testRbf()
iteration number: 1
iteration number: 2
iteration number: 3
iteration number: 4
iteration number: 5
iteration number: 6
iteration number: 7
iteration number: 8
there are 17 Support Vectors
the training error rate is: 0.030000
the test error rate is: 0.040000

# 你可以尝试更换不同的k1参数以观察测试错误率、训练错误率、支持向量个数随k1的变化情况。
>>> import svmMLiA
>>> dataArr, labelArr = loadDataSet('testSet.txt')
>>> b, alphas = smoP(dataArr, labelArr, 0.6, 0.001, 40)	    
iteration number: 1
iteration number: 2
iteration number: 3
iteration number: 4  
>>> svmMLiA.testRbf()	    
iteration number: 1
iteration number: 2
iteration number: 3
iteration number: 4
iteration number: 5
iteration number: 6
there are 26 Support Vectors
the training error rate is: 0.090000
the test error rate is: 0.180000			    
>>> testRbf(0.8)			    
iteration number: 1
iteration number: 2
iteration number: 3
there are 16 Support Vectors
the training error rate is: 0.000000
the test error rate is: 0.080000
  • 支持向量的数目存在一个最优值。SVM的优点在于它能对数据进行高效分类。如果支持向量太少,就可能会得到一个很差的决策边界(下个例子会说明这一点);如果支持向量太多,也就相当于每次都利用整个数据集进行分类,这种分类方法称为k近邻。
  • 我们可以对SMO算法中的其他设置进行随意地修改或者建立新的核函数。
  • 接下来,我们将在一个更大的数据上应用支持向量机,并与以前介绍的一个分类器进行对比。

项目实例:手写数字识别的优化(有核函数)

项目概述

你的老板要求:你写的那个手写识别程序非常好,但是它占用内存太大。顾客无法通过无线的方式下载我们的应用。所以:我们可以考虑使用支持向量机,保留支持向量就行(knn需要保留所有的向量),就可以获得非常好的效果。

开发流程
# 1)收集数据:提供的文本文件
00000000000000001111000000000000
00000000000000011111111000000000
00000000000000011111111100000000
00000000000000011111111110000000
00000000000000011111111110000000
00000000000000111111111100000000
00000000000000111111111100000000
00000000000001111111111100000000
00000000000000111111111100000000
00000000000000111111111100000000
00000000000000111111111000000000
00000000000001111111111000000000
00000000000011111111111000000000
00000000000111111111110000000000
00000000001111111111111000000000
00000001111111111111111000000000
00000011111111111111110000000000
00000111111111111111110000000000
00000111111111111111110000000000
00000001111111111111110000000000
00000001111111011111110000000000
00000000111100011111110000000000
00000000000000011111110000000000
00000000000000011111100000000000
00000000000000111111110000000000
00000000000000011111110000000000
00000000000000011111110000000000
00000000000000011111111000000000
00000000000000011111111000000000
00000000000000011111111000000000
00000000000000000111111110000000
00000000000000000111111100000000

# 2)准备数据:基于二值图像构造向量
# 将 32*32的文本转化为 1*1024的矩阵
# 我们将第二章 knn.py 中的 img2vector() 函数复制过来
def img2vector(filename):
    returnVect = zeros((1, 1024))
    fr = open(filename)
    for i in range(32):
        lineStr = fr.readline()
        for j in range(32):
            returnVect[0, 32 * i + j] = int(lineStr[j])
    return returnVect

# 基于SVM的手写数字识别
def loadImages(dirName):
    from os import listdir
    hwLabels = []
    print(dirName)
    trainingFileList = listdir(dirName)  # load the training set
    m = len(trainingFileList)
    trainingMat = zeros((m, 1024))
    for i in range(m):
        fileNameStr = trainingFileList[i]
        fileStr = fileNameStr.split('.')[0]  # take off .txt
        classNumStr = int(fileStr.split('_')[0])
        if classNumStr == 9:
            hwLabels.append(-1)
        else:
            hwLabels.append(1)
        trainingMat[i, :] = img2vector('%s/%s' % (dirName, fileNameStr))
    return trainingMat, hwLabels

# 3)分析数据:对图像向量进行目测
# 4)训练算法:采用两种不同的核函数,并对径向基核函数采用不同的设置来运行SMO算法
def kernelTrans(X, A, kTup):  # calc the kernel or transform data to a higher dimensional space
    """
    核转换函数
    Args:
        X     dataMatIn数据集
        A     dataMatIn数据集的第i行的数据
        kTup  核函数的信息

    Returns:

    """
    m, n = shape(X)
    K = mat(zeros((m, 1)))
    if kTup[0] == 'lin':
        # linear kernel:   m*n * n*1 = m*1
        K = X * A.T
    elif kTup[0] == 'rbf':
        for j in range(m):
            deltaRow = X[j, :] - A
            K[j] = deltaRow * deltaRow.T
        # 径向基函数的高斯版本
        K = exp(K / (-1 * kTup[1] ** 2))  # divide in NumPy is element-wise not matrix like Matlab
    else:
        raise NameError('Houston We Have a Problem -- That Kernel is not recognized')
    return K

def smoP(dataMatIn, classLabels, C, toler, maxIter, kTup=('lin', 0)):
    """
    完整SMO算法外循环,与smoSimple有些类似,但这里的循环退出条件更多一些
    Args:
        dataMatIn    数据集
        classLabels  类别标签
        C   松弛变量(常量值),允许有些数据点可以处于分隔面的错误一侧。
            控制最大化间隔和保证大部分的函数间隔小于1.0这两个目标的权重。
            可以通过调节该参数达到不同的结果。
        toler   容错率
        maxIter 退出前最大的循环次数
        kTup    包含核函数信息的元组
    Returns:
        b       模型的常量值
        alphas  拉格朗日乘子
    """

    # 创建一个 optStruct 对象
    oS = optStruct(mat(dataMatIn), mat(classLabels).transpose(), C, toler, kTup)
    iter = 0
    entireSet = True
    alphaPairsChanged = 0

    # 循环遍历:循环maxIter次 并且 (alphaPairsChanged存在可以改变 or 所有行遍历一遍)
    while (iter < maxIter) and ((alphaPairsChanged > 0) or (entireSet)):
        alphaPairsChanged = 0

        #  当entireSet=true or 非边界alpha对没有了;就开始寻找 alpha对,然后决定是否要进行else。
        if entireSet:
            # 在数据集上遍历所有可能的alpha
            for i in range(oS.m):
                # 是否存在alpha对,存在就+1
                alphaPairsChanged += innerL(i, oS)
                # print("fullSet, iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))
            iter += 1

        # 对已存在 alpha对,选出非边界的alpha值,进行优化。
        else:
            # 遍历所有的非边界alpha值,也就是不在边界0或C上的值。
            nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]
            for i in nonBoundIs:
                alphaPairsChanged += innerL(i, oS)
                # print("non-bound, iter: %d i:%d, pairs changed %d" % (iter, i, alphaPairsChanged))
            iter += 1

        # 如果找到alpha对,就优化非边界alpha值,否则,就重新进行寻找,如果寻找一遍 遍历所有的行还是没找到,就退出循环。
        if entireSet:
            entireSet = False  # toggle entire set loop
        elif (alphaPairsChanged == 0):
            entireSet = True
        print("iteration number: %d" % iter)
    return oS.b, oS.alphas

# 5)测试算法:便携一个函数来测试不同的和函数并计算错误率
# 基于SVM的手写数字识别
def testDigits(kTup=('rbf', 10)):

    # 1. 导入训练数据
    dataArr, labelArr = loadImages('data/6.SVM/trainingDigits')
    b, alphas = smoP(dataArr, labelArr, 200, 0.0001, 10000, kTup)
    datMat = mat(dataArr)
    labelMat = mat(labelArr).transpose()
    svInd = nonzero(alphas.A > 0)[0]
    sVs = datMat[svInd]
    labelSV = labelMat[svInd]
    # print("there are %d Support Vectors" % shape(sVs)[0])
    m, n = shape(datMat)
    errorCount = 0
    for i in range(m):
        kernelEval = kernelTrans(sVs, datMat[i, :], kTup)
        # 1*m * m*1 = 1*1 单个预测结果
        predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
        if sign(predict) != sign(labelArr[i]): errorCount += 1
    print("the training error rate is: %f" % (float(errorCount) / m))

    # 2. 导入测试数据
    dataArr, labelArr = loadImages('data/6.SVM/testDigits')
    errorCount = 0
    datMat = mat(dataArr)
    labelMat = mat(labelArr).transpose()
    m, n = shape(datMat)
    for i in range(m):
        kernelEval = kernelTrans(sVs, datMat[i, :], kTup)
        # 1*m * m*1 = 1*1 单个预测结果
        predict = kernelEval.T * multiply(labelSV, alphas[svInd]) + b
        if sign(predict) != sign(labelArr[i]): errorCount += 1
    print("the test error rate is: %f" % (float(errorCount) / m))

# 可以将上述代码输入文件中
# 6)使用算法:一个图像识别的完整应用还需要一些图像处理的知识,这里并不打算深入介绍
# 运行命令:
>>> svmMLiA.testDigits(('rbf', 20))
				    
trainingDigits
iteration number: 1
iteration number: 2
iteration number: 3
iteration number: 4
iteration number: 5
iteration number: 6
the training error rate is: 0.000000
testDigits
the test error rate is: 0.016129
>>> 

备注:

学习参考资料:
《机器学习实战》

https://github.com/apachecn/AiLearning/blob/master/docs/ml/6.%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA.md

https://github.com/apachecn/AiLearning/blob/master/docs/ml/6.1.%E6%94%AF%E6%8C%81%E5%90%91%E9%87%8F%E6%9C%BA%E7%9A%84%E5%87%A0%E4%B8%AA%E9%80%9A%E4%BF%97%E7%90%86%E8%A7%A3.md

https://github.com/TrWestdoor/Machine-Learning-in-Action/blob/master/sec6-SVM/svmMLiA.py

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