简介
今天由我来向大家介绍Logistic回归及如何实现。在此之前我们先来了解回归和分类的区别。
机器学习的回归(Regression)和分类(Classification)是两种常见的预测任务,它们在目标和方法上有一些关键区别:
1、目标的性质:
- 回归:旨在预测一个连续的数值。例如,预测房价、温度或者产品的销售量。
- 分类:旨在将实例分配到预定义的类别中。例如,判断邮件是否为垃圾邮件、识别图片中的物体1类别(如猫、狗等)。
2、输出类型:
- 回归:输出是一个连续的数值,可以是任何实数。
- 分类:输出是类别标签,通常为离散的集合(如“是”或“否”,“红色”、“蓝色”、“绿色”等)。
3、评估指标:
- 回归:通常使用均方误差(Mean Squared Error,MSE)、均方根误差(Root Mean Squared Error,RMSE)或平均绝对误差(Mean Absolute Eroor,MAE)等指标来评估模型性能。
- 分类:常用的评估指标包括准确率(Accuracy)、精确度(Precision)、召回率(Recall)、得分等。
一、Logistic回归
1.1什么是Logistic回归
Logistic回归是一种最优化算法。用一条直线对一些数据点进行拟合的过程被称为回归。利用Logistic回归进行分类的主要思想是:根据现有数据利用Logistic回归生成最佳拟合线,并以此作为数据的分类边界线。逻辑回归假设数据服从伯努利分布,通过极大似然估计的方法,运用梯度下降来求解参数,来达到数据二分类的目的。
1.2线性回归函数
线性回归函数的模型
若采用向量的写法,上述公式可以写为
其中,向量x是分类器的输入数据,向量w,b为待求解系数,我们称其为线性回归,线性回归的目的就是学习一个线性模型以尽可能准确地预测实值输出标记。
1.3逻辑函数(Sigmoid函数)
若要处理的是二分类问题,我们期望的函数输出会是0或1,类似于单位阶跃函数,可是该函数是不连续的,不连续不可微。
因此我们换一个函数——Sigmoid函数,当x=0时,y为0.5;随着x的增大,y值趋近于1,随着x的减小,y趋近于0,当横坐标足够大时,Sigmoid函数就会看起来像一个阶跃函数。Sigmoid函数的值域在(0,1)之间,而概率P也是在0到1之间,因此我们可以把Sigmoid的值域和概率联系起来
1.4Logistic回归函数
Logistic回归的原理是用Sigmoid函数把线性回归的结果从(-∞,∞)映射到(0,1)。我们用公式描述这句话:
把线性回归函数的结果y,放到sigmod函数中
去,就构造了Logistic回归函数。 由y的值域和sigmod函数的值域可知,在Logistic回归函数中用sigmod函数把线性回归的结果(-∞,∞)映射到(0,1),得到的这个结果类似一个概率值。
我们转换一下逻辑回归函数,过程如下:
因此Logistic回归属于对数线性模型,也被称为对数几率回归。
1.5Logistic回归的优缺点
优点
(1)对率函数任意阶可导,具有很好的数学性质,许多现有的数值优化算法都可以用来求最优解,训练速度快;
(2)简单易理解,模型的可解释性非常好,从特征的权重可以看到不同的特征对最后结果的影响;
(3)适合二分类问题,不需要缩放输入特征;
(4)内存资源占用小,因为只需要存储各个维度的特征值;
(5)直接对分类可能性进行建模,无需事先假设数据分布,避免了假设分布不准确所带来的问题;
(6)以概率的形式输出,而非知识0.1判定,对许多利用概率辅助决策的任务很有用。
缺点
(1)不能用逻辑回归去解决非线性问题,因为Logistic的决策面试线性的;
(2)对多重共线性数据较为敏感;
(3)很难处理数据不平衡的问题;
(4)准确率并不是很高,因为形式非常的简单(非常类似线性模型),很难去拟合数据的真实分布;
(5)逻辑回归本身无法筛选特征,有时会用gbdt来筛选特征,然后再上逻辑回归。
1.6Logistic回归的一般过程
1.收集数据:采用任意方法收集
2.准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳
3.分析数据:采用任意方法对数据进行分析
4.训练算法:大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数
5.测试算法:一旦训练步骤完成,分类将会很快。
6.使用算法:首 先,我们需要输入一些数据,并将其转换成对应的结构化数值;接着,基于训练好的回归系数就可以对这些数值进行简单回归计算,判定它们属于哪个类别;在这之后,我们就可以在输出的类别上做一些其他分析工作。
二、基于最优化方法的最佳回归系数确定
2.1最大似然估计法
2.2梯度上升法
基本思想:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。如果梯度记为▽,则函数f(x,y)的梯度由下式表示:
这个梯度意味着要沿x的方向移动:
沿y的方向移动:
如图,梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2。如此循环迭代,直到满足停止条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。
用向量来表示的话,梯度算法的迭代公式如下:
,其中
表示步长。
2.3梯度下降法
按梯度上升的反方向迭代公式即可
梯度上升算法用来求函数的最大值,而梯度下降算法用来求函数的最小值。
三、Logistic回归的实现
3.1数据准备
有100个样本点,每个数据有两个特征维度,将第一列看做x1的值,第二列看做x2的值,第三列作为分类的标签
3.2查看数据集的分布情况
# -*- coding:UTF-8 -*-
import matplotlib.pyplot as plt
import numpy as np
"""
函数说明:加载数据
Returns:
dataMat - 数据列表
labelMat - 标签列表
"""
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])) #添加标签
fr.close() #关闭文件
return dataMat, labelMat #返回
"""
函数说明:绘制数据集
"""
def plotDataSet():
dataMat, labelMat = loadDataSet() #加载数据集
dataArr = np.array(dataMat) #转换成numpy的array数组
n = np.shape(dataMat)[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]) #1为正样本
else:
xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2]) #0为负样本
fig = plt.figure()
ax = fig.add_subplot(111) #添加subplot
ax.scatter(xcord1, ycord1, s = 20, c = 'black', marker = 's',alpha=.5)#绘制正样本
ax.scatter(xcord2, ycord2, s = 20, c = 'red',alpha=.5) #绘制负样本
plt.title('DataSet') #绘制title
plt.xlabel('x1'); plt.ylabel('x2') #绘制label
plt.show() #显示
if __name__ == '__main__':
plotDataSet()
如图所示:假设Sigmoid函数的输入记为z,那么,即可将数据分割开。其中,x0为全是1的向量,x1为数据集的第一列数据,x2为数据集的第二列数据。因此,我们需要求出这个方程未知的参数w0,w1,w2,也就是我们需要求的回归系数(最优参数)。
3.3训练算法:使用梯度上升找到最佳参数
"""
函数说明:sigmoid函数
Parameters:
inX - 数据
Returns:
sigmoid函数
"""
def sigmoid(inX):
return 1.0 / (1 + np.exp(-inX))
"""
函数说明:梯度上升算法
Parameters:
dataMatIn - 数据集
classLabels - 数据标签
Returns:
weights.getA() - 求得的权重数组(最优参数)
"""
def gradAscent(dataMatIn, classLabels):
dataMatrix = np.mat(dataMatIn) #转换成numpy的mat
labelMat = np.mat(classLabels).transpose() #转换成numpy的mat,并进行转置
m, n = np.shape(dataMatrix) #返回dataMatrix的大小。m为行数,n为列数。
alpha = 0.001 #移动步长,也就是学习速率,控制更新的幅度。
maxCycles = 500 #最大迭代次数
weights = np.ones((n,1))
for k in range(maxCycles):
h = sigmoid(dataMatrix * weights) #梯度上升矢量化公式
error = labelMat - h
weights = weights + alpha * dataMatrix.transpose() * error
return weights.getA() #将矩阵转换为数组,返回权重数组
现在,我们已经求出了未知的参数w0=4.12414349,w1=0.48007329,w2=-0.6168482,接下去通过求解出的参数,我们就可以确定不同类别数据之间的分隔线,画出决策边界。
3.4分析数据:画出决策边界
# 绘制数据集和Logistic回归最佳拟合直线
def plotBestFit(weights):
dataMat, labelMat = loadDataSet() # 加载数据集,标签
dataArr = np.array(dataMat) # 转换成umPy的数组
n = np.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 = np.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()
if __name__ == '__main__':
weights = gradAscent(dataMat, labelMat)
plotBestFit(weights)
从得出的结果图,我们可以看出,这个分类结果很不错了,从图上看出错的点不多。
3.5训练算法:随机梯度上升
# 随机梯度上升算法
def stocGradAscent0(dataMatrix, classLabels): # dataMatIn数据集、classLabels数据标签
m, n = np.shape(dataMatrix) # 获取数据集矩阵的大小,m为行数,n为列数
alpha = 0.01 # 目标移动的步长
weights = np.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
# 运行测试代码
dataMat, labelMat = loadDataSet()
weigths = stocGradAscent0(np.array(dataMat), labelMat)
plotBestFit(weigths)
print("w0: %f, w1: %f, W2: %f" % (weigths[0], weigths[1], weigths[2]))
随机梯度上升算法在上述数据集上的执行结果,最佳拟合直线并非最佳分类线,可以看出拟合曲线出现了很大的偏差。
3.6改进的随机梯度上升算法
# 改进的随机梯度上升算法
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)))处,这里通过随机选取样本来更新回归系数,每一次都是迭代未用过的样本点。这种方法将减少周期性的波动,计算量减少了,而且从上述的运行结果可以看出,回归效果挺好。
四、实例:从疝气病症预测病马的死亡率
4.1准备数据:处理数据中的缺失值
原始数据集
上述数据还存在一个问题,数据集中有30%的值是缺失的。数据中的缺失值是一个非常棘手的问题,下面给出了一些可选的做法:
使用可用特征的均值来填补缺失值;
使用特殊值来填补缺失值,如-1;
忽略有缺失值的样本;
使用相似样本的均值填补缺失值;
使用另外的机器学习算法预测缺失值。
数据预处理:
所有的缺失值必须要用一个实数值来替换,因为我们使用的Numpy数据类型不允许包含缺失值。这里选择实数0来替换所有的缺失值,恰好能适用于Logistic回归。这样做的直觉是我们需要的是一个在更新时不会影响系数的值。另外,由于sigmoid(0)=5,即它对结果的预测不具有任何倾向性,因此上述做法也不会对误差造成任何影响。
如果测试集中一条数据的类别标签已经缺失,那么我们将该类别数据丢弃,因为类别标签与特征不同,很难确定采用某个合适的值来替换。
处理好的数据
4.2测试算法:用Logistic回归进行分类
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 = stocGradAscent1(np.array(trainingSet), trainingLabels, 1000) #得到最佳回归系数
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() #调用colicTest()函数十次并求结果的平均值
print ("after %d iterations the average error rate is: %f" % (numTests, errorSum/float(numTests)))
if __name__=='__main__':
multiTest()
可以发现调用colicTest()函数十次,很多次算出来的错误率都略有不同的,这就是随机梯度上升算法的特点,因为每次用来迭代回归系数的样本都是随机的;要stocGradAscent1()函数得到的回归系数完全收敛,结果才能确定下来。在30%的数据缺失前提下,十次平均的错误率是34.6%,想要更低的错误率,我们可以对stocGradAscent1()函数中的步长和迭代次数进行调整。
五、问题及总结
问题:为什么与Sigmoid相似的Sign之类的函数不行?
Sign函数,他长得跟Sigmoid函数很类似,也能够将 X 限制在 0 到 1 的范围内。 我们知道,Logistic回归只是在线性回归上增加了一个 g(x) 的限制,而在模型训练的过程中实际上还是对线性回归中的进行训练。我们通过梯度下降在线性回归中进行计算,这就存在一个前提,即损失函数可导。而以 Sign函数为假设函数列出来的损失函数明显存在不可导(左导 = 0,右导 = 1)。而反观 Sigmoid函数,在左右范围内均有导数存在,所以采用Sigmoid函数。
总结
Logistic回归进行分类的主要思想是:根据训练数据利用Logistic回归生成最佳回归系数,并以此进行待测数据的分类。逻辑回归假设数据服从伯努利分布,通过极大似然估计的方法,运用(随机)梯度下降来求解参数,来达到数据二分类的目的。这次实验让我对Logistic回归有了一定的了解和认识,能运用其解决实际问题,但还不能熟练使用,要继续加强对机器学习相关知识的学习。总的来说是一次收获满满的实验。