prolog 二分木作成の改良

wikipedia二分木検索例の出力の冒頭文がおかしい。つまりこんな風になっている。

wiki_1_line_3385_2: ロボットは/いわゆる脳を持た関わらないにもずまるで/生きているかのように行動する//

「持たないにも関わらず」が「持た関わらないにもず」になっている。この原因は、この行の二分木が、

jawiki(wiki_1_line_3385_0,
    node(の,
        node([],
            node(と,
                node([],
                    node(に,
                        これ,
                        [基づいた, 基づく]
                    ),
                    ゲンギス
                ),
                [[呼ば, 呼ぶ], れる]
            ),
            [[[六, 数量], 本], [足, '動物-部位']]
        ),
        node(は,
            [ロボット, '人工物-その他'],
            node(を,
                node(いわゆる,
                    [],
                    [脳, '動物-部位']
                ),
                node('にもず',
                    [[[持た, 持つ], ない], [関わら, 関わる]],
                    node('か|の',
                        node([],
                            まるで,
                            [[生きて, 生きる], いる]
                        ),
                        node(に,
                            よう,
                            node([],
                                [[行動, 抽象物], する],
                                [ ]
                            )
                        )
                    )
                )
            )
        )
    )
).

という感じで、nodeの語が「にもず」になっているからだ。その理由は、knpの出力にまで遡る。その該当箇所のknpの出力は、

* 12D <否定表現><モ><ニ><読点><助詞><〜ない><〜ぬ><用言:動><係:連用><レベル:B><区切:3-5><ID:〜にもかかわらず><連用要素><連用節><動態述語><正規化代表表記:持つ/もつ><主辞代表表記:持つ/もつ>
+ 13D <否定表現><モ><ニ><読点><助詞><〜ない><〜ぬ><用言:動><係:連用><レベル:B><区切:3-5><ID:〜にもかかわらず><連用要素><連用節><動態述語><正規化代表表記:持つ/もつ><用言代表表記:持つ/もつ><時制-未来><格関係8:ヲ:脳><格解析結果:持つ/もつ:動3:ガ/U/-/-/-/-;ヲ/C/脳/8/0/1;ニ/U/-/-/-/-;デ/U/-/-/-/-;ヨリ/U/-/-/-/-;マデ/U/-/-/-/-;時間/U/-/-/-/-;外の関係/U/-/-/-/-;ノ/U/-/-/-/-;修飾/U/-/-/-/-;トスル/U/-/-/-/-;ニオク/U/-/-/-/-;ニツク/U/-/-/-/-;ニカンスル/U/-/-/-/->
持た もた 持つ 動詞 2 * 0 子音動詞タ行 6 未然形 3 "代表表記:持つ/もつ" <代表表記:持つ/もつ><正規化代表表記:持つ/もつ><かな漢字><活用語><自立><内容語><タグ単位始><文節始><文節主辞>
ない ない ない 接尾辞 14 形容詞性述語接尾辞 5 イ形容詞アウオ段 18 基本形 2 "代表表記:ない/ない" <代表表記:ない/ない><正規化代表表記:ない/ない><かな漢字><ひらがな><活用語><否定><付属>
に に に 助詞 9 格助詞 1 * 0 * 0 NIL <かな漢字><ひらがな><付属>
も も も 助詞 9 副助詞 2 * 0 * 0 NIL <かな漢字><ひらがな><付属>
関わら かかわら 関わる 動詞 2 * 0 子音動詞ラ行 10 未然形 3 "代表表記:係わる/かかわる" <代表表記:係わる/かかわる><正規化代表表記:係わる/かかわる><かな漢字><活用語><付属>
ず ず ぬ 助動詞 5 * 0 助動詞ぬ型 27 基本連用形 4 NIL <かな漢字><ひらがな><活用語><付属>

となっている。情報がいっぱい詰まっているが、基本*から始まるのが、一つのフレーズなのだが、ここでは、「持た」という動詞があり、「ないにも」という助詞系の語が続いて、さらに「関わら」という動詞が来て、さらに「ず」という助動詞が来ている。二つのフレーズにして良いものが、一つのフレーズの中に押し込められているのだ。助詞、助動詞はnodeにするというのが、この二分木の原則なので、「ないにも」と「ず」がつなげられてしまうのだ。

「持た」→「ないにも」→「関わら」→「ず」、となっているが、第1と第3の要素はleafになるべきで、第2と第4の要素はnodeになるべき要素なのだが、これらが一つのフレーズなので、leaf要素とnode要素が、それぞれ接合されてしまっているというわけだ。

そこで、面倒だったが、こういう場合には、二つのフレーズに分割するように、二分木を作成するプログラムを改良した。結果、次のように出力するようになった。

jawiki(wiki_1_line_3385_0,
    node(の,
        node([],
            node(と,
                node([],
                    node(に,
                        これ,
                        [基づいた, 基づく]
                    ),
                    ゲンギス
                ),
                [[呼ば, 呼ぶ], れる]
            ),
            [[[六, 数量], 本], [足, '動物-部位']]
        ),
        node(は,
            [ロボット, '人工物-その他'],
            node(を,
                node(いわゆる,
                    [],
                    [脳, '動物-部位']
                ),
                node(にも,
                    [[持た, 持つ], ない],
                    node(かの,
                        node([],
                            node(ず,
                                [関わら, 関わる],
                                まるで
                            ),
                            [[生きて, 生きる], いる]
                        ),
                        node(に,
                            よう,
                            node([],
                                [[行動, 抽象物], する],
                                [ ]
                            )
                        )
                    )
                )
            )
        )
    )
).

少しわかりにくいかもしれないが、「にも」と「ず」が二分木の中で分かれている。これでよし。

Prolog 二分木をWebから利用可能にした

日本語Wikipediaの全本文をWebから利用できるようにした。

Prolog 二分木検索の使い方

Javascript経由でブラウザから、データを受け取り、phpで、prologサーバーにアクセスし、prologサーバーがwikipediaの二分木データにアクセスし、回答すると言う手順になっている。

prologサーバーがメモリ上に展開し、wikipediaの二分木を咥え込むと、26Gバイトくらいになるので、VPSでは対応できないので、このサーバーだけ、自宅のPCにおいて、そこにアクセスするようになっている。自宅PCは、メモリを64G積んでいるので、なんとか対応できる。大学のPCも16Gなので、メモリエラーになってしまうのだ。

だから、自宅PCでサーバーを動かしている時しか検索できない。問い合わせいただければ、動かす時間をお知らせすることができる。

これで、prologでやろうとしたことの1クールが終わったことになる。

次は、twitterデータを使って、話し言葉でこれに対応してみたい。そして、もっと人間の言葉、日本語の言葉の知能的処理に挑戦したいと思っている。

CentoOSのVPSにswi-prologをインストールした

手続き的に言うと、この間、日本語wikipediaのデータをprologの二分木化して、それをprologサーバに処理させて、tcpクライアントからデータ検索できるようにすると言う作業をやっていた。なんのことやら、と思う方がいるかと思うが、こればっかりは、話が長くなるので、このサイトの過去の記事を読んでいただかないといけない。

さて、その7Gもある二分木もそれを検索するprologプログラムも、それを利用可能にするprologのtcpサーバーもできた。直にそれをtcpクライアントから利用してもいいのだが、何かと不便なのでウェッブで使えるようにした。

つまり、ブラウザから、javascriptのajax経由で、サーバーサイトのphpを起動して、tcpクライアントを動かして、検索し、その出力をブラウザに表示すると言うシステムはできたのだ。Macのwebサーバーとprologサーバーでこれが機能することを確かめた。Macはローカルなので、これをこのブログも動かしている「お名前.com」のVPS上において、どこからでも利用できるようにしようと言う段取りになった。

VPSは、なぜかCentoOSで動いている。今は、Ubuntuにすればよかったと思うが、入れ替えると、また色々1から作り直さないけない。一時期、私が管理するネットワークサーバーは、CentoOSと決めていたことがあって、その勢いでのこと。まあ、仕方がない。

このVPSにswi-prologを入れようとしてちょっと詰まってしまった。Linuxなので、ソースからコンパイルする必要がある。まず、cmakeのバージョンが古いと怒られた。cmake 新しくしようとすると、g++のバージョンが古いと怒られた。 g++のバージョンアップは、こちらのサイトを参考にインストールできた。さしあたって、

gcc version 4.8.2 20140120 (Red Hat 4.8.2-15) (GCC) 

ここまで、バージョンアップした(もっと新しいバージョンもあるが、そこまでは必要なかった)。さらに、この辺りかそこらで、zlibが見つからないと言うエラーもあったので、これは、こちらを参考に、簡単に最新バージョンをインストールできた。そして、cmakeを新たにコンパイルし直した。cmakeは、

cmake version 3.14.1

ここまで、バージョンアップした。さらに、swi-prologのインストールページにある、事前準備をしようとしたら(※ dnfをyumに変える必要があった)

エラー:  Multilib version problems found. ・・・・・・続く

と言うことで止まってしまった。経験的に、これがちゃんとインストールできていないと、swi-prologのコンパイルに失敗する。これはやや戸惑ったが、こちらにある通り対処したら、クリアできた。

このような過程を経て、無事、swi-prologはちょっと古びたCentOSにインストールできましたとさ。

(追記:結局VPSはメモリとパワー不足でprologサーバーを動かすことができなかった、笑)

swi-prologから、形態素解析のjumanを実行する

swi-prologに知識そのものを乗せることになるのだが、だとすると、それに対するアクセスのために、prologから、自然言語解析のツールを利用できた方がいいと考えた。

そこで、さしあたって、swi-prologで、jumanを利用した形態素解析をさせるプログラムを作成した。swi-prologのlibrary(process)マニュアルにあるサンプルを利用している。

jumanをサーバーモードで立ち上げる方が、効率的だが、その場合はswi-prologでクライアントを作成するという問題になる。が、ここでは直接jumanを起動する方法をとる。prologから外部コマンドを実行するのが、どのような手続きで実現するのかを確認する意味も込めている。

jumanには、pathが通っていなければ起動できない。

juman(String, Lines) :-
        setup_call_cleanup(
            process_create(path(juman), [  ],
                [ stdout(pipe(Out)), stdin(pipe(In))
                ]),
            proc_stream(In, Out, String, Lines),
            close(Out)).

proc_stream(In, Out, String, Lines) :-
        write(In,String),
        close(In),
        read_line_to_codes(Out, Line1),
        read_morpheme(Line1, Out, Lines).

read_morpheme(end_of_file, _, []) :- !.
read_morpheme(Codes, Out, [Line|Lines]) :-
        atom_codes(Line, Codes),
        read_line_to_codes(Out, Line2),
        read_morpheme(Line2, Out, Lines).

jumanは、標準入力から解析対象の文章を受け取るので、process_createのオプションにstdinを加えた。proc_streamでは、標準入力と標準出力を受け取って、まず、標準入力に文章を書き込む、それを閉じて、標準出力から結果を受け取る。read_morphemeは、end_of_fileまで一行ずつ受け取る(再帰的に繰り返す)。

実行結果は次のようになる。juman.swiが上に示したプログラムファイルである。

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

?- juman(今日はいい天気ですね,Lines).
Lines = ['今日 きょう 今日 名詞 6 時相名詞 10 * 0 * 0 "代表表記:今日/きょう カテゴリ:時間"', '@ 今日 こんにち 今日 名詞 6 時相名詞 10 * 0 * 0 "代表表記:今日/こんにち カテゴリ:時間"', 'は は は 助詞 9 副助詞 2 * 0 * 0 NIL', 'いい いい いい 形容詞 3 * 0 イ形容詞イ段 19 基本形 2 "代表表記:良い/よい 反義:形容詞:悪い/わるい"', '天気 てんき 天気 名詞 6 普通名詞 1 * 0 * 0 "代表表記:天気/てんき カテゴリ:抽象物"', 'です です だ 判定詞 4 * 0 判定詞 25 デス列基本形 27 NIL', 'ね ね ね 助詞 9 終助詞 4 * 0 * 0 NIL', 'EOS'].

出力がリストで獲得できる。

知識をクライアントに送付するprologサーバー

prologの二分木化された日本語wikipediaの出力は、TCPサーバーでやろうとしているわけだが、そのためにはprologでサーバーを立ち上げなければならない。クライアントとのマルチバイト文字のやりとりで手間取ったが、それもなんとかクリアしたので、現状をここにアップしておく。

まず、prologのTCPサーバーは、swi-prologのマニュアルにあるものを参考に以下のように作成する。

%%%%
%% 知識データセクション、簡単な例
%%%%
animal(ライオン,肉食動物).
animal(キリン,草食動物).

%%%%
%% TCPサーバーセクション
%% 参照:
%% http://www.swi-prolog.org/pldoc/man?section=stream-pools
%%%%

%% 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, AnimalCodes),
    %% 入力ストリームを閉じる
    close(In),
    %% バイトシーケンスを文字列に変換する
    utf8tring(AnimalCodes,Animal),
    %% 受け取った文字列の表示 
    write(Animal),nl,
    %% 知識のユニフィケーション           
    animal(Animal,Category),
    %% flush_output(),
    %% 文字列をバイトシーケンスに変換する
    utf8tring(CategoryCodes,Category),
    %% サーバーコンソールへの表示
    format('~s ~s  ~n', [Animal,Category]),
    %% フォーマットされた文字を出力ストリームに書き込む
    %% 与える文字列は、バイトシーケンスのリスト化されたものでなければならない
    %% ~sはCでいう %s 、 ~n は改行
    format(Out, '~s is ~s  ~n', [AnimalCodes,CategoryCodes]), 
    %% 出力ストリームを閉じる
    close(Out),
    %% プールからストリームを削除する
    delete_stream_from_pool(In).

冒頭の知識は、超簡単なものだが、日本語wikipediaの場合は、別ファイルになる。

このサーバーで最も困ったのは、ストリームの送受信は、すべてutf8のバイトシーケンスになっていることだった。ascii文字は、そのままでも良いが、マルチバイト文字に対応していない。前の記事で書いた日本語とバイトシーケンスを相互に変換するprolog手続きを作成しなければならなかった。Qittaには、片方向のものをあげたが、双方向に変換できるprologプログラムは、以下のようになる。

%%
%% utf8のbyte sequenceをprolog内の文字列に変換する
%% 逆に、文字列をutf8のバイトシーケンスに変換する
%% 

%% (例)
%% byte sequence
%% ライオン => [227,131,169,227,130,164,227,130,170,227,131,179]
%% lion => [108, 105, 111, 110]
%% アスキーとマルチバイトの混合
%% ラlイiオoンn = [227,131,169,108,227,130,164,105,227,130,170,111,227,131,179,110]
%% codepoint
%% ライオン => [12521, 12452, 12458, 12531]

%% TOPレベルpredicate, 双方向
%% utf8のバイトシーケンスを文字列に変換する
%% asciiが終わってマルチバイトも調べないために、最後にカットを入れている
%% 何れにしても、ここでバックトラックは不要なのでつけておく
%% 途中経過のコードポイントリストも出力する
utf8tring(L,X) :- var(X),getcodepoint(L,Y),
                         write(Y),nl,atom_codes(X,Y),!.
%% 文字列から、utf8のバイトシーケンスを取得する
utf8tring(S,X) :- var(S),atom_codes(X,L),
                         getutf8sequence(L,S),write(S),nl,!.

%% 再帰の終了条件
getutf8sequence([],[]).
%% コードポイントリスト(L), utf8バイトシーケンス(S)
%% アスキー文字の場合
getutf8sequence(L,[A|X]) :- [A|T] = L, A < 256, 
                         getutf8sequence(T,X). 
%% マルチバイトの場合 
getutf8sequence(L,Z) :- [M|T] = L, isoutf8(M,X), 
                         getutf8sequence(T,Y), append(X,Y,Z). 
%% マルチバイト文字のコードポイントをutf8の3バイトに変換する 
isoutf8(X,[C1,C2,C3]) :- C1 is X >> 12 /\ 15 \/ 224,
                         C2 is X >> 6 /\ 63 \/ 128,
                         C3 is X /\ 63 \/ 128.

%% 再帰の終了条件
getcodepoint([],[]).
%% アスキー文字の場合
getcodepoint(L,[H|X]) :- [H|T] = L
                ,isascii(H),getcodepoint(T,X).
%% utf8マルチバイトの場合
getcodepoint(L,[X1|X2]) :- headthree(L,Y,T),
                utf8iso(Y,X1),getcodepoint(T,X2).

%% マルチバイトの場合、頭の3個を取り出して、残りをTに入れる
headthree(L,[X1,X2,X3],T) :- [X1|T1]=L,
                [X2|T2]=T1,[X3|T]=T2.

%% 最高bitが0ならば、アスキー文字
isascii(A) :- (A >> 7) =:= 0.

%% 3バイトのutf8コードをコードポイントに変換する
%% 00001111 -> 15
%% 00111111 -> 63 
%% nth1 はリスト(L)の指定位置(N)の要素を取り出す(X) 
utf8iso(L,X) :- nth1(1, L, Y1), Z1 is 15 /\ Y1,
                nth1(2, L, Y2), Z2 is 63 /\ Y2,
                nth1(3, L, Y3), Z3 is 63 /\ Y3,
                X is Z1 << 12 \/ Z2 << 6 \/ Z3. 

こちらのプログラムもサーバープログラムと同時に読み込まれなければならない。その後に、ポートを決めてサーバーを立ち上げる。

テスト用に、クライアントプログラムをjavaで作成した。以下に参考例と示しておく。TCPに対応するクライアントならば、どのようなものでも良い。telnetでもいけると思う。

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 = 20000;
            Socket soc = new Socket(server, port);

            OutputStream os = soc.getOutputStream();
            PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os,"UTF-8")));

            //pw.print("ライオン\r\n");
            //pw.flush();
            pw.print("キリン\r\n");
            pw.flush();

            InputStream is = soc.getInputStream();
            BufferedReader in = new BufferedReader(new InputStreamReader(is));
            String line;
            while((line = in.readLine()) != null){
                System.out.println(line);
            }
            pw.close();
            os.close();
            in.close();
            is.close();
        }catch (IOException e) {
            System.out.println("Exception: " + e);
        }
    }
    
}

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

%% サーバー(prolog)側の端末

?- ['server.swi','utf8string.swi'].
true.

?- server(20000).
[12461,12522,12531]
キリン
[232,141,137,233,163,159,229,139,149,231,137,169]
キリン 草食動物  
[12521,12452,12458,12531]
ライオン
[232,130,137,233,163,159,229,139,149,231,137,169]
ライオン 肉食動物  

// クライアント(java)側の端末
キリン is 草食動物
ライオン is 肉食動物  

サーバー側には、変換過程を示すために、バイトシーケンスとコードポイントのリストが表示されている。サーバーは、port番号20000番で開いている。優先的に利用されていないポート番号ならんば、何番でも良い。また、ネットワークにつながっているクライアントであれば、どこからでもprologが処理する知識を獲得できるわけである。

prologで、utf8のバイトシーケンスをコードポイントに変換する

prologで書いた、日本語wikipediaのデータを他から参照するために、prologでサーバーを用意するのが最も便利だと判断している。

そのprologサーバーを動かそうとしているのだが、文字コードのところでつまずいて週末を悶々としていた。結局、なんとか切り分けた問題は、次のようなものだ(これにも時間がかかった)。

例えば、クライアントからある文字列をutf8で送ったとすると、swi-prologのサーバーは、それをバイト列のストリームで受け取る。しかし、prologの内部では、アスキー文字は、そのストリームを処理して文字列に自動変換するのだが、日本語などのマルチバイト文字は変換しないので、日本語の宣言文などとユニフィケーションさせても一致すべきものが一致しなくなるという厄介なことが発生するのだ。

ただ、atom_codesという関数を使えば、文字列とUnicodeのコードポイントの相互変換は可能である。つまり、次のような感じである。

?- atom_codes(ライオン,X).
X = [12521, 12452, 12458, 12531].

?- atom_codes(X,[12521, 12452, 12458, 12531]).
X = ライオン.

ここで、数字のリストは、コードポイントのリストである。

クライアントからprologのサーバーに「ライオン」という文字列を送ると、次のようなリストとして受け取る。

[227,131,169,227,130,164,227,130,170,227,131,179]

227から始まる、3バイト分が一文字になっている。この文字列を先のコードポイントに変換できれば、文字列になるのだ。3バイトを16進に変換するのは、

?- hex_bytes(X,[227,131,169]).
X = e383a9.

で、できるので、最初は、このutf8コードとコードポイントの相互変換データをprologに組み込んで、変換することを考えたが、7000行以上の宣言文を咥えこまなければならないので、とても負担感がある。そこで、数量的変換のアルゴリズムが、こちらに解説されていたので、それを元に、変換のための規則を作ってみた。

utf8コードの3バイトものだけだが、次のように簡単になる。nth1はインデクス番号の要素を取り出す組込述語。

%% 00001111 -> 15
%% 00111111 -> 63 
utf8iso(L,X) :- nth1(1, L, Y1), Z1 is 15 /\ Y1,
                nth1(2, L, Y2), Z2 is 63 /\ Y2,
                nth1(3, L, Y3), Z3 is 63 /\ Y3,
                X is Z1 << 12 \/ Z2 << 6 \/ Z3. 

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

?- utf8iso([227,131,169],X). % ラ
X = 12521 

?- utf8iso([227,130,164],X). % イ
X = 12452 

あと、より完全なものにするためには、半角アスキーコードと3バイト文字列を識別するようになればいい。

その完成バージョンをQiitaに投稿した。
swi-prologで、utf-8のバイトシーケンスをコードポイントリストに変換し文字列にする

日本語wikipedia、prolog化の現状とプログラム

以下、Qittaに書いたものだが、そちらを削除してこちらに持ってきた。書いた日は、3月22日だった。

%%%%%%%%%%%%

wikipediaの本文全体をprologの宣言文(二分木)にするということで、作っている最中だ。Mac Proの100スレッドで動かしているが、4,5日はかかる。形態素解析のjumanも係り受け解析のknpもスレッドの数だけ、ポートを変えてサーバーモードで動かしているが、一日経って、25パーセントが死んでしまった。ロードアベレージは100近くになっている。

死亡スレッドがこれ以上増えて、CPUコア、スレッドを使いきれなくなったら、立ち上げ直さなくてはいけない。

でも、まあ、これはこれで、何度も試みていることなので、そのうち全部が出来上がるだろうと思っている。ちなみに、このprologの宣言文(二分木)を作成するプログラムは、javaで書いてある。

二分木

二分木は、prologの宣言文(事実とも言われる)、から構成され、基本的に助詞的な補助語がノードに来て、二つのリーフは語になっている構造だ。例として、wikipediaの「芸人」に関する定義と、雪国の冒頭の一節を二分木化したものを以下に掲げておこう。

