Logistic回归-从氙气病症预测病马的死亡率
机器学习课程作业,网上找了很多代码基本都是复制粘贴,原理也没有写的很清楚,这里根据个人理解整理一下。
原始数据链接:Horse Colic - UCI Machine Learning Repository
一、问题介绍
疝气病是描述马胃肠痛的术语,然而,这种病并不一定源自马的胃肠问题,其他问题也可能引发疝气病。数据一共包括368个样本和21个特征。该数据集中包含了医院检测马疝气病的一些指标,有的指标比较主观,有的指标难以测量,例如马的疼痛级别。另外,除了部分指标主观和难以测量之外,该数据还存在一个问题,数据集中有30%的值是缺失的。
二、数据处理
将样本分为两组,其中300个作为训练用,记为horseColicTraining.txt,另外68个作为测试用,记为horseColicTest.txt。对数据集中缺失的数据,以0代替。处理后数据中每行为一个样本,前21列为数据,最后一列为0或1,代表分类的类别。
三、理论分析
该问题是一个二分类问题,对于测试集中每一组数据,我们需要输出0或1的预测值,因此采用logistic回归算法。
在logistic回归中,需要寻找参数 w ∈ R n w \in R^{n} w∈Rn和 b ∈ R b \in R b∈R,对于给定特征 x ∈ R n x \in R^{n} x∈Rn,计算 σ ( x ) = 1 1 + e − w T x + b \sigma(x) = \frac{1}{1 + e^{- w^{T}x + b}} σ(x)=1+e−wTx+b1,其中 σ \sigma σ为Sigmoid函数。若 σ ( x ) > 0.5 \sigma(x) > 0.5 σ(x)>0.5则输出1,否则输出0。
为了方便起见,我们可以记新的 x = ( x 1 ) x = \binom{x}{1} x=(1x), w = ( w b ) w = \binom{w}{b} w=(bw),从而 σ ( x ) = 1 1 + e − w T x \sigma(x) = \frac{1}{1 + e^{- w^{T}x}} σ(x)=1+e−wTx1
为了寻找合适的参数,可以采用极大似然估计法:
设训练数据集为 { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x k , y k ) } \left\{ \left( {x^{1},y_{1}} \right),\left( {x^{2},y_{2}} \right),\ldots,\left( x^{k},y_{k} \right) \right\} {(x1,y1),(x2,y2),…,(xk,yk)}。其中每个 x i ∈ R n x^{i} \in R^{n} xi∈Rn, y i ∈ { 0 , 1 } y_{i} \in \left\{ 0,1 \right\} yi∈{0,1}
由logistic回归模型,对于给定的 x x x, P ( Y = 1 | x ) = e − w T x 1 + e − w T x P\left( {Y = 1} \middle| x \right) = \frac{e^{- w^{T}x}}{1 + e^{- w^{T}x}} P(Y=1∣x)=1+e−wTxe−wTx, P ( Y = 0 | x ) = 1 1 + e − w T x P\left( {Y = 0} \middle| x \right) = \frac{1}{1 + e^{- w^{T}x}} P(Y=0∣x)=1+e−wTx1
记 f ( x ) = P ( Y = 1 | x ) f(x) = P\left( {Y = 1} \middle| x \right) f(x)=P(Y=1∣x)
则似然函数为
L ( w ) = ∏ i = 1 n P ( Y = y i | x i ) = ∏ i = 1 n f ( x i ) y i ( 1 − f ( x i ) ) ( 1 − y i ) L(w) = {\prod\limits_{i = 1}^{n}{P\left( Y = y_{i} \middle| x^{i} \right)}} = {\prod\limits_{i = 1}^{n}{{f\left( x^{i} \right)}^{y_{i}}\left( 1 - f\left( x^{i} \right) \right)^{(1 - y_{i})}}} L(w)=i=1∏nP(Y=yi xi)=i=1∏nf(xi)yi(1−f(xi))(1−yi)
我们的目标是使似然函数最大,可以对似然函数取对数,利用 y i ∈ { 0 , 1 } y_{i} \in \left\{ 0,1 \right\} yi∈{0,1}得
l n L ( w ) = ∑ i = 1 n ( y i ln f ( x i ) + ( 1 − y i ) l n ( 1 − f ( x i ) ) lnL(w) = {\sum\limits_{i = 1}^{n}\left( y_{i}{\ln{f\left( x^{i} \right)}} + \left( {1 - y_{i}} \right)ln\left( 1 - f\left( x^{i} \right) \right) \right.} lnL(w)=i=1∑n(yilnf(xi)+(1−yi)ln(1−f(xi))
于是只需要求解使 l n L ( w ) lnL(w) lnL(w)最大,这里可以采用梯度上升法(也可以利用梯度下降法求 − l n L ( w ) - lnL(w) −lnL(w)的最小值,原理相同)。
下面计算梯度。记 z = w T x z = w^{T}x z=wTx,注意到Sigmoid函数的性质:
σ ( z ) = 1 1 + e − z \sigma(z) = \frac{1}{1 + e^{- z}} σ(z)=1+e−z1, σ ( z ) ′ = σ ( z ) ( 1 − σ ( z ) ) {\sigma(z)}^{'} = \sigma(z)\left( 1 - \sigma(z) \right) σ(z)′=σ(z)(1−σ(z))
故
∂ l n σ ( z ) ∂ w i = ∂ l n σ ( z ) ∂ z ∂ z ∂ w i = 1 σ ( z ) d σ ( z ) d z x i = 1 σ ( z ) σ ( z ) ( 1 − σ ( z ) ) x i = ( 1 − σ ( z ) ) x i \frac{\partial ln\sigma(z)}{\partial w_{i}} = \frac{\partial ln\sigma(z)}{\partial z}\frac{\partial z}{\partial w_{i}} = \frac{1}{\sigma(z)}\frac{d\sigma(z)}{dz}x_{i} = \frac{1}{\sigma(z)}\sigma(z)\left( {1 - \sigma(z)} \right)x_{i} = \left( {1 - \sigma(z)} \right)x_{i} ∂wi∂lnσ(z)=∂z∂lnσ(z)∂wi∂z=σ(z)1dzdσ(z)xi=σ(z)1σ(z)(1−σ(z))xi=(1−σ(z))xi
类似可得 ∂ l n ( 1 − σ ( z ) ) ∂ w i = − σ ( z ) x i \frac{\partial ln\left( 1 - \sigma(z) \right)}{\partial w_{i}} = - \sigma(z)x_{i} ∂wi∂ln(1−σ(z))=−σ(z)xi
所以
∂ l n L ( w ) ∂ w j = ∑ i = 1 n ( y i ( 1 − f ( x i ) ) x j i − ( 1 − y i ) f ( x i ) x j i ) = ∑ i = 1 n ( y i − f ( x i ) x j i \frac{\partial lnL(w)}{\partial w_{j}} = {\sum\limits_{i = 1}^{n}\left( y_{i}\left( {1 - f\left( x^{i} \right)} \right)x_{j}^{i} - \left( {1 - y_{i}} \right)f\left( x^{i} \right)x_{j}^{i} \right)} = {\sum\limits_{i = 1}^{n}\left( y_{i} - f\left( x^{i} \right)x_{j}^{i} \right.} ∂wj∂lnL(w)=i=1∑n(yi(1−f(xi))xji−(1−yi)f(xi)xji)=i=1∑n(yi−f(xi)xji
(注:这里的 x j i x_{j}^{i} xji指第i个训练数据的第j个分量)
于是梯度上升算法可表示为 w j ← w j + α ∑ i = 1 n ( y i − f ( x i ) x j i \left. w_{j}\leftarrow w_{j} + \alpha{\sum\limits_{i = 1}^{n}\left( y_{i} - f\left( x^{i} \right)x_{j}^{i} \right.} \right. wj←wj+αi=1∑n(yi−f(xi)xji,其中 α \alpha α为学习率。
具更体地,我们可以把训练数据中的 x x x写成矩阵形式:
x = ( ( x 1 ) , … , ( x k ) ) T = ( x 1 1 ⋯ x n 1 ⋮ ⋱ ⋮ x 1 k ⋯ x n k ) x = \left( \left( x^{1} \right),\ldots,\left( x^{k} \right) \right)^{T} = \begin{pmatrix} x_{1}^{1} & \cdots & x_{n}^{1} \\ \vdots & \ddots & \vdots \\ x_{1}^{k} & \cdots & x_{n}^{k} \end{pmatrix} x=((x1),…,(xk))T= x11⋮x1k⋯⋱⋯xn1⋮xnk
把 ( y i − f ( x i ) \left( y_{i} - f\left( x^{i} \right) \right. (yi−f(xi)记为 e i e_{i} ei, e = ( e 1 , … , e k ) T e = \left( e_{1},\ldots,e_{k} \right)^{T} e=(e1,…,ek)T
于是 ∑ i = 1 n ( y i − f ( x i ) x j i = ∑ i = 1 n e i x j i {\sum\limits_{i = 1}^{n}\left( y_{i} - f\left( x^{i} \right)x_{j}^{i} \right.} = {\sum\limits_{i = 1}^{n}e_{i}}x_{j}^{i} i=1∑n(yi−f(xi)xji=i=1∑neixji
记 w = ( w 1 , … , w n ) w = \left( {w_{1},\ldots,w_{n}} \right) w=(w1,…,wn),则梯度上升每次迭代可表示为 w ← w + α x T e \left. w\leftarrow w + \alpha x^{T}e \right. w←w+αxTe
这便是代码中梯度上升的实现方式。
四、代码实现
Sigmoid()函数:每次处理一个numpy数组
def sigmoid(arr): # sigmoid阶跃函数
m, n = np.shape(arr) #m行1列
res = np.empty((m, 1))
for i in range(m):
inx = arr[i, 0]
if inx>=0: #对sigmoid函数的优化,避免了出现极大的数据溢出
res[i, 0] = (1.0/(1+np.exp(-inx)))
else:
res[i, 0] = (np.exp(inx)/(1+np.exp(inx)))
return res
gradAscent()函数:梯度上升算法实现
# dataMatIn 是一个2维NumPy数组,每列分别代表每个不同的特征,每行则代表每个训练样本。
# classLabels 是类别标签,它是一个 1*300 的向量。为了便于矩阵计算,需要转置
def gradAscent(dataMatIn, classLabels):
dataMatrix = np.mat(dataMatIn) # m*n矩阵,行数m=样本数,列数n=特征数
labelMat = np.mat(classLabels).transpose() # 行数m=样本数,列数=1
m,n = np.shape(dataMatrix)
alpha = 0.1 # alpha代表学习率
maxCycles = 5000 # 迭代次数
weights = np.ones((n, 1)) #n*1矩阵,代表回归系数,初始化为全1
for k in range(maxCycles):
# m*n 的矩阵 * n*1 的单位矩阵 = m*1的矩阵
h = sigmoid(dataMatrix*weights) # 得到m*1矩阵,其中每行代表对每个样本计算sigmoid函数的值
error = (labelMat - h) # 均为m*1矩阵 每行为这个样本的误差
weights = weights + alpha * dataMatrix.transpose() * error # 矩阵乘法,最后得到回归系数
return weights
classifyVector()函数:分类
def classifyVector(inX, weights): # 分类函数,根据回归系数和特征向量来计算 Sigmoid的值
prob = sigmoid(sum(inX * weights))
if prob > 0.5:
return 1.0
else:
return 0.0
colicTest()函数:数据处理,统计,模型训练和测试
# 打开测试集和训练集,并对数据进行格式化处理
def colicTest():
frTrain = open('horseColicTraining.txt')
frTest = open('horseColicTest.txt')
trainingSet = []
trainingLabels = []
# 解析训练数据集中的数据特征和Labels
# trainingSet 中存储训练数据集的特征(前22列),trainingLabels 存储训练数据集的样本对应的分类标签(0或1)
for line in frTrain.readlines():
currLine = ["1"] + line.strip().split('\t')
lineArr = []
for i in range(22):
lineArr.append(float(currLine[i]))
trainingSet.append(lineArr)
trainingLabels.append(float(currLine[22]))
# 使用梯度上升算法 求得在此数据集上的最佳回归系数 trainWeights
trainWeights = gradAscent(np.array(trainingSet), trainingLabels)
errorCount = 0
numTestVec = 0.0
# 读取 测试数据集 进行测试,计算分类错误的样本条数和最终的错误率
for line in frTest.readlines():
numTestVec += 1.0
currLine = ["1"] + line.strip().split('\t')
lineArr = []
for i in range(22):
lineArr.append(float(currLine[i]))
if int(classifyVector(np.array(lineArr), trainWeights)) != int(currLine[22]):
errorCount += 1
errorRate = (float(errorCount) / numTestVec)
print("the error rate of this test is: %f" % errorRate)
return errorRate
五、测试结果
在测试中,选择学习率 α = 0.1 \alpha = 0.1 α=0.1,迭代次数=500,进行训练。
测试结果显示,错误率为25.37%。考虑到训练数据有30%缺失,这个结果可以接受。
附录(完整代码)
import numpy as np
def sigmoid(arr): # sigmoid阶跃函数
m, n = np.shape(arr) #m行1列
res = np.empty((m, 1))
for i in range(m):
inx = arr[i, 0]
if inx>=0: #对sigmoid函数的优化,避免了出现极大的数据溢出
res[i, 0] = (1.0/(1+np.exp(-inx)))
else:
res[i, 0] = (np.exp(inx)/(1+np.exp(inx)))
return res
# dataMatIn 是一个2维NumPy数组,每列分别代表每个不同的特征,每行则代表每个训练样本。
# classLabels 是类别标签,它是一个 1*300 的向量。为了便于矩阵计算,需要转置
def gradAscent(dataMatIn, classLabels):
dataMatrix = np.mat(dataMatIn) # m*n矩阵,行数m=样本数,列数n=特征数+1(22)
labelMat = np.mat(classLabels).transpose() # 行数m=样本数,列数=1
m,n = np.shape(dataMatrix)
alpha = 0.1 # alpha代表学习率
maxCycles = 5000 # 迭代次数
weights = np.ones((n, 1)) #n*1矩阵,代表回归系数,初始化为全1
for k in range(maxCycles):
# m*n 的矩阵 * n*1 的单位矩阵 = m*1的矩阵
h = sigmoid(dataMatrix*weights) # 得到m*1矩阵,其中每行代表对每个样本计算sigmoid函数的值
# labelMat是实际值
error = (labelMat - h) # 均为m*1矩阵 每行为这个样本的误差
weights = weights + alpha * dataMatrix.transpose() * error # 矩阵乘法,最后得到回归系数
return weights
def classifyVector(inX, weights): # 分类函数,根据回归系数和特征向量来计算 Sigmoid的值
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 = []
# 解析训练数据集中的数据特征和Labels
# trainingSet 中存储训练数据集的特征(前22列),trainingLabels 存储训练数据集的样本对应的分类标签(0或1)
for line in frTrain.readlines():
currLine = ["1"] + line.strip().split('\t')
lineArr = []
for i in range(22):
lineArr.append(float(currLine[i]))
trainingSet.append(lineArr)
trainingLabels.append(float(currLine[22]))
# 使用梯度上升算法 求得在此数据集上的最佳回归系数 trainWeights
trainWeights = gradAscent(np.array(trainingSet), trainingLabels)
errorCount = 0
numTestVec = 0.0
# 读取 测试数据集 进行测试,计算分类错误的样本条数和最终的错误率
for line in frTest.readlines():
numTestVec += 1.0
currLine = ["1"] + line.strip().split('\t')
lineArr = []
for i in range(22):
lineArr.append(float(currLine[i]))
if int(classifyVector(np.array(lineArr), trainWeights)) != int(currLine[22]):
errorCount += 1
errorRate = (float(errorCount) / numTestVec)
print("the error rate of this test is: %f" % errorRate)
return errorRate
# 调用 colicTest() 1次并求结果的平均值(确定性算法无需重复)
def multiTest():
numTests = 1
errorSum = 0.0
for k in range(numTests):
errorSum += colicTest()
print("after %d iterations the average error rate is: %f" % (numTests, errorSum / float(numTests)))
multiTest()
均值(确定性算法无需重复)
def multiTest():
numTests = 1
errorSum = 0.0
for k in range(numTests):
errorSum += colicTest()
print("after %d iterations the average error rate is: %f" % (numTests, errorSum / float(numTests)))
multiTest()