prolog二分木知識によるシステム相互の応答

没頭していたというよりも、どうするか苦悶していたので前回書いた時から三週間近く経過してしまった。苦闘の理由は、システムを発展性のあるものにすることだった。

ようやく、基礎的なところで納得できるものになったので、何か書いておくことにした。自然言語解釈のところとprologによるsocket相互通信のシステム構築に手間かけた。

自然言語処理のところでは、大きなところは、質問文の解釈の基礎的なところをどのようにするか、設計上の問題に直面した。色々な知識はprolog二分木で表現されていて、その二分木がどのような構造になっているかを踏まえなければならない。二分木は、ノード値と二つのリーフ値の重層的連結になっている。ここに構文情報がはめ込まれてもいる。

ノード値は助詞を中心に付随する修飾語から成り立っていて、動詞や名詞はそこには全く入ってこない。したがって、色々な意味で有限である。限られている。質問文かどうかは、このノード値を解析すればわかる。主語的なものは、格助詞の「は」などがノード値にあれば、その左のリーフか、ないしは、左側のツリーのいずれかのリーフに主語があるはず。

ノード値からの距離が重要で、例えば、格助詞の場合は左側で一番近いところにある名詞が、その主要候補となるだろう。また、距離に、状況によって左の葉を優先するか、右の葉を優先するかの設定もする。この距離は、ノードやリーフの絶対アドレスから算出する。

一つの文章の二分木ツリーから、ノード値、名詞、動詞、修飾語、およびその絶対アドレスの全てを、それぞれリストにして、最初に一挙に取得してしまうという方法も取り入れた。これは別な形で二分木を分解してしまう方法でもある。こうすると、文章のほとんどがそのリスト軍の中に写像される。

こうすると文章の意味取得が容易になる。質問に対して、このリストを使って返信が可能になる。

通信については、javaを経由すればソケット通信は楽になるが、あくまで、prolog動詞て対話の通信ができるようにした。c++やjavaの時と同様に、ソケットとそのストリームを作成して、あとは自由に相互にやり取りをするというソケット通信の本来の形を実現するのに苦労した。ソケットを作って、一個の文章のやり取りをすることはすぐにできるが、ソケットを二つのシステムの対話の間じゅう維持するのが難しかったのだ。しかし、それもできた。

これから、自由な問いかけに、自らの知識(prolog二分木)に基づいて、自由に返答できるシステムにしていこう。形としては、あの巨大なwikipediaの知識を利用できる形にしてあるのだから。

アドレスという考え方: prolog二分木

自然言語のprolog二分木の中をもっと自由に移動する方法がないかと考えてきたが、アドレスを入れることにした。

二分木はルートを持っている。このルートが原点となる。linuxなどのディレクトリ構造のルートと同じである。ただ、二分木であるから、分岐は全てバイナリである。

そこで、ルートから出発して、左の葉をたどる時には'l'(エル)を加え、右をたどる時には'r'を加えたprologリストを作成することによって、唯一のアドレスを与えることができる。

仮想的な二分木を考える。

node(a,
    node(b,
        node(c,
            d,
            e
        ),
        node(f,
            g,
            h
        )
    ),
    node(i,
        node(j,
            k,
            l
        ),
        node(m,
            n,
            o
        )
    )
)

ルートの値は、aである。左右対称のツリーになっている。これに対し、アドレス[l,r,l]は、値gとなる。あるいは、[l,r]にあるのはサブツリーで、node(f, g, h)である。同じく、[r,l]にあるのは、node(j, k, l)となる。

このように、そのアドレスの部分ツリーを取ってくるプログラムは次のようになる。

%% 現在位置(ツリーアドレス)からの部分ツリーを返す
getsubtree(Tree,[],Tree).  %% 位置が空だったら、そのものを返す
getsubtree([],_,[]).
getsubtree(node(_,L,R),[H|T],Out) :- 
        (T=[] -> 
            (H=l -> Out=L; Out=R)
        ;   (H=l -> getsubtree(L,T,Out)
            ;getsubtree(R,T,Out),
            true)
        ).

名詞の主語としての重要性ウェイト:prolog二分木

二分木には、juman++による形態素解析の結果が組み込まれている。主語を捉える上で、可能性として色々ある場合、扱う順位、ウェイトのようなものが欲しい。たとえば、「アトムと言われているものはなんですか」というのを、二分木にすると、

