【ID3 C4.5 决策树】基于QT/C++实现 可处理连续训练集 可视化图像界面

一、序言

学校工程实践2的题目,基于QT4.8.2以上的版本开发完成,顺手挂在CSDN上。未申请软著,但也不考虑开源,不出售,不经常看CSDN,此篇仅作留恋本科的时光。该软件可以选择ID3决策树和C4.5决策树,均支持多元决策,同时C4.5也支持连续值分类、连续值与离散值混合分类。训练集要求是xlsx格式,最后一行是带决策元素(在可执行文件中有构造样例)。软件包括:多线程读入,展示计算过程、演示生成过程、图形化界面、测试决策树性能。可视化界面是自己写的,背景图片可支持自定义导入到目录中的/background 文件中。大二时做的,写了一两周,就不匿名上网了,认为烂的轻喷。最后第三章附上可运行的exe文件。

二、截图

初始界面:
决策树操作界面文件导入完成
输入完成ID3决策树

在这里插入图片描述
C4.5决策树
在这里插入图片描述测试决策树
在这里插入图片描述

三、执行文件&源码

·可执行文件:
链接: 可执行exe文件

·源码:
不考虑公布

四、部分代码

4.1 数据结构

// 训练集最大边界
static int Rows = 2;
static int Cols = 2;

static const int maxField = 1e3 + 5;
static const int maxTuple = 2e4 + 5;

// 屏幕长宽
static int desktop_width = 1600;
static int desktop_height = 900;

static struct unData {
    // 未处理数据
    QString FieldName; // 字段
    QString typeName; // 数据类型
    QString data[maxTuple]; // 元组
    bool isDispersed; // 是否离散
    char padding[3];
    int Amount; // 有效数量
} pendata[maxField];
Q_DECLARE_METATYPE(unData);

static struct DTree {
    // 决策树本体
    QString root; // 节点属性
    QString branches;  // 分支条件
    QVector<int> child; // 子节点
    bool isRoot = false; // 是否为子节点
    char padding[3];
    int nodeContent[maxTuple]; // 包含哪些元组 1 : 不在该元组的集合
    int attrContent[maxField]; // 包含哪些属性 1 : 不在该元组的属性
    double entropys = 0.0; // 当前节点的熵
    double gain = 0.0;
    double gainrate = 0.0;
    // 绘图位置上 该节点的坐标
    int draw_x = 0;
    int draw_y = 0;
    // 父节点
    int fa_node = 0;
    // 节点深度
    int dep = 0;
} dtree[maxField<<2];

static QVector<int> trainSet[maxTuple]; // 存放处理数据
static QVector<QString> classSet;
static QMap<QString, int> indexDataSet[maxField]; // 存放的属性通过 Map 容器进行构成简单哈希表
// 此步骤在预处理时完成
static int indexData = 0; // 样本的索引

static int visData[maxField]; // 全部属性 : 代表未访问过,或未分裂的属性
static double gainData[maxField]; // 储存信息增益值
static double spiltInfoData[maxField]; // 储存分裂熵值
static double disperSpiltData[maxField]; // 储存分支信息

static QString disNode[maxField]; // 记录分类节点判断
static int disNodeCnt = 0; // 记录分类节点数量
static int nowDisNodeSum[maxField]; // 记录当前节点的数量

static int drawTestWay[maxField];

// 储存 C4.5 树对非离散值的计算
static struct DisData {

    double data;
    QString resData;
} disdata[maxTuple];

static int maxNode = 0; // 当前最大节点

static int draw_flag = 0;

static struct treeQueue{
    // 循环队列
    int Front = 0, Rear = 0;
    int que[maxField];

    void push(int x) {
        if((Rear + 1)%(maxField) == Front)  return;
        que[Rear] = x;
        ++Rear;
    }
    int front() {
        return que[Front];
    }
    void pop(){
        if(Front != Rear)   ++Front;
    }
    bool empty(){
        return Front == Rear?true:false;
    }
    bool full() {
        return Front == (Rear + 1)%maxField?true:false;
    }

} treeq;

4.2 建树

ID3建树:

