CART分类回归树算法
CART分类回归树算法
与上次文章中提到的ID3算法和C4.5算法类似,CART算法也是一种决策树分类算法。CART分类回归树算法的本质也是对数据进行分类的,最终数据的表现形式也是以树形的模式展现的,与ID3,C4.5算法不同的是,他的分类标准所采用的算法不同了。下面列出了其中的一些不同之处:
1、CART最后形成的树是一个二叉树,每个节点会分成2个节点,左孩子节点和右孩子节点,而在ID3和C4.5中是按照分类属性的值类型进行划分,于是这就要求CART算法在所选定的属性中又要划分出最佳的属性划分值,节点如果选定了划分属性名称还要确定里面按照那个值做一个二元的划分。
2、CART算法对于属性的值采用的是基于Gini系数值的方式做比较,gini某个属性的某次值的划分的gini指数的值为:
,pk就是分别为正负实例的概率,gini系数越小说明分类纯度越高,可以想象成与熵的定义一样。因此在最后计算的时候我们只取其中值最小的做出划分。最后做比较的时候用的是gini的增益做比较,要对分类号的数据做出一个带权重的gini指数的计算。举一个网上的一个例子:
比如体温为恒温时包含哺乳类5个、鸟类2个,则:
体温为非恒温时包含爬行类3个、鱼类3个、两栖类2个,则
所以如果按照“体温为恒温和非恒温”进行划分的话,我们得到GINI的增益(类比信息增益):
最好的划分就是使得GINI_Gain最小的划分。
通过比较每个属性的最小的gini指数值,作为最后的结果。
3、CART算法在把数据进行分类之后,会对树进行一个剪枝,常用的用前剪枝和后剪枝法,而常见的后剪枝发包括代价复杂度剪枝,悲观误差剪枝等等,我写的此次算法采用的是代价复杂度剪枝法。代价复杂度剪枝的算法公式为:
α表示的是每个非叶子节点的误差增益率,可以理解为误差代价,最后选出误差代价最小的一个节点进行剪枝。
里面变量的意思为:
是子树中包含的叶子节点个数;
是节点t的误差代价,如果该节点被剪枝;
r(t)是节点t的误差率;
p(t)是节点t上的数据占所有数据的比例。
是子树Tt的误差代价,如果该节点不被剪枝。它等于子树Tt上所有叶子节点的误差代价之和。下面说说我对于这个公式的理解:其实这个公式的本质是对于剪枝前和剪枝后的样本偏差率做一个差值比较,一个好的分类当然是分类后的样本偏差率相较于没分类(就是剪枝掉的时候)的偏差率小,所以这时的值就会大,如果分类前后基本变化不大,则意味着分类不起什么效果,α值的分子位置就小,所以误差代价就小,可以被剪枝。但是一般分类后的偏差率会小于分类前的,因为偏差数在高层节点的时候肯定比子节点的多,子节点偏差数最多与父亲节点一样。
CART算法实现
首先是程序的备用数据,我是把他存在了一个文字中,通过程序进行逐行的读取:
[java] view plaincopyprint?
-
Rid Age Income Student CreditRating BuysComputer
-
1 Youth High No Fair No
-
2 Youth High No Excellent No
-
3 MiddleAged High No Fair Yes
-
4 Senior Medium No Fair Yes
-
5 Senior Low Yes Fair Yes
-
6 Senior Low Yes Excellent No
-
7 MiddleAged Low Yes Excellent Yes
-
8 Youth Medium No Fair No
-
9 Youth Low Yes Fair Yes
-
10 Senior Medium Yes Fair Yes
-
11 Youth Medium Yes Excellent Yes
-
12 MiddleAged Medium No Excellent Yes
-
13 MiddleAged High Yes Fair Yes
-
14 Senior Medium No Excellent No
下面是主程序,里面有具体的注释:
[java] view plaincopyprint?
-
package DataMing_CART;
-
-
import java.io.BufferedReader;
-
import java.io.File;
-
import java.io.FileReader;
-
import java.io.IOException;
-
import java.util.ArrayList;
-
import java.util.HashMap;
-
import java.util.LinkedList;
-
import java.util.Map;
-
import java.util.Queue;
-
-
import javax.lang.model.element.NestingKind;
-
import javax.swing.text.DefaultEditorKit.CutAction;
-
import javax.swing.text.html.MinimalHTMLWriter;
-
-
/**
-
* CART分类回归树算法工具类
-
*
-
* @author lyq
-
*
-
*/
-
public class CARTTool {
-
// 类标号的值类型
-
private final String YES = "Yes";
-
private final String NO = "No";
-
-
// 所有属性的类型总数,在这里就是data源数据的列数
-
private int attrNum;
-
private String filePath;
-
// 初始源数据,用一个二维字符数组存放模仿表格数据
-
private String[][] data;
-
// 数据的属性行的名字
-
private String[] attrNames;
-
// 每个属性的值所有类型
-
private HashMap<String, ArrayList<String>> attrValue;
-
-
public CARTTool(String filePath) {
-
this.filePath = filePath;
-
attrValue = new HashMap<>();
-
}
-
-
/**
-
* 从文件中读取数据
-
*/
-
public void readDataFile() {
-
File file = new File(filePath);
-
ArrayList<String[]> dataArray = new ArrayList<String[]>();
-
-
try {
-
BufferedReader in = new BufferedReader(new FileReader(file));
-
String str;
-
String[] tempArray;
-
while ((str = in.readLine()) != null) {
-
tempArray = str.split(" ");
-
dataArray.add(tempArray);
-
}
-
in.close();
-
} catch (IOException e) {
-
e.getStackTrace();
-
}
-
-
data = new String[dataArray.size()][];
-
dataArray.toArray(data);
-
attrNum = data[0].length;
-
attrNames = data[0];
-
-
/*
-
* for (int i = 0; i < data.length; i++) { for (int j = 0; j <
-
* data[0].length; j++) { System.out.print(" " + data[i][j]); }
-
* System.out.print("\n"); }
-
*/
-
-
}
-
-
/**
-
* 首先初始化每种属性的值的所有类型,用于后面的子类熵的计算时用
-
*/
-
public void initAttrValue() {
-
ArrayList<String> tempValues;
-
-
// 按照列的方式,从左往右找
-
for (int j = 1; j < attrNum; j++) {
-
// 从一列中的上往下开始寻找值
-
tempValues = new ArrayList<>();
-
for (int i = 1; i < data.length; i++) {
-
if (!tempValues.contains(data[i][j])) {
-
// 如果这个属性的值没有添加过,则添加
-
tempValues.add(data[i][j]);
-
}
-
}
-
-
// 一列属性的值已经遍历完毕,复制到map属性表中
-
attrValue.put(data[0][j], tempValues);
-
}
-
-
/*
-
* for (Map.Entry entry : attrValue.entrySet()) {
-
* System.out.println("key:value " + entry.getKey() + ":" +
-
* entry.getValue()); }
-
*/
-
}
-
-
/**
-
* 计算机基尼指数
-
*
-
* @param remainData
-
* 剩余数据
-
* @param attrName
-
* 属性名称
-
* @param value
-
* 属性值
-
* @param beLongValue
-
* 分类是否属于此属性值
-
* @return
-
*/
-
public double computeGini(String[][] remainData, String attrName,
-
String value, boolean beLongValue) {
-
// 实例总数
-
int total = 0;
-
// 正实例数
-
int posNum = 0;
-
// 负实例数
-
int negNum = 0;
-
// 基尼指数
-
double gini = 0;
-
-
// 还是按列从左往右遍历属性
-
for (int j = 1; j < attrNames.length; j++) {
-
// 找到了指定的属性
-
if (attrName.equals(attrNames[j])) {
-
for (int i = 1; i < remainData.length; i++) {
-
// 统计正负实例按照属于和不属于值类型进行划分
-
if ((beLongValue && remainData[i][j].equals(value))
-
|| (!beLongValue && !remainData[i][j].equals(value))) {
-
if (remainData[i][attrNames.length - 1].equals(YES)) {
-
// 判断此行数据是否为正实例
-
posNum++;
-
} else {
-
negNum++;
-
}
-
}
-
}
-
}
-
}
-
-
total = posNum + negNum;
-
double posProbobly = (double) posNum / total;
-
double negProbobly = (double) negNum / total;
-
gini = 1 - posProbobly * posProbobly - negProbobly * negProbobly;
-
-
// 返回计算基尼指数
-
return gini;
-
}
-
-
/**
-
* 计算属性划分的最小基尼指数,返回最小的属性值划分和最小的基尼指数,保存在一个数组中
-
*
-
* @param remainData
-
* 剩余谁
-
* @param attrName
-
* 属性名称
-
* @return
-
*/
-
public String[] computeAttrGini(String[][] remainData, String attrName) {
-
String[] str = new String[2];
-
// 最终该属性的划分类型值
-
String spiltValue = "";
-
// 临时变量
-
int tempNum = 0;
-
// 保存属性的值划分时的最小的基尼指数
-
double minGini = Integer.MAX_VALUE;
-
ArrayList<String> valueTypes = attrValue.get(attrName);
-
// 属于此属性值的实例数
-
HashMap<String, Integer> belongNum = new HashMap<>();
-
-
for (String string : valueTypes) {
-
// 重新计数的时候,数字归0
-
tempNum = 0;
-
// 按列从左往右遍历属性
-
for (int j = 1; j < attrNames.length; j++) {
-
// 找到了指定的属性
-
if (attrName.equals(attrNames[j])) {
-
for (int i = 1; i < remainData.length; i++) {
-
// 统计正负实例按照属于和不属于值类型进行划分
-
if (remainData[i][j].equals(string)) {
-
tempNum++;
-
}
-
}
-
}
-
}
-
-
belongNum.put(string, tempNum);
-
}
-
-
double tempGini = 0;
-
double posProbably = 1.0;
-
double negProbably = 1.0;
-
for (String string : valueTypes) {
-
tempGini = 0;
-
-
posProbably = 1.0 * belongNum.get(string) / (remainData.length - 1);
-
negProbably = 1 - posProbably;
-
-
tempGini += posProbably
-
* computeGini(remainData, attrName, string, true);
-
tempGini += negProbably
-
* computeGini(remainData, attrName, string, false);
-
-
if (tempGini < minGini) {
-
minGini = tempGini;
-
spiltValue = string;
-
}
-
}
-
-
str[0] = spiltValue;
-
str[1] = minGini + "";
-
-
return str;
-
}
-
-
public void buildDecisionTree(AttrNode node, String parentAttrValue,
-
String[][] remainData, ArrayList<String> remainAttr,
-
boolean beLongParentValue) {
-
// 属性划分值
-
String valueType = "";
-
// 划分属性名称
-
String spiltAttrName = "";
-
double minGini = Integer.MAX_VALUE;
-
double tempGini = 0;
-
// 基尼指数数组,保存了基尼指数和此基尼指数的划分属性值
-
String[] giniArray;
-
-
if (beLongParentValue) {
-
node.setParentAttrValue(parentAttrValue);
-
} else {
-
node.setParentAttrValue("!" + parentAttrValue);
-
}
-
-
if (remainAttr.size() == 0) {
-
if (remainData.length > 1) {
-
ArrayList<String> indexArray = new ArrayList<>();
-
for (int i = 1; i < remainData.length; i++) {
-
indexArray.add(remainData[i][0]);
-
}
-
node.setDataIndex(indexArray);
-
}
-
System.out.println("attr remain null");
-
return;
-
}
-
-
for (String str : remainAttr) {
-
giniArray = computeAttrGini(remainData, str);
-
tempGini = Double.parseDouble(giniArray[1]);
-
-
if (tempGini < minGini) {
-
spiltAttrName = str;
-
minGini = tempGini;
-
valueType = giniArray[0];
-
}
-
}
-
// 移除划分属性
-
remainAttr.remove(spiltAttrName);
-
node.setAttrName(spiltAttrName);
-
-
// 孩子节点,分类回归树中,每次二元划分,分出2个孩子节点
-
AttrNode[] childNode = new AttrNode[2];
-
String[][] rData;
-
-
boolean[] bArray = new boolean[] { true, false };
-
for (int i = 0; i < bArray.length; i++) {
-
// 二元划分属于属性值的划分
-
rData = removeData(remainData, spiltAttrName, valueType, bArray[i]);
-
-
boolean sameClass = true;
-
ArrayList<String> indexArray = new ArrayList<>();
-
for (int k = 1; k < rData.length; k++) {
-
indexArray.add(rData[k][0]);
-
// 判断是否为同一类的
-
if (!rData[k][attrNames.length - 1]
-
.equals(rData[1][attrNames.length - 1])) {
-
// 只要有1个不相等,就不是同类型的
-
sameClass = false;
-
break;
-
}
-
}
-
-
childNode[i] = new AttrNode();
-
if (!sameClass) {
-
// 创建新的对象属性,对象的同个引用会出错
-
ArrayList<String> rAttr = new ArrayList<>();
-
for (String str : remainAttr) {
-
rAttr.add(str);
-
}
-
buildDecisionTree(childNode[i], valueType, rData, rAttr,
-
bArray[i]);
-
} else {
-
String pAtr = (bArray[i] ? valueType : "!" + valueType);
-
childNode[i].setParentAttrValue(pAtr);
-
childNode[i].setDataIndex(indexArray);
-
}
-
}
-
-
node.setChildAttrNode(childNode);
-
}
-
-
/**
-
* 属性划分完毕,进行数据的移除
-
*
-
* @param srcData
-
* 源数据
-
* @param attrName
-
* 划分的属性名称
-
* @param valueType
-
* 属性的值类型
-
* @parame beLongValue 分类是否属于此值类型
-
*/
-
private String[][] removeData(String[][] srcData, String attrName,
-
String valueType, boolean beLongValue) {
-
String[][] desDataArray;
-
ArrayList<String[]> desData = new ArrayList<>();
-
// 待删除数据
-
ArrayList<String[]> selectData = new ArrayList<>();
-
selectData.add(attrNames);
-
-
// 数组数据转化到列表中,方便移除
-
for (int i = 0; i < srcData.length; i++) {
-
desData.add(srcData[i]);
-
}
-
-
// 还是从左往右一列列的查找
-
for (int j = 1; j < attrNames.length; j++) {
-
if (attrNames[j].equals(attrName)) {
-
for (int i = 1; i < desData.size(); i++) {
-
if (desData.get(i)[j].equals(valueType)) {
-
// 如果匹配这个数据,则移除其他的数据
-
selectData.add(desData.get(i));
-
}
-
}
-
}
-
}
-
-
if (beLongValue) {
-
desDataArray = new String[selectData.size()][];
-
selectData.toArray(desDataArray);
-
} else {
-
// 属性名称行不移除
-
selectData.remove(attrNames);
-
// 如果是划分不属于此类型的数据时,进行移除
-
desData.removeAll(selectData);
-
desDataArray = new String[desData.size()][];
-
&
CDA数据分析师考试相关入口一览(建议收藏):
▷ 想报名CDA认证考试,点击>>>
“CDA报名”
了解CDA考试详情;
▷ 想加入CDA考试题库,点击>>> “CDA题库” 了解CDA考试详情;
▷ 想学习CDA考试教材,点击>>> “CDA教材” 了解CDA考试详情;
▷ 想查询CDA考试成绩,点击>>> “CDA成绩” 了解CDA考试详情;
▷ 想了解CDA考试含金量,点击>>> “CDA含金量” 了解CDA考试详情;
▷ 想获取CDA考试时间/费用/条件/大纲/通过率,点击 >>>“CDA考试官网” 了解CDA考试详情;