第5回 青空文庫のテキストマイニングをRMeCabパッケージでやってみた|Tech Book Zone Manatee

マナティ

Rではじめよう![モダン]なデータ分析

第5回 青空文庫のテキストマイニングをRMeCabパッケージでやってみた

RMeCabパッケージを使った青空文庫のテキストマイニングを行い、芥川龍之介と太宰治の作品を判別します。サポートベクトルマシンによる著者判別・チューニングまですべてをRで行います。

1. はじめに

テキストマイニング(Text Mining)とは、テキストを対象にしたデータマイニングの理論や技術の総称です。

一般にデータマイニングを行うためにはデータが必要になりますが、テキストマイニングやそれを文学作品の分析に応用した計量言語学の分野では文章データに加えて、しばしばコーパスが用いられます。 コーパスとは、書き言葉や話し言葉をジャンルなどを考慮した上で網羅的に収録したデータベースのことを指します。

また、日本語のテキストマイニングを行う上で重要になるのが形態素解析です。 日本語は英語などの言語と異なり、単語と単語を離して書くことがないため、日本語のテキストを分析する際には文章を単語単位に分割する必要があります。 このような日本語の単語分割を容易に行えるツールとしてMeCabがあり、それをR上で動かすためのパッケージとしてRMeCabがあります。 例えば、「すもももももももものうち」という文章に対してRMeCabを使って単語分割をすると、以下のような出力が得られます。

(なお、MeCabはインストールされていることを前提とします。また、RMeCabパッケージはCRANに登録されてないので、インストールの際にレポジトリの指定が必要です。)

install.packages ("RMeCab", repos = "http://rmecab.jp/R")
library(RMeCab)
RMeCabC("すもももももももものうち")
[[1]]
    名詞
"すもも"

[[2]]
助詞
"も"

[[3]]
  名詞
"もも"

[[4]]
助詞
"も"

[[5]]
  名詞
"もも"

[[6]]
助詞
"の"

[[7]]
  名詞
"うち"

今回は、このRMeCabパッケージを使って青空文庫のテキストマイニングを行ってみたいと思います。

2. 今回のゴール

今回のゴールは、『Rで学ぶ日本語テキストマイニング』(石田基広・小林雄一郎, 2013)の第8章で紹介されている文学作品のテキストマイニングをRMeCabパッケージを使って実際にやってみることです。 具体的には、青空文庫に公開されている文学作品に対して、サポートベクトルマシン(Support Vector Machine, SVM)という手法を用いて、読点の生起位置、つまりどの単語の直後に読点(、)が表れるかを特徴量として簡単な著者判別を行います。 青空文庫はコーパスの一種で、著作権の切れた作者の文学作品について個別にテキストファイルとしてダウンロードすることができます。 判別する著者は書籍の例にならって芥川龍之介と太宰治を用います。 ただし、書籍では解析のためのデータセットがすでに出来上がっている状態で解説がなされていますので、今回の解析ではデータの取得、前処理からSVMでの解析までRで行うこととします。

SVMは、機械学習の分野では頻繁に用いられる手法の一つで、2つの群を判別する手法です。1次元や2次元であれば直線で、3次元以上であれば平面で領域を分割して群を判別します。SVMの詳細は『Rによるデータサイエンス』などを参照してください。

3. 解析

手順

今回の解析は以下のような手順で行います。

 3-1. 青空文庫のWebページをスクレイピングして公開されている作品一覧を取得
 3-2. 新字新仮名のデータに絞り、解析に使うデータをランダムサンプリングで抽出
 3-3. テキストファイルをダウンロードしてルビなどのメタ情報を削除
 3-4. 形態素解析をしてデータを加工
 3-5. SVMによる著者判別

3-1. データの取得

今回使うパッケージを読み込みます。自分の環境にパッケージが入っていなければ適宜インストールします。

install.packages("rvest")
library(rvest)

install.packages("dplyr")
library(dplyr)

install.packages("stringr")
library(stringr)

library(RMeCab)

# 作業ディレクトリを作成
dir.create("./analysis")
# 作業ディレクトリを設定
setwd("./analysis")

まずは芥川の作品リストを取得します。rvestパッケージ1を使って芥川のページをスクレイピングして作品リストを取得し、公開中のものに絞ります。

# 芥川の公開作品リストを取得----
## スクレイピングで作品リストを取得
a_html  <- read_html("http://www.aozora.gr.jp/index_pages/person879.html#sakuhin_list_1")

a_text <- a_html %>%
  html_nodes("ol li") %>%
  html_text()

