當前位置:成語大全網 - 成語詞典 - 求java中文分類實現過程代碼

求java中文分類實現過程代碼

前幾天讀到google研究員吳軍的數學之美系列篇,頗有感觸。而恰好自己前段時間做了個基於統計語言模型的中文切分系統的課程項目,於是乎,帖出來與大家***同學習。

分詞技術在搜索引擎,信息提取,機器翻譯等領域的重要地位與應用就不敖述了。步入正題:)

<!--[if !supportLists]-->壹、 <!--[endif]-->項目概述

本切分系統的統計語料是用我們學校自己開放的那部分,大家可以在 這裏 下載,中文字符約184萬,當然這都是已切分好了的,可以用此建立壹個比較小的語料庫。本系統我主要分下面四個步驟完成:

<!--[if !supportLists]-->1、 <!--[endif]-->語料預處理

<!--[if !supportLists]-->2、 <!--[endif]-->建立 2-gram(統計二元模型)

<!--[if !supportLists]-->3、 <!--[endif]-->實現全切分

<!--[if !supportLists]-->4、 <!--[endif]-->評估測試

下面我分別對這四個方面壹壹道來。

<!--[if !supportLists]-->1、 <!--[endif]-->語料預處理

下載的已切分的語料都是形如“19980131-04-012-001/m 現實/n 的/u 頓悟/vn 卻/d 被/p 描/v 出/v 形/Ng 來/v 。/w ” ,有的前面還保留了日期編號,因為這些切分語料的來源是人民日報。預處理主要是按標點符號分句,句子簡單定義為( 。?! : ;)這五種標點符號結尾的詞串,句子首尾分別添加<BOS>和<EOS>這兩個表示句子開始和結束的標記,這在2-gram建模時要用的,後面會提到。處理過程中,忽略詞類信息和前面的日期信息,因為我這個切分系統不考慮詞類標註。如前面這句預處理後應該為下面形式 “<BOS>現實 的 頓悟 卻 被 描 出 形 來 。<EOS>” ,當然切分詞之間妳可以用妳想用的符號標記,而不必是空格。因為考慮到所有的英文字符和數字的ASCII,我用了下面方法實現之:

out ; //輸出流

in; //輸入流

StringBuffer s1 = new StringBuffer(); //緩沖

char a = in.read();

while (a != -1) //判斷是否已到流的終點

{

if ((a == '。' || a == '?' || a == '!' || a == ':' || a == ';' )) //壹句結束

{

String s2 = new String(s1);

out.write("<BOS>"); //在句子前加 <BOS>

out.write(s2);

out.write("<EOS>"); //在句子末尾加 <EOS>

out.write('/n'); //換行

s1 = new StringBuffer();

}

else if ( a == '/')

s1 = s1.append((char)32); //分詞位置空格

else if (a > 256 )

s1 = s1.append((char)a);

a = in.read();

}

out.close();

in.close();

<!--[if !supportLists]-->2、 <!--[endif]-->建立 2-gram模型(統計二元模型)

在這裏首先簡單介紹壹下n-gram模型和2-gram模型。

根據語言樣本估計出的概率分布P就稱為語言L的語言模型。對給定的句子s = w1w2…wn,(數字,n,i都為下標,wi為句子s的壹個詞)。由鏈式規則(Chain rule),P(s) = p(w1)p(w2|w1)p(w3|w1w2)……p(wn|w1w2w3…w(n-1)) , 對p(wi|w1w2…w(i-1))而言,(w1w2…w(i-1))即為wi的歷史。考慮前面n-1個詞構成歷史的模型即為n-gram模型。 n越大,提供的語境信息也越多,但代價就越大,且需訓練語料多;n較小時,提供的信息比較少,但計算代價小,且無需太多訓練語料。

