机器学习实践笔记--Logistic回归

Logistic回归

主要内容

  • Sigmoid函数和Logistic回归分类器
  • 最优化理论初步
  • 梯度下降最优化算法
  • 数据中的缺失项处理

假设现在有一些数据点,我们用一条直线对这些点进行拟合(该线称为最佳拟合直线),这个拟合过程就称作回归。利用Logistic回归进行分类的主要思想是:根据现有数据对分类边界线建立回归公式。训练分类器时的做法就是寻找最佳拟合参数,使用的是最优化算法。

Logistic回归的一般过程

  1. 收集数据:采用任意方法收集数据。
  2. 准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳。
  3. 分析数据:采用任意方法对数据进行分析。
  4. 训练算法: 大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数。
  5. 测试算法:一旦训练步骤完成,分类将会很快。
  6. 使用算法:首先,我们下需要一些输入数据,并将其转换成对应的数据结构化数值;接着,给予训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别。

1.基于Logistic回归和Sigmoid函数的分类

Logistic回归
优点:计算代价不高,易于理解和实现。

缺点:容易欠拟合,分类精度可能不高

使用数据类型:数值型和标称型数据。

1.1 Sigmoid函数

在这里我们需要这样一个函数,能接收所有输入数据(特征向量)然后预测出类别。例如,有两个类别,那么这个函数的输出可以使0或1。
可能大家会想起单位阶跃函数,又称海维赛德阶跃函数(Heaviside step function)。然而,阶跃函数的问题在于:该函数在跳跃点上从0瞬间跳跃到1,这个瞬间跳跃过程有时很难处理。幸好,另一个函数也有类似的性质,且数学上更好处理,这就是Sigmoid函数
sigmoid函数具体公式如下:
$ \sigma(z)= \frac{1}{1 + e^{-z}} $

Sigmoid函数是一种阶跃函数(step function)。在数学中,如果实数域上的某个函数可以用半开区间上的指示函数的有限次线性组合来表示,那么这个函数就是阶跃函数。而数学中指示函数(indicator function)是定义在某集合X上的函数,表示其中有哪些元素属于某一子集A。

图一 给出了Sigmoid函数在不同坐标尺度下的两条曲线图。当x为0时,Sigmoid函数值为0.5。随着x的增大,对应的Sigmoid值将逼近于1;而随着x的减小,Sigmoid值将逼近于0。如果横坐标刻度足够大(图一),Sigmoid函数将看起来很像一个阶跃函数。

图一
图一
两种坐标尺度下的Sigmoid函数图。上图横坐标为-5到5,这时的曲线变化较为平滑;下图横坐标的尺度足够大,在x = 0点处Sigmoid函数看起来很像是阶跃函数。因此,为了实现Logistic回归分类器,我们可以在每个特征上乘以一个回归系数,然后把所有的结果值相加,将这个总和带入Sigmoid函数,进而得到一个范围0~1之间的数值。最后,结果大于0.5的数据被归为1类,小于0.5的即被归为0类。所以Logistic回归也可以被看成是一种概率估计。

确定了分类器的函数形式之后,现在的问题变成了:最佳回归系数是多少?如何确定它们的大小?

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

Sigmoid函数的输入记为z,由下面公式得出:
$$ z = w_0+w_1+w_2+···+w_n$$
如果使用向量的写法,上述公式可以写成 $ z = w^Tx $ 。它表示将两个数值向量的对应元素相乘然后全部加起来即得到z值。 其中的向量x是分类器的输入数据,向量w也就是我们要找到的最佳参数(参数),从而使得分类尽可能地精确。

2.1 梯度上升法

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

这是机器学习中最容易造成混淆的一个地方,但在数学上并不难,需要做的只是牢记这些符号的意义。这个梯度意味着要沿x的方向移动 $ \frac{\partial f(x,y) }{\partial x} $,沿y的方向移动 $ \frac{\partial f(x,y)}{\partial y} $。其中,函数$f(x,y)$必须要在待计算的点上有定义并且可微。

