jieba分詞原理解析:用戶詞典如何優先於系統詞典


目標

查看jieba分詞組件源碼,分析源碼各個模塊的功能,找到分詞模塊,實現能自定義分詞字典,且優先級大於系統自帶的字典等級,以醫療詞語鄰域詞語為例。

jieba分詞地址:github地址:https://github.com/fxsjy/jieba

jieba四種分詞模式

  • 精確模式,試圖將句子最精確地切開,適合文本分析。

    • 按照優先級只顯示一次需要划分的詞語。

  • 全模式,把句子中所有的可以成詞的詞語都掃描出來, 速度非常快,但是不能解決歧義。

    • 比如清華大學,會划詞顯示 清華/ 清華大學/ 華大/ 大學 四個詞

  • 搜索引擎模式,在精確模式的基礎上,對長詞再次切分。

    • 如中國科學院計算所,會分詞為 中國, 科學, 學院, 科學院, 中國科學院, 計算, 計算所。

  • 使用了Paddlepaddle框架,暫時跳過。

根據任務需求,因為只需要將優先級高的特有名詞顯示一次即可,所以定位在精確模式

相關API

1. 自定義詞典
Tokenizer.suggest_freq(segment, tune=False) -> int
Suggest word frequency to force the characters in a word to be joined or splitted.

Parameter: - segment : The segments that the word is expected to be cut into, If the word should be treated as a whole, use a str. - tune : If True, tune the word frequency.
- 如果需要將兩個詞當成連貫的詞語,則傳入一個str。
- 如果需要把一個詞語中的兩個詞當成不同的詞語,則傳入一個元組。
- tune設為True,表示啟用。            
Note that HMM may affect the final result. If the result doesn't change, set HMM=False.
2.關鍵詞提取 jieba.analyse.extract_tags
3.詞性標注 jieba.posseg.cut
4.返回詞語在原文的起止位置 jieba.tokenize

方案一

將自己需要的分詞的詞語,加入自定義詞典當中

  • 開發者可以指定自己自定義的詞典,以便包含 jieba 詞庫里沒有的詞。雖然 jieba 有新詞識別能力,但是自行添加新詞可以保證更高的正確率

  • 用法: jieba.load_userdict(file_name) # file_name 為文件類對象或自定義詞典的路徑

  • 詞典格式和 dict.txt 一樣,一個詞占一行;每一行分三部分:詞語、詞頻(可省略)、詞性(可省略),用空格隔開,順序不可顛倒。file_name 若為路徑或二進制方式打開的文件,則文件必須為 UTF-8 編碼。

  • 詞頻省略時使用自動計算的能保證分出該詞的詞頻。

分為以下幾步

  1. 構建自定義詞典為相應的格式 部分結果如下:

    21-羥化酶缺陷症
    CO2瀦留
    E字征
    HIV感染
    Howship-Romberg征
    Korsakov綜合征
    Moro反應遲鈍
    Q-T間期延長
    畏寒
  2. 調用相應的方法

  3. 測試相應的數據

#encoding=utf-8
from __future__ import print_function, unicode_literals
import sys
sys.path.append("../")
import jieba
import jieba.posseg as pseg
import os
path = os.getcwd()
# 添加用戶詞典
jieba.load_userdict(path + "\\userdict.txt")

test_sent = (
"無畏寒一過性心尖部收縮期雜音測試"
)
words = jieba.cut(test_sent)
print('/'.join(words))

添加前 無畏寒一過性心尖部收縮期雜音測試 => 無畏/寒一/過性/心尖/部/收縮期/雜音/測試 添加后 => 無畏/寒/一過性心尖部收縮期雜音/測試

分析

添加前后,jiaba分詞能將一過性心尖部收縮期雜音分詞成功,但是無畏寒沒有成功,可能是因為無畏在系統中的頻度較高,(詞頻省略時使用自動計算的能保證分出該詞的詞頻。)而用戶詞典的詞語頻度相對較低。所以下一步,我將嘗試提高用戶詞典頻度,或者降低系統詞典頻度。如若不行,可進一步查看源碼。

繼續測試

jieba.add_word('畏寒', freq=10, tag=None) # 通過添加畏寒的頻度為10
結果仍為:無畏//一過性心尖部收縮期雜音/測試
jieba.add_word('畏寒', freq=100, tag=None) # 添加畏寒的頻度為100
結果為:/畏寒/一過性心尖部收縮期雜音/測試

