樸素貝葉斯演算法的實現與推理

Cylon 發表於 2022-05-06
演算法

什麼是naive bayes

樸素貝葉斯 naive bayes,是一種概率類的機器學習演算法,主要用於解決分類問題

為什麼被稱為樸素貝葉斯?

為什麼被稱為樸素,難道僅僅是因為貝葉斯很天真嗎?實際上是因為,樸素貝葉斯會假設資料屬性之間具有很強的的獨立性。即該模型中的所有屬性彼此之間都是獨立的,改變一個屬性的值,不會直接影響或改變演算法中其他的屬性的值

貝葉斯定理

瞭解樸素貝葉斯之前,需要掌握一些概念才可繼續

  • 條件概率 Conditional probability:在另一個事件已經發生的情況下,另外一個時間發生的概率。如,在多雲天氣,下雨的概率是多少? 這是一個條件概率
  • 聯合概率 Joint Probability:計算兩個或多個事件同時發生的可能性
  • 邊界概率 Marginal Probability:事件發生的概率,與另一個變數的結果無關
  • 比例 Proportionality
  • 貝葉斯定理 Bayes' Theorem:概率的公式;貝葉斯定律是指根據可能與事件的先驗概率描述了事件的後驗概率

邊界概率

邊界概率是指事件發生的概率,可以認為是無條件概率。不以另一個事件為條件;用公式表示為 \(P(X)\) 如:抽到的牌是紅色的概率是 \(P(red) = 0.5\)

聯合概率

聯合概率是指兩個事件在同一時間點發生的可能性,公式可以表示為 \(P(A \cap B)\)

AB 是兩個不同的事件相同相交,$P(A \and B), $ \(P(A,B)\) = AB 的聯合概率

概率用於處理事件或現象發生的可能性。它被量化為介於 0 和 1 之間的數字,其中 0 表示不可能發生的機會,1 表示事件的一定結果。

如,從一副牌中抽到一張紅牌的概率是 \(\frac{1}{2}\)。這意味著抽到紅色和抽到黑色的概率相同;因為一副牌中有52張牌,其中 26 張是紅色的,26 張是黑色的,所以抽到一張紅牌與抽到一張黑牌的概率是 50%。

而聯合概率是對測量同時發生的兩個事件,只能應用於可能同時發生多個情況。例如,從一副52張牌撲克中,拿起一張既是紅色又是6的牌的聯合概率是 \(P(6\cap red) = \frac{2}{52} = \frac{1}{26}\) ;這個是怎麼得到的呢?因為抽到紅色的概率為50%,而一副牌中有兩個紅色6(紅桃6,方片6),而6紅色是兩個獨立的概率,那麼計算公式就為:\(P(6 \cap red) = P(6) \times P(red) = \frac{4}{52} \times \frac{26}{52} = \frac{1}{26}\)

在聯合概率中 $ \cap $ 稱為交集,是事件 A 與 事件 B 發生的概率的相交點,通過圖來表示為:兩個圓的相交點,即6和紅色牌共同的部分

樸素貝葉斯演算法的實現與推理

條件概率

條件概率是指事件發生的可能性,基於先有事件的結果的發生乘後續事件概率來計算的,例如,申請大學的通過率為80%,宿舍僅提供給60%學生使用,那麼這個人被大學錄取並提供宿舍的概率是多少?

\(P(accept\ and\ get\ dorm) = P(Accept|Dorm) = P(Accept) \times P(Dorm) = 0.8 \times 0.6 = 0.48\)

條件概率將會考慮兩個事件之間的關係,例如你被大學錄取的概率, 以及為你提供宿舍的概率;條件概率中的關鍵點

  • 另一個事件發生的情況下,這件事發生的機率
  • 表示為,給定 B 的概率 A 發生的概率,用公式表示為:\(P(A|B)\),其中 A 取決於 B 發生的概率

通過例子瞭解條件概率

上述大致上瞭解到了:條件概率取決於先前的結果,那麼通過幾個例子來熟悉條件概率

例1:袋子裡有紅色,藍色,綠色三顆玻璃球,每種被拿到的概率相等,那麼摸到藍色之後再摸到紅色的條件概率是多少?

  • 這裡需要先得到摸到藍色的概率:\(P(Blue) = \frac{1}{3}\) 因為只有三種可能性
  • 現在還剩下兩顆玻璃球 紅色和藍色,那麼摸到紅色的概率為:\(P(Red) = \frac{1}{2}\) 因為只有兩種可能性
  • 那麼已經摸到藍色在摸到紅色的概率為 \(P(Red|Blue) = \frac{1}{3} \times \frac{1}{2} = \frac{1}{6}\)

例2:色子搖出5的概率為 \(\frac{1}{6}\) 那麼在結果是奇數裡搖出5 那麼可能就是 \(\frac{1}{3}\),而這個奇數就是另外的一個條件,因為只有3個奇數,其中一個是5,那麼在奇數中,丟擲5的概率就是 \(\frac{1}{3}\)

