(五)从零开始学人工智能-搜索:通过搜索解决问题

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/weixin_45552760/article/details/102623821


前面几个章节给大家介绍了人工智能数学基础,从本章开始,我会陆续给大家讲一些人工智能中的具体技术。我们首先从搜索系列课程开始。

1. 生活中的搜索问题

生活中我们经常会遇到很多搜索相关的问题,你能想到那些?

A. 想要开车到杭州东站,怎么找出最快的路线?
在这里插入图片描述
B. 经典的华容道游戏,如何用最少的步骤让曹操移动到棋盘的最下部?
在这里插入图片描述

2.搜索算法

​搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。现阶段一般有枚举算法、深度优先搜索(DFS)、广度优先搜索(BFS)、A*算法、回溯算法、蒙特卡洛树搜索、散列函数等算法。

​通过搜索我们可以解决许多在生活中、科研和工程领域有趣的问题,搜索算法可以被分为以下两种

  • 盲目的搜索算法,或称无信息图搜索算法
    • 除了问题的定义之外,没有给出其他信息。虽然可以解决问题,但不一定是最优解。
      • 广度优先搜索
      • 深度优先搜索
  • 启发式搜索算法,又称为有信息的图搜索算法
    • 给出了一些指引,根据提示找到最佳方案(如贪婪最佳优先算法A*寻路算法)

2.1 搜索问题和解决方案的定义

2.1.1 罗马尼亚度假问题

搜索问题通常可以用图来表示,以经典的路径规划问题为例子(实际生活中地图的路径规划问题要复杂的多)

问题: 假如你现在在罗马尼亚的Arad市度假,明天要飞往布加勒斯特(Bucharest),如何找出一条通往布加勒斯特的最短路径。
在这里插入图片描述

初始状态: 开始的初始状态,例如我们在罗马尼亚的初始状态是 In(Arad)

动作: 描述可能采取的操作。给定一个状态s,Actions(s)返回在s中可以执行的一组操作,例如在状态 In(Arad)中,可以采取的操作是到三个城市 {Go(Sibiu)、Go(Timisoara)、Go(Zerind)}

转换模型: 对每个动作的描述, 它由一个函数Result(s,a)指定,该模型返回在状态中执行动作a后所产生的状态。RESULT(In(Arad),Go(Zerind)) = In(Zerind)

扫描二维码关注公众号,回复: 7636629 查看本文章

图(路径): 初始状态、动作和转换模型隐式定义问题的状态空间

由任意一系列动作从初始状态可到达的所有状态集。状态空间形成有向网络或图,其中节点是状态,节点之间的链接是动作。

目标测试: 测试给定状态是否为目标状态。在这个问题中,我们的目标是 set{In(Bucharest)}。

路径成本: 到达目的地的一条路径的成本可能是以公里为单位的数值。

2.1.2 n皇后问题

现在我们再用上面提到的搜索问题的定义来解释一下经典的n皇后问题。

在国际象棋中,王后是可以攻击同一行、列或对角线中的任何部分的,八皇后问题的目标是把八位皇后放在棋盘(8X8)上,但不会有王后能够攻击任何其他人。
在这里插入图片描述

对于这个问题,我们可以作出下面的定义:

状态: 在棋盘上任意一种从0到8个王后的摆放方法。

初始状态: 在棋盘上没有一个王后。

动作: 在棋盘上添加一个王后

转换模型: 返回已经将王后安放到指定区域的棋盘状态

目标测试: 8个王后都在棋盘上,而且不能相互攻击

2.2 搜索过程

在搜索过程中,从初始状态开始实际上是建立了一棵搜索树,在搜索过程中树的分支对应于已探测的路径。
在这里插入图片描述
搜索树中的分支,实际上对应的是图中的一个分支
在这里插入图片描述

2.3 通用搜索算法

我们已经知道搜索的过程是建立一棵搜索树的过程,那么针对搜索问题,我们可以抽象一个通用的搜索算法

General-search (problem, strategy)
	init() //根据问题的初始状态初始化搜索树	
	loop
		if 没有可进一步可探测的候选节点 
            return failure
		根据算法选择下一个节点来探测
		if 这个节点满足目标条件 
            return 最终方案
		展开节点并将其所有后继节点添加到搜索树  
	end loop

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

不同搜索方法在探索空间的方式上有所不同,也就是他们如何选择下一步要扩展的节点!

下面我们介绍一下主要的搜索遍历方式:

2.4 盲目的搜索算法

2.4.1 深度优先遍历(Deep First Search)

顾名思义,这种遍历方法是以深度为优先对图进行搜索或者遍历,我们可以先看下DFS的基本步骤:

从当前节点开始,先标记当前节点,再寻找与当前节点相邻,且未标记过的节点:

(1)当前节点不存在下一个节点,则返回前一个节点进行DFS

(2)当前节点存在下一个节点,则从下一个节点进行DFS

​ 我们用图的方式来演示一下DFS的过程。

​ 一开始,可以看出,若没有走到最终的叶子节点,这种遍历方式会从start节点沿着一条路一直深入遍历下去(start -> 1 -> 2 -> 3)。
在这里插入图片描述

若走到叶子节点,便会退回上一节点,遍历上一节点的其他相邻节点(2 ->3-> 4)。
在这里插入图片描述

这样一直重复,直到找到最终目标节点。
在这里插入图片描述

​ 如你所见到的一样,这样的搜索方法像一根贪婪的蚯蚓,喜欢往深的地方钻,所以就自然而然的叫做深度优先算法了。

​ 我们可以顺便写出DFS的伪代码:

    DFS_find(节点){ 
        if(此结点已经遍历 || 此节点在图外 || 节点不满足要求) return; 
        if(找到了end节点) 输出结果 ; return; 
        标记此节点,表示已经遍历过了; 
        while(存在下一个相邻节点) find(下一个节点); 
    }

思考:

​由于一个有解的问题树可能含有无穷分枝,深度优先搜索如果误入无穷分枝(即深度无限),则不可能找到目标节点。为了避免这种情况的出现,在实施这一方法时,需要定出一个深度界限,在搜索达到这一深度界限而且尚未找到目标时,即返回重找,所以深度优先搜索算法是不完备的。另外,应用此算法得到的解不一定是最佳解(最短路径)。

2.4.2 广度优先遍历(BFS)

​对于深度优先算法,强迫症就很不爽了,并表示:“为什么不干干净净,一层一层地从start节点搜索下去呢,就像病毒感染一样,这样才像正常的搜索的样子嘛!”于是便有了BFS算法。广度优先算法便如其名字,它是以广度为优先的,一层一层搜索下去的。

还是以图的方式来演示,下图中start为搜索的初始节点,end为目标节点
在这里插入图片描述
我们先把start节点的相关节点遍历一次
在这里插入图片描述
接下来把第一步遍历过的节点当成start节点,重复第一步

在这里插入图片描述
一直重复一二步,这样便是一个放射样式的搜索方法,直到找到end节点
在这里插入图片描述

可以看出,这样放射性的寻找方式,能找到从start到end的最短路径(因为每次只走一步,且把所有的可能都走了,谁先到end说明这就是最短路)。

从实现的角度上,在广度优先遍历的过程中,我们需要用到队列

​ 1. 首先扩展最浅的节点

​ 2. 将后继节点放入队列的末尾(FIFO)

​ BFS是从根节点(起始节点)开始,按层进行搜索,也就是按层来扩展节点。所谓按层扩展,就是前一层的节点扩展完毕后才进行下一层节点的扩展,直到得到目标节点为止。

在这里插入图片描述

我们可以写出BFS的伪代码

     BFS_find(start节点){
        把start节点push入队列;
        while(队列不为空) {
            把队列首节点pop出队列;
            对节点进行相关处理或者判断;
            while(此节点有下一个相关节点){
                把相关节点push入对列;
            }
        } 
    }

思考:

一般情况下,深度优先适合深度大的树,不适合广度大的树,广度优先则正好相反。

所谓深度大的树就是指起始节点到目标节点的中间节点多的树(可以理解成问题有很多中间解,这些解都可以认为是部分正确的,但要得到完全正确的结果——目标节点,就必须先依次求出这些中间解)。

所谓广度大的树就是指起始节点到目标节点的可能节点很多的树(可以理解成问题有很多可能解,这些解要么正确,要么错误。要得到完全正确的结果——目标节点,就必须依次判断这些可能解是否正确)。

多数情况下,深度优先搜索的效率要高于宽度优先搜索。但某些时候,对于这两种搜索算法的优劣(或效率)还需要针对不同的问题进行具体分析比较。

2.4.3 BFS和DFS在搜索引擎中的应用

我们知道搜索引擎爬虫会从全网抓取网页,但是全网有数十万亿的网页,谷歌和百度之类的搜索引擎是如何下载整个互联网中的全部网页呢? 是用BFS还是DFS呢?

