1. 程式人生 > >Python3《機器學習實戰》學習筆記(五):樸素貝葉斯實戰篇之新浪新聞分類

Python3《機器學習實戰》學習筆記(五):樸素貝葉斯實戰篇之新浪新聞分類

一 前言

  • 拉普拉斯平滑
  • 垃圾郵件過濾
  • 新浪新聞分類

二 樸素貝葉斯改進之拉普拉斯平滑

上篇文章提到過,演算法存在一定的問題,需要進行改進。那麼需要改進的地方在哪裡呢?利用貝葉斯分類器對文件進行分類時,要計算多個概率的乘積以獲得文件屬於某個類別的概率,即計算p(w0|1)p(w1|1)p(w2|1)。如果其中有一個概率值為0,那麼最後的成績也為0。我們拿出上一篇文章的截圖。

從上圖可以看出,在計算的時候已經出現了概率為0的情況。如果新例項文字,包含這種概率為0的分詞,那麼最終的文字屬於某個類別的概率也就是0了。顯然,這樣是不合理的,為了降低這種影響,可以將所有詞的出現數初始化為1,並將分母初始化為2。這種做法就叫做拉普拉斯平滑(Laplace Smoothing)又被稱為加1平滑,是比較常用的平滑方法,它就是為了解決0概率問題。

除此之外,另外一個遇到的問題就是下溢位,這是由於太多很小的數相乘造成的。學過數學的人都知道,兩個小數相乘,越乘越小,這樣就造成了下溢位。在程式中,在相應小數位置進行四捨五入,計算結果可能就變成0了。為了解決這個問題,對乘積結果取自然對數。通過求對數可以避免下溢位或者浮點數舍入導致的錯誤。同時,採用自然對數進行處理不會有任何損失。下圖給出函式f(x)和ln(f(x))的曲線。

檢查這兩條曲線,就會發現它們在相同區域內同時增加或者減少,並且在相同點上取到極值。它們的取值雖然不同,但不影響最終結果。因此我們可以對上篇文章的trainNB0(trainMatrix, trainCategory)函式進行更改,修改如下:

"""
函式說明:樸素貝葉斯分類器訓練函式

Parameters:
    trainMatrix - 訓練文件矩陣,即setOfWords2Vec返回的returnVec構成的矩陣
    trainCategory - 訓練類別標籤向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 侮辱類的條件概率陣列
    p1Vect - 非侮辱類的條件概率陣列
    pAbusive - 文件屬於侮辱類的概率
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-12
"""
def trainNB0(trainMatrix,trainCategory): numTrainDocs = len(trainMatrix) #計算訓練的文件數目 numWords = len(trainMatrix[0]) #計算每篇文件的詞條數 pAbusive = sum(trainCategory)/float(numTrainDocs) #文件屬於侮辱類的概率 p0Num = np.ones(numWords); p1Num = np.ones(numWords) #建立numpy.ones陣列,詞條出現數初始化為1,拉普拉斯平滑 p0Denom = 2.0; p1Denom = 2.0 #分母初始化為2,拉普拉斯平滑 for i in range(numTrainDocs): if trainCategory[i] == 1: #統計屬於侮辱類的條件概率所需的資料,即P(w0|1),P(w1|1),P(w2|1)··· p1Num += trainMatrix[i] p1Denom += sum(trainMatrix[i]) else: #統計屬於非侮辱類的條件概率所需的資料,即P(w0|0),P(w1|0),P(w2|0)··· p0Num += trainMatrix[i] p0Denom += sum(trainMatrix[i]) p1Vect = np.log(p1Num/p1Denom) #取對數,防止下溢位 p0Vect = np.log(p0Num/p0Denom) return p0Vect,p1Vect,pAbusive #返回屬於侮辱類的條件概率陣列,屬於非侮辱類的條件概率陣列,文件屬於侮辱類的概率

執行程式碼,就可以得到如下結果:

瞧,這樣我們得到的結果就沒有問題了,不存在0概率。當然除此之外,我們還需要對程式碼進行修改classifyNB(vec2Classify, p0Vec, p1Vec, pClass1)函式,修改如下:

"""
函式說明:樸素貝葉斯分類器分類函式

Parameters:
    vec2Classify - 待分類的詞條陣列
    p0Vec - 侮辱類的條件概率陣列
    p1Vec -非侮辱類的條件概率陣列
    pClass1 - 文件屬於侮辱類的概率
Returns:
    0 - 屬於非侮辱類
    1 - 屬於侮辱類
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-12
"""
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

為啥這麼改?因為取自然對數了。logab = loga + logb。

這樣,我們的樸素貝葉斯分類器就改進完畢了。

三 樸素貝葉斯之過濾垃圾郵件

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

  • 收集資料:提供文字檔案。
  • 準備資料:將文字檔案解析成詞條向量。
  • 分析資料:檢查詞條確保解析的正確性。
  • 訓練演算法:使用我們之前建立的trainNB0()函式。
  • 測試演算法:使用classifyNB(),並構建一個新的測試函式來計算文件集的錯誤率。
  • 使用演算法:構建一個完整的程式對一組文件進行分類,將錯分的文件輸出到螢幕上。

1 收集資料

有兩個資料夾ham和spam,spam檔案下的txt檔案為垃圾郵件。

2 準備資料

對於英文文字,我們可以以非字母、非數字作為符號進行切分,使用split函式即可。編寫程式碼如下:

# -*- coding: UTF-8 -*-
import re

"""
函式說明:接收一個大字串並將其解析為字串列表

Parameters:
    無
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-14
"""
def textParse(bigString):                                                   #將字串轉換為字元列表
    listOfTokens = re.split(r'\W*', bigString)                              #將特殊符號作為切分標誌進行字串切分,即非字母、非數字
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]            #除了單個字母,例如大寫的I,其它單詞變成小寫

"""
函式說明:將切分的實驗樣本詞條整理成不重複的詞條列表,也就是詞彙表

Parameters:
    dataSet - 整理的樣本資料集
Returns:
    vocabSet - 返回不重複的詞條列表,也就是詞彙表
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-11
"""
def createVocabList(dataSet):
    vocabSet = set([])                      #建立一個空的不重複列表
    for document in dataSet:               
        vocabSet = vocabSet | set(document) #取並集
    return list(vocabSet)

if __name__ == '__main__':
    docList = []; classList = []
    for i in range(1, 26):                                                  #遍歷25個txt檔案
        wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())     #讀取每個垃圾郵件,並字串轉換成字串列表
        docList.append(wordList)
        classList.append(1)                                                 #標記垃圾郵件,1表示垃圾檔案
        wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())      #讀取每個非垃圾郵件,並字串轉換成字串列表
        docList.append(wordList)
        classList.append(0)                                                 #標記非垃圾郵件,1表示垃圾檔案   
    vocabList = createVocabList(docList)                                    #建立詞彙表,不重複
    print(vocabList)

這樣我們就得到了詞彙表,結果如下圖所示:

根據詞彙表,我們就可以將每個文字向量化。我們將資料集分為訓練集和測試集,使用交叉驗證的方式測試樸素貝葉斯分類器的準確性。編寫程式碼如下:

# -*- coding: UTF-8 -*-
import numpy as np
import random
import re

"""
函式說明:將切分的實驗樣本詞條整理成不重複的詞條列表,也就是詞彙表

Parameters:
    dataSet - 整理的樣本資料集
Returns:
    vocabSet - 返回不重複的詞條列表,也就是詞彙表
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-11
"""
def createVocabList(dataSet):
    vocabSet = set([])                      #建立一個空的不重複列表
    for document in dataSet:               
        vocabSet = vocabSet | set(document) #取並集
    return list(vocabSet)

"""
函式說明:根據vocabList詞彙表,將inputSet向量化,向量的每個元素為1或0

Parameters:
    vocabList - createVocabList返回的列表
    inputSet - 切分的詞條列表
Returns:
    returnVec - 文件向量,詞集模型
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-11
"""
def setOfWords2Vec(vocabList, inputSet):
    returnVec = [0] * len(vocabList)                                    #建立一個其中所含元素都為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                                                    #返回文件向量