令c(w1,…,wi)表示詞串w1,w2…wi在訓練語料中出現的次數,則由最大似然估計, P(wn|w1,…,w(n-1)) = c(w1,…,wn) / c(w1,…,w(n-1)). 同理,則2-gram為 P(wn|w(n-1)) = c(w(n-1),wn) / c(w(n-1)).

若想了解更多相關知識,大家找相關資料看看,隨便把大學時的那本概率與統計課本拿出來翻翻,數學真是壹個好東東:)

回歸項目:) 訓練語料壹***有5萬多個不同的詞。建立2-gram統計模型時不斷要把每個詞在訓練語料中出現頻率統計出來,還要把每個詞及其後面的那個詞組成的2-gram在訓練語料中出現頻率統計出來。因為在切分時會頻繁的在建立的2-gram模型中查找相關的數據,所有,存儲這個2-gram模型數據的數據結構壹定要能提供高效的查找。故選擇hash表,它能提供常數時間的查找。Java類庫裏提供了HashMap類,基於數據兩還不是非常大,故可直接拿來用。在存儲時,每壹個key值對應壹個在訓練語料中出現過的詞語,而每壹個key值對應的value值又是壹個HashMap。暫且稱為子hashmap.這個結構有點類似文件結構裏的二級索引。 其相關代碼如下:

怎麽在預處理文件裏把詞分別讀出來就不羅嗦了,方法:每讀入壹行,按空格分成String數組,用個正則表達式匹配下即能得到。

//此方法傳入的兩個詞組成壹個2-gram,prewd為前壹個詞,currwd為緊隨其後的詞

public static void add(String prewd , String currwd){

String key = prewd;

String curr = currwd;

boolean bb = HMap.containsKey(key); //Hmap是壹個已存在的HashMap,用來存儲2-gram統計模型。在這裏判斷 preword 是否在 主map 中

if (bb == false) { //若 主map 中無,則添加

HashMap hm = new HashMap(); //首先,新構造壹個 子MAP

hm.put(key , new Integer(1)); //存儲 主KEY 的頻率 hm.put(curr , new Integer(1)); //存儲 主KEY 後面緊接著的那個詞頻率

HMap.put(key,hm); //將 主KEY 和對應的 子MAP 放入 主MAP 中

}

else //若 主map 中含有該詞

{

HashMap temp = (HashMap)HMap.get(key); //返回 主KEY 所對應的 子MAP ,進行值的修改

int count = ((Integer)temp.get(key)).intValue() + 1; //在 子map 中將 主key 次數加 1

temp.put(key , new Integer(count));

if (temp.containsKey(curr)) //判斷 子map 中是否含有該詞

{

int value = ((Integer)temp.get(curr)).intValue() + 1; temp.put(curr , new Integer(value));

}

else

temp.put(curr, new Integer(1)); //若無,則將其存入子map

HMap.put(key , temp); //子map 修改完畢 ,將其重新放入 主map

}

}

}

因為語言中的大部分詞屬於低頻詞,所以稀疏問題肯定存在。而MLE(最大似然估計)給在訓練語料中沒有出現的2-gram的賦給0概率。所以還得對2-gram模型進行數據平滑,以期得到更好的參數。目前平滑技術比較多,如Add-one,Add-delta,Witten-Bell,held-out留存平滑等。本系統主要采用了Add-delta和held-out兩中平滑方式,下面就Add-delta平滑技術為例,對2-gram進行平滑。對2-gram模型,其平滑公式為:

P(wn|w(n-1)) = [c(w(n-1),wn) + delta ] / ( N + delta * V) ,這裏去delta為0.5

其中,N:訓練語料中所有的2-gram的數量

V:所有的可能的不同的2-gram的數量

平滑思路 :1.產生主hashmap的叠代器iterator,依次讀key;

2.對每壹個key,又讀出其value,即壹個子hashmap;

3.然後根據平滑公式對子map裏的值進行計算修改

算法框架:

Iterator it = 主hashmap.keySet().iterator();

While(it.hasNext())

