“机器学习实战”刻意练习——分类问题:逻辑(Logistic)回归

参考:
Python3《机器学习实战》学习笔记(六):Logistic回归基础篇之梯度上升算法 - Jack-Cui - CSDN博客
Python3《机器学习实战》学习笔记(七):Logistic回归实战篇之预测病马死亡率 - Jack-Cui - CSDN博客

一、概述

1.回归

假设现在有一些数据点,我们用一条直线对这些点进行拟合(该线称为最佳拟合直线),这个拟合过程就称作回归。

利用Logistic回归进行分类的主要思想是:
根据现有数据对分类边界线建立回归公式,以此进行分类。
这里的“回归”一词源于最佳拟合,表示要找到最佳拟合参数集,我们将使用最优化算法找到最佳拟合参数集。

2.Logistic回归的一般过程

  1. 收集数据:采用任意方法收集数据。

  2. 准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳。

  3. 分析数据:采用任意方法对数据进行分析。

  4. 训练算法:大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数。

  5. 测试算法:一旦训练步骤完成,分类将会很快。

  6. 使用算法:
    首先,我们需要输入一些数据,并将其转换成对应的结构化数值;
    接着,基于训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别;
    在这之后,我们就可以在输出的类别上做一些其他分析工作。

二、数学原理

1.Sigmoid 函数

我们想要的函数应该是,能接受所有的输入然后预测出类别。
例如,在两个类的情况下,上述函数输出0或1。

Sigmoid函数可以近似实现在跳跃点上从0瞬间跳跃到1,其具体的计算公式如下:
σ ( z ) = 1 1 + e − z \sigma (z)=\frac{1}{1+e^{-z}} σ(z)=1+ez1
下面给出Sigmoid函数在不同坐标尺度下的两条曲线图:
“机器学习实战”刻意练习——分类问题:逻辑(Logistic)回归_第1张图片
可以看出,当x为0时,Sigmoid函数值为0.5;随着x的增大,对应的Sigmoid值将逼近于1;而随着x的减小,Sigmoid值将逼近于0。
如果横坐标刻度足够大,Sigmoid函数看起来很像一个阶跃函数。

2.基于最优化方法确定最佳回归系数

Sigmoid函数的输入记为z,由下面公式得出:
z = w 0 x 0 + w 1 x 1 + w 2 x 2 + • • • + w n x n z = w_{0}x_{0}+w_{1}x_{1}+ w_{2}x_{2}+ •••+ w_{n}x_{n} z=w0x0+w1x1+w2x2++wnxn

如果采用向量的写法,上述公式可以写成 z = w T x z = w^{T}x z=wTx,它表示将这两个数值向量对应元素相乘然后全部加起来即得到z值。

下面介绍梯度上升的最优化方法求得数据集的最佳参数的方法。

3.梯度上升法

梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
如果梯度记为∇,则函数f(x,y)的梯度由下式表示:
▽ f ( x , y ) = ( ∂ f ( x , y ) ∂ x ∂ f ( x , y ) ∂ y ) \bigtriangledown f(x,y)=\begin{pmatrix} \frac{\partial f(x,y)}{\partial x}\\ \\ \frac{\partial f(x,y)}{\partial y} \end{pmatrix} f(x,y)=xf(x,y)yf(x,y)

当然,函数 f ( x , y ) f (x,y) f(x,y)必须要在待计算的点上有定义并且可微。

到移动量的大小。该量值称为步长,记做α。
用向量来表示的话,梯度上升算法的迭代公式如下:
w : = w + α ▽ f ( w ) w:=w+\alpha \triangledown f(w) w:=w+αf(w)

该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围。

三、代码实现(python3)

1.训练算法:使用梯度上升找到最佳参数

import numpy as np

def loadDataSet():
    """
    加载数据集
    - - - -
    """
    #数据列表
    dataMat = []
    #标签列表
    labelMat = [] 
    #从文件中读取数据
    fr = open('4-Logistic/testSet.txt') 
    for line in fr.readlines():
        lineArr = line.strip().split()
        dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])
        labelMat.append(int(lineArr[2])) 
    fr.close() 
    return dataMat, labelMat

def sigmoid(inX):
    """
    sigmoid函数
    - - - -
    inX - 数据向量
    """
    return 1.0 / (1 + np.exp(-inX))