head(a_text) # 先頭の6つを表示
[1] "愛読書の印象 (新字旧仮名、作品ID:4872) "
[2] "秋 (新字旧仮名、作品ID:16) "
[3] "芥川竜之介歌集 (新字旧仮名、作品ID:178) "
[4] "アグニの神 (新字新仮名、作品ID:43014) "
[5] "アグニの神 (新字旧仮名、作品ID:15) "
[6] "悪魔 (新字旧仮名、作品ID:3804) "
## データフレーム化
a_text_open <- a_text[1:374] %>% #公開作品のみを抽出
  as.data.frame() #データフレーム化
colnames(a_text_open) <- c("text_name") #列名を設定

3-2. ランダムサンプリング

次に新字新仮名の作品に絞ります。解析には10作品しか使わないので、さらにランダムサンプリングで10個だけを抽出します。 この部分はデータフレーム操作のためのdplyrパッケージと文字列操作のためのstringrパッケージを用いて行います。

## 新字新仮名の作品に絞り、作品IDのみを残す。さらにランダムサンプリングで10個だけを取得
set.seed(123)

a_10 <- a_text_open %>%
  filter(str_detect(.$text_name, "新字新仮名") == TRUE) %>% #新字新仮名の作品に絞る
  mutate(title = str_replace_all(.$text_name, "(.+?)", ""), #タイトルだけの列を追加
         id = str_replace_all(.$text_name, "[^0-9]", ""), #IDの列を追加
         author = "芥川") %>% #作者を示す列を追加
  select(- text_name) %>% #元のタイトルの列を削除
  sample_n(10)  #ランダムサンプリング
a_10
                 title    id author
156           百合     185   芥川
74      侏儒の言葉     158   芥川
110       トロッコ   43016   芥川
92    滝田哲太郎氏   43383   芥川
17        大河の水     123   芥川
143       三つの窓    1125   芥川
39            疑惑      39   芥川
7         飯田蛇笏   43362   芥川
51 合理的、同時に多量の人間味 ――相互印象・菊池寛氏――   43376   芥川
147        毛利先生    101   芥川

ここまでで芥川のデータが作り終わりました。続いて太宰についても同様の作業を行います。

# 太宰についても同様にやる-----
d_html  <- read_html("http://www.aozora.gr.jp/index_pages/person35.html#sakuhin_list_1")

d_text <- d_html %>%
  html_nodes("ol li") %>%
  html_text()

head(d_text) # 先頭の6つを表示
[1] "ア、秋 (新字新仮名、作品ID:236) "
[2] "I can speak (新字新仮名、作品ID:1572) "
[3] "愛と美について (新字新仮名、作品ID:1578) "
[4] "青森 (新字新仮名、作品ID:46597) "
[5] "青森 (新字旧仮名、作品ID:4357) "
[6] "朝 (新字新仮名、作品ID:1562) "
## データフレーム化
d_text_open <- d_text[1:272] %>% #公開作品のみを抽出
  as.data.frame() #データフレーム化
colnames(d_text_open) <- c("text_name") #列名を設定

## 新字新仮名の作品に絞り、作品IDのみを残す。さらにランダムサンプリングで10個だけを取得
set.seed(123)

d_10 <- d_text_open %>%
  filter(str_detect(.$text_name, "新字新仮名") == TRUE) %>% #新字新仮名の作品に絞る
  mutate(title = str_replace_all(.$text_name, "(.+?)", ""), #タイトルだけの列を追加
         id = str_replace_all(.$text_name, "[^0-9]", ""), #IDの列を追加
         author = "太宰") %>% #作者を示す列を追加
  select(- text_name) %>% #元のタイトルの列を削除
  sample_n(10)  #ランダムサンプリング

太宰のデータができたら芥川のデータと縦に連結させます。

# 芥川と太宰を連結
text_list <- rbind(a_10, d_10)
text_list
                   title    id author
156           百合     185   芥川
74      侏儒の言葉     158   芥川
110       トロッコ   43016   芥川
92    滝田哲太郎氏   43383   芥川
17        大河の水     123   芥川
143       三つの窓    1125   芥川
39            疑惑      39   芥川
7         飯田蛇笏   43362   芥川
51 合理的、同時に多量の人間味 ――相互印象・菊池寛氏――   43376   芥川
147         毛利先生     101   芥川
194         令嬢アユ     311   太宰
921           女生徒     275   太宰
136         如是我聞    1084   太宰
114               誰     251   太宰
21                海   42363   太宰
178           無趣味   45671   太宰
49          狂言の神     290   太宰
9           或る忠告   42356   太宰
64          困惑の弁   18349   太宰
185        もの思う葦 ――当りまえのことを当りまえに語る。    1587   太宰

