機器學(xué)習(xí)之樸素貝葉斯算法

貝葉斯簡介

貝葉斯定理是一個特別簡單但是特別有用的定理,這個定理解決了生活中經(jīng)常遇到的一個問題:在已知P(A|B)的情況下如何計算P(B|A),既交換條件與結(jié)果。樸素貝葉斯是在假設(shè)各個條件完全獨立的情況下對貝葉斯定理的一種簡化算法。在機器學(xué)習(xí)中,樸素貝葉斯是一種簡單但是非常強大的線性分類器,它在垃圾郵件分類,疾病診斷中都取得了很大的成功。

以下摘一段 wikipedia 上的簡介:

所謂的貝葉斯方法源于他生前為解決一個“逆概”問題寫的一篇文章,而這篇文章是在他死后才由他的一位朋友發(fā)表出來的。在貝葉斯寫這篇文章之前,人們已經(jīng)能夠計算“正向概率”,如“假設(shè)袋子里面有N個白球,M個黑球,你伸手進去摸一把,摸出黑球的概率是多大”。而一個自然而然的問題是反過來:“如果我們事先并不知道袋子里面黑白球的比例,而是閉著眼睛摸出一個(或好幾個)球,觀察這些取出來的球的顏色之后,那么我們可以就此對袋子里面的黑白球的比例作出什么樣的推測”。這個問題,就是所謂的逆概問題。

實際上,貝葉斯當(dāng)時的論文只是對這個問題的一個直接的求解嘗試,并不清楚他當(dāng)時是不是已經(jīng)意識到這里面包含著的深刻的思想。然而后來,貝葉斯方法席卷了概率論,并將應(yīng)用延伸到各個問題領(lǐng)域,所有需要作出概率預(yù)測的地方都可以見到貝葉斯方法的影子,特別地,貝葉斯是機器學(xué)習(xí)的核心方法之一。這背后的深刻原因在于,現(xiàn)實世界本身就是不確定的,人類的觀察能力是有局限性的(否則有很大一部分科學(xué)就沒有必要做了——設(shè)想我們能夠直接觀察到電子的運行,還需要對原子模型爭吵不休嗎?),我們?nèi)粘K^察到的只是事物表面上的結(jié)果,沿用剛才那個袋子里面取球的比方,我們往往只能知道從里面取出來的球是什么顏色,而并不能直接看到袋子里面實際的情況。這個時候,我們就需要提供一個猜測,所謂猜測,當(dāng)然就是不確定的,但也絕對不是兩眼一抹黑瞎蒙——具體地說,我們需要做兩件事情:1. 算出各種不同猜測的可能性大小。2. 算出最靠譜的猜測是什么。第一個就是計算特定猜測的后驗概率,對于連續(xù)的猜測空間則是計算猜測的概率密度函數(shù)。第二個則是所謂的模型比較,模型比較如果不考慮先驗概率的話就是最大似然方法。

貝葉斯公式

貝葉斯公式是怎么來的?我們還是使用 wikipedia 上的一個例子:

一所學(xué)校里面有 60% 的男生,40% 的女生。男生總是穿長褲,女生則一半穿長褲一半穿裙子。有了這些信息之后我們可以容易地計算“隨機選取一個學(xué)生,他(她)穿長褲的概率和穿裙子的概率是多大”,這個就是前面說的“正向概率”的計算。然而,假設(shè)你走在校園中,迎面走來一個穿長褲的學(xué)生(很不幸的是你高度近似,你只看得見他(她)穿的是否長褲,而無法確定他(她)的性別),你能夠推斷出他(她)是男生的概率是多大嗎?

我們來算一算:假設(shè)學(xué)校里面人的總數(shù)是 U 個。60% 的男生都穿長褲,于是我們得到了 U * P(Boy) * P(Pants|Boy) 個穿長褲的(男生)(其中 P(Boy) 是男生的概率 = 60%,這里可以簡單的理解為男生的比例;P(Pants|Boy) 是條件概率,即在 Boy 這個條件下穿長褲的概率是多大,這里是 100% ,因為所有男生都穿長褲)。40% 的女生里面又有一半(50%)是穿長褲的,于是我們又得到了 U * P(Girl) * P(Pants|Girl) 個穿長褲的(女生)。加起來一共是 U * P(Boy) * P(Pants|Boy) + U * P(Girl) * P(Pants|Girl) 個穿長褲的,其中有 U * P(Girl) * P(Pants|Girl) 個女生。兩者一比就是你要求的答案。

下面我們把這個答案形式化一下:我們要求的是 P(Girl|Pants),我們計算的結(jié)果是 U * P(Girl) * P(Pants|Girl) / [U * P(Boy) * P(Pants|Boy) + U * P(Girl) * P(Pants|Girl)] 。容易發(fā)現(xiàn)這里校園內(nèi)人的總數(shù)是無關(guān)的,可以消去。于是得到

P(Girl|Pants) = P(Girl) * P(Pants|Girl) / [P(Boy) * P(Pants|Boy) + P(Girl) * P(Pants|Girl)]

把上式收縮起來,分母其實就是 P(Pants) ,分子其實就是 P(Pants, Girl) 。而這個比例很自然地就讀作:在穿長褲的人( P(Pants) )里面有多少(穿長褲)的女孩( P(Pants, Girl) )。

上式中的 Pants 和 Boy/Girl 可以指代一切東西,所以其一般形式就是:

P(B|A) = P(A|B) * P(B) / [P(A|B) * P(B) + P(A|~B) * P(~B) ]

收縮起來就是:

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

其實這個就等于:

P(B|A) * P(A) = P(AB)

樸素貝葉斯

樸素貝葉斯就是在貝葉斯的基礎(chǔ)上引入了條件獨立假設(shè)。
對于貝葉斯公式P(B|A) = P(AB) / P(A),
從機器學(xué)習(xí)的角度來理解,我們把A理解成‘具有某些特征’,把B理解成‘類別標(biāo)簽’,貝葉斯方法是把計算具有某些特征的條件下屬于某類別的概率轉(zhuǎn)換成計算屬于某類別的條件下具有某些特征。

假設(shè)某個體有n項特征(Feature),分別為F1、F2、...、Fn,當(dāng)個體具有不同特征時屬于不同的類別,即
P(C|F1F2...Fn) = P(F1F2...Fn|C)P(C) / P(F1F2...Fn)。
現(xiàn)在假設(shè)特征F1,F2...Fn之間是相互獨立的,就得到P(F1F2...Fn|C)P(C) = P(F1|C)P(F2|C) ... P(Fn|C)P(C),等式右邊每一項都可以從統(tǒng)計資料中獲得,由此就可以計算了具有某些特征條件下屬于某個類別的概率。

用一個例子來理解機器學(xué)習(xí)中貝葉斯相關(guān)的幾個概念

對于一封郵件“我司可辦理正規(guī)發(fā)票”,該郵件是垃圾郵件的概率有多少。

P(屬于某個分類|具有某種特征) = P(具有某種特征|屬于某個分類)/P(具有某種特征)

貝葉斯

P(S|我司可辦理正規(guī)發(fā)票) = P(我司可辦理正規(guī)發(fā)票|S)/P(我司可辦理正規(guī)發(fā)票)

樸素貝葉斯

假設(shè)“我司可辦理正規(guī)發(fā)票”這幾個詞在郵件中的出現(xiàn)是相互獨立的,則
P(S|我司可辦理正規(guī)發(fā)票) = P(我|S)P(司|S)P(可|S)P(辦理|S)P(正規(guī)|S)P(發(fā)票|S)

三種模型

  • 多項式模型:考慮一個詞重復(fù)出現(xiàn),統(tǒng)計多次
  • 伯努利模型:不考慮一個詞重復(fù)出現(xiàn),只統(tǒng)計一次
  • 混合模型:訓(xùn)練時考慮一個詞重復(fù)出現(xiàn),預(yù)測時不考慮

平滑技術(shù)

如果訓(xùn)練集中沒有“正規(guī)發(fā)票”這個詞,測試集中有,那么預(yù)測時P(正規(guī)|S)P(發(fā)票|S)=0,于是整個概率都成0,這就需要平滑技術(shù)來處理。
這種情況很常見,因為哪怕訓(xùn)練集再大,也可能有未覆蓋的詞語。這本質(zhì)上還是樣本數(shù)量太少,不滿足大數(shù)定律,計算出來的概率失真。平滑技術(shù)就是為了處理這樣的問題。

對于伯努利模型的一種平滑算法是:

P(正規(guī)發(fā)票|S)=(出現(xiàn)'正規(guī)發(fā)票'的垃圾郵件的數(shù)量+1)/ 每封垃圾郵件中所有詞出現(xiàn)的次數(shù)(不考慮重復(fù)出現(xiàn)的詞)和+2

機器學(xué)習(xí)中樸素貝葉斯分類器的構(gòu)建方法

計算每個類別中的文檔數(shù)目
對每篇訓(xùn)練文檔:
    對每個類別:
        如果詞條出現(xiàn)文檔中―增加該詞條的計數(shù)值
        增加所有詞條的計數(shù)值
    對每個類別:
        對每個詞條:
            將該詞條的數(shù)目除以總詞條數(shù)目得到條件概率
    返回每個類別的條件概率

代碼實現(xiàn)

用樸素貝葉斯算法實現(xiàn)一個情分分析的分類器

def load_data(self, pos_file, neg_file):
    """載入數(shù)據(jù)"""
    neg_docs = codecs.open(neg_file, 'r', 'utf-8').readlines()
    pos_docs = codecs.open(pos_file, 'r', 'utf-8').readlines()
    return pos_docs, neg_docs
def handle_data(self, sentence, stop_words='data/stop_words.txt'):
    """進行分詞,去除停止詞"""
    stop_words = [word.strip() for word in open(stop_words, 'r')]
    return [word.strip() for word in jieba.cut(sentence.strip()) if word and word not in stop_words]
def create_vocal_list(self, sentence_list):
    """創(chuàng)建詞匯表,詞匯表包含了所有訓(xùn)練樣本中出現(xiàn)的詞(不包含停止詞)"""
    vocab_list = set([])
    for sentence in sentence_list:
        vocab_list = vocab_list | set(sentence)
    return list(vocab_list)
def word_to_vec(self, vocab_list, sentence):
    """把一組詞轉(zhuǎn)換成詞向量,詞向量的長度等于詞匯表的長度"""
    words_vec = [0]*len(vocab_list)
    for word in sentence:
        if word in vocab_list:
            words_vec[vocab_list.index(word)] = 1 # 伯努利模型,不考慮重復(fù)出現(xiàn)的詞,對于一條數(shù)據(jù)中的每個詞,如果改詞存在于詞匯表中,則把詞向量對應(yīng)位置設(shè)為1
#             words_vec[vocab_list.index(word)] += 1 # 多項式模型,考慮重復(fù)出現(xiàn)的詞
    return words_vec
def train(self, train_data, label):
    num_sentence = len(train_data)
    pos_each_word = np.ones(len(train_data[0])) # 統(tǒng)計正向文本中每個詞出現(xiàn)的次數(shù),默認每個詞出現(xiàn)至少一次
    neg_each_word = np.ones(len(train_data[0])) # 統(tǒng)計負向文本中每個詞出現(xiàn)的次數(shù),默認每個詞出現(xiàn)至少一次
    pos_words = 2.0 # 正向文本中所有詞的總數(shù)
    neg_words = 2.0 # 負向文本中所有詞的總數(shù)
    for i in range(num_sentence):
        if label[i] == 1:
            pos_each_word += train_data[i]
            pos_words += sum(train_data[i])
        if label[i] == 0:
            neg_each_word += train_data[i]
            neg_words += sum(train_data[i])
    # 對每個詞出現(xiàn)的概率取對數(shù),這是因為大多數(shù)概率都很小,如果再對很多很小的數(shù)進行乘法操作,會下溢或得不得正確答案。
    # 比如a=0.001, b=0.0003, a*b = exp(ln(a)+ln(b))
    pos_each_word_prob = np.log(pos_each_word/pos_words) 
    neg_each_word_prob = np.log(neg_each_word/neg_words)
    print(pos_each_word_prob, neg_each_word_prob, sum(label)/num_sentence)
    return pos_each_word_prob, neg_each_word_prob, sum(label)/num_sentence

