有日子沒寫博客了,這些天忙着一些雜七雜八的事情,直到某天,老師喊我好好把數據挖掘的算法搞一搞!於是便由再次埋頭看起算法來!說起數據挖掘的算法,我想首先不得的不提起的就是大名鼎鼎的由決策樹算法演化而來的C4.5算法,畢竟這是當年各個“鼻祖”在數據挖掘大會投票結果最高的一個算法了!
那我們現在就來具體看看C4.5算法到底是個什么東東?我想,首先我們應該提起的是決策樹算法,我們首先要弄明白該算法的目的是什么,其本質目的實質就是預測!在一個系統當中,通過輸入某些屬性值可以預測出我們的預測屬性!這么說可能有些繞,我們來舉個現實點的例子來說明。
比如我們判斷一個人能不能結婚,那么每個人就可以作為一個具體的對象,該對象有着很多屬性,比如年齡,性別,帥不帥,工作NB不,有沒有女朋友,是不是富二代6個屬性,而結婚也作為該對象的一個屬性,而”結婚”屬性就可以作為我們的預測屬性!然后根據其他屬性來預測我們的目標屬性--結婚屬性,比如說,年齡:30,性別:男,長的帥,工作不錯,又女朋友,還是富二代!根據這些屬性我們就可以得出該人今年可以結婚!當然這是預測出來的!這時,我們肯定有個疑問了,這是如何預測的呢?這實質上是根據我們的統計數據得出的,比如我們統計10000個人,根據這一萬個人的6個屬性以及目標屬性(結婚)最終得出一組數據,我們用這組數據做成一個決策樹!而其中這10000個人的樣本我們則稱為訓練樣本!
我們還是拿”打高爾夫球”這個經典的例子來作具體研究吧!該例其實就是通過一些列的屬性來決定是否適合打高爾夫!剛剛說了訓練樣本,我們就來看看訓練樣本吧!圖1是我用WPF做了一個簡單的CRUD界面,用來把我們的樣本顯示的展現出來。具體看圖1。

我們從圖中可以看出,該表中共有6列,而每一列中的列名對應一個屬性,而我們以實踐經驗知道,“Day”即日期這個屬性並不能幫我們預測今天是否適合去打Golf.故該屬性我們就應該選擇摒棄!再看該系統中的其他5給屬性。很顯然,圖1中我用紅筆畫出來的屬性“Play Golf”該屬性就是我們的預測屬性。而其他4個屬性“Outlook”(天氣)”、Temperature”(溫度) 、“Humdity”(濕度)、“Windy”(是否刮風)這四個屬性進行判斷今天去 Play Golf。
那我們接下來的工作自然就是根據屬性1-4得出我們的決策樹了!那么我們來想想該決策樹的算法,實質上其遵循一種統一的遞歸模式:即,首先用根節點表示一個給定的數據集(比如在這,就是我們的14個樣本);然后,從根節點開始在每個節點上測試一個特定的屬性,把節點數據集划分成更小的子集(這一步,比如根據屬性Outlook划分,可以划分出三個子集出來,即屬於Sunny的一個子集樣本,屬於Overcast的子集樣本,屬於Rainy的子集樣本),該子集並用子樹進行表示;該過程就開始一直進行,直到子集稱為“純的”,也就是說直到子集中的所有實例都屬於同一個類別,樹才停止生長。
看到這里,可能一些同學還不能理解什么才叫“純”的,我們這里根據該算法推出一個決策樹來,用圖2表示,在這里,我們具體根據圖來說明到底什么是“純”的?先來看圖2。

我們知道該數據集即有14個測試樣例,所謂純的數據集即使指最終的數據集所包含的每一個樣例的最終預測屬性值都是相同的(比如這里全部是Yes 或者全部是No)。比如樣例3,7,12,13這四個樣例的Play Golf值都是Yes。
我們看圖2,首先是根據Outlook屬性進行划分,根據Outlook的三個屬性值(Sunny、Overcast、Rainy)划分出了三個組合,而其中Overcast划分中的集合是“純”的了。故此子樹就停止生長了。而根據Sunny屬性值划分中的樣例集合1,2,8,9,11顯然還不是“純”的(該組樣例中有的PlayGolf是Yes,而有的是No),故需要再次對其進行划分,直到分組中的所有樣例都是“純”的位置,才停止生長。
看到這里,你可能已經明白了該算法的過程。但是你馬上在腦中就會蹦出一個問題來,那就是憑什么先要根據Outlook屬性划分,怎么不是先是根據Humidity屬性划分呢?總不能是隨便根據一個屬性就來划分吧!
在這里我們就需要引入一個信息論里面的概念了---熵。
信息熵是信息論中用於度量信息量的一個概念。一個系統越是有序,信息熵就越低,反之,一個系統越是混亂,信息熵就越高。所以,信息熵也可以是系統有序化程度的一個度量。我們在這里,不妨把信息熵理解為某種特定信息的出現概率。
其具體的計算公式為:
H(x) = E[I(xi)] = E[ log(2,1/p(xi)) ] = -∑p(xi)log(2,p(xi)) (i=1,2,..n)
而在我們C4.5算法中,其目標就是通過合適的提問來獲得信息,實現熵值的下降,我們依次考察每個屬性,計算該屬性導致的熵值下降幅度,而這個下降幅度我們稱為信息增益。
其公式為:

但實際上,信息增益並不能很正確的去預測,信息增益一般偏向於屬性值多的屬性。故我們一般采取的是更為准確的辦法:那就是采取信息增益率,其定義為:
,其中,Gain(a)為屬性a的信息增益,Entropy(a)為a的熵值。
之后,我們通過比較信息增益率,值最大的則為最佳節點。
說了這么多,我們還是上代碼,具體來看如何來計算各個屬性的信息增益率的! 代碼中有詳細注釋哦!
Codeusing System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace C4._5.BLL
{
public class Entropy
{
public int[] statNum = new int[2];//訓練統計結果:0->No 1->Yes
public double EntropyValue = 0;
private int mTotal = 0;
private string mTargetAttribute = "PlayGolf";
public void getEntropy(DataTable samples)
{
CountTotalClass(samples,out statNum[0],out statNum[1]);
EntropyValue = CalcEntropy(statNum[0],statNum[1]);
}
/// <summary>
/// 統計各個樣本集合中所包含的目標屬性Yes或者No的數目
/// </summary>
public void CountTotalClass(DataTable samples,out int no,out int yes)
{
yes = no = 0;
foreach (DataRow aRow in samples.Rows)
{
if ((string)aRow[mTargetAttribute] == "Yes")
yes++;
else if ((string)aRow[mTargetAttribute] == "No")
no++;
else
throw new Exception("出錯!");
}
}
/// <summary>
/// 計算熵值
/// </summary>
/// <returns></returns>
public double CalcEntropy(int no,int yes)
{
double entropy = 0;
double total = (double)(yes + no);
double p = 0;
if (no != 0)
{
p = no / total;
entropy += -p * Math.Log(p,2);
}
if (yes != 0)
{
p = yes / total;
entropy += -p * Math.Log(p, 2);
}
return entropy;
}
/// <summary>
/// 從屬性中的樣本集合中得到yes或者no的數目
/// </summary>
/// <param name="samples"></param>
/// <param name="attribute"></param>
/// <param name="value"></param>
/// <param name="no"></param>
/// <param name="yes"></param>
public void GetValuesToAttribute(DataTable samples, Attribute attribute, string value, out int no, out int yes)
{
no = yes = 0;
foreach (DataRow row in samples.Rows)
{
if ((string)row[attribute.AttributeName] == value)
{
if ((string)row[mTargetAttribute] == "No")
{
no++;
}
else if ((string)row[mTargetAttribute] == "Yes")
{
yes++;
}
else
{
throw new Exception("出錯");
}
}
}
}
/// <summary>
/// 計算信息收益
/// </summary>
/// <param name="samples"></param>
/// <param name="attribute"></param>
/// <returns></returns>
public double Gain(DataTable samples, Attribute attribute)
{
mTotal = samples.Rows.Count;
string[] values=attribute.values;
double sum=0.0;
for (int i = 0; i < values.Length; i++)
{
int no, yes;
no = yes = 0;
GetValuesToAttribute(samples,attribute,values[i],out no,out yes);
if (yes == (yes + no) || no == (yes + no))
{
sum += 0;
}
else
{
sum += (double)(yes + no) / (double)mTotal * (-(double)yes / (double)(yes + no) * Math.Log(((double)yes / (double)(yes + no)), 2) - (double)no / (double)(yes + no) * Math.Log(((double)no / (double)(yes + no)), 2));
}
}
return SplitInfo(samples,mTargetAttribute)- sum;
}
/// <summary>
/// 獲得targetAttribute屬性下的所有屬性值
/// </summary>
/// <param name="samples"></param>
/// <param name="targetAttribute"></param>
/// <returns></returns>
private ArrayList GetDistinctValues(DataTable samples, string targetAttribute)
{
ArrayList distinctValues = new ArrayList(samples.Rows.Count);
foreach (DataRow row in samples.Rows)
{
if (distinctValues.IndexOf(row[targetAttribute]) == -1)
distinctValues.Add(row[targetAttribute]);
}
return distinctValues;
}
/// <summary>
/// 按某個屬性值計算該屬性的熵值
/// </summary>
/// <param name="samples"></param>
/// <param name="attribute"></param>
/// <returns></returns>
public double SplitInfo(DataTable samples, string attribute)
{
ArrayList values = GetDistinctValues(samples,attribute);
for (int i = 0; i < values.Count; i++)
{
if (values[i] == null || (string)values[i] == "")
{
values.RemoveAt(i);
}
}
int[] count=new int[values.Count];
for (int i = 0; i < values.Count; i++)
{
foreach (DataRow aRow in samples.Rows)
{
if ((string)aRow[attribute] == (string)values[i])
count[i]++;
}
}
double entropy = 0;
double total = samples.Rows.Count;
double p = 0;
for (int i = 0; i < values.Count; i++)
{
if (count[i] != 0)
{
p = count[i] / total;
entropy += -p * Math.Log(p,2);
}
}
return entropy;
}
/// <summary>
/// 獲得指定屬性的信息增益率
/// </summary>
/// <param name="samples">樣本集合</param>
/// <param name="attribute"></param>
/// <returns></returns>
public double GainRatio(DataTable samples, Attribute attribute)
{
double splitInfoA = this.SplitInfo(samples,attribute.AttributeName);//計算各個屬性的熵值
double gainA = Gain(samples,attribute);//信息增益
double gainRatioA = gainA / splitInfoA;
return gainRatioA;
}
}
}
寫到這里,我想一個關鍵的問題--如何選取最佳節點已經解決掉了,那么下一步就開始我們算法了!到底如何構造決策樹呢?還記得剛剛說的遞歸模式嗎!具體看代碼吧!在代碼中有詳細的解釋。
Code /// <summary>
/// 構造決策樹
/// </summary>
/// <param name="samples">樣本集合</param>
/// <param name="targetAttribute">目標屬性</param>
/// <param name="attributes">該樣本所含的屬性集合</param>
/// <returns></returns>
private TreeNode BuildTree(DataTable samples, string targetAttribute, Attribute[] attributes)
{
TreeNode temp = new TreeNode();
//如果samples中的元祖是同一類C
string c = AllSamplesSameClass(samples,targetAttribute);
if (c != null) //返回N作為葉節點,以類C標記
return new TreeNode(new Attribute(c).AttributeName + c);
//if attributes為空,then
if (attributes.Length == 0)//返回N作為葉子節點,標記為D中的多數類,多數表決
{
return new TreeNode(new Attribute(GetMostCommonValue(samples,targetAttribute)).AttributeName);
}
//計算目標屬性的熵值,即PlayGolf的熵值
mTargetAttribute = targetAttribute;
en.getEntropy(samples);
//找出最好的分類屬性,即信息熵最大的
Attribute bestAttribute = getBestAttribute(samples,attributes);
//標記為節點root
DTreeNode root = new DTreeNode(bestAttribute);
temp.Text = bestAttribute.AttributeName;
DataTable aSample = samples.Clone();
//為bestAttribute的每個輸出value划分元祖並產生子樹
foreach (string value in bestAttribute.values)
{
aSample.Rows.Clear();
//aSamples為滿足輸出value的集合,即一個划分(分支)
DataRow[] rows = samples.Select(bestAttribute.AttributeName+"="+"'"+value+"'");
foreach (DataRow row in rows)
{
aSample.Rows.Add(row.ItemArray);
}
//刪除划分屬性
ArrayList aArributes = new ArrayList(attributes.Length-1);
for (int i = 0; i < attributes.Length; i++)
{
if (attributes[i].AttributeName != bestAttribute.AttributeName)
{
aArributes.Add(attributes[i]);
}
}
//如果aSample為空,加一個樹葉到節點N,標記為aSample中的多數類
if (aSample.Rows.Count == 0)
{
TreeNode leaf = new TreeNode();
leaf.Text = GetMostCommonValue(samples, targetAttribute).ToString() + "(" + value + ")";
temp.Nodes.Add(leaf);
}
else //加一個由BulidTree(samples,targetAttribute,attributes)返回的節點到節點N
{
DTree_ID3 dc3 = new DTree_ID3();
TreeNode ChildNode = dc3.BuildTree(aSample,targetAttribute,(Attribute[])aArributes.ToArray(typeof(Attribute)));
ChildNode.Text += "(" + value + ")";
temp.Nodes.Add(ChildNode);
}
}
roots = temp;
return temp;
}
最終生成如圖3的決策樹。
既然生成了決策樹,那么我們自然就要真正實時的來一把預測了!根據生成的決策樹進行預測了!界面如圖4所示。

這時,我們在4個屬性中輸入我們所需輸入的值,然后直接點擊“預測”按鈕就可以得到我們今天是否去PlayGolf! 如圖5所示。
事實上,我們還沒有對其預測的准確性進行判斷,以及如何提高其准確性呢?那么下一篇將講講決策樹剪枝來說明這一問題!
大功告成!不妨你也來試一把,看看你是否去PlayGolf!下面附上整個源碼。
點擊此處下載C4.5示例代碼