5 python 决策树实现 分类算法 机器学习

函数

  • log()函数和log2()函数

需要导入math模块。
log( ,2)相当于log2().

  • 列表方法append()

太熟了,不说了

  • 列表方法extend()

和append有点像,但是不一样!
append是在列表末尾添加一个元素,不管这个元素的数据类型是什么。
extend用于在列表末尾一次性追加另一个序列中的多个值。
list.extend(seq)

append:

a = [1, 2, 3]
b = [4, 5, 6]
a.append(b)
print(a)
[1, 2, 3, [4, 5, 6]]

extend:

a = [1, 2, 3]
b = [4, 5, 6]
a.extend(b)
print(a)
[1, 2, 3, 4, 5, 6]
  • set()函数

可以用来去除重复元素。
书上有句话说:从列表中创建集合是python语言得到列表中唯一元素值的最快方法。

信息熵

信息: 如果待分类的事务可能划分在多个分类之中,则符号 x i x_i xi的信息定义为:
l ( x i ) = − log ⁡ 2 p ( x i ) , l(x_i)=-\log_2 p(x_i) , l(xi)=log2p(xi)
其中, p ( x i ) p(x_i) p(xi)选择该分类的概率。
以上定义说明,一个具体事件所发生的概率越小,它发生后带来的信息量就越少。
熵: 所有类别所有可能值包含的信息期望值,公式为:
H = − ∑ i = 1 n p ( x i ) log ⁡ 2 p ( x i ) , H=-\sum_{i=1}^{n} p(x_i)\log_2 p(x_i), H=i=1np(xi)log2p(xi)
也就是各个具体事件发生所带来信息量的加权平均。

上面是书上给出的简单公式,并没有仔细解释。下面再稍微解释下:

信息量指的是以一个具体事件所携带的信息量。
一个离散型随机变量 X X X,取值为 x 0 , … , x n x_0,\dots,x_n x0,,xn,则事件 X = x 0 X=x_0 X=x0的信息量就是 I ( x 0 ) = − log ⁡ 2 p ( x 0 ) I(x_0)=-\log_2 p(x_0) I(x0)=log2p(x0)
一个事件所发生的概率越小,它所携带的信息量就越大。一个必然事件所携带的信息量为0,这也是很直观的,如果一个事件无论如何都会发生,我并不会感到惊奇,是吧?而一个发生概率很小的事件发生了,我们会感到出乎意料,所获得的信息当然就多。
所以,事件所携带的信息量是和其发生概率呈负相关的。

信息熵 ≠ 信息量
信息熵考虑随机变量所有的取值,描述的是所有可能事件所带来的信息量的期望
从信息传播角度来看,信息熵可以表示信息的价值。
变量的不确定性越大,熵就越大。 上面的离散随机变量 X X X 如果是均匀分布,则其熵最大。

计算给定数据集的熵

用频率代替概率。
书上这节dataSet的数据类型是python list,我不理解…
还有,书上定义了一个createDataSet()函数,把属性名组成的列表命名为labels,我也不理解…

  • 我把自己一开始写的和书上写的函数一块儿放上来了。我在用公式计算熵的那部分直接用了列表推导式,书上是用循环累加。
  • 用列表记录对应标签的数量,我使用了字典的get()方法,好像前一节有用过,但是书上直接先初始化为0,然后再累加。
  • 不知道这些写法有什么利弊,非计算机专业不太懂得写代码的一些套路,有时候会很疑惑…
我写的
from math import log2

# 计算给定数据集的香农熵
def calShannonEnt(dataSet):
    """标签是dataSet的最后一列
    先计算每种label的数据数量,再计算频率"""
    labels = []
    m = len(dataSet)  # 数据数量
    # 在不知道labels具体有什么的情况下,可以遍历labels,然后使用字典帮忙,使用dict.get()。
    label_count = {}
    for vec in dataSet:
        cur_label = vec[-1]
        label_count[cur_label] = label_count.get(cur_label, 0) + 1
    freqs_list = [num / m for num in label_count.values()]  # 频率列表
    Ent = sum([-freq * log2(freq) for freq in freqs_list])  # 直接计算熵
    return Ent


