1. 程式人生 > >機器學習實戰筆記3—樸素貝葉斯

機器學習實戰筆記3—樸素貝葉斯

注:此係列文章裡的部分演算法和深度學習筆記系列裡的內容有重合的地方,深度學習筆記裡是看教學視訊做的筆記,此處文章是看《機器學習實戰》這本書所做的筆記,雖然演算法相同,但示例程式碼有所不同,多敲一遍沒有壞處,哈哈。(裡面用到的資料集、程式碼可以到網上搜索,很容易找到。)。Python版本3.6

機器學習十大算法系列文章:

機器學習實戰筆記1—k-近鄰演算法

機器學習實戰筆記2—決策樹

機器學習實戰筆記3—樸素貝葉斯

機器學習實戰筆記4—Logistic迴歸

機器學習實戰筆記5—支援向量機

機器學習實戰筆記6—AdaBoost

機器學習實戰筆記7—K-Means

此係列原始碼在我的GitHub裡:
https://github.com/yeyujujishou19/Machine-Learning-In-Action-Codes

一,演算法原理:

1)貝葉斯公式:

formula

a)正向思維:

       什麼是正向思維?有一個很典型的例子就是,有一個黑箱子,我們已經知道里面有N個白球和M個黑球,那麼我們就能知道閉著眼睛隨機抽出來的球的概率。正向思維就是我們只有知道了所有可能發生的情況,才能推斷出每個情況發生的概率。   

b)倒向思維:

       倒向思維是,如果我們事先並不知道黑箱裡面黑白球的比例,而是閉著眼睛摸出一個或好幾個球,觀察這些取出來的球的顏色之後,就此對黑箱裡面的黑白球的比例進行推測,但是推測不出箱子裡面球的數量。通俗地講,就是通過一些已知的概率可以求一些未知的概率。貝葉斯公式就是用倒向思維來求條件概率的。

c)條件概率:

       先看條件概率的符號表示:P(B|A),意思是在事件A發生的條件下,事件B發生的概率。剛才說貝葉斯公式是求條件概率的,那是時候要亮出貝葉斯公式了:
                                                        P(B|A) = P(AB) / P(A)


       我們先來理解 P(B|A) 究竟是個什麼鬼,P(B|A) 是在事件A發生的條件下,事件B發生的概率。有人還是很難理解,我們換個說法,P(B|A) 在公式裡面的意思是 P(AB) 佔 P(A) 的比重。來看下面的文氏圖來輔助理解 P(AB) 佔 P(A) 的比重是什麼意思。

d)全概率:

      公式表示若事件A1,A2,…,An構成一個完備事件組且都有正概率,則對任意一個事件B都有公式成立。            

formula

 

舉例:以吃雞遊戲玩家小明同學玩了100局遊戲為例(其中涉及“開黑”術語是和朋友一起玩遊戲的意思)。

                                                                                     玩家小明勝利情況

玩家 總遊戲局數 總勝利局數 開黑局數 開黑且勝利的局數
小明 100 80 34 24

 正向思維:

 設A事件為玩家勝利:                                                 P(A) = 80/100 = 0.8

 設B事件為玩家開黑:                                                 P(B) = 34/100 = 0.34

 設AB事件為玩家開黑且勝利:                                    P(AB) = 24/100 = 0.24

 條件概率:

 設C事件為如果玩家勝利,此勝利局為開黑局:         P(C) = P(B|A) = P(AB) / P(A) = 0.24/0.8 = 0.3

 設D事件為如果玩家開黑,此開黑局為勝利局:         P(D) = P(A|B) = P(AB) / P(B) = 0.24/0.34 = 0.7

倒向思維:

                                                                                    玩家小明勝利情況

玩家 總勝率 勝利局中為開黑局的可能性 失敗局中為開黑局的可能性
小明 0.8 0.3 0.5

我們要做的是,根據表格中的已知的概率,來求玩家在開黑局中勝利的概率。

A事件為玩家勝利:

P(A) = 0.8
P(~A) = 1 - P(A) = 1-0.8 = 0.2

B事件為玩家開黑,根據已知條件概率

勝利局中為開黑局的可能性: P(B|A) = 0.3
失敗局中為開黑局的可能性: P(B|~A) = 0.5

根據全概率公式求玩家玩遊戲會開黑的概率

P(B) = P(B|A) * P(A) + P(B|~A) * P(~A)
        = 0.3*0.8 + 0.5*0.2
        = 0.34

