hav-cs50-merge-03

哈佛 CS50 中文官方笔记(四)

结束

原文:cs50.harvard.edu/x/notes/10/

  • 回顾

  • 展望未来

  • 感谢!

  • 总结

回顾

  • 在过去的十周里,你一直在从传说中的水龙卷中汲取知识。

  • 在这门课程中,你学会了用许多不同的语言编程;实际上,我们最大的希望是你在所有这些语言中 学会了如何编程,无论语言是什么。

  • 回想一下课程开始时你做马里奥游戏的时候:你已经走了很长的路,并且学到了很多技能。

  • 你应该将你的成功衡量为课程开始时的技能和课程结束时的技能之间的差异。

  • 此外,我们希望你在所有其他方面都 学会了如何解决问题,如何接受输入,通过算法处理它,并产生一些输出。为此,我们玩了一个用口头指令画图的游戏。

  • 注意一些口头指令是如何 高级 和其他是如何 低级 的,比如在这个课程中使用的不同编程语言。

  • 注意一些指令比其他指令更具体。记住那些你必须精炼你的代码以做到你想要的事情的时刻。

  • 此外,注意一些指令是如何被抽象化的,比如 画手臂。这类似于我们将代码抽象为函数的方式。

  • 看看整个课程的所有周次,你收集了工具并建立了技能。然而,最重要的是,你学会了如何自学。

  • 你的最终项目是你有机会使用这些技能,按照你的规格和设计来构建一个项目的机会。我们希望你能加入我们的 CS50 黑客马拉松,这是一个史诗般的通宵达旦的活动,我们将一起在我们的最终项目中工作。

展望未来

  • 当你从这门课程的工作走向 CS50 世界之外时,你可能想要采取一些步骤来准备并冒险探索。

  • 继续前进,下载学习 VS Code。

  • 要在终端上执行命令,就像你在 CS50.dev 上做的那样,请在你的 MacPC 上安装命令行工具。

  • 更多关于 Git 的信息。

  • 使用 GitHubNetlify 来托管一个网站。

  • 如果适用,可以使用 AWSAzureGoogle Cloud 来托管一个网络应用程序。

  • 在相关的在线社区中提问。

  • 使用基于 AI 的工具,如 ChatGPTGitHub Copilot 来提问。

  • 修读我们其他的 CS50 课程

  • 加入我们众多的 社区

感谢您!

  • 感谢您参与这门课程!

  • 许多人使这门课程成为可能。感谢您使这门课程成为可能,并为我们提供了如此好的支持!

总结

在本节课中,我们回顾了你在课程中的学习旅程。具体来说,我们鼓励你……

  • 将你在本课程中学到的编程技能应用于解决世界上的问题。

  • 将你在本课程中获得的新的技能运用起来,继续你的学习之旅!

  • 坚持到底,提交你的最终项目。

  • Quack!

这就是 CS50!

人工智能

第零讲

原文:cs50.harvard.edu/ai/notes/0/

人工智能

人工智能(AI)涵盖了一系列技术,这些技术使计算机表现出有感知的行为。例如,AI 用于识别社交媒体上的照片中的面孔,击败世界象棋冠军,以及处理你用手机上的 Siri 或 Alexa 说话时的语音。

在本课程中,我们将探讨使人工智能成为可能的一些想法:

  1. 搜索

找到一个问题的解决方案,比如一个导航应用找到从起点到目的地的最佳路线,或者像玩游戏并找出下一步棋。

  1. 知识

从信息中提取信息并得出推论。

  1. 不确定性

使用概率处理不确定事件。

  1. 优化

找到一个正确解决问题的方法,但也是一个更好或最好的方法。

  1. 学习

根据数据访问和经验改进性能。例如,你的电子邮件能够根据以往的经验区分垃圾邮件和非垃圾邮件。

  1. 神经网络

一种受人类大脑启发的程序结构,能够有效地执行任务。

  1. 语言

处理自然语言,这是人类产生和理解的。

搜索

搜索问题涉及一个给定初始状态和目标状态的代理,并返回从前者到后者的解决方案。导航应用使用典型的搜索过程,其中代理(程序的思考部分)接收你的当前位置和你的目标位置作为输入,并根据搜索算法返回建议的路径。然而,还有许多其他形式的搜索问题,如谜题或迷宫。

15 拼图

解决 15 拼图问题需要使用搜索算法。

  • 代理

    一个感知其环境并对该环境采取行动的实体。例如,在一个导航应用中,代理将是一个需要决定采取哪些行动才能到达目的地的汽车的表示。

  • 状态

    代理在其环境中的配置。例如,在 15 拼图 中,任何一种所有数字在棋盘上排列的方式都是一个状态。

    • 初始状态

      搜索算法开始的状态。在导航应用中,那将是当前位置。

  • 动作

    在一个状态下可以做出的选择。更精确地说,动作可以被定义为函数。当接收到状态 s 作为输入时,Actions(s) 返回在状态 s 中可以执行的动作集合。例如,在 15 拼图 中,给定状态的动作是你可以在当前配置中滑动方块的方式(如果空白方块在中间,有 4 种方式,如果靠近边缘,有 3 种方式,如果位于角落,有 2 种方式)。

  • 转换模型

    对任何状态执行任何适用动作的结果的描述。更精确地说,转换模型可以定义为函数。当接收到状态s和动作a作为输入时,Results(s, a)返回在状态s中执行动作a后的状态。例如,给定某个15 个拼图的配置(状态s),将一个方块向任何方向移动(动作a)将导致拼图的新配置(新状态)。

  • 状态空间

    通过任何动作序列从初始状态可达的所有状态的集合。例如,在 15 个拼图游戏中,状态空间由所有 16!/2 个可以在任何初始状态下到达的棋盘配置组成。状态空间可以可视化为一个有向图,其中状态由节点表示,动作由节点之间的箭头表示。

状态空间

  • 目标测试

    确定给定状态是否为目标状态的条件。例如,在导航应用中,目标测试将是代理的当前位置(汽车的表示)是否在目的地。如果是——问题解决。如果不是——我们继续搜索。

  • 路径成本

    与给定路径相关的数值成本。例如,导航应用不仅将您带到目的地;它这样做的同时最小化路径成本,找到您到达目标状态的最快方式。

解决搜索问题

  • 从初始状态到目标状态的一系列动作。

    • 最优解

      在所有解中具有最低路径成本的解。

在搜索过程中,数据通常存储在节点中,这是一种包含以下数据的数据结构:

  • 一个状态

  • 它的父节点,通过它生成了当前节点

  • 应用到父节点状态以到达当前节点的动作

  • 从初始状态到该节点的路径成本

节点包含使它们在搜索算法目的上非常有用的信息。它们包含一个状态,可以使用目标测试来检查它是否是最终状态。如果是,节点 的路径成本可以与其他节点的路径成本进行比较,从而选择最优解。一旦选择了节点,由于存储了父节点和从父节点到当前节点所采取的动作,就可以从初始状态追踪到这个节点的每一步,而这个动作序列就是

然而,节点只是一个数据结构——它们不搜索,它们只保存信息。为了实际搜索,我们使用边界,这是“管理”节点的机制。边界最初包含一个初始状态和一个空的已探索项集合,然后重复以下操作,直到找到解决方案:

重复:

  1. 如果边界为空,

    • 停止。该问题没有解。
  2. 从前沿移除一个节点。这是将要考虑的节点。

  3. 如果节点包含目标状态,

    • 返回解决方案。停止

    否则,

    * Expand the node (find all the new nodes that could be reached from this node), and add resulting nodes to the frontier.
    * Add the current node to the explored set. 
    

深度优先搜索

在上述对前沿的描述中,有一件事没有被提及。在上述伪代码的第 2 阶段,应该移除哪个节点?这个选择对解决方案的质量和实现速度有影响。有几种方法可以处理哪个节点应该首先考虑的问题,其中两种可以通过(在深度优先搜索中)和队列(在广度优先搜索中)的数据结构来表示;这里有一个可爱的卡通演示说明了这两种方法之间的区别)。

我们从深度优先搜索DFS)方法开始。

深度优先搜索算法在尝试另一个方向之前会先耗尽每一个方向。在这些情况下,前沿被管理为一个数据结构。你需要记住的口号是“后进先出。”在节点被添加到前沿后,首先移除并考虑的是最后添加的节点。这导致了一个搜索算法,它在遇到障碍的第一个方向上尽可能深入,同时将所有其他方向留待以后。

(来自课堂外的例子:假设你在找你的钥匙。在深度优先搜索方法中,如果你选择从你的裤子开始搜索,你将首先检查每一个口袋,清空每一个口袋并仔细检查里面的东西。你只有在完全检查完裤子的每一个口袋后,才会停止在裤子中搜索并开始在其他地方搜索。)

  • 优点:

    • 最好的情况是,这个算法是最快的。如果它“运气好”并且总是选择正确的路径到达解决方案(偶然),那么深度优先搜索将花费最短的时间到达解决方案。
  • 缺点:

    • 可能找到的解决方案并非最优。

    • 最坏的情况是,这个算法在找到解决方案之前将探索所有可能的路径,因此到达解决方案之前将花费可能的最长时间。

代码示例:

 # Define the function that removes a node from the frontier and returns it.
    def remove(self):
    	  # Terminate the search if the frontier is empty, because this means that there is no solution.
        if self.empty():
            raise Exception("empty frontier")
        else:
        	  # Save the last item in the list (which is the newest node added)
            node = self.frontier[-1]
            # Save all the items on the list besides the last node (i.e. removing the last node)
            self.frontier = self.frontier[:-1]
            return node 

广度优先搜索

深度优先搜索的对立面是广度优先搜索BFS)。

一个广度优先搜索算法将同时遵循多个方向,在每个可能的方向上先迈出一小步,然后再在每个方向上迈出第二步。在这种情况下,前沿被管理为一个队列数据结构。你需要记住的口号是“先进先出。”在这种情况下,所有新的节点都按顺序添加,节点是根据哪个先添加的来考虑的(先来先服务!)。这导致了一个搜索算法,它在每个可能的方向上迈出一小步,然后再在任何方向上迈出第二步。

(来自课堂外的例子:假设你处于寻找钥匙的情况。在这种情况下,如果你从裤子开始,你会检查右边的口袋。之后,你不会检查左边的口袋,而是会查看一个抽屉。然后是桌子。等等,在每个你能想到的地方。只有在你用尽所有地方之后,你才会回到裤子并检查下一个口袋。)

  • 优点:

    • 这个算法保证能找到最优解。
  • 缺点:

    • 这个算法几乎可以保证运行时间会比最短运行时间更长。

    • 最坏的情况下,这个算法的运行时间是最长的。

代码示例:

 # Define the function that removes a node from the frontier and returns it.
    def remove(self):
    	  # Terminate the search if the frontier is empty, because this means that there is no solution.
        if self.empty():
            raise Exception("empty frontier")
        else:
            # Save the oldest item on the list (which was the first one to be added)
            node = self.frontier[0]
            # Save all the items on the list besides the first one (i.e. removing the first node)
            self.frontier = self.frontier[1:]
            return node 

贪婪最佳优先搜索

广度优先和深度优先都是无信息搜索算法。也就是说,这些算法没有利用它们通过自己的探索获得的问题知识。然而,大多数情况下,确实存在一些关于问题的知识。例如,当人类迷宫解决者在进入一个交汇点时,人类可以看到哪个方向通向解决方案的一般方向,哪个方向则不行。人工智能也可以做到这一点。一种考虑额外知识以尝试提高其性能的算法称为有信息搜索算法。

贪婪最佳优先搜索扩展的是离目标最近的节点,这是通过启发式函数 h(n) 确定的。正如其名所示,该函数估计下一个节点离目标有多近,但它可能会出错。贪婪最佳优先算法的效率取决于启发式函数的好坏。例如,在一个迷宫中,算法可以使用一个依赖于可能节点和迷宫终点之间曼哈顿距离的启发式函数。曼哈顿距离忽略了墙壁,并计算从当前位置到目标位置需要向上、向下或向侧面走多少步。这是一个基于当前位置和目标位置的 (x, y) 坐标可以推导出的简单估计。

曼哈顿距离

曼哈顿距离

然而,重要的是要强调,就像任何启发式方法一样,它可能会出错,并导致算法走上一条比其他情况下更慢的路径。有可能一个无信息搜索算法会更快地提供一个更好的解决方案,但这种情况发生的可能性比有信息算法要小。

A* 搜索

贪婪最佳优先搜索算法的发展,A搜索不仅考虑 h(n),从当前位置到目标的估计成本,还考虑 g(n),到达当前位置的成本。通过结合这两个值,算法有更准确的方式来确定解决方案的成本并优化其选择。算法跟踪(到目前为止的路径成本* + 到目标的估计成本),一旦它超过某些先前选项的估计成本,算法将放弃当前路径并回到先前选项,从而防止自己沿着一个长而低效的路径走下去,而 h(n) 错误地将它标记为最佳。

再次强调,由于这个算法也依赖于启发式方法,因此它的效果取决于所采用的启发式方法。在某些情况下,它可能不如贪婪最佳优先搜索甚至无信息算法高效。为了使A搜索*成为最优的,启发式函数 h(n) 应该是:

  1. 可接受,或者从不高估真实成本,

  2. 一致,这意味着新节点到目标路径成本的估计,加上从先前节点转换到它的成本,大于或等于先前节点到目标路径成本的估计。用方程式表示,如果对于每个节点 n 和具有步长成本 c 的后续节点 n’h(n)h(n’) + c,则 h(n) 是一致的。

对抗搜索

而之前,我们讨论了需要找到问题答案的算法,在对抗搜索中,算法面对一个试图实现相反目标的对手。通常,使用对抗搜索的 AI 在游戏中遇到,例如井字棋。

Minimax

对抗搜索算法中的一种类型,Minimax 将胜利条件表示为一方的(-1)和另一方的(+1)。后续动作将由这些条件驱动,最小化方试图获得最低分数,而最大化方试图获得最高分数。

表示井字棋 AI

  • S₀:初始状态(在我们的例子中,一个空的 3X3 棋盘)

  • 玩家(s):一个函数,给定一个状态 s,返回当前轮到哪个玩家(X 或 O)。

  • 动作(s):一个函数,给定一个状态 s,返回在这个状态下所有合法的移动(棋盘上哪些位置是空的)。

  • 结果(s, a):一个函数,给定一个状态 s 和动作 a,返回一个新的状态。这是在状态 s 上执行动作 a 后得到的棋盘(在游戏中进行一步移动)。

  • 终端(s):一个函数,给定一个状态 s,检查这是否是游戏的最后一步,即是否有人获胜或有平局。如果游戏结束,则返回 True,否则返回 False

  • 效用(s):一个函数,给定一个终端状态 s,返回该状态的有效值:-1,0 或 1。

算法的工作原理

递归地,算法模拟从当前状态开始的所有可能的游戏,直到达到终端状态。每个终端状态的价值被评估为 (-1)、0 或 (+1)。

Minimax in Tic Tac Toe

Minimax 算法在井字棋中的应用

根据当前轮到哪个状态,算法可以知道当前玩家在最优策略下,会选择导致状态值更低或更高的动作。这样,通过交替进行最小化和最大化,算法为每个可能动作的结果状态创建值。为了更具体地说明,我们可以想象最大化玩家在每一轮都会问:“如果我采取这个动作,将产生一个新的状态。如果最小化玩家采取最优策略,该玩家可以采取什么动作将值降到最低?”然而,为了回答这个问题,最大化玩家必须问:“为了知道最小化玩家会做什么,我需要在最小化玩家的思维中模拟相同的过程:最小化玩家会试图问:‘如果我采取这个动作,最大化玩家可以采取什么动作将值提高到最高?’”这是一个递归过程,可能很难理解;查看下面的伪代码可能会有所帮助。最终,通过这个递归推理过程,最大化玩家为当前状态下所有可能的动作结果状态生成值。在得到这些值之后,最大化玩家选择其中最高的一个。

Minimax Algorithm

最大化玩家考虑未来状态的潜在值。

用伪代码来说,Minimax 算法的工作方式如下:

  • 给定一个状态 s

    • 最大化玩家在 Actions(s) 中选择动作 a,该动作产生 Min-Value(Result(s, a)) 的最高值。

    • 最小化玩家在 Actions(s) 中选择动作 a,该动作产生 Max-Value(Result(s, a)) 的最低值。

  • 函数 Max-Value(state)

    • v = -∞

    • if Terminal(state):

      返回 Utility(state)

    • for action in Actions(state):

      v = Max(v, Min-Value(Result(state, action)))

      返回 v

  • 函数 Min-Value(state):

    • v = ∞

    • if Terminal(state):

      返回 Utility(state)

    • for action in Actions(state):

      v = Min(v, Max-Value(Result(state, action)))

      return v

Alpha-Beta Pruning

一种优化 Minimax 的方法是 Alpha-Beta 剪枝,它跳过了一些明显不利的递归计算。在确定一个动作的价值后,如果最初有证据表明接下来的动作可以使对手得到比已确定的动作更好的分数,就没有必要进一步调查这个动作,因为它将明显不如之前确定的动作有利。

这一点最容易被一个例子所说明:一个最大化玩家知道,在下一步,最小化玩家将试图获得最低的分数。假设最大化玩家有三个可能的行为,第一个行为的价值是 4。然后玩家开始为下一步生成价值。为了做到这一点,玩家在当前玩家采取这个行为的情况下,生成最小化玩家行为的值,知道最小化玩家将选择最低的一个。然而,在完成所有可能的最小化玩家行为的计算之前,玩家看到其中一个选项的价值是三。这意味着没有必要继续探索其他可能的最小化玩家行为。尚未评估的行为的价值并不重要,无论是 10 还是(-10)。如果价值是 10,最小化玩家将选择最低的选项,3,这已经比预先设定的 4 更差。如果尚未评估的行为最终是(-10),最小化玩家将选择这个选项,(-10),这对最大化玩家来说更加不利。因此,在这一点上计算最小化玩家的额外可能行为对最大化玩家来说是不相关的,因为最大化玩家已经有一个明确更好的选择,其价值是 4。

Alpha Beta 剪枝

深度限制最小-最大

总共有 255,168 种可能的井字棋游戏,以及 10²⁹⁰⁰⁰种可能的国际象棋游戏。到目前为止所展示的最小-最大算法需要从某个点生成所有假设游戏到终端状态。虽然计算所有井字棋游戏对现代计算机来说并不构成挑战,但用国际象棋来做这一点目前是不可能的。

深度限制最小-最大只考虑在停止之前预先定义的移动数,而不会达到终端状态。然而,这并不允许为每个行为获得精确的价值,因为假设游戏并没有达到终端。为了解决这个问题,深度限制最小-最大依赖于一个评估函数,该函数估计从给定状态的游戏预期效用,换句话说,为状态分配值。例如,在国际象棋游戏中,效用函数将当前棋盘配置作为输入,尝试评估其预期效用(基于每个玩家拥有的棋子和它们在棋盘上的位置),然后返回一个正或负值,表示棋盘对一方玩家相对于另一方的有利程度。这些值可以用来决定正确的行动,评估函数越好,依赖它的最小-最大算法就越好。

第一讲

原文:cs50.harvard.edu/ai/notes/1/

知识

人类基于现有知识进行推理并得出结论。从知识中表示和推理出结论的概念也用于人工智能,在本讲座中,我们将探讨我们如何实现这种行为。

基于知识的智能体

这些是通过操作知识内部表示来进行推理的智能体。

“基于知识进行推理得出结论”是什么意思?

让我们从哈利·波特的例子开始回答这个问题。考虑以下句子:

  1. 如果今天没有下雨,哈利今天拜访了海格。

  2. 哈利今天拜访了海格或邓布利多,但不是两者都拜访了。

  3. 哈利今天拜访了邓布利多。

基于这三句话,我们可以回答“今天是否下雨?”这个问题,尽管没有任何一个单独的句子告诉我们今天是否下雨。我们可以这样进行推理:查看第三句话,我们知道哈利拜访了邓布利多。查看第二句话,我们知道哈利拜访了邓布利多或海格,因此我们可以得出结论

  1. 哈利没有拜访海格。

现在,查看第一句话,我们理解如果没有下雨,哈利会拜访海格。然而,知道第四句话,我们知道情况并非如此。因此,我们可以得出结论

  1. 今天下雨了。

为了得出这个结论,我们使用了逻辑,今天的讲座探讨了人工智能如何使用逻辑根据现有信息得出新的结论。

句子

句子是在知识表示语言中对世界的断言。句子是人工智能存储知识并使用它来推断新信息的方式。

质理逻辑

质量逻辑基于命题,即关于世界的陈述,可以是真或假,如上面第 1-5 句所示。

命题符号

质量符号通常是用字母(P, Q, R)表示的命题。

逻辑连接词

逻辑连接词是连接命题符号的逻辑符号,以便以更复杂的方式对世界进行推理。

  • 非 (¬) 反转命题的真值。例如,如果 P: “正在下雨”,那么 ¬P: “没有下雨”。

    真值表用于比较命题的所有可能的真值分配。这个工具将帮助我们更好地理解命题与不同的逻辑连接词连接时的真值。例如,下面是我们的第一个真值表:

    P ¬P
    false true
    true false
  • 与 (∧) 连接两个不同的命题。当这两个命题 P 和 Q 通过 ∧ 连接时,结果命题 P ∧ Q 仅在 P 和 Q 都为真时才为真。

    P Q P ∧ Q
    false false false
    false true false
    true false false
    true true true
  • 或(∨)只要其论点中的任何一个为真就为真。这意味着,为了 P ∨ Q 为真,P 或 Q 中的至少一个必须为真。

    P Q P ∨ Q
    false false false
    false true true
    true false true
    true true true

    值得注意的是,有两种类型的“或”:包含“或”和排除“或”。在排除“或”中,如果 P ∧ Q 为真,则 P ∨ Q 为假。也就是说,排除“或”只需要其论点中的一个为真,而不是两个都为真。包含“或”在 P、Q 或 P ∧ Q 中的任何一个为真时为真。在“或”(∨)的情况下,意图是包含“或”。

一些在讲座中没有提到的旁注

  • 有时举一个例子有助于理解包含“或”与排除“或”。包含“或”:为了吃甜点,你必须打扫房间或修剪草坪。在这种情况下,如果你做了这两项家务,你仍然会得到饼干。排除“或”:为了甜点,你可以选择饼干或冰淇淋。在这种情况下,你不能两者都要。
  • 如果你好奇,排除“或”通常简称为 XOR,其常见符号是⊕)。
  • 蕴含(→)代表“如果 P 那么 Q”的结构。例如,如果 P:“下雨”和 Q:“我待在室内”,那么 P → Q 意味着“如果下雨,那么我待在室内”。在 P 蕴含 Q(P → Q)的情况下,P 被称为前件,Q 被称为后件

    前件为真时,如果后件也为真,整个蕴含式就为真(这很有道理:如果下雨而我待在室内,那么句子“如果下雨,那么我待在室内”就是真的)。当前件为真时,如果后件为假,蕴含式就是假的(如果我下雨时在室外,那么句子“如果下雨,那么我待在室内”就是假的)。然而,当前件为假时,无论后件如何,蕴含式总是真的。这有时可能是一个令人困惑的概念。从逻辑上讲,如果前件(P)为假,我们就无法从蕴含式(P → Q)中得出任何东西。看看我们的例子,如果不下雨,蕴含式并没有说明我是否在室内。我可能是一个室内型的人,即使不下雨也从不外出,或者我可能是一个室外型的人,不下雨时总是外出。当前件为假时,我们说蕴含式是显然真的。

    P Q P → Q
    false false true
    false true true
    true false false
    true true true
  • 双条件 (↔) 是一个双向的蕴涵。你可以将其读作“如果且仅如果”。P ↔ Q 与 P → Q 和 Q → P 同时成立。例如,如果 P: “正在下雨。” 和 Q: “我在室内,”那么 P ↔ Q 意味着“如果下雨,那么我在室内,”以及“如果我在室内,那么下雨。”这意味着我们可以比简单蕴涵推断出更多内容。如果 P 是假的,那么 Q 也是假的;如果不下雨,我们知道我也不在室内。

    P Q P ↔ Q

模型

模型是对每个命题的真值分配。再次强调,命题是关于世界的陈述,可以是真或假。然而,关于世界的知识是通过这些命题的真值来表示的。模型是提供关于世界信息的真值分配。

例如,如果 P: “正在下雨。” 和 Q: “今天是星期二。”,一个模型可以是以下真值分配:{P = 真, Q = 假}。这个模型意味着在下雨,但不是星期二。然而,在这种情况下还有更多可能的模型(例如,{P = 真, Q = 真},即下雨且是星期二)。实际上,可能模型的数量是命题数量的 2 的幂。在这种情况下,我们有两个命题,所以 2²=4 个可能的模型。

知识库 (KB)

知识库是一组知识库代理所知道句子。这是 AI 以命题逻辑句子的形式提供关于世界的知识,可以用来对世界进行额外的推断。

蕴涵 (⊨)

如果 α ⊨ β (α 蕴涵 β),那么在任何 α 为真的世界中,β 也是真的。

例如,如果 α: “一月份是星期二” 和 β: “是月份,”那么我们知道 α ⊨ β。如果一月份是星期二是真的,我们也知道是月份。蕴涵与蕴涵不同。蕴涵是两个命题之间的逻辑连接词。另一方面,蕴涵是一个关系,意味着如果 α 中的所有信息都是真的,那么 β 中的所有信息也是真的。

推理

推理是从旧句子推导出新句子的过程。

例如,在之前的哈利·波特例子中,句子 4 和 5 是从句子 1、2 和 3 推导出来的。

基于现有知识推断新知识有多种方式。首先,我们将考虑 模型检查 算法。

  • 要确定 KB ⊨ α(换句话说,回答“基于我们的知识库,我们能否得出 α 是真的”)

    • 列举所有可能的模型。

    • 如果在 KB 为真的每个模型中 α 也是真的,那么 KB 蕴涵 α (KB ⊨ α)。

考虑以下例子:

P: 今天是星期二。Q: 正在下雨。R: 哈里会去跑步。KB: (P ∧ ¬Q) → R (换句话说,P 和非 Q 蕴含 R)P (P 是真的) ¬Q (Q 是假的) 查询:R (我们想知道 R 是真还是假;KB ⊨ R?)

要使用模型检查算法回答查询,我们需要枚举所有可能的模型。

P Q R KB
false false false
false false true
false true false
false true true
true false false
true false true
true true false
true true true

然后,我们逐一检查每个模型,看它是否在给定的知识库中为真。

首先,在我们的知识库(KB)中,我们知道 P 是真的。因此,我们可以断言,在 P 不为真的所有模型中,KB 是假的。

P Q R KB
false false false false
false false true false
false true false false
false true true false
true false false
true false true
true true false
true true true

接下来,同样地,在我们的 KB 中,我们知道 Q 是假的。因此,我们可以断言,在 Q 为真的所有模型中,KB 是假的。

P Q R KB
false false false false
false false true false
false true false false
false true true false
true false false
true false true
true true false false
true true true false

最后,我们只剩下两个模型。在这两个模型中,P 是真的,Q 是假的。在一个模型中 R 是真的,在另一个模型中 R 是假的。由于 (P ∧ ¬Q) → R 在我们的 KB 中,我们知道在 P 为真且 Q 为假的情况下,R 必须是真的。因此,我们说对于 R 为假的模型,我们的 KB 是假的,对于 R 为真的模型,我们的 KB 是真的。

P Q R KB
false false false false
false false true false
false true false false
false true true false
true false false false
true false true true
true true false false
true true true false

观察这个表格,只有一个模型中我们的知识库是真的。在这个模型中,我们看到 R 也是真的。根据蕴涵的定义,如果 R 在 KB 为真的所有模型中都是真的,那么 KB ⊨ R。

接下来,让我们看看知识和逻辑如何被表示为代码。

from logic import *

# Create new classes, each having a name, or a symbol, representing each proposition. rain = Symbol("rain")  # It is raining. hagrid = Symbol("hagrid")  # Harry visited Hagrid dumbledore = Symbol("dumbledore")  # Harry visited Dumbledore 
# Save sentences into the KB knowledge = And(  # Starting from the "And" logical connective, becasue each proposition represents knowledge that we know to be true. 
    Implication(Not(rain), hagrid),  # ¬(It is raining) → (Harry visited Hagrid) 
    Or(hagrid, dumbledore),  # (Harry visited Hagrid) ∨ (Harry visited Dumbledore). 
    Not(And(hagrid, dumbledore)),  # ¬(Harry visited Hagrid ∧ Harry visited Dumbledore) i.e. Harry did not visit both Hagrid and Dumbledore. 
    dumbledore  # Harry visited Dumbledore. Note that while previous propositions contained multiple symbols with connectors, this is a proposition consisting of one symbol. This means that we take as a fact that, in this KB, Harry visited Dumbledore.
    ) 

运行模型检查算法需要以下信息:

  • 知识库,将用于得出推论

  • 查询,或我们感兴趣的是否被 KB 蕴含的命题

  • 符号,所有使用的符号(或原子命题)的列表(在我们的例子中,这些是 rainhagriddumbledore

  • 模型,对符号的真假值分配

模型检查算法如下所示:

def check_all(knowledge, query, symbols, model):

    # If model has an assignment for each symbol
    # (The logic below might be a little confusing: we start with a list of symbols. The function is recursive, and every time it calls itself it pops one symbol from the symbols list and generates models from it. Thus, when the symbols list is empty, we know that we finished generating models with every possible truth assignment of symbols.)
    if not symbols:

        # If knowledge base is true in model, then query must also be true
        if knowledge.evaluate(model):
            return query.evaluate(model)
        return True
    else:

        # Choose one of the remaining unused symbols
        remaining = symbols.copy()
        p = remaining.pop()

        # Create a model where the symbol is true
        model_true = model.copy()
        model_true[p] = True

        # Create a model where the symbol is false
        model_false = model.copy()
        model_false[p] = False

        # Ensure entailment holds in both models
        return(check_all(knowledge, query, remaining, model_true) and check_all(knowledge, query, remaining, model_false)) 

注意,我们只对 KB 为真的模型感兴趣。如果 KB 为假,那么我们知道为真的条件在这些模型中不会发生,使它们对我们案例无关紧要。

来自课堂外的一个例子:设 P:哈利玩寻找者,Q:奥利弗玩守门员,R:格兰芬多获胜。我们的知识库(KB)指定 P Q (P ∧ Q) → R。换句话说,我们知道 P 为真,即哈利玩寻找者,Q 为真,即奥利弗玩守门员,并且如果 P 和 Q 都为真,那么 R 也为真,这意味着格兰芬多赢得了比赛。现在想象一个模型,哈利扮演的是打击手而不是寻找者(因此,哈利没有玩寻找者,¬P)。在这种情况下,我们不在乎格兰芬多是否获胜(R 是否为真),因为我们知道哈利扮演的是寻找者而不是打击手。我们只对 P 和 Q 都为真的模型感兴趣。)

此外,check_all函数的工作方式是递归的。也就是说,它选择一个符号,创建两个模型,其中一个模型中该符号为真,另一个模型中该符号为假,然后再次调用自身,现在有两个模型,它们的区别在于该符号的真值分配。函数将持续这样做,直到所有符号在模型中都被分配了真值,使symbols列表为空。一旦它为空(如if not symbols行所示),在函数的每个实例中(其中每个实例持有不同的模型),函数将检查给定模型的知识库(KB)是否为真。如果在这个模型中 KB 为真,则函数将检查查询是否为真,如前所述。

知识工程

知识工程是确定如何在人工智能中表示命题和逻辑的过程。

让我们通过游戏《神秘线索》来练习知识工程。

在游戏中,谋杀是由一个地点使用工具犯下的。人物、工具和地点由卡片表示。随机抽取每个类别的卡片放入信封中,参与者需要揭开信封,找出凶手。参与者通过揭开卡片并从这些线索中推断出信封中必须有什么来做到这一点。我们将使用之前的模型检查算法来揭开这个谜团。在我们的模型中,我们将与谋杀相关的事项标记为True,否则标记为False

为了我们的目的,假设我们有三个人:芥末、梅子和猩红,三个工具:刀、手枪和扳手,以及三个地点:舞厅、厨房和图书馆。

我们可以通过添加游戏的规则来开始创建我们的知识库。我们确定一个人是凶手,一个工具被使用,谋杀发生在某个地点。这可以用命题逻辑以下方式表示:

(芥末 ∨ 梅子 ∨ 猩红)

(刀 ∨ 手枪 ∨ 扳手)

(舞厅 ∨ 厨房 ∨ 图书馆)

游戏开始时,每个玩家看到一个人、一个工具和一个地点,因此知道他们与谋杀无关。玩家不会分享他们在这些卡片上看到的信息。假设我们的玩家得到了 Mustard、厨房和手枪的卡片。因此,我们知道这些与谋杀无关,我们可以添加到我们的 KB 中

¬(Mustard)

¬(kitchen)

¬(revolver)

在游戏的其他情况下,一个人可以做出猜测,提出一个人、工具和地点的组合。假设猜测是 Scarlet 在图书馆使用扳手犯罪。如果这个猜测是错误的,那么以下可以推导出来并添加到 KB 中:

(¬Scarlet ∨ ¬library ∨ ¬wrench)

现在,假设有人向我们展示了 Plum 的牌。因此,我们可以添加

¬(Plum)

到我们的 KB 中。

在这一点上,我们可以得出结论,凶手是 Scarlet,因为凶手只能是 Mustard、Plum 和 Scarlet 中的一个,而我们已经有证据表明前两个人不是凶手。

添加一点额外的知识,例如,比如它不是舞厅,就能给我们更多信息。首先,我们更新我们的知识库(KB)

¬(ballroom)

现在,使用多个先前数据,我们可以推导出 Scarlet 在图书馆用刀犯罪。我们可以推导出是图书馆,因为地点只能是舞厅、厨房或图书馆,前两个已经被证明不是地点。然而,当有人猜测 Scarlet、图书馆、扳手时,这个猜测是错误的。因此,这个陈述中的至少一个元素必须是错误的。因为我们知道 Scarlet 和图书馆是正确的,所以我们知道扳手是错误的。因为三个工具中必须有一个是正确的,而且不是扳手也不是手枪,我们可以得出结论,是刀。

这里是如何将信息添加到 Python 中的知识库中的:

# Add the clues to the KB knowledge = And(

    # Start with the game conditions: one item in each of the three categories has to be true.
    Or(mustard, plum, scarlet),
    Or(ballroom, kitchen, library),
    Or(knife, revolver, wrench),

    # Add the information from the three initial cards we saw
    Not(mustard),
    Not(kitchen),
    Not(revolver),

    # Add the guess someone made that it is Scarlet, who used a wrench in the library
    Or(Not(scarlet), Not(library), Not(wrench)),

    # Add the cards that we were exposed to
    Not(plum),
    Not(ballroom)
) 

我们还可以看看其他的逻辑谜题。考虑以下例子:有四个不同的人,Gilderoy、Pomona、Minerva 和 Horace,被分配到四个不同的学院,Gryffindor、Hufflepuff、Ravenclaw 和 Slytherin。每个学院恰好有一个人。用命题逻辑表示这个谜题的条件相当繁琐。首先,每个可能的分配都将本身成为一个命题:MinervaGryffindor、MinervaHufflepuff、MinervaRavenclaw、MinervaSlytherin、PomonaGryffindor……其次,为了表示每个人属于一个学院,需要一个表示所有可能学院分配的 Or 语句

(MinervaGryffindor ∨ MinervaHufflepuff ∨ MinervaRavenclaw ∨ MinervaSlytherin),对每个人重复。

然后,为了编码如果一个人被分配到一个学院,他们就不会被分配到其他学院,我们将写

(MinervaGryffindor → ¬MinervaHufflepuff) ∧ (MinervaGryffindor → ¬MinervaRavenclaw) ∧ (MinervaGryffindor → ¬MinervaSlytherin) ∧ (MinervaHufflepuff → ¬MinervaGryffindor)……

以此类推,对于所有房屋和所有人。在一阶逻辑部分中提供了解决这种低效的方法。然而,只要有足够的线索,这种类型的谜题仍然可以用任何一种逻辑来解决。

另一种可以使用命题逻辑解决的谜题类型是 Mastermind 游戏。在这个游戏中,第一玩家以某种顺序排列颜色,然后第二玩家必须猜测这个顺序。在每一轮中,第二玩家做出一个猜测,第一玩家给出一个数字,表示第二玩家猜对了多少颜色。让我们模拟一个有四种颜色的游戏。假设第二玩家建议以下顺序:

Mastermind1

第一玩家回答“两个”。因此我们知道有两个颜色在正确的位置,另外两个在错误的位置。基于这个信息,第二玩家尝试交换两个颜色的位置。

Mastermind2

现在,第一玩家回答“零”。因此,第二玩家知道最初交换的颜色在正确的位置,这意味着未被触及的两个颜色在错误的位置。第二玩家将它们交换。

Mastermind3

第一玩家说“四个”,游戏结束。

在命题逻辑中表达这一点需要我们拥有(颜色数量)²个原子命题。所以,在四种颜色的情况下,我们会拥有代表颜色和位置的命题,如 red0、red1、red2、red3、blue0 等。下一步是将游戏的规则在命题逻辑中表示(每个位置只有一个颜色且颜色不重复),并将它们添加到知识库中。最后一步是将我们拥有的所有线索添加到知识库中。在我们的情况下,我们会添加在第一次猜测中,有两个位置是错误的,有两个位置是正确的,在第二次猜测中,没有一个是正确的。使用这些知识,模型检查算法可以给我们提供谜题的解决方案。

推理规则

模型检查不是一个高效的算法,因为它必须在给出答案之前考虑所有可能模型(提醒:如果查询 R 在所有模型(真值赋值)中都是真的,那么 R 是真的)。推理规则允许我们基于现有知识生成新信息,而无需考虑所有可能的模型。

推理规则通常使用一条水平线来表示,该线将上半部分(前提)与下半部分(结论)分开。前提是我们拥有的任何知识,而结论是基于前提可以生成的知识。

Modus Ponens Example

在这个例子中,我们的前提由以下命题组成:

  • 如果下雨,那么哈利就在室内。

  • 正在下雨。

基于此,大多数合理的人类可以得出结论:

  • 哈利在室内。

肯定前件

在这个例子中,我们使用的推理规则是肯定前件式,这是一种比较复杂的方式来表达,即如果我们知道一个蕴涵及其前件是真的,那么后件也是真的。

肯定前件式

合取消除

如果一个合取命题是真的,那么其中任何一个原子命题也是真的。例如,如果我们知道哈利和罗恩以及赫敏是朋友,我们可以得出哈利和赫敏是朋友的结论。

合取消除

双重否定消除

被否定两次的命题是真的。例如,考虑命题“哈利没有通过考试的说法并不正确”。我们可以这样解析它:“哈利没有通过考试的说法并不正确”,或者“¬(哈利没有通过考试)”,最后“¬(¬(哈利通过了考试))”。两次否定相互抵消,将命题“哈利通过了考试”标记为真。

双重否定消除

蕴涵消除

蕴涵等价于否定前件和后件之间的或关系。例如,命题“如果下雨,哈利在室内”等价于命题“(不下雨)或(哈利在室内)。”

蕴涵消除

这一点可能会有些令人困惑。然而,考虑以下真值表:

P Q P → Q ¬P ∨ Q

由于 P → Q 和 ¬P ∨ Q 具有相同的真值赋值,我们知道它们在逻辑上是等价的。另一种思考方式是,如果一个蕴涵在两种可能条件下成立:首先,如果前件是假的,蕴涵就显然是真的(如前所述,在蕴涵部分讨论过)。这由 ¬P ∨ Q 中的否定前件 P 表示,意味着如果 P 是假的,那么命题总是真的。其次,当且仅当后件也是真的时,蕴涵才是真的。也就是说,如果 P 和 Q 都是真的,那么 ¬P ∨ Q 是真的。然而,如果 P 是真的而 Q 不是,那么 ¬P ∨ Q 就是假的。

双条件消除

一个双条件命题等价于一个蕴涵及其逆命题,并且使用合取连接符。例如,“如果下雨,那么哈利在室内”等价于“如果下雨,哈利在室内”和“如果哈利在室内,那么下雨”。

双条件消除

德摩根定律

将“与”连接词转换为“或”连接词是可能的。考虑以下命题:“哈利和罗恩都没有通过考试。”从这个命题中,我们可以得出结论:“哈利没有通过考试”或“罗恩没有通过考试。”也就是说,要使前面的“与”命题为真,至少有一个“或”命题中的命题必须为真。

德摩根 1

同样,也可以得出相反的结论。考虑命题“哈利或罗恩没有通过考试。”这可以重新表述为“哈利没有通过考试”和“罗恩没有通过考试。”

德摩根 2

分配律

一个由“与”或“或”连接词组合的两个元素的命题可以被分配,或分解成由“与”和“或”组成的小单元。

分配律 1

分配律 2

知识和搜索问题

推理可以被视为一个具有以下特性的搜索问题:

  • 初始状态:起始知识库

  • 行动:推理规则

  • 转换模型:推理后的新知识库

  • 目标测试:检查我们试图证明的陈述是否在知识库(KB)中

  • 路径成本函数:证明中的步骤数

这展示了搜索算法是多么的灵活,它允许我们使用推理规则根据现有知识推导出新的信息。

归结

归结是一个强大的推理规则,它指出在一个“或”命题中的两个原子命题中,如果一个是假的,那么另一个必须是真的。例如,给定命题“罗恩在大厅”或“赫敏在图书馆”,除了命题“罗恩不在大厅”,我们还可以得出结论“赫敏在图书馆。”更正式地,我们可以这样定义归结:

归结

归结依赖于互补文字,即两个相同的原子命题,其中一个被否定,另一个没有被否定,例如 P 和 ¬P。

归结可以进一步推广。假设除了命题“罗恩在大厅”或“赫敏在图书馆”,我们还知道“罗恩不在大厅”或“哈利在睡觉。”我们可以通过归结从这个命题中推断出“赫敏在图书馆”或“哈利在睡觉。”用正式的话来说:

归结

互补文字允许我们通过归结推理生成新的句子,从而生成新的知识。因此,推理算法定位互补文字以生成新的知识。

子句 是文字的析取(一个命题符号或命题符号的否定,如 P,¬P)。析取 由命题通过或逻辑连接词连接(P ∨ Q ∨ R)。另一方面,合取 由命题通过与逻辑连接词连接(P ∧ Q ∧ R)。子句允许我们将任何逻辑语句转换为 合取范式 (CNF),例如:(A ∨ B ∨ C) ∧ (D ∨ ¬E) ∧ (F ∨ G)。

将命题转换为合取范式步骤

  • 消去双条件

    • 将 (α ↔ β) 转换为 (α → β) ∧ (β → α)。
  • 消去蕴涵

    • 将 (α → β) 转换为 ¬α ∨ β。
  • 使用德摩根定律将否定内移,直到只有文字被否定(而不是子句)。

    • 将 ¬(α ∧ β) 转换为 ¬α ∨ ¬β

这里有一个将 (P ∨ Q) → R 转换为合取范式的例子:

  • (P ∨ Q) → R

  • ¬(P ∨ Q) ∨ R / 消去蕴涵

  • (¬P ∧ ¬Q) ∨ R / 德摩根定律

  • (¬P ∨ R) ∧ (¬Q ∨ R) / 分配律

在这一点上,我们可以在合取范式中运行推理算法。偶尔,通过解析推理的过程,我们可能会遇到一个子句包含相同的文字两次的情况。在这些情况下,使用称为 因式分解 的过程,其中移除重复的文字。例如,(P ∨ Q ∨ S) ∧ (¬P ∨ R ∨ S) 允许我们通过解析推理得出 (Q ∨ S ∨ R ∨ S)。重复的 S 可以被移除,得到 (Q ∨ R ∨ S)。

解析一个文字及其否定,即 ¬P 和 P,会得到 空子句 (()). 空子句总是假的,这很有道理,因为 P 和 ¬P 同时为真是不可能的。这个事实被解析算法所使用。

  • 要确定 KB ⊨ α:

    • 检查:是否 (KB ∧ ¬α) 是一个矛盾?

      • 如果是这样,那么 KB ⊨ α。

      • 否则,没有蕴涵。

反证法是计算机科学中常用的一种工具。如果我们的知识库是真的,并且它与 ¬α 相矛盾,这意味着 ¬α 是假的,因此 α 必须是真的。更技术地说,算法会执行以下操作:

  • 要确定 KB ⊨ α:

    • 将 (KB ∧ ¬α) 转换为合取范式。

    • 继续检查我们是否可以使用解析产生一个新的子句。

    • 如果我们产生了空子句(相当于 False),恭喜!我们已经达到了矛盾,从而证明了 KB ⊨ α。

    • 然而,如果没有达到矛盾并且无法再推导出更多子句,则不存在蕴涵。

这里有一个例子说明这个算法可能如何工作:

  • (A ∨ B) ∧ (¬B ∨ C) ∧ (¬C) 是否蕴涵 A?

  • 首先,为了进行反证法,我们假设 A 是假的。因此,我们到达 (A ∨ B) ∧ (¬B ∨ C) ∧ (¬C) ∧ (¬A)。

  • 现在,我们可以开始生成新的信息。由于我们知道 C 是假的 (¬C),(¬B ∨ C) 可以成立的唯一方式是 B 也是假的。因此,我们可以将 (¬B) 添加到我们的 KB 中。

  • 接下来,由于我们知道 (¬B),(A ∨ B) 可以成立的唯一方式是 A 为真。因此,我们可以将 (A) 添加到我们的 KB 中。

  • 现在我们的知识库有两个互补的命题,(A) 和 (¬A)。我们解决了它们,得到了空集,( )。空集根据定义是假的,所以我们已经得到了一个矛盾。

一阶逻辑

一阶逻辑是另一种逻辑类型,它允许我们比命题逻辑更简洁地表达更复杂的思想。一阶逻辑使用两种类型的符号:常量符号谓词符号。常量符号代表对象,而谓词符号类似于接受参数并返回真或假值的关联或函数。

例如,我们回到霍格沃茨的逻辑谜题,不同的人和房子分配。常量符号是人或房子,如米涅瓦、波莫娜、格兰芬多、赫奇帕奇等。谓词符号是某些常量符号为真或假的属性。例如,我们可以用句子 Person(Minerva) 表达米涅瓦是人这个想法。同样,我们可以用句子 House(Gryffindor) 表达格兰芬多是一个房子。所有的逻辑连接词在一阶逻辑中与之前一样工作。例如,¬House(Minerva) 表达了米涅瓦不是一个房子的想法。谓词符号也可以接受两个或更多参数,并表达它们之间的关系。例如,BelongsTo 表达了两个人和房子之间的关系。因此,米涅瓦属于格兰芬多的想法可以表达为 BelongsTo(Minerva, Gryffindor)。一阶逻辑允许为每个人和每个房子有一个符号。这比命题逻辑更简洁,在命题逻辑中,每个人—房子的分配都需要一个不同的符号。

全称量化

量化是一种可以在一阶逻辑中使用的工具,用于表示不使用特定常量符号的句子。全称量化使用符号 ∀ 来表达“对所有”。因此,例如,句子 ∀x. BelongsTo(x, Gryffindor) → ¬BelongsTo(x, Hufflepuff) 表达了对于每个符号,如果这个符号属于格兰芬多,它就不属于赫奇帕奇的想法。

存在量化

存在量化是一个与全称量化平行的概念。然而,全称量化被用来创建对所有 x 都为真的句子,而存在量化被用来创建至少对一个 x 为真的句子。它使用符号 ∃ 来表示。例如,句子 ∃x. House(x) ∧ BelongsTo(Minerva, x) 表示至少有一个符号既是房子,又属于米涅瓦。换句话说,这表达了米涅瓦属于一个房子的想法。

存在量词和全称量词可以在同一个句子中使用。例如,句子 ∀x. Person(x) → (∃y. House(y) ∧ BelongsTo(x, y)) 表达了这样的想法:如果 x 是一个人,那么至少有一个房子 y,这个人属于它。换句话说,这个句子的意思是每个人都属于一个房子。

除此之外,还有其他类型的逻辑,它们的共同点在于它们都存在于追求表示信息的过程中。这些是我们用来在我们的 AI 中表示知识的系统。

第二讲

原文:cs50.harvard.edu/ai/notes/2/

不确定性

上次讲座,我们讨论了 AI 如何表示和推导新的知识。然而,在现实中,AI 通常只有对世界的部分了解,这留下了不确定性的空间。尽管如此,我们希望我们的 AI 在这些情况下做出最佳可能的决策。例如,在预测天气时,AI 有关于今天天气的信息,但无法 100%准确地预测明天的天气。尽管如此,我们可以比随机性做得更好,今天的讲座是关于我们如何创建在有限信息和不确定性下做出最优决策的 AI。

概率

不确定性可以用一系列事件及其发生的可能性或概率来表示。

可能的世界

每个可能的情况都可以被视为一个世界,用小写希腊字母 omega ω表示。例如,掷骰子可以产生六个可能的世界:一个世界是骰子显示 1,一个世界是骰子显示 2,以此类推。为了表示某个世界的概率,我们写 P(ω)。

概率公理

  • 0 < P(ω) < 1:每个代表概率的值必须在 0 和 1 之间。

    • 零是一个不可能发生的事件,比如掷一个标准骰子得到 7。

    • 一个是一定会发生的事件,比如掷一个标准骰子得到小于 10 的数值。

    • 通常情况下,数值越高,事件发生的可能性就越大。

  • 所有可能事件的概率之和等于 1。

概率求和

抛掷一个标准骰子得到数字 R 的概率可以表示为 P(R)。在我们的例子中,P(R) = 1/6,因为有六个可能的世界(从 1 到 6 的任意数字),每个世界发生的可能性是相等的。现在,考虑抛掷两个骰子的事件。现在,有 36 个可能的事件,它们再次是同等可能发生的。

36 个事件

然而,如果我们尝试预测两个骰子的和会发生什么?在这种情况下,我们只有 11 个可能值(和必须从 2 到 12),它们并不以相同的频率发生。

两个骰子的和

要得到一个事件的概率,我们将其发生的世界数除以所有可能世界数。例如,当抛掷两个骰子时,有 36 个可能的世界。只有在这 36 个世界中,当两个骰子都显示 6 时,我们才能得到和为 12。因此,P(12) = 1/36,或者说,用文字表达,抛掷两个骰子得到两个数之和为 12 的概率是 1/36。P(7)是多少?我们数一下,发现和为 7 的情况发生在 6 个世界中。因此,P(7) = 6/36 = 1/6。

无条件概率

无条件概率是在没有任何其他证据的情况下对命题的信念程度。我们之前提出的所有问题都是无条件概率问题,因为掷骰子的结果不依赖于先前的事件。

条件概率

条件概率是在已有某些证据被揭示的情况下,对命题的信念程度。正如引言中讨论的,人工智能可以使用部分信息对未来做出有根据的猜测。为了使用这些信息,这些信息会影响未来事件发生的概率,我们依赖于条件概率。

条件概率使用以下符号表示:P(a | b),意味着“在已知事件 b 已经发生的情况下,事件 a 发生的概率,”或者更简洁地说,“给定 ba 的概率。”现在我们可以提出像今天下雨的概率是多少,给定昨天已经下雨 P(rain today | rain yesterday),或者患者有疾病的概率是多少,给定他们的检测结果 P(disease | test results)。

从数学上讲,为了计算给定 ba 的条件概率,我们使用以下公式:

条件概率公式

用语言来说,给定 ba 的概率是真实的,等于 ab 同时为真的概率,除以 b 的概率。对此进行直观推理的一种方式是“我们感兴趣的只是 ab 同时为真的事件(分子),但只从我们知道 b 为真的世界中(分母)。”除以 b 限制了可能的世界,使其只包含 b 为真的世界。以下是与上述公式代数上等价的形式:

等效公式

例如,考虑 P(sum 12 | roll six on one die),或者在我们已经掷出一个六的情况下,掷两个骰子得到总和为十二的概率。为了计算这个概率,我们首先将我们的世界限制在第一个骰子的值为六的情况:

限制世界

现在我们询问事件 a(总和为 12)在我们限制问题的世界中发生的次数是多少(除以 P(b),即第一个骰子掷出 6 的概率)。

条件概率

随机变量

随机变量是概率论中的一个变量,它有一个可能的值域。例如,为了表示掷骰子的可能结果,我们可以定义一个随机变量 Roll,它可以取值 {1, 2, 3, 4, 5, 6}。为了表示航班的状况,我们可以定义一个变量 Flight,它可以取值 {on time, delayed, canceled}。

通常,我们感兴趣的是每个值出现的概率。我们使用概率分布来表示这一点。例如,

  • P(Flight = on time) = 0.6

  • P(Flight = delayed) = 0.3

  • P(Flight = canceled) = 0.1

用文字来解释概率分布,这意味着有 60%的几率航班准点,30%的几率延误,10%的几率取消。注意,如前所述,所有可能结果的概率之和为 1。

概率分布可以用向量更简洁地表示。例如,P(Flight) = <0.6, 0.3, 0.1>. 为了使这种表示法可解释,值必须有一个固定的顺序(在我们的例子中,准点,延误,取消)。

独立性

独立性是知道一个事件的发生不会影响另一个事件发生的概率。例如,当掷两个骰子时,每个骰子的结果是相互独立的。掷出第一个骰子的 4 点不会影响我们掷出的第二个骰子的值。这与像早晨有云和下午下雨这样的相关事件相反。如果早晨有云,下午下雨的可能性更大,所以这些事件是相关的。

独立性可以用数学定义:事件ab是独立的,当且仅当ab的概率等于a的概率乘以b的概率:P(a ∧ b) = P(a)P(b).

贝叶斯定理

贝叶斯定理在概率论中常用以计算条件概率。用文字来说,贝叶斯定理表明,给定ab的概率等于给定ba的概率,乘以b的概率,除以a的概率。

贝叶斯定理

例如,我们想要计算如果早晨有云,下午下雨的概率,或 P(雨 | 云). 我们从以下信息开始:

  • 80%的雨天下午始于多云的早晨,或 P(云 | 雨).

  • 40%的天数早晨有云,或 P().

  • 10%的天数下午有雨,或 P().

应用贝叶斯定理,我们计算(0.1)(0.8)/(0.4) = 0.2. 这意味着,如果早晨有云,下午下雨的概率是 20%。

知道 P(a | b),除了 P(a)和 P(b),我们可以计算 P(b | a)。这很有帮助,因为知道在未知原因给定的情况下可见效果的条件下概率,P(visible effect | unknown cause),允许我们计算给定可见效果的未知原因的概率,P(unknown cause | visible effect)。例如,我们可以通过医学试验学习 P(medical test results | disease),在试验中测试患有疾病的人,看看测试有多频繁地检测到这一点。了解这一点后,我们可以计算 P(disease | medical test results),这是有价值的诊断信息。

联合概率

联合概率是多个事件同时发生的可能性。

让我们考虑以下例子,关于早晨有云和下午下雨的概率。

C = C = ¬云
0.4 0.6
R = R = ¬雨
--- ---
0.1 0.9

观察这些数据,我们无法说早晨的云与下午下雨的可能性有关。要能够做到这一点,我们需要查看两个变量所有可能结果的联合概率。我们可以用以下表格表示:

R = R = ¬雨
C = 0.08 0.32
C = ¬云 0.02 0.58

现在我们能够了解事件共现的信息。例如,我们知道某一天早晨有云和下午下雨的概率是 0.08。早晨无云和下午无雨的概率是 0.58。

使用联合概率,我们可以推导出条件概率。例如,如果我们对下午下雨时早晨有云的概率分布感兴趣。P(C | 雨) = P(C, 雨)/P()(顺便提一下:在概率论中,逗号和 ∧ 可以互换使用)。因此,P(C, 雨) = P(C ∧ 雨)。换句话说,我们将雨和云的联合概率除以雨的概率。

在最后一个方程中,我们可以将 P() 视为一个常数,它乘以 P(C, 雨)。因此,我们可以重写 P(C, 雨)/P() = αP(C, 雨),或 α<0.08, 0.02>。提取 α 后,我们得到在下午下雨的条件下 C 的可能值的概率比例。也就是说,如果下午下雨,早晨有云和早晨无云的概率比例是 0.08:0.02。请注意,0.08 和 0.02 的和并不等于 1;然而,由于这是随机变量 C 的概率分布,我们知道它们的和应该等于 1。因此,我们需要通过计算 α 来归一化这些值,使得 α0.08 + α0.02 = 1。最后,我们可以说 P(C | 雨) = <0.8, 0.2>。

概率规则

  • 否定: P(¬a) = 1 - P(a). 这源于所有可能世界的概率之和为 1,互补命题 a¬a 包括所有可能世界。

  • 包含-排除: P(a ∨ b) = P(a) + P(b) - P(a ∧ b). 这可以这样解释:ab 为真的世界等于所有 a 为真的世界,加上所有 b 为真的世界。然而,在这种情况下,一些世界被计算了两次(即 ab 都为真的世界)。为了消除这种重叠,我们减去一次 ab 都为真的世界(因为它们被计算了两次)。

    这里有一个来自课堂外部的例子可以阐明这一点。假设我 80%的日子里吃冰淇淋,70%的日子里吃饼干。如果我们计算今天我吃冰淇淋或饼干的概率 P(ice cream ∨ cookies) 而不减去 P(ice cream ∧ cookies),我们会错误地得到 0.7 + 0.8 = 1.5。这与概率范围在 0 到 1 之间的公理相矛盾。为了纠正重复计算我同时吃冰淇淋和饼干的日子,我们需要减去一次 P(ice cream ∧ cookies)。

  • 边缘化: P(a) = P(a, b) + P(a, ¬b). 这里面的想法是 b¬b 是互斥的概率。也就是说,b¬b 同时发生的概率是 0。我们还知道 b¬b 的总和为 1。因此,当 a 发生时,b 要么发生,要么不发生。当我们考虑 ab 同时发生的概率,以及 a¬b 同时发生的概率,我们最终得到的就是 a 的概率。

边缘化可以用以下方式表示随机变量:

边缘化

方程式的左边表示“随机变量 X 取值 xᵢ 的概率。”例如,对于前面提到的变量 C,可能的两个值是“早上有云”和“早上无云”。方程式的右边是边缘化的概念。P(X = xᵢ) 等于 xᵢ 和随机变量 Y 的每个值的联合概率之和。例如,P(C = cloud) = P(C = cloud, R = rain) + P(C = cloud, R = ¬rain) = 0.08 + 0.32 = 0.4。

  • 条件化: P(a) = P(a | b)P(b) + P(a | ¬b)P(¬b). 这与边缘化有类似的想法。事件 a 发生的概率等于 ab 条件下的概率乘以 b 的概率,加上 a¬b 条件下的概率乘以 ¬b 的概率。

条件化

在这个公式中,随机变量 X 以 xᵢ 的值出现,其概率等于 xᵢ 在每个随机变量 Y 的值下的概率之和乘以变量 Y 取该值的概率。如果我们记住 P(a | b) = P(a, b)/P(b),这个公式是有意义的。如果我们乘以 P(b),我们最终得到 P(a, b),然后我们就可以像边缘化一样做了。

贝叶斯网络

贝叶斯网络是一种表示随机变量之间依赖关系的数据结构。贝叶斯网络具有以下特性:

  • 它们是有向图。

  • 图表上的每个节点代表一个随机变量。

  • 从 X 到 Y 的箭头表示 X 是 Y 的父节点。也就是说,Y 的概率分布取决于 X 的值。

  • 每个节点 X 都有概率分布 P(X | Parents(X))。

让我们考虑一个涉及影响我们是否准时到达约会的时间的贝叶斯网络的例子。

贝叶斯网络

让我们从上到下描述这个贝叶斯网络:

  • 雨是网络中的根节点。这意味着它的概率分布不依赖于任何先前事件。在我们的例子中,Rain 是一个可以取值 {none, light, heavy} 的随机变量,其概率分布如下:

    none light heavy
    0.7 0.2 0.1
  • 在我们的例子中,Maintenance 编码是否存在火车轨道维护,取值有 {yes, no}。Rain 是 Maintenance 的父节点,这意味着 Maintenance 的概率分布受 Rain 影响。

    R yes no
    none 0.4 0.6
    light 0.2 0.8
    heavy 0.1 0.9
  • Train 是一个变量,表示火车是否准时或延误,取值有 {on time, delayed}。请注意,Train 从 Maintenance 和 Rain 两处都有箭头指向它。这意味着它们都是 Train 的父节点,它们的值会影响 Train 的概率分布。

    R M on time delayed
    none yes 0.8 0.2
    none no 0.9 0.1
    light yes 0.6 0.4
    light no 0.7 0.3
    heavy yes 0.4 0.6
    heavy no 0.5 0.5
  • 约会是一个随机变量,表示我们是否参加约会,取值有 {attend, miss}。请注意,它的唯一父节点是 Train。关于贝叶斯网络的一个值得注意的点:父节点只包括直接关系。确实,维护会影响火车是否准时,火车是否准时会影响我们是否参加约会。然而,最终直接影响我们参加约会机会的是火车是否准时到达,这正是贝叶斯网络所表示的。例如,如果火车准时到达,可能是大雨和轨道维护,但这对我们是否到达约会没有影响。

    T attend miss
    on time 0.9 0.1
    delayed 0.6 0.4

例如,如果我们想找到在无维护和轻雨天气下火车延误时错过会议的概率,或者 P(light, no, delayed, miss),我们将计算以下内容:P(light)P(no | light)P(delayed | light, no)P(miss | delayed)。每个单独概率的值可以在上面的概率分布中找到,然后这些值相乘以产生 P(no, light, delayed, miss)。

推理

在上一堂课中,我们探讨了通过蕴涵进行推理。这意味着我们可以根据我们已有的信息确定性地得出新的信息。我们也可以根据概率推断新的信息。虽然这并不允许我们确定地知道新的信息,但它允许我们找出某些值的概率分布。推理具有多个属性。

  • 查询X:我们想要计算概率分布的变量。

  • 证据变量E:对于事件e已经观察到的变量。例如,我们可能观察到有轻微降雨,这个观察结果有助于我们计算火车延误的概率。

  • 隐藏变量Y:不是查询且尚未观察到的变量。例如,站在火车站,我们可以观察到是否有雨,但我们无法知道道路上是否有轨道维护。因此,在这种情况下,Maintenance 将是一个隐藏变量。

  • 目标:计算P(X | e)。例如,根据我们知道有轻微降雨的证据e,计算 Train 变量(查询)的概率分布。

让我们举一个例子。我们想要计算在已知有轻微降雨且没有轨道维护的证据下,预约变量(Appointment)的概率分布。也就是说,我们知道有轻微降雨且没有轨道维护,我们想要找出我们参加预约和错过预约的概率,即P(Appointment | light, no)。从联合概率部分,我们知道我们可以将预约随机变量的可能值表示为比例,将P(Appointment | light, no)重写为 αP(Appointment, light, no)。如果其父变量只有 Train 变量,而不是 Rain 或 Maintenance,我们该如何计算预约变量的概率分布?在这里,我们将使用边缘化。P(Appointment, light, no)的值等于 α[P(Appointment, light, no, delayed) + P(Appointment, light, no, on time*)]。

枚举推理

枚举推理是一个在给定观察到的证据 e 和一些隐藏变量 Y 的情况下寻找变量 X 的概率分布的过程。

枚举推理

在这个方程中,X 代表查询变量,e 代表观察到的证据,y 代表所有隐藏变量的值,α将结果归一化,使得我们最终得到的概率加起来等于 1。用文字解释这个方程,它表示的是,给定 e 的 X 的概率分布等于 X 和 e 的归一化概率分布。为了得到这个分布,我们求和 X、e 和 y 的归一化概率,其中 y 每次取隐藏变量 Y 的不同值。

Python 中存在多个库来简化概率推理的过程。我们将查看pomegranate库,看看如何用代码表示上述数据。

首先,我们创建节点并为每个节点提供一个概率分布。

from pomegranate import *

# Rain node has no parents rain = Node(DiscreteDistribution({
    "none": 0.7,
    "light": 0.2,
    "heavy": 0.1
}), name="rain")

# Track maintenance node is conditional on rain maintenance = Node(ConditionalProbabilityTable([
    ["none", "yes", 0.4],
    ["none", "no", 0.6],
    ["light", "yes", 0.2],
    ["light", "no", 0.8],
    ["heavy", "yes", 0.1],
    ["heavy", "no", 0.9]
], [rain.distribution]), name="maintenance")

# Train node is conditional on rain and maintenance train = Node(ConditionalProbabilityTable([
    ["none", "yes", "on time", 0.8],
    ["none", "yes", "delayed", 0.2],
    ["none", "no", "on time", 0.9],
    ["none", "no", "delayed", 0.1],
    ["light", "yes", "on time", 0.6],
    ["light", "yes", "delayed", 0.4],
    ["light", "no", "on time", 0.7],
    ["light", "no", "delayed", 0.3],
    ["heavy", "yes", "on time", 0.4],
    ["heavy", "yes", "delayed", 0.6],
    ["heavy", "no", "on time", 0.5],
    ["heavy", "no", "delayed", 0.5],
], [rain.distribution, maintenance.distribution]), name="train")

# Appointment node is conditional on train appointment = Node(ConditionalProbabilityTable([
    ["on time", "attend", 0.9],
    ["on time", "miss", 0.1],
    ["delayed", "attend", 0.6],
    ["delayed", "miss", 0.4]
], [train.distribution]), name="appointment") 

其次,我们通过添加节点并描述它们之间通过添加边连接的节点(回想一下,贝叶斯网络是一个有向图,由带有箭头的节点组成)来创建模型。

# Create a Bayesian Network and add states model = BayesianNetwork()
model.add_states(rain, maintenance, train, appointment)

# Add edges connecting nodes model.add_edge(rain, maintenance)
model.add_edge(rain, train)
model.add_edge(maintenance, train)
model.add_edge(train, appointment)

# Finalize model model.bake() 

现在,要询问某个事件的概率,我们使用感兴趣的值运行模型。在这个例子中,我们想知道没有雨、没有轨道维护、火车准时到达,并且我们参加会议的概率。

# Calculate probability for a given observation probability = model.probability([["none", "no", "on time", "attend"]])

print(probability) 

否则,我们可以使用程序为所有变量提供给定一些观察证据的概率分布。在以下情况下,我们知道火车延误了。根据这个信息,我们计算并打印变量 Rain、Maintenance 和 Appointment 的概率分布。

# Calculate predictions based on the evidence that the train was delayed predictions = model.predict_proba({
    "train": "delayed"
})

# Print predictions for each node for node, prediction in zip(model.states, predictions):
    if isinstance(prediction, str):
        print(f"{node.name}: {prediction}")
    else:
        print(f"{node.name}")
        for value, probability in prediction.parameters[0].items():
            print(f"  {value}: {probability:.4f}") 

上面的代码使用了枚举推理。然而,这种计算概率的方法效率低下,尤其是在模型中有许多变量时。另一种方法可能是放弃精确推理而采用近似推理。这样做,我们在生成的概率中会失去一些精度,但通常这种不精确是可以忽略不计的。相反,我们获得了一种可扩展的概率计算方法。

抽样

抽样是近似推理的一种技术。在抽样中,每个变量根据其概率分布抽取一个值。我们将从一个课外例子开始,然后介绍课内的例子。

要使用骰子进行抽样生成分布,我们可以多次掷骰子并记录每次得到的结果。假设我们掷了 600 次骰子。我们计算得到 1 的次数,预计大约是 100 次,然后对其他值 2-6 重复此操作。然后,我们将每个计数除以总掷骰子次数。这将生成掷骰子值的近似分布:一方面,我们不太可能得到每个值都有 1/6 的概率出现的结果(这是精确概率),但我们会得到一个接近这个值的结果。

这里有一个来自讲座的例子:如果我们从采样 Rain 变量开始,将生成概率为 0.7 的 值,概率为 0.2 的 轻微 值,以及概率为 0.1 的 严重 值。假设我们得到的采样值是 。当我们到达 Maintenance 变量时,我们也对其进行采样,但只从 Rain 等于 的概率分布中进行采样,因为这是一个已经采样的结果。我们将继续这样做,直到所有节点。现在我们有一个样本,重复这个过程多次生成一个分布。现在,如果我们想回答一个问题,比如 P(Train = on time) 是什么,我们可以计算变量 Train 有 准时 值的样本数量,然后将结果除以样本总数。这样,我们就为 P(Train = on time) 生成了一个近似概率。

我们还可以回答涉及条件概率的问题,例如 P(Rain = light | Train = on time)。在这种情况下,我们忽略所有 Train 值不是 准时 的样本,然后像以前一样进行。我们计算在 Train = 准时 的样本中,变量 Rain = 轻微 的样本数量,然后除以 Train = 准时 的样本总数。

在代码中,一个采样函数可以看起来像 generate_sample

import pomegranate

from collections import Counter

from model import model

def generate_sample():

    # Mapping of random variable name to sample generated
    sample = {}

    # Mapping of distribution to sample generated
    parents = {}

    # Loop over all states, assuming topological order
    for state in model.states:

        # If we have a non-root node, sample conditional on parents
        if isinstance(state.distribution, pomegranate.ConditionalProbabilityTable):
            sample[state.name] = state.distribution.sample(parent_values=parents)

        # Otherwise, just sample from the distribution alone
        else:
            sample[state.name] = state.distribution.sample()

        # Keep track of the sampled value in the parents mapping
        parents[state.distribution] = sample[state.name]

    # Return generated sample
    return sample 

现在,为了计算 P(Appointment | Train = delayed),即火车延误时 Appointment 变量的概率分布,我们做以下操作:

# Rejection sampling
# Compute distribution of Appointment given that train is delayed N = 10000
data = []

# Repeat sampling 10,000 times for i in range(N):

    # Generate a sample based on the function that we defined earlier
    sample = generate_sample()

    # If, in this sample, the variable of Train has the value delayed, save the sample. Since we are interested interested in the probability distribution of Appointment given that the train is delayed, we discard the sampled where the train was on time.
    if sample["train"] == "delayed":
        data.append(sample["appointment"])

# Count how many times each value of the variable appeared. We can later normalize by dividing the results by the total number of saved samples to get the approximate probabilities of the variable that add up to 1. print(Counter(data)) 

似然加权

在上面的采样例子中,我们丢弃了不符合我们已有证据的样本。这是低效的。一种绕过这个问题的方法是通过似然加权,使用以下步骤:

  • 首先固定证据变量的值。

  • 使用贝叶斯网络中的条件概率采样非证据变量。

  • 将每个样本按其 似然 加权:所有证据发生的概率。

例如,如果我们观察到火车准时到达,我们就会像以前一样开始采样。我们根据其概率分布采样 Rain 的值,然后是 Maintenance,但当到达 Train 时,我们总是给出观察到的值,在我们的例子中,是 准时。然后我们继续根据 Train = 准时 给定的概率分布采样 Appointment。现在这个样本存在了,我们根据观察变量给定的条件概率来加权。也就是说,如果我们采样了 Rain 并得到 轻微,然后采样 Maintenance 并得到 ,那么我们将对这个样本进行加权,权重为 P(Train = on time | light, yes)。

马尔可夫模型

到目前为止,我们考虑了一些基于我们观察到的某些信息的概率问题。在这种范式下,时间维度没有以任何方式表示。然而,许多任务确实依赖于时间维度,例如预测。为了表示时间变量,我们将创建一个新的变量 X,并根据感兴趣的事件对其进行更改,使得 Xₜ是当前事件,Xₜ₊₁是下一个事件,依此类推。为了能够预测未来的事件,我们将使用马尔可夫模型。

马尔可夫假设

马尔可夫假设是一个假设,即当前状态只依赖于有限数量的先前状态。这对我们来说很重要。想想预测天气的任务。在理论上,我们可以使用过去一年的所有数据来预测明天的天气。然而,这是不可行的,因为这需要巨大的计算能力,而且可能没有关于基于 365 天前的天气明天天气的条件概率的信息。使用马尔可夫假设,我们限制我们的先前状态(例如,在预测明天的天气时考虑多少天前的天气),从而使任务变得可管理。这意味着我们可能得到对感兴趣概率的更粗糙的近似,但这通常足以满足我们的需求。此外,我们可以使用基于最后一个事件的信息的马尔可夫模型(例如,根据今天的天气预测明天的天气)。

马尔可夫链

马尔可夫链是一系列随机变量,其中每个变量的分布遵循马尔可夫假设。也就是说,链中的每个事件都是基于之前事件发生的概率。

要开始构建马尔可夫链,我们需要一个转移模型,该模型将指定基于当前事件可能值的下一个事件的概率分布。

过渡模型

在这个例子中,如果今天是晴天,那么明天也是晴天的概率是 0.8。这是合理的,因为晴天之后接着是晴天的可能性更大。然而,如果今天是雨天,那么明天下雨的概率是 0.7,因为雨天更有可能连续出现。使用这个转移模型,可以采样一个马尔可夫链。从一个雨天或晴天开始,然后根据今天天气是晴天还是雨天来采样下一天。然后,根据明天的情况来调整后天概率,依此类推,从而形成一个马尔可夫链:

马尔可夫链

给定这个马尔可夫链,我们现在可以回答诸如“连续四天降雨的概率是多少?”等问题。以下是一个如何在代码中实现马尔可夫链的示例:

from pomegranate import *

# Define starting probabilities start = DiscreteDistribution({
    "sun": 0.5,
    "rain": 0.5
})

# Define transition model transitions = ConditionalProbabilityTable([
    ["sun", "sun", 0.8],
    ["sun", "rain", 0.2],
    ["rain", "sun", 0.3],
    ["rain", "rain", 0.7]
], [start])

# Create Markov chain model = MarkovChain([start, transitions])

# Sample 50 states from chain print(model.sample(50)) 

隐藏马尔可夫模型

隐藏马尔可夫模型是一种针对具有隐藏状态的系统的马尔可夫模型,这些状态生成某些观测事件。这意味着有时,AI 对世界有一些测量,但没有访问世界精确状态的途径。在这些情况下,世界的状态被称为隐藏状态,而 AI 可以访问的任何数据都是观测。以下是一些例子:

  • 对于探索未知领域的机器人,隐藏状态是它的位置,而观测是机器人传感器记录的数据。

  • 在语音识别中,隐藏状态是所说的单词,而观测是音频波形。

  • 在测量网站上的用户参与度时,隐藏状态是用户的参与程度,而观测是网站或应用的统计分析。

对于我们的讨论,我们将使用以下例子。我们的 AI 想要推断天气(隐藏状态),但它只能访问一个室内摄像头,该摄像头记录了有多少人带着伞。以下是我们的传感器模型(也称为发射模型),它表示这些概率:

传感器模型

在这个模型中,如果天气晴朗,人们最不可能带伞进建筑物。如果下雨,那么人们很可能带伞进建筑物。通过观察人们是否带伞,我们可以以合理的可能性预测外面的天气。

传感器马尔可夫假设

假设证据变量只依赖于对应的状态。例如,对于我们的模型,我们假设人们是否带伞去办公室只取决于天气。这并不一定反映完整的真相,因为例如,更负责任、怕雨的人即使在晴天也可能随身携带伞,如果我们知道每个人的性格,这将向模型添加更多数据。然而,传感器马尔可夫假设忽略了这些数据,假设只有隐藏状态影响观测。

隐藏马尔可夫模型可以用具有两层马尔可夫链来表示。顶层变量 X 代表隐藏状态。底层变量 E 代表证据,即我们拥有的观测。

隐藏马尔可夫链

基于隐藏马尔可夫模型,可以实现多个任务:

  • 过滤:给定从开始到现在的观测,计算当前状态的概率分布。例如,给定从时间开始到今天人们带伞的信息,我们生成今天是否下雨的概率分布。

  • 预测:给定从开始到现在的观测,计算未来状态的概率分布。

  • 平滑化:给定从开始到现在的观察结果,计算过去状态的概率分布。例如,计算给定今天人们带伞的情况下,昨天下雨的概率。

  • 最可能的解释:给定从开始到现在的观察结果,计算最可能的事件序列。

最可能的解释任务可以用于语音识别等过程,其中,基于多个波形,AI 推断出最可能导致这些波形的单词或音节的序列。下面是一个用于最可能解释任务的隐马尔可夫模型的 Python 实现:

from pomegranate import *

# Observation model for each state sun = DiscreteDistribution({
    "umbrella": 0.2,
    "no umbrella": 0.8
})

rain = DiscreteDistribution({
    "umbrella": 0.9,
    "no umbrella": 0.1
})

states = [sun, rain]

# Transition model transitions = numpy.array(
    [[0.8, 0.2], # Tomorrow's predictions if today = sun
     [0.3, 0.7]] # Tomorrow's predictions if today = rain )

# Starting probabilities starts = numpy.array([0.5, 0.5])

# Create the model model = HiddenMarkovModel.from_matrix(
    transitions, states, starts,
    state_names=["sun", "rain"]
)
model.bake() 

注意,我们的模型既有传感器模型也有转换模型。对于隐马尔可夫模型,我们需要这两个模型。在下面的代码片段中,我们看到一系列观察结果,即人们是否带伞进入大楼,基于这个序列,我们将运行模型,该模型将生成并打印最可能的解释(即最可能导致这种观察模式的天气序列):

from model import model

# Observed data observations = [
    "umbrella",
    "umbrella",
    "no umbrella",
    "umbrella",
    "umbrella",
    "umbrella",
    "umbrella",
    "no umbrella",
    "no umbrella"
]

# Predict underlying states predictions = model.predict(observations)
for prediction in predictions:
    print(model.states[prediction].name) 

在这种情况下,程序的输出将是雨,雨,晴,雨,雨,雨,雨,晴,晴。这个输出代表了根据我们对人们是否带伞进入大楼的观察,最可能的天气模式。

第三讲

原文:cs50.harvard.edu/ai/notes/3/

优化

优化是从一组可能选项中选择最佳选项。我们已经在寻找最佳可能选项的问题中遇到过,例如在最小-最大算法中,今天我们将学习我们可以用来解决更广泛范围问题的工具。

局部搜索

局部搜索是一种搜索算法,它保持单个节点并通过移动到相邻节点来搜索。这种算法与我们之前看到的搜索类型不同。例如,在迷宫解决中,我们想要找到到达目标的最快路径,而局部搜索则关注于找到问题的最佳答案。通常,局部搜索会得到一个非最优但“足够好”的答案,从而节省计算能力。考虑以下局部搜索问题的例子:我们有四个位于特定位置的房屋。我们想要建造两家医院,使得每个房屋到医院的距离最小化。这个问题可以如下可视化:

房屋和医院

在这个插图上,我们看到的是房屋和医院的一种可能配置。它们之间的距离使用曼哈顿距离(向上、向下和向侧的移动次数;在第六讲 中详细讨论)来衡量,每个房屋到最近医院的距离之和是 17。我们称之为成本,因为我们试图最小化这个距离。在这种情况下,状态可以是房屋和医院的任何一种配置。

抽象这个概念,我们可以将房屋和医院的每种配置表示为下面的状态空间景观。图片中的每根条形代表一个状态的值,在我们的例子中,这将是房屋和医院某种配置的成本。

状态空间景观

基于这个可视化,我们可以为接下来的讨论定义几个重要术语:

  • 目标函数是我们用来最大化解决方案价值的函数。

  • 成本函数是我们用来最小化解决方案成本(这是我们将在房屋和医院例子中使用的函数。我们希望最小化房屋到医院的距离)的函数。

  • 当前状态是函数目前正在考虑的状态。

  • 相邻状态是当前状态可以转换到的状态。在上面的单维状态空间景观中,相邻状态是当前状态两侧的状态。在我们的例子中,相邻状态可能是将其中一家医院向任何方向移动一步所得到的状态。相邻状态通常与当前状态相似,因此它们的值接近当前状态的值。

注意,局部搜索算法的工作方式是考虑当前状态中的一个节点,然后将节点移动到当前状态的一个相邻状态。这与例如最小-最大算法不同,在最小-最大算法中,状态空间中的每个状态都被递归地考虑。

爬山

爬山是局部搜索算法的一种类型。在这个算法中,将相邻状态与当前状态进行比较,如果其中任何一个更好,我们就将当前节点从当前状态切换到那个相邻状态。什么被认为是更好的,取决于我们是否使用目标函数,偏好更高的值,还是递减函数,偏好更低的值。

爬山算法在伪代码中的表现形式如下:

函数 Hill-Climb(问题):

  • 当前状态 = 问题的初始状态

  • repeat:

    • 相邻状态 = 当前状态的最佳值相邻状态

    • 如果相邻状态不如当前状态好:

      • 返回 当前状态
    • 当前状态 = 相邻状态

在这个算法中,我们从当前状态开始。在某些问题中,我们将知道当前状态是什么,而在其他情况下,我们必须随机选择一个状态作为起点。然后,我们重复以下操作:评估相邻状态,选择具有最佳值的那个。然后,我们将这个相邻状态的值与当前状态的值进行比较。如果相邻状态更好,我们将当前状态切换到相邻状态,并重复这个过程。当我们将最佳相邻状态与当前状态进行比较,且当前状态更好时,过程结束。然后,我们返回当前状态。

使用爬山算法,我们可以开始改进我们示例中分配给医院的地点。经过几次转换后,我们达到以下状态:

局部最小值处的房屋和医院

在这个状态下,成本是 11,这比初始状态的 17 有所改善。然而,这还不是最佳状态。例如,将医院移动到左上角房屋下方,可以将成本降低到 9,这比 11 更好。然而,这个版本的爬山算法无法达到那里,因为所有相邻状态的成本至少与当前状态一样高。从这个意义上说,爬山算法是短视的,通常满足于比其他一些解决方案更好的解决方案,但不一定是所有可能解决方案中的最佳。

局部和全局极小值与极大值

如上所述,爬山算法可能会陷入局部极大值或极小值。一个局部极大值(复数:maxima)是一个比其相邻状态具有更高值的态。与之相反,一个全局极大值是一个在状态空间中所有状态中具有最高值的态。

极大值

相反,一个局部最小值(复数:minima)是一个比其相邻状态具有更低值的态。与这相反,一个全局最小值是一个在状态空间中所有态中具有最低值的态。

最小值

爬山算法的问题在于它们可能最终陷入局部最小值和最大值。一旦算法达到一个点,其邻居的值对于函数的目的来说比当前状态更差,算法就会停止。特殊类型的局部最大值和最小值包括平坦的局部最大值/最小值,其中多个具有相等值的相邻状态形成一个高原,其邻居的值更差,以及肩部,其中多个具有相等值的相邻状态,高原的邻居可以是更好的也可以是更差的。从高原的中间开始,算法将无法向任何方向前进。

平坦的局部最大值/最小值和肩部

爬山法变体

由于爬山法的局限性,人们已经想到了多种变体来克服陷入局部最小值和最大值的问题。所有算法的变体都有一个共同点,即无论策略如何,每个变体仍然有可能最终陷入局部最小值和最大值,并且没有继续优化的手段。下面的算法表述中,较高的值被视为较好,但它们也适用于成本函数,其目标是最小化成本。

  • 最速上升:选择最高值的邻居。这是我们上面讨论的标准变体。

  • 随机性:从更高值的邻居中随机选择。这样做,我们选择走向任何可以提高我们值的方向。例如,如果最高值的邻居导致局部最大值,而另一个邻居导致全局最大值,这样做是有意义的。

  • 首选:选择第一个更高值的邻居。

  • 随机重启:多次进行爬山。每次,从一个随机状态开始。比较每次试验的最大值,并选择其中最高的一个。

  • 局部束搜索:选择k个最高值的邻居。这与大多数局部搜索算法不同,因为它使用多个节点进行搜索,而不仅仅是单个节点。

虽然局部搜索算法并不总是给出最佳可能的解决方案,但在考虑所有可能状态在计算上不可行的情况下,它们通常可以给出足够好的解决方案。

模拟退火

尽管我们已经看到了可以改进爬山法的变体,但它们都存在相同的缺陷:一旦算法达到局部最大值,它就会停止运行。模拟退火允许算法在陷入局部最大值时“摆脱”自己。

热处理是将金属加热并允许其缓慢冷却的过程,这有助于使金属变硬。这个过程被用作模拟退火算法的隐喻,该算法从高温开始,更有可能做出随机决策,随着温度的降低,它做出随机决策的可能性降低,变得更加“坚定”。这种机制允许算法将其状态改变为比当前状态更差的邻居状态,这就是它如何逃离局部最大值的原因。以下是对模拟退火算法的伪代码:

函数 Simulated-Annealing(problem, max):

  • 当前 = 问题的初始状态

  • for t = 1 to max:

    • T = Temperature(t)

    • 邻居 = 当前的随机邻居

    • ΔE = 相较于当前邻居有多好

    • if ΔE > 0:

      • current = neighbor
    • 以概率 e^(ΔE/T) 将当前设置为邻居

  • 返回当前

该算法将问题和一个max(算法应重复的次数)作为输入。对于每一次迭代,使用温度函数设置T。这个函数在早期迭代(当t较低时)返回较高的值,而在后期迭代(当t较高时)返回较低的值。然后,选择一个随机邻居,并计算ΔE,以量化邻居状态相较于当前状态有多好。如果邻居状态比当前状态好(ΔE > 0),就像之前一样,我们将当前状态设置为邻居状态。然而,当邻居状态更差(ΔE < 0)时,我们仍然可能将当前状态设置为那个邻居状态,并且我们以概率 e^(ΔE/T) 来这样做。这里的想法是,更负的ΔE将导致邻居状态被选择的概率更低,而温度T越高,邻居状态被选择的概率就越高。这意味着邻居状态越差,被选择的概率就越低,并且算法在处理过程中越早,将更差的邻居状态设置为当前状态的概率就越高。背后的数学原理如下:e 是一个常数(大约为 2.72),而ΔE是负的(因为邻居状态比当前状态差)。ΔE越负,得到的结果值越接近 0。温度T越高,ΔE/T越接近 0,使得概率越接近 1。

旅行商问题

在旅行商问题中,任务是连接所有点,同时选择最短的可能距离。例如,这就是配送公司需要做的事情:找到从商店到所有客户家并返回的最短路线。

旅行商问题

在这种情况下,一个相邻状态可能被视为两个箭头交换位置的状态。计算所有可能的组合使得这个问题计算量很大(仅有 10 个点就给出了 10!,即 3,628,800 条可能的路径)。通过使用模拟退火算法,可以在较低的计算成本下找到良好的解决方案。

线性规划

线性规划是一类优化线性方程(形式为 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。

线性规划的优化算法需要我们在几何学和线性代数方面的背景知识,我们不希望假设。相反,我们可以使用已经存在的算法,例如单纯形法和内点法。

以下是一个使用 Python 中的 scipy 库的线性规划示例:

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") 

满足约束

满足约束问题是需要分配变量值以满足某些条件的问题类别。

满足约束问题具有以下属性:

  • 变量的集合(x₁, x₂, …, xₙ)

  • 每个变量的域集合

  • 约束集合 C

数独可以表示为一个满足约束问题,其中每个空格都是一个变量,域是数字 1-9,约束是不能相等的方格。

考虑另一个例子。学生 1-4 每人从 A、B、…、G 中选择三门课程。每门课程都需要进行考试,可能的考试日期是星期一、星期二和星期三。然而,同一位学生不能在同一天参加两次考试。在这种情况下,变量是课程,域是日期,约束是哪些课程不能安排在同一天进行考试,因为同一位学生正在学习它们。这可以表示如下:

约束满足设置

这个问题可以通过表示为图的约束来解决。图上的每个节点代表一门课程,如果两门课程不能在同一天安排,则在这两门课程之间画一条边。在这种情况下,图将看起来像这样:

约束满足约束

关于约束满足问题,还有一些术语值得了解:

  • 硬约束是必须在正确解中满足的约束。

  • 软约束是表达相对于其他解决方案更受偏好的约束。

  • 一元约束是只涉及一个变量的约束。在我们的例子中,一元约束可能是说课程 A 不能在星期一进行考试(A ≠ 星期一)。

  • 二元约束是涉及两个变量的约束。这是我们上面例子中使用的那种约束,表示某些两门课程不能有相同的值(A ≠ B)。

节点一致性

节点一致性是指一个变量的域中的所有值都满足该变量的单元约束。

例如,让我们考虑两门课程,A 和 B。每门课程的域是{星期一,星期二,星期三},约束是{A ≠ Mon,B ≠ Tue,B ≠ Mon,A ≠ B}。现在,A 和 B 都不一致,因为现有的约束阻止它们能够取它们域中的每一个值。然而,如果我们从 A 的域中删除星期一,那么它将具有节点一致性。为了在 B 中实现节点一致性,我们必须从它的域中删除星期一和星期二。

弧一致性

弧一致性是指一个变量的域中的所有值都满足该变量的二元约束(注意我们现在使用“弧”来指代我们之前所说的“边”)。换句话说,为了使 X 相对于 Y 弧一致,从 X 的域中删除元素,直到 X 的每个选择都有一个可能的 Y 的选择。

考虑我们之前的例子,其中领域已进行了修改:A:{周二, 周三} 和 B:{周三}。为了使 A 与 B 弧一致,无论 A 的考试(从其领域)安排在什么日子,B 仍然能够安排考试。A 是否与 B 弧一致?如果 A 取值为周二,那么 B 可以取值为周三。然而,如果 A 取值为周三,那么 B 就没有可以取的值(记住,约束条件之一是 A ≠ B)。因此,A 与 B 不弧一致。为了改变这种情况,我们可以从 A 的领域中删除周三。然后,A 取任何值(周二作为唯一选项)都会为 B 留下一个可以取的值(周三)。现在,A 与 B 弧一致。让我们看看一个伪代码算法,该算法使一个变量相对于另一个变量弧一致(注意,csp 代表“约束满足问题”)。

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:

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 相关的所有弧是否仍然一致。也就是说,我们取 X 的所有邻居(除了 Y),并将它们之间的弧添加到队列中。然而,如果 Revise 算法返回 false,意味着领域没有更改,我们只需继续考虑其他弧。

虽然弧一致性算法可以简化问题,但它不一定能解决问题,因为它只考虑二元约束,而不是多个节点可能如何相互连接。我们之前的例子,即每个学生选修 3 门课程,在运行 AC-3 后保持不变。

我们在第一节课中遇到了搜索问题。约束满足问题可以看作是一种搜索问题:

  • 初始状态:空赋值(所有变量都没有分配任何值)。

  • 操作:将一个 {variable = value} 添加到赋值中;即给某个变量赋值。

  • 转换模型:显示添加赋值如何改变赋值。这里没有太多深度:转换模型返回最新操作后的包含赋值的状态。

  • 目标测试:检查所有变量是否分配了值,以及所有约束是否得到满足。

  • 路径成本函数:所有路径都有相同的成本。正如我们之前提到的,与典型的搜索问题不同,优化问题关心的是解决方案,而不是通往解决方案的路线。

然而,将约束满足问题天真地当作常规搜索问题来处理,效率非常低。相反,我们可以利用约束满足问题的结构来更有效地解决问题。

回溯搜索

回溯搜索是一种考虑约束满足搜索问题结构的搜索算法。一般来说,它是一个递归函数,尝试在满足约束的情况下继续分配值。如果违反了约束,它将尝试不同的赋值。让我们看看它的伪代码:

函数 Backtrack(assignment, csp):

  • 如果 assignment 完成:

    • 返回 assignment
  • var = Select-Unassigned-Var(assignment, csp)

  • 对于 value 在 Domain-Values(var, assignment, csp) 中:

    • 如果 valueassignment 一致:

      • 将 {var = value} 添加到 assignment

      • result = Backtrack(assignment, csp)

      • 如果 resultfailure

        • 返回 result
      • assignmentremove {var = value}

  • 返回失败

用话来说,这个算法首先检查当前赋值是否完整。这意味着如果算法完成了,它将不会执行任何额外的操作。相反,它将直接返回完成的赋值。如果赋值不完整,算法将选择任何一个尚未赋值的变量。然后,算法尝试给这个变量赋一个值,并在得到的赋值上再次运行回溯算法(递归)。然后,它检查得到的结果。如果结果不是 失败,这意味着赋值成功,应该返回这个赋值。如果结果是 失败,那么将移除最新的赋值,并尝试新的可能值,重复相同的过程。如果域中所有可能的值都返回 失败,这意味着我们需要回溯。也就是说,问题出在某个前一个赋值上。如果这种情况发生在我们开始的变量上,那么这意味着没有解决方案满足约束。

考虑以下行动方案:

回溯示例

我们从空赋值(左上角)开始。然后,我们选择变量 A,并给它赋一个值,比如周一(右上角)。接着,使用这个赋值,我们再次运行算法。现在 A 已经有了赋值,算法将考虑 B,并将周一赋给它(左下角)。这个赋值返回 false,所以算法不会在给定的前一个赋值的基础上给 C 赋值,而是尝试给 B 赋一个新的值,周二(右下角)。这个新的赋值满足约束条件,因此将根据这个赋值考虑下一个变量。例如,如果给 B 赋周二或周三也会导致失败,那么算法将回溯并回到考虑 A,给它赋另一个值,周二。如果周二和周三都返回 失败,那么这意味着我们已经尝试了所有可能的赋值,问题是无解的。

在源代码部分,你可以找到一个从头开始实现的回溯算法。然而,这个算法被广泛使用,因此多个库已经包含了它的实现。

推理

Although backtracking search is more efficient than simple search, it still takes a lot of computational power. Enforcing arc consistency, on the other hand, is less resource intensive. By interleaving backtracking search with inference (enforcing arc consistency), we can get at a more efficient algorithm. This algorithm is called the **Maintaining Arc-Consistency** algorithm. This algorithm will enforce arc-consistency after every new assignment of the backtracking search. Specifically, after we make a new assignment to X, we will call the AC-3 algorithm and start it with a queue of all arcs (*Y,X*) where Y is a neighbor of X (and not a queue of all arcs in the problem). Following is a revised Backtrack algorithm that maintains arc-consistency, with the new additions in **bold**.

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*:

      • {*var = value*} 添加到 *assignment***

      • ***inferences* = Inference(*assignment, csp*)**

      • if *inferences* ≠ *failure*:

        • add *inferences* to *assignment*
      • *result* = Backtrack(*assignment, csp*)

      • if *result* ≠ *failure*:

        • return *result*
      • *remove* {var = value} **and *inferences*** from *assignment*

  • return failure

The Inference function runs the AC-3 algorithm as described. Its output is all the inferences that can be made through enforcing arc-consistency. Literally, these are the new assignments that can be deduced from the previous assignments and the structure of the constrain satisfaction problem.

There are additional ways to make the algorithm more efficient. So far, we selected an unassigned variable randomly. However, some choices are more likely to bring to a solution faster than others. This requires the use of heuristics. A heuristic is a rule of thumb, meaning that, more often than not, it will bring to a better result than following a naive approach, but it is not guaranteed to do so.

最小剩余值 (MRV) 一种 这样的启发式方法这里的想法是,如果变量的域被推理所限制,并且现在只剩下一个值(或者甚至两个值),那么通过做出这个赋值,我们将减少以后可能需要回溯的次数。 也就是说,由于它是由强制弧一致性推断出来的,我们迟早需要做出这个赋值。 如果这个赋值导致失败,那么最好尽快发现它,而不是稍后回溯。

剩余值最小

For example, after having narrowed down the domains of variables given the current assignment, using the MRV heuristic, we will choose variable C next and assign the value Wednesday to it.

度启发式依赖于变量的度数,其中度数是一个变量与其他变量连接的弧的数量。通过选择具有最高度的变量,通过一次分配,我们可以约束多个其他变量,从而加快算法的进程。

度启发式

例如,上述所有变量的定义域大小相同。因此,我们应该选择具有最高度的定义域,这将变量 E。

这两种启发式方法并不总是适用。例如,当多个变量的定义域中具有相同的最小值数时,或者当多个变量的度数相同时。

另一种提高算法效率的方法是在从变量的定义域中选择一个值时采用另一种启发式方法。在这里,我们希望使用最小约束值启发式,即选择将最少约束其他变量的值。这里的想法是,虽然在我们使用度启发式时,我们希望使用更有可能约束其他变量的变量,但在这里我们希望这个变量对其他变量的约束最少。也就是说,我们希望找到可能成为最大潜在问题来源(度最高的变量),然后将其变得尽可能不麻烦(给它分配最小约束值)。

最小约束值

例如,让我们考虑变量 C。如果我们将其分配给星期二,我们将对 B、E 和 F 的所有变量施加约束。然而,如果我们选择星期三,我们只会在 B 和 E 上施加约束。因此,选择星期三可能更好。

总结来说,优化问题可以用多种方式来表述。今天我们考虑了局部搜索、线性规划和约束满足。

第四讲

原文:cs50.harvard.edu/ai/notes/4/

机器学习

机器学习为计算机提供数据,而不是明确的指令。利用这些数据,计算机学会识别模式,并能够自主执行任务。

监督学习

监督学习是一个任务,其中计算机根据输入-输出对的训练集学习一个将输入映射到输出的函数。

监督学习下有多个任务,其中之一是分类。这是一个将输入映射到离散输出的函数的任务。例如,给定某一天湿度和大气的压力信息(输入),计算机决定那天是否会下雨(输出)。计算机在训练集上完成训练后,该训练集包含多天的湿度和大气的压力信息,并已映射到是否下雨。

这个任务可以形式化为以下内容。我们观察自然界,其中函数 f(湿度, 压力) 将输入映射到离散值,要么是雨,要么是无雨。这个函数对我们来说是隐藏的,它可能受到许多其他变量的影响,而我们无法获取这些变量。我们的目标是创建函数 h(湿度, 压力),它可以近似函数 f 的行为。这样的任务可以通过在湿度、降雨(输入)维度上绘制天数来可视化,如果那天下雨,则将每个数据点着色为蓝色,如果没有下雨,则着色为红色(输出)。白色数据点只有输入,计算机需要确定输出。

分类

最近邻分类

解决上述任务的一种方法是将相关变量分配给最近的观察点的值。例如,图上方的白色点应该着色为蓝色,因为最近的观察点也是蓝色。这可能在某些时候工作得很好,但考虑下面的图。

最近邻分类

按照同样的策略,白色点应该着色为红色,因为最近的观察点也是红色。然而,从更大的角度来看,它看起来周围的大多数其他观察点都是蓝色,这可能会给我们这样的直觉:在这种情况下,蓝色是一个更好的预测,尽管最近的观察点是红色。

一种克服最近邻分类局限性的方法是通过使用k-最近邻分类,其中点根据最近的 k 个邻居中最频繁的颜色着色。程序员需要决定 k 的值。例如,使用 3-最近邻分类,上面的白色点将被着色为蓝色,这直观上看起来是一个更好的决定。

k 最近邻分类的一个缺点是,使用原始方法,算法将不得不测量每个单独的点与问题点的距离,这在计算上很昂贵。这可以通过使用能够更快找到邻居的数据结构或通过剪枝无关观察结果来加速。

感知器学习

与最近邻策略相比,另一种处理分类问题的方式是将数据视为整体,并尝试创建一个决策边界。在二维数据中,我们可以在两种观察结果之间画一条线。每个额外的数据点都将根据其绘制在直线哪一侧进行分类。

决策边界

这种方法的缺点是数据很杂乱,很少能画一条线,将类别干净利落地分成两个观察结果而没有错误。通常,我们会妥协,画出的边界大多数情况下能正确地分隔观察结果,但偶尔还是会错误分类。

在这种情况下,输入为

  • x₁ = 湿度

  • x₂ = 压力

将被提供给一个假设函数 h(x₁, x₂),该函数将输出它对当天是否会下雨的预测。它将通过检查观察结果落在决策边界的哪一侧来完成。形式上,该函数将每个输入乘以一个常数的和,最终得到以下形式的线性方程:

  • Rain w₀ + w₁x₁ + w₂x₂ ≥ 0

  • 无雨否则

通常,输出变量将被编码为 1 和 0,其中如果方程结果大于 0,输出为 1(雨),否则为 0(无雨)。

权重和值由向量表示,这些是数字序列(在 Python 中可以存储在列表或元组中)。我们产生一个权重向量 w: (w₀, w₁, w₂),得到最佳权重向量是机器学习算法的目标。我们还产生一个输入向量 x: (1, x₁, x₂)。

我们计算两个向量的点积。也就是说,我们将一个向量中的每个值乘以第二个向量中相应的值,得到上面的表达式:w₀ + w₁x₁ + w₂x₂。输入向量中的第一个值是 1,因为当我们将其与权重向量 w₀ 相乘时,我们希望将其保持为常数。

因此,我们可以用以下方式表示我们的假设函数:

点积方程

由于算法的目标是找到最佳权重向量,当算法遇到新数据时,它会更新当前权重。它是通过使用 感知器学习规则 来做到这一点的:

感知器学习规则

从这条规则中重要的收获是,对于每个数据点,我们调整权重以使我们的函数更准确。细节,虽然对我们论点不是那么关键,是每个权重都被设置为等于它自己加上括号中的某个值。在这里,y 代表观察到的值,而假设函数代表估计。如果它们相同,这个整个项就等于零,因此权重不会改变。如果我们低估了(在观察到雨时称之为“无雨”),那么括号中的值将是 1,权重将增加由 xᵢ缩放的学习系数α的值。如果我们高估了(在观察到无雨时称之为“雨”),那么括号中的值将是-1,权重将减少由 x 缩放的学习系数α的值。α越高,每个新事件对权重的影响就越强。

这个过程的成果是一个阈值函数,一旦估计值超过某个阈值,就会从 0 切换到 1。

硬阈值

这种类型函数的问题在于它无法表达不确定性,因为它只能等于 0 或 1。它采用硬阈值。一种绕过这个问题的方法是使用对数函数,它采用软阈值。对数函数可以产生一个介于 0 和 1 之间的实数,这将表达对估计的信心。值越接近 1,下雨的可能性就越大。

软阈值

支持向量机

除了最近邻和线性回归之外,分类的另一种方法是支持向量机。这种方法使用决策边界附近的一个附加向量(支持向量)来在分离数据时做出最佳决策。考虑下面的例子。

支持向量机

所有的决策边界都在于它们在没有任何错误的情况下分离数据。然而,它们是否同样好?最左边的两个决策边界与一些观察值非常接近。这意味着一个只与一个组略有不同的新数据点可能会被错误地分类为另一组。相反,最右边的决策边界与每个组保持最大的距离,从而为它内部的变异提供了最大的灵活性。这种尽可能远离它所分离的两个组的边界,被称为最大间隔分离器

支持向量机的另一个好处是,它们可以表示超过两个维度的决策边界,以及非线性决策边界,如下所示。

圆形决策边界

总结来说,处理分类问题有多种方法,没有哪一种方法总是比其他方法更好。每种方法都有其缺点,可能在某些特定情况下比其他方法更有用。

回归

回归是一个监督学习任务,它将一个输入点映射到一个连续值,即某个实数。这与分类不同,因为分类问题将输入映射到离散值(例如,雨天或无雨)。

例如,一家公司可能会使用回归来回答广告支出如何预测销售收入的疑问。在这种情况下,一个观测函数 f(广告) 代表在广告上花费一些钱之后的观测收入(注意该函数可以接受多个输入变量)。这些是我们开始时的数据。有了这些数据,我们希望提出一个假设函数 h(广告),该函数将尝试近似函数 f 的行为。h 将生成一条线,其目标不是区分观察类型,而是根据输入预测输出值。

回归

损失函数

损失函数是量化上述任何决策规则所损失效用的一种方法。预测越不准确,损失就越大。

对于分类问题,我们可以使用 0-1 损失函数

  • L(实际,预测):

    • 0 如果实际值等于预测值

    • 1 否则

用话来说,这个函数在预测不正确时增加价值,而在预测正确时不增加价值(即当观测值和预测值匹配时)。

0-1 损失函数

在上面的例子中,值为 0 的天数是我们正确预测天气的日子(雨天在线下方,非雨天在上方线)。然而,线下方没有下雨而上方线下雨的日子是我们未能预测到的。我们给每个这样的日子赋予值为 1,并将它们加起来以得到一个经验估计,即我们的决策边界有多大的损失。

L₁ 和 L₂ 损失函数可以用于预测连续值。在这种情况下,我们感兴趣的是量化每个预测与观测值差异的程度。我们通过取观测值减去预测值的绝对值或平方值(即预测值与观测值之间的距离)来实现这一点。

  • L₁: L(实际,预测) = |实际 - 预测|

  • L₂: L(实际,预测) = (实际 - 预测)²

可以选择最适合自己目标的损失函数。L₂ 比起 L₁ 更严厉地惩罚异常值,因为它平方了差异。L₁ 可以通过将每个观测点到回归线上的预测点的距离求和来可视化:

L₁

过拟合

过拟合是指模型对训练数据拟合得如此之好,以至于它无法泛化到其他数据集。从这个意义上说,损失函数是一把双刃剑。在下面的两个例子中,损失函数被最小化,使得损失等于 0。然而,它不太可能很好地拟合新数据。

过拟合

例如,在左边的图中,屏幕底部红色点旁边的点很可能是雨(蓝色)。然而,在过拟合的模型中,它将被分类为无雨(红色)。

正则化

正则化是惩罚更复杂假设的过程,以有利于更简单、更一般的假设。我们使用正则化来避免过拟合。

在正则化中,我们通过将假设函数 h 的损失和其复杂度的度量相加来估计 h 的成本。

成本(h) = 损失(h) + λ复杂度(h)

Lambda (λ) 是一个常数,我们可以用它来调节我们在成本函数中对复杂性的惩罚强度。λ越高,复杂性的成本就越高。

测试我们是否过拟合了模型的一种方法是用保留交叉验证。在这个技术中,我们将所有数据分成两部分:一个训练集和一个测试集。我们在训练集上运行学习算法,然后看看它预测测试集中数据的准确性。这里的想法是通过在未用于训练的数据上测试,我们可以衡量学习泛化的程度。

保留交叉验证的缺点是我们无法在半数数据上训练模型,因为它被用于评估目的。解决这个问题的方法是用k-折交叉验证。在这个过程中,我们将数据分成 k 个集合。我们运行训练 k 次,每次留出一个数据集作为测试集。我们最终得到 k 个不同的模型评估,我们可以对这些评估进行平均,从而得到模型泛化的估计,而不会丢失任何数据。

scikit-learn

与 Python 一样,有许多库允许我们方便地使用机器学习算法。其中之一是 scikit-learn。

作为例子,我们将使用一个CSV数据集的假币。

纸币

四个左侧列是我们可以用来预测纸币是真还是假的数据,这是由人类提供的外部数据,编码为 0 和 1。现在我们可以在这个数据集上训练我们的模型,看看我们能否预测新纸币是否为真。

import csv
import random

from sklearn import svm
from sklearn.linear_model import Perceptron
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier

# model = KNeighborsClassifier(n_neighbors=1)
# model = svm.SVC() model = Perceptron() 

注意,在导入库之后,我们可以选择使用哪个模型。其余的代码将保持不变。SVC 代表支持向量分类器(我们称之为支持向量机)。KNeighborsClassifier 使用 k-邻居策略,并需要输入它应该考虑的邻居数量。

# Read data in from file with open("banknotes.csv") as f:
    reader = csv.reader(f)
    next(reader)

    data = []
    for row in reader:
        data.append({
            "evidence": [float(cell) for cell in row[:4]],
            "label": "Authentic" if row[4] == "0" else "Counterfeit"
        })

# Separate data into training and testing groups holdout = int(0.40 * len(data))
random.shuffle(data)
testing = data[:holdout]
training = data[holdout:]

# Train model on training set X_training = [row["evidence"] for row in training]
y_training = [row["label"] for row in training]
model.fit(X_training, y_training)

# Make predictions on the testing set X_testing = [row["evidence"] for row in testing]
y_testing = [row["label"] for row in testing]
predictions = model.predict(X_testing)

# Compute how well we performed correct = 0
incorrect = 0
total = 0
for actual, predicted in zip(y_testing, predictions):
    total += 1
    if actual == predicted:
        correct += 1
    else:
        incorrect += 1

# Print results print(f"Results for model {type(model).__name__}")
print(f"Correct: {correct}")
print(f"Incorrect: {incorrect}")
print(f"Accuracy: {100 * correct / total:.2f}%") 

此算法的手动版本可以在本讲座的源代码中的 banknotes0.py 文件中找到。由于算法经常以类似的方式使用,scikit-learn 包含了额外的函数,使代码更加简洁且易于使用,这个版本可以在 banknotes1.py 文件中找到。

状态集合 S

Q 学习

强化学习

强化学习可以被视为一个马尔可夫决策过程,具有以下特性:

马尔可夫决策过程演示

动作集合 Actions(S)

马尔可夫决策过程

  • 这种类型的算法可以用来训练行走机器人,例如,每一步都会返回一个正数(奖励)和每次跌倒都会返回一个负数(惩罚)。

  • 奖励函数 R(s, a, s’)

  • 转移模型 P(s’ | s, a)

  • Q 学习是强化学习的一种模型,其中函数 Q(s, a) 输出在状态 s 采取动作 a 的价值估计。

例如,考虑以下任务:

强化学习是机器学习的另一种方法,在每次动作之后,代理都会以奖励或惩罚(正或负的数值)的形式获得反馈。

代理是黄色圆圈,它需要到达绿色方块,同时避开红色方块。任务中的每一个方块都是一个状态。向上、向下或向侧面移动是一个动作。转移模型给出了执行动作后的新状态,奖励函数是代理获得的反馈类型。例如,如果代理选择向右移动,它将踩到红色方块并得到负面反馈。这意味着代理将学会,当处于左下角方块的状态时,应该避免向右移动。这样,代理将开始探索空间,学习哪些状态-动作对应该避免。该算法可以是概率性的,根据奖励的增加或减少,在不同状态下选择不同的动作。当代理到达绿色方块时,它将获得正面奖励,学习到在之前的状态采取的动作是有利的。

学习过程从环境向代理提供一个状态开始。然后,代理在状态上执行一个动作。基于这个动作,环境将返回一个状态和一个奖励给代理,其中奖励可以是正的,使行为在未来更有可能发生,或者负的(即惩罚),使行为在未来不太可能发生。

强化学习

模型开始时所有估计的值都等于 0(对于所有 s, aQ(s, a) = 0)。当采取一个动作并收到奖励时,函数做两件事:1)根据当前奖励和预期未来奖励估计 Q(s, a) 的值,2)更新 Q(s, a) 以考虑旧估计和新估计。这给我们提供了一个算法,它能够在不从头开始的情况下改进其过去的知识。

Q(s, a) ⟵ Q(s, a) + α(新值估计 - Q(s, a))

更新后的 Q(s, a) 的值等于 Q(s, a) 的先前值加上一些更新值。这个值被确定为新值与旧值之间的差异,乘以学习系数 α。当 α = 1 时,新估计简单地覆盖旧值。当 α = 0 时,估计值永远不会更新。通过提高和降低 α,我们可以确定旧知识通过新估计更新的速度。

新的价值估计可以表示为奖励(r)和未来奖励估计的总和。为了得到未来奖励估计,我们考虑在执行最后一个动作后得到的新状态,并加上在这个新状态下执行的动作的估计,该动作将带来最高的奖励。这样,我们不仅通过接收到的奖励来估计在状态 s 中执行动作 a 的效用,还通过下一步的预期效用来估计。未来奖励估计的值有时会与一个系数伽马(gamma)相关,该系数控制未来奖励的价值。最终我们得到以下方程:

Q 学习公式

贪婪决策算法完全忽略了未来估计的奖励,总是选择当前状态 s 中具有最高 Q(s, a) 的动作 a

这引出了探索与利用的权衡。贪婪算法总是利用,采取已经确立的行动以带来好的结果。然而,它总是遵循相同的路径到解决方案,永远不会找到更好的路径。另一方面,探索意味着算法可能在通往目标的过程中使用之前未探索的路线,从而允许它沿途发现更有效的解决方案。例如,如果你每次都听相同的歌曲,你知道你会喜欢它们,但你永远不会了解你可能更喜欢的新歌曲!

为了实现探索和利用的概念,我们可以使用ε(epsilon)贪婪算法。在这种类型的算法中,我们将 ε 设置为我们想要随机移动的频率。以 1-ε 的概率,算法选择最佳移动(利用)。以 ε 的概率,算法选择一个随机移动(探索)。

训练强化学习模型的另一种方法是,不是对每个移动给出反馈,而是在整个过程的结束时给出反馈。例如,考虑一个 Nim 游戏的例子。在这个游戏中,不同数量的物体分布在不同的堆中。每个玩家可以从任何单个堆中取走任意数量的物体,取走最后一个物体的玩家输。在这样的游戏中,未经训练的 AI 会随机地玩,很容易战胜它。为了训练 AI,它将从随机玩游戏开始,并在最后获得 1 分的奖励(胜利)和-1 分的奖励(失败)。例如,当它在 10,000 场比赛中训练后,它已经足够聪明,难以战胜。

当一个游戏有多个状态和可能的行为,例如象棋时,这种方法在计算上变得更加复杂。在所有可能的状态中为每个可能的移动生成一个估计值是不切实际的。在这种情况下,我们可以使用函数逼近,这允许我们使用各种其他特征来逼近Q(s, a),而不是为每个状态-动作对存储一个值。因此,算法能够识别出哪些移动足够相似,以至于它们的估计值也应该相似,并在其决策中使用这种启发式方法。

无监督学习

在我们之前看到的所有情况下,就像在监督学习中一样,我们都有算法可以从中学习的带标签的数据。例如,当我们训练一个算法来识别假币时,每张纸币都有四个不同值的变量(输入数据)以及它是否是假币(标签)。在无监督学习中,只有输入数据存在,AI 从这些数据中学习模式。

聚类

聚类是一种无监督学习任务,它将输入数据组织成组,使得相似的对象最终落在同一个组中。例如,在遗传学研究,当试图找到相似基因时,或者在图像分割中,根据像素之间的相似性定义图像的不同部分时,都可以使用这种方法。

k-means 聚类

k-means 聚类是一种执行聚类任务的算法。它将空间中的所有数据点映射出来,然后在空间中随机放置 k 个聚类中心(由程序员决定数量;这是我们在左侧看到的起始状态)。每个聚类中心只是空间中的一个点。然后,每个聚类被分配所有比其他中心更接近其中心的点(这是中间的图片)。然后,在迭代过程中,聚类中心移动到所有这些点的中间(右侧的状态),然后点再次重新分配到中心现在最近的聚类。当重复这个过程后,每个点仍然保持在它之前所在的同一个聚类中,我们就达到了平衡,算法结束,我们得到了在聚类之间划分的点。

k-means 聚类

第五讲

原文:cs50.harvard.edu/ai/notes/5/

神经网络

人工智能神经网络受到神经科学的启发。在大脑中,神经元是相互连接的细胞,形成网络。每个神经元都能够接收和发送电信号。一旦一个神经元接收到的电输入超过某个阈值,该神经元就会被激活,从而发送其电信号。

人工神经网络是一种受生物神经网络启发的学习数学模型。人工神经网络通过网络的结构和参数来模拟将输入映射到输出的数学函数。在人工神经网络中,网络的结构是通过在数据上训练来塑造的。

当在人工智能中实现时,每个神经元的并行单元是连接到其他单元的单元。例如,就像在上一次讲座中提到的,人工智能可能会将两个输入 x₁ 和 x₂ 映射到今天是否会下雨。在上一次讲座中,我们提出了以下假设函数的形式:h(x₁, x₂) = w₀ + w₁x₁ + w₂x₂,其中 w₁w₂ 是修改输入的权重,w₀ 是一个常数,也称为偏差,用于修改整个表达式的值。

激活函数

要使用假设函数来决定是否下雨,我们需要根据其产生的值创建某种类型的阈值。

实现这一点的其中一种方式是使用阶跃函数,它在达到某个阈值之前输出 0,在达到阈值之后输出 1。

阶跃函数

另一种方法是使用对数函数,它输出从 0 到 1 的任何实数,从而表达其判断的分级信心。

对数函数

另一种可能的函数是修正线性单元(ReLU),它允许输出为任何正数值。如果值为负,ReLU 将其设置为 0。

修正线性单元

无论我们选择使用哪个函数,我们在上一次讲座中学到的是,输入除了偏差外还会通过权重进行修改,这些修改的总和传递给激活函数。这对于简单的神经网络来说也是成立的。

神经网络结构

可以将神经网络视为上述想法的一种表示,其中函数将输入求和以产生输出。

神经网络结构

左侧的两个白色单元是输入单元,右侧的单元是输出单元。输入单元通过加权边连接到输出单元。为了做出决定,输出单元将输入乘以其权重(除了偏差 w₀)并使用函数 g 来确定输出。

例如,一个或逻辑连接可以表示为一个具有以下真值表的函数 f

x y f(x, y)
0 0 0
0 1 1
1 0 1
1 1 1

我们可以将这个函数可视化为一个神经网络。x₁ 是一个输入单元,x₂ 是另一个输入单元。它们通过一个权重为 1 的边连接到输出单元。输出单元然后使用函数 g(-1 + 1x₁ + 2x₂) 并以 0 为阈值来输出 0 或 1(假或真)。

或函数的神经网络

例如,在 x₁ = x₂ = 0 的情况下,总和是 (-1)。这低于阈值,所以函数 g 将输出 0。然而,如果 x₁x₂ 中的任何一个或两个等于 1,那么所有输入的总和将是 0 或 1。两者都在或高于阈值,所以函数将输出 1。

可以用类似的过程重复使用与函数(其中偏差将是(-2))。此外,输入和输出不必是不同的。可以使用类似的过程将湿度和气压作为输入,并输出降雨的概率。或者,在另一个例子中,输入可以是广告支出和支出的月份,以获得销售预期收入的输出。这可以通过将每个输入 x₁ … xₙ 乘以权重 w₁ … wₙ,求和得到的值,并添加偏差 w₀ 来扩展到任意数量的输入。

梯度下降

梯度下降是一种在训练神经网络时最小化损失的计算算法。正如之前提到的,神经网络能够从数据中推断出关于自身结构的知识。而到目前为止,我们定义了不同的权重,神经网络允许我们根据训练数据来计算这些权重。为此,我们使用梯度下降算法,其工作原理如下:

  • 从一个随机的权重选择开始。这是我们天真的起点,我们不知道应该给每个输入多少权重。

  • 重复:

    • 根据所有会导致损失减少的数据点计算梯度。最终,梯度是一个向量(一系列数字)。

    • 根据梯度更新权重。

这种算法的问题在于它需要根据 所有数据点 计算梯度,这在计算上代价高昂。有多种方法可以最小化这种成本。例如,在 随机梯度下降 中,梯度是基于随机选择的一个点计算的。这种梯度可能相当不准确,导致 小批量梯度下降 算法,它基于随机选择的几个点计算梯度,从而在计算成本和准确性之间找到一个折衷。正如通常情况下,没有哪种解决方案是完美的,不同的解决方案可能在不同的情境中被采用。

使用梯度下降,可以找到许多问题的答案。例如,我们可能想知道的不仅仅是“今天会下雨吗?”我们可以使用一些输入来生成不同天气类型的概率,然后只需选择最可能的天气。

天气神经网络

这可以用于任意数量的输入和输出,其中每个输入都连接到每个输出,并且输出代表我们可以做出的决策。请注意,在这种类型的神经网络中,输出之间没有连接。这意味着每个输出及其从所有输入关联的权重可以被视为一个独立的神经网络,因此可以单独从其他输出中训练。

到目前为止,我们的神经网络依赖于感知器输出单元。这些单元只能学习线性决策边界,使用直线来分离数据。也就是说,基于线性方程,感知器可以将输入分类为一种类型或另一种类型(例如,左图)。然而,数据往往不是线性可分的(例如,右图)。在这种情况下,我们转向多层神经网络来非线性地建模数据。

线性和非线性模型

多层神经网络

多层神经网络是一种具有输入层、输出层和至少一个隐藏层的人工神经网络。虽然我们提供输入和输出以训练模型,但我们人类不向隐藏层中的单元提供任何值。第一隐藏层中的每个单元从输入层中的每个单元接收加权值,对其进行一些操作并输出一个值。这些值被加权并进一步传播到下一层,重复此过程直到达到输出层。通过隐藏层,可以建模非线性数据。

多层神经网络

反向传播

反向传播是用于训练具有隐藏层的神经网络的主要算法。它通过从输出单元的误差开始,计算前一层权重的梯度下降,并重复此过程直到达到输入层来实现。在伪代码中,我们可以将算法描述如下:

  • 计算输出层的误差

  • 对于每一层,从输出层开始,向内移动到最早的隐藏层:

    • 将误差反向传播一层。换句话说,当前正在考虑的层将误差发送到前一层。

    • 更新权重。

这可以扩展到任意数量的隐藏层,创建深度神经网络,这些神经网络具有多个隐藏层。

深度神经网络

过度拟合

过拟合是指对训练数据建模过于紧密,因此无法推广到新数据的风险。对抗过拟合的一种方法是通过dropout。在这种技术中,我们在学习阶段随机选择并暂时移除一些单元。这样,我们试图防止网络对任何单个单元过度依赖。在整个训练过程中,神经网络将采取不同的形式,每次丢弃一些单元然后再使用它们:

Dropout

注意,训练完成后,整个神经网络将再次使用。

TensorFlow

就像在 Python 中经常发生的那样,多个库已经实现了使用反向传播算法的神经网络,TensorFlow 就是这样的库之一。您可以在这个 web 应用程序 中尝试 TensorFlow 神经网络,它允许您定义网络的不同属性并运行它,可视化输出。现在,我们将转向一个例子,说明我们如何使用 TensorFlow 来执行上次讲座中讨论的任务:区分假币和真币。

import csv
import tensorflow as tf
from sklearn.model_selection import train_test_split 

我们导入 TensorFlow 并将其命名为 tf(以缩短代码)。

# Read data in from file with open("banknotes.csv") as f:
    reader = csv.reader(f)
    next(reader)

    data = []
    for row in reader:
        data.append({
            "evidence": [float(cell) for cell in row[:4]],
            "label": 1 if row[4] == "0" else 0
        })

# Separate data into training and testing groups evidence = [row["evidence"] for row in data]
labels = [row["label"] for row in data]
X_training, X_testing, y_training, y_testing = train_test_split(
    evidence, labels, test_size=0.4
) 

我们将 CSV 数据提供给模型。我们的工作通常需要使数据符合库所需的格式。实际上编码模型的困难部分已经为我们实现了。

# Create a neural network model = tf.keras.models.Sequential() 

Keras 是一个 API,不同的机器学习算法可以通过它访问。一个顺序模型是指层依次排列(就像我们之前看到的那样)。

# Add a hidden layer with 8 units, with ReLU activation model.add(tf.keras.layers.Dense(8, input_shape=(4,), activation="relu")) 

密集层是指当前层中的每个节点都连接到前一层的所有节点。在生成我们的隐藏层时,我们创建了 8 个密集层,每个层有 4 个输入神经元,使用上面提到的 ReLU 激活函数。

# Add output layer with 1 unit, with sigmoid activation model.add(tf.keras.layers.Dense(1, activation="sigmoid")) 

在我们的输出层,我们希望创建一个使用 sigmoid 激活函数的密集层,这种激活函数的输出值介于 0 和 1 之间。

# Train neural network model.compile(
    optimizer="adam",
    loss="binary_crossentropy",
    metrics=["accuracy"]
)
model.fit(X_training, y_training, epochs=20)

# Evaluate how well model performs model.evaluate(X_testing, y_testing, verbose=2) 

最后,我们编译模型,指定哪个算法应该优化它,我们使用哪种类型的损失函数,以及我们如何衡量其成功(在我们的情况下,我们关注输出的准确性)。最后,我们使用 20 次重复(周期)将模型拟合到训练数据,然后在测试数据上评估它。

计算机视觉

计算机视觉包括分析和理解数字图像的不同计算方法,通常使用神经网络实现。例如,当社交媒体使用面部识别自动标记图片中的人时,就会用到计算机视觉。其他例子包括手写识别和自动驾驶汽车。

图像由像素组成,像素由三个范围从 0 到 255 的值表示,一个用于红色,一个用于绿色,一个用于蓝色。这些值通常用缩写 RGB 来表示。我们可以使用这一点来创建一个神经网络,其中每个像素中的颜色值都是一个输入,我们有一些隐藏层,输出是一些单位数,告诉我们图像中展示了什么。然而,这种方法有几个缺点。首先,通过将图像分解成像素及其颜色值,我们无法使用图像的结构作为辅助。也就是说,作为人类,如果我们看到脸部的一部分,我们知道应该期待看到脸的其余部分,这可以加快计算。我们希望能够在我们的神经网络中利用类似的优势。其次,输入的数量非常大,这意味着我们不得不计算很多权重。

图像卷积

图像卷积是将一个滤波器应用于图像的每个像素值,将其与邻居的像素值相加,并根据内核矩阵进行加权。这样做会改变图像,并有助于神经网络处理它。

让我们考虑以下示例:

图像卷积

内核是蓝色的矩阵,图像是左侧的大矩阵。生成的过滤图像是右下角的小矩阵。要使用内核过滤图像,我们从图像左上角的值为 20 的像素(坐标 1,1)开始。然后,我们将它周围的所有值乘以内核中的相应值并将它们相加(100 + 20(-1) + 300 + 10(-1) + 205 + 30(-1) + 200 + 30(-1) + 40*0),得到值 10。然后我们将对右侧的像素(30)、第一个像素下面的像素(30)以及这个像素右侧的像素(40)做同样的处理。这产生了一个具有我们在右下角看到的值的过滤图像。

不同的内核可以完成不同的任务。对于边缘检测,以下内核经常被使用:

边缘检测内核

这里的想法是,当像素与其所有邻居相似时,它们应该相互抵消,得到值为 0。因此,像素越相似,图像的部分就越暗,它们越不同,就越亮。将此内核应用于图像(左侧)会产生具有明显边缘的图像(右侧):

边缘检测

让我们考虑图像卷积的一个实现。我们使用的是 PIL 库(代表 Python Imaging Library),它可以为我们完成大部分繁重的工作。

import math
import sys

from PIL import Image, ImageFilter

# Ensure correct usage if len(sys.argv) != 2:
    sys.exit("Usage: python filter.py filename")

# Open image image = Image.open(sys.argv[1]).convert("RGB")

# Filter image according to edge detection kernel filtered = image.filter(ImageFilter.Kernel(
    size=(3, 3),
    kernel=[-1, -1, -1, -1, 8, -1, -1, -1, -1],
    scale=1
))

# Show resulting image filtered.show() 

尽管如此,由于作为神经网络输入的像素数量众多,处理图像在神经网络中是计算密集型的。另一种方法是池化,通过从输入区域中采样来减少输入的尺寸。相邻的像素属于图像中的同一区域,这意味着它们很可能是相似的。因此,我们可以用一个像素来代表整个区域。一种实现方式是最大池化,其中选定的像素是该区域内所有其他像素中值最高的一个。例如,如果我们把下面的左方形(下方)分成四个 2X2 的小方形,通过从这个输入进行最大池化,我们得到右边的那个小方形。

最大池化

卷积神经网络

卷积神经网络是一种使用卷积的神经网络,通常用于分析图像。它首先应用过滤器,使用不同的核来帮助提取图像的一些特征。这些过滤器可以通过调整它们的核来改进,就像神经网络中的其他权重一样,基于输出的错误进行调整。然后,得到的图像被池化,之后像素被作为输入(称为展平)馈送到传统的神经网络。

卷积神经网络

卷积和池化步骤可以重复多次,以提取额外的特征并减少输入到神经网络的尺寸。这些过程的一个好处是,通过卷积和池化,神经网络对变化的敏感性降低。也就是说,如果从略微不同的角度拍摄相同的图片,卷积神经网络的输入将相似,而如果没有卷积和池化,每张图片的输入将大相径庭。

在代码中,卷积神经网络与传统神经网络差别不大。TensorFlow 提供了测试我们模型的数据库。我们将使用 MNIST,它包含黑白手写数字的图片。我们将训练我们的卷积神经网络来识别数字。

import sys
import tensorflow as tf

# Use MNIST handwriting dataset mnist = tf.keras.datasets.mnist

# Prepare data for training (x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0
y_train = tf.keras.utils.to_categorical(y_train)
y_test = tf.keras.utils.to_categorical(y_test)
x_train = x_train.reshape(
    x_train.shape[0], x_train.shape[1], x_train.shape[2], 1
)
x_test = x_test.reshape(
    x_test.shape[0], x_test.shape[1], x_test.shape[2], 1
)

# Create a convolutional neural network model = tf.keras.models.Sequential([

    # Convolutional layer. Learn 32 filters using a 3x3 kernel
    tf.keras.layers.Conv2D(
        32, (3, 3), activation="relu", input_shape=(28, 28, 1)
    ),

    # Max-pooling layer, using 2x2 pool size
    tf.keras.layers.MaxPooling2D(pool_size=(2, 2)),

    # Flatten units
    tf.keras.layers.Flatten(),

    # Add a hidden layer with dropout
    tf.keras.layers.Dense(128, activation="relu"),
    tf.keras.layers.Dropout(0.5),

    # Add an output layer with output units for all 10 digits
    tf.keras.layers.Dense(10, activation="softmax")
])

# Train neural network model.compile(
    optimizer="adam",
    loss="categorical_crossentropy",
    metrics=["accuracy"]
)
model.fit(x_train, y_train, epochs=10)

# Evaluate neural network performance model.evaluate(x_test,  y_test, verbose=2) 

由于模型需要时间来训练,我们可以保存已经训练好的模型以供以后使用。

# Save model to file if len(sys.argv) == 2:
    filename = sys.argv[1]
    model.save(filename)
    print(f"Model saved to {filename}.") 

现在,如果我们运行一个接收手绘数字作为输入的程序,它将能够使用该模型对数字进行分类并输出结果。有关此类程序的实现,请参阅本讲座源代码中的 recognition.py。

循环神经网络

前馈神经网络是我们迄今为止讨论过的神经网络类型,其中输入数据被提供给网络,最终产生一些输出。下面可以看到前馈神经网络的工作原理图。

前馈神经网络图

与此相反,循环神经网络由一个非线性结构组成,其中网络使用其自身的输出作为输入。例如,微软的captionbot能够用句子中的词语描述图像的内容。这与分类不同,因为输出可以根据图像的特性具有不同的长度。虽然前馈神经网络无法改变输出的数量,但循环神经网络由于其结构,能够做到这一点。在字幕任务中,网络会处理输入以产生输出,然后从这个点继续处理,产生另一个输出,并重复必要的次数。

循环神经网络

循环神经网络在处理序列而不是单个对象的情况下非常有用。上面提到的神经网络需要生成一系列词语。然而,同样的原理也可以应用于分析视频文件,这些文件由一系列图像组成,或者在翻译任务中,处理一系列输入(源语言中的词语)以产生一系列输出(目标语言中的词语)。

第六讲

原文:cs50.harvard.edu/ai/notes/6/

这些笔记反映了 2023 年 8 月 14 日发布的第六讲的新版本。如果您观看了之前的版本,并希望查看其笔记,请点击此处。

语言

到目前为止,在课程中,我们需要塑造任务和数据,以便 AI 能够处理它们。今天,我们将探讨如何构建 AI 以处理人类语言。

自然语言处理涵盖了所有 AI 获取人类语言作为输入的任务。以下是一些此类任务的例子:

  • 自动摘要,其中 AI 被给出文本作为输入,并产生文本的摘要作为输出。

  • 信息提取,其中 AI 被给出文本语料库,并从中提取数据作为输出。

  • 语言识别,其中 AI 被给出文本并返回文本的语言作为输出。

  • 机器翻译,其中 AI 被给出原始语言的文本,并输出目标语言的翻译。

  • 命名实体识别,其中 AI 被给出文本,并从中提取文本中的实体名称(例如,公司名称)。

  • 语音识别,其中 AI 被给出语音,并产生相同的文本。

  • 文本分类,其中 AI 被给出文本,并需要将其分类为某种类型的文本。

  • 词义消歧,其中 AI 需要选择具有多个意义的单词的正确含义(例如,银行既指金融机构也指河流的河岸)。

