機械学習下準備〜CaboChaで係り受けの解析〜

CaboChaインストール

取り敢えず形態素解析はできたので文法沼に沈む前にCaboChaのインストールに移ります。
この時点で「係り受けの判定が入ったらまた大幅に書き直すことになるのでは?」と気が付きちょっと暗澹としています。
目論見としては

  1. 係り受けを意識した主語の判定
  2. 係り受けを意識した述語/形容の判定

あたりができるようになるといいなと。
「佐藤さんの家の猫は白くて可愛い」のような文章があったときに、「佐藤さんの家の猫」「白い」「可愛い」あたりを取得したいのだけど、MeCabだけでやると「猫」「白い」「可愛い」になってしまうのでこの辺の調整です。猫は必ずしも白くない、みたいな。例文が思いつかなくて青空文庫をうろうろしましたがちょうどいい文章が見つからなかった……。

参考サイトはこちら:係り受け解析器CaboChaをPythonから使う – Spot
2016年で更新止まってるし見た感じ消し忘れた過去のページって雰囲気(ヘッダのリンク切れてるし)なのでここ近い内に消えるのかもな……

CaboChaオフィシャルサイトはこちら:CaboCha/南瓜
titleタグがカオボチャになっとる……なんだか悲惨な響き……っていうかこれ最終更新15年って大丈夫か? と思ったらダウンロードページには17年版がありました。サイトが更新されてないだけか。

インストールコマンド

$ tar xvfz cabocha-0.69.tar.bz2 
$ cd cabocha-0.69
$ ./configure --with-mecab-config=`which mecab-config` --with-charset=UTF8
$ make && make check
$ sudo make install
$ cd python
$ python setup.py build
$ python setup.py install

tarコマンドは専門学校時代から合わせて八年は叩いてるはずなんですが未だにxvfz部分が覚えられない。頻度が低いとはいえそろそろ何も見ずに打てるようになりたい……。

んでまあインストールが終わったのでコンソールから適当に叩いてみたのですけど

