2022-07-29
前の章では傾聴の技法のうち簡易なものを選び、シンプルな構成の傾聴チャットボットを作ってみました。 今回はこれをさらに進め、傾聴のいち技法である要約ができるチャットボットを考えます。要約は以下のように話し手の言葉の中で要点になる部分を抽出して話し手に返すことです。
ユーザ: 今日は隣の街のモールに行って、本屋でマンガを買ってきたよ
チャットボット:マンガを?
「話し手の言葉を一部抽出して再利用する」という方法はElizaでも利用されています。その辞書を観察してみましょう。
Elizaの辞書では、会話機能の中核部分はキーkey
、分解decomp
と再構成reasmb
からなっています。
key: remember 5
decomp: * i remember *
reasmb: Do you often think of (2) ?
reasmb: Does thinking of (2) bring anything else to mind ?
reasmb: What else do you recollect ?
reasmb: Why do you remember (2) just now ?
reasmb: What in the present situation reminds you of (2) ?
reasmb: What is the connection between me and (2) ?
reasmb: What else does (2) remind you of ?
この辞書ではユーザの入力にkey
の"remeber"
が含まれていた場合、decomp
のパターンに従って "i remember"
より前の部分を(1)に、後の部分を(2)に代入します。
続いてreasmb
のうちどれか一つを選んで(1)や(2)を当てはめて返答とします。
例えばユーザが「At the moment I remember his face」と言ってきたら、(1)=At the moment、(2)=his faceという抽出を行い、「Do you often think of his face」という返答を生成します。
これを参考に日本語で一部の単語を抽出するエンコーダーを考えます。 まずはElizaを直訳してみます。
in: ["* を思い出しました"],
out: ["(1)を今思い出されたのはどうしてですか?"]
この例では「そういえば昔飼っていた犬のことを思い出しました」に対して「そういえば昔飼っていた犬のことを今思い出されたのはどうしてですか?」が返事になってしまい、やや不快感を与える場合があるかもしれません。 こちらの意図としては「犬のことを今思い出したのはどうしてですか?」くらいのボリュームで良いのです。 英語であってもこのやり取りでは少ししつこいように思いますが、日本語の場合は共通認識になっている情報は省略することが望ましいため、一層違和感を覚える返答になってしまいました。 これを踏まえると日本語の場合は*の部分は一文節程度の抽出が良さそうです。そこで、ベクトル化の方法としてbag-of-wordsの代わりにbag-of-文節(phrases)を利用したテキスト検索を行います。
これから検討する「文節への分割」を行うクラスを PhraseSegmenterと呼ぶことにします。同様の文節区切りによりテキスト検索の性能を向上させようという取り組みは様々試みられています1 2。
また文節のかかり受けを解析する構文解析ツールもSpaCyなど多数存在しますので、それを利用できる環境があれば利用したほうがよいでしょう。 ですが、その前に考えるべきなのはその解析方法は対象としている文字列に適しているのかという点です。 通常の自然言語解析ツールは教科書的な「お行儀の良い」日本語を前提にしていることが多く、チャットで用いられるような「んなわけない」「ちょま」「尊み」といった短縮、省略、スラングを苦手としています。 また「友達を紹介した」「友達のことを紹介した」のように構文的には別でも意味はほぼ同じ言い回しなどもあります。それらは便利なツールを使っていると意外と気が付きにくいものです。 そこでまずは簡易な解析プログラムを自作して実際のテキストがどのような姿をしているか観察してみることも面白いのではないかと思います。
それではエンコーダーの前半部分である分割部に注目して、文節に区切るのと形態素に区切るのとの違いを見ていきます。まず文をUniDic-MeCabで形態素に区切ると
米田 | さん | は | 図書館 | に | 傘 | を | 持っ | て | でかけ | た
となります。bag-of-wordsの考え方では語順は無視されるので、語順をランダムに入れ替えた
は | 図書館 | さん | 米田 | を | 持っ | でかけ | た | て | に | 傘
は日本語的には全く意味が変わってしまいましたが、ベクトル的には元の文と同じとみなされます。一方文節に区切ってシャッフルしてみると
米田さんは | 図書館に | 急いで | でかけた
図書館に | 急いで | 米田さんは | でかけた
となり、「米田さんは」「図書館に」「急いで」の3つは入れ替えても文の意味がほとんど変わりません。 このように入れ替えが可能な文節がある一方、順番が決まっている文節もあります。 最後の動詞「でかけた」は位置を変えるとニュアンスが変わりますが、形態素の場合のような支離滅裂さと比べれば意味はかなり保存されているようです。
図書館に|でかけた|米田さんは|急いで
でかけた|急いで|米田さんは|図書館に
次に「〜の」という所有や修飾を表す文節はかかり受けの結びつきが強く、順番を入れ替えると意味が大きく変わる傾向があります。
隣の|町の|モールに|出かける
街の|隣の|モールに|出かける
分節化することで順序を入れ替えても意味が壊れにくくなることは確かだと思われますが、実際はどうなのでしょう。 Bag-of-文節の性能を数字で比較するには、異なるトピックについて語られたチャットのログを集め、Bag-of-形態素とBag-of-文節から機械学習モデルを作ってトピック分類を行い、その性能を比較する…というような方法ができるでしょう。 そのあたりはチャットのログを集めることを含めて今後の宿題にします。
性能確認はひとまず置いて、今回文字列をベクトル化する目的は類似度の計算で、対象としているのはチャットで使われる短めで砕けた日本語です。 それを踏まえた場合、述語や修飾を表す文節も含めて融通を効かせても良いかもしれません。
少し遠回りしましたが、つまり日本語の場合は語順に融通が効くとよく言われていますが、もう少し細かく言うと
文節の順序は融通がきく
のです。このような特徴と順序を考慮しないbag-of-wordsは非常に相性が良いでしょう。これがbag-of-phrases(文節)の着眼点です。 さて、それぞれの文節の多くは助詞つまり「てにをは」で終わっています。
書字形 | 品詞 |
---|---|
米田 | 名詞-固有名詞-人名-姓 |
さん | 接尾辞-名詞的-一般 |
は | 助詞-係助詞 |
図書 | 名詞-普通名詞-一般 |
館 | 接尾辞-名詞的-一般 |
に | 助詞-格助詞 |
急い | 動詞-一般 |
で | 助詞-格助詞 |
でかけ | 動詞-一般 |
た | 助動詞 |
それを目印にして、一旦形態素解析された文字列を文節に分けなおします。 そのため形態素解析で得られた品詞情報を使えば精度は高いですが、単純化のため、またブラウザ上でも動かせるようにするためTinySengementerによる分かち書きを利用し、書字形だけで判断することにします。 なお、典型的な文節の分類は主語・述語・修飾語・接続語・独立語の5種類ですが、これは機能の分類であって、あまり意味に立ち入っていません。 テキスト解析ではもう少し意味のわかる分類が必要なので、教科書的な分類にはあまりこだわらず独自の分類をしてしまいます。その一部はこのようになります。
表層 | 出力 |
---|---|
X が | X\t主語 |
X さん が | X\t主者 |
X は | X\t主語 |
X さん は | X\t主者 |
X を | X\t対象語 |
X さん を | X\t対象者 |
X の こと を | X\t対象語 |
X さん の こと を | X\t対象者 |
X に | X\t目的語 |
X さん に | X\t目的者 |
X の | X\t修飾語 |
X さん の | X\t修飾者 |
X な | X\t修飾語 |
X だ | X\t修飾語 |
X で | X\t述語 |
X する | X\t述語 |
X し た | X\t述語 |
X による | X\t手段 |
しかし | しかし |
さらに上記の表で、「Xさんが」は「Xちゃんが」「X先生が」などでもOKです。 入力文字列は処理の最初で分かち書きし、形態素のリストになっています。 このリストを調べて表の「表層」に一致する部分があればそれを一つにまとめて「出力」に置き換えます。 Xには助詞以外のどのような形態素が来てもOKです。したがって、
藤野先生が新宿の本屋にいる
は
藤野先生\t主者 新宿\t修飾語 本屋\t目的語 い る
と変換します。ここまでで説明した分節化処理をBNF記法で表すと以下のようになります。
main ::= indep* '*'+ (indep* '*'+)* person_suffix? (subj|obj|dest|mod|by|verb) 'accept()'
subj ::= ('が'|'は'|'と' ) 'subj()'
obj ::= ('を' 'obj()') | ('の' 'mod()' ('こと' ('を' 'obj()'|subj|dest|by) )? )
dest ::= ('に' 'は'?|'まで') 'dest()'
mod ::= ('な'|'だ'|'ね') 'mod()'
by ::= 'で|により|による' 'by()'
verb ::= ('する' | 'し' 'た') 'verb()'
person_suffix::= 'さん'|'君'|'ちゃん'|'先生'
indep::='しかし'|'なので'|'それで'|'、|。|?|!'
これはRailroal Diagram Generatorにコピー&ペーストすればグラフ化できますので、ぜひ確認してみてください。 このうちメインルーチンに相当するmainはFig.1のようになります。
このダイアグラムはプッシュダウン・オートマトンに書き換えることができます。それを実装したデモを以下に示します。
PhraseSegmenter Demo
bag-of-文節エンコーダーの後半部分、内部コード化について考えます。 bag-of-Wordsエンコーダーでは内部コードとして
{
index: 最も類似度が高かった辞書の行番号,
score: 類似度
}
を与えました。今回はこれに抽出結果を追加して
{
index: 最も類似度が高かった辞書の行番号,
score: 類似度,
harvests: 抽出単語のリスト
}
とします。ここまでで述べてきたPhraseSegmenterにより、任意の入力文字列をとりあえず文節に区切ることができるようになりました。 冒頭で見た例を分節化すると以下のようになります。
*
を思い出しました →*
対象語 | 思い出し | まし | た
これを用いて辞書の入力文字列を文節化し、はBag-of-wordsを使ってベクトル化してそれを学習します。 次にユーザから与えられた入力文字列も同じPheaseSegmenterで処理して
そういえば、飼っていた犬のことを思い出しました →
そういえ | ば | 、 | 飼っていた犬 対象語 | 思い出し | まし | た
のようにします。辞書から似た行を探す際には「飼っていた犬 対象語」を「*
対象語」に置き換えて類似度計算をします。
類似度の高い行が見つかった場合、ユーザ入力文字列のどれを「*
文節」に割り当てるかを決めます。
それには以下のルールを順番に適用し、成功したらそれを結果として返し、失敗したら順次次を試す・・・とします。
*
文節」が辞書の該当行やユーザ入力文字列に見つからなければ harvests が空の内部コードを返します。*
文節と同じ種類の文節をすべて抽出し、内部コードのharvestsとして返します。*
文節のなかからランダムに一つを選び、内部コードのharvestsとして返します。抽出した文節を含んだ内部コードが得られたら、それを使って出力文字列を生成します。
harvestエンコーダーとデコーダーを用いるとき、辞書は以下のようになります。
in:[
"*が好きだ"
],
out:[
"*がお気に入りなんだね"
]
inの「*
が」は主語を示す文節として認識され、に当たる文節が記憶されます。
続いてoutで同じく主語を示す「`が」や「
*`は」があれば先程記憶した文節が再利用されます。
(以下製作中)