中文维基百科语料库词向量的训练

要通过计算机进行自然语言处理,首先就需要将这些文本数字化。目前用得最广泛的方法是词向量,根据训练使用算法的不同,目前主要有 Word2VecGloVe 两大方法,本文主要讲述通过这两个方法分别训练中文维基百科语料库的词向量。

获取并处理中文维基百科语料库

下载

中文维基百科语料库的下载链接为:https://dumps.wikimedia.org/zhwiki/, 本试验下载的是最新的 zhwiki-latest-pages-articles.xml.bz2。这个压缩包里面存的是标题、正文部分,该目录下还包括了其他类型的语料库,如仅包含标题,摘要等。

抽取内容

Wikipedia Extractor 是一个开源的用于抽取维基百科语料库的工具,由 python 写成,通过这个工具可以很容易地从语料库中抽取出相关内容。使用方法如下:

1
2
$ git clone https://github.com/attardi/wikiextractor.git wikiextractor
$ wikiextractor/WikiExtractor.py -b 2000M -o zhwiki_extracted zhwiki-latest-pages-articles.xml.bz2

由于这个工具就是一个 python 脚本,因此无需安装,-b 参数指对提取出来的内容进行切片后每个文件的大小,如果要将所有内容保存在同一个文件,那么就需要把这个参数设得大一下,-o 的参数指提取出来的文件放置的目录,抽取出来的文件的路径为 zhwiki_extract/AA/wiki_00。更多参数可参考其 github 主页的说明。

抽取后的内容格式为每篇文章被一对 <doc> </doc> 包起来,而 <doc> 中的包含了属性有文章的 id、url 和 title 属性,如 <doc id="13" url="https://zh.wikipedia.org/wiki?curid=13" title="数学">

繁简转换

由上一步提取出来的中文维基百科中的语料中既有繁体字也有简体字,这里需要将其统一变为简体字,采用的工具也是开源的 OpenCC 转换器。使用方法如下:

1
2
3
$ git clone https://github.com/BYVoid/OpenCC.git
$ cd OpenCC && make && make install
$ opencc -i zhwiki_extract/AA/wiki_00 -o zhwiki_extract/zhs_wiki -c /home/nlp/OpenCC/data/config/t2s.json

我使用的是 centos,yum 源中找不到这个软件,因此通过编译安装最新的版本,需要注意的是编译 OpenCC 要求 gcc 的版本最低为 4.6 。其中 -i 表示输入文件路径, -o 表示输出的文件 ,-c 表示转换的配置文件,这里使用的繁体转简体的配置文件,OpenCC 自带了一系列的转换配置文件,可参考其 github 主页的说明。

去除标点符号

去除标点符号有两个问题需要解决,一是像下面这种为了解决各地术语名称不同的问题

1
他的主要成就包括Emacs及後來的GNU Emacs,GNU C 編譯器及-{zh-hant:GNU 除錯器;zh-hans:GDB 调试器}-。

另外一个就是将所有标点符号替换成空字符,通过正则表达式均可解决这两个问题,下面是具体实现的 python 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import re
import io

reload(sys)
sys.setdefaultencoding('utf-8')

def pre_process(input_file, output_file):
multi_version = re.compile(ur'-\{.*?(zh-hans|zh-cn):([^;]*?)(;.*?)?\}-')
punctuation = re.compile(u"[-~!@#$%^&*()_+`=\[\]\\\{\}\"|;':,./<>?·!@#¥%……&*()——+【】、;‘:“”,。、《》?「『」』]")
with io.open(output_file, mode = 'w', encoding = 'utf-8') as outfile:
with io.open(input_file, mode = 'r', encoding ='utf-8') as infile:
for line in infile:
line = multi_version.sub(ur'\2', line)
line = punctuation.sub('', line.decode('utf8'))
outfile.write(line)

if __name__ == '__main__':
if len(sys.argv) != 3:
print "Usage: python script.py input_file output_file"
sys.exit()
input_file, output_file = sys.argv[1], sys.argv[2]
pre_process(input_file, output_file)

分词

经过上面的步骤基本得到了都是简体中文的纯净文本,下面需要对其进行分词并且整理成每行一篇文本的格式,从而方便后续的处理。

分词采用 python 的分词工具 jieba,通过 pip install jieba 安装即可。且将一篇文章分词后的结果存储在一行,由前面可知,每篇文章存储在一对 <doc></doc> 标签中,由于前面去掉了标点,所以现在变成了 doc doc, 所以只要判断当前行为 doc 时即可认为文章结束,从而开始在新的一行记录下一篇文章的分词结果。实现的 python 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import io
import jieba

reload(sys)
sys.setdefaultencoding('utf-8')


