Tomas Mikolov 等人发布了 word2vec 工具之后,出现了一系列关于词向量表示的一系列文章。其中最出色的一篇是斯坦福大学的 GloVe: Global Vectors for Word Representation。这个方法将 word2vec 的优化技术进行了改进,用一种特殊的 factoriazation 方法来处理词共现矩阵。
这里我会简要地介绍 GloVe 的算法,并演示如何使用 text2vec 的实现。
GloVe 算法包含以下步骤:
\[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} \]
现在让我们来看一下 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
类的参数(包括词向量大小以及迭代次数)来获得更好的结果。更多细节可以参考我的这篇 历史博文。