%% line = アトムと言われているものはなんですか
%% phrases: [ 0 1 2 r3 ] 
testdoc(testline_0_0,
    node(ですか,
        node(は,
            node([],
                node(と,
                    [アトム, 'S:普/C:自然物/D:科学・技術'],
                    [[[言わ, 'V:言う'], れて], いる]
                ),
                [もの, 'S:形']
            ),
            [なん, 'S:数/C:数量']
        ),
        [ ]
    )
).

となる。図で描くと、

となる。「は」という格助詞の前に、名詞は三つある。「なん」が数詞になっているのが少しおかしい(juman++の仕様)だが、他に、「アトム」と「もの」もある。「なんですか」というのも、「ものはなんですか」も日本語としては単独で成立する。しかし、ここでは主語は「アトム」であるべきだ。

となると、名詞の主語になる優先順位というのが必要になるだろう。名詞の種類は、juman++では、次のようになっている(http://www.unixuser.org/~euske/doc/postag/)。

普通名詞 (例)「つくね焼」「鞭打ち症」「パイ中間子」サ変名詞以外のもの。
副詞的名詞 (例)「ところ」「ため」「ぐらい」「~したところ」「~するため」
形式名詞 (例)「の」「こと」「もの」「つもり」「わけ」	
固有名詞 (例)「エスキモー」「広辞苑」「平成」以下の 3カテゴリにあてはまらない固有名詞。
組織名 (例)「NATO」「そごう」「運輸省」	
地名 (例)「東京」
人名 (例)「田中」
サ変名詞 (例)「説明」「あんよ」「埋め合わせ」「発想」「~する」の形をとれるもの。
数詞 (例)「ゼロ」「億」 数値。
時相名詞 (例)「あした」「ほどんど」「それぞれ」

順位をつけると、(1)人名(2)組織名(3)地名(4)固有名詞(5)普通名詞(6)サ変名詞(7)形式名詞(8)数詞(9)時相名詞(10)副詞的名詞、となるのではないか。この優先順位で、文章の主語をとらえることにしよう。

自然言語二分木を平文に変換するprologプログラム

書き忘れていたが、この間、以前も使っていた二分木を平文にするプログラムを少し改良している。AIサーバー、AIクライアントで使っているので掲載しておく。

%%
%% prologツリーをベタな文章に変換して表示する
%% 全て、変数に出力されるように改訂
%%

wsprint(Sent,Out) :- 
    nb_setval(console,''),
    printnode(Sent),
    nb_getval(console,Out).
    %% write(Out),nl.

%% グローバル変数 console に出力をつなげていく
wsappend(S) :-
    nb_getval(console,Tmp),
    atom_concat(Tmp,S,Out),
    nb_setval(console,Out).

%% -----------------------
%%% 出力のclauses
%% -----------------------
% 対象がatomならば、そのまま表示
printnode(N) :- atom(N),wsappend(N).
%% 対象が空でないリストならば、最初の項の表示
printnode(N) :- [_|_] = N,showlist(N). 
%% 対象が空リストならば'/'(半角スラッシュの表示)
%% printnode(N) :- [] = N,wsappend('/'). %% 空リストでもtrueにする
%% スラッシュやめる
printnode([]).
%% 対象が項ならば、元の言葉の順序で表示(語がnodeならば再帰的に表示する
%% ただし、node語がnodeは含まない)
printnode(N) :- node(X,Y,Z) = N,
        printnode(Y),
        printnode(X),
        printnode(Z).

%% -----------------------
%% getlistは、リストが[語, カテゴリ]から構成されているのから、語だけのリストを作る
%% 一つのフレーズに複数の語があると
%% [[[語, カテゴリ],語],[語, カテゴリ]] などのように繋がってリスト化される
%% knpがカテゴリを出力しない場合は、語が単独になることもある
%% HeadとTailをから、それぞれの語を取り出して、結合したのを出力
%% -----------------------
getlist([H|[T]],[X1, X2]) :- getlist(H,X1),
        getlist(T,X2),!.
%% 構造的に、Tailには、単位リストしか入っていない
getlist([H|[T]],[H,H1]) :- atom(H),[H1|_] = T,!.
%% tailがリストでない場合は、atomであるHeadのチェック
getlist([H|[_]],H) :- atom(H).
%% tailが構造化されたリストの場合にはここで処理する
getlist([H|[T]],[Z,T]) :- atom(T),
        getlist(H,Z).

%% -----------------------
%% ベタなリストに変換するのがprintlist
%% swi-prologにflattenという組み込み関数がある
%% -----------------------
printlist(L) :- atom(L),
        wsappend(L).
%% ベタなリスト化は、以下のclauseで単純に作れる
printlist(L) :- [H|[T]] = L,
        printlist(H),
        printlist(T),!.

%% -----------------------
%%  getlistとprintlistを繋げるのがshowlist
%% -----------------------
showlist(L) :- getlist(L,X),
        printlist(X).

実行例を示しておく。prolog二分木化したwikipediaからとってきているかなり複雑な二分木がだ、綺麗に日本語の平文に直している。

?- ['../lib/wsprint.swi'].                                                                              true.

?- wsprint(node(は,[['デビュー', 'S:サ/C:抽象物/D:メディア'], 前],node(で,[[紫苑, 'S:普/C:植物'], [名義, 'S:普/C:抽象物']],node(と,[[杏樹, 'S:人'], [紫苑, 'S:普/C:植物']],node([],[いう, 'V:いう'],node(で,[コ ンビ, 'S:普/C:組織・団体'],node(の,[[耽美, 'S:普/C:抽象物'], [系, 'S:普/C:抽象物']],node([],[漫画, 'S:普/C:人工物-その他;抽象物/D:文化・芸術'],node(を,node(の,[],[パロディ, 'S:普']),node([],[[描いて, 'V:描く'], おり],node([],[[要, 'S:普/C:人工物-その他;抽象物'], [出典, 'S:普/C:抽象物/D:文化・芸術']],node(の,[[ 江口, 'S:人'], [寿史, 'S:人']],node([],[単行本, 'S:普/C:人工物-その他;抽象物/D:文化・芸術'],node(の,[[江口, 'S:地'], [寿史, 'S:人']],node(で,なんとかなる,node([],[ショ, 'S:サ'],node(の,[[江口, 'S:地'], [寿史, 'S:人']],node(に,[[[爆発, 'S:サ/C:抽象物'], ['ディナー', 'S:普/C:人工物-食べ物/D:料理・食事']], ['ショ ー', 'S:普/C:抽象物/D:文化・芸術']],node([],[[[[収録, 'S:サ/C:抽象物/D:文化・芸術;メディア'], [さ, 'V:する']], れて], いる],[ ])))))))))))))))))),Out).

Out = デビュー前は紫苑名義で杏樹紫苑というコンビで耽美系の漫画のパロディを描いており要出典江口寿史の単行本江口寿史のなんとかなるでショ江口寿史の爆発ディナーショーに収録されている .


AIサーバーとAIクライアント:prolog二分木

先の記事では、クライアント側がjavaだったが、クライアント側もprologにした。クライアントプログラムは、先の平文を二分木にするクライアントを少し変えたものだ。

%
% prologのテレパシーサーバーに質問し、回答を得るクライアント
%

% 文字列とutf8のバイトコードを相互に変換する
% http://www.ibot.co.jp/wpibot/?p=2681 など参照
:- ['../lib/utf8string.swi'].

% swi-prologモジュールの組み込み
:- use_module(library(streampool)).

% クライアントをスタートさせて、ストリームを取得、グローバル変数に保存する
telepathy_client(Host, Port) :-
        setup_call_catcher_cleanup(tcp_socket(Socket),
            tcp_connect(Socket, Host:Port),
            exception(_),
            tcp_close_socket(Socket)),
        setup_call_cleanup(tcp_open_socket(Socket, In, Out),
            nb_setval(socketIn,In),
            nb_setval(socketOut,Out)).

telepathy_to_server(Term,Reply) :-
        % 送信文字列をコードに変換する
        utf8tring(Bytes,Term),
        nb_getval(socketIn,In),
        nb_getval(socketOut,Out),
        % コマンをつけて、サーバーに送信
        % format(Out, '~w:~s~n', [Com,Bytes]),
        % 当面コマンドをつけない
        format(Out, '~s~n', [Bytes]),
        flush_output(Out),
        %read(In, ReplyCode), % これではうまくいかない
        % サーバーからコードを受信する
        read_line_to_codes(In, ReplyCode),
        % write(ReplyCode),nl,
        % コードを文字列に変換
        utf8tring(ReplyCode,Reply).
        % 表示する
        %format('Reply: ~s~n', [Reply]).

% ストリームを閉じる
telepathy_close :-
        nb_getval(socketIn,In),
        nb_getval(socketOut,Out),
        close(In, [force(true)]),
        close(Out, [force(true)]).

サーバー側は、前の記事と同じで変更ない。実行結果は以下のようである。

まず、クライアント側。

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

?- telepathy_client(localhost,30000).
true.

?- telepathy_to_server(アトムとはなんですか,Reply).
Reply = アトムはロボットです.

続いて、サーバー側。

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

?- server(30000).
Receive = > アトムとはなんですか 
Send = > アトムはロボットです

これからは、知識が多い場合の処理はもう少し後にして、単純な知識が様々に利用される状況を想定してみる。

知識サーバー(プロトタイプ): prolog二分木

AI同士の対話が当面のターゲットなのだが、そのサーバー部分の作成とテストを実行した。相変わらず知識は「アトムはロボットです」しか持っていない。

まず、ネットワーク上で質問を受け取って、自らの持つ知識でそれに返答するというprologを示すと次のようになる。すでに示している reply.swi を組み込む。私は、ロボット同士のTCP-IPの通信システムは、基本、Telepathyという名前をつけている。

%%%%
%% telepathy_server.swi
%% prologサーバー
%% 参照:
%% http://www.swi-prolog.org/pldoc/man?section=stream-pools
%% utf8string.swiを使用する
%% 記事 http://www.ibot.co.jp/wpibot/?p=2681
%%%%

:- ['reply.swi'].

%% home brewでインストールしたswi-prologでは、次のライブラリが読めない可能性がある
%% その場合は、ソースからコンパイルし直し、最新バージョンを入れる
:- use_module(library(streampool)).

server(Port) :-
    %% tcpソケットの作成
    tcp_socket(Socket),
    %% ソケットをアドレスにつなげる 第二引数は、portのみか、HostPort
    tcp_bind(Socket, Port),
    %% ソケットからリクエストを受け取る、第二引数は、ペンディングリクエストの上限
    tcp_listen(Socket, 5),
    %% ソケットとコミュニケーションのためのストリームを作成する
    tcp_open_socket(Socket, In, _Out),
    %% Inが利用可能になったら、accept(Socket)が呼び出される
    add_stream_to_pool(In, accept(Socket)), 
    %% loopが空になるまでdispatch_stream_poolが呼び出される
    %% dispatch_stream_poolは入力があるとadd_stream_to_poolのGoalを呼び出す
    stream_pool_main_loop.

accept(Socket) :-
    %% ソケットからのクライアントからのリクエストを待つ
    tcp_accept(Socket, Slave, Peer),
    tcp_open_socket(Slave, In, Out), 
    add_stream_to_pool(In, client(In, Out, Peer)).

client(In, Out, _Peer) :-
    %% 入力ストリームから次の行を読み出す 結果はCommandにユニファイされる
    %% 改行までか、ファイルの終わりまで読み込まれる 改行コードは削除される
    %% 改行を含むブロックを読み込むときは read_line_to_codes/3 を使う
    read_line_to_codes(In, Codes),
    %% 入力ストリームを閉じる
    close(In),
    %% バイトシーケンスを文字列に変換する
    utf8tring(Codes,Request),
    %% コンソール出力
    format('Receive = > ~s ~n',[Request]),
    %% 回答を取得
    wsreply(Request,Reply),
    %% 回答を送信
    utf8tring(ReplyCodes,Reply),
    %% write(Out,ReplyCodes),nl(Out),
    %% write(Out,Reply),nl(Out),
    format(Out, '~s~n', [ReplyCodes]),
    format('Send = > ~s~n',[Reply]),
    flush_output(Out),
    %% 出力ストリームを閉じる
    close(Out),
    %%write('Close socket stream ...'),
    %% プールからストリームを削除する
    delete_stream_from_pool(In).        
	

このサーバー、立ち上げるとターミナルをブロックして、他のプロセスを実行させる余地がない感じになるが、prologは、簡単に実行をスレッド化できるので、大きな問題ではない。

クライアントは、ここではjavaで書いた簡単なものを使う(次の段階でこちらもprologにする)。

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;

/**
 *
 * @author washida
 */
public class JPClient {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        try {
            String server = "localhost";
            int port = 30000;
            Socket soc = new Socket(server, port);

            OutputStream os = soc.getOutputStream();
            PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os, "UTF-8")));
            InputStream is = soc.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));
            String question = "アトムとはなんですか";
            System.out.println("質問: "+question);
            pw.print(question+"\r\n");
            pw.flush();
            String line;
            while((line = br.readLine()) != null){
                System.out.println("回答: "+line);
            }
            pw.close();
            os.close();
            br.close();
            is.close();
        } catch (IOException e) {
            System.out.println("Exception: " + e);
        }
    }
    
}

