赞
踩
最近由于文档需求,开始接触了一些机器学习的东西,这里简要记录下学习的过程,目的是简单了解下决策树、随机森林、朴素贝叶斯、聚类这几个分类器的使用。参考书籍为《机器学习实战》,学习之前首先了解下机器学习的分类,主要分为以下几类:
决策树是一种机器学习的方法,它的生成算法有ID3, C4.5和CART等。决策树是一种树形结构(这里只考虑分类不考虑回归),其中每个内部节点表示一个属性上的判断,每个分支代表一个判断结果的输出,最后每个叶节点代表一种分类结果,之后要了解的随机森林也是基于决策树构建的。在分类问题中,可以简单认为是 if-then 规则的集合,表示基于特征对实例进行分类的过程。《机器学习实战》中给的示例图如下图所示:
不同的生成算法有不同的属性划分选择:
这里主要学习基于ID3算法划分数据的决策树,ID3算法核心是在决策树各个结点上对应信息增益准则选择特征,递归地构建决策树。简单描述构建方法如下:
If so return 类标签:
Else
寻找划分数据集的最优特征
划分数据集
创建分支节点
for 每个划分的子集
调用函数createBranch()并增加返回结果到分支节点中
return 分支节点
通过上述步骤其实也可以看到,通过信息增益准则来选择最优特征从而划分数据是比较重要的一步,数据划分的过程就是其实构建决策树的过程,之后我们用例子来看一下前面提到的一些概念。这里参考机器学习实战(三)——决策树中给的例子。
上图是一个贷款申请的数据表,每一行包含一个人的对应列即特征的值,最后一列给出类别即是否可以贷款。在编写代码之前,先对数据集进行属性标注。
首先引入数据集:
def createDataSet(): #数据集 dataSet=[[0, 0, 0, 0, 'no'], [0, 0, 0, 1, 'no'], [0, 1, 0, 1, 'yes'], [0, 1, 1, 0, 'yes'], [0, 0, 0, 0, 'no'], [1, 0, 0, 0, 'no'], [1, 0, 0, 1, 'no'], [1, 1, 1, 1, 'yes'], [1, 0, 1, 2, 'yes'], [1, 0, 1, 2, 'yes'], [2, 0, 1, 2, 'yes'], [2, 0, 1, 1, 'yes'], [2, 1, 0, 1, 'yes'], [2, 1, 0, 2, 'yes'], [2, 0, 0, 0, 'no']] #特征 labels=['年龄','有工作','有自己的房子','信贷情况'] return dataSet, labels
计算给定数据集的香农熵的函数,我们可以计算下原始数据集的香农熵,由于是没有分类的,因此熵值应该是比较大的:
def calcShannonEnt(dataSet): # 求list的长度,表示计算参与训练的数据量 numEntries = len(dataSet) # 计算分类标签label出现的次数 labelCounts = {} # the the number of unique elements and their occurrence for featVec in dataSet: # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签 currentLabel = featVec[-1] # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。 if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0 labelCounts[currentLabel] += 1 # 对于 label 标签的占比,求出 label 标签的香农熵 shannonEnt = 0.0 for key in labelCounts: # 使用所有类标签的发生频率计算类别出现的概率。 prob = float(labelCounts[key])/numEntries # 计算香农熵,以 2 为底求对数 shannonEnt -= prob * log(prob, 2) return shannonEnt if __name__ == "__main__": myData,labels = createDataSet() print("ShannonEnt :" + str(calcShannonEnt(myData)))
运行结果,得到原始数据集的熵,因为还没有分类的原因也就是无序状态,可以看到熵值还是比较大的:
接着按照给定特征划分数据集,这里特征是之后传入的:
def splitDataSet(dataSet, index, value): """ 作用:通过遍历dataSet数据集,求出index对应的colnum列的值为value的行 Args: dataSet 数据集 待划分的数据集 index 表示每一行的index列 划分数据集的特征 value 表示index列对应的value值 需要返回的特征的值 Returns: index列为value的数据集(行)【该数据集需要排除index列】 """ retDataSet = [] for featVec in dataSet: # index列为value的数据集【该数据集需要排除index列】 # 判断index列的值是否为value if featVec[index] == value: # chop out index used for splitting # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行 reducedFeatVec = featVec[:index] reducedFeatVec.extend(featVec[index+1:]) # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据 # 收集结果值 index列为value的行【该行需要排除index列】 retDataSet.append(reducedFeatVec) return retDataSet
然后就是我们上面说的选择最优特征:
def chooseBestFeatureToSplit(dataSet): """ 作用:选择最优特征 Args: dataSet 数据集 Returns: bestFeature 最优的特征列 """ # 求第一行有多少列的 Feature, 最后一列是label列嘛 numFeatures = len(dataSet[0]) - 1 # 数据集的原始信息熵 baseEntropy = calcShannonEnt(dataSet) # 最优的信息增益值, 和最优的Featurn编号 bestInfoGain, bestFeature = 0.0, -1 # iterate over all the features for i in range(numFeatures): # create a list of all the examples of this feature # 获取对应的feature下的所有数据 featList = [example[i] for example in dataSet] # get a set of unique values # 获取剔重后的集合,使用set对list数据进行去重 uniqueVals = set(featList) # 创建一个临时的信息熵 newEntropy = 0.0 # 遍历某一列的value集合,计算该列的信息熵 # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) # 计算概率 prob = len(subDataSet)/float(len(dataSet)) # 计算信息熵 newEntropy += prob * calcShannonEnt(subDataSet) # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值 # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。 infoGain = baseEntropy - newEntropy print('选择第',i,'列作为最优特征得到的信息增益infoGain=', infoGain,'原始数据集熵=',baseEntropy,'按照这个特征值划分后得到的熵=',newEntropy) if (infoGain > bestInfoGain): bestInfoGain = infoGain bestFeature = i return bestFeature if __name__ == "__main__": myData,labels = createDataSet() #print("ShannonEnt :" + str(calcShannonEnt(myData))) bestFeature = chooseBestFeatureToSplit(myData) print('最优特征列',bestFeature)
运行结果:
可以看到,信息增益就等于原始数据集熵-数据集划分后得到的熵,并且选择第2列(有自己的房子)作为最优特征列得到的信息增益最大。
由于特征数目并不是在每次划分数据分组时都减少,因此这些算法在实际使用时可能引起一定的问题。目前我们并不需要考虑这个问题,只需要在算法开始运行前计算列的数目,查看算法是否使用了所有属性即可。如果数据集已经处理了所有属性,但是类标签依然不是唯一
的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们通常会采用多数表决的方法决定该叶子节点的分类:
def majorityCnt(classList): """ 作用:选择出现次数最多的一个结果 Args: classList label列的集合 Returns: sortedClassCount[0][0]:出现次数最多的元素(类标签) """ classCount = {} for vote in classList: if vote not in classCount.keys(): classCount[vote] = 0 classCount[vote] += 1 # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果 sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True) return sortedClassCount[0][0]
接着就开始创建树:
def createTree(dataSet, labels): classList = [example[-1] for example in dataSet] # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行 # 第一个停止条件: 所有的类标签完全相同,则直接返回该类标签。 # count() 函数是统计括号中的值在list中出现的次数 if classList.count(classList[0]) == len(classList): return classList[0] # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果 # 第二个停止条件: 使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。 if len(dataSet[0]) == 1: return majorityCnt(classList) # 选择最优的列,得到最优列对应的label含义 bestFeat = chooseBestFeatureToSplit(dataSet) # 获取label的名称 bestFeatLabel = labels[bestFeat] # 初始化myTree myTree = {bestFeatLabel: {}} # 注: labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改 # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list del(labels[bestFeat]) # 取出最优列,然后它的branch做分类 featValues = [example[bestFeat] for example in dataSet] uniqueVals = set(featValues) for value in uniqueVals: # 求出剩余的标签label subLabels = labels[:] # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree() myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) return myTree if __name__ == "__main__": myData,labels = createDataSet() #print("ShannonEnt :" + str(calcShannonEnt(myData))) #bestFeature = chooseBestFeatureToSplit(myData) #print('最优特征列',bestFeature) myTree = createTree(myData,labels) print(myTree)
运行结果如下:
可以看到,对原始数据的第一次划分时选择的是第2列(实际上是表中的第三列-有自己的房子)作为最优特征列,第一次划分后,第2列(实际上是表中的第三列-有自己的房子)这个特征就分出去了(所以第二次划分的第2列实际上是表中的第4列),接着再次递归建树,第二次时,选择的是第一列(实际上是表中的第二列-有工作)作为最优特征列(infoGain=0.918最大,且划分后熵为0代表数据集为完全有序状态),最后得到的决策树如上图中最后一行所示,二次划分后就已经得到了一个有序的数据集。
使用python提供的Matplotlib进行绘制决策树,使用到的函数如下:
首先初始化一些格式例如节点、箭头,然后依次实现上述函数:
import operator from math import log import matplotlib.pyplot as plt from matplotlib import font_manager as fm, rcParams from matplotlib.font_manager import FontProperties decisionNode = dict(boxstyle="sawtooth", fc="0.8")#设置结点格式 leafNode = dict(boxstyle="round4", fc="0.8")#设置叶结点格式 arrow_args = dict(arrowstyle="<-")#设置箭头格式 def plotNode(nodeTxt, centerPt, parentPt, nodeType): font = FontProperties(fname=r"/Users/iris/Downloads/simsun.ttc", size=10) createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerPt, textcoords='axes fraction', va="center", ha="center", bbox=nodeType, arrowprops=arrow_args,FontProperties=font) def getNumLeafs(myTree): numLeafs = 0 firstStr=next(iter(myTree)) secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict': numLeafs += getNumLeafs(secondDict[key]) else: numLeafs +=1 return numLeafs def getTreeDepth(myTree): maxDepth = 0 firstStr = next(iter(myTree)) secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict': thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth def plotMidText(cntrPt, parentPt, txtString): xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1] createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30) def plotTree(myTree, parentPt, nodeTxt): numLeafs = getNumLeafs(myTree) depth = getTreeDepth(myTree) firstStr=next(iter(myTree)) cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) plotMidText(cntrPt, parentPt, nodeTxt) plotNode(firstStr, cntrPt, parentPt, decisionNode) secondDict = myTree[firstStr] plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict': plotTree(secondDict[key],cntrPt,str(key)) else: plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode) plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key)) plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD def createPlot(inTree): fig = plt.figure(1, facecolor='white') fig.clf() axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) plotTree.totalW = float(getNumLeafs(inTree)) plotTree.totalD = float(getTreeDepth(inTree)) plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; plotTree(inTree, (0.5,1.0), '') plt.rcParams['font.sans-serif']=['SimHei'] plt.rcParams['axes.unicode_minus']=False plt.show() if __name__ == "__main__": myData,labels = createDataSet() #print("ShannonEnt :" + str(calcShannonEnt(myData))) #bestFeature = chooseBestFeatureToSplit(myData) myTree = createTree(myData,labels) #print(myTree) createPlot(myTree)
效果如下:
对应的树和前面运行的结果相同:
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子结点;最后将测试数据定义为叶子结点所属的类型。
...... def classify(inputTree, featLabels, testVec): """classify(给输入的节点,进行分类) Args: inputTree 决策树模型 featLabels Feature标签对应的名称 testVec 测试输入的数据 Returns: classLabel 分类的结果值,需要映射label才能知道名称 """ # 获取tree的根节点对于的key值 firstStr=next(iter(inputTree)) # 通过key得到根节点对应的value secondDict = inputTree[firstStr] # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类 featIndex = featLabels.index(firstStr) # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类 key = testVec[featIndex] valueOfFeat = secondDict[key] #print(firstStr, secondDict, '---', key, '>>>', valueOfFeat) # 判断分枝是否结束: 判断valueOfFeat是否是dict类型 if isinstance(valueOfFeat, dict): classLabel = classify(valueOfFeat, featLabels, testVec) else: classLabel = valueOfFeat return classLabel ...... if __name__ == "__main__": myData,labels = createDataSet() #print("ShannonEnt :" + str(calcShannonEnt(myData))) #bestFeature = chooseBestFeatureToSplit(myData) myTree = createTree(myData,copy.deepcopy(labels)) #print(myTree) #createPlot(myTree) testVec = [2,0,0,1] result=classify(myTree,labels,testVec) if result=='yes': print('放贷') if result=='no': print('不放贷')
运行结果:
《机器学习实战》中提到:构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则可以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用已经构造好的决策树。为了解决这个问题,需要使用Python模块pickle序列化对象。序列化对象可以在磁盘上保存对象,并在需要的时候读取出来。拿上面我们构建的程序来说,每给出一个新的测试用例,运行的时候找最优特征、划分数据建树等过程都会再进行一遍,很耗时。如果我们能存储这个树,那么只需要调用classify这个函数就好,且第一个参数inputTree不用我们再重新构建了,就节省了很多的时间。
def storeTree(inputTree,filename): fw = open(filename,'wb') pickle.dump(inputTree,fw)#使用pickle.dump存储决策树 fw.close() def grabTree(filename): fr = open(filename,'rb') return pickle.load(fr)#pickle.load进行载入决策树 if __name__ == "__main__": myData,labels = createDataSet() #print("ShannonEnt :" + str(calcShannonEnt(myData))) #bestFeature = chooseBestFeatureToSplit(myData) #print(myTree) #createPlot(myTree) storeTree(createTree(myData,copy.deepcopy(labels)),"myTree.txt") myTree = grabTree("myTree.txt") testVec = [2,0,0,1] result=classify(myTree,labels,testVec) if result=='yes': print('放贷') if result=='no': print('不放贷')
效果如下:
隐形眼镜数据集是非常著名的数据集,它包含很多患者眼部状况的观察条件以及医生推荐的隐形眼镜类型。特征包括’age’, ‘prescript’, ‘astigmatic’, ‘tearRate’,隐形眼镜类型包括hard、soft以及no lenses(不适合佩戴隐形眼镜)。
young myope no reduced no lenses young myope no normal soft young myope yes reduced no lenses young myope yes normal hard young hyper no reduced no lenses young hyper no normal soft young hyper yes reduced no lenses young hyper yes normal hard pre myope no reduced no lenses pre myope no normal soft pre myope yes reduced no lenses pre myope yes normal hard pre hyper no reduced no lenses pre hyper no normal soft pre hyper yes reduced no lenses pre hyper yes normal no lenses presbyopic myope no reduced no lenses presbyopic myope no normal no lenses presbyopic myope yes reduced no lenses presbyopic myope yes normal hard presbyopic hyper no reduced no lenses presbyopic hyper no normal soft presbyopic hyper yes reduced no lenses presbyopic hyper yes normal no lenses
那么按照我们之前的步骤。给出如下程序:
def LensesTest(): """ 作用:预测隐形眼镜的测试代码 Args:none Returns:none """ # 加载隐形眼镜相关的 文本文件 数据 fr = open('lenses.txt') # 解析数据,获得 features 数据 lenses = [inst.strip().split('\t') for inst in fr.readlines()] # 得到数据的对应的 Labels lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树 lensesTree = createTree(lenses, lensesLabels) print(lensesTree) # 画图可视化展现 createPlot(lensesTree) if __name__ == "__main__": LensesTest()
效果如下:
这里简单了解一下其概念,之后有时间再具体学习。sklearn(Scikit-learn)是机器学习中常用的第三方模块,对常用的机器学习方法进行了封装,包括回归(Regression)、降维(Dimensionality Reduction)、分类(Classfication)、聚类(Clustering)等方法。sklearn库的结构如下图所示:可以看到和树很像
由图中可以看到sklearn库的算法主要有四类:分类,回归,聚类,降维。其中:
例如sklearn提供了我们这次学习的决策树模型,用于解决分类和回归问题。
class sklearn.tree.DecisionTreeClassifier(criterion=’gini’, splitter=’best’, max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort=False)[source]
参数说明如下:
通常来说只需要调整其中较为重要的几个参数就行:
sklearn.tree.DecisionTreeClassifier还提供了一些方法,其相应的作用如下图所示:
这里尝试使用sklearn构建上面隐形眼镜的决策树,根据上图中的函数,很容易写出下面的程序:
from sklearn import tree
if __name__ == "__main__":
fr = open('lenses.txt')
lenses = [inst.strip().split('/t') for inst in fr.readlines()]
print(lenses)
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
clf = tree.DecisionTreeClassifier()
lenses = clf.fit(lenses, lensesLabels)
但是运行报错:
这是因为在fit()函数不能接收string类型的数据,通过打印的信息可以看到,数据都是string类型的。在使用fit()函数之前,我们需要对数据集进行编码,这里可以使用两种方法:
from sklearn import tree import pandas as pd from sklearn.preprocessing import LabelEncoder if __name__ == '__main__': # 加载文件 with open('lenses.txt', 'r') as fr: # 处理文件 lenses = [inst.strip().split('\t') for inst in fr.readlines()] # 提取每组数据的类别,保存在列表里 lenses_target = [] for each in lenses: lenses_target.append(each[-1]) # 特征标签 lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 保存lenses数据的临时列表 lenses_list = [] # 保存lenses数据的字典,用于生成pandas lenses_dict = {} # 提取信息,生成字典 for each_label in lensesLabels: for each in lenses: lenses_list.append(each[lensesLabels.index(each_label)]) lenses_dict[each_label] = lenses_list lenses_list = [] # 打印字典信息 print(lenses_dict) #生成pandas.DataFrame lenses_pd = pd.DataFrame(lenses_dict) print(lenses_pd)
运行结果如下图所示:
然后通过LabelEncoder序列化数据:
......#省略程序代码和前面一样
#print(lenses_dict)
#生成pandas.DataFrame
lenses_pd = pd.DataFrame(lenses_dict)
#print(lenses_pd)
le = LabelEncoder()
for col in lenses_pd.columns:
lenses_pd[col] = le.fit_transform(lenses_pd[col])
print(lenses_pd)
运行结果如下:
然后接下来我们就要通过sklearn中的tree拟合数据,并通过tree.export_graphviz绘制决策树,完整代码如下:
import pandas as pd from sklearn.preprocessing import LabelEncoder from sklearn import tree from six import StringIO import pydotplus if __name__ == '__main__': # 加载文件 with open('lenses.txt', 'r') as fr: # 处理文件 lenses = [inst.strip().split('\t') for inst in fr.readlines()] # 提取每组数据的类别,保存在列表里 lenses_target = [] for each in lenses: lenses_target.append(each[-1]) # 特征标签 lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 保存lenses数据的临时列表 lenses_list = [] # 保存lenses数据的字典,用于生成pandas lenses_dict = {} # 提取信息,生成字典 for each_label in lensesLabels: for each in lenses: lenses_list.append(each[lensesLabels.index(each_label)]) lenses_dict[each_label] = lenses_list lenses_list = [] # 打印字典信息 #print(lenses_dict) #生成pandas.DataFrame lenses_pd = pd.DataFrame(lenses_dict) #print(lenses_pd) le = LabelEncoder() # 为每一列序列化 for col in lenses_pd.columns: lenses_pd[col] = le.fit_transform(lenses_pd[col]) #print(lenses_pd) clf = tree.DecisionTreeClassifier(max_depth = 4) clf = clf.fit(lenses_pd.values.tolist(), lenses_target) dot_data = StringIO() tree.export_graphviz(clf, out_file = dot_data, #绘制决策树 feature_names = lenses_pd.keys(), class_names = clf.classes_, filled=True, rounded=True, special_characters=True) graph = pydotplus.graph_from_dot_data(dot_data.getvalue()) graph.write_pdf("tree.pdf")
运行后会生成pdf文件:
之后再有新数据需要预测的时候通过clf.predict()方法,例如print(clf.predict([[1,1,1,0]])),程序预测如下:
参考以下文章:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。