Python初心者に送る「人工知能の作り方」
昨今、機械学習やディープラーニングといったキーワードで注目を集めているPython。簡潔な文法から初心者向けの学習目的にも採用されています。
とはいえ、入門書を読んだだけではよくわからないという人も多いはず。「何をどうやって作ればいいの?」「文法はわかったけど書き方がわからない」という疑問は初心者にありがちで、「英語の文法はわかっても読めない・書けない」のと似ています。解決方法はズバリ、人と会話したり、手を動かしてみることです。
この記事ではPython初心者、ひいてはプログラミング初心者に「チャットbotを作る」という目標を設定してもらって、具体的な作り方や設計方法を見てもらいます。Microsoftのりんなほど高度なものではありませんが、プログラムとチャットする楽しさを感じながら、プログラミングテクニックを身に着けてもらえればと思います(今回扱うAIは機械学習とは無関係です)。
初めて'Hello, World!'
を出力させたときの感動をもう一度味わってください。
今回の目標は「とりあえずプログラムと会話する」です。最初はバカバカしいくらい退屈ですが、徐々に成長させていくのでゆっくりやっていきましょう。
まえがき
恋するプログラム―Rubyでつくる人工無脳 (プレミアムブックス版)
- 作者: 秋山智俊
- 出版社/メーカー: マイナビ出版
- 発売日: 2016/11/28
- メディア: 単行本
- この商品を含むブログを見る
個人的なPython勉強メモでもあり、同時にPython初心者がこの記事を参考にしてくれれば幸いです。
また、書いたコードは随時GitHubに反映させていきます。全体像を把握したい場合は、いつでもsandmark/unmoを参照してください。
プロジェクトの準備
まずはプロジェクトを作成して、作業する場所を確保します。といっても、ディレクトリを作るだけなので大変なことはありません。
今回はホームディレクトリ(~
)にrepos/unmo/
というディレクトリを作って作業することにします。
cd ~/
mkdir -p repos/unmo/
cd repos/unmo/
この場所にmain.py
というファイルを作成します。別に'main'という名前じゃなくても構わないのですが、それは後々考えることにしましょう。
好きなエディタでmain.py
を開いたら、いよいよプログラミング開始です。
プログラムと会話する
チャットボットであるからには、人間と会話できなければなりません。手始めに「会話する機能」を実装してみましょう。
注意点として、ここで言う『会話』というのは必ずしも『意思疎通できる』という意味ではありません。ユーザーが何か入力すると、その返事が返ってくるというだけの、オウム返しのようなものです。
オウム返しであっても無いよりはマシですし、今後の改善に期待しましょう。main.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シェルを開きます。
D:\Users\sandmark\repos\unmo>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 17:54:52) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
ここで1 + 1
などのPythonコードを実行することができますが、そういった解説は入門書に任せておきましょう。早速main.py
をインポートしてみます。
>>> from main import Responder
エラーが出なければ正常にインポートできています。これは「main.pyからResponderクラスをロード」という意味で、これでPythonシェルからResponderの動作チェックができるようになりました。
>>> oumu = Responder('Oumu-Gaeshi') # 新たにResponderオブジェクトを作る
>>> oumu.name # Responderオブジェクトの名前を確認する
'Oumu-Gaeshi'
>>> oumu.response('こんにちは') # 会話を試みる
'こんにちはってなに?'
非常にぎこちないですが、会話らしきものが成立しているような気がします。
Responderクラスの詳細
3つのメソッド__init__
, response
, name
が定義されています。
__init__
は「新たにオブジェクトを作るときに自動的に呼ばれるもの」なので、基本的に使う側が明示的に呼び出す必要はありません。Responder('Oumu-Gaeshi')
のタイミングで呼び出されます。
def __init__(self, name): self._name = name
self._name
にResponderの名前を格納していますが、なぜself.name
ではいけないのでしょうか。答えは「外部からの変更を防ぐため」です。もしself.name
にしてしまうとどうなるか。以下の例を考えてみます。
>>> oumu = Responder('Oumu-Gaeshi')
>>> oumu.name
'Oumu-Gaeshi'
>>> oumu.name = 'Karasu' # オブジェクトの名前を変えてみる
>>> oumu.name
'Karasu'
エラーも出ずにオブジェクトの名前が変わってしまいました。オウム返ししかできない応答オブジェクトなのに、頭の良いことで有名なカラスになってしまっては困ります。これを防ぐために、self._name
という場所にして、「変更しちゃいけないよ」という意味を込めて保持しているのです。
関連してname
メソッドを見てみます。
@property def name(self): return self._name
これは単純にself._name
の値を返すだけのメソッドですが、定義の前に@property
というデコレータがついています。name
は本来メソッドなので、呼び出すときはoumu.name()
のように括弧を付けなければなりません。しかし「self._name
の中身を知りたいだけ」なので、いちいち括弧を付けるのは煩わしいです。そこで@property
を付けておけば、括弧を省略できるというわけです。
最後にresponse
メソッドを見てみましょう。
def response(self, text): return '{}ってなに?'.format(text)
「○○ってなに?」という形式にして返しているだけです。文字列の中に変数を埋め込みたいので、ここではformat
関数を使っています。{}
の部分がtext
の値に置き換わるわけです。
この辺りは大いに改善の余地がありますが、今はこのままにしておきましょう。
もっと使いやすく
AIと会話するのにPythonシェルを開いて、変数を作ったりメソッドを呼び出したりするのはナンセンスです。そこで、ユーザーとの便宜を図るためのインターフェイスを作ってみます。
Responder
はあくまでも「AIの応答を考えるクラス」なので、新たにもっとユーザーに近いクラスを作ります。main.py
に以下を追加してください。
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
コードよりもコメントのほうが長いですが、この後さらにコードを追加して改良していくことを考えると、スタート地点としては妥当です。
Unmo
クラスはまさに「人工知能」を表すクラスです。チャットボットの俗称として『人工無脳』という言葉があるのですが、MunoのアナグラムとしてUnmoになっています。漢字で書くと雲母で、宝石の一種であり、きららというかわいい別名もあります。
Pythonシェルで実行してみましょう。
>>> ai = Unmo('prototype')
>>> ai.name
'prototype'
>>> ai.responder_name
'What'
>>> ai.dialogue('こんにちは')
'こんにちはってなに?'
応答オブジェクトが同じなので結果は変わりませんが、Responder
の作成と保持を自動でやってくれるようになりました。つまり、この後どれだけResponder
を追加しようと管理はUnmo
クラスがやってくれるので、インターフェイス部分の設計に集中できるのです。
Unmoクラスの詳細
__init__
メソッドは、文字列を受け取って自身に名前を付けるとともに、'What'
という名前のResponder
オブジェクトをself._responder
に保持しています。
def __init__(self, name): self._name = name self._responder = Responder('What')
将来的には他にも複数の応答オブジェクトを作り、オウム返しだけではなく様々なアプローチで返答させたいので、人工知能クラス(Unmo
)が応答オブジェクト(Responder
)を持つのは理にかなっています。
dialogue
メソッドは:
def dialogue(self, text): return self._responder.response(text)
現状Responder
オブジェクトのresponse
メソッドを呼び出しているだけです。人工知能と思考エンジンが接続されているという認識でいい感じです。
name
メソッドは:
@property def name(self): return self._name
Responder
クラスと同じく、自分の名前を返すプロパティメソッドです。
responder_name
も同様に:
@property def responder_name(self): return self._responder.name
保持しているResponder
オブジェクトの名前を返します。
昔ながらのコマンドラインアプリケーション
さて、パーツは揃いました。インターフェイスを作って、もっとチャットがスムーズにできるようにしましょう。
とはいえ、いきなりGUIに挑戦するのは無駄にハードルが高いですし、オウム返しするアプリケーションがグラフィカルである必要があるでしょうか。ないと思います。
そこで、こんな感じのインターフェイスにしてみたいと思います。
延々と「○○ってなに?」と聞かれるとアイデンティティが崩壊しそうになりますが、もう少しの我慢です。インターフェイス部分のコードをmain.py
に追加しましょう。
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))
build_prompt
関数はAIの名前を整形して返すヘルパーです。今回のようにUnmo
の名前が'proto'で、内部のResponder
が'What'なら、proto:What>
という表現に変換してくれます。
ここでもformat
関数を使っていますが、{name}
にはunmo.name
, {responder}
にはunmo.responder_name
を指定しています。
if __name__ == '__main__'
という条件式は一見意味がわかりませんが、これはスクリプトとして実行されたときに真になります。つまりこのブロックにあるコードは、
$ python main.py
としたときには実行されますが、
$ python
>>> import main
としたときには実行されません。場合によってはmain.py
の動作確認のためにPythonシェルからクラスを呼び出したいときもあるでしょう。そんなときにwhile True
なんてループが始まったら動作確認も何もないので、それを防止するための仕組みです。
スクリプトとして起動すると、
Unmo System prototype : proto
>
このような画面になります。今回作成するAIの名前は'proto'にしました。変数proto
にAIオブジェクトを代入して、そのあとwhile True
ループが始まります。
input()
は「ユーザーからの文字列入力を待つ」関数で、引数はプロンプト文字列、戻り値は入力された文字列です。
直後のif
で、text
が空だった場合にループを抜けるようになっています。「○○ってなに?」と聞かれる無限地獄から脱出したい場合は、何も入力せずにEnterキーを叩きましょう。沈黙は金です。
何かしらの入力があった場合は、AIからの返事をresponse
に代入して表示します。ここでもformat()
関数が登場していますが、build_prompt()
関数のときと使い方は一緒ですね。
次の課題
「○○ってなに?」しか返ってこないのではあんまりなので、新しくResponder
を追加してみましょう。いくつかの応答をあらかじめ用意しておき、その中からランダムに返すというものです。構造的欠陥が解消されるわけではありませんが、それでも今よりはマシになるでしょう。
Notice: ここまでのコードはsandmark/unmoに保存されています。