邏輯程式設計:上古人工智慧語言Prolog

2024.01.01

今天要跟大家介紹一個有趣的程式語言。

它能夠讓電腦像偵探一樣推理,像哲學家一樣思考,這就是邏輯程式設計。

邏輯程式設計就好比我們給電腦一個邏輯謎題,然後他經過一連串的推理,找到答案。

1 邏輯程式設計是什麼?

1.1 邏輯程式設計的定義與特點

想像一下,如果我們可以讓電腦像人類一樣思考和解決問題,那會是怎樣的情景?邏輯程式設計就是這樣一種嘗試,它利用邏輯學的原理,使電腦能夠進行推理和解決問題。

我們只需要告訴電腦:「這是規則,這是事實,現在你告訴我答案。」 就像玩一個連環推理的遊戲,每個線索都逐漸揭開謎題的一部分,最終揭示出整個故事。

邏輯程式設計的特點是高度抽象,不關注如何操作,而是關注「什麼是真的」。

1.2 代表語言:Prolog

Prolog,即Programming in Logic,是一種用邏輯程式設計的代表性語言。它的核心是事實和規則。讓我們來看看Prolog的基本語法:

  • 事實:在Prolog中,我們可以定義一些基本的事實。例如,philosopher(socrates). 表示「蘇格拉底是哲學家」。
  • 規則:規則用來定義事實之間的關係。例如,mortal(X) :- philosopher(X). 表示「如果某人是哲學家,那麼他必然是凡人」。X表示參數,大寫的是變量,小寫的是常數。符號:- 表示推理關係,如果右邊的表達式成立,左邊的表達式也成立。
  • 查詢:查詢是向Prolog程式提出的問題。例如,?- mortal(socrates). 將詢問「蘇格拉底是凡人嗎?」。?-是命令提示符,我們在後邊輸入要執行的命令,回車後執行並返回結果。

注意所有語句的最後都用一個點(.)表示結束。

Prolog的魅力在於它的簡潔與強大。我們不需要告訴電腦如何一步步解決問題,只要告訴它規則和事實,它就能自動進行邏輯推理,最終給我們答案。

2 邏輯程式設計的應用領域

2.1 專家系統

專家系統是邏輯程式設計的一大應用,就像是擁有了一個專家顧問團隊。它們可以幫助醫生診斷疾病,或幫助工程師設計複雜的機械結構。以醫療診斷為例,在Prolog中,我們可以定義一連串的症狀和疾病之間的關係。例如:

disease(flu) :- symptom(fever), symptom(cough).
  • 1.

這條規則告訴電腦:「如果一個人有發燒和咳嗽的症狀,那麼他可能得了流感。」透過這樣的規則,專家系統可以幫助醫生診斷疾病。

2.2 自然語言理解

自然語言理解讓電腦能夠理解人類的語言。就像是給電腦裝上了一副懂人類語言的眼鏡,它可以讀懂你的郵件,理解你的指令。例如,我們可以用Prolog來解析句子的結構:

sentence(Subject, Verb, Object) :- noun(Subject), verb(Verb), noun(Object).
  • 1.

這條規則可以幫助電腦理解「主詞+ 謂詞+ 受詞」的句子結構。透過這種方式,電腦可以更好地理解使用者的輸入。

2.3 智能知識庫

智慧知識庫就像是一個巨大的電子大腦,儲存著大量的資訊。邏輯程式讓這個大腦可以自己思考和推理,幫助我們從中找到需要的知識。例如,一個關於歷史人物的知識庫可能包含以下規則:

born_in(bruce_lee, san_francisco).
performed(bruce_lee, the_game_of_death).
  • 1.
  • 2.

透過這樣的事實和規則,使用者可以查詢李小龍的出生地和參與過哪些電影。

3 邏輯程式設計如何解決問題?

3.1 建立模型

解決問題的第一步是建立一個模型。就像是搭建一座橋樑,我們需要定義橋樑的結構和支撐點。在邏輯程式設計中,我們定義規則或限制條件,例如:“哲學家是凡人。”

mortal(X) :- philosopher(X).
  • 1.

3.2 定義規則與事實

接下來,我們在模型上加入規則和事實。如果說模型是橋樑的結構,那麼規則和事實就像是橋樑上行駛的車輛。例如,我們聲明:“蘇格拉底、柏拉圖、亞里斯多德是哲學家。”

philosopher(socrates).
philosopher(plato).
philosopher(aristotle).
  • 1.
  • 2.
  • 3.

3.3 推導與求解

最後,我們提出問題,例如:「誰是凡人?」邏輯程式設計像是個聰明的小偵探,它會自動推導邏輯,找出所有可能的答案,例如輸出:「蘇格拉底、柏拉圖、亞里斯多德。”

Prolog 有許多實現,本文以目前比較活躍的SWI-Prolog 為例運行這個程式。

需要先把規則和事實儲存到一個檔案中mortal.pl,然後使用指令consult 載入規則,最後執行mortal(X). 取得結果,有多個答案時,我們輸入分號(;),Prolog會繼續搜尋直到所有可能的答案都被找到。

圖片圖片

SWI-Prolog官方網址:https://www.swi-prolog.org/

可執行程式:https://www.swi-prolog.org/download/stable

Docker映像:https://hub.docker.com/_/swipl/

原始碼:https://github.com/SWI-Prolog/swipl-devel

4 邏輯程式設計的常見問題與解決方法

4.1 地圖著色問題

想像一下你有一張地圖,你需要用不同的顏色為每個區域上色,但相鄰區域不能是同一種顏色。

