[手写系列] 3.手写ID3决策树

227 阅读4分钟

决策树

  1. ID3、C4.5 只处理离散特征。 (1次遍历:最优特征)
  2. cart 既能处理离散特征,也能处理连续特征。(2次遍历:最优特征,最优特征分割点)
  3. ID3、C4.5是多叉树。cart是二叉树。
  4. GBDT、Xgboost既可以处理回归问题做预测,也可以处理分类问题,也可以得到高阶特征交叉的特征。
  5. GBDT、Xgboost都是回归树。cart自己做分类时,才是分类树。

image.png

from math import sqrt
from math import log
import operator
# H = sum(-p_i*log(p_i))
def calcShannonEnt(dataSet): #计算信息熵
    numEntries = len(dataSet) #数据条数
    labelCounts = {}
    for featVec in dataSet:
        currentLabel = featVec[-1]  #每行数据最后一个是类别
        if currentLabel not in labelCounts:
            labelCounts[currentLabel]=0
        labelCounts[currentLabel]+=1 #统计多少个类,以及每个类的数量
    shannonEnt = 0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries #计算每个类的熵值
        shannonEnt -= prob*log(prob,2) #累计每个类的熵值 #log(prob,2) 以2为底,prob的对数。
    return shannonEnt
# dataset , labels
def createDataSet1(): #创造示例数据
    dataSet = [['长','粗','男'], # 特征1 + 特征2 + 标签
               ['短','粗','男'],
               ['短','粗','男'],
               ['长','细','女'],
               ['短','细','女'],
               ['短','粗','女'],
               ['长','粗','女'],
               ['长','粗','女'],]
    labels = ['头发','声音']        #两个特征,最后是性别标签。
    return dataSet,labels
# splitDataSet
def splitDataSet(dataSet,axis,value): #按某个特征分类后的数据 axis=1,value='粗'(第1个特征,筛选出‘粗’的数据条)
    retDataSet = []
    for featVec in dataSet: # 遍历数据条
        if featVec[axis]==value: # 第一个特征 = ‘粗’
            # 去掉axis这一列
            reducedFeatVec = featVec[:axis] # 右边取不到 [0,axis)
            reducedFeatVec.extend(featVec[axis+1:]) # .extend / + (list拼接)
            retDataSet.append(reducedFeatVec)
    return retDataSet # 筛选 第一个特征=‘粗’的数据条 ,并删掉第一个特征那列。

# chooseBestFeatureToSplit
# !!!!! ID3只能处理离散特征。
def chooseBestFeatureToSplit(dataSet):    #根据信息熵,来选择最优的分类特征。
    numFeatures = len(dataSet[0])-1       #特征个数
    baseEntropy = calcShannonEnt(dataSet) #原始的熵: 按标签类别划分。
    bestInfoGain = 0
    bestFeature = -1 #记录最优特征
    # ID3离散型特征(特征分割点是固定的离散的,而不是连续的)。   #特征=声音  '粗'6/8【H1】|‘细’2/8【H2】
    # 如果是cart可以处理连续型特征。
    for i in range(numFeatures): #遍历每个特征j
        featList = [example[i] for example in dataSet]
        uniqueVals = set(featList) #每个特征的取值集合set
        newEntropy = 0
        # 按照声音特征划分,得到的熵。
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet,i,value)    # 筛选6个‘粗’的数据
            prob = len(subDataSet)/float(len(dataSet))    # 声音特征==value的概率 = 6/8
            newEntropy += prob*calcShannonEnt(subDataSet) # 按特征分类后的熵 6/8 * H1
        infoGain = baseEntropy - newEntropy
        # (不考虑特征分割点)更新信息增益
        if infoGain>bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

def majorityCnt(classList):    #按分类后类别数量排序,比如:最后分类为2男1女,则判定为男;
    classCount={}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote]=0
        classCount[vote]+=1
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    return sortedClassCount[0][0]

# 构建决策树 dataSet,labels
def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]  # y标签:男或女
    if classList.count(classList[0]) == len(classList):  # 标签只有同一类别,return该类别。
        return classList[0]
    if len(dataSet[0]) == 1:  # 只有标签,没有特征 ['男']
        return majorityCnt(classList)

    bestFeat = chooseBestFeatureToSplit(dataSet)  # 选择最优特征 的 下标
    bestFeatLabel = labels[bestFeat] # 最优特征:声音
    myTree = {bestFeatLabel: {}}  # 分类结果以字典形式保存 myTree={'声音':{}}
    del (labels[bestFeat]) # 特征集合 删除最优特征:声音
    featValues = [example[bestFeat] for example in dataSet] #选取最优特征那一列
    uniqueVals = set(featValues) #最优有2(uniqueVals)个分支
    for value in uniqueVals: #遍历最优特征的所有分支,所有分支再次构建树。
        subLabels = labels[:]
        # 子字典里,创建value={'细': T1, '粗': T2}.
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels) # 嵌套字典,{'声音':{'细':'女'}}
    return myTree # {'声音': {'细': '女', '粗': {'头发': {'长': '女', '短': '男'}}}}


# tips = {"头发":0, "声音":1}
# list1 = [['长', '粗'], ['短', '粗']]
def predict(mytree, tips, list1):
    res = []
    for item in list1:  # 遍历每一个样本  ['长', '粗']
        tmp_tree = mytree  # 训练好的树(用于迭代的树) tmp_tree, iter
        iter = tmp_tree.__iter__()  # 字典是可以__iter__()的对象?????????
        while 1:
            try:
                key = iter.__next__()
                if isinstance(key, str) and (key == "男" or key == "女"):  # 遍历到尾结点
                    res.append(key)
                    break

                v = tmp_tree[key]  # 子树
                index = tips[key]  # key的index
                item_res = item[index]  # 样本的key的值(['长', '粗'],index=0头发 =='长')
                tmp_tree = v[item_res]  # 迭代下一个树 tmp_tree,iter
                iter = tmp_tree.__iter__()
            except StopIteration:  # 不能迭代下去:终止!
                break
    return res

if __name__ == '__main__':
    dataSet, labels = createDataSet1()  # 创造示列数据
    mytree = createTree(dataSet, labels)
    print(mytree)  # 输出决策树模型结果

    # 预测
    tips = {"头发": 0, "声音": 1}  # 两个特征,头发=长/短;声音=粗/细。
    res = predict(mytree, tips, [['长', '粗'], ['短', '粗']])
    print(res) #['女', '男']