ビジネスでR x 自然言語処理を活用する：Quantedaの文書行列に新しい文書を追加する方法

## はじめに

本記事では、主にRで自然言語処理を行う際に必ず登場するパッケージQuantedaが生成した文書行列に新しい文書を追加する方法を説明する。

ビジネスの世界では、作成されたモデル（機械学習界隈では学習済みモデルと呼ぶ）に新しいデータを入れて、何かしらを出す（機械学習界隈では推論と呼ぶ。信頼区間を出したりMCMCや変分推論を実行したりすることとは関係ない）ことは極めて重要である。例えば筆者が作成した記事

https://qiita.com/Gotoubun_taiwan/items/777f537c315cd9e59757

で説明すると、学習済みの多項逆回帰モデルに本日新しく書かれたレビューの高レビュー度（star_rating）を予測する際に、新しいデータを入れる必要がある。

もちろん、まだそこまで事例が多くないと思うが、政治学や経済学などの研究者も、モデルから生成したダッシュボードをウェブ上で一般公開して新しいデータを随時入れて推論させることがあるため、アカデミアの方にも有益な内容であろう。

## コードの説明

まず、必要なパッケージをインポートして、説明のための二つの文書も変数に入れる。
```r
library(tidyverse)
library(quanteda)
library(RMeCab)

old_text <- c("吾輩は猫である", "吾輩は大学生である")
```

次に、MeCabを使って名詞以外の単語を消して、Quantedaに我々が指定した単語分割結果を伝えるために、単語同士は空白で区切って結合する。最後に単語行列を作成する。ここで重要なのは、トークン化する前に、phrase()という関数を一回噛ませることで、なぜかというと、phrase()を噛ませないと、tokens()関数が勝手にこちらがMeCabで綺麗に分割した単語をもう一回分割するからである。

```r
old_text_mecabbed <- c()

for (i in 1:length(old_text)){
  this_old_text <- unlist(RMeCabC(old_text[i], 1))
  old_text_mecabbed[i] <- str_c(this_old_text[which(names(this_old_text) == "名詞")], collapse = " ")
}

old_dfm <- old_text_mecabbed %>%
  phrase() %>%
  tokens() %>%
  dfm()
```

作成した単語行列の中身を見てみよう。

```r
> old_dfm
Document-feature matrix of: 2 documents, 3 features (33.33% sparse) and 0 docvars.
       features
docs    吾輩 猫 大学生
  text1    1  1      0
  text2    1  0      1
```

期待通りの結果になっていることが確認された。

このold_dfmでモデルを学習したとしよう。そこで、「大学生は教授と話している」と言う文に対して推論する必要が出たとする。まずは、「大学生は教授と話している」しか入っていない文書行列を作成する。

```r
new_text <- c("大学生は教授と話している")

new_text_mecabbed <- c()

for (i in 1:length(new_text)){
  this_new_text <- unlist(RMeCabC(new_text[i], 1))
  new_text_mecabbed[i] <- str_c(this_new_text[which(names(this_new_text) == "名詞")], collapse = " ")
}

new_dfm <- new_text_mecabbed %>%
  phrase() %>%
  tokens() %>%
  dfm()
```

中身は

```r
> new_dfm
Document-feature matrix of: 1 document, 2 features (0.00% sparse) and 0 docvars.
       features
docs    大学生 教授
  text1      1    1
```

見てわかる通り、new_dfmの列とモデルの学習で利用したold_dfmの列に整合性がない。例えば、old_dfmでは3列目が「大学生」と言う単語の出現数だが、new_dfmでは1列目になっている。これだと内積などの処理を行う際に、運が良ければエラーが出て、運が悪ければエラーが出ずRが勝手に望ましくない処理をして変な数字を吐き出すことになる。

ではどうすればいいのかというと、まず単純にold_dfmとnew_dfmの行を結合してみる。

```r
> rbind(old_dfm, new_dfm)
Document-feature matrix of: 3 documents, 4 features (50.00% sparse) and 0 docvars.
         features
docs      吾輩 猫 大学生 教授
  text1.1    1  1      0    0
  text2.1    1  0      1    0
  text1.2    0  0      1    1
```

old_dfmに該当する1行目と2行目でもnew_dfmに該当する3行目でも「大学生」がold_dfmの基準で3列目になっているのはいいことだが、old_dfmに存在していない「教授」という単語が追加された。これままだとold_dfmの列の内容と完全一致になっていないため、内積などの処理に際にはやはり問題が生じる。

ここで処理する方法はたくさんあるが、そもそもold_dfmに存在していない単語を「大学生は教授と話している」から消すやり方を紹介する。

old_dfmに存在している単語はcolnames(old_dfm)から取得できる。

```r
refined_new_text_mecabbed <- c()

for (i in 1:length(new_text)){
  this_new_text <- unlist(RMeCabC(new_text[i], 1))
  refined_new_text_mecabbed[i] <- str_c(this_new_text[which(names(this_new_text) == "名詞" & this_new_text %in% colnames(old_dfm))], collapse = " ")
}

refined_new_dfm <- refined_new_text_mecabbed %>%
  phrase() %>%
  tokens() %>%
  dfm()

```

中身を確認すると

```r
> refined_new_dfm
Document-feature matrix of: 1 document, 1 feature (0.00% sparse) and 0 docvars.
       features
docs    大学生
  text1      1
```

大学生だけ残っていることを確認できたので、このままold_dfmと結合してみると

```r
> rbind(old_dfm, refined_new_dfm)
Document-feature matrix of: 3 documents, 3 features (44.44% sparse) and 0 docvars.
         features
docs      吾輩 猫 大学生
  text1.1    1  1      0
  text2.1    1  0      1
  text1.2    0  0      1
```

期待通りの形式になった。

これで、新しい行に対して内積などの処理を行うことができるようになった。