2016.09.13
第5回 青空文庫のテキストマイニングをRMeCabパッケージでやってみた
RMeCabパッケージを使った青空文庫のテキストマイニングを行い、芥川龍之介と太宰治の作品を判別します。サポートベクトルマシンによる著者判別・チューニングまですべてをRで行います。
1. はじめに
テキストマイニング(Text Mining)とは、テキストを対象にしたデータマイニングの理論や技術の総称です。
一般にデータマイニングを行うためにはデータが必要になりますが、テキストマイニングやそれを文学作品の分析に応用した計量言語学の分野では文章データに加えて、しばしばコーパスが用いられます。 コーパスとは、書き言葉や話し言葉をジャンルなどを考慮した上で網羅的に収録したデータベースのことを指します。
また、日本語のテキストマイニングを行う上で重要になるのが形態素解析です。 日本語は英語などの言語と異なり、単語と単語を離して書くことがないため、日本語のテキストを分析する際には文章を単語単位に分割する必要があります。 このような日本語の単語分割を容易に行えるツールとしてMeCabがあり、それをR上で動かすためのパッケージとしてRMeCabがあります。 例えば、「すもももももももものうち」という文章に対してRMeCabを使って単語分割をすると、以下のような出力が得られます。
(なお、MeCabはインストールされていることを前提とします。また、RMeCabパッケージはCRANに登録されてないので、インストールの際にレポジトリの指定が必要です。)
1 2 3 |
[[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. データの取得
今回使うパッケージを読み込みます。自分の環境にパッケージが入っていなければ適宜インストールします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | install.packages( "rvest" ) library(rvest) install.packages( "dplyr" ) library(dplyr) install.packages( "stringr" ) library(stringr) library(RMeCab) # 作業ディレクトリを作成 dir.create( "./analysis" ) # 作業ディレクトリを設定 setwd( "./analysis" ) |
まずは芥川の作品リストを取得します。rvestパッケージ1を使って芥川のページをスクレイピングして作品リストを取得し、公開中のものに絞ります。
1 2 3 4 5 6 7 8 9 | # 芥川の公開作品リストを取得---- ## スクレイピングで作品リストを取得 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) " |
1 2 3 4 | ## データフレーム化 a_text_open <- a_text[1:374] %>% #公開作品のみを抽出 as.data.frame() #データフレーム化 colnames(a_text_open) <- c( "text_name" ) #列名を設定 |
3-2. ランダムサンプリング
次に新字新仮名の作品に絞ります。解析には10作品しか使わないので、さらにランダムサンプリングで10個だけを抽出します。 この部分はデータフレーム操作のためのdplyrパッケージと文字列操作のためのstringrパッケージを用いて行います。
1 2 3 4 5 6 7 8 9 10 11 | ## 新字新仮名の作品に絞り、作品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 芥川 |
ここまでで芥川のデータが作り終わりました。続いて太宰についても同様の作業を行います。
1 2 3 4 5 6 7 8 | # 太宰についても同様にやる----- 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) " |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | ## データフレーム化 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) #ランダムサンプリング |
太宰のデータができたら芥川のデータと縦に連結させます。
1 2 3 | # 芥川と太宰を連結 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の指定自体は自分でやらなければなりません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | # ダウンロードして解凍---- 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); } } ## ダウンロード ### 芥川 ### 太宰 |
3-4. 形態素解析とデータの加工
データのダウンロードが済んだので、次はRMeCabパッケージを用いて形態素解析を行います。 RMeCab::docDF()
は、指定したディレクトリのファイル全て、特定のファイル、あるいはデータフレームに対しNgramを作成してくれる関数です。
Ngramとは、ある文書に対して特定のN語がどのくらいの回数現れているかを示したものです。ここでは、特定の語と読点、つまり2語がどのくらいの頻度で現れているのかを知りたいので、バイグラムを作成します。そのため、docDF()の N = 2
が指定されています。
さらに、『Rで学ぶ日本語テキストマイニング』にならって、20作品における16種類の文字(か, が, く, し, ず, て, で, と, に, は, ば, へ, も, ら, り, れ)と読点の共起頻度を調べるためにフィルタリングをします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | # 形態素解析---- 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になってしまうので、頻度を合計する際は何行目がどの文字であったかに注意しながら処理を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # 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語あたりの相対頻度に変換します。
1 2 3 4 5 6 7 8 | ## 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 | ### 連結してデータフレーム化 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で分析するためのデータセットが完成しました。