CART算法也是一種決策樹分類算法。CART分類迴歸樹算法的本質也是對數據進行分類的,最終數據的表現形式也是以樹形的模式展示的,與ID3,C4.5算法不一樣的是,他的分類標準所採用的算法不一樣了。下面列出了其中的一些不一樣之處:html
一、CART最後造成的樹是一個二叉樹,每一個節點會分紅2個節點,左孩子節點和右孩子節點,而在ID3和C4.5中是按照分類屬性的值類型進行劃分,因而這就要求CART算法在所選定的屬性中又要劃分出最佳的屬性劃分值,節點若是選定了劃分屬性名稱還要肯定裏面按照那個值作一個二元的劃分。java
二、CART算法對於屬性的值採用的是基於Gini係數值的方式作比較,gini某個屬性的某次值的劃分的gini指數的值爲:node
,pk就是分別爲正負實例的機率,gini係數越小說明分類純度越高,能夠想象成與熵的定義同樣。所以在最後計算的時候咱們只取其中值最小的作出劃分。最後作比較的時候用的是gini的增益作比較,要對分類號的數據作出一個帶權重的gini指數的計算。舉一個網上的一個例子:android
好比體溫爲恆溫時包含哺乳類5個、鳥類2個,則:算法
![](http://static.javashuo.com/static/loading.gif)
體溫爲非恆溫時包含爬行類3個、魚類3個、兩棲類2個,則數組
![](http://static.javashuo.com/static/loading.gif)
因此若是按照「體溫爲恆溫和非恆溫」進行劃分的話,咱們獲得GINI的增益(類比信息增益):app
![](http://static.javashuo.com/static/loading.gif)
最好的劃分就是使得GINI_Gain最小的劃分。工具
經過比較每一個屬性的最小的gini指數值,做爲最後的結果。優化
三、CART算法在把數據進行分類以後,會對樹進行一個剪枝,經常使用的用前剪枝和後剪枝法,而常見的後剪枝發包括代價複雜度剪枝,悲觀偏差剪枝等等,我寫的這次算法採用的是代價複雜度剪枝法。代價複雜度剪枝的算法公式爲:ui
α表示的是每一個非葉子節點的偏差增益率,能夠理解爲偏差代價,最後選出偏差代價最小的一個節點進行剪枝。
裏面變量的意思爲:
是子樹中包含的葉子節點個數;
是節點t的偏差代價,若是該節點被剪枝;
![](http://static.javashuo.com/static/loading.gif)
r(t)是節點t的偏差率;
p(t)是節點t上的數據佔全部數據的比例。
是子樹Tt的偏差代價,若是該節點不被剪枝。它等於子樹Tt上全部葉子節點的偏差代價之和。下面說說我對於這個公式的理解:其實這個公式的本質是對於剪枝前和剪枝後的樣本誤差率作一個差值比較,一個好的分類固然是分類後的樣本誤差率相較於沒分類(就是剪枝掉的時候)的誤差率小,因此這時的值就會大,若是分類先後基本變化不大,則意味着分類不起什麼效果,α值的分子位置就小,因此偏差代價就小,能夠被剪枝。可是通常分類後的誤差率會小於分類前的,由於誤差數在高層節點的時候確定比子節點的多,子節點誤差數最多與父親節點同樣。
CART算法實現
首先是程序的備用數據,我是把他存在了一個文字中,經過程序進行逐行的讀取:
[java] view plain copy
print?
- 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 plain copy
print?
- 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()][];
- desData.toArray(desDataArray);
- }
-
- return desDataArray;
- }
-
- public void startBuildingTree() {
- readDataFile();
- initAttrValue();
-
- ArrayList<String> remainAttr = new ArrayList<>();
- // 添加屬性,除了最後一個類標號屬性
- for (int i = 1; i < attrNames.length - 1; i++) {
- remainAttr.add(attrNames[i]);
- }
-
- AttrNode rootNode = new AttrNode();
- buildDecisionTree(rootNode, "", data, remainAttr, false);
- setIndexAndAlpah(rootNode, 0, false);
- System.out.println("剪枝前:");
- showDecisionTree(rootNode, 1);
- setIndexAndAlpah(rootNode, 0, true);
- System.out.println("\n剪枝後:");
- showDecisionTree(rootNode, 1);
- }
-
- /**
- * 顯示決策樹
- *
- * @param node
- * 待顯示的節點
- * @param blankNum
- * 行空格符,用於顯示樹型結構
- */
- private void showDecisionTree(AttrNode node, int blankNum) {
- System.out.println();
- for (int i = 0; i < blankNum; i++) {
- System.out.print(" ");
- }
- System.out.print("--");
- // 顯示分類的屬性值
- if (node.getParentAttrValue() != null
- && node.getParentAttrValue().length() > 0) {
- System.out.print(node.getParentAttrValue());
- } else {
- System.out.print("--");
- }
- System.out.print("--");
-
- if (node.getDataIndex() != null && node.getDataIndex().size() > 0) {
- String i = node.getDataIndex().get(0);
- System.out.print("【" + node.getNodeIndex() + "】類別:"
- + data[Integer.parseInt(i)][attrNames.length - 1]);
- System.out.print("[");
- for (String index : node.getDataIndex()) {
- System.out.print(index + ", ");
- }
- System.out.print("]");
- } else {
- // 遞歸顯示子節點
- System.out.print("【" + node.getNodeIndex() + ":"
- + node.getAttrName() + "】");
- if (node.getChildAttrNode() != null) {
- for (AttrNode childNode : node.getChildAttrNode()) {
- showDecisionTree(childNode, 2 * blankNum);
- }
- } else {
- System.out.print("【 Child Null】");
- }
- }
- }
-
- /**
- * 爲節點設置序列號,並計算每一個節點的偏差率,用於後面剪枝
- *
- * @param node
- * 開始的時候傳入的是根節點
- * @param index
- * 開始的索引號,從1開始
- * @param ifCutNode
- * 是否須要剪枝
- */
- private void setIndexAndAlpah(AttrNode node, int index, boolean ifCutNode) {
- AttrNode tempNode;
- // 最小偏差代價節點,即將被剪枝的節點
- AttrNode minAlphaNode = null;
- double minAlpah = Integer.MAX_VALUE;
- Queue<AttrNode> nodeQueue = new LinkedList<AttrNode>();
-
- nodeQueue.add(node);
- while (nodeQueue.size() > 0) {
- index++;
- // 從隊列頭部獲取首個節點
- tempNode = nodeQueue.poll();
- tempNode.setNodeIndex(index);
- if (tempNode.getChildAttrNode() != null) {
- for (AttrNode childNode : tempNode.getChildAttrNode()) {
- nodeQueue.add(childNode);
- }
- computeAlpha(tempNode);
- if (tempNode.getAlpha() < minAlpah) {
- minAlphaNode = tempNode;
- minAlpah = tempNode.getAlpha();
- } else if (tempNode.getAlpha() == minAlpah) {
- // 若是偏差代價值同樣,比較包含的葉子節點個數,剪枝有多葉子節點數的節點
- if (tempNode.getLeafNum() > minAlphaNode.getLeafNum()) {
- minAlphaNode = tempNode;
- }
- }
- }
- }
-
- if (ifCutNode) {
- // 進行樹的剪枝,讓其左右孩子節點爲null
- minAlphaNode.setChildAttrNode(null);
- }
- }
-
- /**
- * 爲非葉子節點計算偏差代價,這裏的後剪枝法用的是CCP代價複雜度剪枝
- *
- * @param node
- * 待計算的非葉子節點
- */
- private void computeAlpha(AttrNode node) {
- double rt = 0;
- double Rt = 0;
- double alpha = 0;
- // 當前節點的數據總數
- int sumNum = 0;
- // 最少的誤差數
- int minNum = 0;
-
- ArrayList<String> dataIndex;
- ArrayList<AttrNode> leafNodes = new ArrayList<>();
-
- addLeafNode(node, leafNodes);
- node.setLeafNum(leafNodes.size());
- for (AttrNode attrNode : leafNodes) {
- dataIndex = attrNode.getDataIndex();
-
- int num = 0;
- sumNum += dataIndex.size();
- for (String s : dataIndex) {
- // 統計分類數據中的正負實例數
- if (data[Integer.parseInt(s)][attrNames.length - 1].equals(YES)) {
- num++;
- }
- }
- minNum += num;
-
- // 取小數量的值部分
- if (1.0 * num / dataIndex.size() > 0.5) {
- num = dataIndex.size() - num;
- }
-
- rt += (1.0 * num / (data.length - 1));
- }
-
- //一樣取出少誤差的那部分
- if (1.0 * minNum / sumNum > 0.5) {
- minNum = sumNum - minNum;
- }
-
- Rt = 1.0 * minNum / (data.length - 1);
- alpha = 1.0 * (Rt - rt) / (leafNodes.size() - 1);
- node.setAlpha(alpha);
- }
-
- /**
- * 篩選出節點所包含的葉子節點數
- *
- * @param node
- * 待篩選節點
- * @param leafNode
- * 葉子節點列表容器
- */
- private void addLeafNode(AttrNode node, ArrayList<AttrNode> leafNode) {
- ArrayList<String> dataIndex;
-
- if (node.getChildAttrNode() != null) {
- for (AttrNode childNode : node.getChildAttrNode()) {
- dataIndex = childNode.getDataIndex();
- if (dataIndex != null && dataIndex.size() > 0) {
- // 說明此節點爲葉子節點
- leafNode.add(childNode);
- } else {
- // 若是仍是非葉子節點則繼續遞歸調用
- addLeafNode(childNode, leafNode);
- }
- }
- }
- }
-
- }
AttrNode節點的設計和屬性:
[java] view plain copy
print?
- /**
- * 迴歸分類樹節點
- *
- * @author lyq
- *
- */
- public class AttrNode {
- // 節點屬性名字
- private String attrName;
- // 節點索引標號
- private int nodeIndex;
- //包含的葉子節點數
- private int leafNum;
- // 節點偏差率
- private double alpha;
- // 父親分類屬性值
- private String parentAttrValue;
- // 孩子節點
- private AttrNode[] childAttrNode;
- // 數據記錄索引
- private ArrayList<String> dataIndex;
- .....
get,set方法自行補上。客戶端的場景調用:
[java] view plain copy
print?
- package DataMing_CART;
-
- public class Client {
- public static void main(String[] args){
- String filePath = "C:\\Users\\lyq\\Desktop\\icon\\input.txt";
-
- CARTTool tool = new CARTTool(filePath);
-
- tool.startBuildingTree();
- }
- }
數據文件路徑自行修改,不然會報錯(特殊狀況懶得處理了.....)。最後程序的輸出結果,請自行從左往右看,從上往下,左邊的是父親節點,上面的是考前的子節點:
[java] view plain copy
print?
- 剪枝前:
-
- --!--【1:Age】
- --MiddleAged--【2】類別:Yes[3, 7, 12, 13, ]
- --!MiddleAged--【3:Student】
- --No--【4:Income】
- --High--【6】類別:No[1, 2, ]
- --!High--【7:CreditRating】
- --Fair--【10】類別:Yes[4, 8, ]
- --!Fair--【11】類別:No[14, ]
- --!No--【5:CreditRating】
- --Fair--【8】類別:Yes[5, 9, 10, ]
- --!Fair--【9:Income】
- --Medium--【12】類別:Yes[11, ]
- --!Medium--【13】類別:No[6, ]
- 剪枝後:
-
- --!--【1:Age】
- --MiddleAged--【2】類別:Yes[3, 7, 12, 13, ]
- --!MiddleAged--【3:Student】
- --No--【4:Income】【 Child Null】
- --!No--【5:CreditRating】
- --Fair--【8】類別:Yes[5, 9, 10, ]
- --!Fair--【9:Income】
- --Medium--【12】類別:Yes[11, ]
- --!Medium--【13】類別:No[6, ]
結果分析:
我在一開始的時候根據的是最後分類的數據是否爲同一個類標識的,若是都爲YES或者都爲NO的,分類終止,通常狀況下都說的通,可是若是最後屬性劃分完畢了,剩餘的數據還有存在類標識不同的狀況就會誤差,好比說這裏的7號CredaRating節點,下面Fair分支中的[4,8]就不是同類的。因此在後面的剪枝算法就被剪枝了。由於後面的4和7號節點的偏差代價率爲0,說明分類先後沒有類誤差變化,這也見證了後剪枝算法的威力所在了。
在coding遇到的困難和改進的地方:
一、先說說在編碼時遇到的困難,在對節點進行賦索引標號值的時候出了問題,由於是以前生成樹的時候採用了DFS的思想,若是編號時也採用此方法就不對了,因而就用到了把節點取出放入隊列這樣的遍歷方式,就是BFS的方式爲節點標號。
二、程序的一個改進的地方在於算一個非葉子節點的時候須要計算他所包含的葉子節點數,採用了從當前節點開始從上往下遞歸計算,並且每一個非葉子節點都計算一遍,顯然這樣作的效率是不高,後來想到了一種從葉子節點開始計算,從下往上直到根節點,對父親節點的非葉子節點列表作更新操做,就只要計算一次,這有點dp的思想在裏面了,因爲時間關係,沒有來得及實現。
三、第二個優化點就是後剪枝算法的多樣化,我這裏採用的是CCP代價複雜度算法,你們能夠試着實現其餘的諸如悲觀偏差算法進行剪枝,看看能不能把程序中4和7號節點識別出來,而且剪枝掉。