《机器学习实战》之五——Logistic回归

一、什么是回归

假设现在有一些数据点,我们用一条直线对这些点进行拟合(该线称为最佳拟合直线),这个拟合的过程就称作回归
利用Logisitc回归进行分类的主要思想是:根据现有的数据对分类边界线建立回归公式,以此进行分类。训练分类器时的做法就是寻找最佳拟合参数
也就是说,logistic回归是先寻找给定的下面这个公式中的:
在这里插入图片描述
最佳拟合的参数,然后利用sigmoid函数进行分类。

二、sigmoid函数

sigmod函数很像阶跃函数,即当z>0时,sigmod(z)>0.5,且当z无限大时,sigmod(z)无限接近于1,相反则无限接近于0。我们将每个特征都乘以对应的回归系数,然后把所有的结果值相加,将这个总和带入sigmoid函数中,就可以得到一个范围在0~1之间的数值。>=0.5的数据被分为1类,小于0.5的被分为0类。所以,logistic回归被看成是一种概率估计。
在这里插入图片描述

三、基于最优方法的最佳回归系数的确定

将上述的两个公式,进行整合,我们要找的最佳回归系数是w0,w1,w2…wn。使得分类器尽可能的精确。用向量表示为w,找到最佳参数w的方法主要采用的是梯度上升算法。

(一)梯度上升法

梯度上升法的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
梯度上升的迭代公式:
在这里插入图片描述
其中α为步长,该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围。α的值不能太大,也不能太小。太小了,迭代次数需要很多次,计算量大。太大了,有可能会错过最优值,而导致不收敛。
一般使用梯度上升算法来求函数的最大值,而梯度下降算法来求函数的最小值。(梯度下降的迭代公式时将加号变成减号)

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

#载入数据函数
def loadDataSet():
    dataMat = []
    labelMat = []
    fr = open("testSet.txt")
    for line in fr.readlines():
        lineArr = line.strip().split()
        #将x0的值设置为1.0
        dataMat.append([1.0, float(lineArr[0]),float(lineArr[1])])
        labelMat.append(int(lineArr[2]))
    return dataMat, labelMat

#sigmoid函数
def sigmoid(inX):
    return 1.0/(1+np.exp(-inX))    

#梯度上升函数
def gradAscent(dataMatIn, classLabels):
    #将输入的数据转换为矩阵
    dataMatrix = np.mat(dataMatIn)
    #将标签向量转换为矩阵后,还需要将行向量转换为列向量
    labelMat = np.mat(classLabels).transpose()
    
    m,n = np.shape(dataMatrix)
    #alpha为向目标移动的步长
    alpha = 0.01
    #迭代次数设为500
    maxCycles = 500
    weights = np.ones((n,1))
    for k in range(maxCycles):
        h = sigmoid(dataMatrix*weights)
        error = (labelMat-h)
        weights = weights + alpha * dataMatrix.transpose()*error#这里需要数学推导
    return weights

