【无标题】

人工智能的搜索算法

正文
如果我们要解决一个问题,首先我们要先定义一个问题。

一、问题的形式化
要知道,智能体是处在环境之中的。智能体的行为会导致环境的状态发生改变。

为了解决问题,在不同的状态之下会有不同的正确行为。只要我们能在所有状态之下找到正确的行为,再将其放入智能体的行为序列中,智能体就可以解决问题。

举个例子,我们教小孩子吃苹果。现在我们把小孩子看成一个智能体,这个智能体有两种行为:咬一口、丢掉苹果。我们的目标是:让小孩子吃完苹果。那么这个问题中,苹果的初始状态是:完好,没有被咬。小孩的脑子里会分析苹果的状态,并得出正确的行为是:咬一口。进行完此行为之后,苹果的状态改变,变成:被咬一口的苹果。小孩的行为导致了新的状态,再次分析苹果的状态之后,小孩又做出咬一口的行为。这个行为又会导致新的状态,产生新的行为。直到最后,苹果的状态变成了:苹果核。小孩子分析之后,便产生了行为:丢掉苹果。

我们会说,上列情况的智能体是一个理性智能体。这是一个简单的模型。

但是在更复杂的问题中,不同状态下的智能体可能会有更加丰富的行为选择。那么,如何在众多行为中找到正确的行为,我们就需要用到搜索算法。

那么,我们该如何定义一个搜索问题?用“定义问题”这样的方式来表述,可能比较抽象。换而言之的话,就是要开始运用算法解决一个问题,我们需要哪些准备?

1、初始状态。
至少我们得知道智能体现在处于什么样的初始状态。
2、行动。
我们需要知道在状态之下,我们可以采取的行动列表。( 为了用词专业化,“行动列表”之后都用“行为序列”代替。 )
3、转移模型
我们需要知道在任意状态之下执行任意行为,会达到怎样的状态。
这三点能让我们把握住问题的整个“状态空间”。也就是说:初始状态所能够达到的所有状态的集合。我们可以用树或者图来表示状态空间。

如图,这里我们用树来作为例子。(不用树,用图来讲的话,原理也是类似的)

每个结点都表示一个状态,结点 A 便表示问题的初始状态。每一个箭头都表示一个行为,初始状态 A 随着不同的行为会变成不同的结点( B、C、D ),也就是不同的状态。假设我们所需要解决的问题,就是让智能体所处的环境达到 M 状态,那么智能体所需要做的,就是搜索出从 A 到达 M 的正确路径即可。路径便代表解决问题的方法。

除了上述三条概念之外,我们还需要另外两点才可以完成问题的定义。

4、目标测试。
我们需要某种方式来辨别:智能体当前所处的状态是否就是目标状态。
5、路径耗散
要知道,在某些情况下,我们希望采取最不消耗资源的解决方式。为此,我们要找出每种方法会消耗多少资源。而在状态空间里,路径便代表解决问题的方法。所以路径耗散就是指路径的资源耗散值。当然,专业的说法应该是—— 边加权 。
该有的前置知识我们都已经有了,现在我们来正式开始学习搜索算法。

二、无信息搜索
无信息搜索指的是:除了上面所提到的五条信息之外,没有其他任何信息来源的搜索算法。

计算机界认为不要重复造轮子,所以我贴了一些文章的链接来代替我的讲解。链接放在了小标题之后。必要的内容我会进行补充。

1、广度优先搜索:链接

广度优先搜索具有非常好的性能,但是有很致命的缺点——时间复杂度和空间复杂度太高。

2、一致代价搜索:链接

广度优先搜索加上最优化的思想,就是一致代价搜索。当所有路径的耗散等价时,广度优先搜索优于一致代价搜索。

3、深度优先搜索:链接

对于图和树,深度优先搜索在图上的效率更高。它在时间复杂度上相较于 BFS 没有优势,在空间复杂度上优于 BFS。

这里补充一点细节。因为当实际情况的深度无限大时,程序可能会陷入死角。为了防止死循环,我们可以在程序里加入一条深度界限,当搜索的深度达到这条界线时,直接跳出搜索行为。这样虽然解决了死角问题,但是当目标状态的深度深于深度界限时,我们可能会得到一个无解的结果。为此,我们可以引入一种更加灵活的思路——迭代加深的深度搜索。

具体而言:

for (int depth = 0; ;depth++) {//depth 表示深度界限
re = DFS()//用变量 re 来存储深度优先算法的返回值,函数中的参数没有写出
if (re != 0) break;
}
当该次的深度搜索没有找到目标结点时,便加深深度界限,继续运行深度搜索函数。

不过这样的代价实在是太大了。

4、双向搜索

我们可以从初始状态和目标状态两边开始,同时进行搜索算法。若两条搜索路径最终会触碰到一起,我们就认为我们找到了一条解决问题的路径。

三、启发式搜索
我们现在所看到的搜索方法是一种比无信息搜索更加优越的方法。