def cut_words(input_file, output_file):
count = 0
with io.open(output_file, mode = 'w', encoding = 'utf-8') as outfile:
with io.open(input_file, mode = 'r', encoding = 'utf-8') as infile:
for line in infile:
line = line.strip()
if len(line) < 1: # empty line
continue
if line.startswith('doc'): # start or end of a passage
if line == 'doc': # end of a passage
outfile.write(u'\n')
count = count + 1
if(count % 1000 == 0):
print('%s articles were finished.......' %count)
continue
for word in jieba.cut(line):
outfile.write(word + ' ')
print('%s articles were finished.......' %count)

if __name__ == '__main__':
if len(sys.argv) < 3:
print "Usage: python script.py input_file output_file"
sys.exit()
input_file, output_file = sys.argv[1], sys.argv[2]
cut_words(input_file, output_file)

通过 Word2Vec 训练词向量

Word2vec 中包含了两种训练词向量的方法:Continuous Bag of Words (CBOW) 和 Skip-gram。CBOW 的目标是根据上下文来预测当前词语的概率。Skip-gram 刚好相反,根据当前词语来预测上下文的概率。这两种方法都利用人工神经网络作为它们的分类算法。起初,每个单词都是一个随机 N 维向量。训练时,该算法利用 CBOW 或者 Skip-gram 的方法获得了每个单词的最优向量。

最初 Google 开源的 Word2Vec 是用 C 来写的,后面陆续有了 Python ,Java 等语言的版本,这里采用的是 Python 版本的 gensim。通过 gensim 提供的 API 可以比较容易地进行词向量的训练。gensim 的建议通过 conda 安装,步骤如下:

1
2
3
4
$ wget https://repo.continuum.io/archive/Anaconda2-4.1.1-Linux-x86_64.sh
$ bash Anaconda2-4.1.1-Linux-x86_64.sh
$ conda update conda
$ conda install gensim

Linux 系统一般原来会带有 python,直接执行 python 命令可能会调用系统内置的 python 解释器,因此如果要使用 conda 安装的 python, 执行 python 命令的时候需要输入指定其通过 conda 安装的完整目录,或者将这个路径添加在环境变量 $PATH 之前。

下面是对上面处理后的语料库进行训练的一个简单例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python
# -*- coding: utf-8 -*-

import os, sys
import multiprocessing
import gensim

reload(sys)
sys.setdefaultencoding('utf-8')


def word2vec_train(input_file, output_file):
sentences = gensim.models.word2vec.LineSentence(input_file)
model = gensim.models.Word2Vec(sentences, size=300, min_count=10, sg=0, workers=multiprocessing.cpu_count())
model.save(output_file)
model.save_word2vec_format(output_file + '.vector', binary=True)


if __name__ == '__main__':
if len(sys.argv) < 3:
print "Usage: python script.py infile outfile"
sys.exit()
input_file, output_file = sys.argv[1], sys.argv[2]
word2vec_train(input_file, output_file)

上面的训练过程首先将输入的文件转为 gensim 内部的 LineSentence 对象,要求输入的文件的格式为每行一篇文章,每篇文章的词语以空格隔开

然后通过 gensim.models.Word2Vec 初始化一个 Word2Vec 模型,size 参数表示训练的向量的维数;min_count 表示忽略那些出现次数小于这个数值的词语,认为他们是没有意义的词语,一般的取值范围为(0,100);sg 表示采用何种算法进行训练,取 0 时表示采用 CBOW 模型,取 1 表示采用 skip-gram 模型;workers 表示开多少个进程进行训练,采用多进程训练可以加快训练过程,这里开的进程数与 CPU 的核数相等。

最后将训练后的得到的词向量存储在文件中,存储的格式可以是 gensim 提供的默认格式 (save 方法),也可以与原始 c 版本 word2vec 的 vector 相同的格式 (save_word2vec_format 方法),加载时分别采用 load 方法和 load_word2vec_format 方法即可。更详细的 API 可参考 https://rare-technologies.com/word2vec-tutorial/ 和 http://radimrehurek.com/gensim/models/word2vec.html。

假设我们训练好了一个语料库的词向量,当一些新的文章加入这个语料库时,如何训练这些新增的文章从而更新我们的语料库?将全部文章再进行一次训练显然是费时费力的,gensim 提供了一种类似于 “增量训练” 的方法。即可在原来的 model 基础上仅对新增的文章进行训练。如下所示为一个简单的例子:

1
2
model = gensim.models.Word2Vec.load(exist_model)
model.train(new_sentences)

上面的代码先加载了一个已经训练好的词向量模型,然后再添加新的文章进行训练,同样新增的文章的格式也要满足每行一篇文章,每篇文章的词语通过空格分开的格式。这里需要注意的是加载的模型只能 是通过 model.save() 存储的模型,从 model.save_word2vec_format() 恢复过来的模型只能用于查询.

通过 Glove 训练词向量

除了上面的 Word2Vec ,通过 Glove 也可以训练出词向量,只是这种方法并没有 Word2Vec 用得那么广泛。这里简单提及,也算是为训练词向量提供多一个选择。

首先需要下载并编译 Glove,步骤如下:

