目录
Sigmoid函数
最小二乘法(线性模型)
数据是一维的
数据是多维的
对数线性回归(非线性模型)
极大似然估计
梯度下降算法
Logistics回归相关代码理解
使用梯度上升找到最佳回归系数:
读取数据:
梯度上升优化算法:
画出决策边界:
使用随机梯度上升找到最佳回归系数:
随机梯度上升算法:
从疝气病症预测病马的死亡率:
我的实验:
数据:UCI German_Bank
总结:
Logistic回归是一种最优化算法。用一条直线对一些数据点进行拟合的过程被称为回归。利用Logistic回归进行分类的主要思想是:根据现有数据利用Logistic回归生成最佳拟合线,并以此作为数据的分类边界线。逻辑回归假设数据服从伯努利分布,通过极大似然估计的方法,运用梯度下降来求解参数,来达到数据二分类的目的。
若要处理的是二分类问题,我们期望的函数输出会是0或1,类似于单位阶跃函数,可是该函数是不连续的,不连续不可微。
因此我们换一个函数——Sigmoid函数,当x=0时,y为0.;随着x的增大,y值趋近于1,随着x的减小,y趋近于0,当横坐标足够大时,Sigmoid函数就会看起来像一个阶跃函数。
而Sigmoid函数,它的输入会是如下线性模型:
若采用向量的写法,上述公式可以写为 :
其中,向量x是分类器的输入数据,向量w,b为待求解系数,我们称其为线性回归,线性回归的目的就是学习一个线性模型以尽可能准确地预测实值输出标记(f(xi)~yi),为了得到最佳系数,需要用到最优化理论的一些知识。
我们的学习目标就是:
关于上式,分别对w和b求导,可得:
我们有相同的目的,但要把w和b整合成向量形式:
这是我们最小二乘法出来的式子,向量的平方和= 一个行向量*一个列向量:
根据上式对w求导,可得w的解析解;计算整理会得到下式:
我们拿到的数据有时并不满足线性回归模型,这时我们就可以推广至:
注意:函数g一定式单调可微的。
假设实例所对应的输出标记是在指数尺度上变化,那我们就可以将输出标记的对数作为线性模型逼近的目标,则:
我们的Sigmoid函数就不是常规的线性模型,我们的目标同样是求解未知参数w、b,这时我们也不能用线性模型里提到的的最小二乘法,而应该使用极大似然估计法。
至于为什么,我参考了一些资料:最小二乘法的目标函数是非秃函数,有很多局部最优解,不利于求解;而最大似然估计的目标函数就是对数似然函数,是关于(w,b)的高阶连续可导凸函数,可以方便通过一些凸优化算法求解,比如梯度下降法、牛顿法等。
极大似然估计就是根据已知结果去反推最大概率导致该已知结果的参数。这正是我们需要的求参过程。
假设数据服从伯努利分布,以下是一部分的求解过程(为了降低计算难度,通常会采用对数加法替换概率乘法):
因此我们的求参步骤如下:
引入一个知识点,样本作为正例的相对可能性的对数(正例的可能性和负例的可能性的比值的对数),被称为对数几率:
对上面重写的函数求导令其为0,会发现它无法解析求解,只能借助迭代的方法,这里我们选用经典的梯度下降算法。
梯度下降算法基于的思想是:要找到某函数的最小值,最好的方法是沿着该函数的梯度方向探寻。则函数f(x,y)的梯度由下式表示:
其中函数f(x,y)必须要在待计算的点上有定义并且可微,而梯度算法的迭代公式如下:
这里的α是步长,也就是学习率,步长过大会导致迭代过快,错过最优解;步长过小会导致收敛过慢,浪费时间;因此我们可以在模型的学习前期设置较大步长,在后期将步长调小,以此达到更佳的模型学习效果。
Logistics回归其实就是理解好sigmoid函数以及如何求解最佳回归系数,具体就是假设数据服从伯努利分布,通过极大似然估计的方法,运用梯度下降来求解参数,来达到数据二分类的目的。
从指定文件中逐行读取数据,每行的x0被设为1.0(为了方便计算),x1、x2为该行数据(该样本)的两个特征值,从文件中读取的每行的第三个值是该行数据的类别标签,将其append到变量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]))
return dataMat,labelMat
这段代码最主要就是这段梯度算法的迭代:
for k in range(maxCycles):
h = sigmoid(dataMatrix*weights)
error = (labelMat - h)
weights = weights + alpha * dataMatrix.transpose()* error #当前计算出来的系数矩阵,这就是估计偏移方向的过程
其中dataMatrix.transpose()* error的意思就是:
def sigmoid(inX):
return 1.0/(1+exp(-inX))
def gradAscent(dataMatIn, classLabels):
dataMatrix = mat(dataMatIn) #将数据列表转化为矩阵
labelMat = mat(classLabels).transpose() #将标签列表转化为矩阵,并转置,方便后续计算
m,n = shape(dataMatrix)
alpha = 0.001
maxCycles = 500
weights = ones((n,1)) #初始化系数矩阵
for k in range(maxCycles):
h = sigmoid(dataMatrix*weights)
error = (labelMat - h)
weights = weights + alpha * dataMatrix.transpose()* error #当前迭代出的系数矩阵,这就是估计偏移方向的过程
return weights
最后的输出是迭代出的最佳回归系数:
matrix([[ 4.12414349],
[ 0.48007329],
[-0.6168482 ]])
我们从上面得到了最佳回归系数,就可以用它确定不同类别之间的分隔线了 :
def plotBestFit(weights):
import matplotlib.pyplot as plt
dataMat,labelMat=loadDataSet()
dataArr = array(dataMat) #将list转换为数组
n = shape(dataArr)[0] #shape这个数组的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的特征一做X坐标;特征二做y坐标
else:
xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2])
#分类为0的特征一做X坐标;特征二做y坐标
fig = plt.figure() #画张图
ax = fig.add_subplot(111) #1*1的网格 唯一的一张子图
ax.scatter(xcord1, ycord1, s=30, c='red', marker='s') #分类1是红方块
ax.scatter(xcord2, ycord2, s=30, c='green') #分类2是绿圆点
x = arange(-3.0, 3.0, 0.1) #x轴的坐标为-3到3 步长为0.1
y = (-weights[0]-weights[1]*x)/weights[2] #画出咱的最佳拟合直线
ax.plot(x, y)
plt.xlabel('X1'); plt.ylabel('X2');
plt.show()
进行调用,这里面有一个getA()函数,getA()的用法与mat()函数相反,mat()是将数组转换成numpy矩阵,而get(A)则是将numpy矩阵转化成数组形式,gradAscent(dataMatIn, classLabels)函数的返回值weights是一个numpy矩阵(向量),不能作为plotBestFit(weights)的输入,所以要把它变成数组:
weights=logRegres.gradAscent(dataArr,labelMat)
logRegres.plotBestFit(weights.getA())
输出结果为:
这个输出结果相当漂亮,但是我们发现这种梯度上升算法在每次更新回归系数时都要遍历整个数据集,尽管我们的数据只有100条,但是却做了大量的计算(300次乘法),但如果我们面对数十亿样本和成千上万的特征(维度)呢?这种批处理方法的计算代价就太大了,而其中一种改进方法就是一次仅用一个样本点来更新回归系数,这种方法叫做随机梯度上升算法。
与前面的梯度上升算法不同,随机梯度上升算法只取一个样本点,而不是全样本;虽然两者代码相似,但前者的系数weights、变量h和误差error都是向量(numpy矩阵);而后者都是数值,且其所有变量的数据类型都是numpy数组:
def stocGradAscent0(dataMatrix, classLabels):
m,n = shape(dataMatrix)
alpha = 0.01
weights = ones(n) #初始化回归系数数值
for i in range(m):
h = sigmoid(sum(dataMatrix[i]*weights)) #预测出的y
error = classLabels[i] - h
weights = weights + alpha * error * dataMatrix[i]
#当前偏移量,这就是估计偏移方向的过程
return weights
调用stocGradAscent0(dataMatrix, classLabels)函数得到最佳回归系数并show出本次拟合直线图:
weights=logRegres.stocGradAscent0(array(dataArr),labelMat)
logRegres.plotBestFit(weights)
输出结果为:
这次得到的图很明显没有上次的图效果好,这条直线错分了三分之一的样本,但这并不能说明随机梯度上升算法没有梯度上升算法好,因为后者的迭代次数是500次;而前者的迭代次数远远不及500次且得到的参数很可能还未达到稳定值,那就让这个数据集迭代个200次试试:
def stocGradAscent0(dataMatrix, classLabels):
m,n = shape(dataMatrix)
alpha = 0.01
weights = ones(n) #初始化回归系数数值
for j in range(200):
for i in range(m):
h = sigmoid(sum(dataMatrix[i]*weights)) #预测出的y
error = classLabels[i] - h
weights = weights + alpha * error * dataMatrix[i]
#当前偏移量,这就是估计偏移方向的过程
return weights
果然得到了一个漂亮的结果:
但是不同的数据所回归的系数收敛速度是不同的,不能这么草率地用200次迭代来决定系数,200次相对于大数据来说还是太多了,且数据中总会有捣蛋比较难分类的样本,所以每次迭代都不可避免系数的来回波动,因此我们期望算法能避免来回波动,从而收敛到某个值,另外,收敛速度也需要加快,进一步改进算法:
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
m,n = shape(dataMatrix)
weights = ones(n) #初始化回归系数
for j in range(numIter): #迭代次数为numIter
dataIndex = list(range(m)) #做一个包含所有样本下标的list
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
解释一下改进的地方:
调用函数:
weights=logRegres.stocGradAscent1(array(dataArr),labelMat,20)#迭代次数为20
logRegres.plotBestFit(weights)
又是一个漂亮的结果:
这个算法不仅得到了一个漂亮的结果,收敛的速度大幅提升,而且仅仅迭代了20次所有参数就都达到了稳定值。
在实际实验中会遇到一些数据缺失的情况:比如说一条样本缺失某些特征值甚至是样本标签;但我们又不想直接抛弃这条样本,这时我们就需要对数据进行预处理;需要做的有两件事:
实验代码:
def classifyVector(inX, weights):
prob = sigmoid(sum(inX*weights))
if prob > 0.5: return 1.0
else: return 0.0
这个函数就是拿我们得到的回归系数算出类别,大于0.5类别为1,小于0.5类别为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(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(array(lineArr), trainWeights))!= int(currLine[21]): #判断是否分类错误
errorCount += 1
errorRate = (float(errorCount)/numTestVec) #计算错误率
print ("the error rate of this test is: %f" % errorRate)
return errorRate
因为logistics回归较适合处理二分类问题,所以将已经死亡和已经安乐死合并为一个类别,然后训练时的回归系数迭代次数为1000次;
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)))
调用colicTest()函数十次并求错误率的平均值 。
运行代码:
logRegres.multiTest()
输出结果:
the error rate of this test is: 0.358209
the error rate of this test is: 0.343284
the error rate of this test is: 0.343284
the error rate of this test is: 0.343284
the error rate of this test is: 0.328358
the error rate of this test is: 0.358209
the error rate of this test is: 0.343284
the error rate of this test is: 0.343284
the error rate of this test is: 0.358209
the error rate of this test is: 0.343284
after 10 iterations the average error rate is: 0.346269
可以发现调用colicTest()函数十次,很多次算出来的错误率都略有不同的,这就是随机梯度上升算法的特点,因为每次用来迭代回归系数的样本都是随机的;要stocGradAscent1()函数得到的回归系数完全收敛,那么结果才能确定下来。在30%的数据缺失前提下,十次平均的错误率是34.6%,想要更低的错误率,我们可以对stocGradAscent1()函数中的步长和迭代次数进行调整。
UCI German_Bank是UCI的德国信用数据集,里面有原数据和数值化后的数据。German Credit数据是根据个人的银行贷款信息和申请客户贷款逾期发生情况来预测贷款违约倾向的数据集,数据集包含20个维度的,1000条数据。该数据集将通过一组属性描述的人员分类为良好或不良信用风险。
数据说明
名称 | 类型 | 描述 |
---|---|---|
checking_account_status | string | 现有支票帐户的状态(A11:<0 DM,A12:0 <= x <200 DM,A13:> = 200 DM /至少一年的薪水分配,A14:无支票帐户) |
duration | integer D | 持续时间(月) |
credit_history | string | A30:未提取任何信用/已全额偿还所有信用额,A31:已偿还该银行的所有信用额,A32:已到期已偿还的现有信用额,A33:过去的还款延迟,A34:关键帐户/其他信用额现有(不在此银行) |
purpose | string | |
credit_amount | float | |
savings | string | 账户/债券储蓄(A61:<100 DM,A62:100 <= x <500 DM,A63:500 <= x <1000 DM,A64:> = 1000 DM,A65:未知/无储蓄账户 |
present_employment | string | 71:待业,A72:<1年,A73:1 <= x <4年,A74:4 <= x <7年,A75:..> = 7年 |
installment_rate | float | 分期付款率占可支配收入的百分比 |
personal | string | 个人婚姻状况和性别(A91:男性:离婚/分居,A92:女性:离婚/分居/已婚,A93:男性:单身,A94:男性:已婚/丧偶,A95:女性:单身) |
other_debtors | string | A101:无,A102:共同申请人,A103:担保人 |
present_residence | float | 至今居住 |
property | string | A121:不动产,A122:如果不是,那么A121:建筑协会储蓄协议/人寿保险,A123:如果不是,则A121 / A122:不是属性6的汽车或其他,A124:未知/没有财产 |
age | float | 年岁 |
other_installment_plans | string | A141:银行,A142:商店,A143:无 |
housing | string | A151:租房,A152:自有,A153:免费 |
existing_credits | float | 该银行现有信贷的数量 |
job | string | 1:失业/A171 : 非技术人员-非居民,A172:非技术人员-居民,A173:技术人员/官员,A174:管理/个体经营/高度合格的员工/官员 |
dependents | integer | 承担赡养费的人数 |
telephone | string | A191:无,A192:有,登记在客户名下 |
foreign_worker | string | A201: 有, A202: 无 |
customer_type | integer | 预测类别:1 =良好,-1 =不良 |
def classifyVectorGB(inX, weights):
#print("classifyVectorM里的weights")
#print(weights)
#print("classifyVectorM里的inX")
#print(inX)
prob = sigmoid(sum(inX*weights))
#print("classifyVectorM里的prob")
#print(prob)
if prob > 0.5: return 1.0
else: return -1.0
def germanBankTest():
frTrain = open('GBTraining.txt'); frTest = open('GBTest.txt')
trainingSet = []; trainingLabels = []
for line in frTrain.readlines():
currLine = line.strip().split(',')
lineArr =[]
for i in range(20):
lineArr.append(float(currLine[i])) #获取样本的特征数据
trainingSet.append(lineArr)
trainingLabels.append(float(currLine[20])) #获取样本标签
#print(trainingSet)
#print(trainingLabels)
trainWeights = stocGradAscent1(array(trainingSet), trainingLabels,1000) #得到最佳回归系数
#print("mushroomTest里的trainWeights")
#print(trainWeights)
errorCount = 0; numTestVec = 0.0
for line in frTest.readlines():
numTestVec += 1.0 #计算样本条数
currLine = line.strip().split(',')
lineArr =[]
for i in range(20):
lineArr.append(float(currLine[i])) #获取样本的特征数据
if int(classifyVectorGB(array(lineArr), trainWeights))!= int(currLine[20]): #判断是否分类错误
errorCount += 1
errorRate = (float(errorCount)/numTestVec) #计算错误率
print ("the error rate of this test is: %f" % errorRate)
return errorRate
def multiGermanBankTest():
numTests = 10; errorSum=0.0
for k in range(numTests):
errorSum += germanBankTest() #调用germanBankTest()函数十次并求结果的平均值
print ("after %d iterations the average error rate is: %f" % (numTests, errorSum/float(numTests)))
结果:
logRegres.multiGermanBankTest()
the error rate of this test is: 0.373134
the error rate of this test is: 0.353234
the error rate of this test is: 0.358209
the error rate of this test is: 0.363184
the error rate of this test is: 0.363184
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.373134
the error rate of this test is: 0.368159
the error rate of this test is: 0.363184
after 10 iterations the average error rate is: 0.364677
Logistic回归进行分类的主要思想是:根据训练数据利用Logistic回归生成最佳回归系数,并以此进行待测数据的分类。逻辑回归假设数据服从伯努利分布,通过极大似然估计的方法,运用(随机)梯度下降来求解参数,来达到数据二分类的目的。