通過上述資訊可知,B 作為附帶條件修飾 A 發生的概率,稱為給定 BA 發生的條件,用\(P(A|B)\) 表示。那麼可以的出:

  • 給定A,B發生的概率為,A和B的發生概率排除掉A的概率,即
  • $P(B|A) = \frac{P(A \cap B)}{P(A)} $

聯合概率和條件概率的區別

條件概率是一個事件在另一個事件發生的情況下的概率:$P(X,given Y) $ 公式為: \(P(X∣Y)\);即一個事件發生的概率取決於另一事件的發生;如:從一副牌中,假設你抽到一張紅牌,那麼抽到6的概率是 \(\frac{1}{13}\);因為26張紅牌中僅有兩張為6,用公式表示:\(P(6|red) = \frac{2}{26}\)

聯合概率僅考慮兩個事件發生的可能性,對比與條件概率可用於計算聯合概率:\(P(X \cap Y) = P(X|Y) \times P(Y)\)

通過合併上述例子得到,同時抽到6和紅色的概率為:\(\frac{1}{26}\)

貝葉斯定理

貝葉斯定理是確定條件概率的數學公式。貝葉斯定理依賴於先驗概率分佈以計算後驗概率。

後驗概率和先驗概率

  • 先驗概率 prior probability:在收集新資料之前傳送事件的概率
  • 後驗概率 posterior probability:得到新的資料來修正之前事件發生的概率;換句話說是後驗概率是在事件 B 已經發生的情況下,事件 A 發生的概率

例,從一副52 張牌中抽取一張牌,那麼這張牌是K的概率是 \(\frac{4}{52}\) , 因為一副牌中有4張K;假設抽中的牌是一張人物牌,那麼抽到是K的概率則是 \(\frac{4}{12}\);因為一副牌中有12張人物牌。那麼貝葉斯定理的公式為:

  • \(P(A|B) = \frac{P(A \cap B)}{P(B)}\)\(P(B|A) = \frac{P(B \cap A)}{P(A)}\)
    • \(P(A \cap B)\)\(P(B \cap A)\) A和B同時發生和B和A同時發生時相等的
    • \(P(B \cap A) = P(B|A) \times P(A)\)
    • \(P(A \cap B) = P(A|B) \times P(B)\)
  • 那麼根據上面的公式,已知 \(P(A \cap B) = P(B \cap A)\) 可推匯出公式:
    • 因為 \(P(B \cap A) = P(A \cap B)\) ,那麼 \(P(B|A) \times P(A) = P(A|B) \times P(B)\)
    • 那麼吧 \(P(A)\) 放置等式右邊即 \(P(B|A) = \frac{P(A|B) \times P(B)}{P(A)}\)
  • 那麼最終 Formula for Bayes\(P(B|A) = \frac{P(A|B) \times P(B)}{P(A)}\)

其中:

  • \(P(A)\)A 的邊界概率

  • \(P(B)\)B 的邊界概率

  • \(P(A|B)\) :條件概率,已知 BA 的概率

  • \(P(B|A)\) :條件概率,已知 AB 的概率

  • \(P(B \cap A)\):聯合概率 BA 同時發生的概率

一個簡單的概率問題可能會問:茅臺股價下跌的概率是多少?條件概率通過詢問這個問題更進一步:鑑於A股平均指數下跌,茅臺股價下跌的概率是多少? 給定 B 已經發生的條件下 A 的概率可以表示為:

\(P(Mao|AS) = \frac{P(Mao \cap AS)}{P(AS)}\)

\(P(Mao \cap AS)\) 是 A 和 B 同時發生的概率,與 A 發生的情況下 B 也發生的概率 乘 A 發生的概率相等表示為: \(P(Mao) \times P(AS|Mao)\);這兩個表示式相等,也就是貝葉斯定理,可以表示為:

  • 如果, \(P(Mao \cap AS) = P(Mao) \times P(AS|Mao)\)
  • 那麼, \(P(Mao|AS) = \frac{P(Mao) \times P(AS|Mao)}{P(AS)}\)

\(P(Mao)\)\(P(AS)\) 分別為茅臺和A股的下跌概率,彼此間沒有關係

一般情況下,都是以 x (輸入) y (輸出) 在函式中,假設 \(x=AS\) , \(y=MAO\) 那麼替代到公式中就 \(P(Y|X) = \frac{P(X|Y) \times P(Y)}{P(X)}\)

樸素貝葉斯演算法

樸素貝葉斯不是一個的演算法,而是一組演算法,所有這些演算法都基於一個共同的原則,即每一對被分類的特徵必須相互獨立。樸素貝葉斯是一個基本的貝葉斯稱呼,包含三種演算法的集合:多項式 Multinomial、 伯努利 Bernoulli、高斯 Gaussian

伯努利

伯努利樸素貝葉斯,又叫做二項式,只接受二進位制值,簡而言之,在伯努利中必須計算每個值的二進位制出現特徵,即一個單詞是與否出現在文件中。

通俗地來說,伯努利有兩個互斥的結果:

樸素貝葉斯演算法的實現與推理

在伯努利中,可以有多個特徵,但每個特徵都假設為是二進位制的變數,因此,需要將樣本表示為二進位制向量。

那麼擴充套件出的公式為:$P(A|B) = \frac{P(B|A) \times P(A)}{P(A) \times P(B|A) + P(not A) \times P(B|not A)} $

例子:假設COVID-19測試並不準確,有95%機率在感染時測試出陽性(敏感性),這就意味著如果有人並未感染的概率是相同的(特異性);問:如果Jeovanna檢測為陽性,那麼他感染COVID-19的概率是多少?

敏感性和特異性是醫學用語;敏感性,病人測出陽性的比例,特異性,非病人測試陰性的比例

一般情況下沒有更多的資訊來確定Jeovanna是否感染,如駐留場所,是否發燒,喪失味覺等。就需要更多的資訊來計算Jeovanna感染率,比如預估Jeovanna感染率為1%,這1%就是先驗概率。

此時有100000人的測試樣本,預計1000人感染(先驗1%),那麼就是99000為感染,又因為測試具有 95% 的敏感性和 95% 的特異性,這代表了 1000的95% 和 99000的5% 是陽性。整理一個表格

Has COVID-19 Do not Has COVID-19 count
陽性 950 4950 5900
陰性 50 94050 94100

那麼可以看出,陽性的人並感染COVID-19的概率是,\(\frac{950}{5900} = 16\%\) ;那麼也就是Jeovanna有16%機率是感染 COVID-19。

此時將先驗概率設定為16%,那麼愛麗絲得COVID-19的可能性為:

\(P(B|A)\) :在95%成功率情況下又獲得了陽性

\(P(A)\):陽性的檢測成功率

已知,\(P(B|A) = 0.16\)\(P(A) = 0.95\)

\(P(A|B) = \frac{P(B|A) \times P(A)}{P(A) \times P(B|A) + P(not A) \times P(B|not A)} = \frac{0.16\times0.95}{0.95\times 0.16 + (1-0.95)\times(1-0.16)}= \frac{0.152}{0.194} = 0.7835 = 78.35\%\)

那麼就可以得知,在陽性情況下感染COVID-19的情況下,再去檢測會有78%機率陽性

多項式

多項式樸素貝葉斯是基於多項分佈的樸素葉貝斯,用來處理文字,計算 \(d\)\(c\) 中的概率計算如下:

\(P(c|d) \ \propto P(c) \prod_{i=1}^n\ P(t_k|c)\)

通俗來說就是二項式的一個變種,是計算多個不同的例項

\(P(t_k|c)\)\(t_k\) 的 條件概率,發生在資料集 \(c\) 中,\(P(t_k|c)\) 解釋為 \(t_k\) 有多少證據表明 \(c\) 是正確的;\(P(c)\) 是先驗條件 \(t1..\ t2..\ t3..\ tn_d\) 中的標記 \(d\),它們是我們用於分類的詞彙表的一部分,\(n_d\) 是 標記 \(d\) 的數量。

例如:"Peking and Taipei join the WTO",\(<Peking,\ Taipei,\ join,\ WTO>\) ,那麼 \(n_d = 4\)

那麼可以簡化為,

\(P(c=x|d=c_k) = P(c^1=x^1..,\ c^2=x^2..,\ c^n=x^n|d=c_k) = \prod_{i=1}^n(c^i|d)x^i + (1-P(c^i|d)) (1-x^i)\)

\(\prod_{i=1}^n\) 連乘積,即從下標起乘到上標

樸素貝葉斯實現

首先將樸素貝葉斯為 5 個部分:

  • 分類
  • 資料集彙總
  • 按類別彙總資料
  • 高斯密度函式
  • 分類概率

分類

根據資料所屬的類別來計算資料的概率,即所謂的基本率。

先建立一個字典物件,其中每個鍵都是類值,然後新增所有記錄的列表作為字典中的值。

假設每行中的最後一列是型別。

# 按類拆分資料集,返回結構是一個詞典
def separate_by_class(dataset):
	separated = dict()
	for i in range(len(dataset)):
		vector = dataset[i]
		class_value = vector[-1] # dataset最後一行是類別
		if (class_value not in separated):
			separated[class_value] = list()
		separated[class_value].append(vector)
	return separated

準備一些資料集

X1				X2				Class
3.393533211		2.331273381				0
3.110073483		1.781539638				0
7.423436942		4.696522875				1
1.343808831		3.368360954				0
3.582294042		4.67917911				0
9.172168622		2.511101045				1
7.792783481		3.424088941				1
2.280362439		2.866990263				0
5.745051997		3.533989803				1
7.939820817		0.791637231				1

測試分類函式的功能

def separate_by_class(dataset):
	separated = dict()
	for i in range(len(dataset)):
		vector = dataset[i]
		class_value = vector[-1]
		if (class_value not in separated):
			separated[class_value] = list()
		separated[class_value].append(vector)
	return separated
 
# 測試資料集
dataset = [
    [3.393533211,2.331273381,0],
	[3.110073483,1.781539638,0],
	[1.343808831,3.368360954,0],
    [7.423436942,4.696522875,1],
	[3.582294042,4.67917911,0],
    [9.172168622,2.511101045,1],
	[7.792783481,3.424088941,1],
	[2.280362439,2.866990263,0],
	[5.745051997,3.533989803,1],
	[7.939820817,0.791637231,1]
]
separated = separate_by_class(dataset)
for label in separated:
	print(label)
	for row in separated[label]:
		print(row)

可以看到分類是成功的

樸素貝葉斯演算法的實現與推理

資料集彙總

現在需要對給出資料集的兩個資料進行統計,如何對指定資料集做概率計算?需要以下幾步

計算資料集兩個資料的平均值和標準差

平均值為: \(\frac{sum(x)}{n} \times count(x)\) ;其中 \(x\) 為正在查詢值的列表

mean函式用於計算平均值

def mean(numbers):
	return sum(numbers) / float(len(numbers))

樣本標準差的計算方式為平均值的平均差。公式可以為 sqrt((sum i to N (x_i – mean(x))^2) / N-1)

函式用來計算

from math import sqrt
 
# Calculate the standard deviation of a list of numbers
def stdev(numbers):
	avg = mean(numbers) # 平均值
	variance = sum([(x-avg)**2 for x in numbers]) / float(len(numbers)-1)
	return sqrt(variance)

還需要對每個資料的每一列計算平均值和標準偏差統計量。通過將每列的所有值收集到一個列表中並計算該列表的平均值和標準差。計算完成後,將統計資訊收集到資料彙總的列表或元組中。然後,對資料集中的每一列重複此操作並返回統計元組列表。

下面是 資料彙總的函式 summarise_dataset()用來統計每列列表的平均值和標準差

from math import sqrt
 
# 計算平均數
def mean(numbers):
	return sum(numbers)/float(len(numbers))
 
# 計算標準差
def stdev(numbers): # 標準差
	avg = mean(numbers) # 計算平均值
	variance = sum([(x-avg)**2 for x in numbers]) / float(len(numbers)-1) # 計算所有的平方
	return sqrt(variance)
 
# 資料彙總
def summarize_dataset(dataset):
    summaries = [(mean(column), stdev(column), len(column)) for column in zip(*dataset)]
    del(summaries[-1]) # 因為分類不需要所以。刪除掉分類哪行
    return summaries
 
# Test summarizing a dataset
dataset = [
    [3.393533211,2.331273381,0],
	[3.110073483,1.781539638,0],
	[1.343808831,3.368360954,0],
	[3.582294042,4.67917911,0],
	[2.280362439,2.866990263,0],
	[7.423436942,4.696522875,1],
	[5.745051997,3.533989803,1],
	[9.172168622,2.511101045,1],
	[7.792783481,3.424088941,1],
	[7.939820817,0.791637231,1]]
summary = summarize_dataset(dataset)
print(summary)

這裡使用的是zip() 函式,將每列作為提供的引數。使用 * 作為位置函式,運將資料集傳遞給 zip() ,這個運算會將每一行的分割為單獨列表。然後zip() 遍歷每一行的每個元素,返回一列作為數字列表。

然後計算每列中的平均數、標準差和行數。刪掉不需要的列(第三列類別列的平均數,標準差和行數)

可以看到

[
(5.178333386499999, 2.7665845055177263, 10), 
(2.9984683241, 1.218556343617447, 10)
]

根據類別彙總資料

separate_by_class() 是將資料分成行,現在要編寫一個 summarise_dataset();是先計算每列的統計彙總資訊,然後在按照子集分類(X1,X2)

# 按類拆分資料集
def summarize_by_class(dataset):
	separated = separate_by_class(dataset)
	summaries = dict()
	for class_value, rows in separated.items():
		summaries[class_value] = summarize_dataset(rows)
	return summaries

這是完整的程式碼

from math import sqrt
 
# 計算平均數
def mean(numbers):
	return sum(numbers)/float(len(numbers))
 
# 計算標準差
def stdev(numbers): # 標準差
	avg = mean(numbers) # 計算平均值
	variance = sum([(x-avg)**2 for x in numbers]) / float(len(numbers)-1) # 計算所有的平方
	return sqrt(variance)
 
# 資料彙總
def summarize_dataset(dataset):
    summaries = [(mean(column), stdev(column), len(column)) for column in zip(*dataset)]
    del(summaries[-1]) # 因為分類不需要所以。刪除掉分類哪行
    return summaries
 
# 按類進行資料彙總
def summarize_by_class(dataset):
	separated = separate_by_class(dataset)
	summaries = dict()
	for class_value, rows in separated.items():
		summaries[class_value] = summarize_dataset(rows)
	return summaries
 
# 測試資料集
dataset = [
    [3.393533211,2.331273381,0],
	[3.110073483,1.781539638,0],
	[1.343808831,3.368360954,0],
	[3.582294042,4.67917911,0],
	[2.280362439,2.866990263,0],
	[7.423436942,4.696522875,1],
	[5.745051997,3.533989803,1],
	[9.172168622,2.511101045,1],
	[7.792783481,3.424088941,1],
	[7.939820817,0.791637231,1]]
summary = summarize_by_class(dataset)
for label in summary:
	print(label)
	for row in summary[label]:
		print(row)

按照分類,對每列計算平均值和標準差

樸素貝葉斯演算法的實現與推理

高斯分佈

高斯分佈 Gaussian distribution 可以用兩個數字來概括,高斯分佈是具有對稱的鐘形的分佈,所以中心右側是左側的映象,曲線下的面積代表概率,曲線總面積之和等於1。

高斯分佈中的大多數連續資料值傾向於圍繞均值聚集,值離均值越遠,那麼它發生的可能性就越小。接近但從未完全貼合x 軸。

樸素貝葉斯演算法的實現與推理

高斯分佈由均值和標準差兩個引數決定的,任何點 (x) 都可以通過公式 \(z = \frac{x-mean}{standard\ deviation}\) 來計算

Reference

normal distribution

通過這一點,就可以知道就可以計算出給定的概率,公式為:

\(P({x_i}|Y) = \frac{1}{\sqrt2\pi\sigma_y^2}exp(-(\frac{(x_i-\mu_y)^2}{2\sigma_y^2})\)

其中,\(\sigma\) 為標準差,\(\mu\) 為平均值,那麼轉換為可讀懂的公式為:

\(f(x) = \frac{1}{\sqrt{(2 \times pi )}\times sigma} \times exp(-(\frac{(x-mean)^2}{(2 \times sigma)^2}))\)

其中,sigmax 的標準差,meanx 的平均值,PI是 就是 \(\pi\) math.pi 的值。

那麼在轉換成python中的程式碼為:

f(x) = (1 / sqrt(2 * PI) * sigma) * exp(-((x-mean)^2 / (2 * sigma^2)))

那麼用python實現一個函式,來實現高斯公式

# 計算高斯分佈的函式,需要三個引數,x 平均值,標準差
def calculate_probability(x, mean, stdev):
	exponent = exp(-((x-mean)**2 / (2 * stdev**2 )))
	return (1 / (sqrt(2 * pi) * stdev)) * exponent

這裡通過函式測試三個資料,(0,1,1)(1,1,1)(2,1,1)

from math import sqrt
from math import pi
from math import exp
 
# 計算高斯分佈的函式,需要三個引數,x 平均值,標準差
def calculate_probability(x, mean, stdev):
	exponent = exp(-((x-mean)**2 / (2 * stdev**2 )))
	return (1 / (sqrt(2 * pi) * stdev)) * exponent
 
print(calculate_probability(1.0, 1.0, 1.0))
print(calculate_probability(2.0, 1.0, 1.0))
print(calculate_probability(0.0, 1.0, 1.0))

樸素貝葉斯演算法的實現與推理

這裡可以看到結果,(1,1,1) 的概率最可能(三個值中趨於鐘形頂部)

分類概率

到這裡,可以嘗試通過測試資料來統計新資料的概率,這裡每個類別的概率都是單獨計算的,這裡將簡化概率計算公式 \(P(class|data) = P(data|class) \times P(class)\);這是一個常見的簡化,這將意味著將結果為最大值的類的計算作為預測結果。因為通常對預測感興趣,而不是概率

對於上述例子,有兩個變數,這裡以 class=0 的類別來說明,公式為:

\(P(class=0|X1,X2) = P(X1|class=0) \times P(X2|class=0) \times P(class=0)\)

編寫一個聚合函式,將上述四個步驟彙總處理,

# Example of calculating class probabilities
from math import sqrt
from math import pi
from math import exp

# 拆分資料集
def separate_by_class(dataset):
    separated = dict()
    for i in range(len(dataset)):
        vector = dataset[i]
        class_value = vector[-1]
        if (class_value not in separated):
            separated[class_value] = list()
    
        separated[class_value].append(vector)

    print(separated)
    return separated

# 計算平均數
def mean(numbers):
	return sum(numbers)/float(len(numbers))

# 計算標準差
def stdev(numbers): 
	avg = mean(numbers)  # 計算平均值
	variance = sum([(x-avg)**2 for x in numbers]) / float(len(numbers)-1) # 標準差
	return sqrt(variance)

# 資料彙總
def summarize_dataset(dataset):
	summaries = [(mean(column), stdev(column), len(column)) for column in zip(*dataset)]
	del(summaries[-1])
	return summaries

# 按類進行資料彙總
def summarize_by_class(dataset):
	separated = separate_by_class(dataset)
	summaries = dict()
	for class_value, rows in separated.items():
		summaries[class_value] = summarize_dataset(rows)
	return summaries

# 計算高斯分佈的函式,需要三個引數,x 平均值,標準差
def calculate_probability(x, mean, stdev):
	exponent = exp(-((x-mean)**2 / (2 * stdev**2 )))
	return (1 / (sqrt(2 * pi) * stdev)) * exponent

# 計算每個分類的概率
def converge_probabilities(summaries, row):
    # 計算所有分類的個數
    total_rows = sum([summaries[label][0][2] for label in summaries])
    probabilities = dict()
    for class_value, class_summaries in summaries.items():
        # 計算分類的概率,如這個分類在總分類裡概率多少
        probabilities[class_value] = summaries[class_value][0][2]/float(total_rows)
        for i in range(len(class_summaries)):
            mean, stdev, _ = class_summaries[i]
            probabilities[class_value] *= calculate_probability(row[i], mean, stdev)
    return probabilities

# 測試資料集
dataset = [
    [3.393533211,2.331273381,0],
	[3.110073483,1.781539638,0],
	[1.343808831,3.368360954,0],
	[3.582294042,4.67917911,0],
	[2.280362439,2.866990263,0],
	[7.423436942,4.696522875,1],
	[5.745051997,3.533989803,1],
	[9.172168622,2.511101045,1],
	[7.792783481,3.424088941,1],
	[7.939820817,0.791637231,1]]
summaries = summarize_by_class(dataset)
probabilities = converge_probabilities(summaries, dataset[0])
print(probabilities)

樸素貝葉斯演算法的實現與推理

由結果可以得知,dataset[0] X1 的概率(0.0503)要大於 X2 的概率(0.0001),所以可以正確的判斷出 dataset[0] 屬於 X1 分類

鳶尾花(Iris)分類

鳶尾花分類,是模式識別中非常出名的一種資料庫,需要先將資料下載:

實現開始

實驗是根據上述實驗的步驟,將樸素貝葉斯演算法應用在鳶尾花資料集中,鳶尾花資料集的實驗也是需要相同的步驟,只不過對於資料集中的資料還需要一些其他的步驟,大致可分為以下幾種操作:

  • 資料的預處理
    • 從檔案中讀取資料
    • 將資料型別轉換為可用於上面實驗的型別(float
    • 將真實分類轉換為數字 int
  • 分類
  • 資料集彙總
  • 按類別彙總資料
  • 高斯密度函式
  • 分類概率
from csv import reader
from random import seed
from random import randrange
from math import sqrt
from math import exp
from math import pi
 
# 讀取資料集
def load_csv(filename):
	dataset = list()
	with open(filename, 'r') as file:
		csv_reader = reader(file)
		for row in csv_reader:
			if not row:
				continue
			dataset.append(row)
	return dataset
 
# 將每行的數字轉換為float
def str_column_to_float(dataset, column):
    
    for row in dataset:
        row[column] = float(row[column].strip())
 
# 將真實分類轉換為數字,按照下標
def str_column_to_int(dataset, column):
    '''
    :param dataset: list, 資料集
    :param column: string,是為型別的列要傳入
    :return: None
    '''
    # 通過迴圈拿到所有分類
    class_values = [row[column] for row in dataset]
    # 對分型別去重
    unique = set(class_values)
    
    lookup = dict()
    # 拿到分類值的key 下標
    for i, value in enumerate(unique):
        lookup[value] = i

    # 已對應的下標進行替換
    for row in dataset:
        row[column] = lookup[row[column]]
    return lookup
 
# 將資料的一部分作為訓練資料
def cross_validation_split(dataset, n_folds):
	dataset_split = list()
	dataset_copy = list(dataset)
	fold_size = int(len(dataset) / n_folds)
	for _ in range(n_folds):
		fold = list()
		while len(fold) < fold_size:
			index = randrange(len(dataset_copy))
			fold.append(dataset_copy.pop(index))
		dataset_split.append(fold)
	return dataset_split
 
# 計算準確度
def accuracy_metric(actual, predicted):
	correct = 0
	for i in range(len(actual)):
		if actual[i] == predicted[i]:
			correct += 1
	return correct / float(len(actual)) * 100.0
 
# 對演算法資料進行評估
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
    """
    :param dataset:list, 原始資料集
    :param algorithm:function,演算法函式
    :param n_folds:int,取多少資料作為訓練集
    :param args:options ,引數
    :return: None
    """
    folds = cross_validation_split(dataset, n_folds)
    scores = list()
    for fold in folds:
        train_set = list(folds)
        train_set.remove(fold)
        # 合併成一個陣列
        train_set = sum(train_set, [])
        
        test_set = list()
        for row in fold:
            row_copy = list(row)
            test_set.append(row_copy)
            row_copy[-1] = None # 將最後一個型別欄位設定為None
        predicted = algorithm(train_set, test_set, *args)
        # 真實的型別
        actual = [row[-1] for row in fold]
        # 精確的分數,即這一組資料正確率
        accuracy = accuracy_metric(actual, predicted)
        scores.append(accuracy)
    print(scores)
    return scores
 
# 按照分類拆分
def separate_by_class(dataset):
	separated = dict()
	for i in range(len(dataset)):
		vector = dataset[i]
		class_value = vector[-1]
		if (class_value not in separated):
			separated[class_value] = list()
		separated[class_value].append(vector)
	return separated
 
# 計算這一系列的平均值
def mean(numbers):
	return sum(numbers)/float(len(numbers))
 
# 計算一系列數字的標準差
def stdev(numbers):
	avg = mean(numbers)
	variance = sum([(x-avg)**2 for x in numbers]) / float(len(numbers)-1)
	return sqrt(variance)
 
# 計算資料集中每列的平均值 標準差 長度
def summarize_dataset(dataset):
	summaries = [(mean(column), stdev(column), len(column)) for column in zip(*dataset)]
	del(summaries[-1])
	return summaries
 
# 按照分類劃分資料集
def summarize_by_class(dataset):
	separated = separate_by_class(dataset)
	summaries = dict()
	for class_value, rows in separated.items():
		summaries[class_value] = summarize_dataset(rows)
	return summaries
 
# 計算x的高斯概率
def calculate_probability(x, mean, stdev):
    """
    :param x:float, 計算這個值的高斯概率
    :param mean:float,x的平均值
    :param stdev:float,x的標準差
    :return: None
    """
    exponent = exp(-((x-mean)**2 / (2 * stdev**2 )))
    return (1 / (sqrt(2 * pi) * stdev)) * exponent
 
# 計算每行的概率
def converge_probabilities(summaries, row):
    # 計算所有分類的個數
	total_rows = sum([summaries[label][0][2] for label in summaries])
	probabilities = dict()
	for class_value, class_summaries in summaries.items():
        # 計算分類的概率,如這個分類在總分類裡概率多少
        # 公式中的P(class)
		probabilities[class_value] = summaries[class_value][0][2]/float(total_rows)
        # 通過公式  P(X1|class=0) * P(X2|class=0) * P(class=0) 計算高斯概率
		for i in range(len(class_summaries)):
			mean, stdev, _ = class_summaries[i]
			probabilities[class_value] *= calculate_probability(row[i], mean, stdev)
	return probabilities
 
# 通過計算出來的值,預測該花屬於哪個品種,取高斯概率最大的值
def predict(summaries, row):
	probabilities = converge_probabilities(summaries, row)
	best_label, best_prob = None, -1
	for class_value, probability in probabilities.items():
		if best_label is None or probability > best_prob:
			best_prob = probability
			best_label = class_value
	return best_label
 
# Naive Bayes Algorithm
def naive_bayes(train, test):
    # 訓練資料按照類分類排序
    summarize = summarize_by_class(train)
    predictions = list()
    for row in test:
        output = predict(summarize, row)
        predictions.append(output)

    print(predictions)
    return(predictions)
 
# 測試
if __name__ == '__main__':
    seed(1)
    filename = 'iris.csv'
    dataset = load_csv(filename)
    # 轉換數值為float
    for i in range(len(dataset[0])-1):
        str_column_to_float(dataset, i)
    # 將型別轉換為數字
    str_column_to_int(dataset, len(dataset[0])-1)

    # 將資料分位測試資料和訓練資料,folds為多少資料為訓練資料
    n_folds = 5
    scores = evaluate_algorithm(dataset, naive_bayes, n_folds)
    print('Scores: %s' % scores)
    print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

樸素貝葉斯演算法的實現與推理

可以看到執行結果,對鳶尾花資料集的預測正確率,平均為95.333%

現在對 main 部分進行修改,使用全部資料集作為訓練,新增記錄作為預測

# 按照整個資料集分類
model = summarize_by_class(dataset)
# 新加一行預測資料
row = [5.3,3.9,3.2,2.3]
# 根據訓練集進行對資料預測
label = predict(model, row)

print('Data=%s, Predicted: %s' % (row, label))

完整修改過的程式碼如下:

from csv import reader
from random import seed
from random import randrange
from math import sqrt
from math import exp
from math import pi
 
# 讀取資料集
def load_csv(filename):
	dataset = list()
	with open(filename, 'r') as file:
		csv_reader = reader(file)
		for row in csv_reader:
			if not row:
				continue
			dataset.append(row)
	return dataset
 
# 將每行的數字轉換為float
def str_column_to_float(dataset, column):
    
    for row in dataset:
        row[column] = float(row[column].strip())
 
# 將真實分類轉換為數字,按照下標
def str_column_to_int(dataset, column):
    '''
    :param dataset: list, 資料集
    :param column: string,是為型別的列要傳入
    :return: None
    '''
    # 通過迴圈拿到所有分類
    class_values = [row[column] for row in dataset]
    # 對分型別去重
    unique = set(class_values)
    
    lookup = dict()
    # 拿到分類值的key 下標
    for i, value in enumerate(unique):
        lookup[value] = i

    # 增加一行,來顯示下標和真實名稱對應的資料
    print(lookup)

    # 已對應的下標進行替換
    for row in dataset:
        row[column] = lookup[row[column]]
    return lookup
 
# 將資料的一部分作為訓練資料
def cross_validation_split(dataset, n_folds):
	dataset_split = list()
	dataset_copy = list(dataset)
	fold_size = int(len(dataset) / n_folds)
	for _ in range(n_folds):
		fold = list()
		while len(fold) < fold_size:
			index = randrange(len(dataset_copy))
			fold.append(dataset_copy.pop(index))
		dataset_split.append(fold)
	return dataset_split
 
# 計算準確度
def accuracy_metric(actual, predicted):
	correct = 0
	for i in range(len(actual)):
		if actual[i] == predicted[i]:
			correct += 1
	return correct / float(len(actual)) * 100.0
 
# 對演算法資料進行評估
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
    """
    :param dataset:list, 原始資料集
    :param algorithm:function,演算法函式
    :param n_folds:int,取多少資料作為訓練集
    :param args:options ,引數
    :return: None
    """
    folds = cross_validation_split(dataset, n_folds)
    scores = list()
    for fold in folds:
        train_set = list(folds)
        train_set.remove(fold)
        # 合併成一個陣列
        train_set = sum(train_set, [])
        
        test_set = list()
        for row in fold:
            row_copy = list(row)
            test_set.append(row_copy)
            row_copy[-1] = None # 將最後一個型別欄位設定為None
        predicted = algorithm(train_set, test_set, *args)
        # 真實的型別
        actual = [row[-1] for row in fold]
        # 精確的分數,即這一組資料正確率
        accuracy = accuracy_metric(actual, predicted)
        scores.append(accuracy)
    print(scores)
    return scores
 
# 按照分類拆分
def separate_by_class(dataset):
    """
    :param dataset:list, 按分類好的列表
    :return: dict, 每個分類的每列(屬性)的平均值,標準差,個數
    """
    separated = dict()
    for i in range(len(dataset)):
        vector = dataset[i]
        class_value = vector[-1]
        if (class_value not in separated):
            separated[class_value] = list()
        separated[class_value].append(vector)
    return separated
 
# 計算這一系列的平均值
def mean(numbers):
	return sum(numbers)/float(len(numbers))
 
# 計算一系列數字的標準差
def stdev(numbers):
	avg = mean(numbers)
	variance = sum([(x-avg)**2 for x in numbers]) / float(len(numbers)-1)
	return sqrt(variance)
 
# 計算資料集中每列的平均值 標準差 長度
def summarize_dataset(dataset):
	summaries = [(mean(column), stdev(column), len(column)) for column in zip(*dataset)]
	del(summaries[-1])
	return summaries
 
# 按照分類劃分資料集
def summarize_by_class(dataset):
    separated = separate_by_class(dataset)
    summaries = dict()
    for class_value, rows in separated.items():
        summaries[class_value] = summarize_dataset(rows)

    return summaries
 
# 計算x的高斯概率
def calculate_probability(x, mean, stdev):
    """
    :param x:float, 計算這個值的高斯概率
    :param mean:float,x的平均值
    :param stdev:float,x的標準差
    :return: None
    """
    exponent = exp(-((x-mean)**2 / (2 * stdev**2 )))
    return (1 / (sqrt(2 * pi) * stdev)) * exponent
 
# 計算每行的概率
def converge_probabilities(summaries, row):
    # 計算所有分類的個數
    total_rows = sum([summaries[label][0][2] for label in summaries])
    probabilities = dict()
    for class_value, class_summaries in summaries.items():
        # 計算分類的概率,如這個分類在總分類裡概率多少
        # 公式中的P(class)
        probabilities[class_value] = summaries[class_value][0][2]/float(total_rows)
        # 通過公式  P(X1|class=0) * P(X2|class=0) * P(class=0) 計算高斯概率
        for i in range(len(class_summaries)):
            mean, stdev, _ = class_summaries[i]
            probabilities[class_value] *= calculate_probability(row[i], mean, stdev)
      
    return probabilities
 
# 通過計算出來的值,預測該花屬於哪個品種,取高斯概率最大的值
def predict(summaries, row):
	probabilities = converge_probabilities(summaries, row)
	best_label, best_prob = None, -1
	for class_value, probability in probabilities.items():
		if best_label is None or probability > best_prob:
			best_prob = probability
			best_label = class_value
	return best_label
 
# Naive Bayes Algorithm
def naive_bayes(train, test):
    # 訓練資料按照類分類排序
    summarize = summarize_by_class(train)
    predictions = list()
    for row in test:
        output = predict(summarize, row)
        predictions.append(output)

    print(predictions)
    return(predictions)
 
# 測試
if __name__ == '__main__':
    seed(1)
    filename = 'iris.csv'
    dataset = load_csv(filename)
    # 轉換數值為float
    for i in range(len(dataset[0])-1):
        str_column_to_float(dataset, i)
    # 將型別轉換為數字
    str_column_to_int(dataset, len(dataset[0])-1)

    # 按照整個資料集分類
    model = summarize_by_class(dataset)
    # 新加一行預測資料
    row = [5.3,3.9,3.2,2.3]
    # 根據訓練集進行對資料預測
    label = predict(model, row)
    print('Data=%s, Predicted: %s' % (row, label))

樸素貝葉斯演算法的實現與推理

可以看到對資料集 [5.3,3.9,3.2,2.3] 預測為 versicolor,那將屬性修改為,[2.3,0.9,0.2,1.3] 預測結果為 setosa

樸素貝葉斯演算法的實現與推理

還有一些小問題沒有搞明白,為什麼實現時需要省略掉除法

Reference

gaussian naive bayes

Naive Bayes Example

caculator naive bayes

五分鐘瞭解樸素貝葉斯

Joint Probability

Conditional Probability