C事件為玩家在開黑局中取得勝利

(計算過程中將貝葉斯公式展開,其中分母由全概率公式求得)

P(C) = P(A|B) = P(AB) / P(B)
        = P(B|A) * P(A) / ( P(B|A) * P(A) + P(B|~A) * P(~A) )
        = 0.71

 

2)樸素貝葉斯:

樸素貝葉斯演算法,樸素:特徵條件獨立;貝葉斯:基於貝葉斯定理。樸素貝葉斯中的樸素一詞的來源就是假設各特徵之間相互獨立。這一假設使得樸素貝葉斯演算法變得簡單,但有時會犧牲一定的分類準確率。

那麼既然是樸素貝葉斯分類演算法,它的核心演算法又是什麼呢?是下面這個貝葉斯公式:

 換個表達形式就會明朗很多,如下:

我們最終求的p(類別|特徵)即可!就相當於完成了我們的任務。

 

舉例:病人分類的例子,某個醫院早上收了六個門診病人,如下表:

症狀 職業 疾病
打噴嚏 護士 感冒
打噴嚏 農夫 過敏
頭痛 建築工人 腦震盪 
頭痛 建築工人 感冒
打噴嚏 教師 感冒
頭痛 教師

腦震盪 

現在又來了第七個病人,是一個打噴嚏的建築工人。請問他患上感冒的概率有多大? 根據貝葉斯定理:

P(A|B) = P(B|A) P(A) / P(B)

可得:

P(感冒|打噴嚏x建築工人) = P(打噴嚏x建築工人|感冒) x P(感冒) / P(打噴嚏x建築工人)

假定”打噴嚏”和”建築工人”這兩個特徵是獨立的,因此,上面的等式就變成了:

P(感冒|打噴嚏x建築工人) = P(打噴嚏|感冒) x P(建築工人|感冒) x P(感冒) / P(打噴嚏) x P(建築工人)

這是可以計算的:

P(感冒|打噴嚏x建築工人) = 0.66 x 0.33 x 0.5 / 0.5 x 0.33 = 0.66

因此,這個打噴嚏的建築工人,有66%的概率是得了感冒。同理,可以計算這個病人患上過敏或腦震盪的概率。比較這幾個概率,就可以知道他最可能得什麼病。這就是貝葉斯分類器的基本方法:在統計資料的基礎上,依據某些特徵,計算各個類別的概率,從而實現分類。
 

二,演算法的優缺點:

優點:

   (1)在資料較少的情況下仍然有效,可以處理多類別問題。

   (2)演算法邏輯簡單,易於實現(演算法思路很簡單,只要使用貝葉斯公式轉化即可!)

   (3)分類過程中時空開銷小(假設特徵相互獨立,只會涉及到二維儲存)

缺點:

   (1)對於輸入資料的準備方式較為敏感。

   (2)樸素貝葉斯假設屬性之間相互獨立,這種假設在實際過程中往往是不成立的。在屬性之間相關性越大,分類誤差也就越大。

適用資料型別:標稱型資料。

 

三,例項程式碼:使用Python進行文字分類

1)準備資料,從文字構建詞向量

從文字轉換成不重複的單詞,再從單詞轉換成數字。

將單詞分為侮辱性和非侮辱性的,侮辱性單詞標記為1,非侮辱性的標記為0。

#載入資料集
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]    #1代表侮辱性文字,0代表正常言論
    return postingList,classVec

 #建立一個包含在所有文件中出現的不重複詞的列表
def createVocabList(dataSet):
    vocabSet = set([])  #建立一個空集,將詞條列表輸給set建構函式,set就會返回一個不重複詞表
    for document in dataSet:
        vocabSet = vocabSet | set(document) #建立兩個集合的並集,操作符
    return list(vocabSet)

#vocabList 詞彙表
#inputSet 某個文件
#函式輸出是文件向量,向量的每一元素為1或0,分別表示詞彙表中的單詞在輸入文件中是否出現
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0]*len(vocabList) #先建立一個和詞彙表等長的向量,並將其元素都設定為0
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] = 1 #如果詞彙表中的單詞在文件中,將相應位置置1
        else: print ("the word: %s is not in my Vocabulary!" % word)  #Vocabulary詞彙
    return returnVec

#測試
listOPosts,listClasses=loadDataSet()  #載入資料
myVocablist=createVocabList(listOPosts) #建立詞彙表
print(myVocablist)  #列印詞彙表
returnVec=setOfWords2Vec(myVocablist,listOPosts[3]) #檢查詞彙表中的單詞在文件中是否出現
print(returnVec)

程式碼結果:

 

 

2)訓練演算法:從詞向量計算概率

       上面程式碼的功能是如何將一組單詞轉換為一組數字,接下來看看如何使用這些數字計算概率。現在已經知道一個詞是否會出現在一篇文件中,也知道該文件所屬的類別。現在將使用貝葉斯公式,對每個類計算該值,然後比較這兩個概率值得大小。

        計算思想是:先通過類別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)來計算上述概率,這就極大簡化了計算的過程。

下面函式的功能是:

a)計算侮辱性文章佔總文章的比例

b)計算侮辱性文章中各個單詞佔侮辱性文章總單詞的比例

c)計算非侮辱性文章中各個單詞佔非侮辱性文章總單詞的比例

#樸素貝葉斯分類器訓練函式
#trainMatrix    共n個數組,n為文章個數,每個陣列是每一篇文章對應的詞彙表陣列 [0,0,1,0,1...0,0]
#trainCategory  #n個文章的類別
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)  #總文件數
    numWords = len(trainMatrix[0])   #詞彙表長度
    pAbusive = sum(trainCategory)/float(numTrainDocs)   #sum(trainCategory)計算為1的數目,計算侮辱性文章佔總文章的比例
    p0Num = ones(numWords); p1Num = ones(numWords)      #初始化為1,p0Num 是非侮辱性文章各個單詞出現數目,p1Num 是侮辱性文章各個單詞出現數目
    p0Denom = 2.0; p1Denom = 2.0                        #初始化為2,p0Denom是非侮辱性文章單詞總數,p1Denom是侮辱性文章單詞總數
    for i in range(numTrainDocs):                       #總文件數
        if trainCategory[i] == 1:                       #第i篇文章是侮辱性的
            p1Num += trainMatrix[i]                     #統計侮辱性文章各個單詞出現情況
            p1Denom += sum(trainMatrix[i])              #統計侮辱性文章單詞總數
        else:                                           #不是侮辱性的文章
            p0Num += trainMatrix[i]                     #統計非侮辱性文章各個單詞出現情況
            p0Denom += sum(trainMatrix[i])              #統計非侮辱性文章單詞總數
    p1Vect = p1Num/p1Denom                              #侮辱性文章各個單詞出現比例
    p0Vect = p0Num/p0Denom                              #非侮辱性文章各個單詞出現比例
    return p0Vect,p1Vect,pAbusive

#測試
listOPosts,listClasses=loadDataSet()    #載入資料
myVocablist=createVocabList(listOPosts) #建立詞彙表,所有文章裡不重複的單詞表
trainMat=[]
for postinDoc in listOPosts: #一次讀取每一篇文章
    trainMat.append(setOfWords2Vec(myVocablist,postinDoc)) #先將每篇文章轉換成數字陣列後加入trainMat
p0V,p1V,pAb=trainNB0(trainMat,listClasses) #計算各種比例
print("pAb:",pAb)
print("p0V:",p0V)
print("p1V:",p1V)

 程式碼結果:

  現在已經準好構建完整的分類器了

3)測試演算法:根據實際情況修改分類器

    a)利用貝葉斯分類器對文件進行分類時,要計算多個概率的乘積以獲得文件屬於某個類別的概率,即計算p(w0|1)p(w1|1)p(w2|1)。如果其中一個概率值為0,那麼最後的乘積也為0.為了降低這種影響,可以將所有詞的出現數初始化為1,並將分母初始化為2。

    b) 另一個遇到的問題是下溢,這是由於太多很小的數相乘造成的。當計算乘積p(w0|ci)p(w1|ci)p(w2|ci)...p(wn|ci)時,由於大部分銀子都非常小,所以程式會下溢位或者得不到正確的答案。一種解決辦法是對乘積取自然對數。在代數中有ln(a*b)=ln(a)+ln(b),於是通過求對數可以避免下溢位或者浮點數舍入導致的錯誤。同時,採用自然對數進行處理不會有任何損失。

     將上述程式碼中,最後改為取對數。

def trainNB0(trainMatrix,trainCategory):
 ...
 ...

 p1Vect = log(p1Num/p1Denom)                              #侮辱性文章各個單詞出現比例,然後取對數
 p0Vect = log(p0Num/p0Denom)                              #非侮辱性文章各個單詞出現比例,然後取對數
 ...

  現在已經準好構建完整的分類器了,測試一下效果: 