実行結果を示す。まず、javaクライアント側のコンソールは次のようになる。

質問: アトムとはなんですか
回答: アトムはロボットです

回答は、prologサーバーから送り返されてきたものである。

telepathy_server側のコンソールは次のようになる。

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

?- server(30000).
Receive = > アトムとはなんですか 
Send = > アトムはロボットです

ということである。

知識にもとづき質問に答える: prolog二分木

みずからが持っている知識にもとづき、問いに答えるシステムのプリミティブなものを作った。持っている知識は次のような「アトムはロボットです」という知識だけであるとする。現実には、日本語wikipediaの膨大な知識を持っているのだが、それを利用するのはもう少し後にする。

知識は、knowledgeというfanctorで表現されているとする。%で始まる行は、prologのコメント文である。

%% 知識 = アトムはロボットです
%% phrases: [ r0 1 ] 
knowledge(testline_0_0,
    node(は,
        [アトム, 'S:普/C:自然物/D:科学・技術'],
        node(です,
            [ロボット, 'S:普/C:人工物-その他/D:科学・技術'],
            [ ]
        )
    )
).

ここで、「アトムとはなんですか」という問いがあったとする。それに回答するprologプログラムは次のようなものである。

% 
% 質問に答えるプログラム
% 

:- ['../lib/client.swi'].
:- ['../lib/wsprint.swi'].
:- create_client(localhost,25000).

