[翻訳] text2vec vignette: text2vecパッケージによるテキスト分析


この文書は Dmitriy Selivanov によるRパッケージ[`text2vec`](https://cran.r-project.org/web/packages/text2vec/) (version 0.3.0) のビネット "[Analyzing Texts with the text2vec Package](https://cran.r-project.org/web/packages/text2vec/vignettes/text-vectorization.html)" の日本語訳です．
ただし文中の注は全て訳者によるものです[^ss]．

License: [MIT](https://opensource.org/licenses/MIT)

**関連文書**

+ text2vec vignette: text2vecパッケージによるテキスト分析
+ [text2vec vignette: GloVeによる単語埋め込み](http://qiita.com/nakamichi/items/69142b6d19f307ec8d9d)
+ [text2vec vignette: 発展的な話題](http://qiita.com/nakamichi/items/9805cd3831f674d81dcd)

----

<!-- # Features -->
# 特徴
<!-- **text2vec** provides an efficient framework with a concise API for text analysis and natural language processing (NLP) in R.  -->
**text2vec** はRによるテキスト分析と自然言語処理のための，簡潔なAPIを備えたフレームワークを提供します．

<!-- This package is efficient because it is carefully written in C++, which also means that text2vec is memory friendly. Some parts, such as training GloVe word embeddings are fully parallelized using the excellent [RcppParallel](http://rcppcore.github.io/RcppParallel/) package. This means that the word embeddings are computed in parallel on OS X, Linux, Windows, and Solaris (x86) without any additional tuning or tricks. Finally, a streaming API means that  users do not have to load all the data into RAM. -->
このパッケージはC++によって注意深く書かれているため効率的で，メモリにも優しいです．箇所によっては優れた[RcppParallel](http://rcppcore.github.io/RcppParallel/)パッケージによって完全に並列化されており，GloVeによる単語埋め込みの訓練はその一例です．つまり，OS XでもLinuxでもWindowsでもSolaris（x86）でも，余計なチューニングをしたり技巧を凝らすことなしに単語埋め込みが並列に計算されます．また，ストリーミングAPIがあるのでユーザは全てのデータをRAMに読み込む必要がありません．

<!-- This vignette explains how to use text2vec to vectorize text on arbitrary n-grams using either a vocabulary or feature hashing. See the `glove` vignette for an explanation of how to use state-of-the art [GloVe](http://www-nlp.stanford.edu/projects/glove/) word embeddings with this package.  -->
このビネットではtext2vecを使い，任意のnグラムの語彙か素性ハッシングを用いてテキストをベクトル化する方法を説明します．最先端の[GloVe](http://www-nlp.stanford.edu/projects/glove/)単語埋め込みをこのパッケージで使う方法の解説については`glove`ビネットを見てください．

<!-- # Text vectorization -->
# テキストのベクトル化

<!-- Most text mining and NLP modeling use [bag-of-words](https://en.wikipedia.org/wiki/Bag-of-words_model) or [bag-of-n-grams](https://en.wikipedia.org/wiki/N-gram) methods. Despite their simplicity, these models usually demonstrate good performance on text categorization and classification tasks. But, in contrast to their theoretical simplicity and practical efficiency, building bag-of-words models involves technical challenges. This is especially the case in R because of its copy-on-modify semantics.  -->
テキストマイニングや自然言語処理の大抵のモデリングでは[bag-of-words](https://en.wikipedia.org/wiki/Bag-of-words_model)か[bag-of-n-grams](https://en.wikipedia.org/wiki/N-gram)という方法が使われます[^bow]．これらのモデルは単純であるにもかかわらず，テキストの分類タスクにおいて通常は高い性能を発揮します．ですが，その理論的な単純さと実用的な効率性とは対照的に，bag-of-wordsモデルの構築は技術的には難問です．Rの場合はコピー時修正（copy-on-modify）セマンティクスがあるので特に難しいです．

<!-- ## Text analysis pipeline  -->
## テキスト分析のパイプライン

<!-- Let's briefly review some details of a typical text analysis pipeline :   -->
典型的なテキスト分析と分析のパイプラインの細部について，簡単に振り返ってみましょう．

<!-- 1. The reseacher usually begins by constructing a [document-term matrix](https://en.wikipedia.org/wiki/Document-term_matrix) (DTM) from input documents. In other words, the first step is to  *vectorize text* by creating a map from words or n-grams to a [vector space](https://en.wikipedia.org/wiki/Vector_space_model). -->
1. 研究者は通常，入力文書から[文書ターム行列（document-term matrix，DTM）](https://en.wikipedia.org/wiki/Document-term_matrix)を構築することから始めます．言いかえると，最初のステップは単語やnグラムを[ベクトル空間](https://en.wikipedia.org/wiki/Vector_space_model)に写像して*文書をベクトル化する*ことです．

<!-- 2. The researcher fits a model to that DTM. These models might include text classification, topic modeling, or word embedding. Fitting the model will include tuning and validating the model. -->
2. 研究者はモデルをDTMにフィッティングします．モデルはテキスト分類器かもしれませんし，トピックモデルや単語埋め込みかもしれません．モデルのフィッティングにはモデルのチューニングや検証も含みます．

<!-- 3. Finally the researcher applies the model to new data. -->
3. 最後に，研究者はモデルを新しいデータに適用します．

<!-- In this vignette we will primarily discuss first stage. Texts themselves can take up a lot of memory, but vectorized texts usually do not, because they are stored as sparse matrices. Because of R's copy-on-modify semantics, it is not easy to iteratively grow a DTM. So constructing a DTM, even for a small collections of documents, can be a serious bottleneck for analysts and researchers. It involves reading the whole collection of text documents into RAM and processing it as single vector, which can easily increase memory use by a factor of 2 to 4. The text2vec package solves this problem by providing a better way of constructing a document-term matrix. -->
このビネットでは主に1番目の段階について議論します．テキストそれ自体は大量のメモリを消費することがありますが，ベクトル化されたテキストは疎行列として保存されるので，普通はあまりメモリを消費しません．Rにはコピー時修正セマンティクスがあるので，反復的にDTMを大きくしていくことが容易ではありません．それゆえ，小さな文書集合であってもDTMの構築が分析者や研究者にとっての深刻なボトルネックとなり得ます．RでDTMを構築するには文書集合の全体をRAMに読み込んで単一のベクトルとして処理する必要がありますが，そうするとメモリ使用量が容易に2～4倍まで増大してしまいます．text2vecパッケージはより優れた方法で文書ターム行列を構築することでこの問題を解決しています．

<!-- ## Example: Sentiment analysis on IMDB movie review dataset -->
## 例：IMDB映画レビューデータセットのセンチメント分析

<!-- This package provides the `movie_review` dataset. It consists of 5000 movie reviews, each of which is marked as positive or negative. -->
text2vecパッケージは`movie_review`データセットを提供します．このデータは5000件の映画レビューからなっており，それぞれのレビューがポジティブなものかネガティブなものか印が付けてあります．


```r
library(text2vec)
data("movie_review")
set.seed(42L)
```

<!-- To represent documents in vector space, we first have to create `term -> term_id` mappings. We call them *terms* instead of *words*, because they can be arbitrary n-grams, not just single words. We represent a set of documents as a sparse matrix, where each row corresponds to a document and each column corresponds to a term. This can be done in 2 ways: using the vocabulary itself or by [feature hashing](https://en.wikipedia.org/wiki/Feature_hashing). -->
文書をベクトル空間で表現するには，まずタームにタームIDを対応させる必要があります．*単語*ではなく*ターム*と呼ぶのは，タームは1語とは限らず任意のnグラムでよいからです．それから文書集合を疎行列で表現し，各行が1つの文書に，各列が1つのタームに対応するものとします．これにはやり方が2通りあります．語彙そのものを使うか，[素性ハッシング](https://ja.wikipedia.org/wiki/Feature_Hashing)を使うかです．

<!-- ### Vocabulary based vectorization -->
### 語彙に基づくベクトル化

<!-- Let's first create a vocabulary-based DTM. Here we collect unique terms from all documents and mark each of them with a unique_id. Using the `vocabulary()` function. We use an iterator to create the vocabulary. -->
まず語彙に基づくDTMの方を作りましょう．全ての文書からタームを重複なく集め，`vocabulary()`関数を使って各タームに一意のIDを割り当てます．語彙の作成にはイテレータを使います．


```r
it <- itoken(movie_review$review, 
             preprocess_function = tolower, 
             tokenizer = word_tokenizer, 
             ids = movie_review$id)

sw <- c("i", "me", "my", "myself", "we", "our", "ours", "ourselves", "you", "your", "yours")
vocab <- create_vocabulary(it, stopwords = sw)
```

<!-- Alternatively, if your data fits in RAM, you can once create list of tokens and the reuse it in further steps: -->
あるいは，データがRAMに収まるのであれば，トークンのリストを一度作っておいてそれを以降のステップで使いまわすこともできます．


```r
# リストの各要素が文書を表す
tokens <- movie_review$review %>% 
  tolower() %>% 
  word_tokenizer()
it <- itoken(tokens, ids = movie_review$id)
vocab <- create_vocabulary(it, stopwords = sw)
```

<!-- Now that we have a vocabulary, we can construct a document-term matrix. (We could instead use `create_corpus()` and `get_dtm()`). -->
語彙ができたので，文書ターム行列を構築することができます（代わりに`create_corpus()`と`get_dtm()`を使うこともできるでしょう）．



```r
it <- itoken(tokens, ids = movie_review$id)
# Or
# it <- itoken(movie_review$review, tolower, word_tokenizer, ids = movie_review$id)
vectorizer <- vocab_vectorizer(vocab)
dtm <- create_dtm(it, vectorizer)
```

<!-- Now we have a DTM and can check its dimensions. -->
DTMを作ったので，行列の次元を確認できます．


```r
str(dtm)
```

```
## Formal class 'dgCMatrix' [package "Matrix"] with 6 slots
##   ..@ i       : int [1:706047] 4999 4999 4999 4999 4999 4999 4998 4998 4998 49..
##   ..@ p       : int [1:42642] 0 1 2 3 4 5 6 7 8 9 ...
##   ..@ Dim     : int [1:2] 5000 42641
##   ..@ Dimnames:List of 2
##   .. ..$ : chr [1:5000] "5814_8" "2381_9" "7759_3" "3630_4" ...
##   .. ..$ : chr [1:42641] "decent.the" "nudity.i" "nowadays.i" "fantastic.it" ...
##   ..@ x       : num [1:706047] 1 1 1 1 1 1 1 1 1 1 ...
##   ..@ factors : list()
```

```r
identical(rownames(dtm), movie_review$id)
```

```
## [1] TRUE
```

<!-- As you can see, the DTM has 5000 rows, equal to the number of documents, and 42641 columns, equal to the number of unique terms. -->
見ての通りDTMの行数は5000であり，文書の数に等しいです．また列数は5000で，ユニークなタームの数に等しいです．

<!-- Now we are ready to fit our first model. Here we will use the `glmnet` package to fit a logistic regression model with an L1 penalty. -->
さて最初のモデルをフィッティングする準備ができました．ここでは`glmnet`パッケージを使い，L1罰則付きのロジスティック回帰モデルをフィットさせてみます．


```r
library(glmnet)
fit <- cv.glmnet(x = dtm, y = movie_review[['sentiment']], 
                 family = 'binomial', 
                 # lasso 罰則
                 alpha = 1,
                 # 精度はROC曲線下の面積で評価
                 type.measure = "auc",
                 # 5重クロスバリデーション
                 nfolds = 5,
                 # 値を大きくすると精度は下がるが訓練が速くなる
                 thresh = 1e-3,
                 # こっちも反復回数を減らすと訓練が速くなる
                 maxit = 1e3)
plot(fit)
```

![fit_1-1.png](https://qiita-image-store.s3.amazonaws.com/0/58917/cd8224e8-4c70-e785-394b-2427f480858b.png)


```r
print(paste("max AUC =", round(max(fit$cvm), 4)))
```

```
## [1] "max AUC = 0.9186"
```

<!-- We have successfully fit a model to our DTM. -->
うまくDTMにモデルをフィットさせることができました．

<!-- ### Pruning vocabulary -->
### 余分な語彙を取り除く

<!-- We can note, however, that the training time for our model was quite high. We can reduce it and also significantly improve accuracy by pruning the vocabulary. -->
しかし，モデルの訓練時間がかなり長かったことに気付きます．余分な語彙を取り除けば，精度を大きく向上させつつ訓練時間を短くすることが可能です．

<!-- For example we can find words "a," "the," and "in" in almost all documents, but they do not provide much useful information. Usually they called [stop words](https://en.wikipedia.org/wiki/Stop_words). On the other hand, the corpus also contains very uncommon terms, which are contained in only a few documents. These terms are also useless, because we don't have sufficient statistics for them. Here we will remove both very common and very unusual terms. -->
たとえば，"a"や"the"や"in"といった単語はほとんど全ての文書で見つかりますが，これらの単語はあまり有用な情報を提供してはいません．普通，これらの単語は[ストップワード（stop words）](https://en.wikipedia.org/wiki/Stop_words)と呼ばれています．他方，コーパスにはほんの数個の文書にしか出現しないような非常に稀なタームも含まれています．これらのタームも，十分な数のデータがないために有用性が低いものです．ここでは非常に多く出現するタームと非常に稀なタームの両方を取り除くことにします．


```r
pruned_vocab <- prune_vocabulary(vocab, term_count_min = 10,
 doc_proportion_max = 0.5, doc_proportion_min = 0.001)
it <- itoken(tokens, ids = movie_review$id)
vectorizer <- vocab_vectorizer(pruned_vocab)
dtm <- create_dtm(it, vectorizer)
dim(dtm)
```

```
## [1] 5000 7656
```

<!-- Note that the new DTM has many fewer columns than the original DTM.  -->
新しいDTMはもとのDTMより列数がずっと少なくなっていることに注目してください．

<!-- ### TF-IDF -->
### TF-IDF

<!-- We can (and usually should!) also apply [TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf) transformation to our DTM** which will increase the weight of terms which are specific to a single document or handful of documents and decrease the weight for terms used in most documents: -->
DTMに対して[TF-IDF](https://en.wikipedia.org/wiki/Tf%E2%80%93idf)変換を利用することもできます（し，普通は利用するべきです！）．TF-IDFは少数の文書に特有なタームの重みを大きくし，多くの文書で使われているタームの重みは減らします．


```r
dtm <- dtm %>% transform_tfidf()
```

```
## idf scaling matrix not provided, calculating it form input matrix
```

<!-- Now, let's fit our model again: -->
さて，もう一度モデルをフィットさせてみましょう．


```r
t1 <- Sys.time()
fit <- cv.glmnet(x = dtm, y = movie_review[['sentiment']], 
                 family = 'binomial', 
                 alpha = 1,
                 type.measure = "auc",
                 nfolds = 5,
                 thresh = 1e-3,
                 maxit = 1e3)
print(difftime(Sys.time(), t1, units = 'sec'))
```

```
## Time difference of 4.092234 secs
```

```r
plot(fit)
```

![fit_2-1.png](https://qiita-image-store.s3.amazonaws.com/0/58917/f5860152-643d-90ce-264a-a16e897ef93a.png)


```r
print(paste("max AUC =", round(max(fit$cvm), 4)))
```

```
## [1] "max AUC = 0.9199"
```

<!-- We trained this model faster with a larger AUC. -->
モデルをより速く訓練することができ，AUCの値も大きくなりました．

<!-- ### Can we improve the model? -->
### さらにモデルを改善できるか？

<!-- We can try to improve our model by using n-grams instead of words. We will use up to 3-grams: -->
単語ではなくnグラムを使うことでモデルを改善できるか試してみることもできます．3グラムまで使うことにします．


```r
it <- itoken(tokens, ids = movie_review$id)

vocab <- create_vocabulary(it, ngram = c(1L, 3L)) %>% 
  prune_vocabulary(term_count_min = 10, 
                   doc_proportion_max = 0.5, 
                   doc_proportion_min = 0.001)

vectorizer <- vocab_vectorizer(vocab)

dtm <- tokens %>% 
  itoken() %>% 
  create_dtm(vectorizer) %>% 
  transform_tfidf()
```

```
## idf scaling matrix not provided, calculating it form input matrix
```

```r
dim(dtm)
```

```
## [1]  5000 27226
```

```r
fit <- cv.glmnet(x = dtm, y = movie_review[['sentiment']], 
                 family = 'binomial', 
                 alpha = 1,
                 type.measure = "auc",
                 nfolds = 5,
                 thresh = 1e-3,
                 maxit = 1e3)

plot(fit)
```

![ngram_dtm_1-1.png](https://qiita-image-store.s3.amazonaws.com/0/58917/24ec68b4-c1d5-7420-9970-531c3f9d5193.png)


```r
print(paste("max AUC =", round(max(fit$cvm), 4)))
```

```
## [1] "max AUC = 0.9193"
```

<!-- Using n-grams improved our model a little bit more. Further tuning is left up to the reader. -->
nグラムを使うことでモデルがもう少し改善しました．さらなるチューニングは読者に委ねます．

<!-- ### Feature hashing -->
### 素性ハッシング

<!-- If you are not familiar with feature hashing (the so-called "hashing trick") I recommend that you start with the [Wikipedia article](https://en.wikipedia.org/wiki/Feature_hashing), then read the [original paper](http://alex.smola.org/papers/2009/Weinbergeretal09.pdf) by a Yahoo! research team. This techique is very fast because we don't have to perform a lookup over an associative array. Another benefit is that it leads to a very low memory footprint, since we can map an arbitrary number of features into much more compact space. This method was popularized by Yahoo! and widely used in [Vowpal Wabbit](https://github.com/JohnLangford/vowpal_wabbit/).  -->
もし素性ハッシング（いわゆるハッシングトリック（hashing trick））になじみがなければ，[ウィキペディアの記事](https://ja.wikipedia.org/wiki/Feature_Hashing)をまず読んでから，Yahoo!の研究チームによる[原論文](http://alex.smola.org/papers/2009/Weinbergeretal09.pdf)を読むのがよいでしょう．このテクニックは連想配列上での検索を行う必要がないため，非常に高速です．もう一つの利点として，任意の個数の素性をずっとコンパクトな空間へと写像できるので，メモリの使用量も非常に少なくなります．この手法はYahoo!のおかげで普及し，[Vowpal Wabbit](https://github.com/JohnLangford/vowpal_wabbit/)において広範に利用されています．

<!-- Here is how to use feature hashing in text2vec. -->
text2vecで素性ハッシングを使うには以下のようにします．


```r
it <- itoken(tokens, ids = movie_review$id)

vectorizer <- hash_vectorizer(hash_size = 2 ^ 16, ngram = c(1L, 3L))
dtm <- create_dtm(it, vectorizer) %>% 
  transform_tfidf()
```

```
## idf scaling matrix not provided, calculating it form input matrix
```

```r
fit <- cv.glmnet(x = dtm, y = movie_review[['sentiment']], 
                 family = 'binomial', 
                 alpha = 1,
                 type.measure = "auc",
                 nfolds = 5,
                 thresh = 1e-3,
                 maxit = 1e3)

plot(fit)
```


![hash_dtm-1.png](https://qiita-image-store.s3.amazonaws.com/0/58917/0231b8cd-c22a-91ce-d9a5-d26978a5f192.png)


```r
print(paste("max AUC =", round(max(fit$cvm), 4)))
```

```
## [1] "max AUC = 0.9027"
```

<!-- As you can see, we our AUC is a bit worse, but DTM construction time was considerably lower. On large collections of documents this can be a significant advantage. -->
見ての通りAUCは少し悪化していますが，DTMの構築にかかる時間はかなり短くなりました．これは巨大な文書集合に対しては大きなメリットになり得ます．


----

[^bow]: bag-of-wordsもn-gramも日本語ウィキペディアでは項目がないため英語版へのリンクのままとした．以下でも日本語ウィキペディアの項目が存在する場合にはそちらへのリンク，ない場合には英語版へのリンクとしている．

[^ss]: 訳者の環境を示しておく．

    
    ```r
    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-03
    ```
    
    ```
    ## Packages -----------------------------------------------------------------------
    ```
    
    ```
    ##  package       * version date       source        
    ##  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)
    ##  glmnet        * 2.0-5   2016-03-17 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)
    ##  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)
    ##  withr           1.0.2   2016-06-20 CRAN (R 3.3.1)
    ##  yaml            2.1.13  2014-06-12 CRAN (R 3.3.1)
    ```