void RunTree::buildID3(int id) {
    // 构建 ID3 树
    // id 代表当前节点

    // 计算当前根节点 信息熵 Entropy(S)
    //int yesNode = 0; // 为 Yes 的集合数量
    //int noNode = 0; // 为 No 的集合数量
    int totNode = 0; // 总集合数量

    ui -> textBrowser -> insertPlainText("\n当前节点 ➢ " + QString::number(id) + "\n\n");

    memset(nowDisNodeSum, 0, sizeof(nowDisNodeSum)); // 清空

    for(int i = 0; i < Rows; ++i) {

        if(dtree[id].nodeContent[i])    continue; // 不属于该节点的集合
        for(int j = 0; j < disNodeCnt; ++j){
            if(pendata[Cols - 1].data[i] == disNode[j]) {
                ++nowDisNodeSum[j];
                ++totNode;
                break;
            }
        }
    }

    // 判断是否为子节点
    for(int i = 0; i < disNodeCnt; ++i) {

        if(nowDisNodeSum[i] == totNode) {
            // 子节点
            ui -> textBrowser -> insertPlainText(" [结果节点]当前节点为根节点 ➟ " + QString::number(id) + " → 属性为 " + disNode[i] + "\n");
            dtree[id].root = disNode[i];
            dtree[id].isRoot = true;
            return;
        }
    }

    // 计算信息熵
    double entropy_s = 0.0;
    for(int i = 0; i < disNodeCnt; ++i) {
        entropy_s -= (1.0*nowDisNodeSum[i]/totNode)*log2(1.0*nowDisNodeSum[i]/totNode);
    }
    //double entropy_s = - (1.0*yesNode/totNode)*log2(1.0*yesNode/totNode) - (1.0*noNode/totNode)*log2(1.0*noNode/totNode);
    ui -> textBrowser -> insertPlainText("1 ➟ 当前根节点信息熵 (Entropy(S) -> " + QString::number(id) + ") = " + QString::number(entropy_s) + "\n");
    ui -> textBrowser -> insertPlainText("\n");
    dtree[id].entropys = entropy_s;

    // 计算当前根节点 属性信息熵 Entropy(S|T)
    double entropy_s_t = 0.0;
    int attrNode = 0; // 当前属性信息占整个属性的数量
    memset(gainData, 0, sizeof(gainData));

    QMap<QString, int>::iterator iter; // 迭代器
    for(int i = 0; i < Cols - 1; ++i) {

        if(dtree[id].attrContent[i])    continue; // 不属于该节点的属性
        entropy_s_t = 0.0;

        iter = indexDataSet[i].begin(); // 创建迭代器指向当前域
        while(iter != indexDataSet[i].end()) { // 遍历该属性集合

            QString attrName = iter.key();
            //yesNode = 0;
            //noNode = 0;
            attrNode = 0;
            memset(nowDisNodeSum, 0, sizeof(nowDisNodeSum)); // 清空

            for(int j = 0; j < Rows; ++j) {

                if(dtree[id].nodeContent[j])    continue; // 不属于该节点的集合
                if(pendata[i].data[j] == attrName) {

                    for(int k = 0; k < disNodeCnt; ++k) {

                        if(pendata[Cols - 1].data[j] == disNode[k]) {
                            ++nowDisNodeSum[k];
                            ++attrNode;
                            break;
                        }
                    }
                }
            }

            if(attrNode == 0) {

                ui -> textBrowser -> insertPlainText("     ➟ 当前分裂节点不具有 " + attrName + " → (" + QString::number(id) + ") 无关属性值\n");
                iter++;
                continue;
            }

            ui -> textBrowser -> insertPlainText("     ➟ 当前分裂属性值 (Entropy(S|" + attrName + ") → " + QString::number(id) + ") = ");
            // 计算属性值信息熵
            double entropy_ti = 0.0;
            for(int j = 0; j < disNodeCnt; ++j) {
                entropy_ti -= (1.0*nowDisNodeSum[j]/attrNode)*log2(1.0*nowDisNodeSum[j]/attrNode);
            }
            //double entropy_ti = - (1.0*yesNode/attrNode)*log2(1.0*yesNode/attrNode) - (1.0*noNode/attrNode)*log2(1.0*noNode/attrNode);
            ui -> textBrowser -> insertPlainText(QString::number(entropy_ti) + "\n");
            // 计算属性信息熵
            entropy_s_t += (1.0*attrNode/totNode)*entropy_ti;
            iter++;
        }

        gainData[i] = entropy_s_t;
        ui -> textBrowser -> insertPlainText("2 ✒ 当前属性熵 (Entropy(S|" + pendata[i].FieldName + ") → " + QString::number(id) + ") = " + QString::number(entropy_s_t) + "\n\n");
    }

    // 计算信息增益
    double maxGain = -1.0; // 最大熵
    int maxGainFlag = -1; // 最大熵指针

    for(int i = 0; i < Cols - 1; ++i) {

        if(dtree[id].attrContent[i])    continue; // 不属于该节点的属性

        double gain = entropy_s - gainData[i];
        ui -> textBrowser -> insertPlainText("       当前信息增益 (Gain(S|" + pendata[i].FieldName + ") → " + QString::number(id) + ") = " + QString::number(gain) + "\n");
        if(gain > maxGain) {
            // 求最大值
            maxGain = gain;
            maxGainFlag = i;
        }
    }

    ui -> textBrowser -> insertPlainText("\n");

    if(maxGainFlag == -1||(cut_node > 0&&dtree[id].dep >= 3)) {
        // 分到最后一个元素 结果却是混沌
        ui -> textBrowser -> insertPlainText(" [结果节点] ⇇ 当前为混沌根节点 - 不做分裂\n");
        dtree[id].root = "Chaos";
        dtree[id].isRoot = true;
        if(cut_node > 0)    --cut_node;
        return;
    }

    ui -> textBrowser -> insertPlainText("3 ➟ 综上所述 最大信息增益为 (Gain(S|" + pendata[maxGainFlag].FieldName + ") → " + QString::number(id) + ") = " + QString::number(maxGain) + "\n\n");
    ui -> textBrowser -> insertPlainText("4 ➟ 所以当前节点选择 " + pendata[maxGainFlag].FieldName +" 作为分类属性\n");

    // 开始准备构建子节点

    dtree[id].root = pendata[maxGainFlag].FieldName;
    dtree[id].gain = maxGain;

    iter = indexDataSet[maxGainFlag].begin(); // 开始遍历要分裂的属性
    while(iter != indexDataSet[maxGainFlag].end()) {

        ++maxNode; // 赋予节点编号
        QString attrName = iter.key(); // 分类属性类型

        memcpy(dtree[maxNode].nodeContent, dtree[id].nodeContent, sizeof(int)*maxTuple); // 子节点继承父节点属性
        memcpy(dtree[maxNode].attrContent, dtree[id].attrContent, sizeof(int)*maxField); // 子节点继承父节点属性
        dtree[maxNode].attrContent[maxGainFlag] = 1; // 已经分裂的属性

        int nullFlag = 1; // 检测该子节点是否还有集合

        for(int i = 0; i < Rows; ++i) {
            // 挑出该分裂子节点的属性
            if(dtree[maxNode].nodeContent[i])   continue;
            if(pendata[maxGainFlag].data[i] != attrName)    dtree[maxNode].nodeContent[i] = 1;
            if(pendata[maxGainFlag].data[i] == attrName)    nullFlag = 0;
        }

        if(nullFlag) {
            // 子节点里无该项元素 不分裂

            memset(dtree[maxNode].attrContent, 0, sizeof(dtree[maxNode].attrContent));
            memset(dtree[maxNode].nodeContent, 0, sizeof(dtree[maxNode].nodeContent));
            iter++;
            --maxNode;
            continue;
        }

        dtree[id].child.push_back(maxNode); // 放入子节点编号
        dtree[maxNode].branches = attrName; // 放入子节点属性
        dtree[maxNode].dep = dtree[id].dep + 1;

        ui -> textBrowser -> insertPlainText("\n\n   ☯ 当前分裂属性值: " + pendata[maxGainFlag].FieldName + " - " + attrName + "\n");
        buildID3(maxNode); // 递归
        iter++;
    }

}

