AI实践之路:决策树

AI实践之路:决策树,第1张

1.理论

决策树的结构与数据结构的树差不多,树的根部输入所有样本,树的每个结点都是数据的特征,根据特征我们将样本分成一个个子集,使这一特征具有相同表现的样本汇聚,不同的分开,再对剩下的子集样本重复这些 *** 作,直到不能分或没特征可判断为止。这个模型很好想象,它就像带着if-else的树一样。

决策树从最初提出的ID3模型,经历了很多次升级,例如C4.5,CART等。本文先介绍ID3与C4.5。

1.1信息熵、条件熵、信息增益 1.1.1信息熵

熵这个词我们最早应该是在中学的化学课本中听到的,用来描述分子无序、混乱的程度,熵是不断增加的。熵这一概念在各大学科领域都有应用,热力学,物理学,统计学等等。香农(C.E.Shannon)提出了信息论中的熵:信息熵。这个公式与期望有关,以下的分析更为简单直接,没有对公式由来作出解释,有兴趣的可以参考:通俗理解信息熵

公式:

pi表示信息中某一符号出现的频率,比如我们有本书,每个汉字出现的频率就是pi。信息熵可以用来描述语言/信息的混乱程度,但是我们知道信息的混乱程度有什么用呢?

百度百科中说:一般而言,当一种信息出现概率更高的时候,表明它被传播得更广泛,或者说,被引用的程度更高。我们可以认为,从信息传播的角度来看,信息熵可以表示信息的价值。这样子我们就有一个衡量信息价值高低的标准。

从公式的角度分析,若整个信息系统只有一个字、符号或者字母,那么p=1,信息熵为0,没有不确定性,它太确定了,无法传递什么信息,你抱着只有一个字的书能学到什么知识呢?那么我们加一个,这个信息系统里有1和0,p都是0.5,信息熵结果为1,这个信息系统可以传递信息了,就像现实中我们用1和0构建了计算机的逻辑。合理想象随着符号的增加,系统能传递的信息也在增加。另一个角度,假如1和0出现频率不同呢?若1是0.7,0是0.3,信息熵计算的结果是0.88。这说明如果符号出现的频率不相同,或者出现更大的差异,信息熵比频率相同的情况小,不确定减小,很好理解,如果一本书有99个1只有1个0那么我们也很难判断出什么信息。这就是信息熵,经过这段解释,相信对于这句话有了进一步的认识:信息熵表示信息的不确定性,进而可以用于表示信息的价值。

1.1.2条件熵

理解了信息熵,条件熵的概念便利于理解了,它表示在某条件下信息的不确定性。 

它利用了条件概率来计算。(默认log以2为底)

回到我们真实的应用上,我们拥有一堆x与y,x拥有不同的特征,y拥有不同的种类,可以为这些建立一个信息系统模型。信息熵的概率当然是围绕y出现的概率来进行计算,来获得系统的不确定性,条件熵呢?则是在不同x的条件下,系统关于y信息的不确定性。

我之前看到这个公式的时候就发现为什么前面乘的概率是联合概率而不是条件概率呢?这是略微复杂的推导过程,参考自:通俗理解条件熵

1.1.3信息增益

有了信息熵,(信息)条件熵,我们可以分析一下他们的关系,信息熵是整个信息系统不确定性的度量,条件熵则是系统在一定条件下不确定性的度量,显然,给定了条件束缚后,不确定性一定是减少了的,信息熵大于条件熵。

那么将信息熵减去条件熵,我们就获得了当确定某一条件后,系统不确定性损失了多少!而这个差值,我们称为信息增益,显然信息论的学者们起的这个名字十分贴切。

1.2决策树的建立 1.2.1ID3

回到决策树上来,我们似乎可以直接建一个决策树,就是没完没了的if-else就好了,比如我们要辨别西瓜的好坏,特征有西瓜的纹理,根蒂,声响等,一个个判断呗,先拍拍听听声音,分不出来看看根蒂咋样,再根据纹理等等一层层也能够分类出来,人们买瓜不就是这样的嘛。

