前回は、QA対話システムを作成するにあたって必要となる「for文による繰り返し処理」について説明しました。続く今回は、同じくQA対話システムを作成する上で必要となる「正規表現」について解説します。

「正規表現」で、より柔軟な文字列比較を実現する

今回作成するQA対話システムでは、用意した質問回答リストからユーザー発話に近い質問を見つける処理が必要となります。

ここで、これまでに本連載で作成してきた対話システムにおいて、ユーザー発話の内容で分岐する処理を思い出してみましょう。例えば、10回で実装した「くじびき対話システム」では、以下のように「ユーザー発話」と「特定の文字列」を比較し、その結果に応じて分岐処理をしていました。

utterance = input("ユーザー発話> ")
if utterance == "くじびき":
    ...

この処理ではユーザーが「くじびき」とだけ話しかけた(入力した)場合にはくじびきを行えますが、「くじびきをしたいです」や「あの、くじびきをお願いします」のように「くじびき」以外の言葉を伴っていた場合、くじびきを行うことができません。そこで、より柔軟にユーザー発話の内容と比較する手段が必要になるわけです。

今回紹介する正規表現では、「特殊文字」を使い、決まった書式にのっとって「パターン」を記述することで「文字列がそのパターンにマッチするかどうか」という比較ができます。その結果として、通常の文字列同士の比較だけでは実現できないような、より柔軟な比較が可能となるわけです。

したがって、正規表現ではパターンの書き方が重要となります。しかし、それほど身構える必要はありません。簡単なパターンであれば、書き方もすぐに理解できるはずなので、いくつか紹介したいと思います。

パターンの書き方

通常の文字を使ったパターンは、それ自身と同じ文字(列)にマッチします。例えば「対話システム」と記述したパターンは、「対話システム」という文字列にマッチするといった具合です。

それでは、実際にPythonで記述してみましょう。

まず、Pythonで正規表現を使うには、「reモジュール」を使います。reモジュールに定義されているsearch関数は、パターンが文字列の一部分にマッチするかどうかをチェックする関数です。1つ目の引数にパターンを、2つ目の引数に文字列を渡して、パターンが文字列の一部分にマッチするかどうかを確認します。そして、search関数の戻り値をif文で判定し、続くインデントされた行でマッチした場合の処理を、else以下でマッチしなかった場合の処理を記述します。

# re_str.py - 文字列パターンのプログラム
import re

pattern = "対話システム"
text = "対話システム"

# 文字列 text が、パターン pattern にマッチするか判定する
if re.search(pattern, text):
    print("マッチしました")
else:
    print("マッチしませんでした")

このプログラムを「re_str.py」というスクリプト名で保存/実行してみてください。すると、パターン(pattern)「対話システム」は文字列(text)「対話システム」に含まれている(全部にマッチする)ので、if文に続くインデントされた行が実行され「マッチしました」と表示されます。

$ python re_str.py
マッチしました

なお、上述したように、search関数は「パターンが文字列の一部分にマッチするどうか」をチェックするものなので、例えばパターン(pattern)「対話システム」は、文字列(text)「これは対話システムです」にもマッチすることに注意してください。

では、今度はパターンに特殊文字を使ってみましょう。特殊文字にはさまざまな種類があるのですが、例えば、「.(ドット)」は、「あ」や「い」といった1文字から成る文字列にマッチします。この.を通常の文字列と組み合わせて作った「対話.システム」というパターンは、「対話」と「システム」の間に何か1文字挟んだ「対話のシステム」や「対話とシステム」といった文字列にマッチします。

これを確かめるプログラムを「re_dot.py」というスクリプト名で書いてみましょう。

# re_dot.py - dotパターンのプログラム
import re

pattern = "対話.システム"
texts = ["対話のシステム", "対話システム"]

# for文で変数textに順にマッチするか確認する文字列を束縛する
for text in texts:
    if re.search(pattern, text):
        print(pattern, text, "マッチしました")
    else:
        print(pattern, text, "マッチしませんでした")

このコードでは、前回紹介したfor文を使って、変数texts中の文字列を順に変数textに束縛し、正規表現を使ったパターン(pattern)にマッチするかどうかを判定しています。実行すると、「対話のシステム」にはマッチし、「対話システム」にはマッチしないことが確認できます。

$ python re_dot.py
対話.システム 対話のシステム マッチしました
対話.システム 対話システム マッチしませんでした

それでは、「対話システム」「対話のシステム」「対話できるシステム」のように、「対話」と「システム」の間に文字列が入っていたり、入っていなかったりするもの全てにマッチするパターンはどうやって書けばよいのでしょうか。

そこで登場するのが繰り返しを表す特殊文字「*(アスタリスク)」です。*は、直前の文字の0回以上の繰り返しを表します。

「0回以上」という表現が少しわかりにくいかもしれません。例えば、「あい*」というパターンは「あい」や「あいい」、そして「あ」(*の直前の文字「い」がない状態)とマッチします。この「*の直前の文字がない状態」が0回の繰り返しに相当します。

さて、この*による繰り返しを.と共に使って作ったパターン「対話.*システム」を考えてみましょう。.は任意の1文字を表し、*は直前の文字の0回以上の繰り返しを表すので、「.*」は任意の文字列を表すことになります。これならば、「対話システム」「対話のシステム」「対話できるシステム」の全ての文字列とマッチできるはずです。

それでは、先程作成したre_dot.pyのパターンとマッチする文字列のリストを変更して実行してみましょう。次のプログラムを「re_ast.py」というスクリプト名で保存してください。

# re_ast.py - asteriskパターンのプログラム
import re

pattern = "対話.*システム"
texts = ["対話システム", "対話のシステム", "対話できるシステム"]

# for 文で text 変数に順にマッチするか確認する文字列を束縛する
for text in texts:
    if re.search(pattern, text):
        print(pattern, text, "マッチしました")
    else:
        print(pattern, text, "マッチしませんでした")

実行すると、期待通りの動作をしていることが確かめられます。

$ python re_ast.py
対話.*システム 対話システム マッチしました
対話.*システム 対話のシステム マッチしました
対話.*システム 対話できるシステム マッチしました

前回、今回と2回に渡り、QA対話システムを実装する上で必要となるfor文と正規表現について紹介しました。次回はいよいよ、QA対話システムを実装しましょう!

著者紹介


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

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