Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

【Techの道も一歩から】第16回「Rに入門してテキストマイニング」

f:id:kanjirz50:20190104142720j:plain

こんにちは。 DSOC R&D グループの高橋寛治です。

私が所属する R&D グループでは、プログラミングの使用言語は特に定まっていないので、私は使い慣れている Python を利用しています。 つい最近のことですが、別の研究員が R を用いてワードクラウドを作っているのを横目で見ていました。 コードを見させていただいたところ少量でわかりやすく、非常に便利そうだと感じました。 新年新しいことを始めてみようという気持ちも相まって、今回はRに入門してみます。

こちらの「Rによるテキストマイニング」を読みながら、自分のブログの記事を対象にテキストマイニングをしてみます。

Rでテキストデータを取り扱う

R では tidy パッケージがよく使われており、これは整理データ原則と呼ばれる取り扱いやすいデータ構造に則っています。 テキストの整理データである整理テキスト形式は、1行1トークンの表からなっています。

普段 Python でテキスト処理を行う際には、文書-単語行列を利用しています。 1行は1文書を表し、列は単語を表します。 scikit-learn の CountVectorizer を始めとするテキスト処理系でよく使われています。

Rのお作法である整理テキスト形式に入門します。 対象テキストは、自分のブログ記事を利用します。

Git レポジトリに記事をいくつかコピーしたものがあるので、クローンして加工します。

$ git clone https://github.com/kanjirz50/tech-blog-script.git
$ head -3 tech-blog-script/tfidf/docs/20171119.txt
ICDAR2017に参加した

最近 会社の技術ブログにうつつを抜かし 、個人ブログに投稿できていなかったので、久しぶりの投稿となります。 (個人ブログ、少しはがんばろう)

R のファイル読み込みのお作法が全然わからなかったため、使い慣れたシェルで1行1文書形式にしてから R で取り扱います。 今回は、RMeCab を使用せずに、シェルで分かち書きおよび品詞によるフィルタリングと単語の原型化を行います。

コマンドを日本語で書き下します。

  • tech-blog-script/tfidf/docs 以下の *.txt ファイル名を抽出
  • xargs コマンドで1ファイルずつ処理
    • ファイルの内容を表示
    • MeCab で形態素解析
    • 名詞、動詞、形容詞、副詞のみ残す
    • 原型を取得(原型が辞書にない語は除外)
    • 1行1文書となるように改行を取り除き、文末に改行を挿入
$ find tech-blog-script/tfidf/docs/ -name "*.txt" | xargs -I{} sh -c "cat {} | mecab | grep -E $'\t''(名詞|動詞|形容詞|副詞),'| cut -f2 | cut -f7 -d ','|grep -v '*' |tr '\n' ' '; echo" > blogs.txt
$ head -1 blogs.txt
使う みる 日 目 記事 いい 話 聞く これ 使う みる 思う 使う ...

1行1文書となっているテキストを読み込みます。 テキストの読み込みには scan 関数を用います。 scan 関数はファイルをベクター形式で読み込みます。

> text <- scan("blogs.txt", what = character(), sep = "\n", blank.lines.skip = F)
> text[1]
[1] "使う みる 日 目 記事 いい 話 聞く これ 使う みる 思う 使う みる こちら トム 少佐 地上 管制 動作 する クロス プラットフォーム よう シェル 言語 言語 上位 互換 慣れ親しむ ...

次に、読み込んだテキストを dplyrtidytext で取り扱えるようにデータフレームに変換します。 1行が1文書であるということを保持するために、 post_no という列を追加します。

> library(dplyr)
> blog_posts <- data_frame(post_no = 1:length(text), text = text)
> blog_posts
# A tibble: 9 x 2
  post_no text                                                                                                             
    <int> <chr>                                                                                                            
1       9 "使う みる 日 目 記事 いい 話 聞く これ 使う みる 思う 使う みる こちら トム 少佐 地上 管制 動作 する クロス プラットフォーム よう シェル 言語 言語 上位 互換 慣れ親しむ 基本 的 シェル 命令 …

トークンに分割して、整理テキスト形式に変換します。 tidytext::unnest_tokens を用います。 この関数はトークンごとにテキストを分割します。 すでにスペース区切りで分かち書きされているため、英語と同様にスペース区切りで1トークンとし、整理テキスト形式に変換します。

> library(tidytext)
> blog_words <- blog_posts %>% unnest_tokens(output = "word", input = "text", token = "words")
> blog_words
# A tibble: 2,487 x 2
   post_no word 
     <int> <chr>
 1       1 使う 
 2       1 みる 
 3       14       15       1 記事 
 6       1 いい 
 7       18       1 聞く 
 9       1 これ 
10       1 使う 
# ... with 2,477 more rows

これでテキストを R で取り扱う準備ができました。

頻度の計上と TF-IDF

テキスト処理で定番である頻度の数え上げやその応用である TF-IDF 値の計算を行います。

まずは、単純に頻度を計上します。 すべてのテキストを一つのテキストとして、頻度を数えます。