void RunTree::buildID3Tree() {
    // 开始构建 ID3 树
    ui -> textBrowser -> insertPlainText("开始构建ID3决策树:\n\n");
    ui -> textBrowser -> insertPlainText("(一) ID3决策树 计算公式:\n\n");
    ui -> textBrowser -> insertPlainText("  (1)信息熵 计算公式为:  Entropy(S) = -∑Pi * log2Pi;\n");
    ui -> textBrowser -> insertPlainText("  (2)属性值信息熵 计算公式为:  Entropy(Ti​) = -∑Pi * log2Pi;\n");
    ui -> textBrowser -> insertPlainText("  (3)属性信息熵 计算公式为:  Entropy(S|T) = ∑((Si/S) * Entropy(Ti​));\n");
    ui -> textBrowser -> insertPlainText("  (4)信息增益 计算公式为:  Gain(S) = Entropy(S) - Entropy(S|T);\n");
    ui -> textBrowser -> insertPlainText("\n");

    memset(visData, 0, sizeof(visData)); // 全部属性置 0 : 代表未访问过,或未分裂的属性
    ui -> textBrowser -> insertPlainText("(二) ID3决策树参数:\n\n");

    int computId = 1;
    for(int i = 0; i < Cols; ++i) {

        if(pendata[i].isDispersed == false) {
            // ID3 树无法处理非离散值
            dtree[0].attrContent[i] = 1;
            continue;
        }
        if(visData[i] == 0) {

            visData[i] = 1; // 暂时访问该属性
            ui -> textBrowser -> insertPlainText("  (" + QString::number(computId) + ") " + pendata[i].FieldName + " :\n\n");
            if(i == Cols - 1)   ui -> textBrowser -> insertPlainText("    分类数据类型: " + pendata[i].typeName + "\n");
            else    ui -> textBrowser -> insertPlainText("    数据类型: " + pendata[i].typeName + "\n");
            ui -> textBrowser -> insertPlainText("    离散类型: 离散型\n\n");


            QMap<QString, int>::iterator iter; // 迭代器
            iter = indexDataSet[i].begin();
            while(iter != indexDataSet[i].end()) {
                ui -> textBrowser -> insertPlainText("     待分列属性 (" + iter.key() + " -> " + QString::number(iter.value()) + ")\n");

                iter++;
            }

            ++computId;
            ui -> textBrowser -> insertPlainText("\n");
        }
    }

    ui -> textBrowser -> insertPlainText("\n");
    ui -> textBrowser -> insertPlainText("(三) ID3决策树 计算过程:\n\n");

    buildID3(0); // 开始 DFS 建树

}

C4.5 代码涉及到连续值就太长了,不放出来了。其基本思路等同于ID3。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值