【R】日本語文章をtext2vecで分析にかける
RMeCabなしでも、system()関数でMeCabに直接アクセスしてRで形態素解析ができるのでした。
RMeCabにはいろいろ便利な関数が入っていて、N-gramができる、とか、DTMが作れる、とか便利といえば便利です。が、個人的にはちょっと微妙だと思っております。というのも、RMeCabのN-gramは複数のN-gramを同時にできない、つまり1-gram、2-gram、3-gramを同時に作ってそのままDTMに放り込む、ということができません。
たとえば、
「今日 は 雨 です」
と分かち書きされた文は
1-gramの場合
- 今日
- は
- 雨
- です
2-gramの場合
- 今日_は
- は_雨
- 雨_です
3-gramの場合
- 今日_は_雨
- は_雨_です
とN-gramされます。残念なことにRMeCabの場合、1-gram、2-gram、3-gramの分割を同時にできず、3回の処理に分けなければなりません。
こいつぁ面倒だ。
ということで、他のパッケージで何とかできないか、と試してみたところ、text2vecパッケージを使えばうまくできたのでご報告。
目次
text2vecって何?
早速「text2vec R」でググってみると、残念ながら日本語の記事はほとんど出てきません。まとまった記事(英語からの翻訳)はこちら。
一言でいえば「Rで自然言語処理するための統合パッケージの一つ」です。他にtm、NLP、textmineRなどがありますが、text2vecはその名の通り、word embeddingが扱えるため、個人的にはおすすめしております。そのほか、LSA、LDAなどのトピックモデルもパッケージに含まれておりますので、基本的な解析はほぼこのパッケージで済んでしまう、と思っております。
何が問題か?
text2vecは英語で作られたパッケージです。そのため、そのままでは日本語の形態素解析ができません。それはほかのパッケージでも同じです。
そもそも、英語は単語がスペースで区切られているため、そのスペースを目安に区切ることで容易に形態素解析ができます。それ以外に、大文字を小文字に直す、複数形を単数形に直す、活用形の語幹を取り出す、ストップワードを削除する、などの下処理があり、それが英語の場合の形態素解析の主な仕事になります。
日本語の場合、単語が英語のように明確に区切られていないため、「単語ごとに区切る」という英語では単純なタスクが非常に大変になります。ですから、残念ながら日本語をそのままの状態で形態素解析から扱うことはできないのです。
じゃあ、形態素解析はMeCabにやらせて、それ以降の処理をtext2vecにやらせることは可能じゃないのか?
その発想が、この記事の元になりました。
text2vecは半角スペースで区切られた文字列ならば扱うことができます。ですから、MeCabで分かち書きをし、単語ごとに半角スペースを入れた文字列を作ってtext2vecに渡したらうまいこと処理してくれる可能性があるのです。
早速やってみよう。
使用するパッケージ
使用するパッケージは基本的にtext2vecのみですが、textmineRにもいろいろ便利な関数が入っているので、一緒にロードします。インストールしていない場合はインストールしておいてください。
# ライブラリを読み込み library(text2vec) library(textmineR)
ちなみに、英語の場合、tmパッケージにストップワード一式が入っているので便利なのですが、今回は日本語を扱うため、使いません。英語の解析をする場合はtmのストップワード一式が非常に便利なのでおすすめです。
文字列ベクトルでもOK
RMeCabを使わず、system()関数を使ってMeCabに投げるやり方はこちらで紹介しました。
こちらではsystem()関数の引数に文字列を直接打ち込んでいたので、確認のため、文字列ベクトルでも動作するかチェックします。
# 文字列のベクトルを作る sentence_vec <- c("今日はたくさんのマグロが捕れたので旗を掲げて帰った。", "わんこはどうしてこんなにもふもふなのだろう?", "雨が降ってコンクリートが固まらず、現場主任が謎の儀式をしていた。") # system関数の実験 command <- paste("echo",sentence_vec[1],"| mecab") out_mec <- system("cmd.exe", input=command, intern=T)
とします。system()関数の引数であるinputに渡す文字列をpasteを使って作成します。解析する文字列部分をベクトルの要素で指定すれば問題ありません。結果であるout_mecを表示させると
> out_mec [1] "Microsoft Windows [Version 10.0.14393]" [2] "(c) 2016 Microsoft Corporation. All rights reserved." [3] "" [4] "C:\\Users\\YusukeSato\\Documents>echo 今日はたくさんのマグロが捕れたので旗を掲げて帰った。 | mecab" [5] "今日\t名詞,副詞可能,*,*,*,*,今日,キョウ,キョー" [6] "は\t助詞,係助詞,*,*,*,*,は,ハ,ワ" [7] "たくさん\t名詞,副詞可能,*,*,*,*,たくさん,タクサン,タクサン" [8] "の\t助詞,連体化,*,*,*,*,の,ノ,ノ" [9] "マグロ\t名詞,固有名詞,一般,*,*,*,マグロ,マグロ,マグロ" [10] "が\t助詞,格助詞,一般,*,*,*,が,ガ,ガ" [11] "捕れ\t動詞,自立,*,*,一段,連用形,捕れる,トレ,トレ" [12] "た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ" [13] "ので\t助詞,接続助詞,*,*,*,*,ので,ノデ,ノデ" [14] "旗\t名詞,一般,*,*,*,*,旗,ハタ,ハタ" [15] "を\t助詞,格助詞,一般,*,*,*,を,ヲ,ヲ" [16] "掲げ\t動詞,自立,*,*,一段,連用形,掲げる,カカゲ,カカゲ" [17] "て\t助詞,接続助詞,*,*,*,*,て,テ,テ" [18] "帰っ\t動詞,自立,*,*,五段・ラ行,連用タ接続,帰る,カエッ,カエッ" [19] "た\t助動詞,*,*,*,特殊・タ,基本形,た,タ,タ" [20] "。\t記号,句点,*,*,*,*,。,。,。" [21] "EOS" [22] "" [23] "C:\\Users\\YusukeSato\\Documents>"
となって文字列ベクトルsentence_vecの第一要素が形態素解析されているのが確認できました。これで複数の文書も問題なく形態素解析できます。
sapplyで一括処理
であれば、あとはsapplyで文字列ベクトルの各要素を連続処理させれば良いわけです。今回は分かち書きした各形態素のうち、名詞と動詞のみを取り出すこととしました。また、活用によるずれをなくすため、表層形ではなく基本形を使用することとしています。基本形はMeCabの結果のうち、8番目に出ているものです。
[11] "捕れ\t動詞,自立,*,*,一段,連用形,捕れる,トレ,トレ"
ここでいうと「捕れる」にあたりますね。\tと,で区切られているので、要素としては8番目にあたります。
# 文字列ベクトルの各センテンスを形態素解析 morph_out <- sapply(sentence_vec, function(x){ command <- paste("echo", x, "| mecab") out_mec <- system("cmd.exe", input=command, intern=T) #余計な行を削除 out_mec_red <- out_mec[seq(5, (1:length(out_mec))[out_mec=="EOS"]-1)] #結果を分割 out_mec_list <- lapply(out_mec_red, function(x){ out <- unlist(strsplit(x, split="\t")) %>% strsplit(split=",") return(unlist(out)) }) #名詞と動詞の基本形を取得 out_mec_noun <- unlist(sapply(out_mec_list, function(x){ if(x[2] %in% c("名詞","動詞")){ return(x[8]) } })) #半角スペースを空けてひとつにつなげる←ここがポイント out_sentence <- paste(out_mec_noun, collapse=" ") return(out_sentence) })
当初の発想としては、「半角スペースで分かち書きされてさえいれば、英語を扱うことが想定されているパッケージでも、日本語を扱えるのではないか?」というところでした。そのため、分かち書きして基本形に変換した文字列を半角スペースでつないでいます。
out_sentence <- paste(out_mec_noun, collapse=" ")
この部分ですね。ここが今回のキモです。
さて、この結果は
> morph_out 今日はたくさんのマグロが捕れたので旗を掲げて帰った。 "今日 たくさん マグロ 捕れる 旗 掲げる 帰る" わんこはどうしてこんなにもふもふなのだろう? "わんこ もふもふ の" 雨が降ってコンクリートが固まらず、現場主任が謎の儀式をしていた。 "雨 降る コンクリート 固まる 現場 主任 謎 儀式 する いる"
なぜか元の文字列がベクトル要素のラベルになってしまっていますが、2行目、4行目、6行目を見ると、各形態素の基本形が半角スペースを空けてつながっているのがわかるかと思います。つまり、英語と同じ表記ですね。
※ラベルが気になる方は
names(morph_out) <- c(seq(1,length(morph_out)))
とでもしてラベルを気にならない形にしてください。
これで準備は整った。
さぁ、やってみよう。
実は問題があり、解決は難しくない
冒頭に掲げたQiitaの記事にしたがって、Documente Term Matrixを作っていきます。慣れないと手順が面倒ですが、そんなに複雑怪奇なことをやるわけではないので安心してください。
# text2VecでDocument-Term-Matrixを作る # 英語の場合、stop_wordsを設定するが、日本語の場合は品詞ですでにフィルタしているので不要 tokenizer <- itoken(morph_out, ids = seq(1,length(morph_out))) vocab <- create_vocabulary(tokenizer, ngram = c(1,3), # RMeCabだとできないN-gramが一発でできる sep_ngram = "_")
- itoken
前処理用のオブジェクトを返します。本来であれば、itokenに"tolower"(大文字を小文字にする)とか"removePunctuation"(カンマやピリオドを削除する)などの前処理の関数を入れるのですが、大文字/小文字変換は日本語では不要ですし、句読点はMeCabの段階で削除しているので、処理する文字列とIDだけを渡しています。詳細はtokenizerの中身をじっくりご覧ください。
- create_vocabulary
itokenで作成したオブジェクトを使ってTF-IDFを作ります。この"ngram"のところがポイントで、RMeCabでは1-gram、2-gram、3-gramを3回の処理に分けなければいけませんでしたが、ここでは一発で済む、というところです。text2vecを推す理由のひとつがここなのです。
さて、せっかくですからvocab、つまりこの文字列ベクトルのTF-IDFを見てみましよう。
> vocab Number of docs: 3 0 stopwords: ... ngram_min = 1; ngram_max = 3 Vocabulary: terms 1: <U+0082><U+00B7><U+0082><e9>_<U+0082><U+00A2><U+0082><e9> 2: <U+008B>V<U+008E><U+00AE>_<U+0082><U+00B7><U+0082><e9>_<U+0082><U+00A2><U+0082><e9> (・・・以下略)
オゥフ・・・
と一瞬思ったあなた。
読めないからと諦めていませんか?
出力されたものをよくよく眺めてみましょう。
これ、あれなんですよ。
なので、文字コードを戻しちゃえば良いわけです。
# 文字コードをcp932に変換するよ!←ここ重要 # だたしDTMにすると結局文字化けが戻るので、DTMにした後、再度文字コードを変換する vocab$vocab$terms <- iconv(vocab$vocab$terms,"cp932")
iconvで文字コードをcp932に変換してやります。そうすると、
> vocab Number of docs: 3 0 stopwords: ... ngram_min = 1; ngram_max = 3 Vocabulary: terms terms_counts doc_counts 1: する_いる 1 1 2: 儀式_する_いる 1 1 3: 儀式_する 1 1 4: わんこ_もふもふ 1 1 5: わんこ 1 1 6: 旗_掲げる_帰る 1 1 7: 今日_たくさん_マグロ 1 1 8: もふもふ 1 1 9: 掲げる 1 1 10: 降る_コンクリート_固まる 1 1 11: 主任_謎_儀式 1 1 12: 主任 1 1 13: たくさん_マグロ_捕れる 1 1 14: 捕れる_旗 1 1 15: 捕れる 1 1 16: 今日_たくさん 1 1 17: マグロ_捕れる_旗 1 1 18: 旗_掲げる 1 1 19: わんこ_もふもふ_の 1 1 20: 今日 1 1 21: 儀式 1 1 22: たくさん 1 1 23: たくさん_マグロ 1 1 24: 掲げる_帰る 1 1 25: 帰る 1 1 26: マグロ 1 1 27: もふもふ_の 1 1 28: マグロ_捕れる 1 1 29: の 1 1 30: する 1 1 31: 雨_降る 1 1 32: 捕れる_旗_掲げる 1 1 33: 雨_降る_コンクリート 1 1 34: 謎 1 1 35: コンクリート 1 1 36: コンクリート_固まる 1 1 37: コンクリート_固まる_現場 1 1 38: いる 1 1 39: 旗 1 1 40: 降る 1 1 41: 謎_儀式 1 1 42: 固まる 1 1 43: 固まる_現場 1 1 44: 現場 1 1 45: 雨 1 1 46: 固まる_現場_主任 1 1 47: 現場_主任 1 1 48: 降る_コンクリート 1 1 49: 現場_主任_謎 1 1 50: 主任_謎 1 1 51: 謎_儀式_する 1 1 terms terms_counts doc_counts
ラベルがおかしくなっていただけで、ちゃんとTF-IDFになっているのです!
この症状はtext2vecの関数に通すたびに発生するのでその都度文字コードを当てなおしてあげなければいけませんが、それくらいは大した手間ではありません。
勇気をもって前に進んでいきましょう。
Document-Term-Matrixを作る
TF-IDFができたので最後にDocument-Term-Matrixを作ります。
vectorizer <- vocab_vectorizer(vocab) dtm <- create_dtm(tokenizer, vectorizer)
vocab_vectorizerはおまじないです(便利な表現だな)。helpを見るとこの段階でngramやskipgramを定義しても良いみたいです。が、create_vocabularyの段階でやったほうがTF-IDFも確認できるので、今回のやり方をおすすめします。
さて、vocabを作ったときにすでに見たように、text2vecに日本語の文字列を与えるとUnicode表記になるのでした。今回作ったDTMも同じで、
> colnames(dtm) [1] "\u0082�\u0082\xe9_\u0082�\u0082\xe9" [2] "\u008bV\u008e�_\u0082�\u0082\xe9_\u0082�\u0082\xe9" [3] "\u008bV\u008e�_\u0082�\u0082\xe9" [4] "\u0082\xed\u0082\xf1\u0082�_\u0082\xe0\u0082ӂ\xe0\u0082\xd3" (・・・以下略)
期待通りの出来です。
でも、みんなの強い味方、iconvがあるじゃないか。
# 文字コードをcp932に変換するよ! colnames(dtm) <- iconv(colnames(dtm),"cp932")
で、
> colnames(dtm) [1] "する_いる" "儀式_する_いる" "儀式_する" [4] "わんこ_もふもふ" "わんこ" "旗_掲げる_帰る" [7] "今日_たくさん_マグロ" "もふもふ" "掲げる" (・・・以下略)
はい、まったく問題なくなりました。
後は、トピックモデルに突っ込むなり、別途Term-Coocurance-Matrixからword embeddingするなり、好きにしたら良いと思います。
まとめ
- RMeCabは便利なんだけれどところどころ使いにくいところがある
- text2vecはいろいろ便利、使える
- iconvはみんなの味方
- 結局今回は使わなかったけどtextmineRも便利だよ