Word embeddings

After Tomas Mikolov et al. released the word2vec tool, there was a boom of articles about word vector representations. One of the best of these articles is Stanford’s GloVe: Global Vectors for Word Representation, which explained why such algorithms work and reformulated word2vec optimizations as a special kind of factoriazation for word co-occurence matrices.

Here I will briefly introduce the GloVe algorithm and show how to use its text2vec implementation.

GloVe algorithm

THe GloVe algorithm consists of following steps:

  1. Collect word co-occurence statistics in a form of word co-ocurrence matrix \(X\). Each element \(X_{ij}\) of such matrix represents how often word i appears in context of word j. Usually we scan our corpus in the following manner: for each term we look for context terms within some area defined by a window_size before the term and a window_size after the term. Also we give less weight for more distant words, usually using this formula: \[decay = 1/offset\]

  2. Define soft constraints for each word pair: \[w_i^Tw_j + b_i + b_j = log(X_{ij})\] Here \(w_i\) - vector for the main word, \(w_j\) - vector for the context word, \(b_i\), \(b_j\) are scalar biases for the main and context words.

  3. Define a cost function \[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\] Here \(f\) is a weighting function which help us to prevent learning only from extremely common word pairs. The GloVe authors choose the following function:

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

Linguistic regularities

Now let’s examine how GloVe embeddings works. As commonly known, word2vec word vectors capture many linguistic regularities. To give the canonical example, if we take word vectors for the words “paris,” “france,” and “germany” and perform the following operation:

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

the resulting vector will be close to the vector for “rome.”

Let’s download the same Wikipedia data used as a demo by word2vec:

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)

In the next step we will create a vocabulary, a set of words for which we want to learn word vectors. Note, that all of text2vec’s functions which operate on raw text data (create_vocabulary, create_corpus, create_dtm, create_tcm) have a streaming API and you should iterate over tokens as the first argument for these functions.

# 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)

These words should not be too uncommon. Fot example we cannot calculate a meaningful word vector for a word which we saw only once in the entire corpus. Here we will take only words which appear at least five times. text2vec provides additional options to filter vocabulary (see ?prune_vocabulary).

vocab = prune_vocabulary(vocab, term_count_min = 5L)

Now we have 71,290 terms in the vocabulary and are ready to construct term-co-occurence matrix (TCM).

# Use our filtered vocabulary
vectorizer = vocab_vectorizer(vocab)
# use window of 5 for context words
tcm = create_tcm(it, vectorizer, skip_grams_window = 5L)

Now we have a TCM matrix and can factorize it via the GloVe algorithm.
text2vec uses a parallel stochastic gradient descent algorithm. By default it will use all cores on your machine, but you can specify the number of cores if you wish. For example, to use 4 threads call RcppParallel::setThreadOptions(numThreads = 4).

Let’s fit our model. (It can take several minutes to fit!)

glove = GlobalVectors$new(word_vectors_size = 50, vocabulary = vocab, x_max = 10)
wv_main = glove$fit_transform(tcm, n_iter = 10, convergence_tol = 0.01)
## INFO [2017-08-08 11:20:22] 2017-08-08 11:20:22 - epoch 1, expected cost 0.0878
## INFO [2017-08-08 11:20:25] 2017-08-08 11:20:25 - epoch 2, expected cost 0.0611
## INFO [2017-08-08 11:20:28] 2017-08-08 11:20:28 - epoch 3, expected cost 0.0539
## INFO [2017-08-08 11:20:30] 2017-08-08 11:20:30 - epoch 4, expected cost 0.0500
## INFO [2017-08-08 11:20:33] 2017-08-08 11:20:33 - epoch 5, expected cost 0.0475
## INFO [2017-08-08 11:20:36] 2017-08-08 11:20:36 - epoch 6, expected cost 0.0457
## INFO [2017-08-08 11:20:39] 2017-08-08 11:20:39 - epoch 7, expected cost 0.0443
## INFO [2017-08-08 11:20:42] 2017-08-08 11:20:42 - epoch 8, expected cost 0.0433
## INFO [2017-08-08 11:20:44] 2017-08-08 11:20:44 - epoch 9, expected cost 0.0424
## INFO [2017-08-08 11:20:47] 2017-08-08 11:20:47 - epoch 10, expected cost 0.0417
## [1] 71290    50

Alternatively we can train model with R’s S3 interface (but keep in mind that all text2vec models are R6 classes and they are mutable! So fit_transform methods modify models!):

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

Note that model learns two sets of word vectors - main and context. Essentially they are the same since model is symmetric. From our experience learning two sets of word vectors leads to higher quality embeddings. GloVe model is “decomposition” model (inherits from mlapiDecomposition - generic class of models which decompose input matrix into two low-rank matrices). So on par with any other mlapiDecomposition model second low-rank matrix (context word vectors) is available in components field:

wv_context = glove$components
## [1]    50 71290

Note that as in all models which inherit from mlapiDecomposition transformed matrix will has nrow = nrow(input), ncol = rank and second component matrix will has nrow = rank, ncol = ncol(input).

While both of word-vectors matrices can be used as result it usually better (idea from GloVe paper) to average or take a sum of main and context vector:

word_vectors = wv_main + t(wv_context)

We can find the closest word vectors for our paris - france + germany example:

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)
##     paris    berlin    munich   leipzig   germany 
## 0.7736693 0.7444342 0.6592530 0.6482025 0.6062091
# berlin     paris    munich    leipzig   germany 
# 0.8015347 0.7623165 0.7013252 0.6616945 0.6540700 

You can achieve much better results by experimenting with skip_grams_window and the parameters of the GloVe class (including word vectors size and the number of iterations). For more details and large-scale experiments on wikipedia data see this old post on my blog.

text2vec is created by Dmitry Selivanov and contributors. © 2016.
If you have found any BUGS please report them here.