"""
函式說明:根據vocabList詞彙表,構建詞袋模型

Parameters:
    vocabList - createVocabList返回的列表
    inputSet - 切分的詞條列表
Returns:
    returnVec - 文件向量,詞袋模型
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-14
"""
def bagOfWords2VecMN(vocabList, inputSet):
    returnVec = [0]*len(vocabList)                                        #建立一個其中所含元素都為0的向量
    for word in inputSet:                                                #遍歷每個詞條
        if word in vocabList:                                            #如果詞條存在於詞彙表中,則計數加一
            returnVec[vocabList.index(word)] += 1
    return returnVec                                                    #返回詞袋模型

"""
函式說明:樸素貝葉斯分類器訓練函式

Parameters:
    trainMatrix - 訓練文件矩陣,即setOfWords2Vec返回的returnVec構成的矩陣
    trainCategory - 訓練類別標籤向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 侮辱類的條件概率陣列
    p1Vect - 非侮辱類的條件概率陣列
    pAbusive - 文件屬於侮辱類的概率
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-12
"""
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)                            #計算訓練的文件數目
    numWords = len(trainMatrix[0])                            #計算每篇文件的詞條數
    pAbusive = sum(trainCategory)/float(numTrainDocs)        #文件屬於侮辱類的概率
    p0Num = np.ones(numWords); p1Num = np.ones(numWords)    #建立numpy.ones陣列,詞條出現數初始化為1,拉普拉斯平滑
    p0Denom = 2.0; p1Denom = 2.0                            #分母初始化為2,拉普拉斯平滑
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:                            #統計屬於侮辱類的條件概率所需的資料,即P(w0|1),P(w1|1),P(w2|1)···
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:                                                #統計屬於非侮辱類的條件概率所需的資料,即P(w0|0),P(w1|0),P(w2|0)···
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = np.log(p1Num/p1Denom)                            #取對數,防止下溢位         
    p0Vect = np.log(p0Num/p0Denom)         
    return p0Vect,p1Vect,pAbusive                            #返回屬於侮辱類的條件概率陣列,屬於非侮辱類的條件概率陣列,文件屬於侮辱類的概率

"""
函式說明:樸素貝葉斯分類器分類函式

Parameters:
    vec2Classify - 待分類的詞條陣列
    p0Vec - 侮辱類的條件概率陣列
    p1Vec -非侮辱類的條件概率陣列
    pClass1 - 文件屬於侮辱類的概率
Returns:
    0 - 屬於非侮辱類
    1 - 屬於侮辱類
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-12
"""
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

"""
函式說明:樸素貝葉斯分類器訓練函式

Parameters:
    trainMatrix - 訓練文件矩陣,即setOfWords2Vec返回的returnVec構成的矩陣
    trainCategory - 訓練類別標籤向量,即loadDataSet返回的classVec
Returns:
    p0Vect - 侮辱類的條件概率陣列
    p1Vect - 非侮辱類的條件概率陣列
    pAbusive - 文件屬於侮辱類的概率
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-12
"""
def trainNB0(trainMatrix,trainCategory):
    numTrainDocs = len(trainMatrix)                            #計算訓練的文件數目
    numWords = len(trainMatrix[0])                            #計算每篇文件的詞條數
    pAbusive = sum(trainCategory)/float(numTrainDocs)        #文件屬於侮辱類的概率
    p0Num = np.ones(numWords); p1Num = np.ones(numWords)    #建立numpy.ones陣列,詞條出現數初始化為1,拉普拉斯平滑
    p0Denom = 2.0; p1Denom = 2.0                            #分母初始化為2,拉普拉斯平滑
    for i in range(numTrainDocs):
        if trainCategory[i] == 1:                            #統計屬於侮辱類的條件概率所需的資料,即P(w0|1),P(w1|1),P(w2|1)···
            p1Num += trainMatrix[i]
            p1Denom += sum(trainMatrix[i])
        else:                                                #統計屬於非侮辱類的條件概率所需的資料,即P(w0|0),P(w1|0),P(w2|0)···
            p0Num += trainMatrix[i]
            p0Denom += sum(trainMatrix[i])
    p1Vect = np.log(p1Num/p1Denom)                            #取對數,防止下溢位         
    p0Vect = np.log(p0Num/p0Denom)         
    return p0Vect,p1Vect,pAbusive                            #返回屬於侮辱類的條件概率陣列,屬於非侮辱類的條件概率陣列,文件屬於侮辱類的概率