3-3. データのダウンロード

解析に使う作品が決定したので、次は青空文庫から作品のデータをダウンロードします。 RMeCabパッケージのサポートページに、URLを引数として青空文庫からテキストデータをダウンロードし、さらにルビの削除まで行ってくれる関数( Aozora() )が公開されているので、それを使います。 ただし、青空文庫のファイル名は一定の規則がないので、URLの指定自体は自分でやらなければなりません。

# ダウンロードして解凍----
Aozora <- function(url = NULL, txtname  = NULL){
  enc <-  switch(.Platform$pkgType, "win.binary" = "CP932", "UTF-8")
  if (is.null(url)) stop ("specify URL")
  tmp <- unlist (strsplit (url, "/"))
  tmp <- tmp [length (tmp)]

  curDir <- getwd()
  tmp <- paste(curDir, tmp, sep = "/")
  download.file (url, tmp)

  textF <- unzip (tmp)
  unlink (tmp)

  if(!file.exists (textF)) stop ("something wrong!")
  if (is.null(txtname)) txtname <- paste(unlist(strsplit(basename (textF), ".txt$")))
  if (txtname != "NORUBY")  {

    newDir <- paste(dirname (textF), "NORUBY", sep = "/")

    if (! file.exists (newDir)) dir.create (newDir)

    newFile <- paste (newDir,  "/", txtname, "2.txt", sep = "")

    con <- file(textF, 'r', encoding = "CP932" )
    outfile <- file(newFile, 'w', encoding = enc)
    flag <- 0;
    reg1 <- enc2native ("\U005E\U5E95\U672C")
    reg2 <- enc2native ("\U3010\U5165\U529B\U8005\U6CE8\U3011")
    reg3 <- enc2native ("\UFF3B\UFF03\U005B\U005E\UFF3D\U005D\U002A\UFF3D")
    reg4 <- enc2native ("\U300A\U005B\U005E\U300B\U005D\U002A\U300B")
    reg5 <- enc2native ("\UFF5C")
    while (length(input <- readLines(con, n=1, encoding = "CP932")) > 0){
      if (grepl(reg1, input)) break ;
      if (grepl(reg2, input)) break;
      if (grepl("^------", input)) {
        flag <- !flag
        next;
      }
      if (!flag){
        input <- gsub (reg3, "", input, perl = TRUE)
        input <- gsub (reg4, "", input, perl = TRUE)
        input <- gsub (reg5, "", input, perl = TRUE)
        writeLines(input, con=outfile)
      }
    }
    close(con); close(outfile)
    return (newDir);
  }
}

## ダウンロード
### 芥川
Aozora ("http://www.aozora.gr.jp/cards/000879/files/187_ruby_1150.zip", "187_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/158_ruby_1243.zip", "158_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/43016_ruby_16663.zip", "43016_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/43383_ruby_25741.zip", "43383_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/123_ruby_1199.zip", "123_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/123_ruby_1199.zip", "1125_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/39_ruby_881.zip", "39_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/43362_ruby_20778.zip", "43362_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/43376_ruby_25698.zip", "43376_")
Aozora ("http://www.aozora.gr.jp/cards/000879/files/101_ruby_857.zip", "101_")

### 太宰
Aozora ("http://www.aozora.gr.jp/cards/000035/files/311_ruby_20050.zip", "311_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/275_ruby_1532.zip", "275_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/1084_ruby_4753.zip", "1084_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/251_ruby_3560.zip", "251_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/42363_ruby_15857.zip", "42363_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/45671_ruby_20800.zip", "45671_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/290_ruby_19972.zip", "290_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/42356_ruby_15854.zip", "42356_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/18349_ruby_12213.zip", "18349_")
Aozora ("http://www.aozora.gr.jp/cards/000035/files/1587_ruby_18163.zip", "1587_")

3-4. 形態素解析とデータの加工

データのダウンロードが済んだので、次はRMeCabパッケージを用いて形態素解析を行います。 RMeCab::docDF() は、指定したディレクトリのファイル全て、特定のファイル、あるいはデータフレームに対しNgramを作成してくれる関数です。

Ngramとは、ある文書に対して特定のN語がどのくらいの回数現れているかを示したものです。ここでは、特定の語と読点、つまり2語がどのくらいの頻度で現れているのかを知りたいので、バイグラムを作成します。そのため、docDF()の N = 2 が指定されています。

