最近の更新 (Recent Changes)

2014-01-01
2013-01-04
2012-12-22
2012-12-15
2012-12-09

Wikiガイド(Guide)

サイドバー (Side Bar)

--
← 前のページに戻る

会話キャラクター・システムの動作解説

会話キャラクターのプログラム・ソースを解説します。

例題として、「 会話キャラクター 猫耳メイドバージョン」のソースを使いましょう。

ソースパッケージの中の「example/Eliza/jp-utf8/Neko-jp」を対象とします。

1. ライブラリのインクルード

まず、必要なライブラリをインクルードします。

ここでは、リスト処理を行う組み込みライブラリのlistと、組み合わせや順列を処理する組み込み述語のgeneratorと、 形態素解析を行うktaisoをインクルードします。 ライブラリktaisoは、本プログラムNeko-jpと同じディレクトリに入れてあります。


/*
 * 会話キャラクター
 *      猫耳メイド萌え萌えバージョン V2.0
 *                      2010 (C) Copyright H.Niwa
 *
 */

? <include list>;
? <include generator>;
? <include ktaiso>;

2. 人称の置き換え

この「会話キャラクター・システム」では、入力された文の一部を流用して回答メッセージを作ります。 しかし、入力される文には、「私」や「あなた」のような人称を表す単語が含まれているかもしれません。 これをそのまま流用すると「あなたは~です」のような文をそのまま回答としてしまいます。

このような場合に備えて入力された文では、「私」に相当する語は「あなた」に変換します。 「あなた」に相当する語は「私」に変換します。

この処理をReplace述語として実装します。


Replace(置き換え後リスト 入力リスト)

形態素解析されて、入力文を単語ごとに区切られた「入力リスト」をインプットとし、 そのなかの人称に関する単語を置き換えて、「置き換え後リスト」にアウトプットします。

ソースを次に示しましょう。