"""
函式說明:接收一個大字串並將其解析為字串列表

Parameters:
    無
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-14
"""
def textParse(bigString):                                                   #將字串轉換為字元列表
    listOfTokens = re.split(r'\W*', bigString)                              #將特殊符號作為切分標誌進行字串切分,即非字母、非數字
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]            #除了單個字母,例如大寫的I,其它單詞變成小寫

"""
函式說明:測試樸素貝葉斯分類器

Parameters:
    無
Returns:
    無
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-14
"""
def spamTest():
    docList = []; classList = []; fullText = []
    for i in range(1, 26):                                                  #遍歷25個txt檔案
        wordList = textParse(open('email/spam/%d.txt' % i, 'r').read())     #讀取每個垃圾郵件,並字串轉換成字串列表
        docList.append(wordList)
        fullText.append(wordList)
        classList.append(1)                                                 #標記垃圾郵件,1表示垃圾檔案
        wordList = textParse(open('email/ham/%d.txt' % i, 'r').read())      #讀取每個非垃圾郵件,並字串轉換成字串列表
        docList.append(wordList)
        fullText.append(wordList)
        classList.append(0)                                                 #標記非垃圾郵件,1表示垃圾檔案   
    vocabList = createVocabList(docList)                                    #建立詞彙表,不重複
    trainingSet = list(range(50)); testSet = []                             #建立儲存訓練集的索引值的列表和測試集的索引值的列表                       
    for i in range(10):                                                     #從50個郵件中,隨機挑選出40個作為訓練集,10個做測試集
        randIndex = int(random.uniform(0, len(trainingSet)))                #隨機選取索索引值
        testSet.append(trainingSet[randIndex])                              #新增測試集的索引值
        del(trainingSet[randIndex])                                         #在訓練集列表中刪除新增到測試集的索引值
    trainMat = []; trainClasses = []                                        #建立訓練集矩陣和訓練集類別標籤系向量             
    for docIndex in trainingSet:                                            #遍歷訓練集
        trainMat.append(setOfWords2Vec(vocabList, docList[docIndex]))       #將生成的詞集模型新增到訓練矩陣中
        trainClasses.append(classList[docIndex])                            #將類別新增到訓練集類別標籤系向量中
    p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))  #訓練樸素貝葉斯模型
    errorCount = 0                                                          #錯誤分類計數
    for docIndex in testSet:                                                #遍歷測試集
        wordVector = setOfWords2Vec(vocabList, docList[docIndex])           #測試集的詞集模型
        if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:    #如果分類錯誤
            errorCount += 1                                                 #錯誤計數加1
            print("分類錯誤的測試集:",docList[docIndex])
    print('錯誤率:%.2f%%' % (float(errorCount) / len(testSet) * 100))


if __name__ == '__main__':
    spamTest()

執行結果如下:

函式spamTest()會輸出在10封隨機選擇的電子郵件上的分類錯誤概率。既然這些電子郵件是隨機選擇的,所以每次的輸出結果可能有些差別。如果發現錯誤的話,函式會輸出錯誤的文件的此表,這樣就可以瞭解到底是哪篇文件發生了錯誤。如果想要更好地估計錯誤率,那麼就應該將上述過程重複多次,比如說10次,然後求平均值。相比之下,將垃圾郵件誤判為正常郵件要比將正常郵件歸為垃圾郵件好。為了避免錯誤,有多種方式可以用來修正分類器,這些內容會在後續文章中進行討論。

四 樸素貝葉斯之新浪新聞分類(Sklearn)

1 中文語句切分

考慮一個問題,英文的語句可以通過非字母和非數字進行切分,但是漢語句子呢?就比如我打的這一堆字,該如何進行切分呢?我們自己寫個規則?

幸運地是,這部分的工作不需要我們自己做了,可以直接使用第三方分片語件,即jieba,沒錯就是”結巴”。

jieba已經相容Python2和Python3,使用如下指令直接安裝即可:

pip3 install jieba

Python中文分片語件使用簡單:

資料集已經做好分類,分資料夾儲存,分類結果如下:

資料集已經準備好,接下來,讓我們直接進入正題。切分中文語句,編寫如下程式碼:

# -*- coding: UTF-8 -*-
import os
import jieba

def TextProcessing(folder_path):
    folder_list = os.listdir(folder_path)                        #檢視folder_path下的檔案
    data_list = []                                                #訓練集
    class_list = []

    #遍歷每個子資料夾
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根據子資料夾,生成新的路徑
        files = os.listdir(new_folder_path)                        #存放子資料夾下的txt檔案的列表

        j = 1
        #遍歷每個txt檔案
        for file in files:
            if j > 100:                                            #每類txt樣本數最多100個
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #開啟txt檔案
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精簡模式,返回一個可迭代的generator
            word_list = list(word_cut)                            #generator轉換為list

            data_list.append(word_list)
            class_list.append(folder)
            j += 1
        print(data_list)
        print(class_list)
if __name__ == '__main__':
    #文字預處理
    folder_path = './SogouC/Sample'                #訓練集存放地址
    TextProcessing(folder_path)

程式碼執行結果如下所示,可以看到,我們已經順利將每個文字進行切分,並進行了類別標記。

2 文字特徵選擇

我們將所有文字分成訓練集和測試集,並對訓練集中的所有單詞進行詞頻統計,並按降序排序。也就是將出現次數多的詞語在前,出現次數少的詞語在後進行排序。編寫程式碼如下:

# -*- coding: UTF-8 -*-
import os
import random
import jieba

"""
函式說明:中文文字處理

Parameters:
    folder_path - 文字存放的路徑
    test_size - 測試集佔比,預設佔所有資料集的百分之20
Returns:
    all_words_list - 按詞頻降序排序的訓練集列表
    train_data_list - 訓練集列表
    test_data_list - 測試集列表
    train_class_list - 訓練集標籤列表
    test_class_list - 測試集標籤列表
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-22
"""
def TextProcessing(folder_path, test_size = 0.2):
    folder_list = os.listdir(folder_path)                        #檢視folder_path下的檔案
    data_list = []                                                #資料集資料
    class_list = []                                                #資料集類別

    #遍歷每個子資料夾
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根據子資料夾,生成新的路徑
        files = os.listdir(new_folder_path)                        #存放子資料夾下的txt檔案的列表

        j = 1
        #遍歷每個txt檔案
        for file in files:
            if j > 100:                                            #每類txt樣本數最多100個
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #開啟txt檔案
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精簡模式,返回一個可迭代的generator
            word_list = list(word_cut)                            #generator轉換為list

            data_list.append(word_list)                            #新增資料集資料
            class_list.append(folder)                            #新增資料集類別
            j += 1

    data_class_list = list(zip(data_list, class_list))            #zip壓縮合並,將資料與標籤對應壓縮
    random.shuffle(data_class_list)                                #將data_class_list亂序
    index = int(len(data_class_list) * test_size) + 1            #訓練集和測試集切分的索引值
    train_list = data_class_list[index:]                        #訓練集
    test_list = data_class_list[:index]                            #測試集
    train_data_list, train_class_list = zip(*train_list)        #訓練集解壓縮
    test_data_list, test_class_list = zip(*test_list)            #測試集解壓縮

    all_words_dict = {}                                            #統計訓練集詞頻
    for word_list in train_data_list:
        for word in word_list:
            if word in all_words_dict.keys():
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    #根據鍵的值倒序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key = lambda f:f[1], reverse = True)
    all_words_list, all_words_nums = zip(*all_words_tuple_list)    #解壓縮
    all_words_list = list(all_words_list)                        #轉換成列表
    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list

if __name__ == '__main__':
    #文字預處理
    folder_path = './SogouC/Sample'                #訓練集存放地址
    all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = TextProcessing(folder_path, test_size=0.2)
    print(all_words_list)

all_words_list就是將所有訓練集的切分結果通過詞頻降序排列構成的單詞合集。觀察一下列印結果,不難發現,這裡包含了很多標點符號,很顯然,這些標點符號是不能作為新聞分類的特徵的。總不能說,應為這個文章逗號多,所以它是xx類新聞吧?為了降低這些高頻的符號對分類結果的影響,我們應該怎麼做呢?答曰:拋棄他們! 除了這些,還有”在”,”了”這樣對新聞分類無關痛癢的詞。並且還有一些數字,數字顯然也不能作為分類新聞的特徵。所以要消除它們對分類結果的影響,我們可以定製一個規則。

