分詞學習(3),基於ngram語言模型的n元分詞

           最大概率分詞中,認爲每個詞的概率都是獨立的,但是有一部分詞,其切分卻與前一個詞密切相關,特別是中文分詞中更爲明顯,英文中就是如上一篇文章中的“tositdown”的例子。

         這樣就可以使用2元模型,就是如一個分割形式"ab cde f"的概率,

如果按照1-gram計算:P(ab cde f) = P(ab)*P(cde)*P(f)

如果按照2-gram計算:P(ab cde f) = P(ab|<s>)*P(cde|ab)P(f|cde)

基本的方法和最大概率分詞差不多,就是計算片段概率的時候,需要知道選擇的前驅節點的前驅節點位置,這樣才能計算轉移概率。具體如下圖所示:




確定當前節點4的狀態,就是根據幾個概率累計值,取最大的,即可確定前驅節點和當前節點的累積概率

上代碼(Python):

  1. #!/usr/bin/env python  
  2. #coding=utf-8  
  3. #############################################################  
  4. #function: max probility segment  
  5. #          a dynamic programming method  
  6. #  
  7. #input: dict file  
  8. #output: segmented words, divide by delimiter "\ "  
  9. #author: [email protected]  
  10. ##############################################################  
  11. import sys   
  12. import math  
  13.   
  14. #global parameter  
  15. DELIMITER = " " #分詞之後的分隔符  
  16.   
  17. class DNASegment:  
  18.     def __init__(self):  
  19.         self.word1_dict = {} #記錄概率,1-gram  
  20.         self.word1_dict_count = {} #記錄詞頻,1-gram  
  21.         self.word1_dict_count["<S>"] = 8310575403 #開始的<S>的個數   
  22.   
  23.         self.word2_dict = {} #記錄概率,2-gram  
  24.         self.word2_dict_count = {} #記錄詞頻,2-gram  
  25.   
  26.   
  27.         self.gmax_word_length = 0  
  28.         self.all_freq = 0 #所有詞的詞頻總和,1-gram的  
  29.   
  30.     #估算未出現的詞的概率,根據beautiful data裏面的方法估算  
  31.     def get_unkonw_word_prob(self, word):  
  32.         return math.log(10./(self.all_freq*10**len(word)))  
  33.   
  34.     #獲得片段的概率  
  35.     def get_word_prob(self, word):  
  36.         if self.word1_dict.has_key(word): #如果字典包含這個詞  
  37.             prob = self.word1_dict[word]  
  38.         else:  
  39.             prob = self.get_unkonw_word_prob(word)  
  40.         return prob  
  41.   
  42.   
  43.     #獲得兩個詞的轉移概率  
  44.     def get_word_trans_prob(self, first_word, second_word):  
  45.         trans_word =  first_word + " " + second_word  
  46.         #print trans_word  
  47.         if self.word2_dict_count.has_key(trans_word):  
  48.             trans_prob = \  
  49.     math.log(self.word2_dict_count[trans_word]/self.word1_dict_count[first_word])  
  50.         else:  
  51.             trans_prob = self.get_word_prob(second_word)  
  52.         return trans_prob  
  53.   
  54.     #尋找node的最佳前驅節點  
  55.     #方法爲尋找所有可能的前驅片段  
  56.     def get_best_pre_node(self, sequence, node, node_state_list):  
  57.         #如果node比最大詞長小,取的片段長度以node的長度爲限  
  58.         max_seg_length = min([node, self.gmax_word_length])  
  59.         pre_node_list = [] #前驅節點列表  
  60.   
  61.         #獲得所有的前驅片段,並記錄累加概率  
  62.         for segment_length in range(1,max_seg_length+1):  
  63.   
  64.             pre_node = segment_start_node  #取該片段,則記錄對應的前驅節點  
  65.   
  66.             if pre_node == 0:  
  67.                 #如果前驅片段開始節點是序列的開始節點,  
  68.                 #則概率爲<S>轉移到當前詞的概率  
  69.                 #segment_prob = self.get_word_prob(segment)  
  70.                 segment_prob = \  
  71.                         self.get_word_trans_prob("<S>", segment)  
  72.             else#如果不是序列開始節點,按照二元概率計算  
  73.                 #獲得前驅片段的前一個詞  
  74.                 pre_pre_node = node_state_list[pre_node]["pre_node"]  
  75.                 pre_pre_word = sequence[pre_pre_node:pre_node]  
  76.                 segment_prob = \  
  77.                         self.get_word_trans_prob(pre_pre_word, segment)  
  78.   
  79.   
  80.             #當前node一個候選的累加概率值  
  81.             candidate_prob_sum = pre_node_prob_sum + segment_prob  
  82.   
  83.             pre_node_list.append((pre_node, candidate_prob_sum))  
  84.   
  85.         #找到最大的候選概率值  
  86.         (best_pre_node, best_prob_sum) = \  
  87.                 max(pre_node_list,key=lambda d:d[1])  
  88.         return (best_pre_node, best_prob_sum)  
  89.   
  90.     #最大概率分詞  
  91.     def mp_seg(self, sequence):  
  92.         sequence = sequence.strip()  
  93.   
  94.         #初始化  
  95.         node_state_list = [] #記錄節點的最佳前驅,index就是位置信息  
  96.         #初始節點,也就是0節點信息  
  97.         ini_state = {}  
  98.         ini_state["pre_node"] = -1 #前一個節點  
  99.         ini_state["prob_sum"] = 0 #當前的概率總和  
  100.         node_state_list.append( ini_state )  
  101.         #字符串概率爲2元概率  
  102.         #P(a b c) = P(a|<S>)P(b|a)P(c|b)  
  103.   
  104.         #逐個節點尋找最佳前驅節點  
  105.         for node in range(1,len(sequence) + 1):  
  106.             #尋找最佳前驅,並記錄當前最大的概率累加值  
  107.             (best_pre_node, best_prob_sum) = \  
  108.                     self.get_best_pre_node(sequence, node, node_state_list)  
  109.   
  110.             #添加到隊列  
  111.             cur_node = {}  
  112.             cur_node["pre_node"] = best_pre_node  
  113.             cur_node["prob_sum"] = best_prob_sum  
  114.             node_state_list.append(cur_node)  
  115.             #print "cur node list",node_state_list  
  116.   
  117.         # step 2, 獲得最優路徑,從後到前  
  118.         best_path = []  
  119.         node = len(sequence) #最後一個點  
  120.         best_path.append(node)  
  121.         while True:  
  122.             pre_node = node_state_list[node]["pre_node"]  
  123.             if pre_node == -1:  
  124.                 break  
  125.             node = pre_node  
  126.             best_path.append(node)  
  127.         best_path.reverse()  
  128.   
  129.         # step 3, 構建切分  
  130.         word_list = []  
  131.         for i in range(len(best_path)-1):  
  132.             left = best_path[i]  
  133.             word_list.append(word)  
  134.   
  135.         seg_sequence = DELIMITER.join(word_list)  
  136.         return seg_sequence  
  137.   
  138.     #加載詞典,爲詞\t詞頻的格式  
  139.     def initial_dict(self, gram1_file, gram2_file):  
  140.         #讀取1_gram文件  
  141.         dict_file = open(gram1_file, "r")  
  142.         for line in dict_file:  
  143.             sequence = line.strip()  
  144.             key = sequence.split('\t')[0]  
  145.             value = float(sequence.split('\t')[1])  
  146.             self.word1_dict_count[key] = value  
  147.         #計算頻率  
  148.         self.all_freq = sum(self.word1_dict_count.itervalues()) #所有詞的詞頻  
  149.         self.gmax_word_length = 20  
  150.         self.all_freq = 1024908267229.0  
  151.         #計算1gram詞的概率  
  152.         for key in self.word1_dict_count:  
  153.             self.word1_dict[key] = math.log(self.word1_dict_count[key]/self.all_freq)  
  154.   
  155.         #讀取2_gram_file,同時計算轉移概率  
  156.         dict_file = open(gram2_file, "r")  
  157.         for line in dict_file:  
  158.             sequence = line.strip()  
  159.             key = sequence.split('\t')[0]  
  160.             value = float(sequence.split('\t')[1])  
  161.             first_word = key.split(" ")[0]  
  162.             second_word = key.split(" ")[1]  
  163.             self.word2_dict_count[key] = float(value)  
  164.             if self.word1_dict_count.has_key(first_word):  
  165.                 self.word2_dict[key] = \  
  166.                     math.log(value/self.word1_dict_count[first_word])  #取自然對數  
  167.             else:  
  168.                 self.word2_dict[key] = self.word1_dict[second_word]  
  169. #test  
  170. if __name__=='__main__':  
  171.     myseg = DNASegment()  
  172.     myseg.initial_dict("count_1w.txt","count_2w.txt")  
  173.     sequence = "itisatest"  
  174.     seg_sequence = myseg.mp_seg(sequence)  
  175.     print "original sequence: " + sequence  
  176.     print "segment result: " + seg_sequence  
  177.   
  178.     sequence = "tositdown"  
  179.     seg_sequence = myseg.mp_seg(sequence)  
  180.     print "original sequence: " + sequence  
  181.     print "segment result: " + seg_sequence  

可以看到

這樣,itistst,仍然可以分成 it is a test

而前面分錯的tositedown,則正確的分爲to sit down

代碼和字典見附件:http://pan.baidu.com/s/1bnw197L

        但這樣的分詞顯然還有一些問題,就是一個詞是由前一個或者幾個詞決定的,這樣可以去除一部分歧義問題,但是ngram模型還是基於馬爾科夫模型的,其基本原理就是無後效性,就是後續的節點的狀態不影響前面的狀態,就是先前的分詞形式一旦確定,無論後續跟的是什麼詞,都不會再有變化,這在現實中顯然是不成立的。因此就有一些可以考慮到後續詞的算法,如crf等方法,局可以參考相應的資料,這些算法,用幾十行python代碼一般很難寫出來,因此,一般會使用具體的代碼包來做。如crf++,http://crfpp.googlecode.com/svn/trunk/doc/index.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章