词向量(Word2vec)是 2013 年发布的一种自然语言处理技术。word2vec 算法使用神经网络模型从大量文本语料库中学习单词的向量表示。训练好的模型可以用作同义词挖掘(detect synonymous words)、关联词挖掘等任务。词向量可以通过余弦相似度计算量化词之间的相关度。
背景
独热
one-hot representations 即独热表示,在词向量出现之前,一个词用 one hot 进行编码,其方法是使用 N 位状态寄存器来对 N 个状态进行编码,每个状态都由它独立的寄存器位,并且在任意时候,其中只有一位有效。如下图所示,这种方式简单粗暴,将一个词用一个只有一个位置为 1,其他地方为 0 的向量表示。1 的位置就代表了是什么词。
其缺点非常明显:
- 占用空间大;如果特征值的数目特别多,就会产生大量冗余的稀疏矩阵。
- 词与词之间的向量是正交关系,没有任何语义关联。
词向量
为了克服 one-hot 的缺点,distributional representations 的词向量应运而生,它将单词从原先所属的空间(一般是 one-hot encoding)映射到新的低维空间中去,同时,低维空间的词向量还能表达出一些语义,如词的相似性。
2013 年就职于 google 的捷克人 Tomáš Mikolov 领导的一组研究人员提出了 word2vec 的方法,它是一组用于生成词向量的相关模型。这些模型是浅层的神经网络,经过训练可以重建单词的上下文。
word2vec
主要思想
word2vec 的主要思想是一个词的上下文可以很好的表达出词的语义,它是一种通过无监督(本质还是有监督,只是无需人工标注)的学习文本来用产生词向量的方式。
模型
word2vec 有两种经典的模型架构:
- continuous bag-of-words(CBOW),中文称之为词袋模型,通俗一点讲,从周围上下文词的窗口中预测中心词(当前词)。
- skip-gram,和词袋模型恰恰相反,已知中心词预测周围上下文词。
CBOW 和 skip-gram 的模型架构都是一层单层的神经网络,如下图所示,需要注意的部分是:神经网络的参数就是我们最后得到的词向量,神经网络训练过程就是学习词向量(网络参数)的过程。
CBOW 模型分析:
- input 层:
输入的是上下文单词的 one-hot 编码。假设单词向量空间的维度为 V,即整个词库大小为 V,上下文单词窗口的大小为 C。所以输入大小 C × V。
- hidden 层:
假设 hidden layer 最终得到的词向量的维度大小为 N,input 到 hidden 的权值共享矩阵(“共享”即每个词乘的 W 一样)为 W。W 的大小为 V × N,并且初始化。
我们将 C 个 1 × V 大小的向量分别同一个上述所说的 V × N 大小的 W 相乘,得到的是 C 个 1 × N 大小的向量。
再将这 C 个 1 × N 大小的向量取平均,得到一个 1 × N 大小的向量。 - output 层:
初始化输出权重矩阵大小为 N × V 的 W'
将 hidden layer 1 × N 的向量与 W' 相乘,并且用 softmax 处理,得到 1 × V 的向量,此向量的每一维代表词库中的一个词。概率中最大的 index 所代表的单词即为预测出的中间词。
模型训练
word2vec 训练结果对参数很敏感。以下是 word2vec 训练中的一些重要参数。
训练算法
word2vec 模型可以使用分层 softmax 和/或负采样进行训练。为了逼近模型寻求最大化的条件对数似然,分层 softmax 方法使用 Huffman 树来减少计算。另一方面,负采样方法通过最小化采样负实例的对数似然来解决最大化问题。根据作者的说法,分层 softmax 对不常用的词效果更好,而负采样对常用词效果更好,对低维向量效果更好。随着训练 epoch 的增加,分层 softmax 不再有用。
二次采样
高频词通常提供的信息很少。可以对频率高于某个阈值的单词进行二次采样以加快训练速度。
维度
词嵌入向量的质量随着维度的增大而增加。但是在达到某个点之后,边际收益会减少。通常,向量的维数设置在 100 到 1000 之间。
上下文窗口大小
上下文窗口的大小决定了在给定词之前和之后有多少词被包括作为给定词的上下文词。根据作者的说明,skip-gram 的推荐值为 10,CBOW 的推荐值为 5。
扩展
已经提出了 word2vec 的扩展,以从整个文档(而不是单个单词)构建嵌入。这个扩展称为 paragraph2vec 或 doc2vec,并已在 C、Python 和 Java/Scala 工具中实现。
实战
使用开源的 gensim 库,具体代码如下:
import jieba
import psycopg2
from gensim.models import word2vec
from lxml.html import clean
def remove_html_tags(text):
"""Remove html tags from a string"""
import re
tag_clean = re.compile('<.*?>')
return re.sub(tag_clean, '', text)
def segment_sen(sen):
sen_list = []
try:
sen_list = jieba.lcut(sen)
except:
pass
return sen_list
if __name__ == '__main__':
stopwords = set()
stopwords.add(' ')
with open('stopwords.txt') as f:
for r in f.readlines():
stopwords.add(r.strip())
conn = psycopg2.connect(host='xxx.xxx.xxx.xxx', port=5432, user='xxx', password='xxxx', dbname='xxxxx')
cursor = conn.cursor()
cursor.execute(
'select title, content from xxx',
)
all_list = cursor.fetchall()
if all_list:
sentence_list_list = []
cleaner = clean.Cleaner(kill_tags=['pre'])
for tup in all_list:
ss = segment_sen(tup[0])
if ss:
ss = [s for s in ss if s not in stopwords]
if ss:
sentence_list_list.append(ss)
content = cleaner.clean_html(tup[1])
content = remove_html_tags(content)
content = content.split('\n')
for c in content:
ss = segment_sen(c)
if ss:
ss = [s for s in ss if s not in stopwords]
if ss:
sentence_list_list.append(ss)
model = word2vec.Word2Vec(sentence_list_list, min_count=1)
model.save("word2vec.model")
topn = model.wv.most_similar('python')
print(topn)