最強中文自然語言處理工具CKIPtagger

LP Cheung
12 min readJan 23, 2020

--

每一位做中文NLP(Natural Language Processing)的朋友大概都用過Jieba。分詞作為中文NLP工序的第一步,對模型性能影響至鉅。然而,以往可供選擇的工具並不多。Jieba雖然分詞效果不理想,卻幾乎是預設的選項。最近台灣中研院開發的CKIPtagger成為了Jieba的挑戰者。CKIPtagger不但分詞效果更理想,還提供實體識別功能。下文會比較兩者的效果和CKIPtagger的使用方式。

分詞例子

我在HK01上隨意選擇三篇文章,以Jieba和CKIPtagger分別做分詞。三篇文章題材不同,其中一篇較多人名地名,看看兩者的表現有沒有受影響。

Example 1 Input:

def get_jieba_tokenization(text):
tokenized = jieba.lcut(text)
return ' '.join(tokenized)
def get_ckiptagger_tokenization(text):
ckip_tokenized = ws([text])
return ' '.join(ckip_tokenized[0])
text1 = '''2003年沙士一役,港大新發病毒性疾病學講座教授管軼在廣州野味市場的果子狸身上,找到感染人類的沙士冠狀病毒,找到元兇遏止疫情擴散,成為沙士抗疫英雄。事隔17年,他本周二三(21、22日),與團隊到武漢考察,但找不到源頭,「源頭被銷毀得乾乾淨淨,這裏似乎不歡迎專家」,他對內地傳媒說,估計武漢感染規模大沙士十倍起跳,又說政府不作為,無奈做「逃兵」訂機票返港,感到極其無力。'''print (f'jieba result: \n{get_jieba_tokenization(text1)}')
print ('\n')
print (f'ckiptagger result: \n{get_ckiptagger_tokenization(text1)}')

Example 1 Output:

jieba result: 
2003 年 沙士 一役 , 港大新發 病毒性 疾病 學講座 教授 管軼 在 廣州 野味 市場 的 果子狸 身上 , 找到 感染 人類 的 沙士 冠狀 病毒 , 找到 元 兇 遏止 疫情 擴散 , 成為 沙士 抗疫 英雄 。 事隔 17 年 , 他 本周 二三 ( 21 、 22 日 ) , 與 團隊 到 武漢 考察 , 但 找 不到 源頭 , 「 源頭 被 銷毀 得 乾 乾淨淨 , 這 裏 似乎 不歡 迎 專家 」 , 他 對 內 地 傳媒 說 , 估計 武漢 感染 規模 大 沙士 十倍 起跳 , 又 說 政府 不作 為 , 無奈 做 「 逃兵 」 訂機票 返港 , 感到 極其 無力 。


ckiptagger result:
2003年 沙士 一 役 , 港大 新發病毒性 疾病學 講座 教授 管軼 在 廣州 野味 市場 的 果子狸 身 上 , 找到 感染 人類 的 沙士 冠狀 病毒 , 找到 元兇 遏止 疫情 擴散 , 成為 沙士 抗疫 英雄 。 事 隔 17 年 , 他 本 周 二三 ( 21 、 22日 ) , 與 團隊 到 武漢 考察 , 但 找 不 到 源頭 , 「 源頭 被 銷毀 得 乾乾淨淨 , 這裏 似乎 不 歡迎 專家 」 , 他 對 內地 傳媒 說 , 估計 武漢 感染 規模 大 沙士 十 倍 起跳 , 又 說 政府 不 作為 , 無奈 做 「 逃兵 」 訂 機票 返 港 , 感到 極其 無力 。

以上例子可以見到,Jieba在比較長的詞語表現欠佳,如將「港大新發病毒性疾病學講座教授」分成「港大新發 病毒性 疾病 學講座 教授」。CKIPtagger的長詞語分詞比較合理。

Example 2 Input:

text2 = '''農曆新年將至,又是逗利是的時候,打工仔也可望收到開工利是。滙豐連續第二年向本港員工派發電子利是,每位500元,將在員工下一個出糧日透過出糧戶口發放。滙豐香港區行政總裁施穎茵向香港員工發出電郵,宣布派發電子利是的消息。她於電郵內表示,十分感謝同事對公司及客戶無限的貢獻及投入,即使前景仍有不確定性,她有信心克服挑戰,今年再創繁榮。滙豐發言人回覆傳媒查詢時稱,滙豐希望藉著農曆新年,感謝各同事過去一年的努力。跟去年一樣,該行將會向每位香港員工派發500元的電子利是,在各同事下一個出糧日透過出糧戶口發放。'''print (f'jieba result: \n{get_jieba_tokenization(text2)}')
print ('\n')
print (f'ckiptagger result: \n{get_ckiptagger_tokenization(text2)}')

Example 2 Output:

jieba result: 
農 曆 新年 將至 , 又 是 逗利 是 的 時候 , 打工仔 也 可望 收到 開工利 是 。 滙 豐連續 第二年 向 本港 員 工派 發電子 利是 , 每位 500 元 , 將在員 工下 一個 出 糧日 透過 出 糧戶口 發放 。 滙 豐 香港 區 行政 總裁 施穎茵 向 香港 員工 發出 電郵 , 宣布 派 發電子利 是 的 消息 。 她 於 電郵內 表示 , 十分 感謝 同事 對 公司 及 客戶 無限 的 貢獻及 投入 , 即使 前景 仍有 不確 定性 , 她 有 信心 克服 挑戰 , 今年 再創 繁榮 。 滙 豐發言人 回覆 傳媒查 詢時 稱 , 滙 豐 希望 藉 著農 曆 新年 , 感謝 各 同事 過去 一年 的 努力 。 跟 去年 一樣 , 該行 將會 向 每位 香港 員 工派 發 500 元 的 電子利 是 , 在 各 同事 下一個 出 糧日 透過 出 糧戶口 發放 。


ckiptagger result:
農曆 新年 將 至 , 又 是 逗利 是 的 時候 , 打工仔 也 可望 收到 開工利 是 。 滙豐 連續 第二 年 向 本 港 員工 派發 電子 利 是 , 每 位 500 元 , 將 在 員工 下 一 個 出糧日 透過 出糧 戶口 發放 。 滙豐 香港區 行政 總裁 施穎茵 向 香港 員工 發出 電郵 , 宣布派發 電子 利 是 的 消息 。 她 於 電郵 內 表示 , 十分 感謝 同事 對 公司 及 客戶 無限 的 貢獻 及 投入 , 即使 前景 仍 有 不確定性 , 她 有 信心 克服 挑戰 , 今年 再 創 繁榮 。 滙豐 發言人 回覆 傳媒 查詢 時 稱 , 滙豐 希望 藉著 農曆 新年 , 感謝 各 同事 過去 一 年 的 努力 。 跟 去年 一樣 , 該 行 將 會 向 每 位 香港 員工派發 500 元 的 電子利 是 , 在 各 同事 下 一 個 出糧日 透過 出糧 戶口 發放 。

Jieba連「農曆新年」都分錯,而且無法認到「滙豐」。

Example 3 Input:

text3 = '''大阪、京都、奈良、神戶是親子遊的熱門路線,其實除了京阪神,從大阪玩到鳥取也是不錯選擇!官方早前推出鳥取1000円巴士優惠,由大阪難波往鳥取只需1000円,大阪Open Jaw玩鳥取一流,加上適逢鳥取紅楚蟹季開鑼,平食海產、鳥取和牛、21世紀梨……還有必去鳥取砂丘、鬼太郎商店街及小朋友最愛的柯南博物館等,大人小朋友都玩得滿足!'''print (f'jieba result: \n{get_jieba_tokenization(text3)}')
print ('\n')
print (f'ckiptagger result: \n{get_ckiptagger_tokenization(text3)}')

Example 3 Output:

jieba result: 
大阪 、 京都 、 奈良 、 神戶 是 親子遊 的 熱門 路線 , 其實 除了 京 阪神 , 從 大阪 玩到 鳥取 也 是 不錯 選擇 ! 官方 早前 推出 鳥取 1000 円 巴士 優惠 , 由 大阪 難波往 鳥取 只 需 1000 円 , 大阪 Open Jaw 玩鳥取 一流 , 加上 適逢 鳥 取紅楚 蟹 季開鑼 , 平食 海產 、 鳥取 和 牛 、 21 世紀 梨 … … 還有 必去 鳥取 砂丘 、 鬼 太郎 商店 街及 小朋友 最愛的 柯南 博物 館 等 , 大人 小朋友 都 玩 得 滿足 !


ckiptagger result:
大阪 、 京都 、 奈良 、 神戶 是 親子 遊 的 熱門 路線 , 其實 除了 京阪神 , 從 大阪 玩 到 鳥取 也 是 不錯 選擇 ! 官方 早前 推出 鳥取 1000 円 巴士 優惠 , 由 大阪 難波 往 鳥取 只 需 1000 円 , 大阪 Open Jaw 玩 鳥 取 一流 , 加上 適逢 鳥取 紅楚蟹季 開鑼 , 平食 海產 、 鳥取 和 牛 、 21世紀 梨 … … 還 有 必 去 鳥取 砂丘 、 鬼太郎 商店街 及 小朋友 最愛 的 柯南 博物館 等 , 大人 小朋友 都 玩 得 滿足 !

