Java實(shí)現(xiàn)基于樸素貝葉斯的情感詞分析
樸素貝葉斯(Naive Bayesian)是一種基于貝葉斯定理和特征條件獨(dú)立假設(shè)的分類方法,它是基于概率論的一種有監(jiān)督學(xué)習(xí)方法,被廣泛應(yīng)用于自然語言處理,并在機(jī)器學(xué)習(xí)領(lǐng)域中占據(jù)了非常重要的地位。在之前做過的一個(gè)項(xiàng)目中,就用到了樸素貝葉斯分類器,將它應(yīng)用于情感詞的分析處理,并取得了不錯(cuò)的效果,本文我們就來介紹一下樸素貝葉斯分類的理論基礎(chǔ)和它的實(shí)際使用。
在學(xué)習(xí)樸素貝葉斯分類以及正式開始情感詞分析之前,我們首先需要了解一下貝葉斯定理的數(shù)學(xué)基礎(chǔ)。
貝葉斯定理
貝葉斯定理是關(guān)于隨機(jī)事件A和B的條件概率的定理,公式如下:
在上面的公式中,每一項(xiàng)表示的意義如下:
- P(A):先驗(yàn)概率(prior probability),是在沒有任何條件限制下事件A發(fā)生的概率,也叫基礎(chǔ)概率,是對(duì)A事件概率的一個(gè)主觀判斷
- P(A|B):在B發(fā)生的情況下A發(fā)生的可能性,也被稱為A的后驗(yàn)概率(posterior probability)
- P(B|A):似然性,也被稱為條件似然(conditional likelihood)
- P(B):不論A是否發(fā)生,在所有情況下B發(fā)生的概率,它被稱為整體似然或歸一化常量(normalizing constant)
按照上面的解釋,貝葉斯定理可以表述為:
- 后驗(yàn)概率 = 先驗(yàn)概率 * 似然性 / 歸一化常量
通俗的來說,可以理解為當(dāng)我們不能確定某一個(gè)事件發(fā)生的概率時(shí),可以依靠與該事件本質(zhì)屬性相關(guān)的事件發(fā)生的概率去推測該事件發(fā)生的概率。用數(shù)學(xué)語言來表達(dá)就是,支持某項(xiàng)屬性的事件發(fā)生得愈多,則該事件發(fā)生的的可能性就愈大,這個(gè)推理過程也被叫做貝葉斯推理。
在查閱的一些文檔中,P(B|A)/P(B) 可以被稱為可能性函數(shù),它作為一個(gè)調(diào)整因子,表示新信息B對(duì)事件A帶來的調(diào)整,作用是將先驗(yàn)概率(主觀判斷)調(diào)整到更接近真實(shí)的概率。那么,貝葉斯定理也可以理解為:
- 新信息出現(xiàn)后A的概率 = A的先驗(yàn)概率 * 新信息帶來的調(diào)整
舉一個(gè)例子,方便大家更直觀的理解這一過程。假設(shè)統(tǒng)計(jì)了一段時(shí)間內(nèi)天氣和氣溫對(duì)于運(yùn)動(dòng)情況的影響,如下所示:
- 天氣 氣溫 運(yùn)動(dòng)
- 晴天 非常高 游泳
- 晴天 高 足球
- 陰天 中 釣魚
- 陰天 中 游泳
- 晴天 低 游泳
- 陰天 低 釣魚
現(xiàn)在請計(jì)算在晴天,氣溫適中的情況下,去游泳的概率是多少?根據(jù)貝葉斯定理,計(jì)算過程如下:
- P(游泳|晴天,中溫)=P(晴天,中溫|游泳)*P(游泳)/P(晴天,中溫)
- =P(晴天|游泳)*P(中溫|游泳)*P(游泳)/[P(晴天)*P(中溫)]
- =2/3 * 1/3 *1/2 / (1/2 *1/3 )
- =2/3
最終得出去游泳的概率是2/3,上面就是基于貝葉斯定理,根據(jù)給定的特征,計(jì)算事件發(fā)生概率大小的過程。
貝葉斯分析的思路對(duì)于由證據(jù)的積累來推測一個(gè)事物的發(fā)生的概率具有重大作用,當(dāng)我們要預(yù)測一個(gè)事物,首先會(huì)根據(jù)已有的經(jīng)驗(yàn)和知識(shí)推斷一個(gè)先驗(yàn)概率,然后在新證據(jù)不斷的積累的情況下調(diào)整這個(gè)概率。整個(gè)通過累積證據(jù)來得到一個(gè)事件發(fā)生概率的過程我們稱為貝葉斯分析。這樣,貝葉斯底層的思想就可以概括為,如果能夠掌握一個(gè)事情的全部信息,就能夠計(jì)算出它的一個(gè)客觀概率。
另外,在貝葉斯公式的基礎(chǔ)上進(jìn)行變形,可以得到下面的公式:
其中B1,B2,…,Bj是一個(gè)完備事件組,上面的公式可以表示在事件A已經(jīng)發(fā)生的條件下,尋找導(dǎo)致A發(fā)生的各種“原因”的Bi的概率。
樸素貝葉斯
在學(xué)習(xí)樸素貝葉斯之前,首先需要對(duì)貝葉斯分類進(jìn)行一下了解,貝葉斯分類通過預(yù)測一個(gè)對(duì)象屬于某個(gè)類別的概率,通過比較不同類別概率的大小預(yù)測其最可能從屬的類別,是基于貝葉斯定理而構(gòu)成出來的。在處理大規(guī)模數(shù)據(jù)集時(shí),貝葉斯分類器表現(xiàn)出較高的分類準(zhǔn)確性。
貝葉斯分類在處理一個(gè)未知類型的樣本X時(shí),可以先算出X屬于每一個(gè)類別Ci的概率 P(Ci|X),然后選擇其中概率最大的類別。假設(shè)有兩個(gè)特征變量x和y,并且存在兩個(gè)分類類別C1和C2,結(jié)合貝葉斯定理:
- 如果P(C1|x,y) > P(C2|x,y),說明在x和y發(fā)生的條件下,C1比C2發(fā)生的概率要大,那么它應(yīng)該屬于類別C1
- 反之如果P(C1|x,y) < P(C2|x,y),那么它應(yīng)該屬于類別C2
而樸素貝葉斯模型(Naive Bayesian Model)作為一種強(qiáng)大的預(yù)測建模算法,它在貝葉斯定理的基礎(chǔ)上進(jìn)行了簡化,假定了目標(biāo)的特征屬性之間相互獨(dú)立,這也是它被形容為“樸素”的原因。在實(shí)際情況中如果屬性之間存在關(guān)聯(lián),那么分類準(zhǔn)確率會(huì)降低,不過對(duì)于解決絕大部分的復(fù)雜問題非常有效。
設(shè)在樣本數(shù)據(jù)集D上,樣本數(shù)據(jù)的特征屬性集為,類變量可被分為
,即數(shù)據(jù)集D可以被分為
個(gè)類別。我們假設(shè)
相互獨(dú)立,那么由貝葉斯定理可得:
對(duì)于相同的測試樣本,分母P(X)的大小是固定不變的,因此在比較后驗(yàn)概率時(shí),我們可以只比較分子的大小即可。
在這里解釋一下貝葉斯定理、貝葉斯分類和樸素貝葉斯之間的區(qū)別,貝葉斯定理作為理論基礎(chǔ),解決了概率論中的逆概率問題,在這個(gè)基礎(chǔ)上人們設(shè)計(jì)出了貝葉斯分類器,而樸素貝葉斯是貝葉斯分類器中的一種,也是最簡單和常用的分類器,可以使用下面的圖來表示它們之間的關(guān)系:
在實(shí)際應(yīng)用中,樸素貝葉斯有廣泛的應(yīng)用,在文本分類、垃圾郵件過濾、情感預(yù)測及釣魚網(wǎng)站的檢測方面都能夠起到良好的效果。為了訓(xùn)練樸素貝葉斯模型,我們需要先在訓(xùn)練集的基礎(chǔ)上對(duì)分類好的數(shù)據(jù)進(jìn)行訓(xùn)練,計(jì)算出先驗(yàn)概率和每個(gè)屬性的條件概率,計(jì)算完成后,概率模型就可以使用貝葉斯原理對(duì)新數(shù)據(jù)進(jìn)行預(yù)測。
貝葉斯推斷與人腦的工作機(jī)制很像,這也是它為什么能夠成為機(jī)器學(xué)習(xí)的基礎(chǔ),大腦的決策過程就是先對(duì)事物進(jìn)行主觀判斷,然后搜集新的信息,優(yōu)化主觀判斷,如果新的信息符合這個(gè)主觀判斷,那就提高主觀判斷的可信度,如果不符合,就降低主觀判斷的可信度。
代碼實(shí)現(xiàn)
在對(duì)理論有了基本的了解后,我們開始分析怎樣將樸素貝葉斯應(yīng)用于我們文本處理的情感詞分析中。主要步驟如下:
- 對(duì)訓(xùn)練集和測試集完成文本分詞,并通過主觀判斷標(biāo)注所屬的分類
- 對(duì)訓(xùn)練集進(jìn)行訓(xùn)練,統(tǒng)計(jì)每個(gè)詞匯出現(xiàn)在分類下的次數(shù),計(jì)算每個(gè)類別在訓(xùn)練樣本中的出現(xiàn)頻率、及每個(gè)特征屬性對(duì)每個(gè)類別的條件概率(即似然概率)
- 將訓(xùn)練好的模型應(yīng)用于測試集的樣本上,根據(jù)貝葉斯分類計(jì)算樣本在每個(gè)分類下的概率大小
- 比較在各個(gè)分類情況下的概率大小,推測文本最可能屬于的情感分類
使用流程圖表示:
1、準(zhǔn)備階段
首先準(zhǔn)備數(shù)據(jù)集,這里使用了對(duì)某酒店的評(píng)論數(shù)據(jù),根據(jù)主觀態(tài)度將其分為“好評(píng)”或“差評(píng)”這兩類待分類項(xiàng),對(duì)每行分詞后的語句打好了情感標(biāo)簽,并且已經(jīng)提前對(duì)完整語句完成了對(duì)分詞,數(shù)據(jù)格式如下:
在每行的數(shù)據(jù)的頭部,是添加的“好評(píng)”或“差評(píng)”標(biāo)簽,標(biāo)簽與分詞采用tab分割,詞語之間使用空格分割。按照比例,將數(shù)據(jù)集的80%作為訓(xùn)練集,剩余20%作為測試集,分配過程盡量保證隨機(jī)原則。
2、訓(xùn)練階段
在訓(xùn)練階段,主要完成詞頻的統(tǒng)計(jì)工作。讀取訓(xùn)練集,統(tǒng)計(jì)出每個(gè)詞屬于該分類下出現(xiàn)的次數(shù),用于后續(xù)求解每個(gè)詞出現(xiàn)在各個(gè)類別下的概率,即詞匯與主觀分類情感之間的關(guān)系:
- private static void train(){
- Map<String,Integer> parameters = new HashMap<>();
- try(BufferedReader br = new BufferedReader(new FileReader(trainingData))){ //訓(xùn)練集數(shù)據(jù)
- String sentence;
- while(null!=(sentence=br.readLine())){
- String[] content = sentence.split("\t| "); //以tab或空格分詞
- parameters.put(content[0],parameters.getOrDefault(content[0],0)+1);
- for (int i = 1; i < content.length; i++) {
- parameters.put(content[0]+"-"+content[i], parameters.getOrDefault(content[0]+"-"+content[i], 0)+1);
- }
- }
- }catch (IOException e){
- e.printStackTrace();
- }
- saveModel(parameters);
- }
將訓(xùn)練好的模型保存到文件中,可以方便在下次使用時(shí)不用重復(fù)進(jìn)行模型的訓(xùn)練:
- private static void saveModel(Map<String,Integer> parameters){
- try(BufferedWriter bw =new BufferedWriter(new FileWriter(modelFilePath))){
- parameters.keySet().stream().forEach(key->{
- try {
- bw.append(key+"\t"+parameters.get(key)+"\r\n");
- } catch (IOException e) {
- e.printStackTrace();
- }
- });
- bw.flush();
- }catch (IOException e){
- e.printStackTrace();
- }
- }
查看保存好的模型,數(shù)據(jù)的格式如下:
- 好評(píng)-免費(fèi)送 3
- 差評(píng)-真煩 1
- 好評(píng)-禮品 3
- 差評(píng)-臟亂差 6
- 好評(píng)-解決 15
- 差評(píng)-挨宰 1
- ……
這里對(duì)訓(xùn)練的模型進(jìn)行保存,所以如果后續(xù)有同樣的分類任務(wù)時(shí),可以直接在訓(xùn)練集的基礎(chǔ)上進(jìn)行計(jì)算,對(duì)于分類速度要求較高的任務(wù),能夠有效的提高計(jì)算的速度。
3、加載模型
加載訓(xùn)練好的模型:
- private static HashMap<String, Integer> parameters = null; //用于存放模型
- private static Map<String, Double> catagory=null;
- private static String[] labels = {"好評(píng)", "差評(píng)", "總數(shù)","priorGood","priorBad"};
- private static void loadModel() throws IOException {
- parameters = new HashMap<>();
- List<String> parameterData = Files.readAllLines(Paths.get(modelFilePath));
- parameterData.stream().forEach(parameter -> {
- String[] split = parameter.split("\t");
- String key = split[0];
- int value = Integer.parseInt(split[1]);
- parameters.put(key, value);
- });
- calculateCatagory(); //分類
- }
對(duì)詞進(jìn)行分類,統(tǒng)計(jì)出好評(píng)及差評(píng)的詞頻總數(shù),并基于它們先計(jì)算得出先驗(yàn)概率:
- //計(jì)算模型中類別的總數(shù)
- public static void calculateCatagory() {
- catagory = new HashMap<>();
- double good = 0.0; //好評(píng)詞頻總數(shù)
- double bad = 0.0; //差評(píng)的詞頻總數(shù)
- double total; //總詞頻
- for (String key : parameters.keySet()) {
- Integer value = parameters.get(key);
- if (key.contains("好評(píng)-")) {
- good += value;
- } else if (key.contains("差評(píng)-")) {
- bad += value;
- }
- }
- total = good + bad;
- catagory.put(labels[0], good);
- catagory.put(labels[1], bad);
- catagory.put(labels[2], total);
- catagory.put(labels[3],good/total); //好評(píng)先驗(yàn)概率
- catagory.put(labels[4],bad/total); //差評(píng)先驗(yàn)概率
- }
查看執(zhí)行完后的統(tǒng)計(jì)值:
“好評(píng)”對(duì)應(yīng)的詞匯出現(xiàn)的總次數(shù)是46316個(gè),“差評(píng)”對(duì)應(yīng)的詞匯出現(xiàn)的總次數(shù)是77292個(gè),訓(xùn)練集詞頻總數(shù)為123608個(gè),并可基于它們計(jì)算出它們的先驗(yàn)概率:
- 該文檔屬于某個(gè)類別的條件概率= 該類別的所有詞條詞頻總數(shù) / 所有詞條的詞頻總數(shù)
4、測試階段
測試階段,加載我們提前準(zhǔn)備好的測試集,對(duì)每一行分詞后的評(píng)論語句進(jìn)行主觀情感的預(yù)測:
- private static void predictAll() {
- double accuracyCount = 0.;//準(zhǔn)確個(gè)數(shù)
- int amount = 0; //測試集數(shù)據(jù)總量
- try (BufferedWriter bw = new BufferedWriter(new FileWriter(outputFilePath))) {
- List<String> testData = Files.readAllLines(Paths.get(testFilePath)); //測試集數(shù)據(jù)
- for (String instance : testData) {
- String conclusion = instance.substring(0, instance.indexOf("\t")); //已經(jīng)打好的標(biāo)簽
- String sentence = instance.substring(instance.indexOf("\t") + 1);
- String prediction = predict(sentence); //預(yù)測結(jié)果
- bw.append(conclusion + " : " + prediction + "\r\n");
- if (conclusion.equals(prediction)) {
- accuracyCount += 1.;
- }
- amount += 1;
- }
- //計(jì)算準(zhǔn)確率
- System.out.println("accuracyCount: " + accuracyCount / amount);
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
在測試中,調(diào)用下面的predict方法進(jìn)行分類判斷。在計(jì)算前,再來回顧一下上面的公式,在程序中進(jìn)行簡化運(yùn)算:
對(duì)于同一個(gè)預(yù)測樣本,分母相同,所以我們可以只比較分子的大小。對(duì)分子部分進(jìn)行進(jìn)一步簡化,對(duì)于連乘預(yù)算,我們可以對(duì)其進(jìn)行對(duì)數(shù)操作,變成各部分相加:
這樣對(duì)于概率的大小比較,就可以簡化為比較 先驗(yàn)概率和各個(gè)似然概率分別取對(duì)數(shù)后相加的和。先驗(yàn)概率我們在之前的步驟中已經(jīng)計(jì)算完成并保存,所以這里只計(jì)算各詞匯在分類條件下的似然概率即可。predict方法的實(shí)現(xiàn)如下:
- private static String predict(String sentence) {
- String[] features = sentence.split(" ");
- String prediction;
- //分別預(yù)測好評(píng)和差評(píng)
- double good = likelihoodSum(labels[0], features) + Math.log(catagory.get(labels[3]));
- double bad = likelihoodSum(labels[1], features) + Math.log(catagory.get(labels[4]));
- return good >= bad?labels[0]:labels[1];
- }
在其中調(diào)用likelihood方法計(jì)算似然概率的對(duì)數(shù)和:
- //似然概率的計(jì)算
- public static double likelihoodSum(String label, String[] features) {
- double p = 0.0;
- Double total = catagory.get(label) + 1;//分母平滑處理
- for (String word : features) {
- Integer count = parameters.getOrDefault(label + "-" + word, 0) + 1;//分子平滑處理
- //計(jì)算在該類別的情況下是該詞的概率,用該詞的詞頻除以類別的總詞頻
- p += Math.log(count / total);
- }
- return p;
- }
在計(jì)算似然概率的方法中,如果出現(xiàn)在訓(xùn)練集中沒有包括的詞匯,那么會(huì)出現(xiàn)它的似然概率為0的情況,為了防止這種情況,對(duì)分子分母進(jìn)行了分別加1的平滑操作。
最后在主函數(shù)中調(diào)用上面的步驟,最終如果計(jì)算出基于樣本的好評(píng)概率大于等于差評(píng)概率,那么將它分類劃入“好評(píng)”,反之劃入“差評(píng)”類別,到此就完成了訓(xùn)練和測試的全過程:
- public static void main(String[] args) throws IOException {
- train();
- loadModel();
- predictAll();
- }
執(zhí)行全部代碼,結(jié)果如下,可以看到獲取了93.35%的準(zhǔn)確率。

對(duì)比最后輸出的文檔中的標(biāo)簽與預(yù)測結(jié)果,可以看到,預(yù)測結(jié)果的準(zhǔn)確度還是非常高的。
5、總結(jié)
在上面的例子中,還有一些可以進(jìn)行改進(jìn)的地方,例如可以在前期建立情感詞庫,在特征值提取的過程中只提取情感詞,去除其余無用詞匯(如介詞等)對(duì)分類的影響,只選取關(guān)鍵的代表詞作為特征提取,達(dá)到更高的分類效率。另外,可以在建立詞庫時(shí),將測試集的情感詞也放入詞庫,避免出現(xiàn)在某個(gè)分類條件下似然概率為零的情況,簡化平滑步驟。
此外,樸素貝葉斯的分類對(duì)于追加訓(xùn)練集的情況有很好的應(yīng)用,如果訓(xùn)練集不斷的增加,可以在現(xiàn)有訓(xùn)練模型的基礎(chǔ)上添加新的樣本值、或?qū)σ延械臉颖局档膶傩赃M(jìn)行修改,在此基礎(chǔ)上,可以實(shí)現(xiàn)增量的模型修改。