機械学習下準備〜CaboCha組み込み〜

 前々回MeCabに対応させた人工知能にCaboChaを組み込みます。
 とか言ってCaboCha、「美しい水車小屋の乙女」を以下のように展開してくださるのでちょっと不安ではある。

美しい => 水車小屋の
水車小屋の => 乙女
乙女 => None

美しい水車小屋/の/乙女(美しいとは言っていない)

 現在は「名詞,(一般|代名詞|固有名詞|サ変接続|形容動詞語幹)」に合致するものがあれば辞書に登録、というロジックになっていますが、この名詞にかかる修飾も一緒に辞書登録していきましょう。

  • 入力「白い猫は可愛い」
  • 現在の実装による辞書登録「猫 白い猫は可愛い」
  • 目標とする辞書登録「白い猫 可愛い」

 その他、名詞が連続する場合(フルネームや地名がバラける傾向にある)はひとかたまりとして認識するなどの操作も必要になります。
 具体的にこう

    def study_pattern(self, text, parts):
        """ユーザーの発言textを、形態素partsに基づいてパターン辞書に保存する。"""
        noun = ""
        for word, part in parts:
            if self.is_noun(part):
                # 品詞が名詞であれば連結
                noun+=word
            else:
                # 品詞が名詞でなければ
                if noun != "":
                    # かつnounが空でなければ
                    duplicated = next((p for p in self._pattern if p['pattern'] == noun), None)
                    if duplicated:
                        if not text in duplicated['phrases']:
                            # かつファイルに重複がなければ(pattern, phrase双方)
                            # 新しいパターンを辞書に登録
                            duplicated['phrases'].append(text)
                    else:
                        # 既に登録されている場合はパターンを追加
                        self._pattern.append({'pattern': noun, 'phrases': [text]})
                    noun = ""
        if noun != "":
            # おなじ
            duplicated = next((p for p in self._pattern if p['pattern'] == noun), None)
            if duplicated:
                if not text in duplicated['phrases']:
                    duplicated['phrases'].append(text)
            else:
                self._pattern.append({'pattern': noun, 'phrases': [text]})
            noun = ""

 リファクタリングはしましょう。こうすることにより「東京都郊外にある小さなアパート」という文章が以下のように登録されます。

東京都郊外   東京都郊外にある小さなアパート
アパート    東京都郊外にある小さなアパート

 名詞の塊を取得することができたのでこれにかかる修飾子を取得していきます。このままだと例文が悪いので「白い猫は可愛い」に本題を戻します。
 かぼちゃパイの結果はこう

$ python cabocha.py 
白い => 猫は
猫は => 可愛い
可愛い => None

 MeCabの解析はこう

白い  形容詞,自立,*,*,形容詞・アウオ段,基本形,白い,シロイ,シロイ
猫   名詞,一般,*,*,*,*,猫,ネコ,ネコ
は   助詞,係助詞,*,*,*,*,は,ハ,ワ
可愛い 形容詞,自立,*,*,形容詞・イ段,基本形,可愛い,カワイイ,カワイイ
EOS

 ここから「白い猫」が取れればOKです。
 今思うとこれパターン辞書に登録する予定のものじゃないのであとでごっそりリファクタリングしなくてはならないな……

import re
from MeCab import Tagger
import CaboCha
import itertools


class Dictionary:
〜略〜
    def study(self, text):
        """かぼちゃパースはあらかじめやっておく"""
        cp = CaboCha.Parser('-n 0')
        tree = cp.parse(text)
        tokens = self.to_tokens(tree)

        self._head_tokens = [token for token in tokens if self.has_chunk(token)]
        self._lasts = self.chunk_by(self.has_chunk, tokens)

        links = [x.chunk.link for x in self._head_tokens]
        self._link_words = [self.concat_tokens(x, self._head_tokens, self._lasts) for x in links]
        """ランダム辞書、パターン辞書をメモリに保存する。"""
        self.study_random(text)
        self.study_pattern(text, Dictionary.analyze(text))

〜略〜
    def study_pattern(self, text, parts):
        """ユーザーの発言textを、形態素partsに基づいてパターン辞書に保存する。"""
        noun = ""
        for word, part in parts:
            if self.is_noun(part):  # 品詞が名詞であれば学習
                noun+=word
            else:
                if noun != "":
                    phrases = self.get_predicate(noun) # 述語(可愛い)を取得
                    noun = self.get_modifier(noun) + noun # 主語(白い+猫)を取得
                    duplicated = next((p for p in self._pattern if p['pattern'] == noun), None)
                    if duplicated:
                        if not phrases in duplicated['phrases']:
                            duplicated['phrases'].append(phrases)
                    else:
                        self._pattern.append({'pattern': noun, 'phrases': [phrases]})
                    noun = ""
        if noun != "":
            phrases = self.get_predicate(noun)
            noun = self.get_modifier(noun) + noun
            duplicated = next((p for p in self._pattern if p['pattern'] == noun), None)
            if duplicated:
                if not phrases in duplicated['phrases']:
                    duplicated['phrases'].append(phrases)
            else:
                self._pattern.append({'pattern': noun, 'phrases': [phrases]})
            noun = ""

    def get_predicate(self, needle):
        for(i, to_word) in enumerate(self._link_words):
            from_word = self.concat_tokens(i, self._head_tokens, self._lasts)
            if(type(from_word) == str and re.match(needle, from_word)):
                return to_word

    def get_modifier(self, needle):
        for(i, to_word) in enumerate(self._link_words):
            from_word = self.concat_tokens(i, self._head_tokens, self._lasts)
            if(type(to_word) == str and re.match(needle, to_word)):
                return from_word

    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 = [x.surface for x in lasts[i]]
        return word + ''.join(last_words)

〜略〜

 で、結果(辞書ファイル)がこう

白い猫 可愛い

 やったぜ!
 これで入力から言葉を覚えてくれるようになりました。ねこはかわいいのだ。

 ……と喜んだのもつかの間、「ケーキは好き?」と聞いてみたら「好き」が名詞として判定されることがわかり……形容詞語幹……。
 もうちょっと調整に時間がかかりそうです。ぐぬぬ。