一、算法概述
貝葉斯算法是基於統計學的一種概率分類方法,而樸素貝葉斯是其中最簡單的一種;樸素貝葉斯屬於監督學習的算法之一,一般用來解決分類問題,我們之所以稱之爲"樸素",是因爲整個形勢化過程只做最原始、最簡單的假設,即假設數據集所有的樣本之間都是獨立存在,互不影響的。
用一個條件概率公式更好的理解這個假設條件,假設一個樣本中有(a1、a2、a3、… an)共n個樣本,若有P(a1,a2,a3,…,an) = P(a1) * P(a2) * P(a3)…P(an),則稱該數據集中各個樣本之間獨立存在。
假設我們有一個數據集,共有兩個特徵,分別爲三角形和圓形,如下圖所示:
若用P1(x,y)表示數據點(x,y)屬於圓形的概率,用P2(x,y)表示數據點(x,y)屬於三角形的概率,那麼對於一個新數據點(x,y),可以用以下規則判斷其類別:
- 若P1(x,y)>P2(x,y),那麼該點類別爲1
- 若P1(x,y)<P2(x,y),那麼該點類別爲2
總得來說,未知屬性的類別趨向概率高的。這就是貝葉斯決策理論的核心思想,即選擇具有最高概率的決策。
二、條件概率公式
條件概率公式是概率論中十分基礎的一個公式,即在事件B發生的情況下,事件A也發生的概率,如下文氏圖:
通過這幅文氏圖,在在事件B發生的情況下,事件A也發生的概率如下
同理可得
最後推得條件概率的計算公式如下
這個公式被稱爲貝葉斯準則,它告訴我們如何交換條件概率中的條件和結果,例如已知P(B | A),如何計算P(A | B)。
這裏有幾個概念需要了解:
- P(A)稱爲"先驗概率",即在事件B發生之間對事件A發生概率的判斷。
- P(A | B)稱爲"後驗概率",即在事件B發生之後對事件A發生概率的再次判斷。
- P(B | A)/P(B)稱爲"可能性函數",這是一個調整因子,可以幫助預估概率更加接近真實概率。
所以條件概率也可以理解成:
後驗概率 = 先驗概率 * 調整因子
其中"調整因子"的值對條件概率的影響如下:
- 當"調整因子"小於1時,"先驗概率"被減弱,事件A的發生的概率變小
- 當"調整因子"等於1時,"先驗概率"不變,對事件A的發生概率無影響
- 當"調整因子"大於1時,"先驗概率"被增強,事件A的發生的概率變大
三、條件概率實例
再有一年半,偶也要面臨考研or就業的抉擇,向周圍同學詢問了他們的選擇,得到這麼一份小數據集,偶也總結了一下自身條件,學習成績一般、自學能力不錯、家裏的經濟條件也允許,那我是選擇考研還是就業呢?
成績 | 自學能力 | 家庭條件 | 選擇 |
---|---|---|---|
學霸 | 強 | 好 | 考研 |
一般 | 強 | 差 | 考研 |
學渣 | 弱 | 好 | 考研 |
學霸 | 強 | 差 | 就業 |
一般 | 弱 | 好 | 就業 |
學渣 | 弱 | 差 | 就業 |
對於這個例子,按照貝葉斯公式進行求解,可以轉化成P(考研 | 一般 強 好)和P(就業 | 一般 強 好)兩類,因爲貝葉斯的思想就是根據最高概率判斷類別。
"先驗概率"P(考研)很容易計算,但是"可能性函數"中的分母P(一般 強 好)卻不知如何計算,這裏需要引入一個新的公式——全概率公式。
所以依據全概率公式P(一般 強 好)求值公式如下:
最後依據貝葉斯準則可計算二者的概率:
其中考研的概率爲80%,就業的概率爲20%,所以就我自己的條件而言,該算法將我分配至考研黨中。我們從小學就學過一個道理,分母相同的兩個分數,分子大的分數大,因爲樸素貝葉斯的思想是要依據概率判斷類別,所以就可以省去計算全概率這一步,在編寫程序的時候可以提高效率。
四、文本分類
從文本中獲取特徵,需先將文本拆分。這裏的特徵是來自文本的詞條,一個詞條是字符的任意組合。對於文本而言,可以將詞條想象成單詞;對於IP地址而言,又可以將詞條想象成兩個點間的數字組合,不同類型的文本,詞條的類型可以不同。然後將每一個文本片段表示爲一個詞條向量,其中值爲1表示詞條出現在文檔中,0則表示詞條未出現。
平時在刷微博的時候,不管事情好與壞,評論總是有好有壞,因爲避免不了總有槓精的存在。構建一個快速過濾器,這個過濾器的功能就是分類好壞評論,如果某條評論使用了負面或者侮辱性的語言,則將該評論判定爲侮辱類評論,反之則將其歸爲非侮辱類評論,其中侮辱類用1表示,非侮辱類用0表示。
4.1構建詞向量
假設我們已經獲取到文本數據,先考慮出現在文本中的所有單詞,決定將哪些詞納入詞彙表或者說所要的詞彙集合,然後將文本中的句子轉化爲向量,以方便對文本中每句話的類別進行判斷。
#設置文本數據集
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]
return postingList,classVec
loadDataSet函數創建了實驗文本,主要的操作是將每一句話切分成若干個單詞,並且創建了一個類別標籤列表,其中1代表侮辱類,0代表非侮辱類,是通過人的判斷後進行標註。
#創建詞彙表
def createVocabList(dataSet):
#創建一個空的不重複列表
vocabSet = set([])
for document in dataSet:
#取兩者並集
vocabSet = vocabSet | set(document)
return list(vocabSet)
#判斷
def setOfWords2Vec(vocabList, inputSet):
#創建一個元素都爲0的向量
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
#若詞彙表包含該詞彙,則將該位置的0變爲1
returnVec[vocabList.index(word)] = 1
return returnVec
#將所有詞條向量彙總
def get_Mat(inputSet):
trainMat = [] #創建空列表
vocabList = createVocabList(inputSet)
#遍歷輸入文本,將每個詞條向量加入列表中
for Set in inputSet:
returnVec = setOfWords2Vec(vocabList,Set)
trainMat.append(returnVec)
return trainMat
createVocabList函數的作用是通過set方法已經取並集的方式返回一個包含文本中所有出現的不重複詞的集合;setOfWords2Vec函數的輸入參數爲詞彙表和某個文本,輸出的是文本向量,向量的元素包括1或0,分別表示詞彙表中的單詞是否出現在輸入的文本中,思路是首先創建一個同詞彙表等長的向量,並將其元素都設置爲0,然後遍歷輸入文本的單詞,若詞彙表中出現了本文的單詞,則將其對應位置上的0置換爲1。
代碼運行截圖如下
例如詞彙表中第四個單詞has在第一個輸入文本中出現,則向量中的第4個元素置爲1;同理詞彙表中最後一個單詞not在第二個輸入文本中出現,則向量中最後一個元素置爲1。
4.2訓練算法
這裏如果重寫上文提過的貝葉斯準則,W爲一個向量,它由多個數值組成,Ci代表類別,即侮辱類or非侮辱類,公式如下:
若使用上述公式對一個未知類進行判斷,我們只需比較兩個兩個概率值的大小即可,首先通過類別i的文本數除以總文本數可以計算出P(Ci)的數值;然後計算P(W | Ci),因爲W可以展開爲一個個獨立特徵,那麼P(W0,W1,W2…Wn | Ci) = P(W0 | Ci)P(W1 | Ci)P(W2 | Ci)…P(Wn | Ci),簡化了計算的過程。
代碼如下:
def trainNB(trainMatrix,trainCategory):
#訓練文本數量
numTrainDocs = len(trainMatrix)
#每篇文本的詞條數
numWords = len(trainMatrix[0])
#文檔屬於侮辱類(1)的概率
pAbusive = sum(trainCategory)/float(numTrainDocs)
#創建兩個長度爲numWords的零數組
p0Num = np.zeros(numWords)
p1Num = np.zeros(numWords)
#分母初始化
p0Denom = 0.0
p1Denom = 0.0
for i in range(numTrainDocs):
if trainCategory[i] == 1:
#統計侮辱類的條件概率所需的數據,即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
#print(p1Num)
p1Denom += sum(trainMatrix[i])
#print(p1Num)
else:
#統計非侮辱類的條件概率所需的數據,即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
trainNB函數的輸入參數包括文本矩陣trainMatrix和每個詞條的類別標籤所構成的向量trainCategory。首先文本屬於侮辱類的概率只需要將侮辱性詞條的個除以總詞條個數即可;計算P(W | C1)和P(W | C0)時,需要將其分子和分母初始化,遍歷輸入文本時,一旦某個詞語(侮辱性or非侮辱性)在某一文檔中出現,則該詞對應的個數(p1Num或p0Num)就加1,並且在總文本中,該詞條的總次數也相應加1。
代碼運行截圖如下
這裏打印出屬於侮辱性的詞條,當不同詞條出現同一詞語時,會在詞條向量的同一位置上累加,最後每一個詞語出現的次數除以總數就得到了相應的概率,例如出現兩次—>0.105、出現三次—>0.157、出現一次—>0.052。
4.3測試算法
def classifyNB(vec2Classify, p0V,P1V,PAb):
#將對應元素相乘
p1 = reduce(lambda x,y:x*y, vec2Classify * p1V) * pAb
p0 = reduce(lambda x,y:x*y, vec2Classify * p0V) * (1.0 - PAb)
print('p0:',p0)
print('p1:',p1)
if p1 > p0:
return 1
else:
return 0
classifyNB函數傳入的4個參數分別爲測試文本的向量以及訓練函數trainNB返回的三個參數,其作用是將文本的向量與p1V和p2V分別對應相乘,並乘以pAb和其二分類對應概率**(1.0-pAb)**,然後比較p1與p2的大小判別出測試文本屬於屬於哪一類,這裏舉一個reduce方法的小例子,方便理解。
reduce(lambda x,y:x+y,[1,2,3,4])
'''
10
'''
reduce方法是將兩個元素以某種操作運算符爲條件歸併成一個結果,並且它是一個迭代的過程,每次調用該方法直至得到最後一個結果,例如上面數組[1,2,3,4]以加法爲操作運算實現1+2;3+3;6+4 = 10的操作過程。
下面通過調用前文的函數,對測試數據進行分類操作,代碼如下:
def testingNB(testVec):
#創建實驗樣本
postingList,classVec = loadDataSet()
#創建詞彙表
vocabSet = createVocabList(postingList)
#將實驗樣本向量彙總
trainMat = get_Mat(postingList)
#訓練算法
p0V,P1V,PAb = trainNB(trainMat,classVec)
#將測試文本向量化
The_test = setOfWords2Vec(vocabSet,testVec)
#判斷類別
if classifyNB(The_test,p0V,P1V,PAb):
print(testVec,"侮辱類")
else:
print(testVec,"非侮辱類")
傳入測試數據testVec,並返回分類結果如下圖:
哎呀,這stupid怎麼還能被判斷成非侮辱類了呢?會不會是程序變蠢了?程序是正常的,但是需要對程序做一點改進,我們都知道0是一個特別牛皮的數,因爲不論什麼數字乘以0結果都得0,所以只要p1V向量和測試向量有一個對應位置上同時都爲0,那麼最終結果一定爲0。爲了降低上述影響,可以將所有詞的出現數初始化爲1,並將分母初始化爲2,這種方法被稱爲拉普拉斯平滑。
這部分對trainNB函數做以下更改:
p0Num = np.ones(numWords)
p1Num = np.ones(numWords)
p0Denom = 2.0
p1Denom = 2.0
除此之外,還有一個問題是下溢出,什麼是下溢出呢?在許多很小的數相乘時,當計算乘積 **P(W0 | Ci)P(W1 | Ci)P(W2 | Ci)…P(Wn | Ci)**時,由於大部分因子都非常小,所以程序會下溢出或者得不到正確答案,比如程序會將乘積非常小的結果四捨五入後得到0,一種經典的解決辦法是取乘積的自然對數。
在代數中有ln(a*b) = ln(a)+ln(b),由乘法轉爲加法後,就可以避免下溢出或者浮點數舍入導致的錯誤,有人可能會擔心,二者計算出的結果是有差異的,這是事實,但是對於我們所需要的分類結果是無影響的。
f(x)與ln(x)的曲線如下圖:
通過觀察這兩條曲線會發現它們在相同的區域同時增加或同時減少,並且在相同點取到極值,雖然二者的極值不同,但不影響最終結果,因爲我們只需通過比較二者值的大小來判斷測試數據的類別。
這部分對trainNB函數做以下更改:
p1Vect = np.log(p1Num/p1Denom)
p0Vect = np.log(p0Num/p0Denom)
前面計算概率時做了取對數操作,由於log(a*b) = log(a)+log(b),所以可以對classifyNB函數進行改進,用sum方法代替reduce方法即可。
具體代碼如下:
def classifyNB(ClassifyVec, p0V,p1V,pAb):
#p1 = reduce(lambda x,y:x*y, ClassifyVec * p1V) * pAb
#p0 = reduce(lambda x,y:x*y, ClassifyVec * p0V) * (1.0 - PAb)
#將對應元素相乘
p1 = sum(ClassifyVec * p1V) + np.log(pAb)
p0 = sum(ClassifyVec * p0V) + np.log(1.0 - pAb)
print('p0:',p0)
print('p1:',p1)
if p1 > p0:
return 1
else:
return 0
最後測試整體代碼運行截圖如下:
通過p0與p1的比較,可以正確的將測試文本進行分類,stupid最後被判定爲侮辱類,看來程序是不會變蠢的,會變蠢的是我。
4.4詞袋模型拓展
前面程序中,我們將每個次的出現與否作爲一個特徵,這可以被描述爲詞集模型。如果一個詞在文本中出現不止一次,不能將其單純的作爲特徵同等看待,因爲其中涉及到了權重不同,這種方法被稱爲詞袋模型。在詞袋中,每個單詞可以出現若干次,而在詞集中,每個詞只能出現一次。可以在詞集模型的基礎上加以修改,將其轉換成詞袋模型。
代碼如下:
def setOfWords2Vec(vocabList, inputSet):
#創建一個元素都爲0的向量
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
#若每當文本中出現詞彙表中的單詞一次,就將該位置的數字加1
returnVec[vocabList.index(word)] += 1
return returnVec
五、文末總結
樸素貝葉斯對應優點如下:
- 可以處理樣本較少的數據集
- 可以處理多類別問題
- 對缺失數據不太敏感
- 適合進行文本分類
樸素貝葉斯對應缺點如下:
- 對於輸入數據的表達方式敏感
- 需要假設數據中每個特徵之間需要獨立
- 先驗模型建立不當可能導致預測結果不佳
本文就樸素貝葉斯該算法的原理進行簡單介紹,下篇文章會介紹樸素貝葉斯的應用實例。
關注公衆號【奶糖貓】後臺回覆“Bayes”可獲取源碼供參考,感謝閱讀。