機器學習之用Python從零實現貝葉斯分類器
樸素貝葉斯演算法簡單高效,在處理分類問題上,是應該首先考慮的方法之一。
通過本教程,你將學到樸素貝葉斯演算法的原理和Python版本的逐步實現。
更新:檢視後續的關於樸素貝葉斯使用技巧的文章“Better Naive Bayes: 12 Tips To Get The Most From The Naive Bayes Algorithm”
樸素貝葉斯分類器,Matt Buck保留部分版權
關於樸素貝葉斯
樸素貝葉斯演算法是一個直觀的方法,使用每個屬性歸屬於某個類的概率來做預測。你可以使用這種監督性學習方法,對一個預測性建模問題進行概率建模。
給定一個類,樸素貝葉斯假設每個屬性歸屬於此類的概率獨立於其餘所有屬性,從而簡化了概率的計算。這種強假定產生了一個快速、有效的方法。
給定一個屬性值,其屬於某個類的概率叫做條件概率。對於一個給定的類值,將每個屬性的條件概率相乘,便得到一個資料樣本屬於某個類的概率。
我們可以通過計算樣本歸屬於每個類的概率,然後選擇具有最高概率的類來做預測。
通常,我們使用分類資料來描述樸素貝葉斯,因為這樣容易通過比率來描述、計算。一個符合我們目的、比較有用的演算法需要支援數值屬性,同時假設每一個數值屬性服從正態分佈(分佈在一個鐘形曲線上),這又是一個強假設,但是依然能夠給出一個健壯的結果。
預測糖尿病的發生
本文使用的測試問題是“皮馬印第安人糖尿病問題”。
這個問題包括768個對於皮馬印第安患者的醫療觀測細節,記錄所描述的瞬時測量取自諸如患者的年紀,懷孕和血液檢查的次數。所有患者都是21歲以上(含21歲)的女性,所有屬性都是數值型,而且屬性的單位各不相同。
每一個記錄歸屬於一個類,這個類指明以測量時間為止,患者是否是在5年之內感染的糖尿病。如果是,則為1,否則為0。
機器學習文獻中已經多次研究了這個標準資料集,好的預測精度為70%-76%。
下面是pima-indians.data.csv檔案中的一個樣本,瞭解一下我們將要使用的資料。
注意:下載檔案,然後以.csv副檔名儲存(如:pima-indians-diabetes.data.csv)。檢視檔案中所有屬性的描述。
1 2 3 4 5 |
6,148,72,35,0,33.6,0.627,50,1 1,85,66,29,0,26.6,0.351,31,0 8,183,64,0,0,23.3,0.672,32,1 1,89,66,23,94,28.1,0.167,21,0 0,137,40,35,168,43.1,2.288,33,1 |
樸素貝葉斯演算法教程
教程分為如下幾步:
1.處理資料:從CSV檔案中載入資料,然後劃分為訓練集和測試集。
2.提取資料特徵:提取訓練資料集的屬性特徵,以便我們計算概率並做出預測。
3.單一預測:使用資料集的特徵生成單個預測。
4.多重預測:基於給定測試資料集和一個已提取特徵的訓練資料集生成預測。
5.評估精度:評估對於測試資料集的預測精度作為預測正確率。
6.合併程式碼:使用所有程式碼呈現一個完整的、獨立的樸素貝葉斯演算法的實現。
1.處理資料
首先載入資料檔案。CSV格式的資料沒有標題行和任何引號。我們可以使用csv模組中的open函式開啟檔案,使用reader函式讀取行資料。
我們也需要將以字串型別載入進來屬性轉換為我們可以使用的數字。下面是用來載入匹馬印第安人資料集(Pima indians dataset)的loadCsv()函式。
1 2 3 4 5 6 7 |
import csv def loadCsv(filename): lines = csv.reader(open(filename, "rb")) dataset = list(lines) for i in range(len(dataset)): dataset[i] = [float(x) for x in dataset[i]] return dataset |
我們可以通過載入皮馬印第安人資料集,然後列印出資料樣本的個數,以此測試這個函式。
1 2 3 |
filename = 'pima-indians-diabetes.data.csv' dataset = loadCsv(filename) print('Loaded data file {0} with {1} rows').format(filename, len(dataset)) |
執行測試,你會看到如下結果:
1 |
Loaded data file iris.data.csv with 150 rows |
下一步,我們將資料分為用於樸素貝葉斯預測的訓練資料集,以及用來評估模型精度的測試資料集。我們需要將資料集隨機分為包含67%的訓練集合和包含33%的測試集(這是在此資料集上測試演算法的通常比率)。
下面是splitDataset()函式,它以給定的劃分比例將資料集進行劃分。
1 2 3 4 5 6 7 8 9 |
import random def splitDataset(dataset, splitRatio): trainSize = int(len(dataset) * splitRatio) trainSet = [] copy = list(dataset) while len(trainSet) < trainSize: index = random.randrange(len(copy)) trainSet.append(copy.pop(index)) return [trainSet, copy] |
我們可以定義一個具有5個樣例的資料集來進行測試,首先它分為訓練資料集和測試資料集,然後列印出來,看看每個資料樣本最終落在哪個資料集。
1 2 3 4 |
dataset = [[1], [2], [3], [4], [5]] splitRatio = 0.67 train, test = splitDataset(dataset, splitRatio) print('Split {0} rows into train with {1} and test with {2}').format(len(dataset), train, test) |
執行測試,你會看到如下結果:
1 |
Split 5 rows into train with [[4], [3], [5]] and test with [[1], [2]] |
提取資料特徵
樸素貝葉斯模型包含訓練資料集中資料的特徵,然後使用這個資料特徵來做預測。
所收集的訓練資料的特徵,包含相對於每個類的每個屬性的均值和標準差。舉例來說,如果如果有2個類和7個數值屬性,然後我們需要每一個屬性(7)和類(2)的組合的均值和標準差,也就是14個屬性特徵。
在對特定的屬性歸屬於每個類的概率做計算、預測時,將用到這些特徵。
我們將資料特徵的獲取劃分為以下的子任務:
- 按類別劃分資料
- 計算均值
- 計算標準差
- 提取資料集特徵
- 按類別提取屬性特徵
按類別劃分資料
首先將訓練資料集中的樣本按照類別進行劃分,然後計算出每個類的統計資料。我們可以建立一個類別到屬於此類別的樣本列表的的對映,並將整個資料集中的樣本分類到相應的列表。
下面的SeparateByClass()函式可以完成這個任務:
1 2 3 4 5 6 7 8 |
def separateByClass(dataset): separated = {} for i in range(len(dataset)): vector = dataset[i] if (vector[-1] not in separated): separated[vector[-1]] = [] separated[vector[-1]].append(vector) return separated |
可以看出,函式假設樣本中最後一個屬性(-1)為類別值,返回一個類別值到資料樣本列表的對映。
我們可以用一些樣本資料測試如下:
1 2 3 |
dataset = [[1,20,1], [2,21,0], [3,22,1]] separated = separateByClass(dataset) print('Separated instances: {0}').format(separated) |
執行測試,你會看到如下結果:
1 |
Separated instances: {0: [[2, 21, 0]], 1: [[1, 20, 1], [3, 22, 1]]} |
計算均值
我們需要計算在每個類中每個屬性的均值。均值是資料的中點或者集中趨勢,在計算概率時,我們用它作為高斯分佈的中值。
我們也需要計算每個類中每個屬性的標準差。標準差描述了資料散佈的偏差,在計算概率時,我們用它來刻畫高斯分佈中,每個屬性所期望的散佈。
標準差是方差的平方根。方差是每個屬性值與均值的離差平方的平均數。注意我們使用N-1的方法(譯者注:參見無偏估計),也就是在在計算方差時,屬性值的個數減1。
1 2 3 4 5 6 7 8 |
import math def mean(numbers): return sum(numbers)/float(len(numbers)) def stdev(numbers): avg = mean(numbers) variance = sum([pow(x-avg,2) for x in numbers])/float(len(numbers)-1) return math.sqrt(variance) |
通過計算從1到5這5個數的均值來測試函式。
1 2 |
numbers = [1,2,3,4,5] print('Summary of {0}: mean={1}, stdev={2}').format(numbers, mean(numbers), stdev(numbers)) |
執行測試,你會看到如下結果:
1 |
Summary of [1, 2, 3, 4, 5]: mean=3.0, stdev=1.58113883008 |
提取資料集的特徵
現在我們可以提取資料集特徵。對於一個給定的樣本列表(對應於某個類),我們可以計算每個屬性的均值和標準差。
zip函式將資料樣本按照屬性分組為一個個列表,然後可以對每個屬性計算均值和標準差。
1 2 3 4 |
def summarize(dataset): summaries = [(mean(attribute), stdev(attribute)) for attribute in zip(*dataset)] del summaries[-1] return summaries |
我們可以使用一些測試資料來測試這個summarize()函式,測試資料對於第一個和第二個資料屬性的均值和標準差顯示出顯著的不同。
1 2 3 |
dataset = [[1,20,0], [2,21,1], [3,22,0]] summary = summarize(dataset) print('Attribute summaries: {0}').format(summary) |
執行測試,你會看到如下結果:
1 |
Attribute summaries: [(2.0, 1.0), (21.0, 1.0)] |
按類別提取屬性特徵
合併程式碼,我們首先將訓練資料集按照類別進行劃分,然後計算每個屬性的摘要。
1 2 3 4 5 6 |
def summarizeByClass(dataset): separated = separateByClass(dataset) summaries = {} for classValue, instances in separated.iteritems(): summaries[classValue] = summarize(instances) return summaries |
使用小的測試資料集來測試summarizeByClass()函式。
1 2 3 |
dataset = [[1,20,1], [2,21,0], [3,22,1], [4,22,0]] summary = summarizeByClass(dataset) print('Summary by class value: {0}').format(summary) |
執行測試,你會看到如下結果:
1 2 3 |
Summary by class value: {0: [(3.0, 1.4142135623730951), (21.5, 0.7071067811865476)], 1: [(2.0, 1.4142135623730951), (21.0, 1.4142135623730951)]} |
預測
我們現在可以使用從訓練資料中得到的摘要來做預測。做預測涉及到對於給定的資料樣本,計算其歸屬於每個類的概率,然後選擇具有最大概率的類作為預測結果。
我們可以將這部分劃分成以下任務:
- 計算高斯概率密度函式
- 計算對應類的概率
- 單一預測
- 評估精度
計算高斯概率密度函式
給定來自訓練資料中已知屬性的均值和標準差,我們可以使用高斯函式來評估一個給定的屬性值的概率。
已知每個屬性和類值的屬性特徵,在給定類值的條件下,可以得到給定屬性值的條件概率。
關於高斯概率密度函式,可以檢視參考文獻。總之,我們要把已知的細節融入到高斯函式(屬性值,均值,標準差),並得到屬性值歸屬於某個類的似然(譯者注:即可能性)。
在calculateProbability()函式中,我們首先計算指數部分,然後計算等式的主幹。這樣可以將其很好地組織成2行。
1 2 3 4 |
import math def calculateProbability(x, mean, stdev): exponent = math.exp(-(math.pow(x-mean,2)/(2*math.pow(stdev,2)))) return (1 / (math.sqrt(2*math.pi) * stdev)) * exponent |
使用一些簡單的資料測試如下:
1 2 3 4 5 |
x = 71.5 mean = 73 stdev = 6.2 probability = calculateProbability(x, mean, stdev) print('Probability of belonging to this class: {0}').format(probability) |
執行測試,你會看到如下結果:
1 |
Probability of belonging to this class: 0.0624896575937 |
計算所屬類的概率
既然我們可以計算一個屬性屬於某個類的概率,那麼合併一個資料樣本中所有屬性的概率,最後便得到整個資料樣本屬於某個類的概率。
使用乘法合併概率,在下面的calculClassProbilities()函式中,給定一個資料樣本,它所屬每個類別的概率,可以通過將其屬性概率相乘得到。結果是一個類值到概率的對映。
1 2 3 4 5 6 7 8 9 |
def calculateClassProbabilities(summaries, inputVector): probabilities = {} for classValue, classSummaries in summaries.iteritems(): probabilities[classValue] = 1 for i in range(len(classSummaries)): mean, stdev = classSummaries[i] x = inputVector[i] probabilities[classValue] *= calculateProbability(x, mean, stdev) return probabilities |
測試calculateClassProbabilities()函式。
1 2 3 4 |
summaries = {0:[(1, 0.5)], 1:[(20, 5.0)]} inputVector = [1.1, '?'] probabilities = calculateClassProbabilities(summaries, inputVector) print('Probabilities for each class: {0}').format(probabilities) |
執行測試,你會看到如下結果:
1 |
Probabilities for each class: {0: 0.7820853879509118, 1: 6.298736258150442e-05} |
單一預測
既然可以計算一個資料樣本屬於每個類的概率,那麼我們可以找到最大的概率值,並返回關聯的類。
下面的predict()函式可以完成以上任務。
1 2 3 4 5 6 7 8 |
def predict(summaries, inputVector): probabilities = calculateClassProbabilities(summaries, inputVector) bestLabel, bestProb = None, -1 for classValue, probability in probabilities.iteritems(): if bestLabel is None or probability > bestProb: bestProb = probability bestLabel = classValue return bestLabel |
測試predict()函式如下:
1 2 3 4 |
summaries = {'A':[(1, 0.5)], 'B':[(20, 5.0)]} inputVector = [1.1, '?'] result = predict(summaries, inputVector) print('Prediction: {0}').format(result) |
執行測試,你會得到如下結果:
1 |
Prediction: A |
多重預測
最後,通過對測試資料集中每個資料樣本的預測,我們可以評估模型精度。getPredictions()函式可以實現這個功能,並返回每個測試樣本的預測列表。
1 2 3 4 5 6 |
def getPredictions(summaries, testSet): predictions = [] for i in range(len(testSet)): result = predict(summaries, testSet[i]) predictions.append(result) return predictions |
測試getPredictions()函式如下。
1 2 3 4 |
summaries = {'A':[(1, 0.5)], 'B':[(20, 5.0)]} testSet = [[1.1, '?'], [19.1, '?']] predictions = getPredictions(summaries, testSet) print('Predictions: {0}').format(predictions) |
執行測試,你會看到如下結果:
1 |
Predictions: ['A', 'B'] |
計算精度
預測值和測試資料集中的類別值進行比較,可以計算得到一個介於0%~100%精確率作為分類的精確度。getAccuracy()函式可以計算出這個精確率。
1 2 3 4 5 6 |
def getAccuracy(testSet, predictions): correct = 0 for x in range(len(testSet)): if testSet[x][-1] == predictions[x]: correct += 1 return (correct/float(len(testSet))) * 100.0 |
我們可以使用如下簡單的程式碼來測試getAccuracy()函式。
1 2 3 4 |
testSet = [[1,1,1,'a'], [2,2,2,'a'], [3,3,3,'b']] predictions = ['a', 'a', 'a'] accuracy = getAccuracy(testSet, predictions) print('Accuracy: {0}').format(accuracy) |
執行測試,你會得到如下結果:
1 |
Accuracy: 66.6666666667 |
合併程式碼
最後,我們需要將程式碼連貫起來。
下面是樸素貝葉斯Python版的逐步實現的全部程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# Example of Naive Bayes implemented from Scratch in Python import csv import random import math def loadCsv(filename): lines = csv.reader(open(filename, "rb")) dataset = list(lines) for i in range(len(dataset)): dataset[i] = [float(x) for x in dataset[i]] return dataset def splitDataset(dataset, splitRatio): trainSize = int(len(dataset) * splitRatio) trainSet = [] copy = list(dataset) while len(trainSet) < trainSize: index = random.randrange(len(copy)) trainSet.append(copy.pop(index)) return [trainSet, copy] def separateByClass(dataset): separated = {} for i in range(len(dataset)): vector = dataset[i] if (vector[-1] not in separated): separated[vector[-1]] = [] separated[vector[-1]].append(vector) return separated def mean(numbers): return sum(numbers)/float(len(numbers)) def stdev(numbers): avg = mean(numbers) variance = sum([pow(x-avg,2) for x in numbers])/float(len(numbers)-1) return math.sqrt(variance) def summarize(dataset): summaries = [(mean(attribute), stdev(attribute)) for attribute in zip(*dataset)] del summaries[-1] return summaries def summarizeByClass(dataset): separated = separateByClass(dataset) summaries = {} for classValue, instances in separated.iteritems(): summaries[classValue] = summarize(instances) return summaries def calculateProbability(x, mean, stdev): exponent = math.exp(-(math.pow(x-mean,2)/(2*math.pow(stdev,2)))) return (1 / (math.sqrt(2*math.pi) * stdev)) * exponent def calculateClassProbabilities(summaries, inputVector): probabilities = {} for classValue, classSummaries in summaries.iteritems(): probabilities[classValue] = 1 for i in range(len(classSummaries)): mean, stdev = classSummaries[i] x = inputVector[i] probabilities[classValue] *= calculateProbability(x, mean, stdev) return probabilities def predict(summaries, inputVector): probabilities = calculateClassProbabilities(summaries, inputVector) bestLabel, bestProb = None, -1 for classValue, probability in probabilities.iteritems(): if bestLabel is None or probability > bestProb: bestProb = probability bestLabel = classValue return bestLabel def getPredictions(summaries, testSet): predictions = [] for i in range(len(testSet)): result = predict(summaries, testSet[i]) predictions.append(result) return predictions def getAccuracy(testSet, predictions): correct = 0 for i in range(len(testSet)): if testSet[i][-1] == predictions[i]: correct += 1 return (correct/float(len(testSet))) * 100.0 def main(): filename = 'pima-indians-diabetes.data.csv' splitRatio = 0.67 dataset = loadCsv(filename) trainingSet, testSet = splitDataset(dataset, splitRatio) print('Split {0} rows into train={1} and test={2} rows').format(len(dataset), len(trainingSet), len(testSet)) # prepare model summaries = summarizeByClass(trainingSet) # test model predictions = getPredictions(summaries, testSet) accuracy = getAccuracy(testSet, predictions) print('Accuracy: {0}%').format(accuracy) main() |
執行示例,得到如下輸出:
1 2 |
Split 768 rows into train=514 and test=254 rows Accuracy: 76.3779527559% |
實現擴充套件
這一部分為你提供了擴充套件思路,你可以將其作為教程的一部分,使用你已經實現的Python程式碼,進行應用研究。
到此,你已經使用Python一步步完成了高斯版本的樸素貝葉斯。
你可以進一步擴充套件演算法實現:
計算所屬類的概率:將一個資料樣本歸屬於每個類的概率更新為一個比率。計算上就是將一個樣本資料歸屬於某個類的概率,比上其歸屬於每一個類的概率的和。舉例來說,一個樣本屬於類A的概率時0.02,屬於類B的概率時0.001,那麼樣本屬於類A的可能性是(0.02/(0.02+0.001))*100 大約為95.23%。
對數概率:對於一個給定的屬性值,每個類的條件概率很小。當將其相乘時結果會更小,那麼存在浮點溢位的可能(數值太小,以至於在Python中不能表示)。一個常用的修復方案是,合併其概率的對數值。可以研究實現下這個改進。
名詞屬性:改進演算法實現,使其支援名詞屬性。這是十分相似的,你所收集的每個屬性的摘要資訊是對於每個類的類別值的比率。潛心學習參考文獻來獲取更多資訊。
不同的密度函式(伯努利或者多項式):我們已經嘗試了高斯樸素貝葉斯,你也可以嘗試下其他分佈。實現一個不同的分佈諸如多項分佈、伯努利分佈或者核心樸素貝葉斯,他們對於屬性值的分佈 和/或 與類值之間的關係有不同的假設。
學習資源及深入閱讀
這一部分提供了一些用於學習更多樸素貝葉斯演算法的資源,包括演算法理論和工作原理,以及程式碼實現中的實際問題。
問題
更多學習預測糖尿病發作問題的資源
- Pima Indians Diabetes Data Set:這個頁面提供資料集檔案,同時描述了各個屬性,也列出了使用該資料集的論文列表
- Dataset File:資料集檔案
- Dataset Summary:資料集屬性的描述
- Diabetes Dataset Results:許多標準演算法在該資料集上的精度
程式碼
這一部分包含流行的機器學習庫中的樸素貝葉斯的開源實現。如果你在考慮實現自己的用於實際使用的版本,可以查閱這些
- Naive Bayes in Scikit-Learn:scikit-learn庫中樸素貝葉斯的實現
- Naive Bayes documentation:scikit-learn庫中關於樸素貝葉斯的文件和樣例程式碼
- Simple Naive Bayes in Weka:樸素貝葉斯的Weka實現
書籍
你應該有幾本機器學習應用的書籍。這一部分高亮出了常用機器學習書籍中關於樸素貝葉斯的章節。
- Applied Predictive Modeling, page 353
- Data Mining: Practical Machine Learning Tools and Techniques, page 94
- Machine Learning for Hackers, page 78
- An Introduction to Statistical Learning: with Applications in R, page 138
- Machine Learning: An Algorithmic Perspective, page 171
- Machine Learning in Action, page 61 (Chapter 4)
- Machine Learning, page 177 (chapter 6)
下一步
行動起來。
跟著教程,從頭開始實現樸素貝葉斯。將這個例子適用到其他問題。按照擴充套件改進實現。
評論分享你的經驗。
更新:檢視後續的關於樸素貝葉斯使用技巧的文章“Better Naive Bayes: 12 Tips To Get The Most From The Naive Bayes Algorithm”