图二
图二 梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2。如此循环迭代,直到满足停止条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。 可以看到,梯度算子总是指向函数值增长最快的方向。这里所说的是移动方向,而未提到移动量的大小。该量值称为步长,记做$ \alpha $。用向量来表示的话,梯度上升算法的迭代公式如下:$$ w:=w+\alpha \nabla_{w}f(w) $$ 该公式将一直被迭代执行,直到到达某个停止条件为止,比如迭代次数到达某个指定值或算法到达某个可以允许的误差范围。

梯度下降算法

“梯度下降算法,它与这里的梯度上升算法是一样的,只是公式中的加法需要变成减法。因此,对应的公式可以写成:$$ w:=w+\alpha\nabla_{w}f(w) $$

梯度上升算法用来求函数的最大值,而梯度下降算法用来求函数的最小值。

基于上面的内容,我们来看一个Logistic回归分类器的应用例子,从图三可以看到我们采用的数据集。

图三

图三 下面将采用梯度上升法找到Logistic回归分类器在此数据集上的最佳回归系数。

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

中有100个样本点,每个点包含两个数值型特征:X1和X2。在此数据集上,我们将通过使用梯度上升法找到最佳回归系数,也就是拟合出Logistic回归模型的最佳参数。

梯度上升法的伪代码如下:

每个回归系数初始化为1

重复R次:

计算整个数据集的梯度

使用alpha × gradient更新回归系数的向量

返回回归系数

下面是梯度上升算法的具体实现:

def loadDataSet():
    dataMat = []; labelMat = [];
    fr = open('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]))
    return dataMat,labelMat
dataMat,labelMat = loadDataSet()
print(dataMat)
print("----------------------------")
print(labelMat)
[[1.0, -0.017612, 14.053064], [1.0, -1.395634, 4.662541], [1.0, -0.752157, 6.53862], [1.0, -1.322371, 7.152853], [1.0, 0.423363, 11.054677], [1.0, 0.406704, 7.067335], [1.0, 0.667394, 12.741452], [1.0, -2.46015, 6.866805], [1.0, 0.569411, 9.548755], [1.0, -0.026632, 10.427743], [1.0, 0.850433, 6.920334], [1.0, 1.347183, 13.1755], [1.0, 1.176813, 3.16702], [1.0, -1.781871, 9.097953], [1.0, -0.566606, 5.749003], [1.0, 0.931635, 1.589505], [1.0, -0.024205, 6.151823], [1.0, -0.036453, 2.690988], [1.0, -0.196949, 0.444165], [1.0, 1.014459, 5.754399], [1.0, 1.985298, 3.230619], [1.0, -1.693453, -0.55754], [1.0, -0.576525, 11.778922], [1.0, -0.346811, -1.67873], [1.0, -2.124484, 2.672471], [1.0, 1.217916, 9.597015], [1.0, -0.733928, 9.098687], [1.0, -3.642001, -1.618087], [1.0, 0.315985, 3.523953], [1.0, 1.416614, 9.619232], [1.0, -0.386323, 3.989286], [1.0, 0.556921, 8.294984], [1.0, 1.224863, 11.58736], [1.0, -1.347803, -2.406051], [1.0, 1.196604, 4.951851], [1.0, 0.275221, 9.543647], [1.0, 0.470575, 9.332488], [1.0, -1.889567, 9.542662], [1.0, -1.527893, 12.150579], [1.0, -1.185247, 11.309318], [1.0, -0.445678, 3.297303], [1.0, 1.042222, 6.105155], [1.0, -0.618787, 10.320986], [1.0, 1.152083, 0.548467], [1.0, 0.828534, 2.676045], [1.0, -1.237728, 10.549033], [1.0, -0.683565, -2.166125], [1.0, 0.229456, 5.921938], [1.0, -0.959885, 11.555336], [1.0, 0.492911, 10.993324], [1.0, 0.184992, 8.721488], [1.0, -0.355715, 10.325976], [1.0, -0.397822, 8.058397], [1.0, 0.824839, 13.730343], [1.0, 1.507278, 5.027866], [1.0, 0.099671, 6.835839], [1.0, -0.344008, 10.717485], [1.0, 1.785928, 7.718645], [1.0, -0.918801, 11.560217], [1.0, -0.364009, 4.7473], [1.0, -0.841722, 4.119083], [1.0, 0.490426, 1.960539], [1.0, -0.007194, 9.075792], [1.0, 0.356107, 12.447863], [1.0, 0.342578, 12.281162], [1.0, -0.810823, -1.466018], [1.0, 2.530777, 6.476801], [1.0, 1.296683, 11.607559], [1.0, 0.475487, 12.040035], [1.0, -0.783277, 11.009725], [1.0, 0.074798, 11.02365], [1.0, -1.337472, 0.468339], [1.0, -0.102781, 13.763651], [1.0, -0.147324, 2.874846], [1.0, 0.518389, 9.887035], [1.0, 1.015399, 7.571882], [1.0, -1.658086, -0.027255], [1.0, 1.319944, 2.171228], [1.0, 2.056216, 5.019981], [1.0, -0.851633, 4.375691], [1.0, -1.510047, 6.061992], [1.0, -1.076637, -3.181888], [1.0, 1.821096, 10.28399], [1.0, 3.01015, 8.401766], [1.0, -1.099458, 1.688274], [1.0, -0.834872, -1.733869], [1.0, -0.846637, 3.849075], [1.0, 1.400102, 12.628781], [1.0, 1.752842, 5.468166], [1.0, 0.078557, 0.059736], [1.0, 0.089392, -0.7153], [1.0, 1.825662, 12.693808], [1.0, 0.197445, 9.744638], [1.0, 0.126117, 0.922311], [1.0, -0.679797, 1.22053], [1.0, 0.677983, 2.556666], [1.0, 0.761349, 10.693862], [1.0, -2.168791, 0.143632], [1.0, 1.38861, 9.341997], [1.0, 0.317029, 14.739025]]
----------------------------
[0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0]

函数loadDataSet(),它的主要功能是打开文本文件testSet.txt并逐行读取。每行前两个值分别是X1和X2,第三个值是数据对应的类别标签。此外,为了方便计算,该函数还将X0的值设为1.0。

点击 testSet.txt获取文件。

sigmoid函数

def sigmoid(inx):
    return 1.0 / (1+np.exp(-inx))

gradAscent函数

def gradAscent(dataMatIn,classLabels):
    #❶(以下两行)转换为numpy矩阵数据类型
    dataMatrix = np.mat(dataMatIn)
    labelMat = np.mat(classLabels).transpose()
    
    m,n = np.shape(dataMatrix)
    alpha = 0.001
    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

梯度上升算法的实际工作是在函数gradAscent()里完成的,该函数有两个参数。第一个参数是dataMatIn,它是一个2维NumPy数组,每列分别代表每个不同的特征,每行则代表每个训练样本。我们现在采用的是100个样本的简单数据集,它包含了两个特征X1和X2,再加上第0维特征X0,所以dataMath里存放的将是100×3的矩阵。在❶处,我们获得输入数据并将它们转换成NumPy矩阵。这是本书首次使用NumPy矩阵,如果你对矩阵数学不太熟悉,那么一些运算可能就会不易理解。比如,NumPy对2维数组和矩阵都提供一些操作支持,如果混淆了数据类型和对应的操作,执行结果将与预期截然不同。第二个参数是类别标签,它是一个1×100的行向量。为了便于矩阵运算,需要将该行向量转换为列向量,做法是将原向量转置,再将它赋值给labelMat。接下来的代码是得到矩阵大小,再设置一些梯度上升算法所需的参数。

变量alpha是向目标移动的步长,maxCycles是迭代次数。在for循环迭代完成后,将返回训练好的回归系数。需要强调的是,在❷处的运算是矩阵运算。变量h不是一个数而是一个列向量,列向量的元素个数等于样本个数,这里是100。对应地,运算dataMatrix * weights代表的不是一次乘积计算,事实上该运算包含了300次的乘积。

❷中公式的前两行,是在计算真实类别与预测类别的差值,接下来就是按照该差值的方向调整回归系数

import numpy as np
weights = gradAscent(dataMat,labelMat)
print weights
[[ 4.12414349]
 [ 0.48007329]
 [-0.6168482 ]]

调用gradAscent函数,可以得到调整后的回归系数。

画出数据集和logistic回归最佳拟合直线的函数

def plotBestFit(dataMat1,labelMat1,weights):
    import matplotlib.pyplot as plt  
    dataArr = np.array(dataMat1)
    n = np.shape(dataArr)[0]
    xcord1 = []; ycord1 = [];
    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')
    
    x = np.arange(-3.0,3.0,0.1)
    # ❶  最佳拟合直线
    y1 = (-weights[0]-weights[1]*x)/weights[2] 
    y = y1.reshape(60,1)
    ax.plot(x,y)
    
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()

❶处设置了sigmoid函数为0。回忆5.2节,0是两个类别(类别1和类别0)的分界处。因此,我们设定 $ 0 = w0x0+ w1x1 + w2x2 $,然后解出X2和X1的关系式(即分隔线的方程,注意X0=1)。

%matplotlib inline
plotBestFit(np.array(dataMat),np.array(labelMat),weights)
500次迭代拟合直线

图四 梯度上升算法在500次迭代后得到的Logistic回归最佳拟合直线
从这个分类结果相当不错,从图上看只错分了两到四个点。但是,尽管雷子简单且数据集很小,这个方法却需要大量的计算(300次乘法)。因此下一节将对该算法稍作改进,从而使它可以用在真实数据集上。

2.4 训练算法:随机梯度上升

梯度上升算法在每次更新回归系数时都需要遍历整个数据集,该方法在处理100个左右的数据集时尚可,但如果有数十亿样本和成千上万的特征,那么该方案的计算复杂度就太高了。一种改进方法是一次仅用一个样本点来更新回归系数,该方法称为随机梯度上升算法。由于可以在新样本到来时对分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法。与“在线学习”相对应,一次处理所有数据被称作是“批处理”。

随机梯度上升算法可以写成如下的伪代码:

所有回归系数初始化1

对数据集中每个样本

   计算该样本的梯度
   使用alpha x gradient 更新回归系数值

返回回归系数值

以下是随机梯度上升算法实现代码。

def stocGradAscent0(dataMatrix, classLabels):
    m,n = shape(dataMatrix)
    alpha = 0.01
    weights = 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 

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

下面验证该方法的结果:

from numpy import *
dataArr,labelMat=loadDataSet()
weights=stocGradAscent0(array(dataArr),labelMat)
plotBestFit(dataArr,labelMat,weights)
image

执行完毕后将得到 图五 所示的最佳拟合直线图,该图与图四有一些相似之处。可以看到,拟合出来的直线效果还不错,但并不像图四那样完美。这里的分类器错分了三分之一的样本。

直接比较程序清单3和程序清单1的代码结果是不公平的,后者的结果是在整个数据集上迭代了500次才得到的。一个判断优化算法优劣的可靠方法是看它是否收敛,也就是说参数是否达到了稳定值,是否还会不断地变化?对此,我们在程序清单5-3中随机梯度上升算法上做了些修改,使其在整个数据集上运行200次。最终绘制的三个回归系数的变化情况如图六所示。

image

图六 运行随机梯度上升算法,在数据集的一次遍历中回归系数与迭代次数的关系图。回归系数经过大量迭代才能达到稳定值,并且仍然有局部的波动现象。

图六展示了随机梯度上升算法在200次迭代过程中回归系数的变化情况。其中的系数2,也就是图5-5中的X2只经过了50次迭代就达到了稳定值,但系数1和0则需要更多次的迭代。另外值得注意的是,在大的波动停止后,还有一些小的周期性波动。不难理解,产生这种现象的原因是存在一些不能正确分类的样本点(数据集并非线性可分),在每次迭代时会引发系数的剧烈改变。我们期望算法能避免来回波动,从而收敛到某个值。另外,收敛速度也需要加快。

对于图六存在的问题,可以通过修改程序stocGradAscent0 的随机梯度上升算法来解决,代码如下:

def stocGradAscent1(dataMatrix, classLabels, numIter=150):
    import numpy as np
    m,n = np.shape(dataMatrix)
    weights = np.ones(n)
    for j in range(numIter):         
        dataIndex = 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]
            del(dataIndex[randIndex])
    return weights

stocGradAscent1 与 stocGradAscent0 类似,但增加了两处代码来进行改进。

第一处改进在❶处。一方面,alpha在每次迭代的时候都会调整,这会缓解图六上的数据波动或者高频波动。另外。虽然alpha会随着迭代次数不断减小,但永远不会减小到0,这是因为❶中还存在一个常数项。必须这样做的原因是为了保证多次迭代之后新数据仍然具有一定得影响。如果要处理的问题是动态变化的,那么可以适当加大上述常数项,来确保新的值获得更大的回归系数。另一点值得注意的是,在降低alpha的函数中,alpha每次减少** 1/(j+i)** ,其中j时迭代次数,i是样本点的下标。这样当j<<max(i)时,alpha就不是严格下降的。避免参数的严格下降也常见于模拟退火算法等其他优化算法。

第二个改进的地方在❷处,这里通过随机选取样本来更新回归系数。这种方法将减少周期性的波动(如图五中的波动)。这种方法每次随机从列表中选出一个值,然后从列表中删掉该值(再进行下一次迭代)。

此外,改进算法还增加了一个迭代次数作为第3个参数。如果该参数没有给定的话,算法将默认迭代150次。如果给定,那么算法将按照新的参数值进行迭代。

图七 显示了每次迭代时各个回归系数的变化情况。
image

该方法比采用固定alpha的方法收敛速度更快。

比较图七和图六可以看到两点不同。第一点是,图七中的系数没有像图六里那样出现周期性的波动,这归功于stocGradAscent1()里的样本随机选择机制;第二点是,图七的水平轴比图六短了很多,这是由于stocGradAscent1()可以收敛得更快。这次我们仅仅对数据集做了20次遍历,而之前的方法是500次。

看一下在同一个数据集上的分类效果:

%matplotlib inline
dataMat,labelMat = loadDataSet()
weights = stocGradAscent1(np.array(dataMat),np.array(labelMat))
plotBestFit(np.array(dataMat),np.array(labelMat),weights)
png

从结果图中可以看出,分割线达到了与GradientAscent()差不多的效果,但是所使用的计算量更少。默认迭代次数是150,可以通过stocGradAscent()的第3个参数来对此进行修改。

3 示例:从疝气病症预测病马的死亡率

下面将使用Logistic回归来预测患有疝病的马的存活问题。这里的数据1包含368个样本和28个特征。我并非育马专家,从一些文献中了解到,疝病是描述马胃肠痛的术语。然而,这种病不一定源自马的胃肠问题,其他问题也可能引发马疝病。该数据集中包含了医院检测马疝病的一些指标,有的指标比较主观,有的指标难以测量,例如马的疼痛级别。

