すなぶろ

Pythonistaを目指しているつもりが、Hyのおかげで無事脱線。Clojurian目指すことになりました。

Python初心者に送る「人工知能の作り方」

f:id:sandmark:20171007025609j:plain
プログラムと会話(?)しよう!

昨今、機械学習ディープラーニングといったキーワードで注目を集めているPython。簡潔な文法から初心者向けの学習目的にも採用されています。

とはいえ、入門書を読んだだけではよくわからないという人も多いはず。「何をどうやって作ればいいの?」「文法はわかったけど書き方がわからない」という疑問は初心者にありがちで、「英語の文法はわかっても読めない・書けない」のと似ています。解決方法はズバリ、人と会話したり、手を動かしてみることです。

この記事ではPython初心者、ひいてはプログラミング初心者に「チャットbotを作る」という目標を設定してもらって、具体的な作り方や設計方法を見てもらいます。Microsoftりんなほど高度なものではありませんが、プログラムとチャットする楽しさを感じながら、プログラミングテクニックを身に着けてもらえればと思います(今回扱うAIは機械学習とは無関係です)。

初めて'Hello, World!'を出力させたときの感動をもう一度味わってください。

今回の目標は「とりあえずプログラムと会話する」です。最初はバカバカしいくらい退屈ですが、徐々に成長させていくのでゆっくりやっていきましょう。


まえがき

今回のテーマは、Ruby初心者向けの書籍『恋するプログラム』をPythonに書き直し、さらに噛み砕いて説明してみようという試みです。

個人的な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に挑戦するのは無駄にハードルが高いですし、オウム返しするアプリケーションがグラフィカルである必要があるでしょうか。ないと思います。

そこで、こんな感じのインターフェイスにしてみたいと思います。

f:id:sandmark:20171007025609j:plain
プロトタイプによるゲシュタルト崩壊

延々と「○○ってなに?」と聞かれるとアイデンティティが崩壊しそうになりますが、もう少しの我慢です。インターフェイス部分のコードを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に保存されています。