由上述結果可知確實若用戶詞典省略詞頻,則頻度相對系統詞典較低,無法正確分出結果,所以只需要在用戶添加頻度,並設置在100,便可以實現分詞功能,但是不能確定是不是100就能覆蓋所有的系統詞匯,所以進一步查看系統詞匯的詞頻。在源碼中有dict.txt其中包含了所有的詞語的系統詞頻,查看其中的最大值。

#encoding=utf-8
from __future__ import print_function, unicode_literals
import sys
sys.path.append("../")
import jieba
import jieba.posseg as pseg
import os
path = os.getcwd()
# 獲取系統詞典路徑
rpath = path + "\\jieba\\dict.txt"
# 讀取詞典
import pandas as pd
res = pd.read_csv(rpath,sep=' ',header=None,names = ['name','frequence','type'])
print(res.head())
print(res['frequence'].max())
# 得到結果883634 ,也就是如果將詞頻設置為大於883634的數則用戶詞典絕對優先於系統詞典

設置代碼如下

import numpy as np
import pandas as pd

import os
path = os.getcwd() #獲取當前工作路徑
print(path)
output_file = os.path.join(path,'userdict.txt')

# 處理詞典
res = pd.read_csv('zhengzhuang.txt',sep=' ',header = None,names=['name','type','frequence'])
print(res.head())

res = res.drop(labels=['type', 'frequence'],axis=1)
print(res.head())
# 添加頻度
res['frequence'] = 883635
# 轉化為txt文件
res.to_csv(output_file,sep=' ',index=False,header=False)

在添加症狀的詞條后測試如下

患者1月前無明顯誘因及前驅症狀下出現腹瀉,起初稀便,后為水樣便,無惡心嘔吐,每日2-3次,無嘔血,無腹痛,無畏寒寒戰,無低熱盜汗,無心悸心慌,無大汗淋漓,否認里急后重感,否認蛋花樣大便,當時未重視,未就診。
患者/1/月前/無/明顯/誘因/及/前驅/症狀/下/出現/腹瀉/,/起初/稀便/,/后/為/水樣便/,/無/惡心/嘔吐/,/每日/2/-/3/次/,/無/嘔血/,/無/腹痛/,/無/畏寒/ 寒戰/,/無/低熱/盜汗/,/無/心悸/心慌/,/無/大汗淋漓/,/否認/里急后重/感/,/否認/蛋/花樣/大便/,/當時/未/重視/,/未/就診/。

方案二

查看源碼,從cut入手一步步查看其內部如何調用的

__init__.py
cut = dt.cut # cut為全局方法
# 關鍵方法
def cut(self, sentence, cut_all=False, HMM=True, use_paddle=False):
       """
      The main function that segments an entire sentence that contains
      Chinese characters into separated words.

      Parameter:
          - sentence: The str(unicode) to be segmented.
          - cut_all: Model type. True for full pattern, False for accurate pattern.
          - HMM: Whether to use the Hidden Markov Model.
      """
        # 判斷是存在paddle
       is_paddle_installed = check_paddle_install['is_paddle_installed']
       # 轉碼,英文utf8 中文gbk
       sentence = strdecode(sentence)
       # paddle相關
       if use_paddle and is_paddle_installed:
           # if sentence is null, it will raise core exception in paddle.
           if sentence is None or len(sentence) == 0:
               return
           import jieba.lac_small.predict as predict
           results = predict.get_sent(sentence)
           for sent in results:
               if sent is None:
                   continue
               yield sent
           return
       
       re_han = re_han_default
       re_skip = re_skip_default
       # 判斷cut 模式
       if cut_all:
           cut_block = self.__cut_all # 全模式
       elif HMM:
           cut_block = self.__cut_DAG # HMM模型 默認為這種
       else:
           cut_block = self.__cut_DAG_NO_HMM # 無HMM模型
       
       blocks = re_han.split(sentence)  # 正則表達式獲取相應的字符串 現根據標點符號分詞
       print(blocks)
       for blk in blocks:
           if not blk:
               continue
           if re_han.match(blk): # 沒有空格
               for word in cut_block(blk):
                   yield word
           else:
               tmp = re_skip.split(blk) # 去空格
               for x in tmp:
                   if re_skip.match(x):
                       yield x
                   elif not cut_all:
                       for xx in x:
                           yield xx
                   else:
                       yield x

