决策树(Decision Tree)(附C++代码)

系列文章目录

提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
TODO:写完再整理


前言

认知有限,望大家多多包涵,有什么问题也希望能够与大家多交流,共同成长!

本文先对**判定树(决策树)**做个简单的介绍,具体内容后续再更,其他模块可以参考去我其他文章


提示:以下是本篇文章正文内容

一、设计基于固定规则的决策树【适用于逻辑分类】

(1)原理

固定规则的决策树设计原则类似if-this-then-that,适用于简单的逻辑分类判定的业务场景,固定规则的决策树一定是过拟合的,判断的结果是唯一的
在这里插入图片描述

举例:某人决定是否应该在特定的一天打棒球

这棵树从上往下,首先提出一个问题:今天的天气预期如何?接下来会有三种可能的答案:晴;阴;雨。
1. if 天气=晴天,那么判断湿度如何
1. if 湿度高,then 取消
2. if 湿度低,then 去玩
2. if 天气=阴天,then 去
3. if 天气=下雨,then 取消

(2)C++实现

基于固定逻辑规则的决策树在C++中的实现可以通过定义一系列的规则,然后根据这些规则来构建决策树的节点和分支。以下是一个简化的示例

#include <iostream>
#include <string>
#include <vector>
#include <map>

// 决策树节点
struct DecisionTreeNode {
    std::string feature; // 特征名称
    std::map<std::string, DecisionTreeNode*> rules; // 规则映射到子节点
    std::string result; // 如果这个节点是叶节点,存储结果

    // 构造函数
    DecisionTreeNode() {}
    ~DecisionTreeNode() {
        for (auto& rule : rules) {
            delete rule.second;
        }
        rules.clear();
    }

    // 添加规则到节点
    void addRule(const std::string& featureValue, DecisionTreeNode* node) {
        rules[featureValue] = node;
    }

    // 设置叶节点结果
    void setResult(const std::string& res) {
        result = res;
    }

    // 根据特征值找到对应的子节点
    DecisionTreeNode* findChild(const std::string& featureValue) {
        auto it = rules.find(featureValue);
        if (it != rules.end()) {
            return it->second;
        }
        return nullptr;
    }
};

// 构建决策树
DecisionTreeNode* buildDecisionTree() {
    auto root = new DecisionTreeNode();

    // 假设我们有以下固定逻辑规则
    // 如果 feature1 == "value1":
    //   结果 "A"
    //   否则:
    //     如果 feature2 == "value2":
    //       结果 "B"
    //     否则:
    //       结果 "C"

    auto nodeA = new DecisionTreeNode();
    nodeA->setResult("A");

    auto nodeB = new DecisionTreeNode();
    nodeB->setResult("B");

    auto nodeC = new DecisionTreeNode();
    nodeC->setResult("C");

    // 构建子树
    root->addRule("value1", nodeA);
    DecisionTreeNode* notValue1 = new DecisionTreeNode();
    notValue1->addRule("value2", nodeB);
    notValue1->setResult("C");
    root->addRule("value2", notValue1); // 注意这里 value2 是 feature2 的值

    return root;
}

// 决策树预测
std::string predict(DecisionTreeNode* root, const std::map<std::string, std::string>& features) {
    DecisionTreeNode* currentNode = root;
    while (currentNode) {
        auto featureValuePair = features.find(currentNode->feature);
        if (featureValuePair != features.end()) {
            currentNode = currentNode->findChild(featureValuePair->second);
            if (!currentNode) {
                // 没有找到匹配的规则
                break;
            }
        } else {
            // 缺少必要的特征
            break;
        }
    }
    return currentNode ? currentNode->result : "Unknown";
}

int main() {
    auto root = buildDecisionTree();

    // 假设我们有以下特征值
    std::map<std::string, std::string> features = {
        {"feature1", "value1"},
        {"feature2", "value3"}
    };

    std::string prediction = predict(root, features);
    std::cout << "Prediction: " << prediction << std::endl;

    // 清理决策树内存
    delete root;

    return 0;
}

二、设计基于离散变量的分类决策树【适用于离散数据分类】