reply(Sentence,Out) :- 
        chat_to_server('GETTREE',Sentence,Recv),
        %% write(Recv),nl,
        %% utf8string.swiから出てくる文字列は、string!! 
        %% このままでは、unificationに失敗するので、termに変換する
        term_string(Recv1,Recv),
        dialog(_,Tree) = Recv1,
        %% write(Tree),nl,
        isquestion(Tree),
        getsubject(Tree,Sub),
        getreply(Sub,Out),
        wsprint(Out).

%% ----- 疑問文 回答取得 -----
getreply(Subject,Out) :- knowledge(_,Tree),chkreply(Tree,Subject),Out=Tree.
%% 主語のフレーズが一致していたらそれを回答とみなす
chkreply(node(N,L,_),Subject) :- member(N,[は, とは, って]),L=Subject,!.
chkreply(node(_,L,_),Subject) :- chkreply(L,Subject),!.
chkreply(node(_,_,R),Subject) :- chkreply(R,Subject).

%% ----- 疑問文 主語取得 -----
getsubject(node(N,L,_),Out) :- member(N,[は, とは, って]),L=Out,!.
getsubject(node(_,L,_),Out) :- getsubject(L,Out),!.
getsubject(node(_,_,R),Out) :- getsubject(R,Out).

%% ----- 疑問文 チェック -----
%% ノード値がリストのいずれかの語で、右の葉が空リスト [ ] の場合、疑問文 
isquestion(node(N,_,[])) :- member(N,[ですか,なの,か,なのか]),!.
isquestion(node(_,L,_)) :- isquestion(L),!.
isquestion(node(_,_,R)) :- isquestion(R).