#樸素貝葉斯分類函式
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
    p1 = sum(vec2Classify * p1Vec) + log(pClass1)    #element-wise mult
    p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)
    if p1 > p0:
        return 1
    else: 
        return 0

#測試函式
def testingNB():
    listOPosts,listClasses = loadDataSet()     #載入資料
    myVocabList = createVocabList(listOPosts)  #建立詞彙表,所有文章中不重複的單詞表
    trainMat=[]
    for postinDoc in listOPosts: #一次讀取每一篇文章
        trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) #先將每篇文章轉換成數字陣列後加入trainMat
    p0V,p1V,pAb = trainNB0(array(trainMat),array(listClasses))  #計算各種比例
    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))  #列印測試結果

testingNB()

 程式碼結果:

4)準備資料:文件詞袋模型

    目前為止,我們將每個詞的出現與否作為一個特徵,這可以被描述為詞集模型(set-of-words model)。如果一個詞在文件中出現不止一次,這可能意味著該詞是否出現在文件中不能表達某種資訊,這種方法被稱為詞袋模型(bag-of-words model)。在詞袋中,每個單詞可以出現多次,而在詞集中,每個詞只能出現一次。為了適應詞袋模型,需要對函式setOfWords2Vec()稍加修改,改為bagOfWords2Vec()。

    下面的程式給出了基於詞袋模型的樸素貝葉斯程式碼。它與setOfWords2Vec()幾乎完全相同,唯一不同的是每當遇到一個單詞時,它會增加詞向量中對應的值,而不只是將對應的數值設為1。

#樸素貝葉斯詞袋模型
def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0]*len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

 現在分類器已經構建好了,下面我們利用該分類器來過濾垃圾郵件。

四,例項程式碼二:使用樸素貝葉斯過濾垃圾郵件

    在前面那個簡單的例子中,我們引入了字元創列表。使用樸素貝葉斯解決一些現實生活中的問題時,需要先從文字內容得到字元創列表,然後生成詞向量。下面這個例子中,我們將瞭解樸素貝葉斯的一個最著名的應用:電子郵件垃圾過濾。

1)準備資料:切分文字

按空格切分字元

mySent='This book is the best book on Python or M.L. I have ever laid eyes upon.'
rst=mySent.split()
print(rst)

 程式碼結果:

 

 使用正則表示式來切分句子,其中分隔符是除單詞、數字外的任意字元創。

import re
mySent='This book is the best book on Python or M.L. I have ever laid eyes upon.'
regEx=re.compile('\\W*')
listOfTokens=regEx.split(mySent)
print(listOfTokens)

 程式碼結果:

 去除空格,並將大寫字母改成小寫

import re
mySent='This book is the best book on Python or M.L. I have ever laid eyes upon.'
regEx=re.compile('\\W*')
listOfTokens=regEx.split(mySent)
rst=[tok.lower() for tok in listOfTokens if len(tok)>0]
print(rst)

 

2)測試演算法:使用樸素貝葉斯進行交叉驗證

#函式功能,接受一個大字串並將其解析為字串列表
#該函式去掉少於兩個字元的字串,並將所有字串轉換為小寫。
def textParse(bigString):    #input is big string, #output is word list
    import re
    listOfTokens = re.split(r'\W*', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2] 

 #使用貝葉斯分類器對郵件進行自動化處理
def spamTest():
    docList=[]; classList = []; fullText =[]
    for i in range(1,26):
        wordList = textParse(open('email/spam/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        wordList = textParse(open('email/ham/%d.txt' % i).read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)#create vocabulary
    trainingSet = list(range(50)); testSet=[]           #create test set
    for i in range(10):
        randIndex = int(random.uniform(0,len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del(trainingSet[randIndex])  
    trainMat=[]; trainClasses = []
    for docIndex in trainingSet:#train the classifier (get probs) trainNB0
        trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses))
    errorCount = 0
    for docIndex in testSet:        #classify the remaining items
        wordVector = bagOfWords2VecMN(vocabList, docList[docIndex])
        if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]:
            errorCount += 1
            print ("classification error",docList[docIndex])
    print ('the error rate is: ',float(errorCount)/len(testSet))
    #return vocabList,fullText

spamTest()

 程式碼結果:

 

歡迎掃碼關注我的微信公眾號