prolog二分木における副詞の扱い

副詞の扱いに問題があることが判明した。例えば、「ロボットとともに人工知能も注目された」という二分木がこんな感じになってしまう。

testdoc(testline_0_0,
    node(と,
        [[ロボット, 'C:人工物-その他'], ともに],
        node(も,
            [[人工, 'C:抽象物'], [知能, 'C:抽象物']],
            node([],
                [[[注目, 'C:抽象物'], [さ, 'V:する']], れた],
                [ ]
            )
        )
    )
).

これがおかしいのは、「ロボットともに」が左の葉で、ノード値が「と」になっていることだ。もともと、「ロボットとともに」が一つの句の中にあったのに、「と」が助詞で、ノード値に入れられたのちに、副詞の「ともに」が現れ、副詞は一般に左右の葉の値となるものなので、ロボットに継ぎ足されたのである。

一般の副詞の場合、例えば次のようになる。

testdoc(testline_0_0,
    node(の,
        node(には,
            [広場, 'C:場所-施設'],
            かなり
        ),
        node(が,
            [人, 'C:人'],
            node([],
                [[集まって, 'V:集まる'], いた],
                [ ]
            )
        )
    )
).

この場合、「かなり」が副詞で、右の葉に入っていて、不自然さはない。

そこで、すでに、助詞がノード値として入っているときに副詞があらわれたら、それはノード値につなげるようにした。もともと、ノード値は、基本的に、体言や用言のリーフ値をつなげるものなので、品詞で厳密に分けているのではないから、それでいい。

そのようにフォーマットを変えると、次のようになる。

testdoc(testline_0_0,
    node(とともに,
        [ロボット, 'C:人工物-その他'],
        node(も,
            [[人工, 'C:抽象物'], [知能, 'C:抽象物']],
            node([],
                [[[注目, 'C:抽象物'], [さ, 'V:する']], れた],
                [ ]
            )
        )
    )
).

大きな問題はない。これでいこう。ただ、もう少し改定点がまとまってから、wikipediaやtwitterの作り直しをやろう。

prolog二分木のフォーマットを変更する

prolog二分木から初歩的な文章を作るようになったが、少しフォーマットを変更することにした。

一つは、動詞の原形を入れるときに、使われている表現形と同じ場合は、原形情報を与えなかったが、これを与えることにした。動詞と分かれば、原形がなければそれが原形と推測できるが、そもそも二分木には品詞情報を入れていないので、判断できないからだ。

なぜ、これが大事なのかといえば、動詞の原形は、それだけで文章終端になりうるからだ。文章を作るときに、終端が違和感なく終わることはとても大事なのである。このように「だ」「です」「である」などは、多分判定詞で、同士ではないが、例えば「書き」で終わったら、言葉としておかしいが「書く」で終わることは不自然ではない。だから、動詞の原形はとても重要なので、原形がある場合とない場合がはっきりしないというのは、良くないということで、改良した。

もう一つは、動詞の原形と名詞のカテゴリは、ほとんど同じフォーマットで入れ込んでいるが、ときに区別できなくなるときがあるので、それらの冒頭に、名詞のカテゴリの場合は「C:」というヘッダ、動詞の原形の場合は「V:」というヘッダーをつけることにした。例えば、次のようである。

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

二分木を一行にしているので少しわかりにくいのでインデントをつけて表すと次のようになる。

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

これで、例えば、最初の「含む」は、原形と表現形が同じだが、原形を付加している。また、動詞なのでヘッダにV:が付いている。これで、処理がよりしやすくなった。

これで、一昨日から、日本語wikipedia本文とツイッターデータを全部作り直している。

 

文章をprologの二分木で作る

ようやく、ここに到達した。まだ事始めではあるが、ぼちぼちとやっていこう。

次のようなprologプログラムを考える。

%% -----------------------
%% 文章(二分木化されたprolog宣言文)を組み立てる
%% 2019年4月22日
%% -----------------------

%% 空のツリーに、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(Value0, Left0, []),node(Value, Left, Right), node(Value, Left, node(Value0, Left0, Right))).
insert(right,Node,node(Value, Left, Right), node(Value, Left, New)) :-
        insert(right,Node,Right,New).

特に工夫もない、アドホックなもので、文章のノードを与えて、ツリーを作るところから始めてみようというわけである。実行すると次のようになる。

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

?- insert(_,node(は,ロボット,[]),[],Subs).
Subs = node(は, ロボット, []) .

