読者です 読者をやめる 読者になる 読者になる

【自然言語処理】単語変換テーブルが必要になったので作る。そしてちょっと間違える。

text2vecでベクトル化したあと、どうしたら面白いことができるか?を考えているのですが、それ以前の問題にぶち当たってとりあえずの解決方法を思いついたので記録に残します。

やりたいこと。

そもそもの発端はwikipediaフランス革命の記事をベクトル化したら一体何ができるのか?」という思いつきでした。要はword embeddingでどんなことができるかを試してみたかったわけで、とりあえずいろんな人がよく知っているフランス革命をターゲットとしたのでした。や、ターゲットはなんでもよかったんですけどね、そこは、ほら、個人的な趣味、ということで。

環境

ちなみに、MeCabで使っている辞書はneologdですが、最新のではなく、2016年10月26日時点のものです。なぜなら、windowsMeCabにneologdを入れるのに、

github.com

こちらの方法をつかったためです。いずれwindowsMeCabでも常に最新のneologdを使えるようにしたいのですが、今のところはこちらでやっています。まぁ、基本的な固有名詞なら問題ないとは思っています。新語や作品名などになると難しいでしょうけれど、フランス革命に出てくる用語くらいだったら大丈夫でしょう。

発覚した問題点

まず、wikipediaフランス革命の記事を適当にテキストファイルにしてください。改行の処理はお好みで。今回は各小見出し毎にパラグラフにまとめました。テキストファイルの文字コードはSHIFT-JISにしています。UTF-8でも構いませんが、当方のMeCabはSHIFT-JISでインストールしたので、SHIFT-JISの文字コードの方がやりやすいです。もちろん、UTF-8でもどうにでもできますけれども。で、こんな感じに準備していきます。

# ライブラリ設定
library(text2vec)
library(textmineR)
library(ggplot2)

############################################################
# テキストファイル読み込み
############################################################

# ファイルパスを設定
path <- file.path(getwd(),"R/test_codes")
txt <- scan(file=file.path(path,"FrenchRevolution.txt"),
            what = character(),
            sep = "\n",
            blank.lines.skip = T)

後日使う予定でggplot2を読み込んでいますが、今回の記事では使っていません。ファイルパスはお好みで。当方は基本的にDocuments以下にR用のフォルダを作ってその中で作業をしています。RのデフォルトのワーキングディレクトリがDocumentsなので。
上記のコードでテキストファイルを読み込んで、いったん形態素解析にかけます。

############################################################
# 形態素解析
############################################################

morph_out <- sapply(txt, 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[1])
    } else if(x[2] %in% c("動詞")){
      return(x[8])
    }
  }))
  #半角スペースを空けてひとつにつなげる←ここがポイント
  out_sentence <- paste(out_mec_noun, collapse=" ")
  
  return(out_sentence)
})

names(morph_out) <- seq(1,length(morph_out))

RMeCabはつかいません。
何をやっているかは次の記事を参考にしてください。

wanko-sato.hatenablog.com

単語を半角スペースで区切った文字列ベクトルを作っておけば、後は英語と同じ要領でdocument term matrixを作れるので、RMeCabを使う必要がそもそもなくなるのです。個人的にはこのやり方が気に入っています。
さて、こうして形態素解析をした後の文字列ベクトルがどうなっているか、確認してみましょう。

> morph_out[1]

"フランス革命 フランス かく めい 仏 Revolution francaise 英 French Revolution 18世紀 フランス王国 ブルボン朝 起きる 市民革命 世界 史上 代表 的 市民革命 前近代 的 社会体制 変革 する 近代 ブルジョア 社会 樹立 する 革命 1787年 ブルボン朝 王権 貴族 反抗 始まる 擾乱 1789年 全 社会 層 巻き込む 本格 的 革命 なる 政治体制 絶対王政 立憲王政 共和制 移り変わる 1 7 9 4 年 テルミドール 反動 のち 退潮 向かう 1799年 ナポレオン・ボナパルト クーデター 帝政 樹立 至る 1799年 11月 9 日 ブリュメール 1 8 日 クーデター 注 1 一般的 1 7 8 7 年 貴族 反抗 1799年 ナポレオン クーデター 革命 期 する れる いる フランス 王政 アンシャン・レジーム 崩壊 する 過程 封建的 諸 特権 撤廃 する れる 近代 的 所有権 確立 する れる 一方 アッシニア 紙幣 混乱 起こる" 

フランス王国」「ブルボン朝」「アンシャン・レジーム」など、デフォルトのMeCabの辞書では分割されてしまう文字列がちゃんと一語として認識されています。neologdすげぇ。テルミドール反動」も一語で認識されるとうれしかったのですが、そこは後でどうにかします。
ところが、よくよくみると「1 7 9 4 年」とか「1 7 8 7 年」とか、うまく認識されていない箇所があります。これはちょっと問題ですね。どうにかしなければなりません。

変換すべき単語を確認する

そこで、一語として認識されてほしい単語をチェックするため、N-gram(今回は1~5gram)でTF-IDFを出します。

############################################################
# 複合語出現チェック
############################################################

# text2VecでDocument-Term-Matrixを作る
tokenizer <- itoken(morph_out,
                    ids = seq(1,length(morph_out)))
vocab <- create_vocabulary(tokenizer,
                           ngram = c(1,5),
                           sep_ngram = "_")
vocab$vocab$terms <- iconv(vocab$vocab$terms,"cp932")

# DTMを作る
vectorizer <- vocab_vectorizer(vocab)
dtm <- create_dtm(tokenizer, vectorizer)
colnames(dtm) <- iconv(colnames(dtm),"cp932")

# TF-IDF
tfidf <- TermDocFreq(dtm)

で、出てきたTF-IDFをterm_freqの降順でソートして表示させてみると、

term term_freq doc_freq idf
する 193 16 0.0606246218164348
れる 67 15 0.125163142954006
1 64 13 0.268263986594679
する_れる 44 13 0.268263986594679
いる 42 15 0.125163142954006
33 12 0.348306694268216
31 10 0.53062825106217
7 30 11 0.435318071257845
2 28 9 0.635988766719997
8 25 9 0.635988766719997
フランス 24 12 0.348306694268216
革命 24 9 0.635988766719997
4 22 9 0.635988766719997
0 21 10 0.53062825106217
1_7 21 8 0.75377180237638
ルイ16世 21 7 0.887303195000903
中略
第_二 6 3 1.73460105538811
第_二_身分 6 3 1.73460105538811
1_0 6 3 1.73460105538811
承認 6 2 2.14006616349627
財務 6 1 2.83321334405622
5 5 1.22377543162212
支持 5 5 1.22377543162212
9_4 5 5 1.22377543162212
1_7_9_4 5 5 1.22377543162212
9_4_年 5 5 1.22377543162212
7_9_4_年 5 5 1.22377543162212
10月 5 5 1.22377543162212
7_9_4 5 5 1.22377543162212
1_7_9_4_年 5 5 1.22377543162212
中略
1_4_日 3 3 1.73460105538811
以上 3 3 1.73460105538811
革命_勃発 3 3 1.73460105538811
なる_いる 3 3 1.73460105538811
共和制 3 3 1.73460105538811
平民 3 3 1.73460105538811
1789年 3 3 1.73460105538811
復活 3 3 1.73460105538811
成立_する 3 3 1.73460105538811
3 3 1.73460105538811
3 3 1.73460105538811
国内 3 3 1.73460105538811
世界 3 3 1.73460105538811

「する」とか「れる」とかのいらない子が出てくるのでこれは後で削除します。
なぜか「第三身分」は一語で認識されるのですが、「第一身分」「第二身分」はだめでした。neologdは庶民の味方なんでしょうか。
「1789年」はいけてるんですが、「1 7 9 4 年」とかはだめですね。なんででしょう?

※たしかMeCab形態素解析をするとき、「コスト」という概念を使っていたかと思います。一語として認識されるためにコストを最小にする、というようなアルゴリズムになっていたはずで、ということは、たとえば「1789年」のような割と意味のある年号だったらコストは下がるけれど、そうでない場合は一語に認識されない、というような感じでしょうか。詳しいことは下記を参照のこと。

techlife.cookpad.com

単語変換テーブルが必要だ

