一, 簡介
Finite State Transducers 簡稱 FST, 中文名:有窮狀態轉換器。在自然語言處理等領域有很大應用,其功能類似於字典的功能(STL 中的map,C# 中的Dictionary),但其查找是O(1)的,僅僅等於所查找的key長度。目前Lucene4.0在查找Term時就用到了該算法來確定此Term在字典中的位置。
FST 可以表示成FST<Key, Value>的形式,我們可以用O(length(key))的復雜度,找到key所對應的值。除此之外,FST 還支持用Value來查找key以及查找Value最優的key等功能。
FST 如此強大,但是目前網上對其講解的資料很少,中文的就更是微乎其微了。
二,數據結構
FST 是一種類似於Trie或自動機的數據結構,所以在學習之前您一定要對自動機有一個簡單的了解,鑒於篇幅,自動機的內容本文不做介紹。
在查找最優的Value時,會用到求最短路徑的Dijikstra算法,但建圖過程於此無關。
三,創建FST
為了讓大家對FST有一個初步的認識,我們舉一個簡單的例子來進行說明。
我們假設創建一組映射:Key → Value
“cat” - > 5,
“deep” - > 10,
“do” - > 15
“dog” - > 2,
“dogs” - > 8,
對於經典FST算法來說,要求Key必須按字典序從小到大加入到FST中,原因主要是因為在處理大數據的情況下,我們不太可能把整個FST數據結構都同時放在內存中,而是要邊建圖邊將建好的圖存儲在外部文件中,以便節省內存。所以我們第一步要對所有的Key排序,對於我給這個例子來說,已經保證了字典序的順序。
根據此例子的輸入我們可以建立下圖所示的FST:
從上圖可以看出,每條邊有兩條屬性,一個表示label(key的元素),另一個表示Value(out)。注意Value不一定是數字,還可一是另一個字符串,但要求Value必須滿足疊加性,如這里的正整數2 + 8 = 10。字符串的疊加行為: aa + b = aab。
建完這個圖之后,我們就可以很容易的查找出任意一個key的Value了。例如:查找dog,我們查找的路徑為:0 → 4 → 8 → 9。 其權值和為: 2 + 0 + 0 + 0 = 2。其中最后一個零表示 node[9].finalOut = 0。所以“dog”的Value為2。
到這里,我們已經對FST有了一個感性的認識,下面我們詳細討論FST的建圖過程:
1,建一個空節點,表示FST的入口,所有的Key都從這個入口開始。
2, 如果還有未處理的Key,則枚舉Key的每一個label。
處理流程如下:
如果當前節點存在含此label的邊,則
如果Value包含該邊的out值,則
Value = Value – out
否則
令temp=out–Value;
out =Value並使下一個節點的所有邊out都加上temp。
如果下一節點是Final節點 則FinalOut += temp
進入下一個節點
否則: 新建一個節點另其out = Value, Value = 0。
如果你看不懂,沒關系,我們將用例子演示一遍概算法:
四, 存儲FST
通過上面的算法我們看到,FST 本身並不要求輸入要按照字典序從小到大,但正如我文章開頭說的那樣,FST只是一個映射,只能成為我們應用程序的一個工具,所以決不能讓這個工具占用我們過多寶貴的內存空間,因此我們要把不用的節點存入到文件中。但是我們的問題是什么樣的節點才是不要的節點呢,要解決這個問題還得回顧我們剛才的算法流程。
我們發現存儲cat字符串的三個節點自從開始處理deep后就在也沒用到過,這是巧合么?如果這是個巧合,那么當開始處理do后就再也沒用到過存儲eep的三個節點,這是巧合么?如果不是巧合,那到底是什么原因呢?很明顯是字典序在做怪!!
正因為,我們保證了所有的Key都是按照字典序加進來的,所以當加入一個新Key的時侯,我們可以先求出新加的Key和上一次輸入的Key的公共前綴,然后就可以把 上一次輸入的Key除去公共前綴剩下的部分存入文件中了。
綜上,可知FST是強大的,但內存是有限的,導致我們必須保證輸入有序。
五,應用
盡管FST足夠強大,但是在應用過程中,我們仍然可以對其進行再優化,自然語言處理我不太了解,所以不太清楚要如何使用FST來處理自然語言,但是我接觸最多的FST的應用就是Lucene。FST在Lucene4.0以后的版本中用於快速定位所查單詞在字典中的位置即FST<IntsRef,byteSequence>,由於Lucene是以二進制存儲的,所以byteSecquence相當於一個數值,即用多個byte去表示一個數。在Lucene中允許用戶設置兩個整數minCount1,和minCount2,同時每一個節點記錄經過自己的單詞數c。
如果c < minCount1 則不存儲該節點,因為在大量的文檔中,以當前單詞為前綴的單詞數很少則沒有存儲的必要,以節省空間。
如果該節點的父節點所經過的單詞數pc < minCount2 則刪除該節點,原因和上面一樣。一般minCount2 >= minCount1。
同時Lucene盡量縮減存儲一個節點所需要的空間,比如狀態壓縮方法。
六,總結
由於網上資料少,自己英語又戳,所以花廢了整整一天的時間慢慢啃代碼才把此算法弄清楚,鑒於本人時間較緊,所以沒有附上自己的程序,如果想了解請查看Lucene4.0官方開源代碼Builder.java 的add 方法。目前Lucene還支持FST的反映射,即通過Value找Key,以及前k小的Key(按照Value大小排序)。其實就是在FST上用Dijikstra求最短路。