会話の「間」を持たせる「おうむ返し」の実装
この記事に書くこと
- 「おうむ返し」ライブラリの概要
- 実装のポイント
モチベーション
現在、「今日のできごと」について話ができる音声対話システムについて
社会人大学院で研究しています。
そのうえで、研究の一環として、
音声対話を実装する際の「間」をつなぐ仕組みを作りたいと思っていました。
あと単純に、自然言語処理について深く知りたかったので、
いろいろと手段を調べながら「おうむ返し」を実装しました。
「おうむ返し」ライブラリの概要
pythonライブラリを実装しました。
ライブラリを利用するだけで手軽に「おうむ返し」を実現できることを目指しました。
ここでの「おうむ返し」は、「相手の言葉を使って返答文を生成する」ことを意味します。
どういった手順で利用するのかをざっと書いていきます。
インストール
まずpipコマンドでライブラリをインストールします。
文生成にはspaCyという自然言語処理ライブラリの日本語モデルであるja_ginza
を使用します。
$ pip install dialog-reflection==0.1.2
$ pip install ja_ginza==5.1.2
spaCyとja_ginza
の説明は省略します。
以下の記事がわかりやすかったので興味があれば参照ください。
使ってみる
デフォルト設定がライブラリに組み込まれているため、
pythonからライブラリをロードするだけで「おうむ返し」が実現できます。
以下の例では、対話内容から文字を抽出し、
「〜んですね」と語尾をつけて返答文を生成します。
from dialog_reflection.lang.ja.reflector import JaSpacyReflector
refactor = JaSpacyReflector(model="ja_ginza")
message = "今日は旅行へ行った"
reflection_text = refactor.reflect(message)
print(reflection_text)
# => 旅行へ行ったんですね。
Builderからの利用
他処理をspaCyで行ってから返答文を生成するケースを想定して、
Builderクラスから返答文を生成することも可能としています。
from dialog_reflection.lang.ja.reflection_text_builder import (
JaSpacyPlainReflectionTextBuilder,
)
import spacy
nlp = spacy.load("ja_ginza")
builder = JaSpacyPlainReflectionTextBuilder()
message = "今日は旅行へ行った"
doc = nlp(message)
# some code...
reflection_text = builder.build(doc)
print(reflection_text)
# => 旅行へ行ったんですね。
カスタマイズしてみる
ライブラリのデフォルト設定だけでなく、カスタマイズも可能にしました。
語尾の調整
op
を変更することで語尾を調整することが可能になります。
以下の例では、語尾を「〜んだね」に変更しています。
from dialog_reflection.lang.ja.reflector import JaSpacyReflector
from dialog_reflection.lang.ja.reflection_text_builder import (
JaSpacyPlainReflectionTextBuilder,
)
from dialog_reflection.lang.ja.reflection_text_builder_option import (
JaSpacyPlainReflectionTextBuilderOption,
)
refactor = JaSpacyReflector(
model="ja_ginza",
builder=JaSpacyPlainReflectionTextBuilder(
op=JaSpacyPlainRelflectionTextBuilderOption(
fn_last_token_taigen=lambda token: token.text + "なんだね。",
fn_last_token_yougen=lambda token: token.lemma_ + "んだね。",
)
),
)
message = "今日は旅行へ行った"
reflection_text = refactor.reflect(message)
print(reflection_text)
# => 旅行へ行ったんだね。
その他設定項目は reflection_text_builder_option.py を参照ください。
ロジックのカスタマイズ
JaSpacyPlainReflectionTextBuilder
を override することで
ロジックをカスタマイズ可能することも可能です。
以下の例では、文字列がPROPN(=固有名詞)である場合に単語を抽出しています。
加えて、単語がcompound(=結合されている)場合にその単語の依存関係まで抽出して返答文を生成しています。
from dialog_reflection.reflection_cancelled import (
ReflectionCancelled,
)
from dialog_reflection.cancelled_reason import (
NoValidSentence,
)
from dialog_reflection.lang.ja.reflector import JaSpacyReflector
from dialog_reflection.lang.ja.reflection_text_builder import (
JaSpacyPlainReflectionTextBuilder,
)
import spacy
class CustomReflectionTextBuilder(JaSpacyPlainReflectionTextBuilder):
def extract_tokens(self, doc: spacy.tokens.Doc) -> spacy.tokens.Span:
propn_token = next(filter(lambda token: token.pos_ == "PROPN", doc), None)
if propn_token is None:
raise ReflectionCancelled(reason=NoValidSentence(doc=doc))
if propn_token.dep_ in ["compound", "numpound"]:
return doc[propn_token.i : propn_token.head.i + 1]
return doc[propn_token.i : propn_token.i + 1]
refactor = JaSpacyReflector(
model="ja_ginza",
builder=CustomReflectionTextBuilder(),
)
message = "今日は田中さんと旅行へ行った"
reflection_text = refactor.reflect(message)
print(reflection_text)
# => 田中さんなんですね。
こういった実装はspaCyが準拠するUniversal Dependenciesを基に構築しています。
以下の記事が詳しいので、興味があれば参照ください。
また、デフォルト実装である JaSpacyPlainReflectionTextBuilder
は、
インターフェース IReflectionTextBuilder を継承しています。
なので、インターフェースを継承していちから実装することも可能です。
(もしいい実装ができたらPRくださいm(__)m)
実装のポイント
実装で意識したポイントをスライドにまとめています。
詳しく知りたい方は参照ください。
実装開始当初は「ただ相手の言葉を返すだけだから」と侮っていましたが、
結果、スライドが60枚近くあるように、かなり大変な作業でした。。
この記事では、スライドからいくつかポイントを抜き出して解説します。
Universal Dependenciesの依存関係抽出は難易度が高い
概要説明の繰り返しになりますが、
文生成にはUniversal Dependenciesという仕様に準拠した
spaCyという自然言語処理ライブラリを利用しています。
このライブラリによって単語間の依存関係が抽出できるのですが、
どの依存関係を抽出して文章にすべきかは工夫が必要です。
Universal Dependenciesの困難
よって、デフォルト実装としては
「出来る限り単語の連なりを分解しない」方式で実装しました。
Token抽出のPOINT
口語特有の問題の考慮
口語にはいくつか特殊なケースがあり、
代表的なケースはspaCyで対応可能なのですが、吸収しきれないケースがあります。
口語として考慮すべき点::言い換え
ただこれは音声対話においては音声認識モデルとの兼ね合いも考えられるため、
今回の実装では特に対処はしていません。
文整形には恣意的な判断が必要
デフォルト実装の「出来る限り単語の連なりを分解しない」方式では、文末の文字が多いケースでは返答文が冗長となってしまいます。
そのため、「端的な」「会話破綻のない」文末調整を実装しています。
文末調整::手法の設定
しかし、単語を切断する「特定の条件」を決めるには単語を全部見ていくしかなく、「sudachiのコア辞書を参照して単語を列挙→意味を国語辞典から類推→日常会話コーパスから統計情報を確認→省いても日常会話に支障がないかを確認→...」と、ひとつひとつ単語を見ていく必要がありました。
最後の単語の判断基準::助詞の条件
文法と品詞で制御できないケースがある
単語のなかには、意味が特定できないケースが存在します。
か[終助]文末にある種々の語に付く。
https://kotobank.jp/word/か-456254
の[終助](上昇調のイントネーションを伴って)質問または疑問の意を表す。
https://kotobank.jp/word/の-596099
そういった意味が不確定な文字列を文末に含む場合、
文生成自体をCANCELとする必要がありました。
最後の単語の判断基準::助詞の条件
語尾が変わると会話破綻の許容範囲も変わる
デフォルト実装では「〜んですね」に合う品詞や単語を選んでいきましたが、
「〜って感じね」といった言葉だと「〜んですね」よりも許容範囲が広がります。
(若者言葉ってすごいですね)
ちなみに...
特定の言葉にすればもう少し条件を緩められますが、
今回は汎用性(=語尾を柔軟に設定できること)を重視するためにキツめの条件を実装しました。
また、返答文が短縮されるため、
「〜って感じね」であっても文末調整の効果は得られます。
文末調整なし:忘れちゃったんだよね - > 忘れちゃったんだよねって感じね
文末調整あり:忘れちゃったんだよね - > 忘れちゃったって感じね
話者の立場の考慮
文法ルールや語尾だけでなく、
敬語など、話者の立場の考慮したルールも考慮する必要があります。
今回の実装では「です」「ます」の除外のみを実装しました。
立場の変換::敬語
また、この「です」「ます」を除外するため、別ライブラリを実装しているので、
興味がある方はこちらも参照ください。
その他
その他、形態素解析器との噛み合わせ(e.g., 固有名詞が正しく分かち書きされない、意味的には1語だが2語に分かち書きされる助詞「のに」)など諸々ありますが、詳しくはスライドを参照ください。
雑感
話し相手の言葉を使って「おうむ返し」を実現するには、
かなり複雑な考慮が必要でした。
ただどういった点で問題が起こりうるのかを把握できたことで
今後の音声対話システム開発に活かせる知見が得られたかなと思っています。
ゼミの先生からは「京都大学の対話システムでは発話に含まれる単語のうち希少性の高い単語を抽出して質問で返す」という手法があることを教わり、たしかにスマートな方法だな、、と思った次第です。
(出典は見つけられずでした)
また、この実装を始めたときに以下の出版が発表されました。
ひととおり作業が終わったので、これからじっくり読もうと思っています。
最後に
このライブラリを活用していただけるなら幸いです。
もし問題があればissueをお願いします。もし可能ならPRをお願いしますm(__)m
それでは。
参考
spaCy
Universal Dependencies
日常会話コーパス
話し言葉コーパス
係り受け
『日本語話し言葉コーパス』における係り受け構造付与
『日本語日常会話コーパス』に対する自然会話特有の現象を区別するための係り受け関係ラベルの付与
Discussion