跳转至

1.Knowledge - 让电脑理解逻辑

关于逻辑,命题、真值表、范式、蕴含、等价、归结等等概念在离散数学中都有所提及。本次尝试通过代码让电脑完成简单的推理。

logic.py 中利用 python 的类实现了一些逻辑运算以及知识库(Knowledge Base)的功能。下面给出一些运用的例子。

Knights and Knaves

问题描述

有几个人,其中有骑士(Knight)与小偷(Knave)。骑士总是说真话,小偷总是说谎话。现在有他们说的一些话,请你判断他们分别是什么身份。

Puzzle 0

A says “I am both a knight and a knave.”

因为 A 说“我既是骑士又是小偷”,这是不可能的。因此 A 说的是假话,即 A 是小偷。

Puzzle 1

A says “We are both knaves.” B says nothing.

A 说“我们都是小偷”,这是不可能的(假设法)。因此 A 说的是假话,即 A 是小偷。B 什么都没说,因此 B 是骑士。

Puzzle 2

A says “We are the same kind.” B says “We are of different kinds.”

A 说“我们是同一种人”,这是错的(假设法)。因此 A 说的是假话,即 A 是小偷。B 说“我们是不同种类的人”,这是正确的,因此 B 是骑士。

Puzzle 3

A says either “I am a knight.” or “I am a knave.”, but you don’t know which. B says “A said ‘I am a knave.’” B then says “C is a knave.” C says “A is a knight.”

A 说“我是骑士”或“我是小偷”,但你不知道哪个是真的。事实上 A 无论如何也不会说自己是小偷。B 说“A 说‘我是小偷’”,所以这是假话,因此 B 是小偷。B 说“C 是小偷”,这是假的,因此 C 是骑士。C 说“A 是骑士”,这是正确的,因此 A 是骑士。

那么电脑该如何理解并推理出上述的结果呢?下面尝试使用代码来实现。

from logic import *

# Symbols
Aknight = Symbol("A is a Knight")
Aknave = Symbol("A is a Knave")
Bknight = Symbol("B is a Knight")
Bknave = Symbol("B is a Knave")
Cknight = Symbol("C is a Knight")
Cknave = Symbol("C is a Knave")

# Knowledge 0
knowledge0 = And(
    # A is either a knight or a knave, but not both
    Or(Aknight, Aknave),
    Not(And(Aknight, Aknave))
)

# Puzzle 0
# A says “I am both a knight and a knave.”
knowledge0.add(
    Biconditional(Aknight, And(Aknight, Aknave))
)

print("Puzzle 0")
for symbol in [Aknight, Aknave]:
    if model_check(knowledge0, symbol):
        print(symbol)
print()

# Knowledge 1
knowledge1 = And(
    # A is either a knight or a knave, but not both
    Or(Aknight, Aknave),
    Not(And(Aknight, Aknave)),
    # B is either a knight or a knave, but not both
    Or(Bknight, Bknave),
    Not(And(Bknight, Bknave))
)

# Puzzle 1
# A says “We are both knaves.”
# B says nothing
knowledge1.add(
    Biconditional(Aknight, And(Aknave, Bknave))
)

print("Puzzle 1")
for symbol in [Aknight, Aknave, Bknight, Bknave]:
    if model_check(knowledge1, symbol):
        print(symbol)
print()

# Knowledge 2
knowledge2 = And(
    # A is either a knight or a knave, but not both
    Or(Aknight, Aknave),
    Not(And(Aknight, Aknave)),
    # B is either a knight or a knave, but not both
    Or(Bknight, Bknave),
    Not(And(Bknight, Bknave))
)

# Puzzle 2
# A says “We are the same kind.”
# B says “We are of different kinds.”
knowledge2.add(
    Biconditional(Aknight, Or(And(Aknight, Bknight), And(Aknave, Bknave)))
)

knowledge2.add(
    Biconditional(Bknight, Or(And(Aknight, Bknave), And(Aknave, Bknight)))
)

print("Puzzle 2")
for symbol in [Aknight, Aknave, Bknight, Bknave]:
    if model_check(knowledge2, symbol):
        print(symbol)
print()

# Knowledge 3
knowledge3 = And(
    # A is either a knight or a knave, but not both
    Or(Aknight, Aknave),
    Not(And(Aknight, Aknave)),
    # B is either a knight or a knave, but not both
    Or(Bknight, Bknave),
    Not(And(Bknight, Bknave)),
    # C is either a knight or a knave, but not both
    Or(Cknight, Cknave),
    Not(And(Cknight, Cknave))
)

# Puzzle 3
# A says either “I am a knight.” or “I am a knave.”, but you don’t know which.
# B says “A said ‘I am a knave.’”
# B then says “C is a knave.”
# C says “A is a knight.”
knowledge3.add(
    Biconditional(Aknight, Or(Aknight, Aknave))
)

knowledge3.add(
    Biconditional(Bknight, Biconditional(Aknight, Aknave))
)

knowledge3.add(
    Biconditional(Bknight, Cknave)
)

knowledge3.add(
    Biconditional(Cknight, Aknight)
)