這篇文章比較多地名同專有名詞, CKIPtagger大致上都認到,例如「京阪神」、「難波」。而Jieba就認不到。

命名實體識別

除了分詞外,CKIPtagger還有命名實體識別(named entity recognition)能力。比如說如果想抽取第三篇文的地名:

tokenized_result = ws([text3])
pos_result = pos(tokenized_result)
entity_result = ner(tokenized_result, pos_result)
print (entity_result)

結果非常準確:

{(0, 2, 'GPE', '大阪'),
(3, 5, 'GPE', '京都'),
(6, 8, 'GPE', '奈良'),
(9, 11, 'GPE', '神戶'),
(25, 28, 'GPE', '京阪神'),
(30, 32, 'GPE', '大阪'),
(34, 36, 'GPE', '鳥取'),
(49, 51, 'GPE', '鳥取'),
(51, 55, 'CARDINAL', '1000'),
(62, 64, 'GPE', '大阪'),
(64, 66, 'GPE', '難波'),
(67, 69, 'GPE', '鳥取'),
(71, 75, 'CARDINAL', '1000'),
(77, 87, 'GPE', '大阪Open Jaw'),
(97, 99, 'GPE', '鳥取'),
(111, 113, 'GPE', '鳥取'),
(116, 120, 'DATE', '21世紀'),
(127, 129, 'GPE', '鳥取'),
(145, 150, 'FAC', '柯南博物館')}

Remark

雖然CKIPtagger非常好用,但都存在一些缺點:

  1. 相比Jieba,無論啟動速度還是分詞速度都較慢。因為CKIPtagger背後用到深度學習模型,每次使用前都要先將模型加載。
  2. 下載預訓練模型的連結有時會失效。解決方法很簡單:預先下載。

完整代碼:

import jieba
from ckiptagger import WS, POS, NER, data_utils
data_utils.download_data_gdown("./")ws = WS("./data")
pos = POS("./data")
ner = NER("./data")
def get_jieba_tokenization(text):
tokenized = jieba.lcut(text)
return ' '.join(tokenized)
def get_ckiptagger_tokenization(text):
ckip_tokenized = ws([text])
return ' '.join(ckip_tokenized[0])
text1 = '''2003年沙士一役,港大新發病毒性疾病學講座教授管軼在廣州野味市場的果子狸身上,找到感染人類的沙士冠狀病毒,找到元兇遏止疫情擴散,成為沙士抗疫英雄。事隔17年,他本周二三(21、22日),與團隊到武漢考察,但找不到源頭,「源頭被銷毀得乾乾淨淨,這裏似乎不歡迎專家」,他對內地傳媒說,估計武漢感染規模大沙士十倍起跳,又說政府不作為,無奈做「逃兵」訂機票返港,感到極其無力。'''text2 = '''農曆新年將至,又是逗利是的時候,打工仔也可望收到開工利是。滙豐連續第二年向本港員工派發電子利是,每位500元,將在員工下一個出糧日透過出糧戶口發放。滙豐香港區行政總裁施穎茵向香港員工發出電郵,宣布派發電子利是的消息。她於電郵內表示,十分感謝同事對公司及客戶無限的貢獻及投入,即使前景仍有不確定性,她有信心克服挑戰,今年再創繁榮。滙豐發言人回覆傳媒查詢時稱,滙豐希望藉著農曆新年,感謝各同事過去一年的努力。跟去年一樣,該行將會向每位香港員工派發500元的電子利是,在各同事下一個出糧日透過出糧戶口發放。'''text3 = '''大阪、京都、奈良、神戶是親子遊的熱門路線,其實除了京阪神,從大阪玩到鳥取也是不錯選擇!官方早前推出鳥取1000円巴士優惠,由大阪難波往鳥取只需1000円,大阪Open Jaw玩鳥取一流,加上適逢鳥取紅楚蟹季開鑼,平食海產、鳥取和牛、21世紀梨……還有必去鳥取砂丘、鬼太郎商店街及小朋友最愛的柯南博物館等,大人小朋友都玩得滿足!'''print (f'jieba result: \n{get_jieba_tokenization(text1)}')
print ('\n')
print (f'ckiptagger result: \n{get_ckiptagger_tokenization(text1)}')
print (f'jieba result: \n{get_jieba_tokenization(text2)}')
print ('\n')
print (f'ckiptagger result: \n{get_ckiptagger_tokenization(text2)}')
print (f'jieba result: \n{get_jieba_tokenization(text3)}')
print ('\n')
print (f'ckiptagger result: \n{get_ckiptagger_tokenization(text3)}')
tokenized_result = ws([text3])
pos_result = pos(tokenized_result)
entity_result = ner(tokenized_result, pos_result)
print (entity_result)

--

--