plsample(line0_0,
    node(とは,
        [芸人, 人],
        node(または,
            node(いる,
                node(に,
                    node(の,
                        node(や,
                            node(の,
                                なんらか,
                                [技芸, 抽象物]
                            ),
                            [芸能, 抽象物]
                        ),
                        [道, '組織・団体;場所-施設;場所-その他']
                    ),
                    [通じて, 通じる]
                ),
                [人, 人]
            ),
            node(や,
                node([],
                    node(に,
                        [身, '動物-部位'],
                        [備わった, 備わる]
                    ),
                    [技芸, 抽象物]
                ),
                node(を,
                    [芸能, 抽象物],
                    node([],
                        [もって, もつ],
                        node(と,
                            [職業, 抽象物],
                            node([],
                                する,
                                node(の,
                                    [人, 人],
                                    node(を,
                                        こと,
                                        node([],
                                            指す,
                                            node([],
                                                日本,
                                                node([],
                                                    特有の,
                                                    node(である,
                                                        [概念, 抽象物],
                                                        [ ]
                                                    )
                                                )
                                            )
                                        )
                                    )
                                )
                            )
                        )
                    )
                )
            )
        )
    )
).
plsample(line0_1,
    node(を,
        node([],
            node(の,
                [国境, '場所-その他'],
                長い
            ),
            [トンネル, '場所-施設']
        ),
        node(と,
            抜ける,
            node(であった,
                [雪国, '場所-その他'],
                [ ]
            )
        )
    )
).

わかりやすく、ノード毎に行を変えているが、本番では、一つの文章は一行にパッキングされる。二分技は、ルートをどれにするかによって構造が変わってしまうが、文章の区切りや、knpの係り受け解析の結果、接続詞や助詞の強さによって決定される。

jumanとknpが名詞のカテゴリ情報を与えていれば、それを一対のリストとして付随させる。動詞の原形が表示語と違っていれば、それも一対のリストにして入れ込む。knpが一つの句に名詞や動詞を複数入れれば、それを入れこのリスト構造にして入れる。

作成の現状

2019年1月時点の日本語wikipediaをテキストファイルに変換したら、544のファイル(一つ10M程度)ができた。現時点で、prolog化できているのは、200個程度であり、半分に行っていない。(2019年3月22日朝の段階で、268個、全体544個の49.3%である)

現在できているファイルの539番目のファイル(1スレッドが数個のファイルを担当するので、539番目までできたという意味ではない)をswi prologに読み込ませた。100個程度読み込ませると、読み込む時間が12分くらいになる。wikipedia全体ができて、それを読み込ませると1時間かかる計算だが、swi prologは、読み込ませたのち、内部形式に変換したものを出力できるので、それを再度読み込ませる時間は、もっと短いと思っている。

検索ルール

何れにしても、その539番目のファイルを読み込ませた。追加的に読み込ませた検索ルールは次のようなものである。

%%
%% 日本語wikipediaの二分木を探索し、表示する
%%

%% 助詞付きの探索
%% 助詞なしの検索の拡張。Vが拡張した要素
%% 助詞なしの場合に詳細なコメントをつけたのでそちらを参照して
rsearch(S,V,node(V,B,S),V,B).
rsearch(S,V,node(V,B,L),V,B) :- getmember(S,L). %% リストの場合の処理
rsearch(S,V,node(_, Y, _), A, B) :- rsearch(S,V,Y,A,B).
rsearch(S,V,node(_, _, Z), A, B) :- rsearch(S,V,Z,A,B).

lsearch(S,V,node(V,S,B),V,B).
lsearch(S,V,node(V,L,B),V,B) :- getmember(S,L).
lsearch(S,V,node(_, Y, _), A, B) :- lsearch(S,V,Y,A,B).
lsearch(S,V,node(_, _, Z), A, B) :- lsearch(S,V,Z,A,B).

right(X,V) :- jawiki(P,T),rsearch(X,V,T,A,B),write(P),write(': '),printnode(B),printnode(A),printnode(X),nl,fail.
left(X,V) :- jawiki(P,T),lsearch(X,V,T,A,B),write(P),write(': '),printnode(X),printnode(A),printnode(B),nl,fail.

%%% 助詞なしで、語だけ与え、左右の葉っぱを検索する
% 右葉の探索
% 見つけたら、その語以外(AおよびB)を取得する
rsearch(S,node(A,B,S),A,B).
% リストの場合も受け入れる
rsearch(S,node(A,B,L),A,B) :- getmember(S,L). %% リストの場合の処理
% 上で一致しなかったら、左右のノードの内側を調べる
rsearch(S, node(_, Y, _), A, B) :- rsearch(S, Y, A, B).
rsearch(S, node(_, _, Z), A, B) :- rsearch(S, Z, A, B).
% 左葉の探索:基本右葉と同じ
lsearch(S,node(A,S,B),A,B).
lsearch(S,node(A,L,B),A,B) :- getmember(S,L).
% ここは、右と全く同じになる
lsearch(S, node(_, Y, _), A, B) :- lsearch(S, Y, A, B).
lsearch(S, node(_, _, Z), A, B) :- lsearch(S, Z, A, B).