数据集来自2010年1月11日的UCI机器学习数据库(http://archive.ics.uci.edu/ml/datasets/Horse+Colic)。该数据最早由加拿大安大略省圭尔夫大学计算机系的Mary McLeish和Matt Cecile收集。

示例:使用Logistic回归估计马疝病的死亡率

  1. 收集数据:给定数据文件。
  2. 准备数据:用Python解析文本文件并填充缺失值。
  3. 分析数据:可视化并观察数据。
  4. 训练算法:使用优化算法,找到最佳的系数。
  5. 测试算法:为了量化回归的效果,需要观察错误率。根据错误率决定是否回退到训练阶段,通过改变迭代的次数和步长等参数来得到更好的回归系数。
  6. 使用算法:实现一个简单的命令行程序来收集马的症状并输出预测结果。

另外需要说明的是,除了部分指标主观和难以测量之外,该数据集还存在一个问题,数据集中有30%的数据值是缺失的。下面将首先介绍如何处理数据集中的数据缺失问题,然后再利用Logistic回归和随机梯度上升算法来预测病马的生死。

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

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

下面是一些可选的做法:

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

在数据预处理阶段需要做两件事:第一,所有的缺失值必须用一个实数来替换,因为我们使用的NumPy数据类型不允许包含缺失值。这里选择实数0来替换所有缺失值,恰好能适用于Logistic回归。原因在于,我们需要的是一个在更新时不会影响系数的值。回归系数的更新公式如下:
$$ weights = weights + alpha * error * dataMatrix[randIndex] $$

如果dataMatrix的某特征对应值为0,那么该特征的系数将不做更新,即:
$$ weights = weights $$

另外,由于sigmoid(0)=0.5,即它对结果的预测不具有任何倾向性,因此上述做法也不会对误差项造成任何影响。基于上述原因,将缺失值用0代替既可以保留现有数据,也不需要对优化算法进行修改。此外,该数据集中的特征取值一般不为0,因此在某种意义上说它也满足“特殊值”这个要求。

预处理中做的第二件事是,如果在测试数据集中发现了一条数据的类别标签已经缺失,那么我们的简单做法是将该条数据丢弃。这是因为类别标签与特征不同,很难确定采用某个合适的值来替换。采用Logistic回归进行分类时这种做法是合理的,而如果采用类似kNN的方法就可能不太可行。

原始的数据集经过预处理之后保存成两个文件:horseColicTest.txthorseColicTraining.txt。现在我们有一个“干净”可用的数据集和一个不错的优化算法,下面将把这些部分融合在一起训练出一个分类器,然后利用该分类器来预测病马的生死问题。

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

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

Logistic回归分类函数:

def classifyVector(inX,weights):
    prob = sigmoid(sum(inX*weights))
    if prob > 0.5:
        return 1.0
    else:
        return 0.0

函数classifyVector(),它以回归系数和特征向量作为输入来计算对应的Sigmoid值。如果Sigmoid值大于0.5函数返回1,否则返回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),np.array(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 "错误率 = %f" %errorRate
    return errorRate

函数colicTest(),是用于打开测试集和训练集,并对数据进行格式化处理的函数。该函数首先导入训练集,同前面一样,数据的最后一列仍然是类别标签。数据最初有三个类别标签,分别代表马的三种情况:“仍存活”、“已经死亡”和“已经安乐死”。这里为了方便,将“已经死亡”和“已经安乐死”合并成“未能存活”这个标签 。数据导入之后,便可以使用函数stocGradAscent1()来计算回归系数向量。这里可以自由设定迭代的次数,例如在训练集上使用500次迭代,实验结果表明这比默认迭代150次的效果更好。在系数计算完成之后,导入测试集并计算分类错误率。整体看来,colicTest()具有完全独立的功能,多次运行得到的结果可能稍有不同,这是因为其中有随机的成分在里面。如果在stocGradAscent1()函数中回归系数已经完全收敛,那么结果才将是确定的。

def multiTest():
    numTests = 10;errorSum = 0.0
    for k in range(numTests):
        errorSum += colicTest()
    print " %d次平均错误率是:%f" % (numTests,errorSum/float(numTests))

最后一个函数是multiTest(),其功能是调用函数colicTest()10次并求结果的平均值。调用multiTest()函数,可以看到打印的10次错误率:

multiTest()
/usr/local/lib/python2.7/site-packages/ipykernel_launcher.py:2: RuntimeWarning: overflow encountered in exp
  


错误率 = 0.358209
错误率 = 0.313433
错误率 = 0.462687
错误率 = 0.402985
错误率 = 0.358209
错误率 = 0.402985
错误率 = 0.313433
错误率 = 0.373134
错误率 = 0.373134
错误率 = 0.402985
 10次平均错误率是:0.376119

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

小结

Logistic回归的目的是寻找一个非线性函数Sigmoid的最佳拟合参数,求解过程可以由最优化算法来完成。在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以简化为随机梯度上升算法。
随机梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。此外,随机梯度上升是一个在线算法,它可以在新数据到来时就完成参数更新,而不需要重新读取整个数据集来进行批处理运算
机器学习的一个重要问题就是如何处理缺失数据。这个问题没有标准答案,取决于实际应用中的需求。现有一些解决方案,每种方案都各有优缺点。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容