一個簡單的規則可以這樣制定:首先去掉高頻詞,至於去掉多少個高頻詞,我們可以通過觀察去掉高頻詞個數和最終檢測準確率的關係來確定。除此之外,去除數字,不把數字作為分類特徵。同時,去除一些特定的詞語,比如:”的”,”一”,”在”,”不”,”當然”,”怎麼”這類的對新聞分類無影響的介詞、代詞、連詞。怎麼去除這些詞呢?可以使用已經整理好的stopwords_cn.txt文字。下載地址:https://github.com/Jack-Cherish/Machine-Learning/blob/master/Naive%20Bayes/stopwords_cn.txt

這個檔案是這個樣子的:

所以我們可以根據這個文件,將這些單詞去除,不作為分類的特徵。我們先去除前100個高頻詞彙,然後編寫程式碼如下:

# -*- coding: UTF-8 -*-
import os
import random
import jieba

"""
函式說明:中文文字處理

Parameters:
    folder_path - 文字存放的路徑
    test_size - 測試集佔比,預設佔所有資料集的百分之20
Returns:
    all_words_list - 按詞頻降序排序的訓練集列表
    train_data_list - 訓練集列表
    test_data_list - 測試集列表
    train_class_list - 訓練集標籤列表
    test_class_list - 測試集標籤列表
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-22
"""
def TextProcessing(folder_path, test_size = 0.2):
    folder_list = os.listdir(folder_path)                        #檢視folder_path下的檔案
    data_list = []                                                #資料集資料
    class_list = []                                                #資料集類別

    #遍歷每個子資料夾
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根據子資料夾,生成新的路徑
        files = os.listdir(new_folder_path)                        #存放子資料夾下的txt檔案的列表

        j = 1
        #遍歷每個txt檔案
        for file in files:
            if j > 100:                                            #每類txt樣本數最多100個
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #開啟txt檔案
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精簡模式,返回一個可迭代的generator
            word_list = list(word_cut)                            #generator轉換為list

            data_list.append(word_list)                            #新增資料集資料
            class_list.append(folder)                            #新增資料集類別
            j += 1

    data_class_list = list(zip(data_list, class_list))            #zip壓縮合並,將資料與標籤對應壓縮
    random.shuffle(data_class_list)                                #將data_class_list亂序
    index = int(len(data_class_list) * test_size) + 1            #訓練集和測試集切分的索引值
    train_list = data_class_list[index:]                        #訓練集
    test_list = data_class_list[:index]                            #測試集
    train_data_list, train_class_list = zip(*train_list)        #訓練集解壓縮
    test_data_list, test_class_list = zip(*test_list)            #測試集解壓縮

    all_words_dict = {}                                            #統計訓練集詞頻
    for word_list in train_data_list:
        for word in word_list:
            if word in all_words_dict.keys():
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    #根據鍵的值倒序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key = lambda f:f[1], reverse = True)
    all_words_list, all_words_nums = zip(*all_words_tuple_list)    #解壓縮
    all_words_list = list(all_words_list)                        #轉換成列表
    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list

"""
函式說明:讀取檔案裡的內容,並去重

Parameters:
    words_file - 檔案路徑
Returns:
    words_set - 讀取的內容的set集合
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-22
"""
def MakeWordsSet(words_file):
    words_set = set()                                            #建立set集合
    with open(words_file, 'r', encoding = 'utf-8') as f:        #開啟檔案
        for line in f.readlines():                                #一行一行讀取
            word = line.strip()                                    #去回車
            if len(word) > 0:                                    #有文字,則新增到words_set中
                words_set.add(word)                               
    return words_set                                             #返回處理結果

"""
函式說明:文字特徵選取

Parameters:
    all_words_list - 訓練集所有文字列表
    deleteN - 刪除詞頻最高的deleteN個詞
    stopwords_set - 指定的結束語
Returns:
    feature_words - 特徵集
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-22
"""
def words_dict(all_words_list, deleteN, stopwords_set = set()):
    feature_words = []                            #特徵列表
    n = 1
    for t in range(deleteN, len(all_words_list), 1):
        if n > 1000:                            #feature_words的維度為1000
            break                               
        #如果這個詞不是數字,並且不是指定的結束語,並且單詞長度大於1小於5,那麼這個詞就可以作為特徵詞
        if not all_words_list[t].isdigit() and all_words_list[t] not in stopwords_set and 1 < len(all_words_list[t]) < 5:
            feature_words.append(all_words_list[t])
        n += 1
    return feature_words

