Bootstrap

【Python机器学习】Logistic回归——基于最优化方法的最佳回归系数确定

Sigmoid函数的输入记为z,由下面公式得出:

z=w_{0}x_{0}+w_{1}x_{1}+w_{2}x_{2}+...+w_{n}x_{n}

如果采用向量写法,上述公式可以写成z=w^{T}x,它表示将这两个数值向量对应元素相乘然后全部加起来即得到z值。其中的向量x是分类器的输入数据,向量w也就是我们要找到的最佳参数(系数),从而使得分类器尽可能地精确。为了寻找该最佳参数,需要用到最优化理论的一些知识。

梯度上升法

梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。如果梯度记为\bigtriangledown,则函数f(x,y)的梯度可以表达为:

\bigtriangledown f(x,y)=\binom{\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)必须要在待计算的点上有定义并有可微。下面是一个具体的例子:

上图中,梯度上升算法沿梯度方向移动了一步。可以看到,梯度算子懂事指向函数值增长最快的方向。这里所说的是移动方向,而未提到移动量的大小。该量值称为步长,记作\alpha。用向量来表示的话,梯度上升算法的迭代公式如下:

w:=w+\alpha \bigtriangledown _{w}f(w)

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

除了梯度上升算法,还有梯度下降算法,区别只是公式中的加法变成减法,对应的公式:

w:=w-\alpha \bigtriangledown _{w}f(w)

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

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

如下面图中,有100个样本点,每个点包含两个数值型特征:X1和X2

在这个数据集上,我们将通过使用梯度上升法找到最佳回归函数,也就是拟合出Logistic回归模型的最佳参数。

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

每个回归参数初始化为1

重复R次:

    计算整个数据集的梯度

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

返回回归系数

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

def loadDataSet():
    #主要功能:打开文本文件testSet.txt,并逐行读取
    dataMat=[]
    labelMat=[]
    fr=open('testSet.txt')
    for line in fr.readlines():
        lineArr=line.strip().split()
        #每行前两个值分别是X1、X2,第三个值是数据对应的类别标签
        #为了方便计算,函数将X0的值设为1.0
        dataMat.append([1.0,float(lineArr[0]),float(lineArr[1])])
        labelMat.append(int(lineArr[2]))
    return dataMat,labelMat
def sigmoid(inX):
    return 1.0/(1+exp(-inX))
def gradAscent(dataMatIn,classLabels):
    #dataMatIn是一个2维NumPy数组,每列分别代表每个不同的特征,每行代表每个训练样本,里面存放的是100*3的矩阵
    #classLabels是类别标签,是一个1*100的行向量。
    #转换成NumPy矩阵数据类型
    dataMatrix=mat(dataMatIn)
    #为了方便计算,把classLabels转换成列向量
    labelMat=mat(classLabels).transpose()
    m,n=shape(dataMatrix)
    #alpha是向目标移动的步长,maxCycles是迭代次数。
    alpha=0.001
    maxCycles=500
    weights=ones((n,1))
    #矩阵相乘
    for k in range(maxCycles):
        h=sigmoid(dataMatrix*weights)
        error=(labelMat-h)
        weights=weights+alpha*dataMatrix.transpose()*error
    return weights

上述代码中,梯度上升算法的实际工作是gradAscent()函数中完成的,需要注意的事,在这个函数中的循环部分,运算是矩阵运算,变量h不是一个数而是一个列向量,列向量的元素个数等于样本个数,这里是100。对应的,运算dataMatrix*weights达标不止一次乘积计算,事实上该运算包含了300次的乘积。

实际运行效果:

dataArr,labelMat=loadDataSet()
print(gradAscent(dataArr,labelMat))

分析数据:画出决策边界

上面已经解出了一组回归系数,它确定了不同类别数据之间的分隔线。

下面想办法画出该分隔线,使得优化的过程便于理解:

def plotBestFit(weights):
    import matplotlib.pyplot as plt
    dataMat,labelMat=loadDataSet()
    dataArr=array(dataMat)
    n=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=arange(-3.0,3.0,0.1)
    y=(-weights[0]-weights[1]*x)/weights[2]
    ax.plot(x,y)
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.show()

需要注意的是,y赋值的时候,设置了Sigmoid函数为0,也就是设定0=w_{0}x_{0}+w_{1}x_{1}+w_{2}x_{2},然后解出X2和X1的关系式(即分隔线的方程,需要注意的是X0=1)。

运行结果:

这个分类结果相当不错,从图上看只错分了两到四个点。但是,尽管例子简单且数据集很小,这个方法却要需要大量的计算(300次乘法),所以算法需要稍作改进,从而使它可以用在真实数据集上。

训练算法:随机梯度上升

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

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

所有回归系数初始化为1

对数据集中每个样本:

    计算该样本的梯度

    使用alpha*gradient更新回归系数值

返回回归系数值

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

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

可以看到,随机梯度上升算法与梯度上升算法在代码上很相似,但也有一些区别:

1、后者的变量h和误差error都是向量,而前者全是数值;

2、前者没有矩阵的转换过程,所有变量的数据类型都是NumPy数组。

验证算法效果:

dataArr,labelMat=loadDataSet()
weights=stocGradAscent0(array(dataArr),labelMat)
print(weights)
plotBestFit(weights)

目前得到的最佳拟合直线图,与之前的结果有一些相似之处,但是效果差一些,这里的分类器错分了1/3的样本

但是梯度上升算法时迭代了500次之后的记过,这样的对比是不公平的。一个判断优化算法优劣的可靠方法是看它是否收敛,也就是说参数是否达到了稳定值,是否还会不断地变化。

为了更搞笑得到更优的参数,下面对算法进行改进:

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

改进内容:

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

2、通过随机选取样本来更新回归系数。这种方法将减少周期性的波动。这种方法每次随机从列表中选出一个值,然后从列表中删除该值。

此外,改进算法还增加了一个迭代次数作为第三个参数,如果参数没有给定,默认迭代150次。

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

优化后的分类结果:

;