{

主key = it.next();

子hashmap = (HashMap)主hashmap.get(主key);

Iterator itr = 子hashmap.keySet().iterator();

While(itr.hasNext())

{

根據平滑公式依次計算修改

}

}

註意問題:1.因為計算得出的概率值壹般都比較小,為了防止出現下溢,可對其取對數,再取反。

2.每壹個主key所對應的所有沒有出現過的,即頻率為零的2-gram,統壹用壹個鍵值對存儲在相應的子hashmap裏即可。

完畢,對象序列化。使用該系統時,lazy load將其載入內存,然後可讓其壹直存活在內存,這會大大加快速度。

到此,2-gram模型建立完畢。

<!--[if !supportLists]--> 3、 <!--[endif]-->全切分實現

切詞壹般有最大匹配法(MM、RMM),基於規則的方法,基於統計的方法。關於前兩者就不羅嗦了。所謂全切分就是要根據字典得到所以可能的切分形式。歧義識別的方法主要有:基於規則的方法和基於統計的方法。這裏當然是采用基於2-gram統計模型的方法了:)為了避免切分後再進行歧義分析的時間浪費。並且這裏采用邊切分邊評價的方法,即在切分進行的同時進行評價的方法。

對壹個句子進行全切分的結果,即所以可能的組合,可以形成壹棵解空間樹

於是,可用回溯法搜索最優解

若將所有的全切分組合先搜索出來,然後再根據2-gram選擇最佳,顯然會很浪費時間,因為過程中可能存在很多的重復搜索,而回溯搜索的時間復雜度為指數時間

所以,在搜索過程中要結合 剪枝,避免無效搜索,可很大提高效率

采用樹的深度優先法則。可找到最優解

具體算法如下:

Stack.push(BOS) //樹節點

while stack不為空

x=stack.pop()

pos:=x.Pos, w = x.w oldvalue:= x.value preword:=x.preword

if m>O then //m為首詞串的個數

forj:=1 to m do

FWj為fwc的第j個元素l

if length(w+FWj) =length(c)且概率最大 then output w+FWjl且設置最新的句子最大概率值

else

posl:=pos+length(FWj)l

if probability(w+FWj,posl,newsate)>maxValue(pos1)

stack.push(x)

endif

endfor

endif

endwhile

end.

在算法實現過程中需要考慮壹些諸如樹節點保存,首詞串處理等問題。

4.評估測試

環境:windows XP2, AMD Athlon 1800+, Memory 768m,JDK1.5

Delta平滑:隨著delta的取值變小,準確率上升,0.5,0.01,0.0001

召回率: 0.9756 0.9826 0.9928

準確率: 0.9638 0.9710 0.9883

留存平滑

召回率: 0.9946

準確率: 0.9902

壹般情況下,留存平滑應該還是比delta平滑更好

所有建模過程及平滑過程在1分鐘內都可完成。

切分時間與效率:

<!--[if !supportLists]-->n <!--[endif]-->測試語料,17455字符, (中文17287),平均句長 41個字,時間 :340ms, 平均切分速度:5.1 萬/S

<!--[if !supportLists]-->n <!--[endif]-->20.5萬測試語料(取自笑傲江湖), 預處理後 17.46萬 ,時間 110 MS,句子文本行數目 24945,平均句長 7 , 切分時間 1300MS , 平均13.46 萬 / 秒

<!--[if !supportLists]-->n <!--[endif]-->20.5萬測試語料(取自笑傲江湖),不預處理,平均句長 239 ,切分時間40S, 平均 5000字/秒

回溯算法是時間開銷為O(N!),所以在切分過程中句子長度直接決定了切分的速度,因為句子越長詞越多

經過預處理,句子短,平均句長 7, 回溯短,故速度要快很多。

到此,該系統基本完成,告壹段落。感覺寫的挺亂的呵呵

現在在做另壹個作業,做個簡單搜索引擎,準備把這個東東結合在搜索引擎裏面,實現切分功能:)