决策树学习 -- ID3算法和C4.5算法(C++实现)

前言

在学习西瓜书的时候,由于书本讲的大多是概念,所以打算用C++实现它的算法部分(至于python和matlab实现,实现简单了很多,可以自己基于C++代码实现)。至于测试数据,采用了书中关于西瓜的数据集。

什么是决策树

首先,决策树(也叫做分类树或回归树)是一个十分常用的分类方法,在机器学习中它属于监督学习的范畴。由于决策树是基于树结构来决策的,所以学习过数据结构的人,相对来说会比较好理解。

一般的,一颗决策树包含一个根结点,若干个内部结点和若干个叶结点;叶子结点对应于决策结果,其他每个结点则对应一个属性测试;每个结点包含的样本集合根据属性测试的结果被划分到子结点中;根节点包含样本全集。从根结点到每个叶结点的路径对应了一个判定测试序列。决策树学习的目的是为了产生一颗泛化能力强,即处理未见示例能力强的决策树,其基本流程遵循简单且直观的“分而治之”策略。(《机器学习(周志华)》)

例如:书中的关于西瓜问题的一颗决策树

决策树学习 -- ID3算法和C4.5算法(C++实现)_第1张图片

从这个图可以看出,色泽青绿,根蒂蜷缩,敲声浊响的西瓜为好瓜

关键在于哪里?

随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的“纯度”越来越高。这句话的意思是说,在树的相同层里,要么好瓜这一类别越多越好,要么坏瓜这一类别越多越好。

例如上图中,西瓜根蒂的分类有:蜷缩,稍蜷和硬挺。

这里可以打一个比方:足球比赛中,一般会把多个球队分在同一个小组,如果一个小组中的各支球队实力都相当,那么小组出线的可能性就难于预测,如果各个球队的实力差距相当悬殊,那么实力较强的球队出线的可能性会相当大。

同样的道理我们希望每一个分支下面的类别(即好瓜和坏瓜)尽可能属于同一类别,如果同一类别更多,换句话说就是该类别实力比较强,依据该属性(根蒂)分类得出的结果也就更加可靠(明确)。

基本概念

信息熵(information entropy)

在信息论与概率统计中,熵是表示随机变量不确定性的度量,熵越大,随机变量的不确定性就越大。信息熵是度量样本集合“纯度”最常用的一种指标。假定当前样本集合D第k类样本所占比例为

pk(k=12...|γ|)
,则 D的信息熵的定义为

Ent(D)=k=1|γ|pklog2(pk)

注意:

pk<1

log2(pk)>0

y=pklog2(pk)
图像可以知道

决策树学习 -- ID3算法和C4.5算法(C++实现)_第2张图片

当超过0.4的时候,随着

pk
的增大,y的值越小

结论:Ent(D)的值越小,则D的纯度越高

信息增益(information gain)

信息增益表示得到特征X的信息而使得类Y的信息的不确定性减少的程度。假定离散属性a有V个可能值

a1,a2,...,aV
,若使用a来对样本集D进行划分,则会产生V个分支结点,其中第v个分支结点包含了D中所有在属性a上取值为
av
的样本,记为
Dv
。其中由于不同的分支结点所包含的样本数不同,所以
|Dv||D|
表示分支的权重,即样本数越多的分支结点的影响越大。

决策树学习 -- ID3算法和C4.5算法(C++实现)_第3张图片

于是可计算出用属性a对样本集D进行划分所获得的“信息增益”

Gain(D,a)=Ent(D)v=1V|Dv||D|Ent(D)

一般而言,信息增益越大,则意味着使用属性a来划分所获得的“纯度提升”越大。著名的ID3决策树学习算法就是更具信息增益为准则来选择划分属性的。

由于信息增益准则对对可取值数目较多的属性有所偏好,为减少这种偏好可能带来的不利影响著名的C4.5决策树算法不直接使用信息增益,而是使用“增益率”来选择最优划分属性。

增益率(gain ratio)

Gainratio(D,a)=Gain(D,a)IV(a)

其中

IV(a)=v=1V|Dv||D|log2(|Dv||D|)

IV(a)”称为属性a的“固有值”,属性a的可能性数目越多(即V越大),则IV(a)的值通常会越大。

需要注意的是,增益率准则对可能取值数目较少的属性有所偏爱,因此,C4.5算法并不是直接选择增益率最大的候选划分属性,而是使用了一个启发式:先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的

数据集

编号 色泽 根蒂 敲声 纹理 脐部 触感 好瓜
1 青绿 蜷缩 浊响 清晰 凹陷 硬滑
2 乌黑 蜷缩 沉闷 清晰 凹陷 硬滑
3 乌黑 蜷缩 浊响 清晰 凹陷 硬滑
4 青绿 蜷缩 沉闷 清晰 凹陷 硬滑
5 浅白 蜷缩 浊响 清晰 凹陷 硬滑
6 青绿 稍蜷 浊响 清晰 稍凹 软粘
7 乌黑 稍蜷 浊响 清糊 稍凹 软粘
8 乌黑 稍蜷 浊响 清晰 稍凹 硬滑
9 乌黑 稍蜷 沉闷 清糊 稍凹 硬滑
10 青绿 硬挺 清脆 清晰 平坦 软粘
11 浅白 硬挺 清脆 模糊 平坦 硬滑
12 浅白 蜷缩 浊响 模糊 平坦 软粘
13 青绿 稍蜷 浊响 稍糊 凹陷 硬滑
14 浅白 稍蜷 沉闷 稍糊 凹陷 硬滑
15 乌黑 稍蜷 浊响 清晰 稍凹 软粘
16 浅白 蜷缩 浊响 模糊 平坦 硬滑
17 青绿 蜷缩 沉闷 稍糊 稍凹 硬滑

有兴趣的话可以自己算下上面给的公式,这里直接套用书的算式。

利用该数据预测是不是好瓜,显然

|γ|=2
,其中正例占
p1=817
,反例占
p2=917

所以:

Ent(D)=k=1|2|pklog2(pk)=(817log2(817)+917log2(917))=0.998

ID3算法

基于信息增益生成的决策树图

决策树学习 -- ID3算法和C4.5算法(C++实现)_第4张图片

伪代码:

这里直接使用了书上的:

输入:训练集 D = {(x1, y1), (x2, y2), ... , (xm, ym)}
          属性集 A = {a1, a2, ... , ad}.

过程:函数 TreeGenerate(D, A)
                生成结点node;
                if D中样本全属于同一类别C then
                    将node标记为C类叶结点; return
                end if
                if A == ∅ (OR D中样本在A上取值相同) then
                    将node标记为叶结点,其类别标记为D中样本数最多的类; return
                end if
                从A中选择最优划分属性a*;
                for a* 的每一个值 a*_v do
                    为node生成一个分支;令Dv表示D中在a*上取值为a*_v的样本子集;
                    if Dv 为空 then
                        将分支结点标记为叶结点,其类别标记为D中样本最多的类;then
                    else
                        以TreeGenerte(Dv, A \ {a*})为分支结点
                    end if
                end for

输出:以node为根节点的一颗决策树

首先我们来理解下训练集中的x和y分别代表什么

  • 训练集D:表示上面所给的数据集
  • (x, y) : 表示一个训练数据元素。例如:上面的数据集中有17个数据元素
    • x:表示数据元素中除了目标属性的属性集(是一个向量)。(例如编号为1的西瓜:青绿,蜷缩,浊响,清晰,凹陷,硬滑
    • y:表示目标属性。(例如上面数据集中的好瓜属性,编号为1的好瓜属性为
  • 属性集A:表示除目标属性外,供算法学习测试使用的其它属性。(例如:色泽,根蒂,敲声,纹理,脐部,触感

由于C++不像matlab这类语言,所以对数据的处理相对来说会麻烦很多。我将数据进行修改,对应为

  • 色泽 : color
    • 青绿 : darkgreen
    • 乌黑 : jetblack
    • 浅白 : lightwhite
  • 根蒂 : pedicle
    • 蜷缩 : curled
    • 稍蜷 : slightlycurled
    • 硬挺 : stiffened
  • 敲声 : sound
    • 浊响 : turbid
    • 沉闷 : dull
    • 清脆 : crisp
  • 纹理 : texture
    • 清晰 : clear
    • 稍糊 : slightblur
    • 模糊 : blur
  • 脐部 : umbilical
    • 凹陷 : sunken
    • 稍凹 : slightlysunken
    • 平坦 : flat
  • 触感 : touch
    • 硬滑 : hardship
    • 软粘 : softsticky

C++ 实现

// ID3决策算法 -- 西瓜决策树
TreeRoot TreeGenerate(TreeRoot pTree,                                       // 决策树
                      std::vector datas,                            // 训练集
                      std::vector<std::string> attributes,                          // 属性集
                      std::map<std::string, std::vector<std::string>> map_attr) // 表示属性之间的映射关系
                                                                                                                // 例如:颜色 对应 青绿,乌黑,浅白
{
    if (belongs_same_label(datas, "yes")) {
        // All samples are positive.
        pTree->attribute = "yes";
        return pTree;
    }
    else if (belongs_same_label(datas, "no")) {
        // All samples are negative.
        pTree->attribute = "no";
        return pTree;
    }
    else {
        if (attributes.empty()) {
            pTree->attribute = majority_of_category(datas);
            return pTree;
        }
        else {
            std::pair<std::string, std::vector<std::string>> optimal_attrs =  \
                    optimal_attribute(datas, attributes, map_attr);
            pTree->attribute = optimal_attrs.first;
            for (auto aptimal_attr : optimal_attrs.second) {
                Node* new_node = new Node();
                new_node->edgeValue = aptimal_attr;

                std::vector new_datas = remain_watermelon_datas(
                        datas, aptimal_attr, optimal_attrs.first);
                if (new_datas.empty()) {
                    new_node->attribute = majority_of_category(datas);
                    // return pTree;
                }
                else {
                    std::vector<std::string> new_attributes;
                    for(auto train_attribute : attributes) {
                        if(train_attribute.compare(optimal_attrs.first)) {
                            new_attributes.push_back(train_attribute);
                        }
                    }
                    TreeGenerate(new_node, new_datas, new_attributes, map_attr);
                }

                pTree->childs.push_back(new_node);
            }
        }
    }

    return pTree;
}

效果图

决策树学习 -- ID3算法和C4.5算法(C++实现)_第5张图片

完整代码:ID3算法

C4.5算法

伪代码

由于ID3算法和C4.5算法的主要区别在于如何选择最优划分属性

a
,所以伪代码都是一样的

输入:训练集 D = {(x1, y1), (x2, y2), ... , (xm, ym)}
          属性集 A = {a1, a2, ... , ad}.

过程:函数 TreeGenerate(D, A)
                生成结点node;
                if D中样本全属于同一类别C then
                    将node标记为C类叶结点; return
                end if
                if A == ∅ (OR D中样本在A上取值相同) then
                    将node标记为叶结点,其类别标记为D中样本数最多的类; return
                end if
                从A中选择最优划分属性a*;
                for a* 的每一个值 a*_v do
                    为node生成一个分支;令Dv表示D中在a*上取值为a*_v的样本子集;
                    if Dv 为空 then
                        将分支结点标记为叶结点,其类别标记为D中样本最多的类;then
                    else
                        以TreeGenerte(Dv, A \ {a*})为分支结点
                    end if
                end for

输出:以node为根节点的一颗决策树

C++ 实现

这里主要写出划分最优

a
那部分的算法,即optimal_attribute函数

// 选出最优属性a*
// 返回值为一个std::pair类型,first表示该属性,second表示该属性的映射关系
std::pair<std::string, std::vector<std::string>> optimal_attribute(std::vector& datas,
        std::vector<std::string>& attributes,
        std::map<std::string, std::vector<std::string>> map_attr)
{
    std::map<std::string, double> map_gains;
    std::map<std::string, double> map_gains_ratio;
    for (auto attribute : attributes) {
        map_gains[attribute] = calculate_information_gain(datas, attribute, map_attr);
        map_gains_ratio[attribute] = calculate_information_gain_ratio(datas, attribute, map_attr);
    }

    // Sort the information gain and select the attribute of the maximum
    // information gain.The biggest value is in the first.
    //
    std::vector<std::pair<std::string, double>> vec_map_gains(map_gains.begin(), map_gains.end());
    std::vector<std::pair<std::string, double>> vec_map_gains_ratios(map_gains_ratio.begin(), map_gains_ratio.end());
    auto compare_x_y = [](const std::pair<std::string, double> x, const std::pair<std::string, double> y) {
        return x.second > y.second;
    };
    std::sort(vec_map_gains.begin(), vec_map_gains.end(), compare_x_y);
    std::sort(vec_map_gains_ratios.begin(), vec_map_gains_ratios.end(), compare_x_y);

    // Find information gain greater than average.
    //
    std::vector<std::string> vec_map_gains_name;
    int vec_map_gains_size = vec_map_gains.size() / 2;
    for (int i = 0; i < vec_map_gains_size; ++i) {
        vec_map_gains_name.push_back(vec_map_gains[i].first);
    }

    std::string best_attribute;
    for (auto vec_map_gains_ratio : vec_map_gains_ratios) {
        if (std::find(vec_map_gains_name.begin(), vec_map_gains_name.end(), vec_map_gains_ratio.first)
                != vec_map_gains_name.end()) {
            best_attribute = vec_map_gains_ratio.first;
            break;
        }
    }

    if (!best_attribute.empty()) {
        auto search = map_attr.find(best_attribute);
        if (search != map_attr.end()) {
            return std::make_pair(search->first, search->second);
        }
        else {
            return std::make_pair(std::string(""), std::vector<std::string>());
        }
    }
    else {
        return std::make_pair(std::string(""), std::vector<std::string>());
    }
}

其中计算

Gain(D,a)
Gainratio(D,a)
的代码为:

// 计算Gain(D, a)
double calculate_information_gain(std::vector& datas,
                                  std::string attribute,
                                  std::map<std::string, std::vector<std::string>> map_attr)
{
    // Gain(D, a) = Ent(D) - ∑(v=1, V) |D^|/|D| * Ent(D)
    //
    double gain = calculate_information_entropy(datas);
    std::vector<std::string> attrs = map_attr[attribute];

    for (auto attr : attrs) {
        gain -= proportion(datas, attr, attribute) * calculate_information_entropy(datas, attr, attribute);
    }

    return gain;
}

// 计算Gain_ratio(D, a)
double calculate_information_gain_ratio(std::vector& datas,
                                        std::string attribute,
                                        std::map<std::string, std::vector<std::string>> map_attr)
{
    // Gain_ratio(D, a) = Gain(D, a) / IV(a)
    //
    double gain = calculate_information_gain(datas, attribute, map_attr);

    double iv = 0;
    std::vector<std::string> attrs = map_attr[attribute];
    for (auto attr : attrs) {
        iv -= proportion(datas, attr, attribute) * log2(proportion(datas, attr, attribute));
    }

    double gain_ratio = gain / iv;

    return gain_ratio;
}

完整代码:C4.5算法


另:决策树里还有剪枝处理(一种解决“过拟合”的方法),使用连续与缺失值,多变量决策树等等。可以自行深入了解

你可能感兴趣的:(机器学习)