def classify(self, test_data, pos_each_word_prob, neg_each_word_prob, pos_prob):
    p1 = sum(test_data*pos_each_word_prob) + np.log(pos_prob)
    p0 = sum(test_data*neg_each_word_prob) + np.log(1-pos_prob)
    if p1 > p0:
        return 1, np.exp(p1), np.exp(p0)
    else:
        return 0, np.exp(p1), np.exp(p0)

這里對pos_each_word = np.ones(len(train_data[0])),p1 = sum(test_data*pos_each_word_prob) + np.log(pos_prob)這幾個地方簡單解釋下:

假如詞匯表vocal=[.....,我,......愛......],其中包含了“我愛”兩個詞
正向樣本中某條數(shù)據(jù)中有“我愛”,而負樣本中沒有“愛”這個詞。如果每個詞的出現(xiàn)次數(shù)不加一的話,則
正樣本中每個詞出現(xiàn)的概率為P正=[.......p(我)>0,........p(愛)>0,.......]
負樣本中每個詞出現(xiàn)的概率為P負=[.......p(我)>0,........p(愛)=0,.......]
現(xiàn)在來一個測試數(shù)據(jù)“我愛學(xué)習(xí)”,轉(zhuǎn)換為詞向量為test=[0,0,.....1(我),0,0.......1(愛),0,0.......]

計算“我愛學(xué)習(xí)”屬于正樣本的概率就是計算"我愛學(xué)習(xí)"中每個詞在正樣本中出現(xiàn)的概率的乘積,由于概率值都是很小的數(shù),如果再對多個很小的數(shù)進行乘法操作,可能會出現(xiàn)下溢或得不到正確結(jié)果。解決方法就是對每個詞的概率值取對數(shù),a*b=exp(lna+lnb),這樣就可以避免很多乘法操作

P(正|我愛學(xué)習(xí)) = P(正|我)P(正|愛)P(正|學(xué)習(xí))=test*ln(P正)=[0,0,.....1(我),0,0.......1(愛),0,0.......]*[.......ln(p(我)),........ln(p(愛)),.......]=ln(p(我))+ln(p(愛))。

當(dāng)計算“我愛學(xué)習(xí)”是負樣本的概率時,因為負樣本中沒有“愛”,所以“我愛學(xué)習(xí)”屬于負樣本的概率就是0,這顯然不合理,所以需要進行調(diào)整,讓訓(xùn)練集中正負樣本中每個詞的出現(xiàn)次數(shù)=出現(xiàn)次數(shù)+1,保證最少出現(xiàn)一次。

如果測試數(shù)據(jù)中某個詞在詞匯表中不存在,比如“學(xué)習(xí)”就不存在詞匯表中,當(dāng)把“我愛學(xué)習(xí)”轉(zhuǎn)換為詞向量的時候,詞向量中只有“我”和“愛”對應(yīng)的位置為1,直接忽略了“學(xué)習(xí)”,也就不會出現(xiàn)“學(xué)習(xí)”出現(xiàn)的概率為0這個問題了。當(dāng)然,這樣計算的結(jié)果不是太準(zhǔn)確,但是也不可能那么準(zhǔn)確,因為訓(xùn)練樣本不可能包含了所有詞。

def predict(self, test_data):
    pos_sentence_list, neg_sentence_list = self.load_data(pos_data, neg_data)
    sentences = []
    # 把正負樣本集合到一起
    for sentence in pos_sentence_list:
        sentence = self.handle_data(sentence)
        sentences.append(sentence)
    for sentence in neg_sentence_list:
        sentence = self.handle_data(sentence)
        sentences.append(sentence)
    label = [1] * len(pos_sentence_list) + [0] * len(neg_sentence_list)  # 每個正負樣本對應(yīng)的類別    
    vocab_list = self.create_vocal_list(sentences)
    train_data = []
    for sentence in sentences:
        sentence_vec = self.word_to_vec(vocab_list, sentence)
        train_data.append(sentence_vec)
    # 需要對train_data和label轉(zhuǎn)換成矩陣,因為訓(xùn)練時要進行矩陣運算    
    pos_each_word_prob, neg_each_word_prob, pos_prob = self.train(np.array(train_data), np.array(label)) 

    test_data = self.handle_data(test_data)
    test_vec = self.word_to_vec(vocab_list, test_data)
    pred = self.classify(np.array(test_vec), pos_each_word_prob, neg_each_word_prob, pos_prob)
    return pred

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容