1、輔助類,用於計算過程和結果儲存
/// <summary> /// 決策樹節點. /// </summary> public class DecisionTreeNode { /// <summary> /// 類型:分支或葉子 /// </summary> public string Type { get; set; } /// <summary> /// 關鍵字一般存當前屬性因子 /// </summary> public string Key { get; set; } /// <summary> /// 判斷值,葉子節點有效. /// </summary> public string DecisionValue { get; set; } /// <summary> /// 前一個屬性因子,可以看作是分支條件. /// </summary> public string ParentFactor { get; set; } /// <summary> /// 當前節點的樣本數量, /// </summary> public int CalcCount { get; set; } /// <summary> /// 當前節點的樣本索引集合. /// </summary> public List<int> DataIndexes {get;set;} /// <summary> /// 分支節點集合. /// </summary> public Dictionary<string, DecisionTreeNode> Children { get; private set; } /// <summary> /// 父節點 /// </summary> public DecisionTreeNode Parent { get; set; } public DecisionTreeNode() { DataIndexes = new List<int>(); Children = new Dictionary<string, DecisionTreeNode>(); } } /// <summary> /// 用於計算過程存放資料.用數組不是很方便,這裡採用字典,可以減少迴圈次數. /// </summary> public class CalcNode { public string Key { get; set; } public string Type { get; set; } public int CalcCount { get; set; } public List<int> DataIndexes {get;set;} public Dictionary<string, CalcNode> Children { get; private set; } public CalcNode() { DataIndexes = new List<int>(); Children = new Dictionary<string, CalcNode>(); } public void AddChildren(string Key,string AType,int AIndex, int Count = 1) { if (Children.ContainsKey(Key) == false) { Children.Add(Key, new CalcNode()); } Children[Key].Key = Key; Children[Key].Type = AType; Children[Key].CalcCount += Count; Children[Key].DataIndexes.Add(AIndex); } }
2、演算法類,注釋比較詳細,有時間再寫一篇原理文章
/// <summary> /// 決策樹演算法類,不適合連續性值。 /// </summary> public class DecisionTreeAlg { private string PrefixString = " "; /// <summary> /// 構建決策樹,決策分類屬性約定放在第1列。 /// </summary> /// <param name="Inputs">行表示屬性,列為值,注意列等長</param> /// <param name="PNode">父節點</param> /// <param name="PropertyNames">測試屬性名稱</param> /// <param name="TestProperties">當前可用測試屬性索引</param> /// <param name="DefaultClassFactor">預設判別決策分類因子</param> /// <param name="CallLevel">用來測試輸出控制,無實際作用</param> /// <param name="OutContents">輸出內容,為調試用</param> /// <param name="PropertyFactors">屬性因子</param> public void BuildDecisionTree(int CallLevel, ref string OutContents, string[][] Inputs, DecisionTreeNode PNode, string[] PropertyNames, List<int> TestProperties, string DefaultClassFactor, Dictionary<string, List<string>> PropertyFactors) { string thePrefix = PrefixString.Substring(0, CallLevel * 2); CallLevel++; //如果沒有測試屬性,將當前節點設為葉子節點,選擇高機率分類,然後返回 if (TestProperties.Count <= 1) { PNode.Type = "葉子"; PNode.DecisionValue = DefaultClassFactor; return; } //如果沒有學習樣本集,將當前節點設為葉子節點,選擇高機率分類,然後返回 if (PNode.DataIndexes.Count <= 0) { PNode.Type = "葉子"; PNode.DecisionValue = DefaultClassFactor; return; } if (PropertyFactors == null) { PropertyFactors = new Dictionary<string, List<string>>(); } //準備儲存遍曆時的計數儲存結構 Dictionary<string, CalcNode> thePropertyCount = new Dictionary<string, CalcNode>(); foreach (var theProIndex in TestProperties) { thePropertyCount.Add(PropertyNames[theProIndex], new CalcNode() { Key = PropertyNames[theProIndex] }); if (PropertyFactors.ContainsKey(PropertyNames[theProIndex]) == false) { PropertyFactors.Add(PropertyNames[theProIndex], new List<string>()); } } //遍曆當前可遍曆的資料,進行統計,為計算各屬性熵做準備 for (int n = 0; n < PNode.DataIndexes.Count; n++) { int theI = PNode.DataIndexes[n]; for (int k = 0; k < TestProperties.Count; k++) { int theJ = TestProperties[k]; var thePropertyCalcNode = thePropertyCount[PropertyNames[theJ]]; //對當前屬性計數 thePropertyCalcNode.CalcCount++; //對第j個屬性的當前因子計數 thePropertyCalcNode.AddChildren(Inputs[theJ][theI], "測試屬性因子", theI, 1); //對第j個屬性的當前因子的主分類因子計數 thePropertyCalcNode.Children[Inputs[theJ][theI]].AddChildren(Inputs[0][theI], "主分類因子", theI, 1); //統計歸納各屬性因子,採用這種方式可以減少迴圈. if (PropertyFactors[PropertyNames[theJ]].Contains(Inputs[theJ][theI]) == false) { PropertyFactors[PropertyNames[theJ]].Add(Inputs[theJ][theI]); } } } //計算資訊增益量,擷取具有最大資訊增益屬性 string theDefaultClassFactor = DefaultClassFactor; //初始化最大測試屬性熵值. double theMaxEA = double.MinValue; //記錄具有最大熵值屬性的索引位置 int theMaxPropertyIndex = TestProperties[1]; //總資訊熵值,其實就是分類屬性的熵值. double theTotalEA = 0.0; //記錄總的樣本數,用於估算機率. double theTotalSimple = 0; for(int theI=0;theI<TestProperties.Count;theI++) { int thePIndex_1 = TestProperties[theI]; if (thePIndex_1 == 0) { //主分類熵值計算,計算公式與測試屬性有所不同. CalcNode theCalcNode = thePropertyCount[PropertyNames[thePIndex_1]]; double theCount = theCalcNode.CalcCount; theTotalSimple = theCount; double theMaxSubCount = -1; theTotalEA = 0.0; //求和(-Pj*log2(Pj)) foreach (var theSubNode in theCalcNode.Children) { if (theSubNode.Value.CalcCount > 0) { double thePj = theSubNode.Value.CalcCount / theCount; theTotalEA += 0 - thePj * Math.Log(thePj, 2); } if (theMaxSubCount < theSubNode.Value.CalcCount) { theMaxSubCount = theSubNode.Value.CalcCount; theDefaultClassFactor = theSubNode.Key; } //測試輸出,跟蹤計算路徑. OutContents += "\r\n" + thePrefix + theCalcNode.CalcCount + ":: " + PropertyNames[thePIndex_1] + ":: " + theSubNode.Value.Type + " :: " + theSubNode.Key + " :: " + theSubNode.Value.CalcCount; } } else { //測試屬性熵值計算。 CalcNode theCalcNode = thePropertyCount[PropertyNames[thePIndex_1]]; double theJEA = 0.0; foreach (var theSubNode_1 in theCalcNode.Children) { if (theSubNode_1.Value.CalcCount > 0) { double theSjCount = theSubNode_1.Value.CalcCount; double theSj_1 = theSjCount / theTotalSimple; double theSj_2 = 0.0; foreach (var theSubNode_2 in theSubNode_1.Value.Children) { if (theSubNode_2.Value.CalcCount > 0) { double thePj_1 = Convert.ToDouble(theSubNode_2.Value.CalcCount) / theSjCount; theSj_2 += 0.0 - thePj_1 * Math.Log(thePj_1, 2); } OutContents += "\r\n" + thePrefix + theCalcNode.CalcCount + ":: " + PropertyNames[thePIndex_1] + " :: " + theSubNode_1.Value.Type + " :: " + theSubNode_1.Key + " :: " + theSubNode_1.Value.CalcCount + theSubNode_2.Value.Type + " :: " + theSubNode_2.Key + " :: " + theSubNode_2.Value.CalcCount; } theJEA += theSj_1 * theSj_2; } } theJEA = theTotalEA - theJEA; //只記錄最大熵值屬性資訊. if (theMaxEA < theJEA) { theMaxEA = theJEA; theMaxPropertyIndex = thePIndex_1; } } } //如果分類因子只有一個,則置當前節點為葉子節點,設定判定為當前分類因子,然後返回 if (thePropertyCount[PropertyNames[0]].Children.Count <= 1) { PNode.Type = "葉子"; PNode.DecisionValue = theDefaultClassFactor; return; } //具有多個分類因子,還剩有測試屬性,則設當前節點為分支節點,準備分支. PNode.Type = "分支"; //1選取最大增益資訊量測試屬性,做分支處理,做處理,注意屬性一旦處理,將不在後續節點中再處理 //因此需要在測試屬性集合中刪除所選測試屬性.注意保持分類屬性在開始索引處(0). PNode.Key = PropertyNames[theMaxPropertyIndex]; CalcNode theCalcNode_2 = thePropertyCount[PropertyNames[theMaxPropertyIndex]]; List<string> theFactors = PropertyFactors[PropertyNames[theMaxPropertyIndex]]; List<int> theAvailableTestPs = new List<int>(); for (int i = 0; i < TestProperties.Count; i++) { if (theMaxPropertyIndex != TestProperties[i]) { theAvailableTestPs.Add(TestProperties[i]); } } //對所選測試屬性的所有因子進行處理. foreach (var theFactor_1 in theFactors) { //如果當前因子不在計算中,則添加一個葉子節點,判定為高機率分類。 if (theCalcNode_2.Children.ContainsKey(theFactor_1) == false) { DecisionTreeNode theNode_1 = new DecisionTreeNode(); theNode_1.ParentFactor = theFactor_1; theNode_1.CalcCount = 0; theNode_1.DecisionValue = theDefaultClassFactor; theNode_1.Parent = PNode; theNode_1.Key = theFactor_1; theNode_1.Type = "葉子"; PNode.Children.Add(theFactor_1, theNode_1); continue; } //如果當前因子存在,但不存在樣本,則添加一個葉子節點,判定為高機率分類。 if (theCalcNode_2.Children[theFactor_1].CalcCount<=0) { DecisionTreeNode theNode_1 = new DecisionTreeNode(); theNode_1.ParentFactor = theFactor_1; theNode_1.CalcCount = 0; theNode_1.DecisionValue = theDefaultClassFactor; theNode_1.Parent = PNode; theNode_1.Type = "葉子"; theNode_1.Key = theFactor_1; PNode.Children.Add(theFactor_1, theNode_1); continue; } //如果存在,且有學習樣本,則添加一個節點,並以此節點遞迴處理. DecisionTreeNode theNode_2 = new DecisionTreeNode(); theNode_2.ParentFactor = theFactor_1; theNode_2.Parent = PNode; theNode_2.Key = theFactor_1; theNode_2.CalcCount = theCalcNode_2.Children[theFactor_1].CalcCount; theNode_2.DataIndexes.AddRange(theCalcNode_2.Children[theFactor_1].DataIndexes); PNode.Children.Add(theFactor_1, theNode_2); BuildDecisionTree(CallLevel, ref OutContents, Inputs, theNode_2, PropertyNames, theAvailableTestPs, theDefaultClassFactor, PropertyFactors); } } }
3、測試代碼:
private void button1_Click(object sender, EventArgs e) { DecisionTreeAlg theAlg = new DecisionTreeAlg(); string[][] theInputs = new string[4][]; theInputs[0] = new string[] { "no", "yes", "yes", "yes", "yes", "yes", "no", "yes", "yes", "no" }; theInputs[1] = new string[] { "s", "s", "l", "m", "l", "m", "m", "l", "m", "s" }; theInputs[2] = new string[] { "s", "l", "m", "m", "m", "l", "s", "m", "s", "s" }; theInputs[3] = new string[] { "no", "yes", "yes", "yes", "no", "no", "no", "no", "no", "yes" }; string[] thePropertyName = new string[] {"是否真實帳號","日誌密度","好友密度","是否真實頭像" }; DecisionTreeNode theRootNode = new DecisionTreeNode(); theRootNode.DataIndexes.AddRange(new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); List<int> theTestPs = new List<int>() { 0, 1, 2, 3 }; string theOuts = ""; theAlg.BuildDecisionTree(0,ref theOuts, theInputs, theRootNode, thePropertyName, theTestPs, "", null); this.treeView1.Nodes.Clear(); TreeNode theRoot = new TreeNode(); this.treeView1.Nodes.Add(theRoot); VisitTree(theRoot, theRootNode); this.textBox1.Text = theOuts; } private void VisitTree(TreeNode PNode, DecisionTreeNode PDNode) { PNode.Text = PDNode.Key + "(" + PDNode.Type + ")[判定:"+PDNode.DecisionValue +"]"; foreach (var theNode in PDNode.Children.Values) { TreeNode theTmpNode = new TreeNode(); PNode.Nodes.Add(theTmpNode); VisitTree(theTmpNode, theNode); } }