(1)决策树/判定树简介

决策树(Decision Tree)是一种以树形数据结构来展示决策规则和分类结果的模型。决策树(Decision Tree)一种归纳算法,将看似无序、杂乱的已知数据,通过某种固定规则将它们转化成决策结果。

判定决策的过程是有优先级的
每一条从根结点到叶子结点(最终分类结果)的路径都代表一条决策的规则,且越靠近根节点的分类结果贡献越大

一般来说,一棵决策树包含一个根节点,若干个内部结点和若干个叶结点。叶结点对应于决策结果,其他每个结点对应于一个属性测试。每个结点包含的样本集合根据属性测试的结果划分到子结点中,根结点包含样本全集,从根结点到每个叶结点的路径对应了一个判定的测试序列。决策树学习的目的是产生一棵泛化能力强,即处理未见示例强的决策树。

使用决策树进行决策的过程就是从根节点开始,测试待分类项中相应的特征属性,并按照其值选择输出分支,直到到达叶子节点,将叶子节点存放的类别作为决策结果。

(2)设计决策树的 3 个步骤

在这里插入图片描述

1、步骤一:样本数据处理&特征选择

1)【可选】样本数据处理–离散化、线性化、缺失数据补充等等

样本属性于样本分类的定义
在这里插入图片描述

样本属性:纹理、触感、色泽,其中每个属性有若干个可取值,比如纹理(清晰,稍糊,模糊),色泽(青绿,乌黑,浅白),触感(硬滑,软粘)

样本分类:好瓜和坏瓜。

离散化
我们之前使用的例子都是离散型数据,比如纹理(清晰,稍糊,模糊),色泽(青绿,乌黑,浅白),触感(硬滑,软粘),实际上我们的数据还有可能是连续值,比如西瓜的含糖率是0-1之间的连续值。这种情况下,属性的可取值无穷多,就无法直接划分节点,需要先将连续值离散化。

二分法策略进行离散
原理:
给定样本集D和连续属性a,假定a在D上出现了n个不同的取值,将这些值从小到大进行排序,记为{a1,a2,…,an}。基于划分点t可将D分为子集Dt-和Dt+,其中Dt-包含那些在属性a上取值不大于t的样本,而Dt+包含那些在属性a上取值大于t的样本。显然,对相邻的属性取值ai和ai+1来说,t在区间[ai,ai+1]中取任意值所产生的划分结果相同,我们就把区间[ai,ai+1]的中位点作为候选划分点好了。

划分点公式:
在这里插入图片描述

n-1个候选划分点
定义看着如此麻烦,其实很简单,比如属性a的取值为{1.2, 2, 4,6, 7.0},我们选择两两值取中位数,就得到{(1.2+2)/2, (2+4.6)/2, (4.6+7.0)/2}三个候选点。

2)特征选择

特征选择决定了使用哪些特征来做判断。在训练数据集中,每个样本的属性可能有很多个,不同属性的作用有大有小。因而特征选择的作用就是筛选出跟分类结果相关性较高的特征,也就是分类能力较强的特征。
在特征选择中通常使用的准则是:信息增益。