我将用最简单易懂的方式,讨论一下几种基础的启发式搜索方法。关于更加高阶的启发式搜索,我会单独开一篇文章来讨论。

所谓的启发式搜索,就是指我们在搜索的过程中,总能得到一些启发式的信息。这些信息可以启示我们下一次搜索该往哪个方向进行。

很抽象,是吧?别急,我们首先来定义一个启发函数: 。这个函数中, 表示树中的结点,函数 表示当前结点到达目标结点的“估计”代价。注意是“估计”代价。也就是说,这里所指的代价,是我们估计到达目标节点所需要的代价。

比如说,我们先来讨论一下这个算法。

1、贪婪最佳优先搜索

我打算自己写,感不感动?咳咳,开个玩笑。

假设现在我们现在需要从 结点出发,通过搜索算法找到去 结点的路径。我们暂时假设每个行为的代价都相等。如图:

那么运用贪婪最佳优先搜索,我们会首先计算出


的值,找到其中的最小值。因为我们的目标是到达 结点,所以 的值肯定是最小的( 别忘了我们刚刚才说过, 表示到达目标结点的估计代价)。那么我们的智能体就会执行相应行为,现有的环境状态发生改变。接下来,我们会继续计算




。找出其中的最小值,毫无疑问,这里的最小值肯定是 。那么,我们最终就找到了一条解决问题的路径。

那么,什么是贪婪最佳优先搜索?它和贪心算法一样,试图始终让自己走上最小代价的搜索路径。只不过,这种思想和贪心算法一样,也容易出现各种误差。

现在,我们已经讲完一种算法,是不是很轻松?一部分读者读到这,可能会对 的具体表现形式产生疑惑。那么在这里我举一个 定义在问题中的例子。我们依旧用这幅图:

我们把




按照如图的位置放入坐标系中。至于坐标系怎么放置,坐标原点在哪?那些并不影响我们现在的讨论内容。

如果我们延用之前的问题,找到一条从 通往 的路径,那么此时的 可以具有怎样的表示形式呢?比如说,我们可以用 表示为结点 到达 的直线段长度,即 的值( 其中的



分别表示两点的坐标)。

是不是挺简单的?不同的问题,启示函数会有不同的构造方式。这里的构造方式只是该问题的许多启示函数中的一种。

我们现在来看看另一种著名的启发式搜索。

2、A* 搜索

我们现在定义一个评价函数 ,其中 就是我们在上面所定义的启发式函数, 则表示从起点到达目前结点所需要付出的代价。那么两者的结合 就被我们称之为代价函数。

带着新鲜的热乎乎的代价函数,我们来看看这种新的搜索方式。(嗯?我怎么又自己写了?)

我们还是用之前那张图( 主要是我懒得找新图 ):

我们还是延用之前那个问题( 主要是我懒得想新问题 ):

首先,我们待在结点 。我们会计算


,找出其中的最小值。假设最小值是 ,那么我们的智能体就会执行行为,使得环境到达 状态。然后继续计算




,找到最小值,进行行为

嗯?这不是和贪婪最佳优先搜索一样吗?

哈哈哈,还真是一样的。只不过这里我们用的是代价函数,所以我们的搜索可以保证最终的路径具有更小的代价。

虽然我讲得很简单,但是这个算法其实具有更多更加复杂的内容,足够让我水十几篇文章了( 诶嘿嘿 )。要是有兴趣的话,就看看我写的这篇文章吧:

笔记链接:“启发式”。
如果你在阅读本文章之前,对人工智能的搜索算法并不太了解,那么你读到这时,应该已经有点累了。我希望你能打起精神,因为我们还有很多搜索算法的讲解,才刚刚打算开始。

四、局部搜索
我们之前所讨论的算法,都是在整个状态空间已知的前提下才可以进行的。如果我们对全局状态不甚了解呢?

这个时候,我们可以采取局部算法来尝试解决问题。顾名思义,局部算法只需要局部的信息,就可以执行搜索操作。

1、爬山法

这其实是一个很简单的算法,但是为了让读者能够看到它的本质,我会讲得稍微详细点。

重阳节的时候,不少人会选择去爬山。爬山,其实是一种很简单的事情。只要你有足够的体力,那么你所需要做的,就是顺着大路,不断往更高处爬就是了。当我们爬到山的顶点时,我们可能会被眼前壮阔的风景所吸引,但是人的眼界始终是有限的,我们自以为站在了所有山峰的最高点,然而真正的最高点,可能在远方层层雾气里。

上面我所讲到的例子,就是一个典型的爬山算法,也是这个算法名字的由来。当我们站在某个位置时,我们会判断哪个方向通往更高处,然后我们会下意识地往能够通向最高处的路线前进。毕竟我们的目标是通往山顶。

我们现在将山上的位置划分成一个个结点。我们规定,当结点的海拔越高时,结点的估价函数值就会越高。现在,我们来尝试用另一种说明方式来阐述上面的例子。当我们站在某个结点时,我们会用估价函数判断相邻的每个结点的估价函数值,然后我们会下意识地往能够通向最高地估价函数值的路线前进。毕竟我们的目标是通往估价函数值最高的节点处。