def gradAscent(dataMatIn, classLabels):
    """
    梯度上升算法
    - - - -
    dataMatIn - 数据集

    classLabels - 数据标签
    """
    #转换成numpy的矩阵形式
    dataMatrix = np.mat(dataMatIn) 
    #转换成numpy的矩阵形式,并转置
    labelMat = np.mat(classLabels).transpose()
    #返回dataMatrix的大小
    m, n = np.shape(dataMatrix) 
    #步长
    alpha = 0.001
    #最大迭代次数
    maxCycles = 500
    #矩阵相乘
    weights = np.ones((n,1))
    for k in range(maxCycles):
        h = sigmoid(dataMatrix * weights)
        #计算真实类别与预测类别的差值(公式推导见https://blog.csdn.net/c406495762/article/details/77723333)
        error = labelMat - h
        weights = weights + alpha * dataMatrix.transpose() * error
    #将矩阵转换为数组返回
    return weights.getA() 
    
if __name__ == '__main__':
    dataMat, labelMat = loadDataSet()           
    print(gradAscent(dataMat, labelMat))

结果:

[[ 4.12414349]
 [ 0.48007329]
 [-0.6168482 ]]

2.分析数据:画出决策边界

上面已经解出了一组回归系数,它确定了不同类别数据之间的分隔线。
下面画出该分隔线,从而使得优化的过程便于理解。

import matplotlib.pyplot as plt

def plotBestFit(wei):
    """
    画出数据集和Logistic回归最佳拟合直线的函数
    - - - -
    wei - 权重参数数组
    """
    dataMat, labelMat = loadDataSet() 
    dataArr = np.array(dataMat)
    n = np.shape(dataMat)[0]
    #正、负样本
    xcord1 = []; ycord1 = []
    xcord2 = []; ycord2 = []
    #根据数据集标签进行分类
    for i in range(n):
        #1为正样本
        if int(labelMat[i]) == 1:
            xcord1.append(dataArr[i,1]); ycord1.append(dataArr[i,2])
        #0为负样本
        else:
            xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2])
    fig = plt.figure()
    ax = fig.add_subplot(111) 
    #绘制正、负样本
    ax.scatter(xcord1, ycord1, s = 20, c = 'red', marker = 's',alpha=.5)
    ax.scatter(xcord2, ycord2, s = 20, c = 'green',alpha=.5)            
    x = np.arange(-3.0, 3.0, 0.1)
    y = (-wei[0] - wei[1] * x) / wei[2]
    ax.plot(x, y)
    plt.title('BestFit') 
    plt.xlabel('X1'); plt.ylabel('X2')
    plt.show()

if __name__ == '__main__':
    dataMat, labelMat = loadDataSet()           
    weights = gradAscent(dataMat, labelMat)
    plotBestFit(weights)

结果:
“机器学习实战”刻意练习——分类问题:逻辑(Logistic)回归_第2张图片
这个分类结果相当不错,从图上看只错分了两到四个点。
但是,尽管例子简单且数据集很小,这个方法却需要大量的计算(300次乘法)。
因此我们将对该算法稍作改进,从而使它可以用在真实数据集上。

3.训练算法:随机梯度上升

梯度上升算法在每次更新回归系数时都需要遍历整个数据集,该方法在处理100个左右的数据集时尚可,但如果有数十亿样本和成千上万的特征,那么该方法的计算复杂度就太高了。

一种改进方法是一次仅用一个样本点来更新回归系数,该方法称为随机梯度上升算法

由于可以在新样本到来时对分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法
与“在线学习”相对应,一次处理所有数据被称作是**“批处理”**。

def stocGradAscentO(dataMatrix, classLabels): 
    """
    随机梯度上升算法
    - - - -
    dataMatIn - 数据集

    classLabels - 数据标签
    """
    dataMatrix=np.array(dataMatrix)
    m,n = np.shape(dataMatrix)
    #参数初始化
    alpha = 0.01
    weights = np.ones(n)
    for i in range(m):
        h = sigmoid(sum(dataMatrix[i]*weights))
        error = classLabels[i] - h
        weights = weights + alpha * error * dataMatrix[i]
    return weights

