基於統計的無詞典的高頻詞抽取(一)——后綴數組字典序排序


中文全文檢索中很重要的一個環節就是分詞,而一般分詞都是基於字典的,特別是對於特定的業務,需要從特定的語料庫中抽出高頻有意義的詞來生成字典。這系列文章,就一步一步來實現一個從大規模語料庫正抽取出高頻詞的程序。

抽詞的過程如下圖:

本文先講解“子串字典序排序”部分,也就是字典序排序部分。本文使用兩種算法:快排 和 基數排序,兩種算法各有應用場景,快排在分析長度20萬字符串時所用的時間明顯低於基數排序,但是,超過時,基數排序明顯有優勢;本文僅僅對於實現的算法做簡單分析和實現,真正生成環境中,將引入多線程,分布式處理等優化手段,這里不提及。

這里,我要先用通俗一些的話語來解釋一些概念,有不正確的地方,歡迎指出;

字典序:字典序是按照字符的順序來排序。例如,‘a’跟‘b’,則字典序為: ‘a’,‘b’;“adc”跟“acd”的字典序為:“acd”,“adc”。

子串:假設現在有字符串S:“S1S2S3…Sn-1Sn”,如果存在i≥1且j≤n(i≤j), 則“SiSi+1…Sj-1Sj”為S的子串。

后綴數組:后綴樹的代替方案;后綴是指從某個位置 i 開始到整個串末尾結束的一個特殊子串。字符串r的從 第 i 個字符開始的后綴表示為 Suffix(i) ,也 就是Suffix(i)=r[i..len(r)]。而后綴數組則是記錄開始位置i的一個數組。

PAT數組:主要用於對文本進行全文索引;PAT數組是一個字符串的后綴樹的字典序的排列。

LCP數組:對應一個PAT數組,表示PAT數組間相鄰的兩個后綴子串的最長公共前綴。

(注,PAT和LCP兩個概念相當重要)

【例】有字符串“abcba”,則其后綴樹為{“abcba”,“bcba”,“cba”,“ba”,“a”},

后綴數組為:{0,1,2,3,4},字典序排序后為:{“a”,“abcda”,“ba”,“bcba”,“cba”},

PAT為:{4,0,3,1,2}, LCP:{1,0,1,0,0}

從例子可以知道,子串字典序排序的問題可以分解為,求后綴數組,后綴數組字典序排序兩個子問題。

獲取后綴數組是一種特殊的子串集合,相當容易獲取,這里就不累贅。

1. 使用快排來實現后綴數組排序(不重點講解,重點講解基數排序):

 1 public static void DictSort(int[] a, string s, int left, int right) 
 2 {
 3     while (left < right) 
 4     {
 5         int i = Partition(a, s, left, right);
 6         DictSort(a, s, left, i - 1);
 7         left = i + 1;
 8     }
 9 }
10                                 
11 public static int Partition(int[] a, string s, int left, int right) 
12 {
13     var _tmp = a[left];
14     string tmp = s.Substring(a[left]);
15     while (left < right) 
16     {
17         while (left < right && s.Substring(a[right]).CompareTo(tmp) >= 0)
18             right--;
19         if (left < right)
20             a[left] = a[right];
21         while (left < right && s.Substring(a[left]).CompareTo(tmp) <= 0)
22             left++;
23         if (left < right) 
24         {
25             a[right] = a[left];
26             right--;
27         }
28     }
29     a[left] = _tmp;
30     return left;
31 }

其中,a為后綴數組,s為源字符串。程序執行完后,a為PAT數組。

2. 使用基數排序來排序后綴數組:

使用該算法的先決條件:輸入的可枚舉,即有限的集合中。而因為漢字跟英文字母可以枚舉

基數排序:也叫“木桶排序”,是一種分配式排序。時間復雜度為O(nlog(r)m),其中r為所采取的基數,而m為堆數。

【例子】假設現在有“1”,“15”,“26”,“55”,“10”這5個數,進行基數排序過程如下:

Step 1: 將個位數放進相應的位置(如下圖)

Step 2, 從0到9依次讀出該數組:10,1,15,55,26

Step 3: 根據Step 2 的結果,再進行一次對十位數的排序(如下圖),【注:“1”=“01”】:

