前回、前々回で、QA対話システムを実装する上で必要となるfor文と正規表現を紹介しました。それらを踏まえて今回は、いよいよQA対話システムの実装に入りましょう。

質問回答リスト

まず初めに、QA対話システムの肝となる「質問回答リスト」の作り方を考えます。ユーザー発話とリストの質問をマッチさせたいので、質問は正規表現のパターンを使って書くことにし、その質問パターンと回答のペアをリストの要素として作成することにします。

qa_list = [
    ["富士山.*高さ", "富士山の高さは3,776mです。"],
    ["東京.*区.*いくつ", "東京都には23の区があります。"]
]

質問パターンと回答のペアはリストで表現しているので、リストの中にリストが入っている構造になっていることに注意してください。今まで紹介したリストの要素は文字列でしたが、リストでは、このように要素としてリストを含めることもできます。

質問パターンについて少し詳しく見てみましょう。「富士山.*高さ」は、「富士山」と「高さ」が順に出現する文字列にマッチするパターンです。つまり、「富士山の高さって?」や「あの、富士山の高さについて教えてください。」のような文字列にマッチします。同様に、「”東京.*区.*いくつ”」は、「東京」「区」「いくつ」が順に出現する文字列にマッチするパターンとなります。

なお、「qa_list」は以下のように1行で書いても構いません。

qa_list = [["富士山.*高さ", "富士山の高さは3,776mです。"], ["東京.*区.*いくつ", "東京都には23の区があります。"]]

しかし、リストは改行とインデントを入れて記述することができます。読みやすく記述したほうがミスも少なくなるので、今回示すサンプルのように、適宜改行やインデントを入れながらプログラムを書くようにしましょう。

QA対話システムの実装

さて、質問回答リストの作り方を決めたので、次はプログラムの全体像を考えてみましょう。このプログラムでは、ユーザー発話をwhile文の繰り返しで取得し、「ありがとう」を含む場合はあいさつをしてプログラムを終了し、そうでない場合は質問回答リストを検索して回答します。全体は次のような構成になります。

import re

qa_list = [
    ["富士山.*高さ", "富士山の高さは3,776mです。"],
    ["東京.*区.*いくつ", "東京都には23の区があります。"]
]

# while 文で繰り返しユーザー発話を取得する
while True:
    # ユーザー発話を取得
    text = input("> ")

    # ユーザー発話が「ありがとう」を含む場合はあいさつをして終了する
    if re.search("ありがとう", text):
        print("ありがとうございました。また質問してくださいね!")
        break
    else:
        質問回答リストを検索して回答

ユーザー発話が「ありがとう」を含むかどうか判定するのに、「ありがとう」というパターンを使ってマッチしていることに注意してください。

後は、「質問回答リストを検索して回答」する部分を実装すれば完成です。質問回答リストを検索するには、qa_listの要素をfor文で繰り返し処理し、ユーザー発話が質問パターンにマッチするか順次確認します。

それでは、これを「qa.py」というスクリプト名で実装してみましょう。

# qa.py - QA対話システム
import re

qa_list = [
    ["富士山.*高さ", "富士山の高さは3,776mです。"],
    ["東京.*区.*いくつ", "東京都には23の区があります。"]
]

# while 文で繰り返しユーザー発話を取得する
while True:
    # ユーザー発話を取得
    text = input("> ")

    # ユーザー発話が「ありがとう」を含む場合は挨拶をして終了する
    if re.search("ありがとう", text):
        print("ありがとうございました。また質問してくださいね!")
        break
    else:
        # 回答が見つかったか記録する変数
        found = False

        # 質問回答リストの要素を繰り返し処理
        for qa in qa_list:
            # 要素から質問パターンと回答をインデックス指定して取得
            pattern = qa[0]  # QAの質問パターン
            answer = qa[1]  # QAの回答
            if re.search(pattern, text):
                # ユーザー発話に近い質問が見つかった場合の処理
                # 回答を表示
                print(answer)
                # 回答が見つかったことを変数に記録
                found = True
                # break で for の繰り返しを終了する
                break

        # found をチェックし、回答が見つからなかった場合は「すみません、わかりません。」と返答する
        if not found:
            print("すみません、わかりません。")

プログラムの動作を理解するために、「東京の区っていくつあるの?」というユーザー発話が入力されたケースを想定して動きを追ってみましょう。

プログラムはリストqa_listを定義した後、while文で繰り返し処理に入ります。

while文の先頭では、「text = input(“> “)」でユーザー発話が入力されるのを待機しています。そこへ「東京の区っていくつあるの?」が入力されると、これを変数textに束縛します。

その後、if文でユーザー発話が「ありがとう」を含むかを判定します。今回は「ありがとう」を含まないので、else以下を実行します。

else以下では、冒頭で「found = False」と、変数foundを定義している点に注目してください。変数foundは回答が見つかったかどうかを記録するブール値を値に持つ変数で、「True」の場合は回答が見つかったことを、「False」の場合は回答が見つかっていないことを表します。初めはまだ回答が見つかっていないため、変数foundにFalseを束縛します。

続いて、for文で質問回答リストqa_listの要素である質問パターンと回答のペアを変数qaに束縛して繰り返し処理を開始します。1回目の繰り返し処理では、変数qaには「”富士山.*高さ”, “富士山の高さは3,776mです。”」が束縛されることになります。

次に、質問パターンを変数patternに束縛します。質問パターンはリストqaの1番目の要素なので、「qa[0]」とインデックスを指定して取り出します。続いて、同じ要領で「qa[1]」から回答を取り出して変数answerに束縛します。結果、変数pattern には「”富士山.*高さ”」が、変数answerには「”富士山の高さは3,776mです。”」が束縛されることになります。

質問パターンと回答を変数に束縛したら、if文「if re.search(pattern, text):」により、変数textに束縛されたユーザー発話(入力)が、変数patternに束縛された質問パターンにマッチしているか確認します。ユーザー発話「”東京の区っていくつあるの?”」は、質問パターン「”富士山.*高さ”」にマッチしないので、続くインデントされたif文の処理は実行されません。

これで、for文の1回目の処理は完了です。

for文の繰り返しの対象であるリストqa_listには2つの要素が入っているので、for文は終了せずに2回目の繰り返し処理に入ります。for文は、リストqa_listの2番目の要素「”東京.区.いくつ”, “東京都には23の区があります。”」を変数qaに束縛して処理を開始します。今回は、ユーザー発話「”東京の区っていくつあるの?”」が質問パターン「”東京.*区.*いくつ”」にマッチするので、if文の処理が実行されることになります。

if文の中では、初めに「print(answer)」で変数answerに束縛された回答を表示します。変数answerには、「qa[1]」が入っているはずです。そして、これはfor文の2回目の処理なので、変数qaにはリストqa_listの2番目の要素が入っています。したがって、「print(answer)」では、「”東京都には23の区があります。”」が表示されます。

回答が見つかったので、プログラムの冒頭で定義した変数foundに回答が見つかったことを表すブール値「True」を代入します。また、これ以上質問回答リストを検索する必要がないため、「break」でfor文を終了します。

for文を終了すると、if文「if not found:」に到達します。ここでの処理を詳しく見てみましょう。

まずブール値を値に持つ変数foundの前に「not」が付いていることに注目してください。notをブール値の前に付けると、そのブール値を逆転させます。つまり、「not True」は「False」を、「not False」は「True」を表します。したがって、リストで回答が見つかった場合、変数foundの値はTrueのため「not found」はFalseとなり、if文に続く処理は実行されません。

もし回答が見つからなかった場合、not foundはTrueとなるため、続くif文の処理が実行されて「すみません、わかりません。」と表示することになるわけです。こうした用途のために、冒頭でこのfoundという変数を定義したのでした。

ここまで処理が終わるとwhile文の処理が1回完了したことになります。プログラムは、再びwhile文の先頭に戻り「text = input(“> “)」でユーザー発話の入力を待ち受けます。

動作の解説は以上です。それでは実際に実行してみましょう。

$ python qa.py
> ねぇ、富士山の高さは?
富士山の高さは3,776mです。
> 東京の区っていくつあるの?
東京都には23の区があります。
> 今日の天気は?
すみません、わかりません。
> ありがとう!
ありがとうございました。また質問してくださいね!

期待通りの動作をしていますね!

* * *

今回まで3回にわたり、for文と正規表現を理解しながらQA対話システムを実装しました。新しい機能によって、より実用的な対話システムの作成が可能になってきたことを実感できたのではないでしょうか。

多種多様なユーザー発話にマッチする質問パターンを記述することは非常に難しいものです。スマートスピーカーをはじめとする対話システムと話したことがある方は、期待する回答を返してくれないと感じたことも多いかと思います。その原因の1つが、ここにあるわけです。今回、QA対話システムを自分自身で実装することで、その難しさの片鱗を理解いただけたのであれば幸いです。

今回のQA対話システムは、質問応答リストの要素を増やせばより実用的なシステムとなります。ぜひ、リストを増強して自分だけのQA対話システムを作成してみてください。

著者紹介


株式会社NTTドコモ
R&Dイノベーション本部 サービスイノベーション部
阿部憲幸

2015年京都大学大学院理学研究科数学・数理解析専攻修了。 同年、NECに入社。 2016年から国立研究開発法人情報通信研究機構出向。 2018年より現職。 自然言語処理、特に対話システムの研究開発に従事。 毎日話したくなるAIを夢見て日夜コーディングに励む。
GitHub:https://github.com/noriyukipy