其中,在梯度上升的算法中,用到了数学推导,这里我是通过学习吴恩达的machine learning的第七章的知识,然后知道了logistic回归的代价函数,然后再对代价函数求导,推出的。(该课程的在网易云课堂上有https://study.163.com/course/courseMain.htm?courseId=1004570029)大家可以自己学习并试着推导一下。

(二)分析数据,画出决策边界

通过上面的方法,我们已经解出了一组回归系数,它确定了不同类别数据之间的分隔线,那么下面就是要画出该分隔线。

# 画出数据集和Logistic回归最佳拟合直线的函数
def plotBestFit(weights):  
    dataMat, labelMat = loadDataSet()
    dataArr = np.array(dataMat)
    n = np.shape(dataArr)[0]
    xcord1 = []
    ycord1 = []
    xcord2 = []
    ycord2 = []
   #将标签为1的数据元素和为0的分别放在(xcode1,ycode1)、(xcord2,ycord2)
    for i in range(n):
        if int(labelMat[i]) == 1:
            xcord1.append(dataArr[i,1])
            ycord1.append(dataArr[i,2])
        else:
            xcord2.append(dataArr[i,1])
            ycord2.append(dataArr[i,2])
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.scatter(xcord1, ycord1,s=30,c='red', marker='s')
    ax.scatter(xcord2, ycord2,s=30,c='green')
    #绘制出w0 + w1*x + w2*y = 0的直线
    x = np.arange(-3.0,3.0,0.1)
    y = (-weights[0]-weights[1]*x)/weights[2]
    y1 = y.transpose()
    ax.plot(x,y1)
    
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()

测试:

import logRegres
import numpy as np

dataArr, labelMat = logRegres.loadDataSet()

#*******************测试梯度上升算法*****************
weights = logRegres.gradAscent(dataArr,labelMat)
print(weights)
logRegres.plotBestFit(weights.getA())

结果如下图:
在这里插入图片描述

(三)训练算法:随机梯度上升

梯度上升算法在每次更新回归系数时都需要遍历整个数据集,该方法在处理小规模的数据集还可以,但是要处理大规模的数据集,计算复杂度就太大了。
一种改进的方法就是一次仅用一个样本点来更新回归系数,该方法称为随机梯度上升算法。由于可以在新演变到来时度分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法

#随机梯度上升算法
def stocGradAscent0(dataMatrix, classLabels):
    m,n = np.shape(dataMatrix)
    alpha = 0.01
    weights = np.ones(n)
    # h,error 都是数值,而非向量,一次仅用一个样本来更新回归系数
    for i in range(m):
        h = sigmoid(sum(dataMatrix[i]*weights))
        error = classLabels[i] - h
        weights = weights + alpha * error * dataMatrix[i]
    return weights

测试:

import logRegres
import numpy as np

dataArr, labelMat = logRegres.loadDataSet()
#*******************测试随机梯度上升算法*****************
weights = logRegres.stocGradAscent0(np.array(dataArr),labelMat)
print(weights)
logRegres.plotBestFit(weights)

结果如下图:
在这里插入图片描述
显然,这里的分类器错分了好多。但是这个算法只在一个样本集上迭代了一次。所以继续改进。

(四)训练算法:改进的随机梯度上升

一个判断优化算法优劣的可靠方法是看它是否收敛,也就是说参数是否达到了稳定值,是否还会不断地变化。那就要进行多次的迭代。

#改进的随机梯度上升算法
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    m,n = np.shape(dataMatrix)
    weights = np.ones(n)
    for j in range(numIter):
        dataIndex = list(range(m))#这里必须转换为list类型,不然后面不能del
        for i in range(m):
            # alpha每次迭代时需要调整,缓解数据波动或者高频振动
            alpha = 4/(1.0+j+i)+0.01
            # 随机选取样本来更新回归系数
            randIndex = int(np.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

增加了两处改进。1:alpha在每次迭代的时候都会调整,但永远不会减少到0.。这样做保证了在多次迭代之后新数据仍然具有一定的影响。2:这里是通过随机选取样本来更新回归系数的。每次随机从列表中选出一个值,然后从列表中删掉该值(再进行下一次迭代)。
测试:

import logRegres
import numpy as np
dataArr, labelMat = logRegres.loadDataSet()
#*******************测试改进的随机梯度上升算法*****************
weights = logRegres.stocGradAscent1(np.array(dataArr),labelMat)
print(weights)
logRegres.plotBestFit(weights)

结果:
在这里插入图片描述
从图中可以看出该算法达到了跟梯度上升差不多的效果。

四、使用logistic回归预测病马的死亡率

1、使用Logistic回归估计马疝病的死亡率过程

(1) 收集数据:给定数据文件。
(2) 准备数据:用Python解析文本文件并填充缺失值。
(3) 分析数据:可视化并观察数据。
(4) 训练算法:使用优化算法,找到最佳的系数。
(5) 测试算法:为了量化回归的效果,需要观察错误率。根据错误率决定是否回退到训练阶段,通过改变迭代的次数和步长等参数来得到更好的回归系数。

2、准备数据:处理数据中的缺失值

数据中的缺失值是个非常棘手的问题,有很多文献都致力于解决这个问题。那么,数据缺失究竟带来了什么问题?假设有100个样本和20个特征,这些数据都是机器收集回来的。若机器上的某个传感器损坏导致一个特征无效时该怎么办?此时是否要扔掉整个数据?这种情况下,另外19个特征怎么办?它们是否还可用?答案是肯定的。因为有时候数据相当昂贵,扔掉和重新获取都是不可取的,所以必须采用一些方法来解决这个问题。
下面给出了一些可选的做法:

  • 使用可用特征的均值来填补缺失值;
  • 使用特殊值来填补缺失值,如-1;
  • 忽略有缺失值的样本;
  • 使用相似样本的均值添补缺失值;

3、 测试算法:用 Logistic 回归进行分类

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

def classifyVector(inX, weights):
    prob = sigmoid(sum(inX*weights))
    if prob>0.5:
        return 1.0
    else:
        return 0.0
    
def colicTest():
    frTrain = open("horseColicTraining.txt")
    frTest = open("horseColicTest.txt")
    trainingSet = []
    trainingLabels = []
    #读入训练集,并训练最佳拟合的参数
    for line in frTrain.readlines():
        currLine = line.strip().split('\t')
        lineArr = []
        for i in range(21):
            lineArr.append(float(currLine[i]))
        trainingSet.append(lineArr)
        trainingLabels.append(float(currLine[21]))
    trainWeights = stocGradAscent1(np.array(trainingSet),trainingLabels, 500)
    
    errorCount = 0
    numTestVec = 0.0
    #读入测试集,并进行分类
    for line in frTest.readlines():
        numTestVec += 1.0
        currLine = line.strip().split('\t')
        lineArr = []
        for i in range(21):
            lineArr.append(float(currLine[i]))
        if int(classifyVector(np.array(lineArr),trainWeights)) != int(currLine[21]):
            errorCount += 1
    errorRate = (float(errorCount)/numTestVec)
    print("the error rate of this test is : %f" % errorRate)
    return errorRate

def multiTest():
    numTests = 10
    errorSum = 0.0
    for k in range(numTests):
        errorSum += colicTest()
    print("after %d iterations the average error rate is: %f" % (numTests, errorSum/float(numTests)))

上面的multiTest()函数的功能是调用colicTest()10次并求结果的平均值。测试代码如下

import logRegres
import numpy as np

#*******************从疝气病症预测病马的死亡率*****************
logRegres.multiTest()

结果如下图所示:
在这里插入图片描述
在这里遇到了问题,就是因为inX的值太大,而导致sigmoid函数在计算时出现了溢出。解决的方法是将sigmoid函数进行了如下改写。

def sigmoid(inX):
    if inX>0:
        return 1.0/(1+np.exp(-inX))
    else:
        return np.exp(inX)/(1+np.exp(inX))

五、绘制各权重的收敛图

下面是我参考这位博主的博客进行的学习:原文:https://blog.csdn.net/c406495762/article/details/77851973
大家也可以直接去看该博文,写的非常好。我把所有需要改动的代码贴在这儿了。主要是将梯度上升算法和随机梯度上升算法中都增加了一个返回值,weights_array,记录了每次更新的回归系数。而weights返回的是最优的回归系数。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
#载入数据函数
def loadDataSet():
    dataMat = []
    labelMat = []
    fr = open("testSet.txt")
    for line in fr.readlines():
        lineArr = line.strip().split()
        #将x0的值设置为1.0
        dataMat.append([1.0, float(lineArr[0]),float(lineArr[1])])
        labelMat.append(int(lineArr[2]))
    return dataMat, labelMat

#sigmoid函数
def sigmoid(inX):
    return 1.0/(1+np.exp(-inX))    

#梯度上升函数
def gradAscent(dataMatIn, classLabels):
    #将输入的数据转换为矩阵
    dataMatrix = np.mat(dataMatIn)
    #将标签向量转换为矩阵后,还需要将行向量转换为列向量
    labelMat = np.mat(classLabels).transpose()
    
    m,n = np.shape(dataMatrix)
    #alpha为向目标移动的步长
    alpha = 0.01
    #迭代次数设为500
    maxCycles = 500
    weights = np.ones((n,1))
    weights_array = np.array([])
    for k in range(maxCycles):
        h = sigmoid(dataMatrix*weights)
        error = (labelMat-h)
        weights = weights + alpha * dataMatrix.transpose()*error
        weights_array = np.append(weights_array,weights)
    weights_array = weights_array.reshape(maxCycles,n)
    return weights,weights_array
   
#随机梯度上升算法
def stocGradAscent0(dataMatrix, classLabels):
    m,n = np.shape(dataMatrix)
    alpha = 0.01
    weights = np.ones(n)
    x = []
    # h,error 都是数值,而非向量,一次仅用一个样本来更新回归系数
    for i in range(m):
        h = sigmoid(sum(dataMatrix[i]*weights))
        error = classLabels[i] - h
        weights = weights + alpha * error * dataMatrix[i]
        x.append([i,weights[0],weights[1],weights[2]])  
    return weights,x

#改进的随机梯度上升算法
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    m,n = np.shape(dataMatrix)
    weights = np.ones(n)
    weights_array = np.array([])
    for j in range(numIter):
        dataIndex = list(range(m))
        for i in range(m):
            # alpha每次迭代时需要调整,缓解数据波动或者高频振动
            alpha = 4/(1.0+j+i)+0.01
            # 随机选取样本来更新回归系数
            randIndex = int(np.random.uniform(0,len(dataIndex)))
            h = sigmoid(sum(dataMatrix[randIndex]*weights))
            error = classLabels[randIndex]-h
            weights = weights+alpha*error*dataMatrix[randIndex]
            weights_array = np.append(weights_array,weights,axis=0)
            del(dataIndex[randIndex])
    weights_array = weights_array.reshape(numIter*m,n)
    return weights,weights_array
      
def plotWeights(weights_array1,weights_array2):
    #设置汉字格式,windows自带的汉字
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
    #将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8)
    #当nrow=3,nclos=2时,代表fig画布被分为六个区域,axs[0][0]表示第一行第一列
    fig, axs = plt.subplots(nrows=3, ncols=2,sharex=False, sharey=False, figsize=(20,10))
    x1 = np.arange(0, len(weights_array1), 1)
    #绘制w0与迭代次数的关系
    axs[0][0].plot(x1,weights_array1[:,0])
    axs0_title_text = axs[0][0].set_title(u'梯度上升算法:回归系数与迭代次数关系',FontProperties=font)
    axs0_ylabel_text = axs[0][0].set_ylabel(u'W0',FontProperties=font)
    plt.setp(axs0_title_text, size=20, weight='bold', color='black') 
    plt.setp(axs0_ylabel_text, size=20, weight='bold', color='black')
    #绘制w1与迭代次数的关系
    axs[1][0].plot(x1,weights_array1[:,1])
    axs1_ylabel_text = axs[1][0].set_ylabel(u'W1',FontProperties=font)
    plt.setp(axs1_ylabel_text, size=20, weight='bold', color='black')
    #绘制w2与迭代次数的关系
    axs[2][0].plot(x1,weights_array1[:,2])
    axs2_xlabel_text = axs[2][0].set_xlabel(u'迭代次数',FontProperties=font)
    axs2_ylabel_text = axs[2][0].set_ylabel(u'W1',FontProperties=font)
    plt.setp(axs2_xlabel_text, size=20, weight='bold', color='black') 
    plt.setp(axs2_ylabel_text, size=20, weight='bold', color='black')
    x2 = np.arange(0, len(weights_array2), 1)
    #绘制w0与迭代次数的关系
    axs[0][1].plot(x2,weights_array2[:,0])
    axs0_title_text = axs[0][1].set_title(u'改进的随机梯度上升算法:回归系数与迭代次数关系',FontProperties=font)
    axs0_ylabel_text = axs[0][1].set_ylabel(u'W0',FontProperties=font)
    plt.setp(axs0_title_text, size=20, weight='bold', color='black') 
    plt.setp(axs0_ylabel_text, size=20, weight='bold', color='black')
    #绘制w1与迭代次数的关系
    axs[1][1].plot(x2,weights_array2[:,1])
    axs1_ylabel_text = axs[1][1].set_ylabel(u'W1',FontProperties=font)
    plt.setp(axs1_ylabel_text, size=20, weight='bold', color='black')
    #绘制w2与迭代次数的关系
    axs[2][1].plot(x2,weights_array2[:,2])
    axs2_xlabel_text = axs[2][1].set_xlabel(u'迭代次数',FontProperties=font)
    axs2_ylabel_text = axs[2][1].set_ylabel(u'W1',FontProperties=font)
    plt.setp(axs2_xlabel_text, size=20, weight='bold', color='black') 
    plt.setp(axs2_ylabel_text, size=20, weight='bold', color='black')

    plt.savefig('compare.png')
    plt.show()

#对比改进的梯度下降算法和未改进的梯度下降算法的回归系数与迭代次数之间的关系  
if __name__=="__main__":    
    dataMat,labelMat=loadDataSet()
    weights0,a0=gradAscent(np.array(dataMat),labelMat)
    weights,a=stocGradAscent1(np.array(dataMat),labelMat)
    plotWeights(a0,a)

结果
在这里插入图片描述
由于改进的随机梯度上升算法,随机选取样本点,所以每次的运行结果是不同的。但是大体趋势是一样的。我们改进的随机梯度上升算法收敛效果更好。为什么这么说呢?让我们分析一下。我们一共有100个样本点,改进的随机梯度上升算法迭代次数为150。而上图显示15000次迭代次数的原因是,使用一次样本就更新一下回归系数。因此,迭代150次,相当于更新回归系数150*100=15000次。简而言之,迭代150次,更新1.5万次回归参数。从上图左侧的改进随机梯度上升算法回归效果中可以看出,其实在更新2000次回归系数的时候,已经收敛了。相当于遍历整个数据集20次的时候,回归系数已收敛。训练已完成
再让我们看看上图右侧的梯度上升算法回归效果,梯度上升算法每次更新回归系数都要遍历整个数据集。从图中可以看出,当迭代次数为300多次的时候,回归系数才收敛。凑个整,就当它在遍历整个数据集300次的时候已经收敛好了。

六、总结

Logistic回归的目的是寻找一个非线性函数Sigmoid的最佳拟合参数,求解过程可以由最优化算法来完成。在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法的计算复杂度非常大,所以提出了改进的随机梯度上升算法。
改进的随机梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。此外,随机梯度上升是一个在线算法,它可以在新数据到来时就完成参数更新,而不需要重新读取整个数据集来进行运算。
通过学习各个博主的博文,学会了通过绘制权重参数来看他们的收敛程度和效果,对比非常明显。这是书上没有的东西,但是个人感觉特别的实用,非常感谢博主们的分享。

参考文献及博文

[1]python机器学习实战 getA()函数详解(https://blog.csdn.net/douya2016/article/details/78161890)
[2]Python3《机器学习实战》学习笔记(七):Logistic回归实战篇之预测病马死亡率

猜你喜欢

转载自blog.csdn.net/liujh845633242/article/details/85002476
今日推荐