A、算法一:信息熵(原理:最大程度的找样本之间的差异(寻找有序的低度熵信息样本)

信息熵的介绍
结合到现实生活中,如果某些事情令人困惑和混乱(即具有高熵),那么对该事物的理解就会是模糊的,不清楚的或不纯的。
信息熵描述的是随机变量的不确定性(也就是混乱程度)
信息熵能够表示数据的混乱程度。缺失大量信息的东西被认为是无序的(即具有高度熵),反之则是低度熵。

假设某随机变量的概率分布为:
在这里插入图片描述

则它的信息熵计算公式为:
在这里插入图片描述

信息熵计算公式可以体现出随机变量的不确定性

概念举例
下图是一个只有2个取值的随机变量,假如取值只有0和1,则P(X=0)=p,P(X=1)=1-p,其中p是x=0的概率。信息熵为H§=-plogp-(1-p)log(1-p),我们画一个p在0-1的变过过程,H§随着变化的曲线:
在这里插入图片描述

由上图可以看出,当p=0.5的时候,H§达到最大值。因为p=0或p=1的时候,X只有一种可能性,也就是X是确定的,因此熵最小,而随着p接近0.5的过程中,变量X的不确定性就越来越大,我们计算出来的熵也是越来越大,与实际相符。

应用举例
(1)计算划分前D的信息熵(有的地方用Ent(D),有的用H(D))
在这里插入图片描述

这里y=2,因为只有好瓜和坏瓜两种分类。pi表示第i类别的样本数占总样本数D的比例。
(2)计算划分后的子样本集的信息熵
在这里插入图片描述

其中v是该属性的可取值的数量,比如划分属性为色泽,则v=3。Di表示该属性第i个值的样本数。相当于用色泽划分样本集D,样本集D中色泽=青绿的样本数。|Di|/|D|可以想象成一个权重。H(Di)的计算方法同计算H(D)的。
(3)计算信息增益
在这里插入图片描述

信息增益就是划分前的信息熵减去划分后的信息熵。其中a代表的此次划分中所使用的属性。
如果决策树使用信息增益准则来选择最优划分属性的话,在划分前会针对每个属性都计算信息增益,选择能使得信息增益最大的属性作为最优划分属性。

优缺点分析:
信息增益虽然在理论上可以找到最优的划分属性,但在某些情况下会存在问题。信息增益比较偏好可取值较多的属性,比如我们的样本有一个属性叫序号,每一个样本都具有一个单独的序号,因此使用序号划分后,每个子结点只有一个样本,熵为0。这样的话信息增益最大,算法就会以此属性作为最优划分属性。这显然与我们的意愿不同。

决策树生长的过程,也就是信息熵逐步递减的过程,当决策树生长足够深,数据集中的各个样本都被细致的划分为一个类别是,信息熵为0
在这里插入图片描述

在上面的树中,你可以看到起始点的熵为0.918,而停止点的熵为0.这棵树以高信息增益和低熵结束,这正是我们想要的。

B、算法二:增益比(原理:均值找样本之间的差异)

增益比就是为了矫正信息增益偏好的问题。增益比偏好取值较少的属性。
在这里插入图片描述

其中:在这里插入图片描述

增益比就是信息增益除以IV(a),IV(a)是属性a的固有属性,当属性a可取值增多的时候,IV(a)一般也增大,因此在一定程度上能抑制信息增益偏好取值多的属性的特点

C、算法三:基尼指数(原理:追求数据集中随机选择的数据点可能被错误分类的频率最小化)

基尼指数公式
在这里插入图片描述

基尼指数就是在样本集中随机抽出两个样本不同类别的概率。当样本集越不纯的时候,这个概率也就越大,即基尼指数也越大。这个规律与信息熵的相同

公式举例
以刚才的只有两个取值的随机变量为例,我们这次纵坐标除了有信息熵外,再加上基尼指数。
在这里插入图片描述

可以看出,基尼指数与信息熵虽然值不同,但是趋势一致。同样的,使用基尼指数来选择最优划分属性也是对比不同属性划分后基尼指数的差值,选择使样本集基尼指数减小最多的属性。

应用举例
(1)计算划分前的基尼指数
在这里插入图片描述

(2)计算划分前样本集D的基尼指数减去划分后子样本集Di的基尼指数加权和
在这里插入图片描述

分类属性的数量约束

(1)ID3、C4.5划分时,一个节点可以划分为多个子结点,子结点数量根据属性可取值的数量决定
(2)CART决策树是严格的二叉树结构,就是说1个节点最多划分为2子结点。
在这里插入图片描述

这是ID3和C4.5可以生成的决策树,比如根节点根据属性(纹理)划分成了3个子结点。如果是CART决策树,它只能生成2个子结点,结果可能就是将红圈圈起的部分合并称为一个节点。你可能会问这样纹理模糊和稍糊的不就分不开了么,不用担心,如果分开效果更好的话,在该节点继续划分的时候就会将纹理的两个取值分开。因为同一个属性可以不只使用一次。

喂样本数据输入,给定样本的属性,就能训练出对应样本的决策树结构。决策树的训练本质是一种统计学的方法

2、步骤二:决策树生成

选择好特征后,就从根节点触发,对节点计算所有特征的信息增益,选择信息增益最大的特征作为节点特征,根据该特征的不同取值建立子节点;对每个子节点使用相同的方式生成新的子节点,直到信息增益很小或者没有特征可以选择为止。

决策树生成的流程(分类问题就是找特性的问题,树形结构从上往下)
在这里插入图片描述

上图就是在生成决策树的过程中经历的步骤。

1、首先从开始位置,将所有数据划分到一个节点,即根节点。
2、然后经历橙色的两个步骤,橙色的表示判断条件:
-若数据为空集,跳出循环。如果该节点是根节点,返回null;如果该节点是中间节点,将该节点标记为训练数据中类别最多的类
-若样本都属于同一类,跳出循环,节点标记为该类别;
3、如果经过橙色标记的判断条件都没有跳出循环,则考虑对该节点进行划分。既然是算法,则不能随意的进行划分,要讲究效率和精度,选择当前条件下的最优属性划分(什么是最优属性,这是决策树的重点,后面会详细解释)
4、经历上步骤划分后,生成新的节点,然后循环判断条件,不断生成新的分支节点,直到所有节点都跳出循环。
5、结束。这样便会生成一棵决策树。

对于每种决策树算法,决策树的基本流程是一样的。不同的是,虽然决策树基本流程中的每一步骤的目的一样,但不同算法实现的方式不同。
在这里插入图片描述

3、步骤三:决策树剪枝

剪枝的主要目的是对抗「过拟合」,通过主动去掉部分分支来降低过拟合的风险。
剪枝处理–适当控制决策树的生长,防止决策树过拟合,泛化能力差

训练决策树的结果
(1)模型过拟合,泛化能力弱
结果一:如果按照我们之前的方法形成决策树后,决策树会无休止的生长,直到训练样本中所有样本都被划分到正确的分类。

结果二:当决策树节点样本过少、节点样本的特征一致,有漏掉的特征,也可能有不准确的特征,导致样本的特征就少,决策树可能使得该结点划分错误。

剪枝就是把决策树里不该生长的枝叶剪掉,也就是不该划分的节点就不要继续划分了。剪枝分为“预剪枝”和“后剪枝”。两种操作在决策树生辰步骤的位置如下图:
在这里插入图片描述

预剪枝:
在决策树生成过程中,对每个结点在划分前先进性估计,若当前结点的划分不能带来决策树泛化性能提升,则停止划分并将当前结点标记为叶结点。它的位置在每一次生成分支节点前,先判断有没有必要生成,如没有必要,则停止划分。

后剪枝:
先从训练集生成一棵完整的决策树(相当于结束位置),然后自底向上的对非叶结点进行考察,若将该结点对应的子树替换为叶结点能带来决策树泛化性能提升,则将该子树替换为叶结点,相当于将子树剪去。值得注意的是,后剪枝时要用到一个测试数据集合,如果存在某个叶子剪去后能使得在测试集上的准确度或其他测度不降低(不变得更坏),则剪去该叶子。
理论上讲,后剪枝生成的决策树要比预剪枝生成的效果好,但是后剪枝在计算复杂度上比预剪枝高。

(3)C++实现

C++中实现决策树通常涉及到构建树结构、选择最佳分割点、递归地构建树以及预测等步骤。下面是一个简化的决策树实现示例,使用了信息增益作为分裂准则,并且仅用于二分类问题。

#include <iostream>
#include <vector>
#include <map>
#include <algorithm>

// 决策树节点
struct TreeNode {
    std::string feature; // 分割特征
    std::vector<std::string> values; // 分割特征的可能值
    std::map<std::string, TreeNode*> children; // 子节点
    double gini; // 该节点的基尼不纯度
    bool is_leaf; // 是否为叶节点
    std::string class_label; // 叶节点的类别标签

    TreeNode() : is_leaf(false) {}
    ~TreeNode() {
        for (auto& child : children) {
            delete child.second;
        }
    }
};

// 计算基尼不纯度
double giniImpurity(const std::vector<int>& counts) {
    double sum = 0.0, giniSum = 1.0;
    for (int count : counts) {
        sum += count;
        giniSum -= std::pow(count / sum, 2);
    }
    return giniSum;
}

// 选择最佳分割点
std::pair<std::string, std::vector<std::string>> bestSplit(const std::vector<std::vector<std::string>>& data) {
    // 这里需要实现选择最佳分割点的逻辑
    // 这通常涉及到遍历所有特征和所有可能的分割点
    // 计算信息增益,并选择信息增益最大的分割点
    // 为了简化,这里返回一个假设的最佳分割点
    return {"feature1", {"value1", "value2"}};
}

// 构建决策树
TreeNode* buildTree(const std::vector<std::vector<std::string>>& data,
                     const std::vector<std::string>& labels) {
    // 计算当前节点的类别分布
    std::map<std::string, int> classCounts;
    for (const auto& label : labels) {
        classCounts[label]++;
    }

    // 如果所有数据点都属于同一类别,返回叶节点
    if (classCounts.size() == 1) {
        TreeNode* leaf = new TreeNode();
        leaf->is_leaf = true;
        leaf->class_label = classCounts.begin()->first;
        return leaf;
    }

    // 选择最佳分割点
    auto split = bestSplit(data);
    TreeNode* node = new TreeNode();
    node->feature = split.first;
    node->values = split.second;

    // 递归地为每个分割值构建子树
    for (const auto& value : split.second) {
        std::vector<std::vector<std::string>> subset;
        std::vector<std::string> subset_labels;
        // 根据分割特征和值筛选数据子集
        for (size_t i = 0; i < data.size(); ++i) {
            if (data[i][0] == value) { // 假设特征在第一列
                subset.push_back(data[i]);
                subset_labels.push_back(labels[i]);
            }
        }
        TreeNode* child = buildTree(subset, subset_labels);
        node->children[value] = child;
    }

    return node;
}

// 预测函数
std::string predict(TreeNode* root, const std::vector<std::string>& sample) {
    TreeNode* node = root;
    while (!node->is_leaf) {
        std::string feature = node->feature;
        std::string value = sample[0]; // 假设特征值在样本的第一列
        node = node->children[value];
    }
    return node->class_label;
}

int main() {
    // 假设的数据集,每行是一个数据点,最后一列是标签
    std::vector<std::vector<std::string>> data = {
        {"value1", "A"},
        {"value2", "B"},
        // ... 更多数据点
    };
    std::vector<std::string> labels; // 存储所有数据点的标签

    // 构建决策树
    TreeNode* root = buildTree(data, labels);

    // 预测新样本
    std::vector<std::string> new_sample = {"value1"};
    std::string class_label = predict(root, new_sample);
    std::cout << "Predicted class: " << class_label << std::endl;

    // 释放决策树占用的内存
    delete root;

    return 0;
}

请注意,这个示例仅用于演示决策树的基本结构和实现思路。实际中,决策树的实现会更加复杂,需要包括特征选择、信息增益计算、树的剪枝、处理连续特征等。在实际应用中,通常会使用成熟的机器学习库,如scikit-learn、XGBoost等,来构建决策树模型。

(4) sklearn工具库实现

调用算法代码实现:
from sklearn.tree import DecisionTreeClassifier #导入分类模型
from sklearn.tree import DecisionTreeRegressor #导入回归模型

model_c = DecisionTreeClassifier(max_depth=10,max_features=5) #括号内加入要人工设定的参数
model_r = DecisionTreeRegressor(max_depth=10,max_features=5) #同样的,加入参数设定值,不仅局限于这几个

model_c.fit(x_train,y_train) #训练分类模型
model_r.fit(x_train,y_train) #训练回归模型

result_c = model_c.predict(x_test) #使用模型预测分类结果
result_r = model_r.predict(x_test) #使用模型预测回归结果

模型可添加的更多的参数及解释如下:

criterion:字符型,可选,规定了该决策树所采用的的最佳分割属性的判决方法,有两种:“gini”,“entropy”,默认为’gini’,

splitter: 字符型,可选(默认=“best”)用于在每个节点选择分割的策略,可填‘best’或’random’,前者在特征的所有划分点中找出最优的划分点。后者是随机的在部分划分点中找局部最优的划分点。默认的"best"适合样本量不大的时候,而如果样本数据量非常大,此时决策树构建推荐"random"

max_depth: int型或None,可选(默认=None)树的最大深度,对于防止过拟合非常有用。如果不输入的话,决策树在建立子树的时候不会限制子树的深度。一般来说,数据少或者特征少的时候可以不管这个值。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。常用的可以取值10-100之间。

min_samples_split: int型float型,可选(默认值=2)分割内部节点所需的最小样本数量;如果是int型,则min_samples_split为最小样本数量。如果是float型,则min_samples_split是分数,ceil(min_samples_split * n_samples)是每个分割的最小样本数。

min_samples_leaf: int型float型,可选(默认值=1)叶节点上所需的最小样本数。如果是int类型,则将min_samples_leaf作为最小值。若是float,那么min_samples_leaf是分数,ceil(min_samples_leaf * n_samples)是每个节点的最小样本数。

min_weight_fraction_leaf:浮点型,可选(默认=0)。叶节点上(所有输入样本的)权值之和的最小加权分数。当没有提供sample_weight时,样品的重量相等。

max_features: int, float, string or None, 可选(默认=None),在寻找最佳分割时需要考虑的特性数量;如果是int类型,则考虑每个分割处的max_features特性。如果是float,那么max_features是一个分数,并且在每个分割中都考虑int(max_features * n_features)特性。如果“auto”,则max_features=sqrt(n_features)。如果“sqrt”,则max_features=sqrt(n_features)。如果“log2”,则max_features=log2(n_features)。如果没有,则max_features=n_features。

class_weight: 指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别。这里可以自己指定各个样本的权重,或者用“balanced”,如果使用“balanced”,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。当然,如果你的样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的"None"

random_state: int型RandomState实例或None,可选(默认=None)如果是int, random_state是随机数生成器使用的种子;如果是RandomState实例,random_state是随机数生成器;如果没有,随机数生成器是np.random使用的RandomState实例。

三、随机森林–目的是减少分类的过拟合和方差 (拓展方法)

随机森林是许多其他模型的集合,单个决策树可以很好地找到特定问题的解决方案,但如果应用于以前从未见过的问题则非常糟糕。俗话说三个臭皮匠赛过诸葛亮,随机森林就利用了多个决策树,来应对多种不同场景。
在这里插入图片描述
在这里插入图片描述

四、设计基于连续变量的回归决策树【适用于连续数据回归】

决策树回归(回归问题就是找共性,树形结构从下往上)
我们都知道决策树不仅可以处理分类问题,也可以处理回归问题。这里我们就简单介绍一下回归问题的解决办法。

(1)决策树回归的流程

决策树回归就是指CART决策树回归,因此形成的数同样是严格的二叉树形式。
在这里插入图片描述

1、开始算法,将样本集划分到根节点。
2、寻找最优划分点。该划分点是指某属性(j)的某个值(s)。
3、使用最优划分点(j,s)将数据划分到子节点,将属性值小于s的划分到一个节点,属性值大于s的到另一个节点。该节点的输出值为节点均值。
4、判断是否满足终止条件,如果满足则结束;否则循环划分。终止条件有很多,比如子节点最小样本数、决策树最大深度等等
5、结束,形成回归树。

1、重点1:寻找最优切分点(寻找属性特性划分点)

通过最小二乘法来寻找最优切分点(寻找属性特性划分点)
在这里插入图片描述

算法会遍历每个属性的每个值(j,s),就假如到属性j和值s的时候,会将样本集划分为2个子集(x<=s和x>=s),我们这里称这两个子集为R1和R2。
首先,我们要先寻找最优的C1使得R1的误差平方和最小,C2使得R2的误差平方和最小。这个数学上很容易证明,当C1和C2分别为子集R1和R2的y的均值的时候成立。上式也可以写成:
在这里插入图片描述

这样对于每一个(j,s),我们都会根据上式得到一个数值。然后我们取能使得上式取值最小的(j,s),作为最优切分点。

2、重点2:计算子节点的输出值

当找到最优切分点(j,s)后,将样本切分为左右两个子节点。子节点的输出值为该节点内的所有样本y的均值。
在这里插入图片描述

五、总结

1、决策树于行为树的应用场景

决策树偏向处理数据得到分类结果,行为树偏向处理流程得到动作指令
决策树(Decision Tree)和行为树(Behavior Tree)是两种不同类型的树形结构,它们在人工智能领域有着不同的应用和目的:

决策树(Decision Tree):
决策树主要用于分类和回归任务。它们通过一系列的问题将数据分割成不同的分支,直到达到一个叶节点,这个叶节点代表了一个决策或分类结果。

行为树(Behavior Tree):
行为树主要用于控制复杂系统中的决策流程,行为树的节点代表行为,每个行为可以是简单的动作或者更复杂的子行为树。行为树通过这些行为的组合来实现复杂任务。

2、分类与回归的认识

分类的目的–找特性
输入样本数据集,根据样本的属性得到样本的分类(预先要根据规则设计好决策树,或者通过训练得到决策树)
对离散变量做决策树

回归的目的–找共性
输入带有类别的样本数据集,根据样本的分类得到样本的属性
对连续变量做决策树

3、常用固定规则的决策树的设计模式

(原则是输入的条件可以是任意类型,输出的结果一定是某个具体任务的判定结果,一般来说输入的条件维度比输出的结果维度要高)
分类树–对离散变量做决策树

(1)多个bool条件输入,bool条件输出
(2)多个int条件输入,多个int条件输出

回归树–对连续变量做决策树

(3)多个double条件输入,多个int条件输出

4、决策树的优缺点

优点:

1、速度快:计算量相对较小,且容易转化成分类规则。只要沿着树根向下一直走到叶,沿途的分裂条件就能够唯一确定一条分类的谓词。
2、准确性高:挖掘出的分类规则准确性高,便于理解,决策树可以清晰的显示哪些字段比较重要。
3、决策树用于建模非线性关系(与线性回归模型和逻辑回归模型相反)
4、决策树可以对分类和连续结果变量进行建模,尽管它们主要用于分类任务(即分类结果变量)
5、决策树很容易理解! 您可以轻松地对它们进行可视化,并准确找出每个分割点发生的情况。 您还可以查看哪些功能最重要
6、非参数学习,不需要设置参数。

缺点:

1、决策树很容易过拟合,很多时候即使进行后剪枝也无法避免过拟合的问题,因此可以通过设置树深或者叶节点中的样本个数来进行预剪枝控制;
决策树容易过拟合。这是因为无论通过单个决策树运行数据多少次
因为只是一系列if-this-then-that语句,所以总是会得到完全相同的结果。
这意味着决策树可以非常精确地适配训练数据,但一旦开始传递新数据,它可能无法提供有用的预测

参考链接

https://blog.csdn.net/GreenYang5277/article/details/104500739
https://blog.csdn.net/kevinjin2011/article/details/125147134
https://easyai.tech/ai-definition/decision-tree/
https://blog.csdn.net/jiaoyangwm/article/details/79525237


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一个简单的决策树实现的C++代码示例: ``` #include<iostream> #include<vector> using namespace std; // 决策树节点 class DecisionTreeNode { public: DecisionTreeNode() : feature(-1), label(-1) {} // 初始化特征和标签为-1 int feature; // 节点的特征 int label; // 节点的标签 vector<int> children; // 子节点 }; // 决策树 class DecisionTree { public: DecisionTree() : root(0) {} // 初始化根节点为0 void Train(const vector<vector<int>>& data, const vector<int>& labels); // 训练决策树 int Predict(const vector<int>& data); // 预测数据的标签 private: int BuildTree(const vector<vector<int>>& data, const vector<int>& labels); // 构建决策树 int root; // 根节点 vector<DecisionTreeNode> tree; // 决策树节点集合 }; // 训练决策树 void DecisionTree::Train(const vector<vector<int>>& data, const vector<int>& labels) { root = BuildTree(data, labels); } // 预测数据的标签 int DecisionTree::Predict(const vector<int>& data) { int node = root; while (tree[node].label == -1) { // 当前节点不是叶子节点 int feature = tree[node].feature; if (data[feature] == 0) { // 根据数据特征判断下一个节点 node = tree[node].children[0]; } else { node = tree[node].children[1]; } } return tree[node].label; // 返回叶子节点的标签 } // 构建决策树 int DecisionTree::BuildTree(const vector<vector<int>>& data, const vector<int>& labels) { int n = data.size(), m = data[0].size(); if (n == 0) { // 数据为空 DecisionTreeNode node; node.label = -1; return tree.size(); } int cnt0 = 0, cnt1 = 0; for (int i = 0; i < n; i++) { // 统计标签数量 if (labels[i] == 0) { cnt0++; } else { cnt1++; } } if (cnt0 == n) { // 所有数据标签都为0 DecisionTreeNode node; node.label = 0; return tree.size(); } if (cnt1 == n) { // 所有数据标签都为1 DecisionTreeNode node; node.label = 1; return tree.size(); } int bestFeature = -1, bestFeatureSplit = -1; double bestGiniIndex = 1e9; // 初始化最小的Gini指数为一个较大的值 for (int i = 0; i < m; i++) { // 计算每个特征的Gini指数 int cnt00 = 0, cnt01 = 0, cnt10 = 0, cnt11 = 0; for (int j = 0; j < n; j++) { if (data[j][i] == 0 && labels[j] == 0) { cnt00++; } else if (data[j][i] == 0 && labels[j] == 1) { cnt01++; } else if (data[j][i] == 1 && labels[j] == 0) { cnt10++; } else { cnt11++; } } double giniIndex = 1.0 - pow(double(cnt00 + cnt01) / n, 2) - pow(double(cnt10 + cnt11) / n, 2); if (cnt00 + cnt01 > 0) { giniIndex -= pow(double(cnt00) / (cnt00 + cnt01), 2) + pow(double(cnt01) / (cnt00 + cnt01), 2); } if (cnt10 + cnt11 > 0) { giniIndex -= pow(double(cnt10) / (cnt10 + cnt11), 2) + pow(double(cnt11) / (cnt10 + cnt11), 2); } if (giniIndex < bestGiniIndex) { // 更新最小的Gini指数和最优特征 bestGiniIndex = giniIndex; bestFeature = i; if (cnt00 + cnt10 > cnt01 + cnt11) { bestFeatureSplit = 0; } else { bestFeatureSplit = 1; } } } DecisionTreeNode node; node.feature = bestFeature; node.label = -1; tree.push_back(node); vector<vector<int>> dataSplit[2]; vector<int> labelsSplit[2]; for (int i = 0; i < n; i++) { // 按最优特征划分数据集 if (data[i][bestFeature] == bestFeatureSplit) { dataSplit[0].push_back(data[i]); labelsSplit[0].push_back(labels[i]); } else { dataSplit[1].push_back(data[i]); labelsSplit[1].push_back(labels[i]); } } node.children.push_back(BuildTree(dataSplit[0], labelsSplit[0])); // 递归构建子树 node.children.push_back(BuildTree(dataSplit[1], labelsSplit[1])); tree.back() = node; return tree.size() - 1; } // 测试 int main() { vector<vector<int>> data{{0, 0}, {0, 1}, {1, 0}, {1, 1}}; vector<int> labels{0, 1, 1, 0}; DecisionTree dt; dt.Train(data, labels); cout << dt.Predict(vector<int>{0, 0}) << endl; // 输出0 cout << dt.Predict(vector<int>{0, 1}) << endl; // 输出1 cout << dt.Predict(vector<int>{1, 0}) << endl; // 输出1 cout << dt.Predict(vector<int>{1, 1}) << endl; // 输出0 return 0; } ``` 该代码实现了一个简单的二分类决策树,通过Gini指数选择最优特征,使用递归构建决策树,最终可以对新的数据进行分类预测。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

盒子君~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值