---
title: 基於 gensim 訓練中文維基詞向量
date: 2022-04-28
is_modified: true
disqus: cynthiahackmd
image: https://i.imgur.com/9weAgyi.jpg
categories:
- "智慧計算 › 人工智慧"
tags:
- "AI/ML"
- "自然語言處理 NLP"
- "gensim"
- "Word Embedding "
- "Word2Vec"
---
{%hackmd @CynthiaChuang/Github-Page-Theme %}
<br>
過年大掃除的時候發現,我的草稿夾內塞滿之前寫一半的草稿,想說欠都欠過年了,還是讓它繼續欠下去吧(誤)...
這篇是記錄前一陣子(其實也快一年前了...)為了弄出一份符合我們應用的詞向量,開始嘗試著自給自足的故事 XD
<!--more-->
為了方便操作,這邊採用的是使用 python 的函式庫 — [gensim](https://radimrehurek.com/gensim/) 來實作,文章裡所有的程式碼都會傳上 github...如果找不到那肯定是我拖延症又發作了XD
## Word Embedding
先來看看 Word Embedding,又名詞向量,是指將文字的語意用一組向量來表示,其概念是出自於 Bengio 的 《A Neural Probabilistic Language Model》(論文:[原文](http://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf)、[筆記](https://medium.com/%E7%A8%8B%E5%BC%8F%E5%B7%A5%E4%BD%9C%E7%B4%A1/a-neural-probabilistic-language-model-%E8%AB%96%E6%96%87%E7%AD%86%E8%A8%98-61f4c5cecee7))。
在詞向量的訓練過程中,它會儘可能提取特徵,使著具有相同上下文語意的詞,在高緯度空間中盡可能相近;反之,使含義並不相似的詞距盡可能遠離。此外,也是我們能夠用類別的方式推導詞,最著名的例子:
$$
\overrightarrow{King} - \overrightarrow{Man} + \overrightarrow{Woman} \approx \overrightarrow{Queen}
$$
<p class="illustration">
<img src="https://i.imgur.com/9weAgyi.jpg" alt="word vector">
word vector(圖片來源: <a href="https://flashgene.com/archives/48063.html">闪念基因</a>)
</p>
而 word2vec 則是 Google 所提出用來實現 word embedding 的一種方法,主要採用了 Skip-Gram 與 CBOW 兩種模型。
在 Skip-Gram 中,一次只輸入一個字,輸出為其前後一定距離內的文字,所以==同一個輸入會有多個輸出==;與 Skip-Gram 不同的是,CBOW 是將一段句子的中間字當作輸出為,其左右文字為輸入,所以是==多個輸入一個輸出輸入==。
簡單來說,Skip-Gram 是給定 input word 來預測上下文。而 CBOW 則是給定上下文,反過來預測 input word。
<br>
不過,我們這邊不討論 word2vec 的數學公式,也不討論 word2vec 的訓練模型,這邊就只想辦法訓練一包詞向量而已 XD
- 若是對數學公式有興趣的,往這走 ↓
-- [word2vec 中的数学原理详解(一)目录和前言_word2vec,CBOW,Skip-gram|peghoty-CSDN博客](http://blog.csdn.net/itplus/article/details/37969519)
- 若是對神經網路架構有興趣的,看這邊 ↓
-- [類神經網路 -- word2vec (part 1 : Overview)|MARK CHANG'S BLOG](http://cpmarkchang.logdown.com/posts/773062-neural-network-word2vec-part-1-overview)
- 只想訓練一份詞向量的,就往下看吧
## 函式庫安裝
在開始之前,先把 gensim 安裝起來,只要一條指令:
```shell=
$ pip install --upgrade gensim
```
<br>
另外,需要準備一套斷詞工具,顧名思義就是將整篇的語料拆成一個一個詞,才能交給 word2vec 進行訓練。這邊可以依照自己的需求挑選斷詞工具,例如: [jieba](https://github.com/ckiplab/ckiptagger),而我所挑選的是 [pyhanlp](https://github.com/hankcs/pyhanlp),在自定義的字典檔添加上它還滿方便的。
```shell=
$ pip install pyhanlp
```
HanLP 最近似乎有釋出 [HanLP 2.0](https://github.com/hankcs/HanLP) ,但還在 Alpha 階段就是了。不過,不管是 jieba 或是 pyhanlp/HanLP 都是以簡體中文為核心,若想使用繁體中文為核心的,可以考慮在去年(2019)中研院釋出 [CKIP](https://github.com/ckiplab/ckiptagger)的 python API 。
<br>
最後記錄一下我所使用的 pyhanlp 版號,我有一陣子沒更新了說,[1.x branch](https://github.com/hankcs/HanLP/tree/1.x) 最後的版號應該是 v1.7.6 吧
:::info
**hanlp --version**
jar 1.6.8: /py3.6/lib/python3.6/site-packages/pyhanlp/static/hanlp-1.6.8.jar
data 1.6.8: /py3.6/lib/python3.6/site-packages/pyhanlp/static/data
config : /py3.6/lib/python3.6/site-packages/pyhanlp/static/hanlp.properties
:::
## 取得語料
### 下載語料
在機器學習中,萬事起頭的第一步就是備齊資料,當然訓練詞向量也是。為了訓練出針對特定 domain 但又不失一般性的詞向量,因此必須分別收集 general 與 specific domain 的資料。
這邊 general 的資料是此用==中文的維基百科==,因為它資料夠大,也較為全面。維基百科有[最新中文資料](https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2)可供下載,也可以前往[維基百科的資料庫](https://zh.m.wikipedia.org/wiki/Wikipedia:%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%8B%E8%BD%BD)下載所需要的版本。需要特別注意的是,請選擇 `*-pages-articles.xml.bz2` 形式的語料。
下載完成後,先別急著將下載檔案解壓縮,因為這是一份 1.9G 的 xml 文件,如果不想自己寫 parser 來解析這份文件,請先別衝動!...說的就是我自己 XD
### 解析語料
在 gensim 中有提供 API 可以直接調用,以取出文章的標題和內容。
使用的方法很簡單,先初始化 `wikiCorpus`,指定 wikipedia 的路徑與 dictionary...等選項。再使用 `get_texts()` 迭代每一篇文章,它會回傳一個 tokens list,將這些 tokens 用空白符號串接成字串寫出到另一份文件中,就完成解析了。
```python=
from gensim.corpora import WikiCorpus
from tqdm import tqdm
from pyhanlp import *
def hasUnicodeEncodeError(context):
try:
for char in context:
char.encode('utf8')
except UnicodeEncodeError as e:
return True
return False
sources="zhwiki-latest-pages-articles.xml.bz2"
output="wiki_Sentence.txt"
wiki_corpus = WikiCorpus(sources, dictionary={})
with open(output, "w") as f:
for texts in tqdm(wiki_corpus.get_texts(), desc='wiki2txt'):
corpus = []
for text in texts:
w = HanLP.s2tw(text)
w = text if hasUnicodeEncodeError(w) else w
corpus.append(w)
f.write(" ".join(corpus) + ' \n')
```
<br>
在解析的過程中,我發現這份檔案有繁簡交雜的現象,導致我最終的結果不如預期,因此我在寫出檔案前多做了些處理:
1. 每個 token,依序做一次繁簡轉換。
2. 檢查轉換的結果是否符合編碼,若結果符合則留下,反之則保留轉換前的輸入。
## 訓練前處理
### 斷詞
因為 pyhanlp 的預設斷詞是使用簡體中文斷詞,如果想用簡體的斷詞器對繁體的語句進行斷詞,效果不彰,因此必須將語句換成簡體,或改用繁體的斷詞器,這邊是選擇使用==繁體的斷詞器==。
但,想使用繁體的斷詞器必須去你的 pyevn 中的 site-packages 中更改 pyhanlp 的 init 檔案,讓它可以引入繁體的斷詞器:
```bash=
$ cd ~/py3.6/lib/python3.6/site-packages/pyhanlp
```
<br>
接著修改 `__init__.py`,在最下方的 API 列表中加入:
```python=
TraditionalChineseTokenizer= SafeJClass('com.hankcs.hanlp.tokenizer.TraditionalChineseTokenizer')
```
<br>
如此一來就可以使用繁體的斷詞器了:
```python=
from pyhanlp import *
# 用簡體的斷詞器進行斷詞
# [你好/vl, ,/w, 欢迎/n, 在/p, Python/nx, 中/f, 调用/n, HanLP/nx, 的/ude1, API/nx]
HanLP.segment('你好,欢迎在Python中调用HanLP的API')
# 用簡體的斷詞器對繁體的語句進行斷詞
# [你好/vl, ,/w, 歡迎/v, 在/p, Python/nx, 中/f, 調/n, 用/p, HanLP/nx, 的/ude1, API/nx]
HanLP.segment('你好,歡迎在Python中調用HanLP的API')
# 用繁體的斷詞器進行斷詞
# [你好/vl, ,/w, 歡迎/n, 在/p, Python/nx, 中/f, 調用/n, HanLP/nx, 的/ude1, API/nx]
TraditionalChineseTokenizer.segment('你好,歡迎在Python中調用HanLP的API')
```
<br>
若是不想修改 init 檔案,也可以在使用繁體斷詞器前才引入:
```python=
from pyhanlp import *
TraditionalChineseTokenizer= SafeJClass('com.hankcs.hanlp.tokenizer.TraditionalChineseTokenizer')
TraditionalChineseTokenizer.segment('你好,歡迎在Python中調用HanLP的API')
```
<br><br>
完成繁體斷詞器配置之後,就可以針對上一個步驟所解析出來的語料進行斷詞:
```python=
from pyhanlp import *
from tqdm import tqdm
import re
def getWordsAndNatures(term):
words = [w.word for w in term]
natures = [w.nature.toString() for w in term]
return [" ".join(words), " ".join(natures)]
def restructuring(line):
endswith = line.endswith("\n")
line = line.strip()
line = re.sub(r'\s+', " ", line)
if endswith:
line += " \n"
return line
sources=["Sentences_1.txt", "Sentences_2.txt"]
output="segment_result.txt"
with open(output, "w") as fw:
for file in sources:
with open(file, 'r') as fr:
for line in tqdm(fr, desc='segment {0} lines'.format(file)):
line = line.strip()
# (看情境)其他處理-1: 取代 e-mail、取代 url
term = HanLP.segment(line)
corpus = getWordsAndNatures(term)
# (看情境)其他處理-2: 依照詞性取代掉姓名、機關名稱
# 其他處理-3: 移除 stop word
fw.write(restructuring(corpus[0]) + "\n")
```
### 其他前處理
如果有注意到,在上段程式碼中我留了三個其他處理的註解,這邊可以取決你的應用是否希望將它換成特定的標籤,例如將對話中的 e-mail 或 url 換成 `#EMAIL#`、`#URL#` 等標籤,使斷詞結果更符合我們預期,或是可以依照詞性取代掉姓名、機關名稱,使最後訓練出來詞向量可以集中這些詞性的詞,但這樣的修改有好有壞就是了。
最後一個註解是移除 [stop word](https://zh.wikipedia.org/wiki/%E5%81%9C%E7%94%A8%E8%AF%8D),中文翻作停用詞,這些詞極其普遍,但與其他詞相比它並沒有什麼實際含義,如:阿、呀。這些詞的移除可以加強單詞的上下文關係,理論上有助於詞向量的訓練。
除了上述的前處理外,在程式碼看不到的部份,我還對 Hanlp 的字典進行調整,引入了[內政資料開放平臺](https://data.moi.gov.tw/)的資料,並使用 Hanlp 內建的[新詞挖掘](https://github.com/hankcs/HanLP/wiki/%E6%96%B0%E8%AF%8D%E8%AF%86%E5%88%AB)的功能,使它的斷詞結果更符合臺灣的對話情境。
### Out-of-Vocabulary Words,即 OOV
斷詞處理的最後一段,我們設置一個詞頻的門檻值,針對低詞頻的詞將使用 `#UNK#` 這個標籤來取代。這是因為這些低頻詞由於缺乏足夠數量的語料,訓練出來的詞向量往往效果不佳,也可同時訓練出一組詞向量用以表示詞彙表中不存在的詞。
這邊就不分享替換 OOV 的程式碼了,我這邊暫時使用暴力法來實做,先將斷詞結果讀入,並計算每個詞出現次數,最後將低於門檻值的詞換成 UNK 標籤,再將結果寫出。只是這樣實做流程耗時又耗空間,還有待改進 orz...
## 訓練詞向量
最後就是訓練詞向量啦!程式碼很簡單就幾行而已,但麻煩的是超參數的調整只能按你的應用,慢慢實驗來調整。
```python=
from gensim.models import word2vec
source="segment_result.txt"
model_name = "my_model"
vector_size = 200
min_count = 10
window_size = 3
workers = 3
sentences = word2vec.LineSentence(source)
model = word2vec.Word2Vec(sentences, size=vector_size, min_count=min_count, window=window_size, workers=workers)
model.save(model_name)
```
<br>
回頭看看程式碼的部分:
1. **sentences**
指的就是我們用來訓練詞向量的語料,因為我這邊無論是維基百科或是 specific domain 的資料,都是以一行為一篇文章或是對話。因此在讀入語料時,使用 `LineSentence` 的模式。
2. **vector_size**
詞向量的維度。
3. **window**
指訓練窗格大小,白話來說,一個詞在看上下文關係時,上下應該各看幾個字的意思。
4. **workers**
執行緒數目。
5. **min_count**
指一個詞出現的次數若小於 min_count,則拋棄不參與訓練。
## 訓練詞結果
訓練完成了,那就來試試下面的結果!
```python=
from gensim.models import word2vec
from gensim import models
model = models.Word2Vec.load("my_model")
# 前 100 相似詞
model.most_similar("飲料",topn = 100)
# 兩個詞的相關性
model.similarity("棒球","全壘打")
# 相似關係的前 100 個詞
model.most_similar(["日本","東京"], ["美國"], topn= 100)
```
## 參考資料
1. [word2vec和word embedding有什么区别?|知乎](https://www.zhihu.com/question/53354714)
2. [DeepNLP的表示学习·词嵌入来龙去脉·深度学习(Deep Learning)·自然语言处理(NLP)·表示(Representation)|Mr.Scofield-CSDN博客](https://blog.csdn.net/scotfield_msn/article/details/69075227)
3. [A Neural Probabilistic Language Model](http://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf)
4. [A Neural Probabilistic Language Model 論文筆記|程式工作紡 - Medium](https://medium.com/%E7%A8%8B%E5%BC%8F%E5%B7%A5%E4%BD%9C%E7%B4%A1/a-neural-probabilistic-language-model-%E8%AB%96%E6%96%87%E7%AD%86%E8%A8%98-61f4c5cecee7)
5. [zonghan程式筆記: Word2Vec model Introduction (skip-gram & CBOW)](http://zongsoftwarenote.blogspot.com/2017/04/word2vec-model-introduction-skip-gram.html)
6. [word2vec 中的数学原理详解(一)目录和前言_word2vec,CBOW,Skip-gram|peghoty-CSDN博客](http://blog.csdn.net/itplus/article/details/37969519)
7. [類神經網路 -- word2vec (part 1 : Overview)|MARK CHANG'S BLOG](http://cpmarkchang.logdown.com/posts/773062-neural-network-word2vec-part-1-overview)
8. [word2vec实战:获取和预处理中文维基百科(Wikipedia)语料库,并训练成word2vec模型|欢迎来到Jimmy的博客-CSDN博客](http://blog.csdn.net/qq_32166627/article/details/68942216)
9. [中文维基百科语料库词向量的训练|吴良超的学习笔记](https://wulc.me/2016/10/12/%E4%B8%AD%E6%96%87%E7%BB%B4%E5%9F%BA%E7%99%BE%E7%A7%91%E7%9A%84%E8%AF%8D%E5%90%91%E9%87%8F%E7%9A%84%E8%AE%AD%E7%BB%83/)
10. [以 gensim 訓練中文詞向量|雷德麥的藏書閣](http://zake7749.github.io/2016/08/28/word2vec-with-gensim/)
## 更新紀錄
:::spoiler 最後更新日期:2022-04-28
- 2022-04-28 fix 'NameError: name 'self' is not defined'
- 2020-01-17 發布
:::
{%hackmd @CynthiaChuang/Github-Page-Footer %}