R
NLP
text2vec

[翻訳] text2vec vignette: GloVeによる単語埋め込み

More than 1 year has passed since last update.

この文書は Dmitriy Selivanov によるRパッケージtext2vec (version 0.3.0) のビネット "GloVe Word Embeddings" の日本語訳です.
ただし文中の注は全て訳者によるものです1

License: MIT

関連文書


単語埋め込み

Tomas Mikolovらがword2vecのツールを公開して以来,単語のベクトル表現に関する論文がブームとなりました.それらの論文の中で最良のもののひとつはStanford大学のGloVe: Global Vectors for Word Representationです.この論文は,なぜこのようなアルゴリズムがうまくいくのか説明し,word2vecで使われた最適化を単語共起行列の因子分解の特別な場合として定式化し直しています.

ここではGloVeアルゴリズムを簡単に紹介し,text2vecにおける実装の使い方を示します.

GloVeアルゴリズム入門

GloVeアルゴリズムは以下のステップからなります.

  1. 単語の共起回数を数えて,単語共起行列$X$の形にまとめます.行列の各要素$X_{ij}$は,単語$i$が単語$j$の文脈でどのくらい頻繁に出現するかの程度を表しています.普通,次のようにしてコーパスを走査します.各単語に対して,あるウィンドウサイズの分だけ前後の範囲内で文脈語を探します.また,遠くの単語ほど小さな重みを割り当てるようにします.普通,重み付けには以下の式を使います. $$\text{decay} = 1 / \text{offset} .$$
  1. それぞれの単語対に対してソフトな制約を入れます. $$w_i^T w_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} < x_{max} \
1 & \text{otherwise}
\end{cases}
$$

標準的な例:言語の規則性(linguistic regularities)

さて,GloVeによる埋め込みがどのように機能するか調べてみましょう.周知の通り,word2vecの単語ベクトルは言語のいろいろな規則性をうまく捉えています.標準的な例として,もし"paris",france","italy"の単語ベクトルに
$$vector('paris') - vector('france') + vector('italy')$$

という操作を行えば,結果のベクトルは"rome"のベクトルに近いものになるでしょう.

word2vecでデモに使われたのと同じWikipediaのデータをダウンロードしましょう.

library(text2vec)
library(readr)
temp <- tempfile()
download.file('http://mattmahoney.net/dc/text8.zip', temp)
wiki <- read_lines(unz(temp, "text8"))
unlink(temp)

次のステップでは語彙,すなわち単語ベクトルを学習する対象となる単語の集合を作成します.text2vecの関数のうち,生のテキストデータに作用するもの(create_vocabulary, create_corpus, create_dtm, create_tcm)は全てストリーミングAPIを持っており,これらの関数の第一引数にトークンのイテレータを渡すべきです.

# トークンのイテレータを作成
tokens <- strsplit(wiki, split = " ", fixed = T)
# 語彙を作成.タームはユニグラム(単語)
vocab <- create_vocabulary(itoken(tokens))

語彙に含まれる単語は出現頻度が小さすぎてはいけません.たとえば,コーパス全体の中でたった一度しか出現しない単語に対しては,意味のある単語ベクトルを計算することはできません.ここでは最低5回は出現する単語のみ使うことにします.text2vecは余分な単語を語彙から除くためのオプションを他にも提供しています(?prune_vocabularyを見てください).

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

こうして語彙に含まれるタームは71,290個になり,ターム共起行列(term-co-occurence matrix, TCM)を構築する準備ができました.

# create_vocab_corpus関数にイテレータを渡す
it <- itoken(tokens)
# 余分な単語を除いた語彙を使う
vectorizer <- vocab_vectorizer(vocab,
                               # 入力文書のベクトル化はしない
                               grow_dtm = FALSE,
                               # 文脈語のウィンドウサイズは5
                               skip_grams_window = 5L)
tcm <- create_tcm(it, vectorizer)

TCMができたので,これをGloVeアルゴリズムで因子分解することができます.text2vecでは並列化された確率的勾配降下法を使用します.デフォルトではマシンの全てのコアを使いますが,必要ならコア数を指定することもできます.たとえば4スレッド使うにはRcppParallel::setThreadOptions(numThreads = 4)を呼びます.

モデルをフィットさせてみましょう.(フィッティングには数分かかるかもしれません!)2

fit <- glove(tcm = tcm,
             word_vectors_size = 50,
             x_max = 10, learning_rate = 0.2,
             num_iters = 15)
