diff --git a/.vitepress/sidebar.js b/.vitepress/sidebar.js index e6a4520..262279d 100644 --- a/.vitepress/sidebar.js +++ b/.vitepress/sidebar.js @@ -297,6 +297,15 @@ export function chapter4() { { text: '4.3.3.2项目:遗传', link: '/4.人工智能/4.3.3.2项目:遗传' }, ] }, + { + text: '4.3.4最优化', + collapsed: true, + items: [ + { text: '4.3.4最优化', link: '/4.人工智能/4.3.4最优化' }, + { text: '4.3.4.1程序示例', link: '/4.人工智能/4.3.4.1程序示例' }, + { text: '4.3.4.2项目:填词游戏', link: '/4.人工智能/4.3.4.2项目:填词游戏' }, + ] + }, ] }, { text: '4.4FAQ:常见问题', link: '/4.人工智能/4.4FAQ:常见问题' }, diff --git a/4.人工智能/4.3.1.1程序示例——maze迷宫解搜索.md b/4.人工智能/4.3.1.1程序示例——maze迷宫解搜索.md index 4c1cc8f..68d91d7 100644 --- a/4.人工智能/4.3.1.1程序示例——maze迷宫解搜索.md +++ b/4.人工智能/4.3.1.1程序示例——maze迷宫解搜索.md @@ -9,8 +9,6 @@ 本节附件下载 ::: -/4.人工智能/code/MAZE.zip - ## Node ```python @@ -149,7 +147,7 @@ class Maze: actions = [] cells = [] while node.parent is not None: # 遍历父节点得到路径动作 - actions.append(node.action) + actions.append(node.action) cells.append(node.state) node = node.parent actions.reverse() @@ -200,12 +198,11 @@ class Maze: ![](https://cdn.xyxsw.site/UVssbyMxCoEQSuxvjh3caWAFnOb.png) 根节点的值是多少? - -- 2 -- 3 -- 4 -- 5 -- 6 -- 7 -- 8 -- 9 + 1. 2 + 2. 3 + 3. 4 + 4. 5 + 5. 6 + 6. 7 + 7. 8 + 8. 9 diff --git a/4.人工智能/4.3.1搜索.md b/4.人工智能/4.3.1搜索.md index fc3b276..ccfe88e 100644 --- a/4.人工智能/4.3.1搜索.md +++ b/4.人工智能/4.3.1搜索.md @@ -120,9 +120,9 @@ 边域从节点 A 初始化开始 -a. 取出边域中的节点 A,展开节点 A,将节点 B 添加到边域。 -b. 取出节点 B,展开,添加...... -c. 到达目标节点,停止,返回解决方案 +1. 取出边域中的节点 A,展开节点 A,将节点 B 添加到边域。 +2. 取出节点 B,展开,添加...... +3. 到达目标节点,停止,返回解决方案 ![](https://cdn.xyxsw.site/XmnObIGaUoF4ssxkgzUc4vTUnmf.png) @@ -212,9 +212,9 @@ def remove(self): ![](https://cdn.xyxsw.site/SU2DbQeN2oxs5ex3K3NcMaJfnch.png) - $A^*$搜索 - + - 作为贪婪最佳优先算法的一种发展,$A^*$搜索不仅考虑了从当前位置到目标的估计成本$h(n)$,还考虑了直到当前位置为止累积的成本$g(n)$。通过组合这两个值,该算法可以更准确地确定解决方案的成本并在旅途中优化其选择。该算法跟踪(到目前为止的路径成本 + 到目标的估计成本,$g(n)+h(n)$),一旦它超过了之前某个选项的估计成本,该算法将放弃当前路径并返回到之前的选项,从而防止自己沿着$h(n)$错误地标记为最佳的却长而低效的路径前进。 - + - 然而,由于这种算法也依赖于启发式,所以它依赖它所使用的启发式。在某些情况下,它可能比贪婪的最佳第一搜索甚至不知情的算法效率更低。对于最佳的$A^*$搜索,启发式函数$h(n)$应该: - 可接受,从未高估真实成本。 @@ -232,11 +232,11 @@ def remove(self): - 极大极小算法 (Minimax) - 作为对抗性搜索中的一种算法,Minimax 将获胜条件表示为$(-1)$表示为一方,$(+1)$表示为另一方。进一步的行动将受到这些条件的驱动,最小化的一方试图获得最低分数,而最大化的一方则试图获得最高分数。 - + ![](https://cdn.xyxsw.site/FYu3bQwCZofBgsxKDJiciTR7nzc.png) - + - 井字棋 AI 为例 - + - $s_0$: 初始状态(在我们的情况下,是一个空的 3X3 棋盘) ![](https://cdn.xyxsw.site/WstnbmHwYoQauRxUQOCclz8Jngb.png) @@ -285,27 +285,29 @@ def remove(self): - 最大化玩家在$Actions(s)$中选择动作$a$,该动作产生$Min-value(Result(s,a))$ 的最高值。 - 最小化玩家在$Actions(s)$中选择动作$a$,该动作产生$Max-value(Result(s,a))$ 的最小值。 - - Function Max-Value(state): - - $$v=-\infty$$ - - if $Terminal(state)$: - - return $Utility(state)$ - - for $action$ in $Actions(state)$: - - $$v = Max(v, Min-Value(Result(state, action)))$$ - - return $v$ - - Function Min-Value(state): - - $$v=\infty$$ - - if $Terminal(state)$: - - return $Utility(state)$ - - for $action$ in $Actions(state)$: - - $$v = Min(v, Max-Value(Result(state, action)))$$ - - return $v$ + ```txt + Function Max-Value(state): + v=-∞ + if Terminal(state): + return Utility(state) + for action in Actions(state): + v = Max(v, Min-Value(Result(state, action))) + return v + Function Min-Value(state): + v=+∞ + if Terminal(state): + return Utility(state) + for action in Actions(state): + v = Min(v, Max-Value(Result(state, action))) + return v + ``` 不会理解递归?也许你需要看看这个:[阶段二:递归操作](../3.%E7%BC%96%E7%A8%8B%E6%80%9D%E7%BB%B4%E4%BD%93%E7%B3%BB%E6%9E%84%E5%BB%BA/3.6.4.2%E9%98%B6%E6%AE%B5%E4%BA%8C%EF%BC%9A%E9%80%92%E5%BD%92%E6%93%8D%E4%BD%9C.md) - $\alpha$-$\beta$剪枝 (Alpha-Beta Pruning) - 作为一种优化 Minimax 的方法,Alpha-Beta 剪枝跳过了一些明显不利的递归计算。在确定了一个动作的价值后,如果有初步证据表明接下来的动作可以让对手获得比已经确定的动作更好的分数,那么就没有必要进一步调查这个动作,因为它肯定比之前确定的动作不利。 - + - 这一点最容易用一个例子来说明:最大化的玩家知道,在下一步,最小化的玩家将试图获得最低分数。假设最大化玩家有三个可能的动作,第一个动作的值为 4。然后玩家开始为下一个动作生成值。要做到这一点,如果当前玩家做出这个动作,玩家会生成最小化者动作的值,并且知道最小化者会选择最低的一个。然而,在完成最小化器所有可能动作的计算之前,玩家会看到其中一个选项的值为 3。这意味着没有理由继续探索最小化玩家的其他可能行动。尚未赋值的动作的值无关紧要,无论是 10 还是(-10)。如果该值为 10,则最小化器将选择最低选项 3,该选项已经比预先设定的 4 差。如果尚未估价的行动结果是(-10),那么最小化者将选择(-10)这一选项,这对最大化者来说更加不利。因此,在这一点上为最小化者计算额外的可能动作与最大化者无关,因为最大化玩家已经有了一个明确的更好的选择,其值为 4。 ![](https://cdn.xyxsw.site/LDZab4TeMoByvDxF1Onc8WQenpb.png) diff --git a/4.人工智能/4.3.2知识推理.md b/4.人工智能/4.3.2知识推理.md index 780e8a9..2121334 100644 --- a/4.人工智能/4.3.2知识推理.md +++ b/4.人工智能/4.3.2知识推理.md @@ -82,7 +82,7 @@ | 1 | 1 | 1 | - Biconditional ($\leftrightarrow$) :是一个双向的蕴含。你可以把它读成“如果且仅当”$P↔ Q$等同$P→ Q$和$Q→ P$合在一起。例如,如果$P$:“正在下雨”,$Q$:“我在室内”,那么$P↔ Q$的意思是“如果下雨,那么我在室内”,“如果我在室内,那么就在下雨。”这意味着我们可以推断出比简单蕴含更多的东西。如果$P$为假,那么$Q$ 也为假;如果不下雨,我们知道我也不在室内。 - + | $P$ | $Q$ | $P\leftrightarrow Q$ | | --- | --- | -------------------- | | 0 | 0 | 1 | @@ -141,7 +141,7 @@ dumbledore # 哈利拜访了邓布利多。请注意,虽然之前的命题包 ```python def check_all(knowledge, query, symbols, model):# 如果模型对每个符号都有一个赋值 # (下面的逻辑可能有点混乱:我们从命题符号列表开始。该函数是递归的,每次调用自身时,它都会从命题符号列表中弹出一个命题符号并从中生成模型。因此,当命题符号列表为空时,我们知道我们已经完成生成模型,其中包含每个可能的命题真值分配。) - if not symbols: + if not symbols: # 如果知识库在模型中为真,则查询结论也必须为真 if knowledge.evaluate(model): return query.evaluate(model) @@ -271,9 +271,7 @@ def check_all(knowledge, query, symbols, model):# 如果模型对每个符号都 - 使用德摩根定律,将否定向内移动,直到只有原子命题被否定(而不是从句) - 将$\lnot(\alpha∧β)$转换为$\lnotα\lor\lnotβ$ - - 下面是一个转换$(P∧Q)\to R$ - - 到合取范式的例子: + - 下面是一个转换$(P∧Q)\to R$到合取范式的例子: - $(P ∨ Q) → R$ - $\lnot(P\lor Q)\lor R$蕴含消除 diff --git a/4.人工智能/4.3.3不确定性问题.md b/4.人工智能/4.3.3不确定性问题.md index 6966537..6c049bb 100644 --- a/4.人工智能/4.3.3不确定性问题.md +++ b/4.人工智能/4.3.3不确定性问题.md @@ -18,9 +18,7 @@ - 一般来说,值越高,事件发生的可能性就越大。 - 每一个可能发生的事件的概率加在一起等于 1。 -$ -\sum_{\omega\in\Omega}P(\omega)=1 -$ + $\sum_{\omega\in\Omega}P(\omega)=1$ - 用标准骰子掷出数字 R 的概率可以表示为 $P(R)$ 。在我们的例子中,$P(R)=1/6$ ,因为有六个可能的世界(从 1 到 6 的任何数字),并且每个世界有相同的可能性发生。现在,考虑掷两个骰子的事件。现在,有 36 个可能的事件,同样有相同的可能性发生。 diff --git a/4.人工智能/4.3.4.1程序示例.md b/4.人工智能/4.3.4.1程序示例.md new file mode 100644 index 0000000..8e0d0bc --- /dev/null +++ b/4.人工智能/4.3.4.1程序示例.md @@ -0,0 +1,339 @@ +# 程序示例 + +::: tip +阅读程序并运行 + +完成习题 +::: + +::: tip 📥 +本节附件下载 +::: + +## Hospital(局部搜索) + +```python +import random + +class Space(): + def __init__(self, height, width, num_hospitals): + """创建一个具有给定维度的新状态空间""" + self.height = height # 高度 + self.width = width # 宽度 + self.num_hospitals = num_hospitals # 医院数量 + self.houses = set() # 住房位置集合 + self.hospitals = set() # 医院位置集合 + + def add_house(self, row, col): + """在状态空间中的特定位置添加住房""" + self.houses.add((row, col)) + + def available_spaces(self): + """返回住房或医院当前未使用的所有单元格""" + # 考虑所有可能的单元格 + candidates = set( + (row, col) + for row in range(self.height) + for col in range(self.width) + ) + # 排除所有住房和医院 + for house in self.houses: + candidates.remove(house) + for hospital in self.hospitals: + candidates.remove(hospital) + return candidates + + def hill_climb(self, maximum=None, image_prefix=None, log=False): + """执行爬山算法找到解决方案""" + count = 0 + # 从随机初始化的医院位置开始 + self.hospitals = set() + for i in range(self.num_hospitals): + self.hospitals.add(random.choice(list(self.available_spaces()))) + ... + # 执行算法,直到达到最大迭代次数 + while maximum is None or count < maximum: + count += 1 + best_neighbors = [] + best_neighbor_cost = None + # 考虑所有医院移动 + for hospital in self.hospitals: + # 考虑一下那家医院的所有邻居 + for replacement in self.get_neighbors(*hospital): + # 生成一组相邻的医院 + neighbor = self.hospitals.copy() + neighbor.remove(hospital) + neighbor.add(replacement) + # 检查邻居是否是迄今为止最好的 + cost = self.get_cost(neighbor) + if best_neighbor_cost is None or cost < best_neighbor_cost: + best_neighbor_cost = cost + best_neighbors = [neighbor] + elif best_neighbor_cost == cost: + best_neighbors.append(neighbor) + # 没有一个邻居比目前的状态更好 + if best_neighbor_cost >= self.get_cost(self.hospitals): + return self.hospitals + # 移动到价值最高的邻居 + else: + ... + self.hospitals = random.choice(best_neighbors) + ... + + def random_restart(self, maximum, image_prefix=None, log=False): + """多次重复爬山算法""" + best_hospitals = None + best_cost = None + # 重复爬山算法的固定次数 + for i in range(maximum): + hospitals = self.hill_climb() + cost = self.get_cost(hospitals) + if best_cost is None or cost < best_cost: + best_cost = cost + best_hospitals = hospitals + ... + else: + ... + ... + return best_hospitals + + def get_cost(self, hospitals): + """计算从住房到最近医院的距离总和""" + cost = 0 + for house in self.houses: + cost += min( + abs(house[0] - hospital[0]) + abs(house[1] - hospital[1]) + for hospital in hospitals + ) + return cost + + def get_neighbors(self, row, col): + """返回尚未包含住房或医院的邻居""" + candidates = [ + (row - 1, col), + (row + 1, col), + (row, col - 1), + (row, col + 1) + ] + neighbors = [] + for r, c in candidates: + if (r, c) in self.houses or (r, c) in self.hospitals: + continue + if 0 <= r < self.height and 0 <= c < self.width: + neighbors.append((r, c)) + return neighbors + + def output_image(self, filename): + """生成所有房屋和医院的图像(不作要求)""" + from PIL import Image, ImageDraw, ImageFont + cell_size = 100 + cell_border = 2 + cost_size = 40 + padding = 10 + + # Create a blank canvas + img = Image.new( + "RGBA", + (self.width * cell_size, + self.height * cell_size + cost_size + padding * 2), + "white" + ) + house = Image.open("assets/images/House.png").resize( + (cell_size, cell_size) + ) + hospital = Image.open("assets/images/Hospital.png").resize( + (cell_size, cell_size) + ) + font = ImageFont.truetype("assets/fonts/OpenSans-Regular.ttf", 30) + draw = ImageDraw.Draw(img) + + for i in range(self.height): + for j in range(self.width): + + # Draw cell + rect = [ + (j * cell_size + cell_border, + i * cell_size + cell_border), + ((j + 1) * cell_size - cell_border, + (i + 1) * cell_size - cell_border) + ] + draw.rectangle(rect, fill="black") + + if (i, j) in self.houses: + img.paste(house, rect[0], house) + if (i, j) in self.hospitals: + img.paste(hospital, rect[0], hospital) + + # Add cost + draw.rectangle( + (0, self.height * cell_size, self.width * cell_size, + self.height * cell_size + cost_size + padding * 2), + "black" + ) + draw.text( + (padding, self.height * cell_size + padding), + f"Cost: {self.get_cost(self.hospitals)}", + fill="white", + font=font + ) + + img.save(filename) + +# 创建一个状态空间并随机添加住房 +s = Space(height=10, width=20, num_hospitals=3) +for i in range(15): + s.add_house(random.randrange(s.height), random.randrange(s.width)) +# 使用局部搜索来确定医院位置 +hospitals = s.random_restart(maximum=100, image_prefix="hospitals", log=True) +``` + +## Production(线性规划) + +```python +import scipy.optimize +# Objective Function: 50x_1 + 80x_2 +# Constraint 1: 5x_1 + 2x_2 <= 20 +# Constraint 2: -10x_1 + -12x_2 <= -90 +result = scipy.optimize.linprog( + [50, 80], # Cost function: 50x_1 + 80x_2 + A_ub=[[5, 2], [-10, -12]], # Coefficients for inequalities + b_ub=[20, -90], # Constraints for inequalities: 20 and -90 +) +if result.success: + print(f"X1: {round(result.x[0], 2)} hours") + print(f"X2: {round(result.x[1], 2)} hours") +else: + print("No solution") +``` + +## Schedule(约束满足) + +```python +"""没有任何启发式或推理的自然回溯搜索""" +VARIABLES = ["A", "B", "C", "D", "E", "F", "G"] +CONSTRAINTS = [ + ("A", "B"), + ("A", "C"), + ("B", "C"), + ("B", "D"), + ("B", "E"), + ("C", "E"), + ("C", "F"), + ("D", "E"), + ("E", "F"), + ("E", "G"), + ("F", "G") +] + +def backtrack(assignment): + """运行回溯搜索以查找赋值""" + # 检查赋值是否完成 + if len(assignment) == len(VARIABLES): + return assignment + # 尝试一个新变量 + var = select_unassigned_variable(assignment) + for value in ["Monday", "Tuesday", "Wednesday"]: + new_assignment = assignment.copy() + new_assignment[var] = value + if consistent(new_assignment): + result = backtrack(new_assignment) + if result is not None: + return result + return None + +def select_unassigned_variable(assignment): + """按顺序选择尚未赋值的变量""" + for variable in VARIABLES: + if variable not in assignment: + return variable + return None + + +def consistent(assignment): + """检查分配是否一致""" + for (x, y) in CONSTRAINTS: + # 仅考虑变量赋值都已指定的弧 + if x not in assignment or y not in assignment: + continue + # 如果两者的值相同,则不一致 + if assignment[x] == assignment[y]: + return False + # 如果没有不一致的地方,那么赋值是一致的 + return True + +solution = backtrack(dict()) +print(solution) +``` + +使用命令`pip install python-constraint`安装constraint库 + +```python +from constraint import * + +problem = Problem() + +# 添加变量 +problem.addVariables( + ["A", "B", "C", "D", "E", "F", "G"], + ["Monday", "Tuesday", "Wednesday"] +) + +# 添加约束 +CONSTRAINTS = [ + ("A", "B"), + ("A", "C"), + ("B", "C"), + ("B", "D"), + ("B", "E"), + ("C", "E"), + ("C", "F"), + ("D", "E"), + ("E", "F"), + ("E", "G"), + ("F", "G") +] +for x, y in CONSTRAINTS: + problem.addConstraint(lambda x, y: x != y, (x, y)) + +# Solve problem +for solution in problem.getSolutions(): + print(solution) +``` + +## Quiz + +1. 对于以下哪一项,即使多次重新运行算法,也会始终找到相同的解决方案? + 假设一个问题的目标是最小化成本函数,并且状态空间中的每个状态都有不同的成本。 + 1. Steepest-ascent hill-climbing,每次从不同的初始状态开始 + 2. Steepest-ascent hill-climbing,每次都从相同的初始状态开始 + 3. Stochastic hill-climbing,每次从不同的初始状态开始 + 4. Stochastic hill-climbing,每次都从相同的初始状态开始 + 5. 无论是steepest-ascent还是stochastic hill climbing,只要你总是从同一个初始状态开始 + 6. 无论是steepest-ascent还是stochastic hill climbing,只要每次都从不同的初始状态开始 + 7. 没有任何版本的爬山算法能保证每次都能得到相同的解决方案 + +2. 下面两个问题都会问你关于下面描述的优化问题。 + 一位农民正在尝试种植两种作物,作物1和作物2,并希望实现利润最大化。农民将从种植的每英亩作物1中获得500美元的利润,从种植的每英亩作物2中获得400美元的利润。然而,农民今天需要在早上7点到晚上7点之间的12个小时内完成所有的种植。种植一英亩的作物1需要3个小时,种植一英亩作物2需要2个小时。农民在供应方面也很有限:他有足够的供应种植10英亩的作物1,有足够的资源种植4英亩的作物2。假设变量C1表示要种植的作物1的英亩数,变量C2表示要种植作物2的英亩数。 + + 对于这个问题,什么是有效的目标函数? + 1. 10 \* C1 + 4 \* C2 + 2. -3 \* C1 - 2 \* C2 + 3. 500 \* 10 \* C1 + 400 \* 4 \* C2 + 4. 500 \* C1 + 400 \* C2 + 5. C1 + C2 + +3. 这个问题的制约因素是什么? + 1. 3 \* C1 + 2 \* C2 <= 12, C1 <= 10, C2 <= 4 + 2. 3 \* C1 + 2 \* C2 <= 12, C1 + C2 <= 14 + 3. 3 \* C1 <= 10, 2 \* C2 <= 4 + 4. C1 + C2 <= 12, C1 + C2 <= 14 + +4. 下面的问题将问你以下考试安排约束满足图,其中每个节点代表一个课程。每门课程都与可能的考试日的初始域相关联(大多数课程可能在周一、周二或周三;少数课程已经被限制在一天内)。两个节点之间的边意味着这两个课程必须在不同的日子进行考试。 + + 在对整个问题满足弧一致性之后,变量C、D和E的结果域是什么? + 1. C的域是\{Mon,Tue\},D的域是\{Wed\},E的域是\{Mon\} + 2. C的域是\{Mon\},D的域是\{Wed\},E的域为\{Tue\} + 3. C的域是\{Mon\},D的域是\{Tue\},E的域为\{Wed\} + 4. C的域是\{Mon\},D的域是\{Mon,Wed\},E的域是\{Tue,Wed\} + 5. C的域是\{Mon,Tue,Wed\},D的域是\{Mon,Wed\},E的域是\{Mon,Tue,Wed\} + 6. C的域是\{Mon\},D的域是\{Mon,Wed\},E的域是\{Mon,Tue,Wed\} diff --git a/4.人工智能/4.3.4.2项目:填词游戏.md b/4.人工智能/4.3.4.2项目:填词游戏.md new file mode 100644 index 0000000..be3d9b2 --- /dev/null +++ b/4.人工智能/4.3.4.2项目:填词游戏.md @@ -0,0 +1,134 @@ +# 项目——填词游戏 + +::: tip +我们为你提供了一个简单有趣的项目,帮助你进行知识巩固,请认真阅读文档内容。 + +如果你卡住了,请记得回来阅读文档,或请求身边人的帮助。 +::: + +::: tip 📥 +本节附件下载 +::: + +编写一个人工智能来完成填词游戏。 + +```txt +$ python generate.py data/structure1.txt data/words1.txt output.png +██████████████ +███████M████R█ +█INTELLIGENCE█ +█N█████N████S█ +█F██LOGIC███O█ +█E█████M████L█ +█R███SEARCH█V█ +███████X████E█ +██████████████ +``` + +![4.3.4.2-0](static/4.3.4.2-0.png) + +## 背景 + +- 你如何生成一个填字游戏?考虑到填字游戏的结构(即网格中哪些方格需要填入字母),以及要使用的单词列表,问题就变成了选择哪些单词应该填入每个垂直或水平的方格序列。我们可以将这种问题建模为一个约束满足问题。每一个方格序列都是一个变量,我们需要决定它的值(在可能的单词域中哪个单词将被填入该序列)。考虑一下下面的字谜结构。 + +![4.3.4.2-1](static/4.3.4.2-1.png) + +- 在这个结构中,我们有四个变量,代表了我们需要填入这个字谜的四个单词(在上图中每个单词都用数字表示)。每个变量由四个值定义:它开始的行(`i`值),它开始的列(`j`值),单词的方向(纵向或横向down or across),以及单词的长度。例如,`变量1`将是一个由第1行(假设从顶部计数的0索引)、第1列(假设从左边计数的0索引)、方向为`across`和`4`的长度表示的变量。 + +- 与许多约束满足问题一样,这些变量有一元和二元约束。变量的一元约束是由其长度决定的。例如,对于`变量1`来说,数值`BYTE`可以满足一元约束,但是数值`BIT`则不能(它有错误的字母数量)。因此,任何不满足变量的一元约束的值都可以立即从变量的域中删除。 + +- 变量的二元约束是由其与相邻变量的重合度给出的。`变量1`有一个邻居:`变量2`。`变量2`有两个邻居:`变量1`和`变量3`。对于每一对相邻的变量,这些变量都有一个重叠部分:一个它们共同的单方块。我们可以将这种重叠表示为每个变量有用相同字符位置的索引对。例如,`变量1`和`变量2`之间的重叠可以表示为一对`(1, 0)`,这意味着`变量1`在索引1处的字符必然与`变量2`在索引0处的字符相同(再次假设索引从0开始)。因此,`变量2`和`变量3`之间的重叠将被表示为一对`(3, 1)`,`变量2`的值的字符`3`必须与`变量3`的值的字符`1`相同。 + +- 对于这个问题,我们将增加一个额外的约束条件,即所有的单词必须是不同的:同一个单词不应该在谜题中重复出现多次。 + +- 那么,接下来的挑战是写一个程序来找到一个满意的赋值:为每个变量提供一个不同的词(来自给定的词汇表),从而满足所有的一元和二元约束。 +理解 + +- 这个项目中有两个Python文件:`crossword.py`和`generate.py`。第一个文件已经完全为你写好了,第二个文件有一些函数留给你去实现。 + +- 首先,让我们看一下`crossword.py`。这个文件定义了两个类,`Variable`(代表填字游戏中的变量)和`Crossword`(代表填字游戏本身)。 + +- 注意,要创建一个变量,我们必须指定四个值:它的第`i`行,第`j`列,它的方向(常数`Variable.ACROSS`或常数`Variable.DOWN``),以及它的长度(`length``)。 + +- 字谜类需要两个值来创建一个新的字谜:一个定义了字谜结构的`structure_file`(`_`用来代表空白单元格,任何其他字符都代表不会被填入的单元格)和一个定义了字词列表(每行一个)的`word_file`,用来作为游戏的词汇表。这些文件的三个例子可以在项目的数据目录中找到,也欢迎你自己创建。 + +- 特别要注意的是,对于任何一个字谜对象的字谜,我们都会存储以下的数值: + - `crossword.height`是一个整数,代表填字游戏的高度。 + - `crossword.width`是一个整数,代表填字游戏的宽度。 + - `crossword.structure`是一个二维列表,代表字谜的结构。对于任何有效的第i行和第j列,如果该单元格是空白的,`crossword.structure[i][j]`将为真(必须在该单元格中填入一个字符),否则将为假(该单元格中没有字符要填)。 + - `crossword.words`是一个包含所有单词的集合,在构建填字游戏的时候,可以从这些单词中提取。 + - `crossword.variables`是谜题中所有变量的集合(每个变量都是一个Variable对象)。 + - `crossword.overlaps`是一个字典,它将一对变量映射到它们的重合处。对于任何两个不同的变量v1和v2,如果这两个变量没有重叠,`crossword.overlaps[v1, v2]`将是`None`,如果这两个变量有重叠,则是一对整数`(i, j)`。这对`(i, j)`应被解释为:`v1`的第`i`个字符的值必须与`v2`的第`j`个字符的值相同。 + +- `Crossword`对象还支持一个方法`neighbors`,它可以返回与给定变量重叠的所有变量。也就是说,`crossword.neighbors(v1)`将返回一个与变量`v1`相邻的所有变量的集合。 + +- 接下来,看一下`generate.py`。在这里,我们定义了一个`CrosswordCreator`类,我们将用它来解决填字游戏。当一个`CrosswordCreator`对象被创建时,它得到一个填字游戏的属性,它应该是一个`Crossword`对象(因此具有上面描述的所有属性)。每个`CrosswordCreator`对象还得到一个域属性:一个字典,它将变量映射到该变量可能作为一个值的一组词。最初,这组词是我们词汇表中的所有词,但我们很快就会写函数来限制这些域。 + +- 我们还为你定义了一些函数,以帮助你测试你的代码:`print`将向终端打印你的填字游戏的一个给定的赋值(每个赋值,在这个函数和其他地方,是一个字典,将变量映射到它们相应的词)。同时,`save`将生成一个与给定作业相对应的图像文件(如果你无法使用这个函数,你需要`pip3 install Pillow`)。 `letter_grid`是一个被`print`和`save`使用的辅助函数,它为给定的赋值生成一个所有字符在其适当位置的2D列表:你可能不需要自己调用这个函数,但如果你想的话,欢迎你这样做。 + +- 最后,注意`solve`函数。这个函数做了三件事:首先,它调用`enforce_node_consistency`来强制执行填字游戏的节点一致性,确保变量域中的每个值都满足一元约束。接下来,该函数调用`ac3`来强制执行弧一致性,确保二元约束得到满足。最后,该函数在最初的空赋值(空字典dict())上调用`backtrack`,试图计算出问题的解决方案。 + +- 不过,`enforce_node_consistency`、`ac3`和`backtrack`等函数还没有实现(以及其他函数)。这就是你的任务。 + +## 明确 + +- 完成`grece_node_consistency`, `revise`, `ac3`, `assignment_complete`, `consistent`, `order_domain_values`, `selected_unassigned_variable`和`backtrack`在`generate.py`中的实现,这样如果有有解的话你的人工智能就能生成完整的字谜。 + +- `enforce_node_consistency`函数应该更新`self.domains`,使每个变量都是节点一致的。 + - 回顾一下,当对每个变量来说,其域中的每个值都与该变量的一元约束一致时,就实现了节点一致性。在填字游戏的情况下,这意味着要确保变量域中的每个值的字母数与变量的长度相同。 + - 要从一个变量`v`的域中移除一个值`x`,因为`self.domains`是一个将变量映射到数值集的字典,你可以调用`self.domains[v].remove(x)`。 + - 这个函数不需要返回值。 +- `revise`函数应该使变量x与变量y保持弧一致性。 + - `x`和`y`都是`Variable`对象,代表谜题中的变量。 + - 回顾一下,当`x`的域中的每一个值在`y`的域中都有一个不引起冲突的可能值时,`x`就与`y`保持弧一致性。(在填字游戏的背景下,冲突是指一个方格,两个变量对它的字符值意见不一)。 + - 为了使`x`与`y`保持一致,你要从`x`的域中删除任何在`y`的域中没有相应可能值的值。 + - 回顾一下,你可以访问`self.crossword.overlaps`来获得两个变量之间的重叠,如果有的话。 + - `y`的域应该不被修改。 + - 如果对`x`的域进行了修改,该函数应返回`True`;如果没有修改,则应返回`False`。 + +- `ac3`函数应该使用`AC3`算法,对问题实施弧一致性。回顾一下,当每个变量域中的所有值都满足该变量的二进制约束时,就实现了弧一致性。 + - 回顾一下,`AC3`算法保持着一个要处理的弧的队列。这个函数需要一个叫做`arcs`的可选参数,代表要处理的弧的初始列表。如果`arcs`是`None`,你的函数应该从问题中的所有弧的初始队列开始。否则,你的算法应该从一个初始队列开始,该队列中只有在列表`arcs`中的弧(其中每个弧是一个变量`x`和另一个变量`y`的元组`(x,y)`)。 + - 回顾一下,为了实现`AC3`,你要一次一次地修改队列中的每个弧。不过,任何时候你对一个域做了改变,你可能需要在队列中增加额外的弧,以确保其他弧保持一致。 + - 你可能会发现在`ac3`的实现中调用`revise`函数是很有帮助的。 + - 如果在执行弧一致性的过程中,你从一个域中删除了所有剩余的值,则返回`False`(这意味着问题无解,因为这个变量已经没有可能的值了)。否则,返回`True`。 + - 你不需要担心在这个函数中强制执行词的唯一性(你将在`consistent`函数中实现这个检查。) + +- `assignment_complete`函数应该(如其名所示)检查一个给定的赋值是否完成。 + - `assignment`是一个字典,其中键是`Variable`对象,值是代表这些变量将采取的单词的字符串。 + - 如果每个字谜变量都被分配到一个值(不管这个值是什么),那么这个赋值就是完整的。 + - 如果赋值完成,该函数应该返回`True`,否则返回`False`。 + +- `consistent`函数应该检查一个给定的`assignment`是否一致。 + - `assignment`是一个字典,其中的键是`Variable`对象,值是代表这些变量将采取的词语的字符串。请注意,赋值不一定是完整的:不是所有的变量都会出现在赋值中。 + - 如果一个赋值满足问题的所有约束条件,那么它就是一致的:也就是说,所有的值都是不同的,每个值的长度都是正确的,并且相邻的变量之间没有冲突。 + - 如果赋值是一致的,该函数应该返回`True`,否则返回`False`。 + +- `order_domain_values`函数应该返回一个`var`域中所有数值的列表,根据最小约束值启发式排序。 + - `var`将是一个变量对象,代表谜题中的一个变量。 + - 回顾一下,最小约束值启发式的计算方法是一个赋值导致约束邻近的未分配的变量的数量。也就是说,如果给`var`赋值的结果是排除了邻近变量的`n`个可能的选择,你应该按照`n`的升序排列你的结果。 + - 请注意,在`assignment`中出现的任何变量都已经有了一个值,因此在计算相邻未赋值变量被排除的值的数量时不应该被计算在内。 + - 对于排除相邻变量相同数量可能选择的域值,任何排序都是可以接受的。 + - 回顾一下,你可以访问`self.crossword.overlaps`来获得两个变量之间的重叠,如果有的话。 + - 首先通过返回一个任意顺序的数值列表来实现这个函数可能会有帮助(这仍然会产生正确的填字游戏)。一旦你的算法开始工作,你就可以回去确保这些值是以正确的顺序返回的。 + - 你可能会发现根据一个特定的key来对一个[列表](https://docs.python.org/3/howto/sorting.html)进行排序是很有帮助的: Python 包含一些有用的函数来实现这一点。 + +- `select_unassigned_variable`函数应该根据最小剩余值启发式,然后是度启发式,返回字谜中尚未被赋值的单个变量。 + - `assignment`是一个字典,其中键是`Variable`对象,值是代表这些变量将承担的单词的字符串。你可以假设赋值不会是完整的:不是所有的变量都会出现在`assignment`中。 + - 你的函数应该返回一个`Variable`对象。你应该返回在其域中剩余数值最少的变量。如果变量之间存在平局,你应该在这些变量中选择度最大的变量(拥有最多的邻居)。如果在这两种情况下都相同,你可以在相同的变量中任意选择。 + - 首先通过返回任意未分配的变量来实现这个函数可能是有帮助的(这应该仍然会产生正确的填字游戏)。一旦你的算法开始工作,你就可以回去修改这个函数确保你是根据启发式方法返回一个变量。 + - 你可能会发现根据一个特定的key来对一个列表进行[排序](https://docs.python.org/3/howto/sorting.html)是很有帮助的:Python 包含一些有用的函数来实现这一点。 + +- `backtrack`函数应该接受一个部分赋值`assignment`作为输入,并且使用回溯搜索,如果有可能的话,返回一个完整的令人满意的变量赋值。 + - `assignment`是一个字典,其中键是`Variable`对象,值是代表这些变量将承担的单词的字符串。你可以假设赋值不会是完整的:不是所有的变量都会出现在`assignment`中。 + - 如果有可能生成一个令人满意的字谜,你的函数应该返回完整的赋值:一个字典,其中每个变量是一个键,值是该变量应该承担的单词。如果不可能产生令人满意的赋值,该函数应该返回`None`。 + - 如果你愿意,你可能会发现,如果你把搜索和推理交织在一起,你的算法会更有效率(比如每次做新的赋值时都要保持弧一致性)。我们不要求你这样做,但允许你这样做,只要你的函数仍然产生正确的结果。(正是由于这个原因,`ac3`函数允许一个`arcs`的参数,以防你想从不同的弧队列开始)。 + +- 除了要求你实现的函数外,你不应该修改`generate.py`中的任何其他东西,尽管你可以编写额外的函数和/或导入其他Python标准库模块。如果你熟悉`numpy`或`pandas`,你也可以导入它们,但是你不应该使用任何其他的第三方Python模块。你不应该修改`crossword.py`中的任何东西。 + +## 提示 + +- 对于`order_domain_values`和`select_unassigned_variable`来说,不以启发式方法实现它们,然后再添加启发式方法可能会有帮助。你的算法仍然可以工作:只是在找到一个解决方案之前,它可能会探索更多的分配,而不是它需要的。 + +- 要运行你的程序,你可以运行类似`python generate.py data/structure1.txt data/words1.txt`的命令,指定一个结构文件和一个单词文件。如果可以进行赋值,你应该看到打印出来的赋值。你也可以为图像文件添加一个额外的命令行参数,如运行`python generate.py data/structure1.txt data/words1.txt output.png`,可以为生成的填字游戏生成一个图像表示。 + +- `Crossword`类有一个`neighbors`函数,可以用来访问某个特定变量的所有邻居(即重叠的变量)。在你需要确定某个特定变量的邻居时,请随时使用这个函数。 diff --git a/4.人工智能/4.3.4最优化.md b/4.人工智能/4.3.4最优化.md new file mode 100644 index 0000000..9fd70ab --- /dev/null +++ b/4.人工智能/4.3.4最优化.md @@ -0,0 +1,293 @@ +# 最优化 + +- 最优化是指从一组可能的选项中选择最佳选项。我们已经遇到过试图找到最佳选项的问题,比如在极大极小算法中,今天我们将学习一些工具,可以用来解决更广泛的问题。 + +## 局部搜索(Local Search) + +- 局部搜索是一种保持单一节点并通过移动到邻近的节点进行搜索的搜索算法。这种类型的算法与我们之前看到的搜索类型不同。例如,在解决迷宫的过程中,我们想找到通往目标的最快捷的方法,而局部搜索则对寻找问题的最佳答案感兴趣。通常情况下,局部搜索会带来一个不是最佳但 "足够好 "的答案,以节省计算能力。考虑一下下面这个局部搜索问题的例子:我们有四所房子在设定的位置。我们想建两所医院,使每所房子到医院的距离最小。这个问题可以形象地描述如下: + +![4.3.4-0](static/4.3.4-0.png) + +- 在这幅图中,我们看到的是房屋和医院的可能配置。它们之间的距离是用曼哈顿距离(向上、向下和向两侧移动的次数;在[搜索](4.3.1搜索.md) 中详细讨论)来衡量的,从每个房子到最近的医院的距离之和是17。我们称其为成本 __(cost)__,因为我们试图使这个距离最小化。在这种情况下,一个状态将是房屋和医院的任何一个配置。 + +- 把这个概念抽象化,我们可以把每一种房屋和医院的配置表现为下面的状态空间图。图中的每一条都代表一个状态的值,在我们的例子中,它是房屋和医院配置的成本。 + +![4.3.4-1](static/4.3.4-1.png) + +- 从这个可视化的角度来看,我们可以为我们接下来的讨论定义几个重要的术语: + - 目标函数 __(Objective Function)__ 是一个函数,我们用它来最大化解决方案的值。 + - 成本函数 __(Cost Function)__ 是一个我们用来最小化解决方案成本的函数(这就是我们在房屋和医院的例子中要使用的函数。我们想要最小化从房屋到医院的距离)。 + - 当前状态 __(Current State)__ 是指函数目前正在考虑的状态。 + - 邻居状态 __(Neighbor State)__ 是当前状态可以过渡到的一个状态。在上面的一维状态空间图中,邻居状态是指当前状态两侧的状态。在我们的例子中,邻居状态可以是将其中一家医院向任何方向移动一步所产生的状态。邻居状态通常与当前状态相似,因此,其值与当前状态的值接近。 + +- 请注意,局部搜索算法的工作方式是考虑当前状态下的一个节点,然后将该节点移动到当前状态的一个邻节点处。这与极大极小算法不同,例如,在极大极小算法中,状态空间中的每一个状态都被递归地考虑。 + +## 爬山算法(Hill Climbing) + +- 爬山算法是局部搜索算法的一种类型。在这个算法中,邻居的状态与当前的状态进行比较,如果其中任何一个状态更好,我们就把当前的节点从当-的状态改为该邻居的状态。“好状态”的定义是由目标函数决定的,倾向于一个较高的值,或一个递减函数,倾向于一个较低的值。 + +- 一个爬山算法在伪代码中会有以下样子: + +```txt +function Hill-Climb(problem): + current = initial state of problem + repeat: + neighbor = best valued neighbor of current + if neighbor not better than current: + return current + current = neighbor +``` + +- 在这个算法中,我们从一个当前状态开始。在一些问题中,我们会知道当前的状态是什么,而在其他问题中,我们将不得不从随机选择一个状态开始。然后,我们重复以下动作:我们评估邻居状态,选择一个具有最佳值的邻居状态。然后,我们将这个邻居状态的值与当前状态的值进行比较。如果邻居状态更好,我们将当前状态切换到邻居状态,然后重复这个过程。当我们将最佳邻居与当前状态进行比较,并且当前状态更好时,该过程就结束了。然后,我们返回当前状态。 + +- 使用爬山算法,我们可以开始改进我们在例子中分配给医院的位置。经过几次转换,我们得到了以下状态: + +![4.3.4-2](static/4.3.4-2.png) + +- 在这个状态下,成本是11,比初始状态的成本17有所提高。然而,这还不是最佳状态。例如,将左边的医院移到左上角的房子下面,会使成本达到9,比11好。然而,这个版本的爬山算法无法达到这个目标,因为所有的邻居状态都至少和当前状态的成本一样高。从这个意义上说,爬坡算法是短视的,它经常满足于比其他一些解决方案更好的解决方案,但不一定是所有可能的解决方案中最好的。 + +### 局部和全局最小值和最大值 + +- 如上所述,爬山算法可能卡在局部最大值或最小值中。局部最大值是一个比其相邻状态有更高值的状态。而全局最大值是指在状态空间的所有状态中具有最高值的状态。 + +![4.3.4-3](static/4.3.4-3.png) + +- 相比之下,局部最小值是一个比其相邻状态的值更低的状态。与此相对,全局最小值是指在状态空间中所有状态中具有最低值的状态。 + +![4.3.4-4](static/4.3.4-4.png) + +- 爬山算法的问题是,它们可能会在局部最小和最大中结束。一旦算法到达一个点,其邻居状态就目标函数而言,比当前状态更糟糕,算法就会停止。特殊类型的局部最大值和最小值包括平坦的局部最大值/最小值 __(flat local maximum/minimum)__,即多个数值相同的状态相邻,形成一个plateau,其邻居状态的数值更差;以及 __shoulder__ ,邻居状态既可以更好,也可以更差。从plateau的中间开始,算法将无法向任何方向推进。 + +![4.3.4-5](static/4.3.4-5.png) + +### 爬山算法的变体 + +- 由于爬山算法的局限性,人们想到了多种变体来克服卡在局部最小值和最大值的问题。该算法的所有变体的共同点是,无论采用何种策略,每一种变体都有可能陷入局部最小或最大,并且没有办法继续优化。下面的算法是这样表述的:数值越大越好,但它们也适用于成本函数,其目标是使成本最小化。 + - __Steepest-ascent__: 选择值最高的邻居状态。 + - Stochastic: 从值较高的邻居状态中随机选择。这样做,我们选择去任何比我们的值更高的方向。 + - __First-choice__: 选择第一个值较高的邻居状态。 + - __Random-restart__: 进行多次爬山。每次都从一个随机状态开始。比较每次试验的最大值,并在这些最大值中选择一个。 + - __Local Beam Search__: 选择值最高的k个邻居状态。这与大多数本地搜索算法不同,它使用多个节点进行搜索,而不是只有一个节点。 + +- 虽然局部搜索算法并不总是给出最好的解决方案,但在考虑所有可能的状态在计算上不可行的情况下,它们往往能给出足够好的解决方案。 + +## 模拟退火算法(Simulated Annealing) + +- 尽管我们已经看到了可以改进爬山算法的变种,但它们都有一个共同的错误:一旦算法达到局部最大值,它就会停止运行。模拟退火算法允许算法在卡在局部最大值时"摆脱"自己。 + +- 退火是指加热金属并让其缓慢冷却的过程,其作用是使金属变硬。这被用来比喻模拟退火算法,该算法开始时温度较高,更有可能做出随机决定,随着温度的降低,它变得不太可能做出随机决定,变得更加"坚定"。这种机制允许算法将其状态改变为比当前状态更差的邻居状态,这就是它如何摆脱局部最大值的原因。以下是模拟退火法的伪代码: + +```txt +function Simulated-Annealing(problem, max): + current = initial state of problem + for t = 1 to max: + T = Temperature(t) + neighbor = random neighbor of current + ΔE = how much better neighbor is than current + if ΔE > 0: + current = neighbor + with probability e^(ΔE/T) set current = neighbor + return current +``` + +- 该算法将一个`problem`和`max`作为输入,`max`是它应该重复的次数。对于每个迭代,`T`是用一个`Temperature`函数来设置的。这个函数在早期迭代中返回一个较高的值(当`t`较低时),在后期迭代中返回一个较低的值(当`t`较高时)。然后,随机选择一个邻居状态,并计算`ΔE`,使其量化邻居状态比当前状态好的程度。如果邻居状态比当前状态好(`ΔE>0`),像以前一样,我们将当前状态设置为邻居状态。然而,当邻居状态较差时(`ΔE<0`),我们仍然可能将我们的当前状态设置为该邻居状态,并且我们以$e^{ΔE/t}$的概率这样做。这里的意思是,更小的`ΔE`将导致邻居状态被选择的概率降低,而温度`t`越高,邻居状态被选择的概率越高。这意味着邻居状态越差,被选择的可能性就越小,而算法在其过程中越早,就越有可能将一个较差的邻居状态设置为当前状态。这背后的数学原理如下:`e`是一个常数(大约2.72),`ΔE`是负数(因为这个邻居比当前状态更糟糕)。温度`t`越高,ΔE/`t`就越接近于0,使概率更接近于1。 + +### 旅行商问题(Traveling Salesman Problem) + +- 在旅行商问题中,任务是连接所有的点,同时选择最短的距离。例如,这就是快递公司需要做的事情:找到从商店到所有客户家的最短路线,然后再返回。 + +| 优化前 | 优化后 | +| ------------------------------ | ------------------------------ | +| ![4.3.4-6](static/4.3.4-6.png) | ![4.3.4-7](static/4.3.4-7.png) | + +- 在这种情况下,邻居状态可以被看作是两个箭头互换位置的状态。计算每一个可能的组合使得这个问题在计算上要求很高(10个点给了我们10!或者说3,628,800条可能的路线)。通过使用模拟退火算法,可以以较低的计算成本找到一个好的解决方案。 + +## 线性规划(Linear Programming) + +- 线性规划是一个优化线性方程(y=ax₁+bx₂+...形式的方程)的问题系列。 + +- 线性规划有以下内容: + - 一个我们想要最小化的成本函数:c₁x₁ + c₂x₂ + ... + cₙxₙ。这里,每个x是一个变量,它与一些成本c相关联。 + - 一个约束条件,它表示为一个变量的总和,它要么小于或等于一个值(a₁x₁+a₂x₂+...+aₙxₙ≤b),要么正好等于这个值(a₁x₁+a₂x₂+...+aₙxₙ=b)。在这种情况下,x是一个变量,a是与之相关的一些资源,而b是我们可以为这个问题投入多少资源。 + - 变量的域(例如,一个变量不能是负数),形式为lᵢ≤xᵢ≤uᵢ。 + +- 请考虑以下例子: + - 两台机器,X₁和X₂。X₁的运行成本为50美元/小时,X₂的运行成本为80美元/小时。我们的目标是使成本最小化。这可以被表述为一个成本函数:50x₁+80x₂。 + - X₁每小时需要5个单位的劳动力。X₂每小时需要2个单位的劳动力。总共需要花费20个单位的劳动力。这可以被形式化为一个约束条件:5x₁ + 2x₂ ≤ 20。 + - X₁每小时生产10个单位的产品。X₂每小时生产12个单位的产品。公司需要90个单位的产出。这是另一个约束条件。从字面上看,它可以被改写为10x₁+12x₂≥90。然而,约束条件必须是(a₁x₁+a₂x₂+...+aₙxₙ≤b)或(a₁x₁+a₂x₂+...+aₙxₙ=b)。因此,我们乘以(-1),得到一个所需形式的等价方程:(-10x₁)+(-12x₂)≤-90。 + +- 线性规划的优化算法需要几何学和线性代数的背景知识,而我们并不想假设这些知识。相反,我们可以使用已经存在的算法,如Simplex和Interior-Point。 + +- 下面是一个使用Python中scipy库的线性规划例子: + +```python +import scipy.optimize +# Objective Function: 50x_1 + 80x_2 +# Constraint 1: 5x_1 + 2x_2 <= 20 +# Constraint 2: -10x_1 + -12x_2 <= -90 +result = scipy.optimize.linprog( + [50, 80], # Cost function: 50x_1 + 80x_2 + A_ub=[[5, 2], [-10, -12]], # Coefficients for inequalities + b_ub=[20, -90], # Constraints for inequalities: 20 and -90 +) +if result.success: + print(f"X1: {round(result.x[0], 2)} hours") + print(f"X2: {round(result.x[1], 2)} hours") +else: + print("No solution") +``` + +## 约束满足(Constraint Satisfaction) + +- 约束满足问题是一类需要在满足某些条件下为变量赋值的问题。 + +- 约束条件满足问题具有以下特性: + - 变量集合{x₁,x₂,...,xₙ}。 + - 每个变量域的集合{D₁, D₂, ..., Dₙ}。 + - 一组约束条件C + +- 数独可以表示为一个约束满足问题,每个空方块是一个变量,域是数字1-9,而约束是不能彼此相等的方块。 +![4.3.4-8](static/4.3.4-8.png) + +- 再考虑一个例子。每个学生1-4都在选修A、B、...、G中的三门课程。每门课程都需要有考试,可能的考试日是星期一、星期二和星期三。但是,同一个学生不能在同一天有两次考试。在这种情况下,变量是课程,域是天数,约束条件是哪些课程不能在同一天安排考试,因为是同一个学生在考试。这可以直观地显示如下: + +![4.3.4-9](static/4.3.4-9.png) + +- 这个问题可以用约束条件来解决,约束条件用图来表示。图中的每个节点是一门课程,如果两门课程不能安排在同一天,则在它们之间画一条边。在这种情况下,该图看起来是这样的: + +![4.3.4-10](static/4.3.4-10.png) + +- 关于约束满足问题,还有几个值得了解的术语: + - 硬约束(Hard Constraint)是指在一个正确的解决方案中必须满足的约束。 + - 软约束(Soft Constraint)是一种约束,表示哪种解决方案比其他解决方案更受欢迎。 + - 一元约束(Unary Constraint)是指只涉及一个变量的约束。在我们的例子中,一元约束是指课程A在周一不能有考试{A≠周一}。 + - 二元约束(Binary Constraint)是一种涉及两个变量的约束。这就是我们在上面的例子中使用的约束类型,表示两个课程不能有相同的值{A ≠ B}。 + +### 节点一致性(Node Consistency) + +- 节点一致性是指一个变量域中的所有值都满足该变量的一元约束。 + +- 例如,让我们拿两门课程,A和B。每门课程的域是{Monday, Tuesday, Wednesday},约束条件是{A≠Mon,B≠Tue,B≠Mon,A≠B}。现在,A和B都不是一致的,因为现有的约束条件使它们不能取其域中的每一个值。然而,如果我们从A的域中移除Monday,那么它就会有节点一致性。为了实现B的节点一致性,我们必须从它的域中删除Monday和Tuesday。 + +### 弧一致性(Arc Consistency) + +- 弧一致性是指一个变量域中的所有值都满足该变量的二元约束(注意,我们现在用"弧"来指代我们以前所说的 "边")。换句话说,要使X对Y具有弧一致性,就要从X的域中移除元素,直到X的每个选择都有Y的可能选择。 + +- 考虑到我们之前的例子,修改后的域:A:{Tuesday, Wednesday}和B:{Wednesday}。如果A与B是弧一致的,那么无论A的考试被安排在哪一天(从它的域来看),B仍然能够安排考试。A与B是弧一致的吗?如果A取值为Tuesday,那么B可以取值为Wednesday。然而,如果A取值为Wednesday,那么就没有B可以取的值(记住,其中一个约束是A≠B)。因此,A与B不是弧一致的。为了改变这种情况,我们可以从A的域中删除Wednesday。然后,A的任何取值(Tuesday是唯一的选择)都会给B留下一个取值(Wednesday)。现在,A与B是弧一致的。让我们看看一个伪代码的算法,使一个变量相对于其他变量是弧一致的(注意,csp代表 "约束满足问题")。 + +```python +function Revise(csp, X, Y): + revised = false + for x in X.domain: + if no y in Y.domain satisfies constraint for (X,Y): + delete x from X.domain + revised = true + return revised +``` + +- 这个算法从跟踪X的域是否有任何变化开始,使用变量revised,这在我们研究的下一个算法中会很有用。然后,代码对X的域中的每一个值进行重复,看看Y是否有一个满足约束条件的值。如果是,则什么都不做,如果不是,则从X的域中删除这个值。 + +- 通常情况下,我们感兴趣的是使整个问题的弧一致,而不仅仅是一个变量相对于另一个变量的一致性。在这种情况下,我们将使用一种叫做AC-3的算法,该算法使用Revise: + +```python +function AC-3(csp): + queue = all arcs in csp + while queue non-empty: + (X, Y) = Dequeue(queue) + if Revise(csp, X, Y): + if size of X.domain == 0: + return false + for each Z in X.neighbors - {Y}: + Enqueue(queue, (Z,X)) + return true +``` + +- 该算法将问题中的所有弧添加到一个队列中。每当它考虑一个弧时,它就把它从队列中删除。然后,它运行Revise算法,看这个弧是否一致。如果做了修改使其一致,则需要进一步的行动。如果得到的X的域是空的,这意味着这个约束满足问题是无法解决的(因为没有任何X可以取的值会让Y在约束条件下取任何值)。如果问题在上一步中没有被认为是不可解决的,那么,由于X的域被改变了,我们需要看看与X相关的所有弧是否仍然一致。也就是说,我们把除了Y以外的所有X的邻居,把他们和X之间的弧添加到队列中。然而,如果Revise算法返回false,意味着域没有被改变,我们只需继续考虑其他弧。 + +- 虽然弧一致性的算法可以简化问题,但不一定能解决问题,因为它只考虑了二元约束,而没有考虑多个节点可能的相互连接方式。我们之前的例子中,4个学生中的每个人都在选修3门课程,对其运行AC-3后,仍然没有变化。 + +- 我们讲过[搜索](4.3.1搜索.md)问题。一个约束满足问题可以被看作是一个搜索问题: + - 初始状态(Initial state):空赋值(所有变量都没有分配任何数值)。 + - 动作(Action):在赋值中加入一个{变量=值};也就是说,给某个变量一个值。 + - 过渡模型(Transition model):显示添加赋值如何改变变量。这没有什么深度:过渡模型返回包括最新动作后的赋值的状态。 + - 目标测试(Goal test):检查所有变量是否被赋值,所有约束条件是否得到满足。 + - 路径成本函数(Path cost function):所有路径的成本都是一样的。正如我们前面提到的,与典型的搜索问题相比,优化问题关心的是解决方案,而不是通往解决方案的路线。 + +- 然而,把约束满足问题作为一个普通的搜索问题来处理,是非常低效的。相反,我们可以利用约束满足问题的结构来更有效地解决它。 + +### 回溯搜索(Backtracking Search) + +- 回溯搜索是一种考虑约束满足搜索问题结构的搜索算法。一般来说,它是一个递归函数,只要值满足约束,它就会尝试继续赋值。如果违反了约束,它将尝试不同的赋值。让我们看看它的伪代码: + +```python +function Backtrack(assignment, csp): + if assignment complete: + return assignment + var = Select-Unassigned-Var(assignment, csp) + for value in Domain-Values(var, assignment, csp): + if value consistent with assignment: + add {var = value} to assignment + result = Backtrack(assignment, csp) + if result ≠ failure: + return result + remove {var = value} from assignment + return failure +``` + +- 换句话说,如果当前赋值完成,则该算法返回当前赋值。这意味着,如果完成了算法,它将不会执行任何额外的操作,它只会返回已成立的赋值。如果赋值不完整,算法会选择任何尚未赋值的变量。然后,算法尝试为变量赋值,并对结果赋值再次运行回溯算法(递归)。然后,它检查结果值。如果不是失败,则表示赋值已完成,并且应返回此赋值。如果结果值失败,则删除最近的赋值,并尝试新的可能值,重复相同的过程。如果域中所有可能的值都返回失败,这意味着我们需要回溯。也就是说,问题出在之前的一些作业上。如果这种情况发生在我们开始使用的变量上,那么这意味着没有解决方案满足约束。 + +- 考虑以下行动方案: + +![4.3.4-11](static/4.3.4-11.png) + +- 我们从空赋值开始。然后,我们选择变量A,并给它赋值`Mon`。然后,使用这个赋值,我们再次运行算法。既然A已经有了一个赋值,算法将考虑B,并将`Mon`赋值给它。此赋值返回`false`,因此算法将尝试在`Tue`为B赋值,而不是在给定上一个赋值的情况下为C赋值。这个新的赋值满足约束条件,在给定这个赋值的情况下,下一步将考虑一个新的变量。例如,如果将`Tue`或`Wed`也赋值给B会导致失败,那么算法将回溯并返回到考虑A,为其分配另一个值,即`Tue`。如果`Tue`和`Wed`也失败了,那么这意味着我们已经尝试了所有可能的赋值,该问题是无法解决的。 + +- 在源代码部分,您可以从头开始实现的回溯算法。然而,这种算法被广泛使用,因此,多个库已经包含了它的实现。 + +## 推理(Inference) + +- 尽管回溯搜索比简单搜索更有效,但它仍然需要大量的算力。另一方面,满足弧一致性需要的算力较低。通过将回溯搜索与推理交织在一起(满足弧一致性),我们可以得到一种更有效的算法。该算法被称为“保持弧一致性” __(Maintaining Arc-Consistency)__ 算法。该算法将在每次新的回溯搜索分配之后满足弧一致性。具体来说,在我们对X进行新的赋值后,我们将调用`AC-3`算法,并从所有弧(Y,X)的队列开始,其中Y是X的邻居(而不是问题中所有弧的队列)。以下是一个经过修订的Backtrack算法,该算法保持了弧的一致性。 + +```python +function Backtrack(assignment, csp): + if assignment complete: + return assignment + var = Select-Unassigned-Var(assignment, csp) + for value in Domain-Values(var, assignment, csp): + if value consistent with assignment: + add {var = value} to assignment # new here + inferences = Inference(assignment, csp) # new here + if inferences ≠ failure: + add inferences to assignment + result = Backtrack(assignment, csp) + if result ≠ failure: + return result + remove {var = value} and inferences from assignment # new here + return failure +``` + +- Inference函数运行AC-3算法,如前所述。它的输出是通过满足弧一致性可以做出的所有推断。从字面上看,这些是可以从以前的赋值和约束满足问题的结构中推导出来的新赋值。 + +- 还有其他方法可以提高算法的效率。到目前为止,我们随机选择了一个未分配的变量。然而,有些选择比其他选择更有可能更快地找到解决方案。这需要使用启发式方法。启发式是一条经验法则,这意味着,通常情况下,它会比遵循随机的方法带来更好的结果,但不能保证总是更优。 + +- __最小剩余值(Minimum Remaining Values(MRV))__ 就是这样一种启发式方法。这里的想法是,如果一个变量的域被推理限制了,现在它只剩下一个值(甚至是两个值),那么通过进行这种赋值,我们将减少以后可能需要进行的回溯次数。也就是说,我们迟早要做这个赋值,因为它是从满足弧一致性中推断出来的。如果这项任务失败了,最好尽快发现,避免以后的回溯。 + +![4.3.4-12](static/4.3.4-12.png) + +- 例如,在给定当前赋值的情况下缩小变量的域后,使用MRV启发式,我们接下来将选择变量C,并以Wed为其赋值。 + +- __度(Degree)__ 启发式依赖于变量的度,其中度是指将一个变量连接到其他变量的弧数。通过一次赋值选择度最高的变量,我们约束了多个其他变量,从而加快了算法的进程。 + +![4.3.4-13](static/4.3.4-13.png) + +- 例如,上面所有的变量都有相同大小的域。因此,我们应该选择一个度最高的域,它将是变量E。 + +- 这两种启发式方法并不总是适用的。例如,当多个变量在其域中具有相同的最小值时,或者当多个变数具有相同的最高度时。 + +- 另一种提高算法效率的方法是,当我们从变量的域中选择一个值时,使用另一种启发式方法。在这里,我们使用 __最小约束值(Least Constraining Values)__ 启发式,在这里我们选择将约束最少其他变量的值。这里的想法是,在度启发式中,我们希望使用更可能约束其他变量的变量,而在这里,我们希望这个变量对其他变量的约束最少。也就是说,我们希望找到可能是最大潜在麻烦源的变量(度最高的变量),然后使其尽可能不麻烦(为其赋值约束其他变量最少的值)。 + +![4.3.4-14](static/4.3.4-14.png) + +- 例如,让我们考虑变量C。如果我们将`Tue`分配给它,我们将对所有B、E和F施加约束。然而,如果我们选择`Wed`,我们将只对B和E施加约束。因此,选择`Tue`可能更好。 + +- 总之,优化问题可以用多种方式来表述。在这,我们考虑了局部搜索、线性规划和约束满足。 diff --git a/4.人工智能/4.3人工智能导论及机器学习入门.md b/4.人工智能/4.3人工智能导论及机器学习入门.md index 18a1457..81d80b1 100644 --- a/4.人工智能/4.3人工智能导论及机器学习入门.md +++ b/4.人工智能/4.3人工智能导论及机器学习入门.md @@ -15,3 +15,8 @@ ## 学习建议 本节内容是作者根据[哈佛的 CS50AI 导论](https://cs50.harvard.edu/ai/2020/)以及 [Andrew Ng 的机器学习专项课程](https://www.coursera.org/specializations/machine-learning-introduction)简化编写,当然你可以直接学习这两门课程。本节内容的总学习时间应该是二到三个月,如果你在某个知识点上卡住了,你也许需要反复阅读讲义,必要时向身边人求助。 + +## 补充材料 + +![4.3-0](static/4.3-0.jpg) +人工智能现代方法(第四版) diff --git a/4.人工智能/static/4.3-0.jpg b/4.人工智能/static/4.3-0.jpg new file mode 100644 index 0000000..82d94cf Binary files /dev/null and b/4.人工智能/static/4.3-0.jpg differ diff --git a/4.人工智能/static/4.3.4-0.png b/4.人工智能/static/4.3.4-0.png new file mode 100644 index 0000000..ded04da Binary files /dev/null and b/4.人工智能/static/4.3.4-0.png differ diff --git a/4.人工智能/static/4.3.4-1.png b/4.人工智能/static/4.3.4-1.png new file mode 100644 index 0000000..48d0420 Binary files /dev/null and b/4.人工智能/static/4.3.4-1.png differ diff --git a/4.人工智能/static/4.3.4-10.png b/4.人工智能/static/4.3.4-10.png new file mode 100644 index 0000000..a9fe2af Binary files /dev/null and b/4.人工智能/static/4.3.4-10.png differ diff --git a/4.人工智能/static/4.3.4-11.png b/4.人工智能/static/4.3.4-11.png new file mode 100644 index 0000000..802aa17 Binary files /dev/null and b/4.人工智能/static/4.3.4-11.png differ diff --git a/4.人工智能/static/4.3.4-12.png b/4.人工智能/static/4.3.4-12.png new file mode 100644 index 0000000..858b94d Binary files /dev/null and b/4.人工智能/static/4.3.4-12.png differ diff --git a/4.人工智能/static/4.3.4-13.png b/4.人工智能/static/4.3.4-13.png new file mode 100644 index 0000000..4d533e5 Binary files /dev/null and b/4.人工智能/static/4.3.4-13.png differ diff --git a/4.人工智能/static/4.3.4-14.png b/4.人工智能/static/4.3.4-14.png new file mode 100644 index 0000000..363a58c Binary files /dev/null and b/4.人工智能/static/4.3.4-14.png differ diff --git a/4.人工智能/static/4.3.4-2.png b/4.人工智能/static/4.3.4-2.png new file mode 100644 index 0000000..46f5b4d Binary files /dev/null and b/4.人工智能/static/4.3.4-2.png differ diff --git a/4.人工智能/static/4.3.4-3.png b/4.人工智能/static/4.3.4-3.png new file mode 100644 index 0000000..7529cf8 Binary files /dev/null and b/4.人工智能/static/4.3.4-3.png differ diff --git a/4.人工智能/static/4.3.4-4.png b/4.人工智能/static/4.3.4-4.png new file mode 100644 index 0000000..ed1a50f Binary files /dev/null and b/4.人工智能/static/4.3.4-4.png differ diff --git a/4.人工智能/static/4.3.4-5.png b/4.人工智能/static/4.3.4-5.png new file mode 100644 index 0000000..a06c870 Binary files /dev/null and b/4.人工智能/static/4.3.4-5.png differ diff --git a/4.人工智能/static/4.3.4-6.png b/4.人工智能/static/4.3.4-6.png new file mode 100644 index 0000000..fede004 Binary files /dev/null and b/4.人工智能/static/4.3.4-6.png differ diff --git a/4.人工智能/static/4.3.4-7.png b/4.人工智能/static/4.3.4-7.png new file mode 100644 index 0000000..4c98a35 Binary files /dev/null and b/4.人工智能/static/4.3.4-7.png differ diff --git a/4.人工智能/static/4.3.4-8.png b/4.人工智能/static/4.3.4-8.png new file mode 100644 index 0000000..386a1be Binary files /dev/null and b/4.人工智能/static/4.3.4-8.png differ diff --git a/4.人工智能/static/4.3.4-9.png b/4.人工智能/static/4.3.4-9.png new file mode 100644 index 0000000..6799210 Binary files /dev/null and b/4.人工智能/static/4.3.4-9.png differ diff --git a/4.人工智能/static/4.3.4.2-0.png b/4.人工智能/static/4.3.4.2-0.png new file mode 100644 index 0000000..90690ff Binary files /dev/null and b/4.人工智能/static/4.3.4.2-0.png differ diff --git a/4.人工智能/static/4.3.4.2-1.png b/4.人工智能/static/4.3.4.2-1.png new file mode 100644 index 0000000..0605ab6 Binary files /dev/null and b/4.人工智能/static/4.3.4.2-1.png differ