%% 知識 = アトムはロボットです
%% phrases: [ r0 1 ] 
knowledge(testline_0_0,
    node(は,
        [アトム, 'S:普/C:自然物/D:科学・技術'],
        node(です,
            [ロボット, 'S:普/C:人工物-その他/D:科学・技術'],
            [ ]
        )
    )
).

プログラムの最後に、先ほどの知識が加えられている。もしこの知識が多く、あるいは、複雑になれば、それらを調べることになる。

reply()が、topレベルのclauseである。質問文(Sentence)は、平文であり、それらはサーバーに問い合わせして二分木にして返してもらっている(chat_to_server('GETTREE',Sentence,Recv),)。サーバーから返ってきた二分木は、swi-prologのstringであり、このまま dialog(_,Tree) = Recv, などとunificationすると失敗する。この理由がわからなくて、半日くらい無駄にした。term_string(Recv1,Recv),で、stringを文字列に変換している。返ってきたものから、二分木だけを取り出し(dialog(_,Tree) = Recv1,)、質問文であるかどうかをチェックし(isquestion(Tree),)、質問文であるならば、質問の主語にあたるものを取り出し、知識に問い合わせして、回答に当たる知識を得る(getreply(Sub,Out),)、という段取りである。

実行結果は次のようになる。

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

