2018年03月30日

言語処理100本ノックでPython入門 #30 - mecabを使って形態素解析

  
今日から言語処理100本ノック 2015「第4章 形態素解析」に入ります。

僕にとっては未知の分野、どんな問題なのか楽しみです。

■ 問題

夏目漱石の小説『吾輩は猫である』の文章(neko.txt)をMeCabを使って形態素解析し,その結果をneko.txt.mecabというファイルに保存せよ.このファイルを用いて,以下の問に対応するプログラムを実装せよ. なお,問題37, 38, 39はmatplotlibもしくはGnuplotを用いるとよい.

30. 形態素解析結果の読み込み
形態素解析結果(neko.txt.mecab)を読み込むプログラムを実装せよ.ただし,各形態素は表層形(surface),基本形(base),品詞(pos),品詞細分類1(pos1)をキーとするマッピング型に格納し,1文を形態素(マッピング型)のリストとして表現せよ.
第4章の残りの問題では,ここで作ったプログラムを活用せよ.


■ mecabのインストール

Windowsの場合は、http://taku910.github.io/mecab/ から、mecab-0.996.exe をダウンロードしてインストールするだけみたいです。Unixdだと自分でビルドするようなことが書いてありましたが、Macの場合は、brewコマンドでインストールできるみたいです。

で、以下のコマンドを投入。
brew install mecab
 
でも、失敗、どうもXCodeとhomebrewのバージョンが古いみたいなメッセージがでました。

で、まずは、XCodeをAppStoreからインストールします。
けっこうな時間がかかりました。
インストールしたあと、
sudo xcodebuild -license accept
でライセンスを承認。

その後、以下のコマンドでbrewをアップグレード
brew upgrade

brewが最新のものになったので、再度mecabと辞書をインストールします。 

brew install mecab 
brew install mecab-ipadic

これで準備完了です。

■ mecabファイルの作成

Pythonからmecabを呼び出すこともできるようですが、ここではコマンドラインから、neko.txtを読み込み、neko.txt.mecabを作成します。 neko.txtのあるフォルダに移動し、以下のコマンドを実行

mecab neko.txt -o neko.txt.mecab

neko.txt.mecabを作成すれば良いだけなので、これはすんなり行きました。 以下のようなファイルが作成されます。一部だけ抜粋して掲載
一    名詞,数,*,*,*,*,一,イチ,イチ
EOS
EOS
     記号,空白,*,*,*,*, , , 
吾輩    名詞,代名詞,一般,*,*,*,吾輩,ワガハイ,ワガハイ
は    助詞,係助詞,*,*,*,*,は,ハ,ワ
猫    名詞,一般,*,*,*,*,猫,ネコ,ネコ
で    助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
ある    助動詞,*,*,*,五段・ラ行アル,基本形,ある,アル,アル
。    記号,句点,*,*,*,*,。,。,。
EOS
名前    名詞,一般,*,*,*,*,名前,ナマエ,ナマエ
は    助詞,係助詞,*,*,*,*,は,ハ,ワ
まだ    副詞,助詞類接続,*,*,*,*,まだ,マダ,マダ
無い    形容詞,自立,*,*,形容詞・アウオ段,基本形,無い,ナイ,ナイ
。    記号,句点,*,*,*,*,。,。,。
EOS
EOS
     記号,空白,*,*,*,*, , , 
どこ    名詞,代名詞,一般,*,*,*,どこ,ドコ,ドコ
で    助詞,格助詞,一般,*,*,*,で,デ,デ
生れ    動詞,自立,*,*,一段,連用形,生れる,ウマレ,ウマレ
た    助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
か    助詞,副助詞/並立助詞/終助詞,*,*,*,*,か,カ,カ
とんと    副詞,一般,*,*,*,*,とんと,トント,トント
見当    名詞,サ変接続,*,*,*,*,見当,ケントウ,ケントー
が    助詞,格助詞,一般,*,*,*,が,ガ,ガ
つか    動詞,自立,*,*,五段・カ行イ音便,未然形,つく,ツカ,ツカ
ぬ    助動詞,*,*,*,特殊・ヌ,基本形,ぬ,ヌ,ヌ
。    記号,句点,*,*,*,*,。,。,。
EOS
■ mecabファイルの形式

http://taku910.github.io/mecab/ を読むと、mecabの出力フォーマットは、以下の通りです。
表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用型,活用形,原形,読み,発音
これが複数行続いています。文の終わりには、EOSという行があります。

