prolog二分木における単純疑問文の生成

知識に基づく対話を考えるときに、疑問文の生成は避けて通れない。疑問文は、対話のトリガー、対話を動機づけるものだ。まず、終助詞の「か」を加えて単純疑問文を生成することを試みるのが順当である。

準備として、既存二分木に部分二分木(ないしは語)を加える汎用プログラムを作成しておく。

%% 空のツリーに、Nodeを与えると、それ自身を返す
insert(_,Node,[],Node).
%% 既存ツリーが語の場合
insert(left,node(Value,Left,[]),Word,node(Value,Left,Word)) :- 
        atom(Word),!.
insert(right,node(Value,[],Right),Word,node(Value,Word,Right)) :- 
        atom(Word),!.
%% すでにTreeがある場合
insert(left,Node,node(Value, Left, Right), node(Value, New, Right)) :-
        insert(left,Node,Left,New),!.
insert(right,Node,node(Value, Left, Right), node(Value, Left, New)) :-
        insert(right,Node,Right,New),!.

例えば、「アトムはロボットです」という二分木は、

node(は,アトム,ロボットです)

と書ける。

これを先のinsertを用いて単純疑問文に変えてみよう。

?- ['create.swi'].
true.

?- insert(right,node(か,[],[]),node(は,アトム,ロボットです),Q疑問文).
Q疑問文 = node(は, アトム, node(か, ロボットです, [])).

?- insert(right,node(か,[],[]),node(は,アトム, node(の,博士,ロボットです)),Q疑問文).
Q疑問文 = node(は, アトム, node(の, 博士, node(か, ロボットです, []))).

?- insert(right,node(か,[],[]),node(は,アトム,node([],ロボットです,[])),Q疑問文).
Q疑問文 = node(は, アトム, node([], ロボットです, node(か, [], []))).

最後のものは、明らかに、無駄なツリーを持つものになってしまったが、変更は容易である。

次は、「どれ、どちら、どなた、どこ、だれ、いつ、いくつ、どの、どう、なぜ」という、疑問詞を持った疑問文を生成することを試みる。

prolog二分木のフォーマットを変更する

prolog二分木から初歩的な文章を作るようになったが、少しフォーマットを変更することにした。

一つは、動詞の原形を入れるときに、使われている表現形と同じ場合は、原形情報を与えなかったが、これを与えることにした。動詞と分かれば、原形がなければそれが原形と推測できるが、そもそも二分木には品詞情報を入れていないので、判断できないからだ。

なぜ、これが大事なのかといえば、動詞の原形は、それだけで文章終端になりうるからだ。文章を作るときに、終端が違和感なく終わることはとても大事なのである。このように「だ」「です」「である」などは、多分判定詞で、同士ではないが、例えば「書き」で終わったら、言葉としておかしいが「書く」で終わることは不自然ではない。だから、動詞の原形はとても重要なので、原形がある場合とない場合がはっきりしないというのは、良くないということで、改良した。

もう一つは、動詞の原形と名詞のカテゴリは、ほとんど同じフォーマットで入れ込んでいるが、ときに区別できなくなるときがあるので、それらの冒頭に、名詞のカテゴリの場合は「C:」というヘッダ、動詞の原形の場合は「V:」というヘッダーをつけることにした。例えば、次のようである。

jawiki(wiki_543_line_2261_1,node(を,小田,node([],[含む, 'V:含む'],node(は,['4', [名, 'C:抽象物']],node(に,node([],node(ばかりの,node(が,[放送, 'C:抽象物'],[[終了, 'C:抽象物'], [した, 'V:する']]),[アニメ, 'C:抽象物']),[[[機動, 'C:抽象物'], [戦士, 'C:人']], ガンダム]),node([],[[[熱中, 'C:抽象物'], [して, 'V:する']], おり],node(の,node([],node([],node(に,node(から,node([],node(が,node([],まだ,ガンプラ),[[[発売, 'C:抽象物'], [さ, 'V:する']], れる]),前),[同, [作品, 'C:抽象物']]),[[登場, 'C:抽象物'], [する, 'V:する']]),[[ロボット, 'C:人工物-その他'], [兵器, 'C:人工物-その他']]),'モビルスーツ(MS)'),node(を,[模型, 'C:人工物-その他'],node([],[[[自作, 'C:人工物-その他'], [して, 'V:する']], いた],[ ]))))))))).

二分木を一行にしているので少しわかりにくいのでインデントをつけて表すと次のようになる。

jawiki(wiki_543_line_2261_1,
    node(を,
        小田,
            node([],
                [含む, 'V:含む'],
                node(は,
                    ['4', [名, 'C:抽象物']],
                    node(に,
                        node([],
                            node(ばかりの,
                                node(が,
                                    [放送, 'C:抽象物'],
                                    [[終了, 'C:抽象物'], [した, 'V:する']]
                                ),
                                [アニメ, 'C:抽象物']
                            ),
                            [[[機動, 'C:抽象物'], [戦士, 'C:人']], ガンダム]
                        ),
                        node([],
                            [[[熱中, 'C:抽象物'], [して, 'V:する']], おり],
                            node(の,
                                node([],
                                    node([],
                                        node(に,
                                            node(から,
                                                node([],
                                                    node(が,
                                                        node([],
                                                            まだ,
                                                            ガンプラ
                                                        ),
                                                        [[[発売, 'C:抽象物'], [さ, 'V:する']], れる]
                                                    ),
                                                前
                                            ),
                                            [同, [作品, 'C:抽象物']]
                                        ),
                                        [[登場, 'C:抽象物'], [する, 'V:する']]
                                    ),
                                    [[ロボット, 'C:人工物-その他'], [兵器, 'C:人工物-その他']]
                                ),
                                'モビルスーツ(MS)'
                            ),
                            node(を,
                                [模型, 'C:人工物-その他'],
                                node([],
                                    [[[自作, 'C:人工物-その他'], [して, 'V:する']], いた],
                                    [ ]
                                )
                            )
                        )
                    )
                )
            )
        )
    )
).

これで、例えば、最初の「含む」は、原形と表現形が同じだが、原形を付加している。また、動詞なのでヘッダにV:が付いている。これで、処理がよりしやすくなった。

これで、一昨日から、日本語wikipedia本文とツイッターデータを全部作り直している。

 

文章をprologの二分木で作る

ようやく、ここに到達した。まだ事始めではあるが、ぼちぼちとやっていこう。

次のようなprologプログラムを考える。

%% -----------------------
%% 文章(二分木化されたprolog宣言文)を組み立てる
%% 2019年4月22日
%% -----------------------

%% 空のツリーに、Nodeを与えると、それ自身を返す
insert(_,Node,[],Node).
%% 既存ツリーが語の場合
insert(left,node(Value,Left,[]),Word,node(Value,Left,Word)) :- 
        atom(Word).
insert(right,node(Value,[],Right),Word,node(Value,Word,Right)) :- 
        atom(Word).
%% すでにTreeがある場合
insert(left,Node,node(Value, Left, Right), node(Value, New, Right)) :-
        insert(left,Node,Left,New).
insert(right,node(Value0, Left0, []),node(Value, Left, Right), node(Value, Left, node(Value0, Left0, Right))).
insert(right,Node,node(Value, Left, Right), node(Value, Left, New)) :-
        insert(right,Node,Right,New).

特に工夫もない、アドホックなもので、文章のノードを与えて、ツリーを作るところから始めてみようというわけである。実行すると次のようになる。

?- ['create.swi'].
true.

?- insert(_,node(は,ロボット,[]),[],Subs).
Subs = node(は, ロボット, []) .

?- insert(right,node(です,機械,[]),node(は, ロボット, []),Subs).
Subs = node(は, ロボット, node(です, 機械, [])) .

?- insert(left,node(の,産業用,[]),node(は,ロボット,node(です,機械,[])),Subs).
Subs = node(は, node(の, 産業用, ロボット), node(です, 機械, [])) .

?- insert(right,node(の,産業用,[]),node(は,ロボット,node(です,機械,[])),Subs).
Subs = node(は, ロボット, node(の, 産業用, node(です, 機械, []))) ;
Subs = node(は, ロボット, node(です, 機械, node(の, 産業用, []))) .

最初のコマンドで、プログラムを読み込む。なんの条件も考えずに、ただ、ノードを付け加えている。後半の二つは、node(の,産業用,[]) というノードを作成済みツリーの左側と右側に付け加えている。

この結果を出すために、作ったようなプログラムなので、あえて議論しなくても良い。ただ、二分木で表されたprologの文章は、ある種プログラムであって、プログラムがプログラムを作る感じを確認したかっただけである。

読み取った言葉をBNFファイル内容に対応させる

