当年实习公司布置了一个任务让写一个决策树,以前并未接触数据挖掘的东西,但作为一个数据挖掘最基本的知识点,还是应该有所理解的。
程序的源码可以点击这里进行下载,下面简要介绍一下决策树以及相关算法概念。
决策树是一个预测模型;他代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分叉路径则代表的某个可能的属性值,而每个叶结点则对应从根节点到该叶节点所经历的路径所表示的对象的值。决策树仅有单一输出,若欲有复数输出,可以建立独立的决策树以处理不同输出。 数据挖掘中决策树是一种经常要用到的技术,可以用于分析数据,同样也可以用来作预测(就像上面的银行官员用他来预测贷款风险)。从数据产生决策树的机器学习技术叫做决策树学习, 通俗说就是决策树。(来自维基百科)
1986年Quinlan提出了著名的ID3算法。在ID3算法的基础上,1993年Quinlan又提出了C4.5算法。为了适应处理大规模数据集的需要,后来又提出了若干改进的算法,其中SLIQ (super-vised learning in quest)和SPRINT (scalable parallelizableinduction of decision trees)是比较有代表性的两个算法,此处暂且略过。
本文实现了C4.5的算法,在ID3的基础上计算信息增益,从而更加准确的反应信息量。其实通俗的说就是构建一棵加权的最短路径Haffman树,让权值最大的节点为父节点。
下面简要介绍一下ID3算法:
ID3算法的核心是:在决策树各级结点上选择属性时,用信息增益(information gain)作为属性的选择标准,以使得在每一个非叶结点进行测试时,能获得关于被测试记录最大的类别信息。
其具体方法是:检测所有的属性,选择信息增益最大的属性产生决策树结点,由该属性的不同取值建立分支,再对各分支的子集递归调用该方法建立决策树结点的分支,直到所有子集仅包含同一类别的数据为止。最后得到一棵决策树,它可以用来对新的样本进行分类。
某属性的信息增益按下列方法计算:
信息熵是香农提出的,用于描述信息不纯度(不稳定性),其计算公式是Info(D)。
其中:Pi为子集合中不同性(而二元分类即正样例和负样例)的样例的比例;j是属性A中的索引,D是集合样本,Dj是D中属性A上值等于j的样本集合。
这样信息收益可以定义为样本按照某属性划分时造成熵减少的期望,可以区分训练样本中正负样本的能力。信息增益定义为结点与其子结点的信息熵之差,公式为Gain(A)。
ID3算法的优点是:算法的理论清晰,方法简单,学习能力较强。其缺点是:只对比较小的数据集有效,且对噪声比较敏感,当训练数据集加大时,决策树可能会随之改变。
C4.5算法继承了ID3算法的优点,并在以下几方面对ID3算法进行了改进:
1) 用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足,公式为GainRatio(A);
2) 在树构造过程中进行剪枝;
3) 能够完成对连续属性的离散化处理;
4) 能够对不完整数据进行处理。
C4.5算法与其它分类算法如统计方法、神经网络等比较起来有如下优点:产生的分类规则易于理解,准确率较高。其缺点是:在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。此外,C4.5只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时程序无法运行。
实现的C4.5数据集合如下:
它记录了再不同的天气状况下,是否出去觅食的数据。
程序引入状态树作为统计和计算属性的数据结构,它记录了每次计算后,各个属性的统计数据,其定义如下:
[cpp] view plain copy print ? struct attrItem { std::vector<int> itemNum; //itemNum[0] = itemLine.size() //itemNum[1] = decision num set<int> itemLine; }; struct attributes { string attriName; vector<double> statResult; map<string, attrItem*> attriItem; }; vector<attributes*> statTree; 决策树节点数据结构如下:
[cpp] view plain copy print ? struct TreeNode { std::string m_sAttribute; int m_iDeciNum; int m_iUnDecinum; std::vector<TreeNode*> m_vChildren; }; 程序源码如下所示(程序中有详细注解):
[cpp] view plain copy print ? #include "DecisionTree.h" int main(int argc, char* argv[]){ string filename = "source.txt"; DecisionTree dt ; int attr_node = 0; TreeNode* treeHead = nullptr; set<int> readLineNum; vector<int> readClumNum; int deep = 0; if (dt.pretreatment(filename, readLineNum, readClumNum) == 0) { dt.CreatTree(treeHead, dt.getStatTree(), dt.getInfos(), readLineNum, readClumNum, deep); } return 0; } /* * @function CreatTree 预处理函数,负责读入数据,并生成信息矩阵和属性标记 * @param: filename 文件名 * @param: readLineNum 可使用行set * @param: readClumNum 可用属性set * @return int 返回函数执行状态 */ int DecisionTree::pretreatment(string filename, set<int>& readLineNum, vector<int>& readClumNum) { ifstream read(filename.c_str()); string itemline = ""; getline(read, itemline); istringstream iss(itemline); string attr = ""; while(iss >> attr) { attributes* s_attr = new attributes(); s_attr->attriName = attr; //初始化属性名 statTree.push_back(s_attr); //初始化属性映射 attr_clum[attr] = attriNum; attriNum++; //初始化可用属性列 readClumNum.push_back(0); s_attr = nullptr; } int i = 0; //添加具体数据 while(true) { getline(read, itemline); if(itemline == "" || itemline.length() <= 1) { break; } vector<string> infoline; istringstream stream(itemline); string item = ""; while(stream >> item) { infoline.push_back(item); } infos.push_back(infoline); readLineNum.insert(i); i++; } read.close(); return 0; } int DecisionTree::statister(vector<vector<string>>& infos, vector<attributes*>& statTree, set<int>& readLine, vector<int>& readClumNum) { //yes的总行数 int deciNum = 0; //统计每一行 set<int>::iterator iter_end = readLine.end(); for (set<int>::iterator line_iter = readLine.begin(); line_iter != iter_end; ++line_iter) { bool decisLine = false; if (infos[*line_iter][attriNum - 1] == "yes") { decisLine = true; deciNum++; } //如果该列未被锁定并且为属性列,进行统计 for (int i = 0; i < attriNum - 1; i++) { if (readClumNum[i] == 0) { std::string tempitem = infos[*line_iter][i]; auto map_iter = statTree[i]->attriItem.find(tempitem); //没有找到 if (map_iter == (statTree[i]->attriItem).end()) { //新建 attrItem* attritem = new attrItem(); attritem->itemNum.push_back(1); decisLine ? attritem->itemNum.push_back(1) : attritem->itemNum.push_back(0); attritem->itemLine.insert(*line_iter); //建立属性名->item映射 (statTree[i]->attriItem)[tempitem] = attritem; attritem = nullptr; } else { (map_iter->second)->itemNum[0]++; (map_iter->second)->itemLine.insert(*line_iter); if(decisLine) { (map_iter->second)->itemNum[1]++; } } } } } return deciNum; } /* * @function CreatTree 递归DFS创建并输出决策树 * @param: treeHead 为生成的决定树 * @param: statTree 为状态树,此树动态更新,但是由于是DFS对数据更新,所以不必每次新建状态树 * @param: infos 数据信息 * @param: readLine 当前在infos中所要进行统计的行数,由函数外给出 * @param: deep 决定树的深度,用于打印 * @return void */ void DecisionTree::CreatTree(TreeNode* treeHead, vector<attributes*>& statTree, vector<vector<string>>& infos, set<int>& readLine, vector<int>& readClumNum, int deep) { //有可统计的行 if (readLine.size() != 0) { string treeLine = ""; for (int i = 0; i < deep; i++) { treeLine += "--"; } //清空其他属性子树,进行递归 resetStatTree(statTree, readClumNum); //统计当前readLine中的数据:包括统计哪几个属性、哪些行, //并生成statTree(由于公用一个statTree,所有用引用代替),并返回目的信息数 int deciNum = statister(getInfos(), statTree, readLine, readClumNum); int lineNum = readLine.size(); int attr_node = compuDecisiNote(statTree, deciNum, lineNum, readClumNum);//本条复制为局部变量 //该列被锁定 readClumNum[attr_node] = 1; //建立树根 TreeNode* treeNote = new TreeNode(); treeNote->m_sAttribute = statTree[attr_node]->attriName; treeNote->m_iDeciNum = deciNum; treeNote->m_iUnDecinum = lineNum - deciNum; if (treeHead == nullptr) { treeHead = treeNote; //树根 } else { treeHead->m_vChildren.push_back(treeNote); //子节点 } cout << "节点-"<< treeLine << ">" << statTree[attr_node]->attriName << endl; //从孩子分支进行递归 for(map<string, attrItem*>::iterator map_iterator = statTree[attr_node]->attriItem.begin(); map_iterator != statTree[attr_node]->attriItem.end(); ++map_iterator) { //打印分支 int sum = map_iterator->second->itemNum[0]; int deci_Num = map_iterator->second->itemNum[1]; cout << "分支--"<< treeLine << ">" << map_iterator->first << endl; //递归计算、创建 if (deci_Num != 0 && sum != deci_Num ) { //计算有效行数 set<int> newReadLineNum = map_iterator->second->itemLine; //DFS CreatTree(treeNote, statTree, infos, newReadLineNum, readClumNum, deep + 1); } else { //建立叶子节点 TreeNode* treeEnd = new TreeNode(); treeEnd->m_sAttribute = statTree[attr_node]->attriName; treeEnd->m_iDeciNum = deci_Num; treeEnd->m_iUnDecinum = sum - deci_Num; treeNote->m_vChildren.push_back(treeEnd); //打印叶子 if (deci_Num == 0) { cout << "叶子---"<< treeLine << ">no" << endl; } else { cout << "叶子---"<< treeLine << ">yes" << endl; } } } //还原属性列可用性 readClumNum[attr_node] = 0; } } /* * @function compuDecisiNote 计算C4.5 * @param: statTree 为状态树,此树动态更新,但是由于是DFS对数据更新,所以不必每次新建状态树 * @param: deciNum Yes的数据量 * @param: lineNum 计算set的行数 * @param: readClumNum 用于计算的set * @return int 信息量最大的属性号 */ int DecisionTree::compuDecisiNote(vector<attributes*>& statTree, int deciNum, int lineNum, vector<int>& readClumNum) { double max_temp = 0; int max_attribute = 0; //总的yes行的信息量 double infoD = info_D(deciNum, lineNum); for (int i = 0; i < attriNum - 1; i++) { if (readClumNum[i] == 0) { double splitInfo = 0.0; //info double info_temp = Info_attr(statTree[i]->attriItem, splitInfo, lineNum); statTree[i]->statResult.push_back(info_temp); //gain double gain_temp = infoD - info_temp; statTree[i]->statResult.push_back(gain_temp); //split_info statTree[i]->statResult.push_back(splitInfo); //gain_info double temp = gain_temp / splitInfo; statTree[i]->statResult.push_back(temp); //得到最大值*/ if (temp > max_temp) { max_temp = temp; max_attribute = i; } } } return max_attribute; } /* * @function Info_attr info_D 总信息量 * @param: deciNum 有效信息数 * @param: sum 总信息量 * @return double 总信息量比例 */ double DecisionTree::info_D(int deciNum, int sum) { double pi = (double)deciNum / (double)sum; double result = 0.0; if (pi == 1.0 || pi == 0.0) { return result; } result = pi * (log(pi) / log((double)2)) + (1 - pi)*(log(1 - pi)/log((double)2)); return -result; } /* * @function Info_attr 总信息量 * @param: deciNum 有效信息数 * @param: sum 总信息量 * @return double */ double DecisionTree::Info_attr(map<string, attrItem*>& attriItem, double& splitInfo, int lineNum) { double result = 0.0; for (map<string, attrItem*>::iterator item = attriItem.begin(); item != attriItem.end(); ++item ) { double pi = (double)(item->second->itemNum[0]) / (double)lineNum; splitInfo += pi * (log(pi) / log((double)2)); double sub_attr = info_D(item->second->itemNum[1], item->second->itemNum[0]); result += pi * sub_attr; } splitInfo = -splitInfo; return result; } /* * @function resetStatTree 清理状态树 * @param: statTree 状态树 * @param: readClumNum 需要清理的属性set * @return void */ void DecisionTree::resetStatTree(vector<attributes*>& statTree, vector<int>& readClumNum) { for (int i = 0; i < readClumNum.size() - 1; i++) { if (readClumNum[i] == 0) { map<string, attrItem*>::iterator it_end = statTree[i]->attriItem.end(); for (map<string, attrItem*>::iterator it = statTree[i]->attriItem.begin(); it != it_end; it++) { delete it->second; } statTree[i]->attriItem.clear(); statTree[i]->statResult.clear(); } } }
以图形表示为:
1、在设计程序时,对程序逻辑有时会发生混乱,·后者在纸上仔细画了些草图才解决这些问题,画一个好图可以有效的帮助你理解程序的流程以及逻辑脉络,是需求分析时最为关键的基本功。
2、在编写程序之初,一直在纠结用什么样的数据结构,后来经过几次在编程实现推敲,才确定最佳的数据结构,可见数据结构在程序中的重要性。
3、决策树的编写,其实就是理论与实践的相结合,虽然理论上比较简单,但是实践中却会遇到这样那样的问题,而这些问题就是考验一个程序员对最基本的数据结构、算法的理解和熟练程度,所以,勤学勤练基本功依然是关键。
4、程序的效率还有待提高,欢迎各路高手指正。