一. 生成式(generative)學習算法
如果算法直接學習力九,或者嘗試學習從輸入空間到類別的映射關(guān)系的算法筐咧,稱為判別式(discriminative)學習算法误褪;比線性回歸(lineaar regression)的模型:
再比如邏輯回歸(logistic regression):
這里的是sigmoid函數(shù)
而另外一種算法是建立(和)的模型亭螟,這類算法稱為生成式(generative)學習算法,我們今天要討論的樸素貝葉斯算法即是其中一個榆俺。對(先驗 prior)和售躁,使用貝葉斯規(guī)則來推導給定的的后驗(posterior)分布:
其中分母可由全概率公式計算:
實際上,我們在計算做預測時茴晋,可以不用計算分母陪捷,因為:
二. 樸素貝葉斯(Naive Bayes)
2.1 樸素貝葉斯算法
假設(shè)我們想利用機器學習來構(gòu)建一個言論過濾器,希望對網(wǎng)站的留言評論自動區(qū)分是侮辱性言論诺擅,還是非侮辱性言論∈行洌現(xiàn)在已知一個訓練數(shù)據(jù)集(一些已經(jīng)標記為侮辱或者非侮辱的言論集合)如下:
"""
函數(shù)說明:創(chuàng)建實驗樣本
Parameters:
無
Returns:
postingList - 實驗樣本切分的詞條
classVec - 類別標簽向量
"""
def loadDataSet():
postingList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'], # 切分的詞條
['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] # 對應postingList中每個樣本的類別標簽向量,1代表侮辱性言論烁涌, 0代表非侮辱性言論
return postingList, classVec
令 表示該言論是否是侮辱性言論:
接下來苍碟,我們將樣本數(shù)據(jù)蹂安,即postingList涎永,轉(zhuǎn)化成特征向量受扳,用 表示茫陆,其中包含單詞特征:
編碼到向量中的單詞的集合稱為詞匯表痹升,例如“ my”拒课,“stupid”等等鳞骤,向量的長度等于詞匯表的長度探颈。
注意继效,這里的詞匯表不是將英語詞典中單詞全部列出來症杏,通常是將訓練數(shù)據(jù)集中的所有單詞,即使僅出現(xiàn)過一次瑞信,放到詞匯表中厉颤。這樣做可以減少模型的詞匯數(shù)量,從而減少計算量凡简,節(jié)省空間逼友;還有個好處是能夠?qū)⒂⒄Z詞典中不會出現(xiàn)的詞,但會出現(xiàn)在留言評論中的詞秤涩,比如“”放到詞匯表中帜乞;同時還需要剔除一些高頻詞,比如 “” “” “”筐眷,這些詞在很多的文本中都會出現(xiàn)黎烈,對區(qū)分是否是侮辱性言論沒有任何幫助。
特征向量與詞匯表如下圖所示,言論中包含“”照棋,“buy”资溃,不包含“aardvard”,“aardwolf”烈炭,“zygmurgy”:
創(chuàng)建詞匯表代碼如下:
"""
函數(shù)說明:將切分的實驗樣本詞條整理成不重復的詞條列表溶锭,也就是詞匯表
Parameters:
dataSet - 樣本數(shù)據(jù)集 postingList
Returns:
vocabSet - 返回不重復的詞條列表,也就是詞匯表
"""
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document)
return list(vocabSet)
將樣本數(shù)據(jù)轉(zhuǎn)換成特征向量代碼如下:
"""
函數(shù)說明:根據(jù)vocabList詞匯表符隙,將inputSet向量化趴捅,向量的每個元素為1或0
Parameters:
vocabList - 詞匯表
inputSet - 某條言論文檔
Returns:
returnVec - 言論文檔向量
"""
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 創(chuàng)建一個其中所含元素都為0的向量
for word in inputSet: # 遍歷每個詞條
if word in vocabList: # 如果詞條存在于詞匯表中,則置1
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my vocabulary !" % word)
return returnVec
現(xiàn)在我們來構(gòu)建霹疫。上面代碼示例中詞匯表中單詞較少驻售,但如果詞匯表中包含50000個單詞,那么(是50000維0和1組成的向量)更米,如果我們直接用多項式來構(gòu)造的個可能的結(jié)果,那么多項式的參數(shù)向量有維毫痕,顯然參數(shù)太多了征峦。
因此算法做了一個強假設(shè),即假設(shè)給定的情況下條件獨立消请,這個假設(shè)就稱為樸素貝葉斯假設(shè)栏笆,算法稱為樸素貝葉斯分類。例如臊泰,如果表示侮辱性言論蛉加,“”是第個單詞,“”是第個單詞缸逃,那么我們假設(shè)如果已知针饥,那么的值(“”是否出現(xiàn)在言論中) 對的值(“”是否出現(xiàn)在言論中)沒有任何影響。正式一點來描述需频,上述可以寫成丁眼。需要注意的是,這里并不是說和相互獨立昭殉,相互獨立表示為苞七,而是僅假設(shè)和在給定的情況下條件獨立。根據(jù)上述假設(shè)挪丢,有如下等式成立:
第一個等式是概率的基本性質(zhì)蹂风,第二個等式用了樸素貝葉斯假設(shè)。
我們的模型中的參數(shù)為乾蓬,和惠啄,其中為詞匯表中第個單詞。給定一個訓練數(shù)據(jù)集,我們可以寫出似然函數(shù):
最大化關(guān)于參數(shù), , 的上述似然函數(shù)礁阁,得到第個單詞相關(guān)參數(shù)的極大似然估計:
即是對的估計巧号;
即是對的估計;
即是對的估計姥闭;同理
即是對的估計丹鸿;參數(shù)表達式中的 表示“與”。上面的參數(shù)不難理解棚品,就是侮辱性言論()中單詞出現(xiàn)的百分比靠欢。訓練階段代碼如下:
"""
函數(shù)說明:樸素貝葉斯分類器訓練函數(shù)
Parameters:
trainMatrix - 訓練文檔矩陣,即setOfWords2Vec返回的returnVec構(gòu)成的矩陣
trainCategory - 訓練類別標簽向量铜跑,即loadDataSet返回的classVec
Returns:
p0Vect - 非侮辱類的條件概率數(shù)組
p1Vect - 侮辱類的條件概率數(shù)組
pAbusive - 文檔屬于侮辱類的概率
"""
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #計算訓練的文檔數(shù)目
numWords = len(trainMatrix[0]) #計算每篇文檔的詞條數(shù)
pAbusive = sum(trainCategory)/float(numTrainDocs) #文檔屬于侮辱類的概率
p0Num = np.zeros(numWords) #創(chuàng)建numpy.zeros數(shù)組,詞條出現(xiàn)數(shù)初始化為0
p1Num = np.zeros(numWords)
p0Denom = 0.0 #分母初始化為0
p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1: #向量相加门怪,統(tǒng)計屬于侮辱類的條件概率所需的數(shù)據(jù),即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: #向量相加锅纺,統(tǒng)計屬于非侮辱類的條件概率所需的數(shù)據(jù)掷空,即P(w0|0),P(w1|0),P(w2|0)···
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = p1Num/p1Denom
p0Vect = p0Num/p0Denom
return p0Vect, p1Vect, pAbusive #返回屬于侮辱類的條件概率數(shù)組,屬于非侮辱類的條件概率數(shù)組囤锉,文檔屬于侮辱類的概率
已知參數(shù)估計之后坦弟,對一個特征向量的新樣本計算是侮辱性言論的概率如下:
同樣,計算非侮辱性言論的概率如下:
如果計算得出官地,則認為言論是侮辱性言論酿傍,反之是非侮辱性言論,預測代碼如下:
"""
函數(shù)說明:樸素貝葉斯分類器分類函數(shù)
Parameters:
vec2Classify - 待分類的詞條數(shù)組
p0Vec - 侮辱類的條件概率數(shù)組
p1Vec -非侮辱類的條件概率數(shù)組
pClass1 - 文檔屬于侮辱類的概率
Returns:
0 - 屬于非侮辱類
1 - 屬于侮辱類
"""
def classifyNB0(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = reduce(lambda x,y : x * y, vec2Classify * p1Vec) * pClass1 # 對應元素相乘
p0 = reduce(lambda x,y : x * y, vec2Classify * p0Vec) * (1 - pClass1)
print('p0:', p0)
print('p1', p1)
if p1 > p0:
return p1
else:
return p0
2.2 下溢出與拉普拉斯平滑
2.1節(jié)中我們使用樸素貝葉斯算法構(gòu)造了言論分類器驱入,下面我們對算法進行測試:
"""
函數(shù)說明:測試樸素貝葉斯分類器
Parameters:
無
Returns:
無
"""
def testingNB0():
listOPosts, classVec = loadDataSet() # 創(chuàng)建實驗樣本
myVocabList = createVocabList(listOPosts) # 創(chuàng)建詞匯表
# 打印中間結(jié)果
print('myVocabList:\n', myVocabList)
trainMat = []
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 將實驗樣本向量化
p0V, p1V, pAb = trainNB0(np.array(trainMat), np.array(classVec)) # 訓練樸素貝葉斯分類器
# 打印中間結(jié)果
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('classVec:\n', classVec)
print('pAb:\n', pAb)
testEntry = ['love', 'my', 'dalmation'] # 測試樣本1
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 測試樣本向量化
if classifyNB0(thisDoc, p0V, p1V, pAb):
print(testEntry, '屬于侮辱類') # 執(zhí)行分類并打印分類結(jié)果
else:
print(testEntry, '屬于非侮辱類') # 執(zhí)行分類并打印分類結(jié)果
testEntry = ['stupid', 'garbage'] # 測試樣本2
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 測試樣本向量化
if classifyNB0(thisDoc, p0V, p1V, pAb):
print(testEntry, '屬于侮辱類') # 執(zhí)行分類并打印分類結(jié)果
else:
print(testEntry, '屬于非侮辱類') # 執(zhí)行分類并打印分類結(jié)果
if __name__ == "__main__":
testingNB0()
運行結(jié)果截圖如下:
小伙伴們赤炒,發(fā)現(xiàn)問題了嗎?顯然這個結(jié)果是不對的亏较。算法存在兩個問題:
- 某些單詞0概率莺褒,導致整體乘積為0,例如“”宴杀;
- 下溢出(underflow):
對于第一個問題癣朗,我們通過打印中間結(jié)果可以看出:
又或者對一個訓練數(shù)據(jù)集中未出現(xiàn)的詞,概率也是0旺罢,顯然旷余,這樣是不合理的,因為訓練集有限扁达,不能因為訓練集中沒有出現(xiàn)正卧,就認為這個詞永遠不會出現(xiàn)。記得吳恩達老師在機器學習課上講了個段子跪解,斯坦福大學的新酰籃球隊接連輸了5場比賽,問下一場比賽贏的概率???連輸5場窘行,下一場肯定輸嗎饥追?合理的預測是有贏的概率,只是比較小罐盔,設(shè)置贏的概率為1/7吧??但绕。這種做法就叫做拉普拉斯平滑(Laplace Smoothing)。具體做法是修改極大似然估計的參數(shù):
即是對的估計惶看;
即是對的估計捏顺。
其中代表的可能取值數(shù)量,我們的例子中的取值只有和兩種纬黎,因此幅骄。
那么為什么分子加,分母加呢本今?因為要保證相加仍然是拆座。下面我們檢驗一下,假設(shè)隨機變量的取值有個
其中冠息,則應用拉普拉斯平滑之后
不難推出
除此之外懂拾,另外一個遇到的問題就是下溢出,這是由于太多很小的數(shù)相乘造成的铐达。學過數(shù)學的人都知道,兩個小數(shù)相乘檬果,越乘越小瓮孙,這樣就造成了下溢出。在程序中选脊,在相應小數(shù)位置進行四舍五入杭抠,計算結(jié)果可能就變成0了。為了解決這個問題恳啥,對乘積結(jié)果取自然對數(shù)偏灿。通過求對數(shù)可以避免下溢出或者浮點數(shù)舍入導致的錯誤。同時钝的,采用自然對數(shù)進行處理不會有任何損失翁垂。下圖給出函數(shù)f(x)和ln(f(x))的曲線:
檢查這兩條曲線,就會發(fā)現(xiàn)它們在相同區(qū)域內(nèi)同時增加或者減少硝桩,并且在相同點上取到極值沿猜。它們的取值雖然不同,但不影響最終結(jié)果碗脊。因此我們可以對上篇文章的trainNB0(trainMatrix, trainCategory)函數(shù)進行更改啼肩,修改如下:
"""
函數(shù)說明:樸素貝葉斯分類器訓練函數(shù)
Parameters:
trainMatrix - 訓練文檔矩陣,即setOfWords2Vec返回的returnVec構(gòu)成的矩陣
trainCategory - 訓練類別標簽向量,即loadDataSet返回的classVec
Returns:
p0Vect - 非侮辱類的條件概率數(shù)組
p1Vect - 侮辱類的條件概率數(shù)組
pAbusive - 文檔屬于侮辱類的概率
"""
def trainNB(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 計算訓練的文檔數(shù)目
numWords = len(trainMatrix[0]) # 計算每篇文檔的詞條數(shù)
pAbusive = sum(trainCategory) / float(numTrainDocs) # 文檔屬于侮辱類的概率
p0Num = np.ones(numWords)
p1Num = np.ones(numWords) # 創(chuàng)建numpy.ones數(shù)組,詞條出現(xiàn)數(shù)初始化為1祈坠,拉普拉斯平滑
p0Denom = 2.0
p1Denom = 2.0 # 分母初始化為2,拉普拉斯平滑
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 向量相加害碾,統(tǒng)計屬于侮辱類的條件概率所需的數(shù)據(jù),赦拘,即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: # 向量相加慌随,統(tǒng)計屬于非侮辱類的條件概率所需的數(shù)據(jù),即P(w0|0),P(w1|0),P(w2|0)···
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num / p1Denom) # 取對數(shù)另绩,防止下溢出
p0Vect = np.log(p0Num / p0Denom)
return p0Vect, p1Vect, pAbusive # 返回屬于侮辱類的條件概率數(shù)組儒陨,屬于非侮辱類的條件概率數(shù)組,文檔屬于侮辱類的概率
"""
函數(shù)說明:樸素貝葉斯分類器分類函數(shù)
Parameters:
vec2Classify - 待分類的詞條數(shù)組
p0Vec - 非侮辱類的條件概率數(shù)組
p1Vec -侮辱類的條件概率數(shù)組
pClass1 - 文檔屬于侮辱類的概率
Returns:
0 - 屬于非侮辱類
1 - 屬于侮辱類
"""
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) # 對應元素相乘笋籽。logA * B = logA + logB蹦漠,所以這里加上log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
"""
函數(shù)說明:測試樸素貝葉斯分類器
Parameters:
無
Returns:
無
"""
def testingNB():
listOPosts, classVec = loadDataSet() # 創(chuàng)建實驗樣本
myVocabList = createVocabList(listOPosts) # 創(chuàng)建詞匯表
# 打印中間結(jié)果
print('myVocabList:\n', myVocabList)
trainMat = []
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 將實驗樣本向量化
p0V, p1V, pAb = trainNB(np.array(trainMat), np.array(classVec)) # 訓練樸素貝葉斯分類器
# 打印中間結(jié)果
print('p0V:\n', p0V)
print('p1V:\n', p1V)
print('classVec:\n', classVec)
print('pAb:\n', pAb)
testEntry = ['love', 'my', 'dalmation'] # 測試樣本1
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 測試樣本向量化
if classifyNB(thisDoc, p0V, p1V, pAb):
print(testEntry, '屬于侮辱類') # 執(zhí)行分類并打印分類結(jié)果
else:
print(testEntry, '屬于非侮辱類') # 執(zhí)行分類并打印分類結(jié)果
testEntry = ['stupid', 'garbage'] # 測試樣本2
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 測試樣本向量化
if classifyNB(thisDoc, p0V, p1V, pAb):
print(testEntry, '屬于侮辱類') # 執(zhí)行分類并打印分類結(jié)果
else:
print(testEntry, '屬于非侮辱類') # 執(zhí)行分類并打印分類結(jié)果
if __name__ == "__main__":
testingNB()
運行結(jié)果如下: