「雪国」冒頭の文章とprolog

前の記事まででやった、文章のprolog化、二分木化する場合に、もっといろいろ考えるべきことがあるのに気づいた。

題材として、川端康成「雪国」の冒頭の一文をprolog化しよう。

まず、cabochaで、形態素解析および構文解析しておこう。

* 0 2D 0/1 1.211902
国境	名詞,一般,*,*,*,*,国境,コッキョウ,コッキョー
の	助詞,格助詞,一般,*,*,*,の,ノ,ノ
* 1 2D 0/0 2.895743
長い	形容詞,自立,*,*,形容詞・アウオ段,基本形,長い,ナガイ,ナガイ
* 2 3D 0/1 1.600771
トンネル	名詞,一般,*,*,*,*,トンネル,トンネル,トンネル
を	助詞,格助詞,一般,*,*,*,を,ヲ,ヲ
* 3 4D 0/1 1.600771
抜ける	動詞,自立,*,*,一段,基本形,抜ける,ヌケル,ヌケル
と	助詞,接続助詞,*,*,*,*,と,ト,ト
* 4 -1D 0/3 0.000000
雪国	名詞,一般,*,*,*,*,雪国,ユキグニ,ユキグニ
で	助動詞,*,*,*,特殊・ダ,連用形,だ,デ,デ
あっ	助動詞,*,*,*,五段・ラ行アル,連用タ接続,ある,アッ,アッ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
EOS

英語のような明確な主語を持たない、日本語らしい文章だ。1フレーズの形容詞句を除いて、全て、助詞か助動詞があり、フレーズをシーケンシャルに結合させている。*のラインの第2項に示されている2Dなどが示している係り受け解析は、単純に語順を再現しているだけである。雪国を含むフレーズにある、-1D は、その句が、それ以上、どこにもかかっていないことを示している。

そこで、これを前と同様にprologの二分木に変更してみよう。ただし、ここでは、4バージョンの二分木があるのを全て書き下すことにする。(前の記事までの芸人の定義では、一つの直感的prolog化だけを用いた)

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 川端康成『雪国』冒頭
%% 「国境の長いトンネルを抜けると雪国であった」
%% 一行で表示する場合をコメントで示したのちに、インデント付きで表示する
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% (1)
%% 一行表示
%% yukiguni1(node(の, 国境, node(を, [[長い], トンネル], node(と, 抜ける, node(で, 雪国, あった))))).
%% インデント表示
yukiguni1(
    node(の,
        国境,
        node(を,
            [[長い],トンネル],
            node(と,
                抜ける, 
                node(で,
                    雪国,
                    あった
                )
            )
        )
    )
).

%% (2)
%% yukiguni2(node(を, node(の, 国境, [[長い], トンネル]), node(と, 抜ける, node(で, 雪国, あった)))).
yukiguni2(
    node(を,
        node(の,
            国境,
            [[長い],トンネル]
        ),
        node(と,
            抜ける, 
            node(で,
                雪国,
                あった
            )
        )
    )
).

%% (3)
%% yukiguni3(node(と, node(を, node(の, 国境, [[長い], トンネル]), 抜ける), node(で, 雪国, あった))).
yukiguni3(
    node(と,
        node(を,
            node(の,
                国境,
                [[長い],トンネル]
            ),
            抜ける
        ),
        node(で,
            雪国,
            あった
        )
    )
).

%% (4)
%% 
%% yukiguni4(node(で, node(と, node(を, node(の, 国境, [[長い], トンネル]), 抜ける), 雪国), あった)).
yukiguni4(
    node(で,
        node(と,
            node(を,
                node(の,
                    国境,
                    [[長い],トンネル]
                ),
                抜ける
            ),
            雪国
        ),
        あった
    )
).

これをyukiguni.swiファイルとして保存し、swi-prologに問題なく読み込ませることができる。これでけでは少しイメージしにくいと思うので、二分木をノートに書いたのが以下の画像である。

4つのバージョンができることがややこしい。この違いついて議論する前に、フォーマットの若干の拡張について触れておく。

基本、名詞や動詞に直接かかる形容詞や副詞は、その名詞や動詞と一体のものとして扱う。cabochaの構文解析でも、「い」は助詞にもなっていない。

トンネルにかかる形容詞は、「長い」の他のも「暗い」などたくさんありうるので、かかる言葉をprologのリストとして扱う。例えば、それは [[長い,暗い],トンネル]でも良いわけである。トンネルは名詞で、ないと思うが、それが活用などの語形変化があれば、トンネルもリスト化すれば良い。そこは、「芸人」の定義分析でやったところである。

また、今回は、動詞の活用形が問題になる語が「あった」と「ある」しかなく、これは記述の簡単化のために無視した(本番では [あった, ある]のリストにすると思う)。

各バージョンについて見ていこう。

(1)cabochaによる構文解析ともっとも整合的なものがこのバージョンである。「雪国であった」というフレーズが、他のノードに依存しない独立化可能なロバスト(頑健:外部に影響されない)なものになっている(これは、(2)と(3)のバージョンに共通している。)。他に、ロバストなフレーズはない。もっとも際立った特徴は、この唯一のロバストなフレーズに、抜ける、長いトンネル、国境という名詞、動詞がシーケンシャルにかかっていることだ「雪国であった」というフレーズが、絶対的存在感を持つような構造になっている。さらに、全ての左の語から、言葉を始められることである。ちょっとわかりにくい言い方だが、先の芸人の記事において、検索は、右の語を探しに行って、一致したものがあったら、右の語を含めて文章化できるものをその下に向かって作成していった。右の語に独立して存在しなければ、見つけられない仕組みになっていた。しかし、この(1)バージョンでは、そのような独立した右の語は、「あった」しかなく、そこから構成できる文章は「雪国であった」だけだが、同じように左語からの検索と文章構成システムを用いると(それは、前回の記事の中にあるプログラムを若干改訂すればすぐにできる)、全ての部分文章(部分知識)が作り出されることになる。その意味するところは、全ての他のフレーズが「雪国であった」に従属しているのである。

(2)の特徴は、「国境の長いトンネル」というのが、ロバストなフレーズとして独立したことである。そして、「抜けると」というフレーズを接着剤にして「雪国だった」というメインフレーズとつなげられていることになる。

(3)は、(2)と比べて「抜けると」というフレーズが、「国境の長いトンネル」の側により強く繋がっていることが特徴だ。

(4)は、(1)の対称系になっている。「雪国」が単なる飾りのようになってしまった。右語からの検索で、全ての部分文章、部分知識が再現できる。

prologによる文章の知識化としてみたとき、この四つのバージョンのうちのどれを選ぶかが問題である。小説「雪国」冒頭としてみたとき、(4)はしっくりこない。

四つ全てを保持するのは、非効率であることは明らかなので、何らかの知識、言葉、文章を構成するときに、どの形で保持することが効果的かという基準になる。

もともと、このような情報の知識化をprologで行おうという動機になったのは、ロボットに知識を喋らせるときに、どうしても一つの文章を短く、縮約する必要があるのに、単文要約の適切なアルゴリズムが見つからなかったことである。ディープラーニングなどのコンテンポラリな手法にも、最終解決は託せなかった。その過程で読んだ論文に「大規模データを用いた日本語文圧縮」(長谷川駿氏他、2017)があって、その中で「を、に、が、は」は、かかり先文節の必須語になりやすい、と指摘されていた。

それは、言い換えれば、それくらい強い結びつきを作る言葉であるということは「そのような強さを持っていなければならないほど、それによって繋がる二つのフレーズは、相対的に強い独立性を持っている」ということではないか。

この意味をどう解釈するかは別にして、まず、助詞に強さがあることが興味深い。さらに、ここでのシステムに応じた表現に変えれば、これらの語は、二分木ツリーのルートに近いところにあるべきだと解釈しても良さそうである。実際、前節までで使っていたwikipedia芸人の定義は、私が直感的な判断で「芸人とは」の「とは」をルートにしていた。「とは」は「は」とほぼ同じか、それよりも強めの助詞である。

というようなことを考え合わせると、この雪国冒頭の文書の場合も、助詞「を」を二分木のルートとしているバージョン(2)を採用することが妥当のように思える。

wikipedia「芸人」の定義とprolog(3)リスト処理追加

先の記事の段階で無視した、prolog化した文書の中にあるリストを処理可能にした。

%%%%%%%%%%%%%%%%
% geinin3.swi
% 2019年1月26日
% リストの場合の処理を付加した
%%%%%%%%%%%%%%%%

%%% 元の文章:
% 芸人とは、なんらかの技芸や芸能の道に通じている人、
% または身に備わった技芸や芸能をもって職業とする人
% のことを指す日本特有の概念である
%%%

%%% 節(1)
search(S,node(A,B,S),A,B).
search(S,node(A,B,L),A,B) :- member(S,L). %% リストの場合の処理
search(S,node(A,B,S),A,B).
search(S, node(_, Y, _), A, B) :- search(S, Y, A, B).
search(S, node(_, _, Z), A, B) :- search(S, Z, A, B).

%%% 受動語を検索、表示(2)
passive(X) :- sentence(T),search(X, T, A, B),printnode(B),write(A),printnode(X),nl.

%%% ノードの表示(3)
printnode(N) :- atom(N),write(N).
printnode(N) :- [X|Y] = N,write(N). %% リストの場合の表示
printnode(N) :- node(X,Y,Z) = N,printnode(Y),write(X),printnode(Z).

%%% 文章データ(4)
sentence(
    node(とは,
        芸人,
        node(ところの,
            node(を,
                node(の,
                    node(または,
                        node(いる,
                            node(に,
                                node(の,
                                    node(や,
                                        node(の,
                                            なんらか,
                                            技芸
                                        ),
                                        芸能
                                    ),
                                    道
                                ),
                               [通じて,通じる]
                            ),
                            人
                        ),
                        node(とする,
                            node(て,
                                node(を,
                                    node(や,
                                        node(た,
                                            node(に,
                                                身,
                                                [備わっ,備わる]
                                            ),
                                            技芸
                                        ),
                                        芸能
                                    ),
                                    [もっ,もつ]
                                ),
                                職業
                            ),
                            人
                        ) %%%カンマいらない
                    ),
                    こと
                ),
                指す
            ),
            node(の,
                日本特有,
                概念
            )
        )
    )
).

追加したことは、(1)の二行目に、目的とする従属語がリストの場合でもtrueになるようにしたこと。swi-prologの組み込みルールのmemberを使っている。さらに、(2)の表示ルールで、項目がリストの場合も表示するように二行目を加えた。

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

1 ?- passive(人).
なんらかの技芸や芸能の道に[通じて,通じる]いる人
true .

2 ?- passive(通じて).
なんらかの技芸や芸能の道に通じて
true .

3 ?- passive(技芸).
なんらかの技芸
true .

4 ?- passive(職業).
身に[備わっ,備わる]た技芸や芸能を[もっ,もつ]て職業
true .

5 ?- passive(もつ).
身に[備わっ,備わる]た技芸や芸能をもつ
true .

1.は、出力時のリスト処理で、リストをそのまま出力している例だ。2.は、リストの中の文字を検索し、出力している。リストの中にはもともと[通じて,通じる]という、表現型と原型が含まれていたのだが、その表現型の方を検索し、出力している。この場合は、表現型が指定されているので、リストの出力はない。3.は、末端の語を出力させただけである。4.は、二つのリストを出力した例。5.は、「または」の二つ目の山の中にある語を出力させてみた例である。

全て、問題なく、狙った通りに出力している。パチパチ!!

wikipedia「芸人」の定義とprolog(2)改良版

定義をprologの宣言文にするプロセスを書く予定だったが、少し後回しにして、前の記事の宣言文フォーマットを改訂する。

前のような構成では、「は」、「と」とか言葉をつなぐ助詞のような項目が関係子の名前として使ったが、こうすると、助詞そのものを扱うときに面倒になる。そこで、普通の二分木のように、ノードの名前を統一する。ここでは、関係子をnodeと言うように記載しよう。

日本語の文章を、完全な二分木で表現するような試みは、他にあるのだろうが、私はみていない。

二分木を構成するノードは全て次のようになっている。

node(結合子,左項,右項)

結合子は、助詞など、名詞や動詞をノリのようにつなげるものである。ノリがノードになっているのがこのシステムの最大の特徴であると言っても良い。左項や右項は、「語」か「ノード」である。

改良したプログラムの全体を以下に示しておく。

