HanLP二元核心詞典詳細解析
本文分析: HanLP版本1.5.3中二元核心詞典的儲存與查詢。當詞典檔案沒有被快取時,會從文字檔案CoreNatureDictionary.ngram.txt中解析出來儲存到TreeMap中,然後構造start和pair陣列,並基於這兩個陣列實現詞共現頻率的二分查詢。當已經有快取bin檔案時,那直接讀取構建start和pair陣列,速度超快。
原始碼實現
二元核心詞典的載入
二元核心詞典在檔案: CoreNatureDictionary.ngram.txt,約有46.3 MB。程式啟動時先嚐試載入CoreNatureDictionary.ngram.txt.table.bin 快取檔案,大約22.9 MB。這個快取檔案是序列化儲存起來的。
ObjectInputStream in = new ObjectInputStream(IOUtil.newInputStream(path));
start = (int[]) in.readObject();
pair = (int[]) in.readObject();
當快取檔案不存在時,丟擲異常:警告 : 嘗試載入快取檔案E:/idea/hanlp/HanLP/data/dictionary/CoreNatureDictionary.ngram.txt.table.bin發生異常[java.io.FileNotFoundException: 然後解析CoreNatureDictionary.ngram.txt
br = new BufferedReader(new InputStreamReader(IOUtil.newInputStream(path), "UTF-8"));
while ((line = br.readLine()) != null){
String[] params = line.split("\\s");
String[] twoWord = params[0].split("@", 2);
...
}
然後,使用一個 TreeMap<Integer, TreeMap<Integer, Integer>> map來儲存解析的每一行二元核心詞典條目。
TreeMap<Integer, TreeMap<Integer, Integer>> map = new TreeMap<Integer, TreeMap<Integer, Integer>>();
int idA = CoreDictionary.trie.exactMatchSearch(a);//二元接續的 @ 前的內容
int idB = CoreDictionary.trie.exactMatchSearch(b);//@ 後的內容
TreeMap<Integer, Integer> biMap = map.get(idA);
if (biMap == null){
biMap = new TreeMap<Integer, Integer>();
map.put(idA, biMap);//
}
biMap.put(idB, freq);
比如二元接續: “一 一@中”,@ 前的內容是:“一 一”,@後的內容是 “中”。由於同一個字首可以有多個後續,比如:
一一 @中 1
一一 @為 6
一一 @交談 1
所有以 '一 一' 開頭的 @ 後的字尾 以及對應的頻率 都儲存到 相應的biMap中:biMap.put(idB, freq);。注意:biMap和map是不同的,map儲存整個二元核心詞典,而biMap儲存某個詞對應的所有字尾(這個詞 @ 後的所有條目)
map中儲存二元核心詞典示意圖如下:
二元核心詞典主要由 CoreBiGramTableDictionary.java 實現。這個類中有兩個整型陣列 支撐 二元核心詞典的快速二分查詢。
/**
* 描述了詞在pair中的範圍,具體說來<br>
* 給定一個詞idA,從pair[start[idA]]開始的start[idA + 1] - start[idA]描述了一些接續的頻次
*/
static int start[];//支援快速地二分查詢
/**
* pair[偶數n]表示key,pair[n+1]表示frequency
*/
static int pair[];
start 陣列
首先初始化一個與一元核心詞典 Trie樹 size 一樣大小 的start 陣列:
int maxWordId = CoreDictionary.trie.size();
...
start = new int[maxWordId + 1];
然後,遍歷一元核心詞典中的詞,尋找這些詞是 是否有二階共現(或者說:這些詞是否存在 二元接續)
for (int i = 0; i < maxWordId; ++i){
TreeMap<Integer, Integer> bMap = map.get(i);
if (bMap != null){
for (Map.Entry<Integer, Integer> entry : bMap.entrySet()){
//省略其他程式碼
++offset;//統計以 這個詞 為字首的所有二階共現的個數
}
}//end if
start[i + 1] = offset;
}// end outer for loop
if (bMap != null)表示 第 i 個詞(i從下標0開始)在二元詞典中有二階共現,於是 統計以 這個詞 為字首的所有二階共現的個數,將之儲存到 start 陣列中。下面來具體舉例,start陣列中前37個詞的值如下:
其中 start[32]=0,start[33]=0,相應的 一元核心詞典中的詞為 ( )。即,一個左括號、一個右括號。而這個 左括號 和 右括號 在二元核心詞典中是不存在詞共現的(接續)。也就是說在二元核心詞典中 沒有 (@xxx這樣的條目,也沒有 )@xxx 這個條目(xxx 表示任意以 ( 或者 ) 為字首 的字尾接續)。因此,這也是start[32] 和 start[33]=0 都等於0的原因。
部分詞的一元核心詞典如下:
再來看 start[34]=22,start[35]=23。在一元核心詞典中,第34個詞是"一 一",而在二元核心詞典中 '一 一'的詞共現共有22個,如下:
在一元核心詞典中,第 35個詞是 "一 一列舉",如上圖所示,"一 一列舉" 在二元核心中只有一個詞共現:“一 一列舉@芒果臺”。因此,start[35]=22+1=23。從這裡也可以看出:
給定一個詞 idA,從pair[start[idA]]開始的start[idA + 1] - start[idA]描述了一些接續的頻次
比如, idA=35,對應詞“一 一列舉”,它的接續頻次為1,即:23-22=1
這樣做的好處是什麼呢?自問自答一下: ^~^,就是大大減少了二分查詢的範圍。
pair 陣列
pair陣列的長度是二元核心詞典行數的兩倍
int total = 0;
while ((line = br.readLine()) != null){
//省略其他程式碼
total += 2;
}
pair陣列 偶數 下標 儲存 儲存的是 一元核心詞典中的詞 的下標,而對應的偶數加1 處的下標 儲存 這個詞的共現頻率。即: pair[偶數n]表示key,pair[n+1]表示frequency
pair = new int[total]; // total是接續的個數*2
for (int i = 0; i < maxWordId; ++i)
{
TreeMap<Integer, Integer> bMap = map.get(i);//i==0?
if (bMap != null)//某個詞在一元核心詞典中, 但是並沒有出現在二元核心詞典中(這個詞沒有二元核心詞共現)
{
for (Map.Entry<Integer, Integer> entry : bMap.entrySet())
{
int index = offset << 1;
pair[index] = entry.getKey();//詞 在一元核心詞典中的id
pair[index + 1] = entry.getValue();//頻率
}
}
}
舉例來說:對於 '一 一@中',pair陣列是如何儲存這對詞的詞共現頻率的呢?
'一 一'在 map 中第0號位置處, 它是一元核心詞典中的第 34個詞。 共有 22個共現詞。如下:
其中,第一個共現詞是 '一 一 @中',就是'一 一'與 '中' 共同出現,出現的頻率為1。而 ''中'' 在一元核心詞典中的 4124行,如下圖所示:
因此, '一 一@中'的pair陣列儲存如下:
0=4123 (‘中’在一元核心詞典中的位置(從下標0開始計算))
1=1 ('一 一@中'的詞共現頻率)
2=5106 ('為' 在一元核心詞典中的位置) 【為 p 65723】
3=6 ('一 一@為'的詞共現頻率)
由此可知,對於二元核心詞典共現詞而言,共同字首的後續詞 在 pair陣列中是順序儲存的 ,比如說:字首 '一 一'的所有字尾:中、為、交談…… 按順序依次在 pair 陣列中儲存。而這也是能夠對 pair 陣列進行二分查詢的基礎 。
一 一 @中 1
一 一 @為 6
一 一 @交談 1
一 一 @介紹 1
一 一 @作 1
一 一 @分析
.......//省略其他
二分查詢
現在來看看 二分查詢是幹什麼用的?為什麼減少了二分查詢的範圍。為了獲取某 兩個詞( idA 和 idB) 的詞共現頻率,需要進行二分查詢:
public static int getBiFrequency(int idA, int idB){
//省略其他程式碼
int index = binarySearch(pair, start[idA], start[idA + 1] - start[idA], idB);
return pair[index + 1];
}
根據前面介紹, start[idA + 1] - start[idA]就是以 idA 為字首的 所有詞的 詞共現頻率。比如,以 '一 一' 為字首的詞一共有22個,假設我要查詢 '一 一@向' 的詞共現頻率是多少?在核心二元詞典檔案CoreNatureDictionary.ngram.txt中,我們知道 '一 一@向' 的詞共現頻率為2,但是:如何用程式快速地實現查詢呢?
二元核心詞典的總個數還是很多的,比如在 HanLP1.5.3大約有290萬個二元核心詞條,如果每查詢一次 idA@idB 的詞共現頻率就要從290萬個詞條裡面查詢,顯然效率很低。若先定位出 所有以 idA 為字首的共現詞:idA@xx1,idA@xx2,idA@xx3……,然後再從從這些 以idA為字首的共現詞中進行二分查詢,來查詢 idA@idB,這樣查詢的效率就快了許多。
而 start 陣列儲存了一元詞典中每個詞 在二元詞典中的詞共現情況: start[idA] 代表 idA在 pair 陣列中共現詞的起始位置,而start[idA + 1] - start[idA]代表 以idA 為字首的共現詞一共有多少個,這樣二分查詢的範圍就只在 start[idA] 和 start[idA] + (start[idA + 1] - start[idA]) - 1之間了。
private static int binarySearch(int[] a, int fromIndex, int length, int key)
{
int low = fromIndex;
int high = fromIndex + length - 1;
//省略其他程式碼
說到這裡,再多說一點:二元核心詞典的二分查詢 是為了獲取 idA@idB 的詞共現頻率,而這個詞共現頻率的用處之一就是最短路徑分詞演算法(維特比分詞),用來計算最短路徑的權重。關於最短路徑分詞,可參考這篇解析:
//只列出關鍵程式碼
List<Vertex> vertexList = viterbi(wordNetAll);//求解詞網的最短路徑
to.updateFrom(node);//更新權重
double weight = from.weight + MathTools.calculateWeight(from, this);//計算兩個頂點(idA->idB)的權重
int nTwoWordsFreq = CoreBiGramTableDictionary.getBiFrequency(from.wordID, to.wordID);//查核心二元詞典
int index = binarySearch(pair, start[idA], start[idA + 1] - start[idA], idB);//二分查詢 idA@idB共現頻率
總結
有時候由於特定專案需要,需要修改核心詞典。比如新增一個新的二元詞共現詞條 到 二元核心詞典中去,這時就需要注意:新增的新詞條需要存在於一元核心詞典中,否則新增無效。另外,新增到 CoreNatureDictionary.ngram.txt裡面的二元共現詞的位置不太重要,因為相同的字首 共現詞 都會儲存到 同一個TreeMap中,但是最好也是連續放在一起,這樣二元核心詞典就不會太混亂。
文章來源 hapjin 的部落格
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31524777/viewspace-2222419/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- HanLP-實詞分詞器詳解HanLP分詞
- hanlp原始碼解析之中文分詞演算法詳解HanLP原始碼中文分詞演算法
- MapReduce實現與自定義詞典檔案基於hanLP的中文分詞詳解HanLP中文分詞
- Hanlp分詞之CRF中文詞法分析詳解HanLP分詞CRF詞法分析
- HanLP程式碼與詞典分離方案與流程HanLP
- HanLP分詞命名實體提取詳解HanLP分詞
- Hanlp自然語言處理中的詞典格式說明HanLP自然語言處理
- 中文分詞演算法工具hanlp原始碼解析中文分詞演算法HanLP原始碼
- Hanlp配置自定義詞典遇到的問題與解決方法HanLP
- HanLP 關鍵詞提取演算法分析詳解HanLP演算法
- RxLifecycle詳細解析
- Elasticsearch整合HanLP分詞器ElasticsearchHanLP分詞
- HanLP分詞工具中的ViterbiSegment分詞流程HanLP分詞Viterbi
- CoreLocation框架詳細解析框架
- Semaphore最詳細解析
- 3.3 以太坊核心詞彙詳解
- HanLP中文分詞Lucene外掛HanLP中文分詞
- java分詞工具hanlp介紹Java分詞HanLP
- pyhanlp 停用詞與使用者自定義詞典功能詳解HanLP
- 【前端詞典】從輸入 URL 到展現涉及哪些快取環節(非常詳細)前端快取
- Python如何生成詞雲(詳細分析)Python
- Android UI——SpannableString詳細解析AndroidUI
- Hadoop Yarn框架詳細解析HadoopYarn框架
- scala模式匹配詳細解析模式
- MySQL:排序(filesort)詳細解析MySql排序
- HanLP-停用詞表的使用示例HanLP
- hanlp 載入遠端詞庫示例HanLP
- Ansj與hanlp分詞工具對比HanLP分詞
- python呼叫hanlp分詞包手記PythonHanLP分詞
- Pytorch實戰-logistic 迴歸二元分類程式碼詳細註釋PyTorch
- Docker Swarm 核心概念及詳細使用DockerSwarm
- 分詞工具Hanlp基於感知機的中文分詞框架HanLP中文分詞框架
- 搜尋引擎核心技術與演算法 —— 詞項詞典與倒排索引優化演算法索引優化
- linux命令yum的詳細解析Linux
- 網址(URL)的詳細解析
- 超詳細 DNS 協議解析DNS協議
- MyBatis詳細原始碼解析(上篇)MyBatis原始碼
- MyBatis 核心配置檔案詳細內容詳解MyBatis