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ファイルの一つのファイルに、これだけ意味のある文節があったことには、少し驚いた。