由上述代碼可知默認的模型__cut_DAG, 輸入的字符串,首先根據標點符號分詞,然后cut_block負責處理每一個初步拆分過后的字符串,具體的拆分方法為以下兩個函數

 # 得到的最大概率路徑的概率。這里即為動態規划查找最大概率路徑
   def calc(self, sentence, DAG, route):
       N = len(sentence)
       route[N] = (0, 0)
       # 通過詞頻搜索
       logtotal = log(self.total) #利用total進行動態規划
       for idx in xrange(N - 1, -1, -1):
           route[idx] = max((log(self.FREQ.get(sentence[idx:x + 1]) or 1) -
                             logtotal + route[x + 1][0], x) for x in DAG[idx])

   # 函數功能為把輸入的句子生成有向無環圖, 得到所有可能的詞語
   def get_DAG(self, sentence):
       self.check_initialized()
       DAG = {}
       N = len(sentence)
       for k in xrange(N):
           tmplist = []
           i = k
           frag = sentence[k]
           while i < N and frag in self.FREQ: # 查詢FREQ中的詞
               if self.FREQ[frag]:
                   tmplist.append(i)
               i += 1
               frag = sentence[k:i + 1]
           if not tmplist:
               tmplist.append(k)
           DAG[k] = tmplist
       return DAG

由此可知FREQ和total是函數計算的關鍵,所以找到FREQ和total是如何初始化的就可以明白計算的依據了

 self.FREQ, self.total = self.gen_pfdict(self.get_dict_file()) # 得到詞語和詞頻
 # 通過get_dict_file獲得
 def get_dict_file(self):
       if self.dictionary == DEFAULT_DICT:
           return get_module_res(DEFAULT_DICT_NAME)
       else:
           return open(self.dictionary, 'rb')
 # 默認為該目錄下的dict.txt
 DEFAULT_DICT_NAME = "dict.txt"

上述推理可知,是dict.txt中的詞語和詞頻,通過有向無環圖和動態規划路徑得到分詞結果,而在之前通過用戶詞典調用的方法無非就是在此基礎上加上新的詞語和詞頻。

    def load_userdict(self, f):
       '''
      Load personalized dict to improve detect rate.

      Parameter:
          - f : A plain text file contains words and their ocurrences.
                Can be a file-like object, or the path of the dictionary file,
                whose encoding must be utf-8.

      Structure of dict file:
      word1 freq1 word_type1
      word2 freq2 word_type2
      ...
      Word type may be ignored
      '''
       self.check_initialized()
       if isinstance(f, string_types):
           f_name = f
           f = open(f, 'rb')
       else:
           f_name = resolve_filename(f)
       for lineno, ln in enumerate(f, 1):
           line = ln.strip()
           if not isinstance(line, text_type):
               try:
                   line = line.decode('utf-8').lstrip('\ufeff')
               except UnicodeDecodeError:
                   raise ValueError('dictionary file %s must be utf-8' % f_name)
           if not line:
               continue
           # match won't be None because there's at least one character
           word, freq, tag = re_userdict.match(line).groups() # 得到用戶詞典詞語和詞頻
           if freq is not None:
               freq = freq.strip()
           if tag is not None:
               tag = tag.strip()
           self.add_word(word, freq, tag) # 添加到詞典中
 def add_word(self, word, freq=None, tag=None):
       """
      Add a word to dictionary.

      freq and tag can be omitted, freq defaults to be a calculated value
      that ensures the word can be cut out.
      """
       self.check_initialized()
       word = strdecode(word)
       freq = int(freq) if freq is not None else self.suggest_freq(word, False)
       # 添加詞
       self.FREQ[word] = freq
       self.total += freq
       if tag:
           self.user_word_tag_tab[word] = tag
       for ch in xrange(len(word)):
           wfrag = word[:ch + 1]
           if wfrag not in self.FREQ:
               self.FREQ[wfrag] = 0
       if freq == 0:
           finalseg.add_force_split(word)

結論

構建一個用戶詞典表,然后將詞語的詞頻設置為大於883634的數,則用戶詞典絕對優先於系統詞典,其中的工程量主要在如何構造一個合適的字典,在通過調用load_userdict 方法,在用cut方法即可得到設置后的結果,具體操作見方案一。接下來的研究目標:1.用戶詞典大小最大可以有多大 2.用戶詞典大小對速度的影響 3.有相同前綴和后綴的詞匯如何區分 4. 和百度分詞的API對比。

 


免責聲明!

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



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