%% 直下のリストのHeadに入っていればそれでよし
getmember(X,[X|_]).
%% アトムになったら失敗 
getmember(_,[H|_]) :- atom(H),fail.
%% 直下になければ、その直下のHeadのリストの下に無いか再帰的に調べる 
getmember(X,[H|_]) :- getmember(X,H). 

%%% 検索のメインclauses
% 検索語を右側にした部分文章
right(X) :- jawiki(P,T),rsearch(X, T, A, B),write(P),write(': '),printnode(B),printnode(A),printnode(X),nl,fail.
% 検索語を左側にした部分文章
left(X) :- jawiki(P,T),lsearch(X, T, A, B),write(P),write(': '),printnode(X),printnode(A),printnode(B),nl,fail.

%%% 出力のclauses
% 対象がatomならば、そのまま表示
printnode(N) :- atom(N),write(N).
% 対象が空でないリストならば、最初の項の表示
printnode(N) :- [_|_] = N,showlist(N). 
% 対象が空リストならば'/'(半角スラッシュの表示)
printnode(N) :- [] = N,write('/'). %% 空リストでもtrueにする
% 対象が項ならば、元の言葉の順序で表示(語が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
printlist(L) :- atom(L),write(L).
%% ベタなリスト化は、以下のclauseで単純に作れる
printlist(L) :- [H|[T]] = L,printlist(H),printlist(T),!.

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

ただし、これを、先のサンプルに適応するためには、プログラム中の'jawiki'を'plsample'に変更する必要がある。実行例は以下のようなものである。swiplを立ち上げて実行する。

?- ['jawiki-latest-pages-articles-539.swi','jawikirule.swi']. 
true. 
2 ?- left(ロボット,は). 
wiki_539_line_25554_0: ロボットは人間の腕に似た/働きをする/メカニカルアームの一種であり通常はプログラミング可能である// 
wiki_539_line_25556_1: ロボットは溶接や組み立て中の部品の回転や設置などのいろいろな/タスクを行う// 
wiki_539_line_25567_0: ロボットは使われてきた//

これは、ファイルの二分木中の左の葉っぱに、「ロボット」があり、ノードの助詞が「は」であるような、文節を二分木から取り出したものである。

少し説明すると、1では、wikipediaの539番目のファイルを二分技したファイルを読み込み、さらに、先のルールも読み込んでいる。2で、左の葉っぱにロボット、助詞が「は」の節の検索をかけている。wikipediaの544ファイルの一つのファイルだけなのだが、取り出された部分知識は、どれもロボットに関する知識として、意味を感じさせるものである。

カテゴリはまだ使っていない。いずれ、使わなくてはならなくなる。

おわりに

日本語wikipedia全体の544ファイルの一つのファイルに、これだけ意味のある文節があったことには、少し驚いた。


コマンドラインでMacのCPU温度をチェックする iStats

大学にあるMacproをsshでリモートコントロールして、WikipediaのProlog二分木化をやっている。途中で死んでしまうスレッドが目立つようになったので、今朝、思い切って200スレッド立ち上げて実行してみた。

現在作成済みのwikipediaファイルは309で全体544の56.8%まできている。しかし上のスレッド問題のために、丸1日かけて50個くらいしか増えなくなっているので、思い切って200スレッドを並列に動かしてみたわけだ。これで100スレッドが死んでも、1日で100個できるという、皮算用だ。(※ 結局、このもくろみは失敗した。逆にほとんどのスレッドが1日で死んでしまった、笑)

ただ、困ったのは、今見るとロードアベレージが150を超えている。一方、CPU負荷は、90数パーセントとこれまでと同じくらいなのだ。係り受け解析KNPは、ポート番号を変えてスレッド数だけ立ち上がる仕組みになっているが、これがsleepしていない、つまりrunning状態のものを

ps aux | grep knp | grep R | wc

というコマンドで確認すると、141個になっている。動かして数時間だが、すでに50スレッド以上が死んでしまっているわけだ。

なぜスレッドが死ぬのか、KNPとのTCPIPのプロトコルによる通信に何らかの障害が発生し、KNPがレスポンスを返さなくなると、スレッドがsleepしてしまうので、その辺の問題だろうと思っているが、実は詳しく調べていない。まずは、できるものをどんどん作成していくのが先だと思っているからだ。

(必ずしも死んだとは限らない。200スレッドということは、一つのスレッドが受け持つファイルはせいぜい3個である。全体で544個しかないのだから。そのうち300余は、完成済みなので、担当したファイルが全て作成済みの場合もある。ただ、ログを見る限り何もなくなり終了したスレッドのメッセージは見つからない)

本題になかなか入らなかったが、このように、高負荷で大学のマックを動かしていて、CPUが過熱状態にならないかが心配なのだ。

だから、本当は大学に出かけて、MACの様子を見ようかと思ったが、行ったところで、MACの筐体に触って、熱さを確かめるくらいなので、ならば、リモートで、CPUの温度くらいは確かめられるだろうと思った。

istatsというのがある。

sudo gem install iStats

で、インストールできる。istatsというコマンドをターミナルから打てば結果を出す

という結果で、この温度だと、大丈夫そうだった。

日本語wikipediaをprologの二分木で処理するパフォーマンス

現在、日本語wikipediaの50%を二分木化したが、そのデータを使って、およそのパフォーマンスがわかってきた。この段階でのファイルサイズは、3.68Gバイト。つまり、もし全てのwikipedia本文を二分木化すると7Gバイトになるということだ。

これをswi-prologで読み込ませると(ルールファイルも読み込ませたが、こちらはサイズはとても小さい)、読み込みに約30分かかる。これは、前にも予想した通りだ。そうすると、swi-prologの実行プログラムのメモリ占有量が15Gバイトくらいになる。これは確かに大きい。私のMac Proは64ギガ積んでいるので、それから見ると余裕がまだあるのだが、大きいことは間違いない。

それで、例えば、「ロボット」という語を「は」という助詞付きで検索させると、次のような感じの結果を303行出力する。途中省略している。

1 ?- left(ロボット,は).
wiki_1_line_3385_2: ロボットは/いわゆる脳を持た関わらないにもずまるで/生きているかのように行動する//
wiki_1_line_28713_0: ロボットはそれまでのリアルロボットアニメのデザインとは一線を画した/斬新な/ものであった/
wiki_2_line_62217_1: ロボットはボートに4体の演奏人形が乗った/もので宮廷のパーティで池に浮かべて/音楽を演奏したと言われている/
4の演奏人形が乗った/もので宮廷のパーティで池に浮かべて/音楽を演奏したと言われている/
wiki_3_line_31429_2: ロボットは胸部から放つ/ビームのシグナルフラッシュ//
wiki_3_line_44126_0: ロボットは独自に/考案した/ものであると説明していた/
wiki_3_line_69544_0: ロボットは鉄神/てつじんと呼ばれサイズの大きい/順に/G~Aクラスの等級が付けられている/
G~Aの等級が付けられている/
・・・・・・
・・・・・・
・・・・・・
・・・・・・
wiki_508_line_38121_0: ロボットは補高部を取り除いた/構成となっている/
wiki_510_line_87051_1: ロボットはアッセイプレートを移動させたりサンプルおよび試薬を添加/混合/インキュベーションをしたりして最終的に検出のための場所に動かす/ことをする//
wiki_517_line_78404_1: ロボットはスーパー戦隊シリーズとしてはパワーレンジャーも含めても初の試みとなる//
wiki_526_line_43826_0: ロボットは2体/いて/青が太郎/赤が花子という/名前がつけられている/
2/いて/青が太郎/赤が花子という/名前がつけられている/
wiki_532_line_38273_2: ロボットは据え置き型が多かったがFetchは障害物などを避けながら自律的に移動できる/特徴がある//
wiki_532_line_38273_4: ロボットは長さが約60cm/7自由度を持ち/6kgまでの商品をピックアップ/可能//
7自由を持ち/6kgまでの商品をピックアップ/可能//
wiki_539_line_25554_0: ロボットは人間の腕に似た/働きをする/メカニカルアームの一種であり通常はプログラミング可能である//
wiki_539_line_25556_1: ロボットは溶接や組み立て中の部品の回転や設置などのいろいろな/タスクを行う//
wiki_539_line_25567_0: ロボットは使われてきた/

この303行を出力するのは約1分で、1秒あたり5行出力していることになり、検索は、超高速だ。

読み込んだ日本語wikipediaの二分木をswi-prologのqsave_programで保存する。保存は、数分しか狩らなかったと思う。ここだけは計っていない。何しろ、短時間で保存できたのだ。保存した結果のバイナリファイルは、2.5Gバイトだった。

これは、実行形式になっているので、そのまま(swi-prologを開始することなく)実行できる。コマンドを打って、prologのプロンプトが出るまで、40秒しかかからなかった!!超早い。巨大なデータも、一旦、バイナリ形式(swi-prologの内部表現形式)に変換して保存すると、超高速で実行可能な状況になるのだ。

十分実用範囲だ。