(一)准备工作
1.编程环境:Python3.5.2(使用其自带的IDLE,并已经配置好环境变量),win10。
2.使用到的包:matplotlib, Pandas, sklearn, OrderedDict, NumPy, imp, math, random。
(注:可用pip install xxx直接安装,有问题就直接搜索引擎解决)。
其中imp,math,random不需要另外安装。
3.数据来源:
4.参考资料:
a.《机器学习实战》第5章。
(二)逻辑(Logistic)回归介绍
假设现在有一些数据点,我们用一条直线对这些点进行拟合(该线称为最佳拟合直线),这个过程就称作回归。
而利用Losgistic回归进行分类的主要思想是:根据现有数据对分类边界建立回归公式,以此进行分类。注意,Losgistic回归是一个分类算法,但不是一个回归算法,它可以处理二元分类以及多元分类。
Logistic回归的目的是寻找一个非线性函数Sigmoid的最佳拟合参数,求解过程中可以由最优化算法完成。在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以简化为随机梯度上升算法。
随机梯度梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。此外,随机梯度上升是一个在线算法,它可以在新数据到来时就完成参数更新,而不需要重新读取整个数据集来进行批处理运算。
Logistic回归的优点:计算代价不高,易于理解和实现。
Logistic回归的缺点:容易欠拟合,分类精度可能不高。
Logistic回归适用数据类型:数值型和标称型数据。
接下来对逻辑回归进行简单的演示。
1.准备数据
from collections import OrderedDict
import pandas as pd
import matplotlib.pyplot as plt
examDict = {
'学习时间':[0.50,0.75,1.00,1.25,1.50,1.75,1.75,2.00,2.25,
2.50,2.75,3.00,3.25,3.50,4.00,4.25,4.50,4.75,5.00,5.50],
'考试结果':[0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,1,1]
}
examOrderDict = OrderedDict(examDict)
exam = pd.DataFrame(examOrderDict)
打印结果:
>>> exam.head()
考试结果 学习时间
0 0 0.50
1 0 0.75
2 0 1.00
3 0 1.25
4 0 1.50
接下来通过绘制散点图判断是否符合逻辑回归,继续添加以下代码:
#判断是否适用于Logisti回归。
exam_X = exam['学习时间']
exam_Y = exam['考试结果']
plt.scatter(exam_X, exam_Y, color = 'blue')
plt.ylabel('Scores')
plt.xlabel('Times')
plt.title('exam data')
plt.show() #绘制之后可注释此行代码。
效果如下:绘制数据散点图
容易得出结论,上述数据适合Logistic回归。
2.拆分数据
将上面的数据拆分为训练集和测试集,方便接下来的训练和测试,继续添加以下代码:
#拆分数据。
from sklearn.cross_validation import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(exam_X,exam_Y,train_size=0.8)
3.使用拆分好的数据集进行训练
X_train = X_train.values.reshape(-1,1) #改变数据形状
X_test = X_test.values.reshape(-1,1)
from sklearn,linear_model import LogisticRegression
model = LogisticRegression() #创建模型
model.fit(X_train, Y_train) #训练
4.测试
>>> model.score(X_test, Y_test)
1.0
由于数据量实在是太小,本次的准确率过高(或者说是过拟合了),需要多训练几次或者是增加数据集的数据量。
5.特征的概率
>>> model.predict_proba(3)
array([[0.35385898, 0.64614102]])
当输入一个特征的时候,可以返回其概率值,返回的第一个是其为0的概率值,第二个是为1的概率值。当上文散点图中 Times = 3,即x = 3时,Logistic回归的函数值就是其为1的概率值。当这个值大于0.5的时候(0.64614102),则认为它的值为1,当这个概率值小于0.5的时候(0.35385898),则认为它的值为0。
本次演示中,这个概率值为0.64614102,所以认为x=3时,y=1。
全部代码:
from collections import OrderedDict
import pandas as pd
import matplotlib.pyplot as plt
examDict = {
'学习时间':[0.50,0.75,1.00,1.25,1.50,1.75,1.75,2.00,2.25,
2.50,2.75,3.00,3.25,3.50,4.00,4.25,4.50,4.75,5.00,5.50],
'考试结果':[0,0,0,0,0,0,0,0,1,0,0,1,1,1,1,1,1,1,1,1]
}
examOrderDict = OrderedDict(examDict)
exam = pd.DataFrame(examOrderDict)
#exam.head()
#判断是否适用于Logisti回归。
exam_X = exam['学习时间']
exam_Y = exam['考试结果']
plt.scatter(exam_X, exam_Y, color = 'blue')
plt.ylabel('Scores')
plt.xlabel('Times')
plt.title('exam data')
#plt.show()
#拆分数据。
from sklearn.cross_validation import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(exam_X,exam_Y,train_size=0.8)
#开始训练
X_train = X_train.values.reshape(-1,1) #改变数据形状
X_test = X_test.values.reshape(-1,1)
from sklearn.linear_model import LogisticRegression
model = LogisticRegression() #创建模型
model.fit(X_train, Y_train) #训练
#model.score(X_test, Y_test)
#model.predict_proba(3)
(三)基于Logistic回归和Sigmoid函数的分类
上文阐述了Logistic回归的定义以及一个简单的演示,接下来将介绍一些优化算法,包括基本的梯度上升发和一个改进的随机梯度上升法,这些优化算法将用于分类器的训练,最后会给出一个Logistic回归的实例,预测一匹病马是否能被治愈。
1.Sigmoid函数
Sigmoid函数具体的计算公式如下:
下面给出一张Sigmoid函数分布图:
可以看出,当x=0时,Sigmoid函数值约为0.5,随着x的增大,对应的Sigmoid值将逼近1;而随着x的减小,Sigmoid值将逼近于0.
因此,为了实现Logistic回归分类器,可以在每个特征上都乘以一个回归系数,然后把所有结果相加,将这个总和代入Sigmoid函数中,进而得到一个范围在0~1之间的数值。任何大于0.5的数据被分为1类,小于0.5被归入0类,所以,Logistic回归也被看成是一种概率估计。
当确定了分类器的函数形式之后,现在的问题就变成了:最佳回归系数是多少?
2.基于最优化方法的最佳回归系数确定
Sigmoid函数的输入记为z,由下面公式得出:
上述公式也可以用向量的写法:
向量x是分类器的输入数据,向量w是要找到的最佳参数。
为了找到最佳参数,需要用到最优化理论的一些知识。
2.1梯度上升法
梯度上升法大体思想:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
梯度上升算法到达每个点都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1.在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2,如此循环迭代,直到满足条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。
梯度上升算法的迭代公式如下:
称为步长,是梯度上升时移动量的大小。∇是算子
上式将会一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算法达到某个可以允许的误差范围。
延伸:
梯度下降算法,对应的公式为:
梯度上升算法用看来求函数的最大值,而梯度下降算法用来求函数的最小值。
2.2训练算法:使用梯度上升找到最佳参数
梯度上升法的伪代码如下:
每个回归系数初始化为1
重复R次:
计算整个数据集的梯度
使用alpha x gradient更新回归系数的向量
返回回归系数
接下来的代码是梯度上升算法的具体实现:
#文件名:demo004_logRegres
import matplotlib.pyplot as plt
import numpy as np
import math
import imp
import random
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+np.exp(-inX))
#这里不能用math.exp(),会报错:TypeError: only size-1 arrays can be converted to Python scalars
#原因是:dataMatrix 与weights均为numpy矩阵,相乘也是numpy矩阵,而math.exp()函数只处理python标准数值。
#梯度上升算法的实际工作在gradAscent()里完成。
# dataMatIn是一个2维NumPy数组,每列代表每个不同的特征,每行则代表每个训练样本。
# classLabels是类别标签,是一个1x100的行向量
def gradAscent(dataMatIn,classLabels):
#转换为NumPy矩阵数据类型。
dataMatrix = np.mat(dataMatIn)
labelMat = np.mat(classLabels).transpose() #将classLabels转换为列向量,转置。
m,n = np.shape(dataMatrix) #得到矩阵的行列数,也就是矩阵的大小。
alpha = 0.001 #alpha是向目标移动的步长。
maxCycles = 500 #maxCycles是迭代次数。
weights = np.ones((n,1))
for k in range(maxCycles):
#计算真实类别与预测类别的差值。
h = sigmoid(dataMatrix * weights) #h是一个有100个元素的列向量。
#运算dataMatrix * weights包含了300次的乘积。
error = (labelMat - h)
weights = weights + alpha * dataMatrix.transpose( ) * error
return weights
在Python提示符下:
>>>#按照差值的方向调整回归系数。
>>>import demo004_logRegres
>>>dataArr,labelMat = loadDataSet()
>>>gradAscent(dataArr,labelMat)
matrix([[ 4.12414349],
[ 0.48007329],
[-0.6168482 ]])
2.3分析数据:画出决策边界
在demo004_logRegres中继续添加代码:
def plotBestFit(weights):
dataMat, labelMat = loadDataSet()
dataArr = np.array(dataMat)
n = np.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 = np.arange(-3.0, 3.0, 0.1)
y = (-weights[0] - weights[1]*x)/weights[2]#这里设置sigmoid函数为0,0是两个分类(类别1和类别2)的分界处。
ax.plot(x,y)
plt.xlabel('X1'); plt.ylabel('X2');
plt.show()
运行程序,在Python提示符下输入:
>>> import demo004_logRegres
>>> from imp import reload
>>> reload(demo004_logRegres)
>>> dataArr,LabelMat = loadDataSet()
>>> weights = gradAscent(dataArr,LabelMat)
>>> demo004_logRegres.plotBestFit(weights.getA())
效果如下图:
从上图看只分错了两到4个点,但是这个方法却需要大量的计算(300次乘法),需要改进。
注意:
使用reload需要先from imp import reload。
在reload某个模块的时候,需要先import来加载需要的模块。
2.4训练算法:随机梯度上升
梯度上升算法在每次更新回归系数时都需要遍历整个数据集,但是当特征成千上万时就不可行了,复杂度太高。而随机梯度上升法是用一次仅用一个样本点来更新回归系数,随机梯度上升法是一个在线学习算法,而一次处理所有数据被称作是“批处理”。
随机梯度上升算法伪代码:
所有回归系数初始化为1
对数据集中每个样本
计算该样本的梯度
使用alpha x gradient更新回归系数值
返回回归系数值
在demo004_logRegres.py中继续添加代码,随机梯度上升算法实现代码如下:
def stoGradAscent0(dataMatrix, classLabels):
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
运行以上程序,在Python提示符下输入:
>>> import demo004_logRegres
>>> from imp import reload
>>> reload(demo004_logRegres)
>>> dataArr,LabelMat = loadDataSet()
>>> weights = stoGradAscent0(np.array(dataArr),LabelMat)
>>> demo004_logRegres.plotBestFit(weights)
得出效果图如下:随机梯度上升算法在上述数据集上的执行结果,最佳拟合直线并非最佳分类线
在demo004_logRegres.py中继续添加代码,改进的随机梯度上升算法:
def stoGradAscent1(dataMatrix, classLabels, numIter=150):
m,n = np.shape(dataMatrix)
weights = np.ones(n)
for j in range(numIter):
dataIndex = list(range(m))
for i in range(m):
alpha = 4/(1.0+j+i) +0.01 #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
在Python提示符下输入以下命令:
>>> import demo004_logRegres
>>> from imp import reload
>>> reload(demo004_logRegres)
>>> dataArr,LabelMat = loadDataSet()
>>> weights = stoGradAscent1(np.array(dataArr),LabelMat)
>>> demo004_logRegres.plotBestFit(weights)
得出效果图:使用随机梯度上升算法得到的系数
a.第一个改进的地方在alpha = 4/(1.0+j+i) +0.01,一方面,alpha在每次迭代的时候都会调整,这会缓解数据波动或高频波动。另外,虽然alpha会随着迭代次数不断减小,但永远不会减小到0,这是因为alpha = 4/(1.0+j+i) +0.01中还存在一个常数项。
必须这样做的原因是为了保证在多次迭代之后新数据仍然能够具有一定的影响。如果要处理的问题是动态变化的,那么可以适当加大上述常数项,来确保新的值获得更大的回归系数。
值得注意的是,在降低alpha的函数中,alpha每次减少1/(j+i),其中j是迭代次数,i是样本下标(表示本次迭代中第i个选出来的样本)。这样当j<
b.第二个改进的地方,randIndex = int(random.uniform(0,len(dataIndex))),
通过随机选取样本来更新回归系数,这样可以减少周期性的波动。每次随机从列表中选取一个值,然后从列表中删掉该值(再进行下一次迭代)。
c.此外,改进版的算法还增加了一个迭代次数作为第3个参数,如果该参数没有给定的话,算法将默认迭代150次。如果给定,那么算法将按照新的参数值进行迭代。
3.示例:从疝气病症预测病马的死亡率
接下来使用Logistic回归来预测患有疝气病症的马的存活问题。
使用Logistic回归方法进行分类并不需要做很多工作,所需要的只是把测试集上的每个特征向量乘以最优化方法得来的回归系数,再将该乘积结果求和,最后输入到Sigmoid函数中即可。
3.1准备数据:处理数据中的缺失值
下面给出一些可选的做法:
a.使用可用特征的均值来填补缺失值;
b.使用特殊值来填补缺失值,如-1;
c.忽略有缺失值得样本;
d.使用相似样本的均值添补缺失值;
e.使用另外的机器学习算法预测缺失值。
使用的NumPy数据类型不允许包含缺失值。
回归系数更新公式如下:
weights = weights +alpha * error * dataMatrix[randIndex]
如果dataMatrix的某特征对应值为0,那么该特征的系数将不做更新,即:
weights = weights
3.2测试算法:用Logistic回归进行分类
在demo004_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 = stoGradAscent1(np.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(np.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)))
解析:
a. classifyVector(),以回归系数和特征向量作为输入来计算对应的Sigmoid值。
如果Sigmoid值大于0.5返回1,否则返回0.
b. colicTest(),用于打开测试集合训练集,并对数据进行格式化处理的函数。
数据导入后,可以使用函数stoGradAscent1()来计算回归系数向量。
c.multiTest(),其功能是调用函数colicTest()10次并求结果的平均值。
运行以上程序,在Python提示符下输入:
>>> import demo004_logRegres
>>> from imp import reload
>>> reload(demo004_logRegres)
>>> demo004_logRegres.multiTest()
the error rate of this test is: 0.432836
the error rate of this test is: 0.388060
the error rate of this test is: 0.388060
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.522388
the error rate of this test is: 0.238806
the error rate of this test is: 0.358209
the error rate of this test is: 0.373134
the error rate of this test is: 0.358209
after 10 iterations the average error rate is: 0.379104
从结果可以看到,10次迭代之后的平均错误率为37%。
如果调整colicTest()中的迭代次数和stoGradAscent1()中的步长,平均错误率可以降到20%左右。