2023年11月30日 星期四

Python 學習筆記 : 用 Jieba 模組做中文斷詞

今天在 Hahow 企業版的 "開啟 AI 即戰力 : NL 無痛入門" 課程中, 學習到如何將中文斷詞模組 Jieba 用在語音聊天機器人的作法, 這模組在我手邊的幾本 Python 與 NLP 的書上都有介紹, 以前看完也沒時間測試, 趁著今天記憶猶新紀錄一下測試結果唄. 

Jieba 是中國搜尋引擎百度所開發, 是目前最為廣用的中文斷詞工具, 其預設詞庫據說最早是來自人民日報. Jieba 基本上使用詞庫匹配方式進行規則斷詞, 但對於詞庫中沒有的詞條也支援使用隱藏式馬可夫模型 (HMM) 進行統計斷詞. Jieba 採 MIT 開源政策, 其原始碼寄存於 GitHub, 參考 :


Jieba 的特點摘要如下 :
  • 支援簡體繁體斷詞
  • 支援自定義詞典
  • 提供四種斷詞模式 : 精確, 全文, 搜尋引擎, 以及 Paddle 模式
其中 Paddle 模式使用深度學習框架斷詞, 須先安裝 paddlepadle-tiny 模組才能使用, 且僅支援 Jieba 0.40 版以上. 雖然 Jieba 同時支援繁簡中文, 但因為預設是針對簡體中文詞彙用法而開發, 繁體文本的斷詞結果可能不甚完美, 但這可以透過所提供的設定詞庫功能下載繁體中文詞庫解決.


1. 安裝 Jieba :

可以直接在命令列下 pip install 指令直接安裝 Jieba 模組 :

pip install jieba   

我習慣使用 Thonny 開發 Python 程式, 所以是在 "套件管理" 搜尋 Jieba 後按 "安裝" :




在命令列用 import 匯入 jieba 若沒報錯即可開始用它來斷詞 :

>>> import jieba    
>>> jieba.__version__    
'0.42.1'

用 Python 內建函式 ddir() 檢視 Jieba 模組的內容 : 

>>> dir(jieba)    
['DEFAULT_DICT', 'DEFAULT_DICT_NAME', 'DICT_WRITING', 'PY2', 'Tokenizer', '__builtins__', '__cached__', '__doc__', '__file__', '__license__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_compat', '_get_abs_path', '_lcut', '_lcut_all', '_lcut_for_search', '_lcut_for_search_no_hmm', '_lcut_no_hmm', '_pcut', '_pcut_for_search', '_replace_file', 'absolute_import', 'add_word', 'calc', 'check_paddle_install', 'cut', 'cut_for_search', 'default_encoding', 'default_logger', 'del_word', 'disable_parallel', 'dt', 'enable_paddle', 'enable_parallel', 'finalseg', 'get_DAG', 'get_FREQ', 'get_dict_file', 'get_module_res', 'initialize', 'iteritems', 'iterkeys', 'itervalues', 'lcut', 'lcut_for_search', 'load_userdict', 'log', 'log_console', 'logging', 'marshal', 'md5', 'os', 'pkg_resources', 'pool', 're', 're_eng', 're_han_default', 're_skip_default', 're_userdict', 'resolve_filename', 'setLogLevel', 'set_dictionary', 'strdecode', 'string_types', 'suggest_freq', 'sys', 'tempfile', 'text_type', 'threading', 'time', 'tokenize', 'unicode_literals', 'user_word_tag_tab', 'xrange']

常用函式如下表 : 


 Jeiba 常用函式 說明
 cut(text) 支援精確, 全文, 與 Paddle 模式斷詞, 傳回一個 generator
 cut_for_search(text) 支援搜尋引擎模式斷詞, 傳回一個 generator
 lcut(text) 支援精確, 全文, 與 Paddle 模式斷詞, 傳回一個 list
 lcut_for_search(text) 支援搜尋引擎模式斷詞, 傳回一個 list
 enable_paddle() 開啟 Paddle 斷詞模式 (僅支援 0.40 以上版本)
 set_dictionary(file_name) 設定自訂詞典 (傳入字典檔案名稱)
 load_dictionary(file_name) 載入自訂詞典 (傳入字典檔案名稱)