問題にある「基本形」がどの項目を指しているのかわからないのですが、「原形」ってのがあるから、たぶんこのことだと思います。

mecabを使ってみて、株式会社平和情報センターが開発したHAPPINESSという日本語を分かち書きをするソフトを使って文章のなかからキーワードを抽出するプログラムをはるか昔に書いたことを思い出しました。 その頃のプラットホームはホストコンピュータでした。懐かしいなー。

■ Pythonのコード
import re

def analyze():
    lines = []
    sentence = []
    with open('neko.txt.mecab', 'r', encoding='utf8') as fin:
        for line in fin:
            words = re.split(r'\t|,|\n', line)
            if words[0] == 'EOS':
                if sentence:
                    lines.append(sentence)
                    sentence = []
                continue
            sentence.append({
                "surface": words[0],
                "base": words[7],
                "pos": words[1],
                "pos1": words[2],
            })
    return lines

def main():
    article = analyze()
    print(article[0])
    print()
    print(article[1])
    print()
    print(article[2])

if __name__ == '__main__':
    main()

ソースコードは、GitHubで公開しています。

■ 採用するデータの構造


関数analyzeが今回の目的のコードで、ここで問題が求めているデータを作成しています。


問題文のマッピング型ってのは、辞書形式のことらしいです。なので、ファイルの1行をマッピング型に格納し、1文をリストに格納することとします。

問題文は、「1文を形態素(マッピング型)のリストとして表現せよ」とありますが、ファイルには複数文のデータがあるわけだから、さらにこれをリストに格納するってことなのだと判断しました。
文章:リスト型 (文が複数格納される)
    1文:リスト型 (形態素が複数格納される)
        形態素:マッピング型 (surface,base, pos, pos1をキーとする)

■ 形態素のマッピング型を作成


読み込んだ1行の分割は、正規表現ライブラリの re.splitを使って分割します。
words = re.split(r'\t|,|\n', line)
この1行が1形態素になります。 これをsurface, base, pos, pos1をキーとするマッピング型にするということで、たぶん以下のようなオブジェクトにすればいいのだと思います。
{
    "surface": words[0],
    "base": words[7],
    "pos": words[1],
    "pos1": words[2],
}
C#だと、こういった場合は、4つのプロパティを持つクラスを定義することになると思いますが、Pythonだとこうするのが普通なのかな? JavaScriptのオブジェクトっぽいです。

このオブジェクトを1文を表すsentenceリストにappendメソッドで追加します。 

■ 文単位に分割する


入力データに'EOS'が来たら、それまでため込んでいた1文のデータ(sentence)を、linesに追加。
最初は以下のように書きました。
# if len(sentense) > 0: って書いたらpylintに怒られた
if sentence:
    lines.append(sentence)
    sentence.clear()  # 次の文を格納するために初期化
continue
でも、実行してみると、次のような結果になったので「なんで?」ってなってしまいました。
[]
[]
[]

そりゃそうですよね。
linesにアペンドしたものをクリアしちゃってるんだから。
なので、以下のように書き直します。

if sentence:
    lines.append(sentence)
    sentence = []
continue
これでOKです。
以下のようにオブジェクトの複製を作る方法もありですが、ちょっと効率が悪そうかな。
if sentence:
    lines.append(sentence.copy())
    sentence.clear()
continue


■ 結果

作成したプログラムでは、確認のために最初の3文の形態素を出力しています。

[{'surface': '一', 'base': '一', 'pos': '名詞', 'pos1': '数'}]

[{'surface': '\u3000', 'base': '\u3000', 'pos': '記号', 'pos1': '空白'}, {'surface': '吾輩', 'base': '吾輩', 'pos': '名詞', 'pos1': '代名詞'}, {'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'}, {'surface': '猫', 'base': '猫', 'pos': '名詞', 'pos1': '一般'}, {'surface': 'で', 'base': 'だ', 'pos': '助動詞', 'pos1': '*'}, {'surface': 'ある', 'base': 'ある', 'pos': '助動詞', 'pos1': '*'}, {'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}]

[{'surface': '名前', 'base': '名前', 'pos': '名詞', 'pos1': '一般'}, {'surface': 'は', 'base': 'は', 'pos': '助詞', 'pos1': '係助詞'}, {'surface': 'まだ', 'base': 'まだ', 'pos': '副詞', 'pos1': '助詞類接続'}, {'surface': '無い', 'base': '無い', 'pos': '形容詞', 'pos1': '自立'}, {'surface': '。', 'base': '。', 'pos': '記号', 'pos1': '句点'}]