機器學習筆記(六)——樸素貝葉斯構建「飢餓站臺」豆瓣短評情感分類器

前文回顧

上一篇文章介紹了樸素貝葉斯算法的相關知識,包括如下幾方面:python

  • 樸素貝葉斯算法的基本原理
  • 公式推導貝葉斯準則(條件機率公式)
  • 構建訓練、測試簡易文本分類算法
  • 拉普拉斯平滑修正

其中公式推導這一部分較爲重要,利用條件機率解決問題也是樸素貝葉斯的基本思想,因此理解貝葉斯準則如何獲得,以及如何應用十分重要,也是後期構建算法的基礎。算法

現實生活中樸素貝葉斯算法應用普遍,如文本分類,垃圾郵件的分類,信用評估,釣魚網站檢測等等;就文本分類而言,在衆多分類算法,樸素貝葉斯分類算法也是學習效率和分類效果較好的分類器之一,由於樸素貝葉斯原理簡單,構建算法相對容易,而且具備很好的可解釋性。數組

可是樸素貝葉斯算法特色是假設全部特徵的出現相互獨立互不影響,每一特徵都同等重要。但事實上這個假設在現實世界中並不成立:首先,相鄰的兩個詞之間的必然聯繫,不能獨立;其次,對一篇文章來講,其中的某一些表明詞就肯定它的主題,不須要通讀整篇文章、查看全部詞。因此須要採用合適的方法進行特徵選擇,這樣樸素貝葉斯分類器才能達到更高的分類效率。app

本文背景

本文利用樸素貝葉斯方法構建一個情感分類器,用於判斷一個未知的語句,其所表達的是正面情緒or負面情緒,並經過比對預測結果和真實結果,獲得該分類器的準確率。dom

最近在抖音上偶然看了一部電影的片斷——飢餓站臺,背景是在將來的反烏托邦國度中,囚犯們被關押在垂直堆疊的牢房裏,飢腸轆轆地看着食物從上層落下,靠近頂層的人吃得飽飽的,而位於底層的人則因飢餓而變得激進,主要講述了人性黑暗和飢渴的一面。我的認爲這部電影仍是不錯的,因此我選擇了豆瓣上這部電影的短評做爲本文數據,可是比較遺憾的是爬取的數據並很少,但主要是講述的是思想嘛。機器學習

豆瓣爬蟲相對容易,因此爬蟲部分不過多概述,我這裏用的是requestsBeautifulSoup結合,但須要注意的是模擬登錄部分,若是不進行模擬登錄只能獲取前10頁的短評,而模擬登錄後可獲取共24頁短評。小Tip:熱門短評和最新短評是不衝突的,最新短評可獲取100條,這樣數據樣本能多一些。ide

最後獲得的數據集共580個樣本、三個屬性,截圖以下:
在這裏插入圖片描述函數

文本預處理

在這個構建情感分類器的小實戰中,算法部分並非很複雜,很大一部分都是上文說起過的,而更多操做是在預處理數據集。若是是公共數據源上獲取的數據集,可能只須要進行簡單處理,由於大部分問題數據集的做者已經解決,可是我的爬蟲獲得的數據集,存在的問題相對較多,咱們但願的是將全部短評文本轉化成以詞彙組成的列表格式,下面對文本進行預處理。post

在原始數據集中,rating這一列是由評分+推薦指數構成,格式不是咱們須要的,因此這裏利用一個自定義函數,將其劃分紅1-5五個等級,咱們能夠將評分等級視爲其對應短評的情感分類。學習

#將評分劃分紅1-5五個等級
def rating(e):
   if '50' in e:
       return 5
   elif '40' in e:
       return 4
   elif '30' in e:
       return 3
   elif '20' in e:
       return 2
   elif '10' in e:
       return 1
   else:
       return 'none'
# 利用map方法依據rating函數建立新一列
data['new_rating'] = data['rating'].map(rating)

