経済学が想定する人間と評価関数のこと

コンピュータが自然言語を構成することを想定すると、コンピュータが目的とか意欲とか意図を持たなければならないとつくづく思う。単純に、意欲という言葉で表そう。いろいろなレベルの意欲というのがある。何かを話そうというか、話し言葉を構成しようとするときには、これを相手に伝えたいという意欲が大事な働きをするはずだ。それは、どのように表現するのかを少し考えてみる。

何を隠そう、私が生業としているのは、経済学である。博士号も、経済学でとっている。大学は工学部で、情報を専門としていた。共通しているのは、数学をツールとするところか。まあ、いろいろあった。

経済学は理論的には、人間を一個の経済評価をする機械のように考えている。多次元の効用関数を持っているのだ。その効用関数で見て、より評価値の高いものを求めて「選択」をする機械が経済学が見る人間なのだ。

が評価関数としての効用関数である。という変数の値によって評価値が変わる。

これが経済学の想定する人間の「意欲」の源泉なのである。ロボットの場合、は、外界からの反応(たとえば、センサー値だったり)、何らかの内部状態の値だったりするのだろう。

言葉を構成するときにも、このような評価関数を持っていなければならないのではないかと思ったのだ。

 

日本語というコンピュータ言語とコンピュータの意思ないしは意欲について

そういう意味では、自分がやりたいのは、日本語というコンピュータ言語を開発したいということなのかもしれない。

通常コンピュータ言語は、人間がある目的、意図を実現するために組み立てるものである。が、その日本語というコンピュータ言語はコンピュータ自身がその意図を実現するために組み上げるものなのだ。もちろん、文法をコンピュータが理解していなければならない。

ただ、そこで、一番かけているのは、コンピュータの意図や意識なのかもしれない。

人は、何か、他者に伝えたいという意思や意図を持つから、それをなんとか言葉で表現しようとする。日本語で無口な人間は、英語でも無口なように。表現したいという意欲や意図がなかったら、言葉は出てこない。

コンピュータがそうした意欲や意図を持つとはどういうことなのだろうかと思う。まず、それから始めるのが妥当なのだろう。

日本語の文法について

少し考え違いをしていたのかもしれないと思う。

ある日本語の文章の意味を捉えようとするとき、何かしら文法が役に立つべきだと考えていた。確かに言葉や文章は文法によって構成されているので、その考えに間違いはないだろう。私たちは、何かを喋ろうとするときに、あるいは文章を書くときに、その言葉が文法に適ったものになっているはずだと思っている。

ただ以外に、喋った後で、文章を書いた後で、この言葉、語順では意味が伝わらない、あるいは誤解されたりすると思って、言い換えたり、文章を書きなおしたりする。聞いたり、読んだりするときも同じだ。

ふと思ったのは、言葉の意味を理解するときの文法の役割と意味を伝えようとするときの文法の役割は違うのではないだろうかということだ。

我々は、文章が、言葉が与えられるとその意味を文法に沿って理解していると思う。それは正しいのだが、そのとき、文法の役割はそれほど決定的ではない。英語を話したり聞いたりするときのことを考えれば良い。自分の英語は、文法的に結構間違っているが、ちゃんと伝わっていると思うときが多い。相手も、単語をいくつか聞き取ると、それだけで意味が伝わったと判断しているのだ。

文章の意味を理解しようとするときは、文法も大事だが、状況やイメージもとても大事なのだ。

逆に、意味やイメージを伝える時には、なるべく単純な文法で、必要な単語を構成し、文章、言葉を組み立てれば良い。意味やイメージの伝え方は、多様に存在する。その中で、最も基本的で単純なものを選んでも、伝えるという基本的な機能においては遜色はないのだと思う。

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というコマンドをターミナルから打てば結果を出す

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