## 2016-01-10 14:12:37 - epoch 1, expected cost 0.0662
## 2016-01-10 14:12:51 - epoch 2, expected cost 0.0472
## 2016-01-10 14:13:06 - epoch 3, expected cost 0.0429
## 2016-01-10 14:13:21 - epoch 4, expected cost 0.0406
## 2016-01-10 14:13:36 - epoch 5, expected cost 0.0391
## 2016-01-10 14:13:50 - epoch 6, expected cost 0.0381
## 2016-01-10 14:14:05 - epoch 7, expected cost 0.0373
## 2016-01-10 14:14:19 - epoch 8, expected cost 0.0366
## 2016-01-10 14:14:33 - epoch 9, expected cost 0.0362
## 2016-01-10 14:14:47 - epoch 10, expected cost 0.0358
## 2016-01-10 14:15:01 - epoch 11, expected cost 0.0355
## 2016-01-10 14:15:16 - epoch 12, expected cost 0.0351
## 2016-01-10 14:15:30 - epoch 13, expected cost 0.0349
## 2016-01-10 14:15:44 - epoch 14, expected cost 0.0347
## 2016-01-10 14:15:59 - epoch 15, expected cost 0.0345

こうして単語ベクトルが得られます.

word_vectors <- fit$word_vectors[[1]] + fit$word_vectors[[2]]
rownames(word_vectors) <- rownames(tcm)

例に挙げたparis - france + italyに最も近いベクトルを見つけることができます3

word_vectors_norm <- sqrt(rowSums(word_vectors ^ 2))

rome <- word_vectors['paris', , drop = FALSE] -
  word_vectors['france', , drop = FALSE] +
  word_vectors['italy', , drop = FALSE]

cos_dist <- text2vec:::cosine(rome,
                              word_vectors,
                              word_vectors_norm)
head(sort(cos_dist[1,], decreasing = T), 10)
##     paris    venice     genoa      rome  florence
## 0.7811252 0.7763088 0.7048109 0.6696540 0.6580989

skip_grams_windowglove()関数のパラメータ(単語ベクトルのサイズや反復回数を含みます)を変えて実験すれば,はるかに良い結果を得ることができます.さらなる詳細とWikipediaデータによる大規模実験については私のブログの記事を見てください.



  1. 訳者の環境を示しておく. 

    devtools::session_info()
    
    ## Session info --------------------------------------------------------------
    
    ##  setting  value                       
    ##  version  R version 3.3.1 (2016-06-21)
    ##  system   x86_64, mingw32             
    ##  ui       RTerm                       
    ##  language (EN)                        
    ##  collate  Japanese_Japan.932          
    ##  tz       Asia/Tokyo                  
    ##  date     2016-09-04
    
    ## Packages ------------------------------------------------------------------
    
    ##  package       * version date       source        
    ##  assertthat      0.1     2013-12-06 CRAN (R 3.3.1)
    ##  chron           2.3-47  2015-06-24 CRAN (R 3.3.1)
    ##  codetools       0.2-14  2015-07-15 CRAN (R 3.3.1)
    ##  data.table      1.9.6   2015-09-19 CRAN (R 3.3.1)
    ##  devtools        1.12.0  2016-06-24 CRAN (R 3.3.1)
    ##  digest          0.6.10  2016-08-02 CRAN (R 3.3.1)
    ##  evaluate        0.9     2016-04-29 CRAN (R 3.3.1)
    ##  foreach         1.4.3   2015-10-13 CRAN (R 3.2.2)
    ##  formatR         1.4     2016-05-09 CRAN (R 3.3.1)
    ##  htmltools       0.3.5   2016-03-21 CRAN (R 3.3.1)
    ##  iterators       1.0.8   2015-10-13 CRAN (R 3.2.2)
    ##  knitr           1.14    2016-08-13 CRAN (R 3.3.1)
    ##  lattice         0.20-33 2015-07-14 CRAN (R 3.3.1)
    ##  magrittr        1.5     2014-11-22 CRAN (R 3.3.1)
    ##  Matrix          1.2-6   2016-05-02 CRAN (R 3.3.1)
    ##  memoise         1.0.0   2016-01-29 CRAN (R 3.3.1)
    ##  Rcpp            0.12.6  2016-07-19 CRAN (R 3.3.1)
    ##  RcppParallel    4.3.20  2016-08-16 CRAN (R 3.3.1)
    ##  readr         * 1.0.0   2016-08-03 CRAN (R 3.3.1)
    ##  RevoUtils       10.0.1  2016-08-24 local         
    ##  RevoUtilsMath * 8.0.3   2016-04-13 local         
    ##  rmarkdown       1.0     2016-07-08 CRAN (R 3.3.1)
    ##  stringi         1.1.1   2016-05-27 CRAN (R 3.3.0)
    ##  stringr         1.1.0   2016-08-19 CRAN (R 3.3.1)
    ##  text2vec      * 0.3.0   2016-03-31 CRAN (R 3.3.1)
    ##  tibble          1.2     2016-08-26 CRAN (R 3.3.1)
    ##  withr           1.0.2   2016-06-20 CRAN (R 3.3.1)
    ##  yaml            2.1.13  2014-06-12 CRAN (R 3.3.1)
    
  2. 以下の実行結果は原文におけるもの. 

  3. 実験の結果は乱数に依存する.ここでの結果は原文に記載されているもの(訳者の実験ではromeが出てこなかったので……).