前言
最近在上机器学习基础,作业要求手写一个决策树代码,一开始以为很难,所以一直没动工,但是当自己亲自去实践,学习一下别人的思路,然后自己动手敲代码,发现其实也不难。 PPT转Python完全可行!【 关键是python要熟悉,敲代码要仔细,每写完一个函数一定要验证一下,不要都写完了再去验证,debug其实很难的】
参考链接
上面这个博客在决策树理论部分写得还行(当然如果时间充足建议还是去看西瓜书),所以这里就不再赘述决策树的理论了。但是代码部分感觉有点乱(当然不是分文件写这样基本的操作。。。),而且好像用的还是python2?但是代码思路还是非常不错的,是 典型的面向过程编程,利用递归的思路,通过不断修剪数据集并传递给递归的函数,思路逻辑非常清晰,本文也主要是借鉴上面那篇博客的思路。
上代码!
"""
Created on 2022.10.11
@author: Zoey
@version: Python 3.10
@简写说明
+ feat: feature
+ calc: calculate
+ Vec: Vector
+ chd: child
"""
import base
from LSM import xlsRead
import numpy as np
from sklearn import tree
from scipy import signal
import math
import matplotlib.pylab as plt
def labelGet2(s:list|np.ndarray)->np.ndarray:
''' @func: 给数据集打标签
@para s: 划分好的列表
@return: 一个标签list, 长度等于s的行数
'''
n = len(s)
labels = np.zeros(n,dtype="int")
for i in range(n):
if(np.mean(s[i]) >= 0.004): labels[i] = 1
elif(np.mean(s[i]) -0.006): labels[i] = -1
return labels
def DataSetExtract(dataset:np.ndarray, featIndex, featValue) -> np.ndarray:
''' @func: 在给定数据集(特征+标签)中提取出某个特征为某个特定值的子数据集
@para dataset: 数据集, 行为样本数, 列包含特征和标签
featIndex: 目标特征在数据集中的索引
featValue: 目标特征的一个取值
@return: 划分后的子数据集
'''
SampleNum = len(dataset)
featList = dataset[:, featIndex]
col = []
for i in range(SampleNum):
if(featList[i] == featValue): col.append(i)
subDataset = np.delete(dataset[col],featIndex,1)
return subDataset
def FreqCalc(A:np.ndarray, index) -> dict:
''' @func: 获取矩阵A的某一列(属性or标签)中各个取值的频次
@para A: 对象矩阵
index: 要取的列的索引
@return: a dict
'''
Value = A[:, index]
unique_value,occurrence = np.unique(Value, return_counts=True)
Dict = dict(zip(unique_value,occurrence))
return Dict
def InfoEntropy(dataset:np.ndarray) -> float:
''' @func: 计算某个数据集的标签信息熵
@para dataset: 数据集, 行为样本数, 列为特征+标签
@return: 数据集的信息熵
'''
labelDict = FreqCalc(dataset,-1)
EN = 0.0
for item in labelDict:
tmp = float(labelDict[item]) / len(dataset)
EN += - tmp * math.log2(tmp)
return EN
def GiniRatio(dataset:np.ndarray) -> float:
''' @func: 计算基尼指数
@para dataset:数据集
@return: Gini Ratio
'''
labelDict = FreqCalc(dataset,-1)
Gini = 0.0
for item in labelDict:
tmp = float(labelDict[item]) / len(dataset)
Gini += tmp**2
return 1 - Gini
def EntropyGain(dataset:np.ndarray, featIndex) -> float:
''' @func: 计算按某个特征分类得到的熵增益
@para dataset: 数据集, 行为样本数, 列为特征+标签
featIndex: 目标特征在数据集中的索引
@return: 熵增益, 浮点类型
'''
EN0 = InfoEntropy(dataset)
featDict = FreqCalc(dataset, featIndex)
EN1 = 0.0
for item in featDict:
subDataset = DataSetExtract(dataset,featIndex,item)
EN1 += InfoEntropy(subDataset) * (featDict[item] / len(dataset))
return EN0 - EN1
def EntropyGainRate(dataset:np.ndarray, featIndex) -> float:
''' @func: 计算特征的熵增益率
@para dataset: 数据集
featIndex: 特征索引
@return: 熵增益率
'''
ENG = EntropyGain(dataset,featIndex)
dic = FreqCalc(dataset,featIndex)
num_sum = sum(list(dic.values()))
Q = 0.0
for item in dic:
tmp = dic[item] / num_sum
Q += - tmp * math.log2(tmp)
return ENG/Q if Q else ENG
def GetBestFeat(dataset:np.ndarray):
''' @func: 遍历当前数据集下各个特征, 然后取熵增益最大的特征
@para dataset:当前分类下的数据集
@return: the index of the beat feature
'''
featNum = len(dataset[0])
MAX_EN = 0.0; INDEX = 0
for i in range(featNum-1):
EN_tmp = EntropyGain(dataset,i)
if(EN_tmp > MAX_EN): MAX_EN = EN_tmp ; INDEX = i
if(int(MAX_EN * 100) == 0): return -1
else: return INDEX
def MajorFeature(labelList:np.ndarray):
''' @func: 当特征列都被删完了只剩下标签列, 但是仍然有不同的标签,
此时只能取样本数较多的标签作为叶子节点
@para labelList: 最后的标签列表
@return: A label as leaf node
'''
labelDict = FreqCalc(labelList,0)
sortedDict = sorted(labelDict.items(), key=lambda d:d[1])
return sortedDict[-1][0]
def FeatDiscrete(srcDataSet:np.ndarray, colIndex = ..., classNum:int = 2) -> np.ndarray:
''' @func: 采用C4.5中处理连续值的方式, 对连续值进行离散【如果数据集本身是离散的则不需要管】
@para srcDataSet: 原始数据集, 特征矩阵
colIndex: 需要处理的特征列的索引, 是一个索引列表, 默认取所有特征列(除标签列)
classNum: 分类数目, 默认二分类
@return: dstDataSet
'''
def MidNumList(srcList:np.ndarray) -> np.ndarray:
''' @func: 提取一个列表的相邻元素中位数列表, n个数可以得到n-1个中位数
@para srcList: 原始数据列表
@return: dstList
'''
sortedList = np.sort(srcList)
dstList = np.array([])
for i in range(len(sortedList)-1):
dstList = np.append(dstList, np.median(sortedList[i:i+2]))
return dstList
def GetCombine(MidList:list|np.ndarray, Num = 2) -> list:
''' @func: 根据中位数列表取几个分界点
@para MidList: 中位数列表
Num: 取点的个数
@return: 取的关键点形成的列表, 是一个二维列表
'''
import itertools as it
CombineList = [e for e in it.combinations(MidList, Num)]
return CombineList
def discrete(srcDataVec:list|np.ndarray, CombineList:list):
''' @func: 根据分界点列表, 分配离散值, 从0开始
@para srcDataVec: 原始特征向量, 全部是连续值
CombineList: 分界点列表中的一行, 即一种情况
@return: 特征的离散值, 长度与原数据长度相同
'''
length = len(srcDataVec)
discreteOut = - np.ones(length)
for i in range(length):
for j in range(len(CombineList)):
if(srcDataVec[i] CombineList[j]): discreteOut[i] = j
if(discreteOut[i] == -1): discreteOut[i] = len(CombineList)
return discreteOut
if(colIndex == ...): colIndex = range(srcDataSet[0].size - 1)
for i in colIndex:
featList = srcDataSet[:,i].copy()
midlist = MidNumList(featList)
CombineList = GetCombine(midlist,classNum-1)
EN_GAIN_MAX = 0.0 ; DEVIDE_LIST = []
for item in CombineList:
srcDataSet[:,i] = discrete(featList,item)
tmp = EntropyGain(srcDataSet,i)
if(tmp>EN_GAIN_MAX): EN_GAIN_MAX = tmp; DEVIDE_LIST = item
srcDataSet[:,i] = discrete(featList, DEVIDE_LIST)
return srcDataSet
def CreateTree(dataset:np.ndarray, featList) -> dict:
''' @func: 根据输入的特征矩阵和标签向量构造决策树【核心函数】
@para dataset: 传入的样本数据, 即特征+标签
featList: 特征列表, 存储特征的名称
@return: A tree dict
'''
labelList = dataset[:,-1].copy()
if(len(dataset[0]) == 1): return MajorFeature(dataset)
if(np.unique(labelList).size == 1): return labelList[0]
BestFeatIndex = GetBestFeat(dataset)
if(BestFeatIndex == -1): return MajorFeature(dataset)
BestFeat = featList[BestFeatIndex]
Tree = {BestFeat:{}}
BestFeatValue = np.unique(dataset[:,BestFeatIndex])
for item in BestFeatValue:
subDataSet = DataSetExtract(dataset,BestFeatIndex,item)
Tree[BestFeat][item] = CreateTree(subDataSet, np.delete(featList,BestFeatIndex,0))
return Tree
def Predict(tree:dict, testDataset:np.ndarray, featList:list):
''' @func: 根据得到的树来预测某个样本的标签取值
@para tree: 已经创建好的一个树, 是一个字典
testDataset: 测试集
featList: 特征列表
@return: 预测出的标签取值
'''
if(testDataset.ndim == 1): testDataset = testDataset.reshape(1,-1)
sampleNum, _ = testDataset.shape
labelPre = np.zeros(sampleNum,testDataset.dtype)
for i in range(sampleNum):
testVec = testDataset[i]
firstFeat = list(tree.keys())[0]
featIndex = featList.index(firstFeat)
actualValue = testVec[featIndex]
subTree = tree[firstFeat][actualValue]
if isinstance(subTree, dict):
labelPre[i] = Predict(subTree, testVec, featList)[0]
else:
labelPre[i] = subTree
return labelPre
def ACC_Calc(src:np.ndarray|list, pre:np.ndarray|list) -> float:
''' @func: 计算分类的准确率
@para src:原标签列表
pre:预测的标签
@return: 准确率
'''
src = np.array(src); pre = np.array(pre)
return np.sum(src == pre) / src.size
def TreePlot(tree:dict, layer = 0) -> None:
''' @func: 以更易读的方式展示字典形式的树
@para tree: 树字典
@return: None
'''
firstFeat = list(tree.keys())[0]
secondTree = tree[firstFeat]
valuelist = list(secondTree.keys())
for item in valuelist:
chdtree = secondTree[item]
if isinstance(chdtree, dict):
print('|\t'*layer, firstFeat, item, ' : ')
TreePlot(chdtree, layer+1)
else: print('|\t'*layer, firstFeat, item, ' : ', chdtree)
USE_MY_TREE = 0
def main():
print("Please Wait for a few seconds...\n")
for e in range(2):
train = 0.8; test = 1-train
trainSet = dataset[:int(train*len(dataset))]
testSet = dataset[int(train*len(dataset)):]
if(USE_MY_TREE):
treedic = CreateTree(trainSet,cols)
print('Tree Structure of',e,':'); TreePlot(treedic)
labelOUT = Predict(treedic,testSet,cols)
print("Predicted LabelList:\n",labelOUT)
print("Source LabelList:\n",testSet[:,-1])
print("Accuracy: ",ACC_Calc(testSet[:,-1], labelOUT),'\n')
else:
clf = tree.DecisionTreeClassifier()
clf.fit(trainSet[:,:-1],trainSet[:,-1])
tree.plot_tree(clf)
labelOUT = clf.predict(testSet[:,:-1])
print('Predicted LabelList:\n',labelOUT)
print('Source LabelList:\n',testSet[:,-1])
print("Accuracy: ",ACC_Calc(testSet[:,-1], labelOUT),'\n')
if __name__ == "__main__":
main()
input("按下任意键继续...")
总结
- 本文所用代码为 基于过程编程,通过不断修剪数据集并传递给下一级递归函数,并不断给决策树字典添加内容,从而完成决策树的构建。这样的好处在于 逻辑清晰明了,但不如类的方式紧凑。用类的形式来完成最主要的难点在于 节点数据结构的创建,这里提供一种思路: 将节点定义为一个子类,其包含了①符合该特征的当前数据集,②剩余属性列表,③标签取值,④下一级节点(类似于一个指针)。
- 此外,这里对连续数据的处理采用的是二分类,但本文代码完全可以实现多分类来离散特征,只需要修改离散函数即可。这里采用的是 排列组合的方式生成分界点,然后遍历所有情况。因此,当样本数越多(或连续值个数越多),则代码运行时间成本将以 指数增长,比如300个连续值,如果取一个分界点,则有299个取值,如果取2个分界点,则有44551个取值,如果取3个分界点,则有4410549个取值。因此需要根据该特征的重要性合理选择。但是需要注意,增加分界点的意义只在于扩大搜索范围,但并不代表一定能找到一个相比于二分类更优的分界点,也有可能得到的仍然是二分类数据。
- 对于最优特征的选择,本文代码也添加了根据 熵增益率和 基尼指数来选择的代码,合理选择。
- 仍有未尽之处,可以在评论区指出,但希望是完全了解决策树且具有基本python常识提出的问题,否则,还请先把代码看完。
Original: https://blog.csdn.net/ZHOU_YONG915/article/details/127791310
Author: 记录无知岁月
Title: 【Python】决策树代码实践
原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/660553/
转载文章受原作者版权保护。转载请注明原作者出处!