さらに、『Rで学ぶ日本語テキストマイニング』にならって、20作品における16種類の文字(か, が, く, し, ず, て, で, と, に, は, ば, へ, も, ら, り, れ)と読点の共起頻度を調べるためにフィルタリングをします。

# 形態素解析----
res <- docDF("./analysis/noruby", type = 1, N = 2) %>%
  filter((str_detect(.$TERM, "か-、") == TRUE | #ここからフィルタリング
          str_detect(.$TERM, "が-、") == TRUE |
          str_detect(.$TERM, "く-、") == TRUE |
          str_detect(.$TERM, "し-、") == TRUE |
          str_detect(.$TERM, "ず-、") == TRUE |
          str_detect(.$TERM, "て-、") == TRUE |
          str_detect(.$TERM, "で-、") == TRUE |
          str_detect(.$TERM, "と-、") == TRUE |
          str_detect(.$TERM, "に-、") == TRUE |
          str_detect(.$TERM, "は-、") == TRUE |
          str_detect(.$TERM, "ば-、") == TRUE |
          str_detect(.$TERM, "へ-、") == TRUE |
          str_detect(.$TERM, "も-、") == TRUE |
          str_detect(.$TERM, "ら-、") == TRUE |
          str_detect(.$TERM, "り-、") == TRUE |
          str_detect(.$TERM, "れ-、") == TRUE),
         str_length(.$TERM) == 3)

res[1:10, 1:5] # 先頭10行、5列を表示
file_name =  ./analysis/noruby/101_2.txt opened
file_name =  ./analysis/noruby/1084_2.txt opened
file_name =  ./analysis/noruby/1125_2.txt opened
file_name =  ./analysis/noruby/123_2.txt opened
file_name =  ./analysis/noruby/158_2.txt opened
file_name =  ./analysis/noruby/1587_2.txt opened
file_name =  ./analysis/noruby/18349_2.txt opened
file_name =  ./analysis/noruby/187_2.txt opened
file_name =  ./analysis/noruby/251_2.txt opened
file_name =  ./analysis/noruby/275_2.txt opened
file_name =  ./analysis/noruby/290_2.txt opened
file_name =  ./analysis/noruby/311_2.txt opened
file_name =  ./analysis/noruby/39_2.txt opened
file_name =  ./analysis/noruby/42356_2.txt opened
file_name =  ./analysis/noruby/42363_2.txt opened
file_name =  ./analysis/noruby/43016_2.txt opened
file_name =  ./analysis/noruby/43362_2.txt opened
file_name =  ./analysis/noruby/43376_2.txt opened
file_name =  ./analysis/noruby/43383_2.txt opened
file_name =  ./analysis/noruby/45671_2.txt opened
Warning! number of extracted terms =  47363


    TERM        POS1                          POS2 101_2.txt  1084_2.txt
1  か-、   助詞-記号 副助詞/並立助詞/終助詞-読点         8         15
2  が-、   助詞-記号                 接続助詞-読点        10         62
3  が-、   助詞-記号                   格助詞-読点        40         52
4  が-、 接続詞-記号                        *-読点        19          0
5  く-、   動詞-記号                   非自立-読点         0          0
6  し-、   助詞-記号                 接続助詞-読点         0          7
7  て-、   助詞-記号                 接続助詞-読点        71         79
8  て-、   助詞-記号                   格助詞-読点         0          5
9  で-、   助詞-記号                 接続助詞-読点         4          3
10 で-、   助詞-記号                   格助詞-読点         6         23

データの出力はすべてやると膨大なので、先頭の10行5列のみを出力しています。さて、後述のkernlab::ksvm()でSVMを行うにはデータの形式として①特徴量は列でなければならない、②同じ名前の列が複数あってはならない、という制約があるのでこの状態ではSVMの解析はできません。そこで、SVMでの分析ができるようにデータフレームを転置し、品詞は異なるが同じ文字であるもの(たとえば、2-4行目の「が」)については頻度を合計するという処理を行います。 このとき、上記の16種類の文字のうち、「ず」「れ」については今回扱ったデータでは読点との共起がなかったので列としては省略することに注意します。

また、下記の処理の中でデータフレームを転置すると行列になるのですが、データフレームに戻す際に変数名がV1-V22になってしまうので、頻度を合計する際は何行目がどの文字であったかに注意しながら処理を行います。