?- insert(right,node(です,機械,[]),node(は, ロボット, []),Subs).
Subs = node(は, ロボット, node(です, 機械, [])) .

?- insert(left,node(の,産業用,[]),node(は,ロボット,node(です,機械,[])),Subs).
Subs = node(は, node(の, 産業用, ロボット), node(です, 機械, [])) .

?- insert(right,node(の,産業用,[]),node(は,ロボット,node(です,機械,[])),Subs).
Subs = node(は, ロボット, node(の, 産業用, node(です, 機械, []))) ;
Subs = node(は, ロボット, node(です, 機械, node(の, 産業用, []))) .

最初のコマンドで、プログラムを読み込む。なんの条件も考えずに、ただ、ノードを付け加えている。後半の二つは、node(の,産業用,[]) というノードを作成済みツリーの左側と右側に付け加えている。

この結果を出すために、作ったようなプログラムなので、あえて議論しなくても良い。ただ、二分木で表されたprologの文章は、ある種プログラムであって、プログラムがプログラムを作る感じを確認したかっただけである。

AIどうしに会話させる

帽子を買いに、妻と池袋のサンシャイン通りを歩いていたときに思いついたことだ。

日本語wikipediaの本文全文や1億1千万のツイートのprolog二分木化された言語的知識はできている。これに基づいて会話的言葉を創作させるのが次の課題だ。どうして実現するのか、ぼんやりとしたイメージのようなものしかなかったが、言葉の文章を創造するAIどうしに会話させて、その会話にスコアを与えて、いい会話を作る能力を形成することができるような気がしてきた。

AIどうしに会話させれば、膨大な会話データを自動的に取得できる。スコアの作り方を改良していけば、会話がより自然なものになり、また、AIの会話的能力も高めることができる。

prologの二分木による宣言文を複数語で検索する

文章をロジカルに二分木として作成する課題の初めに、膨大な二分木を複数語で検索するというの自在にできるようにしたい。

複数のキーワードをリストで与えて、二分木の中からそれらを同時に持つものを抽出する。

例えば、次のような文章を考える(実際 wikipediaの一文)。
「これに基づいたゲンギスと呼ばれる六本足のロボットは、いわゆる脳を持たないにも関わらず、まるで生きているかのように行動する。」

これをすでに何度か述べた方法(javaで書いたプログラム)でprologの二分技化すると次のようになる。

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

これでは、二分技とは見えにくいと思うので、図で書くと次のようになる。

[ ]は、空リストだ。基本、ノードに体言(名詞、動詞)はこない。「の」は、強い助詞なので、全体のルートに来ている(この辺りの手続きは別の記事で書いている)。左側には個別的、特殊な事柄が、右側には、一般的な事柄が吐き出されている。しかし、こうした二分木の構造は全体としてみれば、二次的なものだ。

全体として、端末の左の葉の言葉が少ないという特徴も出ている。

検索用に、次のようなprologのプログラムを作成した。左からの検索プログラムしかまだ書いていない。

%%
%% キーワードのリストから部分二分木を拾ってくる
%% 2019年4月21日
%%

sentence(L) :- jawiki(_,Node),getsentence(L,Node).

getsentence([],_).
getsentence([S|T],Node) :- lsearch(S,Node,Out),
    format('~w ==> ~w ~n',[S,Out]),
    getsentence(T,Node).

%getSentence(S,Node,Out) :- jawiki(_,Node),
%    lsearch(S,Node,Out).

%% 見つかったら、node(A,B,S)を返す(改訂)
lsearch(S,node(A,S,B),node(A,S,B)).
% リストの場合
lsearch(S,node(A,[H|T],B),node(A,C,B)) :- getmember(S,[H|T]),
        getlist([H|T],F),
        flatten(F,G),
        atomic_list_concat(G,C).
% 上で一致しなかったら、左右のノードの内側を調べる
lsearch(S, node(_, Y, _), N) :- lsearch(S, Y, N).
lsearch(S, node(_, _, Z), N) :- lsearch(S, Z, N).

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

このプログラムに基づいて、次のような処理をさせる。

?- sentence([これ,ロボット,持た,行動]).

これ ==> node(に,これ,[基づいた,基づく]) 

ロボット ==> node(は,ロボット,node(を,node(いわゆる,[],[脳,動物-部位]),node(にも,[[持た,持つ],ない],node(かの,node([],node(ず,[関わら,関わる],まるで),[[生きて,生きる],いる]),node(に,よう,node([],[[行動,抽象物],する],[])))))) 

