知識サーバー(プロトタイプ): 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二分木を取ってくるクライアント

質問文二分木の作成をやってきたが、ここで、質問を受け取って、自分の知識に基づいて解答を作成するシステムを作る。

質問は、平文で受け取るので、そこから知識の二分木に結びつけなければならないのだが、一旦、質問文を二分木に変換するのがいいと思った。prologで作成しようと思ったが、いろいろ面倒なので、これまでのjavaの作成システムにサーバー機能も持たせて、そこにアクセスして単文を二分木にしようと思う。

prologのクライアントを作成した。

%
% 文章を与えて、サーバーからprolog二分木を取ってくるクライアント
%

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

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

% クライアントをスタートさせて、ストリームを取得、グローバル変数に保存する
create_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)).

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

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

prologは、utf8のバイトコードで送受信するので、その変換が必要だった。すでに作成したシステムがあったので、簡単に済ますことができた。

実行結果は次のよう。

?- ['../client.swi'].
true.

?- create_client(localhost,25000).
true.

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

「どんな」型の疑問文: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(か,[],[]) というフレーズを挿入して、疑問文を作る方法を示したが、もう少し柔軟に作成する方法を試みよう。

%%
%% 言語二分木を疑問文に変換する
%% mkquestionは、単純な確認疑問文を作成する
%%

%% -------------------------

confirm :- knowledge(_,Node),mkquestion(Node,Q),write(Q).

% 再帰の終端処理
mkquestion(node(N,L,R),P) :- atom(R),atom_concat(R,'か',R1),P = node(N,L,R1).
mkquestion(node(N,L,[]),P) :- atom(N),atom_concat(N,'か',N1),P = node(N1,L,[]).
mkquestion(node([],L,[]),P) :- atom(L),atom_concat(L,'か',L1),P = node([],L1,[]).
% こんなことがあるのだろうか?
mkquestion(node([],L,[]),P) :- node(N2,L2,R2) = L,mkquestion(R2,P2),P = node(N2,L2,P2).
% 基本的な再帰処理
mkquestion(node(N,L,R),P) :- mkquestion(R,P1),P = node(N,L,P1).


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

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

これは、knowledgeの宣言文を単純疑問文に改訂するprologである。状況としては、知識は持っているが、それが確かであることを確認する単純な疑問文と考えれば良い。次のようになる。

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

?- confirm.
%出力、タブは事後的に入力

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

プログラムの方には、questionの頭部で、単純疑問文の例を書いておいた(javaの二分木作成システムで作ったもの)。が、それとは違う二分木システムの単純疑問文になる。プログラムのものは、左の葉に長いシステムで、こちらは、元の文章に引きづられているので、右に長い文章になっている。

 

prolog二分木のフォーマットの改良

jumanをjuman++することによって、名詞のカテゴリ情報がより確実に組み込まれるようになり、ドメイン情報も使える。そこで、二分木のフォーマットをさらに改良することにした。それによって、会話で知識を使う道がより望ましいものになる。

変更後の二分木例を示すと次のようだ(文章はWikipediaより)

%% line = 小田を含む4名は、放送が終了したばかりのアニメ『機動戦士ガンダム』に熱中しており、まだガンプラが発売される前から同作品に登場するロボット兵器「モビルスーツ (MS)」の模型を自作していた。
%% phrases: [ r0 1 2 [ 3 4 5 r6 7 [ 8 9 10 11 12 13 14 r15 16 ] ] 17 ] 
testdoc(testline_0_0,
    node(を,
        [小田, 'S:地'],
        node([],
            [含む, 'V:含む'],
            node(は,
                [['4', 'S:数/C:数量'], [名, 'C:人']],
                node(に,
                    node([],
                        node(ばかりの,
                            node(が,
                                [放送, 'S:サ/C:抽象物/D:メディア'],
                                [[終了, 'S:サ/C:抽象物'], [した, 'V:する']]
                            ),
                            [アニメ, 'S:普/C:抽象物/D:文化・芸術']
                        ),
                        [[[機動, 'S:普/C:抽象物'], [戦士, 'S:普/C:人']], [ガンダム, 'S:固']]
                    ),
                    node([],
                        [[[熱中, 'S:サ/C:抽象物'], [して, 'V:する']], おり],
                        node(の,
                            node([],
                                node([],
                                    node(に,
                                        node(から,
                                            node([],
                                                node(が,
                                                    node([],
                                                        まだ,
                                                        [ガンプラ, 'S:普']
                                                    ),
                                                    [[[発売, 'S:サ/C:抽象物/D:ビジネス'], [さ, 'V:する']], れる]
                                                ),
                                                [前, 'S:副']
                                            ),
                                            [同, [作品, 'S:普/C:抽象物/D:文化・芸術']]
                                        ),
                                        [[登場, 'S:サ/C:抽象物'], [する, 'V:する']]
                                    ),
                                    [[ロボット, 'S:普/C:人工物-その他/D:科学・技術'], [兵器, 'S:普/C:人工物-その他/D:政治']]
                                ),
                                [[[['モビルスーツ', 'S:普'], ['(', 'S:普']], ['MS', 'S:組']], [')', 'S:普']]
                            ),
                            node(を,
                                [模型, 'S:普/C:人工物-その他'],
                                node([],
                                    [[[自作, 'S:サ/C:人工物-その他'], [して, 'V:する']], いた],
                                    [ ]
                                )
                            )
                        )
                    )
                )
            )
        )
    )
).

まず、最初の方の [小田, 'S:地']にあるように、リーフ値が名詞の場合はサブタイプをS:のヘッダをつけて、組み込むようにした。サブタイプ名は、節約のため、実際の名前の最初の1文字だけにしている。サブタイプ名は、「普通名詞, 副詞的名詞, 形式名詞, 固有名詞, 組織名, 地名, 人名, サ変名詞, 数詞, 時相名詞」だけのようなので、重なりはない。ただし、小田は地名になっているが、現実は、人名である。このように、人名か地名がわかれば、会話に利用できるのだ。

さらにカテゴリとサブタイトルが両方ある場合は、'S:サ/C:抽象物'のように/で区切って、繋げるようにした。リストにする方法も考えたが、やらた、リストがネストされるので、わかりにくくなると思い、回避した。

ドメインがある場合は、これにさらにD:のヘッダーでつなげる。[放送, 'S:サ/C:抽象物/D:メディア']あるいは[アニメ, 'S:普/C:抽象物/D:文化・芸術']という感じである。ドメインは、どういう状況の中に単語が含まれているのかがわかるので、貴重な情報である。

こうなると、二分木の中にシソーラス辞書が同時に組み込まれている感じになる。

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二分木化

先の記事で出力するようになった部分文章を再び二分木にするようにした。すなわち、次のようである。

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

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

元の二分木と構造は必ずしも一致せず、動詞が原型になって、自立した文章の体をしていることが異なっている。壊したり作ったり。これで、一つの文章でできることは大体終わった。

文章の中から部分知識を取り出すProlog文

知識の本質は言い換えである。文章の中には、様々な知識が詰まっていて、それらは部分的な言い換え、部分知識である。という前提のもとに自然言語解析を行なっているが、その入り口のところのprologプログラムを記録しておく。

prologの二分木化された文章例は以下のようなものである。wikipediaからの一文である。

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:する']], いた],
                                     [ ]
                                 )
                             )
                         )
                     )
                 )
             )
         )
     )
).

冒頭にもあるように、もと文章は、日本語wikipediaのテキスト化ファイルの543番ファイルの2261パラグラフ目にある文章で、
「小田を含む4名は、放送が終了したばかりのアニメ『機動戦士ガンダム』に熱中しており、まだガンプラが発売される前から同作品に登場するロボット兵器「モビルスーツ (MS)」の模型を自作していた」
をprologの二分木化してものである。

この中にある部分知識を、動詞を原形で終わらせた、一つの整合的な文章と理解して抜き出すプログラムをprologで作成した。次のようになる。

%% -----------------------
%% リストから動詞の原形を取得する 原形までの名詞もつなげる
%% ex. [[[正式, [発表, 'C:抽象物']], [さ, 'V:する']], れた] を 「正式発表する」 に変換
%% 先行するフレーズを取得し、部分知識として獲得する
%% グローバル変数 ws_endverb ws_prewords ws_pushedword を使用する
%% 2019年4月30日〜
%% -----------------------

ws_testverb :- jawiki(_,Node),
        %% 初期化が必要なグローバル変数
        nb_setval(ws_prewords,[]),
        nb_setval(ws_pushedword,'NOTDEFINED'),
        ws_getverb(Node,Out),
        format('EndWD = ~w ~n',[Out])
        .

%% -----------------------
%% ws_memory/2
%% 言葉の記憶数:先行する語をいくつまで記憶しておくか
%% -----------------------
ws_memory(10).

%% -----------------------
%% ws_getverb/2
%% -----------------------
ws_getverb(A,_) :- atomic(A),fail.
ws_getverb(node(_,Left,_),Out) :-
        nb_setval(ws_endverb,''),
        ws_getoriginal(Left,Out),
        nb_getval(ws_prewords,S),
        nb_setval(ws_prewords,[]),
        format('PreWD = ~w ',[S]).