(圖片與解題方法來源:https://ruanyifeng.com/blog/2019/01/prolog.html)

圖片圖片

規則如下:

color(red).
color(green).
color(blue).

color_solution(A,B,C,D,E) :-
    color(A), color(B), color(C), color(D), color(E),
    \+ A=B, \+ A=C, \+ A=D, \+ A=E,
    \+ B=C, \+ C=D, \+ D=E.
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

如果右邊有多個條件,用逗號隔開;如果要用否定表達式,在表達式前加上\+ 。

計算速度還是比較快的,執行效果如下:

圖片圖片

4.2 嫌疑人推理問題

當一個偵探在解決一個案件時,他需要考慮所有嫌疑人的動機和證據。邏輯程式設計可以幫助偵探整理這些信息,推斷出真正的罪犯。

假設規則如下:

% 犯罪嫌疑人
suspect(zhangsan).
suspect(lisi).
suspect(wangwu).

% 犯罪发生在夜晚
crime_occurred(night).

% 无辜的
innocent(Person) :- suspect(Person), not_at_scene(Person, Time), crime_occurred(Time).

% 假设我们知道Alice和Charlie在犯罪发生时有不在场证明
not_at_scene(zhangsan, night).
not_at_scene(lisi, night).
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

我們依照規則和事實就可以求解誰是無辜的,誰不能排除嫌疑,示範效果如下:

圖片圖片

4.3 求近似最優解

有時候,問題可能沒有一個完美的解決方案,求解最優解的成本很高,我們找一個近似解就可以了。這裡以背包問題(Knapsack Problem)舉個例子,在這個問題中,我們需要在不超過背包重量限制的情況下,盡可能多地裝入價值總和最大的物品。

假設我們有一系列物品,每個物品有重量和價值,以及一個背包的最大重量限制。這段程式碼比較複雜,也有更好的解決方案,有興趣的可以研究下。

% 物品列表:item(物品ID, 价值, 重量).
item(1, 15, 20).
item(2, 20, 30).
item(3, 15, 25).
item(4, 22, 25).

% 背包最大重量限制
max_weight(50).

% 检查物品组合是否超过最大重量
within_weight_limit(Items, MaxWeight) :-
    total_weight(Items, TotalWeight), %通过调用total_weight计算物品重量
    TotalWeight =< MaxWeight.

% 计算物品组合的总重量,这里是一个递归规则。
% 它分两种情况:一个是空列表的总重量为0,另一个是包含至少一个物品的列表。
% 对于非空列表,它计算列表头部物品的重量,然后递归地计算列表剩余部分的总重量,最后将两者相加。
total_weight([], 0).
total_weight([Item|Rest], TotalWeight) :-
    item(Item, _, ItemWeight),
    total_weight(Rest, RestWeight),
    TotalWeight is ItemWeight + RestWeight.

% 计算物品组合的总价值,这也是一个递归规则
total_value([], 0).
total_value([Item|Rest], TotalValue) :-
    item(Item, ItemValue, _),
    total_value(Rest, RestValue),
    TotalValue is ItemValue + RestValue.

% 暴力搜索所有的组合,可以看作是一种深度优先搜索
% max_weight把背包的最大容量取出来
% findall是Prolog内置的,把所有物品的Id取出来
% subset把所有物品的组合取出来,定义见下方,结果将被绑定到变量Solution
% within_weight_limit 判断方案是否在重量限制中
% 如果Solution的总重量在限制范围内,这个调用会计算其总价值并将其绑定到变量Value。
iterative_deepening_knapsack(Solution, Value) :-
    max_weight(MaxWeight),
    findall(Item, item(Item, _, _), AllItems),
    subset(AllItems, Solution),
    within_weight_limit(Solution, MaxWeight),
    total_value(Solution, Value).

% 用于查找所有可能组合(子集),这也是一个递归定义
% 空列表是其自身的子集。
% 如果列表的头部是E,并且Tail的一个子集是NTail,那么添加E到NTail的头部形成的列表也是原始列表的一个子集。这条规则包含列表中的第一个元素。
% 无论列表的头部是什么(这里用匿名变量_表示),Tail的一个子集NTail也是原始列表的一个子集。这条规则不包含列表中的第一个元素。
subset([], []). 
subset([E|Tail], [E|NTail]):- subset(Tail, NTail).
subset([_|Tail], NTail):- subset(Tail, NTail).

% 寻找近似最优解
% 使用findall运行iterative_deepening_knapsack 来收集所有解决方案的价值
% 使用max_list找到最高的价值
% 再次运行iterative_deepening_knapsack来找到与最高价值对应的物品组合
% 返回最高价值ApproxValue和对应的物品组合ApproxItems作为查询结。
approximate_solution(ApproxValue, ApproxItems) :-
    findall(Value, iterative_deepening_knapsack(_, Value), Values),
    max_list(Values, ApproxValue),
    iterative_deepening_knapsack(ApproxItems, ApproxValue).
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.

實際運作效果如下:

圖片圖片

結語

邏輯程式語言歷史悠久,通常用於教育、研究和某些專業應用領域,如人工智慧、知識表示、自然語言處理、專家系統等,但是我們日常用的很少,主要是因為它的表現不太好,而且學習曲線比較陡峭,業界的接受度也不太高。

不過在人工智慧中,其表示知識和推理方面的能力還是不錯的,未來或許可以和命令式、函數式程式設計範式更多的結合,從而提供更強大和靈活的程式設計工具。

最後,邏輯程式設計就像是賦予了電腦一種思考能力,有興趣的可以一起學習下。