劃分等級以後,咱們須要對每一條短評情感標註,這裏選擇刪去評分等級爲3的短評,緣由是沒法肯定其短評情緒的類別。而後將評分等級爲四、5的短評用1標註,視爲正面情緒;將評分等級爲一、2的短評用0標註,視爲負面情緒。

# 刪去評分爲3的短評,斷定評分爲3的情感持中性
data = data[data['new_rating'] != 3]
#將四、5評分標註成1,視爲正面情緒;將一、2評分標註成0,視爲負面情緒
data['sentiment'] = data['new_rating'].apply(lambda x: +1 if x > 3 else 0)

下圖爲五個評分等級的佔比餅圖,能夠看出3分的佔比是比較大的,因此刪去評分爲3的操做讓數據集損失不少數據樣本;4分、5分的佔比要遠多於1分、2分,因此數據集中正面情緒佔比極大,電影多是真不錯,可是也凸顯出了數據比例不均衡的問題。

在這裏插入圖片描述
爬蟲獲取的短評可能包含不少英文符號、單詞、字母,這些對於中文情感分析是沒有任何幫助的,因此在分詞以前,利用兩個自定義函數刪去短評中的符號和英文字母,這裏沒有對數字操做是由於下文停用詞中包含了刪去數字的操做,jieba分詞模式選擇默認的精準模式,精準模式能夠將句子精確地切開,比較適合文本分析。

# 刪去短評中的符號、英文字母
punc = '~`!#$%^&*()_+-=|\';":/.,?><~·!@#¥%……&*()——+-=「:’;、。,?》《{}'
def remove_fuhao(e):
    return re.sub(r"[%s]+" % punc, " ", e)
def remove_letter(new_short):
    return re.sub(r'[a-zA-Z]+', '', new_short)
# 利用jieba切割文本
def cut_word(text):
    text = jieba.cut(str(text))
    return ' '.join(text)
# 同apply方法依據以上三個自定義函數爲依據建立新一列
data['new_short'] = data['short'].apply(remove_fuhao).apply(remove_letter).apply(cut_word)

短評切分後必定會產生許多無關情感的詞彙,例如一個、這個、人們等等,因此停用詞函數的做用就是將此類詞彙從短評中過濾掉。該函數主要思想是將短評按空格切分紅詞彙,而後遍歷這個詞彙列表,若是一個詞彙未出如今停用詞表中、詞彙長度大於一、詞彙不爲Tab,則將鏈接至字符串outstr中;若是某個詞彙已經存在於outstr,則再也不添加,達到去重的效果。

文末提供中文停用詞表獲取方式

# 讀取停用詞表函數
def stopwordslist(filepath):
    stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]
    return stopwords
# 將短評中的停用詞刪去
def sentence_div(text):
    # 將短評按空格劃分紅單詞並造成列表
    sentence = text.strip().split()
    # 加載停用詞的路徑
    stopwords = stopwordslist(r'中文停用詞表.txt')
    #建立一個空字符串
    outstr = ' '
    # 遍歷短評列表中每一個單詞
    for word in sentence:
        if word not in stopwords: # 判斷詞彙是否在停用詞表裏
            if len(word) > 1:  # 單詞長度要大於1
                if word != '\t':  # 單詞不能爲tab
                    if word not in outstr:  # 去重:若是單詞在outstr中則不加入
                        outstr += ' '  # 分割
                        outstr += word # 將詞彙加入outstr
    #返回字符串
    return outstr
data['the_short'] = data['new_short'].apply(sentence_div)

可能有一條短評說的不少都是是廢話,恰巧都被停用詞函數過濾了,剩下的詞彙較少對這條短評的情感分析幫助很小,因此這裏將詞彙數量少於4個的短評刪去;因爲上面依據自定義函數建立了許多新的屬性,內容過於冗雜,因此選出情感分析須要的兩列(處理後的短評和標註)合併成一個新的DataFrame