其中的 cut() 與 cut_for_sarch() 函式為主要的斷詞的工具, 兩者均傳回一個生成器 generator, 可以將生成器傳給 list() 轉成串列, 或傳給字串的 join() 方法用一個 delimiter 字元將斷詞結果串接成字串, 但對於超大文本則可用迴圈迭代方式從 generator 取出斷詞結果, 以免發生記憶體不足問題.  

>>> type(jieba.cut)   
<class 'method'>  
>>> type(jieba.cut_for_search)  
<class 'method'>


2. 呼叫 jieba.cut() 或 jieba.cut_for_seach() 將文本斷詞 :

jieba.cut() 的參數有 4 個 : 

jieba.cut(text [, cut_all=False, HMM=False, use_paddle=False])

必要參數 text 為要進行斷詞的文本, 必須是 UTF-8 或 unicode 編碼的字串; 參數 cut_all 用來設定精確或完全斷詞模式 : 
  • cut_all=False (預設) : 精確模式, 將句子中的詞精確地切開
  • cut_all=True : 完全模式, 將句子中所有可以成詞的都切出來, 速度快但可能有歧異
HMM 參數用來設定是否使用隱藏式馬可夫模型, 用於處理未登錄詞; use_paddle 參數用來設定是否開啟 paddle 模式 (深度學習框架). 

jieba.cut_for_search() 是以精確模式為基礎對較長的詞彙再切分, 主要用在搜尋引擎的斷詞, 其參數有 2 個, 用法與 cut() 一樣 : 