?- reply(アトムとはなんですか,Out).

アトムはロボットです/

Out = node(は, [アトム, 'S:普/C:自然物/D:科学・技術'], node(です, [ロボット, 'S:普/C:人工物-その他/D:科学・技術'], [])) .

「どんな」型の疑問文:prolog二分木

次の疑問文生成は、「どんな」あるいは「どのような」という疑問文である。例えば、「アトムはロボットです」を疑問文にすると「アトムはどんなロボットですか」ということになる。

この「どんな」が生成できれば、他も「なんで、なんの、なぜ、どの、どうして、どんなふうに」などの多くの疑問詞がほぼ同様の操作で生成可能になる。

まず、前にも示したと思うが、「アトムはロボットです」をシステムで二分木を生成すると次のようになる。

%% phrases: [ r0 1 ] 
testdoc(testline_0_0,
    node(は,
        [アトム, 'S:普/C:自然物/D:科学・技術'],
        node(です,
            [ロボット, 'S:普/C:人工物-その他/D:科学・技術'],
            [ ]
        )
    )
).

最初の行は、システムが生成するもので、コメント文となり、次は、生成二分木のインデクスで、他が二分木の実体である。

次に「アトムはどんなロボットですか」を二分木化すると次のようになる。

%% phrases: [ 0 1 r2 ] 
testdoc(testline_0_0,
    node(ですか,
        node([],
            node(は,
                [アトム, 'S:普/C:自然物/D:科学・技術'],
                どんな
            ),
            [ロボット, 'S:普/C:人工物-その他/D:科学・技術']
        ),
        [ ]
    )
).

ちょっと、基本構造が違っている。上の二つの二分木を図で示すと次のようになる。

左が平常文であり右が疑問文だ。違いは、rootノードの違いである。先のシステム出力のコメント文、%% phrases:の右側に書いたものが、二分木のリストで、rのついた番号のものをルートフレーズとしているということである。

フレーズは、名詞や動詞などの単独で意味を持つ語と助詞などの付随語のペアからなっていて、システムは、いくつかの判断基準を複合させて、ルートフレーズを確定する。また、ときには、フレーズリストをネストさせて、それぞれのリストごとにルートフレーズを決定し、二分木の形を決めている。この辺りのことは、こちらなどを参照してください

「どんな」疑問文も、ルートの違いによって、色々ありうるということである。例えば、左の構造を基本にしながら「どんな」疑問文を生成すると、次の図のような場合もありうる。

基本これらの違いは、「部分知識を取り出す上でどちらが望ましいか」だが、こちらは、赤い枠の中に、アトムが入っていないのが気になる。しかし、まあ、prolog二分木としてはどちらも有効である。

どちらを生成するかという問題になるが、疑問文の生成は、部分知識の取得とは無関係なので、作りやすい方がよく、その点では後者の「どんな」疑問文をつくようにした方がいい。

この場合、生成ロジックは、「です」を「ですか」に変更し、「ロボット」を値にもつ左の葉を一つのnodeとして「どんな」を左のは「ロボット」を右のは、ノード値は空リスト [ ] にすればよい。

プログラムは、先ほどの「なんですか」疑問文を少し変えた次のようなものになる。

%% ----- どんな 疑問文 -----
donna(T,Out) :- not(functor(T,node,3)),Out=T.
donna(node(N,L,R),Out) :-
        (
            'です'=N,
            not(functor(L,node,3))
        ->  N1='ですか',
            L1=node([],'どんな',L),  %% ← 変更点
            R1=R
        ;   (
                'は'=N
            ->  N1='とは'
            ;   N1=N,
                true
            ),
            donna(L,L1),
            donna(R,R1),
            true
        ),
        Out = node(N1,L1,R1).

変更点は、一行だけである。 「ですか」の左文字を、部分木に変えたことである。その際、元々の左語をそのノードの右葉にして左は、「どんな」という疑問詞を入れたことである。