data['split'] = data['the_short'].apply(lambda x: 1 if len(x.split()) > 3 else 0)
data = data[~data['split'].isin(['0'])]
# 將須要的兩列數據索引出,合併成一個新的DataFrame
new_data1 = data.iloc[:, 3]
new_data2 = data.iloc[:, 5]
new_data = pd.DataFrame({'short': new_data2, 'sentiment': new_data1})

通過預處理的數據集只剩下了280個樣本,截圖以下:
在這裏插入圖片描述
上文說起過一個問題,短評正面情緒所佔比例要遠大於負面情緒,爲了不測試數據集中的樣本全爲正面情緒,因此這裏採用隨機選擇的方式劃分數據集。利用random庫中的sample方法隨機選擇10%的數據的索引做爲測試數據集的索引,剩下的部分做爲訓練數據集的索引;而後按照兩類索引將數據集切割成兩部分,並分別保存。

def splitDataSet(new_data):
    # 獲取數據集中隨機的10%做爲測試集,獲取測試數據集的索引
    test_index = random.sample(new_data.index.tolist(), int(len(new_data.index.tolist()) * 0.10))
    # 剩下的部分做爲訓練集,獲取訓練數據集的索引
    train_index = [i for i in new_data.index.tolist() if i not in test_index]
    #分別索引出訓練集和測試集
    test_data = new_data.iloc[test_index]
    train_data = new_data.iloc[train_index]
    # 分別保存爲csv文件
    train_data.to_csv('bayes_train.csv', encoding='utf_8_sig', index=False)
    test_data.to_csv('bayes_test.csv', encoding='utf_8_sig', index=False)

構建分類器

構建分類器部分與上一篇文章的代碼會衝突,因此下面的算法部分不會過多講述其原理;若是你剛接觸樸素貝葉斯或者想了解其原理,推薦先觀看上一篇文章:機器學習筆記(五)——輕鬆看透樸素貝葉斯;若是你對樸素貝葉斯原理已經足夠理解了,若只對源碼和數據感興趣能夠直接跳過此部分劃到文末喲。

構建詞向量

loadDataSet函數的做用是將短評轉化成所須要的詞條向量格式,即每一條短評的詞彙構成一個列表,再將全部列表添加至一個列表中,構成一個詞條集合,classVec是由短評對應的情感標註構成的列表。

def loadDataSet(filename):
    data = pd.read_csv(filename)
    postingList = []
    #文本語句切分
    for sentence in data['short']:
        word = sentence.strip().split()# split方法返回一個列表
        postingList.append(word)#  將每一個詞彙列表添至一個列表中
    #類別標籤的向量
    classVec = data['sentiment'].values.tolist()
    return postingList,classVec

createVocabList函數的做用是經過set方法已經取並集的方式返回一個包含文本中全部出現的不重複詞的集合。

#建立詞彙表
def createVocabList(dataSet):
    #建立一個空的不重複列表
    vocabSet = set([])
    for document in dataSet:
        #取二者並集
        vocabSet = vocabSet | set(document)
    return list(vocabSet)

setOfWords2Vec函數的做用是將短評向量化,輸入參數爲總詞彙表和某個短評,輸出的是文本向量,向量的元素包括1或0,分別表示詞彙表中的單詞是否出如今輸入的文本中,思路是首先建立一個同詞彙表等長的向量,並將其元素都設置爲0,而後遍歷輸入文本的單詞,若詞彙表中出現了本文的單詞,則將其對應位置上的0置換爲1。

#詞條向量化函數
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

getMat函數的做用是將全部處理後的詞條向量彙總合併成一個詞條向量矩陣,方便測試算法時調用。

#詞條向量彙總
def getMat(inputSet):
    trainMat = []
    vocabList = createVocabList(inputSet)
    for Set in inputSet:
        returnVec = setOfWords2Vec(vocabList,Set)
        trainMat.append(returnVec)
    return trainMat

訓練算法

