麺匠 一丞の塩ラーメンとめかぶご飯

 やってきました三連休!
 というわけで(?)母と弟と合流して長町はバイパス近くの麺匠 一丞さんへ。中の人はペーパードライバー&車を持っていないので普段は電車移動ですが、今日は弟が運転してくれました。いぇーい楽。
弟が醤油、母が味噌だったので私は塩で。なんとなく選択を分ける習慣があります。

 一丞さんのラーメンは鰆のぶしでだしをとっています。
 透き通ったスープはぶしの香りがぶわっと香り、それを邪魔しない塩味のスープは大正解だったもよう。こしのあるシャキシャキの細麺、塩気ほのかなぷりぷりのチャーシュー、そこにアクセントを加える白髪ねぎと糸唐辛子。
ここしばらくげんちゃんラーメンの濃厚な味噌スープばかり食べていたので優しい味が臓腑に染み渡る心地です(もちろんどちらもとてもおいしいです笑)。

食べ終わってまだお腹に余裕があったので、メニューを見て気になっていためかぶご飯を追加。

 塩ラーメンのスープを掛けて食べると……これもおいしい。めかぶの香りが塩ラーメンのスープの香りと合わさって、ネバネバしためかぶとスープかけご飯のさらさら食感が合わさって、これだけでもどんぶりいっぱい食べたいおいしさ。頼んでみてよかったー!! と内心で喝采。
あとで調べてみたら長町駅から歩いても行ける距離なので、気になってたチャーシュー丼とか味玉丼とかも食べに行きたいです。

てるやのちんすこう

 明日発売のはずの本のフラゲ情報がツイッターのTLにズンドコ流れてきて羨ましくなってamazonで予約注文しているにもかかわらず本屋さんに行ってみたら目当ての本どころかラノベコーナーすら無くてがっかりして帰る道すがら、ちんすこうやさんを見つけました。
ちんすこう? 宮城で? こんなお店あった? あったら気付くよねこんな珍しいもの? と思いながら入って店主さんに訊いてみると、おとといオープンだったとのこと。なるほど知らないはずだ。

 ショーケースに入ったちんすこうはお土産でよく見る楕円形ではなく、スノーパフやまころん(※ローカル菓子)(マカロンではない)のような半円形。味はプレーン・シナモン・ココア・抹茶・紫芋のいつつ。
お店の中はまだ新しい木の香り。入って左手にかまどがあり、奥にはカフェスペースらしきもの。フラワースタンドで塞がれていたので後日入れるようになるのかもしれません(訊いてくればよかった)。
 全味八つ入りのバラエティパックてきなもの(¥500)があったのでそれを購入。


左:仙台まころん、右:てるやのちんすこう

 わざわざ比較のためにまころんを買ってきました。

プレーンはキビ糖の素朴な甘さ。ココアはココアパウダーの苦味と香りがきちんと効いています。香ばしくてほろ苦くて、でもクッキーとは違う口溶け。牛乳に合いそうです。
抹茶も香りが強くてなめらかでおいしい。とてもいい香り。紫芋は少し塩が入っていてこれは緑茶向き。ぴりっと引き締まっておいしい。沖縄塩とかなんかあるんでしたっけ、あの少し変わった味。
シナモンはシナモンの香り。キビ糖の甘さがよく合います。これはきっと砂糖なしのロイヤルミルクティーがおいしい。

奥のカフェスペースらしきものが気になるので一、二週間くらい経ったらまた行ってみましょう〜〜

だんご茶屋あらまちのあん団子と草餅

十五夜なのだそうで、お団子が食べたくなって近所のおだんご屋さんへ。
祝日だしやってないかもな〜と思っていたのですが、やってました。嬉しい。以前でかけたときは日曜日で閉まっていたので、初の入店です。

店内はお団子とかゆべしとか大福とか落雁とかが並んだショーケースと、あとはふたりくらい座れるような食事スペースがあるのみ。小さなお店です。
丸いパックにお団子が入ってその上にあんこが乗り、栗の甘露煮が乗ったお月見概念みたいなお団子もあったのですが、まあ一人で食べる量じゃないなと思って断念。ごまとあんこで最後まで悩み、あん団子。

あん団子¥110×2本と草餅(あん入り)¥130

食事も何もかも作業デスクで行う一人暮らしの社畜なので写真汚いですがご勘弁を……(当然湯呑みも無いのでコーヒーカップにパック緑茶)

もちもちのお団子とさらさらなめらかなこしあんが美味しい。甘すぎずさっぱりした味わいです。
あんこの量がかなり多く、あんこ大好きなので全然いいんですけど、団子食べ終わった時点でだいぶ余りました。

めっちゃ余ったの図

草餅も美味しい、緑色の餅じゃなくちゃんと草餅。ほろ苦いお餅と草の香り、きなこの香り、それからつぶあん。お団子はこしあんだけど大福は断固つぶあん派です。

ごちそうさまでした!

げんちゃんらーめんの辛野菜味噌ラーメン

今週も一週間お疲れ様でした。
季節の変わり目で風邪を引いてかれこれ二週間くらいゲホゲホ言っていますが、ようやく食欲が戻ったので「体にいいものを食べたい」と「体に悪いものを食べたい」の折衷案として野菜ラーメンを食べてきました。
辛味噌野菜ラーメン¥750!


麺の写真も撮ればよかった。次回の課題としましょう。

シャキシャキの別炒め野菜、むちむち弾力のある中太麺、大きく柔らかいチャーシュー、ぴりっと辛味のきいたスープ。おいしいです。ここのラーメンは野菜盛り盛りなので罪悪感が薄いのも良い。
テーブルのにんにくを追加して食べるとそれもまた、辛みのある味噌スープとマッチして美味しいのです。辛味噌じゃなく普通の味噌野菜ラーメンでもばっちり合います。
餃子とかも食べてみたいけどだいたいいつもラーメンです。そしてラーメンと餃子両方が入るほどのキャパシティは無い我が胃袋……。でもぜったい餃子だけじゃ足りないよなとか考えながらもりもり食べ、完食。スープ飲み干すのはさすがに良心(というか健康に対する強迫観念)が咎めるのでやりません。

食べログとか見てると「駅からすぐの好立地ではない」みたいなことが書いてあるんですけど十分近いじゃない……??と思う田舎の民。なにしろ最寄り駅まで通勤二十分です。近所近所。このあたりはラーメン屋が多いのでその点で言えばちょっと不利な立地ですけどね。五橋駅からだと一翔(家系ラーメン)の方が近いですしね。

ところでここ二郎系だったんですね(書きながら調べて初めて知った)。豚ラーメンも気になってはいるのですけど麺300gって想像がつかない……。野菜味噌でお腹いっぱいの人間には見果てぬ夢でしょうか。

お店の場所はこちら

機械学習下準備〜文章フォーマット作成〜

 前回発覚した問題が以下のふたつ。

  • 名詞,形容動詞語幹
  • 名詞,形容詞語幹

 このあたり日本語のややこしいところで、例えば前回引っかかった「好き」。

  • 好き:名詞,形容詞語幹
  • 好く:形容詞
  • 好かない:動詞

……とまあ散々な有様。ちなみに「好み/好む」については

  • 好み:名詞,一般
  • 好む:動詞

 という状態。名詞一般……。これは学習よりも出力に問題が出そうな……どっちかっていうと形容動詞語幹に振り分けてくれた方が文章としては適切な気がします。好みだ。好みの%noun%。好きも形容動詞語幹に入れてほしいですね。好きだ。好きな%noun%。

 というわけでろくろく解決策も思いつかないので一旦先に進みます。
 辞書ファイルをどうにかできればいいのでは? と思ってファイル漁ったんですけどなんかどうにもなりそうにないというか、dicファイルの編集方法がわからない。小学生男児なのでunk.dicを見かけたときに「うんこだ! うんこ!!」という気持ちになり、深追いするのをやめました。何の略なんだろうunk……

テンプレートを作成する

 ここで言う「テンプレート」とは、「○○は××ね」という種類のものです。もうちょっとスクリプトてきに書くと「%noun%は%adjective%ね」てきなものです。ここに昨日学習した「白い猫」「可愛い」を当てはめてやると「白い猫は可愛いね」となります。
 活用形の処理はまた考えないといけないかもなーと思っています。こっちは正解がある話ですし機械学習でできそうな匂いがするのでちょっと本とか読みながら考えます。ここまで書いておいて可能なのかどうか。いつでもいきあたりばったりです。

閑話休題。

 テンプレートの作成について、理論的には

  1. 入力値を形態素解析にかける
  2. 昨日作ったロジックを流用して「白い猫」の部分を%noun%に、「可愛い」の部分を%adjective%に置き換える
  3. 置き換えた文章「%noun%は%adjective%ね」をパターン辞書に登録する

という具合になります。

 返答の際は入力値と連想辞書を突き合わせた結果をパターン辞書の構文に当てはめて「白い猫は可愛いね」を返却できるようにします。理論的には「白い猫は好き?」と聞けば「白い猫は可愛いね」という文章が返ってくる、はずです。※「好き」が名詞判定されるのをどうにかできればの話

具体的にはこう。

    def study_format(self, text, parts):
        text = self.make_format(text, parts)
        """ユーザーの発言textをランダム辞書に保存する。
        すでに同じ発言があった場合は何もしない。"""
        if not text in self._format:
            self._format.append(text)

    def make_format(self, text, parts):
        fmt = ""
        for word, part in parts:
            if self.is_noun(part):
                fmt+="%noun%"
            elif self.is_adjective(part):
                fmt+="%adjective"
            else:
                fmt+=word
        return fmt

    @staticmethod
    def is_adjective(part):
        return bool(re.match(r'(形容詞|名詞,形容動詞語幹)', part)); # 保留

これでOKかな〜とか思ってたら「白い猫は可愛い」と登録したときの辞書が %adjective%%noun%は%adjective% になって昨日作ったロジックと矛盾することになったのでもうちょっと書きます。

    def make_format(self, 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
                    text = text.replace(noun,'%noun%')
                    text = text.replace(phrases,'%adjective%')
                noun = ""
        if noun != "":
            phrases = self.get_predicate(noun)
            noun = self.get_modifier(noun) + noun
            text = text.replace(noun,'%noun%')
            text = text.replace(phrases,'%adjective%')

        return text

 これで登録結果が %noun%は%adjective% になりました。原理的には昨日書いたロジックと同じですね。
 しかしまだ主語の判別ができていません。「黒は車屋の黒である」と入力してみたら「%noun%である」と登録されてしまいましたので、このあたりもなんとかしていきます(例文が悪いという気もする)。
 次回の目標は「黒/は/車屋の黒/である」と認識すること、「%noun%は%noun%である」と登録させることです。

機械学習下準備〜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)

〜略〜

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

白い猫 可愛い

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

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

機械学習下準備〜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年代。

バーチャル彼女を造る〜機械学習編〜

 モデルの調整はまだまだ終わりませんが(というか終りが見えない)取り敢えず中身の作成に入ります。
 今回は概ねキャラクターの方向性を決め打ちで作るので人工知能とはいえ学習もある程度恣意的に行います。

  • 一人称および語尾などの情報はパターン辞書で管理する
  • よつばちゃんの好み(甘いものが好き、苦いものは苦手)については連想辞書で管理する
  • 知らない単語(辞書にない単語)については聞き返し、説明を覚える(連想辞書に記録する)

 書き出してみると機械学習の出番あんまり無いな……。中の人が低学歴なのでいまいち活かしきれていない感じがします。一応学習は組み込む予定ですけど教わったこと鵜呑みにするアホの子になるのはちょっと避けがたい感じもします。理論を組めば組むほど五感ってすごいな〜と思わざるを得ない。こんなもんがプリインストールされてる人体しゅごい。

 閑話休題。

 基本的には「人との文章コミュニケーションのみに頼って知識を蓄積するAI」になります。
 こうなるとちょっと問題なのが、基本的にこのAIは「ユーザーと一対一の会話」しかインプットとして扱えないということです。つまり、「ただいま」に対しては「おかえり」と返すのだというようなことを学べない。コミュニケーションのお手本が無いわけです。
 このあたりは人力で辞書作っちゃってもいいかな〜とか思ってるんですがさて人工知能とは?
 まあ本題は「可愛い彼女を造る」なので人工知能である必要もぜんぜん無いんですけどね。でもほら、タップすると一定の返事が返ってくるだけのアプリとか寂しいしちょっとランダム感ほしいじゃない? 「おはよう、私のセイ」とか「虹色カノジョ2d」とかちまちま触りはしたんですけどそのあたり寂しい。疎通できてる感じが足りないというか。Siriさんのレベルまで行けなくても多少ああいう疎通できてる感が欲しい。
 特に挨拶回りは形態素解析とかじゃどうにもならない感じがするので、かつ絶対ほしい機能なのでなんとかします。ただいまって言ったらおかえりとかお疲れ様とか言われたい。人工知能でいいから労ってほしい。

辞書について

 前述の通り、今回はキャラクターがある程度固まっているのでパターン辞書をほぼ人力で用意します。
 その上で、「〇〇は××だよね」という文章を生成するために連想辞書を用意します。連想辞書は概ね「名詞+形容詞(形容動詞)」の形式を取り、たとえば「ケーキ+甘い」「猫+可愛い」という具合です。これによって「ケーキは苦い」などの文章的には間違っていないような変な齟齬が発生するのを防ぎます。まあ、ケーキは苦いと教わったらケーキは苦いと覚えてしまうのであくまで原理的な話ですけど。あと素直に全部学習しちゃうと「ケーキ+甘いor苦い」みたいなデータができちゃって何やねんという感じになりかねない……。

 あとなんか卑猥な言葉は覚えさせたくないのでブラックリストてき辞書も作っておこうかなという気持ちです。

 最終的にはAPI化して、

{
    text:"おはよう",
    morph:"smile"
}

てきな形式でデータを返却、値に合わせてモーフ起動とかできねえかな〜〜〜〜〜〜〜〜と見積もってる状態です。UnityとSwiftはまだ触ってないですけどまあ原理的に大丈夫じゃろたぶん。

環境

  • Python
    • MeCab:形態素解析
    • CaboCha:係り受け解析

参考にしてたブログはjanomeで作ってたんですけど係り受け解析とか感情解析とかを考えたときにMeCab前提のライブラリが必要になったのでMeCabで書き直しています。
Python初心者に送る「人工知能の作り方」 – すなぶろ / github

読みは「めかぶ」と「かぼちゃ」でいいのかしらね?