使用 monpa 多工程序加快斷詞

Wen-Chao Yeh
9 min readMay 28, 2021
MONPA 罔拍是一個提供正體中文斷詞、詞性標註以及命名實體辨識的多任務模型。

monpa 從 v0.3.2 開始提供多執行程序 (Multi-processing) 的輔助功能,藉由同時啟動 CPU 多核心支援來提升大量句子的斷詞效率。只要以 list 或是 list of list 格式輸入原始文字資料,並設定要使用的 worker 數量(一個 worker 就是一個 CPU 內核),就可以啟動同時多程序斷詞,完成後的回傳值也是 list 或是 list of list 格式,並自動關閉原先啟動的多核程序。

必須注意:同時多工將於初始階段先做程序啟動及資料複製等準備工作而耗費一些額外時間,所以若非一次性大量斷詞,可使用 cut function 即可。

這裏舉例給有需要的開發者參考。

此範例的文字資料是100 則 wiki 正體中文條目摘要,每則幾乎都會超過 200 字元,所以 pandas 讀入後,再用 monpa 的 short_sentence function 將長句切成短句再來斷詞。作法可參考上一篇文章。

import pandas as pddf = pd.read_fwf("./zhwiki-tw_100.txt",
encoding="utf-8",
header=None).loc[:,:4]
df.rename(columns = {0:"content", 1:"chunk", 2:"cut", 3:"cut_mp_a", 4:"cut_mp_b"}, inplace=True)

content 欄位是放從 txt 檔載入的原始文字資料,共 100 則。

chunk 欄位是預留放置經 short_sentence 處理的短句 list。

cut 欄位預留放置 cut function 斷詞結果。

cup_mp_a 欄位預留放置 utils.cut_mp function 費時用法的斷詞結果。

cup_mp_b 欄位預留放置 utils.cut_mp function 省時用法的斷詞結果。

import monpa
from monpa import utils
df["chunk"] = df["content"].map(utils.short_sentence)

pandas 支援以欄位資料(column)呼叫 map 一一處理每排(row)的內容。上述程式講人話是將 content 欄位的第一筆(row)資料取出經 utils.short_sentence 切成短句,以 list 格式回存到 chunk 欄位的第一排(row)位置。依序完成 content 欄位所有資料的切短句作業。

取出第一筆資料比對看看,原本的長句被切成三個短句,並以 list 格式存放。