trainNB函數的輸入參數包括短評矩陣trainMatrix和每一個詞條的情感標註所構成的向量trainCategory。首先短評屬於正面情緒的機率只須要將正面情緒短評的個數除以總詞條個數便可;計算P(W | C1)和P(W | C0)時,須要將其分子和分母初始化,遍歷輸入文本時,一旦某個詞語(正面情緒or負面情緒)在某一文檔中出現,則該詞對應的個數(p1Num或p0Num)就加1,而且在總文本中,該詞條的總次數也相應加1。

def trainNB(trainMatrix,trainCategory):
    #訓練文本數量
    numTrainDocs = len(trainMatrix)
    #每篇文本的詞條數
    numWords = len(trainMatrix[0])
    #文檔屬於正面情緒(1)的機率
    pAbusive = sum(trainCategory)/float(numTrainDocs)
    #建立兩個長度爲numWords的零數組
    p0Num = np.ones(numWords)
    p1Num = np.ones(numWords)
    #分母初始化
    p0Denom = 2.0
    p1Denom = 2.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(p1Denom)
        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)
    #print("\n",p0Vect,"\n\n",p1Vect,"\n\n",pAbusive)
    return p1Vect,p0Vect,pAbusive

測試算法

classifyNB函數是一個判斷類別的函數,輸入參數爲向量格式的測試數據和訓練函數trainNB的三個返回值,如p1的機率大於p0的機率則表明該測試數據爲正面情緒,返回值爲1;返之則是負面情緒,返回值爲0。

def classifyNB(ClassifyVec, p1V,p0V,pAb):
    #將對應元素相乘
    print(pAb)
    p1 = sum(ClassifyVec * p1V) + np.log(pAb)
    p0 = sum(ClassifyVec * p0V) + np.log(1.0 - pAb)
    print('p1:',p1)
    print('p0:',p0)
    if p1 > p0:
        return 1
    else:
        return 0

testNB爲測試函數,經過調用上述函數對測試集進行預測,並經過比較真實結果和測試結果以獲得分類器的準確率。

def testNB():
    #加載訓練集數據
    train_postingList,train_classVec = loadDataSet('bayes_train4.csv')
    #建立詞彙表
    vocabSet = createVocabList(train_postingList)
    #將訓練樣本詞條向量彙總
    trainMat = getMat(train_postingList)
    #訓練算法
    p1V,P0V,PAb = trainNB(trainMat,train_classVec)
    #加載測試集數據
    test_postingList,test_classVec = loadDataSet('bayes_test4.csv')
    # 將測試文本向量化
    predict = []
    for each_test in test_postingList:
        testVec = setOfWords2Vec(vocabSet,each_test)
        #判斷類別
        if classifyNB(testVec,p1V,P0V,PAb):
            print(each_test,"正面情緒")
            predict.append(1)
        else:
            print(each_test,"負面情緒")
            predict.append(0)
    corr = 0.0
    for i in range(len(predict)):
        if predict[i] == test_classVec[i]:
            corr += 1
    print("樸素貝葉斯分類器準確率爲:" + str(round((corr/len(predict)*100),2)) + "%")

最後程序運行截圖以下:
在這裏插入圖片描述
由於咱們是利用隨機選擇的方法劃分訓練集與測試集,因此每次運行程序,樸素貝葉斯分類器的準確率都會改變,能夠多運行幾回取其平均值做爲該模型的準確率。最後附上依據該數據集繪製的詞雲圖,不知道這部電影的體裁能不能引發你的興趣的呢?
在這裏插入圖片描述

總結

在利用樸素貝葉斯算法進行相似的情感分析或者文本分類時,儘量要保持原始數據充足,像上文580條原始數據通過文本預處理以後只剩下280條。只有數據充足,模型纔能有具備實用性,數據太少會致使模型的準確率浮動較大,而且也具備極高的偶然性。

關注公衆號【奶糖貓】後臺回覆「飢餓站臺」可獲取源碼和數據供參考,感謝閱讀。

機器學習筆記(六)——樸素貝葉斯構建「飢餓站臺」豆瓣短評情感分類器

相關文章
相關標籤/搜索