​现在的互联网非常庞大,今天Google的索引中有超过1万亿个网页,即使更新最频繁的基础索引也有几百亿个网页,假如下载一个网页需要一秒钟,下载这100亿个网页则需要317年。

​我们已经了解了BFS和DFS的原理,虽然从理论上讲,这两个算法(在不考虑时间因素的前提下)都能够在大致相同的时间里”爬下”整个”静态”互联网上的内容,但这只是理论上的可行性,它有两个假设——不考虑时间因素,互联网静态不变,都是现实中做不到的。

在这里插入图片描述

​搜索引擎的网络爬虫问题更应该定义成”如何在有限时间里最多地爬下最重要的网页”。显然各个网站最重要的网页应该是它的首页。在最极端的情况下,如果爬虫非常小,只能下载非常有限的网页,那么应该下载的是所有网站的首页,如果把爬虫再扩大些,应该爬下从首页直接链接的网页(就如同和北京直接相连的城市),因为这些网页是网站设计者自己认为相当重要的网页。在这个前提下,显然BFS明显优于DFS。

​事实上在搜索引擎的爬虫里,虽然不是简单地采用BFS,但是先爬哪个网页,后爬哪个网页的调度程序,原理上基本上是BFS。

那么是否DFS就不使用了呢?也不是这样的。实际的网络爬虫都是一个由成百上千甚至成千上万台服务器组成的分布式系统。对于某个网站,一般是由特定的一台或者几台服务器专门下载。这些服务器下载完一个网站,然后再进入下一个网站,而不是每个网站先轮流下载5%,然后再回过头来下载第二批。这样可以避免握手的次数太多。如果是下载完第一个网站再下载第二个,那么这又有点像DFS,虽然下载同一个网站(或者子网站)时,还是需要用BFS的。

​网络爬虫对网页遍历的次序不是简单的BFS或者DFS,而是有一个相对复杂的下载优先级排序的方法。管理这个优先级排序的子系统一般称为调度系统(Scheduler),由它来决定当一个网页下载完成后,接下来下载哪一个。
在这里插入图片描述

扩展: 
搜索引擎技术之网络爬虫 https://www.cnblogs.com/maybe2030/p/4778134.html
scrapy爬虫框架 https://scrapy.org/
pyspider爬虫框架 http://docs.pyspider.org/en/latest/

2.4.4 Dijkstra 算法

​广度优先算法,解决了起始顶点到目标顶点路径规划问题,但不是最优以及合适的,因为它的边没有权值(比如距离),路径无法进行估算比较最优解。为何权值这么重要,因为真实环境中,2个顶点之间的路线并非一直都是直线,需要绕过障碍物才能达到目的地,比如森林,湖水,高山,都需要绕过而行,并非直接穿过。

比如我采用广度优先算法,遇到如下情况,他会直接穿过障碍物(绿色部分),明显这个不是我们想要的结果:

在这里插入图片描述

那么如何解决带有权重时的寻找最佳路径的问题呢?

寻找图中一个顶点到另一个顶点的最短以及最小带权路径是非常重要的提炼过程。为每个顶点之间的边增加一个权值,用来跟踪所选路径的消耗成本,如果位置的新路径比先前的最佳路径更好,我们就将它放到新的路线中。

Dijkstra 算法基于BFS算法进行改进,把当前看起来最短的边加入最短路径树中,利用贪心算法计算并最终能够产生最优结果的算法。具体步骤如下:

1、每个顶点都包含一个预估值cost(起点到当前顶点的距离),每条边都有权值v,初始时,只有起始顶点的预估值cost为0,其他顶点的预估值d都为无穷大 ∞。

2、查找cost值最小的顶点A,放入path队列

3、循环A的直接子顶点,获取子顶点当前cost值命名为current_cost,并计算新路径new_cost,new_cost=父节点A的cost+v(父节点到当前节点的边权值),如果new_cost<current_cost,当前顶点的cost=new_cost

4、重复2,3直至没有顶点可以访问.

在这里插入图片描述

我们看到虽然Dijkstra 算法相对于广度度优先搜索更加智能,基于cost_so_far ,可以规避路线比较长或者无法行走的区域,但依然会存在盲目搜索的倾向,我们在地图中常见的情况是查找目标和起始点的路径,具有一定的方向性,而Dijkstra 算法从上述的图中可以看到,也是基于起点向子节点全方位扩散。

也就是对于Dijkstra算法来说,对全部路径进行了一遍查找,这其实是花费了非常多的工作量的,这种搜索具有有一定的盲目性,那么有没有更好的方法能够解决这种盲目性呢?

