一、序言
学校工程实践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。