ELIZA/DOCTORに(無理やり)日本語を喋らせる

2019/05/15
藤田昭人


以前書いた記事 の締めで「ELIZAと日本語で対話したいと思いつつ、本ページを終了します。」などと書いたまま音沙汰なしだったのですが、 実は裏で作業をしてました。それなりに区切りが着いたので(開発途上ですが)紹介します。

2019/05/17 追記:申し訳ございません。
githubにアップしたソースコードにバグが見つかりました。
アップデートしましたのでチェックアウトし直してください。


まずはソースコードのありかから

ソースコードを次の Github の僕のリポジトリに起きました。

github.com

例によって nodejs で動作しますので、お手元の nodejs/npm の環境で以下の手順で実行してみてください。

$ git clone https://github.com/m04uc513/eliza-ja-js.git
Cloning into 'eliza-ja-js'...
remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 16 (delta 0), reused 13 (delta 0), pack-reused 0
Unpacking objects: 100% (16/16), done.
$ cd eliza-ja-js/
$ node ElizaApp.js doctor-ja.json test-ja.txt
C: こんにちは
E: ごきげんよう。あなたの問題を述べてください。
C: 男性はみんな似ている。
E: どのように?
C: 彼らはいつも何か他のものについて私たちを悩ませています。
E: あなたは特定の例を考えることができますか?
C: はい、私のボーイフレンドが私をここに来させました。
E: あなたのボーイフレンドがあなたをここに来させましたか?
C: 彼は私はいつでも落ち込んでいると言います。
E: あなたが落ち込んでいると聞いて残念です。
C: それは本当です。私は不幸です。
E: あなたはここに来ることが、あなたが不幸ではなくなることに役立つと思いますか?
C: 私はいくらか援助を必要とします。それは確かです。
E: あなたがいくらか援助を手に入れたら、それはあなたにとって何を意味するのでしょうか?
C: おそらく私の母と仲良くすることを学ぶことができるでしょう。
E: あなたの家族についてもっと教えてください。
C: 私の母は私を気遣ってくれます。
E: あなたのお母さんの他に誰はあなたを気遣ってくれますか?
C: 私の父です。
E: あなたのお父さん?
C: あなたは私の父の様です。
E: その類似性はあなたに何を示唆していますか?
C: あなたはそれほど積極的でないが、それを私にそれに気付かせたくないと思っています。
E: あなたがそれほど積極的でないと思う理由は何ですか?
C: あなたは私と議論しない。
E: あなたは何故、議論しないと思いますか?
C: あなたは私を恐れている。
E: あなたが恐れていると思う理由は何ですか?
C: 私の父は皆を恐れている。
E: あなたがお父さんについて考えるとき、あなたは他に何が思い浮かべますか?
C: いじめっ子。
E: あなたのボーイフレンドがあなたをここに来させましたのは何故なのか、さらに議論しましょう。
$ 

第1引数は DOCTOR スクリプトの日本語版(後述)、 第2引数は日本語による入力文のテキストでELIZA論文に掲載されていた実行例に基づきます。 ここで過去に紹介した実装と同様に1行毎に取り込み応答文を生成しています。 内容は DOCTOR の応答(の日本語訳)に概ね沿っています。 (って言うか、そのように出力するように調整した) 微妙な日本語何ですけどもね。ちなみに、オリジナルのELIZAの応答は ELIZA(1)「傾聴」を模倣するプログラムとは? で紹介しています。気になる人は比較してみてください。


プログラムの動作

このプログラムは ELIZA の振る舞いを日本語ベースで再現する目的で書きました。詳細は ELIZA(4)DOCTOR スクリプト に書いたので(気になる人は)そちらをみていただくとして、 ELIZA の応答メッセージの生成は次の7つの手順を取ります。

  タグ 説明
入力文をスペースで区切られた単語に分割(いわゆるわかち書き)を行う。
pre 入力文に対し一連の事前の文字列置き換えを行う。
key 入力文の全ての単語を走査しキーワードリストを作成し、以降はキーワード毎に処理を行う。
decomp 与えられたキーワードに対し分解パターン(decomposition pattern)をマッチさせ、最初に一致した要素を選択する。
assemble 分解パターンに対応する再構成パターン(reassembly pattern)を選択する。
post 生成された応答文に対し一連の事後の文字列置き換えを行う。
  応答文を出力する。

今回は特に2〜3の 「 key-match, decomposition, reassmbe が日本語でも上手く機能するのか?」 を試したかったのがフォーマルな開発目的って言ってもいいかも。(後付けです)


わかち書きについて

さて、先の表の0の項、わかち書きについてです。*1 今回の実装では下記のTinySegmenterを使ってます。

chasen.org

このソフトウェアは機械学習AdaBoost)を使って辞書なしで平文をわかち書きに変換してくれる軽量ライブラリです。 学習結果がコードに埋め込まれているので非常にコンパクトなのですが、 その反面、独自に再学習させることができないのが難点なんだそうです。 もっとも、再学習させるためには次の TinySegmenterMaker があるそうです。

shogo82148.github.io

今回は TinySegmenter をそのまま使っています。 「分かち書きの単位はMeCab + ipadicと互換性があります」 との事なので、まずはこの単位で試してみることにしました。


スクリプト実行の実際

実際の開発では ELIZA(4)DOCTOR スクリプト で紹介したオリジナルのELIZAの実行トレースを参考に、ELIZA論文の実行例の入力文翻訳を順次投入しながらスクリプトを追記して行きました。 ここではオリジナルのELIZAスクリプトの記述とは異なる記述方法を用いてるところのみ簡単に紹介します。

まず初っ端の「こんにちは」です。TinySegmenterの「MeCab + ipadic互換」の単位は非常に粒度が細かくて[こん][にち][は]と3つに分割されてしまいます。 そこでデフォルトの応答メッセージのセット "xnone" の仕様を拡張して "*" 以外の分解パターンを追加できるようにしました。 この分解パターンは単なる検索文字列として扱われ(わかち書き以前の)平文の入力に含まれるか?否か?をテストします。 含まれた場合には応答文として、オリジナルの "hello" の応答文(の翻訳)のセットを使用しました。

次にオリジナルではkeyが "i"(私は)と "my"(私の)との2つに分かれていたルールですが、 TinySegmenterでは [私][は] だとか [私][の] といった形に区切られてしまうので、 key では "私" のルールに統合して、分解ルールで " 私 は " だとか " 私 の " といった形で定義するように変更しました。

さらに類義語の "family" の翻訳版 "家族" は入力文に "父" や "母" が登場するとマッチしますが、 その際に入力文に含まれる対応語("父" や "母")を分解結果を収める配列に記録しておくため、 類義語の定義では語の後に "*" を付加しました。 それにより4の応答文生成では"父"や"母"が埋め込まれ、 さらにその後の5の post で "お父さん" や "お母さん" に変換されます。

実際には、まずは応答文を生成してみて不自然な日本語になる場合は、 意味が変わらない範囲で入力文を修正する反則も行いました。 が、これはオリジナルの ELIZA でも行われた調整なんじゃないかな?と想像しています。


現時点での評価

現時点での評価を書いとくと 「一応、それっぽく動いてるけど、応答が微妙な日本語になる場合もあるんじゃないの?」 とか 「これ単体だと基本的にスクリプトガリガリ書いてくアプローチになりそう」 とかと思ってます。

実際、ELIZAの応答文生成は複数の文案のラウンドロビンと(かなり強引な)虫喰い文案の穴埋めを基本に実現されてますので「より自然な文表現を実現するにはもっといろんな工夫が必要なんだろうなぁ…」と考えてはいたのですが、今回の論文掲載事例の翻訳再現というお題ではイメージどおりの作業となりました。

ただ、実際の人間どおしの会話でも、それほど「自然な文表現ではない、特に日常会話では案外不十分な言い回しの応酬なのではないか?」とも思うので、こういったラフな方法の方が有効なケースも案外あるのではないかとも思います。

この段階の実装で間違いなく役に立ちそうな機能といえばJSONフォーマットによるELIZAスクリプトの定義かな?と考えています。

ともあれ…

DOCTORスクリプトは「私」だとか「あなた」と言った、一般的な人工無脳ではあまりターゲットにしないキーワードを検知して応答を生成してるところがELIZAが傾聴を得意とする秘密なんじゃないかと考えるに至りました。その意味ではオリジナルのスクリプトを丁寧に追いかける機会を得たのは収穫だと思っています。



以上


付録:DOCTORスクリプト日本語版(仮)