# SVMで分析するための形式に加工
res_trans <- res %>%
  select(-POS1, -POS2)
  t() %>%
  as.data.frame() %>%
  slice(-1) %>%
  mutate_all(.funs = as.integer) %>%
  mutate( か = V1,
          が = V2 + V3 + V4,
          く = V5,
          し = V6,
          て = V7 + V8,
          で = V9 + V10,
          と = V11 + V12 + V13 + V14,
          に = V15 + V16,
          は = V17,
          ば = V18,
          へ = V19,
          も = V20,
          ら = V21,
          り = V22) %>%
  select(か, が, く ,し, て, で, と, に, は, ば, へ, も, ら, り)

# 作品一覧と連結
text_bigram <- text_list %>%
  arrange(id) %>%
  cbind(., res_trans)

上で求めたバイグラムでは単純に作品ごとの頻度を足しただけですので絶対頻度が算出されているのですが、各作品の文字数が異なることに注意しなくてはなりません。そこで、バイグラムを1000語あたりの相対頻度に変換します。

## 1000語ごとの頻度に直すために、各作品の文字数を取得
setwd("./analysis/noruby")
### ディレクトリ中のファイル一覧
files <- list.files("./analysis/noruby")
### 文字数の取得
for (i in 1:length(files)) {
  assign(paste("wn", i, sep = ""), length(RMeCabText(files[i])))
}
file = 101_2.txt
file = 1084_2.txt
file = 1125_2.txt
file = 123_2.txt
file = 158_2.txt
file = 1587_2.txt
file = 18349_2.txt
file = 187_2.txt
file = 251_2.txt
file = 275_2.txt
file = 290_2.txt
file = 311_2.txt
file = 39_2.txt
file = 42356_2.txt
file = 42363_2.txt
file = 43016_2.txt
file = 43362_2.txt
file = 43376_2.txt
file = 43383_2.txt
file = 45671_2.txt
### 連結してデータフレーム化
wn <- rbind(wn1, wn2, wn3, wn4, wn5, wn6, wn7, wn8, wn9, wn10,
                wn11, wn12, wn13, wn14, wn15, wn16, wn17, wn18, wn19, wn20) %>%
  as.data.frame()

# 相対頻度に変換して解析用データ完成
svm_dat <- text_bigram %>%
  cbind(., wn) %>%
  rename(wn = V1) %>%
  mutate_each(funs(.*1000/wn), -title, -id, -author, -wn) %>%
  select(-wn)

head(svm_dat) #先頭6行を表示
           title   id author        か        が         く        し
1   毛利先生    101   芥川 1.1342155 3.9067423 0.37807183 0.5040958
2   如是我聞   1084   太宰 0.3427240 1.0281719 0.06854479 0.2056344
3   三つの窓   1125   芥川 1.1574074 6.1728395 1.15740741 1.5432099
4   大川の水    123   芥川 1.1574074 6.1728395 1.15740741 1.5432099
5 侏儒の言葉    158   芥川 0.5505908 0.6776502 0.12705942 0.1694126
6 もの思う葦 ――当りまえのことを当りまえに語る。   1587   太宰 0.3074558 0.6917756 0.07686395 0.1537279
         て        で        と         に        は        ば         へ
1 2.5204789 2.0163831 4.7889099  3.4026465 2.1424071 1.2602394 0.88216761
2 0.5483584 0.4798136 0.8225375  0.6169031 0.6169031 0.4112688 0.06854479
3 5.0154321 4.2438272 8.1018519 10.0308642 3.8580247 3.4722222 1.92901235
4 5.0154321 4.2438272 8.1018519 10.0308642 3.8580247 3.4722222 1.92901235
5 0.2541188 0.2541188 1.0588285  1.3129474 0.2964720 0.5505908 0.21176570
6 0.3843198 0.2305919 0.6149116  0.3843198 0.4611837 0.2305919 0.15372790
         も         ら         り
1 1.8903592 0.37807183 0.37807183
2 0.4112688 0.06854479 0.06854479
3 4.2438272 0.77160494 1.15740741
4 4.2438272 0.77160494 1.15740741
5 0.7200034 0.12705942 0.12705942
6 0.3074558 0.07686395 0.07686395

これで、SVMで分析するためのデータセットが完成しました。

著者プロフィール

松村優哉(著者)
(Twitter : @y__mattu
慶應義塾大学経済学部4年生。専攻は統計学とそのマーケティングへの応用。周辺分野として機械学習や自然言語処理の理論や技術も学習中。RやSASによるデータ解析のブログ「データ解析備忘録」を運営。
匿名知的集団ホクソエム(著者)
ホクソエム (hoxo_m) は架空のデータ分析者であり、日本の若手のデータ分析者集団のペンネームである。当初このデータ分析者集団は秘密結社として活動し、ホクソエムを一個人として活動させ続けた。