持た ==> node(にも,持たない,node(かの,node([],node(ず,[関わら,関わる],まるで),[[生きて,生きる],いる]),node(に,よう,node([],[[行動,抽象物],する],[])))) 

行動 ==> node([],行動する,[]) 

true

swi-prologで実行している。ゴール sentenceの引数として、検索キーワードのリストを与えている。これらすべてのキーワードを抱えている二分木が引っかかってくる。プログラムは、引っかかった後のノードを出力するようにしている。

厳密にいうと、そのキーワードよりも深いところを構成する部分木を出力させている。

画像にあるように「これ」というキーワードは、最下層のノードにぶら下がっているので、その最下層の部分木だけが出力される。逆にロボットは、最も上位のノードにぶら下がっているので、右の部分木全体を出力している。

「これに基づく」という表現は、我々が日常的に使う表現であり、私がいう部分知識というものの最も小さい単位である。また、「行動する」も、行動という名詞が、するという補助語につながっている、小さい部分知識である。一方「ロボットは、いわゆる脳を持たないにも関わらず、まるで生きているかのように行動する」という表現は、部分知識ではあるが、かなりまとまった知識となっている。

この辺りの違いが重要な意味を持っている。

prolog二分木から、新たな文章を二分木で作る

先月からすったもんだしてやってきた、日本語wikipediaの本文全部とtwitterの一億一千万ツイートをprolog二分技化するという作業がひと段落したので、本来の目的であって、これらを部分知識として利用して言葉を作成するという作業に入りたい。

prologには、二分木を作り出すというアルゴリズムがある。言葉を作り出すというのを、二分木創造の考え方を利用する。言葉づくりの人工知能には、ディープラーニングを利用したものもあるが、あまり好かない。言葉は、左脳の作業、ロジカルな作業なのだから、ディープラーニングやニューラルネットワークを直接応用するのは少し外れている気がする。もちろん、いずれは使う。言葉を利用する人間の脳は、論理ばかりで操っているわけではないだろうから、直感的な作用の結果でもあると思うからである。

1億1千万ツイートのprolog化が終わって

合計で、延べ40時間ほどcore-i9プロセッサを動かし続けて、ようやく全てのツイートの処理が終わった。一つのツイートに複数の文章があることも含めて、合計で、1億4084万5837行のprologの二分木ツリー宣言文となった。ファイルサイズは、42.96GBである。

ただ、絵文字が多数で文章的に意味をなさないものや、キャラクタコードがおかしいものなどがprologの読み込み時にエラーになるので、それを今取り除いている。

それはつまりswi-prologに読み込んでいるということなのだが、1000万行を読み込むのに15分かかるので、全部読み込んで、エラー行を確定するのに、3時間30分程度かかる予定だ。

それが終わったら、読み込めたものを実行可能ファイルにすることと、元のswiファイルから、それらの行を全て削除する予定だ。

Jumanの死

1億1千万ツイートのprolog化も、最終局面に来ている。トラブルがなければ、明日の朝には、終わっているはずだ。

前の記事からの進展は、サーバーモードのJumanが、耐えられなくなって死んでしまうという現象を回避するようにしたことだ。

全てのツイッターを4分割して、その三つ目をやっているときに、どうしてもSocketExceptionエラーが発生して、サーバーモードのJumanのソケットが死んでしまって、そのスレッド自体がフリーズしてしまうという現象が発生するようになった。そうなると、他はうまくやっているようなのだが、私の方が続ける気が無くなって、全体を止めてしまうというのが繰り返された。(一つのスレッドに、一つのJumanを、ポートを変えて立ち上げているので、他のJumanには影響を与えない)

よくよく原因を調べると、異常なツイートにJumanが疲弊してしまったようだ。ツイートの中に、次のような二つのツイートが続いてあった。

かぁぁぁぁぁぁぁぁぁぁぁぁぁぁぁわぁぁぁぁぁぁぁぁぁぁぁぁぁぁぁいぃぃぃぃぃぃぃぃぃぃぃぃぃぃぃε=ε=(ノ≧∇≦)ノ#ボタンインコ#インコ
【1週間限定】セブン-イレブン、かわいすぎるXmasスイーツが登場sCMXcバニラといちごのサンタケーキ、チョコとクッキーのトナカイケーキ、いちごムースを使った柊のケーキを展開しています。 https…