df["content][0]### output ###
'《松江府志》是指松江府(今上海松江)的方志。松江之建置,始於唐玄宗天寶年間的華亭縣,明初洪武年間改為直隸府,領華亭、上海、青浦三縣,為江南繳稅大區。《松江府志》最早撰寫於元朝,如張之翰有大德《松江郡志》八卷,南宋時還有楊潛撰紹熙《雲間志》三卷。明朝與清朝曾多次增修。正德七年(1512年)顧清應當時任松江知府陳威之聘,纂有正德《松江府志》三十二卷,共有三十二目,又附目十七,《鄭堂讀書記補逸》稱「其書取諸舊志,參訂考證,正訛補闕」「詳博而不傷於冗濫,敘述亦具史體,堪與王守溪《姑蘇志》並稱焉」。雷琳《雲間志略》稱顧清「素留心經濟,而復注念桑梓,遂加修輯,成郡信史」。崇禎年間還修有《松江府志》五十八卷,董其昌手書《崇禎松江府志序》。清朝多次重修《松江府志》,計有《康熙松江府志》、《康熙補輯松江府志》、《嘉慶松江府志》、《光緒松江府續志》。'
df["chunk"][0]### output ###
['《松江府志》是指松江府(今上海松江)的方志。松江之建置,始於唐玄宗天寶年間的華亭縣,明初洪武年間改為直隸府,領華亭、上海、青浦三縣,為江南繳稅大區。《松江府志》最早撰寫於元朝,如張之翰有大德《松江郡志》八卷,南宋時還有楊潛撰紹熙《雲間志》三卷。明朝與清朝曾多次增修。',
'正德七年(1512年)顧清應當時任松江知府陳威之聘,纂有正德《松江府志》三十二卷,共有三十二目,又附目十七,《鄭堂讀書記補逸》稱「其書取諸舊志,參訂考證,正訛補闕」「詳博而不傷於冗濫,敘述亦具史體,堪與王守溪《姑蘇志》並稱焉」。雷琳《雲間志略》稱顧清「素留心經濟,而復注念桑梓,遂加修輯,成郡信史」。崇禎年間還修有《松江府志》五十八卷,董其昌手書《崇禎松江府志序》。',
'清朝多次重修《松江府志》,計有《康熙松江府志》、《康熙補輯松江府志》、《嘉慶松江府志》、《光緒松江府續志》。']

以上 100 則原文總共有 716 段短句。

一筆一筆斷詞的 cut 方法

%%time
df["cut"] = df["chunk"].map(lambda x: [monpa.cut(item) for item in x if item])
### output ###
CPU times: user 54 s, sys: 3.79 s, total: 57.8 s
Wall time: 50 s

chunk 欄位內是 list 資料格式,因此以 List Comprehensions 寫法一一取出短句並呼叫 monpa.cut function 斷詞,將結果回存到 cut 欄位。

716 條短句完成斷詞,整個 cell 作業要花費 50 秒。

你的機器 CPU 有多少核心?

import multiprocessingprint(f"我的 CPU 有 {multiprocessing.cpu_count()} 個核心")### output ###
我的 CPU 有 8 個核心

建議在使用多工程序前先查一下機器 CPU 支援幾個內核。

utils.cut_mp function 費時錯誤用法

%%time
df["cut_mp_a"] = df["chunk"].map(lambda x: utils.cut_mp(x, 4))
### output ###
CPU times: user 798 ms, sys: 1.63 s, total: 2.43 s
Wall time: 2min 1s

chunk 欄位內是 list 資料格式,而 utils.cut_mp 支援 list 格式輸入文字資料,因此以 map 來將 chunk 欄位內的資料一一丟入 utils.cut_mp function 斷詞,並設定開啟 4 核心做同步多工,最後將結果回存到 cut 欄位。

716 條短句完成斷詞,整個 cell 作業要花費 2 分 1 秒。

發生什麼事?

本例是啟動 CPU 4 個核心來同步多工,也就是同時會有 4 個程序做斷詞,應該會比一個程序做斷詞還要快,但結果卻讓人傻眼。這不是分組作業同學相互擺爛的情況,而是程式寫作時未考慮前面提到的,

做完斷詞後,會將多工程序關閉。

這個程式的寫法是將 chunk 欄位的一筆 list 資料取出,丟入 utils.cut_mp 作業,這時會花費一些時間來啟動 4 核心再做同步多工斷詞,完成後就關閉同步執行的 4 個程序。然後,再從 chunk 欄位取出一筆 list 資料,丟入 utils.cut_mp 作業,這時會花費一些時間來啟動 4 核心再做同步多工斷詞,完成後就關閉同步執行的 4 個程序。然後,再從 chunk 欄位取出一筆 list 資料,丟入 utils.cut_mp 作業,這時會花費一些時間來啟動 4 核心再做同步多工斷詞,完成後就關閉同步執行的 4 個程序。…然後,再從 chunk 欄位取出一筆 list 資料,丟入 utils.cut_mp 作業,這時會花費一些時間來啟動 4 核心再做同步多工斷詞,完成後就關閉同步執行的 4 個程序。

就這麼開關開關做了 716 次,總體花費時間當然高於乖乖地ㄧ筆一筆斷詞。

utils.cut_mp function 省時正確用法

%%time
df["cut_mp_b"] = utils.cut_mp(df["chunk"].tolist(), 4)
### output ###
CPU times: user 154 ms, sys: 67.3 ms, total: 222 ms
Wall time: 24.1 s

chunk 欄位內是 list 資料格式,而 utils.cut_mp 也支援 list of list 格式輸入文字資料,因此以 tolist function 來將 chunk 欄位內的所有資料轉成 list of list 一次丟給 utils.cut_mp function 斷詞,並設定開啟 4 核心做同步多工,最後將結果回存到 cut 欄位。

將 716 條短句完成斷詞,整個 cell 花費 24.1 秒。

真心不騙,比較快!

本例是啟動 CPU 4 個核心來同步多工,也就是同時會有 4 個程序做斷詞,並考慮前面提到的,

做完斷詞後,會將多工程序關閉。

程式寫法改成將 chunk 欄位的所有 list 資料取出包成 list of list,一次丟入 utils.cut_mp 作業,這時會花費一些時間來啟動 4 核心再做同步多工斷詞,完成後就關閉同步執行的 4 個程序。沒有然後,就這麼開關 1 次,總體花費時間當然低於乖乖地ㄧ筆一筆斷詞。

(df["cut"]==df["cut_mp_a"]).value_counts()### output ###
True 100
dtype: int64
(df["cut"]==df["cut_mp_b"]).value_counts()### output ###
True 100
dtype: int64

驗證三種方式的斷詞結果都相同。 monpa 斷詞不慢,只是要會用。

--

--

Wen-Chao Yeh

行動通訊業界工作十多年,曾任系統架構師與產品經理。清華大學資訊系統與應用研究所博士候選人,研究領域包含但不限於 Generative AI, LLM, RAG, Semantic Search, Sentiment Analysis。