轉自 博客園 洞庭散人的博客
最近在看貝葉斯分類,然後網上發現這篇博客,代碼寫的很容易理解,轉載保存。
Preface
文本的分類和聚類是一個比較有意思的話題,我以前也寫過一篇blog《基於K-Means的文本聚類算法》,加上最近讀了幾本數據挖掘和機器學習的書籍,因此很想寫點東西來記錄下學習的所得。
在本文的上半部分《基於樸素貝葉斯分類器的文本分類算法(上)》一文中簡單介紹了貝葉斯學習的基本理論,這一篇將展示如何將該理論運用到中文文本分類中來,具體的文本分類原理就不再介紹了,在上半部分有,也可以參見代碼的註釋。
文本特徵向量
文本特徵向量可以描述爲文本中的字/詞構成的屬性。例如給出文本:
Good good study,Day day up.
可以獲得該文本的特徵向量集:{ Good, good, study, Day, day , up.}
樸素貝葉斯模型是文本分類模型中的一種簡單但性能優越的的分類模型。爲了簡化計算過程,假定各待分類文本特徵變量是相互獨立的,即“樸素貝葉斯模型的假設”。相互獨立表明了所有特徵變量之間的表述是沒有關聯的。如上例中,[good]和[study]這兩個特徵變量就是沒有任何關聯的。
在上例中,文本是英文,但由於中文本身是沒有自然分割符(如空格之類符號),所以要獲得中文文本的特徵變量向量首先需要對文本進行中文分詞
中文分詞
這裏採用極易中文分詞組件,這個中文分詞組件可以免費使用,提供Lucene接口,跨平臺,性能可靠。
package com.vista;
import java.io.IOException;
import jeasy.analysis.MMAnalyzer;
/**
* 中文分詞器
*/
public class ChineseSpliter
{
/**
* 對給定的文本進行中文分詞
* @param text 給定的文本
* @param splitToken 用於分割的標記,如"|"
* @return 分詞完畢的文本
*/
public static String split(String text,String splitToken)
{
String result = null;
MMAnalyzer analyzer = new MMAnalyzer();
try
{
result = analyzer.segment(text, splitToken);
}
catch (IOException e)
{
e.printStackTrace();
}
return result;
}
}
停用詞處理
去掉文檔中無意思的詞語也是必須的一項工作,這裏簡單的定義了一些常見的停用詞,並根據這些常用停用詞在分詞時進行判斷。
package com.vista;
/**
* 停用詞處理器
* @author phinecos
*
*/
public class StopWordsHandler
{
private static String stopWordsList[] ={"的", "我們","要","自己","之","將","“","”",",","(",")","後","應","到","某","後","個","是","位","新","一","兩","在","中","或","有","更","好",""};//常用停用詞
public static boolean IsStopWord(String word)
{
for(int i=0;i<stopWordsList.length;++i)
{
if(word.equalsIgnoreCase(stopWordsList[i]))
return true;
}
return false;
}
}
訓練集管理器
我們的系統首先需要從訓練樣本集中得到假設的先驗概率和給定假設下觀察到不同數據的概率。
package com.vista;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 訓練集管理器
*/
public class TrainingDataManager
{
private String[] traningFileClassifications;//訓練語料分類集合
private File traningTextDir;//訓練語料存放目錄
private static String defaultPath = "D:\\TrainningSet";
public TrainingDataManager()
{
traningTextDir = new File(defaultPath);
if (!traningTextDir.isDirectory())
{
throw new IllegalArgumentException("訓練語料庫搜索失敗! [" +defaultPath + "]");
}
this.traningFileClassifications = traningTextDir.list();
}
/**
* 返回訓練文本類別,這個類別就是目錄名
* @return 訓練文本類別
*/
public String[] getTraningClassifications()
{
return this.traningFileClassifications;
}
/**
* 根據訓練文本類別返回這個類別下的所有訓練文本路徑(full path)
* @param classification 給定的分類
* @return 給定分類下所有文件的路徑(full path)
*/
public String[] getFilesPath(String classification)
{
File classDir = new File(traningTextDir.getPath() +File.separator +classification);
String[] ret = classDir.list();
for (int i = 0; i < ret.length; i++)
{
ret[i] = traningTextDir.getPath() +File.separator +classification +File.separator +ret[i];
}
return ret;
}
/**
* 返回給定路徑的文本文件內容
* @param filePath 給定的文本文件路徑
* @return 文本內容
* @throws java.io.FileNotFoundException
* @throws java.io.IOException
*/
public static String getText(String filePath) throws FileNotFoundException,IOException
{
InputStreamReader isReader =new InputStreamReader(new FileInputStream(filePath),"GBK");
BufferedReader reader = new BufferedReader(isReader);
String aline;
StringBuilder sb = new StringBuilder();
while ((aline = reader.readLine()) != null)
{
sb.append(aline + " ");
}
isReader.close();
reader.close();
return sb.toString();
}
/**
* 返回訓練文本集中所有的文本數目
* @return 訓練文本集中所有的文本數目
*/
public int getTrainingFileCount()
{
int ret = 0;
for (int i = 0; i < traningFileClassifications.length; i++)
{
ret +=getTrainingFileCountOfClassification(traningFileClassifications[i]);
}
return ret;
}
/**
* 返回訓練文本集中在給定分類下的訓練文本數目
* @param classification 給定的分類
* @return 訓練文本集中在給定分類下的訓練文本數目
*/
public int getTrainingFileCountOfClassification(String classification)
{
File classDir = new File(traningTextDir.getPath() +File.separator +classification);
return classDir.list().length;
}
/**
* 返回給定分類中包含關鍵字/詞的訓練文本的數目
* @param classification 給定的分類
* @param key 給定的關鍵字/詞
* @return 給定分類中包含關鍵字/詞的訓練文本的數目
*/
public int getCountContainKeyOfClassification(String classification,String key)
{
int ret = 0;
try
{
String[] filePath = getFilesPath(classification);
for (int j = 0; j < filePath.length; j++)
{
String text = getText(filePath[j]);
if (text.contains(key))
{
ret++;
}
}
}
catch (FileNotFoundException ex)
{
Logger.getLogger(TrainingDataManager.class.getName()).log(Level.SEVERE, null,ex);
}
catch (IOException ex)
{
Logger.getLogger(TrainingDataManager.class.getName()).log(Level.SEVERE, null,ex);
}
return ret;
}
}
先驗概率
先驗概率是我們需要計算的兩大概率值之一
package com.vista;
/**
* 先驗概率計算
* <h3>先驗概率計算</h3>
* P(c<sub>j</sub>)=N(C=c<sub>j</sub>)<b>/</b>N <br>
* 其中,N(C=c<sub>j</sub>)表示類別c<sub>j</sub>中的訓練文本數量;
* N表示訓練文本集總數量。
*/
public class PriorProbability
{
private static TrainingDataManager tdm =new TrainingDataManager();
/**
* 先驗概率
* @param c 給定的分類
* @return 給定條件下的先驗概率
*/
public static float calculatePc(String c)
{
float ret = 0F;
float Nc = tdm.getTrainingFileCountOfClassification(c);
float N = tdm.getTrainingFileCount();
ret = Nc / N;
return ret;
}
}
分類條件概率
這是另一個影響因子,和先驗概率一起來決定最終結果
package com.vista;
/**
* <b>類</b>條件概率計算
*
* <h3>類條件概率</h3>
* P(x<sub>j</sub>|c<sub>j</sub>)=( N(X=x<sub>i</sub>, C=c<sub>j
* </sub>)+1 ) <b>/</b> ( N(C=c<sub>j</sub>)+M+V ) <br>
* 其中,N(X=x<sub>i</sub>, C=c<sub>j</sub>)表示類別c<sub>j</sub>中包含屬性x<sub>
* i</sub>的訓練文本數量;N(C=c<sub>j</sub>)表示類別c<sub>j</sub>中的訓練文本數量;M值用於避免
* N(X=x<sub>i</sub>, C=c<sub>j</sub>)過小所引發的問題;V表示類別的總數。
*
* <h3>條件概率</h3>
* <b>定義</b> 設A, B是兩個事件,且P(A)>0 稱<br>
* <tt>P(B∣A)=P(AB)/P(A)</tt><br>
* 爲在條件A下發生的條件事件B發生的條件概率。
*/
public class ClassConditionalProbability
{
private static TrainingDataManager tdm = new TrainingDataManager();
private static final float M = 0F;
/**
* 計算類條件概率
* @param x 給定的文本屬性
* @param c 給定的分類
* @return 給定條件下的類條件概率
*/
public static float calculatePxc(String x, String c)
{
float ret = 0F;
float Nxc = tdm.getCountContainKeyOfClassification(c, x);
float Nc = tdm.getTrainingFileCountOfClassification(c);
float V = tdm.getTraningClassifications().length;
ret = (Nxc + 1) / (Nc + M + V); //爲了避免出現0這樣極端情況,進行加權處理
return ret;
}
}
分類結果
用來保存各個分類及其計算出的概率值,
package com.vista;
/**
* 分類結果
*/
public class ClassifyResult
{
public double probility;//分類的概率
public String classification;//分類
public ClassifyResult()
{
this.probility = 0;
this.classification = null;
}
}
樸素貝葉斯分類器
利用樣本數據集計算先驗概率和各個文本向量屬性在分類中的條件概率,從而計算出各個概率值,最後對各個概率值進行排序,選出最大的概率值,即爲所屬的分類。
package com.vista;
import com.vista.ChineseSpliter;
import com.vista.ClassConditionalProbability;
import com.vista.PriorProbability;
import com.vista.TrainingDataManager;
import com.vista.StopWordsHandler;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Vector;
/**
* 樸素貝葉斯分類器
*/
public class BayesClassifier
{
private TrainingDataManager tdm;//訓練集管理器
private String trainnigDataPath;//訓練集路徑
private static double zoomFactor = 10.0f;
/**
* 默認的構造器,初始化訓練集
*/
public BayesClassifier()
{
tdm =new TrainingDataManager();
}
/**
* 計算給定的文本屬性向量X在給定的分類Cj中的類條件概率
* <code>ClassConditionalProbability</code>連乘值
* @param X 給定的文本屬性向量
* @param Cj 給定的類別
* @return 分類條件概率連乘值,即<br>
*/
float calcProd(String[] X, String Cj)
{
float ret = 1.0F;
// 類條件概率連乘
for (int i = 0; i <X.length; i++)
{
String Xi = X[i];
//因爲結果過小,因此在連乘之前放大10倍,這對最終結果並無影響,因爲我們只是比較概率大小而已
ret *=ClassConditionalProbability.calculatePxc(Xi, Cj)*zoomFactor;
}
// 再乘以先驗概率
ret *= PriorProbability.calculatePc(Cj);
return ret;
}
/**
* 去掉停用詞
* @param text 給定的文本
* @return 去停用詞後結果
*/
public String[] DropStopWords(String[] oldWords)
{
Vector<String> v1 = new Vector<String>();
for(int i=0;i<oldWords.length;++i)
{
if(StopWordsHandler.IsStopWord(oldWords[i])==false)
{//不是停用詞
v1.add(oldWords[i]);
}
}
String[] newWords = new String[v1.size()];
v1.toArray(newWords);
return newWords;
}
/**
* 對給定的文本進行分類
* @param text 給定的文本
* @return 分類結果
*/
@SuppressWarnings("unchecked")
public String classify(String text)
{
String[] terms = null;
terms= ChineseSpliter.split(text, " ").split(" ");//中文分詞處理(分詞後結果可能還包含有停用詞)
terms = DropStopWords(terms);//去掉停用詞,以免影響分類
String[] Classes = tdm.getTraningClassifications();//分類
float probility = 0.0F;
List<ClassifyResult> crs = new ArrayList<ClassifyResult>();//分類結果
for (int i = 0; i <Classes.length; i++)
{
String Ci = Classes[i];//第i個分類
probility = calcProd(terms, Ci);//計算給定的文本屬性向量terms在給定的分類Ci中的分類條件概率
//保存分類結果
ClassifyResult cr = new ClassifyResult();
cr.classification = Ci;//分類
cr.probility = probility;//關鍵字在分類的條件概率
System.out.println("In process.");
System.out.println(Ci + ":" + probility);
crs.add(cr);
}
//對最後概率結果進行排序
java.util.Collections.sort(crs,new Comparator()
{
public int compare(final Object o1,final Object o2)
{
final ClassifyResult m1 = (ClassifyResult) o1;
final ClassifyResult m2 = (ClassifyResult) o2;
final double ret = m1.probility - m2.probility;
if (ret < 0)
{
return 1;
}
else
{
return -1;
}
}
});
//返回概率最大的分類
return crs.get(0).classification;
}
public static void main(String[] args)
{
String text = "微軟公司提出以446億美元的價格收購雅虎中國網2月1日報道 美聯社消息,微軟公司提出以446億美元現金加股票的價格收購搜索網站雅虎公司。微軟提出以每股31美元的價格收購雅虎。微軟的收購報價較雅虎1月31日的收盤價19.18美元溢價62%。微軟公司稱雅虎公司的股東可以選擇以現金或股票進行交易。微軟和雅虎公司在2006年底和2007年初已在尋求雙方合作。而近兩年,雅虎一直處於困境:市場份額下滑、運營業績不佳、股價大幅下跌。對於力圖在互聯網市場有所作爲的微軟來說,收購雅虎無疑是一條捷徑,因爲雙方具有非常強的互補性。(小橋)";
BayesClassifier classifier = new BayesClassifier();//構造Bayes分類器
String result = classifier.classify(text);//進行分類
System.out.println("此項屬於["+result+"]");
}
}
訓練集與分類測試
作爲測試,這裏選用Sogou實驗室的文本分類數據,我只使用了mini版本。迷你版本有10個類別
,共計100篇文章,總大小244KB
使用的測試文本:
微軟公司提出以446億美元的價格收購雅虎
中國網2月1日報道 美聯社消息,微軟公司提出以446億美元現金加股票的價格收購搜索網站雅虎公司。
微軟提出以每股31美元的價格收購雅虎。微軟的收購報價較雅虎1月31日的收盤價19.18美元溢價62%。微軟公司稱雅虎公司的股東可以選擇以現金或股票進行交易。
微軟和雅虎公司在2006年底和2007年初已在尋求雙方合作。而近兩年,雅虎一直處於困境:市場份額下滑、運營業績不佳、股價大幅下跌。對於力圖在互聯網市場有所作爲的微軟來說,收購雅虎無疑是一條捷徑,因爲雙方具有非常強的互補性。(小橋)
使用mini版本的測試結果:
In process.
IT:2.8119528E-5
In process.
體育:2.791735E-21
In process.
健康:3.3188528E-12
In process.
軍事:2.532662E-19
In process.
招聘:2.3753596E-17
In process.
教育:4.2023427E-19
In process.
文化:6.0595915E-23
In process.
旅遊:5.1286412E-17
In process.
汽車:4.085446E-8
In process.
財經:3.7337095E-10
此項屬於[IT]