目录
- 一、Logistic回归函数
- 二、确定最佳回归系数——极大似然估计+最优化
- 三、梯度上升算法
- 四、算法实例——从疝气病症状预测病马的死亡率
- 五、拓展——使用Sklearn构建Logistic回归分类器
- 六、实验总结
一、Logistic回归函数
逻辑斯谛回归(logistic regression)是统计学习中的经典分类方法,属于对数线性模型,所以也被称为对数几率回归。这里要注意,虽然带有回归的字眼,但是该模型是一种分类算法,逻辑斯谛回归是一种线性分类器,针对的是线性可分问题。利用logistic回归进行分类的主要思想是:根据现有的数据对分类边界线建立回归公式,以此进行分类。这里的“回归”一词源于最佳拟合,表示要找到最佳拟合参数集,因此,logistic训练分类器时的做法就是寻找最佳拟合参数,使用的是最优化方法。
我们先说一个概念,事件的几率(odds),是指该事件发生的概率与该事件不发生的概率的比值。如果事件发生的概率是p,那么该事件的几率是p/(1-p)。取该事件发生几率的对数,定义为该事件的对数几率(log odds)或logit函数:
我对其取对数的理解大概是这样:事件发生的概率p的取值范围为[0,1],对于这样的输入,计算出来的几率只能是非负的(大家可以自己验证),而通过取对数,便可以将输出转换到整个实数范围内,下面是log函数的在二维坐标系中的图像,依照图像就会对标黄的那句话有一个形象的了解了。
那我们将输出转换到整个实数范围内的目的是什么呢?因为这样,我们就可以将对数几率记为输入特征值的线性表达式 :
其中,p(y =1|x)是条件概率分布,表示当输入为x时,实例被分为1类的概率,依据此概率我们能得到事件发生的对数几率。但是,我们的初衷是做分类器,简单点说就是通过输入特征来判定该实例属于哪一类别或者属于某一类别的概率。所以我们取logit函数的反函数,经如下推导:
公式1就是logistic函数。而Φ(x)是一个sigmoid函数,类似于阶跃函数的S型生长曲线:
上图给出了sigmoid函数在不同坐标尺度下的两条曲线图。当x为0时,sigmoid函数值为0.5。随着x的增大,对应的sigmoid函数的值将逼近于1;而随着x的减小,sigmoid函数的值将逼近于0。而第二幅图中我们能看到在横坐标的刻度足够大是,在x=0处sigmoid函数看起来很像阶跃函数。
那么对于公式1,我们可以这样解释:为了实现logistic回归分类器,我们可以在每个特征上都乘以一个回归系数,然后把所有的结果值相加,将这个总和带入sigmoid函数中。进而得到一个范围在0-1之间的数值。最后设定一个阈值,在大于阈值时判定为1,否则判定为0。以上便是逻辑斯谛回归算法是思想,公式就是分类器的函数形式。
二、确定最佳回归系数——极大似然估计+最优化
已经确定了logisti分类函数,有了分类函数,我们输入特征向量就可以得出实例属于某个类别的概率。但这里有个问题,权重w(回归系数)我们是不确定的。正如我们想的那样,我们需要求得最佳的回归系数,从而使得分类器尽可能的精确。
如何才能获得最佳的回归系数呢?这里就要用到最优化方法。在很多分类器中,都会将预测值与实际值的误差的平方和作为损失函数(代价函数),通过梯度下降算法求得函数的最小值来确定最佳系数。前面我们提到过某件事情发生的概率为p,在逻辑斯蒂回归中所定义的损失函数就是定义一个似然函数做概率的连乘,数值越大越好,也就是某个样本属于其真实标记样本的概率越大越好。如,一个样本的特征x所对应的标记为1,通过逻辑斯蒂回归模型之后,会给出该样本的标记为1和为-1的概率分别是多少,我们当然希望模型给出该样本属于1的概率越大越好。既然是求最大值,那我们用到的最优化算法就是梯度上升,其实也就是与梯度下降相反而已。
2.1 最大似然函数
我们需要先定义一个最大似然函数L,假定数据集中的每个样本都是独立的,其计算公式如下:
L(w)就是我们以上说的对于损失函数的最原始的定义,但我们还要进一步处理,那就是取对数,在进行极大似然估计的时候我们都知道要取对数,那为什么我们要取对数呢?
首先,在似然函数值非常小的时候,可能出现数值溢出的情况(简单点说就是数值在极小的时候因为无限趋近于0而默认其等于0,具体的可以点解数值移溢出的链接看看这篇博客中的讲解),使用对数函数降低了这种情况发生的可能性。其次,我们可以将各因子的连乘转换为和的形式,利用微积分中的方法,通过加法转换技巧可以更容易地对函数求导。
2.2 取似然函数的对数
这里似然函数是取最大值的,我们可以直接将其确定为损失函数然后使用梯度上升算法求最优的回归系数。但是既然是损失函数,应该还是取最小值好一点,且我们最耳熟能详的方法也是梯度下降,很多书中讲到这里也都是用的梯度下降算法,所以我将以上公式取反来使用梯度下降算法来求最小值(说到底都一样,无非就是取反,一个加一个减)。
接下来我们就要使用梯度下降求最小值了。首先,计算对数似然函数对j个权重的偏导:
先计算一下sigmoid函数的偏导:
我们的目标是求得使损失函数最小化的权重w,所以按梯度下降的方向不断的更新权重:
所以综上,我们将梯度下降的更新规则定义为:
三、梯度上升算法
3.1 算法原理
梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。如果梯度记为∇ \nabla∇,则函数f(x,y)的梯度由下式表示:
其中,函数f(x,y)必须要在待计算的点上有定义并且可微。一个具体的函数例子见下图:
梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2.如此循环迭代,知道满足通知条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向
上图中的梯度上升算法沿梯度方向移动了一步。可以看到,梯度算子总是指向函数值增长最快的方向。这里所说的是移动方向,而未提到移动量的大小。该量值称为步长,记作α \alphaα。用向量来表示的话,梯度上升算法的迭代公式如下:
该公式将一直迭代执行,直至达到某个停止条件为止,b比如迭代次数达到某个值或者算法达到某个可以允许的误差范围。
注:如果是梯度下降法,那就是按梯度上升的反方向迭代公式即可,对应的公式如下:
3.1 源码实现
// An highlighted block
from numpy import *
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 + exp(-inX))
def gradAscent(dataMatIn, classLabels):
dataMatrix = mat(dataMatIn) # convert to NumPy matrix
labelMat = mat(classLabels).transpose() # convert to NumPy matrix
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) # matrix mult
error = (labelMat - h) # vector subtraction
weights = weights + alpha * dataMatrix.transpose() * error # matrix mult
return weights
def plotBestFit(weights):
import matplotlib.pyplot as plt
dataMat, labelMat = loadDataSet()
dataArr = array(dataMat)
n = 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 = arange(-3.0, 3.0, 0.1)
y = (-weights[0] - weights[1] * x) / weights[2] #最佳拟合直线
ax.plot(x, y)
plt.xlabel('X1');
plt.ylabel('X2');
plt.show()
if __name__ =='__main__':
dataArr,labelMat=loadDataSet()
weights=gradAscent(dataArr,labelMat)
plotBestFit(weights.getA())
运行结果:
这里设置了sigmoid函数中的输入为0,就如我们在最开始的图中所看到的,x=0是两个类的分界线,分类效果尚可。
四、算法实例——从疝气病症状预测病马的死亡率
4.1 实战背景
本次实战内容,将使用Logistic回归来预测患疝气病的马的存活问题。这里的数据包含了500个样本和28个特征。这种病不一定源自马的肠胃问题,其他问题也可能引发马疝病。该数据集中包含了医院检测马疝病的一些指标,有的指标比较主观,有的指标难以测量,例如马的疼痛级别。另外需要说明的是,除了部分指标主观和难以测量外,该数据还存在一个问题,数据集中有30%的值是缺失的。下面将首先介绍如何处理数据集中的数据缺失问题,然后再利用Logistic回归和随机梯度上升算法来预测病马的生死。
4.2 准备数据
预处理数据做两件事:
- 如果测试集中一条数据的特征值已经确实,那么我们选择实数0来替换所有缺失值,因为本文使用Logistic回归。因此这样做不会影响回归系数的值。sigmoid(0)=0.5,即它对结果的预测不具有任何倾向性。
- 如果测试集中一条数据的类别标签已经缺失,那么我们将该类别数据丢弃,因为类别标签与特征不同,很难确定采用某个合适的值来替换。
原始的数据集经过处理,保存为两个文件:horseColicTest.txt和horseColicTraining.txt
有了这些数据集,我们只需要一个Logistic分类器,就可以利用该分类器来预测病马的生死问题了。
4.3 构建Logistic回归分类器
之前小节已经分析过原理,使用Logistic回归方法进行分类并不需要做很多工作,所需做的只是把测试集上每个特征向量乘以最优化方法得来的回归系数,再将乘积结果求和,最后输入到Sigmoid函数中即可。如果对应的Sigmoid值大于0.5就预测类别标签为1,否则为0。
# -*- coding:UTF-8 -*-
import numpy as np
import random
def sigmoid(inX):
return 1.0 / (1 + np.exp(-inX))
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
m,n = np.shape(dataMatrix) #返回dataMatrix的大小。m为行数,n为列数。
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的大小,每次减小1/(j+i)。
randIndex = int(random.uniform(0,len(dataIndex))) #随机选取样本
h = sigmoid(sum(dataMatrix[randIndex]*weights)) #选择随机选取的一个样本,计算h
error = classLabels[randIndex] - h #计算误差
weights = weights + alpha * error * dataMatrix[randIndex] #更新回归系数
del(dataIndex[randIndex]) #删除已经使用的样本
return weights #返回
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(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
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) * 100 #错误率计算
print("测试集错误率为: %.2f%%" % errorRate)
def classifyVector(inX, weights):
prob = sigmoid(sum(inX*weights))
if prob > 0.5: return 1.0
else: return 0.0
if __name__ == '__main__':
colicTest()
运行结果:
错误率稳定且较低,所以可以得到如下结论:
- 当数据集较小时,我们使用梯度上升算法
- 当数据集较大时,我们使用改进的随机梯度上升算法
对应的,在Sklearn中,我们就可以根据数据情况选择优化算法,比如数据较小的时候,我们使用liblinear,数据较大时,我们使用sag和saga。
五、拓展——使用Sklearn构建Logistic回归分类器
5.1 LogisticRegressioin
sklearn.linear_model模块提供了很多模型供我们使用,比如Logistic回归、Lasso回归、贝叶斯脊回归等,可见需要学习的东西还有很多很多。此次,我们使用LogisticRegressioin
5.2 源码实战
# -*- coding:UTF-8 -*-
from sklearn.linear_model import LogisticRegression
"""
函数说明:使用Sklearn构建Logistic回归分类器
"""
def colicSklearn():
frTrain = open('horseColicTraining.txt') #打开训练集
frTest = open('horseColicTest.txt') #打开测试集
trainingSet = []; trainingLabels = []
testSet = []; testLabels = []
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]))
for line in frTest.readlines():
currLine = line.strip().split('\t')
lineArr =[]
for i in range(len(currLine)-1):
lineArr.append(float(currLine[i]))
testSet.append(lineArr)
testLabels.append(float(currLine[-1]))
classifier = LogisticRegression(solver='liblinear',max_iter=10).fit(trainingSet, trainingLabels)
test_accurcy = classifier.score(testSet, testLabels) * 100
print('正确率:%f%%' % test_accurcy)
if __name__ == '__main__':
colicSklearn()
运行结果:
可以看到,正确率又高一些了。更改solver参数,比如设置为sag,使用随机平均梯度下降算法会发现产生了警告:
而产生警告是因为算法还没有收敛,更改max_iter为5000,再运行代码:
可以看到,对于这样的500个样本的数据集,sag算法需要迭代上千次才收敛,而liblinear只需要不到10次。据此可以得出一个结论:我们需要根据数据集情况,选择最优化算法。
六、实验总结
6.1 Logistic回归的优缺点
优点:
- 实现简单,易于理解和实现;计算代价不高,速度很快,存储资源低。
- 基于概率建模,输出值落在0到1之间,并且有概率意义
- 解决过拟合的方法很多,如L1、L2正则化,L2正则化就可以解决多重共线性问题
缺点:
- 容易欠拟合,分类精度可能不高。
- 因为它本质上是一个线性的分类器,所以处理不好特征之间相关的情况,对模型中自变量多重共线性较为敏感,例如两个高度相关自变量同时放入模型,可能导致较弱的一个自变量回归符号不符合预期,符号被扭转,正好变负号。
- 当特征空间很大时,性能不好
6.2 一些思考
-
分类和回归任务的区别
我们可以按照任务的种类,将任务分为回归任务和分类任务.那这两者的区别是什么呢?按照较官方些的说法,输入变量与输出变量均为连续变量的预测问题是回归问题,输出变量为有限个离散变量的预测问题成为分类问题.通俗一点讲,我们要预测的结果是一个数,比如要通过一个人的饮食预测一个人的体重,体重的值可以有无限多个,有的人50kg,有的人51kg,在50和51之间也有无限多个数.这种预测结果是某一个确定数,而具体是哪个数有无限多种可能的问题,我们会训练出一个模型,传入参数后得到这个确定的数,这类问题我们称为回归问题.预测的这个变量(体重)因为有无限多种可能,在数轴上是连续的,所以我们称这种变量为连续变量.
我们要预测一个人身体健康或者不健康,预测会得癌症或者不会得癌症,预测他是水瓶座,天蝎座还是射手座,这种结果只有几个值或者多个值的问题,我们可以把每个值都当做一类,预测对象到底属于哪一类.这样的问题称为分类问题.如果一个分类问题的结果只有两个,比如"是"和"不是"两个结果,我们把结果为"是"的样例数据称为"正例",讲结果为"不是"的样例数据称为"负例",对应的,这种结果的变量称为离散型变量.
-
逻辑回归需要归一化吗?
逻辑回归不需要归一化。即使是量纲不一样,因为变量x前面有权重w,通过训练,w会对x进行调整。像基于距离计算的算法KNN,而SVM就需要归一化。能够谈到逻辑回归需不需要归一化的场景,个人认为是逻辑回归做回归任务时。在做分类时,一般要对数据进行处理,类别型one-hot,连续值会进行分桶,分桶之后进行one-hot, 整体数据只有0,1。