5.1 基于Logistic回归和Sigmoid函数的分类
Logistic回归
优点:计算代价不高,易于理解和实现。
缺点:容易欠拟合,分类精度可能不高。
适用数据类型:数值型和标称型数据。
我们想要的函数是,能接受所有的输入然后预测出类别。该函数称为海维赛德阶跃函数,或者直接称为单位阶跃函数。然而,该函数存在一些问题:在跳跃点上从0瞬间跳跃到1,这个瞬间跳跃过程有时很难处理。而另一个函数也有类似的性质,且在数学上更易处理,它就是Sigmoid函数。公式如下:
Sigmoid函数图如图,当横坐标刻度足够大时,Sigmoid函数看起来就像阶跃函数一样。
因此,为了实现Logistic回归分类器,我们可以在每个特征上都乘以一个回归系数,然后把所有的结果值相加,将总和带入Sigmoid函数中,得到一个范围在0~1之间的数值。大于0.5的数据被分入1类,小于0.5被归入0类。所以,Logistic回归也可以被看成是一种概率估计。
5.2 基于最优化方法的最佳回归系数确定
Sigmoid函数的输入记为z,由下面公式得出:
如果采用向量的写法,上述公式可以写成,它表示将这两个数值向量对应元素相乘然后全部加起来即得到z值。其中的向量x是分类器的输人数据,向量w也就是我们要找到的最佳参数(系数),从而使得分类器尽可能地精确。
5.2.1 梯度上升法
梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
该梯度意味着要沿x的方向移动,沿y的方向移动
。其中,函数f(x,y)必须要在待计算的点上有定义并且微。
梯度算子总是指向函数值增长最快的方向,而移动量的大小用步长来表示,记作α。用向量表示,梯度算法的迭代公式如下:
该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围。
梯度下降算法
梯度上升算法用来求函数的最大值,而梯度下降算法用来求函数的最小值。
5.2.2 训练算法:使用梯度上升找到最佳参数
采用数据集中每个点包含两个数值型特征:X1和X2。我们将通过使用梯度上升法找到最佳回归系数,拟合出Logistic回归模型的最佳参数。
伪代码如下:
新建 logRegres.py 文件,输入代码:
from numpy import *
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
def sigmoid(inX):
return 1.0/(1+exp(-inX))
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): #矩阵运算
h = sigmoid(dataMatrix*weights) #多次矩阵运算
error = (labelMat - h) #vector subtraction
weights = weights + alpha * dataMatrix.transpose()* error #matrix mult
return weights
代码在开头提供了一个便利函数 loadDataSet(),它的主要功能是打开文本文件 testSet.txt 并逐行读取。每行前两个值分别是X1和X2,第三个值是数据对应的类别标签。此外,为了方便计算,该函数还将X0的值设为1.0。接下来的函数是 sigmoid()。
梯度上升算法的实际工作是在函数 gradAscent () 里完成的,该函数有两个参数。第一个参数是dataMathIn,它是一个2维NumPy数组,每列分别代表每个不同的特征,每行则代表每个训练样本。我们现在采用的是100个样本的简单数据集,它包含了两个特征X1和X2,再加上第0维特征X0,所以dataMathln里存放的将是100×3的矩阵。在处,我们获得输人数据并将它们转换成NumPy矩阵。第二个参数是类别标签,它是一个1×100的行向量。为了便于矩阵运算,需要将该行向量转换为列向量,做法是将原向量转置,再将它赋值给labelMat。接下来的代码是得到矩阵大小,再设置一些梯度上升算法所需的参数。
变量alpha是向目标移动的步长,maxCycles是迭代次数。在for循环迭代完成后,将返回训练好的回归系数。需要强调的是,所做的运算是矩阵运算。变量h不是一个数而是一个列向量,列向量的元素个数等于样本个数,这里是100。对应地,运算dataMatrix * weights代表的不止一次乘积计算,事实上该运算包含了300次的乘积。
import logRegres
dataArr,labelMat = logRegres.loadDataSet()
print(logRegres.gradAscent(dataArr,labelMat))
5.2.3 分析数据:画出决策边界
为logRegres.py添加代码:
def plotBestFit(weights):
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()
代码中设置了sigmoid函数为0。0是两个分类(类别1和类别0)的分界处。因此,设定 0=w0x0 + w1x1 + w2x2,然后解出X1和X2的关系式(即分割线的方程,注意X0=1)。
import logRegres
dataArr,labelMat = logRegres.loadDataSet()
weights = logRegres.gradAscent(dataArr,labelMat)
logRegres.plotBestFit(weights.getA())
5.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 = sigmoid(sum(dataMatrix[i]*weights))
error = classLabels[i] - h
weights = weights + alpha * error * dataMatrix[i]
return weights
随机梯度上升算法与梯度上升算法不同之处:
- 后者的变量h和误差error都是向量,而前者则全是数值;
- 前者没有矩阵的转换过程,所有变量的数据类型都是NumPy数组。
import logRegres
from numpy import *
dataArr,labelMat = logRegres.loadDataSet()
weights = logRegres.stocGradAscent0(array(dataArr),labelMat)
logRegres.plotBestFit(weights)
拟合直线效果尚可,但分类效果并不好。两者算法效果虽然有差别,但迭代次数也不相同,前者迭代次数多于后者。一个判断优化算法优劣的可靠方法是看它是否收敛,也就是说参数是否达到稳定值,是否还会不断变化。
上图展示了随机梯度上升算法在200次迭代过程中回归系数的变化情况。其中系数2,即X2经过50次迭代就达到了稳定值,而其他两个系数则需要更多次的迭代。同时,在大的波动停止后,还有一些小的周期性波动。产生这种现象的原因是存在一些不能正确分类的样本点(数据集并非线性可分),在每次迭代时会引发系数的剧烈改变。我们期望算法能避免来回波动,从而收敛到某个值,收敛速度也需要加快。
对于上图存在的问题,可以对随机梯度上升算法进行改进来解决:
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 #apha decreases with iteration, does not
randIndex = int(random.uniform(0,len(dataIndex)))#go to 0 because of the constant
h = sigmoid(sum(dataMatrix[randIndex]*weights))
error = classLabels[randIndex] - h
weights = weights + alpha * error * dataMatrix[randIndex]
del(dataIndex[randIndex])
return weights
- 第一处改进:alpha在每次迭代时都会调整,这能缓解数据波动或者高频波动。另外,虽然alpha会随着迭代次数不断减少,但永远不会减小到0,这是因为式子中存在一个常数项。必须这样做的原因是为了保证在多次迭代后新数据仍然具有一定的影响。如果要处理的问题是动态变化的,那么可以适当加大上述常数项来确保新的值获得更大的回归系数。另一点值得注意的是,在降低alpha的函数中,alpha每次减少1/(j+i),其中j是迭代次数,i是样本点的下标。这样当j<<max(i)时,alpha就不是严格下降的。避免参数的严格下降也常见于模拟退火算法等其他优化算法中。
- 第二处改进:randomIndex通过随机选取样本来更新回归系数。这种方法将减少周期性的波动,每次随机从列表中选出一个值,然后从列表中删掉该值(再进行下一次迭代)。
- 改进算法还增加了一个迭代次数作为第3个参数。如果该参数没有给定的话,算法将默认迭代150次。如果给定,那么算法将按照新的参数值进行迭代。
下图显示了每次迭代时各个回归系数的变化情况。
两幅图可以看到有两点不同。第一,后者的系数没有像前者那样出现周期性的波动,原因是函数中的样本随机选择机制;第二,后者的水平轴比前者短了很多,这是由于函数可以收敛得更快。这次仅仅对数据集做了20次遍历,而之前的方法是500次。
接下来观察在同一个数据集上的分类效果:
结果图可以看出,使用更少的计算量获得了与梯度上升算法差不多的效果。
5.3 示例:从疝气病症预测病马的死亡率
5.3.1 准备数据:处理数据中的缺失值
当数据缺失时可选的做法:
- 使用可用特征的均值来填补缺失值;
- 使用特殊值来填补缺失值;
- 忽略有缺失值的样本;
- 使用相似样本的均值填补缺失值;
- 使用另外的机器学习算法预测缺失值
对下一节要用的数据集进行预处理:
- 所有缺失值用一个实数值来替换,因为我们使用的NumPy数据类型不允许包含缺失值。这里选择实数0来替换所有缺失值,恰好能适用于Logistic回归。回归系数更新公式如下:
如果dataMatrix某特征对应值为0,那么该特征的系数将不做更新:
另外,由于 sigmoid(0)=0.5 ,即它对结果的预测不具有任何倾向性,因此上述做法也不会对误差项造成任何影响。 - 如果在测试数据集中发现了一条数据的类别标签已经缺失,那么我们的简单做法是将该条数据丢弃。这是因为类别标签与特征不同,很难确定采用某个合适的值来替换。
5.3.2 测试算法:用Logistic回归进行分类
使用Logistic回归方法进行分类并不需要做很多工作,所需做的只是把测试集上每个特征向量乘以最优化方法得来的回归系数,再将成绩结果求和,最后输入到Sigmoid函数中即可。如果对应的Sigmoid值大于0.5就预测类别标签为1,否则为0。
将下面的代码添加到logRegres.py中:
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(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(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)))
第一个函数是 classifyVector(),它以回归系数和特征向量作为输入来计算对应的Sigmoid值。如果Sigmoid值大于0.5函数返回1,否则返回0。
接下来的函数是 colicTest(),是用于打开测试集和训练集,并对数据进行格式化处理的函数。该函数首先导入训练集,同前面一样,数据的最后一列仍然是类别标签。数据最初有三个类别标签,分别代表马的三种情况:“仍存活”、“已经死亡”和“已经安乐死”。为了方便,将“已经死亡”和“已经安乐死”合并成“未能存活”这个标签。数据导入之后,便可以使用函数 stocGradAscent1() 来计算回归系数向量。这里可以自由设定迭代的次数,例如在训练集上使用500次迭代,实验结果表明这比默认迭代150次的效果更好。在系数计算完成之后,导入测试集并计算分类错误率。整体看来,colicTest() 具有完全独立的功能,多次运行得到的结果可能稍有不同,这是因为其中有随机的成分在里面。如果在 stocGradAscent1() 函数中回归系数已经完全收敛,那么结果才将是确定的。
最后一个函数是 multiTest(),其功能是调用函数 colicTest() 10次并求结果的平均值
import logRegres
logRegres.multiTest()
运行过程中可能会出现“RuntimeWarning: overflow encountered in exp”报错,原因是sigmoid函数公式出现了上溢。分析及解决方法可参考下面的链接:https://blog.csdn.net/CY_TEC/article/details/106083366
多次运行得到结果可能稍有不同,这是因为其中有随机的成分。如果在 stocGradAscent1() 函数中回归系数已经完全收敛,那么结果才将是确定的。
5.4 本章小结
Logistic回归的目的是寻找一个非线性函数Sigmoid的最佳拟合参数,求解过程可以由最优化算法来完成。在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以简化为随机梯度上升算法。
随机梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。此外,随机梯度上升是一个在线算法,它可以在新数据到来时就完成参数更新,而不需要重新读取整个数据集来进行批处理运算。
机器学习的一个重要问题就是如何处理缺失数据。这个问题没有标准答案,取决于实际应用中的需求。现有一些解决方案,每种方案都各有优缺点。