if __name__ == '__main__':
    dataMat, labelMat = loadDataSet()           
    weights = stocGradAscentO(dataMat, labelMat)
    plotBestFit(weights)

可以看到,随机梯度上升算法与梯度上升算法在代码上很相似,但也有一些区别:
第一,后者的变量h和误差error都是向量,而前者则全是数值
第二,前者没有矩阵的转换过程,所有变量的数据类型都是NumPy数组。

结果:
“机器学习实战”刻意练习——分类问题:逻辑(Logistic)回归_第3张图片
可以看到,拟合出来的直线效果还不错,但并不像梯度上升算法那样完美。这里的分类器错分了三分之一的样本。

所以我们增加了两处代码来进行改进随机梯度上升算法。

import random

def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    """
    随机梯度上升算法
    - - - -
    dataMatIn - 数据集

    classLabels - 数据标签
    """
    dataMatrix=np.array(dataMatrix)
    m,n = np.shape(dataMatrix) 
    #参数初始化
    weights = np.ones(n)
    for j in range(numIter):                                           
        dataIndex = list(range(m))
        for i in range(m):           
            #降低alpha的大小,每次减小1/(j+i)
            alpha = 4/(1.0+j+i)+0.01 
            #随机选取样本,计算h
            randIndex = int(random.uniform(0,len(dataIndex)))
            h = sigmoid(sum(dataMatrix[randIndex]*weights))
            #计算误差
            error = classLabels[randIndex] - h 
            #更新回归系数
            weights = weights + alpha * error * dataMatrix[randIndex]
            #删除已经使用的样本
            del(dataIndex[randIndex])
    return weights  

if __name__ == '__main__':
    dataMat, labelMat = loadDataSet()           
    weights = stocGradAscent1(dataMat, labelMat)
    plotBestFit(weights)
  • 第一处改进:
    alpha在每次迭代的时候都会调整,这会缓解数据波动或者高频波动
    另外,虽然alpha会随着迭代次数不断减小,但永远不会减小到0,这是因为还存在一个常数项。必须这样做的原因是为了保证在多次迭代之后新数据仍然具有一定的影响。
    如果要处理的问题是动态变化的,那么可以适当加大上述常数项,来确保新的值获得更大的回归系数。
    另一点值得注意的是,在降低alpha的函数中,alpha每次减少1/(j+i) ,其中j是迭代次数,i是样本点的下标。这样当j<不是严格下降的。
    避免参数的严格下降也常见于模拟退火算法等其他优化算法中。
  • 第二个改进:
    通过随机选取样本来更新回归系数。这种方法将减少周期性的波动
    具体实现方法与决策树中类似,这种方法每次随机从列表中选出一个值,然后从列表中删掉该值(再进行下一次迭代)。
  • 此外,改进算法还增加了一个迭代次数作为第3个参数。如果该参数没有给定的话,算法将默认迭代150次。如果给定,那么算法将按照新的参数值进行迭代。

四、实战:从疝气病症预测病马的死亡率

本节将使用Logistic回归来预测患有疝病的马的存活问题。
这里的数据集中包含368个样本和28个特征。该数据集中包含了医院检测马疝病的一些指标,有的指标比较主观,有的指标难以测量,例如马的疼痛级别。
另外需要说明的是,除了部分指标主观和难以测量外,该数据还存在一个问题,数据集中有30%的值是缺失的。下面将首先介绍如何处理数据集中的数据缺失问题,然后再利用Logistic回归和随机梯度上升算法来预测病马的生死。

1. 准备数据:处理数据中的缺失值

下面给出了一些处理数据中的缺失值可选的做法:

  • 使用可用特征的均值来填补缺失值;
  • 使用特殊值来填补缺失值,如-1;
  • 忽略有缺失值的样本;
  • 使用相似样本的均值添补缺失值;
  • 使用另外的机器学习算法预测缺失值。

现在,我们对下一节要用的数据集进行预处理,使其可以顺利地使用分类算法。在预处理阶段需要做两件事:

第一,所有的缺失值必须用一个实数值来替换,因为我们使用的NumPy数据类型不允许包含缺失值。这里选择实数0来替换所有缺失值,这样在更新时不会影响系数的值。恰好能适用于Logistic回归。