ALDialogやALSpeechRecognitionで言葉を読み取った時、不思議なことは、一致した言葉をBNFファイルの内容に対応させれば、topicファイルなどに書かれているそれに対するリアクションの言葉の内容がすぐに探し出せるのに、それをせずに、あらためてtopicファイルの中のruleに戻って調べなおしていることだ。こんな面倒なことをする理由がわからない。
たとえば、BNFファイルに、
<r410>:((やすみなさい)|(すわりなさい));
というトリガー(rule)があったとして、ろぼっとが「やすみなさい」という言葉を認識したとする。もちろん、この行があって認識できたのである。しかし、このトリガーとしての<r410>が情報として出されていれば、topicファイルのどの行に対応しているかはすぐわかるはずだ。わかるようにプログラムを組むことができるはずだ。それをしていないのである。ログを見ている限り、もう一度topicファイルから調べ直している。
理由はわからない。BNFをコンパイルしたlcfファイルがらみなのかもしれなない。
だから、自前のDialogでは、これを直接捉えたいと思った。ただ、bnfファイルは、プログラミング構文に使われる文法なので、使うためには構文解析アルゴリズムを使わなければならない。ただし、普通の数式処理構文解析プログラムならば、たくさんサンプルが出ていて、わかりやすく解説されているが、bnfを通してある言葉を認識し、それがbnfのどの構文に一致したのかを理解するアルゴリズムは、サンプルがない。もしかして誰もやっていない。
なので、1から作らなければならない。再帰下降構文解析法を応用しようと思っている。できたらここに掲載するつもりだが。できるのか。だいぶできているが複雑。

kakasiのインストール

英語モードと日本語モードを、「話」と「聴き」において自由に切り替える際に、日本語をローマ字に変換するため、kakasiをインストールした。つまり、opennaoでコンパイルしたということだ。これはそのままnaoに持ち込める。
kakasi -Ja -Ha -Ka -Ea -iutf8 -outf8
で、漢字、ひらがな、カタカナ、記号の全てをローマ字に変換する。たとえば、
kakasi -Ja -Ha -Ka -Ea -iutf8 -outf8
お題はコンビニです
odaihakonbinidesu

ALDialogの代替モジュール

ALDialogに変わるモジュールを作成することにした。完全なものでなくてもいい、私が必要な機能が全て含まれるものにしたい。言語の切り替えを、聞き取り、話し、のなかで自由に切り替えることが基本だ。Topicファイルも新しいものにするが、コンセプトやルールの基本的なものはそのままにしたい。私がほとんど使わない機能は組み込まない。だから、結構シンプルなものになると思う。

英語と日本語の混在

英語と日本語の混在としては、ALSpeechRecognitionでは、言語設定を日本語にして、ALTextToSpeechでは、言語設定を英語にする、あるいはその逆にしたりできれば良い。
ALSpeechRecognitionは、文脈ファイルに依存させるので、その言語は、ALSpeechRecognitionの言語設定と同じにしておかなければならない。これについては、かなり自由に操作できるようになった。その認識した言語に沿って、もう一方、トピックファイルには、そのリアクションも書かれているので、それを読み取って、speechrecognitionのイベント処理をやるメソッドを作成すれば良い。
まず、トピックファイルから、うまく、リアクション情報、すなわち喋る情報を取り出す部分を完成させよう。もう、ほぼ、頭の中では出来上がっている。

TopicファイルからBNFファイルへのコンバーター作成

基本ALDialog軸にやってきたが、英語日本語を混在させたりすると、ALDialogではうまく対応できないことがわかって、ALSpeechRecognitionレベルで対応することが便利になってきた。
そこで、どうしても必要になったのが、音声認識の文脈情報を与えているBNFファイルの作成だった。直接作成してもいいのだが、一番いいのは、qichatで書かれたTOPICファイルをBNFファイルに変換することだ。もちろん、これはNAOQIでやれる。ある意味、ALDialogを使っているうちに作成してくれるのだが、そのタイミングが理解できない。実に面倒なのだ。一旦作成してくれれば、ALSPeechRecongnitionで、コンパイルしてlcfというバイナリファイルを作成して、極めて効率よく、ロボットは聞き取りしてくれる。
昨日から今日にかけて、必死で作って、ようやく、まあ、そこそこ正しく作れたようだ。いくつか問題もあるが。ちゃんとロボットは作成したBNFファイルをコンパイルできていて、また、アクティベイトすると、正しく聞き取りしてくれる。
作成した、IbotBnf170328Japanese.bnfというBNFファイルをロボットに転送して、文脈に加えて、活性化させて、登録する。手順をpython SDKでやると以下のようになる。これで聞き取りできるようになった。
>>> from naoqi import ALProxy
>>> spr = ALProxy("ALSpeechRecognition","192.168.1.xx",9559)
>>> spr.compile("/home/nao/.local/share/dialog/IbotBnf170328Japanese.bnf","/home/nao/.local/share/dialog/IbotBnf170328Japanese.lcf","Japanese")
>>> spr.addContext("/home/nao/.local/share/dialog/IbotBnf170328Japanese.lcf","IbotContext")
>>> spr.activateAllRules("IbotContext")
>>> spr.pause(False)
>>> spr.getRules("IbotContext","active")
['IbotBnf170328Japanese#start', 'IbotBnf170328Japanese#take170320 trig']
>>> spr.subscribe("MySpeechRecognition")
>>> spr.unsubscribe("MySpeechRecognition")
聞き取った内容を、受け止めるのは、WordRecognizedのイベントを受け取ってやるので、それはまた別な作業なのだ。