if __name__ == '__main__':
    #文字預處理
    folder_path = './SogouC/Sample'                #訓練集存放地址
    all_words_list, train_data_list, test_data_list, train_class_list, test_class_list = TextProcessing(folder_path, test_size=0.2)

    #生成stopwords_set
    stopwords_file = './stopwords_cn.txt'
    stopwords_set = MakeWordsSet(stopwords_file)

    feature_words = words_dict(all_words_list, 100, stopwords_set)
    print(feature_words)

執行結果如下:

可以看到,我們已經濾除了那些沒有用的片語,這個feature_words就是我們最終選出的用於新聞分類的特徵。隨後,我們就可以根據feature_words,將文字向量化,然後用於訓練樸素貝葉斯分類器。這個向量化的思想和第三章的思想一致,因此不再累述。

3 使用Sklearn構建樸素貝葉斯分類器

資料已經處理好了,接下來就可以使用sklearn構建樸素貝葉斯分類器了。

樸素貝葉斯是一類比較簡單的演算法,scikit-learn中樸素貝葉斯類庫的使用也比較簡單。相對於決策樹,KNN之類的演算法,樸素貝葉斯需要關注的引數是比較少的,這樣也比較容易掌握。在scikit-learn中,一共有3個樸素貝葉斯的分類演算法類。分別是GaussianNB,MultinomialNB和BernoulliNB。其中GaussianNB就是先驗為高斯分佈的樸素貝葉斯,MultinomialNB就是先驗為多項式分佈的樸素貝葉斯,而BernoulliNB就是先驗為伯努利分佈的樸素貝葉斯。上篇文章講解的先驗概率模型就是先驗概率為多項式分佈的樸素貝葉斯。

對於新聞分類,屬於多分類問題。我們可以使用MultinamialNB()完成我們的新聞分類問題。另外兩個函式的使用暫且不再進行擴充套件,可以自行學習。MultinomialNB假設特徵的先驗概率為多項式分佈,即如下式:

其中,P(Xj = Xjl | Y = Ck)是第k個類別的第j維特徵的第l個取值條件概率。mk是訓練集中輸出為第k類的樣本個數。λ為一個大於0的常數,嚐嚐取值為1,即拉普拉斯平滑,也可以取其他值。

接下來,我們看下MultinamialNB這個函式,只有3個引數:

引數說明如下:

  • alpha:浮點型可選引數,預設為1.0,其實就是新增拉普拉斯平滑,即為上述公式中的λ ,如果這個引數設定為0,就是不新增平滑;
  • fit_prior:布林型可選引數,預設為True。布林引數fit_prior表示是否要考慮先驗概率,如果是false,則所有的樣本類別輸出都有相同的類別先驗概率。否則可以自己用第三個引數class_prior輸入先驗概率,或者不輸入第三個引數class_prior讓MultinomialNB自己從訓練集樣本來計算先驗概率,此時的先驗概率為P(Y=Ck)=mk/m。其中m為訓練集樣本總數量,mk為輸出為第k類別的訓練集樣本數。
  • class_prior:可選引數,預設為None。

總結如下:

fit_prior class_prior 最終先驗概率
False 填或不填沒有意義 P(Y = Ck) = 1 / k
True 不填 P(Y = Ck) = mk / m
True P(Y = Ck) = class_prior

除此之外,MultinamialNB也有一些方法供我們使用:

MultinomialNB一個重要的功能是有partial_fit方法,這個方法的一般用在如果訓練集資料量非常大,一次不能全部載入記憶體的時候。這時我們可以把訓練集分成若干等分,重複呼叫partial_fit來一步步的學習訓練集,非常方便。GaussianNB和BernoulliNB也有類似的功能。 在使用MultinomialNB的fit方法或者partial_fit方法擬合數據後,我們可以進行預測。此時預測有三種方法,包括predict,predict_log_proba和predict_proba。predict方法就是我們最常用的預測方法,直接給出測試集的預測類別輸出。predict_proba則不同,它會給出測試集樣本在各個類別上預測的概率。容易理解,predict_proba預測出的各個類別概率裡的最大值對應的類別,也就是predict方法得到類別。predict_log_proba和predict_proba類似,它會給出測試集樣本在各個類別上預測的概率的一個對數轉化。轉化後predict_log_proba預測出的各個類別對數概率裡的最大值對應的類別,也就是predict方法得到類別。具體細節不再講解,可參照官網手冊。

