樸素貝葉斯分類算法的核心是貝葉斯公式:

‘樸素’二字的含義是指,某一特征取值的概率相對(duì)于其他特征的取值獨(dú)立,也就是實(shí)例的各個(gè)特征的分布相對(duì)獨(dú)立。
樸素貝葉斯分類器的另一個(gè)假設(shè)是,每個(gè)特征同等重要。
關(guān)于樸素貝葉斯講的清晰易懂的文章可見這里。
樸素貝葉斯是用于文檔分類的常用算法,本章也是使用文檔分類來講解的。
- 從文本中構(gòu)建詞向量
- 使用詞向量訓(xùn)練分類器
- 使用分類器來分類
1. 從文本中構(gòu)建詞向量
想要對(duì)文本進(jìn)行分類,一個(gè)問題是不同文本的長(zhǎng)度并不相同,該如何操作呢?可以先構(gòu)建一個(gè)詞典(詞匯表),其中包含n個(gè)詞。對(duì)于一篇文章將其表示為一個(gè)長(zhǎng)為n的詞典向量,其中值為1表示詞典中的對(duì)應(yīng)詞在文章中出現(xiàn)過(不論次數(shù)),0表示未出現(xiàn)。這樣對(duì)于每篇文章都可以得到一個(gè)對(duì)應(yīng)的詞向量,可用于訓(xùn)練分類器,以及進(jìn)行分類。
那么該如何構(gòu)造詞典呢?可以使用訓(xùn)練集,或者通過對(duì)大量文章的統(tǒng)計(jì)來構(gòu)建詞典。注意詞典中的詞都是唯一的。書中使用了一個(gè)小小的訓(xùn)練集來講解構(gòu)造詞典的過程。
"""
實(shí)驗(yàn)樣本,注意這里給的例子只是二分類問題。
"""
def loadDataSet():
# 每一行為一個(gè)進(jìn)行了詞條切分的句子
# 可用正則表達(dá)式來處理文本。
postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is' , 'so', 'sute', '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]
return postingList, classVec
"""
根據(jù)輸入數(shù)據(jù)集構(gòu)造詞典,注意這里沒有排序
"""
# dataSet即為上面postingList的形式。
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
# 或,即為集合的并。。。
vocabSet = vocabSet | set(document)
return list(vocabSet)
>>> listOPosts, listClasses = loadDataSet()
# myVocabList就是根據(jù)數(shù)據(jù)集構(gòu)造好的詞典
# 其中的單詞是隨機(jī)排列的
>>> myVocabList = createVocabList(listOPosts)
下面代碼根據(jù)上面構(gòu)造好的詞典對(duì)于屬于文章構(gòu)造對(duì)應(yīng)的詞向量:
"""
根據(jù)構(gòu)造好的字典,對(duì)于輸入文檔,構(gòu)造該文檔的詞向量
"""
# vocabList為構(gòu)造好的詞典
# inputSet為進(jìn)行了詞條切分的文章,形式如postingList的各個(gè)行。
# 返回returnVec為對(duì)應(yīng)的詞向量。
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
pirnt("the word: %s is not in my Vocabulary!"%word)
return returnVec
# 得到第一個(gè)句子對(duì)應(yīng)的詞向量
>>> setOfWords2Vec(myVocabList, listOPosts[0])
2. 使用詞向量訓(xùn)練分類器
對(duì)于樸素貝葉斯分類器,訓(xùn)練分類器時(shí)到底是使分類器學(xué)習(xí)什么呢?
已知輸入實(shí)例為w,w=[w_1, w_2, w_3, ..., w_n](有n個(gè)特征,w_j為特征wj的取值),要預(yù)測(cè)其類別,即找到類別Ci,使P(Ci | w)最大。
而根據(jù)貝葉斯公式可得到:
P(Ci | w) = P(w | Ci ) * P(Ci) / P(w)。
又由于樸素貝葉斯的假設(shè),即各個(gè)特征的分布獨(dú)立,則可得到:
P(w | Ci) = P(w1 | Ci) * P(w2 | Ci) * P(w3 | Ci) * ... * P(wn | Ci)
也就是對(duì)于各個(gè)類別Ci,在該類別中,實(shí)例w各特征取值的概率。
P(w) = P(w1) * P(w2) * P(w3) * ... * P(wn)
也就是實(shí)例各特征取值的概率。
所以,學(xué)習(xí)的就是三個(gè)東西,對(duì)應(yīng)公式中的三項(xiàng)(注意,都是根據(jù)訓(xùn)練集來學(xué)習(xí)的這些概率,如果訓(xùn)練集分布越全面,那么學(xué)到的效果也就越好),知道了這三項(xiàng)就可以對(duì)新實(shí)例進(jìn)行分類:
- P(wj | Ci) :j為特征索引(1<= j <= n),i即為類別索引。該概率也就是指,對(duì)于各個(gè)類別i,在該類別中,求出各特征j取各個(gè)值的概率。
- P(Ci) :求訓(xùn)練集中各類別出現(xiàn)的概率,很簡(jiǎn)單,用各類別數(shù)量除以樣本數(shù)量即可。
- P(wj) :就是特征j的各個(gè)取值,在樣本中出現(xiàn)的頻率。注意,該項(xiàng)與類別無關(guān),訓(xùn)練集確定了,該項(xiàng)的值也就確定了,所以在進(jìn)行預(yù)測(cè)時(shí)不需要計(jì)算該項(xiàng),只需計(jì)算上面兩項(xiàng)即可。
下面為書中給的訓(xùn)練分類器的代碼,只適用于二分類的問題,如果要用于多分類則需要重寫代碼嘍:
"""
該代碼針對(duì)應(yīng)于兩種特殊情況寫的:
1. 這是一個(gè)二分類問題,label為0或1.
2. 由于要對(duì)文本分類,且使用的是詞向量,那么每個(gè)特征的取值只有兩種:
1或0(該詞出現(xiàn)與否)。
由于都是0或1的取值,所有代碼中有取巧的部分。
如果是特征值有多種取值情況,或多分類,那么該代碼不適用了。
"""
import numpy as np
# trianMatrix為文檔矩陣,每一行都是詞向量
# trainCategory對(duì)應(yīng)于trainMatrix中每一行的類別
# 這里代碼為二分類,所以trainCategory為0、1組成的向量。
def trainNB0(trainMatrix, trainCategory):
# 樣本數(shù)量
numTrainDocs = len(trainMatrix)
# 特征個(gè)數(shù)
numWords = len(trainMatrix[0])
# 類別1的數(shù)量/總樣本數(shù)量=類別1出現(xiàn)的概率
pAbusive = np.sum(trainCategory) / float(numTrainDocs)
# 用于存儲(chǔ)兩個(gè)類別下各個(gè)特征出現(xiàn)的次數(shù)。
p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)
# 按理來說P(wj | Ci)為類別Ci時(shí),特征wj取各個(gè)值的概率
# 應(yīng)等于wj為某一特征值的數(shù)量處理該類別Ci中樣本數(shù)量的。
# 但這里的做法是除以所有單詞出現(xiàn)的次數(shù)了,這樣條件概率的值就小得很多了。
p0Denom = 0.0; p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
# 由于詞向量為0、1組成的向量,所以直接向量相加可得該
# 類別下某一特征值出現(xiàn)的次數(shù)(詞出現(xiàn)的次數(shù))。
p1Num += trainMatrix[i]
# 這里p1Denom為類別1時(shí)所有詞出現(xiàn)的次數(shù)的和。
# 然后用pqNum / p1Denom。
# 我感覺這是不對(duì)的,應(yīng)改為p1Denom += 1
# 使p1Denom統(tǒng)計(jì)類別1時(shí)的樣本數(shù)量,因?yàn)楦魈卣髦抵挥? # 兩種取值,所以這樣pqNum / p1Denom就為類別1時(shí)
# 特征值為1的頻率(也就是該詞出現(xiàn)的頻率)。
p1Denom += np.sum(trainMatrix[i])
else:
# 同上,不過是類別0
p0Num += trainMatrix[i]
p0Denom += np.sum(trainMatrix[i])
# 如上注釋所示。
p1Vect = p1Num / p1Denom
p0Vect = p0Num / p0Denom
# p0Vect各列應(yīng)為類別0時(shí),各特征為1的概率(也就是各詞出現(xiàn)的概率)
return p0Vect, p1Vect, pAbusive
文中對(duì)上述代碼的改進(jìn)有下面兩點(diǎn):
由于在進(jìn)行預(yù)測(cè)時(shí),進(jìn)行的計(jì)算P(w | Ci) = P(w1 | Ci) * P(w2 | Ci) * P(w3 | Ci) * ... * P(wn | Ci),當(dāng)特征過多時(shí),很容易造成下溢出問題。所以這里應(yīng)對(duì)p0Vect和p1Vect取log,那么就變?yōu)?
log(P(w | Ci) * P(Ci)) = log(P(w | Ci) ) + log(Ci))
log(P(w | Ci) )= log(P(w1 | Ci)) + log(P(w2 | Ci)) + log(P(w3 | Ci)) + ... + log(P(wn | Ci))另一點(diǎn),對(duì)于某一i和j,P(wj | Ci)可能為0,也就是在該類別中該詞從未出現(xiàn)過。這很正常,如罵人的詞就只能出現(xiàn)在臟話中。而對(duì)0取log會(huì)得到負(fù)無窮,這可不行,所以修改了上面代碼,假設(shè)每個(gè)詞在每一類中都出現(xiàn)過一次:p0Num = np.ones(numWords); p1Num = np.ones(numWords)
文中的另一項(xiàng)修改是p0Denom = 2.0; p1Denom = 2.0,這我就沒看懂了,感覺改的毫無意義。。。
以上就為訓(xùn)練過程了,很簡(jiǎn)單,對(duì)于文本的二分類是適用的。更復(fù)雜的分類就要重寫代碼了。過程不重要,重要的是思路。
3. 使用分類器來分類
"""
很簡(jiǎn)單,思路就如上所示。
"""
# vec2Classify為待分類的詞向量
# p0Vec和p1Vec各列為條件概率log(P(wj | Ci))
# pClass1為P(C1)
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
# log(P(w | C1) * P(C1))
p1 = np.sum(vec2Classify * p1Vec) + np.log(pClass1)
# log(P(w | C2) * P(C2))
p0 = np.sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0