1. 程式人生 > >學習資料探勘決策樹ID3演算法

學習資料探勘決策樹ID3演算法

微笑一個月前的C語言程式設計課上學習了決策樹ID3演算法
然後自己用了兩個多星期的時間開始用C語言實現,結果由於過程太過於複雜,寫出來的東西就跟屎一樣。
可能是自己對於這個演算法理解的不夠深刻,或者是在設計的時候沒有構思好。
所以決定在這裡寫一寫大概的構思然後再去用C實現。
這樣可能會更加有效率一點。


決策樹之ID3演算法:


ID3演算法的實質是檢索哪個屬性的分類能力更強,然後用拿個分類能力強的屬性將資料分類,然後繼續檢索繼續分類。

這樣最後分完之後就會是一個樹的結構。

分到最後的時候資料會到達一定的純度。

之後你拿一個樣本過來,順著這個樹往下找,找到它所屬於的那一堆資料裡,也就是葉節點上。

就能根據這一堆資料生和死的比例來對樣本進行判斷。

如何判斷一個屬性的分類能力?

根據夏農的資訊理論。夏農定義了一個名叫資訊熵的東西,來說明一個系統的資訊穩定程度。

一個系統的資訊熵越小,那麼用它分類出的系統就越穩定,它的分類能力就比較強。


資訊熵的計算公式:(n1 / m) * log2(n1 / m) + (n2 / m) * log2(n2 / m) + ........... +  (nn / m) * log2(nn / m)


具體的構建步驟:(以泰坦尼克號的資料為訓練模型)


訓練資料的結構:(第一列是生死標籤)


//上一節課老師跟我說可以不用樹的結構去構建資料結構,但是我實在不知道該怎麼寫,在他的口中用所謂字元陣列的結構去做要比我這個簡單很多。

//所以在這裡還是採用我以前想的那個樹的結構去做。

第一步,我們需要有一個函式來讀取檔案資料,我們選擇一個二維陣列去儲存。
我覺得這個函式還是不錯的。

int getData()//獲取資料
{
	FILE *p;
	if (auto err = fopen_s(&p, "C:\\Users\\XueChuanyu\\Desktop\\test.txt", "r") == NULL)
	{
		printf("\nNo sucn a file");
	}

	int i = 0, k = 0;
	for (i = 0; i < DATASIZE; i++)
	{
		for (k = 0; k < 8; k++)
		{
			fscanf_s(p, "%lf", &data[i][k]);
			if (k == 3)
			{
				disperseAge(data[i][3], 15);
			}
			if (k == 6)
			{
				disperseAge(data[i][6], 70);
			}
		}
	}

	fclose(p);

	return 0;

}


但是我們有很多資料都是連續的資料,所以要先進行離散化
這個函式也還算能用的。
double disperseAge(double x, int different)// 根據離散間隔值離散化
{

	if (x < different)
	{
		x = 0;
		return x;
	}
	else
	{
		if (different <= x && x <= 2 * different)
		{
			x = 1.0;
			return x;
		}
		else
		{
			if (2 * different < x && x <= 3 * different)
			{
				x = 2.0;
				return x;
			}
			else
			{
				x = 3.0;
				return x;
			}
		}
	}
}

下一步我們需要有計算熵的函式,之前寫的東西寫之前還覺得寫的挺好的,但是寫完了了以後一個星期看根本看不懂了。
事實上我覺得是因為資料結構沒想好就寫,然後寫了之後發現不合適,所以程式碼都看不懂了。
所以現在先來模擬一下該如何去做。
首先這個計算熵的函式必須具有通用性,也就是在每一次進行分類的時候都能用,所以這個檢索也就變成了在某一個數據集裡檢索某個屬性的熵。

所以這就要求我們必須在每個節點能夠知道目前此節點的資料集是什麼,所以我們的節點至少是這樣的。

struct node
{
	int includeData[1000];
};


2017.04.10

今天上午上課的時候測試了一下自己之前寫的那個在某個節點判斷熵值的函式,發現還是可以用的,用那個訓練集可以大概算出來在第一個節點的時候(也就是包含所有資料的節點)性別是最好的分類依據,雖然實現的效率和方法不是太好,但是畢竟也已經寫出來了。自己也懶得改了,這樣查詢熵值的工作就完成了,下一步就只剩下根據訓練集建樹了。


(老師上課講,當初沒考慮到,現在自己試試用C去實現,確實很複雜,可能沒個1000行寫不出來,如果你不用各種庫函式會相當痛苦)