自分が分析したい対象の文書集合で重要な単語がうまく形態素解析されない場合、アプローチとしてまず思いつくのは「ユーザ辞書を作る」です。これは正攻法ですし、一番やりやすい方法だと思うのですが、ちょっと気になるのが上記で上げた「コスト」の問題です。ユーザ辞書を作るにせよ、「コスト」の設定が正しくできなければ思うような結果が得られないのではないか、と考えます。

そこで、もっと単純に、「一語に認識してほしい単語を形態素解析した後に一語に変換する」というアプローチでも良いんじゃないか、と思いついたわけです。

「第一身分」「第二身分」のような単語は後日変換テーブルを作成するとして(なんせ手作業なもんで)、今回は「年」「月」「日」の変換テーブルを簡単に作っちゃいます。

# 空のデータフレームを作る
convertTBL <- data.frame(before="",
                         after="",
                         stringsAsFactors = F)

x <- 1

# 年の処理
for(i in 1600:2200){
  convertTBL[x,1] <- paste(strsplit(paste(i, "年",sep=""),split="")[[1]],collapse=" ")
  convertTBL[x,2] <- paste(strsplit(paste(i, "年",sep=""),split="")[[1]],collapse="")
  x <- x + 1
}

# 月の処理
for(i in 1:12){
  convertTBL[x,1] <- paste(strsplit(paste(i, "月",sep=""),split="")[[1]],collapse=" ")
  convertTBL[x,2] <- paste(strsplit(paste(i, "月",sep=""),split="")[[1]],collapse="")
  x <- x + 1
}

# 日の処理
for(i in 1:31){
  convertTBL[x,1] <- paste(strsplit(paste(i, "日",sep=""),split="")[[1]],collapse=" ")
  convertTBL[x,2] <- paste(strsplit(paste(i, "日",sep=""),split="")[[1]],collapse="")
  x <- x + 1
}

これで「1600年」~「2200年」までの年の変換テーブル、および月、日の変換テーブルができました。Rではforループがあまり推奨されていませんが、この程度の単純なループならわざわざapply使うよりもコードが簡単です。「forループを使ってはいけない!」と凝り固まるよりは、柔軟に対応してもいいんじゃないでしょうか。
で、このデータフレームをCSVで吐き出して保存しておけば、いつでも使えるようになります。

############################################################
# 年月日調整
############################################################

morph_out_c <- sapply(morph_out,function(x){
  out <- x
  for(i in seq(1:nrow(convertTBL))){
    out <- gsub(convertTBL[i,1],convertTBL[i,2],out)
  }
  return(out)
})

これでうまくいくはずです。

> morph_out_c[1]

"フランス革命 フランス かく めい 仏 Revolution francaise 英 French Revolution 18世紀 フランス王国 ブルボン朝 起きる 市民革命 世界 史上 代表 的 市民革命 前近代 的 社会体制 変革 する 近代 ブルジョア 社会 樹立 する 革命 1787年 ブルボン朝 王権 貴族 反抗 始まる 擾乱 1789年 全 社会 層 巻き込む 本格 的 革命 なる 政治体制 絶対王政 立憲王政 共和制 移り変わる 1794年 テルミドール 反動 のち 退潮 向かう 1799年 ナポレオン・ボナパルト クーデター 帝政 樹立 至る 1799年 11月 9日 ブリュメール 1 8日 クーデター 注 1 一般的 1787年 貴族 反抗 1799年 ナポレオン クーデター 革命 期 する れる いる フランス 王政 アンシャン・レジーム 崩壊 する 過程 封建的 諸 特権 撤廃 する れる 近代 的 所有権 確立 する れる 一方 アッシニア 紙幣 混乱 起こる"

あっ、「1 8日」になってしまっていました。文字数の長い順に並べないといけなかったですね。それについては後日修正するとして、ひとまずはこれで当初の問題が解決できるようになりました。めでたし。

本来の用途

日本語を対象にした場合、辞書の問題がまずはじめにあるのですが、英語の場合はこの方法が効くんじゃないかと思っています。たとえば"confidential interval"はばらしてしまうと違う意味になってしまうし、わざわざそのためにN-gramにするのもノイズが入って面倒です。ならば、はじめからアンダーバーなりなんなりでつないで一語として扱えるようにしちゃえばいいじゃないか。そういう発想です。