python
Python 3.6.5 (default, Aug  6 2018, 11:02:37) 
[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import CaboCha
>>> import itertools
>>> cp = CaboCha.Parser('-f1')
>>> tree = cp.parse('東京のラーメン屋がいつも混雑しているわけではない')
>>> tokens = [tree.token(i) for i in range(0, tree.size())]
>>> print(tokens)
[<CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b600> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b090> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b630> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c02b660> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3060> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3240> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3300> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b32d0> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3330> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b33f0> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3420> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3450> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b3480> >, <CaboCha.Token; proxy of <Swig Object of type 'CaboCha::Token *' at 0x10c4b34b0> >]

んん。
MeCabのようにサクッと中身が見えたりはしません。参考サイトのソースを拝借してcabocha.pyとしてファイル保存してテストします。
どうでもいいけどかぼちゃパイって美味しそうですね。ふかした南瓜をなめらかに潰してはちみつとかで甘みを足して、焼いた後でかけるのは生クリームでもカラメルソースでもいいですね。中身に歯ごたえがないので生地にかぼちゃの種とかナッツ類を混ぜ込んでも美味しいと思います。何の話?

ただし参考サイトのソースはPython2系で書かれているらしく、そのまま動かそうとしても 'filter' object is not subscriptable と言われて落ちます。

Python3用に書き直したソース

# -*- coding: utf-8 -*-
import CaboCha
import itertools

def chunk_by(func, col):
    '''
    `func`の要素が正のアイテムで区切る
    '''
    result = []
    for item in col:
        if func(item):
            result.append([])
        else:
            result[len(result) - 1].append(item)
    return result

def has_chunk(token):
    '''
    チャンクがあるかどうか
    チャンクがある場合、その単語が先頭になる
    '''
    return token.chunk is not None

def to_tokens(tree):
    '''
    解析済みの木からトークンを取得する
    '''
    return [tree.token(i) for i in range(0, tree.size())]

def concat_tokens(i, tokens, lasts):
    '''
    単語を意味のある単位にまとめる
    '''
    if i == -1:
        return None
    word = tokens[i].surface
 -     last_words = map(lambda x: x.surface, lasts[i])
 +     last_words = [x.surface for x in lasts[i]]
    return word + ''.join(last_words)

raw_string = u'東京のラーメン屋がいつも混雑しているわけではない'

cp = CaboCha.Parser('-f1')
tree = cp.parse(raw_string)
tokens = to_tokens(tree)

 - head_tokens = filter(has_chunk, tokens)
 + head_tokens = [token for token in tokens if has_chunk(token)]
 - words = map(lambda x: x.surface, head_tokens)
 + words = [x.surface for x in head_tokens]

lasts = chunk_by(has_chunk, tokens)

 - links = map(lambda x: x.chunk.link, head_tokens)
 + links = [x.chunk.link for x in head_tokens]
 - link_words = map(lambda x: concat_tokens(x, head_tokens, lasts), links)
 + link_words = [concat_tokens(x, head_tokens, lasts) for x in links]

for (i, to_word) in enumerate(link_words):
    from_word = concat_tokens(i, head_tokens, lasts)
    print("{0} => {1}".format(from_word, to_word))
$ python cabocha.py
東京の => ラーメン屋が
ラーメン屋が => 混雑しているわけではない
いつも => 混雑しているわけではない
混雑しているわけではない => None

よろしい。
次回はこれを人工知能に組み込んでいきます。

機械学習下準備〜MeCabで形態素解析〜

MeCabインストール

参考サイトはこちら:今更ながらPythonとMeCabで形態素解析してみた – イノベーション エンジニアブログ

$ brew install mecab-ipadic

$ mecab
すもももももももものうち
すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の   助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

すっもっもっもっももっもっ。
あとMeCabをPythonから使うために mecab-python3 も入れます。

$ pip install mecab-python3

$ python
Python 3.6.5 (default, Aug  6 2018, 11:02:37) 
[GCC 4.2.1 Compatible Apple LLVM 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import MeCab
>>> m = MeCab.Tagger()
>>> print(m.parse("すもももももももものうち"))
すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
も   助詞,係助詞,*,*,*,*,も,モ,モ
もも  名詞,一般,*,*,*,*,もも,モモ,モモ
の   助詞,連体化,*,*,*,*,の,ノ,ノ
うち  名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
EOS

参考サイトのソースをMeCabで書き直し

先人のリソースありがたやありがたや。
ちなみに途中からやりたかったのでMasterからコピーするのではなく途中のコミットからコピーしてます。→79b4b990da6220543208879551737ac31e6cddf0
一番困ったのはjanome.tokenizer.Tokenオブジェクトが無くなったことです。これにより t.surface とかのあたりが使えなくなり、かつMeCabの解析結果がStringだったのでなんかごにょごにょとパースするはめになりました。このあたりはjanomeの方が便利なんだなという印象。
Pythonの標準では複数文字を指定したsplitもできなかったのでreを使って下記のごとく処理しました。

[re.split('[\t,]',line) for line in re.split('[\n]',m.parse("すもももももももものうち"))]

"""結果"""
[
    ['すもも', '名詞', '一般', '*', '*', '*', '*', 'すもも', 'スモモ', 'スモモ'], 
    ['も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ'], 
    ['もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ'], 
    ['も', '助詞', '係助詞', '*', '*', '*', '*', 'も', 'モ', 'モ'], 
    ['もも', '名詞', '一般', '*', '*', '*', '*', 'もも', 'モモ', 'モモ'], 
    ['の', '助詞', '連体化', '*', '*', '*', '*', 'の', 'ノ', 'ノ'], 
    ['うち', '名詞', '非自立', '副詞可能', '*', '*', '*', 'うち', 'ウチ', 'ウチ'], 
    ['EOS'], 
    ['']
]

なんかEOSとか空行とかいらないものもありますがこの辺は手動でアレしましょう。

修正前(janome)

def analyze(text):
    """文字列textを形態素解析し、[(surface, parts)]の形にして返す。"""
    return [(t.surface, t.part_of_speech) for t in TOKENIZER.tokenize(text)]

修正後(MeCab)

def analyze(text):
    """文字列textを形態素解析し、[(surface, parts)]の形にして返す。"""
    tokens = [re.split('[\t,]',line) for line in re.split('[\n]',Dictionary.TAGGER.parse(text))]
    return [(t[0], ','.join([t[1], t[2], t[3], t[4]])) for t in tokens if len(t)==10]

ゴミを取り除くために if len(t)==10 とか書いてるんですけどどうなんですかねこれ。初心者なのでいまいち正しさがわかりません。Pythoner(Pythonist?)てきには気持ち悪い書き方だったりするかしら。
おとなしくオブジェクトクラス自作しちゃった方がソースが綺麗だったかもしれない……。後の課題にしましょう。
ちょっと動かしてみた感じだと形容動詞連体詞周りがヤバい。「大きな桃」だと連体詞判定、「大きい桃」だと名詞,形容動詞語幹になるのでこのあたりをパースして辞書に突っ込むのがはちゃめちゃに面倒臭そう。

形態素解析周りで「すもももももももものうち」をいっぱい見たせいで最強○×計画を聞きたくなり最強○×計画→ふぃぎゅ@メイト→巫女みこナースからニコニコ組曲に飛んでYoutubeの再生履歴がここだけ00年代。