好了,我们再来看看一般形式下的爬山算法。你猜到我要用哪张图了吗?

假设我们要从结点 到结点 。那么站在初始结点,我们判断我所有相邻结点的估价函数值,然后将值最高的结点当作我们下一个结点,并让智能体进行相应行为。到达结点后,我们继续判断我所有相邻结点的估价函数值,然后选择好下一个结点,并让智能体进行行为。接下来,智能体会不断重复这一步骤,知道找到这样一个结点:它的估价函数值,比它的所有相邻结点都要高。那么智能体就会将它当作问题的解。

按照这样的步骤的话,就会产生一些问题。比如说,如果智能体所找到的结点只是一个极值,而不是最值,那该怎么办呢?如果智能体爬山的时候,到达了一个高原,然后因为高原是等高的,智能体迷了路怎么办?如果智能体恰好爬到了山峰顶点的左边,结果因为步子设置得过大,稍微迈一步就会买到右边,然后又迈到左边,如此死循环怎么办?

这些问题,我们都会有一些相应的对策。但是我想之后再讨论这些细节,我们先看揭露爬山算法的本质——这就是一种贪心算法。如果你真正理解了贪心算法,应该可以直接理解这句话是什么含义。我相信读者中大部分人应该都可以理解,所以我不再多做解释。没看懂的读者,稍微想一下,也是可以想明白的。爬山算法只是一种特殊的贪心算法而已。我们现在再来讨论一下之前所提到的问题。

如果搜索到的只是“极值”该怎么办?要知道,因为我们对状态空间的全貌是不了解的,所以我们也不能保证它一定就不是最值。那么怎么办呢?你可以在开始搜索之前,先根据某些先验知识,估计一下目标节点的估价函数值。然后在算法的开头加上一句:如果最终搜索到的估价函数值小于我们所估计的值,则随机生成另一个结点,重新开始搜索。这样就可以在一定程度上解决问题。这种方法也被称为:“随机重启爬山法”。

如果遇到了“高原”该怎么办?你只要设置一个“横向移动”程序就可以了。当该节点与相邻结点的值相等时,暂时停止搜索行为,随机找一个相邻结点作为自己的下一个结点,直到相邻结点中出现比自己更高值的结点。这样的话,理论上只要你有足够的时间,你总能拜托高原。不是现实往往是我们并没有足够多的时间,万一程序运行几个月相信也不是不可能( 哈哈 ),所以一般我们都会给“横向移动”设立一个界限。当横向移动的次数超过一定界限,将停止横向移动,若依旧没有拜托高原,那么只能搜索失败。

如果遇到了“在山峰左右不断循环”该怎么办?还能怎么办,重新建模呗。事实上,这儿确实记录了其他方法。比如,你可以随机生成另一个结点,然后重新进行搜索。毕竟其他途径逼近极值点的话,可能不会出现这样的问题。但是这种方法可能还不如去重新建模来得实在点。

爬山算法有很多变式,但是内核都是一样的。理解了我上面所写的内容,学什么样的变式都会很快的。要是觉得这个算法很有意思,想要继续学习的话。你可以去学一下“禁忌搜索”和“STAGE”算法。

我们现在来讨论更多的局部搜索算法。

2、模拟退火算法:模拟退火算法原理与实例 - 小侯飞氘的文章 - 知乎

我想了一下,笔者的精力和时间毕竟有限,为了更新更多的内容,该贴链接的还是要用贴链接来解决。实在找不到的,我会自己进行讲解。适当的情况下,我再进行补充。

若是感觉没有看太懂,这里再添加一个例子。

依旧用之前的爬山例子做模型。现在我们假设一个小孩子也想要到达山顶,但是这个小孩子不喜欢一般的走路方式,他喜欢跳着走。他的想法也很简单,那就是哪个方向比较高,他就往哪个方向跳。我们再加强一下假设,假设这个小孩不是一个普通的小孩,他是一个超人小孩,他随便一跳可能就是十万八千米。因为这个小孩实在心太大了,所以在他跳着爬山的过程中,经常可能会跳过头。不过没关系,因为小孩子的体力是有限的,所以这个超人小孩所跳的距离会越来越短。虽然他经常会跳过头,但是每次他都会盲目地往最高的方向上跳,所以只要给他足够的时间,他就会成为一个典型的模拟退火算法。

3、遗传算法:如何通俗易懂地解释遗传算法? - 尼布甲尼撒的回答 - 知乎

这篇讲得挺好的。

关于局部搜索,我们还可以讨论更多的东西。目前我们所讨论的搜索方法都是假定在智能体可以从环境中收集所有信息的情况,然后事实上,智能体在现实环境中只能收集到部分信息。这就导致了不确定性,这意味着环境的变化可能不会如实按照状态空间所记录的那样。这个时候,我们就需要引入“与或树”的概念。但是这里不打算讨论过多的内容。有兴趣的读者可以自行进行探索。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值