判断瓜太简单了, 我们实际需要面对的问题和数据会非常复杂,数据的分布会千变万化,你需要谨慎选择用来判断特征的顺序,你随便选的特征顺序可能会造成非常大规模的误判,如果特征数量很多,你总不能每种顺序都试一遍吧?

所以我们需要用到信息增益。它用来帮我们决定如何选用特征再合适不过,它会帮我们计算在已知某一特征的情况下,不确定性减少了多少,减少的越多,说明这个特征包含的信息就越大,我们越应该先进行判断!

运用这个思想,我们可以简单写出决策树的构建过程了:

数据:X,N个样本,每个样本包含M个特征。每个样本有一个标签,共有K类标签。

视整个样本集合为S。

  • 若根结点为空,建立根结点,根结点作为当前结点。否则建立新结点,当前结点作为新结点的父结点,新结点作为当前结点。
  • 对当前结点输入S,若S样本个数为1,返回,若当前特征为0,返回。
  • 计算整体样本标签的信息熵H(S),计算M个特征的条件熵H(S|X),计算信息增益。
  • 选取最大信息增益的特征作为当前结点决策特征,根据特征不同的值将S分为S1,S2,S3等,更新整体特征M变为M-1。
  • 对于每个新的样本集,重复以上过程

以上便是最早版本的决策树ID3,简单实用。


随着决策树的应用,其缺点渐渐暴露:

  • 只能处理离散特征,连续型数据无法应用。
  • 可能发生严重的过拟合,树会因为样本与特征的存在不断细分下去,使得模型不断拟合训练数据导致测试误差增加。
  • 没有应对缺失值的能力。(这一点很多地方都提到了,实际上有点奇怪,很多算法都没有对缺失值的处理,这本是特征工程的活儿,为什么要对决策树有处理缺失值的期望呢?可能是因为决策树的可解释性强,是个难得的算法,希望它更健壮完美吧)
  • 选用信息增益作为特征选择标准。

前三点很好理解,最后一点是因为信息增益并不完美,它倾向于选择取值多的特征,比如一个特征A有两个值1和0,概率相同各0.5;另一个特征B有0,1和2三种取值,概率相同各1/3,信息增益一定会选择B,但A与B的不确定性都很充足,这使得一些情况我们得不到正确的模型。

1.2.2C4.5

随后经过多次更新,新的决策树模型C4.5解决了ID3的缺点。此处参考:决策树算法--C4.5算法,强烈推荐。

特征选择:

使用信息增益比作为选择标准,信息增益比消除了特征取值数量的影响,其为信息增益与特征熵的比值,若样本为S,特征为A,特征熵:

缺失值:

  • 在有缺失值的特征上如何计算信息增益比?根据缺失比例,折算信息增益(无缺失值样本所占的比例乘以无缺失值样本子集的信息增益)和信息增益比
  • 选定了划分属性,若样本在该属性上的值是缺失的,那么如何对这个样本进行划分?将样本以不同概率同时划分到不同节点中,概率是根据其他非缺失属性的比例来得到的
  • 对新的样本进行分类时,如果测试样本特性有缺失值如何判断其类别?走所有分支,计算每个类别的概率,取概率最大的类别赋值给该样本

连续值:

离散化处理,若该连续特征有m个值,将m个值从小到大排序,计算每两个相邻的值的平均值,共m-1个平均值,将这m-1个平均值视为新的m-1个划分点来计算它们的信息增益比,选取最优的划分点,样本根据该划分点分为小于该点的子样本和大于该点的子样本。

过拟合:

决策树过拟合的来源主要因为树会不断产生分支,这个过程会丧失泛化能力,于是解决过拟合的手段就是剪枝。剪枝包括前剪枝和后剪枝两种。

