文章目录
第五章 Logistic 回归
引言
利用(logistic)回归进行分类的主要思想是:根据现有数据对分类边界线建立回归公式,以此进行分类。
Logistic 回归优缺点
优点:计算代价不高,易于理解和实现。
缺点:容易欠拟合,分类精度可能不高。 .
适用数据类型:数值型和标称型数据。
Logistic 回归的一般过程
- 收集数据:采用任意方法收集数据。
- 准备数据:由于需要进行距离计算,因此要求数据类型为数值型。另外,结构化数据格式则最佳。
- 分析数据:采用任意方法对数据进行分析。
- 训练算法:大部分时间将用于训练,训练的目的是为了找到最佳的分类回归系数。
- 测试算法:一旦训练步驟完成,分类将会很快。
- 使用算法:首先,我们需要输入一些数据,并将其转换成对应的结构化数值;接着,基于训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别,在这之后,我们就可以在输出的类别上做一些其他分析工作。
5.1 基 于Logistic回归和Sigmoid函数的分类
目标函数的功能: 能接受所有的输人然后预测出类别
在两个类的情况下,输出不是0就是1,类似信号系统中的阶跃函数,
阶跃函数:
ε
(
t
)
=
{
1
,
t
>
0
0
,
t
<
0
\varepsilon(t) = \begin{cases} 1, &t> 0 \\ 0, &t<0 \end{cases}
ε(t)={1,0,t>0t<0
阶跃函数图像:
由于0到1之间会产生一个跳跃,跳跃时不方便处理,所以引进了Sigmoid 函数。
Sigmoid 函数表达式:
σ
(
z
)
=
1
1
+
e
−
z
\sigma(z) = \frac{1}{1+e^{-z}}
σ(z)=1+e−z1
Sigmoid函数在不同坐标尺度下的两条曲线图:
两种坐标尺度下的Sigmoid函数图。上图的横坐标为-5 到5,这时的曲线变化较为平滑;下图横坐标的尺度足够大,可以看到,在X= 0点处Sigmoid函数看起来很像阶跃函数。
5.2 基于最优化方法的最佳回归系数确定
将Sigmoid函数输入记为z且z满足:
z
=
w
0
x
0
+
w
1
x
1
+
w
2
x
2
+
.
.
.
+
w
n
x
n
z= w_0x_0 +w_1x_1 +w_2x_2 +...+w_nx_n
z=w0x0+w1x1+w2x2+...+wnxn
使用向量表达为:
z
=
w
T
X
z = w^TX
z=wTX
X为输入数据,w为需要找到的最佳参数(系数)从而使分类器尽可能的精确。
为了寻找该最佳参数,引入了最优化理论以及一些对应方法。
梯度上升法
梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
函数f(x,y)的梯度表示为:
∇
f
(
x
,
y
)
=
(
∂
f
(
x
,
y
)
∂
x
∂
f
(
x
,
y
)
∂
y
)
\nabla f(x,y) =\begin{pmatrix} \frac{\partial f(x,y)}{\partial x} \\\ \frac{\partial f(x,y)}{\partial y} \end{pmatrix}
∇f(x,y)=(∂x∂f(x,y) ∂y∂f(x,y))
实际就是函数
f
f
f的偏导数。
函数f(x,y)必须要在待计算的点上有定义并且可微。
梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2。如此循环迭代,直到满足停止条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。
梯度算法的迭代公式如下:
w
=
w
+
α
∇
w
f
(
(
w
)
w= w + \alpha \nabla_w f((w)
w=w+α∇wf((w)
α \alpha α称为步长,代表移动量的大小。
该公式将一直被迭代执行,直至达到某个停止条件为止,比如迭代次数达到某个指定值或算
法达到某个可以允许的误差范围。
训练算法:使用梯度上升找到最佳参数
数据集如下图:
梯度上升法的伪代码如下:
每个回归系数初始化为1
重复R次:
计算整个数据集的梯度
使用alpha x gradient更新回归系数的向量
返回回归系数
加载数据
将文件中的数据加载到程序中
#加载数据
def loadDataSet():
dataMat = []
labelMat = []
#打开文件
fr = open('./Ch05/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
dataMat, labelMat = loadDataSet()
print(dataMat,end="\n")
print(labelMat,end="\t")
输出结果如下:
数据在载入时,人为的增加了一个数据1.0,书中讲的是为了方便计算,没想明白去掉哪里会产生影响。(类似于偏置电压的作用吗?)
训练算法
由两部分构成,一部分是Sigmoid函数,一部分就是梯度上升优化算法。
#Sigmoid函数
def sigmoid(inX):
#np.exp()函数是求e^x的值的函数
return 1.0 / (1 + np.exp(-inX))
def gradAscent(dataMatIn, classLabels):
#转换成numpy的mat矩阵
dataMatrix = np.mat(dataMatIn)
#转换成numpy的mat,并进行转置
labelMat = np.mat(classLabels).transpose()
#返回dataMatrix的大小。m为行数,n为列数。
m, n = np.shape(dataMatrix)
#移动步长,也就是学习速率,控制更新的幅度。
alpha = 0.001
#最大迭代次数
maxCycles = 500
weights = np.ones((n,1))
for k in range(maxCycles):
#梯度上升矢量化公式,这里是矩阵相乘,[100*3]*[3*1]weights就是需要找的参数,
h = sigmoid(dataMatrix * weights)
#使用标签值与经过Sigmoid函数转换的值进行比较
error = labelMat - h
#根据误差值对参数进行修改
#alpha是步进值,数据矩阵转置后是[3*100],error是[100*1]
weights = weights + alpha * dataMatrix.transpose() * error
#将矩阵转换为数组,返回权重数组
return weights.getA()
dataMat, labelMat = loadDataSet()
# print(dataMat,end="\n")
# print(labelMat,end="\t")
weights = gradAscent(dataMat, labelMat)
print(weights)
输出参数如下:
[[ 4.12414349]
[ 0.48007329]
[-0.6168482 ]]
关键是下面这一行代码:
#alpha是步进值,数据矩阵转置后是[3*100],error是[100*1]
weights = weights + alpha * dataMatrix.transpose() * error
为什么将原有的数据乘以步进值,乘以误差值,再加上现在的参数就是下一次的参数?
分析数据:画出决策边界
根据之前得到的回归系数,该系数确定了不同类别数据之间的分隔线,画出该分隔线。
实现代码如下:
def plotBestFit(weights):
#加载数据集
dataMat, labelMat = loadDataSet()
#转换成numpy的array数组
dataArr = np.array(dataMat)
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])
else:
xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2])
fig = plt.figure()
#添加subplot
ax = fig.add_subplot(111)
#绘制正样本,使用散点图
ax.scatter(xcord1, ycord1, s = 20, c = 'red', marker = 's',alpha=.5)
ax.scatter(xcord2, ycord2, s = 20, c = 'green',alpha=.5)
#设置x的范围,(-3,3)的范围是根据数据值得到的
x = np.arange(-3.0, 3.0, 0.1)
#我们是参考阶跃函数延伸出Sigmoid函数的,所以是以0为分界线来化分数据的。
#所以Sigmoid函数输入Z = 0,设定0= w0*x0 + w1*x1 + w2*x2 , 然后解出x2和x1的关系式(即 分隔线的方程,注意x0 = 1)。
y = (-weights[0] - weights[1] * x) / weights[2]
ax.plot(x, y)
#绘制title
plt.title('BestFit')
#绘制label
plt.xlabel('X1'); plt.ylabel('X2')
plt.show()
dataMat, labelMat = loadDataSet()
weights = gradAscent(dataMat, labelMat)
plotBestFit(weights)
输出结果如下:
可以看到这条线基本将两类数据划分开了。但是,有少量点交错掺杂,使得分类结果有部分不准确。
这条分界线来源,我们是参考阶跃函数延伸出Sigmoid函数的,所以是以0为分界线来化分数据的。 所以Sigmoid函数输入Z = 0,设定$ 0= w_0x_0 +w_1x_1 +w_2x_2 $ 然后解出x2和x1的关系式(即分隔线的方程,注意x0 = 1)。
y = (-weights[0] - weights[1] * x) / weights[2]
训练算法:随机梯度上升
梯度上升算法在每次更新回归系数时都需要遍历整个数据集, 当样本过大时,该方法的计算复杂度就太高了。
随机梯度上升算法,就是进行的改进,一次仅用一个样本点来更新回归系数。由于可以在新样本到来时对分类器进行增量式更新,因而随机梯度上升算法是一个在线学习算法。
与 “ 在线学习”相对应,一次处理所有数据被称作是“批处理” 。
随机梯度上升算法可以写成如下的伪代码:
所有回归系数初始化为1
=对数据集中每个样本
计算该样本的梯度
使用alpha x gradient^ .新回归系数值
返回回归系数值
实现代码如下:
代码参考了该博客——>机器学习笔记
但是要增加一句
dataMatrix=np.array(dataMatrix)
修改了类型后,后面在计算回归参数时才不会报错。
#随机梯度上升算法
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
#返回dataMatrix的大小。m为行数,n为列数
m,n = np.shape(dataMatrix)
#防止后面计算系数时报类型错误
dataMatrix=np.array(dataMatrix)
#参数初始化
weights = np.ones(n)
for j in range(numIter):
dataIndex = list(range(m))
for i in range(m):
#降低alpha的大小,每次减小。
alpha = 4/(1.0+j+i)+0.01
#随机选取样本
randIndex = int(random.uniform(0,len(dataIndex)))
#选择随机选取的一个样本,计算h
h = sigmoid(sum(dataMatrix[randIndex]*weights))
#计算误差
error = classLabels[randIndex] - h
#更新回归系数
weights = weights + alpha * error * dataMatrix[randIndex]
#删除已经使用的样本
del(dataIndex[randIndex])
return weights
dataMat, labelMat = loadDataSet()
weights = stocGradAscent1(dataMat, labelMat)```
print(weights,end= '\n')
输出结果如下:
[14.60832331 1.22689838 -2.19665258]
利用该输出参数画出拟合曲线如下:
这里直接调用用poltBestFit()函数即可。
plotBestFit(weights)
上图可以看到绿色的部分处于分割线的下方,即为分类错误的部分,较之前的结果,总体来说要差。时间因素,因为数据量不大,所以并不是十分明显。
回归系数与迭代次数的关系
将每次循环的到的回归系数记录下来,并记录对应的次数,将得到的数据打印出来。
代码参考——>机器学习笔记
需要修改代码中的函数参数FontProperties,修改为fontproperties。
实现代码如下:
#绘制回归系数与迭代次数的关系
def plotWeights(weights_array1,weights_array2):
#设置汉字格式
font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)
#将fig画布分隔成1行1列,不共享x轴和y轴,fig画布的大小为(13,8)
#当nrow=3,nclos=2时,代表fig画布被分为六个区域,axs[0][0]表示第一行第一列
fig, axs = plt.subplots(nrows=3, ncols=2,sharex=False, sharey=False, figsize=(20,10))
x1 = np.arange(0, len(weights_array1), 1)
#绘制w0与迭代次数的关系
axs[0][0].plot(x1,weights_array1[:,0])
axs0_title_text = axs[0][0].set_title(u'梯度上升算法:回归系数与迭代次数关系',fontproperties=font)
axs0_ylabel_text = axs[0][0].set_ylabel(u'W0',fontproperties=font)
plt.setp(axs0_title_text, size=20, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=20, weight='bold', color='black')
#绘制w1与迭代次数的关系
axs[1][0].plot(x1,weights_array1[:,1])
axs1_ylabel_text = axs[1][0].set_ylabel(u'W1',fontproperties=font)
plt.setp(axs1_ylabel_text, size=20, weight='bold', color='black')
#绘制w2与迭代次数的关系
axs[2][0].plot(x1,weights_array1[:,2])
axs2_xlabel_text = axs[2][0].set_xlabel(u'迭代次数',fontproperties=font)
axs2_ylabel_text = axs[2][0].set_ylabel(u'W1',fontproperties=font)
plt.setp(axs2_xlabel_text, size=20, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=20, weight='bold', color='black')
x2 = np.arange(0, len(weights_array2), 1)
#绘制w0与迭代次数的关系
axs[0][1].plot(x2,weights_array2[:,0])
axs0_title_text = axs[0][1].set_title(u'改进的随机梯度上升算法:回归系数与迭代次数关系',fontproperties=font)
axs0_ylabel_text = axs[0][1].set_ylabel(u'W0',fontproperties=font)
plt.setp(axs0_title_text, size=20, weight='bold', color='black')
plt.setp(axs0_ylabel_text, size=20, weight='bold', color='black')
#绘制w1与迭代次数的关系
axs[1][1].plot(x2,weights_array2[:,1])
axs1_ylabel_text = axs[1][1].set_ylabel(u'W1',fontproperties=font)
plt.setp(axs1_ylabel_text, size=20, weight='bold', color='black')
#绘制w2与迭代次数的关系
axs[2][1].plot(x2,weights_array2[:,2])
axs2_xlabel_text = axs[2][1].set_xlabel(u'迭代次数',fontproperties=font)
axs2_ylabel_text = axs[2][1].set_ylabel(u'W1',fontproperties=font)
plt.setp(axs2_xlabel_text, size=20, weight='bold', color='black')
plt.setp(axs2_ylabel_text, size=20, weight='bold', color='black')
plt.show()
dataMat, labelMat = loadDataSet()
weights1,weights_array1 = gradAscent(dataMat, labelMat)
weights2,weights_array2 = stocGradAscent1(dataMat, labelMat)
plotWeights(weights_array1,weights_array2)
此外需要在另外两个梯度上升算法中增加记录回归参数的向量,并且要返回该记录值。
如果直接使用append()函数将回归参数存到数组中,则需要使用reshape()函数将数组重新化分维度。
输出结果如下:
梯度上升算法这里的步长alpha为0.001,出来参数走势是上图
修改alpha为0,01得到效果如下图:
明显看到梯度上升算法的回归参数发生明显变化,步长长时毛刺抖动明显比小步长多,而且走势也出现较大差别。三个参数在大步长时已经出现趋于稳定,而小步长时仍处在变换中。没有稳定迹象。
在小步长下,增大梯度上升法的计算次数,直到出现稳定,对比一下区别。
小步长趋于稳定是=时。结果如下
我是将循环次数增大到50000后得到下图
梯度上升算法整个循环周期,回归参数并没有出现大的波动。对比改进的随机梯度算法,参数稳定周期差了几个量级。
对比小步长短循环次数与较多循环次数,回归参数的走势基本相同。
5.3 示例:从疝气病症预测病马的死亡率
准备数据:处理被据中的缺失值
对于特征数据缺失一般采用如下方法:
- 使用可用特征的均值来填补缺失值;
- 使用特殊值来填补缺失值,如-1;
- 忽略有缺失值的样本;
- 使用相似样本的均值添补缺失值;
- 使用另外的机器学习算法预测缺失值。
如果在测试数据集中类别标签已经缺失,那么我们的简单做法是将该条数据丢弃。
使用这个博客中的已经处理好的数据集——>机器学习笔记
测试算法:用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('./Ch05/horseColicTraining.txt')
#打开测试集
frTest = open('./Ch05/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)
colicTest()
classifyVector函数为分类函数,使用已经计算好的回归参数和测试数据,得到Sigmoid返回值,当值大于0.5就返回1,小于0.5就返回0。
这里在调用之前的随机梯度算法函数时,要讲统计的回归参数相关的内容注释掉,并且要删除返回值。
此外使用程序原本的Sigmoid函数得到的准确率非常感人(试集错误率为: 56.72%)修改使用scipy.special import expit 得到的准确率能高20%左右(测试集错误率为: 32.84%)
这里有问题,测试集错误率为: 41.79% 、47.76%、25.37%这个准确率范围波动有点大?这样的测试结果能用吗?
使用书中的Sigmoid函数,得到的测试正确率同样会变化。
调整步长与循环次数后试一下:
出现测试集错误率为: 26.87%。
这个数值是之前在这个条件下测试时没有出现过得数值。但是由于他的册数准确率还是在变化,不稳定。所以不能确定准确率一定是提高了。但是要怎么让输出结果保持稳定呢?
5.4小结
本章引入了logistic回归,目的就是为了找到一个非线性函数SIgmoid的最佳拟合参数,求解过程可以由最优化算法来完成。在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法又可以简化为随机梯度上升算法。此外引入了一些处理缺失数据的方法。
最后,调整随机梯度上升算法的步长与循环周期,能够影响分类结果,但是该分类存在分类结果不稳定的问题。如何调整参数与如何使分类结果稳定都是问题.,是我使用不正确吗?