前言
目前做分詞比較流行的是用深度學習來做,比如用迴圈神經網路和條件隨機場,也有直接用條件隨機場或隱馬爾科夫模型的。前面也實現過上面幾種,效果挺不錯,基於隱馬爾科夫模型的差一點,條件隨機場的效果較好,而加入深度學習的效果最好。
而最最傳統的分詞做法很多都是基於字典的,然後通過最大匹配法匹配,效果比較一般。效果雖然一般,但我們還是看下怎麼實現的吧。
Trie樹結構
Trie 是一種搜尋樹,它的 key 都為字串,通過 key 可以找到 value。能做到高效查詢和插入,時間複雜度為O(k),缺點是耗記憶體。它的核心思想就是減少沒必要的字元比較,使查詢高效率,即用空間換時間,再利用共同字首來提高查詢效率。
Trie樹的根節點不包含字元,根節點到某節點的路徑連起來的字串為該節點對應的字串,每個節點只包含一個字元,此外,任意節點的所有子節點的字元都不相同。
比如如下,將五個詞語新增到Trie樹中,最後的結構如圖所示。
TrieTree tree = new TrieTree();
tree.put("美利堅");
tree.put("美麗");
tree.put("金幣");
tree.put("金子");
tree.put("帝王");
複製程式碼
Github
https://github.com/sea-boat/TextAnalyzer/blob/master/src/main/java/com/seaboat/text/analyzer/segment/
效果
可以看到基於字典的分詞效果是存在缺點的,需要用機器學習進一步優化。
DictSegment segment = new DictSegment();
System.out.println(segment.seg("我是中國人"));
System.out.println(segment.seg("人工智慧是什麼"));
System.out.println(segment.seg("北京網際網路違法和不良資訊舉報中心"));
複製程式碼
[我, 是, 中國人]
[人工智慧, 是, 什麼]
[北京, 網際網路, 違法, 和不, 良, 資訊, 舉報中心]
複製程式碼
簡易實現
定義一個節點類代表Trie樹節點,包含若干子節點、值和刪除標記。getChild
方法用於遍歷該節點下的指定字元的子節點,allChildrenDeleted
方法用於檢測節點下的子節點是否已被刪除了,setChild
方法用於將子節點設定到某個節點上。
public class TrieNode {
private TrieNode[] children;
private String value;
private boolean deleted = false;
public TrieNode(String value) {
this.value = value == null ? null : value.intern();
}
public boolean isEmpty() {
return this.value == null && this.children == null;
}
public TrieNode[] getChildren() {
return children;
}
public TrieNode getChild(String word) {
if (children == null)
return null;
for (TrieNode c : children) {
if (c.getValue() == word.intern() && !c.deleted)
return c;
}
return null;
}
public boolean allChildrenDeleted() {
if (children == null)
return true;
for (TrieNode c : children) {
if (!c.deleted)
return false;
}
return true;
}
public void setChild(TrieNode child) {
if (children == null) {
children = new TrieNode[1];
children[0] = child;
} else {
TrieNode[] temp = children;
children = new TrieNode[temp.length + 1];
System.arraycopy(temp, 0, children, 0, temp.length);
children[children.length - 1] = child;
}
}
}
複製程式碼
定義一個 TrieTree 類代表樹物件,包含了樹的根節點。put
方法用於將字串放到樹結構中,需要先遍歷檢測是否已經有字串字首,沒有則要建立對應的節點,然後新增到對應節點的子節點中。get
和remove
操作都需要針對樹結構做處理,最終完成查詢和刪除,刪除操作為了方便僅僅是設定下指定節點的刪除標識。
public class TrieTree {
protected TrieNode root;
public TrieTree() {
this.root = new TrieNode(null);
}
public void put(String word) throws IllegalArgumentException {
if (word == null) {
throw new IllegalArgumentException();
}
TrieNode current = this.root;
for (String s : word.split("")) {
TrieNode child = current.getChild(s);
if (child == null) {
child = new TrieNode(s);
current.setChild(child);
}
current = child;
}
}
public TrieNode get(String word) throws IllegalArgumentException {
if (word == null) {
throw new IllegalArgumentException();
}
TrieNode current = this.root;
for (String s : word.split("")) {
TrieNode child = current.getChild(s);
if (child == null)
return null;
current = child;
}
return current;
}
public void remove(String word) {
if (word == null || word.length() <= 0) {
return;
}
for (int i = 0; i < word.length(); i++) {
String sub_word = word.substring(0, word.length() - i);
TrieNode current = this.root;
for (String s : sub_word.split("")) {
TrieNode child = current.getChild(s);
if (child != null && (child.getChildren() == null || child.allChildrenDeleted()))
child.setDeleted(true);
current = child;
}
}
}
}
複製程式碼
seg
為分詞方法,它主要就是嘗試進行最大字串匹配,儘量匹配字典中最長詞,其中查詢是否存在字串在teri樹中查詢。
public List<String> seg(String text) {
int flag = 0;
int delta = 1;
List<String> words = new ArrayList<String>();
while (flag + delta <= text.length()) {
String temp = text.substring(flag, flag + delta);
if (tree.get(temp) != null) {
if ((flag + delta) == text.length()) {
words.add(temp);
break;
}
delta++;
continue;
}
words.add(temp.substring(0, temp.length() - 1));
flag = flag + delta - 1;
delta = 1;
}
return words;
}
複製程式碼
-------------推薦閱讀------------
跟我交流,向我提問:
公眾號的選單已分為“讀書總結”、“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。
歡迎關注: