参考:
Python3《机器学习实战》学习笔记(六):Logistic回归基础篇之梯度上升算法 - Jack-Cui - CSDN博客
Python3《机器学习实战》学习笔记(七):Logistic回归实战篇之预测病马死亡率 - Jack-Cui - CSDN博客
假设现在有一些数据点,我们用一条直线对这些点进行拟合(该线称为最佳拟合直线),这个拟合过程就称作回归。
利用Logistic回归进行分类的主要思想是:
根据现有数据对分类边界线建立回归公式,以此进行分类。
这里的“回归”一词源于最佳拟合,表示要找到最佳拟合参数集,我们将使用最优化算法找到最佳拟合参数集。
收集数据:采用任意方法收集数据。
准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳。
分析数据:采用任意方法对数据进行分析。
训练算法:大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数。
测试算法:一旦训练步骤完成,分类将会很快。
使用算法:
首先,我们需要输入一些数据,并将其转换成对应的结构化数值;
接着,基于训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别;
在这之后,我们就可以在输出的类别上做一些其他分析工作。
我们想要的函数应该是,能接受所有的输入然后预测出类别。
例如,在两个类的情况下,上述函数输出0或1。
Sigmoid函数可以近似实现在跳跃点上从0瞬间跳跃到1,其具体的计算公式如下:
σ ( z ) = 1 1 + e − z \sigma (z)=\frac{1}{1+e^{-z}} σ(z)=1+e−z1
下面给出Sigmoid函数在不同坐标尺度下的两条曲线图:
可以看出,当x为0时,Sigmoid函数值为0.5;随着x的增大,对应的Sigmoid值将逼近于1;而随着x的减小,Sigmoid值将逼近于0。
如果横坐标刻度足够大,Sigmoid函数看起来很像一个阶跃函数。
Sigmoid函数的输入记为z,由下面公式得出:
z = w 0 x 0 + w 1 x 1 + w 2 x 2 + • • • + w n x n z = w_{0}x_{0}+w_{1}x_{1}+ w_{2}x_{2}+ •••+ w_{n}x_{n} z=w0x0+w1x1+w2x2+•••+wnxn
如果采用向量的写法,上述公式可以写成 z = w T x z = w^{T}x z=wTx,它表示将这两个数值向量对应元素相乘然后全部加起来即得到z值。
下面介绍梯度上升的最优化方法求得数据集的最佳参数的方法。
梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
如果梯度记为∇,则函数f(x,y)的梯度由下式表示:
▽ f ( x , y ) = ( ∂ f ( x , y ) ∂ x ∂ f ( x , y ) ∂ y ) \bigtriangledown f(x,y)=\begin{pmatrix} \frac{\partial f(x,y)}{\partial x}\\ \\ \frac{\partial f(x,y)}{\partial y} \end{pmatrix} ▽f(x,y)=⎝⎜⎛∂x∂f(x,y)∂y∂f(x,y)⎠⎟⎞
当然,函数 f ( x , y ) f (x,y) f(x,y)必须要在待计算的点上有定义并且可微。
到移动量的大小。该量值称为步长,记做α。
用向量来表示的话,梯度上升算法的迭代公式如下:
w : = w + α ▽ f ( w ) w:=w+\alpha \triangledown f(w) w:=w+α▽f(w)
该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围。
import numpy as np
def loadDataSet():
"""
加载数据集
- - - -
"""
#数据列表
dataMat = []
#标签列表
labelMat = []
#从文件中读取数据
fr = open('4-Logistic/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]))
fr.close()
return dataMat, labelMat
def sigmoid(inX):
"""
sigmoid函数
- - - -
inX - 数据向量
"""
return 1.0 / (1 + np.exp(-inX))
def gradAscent(dataMatIn, classLabels):
"""
梯度上升算法
- - - -
dataMatIn - 数据集
classLabels - 数据标签
"""
#转换成numpy的矩阵形式
dataMatrix = np.mat(dataMatIn)
#转换成numpy的矩阵形式,并转置
labelMat = np.mat(classLabels).transpose()
#返回dataMatrix的大小
m, n = np.shape(dataMatrix)
#步长
alpha = 0.001
#最大迭代次数
maxCycles = 500
#矩阵相乘
weights = np.ones((n,1))
for k in range(maxCycles):
h = sigmoid(dataMatrix * weights)
#计算真实类别与预测类别的差值(公式推导见https://blog.csdn.net/c406495762/article/details/77723333)
error = labelMat - h
weights = weights + alpha * dataMatrix.transpose() * error
#将矩阵转换为数组返回
return weights.getA()
if __name__ == '__main__':
dataMat, labelMat = loadDataSet()
print(gradAscent(dataMat, labelMat))
结果:
[[ 4.12414349]
[ 0.48007329]
[-0.6168482 ]]
上面已经解出了一组回归系数,它确定了不同类别数据之间的分隔线。
下面画出该分隔线,从而使得优化的过程便于理解。
import matplotlib.pyplot as plt
def plotBestFit(wei):
"""
画出数据集和Logistic回归最佳拟合直线的函数
- - - -
wei - 权重参数数组
"""
dataMat, labelMat = loadDataSet()
dataArr = np.array(dataMat)
n = np.shape(dataMat)[0]
#正、负样本
xcord1 = []; ycord1 = []
xcord2 = []; ycord2 = []
#根据数据集标签进行分类
for i in range(n):
#1为正样本
if int(labelMat[i]) == 1:
xcord1.append(dataArr[i,1]); ycord1.append(dataArr[i,2])
#0为负样本
else:
xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2])
fig = plt.figure()
ax = fig.add_subplot(111)
#绘制正、负样本
ax.scatter(xcord1, ycord1, s = 20, c = 'red', marker = 's',alpha=.5)
ax.scatter(xcord2, ycord2, s = 20, c = 'green',alpha=.5)
x = np.arange(-3.0, 3.0, 0.1)
y = (-wei[0] - wei[1] * x) / wei[2]
ax.plot(x, y)
plt.title('BestFit')
plt.xlabel('X1'); plt.ylabel('X2')
plt.show()
if __name__ == '__main__':
dataMat, labelMat = loadDataSet()
weights = gradAscent(dataMat, labelMat)
plotBestFit(weights)
结果:
这个分类结果相当不错,从图上看只错分了两到四个点。
但是,尽管例子简单且数据集很小,这个方法却需要大量的计算(300次乘法)。
因此我们将对该算法稍作改进,从而使它可以用在真实数据集上。
梯度上升算法在每次更新回归系数时都需要遍历整个数据集,该方法在处理100个左右的数据集时尚可,但如果有数十亿样本和成千上万的特征,那么该方法的计算复杂度就太高了。
一种改进方法是一次仅用一个样本点来更新回归系数,该方法称为随机梯度上升算法。
由于可以在新样本到来时对分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法。
与“在线学习”相对应,一次处理所有数据被称作是**“批处理”**。
def stocGradAscentO(dataMatrix, classLabels):
"""
随机梯度上升算法
- - - -
dataMatIn - 数据集
classLabels - 数据标签
"""
dataMatrix=np.array(dataMatrix)
m,n = np.shape(dataMatrix)
#参数初始化
alpha = 0.01
weights = np.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
if __name__ == '__main__':
dataMat, labelMat = loadDataSet()
weights = stocGradAscentO(dataMat, labelMat)
plotBestFit(weights)
可以看到,随机梯度上升算法与梯度上升算法在代码上很相似,但也有一些区别:
第一,后者的变量h和误差error都是向量,而前者则全是数值;
第二,前者没有矩阵的转换过程,所有变量的数据类型都是NumPy数组。
结果:
可以看到,拟合出来的直线效果还不错,但并不像梯度上升算法那样完美。这里的分类器错分了三分之一的样本。
所以我们增加了两处代码来进行改进随机梯度上升算法。
import random
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
"""
随机梯度上升算法
- - - -
dataMatIn - 数据集
classLabels - 数据标签
"""
dataMatrix=np.array(dataMatrix)
m,n = np.shape(dataMatrix)
#参数初始化
weights = np.ones(n)
for j in range(numIter):
dataIndex = list(range(m))
for i in range(m):
#降低alpha的大小,每次减小1/(j+i)
alpha = 4/(1.0+j+i)+0.01
#随机选取样本,计算h
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
if __name__ == '__main__':
dataMat, labelMat = loadDataSet()
weights = stocGradAscent1(dataMat, labelMat)
plotBestFit(weights)
本节将使用Logistic回归来预测患有疝病的马的存活问题。
这里的数据集中包含368个样本和28个特征。该数据集中包含了医院检测马疝病的一些指标,有的指标比较主观,有的指标难以测量,例如马的疼痛级别。
另外需要说明的是,除了部分指标主观和难以测量外,该数据还存在一个问题,数据集中有30%的值是缺失的。下面将首先介绍如何处理数据集中的数据缺失问题,然后再利用Logistic回归和随机梯度上升算法来预测病马的生死。
下面给出了一些处理数据中的缺失值可选的做法:
现在,我们对下一节要用的数据集进行预处理,使其可以顺利地使用分类算法。在预处理阶段需要做两件事:
第一,所有的缺失值必须用一个实数值来替换,因为我们使用的NumPy数据类型不允许包含缺失值。这里选择实数0来替换所有缺失值,这样在更新时不会影响系数的值。恰好能适用于Logistic回归。
第二,如果在测试数据集中发现了一条**数据的类别标签已经缺失,那么我们的简单做法是将该条数据丢弃。**这是因为类别标签与特征不同,很难确定采用某个合适的值来替换。
使用Logistic回归方法进行分类并不需要做很多工作,所需做的只是把测试集上每个特征向量乘以最优化方法得来的回归系数,再将该乘积结果求和,最后输入到Sigmoid函数中即可。
如果对应的Sigmoid值大于0.5就预测类别标签为1,否则为0。
def classifyVector(inX, weights):
"""
分类函数
- - - -
inX - 特征向量
weights - 回归系数
"""
prob = sigmoid(sum(inX*weights))
if prob > 0.5:
return 1.0
else:
return 0.0
def colicTest():
"""
Logistic回归分类预测病马的死亡率
- - - -
"""
#打开训练集并读取
frTrain = open('4-Logistic/horseColicTraining.txt')
trainingSet = []; trainingLabels = []
for line in frTrain.readlines():
currLine = line.strip().split('\t')
lineArr = []
for i in range(len(currLine)-1):
lineArr.append(float(currLine[i]))
trainingSet.append(lineArr)
trainingLabels.append(float(currLine[-1]))
#使用改进的随机上升梯度算法训练分类器
trainWeights = stocGradAscent1(np.array(trainingSet), trainingLabels, 500)
#测试分类器错误率
errorCount = 0; numTestVec = 0.0
#打开测试集并读取
frTest = open('4-Logistic/horseColicTest.txt')
for line in frTest.readlines():
numTestVec += 1.0
currLine = line.strip().split('\t')
lineArr =[]
for i in range(len(currLine)-1):
lineArr.append(float(currLine[i]))
if int(classifyVector(np.array(lineArr), trainWeights))!= int(currLine[-1]):
errorCount += 1
errorRate = (float(errorCount)/numTestVec) #错误率计算
print("the error rate of this test is: %f" % errorRate)
return errorRate
def multiTest(numTests):
"""
计算平均错误率
- - - -
numTests - 分类次数
"""
errorSum=0.0
for k in range(numTests):
errorSum += colicTest()
print ("after %d iterations the average error rate is:%f" % (numTests, errorSum/float(numTests)))
if __name__ == '__main__':
#1.代码实现
""" dataMat, labelMat = loadDataSet()
weights = stocGradAscent1(dataMat, labelMat)
plotBestFit(weights) """
#2.实战:从疝气病症预测病马的死亡率
multiTest(10)
注意:
分类时,同前面一样,数据的最后一列仍然是类别标签。数据最初有三个类别标签,分别代表马的三种情况:“仍存活”、“已经死亡”和“已经安乐死”。这里为了方便,将“已经死亡”和“已经安乐死”合并成“未能存活”这个标签 。
结果:
the error rate of this test is: 0.313433
the error rate of this test is: 0.462687
the error rate of this test is: 0.402985
the error rate of this test is: 0.328358
the error rate of this test is: 0.373134
the error rate of this test is: 0.373134
the error rate of this test is: 0.358209
the error rate of this test is: 0.388060
the error rate of this test is: 0.358209
the error rate of this test is: 0.388060
after 10 iterations the average error rate is:0.374627
从上面的结果可以看到,10次迭代之后的平均错误率为37%。事实上,这个结果并不差,因为有30%的数据缺失。
当然,如果调整colicTest()中的迭代次数和stochGradAscent1()中的步长,平均错误率可以降到20%左右。
Logistic回归的目的是寻找一个非线性函数Sigmoid的最佳拟合参数,求解过程可以由最优化算法来完成。
在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以简化为随机梯度上升算法。
随机梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。
此外,随机梯度上升是一个在线算法,它可以在新数据到来时就完成参数更新,而不需要重新读取整个数据集来进行批处理运算。
机器学习的一个重要问题就是如何处理缺失数据。这个问题没有标准答案,取决于实际应用中的需求。现有一些解决方案,每种方案都各有优缺点。