From 5ad0a5099b5e2f2ba525e8892628a8d9bc7f1405 Mon Sep 17 00:00:00 2001 From: jiangzhonglian Date: Wed, 1 Mar 2017 20:30:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0Sklearn=20=E5=86=B3=E7=AD=96?= =?UTF-8?q?=E6=A0=91=E7=9A=84=E4=BD=BF=E7=94=A8Demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/python/03.DecisionTree/DTSklearn.py | 115 +++++++++++++++++++ src/python/03.DecisionTree/DecisionTree.py | 2 +- src/python/tools/DecisionTree.py | 124 +++++++++++++++++++++ testData/DT_data.txt | 10 ++ testResult/tree.pdf | Bin 0 -> 16586 bytes 5 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/python/03.DecisionTree/DTSklearn.py create mode 100644 src/python/tools/DecisionTree.py create mode 100644 testData/DT_data.txt create mode 100644 testResult/tree.pdf diff --git a/src/python/03.DecisionTree/DTSklearn.py b/src/python/03.DecisionTree/DTSklearn.py new file mode 100644 index 00000000..5155b214 --- /dev/null +++ b/src/python/03.DecisionTree/DTSklearn.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# coding: utf8 +# 原始链接: http://blog.csdn.net/lsldd/article/details/41223147 +import numpy as np +from sklearn import tree +from sklearn.metrics import precision_recall_curve +from sklearn.metrics import classification_report +from sklearn.cross_validation import train_test_split + + +def createDataSet(): + ''' 数据读入 ''' + data = [] + labels = [] + with open("testData/DT_data.txt") as ifile: + for line in ifile: + # 特征: 身高 体重 label: 胖瘦 + tokens = line.strip().split(' ') + data.append([float(tk) for tk in tokens[:-1]]) + labels.append(tokens[-1]) + # 特征数据 + x = np.array(data) + # label分类的标签数据 + labels = np.array(labels) + # 预估结果的标签数据 + y = np.zeros(labels.shape) + + ''' 标签转换为0/1 ''' + y[labels == 'fat'] = 1 + print data, '-------', x, '-------', labels, '-------', y + return x, y + + +def predict_train(x_train, y_train): + ''' + 使用信息熵作为划分标准,对决策树进行训练 + 参考链接: http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier + ''' + clf = tree.DecisionTreeClassifier(criterion='entropy') + # print(clf) + clf.fit(x_train, y_train) + ''' 系数反映每个特征的影响力。越大表示该特征在分类中起到的作用越大 ''' + print 'feature_importances_: %s' % clf.feature_importances_ + + '''测试结果的打印''' + y_pre = clf.predict(x_train) + # print(x_train) + print(y_pre) + print(y_train) + print(np.mean(y_pre == y_train)) + return y_pre, clf + + +def show_precision_recall(x, clf, y_train, y_pre): + ''' + 准确率与召回率 + 参考链接: http://scikit-learn.org/stable/modules/generated/sklearn.metrics.precision_recall_curve.html#sklearn.metrics.precision_recall_curve + ''' + precision, recall, thresholds = precision_recall_curve(y_train, y_pre) + # 计算全量的预估结果 + answer = clf.predict_proba(x)[:, 1] + + ''' + 展现 准确率与召回率 + precision 准确率 + recall 召回率 + f1-score 准确率和召回率的一个综合得分 + support 参与比较的数量 + 参考链接:http://scikit-learn.org/stable/modules/generated/sklearn.metrics.classification_report.html#sklearn.metrics.classification_report + ''' + # target_names 以 y的label分类为准 + target_names = ['thin', 'fat'] + print(classification_report(y, answer, target_names=target_names)) + print(answer) + print(y) + + +def show_pdf(clf): + ''' + 可视化输出 + 把决策树结构写入文件: http://sklearn.lzjqsdd.com/modules/tree.html + + Mac报错:pydotplus.graphviz.InvocationException: GraphViz's executables not found + 解决方案:sudo brew install graphviz + 参考写入: http://www.jianshu.com/p/59b510bafb4d + ''' + # with open("testResult/tree.dot", 'w') as f: + # from sklearn.externals.six import StringIO + # tree.export_graphviz(clf, out_file=f) + + import pydotplus + from sklearn.externals.six import StringIO + dot_data = StringIO() + tree.export_graphviz(clf, out_file=dot_data) + graph = pydotplus.graph_from_dot_data(dot_data.getvalue()) + graph.write_pdf("testResult/tree.pdf") + + # from IPython.display import Image + # Image(graph.create_png()) + +if __name__ == '__main__': + x, y = createDataSet() + + ''' 拆分训练数据与测试数据, 80%做训练 20%做测试 ''' + x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2) + print '拆分数据:', x_train, x_test, y_train, y_test + + # 得到训练的预测结果集 + y_pre, clf = predict_train(x_train, y_train) + + # 展现 准确率与召回率 + show_precision_recall(x, clf, y_train, y_pre) + + # 可视化输出 + show_pdf(clf) diff --git a/src/python/03.DecisionTree/DecisionTree.py b/src/python/03.DecisionTree/DecisionTree.py index c7920bd1..03a4d25f 100644 --- a/src/python/03.DecisionTree/DecisionTree.py +++ b/src/python/03.DecisionTree/DecisionTree.py @@ -129,7 +129,7 @@ def chooseBestFeatureToSplit(dataSet): subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet)/float(len(dataSet)) newEntropy += prob * calcShannonEnt(subDataSet) - # 计算label的信息熵和每个特征的信息熵 的增益值,如果增益值大于最大值,那么效果越好 + # gain[信息增益] 值越大,意味着该分类提供的信息量越大,该特征对分类的不确定程度越小 infoGain = baseEntropy - newEntropy if (infoGain > bestInfoGain): bestInfoGain = infoGain diff --git a/src/python/tools/DecisionTree.py b/src/python/tools/DecisionTree.py new file mode 100644 index 00000000..a2974d91 --- /dev/null +++ b/src/python/tools/DecisionTree.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# coding: utf8 + +from math import log + + +def calcShannonEnt(dataSet): + """calcShannonEnt(calculate Shannon entropy 计算label分类标签的香农熵) + + Args: + dataSet 数据集 + Returns: + 返回香农熵的计算值 + Raises: + + """ + # 求list的长度,表示计算参与训练的数据量 + numEntries = len(dataSet) + # print type(dataSet), 'numEntries: ', numEntries + + # 计算分类标签label出现的次数 + labelCounts = {} + # the the number of unique elements and their occurance + for featVec in dataSet: + currentLabel = featVec[-1] + if currentLabel not in labelCounts.keys(): + labelCounts[currentLabel] = 0 + labelCounts[currentLabel] += 1 + # print '-----', featVec, labelCounts + + # 对于label标签的占比,求出label标签的香农熵 + shannonEnt = 0.0 + for key in labelCounts: + prob = float(labelCounts[key])/numEntries + # log base 2 + shannonEnt -= prob * log(prob, 2) + # print '---', prob, prob * log(prob, 2), shannonEnt + return shannonEnt + + +def splitDataSet(dataSet, axis, value): + """splitDataSet(通过遍历dataSet数据集,求出axis对应的colnum列的值为value的行) + + Args: + dataSet 数据集 + axis 表示每一行的axis列 + value 表示axis列对应的value值 + Returns: + axis列为value的数据集【该数据集需要排除axis列】 + Raises: + + """ + retDataSet = [] + for featVec in dataSet: + # axis列为value的数据集【该数据集需要排除axis列】 + if featVec[axis] == value: + # chop out axis used for splitting + reducedFeatVec = featVec[:axis] + ''' + 请百度查询一下: extend和append的区别 + ''' + reducedFeatVec.extend(featVec[axis+1:]) + # 收集结果值 axis列为value的行【该行需要排除axis列】 + retDataSet.append(reducedFeatVec) + return retDataSet + + +def getFeatureShannonEnt(dataSet, labels): + """chooseBestFeatureToSplit(选择最好的特征) + + Args: + dataSet 数据集 + Returns: + bestFeature 最优的特征列 + Raises: + + """ + # 求第一行有多少列的 Feature + numFeatures = len(dataSet[0]) - 1 + # label的信息熵 + baseEntropy = calcShannonEnt(dataSet) + # 最优的信息增益值, 和最优的Featurn编号 + bestInfoGain, bestFeature, endEntropy = 0.0, -1, 0.0 + # iterate over all the features + for i in range(numFeatures): + # create a list of all the examples of this feature + # 获取每一个feature的list集合 + featList = [example[i] for example in dataSet] + # get a set of unique values + # 获取剔重后的集合 + 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[信息增益] 值越大,意味着该分类提供的信息量越大,该特征对分类的不确定程度越小 + # gain[信息增益]=0, 表示与类别相同,无需其他的分类 + # gain[信息增益]=baseEntropy, 表示分类和没分类没有区别 + infoGain = baseEntropy - newEntropy + # print infoGain + if (infoGain > bestInfoGain): + endEntropy = newEntropy + bestInfoGain = infoGain + bestFeature = i + else: + if numFeatures < 0: + labels[bestFeature] = 'null' + + return labels[bestFeature], baseEntropy, endEntropy, bestInfoGain + + +if __name__ == '__main__': + labels = ['no surfacing', 'flippers'] + dataSet1 = [['yes'], ['yes'], ['no'], ['no'], ['no']] + dataSet2 = [['a', 1, 'yes'], ['a', 2, 'yes'], ['b', 3, 'no'], ['c', 4, 'no'], ['c', 5, 'no']] + dataSet3 = [[1, 'yes'], [1, 'yes'], [1, 'no'], [3, 'no'], [3, 'no']] + infoGain1 = getFeatureShannonEnt(dataSet1, labels) + infoGain2 = getFeatureShannonEnt(dataSet2, labels) + infoGain3 = getFeatureShannonEnt(dataSet3, labels) + print '香农熵: \n\t%s, \n\t%s, \n\t%s' % (infoGain1, infoGain2, infoGain3) + diff --git a/testData/DT_data.txt b/testData/DT_data.txt new file mode 100644 index 00000000..3bf55b88 --- /dev/null +++ b/testData/DT_data.txt @@ -0,0 +1,10 @@ +1.5 50 thin +1.5 60 fat +1.6 40 thin +1.6 60 fat +1.7 60 thin +1.7 80 fat +1.8 60 thin +1.8 90 fat +1.9 70 thin +1.9 80 thin diff --git a/testResult/tree.pdf b/testResult/tree.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f2b728290b9ceb5f02f52eeaf196369a36f37412 GIT binary patch literal 16586 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 R7}*$DIN(T0MPx