语法和语义

语法是句子结构。作为某些人类语言的母语者,我们不会在产生语法正确的句子和标记非语法正确的句子为错误时感到困难。例如,句子“在九点之前,福尔摩斯敏捷地走进了房间”是语法正确的,而句子“在福尔摩斯九点之前敏捷地走进了房间”则是非语法正确的。语法可以同时是语法正确的和模糊的,例如,“我看到了拿着望远镜的男人。”我是看到了(拿着望远镜的男人)还是我看到了(男人),通过望远镜看到了?为了能够解析人类语言并产生它,AI 需要掌握语法。

语义是单词或句子的意义。虽然句子“在九点之前,福尔摩斯敏捷地走进了房间”在语法上与“福尔摩斯敏捷地走进了房间,就在九点之前”不同,但它们的内容实际上是相同的。同样,尽管句子“A few minutes before nine, Sherlock Holmes walked quickly into the room”使用了与前句不同的单词,但它仍然传达了非常相似的意义。此外,一个句子可以完全语法正确,但完全无意义,如乔姆斯基的例子,“无色的绿色想法疯狂地睡觉。”为了能够解析人类语言并产生它,AI 需要掌握语义。

上下文无关语法

形式语法是一种用于生成语言中句子的规则系统。在上下文无关语法中,文本从其意义中抽象出来,使用形式语法来表示句子的结构。让我们考虑以下示例句子:

  • 她看到了这个城市。

这是一个简单的语法句子,我们希望生成一个表示其结构的语法树。

我们首先为每个单词分配其词性。城市是名词,我们将它们标记为 N。看到是动词,我们将它标记为 V。这个是限定词,标记后面的名词是确定的还是不确定的,我们将它标记为 D。现在,上述句子可以重写为

  • N V D N

到目前为止,我们已经将每个单词从其语义意义抽象到其词性。然而,句子中的单词相互连接,要理解句子,我们必须了解它们是如何连接的。名词短语(NP)是一组与名词连接的单词。例如,单词是这个句子中的名词短语。此外,单词这个城市也形成一个名词短语,由一个限定词和一个名词组成。动词短语(VP)是一组与动词连接的单词。单词看到本身就是一个动词短语。然而,单词看到这个城市也构成一个动词短语。在这种情况下,它是一个由动词和名词短语组成的动词短语,而名词短语又由一个限定词和一个名词组成。最后,整个句子(S)可以表示如下:

句法树

使用形式语法,人工智能能够表示句子的结构。在我们描述的语法中,有足够的规则来表示上述简单句子。要表示更复杂的句子,我们不得不向我们的形式语法中添加更多规则。

nltk

在 Python 中,通常会有多个库被编写来实现上述想法。nltk(自然语言工具包)就是这样一个库。为了分析上述句子,我们将为语法提供算法规则:

import nltk

grammar = nltk.CFG.fromstring(""" S -> NP VP

    NP -> D N | N
    VP -> V | V NP

    D -> "the" | "a" N -> "she" | "city" | "car" V -> "saw" | "walked"  """)

parser = nltk.ChartParser(grammar) 

与我们上面所做的一样,我们定义了可能包含在其他中的可能组件。一个句子可以包含一个名词短语和一个动词短语,而短语本身可以由其他短语、名词、动词等组成,最终,每个词性在语言中跨越一些单词。

sentence = input("Sentence: ").split()
try:
    for tree in parser.parse(sentence):
        tree.pretty_print()
        tree.draw()
except ValueError:
    print("No parse tree possible.") 

在向算法提供一个输入句子并将其拆分为单词列表后,函数将打印出结果语法树(pretty_print)并生成图形表示(draw)。

句法树

n-grams

n-元组是从文本样本中提取的n个项目的序列。在字符n-元组中,项目是字符,而在单词n-元组中,项目是单词。单元组二元组三元组分别是一、两个和三个项目的序列。在以下句子中,前三个n-元组是“how often have”、“often have I”和“have I said”。

“我曾经说过多少次,当你排除了不可能的,无论多么不可能,剩下的就一定是真相?”

n-元组在文本处理中很有用。尽管 AI 之前不一定看到过整个句子,但它肯定看到过句子的一部分,比如“我曾经说过。”由于一些词比其他词更经常一起出现,因此也有可能用一定的概率预测下一个词。例如,你的智能手机根据你输入的最后几个词的概率分布来为你建议单词。因此,自然语言处理中的一个有用步骤是将句子分解成 n 元组。

分词

分词是将字符序列分割成片段(标记)的任务。标记可以是单词,也可以是句子,在这种情况下,该任务被称为单词分词句子分词。我们需要分词来查看n-元组,因为它们依赖于标记的序列。我们首先根据空格字符将文本分割成单词。虽然这是一个好的开始,但这种方法并不完美,因为我们最终会得到带有标点的单词,例如“remains”。因此,例如,我们可以移除标点。然而,然后我们会面临额外的挑战,例如带有撇号的单词(例如“o'clock”)和带有连字符的单词(例如“pearl-grey”)。此外,一些标点对于句子结构很重要,比如句号。然而,我们需要能够区分单词“Mr.”结尾的句号和句子结尾的句号。处理这些问题是分词的过程。最后,一旦我们有了标记,我们就可以开始查看n-元组。

马尔可夫模型

如前几节课所讨论的,马尔可夫模型由节点组成,每个节点的值基于有限数量的前一个节点具有概率分布。马尔可夫模型可以用来生成文本。为此,我们在文本上训练模型,然后根据前 n 个词为每个 n-gram 的每个n-th 标记建立概率。例如,使用三元组,在马尔可夫模型有两个词之后,它可以从基于前两个词的概率分布中选择第三个词。然后,它可以从基于第二个和第三个词的概率分布中选择第四个词。要查看使用 nltk 实现此类模型的示例,请参阅源代码中的 generator.py,其中我们的模型学习生成莎士比亚风格的句子。最终,使用马尔可夫模型,我们能够生成通常语法正确且表面上听起来与人类语言输出相似的文本。然而,这些句子缺乏实际的意义和目的。

词袋模型

词袋模型是一种将文本表示为无序单词集合的模型。该模型忽略了语法,只考虑句子中单词的意义。这种方法在某些分类任务中很有帮助,例如情感分析(另一个分类任务可能是区分常规电子邮件和垃圾邮件)。情感分析可以用于产品评论,将评论分类为正面或负面。考虑以下句子:

  1. “我的孙子很喜欢它!太有趣了!”

  2. “产品几天后就坏了。”

  3. “这是我很久以来玩过的最好的游戏之一。”

  4. “有点便宜且脆弱,不值得。”

仅基于每个句子中的单词,忽略语法,我们可以看到句子 1 和 3 是积极的(“loved”,“fun”,“best”),而句子 2 和 4 是消极的(“broke”,“cheap”,“flimsy”)。

简单贝叶斯

简单贝叶斯是一种可以与词袋模型一起用于情感分析的技术。在情感分析中,我们问“给定句子中的单词,句子是积极的/消极的概率是多少。”回答这个问题需要计算条件概率,回忆第二部分课中的贝叶斯定理会有所帮助:

贝叶斯定理

现在,我们想使用这个公式来找到 P(sentiment | text),例如,P(positive | “my grandson loved it”)。我们首先对输入进行标记化,这样我们最终得到 P(positive | “my”, “grandson”, “loved”, “it”)。直接应用贝叶斯定理,我们得到以下表达式:P(“my”, “grandson”, “loved”, “it” | positive)*P(positive)/P(“my”, “grandson”, “loved”, “it”)。这个复杂表达式将给我们 P(positive | “my”, “grandson”, “loved”, “it”)的精确答案。

然而,如果我们愿意得到一个不等于 P(positive | “my”, “grandson”, “loved”, “it”),但与其成比例的答案,我们就可以简化这个表达式。稍后,我们知道概率分布需要加起来等于 1,我们可以将得到的结果值归一化成一个确切的概率。这意味着我们可以将上面的表达式简化为仅包含分子:P(“my”, “grandson”, “loved”, “it” | positive)P(positive)。再次,我们可以根据已知条件概率a给定bab的联合概率成比例的知识来简化这个表达式。因此,我们得到以下概率表达式:P(positive, “my”, “grandson”, “loved”, “it”)P(positive)。然而,计算这个联合概率是复杂的,因为每个词的概率都是基于它前面词的概率。这需要我们计算 P(positive)P(“my” | positive)P(“grandson” | positive, “my”)P(loved | positive, “my”, “grandson”)P(“it” | positive, “my”, “grandson”, “loved”)。

正是在这里,我们天真地使用了贝叶斯定理:我们假设每个词的概率与其他词是独立的。这并不正确,但尽管这种不精确,朴素贝叶斯仍然能产生一个好的情感估计。使用这个假设,我们最终得到以下概率:P(positive)P(“my” | positive)P(“grandson” | positive)P(“loved” | positive)P(“it” | positive),这并不难计算。P(positive) = 所有正样本的数量除以总样本的数量。P(“loved” | positive)等于包含单词“loved”的正样本数量除以正样本的数量。让我们考虑以下例子,其中微笑和皱眉表情符号代替了单词“positive”和“negative”:

朴素贝叶斯

在右侧,我们看到一个表格,其中包含左侧每个词在句子中出现的条件概率,前提是句子是积极的或消极的。在左侧的小表格中,我们看到积极或消极句子的概率。在左下角,我们看到计算后的结果概率。在这个阶段,它们之间是成比例的,但它们在概率方面并没有告诉我们太多。为了得到概率,我们需要归一化这些值,得到 P(positive) = 0.6837 和 P(negative) = 0.3163。朴素贝叶斯的优势在于它对在一个类型的句子中比另一个类型句子中出现频率更高的词很敏感。在我们的例子中,单词“loved”在积极句子中出现的频率更高,这使得整个句子更有可能被判定为积极而不是消极。要查看使用 nltk 库实现的朴素贝叶斯情感评估的示例,请参考 sentiment.py。

我们可能会遇到的一个问题是,某些词可能永远不会出现在某种类型的句子中。假设我们样本中的所有积极句子都没有“孙子”这个词。那么,P(“孙子” | 积极) = 0,在计算句子为积极的概率时,我们会得到 0。然而,在现实中并非如此(提到孙子的句子并不都是消极的)。解决这个问题的方法之一是加性平滑,即在我们分布的每个值上添加一个值α来平滑数据。这样,即使某个值是 0,通过向其添加α,我们也不会将正句或负句的整个概率乘以 0。一种特定的加性平滑方法,拉普拉斯平滑,将 1 加到我们分布的每个值上,假装所有值都至少被观察过一次。

词表示

我们想在我们的 AI 中表示词义。正如我们之前看到的,以数字形式向 AI 提供输入是方便的。解决这个问题的方法之一是使用独热表示,其中每个词用一个向量表示,该向量包含与我们有相同数量的值。除了向量中的一个值等于 1 之外,所有其他值都等于 0。我们可以通过哪个值是 1 来区分单词,最终为每个单词得到一个唯一的向量。例如,句子“他写了一本书”可以表示为四个向量:

  • [1, 0, 0, 0] (他)

  • [0, 1, 0, 0] (已写)

  • [0, 0, 1, 0] (a)

  • [0, 0, 0, 1] (书)

然而,虽然这种表示在只有四个词的世界中是有效的,但如果我们想表示词典中的词,当我们有 50,000 个词时,我们最终会得到 50,000 个长度为 50,000 的向量。这是极其低效的。这种表示方式中的另一个问题是,我们无法表示像“wrote”和“authored”这样的词之间的相似性。因此,我们转向分布式表示的想法,其中意义分布在向量中的多个值上。在分布式表示中,每个向量有有限数量的值(远少于 50,000),其形式如下:

  • [-0.34, -0.08, 0.02, -0.18, …] (他)

  • [-0.27, 0.40, 0.00, -0.65, …] (写了)

  • [-0.12, -0.25, 0.29, -0.09, …] (a)

  • [-0.23, -0.16, -0.05, -0.57, …] (书中)

这使我们能够为每个词生成独特的值,同时使用较小的向量。此外,现在我们能够通过它们向量中值的差异来表示词之间的相似性。

“你将通过与你相伴的词来认识一个词”是 J. R. Firth,一位英国语言学家的一个想法。遵循这个想法,我们可以通过定义词的相邻词来定义词。例如,我们可以用有限的词来完成句子“for ___ he ate.” 这些词可能是像“breakfast”、“lunch”和“dinner”这样的词。这使我们得出结论,通过考虑某个词倾向于出现的环境,我们可以推断出该词的意义。

word2vec

word2vec 是一种生成单词分布式表示的算法。它通过 Skip-Gram 架构 来实现,这是一种针对给定目标词预测上下文的神经网络架构。在这个架构中,神经网络为每个目标词都有一个输入单元。一个较小的、单一的隐藏层(例如,50 或 100 个单元,尽管这个数字是灵活的)将生成代表单词分布式表示的值。隐藏层中的每个单元都与输入层中的每个单元相连。输出层将生成与目标词在相似上下文中可能出现的单词。类似于我们在上一节课中看到的,这个网络需要使用训练数据集并通过反向传播算法进行训练。

Skip-Gram 架构

这个神经网络证明非常强大。在处理过程的最后,每个单词最终都变成一个向量,或者一系列数字。例如,

书籍:[-0.226776 -0.155999 -0.048995 -0.569774 0.053220 0.124401 -0.091108 -0.606255 -0.114630 0.473384 0.061061 0.551323 -0.245151 -0.014248 -0.210003 0.316162 0.340426 0.232053 0.386477 -0.025104 -0.024492 0.342590 0.205586 -0.554390 -0.037832 -0.212766 -0.048781 -0.088652 0.042722 0.000270 0.356324 0.212374 -0.188433 0.196112 -0.223294 -0.014591 0.067874 -0.448922 -0.290960 -0.036474 -0.148416 0.448422 0.016454 0.071613 -0.078306 0.035400 0.330418 0.293890 0.202701 0.555509 0.447660 -0.361554 -0.266283 -0.134947 0.105315 0.131263 0.548085 -0.195238 0.062958 -0.011117 -0.226676 0.050336 -0.295650 -0.201271 0.014450 0.026845 0.403077 -0.221277 -0.236224 0.213415 -0.163396 -0.218948 -0.242459 -0.346984 0.282615 0.014165 -0.342011 0.370489 -0.372362 0.102479 0.547047 0.020831 -0.202521 -0.180814 0.035923 -0.296322 -0.062603 0.232734 0.191323 0.251916 0.150993 -0.024009 0.129037 -0.033097 0.029713 0.125488 -0.018356 -0.226277 0.437586 0.004913]

这些数字本身并没有什么意义。但是,通过找到语料库中与这些数字最相似的词汇,我们可以运行一个函数,生成与单词 book 最相似的词汇。在这个网络中,这些词汇将是:book, books, essay, memoir, essays, novella, anthology, blurb, autobiography, audiobook。这对于计算机来说已经很不错了!通过一些本身没有特定意义的数字,人工智能能够生成与 book 在意义而非字母或声音上非常相似的词汇!我们还可以根据词汇向量之间的差异来计算词汇之间的差异。例如,kingman 之间的差异类似于 queenwoman 之间的差异。也就是说,如果我们把 kingman 之间的差异加到 woman 的向量上,与结果向量最接近的词汇是 queen!同样地,如果我们把 ramenjapan 之间的差异加到 america 上,我们得到 burritos。通过使用神经网络和词汇的分布式表示,我们使我们的 AI 能够理解语言中词汇之间的语义相似性,使我们更接近能够理解和生成人类语言的 AI。

神经网络

回想一下,神经网络接受一些输入,将其传递到网络中,并创建一些输出。通过向网络提供训练数据,它可以越来越准确地翻译输入为输出。通常,机器翻译使用神经网络。在实践中,当我们翻译词汇时,我们希望翻译一个句子或段落。由于句子是固定大小的,我们遇到了将一个序列翻译为另一个序列的问题,其中大小不是固定的。如果你曾经与一个 AI 聊天机器人交谈过,它需要理解一个词汇序列并生成一个适当的输出序列。

循环神经网络可以多次运行神经网络,同时跟踪一个包含所有相关信息的状态。输入被输入到网络中,创建一个隐藏状态。将第二个输入传递到编码器,同时带有第一个隐藏状态,产生一个新的隐藏状态。这个过程会重复进行,直到传递一个结束标记。然后,开始解码状态,创建一个隐藏状态接着一个隐藏状态,直到我们得到最终的词汇和另一个结束标记。然而,一些问题也随之而来。编码阶段的一个问题是,所有来自输入阶段的信息必须存储在一个最终状态中。对于长序列,将所有这些信息存储到一个单一的状态值中是非常具有挑战性的。如果能以某种方式组合所有隐藏状态将是有用的。另一个问题是,输入序列中的某些隐藏状态比其他状态更重要。是否有可能知道哪些状态(或词汇)比其他状态更重要?

注意

注意力指的是神经网络决定哪些值比其他值更重要。在句子“马萨诸塞州的首府是什么?”中,注意力使神经网络能够决定在生成输出句子的每个阶段它将关注哪些值。进行这样的计算,神经网络将显示,在生成答案的最后一个词“capital”和“Massachusetts”是最需要关注的。通过取注意力分数,将它们乘以网络生成的隐藏状态值,并将它们相加,神经网络将创建一个解码器可以用来计算最后一个词的最终上下文向量。在这些计算中出现的挑战是,循环神经网络需要逐词顺序训练。这需要花费大量时间。随着大型语言模型的增长,它们的训练时间越来越长。随着需要训练的更大数据集的出现,对并行化的需求稳步增长。因此,引入了一种新的架构。

转换器

Transformers是一种新的训练架构,其中每个输入词同时通过神经网络。一个输入词进入神经网络,并被捕获为一个编码表示。由于所有单词同时输入神经网络,单词顺序很容易丢失。因此,位置编码被添加到输入中。因此,神经网络将使用单词及其在编码表示中的位置。此外,添加了一个自注意力步骤来帮助定义输入单词的上下文。实际上,神经网络通常会使用多个自注意力步骤,以便它们可以进一步理解上下文。这个过程对序列中的每个单词重复多次。结果是编码表示,在解码信息时将非常有用。

在解码步骤中,前一个输出词及其位置编码被提供给多个自注意力步骤和神经网络。此外,多个注意力步骤被输入编码过程中的编码表示,并提供给神经网络。因此,单词能够相互关注。进一步来说,并行处理成为可能,计算既快又准确。

总结

我们在多种情境下探讨了人工智能。我们研究了人工智能如何寻找解决方案的搜索问题。我们探讨了人工智能如何表示知识和创造知识。我们研究了当它不确定某些事情时的情况。我们研究了优化、最大化函数和最小化函数。我们研究了通过观察训练数据来寻找模式的机器学习。我们学习了神经网络以及它们如何使用权重从输入到输出。今天,我们探讨了语言本身以及我们如何让计算机理解我们的语言。我们只是刚刚触及了这个过程的表面。我们真心希望您喜欢与我们一同经历的这段旅程。这是《使用 Python 的人工智能入门》。

posted @ 2025-11-08 11:25  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报