1.算法概述
首先要理解最优化的含义,求不同地点间的最短距离、最少工作量得到最大效率,这种就是最优化,而Logistic回归就是一个最优化算法,它对数据进行分类,其主要思想是:根据现有的数据对分类边界线建立回归公式,以此进行分类。其中回归一词是指用一条线对数据点进行拟合的过程,通过最佳拟合达到“最优化”
下面举一个Logistic回归的一个二分类例子:
假设我们有一组包含以下特征的数据集:
- 邮件长度
- 是否包含特定关键词
- 是否包含多个感叹号
- 来自陌生发件人
- 邮件是否包含大量大写字母
并且有标签垃圾邮件为1、非垃圾邮件为0
我们将这些数据输入逻辑回归模型,模型将学习如何根据这些特征来预测电子邮件是否为垃圾邮件。逻辑回归模型的输出是一个概率值,表示电子邮件为垃圾邮件的概率
例如,经过训练后的模型可能会给出如下预测:
特征:
- 邮件长度:200字
- 是否包含特定关键词:是
- 是否包含多个感叹号:是
- 来自陌生发件人:是
- 邮件是否包含大量大写字母:否
预测概率:0.95
这意味着模型预测该电子邮件有95%的概率是垃圾邮件。根据实际情况和需求,我们可以设置一个阈值,如0.5。如果预测概率大于等于0.5,我们将电子邮件分类为垃圾邮件;否则,我们将其分类为非垃圾邮件
2.Logistic回归和Sigmoid函数
之前那种非1即0的分类性质的函数称为**海维塞德阶跃函数(单位阶跃函数)**如下图
这种函数在瞬跳的过程很难处理,因此使用数学上更易处理的 Sigmoid函数,Sigmoid函数的计算公式为
σ
(
z
)
=
1
1
+
e
−
z
\sigma(z)=\frac{1}{1+e^{-z}}
σ(z)=1+e−z1它的图像如下
当Sigmoid函数的x轴无限大时,它的图像就可以近似看成是阶跃函数
而Logistic回归分类器的实现是根据每个特征乘以一个回归系数,再把结果值相加,把总和带入Sigmoid函数中得到0~1范围内的数值,根据是否大于0.5分入1和0两类,因此,Logistic回归也可看成一个概率估计
3.回归系数确定
记Sigmoid函数输入为 z 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 x x是分类器的输入数据, w w w是找到的最佳系数,在已经知道函数形式后,现在要确定最佳回归系数,可以通过梯度上升法得到
梯度上升法
梯度上升法的思想是:沿函数梯度方向探寻找到函数的最大值。记梯度为 ∇ \nabla ∇,函数 f ( x , y ) f(x,y) f(x,y)的梯度表示为 ∇ f ( x , y ) = ( θ f ( x , y ) θ x θ f ( x , y ) θ y ) \nabla f(x,y)=\begin{pmatrix} \frac{\theta f(x,y)}{\theta x}\\ \frac{\theta f(x,y)}{\theta y} \end{pmatrix} ∇f(x,y)=(θxθf(x,y)θyθf(x,y))这个梯度意味着沿 x x x方向移动 θ f ( x , y ) θ x \frac{\theta f(x,y)}{\theta x} θxθf(x,y),沿 y y y方向移动 θ f ( x , y ) θ y \frac{\theta f(x,y)}{\theta y} θyθf(x,y),其中 f ( x , y ) f(x,y) f(x,y)在计算点上有定义并可微,具体函数如下
梯度算子总是沿指向函数值增长最快的方向,而移动的距离称为步长记做
α
\alpha
α,用向量表示的梯度上升迭代公式为
w
:
=
w
+
α
∇
w
f
(
w
)
w:=w+\alpha\nabla_wf(w)
w:=w+α∇wf(w)迭代到某个指定值或者达到停止条件停止,梯度下降则相反,其公式为
w
:
=
w
−
α
∇
w
f
(
w
)
w:=w-\alpha\nabla_wf(w)
w:=w−α∇wf(w)
接下来利用以上的数据集进行用梯度上升法进行最佳参数的寻找最佳参数,其中数据集包括两个特征X1和X2,然后用下列代码进行实现
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):
# 计算 sigmoid 函数值
return 1.0 / (1 + np.exp(-inX))
# 梯度上升算法
def gradAscent(dataMatIn, classLabels):
dataMatrix = np.mat(dataMatIn) # 将数据转换为 NumPy 矩阵
labelMat = np.mat(classLabels).transpose() # 将类别标签转换为 NumPy 矩阵
m, n = np.shape(dataMatrix) # 获取数据矩阵的行数和列数
alpha = 0.001 # 学习率,即步长
maxCycles = 500 # 最大迭代次数
weights = np.ones((n, 1)) # 初始化权重向量,所有元素都为 1
for k in range(maxCycles): # 进行多次矩阵运算
h = sigmoid(dataMatrix*weights) # 计算 h 值
error = (labelMat - h) # 计算误差
weights = weights + alpha * dataMatrix.transpose()* error # 更新权重
return weights # 返回更新后的权重向量
这段代码中,loadDataSet
函数用于从 testSet.txt
文件中读取数据,并转换为数据矩阵 dataMat
和标签矩阵 labelMat
。sigmoid
函数用于计算 sigmoid 函数值。gradAscent
函数采用梯度上升法进行分类器的权重更新。函数首先将输入数据矩阵转换为 NumPy 矩阵,并计算权重和误差。接着,通过迭代更新权重,使误差不断减小,最终得到最优的权重向量,也就是回归系数
可以看到经过函数处理,得到了一组回归系数,它确定了不同类别数据间的分隔线,接下来是画出不同类别数据点间的分隔线,用以下代码来实现
# 绘制最佳拟合线
def plotBestFit(weights):
import matplotlib.pyplot as plt
dataMat, labelMat = loadDataSet() # 加载数据集
dataArr = np.array(dataMat) # 将数据转换为 NumPy 数组
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]) # 将坐标添加到 xcord1 和 ycord1
else: # 如果标签为 0
xcord2.append(dataArr[i, 1]); ycord2.append(dataArr[i, 2]) # 将坐标添加到 xcord2 和 ycord2
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] # 计算 Y 轴的坐标值
ax.plot(x, y) # 绘制拟合线
plt.xlabel('X1') # 设置 X 轴标签
plt.ylabel('X2') # 设置 Y 轴标签
plt.show() # 显示图形
通过plotBestFit
函数得到一条分割线结果如下
从上图能看到这条分隔线错分了两个点,算是较好的一个效果
尽管上述的分割情况不错,但是只是针对一百个数据点,由梯度上升算法知道每次更新回归系数时都要遍历整个数据集,这对处理很多数据的数据集来说计算复杂度过高,因此尝试通过用一个样本点来更新回归系数,该方法称作随机梯度上升算法其实现代码如下
#遍历所有数据点
def stocGradAscent0(dataMatrix, classLabels):
m, n = np.shape(dataMatrix)
alpha = 0.01
weights = np.ones(n) #初始化为全1
#遍历所有数据点
for i in range(m):
#计算预测值
h = sigmoid(sum(dataMatrix[i]*weights))
#计算误差
error = classLabels[i] - h
#更新权重
weights = weights + alpha * error * dataMatrix[i]
return weights
可以看到通过一个样本点的拟合直线效果还行,但是对比之前函数效果的来说差了一点,因此尝试修改步长和随机选取样本来更新回归系数,修改后的代码如下
def stocGradAscent1(dataMatrix, classLabels, numIter=150):
m, n = np.shape(dataMatrix)
weights = np.ones(n) #初始化为全1
#设置迭代次数
for j in range(numIter):
#打乱数据索引
dataIndex = list(range(m))
#遍历所有数据点
for i in range(m):
#动态调整学习率
alpha = 4/(1.0+j+i) + 0.0002 #alpha随着迭代次数逐渐减小
#随机选择数据点
randIndex = int(np.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
其中步长中的+ 0.0002
是为了防止减小到0导致多次迭代的意义性,同时增加了迭代次数numIter
作为参数输入,以下是上述代码的结果,左边是迭代了160次,右边迭代了200次,分隔线右边比左边上升了一点,且把经过数据点的线条挪到了空白处
4.示例
算法流程
- 收集数据:采用任意方法收集数据
- 准备数据:由于需要进行距离计算,因此要求數据类型为数值型。另外,结构化数据
格式则最佳 - 分析数据:采用任意方法对数据进行分析
- 训练算法:大部分时问将用于训练,训练的目的是为了找到最佳的分类回归系数
- 测试算法:一旦训练步骤完成,分类将会很快
- 使用算法:首先,我们需要输人一些数据,开将其转换成对应的结构化数值;接着,基于训练好的回归系数就可以对这些数值进行简单的回归计算,判定它们属于哪个类别;在这之后,我们就可以在输出的类别上做一些其他分析工作
准备数据
以从疝气病症预测病马的死亡率作为示例,在数据集中有30%的数据缺失或者有问题时,可以通过以下方法解决:
- 使用可用特征的均值来填补缺失值
- 使用特殊值来填补缺失值,如-1
- 忽略有缺失值的样本
- 使用相似样本的均值添补缺失值
- 使用另外的机器学习算法预测缺失值
这里选择使用第二种方法来填补缺失值
测试算法
在填补后用以下代码实现预测分类
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()
# 计算平均错误率并输出
print("after %d iterations the average error rate is: %f" % (numTests, errorSum / float(numTests)))
classifyVector
函数接收一个特征向量(inX)和一个权重向量(weights)作为输入。它首先计算预测概率,然后根据预测概率判断样本属于哪个类别。如果预测概率大于0.5,则将该样本预测为正类(1.0),否则预测为负类(0.0);colicTest
函数实现了从训练数据集和测试数据集中获取数据,并使用随机梯度上升训练逻辑回归模型。在训练好模型后,函数使用测试数据集对模型进行测试,计算错误率并将其输出。最后,函数返回错误率;multiTest
函数执行numTests
次colicTest()
函数,每次执行后累加错误率。最后,函数计算平均错误率并将其输出。通过多次测试,我们可以得到一个更稳定的模型性能评估结果
在缺少30%的数据值情况下十次迭代达到了36%的错误率,算是一个不错的结果
5.总结
Logistic回归是一种广泛应用于分类问题的简单高效的机器学习算法,其主要思想是根据现有的数据对分类边界线建立回归公式,并以此进行分类,其目的是寻找一个非线性函数Simoid的最佳拟合参数,求解过程可以由最优化算法法完成
在最优化算法中,最常用的就是梯度上升算法,而梯度上升算法叉可以简化为随机梯度上升算法。随机梯度上升算法与梯度上升算法的效果相当,但占用更少的计算资源。此外,随机梯度上升是一个在线算法,它可以在新效据到来时就元成参数更新,而不需要重新读取整个数据集来进行批处理运算