瞭解了這些,我們就可以編寫程式碼,通過觀察取不同的去掉前deleteN個高頻詞的個數與最終檢測準確率的關係,確定deleteN的取值:

# -*- coding: UTF-8 -*-
from sklearn.naive_bayes import MultinomialNB
import matplotlib.pyplot as plt
import os
import random
import jieba

"""
函式說明:中文文字處理

Parameters:
    folder_path - 文字存放的路徑
    test_size - 測試集佔比,預設佔所有資料集的百分之20
Returns:
    all_words_list - 按詞頻降序排序的訓練集列表
    train_data_list - 訓練集列表
    test_data_list - 測試集列表
    train_class_list - 訓練集標籤列表
    test_class_list - 測試集標籤列表
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-22
"""
def TextProcessing(folder_path, test_size = 0.2):
    folder_list = os.listdir(folder_path)                        #檢視folder_path下的檔案
    data_list = []                                                #資料集資料
    class_list = []                                                #資料集類別

    #遍歷每個子資料夾
    for folder in folder_list:
        new_folder_path = os.path.join(folder_path, folder)        #根據子資料夾,生成新的路徑
        files = os.listdir(new_folder_path)                        #存放子資料夾下的txt檔案的列表

        j = 1
        #遍歷每個txt檔案
        for file in files:
            if j > 100:                                            #每類txt樣本數最多100個
                break
            with open(os.path.join(new_folder_path, file), 'r', encoding = 'utf-8') as f:    #開啟txt檔案
                raw = f.read()

            word_cut = jieba.cut(raw, cut_all = False)            #精簡模式,返回一個可迭代的generator
            word_list = list(word_cut)                            #generator轉換為list

            data_list.append(word_list)                            #新增資料集資料
            class_list.append(folder)                            #新增資料集類別
            j += 1

    data_class_list = list(zip(data_list, class_list))            #zip壓縮合並,將資料與標籤對應壓縮
    random.shuffle(data_class_list)                                #將data_class_list亂序
    index = int(len(data_class_list) * test_size) + 1            #訓練集和測試集切分的索引值
    train_list = data_class_list[index:]                        #訓練集
    test_list = data_class_list[:index]                            #測試集
    train_data_list, train_class_list = zip(*train_list)        #訓練集解壓縮
    test_data_list, test_class_list = zip(*test_list)            #測試集解壓縮

    all_words_dict = {}                                            #統計訓練集詞頻
    for word_list in train_data_list:
        for word in word_list:
            if word in all_words_dict.keys():
                all_words_dict[word] += 1
            else:
                all_words_dict[word] = 1

    #根據鍵的值倒序排序
    all_words_tuple_list = sorted(all_words_dict.items(), key = lambda f:f[1], reverse = True)
    all_words_list, all_words_nums = zip(*all_words_tuple_list)    #解壓縮
    all_words_list = list(all_words_list)                        #轉換成列表
    return all_words_list, train_data_list, test_data_list, train_class_list, test_class_list

"""
函式說明:讀取檔案裡的內容,並去重

Parameters:
    words_file - 檔案路徑
Returns:
    words_set - 讀取的內容的set集合
Author:
    Jack Cui
Blog:
    http://blog.csdn.net/c406495762
Modify:
    2017-08-22
"""
def MakeWordsSet(words_file):
    words_set = set()                                            #建立set集合
    with open(words_file, 'r', encoding = 'utf-8') as f:        #開啟檔案
        for line in f.readlines():                                #一行一行讀取
            word = line.strip()                                    #去回車
            if len(word) > 0:                                    #有文字,則新增到words_set中
                words_set.add(word)                               
    return words_set                                             #返回處理結果

"""
函式說明:根據feature_words將文字向量化

Parameters:
    train_data_list - 訓練集
    test_data_list - 測試集
    feature_w