2022/5/6 HPトップとサービス一覧を一新しました!トップの新着情報にご注目ください!

29-1. 論理プログラミング言語「Prolog」(その1:ファクトとクエリをPythonで再現してみた)

概要

Prolog(ぷろろぐ)は論理プログラミング言語の一つで、データの性質や関係を「命題」として記述することで、問い合わせに対して論理的な答えを導き出してくれます。

例えば、

  • ピチューの進化後はピカチュウ
  • ピカチュウの進化後はライチュウ
  • 進化後の進化後もまた進化後と言える

と定義しておけば、「ライチュウの進化前は?」という問い合わせに対して「ピカチュウとピチューです」と答えてくれます。

Prologは第2次AIブーム(1980年代)を牽引したエキスパートシステムの発展に寄与しました。昨今のディープでポンなAIとは対照的な技術として、その価値が再評価されているとかいないとか。

ここではPrologの動作をPythonで模倣することによって、その仕組みや利点を理解することを目指します。なお、専門用語は平易な表現にしている部分があります。

参考文献

こちらの参考書の例題を踏襲しています。
Ivan Bratko(著)、安部憲広(翻訳)『Prologへの入門 (PrologとAI)』近代科学社

Prologの実行環境には「AZ-Prolog」等ありますがライセンスにご注意ください。

AZ-Prolog | 最新版ダウンロード

Python実行環境には

WinPython3.6をおすすめしています。

WinPython - Browse /WinPython_3.6/3.6.7.0 at SourceForge.net
Portable Scientific Python 2/3 32/64bit Distribution for Windows

Google Colaboratoryが利用可能です。

Google Colaboratory

家系図

こちらの家系図を参照します。

ファクトと”変数なし質問”

Prologで6つの親子関係を定義します。このような定義をファクトと呼びます。

parent(pam, bob).
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
parent(pat, jim).

その後、「pamはbobの親か?」と質問するとyes/noで答えてくれます。このような質問をクエリと呼びます。

?- parent(pam, bob).   % pamはbobの親か?
yes

さて、定義した parent(pam, bob). については今回の例では第1引数が出力、第2引数が入力のように見えます。しかし機械にとってその順序は関心がないことで、単にpamとbobはparent関係にあると記憶します。人間が「第1引数が親、第2引数が子」と決めて定義文を打ち、同じ決まりに従って質問をすれば順序を間違うことなく回答が得られるのです。

また、?- parent(pam, bob). は厳密には「parent(pam, bob) を満たす情報が一つでもあるか?」という意味です。定義された情報を上から順に参照し、マッチした瞬間にyesを返して終わります。

ここまでをPythonで再現します。定義を追加する関数 DEF() を作りました。

import pandas as pd
from copy import deepcopy

#初期化
data = pd.DataFrame()

#定義を追加する関数
def DEF(data, info):
    tmp = pd.DataFrame(info, index=[0]) #情報展開
    data = data.append(tmp, ignore_index=True)
    #重複削除
    if len(data) > 0:
        data = data.drop_duplicates()
    return data

#定義
data = DEF(data, {'func':'parent', 'arg1':'pam', 'arg2':'bob'})
data = DEF(data, {'func':'parent', 'arg1':'tom', 'arg2':'bob'})
data = DEF(data, {'func':'parent', 'arg1':'tom', 'arg2':'liz'})
data = DEF(data, {'func':'parent', 'arg1':'bob', 'arg2':'ann'})
data = DEF(data, {'func':'parent', 'arg1':'bob', 'arg2':'pat'})
data = DEF(data, {'func':'parent', 'arg1':'pat', 'arg2':'jim'})
print(data)
     func arg1 arg2
0  parent  pam  bob
1  parent  tom  bob
2  parent  tom  liz
3  parent  bob  ann
4  parent  bob  pat
5  parent  pat  jim

定義された情報はpandasデータフレームで持つことにしました。これを見ると、やはり入力と出力の順序に決まりはなく、人言側の解釈が一貫していればよいことが納得できます。

さらに質問にyes/noで答える関数を ASK() としました。

#変数なし質問(Yes/No)
def ASK(info):
    tmp = pd.DataFrame(info, index=[0])
    #定義を上から検索
    for i in range(len(data)):
        #ある行が入力条件をすべて満たしているかチェック
        flg = True
        for col in tmp.columns:
            if data.loc[i, col] != tmp.loc[0, col]:
                flg = False
        #すべて満たしていればyesを返す
        if flg:
            return True
            break
    #条件をすべて満たす行が一つもなければnoを返す
    return False

#?- parent(bob, pat).
print(ASK({'func':'parent', 'arg1':'bob', 'arg2':'pat'}))
True

今後の処理の都合上、yes/noではなくTrue/Falseを返すようにしています。

“変数あり質問”

Prologでは大文字始まりの語は変数、小文字始まりの語は値(Prologではアトムと呼ぶ)として扱われます。変数を使って次のような質問ができます。

?- parent(X, liz).   % lizの親は誰か?
X = tom

これは厳密には「parent(X, liz) を満たす X は何か?」という処理が行われています。

複数の変数を使うこともできます。

?- parent(X, Y).
X = pam
Y = bob;
X = tom
Y = bob;
X = tom
Y = liz;
X = bob
Y = ann;
X = bob
Y = pat;
X = pat
Y = jim;

これは定義された6つがすべて取り出されました。

これもPythonで再現します。変数あり質問に答える関数 FIND() を作りました。

#変数あり質問
def FIND(info1, info2):
    tmp1 = pd.DataFrame(info1, index=[0]) #情報展開
    tmp2 = pd.DataFrame(info2, index=[0]) #情報展開
    #変数名を取り出しておく
    vcols = tmp2.loc[0].values
    tmp2 = tmp2[1:]
    
    #定義を上から検索
    for i in range(len(data)):
        #ある行が入力条件をすべて満たしているかチェック
        flg = True
        for col in tmp1.columns:
            if data.loc[i, col] != tmp1.loc[0, col]:
                flg = False
        #すべて満たしていれば変数DFに記録
        if flg:
            tmp2 = tmp2.append(data.loc[i, tmp2.columns], ignore_index=True)
    
    if len(tmp2) > 0:
        #列名を変数に変更して返す
        tmp2 = pd.DataFrame(tmp2.values, columns=vcols)
        return tmp2
    else:
        return False

#?- parent(X, liz).
print(FIND({'func':'parent', 'arg2':'liz'}, {'arg1':'X'}))

#?- parent(X, Y).
print(FIND({'func':'parent'}, {'arg1':'X', 'arg2':'Y'}))
     X
0  tom
     X    Y
0  pam  bob
1  tom  bob
2  tom  liz
3  bob  ann
4  bob  pat
5  pat  jim

こちらも処理の都合上、pandasデータフレームで返すようにしています。

AND処理

「jimの祖父は誰か?」という質問は次のようにできます。

?- parent(Y, jim), parent(X, Y).   % jimの祖父母は誰か?
X = bob
Y = pat

カンマはAND処理を意味していて、「jimの親をYとする」「Yの親をXとする」をANDしています。もちろん厳密には「parent(Y, jim) を満たす Y は何か?」と「parent(X, Y) を満たす X, Y は何か?」のANDが取られています。

類似の質問です。

?- parent(tom, X), parent(X, Y).   % tomの孫は誰か?
X = bob
Y = ann;
X = bob
Y = pat

条件を並べる順序は問わず、 ?- parent(X, Y), parent(tom, X). としても答えは変わりません。

Pythonで「2つの変数あり質問の回答をANDする関数」 AND() を作りました。

#AND処理
def AND(ret1, ret2):
    #列名の和集合
    ucols = list(set(list(ret1.columns)) | set(list(ret2.columns)))
    #列名の積集合
    icols = list(set(list(ret1.columns)) & set(list(ret2.columns)))
    #ANDしたDF
    u = pd.DataFrame(columns=ucols)
    for i in range(len(ret1)):
        for j in range(len(ret2)):
            #積集合列がすべてマッチしているかチェック
            flg = True
            for col in icols:
                if ret1.loc[i, col] != ret2.loc[j, col]:
                    flg = False
            #すべて満たしていれば和集合列の情報を記録
            if flg:
                tmp1 = ret1.loc[i].to_dict()
                tmp2 = ret2.loc[j].to_dict()
                tmp3 = dict(tmp1, **tmp2)
                tmp4 = pd.DataFrame(tmp3, index=[0])
                u = u.append(tmp4, ignore_index=True)
    return u

#?- parent(Y, jim), parent(X, Y).
#1つ目
ret1 = FIND({'func':'parent', 'arg2':'jim'}, {'arg1':'Y'})
print(ret1)
#2つ目
ret2 = FIND({'func':'parent'}, {'arg1':'X', 'arg2':'Y'})
print(ret2)
#AND
print(AND(ret1, ret2))
     Y
0  pat
     X    Y
0  pam  bob
1  tom  bob
2  tom  liz
3  bob  ann
4  bob  pat
5  pat  jim
     X    Y
0  bob  pat

正しく回答が得られました。

#?- parent(tom, X), parent(X, Y).
#1つ目
ret1 = FIND({'func':'parent', 'arg1':'tom'}, {'arg2':'X'})
print(ret1)
#2つ目
ret2 = FIND({'func':'parent'}, {'arg1':'X', 'arg2':'Y'})
print(ret2)
#AND
print(AND(ret1, ret2))
     X
0  bob
1  liz
     X    Y
0  pam  bob
1  tom  bob
2  tom  liz
3  bob  ann
4  bob  pat
5  pat  jim
     X    Y
0  bob  ann
1  bob  pat

こちらも正しく得られました。条件の順序を入れ替えると ret1, ret2 の中身が変わりますが、AND処理後の情報に違いはありません。

おわりに

今回は「ファクト」「変数なし質問」「変数あり質問」「AND処理」までを再現しました。次回は「ルール」を見ていきます。

29-2. 論理プログラミング言語「Prolog」(その2:ルールをPythonで再現してみた)
概要PrologをPythonで再現しながら学んでいきます。内容もプログラムも前回の続きです。参考文献こちらの参考書の例題を踏襲しています。Ivan Bratko...
タイトルとURLをコピーしました