参考:
机器学习实战
Logistic回归基础篇之梯度上升算法
建模算法系列二十五:Logistic回归——多分类
logistic回归虽然名为回归,但实际用于分类问题。
本文将会介绍logistic回归、梯度上升算法以及logistic回归的二分类及多分类问题。
接收输入后能够输出类别。
在二分类中,输出的为0或1。在多分类中,输出的类别就为1,…,n。(多分类可以看作是多个二分类的结合,后文中会介绍)
简要流程如下:
1.回归;
2.求和;
3.Sigmoid。
具体流程为:首先对输入的数据进行回归(即在输入数据的每个特征上都乘一个回归系数),然后将结果相加,将每个输入数据由一个向量变为一个标量,最后将这个标量输入到Sigmoid函数当中,得到一个0~1之间的值。如果得到的值<0.5则视为0类,否则为1类。
σ ( z ) = 1 1 + e − z \sigma(z)=\frac{1}{1+e^{-z}} σ(z)=1+e−z1
Sigmoid函数可以实现将输入数据变为0~1之间的效果。当x=0时,sigmoid函数值为0.5;当x不断增大,sigmoid函数值接近1;当x不断减小,sigmoid函数值接近0。
优点:计算代价不高,易于理解和实现。
缺点:容易欠拟合,分类精度可能不高。
为了使分类器能够得到更好的分类效果,我们需要使用最优化方法来找到最佳回归参数w。 下文介绍梯度上升算法。
梯度上升法基于的思想是:要找到某函数的最大值,最好的方法是沿着该函数的梯度方向探寻。
函数f(x,y)的梯度由下式表示:
∇ f ( x , y ) = ( ∂ f ( x , y ) ∂ x ∂ f ( x , y ) ∂ y ) \nabla{f(x,y)}=\left(\begin{matrix}\frac{\partial{f(x,y)}}{\partial{x}}\\{\frac{\partial{f(x,y)}}{\partial{y}}}\end{matrix}\right) ∇f(x,y)=(∂x∂f(x,y)∂y∂f(x,y))
这个梯度意味着要沿着x的方向移动 ∂ f ( x , y ) ∂ x \frac{\partial{f(x,y)}}{\partial{x}} ∂x∂f(x,y),沿着y的方向移动 ∂ f ( x , y ) ∂ y \frac{\partial{f(x,y)}}{\partial{y}} ∂y∂f(x,y)。
梯度上升算法到达每个点后都会重新估计移动的方向。从P0开始,计算完该点的梯度,函数就根据梯度移动到下一点P1。在P1点,梯度再次被重新计算,并沿新的梯度方向移动到P2。如此循环迭代,直到满足停止条件。迭代的过程中,梯度算子总是保证我们能选取到最佳的移动方向。
移动方向:梯度算子总是指向函数值增长最快的方向。
移动量:又称为步长,记作 α \alpha α。
迭代公式:
w : = w + α ∇ w f ( w ) w:=w+\alpha\nabla_wf(w) w:=w+α∇wf(w)
伪代码:
每个回归系数初始化为1
重复R次:
计算整个数据集的梯度
使用alpha×gradient更新回归系数的向量
返回回归系数
区别:
梯度上升算法用来求函数的最大值,而梯度下降算法用来求函数的最小值。
为什么logistic算法中使用的是梯度上升算法而不是梯度下降算法?
由于logistic算法中使用的损失函数是计算样本分类正确的概率,分类正确的概率越大越好,因而该损失函数的值也是越大越好。所以在logistic算法中我们使用梯度上升法来最大化该损失函数。
梯度下降算法的迭代公式与梯度上升算法的迭代公式差不多,只是公式中的加号变为了减号。
迭代公式:
w : = w − α ∇ w f ( w ) w:=w-\alpha\nabla_wf(w) w:=w−α∇wf(w)
伪代码与梯度上升算法相同。
问题:
梯度上升算法在每次更新回归系数时都需要遍历整个数据集,当数据集很大时复杂度也很高。随机梯度上升法就是针对梯度上升算法这一问题进行的改进。
改进点:
一次仅用一个样本点来更新回归系数。
伪代码:
每个回归系数初始化为1
对数据集中每个样本:
计算该样本的梯度
使用alpha×gradient更新回归系数的向量
返回回归系数
使用logistic算法进行二分类很简单,经过Sigmoid函数之后数据的范围就变为0~1之间,将最终结果小于0.5的视为0、1分类中的0类,大于等于0.5的视为0、1分类中的1类即可。
一个多分类问题可以分解成多个二分类的问题。我们可以将n分类问题分解为n(n-1)/2个二分类问题。
如下图,将原本的3分类问题分解为3个二分类问题。
1. 我们先将原始数据分成3个数据集合,对每两两数据集合都产生一个单独的分类器,该分类器用于鉴别输入的数据样本是属于这两个数据集合中的哪一个数据集合。
2. 在训练时,将1数据集合的数据分别输入到1、2分类器和1、3分类器中通过梯度上升算法求最佳回归系数;同理,将2数据集合的数据分别输入到1、2分类器以及2、3分类器;将3数据集合的数据分别输入到1、3分类器以及2、3分类器。
3. 训练完成后每个分类器中的参数都被调整为最佳参数,能够比较精确的分辨输入数据是其中的哪一个类别。
4. 在测试时,逐一将样本输入到每一个分类器当中,每个分类器会给出自己的预测分类结果。根据结果转化成的实际类别个数,选取预测分类个数最多的类别为本次分类的预测类别。例如上图中的情况1,1、2分类器输出结果为0,对应真实类别为1,1、3分类器输出结果为0,对应真实类别为1,2、3分类器输出结果为0,对应真实类别为2。分类结果为1、1、2,选取个数最多的类别也就是1类别为本次的预测分类结果。
伪代码:
每个分类器每个回归系数初始化为1
训练:
对每个分类器:
寻找与该分类器的0、1类别对应的样本
使用随机梯度上升算法得到该分类器的最佳回归系数
测试:
对测试集中每个样本:
输入每个分类器,得到分类结果
统计分类结果,得出最终预测分类
数据集以集美大学为背景,数据集中的前四列代表从宿舍至该楼的时间,单位为分钟,最后一列为对应的交通方式,共有21个数据以csv文件方式存储,其中前14个作为训练集,后7个作为测试集。
禹州楼 | 建发楼 | 美玲楼 | 陆大楼 | 交通方式 |
---|---|---|---|---|
4 | 3.5 | 3.5 | 5.5 | 电动车 |
8 | 7 | 6.8 | 11 | 步行 |
5 | 4 | 4 | 6 | 自行车 |
5.5 | 4.5 | 4.5 | 7 | 自行车 |
3 | 2.5 | 2.5 | 4 | 自行车 |
7 | 6 | 6 | 11 | 步行 |
5.2 | 4.7 | 4.6 | 6.2 | 自行车 |
4 | 3.8 | 3.8 | 5 | 电动车 |
8 | 7 | 7 | 12 | 步行 |
6 | 5.5 | 5.2 | 9 | 步行 |
5 | 4.3 | 4.2 | 6.3 | 电动车 |
7 | 6 | 6 | 12 | 步行 |
3.5 | 3.2 | 3.1 | 5 | 自行车 |
4.5 | 4.1 | 4.1 | 5.5 | 电动车 |
4.2 | 3.9 | 3.9 | 5.6 | 电动车 |
4.1 | 3.7 | 3.7 | 5.2 | 自行车 |
7.2 | 6.4 | 6.2 | 10.1 | 步行 |
6.7 | 6.1 | 5.9 | 9.8 | 步行 |
9 | 8 | 8 | 13 | 步行 |
4 | 3.7 | 3.8 | 5.8 | 自行车 |
3.5 | 3.2 | 3.1 | 4.8 | 电动车 |
本文中实现了使用logistic回归进行二分类以及多分类的代码。
1 定义类别字典
由于本文中数据集的类别是多个类别。通过定义类别字典的方式来实现函数内部自动识别进行多分类还是二分类。这里的多分类就是识别出类别是步行、电动车还是自行车。这里的二分类就是将原本的三个分类划分为两个,例如自行车与非自行车。
'''
以下有四个类别字典,第一个为多分类的字典,后面三个为二分类的字典
'''
####################################################
# 进行多分类时的类字典
classDict = {'自行车': 0, '电动车': 1, '步行': 2}
# 进行二分类时的类字典
# 将类别转化为0(非电动车),1(电动车)
# 进行是否是电动车的二分类
#classDict = {'自行车': 0, '电动车': 1, '步行': 0}
# 进行是否是自行车的二分类
#classDict = {'自行车': 1, '电动车': 0, '步行': 0}
# 进行是否是步行的二分类
#classDict = {'自行车': 0, '电动车': 0, '步行': 1}
####################################################
2 对调字典的key和value
将上面的类别字典key和value对调,方便分类器通过类别的标签找到类别名称。
# 将类别字典的key和value对调
def getIndexDict(classDict):
indexDict = {}
for key, value in classDict.items():
indexDict[value] = key
return indexDict
2 Sigmoid函数
将输入数据变化到0~1之间。
# sigmoid函数
def sigmoid(x):
try:
return 1.0/(1+np.exp(-x))
except:
return 0.0 #溢出时返回0.0
3 随机梯度上升算法
不断迭代,调整回归参数,最终返回最佳参数。
# 随机梯度上升算法
def stocGradAscent(xMat, classLabels, numIter=150):
'''
Parameters:
xMat: 输入样本矩阵
classLabels:输入样本类别标签
numIter:随机梯度上升算法迭代次数
'''
m,n = np.shape(xMat) # m:样本个数,n:特征数+1(类别数)
w = np.ones(n) #初始化一个全1矩阵
for j in range(numIter): #迭代
dataIdx = list(range(m)) # 生成样本索引列表
for i in range(m): #随机遍历一遍样本集
alpha = 4/(1.0+j+i)+0.01 #步长,不断变化
randIdx = int(np.random.uniform(0,len(dataIdx))) #随机生成一个样本索引
#取出对应样本与w做线性运算,求和后通过sigmoid函数生成0-1之间的一个数字
h = sigmoid(sum(xMat[dataIdx[randIdx]]*w))
err = classLabels[dataIdx[randIdx]] - h #计算真实类别(0或1)与预测出数字的差异
w = w + alpha*err*xMat[dataIdx[randIdx]] #更新权重
del(dataIdx[randIdx]) #删除已访问过的索引
return w
4 测试集数据分类
# 根据权重w对样本x做分类
def classifyVector(x, w):
prob = sigmoid(sum(x*w)) #线性模型后经过sigmoid函数值为0-1之间,视为概率
if prob>0.5:return 1.0 #概率>0.5时为正类,否则为负类
else:return 0.0
5 计数器
输出类别列表中个数最多的类别。
# 统计每个类别的个数,返回出现次数多的类别
def majorityCnt(classList):
# 类别计数器
classCount={}
for c in classList:
if c not in classCount.keys():
classCount[c] = 0
classCount[c] += 1
# reverse = True 从大到小排列,key x[1]指比较key、value中的value
sortedClassCount = sorted(classCount.items(),key=lambda x:x[1],reverse=True)
return sortedClassCount[0][0]
6 二分类及多分类
通过类别字典的设置,自动识别是二分类还是多分类。在本数据集上如果进行二分类,例如进行自行车和非自行车的分类,首先将类别字典设置为**{自行车:1,电动车:0,步行:0},在该函数内会自动将步行与电动车类别转换为非自行车类别**。函数最终返回错误率。
# 进行多分类或二分类测试
def test(fileName = '', numIter=150, trainRatio = 0.8):
'''
Parameters:
fileName:数据集所在文件
numIter:迭代次数
trainRatio:训练集占数据集比例
Returns:
errRate:错误率
'''
allData = open(fileName) #打开文件
allSet = [] #记录所有数据
allLab = [] #记录所有数据的类别
lines = allData.readlines() #获取文件中所有内容
head = lines[0].strip().split(',') #得到第一行
data = lines[1:] #所有数据
numFeatures = len(head) - 1 #特征个数,减去的是类别那一列
for line in data: #遍历每个数据
curLine = line.strip().split(',')
lineArr = [] #记录处理后的每行数据
for i in range(numFeatures): # 遍历每个样本的四个特征
lineArr.append(0 if curLine[i] == '?' else float(curLine[i])) #缺失数据补为0
allSet.append(lineArr) #将处理后的样本加入样本全集中
label = curLine[numFeatures] #当前样本类别
allLab.append(classDict[label])# 加入标签,numFeatures为类别下标
numExamples = len(data) #样本总数
numTrain = int(numExamples * trainRatio) #根据训练集的比例得出训练集总数
numTest = numExamples - numTrain #测试集总数
trainSet = np.array(allSet[:numTrain]) # 共21个样本,前14个用作训练,后7个用于预测
trainLab = np.array(allLab[:numTrain])
testSet = np.array(allSet[numTrain:])
testLab = np.array(allLab[numTrain:])
labels = set(trainLab) #去重样本标签集合
trainW = [] #记录每个分类器的权重
labelPairs = [] #记录每个分类器对应的0、1原类别
for label1 in labels: #对于二分类,由于labels中只有两个元素,只有一个分类器
for label2 in labels: #对于多分类,labels中有多个元素,两两元素之间有一个分类器
if label1 >= label2: #只允许label1 < label2
continue
labelPairs.append([label1, label2]) #记录当前的两个类别
curTrainLabels = [] #记录标签全集中是当前两个类别的子集标签
curTrainSet = [] #记录样本全集中是当前两个类别的子集样本
#遍历每个训练样本,如果属于当前选择的两个类别
#则将该样本与该样本的标签记录下来
for i in range(numTrain):
if trainLab[i] == label1 or trainLab[i] == label2:
if trainLab[i] == label1: #属于二分类中的'0'类别
curTrainLabels.append(0)
if trainLab[i] == label2: #属于二分类中的'1'类别
curTrainLabels.append(1)
curTrainSet.append(trainSet[i])
# 对当前分类器通过梯度上升求回归系数
curTrainW = stocGradAscent(curTrainSet, curTrainLabels, numIter)
trainW.append(curTrainW) #记录当前分类器的回归系数
# ----------------------------- 预测样本集 ------------------------------
predLab = [] #记录预测结果
errCount = 0.0 #记录预测错误个数
numClassifiers = len(labelPairs) #分类器的数目
indexDict = getIndexDict(classDict) #将类别字典的key与value对调
#当执行二分类且类别字典中的类别大于2种时,定义负类的名字就为正类的名字前加个“非”字
if numClassifiers == 1 and len(classDict.items()) > 2:
indexDict[0] = '非'+indexDict[1]
for i in range(numTest): #依次计算每个预测样本
curPreLab = [] #当前预测类别
for j in range(numClassifiers): #遍历每个分类器对预测样本进行预测
curPartPreLab = classifyVector(testSet[i], trainW[j]) #预测类别
curPartPreLab = labelPairs[j][int(curPartPreLab)] #将预测的0、1类别还原为原类别
curPreLab.append(curPartPreLab) #记录当前分类器预测类别
curPreLab = majorityCnt(curPreLab) #统计每个分类器的预测结果,找出预测次数最大的类别
predLab.append(curPreLab) #记录当前样本预测类别
print("分类预测类别为:%s, 真实类别为:%s"%(indexDict[curPreLab], indexDict[testLab[i]]))
if curPreLab != testLab[i]: #如果预测类别!=实际类别,错误量+1
errCount += 1.0
errRate = float(errCount) / numTest #错误率
print('错误率为:%f, 总测试集样本数为:%d,预测错误数为:%d' % (errRate, numTest, errCount))
return errRate
7 完整代码
import numpy as np
'''
以下有四个类别字典,第一个为多分类的字典,后面三个为二分类的字典
'''
####################################################
# 进行多分类时的类字典
classDict = {'自行车': 0, '电动车': 1, '步行': 2}
# 进行二分类时的类字典
# 将类别转化为0(非电动车),1(电动车)
# 进行是否是电动车的二分类
#classDict = {'自行车': 0, '电动车': 1, '步行': 0}
# 进行是否是自行车的二分类
#classDict = {'自行车': 1, '电动车': 0, '步行': 0}
# 进行是否是步行的二分类
#classDict = {'自行车': 0, '电动车': 0, '步行': 1}
####################################################
# 将类别字典的key和value对调
def getIndexDict(classDict):
indexDict = {}
for key, value in classDict.items():
indexDict[value] = key
return indexDict
# sigmoid函数
def sigmoid(x):
try:
return 1.0/(1+np.exp(-x))
except:
return 0.0 #溢出时返回0.0
# 随机梯度上升算法
def stocGradAscent(xMat, classLabels, numIter=150):
'''
Parameters:
xMat: 输入样本矩阵
classLabels:输入样本类别标签
numIter:随机梯度上升算法迭代次数
'''
m,n = np.shape(xMat) # m:样本个数,n:特征数+1(类别数)
w = np.ones(n) #初始化一个全1矩阵
for j in range(numIter): #迭代
dataIdx = list(range(m)) # 生成样本索引列表
for i in range(m): #随机遍历一遍样本集
alpha = 4/(1.0+j+i)+0.01 #步长,不断变化
randIdx = int(np.random.uniform(0,len(dataIdx))) #随机生成一个样本索引
#取出对应样本与w做线性运算,求和后通过sigmoid函数生成0-1之间的一个数字
h = sigmoid(sum(xMat[dataIdx[randIdx]]*w))
err = classLabels[dataIdx[randIdx]] - h #计算真实类别(0或1)与预测出数字的差异
w = w + alpha*err*xMat[dataIdx[randIdx]] #更新权重
del(dataIdx[randIdx]) #删除已访问过的索引
return w
# 根据权重w对样本x做分类
def classifyVector(x, w):
prob = sigmoid(sum(x*w)) #线性模型后经过sigmoid函数值为0-1之间,视为概率
if prob>0.5:return 1.0 #概率>0.5时为正类,否则为负类
else:return 0.0
# 统计每个类别的个数,返回出现次数多的类别
def majorityCnt(classList):
# 类别计数器
classCount={}
for c in classList:
if c not in classCount.keys():
classCount[c] = 0
classCount[c] += 1
# reverse = True 从大到小排列,key x[1]指比较key、value中的value
sortedClassCount = sorted(classCount.items(),key=lambda x:x[1],reverse=True)
return sortedClassCount[0][0]
# 进行多分类或二分类测试
def test(fileName = '', numIter=150, trainRatio = 0.8):
'''
Parameters:
fileName:数据集所在文件
numIter:迭代次数
trainRatio:训练集占数据集比例
Returns:
errRate:错误率
'''
allData = open(fileName) #打开文件
allSet = [] #记录所有数据
allLab = [] #记录所有数据的类别
lines = allData.readlines() #获取文件中所有内容
head = lines[0].strip().split(',') #得到第一行
data = lines[1:] #所有数据
numFeatures = len(head) - 1 #特征个数,减去的是类别那一列
for line in data: #遍历每个数据
curLine = line.strip().split(',')
lineArr = [] #记录处理后的每行数据
for i in range(numFeatures): # 遍历每个样本的四个特征
lineArr.append(0 if curLine[i] == '?' else float(curLine[i])) #缺失数据补为0
allSet.append(lineArr) #将处理后的样本加入样本全集中
label = curLine[numFeatures] #当前样本类别
allLab.append(classDict[label])# 加入标签,numFeatures为类别下标
numExamples = len(data) #样本总数
numTrain = int(numExamples * trainRatio) #根据训练集的比例得出训练集总数
numTest = numExamples - numTrain #测试集总数
trainSet = np.array(allSet[:numTrain]) # 共21个样本,前14个用作训练,后7个用于预测
trainLab = np.array(allLab[:numTrain])
testSet = np.array(allSet[numTrain:])
testLab = np.array(allLab[numTrain:])
labels = set(trainLab) #去重样本标签集合
trainW = [] #记录每个分类器的权重
labelPairs = [] #记录每个分类器对应的0、1原类别
for label1 in labels: #对于二分类,由于labels中只有两个元素,只有一个分类器
for label2 in labels: #对于多分类,labels中有多个元素,两两元素之间有一个分类器
if label1 >= label2: #只允许label1 < label2
continue
labelPairs.append([label1, label2]) #记录当前的两个类别
curTrainLabels = [] #记录标签全集中是当前两个类别的子集标签
curTrainSet = [] #记录样本全集中是当前两个类别的子集样本
#遍历每个训练样本,如果属于当前选择的两个类别
#则将该样本与该样本的标签记录下来
for i in range(numTrain):
if trainLab[i] == label1 or trainLab[i] == label2:
if trainLab[i] == label1: #属于二分类中的'0'类别
curTrainLabels.append(0)
if trainLab[i] == label2: #属于二分类中的'1'类别
curTrainLabels.append(1)
curTrainSet.append(trainSet[i])
# 对当前分类器通过梯度上升求回归系数
curTrainW = stocGradAscent(curTrainSet, curTrainLabels, numIter)
trainW.append(curTrainW) #记录当前分类器的回归系数
# ----------------------------- 预测样本集 ------------------------------
predLab = [] #记录预测结果
errCount = 0.0 #记录预测错误个数
numClassifiers = len(labelPairs) #分类器的数目
indexDict = getIndexDict(classDict) #将类别字典的key与value对调
#当执行二分类且类别字典中的类别大于2种时,定义负类的名字就为正类的名字前加个“非”字
if numClassifiers == 1 and len(classDict.items()) > 2:
indexDict[0] = '非'+indexDict[1]
for i in range(numTest): #依次计算每个预测样本
curPreLab = [] #当前预测类别
for j in range(numClassifiers): #遍历每个分类器对预测样本进行预测
curPartPreLab = classifyVector(testSet[i], trainW[j]) #预测类别
curPartPreLab = labelPairs[j][int(curPartPreLab)] #将预测的0、1类别还原为原类别
curPreLab.append(curPartPreLab) #记录当前分类器预测类别
curPreLab = majorityCnt(curPreLab) #统计每个分类器的预测结果,找出预测次数最大的类别
predLab.append(curPreLab) #记录当前样本预测类别
print("分类预测类别为:%s, 真实类别为:%s"%(indexDict[curPreLab], indexDict[testLab[i]]))
if curPreLab != testLab[i]: #如果预测类别!=实际类别,错误量+1
errCount += 1.0
errRate = float(errCount) / numTest #错误率
print('错误率为:%f, 总测试集样本数为:%d,预测错误数为:%d' % (errRate, numTest, errCount))
return errRate
if __name__ == '__main__':
minErrRate= 100 #记录最小错误率
bestNumIter = 0 #记录最佳迭代次数
for numIter in range(50, 200, 10): #试验不同迭代次数对算法的影响
curErrRate = test(fileName = 'time.csv', numIter = numIter, trainRatio = 0.67)
if curErrRate < minErrRate : #记录最小错误率以及最佳迭代次数
minErrRate = curErrRate
bestNumIter = numIter
print('最佳训练时迭代次数为%d次, 最小错误率为%f'%(bestNumIter, minErrRate))
结果分析:
上面展示的运行结果为不同分类在迭代次数从50间隔10一直到190之间的最后一次预测结果以及最佳的错误率和预测结果。可以看出该分类器进行二分类以及多分类的效果都还可以。
不足:
可以在文章中增加成本函数以及对应梯度的推导。
链接:https://pan.baidu.com/s/1eFfBmHFo48UD57uSB4gP6g?pwd=ajr2
提取码:ajr2