前剪枝:边生长边剪枝。每次建立新的分支之前,检查是否有必要继续进行分支,是否当前结点中的子样本数量是否已经足够小不应该再分了?是否还有特征能够用来进行分支?分支后是不是准确率反而会下降?基于这三个问题可以控制树没完没了地分支下去。其思想借鉴于贪心算法,降低了过拟合的风险,但是增加了欠拟合风险。

后剪枝:生长结束后剪枝。C4.5使用的是悲观错误剪枝方法,具体理论公式较占篇幅,本人也是刚刚学习,所以直接附上参考:决策树C4.5。在发展中后剪枝有了更多方法,最小错误剪枝技术(MEP),代价复杂度剪枝(CCP)等等,参考自:【机器学习】 树的剪枝策略。

2.实践 2.1代码 V1

既然讲了C4.5,为了实践如何处理连续值继续选用iris数据集,不够完美的是特征全部为连续值。与之前线性模型相比,程序复杂度一下子提升了不少,对于我这种代码菜鸟来说想手动实现还真不容易,费劲写出来的第一版算法并不成功,训练准确率差不多67%,测试准确率50%,应该是模型还不够好。尝试分析了一下原因并总结缺点:

  1. C4.5对于连续值特征,不会像离散值特征那样用一遍就丢弃。计算所有中位划分点并选择完最优划分点后,其余的划分点仍可以在后续的判断中继续使用。但我的算法则是选择了一个最优的划分点就将整个特征抛弃了。我认为这应该是最主要的原因。
  2. 数据本身的分布可能不太适合使用决策树,作为全连续值数据,若连续使用特征会导致决策树需要分很多支,形成很大的过拟合问题,但是若像我一样只用一次,会导致欠拟合问题。
  3. 特征选择使用二分点所以只生成二叉树,在这个数据集上可能选用能多叉划分的决策点比较好,那就需要变换划分方法了。
  4. 没有用剪枝来正则化,不过这个功能在我这最初版本也用不上,我的决策树不深。
  5. 模型也没有对缺失值的处理。不过我觉得没必要,这是特征工程需要做的事情。
"""
Author: Jhll97
"""
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import collections
import numpy as np

iris = load_iris()
data = iris.data
target = iris.target
X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=30)


