Python初心者に送る「人工知能の作り方」 Part7(終)
前回はマルコフ連鎖で学習・応答するモジュールを作りました。それなりに複雑な処理が含まれていましたが、設計時点で仕様を決めておき、それをコメントを含めたコードにすることで、メンテナンス性を保っています。
今回はmarkov.py
をDictionary
クラスに組み込み、新たにMarkovResponder
を追加します。といっても、学習も応答もmarkov.py
内で完結しているのでほとんど変更を加える必要はありません。
その代わりといっては何ですが、辞書ファイルが存在しなかった場合の例外処理を組み込んでみます。今回のソースコードはこちら
例外処理
以前、辞書が空であった場合のコードを追加しました。main.py
に変更を加えたものでしたが、より細かく砕いてDictionary
クラスに実装してみましょう。
今回捕捉する例外はIOError
というもので、ファイルの入出力に失敗したときに投げられる例外です。辞書ファイルは複数あるので、その分だけ例外を捕捉する必要がありますが、警告文を表示するコードは同じなので、util.py
に定義しました。
def format_error(error): """例外errorを受け取り、'名前: メッセージ'の形式で返す""" return '{}: {}'.format(type(error).__name__, str(error))
もしerror
の中身がIOError
オブジェクトで、random.txt
を読み込もうとして失敗した場合は、
FileNotFoundError: [Errno 2] No such file or directory: 'dics/random.txt'
このようなメッセージに整形されます。
これを踏まえて、まずはDictionary
クラスの__init__
メソッドを見ていきます。
from markov import Markov from util import format_error ...中略... class Dictionary: DICT = {'random': 'dics/random.txt', 'pattern': 'dics/pattern.txt', 'template': 'dics/template.txt', 'markov': 'dics/markov.dat', } def __init__(self): """ファイルから辞書の読み込みを行う。""" self._random = Dictionary.load_random(Dictionary.DICT['random']) self._pattern = Dictionary.load_pattern(Dictionary.DICT['pattern']) self._template = Dictionary.load_template(Dictionary.DICT['template']) self._markov = Dictionary.load_markov(Dictionary.DICT['markov']) @staticmethod def load_random(filename): """filenameをランダム辞書として読み込み、リストを返す""" try: with open(filename, encoding='utf-8') as f: return [l for l in f.read().splitlines() if l] except IOError as e: print(format_error(e)) return ['こんにちは'] @staticmethod def load_pattern(filename): """filenameをパターン辞書として読み込み、リストを返す""" try: with open(filename, encoding='utf-8') as f: return [Dictionary.make_pattern(l) for l in f.read().splitlines() if l] except IOError as e: print(format_error(e)) return [] @staticmethod def load_template(filename): """filenameをテンプレート辞書として読み込み、ハッシュを返す""" templates = defaultdict(lambda: []) try: with open(filename, encoding='utf-8') as f: for line in f: count, template = line.strip().split('\t') if count and template: count = int(count) templates[count].append(template) return templates except IOError as e: print(format_error(e)) return templates @staticmethod def load_markov(filename): """Markovオブジェクトを生成し、filenameから読み込みを行う。""" markov = Markov() try: markov.load(filename) except IOError as e: print(format_error(e)) return markov
辞書の読み込みコードが長くなってきたので、それぞれメソッドに分割しました。オブジェクト変数(self._random
など)を__init__
の外側で定義するとPythonから警告が出るので、ロードメソッドは値を返すのみに留めています。
ともあれ、これで辞書ファイルが無くても起動するようになりました。
マルコフ辞書の学習・保存・プロパティ
実質Markov
クラスがほとんどやってくれるので、追加するコードは最小限で済みます。
def study(self, text, parts): """ランダム辞書、パターン辞書、テンプレート辞書をメモリに保存する。""" self.study_random(text) self.study_pattern(text, parts) self.study_template(parts) self.study_markov(parts) # 追加 def study_markov(self, parts): """形態素のリストpartsを受け取り、マルコフ辞書に学習させる。""" self._markov.add_sentence(parts) def save(self): # ...中略... self._markov.save(Dictionary.DICT['markov']) @property def markov(self): """マルコフ辞書""" return self._markov
ファイルの入出力処理に比べると驚くほど短いですね。dill
によるオブジェクト永続化もmarkov.py
がやってくれるので、dictionary.py
からimport dill
する必要もありません。これでDictionary
の定義は完了しました。続いてMarkovResponder
です。
MarkovResponder
Markov
クラスの使い方は、markov.py
で予習済みです。Responderにもほぼそのまま書くだけで動作させることができます。responder.py
に以下を追加しましょう。
class MarkovResponder(Responder): def response(self, _, parts): """形態素のリストpartsからキーワードを選択し、それに基づく文章を生成して返す。 キーワードに該当するものがなかった場合はランダム辞書から返す。""" keyword = next((w for w, p in parts if morph.is_keyword(p)), '') response = self._dictionary.markov.generate(keyword) return response if response else choice(self._dictionary.random)
これでMarkovResponder
は完成です。ユーザーの発言は処理する必要がないので_
を指定しています。Markov
オブジェクトからの応答があればresponse
を返し、何もなければランダム辞書から無作為に選択して返しています。
ジェネレータ
next
にはそろそろ決着をつけましょう。
例えば[w for w, p in parts if morph.is_keyword(p)]
というリスト内包表記があった場合、morph.is_keyword(p)
がTrue
となるw
のリストが生成されます。[w1, w2, w3...]
といった具合ですね。
求めているのは「合致する最初のキーワード」なので、このリストの名前を仮にwords
としたら、words[0]
で得ることもできます。しかしその場合、使われないw2
, w3
といった要素は捨てられます。parts
がものすごく長いリストで、時間をかけてw100
くらいまで計算しても、使わないものは使わないのです。
そこでジェネレータを使います。(w for w, p in parts if morph.is_keyword(p))
というように、[]
ではなく()
で囲まれているのが特徴です。こちらはリスト内包表記とは違い、記述しても即座に計算が行われるわけではありません。「morph.is_keyword(p)
がTrue
となるw
が集まる予定のデータ」を作成するだけです。
あくまでも予定に過ぎないので、実際のデータは明示的に取り出してやらなければなりません。それを行うのがnext
です。next
は引数にジェネレータを受け取り、一度だけ計算してw1
を見つけたら、それを返して再び処理を停止します。
もう一度next
を呼び出せば、今度はw2
を計算して返し、また止まります。そうして一致する要素がなくなったとき、StopIteration
例外を投げて「もう返せるデータがない」ことを示して終わりです。
では最初からmorph.is_keyword(p)
に該当するデータがなかったらどうなるのでしょう。リスト内包表記であれば空リストになるだけですが、ジェネレータの場合はStopIteration
例外が発生します。
これをtry
で捕捉してもいいのですが、next
の第二引数にデフォルト値を指定することで、例外の代わりにその値を返させることができます。今回は''
を指定しているので、keyword
にはmorph.is_keyword(p)
がTrue
となる何らかのw
か、何もなければ空文字列が入ります。
Rubyのfind
やselect
が恋しくなることもありますが、慣れるとこれはこれで柔軟性があって使いやすいですね。
Unmoクラス
材料が整ったので、Unmo
オブジェクトに使ってもらいましょう。unmo.py
を編集します。
# 中略 class Unmo: def __init__(self, name): # 中略 self._responders = { 'what': WhatResponder('What', self._dictionary), 'random': RandomResponder('Random', self._dictionary), 'pattern': PatternResponder('Pattern', self._dictionary), 'template': TemplateResponder('Template', self._dictionary), 'markov': MarkovResponder('Markov', self._dictionary), # 追加 } # 中略 def dialogue(self, text): # 中略 chance = randrange(0, 100) if chance in range(0, 29): self._responder = self._responders['pattern'] elif chance in range(30, 49): self._responder = self._responders['template'] elif chance in range(50, 69): self._responder = self._responders['random'] elif chance in range(70, 89): self._responder = self._responders['markov'] else: self._responder = self._responders['what']
self._markov
を用意してMarkovResponder
を設定し、20%の確率で選択されるようにしました。辞書が大きくなって充分なデータが集まったら、もっと高い確率に変えてもいいかも知れません。
学習の進まないうちはほとんどがランダム辞書からのオウム返しですが、辛抱強く教え込んでいるうちにいろいろな文章を作り始めます。じっくり育ててみてください。
次回の課題
実は未定です。Webサービスにするか、Googleと組み合わせて学習能力を強化するか。それともここで一旦終了しておくべきか。
『恋するプログラム』のWebサービス化については他の方々がやっていらっしゃるので、改めてここで扱う必要もないかなと思います。Googleと組み合わせるアプローチには心惹かれるものがありますが、IRCやTwitter, LINEのbotにしても面白いかもしれません。
というわけで、今回はひとまずここまでということにしておきましょう。Python的に「どうなの」というコードもあると思いますので、ボランティア感覚でsandmark/unmoをforkしていじってもらって構いません。
ではでは、ここまでお読み頂きありがとうございました。快適なAIライフを願っています。