1
2
3
$ wget http://www-nlp.stanford.edu/software/GloVe-1.2.zip
$ unzip Glove-1.2.zip
$ cd Glove-1.2 && make

编译后会在 Glove-1.2 目录下生成一个 build 目录,里面包含了训练需要用到的工具。目录结构如下所示:

1
2
3
4
5
build/
|-- cooccur
|-- glove
|-- shuffle
`-- vocab_count

训练过程总共分为四步,对应上面的四个工具,顺序依次为 vocab_count --> cooccur --> shuffle --> glove,下面是具体的训练过程

1
2
3
4
5
6
7
$ build/vocab_count -min-count 5 -verbose 2 < zhs_wiki_cutwords > zhs_wiki_vocab

$ build/cooccur -memory 4.0 -vocab-file zhs_wiki_vocab -verbose 2 -window-size 5 < zhs_wiki_cutwords > zhs_wiki_cooccurence.bin

$ build/shuffle -memory 4.0 -verbose 2 < zhs_wiki_cooccurence.bin >zhs_wiki_shuff.bin

$ build/glove -save-file zhs_wiki_glove.vectors -threads 8 -input-file zhs_wiki_shuff.bin -vocab-file zhs_wiki_vocab -x-max 10 -iter 5 -vector-size 300 -binary 2 -verbose 2

上面四条命令分别对应于训练的四个步骤,每个步骤含义如下

  1. vocab_count 从语料库 (zhs_wiki_cutwords 是上面第一步处理好的语料库) 中统计词频,输出文件 zhs_wiki_vocab,每行为词语 词频-min-count 5 指示词频低于 5 的词舍弃,-verbose 2 控制屏幕打印信息的,设为 0 表示不输出

  2. cooccur 从语料库中统计词共现,输出文件 zhs_wiki_cooccurence.bin,格式为非文本的二进制;-memory 4.0 指示 bigram_table 缓冲器,-vocab-file 指上一步得到的文件,-verbose 2 同上,-window-size 5 指示词窗口大小。

  3. shufflezhs_wiki_cooccurence.bin 重新整理,输出文件 zhs_wiki_shuff.bin

  4. glove 训练模型,输出词向量文件。-save-file-threads-input-file-vocab-file 直接按照字面应该就可以理解了,-iter 表示迭代次数,-vector-size 表示向量维度大小,-binary 控制输出格式 0: save as text files; 1: save as binary; 2: both

训练后得到的二进制词向量模型格式与原始 c 版本 word2vec 的 vector 格式也相同,可以通过下面的方法统一加载使用。

使用词向量模型

训练好的词向量可以供后续的多项自然语言处理工作使用,下面是通过 gensim 加载训练好的词向量模型并进行查询的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# 加载模型
>>> from gensim.models import Word2Vec
>>> model = Word2Vec.load_word2vec_format('/home/nlp/zhs_wiki_trained.vector',binary = True)

# 词向量维度
>>> len(model[u'男人'])
300

# 具体词向量的值
>>> model[u'男人']
array([ 0.56559366, -1.96017861, -1.57303607, 1.2871722 , -1.38108838.....

# 词语相似性
>>> model.similarity(u'男人',u'女人')
0.86309866214314379

# 找某个词的近义词,反义词
>>> words = model.most_similar(u"男人")
>>> for word in words:
... print word[0], word[1]
...
女人 0.863098621368
女孩 0.67369633913
女孩子 0.658665597439
陌生人 0.654322624207
小女孩 0.637025117874
小孩 0.630155563354
男孩 0.625135600567
男孩子 0.617452859879
小孩子 0.613232254982
老婆 0.584552764893

>>> words = model.most_similar(positive=[u"女人", u"皇后"], negative=[u"男人"], topn=5)
>>> for word in words:
... print word[0], word[1]
...
皇太后 0.630089104176
太后 0.613425552845
王妃 0.581929504871
贵妃 0.581658065319
王后 0.577878117561

# 若干个词中剔除与其他最不相关的
>>> print model.doesnt_match(u"早餐 晚餐 午餐 食堂".split())
食堂
>>> print model.doesnt_match(u"早餐 晚餐 午餐 食堂 教室".split())
教室

# 多个词语的相似性
>>> model.n_similarity([u"女人", u"皇帝"], [u"男人", u"皇后"])
0.76359309631510597

这里并没有对训练出来的词向量质量进行评估,虽然 Google 提供了一种测试集,约 20000 句法和语义的测试实例(questions-words.txt),检查如 A对于B类似C对于D 这种线性平移关系。由于测试集是英文的,因此可以考虑翻译过来然后对中文的采用同样的评估方法,但是实际的效果还是要看具体应用中的效果。


参考: https://flystarhe.github.io/2016/09/04/word2vec-test/ https://flystarhe.github.io/2016/08/31/wiki-corpus-zh/ http://radimrehurek.com/gensim/models/word2vec.html https://rare-technologies.com/word2vec-tutorial/