微笑然而我已經痛苦好幾個星期了,已經麻木。

      自己的隊友已經放棄C,去用C++這種高階魔法去做了。

     但我覺得我的程式碼還能再搶救一下。

下面貼出實現資料檢索和熵值計算的函式。


檢索在某一個節點(資料集)中某一屬性下滿足某個元素時另一屬性中某元素的數量

這個函式是直接從之前的樸素貝葉斯演算法中拉過來的,感覺用在這個模型上有點牽強了,感覺就是這個檢索拖累了我。
這個函式的優勢是可以檢索單一屬性中的元素,也可以同時檢索兩個屬性,但是可能感覺有點難理解。

int checker(node *p, int typeSon, int typeDad, int sampleSon, int sampleDad)//在某節點中某屬性下某屬性的數量
{
	int result = 0, i = 0;
	for (i = 0; i < DATASIZE; i++)
	{
		if (p->typeNumber[i] != 0 && data[i][typeSon] == sampleSon && data[i][typeDad] == sampleDad)
		{
			result++;
		}
	}

	return result;
}


計算在某個節點(資料集)中某個特定屬性的熵值

計算在某個節點的熵值,前面的這個檢索某屬性的種類,為的是能夠提高程式的通用性。

類似於建立一個元素列表吧,告訴你這個屬性下有什麼元素。

但是實際上針對於這個資料其實可以完全用個一維陣列。。。前面那部分弄麻煩了,但是也能用。

double entropy(node *p, int type)//計算某節點中某類別的熵值
{
	int property[DATASIZE][3] = {0};//property[某種屬性][0:總數,1:對應存活,2:對應死亡]
							 //用於記錄各屬性及其對應值
	int dataSize = 0;
	int flag = 1;
	double entropy = 0;

	for (int i = 0; i < DATASIZE - 1; i++)//知道型別下屬性的種類
	{
		if (flag && p->typeNumber[i] != 0)
		{
			int temp = data[i][type];
			property[temp][0]++;
			flag = 0; // 獲得首個數據
		}
		else
		{
			continue;
		}

		if (p->typeNumber[i] != 0 && data[i][type] != data[i + 1][type])
		{
			int temp = data[i + 1][type];
			property[temp][0]++;//如果和前面的資料不同 再新增一個型別
		}
	}

	for (int i = 0; i < 8; i++)// 這個i有屬性值的作用
	{
		if (property[i][0] != 0)
		{
			property[i][0] = checker(p, type, type, i, i);//每種屬性的數量
			dataSize += property[i][0];
			property[i][1] = checker(p, type, 0, i, 1);//在此屬性下生存的數量
			property[i][2] = checker(p, type, 0, i, 0);//在此屬性下死亡的數量
		}
	}
	
	for (int i = 0; i < 8; i++)//計算熵
	{
		if (property[i][0] != 0)
		{
			entropy -= ((double)property[i][0] / dataSize) * (((double)property[i][1] / property[i][0]) * (log2((double)property[i][1] / property[i][0])));
		}
	}

	return entropy;

}

比較在某節點下所有屬性的分類能力

一個小函式,沒啥可說的。

int compare(node *p)//在某個節點下尋找最優劃分型別
{
	int i = 0, result = 0;
	double lastType = 1, nowType = 1;

	for (i = 1; i < 8; i++)
	{
		nowType = entropy(p, i);
		
		if (nowType < lastType)
		{
			lastType = nowType;
			result = i;
		}
	}

	return result;
}

考慮了一下,作為一個樹的結構,沒個節點至少提供三個資訊


1.該節點包含的資料集
2.記錄該節點是以什麼分類的
3.該節點上掛載的子節點


所以做了這樣的節點

struct node
{
	int typeNumber[1000];
	double entropy;
	node *pNode[MAXTYPE];
};

如何建一棵樹?



首先需要一個建立節點的函式:



函式需要得到的資訊:


1. 當前所屬的節點以及是否為空,以便知道下一步該如何建樹。

2. 若當前節點的純度到達一定程度結束函式。

2. 在當前節點下最優的分類屬性(呼叫compare函式)。

3. 根據分類屬性所擁有元素的數量呼叫建立節點的函式並建立節點(遞迴實現,該函式要返回一個指標,給上一個節點的指標陣列)。

本來是打算用熵值作為結束樹的條件的

但是如果用熵找不到好的衡量標準

所以寫了這個計算純度的函式

double getPureValue(node *p)
{
	int dead = 0, alive = 0;

	for (int i = 0; i != DATASIZE; i++)
	{
		if (p->typeNumber[i] == 1)
		{
			if (data[i][0] == 0)
			{
				dead++;
			}
			else
			{
				alive++;
			}
		}
	}
	return (dead > alive) ? (dead / static_cast<double>(dead + alive)) : (alive / static_cast<double>(dead + alive));

}


大哭

這裡是前天寫好的建樹。。但是有些小問題。。

出現了這樣的狀況。。。跟平時野指標記憶體洩漏棧溢位的情況都不一樣。。。暫時還沒找到解決方法,感覺邏輯還是沒什麼問題的

如果哪位大神找到錯誤可以在下面評論。。感覺應該不是爆棧。。



貼下建樹碼

node* bulidTree(node *pSender, int bestType, int devideCondition)
{
	node *dadP = NULL;

	if (pSender == NULL)
	{
		dadP = new node;

		for (auto &i : dadP->typeNumber)
		{
			i = 1;
		}

		int bestType = compare(dadP);
		isUsed[bestType] = true;
		dadP->purity = getPureValue(dadP);

		int nodeAmount = typeAmount(dadP, bestType);

		for (int i = 0; i != nodeAmount; i++)
		{
			dadP->pNode[i] = bulidTree(dadP, bestType, i);
		}
		
	}
	else
	{
		dadP = pSender;
		
		if (dadP->purity > 0.75)
		{
			return NULL;
		}
		else
		{
			node *sonP = new node;
			
			for (int i = 0; i != DATASIZE; i++)
			{
				if (data[i][bestType] == devideCondition && dadP->typeNumber[i] != 0)
				{
					sonP->typeNumber[i] = 1;
				}
				else
				{
					sonP->typeNumber[i] = 0;
				}
			}

			int bestestType = compare(sonP);
			isUsed[bestestType] = true;
			sonP->purity = getPureValue(sonP);

			int nodeAmount = typeAmount(sonP, bestestType);

			for (int i = 0; i != nodeAmount; i++)
			{
				sonP->pNode[i] = bulidTree(sonP, bestestType, i);
			}

			delete dadP;
			return sonP;

		}

	}
}

之前寫的那個東西,在除錯的時候出現了不停在兩個屬性迴圈分類的情況

後來發現ID3演算法是不能重複用某個屬性的,原理如下


所以這個還是未完成吧

不過暫時這個就到這一段落了

下面要學習OpenCV和python

這個以後有時間再慢慢研究了

2017/4/24

建樹的問題解決了

主要
之前出現了兩個問題

1.之前在函式開始的時候建立的一個指標,把那個指標刪掉改為用後面直接用new建立node物件,就沒有了已觸發了一個斷點的東西

   分析可能是以前的那種建立指標的方式,在後來用到了沒有開闢空間的指標,也就是用到了一個空指標的情況

2.出現的在建樹過程中,純度不斷提高,到最後突然變成66。6%的情況。

   原因是這個ID3選用不同分類屬性的時候是不能重複使用分類屬性的,所以在後來分類的屬性肯定越來越差(好的都被用完了)

   所以用一個超差的分類屬性對較少的資料進行分類,結果就是出現這種純度下降的情況,解決辦法就是設定一個根據樹的深度停止的判斷

   新的程式碼貼到如下:

node* bulidTree(node *pSender, int bestType, int devideCondition)
{
	if (pSender == NULL)
	{
		node *dadP = new node;

		for (auto &i : dadP->typeNumber)
		{
			i = 1;
		}

		int bestType = compare(dadP);
		isUsed[bestType] = true;
		dadP->purity = getPureValue(dadP);
		dadP->deep = 0;
		int nodeAmount = typeAmount(dadP, bestType);

		for (int i = 0; i != nodeAmount; i++)
		{
			dadP->pNode[i] = bulidTree(dadP, bestType, i);
		}
		
	}
	else
	{
		node *dadP = pSender;
		
		if (dadP->purity > 0.8 || dadP->deep >= 3)
		{
			return NULL;
		}
		else
		{
			node *sonP = new node;
			sonP->deep = dadP->deep + 1;
			
			for (int i = 0; i != DATASIZE; i++)
			{
				if (data[i][bestType] == devideCondition && dadP->typeNumber[i] != 0)
				{
					sonP->typeNumber[i] = 1;
				}
				else
				{
					sonP->typeNumber[i] = 0;
				}
			}

			int bestestType = compare(sonP);
			isUsed[bestestType] = true;
			sonP->purity = getPureValue(sonP);

			int nodeAmount = typeAmount(sonP, bestestType);

			for (int i = 0; i != nodeAmount; i++)
			{
				sonP->pNode[i] = bulidTree(sonP, bestestType, i);
			}

			return sonP;

		}

	}
}