词向量

Tomas Mikolov 等人发布了 word2vec 工具之后,出现了一系列关于词向量表示的一系列文章。其中最出色的一篇是斯坦福大学的 GloVe: Global Vectors for Word Representation。这个方法将 word2vec 的优化技术进行了改进,用一种特殊的 factoriazation 方法来处理词共现矩阵。

这里我会简要地介绍 GloVe 的算法,并演示如何使用 text2vec 的实现。

GloVe algorithm

GloVe 算法包含以下步骤:

  1. 使用词共现矩阵收集词共现信息。每个 \(X_{ij}\) 元素代表词汇 i 出现在词汇 j上下文的概率。一般我们需要扫描一遍我们的语料库:对于每一个字段,我们查看在这个字段之前或者之后,使用 window_size 定义的一定范围内的上下文。一个词距离这个字段越远,我们给予这个词的权重越低。一般使用这个公式: \[decay = 1/offset\]
  1. 对于每一组词对,\[w_i^Tw_j + b_i + b_j = log(X_{ij})\] 这里 \(w_i\) 向量代表主词,\(w_j\) 代表上下文词的向量。\(b_i\), \(b_j\) 是主词和上下文词的常数偏倚。
  1. 定义损失函数

\[J = \sum_{i=1}^V \sum_{j=1}^V \; f(X_{ij}) ( w_i^T w_j + b_i + b_j - \log X_{ij})^2\]

这里 \(f\) 是帮助我们避免只学习到常见词到一个权重函数。GloVe 到作者选择如下到函数:

\[ f(X_{ij}) = \begin{cases} (\frac{X_{ij}}{x_{max}})^\alpha & \text{if } X_{ij} < XMAX \\ 1 & \text{otherwise} \end{cases} \]

语义规则 Linguistic regularities

现在让我们来看一下 GloVe 词向量是怎么样工作的。一般来说,word2vec 词向量保留了语义规则。一个简单的例子,如果我们将 “paris,” “france,” and “germany” 进行下面的操作:

\[vector("paris") - vector("france") + vector("germany")\]

输出向量将会十分接近 “rome”。

让我们来下载 Wikipedia 数据来展示 word2vec 的例子:

library(text2vec)
text8_file = "~/text8"
if (!file.exists(text8_file)) {
  download.file("http://mattmahoney.net/dc/text8.zip", "~/text8.zip")
  unzip ("~/text8.zip", files = "text8", exdir = "~/")
}
wiki = readLines(text8_file, n = 1, warn = FALSE)

在下一个步骤中,我们将会生成一个词汇表,一系列我们希望学习的词向量。注意到,所有 text2vec 函数在出文本数据上进行操作,create_vocabulary, create_corpus, create_dtm, create_tcm,这些函数有一个 streaming 流 API,你可以在第一个参数位置使用迭代器。

# Create iterator over tokens
tokens <- space_tokenizer(wiki)
# Create vocabulary. Terms will be unigrams (simple words).
it = itoken(tokens, progressbar = FALSE)
vocab <- create_vocabulary(it)

这些词不能稀有词,比如对于一个只出现过一次的词,我们不能计算出一个有意义的词向量。这里我们只使用那些在整个文档中出现过至少 5 次的词。text2vec 提供了额外的用来筛选词汇的选项。(见 ?prune_vocabulary

vocab <- prune_vocabulary(vocab, term_count_min = 5L)

现在词汇表有 71,290 个字段,我们可以开始构建字段共现矩阵 term-co-occurence matrix (TCM)。

# Use our filtered vocabulary
vectorizer <- vocab_vectorizer(vocab,
                               # don't vectorize input
                               grow_dtm = FALSE,
                               # use window of 5 for context words
                               skip_grams_window = 5L)
tcm <- create_tcm(it, vectorizer)

TCM 矩阵已经构建好了,我们可以使用 GloVe 算法来分解它。

text2vec 使用了一个并行随机梯度递降算法,默认它会使用机器上的所有核心,你也可以使用你所想要使用的核心数。比如,使用 4 线程,RcppParallel::setThreadOptions(numThreads = 4)

让我们来拟合我们的模型,这可能会花上几分钟。

glove = GlobalVectors$new(word_vectors_size = 50, vocabulary = vocab, x_max = 10)
glove$fit(tcm, n_iter = 20)
# 2016-10-03 10:09:14 - epoch 1, expected cost 0.0893
# 2016-10-03 10:09:17 - epoch 2, expected cost 0.0608
# 2016-10-03 10:09:19 - epoch 3, expected cost 0.0537
# 2016-10-03 10:09:22 - epoch 4, expected cost 0.0499
# 2016-10-03 10:09:25 - epoch 5, expected cost 0.0475
# 2016-10-03 10:09:28 - epoch 6, expected cost 0.0457
# 2016-10-03 10:09:30 - epoch 7, expected cost 0.0443
# 2016-10-03 10:09:33 - epoch 8, expected cost 0.0431
# 2016-10-03 10:09:36 - epoch 9, expected cost 0.0423
# 2016-10-03 10:09:39 - epoch 10, expected cost 0.0415
# 2016-10-03 10:09:42 - epoch 11, expected cost 0.0408
# 2016-10-03 10:09:44 - epoch 12, expected cost 0.0403
# 2016-10-03 10:09:47 - epoch 13, expected cost 0.0400
# 2016-10-03 10:09:50 - epoch 14, expected cost 0.0395
# 2016-10-03 10:09:53 - epoch 15, expected cost 0.0391
# 2016-10-03 10:09:56 - epoch 16, expected cost 0.0388
# 2016-10-03 10:09:59 - epoch 17, expected cost 0.0385
# 2016-10-03 10:10:02 - epoch 18, expected cost 0.0383
# 2016-10-03 10:10:05 - epoch 19, expected cost 0.0380
# 2016-10-03 10:10:08 - epoch 20, expected cost 0.0378

或者我们也可以使用 R 的 S3 接口。(注意所有 text2vec 模型是 R6 类,他们是可变的,所以 fit, fit_transform 方法会修改模型 )

glove = GlobalVectors$new(word_vectors_size = 50, vocabulary = vocab, x_max = 10)
# `glove` object will be modified by `fit()` call !
fit(tcm, glove, n_iter = 20)

现在我们可以取得词向量。

word_vectors <- glove$get_word_vectors()

我们可以获得距离 paris - france + germany 最近的词向量。

berlin <- word_vectors["paris", , drop = FALSE] -
  word_vectors["france", , drop = FALSE] +
  word_vectors["germany", , drop = FALSE]
cos_sim = sim2(x = word_vectors, y = berlin, method = "cosine", norm = "l2")
head(sort(cos_sim[,1], decreasing = TRUE), 5)
# berlin     paris    munich    leipzig   germany
# 0.8015347 0.7623165 0.7013252 0.6616945 0.6540700

你可以使用 skip_grams_window 以及 GloVe 类的参数(包括词向量大小以及迭代次数)来获得更好的结果。更多细节可以参考我的这篇 历史博文

text2vecDmitry Selivanov 和其他开发者一起开发。 © 2016.
如果您发现了 bugs 等问题,请到GitHub 报告。