jieba.cut(text [, HMM=False)

例如 :

>>> text='只有退潮的時候,你才知道誰在裸泳'    
>>> g=jieba.cut(text)    
>>> type(g)     
<class 'generator'>      
>>> '|'.join(g)    
'只有|退潮|的|時候|,|你|才|知道|誰|在|裸泳'    
>>> g=jieba.cut(text, cut_all=True)      >>> type(g)     
<class 'generator'>
>>> '|'.join(g)      
'只有|退潮|的|時|候|,|你|才|知道|誰|在|裸泳'     
>>> g=jieba.cut_for_search(text)        
>>> type(g)     
<class 'generator'>  
>>> '|'.join(g)      
'只有|退潮|的|時候|,|你|才|知道|誰|在|裸泳'    

可見不論是 cut() 還是 cut_for_search() 都會傳回一個 generator, 欲取得斷詞結果可以將這個 generator 傳給字串的 join() 方法, 用一個分界字元 (delimiter) 將其產生的元素串起來. 也可以傳給 list() 函式轉成串列 :

>>> text='只有退潮的時候,你才知道誰在裸泳'    
>>> g=jieba.cut(text)  
>>> glist=list(g)      
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\tony1\AppData\Local\Temp\jieba.cache
Loading model cost 0.363 seconds.
Prefix dict has been built successfully.
>>> list(g)     
['只有', '退潮', '的', '時候', ',', '你', '才', '知道', '誰', '在', '裸泳']

上面的測試句子不論用哪個模式得到的斷詞結果都一樣, 改用下面句子就能看出不同 :

>>> text='這次到台北一定要去搭捷運,還要去台北101看看'    
>>> g=jieba.cut(text)                               # 精確模式
>>> '|'.join(g)    
'這次|到|台北|一定|要|去|搭|捷運|,|還要|去|台北|101|看看'    
>>> g=jieba.cut(text, cut_all=True)       # 完全模式
>>> '|'.join(g)    
'這|次|到|台北|北一|一定|定要|去|搭|捷|運|,|還|要|去|台北|101|看看'    
>>> g=jieba.cut_for_search(text)           # 搜尋引擎模式
>>> '|'.join(g)      
'這次|到|台北|一定|要|去|搭|捷運|,|還要|去|台北|101|看看'   

此例可知完全模式會完全切出可能的詞, 例如 "一定要" 會切出 "一定" 與 "定要" 兩個詞; "台北一" 會切出 "台北" 與 "北一"; 而精確模式與搜尋引擎模式結果仍是相同.


3. 使用繁體中文詞庫 :

Jieba 預設詞庫為簡體中文, 詞彙與繁體中文有些差別, 這使得斷詞的結果有時並不恰當, 所幸 Jieba 提供詞庫設定功能, 可以從 GitHub 下載 Jieba 的繁體中文詞庫 dict.txt.big.txt (約 8.4MB, 詞條總數約 58 萬個) :


將其放在目前工作目錄下的 dict 子目錄下, 然後用 jieba.set_dictionary() 函式更改使用的詞庫, 這樣就會套用繁體中文詞庫了 :

jieba.set_dictionary('dict/dict.txt.big.txt')

用純文字編輯器開啟檢閱詞庫內容如下 :

1号店 3 n
1號店 3 n
4S店 3 n
4s店 3 n
AA制 3 n
AB型 3 n
AT&T 3 nz
A型 3 n
A座 3 n
A股 3 n
A輪 3 n
A轮 3 n
.... (略)....

詞條的格式為 : 

詞彙 頻率 詞性

其中詞類 n 為名詞, v 為動詞等. 




例如 :

>>> jieba.set_dictionary('dict/dict.txt.big.txt')   
>>> text='這次到台北一定要去搭捷運,還要去台北101看看'     
>>> g=jieba.cut(text)      
>>> '|'.join(g)    
Building prefix dict from D:\python\test\dict\dict.txt.big.txt ...
Dumping model to file cache C:\Users\tony1\AppData\Local\Temp\jieba.ufcd6a507dc974c1ccf010c4f07a7bccd.cache
Loading model cost 1.488 seconds.
Prefix dict has been built successfully.
'這次|到|台北|一定|要|去|搭|捷運|,|還要|去|台北|101|看看'

原本以為 '台北101' 會被切成一個詞, 可見這個繁中詞庫還是不夠完整, 甚至連 '小港機場' 都被切成 '小港' 與 '機場' 兩個詞, 例如 :

>>> text='這次去日本玩從小港機場出境'  
>>> jieba.set_dictionary('dict/dict.txt.big.txt')   
>>> g=jieba.cut(text)    
>>> '|'.join(g)   
Building prefix dict from D:\python\test\dict\dict.txt.big.txt ...
Loading model from cache C:\Users\tony1\AppData\Local\Temp\jieba.ufcd6a507dc974c1ccf010c4f07a7bccd.cache
Loading model cost 0.722 seconds.
Prefix dict has been built successfully.
'這次|去|日本|玩|從小|港|機場|出境'
>>> g=jieba.cut(text, cut_all=False, HMM=True)     # 啟動 HMM 結果相同
>>> '|'.join(g)   
'這次|去|日本|玩|從小|港|機場|出境'

看來它是把 '從' 與 '小' 切成一個詞了 ('從小' 是一個時間副詞). 解決此問題可以編輯繁中詞庫更改詞頻, 或者使用 Jieba 提供的自定義詞庫功能製作自己的詞庫, 其優先權最高. 


4. 自定義詞庫 :   

教學文件說可以使用自定義詞庫來解決, 先編輯一個自定義詞庫 user_dict.txt, 內容如下 :

小港機場 3 n
台北101 3 n
蔡英文


詞條後面的數字是頻率, 最後面的是詞類, 這兩項可有可無, 有的話格式一定要正確 (即藥用空格隔開), 否則載入時會出現剖析錯誤.  




以 UTF-8 編碼存檔放在 dict 目錄底下, 然後在呼叫 cut() 前先呼叫 load_userdict() 並傳入此字定義詞庫檔名, 但測試結果無效 :

>>> text='這次去日本玩從小港機場出境'   
>>> jieba.set_dictionary('dict/dict.txt.big.txt')   
>>> jieba.load_userdict('dict/user_dict.txt')      
Building prefix dict from D:\python\test\dict\dict.txt.big.txt ...
Dumping model to file cache C:\Users\tony1\AppData\Local\Temp\jieba.uad5f395071014f3797318be3d45203bb.cache
Loading model cost 0.837 seconds.
Prefix dict has been built successfully.
>>> g=jieba.cut(text)   
>>> '|'.join(g)   
'這次|去|日本|玩|從小|港|機場|出境'

我猜可能是版本問題, 於是在另一台電腦指定安裝 0.39 版的 Jieba 來驗證此問題 : 

E:\python\test>pip install jieba==0.39    
Collecting jieba==0.39
  Downloading jieba-0.39.zip (7.3 MB)
     ---------------------------------------- 7.3/7.3 MB 2.6 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
Building wheels for collected packages: jieba
  Building wheel for jieba (setup.py) ... done
  Created wheel for jieba: filename=jieba-0.39-py3-none-any.whl size=7282594 sha256=cc61f12c9a40cf1b040490b5c38de9deded61c4d3428b44ac7ab2a3fbdc7bdd0
  Stored in directory: c:\users\yhhuang\appdata\local\pip\cache\wheels\40\03\0d\c7671d2efcae3ed01aee03a390bcb970518abcdcd3d5df6c88
Successfully built jieba
Installing collected packages: jieba
Successfully installed jieba-0.39

>>> import jieba   
>>> jieba.__version__     
'0.39'   
>>> text='這次去日本玩從小港機場出境'    
>>> g=jieba.cut(text)   
>>> '|'.join(g)   
Building prefix dict from the default dictionary ...
Dumping model to file cache C:\Users\yhhuang\AppData\Local\Temp\jieba.cache
Loading model cost 0.797 seconds.
Prefix dict has been built succesfully.
'這次|去|日本|玩|從|小港|機場|出境'

可見 0.39 版的 Jieba 在預設的簡體詞庫下, 正確地切出 '小港', 而上面的新版 0.43 版反而是錯誤地切成 '從小' 與 '港'. 

接著下載上面的繁體詞庫檔 dict.txt.big.txt 放在目前工作目錄的子目錄 dict 底下, 用 set_dictionary() 設定使用此繁體詞庫, 重新進行斷詞, 結果反而跟 0.43 版一樣得到不正確的切詞 :

>>> jieba.set_dictionary('dict/dict.txt.big.txt')   
>>> text='這次去日本玩從小港機場出境'    
>>> g=jieba.cut(text)    
>>> '|'.join(g)     
Building prefix dict from E:\python\test\dict\dict.txt.big.txt ...
Dumping model to file cache C:\Users\yhhuang\AppData\Local\Temp\jieba.ua930d4894de9d82d45250971e025c191.cache
Loading model cost 1.172 seconds.
Prefix dict has been built succesfully.
'這次|去|日本|玩|從小|港|機場|出境'   

這可能是使用的繁中詞庫的關係, 我在下面這個網站找到另一個繁中詞庫 (比較小, 才 4MB) 就能切出正確結果 :


將此 dict.txt 同樣放在 dict 資料夾下測試 OK :

>>> jieba.set_dictionary('dict/dict.txt')   
>>> g=jieba.cut(text)   
>>> '|'.join(g)     
Building prefix dict from E:\python\test\dict\dict.txt ...
Dumping model to file cache C:\Users\yhhuang\AppData\Local\Temp\jieba.u0fd681cbe0a9699d869d3e014fd1e58b.cache
Loading model cost 0.625 seconds.
Prefix dict has been built succesfully.
'這次|去|日本|玩|從|小港|機場|出境'   

我用 0.39 版重新測試使用者定義詞庫結果就正確了 :  

>>> text='這次去日本玩從小港機場出境'   
>>> jieba.set_dictionary('dict/dict.txt')    
>>> jieba.load_userdict('dict/user_dict.txt')     
Building prefix dict from E:\python\test\dict\dict.txt ...
Loading model from cache C:\Users\yhhuang\AppData\Local\Temp\jieba.u0fd681cbe0a9699d869d3e014fd1e58b.cache
Loading model cost 0.515 seconds.
Prefix dict has been built succesfully.
>>> g=jieba.cut(text)    
>>> '|'.join(g)    
'這次|去|日本|玩|從|小港機場|出境'
>>> text='這次到台北一定要去搭捷運,還要去台北101看看'   
>>> jieba.set_dictionary('dict/dict.txt')    
>>> jieba.load_userdict('dict/user_dict.txt')  
Building prefix dict from E:\python\test\dict\dict.txt ...
Loading model from cache C:\Users\yhhuang\AppData\Local\Temp\jieba.u0fd681cbe0a9699d869d3e014fd1e58b.cache
Loading model cost 0.500 seconds.
Prefix dict has been built succesfully.
>>> g=jieba.cut(text)   
>>> '|'.join(g)   
'這次|到|台北|一定|要|去|搭|捷運|,|還要|去|台北101|看看'

結果 '小港機場' 與 '台北101' 都能正確切分出來, 可見自定義詞庫 userdict 會最優先被套用. 總之, 斷詞結果取決於 Jieba 版本與所使用的詞庫.


5. 去除停用詞 (stop word) :   

所謂停用詞 stop words 是指在自然語言處理任務中, 會影響統計與演算效率的字詞符號 (token), 例如標點符號, 或 of, on, at, in, which 等語法功能詞, 這些通常需要在語料錢處理過程中濾除. 停用字並沒有嚴格限定, 是根據 NLP 任務需要而自行定義的一張表. 參考 :


從上面的測試範例可知, Jieba 在斷詞時會把標點符號與空格等字符也切分成一個詞 (token), 這在統計字頻的任務中會影響計算結果, 所以需要將其列入停用字, 在用 Jieba 斷詞後進一步將停用字濾掉. 不過 Jieba 未內建濾除停用字的函式, 必須自行用檔案處理方式濾掉. 

首先編輯一個停用字表 stopword.txt, 一列一個停用字 (此處只是針對標點符號), 然後以 UTF-8 編碼格式存檔, 放在目前工作目錄或例如 dict 的子目錄下 :





然後用檔案處理方式讀取這個停用字表存入串列中, 將文本用 Jieba 斷詞後用迴圈迭代所得倒的 generator, 在每次迭代中檢查 generator 產出的斷詞是否在停用詞表中, 沒有的話就輸出, 這樣就能濾掉停用字了, 例如 : 

>>> with open('dict/stopwords.txt', encoding='utf-8-sig') as f:   
    stopwords=f.read().split('\n')     # 讀取整個檔案後以跳行字元拆分為串列
    
>>> stopwords     
['.', ',', ';', '?', '!', ':', "'", '"', ',', '。', '[', ']', '(', ')', '{', '}', '\\', '/', '|', '「', '」', ':']   
>>> text='這次到台北一定要去搭捷運,還要去台北101看看'      
>>> g=jieba.cut(text)    
>>> words=[]     # 儲存過濾後的 token
>>> for token in g:     # 迭代產生器
    if token not in stopwords:     # 如果不是停用字就存入串列
        words.append(token)    
        
>>> '|'.join(words)      
'這次|到|台北|一定|要|去|搭|捷運|還要|去|台北101|看看'

可見逼點符號已經被濾掉了. 但是在語音聊天機器人中似乎不用濾掉, 因為在 TTS 中合成語音時標點符號可作為停頓標誌. 

參考 :

https://github.com/ldkrsi/jieba-zh_TW (有七年前的繁體詞庫)

沒有留言 :