第4章 基於概率論的分類方法:朴素貝葉斯
朴素貝葉斯 概述
貝葉斯分類是一類分類算法的總稱,這類算法均以貝葉斯定理為基礎,故統稱為貝葉斯分類。本章首先介紹貝葉斯分類算法的基礎——貝葉斯定理。最后,我們通過實例來討論貝葉斯分類的中最簡單的一種: 朴素貝葉斯分類。
貝葉斯理論 & 條件概率
貝葉斯理論
我們現在有一個數據集,它由兩類數據組成,數據分布如下圖所示:
我們現在用 p1(x,y) 表示數據點 (x,y) 屬於類別 1(圖中用圓點表示的類別)的概率,用 p2(x,y) 表示數據點 (x,y) 屬於類別 2(圖中三角形表示的類別)的概率,那么對於一個新數據點 (x,y),可以用下面的規則來判斷它的類別:
- 如果 p1(x,y) > p2(x,y) ,那么類別為1
- 如果 p2(x,y) > p1(x,y) ,那么類別為2
也就是說,我們會選擇高概率對應的類別。這就是貝葉斯決策理論的核心思想,即選擇具有最高概率的決策。
條件概率
如果你對 p(x,y|c1) 符號很熟悉,那么可以跳過本小節。
有一個裝了 7 塊石頭的罐子,其中 3 塊是白色的,4 塊是黑色的。如果從罐子中隨機取出一塊石頭,那么是白色石頭的可能性是多少?由於取石頭有 7 種可能,其中 3 種為白色,所以取出白色石頭的概率為 3/7 。那么取到黑色石頭的概率又是多少呢?很顯然,是 4/7 。我們使用 P(white) 來表示取到白色石頭的概率,其概率值可以通過白色石頭數目除以總的石頭數目來得到。
如果這 7 塊石頭如下圖所示,放在兩個桶中,那么上述概率應該如何計算?
計算 P(white) 或者 P(black) ,如果事先我們知道石頭所在桶的信息是會改變結果的。這就是所謂的條件概率(conditional probablity)。假定計算的是從 B 桶取到白色石頭的概率,這個概率可以記作 P(white|bucketB) ,我們稱之為“在已知石頭出自 B 桶的條件下,取出白色石頭的概率”。很容易得到,P(white|bucketA) 值為 2/4 ,P(white|bucketB) 的值為 1/3 。
條件概率的計算公式如下:
P(white|bucketB) = P(white and bucketB) / P(bucketB)
首先,我們用 B 桶中白色石頭的個數除以兩個桶中總的石頭數,得到 P(white and bucketB) = 1/7 .其次,由於 B 桶中有 3 塊石頭,而總石頭數為 7 ,於是 P(bucketB) 就等於 3/7 。於是又 P(white|bucketB) = P(white and bucketB) / P(bucketB) = (1/7) / (3/7) = 1/3 。
另外一種有效計算條件概率的方法稱為貝葉斯准則。貝葉斯准則告訴我們如何交換條件概率中的條件與結果,即如果已知 P(x|c),要求 P(c|x),那么可以使用下面的計算方法:
使用條件概率來分類
上面我們提到貝葉斯決策理論要求計算兩個概率 p1(x, y) 和 p2(x, y):
- 如果 p1(x, y) > p2(x, y), 那么屬於類別 1;
- 如果 p2(x, y) > p1(X, y), 那么屬於類別 2.
這並不是貝葉斯決策理論的所有內容。使用 p1() 和 p2() 只是為了盡可能簡化描述,而真正需要計算和比較的是 p(c1|x, y) 和 p(c2|x, y) .這些符號所代表的具體意義是: 給定某個由 x、y 表示的數據點,那么該數據點來自類別 c1 的概率是多少?數據點來自類別 c2 的概率又是多少?注意這些概率與概率 p(x, y|c1) 並不一樣,不過可以使用貝葉斯准則來交換概率中條件與結果。具體地,應用貝葉斯准則得到:
使用上面這些定義,可以定義貝葉斯分類准則為:
- 如果 P(c1|x, y) > P(c2|x, y), 那么屬於類別 c1;
- 如果 P(c2|x, y) > P(c1|x, y), 那么屬於類別 c2.
在文檔分類中,整個文檔(如一封電子郵件)是實例,而電子郵件中的某些元素則構成特征。我們可以觀察文檔中出現的詞,並把每個詞作為一個特征,而每個詞的出現或者不出現作為該特征的值,這樣得到的特征數目就會跟詞匯表中的詞的數目一樣多。
我們假設特征之間 相互獨立 。所謂 獨立(independence) 指的是統計意義上的獨立,即一個特征或者單詞出現的可能性與它和其他單詞相鄰沒有關系,比如說,“我們”中的“我”和“們”出現的概率與這兩個字相鄰沒有任何關系。這個假設正是朴素貝葉斯分類器中 朴素(naive) 一詞的含義。朴素貝葉斯分類器中的另一個假設是,每個特征同等重要。
Note: 朴素貝葉斯分類器通常有兩種實現方式: 一種基於伯努利模型實現,一種基於多項式模型實現。這里采用前一種實現方式。該實現方式中並不考慮詞在文檔中出現的次數,只考慮出不出現,因此在這個意義上相當於假設詞是等權重的。
朴素貝葉斯 場景
機器學習的一個重要應用就是文檔的自動分類。
在文檔分類中,整個文檔(如一封電子郵件)是實例,而電子郵件中的某些元素則構成特征。我們可以觀察文檔中出現的詞,並把每個詞作為一個特征,而每個詞的出現或者不出現作為該特征的值,這樣得到的特征數目就會跟詞匯表中的詞的數目一樣多。
朴素貝葉斯是上面介紹的貝葉斯分類器的一個擴展,是用於文檔分類的常用算法。下面我們會進行一些朴素貝葉斯分類的實踐項目。
朴素貝葉斯 原理
朴素貝葉斯 工作原理
提取所有文檔中的詞條並進行去重
獲取文檔的所有類別
計算每個類別中的文檔數目
對每篇訓練文檔:
對每個類別:
如果詞條出現在文檔中-->增加該詞條的計數值(for循環或者矩陣相加)
增加所有詞條的計數值(此類別下詞條總數)
對每個類別:
對每個詞條:
將該詞條的數目除以總詞條數目得到的條件概率(P(詞條|類別))
返回該文檔屬於每個類別的條件概率(P(類別|文檔的所有詞條))
朴素貝葉斯 開發流程
收集數據: 可以使用任何方法。
准備數據: 需要數值型或者布爾型數據。
分析數據: 有大量特征時,繪制特征作用不大,此時使用直方圖效果更好。
訓練算法: 計算不同的獨立特征的條件概率。
測試算法: 計算錯誤率。
使用算法: 一個常見的朴素貝葉斯應用是文檔分類。可以在任意的分類場景中使用朴素貝葉斯分類器,不一定非要是文本。
朴素貝葉斯 算法特點
優點: 在數據較少的情況下仍然有效,可以處理多類別問題。
缺點: 對於輸入數據的准備方式較為敏感。
適用數據類型: 標稱型數據。
朴素貝葉斯 項目案例
項目案例1: 屏蔽社區留言板的侮辱性言論
項目概述
構建一個快速過濾器來屏蔽在線社區留言板上的侮辱性言論。如果某條留言使用了負面或者侮辱性的語言,那么就將該留言標識為內容不當。對此問題建立兩個類別: 侮辱類和非侮辱類,使用 1 和 0 分別表示。
開發流程
收集數據: 可以使用任何方法
准備數據: 從文本中構建詞向量
分析數據: 檢查詞條確保解析的正確性
訓練算法: 從詞向量計算概率
測試算法: 根據現實情況修改分類器
使用算法: 對社區留言板言論進行分類
收集數據: 可以使用任何方法
本例是我們自己構造的詞表:
def loadDataSet(): """ 創建數據集 :return: 單詞列表postingList, 所屬類別classVec """ postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'], #[0,0,1,1,1......] ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'], ['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'], ['stop', 'posting', 'stupid', 'worthless', 'garbage'], ['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'], ['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']] classVec = [0, 1, 0, 1, 0, 1] # 1 is abusive, 0 not return postingList, classVec
准備數據: 從文本中構建詞向量
def createVocabList(dataSet): """ 獲取所有單詞的集合 :param dataSet: 數據集 :return: 所有單詞的集合(即不含重復元素的單詞列表) """ vocabSet = set([]) # create empty set for document in dataSet: # 操作符 | 用於求兩個集合的並集 vocabSet = vocabSet | set(document) # union of the two sets return list(vocabSet) def setOfWords2Vec(vocabList, inputSet): """ 遍歷查看該單詞是否出現,出現該單詞則將該單詞置1 :param vocabList: 所有單詞集合列表 :param inputSet: 輸入數據集 :return: 匹配列表[0,1,0,1...],其中 1與0 表示詞匯表中的單詞是否出現在輸入的數據集中 """ # 創建一個和詞匯表等長的向量,並將其元素都設置為0 returnVec = [0] * len(vocabList)# [0,0......] # 遍歷文檔中的所有單詞,如果出現了詞匯表中的單詞,則將輸出的文檔向量中的對應值設為1 for word in inputSet: if word in vocabList: returnVec[vocabList.index(word)] = 1 else: print "the word: %s is not in my Vocabulary!" % word return returnVec
分析數據: 檢查詞條確保解析的正確性
檢查函數執行情況,檢查詞表,不出現重復單詞,需要的話,可以對其進行排序。
>>> listOPosts, listClasses = bayes.loadDataSet() >>> myVocabList = bayes.createVocabList(listOPosts) >>> myVocabList ['cute', 'love', 'help', 'garbage', 'quit', 'I', 'problems', 'is', 'park', 'stop', 'flea', 'dalmation', 'licks', 'food', 'not', 'him', 'buying', 'posting', 'has', 'worthless', 'ate', 'to', 'maybe', 'please', 'dog', 'how', 'stupid', 'so', 'take', 'mr', 'steak', 'my']
檢查函數有效性。例如:myVocabList 中索引為 2 的元素是什么單詞?應該是是 help 。該單詞在第一篇文檔中出現了,現在檢查一下看看它是否出現在第四篇文檔中。
>>> bayes.setOfWords2Vec(myVocabList, listOPosts[0]) [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1] >>> bayes.setOfWords2Vec(myVocabList, listOPosts[3]) [0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
訓練算法: 從詞向量計算概率
現在已經知道了一個詞是否出現在一篇文檔中,也知道該文檔所屬的類別。接下來我們重寫貝葉斯准則,將之前的 x, y 替換為 w. 粗體的 w 表示這是一個向量,即它由多個值組成。在這個例子中,數值個數與詞匯表中的詞個數相同。
我們使用上述公式,對每個類計算該值,然后比較這兩個概率值的大小。
首先可以通過類別 i (侮辱性留言或者非侮辱性留言)中的文檔數除以總的文檔數來計算概率 p(ci) 。接下來計算 p(w | ci) ,這里就要用到朴素貝葉斯假設。如果將 w 展開為一個個獨立特征,那么就可以將上述概率寫作 p(w0, w1, w2...wn | ci) 。這里假設所有詞都互相獨立,該假設也稱作條件獨立性假設(例如 A 和 B 兩個人拋骰子,概率是互不影響的,也就是相互獨立的,A 拋 2點的同時 B 拋 3 點的概率就是 1/6 * 1/6),它意味着可以使用 p(w0 | ci)p(w1 | ci)p(w2 | ci)...p(wn | ci) 來計算上述概率,這樣就極大地簡化了計算的過程。
朴素貝葉斯分類器訓練函數
def _trainNB0(trainMatrix, trainCategory): """ 訓練數據原版 :param trainMatrix: 文件單詞矩陣 [[1,0,1,1,1....],[],[]...] :param trainCategory: 文件對應的類別[0,1,1,0....],列表長度等於單詞矩陣數,其中的1代表對應的文件是侮辱性文件,0代表不是侮辱性矩陣 :return: """ # 文件數 numTrainDocs = len(trainMatrix) # 單詞數 numWords = len(trainMatrix[0]) # 侮辱性文件的出現概率,即trainCategory中所有的1的個數, # 代表的就是多少個侮辱性文件,與文件的總數相除就得到了侮辱性文件的出現概率 pAbusive = sum(trainCategory) / float(numTrainDocs) # 構造單詞出現次數列表 p0Num = zeros(numWords) # [0,0,0,.....] p1Num = zeros(numWords) # [0,0,0,.....] # 整個數據集單詞出現總數 p0Denom = 0.0 p1Denom = 0.0 for i in range(numTrainDocs): # 是否是侮辱性文件 if trainCategory[i] == 1: # 如果是侮辱性文件,對侮辱性文件的向量進行加和 p1Num += trainMatrix[i] #[0,1,1,....] + [0,1,1,....]->[0,2,2,...] # 對向量中的所有元素進行求和,也就是計算所有侮辱性文件中出現的單詞總數 p1Denom += sum(trainMatrix[i]) else: p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) # 類別1,即侮辱性文檔的[P(F1|C1),P(F2|C1),P(F3|C1),P(F4|C1),P(F5|C1)....]列表 # 即 在1類別下,每個單詞出現的概率 p1Vect = p1Num / p1Denom# [1,2,3,5]/90->[1/90,...] # 類別0,即正常文檔的[P(F1|C0),P(F2|C0),P(F3|C0),P(F4|C0),P(F5|C0)....]列表 # 即 在0類別下,每個單詞出現的概率 p0Vect = p0Num / p0Denom return p0Vect, p1Vect, pAbusive
測試算法: 根據現實情況修改分類器
在利用貝葉斯分類器對文檔進行分類時,要計算多個概率的乘積以獲得文檔屬於某個類別的概率,即計算 p(w0|1) * p(w1|1) * p(w2|1)。如果其中一個概率值為 0,那么最后的乘積也為 0。為降低這種影響,可以將所有詞的出現數初始化為 1,並將分母初始化為 2 (取1 或 2 的目的主要是為了保證分子和分母不為0,大家可以根據業務需求進行更改)。
另一個遇到的問題是下溢出,這是由於太多很小的數相乘造成的。當計算乘積 p(w0|ci) * p(w1|ci) * p(w2|ci)... p(wn|ci) 時,由於大部分因子都非常小,所以程序會下溢出或者得到不正確的答案。(用 Python 嘗試相乘許多很小的數,最后四舍五入后會得到 0)。一種解決辦法是對乘積取自然對數。在代數中有 ln(a * b) = ln(a) + ln(b), 於是通過求對數可以避免下溢出或者浮點數舍入導致的錯誤。同時,采用自然對數進行處理不會有任何損失。
下圖給出了函數 f(x) 與 ln(f(x)) 的曲線。可以看出,它們在相同區域內同時增加或者減少,並且在相同點上取到極值。它們的取值雖然不同,但不影響最終結果。
def trainNB0(trainMatrix, trainCategory): """ 訓練數據優化版本 :param trainMatrix: 文件單詞矩陣 :param trainCategory: 文件對應的類別 :return: """ # 總文件數 numTrainDocs = len(trainMatrix) # 總單詞數 numWords = len(trainMatrix[0]) # 侮辱性文件的出現概率 pAbusive = sum(trainCategory) / float(numTrainDocs) # 構造單詞出現次數列表 # p0Num 正常的統計 # p1Num 侮辱的統計 p0Num = ones(numWords)#[0,0......]->[1,1,1,1,1.....] p1Num = ones(numWords) # 整個數據集單詞出現總數,2.0根據樣本/實際調查結果調整分母的值(2主要是避免分母為0,當然值可以調整) # p0Denom 正常的統計 # p1Denom 侮辱的統計 p0Denom = 2.0 p1Denom = 2.0 for i in range(numTrainDocs): if trainCategory[i] == 1: # 累加辱罵詞的頻次 p1Num += trainMatrix[i] # 對每篇文章的辱罵的頻次 進行統計匯總 p1Denom += sum(trainMatrix[i]) else: p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) # 類別1,即侮辱性文檔的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表 p1Vect = log(p1Num / p1Denom) # 類別0,即正常文檔的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表 p0Vect = log(p0Num / p0Denom) return p0Vect, p1Vect, pAbusive
使用算法: 對社區留言板言論進行分類
朴素貝葉斯分類函數
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1): """ 使用算法: # 將乘法轉換為加法 乘法:P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C)/P(F1F2...Fn) 加法:P(F1|C)*P(F2|C)....P(Fn|C)P(C) -> log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C)) :param vec2Classify: 待測數據[0,1,1,1,1...],即要分類的向量 :param p0Vec: 類別0,即正常文檔的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表 :param p1Vec: 類別1,即侮辱性文檔的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表 :param pClass1: 類別1,侮辱性文件的出現概率 :return: 類別1 or 0 """ # 計算公式 log(P(F1|C))+log(P(F2|C))+....+log(P(Fn|C))+log(P(C)) # 大家可能會發現,上面的計算公式,沒有除以貝葉斯准則的公式的分母,也就是 P(w) (P(w) 指的是此文檔在所有的文檔中出現的概率)就進行概率大小的比較了, # 因為 P(w) 針對的是包含侮辱和非侮辱的全部文檔,所以 P(w) 是相同的。 # 使用 NumPy 數組來計算兩個向量相乘的結果,這里的相乘是指對應元素相乘,即先將兩個向量中的第一個元素相乘,然后將第2個元素相乘,以此類推。 # 我的理解是:這里的 vec2Classify * p1Vec 的意思就是將每個詞與其對應的概率相關聯起來 p1 = sum(vec2Classify * p1Vec) + log(pClass1) # P(w|c1) * P(c1) ,即貝葉斯准則的分子 p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1) # P(w|c0) * P(c0) ,即貝葉斯准則的分子· if p1 > p0: return 1 else: return 0 def testingNB(): """ 測試朴素貝葉斯算法 """ # 1. 加載數據集 listOPosts, listClasses = loadDataSet() # 2. 創建單詞集合 myVocabList = createVocabList(listOPosts) # 3. 計算單詞是否出現並創建數據矩陣 trainMat = [] for postinDoc in listOPosts: # 返回m*len(myVocabList)的矩陣, 記錄的都是0,1信息 trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 4. 訓練數據 p0V, p1V, pAb = trainNB0(array(trainMat), array(listClasses)) # 5. 測試數據 testEntry = ['love', 'my', 'dalmation'] thisDoc = array(setOfWords2Vec(myVocabList, testEntry)) print testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb) testEntry = ['stupid', 'garbage'] thisDoc = array(setOfWords2Vec(myVocabList, testEntry)) print testEntry, 'classified as: ', classifyNB(thisDoc, p0V, p1V, pAb)
完整代碼地址: https://github.com/apachecn/MachineLearning/blob/master/src/python/4.NaiveBayes/bayes.py
項目案例2: 使用朴素貝葉斯過濾垃圾郵件
項目概述
完成朴素貝葉斯的一個最著名的應用: 電子郵件垃圾過濾。
開發流程
使用朴素貝葉斯對電子郵件進行分類
收集數據: 提供文本文件
准備數據: 將文本文件解析成詞條向量
分析數據: 檢查詞條確保解析的正確性
訓練算法: 使用我們之前建立的 trainNB() 函數
測試算法: 使用朴素貝葉斯進行交叉驗證
使用算法: 構建一個完整的程序對一組文檔進行分類,將錯分的文檔輸出到屏幕上
收集數據: 提供文本文件
文本文件內容如下:
Hi Peter,
With Jose out of town, do you want to
meet once in a while to keep things
going and do some interesting stuff?
Let me know
Eugene
准備數據: 將文本文件解析成詞條向量
使用正則表達式來切分文本
>>> mySent = 'This book is the best book on Python or M.L. I have ever laid eyes upon.' >>> import re >>> regEx = re.compile('\\W*') >>> listOfTokens = regEx.split(mySent) >>> listOfTokens ['This', 'book', 'is', 'the', 'best', 'book', 'on', 'Python', 'or', 'M.L.', 'I', 'have', 'ever', 'laid', 'eyes', 'upon', '']
分析數據: 檢查詞條確保解析的正確性
訓練算法: 使用我們之前建立的 trainNB0() 函數
def trainNB0(trainMatrix, trainCategory): """ 訓練數據優化版本 :param trainMatrix: 文件單詞矩陣 :param trainCategory: 文件對應的類別 :return: """ # 總文件數 numTrainDocs = len(trainMatrix) # 總單詞數 numWords = len(trainMatrix[0]) # 侮辱性文件的出現概率 pAbusive = sum(trainCategory) / float(numTrainDocs) # 構造單詞出現次數列表 # p0Num 正常的統計 # p1Num 侮辱的統計 p0Num = ones(numWords)#[0,0......]->[1,1,1,1,1.....] p1Num = ones(numWords) # 整個數據集單詞出現總數,2.0根據樣本/實際調查結果調整分母的值(2主要是避免分母為0,當然值可以調整) # p0Denom 正常的統計 # p1Denom 侮辱的統計 p0Denom = 2.0 p1Denom = 2.0 for i in range(numTrainDocs): if trainCategory[i] == 1: # 累加辱罵詞的頻次 p1Num += trainMatrix[i] # 對每篇文章的辱罵的頻次 進行統計匯總 p1Denom += sum(trainMatrix[i]) else: p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) # 類別1,即侮辱性文檔的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表 p1Vect = log(p1Num / p1Denom) # 類別0,即正常文檔的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表 p0Vect = log(p0Num / p0Denom) return p0Vect, p1Vect, pAbusive
測試算法: 使用朴素貝葉斯進行交叉驗證
文件解析及完整的垃圾郵件測試函數
# 切分文本 def textParse(bigString): ''' Desc: 接收一個大字符串並將其解析為字符串列表 Args: bigString -- 大字符串 Returns: 去掉少於 2 個字符的字符串,並將所有字符串轉換為小寫,返回字符串列表 ''' import re # 使用正則表達式來切分句子,其中分隔符是除單詞、數字外的任意字符串 listOfTokens = re.split(r'\W*', bigString) return [tok.lower() for tok in listOfTokens if len(tok) > 2] def spamTest(): ''' Desc: 對貝葉斯垃圾郵件分類器進行自動化處理。 Args: none Returns: 對測試集中的每封郵件進行分類,若郵件分類錯誤,則錯誤數加 1,最后返回總的錯誤百分比。 ''' docList = [] classList = [] fullText = [] for i in range(1, 26): # 切分,解析數據,並歸類為 1 類別 wordList = textParse(open('input/4.NaiveBayes/email/spam/%d.txt' % i).read()) docList.append(wordList) classList.append(1) # 切分,解析數據,並歸類為 0 類別 wordList = textParse(open('input/4.NaiveBayes/email/ham/%d.txt' % i).read()) docList.append(wordList) fullText.extend(wordList) classList.append(0) # 創建詞匯表 vocabList = createVocabList(docList) trainingSet = range(50) testSet = [] # 隨機取 10 個郵件用來測試 for i in range(10): # random.uniform(x, y) 隨機生成一個范圍為 x - y 的實數 randIndex = int(random.uniform(0, len(trainingSet))) testSet.append(trainingSet[randIndex]) del(trainingSet[randIndex]) trainMat = [] trainClasses = [] for docIndex in trainingSet: trainMat.append(setOfWords2Vec(vocabList, docList[docIndex])) trainClasses.append(classList[docIndex]) p0V, p1V, pSpam = trainNB0(array(trainMat), array(trainClasses)) errorCount = 0 for docIndex in testSet: wordVector = setOfWords2Vec(vocabList, docList[docIndex]) if classifyNB(array(wordVector), p0V, p1V, pSpam) != classList[docIndex]: errorCount += 1 print 'the errorCount is: ', errorCount print 'the testSet length is :', len(testSet) print 'the error rate is :', float(errorCount)/len(testSet)
使用算法: 構建一個完整的程序對一組文檔進行分類,將錯分的文檔輸出到屏幕上
完整代碼地址: https://github.com/apachecn/MachineLearning/blob/master/src/python/4.NaiveBayes/bayes.py
項目案例3: 使用朴素貝葉斯分類器從個人廣告中獲取區域傾向
項目概述
廣告商往往想知道關於一個人的一些特定人口統計信息,以便能更好地定向推銷廣告。
我們將分別從美國的兩個城市中選取一些人,通過分析這些人發布的信息,來比較這兩個城市的人們在廣告用詞上是否不同。如果結論確實不同,那么他們各自常用的詞是那些,從人們的用詞當中,我們能否對不同城市的人所關心的內容有所了解。
開發流程
收集數據: 從 RSS 源收集內容,這里需要對 RSS 源構建一個接口
准備數據: 將文本文件解析成詞條向量
分析數據: 檢查詞條確保解析的正確性
訓練算法: 使用我們之前簡歷的 trainNB0() 函數
測試算法: 觀察錯誤率,確保分類器可用。可以修改切分程序,以降低錯誤率,提高分類結果
使用算法: 構建一個完整的程序,封裝所有內容。給定兩個 RSS 源,改程序會顯示最常用的公共詞
收集數據: 從 RSS 源收集內容,這里需要對 RSS 源構建一個接口
也就是導入 RSS 源,我們使用 python 下載文本,在http://code.google.com/p/feedparser/ 下瀏覽相關文檔,安裝 feedparse,首先解壓下載的包,並將當前目錄切換到解壓文件所在的文件夾,然后在 python 提示符下輸入:
>>> python setup.py install
准備數據: 將文本文件解析成詞條向量
文檔詞袋模型
我們將每個詞的出現與否作為一個特征,這可以被描述為 詞集模型(set-of-words model)。如果一個詞在文檔中出現不止一次,這可能意味着包含該詞是否出現在文檔中所不能表達的某種信息,這種方法被稱為 詞袋模型(bag-of-words model)。在詞袋中,每個單詞可以出現多次,而在詞集中,每個詞只能出現一次。為適應詞袋模型,需要對函數 setOfWords2Vec() 稍加修改,修改后的函數為 bagOfWords2Vec() 。
如下給出了基於詞袋模型的朴素貝葉斯代碼。它與函數 setOfWords2Vec() 幾乎完全相同,唯一不同的是每當遇到一個單詞時,它會增加詞向量中的對應值,而不只是將對應的數值設為 1 。
def bagOfWords2VecMN(vocaList, inputSet): returnVec = [0] * len(vocabList) for word in inputSet: if word in inputSet: returnVec[vocabList.index(word)] += 1 return returnVec
#創建一個包含在所有文檔中出現的不重復詞的列表 def createVocabList(dataSet): vocabSet=set([]) #創建一個空集 for document in dataSet: vocabSet=vocabSet|set(document) #創建兩個集合的並集 return list(vocabSet) def setOfWords2VecMN(vocabList,inputSet): returnVec=[0]*len(vocabList) #創建一個其中所含元素都為0的向量 for word in inputSet: if word in vocabList: returnVec[vocabList.index(word)]+=1 return returnVec #文件解析 def textParse(bigString): import re listOfTokens=re.split(r'\W*',bigString) return [tok.lower() for tok in listOfTokens if len(tok)>2]
分析數據: 檢查詞條確保解析的正確性
訓練算法: 使用我們之前簡歷的 trainNB0() 函數
def trainNB0(trainMatrix, trainCategory): """ 訓練數據優化版本 :param trainMatrix: 文件單詞矩陣 :param trainCategory: 文件對應的類別 :return: """ # 總文件數 numTrainDocs = len(trainMatrix) # 總單詞數 numWords = len(trainMatrix[0]) # 侮辱性文件的出現概率 pAbusive = sum(trainCategory) / float(numTrainDocs) # 構造單詞出現次數列表 # p0Num 正常的統計 # p1Num 侮辱的統計 # 避免單詞列表中的任何一個單詞為0,而導致最后的乘積為0,所以將每個單詞的出現次數初始化為 1 p0Num = ones(numWords)#[0,0......]->[1,1,1,1,1.....] p1Num = ones(numWords) # 整個數據集單詞出現總數,2.0根據樣本/實際調查結果調整分母的值(2主要是避免分母為0,當然值可以調整) # p0Denom 正常的統計 # p1Denom 侮辱的統計 p0Denom = 2.0 p1Denom = 2.0 for i in range(numTrainDocs): if trainCategory[i] == 1: # 累加辱罵詞的頻次 p1Num += trainMatrix[i] # 對每篇文章的辱罵的頻次 進行統計匯總 p1Denom += sum(trainMatrix[i]) else: p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) # 類別1,即侮辱性文檔的[log(P(F1|C1)),log(P(F2|C1)),log(P(F3|C1)),log(P(F4|C1)),log(P(F5|C1))....]列表 p1Vect = log(p1Num / p1Denom) # 類別0,即正常文檔的[log(P(F1|C0)),log(P(F2|C0)),log(P(F3|C0)),log(P(F4|C0)),log(P(F5|C0))....]列表 p0Vect = log(p0Num / p0Denom) return p0Vect, p1Vect, pAbusive
測試算法: 觀察錯誤率,確保分類器可用。可以修改切分程序,以降低錯誤率,提高分類結果
#RSS源分類器及高頻詞去除函數 def calcMostFreq(vocabList,fullText): import operator freqDict={} for token in vocabList: #遍歷詞匯表中的每個詞 freqDict[token]=fullText.count(token) #統計每個詞在文本中出現的次數 sortedFreq=sorted(freqDict.iteritems(),key=operator.itemgetter(1),reverse=True) #根據每個詞出現的次數從高到底對字典進行排序 return sortedFreq[:30] #返回出現次數最高的30個單詞 def localWords(feed1,feed0): import feedparser docList=[];classList=[];fullText=[] minLen=min(len(feed1['entries']),len(feed0['entries'])) for i in range(minLen): wordList=textParse(feed1['entries'][i]['summary']) #每次訪問一條RSS源 docList.append(wordList) fullText.extend(wordList) classList.append(1) wordList=textParse(feed0['entries'][i]['summary']) docList.append(wordList) fullText.extend(wordList) classList.append(0) vocabList=createVocabList(docList) top30Words=calcMostFreq(vocabList,fullText) for pairW in top30Words: if pairW[0] in vocabList:vocabList.remove(pairW[0]) #去掉出現次數最高的那些詞 trainingSet=range(2*minLen);testSet=[] for i in range(20): randIndex=int(random.uniform(0,len(trainingSet))) testSet.append(trainingSet[randIndex]) del(trainingSet[randIndex]) trainMat=[];trainClasses=[] for docIndex in trainingSet: trainMat.append(bagOfWords2VecMN(vocabList,docList[docIndex])) trainClasses.append(classList[docIndex]) p0V,p1V,pSpam=trainNBO(array(trainMat),array(trainClasses)) errorCount=0 for docIndex in testSet: wordVector=bagOfWords2VecMN(vocabList,docList[docIndex]) if classifyNB(array(wordVector),p0V,p1V,pSpam)!=classList[docIndex]: errorCount+=1 print 'the error rate is:',float(errorCount)/len(testSet) return vocabList,p0V,p1V #朴素貝葉斯分類函數 def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1): p1=sum(vec2Classify*p1Vec)+log(pClass1) p0=sum(vec2Classify*p0Vec)+log(1.0-pClass1) if p1>p0: return 1 else: return 0
使用算法: 構建一個完整的程序,封裝所有內容。給定兩個 RSS 源,改程序會顯示最常用的公共詞
函數 localWords() 使用了兩個 RSS 源作為參數,RSS 源要在函數外導入,這樣做的原因是 RSS 源會隨時間而改變,重新加載 RSS 源就會得到新的數據
>>> reload(bayes) <module 'bayes' from 'bayes.pyc'> >>> import feedparser >>> ny=feedparser.parse('http://newyork.craigslist.org/stp/index.rss') >>> sy=feedparser.parse('http://sfbay.craigslist.org/stp/index.rss') >>> vocabList,pSF,pNY=bayes.localWords(ny,sf) the error rate is: 0.2 >>> vocabList,pSF,pNY=bayes.localWords(ny,sf) the error rate is: 0.3 >>> vocabList,pSF,pNY=bayes.localWords(ny,sf) the error rate is: 0.55
為了得到錯誤率的精確估計,應該多次進行上述實驗,然后取平均值
接下來,我們要分析一下數據,顯示地域相關的用詞
可以先對向量pSF與pNY進行排序,然后按照順序打印出來,將下面的代碼添加到文件中:
#最具表征性的詞匯顯示函數 def getTopWords(ny,sf): import operator vocabList,p0V,p1V=localWords(ny,sf) topNY=[];topSF=[] for i in range(len(p0V)): if p0V[i]>-6.0:topSF.append((vocabList[i],p0V[i])) if p1V[i]>-6.0:topNY.append((vocabList[i],p1V[i])) sortedSF=sorted(topSF,key=lambda pair:pair[1],reverse=True) print "SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**" for item in sortedSF: print item[0] sortedNY=sorted(topNY,key=lambda pair:pair[1],reverse=True) print "NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**" for item in sortedNY: print item[0]
函數 getTopWords() 使用兩個 RSS 源作為輸入,然后訓練並測試朴素貝葉斯分類器,返回使用的概率值。然后創建兩個列表用於元組的存儲,與之前返回排名最高的 X 個單詞不同,這里可以返回大於某個閾值的所有詞,這些元組會按照它們的條件概率進行排序。
保存 bayes.py 文件,在python提示符下輸入:
>>> reload(bayes) <module 'bayes' from 'bayes.pyc'> >>> bayes.getTopWords(ny,sf) the error rate is: 0.55 SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF** how last man ... veteran still ends late off own know NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY** someone meet ... apparel recalled starting strings
當注釋掉用於移除高頻詞的三行代碼,然后比較注釋前后的分類性能,去掉這幾行代碼之后,錯誤率為54%,,而保留這些代碼得到的錯誤率為70%。這里觀察到,這些留言中出現次數最多的前30個詞涵蓋了所有用詞的30%,vocabList的大小約為3000個詞,也就是說,詞匯表中的一小部分單詞卻占據了所有文本用詞的一大部分。產生這種現象的原因是因為語言中大部分都是冗余和結構輔助性內容。另一個常用的方法是不僅移除高頻詞,同時從某個預定高頻詞中移除結構上的輔助詞,該詞表稱為停用詞表。
最后輸出的單詞,可以看出程序輸出了大量的停用詞,可以移除固定的停用詞看看結果如何,這樣做的花,分類錯誤率也會降低。
完整代碼地址: https://github.com/apachecn/MachineLearning/blob/master/src/python/4.NaiveBayes/bayes.py
- 作者:羊三 小瑤
- GitHub地址: https://github.com/apachecn/MachineLearning
- 版權聲明:歡迎轉載學習 => 請標注信息來源於 ApacheCN