この二つのツイート、それぞれ単独では、prolog 化できるのだが、続けると必ず、二つ目の文章のJumanによる形態素解析の場面で、SocketExceptionエラーが発生してしまう。Jumanのプログラムを調べていないので(ソースはあるが、面倒でできない)よくわからないが、最初の文章でJumanが内部エラーを引き起こしてしまっているようなのだ。

さらに調べると、最初の「ぁぁぁぁぁぁぁぁ」あるいは「ぃぃぃぃぃ」というような文字の連続を一部やめる(少し短くする)と、続けても問題なくprolog化できる。およそ、このツイートは日本語ではないので、Jumanが投げ出してしまう気持ちもよく理解できる。(元文章はなるべくいじらず、可能な限り受け入れようというのがポリシーだ。そうすることによって、どんな文章にもprolog化アルゴリズムが適用できるだろうと思っているので)

内部エラーを起こして、フリーズしてしまうのは実は係り受け解析のKNPなのだ。なぜそうなるのかというと、Jumanの内部エラーで、中途半端に構文解析終えて、最後にEOSを返さないのだ。そのために、EOSをKNPがいつまでも待つという形でスレッドがフリーズする。だから、最後にEOSをあえてつけるようにしたが、それでKNPはフリーズしなくなるのだが、その後Jumanは動かなくなってしまうので、処理が進まない。

結局、先のような同一文字が5文字以上続いているような文章は、prolog化の対象から外すことにした。正規表現の後方参照で、そのような連続文字をチェックできる。

ただ、そうしてみると、ツイッターの中には、そうした同一文字の連続うちという現象がいたるところで観れる。だから、多少躊躇したが、そんなの文章じゃないだろうという思いの方が打ち勝って、この基準を導入することにした。

今、最後の1/4を実行している。210このスレッドが動いている。早くて超高機能のパソコンのおかげで、大きな問題なくここまできた。

TweetsのProlog化を高速で処理する

集めた1億1千万のツイートサンプルを全部、prologの宣言文にしたい。それが、日本語wikipediaの本文全文のprolog化よりも何倍も深刻なものだとわかった。wikipediaと同じアルゴリズムでやろうとすると、core_i9のlinuxを動かし続けても、50日くらいかかる。動かせば良いというのはそうなのだが、そうすると途中でミスやバグに気づいても、もうやり直すリスク異常に高くなる。

長いこと悩んだが、最適な解決方法がわかった。それは、一番、時間コストがかかっている構文解析のknpを高速化することだ。knpは、それまで使っていたCabochaと比べるととてつもなく時間をかけて解析する。それで、カテゴリなどの面白い結果を出してくるのだから仕方がないと思っていたのだが、もう、ここを高速化するしかないと思って調べると、

-dpnd-fast -no-ne

の二つのオプションをつけると高速化することがわかった。knpを供給していただいているサイトには、

-dpnd-fast: 精度を犠牲にした高速な係り受け解析,高速に固有表現認識を行いたい場合などに用いる
-no-ne: 固有表現認識を行わないオプション,-dpnd-fastと合わせて使うことで高速に係り受け解析を行える

と解説されている。そもそも固有表現認識が何かわかっていないのだが、カテゴリなどのこちらの必要としているものが犠牲にならないならば、問題ないと試みてみた。すると、立ち上がり時間も含めて、3倍早くなった。しかし、我々の場合、サーバーモードだから、立ち上がりのオーバーヘッドは考慮しなくて良い。それで、実際やってみた印象では、10倍から20倍早くなる!!

これでやってみると、というか、現在やらせているのだが、およそ二日で計算を終える勘定だ。

1スレッドで80万ツイートを処理すべく、ファイル単位で処理するため、短い処理しか必要がない場合もあり、180スレッドくらいを立ち上げる。

途中で、停電などのアクシデントがないことを願うばかりだ。cpuは、12コア24スレッドの並列処理を3.5Gのクロックスピードで実行する。実質、切り替えをやっているので、オーバーヘッドが発生するが、実行スレッドを小さくすると、CPUの稼働率が大きく下がってしまうので、かなり無理させている。CPU温度は、どのコアについても70度を超えることがないことがわかっているので、なんとかなるだろうと思っている。何しろ30数万円もの金をかけたコンピュータだ、それくらい、働いてもらわないと困る。

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

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

Prolog 二分木検索の使い方

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

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

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

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

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