# 编写决策树类
class DecisionTreeC45(object):
    def __init__(self, x, y):
        self.n = len(y)  # 样本数量
        self.m = x.shape[1]  # 特征维度
        self.features = list(range(self.m))
        self.labels = np.unique(y)  # 标签种类
        self.info_entropy = self.compute_entropy(y)  # 信息熵
        self.root = None

    def compute_entropy(self, sub_label):  # 计算信息熵,特征熵,以及帮助计算条件熵
        # 可以直接利用dataframe的value_counts,这里手动写了
        mp = {i: 0 for i in np.unique(sub_label)}
        for i in range(len(sub_label)):
            mp[sub_label[i]] += 1
        res = 0
        for key in mp:
            prob = mp[key]
            prob /= len(sub_label)
            res += -prob * np.log2(prob)
        return res

    def compute_gain_rate(self, sample, label, features):
        ratio = {}
        for i in features:
            feature = np.sort(sample[:, i])
            feature_et = self.compute_entropy(feature)  # 特征熵
            mids = []  # 储存划分点
            for j in range(sample.shape[0] - 1):
                mids.append((feature[j] + feature[j + 1]) / 2)
            best_rate = 0
            best_point = 0
            for j in range(len(mids)):
                sub_label1 = label[np.where(sample[:, i] <= mids[j])]
                sub_label2 = label[np.where(sample[:, i] > mids[j])]
                n1 = len(sub_label1)
                n2 = len(sub_label2)
                # 以下计算信息增益率
                con_et = n1 / len(label) * self.compute_entropy(sub_label1) + \
                         n2 / len(label) * self.compute_entropy(sub_label2)  # con_et 为根据每个划分点计算出的条件熵
                gain = self.info_entropy - con_et  # gain 为根据每个划分点计算的信息增益
                gain_rate = gain / feature_et  # 信息增益率
                # 寻找最优增益率
                if gain_rate > best_rate:
                    best_rate = gain_rate
                    best_point = mids[j]  # 记录这个划分点信息
            ratio[i] = (best_point, best_rate)  # 记录每个维度的最优信息增益比
        decision_feature, decision_point = self.get_best_ratio(ratio)
        return decision_feature, decision_point, len(features)

    def get_best_ratio(self, ratio):
        decision_feature = 0
        decision_point = 0
        gainrate = 0
        for key, value in ratio.items():
            point, temp = value
            if temp > gainrate:
                gainrate = temp
                decision_feature = key
                decision_point = point
        return decision_feature, decision_point

    def build_tree(self, data, label, least_sample=1, leaf=False):  # 建立决策树
        if len(label) <= least_sample or leaf:  # 如果样本数量达到阈值或被告知叶子结点,分类
            new_node = Node(label, 0, 0, True)
            if not self.root:
                self.root = new_node
            return new_node
        else:
            decision_feature, decision_point, feature_dim = self.compute_gain_rate(data, label, features=self.features)
            new_node = Node(label, decision_feature, decision_point, False)
            if not self.root:
                self.root = new_node
            # 分到划分点左侧的样本
            sub_sample1 = data[np.where(data[:, decision_feature] <= decision_point)]
            sub_label1 = label[np.where(data[:, decision_feature] <= decision_point)]
            # 分到划分点右侧的样本
            sub_sample2 = data[np.where(data[:, decision_feature] > decision_point)]
            sub_label2 = label[np.where(data[:, decision_feature] > decision_point)]
            new_leaf = True
            if feature_dim > 1:
                new_leaf = False
                self.features.remove(decision_feature)
            new_node.left = self.build_tree(sub_sample1, sub_label1, leaf=new_leaf)
            new_node.right = self.build_tree(sub_sample2, sub_label2, leaf=new_leaf)

            return new_node

    def test(self, test_data):
        result = []
        for i in range(len(test_data)):
            cur_node = self.root
            x = test_data[i]
            while not cur_node.is_leaf:     # 在决策树中向下遍历到叶子结点
                if x[cur_node.decision_feature] <= cur_node.decision_point:
                    cur_node = cur_node.left
                else:
                    cur_node = cur_node.right
            result.append(cur_node.label)
        return result

# 编写结点类
class Node(object):
    def __init__(self, y, decision_feature=0, decision_point=0, leaf=False):
        self.is_leaf = leaf  # 默认中间结点
        self.n = len(y)  # 结点样本数量
        self.label = 0  # 叶子结点的分类结果
        self.left = None
        self.right = None
        self.decision_feature = decision_feature
        self.decision_point = decision_point

        if self.is_leaf:  # 叶子结点,分类
            self.label = self.set_label(y)

    def set_label(self, y):
        # 叶子结点最终分类采取众数规则,哪个种类最多选哪个
        mp = collections.defaultdict(int)
        for i in range(self.n):
            mp[y[i]] += 1
        count = 0
        res = 0
        for key, value in mp.items():
            if value > count:
                count = value
                res = key
        return res


dt = DecisionTreeC45(X_train, y_train)
dt.build_tree(X_train, y_train)
# 测试误差
result = dt.test(X_train)
accuracy = collections.Counter(y_train == result)
print(accuracy)

希望后面能改进一些拿出来V2版本,不过嘿嘿有点累了,再说再说。


更新:V1版本发现了一个低级错误,第50,51行将np.where中判断的对象由feature <=(或大于) mids[j] 改为sample[:, i] <=(或大于) mids[j]。训练准确率平均升至73%左右,测试准确率平均升至65%。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/875048.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-13
下一篇 2022-05-13

发表评论

登录后才能评论

评论列表(0条)

保存