2.5 启发式搜索算法(有信息的图搜索算法)

在启发式搜索算法中,在问题本身定义之外给出了一些指引,能够比无信息的图搜索算法(如DFS、BFS等)更有效地找到解决方案。

比较经典的启发式搜索算法有贪婪最佳优先算法和A*寻路算法等,在游戏中应用比较广泛。

所有寻路算法都需要一种方法以数学的方式估算某个节点是否应该被选择。大多数游戏都会使用启发式(heuristic) ,以 h(x) 表示,就是估算从某个位置到目标位置的开销。理想情况下,启发式结果越接近真实越好。

——《游戏编程算法与技巧》

2.5.1 贪婪最佳优先搜索算法(Greedy Best-First Search)

贪心算法的含义是:求解问题时,总是做出在当前来说最好的选择。通俗点说就是,这是一个“短视”的算法。

搜索的每一步,都会查找相邻的节点,计算它们距离终点的曼哈顿距离,即最低开销的启发式。

曼哈顿距离——两点在南北方向上的距离加上在东西方向上的距离,即d(i,j)=|xi-xj|+|yi-yj|。 对于一个具有正南正北、正东正西方向规则布局的城镇街道,从一点到达另一点的距离正是在南北方向上旅行的距离加上在东西方向上旅行的距离,因此,曼哈顿距离又称为出租车距离。

在Dijkstra算法中,我们已经发现了其最重要的缺陷:搜索存在盲目性。而贪婪最佳优先搜索在障碍物少的时候足够的快,但最佳优先搜索得到的都是次优的路径。举例来说,如果目标节点在起始点的南方,那么贪婪最佳优先搜索算法会将注意力集中在向南的路径上。
在这里插入图片描述

例如下图,算法不断地寻找当前 h(启发式)最小的值,但这条路径很明显不是最优的。贪心最好优先算法虽然做了较少的计算,但却并不能找到一条较好的路径。

在这里插入图片描述
在这里插入图片描述

从上图中我们可以明显看到右边的算法(贪婪最佳优先搜索)寻找速度要快于左侧,虽然它的路径不是最优和最短的,但障碍物最少的时候,他的速度却足够的快。这就是贪心算法的优势,基于目标去搜索,而不是完全搜索。

那么这种算法有没有缺点呢? 肯定是有的,那就是得到的路径不是最短路径,只能是较优。

如何在搜索尽量少的顶点同时保证最短路径?我们来看A*算法。

2.5.2 基于BFS的A*寻路算法

从上面算法的演进,我们逐渐找到了最短路径和搜索顶点最少数量的两种方案,Dijkstra 算法和 贪婪最佳优先搜索。那么我们有没有可能汲取两种算法的优势,令寻路搜索算法即便快速又高效?

答案是可以的,A*算法正是这么做了,它吸取了Dijkstra 算法中的cost_so_far,为每个边长设置权值,不停的计算每个顶点到起始顶点的距离,以获得最短路线,同时也汲取贪婪最佳优先搜索算法中不断向目标前进优势,并持续计算每个顶点到目标顶点的距离,以引导搜索队列不断想目标逼近,从而搜索更少的顶点,保持寻路的高效。
在这里插入图片描述

​ 假设g(n)表示从起点到任意节点n的路径花费,h(n)表示从节点n到目标节点路径花费的估计值(启发值)。在上面的图中,黄色体现了节点距离目标较远,而青色体现了节点距离起点较远。A* 算法在物体移动的同时平衡这两者的值。定义f(n)=g(n)+h(n),A*算法将每次检测具有最小f(n)值的节点。然后朝着f(n)最小的点移动。

以下分别是Dijkstra算法,贪婪最佳优先搜索算法,以及A*算法的寻路雷达图,其中格子有数字标识已经被搜索了,可以对比下三种效率:
在这里插入图片描述

好了,到现在为止我们介绍了盲目的图搜索算法(DFS、BFS、Dijkstra), 启发式搜索算法(贪婪最佳优先、A*),你是否已经对搜索算法有所了解了呢?

声明

本博客所有内容仅供学习,不为商用,如有侵权,请联系博主谢谢。

参考文献

[1] 人工智能:一种现代的方法(第3版)
[2] 吴军说谷歌爬虫技术
[3] A* Pathfinding for Beginners
[4] A*’s Use of the Heuristic
[5] Solving problems by searching
[6] 夜深人静写算法

猜你喜欢

转载自blog.csdn.net/weixin_45552760/article/details/102623821