日本語と英語のクロス

ロボットに日本語で問いかけると英語で答える、英語で問いかけると日本語で答えるようにしたいと思って、前回の記事のように、ALDialogでやろうとしたが結局破綻した。
そこで、もっとCoreなところにかえって、ALSpeechRecognitionでやったら、うまくいった。
結果はYoutubeに掲載した。

日本語と英語の会話をさせる

(この記事の方法は破綻した -> 参照
日本語を聞き取って、英語を喋らせたり、英語を聞き取って日本語を喋らせたいと思った。
意外と面倒だった。本当は簡単にできるのかもしれないが。ここまで得た知識でなんとかなりそうなので、記録しておく。
これをやらせるためには、日本語を聞き取った後で、すぐに、ALDialogのsetLanguageを実行させて、英語に切り替え、英語を喋らせるようにすれば良い。ただし、一つのtopicファイルには、英語か日本語のどちらかしかプログラム化できないので、日本語を聞き取った後に、英語のtopicファイルにある、ルールをイベントで起動させることが必要だ。
何れにしても、英語と日本語のtopicファイルを同時にloadTopicをさせなければならない。
ここで大事なことをいかに整理する。
(1)聞き取りのためには、$HOME/.local/share/dialog/ 以下にあるbnfファイル(文脈処理)が必要だが、これはシステムが作る(自分で書いてもいいようだが、何しろ、このフォルダはALDialogが管理しているので、あまりいじらない方がいいよと思う)。このシステムファイルは、コンパイルされて、lcfファイルに変換される。
(2)この文脈処理ファイル群は、ロボットがスタートして、ALDialogがロードされて以降は、ALDialogが管理しているようで、その後勝手に、削除したりするとエラーが報告される。
(3)今まで日本語をやっていたのに、新たに英語のファイルを付け加えると、いろいろトラブルが起きる。
(4)一旦、文脈ファイルができると、別なトピックファイルが組み込まれても、その変更だけになり、新たにbnfファイルが作成されたりしない。
(5)ALDialogには、compileALLというメソッドがあって、これは文脈ファイルつくるのだが、bnfファイルがないときは、それを作成するきっかけにもなる。
(6)何もなければ、すべてのトピックファイルがloadTopicでロードした後に、compileAllを実行するとうまくいくみたいだ。
(7)その後、setLanguageで言語を切れ変えれば良い。
(8)日本語と英語を切り替える上では、それぞれのトピックファイルが共通のトピックを持っているようにするべきだ。
何しろ、文脈ファイルが決定的に重要な意味を持っている。
(追記)上の方法でもうまくいかない。どうしても、日本語のBNFしかつくらないときは、次の方法をとる。
(1)$HOME/.local/share/dialog/ にあるファイル(bnfファイル)等を一旦全て削除する。
(2)ロボットを再起動する
(3)pythonのnaoqi sdkを起動する(PCでいい)。そして、以下のようにやる。
>>> from naoqi import ALProxy
# ALdialogのオブジェクトを取得
>>> dlg = ALProxy("ALDialog","192.168.xxx.xxx",9559)
# 日本語と英語のトピックファイルをロードする
>>> dlg.loadTopic("/home/nao/ibot/user/topic/test170323-enu.top")
'test170323'
>>> dlg.loadTopic("/home/nao/ibot/user/topic/test170323-jpj.top")
'test170323'
# コンパイルする
>>> dlg.compileAll()
これでできるはずである。あとは、bnfファイルを気にせずにやれば良い。というか、二度とbnfファイルを直接いじらない