%%%%%%%%%%%%%%%%
% geinin3.swi
% 2019年1月26日
% リストの場合の処理はしていない(リスト処理バージョンは次の記事で)
%%%%%%%%%%%%%%%%

%%% 元の文章:
% 芸人とは、なんらかの技芸や芸能の道に通じている人、
% または身に備わった技芸や芸能をもって職業とする人
% のことを指す日本特有の概念である
%%%

%%% (1)節、再帰的検索
search(S,node(A,B,S),A,B).
search(S, node(_, Y, _), A, B) :- search(S, Y, A, B).
search(S, node(_, _, Z), A, B) :- search(S, Z, A, B).

%%% (2)受動語を検索、表示
passive(X) :- sentence(T),search(X, T, A, B),printnode(B),write(A),printnode(X),nl.

%%% (3)ノードの表示:再帰的処理
printnode(N) :- atom(N),write(N).
%% printnode(N) :- [X|Y] = N,write(X). %% リストの場合の表示、使っていない
printnode(N) :- node(X,Y,Z) = N,printnode(Y),write(X),printnode(Z).

%%% (4)文章データ
sentence(
    node(とは,
        芸人,
        node(ところの,
            node(を,
                node(の,
                    node(または,
                        node(いる,
                            node(に,
                                node(の,
                                    node(や,
                                        node(の,
                                            なんらか,
                                            技芸
                                        ),
                                        芸能
                                    ),
                                    道
                                ),
                               [通じて,通じる]
                            ),
                            人
                        ),
                        node(とする,
                            node(て,
                                node(を,
                                    node(や,
                                        node(た,
                                            node(に,
                                                身,
                                                [備わっ,備わる]
                                            ),
                                            技芸
                                        ),
                                        芸能
                                    ),
                                    [もっ,もつ]
                                ),
                                職業
                            ),
                            人
                        ) %%%カンマいらない
                    ),
                    こと
                ),
                指す
            ),
            node(の,
                日本特有,
                概念
            )
        )
    )
).

後半部分が、宣言に再構成された文章である。プログラムそのものの説明は後回しにして、まず、動かしてみよう。上のプログラムを geinin2.swi というテキストファイルにしたとする。

$ swipl -f geinin3.swi

で、swi-prologに読み込み、コンパイルする。以下のように、実行を試みる。

1 ?- passive(道 ).
なんらかの技芸や芸能の道
true .

2 ?- passive(人).
なんらかの技芸や芸能の道に身に
false.

3 ?- passive(概念).
日本特有の概念
true .

最初のものは、「道」にかかる言葉をフレーズとして表示している。2番目のものは、人にかかる言葉を表示させようとしているのであるが、 [通じて,通じる]にぶつかっているので、そこで表示が止まって、falseになっている。このリストの場合の処理は、次の記事(3)で対応しているので参照されたい。最後のものは、最後のフレーズから取り出してみたものだ。

まだ、完璧ではない。リストに一旦引っかかると、エラーを出してしまう面もあるようだ。それもまた、チェックしていこうと思う。

プログラムの解説をしておこう。まず(1)の部分は、基本的なルールである。その最初のものは、終端条件である。すなわち、指定された用語と、ノードの右側の項目が一致した場合は終了である。それと同時に、A,Bに当たる情報を保持するようにしている。2番目の指揮は、左側のノードへの検索であり、3番目のものは、右側のノードへの検索である。再帰的に定義されている。A,Bの語情報を維持している。

(2)は、質問の開始節である。sentence(T)は、のちに作成した二分木データをTに取り組むための関係子(?)である。sentenceは、ルートノードと考えれば良い。X,A,Bから、検索によって得られた知識を表示させている。

(3)は、再帰的に、ノードの内容を表示させて、元の文章を再現している。最初の説は、アトムの場合は、そのまま表示させるというものである。node(X,Y,Z) = N は、Nがノードであることがわかっているので、ユニフィケーションによって、X,Y,Zの値を得ているのである。

(4)は、二分木で表された文章である。二分木の作り方は他にも色々ある。これは、私が直感的に作ったものであるが、本来目指しているのは、実際のデータをcabocha等で構文解析し、JAVAのプログラムで作成しようと考えているものである。