def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    feature_names = ['no surfacing', 'flippers']
    return dataSet, feature_names


my_dataset, my_feature_names = createDataSet()
print(my_dataset)
ent = calShannonEnt(my_dataset)
print(ent)
[[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
0.9709505944546686
书上写的
def calShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    # 下面循环计算各label的个数
    for featVec in dataSet:
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    shannonEnt = 0.0
    # 下面循环计算熵
    for key in labelCounts:
        prob = float(labelCounts[key]) / numEntries
        shannonEnt -= prob * log2(prob)
    return shannonEnt
    
def createDataSet():
    dataSet = [[1, 1, 'maybe'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    feature_names = ['no surfacing', 'flippers']
    return dataSet, feature_names

my_dataset, my_feature_names = createDataSet()
print(my_dataset)
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
1.3709505944546687

把数据集的label多加了一个类型’maybe‘

划分数据集

给定一个属性,我们需要根据属性的取值数据集进行一个划分
[之后的步骤是,算出划分后各个数据集的熵,然后根据属性取值的比例对这些数据集的熵求加权平均(期望)]

  • 写一个函数,输入:原始数据集,划分所依据的属性,属性取值。返回:该取值下的数据子集。
  • 注意: 提取子集的时候,要把这个属性对应的那一维数据给去掉。所以使用到了extend方法
  • 下面自己写的,和书上一模一样,只是变量名改了下,比较简单。
# 数据集划分
def splitDataSet(dataSet, pos, feat_val):
    """
    :param dataSet: 要划分的数据集,这里是python list
    :param pos: 划分所依据的属性 的位置
    :param feat_val: 要划分出来的属性取值
    :return: 数据子集
    """
    dataSubSet = []  # 用来存储子集
    for vec in dataSet:
        if vec[pos] == feat_val:
            new_vec = vec[:pos]
            new_vec.extend(vec[pos + 1:])
            dataSubSet.append(vec)
    return dataSubSet

选择最好的数据集划分方式

前面实现了计算一个数据集的熵,根据属性划分数据集。
现在可以将前面两个组合起来。
对特征进行遍历:划分数据集,然后分别计算熵,计算该特征划分下的熵(取期望)。 求哪种划分熵增益最大。

自己写的

  • 我自己写的程序另外定义了一个计算熵增益的函数calGainEnt(),我只是程序看起来更清晰一点
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
    """对每个特征进行遍历,选择熵增益最大的"""
    entD = calShannonEnt(dataSet)
    num_data = len(dataSet)
    num_features = len(dataSet[0]) - 1
    ent_gain_list = []  # 存放各个特征划分后的熵增益
    # 下面循环计算各个pos下的熵增益
    for cur_pos in range(num_features):
        cur_pos_gain_ent = calGainEnt(dataSet, num_data, entD, cur_pos)
        ent_gain_list.append(cur_pos_gain_ent)
    return ent_gain_list.index(max(ent_gain_list))


def calGainEnt(dataSet, num_data, entD, pos):
    """计算给定特征下的熵增益
    我传入num_data, entD的原因是,避免对每个pos重复计算这两个值"""
    # 先获取该属性/特征有哪些取值,并且这些取值所对应的数量有多少(需要计算比例)
    vals_counts = {}
    # 下面循环计算属性各个取值的样本数量,得到一个{val:数量}字典
    for vec in dataSet:
        cur_val = vec[pos]
        vals_counts[cur_val] = vals_counts.get(cur_val, 0) + 1
    pos_ent = 0.0
    for val, num in vals_counts.items():
        subSet = splitDataSet(dataSet, pos, val)
        subset_ent = calShannonEnt(subSet)
        pos_ent += subset_ent * (num / num_data)
    return entD - pos_ent

print(chooseBestFeatureToSplit(my_dataset))
0

书上的

  • 感觉确实书上的结构更清晰…不管从结构,还是变量命名…
  • 又是一边遍历一边处理的思想,确实这样可以:1.省掉很多变量的定义 2.没有那么多循环结构 3.说不定还可以节省内存 3.结构清晰 (个人感受)
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1
    baseEntrophy = calShannonEnt(dataSet)
    bestInfoGain = 0.0;
    bestFeature = -1
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet]  # 存放i位置属性的所有样本取值
        uniqueVals = set(featList)  # 提取属性取值种类
        newEntrophy = 0.0
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet) / float(len(dataSet))
            newEntrophy += prob * calShannonEnt(subDataSet)
        infoGain = baseEntrophy - newEntrophy
        if infoGain > bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

记住下面的写法:(列表推导和set去除重复元素
featList = [example[i] for example in dataSet] # 存放i位置属性的所有样本取值 uniqueVals = set(featList) # 提取属性取值种类

在遍历种求取最值:
if infoGain > bestInfoGain: bestInfoGain = infoGain bestFeature = i

多数表决函数

如果数据集已经处理完了所有属性,但是类标签依然不是唯一的,采用多数表决方法决定叶子节点分类。
记住这个函数的写法:

def majority(dataSet):
    """用一个字典记录各个类别有多少数量然后字典排序,返回数量最多的类"""
    class_counts = {}
    for vec in dataSet:
        label = vec[-1]
        class_counts[label] = class_counts.get(label, 0) + 1
    major_class = sorted(class_counts.items(), key=itemgetter(1))[0][0]  # sorted返回的是元组列表
    return major_class

其中,

class_counts[label] = class_counts.get(label, 0) + 1

可以等价替换为

        if label not in class_counts.keys(): class_counts[label] = 0
        class_counts[label] += 1

递归构建决策树

到了本节最重要的部分
本书提到的base case就下面两种:
1.如果子集不是都属于同一类,那么就递归构建子树。
2.如果属性用完了,采用多数表决。
但是我看西瓜书还有一种:3.如果一个属性取值下,划分的子集是空集,那么对原数据集进行多数表决,得到的类别作为当前子节点的类别。
决策树的递归形式:
{best_feature: {val1:子树1,…,valn:子树n}}

def createTree(dataSet, features):
    """如果子集不全是同一类,就创建子树。这里树直接用 ’字典之字典的形式表示‘ """
    # base case: dataSet全属于同一类
    class_list = [vec[-1] for vec in dataSet]
    # 如果class_list只有一种元素,说明所有元素都一样
    if len(set(class_list)) == 1:
        return class_list[0]
    # base case2:特征全部都用完了,多数表决
    if len(dataSet[0]) == 1:
        return majority(dataSet)
    # 以上都不满足,就是要分裂创建子树了
    best_feature = chooseBestFeatureToSplit(dataSet)
    best_feature_name = features[best_feature]
    my_tree = { best_feature_name: {}}  # 树的根节点就是分裂属性
    features.pop(best_feature)  # 删除这个属性
    values_list = [vec[best_feature] for vec in dataSet]
    unique_values = set(values_list)
    for value in unique_values:
        my_tree[best_feature_name][value] = createTree(splitDataSet(dataSet, best_feature, value), features[:])
    return my_tree

my_tree = createTree(my_dataset,my_feature_names)
print(my_tree)
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

总结

  1. 本节的数据集将特征与标签放到一起,应该是因为划分数据集时,特征集和标签集要一起划分,所以干脆放到一起。
  2. 实现算法需要将算法的过程一小步一小步拆解,先实现里面的小功能,然后再封装到一起
  3. 一开始看到算法有些不知从何入手,但是书上的实现思路却能如此清晰,将功能进行拆解后,每一小步的实现似乎都不是太难。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值