目录
一、回归的基础概念
二、基于Logistic回归和Sigmoid函数的分类
三、基于最优化方法的最佳回归系数确定
1、极大似然估计 Recap
2、梯度上升法
3、准备数据:一个简单的数据集
4、训练算法:使用梯度上升找到最佳参数
5、分析数据:画出决策边界
6、训练算法:随机梯度上升算法
7、改进的随机梯度上升算法
四、实例:从疝气病症状预测病马的死亡率
1、准备数据:处理数据中的缺失值
2、测试算法:使用梯度上升算法进行分类
3、测试算法:使用改进后的随机梯度上升算法进行分类
4、实例:实践小结
现有一些数据点,我们用 一条直线对这些点进行拟合,该线称为最佳拟合直线,这个拟合过程就称作回归。利用Logistic 回归进行分类的主要思想是:根据现有数据对分类边界线建立回归公式,以此进行分类。这里的 “回归”一词源于最佳拟合,表示要找到最佳拟合参数集。 训练分类器时的做法就是寻找最佳拟合参数,使用的是最优化算法。
Logistic回归的一般过程 |
(1) 收集数据:采用任意方法收集数据。 (2) 准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳。 (3) 分析数据:采用任意方法对数据进行分析。 (4) 训练算法:大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数。 (5) 测试算法:一旦训练步骤完成,分类将会很快。 (6) 使用算法:首先,我们需要输入一些数据,并将其转换成对应的结构化数值; 接着,基于训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别;在这之后,我们就可以在输出的类别上做一些其他分析工作。 |
Logistic回归 |
|
Logistic回归来做分类问题,我们想要的函数应该是,能接受所有的输入然后预测出类别。例如,在两个类的情况下,上述函数输出0或1。例如海维塞德阶跃函数 (Heaviside step function),也称为单位阶跃函数。
海维塞德阶跃函数的问题在于: 该函数在跳跃点上从0瞬间跳跃到1(不连续、不可微),这个瞬间跳跃过程有时很难处理。但是在数学上,Sigmoid函数可以可以解决这个问题。Sigmoid函数具体的计算公式如下:
下图给出了Sigmoid函数在不同坐标尺度下的两条曲线图。当x为0时,Sigmoid函数值为0.5。 随着x的增大,对应的Sigmoid值将逼近于1;而随着x的减小,Sigmoid值将逼近于0。如果横坐标 刻度足够大,Sigmoid函数看起来很像一个阶跃函数。
所以,为了实现Logistic回归分类,我们可以在每个特征上都乘以一个回归系数,然后把 所有的结果值相加,将这个总和代入Sigmoid函数中,进而得到一个范围在0~1之间的数值。任何大于0.5的数据被分入1类,小于0.5即被归入0类。所以,Logistic回归也可以被看成是一种概率估计。
预测值与输出标记:
其中Sigmoid函数的输入值为z、向量x是分类器的输入数据,向量w是找到的最佳系数,b为常数。
运用Sigmoid函数:
对数几率(log odds):样本作为正例的相对可能性的对数
因此有:
上面两个式子分别表示y=1和y=0的概率。通过,我们获得z值,再通过Sigmoid函数把z值映射到0~1之间,获得数值之后就可进行分类。比如定义,大于0.5的分类为1,反之,即分类为0。所以我们要解决的问题就是获得最佳回归系数,即求解w和b得值。
① 极大似然估计的方法步骤
②最大化样本属于其真实标记的概率,等同于最大化对数似然函数:
④根据sigmoid函数,似然函数可重写为:
故等价形式为要最小化该方程。
梯度上升法基于的思想是:要找到某函数的 最大值,最好的方法是沿着该函数的梯度方向探寻。如果梯度记为∇,则函数f(x,y)的梯度由 下式表示:
这个梯度意味着要沿x的方向移动 ,沿y的方向移动 。其中,函数f(x,y) 必须要在待计算的点上有定义并且可微。如下图:
如图,梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2。如此循环迭代,直到满足停止条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。
梯度上升算法沿梯度方向移动了一步。可以看到,梯度算子总是指向函数值增长最快的方向。这里所说的是移动方向,而未提到移动量的大小。该量值称为步长,记做α。用向 量来表示的话,梯度上升算法的迭代公式如下:
该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算 法达到某个可以允许的误差范围。
梯度下降算法
梯度下降算法,它与这里的梯度上升算法是一样的,只是公式中的加法需要变成减法。因此,对应的公式可以写成:
梯度上升算法用来求函数的最大值,而梯度下降算法用来求函数的最小值。
X1 | X2 | 分类标签 |
0.355715 | 10.325976 | 0 |
-1.39563 | 4.662541 | 1 |
-0.75216 | 6.53862 | 0 |
-1.32237 | 7.152853 | 0 |
...... | ······ | ······ |
1.985298 | 3.230619 | 1 |
-1.69345 | -0.55754 | 1 |
-0.57653 | 11.77892 | 0 |
-0.34681 | -1.67873 | 1 |
-2.12448 | 2.672471 | 1 |
有100个样本点,每个点包含两个数值型特征:X1和X2。在此数据集上,通过使用梯度上升法找到最佳回归系数,也就是拟合出Logistic回归模型的最佳参数。
# 加载数据集
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 showData():
dataMat, labelMat = loadDataSet() # 加载数据集,标签
dataArr = array(dataMat) # 转换成umPy的数组
n = shape(dataArr)[0] # 获取数据总数
xcord1 = []; ycord1 = [] # 存放正样本
xcord2 = []; ycord2 = [] # 存放负样本
for i in range(n): # 依据数据集的标签来对数据进行分类
if int(labelMat[i]) == 1: # 数据的标签为1,表示为正样本
xcord1.append(dataArr[i, 1]); ycord1.append(dataArr[i, 2])
else: # 否则,若数据的标签不为1,表示为负样本
xcord2.append(dataArr[i, 1]); ycord2.append(dataArr[i, 2])
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(xcord1, ycord1, s=15, c='blue') # 绘制正样本
ax.scatter(xcord2, ycord2, s=15, c='red', marker='s') # 绘制负样本
plt.title('DateSet') # 标题
plt.xlabel('X1'); plt.ylabel('X2') # x,y轴的标签
plt.show()
# sigmoid函数
def sigmoid(inX):
return 1.0 / (1 + exp(-inX))
# 梯度上升算法
def gradAscent(dataMatIn, classLabels): # dataMatIn数据集、classLabels数据标签
dataMatrix = mat(dataMatIn) # 转换为NumPy矩阵
labelMat = mat(classLabels).transpose() # 转换为NumPy矩阵,并且矩阵转置
m, n = shape(dataMatrix) # 获取数据集矩阵的大小,m为行数,n为列数
alpha = 0.001 # 目标移动的步长
maxCycles = 500 # 迭代次数
weights = ones((n, 1)) # 权重初始化为1
for k in range(maxCycles): # 重复矩阵运算
h = sigmoid(dataMatrix * weights) # 矩阵相乘,计算sigmoid函数
error = (labelMat - h) # 计算误差
weights = weights + alpha * dataMatrix.transpose() * error # 矩阵相乘,更新权重
return weights
# 获得回归系数
dataMat, labelMat = loadDataSet()
weigths = gradAscent(dataMat, labelMat)
print("w0: %f, w1: %f, W2: %f" % (weigths[0], weigths[1], weigths[2]))
上面已经解出了一组回归系数,它确定了不同类别数据之间的分隔线。现在我们可以尝试去画出这个分隔线,
# 绘制数据集和Logistic回归最佳拟合直线
def plotBestFit(weights):
dataMat, labelMat = loadDataSet() # 加载数据集,标签
dataArr = array(dataMat) # 转换成umPy的数组
n = shape(dataArr)[0] # 获取数据总数
xcord1 = []; ycord1 = [] # 存放正样本
xcord2 = []; ycord2 = [] # 存放负样本
for i in range(n): # 依据数据集的标签来对数据进行分类
if int(labelMat[i]) == 1: # 数据的标签为1,表示为正样本
xcord1.append(dataArr[i, 1]); ycord1.append(dataArr[i, 2])
else: # 否则,若数据的标签不为1,表示为负样本
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) # x区间
y = (-weights[0] - weights[1] * x) / weights[2] # 最佳拟合直线
ax.plot(x, y)
plt.title('BestFit') # 标题
plt.xlabel('X1'); plt.ylabel('X2') # x,y轴的标签
plt.show()
# 运行画出图
dataMat, labelMat = loadDataSet()
weigths = gradAscent(dataMat, labelMat)
plotBestFit(weigths.getA())
print("w0: %f, w1: %f, W2: %f" % (weigths[0], weigths[1], weigths[2]))
从得出的结果图,我们可以看出,这个分类结果很不错了,从图上看出错分点的点不多。但是,尽管例子简单且数据集很小, 这个方法却需要300次乘法大量的计算。
梯度上升算法在每次更新回归系数时都需要遍历整个数据集,该方法在处理100个左右的数 据集时尚可,但如果有数十亿样本和成千上万的特征,那么该方法的计算复杂度就太高了。一种 改进方法是一次仅用一个样本点来更新回归系数,该方法称为随机梯度上升算法。由于可以在新 样本到来时对分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法。与“在线学习”相对应,一次处理所有数据被称作是“批处理”。
# 随机梯度上升算法
def stocGradAscent0(dataMatrix, classLabels): # dataMatIn数据集、classLabels数据标签
m, n = shape(dataMatrix) # 获取数据集矩阵的大小,m为行数,n为列数
alpha = 0.01 # 目标移动的步长
weights = ones(n) # 所以初始化为1
for i in range(m): # 重复矩阵运算
h = sigmoid(sum(dataMatrix[i] * weights)) # 矩阵相乘,计算sigmoid函数
error = classLabels[i] - h # 计算误差
weights = weights + alpha * error * dataMatrix[i] # 矩阵相乘,更新权重
return weights
可以看到,随机梯度上升算法与梯度上升算法在代码上很相似,但也有一些区别:第一,后 者的变量h和误差error都是向量,而前者则全是数值;第二,前者没有矩阵的转换过程,所有 变量的数据类型都是NumPy数组。
# 运行测试代码
dataMat, labelMat = loadDataSet()
weigths = stocGradAscent0(array(dataMat), labelMat)
plotBestFit(weigths)
print("w0: %f, w1: %f, W2: %f" % (weigths[0], weigths[1], weigths[2]))
随机梯度上升算法在上述数据集上的执行结果,最佳拟合直线并非最佳分类线,可以看出拟合曲线出现了很大的偏差。
如上图,展示了随机梯度上升算法在200次迭代过程中回归系数的变化情况。上图中的X2只经过了50次迭代就达到了稳定值,但X1和X0则需要更多次的迭代。在大的波动停止后,还有一些小的周期性波动。产生这种现象的原因是存在一些不能正确分类的样本点,在每次迭代时会引发系数的剧烈改变。
# 改进的随机梯度上升算法
def stocGradAscent1(dataMatrix, classLabels, numIter=150): # dataMatIn数据集、classLabels数据标签、numIter迭代次数
m, n = shape(dataMatrix) # 获取数据集矩阵的大小,m为行数,n为列数
weights = ones(n) # 所以初始化为1
for j in range(numIter):
dataIndex = list(range(m)) # 创建数据下标列表
for i in range(m):
alpha = 4 / (1.0 + j + i) + 0.0001 # apha目标移动的步长,每次迭代调整
randIndex = int(random.uniform(0, len(dataIndex))) # 随机选取更新样本
h = sigmoid(sum(dataMatrix[randIndex] * weights)) # 矩阵相乘,计算sigmoid函数
error = classLabels[randIndex] - h # 计算误差
weights = weights + alpha * error * dataMatrix[randIndex] # 矩阵相乘,更新权重
del (dataIndex[randIndex]) # 删除已使用过的样本
return weights
# 测试
dataMat, labelMat = loadDataSet()
weigths = stocGradAscent1(array(dataMat), labelMat)
plotBestFit(weigths)
print("w0: %f, w1: %f, W2: %f" % (weigths[0], weigths[1], weigths[2]))
改进的第一个方面在alpha = 4 / (1.0 + j + i) + 0.0001,alpha在每次迭代的时候都会调整,另外,虽然alpha会随着迭代次数不断减小,但永远不会减小到0。必须这样做的原因是为了保证在多次迭代之后新数据仍然具有一定的影响。 如果要处理的问题是动态变化的,那么可以适当加大上述常数项,来确保新的值获得更大的回归系数。另一点值得注意的是,在降低alpha的函数中,alpha每次减少1/(j+i) ,其中j是迭代次数, i是样本点的下标。
第二个改进的地方在randIndex = int(random.uniform(0, len(dataIndex)))处,这里通过随机选取样本来更新回归系数,每一次都是迭代未用过的样本点。这种方法将减少周期性的波动,计算量减少了,而且从上述的运行结果可以看出,回归效果挺好。
如上图,使用样本随机选择和alpha动态减少机制的随机梯度上升算法stocGradAscent1() 所生成的系数收敛示意图。该方法比采用固定alpha的方法收敛速度更快。
使用 Logistic 回归来预测患有疝病的马的存活问题。疝病是描述马胃肠痛的术语。然而,这种病不一定源自马的胃肠问题,其他问题也可能引发马疝病。这个数据集中包含了医院检测马疝病的一些指标,有的指标比较主观,有的指标难以测量,例如马的疼痛级别。
实例:从疝气病症状预测病马的死亡率 |
(1) 收集数据:给定数据文件。 (2) 准备数据:用Python解析文本文件并填充缺失值。 (3) 分析数据:可视化并观察数据。 (4) 训练算法:使用优化算法,找到最佳的系数。 (5) 测试算法:为了量化回归的效果,需要观察错误率。根据错误率决定是否回退到训练 阶段,通过改变迭代的次数和步长等参数来得到更好的回归系数。 (6) 使用算法:实现一个简单的命令行程序来收集马的症状并输出预测结果并非难事,这 可以做为留给读者的一道习题。 |
病马的训练数据,如下形式存储在文本文件中:
# 分类函数
def classifyVector(inX, weights):
prob = sigmoid(sum(inX * weights)) # 计算sigmoid值
if prob > 0.5: # 概率大于0.5,返回分类结果1.0
return 1.0
else: # 概率小于等于0.5,返回分类结果0.0
return 0.0
def colicTest1():
# 读取测试集和训练集,并对数据进行格式化处理
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 = gradAscent(array(trainingSet), trainingLabels)
errorCount = 0 # 错误数
numTestVec = 0.0
for line in frTest.readlines(): # 遍历每行数据
numTestVec += 1.0 # 测试集数量加1
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 # 预测结果与真值不一致,错误数加1
errorRate = (float(errorCount) / numTestVec) # 计算错误率
print("测试的错误率为: %f" % errorRate)
return errorRate
# 求结果的平均值
def multiTest():
numTests = 10
errorSum = 0.0
for k in range(numTests):
errorSum += colicTest1()
print("在 %d 迭代之后, 平均错误率为: %f" % (numTests, errorSum / float(numTests)))
从上面的结果可以看到,10次迭代之后的平均错误率为28%。
使用Logistic 回归方法进行分类并不需要做很多工作,所需做的只是把测试集上每个特征向量乘以最优化方法得来的回归系数,再将该乘积结果求和,最后输入到Sigmoid函数中即可。如果对应的Sigmoid值大于0.5就预测类别标签为1,否则为0。
# 分类函数
def classifyVector(inX, weights):
prob = sigmoid(sum(inX * weights)) # 计算sigmoid值
if prob > 0.5: # 概率大于0.5,返回分类结果1.0
return 1.0
else: # 概率小于等于0.5,返回分类结果0.0
return 0.0
# 基于改进后的随机梯度上升算法的Logistic分类器
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, 1000)
errorCount = 0 # 错误数
numTestVec = 0.0
for line in frTest.readlines(): # 遍历每行数据
numTestVec += 1.0 # 测试集数量加1
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 # 预测结果与真值不一致,错误数加1
errorRate = (float(errorCount) / numTestVec) # 计算错误率
print("测试的错误率为: %f" % errorRate)
return errorRate
# 求结果的平均值
def multiTest():
numTests = 10
errorSum = 0.0
for k in range(numTests):
errorSum += colicTest()
print("在 %d 迭代之后, 平均错误率为: %f" % (numTests, errorSum / float(numTests)))
从上面的结果可以看到,10次迭代之后的平均错误率为34%。
梯度上升算法:每次更新回归系数所有样本都参与。
随机梯度下降法:每次更新回归系数只有一个样本参与。
本次实验过程,遇到了好几个小问题,比如绘图的时候,拟合曲线不显示的问题,没有注意到x,y的维度问题,导致卡在绘图上半天;还有就是容易忽略变量的数据类型,因为python语言,在定义变量类型这方面是没有严格要求,不像C或C++等语言一样要明确写出来的,所以我老是忘记把数据转换为列表或矩阵,导致过程中一直运行出错。到底是把实验弄明白,做出来了,收获了一种分类方法,继续努力!!!