前回のエントリで書いたように、外国人向け日本語教科書に登場する単語の分析をしている友人の作業を手伝うために、形態素解析エンジンMeCabのインストールと、RMeCabのインストールを行いました。
今回は、その手伝い作業の内容について書いておきます。
その友人がやりたがっていた作業は何なのかというと、まず
こんな感じで単語が縦に並んだCSVファイル(1列なのでカンマは出てこないですが)が20個ぐらいあって、それぞれの単語に対し、品詞を判定して横に入力したいとのこと。つまり実際には形態素解析と言っても分かち書きをやりたいわけではなく、単に品詞が分かればいい。
判定対象の単語は全部で述べ3万8000ぐらいあるので、手作業は厳しいので、MeCabに判定させようと思います。
なおデータをよく見ると、1行1単語になってなくて複数単語になってしまってる行があったり、カッコなど無駄な記号が入ってる行とかもあって綺麗なデータとはいえず、分かち書きも結局必要になりました。
さて実際の処理ですが、Rから実行することとします。
内容は単純で、RMeCabパッケージのRMeCabC()という関数を使います。この関数にテキストを渡すと、テキストを単語に分解した上で、要素名に品詞名を持った単語のリストが返ってきます。
> library(RMeCab) > RMeCabC("アジアけんきゅう") [[1]] 名詞 "アジア" [[2]] 動詞 "けん" [[3]] 名詞 "きゅう"
こんな感じで、リストの要素名称が品詞になっていて、要素として単語が入っています。
なお、上の結果をみると「けんきゅう」が2語に誤って分かれてしまっていますね。元のデータは、外国人向けの日本語教科書なのでひらがなが多いんですが、ひらがなばかりの文字列に解析をかけると誤認識が多くなるようです。
またそもそも、単語に分けた一覧表を解析してるので文脈からの判断ができないと思われ、もとの文章のまま文を単位として解析したほうが精度が高い気もします。
しかしまぁ、友人はなぜか上記のようなデータを作ってたので、仕方ない・・・。とにかく、このデータの横に、自動的に判定された品詞を付けてデータを返すことにしました。先にアウトプットのイメージを貼っておきます。
こんな感じで、Originalという列にもともとの解析対象となった単語が並んでおり、それぞれ形態素解析をかけて、Originalの横に単語と品詞が表示されるようにします。1行が1単語じゃないものについては、単語→品詞→単語→品詞...というふうに表示されるようにします。精度低いですねw
書いたコードの主要部分だけ貼り付けておくと、以下のような感じです。
# 元のcsvファイルを読み込んで、単語の一覧表をベクトルに格納する関数を記述。 read.source <- function(path) { # pathのところにcsvファイルのパスを記述すると、単語一覧をベクトルとして返す。 t <- read.csv(path, header=TRUE) t <- t[,1] t <- as.character(t) return(t) } # 単語一覧のベクトル(入力ベクトルと呼んでおく。)を与えると、1個1個に # MeCabの解析をかけた結果を返す関数を記述する。 # ただし入力ベクトルは、単語一覧といいながら複数の単語が混じっている要素 # もあるので、それぞれ分かち書きを行った上で、単語、品詞、単語、品詞、、、 # と横に並べることにする。 get.pos <- function(vec) { # vecに入力ベクトルを与えると、最終的に元の単語と品詞(複数の単語に分解 # されるものは単語→品詞→単語→品詞...と横に並ぶ。)を1行ずつまとめた # データフレームが出力されるような関数をつくる。 # 品詞は英語でpart of speechというらしいので、get.posにしておいた。 # まず、入力ベクトルの各要素にRMeCabC関数で形態素解析を行い、各要素の # 解析結果を1要素とするリストを返す。 # ただし、RMeCabC関数はそれ自体がリストを返すので、unlist()でいったん # ベクトルに変換している。 list <- lapply(vec, function(p){unlist(RMeCabC(p))}) # 入力ベクトルの要素の中には、5つ、6つの単語に分かち書きされてしまう # ものがある。アウトプットのテーブルには、分かち書きの最大個数分だけ列の # 数が必要なので、最大で何語に分離されてるかを確認。 elements.length <- c() for (i in 1:length(list)) { elements.length <- c(elements.length, length(list[[i]])) } max.element.length <- max(elements.length) # 最終的に、単語、品詞、単語、品詞と並ぶようなテーブルを出力したい。 # そのためまず、入力ベクトルの要素数を行数とし、分かち書き個数が最大の # 要素の要素数を列数とする行列をつくり、入力ベクトルの各要素に形態素解析 # を行って、1行1行入れていくことにする。 # 分かち書き個数が最大より小さい行については、後ろに空の要素を追加して、 # 長さをそろえる。 # 上のリスト要素1個1個を、単語・品詞・単語・品詞...という順番に並び # 替えてベクトルを返す。 get.row <- function(element){ names <- names(element) row <- c() for(i in 1:length(element)) { row <- c(row, element[i], names[i]) } names(row) <- NULL # 要素名を消す # 最大長より短いものについて、足りない分を空の要素で埋める。 if (length(row) < (max.element.length * 2)) { for (i in 1:((max.element.length * 2)-length(row))) { row <- c(row, "") } } return(row) } # 結果をリストにまとめる。 # このリストの1要素1要素が、最終的なアウトプットの1行1行になる。 rows.list <- lapply(list, get.row) # アウトプット用に行列をまとめる。空の行列を作って、要素を置き換えていく。 output.matrix <- matrix(nrow=length(rows.list), ncol=(max.element.length * 2)) for (i in 1:length(rows.list)) { output.matrix[i,] <- rows.list[[i]] } # 最終アウトプットをデータフレームで得る。 output.df <- as.data.frame(cbind(Original=vec, output.matrix)) # 列に名前をつける。 colnames <- c() for (i in 1:max.element.length) { colnames <- c(colnames, "Word", "POS") } colnames(output.df)[2:((max.element.length * 2)+1)] <- colnames # ふつうにrep()でつくればよかったな return(output.df) } # 実行用の関数 parse.words <- function(path) { vec <- read.source(path) # ファイルを読み込んでベクトルに変換 output <- get.pos(vec) # 解析して出力用DFを得る return(output) }
このあとは20個ぐらいある解析対象のcsvファイルのパスを最後のparse.words()に渡し、作成されたデータフレームをまたwrite.csv()でcsvとして書き出しました。
精度は低いですが、後は自分で何とかしてくれと言って、友人に渡しました。