// 置き換え
<Replace () ()>;
<Replace (あなた :#b2) (私 :#b1)>       <Replace #b2 #b1>;
<Replace (あなた :#b2) (わたし :#b1)>   <Replace #b2 #b1>;
<Replace (あなた :#b2) (あたし :#b1)>   <Replace #b2 #b1>;
<Replace (あなた :#b2) (俺 :#b1)>       <Replace #b2 #b1>;
<Replace (あなた :#b2) (おれ :#b1)>     <Replace #b2 #b1>;
<Replace (あなた :#b2) (オレ :#b1)>     <Replace #b2 #b1>;
<Replace (私 :#b2) (あなた :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (アナタ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (おまえ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (オマエ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (お前 :#b1)>         <Replace #b2 #b1>;
<Replace (私 :#b2) (きさま :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (キサマ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (貴方 :#b1)>         <Replace #b2 #b1>;
<Replace (私 :#b2) (貴女 :#b1)>         <Replace #b2 #b1>;
<Replace (#f : #b2) (#f : #b1)>         <Replace #b2 #b1>;

Replaceプログラムは、再帰的に処理していきます。

入力リストの最初の要素を取り出して、それが人称を表す単語であった場合に、 置き換え後リストの最初の要素に置き換えて、残りの入力リストをReplace述語を 再帰的に呼び出すことによって処理していきます。 入力リストが空リスト()になると処理が終了します。

上記リストを見るとわかると思いますが、右にある「私」 の単語を、左のほうで「あなた」に変換しているのがわかると思います。

人称については、変換後は「あなた」または「私」に統一して変換します。 これにより、後のプログラムでは、人称については「あなた」または「私」だけを対象として処理すればよくなります。

変換の種類を増やすには、単純に上記ソースに追加すれば可能です。

2.1 空リストの処理

Replace述語の処理は、一番最初の行と最後の行だけ特殊です。

まず、最初の行の処理を見てみましょう。


<Replace () ()>;

これは、<Replace 変数 ()>と、パターンマッチすると変数に空リスト()を設定する処理です。

Replace述語は、再帰的にリストの頭から一つずつ単語を除いた処理を行うので、この処理は最後に必ず行われることになります。

2.2 人称パターン以外の単語が先頭になかった場合の処理

次に最後の行の処理についてみてみましょう。

人称パターン以外の単語が先頭にあった場合の処理です。


<Replace (#f : #b2) (#f : #b1)>                 <Replace #b2 #b1>;

これは、これより上の処理がパターンマッチしなかった場合に呼ばれる処理です。 上には、入力である第2引数が空リストか、あるいは、人称を表す単語にパターンマッチする処理が並んでいます。 つまり、それ以外の場合の処理を行います。

まず左の処理である<Replace (#f : #b2) (#f : #b1)> の第2引数を見てみましょう。

(#f : #b1)となっています。 これは、入力された文章のリストの最初の要素を#fに設定し、残りのリストを#b1に設定するパターンマッチの処理です。

例えば、入力リストが (デカルト言語 は 便利です) の場合、#fには、「デカルト言語」が設定され、#b1には (は 便利です) が設定されます。

次に右の処理を見てみましょう。 左の処理で設定した#b1を入力として、<Replace #b2 #b1>を呼び出しています。#b2には、この処理を呼び出した結果が入って返ってきます。

そして、もう一度左の処理の第1引数を見てみましょう。

この第一引数は、結果を返すためにあるのですが、先ほど取り出した先頭の単語#fと、右の処理で返された#b2を結合しています。

つまり、この処理はまとめると、先頭の単語以外のリストに人称を示す単語があるか調べて、その結果を先頭の単語と結合して返すということになります。

2.3 人称パターンの単語が先頭にあった場合の処理

次に、対象の処理である人称パターンの単語が先頭にあった場合の処理について説明しましょう。

この処理は回答するメッセージに、入力された文をアレンジして引用する場合に、1人称と2人称を変換しておく必要があるためです。

いくつかの処理が登録されていますが、その中の一つを取り出して解説します。


<Replace (あなた :#b2) (私 :#b1)>       <Replace #b2 #b1>;

「2.2 人称パターン以外の単語が先頭にあった場合の処理」で示した処理とよく似ています。 前項と比較して参照すると判りやすいでしょう。

まず左の処理である<Replace (あなた :#b2) (私 :#b1)> の第2引数を見てみましょう。 これが入力される文のリストに相当します。

(私 :#b1)> となっています。 これは、入力された文章のリストの最初の要素が"私"とパターンマッチした場合に、その文の残りをリストを#b1に設定するパターンマッチの処理です。

例えば、入力リストが (私 は 人間です) の場合、#b1には (は 人間です) が設定されます。

次に右の処理を見てみましょう。 左の処理で設定した#b1を入力として、<Replace #b2 #b1>を呼び出しています。#b2には、この処理を呼び出した結果が入って返ってきます。

そして、もう一度左の処理の第1引数を見てみましょう。

この第一引数は、結果を返すためにあるのですが、"私"を"あなた"に変換した値と、右の処理で返された#b2を結合しています。

つまり、この処理はまとめると、先頭の単語以外のリストに"私"があるか調べて、その結果を"あなた"と結合して返すということになります。

この処理により、入力文 (私 は 人間です) は、(あなた は 人間です) に変換されます。

加えてこのような変換の種類を増やす応用例として、例えば"あたい"を"あなた"に変換する処理を追加したい場合は、 次のように追加行を最初と最後の行の間に追加するだけで良いのです。


// 置き換え
<Replace () ()>;
<Replace (あなた :#b2) (私 :#b1)>       <Replace #b2 #b1>;
<Replace (あなた :#b2) (わたし :#b1)>   <Replace #b2 #b1>;
<Replace (あなた :#b2) (あたし :#b1)>   <Replace #b2 #b1>;
<Replace (あなた :#b2) (俺 :#b1)>       <Replace #b2 #b1>;
<Replace (あなた :#b2) (おれ :#b1)>     <Replace #b2 #b1>;
<Replace (あなた :#b2) (オレ :#b1)>     <Replace #b2 #b1>;
<Replace (あなた :#b2) (あたい :#b1)>    <Replace #b2 #b1>;  //ここに追加した
<Replace (私 :#b2) (あなた :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (アナタ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (おまえ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (オマエ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (お前 :#b1)>         <Replace #b2 #b1>;
<Replace (私 :#b2) (きさま :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (キサマ :#b1)>       <Replace #b2 #b1>;
<Replace (私 :#b2) (貴方 :#b1)>         <Replace #b2 #b1>;
<Replace (私 :#b2) (貴女 :#b1)>         <Replace #b2 #b1>;
<Replace (#f : #b2) (#f : #b1)>         <Replace #b2 #b1>;


ここでは、人称については、変換後は「あなた」または「私」に統一して変換します。 これにより、後のプログラムでは、人称については「あなた」または「私」だけを対象として処理すればよくなり後の処理が簡単になります。

変換後の「あなた」または「私」の人称については、キャラクターによって特徴あるものにすると面白いものになりますよ。

例えば忍者キャラクターならば「おのれ」と「それがし」とかするとか。 キャラ付けのポイントです。

3. メイン処理

メイン処理は、このプログラム全体の処理を制御しています。 そこで、この処理については、より詳しくみていきましょう。


// メイン処理
<Eliza>
        <print "おかえりなさいませ、ご主人様">
        <print "何か話してくださいませんかにゃ?">
        <print>
        {
                <printf "> ">
                ::sys <getline #x
                        ::sys <strdelcntl #x1 #x>
                        ::sys <split #x2 #x1 "。??">
//                      [<noteq #x2 ()> <print>]
                        <foreach (#x3 #x2)
                            (
                              ::sys <strlen #slen #x3>
                              <compare #slen <= 1>
                             |
                              ::sys <syntax #x3
                                (
                                  ::ktaiso <文 #a #b>
                                  <Replace #a2 #a>
                                  <is #n (#a2 #b)>
                                |
                                  <is #n (#x2)>
                                )
                                ::list <flatten #list #n>
                                <c #list2 #list>
                                ::list <flatten #list3 (#list2)>
                                ::sys <concat #s #list3>
                                <print "#" #s>
                            >
                            )
                        >
//                      [<noteq #x2 ()> <print>]
                >
        };


3.1 初期メッセージによる挨拶

まず、最初に、print文で挨拶を表示します。このメッセージは起動時に一度だけ表示されます。


おかえりなさいませ、ご主人様
何か話してくださいませんかにゃ?

>

改造するときには、キャラクターにふさわしいメッセージを表示するようにするとよいでしょう。

3.2 ループ

そして、その処理の後は、{}で囲まれています。デカルト言語では、{}はループ処理を表します。 囲まれた処理は繰り返し実行されます。

3.3 メッセージの読み込み

ループ処理の最初では、プロンプトである"> "を表示しています。

そして組み込み述語であるsysモジュールのgetline述語を使いメッセージを読み込みます。

読み込んだ1行は、第2引数に設定されます。

3.4 入力メッセージの前処理


                           ::sys <strdelcntl #x1 #x>
                        ::sys <split #x2 #x1 "。??">

まず、組み込み述語であるsysモジュールのstrdelcntl述語を使い、入力メッセージに含まれている改行コードを削除します。 改行コード以外にも文字以外の制御コードが入力メッセージが含まれていても削除します。

次に組み込み述語split述語を使います。split述語は第3引数にある文字を区切り文字として、入力行を切り分けます。 split述語の第3引数にある "。??" のいずれかの文字があれば、入力行を分けたリストとします。

これは、入力行が複数の文であった場合に切り分ける意図です。


  "これが良いです。でも、そちらも良いです。迷いますね。"

                    ↓

  ("これが良いです"  "でも、そちらも良いです"  "迷いますね")

上の例では、入力行を3つの文字列の文に分けています。区切りに使われた "。" は消えます。

3.5 一つずつの文の処理

まず、for述語で、区切り文字で分けられた文を一つずつ取り出します。

そして、取り出した文があまりに短い(1文字)場合には、特に反応せずに終わります。 その処理を以下で行います。 入力された一行から取り出された一つの文は、ここまでの処理により変数#x3に入っています。


                            (
                              ::sys <strlen #slen #x3>
                              <compare #slen <= 1>
                             |

論理和"|"が付いているので、入力された文字列の長さが1文字より大きい場合には、"|"の下の処理を実行します。


                                  ::sys <syntax #x3
                                (
                                  ::ktaiso <文 #a #b>
                                  <Replace #a2 #a>
                                  <is #n (#a2 #b)>
                                |
                                  <is #n (#x2)>
                                )
                                ::list <flatten #list #n>
                                <c #list2 #list>
                                ::list <flatten #list3 (#list2)>
                                ::sys <concat #s #list3>
                                <print "#" #s>
                            >

入力された文字列の長さが1文字より大きい場合には、組み込み述語syntaxで入力された一つの文を構文解析して処理します。 syntax述語は、第1引数の文字列を標準入力ファイルにみなして、第2引数以降の述語に処理をさせる述語です。

上のプログラムでは、syntax述語の第2引数からの述語により形態素解析を行い、その結果に基づいて回答メッセージを作成します。 それは、次に示すような処理で組み立てられています。


1) 形態素解析

2) 人称の置き換え

3) 回答メッセージの合成

4) 回答メッセージの表示

1) 形態素解析

『::ktaiso <文 #a #b>』で、形態素解析を行います。

::ktaisoはktaisoライブラリを示し、会話キャラクターと同じディレクトリに置いてあるktaisoファイルに記述されています。

そして、ktaisoライブラリの中の"文"述語を呼び出します。 "文"述語には、入力された文を形態素解析して、単語ごとにに文を分解します。第1引数と第2引数には、結果が単語のリストとして返されます。

第2引数には述語に相当する単語のリストが設定され、第1引数にはそれ以外の単語がリストとして設定されます。


入力文 : 会話キャラクターは、プログラムです
出力 #a : (会話キャラクター は)
出力 #b : (プログラム です)


2) 人称の置き換え

『<Replace #a2 #a>』で、回答メッセージの合成を行います。

Replace述語については、前の項「2. 人称の置き換え」を参照してください。


3) 回答メッセージの合成

まず、"::list <flatten #list #n>"で、ここまで入力文を処理してきた#nリストを、平坦なリストに変換します。 flatten述語は組み込み述語であり、((会話キャラクター は) (プログラム です))のような、 リストの中にリストが含まれるリストを単純な(会話キャラクター は プログラム です)のようなリストに変換します。

このような単語毎に区切られた平坦なリストを入力として、後で説明するc述語"<c #list2 #list>"で回答文に合成します。

c述語は、changeのcに因んで作成しました。さまざまな入力パターンに対する回答を登録するためにc述語の処理は数が多くなります。 そのため、単純なcと名づけることにより、処理の全体が見やすいようにしています。

c述語については、「4. 回答メッセージ生成」で説明します。


4) 回答メッセージの表示

変換した回答メッセージを再度、flatten述語によりフラットなリスト構造に変換します。 そして、その結果のリストを組み込み述語"::sys <concat #s #list3>"により、一つの文字列として連結します。

concat述語は、第2引数のリストを文字列に連結して、第1引数に返します。

この結果の文字列を、プリントすることで回答メッセージを表示します。

これらの処理を繰り返し入力された一つの文に行うことによって、会話キャラクタの処理は進められていきます。

4. 回答メッセージ生成

4.1 回答メッセージ生成のためのユーティリティ

回答メッセージの生成は、c述語が行っています。 その、c述語は、2つの述語compとdを使って組み立てられています。


// select one at random
<comb #r #l>
        ::generator <permutation #r1 #l 1>
        ::sys <car #r #r1>
        ;

// data operation
<d #o #list>
        <comb #o #list>
        ;

c述語からは、d述語をインタフェースとして使います。

そして、d述語とcomp述語は、現在はスルーで引数と返り値を受け渡します。 本来は、ここで処理をフックすることを考えていたのでこのような形になっていますが、現在はその機能を付けていません。

第2引数が、回答メッセージになる文が複数登録されているリストです。 第1引数には、第2引数の回答メッセージ・リストから一つの文だけをランダムに選んで返します。

4.2 回答メッセージ生成のためのパターンマッチングと変換処理

回答メッセージは、入力文を形態素解析して結果のリストから、"c述語"によって回答メッセージを選択して合成されます。

このc述語が、会話キャラクタの回答を処理する肝の部分です。 この処理は、入力文の中の合致するパターンと対応する回答メッセージの候補のリストで構成されています。

c述語の再帰処理

c述語では、形態素解析された入力文のリストを再帰処理によって、頭から順に一つずつ取り出しながら処理していきます。

パターンマッチする文字列がない場合には以下の処理で再帰的に処理されます。


<c #o (#l1 : #l2)>
        <c #o #l2>
        ;

入力リストを、リストの頭の部分#l1とそれ以外のリストの部分#l2に分解しています。 そして、#l2だけを新たに入力リストとして、<c #o #l2>を呼び出します。よって頭の部分#l1を除いた処理を対象として実行するのです。

この処理は、どのパターンにもマッチしなかった処理なので、会話キャラクター・プログラムの最後の部分に置いてあります。

この処理より上にパターンマッチするc述語の処理があった場合には、そちらが実行されてこの処理は実行されません。

c述語のフォーマット

回答メッセージを生成するためのc述語は、パターンマッチ部分と変換処理部分で構成されています。

c述語は複数登録することができます。登録された順番にパターンマッチを試み、合致した処理の変換処理を実行して回答メッセージを生成します。

例えば、入力文のリストに「ただいま」という単語が含まれている場合のc述語の処理を説明しましょう。


<c #o (ただいま :#l)>
        <d #o (
                (おかえりなさいませ、ご主人様!)
                (おかえりなさいませ)
                (お帰りにゃ)
                )>
        ;

例えば「やっほー、ただいま、帰ってきたよ」という入力文だったとします。

形態素解析されると、(やっほー ただいま 帰ってきたよ)というリストに分解されます。

そして、パターンマッチさせていくのですが、最初の「やっほー」にはパターンマッチするものがないので再帰処理により、 この単語をはずした、(ただいま 帰ってきたよ)が次の入力リストとして使われます。

c述語は以下のようなフォーマットでプログラムします。


<c 回答を入れる変数 ("文字列パターン" : リストの残りとマッチングする変数)>
    <d 回答を入れる変数 (
                 回答の候補リスト...
                 )>
      ;

上に示した"文字列パターン"が「ただいま」のプログラム例と対比して参照してみてください。

このパターンに合致すると、(おかえりなさいませ、ご主人様!)、(おかえりなさいませ)、(お帰りにゃ)のいずれかのメッセージが d述語によって選択され回答を入れる変数#oに設定されて返ります。

もう少しいくつか違ったパターンの例を見て見ましょう。


<c #o (あなた は :#l)>
        <d #o (
                (あなたは、#l にゃ)
                (#l 、それで良かったのですにゃ)
                (#l 、それでどうなったですかにゃ?)
                (それについてどう思いますかにゃ?)
                (私も同じですにゃ)
                )>
        ;

これは、入力文に(あなた は ~)のパターンに一致した場合の処理です。 ~の部分は、対応する変数#lに入ります。

たとえば、(あなた は 素晴らしい です)という入力文の場合には、#lには(素晴らしい です) が設定されます。

設定された変数#lは、回答メッセージの候補の中で使われ、最終的に回答メッセージに合成されて返されます。


もうひとつ別のパターンを見てみましょう。 前の例よりも一般的な文書に対応するものです。


<c #o (#h は :#l)>
        <#rand = ::sys<random _> % 2> <is #rand 1>
        <d #o (
                (#h はどうなるのでしょうかにゃ?)
                (それが #h ですにゃ)
                (#h についてどう思いますかにゃ)
                (#h はどうでしょうかにゃ)
                (こうなると #h に期待できますにゃ)
                (おそらく、 #h のことだと思いますにゃ)
                (#h について話してくださいにゃ)
                (一般的な #h ですにゃ)
                (#h なんて思いつかないですにゃ)
                (でも、 #h は本当はこんなものじゃないですにゃ)
                (#h は、本質ですにゃ)
                (#h の狙いは何でしょうかにゃ)
                (#h の問題は何でしょうかにゃ)
                (#h を実感できましたにゃ)
                (#l ということですにゃ)
                (#l には、ワクワクしますにゃ)
                (にゃんと! #l とは!!)
                )>
        ;

これは、入力文に(~ は ~)のパターンに一致した場合の処理です。 ~の部分は、対応する変数#hと#lに順に入ります。

この処理には、"<#rand = ::sys<random _> % 2> <is #rand 1>"という、ここまでの説明した処理にはついてなかった記述があります。 これは、この処理があまりに一般的なパターンであるため、合致するパターンをすべてここで処理してしまわないための記述です。 この処理に合致しなければ、以降の処理で合致するものを探します。 ここで処理してしまうかどうかは、#random変数に0か1の値がランダムに設定されるようにし、その値が1の場合だけここで処理します。 値が0の場合には、パターンマッチしていてもここでは処理されません。

すべてのパターンに一致しない場合の処理

さまざまの入力パターンをあらかじめ準備しておきますが、どのパターンにも合致しない場合にはどうしたらよいでしょうか。

このような場合にも、何か返答する必要があります。それは、相槌であったり、次の発言を促したり、それまでの発言に対する反応をしたりすると、自然な会話に見えるのではないでしょうか。

そのために、以下のような応答メッセージのテンプレートを用意します。


<c #o ()>
        <d #o (
                (ふむ、ふむ)
                (なるほど)
                (そんなことあるわけないですにゃ!)
                (そうなんですにゃ)
                (にゃぁ)
                (にゃぁにゃぁにゃぁ)
                (それで、どうなるのですかにゃ?)
                (えーっと)
                (そうですにゃ)
                (どうでしょうにゃ)
                (どうしましょうにゃ)
                (思わせぶりですにゃ)
                (えっ)
                (どうですかにゃ)
                (嘘ですかにゃ)
                (何ですにゃ?、それは)
                (わかりましたにゃ)
                (あにゃたね!)
                (教えてくださいにゃ)
                (ははは)
                (へへ)
                (ふふふ)
                (にゃはは)
                (にゃへっ)
                (にゃふ!)
                (へー)
                (へぇー)
                (えぇ)
                (えー)
               )>
       ;

これらのテンプレートの文章は、会話の中のどのような場合に出てきても、不自然にならないようなメッセージを選ぶ必要があります。

また、用意したどのパターンにも合致しない場合は、実際の会話の中では多々あるでしょう。そのため、この場合の回答メッセージの種類は多く用意しておかなければなりません。応答メッセージの種類の数が少ないと、どうしても同じような回答が続くことになるためです。