Python初心者に送る「人工知能の作り方」 Part2
前回はAIを作るにあたって、全体の骨組みとインターフェイスを作ってみました。結果として「○○ってなに?」としか発言しない、とてもAIとは呼べないものになっていますが、今回はもう少し反応を改善してみます。
前回のおさらい:
Responder
クラス- 入力にもとづいて応答を返す思考エンジン。
Unmo
クラス- 人工無脳コア。
- 思考エンジン
Responder
を持ち、ユーザーと思考エンジンを接続する。
- フロントエンド
- とりあえずコンソールアプリケーション。
- ユーザーからの入力を受け取り、
Unmo
オブジェクトへ渡し、応答結果を表示する。
しかし改善するといっても、どうやって行えばいいのでしょうか。また、Responder
, Unmo
, インターフェイスがひとつのmain.py
に記述されているのもいただけません。ひとつひとつ見ていきましょう。
今回のソースはsandmark/unmoに置いてあります。
ファイルを分割する
「応答のバリエーションを増やす」ということは、「Responder
を増やす」ということに他なりません。つまりResponder
クラスの定義が長くなるということなので、ひとつのファイル(main.py
)にいろいろなメソッドや関数が増えていくと管理が大変です。
そんなわけで、Responder
, Unmo
, インターフェイスの3つのファイルに分割しましょう。まずは短くなったmain.py
からご覧ください。
from unmo import Unmo def build_prompt(unmo): """AIインスタンスを取り、AIとResponderの名前を整形して返す""" return '{name}:{responder}> '.format(name=unmo.name, responder=unmo.responder_name) if __name__ == '__main__': print('Unmo System prototype : proto') proto = Unmo('proto') while True: text = input('> ') if not text: break response = proto.dialogue(text) print('{prompt}{response}'.format(prompt=build_prompt(proto), response=response))
import
が追加され、Unmo
とResponder
の記述がなくなりました。
from unmo import Unmo
というのは、「unmo.py
からUnmo
という名前をロードする」という意味です。インターフェイスであるmain.py
からResponder
を直接操作する必要はないので、人工無脳コアクラスのロードに留めています。
この一文がないと
Traceback (most recent call last):
File "main.py", line 12, in <module>
proto = Unmo('proto')
NameError: name 'Unmo' is not defined
というエラーが表示されます。「Unmo
という定義されていない名前を使おうとしている」という内容なので、正しいエラーです。ここで知っておいて欲しいのが、「エラーが出たから一巻の終わりだ」という考え方は間違いだ、ということです。
本当に怖ろしいのは「間違っているのにエラーが出ない。その結果バグに気づかず、動作がおかしいまま起動してしまうこと」です。おかしいコードに対しておかしいと言ってくれるエラーという存在は、むしろ親のように大切にすべきものです。
話を戻して、次は新しく作ったunmo.py
の内容です。
from responder import Responder class Unmo: """人工無脳コアクラス。 プロパティ: name -- 人工無脳コアの名前 responder_name -- 現在の応答クラスの名前 """ def __init__(self, name): """文字列を受け取り、コアインスタンスの名前に設定する。 ’What' Responderインスタンスを作成し、保持する。 """ self._name = name self._responder = Responder('What') def dialogue(self, text): """ユーザーからの入力を受け取り、Responderに処理させた結果を返す。""" return self._responder.response(text) @property def name(self): """人工無脳インスタンスの名前""" return self._name @property def responder_name(self): """保持しているResponderの名前""" return self._responder.name
こちらも違いはわずかです。main.py
と違って、Unmo
クラスはResponder
クラスを扱わなければなりません。そこで、from responder import Responder
としてロードしています。
最後にresponder.py
を見てみましょう。
class Responder: """AIの応答を制御するクラス。 プロパティ: name -- Responderオブジェクトの名前 """ def __init__(self, name): """文字列を受け取り、自身のnameに設定する。""" self._name = name def response(self, text): """ユーザーからの入力(text)を受け取り、AIの応答を生成して返す。""" return '{}ってなに?'.format(text) @property def name(self): """応答オブジェクトの名前""" return self._name
このファイルからは何もインポートしていません。Responder
クラスは他の機能を使わないので、何もロードする必要はありません。
こうしてファイルを分割した状態でも、python main.py
で以前と同じ動作をすることを確認してください。
Responderの拡張
無事ファイルを分割できたところで、Responderを集中して拡張する準備ができました。
「○○ってなに?」と返すResponderだけではなく、ランダムな文字列から選んで返すものを追加してみましょう。responder.py
を見てみます。
from random import choice class Responder: """AIの応答を制御する思考エンジンクラス。 プロパティ: name -- Responderオブジェクトの名前 """ def __init__(self, name): """文字列を受け取り、自身のnameに設定する。""" self._name = name def response(self, text): """ユーザーからの入力(text)を受け取り、AIの応答を生成して返す。""" return '{}ってなに?'.format(text) @property def name(self): """思考エンジンの名前""" return self._name class RandomResponder: """AIの応答を制御する思考エンジンクラス。 登録された文字列からランダムなものを返す。 クラス変数: RESPONSES -- 応答する文字列のリスト プロパティ: name -- RandomResponderオブジェクトの名前 """ RESPONSES = ['今日はさむいね', 'チョコたべたい', 'きのう10円ひろった'] def __init__(self, name): """文字列を受け取り、自身のnameに設定する。""" self._name = name def response(self, _): """ユーザーからの入力は受け取るが、使用せずにランダムな応答を返す。""" return choice(RandomResponder.RESPONSES) @property def name(self): """思考エンジンの名前""" return self._name
新たにRandomResponder
が追加されました。見ていきましょう。
RandomResponder.RESPONSES
には、3つの文字列を含むリストが定義されています。response
メソッドを呼び出したときに、このうちのどれかがランダムに選択されて返ってくるわけです。
「リストからひとつをランダムに選択する」という機能は、Pythonにデフォルトで用意されています。random
モジュールに含まれるchoice
関数で、ファイルの先頭でインポートし、response
メソッドの中で使っています。
ユーザーの入力は一切無視する形になるので、response
メソッドの引数は「使用しない」ことを明示する_
になっています。Unmo
クラスは必ずresponse(text)
の形式で呼び出すので、引数は受け取らなければなりません。ただ使用しないだけです。
動作確認
Pythonシェルを起動します。
$ python
>>> from responder import RandomResponder
>>> r = RandomResponder('random')
>>> r.response('こんにちは、トム。')
'今日はさむいね'
>>> r.response('これはトムですか?')
'今日はさむいね'
>>> r.response('いいえ、それはトムではありません。')
'チョコたべたい'
>>> r.response('これは何ですか?')
'今日はさむいね'
>>> r.response('それは花瓶です。')
'きのう10円ひろった'
もはや会話の体をなしていません。こちらの言うことにある程度の興味を示してくれるResponder
のほうが、まだコミュニケーションのしがいがあったというものです。
同じコードをまとめる
それにしても、__init__
やname
メソッドは変わり映えしませんね。必要だから定義せざるを得ないのですが、仮に「name
メソッドはself._name
ではなく'Hey!'
という文字列にしたい」という意味不明で抗いがたい欲求に苛まれたときに困ります。
Responder
とRandomResponder
に定義されているname
メソッドを両方書き換えなければなりませんし、これからもResponderは増えていく予定ですから、同じコードが増えるばかりでメンテナンスが大変です。
そこで、共通の機能はResponder
にまとめてしまうことにします。「○○ってなに?」と返す思考エンジンには新たにWhatResponder
という名前を与えましょう。
新生responder.py
はこちらです。
from random import choice class Responder: """AIの応答を制御する思考エンジンの基底クラス。 継承して使わなければならない。 メソッド: response(str) -- ユーザーの入力strを受け取り、思考結果を返す プロパティ: name -- Responderオブジェクトの名前 """ def __init__(self, name): """文字列を受け取り、自身のnameに設定する。""" self._name = name def response(self, *args): """文字列を受け取り、思考した結果を返す""" pass @property def name(self): """思考エンジンの名前""" return self._name class WhatResponder(Responder): """AIの応答を制御する思考エンジンクラス。 入力に対して疑問形で聞き返す。""" def response(self, text): """文字列textを受け取り、'{text}ってなに?'という形式で返す。""" return '{}ってなに?'.format(text) class RandomResponder(Responder): """AIの応答を制御する思考エンジンクラス。 登録された文字列からランダムなものを返す。 クラス変数: RESPONSES -- 応答する文字列のリスト """ RESPONSES = ['今日はさむいね', 'チョコたべたい', 'きのう10円ひろった'] def response(self, _): """ユーザーからの入力は受け取るが、使用せずにランダムな応答を返す。""" return choice(RandomResponder.RESPONSES)
ぐっと短くなりました。WhatResponder
とRandomResponder
の間で同じ処理をしていた__init__
とname
メソッドはResponder
クラスにまとめられ、それぞれのクラスに同じコードを書かなくても済みます。これでコピペしたりする必要もありませんし、バグの発生確率もかなり減りました(専門用語でDRYとか『差分プログラミング』といいます)。
まとめられた共通のコードは、class RandomResponder(Responder)
のようにResponder
を指定することで組み込むことができます。これを継承と呼び、今回の場合は「Responder
クラスをWhatResponder
とRandomResponder
クラスが継承している」または「WhatResponder
およびRandomResponder
は、親クラスとしてResponder
を持つ」と表現します。
__init__
とname
メソッドは継承により省略できますが、response
メソッドはそうも行きません。というより、子クラスで独自に定義してもらうものなので、親クラスであるResponder
のresponse
メソッドの定義が不可能なのです。
再掲しましょう。
def response(self, *args): """文字列を受け取り、思考した結果を返す""" pass
今回は*args
引数とpass
文を使っています。
*args
はいわば「引数はなんでもいい」ことを表すキーワードで、WhatResponder
やRandomResponder
では引数が使われたり無視されたりするため、一定の形を持たないことを宣言しています(これが正しいのかどうか私には判断がつかないのでPythonistaからの意見募集中です)。
pass
文は「何もしない」ことを宣言するキーワードです。Responder
は継承されることが前提の骨組みのようなクラスなので、実際に応答を考える必要はありません。かといってメソッド定義に何も書かないわけにはいかないので、pass
文を使って明示的に「何もしない」と宣言しています。
Unmoクラスに反映する
では早速、RandomResponder
を使うようにUnmo
クラスを変更しましょう。unmo.py
はこんな感じになります。
from responder import RandomResponder class Unmo: """人工無脳コアクラス。 プロパティ: name -- 人工無脳コアの名前 responder_name -- 現在の応答クラスの名前 """ def __init__(self, name): """文字列を受け取り、コアインスタンスの名前に設定する。 ’Random' Responderインスタンスを作成し、保持する。 """ self._name = name self._responder = RandomResponder('Random') def dialogue(self, text): """ユーザーからの入力を受け取り、Responderに処理させた結果を返す。""" return self._responder.response(text) @property def name(self): """人工無脳インスタンスの名前""" return self._name @property def responder_name(self): """保持しているResponderの名前""" return self._responder.name
といってもちょっとしか変わっていません。インポートするものがRandomResponder
になり、self._responder
の中身がRandomResponder('Random')
に変わっただけです。
あれだけresponder.py
の中身をいじったのにunmo.py
の変更がこれだけで済むというのは大きな利点です。ファイルを分割したこともそうですが、「RandomResponder
はあくまでもResponder
の一種である」という保証があるため、Responder
を扱う側(Unmo
)のコードの変更は最小限で済みます。これがオブジェクト指向プログラミングを行うメリットのひとつであると言えます。
オブジェクト指向プログラミングでなくとも、こういった『構造化』はデザインテクニックとして様々な言語で用いられています。Haskellであれば型システムを使って、Lispであればジェネリック関数を使って、C言語においては構造体を使うことで実現しています。オブジェクト指向プログラミングが必ずしも「ベストな答え」を提供するわけではなく、「ベターを目指すための手法のひとつ」に過ぎないことを忘れないでください。そうすれば、オブジェクト指向言語でなくともクリーンなコードを書くことができます。
動かしてみる
せっかく書き直したWhatResponder
が使われていませんが、それは後の課題として、とりあえず動かしてみましょう。今回はどんなチグハグな会話になるのか楽しみです。
……会話になっているのでしょうか。そして今回のテーマは『改善』だったにも関わらず、こちらがかなり歩み寄らなければならない状態になっています。こんな調子で大丈夫でしょうか……。
次回の課題
複数のResponderを定義することに成功したので、今度はこれらをプログラムの実行中に動的に切り替えてみたいと思います。コンパイルの必要がないとはいえ、いちいちソースを編集していたのでは手間がかかります。プログラミングの真髄は自動化なのです。
「どういうときにどのResponderを使うか」を判断して……というのは難しいので後回しにしましょう。現時点のResponder達はそんな高尚なものを扱っても期待に応えてくれるとは思えません。
また、ユーザーの入力を学習してファイルに保存する機能も実装します。『学習』というワードが入ると一気にAIっぽくなりますね。ただしこれまでの経験上、過度な期待はしないほうが良さそうですが。予想以上に記事が長くなったので次々回に持ち越しです。ごめんなさい。