前文回顧
上一篇文章介紹了樸素貝葉斯算法的相關知識,包括以下幾方面:
- 樸素貝葉斯算法的基本原理
- 公式推導貝葉斯準則(條件概率公式)
- 構建訓練、測試簡易文本分類算法
- 拉普拉斯平滑修正
其中公式推導這一部分較爲重要,利用條件概率解決問題也是樸素貝葉斯的基本思想,所以理解貝葉斯準則如何得到,以及如何應用十分重要,也是後期構建算法的基礎。
現實生活中樸素貝葉斯算法應用廣泛,如文本分類,垃圾郵件的分類,信用評估,釣魚網站檢測等等;就文本分類而言,在衆多分類算法,樸素貝葉斯分類算法也是學習效率和分類效果較好的分類器之一,因爲樸素貝葉斯原理簡單,構建算法相對容易,並且具有很好的可解釋性。
但是樸素貝葉斯算法特點是假設所有特徵的出現相互獨立互不影響,每一特徵都同等重要。但事實上這個假設在現實世界中並不成立:首先,相鄰的兩個詞之間的必然聯繫,不能獨立;其次,對一篇文章來說,其中的某一些代表詞就確定它的主題,不需要通讀整篇文章、查看所有詞。所以需要採用合適的方法進行特徵選擇,這樣樸素貝葉斯分類器才能達到更高的分類效率。
本文背景
本文利用樸素貝葉斯方法構建一個情感分類器,用於判斷一個未知的語句,其所表達的是正面情緒or負面情緒,並通過比對預測結果和真實結果,得到該分類器的準確率。
最近在抖音上偶然看了一部電影的片段——飢餓站臺,背景是在未來的反烏托邦國度中,囚犯們被關押在垂直堆疊的牢房裏,飢腸轆轆地看着食物從上層落下,靠近頂層的人喫得飽飽的,而位於底層的人則因飢餓而變得激進,主要講述了人性黑暗和飢渴的一面。個人認爲這部電影還是不錯的,所以我選擇了豆瓣上這部電影的短評作爲本文數據,但是比較遺憾的是爬取的數據並不多,但主要是講述的是思想嘛。
豆瓣爬蟲相對容易,所以爬蟲部分不過多概述,我這裏用的是requests和BeautifulSoup結合,但需要注意的是模擬登陸部分,如果不進行模擬登陸只能獲取前10頁的短評,而模擬登陸後可獲取共24頁短評。小Tip:熱門短評和最新短評是不衝突的,最新短評可獲取100條,這樣數據樣本能多一些。
最後得到的數據集共580個樣本、三個屬性,截圖如下:
文本預處理
在這個構建情感分類器的小實戰中,算法部分並不是很複雜,很大一部分都是上文提及過的,而更多操作是在預處理數據集。如果是公共數據源上獲取的數據集,可能只需要進行簡單處理,因爲大部分問題數據集的作者已經解決,但是個人爬蟲得到的數據集,存在的問題相對較多,我們希望的是將所有短評文本轉化成以詞彙組成的列表格式,下面對文本進行預處理。
在原始數據集中,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的短評,原因是無法確定其短評情緒的類別。然後將評分等級爲4、5的短評用1標註,視爲正面情緒;將評分等級爲1、2的短評用0標註,視爲負面情緒。
# 刪去評分爲3的短評,判定評分爲3的情感持中性
data = data[data['new_rating'] != 3]
#將4、5評分標註成1,視爲正面情緒;將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)
短評切分後一定會產生許多無關情感的詞彙,例如一個、這個、人們等等,所以停用詞函數的作用就是將此類詞彙從短評中過濾掉。該函數主要思想是將短評按空格切分成詞彙,然後遍歷這個詞彙列表,如果一個詞彙未出現在停用詞表中、詞彙長度大於1、詞彙不爲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條。只有數據充足,模型纔能有具有實用性,數據太少會導致模型的準確率浮動較大,並且也具有極高的偶然性。
關注公衆號【奶糖貓】後臺回覆“飢餓站臺”可獲取源碼和數據供參考,感謝閱讀。