利用logistic回归进行分类的主要思想是:根据现有的数据对分类边界建立回归公式,以此进行分类。训练分类器时的做法就是寻找最佳拟合参数,使用的是最优化算法。接下来介绍这个二值输出分类器的数学原理。
首先阐述logistic回归的定义,然后再介绍一些最优化算法,其中包括梯度上升法和一个改进的随机梯度上升法。这些算法将会用于分类器的训练。
1. 基于logistic回归和sigmoid函数的分类
我们需要一个函数,接受所有的输入然后预测所有的输出类别。在两个类别的情况下,函数会输出1和0 。
阶跃函数具有这种性质,但是该函数在从0瞬间跳跃到1的这个瞬间过程很难处理。我们换用另外一个在数学上更容易处理,也具有类似性质的函数,就是sigmoid函数。计算公式如下所示:
下图给出了sigmoid函数在不同坐标尺度下的两条曲线图。当x为0时,sigmoid函数值为0.5,随着x的增大,对应的sigmoid函数值逼近1;而x减小时,sigmoid函数逼近0。假如横坐标刻画足够大,sigmoid函数看起来更像一个阶跃函数。
为了实现logistic回归分类器,我们可以在每个特征上都乘一个回归系数,把所有的结果相加,如果大于0.5,数据将被分入1类,小于0.5将被分入0类。确定分类器后,现在问题就是怎么确定最佳回归系数,如何确定它们的大小?
2. 基于最优化方法确定最佳回归系数
2.1梯度上升法
思想:要找到某个函数的最大值,最好的方法是沿着该函数的梯度方向探寻。函数的梯度又下式表示:
梯度意味要沿着x的方向移动:,沿y的方向移动:。其中f(x,y)必须要在待计算点上有定义并且可微。
一个具体的函数例子如下:
上图中的梯度上升算法沿梯度方向移动了一步,可以看到,梯度算子总是指向函数增长最快的方向。这里所说的是移动方向,而未提到移动量的大小。该量值称为步长,记做alpha。 用向量来表示,梯度算法的迭代公式:
该过程一直被迭代,直到到达某个停止条件为止,比如迭代次数到达某个指定值或则某个可以允许的误差。
2.2训练算法:使用梯度上升找到最佳参数
梯度上升的伪代码如下:
下面是梯度上升算法的具体实现代码。
from numpy import * def loadDataSet(): dataMat = []; labelMat = [] fr = open('testSet.txt') for line in fr.readlines(): #print(line) lineArr = line.strip().split() #print(lineArr) # 每行前两个值是X1和X2,第三个值是标签,为了方便计算将X0预设为1 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)) # 梯度计算 # dataMatIn是2维Numpy数组,每列对应不同特征,每行是一个训练样本 # 现在里面有X0,X1,X2的100*3的矩阵 def gradAscent(dataMatIn, classLabels): dataMatrix = mat(dataMatIn) #转化为Numpy矩阵 labelMat = mat(classLabels).transpose() #由行向量转化为列向量,矩阵的转置 m,n = shape(dataMatrix) # 步长 alpha = 0.001 # 迭代次数 maxCycles = 500 weights = ones((n,1)) for k in range(maxCycles): #heavy on matrix operations # 每一个样本乘上一个回归系数 h = sigmoid(dataMatrix*weights) #矩阵相乘,h是一个列向量 1*100 error = (labelMat - h) #vector subtraction weights = weights + alpha * dataMatrix.transpose()* error #按差值方向调整回归系数 # 这里dataMatrix.transpose()* error就是当前对应的梯度 return weights
输出的结果为:
2.3 分析数据:画出决策边界
上面已经求解了一组回归系数,确定了不同的数据之间的分界线,现在画出分界线,让优化过程更易理解。
画出数据集及最佳拟合直线:
#将分类给画出来,画出最佳的拟合直线 def plotBestFit(weights): """ :param weights: 最佳回归系数数组 :return: """ dataMat, labelMat = loadDataSet() #加载数据集,返回数据集合分类 dataArr = array(dataMat) #将数据集数组化 dataShape = shape(dataArr) #data的形状 print("data的形状:", dataShape) n = dataShape[0] #形状的第一个是行数(即数据集的个数),第二个是列数(即数据集的特征) xcord1 = [] #分类为1的点 ycord1 = [] xcord0 = [] #分类为0的点 ycord0 = [] for i in range(n): if int(labelMat[i]) == 1: #如果分类为1,添加到1分类的点集,否者返回到0分类的点集 xcord1.append(dataArr[i, 1]) #这个dataArr有3列,其中第0列为1,第1,2列为x,y ycord1.append(dataArr[i, 2]) else: xcord0.append(dataArr[i, 1]) ycord0.append(dataArr[i, 2]) fig = plt.figure() #figure()函数创建一个新的图 ax = fig.add_subplot(111) #add_subplot()函数在一张figure里面生成多张子图参数111,表示1行1列第1个位置 ax.scatter(xcord1, ycord1, s=30, c='red', marker='s') #散点图 ax.scatter(xcord0, ycord0, s=30, c='green') # 直线 x 的范围 x = arange(-3.0, 3.0, 0.1) # (start, end, step)可以调step执行起来,看看图 # 画出直线,weights[0]*1.0+weights[1]*x+weights[2]*y=0 # 之前计算时对原始数据做了拓展,将两维拓展为三维,第一维全部设置为1.0,实际他是一个 y=ax+b, b常量 y = (-weights[0]-weights[1]*x)/weights[2] ax.plot(x, y) #画出直线 plt.xlabel('x1') #X轴标记为x1 plt.ylabel('x0') #Y轴标记为x0 plt.show() plotBestFit(weights.getA()) #getA()将weights矩阵转换为数组,getA()函数与mat()函数的功能相反 """ 如果是矩阵的话会报这样的错: "have shapes {} and {}".format(x.shape, y.shape)) ValueError: x and y must have same first dimension, but have shapes (60,) and (1, 60) 为啥要用数组呢?因为 x = arange(-3.0, 3.0, 0.1),len(x) = [3-(-3)]/0.1 = 60 而weights是矩阵的话,y = (-weights[0]-weights[1]*x)/weights[2],len(y) = 1,有60个x,y只有一个,你这样都画不了线 而weights是数据的话,len(y) = 60 """
输出的结果:
从图上看,分类效果相当不错。只分错了4个点。尽管数据集很小,但是却需要计算300次乘法。下面我们将对算法进行改进,从而使他们可以再真实数据集上使用。
2.4 训练算法:随机梯度上升
梯度上升算法在每次更新回归系数时都需要遍历整个数据集,该方法在处理小数量的数据集时还可以,但是在大数据特征计算时改方法的复杂度就太高了。一种改进方法就是一次仅利用一个样本点来更新回归系数,该方法称为随机梯度上升算法。由于可以再新样本到来时对分类器进行增量式更新,因而称该算法是一个在 线学习算法。与在线学习相对应的就是一次性处理所有数据的 批处理 。
以下是随机梯度算法的实现代码:
def stocGradAscent0(dataMatrix, classLabels): m,n = shape(dataMatrix) alpha = 0.01 weights = ones(n) #initialize to all ones for i in range(m): # h,error全是数值,没有矩阵转换过程。 h = sigmoid(sum(dataMatrix[i]*weights)) error = classLabels[i] - h weights = weights + alpha * error * dataMatrix[i] return weights
画图:
拟合的不如上图的那样完美,这里大概错分了三分之一的样本。但是直接这样比较是不公平的,梯度上升法是在整个数据集上迭代了500次才得到的结果。我们判断一个算法优劣是看他是否收敛,还是会不断的变化?
下面我们修改随机梯度方法,使其在数据集上运行200次。最终绘制的三个回归系数变化如图:
上图展示了随机梯度上升算法在200次迭代过程中回归系数的变化情况。其中X2,也就是系数2,只经过了50次迭代就达到了稳定值,但是系数1和0则需要多次迭代。另外值得注意的是,在大的波动停止后还有一些小的周期性波动。产生这种情况的原因是存在一些不能正确分类的样本点,在每次迭代后回引发系数的剧烈改变,我们希望算法能够避免来回波动,尽快的收敛到某个具体的值。另外收敛速度也需要加快。
针对上图的问题,可以通过修改程序来解决问题,具体代码如下:
def stocGradAscent1(dataMatrix, classLabels, numIter=150): m,n = shape(dataMatrix) weights = ones(n) #initialize to all ones for j in range(numIter): dataIndex = list(range(m)) for i in range(m): alpha = 4/(1.0+j+i)+0.0001 # alpha在每次迭代时都会调整 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
比较上面两个图,第一点图5-7中的系数没有像图5-6那样出现周期性波动,这要归功于stoGradAscent1()中的样本随机选择机制,第二点是,图5-7比图5-6的横坐标要短很多,这是由于更新过的函数可以收敛的更快,我们仅对数据集进行了20次遍历,而之前进行了500次。
观察这次的分类结果:
可以看到,分隔效果差不多但是计算的量更少。
默认迭代次数是150,可以自行修改。