print("Puzzle 3")
for symbol in [Aknight, Aknave, Bknight, Bknave, Cknight, Cknave]:
    if model_check(knowledge3, symbol):
        print(symbol)
print()

上面的代码在运行后即可得出结果如下:

Puzzle 0
A is a Knave

Puzzle 1
A is a Knave
B is a Knight

Puzzle 2
A is a Knave
B is a Knight

Puzzle 3
A is a Knight
B is a Knave
C is a Knight

Minesweeper

" 扫雷游戏是一款经典的益智游戏,游戏的目标是在不触雷的情况下,揭开所有的方块。每个方块上都有一个数字,代表周围 8 个方块中有多少个雷。如果一个方块上没有数字,那么它周围的 8 个方块都不是雷。如果一个方块上的数字为 0,那么它周围的 8 个方块都不是雷。如果一个方块上的数字为 1,那么它周围的 8 个方块中有 1 个雷。如果一个方块上的数字为 2,那么它周围的 8 个方块中有 2 个雷。以此类推。"

要让电脑通过逻辑推理完成扫雷游戏,需要完成以下几个步骤:

  1. 读取扫雷游戏的初始状态
  2. 根据初始状态,推理出每个方块周围的雷的数量
  3. 根据推理出的结果,判断哪些方块是雷,哪些方块不是雷
  4. 根据判断出的结果,标记出雷的位置

其中关键在于,在得到某个块周围的雷的数量后,如何判断哪些方块是雷,哪些方块不是雷。这里可以使用以下的逻辑:

  1. 如果某个方块周围的雷的数量为 0,那么它周围的方块都不是雷
  2. 如果某个方块周围的雷的数量与未知方块的数量相同,那么这些未知的方块都是雷

而扫雷的逻辑推理如果使用真值表来建立知识库的话,那么知识库的规模将会非常大,因此这里使用逻辑语句来建立知识库,这样可以大大减少知识库的规模。

我定义一个列表 knowledge 表示知识库,其中每个元素都是一个 Sentence 对象,表示一个逻辑语句。其中 Sentence 对象的 cells 属性表示该逻辑语句中的所有方块,count 属性表示该逻辑语句中的雷的数量。例如一个 Sentence 对象 Sentence({(1, 1), (1, 2), (2, 1)}, 1) 表示,这三个方块中有一个雷。

此外,处理以上的两种推理方法,我们还可以通过“做差”的方式推理得到更多信息。例如有两个 Sentence 对象 Sentence({(1, 1), (1, 2), (2, 1)}, 1)Sentence({(1, 1), (1, 2), (2, 1), (2, 2)}, 2),那么这两个 Sentence 对象的差集 Sentence({(2, 2)}, 1) 表示,这个方块中有一个雷。这种方法可以推理出更多的信息,因此在推理的过程中,我们需要不断地使用这种方法来推理出更多的信息。于是我们根据上述内容写出以下关键部分代码:

def add_knowledge(self, cell, count):

    # 1) mark the cell as a move that has been made
    self.moves_made.add(cell)

    # 2) mark the cell as safe
    self.mark_safe(cell)

    # 3) add a new sentence to the AI's knowledge base
    #    based on the value of `cell` and `count`
    #    (i.e. the sentence should represent the fact that
    #    `count` of `cell`'s neighbors are mines)
    neighbors = set()
    for i in range(cell[0] - 1, cell[0] + 2):
        for j in range(cell[1] - 1, cell[1] + 2):

            if (i, j) == cell:
                continue

            if 0 <= i < self.height and 0 <= j < self.width:
                if (i, j) in self.mines:
                    count -= 1
                elif (i, j) not in self.safes:
                    neighbors.add((i, j))

    self.knowledge.append(Sentence(neighbors, count))

    while True:
        pre_knowlodges = self.knowledge.copy()
        # 4) mark any additional cells as safe or as mines
        #    if it can be concluded based on the AI's knowledge base
        for sentence in self.knowledge:
            mines = set(sentence.known_mines())
            safes = set(sentence.known_safes())
            for cell in mines:
                self.mark_mine(cell)
            for cell in safes:
                self.mark_safe(cell)

        # 5) add any new sentences to the AI's knowledge base
        #    if they can be inferred from existing knowledge
        for sentence1 in self.knowledge:
            for sentence2 in self.knowledge:
                if sentence1 == sentence2:
                    continue
                if sentence1.cells.issubset(sentence2.cells):
                    new_cells = sentence2.cells - sentence1.cells
                    new_count = sentence2.count - sentence1.count
                    new_sentence = Sentence(new_cells, new_count)
                    if new_sentence and new_sentence not in self.knowledge:
                        self.knowledge.append(new_sentence)

        # Remove empty sentences
        self.knowledge = [sentence for sentence in self.knowledge if sentence.cells] 
        if pre_knowlodges == self.knowledge: break

附上完整的 minesweeper.py runner.py

(建议前往官网下载相关内容以获得更佳体验)

Thanks

本笔记中代码大部分注释由 Copilot 倾情提供 ~