实验三 分类算法实验
代码已开源:https://github.com/LinXiaoDe/ArtificialIntelligence/tree/master/lab3
在本实验中,我手工实现了所有算法,包括朴素贝叶斯,决策树ID3算法,决策树C4.5算法,决策树CART算法,人工神经网络BP算法,支持向量积SVM的SMO算法,并且对决策树的三个算法决策树结果进行了可视化。统计了所有算法的训练准确率和测试准确率,给出了时间开销。
🔍一.实验目的
- 巩固4种基本的分类算法的算法思想:朴素贝叶斯算法,决策树算法,人工神经网络,支持向量机;
- 能够使用现有的分类器算法代码进行分类操作
- 学习如何调节算法的参数以提高分类性能;
🔍二 实验的硬件、软件平台
-
硬件:计算机
-
操作系统:Linux
-
应用软件:python
🔍三 实验内容及步骤
利用现有的分类器算法对文本数据集进行分类
- 了解文本数据集的情况并阅读算法代码说明文档;
- 编写代码设计朴素贝叶斯算法,决策树算法,人工神经网络,支持向量机等分类算 法,利用文本数据集中的训练数据对算法进行参数学习;
- 利用学习的分类器对测试数据集进行测试;
- 统计测试结果;
🔍四、思考题
- 如何在参数学习或者其他方面提高算法的分类性能?
🔍五、实验报告要求
- 对各种算法的原理进行说明;
- 对实验过程进行描述;
- 统计实验结果,对分类性能进行比较说明;
- 对算法的时间空间复杂度进行比较分析。
一. 数据特征分析
在正式开始我们的实验前,我们有必要对数据集做足够的初步分析,了解他的数据特征,以决定用什么模型来解决这个问题,数据分析这个步骤在机器学习中是十分重要的。
-
了解文本数据集结构:
我们观察助教老师下发的文件,其中有三个数据集文件,
dataset.txt
是数据总集,包含1728条数据,test.txt是训练数据集,包含1350条数据;predict.txt是预测数据集,包含378条数据。如下图:
- 数据特征分析:
这是一个汽车评估数据集,包含1728个数据,其中训练数据1350,测试数据378个。每个数据包含6个属性,所有的数据分为4类:
标签 Class Values:
属性 unacc, acc, good, vgood
特征 buying: vhigh, high, med, low.
特征 maint: vhigh, high, med, low.
特征 doors: 2, 3, 4, 5,more.
特征 persons: 2, 4, more.
特征 lug_boot: small, med, big.
特征 safety: low, med, high.
- 其中Attributes是指它的属性子集。而其属性子集包括了六类,分别是购买(buying),维修(maint),车门数(doors),承载人数(Persons),载行李量(Luggage boot),安全性(safety)。每种属性又分成了相应的子集,分别为高中低或者是能承载的数量。
- 下面是一个典型的输入向量:
购买 | 维修 | 车门数 | 承载人数 | 载行李量 | 安全性 | 标签 |
---|---|---|---|---|---|---|
buying | maint | doors | persons | lug_boot | safety | Class |
二. 朴素贝叶斯算法
(1) 算法原理
假设我们有一个数据集,每一条由属性和他的类型构成,属性用X来表示,类型由Y表示,具体的类型为y1,y2…yn。对于一条新的数据,假如我们只知道他的属性,我们想根据他的属性来分类到具体的yi,这时就可以用贝叶斯算法。
- 贝叶斯定理
贝叶斯定理,他是朴素贝叶斯算法的基础
- 更一般的情况:假设B由很多个独立事件组成,或者说,B由很多个属性组成B1,B2…Bn他们相互独立,上式子表述为:
- 算法分类原则:
对于某一个条件,这个条件下哪个类的个数最多,这个情况就可能是这个类的。即:
event= max{P(y1|X),P(y2|X)...P(yn|X)},X是条件(属性),y是类。
-
拉普拉斯平滑
- 由于某些特征属性的值P(Xi|Ci)可能很小,多个特征的p值连乘后可能被约等于0。可以公式两边取log然后变乘法为加法,避免连乘问题。
- P(Ci) 和P(Xi|Ci) 一般不直接使用样本的频率计算出来,一般会使用拉普拉斯平滑。
上面公式中,Dc为该类别的频数,N表示所有类别的可能数。
上面公式中,Dc,xi为该特征对应属性的频数,Dc为该类别的频数,Ni表示该特征的可能的属性数。
(2) 算法步骤
(3) 算法伪代码
- 首先遍历整个标签集合,计算P(Vi),即每个标签的概率
- 然后对于任何一个特征a,计算它的所有属性ai在Vj条件下的概率。
- 最后执行分类预测,对于每一个可能的标签Vi,由贝叶斯公式计算发生的概率,求解其中的最大值。所对应的标签也就是预测的结果。
(4) 算法实现
算法实现比较长,我取其中的核心部分来讲解,具体见附件src,首先遍历整个标签集合,计算P(Vi),即每个标签的概率,然后对于任何一个特征a,计算它的所有属性ai在Vj条件下的概率。最后执行分类预测,对于每一个可能的标签Vi,由贝叶斯公式计算发生的概率,求解其中的最大值。所对应的标签也就是预测的结果。
- 构建贝叶斯算法
首先遍历整个标签集合,计算P(Vi),即每个标签的概率,然后对于任何一个特征a,计算它的所有属性ai在Vj条件下的概率。
def buildNaiveBayes(self, xTrain):
yTrain = xTrain.iloc[:,-1] # 标签labels
yTrainCounts = yTrain.value_counts() # 得到各个标签的个数
# 使用拉普拉斯平滑 P(ci) = Dc+1 / D + N, Dc为该类别的频数,N表示所有类别的可能数。
yTrainCounts = yTrainCounts.apply(lambda x : (x + 1) / (yTrain.size + yTrainCounts.size))
retModel = {}
# 遍历标签集合中的每一项,用字典的数据结构保存,将会作为字典树返回
for nameClass, val in yTrainCounts.items():
retModel[nameClass] = {'PClass': val, 'PFeature':{}}
# 训练集所有特征
propNamesAll = xTrain.columns[:-1]
allPropByFeature = {}
# 遍历标特征集合,计算每个特征的数量,这个关系字典allPropByFeature十分重要,在后续将会用到
for nameFeature in propNamesAll:
allPropByFeature[nameFeature] = list(xTrain[nameFeature].value_counts().index)
# 使用groupby函数根据标签进行分组,并且遍历它
for nameClass, group in xTrain.groupby(xTrain.columns[-1]):
# 遍历训练集中的所有特征a
for nameFeature in propNamesAll:
eachClassPFeature = {}
propDatas = group[nameFeature] # 当前特征取值
propClassSummary = propDatas.value_counts() # 频次汇总 得到各个特征对应的频数
for propName in allPropByFeature[nameFeature]: # 遍历每个特征的属性ai
if not propClassSummary.get(propName): # 如果有属性没有,那么自动补0
propClassSummary[propName] = 0
Ni = len(allPropByFeature[nameFeature]) # Ni表示所有特征出现的可能数
#使用拉普拉斯平滑 P(ci) = Dc+1 / D + N, Dc为该类别的频数,N表示所有类别的可能数。
propClassSummary = propClassSummary.apply(lambda x : (x + 1) / (propDatas.size + Ni))
# 我们现在已经统计好了每个ai的概率,以字典的结构保存在propClassSummary中,我们要将其映射到eachClassPFeature上,然后一并返回
for nameFeatureProp, valP in propClassSummary.items():
eachClassPFeature[nameFeatureProp] = valP
# 保存每个特征的概率ai,返回
retModel[nameClass]['PFeature'][nameFeature] = eachClassPFeature
return retModel
- 预测分类,取最大值
对于每一个可能的标签Vi,由贝叶斯公式计算发生的概率,求解其中的最大值。所对应的标签也就是预测的结果。
def predictBySeries(self, data):
curMaxRate = None # 概率最大值
curClassSelect = None # 最大概率对应的类
for nameClass, infoModel in self.model.items(): # 遍历朴素贝叶斯结构中的每一个类
rate = 0 # 当前标签类nameClass的概率
# 为防止由于某些特征属性的值P(Xi|Ci)可能很小,多个特征的p值连乘后可能被约等于0
# 取log然后变乘法为加法,避免连乘问题。
rate += np.log(infoModel['PClass']) # 求和条件概率
PFeature = infoModel['PFeature'] # 取出当前特征
# 遍历数据集中的每一个特征向量
for nameFeature, val in data.items():
propsRate = PFeature.get(nameFeature) # 获取当前特征发生的概率a
if not propsRate: # 为0则直接跳过
continue
#使用log加法避免很小的小数连续乘,接近零
rate += np.log(propsRate.get(val, 0)) # 计算当前标签类nameClass的概率
if curMaxRate == None or rate > curMaxRate: # 迭代,取最大值
curMaxRate = rate
curClassSelect = nameClass
return curClassSelect # 返回被选择的类
(5) 测试结果
为了对几个算法是性能进行比较,我对训练数据进行测试得到了训练准确率, 对测试数据进行预测得到了测试准确率,并且统计了算法的时间开销:
- 执行结果:
$ python NaiveBayes.py
朴素贝叶斯算法求解car数据集训练准确率为88.296%,测试准确率为68.783%,时间开销为36.251ms
三. 决策树算法
(1) 决策树的基本思想:
决策树是一种基本的分类与回归方法,它可以看作if-then规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。
将决策树转换成if-then规则的过程如下:
- 由决策树的根节点到叶节点的每一条路径构建一条规则;
- 路径内部结点的特征对应规则的条件;
- 叶节点的类对应规则的结论.
决策树的路径具有一个重要的性质:互斥且完备,即每一个样本均被且只能被一条路径所覆盖。
(2) 特征选择
我们应该基于什么准则来判定一个特征的分类能力呢?这时候,需要引入一个概念:信息增益
- 熵
在信息论与概率论中,熵(entropy)用于表示随机变量不确定性的度量。
设X是一个有限状态的离散型随机变量,其概率分布为
则随机变量X的熵定义为
从而可知,当p=0.5时,熵取值最大,随机变量不确定性最大。
1.ID3算法
(1) 算法原理
ID3算法的核心是在决策树的各个结点上应用信息增益准则进行特征选择。具体做法是:
- 从根节点开始,对结点计算所有可能特征的信息增益,选择信息增益最大的特征作为结点的特征,并由该特征的不同取值构建子节点;
- 对子节点递归地调用以上方法,构建决策树;
- 直到所有特征的信息增益均很小或者没有特征可选时为止。
(2) 算法实现
在createTree之前,我们需要定义一些辅助函数,完成计算熵,拆分数据集,…
- 计算经验熵
calcEntropy
#计算经验熵
def calcEntropy(dataSet):
mD = len(dataSet) # mD表示数据集的数据向量个数
dataLabelList = [x[-1] for x in dataSet] # 数据集最后一列 标签
dataLabelSet = set(dataLabelList) # 转化为标签集合,集合不重复,所以转化
ent = 0
for label in dataLabelSet: # 对于集合中的每一个标签
mDv = dataLabelList.count(label) # 统计它出现的次数
prop = float(mDv) / mD # 计算频率
ent = ent - prop * np.math.log(prop, 2) # 计算条件熵,见算法预备知识
return ent
-
计算条件熵:
(2) 计算特征A对数据集D的经验条件熵
#计算条件熵
def calcCondEntropy(dataSet,featureSet,i):
mD = len(dataSet)
ent=0
for feature in featureSet:
# 拆分数据集,去除第i行数据特征
splitedDataSet = splitDataSet(dataSet, i, feature)
mDv = len(splitedDataSet)
# H(D) - H(D|A) 计算信息增益
ent = ent - float(mDv) / mD * calcEntropy(splitedDataSet)
return ent
- 拆分数据集
splitDataSet
:
在createTree中,如果我们选定了一个标签,创建新的节点,意味着作出一个分类,因此一个label将会被划分出去,下面的splitDataSet正是完成这样的工作,他将拆分数据集:
def splitDataSet(dataSet, index, feature):
splitedDataSet = []
mD = len(dataSet)
for data in dataSet:
if(data[index] == feature): # 将数据集拆分
sliceTmp = data[:index] # 取[0,index)
sliceTmp.extend(data[index + 1:]) # 扩展(index,len]
splitedDataSet.append(sliceTmp)
return splitedDataSet
- 根据信息增益 - 选择最好的特征
chooseBestFeature
在ID3的算法中,从根节点开始,对结点计算所有可能特征的信息增益,选择信息增益最大的特征作为结点的特征,并由该特征的不同取值构建子节点;信息增益最大的节点选择工作,由chooseBestFeature完成:
def chooseBestFeature(dataSet):
entD = calcEntropy(dataSet) # 计算经验熵
mD = len(dataSet)
featureNumber = len(dataSet[0]) - 1
maxGain = -100 # 最大增益
maxIndex = -1 # 最大增益的下标
Gain=0
for i in range(featureNumber):
featureI = [x[i] for x in dataSet] # 数据集合中的第i列特征
featureSet = set(featureI) # 特征集合
Gain = entD - calcCondEntropy(dataSet,featureSet,i) # 计算信息增益
if(maxIndex == -1):
maxGain = Gain
maxIndex = i
elif(maxGain < Gain): # 记录最大的信息增益和下标
maxGain = Gain
maxIndex = i
return maxIndex # 返回下标
- 寻找最多的标签
对于一个标签集合,我们要选择其中数量最多的标签作为该集合的主要分类标签,下面的mainLabel
函数正是实现这个过程:
# 寻找最多的,作为标签
def mainLabel(labelList):
labelRec = labelList[0]
maxLabelCount = -1
labelSet = set(labelList)
for label in labelSet:
if(labelList.count(label) > maxLabelCount):
maxLabelCount = labelList.count(label)
labelRec = label
return labelRec
- 下面是生成决策树的主体部分:
如果数据集为空,返回父节点标签列表的主要标签;如果没有可划分的属性,选出最多的label作为该数据集的标签,如果全部都属于同一个Label,返回labList[0],不满足上面的边界情况则需要创建新的分支节点,根据信息增益,选择数据集中最好的特征下标,下面是它对应的伪代码和具体实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6LmtlD98-1611578932497)(https://i.loli.net/2020/12/19/mMvzC1D9plXIWEA.png)]
# 生成决策树,注意,是列表的形式存储
# dataSet:数据集, featureNames:数据属性类别, featureNamesSet:属性类别集合, labelListParent:父节点标签列表
def createFullDecisionTree(dataSet, featureNames, featureNamesSet, labelListParent):
labelList = [x[-1] for x in dataSet]
if(len(dataSet) == 0): # 如果数据集为空,返回父节点标签列表的主要标签
return mainLabel(labelListParent)
elif(len(dataSet[0]) == 1): # 没有可划分的属性,选出最多的label作为该数据集的标签
return mainLabel(labelList)
elif(labelList.count(labelList[0]) == len(labelList)): # 全部都属于同一个Label,返回labList[0]
return labelList[0]
# 不满足上面的边界情况则需要创建新的分支节点
bestFeatureIndex = chooseBestFeature(dataSet) # 根据信息增益,选择数据集中最好的特征下标
bestFeatureName = featureNames.pop(bestFeatureIndex) # 取出属性类别
myTree = {bestFeatureName: {}} # 新建节点,一个字典
featureList = featureNamesSet.pop(bestFeatureIndex) # 取出最佳属性的类别
featureSet = set(featureList) # 剔除属性类别集合
for feature in featureSet: # 遍历最佳属性所有取值
featureNamesNext = featureNames[:]
featureNamesSetNext = featureNamesSet[:][:]
splitedDataSet = splitDataSet(dataSet, bestFeatureIndex, feature) # 剔除最佳特征
# 递归地生成新的节点
# featureNames:数据属性类别, featureNamesSet:属性类别集合, labelListParent:父节点标签列表
# 一个二叉树
myTree[bestFeatureName][feature] = createFullDecisionTree(splitedDataSet, featureNamesNext, featureNamesSetNext, labelList)
return myTree
(4) 测试结果
为了可视化生成的决策树,我定义了一个工具类,他用于决策树的可视化,并保存可视化的结果。为了对几个算法是性能进行比较,我对训练数据进行测试得到了训练准确率, 对测试数据进行预测得到了测试准确率,并且统计了算法的时间开销:
- 键入命令测试:
$ python DecisionTree.py
- 生成的决策树(太密集了,无法显示清楚)
- 结果分析:
观察上图,D3算法求解car数据集训练准确率为100%,测试准确率为70.3703%,时间开销为19.308ms,(生成的决策树分支太多了,难以显示清楚)
2 C4.5算法
(1) 算法思想
结合ID3算法思想,C4.5算法与ID3算法的区别主要在于它在生产决策树的过程中,使用信息增益比来进行特征选择。
- 信息增益比算法实现:
以信息增益作为特征选择准则,会存在偏向于选择取值较多的特征的问题。可以采用信息增益比对这一问题进行校正。
特征A对训练数据集D的信息增益比定义为其信息增益与训练集D关于特征A的值的熵之比,即
(2) 算法实现:
# 根据信息增益比,选择最佳的特征,并且返回最佳特征的下标
def chooseBestFeature_C45(dataSet):
entD = calcEntropy(dataSet) # 计算经验熵
featureNumber = len(dataSet[0]) - 1
maxGainRatio = -100 # 最大增益比
maxIndex = -1 # 最大增益比下标
GainRatio=0
for i in range(featureNumber):
featureI = [x[i] for x in dataSet] # 数据集合中的第i列特征
featureSet = set(featureI) # 特征集合
# 计算信息增益比
GainRatio = (entD - calcCondEntropy(dataSet,featureSet,i)) / entD
if(maxIndex == -1):
maxGainRatio = GainRatio
maxIndex = i
elif(maxGainRatio < GainRatio): # 记录最大的信息增益和下标
maxGainRatio = GainRatio
maxIndex = i
return maxIndex # 返回下标
(3) 测试结果
- 生成的决策树(太密集了,无法显示清楚)
观察上图,C4.5算法求解car数据集训练准确率为100%,测试准确率为70.3703%,时间开销为14.732ms,(生成的决策树分支太多了,难以显示清楚)
3.CART
(1) 算法思想
分类与回归树(classification and regression tree,CART)与C4.5算法一样,由ID3算法演化而来。CART假设决策树是一个二叉树,它通过递归地二分每个特征,将特征空间划分为有限个单元,并在这些单元上确定预测的概率分布。
CART算法中,对于回归树,采用的是平方误差最小化准则;对于分类树,采用基尼指数最小化准则。
- 基尼值公式:
数据集D的纯度可以用基尼值来度量,Gini(D)越小,数据集D的纯度越高
- 基尼指数公式:
数据集D,选择属性a划分后的基尼指数
比如数据集D,属性a的值把D分为D1、D2两个部分,那么在属性a的条件下,D的基尼指数表达式为:
- 平方误差最小化(用于回归树 本实验用不到)
(2) 算法实现
- 基尼值的计算
数据集D的纯度可以用基尼值来度量,Gini(D)越小,数据集D的纯度越高
# 计算基尼值
def calcGini(splitedDataSet):
gini_index = 1
y = list(splitedDataSet[-1]) # 标签列表
for unique_val in set(y): # 对于标签列表中的每一个value
p = y.count(unique_val) / len(y) # 计算它出现的概率
gini_index -= p**2 # 计算gini-=p^2
return gini_index
- 基尼指数的计算
数据集D,选择属性a划分后的基尼指数,遍历特征a的所有属性,然后计算基尼值的和。
# 计算基尼指数
for feature in featureSet: # 遍历某一特征的所有属性
splitedDataSet = splitDataSet(dataSet, i, feature) # 特征将其划分为Di
count = len(splitedDataSet)
# 计算Di / D * Gini(Di)
Gini += (count / len(dataSet[0]))*calcGini(splitedDataSet)
(3) 测试结果
- 可视化CART算法决策树
观察上图,CART算法求解car数据集训练准确率为100%,测试准确率为64.28%,时间开销为14.773ms,(生成的决策树分支太多了,难以显示清楚)
四.人工神经网络
(1) 算法原理
一种典型的神经网络:
- 人工神经网络模拟人的大脑,由很多神经元单元组成(胞体), 神经元单元由许多带权值的有向链相连接(轴突), 一个神经元单元可以同时向多个分支发送同样的传播信号(并行性)
- 下面是一种典型的神经网络:
最简单的人工神经网络:
- 其中x=(x1,…xn)T 输入向量
- y为输出
- wi是权系数;
- 输入与输出具有如下关系:
θ为阈值,f(X)是激活函数
激活函数
神经元单元使用激活函数(Y部分)来将输入信号转换为输出信号典型的激活函数为Sign函数
多层人工神经网络结构
一个输入层,至少一个中间隐藏层,一个输出层,反馈的神经网络, 信号从输入层向前传播,误差从输出层向后反馈
(2) 反向传播算法BP
back propagation
- 算法思想:
训练数据输入到神经网络中,神经网络计算真实输出与实际输出之间的差异,然后通过调节权值来减小这种差异 - 两阶段
训练数据输入,通过前向层层计算到输出层输出结果
计算真实输出与实际输出之间的错误,通过反向从输出层-隐藏层-输入层的调节权值来减少错误
(3) BP算法步骤:
- 第一步,初始化
设定初始的权值w1,w2,…,wn和阈值θ为如下的一致分布中的随机数
Fi是输入神经元数量总和
- 第二步:计算激活函数值
根据输入x1§, x2§,…, xn§ 和权值w1,w2,…,wn计算输出y1§, y2§,…, yn§
(a)计算隐藏层神经元的输出
n是第j个隐藏层神经元的输入数量,sigmoid是激活函数
(b)计算输出层神经元的输出
m是第k个输出神经元的输入数量
- 第三步:权值更新(从后往前)
(a)计算输出层的错误梯度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BDv659MG-1611580229173)(https://i.loli.net/2020/12/19/KQS6iNLynqlF5GD.png)]
计算权值纠正值
更新输出层的权值
(b)计算隐藏层的错误梯度
计算权值纠正值
更新隐藏层的权值
- 第四步:迭代循环
增加p值,不断重复步骤二和步骤三直到收
(5) 算法实现
- 一位编码
因为数据集输入中,数据特征是string
类型,为了方便BP算法的权值更新,我们要使用get_dummies对dataFrame对象进行1位编码。
# 加载数据,返回1位编码数据特征矩阵和label矩阵
def load_data(path):
data = pd.read_csv(path)
print("data_before encoding:\n",data)
# 一位编码,使得可以在神经网络中计算
data = hash_encode(data)
print("data_after_encoding:\n",data)
data_mat = data.values
col_num = data_mat.shape[1]
return data_mat[:, 0:col_num-1], data_mat[:, col_num-1]
-
编码结果:
-
神经网络初始化
# 初始化
def __init__(self, learning_rate, layers=1, iter_nums=5000):
self.lr = learning_rate # 学习率
self.layers = layers # 神经隐藏层数
self.hide_node = 3 # 隐藏层节点数
self.iters = iter_nums # 训练次数
self.weight = [] # 权值
self.bias = [] # 偏移量
- 激活函数
# 激活函数sigmod
def __active_fun(self, x):
y = 1 / (1 + np.exp(-x))
return y
- 初始化权值和域值
# 初始化w, b
def __init_para(self, x_train):
rows, cols = x_train.shape
w_1 = np.random.randn(cols, self.hide_node)
# 返回用0填充的数组,1*hide_node规模
b_1 = np.zeros((1, self.hide_node))
w_2 = np.random.randn(self.hide_node, 1)
b_2 = np.zeros((1, 1))
return w_1, w_2, b_1, b_2
- 计算误差
# 计算error
def __mean_square_error(self, predict_y, y):
differ = predict_y - y
error = 0.5*sum(differ**2)
return error
- 使用BP算法进行训练:
# 训练拟合
def fit(self, x_train, y_train):
self.hide_node = int(np.round(np.sqrt(x_train.shape[0]))) # 隐藏层节点数
# initialize the network parameter
w_1, w_2, b_1, b_2 = self.__init_para(x_train) # 初始化权值w,阈值b
for i in range(self.iters): # 迭代iters次
accum_error = 0
for s in range(x_train.shape[0]): # 遍历每一个数据输入
hidein_1 = np.dot(x_train[s, :], w_1) + b_1 # 计算隐藏层1 y1 = w1*x +b1
hideout_1 = self.__active_fun(hidein_1) # 激活
hidein_2 = np.dot(hideout_1, w_2) + b_2 # 计算隐藏层2 y2 = w2*x +b2
hideout_2 = self.__active_fun(hidein_2) # 激活
predict_y = hideout_2
accum_error += self.__mean_square_error(predict_y, y_train[s]) # 计算误差error,第二层
if accum_error <= 0.001: # 如果误差小于0.001,我们认为这个权值可以接受
break
else: # update the parameter # 否则更新,开始反向传播BP
for s in range(x_train.shape[0]):
in_nums = x_train.shape[1]
# layer 1
hidein_1 = np.dot(x_train[s, :], w_1) + b_1 # 重新计算
hideout_1 = self.__active_fun(hidein_1)
# layer 2
hidein_2 = np.dot(hideout_1, w_2) + b_2
hideout_2 = self.__active_fun(hidein_2)
predict_y = hideout_2
g = predict_y*(1 - predict_y)*(y_train[s] - predict_y) # 计算权值增量g
e = g*w_2.T*(hideout_1*(1 - hideout_1)) # 计算误差
w_2 = w_2 + self.lr*hideout_1.T*g # 更新w2
b_2 = b_2 - self.lr*g # 更新b2
w_1 = w_1 + self.lr*x_train[s, :].reshape(in_nums, 1)*e # 更新w1
b_1 = b_1 - self.lr*e # 更新b1
self.weight.append(w_1)
self.weight.append(w_2)
self.bias.append(b_1)
self.bias.append(b_2)
- 测试结果:
$ python NeuralNetwork.py
(6) 测试结果
- 在服务器上使用BP算法的准确率:
观察上图,BP算法求解car数据集训练准确率为96.3%,测试准确率为72.75%,时间开销稍长,8min左右,我在个人电脑上做了一次时间测试,耗时
373.4158012866974 s.
- 调参优化:
为了得到更加精准的准确率,我试图通过调整卷积层数量和学习率来优化,下面是优化记录:
卷积层数 | 学习率 | 准确率 |
---|---|---|
1 | 0.1 | 0.727513227 |
1 | 0.2 | 0.742316549 |
1 | 0.25 | 0.7486772486772487 |
1 | 0.225 | 0.6507936507936508 |
1 | 0.15 | 0.7433862433862434 |
2 | 0.25 | 0.746031746031746 |
3 | 0.25 | 0.7195767195767195 |
- 当然这是我手工实现的神经网络,后来我使用sklearn库实现神经网络,对比我手工实现的BP准确率提高到了
0.89655
,时间开销缩短到了1min之内。下图是执行结果
五. 支持向量机
支持向量机(support vector machines, SVM)是一种二分类模型,它的基本模型是定义在特征空间上的间隔最大的线性分类器,间隔最大使它有别于感知机;SVM还包括核技巧,这使它成为实质上的非线性分类器。SVM的的学习策略就是间隔最大化,可形式化为一个求解凸二次规划的问题,也等价于正则化的合页损失函数的最小化问题。SVM的的学习算法就是求解凸二次规划的最优化算法。
(1)算法原理
(2) SMO算法
(3) 算法实现
- 我们采取简化版SMO算法解决求解SVM
每次迭代随机选取alpha_i和alpha_j,当然其中要有一个违反kkt条件,通常先选一个违反kkt条件的alpha_i,然后随机选择一个alpha_j,然后用类似坐标上升(下降)的算法来优化目标函数
- 首先是一些辅助函数,用来帮助加载数据,修剪 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-amjvCxF8-1611578932582)(https://www.zhihu.com/equation?tex=%5Calpha)] 的值以及随机选取
# 在m中随机选择除了i之外剩余的数
def selectJrand(i, m):
j = i # 排除i
while (j == i):
j = int(random.uniform(0, m))
return j
# 修建alpha的值到L和H之间.
def clipAlpha(aj, H, L):
if aj > H:
aj = H
if L > aj:
aj = L
return aj
- 为了能在最后绘制SVM分割线,我们需要根据获取的
def calcWs(alphas, dataMatrix, labelMat):
m, n = shape(dataMatrix)
w = zeros((n, 1))
for i in range(m):
w += multiply(alphas[i] * labelMat[i], dataMatrix[i, :].T)
return w
- 下面是具体实现过程
# 简化版SMO算法实现
def smoSimple(dataMatrix, classLabels, C, toler, maxIter):
labelMat = mat(classLabels).T
b = -1;
m, n = shape(dataMatrix)
alphas = mat(zeros((m, 1)))
iter = 0
while (iter < maxIter):
alphaPairsChanged = 0 # alpha是否已经进行了优化
for i in range(m):
# w = alpha * y * x;
# "SVM分类器函数 y = w^T*x + b"
# 预测的类别
fXi = float(multiply(alphas, labelMat).T * dataMatrix * dataMatrix[i, :].T) + b
Ei = fXi - float(labelMat[i]) # 计算误差,如果误差太大,检查是否可能被优化
# 满足约束
if ((labelMat[i] * Ei < -toler) and (alphas[i] < C)) or ((labelMat[i] * Ei > toler) and (alphas[i] > 0)):
j = selectJrand(i, m)
fXj = float(multiply(alphas, labelMat).T * (dataMatrix * dataMatrix[j, :].T)) + b
Ej = fXj - float(labelMat[j])
alphaIold = alphas[i].copy()
alphaJold = alphas[j].copy()
# 对原始解进行修剪
if (labelMat[i] != labelMat[j]):
L = max(0, alphas[j] - alphas[i])
H = min(C, C + alphas[j] - alphas[i])
else:
L = max(0, alphas[j] + alphas[i] - C)
H = min(C, alphas[j] + alphas[i])
if L == H: # print "L==H";
continue
# Eta = -(2 * K12 - K11 - K22),且Eta非负,此处eta = -Eta则非正
eta = 2.0 * dataMatrix[i, :] * dataMatrix[j, :].T - dataMatrix[i, :] * dataMatrix[i, :].T - dataMatrix[j,:] * dataMatrix[j, :].T
if eta >= 0:
continue
alphas[j] -= labelMat[j] * (Ei - Ej) / eta
alphas[j] = clipAlpha(alphas[j], H, L)
# 如果内层循环通过以上方法选择的α_2不能使目标函数有足够的下降,那么放弃α_1
if (abs(alphas[j] - alphaJold) < 0.00001): # print "j not moving enough";
continue
alphas[i] += labelMat[j] * labelMat[i] * (alphaJold - alphas[j])
# 更新阈值b
b1 = b - Ei - labelMat[i] * (alphas[i] - alphaIold) * dataMatrix[i, :] * dataMatrix[i, :].T - labelMat[j] * (alphas[j] - alphaJold) * dataMatrix[i, :] * dataMatrix[j, :].T
b2 = b - Ej - labelMat[i] * (alphas[i] - alphaIold) * dataMatrix[i, :] * dataMatrix[j, :].T - labelMat[j] * (alphas[j] - alphaJold) * dataMatrix[j, :] * dataMatrix[j, :].T
if (0 < alphas[i]) and (C > alphas[i]):
b = b1
elif (0 < alphas[j]) and (C > alphas[j]):
b = b2
else:
b = (b1 + b2) / 2.0
alphaPairsChanged += 1
if (alphaPairsChanged == 0):
iter += 1
else:
iter = 0
return b, alphas
- 通过已知数据点和拉格朗日乘子获得分割超平面参数w
def calcWs(alphas, dataMatrix, labelMat):
# 根据获取的 [alpha] ,数据点以及标签来获取 [w] 的值:
m, n = shape(dataMatrix)
w = zeros((n, 1))
for i in range(m):
w += multiply(alphas[i] * labelMat[i], dataMatrix[i, :].T)
return w
(4) 测试结果
$ python SVM.py
观察上图,SVM算法采用线性核求解car数据集训练准确率为86.89%,测试准确率为80.37%,时间开销17ms,SVM算法采用高斯核求解car数据集训练准确率为97.18%,测试准确率为87.26%,时间开销20ms.
六.算法性能比较
- 在对算法的测试过程中,我记录了算法的运行时间,训练准确率,测试准确率下面我将对他们一一列举:
算法 | 训练准确率 | 测试准确率 | 时间开销 | 备注 |
---|---|---|---|---|
NaiveBayes | 88.296% | 68.783% | 36.251ms | 朴素贝叶斯 |
DecisionTree ID3 | 100% | 70.3703% | 19.308ms | ID3决策树 |
DecisionTree C4.5 | 100% | 70.3703% | 14.732ms | C4.5决策树 |
DecisionTree CART | 100% | 64.28% | 14.773ms | CART决策树 |
NeuralNetwork | 96.3% | 72.75% | 373.4158s | BP人工神经网络 |
SVM 线性核 | 86.89% | 72.75% | 17ms | SVM 线性核 |
SVM 高斯核 | 97.18% | 87.26% | 20ms. | SVM 高斯核 |
- 准确率比较
观察上表可知,准确率最高的算法是SVM高斯核,BP人工神经网络次之;DecisionTree的三个算法中 ID3决策树和 C4.5决策树准确率一致,都达到 70.3703%;朴素贝叶斯测试准确率为 68.783%,最后是 CART决策树,测试准确率为64.28%。后来我使用sklearn库实现神经网络,对比我手工实现的BP准确率提高到了0.89655
,时间开销缩短到了1min之内。
- 时间开销比较
观察上表可知,时间开销最低的是决策树算法和SVM,算法的时间都接近14.732ms;其次是朴素贝叶斯算法,时间开销是36.251ms;运行时间最长的是BP人工神经网络,我设置了迭代次数为5000, 2个中间隐藏层,学习率设置为0.25,实际运行时间达到了8min左右。后来我使用sklearn库实现神经网络,对比我手工实现的BP准确率提高到了0.89655
,时间开销缩短到了1min之内。
- 时间复杂度比较:
时空复杂度和具体实现算法有关,我手工实现的算法可能性能不如机器学习库。
(1) 时间复杂度最高的是 BP人工神经网络为,拿一个简单的三层BP神经网络来说好了,假设每层神经元数量分别为n1,n2,n3。拿一个样本(n1 * 1)进行前馈计算,那么就要进行两次矩阵运算,两次矩阵乘法(实际上是向量和矩阵相乘)分别要进行n1 * n2 和 n2 * n3次计算,由于输入层和最终输出层结点数量(n1和n3)是确定的,所以可以视为常量,中间的隐藏层n2可以由自己设定。对一个样本的前馈计算时间复杂度应该是O(n1 * n2 + n2 * n3) = O(n2)。反向传播时时间复杂度和前馈计算相同,假设总共有m个训练样本,每个样本只训练一次,那么训练一个神经网络的时间复杂度应该是O(m*n2).后来我去网上找了下,正确的时间复杂度为
(2) 朴素贝叶斯由于要计算每一个属性的概率,其时间复杂度为O(C*n2)
(3) 决策树算法构建一颗分类树,分类树的节点数量和数据特征有关,假设有m个特征,那么决策树算法的时间复杂度为O(n*2^m)
(4) SVM算法,原始问题的时间复杂度为O(d^3+n*d^2)
,对偶的时间解法时间复杂度为O(n_sv^3+n*n_sv^2)
- 空间开销比较
时空复杂度和具体实现算法有关,我手工实现的算法可能性能不如机器学习库。
(1) 训练一个神经网络的空间复杂度
(2) 朴素贝叶斯不用过多的辅助空间,只需一个长度为m*n的数组来记录条件概率即可,S(n) = O(m*n)
(3) 决策树算法构建一颗分类树,分类树的节点数量和数据特征有关,假设有m个特征,那么决策树算法的空间复杂度为O(n*2^m)
(4) SVM算法的空间复杂度:假定分类数量为,也是支持向量的个数,输入向量维度为相当于在维度为的高维空间上,存储个高维超平面,用于对每个维度的样本点进行分类。线性SVM需要存储的参数就是这个高维超平面的参数,共个数字需要存储。因此空间复杂度为S(n) = O(dlNs)