ws_getverb(node(_,Left,_),Out) :-
        ws_memory(M),
        ws_pushglobal(ws_prewords,Left,M),
        ws_getverb(Left,Out).
        %format('DEBUG Left2 = ~w ~n',[Left]).
ws_getverb(node(A,_,Right),Out) :-
        ws_memory(M),
        ws_pushglobal(ws_prewords,A,M), %% 左から右に変わるときにNode値を確保する
        nb_setval(ws_endverb,''),
        ws_getoriginal(Right,Out),
        nb_getval(ws_prewords,S),
        nb_setval(ws_prewords,[]),
        format('PreWD = ~w ',[S]).
ws_getverb(node(_,_,Right),Out) :-
        ws_memory(M),
        %% 右に [同, [作品, 'C:抽象物']] と言うのがあるとここで処理
        %% 左も同じ機能
        ws_pushglobal(ws_prewords,Right,M),
        ws_getverb(Right,Out).

%% -----------------------
%% ws_pushglobal/3
%% グローバル変数に値を左から詰める
%% リストに限定する
%% -----------------------
ws_pushglobal(VName,Term,Size) :-
        %format('DEBUG Push Term = ~w ~n',[Term]),
        nb_getval(VName,S0),
        %format('DEBUG Push S0 = ~w Term = ~w ~n',[S0,Term]),
        (atom(Term),
        not(last(S0,Term)) %% 既存最終項が重なっていないかだけチェック
         ->  (length(S0,Size1),
            Size1 >= Size
            -> [_|T] =S0,
                append(T,[Term],S1)
            ;   append(S0,[Term],S1)
            )
        ;   ([_|_] = Term, % Termがリストならば
            %% カテゴリ等を除いたリストを得る
            ws_getlist(Term,L2),
            %% そのリストをつなげてatomにする
            %%format('DEBUG Pushglobal Term = ~w L2 = ~w ~n',[Term,L2]),
            %%format('DEBUG Pushglobal Term = ~w S0 = ~w ~n',[Term,S0]),
            flatten(L2,L3),
            %% すでにグローバル変数に、このリストの統合した後が、個別に入っている可能性がある
            %% もし入っていたら、最後の方から、それに一致するものを全て削除する
            %format('DEBUG Pushglobal  S0 = ~w L3 = ~w ~n',[S0,L3]),
            ws_deletelast(S0,L3,S2),
            %%S2 = S0,
            concat_atom(L3,H),
            %format('DEBUG Pushglobal Term = ~w S0 = ~w ~n',[Term,S0]),
            not(last(S2,H))
            ->  (length(S2,Size1),
                Size1 >= Size
                ->  [_|T2] =S2,
                    append(T2,[H],S1)
                ;   append(S2,[H],S1)
                )
            ;S1 = S0
            )
        ),
        nb_setval(VName,S1).

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

%% -----------------------
%% ws_popglobal/2
%% グローバル変数の最後の要素を取得する
%% グローバル変数は、リストでなければならない
%% -----------------------
ws_popglobal(VName,Term) :-
        nb_getval(VName,S0),
        %format('DEBUG POPGLOBAL Term = ~w S0 = ~w ~n',[Term,S0]),
        (S0 = []
        -> Term = [] %% Term = '' の方がいいと思う
        ;   (last(S0,Term)
            ->  delete(S0,Term,S1),
                nb_setval(VName,S1)
            ; true % これを入れないと全体がfailになってしまう
            )
        ).

%% -----------------------
%% ws_popglobalfromlist/2
%% リストからポップする → 使っていない
%% -----------------------
ws_popglobalfromlist(VName,List) :-
        nb_getval(VName,L),
        ws_deletelast(L,List,Out),
        nb_setval(VName,Out).

%% -----------------------
%% ws_deletelast/3 
%% ws_pushglobalの中で使っている
%% Lの最後から L1と一致するものを全て削除する
%% L=[a,b,c,d,e,f,g] L1=[e,f,g] → Out=[a,b,c,d]
%% もし、一致しないものがあったら、元のリストをそのまま返す
%% -----------------------
ws_deletelast([],_,[]). %% 元リストが空の場合は、空を返す これを入れないと空がエラーになる
ws_deletelast(Out,[],Out).
ws_deletelast(L,L1,Out) :-
        reverse(L,L0),
        [H0|T0] = L0,
        reverse(L1,L2),
        [H2|T2] = L2,
        (H2 == H0
        ->  reverse(T0,R0),
            reverse(T2,R2),
            ws_deletelast(R0,R2,Out)
        ; Out = L  % 等しくないものがあった場合は、元のを変更せずに返す
        ).