第二,如果在测试数据集中发现了一条**数据的类别标签已经缺失,那么我们的简单做法是将该条数据丢弃。**这是因为类别标签与特征不同,很难确定采用某个合适的值来替换。

2.测试算法:用 Logistic 回归进行分类

使用Logistic回归方法进行分类并不需要做很多工作,所需做的只是把测试集上每个特征向量乘以最优化方法得来的回归系数,再将该乘积结果求和,最后输入到Sigmoid函数中即可。
如果对应的Sigmoid值大于0.5就预测类别标签为1,否则为0。

def classifyVector(inX, weights):
    """
    分类函数
    - - - -
    inX - 特征向量

    weights - 回归系数
    """
    prob = sigmoid(sum(inX*weights))
    if prob > 0.5: 
        return 1.0
    else: 
        return 0.0

def colicTest():
    """
    Logistic回归分类预测病马的死亡率
    - - - -
    """
    #打开训练集并读取
    frTrain = open('4-Logistic/horseColicTraining.txt') 
    trainingSet = []; trainingLabels = []
    for line in frTrain.readlines():
        currLine = line.strip().split('\t')
        lineArr = []
        for i in range(len(currLine)-1):
            lineArr.append(float(currLine[i]))
        trainingSet.append(lineArr)
        trainingLabels.append(float(currLine[-1]))
    #使用改进的随机上升梯度算法训练分类器
    trainWeights = stocGradAscent1(np.array(trainingSet), trainingLabels, 500)

    #测试分类器错误率
    errorCount = 0; numTestVec = 0.0
    #打开测试集并读取
    frTest = open('4-Logistic/horseColicTest.txt')
    for line in frTest.readlines():
        numTestVec += 1.0
        currLine = line.strip().split('\t')
        lineArr =[]
        for i in range(len(currLine)-1):
            lineArr.append(float(currLine[i]))
        if int(classifyVector(np.array(lineArr), trainWeights))!= int(currLine[-1]):
            errorCount += 1
    errorRate = (float(errorCount)/numTestVec)                                #错误率计算
    print("the error rate of this test is: %f" % errorRate)
    return errorRate

def multiTest(numTests):
    """
    计算平均错误率
    - - - -
    numTests - 分类次数
    """
    errorSum=0.0 
    for k in range(numTests):
        errorSum += colicTest()
    print ("after %d iterations the average error rate is:%f" % (numTests, errorSum/float(numTests)))

if __name__ == '__main__':
    #1.代码实现
    """ dataMat, labelMat = loadDataSet()           
    weights = stocGradAscent1(dataMat, labelMat)
    plotBestFit(weights) """
    #2.实战:从疝气病症预测病马的死亡率
    multiTest(10)

注意:
分类时,同前面一样,数据的最后一列仍然是类别标签。数据最初有三个类别标签,分别代表马的三种情况:“仍存活”、“已经死亡”和“已经安乐死”。这里为了方便,将“已经死亡”和“已经安乐死”合并成“未能存活”这个标签 。

结果:

the error rate of this test is: 0.313433
the error rate of this test is: 0.462687
the error rate of this test is: 0.402985
the error rate of this test is: 0.328358
the error rate of this test is: 0.373134
the error rate of this test is: 0.373134
the error rate of this test is: 0.358209
the error rate of this test is: 0.388060
the error rate of this test is: 0.358209
the error rate of this test is: 0.388060
after 10 iterations the average error rate is:0.374627

从上面的结果可以看到,10次迭代之后的平均错误率为37%。事实上,这个结果并不差,因为有30%的数据缺失。
当然,如果调整colicTest()中的迭代次数和stochGradAscent1()中的步长,平均错误率可以降到20%左右。

五、小结

  • Logistic回归的目的是寻找一个非线性函数Sigmoid的最佳拟合参数,求解过程可以由最优化算法来完成。

  • 在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以简化为随机梯度上升算法
    随机梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。
    此外,随机梯度上升是一个在线算法,它可以在新数据到来时就完成参数更新,而不需要重新读取整个数据集来进行批处理运算。

  • 机器学习的一个重要问题就是如何处理缺失数据。这个问题没有标准答案,取决于实际应用中的需求。现有一些解决方案,每种方案都各有优缺点。

你可能感兴趣的:(机器学习,算法,Logistic回归,机器学习,python)