$ cat doctor-ja.json
{
    "initmsg": "How do you do.  Please tell me your problem.",
    "lastmsg": "Goodbye.  Thank you for talking to me.",
    "quitmsg": [
        "bye",
        "goodbye",
        "quit"
    ],
    "pre": [
        [ "。", "" ],
    [ "、", "" ],
    [ ".", "" ],
    [ ",", "" ]
    ],
    "post": [
        [ "母", "お母さん" ],
        [ "父", "お父さん" ],
        [ "私", "あなた" ]
    ],
    "synon": [
        [ "家族", "母*", "ママ*", "父*", "パパ*",
      "姉*", "お姉ちゃん*", "妹*", "兄*", "お兄ちゃん*", "弟*",
      "妻*", "女房*", "夫*", "亭主*", "子供達*", "息子*", "娘*" ],
        [ "欲望", "欲しい", "必要*" ],
        [ "悲しい", "不幸*", "落ち込*", "病*" ]
    ],
    "key": [
        {
            "word" : "似", "rank": 10,
            "decomp" : [
                {
                    "pattern" : "*", "curr" : 0,
                    "reasmbl" : [
                        "どのように?",
                        "どのような類似性がありますか?",
                        "その類似性はあなたに何を示唆していますか?",
                        "他にどのようなつながりがありますか?",
                        "似ているとはどういう意味ですか?",
                        "その関係は何だとあなたは思いますか?",
                        "本当に何らかの関係があるのでしょうか?",
                        "どうして?"
                    ]
                }
            ]
        },
        {
            "word" : "様", "rank": 10,
            "decomp" : [
                {
                    "pattern" : "* 様 *", "curr" : 0,
                    "reasmbl" : [
                        "goto 似"
                    ]
                }
            ]
        },
        {
            "word" : "いつも", "rank":  1,
        "decomp" : [
                {
                    "pattern" : "*", "curr" : 0,
                    "reasmbl" : [
                        "あなたは特定の例を考えることができますか?",
                        "いつ?",
                        "あなたはどんな出来事を考えていますか?",
                        "本当にいつでも?"
                    ]
                }
            ]
        },
        {
            "word" : "私", "rank":  1,
            "decomp" : [
                {
                    "pattern" : "$ * 私 の *", "curr" : 0,
                    "reasmbl" : [
                        "あなたの%1%のは何故なのか、さらに議論しましょう。",
                        "あなたは以前、あなたの%1%と言いましたよね?",
                        "しかし、あなたの%1%か?",
                        "それはあなたの%1%と何か関係がありますか?"
                    ]
                },
                {
                    "pattern" : "* 私 の @家族 *", "curr" : 0,
                    "reasmbl" : [
                        "あなたの家族についてもっと教えてください。",
                        "あなたの%2%の他に誰%3%か?",
                        "あなたの%2%?",
                        "あなたが%2%について考えるとき、あなたは他に何が思い浮かべますか?"
                    ]
                },
                {
                    "pattern" : "* 私 の *", "curr" : 0,
                    "reasmbl" : [
                        "あなたの%1%か?",
                        "何故あなたは、あなたの%1%と言うのですか?",
                        "それはあなたに属する何か他のことを示唆していますか?",
                        "あなたの%1%は重要ですか?"
                    ]
                },
                {
                    "pattern" : "* 私 は * @欲望 *", "curr" : 0,
                    "reasmbl" : [
                        "あなたが%1%手に入れたら、それはあなたにとって何を意味するのでしょうか?",
                        "なぜ、あなたは%1%欲しいのですか?",
                        "すぐに%1%手に入れたらどうでしょうか?",
                        "あなたが%1%持っていない場合はどうなりますか?",
                        "あなたにとって%1%得ることは何を意味するのでしょうか?",
                        "%1%欲することは、この議論と何の関係があるのでしょうか?"
                    ]
                },
                {
                    "pattern" : "* 私 は * @悲しい *", "curr" : 0,
                    "reasmbl" : [
                        "あなたが%2%でいると聞いて残念です。",
                        "あなたはここに来ることが、あなたが%2%ではなくなることに役立つと思いますか?",
                        "%2%でいるのは楽しいことではないと、わかってます。",
                        "あなたを%2%でいる理由をあなたは説明できますか?"
                    ]
                }
            ]
        },
        {
            "word" : "あなた", "rank":  1,
        "NOTE":  "助詞や接続語を追加しないと文節が適切に切り取れない",
        "TODO":  "品詞推定結果を併用する",
            "decomp" : [
                {
                    "pattern" : "* あなた は * が *", "curr" : 0,
                    "reasmbl" : [
                        "あなたが%1%と思う理由は何ですか?",
                        "わたしは%1%と信じていいですか?",
                        "時々、あなたはわたしが%1%ことを望みますか?",
                        "おそらく、あなたが%1%のでしょう。"
                    ]
                },
                {
                    "pattern" : "* あなた は * を *", "curr" : 0,
                    "reasmbl" : [
                        "あなたが%2%と思う理由は何ですか?",
                        "わたしは%2%と信じていいですか?",
                        "時々、あなたはわたしが%2%ことを望みますか?",
                        "おそらく、あなたが%2%のでしょう。"
                    ]
                },
                {
                    "pattern" : "* あなた は * と *", "curr" : 0,
                    "reasmbl" : [
                        "あなたは何故、%2%と思いますか?",
                        "あなたは%2%と思いたいのですね -- 違いますか?",
                        "あなたと%2%ことをあなたはどう思いますか?",
                        "本当にあなたと%2%ですか?",
                        "あなたと%2%と思うことをあなたは望みますか?",
                        "あなたと%2%としましょう -- それにはどういう意味がありますか?",
                        "あなたと%2%と他の誰かが信じますか?"
                    ]
                }
            ]
        },
        {
            "word" : "xnone", "rank":  1,
            "decomp" : [
                {
                    "pattern" : "こんにちは", "curr" : 0,
                    "reasmbl" : [
                        "ごきげんよう。あなたの問題を述べてください。",
                        "こんにちは。あなたの問題は何ですか?"
                    ]
                },
                {
                    "pattern" : "*", "curr" : 0,
                    "reasmbl" : [
                        "あなたを完全に理解しているかどうかわかりません",
                        "続けてください",
                        "それはあなたに何を示唆していますか?",
                        "そのようなことを話し合うことについてあなたは強く感じることはありますか?"
                    ]
                }
            ]
        }
    ]
}

*1:わかち書きについては後ほど別途、独立した記事を書くつもりなので、ここではサラッと