> blog_words %>% count(word, sort = TRUE)
# A tibble: 805 x 2
   word       n
   <chr>  <int>
 1 する     202
 2 いる      58
 3 文字      40
 4 こと      36
 5 なる      33
 6 よう      33
 7 れる      31
 8 ため      29
 9 できる    27
10 使う      25
# ... with 795 more rows

文書ごとに頻度を数えます。 count 関数に対して引数に列名を増やすことで、文書ごとに数えることができます。

> blog_words %>% count(post_no, word, sort = TRUE)
# A tibble: 1,279 x 3
   post_no word       n
     <int> <chr>  <int>
 1       3 する      31
 2       2 する      30
 3       2 文字      27
 4       4 する      26
 5       5 する      25
 6       1 する      22
 7       7 する      22
 8       2 コード    21
 9       8 する      20
10       6 する      17
# ... with 1,269 more rows

ノイズとなる単語が多いので、ストップワードを定義してみます。 今回のストップワードは独断と偏見で決めます。 anti_join によりストレッチを取り除くので、列名を合わせておきます。

> ja_stop_words <- data_frame(word = c("する", "いる", "こと", "なる", "よう", "れる", "ため", "できる", "使う", "の", "みる", "ある", "思う", "的"))
> blog_words %>% anti_join(ja_stop_words) %>%  count(word, sort = TRUE)
Joining, by = "word"
# A tibble: 791 x 2
   word           n
   <chr>      <int>
 1 文字          40
 2 コード        24
 3 抽出          18
 4 モジュール    17
 5 言語          17
 6 勉強          17
 7 処理          16
 8 推定          16
 9 素性          14
1012
# ... with 781 more rows

それらしい単語が出てくるようになりました。 それでは TF-IDF 値を計算します。

> blog_post_words <- blog_words %>% anti_join(ja_stop_words) %>% count(post_no, word, sort = TRUE) %>% ungroup()
Joining, by = "word"
> blog_post_words <- blog_post_words %>% bind_tf_idf(word, post_no, n)
> blog_post_words
# A tibble: 1,176 x 6
   post_no word           n     tf   idf tf_idf
     <int> <chr>      <int>  <dbl> <dbl>  <dbl>
 1       2 文字          27 0.0906 1.10  0.0995
 2       2 コード        21 0.0705 1.10  0.0774
 3       8 素性          14 0.0843 2.20  0.185 
 4       8 抽出          14 0.0843 0.811 0.0684
 5       2 モジュール    11 0.0369 1.50  0.0555
 6       5 文字          11 0.0418 1.10  0.0459
 7       6 勉強          10 0.0410 1.10  0.0450
 8       7 企業          10 0.0450 2.20  0.0990
 9       2 推定           9 0.0302 1.10  0.0332
10       1 ファイル       8 0.0316 1.10  0.0347
# ... with 1,166 more rows

TF-IDF 値が高いものを表示してみます。

> blog_post_words %>% arrange(desc(tf_idf))
# A tibble: 1,176 x 6
   post_no word             n     tf   idf tf_idf
     <int> <chr>        <int>  <dbl> <dbl>  <dbl>
 1       8 素性            14 0.0843 2.20  0.185 
 2       2 文字            27 0.0906 1.10  0.0995
 3       7 企業            10 0.0450 2.20  0.0990
 4       9 インストール     7 0.103  0.811 0.0835
 5       8 テンプレート     6 0.0361 2.20  0.0794
 6       2 コード          21 0.0705 1.10  0.0774
 7       8 抽出            14 0.0843 0.811 0.0684
 8       9 環境             3 0.0441 1.50  0.0664
 9       9 パスワード       2 0.0294 2.20  0.0646
10       1 プロセス         7 0.0277 2.20  0.0608
# ... with 1,166 more rows

TF-IDF の特徴である、ある文書で頻出するが他の文書では見ないという、そういった単語の TF-IDF 値が高いように思います。

ワードクラウド

頻出語をワードクラウドで可視化します。 先ほどまでに用意した整理テキスト形式を入力し、作成します。

日本語フォントは適宜インストールされているものを指定して下さい。

library(wordcloud)
blog_words %>% anti_join(ja_stop_words) %>% count(word) %>% with(wordcloud(word, n, max.words = 50, family="Osaka"))

f:id:kanjirz50:20190104142431p:plain

言語や文字、素性といった言語処理でよく見る単語が大きく表示されている(=頻出する)ことが一目でわかります。

パイプでつなぐだけで簡単に可視化できました。

手札を増やす

食わず嫌いで使っていなかった R でしたが、整理テキスト形式の考え方やパイプによる処理など非常に便利と感じるものが多かったです。 手慣れた Python を使い続けるだけでなく、いろいろと取り組んでみようと思います。

執筆者プロフィール

高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) R&Dグループ研究員

阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。現在はキーワード抽出など自然言語処理を生かした研究に取り組んでいる。

© Sansan, Inc.