JieBa使用java
List<SegToken> process = segmenter.process("今天早上,出門的的時候,天氣很好", JiebaSegmenter.SegMode.INDEX);
for (SegToken token:process){
//分詞的結果
System.out.println( token.word);
}
複製代碼
輸出內容以下node
今天
早上
,
出門
的
的
時候
,
天氣
很
好
複製代碼
int N = chars.length; //獲取整個句子的長度
int i = 0, j = 0; //i 表示詞的開始 ;j 表示詞的結束
while (i < N) {
Hit hit = trie.match(chars, i, j - i + 1); //從trie樹中匹配
if (hit.isPrefix() || hit.isMatch()) {
if (hit.isMatch()) {
//徹底匹配
if (!dag.containsKey(i)) {
List<Integer> value = new ArrayList<Integer>();
dag.put(i, value);
value.add(j);
}
else
dag.get(i).add(j); //以當前字符開頭的詞有哪些,詞結尾的座標記下來
}
j += 1;
if (j >= N) {
//以當前字符開頭的全部詞已經匹配完成,再以當前字符的下一個字符開頭尋找詞
i += 1;
j = i;
}
}
else {
//以當前字符開頭的詞已經所有匹配完成,再以當前字符的下一個字符開頭尋找詞
i += 1;
j = i;
}
}
複製代碼
好比輸入的是 "今天早上"git
今/今天/早/早上/上
複製代碼
JieBa內部存儲了一個文件dict.txt,好比記錄了 X光線 3 n
。在內部的存儲trie樹結構則爲github
nodeState:當前DictSegment狀態 ,默認 0 , 1表示從根節點到當前節點的路徑表示一個詞 ,好比 x光和 x光線算法
storeSize:當前節點存儲的Segment數目數組
好比除了x光線以外,還有x射bash
storeSize <=ARRAY_LENGTH_LIMIT ,使用數組存儲, storeSize >ARRAY_LENGTH_LIMIT,則使用Map存儲 ,取值爲3app
核心代碼以下ui
for (int i = N - 1; i > -1; i--) {
//從右往左去查看句子,這是由於中文的重點通常在後面
//表示詞的開始位置
Pair<Integer> candidate = null;
for (Integer x : dag.get(i)) {
//x表示詞的結束位置
// wordDict.getFreq表示獲取trie這個詞的頻率
//route.get(x+1)表示當前詞的後一個詞的機率
//因爲頻率自己存儲的是數學上log計算後的值,這裏的加法其實就是當前這個詞爲A而且後面緊跟着的詞爲B的機率,B已經由前面算出
double freq = wordDict.getFreq(sentence.substring(i, x + 1)) + route.get(x + 1).freq;
if (null == candidate) {
candidate = new Pair<Integer>(x, freq);
}
else if (candidate.freq < freq) {
//保存機率高的詞
candidate.freq = freq;
candidate.key = x;
}
}
//可見route中存儲的數據爲key:詞頭下標 value:詞尾下標,詞的頻率
route.put(i, candidate);
}
複製代碼
高頻詞選取過程:spa
(3,<3,-5.45>) :第一個3是詞頭,第二個3是 '上' 的詞尾下標;-5.45是它出現的機率;
(4,<0,0>):初始機率
此時 route保留了 (3,<3,-5.45>)、(4,<0,0>)和(2, <3,-10.81> )
依此類推,通過route以後的取詞以下
取完了高頻詞以後,核心邏輯以下
while (x < N) {
//獲取當前字符開頭的詞的詞尾
y = route.get(x).key + 1;
String lWord = sentence.substring(x, y);
if (y - x == 1)
sb.append(lWord); //單個字符成詞,先保留
else {
if (sb.length() > 0) {
buf = sb.toString();
sb = new StringBuilder();
if (buf.length() == 1) {
tokens.add(buf);
}
else {
if (wordDict.containsWord(buf)) {
tokens.add(buf); //多個字符而且字典中存在,做爲分詞的結果
}
else {
finalSeg.cut(buf, tokens);
}
}
}
//保留多個字符組成的詞
tokens.add(lWord);
}
x = y; //從當前詞的詞尾開始找下一個詞
}
複製代碼
詞提取的過程
至此 '今天早上' 這句話分詞結束。能夠看到這都是創建在這個詞已經存在於字典的基礎上成立的。
若是出現了多個單個字成詞的狀況,好比 '出門的的時候' 中的 '的',一方面它成爲了單個的詞,另外一方面後面緊跟着的 '的'與它一塊兒成爲了兩個字符組成的詞,又在詞典中不存在 '的的' ,於是識別爲未知的詞,調用 finalSeg.cut
使用的方法爲Viterbi算法。首先預加載以下HMM模型的三組機率集合和隱藏狀態集合
未知的詞定義了4個隱藏狀態。 B 表示詞的開始 M 表示詞的中間 E 表示詞的結束 S 表示單字成詞
初始化每一個隱藏狀態的初始機率
start.put('B', -0.26268660809250016);
start.put('E', -3.14e+100);
start.put('M', -3.14e+100);
start.put('S', -1.4652633398537678);
複製代碼
初始化狀態轉移矩陣
trans = new HashMap<Character, Map<Character, Double>>();
Map<Character, Double> transB = new HashMap<Character, Double>();
transB.put('E', -0.510825623765990);
transB.put('M', -0.916290731874155);
trans.put('B', transB);
Map<Character, Double> transE = new HashMap<Character, Double>();
transE.put('B', -0.5897149736854513);
transE.put('S', -0.8085250474669937);
trans.put('E', transE);
Map<Character, Double> transM = new HashMap<Character, Double>();
transM.put('E', -0.33344856811948514);
transM.put('M', -1.2603623820268226);
trans.put('M', transM);
Map<Character, Double> transS = new HashMap<Character, Double>();
transS.put('B', -0.7211965654669841);
transS.put('S', -0.6658631448798212);
trans.put('S', transS);
複製代碼
好比trans.get('S').get('B')表示若是當前字符是 'S',那麼下個是另外一個詞(非單字成詞)開始的機率爲 -0.721
讀取實現準備好的混淆矩陣,存入 emit中
另外它預約義了每一個隱藏狀態以前只能是那些狀態
prevStatus.put('B', new char[] { 'E', 'S' }); prevStatus.put('M', new char[] { 'M', 'B' }); prevStatus.put('S', new char[] { 'S', 'E' }); prevStatus.put('E', new char[] { 'B', 'M' }); 複製代碼
好比 'M' 它的前面一定是 'M' 和 'B' 之間的一個
算法的流程以下:
for (char state : states) {
Double emP = emit.get(state).get(sentence.charAt(0));
if (null == emP)
emP = MIN_FLOAT;
//存儲第一個字符 是 'B' 'E' 'M' 'S'的機率,即初始化轉移機率
v.get(0).put(state, start.get(state) + emP);
path.put(state, new Node(state, null));
}
複製代碼
for (int i = 1; i < sentence.length(); ++i) {
Map<Character, Double> vv = new HashMap<Character, Double>();
v.add(vv);
Map<Character, Node> newPath = new HashMap<Character, Node>();
for (char y : states) {
//y表示隱藏狀態
//emp是獲取混淆矩陣的機率,好比 在 'B'發生的狀況下,觀察到字符 '要' 的機率
Double emp = emit.get(y).get(sentence.charAt(i));
if (emp == null)
emp = MIN_FLOAT; //樣本中沒有,就設置爲最小的機率
Pair<Character> candidate = null;
for (char y0 : prevStatus.get(y)) {
Double tranp = trans.get(y0).get(y);//獲取狀態轉移機率,好比 E -> B
if (null == tranp)
tranp = MIN_FLOAT; //轉移機率不存在,取最低的
//v中放的是當前字符的前一個字符的機率,即前一個狀態的最優解
//tranp 是狀態轉移的機率
//三者相加即計算已知觀察序列和HMM的條件下,求得最可能的隱藏序列的機率
tranp += (emp + v.get(i - 1).get(y0));
if (null == candidate)
candidate = new Pair<Character>(y0, tranp);
else if (candidate.freq <= tranp) {
//存儲最優可能的隱藏機率
candidate.freq = tranp;
candidate.key = y0;
}
}
//存儲是'B'仍是 'E'各自的機率
vv.put(y, candidate.freq);
//記下先後兩個詞最優的路徑,以便還原原始的隱藏狀態分隔點
newPath.put(y, new Node(y, path.get(candidate.key)));
}
//存儲最終句子的最優路徑
path = newPath;
}
複製代碼
double probE = v.get(sentence.length() - 1).get('E');
double probS = v.get(sentence.length() - 1).get('S');
Vector<Character> posList = new Vector<Character>(sentence.length());
Node win;
if (probE < probS)
win = path.get('S');
else
win = path.get('E');
while (win != null) {
//沿着指針找到句子的每一個字符的個子位置
posList.add(win.value);
win = win.parent;
}
Collections.reverse(posList);
複製代碼
int begin = 0, next = 0;
for (int i = 0; i < sentence.length(); ++i) {
char pos = posList.get(i);
if (pos == 'B')
begin = i;
else if (pos == 'E') {
//到詞尾了,記下
tokens.add(sentence.substring(begin, i + 1));
next = i + 1;
}
else if (pos == 'S') {
//單個字成詞,記下
tokens.add(sentence.substring(i, i + 1));
next = i + 1;
}
}
if (next < sentence.length())
tokens.add(sentence.substring(next));
複製代碼
自此執行結束