《机器学习实战笔记--第一部分 分类算法:logistic回归1》
利用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,可以自行修改。