前言
logistic回归的主要思想:根据现有数据对分类边界建立回归公式,以此进行分类
所谓logistic,无非就是True or False两种判断,表明了这其实是一个二分类问题
LR优点:计算代价不高,易于理解和实现
我们又知道回归就是对一些数据点拟合成线性函数,但是线性函数的值域是无穷的
所以logistic和回归加在一起,就是要把取值范围从无穷映射到(0,1)上,使之成为一个二分类器
由前言中知道,我们需要一个阶跃函数,不管接受什么输入,输出的都是0或1
所以本文会介绍怎么拟合一个回归函数,然后再把它作为自变量输入丢进一个阶跃函数,然后输出一个(0,1)的二值结果
这就是所谓的logistic回归
本文的参考书是《机器学习实战》
sigmoid函数
由前言中知道,我们需要一个阶跃函数,不管接受什么输入,输出的都是0或1
sigmoid函数刚好满足这样的特性:
它的图形如下:
当z=0时,函数值为0.5。
把自变量带入函数会得到一个0~1之间的数值,这时就可以把大于0.5的数据分为1类,小于0.5的归为0类。
所以logistic回归可以被看成是一种概率估计。
这样就完成了把无穷取值范围映射到0和1的使命。
接下来需要做的就是就是对数据点进行回归,使回归得到的结果成为sigmoid函数的输入
最佳回归系数
线性回归的形式是:
其中W就是回归系数向量,向量的每个元素对应数据的一个维度也就是一种特征
现在的任务就是确定最佳回归系数,常用的方法有最小二乘法、梯度上升法等最优化方法。
本文主要是使用梯度上升法作为讨论的基础
梯度上升法
梯度上升基于的思想是:要找函数的最大值,最好的方法是沿着该函数的梯度方向探寻,因为梯度总是指向函数增长最快的方向
有了方向,那么还要有步长才能朝着最优值移动,这个步长可以自己指定,这里设步长为alpha,那么算法的迭代公式为:
顺便提一下,相应的有个叫梯度下降的算法,用于求函数的最小值,只用把中间的加号变成减号就行了
梯度上升的伪代码如下:
每个回归系数初始化为1
重复R次:
计算整个数据集的梯度
使用 alpha * gradient 更新回归系数向量
返回回归系数
代码实现如下:
# 载入测试数据,返回测试数据集和类别标签
def loadDataSet():
dataMat = []
labelMat = []
fr = open(r'E:mlmachinelearninginactionCh05 estSet.txt')
dataFromFile = fr.readlines()
print len(dataFromFile)
for line in dataFromFile:
lineArr = line.strip().split()
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+exp(-inX))
# =====================================
# 梯度上升算法
# 输入:
# dataMatIn: 2维数组,每列代表一种特征
# classLabels: 类别标签
# 返回:逻辑回归参数
# =====================================
def gradAscent(dataMatIn, classLabels):
dataMat = mat(dataMatIn)# 装换成numpy矩阵
labelMat = mat(classLabels).transpose()# 转置
m, n = shape(dataMat)
weights = ones((n, 1))# 初始化权重都为1
alpha = 0.001
maxCycles = 500# 迭代次数
for i in range(maxCycles):
# 以下三行代码是梯度上升算法的具体实现
h = sigmoid(dataMat * weights)# 矩阵乘法
error = (labelMat - h)
weights = weights + alpha * dataMat.transpose() * error
return weights
关于代码有两点需要指出,第10行中loadData函数在特征列加了一列全是1的特征。
21~23行代码是梯度上升的具体实现,由上面提到的迭代公式到这里的实现需要一些数学推导,大概来说如下:
本文的数据集用的是《机器学习实战》的testSet.txt数据集,概览图如下:
导入数据集到上面的函数得到如下结果:
这就是回归系数
画出决策函数
为了使优化过程便于理解,我们可以把这个数据集已经刚才已经得到的回归线可视化
代码如下:
# 画出数据集合logistic回归最佳拟合直线的函数
# 输入wei是系数向量
def plotBestFit(wei):
import matplotlib.pyplot as plt
weight = wei.getA() # 矩阵转换成数组
dataMat, labelMat = loadDataSet()
dataArray = array(dataMat)
n = shape(dataArray)[0] # 获得数组的行数
xcord1 = []
ycord1 = []
xcord2 = []
ycord2 = []
for i in range(n):
if labelMat[i] == 1:
xcord1.append(dataArray[i, 1])
ycord1.append(dataArray[i, 2])
else:
xcord2.append(dataArray[i, 1])
ycord2.append(dataArray[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 = (-weight[0] - weight[1] * x) / weight[2]
ax.plot(x, y)
plt.show()
这就是使用梯度上升500次得到的结果,效果还不错
随机梯度上升
从上面的代码可以看出梯度上升法每次更新回归系数的时候都要遍历整个数据集,计算复杂度太高
一种改进的方法是一次只用一个样本点来更新回归系数,这种方法叫做随机梯度上升算法
这样的话,每输入一个新的样本就可以对分类器进行一次更新,这种方式叫做增量式更新,所以这个算法是一个在线学习算法
与在线学习算法相对应的,一次处理所有数据被称作“批处理”
改进后代码如下:
# =========================================
# 随机梯度上升算法
# =========================================
def stoGradAsent0(dataMatrix, classLabels):
m, n = shape(dataMatrix)
weights = ones(n)
alpha = 0.001
for i in range(m):
h = sigmoid(sum(dataMatrix[i] * weights))
err = h - classLabels[i]
weights = weights + alpha * err * dataMatrix[i]
return weights
用下面的代码调用上面的函数:
data, label = loadDataSet()
wei = stocGradAscent0(array(data), label)
plotBestFit(wei)
得到如下分类结果:
可以看到这个分类效果没有之前的分类效果好。但是之前的效果是迭代500次才得到的,所以不具有可比性。
改进的随机梯度上升
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
m,n = shape(dataMatrix)
weights = ones(n)
for j in range(numIter):
dataIndex = range(m)
for i in range(m):
# alpha随着迭代次数减小,但是有常数项保证永远不会小到0
alpha = 4/(1.0+j+i)+0.0001
# 随机选取样本来更新回归系数
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
从代码中我们可以看到主要做出了两个改进:
- 第八行代码表示alpha随着迭代次数减小,这是为了缓解回归系数的波动,尽快达到收敛状态(收敛是判断一个优化算法是否可靠的重要方法)。并且alpha有常数项保证永远不会小到0
- 第十行表示随机选取样本来更新回归系数,这样是为了减少周期波动
对数据集作20次遍历stocGradAscent1(array(data), label, 20)得到的分类效果如下:
很明显效果比没有改进前的随机梯度上升算法要好多了。
例子:从疝气病预测病马的死亡率
缺失值处理
在正式开始这个例子之前我们想先讨论一下数据中的缺失值处理
因为即将用到这个数据集的原始数据集是有缺失值的,而且缺失值的处理的数据的预处理中非常重要
常用的缺失值处理方法:
- 使用可用特征的均值填补
- 使用特殊值来填补缺失值,比如0或-1
- 忽略有缺失值的样本
- 使用其他机器学习算法来预测缺失值
- 使用相似样本对应特征的均值俩填补
以下两种情况的值缺失处理方法是不同的
- 特征缺失:可以丢弃此样本;否则的话,由于numpy不支持包含缺失值,所以必须要填补上
- 标签缺失:基本上只能直接丢弃,因为和特征值不同,它很难使用某个合适值来替换
用logistic回归进行分类
# 用于为每个输入样本分类
# inX为待分类的样本的特征值
# weights 为训练好的权重
def classifyVector(inX, weights):
prob = sigmoid(sum(inX * weights))
if prob > 0.5:
return 1
else:
return 0
def colicTest():
frTrain = open(r'E:mlmachinelearninginactionCh05horseColicTraining.txt')
frTest = open(r'E:mlmachinelearninginactionCh05horseColicTest.txt')
trainData = []
trainLabels = []
for line in frTrain.readlines():
currLine = line.strip().split(' ')
lineArr = []
for i in range(21):#这个数据集有21个特征
lineArr.append(float(currLine[i]))
trainData.append(lineArr)
trainLabels.append(float(currLine[21]))
# 用训练集作500次迭代得到权重
trainWeights = stocGradAscent1(array(trainData), trainLabels, 500)
numTestVec = 0 # 记录测试样本的数量
errorCount = 0 # 预测错误的数量
for line in frTest.readlines():
numTestVec += 1
currLine = line.strip().split(' ')
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))
测试效果如下:
每次的错误率不同是因为随机策略导致的。
小结
LR优点:计算代价不高,易于理解和实现
LR缺点:容易欠拟合,分类精度可能不高
使用数据类型:数值型和标称型
梯度上升是非常常用的最优化方法
随机梯度上升算法降低了计算复杂度,而且这是一个在线学习算法
缺失数据处理是数据分析乃至机器学习的重要组成部分,它没有标准的做法,取决于具体情况的不同