%% -----------------------
%% ws_getoriginal/2
%% -----------------------
ws_getoriginal([H0|T],Out2) :-
        %% 動詞の場合、H0:表現形, H1:原形
        %% atomでなければならない
        atom(H0),
        [H1|_] = T,
        atom(H1),
        atom_codes(H1,S1),
        %% 'V:' のコードリストは [86, 58]
        %% 一致する場合、動詞の原形である
        (ws_listncomp([86,58],2,S1)
    ->      split_string(H1,":","", [_|[T2]]),
            atom_string(Out1,T2),
            nb_getval(ws_endverb,Out0),
            %%atom_concat(Out0,Out1,Out2),
            format(atom(Out2),'~w~w/~w',[Out0,Out1,H0]),
            %% 動詞に組み込まれた先行語をpopする
            nb_getval(ws_pushedword,PW),
            %format('DEBUG ws_prewords PW = ~w H0 = ~w ~n',[PW,H0]),
            ws_popglobal(ws_prewords,PW)
    ;
            %format('DEBUG H0 = ~w H1 = ~w ~n',[H0,H1]),
            Out1 = H0,
            nb_getval(ws_endverb,Out0),
            atom_concat(Out0,Out1,Out2),
            ws_memory(M),
            ws_pushglobal(ws_prewords,H0,M),
            %% ここでpushしたものを記憶しておき、動詞に入った場合は上でpopする
            nb_setval(ws_pushedword,H0),
            %format('DEBUG ws_prewords PUSH H0 = ~w ~n',[H0]),
            nb_setval(ws_endverb,Out2),!,fail %% !とfailは、ともに不可欠
        ).

ws_getoriginal([H|_],_) :- atom(H),
        %format('DEBUG H_2 = ~w ~n',[H]),
        nb_getval(ws_endverb,Out0),
        Out1 = H,
        atom_concat(Out0,Out1,Out2),
        nb_setval(ws_endverb,Out2),!,fail. %% !,failは不可欠

ws_getoriginal(A,_) :- atom(A),
        %% C:やC:抜きで入っている単体の語をひろう
        %format('DEBUG A = ~w ~n',[A]),
        ws_memory(M),
        ws_pushglobal(ws_prewords,A,M),fail.

% 左がリストになっている場合
ws_getoriginal([H|_],Out) :-
        ws_getoriginal(H,Out).
% 右がリストになっている場合
ws_getoriginal([_|[T]],Out) :-
        ws_getoriginal(T,Out).

%% -----------------------
%% ws_listncomp/3
%% -----------------------
%% リストのN番目までリストを比較する
ws_listncomp(_,0,_).
ws_listncomp([H0|T0],N,[H1|T1]) :-
        N > 0,
        N_1 is N-1,
        H0 == H1,
        ws_listncomp(T0,N_1,T1),!.        

このプログラムの末尾に、先のwikipediaのprolog二分木をくっつけるか、別ファイルにしてそれぞれを読み込む必要がある。プログラムは、何日もかけて改訂しているもので、説明する気が起きないくらい複雑なものだ。

実行例は次のようになる。

?- ['verb.swi'].
true.
?- ws_testverb.
PreWD = [小田,を] EndWD = 含む/含む 
true ;
PreWD = [含む,4名,は,放送,が] EndWD = 終了する/した 
true ;
PreWD = [終了した,ばかりの,アニメ,機動戦士ガンダム,に] EndWD = 熱中する/して 
true ;
PreWD = [熱中しており,まだ,ガンプラ,が] EndWD = 発売する/さ 
true ;
PreWD = [発売される,前,から,同作品,に] EndWD = 登場する/する 
true ;
PreWD = [登場する,ロボット兵器,モビルスーツ(MS),の,模型,を] EndWD = 自作する/して 
true ;
false.
?- ^D

先のプログラムを verv.swiとして、swi-prologに読み込んで、実行している。

PreWDは、先行語(ノード値と左右葉の語)、動詞に先行するフレーズであり、プログラム上、10語までのものを取り出す設定にしている(ws_memory(10).で定義されている)。その後に、動詞の原形という(EndWD)終了後で、部分文章は閉じるようになっている。先行語はどこまでが構成要素になるかは、柔軟に考えれば良い。基本、最低、前の二つの語を採用すればいいだろう。

最初に、「小田を含む」という自立したフレーズ、部分文章、部分知識を取り出す。次が「終了したばかりのアニメ機動戦士ガンダムに熱中する」、「ガンプラが発売する」は文章的には少し変になっている、そして「同作品に登場する」、最後は「ロボット兵器、モビルスーツ(MS)の模型を自作する」となる。

一つの文章からはこのような部分文章を引き出せるが、wikipediaとtwitterの膨大なデータを用いて、これを会話の中に適合的なフレーズに鍛錬する必要がある。

次に、文章構成の基本的な手続きを再び確認したい。