まあ、この L1=node([],'どんな',L) 書き方は、CとかJavaとかやっているものには、なんとも変な感じである。なぜなら、node([],'どんな',L)というのは、いわゆる関数ではない。functorという複合項目、ある意味、文字のようなものなのであるが、Lというのは、prologにおける正真正銘の変数である。変数が前触れもなく、裸のままで入っている。prologは、これが半角大文字であることの一点で、変数とみなすのである。しかも、L1=node([],'どんな',L)の演算子"="は、Cやjavaでいう代入処理ではない。ユニフィケーション(単一化)という。L1が何も確定していない変数であるので、結果的に、やっていることは代入なのであるが、元々の機能は、とんでもなく違っている。例えばこれが、node(N,L1,R)=node([],'どんな',L)とかなっていたら、Nが未確定変数ならば、Nには、[]が代入される。確定変数ならば、両者の一致性がチェックされ出力される。変数が確定しているか未確定かによって結果は違ってくる。ユニフィケーションの場合、未確定語が左あろうが右にあろうが関係ない。

ユニフィケーションは不思議な操作ではあるが、prologの根幹を成している機能であるから、とても大事なのである。

このプログラムの実行結果は次のようになる。

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

?- donna(node(は,[アトム, 'S:普/C:自然物/D:科学・技術'],node(です,[ロボット, 'S:普/C:人工物- の   /D:科学・技術'],[ ])),Out).

Out = node(とは, [アトム, 'S:普/C:自然物/D:科学・技術'], node(ですか, node([], どんな, [ロボット, 'S:普/C:人工物- の\n/D:科学・技術']), [])).

相変わらず形態素情報は維持されて複雑に見えるが、結果は、先の図のようなものである。

「とはなんですか」疑問文: prolog二分木

知識に基づいて、「とはなんですか」という疑問文を生成するprolog文を作った。例えば、「アトムはロボットです」という部分知識があったときに、これを回答にするような問いを考えるということである。プログラムは次のようになる。細かい調整はしていないので、色々バグもあると思う。

単純疑問文の場合と発想を変えてプログラム化した。シンプルだ。単純疑問文の場合も、この発想で書き直せば、もっといい感じになると思う。

%% ----- なんですか 疑問文 -----
nandesuka(T,Out) :- not(functor(T,node,3)),Out=T.
nandesuka(node(N,L,R),Out) :-
        (
            'です'=N,
            not(functor(L,node,3))
        ->  N1='ですか',
            L1='なん',
            R1=R
        ;   (
                'は'=N
            ->  N1='とは'
            ;   N1=N,
                true
            ),
            %% 左右の葉を再帰的に処理する
            nandesuka(L,L1),
            nandesuka(R,R1),
            true
        ),
        Out = node(N1,L1,R1).

変な感じではあるが、元々に知識的ツリーを入れると、それを答えとするような疑問文を生成するということである。

if文を使っている。if文の中にif文を使って、ネストしている。ある意味、if文は、Cやjavaなどの逐次処理言語の得意芸であり、あまりprolog的にはなじまないのかもしれないが、prolog的には、cut(!)を使った単純な構造である。if文の最後にtrueが付いているのは、if文の条件節が成立しなくても、カッコの次を実行するためである。そうしないと、成立しないときに、バックトラックに入って、その後を実行しなくなる。

最初のif文は、node値が「です」の部分木に出会ったら、それを「ですか」に変えて、その左葉の値がツリーでない限り、「なん」という疑問詞に変えるということである。この時の左葉が部分ツリーになる場合は、うまくいかないが、そういう状況は、想像できない。ないと思う。

さらに、「は」とう格助詞がnode値の時は、それを「とは」に変える処理もする。

「です」nodeに出会った時は、それ以上の再帰的処理はしないが(文の最後であるはずだから)、そうでない限り、各葉の再帰的処理に入り、各葉がnodeでないときは、そのまま値を返すという再帰的処理の終端処理が、最初のprolog文である。

実行結果は次のようになる。

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

?- nandesuka(node(は,[アトム, 'S:普/C:自然物/D:科学・技術'],node(です,[ロボット, 'S:普/C:人工物-その他/D:科学・技術'],[ ])),Out).

Out = node(とは, [アトム, 'S:普/C:自然物/D:科学・技術'], node(ですか, なん, [])).

知識文は「アトムはロボットです」をjavaのprolog二分木作成システムで作った二分木である。フォーマット改変の記事で、それぞれの記号的意味は解説している。

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(か, [], []))).

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

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