Step 4: 再執行一次Step 2,得到“1”,“10”,“15”,“26”,“55”,結束。

要將該算法運用到字符串中,我們首先得對字符進行編碼:

1. 字符串編碼映射

用整型來表示編碼,對於GB2312編碼,有7445個字符,其中6763個漢字和682個其他字符。漢字的內碼范圍高字節B0-F7,低字節A1-FE。則映射公式可表示為:

Code = (High-176)*94+(Low-161)+Δ;

然而,對於很多應用來說,GB2312編碼不夠用,因為存在一些繁體字,生僻字等未收錄到GB2312編碼中,所以我們采用GBK編碼,GBK共可以表示23940個字符,其中21003個漢字,映射公式可以表示為:

Code =(Heigh-129)*94+(Low-64)+ Δ;

代碼如下:

 1 public static int[] GetGBCode(string s) 
 2 {
 3     int len = s.Count();
 4     int[] gbCode = new int[len];
 5     Encoding chs = Encoding.GetEncoding("GBK");
 6     for (var i = 0; i < len; i++) 
 7     {
 8         byte[] bytes = chs.GetBytes(s[i].ToString());
 9         if (bytes.Length == 1)
10             gbCode[i] = bytes[0];
11         else
12             gbCode[i] = (bytes[0] - 129) * 94 + (bytes[1] - 64);
13     }
14     return gbCode;
15 }

2. 用基數排序來排序編碼后的字符串:

【例】輸入為“張則智”,“是個天才”,“絕對天才”:

Step 1:映射編碼(如下圖)

Step 2: 我們從最后一個字符開始進行基數排序【注:空字符為0】

Sort 1: 張則智,是個天才,絕對天才

Sort 2:是個天才,絕對天才,張則智

Sort 3:絕對天才,是個天才,張則智

Sort 4:絕對天才,是個天才,張則智

排序結束;也就是說,我們需要進行輸入串S.Lengh次線性排序。

代碼片段1:

 1 public static void RadixSort(int[, ] A, int[] B, int t, int n, int M) 
 2 {
 3     int[] C;
 4     for (int k = n - 1; k >= 0; k--) 
 5     {
 6         C = new int[M];
 7         for (int i = 0; i < n; i++) 
 8         {
 9             C[A[i, k]]++;
10         }
11         for (int m = 1; m < M; m++) 
12         {
13             C[m] += C[m - 1];
14         }
15         for (int j = 0; j < n; j++) 
16         {
17             B[--C[A[j, k]]] = j;
18         }
19     }
20 }

上述代碼段,簡單易理解,但是有個問題,當輸入過大時,A這個二維數據太大,會導致內存泄露,所以,進行了改進:

代碼片段2:

 1 public static void RadixSort(int[] A, int[] B, int t, int n, int M) 
 2 {
 3     int[] C;
 4     int[] tmp = new int[t];
 5     for (var x = 0; x < t; x++) 
 6     {
 7         tmp[x] = x;
 8     }
 9     for (int k = n - 1; k >= 0; k--) 
10     {
11         C = new int[M];
12         for (int i = 0; i < n; i++) 
13         {
14             int z = k + tmp[i];
15             if (z <= t - 1) 
16             {
17                 C[A[z]]++;
18             } 
19             else 
20             {
21                 C[0] += n - i;
22                 break;
23             }
24         }
25         for (int m = 1; m < M; m++) 
26         {
27             C[m] += C[m - 1];
28         }
29         for (int j = 0; j < n; j++) 
30         {
31             int u = k + tmp[j];
32             if (u <= t - 1)
33                 B[--C[A[u]]] = j;
34             else 
35             {
36                 for (int y = j; j < n; j++) 
37                 {
38                     B[--C[0]] = j;
39                 }
40                 break;
41             }
42         }
43     }
44 }

程序2做了優化,其中A為對應的編碼,B為后綴數組,t為后綴數組的長度,n為字符串的長度,M為GBK中漢字的個數(本應用中)。

當語料庫增長到百萬級別后,平均花費時間明顯低於快排。

兩個算法可以使用多線程來進行分塊排序在合並,此處未做這方面的優化。

好了,第一部分就先講到這里,如果覺得文章對您有用或者對其他人有幫助,請幫忙點文章下面的“推薦”;如果文章有任何紕漏,歡迎指正,謝謝!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM