最优化算法指南-全-
最优化算法指南(全)
原文:
zh.annas-archive.org/md5/3c57cb9017f2102d6ec356e58bd55c38译者:飞龙
第一部分. 确定性搜索算法
欢迎来到本书的第一部分,我们将开始探索确定性图搜索算法。本部分包含四个章节。
在第一章中,你将学习搜索和优化的基本概念,并了解它们在现实世界中的重要性。你将发现如何定义优化问题,区分结构良好和结构不良的问题,深入了解搜索算法的挑战,并理解搜索困境。
第二章深入探讨了优化问题的分类。你将学习如何根据不同的标准对搜索和优化算法进行分类。此外,你还将了解启发式方法、元启发式方法和启发式搜索策略,并提前了解受自然界启发的算法。
在第三章中,你将探索图搜索技术,揭示图遍历方法,并发现如何使用盲目搜索算法在图中找到两个节点之间的最短路径,同时解决实际的路由问题。
在第四章中,你将深入研究信息搜索的概念。你将学习如何使用信息搜索算法解决最小生成树问题,并找到最短路径,同时获得解决现实世界路由问题的实际问题解决技能。
当你完成本书的这一部分时,你将牢固掌握优化的基础知识、确定性图搜索算法以及适用于现实场景的实际问题解决技能,为本书后续部分探讨的多种优化算法奠定基础。
第一章:搜索与优化简介
本章涵盖
-
搜索与优化是什么?
-
为什么关注搜索与优化?
-
从“玩具问题”到现实世界解决方案的转变
-
定义优化问题
-
介绍结构良好的问题和结构不良的问题
-
搜索算法和搜索困境
优化深深植根于自然以及我们构建的系统和技术中。自然是对优化普遍性和普遍存在的非凡证明。以社会昆虫如蚂蚁和蜜蜂的觅食行为为例。它们已经发展出自己独特的优化方法,从找到现有食物源的最短路径到在未知的外部环境中发现新的食物源。蜜蜂群体将它们的觅食努力集中在最有利可图的区域。它们合作建造六边形的蜂巢,以实现空间的高效利用(在给定区域内可以建造的最大细胞数)、材料效率(使用更少的蜂蜡)、结构强度以及防止蜂蜜从细胞中溢出的最佳角度。同样,鸟类在它们的年度迁徙过程中也表现出优化行为。它们从繁殖地长途跋涉到越冬地,它们的迁徙路线经过多代优化以节省能量。这些路线考虑了诸如盛行风模式、食物可用性和免受捕食者威胁等因素。这些例子强调了自然界如何本能地应用优化策略以生存和成长,为我们提供了可以转化为算法解决问题的教训。
优化也是我们日常生活中的一种常规方面,通常如此无缝集成,以至于我们几乎注意不到它的持续影响。作为人类,我们努力优化我们的日常生活。考虑一下计划你的一天这个简单的行为。我们本能地按顺序或分组任务或差事,以最大限度地减少旅行时间或最大限度地增加我们的空闲时间。我们在预算内应对购物的挑战,试图从每花费的每一美元中获得最大价值。我们创建锻炼计划,旨在在有限的时间内获得最大的健身效益。即使在家庭中,我们也优化我们的能源使用,以控制我们的公用事业账单。
同样,公司通过提高效率和消除浪费来最大化利润。例如,像联邦快递(FedEx)、联合包裹服务公司(UPS)和亚马逊这样的物流巨头每年花费数百万美元研究降低包裹配送成本的新方法。电信机构寻求确定关键基础设施,如蜂窝塔的最佳位置,以服务尽可能多的客户,同时投资于最低限度的设备。同样,像优步(Uber)、Lyft 和滴滴出行这样的交通网络公司,在乘客行程中高效地安排司机路线,在空闲期间将司机引导至叫车热点,以最小化乘客等待时间。随着全球城市化进程的加剧,地方紧急服务依赖于高效的调度和路线规划平台,以选择和调度适当的车辆、设备和人员,以应对日益复杂的城市道路网络中的事件。航空公司需要解决多个优化问题,例如飞行计划、机队分配、机组人员排班、飞机航线规划和飞机维护计划。医疗保健系统也处理诸如医院资源规划、紧急程序管理、患者入院排班、手术排班和疫情控制等优化问题。作为优化技术的主要客户,工业 4.0 处理诸如智能调度和重新调度、装配线平衡、供应链优化和运营效率等复杂的优化问题。智能城市处理诸如固定资产最优分配、移动资产部署、能源优化、水控制、污染减少、废物管理和野火控制等大规模优化问题。
这些例子展示了优化作为一种提高不同领域操作效率的普遍性和重要性。在这本书中,我们将深入探索优化算法的激动人心世界。我们将揭示这些算法如何被用于解决不同领域中的复杂连续和离散问题。
1.1 为什么关注搜索和优化?
搜索是对状态的系统性检查,从初始状态开始,最终(希望)达到目标状态。实际上,优化技术是搜索方法,其目标是找到可行搜索空间内的一个最优或近似最优状态。这个可行搜索空间是优化问题空间的一个子集,其中所有问题的约束都得到了满足。很难找到一个不使用某种形式的搜索或优化方法、软件或算法的行业。在您的工作场所或行业中,您很可能每天都在处理优化问题,尽管您可能没有意识到这一点。虽然搜索和优化在几乎所有行业中都很普遍,但使用复杂的算法来优化流程可能并不总是实用的。例如,考虑一家为当地顾客提供食品配送的小型比萨饼店。假设该餐厅在平均一周的夜晚处理大约十次配送。虽然提高效率的策略(如避免在右行国家左转或在左行国家右转、避免主要交叉口、在接送时间避免学校区域、在提升时间避免桥梁、优先选择下坡路)从理论上可以缩短配送时间并降低成本,但问题的规模如此之小,实施这些改变可能不会产生任何明显的效果。
在更大规模的问题中,如车队分配和调度、多标准随机车辆路径、资源分配和机组人员调度,将搜索和优化技术应用于问题必须是一个合格的决定。一些公司或企业可能由于缺乏专业知识或资源来实施这些改变而不会从过度流程改变中受益。还可能存在关于利益相关者潜在缺乏后续行动的担忧。实施这些改变也可能比通过优化过程获得的节省更多。在本书的后面部分,我们将看到如何在开发搜索和优化算法时考虑这些成本。
这本书将帮助大多数人从从未解决搜索和优化问题到成为一个全面发展的搜索和优化实践者,能够为正确的问题选择、实施和调整正确的求解器。它不假设任何关于搜索和优化的先验知识,只需具备数据结构和 Python 的中级知识。对于在职场中参与高级技术决策的管理者或专业人士来说,这些技能在讨论流程改进时理解基于软件的方法、机会和局限性至关重要。相比之下,IT 专业人士在考虑为内部使用开发或选择新的软件套件和技术时,会发现这些技能直接适用。以下部分描述了我们将在这本书中遵循的方法。
1.2 从玩具问题到现实世界
在讨论算法时,许多书籍和参考资料将它们作为形式定义来呈现,然后应用于所谓的“玩具问题”。这些简单问题是有帮助的,因为它们通常涉及较小的数据集和搜索空间,同时可以通过手动迭代来解决。本书采用类似的方法,但更进一步,通过展示现实世界的数据实现。尽可能使用数据集和值等资源来说明所讨论算法的直接适用性和实际缺点。最初,缩小的玩具问题将帮助您欣赏各种算法中涉及的逐步操作。后来,Python 实现将教会您如何使用多个数据集和 Python 库来应对现实世界问题的增加复杂性和范围。
如图 1.1 所示,每个搜索或优化算法的灵感来源被确定,然后展示算法伪代码、算法参数以及使用的启发式/解决方案策略。接着描述算法的优缺点和适应方法。本书包含许多示例,让您能够对问题的缩小版本进行手动迭代,并完全理解每个算法的工作原理。它还包括许多以特殊问题-解决方案-讨论格式编排的编程练习,让您可以看到如何使用 Python 解决之前手动解决的问题的放大版本。通过编程,您可以优化调整算法,并研究其性能和可扩展性。

图 1.1 本书的方法——每个算法都将按照从解释到应用的模式进行介绍。
在整本书中,我们将考虑几个经典和现实世界的优化问题,以向您展示如何使用书中讨论的搜索和优化算法。图 1.2 展示了这些优化/搜索问题的示例。

图 1.2 经典和现实世界优化问题的示例
在时间不是那么重要,而解决方案的质量更重要,并且用户愿意等待(有时甚至几天)以获得最优解决方案的情况下,可以使用现实世界的设计问题或战略函数。规划问题或战术函数需要在几秒到几分钟的时间范围内解决。控制问题或操作函数需要在几毫秒到几秒的时间范围内重复快速解决。为了在如此短的时间内找到解决方案,通常以速度提升为代价来牺牲最优性。在下一章中,我们将更详细地讨论这些问题类型。
我强烈建议您首先对每个算法后面的示例进行手动迭代,然后尝试自己重现 Python 实现。您可以在代码中随意调整参数和问题规模;通过软件运行优化算法的一个优点是能够调整以实现最优性。
1.3 优化问题的基本要素
优化是指寻找给定问题的“最佳”解决方案的实践,其中“最佳”通常意味着令人满意或可接受的,可能受限于一组给定的约束。解决方案可以分为可行、最优和近似最优解决方案:
-
可行解是满足所有给定约束的解决方案。
-
最优解既是可行的,又提供了最佳的目标函数值。
-
近似最优解是可行的解决方案,它提供了更好的目标函数值,但不一定是最好的。
假设我们有一个最小化问题,目标是找到决策变量的值,以最小化某个目标函数,搜索空间可能包含多个全局最小值、强局部最小值和弱局部最小值,如图 1.3 所示:
-
全局最优解(或最小化问题中的全局最小值)是一组候选解决方案中的最佳(即整个可行搜索空间中的最低点)。从数学上讲,如果ƒ(x)是目标函数,那么 x是全局最小值,如果对于ƒ的定义域中的所有 x,有ƒ(x**) ≤ ƒ(x)。
-
一个强局部最小值是指函数值小于(或等于)该点周围邻域内函数值的点,但高于全局最小值。从数学上讲,如果 x是一个点,那么它是一个强局部最小值,如果存在一个以 x 为中心的邻域 N,使得对于 N 中的所有 x(x ≠ x),有ƒ(*x**) < ƒ(x)。
-
一个弱局部极小值是指函数值小于或等于相邻点的函数值,但对于收敛到该点的点的序列,函数值严格递减。从数学上讲,如果存在一个以 x 为中心的邻域 N,使得对于 N 中的所有 x,都有 ƒ(x**) ≤ ƒ(x*),则点 x* 是一个弱局部极小值。

图 1.3 可行解位于问题的约束范围内。一个可行的搜索空间可能显示全局、强局部和弱局部极小值的组合。
这些寻求最优解的方法,也称为优化技术,通常作为运筹学(OR)的一部分进行研究。运筹学,也称为决策或管理科学,是一个起源于第二次世界大战初期的领域,由于军事行动中分配稀缺资源的迫切需要。它是应用高级科学分析方法于决策和管理问题以找到最佳或最优解的数学分支。
优化问题通常可以表述如下。找到 X,使其优化 ƒ,同时满足可能的一组等式和不等式约束:
| g[i](X) = 0, i = 1, 2, ..., m**h[j](X) ≤ 0, j = 1, 2, ..., p | 1.1 |
|---|
其中
-
X = (x[1], x[2],…, x[n])^T 是代表决策变量的向量
-
ƒ(X) = (ƒ1, ƒ2,…, ƒ[M](X)) 是要优化的目标向量
-
g[i]**(X) 是一组等式约束
-
h[j](X) 是一组不等式约束
以下小节描述了优化问题的三个主要组成部分:决策变量、目标函数和约束。
1.3.1 决策变量
决策变量代表影响目标函数值的一组未知数或变量。这些是定义优化问题可能解的变量。如果 X 代表未知数,也称为自变量,那么 f(X) 量化了候选解或可行解的质量。
例如,假设一个活动组织者正在计划一个关于搜索和优化算法的会议。组织者计划支付 a 用于固定成本(场地租赁、安保和嘉宾演讲费)和 b 用于变动成本(传单、吊牌、身份证牌和午餐),这些成本取决于参与者的数量。根据过去的会议,组织者预测门票需求如下:
| Q = 5000 – 20x | 1.2 |
|---|
其中 x 是票价,Q 是预期售出的票数。因此,公司预计以下场景:
-
如果公司不收费(x = 0),他们将免费赠送 5,000 张门票。
-
如果票价是 x = $250,公司将没有参与者,预期售出的票数将为 0。
-
如果票价x < $250,公司将会卖出一定数量的票,0 ≤ Q ≤ 5,000。
活动组织者可以预期的利润f(x)可以按以下方式计算:
| Profit = Revenue – Costs | 1.3 |
|---|
其中Revenue = Qx和Costs = a + Qb。总的来说,利润(或目标)函数看起来是这样的:
| ƒ(x) = Revenue – Costs = Qx – (a + Qb) = –20x² + (5000 + 20b)x – 5000b – a | 1.4 |
|---|
在这个问题中,预定义的参数包括固定成本a和变动成本b。有一个单一的决策变量x,它是票价,其中x[LB] ≤ x ≤ x[UB]。票价的下限x[LB]和上限x[UB]被认为是边界约束。解决这个优化问题集中在找到最大化利润ƒ(x)的最佳x值。
1.3.2 目标函数
目标函数ƒ(x),也称为标准,优点函数,效用函数,成本函数,代表要优化的数量。不失一般性,优化可以解释为值的极小化,因为原函数ƒ(x)的最大化可以仅仅是应用数学运算后生成的对偶问题的极小化。这意味着如果原函数是一个极小化问题,那么对偶问题就是一个最大化问题(反之亦然)。根据优化问题的这个对偶方面,一个解x,它是原极小化问题的最小值,同时,也是对偶最大化问题的最大值,如图 1.4 所示。
此外,像加法、减法、乘法和除法这样的简单数学运算不会改变最优点的值。例如,将ƒ(x)乘以一个正常数或向ƒ(x)中添加或减去一个正常数,都不会改变决策变量的最优值,如图 1.4 所示。

图 1.4 优化问题的对偶原理和数学运算
在早期的票价定价问题中,假设a = 50,000,b = 60,x[LB] = 0,和x[UB] = 250。使用这些值,我们得到一个利润函数:ƒ(x) = –20x² + 6,200x – 350,000。采用基于导数的方法,我们可以简单地推导出函数以找到其最大值:df/dx = –40x + 6,200 = 0 或 40x = 6,200。因此,最优票价是$155,这会产生$130,500 的净利润,如图 1.5 所示。

图 1.5 票价定价问题——最大化利润的最优票价是每张票$155。
在票价定价问题中,我们有一个需要优化的单一目标函数,即利润。在这种情况下,该问题被称为单目标优化问题。涉及多个目标函数的优化问题称为多目标优化问题。例如,假设我们想要设计一辆电动汽车(EV)。这个设计问题的目标函数可以是最小化加速度时间和最大化环境保护署(EPA)的驾驶范围。加速度时间是电动汽车从 0 加速到 60 英里/小时所需的时间(以秒计)。EPA 驾驶范围是车辆在需要重新充电之前,在综合城市和高速公路驾驶中可以行驶的大约英里数(使用 55%的高速公路和 45%的城市驾驶混合)。根据 EPA 的测试方法,EPA 驾驶范围是车辆在需要重新充电之前,在综合城市和高速公路驾驶中可以行驶的大约英里数。决策变量可以包括车轮的大小、电动机的功率和电池的容量。需要更大的电池来延长电动汽车的驾驶范围,这会增加额外的重量,因此加速度时间会增加。在这个例子中,两个目标存在冲突,因为我们需要最小化加速度时间并最大化 EPA 范围,如图 1.6 所示。

图 1.6 最大化 EPA 范围和最小化加速度时间的电动汽车设计问题
这个多目标优化问题可以通过基于偏好的多目标优化程序或使用帕累托优化方法来处理。在前一种方法中,首先应用对偶原理来转换所有用于最大化的冲突目标(例如,最大化 EPA 范围和加速度时间的倒数)或用于最小化的目标(例如,最小化加速度时间和 EPA 范围的倒数)。然后,我们通过使用相对偏好向量或加权方案将这些多个目标组合成一个整体目标函数,以将多个目标标量化。例如,你可能更重视 EPA 范围而不是加速度时间。然而,找到这个偏好向量或权重是主观的,有时并不直接。帕累托优化方法依赖于找到多个权衡最优解,并使用高级信息选择其中一个。该程序试图通过将备选方案的数量减少到最优的非支配解集(称为帕累托前沿)来找到最佳的权衡,这可以用于在多目标空间中做出战略决策。多目标优化在第八章中讨论。
约束满足问题(CSP)没有定义一个明确的目标函数。相反,目标是找到一个满足给定约束的解决方案。n-后问题是一个 CSP 的例子。在这个问题中,目标是把 n 个皇后放在 n x n 的棋盘上,且没有两个皇后在同一行、同一列或同一对角线上。4 x 4 皇后 CSP 问题有两个最优解。这两个最优解在本质上或客观上并不优于对方。问题的唯一要求是满足给定的约束。
1.3.3 约束
约束优化问题有一组等式和/或不等式约束 g[i](X),l[j](X),这些约束限制了分配给决策变量的值。此外,大多数问题还有一组边界约束,这些约束定义了每个变量的值域。此外,约束可以是硬的(必须满足)或软的(希望满足)。以下是一个学校排课问题的例子:
-
同一时间在同一教室里没有多场讲座是一个硬约束。
-
没有老师在同一时间进行多场讲座也是一个硬约束。
-
为每位老师保证至少三天教学时间可能是一个软约束。
-
将连续讲座安排在附近的教室可能是一个软约束。
-
避免安排非常早或非常晚的讲座也可能是一个软约束。
作为硬约束和软约束的另一个例子,导航应用如 Google Maps、Apple Maps、Waze 或 HERE WeGo 可能允许用户设置路由偏好:
-
避免渡轮、收费公路和高速公路将是硬约束。
-
避免高峰时段繁忙的交叉路口、高速公路或接送学生时段的学校区域可能是一个软约束。
软约束可以通过将奖励/惩罚函数作为目标函数的一部分来建模。该函数可以奖励满足软约束的解决方案,并惩罚那些不满足的解决方案。
例如,假设图 1.7 中有 10 个包裹需要装在载货自行车上。

图 1.7 载货自行车装载问题是一个具有软约束的问题的例子。虽然包裹的重量可以超过自行车的容量,但如果自行车超重,将会应用惩罚。
每个包裹都有自己的重量、利润和效率值(每公斤利润)。目标是选择要装载的包裹,以便最大化利润函数 ƒ[1] 并最小化重量函数 ƒ[2]。这是一个经典的组合问题:
|

| 1.5 |
|---|
其中 n 是包裹总数,E[i] 是包裹 i 的效率
|

| 1.6 |
|---|
其中 w[i] 是包裹 i 的重量,C 是自行车的最大容量。如果添加的包裹总重量超过最大容量,则将添加 50 的惩罚。
软约束也可以用来使搜索算法更具适应性。例如,随着算法的进展,惩罚的严重程度可以动态改变,最初施加较宽松的惩罚以鼓励探索,但在接近结束时施加更严重的惩罚,以生成一个主要受约束的结果。
1.4 良好结构化问题与不明确问题
我们可以根据其结构和存在(或不存在)的解决程序来对优化问题进行分类。以下小节介绍了良好结构化和不明确问题。
1.4.1 良好结构化问题
在《不明确问题的结构》一文中,赫伯特·西蒙概述了良好结构化问题(WSPs)的六个关键特征[1]。这些特征包括存在一个明确的测试所提解决方案的标准,存在一个能够表示初始问题状态和潜在解决方案的问题空间,以及在该问题空间内表示可达到和相当大的状态变化。此外,问题解决者所获得的知识可以在这些空间中表示,如果问题涉及与外部世界的交互,状态变化将反映现实世界的规律。西蒙强调,这些条件强烈成立,意味着这些过程需要可行的计算,并且用于问题解决的信息在没有过度搜索努力的情况下是有效可得的。
假设我们正在规划一个在检查系统中的机器人拾取和放置任务。在这种情况下,机器人会等待接收到来自存在传感器的信号,该信号指示传送带上存在一个有缺陷的工件。机器人停止传送带,拾起有缺陷的工件,并将其放入废料箱。然后机器人重新激活传送带的运动。在此操作之后,机器人返回到其初始位置,循环重复。如图 1.8 所示,这个问题具有以下良好结构化组件:
-
可行状态—机器人臂的位置和速度及其末端执行器(夹爪)的方向和状态(开或关和方向)
-
操作员(后继)—机器人臂运动控制命令,按照一定的无奇点轨迹(空间中的位置或关节角度和运动速度)从一个点到另一个点移动,以及末端执行器控制(方向和开或关)
-
目标—无论其方向如何,拾取和放置一个有缺陷的工件
-
解决方案/路径—通过状态空间的最优序列,以实现最快的拾取和放置操作
-
停止标准—有缺陷的工件从传送带上拾取并放置在废料箱中,机器人返回到其初始位置
-
评估标准——拾取和放置的持续时间以及/或拾取和放置过程的成功率

图 1.8 一个 WPS 具有定义明确的问题空间、允许的操作运算符、清晰的评估标准和计算可行性。
如您所见,工作环境高度结构化、静态且完全可观察。问题可以数学建模,并且可以生成并执行一个最优的拾取和放置计划,具有高度的确定性。这个拾取和放置问题可以被视为一个 WSP。
1.4.2 不良结构化问题
不良结构化问题(ISPs)是没有算法解决方案或通用问题求解器的复杂离散或连续问题。ISPs 的特点是具有以下一个或多个特征:
-
具有不同的问题视图、不明确的目标、多模态和动态性质的问题空间
-
缺乏精确的数学模型或缺乏经过充分验证的算法解决方案
-
产生矛盾解决方案、难以预测的后果和难以或无法计算的风险,导致缺乏明确的评估标准
-
在不确定性、部分可观察性、模糊性、不完整信息、歧义或不确定性方面存在相当大的数据不完善,这使得监控解决方案的执行变得困难,有时甚至不可能
-
计算不可行性
假设我们需要找到最优的调度方案,以服务于 10 层楼之间的用户,如图 1.9 所示。这是一个使用传统方法无法解决的经典问题示例。

图 1.9 电梯调度问题——有四辆电梯车和 10 层楼,这个问题有大约 10 的 21 次方可能的状态。
在这个最优调度问题中可以考虑以下目标函数:
-
最小化平均等待时间——用户在进入电梯前需要等待多长时间
-
最小化平均系统时间——用户在到达目的地楼层前需要等待多长时间
-
最小化等待时间超过 60 秒的用户百分比
-
确保为所有电梯用户提供服务时的公平性
这个最优调度问题是一个 ISP 的例子,因为问题空间具有动态性和部分可观察性;无法预测用户的呼叫和目的地。定义一个最优解几乎是不可能的,因为它可以在基于已知情况(如如果收到相反方向的移动新请求)做出决策后立即改变。此外,由于考虑了不同的电梯位置、电梯按钮和厅堂呼叫按钮,搜索空间非常大:
-
电梯位置—每部电梯可以位于 10 个楼层中的任何一个。因此,对于每部电梯,有 10 种不同的可能状态。由于有四部电梯,电梯位置的组合数是 10 的 4 次方。
-
电梯按钮—每部电梯有 10 个按钮,可以是开启(按下)或关闭(未按下)。因此,对于一部电梯,有 2 的 10 次方种不同的可能状态。由于有四部电梯,电梯按钮的组合数是 2 的 40 次方。
-
楼层呼叫按钮—有 18 个楼层呼叫按钮(每层楼都有上下按钮,除了第一层和最后一层)可以是开启或关闭状态。因此,楼层呼叫按钮的组合数是 2 的 18 次方。
假设每个按钮按下的组合都是有效的(即,忽略电梯系统的物理或逻辑限制,例如不允许同一楼层的上下呼叫按钮同时按下),总状态数可以计算如下:可能状态数 = 10 的 4 次方(电梯位置)* 2 的 40 次方(电梯按钮)* 2 的 18 次方(楼层呼叫按钮)= 2.88 x 10 的 21 次方种不同的状态。总状态数超过了宇宙中的星星数量!
1.4.3 WSP,但在实践中是 ISP
旅行商问题(TSP)是一个在原则上可能结构良好的问题,但在实践中却变得结构不良的例子。这是因为解决该问题所需的计算能力在现实中是不切实际的。
假设一位旅行商被分配去访问一个包含n个城市的列表。旅行商希望以最短的时间访问所有这些城市,因为销售人员通常是按佣金而不是按小时支付的。此外,城市的巡回可能是不对称的;从城市 A 到城市 B 所需的时间可能不等于反向,这可能是由于基础设施、交通模式和单行道的原因。例如,有 13 个城市要访问,这个问题最初可能看起来很简单。然而,经过仔细检查,使用朴素算法的 TSP 搜索空间会导致 13! = 6,227,020,800 种不同的可能路线需要检查!幸运的是,动态规划算法可以降低复杂性,正如我们将在下一章中看到的。
本书主要关注 ISP,以及实践中是 ISP 的 WSP,原因如下:
-
WSP 通常有已知的解决算法,这些算法通常提供简单的、逐步的程序。因此,对于这类问题,往往存在非常高效且广为人知的解决方案。此外,一些 WSP 可以使用基于导数的通用求解器来解决。
-
解决 WSP 所需的计算能力通常是可以忽略不计的,或者在最坏的情况下也是可以管理的。特别是随着消费级计算机的持续改进,更不用说通过云计算和分布式处理提供的庞大资源,我们通常不必满足于由计算瓶颈产生的近似最优 WSP 解决方案。
-
世界上大多数问题都是 ISP 问题,因为问题范围、状态和环境是动态的,有时部分可观察,并具有一定的不确定性。因此,ISP 的解决方案或算法在现实世界场景中有更广泛的应用性,并且有更大的动力去寻找这些问题的解决方案。
本书探讨的大多数算法都是无导数和随机的;它们在参数和决策过程中使用随机性。这些算法通常非常适合解决 ISP 问题,因为它们初始状态和操作者的随机性使得算法能够逃离局部最小值并找到最优或近似最优解。相比之下,确定性算法使用定义良好和程序化的路径来达到解决方案,通常不适合 ISP 问题,因为它们要么无法在未知搜索空间中工作,要么无法在合理的时间内返回解决方案。此外,本书涵盖的大多数算法都是黑盒求解器,它们将优化问题视为黑盒。这个黑盒为某些决策变量值提供相应的目标函数和约束函数的值。重要的是,这种方法消除了考虑目标函数和约束函数的各种特性的需要,例如非线性、可微性、非凸性、单调性、间断性,甚至随机噪声。
1.5 搜索算法和搜索困境
任何优化方法的目标是为决策变量分配值,以优化目标函数。为了实现这一点,优化算法在解空间中搜索候选解。约束条件仅仅是搜索空间中特定区域的限制。因此,所有优化技术实际上都是搜索方法,其目标是找到满足约束条件的可行解,并最大化(或最小化)目标函数。我们将“搜索”定义为从初始状态开始,系统地检查可行状态,并希望最终达到目标状态。然而,当我们探索可行搜索空间时,我们可能会找到一些相当好的邻近解,问题是我们是否应该利用这个区域,或者继续探索,在其他可行搜索空间的区域寻找更好的解决方案。
探索(或多样化)是在可行搜索空间中调查新区域的过程,希望找到其他有希望的解决方案。另一方面,利用(或强化)是将搜索代理引导到关注已知有良好或精英解决方案的搜索空间中的吸引区域的过程。
这种探索-利用困境是搜索和优化,以及生活中的最重要的问题之一。我们在生活中应用探索-利用策略。当我们搬到新城市时,我们首先探索不同的商店和餐馆,然后关注我们周围的短名单选项。在人生的中年危机中,一些中年人觉得在日常的日常生活和生活方式中感到无聊,并且没有令人满意的成就,他们倾向于采取探索性行动。美国移民系统试图避免利用特定的申请人群体(例如,家庭、技术工人、难民和寻求庇护者),并通过计算机生成的彩票实现更多样化。在社会昆虫如蜜蜂中,寻找食物来源是由两个不同的工人群组执行的,即觅食者和侦察兵(觅食者的 5-25%)。觅食蜜蜂专注于特定的食物来源,而侦察兵是新颖性寻求者,他们不断在周围寻找丰富的花蜜。在搜索和优化中,探索-利用困境代表了在搜索空间中探索新未访问状态或解决方案与利用在搜索空间某个邻域中找到的精英解决方案之间的权衡(图 1.10)。

图 1.10 搜索困境——在扩展到搜索空间的新区域或专注于已知有良好或精英解决方案的区域之间总是存在权衡。
本地搜索算法是利用算法,如果搜索空间是多模态的,则很容易陷入局部最优。在另一个极端,随机搜索算法以高概率达到全局最优为代价,不断探索搜索空间,但搜索时间不切实际。一般来说,探索算法可以在处理时间的基础上找到全局最优,而利用算法则存在陷入局部最小值的风险。
摘要
-
优化在生活的许多领域、工业和研究领域无处不在且普遍存在。
-
决策变量、目标函数和约束条件是优化问题的关键成分。决策变量是你能够控制并影响目标函数值的输入。目标函数是需要优化的函数,可以是最小化或最大化。约束条件是解决方案必须满足的限制或约束。
-
优化是寻找问题“最佳”解决方案的搜索过程,提供最佳的目标函数值,并可能受到一组硬(必须满足)和软(希望满足)约束的限制。
-
结构不良问题是复杂的不连续或连续问题,没有精确的数学模型和/或算法解决方案或通用问题求解器。它们通常具有动态和/或部分可观察的大搜索空间,无法通过经典优化方法处理。
-
在许多实际应用中,快速找到一个近似最优解比花费大量时间寻找最优解要好。
-
在未来的章节中,你将频繁遇到的两个关键概念是探索(或多样化)和利用(或强化)搜索困境。在探索和利用之间取得平衡将允许算法在没有陷入搜索空间的吸引力区域中的局部最优,并且不花费大量时间的情况下找到最优或近似最优解。
第二章:深入了解搜索和优化
本章涵盖
-
根据不同标准对优化问题进行分类
-
根据搜索空间探索方式和算法的确定性对搜索和优化算法进行分类
-
介绍启发式算法、元启发式算法和启发式搜索策略
-
首次了解受自然启发的搜索和优化算法
在我们深入探讨第一章中提到的问题和算法之前,弄清楚我们如何讨论这些问题和算法将是有用的。对问题进行分类使我们能够将类似的问题分组,并可能利用现有的解决方案。例如,一个涉及地理值(即城市和道路)的旅行商问题可能被用作模型来找到连接引脚的最小长度,这在超大规模集成电路(VLSI)设计中是非常大的。同样,对算法本身进行分类也是有益的,因为将具有相似属性的算法分组可以让我们轻松地识别出解决特定问题的正确算法,并满足期望,例如解决方案的质量和允许的搜索时间。
在本章中,我们将讨论优化问题和算法的常见分类。启发式算法和元启发式算法也将作为指导搜索过程的通用算法框架或高级策略进行介绍。许多这些策略都受到自然的启发,因此我们将对受自然启发的算法进行一些探讨。让我们首先讨论如何根据不同的标准对优化问题进行分类。
2.1 优化问题的分类
优化无处不在!在日常生活中,你会遇到各种优化问题。例如,你可能喜欢将恒温器设置到一定的温度,以保持舒适并同时节省能源。你可能选择灯具并调整灯光水平以降低能源成本。当你开始驾驶电动汽车(EV)时,你可能寻找最快或最节能的路线到达目的地。在你到达目的地之前,你可能寻找一个价格合理、提供最短步行距离到达目的地、提供电动汽车充电且最好是地下停车场的停车位。这些优化问题的复杂程度不同,主要取决于问题的类型。正如前一章所述,优化过程涉及从给定的可行搜索空间中选择决策变量,以便优化(最小化或最大化)给定的目标函数,或者在某些情况下,多个目标函数。
优化问题由三个主要组成部分特征化:决策变量或设计向量、要优化的目标函数或标准,以及需要满足的一组硬约束和软约束。这三个组成部分的性质、解决问题允许的时间以及预期解决方案的质量导致不同类型的优化问题,如图 2.1 所示。

图 2.1 优化问题分类——一个优化问题可以分解为其组成部分,这些组成部分是此类问题分类的基础。
以下小节将更详细地解释这些类型,并为每种优化问题提供示例。
2.1.1 决策变量的数量和类型
根据决策变量的数量,优化问题可以广泛分为单变量(单变量)或多变量(多变量)问题。例如,车辆速度、加速度和轮胎压力是影响车辆燃油经济的参数之一,其中燃油经济性指的是车辆在特定燃料量下能行驶多远。根据美国能源部,控制车辆的速度和加速度可以在高速公路上提高其燃油经济性 15%至 30%,在走走停停的交通中提高 10%至 40%。美国国家公路交通安全管理局(NHTSA)的一项研究发现,轮胎压力降低 1%与燃油经济性降低 0.3%相关。如果我们只寻找最大燃油经济性的最优车辆速度,则该问题是单变量优化问题。寻找最大燃油经济性的最优速度和加速度是双变量优化问题,而寻找最优速度、加速度和轮胎压力是多变量问题。
问题分类也根据决策变量的类型而变化。连续问题涉及连续值变量,其中 x[j] ∈ R。相比之下,如果 x[j] ∈ Z,则问题是整数或离散优化问题。混合整数问题既有连续值变量也有整数值变量。例如,优化电梯速度和加速度(连续变量)以及乘客上下车的顺序(一个离散变量)是一个混合整数问题。解决方案是整数值变量的集合、组合或排列的问题被称为组合优化问题。
组合与排列
组合数学是研究一组元素组合和排列的数学分支。组合与排列的主要区别在于顺序。如果元素的顺序不重要,则它是组合;如果顺序很重要,则它是排列。因此,排列是有序组合。根据是否允许元素重复,我们可以有不同的组合和排列形式。

组合和排列——排列尊重顺序,因此是按顺序排列的组合。组合和排列都有带重复和不带重复的变体。
例如,假设我们正在设计一个包含多个健身活动的健身计划。健身计划中可以包含五种类型的练习:慢跑、游泳、骑自行车、瑜伽和有氧运动。在一周的计划中,如果我们只选择这五种练习中的三种,并且允许重复,可能的组合数将是(n + r – 1)! / r!(n – 1)! = (5 + 3 – 1)! / 3!(5 – 1)! = 7! / (3! × 4!) = 35。这意味着我们可以通过选择五种可用练习中的三种并允许重复来生成 35 种不同的健身计划。
然而,如果不允许重复,可能的组合数将是C(n,r) = n! / r!(n – r)! = 5! / (3! × 2!) = 10。这个公式通常被称为“n选r”(例如“5 选 3”),它也被称为二项式系数。这意味着如果我们不想重复任何练习,我们只能生成 10 个计划。
在带重复和不带重复的组合中,健身计划不包括包含的练习的执行顺序。如果我们尊重特定的顺序,计划将采取排列的形式。如果允许重复练习,选择三个可用练习的可能排列数将是n^r = 5³ = 125。然而,如果不允许重复,可能的排列数将是P(n,r) = n! / (n – r)! = 5! / (5 – 3)! = 60。
当从头开始编写代码时,组合学在 Python 中可以相当容易地实现,但也有一些优秀的库可用,例如 SymPy,这是一个开源的 Python 符号数学库。它的功能包括但不限于统计学、物理学、几何学、微积分、方程求解、组合学、离散数学、密码学和解析。例如,可以使用以下简单的代码在 SymPy 中计算二项式系数:
from sympy import binomial
print(binomial(5,3))
想要了解更多关于在 Python 中实现组合学的信息,请参阅附录 A 和 SymPy 的文档。
旅行商问题(TSP)是组合问题的一个常见例子,其解决方案是一个排列——要访问的城市序列。在 TSP 中,给定n个城市,旅行商必须访问所有城市然后返回家中,形成一个循环(往返)。旅行商希望以最有效的方式旅行(例如最快的、最便宜的或最短的路程)。
TSP 可以分为对称 TSP(STSP)和非对称 TSP(ATSP)。在 STSP 中,两个城市之间的距离在两个方向上是相同的,形成一个无向图。这种对称性将可能的解决方案数量减半。ATSP 是对称版本的严格推广。在 ATSP 中,路径可能不是双向的,或者距离可能不同,形成一个有向图。交通碰撞、单行道、桥梁以及不同出发和到达费用的城市的机票价格都是这种对称性可能崩溃的例子。
TSP 中的搜索空间非常大。例如,假设销售人员需要访问大多伦多地区(GTA)的 13 个主要城市,如图 2.2 所示。朴素解的复杂度是O(n!)。这意味着在 ATSP 的情况下,有n! = 13! = 6,227,020,800 种可能的旅行路线。在 STSP 和 ATSP 中,这都是一个非常大的搜索空间。然而,动态规划(DP)算法能够降低复杂度。

图 2.2 大多伦多地区(GTA)的 TSP。旅行销售人员必须访问所有 13 个城市,并希望选择“最佳”路径,无论这是基于距离、时间还是其他标准。
动态规划是一种通过将问题分解成更小的子问题并独立解决每个子问题来解决问题的方法。例如,Bellman-Held-Karp 算法[1]的复杂度是O(2^n × n²)。还有其他具有不同计算复杂度和近似比率的求解器和算法,如 Concorde TSP 求解器、2-opt 和 3-opt 算法、分支和界限算法、Christofides 算法(或 Christofides-Serdyukov 算法)、Lin-Kernighan 算法、基于元启发式的算法、图神经网络和深度强化学习方法。例如,Christofides 算法[2]是一个多项式时间近似算法,它产生的 TSP 解决方案保证不会比最优解长 50%,时间复杂度为O(n³)。有关使用 NetworkX 包实现的 Christofides 算法解决 TSP 的解决方案,请参阅附录 A。我们将在本书中讨论如何使用这些算法中的许多来解决 TSP。
广泛的离散优化问题可以建模为 TSP。这些问题包括但不限于微芯片制造、排列流程车间调度、为学校区域内的儿童安排校车路线、分配飞机路线、运输农业设备、服务调用调度、餐食配送以及包裹配送和取件的卡车路线规划。例如,容量车辆路径问题(CVRP)是 TSP 的一种推广,其中必须使用位于一个共同仓库的车辆车队为一系列客户提供服务。每个客户对位于仓库的某些商品有一定的需求。任务是设计从仓库开始和结束的车辆路线,以满足所有客户的需求。在本书的后续章节中,我们将探讨使用随机方法解决 TSP 及其变体的几个示例。
问题类型
决策问题是算法复杂性研究的基础。一般来说,决策问题是一种需要确定给定输入是否满足某种属性或条件的类型的问题。这个问题可以用简单的“是”或“否”来回答。
决策问题通常根据其复杂度水平进行分类。这些类别也可以应用于优化问题,因为优化问题可以被转换为决策问题。例如,一个优化问题的目标是找到一个在可行搜索空间内的最优或近似最优解,可以改写为一个决策问题,回答的问题是“在可行搜索空间内是否存在最优或近似最优解?”答案将是“是”或“否”,或者“真”或“假”。
一个算法效率的普遍接受的观点是它的运行时间是多项式。这意味着解决问题所需的时间或计算成本可以用算法输入大小的多项式函数来描述。例如,在 TSP 的背景下,输入的大小通常是销售人员需要访问的城市数量。可以在多项式时间内解决的问题被称为可解的。以下图显示了不同类型的问题,并给出了常用基准(玩具问题)和每种类型的实际应用示例。

基于难度和完整性的问题类别。问题可以被分类为 NP-hard、NP-complete、NP 或 P。
例如,复杂性类 P 代表所有可以通过确定性算法(即不猜测解决方案的算法)在多项式时间内解决的问题。NP 或非确定性多项式问题是指解决方案难以找到但容易验证,并且可以通过非确定性算法在多项式时间内解决的问题。NP-complete 问题既是 NP-hard 又可以在多项式时间内验证的问题。最后,如果一个问题是 NP-hard,那么它至少与 NP-complete 中最困难的问题一样难。NP-hard 问题通常通过近似或启发式求解器来解决,因为找到解决此类问题的有效精确算法很困难。
聚类是一种组合问题,其解决方案的形式是一种组合,其中顺序不重要。在聚类中,给定 n 个对象,我们需要将它们分成 k 组(聚类),使得单个组或聚类中的所有对象彼此之间都有一个“自然”的关系,而不同组的对象在某种程度上是不同的。这意味着对象将根据某些相似性或差异性度量进行分组。
斯特林数可以用于计数组合问题中的划分和排列。第一类斯特林数根据它们的循环数来计数排列,而第二类斯特林数表示将一组对象划分成非空子集的方法数。以下公式是第二类斯特林数(即 斯特林划分数),它给出了在聚类问题的背景下,将 n 个对象划分成 k 个非空子集的方法数:
|

| 2.1 |
|---|
让我们以智能购物车聚类为例。购物和行李车在购物中心和大型机场中很常见。购物者或旅客在指定地点取走这些购物车,并将它们随意放置。重新收集它们是一项相当艰巨的任务,因此如果这些购物车能够自动聚集到最近的集合点,就像图 2.3 所示的那样,这将是有益的。

图 2.3 智能购物车聚类。未使用的购物或行李车聚集在指定的集合点,以便于收集和重新分配。
在实践中,这个问题被认为是一个 NP-hard 问题,因为基于可用购物车和集合点的数量,搜索空间可以非常大。为了有效地聚类这些购物车,必须找到聚类的中心(即 质心)。然后,每个聚类中的购物车将被引导到离质心最近的集合点。
例如,假设有 50 个车需要围绕四个装配点进行分组。这意味着 n = 50 和 k = 4。可以使用 SymPy 库生成斯特林数。要做到这一点,只需在两个数字 n 和 k 上调用 stirling 函数:
from sympy.functions.combinatorial.numbers import stirling
print(stirling(50,4))
print(stirling(100,4))
结果是 5.3 × 10²⁸,如果 n 增加到 100,这个数字变为 6.7 × 10⁵⁸。对于大问题,枚举所有可能的划分是不切实际的。
2.1.2 景观和目标函数的数量
目标函数的 景观 代表函数值在可行搜索空间中的分布。在这个景观中,你会找到最优解或全局最小值在最低谷,假设你处理的是一个最小化问题,或者在高峰处,如果是最大化问题。根据目标函数的景观,如果只有一个清晰的全球最优解,则问题为 单峰(例如,凸和凹函数)。在 多峰 问题中,存在多个最优解。当全局最小值位于一个非常狭窄的谷地,并且还存在一个具有宽吸引盆地的强局部最小值时,目标函数被称为 欺骗性,这样这个目标函数的值就接近全局最小值处的目标函数值 [3]。图 2.4 是使用 Python 在下一列表中生成的单峰、多峰和欺骗性函数的 3D 可视化。完整的列表可在本书的 GitHub 仓库中找到。
列表 2.1 目标函数的例子
import numpy as np
import math
import matplotlib.pyplot as plt
def objective_unimodal(x, y): ①
return x**2.0 + y**2.0
def objective_multimodal(x, y): ②
return np.sin(x) * np.cos(y)
def objective_deceptive(x, y): ③
return (1-(abs((np.sin(math.pi*(x-2))*np.sin(math.pi*(y-2)))/
➥ (math.pi*math.pi*(x-2)*(y-2))))**5)*(2+(x-7)**2+2*(y-7)**2)
fig = plt.figure(figsize = (25,25))
ax = fig.add_subplot(1,3,1, projection='3d')
x = np.arange(-3, 3, 0.01)
y = np.arange(-3, 3, 0.01)
X, Y = np.meshgrid(x, y)
Z = objective_unimodal(X, Y)
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.cividis)
ax.set_xlabel('x', fontsize=15)
ax.set_ylabel('y', fontsize=15)
ax.set_zlabel('Z', fontsize=15)
ax.set_title("Unimodal/Convex function", fontsize=18)
ax = fig.add_subplot(1,3,2, projection='3d')
Z = objective_multimodal(X, Y)
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.cividis)
ax.set_xlabel('x', fontsize=15)
ax.set_ylabel('y', fontsize=15)
ax.set_zlabel('Z', fontsize=15)
ax.set_title("Multimodal function", fontsize=18)
X, Y = np.meshgrid(x, y)
Z = objective_unimodal(X, Y)
ax = fig.add_subplot(1,3,3, projection='3d')
Z = objective_deceptive(X, Y)
surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.cividis, antialiased=False)
ax.set_xlabel('x', fontsize=15)
ax.set_ylabel('y', fontsize=15)
ax.set_zlabel('Z', fontsize=15)
ax.set_title("Deceptive function", fontsize=18)
plt.show()
① 单峰函数
② 多峰函数
③ 欺骗性函数

图 2.4 单峰、多峰和欺骗性函数。单峰函数有一个全局最优解,而多峰函数可以有多个。欺骗性函数包含接近全局最小值处目标函数值的虚假最优解,这可能导致某些算法陷入困境。
如果要优化的量仅使用一个目标函数来表示,则该问题被称为单目标或单目标优化问题(例如凸或凹函数)。多目标优化问题指定了要同时优化的多个目标。没有显式目标函数的问题称为约束满足问题(CSPs)。在这种情况下,目标是找到一个满足给定约束集的解。
n 后宫问题是一个约束满足问题的例子。在这个问题中,目标是把 n 个皇后放在 n × n 的棋盘上,且没有两个皇后在同一行、列或对角线上,如图 2.5 所示。在这个 4 皇后问题中,初始状态下有 5 个冲突({Q1,Q2}, {Q1,Q3}, {Q2,Q3}, {Q2,Q4}, 和 {Q3,Q4})。移动 Q4 后,冲突数减少 2,移动 Q3 后,冲突数只有 1,这是 Q1 和 Q2 之间的冲突。

图 2.5 n-后问题。这个问题没有目标函数,只有必须满足的一组约束。
如果我们继续移动或放置棋子,我们可以达到一个目标状态,其中冲突的数量为 0,这意味着没有一枚棋后可以攻击任何其他棋后,无论是水平、垂直还是对角线方向。下面的列表展示了 4 后问题的 Python 实现。
列表 2.2 n-后 CSP
from copy import deepcopy
import math
import matplotlib.pyplot as plt
import numpy as np
board_size = 4
board = np.full((board_size, board_size), False) ①
def can_attack(board, row, col):
if any(board[row]): ②
return True ②
offset = col - row ③
if any(np.diagonal(board, offset)): ③
return True ③
offset = (len(board) - 1 - col) - row ③
if any(np.diagonal(np.fliplr(board), offset)): ③
return True ③
return False
board[0][0] = True
col = 1
states = [deepcopy(board)]
while col < board_size:
row = 0
while row < board_size:
if not can_attack(board, row, col): ④
board[row][col] = True
col += 1
states.append(deepcopy(board))
break
row += 1
if row == board_size: ⑤
board = np.delete(board, 0, 1)
new_col = [[False]] * board_size
board = np.append(board, new_col, 1)
states.append(deepcopy(board))
col -= 1
continue
① 创建一个 n x n 棋盘。
② 检查同一行的棋后。
③ 检查对角线上的棋后。
④ 棋子可以放置在这个列中。
⑤ 棋子不能放置在这个列中。
在前面的列表中,can_attack函数检测新放置的棋子是否可以攻击之前放置的棋子。如果一枚棋子在同一行、同一列或同一对角线上,它可以攻击另一枚棋子。图 2.6 显示了六步后获得的解决方案。

图 2.6 n-后解决方案
第一枚棋子可以简单地放在第一个位置。第二枚棋子必须放在第三个或第四个位置,因为前两个位置可以被攻击。然而,将棋子放在第三个位置时,第三枚棋子就无法放置。因此,第一枚棋子被移除(棋盘“滑动”一列),然后我们再次尝试。这个过程会一直持续到找到解决方案。
该问题的完整代码,包括用于生成可视化的代码,可以在列表 2.2 的代码文件中找到,该文件位于书的 GitHub 仓库中。解决方案算法如下:
-
在列中从上到下移动,算法尝试放置棋子同时避免冲突。对于第一列,这默认为 Q1 = 0。
-
移动到下一列,如果棋子不能放在第 0 行,它将被放在第 1 行,依此类推。
-
当放置一枚棋子后,算法移动到下一列。
-
如果在给定的列中无法放置棋子,整个棋盘的第一列将被移除,然后重新尝试当前列。
Google OR-Tools 中可用的约束编程求解器也可以用来解决这个n × n后问题。下面的列表展示了使用 OR-Tools 的解决方案步骤。
列表 2.3 使用 OR-Tools 解决n-后问题
import numpy as np
import matplotlib.pyplot as plt
import math
from ortools.sat.python import cp_model ①
board_size = 4
②
model = cp_model.CpModel() ③
queens = [model.NewIntVar(0, board_size - 1, 'x%i' % i)
➥for i in range(board_size)] ④
model.AddAllDifferent(queens) ⑤
model.AddAllDifferent(queens[i] + i for i in range(board_size))
model.AddAllDifferent(queens[i] - i for i in range(board_size))
solver = cp_model.CpSolver() ⑥
solver.parameters.enumerate_all_solutions = True ⑥
solver.Solve(model) ⑥
all_queens = range(board_size) ⑦
state=[]
for i in all_queens:
for j in all_queens:
if solver.Value(queens[j]) == i:
# There is a queen in column j, row i.
state.append(True)
else:
state.append(None)
states=np.array(state).reshape(-1, board_size)
fig = plt.figure(figsize=(5,5)) ⑧
markers = [ ⑧
x.tolist().index(True) if True in x.tolist() else None ⑧
for x in np.transpose(states) ⑧
] ⑧
res = np.add.outer(range(board_size), range(board_size)) % 2 ⑧
plt.imshow(res, cmap="binary_r") ⑧
plt.xticks([]) ⑧
plt.yticks([]) ⑧
plt.plot(markers, marker="*", linestyle="None", ⑧
➥markersize=100/board_size, color="y")H ⑧
① 导入使用 SAT(可满足性)方法的约束编程求解器。
② 为 n x n 后问题设置棋盘大小。
③ 定义求解器。
④ 定义变量。数组索引表示列,值表示行。
⑤ 定义约束:所有行必须不同。
⑥ 解决模型。
⑦ 定义约束:没有两枚棋后可以位于同一对角线上。
⑧ 可视化解决方案。
运行此代码会产生图 2.7 中的输出。有关 Google OR-Tools 的更多信息,请参阅附录 A。

图 2.7 使用 OR-Tools 的n-后解决方案
2.1.3 约束
约束问题具有等式、不等式或两者的硬约束或软约束。硬约束必须满足,而软约束则很好满足(但不是强制性的)。如果没有需要考虑的约束,除了边界约束之外,问题就是一个无约束优化问题。
让我们回顾一下在 1.3.1 节中引入的门票定价问题。Python 中有许多基于导数的求解器可以处理这类可微数学优化问题(参见附录 A)。下面的列表显示了如何使用 SciPy 解决这个简单的门票定价问题。SciPy 是一个包含所有计算工具的宝贵库。
列表 2.4 门票定价优化
import numpy as np
import scipy.optimize as opt
import matplotlib.pyplot as plt
def f(x): ①
return -(-20*x**2+6200*x-350000)/1000
res=opt.minimize_scalar(f, method='bounded', bounds=[0, 250]) ②
print("Optimal Ticket Price ($): %.2f" % res.x)
print("Profit f(x) in K$: %.2f" % -res.fun)
① 目标函数,由 minimize_scalar 所需的最小化函数
② 有界方法是寻找解的约束最小化过程。
运行此代码会产生以下输出:
Optimal Ticket Price ($): 155.00
Profit f(x) in K$: 130.50
此代码在 0 到 250 美元的范围内找到最优门票价格,以最大化利润。如您所注意到的,利润公式通过在目标函数中添加负号转换为最小化问题,以匹配 scipy.optimize 中的 minimize 函数。在 print 函数中添加负号,将其转换回利润。
如果我们对这个问题施加一个等式约束会怎样呢?假设由于对我们活动的国际需求难以置信,我们现在正在考虑使用不同的活动策划公司,并为我们的会议开放虚拟参会,以便国际客人也能参加。感兴趣的参与者现在可以选择亲自参加活动或通过直播加入。所有参与者,无论是亲自参加还是虚拟参加,都将收到一个实物欢迎礼包,该礼包限量 10,000 份。因此,为了确保活动“满员”,我们必须要么卖出 10,000 张亲自参加的门票,要么卖出 10,000 张虚拟门票,或者两者的组合。新的活动公司对我们活动的收费是 100 万美元的固定费用,因此我们希望卖出尽可能多的门票(正好是 10,000 张)。与这个问题相关联的以下方程式:
设 x 为实物门票销售数量,设 y 为虚拟门票销售数量。此外,设 f(x,y) 为从活动产生的利润函数,其中
|

| 2.2 |
|---|
实际上,我们通过亲自参加获得 155 美元的利润,而在线参加的利润是 70 美元,但随着我们拥有的实物参加人数的增加而增加(让我们假设,当活动看起来“更拥挤”时,我们可以对在线参与者收取更高的费用)。
假设我们添加一个约束函数,x + y ≤ 10000,这表明组合票销售不能超过 10,000。现在问题变成了一个双变量单目标约束优化问题。可以使用拉格朗日乘数 λ 将这个约束优化问题转换为无约束优化。我们可以使用 SymPy 实现拉格朗日乘数并求解虚拟和实物票销售的优化组合。思路是将由目标函数 f(x,y) 和等式约束 g(x,y) 定义的约束优化问题转换为使用拉格朗日函数 L(x,y,λ) = f(x,y) + λg(x,y) 的无约束优化问题。这个函数结合了目标函数和约束条件,使得可以通过使用拉格朗日乘数将约束优化问题表述为无约束问题。为此,我们取目标函数和约束条件关于决策变量 x 和 y 的偏导数,以形成 SymPy 求解器使用的无约束优化方程,如图 2.8 所示。

图 2.8 使用拉格朗日方法解决票价问题的步骤
下一个列表展示了使用 SymPy 的 Python 实现。
列表 2.5 使用拉格朗日乘数最大化利润
import sympy as sym
x,y=sym.var('x, y', positive=True) ①
f=155*x+(0.001*x**sym.Rational(3,2)+70)*y-1000000 ②
g=x+y-10000 ③
lamda=sym.symbols('lambda') ④
Lagr=f-lamda*g ⑤
eqs = [sym.diff(Lagr, x), sym.diff(Lagr, y), g] ⑥
sol=sym.solve(eqs,[x,y,lamda], dict=True) ⑦
def getValueOf(k, L):
for d in L:
if k in d:
return d[k]
profit=[f.subs(p) for p in sol]
print("optimal number of physical ticket sales: x = %.0f" % getValueOf(x, sol))
print("optimal number of online ticket sales: y = %.0f" % getValueOf(y, sol))
print("Expected profil: f(x,y) = $%.4f" % profit[0])
① 定义决策变量。
② 定义票价目标函数。
③ 定义等式约束。
④ 拉格朗日乘数
⑤ 拉格朗日函数
⑥ 方程式求解器
⑦ 使用 SymPy 在三个变量(x,y,lambda)中求解这三个方程。
通过求解前面的三个方程,我们得到 x 和 y 的值,这些值对应于虚拟和实物票销售的优化数量。通过列表 2.5 中的代码,我们可以看到最佳结果是销售 6,424 张现场票和 3,576 张在线票。这导致最大利润为 $2,087,260。
2.1.4 目标函数和约束条件的线性
如果所有目标函数和相关约束条件都是线性的,则优化问题被归类为 线性优化问题 或 线性规划问题(LPP 或 LP),其目标是找到在线性约束下线性函数的最优值。混合问题是混合整数线性规划(MILP)的典型应用,其中需要将多种成分混合或混合以获得具有特定特性或属性的产品。在保罗·詹森的《运筹学模型与方法》[4]中描述的动物饲料混合问题中,需要确定动物饲料混合中三种成分的最佳数量。可能的成分、它们的营养含量(每千克成分的营养千克数)和单位成本如表 2.1 所示。
表 2.1 动物饲料混合问题
| 成分 | 成分营养含量和价格 |
|---|---|
| 钙(kg/kg) | 蛋白质(kg/kg) |
| --- | --- |
| 玉米 | 0.001 |
| 石灰石 | 0.38 |
| 大豆粕 | 0.002 |
混合物必须满足以下限制条件:
-
钙含量——至少 0.8%,但不超过 1.2%
-
蛋白质——至少 22%
-
纤维——最多 5%
问题是要找到满足这些约束条件的同时最小化成本的最佳混合物。决策变量是 x[1]、x[2] 和 x[3],分别代表石灰石、玉米和大豆粕的比例。
目标函数 f = 30.5x[1] + 10x[2] + 90x[3] 需要被最小化,同时满足以下约束条件:
-
钙含量限制:0.008 ≤ 0.001x[1] + 0.38x[2] + 0.002x[3] ≤ 0.012
-
蛋白质约束:0.09x[1] + 0.5x[3] ≥ 0.22
-
纤维约束:0.02x[1] + 0.08x[3] <= 0.05
-
非负性限制:x[1]、x[2]、x[2] ≥ 0
-
保守性:x[1] + x[2] + x[2] = 1
在这个问题中,目标函数和约束条件都是线性的,因此它是一个线性规划问题。有几个 Python 库可以用来解决数学优化问题。
我们将尝试使用 PuLP 解决动物饲料混合问题。PuLP 是一个 Python 线性规划库,允许用户定义线性规划问题并使用优化算法(如 COIN-OR 的线性整数规划求解器)来解决问题。有关 PuLP 和其他数学规划求解器的更多信息,请参阅附录 A。下一个列表显示了使用 PuLP 解决动物饲料混合问题的步骤。
列表 2.6 使用 PuLP 解决线性规划问题
from pulp import *
model = LpProblem("Animal_Feed_Mix_Problem", LpMinimize) ①
x1 = LpVariable('Corn', lowBound = 0, upBound = 1, cat='Continous') ②
x2 = LpVariable('Limestone', lowBound = 0, upBound = 1, cat='Continous') ②
x3 = LpVariable('Soybean meal', lowBound = 0, upBound = 1, cat='Continous') ②
model += 30.5*x1 + 10.0*x2 + 90*x3, 'Cost' ③
model +=0.008 <= 0.001*x1 + 0.38*x2 + 0.002*x3 <= 0.012, 'Calcium limits' ④
model += 0.09*x1 + 0.5*x3 >=0.22, 'Minimum protein' ④
model += 0.02*x1 + 0.08*x3 <=0.05, 'Maximum fiber' ④
model += x1+x2+x3 == 1, 'Conservation' ④
model.solve() ⑤
for v in model.variables(): ⑥
print(v.name, '=', round(v.varValue,2)*100, '%') ⑥
⑥
print('Total cost of the mixture per kg = ', ⑥
➥round(value(model.objective)/100, 2), '$') ⑥
① 创建一个线性规划模型。
② 定义三个变量,代表混合物中玉米、石灰石和大豆粕的百分比。
③ 将总成本定义为要最小化的目标函数。
④ 添加约束条件。
⑤ 使用 PuLP 的选择求解器解决问题。
⑥ 打印结果(原料的最佳百分比和每公斤混合物的成本)
如此列表所示,我们首先导入 PuLP 并创建一个作为线性规划问题的模型。然后定义与每个变量相关的参数,例如变量的名称、范围的下限和上限以及变量的类型(例如,整数、二元或连续)。然后使用求解器来解决问题。PuLP 支持多个求解器,如 GLPK、GUROBI、CPLEX 和 MOSEK。PuLP 的默认求解器是 Cbc(COIN-OR 分支和切割)。运行此代码将给出以下输出:
Corn = 65.0%
Limestone = 3.0%
Soybean_meal = 32.0%
Total cost of the mixture per kg = 0.4916$
如果目标函数中至少有一个或约束中至少有一个是非线性的,则问题被认为是非线性优化问题或非线性规划问题(NLP),比线性问题更难解决。当目标函数是二次函数时,NLP 的一个特殊情况称为二次规划(QP)。例如,植物布局问题(PLP)或设施定位问题(FLP)是一个二次分配问题(QAP),旨在将不同的设施(部门)F分配到不同的位置L,以最小化给定的函数成本,如图 2.9 所示。

图 2.9 植物布局问题——每个部门的最优位置是什么,以最小化整体物料处理成本?
假设ω[ij]是这些设施之间交互或产品流动的频率,或者d[f(i)f(j)]是设施i和j之间的距离。物料处理成本(MHC)是
| MHC[ij] = flow × distance = 𝜔[ij] × d[f][(][i][)][f][(][j][)] | 2.3 |
|---|
总物料处理成本(TMHC)是物料处理成本矩阵内部所有物料处理成本的累加。在矩阵表示法中,问题可以表示为
找到X,使其最小化trace(WXDX^T)
其中X代表分配向量,W是流量矩阵,D是距离矩阵。迹是结果物料处理成本矩阵主对角线上(从左上角到右下角)元素的总和。
在更一般的情况下,NLP 包括任何形式的非线性目标函数或至少非线性约束。例如,想象你正在设计一个地雷探测和销毁无人地面车辆(UGV)[5]。在户外应用,如人道主义排雷中,UGV 应能够在崎岖地形中导航。沙质土壤、有障碍物的岩石地形、陡峭的斜坡、沟渠和涵洞可能对车辆造成困难。这类车辆的移动系统需要精心设计,以确保运动流畅。
假设你负责寻找轮参数(例如,直径、宽度和负载)的最优值,这将
-
最小化车轮下陷,这是车轮在其移动的土壤中下沉的最大量。
-
最小化运动阻力,这是 UGV 单位由于不同阻力成分(压实、重力等)所面临的总体阻力。
-
最小化驱动扭矩,这是每个车轮所需的驱动扭矩。
-
最小化驱动功率,这是每个车轮所需的驱动功率。
-
最大化爬坡可接受性,这代表 UGV 单位在考虑其重量和土壤参数的情况下能够爬上的最大坡度。
由于市场供应或制造方面的考虑和成本,轮径应在 4 至 8.2 英寸的范围内,轮宽应在 3 至 5 英寸的范围内,轮载应在每轮 22 至 24 磅的范围内。这个轮设计问题(图 2.10)可以表述如下:
找到 X,使其优化 ƒ,同时满足一组可能的边界约束,其中 X 是由多个决策变量组成的向量,例如
-
x[1] = 轮径,x[1] ∈ [4, 8.2]
-
x[2] = 轮宽,x[2] ∈ [3, 5]
-
x[3] = 轮载,x[2] ∈ [22, 24]
我们还可以考虑目标函数 ƒ={ƒ[1],ƒ[2],…}。例如,轮沉降的函数可能看起来像这样:
|

| 2.4 |
|---|
其中 n 是沉降的指数,k[c] 是土壤变形的粘聚力模量,k[φ] 是土壤变形的摩擦模量。这个问题被认为是非线性的,因为目标函数是非线性的。

图 2.10 MineProbe 轮设计问题 [5]
Veselić 在“有限悬链线和拉格朗日方法”文章 [6] 中讨论的悬链线问题是一个非线性优化问题的另一个例子。悬链线是由多个部分组成的柔性悬挂物体,如链条或电话线(图 2.11)。在这个问题中,我们提供了 n 个同质梁,长度为 d[1],d[2],… d[n] > 0,质量为 m[1],m[2],… m[n] > 0,它们通过 n + 1 个节点 G[0],G[2],… G[n] [+ 1] 连接。每个节点的位置由笛卡尔坐标 (x[i],y[i],z[i]) 表示。悬链线的两端是 G[0] 和 G[n] [+ 1],它们都具有相同的 y 和 z 值(它们在同一高度上,并且彼此对齐)。

图 2.11 有限悬链线问题——悬链线(或链条)从两个点 G[0] 和 G[n] [+ 1] 悬挂。
假设梁长和质量是预定义的参数,我们的目标是寻找重力场中的稳定平衡位置——那些使势能最小化的位置。要最小化的势能定义如下:
|

| 2.5 |
|---|
满足以下约束条件:
|

| 2.6 |
|---|
其中 γ 是重力常数。尽管目标函数是线性的,但约束的非线性使得这个问题是非线性的。
2.1.5 解决方案的期望质量和允许时间
优化问题也可以根据期望的解决方案质量和找到解决方案所允许的时间进行分类。图 2.12 展示了三种主要类型的问题:设计问题(战略函数)、计划问题(战术函数)和控制问题(操作函数)。

图 2.12 解决方案的质量与搜索时间的关系。某些类型的问题需要快速计算,但不需要极其精确的结果,而其他问题(如设计问题)则可以通过更长的处理时间来换取更高的精度。
在设计问题中,时间并不像解决方案的质量那样重要,用户愿意等待(有时甚至几天)以获得最优或近似最优的结果。这些问题可以在离线状态下解决,并且优化过程通常在很长时间内只进行一次。设计问题的例子包括车辆设计、课程安排、资产分配、资源规划、生产线平衡、库存管理、航班安排和政治区域划分。
让我们更详细地讨论政治区域划分作为设计问题。区域划分是将称为基本单元的小地理区域分组到称为区域的较大地理集群中的问题,以便后者根据相关规划标准[7]是可接受的。基本单元的典型例子是客户、街道或邮政编码区域。规划标准可能包括以下内容:
-
在人口背景、公平规模、平衡工作量、相等销售潜力或客户数量方面的平衡或公平
-
连续性,以便在无需离开区域的情况下在区域的基本单元之间旅行
-
紧凑性,以便在不产生空洞的情况下允许圆形或方形无畸变的区域
-
尊重边界,例如行政边界、铁路、河流或山脉
-
社会经济异质性,以便更好地代表不同收入、民族、关注点或观点的居民
政治区域划分、学区划分、健康服务区域划分、电动汽车充电站区域划分、微型移动站点(例如,电动自行车和电动滑板车)区域划分以及销售或配送区域划分都是区域划分问题的例子。
政治区域划分是自罗马共和国代表民主制度出现以来一直困扰着社会的问题。在代表民主制度中,官员被提名和选举出来代表选举他们的民众的利益。为了在决定涉及整个国家的事务时拥有更大的发言权,出现了政党制度,该制度定义了候选人用来与竞争对手区分自己的政治平台。操纵选举区的形状以决定选举结果的行为被称为操纵选区(以 19 世纪初的马萨诸塞州州长 Elbridge Gerry 命名,他在 1810 年重新绘制了参议院的区域地图,以削弱反对联邦党的力量)。图 2.13 显示了如何通过操纵区域的形状来使投票偏向于一个本不会获胜的决定。

图 2.13 选举操纵的例子。两个主要政党,盾牌和钟铃,试图通过操纵选区边界来获得优势,压制不希望的利益并促进自己的利益。
需要一个有效且透明的政治选区划分策略,以避免选举操纵并生成一个在可重复的方式下保护个别子区完整性并将人口划分为几乎相等的投票人口的解决方案。在许多国家,选区会定期审查,以反映国家人口的变化和流动。例如,加拿大的宪法要求在每 10 年的人口普查后审查联邦选区。
政治选区划分是指将一个领土的 n 个子区域聚合为 m 个选区,并受到诸如
-
选区应具有近乎相等的投票人口。
-
每个选区内的社会经济同质性以及不同社区的完整性应最大化。
-
选区必须紧凑,每个选区的子区域必须连续。
-
应将子区域视为不可分割的政治单位,并尊重其边界。
该问题可以表述为一个优化问题,其中最大化一个量化上述因素的函数。以下是这个函数的一个例子:
| F(x) = α[pop]ƒpop + α[comp]ƒcomp + α[soc]ƒsoc + α[sim]ƒsim | 2.7 |
|---|
其中 x 是问题的解决方案或选区,α[i] 是用户指定的乘数 0 ≤ α[i] ≤ 1,而 ƒ[pop]、ƒ[comp]、ƒ[soc]、ƒ[int] 和 ƒ[sim] 是量化人口平等、选区紧凑性、社会经济同质性、不同社区完整性和与现有选区的相似性的函数。在接下来的章节中,我将向您展示我们如何使用离线优化算法来处理最优多标准分配设计问题。
规划问题需要比设计问题更快地解决,时间跨度从几秒到几分钟。要在如此短的时间内找到解决方案,通常需要牺牲最优性以换取速度。规划问题的例子包括车辆运动规划、紧急车辆调度和路线规划、患者入院安排、手术安排以及机组人员安排。让我们以拼车问题作为一个规划问题的例子。
拼车涉及一支按使用付费的车辆车队和一组具有预定义的接车和下车点的乘客(图 2.14)。调度服务需要按照特定顺序为每位司机分配一组乘客,以实现一系列目标。这个拼车问题是一个多目标约束优化问题。拼车优化的非详尽列表包括
-
最小化驾驶员行程的总旅行距离或时间
-
最小化乘客行程的总旅行时间
-
最大化匹配(服务)请求的数量
-
最小化司机行程的成本
-
最小化乘客行程的成本
-
最大化司机的收入
-
最小化乘客的等待时间
-
最小化所需的司机总数

图 2.14 共享出行问题——这个规划问题需要在更短的时间内解决,因为延误可能导致行程丢失和糟糕的用户体验。
对于共享出行问题,搜索时间和解决方案的质量都很重要。在许多流行的共享出行平台上,数十甚至数百名用户可能同时在同一地区的同一地点寻找乘车。过于昂贵和耗时解决方案会导致更高的运营成本(即雇佣比必要的更多司机或从其他地区调用司机)以及潜在的业务损失(糟糕的用户体验可能会阻止乘客再次使用该平台)和高司机流失率。
在实践中,司机分配给乘客的任务远远超出了乘客与司机之间的距离——它可能还包括诸如司机可靠性、乘客评分、车辆类型以及接车和目的地位置类型等因素。例如,前往机场的客户可能要求一辆更大的车辆来容纳行李。在接下来的章节中,我们将讨论如何使用不同的搜索和优化算法来解决规划问题。
控制问题需要非常快速的实时解决方案。在大多数情况下,这意味着从毫秒到几秒的时间跨度。车辆横向或纵向运动控制、手术机器人运动控制、中断管理以及临时通信中继都是控制问题的例子。需要在线优化算法来处理这些问题。规划和控制问题中的优化任务通常需要重复执行——例如,新订单将不断到达生产设施,需要以最小化所有工作的等待时间为方式安排到机器上。
想象一个现实世界的情况,其中一群无人机(UAV)或微型空中车辆(MAV)被部署在自然灾害(如地震、雪崩、海啸、龙卷风、野火等)后搜索被困在不可通行的地形上的受害者。任务分为两个阶段:搜索阶段和接力阶段。在搜索阶段,MAV 将根据部署算法进行搜索。当发现目标时,MAV 群将自我组织,利用它们有限的通信能力,在受害者和基站之间建立一个临时的通信中继网络,如图 2.15 所示。

图 2.15 通信中继问题——一群微型飞行器必须在基站和被困受害者之间形成一个临时的通信中继。微型飞行器的移动是一个需要反复解决的控制问题,每秒可能需要多次。在这种情况下,速度比精度更重要,因为小错误可以在下一个周期立即纠正。
在搜索阶段,微型飞行器可以被部署以最大化覆盖面积。一旦它们检测到受害者,微型飞行器可以被重新定位以最大化受害者的可见性。随后建立临时的通信中继网络,以最大化群体中的无线电覆盖范围,并找到检测到受害者的微型飞行器和基站之间的最短路径,前提是以下假设:
-
微型飞行器可以通过结合来自三个易受噪声干扰的传感器的数据来实现态势感知:一个指南针用于方向,一个速度表用于速度,一个高度计用于高度。
-
微型飞行器可以通过 IEEE 802.11b 等标准协议进行通信,其通信范围有限,约为 100 米。
-
微型飞行器能够中继地面信号,以及控制微型飞行器之间发送的信号。
-
微型飞行器(MAVs)拥有足够的机载电力来维持 30 分钟的连续飞行,在此之后它们必须返回基地进行充电。然而,飞行时间取决于飞行过程中完成的信号量。
-
微型飞行器能够迅速加速到恒定的飞行速度 10 米/秒。
-
微型飞行器不能悬停,并且最小转弯半径约为 10 米。
对于如微型飞行器重新定位这样的控制问题,搜索时间至关重要。由于微型飞行器不能悬停,因此必须保持持续运动,延迟的决策可能导致意外情况,例如空中碰撞或信号丢失。由于指令每几毫秒就会发送(或重复),每个微型飞行器必须能够在那个时间段内决定其下一步行动。微型飞行器不仅要考虑其当前位置、目标位置和速度,还必须考虑障碍物、通信信号强度、风速和其他环境因素。小错误是可以接受的,因为它们可以在后续搜索中纠正。在接下来的章节中,我们将讨论如何解决这类控制问题。
本书将主要关注复杂、结构不良的问题,这些问题无法通过传统的数学优化或基于导数的求解器来处理。我们将探讨各个领域中的设计、规划和控制问题的例子。接下来,让我们看看搜索和优化算法是如何进行分类的。
2.2 对搜索和优化算法进行分类
当我们进行搜索时,我们试图检查不同的状态,以找到从起始(初始)状态到目标状态的一条路径。通常,优化算法通过迭代地将当前状态或候选解转换成一个新的、希望更好的解来寻找最优解。搜索算法可以根据探索搜索空间的方式分类:
-
局部搜索 仅使用关于当前解周围搜索空间的信息来产生新的解。由于只使用局部信息,局部搜索算法(也称为局部优化器)定位局部最优解(这些解可能是或可能不是全局最优解)。
-
全局搜索 使用更多关于搜索空间的信息来定位全局最优解。
换句话说,全局搜索算法会探索整个搜索空间,而局部搜索算法只利用邻域信息。
另一种分类区分了确定性和随机算法,如图 2.16 所示:
-
确定性算法 在其路径上遵循严格的程序,它们的设计变量值和函数都是可重复的。从相同的起始点出发,无论你今天还是明天运行程序,它们都会遵循相同的路径。例子包括但不限于图形方法、基于梯度和 Hessian 的方法、惩罚方法、梯度投影方法和图搜索方法。图搜索方法可以进一步细分为盲搜索方法(例如,深度优先、广度优先或 Dijkstra)和信息搜索方法(例如,爬山法、束搜索、最佳优先、A*或收缩层次)。确定性方法在本书的第一部分有所介绍。
-
随机算法在它们的参数或决策过程中明确使用随机性,或者两者都使用。例如,遗传算法使用一些随机数或伪随机数,导致个体路径不可精确重复。使用随机算法,获得最优解所需的时间无法准确预测。解决方案并不总是变得更好,随机算法有时会错过找到最优解的机会。然而,这种行为可能是有利的,因为它可以防止它们陷入局部最优。随机算法的例子包括禁忌搜索、模拟退火、遗传算法、差分进化算法、粒子群优化、蚁群优化、人工蜂群、萤火虫算法等。大多数统计机器学习算法都是随机的,因为它们在学习的阶段利用随机性,并在推理阶段以一定的不确定性进行预测。此外,一些机器学习模型,就像人一样,是不可预测的。使用基于人类行为数据作为独立变量的模型比使用严格遵循物理定律的独立变量训练的模型更有可能不可预测。例如,人类意图识别模型比预测材料应力-应变曲线的模型更不可预测。由于机器学习预测的不确定性,用于解决优化问题的基于机器学习的算法可以被认为是随机方法。本书的第 2 至 5 部分涵盖了随机算法。

图 2.16 确定性算法与随机算法。确定性算法遵循一套既定的程序,其结果是可重复的,而随机搜索算法则将随机性元素构建到算法中。
寻宝任务
在给定的搜索空间中寻找最优解可以比作寻宝任务。想象一下你和一群朋友决定去一个岛屿寻找海盗宝藏。
岛上的所有区域(除了活跃火山区域)都对应着优化问题的可行搜索空间。宝藏对应于这个可行空间中的最优解。你和你的朋友们是“搜索代理”,被派去寻找解决方案,每个人遵循不同的搜索方法。如果你在搜索过程中没有任何可以引导你的信息,你就是在遵循一种盲目的(无信息的)搜索方法,这通常效率低下且耗时。如果你知道海盗们过去习惯于在高地藏宝,那么你就可以直接爬上最陡峭的悬崖,尝试到达最高的山峰。这种情况对应于经典的爬山法(信息搜索)。无信息和有信息搜索算法将在下一章中介绍。你也可以采取试错的方法,寻找线索,并反复从一个可能的地方移动到另一个可能的地方,直到找到宝藏。这对应于基于轨迹的搜索,我们将在本书的第二部分讨论。
如果你不想冒一无所获的风险,并决定与朋友们分享信息而不是独自寻宝,你将遵循一种基于群体的搜索方法。在团队合作中,你可能会注意到一些寻宝者比其他人表现更好。在这种情况下,只有表现更好的寻宝者才能保留,新的寻宝者可以被招募来替换表现较差的寻宝者。这类似于进化算法,如遗传算法,其中最适应的寻宝者生存下来。遗传算法将在本书的第三部分介绍。或者,你和你的其他朋友们可以尝试模仿在宝藏岛每个区域中表现优异的寻宝者的成功,而不需要淘汰任何团队成员,也不需要招募新的成员。这种情况使用所谓的群体智能,对应于基于群体的优化算法,如粒子群优化、蚁群优化和人工蜂群算法。这些算法将在本书的第四部分讨论。
你可以独自一人,或者在你的朋友们的帮助下,基于之前和类似的寻宝任务的历史数据建立一个心理模型,或者你可以基于与宝藏岛(搜索空间)的试错交互训练一个奖励预测器,以金属探测器信号的强度作为奖励指标。经过几次迭代后,你将学会最大化预测器的奖励,并改进你的行为,直到你达到预期的目标并找到宝藏。这对应于基于机器学习的方法,我们将在本书的第五部分讨论。
2.3 启发式和元启发式
启发式方法(也称为思维捷径或经验法则)是解决方案策略、寻求方法或规则,旨在在实用时间内帮助找到复杂问题的可接受(最优或近似最优)解决方案。尽管启发式方法可以在合理的计算成本下寻求近似最优解,但它们不能保证可行性或最优程度。
“欧拉!欧拉!”
词语启发式来源于希腊语单词heuriskein,意为“找到或发现”。这个动词的过去式eureka被古希腊数学家、物理学家、工程师、天文学家和发明家阿基米德使用。阿基米德受雇于检测金冠制造中的欺诈行为,并接受了挑战。在随后的公共浴场访问中,他得到了启示。当他身体浸入水中时,他观察到他下沉得越多,排开的水就越多,这为他体积提供了一个精确的测量。意识到起作用的原理,他推断出含有银的皇冠,由于密度低于纯金,需要更大的体积才能匹配纯金皇冠的重量。因此,它会排开更多的水。认识到解决方案后,阿基米德从浴缸中跳出来,急忙回家,大声喊道“欧拉!欧拉!”这翻译成“我找到了!我找到了!”
元启发式这个术语是由两个希腊词组合而成的:meta,意为“超越,在更高层次上”,和heuristics。这是由禁忌搜索(在第六章中讨论)的发明者 Fred Glover 创造的术语,用来指代用于指导并修改其他启发式方法以提高其性能的高级策略。元启发式的目标是高效地探索搜索空间,以找到最优或近似最优解。元启发式可能包含机制,以在探索(多样化)和利用(强化)搜索空间之间取得平衡,以避免陷入搜索空间的局限区域,同时也在合理的时间内找到最优或近似最优解。在启发式中找到这种探索和利用的平衡是至关重要的,如第 1.5 节所述。元启发式算法通常是全局优化器,可以应用于不同的线性和非线性优化问题,对特定问题的修改相对较少。这些算法通常很鲁棒,可以处理不同的问题规模、问题实例和随机变量。
假设我们有 6 个不同尺寸的对象(2, 4, 3, 6, 5 和 1),我们需要将它们装入尽可能少的箱子中。每个箱子的大小有限,为 7,因此箱子中对象的总体积应不超过 7。如果我们有n个对象,那么有n!种可能的装箱方式。我们需要的最小箱子数是下界。为了计算这个下界,我们需要找到所有对象尺寸的总和(2 + 4 + 3 + 6 + 5 + 1 = 21)。下界是 21 / 7 = 3 个箱子。这意味着我们需要至少 3 个箱子来装这些对象。图 2.17 说明了可以用来解决这个盒子装填问题的两种启发式方法。

图 2.17 使用首次适应和首次适应递减启发式方法处理盒子装填问题
首次适应启发式方法按照对象的顺序装箱,不考虑它们的大小。这导致需要四个箱子,其中三个箱子未充分利用,因为这三个箱子中还有七个空间。如果我们应用首次适应递减启发式方法,我们将根据对象的大小对它们进行排序,并按照这个顺序装箱。这个启发式方法允许我们将所有对象装入三个完全利用的箱子中,这是下界。
在前一个例子中,所有对象的高度都是相同的。然而,在更通用的版本中,让我们考虑具有不同宽度和高度的对象,如图 2.18 所示。应用如“先小后大”这样的启发式方法可以使我们更快地装载容器。一些启发式方法并不保证最优解;例如,最大的先启发式方法给出的是一个次优解,因为有一个对象被遗漏了。如果我们需要将所有对象装入容器,这可以被认为是一个不可行解;如果目标是装入尽可能多的对象,那么它将是一个次优解。

图 2.18 盒子装填问题。使用启发式方法可以比暴力方法更快地解决问题。然而,某些启发式函数可能会导致不可行或次优解,并且它们不能保证最优解。
要在 Python 中解决这个问题,我们首先定义对象、容器以及将对象放入容器中的含义。为了简化,以下列表避免了自定义类,并使用numpy数组。
列表 2.7 盒子装填问题
import numpy
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.colors import rgb2hex
width = 4 ①
height = 8 ①
container = numpy.full((height,width), 0) ①
objects = [[3,1],[3,3],[5,1],[4,2],[3,2]] ②
def fit(container, object, obj_index, rotate=True): ③
obj_w = object[0] ③
obj_h = object[1] ③
for i in range(height - obj_h + 1): C ③
for j in range(width - obj_w + 1): ③
placement = container[i : i + obj_h, j : j + obj_w] ③
if placement.sum() == 0: ③
container[i : i + obj_h, j : j + obj_w] = obj_index ③
return True ③
return fit(container, object[::-1], obj_index, rotate=False) ③
① 定义容器的尺寸,并将 numpy 数组初始化为 0。
② 将要放置的对象表示为[宽度,高度]。
③ fit 函数将对象放入容器中,通过直接放置、移动或旋转。
fit函数尝试将一个值写入容器的 2D 切片中,前提是该切片中没有其他值(总和为 0)。如果失败,它将从容器顶部到底部、从左到右移动,并再次尝试。作为最后的手段,它尝试以 90 度旋转物体进行同样的操作。
第一个启发式算法优先考虑按物体面积降序进行适配:
def largest_first(container, objects):
excluded = []
assigned = []
objects.sort(key=lambda obj: obj[0] * obj[1], reverse=True) ①
for obj in objects:
if not fit(container, obj, objects.index(obj) + 1):
excluded.append(objects.index(obj) + 1) ②
else:
assigned.append(objects.index(obj) + 1)
if excluded: print(f"Items excluded: {len(excluded)}")
visualize(numpy.flip(container, axis=0), assigned) ③
① 按面积降序排序元素。
② 一些物体可能无法放入;我们可以使用列表来跟踪它们。
③ 可视化已填充的容器。
此代码的输出如图 2.19 所示。可视化此结果的代码包含在列表 2.7 的完整代码文件中,可在本书的 GitHub 仓库中找到。

图 2.19 使用最大优先启发式算法的装箱问题——一个物体被排除在外,因为它无法放入剩余的空间。
第二个启发式算法首先按宽度排序,然后按总面积排序,顺序为升序:
def smallest_width_first(container, objects):
excluded = []
assigned = []
objects.sort(key=lambda obj: (obj[0], obj[0] * obj[1])) ①
for obj in objects:
if not fit(container, obj, objects.index(obj) + 1):
excluded.append(objects.index(obj) + 1)
else:
assigned.append(objects.index(obj) + 1)
if excluded: print(f"Items excluded: {len(excluded)}")
visualize(numpy.flip(container, axis=0), assigned) ②
① 以宽度为主要键排序,然后按面积升序排序。
② 可视化解决方案。
smallest_width_first启发式算法成功地将所有物体放入容器中,如图 2.20 所示。

图 2.20 使用最小优先启发式算法的装箱问题——所有五个物体都成功放置在容器中。
可以使用不同的启发式搜索策略来生成候选解决方案。这些策略包括但不限于通过重复构建解决方案进行搜索(例如,图搜索和蚁群优化),通过重复修改解决方案进行搜索(例如,禁忌搜索、模拟退火、遗传算法和粒子群优化),以及通过重复解决方案重组进行搜索(例如,遗传算法和差分进化)。
让我们重新考虑 1.3.3 节中讨论的货运自行车装载问题。我们可以根据效率(每公斤利润)对要交付的物品进行排序,如表 2.2 所示。
表 2.2 按效率排序的包裹。包裹的效率定义为每公斤利润。
| 物品 | 重量(kg) | 利润($) | 效率($/kg) |
|---|---|---|---|
| 10 | 7.8 | 20.9 | 2.68 |
| 7 | 4.9 | 10.3 | 2.10 |
| 4 | 10 | 12.12 | 1.21 |
| 1 | 14.6 | 14.54 | 1 |
| 8 | 16.5 | 13.5 | 0.82 |
| 6 | 9.6 | 7.4 | 0.77 |
| 2 | 20 | 15.26 | 0.76 |
| 9 | 8.77 | 6.6 | 0.75 |
| 3 | 8.5 | 5.8 | 0.68 |
| 5 | 13 | 8.2 | 0.63 |
基于基于重复解决方案构建的启发式搜索策略,我们可以首先应用贪婪原则,根据效率选择物品,直到达到货运自行车的最大载重(100 公斤)作为硬约束。这一过程的步骤如表 2.3 所示。
表 2.3 重复解决方案构建——包裹被添加到自行车上,直到达到最大容量。
| 步骤 | 物品 | 添加? | 总重量(kg) | 总利润($) |
|---|---|---|---|---|
| 1 | 10 | 是 | 7.8 | 20.9 |
| 2 | 7 | 是 | 12.7 | 31.2 |
| 3 | 4 | 是 | 22.7 | 43.32 |
| 4 | 1 | 是 | 37.3 | 57.86 |
| 5 | 8 | 是 | 53.8 | 71.36 |
| 6 | 6 | 是 | 63.4 | 78.76 |
| 7 | 2 | 是 | 83.4 | 94.02 |
| 8 | 9 | 是 | 92.17 | 100.62 |
| 9 | 3 | 否 | (100.67) | - |
| 10 | 5 | 否 | (113.67) | - |
我们得到了以下物品子集:10, 7, 4, 1, 8, 6, 2, 和 9。这也可以写成(1,1,0,1,0,1,1,1,1,1),从左到右读取时显示我们包括了物品 1, 2, 4, 6, 7, 8, 9 和 10(并排除了物品 3 和 5)。这产生了总利润为 $100.62 和重量为 92.17 kg 的结果。我们可以通过重复添加物体的过程来生成更多解决方案,从空容器开始。
我们不仅可以从头开始创建一个或多个解决方案,还可以考虑修改现有可行解决方案的方法——这是一种 基于重复解决方案修改 的启发式搜索策略。考虑为载货自行车问题生成的先前解决方案:(1,1,0,1,0,1,1,1,1,1)。我们知道这个可行解决方案不是最优的,但我们如何改进它?我们可以通过从载货自行车中移除物品 9 并添加物品 5 来做到这一点。移除和添加的过程产生了一个新的解决方案,(1,1,0,1,1,1,1,1,0,1),总利润为 $102.22,重量为 96.4 kg。
另一种方法是结合现有解决方案来生成新的解决方案以在搜索空间中取得进展——这是 重复解决方案重组。假设给出了以下两个解决方案:
-
S[1] = (1,1,1,1,1,0,0,1,0,1) 重量为 75.8 kg,利润为 $75.78
-
S[2] = (0,1,0,1,1,0,1,1,1,1) 重量为 80.97 kg,利润为 $86.88
如图 2.21 所示,我们可以取 S[1] 的前两个项目和 S[2] 的最后八个项目来得到一个新的解决方案。这意味着我们在新的解决方案中包括了物品 1, 2, 4, 5, 7, 8, 9 和 10,并排除了物品 3 和 6。这产生了一个新的解决方案:S[3] = (1,1,0,1,1,0,1,1,1,1),重量为 95.57 kg,利润更高,为 $101.42。

图 2.21 重复解决方案重组——取 S[1] 的前两个元素并添加 S[2] 的最后八个元素得到一个新的、更好的解决方案。
2.4 受自然启发的算法
自然是灵感的终极源泉。自然中的问题通常是不良结构的、动态的、部分可观察的、非线性的、多模态的、多目标的,具有硬和软约束,并且没有或有限的全局信息访问。受自然启发的算法是模仿或逆向工程自然界中观察到的智能行为的计算模型。例如包括分子动力学、合作觅食、劳动分工、自我复制、免疫、生物进化、学习、集群、鱼群和自我组织,仅举几例。
分子动力学(模拟粒子系统运动的科学)和热退火启发了科学家们创造一种名为模拟退火的优化算法,我们将在第五章中讨论。进化计算算法,如遗传算法(GA)、遗传编程(GP)、进化编程(EP)、进化策略(ES)、差分进化(DE)、文化算法(CA)和协同进化(CoE),都是受进化生物学(研究进化过程)和生物进化启发的。本书的第三部分将涵盖多种进化计算算法。
生态学(动物行为的研究)是群体智能算法,如粒子群优化(PSO)、蚁群优化(ACO)、人工蜂群(ABC)、萤火虫算法(FA)、蝙蝠算法(BA)、社会蜘蛛优化(SSO)、蝴蝶优化算法(BOA)、蜻蜓算法(DA)、磷虾群(KH)、洗牌青蛙跳跃算法(SFLA)、鱼群搜索(FSS)、海豚伙伴优化(DPO)、海豚群优化算法(DSOA)、猫群优化(CSO)、猴子搜索算法(MSA)、狮子优化算法(LOA)、杜鹃搜索(CS)、杜鹃优化算法(COA)、狼搜索算法(WSA)和灰狼优化器(GWO)的主要灵感来源。基于群体智能的优化算法将在本书的第四部分中介绍。
神经网络(NNs)是受生物神经网络的结构和功能启发的计算模型。本书的第五部分将描述如何使用神经网络来解决搜索和优化问题。禁忌搜索(在第六章中解释)基于进化记忆(自适应记忆和响应探索),这在行为心理学(研究行为和心灵的科学)中得到了研究。强化学习是机器学习的一个分支,它从心理学、神经科学和控制理论等多个来源汲取灵感,并且可以用来解决搜索和优化问题,如书中最后一章所述。
其他受自然界启发的搜索和优化算法包括但不限于细菌觅食优化算法 (BFO)、细菌集群算法 (BSA)、生物地理学优化 (BBO)、入侵杂草优化 (IWO)、花授粉算法 (FPA)、森林优化算法 (FOA)、水流算法 (WFA)、水循环算法 (WCA)、头脑风暴优化算法 (BSO)、随机扩散搜索 (SDS)、联盟算法 (AA)、黑洞算法 (BH)、黑洞力学优化 (BHMO)、自适应黑洞算法 (BHA)、改进黑洞算法 (IBH)、莱维飞行黑洞 (LBH)、多种群莱维飞行黑洞 (MLBH)、基于螺旋星系的搜索算法 (GbSA)、基于星系的搜索算法 (GSA)、大爆炸大坍缩 (BBBC)、射线优化 (RO)、量子退火 (QA)、量子启发的遗传算法 (QGA)、量子启发的进化算法 (QEA)、量子群体进化算法 (QSE) 和量子启发的粒子群优化 (QPSO)。有关元启发式算法的完整列表,请参阅 S.M. Almufti 的“元启发式算法的历史调查” [8]。
在本书的五大部分中,我们将探讨五种主要的搜索和优化算法类别:图搜索算法、基于轨迹的优化、进化计算、群体智能算法和机器学习方法。以下算法包含在这些类别中:
-
图搜索方法(盲目或无信息搜索和无信息搜索算法)
-
模拟退火 (SA)
-
表搜索 (TS)
-
遗传算法 (GA)
-
粒子群优化 (PSO)
-
蚂蚁群优化 (ACO)
-
人工蜂群 (ABC)
-
图卷积网络 (GCN)
-
图注意力网络 (GAT)
-
自组织映射 (SOM)
-
行为者-评论家 (A2C) 架构
-
近端策略优化 (PPO)
-
多臂老丨虎丨机 (MAB)
-
上下文多臂老丨虎丨机 (CMAB)
在本书中,我们将探讨几个现实世界的问题,并看看这些算法如何应用。
摘要
-
搜索和优化问题可以根据决策变量的数量(单变量和多变量问题)、决策变量的类型(连续、离散或混合整数)、目标函数的数量(单目标、多目标或约束满足问题)、目标函数的景观(单峰、多峰或欺骗性)、约束的数量(无约束和约束问题),以及目标函数和约束的线性度(线性问题和非线性问题)进行分类。
-
根据解决方案的预期质量和找到解决方案所允许的搜索时间,优化问题也可以被分类为设计问题(战略函数)、规划问题(战术函数)或控制问题(操作函数)。
-
搜索和优化算法可以根据搜索空间被探索的方式(局部搜索与全局搜索)、它们的优化速度(在线优化与离线优化)以及算法的确定性(确定性算法与随机算法)进行分类。
-
启发式(也称为思维捷径或经验法则)有助于在合理的时间内找到可接受的(最优或近似最优)解决方案,以解决复杂问题。
-
元启发式是高级策略,用于指导和修改其他启发式算法,以增强其性能。
-
受自然启发的算法是计算模型,它们模仿或逆向工程自然界中观察到的智能行为,以解决复杂的不规则问题。
第三章:盲搜索算法
本章涵盖
-
应用不同的图类型
-
图搜索算法
-
使用图遍历算法在两个节点之间找到路径
-
使用盲搜索算法在图中的两个节点之间找到最短路径
-
使用图搜索算法解决现实世界的路由问题
在第二章中,您已经介绍了确定性和随机性算法。在本章中,我们将重点关注确定性算法,特别是盲搜索算法,以及它们在探索树或图结构以及找到节点之间最短路径中的应用。使用这些算法,您可以从一个初始状态探索迷宫到目标状态,解决n-拼图问题,确定社交媒体图中您与其他任何人的距离,搜索家谱以确定任何两个相关人员的确切关系,或者找到任何起点(例如,您的家)和任何目的地之间的最短路径。盲搜索算法很重要,因为当处理简单、定义明确的问题时,它们通常更高效且实用。
3.1 图的介绍
一个图是一个由称为顶点(或节点)和它们之间的关系组成的非线性数据结构,称为边(或弧或链接)。这种数据结构不遵循顺序模式,因此是非线性的,与数组、栈或队列等线性结构不同。
一个图可以用数学上的G表示,其中G = (V, E)。V表示节点或顶点的集合,E表示边或链接的集合。还可以将各种属性作为组件添加到边元组中,例如边长、容量或任何其他独特属性(例如,道路材料)。图可以分为无向、有向、多重图、无环和超图。
一个无向图是一个使用双向边连接节点集的图。这意味着两个连接节点的顺序不是必要的。
NetworkX 是一个常用的 Python 库,用于创建、操作和研究图和复杂网络的结构、动态和功能(有关图库的更多信息,请参阅附录 A)。以下列表显示了如何使用 NetworkX 创建无向图。
列表 3.1 使用 NetworkX 创建无向图
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.Graph()
nodes = list(range(5)) ①
graph.add_nodes_from(nodes)
edges = [(0,1),(1,2), (1,3), (2,3),(3,4)] ②
graph.add_edges_from(edges)
nx.draw_networkx(graph, font_color="white")
① 从 0 到 4 生成节点列表。
② 定义边列表。
该代码的输出显示在图 3.1 中。您实际得到的布局可能不同,但顶点之间的连接将与这里显示的相同。

图 3.1 无向图
有向图是一种使用方向边连接节点集的图。有向图有许多应用,例如表示流量约束(例如单向街道)、关系(例如因果关系)和依赖关系(例如依赖于其他任务完成的任务)。以下列表展示了如何使用 NetworkX 创建有向图。
列表 3.2 使用 NetworkX 创建有向图
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.DiGraph() ①
nodes = list(range(5))
edges = [(0,1),(1,2), (1,3), (2,3),(3,4)]
graph.add_edges_from(edges)
graph.add_nodes_from(nodes)
nx.draw_networkx(graph, font_color="white")
① DiGraph 允许有向边。
代码输出显示在图 3.2 中。注意指示边方向的箭头。

图 3.2 一个有向图
多图是一种可能连接相同顶点对的多条边的图。这些边称为平行边。多图可以用来表示节点之间的复杂关系,例如交通路由中两个位置之间的多条并行道路、资源分配问题中的多个容量和需求,以及社交网络中个人之间的多个关系,仅举几例。不幸的是,NetworkX 在可视化具有平行边的多图方面并不特别擅长。以下列表展示了如何结合使用 NetworkX 和 Matplotlib 库创建多图。
列表 3.3 使用 NetworkX 创建多图
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.MultiGraph()
nodes = list(range(5))
edges = [(0,1),(0,1),(4,3),(1,2), (1,3), (2,3),(3,4),(0,1)]
graph.add_nodes_from(nodes)
graph.add_edges_from(edges)
pos = nx.kamada_kawai_layout(graph) ①
ax = plt.gca()
for e in graph.edges: ②
ax.annotate("",xy=pos[e[0]], xycoords='data', xytext=pos[e[1]], ②
➥textcoords='data', arrowprops=dict(arrowstyle="-", ②
➥connectionstyle=f"arc3, rad={0.3*e[2]}"),zorder=1) ②
nx.draw_networkx_nodes(graph, pos) ③
nx.draw_networkx_labels(graph,pos, font_color='w') ③
plt.show()
① 使用 Kamada-Kawai 路径长度成本函数生成节点位置。
② 逐个绘制每条边,根据其索引(即节点 0 和 1 之间的第二条边)修改边的曲率。
③ 绘制节点和节点标签。
值得注意的是,kamada_kawai_layout试图在空间中定位节点,使得它们之间的几何(欧几里得)距离尽可能接近它们之间的图论(路径)距离。图 3.3 展示了由该代码生成的多图示例。

图 3.3 多图示例。注意连接节点 0 和 1 的三条平行边,以及连接节点 3 和 4 的两条边。
如其名所示,一个无环图是一个没有环的图。树作为图的特例,是一个没有环或自环的连通图。在图论中,一个连通图是一种图中每对顶点之间都存在路径的图。环,也称为自环或回路,是图中连接一个顶点(或节点)到自身的边。在任务调度中,无环图可以用来表示任务之间的关系,其中每个节点代表一个任务,每条有向边代表一个优先约束。这种约束意味着表示终点节点的任务不能开始,直到表示起点节点的任务完成。我们将在第六章中以装配线平衡问题作为调度问题的例子进行讨论。
以下列表展示了如何使用 NetworkX 创建和验证无环图。图 3.4 展示了无环图的一个示例。
列表 3.4 使用 NetworkX 创建无环图
import networkx as nx
import matplotlib.pyplot as plt
graph = nx.DiGraph()
nodes = list(range(5))
edges = [(0,1), (0,2),(4,1),(1,2),(2,3)]
graph.add_nodes_from(nodes)
graph.add_edges_from(edges)
nx.draw_networkx(graph, nx.kamada_kawai_layout(graph), with_labels=True,
➥ font_color='w')
plt.show()
nx.is_directed_acyclic_graph(graph) ①
① 检查图是否无环。

图 3.4 无环图——没有路径会回到任何起始节点。
超图是图的推广,其中推广的边(称为超边)可以连接任意数量的节点。超图用于表示复杂网络,因为它们可以捕捉更高阶的多对多关系。它们被用于社交媒体、信息系统、计算几何、计算病理学和神经科学等领域。例如,一个在项目上工作的团队可以用超图来表示。每个人由一个节点表示,项目由一个超边表示。超边连接所有参与项目的人,无论涉及多少人。超边还可以包含其他属性,如项目的名称、开始和结束日期、预算等。
下面的列表展示了如何使用 HyperNetX(HNX)来创建超图。HNX 是一个 Python 库,它使我们能够将复杂网络中发现的实体和关系建模为超图。图 3.5 展示了超图的一个示例。
列表 3.5 使用 HyperNetX 创建超图
import hypernetx as hnx
data = {
0: ("A","B","G"),
1: ("A","C","D","E","F"),
2: ("B","F"),
3: ("A","B","D","E","F","G")
} ①
H = hnx.Hypergraph(data) ②
hnx.draw(H) ③
① 超图的数据以超边名称/超边节点组的键值对形式提供。
② 为提供的数据创建超图。
③ 可视化超图。

图 3.5 超图的示例。超边可以连接超过两个节点,例如超边 0,它连接节点 A、B 和 G。
图也可以是有权或无权的。在加权图中,每个边都分配一个权重或值。例如,在道路网络的情况下,边可以有代表穿越道路成本的权重。这个权重可以代表距离、时间或任何其他度量。在电信网络中,权重可能代表使用该边的成本或通信设备之间连接的强度。
列表 3.6 展示了如何创建和可视化电信设备之间的加权图。在这个例子中,权重代表设备之间连接的速度,单位为 Mbps。运行此代码生成了图 3.6 所示的加权图。
列表 3.6 使用 NetworkX 创建加权图
import networkx as nx
import matplotlib.pyplot as plt
G = nx.Graph() ①
G.add_node("Device1", pos=(0,0)) ②
G.add_node("Device2", pos=(0,2)) ②
G.add_node("Device3", pos=(2,0)) ②
G.add_node("Device4", pos=(2,2))
G.add_weighted_edges_from([("Device1", "Device2", 45.69),
("Device1", "Device3", 56.34),
("Device2", "Device4", 18.5)]) ③
pos = nx.get_node_attributes(G, 'pos') ④
nx.draw_networkx(G, pos, with_labels=True) ⑤
nx.draw_networkx_edge_labels(G, pos, ⑤
➥edge_labels={(u, v): d['weight'] for ⑤
➥u, v, d in G.edges(data=True)}) ⑤
plt.show()
① 创建一个空的加权图。
② 向图中添加节点(代表设备)。
③ 向图中添加加权边(代表连接)。
④ 从图中获取节点位置属性。
⑤ 绘制图。

图 3.6 加权图的示例
图无处不在。搜索引擎如谷歌将互联网视为一个巨大的图,其中每个网页都是一个节点,如果从一个页面有链接指向另一个页面,则两个页面通过边连接。社交媒体平台如 Facebook 将每个用户资料视为社交图上的一个节点,如果两个节点互为好友或存在社交联系,则称它们是连接的。在像 X(以前是 Twitter)这样的平台上“关注”用户的概念可以通过一个有向边表示,其中用户A可以关注用户B,但反之不一定成立。表 3.1 显示了不同社交媒体平台上节点和边的含义。
表 3.1 社交媒体背景下的图示例
| 社交媒体平台 | 节点 | 边 | 边的类型 |
|---|---|---|---|
| 用户、群组、帖子以及活动 | 友谊、群组成员、消息、发帖以及帖子上的反应 | 无向:点赞、评论或反应;有向:好友请求 | |
| X(以前是 Twitter) | 用户、群组、未注册人员和帖子 | 关注、群组成员、消息、发帖以及帖子上的反应 | 无向:提及或转发;有向:关注关系(当你关注一个人时,他们不会自动回关你) |
| 领英 | 用户、群组、未注册人员、帖子、技能和职位 | 联系、群组成员、发帖、帖子上的反应、消息、推荐、邀请、推荐职位 | 无向:推荐或推荐;有向:一个联系 |
| 用户、评论、发布帖子的容器、标签、媒体(例如,照片、视频、故事或专辑)以及页面(Facebook 页面) | 用户之间的关系,如关注、点赞和评论 | 无向:点赞或评论;有向:关注关系 | |
| TikTok | 用户、视频、标签、位置和关键词 | 用户之间的关系,如关注、点赞和评论 | 无向:点赞或评论;有向:关注关系 |
在道路网络图中,节点代表地标,如交叉口和兴趣点(POI),边代表道路。在这样的图中,大多数边都是有向的,这意味着它们有特定的方向,并且可能包含额外的信息,如长度、速度限制、容量等。每条边是两个节点之间的两端点连接,其中边的方向代表交通流的方向。路线是一系列连接起点节点和终点节点的边。
OSMnx 是一个 Python 库,旨在简化从 OpenStreetMap (OSM; openstreetmap.org) 获取和操作数据。OSM 是一个全球性的众包地理数据库(有关如何从开放地理空间数据源获取数据的更多信息,请参阅附录 B)。OSMnx 允许您从 OSM 下载过滤后的数据,并以 NetworkX 图数据结构返回网络。它还可以将地点的文本描述符转换为 NetworkX 图(有关图和映射库的更多信息,请参阅附录 A)。以下列表以多伦多大学为例。
列表 3.7 多伦多大学示例
import osmnx as ox
import matplotlib.pyplot as plt
place_name = "University of Toronto"
graph = ox.graph_from_address(place_name) ①
ox.plot_graph(graph,figsize=(10,10))
① graph_from_address 也可以接受城市名称和邮寄地址作为输入。
图 3.7 显示了多伦多大学圣乔治校园周边的 OSM 地图。图中显示了多伦多市中心校园周边的道路网络节点和边。

图 3.7 多伦多大学圣乔治校园
虽然地图可能看起来在视觉上很有趣,但它缺乏周围地理特征的上下文。让我们使用 folium 库(请参阅附录 A)创建一个带有街道名称、社区名称甚至建筑足迹的基础层地图。
graph = ox.graph_from_address(place_name)
ox.folium.plot_graph_folium(graph)
图 3.8 显示了圣乔治校园周边的道路网络。

图 3.8 显示了多伦多大学圣乔治校园周边的道路网络。
假设您想在这所校园内从一个地点到另一个地点。例如,想象您从多伦多皇后公园附近的国王爱德华七世骑马雕像出发,需要穿越校园去巴嫩信息技术中心参加讲座。在本章的后面部分,您将看到如何计算这两个点之间的最短路径。
现在,让我们仅使用 folium 库将这些两个地点绘制在地图上。图 3.9 显示了 folium 地图和标记。
列表 3.8 使用 folium 绘图
import folium
center=(43.662643, -79.395689) ①
source_point = (43.664527, -79.392442) ②
destination_point = (43.659659, -79.397669) ③
m = folium.Map(location=center, zoom_start=15) ④
folium.Marker(location=source_point,icon=folium. ⑤
➥Icon(color='red',icon='camera', prefix='fa')).add_to(m) ⑤
folium.Marker(location=center,icon=folium.Icon(color='blue', ⑤
➥icon='graduation-cap', prefix='fa')).add_to(m) ⑤
folium.Marker(location=destination_point,icon=folium.Icon(color='green', ⑤
➥icon='university', prefix='fa')).add_to(m) ⑤
① 多伦多大学的 GPS 坐标(纬度和经度)
② 骑马雕像的 GPS 坐标作为源点
③ 巴嫩信息技术中心的 GPS 坐标作为目的地
④ 创建以指定点为中心的地图。
⑤ 添加带有图标的标记。

图 3.9 使用 folium 标记可视化兴趣点
代码的输出是交互式的,允许进行缩放、平移甚至图层过滤(当启用时)。附录 A 提供了有关 Python 中地图可视化库的更多详细信息。
3.2 图搜索
正如我在第二章中提到的,搜索算法可以广泛分为确定性和随机性算法。在确定性搜索中,搜索算法遵循严格的程序,其路径以及设计变量和函数的值是可重复的。无论您今天运行程序还是十年后运行,只要起始点相同,算法都会遵循相同的路径。另一方面,在随机搜索中,算法总是存在一些随机性,解决方案并不完全可重复。每次运行算法时,您可能会得到略微不同的结果。
根据搜索空间或领域知识(例如,从当前状态到目标的状态距离)的可用性,确定性搜索算法可以广泛分为盲目(或无信息)搜索和信息搜索,如图 3.10 所示。其中一些算法,如 Kruskal 的最小生成树(MST)算法,将在下一章中介绍。本章重点介绍盲目搜索算法。盲目搜索是一种不需要关于搜索空间信息的搜索方法。

图 3.10 图搜索方法
盲目搜索可能在发现第一个解决方案时结束,这取决于算法的终止条件。然而,搜索空间可能包含许多有效但非最优的解决方案,因此盲目搜索可能会返回一个满足所有要求但以非最优方式实现的解决方案。可以通过运行一个盲搜索,在穷举搜索或暴力搜索策略之后找到所有可行的解决方案,然后进行比较以选择最佳方案。这类似于应用大英博物馆算法,该算法通过逐一检查所有可能性来找到解决方案。鉴于盲目搜索对待图或树中的每个节点都是平等的,这种搜索方法通常被称为均匀搜索。
盲目搜索算法的例子包括但不限于以下内容:
-
广度优先搜索(BFS)是一种图遍历算法,通过层次结构构建搜索树。
-
深度优先搜索(DFS)是一种图遍历算法,首先探索通过根节点的一个相邻节点,然后是下一个相邻节点,直到找到解决方案或达到死胡同。
-
深度限制搜索(DLS)是一种具有预定深度限制的 DFS。
-
迭代加深搜索(IDS),或迭代加深深度优先搜索(IDDFS),结合了 DFS 的空间效率和 BFS 的快速搜索,通过增加深度限制直到达到目标。
-
Dijkstra 算法用于解决具有非负边成本的加权图的单源最短路径问题。
-
一致代价搜索(UCS)是迪杰斯特拉算法的一种变体,它使用最低的累积代价来找到从源到目的地的路径。如果所有边的路径代价相同,则它与 BFS 算法等价。
-
双向搜索(BS)是正向搜索和反向搜索的结合。它同时从起点向前搜索,从目标向后搜索。
以下章节讨论图遍历算法和最短路径算法,重点关注 BFS、DFS、迪杰斯特拉算法、UCS 和 BS 作为盲目搜索方法的例子。
3.3 图遍历算法
图遍历是通过访问节点并遵循特定的、明确定义规则来探索树或图结构的过程。这类图搜索算法仅寻求找到两个节点之间的路径,而不优化最终路径的长度。
3.3.1 广度优先搜索
广度优先搜索(BFS)是一种算法,其遍历从指定的节点(源或起始节点)开始,逐层遍历图,从而探索当前节点的所有相邻节点(与当前节点直接相连的节点)。然后,如果没有找到结果,算法将搜索下一级相邻节点。如果存在解决方案,此算法可以找到解决方案,假设任何节点总是有有限数量的后继者或分支。算法 3.1 显示了 BFS 步骤。
算法 3.1 广度优先搜索(BFS)
Inputs: Source node, Destination node
Output: Route from source to destination
Initialize *queue* ← a FIFO initialized with source node
Initialize *explored* ← empty
Initialize *found* ← False
While *queue* is not empty and *found* is False do
Set *node* ← *queue.*dequeue()
Add *node* to *explored*
For *child* in *node.*expand() do
If *child* is not in *explored* and *child* is not in *queue* then
If *child* is *destination* then
Update *route* ← *child* route()
Update *found* ← True
Add *child* to *queue*
Return *route*
BFS 使用队列作为数据结构来维护要探索的状态。队列是一种先进先出(FIFO)数据结构,其中在队列上等待时间最长的节点是下一个要扩展的节点。BFS 从队列中删除一个状态,然后将其后继者重新入队到队列中。
让我们考虑 8 数码问题(有时称为滑动块问题或拼图问题)。这个拼图由一个分成 3×3 网格的区域组成。瓷砖编号为 1 到 8,除了一个空(或空白)瓷砖。空白瓷砖可以通过与其直接相邻的任何瓷砖交换位置来移动(上、下、左、右)。拼图的目的是将瓷砖排列成顺序。拼图的变体允许空白瓷砖最终位于第一个或最后一个位置。这个问题是一个具有以下明确定义组件的良好结构问题(WSP)的例子:
-
状态—空白瓷砖的位置和八个瓷砖的位置
-
操作符(后继者)—空白瓷砖向左、右、上、下移动
-
目标—匹配给定的目标状态
-
解决方案/路径—通过状态空间的状态序列
-
停止标准—有序拼图(达到目标状态)
-
评估标准—步数或路径成本(路径长度)
图 3.11 展示了使用 BFS 解决 8 个拼图问题的步骤和搜索树遍历顺序。在这个图中,状态代表 8 个拼图问题的物理配置,搜索树中的每个节点都是一个包含其父节点、子节点、深度和从初始状态到该节点的路径成本的数据结构。第 1 层节点通过分别向左、上、右移动空白标题从左到右生成。向前移动,第 2 层节点通过扩展第 1 层中先前生成的节点生成,避免先前探索的节点。我们重复执行此程序以遍历所有可能的节点或直到达到目标(阴影网格)。达到目标所需的步数主要取决于 8 个拼图板的初始状态。突出显示的数字显示了遍历的顺序。如您所见,BFS 在垂直之前先水平前进。

图 3.11 使用 BFS 解决 8 个拼图问题
列表 3.9 使用了为本书开发的通用 BFS 算法,该算法可在 Optimization Algorithm Tools (optalgotools) Python 包中找到(安装说明见附录 A)。该算法以起始状态和目标状态作为输入,并返回一个solution对象。这个solution对象包含实际结果和一些性能指标,例如处理时间、最大空间使用量和已探索的解决方案状态数量。State类和visualize函数定义在书中 GitHub 仓库中可用的完整列表中。State类帮助管理一些数据结构和实用函数,并允许我们以后使用不同的算法重用此问题的结构。
列表 3.9 使用 BFS 解决 8 个拼图问题
#!pip install optalgotools
from optalgotools.algorithms.graph_search import BFS ①
init_state = [[1,4,2], [3,7,5], [6,0,8]]
goal_state = [[0,1,2], [3,4,5], [6,7,8]]
init_state = State(init_state) ②
goal_state = State(goal_state)
if not init_state.is_solvable(): ③
print("This puzzle is not solvable.")
else:
solution = BFS(init_state, goal_state)
print(f"Process time: {solution.time} s")
print(f"Space required: {solution.space} bytes")
print(f"Explored states: {solution.explored}")
visualize(solution.result) ④
① 从名为 optalgotools 的库中导入 BFS 算法。
② 请参阅完整列表中的 State 类。
③ 有些拼图板是无法解决的
④ 请参阅完整列表中的可视化函数。
这是一个示例解决方案,基于前面的输入:
Process time: 0.015625 s
Space required: 624 bytes
Explored states: 7
图 3.12 显示了 BFS 算法后的状态变化。

图 3.12 使用 Python 逐步 BFS 解决方案。BFS 寻找解决方案但不考虑最优性。
要真正理解 BFS 是如何工作的,让我们看看简单路径规划问题中涉及的步骤。这个问题在障碍物中找到一个移动机器人或自主车辆从起始位置到指定目的地的无碰撞路径。
- 将源节点添加到队列中(图 3.13)。

图 3.13 使用 BFS 解决路径规划问题——步骤 1
- 机器人只能移动到南(S)节点,因为东(E)和东南(SE)节点被阻挡(图 3.14)。

图 3.14 使用 BFS 解决路径规划问题——步骤 2
- 取出 S(先进先出),并探索其相邻节点,S 和 SE,其中 E 是障碍节点(图 3.15)。

图 3.15 使用 BFS 解决路径规划问题——第 3 步
- 取出 S(先进先出),并探索其相邻节点,S 和 SE(图 3.16)。

图 3.16 使用 BFS 解决路径规划问题——第 4 步
- 取出 SE(先进先出),并探索其相邻节点,E 和 NE(图 3.17)。

图 3.17 使用 BFS 解决路径规划问题——第 5 步
- 先进先出队列继续,直到找到目标节点(图 3.18)。为了简单起见,假设机器人想要到达图 3.18 中所示的节点 E,我们可以沿着树向上追踪以找到从源节点到目标节点的路径,这将是从 Start-S-SE-E。

图 3.18 使用 BFS 解决路径规划问题——中间目标节点 E 和最终目的地的最终路线
在广度优先搜索(BFS)中,每个生成的节点都必须保留在内存中。生成的节点数最多为 O(b^d),其中 b 代表每个节点的最大分支因子(即节点拥有的子节点数),d 是达到目标所需的扩展深度。在先前的例子中,以 E 作为目标节点(b=2,d=3),遍历的总节点数是 2³=8,包括起始节点。
除了算法解决当前问题的能力外,算法效率还基于运行时间(时间复杂度)、内存需求以及在最坏情况下解决问题所需的原始操作数。这些原始操作包括但不限于表达式评估、变量值赋值、数组索引和方法或函数调用。
大 O 表示法
大 O 表示法描述了算法的性能或复杂度,通常是在最坏情况下。大 O 表示法帮助我们回答问题:“算法能否扩展?”
要获得函数 f(x) 的大 O 表示法,如果 f(x) 是几个项的和,则保留增长速度最快的那个项,其余的项被省略。此外,如果 f(x) 是几个因子的乘积,则省略所有常数(乘积中不依赖于 x 的项)。
例如,让我们看看第一章中提出的票价问题:f(x) = –20x² + 6200x – 350000。假设x是一个大小为n的向量,代表n个不同的票价。这个函数是三个项的和,其中增长最快的项是x的指数最大的项,即 –20x²。现在我们可以应用第二个规则:–20x²是 –20 和 x² 的乘积,其中第一个因子不依赖于x。去掉这个因子后,得到简化的形式 x²。因此,我们说 f(x) 是 n² 的 big O,其中 n 是决策变量 x 的大小。从数学上我们可以写成 f(x) ∈ O(n²)(发音为“order n squared”或“O* of n squared”),这代表二次复杂度(即,增长速率与票价向量大小的平方成正比)。
表 3.2 展示了算法复杂度的示例,图 3.19 展示了大 O 符号的示例。
表 3.2 算法复杂度
| 符号 | 名称 | 有效性 | 描述 | 示例 |
|---|---|---|---|---|
| O(1) | 常数 | 极佳 | 运行时间不依赖于输入大小。随着输入大小的增长,操作次数不受影响。 | 变量声明访问数组元素从哈希表查找中检索信息从队列中插入和删除在栈上推和弹 |
| O(log n) | 对数 | 高 | 随着输入大小的增长,操作次数增长非常缓慢。每当n加倍或三倍等,运行时间增加一个常数。 | 二分查找 |
| O(n^c),0 < c < 1 | 分数幂或亚线性 | 高 | 随着输入大小的增长,操作次数在乘法中复制。 | 测试图连通性近似图中连通组件的数量近似最小生成树(MST)的权重 |
| O(n) | 线性 | 中等 | 随着输入大小的增长,操作次数线性增加。每当n加倍,运行时间也加倍。 | 打印数组元素简单搜索 Kadane 算法 |
| O(n log n) = O(log n!) | 线性对数,对数线性或准线性 | 中等 | 随着输入大小的增长,操作次数比线性增长略快。 | 归并排序堆排序 Tim 排序 |
| O(n^c),c > 1 | 多项式或代数 | 低 | 随着输入大小的增长,操作次数随指数增加而增加。 | 最小生成树(MST)矩阵行列式 |
| O(n²) | 二次 | 低 | 当n加倍时,运行时间增加四倍。二次函数仅适用于小问题。 | 选择排序冒泡排序插入排序 |
| O(n³) | 立方 | 低 | 当n加倍时,运行时间增加八倍。立方函数仅适用于小问题。 | 矩阵乘法 |
| O(c^n),c > 1 | 指数 | 非常低 | 随着输入大小的增长,操作次数呈指数增长。它很慢,通常不适用于实际应用。 | 力集汉诺塔密码破解暴力搜索 |
| O(n!) | 阶乘 | 非常低 | 非常慢,因为需要检查所有可能的输入数据排列。阶乘算法甚至比指数函数还要差。 | 旅行商问题字符串排列 |

图 3.19 大O表示法的示例
假设使用每秒一百万次操作的计算机处理大小为n = 20,000 的问题。表 3.3 显示了根据大O表示法显示的算法运行时间。
表 3.3 算法复杂度和运行时间
| 大O | 运行时间 |
|---|---|
| --- | --- |
| O(1) | 10^(-6)秒 |
| O(log n) | 14 × 10^(-6)秒 |
| O(n) | 0.02 秒 |
| O(n log n) = O(log n!) | 0.028 秒 |
| O(n²) | 6.66 分钟 |
| O(n³) | 92.6 天 |
| O(c^n), c = 2 | 1262.137 × 10⁶⁰¹⁵年 |
| O(n!) | 5768.665 × 10⁷⁷³³¹年(这比宇宙的年龄大得多,宇宙年龄约为 137 亿年) |
对于一个目标为深度的大工作空间,节点数量可能会呈指数增长,并需要大量的内存需求。在时间复杂度方面,对于图 G = (V, E),BFS 的运行时间为O(|V| + |E|),因为每个顶点最多入队一次,每条边要么检查一次(对于有向图),要么最多检查两次(对于无向图)。BFS 的时间和空间复杂度也以分支因子b和最浅目标深度d来定义。时间复杂度为O(bd*),空间复杂度也是*O*(*bd)。
考虑一个具有常数分支因子b = 5,节点大小为 1KB,每秒扫描 1000 个节点的图。节点总数N由以下方程给出:
|

| 3.1 |
|---|
表 3.4 显示了使用 BFS 遍历此图的时间和内存需求。
表 3.4 BFS 时间和空间复杂度
| 深度d | 节点N | 时间 | 内存 |
|---|---|---|---|
| 2 | 31 | 31 毫秒 | 31KB |
| 4 | 781 | 0.781 秒 | 0.78MB |
| 6 | 19,531 | 5.43 小时 | 19.5MB |
| 8 | 488,281 | 56.5 天 | 488MB |
| 10 | 12,207,031 | 3.87 年 | 12.2GB |
| 12 | 305,175,781 | 96.77 年 | 305GB |
| 14 | 7,629,394,531 | 2,419.26 年 | 7.63TB |
接下来,我们将查看 BFS 算法的对应算法,该算法首先深度搜索图,而不是广度搜索。
3.3.2 深度优先搜索
深度优先搜索(DFS)是一种递归算法,它使用回溯的思想。它涉及通过首先尽可能深入图中的所有节点来全面搜索所有节点。然后,当它达到没有结果的最后一层(即达到死胡同时),它会回溯一层并继续搜索。在 DFS 中,最深的节点首先扩展,深度相同的节点任意排序。算法 3.2 显示了 DFS 步骤。
算法 3.2 深度优先搜索(DFS)
Inputs: Source node, Destination node
Output: Route from source to destination
Initialize *Stack* ← a LIFO initialized with *source*node
Initialize *Explored* ← empty
Initialize *Found* ← False
While *stack* is not empty and *found* is False do
Set *node* ← *stack*.pop()
Add *node* to *explored*
For *child* in *node*.expand() do
If *child* is not in *explored* and *child* is not in *stack* then
If *child* is *destination* then
Update *route* ← *child*.route()
Update *found* ← True
Add *child* to *stack*
Return *route*
如您可能已经注意到的,DFS 和 BFS 之间的唯一区别在于数据结构的工作方式。DFS 不是按层向下工作(FIFO),而是钻到最底层,然后使用后进先出(LIFO)的数据结构(称为栈)返回到起始节点。栈包含已发现节点的列表。最近发现的节点被推到 LIFO 栈的顶部。随后,下一个要扩展的节点从栈顶弹出,然后将其所有后继节点添加到栈中。
图 3.20 显示了基于移动空白瓷砖的我们之前看到的 8 个拼图问题的 DFS 解决方案。如您所见,当算法达到死胡同或终端节点(如节点 7)时,它会回到最后一个决策点(节点 3)并继续另一个替代方案(节点 8 等)。在这个例子中,深度界限设置为 5,以限制节点扩展。这个深度界限使得节点 6、7、10、11、13、14、16、17、22、23、26 和 27 成为搜索树中的终端节点(即它们没有后继节点)。

图 3.20 使用 DFS 解决 8 个拼图问题
如您在列表 3.10 中看到的,我们只需要更改代码中的算法以使用 DFS。我还省略了解决方案的可视化,原因您很快就会看到。《State》类在书中 GitHub 仓库的完整列表中定义。
列表 3.10 使用 DFS 解决 8 个拼图问题
from optalgotools.algorithms.graph_search import DFS
init_state = [[1,4,2],[3,7,5],[6,0,8]]
goal_state = [[0,1,2],[3,4,5],[6,7,8]]
init_state = State(init_state)
goal_state = State(goal_state)
if not init_state.is_solvable(): ①
print("This puzzle is not solvable.")
else:
solution = DFS(init_state, goal_state) ②
print(f"Process time: {solution.time} s")
print(f"Space required: {solution.space} bytes")
print(f"Explored states: {solution.explored}")
print(f"Number of steps: {len(solution.result)}")
① 一些谜题并不
② DFS 的输入与 BFS 相同。
这里是使用前面输入运行此代码的输出:
Process time: 0.5247 s
Space required: 624 bytes
Explored states: 29
Number of steps: 30
如您所见,DFS 在处理非常深的图时并不出色,解决方案可能更接近顶部。您也可以看到为什么我选择不可视化最终解决方案:与 BFS 相比,解决方案中的步骤要多得多!因为此问题的解决方案更接近根节点,所以 DFS 生成的解决方案比 BFS 要复杂得多(30 步)。
回顾路径规划问题,DFS 可以用来从起点生成到目的地的无障碍路径,如下所示:
- 将源节点添加到栈中(图 3.21)。

图 3.21 使用 DFS 解决路径规划问题——步骤 1
- 探索 S 节点,因为 E 和 SE 节点被阻挡(图 3.22)。

图 3.22 使用 DFS 解决路径规划问题——步骤 2
- 取出 S(后进先出),并探索其相邻节点 S 和 SE,因为 E 是一个阻塞节点(图 3.23)。

图 3.23 使用 DFS 解决路径规划问题——步骤 3
- 取出 SE(后进先出),并探索其相邻节点 SW、S、E 和 NE(图 3.24)。

图 3.24 使用 DFS 解决路径规划问题——步骤 4
- 下一个要扩展的节点将是 NE,其后续节点将被添加到栈中。后进先出栈将继续,直到找到目标节点。一旦找到目标,就可以通过树回溯以获得车辆应遵循的路径(图 3.25)。

图 3.25 使用 DFS 解决路径规划问题——步骤 5
DFS 通常比 BFS 需要的内存少得多。这主要是因为 DFS 并不总是在每个深度展开每个节点。然而,在具有无限深度的搜索树中,DFS 可能会无限期地沿着一个无界分支向下继续,即使目标不在该分支上。
处理这个问题的方法之一是使用约束深度优先搜索,搜索在达到一定深度后停止。DFS 的时间复杂度为O(b^d),其中b是分支因子,d是搜索树的最大深度。如果d远大于b,这将是可怕的,但如果解决方案位于树的深处,它可能比 BFS 快得多。DFS 的空间复杂度为O(bd),这是线性空间!这种空间复杂度表示在内存中存储的最大节点数。
表 3.5 总结了 BFS 和 DFS 之间的差异。
表 3.5 BFS 与 DFS 的比较
| 广度优先搜索(BFS) | 深度优先搜索(DFS) | |
|---|---|---|
| 空间复杂度 | 更昂贵 | 更便宜。只需要O(d)空间,无论每个节点的子节点数量是多少。 |
| 时间复杂度 | 更高效。在访问较高层(远离根)的顶点之前,先访问较低层(靠近根)的顶点。 | 较低效 |
| 何时更受欢迎 |
-
如果树非常深
-
如果分支因子不过度
-
如果解决方案出现在相对较浅的水平(即解决方案接近树的起点)
-
示例:搜索英国王室家族树中很久以前去世的人,因为他们会接近树的顶部(例如,乔治六世国王)。
|
-
如果图或树非常宽,有太多的相邻节点
-
如果没有路径过于深入
-
如果解决方案出现在树的深处(即目标远离源点)
-
示例:搜索英国王室家族树中仍健在的人,因为他们会在树的底部附近(例如,威廉王子)。
|
在应用中,如果图中边的权重都相等(例如,所有长度为 1),则 BFS 和 DFS 算法在时间上优于 Dijkstra 等最短路径算法。最短路径算法将在下一节中解释。
3.4 最短路径算法
假设你正在寻找从家到工作的最快路线。像 BFS 和 DFS 这样的图遍历算法最终可能带你到达目的地,但它们肯定不会优化旅行距离。我们将讨论 Dijkstra 算法、一致代价搜索(UCS)和双向 Dijkstra 搜索,作为尝试在源节点和目标节点之间找到最短路径的盲目搜索算法的例子。
3.4.1 Dijkstra 搜索
Dijkstra 算法是一种图搜索算法,用于解决完全连接图的单源最短路径问题,具有非负边路径成本,生成最短路径树。Dijkstra 算法于 1959 年发表,并以荷兰计算机科学家 Edsger Dijkstra 的名字命名。该算法是其他几个常用图搜索算法的基础,这些算法通常用于解决流行导航应用中的路由问题,如图 3.26 所示。该算法遵循动态规划方法,将问题递归地分解为简单的子问题。Dijkstra 算法是无信息的,这意味着它不需要事先知道目标节点,也不使用启发式信息。

图 3.26 Dijkstra 算法及其变体和扩展示例
算法 3.3 展示了 Dijkstra 算法原始版本寻找图中已知单源节点到所有其他节点的最短路径的步骤。
算法 3.3 Dijkstra 算法
Inputs: A graph with weighted edges and a source node
Output: Shortest path from the source to all other nodes in the graph
Initialize *shortest_dist* ← empty
Initialize *unrelaxed_nodes* ← empty
Initialize *seen* ← empty
For *node* in *graph*
Set *shortest_dist[node]* = Infinity
Add *node* to *unrelaxed_nodes*
Set *shortest_dist[source]* ← 0
While *unrelaxed_nodes* is not empty do
Set *node* ← *unrelaxed_nodes*.pop()
Add *node* to *seen*
For *child* in *node*.expand() do
If *child* in *seen* then skip
Update *distance* ← *shortest_dist[node]* + length of edge to *child*
If *distance* < *shortest_dist[child]* then
Update *shortest_dist[child]* ← *distance*
Update *child.parent* ← node
Return *shortest_dist*
书中代码中提供的 Dijkstra 算法及其变体都进行了修改,要求指定目标节点。这提高了处理大型图(例如,道路网络)时的处理时间。
让我们看看 Dijkstra 算法是如何在图中找到任意两个节点之间的最短路径的。优先队列用于根据某种排序函数(在这种情况下,节点与源节点之间的最短距离)弹出队列中具有最高优先级的元素。
- 初始列表,没有前驱节点:优先队列 = {}(图 3.27)。

图 3.27 使用 Dijkstra 算法寻找最短路径——步骤 0
- 源节点最近的节点是 S,因此将其添加到优先队列中。更新 A、C 和 D 的累积距离(即从源节点 S 到达节点的距离)和前驱节点。优先队列 = {S}(图 3.28)。

图 3.28 使用 Dijkstra 算法寻找最短路径——步骤 1
- 下一个最近的节点是 C,因此将其添加到优先队列中。更新 A 和 D 的距离和前驱节点。优先队列 = {S, C}(图 3.29)。

图 3.29 使用迪杰斯特拉算法寻找最短路径——步骤 2
- 下一个最近的节点是 D,因此将其添加到优先队列中。更新 B 的距离和前驱节点。优先队列 = {S, C, D}(图 3.30)。

图 3.30 使用迪杰斯特拉算法寻找最短路径——步骤 3
- 次近的节点是 A,因此将其添加到优先队列中。优先队列 = {S, C, D, A}(图 3.31)。

图 3.31 使用迪杰斯特拉算法寻找最短路径——步骤 4
- 下一步是将剩余的节点 B 添加以完成搜索(图 3.32)。优先队列 = {S, C, D, A, B}。现在所有节点都已添加。

图 3.32 使用迪杰斯特拉算法寻找最短路径——步骤 5
搜索完成后,你可以选择目标节点并从表中找到最短路径。例如,如果目标节点是 A,则 S 和 A 之间的最短路径是 S-C-A,长度为 9。同样,如果目标节点是 B,则 S 和 B 之间的最短路径是 S-C-D-B,距离为 10。
注意,我们不能在我们的 8 数码问题中使用迪杰斯特拉搜索,因为迪杰斯特拉搜索需要事先了解整个问题空间。虽然该问题有有限数量的可能状态(正好是 9!/2),但该解决方案空间的规模使得迪杰斯特拉搜索不太可行。
3.4.2 均匀代价搜索(UCS)
均匀代价搜索(UCS)算法是一种盲目搜索算法,它使用最低的累积成本从起点找到到终点的路径。本质上,该算法通过成本(最小化问题中最低成本为最高优先级)或效用(最大化问题中最高效用为最高优先级)来组织要探索的节点。
随着节点从队列中弹出,我们将节点的子节点添加到队列中。如果一个子节点已经在优先队列中存在,则比较两个子节点的优先级,并接受最小化问题中的最低成本(最高优先级)。这确保了到达每个子节点的路径是可用的最短路径。我们还维护一个已访问列表,以便我们可以避免重新访问已经从队列中弹出的节点。当图中的所有边成本相等或相同的时候,UCS 的行为类似于 BFS。在这种情况下,UCS 将以 BFS 相同的顺序扩展节点——按层或广度优先。算法 3.4 展示了 UCS 算法的步骤。
算法 3.4 均匀代价搜索(UCS)
Inputs: A graph with edges, a source node, a destination node
Output: Shortest path from source to destination in the graph
Initialize *priority_queue* ← *source*
Initialize *found* ← False
Initialize *seen* ← *source*
While *priority_queue* is not empty and *found* is False do
Set *node* ← priority_queue.pop()
Update *seen* ← *node*
Update node_cost ← cumulative distance from source
If *node* is *destination* then
Update *route* ← *node*.route()
Update *found* ← True
For child in node.expand() do
If child in priority_queue then
If child.priority < priority_queue[child].priority then
Set priority_queue[child].priority = child.priority
Else
Update priority_queue ← child
Update priority_queue[child].priority ← node_cost
Return route
UCS 是 Dijkstra 算法的一个变体,对于大型图非常有用,因为它耗时较少,空间要求也较少。与 Dijkstra 算法在开始时将所有节点以无限代价添加到队列中不同,UCS 逐渐填充优先队列。例如,考虑在图中找到每对节点之间最短路径的问题。随着图的大小和复杂性的增长,很快就会明显看出 UCS 更有效率,因为它不需要事先知道整个图。表 3.6 显示了 Dijkstra 算法和 UCS 在不同大小图上的处理时间差异。这些数字是在没有多进程或多线程的情况下,在 3.60 GHz 的 Intel Core i9-9900K 上使用 Comparison.ipynb 中的代码收集的,该代码可在书籍的 GitHub 仓库中找到。
表 3.6 UCS 与 Dijkstra 算法比较
| 图大小 = | V | + | E | | Dijkstra 时间 | 统一代价搜索(UCS)时间 |
| --- | --- | --- |
| 108 | 0.25 s | 0.14 s |
| 628 | 84.61 s | 58.23 s |
| 1,514 | 2,082.97 s | 1,360.98 s |
注意,在我们的 8 数码问题上进行 UCS 搜索需要每个状态的距离属性(默认为 1),并且总体上生成相当不错的结果(大约使用 6.2 KB 的空间和探索了 789 个状态)。重要的是要注意,因为边的长度都相等,UCS 无法优先探索新节点。因此,解决方案失去了最短路径算法的优势,即优化更紧凑解决方案的能力。在下一章中,你将看到计算这些状态之间人工距离的方法,最终快速生成解决方案并最小化所需的步骤数。
3.4.3 双向 Dijkstra 搜索
双向搜索同时应用正向搜索和反向搜索。如图 3.33 所示,它从初始源状态 S→G 正向搜索,并从最终目标状态 G→S 反向搜索,直到它们相遇。

图 3.33 双向 Dijkstra 算法
如图 3.33 所示,Dijkstra 搜索空间为C[1] = 4πr²,双向 Dijkstra 搜索空间由C[2] + C[3] = 2πr²表示。这意味着我们减少了大约两倍的搜索空间。以下算法显示了双向 Dijkstra 算法的步骤。
算法 3.5 双向 Dijkstra 算法
Inputs: A graph, a source node, a destination node
Output: Shortest path from source to destination in the graph
Initialize *frontier_f* ← initialized with source
Initialize *frontier_b* ← initialized with destination
Initialize *explored_f* ← empty
Initialize *explored_b* ← empty
Initialize *found* ← False
Initialize *collide* ← False
Initialize *altr_expand* ← False
While *frontier_f* is not empty and *frontier_b* is not empty and not *collide* and not *found* do
If altr_expand then
Set node ← frontier_f.pop()
Add node to explored_f
For child in node.expand() do
If child in explored_f then continue
If child is destination then
Update route ← child.route()
Update found ← True
If child in explored_b then
Update route ← child.route() + reverse(overlapped.route())
Update collide ← True
Add child to frontier_f
Update altr_expand ← not altr_expand
Else
Update node ← frontier_b.pop()
Add node to explored_b
For child in node.expand() do
If child in explored_b then continue
If child is origin then
Update route ← child.route()
Update found ← True
If child in explored_f then
Update route ← reverse(child.route()) + overlapped.route()
Update collide ← True
Add child to frontier_b
Update altr_expand ← not altr_expand
Return route
这种方法由于涉及的时间复杂度更高,因此更有效率。例如,具有常数分支因子b和深度d的 BFS 搜索的整体空间复杂度为O(b^d)。然而,通过以只有一半深度(d/2)运行两个相反方向的 BFS 搜索,空间复杂度变为O*(bd(1/2) + bd(1/2)),或者简单地 O(bd(1/2)),这显著降低了。
图 3.34 显示了在探索多伦多市 50,841 个节点时,Dijkstra 算法和双向 Dijkstra 算法之间的差异。

图 3.34 Dijkstra 算法与双向 Dijkstra 算法——从左向右的前向探索和从右向左的后向探索
3.5 将盲目搜索应用于路由问题
逻辑谜题游戏和简单的网格路由问题有助于理解算法的工作原理。然而,是时候看看一些使用这些算法的真实世界示例和结果了。例如,想象一下,当你正在多伦多的皇后公园参观爱德华七世骑马雕像时,你突然想起你有一个在多伦多大学的信息技术中心(Bahen Centre)的会议。我最初在讨论本章开头道路网络图时提出了这个问题。在考虑这个问题时,我们将做出以下假设:
-
由于你的手机电量耗尽,你无法打开导航应用或打电话求助。
-
你知道你的目的地在多伦多某个地方,但你不知道它相对于你的起始位置在哪里。(在后面的章节中,你将学习到知道目的地方向如何在极短的时间内生成近似最优解。)
-
一旦你开始使用一条规则来规划你的目的地,你就会坚持这条规则。
让我们看看我们如何可能使用 BFS、DFS、Dijkstra 算法、UCS 和双向 Dijkstra 算法来模拟我们的路径搜索技巧。这个示例的代码位于本书的 GitHub 仓库中(Comparison.ipynb)。图 3.35 至图 3.37 展示了这些盲目搜索算法生成的路径。

图 3.35 使用 BFS 生成的最短路径。BFS 在移动到下一层之前先搜索每一层。当图不是很宽且解决方案靠近根节点时,这种方法效果最佳。

图 3.36 使用 DFS 生成的最短路径。DFS 在回溯之前尽可能深入地搜索图。当图不是很深且解决方案位于根节点较远的地方时,这种方法效果最佳。

图 3.37 使用 Dijkstra 算法、UCS 和双向 Dijkstra 算法生成的最短路径。这三种算法都会产生相同的解决方案(最优路由),但它们在内存使用和节点探索方面会有不同的处理方式。
值得注意的是,NetworkX 中的dijkstra_path函数使用 Dijkstra 方法来计算图中两个节点之间的最短加权路径。我们的 optalgo-tools 包还提供了不同图搜索算法的实现,例如 BFS、DFS、Dijkstra 算法、UCS 和双向 Dijkstra 算法。optalgotools 中对 Dijkstra 算法的实现已经修改,以便与我们的 OSM 数据一起工作,因为从地图生成的图自然会有自环和并行边。并行边可能导致不是最短可用路径的路线,因为路线长度在很大程度上取决于在生成特定路径时选择了哪条并行边。在图 3.38 中,从 0 到 2 的最短路径可能返回长度为 7,如果计算该路径时选择了连接 0 和 1 的顶部边,而选择底部边时长度为 3。

图 3.38 并行边可能存在问题,因为找到最短路径取决于在图探索过程中选择了哪条并行边。
自环也会给原始的 Dijkstra 算法带来麻烦。如果一个图中包含自环,到某个节点的最短路径可能来自该节点本身。在那个时刻,我们就无法生成一条路径(图 3.39)。

图 3.39 自环可能会打断父子节点链,这阻止我们在找到解决方案后重新追踪路径。
这两个问题通常容易解决但非同寻常,需要避免。对于并行边,我们选择权重最低(长度最短)的边,并丢弃任何其他并行边。对于自环,我们可以完全忽略该环,因为在大多数路由问题中不存在负权重环(道路的长度不能为负),而正权重环不能成为最短路径的一部分。此外,本书中使用的 Dijkstra 算法在找到目标节点时终止,而传统的实现方式只有在找到从根节点到所有其他节点的最短路径时才结束。
表 3.7 比较了 BFS、DFS、Dijkstra 算法和 UCS 在路径长度、处理时间、所需空间和已探索节点数方面的差异。从这些结果中可以看出,Dijkstra 算法、UCS 和双向 Dijkstra 算法产生最优结果,时间和空间成本各不相同。虽然 BFS 和 DFS 都能在最短时间内找到可行解,但提供的解不是最优的,在 DFS 的情况下甚至是不合理的。另一方面,DFS 需要事先知道整个图,这既昂贵又不太实用。为特定问题选择合适的搜索算法很大程度上涉及确定处理时间和空间需求之间的理想平衡。在后面的章节中,我们将探讨产生近似最优解的算法,这些算法通常在最优解既不可能找到又不太实用时使用。请注意,所有这些解决方案都是可行的;它们都能从点 A 到点 B 产生一个有效(如果有时复杂)的路径。
表 3.7 比较了 BFS、DFS、Dijkstra 算法和 UCS,其中b是分支因子,m是搜索树的最大深度,d是最浅的图深度,E是边的数量,V是顶点的数量。
| 算法 | 成本(米) | 处理时间(秒) | 空间(字节) | 已探索节点 | 最坏情况时间 | 最坏情况空间 | 最优性 |
|---|---|---|---|---|---|---|---|
| BFS | 955.962 | 0.015625 | 1,152 | 278 | O(b^d) | O(b^d) | No |
| DFS | 3347.482 | 0.015625 | 1,152 | 153 | O(b^m) | O(bm) | No |
| Dijkstra’s | 806.892 | 0.0625 | 3,752 | 393 | O(|E| + |V| log |V|) | O(|V|) | Yes |
| UCS | 0.03125 | 592 | 393 | O((b + |E|) * d) | O(b^d) | Yes | |
| 双向 Dijkstra’s | 0.046875 | 3,752 | 282 | O(*bd*(/2)) | O(*bd*(/2)) | Yes |
在下一章中,我们将探讨如果我们利用领域特定知识而不是盲目搜索,如何优化搜索。我们将直接深入研究信息搜索方法,看看我们如何使用这些算法来解决最小生成树和最短路径问题。
摘要
-
传统的图搜索算法(盲目和信息搜索算法)是确定性搜索算法,它们探索图是为了一般发现或显式搜索。
-
图是一种非线性数据结构,由顶点和边组成。
-
盲(无信息)搜索是一种搜索方法,其中不使用关于搜索空间的信息。
-
广度优先搜索(BFS)是一种图遍历算法,它在考虑下一层的节点之前,先检查搜索树同一层上的所有节点。
-
深度优先搜索(DFS)是一种图遍历算法,它从根节点或初始节点或顶点开始,尽可能跟随一条分支,然后回溯以探索其他分支,直到找到解决方案或所有路径都耗尽。
-
深度限制搜索(DLS)是具有预定深度限制的 DFS 的约束版本,防止它探索超过一定深度的路径。
-
迭代加深搜索(IDS),或迭代加深深度优先搜索(IDDFS),通过增加深度限制直到达到目标,结合了 DFS 的空间效率和 BFS 的快速搜索。
-
Dijkstra 算法解决了加权图中具有非负边成本的单一源最短路径问题。
-
广度优先搜索(UCS)是 Dijkstra 算法的一个变体,它使用最低的累积成本来找到从源到目的地的路径。如果所有边的路径成本相同,则它与 BFS 算法等价。
-
双向搜索(BS)是正向搜索和反向搜索的结合。它同时从起点正向搜索和从目标反向搜索。
-
选择搜索算法涉及确定目标平衡,包括时间复杂度、空间复杂度以及搜索空间的前知等因素。
第四章:启发式搜索算法
本章涵盖
-
定义启发式搜索
-
学习如何解决最小生成树问题
-
学习如何使用启发式搜索算法找到最短路径
-
使用这些算法解决实际世界的路由问题
在上一章中,我们介绍了盲搜索算法,这些算法在搜索过程中不需要关于搜索空间的信息。在本章中,我们将探讨如果在搜索过程中利用一些关于搜索空间的信息,搜索可以进一步优化。
随着问题和搜索空间变得更大、更复杂,算法本身的复杂性也会增加。我将首先介绍启发式搜索算法,然后我们将讨论最小生成树算法和最短路径搜索算法。将路由问题作为一个实际应用来展示如何使用这些算法。
4.1 介绍启发式搜索
如我们在上一章中讨论的,盲搜索算法在没有关于搜索空间信息的情况下工作,除了区分目标状态和其他状态所需的信息。就像俗语“当我看到它时,我就知道它”一样,盲搜索遵循一套规则框架(例如,广度优先、深度优先或 Dijkstra 算法)来系统地导航搜索空间。启发式搜索算法与盲搜索算法的不同之处在于,算法在搜索过程中使用在搜索过程中获得的知识来指导搜索本身。这种知识可以采取目标距离或产生成本的形式。
例如,在 8 个拼图问题中,我们可能会使用错位拼图的数目作为启发式来确定任何给定状态与目标状态的距离。这样,我们可以在算法的任何给定迭代中确定其性能如何,并根据当前条件修改搜索方法。对“良好性能”的定义取决于所使用的启发式算法。
启发式搜索算法可以广泛分为解决最小生成树(MST)问题的算法和计算两个特定节点或状态之间最短路径的算法,如图 4.1 所示。

图 4.1 启发式搜索算法的示例。每种算法都有基于改进、特定用例和特定领域的多个变体。
已提出几种算法来解决最小生成树(MST)问题:
-
Borůvka 算法在所有边权重都不同的图中找到一个最小生成树。在图不连通的情况下,它还找到一个最小生成森林。它从每个节点作为其自己的树开始,识别离开每个树的最低成本边,然后通过这些边合并连接的树。不需要或维护优先队列中的边预排序。
-
Jarník-Prim 算法从根顶点开始,找到从 MST 顶点到非 MST 顶点的最低权重边,并在每一步将其添加到 MST 中。
-
克鲁斯卡尔算法按权重递增排序边,并从最轻的边开始形成一个小的 MST 组件,然后将其扩展成一个大的 MST。我将在下一节更详细地描述这个算法。
爬山法(HC)、束搜索、A*算法和收缩分层(CH)是有信息搜索算法的例子,可以用来找到两个节点之间的最短路径:
-
爬山法 是一种局部搜索算法,它持续地向优化目标函数的方向移动,在最大化问题中增加,在最小化问题中减少。
-
束搜索 通过在有限的预定义集合内扩展最有希望的节点来探索图或树。
-
A算法*结合了到达一个节点所累积的成本和启发式信息,例如该节点与目标节点之间的直线距离,以选择新的扩展节点。
-
分层方法,如基于可达性的路由、高速公路分层(HH)、高速公路节点路由(HNR)、交通节点路由(TNR)和收缩分层(CH),是考虑节点重要性的分层方法,试图通过可接受启发式方法剪枝搜索空间。
下一个部分介绍了最小生成树(MST)的概念,并介绍了一种可以生成任何给定图的最小生成树的算法。
4.2 最小生成树算法
想象一下,你是偏远农村小镇的基础设施经理。与大多数城镇不同,这里实际上没有主街道或市中心区域,所以大多数兴趣点都分散各处。此外,前几年的预算削减导致道路要么损坏,要么根本不存在。损坏的道路都被泥土掩埋,基本上无法通行。你被分配了一小笔预算来修复或建造道路以改善情况,但这笔钱不足以修复所有现有的道路或建造新的道路。图 4.2 显示了城镇的地图以及现有损坏道路的位置。

图 4.2 污染城市问题。这个城镇的道路严重损坏,但资金不足以修复所有道路。
解决这个问题的方法有很多,从不可行的(修复所有损坏的道路并忍受城镇破产的后果)到过于保守的(只修复几条道路,或者一条都不修,并忽略所有抱怨的市民)。这个问题通常被称为污染城市问题,其中图中的各种节点必须连接起来,同时最小化边的权重。这些权重可以表示修复或铺路的开销,这可能会根据道路的状况、长度和拓扑结构而变化。
解决泥泞城市问题的数学方法涉及到最小生成树(MST)的概念。一般来说,生成树是无向图的一个无环或无环子图,它用最少数量的边连接图的所有顶点。在图 4.3 中,左边的树显示了从 A 到 F 的节点组成的图G,而中间和右边的树显示了G的生成树。请注意,一般的生成树不需要边的权重(即,不需要与长度、速度、时间或成本相关联)。

图 4.3 生成树的示例。中间和右边的树没有回路或环,连接了图G的所有节点。
最小生成树(MST)或边权重图的最小权重生成树是一棵生成树,其权重(其边的权重之和)不大于任何其他生成树的权重。
如果G=(V, E)是一个图,那么G的任何子图都是一棵生成树,如果满足以下两个条件:
-
子图包含G的所有顶点V。
-
子图是连通的,没有回路和自环。
对于一个图G的给定生成树T,生成树T的权重w是T中所有边的权重之和。如果T的权重是所有可能的生成树权重中的最低值,那么我们可以称它为最小生成树。
之前描述的泥泞城市问题将被作为一个最小生成树(MST)来解决。Kruskal、Borůvka、Jarník-Prim 和 Chazelle 都是可以用来找到最小生成树的算法示例。算法 4.1 展示了 Kruskal 算法的伪代码。
算法 4.1 Kruskal 算法
Input: Graph G = (V, E) with each edge e ∈ E having a weight w(e)
Output: A minimum spanning tree T
Create a new graph T:= ∅ with the same vertices as G, but with no edges.
Define a list S containing all the edges in the graph G
Sort the edges list S in ascending order of their weights.
For each edge e in the sorted list:
If adding edge e to T does not form a cycle:
Add this edge to T.
Else:
Skip this edge and move to the next edge in the list.
Continue this process until all the edges are processed.
Return T as the minimum spanning tree of graph G.
为了更好地理解这些步骤,让我们应用 Kruskal 算法来解决泥泞城市问题。图 4.4 显示了原始图。边附近的数字代表边权重,还没有添加任何边到最小生成树中。以下步骤将通过手动迭代生成最小生成树。

图 4.4 使用 Kruskal 算法解决泥泞城市问题——原始图
- 最短的边是 E-H,长度为 1,因此它被突出显示并添加到最小生成树中(图 4.5)。

图 4.5 使用 Kruskal 算法解决泥泞城市问题——第 1 步
- B-C、C-G、G-F 和 I-J 现在是长度为 2 的最短边。B-C 被任意选择并突出显示,然后是 C-G、G-F 和 I-J,因为它们没有形成环(图 4.6)。

图 4.6 使用 Kruskal 算法解决泥泞城市问题——第 2 步
- C-F、F-J、G-I、A-D 和 D-E 现在是长度为 3 的最短边。C-F 不能被选择,因为它形成了一个环。A-D 被任意选择并突出显示,然后是 D-E 和 G-I。F-J 不能被选择,因为它形成了一个环(图 4.7)。

图 4.7 使用 Kruskal 算法解决泥泞城市问题——第 3 步
- 下一个最短边是 A-E 和 G-H,长度为 4。由于 A-E 会形成一个环,所以不能选择它,因此过程以边 G-H 结束。最小生成树已经找到(图 4.8)。

图 4.8 使用 Kruskal 算法解决泥泞城市问题——步骤 4
图 4.9 显示了所有节点在图中都连接的最终解决方案。

图 4.9 使用 Kruskal 算法解决泥泞城市问题。算法通过按升序权重顺序添加边到最终树中,忽略会形成环的边。
这个算法可以通过使用 NetworkX 的find_cycle()和is_connected()方法轻松地在 Python 中实现,这些方法分别确定边是否是 MST 的可行候选,以及整体算法的终止条件。为了视觉展示,我还使用了spring_layout()来设置图的节点和边的位置。spring_layout()方法内部使用随机数生成器来生成这些位置,我们可以传递一个种子(允许生成所谓的“伪随机”数)来保证每次执行都得到特定的布局。尝试修改种子参数,看看会发生什么。
列表 4.1 使用 Kruskal 算法解决泥泞城市问题
import matplotlib.pyplot as plt
import networkx as nx
G = nx.Graph() ①
G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) ①
edges = [ ①
("A", "B", {"weight": 10}), ①
("A", "C", {"weight": 5}), ①
("A", "D", {"weight": 3}), ①
("A", "E", {"weight": 4}), ①
("B", "C", {"weight": 2}), ①
("B", "F", {"weight": 6}), ①
("C", "E", {"weight": 11}), ①
("C", "F", {"weight": 3}), ①
("C", "G", {"weight": 2}), ①
("D", "E", {"weight": 3}), ①
("D", "H", {"weight": 5}), ①
("E", "G", {"weight": 5}), ①
("E", "H", {"weight": 1}), ①
("F", "G", {"weight": 2}), ①
("F", "I", {"weight": 7}), ①
("F", "J", {"weight": 13}), ①
("G", "H", {"weight": 4}), ①
("G", "I", {"weight": 3}), ①
("H", "I", {"weight": 6}), ①
("I", "J", {"weight": 2}), ①
] ①
G.add_edges_from(edges)
pos = nx.spring_layout(G, seed=74) ②
def Kruskal(G, attr = "weight"):
edges = sorted(G.edges(data=True), key=lambda t: t[2].get(attr, 1)) ③
mst = nx.Graph()
mst.add_nodes_from(G)
for e in edges:
mst.add_edges_from([e])
try: ④
nx.find_cycle(mst) ④
mst.remove_edge(e[0], e[1]) ④
except:
if nx.is_connected(mst): ⑤
break
continue
return mst
① 创建一个无向图,并用节点和边填充它。
② 使用种子与 spring_layout 方法保证每次放置节点相同。
③ 按照权重升序排序边。
④ 如果图中不存在环,find_cycle会引发错误。我们可以尝试/捕获这个错误,以确定添加新边是否会形成一个环。
⑤ 如果由这些边形成的图是连通的,那么 mst 中的边集就是一个生成树。
作为列表 4.1 的延续,以下代码片段用于生成使用 Kruskal 的最小生成树并可视化它:
MST = Kruskal(G).edges ①
labels = nx.get_edge_attributes(G, "weight")
nx.draw_networkx_edges(G, pos, list(MST), width=4, edge_color="red") ②
nx.draw_networkx_edge_labels(G, pos, edge_labels=labels) ③
nx.draw_networkx(G, pos, with_labels=True, font_color="white") ④
plt.show()
① 调用 Kruskal 算法生成最小生成树。
② 绘制边。
③ 添加标签。
④ 绘制最小生成树。
如您在图 4.10 中看到的,我们用 Python 实现的泥泞城市问题产生了与手动迭代相同的结果。节点 G,即市政厅,成为城镇交通基础设施的一个中心枢纽,道路建设的总成本被最小化。然而,值得注意的是,尽管最小生成树(MST)最小化了连接所有节点的总成本,但它通常不会产生最“方便”的解决方案。例如,如果有人想从宗教场所前往医院,最短可达距离将是 7(经过警察局),而我们的道路网络需要总距离为 15。

图 4.10 使用 Kruskal 算法解决的泥泞城市问题。高亮显示的边是 MST 的一部分。
让我们将此算法和此代码应用于找到多伦多大学周围搜索空间内所有节点的 MST。想象一下,我们被分配在城市中安装新的通信电缆的任务,我们希望最小化使用的电缆长度。
列表 4.2 多伦多大学 MST
import networkx as nx
import osmnx
import matplotlib.pyplot as plt
from optalgotools.algorithms.graph_search import Kruskal
reference = (43.661667, -79.395)
G = osmnx.graph_from_point(
reference, dist=1000, clean_periphery=True, simplify=True, network_
type="drive"
) ①
fig, ax = osmnx.plot_graph(G)
undir_G = G.to_undirected() ②
sorted_edges = sorted(undir_G.edges(data=True), key=lambda t: ②
➥ t[2].get("length",1)) ②
mst = Kruskal(G, sorted_edges=True, edges= sorted_edges,
➥ graph_type=nx.MultiDiGraph) ③
highlight_edges = ['b' if e in mst.edges else 'r' for e in G.edges] ④
edge_alphas = [1 if e in mst.edges else 0.25 for e in G.edges] ④
osmnx.plot_graph(
G,
edge_linewidth=2,
edge_color=highlight_edges,
edge_alpha=edge_alphas
) ④
plt.show() ④
① 使用驾驶网络仅关注可驾驶的道路。这可以防止代码使用道路和人行道组合构建 MST。
② 获取无向图副本,并按边长对道路网络边进行排序。
③ 使用预排序列表并指定图类型,从 optalgotools 调用 Kruskal 算法。
④ 通过突出显示包含的边来可视化最小生成树(MST)。
图 4.11 显示了 Kruskal 算法生成的结果 MST。图中的道路网络图可能看起来像一个大连通分量,但实际上并非如此。图中似乎连接相邻节点的单行道实际上并没有连接(你可以从 A 到 B,但不能反过来)。我们通过使用 NetworkX 中的 to_undirected 函数将有向图转换为无向图来克服这个问题。
列表中使用的 Kruskal 算法版本与泥泞城市问题中使用的相同。我们将其从 optalgotools 导入以减少所需的代码量。

图 4.11 Kruskal 算法生成的 MST。所有包含在 MST 中的边都被突出显示。MST 中没有环,且树的总权重最小化。
MST 在现实世界中有着广泛的应用,包括网络设计、图像分割、聚类和设施定位问题。当处理涉及预算的问题时,如规划网络,MST 特别有用,因为它们允许所有节点以最低的总成本连接。如前所述,信息搜索算法可以用来找到本节所述的 MST,以及下一节讨论的最短路径算法。
4.3 最短路径算法
信息搜索算法可以通过使用关于问题的知识(领域特定知识)来剪枝搜索,以找到两个节点之间的最短路径。这种知识以启发式函数的形式给出对目标距离的估计。信息搜索算法的例子包括岗位攀登、束搜索、最佳优先、A* 和收缩层次。以下小节将详细讨论这些算法。
4.3.1 岗位攀登算法
假设你正在试图在浓雾中攀登山顶。上山和下山的路径只有一条,但你不确定确切的山顶位置。因此,你只能通过查看你上一步之后的进展来判断你是否已经上山或下山。你如何知道你已经到达山顶?一个不错的猜测是当你不再上山时!
从一个已知(非优化)的函数解或初始状态开始,岗位攀登算法检查该解决方案的邻居,并选择更优化的邻居。这个过程会重复进行,直到找不到更好的解决方案,此时算法终止。
岗位攀登算法是一种局部贪婪搜索算法,它试图通过结合领域特定知识或启发式信息来提高深度优先搜索的效率,因此它可以被认为是一种有信息的深度优先搜索算法。算法 4.2 中应用于图搜索的岗位攀登算法的伪代码假设为最小化问题。
算法 4.2 岗位攀登算法
Inputs: Source node, Destination node
Output: Route from source to destination
Initialize current ← random route from source to destination
Initialize neighbours ← children of current
While min(neighbours) > current do
Set current ← min(neighbours)
Update neighbours ← children of current
Return current as the route from source to destination
该算法在将节点后继者(根据它们的启发式值)添加到待扩展列表之前,对它们进行排序。这种算法在内存和计算开销方面要求非常低,因为它只是简单地记住当前的后继者作为当前正在工作的路径。它是一种非穷举技术;它不会检查整个树,因此其性能将相对较快。然而,尽管这种算法在凸问题上相对表现良好,但具有多个局部最大值的函数通常会得到一个不是全局最大值的答案。当存在高原(一组所有解决方案都同样优化的局部解决方案)时,它的表现也较差。
如图 4.12 所示,根据初始状态,岗位攀登算法可能会陷入局部最优。一旦它达到山顶,算法将停止,因为任何新的后继者都会在山脚下。这类似于在雾中爬山,达到一个较小的顶峰,并认为你已经到达了主峰。

图 4.12 根据初始状态,岗位攀登算法可能会陷入局部最优。一旦它达到一个顶峰,算法将停止,因为任何新的后继者都会在山脚下。
简单的岗位攀登、最陡上升岗位攀登、随机岗位攀登和随机重启岗位攀登都是岗位攀登算法的变体,如图 4.13 所示。

图 4.13 岗位攀登算法的变体
简单爬山法逐个检查邻近节点,并选择第一个优化目标函数的邻近节点作为下一个要探索的节点。最陡上升或最陡下降爬山法是简单爬山算法的一种变体,它首先检查当前状态的所有邻近节点,并选择一个最接近目标状态的邻近节点。随机爬山法是爬山算法的一个随机版本,它随机选择一个邻近节点,而不检查所有邻近节点。此算法根据该邻近节点的改进量决定是否移动到该邻近节点或检查另一个节点。随机重启爬山法或首次选择爬山法遵循尝试策略,并迭代搜索节点,在每一步中选择最佳节点,直到找到目标。如果陷入局部最大值,它将从新的随机初始状态重新开始过程。与其他爬山变体相比,如果存在高原、局部最优和脊,此算法更有可能到达目的地。
梯度下降算法
梯度下降算法在机器学习中被广泛用于训练模型和进行预测。梯度下降和爬山算法是两种基本不同的算法,不能相互混淆。与爬山不同,梯度下降可以看作是向山谷底部徒步。梯度下降是一种迭代算法,它观察局部邻居的斜率,并朝着斜率最陡或负梯度方向移动以优化连续可微函数。在最大化问题的情形下,梯度上升则朝着正梯度方向移动以优化目标函数。
如果函数是凸函数(即,如果任何局部最小值也是全局最小值)并且学习率选择得当,梯度下降算法通常收敛到全局最小值。爬山算法是一种启发式贪婪算法,容易陷入局部最优。它主要用于离散优化问题,如旅行商问题,因为它不需要目标函数是可微的。
假设我们有一个如图 4.14 所示的简单图。源节点是 S,目标节点是 G。

图 4.14 以图的形式表示的 8 个兴趣点(POIs)道路网络
此图可以通过找到一个包含原始图所有顶点且连通且无环的生成树来转换为树,如图 4.15 所示。

图 4.15 以树的形式表示的 8 个兴趣点(POIs)道路网络
如您所见,从 S 到 G 有多种路径,每种路径的成本不同。遵循爬山算法,S 和 G 之间的最短路径将是 S→A→C→E→G,如图 4.16 所示。

图 4.16 使用爬山算法在 S 和 G 之间找到的最短路径
现在我们使用 8 个拼图问题来说明爬山方法。图 4.17 展示了爬山搜索的进展,使用不包括空白瓷砖的错位瓷砖数量作为启发式信息 h(n)。例如,在第 2 步中,瓷砖 1、4、6 和 7 放置错误,所以 h = 4。在第 3 步中,瓷砖 1 和 4 放置错误,所以 h = 2。

图 4.17 使用爬山算法解决 8 个拼图问题。在每次迭代中,算法探索相邻状态,寻找最小启发式值。
列表 4.3 展示了 Python 中爬山算法的简单实现。代码通过最小化下一个节点的启发式值来选择探索的节点。更复杂的版本,涉及生成最短路径,将在本章末尾介绍。
列表 4.3 使用爬山法解决 8 个拼图问题
import matplotlib.pyplot as plt
def Hill_Climbing(origin, destination, cost_fn):
costs = [cost_fn(origin)]
current = origin
route = [current]
neighbours = current.expand() ①
shortest = min(neighbours, key=lambda s: cost_fn(s)) ②
costs.append(cost_fn(shortest))
route.append(shortest)
while cost_fn(shortest) < cost_fn(current):
current = shortest
neighbours = current.expand()
shortest = min(neighbours, key=lambda s: cost_fn(s))
costs.append(cost_fn(shortest))
route.append(shortest)
if shortest == destination: ③
break
return route, costs
def misplaced_tiles(state: State): ④
flat = state.flatten() ④
goal = range(len(flat)) ④
return sum([0 if goal[i] == flat[i] else 1 for i in range(len(flat))])
① 可以使用 expand() 函数生成当前状态的邻居节点。
② “最近”的邻居是成本函数最低的(即错位瓷砖最少)。
③ 如果达到目标状态,则终止算法。
④ 计算并返回不在目标位置上的错位瓷砖数量。
注意:State 类和 visualize 函数在完整的列表中定义,可在本书的 GitHub 仓库中找到。
以下代码片段定义了拼图的初始状态和目标状态,并使用爬山法来解决拼图问题:
init_state = [[1,4,2],
[3,7,5],
[6,0,8]]
goal_state = [[0,1,2],
[3,4,5],
[6,7,8]]
init_state = State(init_state)
goal_state = State(goal_state)
if not init_state.is_solvable(): ①
print("This puzzle is not solvable.")
else:
solution, costs = Hill_Climbing(init_state, goal_state,
➥ misplaced_tiles) ②
plt.xticks(range(len(costs))) ③
plt.ylabel("Misplaced tiles") ③
plt.title("Hill climbing: 8 Puzzle") ③
plt.plot(costs) ③
visualize(solution) ③
① 检查是否有解决方案。
② 使用爬山法解决拼图问题
③ 绘制搜索进度,并可视化解决方案。
输出显示在图 4.18 中。

图 4.18 显示了 8 个拼图问题爬山解决方案的状态。每个后续状态都是通过将其成本与邻居相比最小化来选择的。由于 8 个拼图问题不仅有一个明确定义的目标状态,而且这个目标状态是可以实现的,算法的终止条件(达到目标)与“山峰”的“顶峰”相吻合(在这种情况下是一个山谷,因为它是一个最小化问题)。
由于 8 个拼图问题使用启发式作为成本,它本质上变成了一个最小化问题。这种实现与标准爬山算法的不同之处在于,由于总可以找到解决方案,并且图是完全连接的(您可以通过一些瓷砖移动的组合从任何状态转换到另一个状态),算法最终保证找到最优解。更复杂的问题通常会生成接近最优的解决方案。
4.3.2 光束搜索算法
束搜索算法试图最小化广度优先算法的内存需求,因此它可以被视为一种启发式广度优先算法。在爬山法在整个运行过程中保持一个最佳状态的同时,束搜索在内存中保持W个状态,其中W是束宽度。在每次迭代中,它为每个状态生成邻居并将它们放入与原始束中的状态相同的池中。然后,它在每个级别上从池中选择最佳的W个状态成为新的束,其余状态被丢弃。这个过程然后重复。该算法在每个级别上仅扩展前W个有希望的节点。
这是一个非穷尽搜索,但也是一个危险的过程,因为可能会错过目标状态。由于这是一个局部搜索算法,它也容易陷入局部最优。当w等于每个级别的节点数时,束搜索与 BFS 相同。因为存在可能丢弃导致最优解的状态的风险,所以束搜索是不完整的(它们可能不会以解结束)。
算法 4.3 展示了将束搜索算法应用于图搜索的伪代码。
算法 4.3 束搜索算法
Inputs: A source node, a destination node, and beam width w
Output: A route from source to destination
Initialize Seen ← nil
Initialize beam ← random w routes from source to destination
Add beam to seen
Initialize pool ← children of routes in the beam with consideration of seen + beam
Initialize last_beam ← nil
While beam is not last_beam do
Update last_beam ← beam
Update beam ← the best w routes from pool
If last_beam == beam then break
Add beam to seen
Update pool ← children of routes in the beam + beam
Return optimal route in beam
在 2.3.1 节中,您看到 BFS 具有指数复杂度O(b^d),其中b代表每个节点的最大分支因子,d是必须扩展以达到目标深度的深度。在束搜索的情况下,我们只在任何深度探索w×b个节点,与 BFS 相比,节省了许多不必要的节点。然而,找到最佳状态或路线需要排序,如果w×b是一个很大的数字,那么排序将是耗时的。可以使用束阈值来处理这个问题,其中根据启发式函数h(n)在某个阈值内选择最佳节点,并将所有超出此阈值的节点剪枝掉。
重新审视具有 8 个兴趣点的简单路由问题(图 4.14)并遵循束搜索算法(w = 2),S 和 G 之间的最短路径将是 S-A-C-E-G,如图 4.19 所示。

图 4.19 使用束搜索算法在 S 和 G 之间找到的最短路径。束宽度w = 2 时,在每个迭代中保持束中的两个状态。在生成束中每个元素的邻居后,仅保留最佳的w束。
以下列表展示了用于简单路由问题的基本束搜索实现。要查看如何初始化图以及如何生成可视化(它与列表 4.1 中的类似),请参阅 GitHub 仓库中的完整代码。
列表 4.4 使用束搜索的简单路由
import matplotlib.pyplot as plt
import networkx as nx
import heapq
from optalgotools.structures import Node
from optalgotools.routing import cost
G = nx.Graph() ①
G.add_nodes_from(["A", "B", "C", "D", "E", "F", "G", "S"]) ①
edges = [
("A", "B", {"weight": 4}),
("A", "C", {"weight": 3}),
("A", "S", {"weight": 2}),
("B", "D", {"weight": 3}),
("B", "S", {"weight": 4}),
("C", "E", {"weight": 4}),
("C", "D", {"weight": 5}),
("D", "E", {"weight": 6}),
("D", "F", {"weight": 5}),
("E", "F", {"weight": 7}),
("E", "G", {"weight": 3}),
("F", "G", {"weight": 3}),
] ①
G.add_edges_from(edges) ①
G=G.to_directed() ①
def Beam_Search(G, origin, destination, cost_fn, w=2, expand_kwargs=[],
➥ cost_kwargs=[]):
seen = set()
seen.add(origin)
last_beam = None
pool = set(origin.expand(**expand_kwargs)) ②
beam = []
while beam != last_beam:
last_beam = beam
beam = heapq.nsmallest(
w, pool, key=lambda node: cost_fn(G, node.path(),
➥ **cost_kwargs)) ③
current = beam.pop(0)
seen.add(current)
pool.remove(current)
children = set(current.expand(**expand_kwargs))
for child in children: ④
if child in seen: next ④
else: #D ④
if child == destination: ④
return child.path() ④
beam.append(child) ④
pool.update(beam)
return None ⑤
①创建一个有向图。
②使用原始类的 expand()方法获取节点的邻居,传递任何必要的参数。
③将池修剪到仅包含最佳 k 路径,传递任何必要的参数到成本函数。
④ 为每条路径生成子路径,通过在路径中添加一个额外的节点来实现。对于这些新路径中的每一个,它们会被拒绝(已探索)、添加到束中(然后到池中)或接受(因为它们到达了目的地)。
⑤ 如果找不到路径,则不返回任何内容。
此函数可以使用以下示例参数调用:
result = Beam_Search(
G,
Node(G, "S"),
Node(G, "G"),
cost,
expand_kwargs={"attr_name": "weight"},
cost_kwargs={"attr_name": "weight"},
)
可视化此算法的输出产生图 4.20,其中高亮线表示从 S 到 G 的解决方案路径。

图 4.20 使用束宽 w = 2 的解决方案。高亮线表示解决方案路径。
如您将在后续章节中看到,当将束搜索应用于现实生活中的路由问题时,生成束搜索中的子节点可能会变得非常复杂和耗时。
4.3.3 A* 搜索算法
A(发音为 A-star)算法是一种在路径查找和图遍历中广泛使用的启发式搜索算法。该算法是最佳优先搜索算法的一个特例。最佳优先搜索是一种混合深度优先搜索和广度优先搜索的方法,它根据到达节点的成本或从该节点到达目标的估计或启发式成本值来扩展最期望的未扩展节点。A* 算法考虑到达节点的成本和到达目标的估计成本,以找到最优解。
A* 算法的伪代码在算法 4.4 中展示。
算法 4.4 A* 算法
Inputs: Source node, Destination node
Output: Route from source to destination
Initialize A* heuristic ← sum of straight-line distance to source and destination
Initialize PQ ← min heap according to A* heuristic
Initialize frontier ← a PQ initialized with source
Initialize explored ← empty
Initialize found ← False
While frontier is not empty and found is False do
Set node ← frontier.pop()
Add node to explored
For child in node.expand() do
If child is not in explored and child is not in frontier then
If child is destination then
Update route ← child.route()
Update found ← True
Add child to frontier
Return route
A* 搜索试图通过结合实际成本和从给定状态到达目标的启发式成本估计来减少探索的总状态数。A* 的驱动力是基于最低值选择新的顶点(或节点)进行探索。评估函数 f(n) 的值使用以下公式计算:
| f(n) = g(n) + h(n) | 4.1 |
|---|
在方程 4.1 中,g(n) 是从源节点 S 到节点 n 已经走过的部分路径的实际成本。启发式信息 h(n) 可以是节点 n 和目标节点 G 之间的直线距离,或者某个其他函数。当所有节点的 h(n) = 0 时,A* 将表现得像均匀成本搜索(UCS),这在第 3.4.2 节中已解释,因此,无论到达目标的估计成本如何,都会扩展成本最低的节点。
在 加权 A 中,将一个常数权重添加到启发式函数中,如下所示:
| f(n) = g(n) + w × h(n) | 4.2 |
|---|
为了增加 h(n) 的重要性,w 应该大于 1。也可以使用动态权重 w(n)。启发式信息的选取对搜索结果至关重要。当且仅当启发式信息 h(n) 小于从节点 n 到目标状态的实际成本时,启发式信息 h(n) 是可接受的。这意味着可接受启发式永远不会高估到达目标状态的成本,并且只有当启发式函数接近真实剩余距离时,才能导致最优解。
A* 启发式算法通过以 贪婪 的方式选择下一个要探索的顶点,根据启发函数的值优先考虑节点。由于当 n 位于从 S 到 G 的直线上时,到起点和终点的距离之和最小,这个启发式优先考虑那些更接近起点到终点直线距离的节点。
为了更好地理解 A* 算法,让我们考虑这样一个简单问题:在一个 8 个兴趣点(POIs)路网中,寻找源节点 S 和目标节点 G 之间的最短路径。这是与图 4.14 相同的 POI 图,但每个节点都添加了启发值。启发信息的例子是图 4.21 中每个顶点上方显示的到目标点的直线距离。

图 4.21 显示了一个具有启发信息(到目标点的直线距离)的 8 个 POIs 路网图,每个顶点上方都标明了这些信息
图 4.22 展示了使用 A* 寻找从 S 到 G 的最短路径的步骤。

图 4.22 显示了在 8 个 POIs 路网中,从源节点 S 到目标节点 G 的 A* 步骤寻找最短路径。使用已发生的成本总和以及到目标点的距离作为启发值,以确定是否展开某个节点。
这个算法可能看起来很复杂,因为它似乎需要在多个地方存储不完整的路径及其长度。然而,使用递归最佳优先搜索实现可以优雅地解决这个问题,而无需显式存储路径。从每个节点到下界目标距离的质量极大地影响了算法的时间复杂度。给定的下界越接近真实距离,执行时间就越短。
我们可以将 A* 算法应用于简单的路由问题,如下所示(有关完整代码,包括图初始化和可视化,请参阅本书的 GitHub 仓库)。
列表 4.5 使用 A* 搜索进行简单路由
import matplotlib.pyplot as plt
from optalgotools.structures import Node
from optalgotools.routing import cost
import networkx as nx
import heapq
def A_Star(
G, origin, destination, heuristic_fn, cost_fn, cost_kwargs=[],
➥ expand_kwargs=[]
):
toDestination = heuristic_fn(G, origin, destination)
toOrigin = {}
route = []
frontier = list()
frontier.append(origin)
toOrigin[origin] = 0
explored = set()
found = False
while frontier and not found:
node = min(frontier, key=lambda node: toOrigin[node] +
➥ toDestination[node]) ①
frontier.remove(node)
explored.add(node)
for child in node.expand(**expand_kwargs): ②
if child not in explored and child not in frontier: ②
if child == destination: ②
route = child.path() ②
found = True ②
continue ②
frontier.append(child) ②
toOrigin[child] = cost_fn(G, child.path(), **cost_kwargs) ③
return route
① 根据启发值选择一个节点
② 展开节点的子节点,将它们添加到前沿或如果找到目的地则终止。
③ 在线为每个节点添加 toOrigin 值
列表 4.5 中的 A* 实现没有使用“真正的”A* 启发式算法,原因如下:
-
由于我们只有边权重而没有实际的空间数据(坐标)来确定每个节点,因此无法直接确定从任何节点到目的地的直线距离或 haversine 距离。为了解决这个问题,我创建了一个名为
dummy_astar_heuristic的函数,该函数为此示例返回静态、任意生成的距离。 -
由于与前一点相同的原因,无法提前确定从起点到任何节点(直线或其他)的距离。因此,我们使用已行驶的距离(即从起点到已探索节点的成本),并在算法发现新节点时更新该值。在本章的后面部分,我们将看到如何处理地理数据(如道路网络)允许我们在事先捕获此信息。
A_Star 可以如下调用,以下是一些示例参数:
result = A_Star(
G,
Node(G, "S"),
Node(G, "G"),
dummy_astar_heuristic,
cost,
expand_kwargs={"attr_name": "weight"},
cost_kwargs={"attr_name": "weight"},)
这给出了与波束搜索和爬山搜索相同的结果:路径为 S-A-C-E-G。
Haversine 距离
Haversine 公式用于根据两点在地球上的经纬度以及平均球面地球半径来计算两点之间的地理距离。这个距离也被称为大圆距离,并使用以下公式计算:
d = R × C 和 C = 2 × atan2(√a, √(1-a)) 4.3
在前面的公式中,a = sin²(∆lat/2) + cos(lat1) × cos(lat2) × sin²(∆lon/2),R 是地球半径(6,371 公里或 3,691 英里),d 是两点之间的最终距离。以下图显示了美国洛杉矶(34.0522° N, 118.2437° W)和西班牙马德里(40.4168° N, 3.7038° W)之间的 haversine 距离。

洛杉矶和马德里之间的 Haversine 距离
以下 Python 代码可以用来计算 Haversine 距离:
!pip install haversine ①
from haversine import haversine ①
LA = (34.052235, -118.243683) ②
Madrid = (40.416775, -3.703790) ③
distance = haversine(LA, Madrid) ④
print(distance)
① 安装 Haversine 包,并导入 haversine 函数。
② 以(纬度,经度)格式设置两个点的坐标。
③ 以(纬度,经度)格式设置两个点的坐标。
④ 计算千米距离。
optalgotools 中 A* 启发式实现的默认行为是计算距离,好像地球是平的一样。对于局部搜索,这会产生最佳结果。如果搜索区域的大小较大,最好通过将 optalgotools.utilities.haversine_distance 传递到 measuring_dist 参数中来计算距离,该参数考虑了地球的曲率。
4.3.4 层次方法
当面对更大规模的路线问题时,例如涉及整个国家或具有数百万个节点的图时,使用基本方法(如 Dijkstra 算法)是不可思议的。在上一章中,你看到双向 Dijkstra 算法比 Dijkstra 算法快两倍。然而,对于交互式应用(如导航应用)需要更快的路由算法。实现这一目标的一种方法是在服务器上预先计算某些路线并将它们缓存起来,以便对用户查询的响应时间合理。另一种方法涉及剪枝搜索空间。分层搜索算法通过生成可接受启发式算法来剪枝搜索空间,这些算法抽象了搜索空间。
注意:有关分层方法的一般方法细节,请参阅 Leighton、Wheeler 和 Holte 的“更快的最优和次优分层搜索” [1]。
高速公路分层涉及将“层级”分配给道路网络图中的每条道路。这区分了道路段类型(例如,住宅道路、国家道路、高速公路)。这还通过相关数据得到补充,例如最大指定驾驶速度以及道路中的转弯次数。在为图生成启发式算法之后,数据将通过一个修改后的搜索函数(双向 Dijkstra 算法、A*等)传递,该函数考虑目的地距离和潜在扩展节点类型。高速公路分层算法通常在距离目标更远时将高速公路视为可行的扩展节点,并将开始包括国家道路,最后是住宅街道,随着接近目的地。在旅行过程中,不太重要的道路会与更重要的道路合并(例如,住宅道路与国家道路合并,国家道路与高速公路合并)。这使我们能够避免探索数百万个节点。
以从纽约到迈阿密的远程驾驶旅行为例。在旅行的开始阶段,你需要导航到高速公路或州际公路的当地道路。在旅行的中间部分,你将仅在州际公路或高速公路上驾驶。接近目的地时,你将离开州际公路,再次走上当地道路。虽然这种方法是有道理的,但也有一些缺点。首先,算法忽略了人类更喜欢驾驶的道路类型。虽然高速公路对于给定的路线可能是有意义的,但用户可能更喜欢走当地道路(例如,开车去住在附近的朋友的房子)。其次,高速公路分层没有考虑交通等因素,交通经常波动,给“最优”路线增加了显著的成本。你可以在 Sanders 和 Schultes 的文章“高速公路分层加速精确最短路径查询” [2]中了解更多关于高速公路分层的信息。
收缩层次(CH)算法是另一种分层方法。它是一种加速技术,通过基于节点收缩的概念剪枝搜索空间来提高最短路径计算的性能。例如,对于一个 80 英里单源单目的最短路径搜索查询,双向 Dijkstra 算法探索了 220,000 个节点,单向 A探索了 50,000 个节点,双向 A通过探索大约 25,000 个节点来改进这些结果。收缩层次通过只探索大约 600 个节点来解决相同的问题。这使得 CH 比 Dijkstra 的、双向 Dijkstra 的以及 A*都要快得多。
注意:收缩层次在 Geisberger 等人 2008 年的“收缩层次:道路网络中更快、更简单的分层路由”文章[3]中被引入。80 英里单源单目的最短路径搜索查询在GraphHopper博客上进行了讨论(mng.bz/n142)。
CH 算法包含两个主要阶段:
-
预处理阶段是节点和边根据某些重要性概念进行分类的地方。重要的节点可以是主要城市、主要交叉口、连接城市两边的桥梁,或者最短路径经过的兴趣点。每个节点根据从最不重要到最重要的级别进行收缩。在收缩过程中,向图中添加了一组快捷边以保留最短路径。
-
查询阶段是在预处理图上运行双向 Dijkstra 搜索(或任何其他搜索)的地方,只考虑越来越重要的边。这导致有选择地忽略不那么重要的节点,从而整体提高查询速度。
值得注意的是,CH 算法主要是一个预处理算法,这意味着它在查询最短路径之前使用。这个预处理阶段需要一些时间,但一旦完成,查询阶段就非常快。该算法可以处理大型图,并且可用于各种类型的图,而不仅仅是道路网络。让我们更详细地探讨这两个阶段。
CH 预处理阶段
预处理阶段以原始图作为输入,并返回用于查询阶段的增强图和节点顺序。
假设一个加权有向图 G = (V,E)。该图的节点根据节点重要性进行排序。在道路网络的情况下,节点重要性可以基于道路类型:住宅道路、国家道路、以及连接城市两边的桥梁或最短路径经过的兴趣点。基本直觉是,靠近源点或目标点时,我们通常考虑住宅道路;远离源点或目标点时,考虑国家道路;而更远离源点或目标点时,考虑高速公路是有意义的。影响节点重要性的其他启发式方法包括最大速度、通行费率、转弯次数等。
一旦确定了节点顺序,顶点集或节点就按重要性排序:V = {1,2,3…,n}。使用以下程序按此顺序收缩或删除节点:
for each pair (u,v)and (v,w)of edges:
if <u,v,w> is a unique shortest path then
add shortcut(u,w) with weight ω(<u,v,w>)or ω(<u,w>)+ω(<v,w>)
如图 4.23 所示,节点 v 可以从图 G 中收缩。如果需要,应添加一个成本为 5 的捷径或边,以确保在收缩 v 后,u 和 w 之间的最短距离保持不变或保持相同。收缩节点 v 意味着用捷径替换通过 v 的最短路径。新的图称为 overlay graph 或 augmented graph(即具有增强边的图)。此图包含与初始图相同的顶点集和所有边,以及用于在原始图中保留最短距离的所有添加的边(捷径)。

图 4.23 节点收缩操作—括号中的数字表示添加的捷径的成本。
当收缩节点 v 时,如果存在一条从 u 到 w 的路径 P,且 w(P) <= w(<u,v,w>),则不需要任何捷径。这条路径被称为 witness path,如图 4.24 所示。

图 4.24 证人路径—从 P 到 w 存在另一条更短的路径,因此在收缩 v 时不需要捷径。
在 CH 预处理阶段,由于节点是根据重要性排序的,因此节点可以迭代收缩,并添加一个额外的捷径弧以保留短距离并形成增强图。我们最终得到一个收缩层次结构,每个节点都有一个覆盖图。这种预处理是在离线完成的,增强图在查询阶段稍后使用。
让我们考虑一个具有四个节点的简单图。图 4.25 显示了收缩过程的步骤。我们将按照从最不重要到最重要的节点的顺序(即从 1 到 n 的重要性或层次级别)收缩每个节点。这个过程将形成捷径,这将使我们能够更快地搜索图,因为我们能够忽略已经被剪枝的节点。初始图如图 4.25a 所示:
-
通过收缩最不重要的节点,节点 1,由于相邻节点 2 和 4 之间的最短路径不经过节点 1,因此没有任何变化(如图 4.25b 所示)。
-
继续前进并收缩下一个最重要的节点,节点 2,我们现在已经改变了 1→3、1→4 和 3→4 的最短路径。我们可以通过创建新的边(捷径)来编码这些最短路径。括号中的数字表示添加的捷径的成本(如图 4.25c 所示)。
-
收缩节点 3 不会引起任何变化,因为节点 2 和 4 之间存在一条更短的路径,且不经过节点 3(如图 4.25d 所示)。
-
我们不需要收缩节点 4,因为它是图中的最后一个节点(如图 4.25e 所示)。
收缩过程后的生成覆盖图如图 4.25f 所示。节点根据重要性排序。

图 4.25 CH 预处理阶段的示例
合并的顺序不会影响 CH 的成功,但它将影响预处理和查询时间。一些合并排序系统最小化增强图中添加的快捷路径数,从而降低整体运行时间。
首先,我们需要使用某种重要性概念,并按重要性递减的顺序将所有节点保持在优先队列中。边差、懒惰更新、合并邻居的数量和快捷覆盖(所有内容将简要解释)是重要性标准的一些例子。图中每个节点的 重要性 是其 优先级。这个指标指导了节点合并的顺序。这个 优先级 是动态的,并且随着节点的合并而持续更新。典型的重要性标准包括
-
懒惰更新—在从优先队列中移除之前,更新优先队列顶部的节点(即具有最小优先级的节点)。如果更新后此节点仍然位于顶部,它将被合并。否则,新的最顶部节点将以相同的方式进行处理。
-
边差(ED)—一个节点的边差是需要添加的边数与需要移除的边数之差。我们希望最小化添加到增强图中的边数。对于一个图中的节点 v,假设
-
in(v) 是入度(即进入节点的边的数量)
-
out(v) 是出度(即从节点发出的边的数量)
-
deg(v) 是节点的总度数,它是其入度和出度的总和,因此 deg(v) = in(v) + out(v)
-
add(v) 是添加的快捷路径数
-
ED(v) 是合并节点 v 后的边差,它由 ED(v) = add(v) – deg(v) 给出
下两个图显示了边差是如何计算并用于在合并边节点如 A(图 4.26)和中心节点如 E(图 4.27)之间进行选择的。
-

图 4.26 边节点 A 情况下的边差

图 4.27 中心节点情况下的边差。括号中的数字表示添加的快捷路径的成本。
-
合并邻居的数量—这反映了节点在地图上的分布情况。最好避免在图的小区域内合并所有节点,并确保合并过程中的均匀性。我们首先合并合并邻居数量最少的节点。
-
快捷覆盖—这种方法近似地表示节点不可避免的程度(例如,连接城市两部分的桥梁)。它代表节点的邻居数量,因此表示在合并节点后我们需要创建多少快捷路径到或从它们,因为它们是不可避免的节点。快捷覆盖数较少的节点将首先合并。
节点的优先级估计收缩节点的吸引力,可以是先前描述的重要性标准(如边差异、收缩邻居的数量和捷径覆盖)的加权线性组合。在每次迭代中提取最不重要的节点。收缩过程可能会影响节点的重要性,因此我们需要重新计算这个节点的重要性。然后,将新更新的重要性与优先队列顶部的节点(重要性最低)进行比较,以决定是否需要收缩这个节点。更新后重要性最小的节点总是被收缩。
CH 查询阶段
在 CH 查询阶段,我们应用双向 Dijkstra 算法以找到源节点和目标节点之间的最短路径,如下所示(图 4.28):
-
Dijkstra 算法从源节点出发仅考虑边 u,v 其中 level(u) > level(v),因此你只想放松比你在该迭代中放松的节点级别高的节点。这被称为 向上图。在 Dijkstra 算法的上下文中,放松一个节点是指通过考虑通过相邻节点更短的路径来更新从源节点到达该节点的估计距离或成本的过程。这有助于细化从源节点到节点的最短路径的估计。
-
Dijkstra 算法从目标节点出发仅考虑边 u,v 其中 level(u) < level(v),因此你只想放松比你在该迭代中放松的节点级别低的节点。这被称为 向下图。

图 4.28 CH 查询阶段
一个 CH 示例
考虑以下具有任意节点排序的网络(图 4.29)。圆圈中的数字是节点将被收缩的顺序。边上的数字代表成本。

图 4.29 一个 CH 示例
让我们在该图上运行 CH 算法以获取该道路网络中两个节点之间的最短路径。以下步骤显示了如何应用 CH 算法:
- 收缩节点 1—不需要添加捷径,因为我们没有丢失最短路径(图 4.30)。

图 4.30 使用任意节点排名进行图收缩—收缩节点 1
- 收缩节点 2—不需要添加捷径,因为我们没有丢失最短路径(图 4.31)。

图 4.31 使用任意节点排名进行图收缩—收缩节点 2
- 收缩节点 3—需要添加一个捷径以保持 8 和 5 之间的最短路径,因为没有见证路径。添加的弧的成本是 7(图 4.32)。

图 4.32 使用任意节点排名进行图收缩—收缩节点 3
- 收缩节点 4—不需要添加捷径(图 4.33)。

图 4.33 使用任意节点排序进行图收缩——收缩节点 4
- 收缩节点 5——不需要添加任何捷径(图 4.34)。

图 4.34 使用任意节点排序进行图收缩——收缩节点 5
- 收缩节点 6——不需要添加任何捷径,因为 7 和 10 之间存在一个见证路径,即 7-11-12-10(图 4.35)。

图 4.35 使用任意节点排序进行图收缩——收缩节点 6
- 收缩节点 7——不需要添加任何捷径(图 4.36)。

图 4.36 使用任意节点排序进行图收缩——收缩节点 7
- 收缩节点 8——需要添加一个捷径以保持 9 和 12 之间的最短路径(图 4.37)。

图 4.37 使用任意节点排序进行图收缩——收缩节点 8
- 收缩节点 9——不需要添加任何捷径(图 4.38)。

图 4.38 使用任意节点排序进行图收缩——收缩节点 9
- 收缩节点 10——不需要添加任何捷径(图 4.39)。

图 4.39 使用任意节点排序进行图收缩——收缩节点 10
- 收缩节点 11——不需要添加任何捷径(图 4.40)。

图 4.40 使用任意节点排序进行图收缩——收缩节点 11
- 收缩节点 12——不需要添加任何捷径(图 4.41)。

图 4.41 使用任意节点排序进行图收缩——收缩节点 12
现在可以使用双向 Dijkstra 搜索查询收缩后的图。在以下图中,括号中的数字表示添加的捷径的成本。
图 4.42 中的向上图显示了从源到目标的前向 Dijkstra 搜索。实线代表已访问的边,粗实线代表源节点和汇合节点之间的最短路径。

图 4.42 使用 CH 算法解决道路网络问题——向上图
图 4.43 中的向下图显示了从目标到源的反向 Dijkstra 搜索。实线代表已访问的边,粗实线代表目标节点和汇合节点之间的最短路径。

图 4.43 使用 CH 算法解决道路网络问题——向下图
最小值在节点 12(4 + 8 = 12),因此节点 12 是汇合点(图 4.44)。

图 4.44 使用 CH 算法解决道路网络问题——汇合点
最短路径将是 1-10-12-8-5。然而,这条路径包含一个快捷(5-8)。实际弧(8-3-5)需要根据收缩过程中存储的快捷指针(节点 3)进行解包。实际最短路径是 1-10-12-8-3-4,成本为 12(图 4.45)。

图 4.45 使用 CH 算法解决道路网络问题——最短路径
列表 4.6 展示了 Python 中的实现。请注意,这里省略了图初始化的代码,因为它与之前的示例类似,但可以在书中 GitHub 仓库的完整列表中查看。同样,图可视化的代码也在完整列表中。
列表 4.6 预定节点顺序的收缩层次
import networkx as nx
shortcuts = {}
shortest_paths = dict(nx.all_pairs_dijkstra_path_length(G,
➥ weight="weight"))
current_G = G.copy() ①
for node in G.nodes:
current_G.remove_node(node) ②
current_shortest_paths = dict(
nx.all_pairs_dijkstra_path_length(current_G, weight="weight")
) ③
for u in current_shortest_paths:
if u == node:
continue
SP_contracted = current_shortest_paths[u]
SP_original = shortest_paths[u]
for v in SP_contracted:
if u == v or node == v:
continue
if (
SP_contracted[v] != SP_original[v]
and G.has_edge(node, u)
and G.has_edge(node, v)
):
G.add_edge(u, v, weight=SP_original[v],contracted=True) ④
shortcuts[(u,v)] = node ④
① 复制主图,以便节点只从副本中移除,而不是从主图中移除。
② 通过从复制的图中移除节点来收缩节点。
③ 重新计算最短路径矩阵,现在节点已被收缩。
④ 添加一个快捷边来替换改变后的最短路径,并跟踪它,以便在以后查询时可以解包它。
你会注意到,前面的代码为每个收缩创建了两个快捷边,一个从 P 到 v,另一个从 v 到 u 的反向边。由于我们使用的是无向图,这种重复没有影响,因为边 (u, v) 与边 (v, u) 相同。
查询生成的图需要一个简单的修改后的双向 Dijkstra 搜索,其中如果邻居节点在层次结构中低于当前节点,则取消资格进行扩展。为了本书的目的,我们将使用networkx.algorithms.shortest_paths.weighted.bidirectional_dijkstra,略有变化(只有比当前节点层次结构高的节点可以被探索)。作为列表 4.6 的延续,以下代码片段显示了查询过程。修改算法的完整代码可以在书中 GitHub 仓库的列表 4.6 中找到:
sln = bidirectional_dijkstra(G, 1, 5, hierarchy, weight="weight") ①
uncontracted_route = [sln.result[0]] ②
for u, v in zip(sln.result[:-1], sln.result[1:]): ]] ②
if (u, v) in shortcuts: ]] ②
uncontracted_route.append(shortcuts[(u, v)]) ②
uncontracted_route.append(v) ②
使用 NetworkX 运行双向 Dijkstra 算法。
② 解包任何标记为收缩的边,并生成解包路线。
前面的代码将生成一个可以如图 4.46 可视化的解包路线。

图 4.46 解包收缩边后的解决方案路径。双向 Dijkstra 算法返回的原始路线通过收缩边(8,5),然后解包为(8,3)和(3,5)。
收缩层次在预处理阶段消耗了大量的处理时间,但一个正确剪枝的图(即节点收缩顺序良好)可以允许进行更快的查询。虽然在一个有 21 个节点的图上,搜索空间的微小减少是可以忽略的,但某些图可以被剪枝高达 40%,在查询时可以节省显著的成本和时间。在 4.6 列表的例子中,从节点 1 到节点 5 的搜索探索了 11 个节点,而正常双向 Dijkstra 的原始节点是 16 个。这几乎减少了 33%!
4.4 将启发式搜索应用于路由问题
让我们再次看看第 3.5 节中介绍的多伦多大学路由问题。我们需要找到从女王公园的国王爱德华七世骑马雕像到信息科技中心的最短路径。搜索空间由一个道路网络表示,其中交叉口和兴趣点(包括起点和终点)是节点,边用于表示带有权重(例如,距离、旅行时间、燃料消耗、转弯次数等)的道路段。让我们看看我们如何可以使用本章讨论的启发式搜索算法找到最短路径。
4.4.1 路由的爬山搜索
列表 4.7 使用了来自optalgotools.routing的两个函数,这些函数生成随机和子路线。虽然实际的 HC 算法是确定的,但随机化的初始路线意味着在不同的运行中可以得到不同的结果。为了应对这种情况,我们将使用更高的n值,这允许更广泛的子路线多样性,因此更有可能得到最优(或近似最优)的解决方案。
列表 4.7 U of T 路由使用爬山算法
def Hill_Climbing(G, origin, destination, n=20):
time_start = process_time() ①
costs = [] ①
current = randomized_search(G, origin.osmid, destination.osmid) ②
costs.append(cost(G, current))
neighbours = list(islice(get_child(G, current), n)) ③
space_required = getsizeof(neighbours)
shortest = min(neighbours, key=lambda route: cost(G, route))
while cost(G, shortest) < cost(G, current):
current = shortest
neighbours = list(islice(get_child(G, current), n))
shortest = min(neighbours, key=lambda route: cost(G, route))
costs.append(cost(G, current))
route = current
time_end = process_time()
return Solution(route, time_end - time_start, space_required, costs)
①跟踪时间和成本以进行比较。
②随机生成一个初始路线。
③获取 k 个邻居(子节点)。
虽然列表 4.7 中的实现是确定的,但初始路线仍然是随机的。这意味着在不同的运行中可能会得到不同的结果。爬山搜索将返回一些相当不错的结果,因为路线函数中局部最优点的数量很少。然而,较大的搜索空间自然会有更多的局部最大值和平台,HC 算法会很快陷入困境。
图 4.47 显示了一个最终解决方案为 806.892 米,这恰好与第三章中 Dijkstra 算法生成的结果相同(一个最优解)。

图 4.47 使用爬山算法生成的最短路径解决方案。这里显示的解决方案使用了一个* n *值为 100,这增加了总处理时间,但返回了更好、更一致的结果。
4.4.2 路由的束搜索
路由的束搜索将与 HC 搜索具有类似的格式,但不同之处在于在每个迭代中都会保留一个用于比较的“束”解决方案。4.8 列表的完整代码,包括图初始化和可视化,可以在本书的 GitHub 仓库中找到。
列表 4.8 使用光束搜索算法的 U of T 路由
def get_beam(G, beam, n=20):
new_beam = []
for route in beam:
neighbours = list(islice(get_child(G, route), n)) ①
new_beam.extend(neighbours)
return new_beam
def Beam_Search(G, origin, destination, k=10, n=20):
start_time = process_time()
seen = set() ②
costs = [] ②
beam = [randomized_search(G, origin.osmid, destination.osmid) for _ in ②
➥ range(k)] ②
for route in beam: ③
seen.add(tuple(route))
pool = []
children = get_beam(G, beam, n)
costs.append([cost(G, r) for r in beam])
for r in children:
if tuple(r) in seen:
continue
else:
pool.append(r)
seen.add(tuple(r))
pool += beam
space_required = getsizeof(pool)
last_beam = None
while beam != last_beam: ④
last_beam = beam
beam = heapq.nsmallest(k, pool, key=lambda r: cost(G, r))
for route in beam:
seen.add(tuple(route))
pool = []
children = get_beam(G, beam, n)
costs.append([cost(G, r) for r in beam])
for r in children:
if tuple(r) in seen:
continue
else:
pool.append(r)
seen.add(tuple(r))
pool += beam
space_required = (
getsizeof(pool) if getsizeof(pool) > space_required else
➥ space_required
)
route = min(beam, key=lambda r: cost(G, r)) ⑤
end_time = process_time()
return Solution(
route, end_time - start_time, space_required, np.rot90(costs)) ⑥
① 为光束中的每条路线生成子路线。
② 初始化空集合以跟踪已访问节点和路径成本。
③ 已看到的路线必须转换为元组,以便它们是可哈希的,可以存储在集合中。
④ 在生成新光束不再找到更好的解决方案之前,在每个迭代中保留 k 条最佳路线。
⑤ 最终路线是最后光束中的最佳路线。
⑥ 返回最终路线、其成本、处理时间和所需空间。
光束搜索在路由中特别昂贵,因为它们需要为每个光束生成多个子路线。像 HC 一样,生成更多子路线会导致搜索空间的更广泛渗透,因此更有可能返回一个接近或达到最优解的解决方案。图 4.48 显示了由光束搜索生成的最终解决方案。

图 4.48 使用光束搜索算法的最短路径。此解决方案使用k = 10 和n = 20,这意味着为光束中的每条路线生成 20 条路线,并为每个新光束保留前 10 条路线。较低的k和n值将提高处理时间,但会降低生成接近最优或最优解决方案的可能性。
4.4.3 路由的 A*
下一个列表显示了我们可以如何使用 A*搜索找到两个感兴趣点之间的最短路线。
列表 4.9 使用 A*的 U of T 路由
import osmnx
from optalgotools.routing import (cost, draw_route, astar_heuristic)
from optalgotools.structures import Node
from optalgotools.algorithms.graph_search import A_Star
from optalgotools.utilities import haversine_distance
reference = (43.661667, -79.395) ①
G = osmnx.graph_from_point(reference, dist=300, clean_periphery=True,
➥ simplify=True) ②
origin = (43.664527, -79.392442) ③
destination = (43.659659, -79.397669) ④
origin_id = osmnx.distance.nearest_nodes(G, origin[1], origin[0]) ⑤
destination_id = osmnx.distance.nearest_nodes(G, destination[1], ⑤
➥ destination[0]) ⑤
origin = Node(graph=G, osmid=origin_id) ⑥
destination = Node(graph=G, osmid=destination_id) ⑥
solution = A_Star(G, origin, destination, astar_heuristic, ⑦
➥ heuristic_kwargs={"measuring_dist": haversine_distance}) ⑦
route = solution.result
print(f"Cost: {cost(G,route)} m") ⑧
print(f"Process time: {solution.time} s") ⑧
print(f"Space required: {solution.space} bytes") ⑧
print(f"Explored nodes: {solution.explored}") ⑧
draw_route(G,route)
① 将多伦多国王学院环道设为参考。
② 创建一个图。
③ 将爱德华七世骑马雕像设为起点。
④ 将多伦多大学的信息技术中心设为目标。
⑤ 获取最近节点的 osmid。
⑥ 将源节点和目标节点转换为节点。
⑦ 使用 A*找到最短路径。
⑧ 打印成本、处理时间、所需空间和已探索的节点,并绘制最终路线。
A搜索的最优性取决于使用的启发式函数。在这种情况下,返回的解决方案不是最优的,但所达到的极高处理速度对于大多数应用来说更为重要。图 4.49 显示了由 A搜索生成的最终解决方案。

图 4.49 使用 A*算法的最短路径。更接近实际成本的启发式函数将返回更好的结果。
4.4.4 路由的收缩层次
为了在道路网络图中运行 CH,我们首先需要按重要性对节点进行排序,然后对图进行收缩。对于这个例子,我们选择边差异(ED)作为节点重要性的度量。
列表 4.10 使用 CH 的 U of T 路由
def edge_differences(G, sp):
ed = {}
degrees = dict(G.degree)
for node in G.nodes:
req_edges = 0
neighbours = list(G.neighbors(node))
if len(neighbours)==0: ed[node] = - degrees[node] ①
for u, v in G.in_edges(node):
for v, w in G.out_edges(node):
if u == w: continue ②
if v in sp[u][w]:
req_edges += 1
ed[node] = req_edges - degrees[node] ③
return dict(sorted(ed.items(), key=lambda x: x, reverse=True))
① 一些节点实际上是死胡同,它们没有出边。这些节点的 ED 等于它们的度数。
② 我们可以忽略双向边——一个起点和终点相同的节点的一个入边和一个出边。
③ 边的差异是需要添加到图中边的数量与节点的度数之间的差异。
将图收缩的过程就像为每条被收缩操作改变的路径添加一条边一样简单。图收缩的完整代码可以在本书的 GitHub 仓库中找到。收缩的边用名为midpoint的属性标记,该属性存储了被收缩的节点的 ID。遵循类似于列表 4.6 中使用的修改后的双向 Dijkstra 算法,可以使用以下代码片段轻松地解包最终路由:
def unpack(G, u,v):
u = int(u)
v = int(v)
if "midpoint" in G[u][v][0]:
midpoint = G[u][v][0]["midpoint"]
return unpack(G,u,midpoint) + unpack(G,midpoint, v) ①
return [u]
route = []
for u,v in zip(solution.result[:-1], solution.result[1:]): ②
route.extend(unpack(G,u,v))
route += [solution.result[-1]]
print(route)
① 对于每个解包的中点,递归地解包产生的两个边,因为一些收缩边可能包含其他收缩边。
② 解包收缩路由中的每个节点对。
GitHub 仓库还包含了 CH 路由的完整 Python 实现。生成的路由与正常双向 Dijkstra 算法(如第三章中所示)显示的路由相同。如果你还记得,在第三章中运行正常双向 Dijkstra 算法的结果是在搜索过程中探索了 282 个节点。对于我们的 CH 结果,只探索了 164 个节点,这意味着搜索空间减少了超过 40%!因此,虽然算法的优化性保持不变,但收缩层次结构允许在合理的时间内搜索更大的空间。
表 4.1 比较了本章讨论的搜索算法在应用于 U of T 路由问题时的表现。第三章中也有一个类似的表格,用于盲搜索算法。
表 4.1 比较了基于时间和空间复杂度的信息搜索算法,其中b是分支因子,w是光束宽度,d是最浅的图深度,E是边的数量,V是顶点的数量
| 算法 | 成本(米) | 时间(秒) | 空间(字节) | 探索 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|---|---|---|
| 岩石上升法 | 806.892 | 21.546 | 976 | 400 节点 | O(∞) | O(b) |
| 光束搜索 | 825.929 | 44.797 | 1,664 | 800 节点 | O(wd) | O(wb) |
| A*搜索 | 846.92 | 0.063 | 8,408 | 80 节点 | O(b^d) | O(b^d) |
| 双向 Dijkstra 算法的 CH | 806.892 | 0.0469 | 72 | 164 节点 | O(E + VlogV) | O(b^d/2) |
注意:双向 Dijkstra 算法的 CH 所列出的时间仅用于查询。请记住,预处理步骤通常成本很高。在这种情况下,收缩 404 个节点的道路网络大约需要 24.03125 秒。
虽然岩石上升法和光束搜索产生了可尊敬的结果,但它们在时间上的成本太高,对于更大的图来说没有用。A*给出了最快的搜索结果,但非最优的启发式函数,并且需要大量的空间来存储启发式值,因此它也有自己的缺点。双向 Dijkstra 算法的 CH 是表 4.1 中唯一保证最优性的算法,但昂贵的预处理步骤可能不适合所有应用。
在比较搜索算法时,了解任何给定问题的约束条件非常重要,并基于这些约束条件选择算法。例如,某些爬山算法的实现可能导致快速退出条件。如果目标是最大化解决问题的数量(并且如果局部最大值是可以接受的结果),HC 算法会导致快速解决方案,具有一定的最优性。另一方面,预处理密集型算法如 CH 提供了极低的存储成本(在双向搜索实现时更是如此),以及快速搜索保证最优解(如果使用 Dijkstra 算法)。对于预处理不是问题的使用量大的实现(例如,Uber),收缩层次结构是一个可行的选择。实际上,本书中使用的 osrm 包主要基于收缩层次结构的一个实现。
Pandana 是一个用于网络分析的 Python 库,它使用 CH 来计算最短路径和快速旅行可达性指标。在 Pandana 中,CH 的后端代码是用 C++编写的,但可以使用 Python 访问。Pyrosm 是另一个用于读取和解析 OpenStreetMap 数据的 Python 库。它与 OSMnx 类似,但更快,并且与 Pandana 兼容。
下一个列表是一个代码片段,它使用 Pandana 中实现的 CH 算法计算选定城市中感兴趣设施的最短距离。完整的代码可在本书的 GitHub 仓库中找到。
列表 4.11 使用 CH 计算到设施的最近距离
from pyrosm import OSM, get_data
import numpy as np
import matplotlib.pyplot as plt
osm = OSM(get_data("Toronto")) ①
nodes, edges = osm.get_network(network_type="driving", nodes=True) ②
hospitals = osm.get_pois({"amenity": ["hospital"]}) ③
G = osm.to_graph(nodes, edges, graph_type='pandana') ④
hospitals['geometry'] = hospitals.centroid ⑤
hospitals = hospitals.dropna(subset=['lon', 'lat']) ⑤
G.precompute(1000) ⑥
G.set_pois(category='hospitals', maxdist=1000, maxitems=10, ⑦
➥ x_col=hospitals.lon, y_col=hospitals.lat) ⑦
nearest_five = G.nearest_pois(1000, "hospitals", num_pois=5) ⑧
① 获取感兴趣的城市、地区或国家的数据。
② 从具有“驾驶”类型的道路网络中获取节点和边。
③ 获取城市中特定设施的感兴趣点。
④ 创建网络图。
⑤ 确保所有医院都表示为点。
⑥ 预先计算 1,000 米内的距离。
⑦ 将医院附加到 Pandana 图上。
⑧ 对于每个节点,找到距离 1,000 米内最近的五家医院。
在这个例子中,使用 OpenStreetMap 获取多伦多的数据,并创建一个子集以包含该城市医院的数据。然后创建一个 Pandana 对象,并预先计算范围查询,给定一个视距(例如,1,000 米)来表示此距离内的可达节点。对于网络中的每个节点,我们可以使用 Pandana 中实现的快速 CH 算法找到距离 1,000 米内最近的五家医院。
在本书的下一部分,我们将从模拟退火算法和禁忌搜索算法开始,探讨基于轨迹的算法。这些算法改进了局部搜索,并且比之前讨论的贪婪算法更不容易陷入局部最优,因为贪婪算法只接受改进的移动。
摘要
-
有信息搜索算法使用领域特定知识或启发式信息来简化搜索过程,同时力求找到最优解或在必要时接受近似最优解。
-
有信息搜索算法可用于解决最小生成树(MST)问题,以及在图中找到两个节点之间的最短路径。
-
Borůvka 算法、Jarník-Prim 算法和 Kruskal 算法是解决最小生成树(MST)问题的启发式搜索算法。MST 是包含所有其他连通加权图生成树中最小权重的树。Kruskal 算法是一种贪婪算法,通过反复添加不会产生环的下一个最短边来计算无向连通加权图的最小生成树。
-
Hill climbing(HC)、beam search、best-first search、A*算法和收缩层次(CH)是有信息搜索算法的例子,可用于在两个节点之间找到最短路径。
-
HC 算法是一种局部贪婪搜索算法,它通过结合领域特定知识或启发式信息来提高深度优先搜索的效率。
-
Beam search 在由 beam width 定义的有限预定义集合内扩展最有希望的节点。
-
最佳优先搜索是一种贪婪算法,它仅基于启发式信息始终扩展离目标节点最近的节点。
-
A*算法是结合了实际成本和从给定状态到达目标状态的启发式成本估计的最佳优先算法的特殊情况。
-
CH 是一种加速技术,用于提高路径查找的性能。在预处理阶段,每个节点按照重要性顺序(从最不重要到最重要)进行收缩,并添加捷径以保留最短路径。然后应用双向 Dijkstra 算法到结果增强图,以计算源节点和目标节点之间的最短路径。
第二部分. 基于轨迹的算法
现在第一部分已经为你打下了优化的坚实基础,我们将继续探索优化算法的领域,重点关注基于轨迹的算法。本书的这一部分,包含两个章节,将把你的优化知识提升到新的水平。
在第五章中,你将学习关于基于轨迹的优化算法,特别是模拟退火算法——你将发现模拟退火如何应用于解决连续和离散优化问题。你将以函数优化作为连续优化的例子进行探索,以数独游戏作为约束满足问题的实例进行挑战,深入研究排列问题,如旅行商问题,甚至将模拟退火应用于现实世界问题,如优化半挂车的配送路线。
第六章将向你介绍禁忌搜索作为另一种基于轨迹的优化算法。你将学习局部搜索的基本原理以及禁忌搜索如何在此基础上构建。这一章节将带你解决约束满足问题、连续优化问题、路由问题以及制造业中装配线的平衡问题。
到这部分结束时,你将深入理解基于轨迹的优化算法及其能够有效解决的多样化问题领域。这些章节将为你提供宝贵的工具,以应对广泛应用中的复杂优化问题。
第五章:模拟退火
本章涵盖
-
介绍轨迹型优化算法
-
理解模拟退火算法
-
以函数优化为例,解决连续优化问题
-
以数独游戏问题为例,解决约束满足问题
-
以 TSP(旅行商问题)为例,解决离散问题
-
解决现实世界的半挂车配送路线问题
在本章中,我们将探讨模拟退火作为一种轨迹型元启发式优化技术。我们将讨论该算法的不同元素及其适应性方面。将展示一些案例研究,以展示这种元启发式算法解决连续和离散优化问题的能力。
5.1 轨迹型优化介绍
想象一下,你自己在一次徒步旅行中寻找一个崎岖地形中众多山谷和山丘中的最低谷。你没有全局信息或显示最低谷位置的地图。你通过随机选择一个方向开始徒步旅行。你一步步地移动,直到你被困在一个被山丘包围的局部山谷中。你对这个位置并不十分满意,因为你相信在这个区域可能还有更低的山谷,可能就在山丘后面。你的好奇心驱使你爬上一座山丘,以寻找最低谷。
这正是模拟退火(SA)所做的事情。模拟退火算法的基本思想是使用一种随机搜索,遵循试错方法,接受能改进目标函数的变化,同时也保留一些不理想的变化。例如,在最小化问题中,任何能降低目标函数值的移动或变化都将被接受。然而,某些增加目标函数值的移动也会以一定的概率被接受。SA 是一种轨迹型元启发式算法,可用于寻找复杂优化问题的全局最优解。
通常来说,元启发式算法可以分为轨迹型和种群型算法,如图 5.1 所示。

图 5.1 优化算法的探索与开发
轨迹型元启发式算法或S 元启发式算法,如 SA 或禁忌搜索,使用单个搜索代理以分段方式在搜索空间中移动。更好的移动或解决方案总是被接受,而不太好的移动可以以一定的概率被接受。步骤或移动在搜索空间中绘制出轨迹,该轨迹有非零概率达到全局最优。
相比之下,种群型算法或P 元启发式算法,如遗传算法、粒子群优化和蚁群优化,使用多个代理来搜索最优或近似最优的全局解。
由于初始种群具有很大的多样性,基于种群的算法自然更偏向于探索,而基于单个或轨迹的算法则更偏向于利用。下一节将更详细地解释 SA 算法。
5.2 模拟退火算法
无论你需要解决的是复杂的非线性非微分函数优化问题,还是像数独这样的谜题游戏,学术课程排课问题,旅行商问题(TSP),网络设计问题,任务分配问题,电路划分和放置问题,生产计划和调度问题,甚至是网球锦标赛规划问题,SA 都可以作为这些不同连续和离散优化问题的通用求解器。
在我们使用它来解决不同问题之前,让我们先看看这个求解器的细节。我们将首先了解一下物理退火过程,这是 SA 的灵感来源。
5.2.1 物理退火
退火作为一种热处理过程,在包括冶金、玻璃制造和陶瓷在内的各个行业中已经使用了几个世纪。例如,在制造玻璃瓶的背景下,退火消除了由于成型而产生的玻璃中的应力和应变。这是一个重要的步骤,如果不这样做,玻璃可能会因为冷却不均匀而产生的张力积累而破碎。在瓶子冷却到室温后,它们会被检查并最终包装。
退火会改变材料的性质,如强度和硬度。这个过程将材料加热到高于再结晶温度,保持适当的温度,然后冷却材料。随着温度的降低,分子的流动性降低,分子倾向于以晶体结构(图 5.2)排列自己。

图 5.2 温度对分子流动性的影响
对齐结构是系统的最低能量状态。为了确保这种对齐能够实现,冷却必须以足够慢的速度进行。如果物质冷却得太快,可能会达到具有不规则三维图案的非晶态,如图 5.3 所示。石英、氯化钠、钻石和糖是具有规则排列的构成粒子、原子、离子或分子的晶体固体例子。玻璃、橡胶、沥青和许多塑料是非晶体非晶态固体的例子。正如你可能知道的,由于它们的对称分子结构,石英晶体比玻璃硬。

图 5.3 物理退火。左:具有晶体结构的金属。右:具有无序原子尺度结构的非晶态金属。
注意:退火过程涉及对温度和冷却速率的仔细控制,通常称为退火或冷却计划。退火时间应足够长,以便材料完成所需的转变。如果材料内外温度变化率差异太大,这可能会导致缺陷和裂纹。
对齐结构代表系统最低能量状态的事实启发了科学家们思考如何模仿这个过程来解决优化问题。模拟退火是一种模仿物理退火过程的计算模型。在数学优化的背景下,目标函数的最小值代表系统的最低能量。模拟退火是冷却过程的算法实现,用于寻找目标函数的最优值。表 5.1 概述了模拟退火与物理退火过程之间的类比。
表 5.1 物理退火和模拟退火类比
| 物理退火 | 模拟退火 |
|---|---|
| 材料状态 | 优化问题的解 |
| 状态的能量 | 解的成本 |
| 温度 | 控制参数(温度) |
| 高温使分子自由移动 | 高温有利于搜索空间探索 |
| 低温限制分子的运动 | 低温导致利用搜索空间 |
| 逐渐冷却有助于减少应力并增加均匀性和结构稳定性。 | 逐渐冷却有助于避免陷入次优局部最小值,并找到全局最优或近似最优解。 |
1953 年,第一个复制物理退火过程的计算模型被引入。该模型被提出作为一种计算物质性质的通用方法,这些物质可以被认为是相互作用的单个分子的集合。S. Kirkpatrick 等人是利用模拟退火进行优化的先驱,如他们在论文《模拟退火优化》[1]中所描述。以下小节解释了模拟退火算法中涉及到的步骤。
5.2.2 模拟退火伪代码
模拟退火采用基于马尔可夫链的随机搜索方法,不仅接受降低目标函数(假设为最小化问题)的新解,还可以接受增加目标函数值的概率解。
马尔可夫链
马尔可夫性质,以俄罗斯数学家安德烈·马尔可夫(1856-1922)的名字命名,是一个无记忆的随机过程。这意味着下一个状态只取决于当前状态,而不取决于之前的事件序列。马尔可夫链(MC)是一个随机或概率模型,描述了一系列可能的移动,其中每个移动的概率只取决于前一个移动达到的状态。这意味着从一个状态到另一个状态的转移只取决于当前完全可观察的状态和转移概率。
遵循这个无记忆的随机过程,当前已知状态 A 到下一个邻近状态 B 之间的转移由以下图示的转移概率控制。马尔可夫链在不同的领域中被使用,如随机优化、经济、语音识别、天气预报和控制系统。值得一提的是,Google 的 PageRank 算法使用马尔可夫链来模拟用户在网上的行为。SymPy 通过sympy.stats.DiscreteMarkovChain类提供了一个有限离散时间同质马尔可夫链的 Python 实现。

马尔可夫链—p[AB]、p[BA]、p[BC]和p[CB]是状态 A、B 和 C 之间的转移概率。
如图 5.4 所示,如果一个新的邻近解或状态 x[k] 是一个改进的解(即 f(x[k]) < f(x[i])),那么它总是被接受。改进的解是指在最小化问题中给出目标函数更低值的解,或者在最大化问题中给出更高值的解。对于非改进解,例如 x[j],解决方案仍然可以以概率接受,作为避免陷入局部最优的风险的一种方式。这与贪婪算法倾向于只接受改进解的趋势形成对比,使得贪婪算法更容易陷入局部最优。

图 5.4 转移概率,假设是一个最小化问题。由于解 x[k] 是一个改进的移动,它总是被接受,而由于解 x[j] 是非改进的,它可能基于转移概率以概率接受。
温度 T 出现在转移概率中,并控制搜索空间中的探索和利用。在高温下,非改进的移动有很好的机会被接受,但随着温度的降低,接受更差移动的概率会降低。我们将在以下小节中更详细地讨论这一点。
模拟退火算法的步骤可以总结如下伪代码。
算法 5.1 模拟退火算法
Objective function f(x), x = (x_1, . . . , x_p)^T
Initialize initial temperature T_o, initial guess x_o, iteration counter n=0 and iteration per temperature counter k=0
Set final temperature T_f, kmax maximum number of iterations per temperature and max number of iterations N
Define cooling schedule
Begin
While T > T_f and n < N do
While k<k_{max}
Move randomly to a new location/state x_n + 1
Calculate Δf = f_{n+1}(x_{n+1}) – f_n(x_n)
If the new solution if better then
Accept the new solution
Else
Generate a random number r
Accept if exp(−Δf/T)>r
k=k+1
End
Update T according to the cooling schedule
n = n + 1
End
Return the final solution
SA 算法具有易用性和为广泛连续和离散问题提供最优或近似最优解的能力。该算法的主要缺点是需要调整许多参数,以及算法偶尔会缓慢收敛到最优或近似最优解。
除了这个原始的 SA 算法——经典模拟退火(CSA)之外,还提出了各种变体来提高算法的性能。例如,快速模拟退火(FSA)是一种半局部搜索,由偶尔的长跳跃组成。双重退火是一种用于处理复杂非线性优化问题的随机全局优化算法。它基于经典模拟退火和快速模拟退火算法的组合。广义模拟退火(GSA)算法使用扭曲的柯西-洛伦兹访问分布[2]。
量子退火(QA)
在量子力学中,量子粒子被视为一种可以以一定概率穿透势垒的电磁波。由于量子层面上物质的波动性质,如果势垒足够薄,量子粒子确实有可能穿越这样的势垒。这种现象称为量子隧穿。量子隧穿效应是一种现象,其中波函数或粒子可以穿透一个看似不可逾越的势垒,即使粒子的总能量小于势垒高度。
如以下图所示,SA 使用热跃迁将搜索粒子推出局部山谷,以避免陷入局部最小值。另一方面,QA 通过应用量子效应在能量景观中搜索,以找到最优或近似最优解。量子退火不仅可以通过函数的景观漫步,还可以通过隧道。这允许算法通过量子隧道(隧道效应)而不是 SA 中使用的热跃迁来逃离局部最小值。

模拟退火与量子退火
在 QA 中,多个候选状态以相等的权重初始化。使用量子力学概率来平行地逐渐改变所有状态振幅。有关更多信息及量子退火器的示例,请参阅 D-Wave 实现:docs.dwavesys.com/docs/latest/c_gs_2.html。
下面的子节将解释 SA 算法的不同组件,从允许 SA 接受或拒绝非改进移动的转换概率开始。
5.2.3 接受概率
与爬山法(参见第 4.3.1 节)不同,SA 以概率允许向下步骤,由当前温度和移动的糟糕程度控制。在 SA 中,总是接受更好的移动。如图 5.4 所示,非改进的移动可以根据玻尔兹曼-吉布斯分布以概率接受。
在热力学中,温度为 t 的状态具有能量幅度增加 ΔE 的概率,该概率由玻尔兹曼-吉布斯分布给出,如方程 5.1 所示:
|

| 5.1 |
|---|
其中 k 是玻尔兹曼常数,它是将气体中粒子的平均相对动能与气体的热力学温度相关联的比例系数,其值为 1.380,649 × 10^(-23) m² kg s^(-2) K^(-1)。然而,在模拟物理退火过程的计算模型中,无需使用此常数,因此它被替换为 1。
此外,能量的变化可以用目标函数的变化来代替,作为量化搜索过程向最优或近似最优状态进展的方法。因此,ΔE 可以通过方程 5.2 与目标函数的变化联系起来:
|

| 5.2 |
|---|
其中 γ 是一个实数常数。为了简化,并且不改变核心意义,我们可以使用 k = 1 和 γ = 1。因此,转换概率 p 简单地变为
|

| 5.3 |
|---|
其中 T 是系统的温度。为了确定是否接受变化,我们通常使用区间 [0,1] 内的随机数 r 作为阈值。因此,如果 p > r,或者 p = e((–Δ)*f* /*T*^) > r,则移动被接受。否则,移动被拒绝。
如果 P[ij] 是从点 x[i] 移动到 x[j] 的概率,那么 P[ij] 是通过以下方式计算的
|

| 5.4 |
|---|
P[ij] 概率被称为转换或接受概率。以概率接受非改进的移动使得算法能够避免陷入某些局部最小值。如果接受概率设置为 0,模拟退火 (SA) 的行为类似于爬山法,因为它只会接受比当前解更好的解。相反,如果接受概率设置为 1,SA 变得更具探索性,因为它将始终接受更差的解,这使得它更类似于随机搜索。
接受较差状态的概率是系统温度和成本函数变化的一个函数。随着温度降低,接受较差移动的概率降低。温度可以看作是平衡搜索空间中探索和利用的一个参数。在高温下,接受概率高,这意味着算法接受大多数移动来探索参数空间。另一方面,当温度低时,接受概率低,意味着算法限制探索。如图 5.5 所示,如果T = 0,则不接受任何非改进移动。在这种情况下,SA 转化为爬山法。可以看出,冷却过程对搜索进度有重要影响。下一节将介绍 SA 中使用的不同冷却调度组件。

图 5.5 展示了接受概率随温度和目标函数变化的变化。目标函数的变化是指当前解与候选解之间目标函数值的差异。在最小化问题中,目标函数的正变化表示候选解比当前解更差。随着目标函数变化的增加,接受概率会降低。在高温下,SA 倾向于通过接受非改进移动来探索更多。随着温度降低,算法限制探索,偏向于利用。
由于基于玻尔兹曼的接受概率需要大量的计算时间(约占总 SA 计算的 1/3),可以使用查找表或非指数概率公式来代替。可以通过离线进行指数计算,仅针对f和T的变化范围生成查找表。其他非指数概率公式,如p(Δf) = 1 – Δf/T,也可以用作接受概率。此公式应归一化,以确保最大值为 1,最小值为 0。
在类似于模拟退火(SA)的计算模型中,没有必要严格模仿控制物理退火过程的动力学模型。图 5.6 展示了指数和非指数接受概率函数之间的差异。代码可在本书的 GitHub 仓库中找到。对于目标函数的小幅变化,指数和非指数接受概率函数之间的差异很小——你可以通过提供的代码进行实验。

图 5.6 指数与非指数接受概率
由于温度是接受概率的一部分,它在控制 SA 的行为中起着重要作用。以下小节将探讨我们如何控制温度,以在探索和利用之间达到权衡。
5.2.4 退火过程
模拟退火中的退火过程涉及对温度和冷却速率的仔细控制,通常称为退火计划。这个过程包括定义以下参数:
-
起始温度
-
按照冷却计划后的温度递减
-
每个温度下的迭代次数
-
最终温度
这在图 5.7 中显示。

图 5.7 退火过程参数
以下小节提供了关于这些参数的深入了解。
初始温度
选择正确的初始温度至关重要。如方程 5.4 所示,对于给定的变化 Δf
-
如果 T 过高 (T → ∞),则 p → 1,这意味着几乎所有的变化都将被接受,算法将表现得像随机搜索算法。
-
如果 T 太低 (T → 0),则任何 Δf > 0(在最小化问题中假设为更差的解)很少被接受,因为 p → 0,因此解的多样性有限,但任何改进(即,在最小化问题中任何 Δf < 0)几乎总是被接受。在这种情况下,模拟退火表现得像局部搜索,可能很容易陷入局部最小值。
为了找到一个合适的起始温度,我们可以使用关于目标函数的任何可用信息。如果我们知道目标函数的最大变化 max(Δf),我们可以使用这个来估计给定接受概率 p[o] 的初始温度 T[o],使用方程 5.5:
|

| 5.5 |
|---|
如果目标函数的最大潜在变化未知,我们可以使用以下启发式方法:
-
以非常高的温度开始评估,以便接受几乎所有变化。
-
快速降低温度,直到大约 50%到 60%的较差移动被接受。
-
使用这个温度作为新的初始温度 T[o],进行适当且相对缓慢的冷却处理。
温度递减
冷却计划是随着算法的进行系统性地降低温度的速率。这是模拟退火的可调参数之一。以下是一些常用的冷却计划:
- 线性冷却计划—使用方程 5.6 线性递减温度:
|

| 5.6 |
|---|
其中 T[o] 是初始温度,i 是迭代的伪时间,β 是冷却速率,应该选择的方式是当 i → i[f](或最大迭代次数 N)时 T → 0。这通常给出
|

| 5.7 |
|---|
这种冷却计划简单易行,但可能不是所有类型问题的最佳选择。此外,它需要关于最大迭代次数的先验知识或假设。
- 线性逆冷却计划—在线性逆冷却中,温度在高温时迅速下降,在低温时逐渐下降,如方程 5.8 所示。在这个方程中,α是冷却因子,应该在 0 和 1 之间:
|

| 5.8 |
|---|
- 几何冷却计划—几何冷却计划本质上是通过冷却因子 0 < α < 1 根据方程 5.9 降低温度:
|

| 5.9 |
|---|
冷却过程应该足够慢,以便系统可以轻松稳定。在实践中,α = 0.7 ~ 0.95 是常用的。α的值越高,达到最终(低)温度所需的时间就越长。几何方法的主要优点是当i趋向于无穷大时,T趋向于 0,因此不需要指定最大迭代次数。此外,几何退火计划提供了更平缓的冷却,如图 5.8 所示。
- 对数冷却计划—在这个冷却计划中,温度根据方程 5.10 以对数方式降低:
|

| 5.10 |
|---|
其中 α > 1。从理论上讲,这个冷却过程趋向于全局最小值的渐近收敛。然而,它需要巨大的计算时间。
- 指数冷却计划—在这个冷却计划中,温度根据方程 5.11 以指数方式降低:
|

| 5.11 |
|---|
其中 α是冷却因子,n是模型空间的维度。在这个冷却过程中,温度在最初的迭代中迅速下降,但指数衰减的速度后来会减慢,并且可以通过冷却因子来控制。

图 5.8 不同的 SA 冷却计划
如您所见,这些冷却计划都是单调递减的函数,并没有明确考虑搜索的进展情况。在第 5.2.5 节中,我们将探讨一种非单调自适应冷却计划。
每个温度下的迭代次数
在应用冷却计划(即,降低温度)之前,允许在每个温度级别进行足够数量的迭代以在该温度下稳定系统是很重要的。通常,这是通过使用一个恒定值来实现的。例如,每个温度下的迭代次数可能与问题规模呈指数关系(例如,TSP 问题中城市的数量作为离散问题或连续问题中数学函数的维度)。然而,这个值可以动态地改变。
实现这一目标的一种方法是在搜索的探索阶段限制初始高温时的迭代次数。例如,当温度高时,我们可以在每个温度下进行少量迭代,然后实施冷却过程。随着搜索的继续和温度的降低,我们可以在较低温度下进行更多迭代,从而转向利用。
最终温度
通常会让温度降低直到为零。然而,这可能会使算法运行时间更长,尤其是在使用某些冷却计划,如几何冷却时。实际上,如果当前温度下接受非改进移动的概率几乎与温度为零时相同,则没有必要让温度达到零。因此,停止标准可以是以下两种之一:
-
一个合适的低温(T[f] = 10^(–10) ~ 10^(–5))
-
当系统达到“冻结”或最小能量状态(假设是优化问题),既不接受更好的移动也不接受更差的移动时。
5.2.5 SA 中的自适应
SA 中的一些参数可以用来使算法更适应搜索的进程。这些参数中最关键的是初始温度、冷却计划和每温度下的迭代次数。其他组件包括成本函数、生成邻域解的方法和接受概率。
如图 5.9 所示,初始温度可以用来控制 SA 的探索和利用行为。高温导致高水平的探索,而低温则导致利用行为(即限制在邻居周围的搜索)。

图 5.9 SA 中温度的影响。高温导致更多的探索,而低温则限制探索并导致搜索空间中更多的利用。
你可以从分子的运动来思考这个问题。假设分子是搜索代理。在高温下,分子在搜索空间中自由移动,探索不同的解决方案。在低温下,分子的运动变得有限,因此探索受到限制,搜索代理专注于搜索空间的一个特定部分。在搜索开始时的高温下,由于探索行为使算法以高概率接受非改进移动,SA 会因振荡而波动。随着搜索的进行和温度的降低,算法开始由于利用行为而稳定下来,这种利用行为使算法接受较少的非改进移动,并专注于精英改进解决方案。
始终建议您从高温开始,随着搜索的进行逐渐降低温度。然而,正确的初始温度取决于问题。您可以尝试不同的值,看看哪个能导致更好的解决方案。一些研究人员建议自适应地这样做,使用其他搜索方法或元启发式算法,如遗传算法。
冷却计划也可以用来使算法更具适应性。可以在搜索的不同阶段使用不同的冷却计划,考虑到最有用的工作通常在计划的中部完成。如果观察到没有进展,也可以尝试重新加热。冷却可以在每次接受移动(或特定数量的移动)时发生。可以尝试非单调的自适应冷却计划,其中使用一个自适应因子,基于当前解决方案的目标与算法到那时为止实现的最佳目标之间的差异,根据以下公式:
|

| 5.12 |
|---|
其中 T 是每个状态转换时的系统温度,T(i) 是当前温度,f[i] 是迭代 i 时目标函数的值,f^* 是迄今为止获得的目标函数的最佳值。
另一个适应参数是每个温度下的迭代次数。这个数字可以通过在高温度下允许少量迭代和在低温度下允许大量迭代来自适应地改变,以充分探索局部最优解。
SA 算法的适应能力也可能受到目标函数和问题约束表示的影响。一般来说,建议避免产生多个状态相同结果的成本函数(例如,在 TSP 路中包含的边的数量)。这种类型的函数不会引导搜索,因为它可能不会在目标函数从一个状态到另一个状态时发生变化。
例如,想象两条具有相同边数的可行路线(即,Δf = 0)。依赖边的数量作为成本函数并不是一个好主意。然而,许多问题都有可以使用奖励或惩罚项表示的约束。使算法更具适应性的方法之一是动态改变奖励和惩罚项的权重。在搜索的初始阶段,约束可以比搜索的高级阶段更宽松。
已有众多努力使 SA 参数的选择和控制完全自适应。这种努力的例子之一是由 Ingber 在“自适应模拟退火(ASA):经验教训” [3] 中提出的。ASA 自动调整控制温度计划的算法参数,只需用户指定冷却速率。该方法使用先前接受的步骤和参数的线性随机组合来估计新的步骤和参数。
Geng 等人提出了一种带有贪婪搜索的 ASA 算法(ASA-GS)来解决 TSP 问题[4]。ASA-GS 基于经典的 SA 算法,并利用贪婪搜索技术来加速收敛速度。ASA 利用动态调整参数,如温度冷却系数、贪婪搜索迭代次数、强制接受实例以及接受新解决方案的概率。这些自适应参数控制旨在增强质量和时间效率之间的权衡。
SA 在各个领域都有广泛的应用。它的效用扩展到解决各种优化问题,包括非线性函数优化、TSP、学术课程安排、网络设计、任务分配、电路划分和放置、机器人运动规划、车辆路径以及资源分配和调度。以下各节将展示如何在不同领域使用 SA 来解决连续和离散优化问题。
5.3 函数优化
作为连续优化问题的例子,让我们考虑以下简单的函数优化问题:在约束条件 0 ≤ x ≤ 31 下,找到 x 以最小化 f(x) = (x – 6)²。
我们可以从 x 的范围内开始一个初始随机解决方案。可以通过添加一个从高斯或正态分布中选择的随机浮点值来生成不同的邻近解决方案,该分布具有给定的均值和标准差。以下是如何在 Python 中使用 random.gauss() 函数的示例:
import random
mu, sigma = 0, 1 # mean and standard deviation
print(random.gauss(mu, sigma))
假设初始温度 T[0] = 5,每个温度的迭代次数为 2,几何冷却因子 α = 0.85。让我们进行几次手动迭代,以展示 SA 如何解决这个问题:
-
初始化—随机生成一个初始解决方案,并按以下方式评估其成本:x = 2 和 f(2) = 16。
-
迭代 1—通过以下代码使用高斯分布添加一个随机值来生成一个新的解决方案 x = 2.25:
import numpy as np
x=x+np.random.normal(mu, sigma, 1)
f(2.25) = 14.06 是一个改进的解决方案,因此被接受。
-
迭代 2—通过向上一迭代中最后接受的解决方案添加一个随机值来生成一个新的解决方案。新的解决方案 x = 2.25 – 1.07 = 1.18,f(1.18) = 23.23 是一个非改进的解决方案,因此必须计算接受概率:p = e^(–Δf /T) = e^(–(23.23 – 14.06)/5) = 0.1597。我们生成一个介于 (0,1) 之间的随机数 r,假设它是 r = 0.37。由于 p ≯ r,我们拒绝这个解决方案。
-
迭代 3—我们更新了温度,因为我们已经使用了初始温度 T[0] = 5 进行了两次迭代。按照几何冷却,新的温度是 T[1] = T[o] α^i = 5*0.85¹ = 4.25。我们将从这次迭代开始使用这个值进行两次迭代。现在我们将基于最后一个接受的解生成一个新的解,通过添加高斯分布的随机值。新的解是 x = 2.25 + 1.57 = 3.82,且 f(3.82) = 4.75。这是一个改进的解,因此它被接受,搜索继续。
SciPy 为 SA 算法和其他处理数学优化问题的算法提供了 Python 实现。scipy.optimize.anneal 在 SciPy 中已弃用,取而代之的是 dual_annealing() 函数。以下列表展示了使用 SciPy 双重退火算法求解 Bohachevsky 函数(该函数的公式为 f(x[1],x[2]) = x[1]² + 2x[2]² – 0.3cos(3πx[1]) – 0.4cos(3πx[2]) + 0.7)的解。完整的列表可在本书的 GitHub 仓库中找到。
列表 5.1 使用 scipy.optimize.dual_annealing 进行函数优化
#!pip install scipy
import numpy as np
from scipy.optimize import dual_annealing
def objective_function(solution): ①
return solution[0]**2 +2*(solution[1]**2) - 0.3*np.cos(3*np.pi*solution[0]) –
➥ 0.4*np.cos(4*np.pi*solution[1]) + 0.7
bounds = np.asarray([[-100, 100], [-100, 100]]) ②
res_dual = dual_annealing(objective_function, bounds=bounds, maxiter = 100) ③
print('Dual Annealing Solution: f(%s) = %.5f' % (res_dual['x'], res_dual['fun'])) ④
① 定义目标函数或函数(例如,Bohachevsky 函数)。
② 定义决策变量的边界约束。
③ 执行双重退火搜索。
④ 打印双重退火解。
MEALPY 是另一个提供不同自然启发式元启发式算法实现的 Python 库(更多详细信息请见附录 A)。作为延续,以下代码展示了使用 MEALPY SA(列表 5.1 的完整版本可在本书的 GitHub 仓库中找到)求解 Bohachevsky 函数的解:
#!pip install mealpy
from numpy import exp, arange
import matplotlib.pyplot as plt
from pylab import meshgrid,cm,imshow,contour,clabel,colorbar,axis,title,show
from mealpy.physics_based.SA import OriginalSA
problem = {"fit_func": objective_function,"lb": [bounds[0][0], bounds[1][0]], "ub":
➥ [bounds[0][1], bounds[1][1]], "minmax": "min", "obj_weights": [1, 1]} ①
epoch = 100 ②
pop_size = 10 ②
max_sub_iter = 2 ②
t0 = 1000 ②
t1 = 1 ②
move_count = 5 ②
mutation_rate = 0.1 ②
mutation_step_size = 0.1 ②
mutation_step_size_damp = 0.99 ②
model = OriginalSA(epoch, pop_size, max_sub_iter, t0, t1, move_count, mutation_rate,
➥ mutation_step_size, mutation_step_size_damp) ③
mealpy_solution, mealpy_value = model.solve(problem) ④
print('MEALPY SA Solution: f(%s) = %.5f' % (mealpy_solution, mealpy_value)) ⑤
① 定义问题
② 定义 MEALPY 算法参数以使用 MEALPY 进行 SA 搜索。
③ 定义 MEALPY SA 求解器。
④ 使用定义的求解器解决问题。
⑤ 打印 MEALPY SA 解。
图 5.10 展示了 Bohachevsky 函数的解。算法的性能主要取决于其参数调整和停止标准。MEALPY 运行 SA 的并行版本,并公开了许多可调整的参数。

图 5.10 使用 SA 解决连续函数优化问题的解。中心处的十字是最佳解。三角形是 MEALPY SA 获得的解。点是 SciPy 双重退火解。
注意附录 A 展示了如何在其他 Python 包中使用 SA 解决数学优化问题。
让我们从头实现 SA 算法,这样我们可以获得更多的控制,并更好地处理不同类型的连续和离散优化问题。在我们的 optalgotools 包实现中,我们将问题定义与求解器解耦,这样我们就可以使用求解器来处理不同的问题。
让我们将我们的实现应用于寻找上述简单函数优化问题的全局最小值以及更复杂的函数优化问题。在多维空间中存在几个复杂的数学函数,例如 Rosenbrock 函数、Ackley 函数、Rastrigin 函数、Schaffer 函数、Schwefel 函数、Langermann 函数、Levy 函数、Bukin 函数、Eggholder 函数、交叉托盘函数、滴波函数和 Griewank 函数。函数优化测试问题和数据集的示例可以在附录 B 中找到。
列表 5.2 展示了如何使用 SA 解决以下数学函数:
-
一个简单的二次方程—这是我们手动迭代中使用的。
-
Bohachevsky 函数(全局最小值 0)—这是一个具有碗形形状的 2D 单峰函数。此函数已知是连续的、凸的、可分离的、可微的、非多模态的、非随机的和非参数的,因此基于导数的求解器可以有效地处理它。请注意,变量可以分离的函数被称为可分离函数。非随机函数不包含随机变量。非参数函数假设数据分布不能通过有限参数集来定义。
-
Bukin 函数—此函数具有许多局部极小值,所有这些极小值都位于脊上,并且在 x[0] = f(−10,1) 处有一个全局最小值 f(x[0]) = 0。此函数是连续的、凸的、不可分离的、不可微的、多模态的、非随机的和非参数的。这需要一个无导数求解器(也称为黑盒求解器)如 SA。
-
Gramacy & Lee 函数—这是一个具有多个局部极小值和局部及全局趋势的 1D 函数。此函数是连续的、非凸的、可分离的、可微的、非多模态的、非随机的和非参数的。
-
Griewank 1D、2D 和 3D 函数—这些函数具有许多广泛分布的局部极小值。这些函数是连续的、非凸的、可分离的、可微的、多模态的、非随机的和非参数的。
在我们的实现中,这些是 SA 参数:
-
最大迭代次数:
max_iter=1000 -
每个温度的最大迭代次数:
max_iter_per_temp=100 -
一个初始温度:
initial_temp=1000 -
一个最终温度:
final_temp=0.0001 -
一个冷却计划:
cooling_schedule='geometric'(可用选项:'linear'、'geometric'、'logarithmic'、'exponential'、'linear_inverse') -
一个冷却因子:
cooling_alpha=0.9 -
一个调试选项:
debug=1(debug=1打印初始和最终解;debug=2提供手动迭代跟踪)
随意更改这些设置并观察它们对算法性能的影响。
列表 5.2 使用 SA 进行连续函数优化
import random
import math
import numpy as np
from optalgotools.algorithms import SimulatedAnnealing
from optalgotools.problems import ProblemBase, ContinuousFunctionBase
def simple_example(x): ①
return (x-6)**2 ①
simple_example_bounds = np.asarray([[0, 31]]) ①
simple_example_obj = ContinuousFunctionBase(simple_example, simple_example_ ①
➥ simple_example_bounds) ①
sa = SimulatedAnnealing(max_iter=1000, max_iter_per_temp=100, ①
➥ initial_ temp=1000, final_temp=0.0001, cooling_schedule='geometric', ①
➥ cooling_alpha=0.9, debug=1) ①
sa.run(simple_example_obj) ①
def Bohachevsky(x_1, x_2): ②
return x_1**2 +2*(x_2**2)-0.3*np.cos(3*np.pi*x_1)-0.4*np.cos(4*np. ②
pi*x_2)+0.7 ②
Bohachevsky_bounds = np.asarray([[-100, 100], [-100, 100]]) ②
Bohachevsky_obj = ContinuousFunctionBase(Bohachevsky, Bohachevsky_bounds, 5) ②
sa.run(Bohachevsky_obj) ②
def bukin(x_1, x_2): ③
return 100*math.sqrt(abs(x_2-0.01*x_1**2)) + 0.01 * abs(x_1 + 10) ③
bukin_bounds = np.asarray([[-15, -5], [-3, 3]]) ③
bukin_obj = ContinuousFunctionBase(bukin, bukin_bounds, 5) ③
sa.run(bukin_obj) ③
def gramacy_and_lee(x): ④
return math.sin(10*pi*x)/(2*x) + (x-1)**4 ④
gramacy_and_lee_bounds = np.asarray([[0.5, 2.5]]) ④
gramacy_and_lee_obj = ContinuousFunctionBase(gramacy_and_lee, gramacy_and_ ④
➥ lee_bounds, .1) ④
sa.run(gramacy_and_lee_obj) ④
def griewank(*x): ⑤
x = np.asarray(x) ⑤
return np.sum(x**2/4000) - np.prod(np.cos(x/np.sqrt(np.asarray(range(1, ⑤
➥ len(x)+1))))) + 1 ⑤
griewank_bounds = np.asarray([[-600, 600]]) ⑤
griewank_1d=ContinuousFunctionBase(griewank, griewank_bounds, 10) ⑤
sa.run(griewank_1d) ⑤
griewank_bounds_2d = np.asarray([[-600, 600]]*2) ⑥
griewank_2d=ContinuousFunctionBase(griewank, griewank_bounds_2d, ⑥
➥ (griewank_bounds_2d[:, 1] - griewank_bounds_2d[:, 0])/10) ⑥
sa.run(griewank_2d) ⑥
griewank_bounds_3d = np.asarray([[-600, 600]]*3) ⑦
griewank_3d=ContinuousFunctionBase(griewank, griewank_bounds_3d, ⑦
➥ (griewank_bounds_3d[:, 1] - griewank_bounds_3d[:, 0])/10) ⑦
sa.run(griewank_3d) ⑦
① 二次函数 SA 的解决方案
② 基于 Bohachevsky SA 的解决方案
③ 基于 Bukin SA 的解决方案
④ 基于 Gramacy & Lee SA 的解决方案
⑤ 基于 Griewank 1D SA 的解决方案
⑥ 基于 Griewank 2D SA 的解决方案
⑦ Griewank 3D SA-based solution
这是 Bukin 函数输出的一个示例:
Simulated annealing is initialized:
current value = 60.73784664253138, current temp=1000
Simulated Annealing is done:
curr iter: 154, curr best value: 0.6093437608551259, curr temp:9.97938882337113e-05, curr best: sol: [-14.63282848 2.14122839]
global minimum: x = -14.6328, 2.1412, f(x) = 0.6093
如所示,模拟退火(SA)能够处理不同的多维、非线性函数优化问题。这个随机全局优化算法能够适应目标函数的景观并避免陷入局部最小值。然而,在多维函数如 Griewank 2D 和 3D 的情况下,SA 需要时间才能收敛。以下章节将展示 SA 如何处理如数独和 TSP 之类的离散问题。
5.4 解数独
数独,也称为苏杜克,是有史以来最受欢迎的数字谜题之一。这个游戏是从一个称为拉丁方的数学概念改编而来的。数独谜题的第一个版本是由一位名叫霍华德·加恩斯的退休建筑师创造的,于 20 世纪 70 年代末作为《Dell 铅笔谜题和文字游戏》中的谜题出现。该游戏随后于 1984 年在日本以“数独”的名字出现,这个名字是从日语“数は独り立ちます”缩写而来,意味着数字(或数字)必须保持单一。如今,数独游戏在全球范围内都很受欢迎,并发表在游戏网站、谜题手册和报纸上。
数独游戏可以被视为一个约束满足问题(CSP),通过正确填充一个 9 × 9 的网格,使得每一列、每一行以及九个 3 × 3 的子网格(又称“盒子”、“区块”或“区域”)都包含从 1 到 9 的所有数字。任何一行、一列或 3 × 3 的子网格都不应包含超过一个相同的数字(从 1 到 9)。
除了娱乐之外,数独在现实生活中的应用还包括发展心理学和隐写术。例如,有几项研究表明,解决数独或填字游戏或其他脑力游戏可能有助于保持你的大脑年轻 10 年,并可以减缓如阿尔茨海默病等疾病的发展。数独还可以用作提高解决问题的能力、批判性思维和注意力的工具。最后,隐写术是将图像、消息、文件或其他秘密数据隐藏在非秘密事物中的技术。在秘密数据传输应用中,数字图像可以用来隐藏秘密数据。然后,使用数独谜题根据特别设计的参考矩阵修改封面图像中的选定像素对,以插入秘密数字。
拉丁方
拉丁方是由 10 世纪的阿拉伯数字学家发明的,他们处理数字的神秘力量。13 世纪的伊斯兰护身符,称为 wafq majazi,被发现,它们被绘制在 16 世纪阿拉伯医学文本的边缘。名字“拉丁”是受到著名的瑞士数学家莱昂哈德·欧拉(1707–1783)的启发,他在方阵中使用拉丁字母作为符号。
拉丁方是一个n × n的数组,填充了n个不同的数字、符号或颜色,以这样的方式排列,即任何正交(行或列)都不会包含相同的数字、符号或颜色两次。这里展示了一个 4 × 4 拉丁方的例子:

拉丁方与幻方不同。幻方是一个正整数 1, 2, ..., n²的平方数组,这些数按照这样的方式排列,即任何水平、垂直或主对角线上的n个数的和总是相同的数。数独基于拉丁方。事实上,任何数独谜题的解决方案都是一个拉丁方。KenKen 和 KenDoku 是基于拉丁方增强版本的其它数字谜题,需要一定程度的算术技能。
通常来说,数独的搜索空间非常庞大。存在 6.671 × 10²¹种可能的可解数独网格,它们会产生一个唯一的结果[5]。根据大英百科全书,如果地球上每个人每秒解决一个数独谜题,他们要到大约 30,992 年才能全部解决。然而,去除对称性,例如旋转、反射、列和行的排列以及数字的交换,本质上不同的数独网格数量减少到 5,472,730,538 ≈5.473 × 10⁹[6]。广义的 n × n 数独问题是一个 NP 完全问题。然而,一些实例,例如标准的 9 × 9 数独,并不是 NP 完全的。存在常数时间算法可以解决一些 9 × 9 数独的实例,在O(1)时间内解决,因为每个 9 × 9 数独都可以在一个有限的字典或查找表中列出、枚举和索引,以找到解决方案。然而,这些算法无法处理任意的广义 n × n 数独问题。
回溯法、舞链法和克罗克的纸笔法是解决数独的常见算法,尤其是当问题规模较小时。回溯法主要是一种经典的深度优先搜索,它会测试整个分支,直到该分支违反规则或返回一个解决方案。
舞链法(DLX),由唐纳德·克努斯于 2000 年发明,使用算法 X 来解决数独谜题,将其作为精确覆盖问题处理。在精确覆盖问题中,给定一个二元矩阵(即仅由 0 和 1 组成的矩阵),需要找到一组行,每列恰好包含一个 1。算法 X,一种递归搜索算法,通过回溯法应用于解决精确覆盖问题。
在克罗克的铅笔和纸算法中,每个单元格中所有可能的数字都被列出。这个数字列表被称为单元格的标记。然后我们尝试找出是否有行、列或块中只有一个可能的值贯穿整个行、列或块。一旦找到,我们就用这个数字填写这个单元格,并更新任何受影响的行、列或框的标记。下一步是找到先发制人的集合。正如克罗克的论文中描述的那样,先发制人的集合由集合[1,2,…,9]中的数字组成,是一个大小为m的集合,2 ≤ m ≤ 9,其数字是m个单元格的潜在占用者,其中“独家”意味着集合[1,2,…,9]中的其他数字(除了先发制人集合的成员)不可能是那些m个单元格的潜在占用者。最后一步是消除先发制人集合外的可能数字。
回溯
回溯算法通常通过递归用于解决搜索和优化问题。回溯算法增量地构建一个可行解或一组可行解。给定一个 9×9 数独板,算法按照深度优先遍历顺序访问所有空单元格,增量地填写数字,并在找不到有效数字时回溯。以下图展示了 9×9 数独谜题的回溯算法步骤。

9×9 数独的回溯步骤
下一个列表展示了如何使用 SA 算法解决 9×9 数独。
列表 5.3 使用 SA 解决数独
from optalgotools.algorithms import SimulatedAnnealing ①
from optalgotools.problems import Sudoku ②
sa = SimulatedAnnealing(max_iter=100000, max_iter_per_temp=1000,
➥ initial_temp=500, final_temp=0.001, cooling_schedule='geometric',
➥ cooling_alpha=0.9, debug=1) ③
Sudoku_hard = [
[9, 0, 0, 1, 0, 0, 0, 5, 4],
[0, 0, 0, 0, 8, 0, 0, 0, 0],
[0, 0, 5, 0, 0, 9, 0, 0, 3],
[0, 9, 0, 0, 3, 5, 0, 4, 1],
[0, 0, 0, 0, 1, 0, 0, 0, 0],
[4, 1, 0, 2, 6, 0, 0, 8, 0],
[7, 0, 0, 3, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 4, 0, 0, 0, 0],
[3, 5, 0, 0, 0, 1, 0, 0, 6],
]
sudoku_prob = Sudoku(Sudoku_hard)
sudoku_prob.print() ④
④
sudoku_prob.solve_backtrack() ⑤
sa.run(sudoku_prob, 0) ⑥
① 导入 SA 求解器。
② 导入数独问题。
③ 使用所选参数创建 SA 求解器。
④ 创建一个硬 9×9 数独(可用变体包括简单、容易、中等、困难、邪恶)。
⑤ 使用回溯算法解决数独。
⑥ 使用 SA 解决数独。
您可以通过更改谜题配置来尝试不同的数独变体。在简单的数独问题中,单元格包含比中等或困难问题更多的预填数字。邪恶数独是谜题难度的最高级别。表 5.2 比较了 SA、回溯和 Python 线性规划(PuLP)库解决不同 9×9 数独实例所需的时间。PuLP 提供线性混合编程求解器。PuLP 中使用的默认求解器是 Cbc(COIN-OR 分支和切割),它是一个用于混合整数线性规划问题的开源求解器。有关 PuLP 的更多信息,请参阅附录 A。
表 5.2 SA 与回溯与 PuLP 在解决 9×9 数独谜题中的比较
| 找到解的时间 | 简单 | 容易 | 中等 | 困难 | 邪恶 |
|---|---|---|---|---|---|
| 回溯 | 0.01 | 0.01 | 0.11 | 0.69 | 1.58 |
| PuLP | 0.69 | 0.12 | 0.11 | 0.13 | 0.12 |
| 经典 SA | 0.10 | 0.07 | 0.01 | 3:17 | 在 3:16 时次优 |
如您所见,经典 SA 在解决数独问题的困难实例和邪恶实例时,并不优于回溯方法,并且速度要慢得多。与回溯和 SA 相比,PuLP 能够高效地以一致的时间处理不同类型的数独。
在邪恶数独的情况下,尽管尝试了不同的参数设置,SA 仍然收敛到一个次优解。鉴于这是一个约束满足问题,次优解的概念是不成立的,因为次优解是一个无效解。这意味着 SA 无法解决数独的邪恶实例。作为一个结构良好的问题,不同难度的 9×9 数独可以很容易地使用回溯算法解决。一般来说,如果问题结构良好,有已知的算法解决方案,元启发式方法通常不会优于这些经典和更确定性的方法。
5.5 解决 TSP
如 2.1.1 节所述,旅行商问题(TSP)被用作研究可以应用于广泛离散优化问题的一般方法的平台。考虑使用 SA 解决图 5.11 所示的 TSP 实例。在这个 TSP 中,旅行商必须访问五个城市并返回家中,形成一个循环(往返)。

图 5.11 五城市 TSP—假设对称 TSP,有 5!/2=60 种可能的旅行路线。图中的边权重代表城市之间的旅行距离。
假设以下值:初始温度=500,最终温度=50,线性递减率为 50,每个温度下进行一次迭代。TSP 解的形式如下:解=[1, 3, 4, 2, 5]。目标函数是路线的总距离。交换是一个合适的算子,可以用来生成邻近解:
- 迭代 0—初始解是解=[1, 3, 4, 2, 5],成本=2+4+5+5+12=28,如图 5.12 所示。

图 5.12 5 个城市 TSP 的 SA 迭代 0
- 迭代 1—为了生成一个候选解,选择两个随机城市(例如,2 和 3),并交换它们。这导致一个新的解[1, 2, 4, 3, 5],成本为 35(见图 5.13)。

图 5.13 5 个城市 TSP 的 SA 迭代 1
由于新解的旅行路线更长,它将根据概率p = e–Δ^f /*T* = e^(–(35–28) /)^T = e^(–7 /)^T(在较高温度下,接受概率更高)有条件地被接受。我们在 0 和 1 之间随机选择一个值r。如果P > r,我们接受这个解。否则,我们拒绝这个解。假设新解没有被接受,我们从初始解开始生成另一个解:
- 迭代 2—通过交换初始解中的城市 2 和 5 生成一个解。候选解的旅行长度为 18(图 5.14)。

图 5.14 5 城市 TSP 的 SA 迭代 2
由于这个解的旅行长度更短,它将被接受,并且搜索将继续,直到满足终止条件。下面的列表显示了此简单 TSP 问题的 SA 解。
列表 5.4 使用 SA 解决 TSP
from optalgotools.algorithms import SimulatedAnnealing
from optalgotools.problems import TSP
dists = [ [0] * 5 for _ in range(5)]
dists[0][1] = dists[1][0] = 4
dists[0][2] = dists[2][0] = 2
dists[0][3] = dists[3][0] = 9
dists[0][4] = dists[4][0] = 12
dists[1][2] = dists[2][1] = 7
dists[1][3] = dists[3][1] = 5
dists[1][4] = dists[4][1] = 5
dists[2][3] = dists[3][2] = 4
dists[2][4] = dists[4][2] = 10
dists[3][4] = dists[4][3] = 3
tsp_sample = TSP(dists, 'random_swap') ①
sa = SimulatedAnnealing(max_iter=10000, max_iter_per_temp=1, initial_temp=500,
➥ final_temp=50, cooling_schedule='linear_inverse', cooling_alpha=0.9, debug=2) ②
sa.run(tsp_sample) ③
① 创建一个 TSP 实例的实例。
② 创建一个 SA 求解器的实例。
③ 运行 SA 求解器,并在每次迭代中显示结果。
让我们现在考虑一些 TSP 的基准实例,例如来自 TSPLIB 的柏林 52 (comopt.ifi.uni-heidelberg.de/software/TSPLIB95/))。这个数据集包含柏林市的 52 个位置。柏林 52 数据集获得的最短路线为 7,542。下面的列表显示了我们可以如何使用我们的 SA 实现解决这个 TSP 实例。
列表 5.5 使用 SA 解决柏林 52 TSP
from optalgotools.problems import TSP
from optalgotools.algorithms import SimulatedAnnealing
import matplotlib.pyplot as plt
berlin52_tsp_url = 'https://raw.githubusercontent.com/coin-or/
jorlib/b3a41ce773e9b3b5b73c149d4c06097ea1511680/jorlib-core/src/test/resources/
tspLib/tsp/berlin52.tsp' ①
berlin52_tsp = TSP(load_tsp_url=berlin52_tsp_url, gen_method='mutate',
➥ rand_len=True, init_method='random') ②
sa = SimulatedAnnealing(max_iter=1200, max_iter_per_temp=500, initial_temp=150,
➥ final_temp=0.01, cooling_schedule='linear', debug=1) ③
sa.run(berlin52_tsp, repetition=1) ④
print(sa.val_allbest) ④
berlin52_tsp.plot(sa.s_best) ⑤
① 柏林 52 数据集的永久 URL
② 为柏林 52 创建一个 TSP 对象。
③ 创建一个 SA 模型。
④ 运行 SA,并评估最佳解的距离。
⑤ 绘制路线。
下面是一个输出示例:
sol: [0, 48, 34, 35, 33, 43, 45, 36, 37, 47, 23, 4, 14, 5, 3, 24, 11, 50, 10,
51, 13, 12, 26, 27, 25, 46, 28, 15, 49, 19, 22, 29, 1, 6, 41, 20, 30, 17, 16,
2, 44, 18, 40, 7, 8, 9, 32, 42, 39, 38, 31, 21, 0]
8106.88
图 5.15 显示了 SA 为 Belin52 TSP 生成的路线。

图 5.15 使用 SA 解决 Belin52
如您所见,SA 找到的近似最优解为 8,106.88。这个值略高于柏林 52 TSP 的最佳已知解,即 7,542。参数调整和算法适应可以帮助提高结果。例如,Geng 等人在“基于自适应模拟退火算法和贪婪搜索解决旅行商问题”的论文中讨论了在搜索过程中使用三种不同的突变(顶点插入突变、块插入突变和块反转突变)以及不同的概率来提高 SA 在解决 TSP 问题时的准确性。此外,可以根据 TSP 实例的大小调整诸如温度的冷却系数、用于加速收敛率的贪婪搜索次数、强制接受次数以及接受新解的概率等参数。此自适应算法的实现可以从以下 GitHub 仓库获取:github.com/ildoonet/simulated-annealing-for-tsp。
可以研究算法参数的影响,例如初始温度、冷却计划、每个温度的迭代次数和最终温度。例如,对柏林 52 TSP 实例应用了以下设置的 SA:
-
最大迭代次数 = 1200
-
每个温度 T 的迭代次数 = 500
-
T[初始] = 150
-
T[最终] = 0.01
-
线性冷却
我们的实现支持以下方法从旧解中突变出新解:
-
random_swap—在路径中交换两个城市。这可以通过使用num_swaps多次对同一解决方案进行操作。此外,交换可以在整个路径的较小窗口中进行,使用swap_wind = [1 - n]。例如,假设路线是[A, B, C, D, F]。交换两个随机城市,如 B 和 F,将导致新的路线[A, F, C, D, B]。 -
reverse—使用rand_len或rev_len(默认为 2)以随机长度或默认长度反转城市子集的顺序。例如,从解决方案[A, B, C, D, F]开始,如果我们应用长度为 3 的reverse,我们可以得到新的解决方案[A, D, C, B, F]。 -
insert—随机选择一个城市,将其从路径中移除,并在另一个随机城市之前重新插入。例如,从解决方案[A, B, C, D, F]开始,我们可以选择城市 B 并将其插入到城市 F 之前,从而得到新的解决方案[A, C, D, B, F]。 -
mutate—随机选择当前解决方案中的一系列连续城市,并对其进行洗牌。例如,从解决方案[A, B, C, D, F]开始,我们可能会选择 C, D, F 并对其进行洗牌,从而得到新的解决方案[A, B, F, C, D]。
实现还支持两种初始化路径的方法:
-
random—这意味着路径是完全随机生成的。 -
greedy—这试图通过选择城市之间的成对最短距离来选择可能次优的初始路径。这不会导致最短路径,但它可能比随机初始化更好。
值得注意的是,SA 算法的结果可能并不完全可重复。由于算法中包含的随机性,每次运行算法时,您可能会得到略微不同的结果。为了避免这种情况,SimulatedAnnealing类中包含的run函数包含一个repetition参数,允许您报告多次运行中生成的最佳解决方案,如下所示:
run(self, problem_obj=None, stoping_val=None, init=None, repetition=1)
您可以将重复次数设置为 10,这样算法就会报告从 10 次运行中生成的最佳解决方案。
5.6 解决半挂车配送路线问题
让我们考虑一个更贴近现实生活的 TSP 例子。假设沃尔玛超市是配送半挂车要访问的兴趣点(POI)。车辆将从位于安大略省 Ajax 的 270 Kingston Rd. E 的沃尔玛超市 3001 号开始。需要找到卡车可以遵循的最短路线,以访问每个 POI 一次并返回到起始位置。在所选的大多伦多地区(GTA)部分有 18 个沃尔玛超市,如图 5.16 所示。这导致了访问位于 Durham 地区、York 地区和安大略省多伦多的这些商店的 18!条可能的路线。

图 5.16 大多伦多地区(GTA)选定的沃尔玛超市
每个 POI 的 GPS 坐标(经度和纬度)以及地址可在 POI 工厂网站(www.poi-factory.com/node/25560)上找到,并且包含在可免费下载的 Walmart_United States&Canada.csv 文件中(仅限非商业用途),下载前需注册。Google Places API、Here Places API 以及 ArcGIS Marketplace 上的 SafeGraph 也可以用来获取关于医院、餐厅、零售店和杂货店等兴趣点的数据。附录 B 提供了更多关于公开数据源的信息。
在本例中,使用 OSMnx 库创建了一个表示超级中心位置的 NetworkX 图。也可以使用 Pyrosm 代替 OSMnx。这些位置之间的最短距离使用 NetworkX 内置函数shortest_path计算,该函数默认使用 Dijkstra 算法(见第 3.4.1 节)。根据 GPS 坐标在 OpenStreetMap 上使用 folium 库渲染超级中心位置。附录 A 提供了关于这些库的更多详细信息。
在我们的实现中,问题求解器与问题对象解耦。我们首先为这个离散问题创建一个 TSP 对象。然后创建一个 SA 对象来解决问题。使用mutate方法生成一个初始解。如图 5.17 所示,这个初始解远非最优。初始路线的总长度为 593.88 公里,这条路线在实际中不方便或难以遵循。

图 5.17 沃尔玛配送半挂车路线的初始解决方案,总距离为 593.88 公里
让我们使用以下参数运行模拟退火算法:
-
最大迭代次数 = 10000
-
每个温度的最大交互次数 = 100
-
初始温度 = 85
-
最终温度 = 0.0001
-
线性冷却计划
还可以使用其他冷却计划。例如,与其它方案相比,几何冷却可以生成一致、高质量且及时的解决方案。然而,这是可以调整的算法参数之一,因为它有时取决于问题的性质。图 5.18 显示了总距离为 227.17 公里的最短路线。

图 5.18 沃尔玛配送半挂车路线的模拟退火解决方案,总距离为 227.17 公里
下一个列表是使用模拟退火算法生成配送半挂车最短路线的代码片段。完整的代码可在本书的 GitHub 仓库中找到。
列表 5.6 使用模拟退火算法生成沃尔玛配送半挂车路线
from optalgotools.algorithms import SimulatedAnnealing
from optalgotools.problems import TSP
import numpy as np
import pandas as pd
import osmnx as ox
import networkx as nx
import folium
import folium.plugins
wal_df = pd.read_csv("https://raw.githubusercontent.com/Optimization-Algorithms-
➥Book/Code-Listings/main/Appendix%20B/data/TSP/Walmart_ON.csv") ①
cities_list = [city for city, region in cityToRegion.items() if city in
➥wal_df.city.unique() and region in ['Durham Region', 'York Region', 'Toronto']] ②
gta_part = wal_df[wal_df.store_number.str.startswith('Walmart Supercentre') &
➥ wal_df.city.isin(cities_list)].reset_index(drop=True) ③
wal_gta_count = gta_part.shape[0]
gta_part_loc = gta_part[['latitude', 'longitude']] ④
G = ox.graph_from_point(tuple(gta_part_loc.mean().to_list()), dist=42000, ④
➥ dist_type='network', network_type='drive', clean_periphery=True, simplify=True, ④
➥ retain_all=True, truncate_by_edge=True) ④
gta_part['osmid'], gta_part['osmid_dist_m'] = zip(*gta_part.apply(lambda row:
➥ox.nearest_nodes(G, row.longitude, row.latitude, return_dist=True), axis = 1))
gta_part_dists = np.zeros([wal_gta_count, wal_gta_count]) ⑤
gta_part_pathes = [[[] for i in range(wal_gta_count)] for j in range(wal_gta_count)] ⑤
for i in range(wal_gta_count): ⑤
for j in range(wal_gta_count): ⑤
if i==j: ⑤
continue ⑤
gta_part_pathes[i][j] = nx.shortest_path(G=G, source=gta_part.osmid[i], ⑤
➥ target=gta_part.osmid[j], weight='length', method='dijkstra') ⑤
gta_part_dists[i][j] = nx.shortest_path_length(G=G, ⑤
➥source=gta_part.osmid[i], target=gta_part.osmid[j], weight='length', ⑤
➥ method='dijkstra')/1000 ⑤
gta_part_tsp = TSP(dists=gta_part_dists, gen_method='mutate') ⑥
sa = SimulatedAnnealing(max_iter=1000, max_iter_per_temp=100, initial_temp=85,
➥ final_temp=0.0001, cooling_schedule='linear') ⑦
sa.init_annealing(gta_part_tsp) ⑧
sa.run(gta_part_tsp) ⑨
① 加载安大略省所有沃尔玛门店的列表。
② 选择位于达勒姆地区、约克地区或多伦多的城市。
③ 选择前述列表中的沃尔玛门店,并且是超级中心。
④ 获取前述沃尔玛门店的经纬度位置,并创建一个连接这些门店且在 42 公里范围内的道路图。
⑤ 使用图计算沃尔玛门店之间的距离。
⑥ 为问题创建一个 TSP 对象。
⑦ 创建一个 SA 对象以帮助解决 TSP 问题。
⑧ 获取一个初始随机解,并检查其长度。
⑨ 运行 SA,并评估最佳解的距离。
正如你所见,我们在 optalgotools 中将求解器类与问题对象分离。求解器来自algorithms,而问题是problems类中 TSP 问题的一个实例。这种实现允许你更改问题实例并调整算法的参数,以达到最优或近似最优解。你可以考虑尝试第 5.2.5 节中解释的 SA 的适应性方面,以了解它们对算法性能的影响,包括获得路线的长度和运行时间(CPU 时间和墙钟时间)。
类似于 SA 的元启发式算法在合理的计算成本下寻求最优或近似最优解,但不能保证它们的可行性或优化程度。通过适当的参数调整,算法可以提供可接受的解,而无需进一步的后处理。在下一章中,我们将讨论禁忌搜索作为另一种基于轨迹的优化算法。
摘要
-
元启发式算法可以大致分为基于轨迹的算法和基于种群的算法。基于轨迹的元启发式算法,或称为 S 元启发式算法,使用单个搜索代理以分段方式在设计或搜索空间中移动。基于种群的算法,或称为 P 元启发式算法,使用多个代理来寻找最优或近似最优的全局解。模拟退火是一种基于轨迹的元启发式算法。
-
模拟退火模仿材料处理中的退火过程,其中金属冷却并冻结成具有最小能量和较大晶体尺寸的晶体状态,以减少金属结构中的缺陷。退火过程涉及对温度和冷却速率的仔细控制,通常称为退火计划。
-
模拟退火在不同的热力学条件下运行一系列移动,并且总是接受改进的移动,并且可以以概率接受非改进的移动。
-
接受概率与温度成正比。高温增加了在搜索初期接受非改进移动的机会,以有利于搜索空间的探索。随着搜索的进行,温度递减以限制探索并有利于利用。
-
随着温度趋近于零,SA 表现得像爬山法一样贪婪,而当温度趋近于无穷大时,SA 表现得像随机游走。温度应逐渐降低,以实现探索和利用之间的最佳权衡。
-
模拟退火是一种随机搜索算法和无导数求解器,可以在导数信息不可用、不可靠或过于昂贵的情况下使用。模拟退火在合理的计算成本下寻求最优或近似最优解,但它不能保证解的可行性或最优程度。
-
自适应模拟退火可以根据搜索进程动态改变其参数,以控制探索和开发行为。
-
模拟退火是一种易于实现的概率近似算法,可用于解决不同领域中的连续和离散问题。
第六章:禁忌搜索
本章涵盖
-
理解局部搜索
-
理解禁忌搜索如何扩展局部搜索
-
解决约束满足问题
-
解决连续问题
-
解决路径问题
-
解决装配线平衡问题
在上一章中,你被介绍到基于轨迹的元启发式算法,并学习了模拟退火(SA)作为这些元启发式算法的例子。元启发式算法的实际首次使用可能是弗雷德·格洛弗的禁忌搜索(TS)在 1986 年,尽管他关于禁忌搜索的开创性文章是在 1997 年[1]发表的。单词“tabu”(也拼作“taboo”)起源于南太平洋的波利尼西亚语。它是一个用来描述在特定文化或社会中被认为是禁止的、禁止的或被认为是不被社会接受的术语。禁忌搜索被称为“tabu”,因为它使用一个记忆结构来跟踪最近探索过的解决方案,以便避免返回它们,特别是在搜索的早期阶段,以避免陷入局部最优。
TS 是一种强大的基于轨迹的优化技术,它已成功应用于解决不同领域中的不同优化问题,如调度、设计、分配、路径、生产、库存和投资、电信、逻辑和人工智能、技术、图优化和一般组合优化。TS 可以被认为是局部搜索和记忆结构的组合。
本章介绍了禁忌搜索作为一种基于轨迹的元启发式优化技术,讨论了其优缺点,并探讨了其在不同领域的应用。为了说明该算法如何用于解决优化问题,将展示各种案例研究和练习。让我们首先近距离探索局部搜索。
6.1 局部搜索
想象一下自己在一家提供多种餐厅的度假村度假,每家餐厅都提供多样化的菜品以满足你的每一个需求。在你入住的第一天,你可能会随机选择一家餐厅,或者如果你在旅途中感到疲惫,就选择离你房间最近的一家。你可能继续在那家特定的餐厅用餐,或者探索度假村内的其他选项。在这种情况下,你通过限制你的选择仅限于度假村内的选项来应用局部搜索,而不考虑在线订购食物或离开度假村在其他地方用餐的可能性。
局部搜索(LS)是一种搜索技术,它通过迭代地探索当前解或状态附近的搜索空间的一个子集,以通过局部变化来改进这个解。可以应用于解的局部变化类型由一个邻域结构定义。对于一个有限候选解集 S,邻域结构表示一个由对当前解 s ∈ S 进行微小改变所能生成的邻近解集 N(s) ⊆ S。N(s) 作为 s 的邻域,其范围从探索当前解的所有可能邻近者(随机搜索)到仅考虑一个邻近者(局部搜索)。前者可能计算量很大,而后者具有非常有限的视野或搜索空间,并且很容易陷入局部最小值。
如算法 6.1 所示,局部搜索算法从一个初始可行解开始,只要新的邻近解比旧的好,就迭代地移动到一个邻近解。
算法 6.1 局部搜索
Input: an initial feasible solution
Output: optimal solution
Begin
While termination criteria not met do
Generate a neighboring solution by applying a series of local modifications (or moves)
if the new solution is better then
Replace the old one
通常,每个可行解都有多个邻近解。名称“局部搜索”意味着算法在当前解的邻域中搜索新的解。例如,爬山法可以被认为是一种局部搜索技术,其中在每个迭代中考虑一个局部最大化标准或目标函数的新邻近解。爬山算法是一种贪婪算法,因为它只接受改进的解。这有时会导致它收敛到局部最优,除非搜索非常幸运,否则这些局部最优通常是平均解。解决方案的质量和计算时间通常取决于所选的局部移动。
局部搜索算法已被成功应用于在合理时间内解决许多困难的组合优化问题。应用领域包括运筹学、管理科学、工程和生物信息学等领域。通过引入从搜索空间中的局部最小值中逃逸的机制,可以进一步提高基于 LS 方法的性能。这些机制包括但不限于模拟退火、随机噪声、混合随机游走和禁忌搜索。禁忌搜索最初被提出是为了允许局部搜索克服局部最优的困难,并通过允许非改进移动和记住搜索的最近历史来防止循环。
让我们讨论 TS 的各个组成部分。
6.2 禁忌搜索算法
回到我们的度假村例子,即使你在度假村内的某个餐厅的第一顿饭很享受,你也可能会选择在第二天去不同的餐厅用餐,以探索其他选项并避免陷入局部最优。假设你承诺自己不会连续几天在同一个餐厅用餐,这样你就可以探索度假村的其他餐饮选择。一旦你尝试过各种餐厅,你可能会选择回到你之前访问过的其中一家餐厅,并在那里度过你剩余的时光。你通过记住你在尝试的每家餐厅的每顿饭的印象来应用禁忌搜索,并且你可以寻找替代方案,同时考虑你之前记住的喜好。这使你能够通过使用记忆来更灵活、更响应式地探索搜索空间,从而增强你的局部搜索,超越局部最优性。
这个例子展示了禁忌搜索结合了自适应记忆和响应式探索。自适应记忆涉及在搜索过程中记住相关信息或有用信息,例如算法最近做出的移动和找到的有希望的解决方案。响应式探索是一种问题解决方法,它根据新信息和搜索历史调整求解器的行为,以更快地找到更优的解决方案。
禁忌搜索
“禁忌搜索基于这样一个前提,即为了成为智能的,问题解决必须结合自适应记忆和响应式探索。TS 的自适应记忆功能允许实施能够经济有效地搜索解空间的程序。由于局部选择是由搜索过程中收集到的信息引导的,TS 与依赖于半随机过程的记忆无设计形成对比,这些过程实现了一种采样形式。禁忌搜索中强调响应式探索,无论是在确定性还是概率实现中,都源于这样一个假设:一个糟糕的战略选择往往能提供比一个好的随机选择更多的信息。”(摘自 Glover, Laguna 和 Marti 的《禁忌搜索原理》[2]。)
禁忌搜索是一种迭代邻域搜索算法,其中邻域动态变化。该算法最初提出是为了允许局部搜索克服局部最优。TS 通过积极避免搜索空间中已访问的点来增强局部搜索。通过避免已访问的点,可以避免搜索轨迹中的循环并逃离局部最优。禁忌搜索通过禁忌列表使用记忆,禁止重新访问最近探索过的邻域。这样做是为了避免陷入局部最优。这种组合可以显著提高解决某些问题的效率。TS 的主要特点是使用显式记忆,它有两个目的:避免重新访问先前探索过的解,并探索解空间中未访问的区域。TS 过程从初始随机解开始,然后找到邻近解。然后选择最佳解并将其添加到禁忌列表中。在后续迭代中,除非足够的时间已经过去并且它们可以重新考虑,否则排除禁忌活跃项作为潜在候选解。这种方法有助于防止 TS 陷入局部最优。此外,为了减轻禁忌列表排除某些好解的影响,可以采用渴望标准 A(s),它允许重新考虑先前禁忌的移动,如果它们导致比当前最佳已知解更好的解。
算法 6.2 展示了禁忌搜索如何结合局部搜索和记忆结构。
算法 6.2 搜索禁忌算法
Input: an initial feasible solution
Output: optimal solution
Begin
While termination criteria not met do
Choose the best: s’∈N(s) ← N(s)-T(s)+A(s)
Memorize s’ if it improves the best known solution
s←s’
Update Tab list T(S) and Aspiration criterion A(s)
如算法所示,禁忌搜索首先使用一个初始可行解 s,然后迭代地探索搜索空间以生成最优或近似最优解。在每次迭代中,并且当终止条件未满足时,算法创建一个候选移动列表,该列表从当前解在邻域 N(s) 内生成新的解。如果新解 s’ 是一个改进的解,且未列为禁忌活跃 T(s) 或在考虑渴望标准 A(s) 的情况下是可接受的解,则获得的解被指定为新的当前解。然后通过更新禁忌限制和渴望标准来修订可接受性。
图 6.1 以流程图的形式总结了禁忌搜索(TS)的步骤。我们首先从初始化或中间或长期记忆组件中获取一个解。然后,通过在当前解上应用操作员(如交换、删除和插入等),根据手头问题的性质创建候选移动列表。评估这些候选邻近解,并选择最佳可接受候选解。如果不满足停止条件,我们继续更新可接受条件、禁忌限制和渴望标准。

图 6.1 禁忌搜索步骤(基于 F. Glover 的“禁忌搜索和自适应记忆编程——进展、应用和挑战” [1])
以下标准可以用来终止 TS:
-
邻域为空,意味着已经探索了所有可能的邻近解。
-
自上次改进以来执行的迭代次数超过了一个指定的限制。
-
有外部证据表明已经达到了一个最优或近似最优解。
为了更好地理解 TS 算法,让我们考虑一个简化的对称旅行商问题(TSP)的版本,该版本只有四个城市,如图 6.2 所示。

图 6.2 一个 4 城市 TSP。图中边的权重代表城市之间的旅行距离。
一个可行解可以表示为一系列城市或节点,其中每个城市恰好访问一次。假设家乡城市是城市 1,一个初始可行解可以随机选择或使用贪婪方法选择。一种可能的贪婪方法是选择离当前节点最近的未访问节点,并继续此过程,直到所有节点都被访问,从而形成一个覆盖所有节点的完整可行旅行。这个初始解可以用排列表示,如{1,2,4,3}。
为了生成一个邻近解,我们可以应用一个交换算子。邻域代表一组可以通过交换解决方案中任意两个城市的成对交换生成的邻近解。对于这个 4 城市 TSP,并且将节点 1 作为起始节点或家乡城市,邻域的数量是不重复组合数 C(n,k) 或 n-choose-k:

给定初始解为{1,2,4,3},通过应用交换算子可以生成以下三个可行的邻近解:
-
通过交换 3 和 4 得到
-
通过交换 2 和 3 得到
-
通过交换 2 和 4 得到
在每次迭代中,选择具有最佳目标值(最小总距离)的邻近解。
6.2.1 记忆结构
局部搜索策略通常是记忆无的,它们不保留过去移动或解决方案的记录。TS 的主要特点是使用显式记忆。显式记忆指的是在搜索过程中记住之前访问过的移动的一种机制。简单的 TS 通常实现以下两种自适应记忆机制:
-
基于最近或短期记忆——这是一种在搜索过程中跟踪最近访问过的移动的机制。它在防止算法重新访问最近已探索的移动中发挥作用。
-
基于频率或长期记忆——这是一种在整个搜索过程中跟踪特定移动的历史频率的机制,并惩罚那些频繁访问但没有成功或已被证明不太有希望的移动。
记忆类型
根据阿特金森-希夫林模型(也称为多存储模型或模式模型),人类记忆有三个组成部分:感觉记忆、工作记忆(有时称为短期记忆)和长期记忆,如图所示。
感觉记忆是一种非常短暂的记忆,它自动由我们的感知产生,并在原始刺激停止后通常消失。我们的五种感官各有不同的记忆存储。例如,视觉信息存储在图像记忆中,而听觉信息存储在回声记忆中。
短期记忆中存储的信息量取决于对感觉记忆元素的关注程度。工作记忆是短期记忆概念的最新扩展。这种记忆允许你存储和使用执行特定任务所需的临时信息。复述和重复可以帮助增加短期记忆的持续时间。例如,想象你自己是一名快餐或饮料外卖窗口的客户服务代表,从客户那里接收订单并确保这些订单得到满足。客户提供的订单信息存储在你的短期或工作记忆中,一旦订单得到满足,这些信息就不会保留在你的记忆中。

记忆类型
长期记忆存储了你的终身记忆和大量信息,例如你的生日、你的地址、你学到的职业技能等。一些通过工作记忆捕获的重要信息可以被编码并存储在长期记忆中。编码的目的是为正在记忆的信息赋予意义。例如,你可能会将单词“omelet”编码为“鸡蛋,打散,煎”。如果你不能自发地回忆起“omelet”这个词,你仍然可以通过调用你用来编码它的一个索引来检索它,比如“鸡蛋”。这类似于使用查找表进行快速信息检索的编码方式。
图 6.3 展示了背包问题作为一个例子。在这个问题中,每个物品都有一个效用和一个重量,我们希望在不超出最大重量的情况下最大化背包内容的效用。这个问题受背包容量的限制。可以通过在背包中交换物品来生成相邻的解决方案。

图 6.3 背包问题
如图 6.3 所示,可以通过交换 1 和 4 号项来生成一个新的候选解。在这种情况下,这种交换将在接下来的三次迭代中保持 tabu 状态,如图中的 tabu 结构所示。tabu 状态的移动目前位于 tabu 列表中,不能在当前迭代中被选中进行探索。我们还可以通过添加或删除不同的项来生成邻近解。如果邻近结构将“添加”和“删除”视为独立的移动,那么为每种类型的移动保留单独的 tabu 列表可能是个好主意。基于频率的记忆跟踪在指定时间间隔内执行的不同交换的频率。其想法是对频繁访问的交换进行惩罚。
在 TS 中使用最近和频率记忆主要为了防止搜索过程循环,这涉及到无限重复相同的移动序列或重新访问相同的解集。此外,这两种记忆机制在如图 6.4 所示的探索和利用之间起到权衡的作用。

图 6.4 TS 短期记忆和长期记忆以及搜索困境
基于最近记忆的记忆限制搜索在潜在繁荣或精英解集内,以增强搜索同时避免重复或逆转之前访问过的解。基于频率的记忆强调不同移动的频率,引导算法向搜索空间中可能尚未探索的新区域移动。通过阻止最近移动的重复,基于最近记忆的记忆在一定程度上促进了探索,但探索的主要强化来自基于频率的记忆。这些记忆机制之间的相互作用保持平衡,允许算法有效地导航可行搜索空间。
对于 4 城市 TSP 问题,可以使用 tabu 结构来表示这两种记忆形式,如图 6.5 所示。在基于最近记忆中,tabu 结构存储了禁止给定交换的迭代次数。

图 6.5 4 城市 TSP 的 tabu 结构。基于最近记忆中的数字表示 tabu 状态的移动剩余的迭代次数;长期记忆中的数字表示使用该移动的频率计数。
这种基于最近记忆的机制通过使用 tabu 列表作为数据结构来跟踪禁止或 tabu 状态的移动,防止算法在指定数量的迭代中重新访问它们,这被称为tabu 任期。在每次迭代中,tabu 列表中每个移动的任期减少 1,任期为零的移动将从 tabu 列表中删除。tabu 任期T可以通过不同的方法选择:
-
静态—选择 T 为一个常数,这可能会依赖于问题大小,例如使用类似于 √N 或 N/10 迭代的指南,其中 N 是问题大小。已经证明,静态禁忌任期并不能总是防止循环[3]。
-
动态—选择 T 在一个特定范围 T[min] 和 T[max] 内随机变化,这个范围随着搜索进程而变化。阈值 T[min] 和 T[max] 可以根据在特定次数的迭代中解决方案的改进情况而变化。
在先前的 4 城市 TSP 示例(图 6.2)中,假设禁忌任期设置为 3 次迭代。如果基于交换(1,4)生成解决方案,这个交换将在接下来的三次迭代中处于禁忌活跃状态,这意味着在接下来的三次迭代中不能执行这个交换。
基于频率的记忆,如图 6.5 左下角所示,包含与交换频率计数相对应的值。每当两个城市之间发生交换时,频率表中相应交换值的频率计数器将增加 1。在搜索最优解时,频率计数器中的值被视为对频繁访问的解决方案的惩罚。可以将与频率计数成正比的惩罚值直接添加到解决方案的成本或适应度函数中。
6.2.2 渴望标准
避免禁忌活跃移动是必要的,但其中一些移动可能具有显著潜力。在这种情况下,禁忌限制可能会阻碍有希望的解决方案,即使没有循环风险。这个问题被称为停滞。在禁忌搜索中,当算法因为候选移动是禁忌活跃的而持续拒绝它们,并且所有禁忌非活跃移动都已经探索过或都是非改进移动时,就会发生停滞。这可能导致算法反复访问相同的解决方案,而没有在向更好的解决方案取得任何重大进展。
渴望标准可以通过允许算法考虑那些虽然禁忌但能带来比当前最佳解决方案更好的解决方案的移动来减轻这种停滞。通过暂时解除解决方案某些属性的禁忌条件,算法可以探索搜索空间的新区域,并可能发现更好的解决方案。在几乎所有禁忌搜索实现中,常用的渴望标准是允许如果移动产生比迄今为止获得的最佳解决方案(即现任解决方案)更好的解决方案,并且在距离这个禁忌活跃移动从禁忌列表中移除之前迭代次数很少时,可以覆盖禁忌激活规则。
6.2.3 TS 中的适应性
TS 适用于离散和连续解空间。对于一些复杂问题,如调度、二次分配和车辆路径问题,禁忌搜索获得的解通常优于其他方法之前找到的最佳解。然而,为了获得最佳结果,需要仔细调整许多参数,并且所需的迭代次数也可能很大。
对于所有元启发式算法,根据参数设置,可能无法找到全局最优解。TS 参数包括初始解生成方法(随机、贪婪、启发式等)、禁忌任期、邻域结构、渴望标准、停止标准以及频率计数的惩罚值。这些参数可以通过预调优或自动调优来提高 TS 的性能。参数调优是指在算法运行之前找到不同算法参数的合适值,但也可以在算法运行时动态调整,采用确定性、自适应或自适应性方法来平衡探索和利用:
-
确定性调优是指根据某些确定性更新规则改变控制参数,而不考虑搜索算法的任何信息。
-
自适应调优是指更新规则从搜索算法中获取信息并相应地改变控制参数。
-
自适应性调优是指更新规则本身进行适应性调整。
TS 最重要的参数之一是禁忌任期。图 6.6 说明了禁忌任期对 TS 性能的影响。过短的禁忌任期可能导致频繁的循环,算法以重复的方式执行相同的移动或重新访问相同的解。这阻碍了在解空间中探索不同的区域,并可能阻止发现最优或近似最优解。此外,过短的禁忌任期可能会迅速解除对移动的限制,可能导致算法忽略那些暂时被认为不利的有希望的解。相反,过长的禁忌任期可能导致停滞,某些移动在较长时间内被禁止。这可能会阻止算法探索解空间的新区域,从而阻碍发现更好的解。此外,长的禁忌任期会增加算法的内存占用,可能导致效率低下和计算需求增加。这在大规模问题中尤其成问题。

图 6.6 禁忌任期的效果
TS 中纳入的一种自适应方法是允许短期记忆(禁忌期)的长度动态变化,并在指示器识别出有希望的领域时加强搜索,或者在改进似乎最小或检测到局部最优时促进多样化。例如,你可以为禁忌期设置一个下限L[min]和一个上限L[max]。然后,如果在上一个迭代中解决方案有所改进,你可以通过减去 1 来递减禁忌期,以便搜索将集中在潜在改进的区域。如果在上一个迭代中解决方案恶化,你可以通过增加 1 来递增禁忌期,以引导搜索远离显然不良的区域,如图 6.7 所示。L[min]和L[max]的值可以在每特定次数的迭代后随机更改。

图 6.7 动态控制禁忌期
反应性 TS 通过自动学习最优禁忌期来防止循环发生[4]。在此方法中,考虑了两种可能的反应机制。一种称为立即反应机制,通过增加禁忌期来阻止额外的重复。经过若干次R立即反应后,几何增长足以打破任何极限循环。第二种机制,称为逃逸机制,计算重复多次(超过REP次)的移动次数。当这个数字超过预定义的阈值REP时,强制执行多样化的逃逸运动。在创建禁忌搜索的适应性版本时,也可以考虑其他算法参数,例如应用基于频率的记忆或渴望标准。
现在你已经很好地理解了禁忌搜索的各个组成部分,让我们来探讨这个算法如何被用来解决各种优化问题。
6.3 解决约束满足问题
n后问题是一个经典的谜题,它涉及将n个棋后放置在n×n的棋盘上,使得没有两个棋后相互威胁。换句话说,没有两个棋后应该共享同一行、列或对角线。这是一个不定义显式目标函数的约束满足问题(CSP)。假设我们正在尝试使用禁忌搜索解决 7 后问题。在此问题中,图 6.8a 中显示的初始随机配置中的冲突数为 4:{Q1–Q2},{Q2–Q6},{Q4–Q5}和{Q6–Q7}。

图 6.8 7 后问题的 TS 初始化。在左侧,虚线显示了皇后之间的 4 次冲突。在中间,C 代表放置皇后 Q 的列。在右侧,*表示给出最佳邻近解的交换。
图 6.8a 中的初始解可以表示为图 6.8b 中所示的顺序。可以通过如图 6.8c 所示的方式交换生成多个候选邻近解。交换(Q1,Q7)、(Q2,Q3)、(Q2,Q6)和(Q5,Q6)给出相同的值,所以我们可以假设(Q1,Q7)是任意选择的移动,以给出一个新解,如图 6.9 所示。在初始迭代中,Q1 被放置在第 4 列,Q7 被放置在第 2 列。交换 Q1 和 Q7 意味着将 Q1 放置在第 2 列,Q7 放置在第 4 列。

图 6.9 一个 7 皇后问题—TS 迭代 1
碰撞次数现在减少到 2,分别是{Q2-Q6}和{Q4-Q5}。禁忌结构更新如图 6.9c 所示,禁止最近执行的交换(Q1,Q7)进行三次迭代,假设禁忌期为 3。
在下一次迭代中,可以通过交换 Q2 和 Q4 生成其他邻近解,如图 6.10 所示。交换(Q2,Q4)给出一个新候选解,因为它减少了 1 次碰撞。此解决方案相关的碰撞数是 1。禁忌结构更新,搜索继续。

图 6.10 一个 7 皇后问题—TS 迭代 2
在下一次迭代(图 6.11)中,选择交换(Q1,Q3)作为给出新解决方案的移动。

图 6.11 一个 7 皇后问题—TS 迭代 3
在新的迭代(图 6.12)中,选择交换(Q5,Q7)。图 6.12a 中的T表示禁忌活跃移动。

图 6.12 一个 7 皇后问题—TS 迭代 4
在下一次迭代中,选择交换(Q4,Q7)(图 6.13)。

图 6.13 一个 7 皇后问题—TS 迭代 5
在下一次迭代中,由于改进的交换是禁忌活跃的,我们可以应用渴望标准来选择交换(Q1,Q3),因为在此交换出禁忌列表之前只剩下一个迭代(图 6.14)。

图 6.14 一个 7 皇后问题—TS 迭代 6
基于此解决方案,板配置将如图 6.15 所示。这是各种可能解决方案之一。

图 6.15 通过手动迭代生成的 7 皇后解决方案
让我们探讨如何使用 Python 通过禁忌搜索来解决这个问题。首先,我们将导入以下 Python 库,用于随机数生成、多维数组和绘图。然后,我们将定义一个函数来生成n-皇后板的随机配置,基于预定义的板大小。
列表 6.1 解决 7 皇后问题
import random
import numpy as np
import matplotlib.pyplot as plt
def get_initial_state(board_size):
queens = list(range(board_size))
random.shuffle(queens)
return queens
假设板大小为 7,调用此函数返回一个随机板配置,如[0, 4, 1, 5, 6, 2, 3]。这意味着 Q1、Q2、Q3、Q4、Q5、Q6 和 Q7 分别放置在第 1、5、2、6、7、3 和 4 列。
然后,我们可以定义一个函数来计算棋盘上相互攻击的皇后的数量。此函数定义如下:
def num_attacking_queens(queens):
board_size = len(queens)
num_attacks = 0
for i in range(board_size):
for j in range(i + 1, board_size):
if queens[i]==queens[j] or abs(queens[i] - queens[j]) == j - i:
num_attacks += 1
return num_attacks
接下来,我们可以创建一个函数来确定最佳可能的移动,该移动可以减少棋盘上的攻击次数,同时确保该移动当前不在禁忌列表上(即,不是禁忌活跃的)。此函数定义如下:
def get_best_move(queens, tabu_list):
board_size = len(queens)
best_move = None
best_num_attacks = board_size * (board_size - 1) // 2
for i in range(board_size):
for j in range(board_size):
if queens[i] != j:
new_queens = queens.copy()
new_queens[i] = j
if str(new_queens) not in tabu_list:
num_attacks = num_attacking_queens(new_queens)
if num_attacks < best_num_attacks:
best_move = (i, j)
best_num_attacks = num_attacks
return best_move
如你所注意到的,最佳攻击次数初始化为最大攻击次数,即 n * (n – 1) / 2。在 7 皇后问题中,这个数字是 7 * 6 / 2 = 21。
我们还需要实现一个函数,根据预定义的禁忌任期更新禁忌列表。以下是此函数的定义:
def update_tabu_list(tabu_list, tabu_tenure, move):
tabu_list.append(str(move))
if len(tabu_list) > tabu_tenure:
tabu_list.pop(0)
以下函数执行禁忌搜索的步骤,接受如最大迭代次数、禁忌任期、在得出解决方案陷入停滞的结论之前,最大无改进移动次数以及初始解决方案等输入参数:
def tabu_search(num_iterations, tabu_tenure, max_non_improvement, queens):
num_non_improvement = 0
best_queens = queens
best_num_attacks = num_attacking_queens(queens)
tabu_list = []
for i in range(num_iterations):
move = get_best_move(queens, tabu_list)
if move is not None:
queens[move[0]] = move[1]
update_tabu_list(tabu_list, tabu_tennure, move)
num_attacks = num_attacking_queens(queens)
if num_attacks < best_num_attacks:
best_queens = queens
best_num_attacks = num_attacks
num_non_improvement = 0
else:
num_non_improvement += 1
if num_non_improvement >= max_non_improvement:
break
return best_queens, num_attacks
对于 7 大小的棋盘,最大迭代次数为 2,000,禁忌任期是 10,在考虑解决方案陷入停滞之前,最大无改进移动次数是 50。调用禁忌搜索给出解决方案[5, 1, 4, 0, 3, 6, 2],如图 6.16 所示。

图 6.16 由 Python 代码生成的 7 皇后解决方案
该实现的完整代码可以在本书 GitHub 仓库的列表 6.1 中找到。代码中使用的迭代次数作为停止标准。作为练习,你可以修改代码以添加一个停止标准,一旦找到零攻击的解决方案,就终止搜索。
n 皇后问题是一个离散问题,因为它涉及到在离散棋盘上找到棋后可行配置。在下一节中,我们将探讨禁忌搜索如何应用于连续问题,以函数优化形式。
6.4 解决连续问题
作为连续问题的示例,让我们从函数优化开始。Himmelblau 函数 (f(x,y) = (x² + y – 11)² + (x + y² – 7)²),以 David Mautner Himmelblau(1924–2011)的名字命名,是一个多模态函数,常被用作优化算法的测试问题。它是一个非凸函数,在(3.0, 2.0),(–2.805118, 3.131312),(–3.779310, –3.283186),和(3.584428, –1.848126)有四个相同的局部最小值,如图 6.17 所示。

图 6.17 Himmelblau 函数在(3.0, 2.0),(–2.805118, 3.131312),(–3.779310, –3.283186),和(3.584428, –1.848126)有四个相同的局部最小值。
我们 optalgotools 包中提供了一个通用的 Tabu 搜索的 Python 实现。在这个实现中,使用哈希表或字典作为索引数据结构来实现 Tabu 结构。哈希表是一组键值对,没有重复的键。它可以用来快速检索数据,无论数据量有多大,因为它具有大 O(1) 的添加、获取和删除函数。
通用 TS 求解器接受以下参数:
-
最大迭代次数(默认
max_iter=1000) -
Tabu tenure (默认
tabu_tenure=1000) -
Neighborhood size (默认
neighbor_size=10) -
Aspiration criteria (默认
use_aspiration=True) -
剩余迭代次数以退出 Tabu(默认
aspiration_limit=None) -
Incorporating frequency-based memory (默认
use_longterm=False)
下面的列表展示了如何使用在 optalgotools 中实现的通用 Tabu 搜索求解器来解决 Himmelblau 函数的最小化问题。
列表 6.2 使用 Tabu 搜索解决 Himmelblau 函数
import numpy as np
from optalgotools.algorithms import TabuSearch ①
from optalgotools.problems import ProblemBase, ContinuousFunctionBase ②
def Himmelblau(x,y):
return (((x**2+y-11)**2) + (((x+y**2-7)**2))) ③
Himmelblau_bounds = np.asarray([[-6, 6], [-6, 6]]) ④
Himmelblau_obj = ContinuousFunctionBase(Himmelblau, Himmelblau_bounds) ⑤
ts = TabuSearch(max_iter=100, tabu_tenure=5, neighbor_size=50, use_aspiration=True,
➥ aspiration_limit=2, use_longterm=False, debug=1) ⑥
ts.run(Himmelblau_obj) ⑦
① 从 optalgotools 导入通用的 Tabu 搜索求解器。
② 导入连续问题库
③ 定义目标函数。
④ 定义边界。
⑤ 创建一个连续函数对象。
⑥ 定义 TS 求解器。添加 debug = 1 以打印初始和最终解。
⑦ 运行求解器。
运行此代码将为 Himmelblau 函数提供一个潜在解:
Tabu search is initialized:
current value = 148.322
Tabu search is done:
curr iter: 100, curr best value: 0.005569730862620958, curr best: sol: [3.00736837 1.98045825], found at iter: 21
适当的调整各种算法参数可以使你找到最优或近似最优解。附录 B 中提供了其他几个优化测试函数。你可以通过修改列表 6.2 来尝试不同的函数。
接下来,让我们看看 Tabu 搜索如何解决旅行商问题。
6.5 解决 TSP 和路由问题
让我们看看如何使用在 Google OR-Tools 中实现的 Tabu 搜索来解决 TSP 的 Berlin52 实例。这个数据集包含柏林市的 52 个地点 (comopt.ifi.uni-heidelberg.de/software/TSPLIB95/STSP.html)。问题的目标是找到最短的旅行路线,该路线访问每个地点一次,然后返回起点。Berlin52 数据集的最短路线是 7,542,如前一章所述。
我们首先导入 TSP 问题类、OR-Tools 约束编程求解器和定义 OR-Tools 路由库中使用的各种枚举(enums)的协议缓冲模块。然后,我们将从 optalgotools 中实现的通用 tsp 类创建一个 tsp 对象。我们将提取感兴趣的点、节点或城市并计算成对距离。成对距离将被转换为整数,以满足 OR-Tools 的要求。然后,我们将问题数据以字典的形式存储。在这个字典中,distance_matrix 将表示数据集中感兴趣点之间的成对距离。
列表 6.3 使用 OR-Tools 禁忌搜索解决 Belin52 TSP
import numpy as np
from optalgotools.problems import TSP ①
from ortools.constraint_solver import pywrapcp ②
from ortools.constraint_solver import routing_enums_pb2 ③
import matplotlib.pyplot as plt
berlin52_tsp_url = 'https://raw.githubusercontent.com/coin-or/jorlib/
b3a41ce773e9b3b5b73c149d4c06097ea1511680/jorlib-core/src/test/resources/
tspLib/tsp/berlin52.tsp' ④
berlin52_tsp = TSP(load_tsp_url=berlin52_tsp_url, gen_method='mutate',
➥ init_method='random') ⑤
cities = berlin52_tsp.cities ⑥
tsp_dist=berlin52_tsp.eval_distances_from_cities(cities) ⑥
tsp_dist_int=list(np.array(tsp_dist).astype(int)) ⑥
① 从 optalgotools 导入 TSP 问题类。
② 导入 OR-Tools 中 C++ 约束编程求解器的 Python 包装器。
③ 导入协议缓冲区模块。
④ 从永久链接获取 berlin52。
⑤ 从问题类创建不同的 tsp 对象。
⑥ 定义问题参数。
我们需要通过定义 data、索引管理器(manager)和路由模型(routing)来创建一个路由模型。任何两个节点之间的成对距离将由 distance_callback 函数返回,该函数还将从路由变量 Index 转换到距离矩阵 NodeIndex。数据集中任何两个感兴趣点之间的边成本是通过弧成本评估器计算的,该评估器告诉求解器如何计算任何两个位置之间的旅行成本。
数据模型是定义距离矩阵、车辆数量和起始城市或初始仓库的地方:
def create_data_model():
data = {}
data['distance_matrix'] = tsp_dist_int
data['num_vehicles'] = 1
data['depot'] = 0
return data
以下函数返回任何两个节点之间的成对距离:
def distance_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return data['distance_matrix'][from_node][to_node]
可以使用以下函数打印获得的路线及其成本或长度:
def print_solution(manager, routing, solution):
print('Objective: {} meters'.format(solution.ObjectiveValue()))
index = routing.Start(0)
plan_output = 'Route for vehicle 0:\n'
route_distance = 0
while not routing.IsEnd(index):
plan_output += ' {} ->'.format(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(previous_index, index, 0)
plan_output += ' {}\n'.format(manager.IndexToNode(index))
plan_output += 'Route distance: {}meters\n'.format(route_distance)
现在我们来看看如何使用 OR-Tools 中实现的禁忌搜索算法来解决 TSP 问题。我们将首先使用前面的函数来创建一个数据模型,该模型生成 TSP 所需的数据,例如城市之间的距离矩阵、车辆数量以及起始城市或初始仓库。
接下来,我们将定义一个管理器来管理路由问题的索引。为此,我们将使用 OR-Tools 中的 pywrapcp 模块中的 RoutingIndexManager 类。此模块为 CP-SAT 求解器提供了一个 Python 包装器,CP-SAT 求解器是由 Google 开发的约束编程求解器。
然后,我们将使用 RoutingIndexManager 对象创建 RoutingModel 对象。此 RoutingModel 对象用于定义容量车辆路径问题(CVRP)的约束和目标,CVRP 被视为 TSP 的一般化。RegisterTransitCallback() 方法将注册一个回调函数,该函数计算两个城市之间的距离。此回调函数在 distance_callback 函数中定义。
SetArcCostEvaluatorOfAllVehicles() 方法将所有车辆的弧成本评估器设置为传输回调索引,该索引计算两个节点之间的距离。在我们的情况下,我们有一个单独的旅行商或单一车辆(因此 num_vehicles=1),但此代码也可以处理多个 TSP(mTSP)或多个车辆。
DefaultRoutingSearchParameters() 方法将创建一个 RoutingSearchParameters 类的对象,该对象指定了解决路由问题的搜索参数。在这种情况下,局部搜索元启发式设置为禁忌搜索,时间限制设置为 30 秒。其他可用方法包括 GREEDY_DESCENT、SIMULATED_ANNEALING 和 GENERIC_TABU_SEARCH。TABU_SEARCH 和 GENERIC_TABU_SEARCH 之间的主要区别在于它们处理禁忌列表的方式。TABU_SEARCH 为每个变量维护一个禁忌列表,并将禁忌约束应用于当前分配。另一方面,GENERIC_TABU_SEARCH 维护一个用于整个搜索的单个禁忌列表,并将禁忌约束应用于搜索所做的移动。
SolveWithParameters() 方法使用指定的搜索参数解决路由问题。如果找到解决方案,它将调用 print_solution() 函数来打印解决方案:
data = create_data_model() ①
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
➥ data['num_vehicles'], data['depot'])
routing = pywrapcp.RoutingModel(manager)
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.local_search_metaheuristic = (
➥ routing_enums_pb2.LocalSearchMetaheuristic.TABU_SEARCH) ②
search_parameters.time_limit.seconds = 30
search_parameters.log_search = True
solution = routing.SolveWithParameters(search_parameters) ③
if solution:
print_solution(manager, routing, solution)
① 创建模型。
② 将 TABU_SEARCH 设置为求解器。
③ 寻找解决方案。
以下 get_routes() 函数可以调用以从解决方案中提取每辆车的路线。此函数遍历每辆车,从起始节点开始,并添加车辆访问的节点,直到达到终点节点。然后,它返回每辆车的路线列表:
def get_routes(solution, routing, manager):
routes = []
for route_nbr in range(routing.vehicles()):
index = routing.Start(route_nbr)
route = [manager.IndexToNode(index)]
while not routing.IsEnd(index):
index = solution.Value(routing.NextVar(index))
route.append(manager.IndexToNode(index))
routes.append(route)
return routes
routes = get_routes(solution, routing, manager)
for i, route in enumerate(routes): ①
print('Route', i, route)
berlin52_tsp.plot(route) ②
① 打印路线。
② 可视化路线。
运行此代码产生以下结果,并在图 6.18 中显示了路线。
Objective: 7884 meters
Route for vehicle 0:
0 -> 21 -> 31 -> 44 -> 18 -> 40 -> 7 -> 8 -> 9 -> 42 -> 32 -> 50 -> 11 -> 10
-> 51 -> 13 -> 12 -> 26 -> 27 -> 25 -> 46 -> 28 -> 29 -> 1 -> 6 -> 41 -> 20
-> 16 -> 2 -> 17 -> 30 -> 22 -> 19 -> 49 -> 15 -> 43 -> 45 -> 24 -> 3 -> 5
-> 14 -> 4 -> 23 -> 47 -> 37 -> 36 -> 39 -> 38 -> 33 -> 34 -> 35 -> 48 -> 0

图 6.18 使用 OR-Tools 中的禁忌搜索解决 TSP 问题。图中显示了包含在数据集中的感兴趣点的 x 和 y 位置(单位:公里)。
前面的实现应用了一个简单的渴望标准,即如果解决方案比迄今为止遇到的任何其他解决方案都好,则接受该解决方案。OR-Tools 在解决这个问题上非常高效(获得的路线长度为 7,884,而最优解为 7,542)。然而,实现的禁忌搜索主要用于解决路由问题。
作为列表 6.3 的延续,以下代码片段展示了 optalgotools 中的一个通用禁忌搜索求解器,可用于解决相同的问题:
from optalgotools.algorithms import TabuSearch ①
ts = TabuSearch(max_iter=100, tabu_tenure=5, neighbor_size=10000,
➥ use_aspiration=True, aspiration_limit=2, use_longterm=False, debug=1) ②
ts.init_ts(berlin52_tsp,'random') ③
ts.val_cur
ts.run(berlin52_tsp, repetition=1) ④
print(ts.s_best) ⑤
print(ts.val_best) ⑥
berlin52_tsp.plot(ts.s_best) ⑦
① 为问题创建一个 TSP 对象。
② 创建一个 TS 对象以帮助解决 TSP 问题。
③ 获取一个初始随机解,并检查其长度。
④ 运行 TS,并评估最佳解决方案的距离。
⑤ 打印最佳路线。
⑥ 打印路线长度。
⑦ 可视化最佳路线。
运行此代码产生以下结果:
sol: [0, 21, 17, 2, 16, 20, 41, 6, 1, 29, 28, 15, 45, 47, 23, 36, 33, 43, 49,
19, 22, 30, 44, 18, 40, 7, 8, 9, 42, 32, 50, 10, 51, 13, 12, 46, 25, 26, 27,
11, 24, 3, 5, 14, 4, 37, 39, 38, 35, 34, 48, 31, 0], found at iter: 51
7982.79
如您所见,使用我们的禁忌搜索求解器获得的路线长度为 7,982.79,而 OR-Tools 中实现的禁忌搜索提供的是 7,884,最优解为 7,542。optalgotools 中实现的禁忌搜索算法也比 Google 的 OR-Tools 中实现的优化禁忌搜索慢。
让我们回顾一下在 5.6 节中讨论的配送半挂车路线规划问题。在这个问题中,我们需要找到一辆配送半挂车从位于安大略省 Ajax 的 270 Kingston Rd. E 的沃尔玛超级中心 3001 号开始,访问大多伦多地区(GTA)选定区域内的 18 个沃尔玛超级中心的最佳路线。下面的列表显示了我们可以如何使用通用的禁忌搜索求解器来处理这个问题。完整的列表可在本书的 GitHub 仓库中找到。
列表 6.4 使用禁忌搜索解决配送半挂车问题
from optalgotools.algorithms import TabuSearch
from optalgotools.problems import TSP
gta_part_tsp = TSP(dists=gta_part_dists, gen_method='mutate') ①
ts = TabuSearch(max_iter=1000, tabu_tenure=5, neighbor_size=100,
➥ use_aspiration=True, aspiration_limit=2, use_longterm=False, debug=1) ②
ts.init_ts(gta_part_tsp,'random') ③
draw_map_path(G, ts.s_cur, gta_part_loc, gta_part_pathes) ④
ts.run(gta_part_tsp, repetition=5) ⑤
print(ts.s_allbest) ⑥
print(ts.val_allbest) ⑦
draw_map_path(G, ts.s_allbest, gta_part_loc, gta_part_pathes) ⑧
① 为问题创建一个 TSP 对象。
② 创建一个 TS 对象以帮助解决 TSP 问题。
③ 获取一个初始随机解,并检查其长度。
④ 绘制随机初始解的路径。
⑤ 运行禁忌搜索五次,并返回最佳解决方案。
⑥ 打印最佳解决方案。
⑦ 打印最佳路线长度。
⑧ 可视化获得的路线。
配送半挂车问题的生成路线如图 6.19 所示。

图 6.19 沃尔玛配送半挂车路线的 TS 解决方案,总距离为 223.53 公里
求解禁忌搜索算法生成的路线(223.53 公里)比模拟退火算法(227.17 公里)略短。与 OR-Tools 中实现的禁忌搜索算法相比,optalgotools 中的禁忌搜索算法为你提供了更多的自由度来调整更多参数,并处理不同类型的离散和连续问题。
在下一节中,我们将深入探讨制造业面临的另一个显著挑战。
6.6 装配线平衡问题
亨利·福特于 1913 年设计和安装了用于汽车大批量生产的装配线。这种装配线制造的发展使得第二次工业革命及其之后的批量生产成为可能。装配线是一种面向流程的生产系统,其中执行操作的生产品单位,被称为工作站或简单地称为站,是按顺序排列的。工件沿着生产线依次经过各个工作站,通常是通过某种运输系统,如传送带。在每个工作站,添加新部件或进行新的组装,最终在生产线末端形成成品。
例如,图 6.20 展示了具有五个工作站的自行车装配线示例。从初始工作站 WS-1 开始,工人专注于组装车架,为后续任务奠定基础。沿着生产线移动,WS-2 负责安装前叉和把手,而 WS-3 则安装车轮。随后,在 WS-4,工人进行曲柄、链条、变速器、齿轮和踏板的复杂组装。最后,在 WS-5,座椅被牢固地固定,并添加其他附件,完成组装过程。使用三个灯来指示每个工作站的操作状态:紧急、完成和进行中(WIP)。

图 6.20 装配线平衡问题
在实施装配线之前优化其设计至关重要,因为装配线旨在确保高生产效率,而重新配置它们可能导致巨大的投资成本。装配线平衡问题(ALBP)旨在通过将任务(工作元素)分配给工作站来最小化线的闲置时间,同时满足特定约束。ALBP 通常包括在开始实际装配过程之前与给定生产过程相关的所有任务和决策,涉及为生产单元设置系统能力,包括循环时间、站点数量和站点设备,以及将工作内容分配给生产单元,包括任务分配和确定操作顺序。这种装配线的平衡是一个困难的组合优化问题,在制造业中经常出现。
装配线平衡问题可以分为两大类:简单装配线平衡问题(SALBPs)和广义装配线平衡问题(GALBPs)。SALBP 涉及在一个单侧工作站上的串联线生产单一产品,而 GALBP 考虑不同的装配线目标,如混合模型装配线、并行线、U 型线和双边线。
在 SALBP 中,我们有多个任务需要由多个工作站完成。每个任务i有一个时间要求t[i],我们给定一个最大工作站数量。每个工作站有一个循环时间C,这指的是装配线中每个站点完成其分配的任务并将产品传递给下一个站点的分配时间。目标是使所需工作站数量最小化。
为了捕捉工业中 ALBPs 的更真实条件,时间和空间装配线平衡问题(TSALBP)纳入了额外的空间约束。TSALBP 涉及分配一组具有时间和空间属性的任务和优先级图。每个任务必须只分配给一个站点,前提是
-
所有优先约束都得到满足
-
每个站点的作业时间不超过循环时间
-
每个站点的所需空间不超过全局可用空间
表 6.1 展示了不同复杂程度的 TSALBP 的多种变化。
表 6.1 TSALBP 变化:F(可行性问题),OP(单目标优化问题),MOP(多目标优化问题)
| 问题 | 站点数量 | 循环时间 | 站点空间或布局 | 类型 |
|---|---|---|---|---|
| TSALBP-F | 给定 | 给定 | 给定 | F |
| TSALBP-1 | 最小化 | 给定 | 给定 | OP |
| TSALBP-2 | 给定 | 最小化 | 给定 | OP |
| TSALBP-3 | 给定 | 给定 | 最小化 | OP |
| TSALBP-1/2 | 最小化 | 最小化 | 给定 | MOP |
| TSALBP-1/3 | 最小化 | 给定 | 最小化 | MOP |
| TSALBP-2/3 | 给定 | 最小化 | 最小化 | MOP |
| TSALBP-1/2/3 | 最小化 | 最小化 | 最小化 | MOP |
在图 6.20 所示的自行车装配线中,安装前叉和把手取决于组装好的车架的可用性。同样,安装车轮取决于车架和前叉组装的完成。这种依赖关系由一个优先级图定义,该图显示了任务之间的关系,表明哪些任务必须在其他任务开始之前完成。例如,根据图 6.21 所示的优先级图,任务 2 应在开始任务 3 和 4 之前执行。在 ALBPs 中,由于任务之间的依赖关系,任务的顺序不应违反指定的优先级。

图 6.21 一个优先级图
简单的装配线平衡问题可以分为两种类型:类型 1(SALBP-1)和类型 2(SALBP-2)。在类型 1(SALBP-1)下,目标是对于一个给定的周期时间最小化站点的数量。相反,在类型 2(SALBP-2)下,目标是对于一个给定的站点数量最小化周期时间。让我们考虑一个类型 1(SALBP-1)问题,该问题由最小化固定周期时间 CT 和每个站点的可用面积 A 的数量 NS 组成。如果 A → ∞,TSALBP-1 等同于 SALBP-1。我们将使用平滑指数(SI)作为评估工作站之间工作量分布均匀性的定量指标。每个相邻解将使用此 SI 进行定量评估。SI 旨在为每个站点获取最佳任务分配,以最小化站点之间的空闲时间,同时考虑到对站点工作量的约束不能超过周期时间。
SI 按方程 6.1 计算:
|

| 6.1 |
|---|
其中
-
WL[i] 是工作站 i 的工作量
-
WL[max] 是最大工作量
-
NS 是站点的数量
将任务分配给站点,使得工作量不超过周期时间,并且不违反它们的优先级。假设周期时间 CT 为 4 分钟,任务数量 n 为 6,优先级图如图 6.22 所示。

图 6.22 六个任务的优先级图示例
让我们进行手动迭代,了解如何使用禁忌搜索(TS)来解决这个问题,考虑禁忌期限为 3。生成一个随机初始解,如图 6.23 所示,并使用方程 6.1 评估其 SI。禁忌结构或邻域可以定义为通过任何两个任务的成对交换获得的任何其他解。在我们的情况下,我们有六个任务(即n = 6)和一个成对交换(即k = 2)。因此,相邻解的最大数量是不重复组合数 C(n, k),或n选k,或n! / (k!(n – k)!) = 6! / 2!4! = 15 个相邻解。解以任务的排列形式呈现。例如,初始解[1 2 3 4 5 6]反映了六个任务的执行顺序,考虑到优先级约束。

图 6.23 SALBP 的禁忌搜索初始化
图 6.24 显示了 SALBP 的 TS 第一次迭代。为了生成相邻解,我们必须检查优先级图(图 6.22)。例如,为了开始任务 5,前驱任务 3 和 4 必须完成。遵循这个优先级图,当任务 4 完成时,任务 5 和 6 都可以开始。
让我们使用交换方法找到一个相邻解。对于这次迭代,相邻的可行解有(1-2)、(2-3)、(3-4)和(5-6)。由于这三个交换导致相同的 SI,我们可以任意选择一个,例如(1-2),这将导致任务执行顺序的新顺序(即新的候选解)。这个解是[2 1 3 4 5 6]。假设禁忌期限为 3,(1-2)交换应该被添加到禁忌结构中三个迭代。

图 6.24 SALBP 的 TS 第一次迭代
接下来,图 6.25 显示了禁忌搜索的第二次迭代。对于这次迭代,相邻的可行解有(2-1)、(2-3)、(3-4)和(5-6)。请注意,移动(2-1)是禁忌活跃的。选择(3-4)交换是因为它具有最小的 SI。新解为[2 1 4 3 5 6],SI = 0,使用方程 6.1 计算得出。

图 6.25 SALBP 的禁忌搜索第二次迭代
在开始下一次迭代之前,禁忌列表被更新,如图所示。下一个列表显示了用于解决 SALBP 的禁忌搜索实现的代码片段。完整的列表可在本书的 GitHub 仓库中找到。
列表 6.5 使用禁忌搜索解决 SALBP
import pandas as pd
import numpy as np
import random as rd
import math
import matplotlib.pyplot as plt
tasks = pd.DataFrame(columns=['Task', 'Duration']) ①
tasks= pd.read_csv("https://raw.githubusercontent.com/Optimization-Algorithms ①
➥ -Book/Code-Listings/main/Appendix%20B/data/ALBP/ALB_TS_DATA.txt", sep =",") ①
Prec= pd.read_csv("https://raw.githubusercontent.com/Optimization-Algorithms ①
➥ -Book/Code-Listings/main/Appendix%20B/data/ALBP/ALB_TS_PRECEDENCE.txt", ①
➥ sep =",") ①
Prec.columns=['TASK', 'IMMEDIATE_PRECEDESSOR'] ①
Cycle_time = 4 ②
tenure = 3
max_itr=100
solution = Initial_Solution(len(tasks)) ③
soln_init = Make_Solution_Feasible(solution, Prec) ④
sol_best, SI_best=tabu_search(max_itr, soln_init, SI_init, tenure, WS, tasks,
➥ Prec_Matrix, Cycle_time) ⑤
Smoothing_index(sol_best, WS, tasks, Cycle_time, True) ⑥
plt = Make_Solution_to_plot(sol_best, WS, tasks, Cycle_time) ⑦
plt.show() ⑦
① 直接从附录 B 读取数据。
② 定义周期时间。
③ 获取一个初始解。
④ 确保解的可行性,考虑任务优先级约束。
⑤ 运行禁忌搜索。
⑥ 计算最佳解的 SI 值。
⑦ 可视化解。
运行此代码将产生以下输出:
The Smoothing Index value for ['T3', 'T5', 'T6', 'T1', 'T4', 'T2'] solution sequence is: 0.0
The number of workstations for ['T3', 'T5', 'T6', 'T1', 'T4', 'T2'] solution sequence is: 5
The workloads of workstation for ['T3', 'T5', 'T6', 'T1', 'T4', 'T2'] solution sequence are: [3\. 3\. 3\. 3\. 3.]
图 6.26 显示了禁忌搜索在工作站之间负载平衡合理的初始和最终解。

图 6.26 SALBP 的初始和最终解
现在我们使用作为我们 optalgotools 包一部分实现的通用禁忌搜索求解器。有几个 ALBPs 基准数据集。这些数据集在书籍 GitHub 仓库的附录 B 中可用,您可以通过使用文件的原始内容 URL 直接访问它们,这可以通过在 GitHub 中使用“原始”视图来获取。优先级图在扩展名为.IN2 的文件中提供。
下一个列表展示了如何使用通用求解器来解决 MANSOOR 基准 SALBP(对于给定的CT = 48 的最佳NS是 4)。解决方案显示了工作站的最小数量和 SI。
列表 6.6 装配线平衡问题基准测试
from optalgotools.algorithms import TabuSearch ①
from optalgotools.problems import ALBP ②
data_url="https://raw.githubusercontent.com/Optimization-Algorithms-Book/
Code-Listings/main/Appendix%20B/data/ALBP/SALBP-data-sets/precedence%20graphs/" ③
albp_instance= ALBP(data_url, "MANSOOR.IN2", 48.0) ④
ts = TabuSearch(max_iter=20, tabu_tenure=4, neighbor_size=5, use_aspiration=True,
➥ aspiration_limit=None, use_longterm=False) ⑤
ts.init_ts(albp_instance)
ts.run(albp_instance, repetition=5) ⑥
SI = albp_instance.Smoothing_index(list(ts.s_best), ts.val_best,
➥ albp_instance.tasks, True) ⑦
print(SI) ⑧
① 从 optalgotools 导入禁忌搜索求解器。
② 从通用问题类导入 ALBP 类。
③ 定义数据集的 URL。
④ 创建一个 ALBP 实例。
⑤ 创建禁忌搜索求解器的一个实例。
⑥ 使用禁忌搜索解决问题。
⑦ 计算解决方案的 SI。
⑧ 打印结果。
运行此代码给出以下结果:
The Smoothing Index value for ['T1', 'T2', 'T4', 'T5', 'T6', 'T7', 'T9', 'T8', 'T10', 'T3', 'T11'] solution sequence is: 12.296340919151518
The number of workstations for ['T1', 'T2', 'T4', 'T5', 'T6', 'T7', 'T9', 'T8', 'T10', 'T3', 'T11'] solution sequence is: 5
The workloads of workstation for ['T1', 'T2', 'T4', 'T5', 'T6', 'T7', 'T9', 'T8', 'T10', 'T3', 'T11'] solution sequence are: [42\. 44\. 20\. 45\. 34.]
书籍 GitHub 仓库中的完整列表显示了几个不同的数据集,包括以下内容:
-
MITCHELL(对于给定的 CT = 26 的最佳 NS 是 5)
-
SAWYER30(对于给定的 CT = 26 的最佳 NS 是 10)
-
HAHN(对于给定的 CT = 2338 的最佳 NS 是 7)
-
GUNTHER(对于给定的 CT = 44 的最佳 NS 是 12)
-
BUXEY(对于给定的 CT = 47 的最佳 NS 是 7)
-
LUTZ2(对于给定的 CT = 11 的最佳 NS 是 49)
-
BARTHOL2(对于给定的 CT = 104 的最佳 NS 是 41)
-
JACKSON(对于给定的 CT = 9 的最佳 NS 是 6)
-
TONGE70(对于给定的 CT = 293 的最佳 NS 是 13)
这本书的第二部分到此结束。我们将现在将重点转向进化计算算法,如遗传算法。这些算法具有固有的并行性和动态适应搜索以找到最优解的能力。
摘要
-
局部搜索通过在当前解决方案或状态附近迭代探索搜索空间的一个子集,通过进行局部改变来改进解决方案。
-
禁忌搜索通过结合自适应内存结构扩展了局部搜索。它引导局部搜索过程探索解决方案空间,超越任何局部最优性。
-
使用自适应内存结构来记住最近的算法移动并捕获有希望的解决方案。
-
禁忌列表是一种数据结构,用于跟踪禁忌活跃的移动。
-
禁忌任期是指某些移动或解决方案被标记为禁忌活跃的指定迭代次数。
-
过短的禁忌任期可能导致循环和忽略有希望的解决方案,而过长的禁忌任期可能导致停滞和内存过载。
-
为了避免搜索停滞,吸力标准允许通过放宽或暂时解除禁忌条件来接受禁忌活跃的移动。
-
自适应禁忌搜索的一个关键方面是在利用搜索和探索之间取得平衡。
第三部分:进化计算算法
随着我们继续探索优化算法的世界,这部分内容将带您进入遗传算法的迷人领域,遗传算法是种群启发式算法的一个典型例子。在本部分的两章中,您将深入进化计算的内核,并解锁遗传算法作为解决各种优化问题的多功能工具的潜力。
在第七章中,您将了解基于种群的优化算法,特别是遗传算法。您将发现进化计算的内部运作机制,并全面理解构成遗传算法的各个组成部分。我们将通过在 Python 中实现遗传算法来采取实践方法,让您能够将这一强大技术应用于实际问题解决。
第八章将带您进一步探索遗传算法的世界,研究增强其适应不同问题类型的变体。您将深入研究灰度编码遗传算法,探索实值遗传算法及其遗传算子,并理解基于排列的遗传算法及其应用。此外,您还将了解多目标优化的概念,并学习如何微调遗传算法以在探索和利用之间取得平衡。通过实际案例,您将看到遗传算法如何有效地解决连续和离散优化问题。
第七章:遗传算法
本章涵盖
-
介绍基于种群的优化算法
-
理解进化计算
-
理解遗传算法的不同组成部分
-
在 Python 中实现遗传算法
假设你正在进行一次寻宝任务,你不想独自搜索并空手而归。你可能会决定与一群朋友合作并共享信息。这种方法遵循基于种群的搜索策略,其中多个代理参与搜索过程。
在这次协作过程中,你可能会注意到一些猎人表现优于其他人。在这种情况下,你可能会选择只保留表现最好的猎人,并用新招募的成员替换表现不佳的猎人。这个过程类似于遗传算法等进化算法的工作原理,其中最适应的个体生存下来并将它们的特征传递给下一代。
在本章中,我们将介绍并讨论二进制编码的遗传算法作为一种进化计算算法。我们将探讨该算法的不同元素及其实现细节。下一章将讨论遗传算法的其他变体,如灰度编码遗传算法、实值遗传算法和基于排列的遗传算法。
7.1 基于种群的元启发式算法
基于种群的元启发式算法(P-元启发式),如遗传算法、粒子群优化和蚁群优化,利用多个代理来搜索最优或近似最优的全局解。由于这些算法从多样化的初始种群开始,它们自然更侧重于探索,从而有可能找到可能被基于轨迹(S-元启发式)算法遗漏的更好解决方案,后者更侧重于利用。
基于种群的元启发式算法可以根据其灵感来源分为两大类:进化计算算法和群体智能算法,如图 7.1 所示。

图 7.1 元启发式算法
进化计算(EC)算法,正如其名所示,是受生物进化过程的启发。这些算法使用一组潜在解的种群,这些种群通过遗传操作,如变异和交叉,来创造新的后代,这些后代可能具有更好的适应度值。选择过程决定了种群中哪些个体被选中进行繁殖并创造下一代。遗传算法(GA)、差分进化(DE)、遗传编程(GP)、进化编程(EP)、进化策略(ES)、文化算法(CA)和协同进化(CoE)是进化计算算法的例子。
另一方面,群体智能(SI)算法受到蚂蚁、蜜蜂和鸟类等社会生物集体行为的启发,将在本书的第四章中讨论。这些算法使用一个相互作用的代理种群来寻找解决方案。它们使用各种机制,如通信、合作和自组织,来优化搜索过程。群体智能算法的例子包括粒子群优化(PSO)、蚁群优化(ACO)、人工蜂群(ABC)、萤火虫算法(FA)、蝙蝠算法(BA)和狼搜索算法(WSA)。
无论是进化计算还是群体智能算法,它们都是基于种群的算法,从候选解的初始种群开始搜索最优或近似最优解。初始种群的质量和多样性显著影响算法的性能和效率。一个构建良好的初始种群为搜索过程提供了一个良好的起点,并有助于算法快速收敛到搜索空间的希望区域。相反,一个构建不良的初始种群可能导致过早收敛到次优解,可能使算法陷入次优区域,或者可能需要更长的时间收敛到解。为了确保探索和利用之间的良好平衡,初始种群应该是多样化的,并覆盖广泛的潜在解。
在 El-Ghazali Talbi 的《元启发式算法:从设计到实现》[1]中,提供了基于种群元启发式算法的不同初始化策略的比较,基于三个关键方面:多样性、计算成本和初始解的质量。初始解可以通过伪随机过程或准随机搜索生成。初始解也可以顺序生成(顺序多样性)或并发生成(并行多样性)以实现非常高的多样性。启发式方法涉及使用局部搜索或贪婪方法来生成初始解。
如表 7.1 所示,伪随机策略提供了适中的多样性、低计算成本和低质量的初始解。准随机策略表现出更高的多样性,具有可比的计算成本和低质量的初始解。顺序多样性和并行多样性都表现出非常高的多样性,但前者产生适中的计算成本,而后者具有低计算成本;两种方法都导致低质量的初始解。相比之下,使用诸如局部搜索或贪婪启发式等启发式方法可以产生高质量的初始解,但多样性和计算成本较低。
表 7.1 基于种群的元启发式算法的初始化策略
| 初始化策略 | 多样性 | 计算成本 | 初始解的质量 |
|---|---|---|---|
| 伪随机 | 中等 | 低 | 低 |
| 近似随机 | 高 | 低 | 低 |
| 顺序多样化 | 非常高 | 中等 | 低 |
| 并行多样化 | 非常高 | 低 | 低 |
| 启发式(例如,局部搜索或贪婪启发式) | 低 | 高 | 高 |
使用随机化方法生成初始种群通常是有益的,其中候选者是从搜索空间的不同区域抽取的样本,以最大化找到最优解的机会。下面的列表展示了我们如何使用 Python 来采样初始解。让我们首先生成 200 个伪随机数。
列表 7.1 在 Python 中生成初始种群
import math
import numpy as np
import matplotlib.pyplot as plt
np.random.seed(6345245) ①
N=200 ②
P_random_pseudo=np.random.rand(N,N) ③
① 为随机数生成器设置种子。
② 样本数量
③ 伪随机采样
注意:随机数本质上不可预测,伪随机数是确定的但看起来是随机的,而近似随机数是具有均匀分布模式的确定数。
ghalton 库中的广义 Halton 数生成器可以用来生成近似随机数。这种方法基于 Halton 序列,它使用互质数作为其基数。你可以如下使用广义 Halton 数生成器:
!pip install ghalton
import ghalton
sequencer = ghalton.GeneralizedHalton(7,23)
P_random_quasi = np.array(sequencer.get(N))
Box-Muller 变换用于从均匀分布的随机数对生成独立的标准正态分布随机数对。Box-Muller 是一种二维高斯采样方法,可以使用如下方式:
u1 = np.random.uniform(size=(N)) ①
u2 = np.random.uniform(size=(N)) ①
P_BM_x = np.sqrt(-2*np.log(u1))*np.cos(2*math.pi*u2) ②
P_BM_y = np.sqrt(-2*np.log(u1))*np.sin(2*math.pi*u2) ②
① 生成 0 到 1 之间的均匀分布值。
② 使用 Box-Muller 计算 x 和 y 值。
Box-Muller 变换的一个缺点是它倾向于将值聚集在平均值周围,因为它依赖于均匀分布。此外,计算平方根可能很昂贵。
中心极限定理(CLT)采样是另一种采样方法,其中样本均值的分布随着样本量的增大而近似正态分布,而不管总体分布如何。以下代码片段展示了如何实现这种方法:
import random
P_CLT_x=[2.0 * math.sqrt(N) * (sum(random.randint(0,1) for x in range(N)) / N - 0.5)
➥ for x in range(N)]
P_CLT_y=[2.0 * math.sqrt(N) * (sum(random.randint(0,1) for x in range(N)) / N - 0.5)
➥ for x in range(N)]
Sobol 低偏差序列(LDS)是 sobol_seq 包中可用的一种近似随机采样方法。这种方法生成一系列点,这些点在样本空间中均匀分布,使得相邻点之间的间隔尽可能小。它可以如下使用:
!pip install sobol_seq
import sobol_seq
P_sobel=sobol_seq.i4_sobol_generate(2,N)
拉丁超立方体采样是一种并行多样化方法,其中搜索空间被分解成 25 个块,每个块中伪随机地生成一个解。下面展示了在 pyDOE(实验设计)Python 包中使用拉丁超立方体采样方法的示例:
!pip install pyDOE
from pyDOE import *
P_LHS=lhs(2, samples=N, criterion='center')
让我们可视化所有这些采样方法,以便我们可以很好地了解它们之间的差异:
f, (ax1, ax2) = plt.subplots(ncols=2, figsize=(18,8))
f, (ax3,ax4) = plt.subplots(ncols=2, figsize=(18,8))
f, (ax5, ax6) = plt.subplots(ncols=2, figsize=(18,8))
ax1.scatter(P_random_pseudo[:,0], P_random_pseudo[:,1], color="gray")
ax2.scatter(P_random_quasi[:100], P_random_quasi[100:], color="red")
ax3.scatter(P_BM_x, P_BM_y, color="green")
ax4.scatter(P_CLT_x, P_CLT_y, color="cyan")
ax5.scatter(P_sobel[:,0], P_sobel[:,1], color="magenta")
ax6.plot(P_LHS[:,0], P_LHS[:,1], "o")
ax1.set_title("Pseudo-random")
ax2.set_title("Quasi-random")
ax3.set_title("Box-Muller")
ax4.set_title("Central Limit Theorem")
ax5.set_title("Sobol")
ax6.set_title("Latin Hypercube")
plt.show()
运行此代码生成图 7.2 所示的图表。在此图中,候选解已使用各种采样方法从可行搜索空间中采样,每个点代表一个不同的解。可以通过观察点之间的间隙及其分散程度来评估每种采样方法达到的多样性水平。

图 7.2 生成初始种群的各种采样方法
如附录 A(见 liveBook)所述,有几个 Python 包用于进化计算。在本章中,我们将重点介绍使用 pymoo:Python 中的多目标优化。Pymoo 提供了不同的采样方法来创建初始种群或初始搜索点。例如,随机采样和拉丁超立方采样。作为列表 7.1 的延续,以下代码片段展示了 pymoo 中的随机采样:
!pip install -U pymoo
from pymoo.core.problem import Problem ①
from pymoo.operators.sampling.rnd import FloatRandomSampling ②
from pymoo.util import plotting ③
problem = Problem(n_var=2, xl=0, xu=1) ④
sampling = FloatRandomSampling() ⑤
X = sampling(problem, 200).get("X") ⑥
plotting.plot(X, no_fill=True) ⑦
① 导入问题类的实例。
② 导入随机采样方法。
③ 导入可视化方法。
④ 创建一个有两个变量的问题,并指定下限和上限。
⑤ 创建随机采样器的实例。
⑥ 生成 200 个随机解/个体。
⑦ 可视化生成的个体。
以下代码使用拉丁超立方采样生成并可视化 200 个初始解:
from pymoo.operators.sampling.lhs import LHS ①
sampling = LHS()
X = sampling(problem, 200).get("X")
plotting.plot(X, no_fill=True)
① 导入拉丁超立方采样模块。
如果解的形式是排列,可以如下生成随机排列:
per1=np.random.permutation(10) ①
print(per1)
per2 = np.array([5, 4, 9, 0, 1, 2, 6, 8, 7, 3]) ②
np.random.shuffle(per2) ②
print(per2)
pop_init = np.arange(50).reshape((10,5)) ③
np.random.permutation(pop_init) ③
from itertools import combinations ④
size=5 ④
ones=2 ④
for pos in map(set, combinations(range(size), ones)): ④
print([int(i in pos) for i in range(size)], sep='\n') ④
① 随机排列一个序列,或返回一个排列的范围。
② 随机打乱一个序列。
③ 初始解的种群作为实值排列
④ 初始解的种群以二进制排列的形式,其中二进制字符串的位数和每个二进制字符串中 1 的个数
您也可以使用以下代码在两点之间生成一个随机路径:
import osmnx as ox
import random
from collections import deque
from optalgotools.structures import Node
G = ox.graph_from_place("University of Toronto")
fig, ax = ox.plot_graph(G)
def randomized_search(G, source, destination): ①
origin = Node(graph = G, osmid = source)
destination = Node(graph = G, osmid = destination)
route = []
frontier = deque([origin])
explored = set()
while frontier:
node = random.choice(frontier) ②
frontier.remove(node)
explored.add(node.osmid)
for child in node.expand():
if child not in explored and child not in frontier:
if child == destination:
route = child.path()
return route
frontier.append(child)
raise Exception("destination and source are not on same component")
random_route = randomized_search(G, 24959528, 1480794706) ③
fig, ax = ox.plot_graph_route(G, random_route) ④
① 这是一个典型的带有打乱边界的图搜索。
② 这部分是随机化部分。
③ 在两个节点之间生成随机路径。
④ 可视化随机路径。
前面的代码通过打乱边界节点修改了一个典型的图搜索算法。这意味着扩展的候选者是“随机”的,这意味着在重复调用时会产生不同的路径。一些生成的随机路径如图 7.3 所示。

图 7.3 生成随机初始路径
在下一节中,我将介绍基于种群的元启发式算法——进化计算。
7.2 介绍进化计算
进化可以被视为一种优化过程,因为它涉及随着时间的推移逐渐改善生物体的特征,从而适应动态变化和竞争的环境,并增强在这些环境中生存的能力。在本节中,我将概述生物进化的基本概念。理解这些原理对于深入了解进化计算至关重要。
7.2.1 生物学基础简述
细胞核是任何活细胞的核心部分,其中包含遗传信息。这种遗传信息存储在染色体中,每条染色体都是由脱氧核糖核酸(DNA)构成的,它携带用于所有生物体生长、发育、功能和繁殖的遗传信息。人类在其每个细胞中都有 23 对染色体,即总共 46 条染色体。每条染色体由许多不同的部分组成,称为基因,这些基因负责编码个体的特定属性。决定这些属性的基因的变异形式,位于染色体上的特定位置,称为等位基因。每个基因在染色体上都有一个独特的位置,称为座位。所有基因的组合称为基因型,它为生物体提供遗传蓝图,决定个体的特征和特性的潜力。表型一词指的是生物体的可观察的物理、行为和生理特征,这些特征是其基因型和环境的相互作用的结果。
为了说明基因及其在决定生物体特征中的作用,让我们考虑一个例子,其中 DNA 分子由四个基因组成,这些基因负责不同的特征:食欲、运动、足部和皮肤类型。食欲基因可能具有不同的值,反映生物体的饮食,如草食性(H)、肉食性(C)或食虫性(I)。运动基因可能决定生物体的运动方式,如攀爬(CL)、飞行(FL)、奔跑(R)或游泳(SW)。足部基因可能决定生物体具有的足部或肢体类型,如爪子(CLW)、鳍(FLP)、蹄(HV)或翅膀(WNG)。最后,皮肤基因可能决定生物体的皮肤覆盖物,如毛皮(F)、鳞片(S)或羽毛(FTH),如图 7.4 所示。

图 7.4 基因型、表型和分类学
在本例中,基因型指的是生物体的特定遗传构成,这是由个体从其父母那里继承的特定等位基因组合所决定的。这些基因的具体值将决定生物体的表型,或可观察的特征。例如,具有食虫食欲基因、飞行运动基因、翅膀脚基因和羽毛皮肤基因的生物体很可能是鸟类。另一方面,具有食草食欲基因、奔跑运动基因、蹄脚基因和毛皮皮肤基因的生物体很可能是哺乳动物,如白尾鹿。
生物体所属的类别,如物种、属或科,是根据与其他生物体共享的特征进行的分类学分类所确定的。
7.2.2 进化理论
进化理论解释了生物物种是如何随着时间的推移而变化并多样化成我们今天所看到的形态的。这一理论由查尔斯·达尔文提出,为生物多样性和其潜在机制提供了解释。
根据这一理论,自然选择是推动进化的主要机制。在无数代的过程中,适应性从连续的、微小的、随机的性状变化累积效应中产生,自然选择青睐那些最适合其环境的变体。这一现象被称为适者生存:被选中的个体繁殖,将它们的特性传递给后代。其他个体在没有交配的情况下死亡,因此它们的特性被丢弃。随着时间的推移,自然选择在塑造种群的特征和适应性方面发挥着重要作用,促进有利特性的传递,并消除不那么有利的特性。
进化理论
自然选择进化理论可以概括如下:
-
在资源有限和人口稳定的世界上,每个个体都与其他个体竞争生存。
-
具有最佳特征(性状)的个体更有可能生存和繁殖,这些特征将传递给其后代。
-
这些理想特性被后代继承,并且(随着时间的推移)在种群中变得占主导地位。
-
在生产一个生物体后代的过程中,随机事件会导致后代特征发生随机变化。
-
如果这些新特征对生物体有益,那么该生物体的生存机会就会增加。
进化计算技术模拟生物进化过程,并执行一系列操作,例如创建初始种群(染色体集合)、评估种群,并通过多代进化种群。
7.2.3 进化计算
计算智能(CI)是人工智能(AI)的一个子领域,强调设计、应用和开发能够学习和适应以解决复杂问题的算法。它侧重于软计算方法,如模糊逻辑、神经网络、进化计算和群体智能。进化计算(EC)作为 CI 的一个分支,采用受生物进化启发的各种计算方法。这些方法以自然选择、适者生存和繁殖作为其计算系统的核心元素。
通常来说,EC 算法主要由以下主要组件组成:
-
个体种群—这是一组初始通过随机生成或某些启发式方法生成的候选解,然后随着时间的推移进行改进。种群的大小通常很大,以便探索问题可能的广泛解决方案。然而,最佳种群大小取决于各种因素,如问题的复杂性、问题中的变量数量、所需解的精度以及可用的计算资源。在实践中,最佳种群大小通常通过实验确定,通过评估不同种群大小的算法性能,并选择表现最佳的大小。
-
适应度函数—这个函数评估候选解的质量。它通过为种群中的每个个体分配一个适应度值来确定每个解解决给定问题的程度。适应度值越高,解越好。
-
父代选择方法—这种方法用于从种群中选择最有潜力的个体,以便为下一代创造新的后代。
-
遗传算子—这些算子包括交叉和变异,它们用于从选定的父母中创建新的后代。交叉算子通过在两个选定的个体之间交换遗传物质来创建具有父母双方特征组合的新后代。变异算子通过在后代遗传组成中引入随机变化来增加种群多样性并防止停滞。
-
生存方法—这些方法确定种群中哪些个体将幸存到下一代。
这些五个组件共同构成了 EC 算法的基础,可以有效地解决各种优化问题。如图 7.5 所示,存在几种 EC 范式。

图 7.5 EC 范式
这些范式主要在表示个体、父母、生存选择方法和遗传算子的方法上有所不同:
-
遗传算法 (GA)—这种搜索算法模仿自然进化,其中每个个体都是一个作为二进制、实值或排列向量编码的候选解决方案。我们将在本书的这一部分详细讨论遗传算法。
-
微分进化 (DE)—该算法使用实值向量作为个体,通过添加现有解决方案对之间的加权差异来生成新的解决方案。它与遗传算法 (GA) 类似,但在繁殖机制上有所不同。
-
遗传编程 (GP)—这是遗传算法 (GA) 的一个特例,其中每个个体都是一个作为变长树编码的计算机程序。这种树结构用于表示函数和运算符,例如
if-else语句和数学运算。 -
进化编程 (EP)—这与遗传规划 (GP) 类似,但它侧重于进化行为特征而不是程序结构。这是一个开放的框架,其中可以应用任何表示和变异操作,但没有重组操作。
-
进化策略 (ES)—该算法使用实值向量作为个体,并在进化过程中调整变异和重组参数。选择方法包括加法选择、逗号选择、贪婪选择和基于距离的选择。
-
文化算法 (CA)—这种方法将来自共享信念空间的社会学习纳入传统的基于种群的进化过程。CA 模拟了种群文化的进化及其对个体遗传和表型进化的影响。
-
协同进化 (CoE)—这是基于相互作用种群之间发生的相互进化变化,其中每个种群代表一个给定的物种,共同优化耦合目标。
EC 是一种强大的优化方法,具有多个优点以及一些缺点。优点包括以下方面:
-
EC 算法不对问题空间做出任何假设,这使得它们适用于广泛的领域。
-
它们在不同的领域中被广泛适用,可以用于解决各种领域的连续和离散问题。
-
EC 算法产生的解决方案比神经网络或其他黑盒优化技术更易于解释。这主要是因为 EC 算法使用了一个更透明的选择、变异和重组过程,可以逐步跟踪和理解,而神经网络由于其复杂、分层结构和非线性操作,通常被视为“黑盒”。
-
EC 算法提供多个替代解决方案,这在没有单一最佳解决方案的情况下非常有用。
-
EC 算法表现出固有的并行性,这使得它们非常适合在现代硬件上的简单并行实现。
EC 的缺点包括以下方面:
-
EC 算法可能计算成本高昂,这意味着它们可能收敛缓慢或需要大量的计算资源来运行。
-
尽管 EC 算法,如许多元启发式算法一样,不能保证找到最优解,但它们通常在有限的时间内收敛到近似最优解。
-
EC 算法通常需要参数调整以达到良好的性能,这可能既耗时又具有挑战性。
本章主要关注遗传算法。下一节将探讨遗传算法的各个组成部分。
7.3 遗传算法构建模块
遗传算法是 EC 最广泛使用的形式。它们是自适应启发式搜索算法,旨在模拟自然系统中的进化过程,这是查尔斯·达尔文在其进化论中提出的。这些算法代表了在定义的搜索空间内对随机搜索的智能利用。
第一个遗传算法,称为简单遗传算法(SGA),也称为经典或标准 GA,由约翰·霍兰德在 1975 年开发。通过他的研究,霍兰德为设计鲁棒、自适应并能适应新挑战的人工系统提供了见解。通过研究自然系统的过程,他试图创建算法和计算模型,这些模型可以像自然系统一样解决复杂问题。霍兰德将 GA 定义为一种计算机程序,其进化方式类似于自然选择,并且可以解决即使其创造者也不完全理解的复杂问题。GA 基于自然选择的进化原理,使用个体种群,这些个体在变异诱导操作(如变异和交叉)的存在下进行选择。适应度函数用于评估个体,它们的繁殖成功率与它们的适应度成正比。图 7.6 显示了 GA 与自然进化的类比。

图 7.6 GA 与自然进化的对比
遗传算法(GA)首先初始化一个个体或候选解的种群。根据定义的适应度函数评估种群中所有个体的适应度,然后通过执行交叉和变异操作来创建新种群,这些操作生成子代或新解。在约束优化问题中,应在产生后代后进行可行性检查和修复。
种群会持续进化,直到满足某些停止标准,如图 7.7 所示。这些终止标准可能是
-
指定数量的代数或适应度评估(100 或 150 代)
-
一个达到最小阈值的充分解
-
当在指定数量的代数内最佳个体没有改进时
-
当达到内存或时间限制时
-
前述任何组合点

图 7.7 遗传算法步骤
算法 7.1 总结了遗传算法的主要步骤。
算法 7.1 遗传算法
Initialization: Randomly generate an initial population M(0)
Evaluate all individuals: Compute and save the fitness f(m) for each individual in the current population M(t)
While termination criteria are not met
Select parents: Define selection probabilities p(m)for each individual p in M(t)
Apply crossover: Generate M(t+1) by probabilistically selecting individuals from M(t) to produce offspring via genetic operators
Apply mutation: Introduce random changes to individuals
Evaluate: evaluate the fitness of the new individuals
Select survivors: select individuals to form the next generation
GA(遗传算法)的概念简单易懂,因为它模拟了自然进化的过程。它是一个模块化算法,可以并行操作,并且易于分布式执行。GA 功能多样,能够有效地处理多目标优化问题。在噪声环境中尤其有效。GA 被广泛应用于解决复杂的连续和离散优化问题,并且在具有众多组合参数和变量之间非线性相互依赖的场景中表现出色。值得注意的是,截至本书 2024 年出版时,在谷歌专利搜索中搜索“遗传算法”作为复合关键词,大约有 100,000 个结果,而谷歌学术搜索则呈现惊人的 1,940,000 个结果。本卷反映了遗传算法在学术和工业领域内受到的广泛关注及其多样化的应用。
7.3.1 适应度函数
如前所述,GA 在搜索过程中模仿了自然界的适者生存原则。因此,遗传算法自然适用于解决最大化问题。然而,可以使用各种数学变换将最小化问题转换为最大化问题,例如以下这些:
-
否定变换——最简单的变换是对目标函数取反。例如,最大化适应度函数 f(x) = –O(x) 等同于最小化原始目标函数 O(x)。
-
倒数变换——将最小化问题转换为最大化问题的另一种方法是取目标函数的倒数。这仅当目标函数始终非负时才有效。方程 7.1 展示了示例:
|

| 7.1 |
|---|
- 其他数学变换——方程 7.2 展示了另一种变换,将最小化问题中的目标函数 O(x) 转换为最大化问题中的适应度函数 f(x)。在这个方程中,O[i] 是个体 i 的目标函数值,N 是种群大小,V 是一个很大的值,以确保适应度值非负。V 的值可以是方程第二项的最大值,这样对应于目标函数最大值的适应度值为零:
|

| 7.2 |
|---|
根据第 1.3.2 节中引入的对偶原理,这些变换不会改变最小值的位置,但将最小化问题转换为等价的最大化问题。
7.3.2 表示方案
编码 是表示候选解决方案的数据结构,一个好的编码可能是 GA 性能的最重要因素。在 GA 中,候选解决方案的参数(基因)被连接起来形成一个字符串(染色体)。二进制编码、实值编码和排列编码可以用来编码解决方案。二进制编码用于二进制编码 GA(BGA),其中解决方案表示为二进制字符串,如图 7.8 所示。

图 7.8 二进制编码
让我们再次回顾 1.3.1 节中介绍的票价定价示例,其中活动组织者正在计划一个会议,并希望确定最优票价以最大化利润。预期利润由以下方程给出:
|

| 7.3 |
|---|
其中 x 是票价。二进制遗传算法(BGA)可用于找到最大化利润的最优票价,受限于边界约束 75.0 ≤ x ≤ 235.0,这将确保利润为正。BGA 具有简单的二进制编码。前一个函数的边界约束要求我们使用 8 位二进制编码,如下面的侧边栏所述。因此,染色体由长度为 8 的位字符串表示。
计算解决方案所需的最小位数
要计算表示下限(LB)和上限(UB)之间范围所需的位数,以期望的精度 p 为准,遵循以下步骤:
-
计算范围大小:R = (UB – LB).
-
将范围大小除以期望的精度:R / P.
-
向上取整到最接近的整数:number_of_steps = ceil(R / P),其中 ceil 是向上取整函数,将结果向上取整到最接近的整数。
-
计算位数:number_of_bits = ceil(log2),其中 log[2] 是以 2 为底的对数。
让我们计算票价问题所需的位数:75.0 ≤ x ≤ 235.0,假设精度为 0.1:
-
计算范围大小:(235.0 – 75.0) = 160
-
将范围大小除以期望的精度:160 / 0.1 = 1600
-
向上取整到最接近的整数:1600。现在你有 1600 个步骤(值)来表示从 75.0 到 235.0 的数字,精度为 0.1。
-
要找到所需的最小位数,可以使用公式 number_of_bits = ceil(log2)。
number_of_bits = ceil(log2) ≈ ceil(10.64) = 11
因此,你需要 11 位来表示从 75.0 到 235.0 的数字,精度为 0.1。如果你只想考虑整数值(即,精度为 1),则需要 ceil(logs(160)) = ceil(7.32) = 8 位。
如前所述,遗传算法从候选解的初始种群开始。种群大小必须仔细选择,因为非常大的种群大小通常不会提高遗传算法的性能。一些研究也表明,最佳种群大小取决于编码字符串(染色体)的大小。这意味着如果你有 32 位的染色体,种群应该比 16 位的染色体大。
在票价定价问题中,假设我们从大小为 5 的种群开始。表 7.2 显示了可以生成以形成初始种群的随机解的示例。
表 7.2 初始种群
| 候选解 x | 解空间中x的值 | 二进制编码空间中的候选解 | 目标函数 f(x) |
|---|---|---|---|
| x[1] | 77 | 01001101 | 8,820 |
| x[2] | 203 | 11001011 | 84,420 |
| x[3] | 110 | 01101110 | 90,000 |
| x[4] | 145 | 10010001 | 128,500 |
| x[5] | 230 | 11100110 | 18,000 |
一旦我们有了初始种群,我们就可以继续选择将受到遗传算子(交叉和变异)处理的父母。我们将接下来查看选择算子。
7.3.3 选择算子
存在着不同的父母选择方法(算子),它们具有不同的选择压力水平。选择压力指的是最佳个体被选中的概率与所有个体平均选择概率相比。当在遗传算法中使用具有高选择压力的算子时,种群内的多样性比使用具有较低选择压力的算子时下降得更快。这听起来可能很好,但它可能导致种群过早地收敛到次优解,从而限制种群的探索能力并消除不符合由选择压力确定的特定标准的个体。这可能导致种群缺乏多样性,从而降低找到更好解的机会。
平衡选择压力与种群探索能力对于避免过早收敛并鼓励发现多样化的最优解至关重要。图 7.9 展示了某些选择方法。

图 7.9 选择方法及其选择压力
精英主义
精英主义在遗传算法中涉及选择最适应的个体进行交叉和变异,并保留当前种群中表现最好的个体以传播到下一代。保留的个体数量越多,后续种群中的多样性就越低。这种选择方法具有最高的选择压力,如图 7.9 所示。
在票价定价示例中,最佳解(x[4]和x[3])将被选为父母以生成后代,如表 7.3 所示。
表 7.3 解排名
| 候选解x | 解空间中x的值 | 二进制编码空间中的候选解 | 目标函数f(x) | 排名 |
|---|---|---|---|---|
| x[1] | 77 | 01001101 | 8,820 | 5 |
| x[2] | 203 | 11001011 | 84,420 | 3 |
| x[3] | 110 | 01101110 | 90,000 | 2 (第二优个体) |
| x[4] | 145 | 10010001 | 128,500 | 1 (最优个体) |
| x[5] | 230 | 11100110 | 18,000 | 4 |
适应度比例选择
适应度比例选择(FPS)是一种选择方法,它倾向于选择种群中最适应的个体。这种方法创建了一个概率分布,其中个体的选择概率与其适应度值成正比。个体通过从这个分布中随机采样来选择。相对于整个种群,个体适应度分配的计算如下:
|

| 7.4 |
|---|
其中f是由个体染色体表示的解,N 是种群大小。轮盘赌选择是适应度比例选择(FPS)的一个例子。
在我们的票价定价示例中,可以通过以下步骤构建轮盘赌选择:
-
计算种群的总适应度:F = 8,820 + 84,420 + 90,000 + 128,500 + 18,000 = 329,740.
-
计算每个染色体x[k]的选择概率p[k],其中p[k] = f(x[k]) / F. 表 7.4 显示了计算出的选择概率。
表 7.4 选择概率
| 候选解x | 解空间中x的值 | 二进制编码空间中的候选解 | 目标函数f(x) | 选择概率p[k] |
|---|---|---|---|---|
| x[1] | 77 | 01001101 | 8,820 | 0.03 |
| x[2] | 203 | 11001011 | 84,420 | 0.26 |
| x[3] | 110 | 01101110 | 90,000 | 0.27 |
| x[4] | 145 | 10010001 | 128,500 | 0.39 |
| x[5] | 230 | 11100110 | 18,000 | 0.05 |
- 计算每个染色体x[k]的累积概率q[k],其中q[k] = sum(p[j]), j = {1,k}. 表 7.5 显示了计算出的累积概率。
表 7.5 累积概率
| 候选解x | 解空间中x的值 | 二进制编码空间中的候选解 | 目标函数f(x) | 选择概率p[k] | 累积概率q[k] |
|---|---|---|---|---|---|
| x[1] | 77 | 01001101 | 8,820 | 0.03 | 0.03 |
| x[2] | 203 | 11001011 | 84,420 | 0.26 | 0.28 |
| x[3] | 110 | 01101110 | 90,000 | 0.27 | 0.56 |
| x[4] | 145 | 10010001 | 128,500 | 0.39 | 0.95 |
| x[5] | 230 | 11100110 | 18,000 | 0.05 | 1.00 |
- 生成一个范围[0,1]内的随机数r。
5. 如果 q[1] >= r,则选择第一个染色体 x[1];否则,选择 k^(th) 染色体 x[k](2 ≤ k ≤ N),使得 q[k][-1] < r ≤ q[k]。如果我们假设随机生成的数字 r = 0.25,则 x[2](q[2] = 0.28)将被选中,因为 q[2] > 0.25,而如果 r = 0.58,则 x[4] 将被选中,因为 q[4] > 0.58。图 7.10 展示了票价定价示例的轮盘赌图。

图 7.10 票价定价示例的轮盘赌图
如您所见,最适应的个体占据了轮盘赌轮上最大的部分,而最不适应的个体占据了轮盘赌轮上最小的部分。由于比例选择中适应度与选择之间的直接相关性,存在主导个体不成比例地贡献下一代后代的可能性,从而导致种群多样性的减少。这表明比例选择会导致高选择压力。
基于排名的选择
解决遗传算法中 FPS 局限性的一种方法是在确定选择概率时使用相对适应度而不是绝对适应度——个体根据其在种群中相对于其他个体的适应度被选中。这种方法确保选择过程不会被种群中的最佳个体主导。
线性排名和非线性排名都可以使用。在 线性排名 中,个体 i 被选中的基于排名的概率使用以下公式计算:
|

| 7.5 |
|---|
其中 N 代表种群大小,SP 是选择压力(1.0 < SP ≤ 2.0),而 r(i) 是与个体 i 相关的排名(排名越高越好)。在票价定价示例中,N = 5,假设 SP = 1.5,种群中每个个体的基于排名的选择概率显示在表 7.6 中。
表 7.6 基于排名的选择概率
| 候选解 x | 解空间中 x 的值 | 二进制编码空间中的候选解 | 目标函数 f(x) | 排名 r[i] | FPS 累积概率 q[k] | 基于排名的选择概率 |
|---|---|---|---|---|---|---|
| x[1] | 77 | 01001101 | 8,820 | 1 | 0.03 | 0.50 |
| x[2] | 203 | 11001011 | 84,420 | 3 | 0.28 | 0.75 |
| x[3] | 110 | 01101110 | 90,000 | 4 | 0.56 | 0.88 |
| x[4] | 145 | 10010001 | 128,500 | 5 | 0.95 | 1.00 |
| x[5] | 230 | 11100110 | 18,000 | 2 | 1.00 | 0.63 |
如您所见,基于排名的选择通过将更大的选择概率分配给适应性较低的个体来减少 FPS 的偏差。
非线性排名 允许比线性排名更高的选择压力。选择概率使用以下公式计算:
|

| 7.6 |
|---|
其中 X 是多项式(SP–N).X(N–)*¹)+SP.X*(N–)²+…+SP.X+SP=0 的根。这种非线性排序允许选择压力在[1, N – 2]区间内。
随机均匀抽样
随机均匀抽样(SUS)是缓解轮盘赌选择方法潜在偏差的另一种方法。这种方法涉及在带有m个均匀分布指针的饼周围放置一个外部轮盘。使用 SUS,轮盘的一次旋转即可同时选择所有m个个体进行繁殖。图 7.11 显示了使用四个选择点的 SUS 在票价问题中的应用。

图 7.11 随机均匀抽样(SUS)策略
锦标赛选择
锦标赛选择涉及从当前种群中随机选择一组k个个体,其中k是锦标赛小组的大小。一旦形成小组,就在其成员之间举行锦标赛,根据他们的适应度值确定表现最佳的个体。适应度分数最高的个体是赢家,并进入遗传算法的下一阶段。图 7.12 显示了锦标赛选择过程。

图 7.12 锦标赛选择
为了选择m个个体进行繁殖,进行m次锦标赛过程。在每次迭代中,从种群中随机选择一个新的锦标赛小组,小组中的个体相互竞争,直到确定表现最佳的个体。然后从每个锦标赛中选出的赢家用于繁殖,这涉及到应用如交叉和变异等遗传算子来创建新的后代。
随机选择
随机选择是最简单的选择算子,其中每个个体都有相同的 1/N选择概率(其中N是种群大小)。不使用适应度信息,这意味着最佳和最差的个体被选中的概率完全相同。随机选择在所有选择算子中具有最低的选择压力,因为种群中的所有个体都有相同的机会被选中。
其他选择方法
其他选择方法包括但不限于玻尔兹曼选择、(μ, λ)-选择和(μ + λ)-选择,以及名人堂选择。随机选择和锦标赛选择方法作为 pymoo.operators.selection 类的一部分在 pymoo 中实现。
在我们选择父母之后,我们需要通过应用生育算子来产生后代。
7.3.4 生育算子
遗传算法采用两种主要的遗传算子,即交叉和变异,来生成后代。让我们详细看看这两个生育算子。
交叉
交叉受到生物学过程中的重组过程的启发,其中两个染色体之间交换一部分遗传信息。这种遗传物质的交换导致后代的产生,因此两个父代可以产生两个后代。为了确保最佳个体能够贡献其遗传物质,通常通过交叉给予优秀个体更多的繁殖机会。这种机制促进了位于不同染色体上的方案的有效组合,这些方案是子解决方案。单点交叉、n-点交叉和均匀交叉是常用的交叉方法。
在单点交叉中,我们首先在两个父代中选择一个随机点,并在该交叉点处分割父代。然后通过交换尾部创建两个子代,如图 7.13 所示。这种交叉操作产生了两个新的子代(候选解),在图中分别是 01010001 和 01001101(或十进制的 81 和 77),作为潜在的车票价格。根据方程 7.3,这些解决方案分别产生总利润$20,980 和$8,820。


在n-点交叉中,它是单点交叉的推广,我们选择n个随机的交叉点并沿着这些点分割。子代是通过将部分粘合并交替在父代之间进行生成的,如图 7.14 所示。按照图 7.14 中所示的 2 点交叉,生成了两个候选解,分别是 141 和 81,适应度值分别为$126,580 和$20,980。

图 7.14 n-点交叉
在均匀交叉中,从两个父代中交换随机位点的比特来创建两个后代。一个父代被分配标签“正面”,另一个父代被分配标签“反面”。对于第一个子代,对每个基因抛硬币以确定它应该来自“正面”还是“反面”父代。第二个子代是通过取第一个子代中每个基因的逆来创建的,如图 7.15 所示。在这个例子中,应用均匀交叉产生了 217 和 5,适应度值分别为\(53,620 和\)–319,500。正如你所看到的,5 不是一个可行的解,因为它不在{75.0,235.0}的边界约束内。这个解决方案被拒绝。


在 pymoo 中,可以使用修复算子确保算法只在可行空间中搜索。它是在产生后代之后应用的。
突变
突变 是一个将新的遗传物质引入个体的过程,有助于增加群体的多样性。这种多样性很重要,因为它允许群体探索更广泛的可能解决方案来应对当前的问题。突变通常与交叉结合使用,以确保每个基因都能访问到所有等位基因。在突变的情况下,选择机制可能会集中在“弱”个体上,希望突变能将这些个体引入更好的特征,从而增加它们的生存机会。
在二进制遗传算法中,突变是通过以概率 p[m] 独立改变每个基因来实现的。对于每个基因,我们生成一个介于 0 和 1 之间的随机数 r。如果 p[m] > r,我们改变该基因。图 7.16 展示了票价问题中一个个体的突变过程。

图 7.16 突变
新群体
在应用交叉和突变后,我们将有代表新的候选解决方案的新后代。为了开始新一代,我们需要通过从旧群体和新产生的后代中选择个体来创建一个新的群体。新群体的规模将与旧群体保持相同。
生成式遗传算法和稳态遗传算法是遗传算法中使用的两种模型。如图 7.17 所示,在 生成式遗传算法 模型中,整个群体被其后代取代以开始“下一代”。在 稳态遗传算法 中,产生的后代数量少于群体大小。旧个体可能被新个体取代。为新群体选择个体的过程称为 生存选择。我们将接下来探讨生存选择方法。

图 7.17 遗传算法的生成式和稳态模型
7.3.5 生存选择
随机选择、基于年龄的选择、适应度比例选择和锦标赛选择是生存选择方法的例子,这些方法可以通过利用新产生的后代来保留最佳个体,同时向群体引入多样性:
-
在 随机选择 中,新群体是通过随机选择 N 个个体来形成的。
-
使用 基于年龄的选择(或先进先出),最老的个体将被删除。
-
适应度比例选择(FPS)考虑了每个个体的适应度——我们可以根据适应度的倒数删除或替换个体,始终保留最佳个体或删除最差的个体。例如,精英选择 仅涉及从旧群体和新后代中选择最佳个体来创建新群体。这种方法确保了最佳解决方案在代际之间得以保留。
-
赛选法涉及从旧种群和新后代中随机选择个体,然后从每个组中选择最佳个体以创建新种群。这种方法可以在种群中更有效地保持多样性。
在票价示例中,如果我们应用精英选择,新种群将由表 7.7 中显示的所选解决方案组成。
表 7.7 精英选择
| 来源 | 解空间中的候选解 x | 二进制编码空间中的候选解 | 目标函数 f(x) | 排名 | 被选中 |
|---|---|---|---|---|---|
| 旧个体 | 77 | 01001101 | 8,820 | 7 | 否 |
| 203 | 11001011 | 84,420 | 3 | 是 | |
| 110 | 01101110 | 90,000 | 2 | 是 | |
| 145 | 10010001 | 128,500 | 1 | 是 | |
| 230 | 11100110 | 18,000 | 6 | 否 | |
| 由 1 点交叉产生的新个体 | 81 | 01010001 | 20,980 | 5 | 是 |
| 77 | 01001101 | 8,820 | 7 | 否 | |
| 由突变个体 77 生成的新个体 | 103 | 01100111 | 76,420 | 4 | 是 |
你可能已经注意到,单点交叉产生了一个已经在初始种群中存在的解决方案。这种现象并不一定是问题,因为在搜索过程中应用遗传操作时,这是一个预期的结果。交叉和突变可以导致探索性和利用性行为。例如,在单点或 n 点交叉中,基于随机的分割点位置,新的解决方案可以与父母相同或接近,也可以产生更多样化的后代。
7.4 在 Python 中实现遗传算法
遗传算法是一个易于实现的算法。让我们看看我们如何使用 Python 中的遗传算法来解决票价问题。
我们将首先导入必要的包并定义问题。
列表 7.2 使用二进制遗传算法解决票价问题
import numpy as np
import random
from tqdm.notebook import tqdm
from copy import copy
import matplotlib.pyplot as plt
def profit(x):
return -20*x*x+6200*x-350000
因为我们使用二进制遗传算法来解决这个问题,我们需要生成一个初始随机种群。作为列表 7.2 的延续,下面的 init_pop 函数接受两个输入参数——pop_size,表示种群大小,以及 chromosome_length,表示每个染色体的长度:
def init_pop(pop_size, chromosome_length):
ints = [random.randint(75,235) for i in range(pop_size)] ①
strs = [bin(n)[2:].zfill(chromosome_length) for n in ints] ②
bins = [[int(x) for x in n] for n in strs] ③
return bins ④
① 生成一个随机整数列表。
② 将整数转换为二进制字符串。
③ 将二进制字符串转换为二进制数字列表。
④ 返回最终的二进制染色体列表。
init_pop 函数首先生成一个长度等于 pop_size 的从 75 到 235(包含)的随机整数列表。这个列表稍后将转换为二进制表示。ints 列表中的整数随后使用 bin() 函数转换为二进制字符串,该函数返回一个带有前缀 0b 的给定数字的二进制字符串表示。为了去除这个前缀,我们使用切片 [2:]。然后我们使用 zfill() 方法在二进制字符串前面填充前导零,以确保它与 chromosome_length 具有相同的长度。strs 列表中的二进制字符串被转换为二进制位(0 或 1)的列表。这是通过嵌套列表推导式完成的,该推导式遍历二进制字符串中的每个字符并将其转换为整数。函数最终返回一个二进制染色体的列表,其中每个染色体是一个二进制位的列表。
对于给定的种群,我们可以使用以下 fitness_score 函数计算种群中每个元素的适应度。这个适应度函数本质上决定了特定后代的“好坏”。它将种群中的每个单元转换为二进制数(基因型),评估优化利润的函数,然后返回“最佳”的后代。该函数主要接受一个种群作为输入,并返回一个包含两个列表的元组,一个是排序后的适应度值列表,另一个是排序后的种群列表:
def fitness_score(population):
fitness_values = []
num = []
for i in range(len(population)):
num.append(int("".join(str(x) for x in population[i]), base=2)) ①
fitness_values.append(profit(num[i])) ②
tuples = zip(*sorted(zip(fitness_values, population),reverse=True)) ③
fitness_values, population = [list(t) for t in tuples] ④
return fitness_values, population ⑤
① 将二进制转换为十进制。
② 评估每个染色体的适应度,并将适应度值追加到 fitness_values 列表中。
③ 创建适应度值及其对应染色体的元组,然后根据适应度值按降序排序。
④ 将排序后的元组分别解压回单独的列表,用于适应度值和种群。
⑤ 返回排序后的适应度值和种群。
现在我们将使用以下 select_parent 函数中实现的随机选择方法选择两个父代。该函数接受两个输入参数:population,它是种群中个体的列表,以及 num_parents,它表示要选择的父代数量。它返回一个所选父代的列表:
def select_parent(population, num_parents):
parents=random.sample(population, num_parents) ①
return parents ②
① 从给定的种群中随机选择指定数量的独特父代。
② 返回所选父代的列表。
select_parent 函数实现了一种简单的随机抽样选择方法,它给种群中的每个个体以平等的机会被选为父代。其他选择方法,如 FPS 或轮盘赌选择,也可以用来给适应度值更高的个体更高的机会。
以下 roulette_wheel_selection 函数展示了轮盘赌选择的步骤。该函数接受两个输入参数——population,它是种群中个体的列表,以及 num_parents,它表示要选择的父代数量:
def roulette_wheel_selection(population, num_parents):
fitness_values, population = fitness_score(population) ①
total_fitness = sum(fitness_values)
probabilities = [fitness / total_fitness for fitness in fitness_values]②
selected_parents = []
for i in range(num_parents): ③
r = random.random() ④
cumulative_probability = 0 ⑤
for j in range(len(population)):
cumulative_probability += probabilities[j]
if cumulative_probability > r:
selected_parents.append(population[j])
break
return selected_parents
① 计算总适应度。
② 计算每个个体的选择概率。
③ 只选择两个父代。
④ 生成一个介于 0 和 1 之间的随机数 r。
⑤ 找到累积概率包括 r 的个体。
在选择父母后,是时候应用遗传算子来产生后代了。下面的crossover函数实现了单点交叉。该函数接受两个参数作为输入:parents,它是一个包含两个父母染色体的列表,以及crossover_prob,它表示父母之间发生交叉的概率。它返回一个包含父母和后代的列表。第一个后代是通过取第一个父母的第一部分(包括交叉点)和第二个父母从交叉点+1 到染色体末尾的第二部分生成的。同样,第二个后代是通过取第二个父母的第一部分(包括交叉点)和第一个父母从交叉点+1 到染色体末尾的第二部分生成的:
def crossover(parents, crossover_prob):
chromosome_length = len(parents[0])
if crossover_prob > random.random(): ①
cross_point = random.randint(0,chromosome_length) ②
parents+=tuple([(parents[0][0:cross_point+1] +parents[1][cross_ ③
point+1])])
parents+=tuple([(parents[1][0:cross_point+1] +parents[0][cross_ ④
point+1])])
return parents ⑤
① 如果交叉概率大于随机生成的数字,则应用交叉。
② 在染色体索引范围内选择一个随机的交叉点。
③ 创建第一个后代。
④ 创建第二个后代。
⑤ 返回原始父母和通过交叉操作生成的新后代。
现在我们应用突变过程。下面的mutation函数对给定的染色体种群执行突变操作。它接受两个参数作为输入:population,它是一个二进制染色体的列表,以及mutation_prob,它表示染色体中每个基因发生突变的概率。它返回突变后的种群:
def mutation(population, mutation_prob) :
chromosome_length = len(population[0])
for i in range(len(population)): ①
for j in range(chromosome_length-1): ②
if mutation_prob > random.random(): ③
if population[i][j]==1: ④
population[i][j]=0 ④
else: ④
population[i][j]=1 ④
return population ⑤
① 遍历种群中的每个染色体。
② 遍历染色体中的每个基因,除了最后一个。
③ 如果突变概率大于随机生成的数字,则应用突变。
④ 翻转基因的值。
⑤ 返回突变后的种群。
现在我们将所有内容组合起来,并定义二进制遗传算法(BGA)函数。此函数接受以下参数作为输入:
-
population—二进制染色体的初始种群 -
num_gen—算法将运行的代数 -
num_parents—用于交叉的父母数量 -
crossover_prob—父母之间发生交叉的概率 -
mutation_prob—每个基因发生突变的概率 -
use_tqdm(可选,默认=False)—一个布尔标志,用于启用或禁用使用 tqdm 库的进度条
这是 BGA 函数:
def BGA(population, num_gen, num_parents, crossover_prob, mutation_prob, use_tqdm =
➥ False):
states = [] ①
best_solution = [] ①
best_score = 0 ①
if use_tqdm: pbar = tqdm(total=num_gen)
for _ in range(num_gen): ②
if use_tqdm: pbar.update()
scores, population = fitness_score(population) ③
current_best_score = scores[0] ④
current_best_solution = population[0] ④
states.append(current_best_score)
if current_best_score > best_score:
best_score = current_best_score
best_solution = int("".join(str(x) for x in
➥ copy(current_best_solution)), base=2)
parents = select_parent(population, num_parents) ⑤
parents = crossover(parents, crossover_prob) ⑥
population = mutation(population,mutation_prob) ⑦
return best_solution, best_score, states ⑧
① 初始化
② 使用 for 循环运行遗传算法 num_gen 代。
③ 通过调用 fitness_score 函数计算适应度分数并按适应度值对种群进行排序。
④ 更新最佳解决方案和最佳分数。
⑤ 使用 select_parent 随机方法进行父母选择。您可以将此方法替换为 roulette_wheel_selection(population, num_parents)。
⑥ 对选定的父母执行交叉。
⑦ 执行变异。
⑧ 返回最佳解。
此函数返回最佳解、最佳得分以及每代最佳得分的列表。
现在我们可以解决票价问题,从生成以下参数的初始种群开始:
num_gen = 1000
pop_size = 5
crossover_prob = 0.7
mutation_prob = 0.3
num_parents = 2
chromosome_length = 8
best_score = -100000
population = init_pop(pop_size, chromosome_length)
print("Initial population: \n", population)
运行此代码生成了以下初始种群:
Initial population: [[1, 1, 1, 0, 1, 0, 0, 0], [1, 0, 0, 0, 0, 1, 1, 0], [1, 1, 0, 1, 1, 1, 0, 1], [1, 1, 0, 0, 0, 0, 1, 1], [1, 0, 1, 0, 0, 0, 0, 0]]
我们现在可以运行二进制 GA 求解器来获取解决方案,如下所示:
best_solution, best_score, states = BGA(population, num_gen, num_parents,
➥ crossover_prob, mutation_prob, use_tqdm=True)
运行此代码会产生与 SciPy 优化器(见列表 2.4)得到相同的结果:
Optimal ticket price ($): 155
Profit ($): 130500
而不是从头开始编写自己的遗传算法代码,你可以利用现有的 Python 包,这些包提供了 GA 实现。许多开源 Python 库可以帮助简化开发过程并节省时间。这些库通常包括遗传算子、选择方法和其他功能,使得将遗传算法适应不同的优化问题变得更加容易。这些库的例子包括但不限于以下:
-
Pymoo (Python 多目标优化;
pymoo.org/algorithms/moo/nsga2.html) — 一个用于使用进化算法和其他元启发式技术进行多目标优化的 Python 库。Pymoo 提供多种算法,如 GA、微分进化、进化策略、非支配排序遗传算法(NSGA-II)、NSGA-III 和粒子群优化(PSO)。 -
DEAP (Python 分布式进化算法;
deap.readthedocs.io/en/master/— 一个用于在 Python 中实现遗传算法的库。它提供了定义、训练和评估遗传算法模型以及可视化优化过程的工具。DEAP 提供了多种内置遗传算子,包括变异、交叉和选择,以及针对特定优化问题的定制算子支持。 -
PyGAD (Python 遗传算法;
pygad.readthedocs.io/en/latest/) — 一个用于实现遗传算法和微分进化(DE)算法的 Python 库。PyGAD 适用于单目标和多目标优化任务,并且可以应用于广泛的领域,包括机器学习和其他问题域。 -
jMetalPy (
github.com/jMetal/jMetalPy) — 一个专为开发和使用元启发式算法解决多目标优化问题而设计的 Python 库。它支持多种元启发式算法,包括非支配排序遗传算法(NSGA-II)、NSGA-III、强度 Pareto 进化算法(SPEA2)以及基于分解的多目标进化算法(MOEA/D),以及其他优化技术,如模拟退火和粒子群优化。 -
PyGMO (Python Parallel Global Multi-objective Optimizer;
esa.github.io/pygmo/)—一个科学库,提供了大量的优化问题和算法,例如 NSGA-II、SPEA2、非支配排序粒子群优化(NS-PSO)和参数自适应差分进化(PaDE)。它使用广义岛屿模型范式对优化算法进行粗粒度并行化,因此允许用户开发异步和分布式算法。 -
Inspyred (Bio-inspired Algorithms in Python;
pythonhosted.org/inspyred/)—一个用于创建和使用生物启发式计算智能算法的库。它支持多种生物启发式优化算法,如遗传算法(GA)、进化策略、模拟退火、差分进化算法、分布估计算法、帕累托存档进化策略(PAES)、非支配排序遗传算法(NSGA-II)、粒子群优化(PSO)和蚁群优化(ACO)。 -
Platypus (
platypus.readthedocs.io/en/latest/)—一个专注于多目标进化算法(MOEAs)的 Python 进化计算框架。它提供了分析和可视化算法性能和解决方案集的工具。 -
MEALPY (
mealpy.readthedocs.io/en/latest/index.html)—一个提供基于种群元启发式算法实现的 Python 库,如进化计算算法、受群智能启发的计算、受物理启发的计算、受人类启发的计算和受生物学启发的计算。 -
Mlrose (Machine Learning, Randomized Optimization and Search;
mlrose.readthedocs.io/en/stable/index.html)—一个开源的 Python 库,提供了标准遗传算法的实现,用于寻找给定优化问题的最优解。 -
Pyevolve (
pyevolve.sourceforge.net/)—一个开源的 Python 库,旨在用于处理遗传算法和其他进化计算技术。 -
EasyGA (
github.com/danielwilczak101/EasyGA)—一个设计用于提供易于使用遗传算法的 Python 包。值得注意的是,EasyGA 和 Pyevolve 是功能较少且预定义问题较少的简单库,与 DEAP 和 Pymoo 等其他库相比。
列表 A.3,可在本书的 GitHub 仓库中找到,展示了如何使用这些库中的某些功能。
在本书中,我们将专注于使用 pymoo 库,因为它是一个提供多种优化算法、可视化工具和决策能力的综合框架。这个库特别适合多目标优化,我们将在下一章中更详细地探讨。Pymoo 的丰富功能使其成为在各个问题域中实现和分析遗传算法的绝佳选择。表 7.8 总结了所选进化计算框架的比较研究,包括 pymoo [2]。
表 7.8 比较 Python 中选定的进化计算框架
| Library | License | 纯 Python | 可视化 | 专注于多目标 | 决策 |
|---|---|---|---|---|---|
| jMetalPy | MIT | 是 | 是 | 是 | 否 |
| PyGMO | GPL-3.0 | 否(C++带 Python 包装器) | 否 | 是 | 否 |
| Platypus | GPL-3.0 | 是 | 否 | 是 | 否 |
| DEAP | LGPL-3.0 | 是 | 否 | 否 | 否 |
| inspyred | MIT | 是 | 否 | 否 | 否 |
| pymoo | Apache 2.0 | 是 | 是 | 是 | 是 |
以下列表显示了使用 pymoo 中实现的 GA 解决票价问题的步骤。我们将首先从 pymoo 库中导入各种类和函数。
列表 7.3 使用 pymoo 中的 GA 解决票价问题
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.crossover.pntx import PointCrossover,
➥ SinglePointCrossover,
➥ TwoPointCrossover
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
GA类代表 pymoo 库中的单目标遗传算法。PointCrossover、SinglePointCrossover和TwoPointCrossover类代表不同的交叉算子,用于结合父染色体的遗传物质以创建后代。PolynomialMutation类代表一个突变算子,它在染色体的基因中引入小的随机变化。RoundingRepair类代表一个修复算子,它将染色体的变量值四舍五入,确保它们保持在特定范围内或满足某些约束。FloatRandomSampling类代表一个随机采样算子,它生成具有随机浮点值的初始染色体种群。Problem类用于通过指定目标、约束和变量界限来定义优化问题。最后,minimize函数用于执行优化过程。值得注意的是,pymoo 只能处理最小化问题,所以如果你需要用它与最大化问题一起使用,你必须将问题转换为最小化问题,如第 7.3.1 节所述。
在从 pymoo 库导入必要的类和函数之后,我们可以通过以下方式通过继承 pymoo 库中的Problem类来定义TicketPrice问题:
class TicketPrice(Problem):
def __init__(self): ①
super().__init__(n_var=1,
n_obj=1,
n_constr=0,
xl=75.0,
xu=235.0, vtype=float) ②
def _evaluate(self, x, out, *args, **kwargs): ③
out["F"]= 20*x*x-6200*x+350000 ④
① 定义TicketPrice类的构造函数。
② 调用父类 Problem 的构造函数。
③ 定义TicketPrice类的评估函数。
④ 使用给定的公式评估目标函数的值。
如所示,父类Problem的构造函数包含以下具有自定义值的组件,用于票务定价问题:
-
n_var=1—问题中的决策变量数量,设置为 1,表示票价的单个决策变量。 -
n_obj=1—问题中的目标数量,设置为 1,表示单目标优化问题。 -
n_constr=0—问题中的约束数量,设置为 0,表示此优化问题中没有约束。 -
xl=75.0—决策变量的下界,设置为 75.0。 -
xu=235.0—决策变量的上界,设置为 235.0。 -
vtype=float—决策变量的变量类型,设置为浮点型。其他类型包括int和bool。
现在我们可以应用 GA 来解决问题,如下所示:
problem = TicketPrice() ①
algorithm = GA(
pop_size=100,
sampling=FloatRandomSampling(),
crossover=PointCrossover(prob=0.8, n_points=2),
mutation = PolynomialMutation(prob=0.3, repair=RoundingRepair()),
eliminate_duplicates=True
) ②
res = minimize(problem, algorithm, ('n_gen', 100), seed=1, verbose=True) ③
print(f"Optimal ticket price ($): {res.X}") ④
print(f"Profit ($): {-res.F}") ⑤
① 创建TicketPrice问题的实例。
② 实例化一个 GA 对象。
③ 运行求解器。
④ 打印最优票价。
⑤ 打印利润。打印结果时取目标值的相反数。
GA 参数包括以下内容:
-
pop_size=100—将种群大小设置为 100 个个体。 -
sampling=FloatRandomSampling()—使用FloatRandomSampling类生成具有随机浮点值的染色体初始种群。 -
crossover=PointCrossover(prob=0.8, n_points=2)—使用概率为 0.8 和两个交叉点的PointCrossover类作为交叉算子。 -
mutation=PolynomialMutation(prob=0.3, repair=RoundingRepair())—使用概率为 0.3 的PolynomialMutation类作为变异算子,并在需要时应用RoundingRepair类来修复变异解。修复确保每个评估的解实际上都是可行的。 -
eliminate_duplicates=True—设置标志以消除种群中的重复个体。 -
res = minimize(...)—从 pymoo 调用最小化函数以运行优化过程。
运行列表 7.3 产生以下输出:
Optimal ticket price ($): [155]
Profit ($): [130500.]
到目前为止,我们只是对遗传算法进行了初步了解。我们将在下一章深入探讨细节,研究遗传算法的不同变体,并讨论更多实际应用案例。
摘要
-
基于种群的元启发式算法,通常被称为 P-元启发式算法,使用多个代理来寻找最优或近似最优的全局解。这些算法可以根据它们的灵感来源分为两大类:进化计算(EC)算法和群体智能(SI)算法。
-
EC 算法从生物进化过程中汲取灵感。EC 算法的例子包括遗传算法(GA)、差分进化(DE)、遗传编程(GP)、进化编程(EP)、进化策略(ES)、文化算法(CA)和协同进化(CoE)。
-
遗传算法是 EC(进化计算)最广泛使用的形式。它是一种自适应启发式搜索方法,旨在模拟自然系统进化过程中所需的过程,正如查尔斯·达尔文的进化论所概述的那样。
-
假随机策略、准随机策略、顺序多样化、并行多样化和启发式方法代表了 P-元启发式算法(如遗传算法)的各种初始化策略。每种策略提供不同的多样性、计算成本和初始解质量水平。
-
在遗传算法中,交叉和突变算子对于搜索解空间和保持种群内的多样性起着至关重要的作用。这些算子的主要目的是通过平衡探索(搜索解空间的新区域)和利用(优化现有解)来处理搜索困境。
-
建议采用高交叉率和低突变率来平衡探索和利用。高交叉率有助于个体之间优良特性的共享,而低突变率则引入小的、随机的改变以维持多样性并防止过早收敛。这种组合使得算法能够高效地搜索解空间并找到高质量的解。
-
在遗传算法的代际模型中,整个种群被替换,而在遗传算法的稳态模型中,只有一小部分种群被替换。与遗传算法的代际模型相比,稳态模型在计算成本上更低,但代际模型在多样性保持方面优于稳态模型。
-
存在着广泛的用于处理遗传算法的开源 Python 库。其中一个这样的库是 pymoo(Python 中的多目标优化),它包括遗传算法、差分进化、进化策略、非支配排序遗传算法(NSGA-II)、NSGA-III 和粒子群优化(PSO)等流行算法。
第八章:种遗传算法变体
本章涵盖
-
介绍格雷码遗传算法
-
理解实值 GA 及其遗传算子
-
理解基于排列的 GA 及其遗传算子
-
理解多目标优化
-
将遗传算法调整为在探索和利用之间取得平衡
-
使用 GA 解决连续和离散问题
本章继续第七章的主题:我们将探讨各种形式的遗传算法(GA)并深入探讨其现实世界的应用。我们还将探讨一些案例研究和练习,例如旅行商问题(TSP)、比例积分微分(PID)控制器设计、政治选区划分、载货自行车装载问题、制造计划、设施分配以及本章及其在线附录 C 中包含的露天采矿问题。
8.1 格雷码 GA
哈明悬崖效应指的是染色体中微小的变化可能导致解的适应度发生大的变化,这可能导致适应度景观急剧下降,并导致算法过早收敛。在二进制遗传算法中,由于哈明悬崖效应,交叉和变异操作可以显著影响解,尤其是在要更改的位是二进制字符串中最显著的位时。为了减轻哈明悬崖效应,格雷码 GA 使用格雷码编码方案对染色体进行编码。
反射二进制码,通常称为格雷码,是以其发明者 Frank Gray 的名字命名的,是一种独特的二进制数制系统,其特点是相邻数值仅相差一位,如表 8.1 所示。在这个数制系统中,每个值都有一种独特的表示方式,这种表示方式接近其相邻值的表示方式,有助于最小化交叉和变异操作对解的影响。这种编码确保了值之间的平稳过渡,并最小化了在转换过程中或在使用于各种应用(如旋转编码器和数字到模拟转换器)时的错误风险。表 8.1 显示了十进制数 1 到 15 及其对应的二进制和格雷码等价值。
表 8.1 十进制、二进制和格雷码编码
| 十进制数 | 二进制码 | 格雷码 |
|---|---|---|
| 0 | 0000 | 0000 |
| 1 | 0001 | 0001 |
| 2 | 0010 | 0011 |
| 3 | 0011 | 0010 |
| 4 | 0100 | 0110 |
| 5 | 0101 | 0111 |
| 6 | 0110 | 0101 |
| 7 | 0111 | 0100 |
| 8 | 1000 | 1100 |
| 9 | 1001 | 1101 |
| 10 | 1010 | 1111 |
| 11 | 1011 | 1110 |
| 12 | 1100 | 1010 |
| 13 | 1101 | 1011 |
| 14 | 1110 | 1001 |
| 15 | 1111 | 1000 |
独异或(XOR)门用于将 4 位二进制数转换为格雷码,如图 8.1 所示,这是在上一章讨论的票价示例中。这些 XOR 门仅在输入不同时产生 1,在输入相同时产生 0。

图 8.1 格雷码和二进制到格雷码转换
在格雷码中,两个连续的值只相差一位。这个性质减少了相邻数字之间的汉明距离,导致搜索空间或基因型到表型的映射更加平滑。
汉明悬崖问题
将变量编码为二进制字符串的一个缺点是存在汉明悬崖。在二进制编码的遗传算法中,编码值的小幅变化(例如,翻转单个位)可能导致解码值发生显著变化,特别是如果翻转的位位于最高位位置。搜索空间中两个相邻数字之间的这种突然变化被称为汉明悬崖。这个问题通过破坏搜索空间的平滑性,导致收敛不良,并导致探索和利用效率低下,从而对二进制编码的遗传算法产生负面影响。为了解决汉明悬崖问题,可以使用像格雷码或实值编码这样的替代表示,因为它们提供了更好的局部性和更平滑的搜索空间,最小化了小变化对解码值的影响。
例如,假设我们有一个范围在 [0, 15] 内的决策变量,如图所示。在二进制编码的遗传算法中,我们会使用 4 位二进制表示来编码候选解。假设我们在搜索空间中有两个相邻的解:7 和 8,或者二进制表示中的 0111 和 1000。汉明距离是位差的数量,所以 1000 和 0111 之间的汉明距离是 4。这两个解(7 和 8)在搜索空间中是邻居,但当你看它们的二进制表示时,你会发现它们在所有 4 位上都有差异。翻转最高位会导致解码值发生显著变化。在格雷码的情况下,格雷码表示 0100(十进制的 7)和 1100(十进制的 8)之间的汉明距离仅为 1。这意味着这两个相邻解的格雷码表示只相差 1 位,提供了更平滑的搜索空间,并可能提高遗传算法的性能。

二进制和格雷码中十进制数从 0 到 15 的汉明距离
格雷码表示提供了更好的局部性,这意味着编码值的小幅变化会导致解码值的小幅变化。这个性质可以通过减少交叉和变异操作期间破坏性变化的概率来提高遗传算法的收敛性。然而,值得注意的是,格雷码提供的性能改进是问题相关的,并且与二进制编码的遗传算法相比,这种表示方法并不常用。
8.2 实值遗传算法
实值遗传算法是标准遗传算法的一种变体,它使用实数来编码染色体,而不是二进制或灰度码表示。许多优化问题涉及连续变量或实值参数,例如曲线拟合、具有实值输入的函数优化、比例积分微分(PID)控制器参数调整或优化神经网络的权重。为了处理这些连续问题,建议我们直接使用实值遗传算法,以下是一些原因:
-
精度—实值遗传算法在搜索空间中可以达到比二进制遗传算法更高的精度水平。二进制编码需要将搜索空间离散化成有限数量的可能解决方案,这可能会限制搜索的准确性。另一方面,实值编码允许进行连续的搜索空间搜索,这可以提供更精确的搜索。
-
效率—与二进制遗传算法相比,实值遗传算法可能需要更少的位来编码一个解决方案。例如,假设要表示的决策变量有一个下限(LB)为 0 和一个上限(UB)为 10,并且我们需要用精度(P)为 0.0001 来表示解决方案。正如前一章所解释的,用所需的精度P表示LB和UB之间范围的位数是number_of_bits = ceil(log2(UB – LB)/P)) = ceil(log2(ceil(10/0.0001))) = ceil(log2(100000)) = 17 位。实值编码可以使用更少的位来表示比二进制编码更广泛的值。这可能导致更有效地使用可用的内存和计算资源。
-
平滑性—实值遗传算法可以保持搜索空间的连续性和平滑性,这在某些应用中可能很重要。相比之下,二进制遗传算法可能会受到前述章节中讨论的汉明悬崖效应的影响。
-
适应性—实值遗传算法可以更容易地适应搜索空间或适应度景观的变化。例如,如果适应度景观突然变化,实值遗传算法可以调整步长或变异率,以更有效地探索新的景观。另一方面,二进制遗传算法可能需要更广泛的编码或操作参数的重设计,以适应搜索空间的变化。
在以下小节中,我们将探讨实值遗传算法中使用的交叉和变异方法。
8.2.1 交叉方法
实值遗传算法中一些流行的交叉方法包括单算术交叉、简单算术交叉和整体算术交叉。
单算术交叉
单个算术交叉方法涉及随机选择一个基因(k)并生成一个位于[0, 1]范围内的随机权重α。交叉点之前和之后的基因(i < k或i > k)将从相应的父染色体继承基因。对于交叉点的基因(i = k),我们通过取父染色体中相应基因的加权平均来创建后代基因:
子代[1]基因[i] = α × 父代[1]基因[i] + (1 – α) × 父代[2]基因[i]
子代[2]基因[i] = α × 父代[2]基因[i] + (1 – α) × 父代[1]基因[i]
图 8.2 展示了实值 GA 中的单个算术交叉。

图 8.2 实值 GA 中的单个算术交叉
简单算术交叉
简单算术交叉与单个算术交叉相似。在随机选择的交叉点(i < k)之前,基因从相应的父染色体继承。在交叉点(i >= k)之后,我们通过取父染色体中相应基因的加权平均来创建后代基因。图 8.3 展示了实值 GA 中的简单算术交叉。

图 8.3 实值 GA 中的简单算术交叉
整体算术交叉
在整体算术交叉方法中,我们通过对整个父染色体进行加权平均来创建后代。图 8.4 展示了实值 GA 中的这种方法。

图 8.4 实值 GA 中的整体算术交叉
模拟二进制交叉
模拟二进制交叉(SBX)[1]是实值 GA 中另一种交叉方法。在 SBX 中,实值可以通过二进制表示,然后执行点交叉。SBX 通过创建概率分布函数来生成接近父染色体的后代,从而在搜索空间中保持探索和利用之间的平衡。SBX 在 pymoo 中实现。
8.2.2 变异方法
对连续变量进行变异的最简单方法是通过向个体的基因引入小的随机扰动来维持种群中的多样性,并帮助搜索过程逃离局部最优。实值 GA 中使用了多种常见的变异方法:
-
高斯变异——高斯变异向基因添加一个随机值,该随机值是从均值为 0、指定标准差σ的高斯分布中抽取的。标准差控制变异的大小(也称为变异步长)。
-
柯西变异—与高斯变异类似,柯西变异将一个随机值添加到基因中,但这个随机值是从柯西分布(也称为洛伦兹分布或柯西-洛伦兹分布)中抽取的,而不是从高斯分布中抽取。柯西分布的尾部比高斯分布重,导致较大变异的概率更高。
-
边界变异—在边界变异中,变异的基因是从变量的范围(由下限 (LB) 和上限 (UB) 定义)内的均匀分布中随机抽取的。这种方法类似于二进制编码 GA 中的位翻转变异,并有助于探索搜索空间的边界。当最优解位于变量极限附近时,这可能是有用的。
-
多项式变异—多项式变异是一种通过创建概率分布函数 [2] 生成接近父母的子代的方法。分布指数 (η) 控制概率分布函数的形状,较高的值会导致子代更接近其父母(利用)而较低的值会导致子代在搜索空间中分布更广(探索)。
为了说明这些遗传算子,让我们考虑一个曲线拟合的例子。假设我们有表 8.2 中显示的数据点,并且我们想使用实值 GA 将这些数据点拟合到三次多项式。
表 8.2 曲线拟合问题数据
| x | 0 | 1.25 | 2.5 | 3.75 | 5 |
|---|---|---|---|---|---|
| y | 1 | 5.22 | 23.5 | 79.28 | 196 |
三次多项式具有形式 y = ax³ + bx² + cx + d。实值 GA 可以用来找到多项式的四个系数:a, b, c 和 d。这个问题被视为一个最小化问题,目标是最小化衡量拟合多项式与给定数据点接近程度的均方误差(MSE)。MSE 使用以下公式计算:
|

| 8.1 |
|---|
其中 n 是数据点的数量,y 是每个数据点的 y 坐标值,而 y′ 是位于我们创建的直线上的期望值。
在实值 GA 中,候选解由参数向量 a, b, c 和 d 表示,这些参数可以用实数值表示。让我们从以下初始随机解开始:父代[1] = [1 2 3 4]。我们通过将这些值代入函数 (y = x³ + 2x² + 3x + 4) 来计算其适应性,计算每个对应 x 的 y′,并像表 8.3 中那样计算 MSE。
表 8.3 父代 1 的 MSE 计算
| x | 0 | 1.25 | 2.5 | 3.75 | 5 |
|---|---|---|---|---|---|
| y | 1 | 5.22 | 23.5 | 79.28 | 196 |
| y′ | 4 | 12.83 | 39.63 | 96.11 | 194 |
| 平方误差 | 9 | 57.88 | 260 | 283.23 | 4 |
| MSE | 122.83 |
让我们生成另一个随机解:父代[2] = [2 2 2 2],这给出了公式 2x³ + 2x² + 2x + 2 和表 8.4 中的 MSE。
表 8.4 父代 2 的 MSE 计算
| x | 0 | 1.25 | 2.5 | 3.75 | 5 |
|---|---|---|---|---|---|
| y | 1 | 5.22 | 23.5 | 79.28 | 196 |
| y′ | 2 | 11.53 | 50.75 | 143.09 | 312 |
| 误差平方 | 1 | 39.83 | 742.56 | 4,072.2 | 13,456 |
| MSE | 3,662.32 |
对两个父代 P[1] = [1 2 3 4] 和 P[2] = [2 2 2 2] 应用整体算术交叉,权重 α = 0.2,得到以下子代:


假设 Child[1] 受高斯变异的影响。此变异过程导致另一个子代如下:Child[3] = Child[1] + N(0, σ),其中 N(0, σ) 是均值为 0、标准差为 σ 的正态分布中的随机数。假设 σ = 1.2,通过 numpy.random.normal(0, 1.2) 生成了一个随机值 0.43,因此 Child[3] = [1.8 2 2.2 2.4] + 0.43 = [2.23 2.43 2.63 2.83]。
列表 8.1 展示了如何使用 pymoo 中实现的实值 GA 执行此曲线拟合。我们将首先生成一个由三次多项式驱动的数据集,稍后将用作真实值。您可以自由地将这个合成的数据替换为您可能拥有的任何实验数据。
列表 8.1 使用实值 GA 进行曲线拟合
import numpy as np
def third_order_polynomial(x, a, b, c, d):
return a * x**3 + b * x**2 + c * x + d
a, b, c, d = 2, -3, 4, 1 ①
x = np.linspace(0, 5, 5) ②
y = third_order_polynomial(x, a, b, c, d) ③
data_samples = np.column_stack((x, y)) ④
① 定义三次多项式的系数。
② 生成五个值,如手迭代示例中所示。
③ 使用三次多项式函数计算 y 值。
④ 将 x 和 y 值组合成一个数据样本数组。
作为列表 8.1 的延续,我们可以通过继承 pymoo 的 Problem 类来定义一个曲线拟合问题,确保我们传递参数给超类并提供一个 _evaluate 函数。CurveFittingProblem 类有一个初始化方法,该方法将决策变量的数量设置为 4,目标数量的数量设置为 1,约束数量的数量设置为 0,决策变量的下限设置为 –10.0,决策变量的上限设置为 10.0。vtype 参数指定决策变量的数据类型,设置为 float。此初始化方法创建了一个使用遗传算法解决的问题的实例。_evaluate 方法接受一组候选解(X)和一个输出字典(out),并返回 out 字典中每个候选解的适应度 F 字段:
import numpy as np
from pymoo.algorithms.soo.nonconvex.ga import GA ①
from pymoo.operators.crossover.sbx import SBX ②
from pymoo.operators.mutation.pm import PolynomialMutation ③
from pymoo.operators.repair.rounding import RoundingRepair ④
from pymoo.operators.sampling.rnd import FloatRandomSampling ⑤
from pymoo.core.problem import Problem ⑥
from pymoo.optimize import minimize ⑦
class CurveFittingProblem(Problem): ⑧
def __init__(self): ⑨
super().__init__(n_var=4,
n_obj=1,
n_constr=0,
xl=-10.0,
xu=10.0, vtype=float)
def _evaluate(self, X, out, *args, **kwargs): ⑩
Y = np.zeros((X.shape[0], 1))
for i, coeffs in enumerate(X):
y_pred = np.polyval(coeffs, x)
mse = np.mean((y - y_pred)**2)
Y[i] = mse
out["F"] = Y
① 导入用于非凸场景单目标优化的 GA 实现。
② 导入模拟二进制交叉(SBX)算子。
③ 导入多项式变异算子。
④ 导入四舍五入修复算子以确保生成的解保持在指定的范围内。
⑤ 导入浮点随机采样算子以生成随机初始解。
⑥ 导入通用的优化问题类。
⑦ 导入最小化函数。
⑧ 定义曲线拟合的优化问题。
⑨ 使用四个决策变量初始化问题,范围从 –10.0 到 10.0,以及一个无约束的单目标。
⑩ 计算输入变量中每组系数的均方误差。
现在我们可以实例化CurveFittingProblem类来创建要解决的问题的实例。然后我们可以定义用于优化的遗传算法。GA类用于定义算法,pop_size参数将种群大小设置为 50。sampling参数使用FloatRandomSampling算子随机生成候选解的初始种群。crossover参数使用具有交叉概率 0.8 的SBX算子。mutation参数使用具有突变概率 0.3 和舍入修复算子的PolynomialMutation算子,以确保决策变量保持在指定的界限内。eliminate_duplicates参数设置为True以从种群中删除重复的候选解。
接下来,我们可以运行遗传算法,使用minimize函数来解决曲线拟合问题。此函数接受三个参数:要解决的问题的实例(problem)、要使用的算法的实例(algorithm),以及指定算法停止标准的元组('n_gen', 100),表示算法应运行 100 代。seed参数设置为 1 以确保结果的再现性。verbose参数设置为True以在优化过程中显示算法的进度:
problem = CurveFittingProblem() ①
algorithm = GA(
pop_size=50,
sampling=FloatRandomSampling(),
crossover= SBX(prob=0.8),
mutation = PolynomialMutation(prob=0.3, repair=RoundingRepair()),
eliminate_duplicates=True
) ②
res = minimize(problem, algorithm, ('n_gen', 100), seed=1, verbose=True) ③
① 初始化CurveFittingProblem类的实例。
② 创建一个 GA 求解器。
③ 对 100 代进行优化。
您可以按以下方式打印 GA 获得的四个系数:
best_coeffs = res.X
print("Coefficients of the best-fit third-order polynomial:")
print("a =", best_coeffs[0])
print("b =", best_coeffs[1])
print("c =", best_coeffs[2])
print("d =", best_coeffs[3])
这导致以下输出:
Coefficients of the best-fit third-order polynomial:
a = 2, b = -3, c = 4, d = 1
如您所见,四个系数的估计值与真实多项式的系数(a, b, c, d = 2, –3, 4, 1)相同。您可以通过更改多项式系数、使用自己的数据和使用不同的交叉和突变方法来实验代码。
接下来,我们将探讨基于排列的遗传算法。
8.3 基于排列的遗传算法
基于排列的遗传算法旨在解决解决方案是一组元素排列的优化问题。这类问题的例子包括旅行商问题(TSP)、车辆路径问题、体育锦标赛调度和作业调度问题。在这些问题中,解决方案表示为一组元素或事件的最佳顺序或排列。
通常有两种主要类型的问题,其目标是确定事件的最佳顺序:
-
资源或时间限制问题——在这些问题中,事件依赖于有限的资源或时间,这使得事件的顺序对于最佳解决方案至关重要。这类问题的一个例子是拼车调度,其目标是高效地分配车辆和司机等资源,以在尽可能短的时间内服务于最多的乘客。
-
基于邻接的问题——在这些问题中,元素之间的邻近性或邻接性在寻找最佳解决方案中起着重要作用。这类问题的一个例子是旅行商问题(TSP),其目标是访问一组城市,同时最小化总旅行距离,并考虑巡游中相邻城市之间的距离。
这些问题通常被表述为排列问题。在排列表示中,如果有 n 个变量,解决方案是一个包含 n 个不同整数的列表,每个整数恰好出现一次。这种表示确保了解决方案中元素的顺序或邻接性被明确编码,这对于在这些类型的问题中找到最佳事件序列至关重要。例如,让我们考虑以下 8 个城市的 TSP。该 TSP 的一个候选解决方案由排列表示,例如 [1, 2, 3, 4, 5, 6, 7, 8]。在基于排列的遗传算法中,使用专门的交叉和变异算子来保留排列问题的约束,例如在 TSP 中保持城市的有效序列,其中每个城市只出现一次。
以下小节描述了基于排列的遗传算法中常用的交叉和变异方法。遗传算法中交叉和变异方法的选择取决于要解决的问题、所寻求的解决方案类型以及优化问题的目标。通过仔细选择和设计这些算子,遗传算法可以有效地探索和利用搜索空间以找到高质量的解决方案。
8.3.1 交叉方法
在基于排列的遗传算法中,常用的交叉方法包括部分映射交叉(PMX)、边交叉(EC)、顺序 1 交叉(OX1)和循环交叉(CX)。
部分映射交叉
部分映射交叉(PMX)方法通过结合两个父代染色体的遗传信息来创建后代,同时通过算法 8.1 中所示的程序保持后代的可行性。
算法 8.1 部分映射交叉(PMX)
Input: two parents P1 and P2
Output: two children C1 and C2
1\. Initialize: Choose two random crossover points and copy the segment between these two points from parent P1 to child C1 and from the second parent P2 to the second child C2.
2\. For each element in the copied segment of C1:
3\. Find the corresponding element in P2's segment.
4\. If the corresponding element is not already in C1:
5\. Replace the element in C1 at the same position as in P2 with the
corresponding element from P2.
6\. Fill the remaining positions in the offspring with the elements from the other parent, ensuring that no duplicates are introduced.
7\. Repeat steps 2-6 for the second offspring, using P1's segment as the reference.
8\. Return C1 and C2.
图 8.5 展示了 8 个城市的 TSP 问题中的这些步骤。在第 1 步中,选择两个随机的交叉点,并将这两个点之间的城市从父代 P1 复制到子代 C1,从第二个父代 P2 复制到第二个子代 C2。然后我们对第 1 步中没有包含的城市执行步骤 2 到 5。对于 C1 中的第一个城市,即 3,我们需要找到 P2 段中对应的城市,即 7。城市 7 已经不在 C1 中,因此我们需要将城市 7 放在 P2 中城市 3 出现的位置,即右侧的最后位置,如图 8.5 中的实心黑色箭头所示。

图 8.5 部分映射交叉(PMX)
以下列表展示了执行部分映射交叉以生成两个后代的代码。
列表 8.2 部分映射交叉(PMX)
import random
def partially_mapped_crossover(parent1, parent2):
n = len(parent1)
point1, point2 = sorted(random.sample(range(n), 2)) ①
child1 = [None] * n ②
child2 = [None] * n ②
child1[point1:point2+1] = parent1[point1:point2+1] ②
child2[point1:point2+1] = parent2[point1:point2+1] ②
for i in range(n): ③
if child1[i] is None: ③
value = parent2[i] ③
while value in child1: ③
value = parent2[parent1.index(value)] ③
child1[i] = value ③
if child2[i] is None:
value = parent1[i]
while value in child2:
value = parent1[parent2.index(value)]
child2[i] = value
return child1, child2 ④
① 选择两个随机的交叉点。
② 从父节点复制交叉点之间的段到子节点。
③ 将另一个父节点中的剩余元素映射过来。
④ 返回生成的后代。
运行此代码将产生如图 8.6 所示的输出,具体取决于生成的随机样本。

图 8.6 PMX 结果
列表 8.2 的完整版本可在本书的 GitHub 仓库中找到。
边交叉
边交叉(EC)方法保留了父染色体中元素之间的连接性和邻接信息。为了实现这一点,构建了一个边表(或邻接表)。例如,在 8 城市 TSP 中,通过计算两个父节点 P1 = [1, 2, 3, 4, 5, 6, 7, 8]和 P2 = [1, 6, 7, 8, 5, 2, 4, 3]中的相邻元素,创建了边表,如表 8.5 所示。表中的“+”符号表示两个父节点之间的公共边。
表 8.5 边表(或邻接表)
| 城市 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| 边 | 2,8,6,3 | 1,3,5,4 | 2,4+,1 | 3+,5,2 | 4,6,8,2 | 5,7+,1 | 6+,8+ | 7+,1,5 |
算法 8.2 展示了边交叉涉及的步骤。
算法 8.2 边交叉(EC)
Input: two parent P1 and P2
Output: offspring C
1\. Construct an edge table.
2\. Start by selecting an arbitrary element from one of the parents as the starting point for the offspring.
3\. For the current element in the offspring, compare its edges.
4\. If an edge is common in both parents, choose that as the next element in the offspring.
5\. If there is no common edge or the common edge is already in the offspring, choose the next element from the parent with the shortest edge list.
6\. Repeat until the offspring is complete.
7\. Return C
图 8.7 展示了 8 城市 TSP 的这些步骤。此图说明了如何构建每个城市的边表或邻接表。图中展示了计算边的流程。例如,城市 3 和 5 是第一个父节点中与城市 4 相邻的城市或边。在第二个父节点中,城市 2 和 3 是城市 4 的边。这意味着城市 3 是一个公共边。
创建子节点首先随机选择城市 1 或作为家乡城市。在表格的第二行中,我们列出城市 1 的相邻城市,即 2、8、6 和 3。请注意,城市是循环的,这意味着在第一个父节点中,城市 1 与城市 8 相邻,在第二个父节点中,城市 1 与城市 3 相邻。丢弃已经访问过的城市 1,这些城市对城市 2 有{3,5,4}的相邻城市,对城市 8 有{7,5}的相邻城市,对城市 6 有{5,7}的相邻城市,对城市 3 有{2,4}的相邻城市。我们丢弃城市 2,因为它有三个相邻城市,并从 8、6 和 3 中任意选择城市 3,因为它们有相同数量的边。我们继续按照算法 8.2 添加城市到子节点,直到所有城市都添加完毕。

图 8.7 边交叉
列表 8.2 的完整版本可在本书的 GitHub 仓库中找到。它展示了使用 TSP 示例的 Python 实现边交叉。
顺序 1 交叉
顺序 1 交叉(OX1)通过结合两个父染色体中的遗传信息来创建后代,同时保持结果解中元素的相对顺序。算法 8.3 展示了顺序 1 交叉的步骤。
算法 8.3 顺序 1 交叉(OX1)
Input: two parents P1 and P2
Output: offspring C1 and C2
1\. Choose two random crossover points within the chromosomes and copy the segment between the crossover points from P1 to C1 and from P2 to C2.
2\. Starting from the second crossover point, go through the remaining elements in P2.
3\. If an element is not already present in C1, append it to the end in the same order as it appears in P2.
4\. Wrap around P2 and continue appending the elements until C1 is complete.
Repeat steps 2-4 for C2, using P1 as the reference.
5\. Return C1 and C2
图 8.8 说明了 8 个城市 TSP 的这些步骤。从第二个交叉点开始,城市 4 和 3 不能添加,因为它们已经在 C1 中。P2 中的下一个元素是城市 1,因此它在第二个交叉点之后添加到 C1 中,然后是城市 7,因为城市 6 已经包含在内。接下来添加城市 8,然后是城市 2,因为城市 5 已经在 C1 中。

图 8.8 顺序 1 交叉(OX1)——圆圈中的数字显示了从父代 2 到子代 1 添加元素的顺序。
列表 8.2 的完整版本可在本书的 GitHub 仓库中找到,它展示了使用 TSP 示例的 OX1 的 Python 实现。
循环交叉
循环交叉(CX)通过将元素划分为周期来实现,其中周期是一组元素,当两个父代染色体对齐时,这些元素成对一致地出现。给定两个父代,通过从第一个父代中选择一个元素,找到其在第二个父代中的对应位置,然后重复这个过程,直到回到起始元素,形成一个周期。CX 算子有效地结合了两个父代的遗传信息,同时保持结果后代中元素之间的顺序和相邻关系,并保持后代解的可行性和多样性。算法 8.4 展示了这种交叉方法的步骤。
算法 8.4 循环交叉(CX)
Input: two parents P1 and P2
Output: offspring C1 and C2
1\. Identify cycles between the two parents. A cycle of elements from a parent P1 is created following these steps:
a) Begin with the starting element of P1.
b) Look at the element at the corresponding position in P2.
c) Move to the position with the same element in P1.
d) Include this element in the cycle.
e) Iterate through steps b to d until you reach the starting element of P1.
2\. Create offspring by placing the elements of the identified cycles, preserving their positions from the corresponding parents.
3\. Fill in the remaining positions in C1 with elements from P2 and the remaining positions of C2 with elements from P1 that were not included in the identified cycles. Maintain the order of the elements as they appear in the parents.
4\. Return C1 and C2.
图 8.9 说明了 10 个城市 TSP 的这些步骤。

图 8.9 循环交叉(CX)
在本书的 GitHub 仓库中完整版列表 8.2 中包含了一个使用 TSP 示例的 CX 的 Python 实现。需要注意的是,交叉算子的性能通常依赖于问题本身,也可能受到遗传算法特定参数设置的影响,例如种群大小、突变率和选择压力。因此,建议您尝试不同的交叉算子,并微调遗传算法的参数以适应要解决的问题。
8.3.2 突变方法
插入、交换、倒置和打乱是基于排列的 GA 中常用的突变方法。这些方法旨在在保持解的可行性的同时引入小的扰动:
-
插入突变——随机选择两个基因值,并将第二个移动到第一个之后,以适应它们而移动其余部分。这种方法主要保持基因的顺序和相邻信息。
-
交换突变——随机选择两个基因,并交换它们的位置。这种方法主要保留相邻信息,同时会对原始顺序造成一些破坏。
-
倒置突变——随机选择两个基因,并倒置它们之间的子串。这种方法在很大程度上保持相邻信息,但对顺序信息有破坏性。
-
洗牌变异——随机选择两个基因值,并在所选位置之间非连续地重新排列基因,应用随机顺序。
图 8.10 展示了这些方法在 8 城市旅行商问题(TSP)的第一个父代作为选定的个体中的应用。

图 8.10 基于排列的遗传算法中的变异方法
作为对列表 8.2 的延续,以下代码片段展示了如何在 Python 中实现反转变异:
def inversion_mutation(individual, mutation_rate):
n = len(individual)
mutated_individual = individual.copy()
if random.random() < mutation_rate:
i, j = sorted(random.sample(range(n), 2))
mutated_individual[i:j+1] = reversed(mutated_individual[i:j+1])
return mutated_individual
运行此代码将产生如图 8.11 所示的输出。

图 8.11 反转变异结果
列表 8.2 的完整版本,可在本书的 GitHub 仓库中找到,包括在基于排列的遗传算法中常用的不同交叉和变异方法的实现。
8.4 多目标优化
如前文 1.3.2 节所述,具有多个目标函数的优化问题被称为多目标优化问题(MOPs)。这些问题可以通过基于偏好的多目标优化程序或使用帕累托优化方法来处理。在前一种方法中,使用相对偏好向量或加权方案将多个目标组合成一个或整体目标函数,以标量化多个目标。然而,找到这个偏好向量或权重是主观的,有时并不直接。
帕累托优化,以意大利经济学家和社会学家维弗雷多·帕累托(1848-1923)的名字命名,依赖于寻找多个权衡最优解,并使用高级信息选择其中一个。此过程试图通过减少备选方案的数量到一个最优的非支配解集,称为帕累托前沿(或帕累托边界),在多目标空间中用于做出战略决策。在存在冲突目标函数的情况下,如果一个解没有其他解能在不降低另一个目标的情况下提高一个目标,则该解是帕累托最优的。因此,对于多目标优化问题(MOPs)的最优解不是一个单一解,如单目标或单一优化问题(SOPs)那样,而是一组定义为帕累托最优解的解。这些帕累托最优解也被称为可接受、有效、非支配或非劣解。在帕累托优化中,非支配解代表了在多个冲突目标中没有被其他任何解超越的最佳折衷方案。
在第一章中,我们研究了电动汽车的例子:加速时间和续航里程是相互冲突的目标函数,因为我们需要最小化车辆的加速时间并最大化其续航里程。没有一种车辆能够同时实现这两个目标,如图 8.12 所示,该图基于从 Inside EVs 网站获取的真实数据(insideevs.com/)。例如,Lucid Air Dream Edition 具有最长的续航里程,但不是最低的加速时间。虚线表示帕累托前沿——这些车辆在加速时间和续航里程之间实现了最佳权衡。

图 8.12 19 种电动汽车的加速时间与续航里程对比,截至 2021 年 9 月
多目标优化算法
解决多目标优化问题的算法有很多。非支配排序遗传算法(NSGA-II)是最常用的之一。其他算法包括但不限于强度 Pareto 进化算法 2(SPEA2)、Pareto 存档进化策略(PAES)、带生态位 Pareto 遗传算法(NPGA)、基于支配超体积的多目标选择(SMS-EMOA)和基于分解的多目标进化算法(MOEA/D)。
这些算法各有优缺点,你选择的算法将取决于要解决的问题的具体情况和你的偏好。NSGA-II 有几个优点,如多样性维护、非支配排序和快速收敛。有关多目标优化的更多详细信息,请参阅 Deb 的《使用进化算法进行多目标优化》[3] 和 Zitzler 的《多目标优化的进化算法》[4]。
让我们通过一个例子使用 NSGA-II 解决一个多目标优化问题。假设一个制造商生产两种产品,P1 和 P2,涉及两种不同的机器,M1 和 M2。每台机器一次只能生产一种产品,每种产品在每个机器上的生产时间和成本都不同:
-
P1 在 M1 上需要 2 小时,在 M2 上需要 3 小时,生产成本分别为 100 美元和 150 美元。
-
P2 在 M1 上需要 4 小时,在 M2 上需要 1 小时,生产成本分别为 200 美元和 50 美元。
在每个班次中,两台机器 M1 和 M2 的生产能力分别为生产 100 单位的 P1 和 500 单位的 P2。制造商希望生产至少 80 单位的 P1 和 300 单位的 P2,同时最小化生产成本并最小化两台机器的生产时间差异。
我们将令 x[1] 和 x[2] 分别表示在 M1 和 M2 上生产的 P1 的单位数量,y[1] 和 y[2] 分别表示在 M1 和 M2 上生产的 P2 的单位数量。问题可以表述如下:

受限于:

第一个目标函数(f[1])表示总生产成本,第二个目标函数(f[2])表示两个机器的生产时间差异。列表 8.3 显示了使用 NSGA-II 找到在一个班次中应生产的单位最优数量的代码。
我们将首先从ElementwiseProblem继承,这允许我们以逐元素的方式定义优化问题。n_var指定变量的数量(本例中为 4),n_obj定义目标函数的数量(2),而n_ieq_constr表示不等式约束的数量(2)。xl和xu参数分别定义每个变量的下界和上界。_evaluate方法接受一个输入x(一个解候选)并计算目标值f1和f2,以及不等式约束g1和g2。第三个约束是边界约束,由决策变量的下界和上界表示。
列表 8.3 使用 NSGA-II 解决制造问题
import numpy as np
import matplotlib.pyplot as plt
from pymoo.core.problem import ElementwiseProblem ①
class ManufacturingProblem(ElementwiseProblem):
def __init__(self):
super().__init__(n_var=4,
n_obj=2,
n_ieq_constr=2,
xl=np.array([0,0,0,0]),
xu=np.array([100,100,500,500])) ②
def _evaluate(self, x, out, *args, **kwargs):
f1 = 100*x[0] + 150*x[1] + 200*x[2] + 50*x[3] ③
f2 = np.abs((2*x[0] + 4*x[2]) - (3*x[1] + x[3])) ④
g1 = -x[0] - x[1] + 80 ⑤
g2 = -x[2] - x[3] + 300 ⑤
out["F"] = [f1, f2]
out["G"] = [g1, g2]
problem = ManufacturingProblem()
① 导入问题类的一个实例。
② 定义变量数量、目标函数、约束以及下界和上界。
③ 总生产成本作为第一个目标函数
④ 两个机器的生产时间差异作为第二个目标函数
⑤ 定义约束。
我们现在可以设置一个 NSGA-II 算法的实例作为求解器:
from pymoo.algorithms.moo.nsga2 import NSGA2 ①
from pymoo.operators.crossover.sbx import SBX ②
from pymoo.operators.mutation.pm import PM ③
from pymoo.operators.sampling.rnd import FloatRandomSampling ④
algorithm = NSGA2(
pop_size=40,
n_offsprings=10,
sampling=FloatRandomSampling(),
crossover=SBX(prob=0.9, eta=15),
mutation=PM(eta=20),
eliminate_duplicates=True
) ⑤
① 导入 NSGA-II 类。
② 将模拟二进制交叉(SBX)作为交叉算子导入。
③ 将多项式变异(PM)作为变异算子导入。
④ 导入 FloatRandomSampling 方法以生成指定范围内的每个变量的随机浮点值。
⑤ 设置 NSGA-II 的一个实例。
求解器具有 40 个个体的人口大小(pop_size),使用FloatRandomSampling生成 10 个后代,采用概率为 0.9 的 SBX 交叉,并使用eta参数为 15 的指数分布进行微调。使用eta参数为 20 的 PM 变异。此eta参数控制变异分布的扩散。eliminate_duplicates设置为True,以便在每一代中从种群中删除重复的候选解。
我们通过指定代数数来定义终止标准:
from pymoo.termination import get_termination
termination = get_termination("n_gen", 40)
我们现在可以运行求解器以同时最小化两个目标函数:
from pymoo.optimize import minimize
res = minimize(problem,
algorithm,
termination,
seed=1,
save_history=True,
verbose=True)
X = res.X
F = res.F
最后,我们按照以下方式打印出最佳 10 个解决方案:
print("Solutions found: ")
print("Number of units of product P1 produced on machines M1 and M2\n and
➥ Number of units of product P2 produced on machines M1 and M2 are:\n",
➥ np.asarray(X, dtype = 'int'))
np.set_printoptions(suppress=True, precision=3)
print("The total production costs and \n difference in production times
➥ between the two machines are:\n",F)
此代码将生成表示 NSGA-II 获得的最佳 10 个非支配解的输出,其外观如下:
Solutions found:
Number of units of product P1 produced on machines M1 and M2
and Number of units of product P2 produced on machines M1 and M2 are:
[[ 90 18 39 300]
[ 91 19 39 297]
[ 91 16 12 300]
[ 90 12 30 310]
[ 90 14 21 300]
[ 90 14 47 328]
[ 34 48 1 305]
[ 87 13 3 299]
[ 91 11 7 297]
[ 30 51 0 300]]
The total production costs and
difference in production times between the two machines are:
[[34757.953 16.105]
[34935.538 13.813]
[29235.912 114.763]
[32498.687 43.463]
[30481.316 79.233]
[37228.051 0.652]
[26307.998 378.004]
[26388.316 150.968]
[27199.394 118.385]
[25980.561 392.176]]
由于这两个目标函数没有通用的最佳解决方案,因此可以使用多标准决策来选择最佳权衡——帕累托最优。在 pymoo 中,决策过程首先通过定义称为理想点和下界点的边界点来开始:
-
理想点——这指的是在整个问题可行区域内可以实现的每个目标函数的最佳可能值。这一点代表了所有目标函数同时最小化的场景。
-
谷点——这是每个目标函数在满足所有问题约束条件的同时达到最大化的点。它是理想点的对立面,代表了整个问题可行区域内每个目标函数的最坏可能值。
这些点在多目标优化问题中用于标准化目标函数,并将它们转换为共同尺度,从而允许对不同的解决方案进行公平的比较。这两个点的计算如下:
approx_ideal = F.min(axis=0)
approx_nadir = F.max(axis=0)
nF = (F - approx_ideal) / (approx_nadir - approx_ideal)
然后,我们根据开发者对每个目标函数重要性的看法,定义了分解函数所需的权重:
weights = np.array([0.2, 0.8]) ①
① f1 和 f2 的权重
使用增强标量化函数(ASF),如 Wierzbicki 在“多目标优化中参考目标的使用”[5]中讨论的,定义了一种分解方法:
from pymoo.decomposition.asf import ASF
decomp = ASF()
为了找到最佳解决方案,我们选择从所有解决方案中计算出的最小 ASF 值,并使用 ASF 所需的权重的倒数:
i = decomp.do(nF, 1/weights).argmin()
print("Best regarding ASF: Point \ni = %s\nF = %s" % (i, F[i]))
plt.figure(figsize=(7, 5))
plt.scatter(F[:, 0], F[:, 1], s=30, facecolors='none', edgecolors='blue')
plt.scatter(F[i, 0], F[i, 1], marker="x", color="red", s=200)
plt.title("Objective Space")
plt.xlabel("Total production costs")
plt.ylabel("Difference in production times")
plt.show()
输出如图 8.13 所示。

图 8.13 制造问题解决方案——标记为 X 的点代表选定的帕累托最优或最佳权衡点。
运行代码产生以下输出:
The best solution found:
Number of units of product P1 produced on machines M1 and M2 are 90 and 12 respectively
Number of units of product P2 produced on machines M1 and M2 are 30 and 310 respectively
The total production costs are 32498.69
The difference in production times between the two machines is 43
列表 8.3 的完整版本可在本书的 GitHub 仓库中找到。它包括另一种方法,使用伪权重在多目标优化的上下文中从解决方案集中选择一个解决方案。
8.5 自适应遗传算法(GA)
自适应方法帮助遗传算法在探索和利用之间取得平衡,使用不同的参数,如初始化种群大小、交叉算子和变异算子。这些参数可以根据搜索进度确定性地或动态地调整,使算法收敛到复杂优化问题的优质解决方案。
例如,种群大小可以是自适应的。较大的种群大小促进多样性和探索,而较小的种群大小允许更快地收敛。如果算法难以找到更好的解决方案,则可以增加种群大小;如果种群变得过于多样化,则可以减少种群大小。
突变算子参数可以用来适应遗传算法并平衡其探索和利用方面。例如,在高斯突变的情况下,我们可以在运行过程中自适应地设置高斯分布的标准差σ的值。高斯分布的标准差可以按照确定性方法、自适应方法或自适应性方法来改变。如果您使用确定性方法,σ的值可以在每一代使用此公式计算:σ(i) = 1 – 0.9 * i/ N,其中i是代数,范围从 0 到N(最大代数)。在这种情况下,σ的值在优化过程开始时为 1,并逐渐减少到 0.1,以将搜索算法的行为从探索转变为利用。
自适应方法将搜索过程中的反馈纳入其中,以调整方差并提高搜索性能。Rechenberg 的1/5 成功规则是一种众所周知的方法,通过监控搜索的成功率来调整搜索的步长。这个规则涉及在一定的百分比的前一次突变成功找到更好的解决方案(即,在五次尝试中有多于一次的成功突变)时增加方差,以促进探索,从而避免陷入局部最优。否则,如果成功率较低,方差应减少以促进利用。这允许搜索根据其进度微调其参数,从而实现更好的性能和更快地收敛到最优解。
图 8.14 展示了应用 Rechenberg 的 1/5 成功规则的步骤。这个更新规则在每一代都应用,并使用一个常数 0.82 <= c <= 1 来更新高斯分布的标准差。正如您所看到的,标准差越高,x的值就越高,与当前解的偏差也越大(更多的探索),反之亦然。

图 8.14 Rechenberg 的 1/5 成功规则。遵循此规则,高斯分布的标准差通过一个常数进行更新。标准差越高,x的值(即更大的步长)就越高,与当前解的偏差也越大(更多的探索),反之亦然。
自适应方法将突变步长纳入每个个体——这是一种最初在进化策略(ES)中使用的技巧。在这个方法中,σ(标准差或突变步长)的值与个体一起进化,导致种群中每个个体的突变步长各不相同。以下方程用于这种自适应方法:
|

| 8.2 |
|---|
|

| 8.3 |
|---|
其中 τ[o] 是学习率。
现在您已经对 GAs 的各个组成部分有了扎实的理解,我们可以将这种强大的优化技术应用于现实世界的问题。在接下来的几节中,我们将使用 GAs 来解决三个不同的问题:旅行商问题、PID 控制器参数调整以及政治区划问题。
8.6 解决旅行商问题
让我们考虑以下美国前 20 大城市的旅行商问题(TSP),从纽约市开始,如图 8.15 所示。

图 8.15 美国前 20 大城市 TSP
在列表 8.4 中,我们首先导入我们将使用的库并定义 TSP。首先,我们定义城市名称及其纬度和经度。然后,我们使用这些坐标创建一个哈夫曼距离矩阵,然后将数据字典转换为 dataframe。
列表 8.4 使用 GA 解决 TSP
import numpy as np
import pandas as pd
import networkx as nx
from collections import defaultdict
from haversine import haversine
import matplotlib.pyplot as plt
from pymoo.core.problem import ElementwiseProblem
from pymoo.core.repair import Repair
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.optimize import minimize
from pymoo.operators.sampling.rnd import PermutationRandomSampling
from pymoo.operators.crossover.ox import OrderCrossover
from pymoo.operators.mutation.inversion import InversionMutation
from pymoo.termination.default import DefaultSingleObjectiveTermination
from pymoo.optimize import minimize
cities = {
'New York City': (40.72, -74.00),
'Philadelphia': (39.95, -75.17),
'Baltimore': (39.28, -76.62),
'Charlotte': (35.23, -80.85),
'Memphis': (35.12, -89.97),
'Jacksonville': (30.32, -81.70),
'Houston': (29.77, -95.38),
'Austin': (30.27, -97.77),
'San Antonio': (29.53, -98.47),
'Fort Worth': (32.75, -97.33),
'Dallas': (32.78, -96.80),
'San Diego': (32.78, -117.15),
'Los Angeles': (34.05, -118.25),
'San Jose': (37.30, -121.87),
'San Francisco': (37.78, -122.42),
'Indianapolis': (39.78, -86.15),
'Phoenix': (33.45, -112.07),
'Columbus': (39.98, -82.98),
'Chicago': (41.88, -87.63),
'Detroit': (42.33, -83.05)
} ①
distance_matrix = defaultdict(dict) ②
for ka, va in cities.items():
for kb, vb in cities.items():
distance_matrix[ka][kb] = 0.0 if kb == ka else haversine((va[0], va[1]),
➥ (vb[0], vb[1])) ②
distances = pd.DataFrame(distance_matrix) ③
city_names=list(distances.columns) ③
distances=distances.values ③
G=nx.Graph() ④
for ka, va in cities.items(): ④
for kb, vb in cities.items(): ④
G.add_weighted_edges_from({(ka,kb, distance_matrix[ka][kb])}) ④
G.remove_edges_from(nx.selfloop_edges(G)) ④
① 定义 20 个主要美国城市的城市名称、纬度和经度。
② 基于经纬度坐标创建哈夫曼距离矩阵。
③ 将距离字典转换为 dataframe。
④ 创建一个 networkx 图。
然后,我们可以创建TravelingSalesman作为 pymoo 中可用的ElementwiseProblem类的子类。这个类定义了城市数量和城市间距离作为问题参数,并将总路径长度作为要最小化的目标函数进行评估:
class TravelingSalesman(ElementwiseProblem):
def __init__(self, cities, distances, **kwargs):
self.cities = cities
n_cities = len(cities)
self.distances = distances
super().__init__(
n_var=n_cities,
n_obj=1,
xl=0,
xu=n_cities,
vtype=int,
**kwargs
)
def _evaluate(self, x, out, *args, **kwargs):
f = 0
for i in range(len(x) - 1):
f += distances[x[i], x[i + 1]]
f += distances[x[-1], x[0]]
out["F"] = f
以下函数是Repair类的子类,它提供了一个用于修复 TSP 解决方案的方法,确保每个解决方案都以索引为 0 的城市(在本例中为纽约市)开始。pymoo 中的修复操作员用于确保算法只搜索可行空间。它是在后代繁殖之后应用的:
class StartFromZeroRepair(Repair):
def _do(self, problem, X, **kwargs):
I = np.where(X == 0)[1]
for k in range(len(X)):
i = I[k]
X[k] = np.concatenate([X[k, i:], X[k, :i]])
return X
现在是定义 GA 求解器并将其应用于解决问题的时刻。
problem = TravelingSalesman(cities,distance_matrix) ①
algorithm = GA(
pop_size=20,
sampling=PermutationRandomSampling(),
mutation=InversionMutation(),
crossover=OrderCrossover(),
repair=StartFromZeroRepair(),
eliminate_duplicates=True
) ②
termination = DefaultSingleObjectiveTermination(period=300, n_max_gen=np.inf) ③
res = minimize(
problem,
algorithm,
termination,
seed=1,
verbose=False
) ④
① 为给定城市和城市间距离创建 TSP 实例。
② 定义 GA 求解器。
③ 如果算法在最后 300 代中没有改进,则终止(并禁用最大代数)。
④ 找到最短路径。
我们可以按如下方式打印找到的路线及其长度:
Order = res.X
Route = [city_names[i] for i in Order]
arrow_route = ' → '.join(Route)
print("Route:", arrow_route)
print("Route length:", np.round(res.F[0], 3))
print("Function Evaluations:", res.algorithm.evaluator.n_eval)
这将产生以下输出:
Route: New York City → Detroit → Columbus → Indianapolis → Chicago → San Francisco → San Jose → Los Angeles → San Diego → Phoenix → San Antonio → Austin → Houston → Fort Worth → Dallas → Memphis → Jacksonville → Charlotte → Baltimore → Philadelphia
Route length: 10934.796
Function Evaluations: 6020
以下代码用于使用 NetworkX 可视化获得的路线:
fig, ax = plt.subplots(figsize=(15,10))
H = G.copy() ①
reversed_dict = {key: value[::-1] for key, value in cities.items()} ②
keys_list = list(cities.keys()) ③
included_cities = {keys_list[index]: cities[keys_list[index]] for index in ④
➥ list(res.X)} ④
included_cities_keys=list(included_cities.keys()) ④
edge_list =list(nx.utils.pairwise(included_cities_keys)) ⑤
nx.draw_networkx_edges(H, pos=reversed_dict, edge_color="gray", width=0.5) ⑥
ax=nx.draw_networkx(
H,
pos=reversed_dict,
with_labels=True,
edgelist=edge_list,
edge_color="red",
node_size=200,
width=3,
) ⑦
plt.show() ⑦
① 创建问题图和属性的独立浅拷贝。
② 反转纬度和经度以正确可视化。
③ 创建原始字典的键列表。
④ 创建一个具有所需顺序键的新字典。
⑤ 创建边列表。
⑥ 仅在每个节点上绘制最近的边。
⑦ 绘制并显示路线。
图 8.16 显示了该旅行商问题的获得路线。

图 8.16 美国前 20 大城市 TSP 解决方案
您可以通过更改问题数据和遗传算法参数(如种群大小、采样、交叉和变异方法)在书的 GitHub 仓库中实验完整的代码。
8.7 PID 调整问题
你是否曾想过你的房间是如何保持舒适温度的?你是否想过加热或冷却系统是如何知道何时自动开启和关闭以维持恒温器上设定的温度的?这就是控制系统发挥作用的地方。控制系统就像幕后的魔术师,确保事物平稳高效地运行。它们是一套规则和机制,引导设备或过程实现特定目标。
一种控制系统类型是闭环系统。想象一下:你将房间的恒温器设定在舒适的 22°C(72°F),加热或冷却系统启动以达到该温度。但如果变得太冷或太热怎么办?这时闭环系统开始采取行动。它持续跟踪房间当前的温度,将其与期望温度进行比较,并做出必要的加热或冷却调整。
比例积分微分(PID)控制器是控制系统工程中最常用的算法。该控制器旨在补偿测量状态(例如,当前室温)与期望状态(例如,期望的温度值)之间的任何误差。让我们以使用 PID 控制器进行房间温度控制为例。
如图 8.17 所示,控制器接收误差信号 e(t)(期望状态与反馈信号之间的差异)并产生适当的控制信号 u(t)以打开或关闭加热器,以最小化当前室温与期望值之间的差异。控制信号的计算使用方程 8.4:
|

| 8.4 |
|---|
如此方程所示,比例项 K[p]e(t)倾向于产生一个与误差成比例的控制信号,旨在纠正它。积分项(方程右侧的第二项)倾向于产生一个与误差的大小及其持续时间成比例的控制信号,或者误差曲线下的面积。微分项(方程右侧的第三项)倾向于产生一个与误差变化率成比例的控制信号,从而提供一个预测控制信号。

图 8.17 基于 PID 的闭环控制系统——PID 控制器接收误差信号并产生控制信号以将误差减少到零。
利用 PID 控制器可以使系统(例如,空调或加热器)遵循指定的输入并达到期望或最优的稳态误差、上升时间、调整时间和超调:
-
上升时间—上升时间是响应从最终值的 10%上升到 90%所需的时间。
-
峰值超调—峰值超调(也称为最大超调)是响应在峰值时刻与最终值之间的偏差。
-
稳定时间—稳定时间是响应达到稳态并保持在指定的容差带内(例如,最终值的 2-5%)所需的时间,在瞬态响应稳定后。
-
稳态误差—稳态误差是在系统达到稳定状态时,系统输出期望值与实际值之间的差异。
如图 8.18 所示,当当前室温低于设定点或期望值时,加热器开启(即,通电)。当温度高于设定点时,加热器关闭(即,断电)。

图 8.18 系统的阶跃响应。加热器根据实际温度与期望值之间的差异开启或关闭。
表 8.6 显示了 PID 控制器参数对系统时间响应的影响。请注意,这些相关性可能并不完全准确,因为 K[p],K[i] 和 K[d] 之间相互依赖。事实上,改变其中一个变量可能会改变其他两个变量的影响。因此,在确定 K[p],K[i] 和 K[d] 的值时,应仅将此表作为参考。
表 8.6 添加 PID 控制器参数对系统响应的影响
| 参数 | 上升时间 | 超调 | 稳定时间 | 稳态误差 |
|---|---|---|---|---|
| K[p] | 减少 | 增加 | 小变化 | 减少 |
| K[i] | 减少 | 增加 | 增加 | 显著减少 |
| K[d] | 小变化 | 减少 | 减少 | 小变化 |
寻找 PID 控制器参数的最佳值以实现最佳控制器响应是一个多变量优化问题,通常被称为PID 调整问题。以下四个性能指标通常用于评估控制系统的质量,如 PID 控制器:
-
ITAE (积分时间绝对误差)—这个指标惩罚随时间持续存在的错误,使其适用于瞬态响应和稳定时间都很重要的系统。它是通过以下公式计算的:ITAE = ∫(t|e(t)|) dt,其中 t 是时间,e(t) 是时间 t 的误差,定义为 e(t) = r(t) – y(t),r(t) 是时间 t 的参考信号(期望输出)(对于阶跃响应 r(t) = 1),y(t) 是时间 t 时系统的实际输出。
-
ITSE (积分时间平方误差)—与 ITAE 类似,这个指标也惩罚持续时间较长的错误,但由于平方项,它更强调较大的错误。它是通过以下公式计算的:ITSE = ∫(te(t)²) dt。
-
IAE (积分绝对误差)—这个指标衡量了误差的整体幅度,不考虑误差的持续时间。这是一个简单且广泛使用的性能指标,它使用以下公式计算:IAE = ∫|e(t)| dt。
-
ISE (积分平方误差)—这个指标强调较大的误差,由于平方项的存在,使其在需要优先最小化大误差的系统中非常有用。它使用以下公式进行计算:ISE = ∫e(t)² dt。如果误差发生在响应演化的后期,它会对误差进行更重的惩罚。它对时间 dt 内的误差 E 的惩罚比对 E/α(其中 α > 1)的惩罚更重。这种预期的响应可能具有较慢的上升时间,但具有更振荡的行为。
-
综合标准—这个指标结合了超调、上升时间、稳定时间和稳态误差 [6]。它使用以下公式进行计算:W = (1 – e–*β)(M[p]* + error[ss]) + e–*β* (t[s] – t[r]),其中 M[p] 是超调,error[ss] 是稳态误差,t[s] 是稳定时间,t[r] 是上升时间,β 是介于 0.8 到 1.5 之间的平衡因子。你可以将 β 设置得大于 0.7 以减少超调和稳态误差。另一方面,你可以将 β 设置得小于 0.7 以减少上升时间和稳定时间。
这些指标中的每一个都以不同的方式量化了期望输出和系统实际输出之间的误差,强调了控制系统性能的不同方面。请注意,性能指标并不严格局限于上述指标。工程师有灵活性,可以设计定制的性能指标,以适应所考虑的控制系统的特定目标和特征。
图 8.19 展示了一个闭环控制系统,其中传递函数用于描述系统在拉普拉斯域中输入和输出之间的关系。这个域是频率域的推广,提供了一个更全面的表示,包括瞬态行为和初始条件。假设 T[sp] 是设定点或期望输出,G 代表框图中指示的传递函数。

图 8.19 闭环控制系统
所有变量都是 s 的函数,s 是拉普拉斯变换的输出变量。PID 控制器的传递函数由以下方程给出:
|

| 8.5 |
|---|
其中 K[p] 是比例增益,K[i] 是积分增益,K[d] 是微分增益。假设 HVAC 系统的传递函数由以下方程给出:
|

| 8.6 |
|---|
假设 G[s] = 1(单位反馈)并使用框图简化,我们可以找到闭环系统的整体传递函数 T(s):
|

| 8.7 |
|---|
现在我们来看看如何使用 Python 找到 PID 参数的最佳值。在下一个列表中,我们首先导入我们将使用的库并定义控制系统的整体传递函数。
列表 8.5 使用 GA 解决 PID 调节问题
import numpy as np
import control ①
import math
import matplotlib.pyplot as plt
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.crossover.pntx import PointCrossover
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
def transfer_function(Kp,Ki,Kd): ②
num = np.array([Kd,Kp,Ki]) ③
den = np.array([1,(Kd+10),(Kp+20),Ki]) ④
T = control.tf(num, den) ⑤
t, y = control.step_response(T) ⑥
return T, t, y
#A Import the control module.
① 导入控制模块。
② 将 PID 参数作为输入。
③ 定义传递函数的分子。
④ 定义传递函数的分母。
⑤ 创建传递函数。
⑥ 使用阶跃函数作为系统输入来获取时间响应输出。
接下来,我们可以定义目标函数或性能标准:
def objective_function(t, error, Kp,Ki,Kd, criterion):
if criterion == 1:
ITAE = np.trapz(t, t*error) ①
objfnc= ITAE
elif criterion == 2:
ITSE = np.trapz(t, t*error**2) ②
objfnc= ITSE
elif criterion == 3:
IAE = np.trapz(t, error) ③
objfnc= IAE
elif criterion == 4:
ISE = np.trapz(t, error**2) ④
objfnc= ISE
elif criterion == 5:
T, _, _ =transfer_function(Kp,Ki,Kd)
info = control.step_info(T)
beta = 1
Mp = info['Overshoot']
tr = info['RiseTime']
ts = info['SettlingTime']
ess = abs(1-info['SteadyStateValue'])
W = ((1-math.exp(-beta))*(Mp+ess))+((math.exp(-beta))*(ts-tr)) ⑤
objfnc=W;
return objfnc
① ITAE(积分时间绝对误差)
② ITSE(积分时间平方误差)
③ IAE(积分绝对误差)
④ ISE(积分平方误差)
⑤ W(综合标准)
我们现在可以定义 PID 控制器的优化问题:
class PIDProblem(Problem):
def __init__(self):
super().__init__(n_var=3, ①
n_obj=1, ②
n_constr=0, ③
xl=0, ④
xu=100, ④
vtype=float)
def _evaluate(self, X, out, *args, **kwargs): ⑤
f = np.zeros((X.shape[0], 1))
for i, params in enumerate(X):
Kp, Ki, Kd = params
T, t, y =transfer_function(Kp,Ki,Kd)
error = 1 - y
f[i]=objective_function(t, np.abs(error), Kp,Ki,Kd, 5)
out["F"] = f
① 三个决策变量,代表 PID 控制器的 Kp、Ki 和 Kd 增益
② 目标函数的数量
③ 无约束
④ 决策变量的上下限
⑤ 评估目标函数。
接下来,我们可以使用遗传算法(GA)设置和解决 PID 调整问题。之前定义的PIDProblem类用于建模优化问题。GA 求解器配置了 50 个种群大小。初始解通过FloatRandomSampling采样,交叉操作采用两点交叉,概率为 0.8。此外,应用多项式变异,概率为 0.3,算法运行 60 代:
problem = PIDProblem()
algorithm = GA(
pop_size=50,
sampling=FloatRandomSampling(),
crossover=PointCrossover(prob=0.8, n_points=2),
mutation = PolynomialMutation(prob=0.3, repair=RoundingRepair()),
eliminate_duplicates=True
)
res = minimize(problem, algorithm, ('n_gen', 60), seed=1, verbose=True)
现在让我们打印结果:
best_params = res.X
print("Optimal PID controller parameters:")
print("Kp =", best_params[0])
print("Ki =", best_params[1])
print("Kd =", best_params[2])
我们将可视化时间响应:
Kp = best_params[0]
Ki = best_params[1]
Kd = best_params[2]
T, t, y =transfer_function(Kp,Ki,Kd)
plt.plot(t,y)
plt.title("Step Response")
plt.xlabel("Time (s)")
plt.grid()
图 8.20 描述了系统的阶跃响应,展示了当输入迅速从 0 变为 1 时,其输出随时间的变化情况。

图 8.20 阶跃响应
为了展示阶跃响应特性(上升时间、稳定时间、峰值等),你可以使用以下函数:
control.step_info(T)
这将产生以下输出:
{'RiseTime': 0.353,
'SettlingTime': 0.52,
'SettlingMin': 0.92,
'SettlingMax': 1.0,
'Overshoot': 0,
'Undershoot': 0,
'Peak': 0.99,
'PeakTime': 3.62,
'SteadyStateValue': 1.0}
你可以尝试调整算法的参数(如种群大小、交叉方法和概率、变异方法和概率、代数数量等)以及改变性能指标,以观察对系统性能的影响。
8.8 政治选区划分问题
我在第 2.1.5 节介绍了政治选区划分——它可以定义为将一个领土内的n个子区域划分为m个选区的过程,同时遵守某些约束。假设我们需要将多伦多市的n个社区合并为m个选区,同时确保足够的人口平等水平。图 8.21 显示了包含东多伦多 16 个社区的人口和中等家庭收入样本数据集。

图 8.21 东多伦多的 16 个社区及其人口和中等家庭收入
在解决政治选区划分问题时,一个可行的解决方案必须确保每个选区都有令人满意的种群均衡程度(即公平和平衡的分布)。例如,我们可以通过计算一个选区的人口平衡度,即在上限(pop[UB])和下限(pop[LB])内与理想人口规模的偏差来评估一个选区的人口平衡度,如下所示:

其中 pop[av] 代表可以被认为是所有邻里平均值的预期人口规模,而 pop[margin] 表示从理想人口规模可接受的偏差程度。n 是邻里数量,而 m 是区域数量。
如果一个区域的总人口超过上限,则该区域将被视为过度拥挤,反之,如果一个区域的总人口低于下限,则该区域将被视为人口不足。如果一个区域的居民人数在上下限之间,则该区域将被视为具有适当的人口规模。目标函数是最小化过度拥挤和人口不足的区域总数。搜索过程将持续进行,直到目标函数的最小值获得,理想情况下为零。这表明没有区域是过度拥挤或人口不足的。
下一个列表展示了如何使用遗传算法(GA)查找政治区域。我们将从读取本地文件夹中的数据或使用 URL 开始。
列表 8.6 使用遗传算法解决政治区域划分问题
import geopandas as gpd
import pandas as pd
import folium
data_url="https://raw.githubusercontent.com/Optimization-Algorithms-Book/ ①
Code-Listings/main/Appendix%20B/data/PoliticalDistricting/"
toronto = gpd.read_file(data_url+"toronto.geojson") ②
neighborhoods = pd.read_csv(data_url+"Toronto_Neighborhoods.csv") ③
range_limit = 16 ④
toronto_sample = toronto.tail(range_limit) ④
values = neighborhoods.tail(range_limit) ④
values = values.join(toronto_sample["cartodb_id"]) ④
① 数据文件夹的 URL
② 读取多伦多地区行政边界。
③ 读取邻里信息(例如,名称、人口和平均家庭收入)。
④ 选择 16 个邻里作为子集来代表东多伦多的邻里。
在读取数据集后,我们将进行以下数据预处理,以获取每个邻里的居民人数以及每个可能邻里的每对邻里之间的邻接关系,以布尔矩阵的形式。
import numpy as np
def get_population(lst, table):
return table["population"].iloc[lst].to_numpy() ①
eval = get_population(range(range_limit), values) ②
def get_neighboors(database): ③
result = []
for i in range(database['name'].size):
tmp = np.zeros(database['name'].size)
geo1 = database.iloc[i]
for j in range(database['name'].size):
if i != j:
geo2 = database.iloc[j]
if geo1["geometry"].intersects(geo2["geometry"]):
tmp[j] = 1
result.append(tmp)
return np.stack(result)
neighbor = get_neighboors(toronto_sample)
① 获取每个邻里的居民人数。
② 准备人口数据集。
③ 表示每个可能邻里的每对邻里之间的邻接关系。
现在我们将定义一个具有单个目标函数、三个约束条件、给定数量的区域、给定的人口边际和邻里之间的邻接矩阵的政治区域划分类。PoliticalDistricting是一个自定义问题类,它扩展了来自 pymoo 的Problem类。Problem类实现了一个方法,该方法评估一组解决方案而不是一次评估一个解决方案,就像在ElementwiseProblem类的情况下。在PoliticalDistricting类中,以下参数被定义:
-
num_dist—将区域划分为多少个区域 -
neighbor—表示区域中位置之间邻里关系的矩阵 -
populations—每个邻里的居民人数 -
margin—从理想人口规模可接受的偏差程度 -
average—平均人口 -
n_var—决策变量的数量,等于邻里的数量 -
n_obj=1—目标函数的数量,对于这个问题是 1 -
n_eq_constr=3—等式约束的数量,对于这个问题是 3 -
xl=0—决策变量的下限,对于这个问题是 0 -
xu=num_dist-1—决策变量的上限,对于这个问题是num_dist-1 -
vtype=int—决策变量的类型,对于这个问题是整数
以下代码展示了如何定义具有不同参数的PoliticalDistricting类,例如区域数量、邻域信息、人口和边际值:
from pymoo.core.problem import Problem
class PoliticalDistricting(Problem):
def __init__(self,
num_dist,
neighbor,
populations,
margin
): ①
self.populations = populations ②
self.average = np.mean(populations) ③
super().__init__(n_var=len(self.populations), n_obj=1, n_eq_constr=3,
➥ xl=0, xu=num_dist-1, vtype=int) ④
self.n_var = len(self.populations)
self.n_dist = num_dist
self.margin = margin
self.neighbor = neighbor
self.func = self._evaluate ⑤
① 定义一个具有特定参数的构造函数。
② 保存人口数据。
③ 存储所有区域的平均人口。
④ 使用特定参数调用父类的构造函数。
⑤ 对解决方案进行目标函数和约束的评估。
作为延续并作为problem类的一部分,我们将使用以下函数提取属于特定区域的邻域:
def _gather(self, x, district):
return np.where(x==district, 1, 0)
然后,我们将根据给定的人口值和边际值计算上下限,如下所示:
def _get_bounds(self):
ub = np.ceil(self.average + self.margin) *
➥ (len(self.populations)/self.n_dist)
lb = np.ceil(self.average - self.margin) *
➥ (len(self.populations)/self.n_dist)
return ub, lb
以下函数用于判断选举区域是否过度拥挤或人口不足:
def _get_result(self, gathered, ub, lb):
product = gathered * self.populations
summed_product = np.sum(product, axis=1)
return np.where((summed_product > ub), 1, 0) + np.where((summed_product <
➥ lb), 1, 0)
由于所有约束都是等式约束,以下函数在约束满足时返回 true:
def _get_constraint(self, constraint):
constraint = np.stack(constraint)
return np.any(constraint==0, axis=0)
为了确保区域内没有远离其他邻域的孤立邻域,除非该区域只有一个邻域,以下函数被使用:
def _get_neighbor(self, gathered):
singleton = np.sum(gathered, axis=1)
singleton = np.where(singleton==1, True, False)
tmp_neighbor = np.dot(gathered, self.neighbor)
tmp_neighbor = np.where(tmp_neighbor > 0, 1, 0)
product = gathered * tmp_neighbor
return np.all(np.equal(product, gathered), axis=1) + singleton
以下函数确定将选举区域变成连续块的最佳近似:
def cap_district(self, gathered):
result = np.zeros(gathered.shape[0])
for i in range(len(gathered)):
nonzeros = np.nonzero(gathered[i])[0]
if nonzeros.size != 0:
mx = np.max(nonzeros)
mn = np.min(nonzeros)
result[i] = self.neighbor[mx][mn] or (mx == mn)
return result
problem类中的最后一个函数用于评估解决方案与目标函数的对比,包括检查约束:
def _evaluate(self, x, out, *args, **kwargs):
x=np.round(x).astype(int) # Ensure X is binary
pop_count = []
constraint1 = []
constraint2 = []
constraint3 = []
for i in range(self.n_dist):
gathered = self._gather(x, i)
ub, lb = self._get_bounds()
result = self._get_result(gathered, ub, lb)
pop_count.append(result)
constraint1.append(np.sum(gathered, axis=1)) ①
constraint2.append((self._get_neighbor(gathered))) ②
constraint3.append(self.cap_district(gathered)) ③
holder = np.sum(np.stack(pop_count), axis=0)
out["F"] = np.expand_dims(holder, axis=1)
out["H"] = [self._get_constraint(constraint1),
self._get_constraint(constraint2),
self._get_constraint(constraint3)]
def create_districting_problem(number_of_districts, neighborlist, population_
➥ list, margin, seed=1):
np.random.seed(seed)
problem = PoliticalDistricting(number_of_districts, neighborlist,
➥ population_list, margin)
return problem
① 约束 1:确保没有空区域,
② 约束 2:确保区域内没有孤立的邻域,除非该区域只有一个邻域。
③ 约束 3:通过实现最佳可能近似来确保选举区域是连续块。
我们现在可以定义 GA 求解器并将其应用于解决问题,如下所示:
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.operators.crossover.pntx import PointCrossover
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.termination import get_termination
from pymoo.optimize import minimize
num_districts = 3
margin=6000
problem = create_districting_problem(num_districts, neighbor, eval, margin, seed=1)
algorithm = GA(
pop_size=2000,
sampling=FloatRandomSampling(),
crossover=PointCrossover(prob=0.8, n_points=2),
mutation = PolynomialMutation(prob=0.3, repair=RoundingRepair()),
eliminate_duplicates=True
)
termination = get_termination("n_gen", 100)
res = minimize(problem,
algorithm,
termination,
seed=1,
save_history=True,
verbose=True)
列出的结果政治区域在此处,并在图 8.22 中可视化:
Political District- 1 : ['Woburn', 'Highland Creek', 'Malvern']
Political District- 2 : ['Bendale', 'Scarborough Village', 'Guildwood', 'Morningside', 'West Hill', 'Centennial Scarborough', 'Agincourt South-Malvern West']
Political District- 3 : ['Rouge', 'Hillcrest Village', 'Steeles', "L'Amoreaux", 'Milliken', 'Agincourt North']

图 8.22 结合 16 个邻域的三个政治区域
该问题被视为一个单目标优化问题,目标是最小化过度拥挤和人口不足的区域总数。数据集包含每个邻域的中等家庭收入,因此您可以替换目标函数以关注中等家庭收入的不均匀性。您还可以通过考虑这两个标准将问题视为多目标优化问题。
本章标志着本书第三部分的结束,该部分主要关注遗传算法及其在解决复杂优化问题中的应用。本书的第四部分将深入探讨群智能算法的迷人领域。
摘要
-
汉明悬崖问题是由二进制表示的固有性质引起的,它通过破坏搜索空间的平滑性,对二进制编码的遗传算法产生负面影响,导致收敛不良,并导致探索和利用效率低下。为了解决这个问题,可以使用如格雷码或实值编码等替代表示方法,因为它们提供了更好的局部性和更平滑的搜索空间,最小化了小变化对解码值的影响。
-
实值遗传算法(GA)非常适合涉及连续变量或实值参数的优化问题。它提供了诸如更好的表示精度、更快的收敛速度、多样化的交叉和变异操作以及降低复杂度等好处,使其成为许多连续优化问题的理想选择。
-
基于排列的遗传算法是一类专门设计用来处理组合优化问题的遗传算法,其中解决方案可以表示为元素的有序序列或排列。
-
多目标优化问题可以使用基于偏好的多目标优化方法或帕累托优化方法来解决。在基于偏好的方法中,多个目标通过使用加权方案组合成一个或整体目标函数。帕累托优化方法侧重于识别多个权衡最优解,即帕累托最优解。这些解决方案可以通过更高层次的信息或决策过程进一步优化。
-
交叉操作主要是利用性的,因为它结合了两个父个体的遗传物质以产生后代,促进了解决方案之间有益特征的交换。然而,根据实现方式,交叉也可以具有一些探索性,因为它可以产生具有新基因组合的后代,从而发现新的解决方案。
-
变异操作可以根据影响因子,如变异率和变异步长,起到探索或利用的作用。
-
通常,交叉率应该相对较高,因为它促进了父染色体之间遗传信息的交换。另一方面,变异通常以低概率应用,因为其主要目的是向种群中引入随机变异。
第四部分:群体智能算法
随着我们探索优化算法的深入,这部分将带你沉浸到集体智能的世界。在这里,你将发现包括粒子群优化、蚁群优化和人工蜂群在内的群体智能算法的力量。通过本部分的两章,你将见证自然界启发的群体行为如何被利用来找到最佳解决方案。
在第九章中,你将了解群体智能,并深入探讨粒子群优化(PSO)。你将了解粒子群如何集体探索解空间以找到最佳答案。你将探索连续 PSO 算法,深入研究二进制 PSO 用于离散问题,并理解基于排列的 PSO 用于组合优化。你将发现如何调整 PSO 以在探索和利用之间取得平衡,并看到它如何有效地解决连续和离散优化问题。
第十章将在你探索其他群体智能(SI)算法时拓宽你的视野。你将熟悉蚁群优化(ACO)元启发式算法的原则,并了解适应各种问题类型的 ACO 的不同变体。此外,你将深入研究人工蜂群(ABC)算法,并掌握使 SI 算法如此通用的适应性方面。你将见证这些 SI 算法如何应用于解决广泛的连续和离散优化问题。
第九章:粒子群优化
本章涵盖
-
介绍群体智能
-
理解连续粒子群优化算法
-
理解二进制粒子群优化
-
理解基于排列的粒子群优化
-
适应粒子群优化以实现探索和利用之间的更好权衡
-
使用粒子群优化解决连续和离散问题
在第二章中我介绍的寻宝任务中,假设你想要与你的朋友合作并共享信息,而不是独自进行寻宝。然而,你不想采取一种竞争性的方法,在这种方法中,你只保留表现更好的猎人,并招募新的猎人替换表现较差的猎人,就像在前几章中解释的遗传算法(GA)那样。你想要采取一种更合作的方法,保留所有猎人,不进行任何替换,但你希望给予表现更好的猎人更多的权重,并尝试模仿他们的成功。这种场景使用了群体智能,对应于基于群体的优化算法,如粒子群优化(PSO)、蚁群优化(ACO)和人工蜂群(ABC)算法,这些算法将在本书的第四部分中解释。
在本章中,我们将关注 PSO 算法的不同变体,并将它们应用于解决连续和离散优化问题。这些变体包括连续 PSO、二进制 PSO、基于排列的 PSO 和自适应 PSO。本章讨论了函数优化、旅行商问题、神经网络训练、三角测量、咖啡馆规划以及医生排班问题,并在附录 C 中包含了补充练习。下一章将介绍 ACO 和 ABC 算法。
9.1 介绍群体智能
在这个星球上,集体行为充满了令人惊叹的例子。各种物种为了生存相互依赖,常常形成令人惊讶的联盟以实现共同的目标:物种的延续。大多数生物也表现出惊人的利他主义,为了保护和为它们的后代提供最佳护理,这与人类所表现出的任何形式的牺牲相当。它们可以合作完成复杂的任务,如觅食、分配劳动、建造巢穴、孵化排序、保护、放牧、鱼群和鸟群,仅举几例。这些复杂的集体行为是从空间分布的简单实体之间的个体互动中产生的,没有中央控制器或协调者,也没有脚本或访问全局信息。各种合作模式、通信机制和适应策略被采用,以实现这种复杂的集体行为。
群体智能
群体智能是人工智能的一个子领域,它探索了大量的相对简单且空间分布的智能体如何以去中心化和自我组织的方式相互以及与它们的环境互动,以集体实现复杂目标。
已经设计了几种高效的基于群体的算法,通过模仿自然界中观察到的集体行为来利用集体智慧的力量,以解决复杂的优化问题。表 9.1 提供了一个非详尽的群体智能算法及其灵感来源的单细胞和多细胞生物列表。
表 9.1 群体智能算法及其灵感来源的示例
| 生物体 | 类别 | 灵感来源 | 算法 |
|---|---|---|---|
| 单细胞生物 | 细菌 | 细菌集群觅食 | 细菌觅食优化算法 (BFO) 细菌集群算法 (BSA) |
| 多细胞生物 | 鸟/鱼 | 鸟群飞行和鱼群游动 | 粒子群优化 (PSO) |
| 蚂蚁 | 蚂蚁的觅食行为 | 蚂蚁群优化 (ACO) | |
| 蜜蜂 | 蜜蜂的觅食行为 | 人工蜂群 (ABC) | |
| 蝙蝠 | 蝙蝠的回声定位行为 | 蝙蝠算法 (BA) | |
| 萤火虫 | 萤火虫的闪烁行为 | 萤火虫算法 (FA) | |
| 蝴蝶 | 蝴蝶的觅食行为 | 蝴蝶优化算法 (BOA) | |
| 蜻蜓 | 蜻蜓的静态和动态集群行为 | 蜻蜓算法 (DA) | |
| 蜘蛛 | 社会性蜘蛛的协作行为 | 社会性蜘蛛优化 (SSO) | |
| 食藻类动物 | 食藻类动物的群居行为 | 食藻类动物群 (KH) | |
| 青蛙 | 青蛙的觅食合作 | 混洗青蛙跳跃算法 (SFLA) | |
| 鱼 | 鱼的群居行为 | 鱼群搜索 (FSS) | |
| 海豚 | 海豚在检测、追逐和捕食沙丁鱼群的行为 | 海豚伙伴优化 (DPO) 海豚群优化算法 (DSOA) | |
| 猫 | 猫的休息和追踪行为 | 猫群优化 (CSO) | |
| 猴子 | 寻找食物 | 猴子搜索算法 (MSA) | |
| 狮子 | 狮子的独居和合作行为 | 狮子优化算法 (LOA) | |
| 鹰科鸟类 | 鹰科鸟类的繁殖策略 | 鹰科搜索 (CS) 鹰科优化算法 (COA) | |
| 狼 | 灰狼的领导层级和狩猎机制 | 狼搜索算法 (WSA) 灰狼优化器 (GWO) |
例如,细菌,作为单细胞生物,拥有潜在的社会智慧,这使得它们能够合作解决挑战。细菌发展出复杂的通讯能力,如趋化性信号,以合作的方式自我组织成高度结构化的群体,并提高环境适应性。细菌的趋化性是指细菌细胞通过化学吸引剂和排斥剂的浓度梯度迁移的过程。大肠杆菌在觅食过程中就利用了这种细菌趋化性。这种集体行为为优化算法如细菌觅食优化算法(BFO)和细菌集群算法(BSA)提供了基础。
生态学,动物行为的研究,是群体智能算法如粒子群优化(PSO)、蚁群优化(ACO)、人工蜂群(ABC)、蝙蝠算法(BA)、萤火虫算法(FA)和社会蜘蛛优化(SSO)的主要灵感来源。例如,蜜蜂是一种高度合作的社交昆虫,它们合作建造蜂巢,大约有 30,000 只蜜蜂可以生活在其中并共同工作。它们分工明确:有的制作蜂蜡,有的制作蜂蜜,有的制作蜂粮,有的塑造和塑造蜂巢,有的将水带到蜂房并与蜂蜜混合。年轻的蜜蜂从事户外工作,而年老的蜜蜂则从事室内工作。在觅食过程中,蜜蜂群体不是在所有方向上消耗能量搜索,而是使用单个觅食者来降低成本/收益比。此外,群体将觅食努力集中在最有利可图的区域,并忽视那些质量较差的区域。观察到,当群体的食物资源稀缺时,觅食者会招募更多的巢居者到它们找到的食物来源,它们在返回蜂巢时舞蹈模式的改变有助于这种增加的招募。
群智能算法的基本组件通常涉及大量无需中央监督的分布式处理代理。这些代理与邻近代理进行通信,并根据接收到的信息调整其行为。此外,对群智能算法进行的绝大多数研究主要基于对生物体集体行为的实验观察。这些观察结果被转化为模型,然后通过模拟进行测试,以推导出构成群智能算法基础的元启发式算法,如图 9.1 所示。这种实验方法使研究人员能够更深入地了解个体代理之间的复杂相互作用以及它们如何产生集体行为。通过模拟这些相互作用并测试各种场景,研究人员可以改进算法并提高其有效性。例如,您可以通过观看乔治亚理工学院计算机学院进行的“蜜蜂摇摆舞”实验视频来了解蜜蜂如何传达新食物源的位置(www.youtube.com/watch?v=bFDGPgXtK-U)。

图 9.1 群智能算法的推导过程
算法 9.1 展示了群智能算法中的常见步骤。算法首先初始化算法参数,例如群体中个体的数量、最大迭代次数和终止条件。然后从初始候选解群体中采样(不同的采样方法在第 7.1 节中已解释)。算法随后遍历群体中的所有个体,执行以下操作:找到迄今为止的最佳解、找到最佳邻居以及更新个体。
算法 9.1 群智能算法
Initialize parameters
Initialize swarm
While (stopping criteria not met) loop over all individuals
Find best so far
Find best neighbor
Update individual
使用定义的目标/适应度函数对个体及其邻居进行评估。邻域结构和更新机制取决于所使用的算法。这个遍历所有个体的循环会重复进行,直到满足终止条件,这可能是一个最大迭代次数或达到令人满意的适应度水平。在此阶段,算法停止并返回优化过程中找到的最佳解。在接下来的章节中,我们将深入探讨 PSO 算法。
9.2 连续 PSO
粒子群优化(PSO)是一种由 Russell Eberhart 和 James Kennedy 于 1995 年开发的基于群体的随机优化技术。从那时起,PSO 已经变得流行,并被应用于不同领域的各种现实世界应用。该算法受到鸟类、鱼类、蚂蚁、白蚁、黄蜂和蜜蜂等社会生物行为的启发。PSO 模仿这些生物的行为,群体中的每个成员被称为粒子,类似于鸟群中的鸟、鱼群中的鱼或蜂群中的蜜蜂。Eberhart 和 Kennedy 选择使用“粒子”一词来指代优化中的个体或候选解,因为他们认为这更适合描述粒子的速度和加速度。
鸟群行为
鸟群行为是由三个简单规则控制的行为,如下面的图所示:
-
分离—避免靠近附近的鸟,以防止过度拥挤。
-
对齐—调整航向以对应邻近鸟类的平均方向。
-
一致性—向邻近鸟类的平均位置移动。

鸟群行为规则:分离、对齐和一致性
当鸟类——作为在去中心化和自我组织的方式下相互作用的分布式代理,与它们的环境相互作用,但没有访问全局信息——应用这三个简单规则时,结果是鸟群行为的涌现行为。
粒子(候选解)通过跟随当前最佳粒子在可行搜索空间中移动或飞行。因此,PSO 遵循一个简单的原则:模仿邻近个体的成功。群体中的每个粒子以去中心化的方式运作,利用自身的智能和群体的集体智能。因此,如果一个粒子发现了一条通往食物的有利路径,群体中的其他成员可以立即采用相同的路径。
PSO 算法
“这个[PSO]算法在意识形态上属于那个允许智慧自然涌现而不是试图强加智慧、模仿自然而不是试图控制自然、寻求使事物更简单而不是更复杂的哲学学派。” J. Kennedy 和 R. Eberhart,PSO 的发明者 [1]。
图 9.2 展示了 PSO 流程图。我们首先初始化算法参数并创建一个初始粒子群。这些粒子代表候选解。搜索空间中的每个粒子都持有当前位置 x^i 和当前速度 v^i。每个粒子迄今为止达到的最佳位置称为个人最佳或 pbest。粒子在其邻域中达到的最佳位置称为 nbest。如果邻域限制为少数几个粒子,则最佳称为局部最佳,lbest。如果邻域是整个群体,则整个群体达到的最佳称为全局最佳,gbest。我们将在 9.2.3 节中进一步讨论 PSO 的邻域结构。

图 9.2 PSO 算法
在评估每个粒子的适应度后,PSO(粒子群优化)会更新每个粒子的个人最佳位置,如果当前适应度更高,然后根据整个群体中最佳适应度确定全局最佳位置,并使用个人和全局信息的组合来调整粒子的速度和位置。这些步骤通过平衡个体和集体学习,促进搜索空间中的探索和利用,引导群体向最优或近似最优解发展。这个过程会迭代进行,直到满足终止条件。
9.2.1 运动方程
每个粒子的速度 (v) 和位置 (x) 使用以下方程进行更新:
|

| 9.1 |
|---|
|

| 9.2 |
|---|
其中
-
k 是迭代次数。
-
i 和 d 是粒子编号和维度。例如,在只有一个决策变量的单变量优化问题中,维度 = 1;在双变量问题中,维度 = 2 等。
-
ω 是惯性权重。
-
c1, c2 是加速度系数。
-
r1, r2 是介于 0 和 1 之间的随机数,并在每个迭代中为每个维度生成,而不是为每个粒子生成。
-
pbest 是粒子达到的最佳位置。
-
gbest 是整个群体达到的最佳位置。如果您将群体划分为多个邻域,则应将 gbest 替换为 nbest 或 lbest。
如这两个方程所示,我们首先更新速度 v[(][k] [+ 1)]^(id)。然后,通过将当前位置 x[k]^(id) 与新的位移 v[(][k] [+ 1)]^(id) × timestamp 相加,更新位置到 x[(][k] [+ 1)]^(id),其中 timestamp = 1,这代表单次迭代。
为了理解这些运动更新方程,让我们使用二维笛卡尔坐标系中的向量来可视化这些方程,如图 9.3 所示。正如您所看到的,速度更新方程由三个主要组成部分组成,每个部分都贡献于搜索空间中粒子的运动:
-
惯性成分—速度更新方程的第一部分代表了粒子的惯性影响,考虑到粒子(如鱼群中的鱼或鸟群中的鸟)不能突然改变方向。您将在后面看到,这个惯性成分至关重要,因为它使算法更具适应性,并有助于在探索和利用之间保持平衡。
-
认知成分—方程的第二部分被称为认知成分,它代表了粒子对其个人最佳位置的吸引力,或个体邻近性(i-邻近性)。这个成分反映了粒子对其自身过去经验的信任程度,不考虑其邻居或整个群体的经验。认知成分鼓励粒子探索其个人最佳位置周围的区域,使他们能够在有希望的区域内微调搜索。
-
社会成分—速度更新方程的第三部分是社会成分,它代表了粒子对群体集体知识或群体邻近性的吸引力(g-邻近性)。这个成分考虑了邻近粒子和整个群体的经验,引导粒子向迄今为止找到的全局最佳位置移动。社会成分促进了粒子之间的协作,帮助他们更有效地收敛到最佳解。

图 9.3 展示了群体中粒子的运动方程
为了更好地理解每个成分的含义,想象一群朋友第一次参观一个大型游乐园。他们的目标是尽可能高效地参观公园中最刺激的游乐设施。这些朋友可以被看作是 PSO 算法中的粒子,每个人对游乐设施的享受作为要优化的目标函数。每个人都有自己探索可用游乐设施的首选方式,比如穿过公园的某些部分或尝试特定的游乐设施,如过山车或水上滑梯。这类似于 PSO 中的惯性成分,其中粒子保持其当前的速度和方向,确保他们不会过于突然地改变探索模式。
每个朋友都依靠自己的个人经验来寻找最刺激的游乐设施。例如,一个朋友可能在当天早些时候在过山车上玩得很开心。他们更有可能回到他们最喜欢的游乐设施,或者想要找到更多类似的游乐设施,因为他们知道这是一个好选择。他们信任自己的判断,并专注于探索过山车周围的区域,寻找他们认为会喜欢的游乐设施,基于他们的个人经验。这就是认知成分,在 PSO 中,粒子被吸引到它们的个人最佳位置,遵循它们过去的经验和个人偏好。
然后,朋友们根据他们的共同经历合作寻找最刺激的游乐设施。想象一下,其中一位朋友刚刚乘坐了最刺激的过山车,迫不及待地想告诉其他人。当他们分享他们的兴奋时,整个群体对那个游乐设施的兴趣会集体增加,影响他们的个人选择。这是社会成分,其中 PSO 中的粒子受到全局最佳位置或群体集体知识的 影响。
下面的子节将更详细地介绍不同的 PSO 参数。
9.2.2 适应度更新
移动后,每个粒子使用以下方程更新其个人最佳值,假设是一个最小化问题:
|

| 9.3 |
|---|
之后,每个邻域按照以下方式更新其最佳值:
|

| 9.4 |
|---|
如果邻域是整个群体,则邻域最佳 (nbest) 与全局最佳 (gbest) 相同。
PSO 根据粒子位置和速度的更新方式主要有两种变体——同步和异步 PSO:
-
同步 PSO (S-PSO)——群体中的所有粒子以全局方式同时更新其位置和速度。然后更新局部和全局最佳值。这种同步方法确保了在更新速度和位置时,所有粒子都能访问相同的全局最佳位置,从而促进全局探索。
-
异步 PSO (A-PSO)——粒子根据群体的当前状态进行更新。这种异步方法允许粒子根据最新的可用信息更新其位置和速度。
图 9.4 显示了 S-PSO 和 A-PSO 之间的差异。你会注意到,在 A-PSO 中,邻域最佳更新被移动到粒子的更新循环中。这允许粒子独立和异步地评估其适应度并更新其位置和速度。

图 9.4 同步和异步 PSO
尽管同步和异步 PSO 策略都可以用于处理优化问题,但异步版本通常更有效,因为它允许粒子利用最新的邻域信息。
9.2.3 初始化
PSO 初始化包括初始化粒子位置、速度和个人最佳值,以及初始化算法的参数:
-
粒子位置初始化——粒子位置代表问题的候选解,它们可以使用不同的采样方法进行采样,如第 7.1 节所述。例如,粒子的初始位置可以在定义的可行搜索空间内随机分配。
-
粒子速度初始化——粒子的速度可以最初设置为零或小值。用小速度初始化它们确保粒子的更新是渐进的,防止它们远离起始位置。相比之下,大的初始速度可能导致显著的更新,这可能会引起发散并阻碍算法的收敛。
-
个人最佳位置初始化——每个粒子的个人最佳位置,代表粒子迄今为止找到的最佳解,应初始化为其初始位置。这允许粒子以它们的起始点作为参考开始搜索,并在发现更好的解决方案时更新它们的个人最佳位置。
如方程 9.1 所示,PSO 有三个主要参数,这些参数在控制搜索算法行为方面起着关键作用:惯性权重(ω)和加速度系数(c1,c2)。这些参数影响优化过程中探索与利用之间的平衡:
-
惯性权重——大的ω值鼓励探索,而小的值促进利用,允许认知和社会组件发挥更大的控制作用。ω的一个广泛采用值是 0.792。
-
加速度系数——将c1 设置为 0 将 PSO 算法简化为仅社交或无私的 PSO模型。在这种情况下,粒子仅被吸引到群体最佳位置,而忽略它们个人的最佳位置。这导致基于群体集体知识的全局探索得到强调。将c2 设置为 0 将导致仅认知模型,其中粒子作为独立的爬山者行动,仅依靠它们个人的最佳位置。在这种情况下,粒子不考虑其他群体成员的经验,专注于基于它们个人经验的局部利用。在许多应用中,c1 和c2 被设置为 1.49。尽管没有理论上的依据来支持这个特定值,但经验上发现它在各种优化问题中表现良好。一般来说,c1 和c2 的和应小于或等于 4,以保持算法的稳定性和收敛特性。
需要考虑的其他参数包括群体大小和邻域大小。没有一种适合所有情况的解决方案,因为最佳值取决于要解决的问题的具体情况。然而,一些最佳实践和指南可以帮助你做出选择:
-
群体大小—较大的群体大小可以促进全局探索并防止过早收敛,但代价是增加了计算工作量。较小的群体大小可以导致更快收敛和减少计算工作量,但可能会增加过早收敛的风险。对于许多问题,20 到 100 个粒子的群体大小已被发现能产生良好的结果。建议进行不同群体大小的实验,以确定针对特定问题的探索、利用和计算复杂度之间的最佳权衡。
-
邻域大小—较大的邻域大小可以鼓励全局探索和粒子之间的信息共享,但可能会降低利用局部最优的能力。较小的邻域大小可以促进局部利用和收敛速度,但可能会限制全局探索。你可以使用不同的邻域结构,如下一小节所示。
通常来说,选择最佳算法参数需要根据你试图解决的问题进行实验和微调。进行敏感性分析或使用参数调整技术以找到问题的最佳参数值通常是很有益的。我们将在 9.5 节中更详细地探讨这一点。
9.2.4 邻域
在 PSO 算法中,特定邻域内的粒子通过共享该局部区域内彼此的成功细节进行相互通信。随后,所有粒子都会向一个被认为基于关键性能指标有所改进的位置聚集。PSO 算法的有效性高度依赖于所采用的社会网络结构。选择合适的邻域拓扑在确保算法收敛和防止其陷入局部最优中起着至关重要的作用。
在 PSO 中使用的常见邻域拓扑包括星型社交结构、环形拓扑、冯·诺伊曼模型和轮形拓扑:
-
星型社交结构,也称为全局最优(gbest)PSO—这是一个所有粒子都相互连接的邻域拓扑,如图 9.5a 所示。这种结构允许群体内访问全局信息,结果是每个粒子都会被整个群体发现的最佳解所吸引。gbest PSO 已被证明比其他网络结构收敛得更快。然而,它更容易在没有完全探索搜索空间的情况下陷入局部最优。当应用于单峰问题时,这种拓扑特别出色,因为它允许在这种情况下进行高效有效的优化。
-
环形拓扑,也称为局部最优(lbest)PSO—遵循此拓扑,粒子仅与其直接相邻的邻居相互作用(如图 9.5b 所示)。每个粒子都试图通过向局部范围内发现的最佳解靠近来模仿其最成功的邻居。尽管收敛速度比星型结构慢,但环形拓扑探索了更广泛的搜索空间。这种拓扑建议用于多模态问题。
-
冯·诺伊曼模型—在这个拓扑中,粒子以网格状结构或正方形拓扑排列,每个粒子与四个其他粒子(上方、下方、右侧和左侧的邻居)相连,如图 9.5c 所示。
-
轮形拓扑—在这个拓扑中,粒子彼此隔离,随机选择一个粒子作为所有信息流的焦点或中心,如图 9.5d 所示。
邻域拓扑的选择取决于问题的特征以及探索和利用之间所需达到的平衡。尝试不同的拓扑结构以找到最适合您问题的最佳匹配。

图 9.5 PSO 邻域拓扑:a) 星型社会结构,b) 环形拓扑,c) 冯·诺伊曼模型,和 d) 轮形拓扑
现在我们来看如何使用 PSO 解决连续优化问题。Michalewicz 函数是一种非凸数学函数,通常用作优化算法的测试问题。该函数由以下公式给出:
|

| 9.5 |
|---|
其中d是问题的维度,m是一个常数(通常m = 10)。该函数有d个局部最小值。对于d = 2,最小值是-1.8013,在(2.20, 1.57)处。
让我们从定义 Michalewicz 函数开始,如列表 9.1 所示。该函数可以接受大小为 1 的数组,具有单行或多行。如果输入位置是大小为 1 的单行数组,我们使用reshape()函数将其重塑为单行的二维数组。大小为 1 的数组,也称为单元素数组,是一种只包含一个元素的数据结构。这种重塑使得可以统一处理单行大小为 1 的数组和多行数组。这在 PSO 求解器的实现中很明显,其中该函数一次解决一个解。此外,该函数无缝地管理多行数组,这在同时评估多个解时遇到。这一方面将在 pymoo 和 PySwarms 求解器的上下文中进一步阐述。
列表 9.1 使用 PSO 求解 Michalewicz 函数
import numpy as np
import math
import matplotlib.pyplot as plt
def michalewicz_function(position):
m = 10
position = np.array(position)
if len(position.shape) == 1: ①
position = position.reshape(1, -1) ①
n = position.shape[1]
j = np.arange(1, n + 1)
s = np.sin(position) * np.power(np.sin((j * np.square(position)) /
➥ np.pi), 2 * m) ②
return -np.sum(s, axis=1) ②
① 如果位置是大小为 1 的数组,则将其重塑为单行的二维数组。
② Michalewicz 公式
让我们现在从头开始创建一个 PSO 求解器。作为 9.1 列表的延续,我们首先定义一个具有位置、速度和个人最佳值的粒子类,如下所示:
class Particle:
def __init__(self, position, velocity, pbest_position, pbest_value):
self.position = position
self.velocity = velocity
self.pbest_position = pbest_position
self.pbest_value = pbest_value
要最小化的适应度函数是本例中的 Michalewicz 函数:
def fitness_function(position):
return michalewicz_function(position)
我们现在可以定义速度更新函数,根据方程 9.1。该函数接受三个参数——particle,它是一个表示当前粒子的对象;gbest_position,它是迄今为止群体找到的全局最佳位置;以及options,它是一个包含算法参数的字典(特别是惯性权重w和认知及社会加速度系数c1和c2):
def update_velocity(particle, gbest_position, options):
w = options['w']
c1 = options['c1']
c2 = options['c2']
inertia = w * particle.velocity
cognitive = c1 * np.random.rand() * (particle.pbest_position –
➥ particle.position)
social = c2 * np.random.rand() * (gbest_position - particle.position)
new_velocity = inertia + cognitive + social
return new_velocity
函数根据方程 9.1 计算新速度的三个分量:惯性分量、认知分量和社会分量。它返回三个分量的总和作为更新后的速度。
我们现在可以定义 PSO 求解器函数。此函数接受四个参数作为输入——swarm_size,它是粒子群的大小;iterations,它是运行算法的最大迭代次数;bounds,它是一个元组列表,定义了输入向量每个维度的搜索空间的上下边界;以及options,它是一个包含算法参数的字典(例如惯性权重和认知及社会加速度系数):
def pso(swarm_size, iterations, bounds, options): ①
swarm = [] ①
for _ in range(swarm_size): ①
position = np.array([np.random.uniform(low=low, high=high) for low, ①
➥ high in bounds]) ①
velocity = np.array([np.random.uniform(low=-abs(high-low), ①
➥ high=abs(high-low)) for low, high in bounds]) ①
pbest_position = position ①
pbest_value = fitness_function(position) ①
particle = Particle(position, velocity, pbest_position, pbest_value) ①
swarm.append(particle) ①
gbest_position = swarm[np.argmin([particle.pbest_value for particle in ②
➥ swarm])].pbest_position ②
gbest_value = np.min([particle.pbest_value for particle in swarm]) ②
for _ in range(iterations):
for _, particle in enumerate(swarm):
particle.velocity = update_velocity(particle, gbest_position, ③
options) ③
particle.position += particle.velocity ③
particle.position = np.clip(particle.position, [low for low, high
➥ in bounds], [high for low, high in bounds]) ④
current_value = fitness_function(particle.position) ⑤
if current_value < particle.pbest_value: ⑤
particle.pbest_position = particle.position ⑤
particle.pbest_value = current_value ⑤
if current_value < gbest_value: ⑥
gbest_position = particle.position ⑥
gbest_value = current_value ⑥
particle.position += particle.velocity ⑦
return gbest_position, gbest_value ⑧
① 初始化一个随机群体。
② 初始化全局最佳。
③ 更新速度和位置。
④ 应用边界。
⑤ 更新个人最佳(pbest)。
⑥ 更新全局最佳(gbest)。
⑦ 更新位置。
⑧ 返回全局最佳位置和相应的值。
函数首先通过在bounds定义的边界内随机生成每个粒子的初始位置和速度来初始化粒子群。然后,它评估每个粒子的适应度函数,并相应地更新其个人最佳位置和值。然后,函数进入一个循环,在该循环中使用update_velocity函数更新每个粒子的速度和位置,该函数接受迄今为止找到的全局最佳位置作为输入。该函数还应用边界到粒子位置,并更新其个人最佳位置和值。然后,根据星型拓扑更新全局最佳位置和值。其他拓扑,如环形、冯·诺伊曼和轮形,可以在 9.1 列表的完整代码中找到,该代码可在本书的 GitHub 存储库中找到。最后,函数返回算法找到的全局最佳位置和值。
在设置问题和算法参数后,我们现在可以使用此 PSO 求解器最小化 Michalewicz 函数,如下所示:
swarm_size = 50 ①
iterations = 1000 ①
options = {'w': 0.9, 'c1': 0.5, 'c2': 0.3} ①
dimension = 2 ②
bounds = [(0, math.pi)] * dimension ②
best_position, best_value = pso(swarm_size, iterations, bounds, options) ③
① PSO 参数
② Michalewicz 函数的维度和域
③ 使用已实现的 PSO 求解器解决问题。
你可以在运行 PSO 后打印出最优解和函数的最小值:
print(f"Optimal solution: {np.round(best_position,3)}")
print(f"Minimum value: {np.round(best_value,4)}")
print()
输出如下:
Optimal solution: [2.183 1.57]
Minimum value: [-1.8013]
与遗传算法相比,Python 库中可用的 PSO 更少。Pymoo 为连续问题提供了一个 PSO 实现。作为列表 9.1 的延续,pymoo PSO 可以如下用于解决相同的问题:
from pymoo.algorithms.soo.nonconvex.pso import PSO
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
class MichalewiczFunction(Problem): ①
def __init__(self):
super().__init__(n_var=2, ②
n_obj=1,
n_constr=0,
xl=0, ③
xu=math.pi, ③
vtype=float)
def _evaluate(self, x, out, *args, **kwargs):
out["F"]= michalewicz_function(x) ④
problem = MichalewiczFunction() ⑤
algorithm = PSO(w=0.9, c1=2.0, c2=2.0) ⑥
res = minimize(problem,
algorithm,
seed=1,
verbose=False) ⑦
print(f"Optimal solution: {np.round(res.X,3)}") ⑧
print(f"Minimum value: {np.round(res.F,4)}") ⑧
① 定义问题。
② 2D Michalewicz 函数
③ 设置下限和上限。
④ 评估目标函数。
⑤ 创建问题实例。
⑥ 使用参数定义求解器。
⑦ 将 PSO 应用于解决问题。
⑧ 运行 PSO 后打印最优解和函数的最小值。
此代码产生以下输出:
Optimal solution: [2.203 1.571]
Minimum value: [-1.8013]
PySwarms 是另一个用于 Python 的开源优化库,它实现了 PSO 的不同变体。PySwarms 可以如下使用来处理问题:
!pip install pyswarms
import pyswarms as ps ①
dimension = 2 ②
bounds = (np.zeros(dimension), np.pi * np.ones(dimension)) ③
options = {'w': 0.9, 'c1': 0.5, 'c2': 0.3} ④
optimizer = ps.single.GlobalBestPSO(n_particles=100, dimensions=dimension,
➥ options=options, bounds=bounds) ⑤
cost, pos = optimizer.optimize(michalewicz_function, iters=1000) ⑥
print(f"Optimal solution: {np.round(pos,3)}") ⑦
print(f"Minimum value: {np.round(cost,4)}") ⑦
① 从 pyswarms 导入 PSO 求解器。
② Michalewicz 函数的维度
③ 为搜索空间创建边界。
④ 设置优化器。
⑤ 创建优化器的实例。
⑥ 优化 Michalewicz 函数
⑦ 运行 PSO 后打印最优解和函数的最小值。
此代码产生以下输出:
Optimal solution: [2.203 1.571]
Minimum value: -1.8013
pyswarms.single 包实现了连续单目标优化中的各种技术。从该模块中,我们使用了之前代码中的全局最优 PSO(gbest PSO)算法。你可以通过将此求解器替换为局部最优来实验。
图 9.6 显示了 Michalewicz 函数的 3D 地形和 2D 等高线,最优解以及 PSO、PSO Pymoo 和 PSO PySwarms 获得的解。

图 9.6 Michalewicz 函数的 3D 和 2D 图。三个解都非常接近最优解。
三个版本的 PSO 提供了可比的结果。然而,PSO PySwarms 和 PSO pymoo 更稳定,因为它们每次运行代码时都提供更一致的结果。与 pymoo 相比,PySwarms 库在 PSO 方面更全面,因为它提供了不同变体和拓扑结构的 PSO 实现,包括离散 PSO,这将在下一两节中解释。
9.3 二进制 PSO
PSO 最初是为涉及连续值变量的问题开发的。然而,许多现实世界的问题在本质上是非连续的或组合的,例如 TSP、任务分配、调度、分配问题和特征选择等。这类问题涉及在有限的可能解集中搜索,而不是在连续空间中搜索。为了处理这些离散问题,已经开发了 PSO 的变体,如二进制 PSO 和基于排列的 PSO。
在 二进制粒子群优化(BPSO)中,每个粒子代表二进制空间中的一个位置,其中每个元素是 0 或 1。二进制序列根据其当前值、粒子中特定位的基于适应度的值以及迄今为止观察到的相邻粒子中相同位的最佳值逐位更新。这种方法使得搜索可以在二进制空间而不是连续空间中进行,这对于变量为二进制的实际问题非常适合。
在 BPSO 中,速度是根据位变化的概率定义的。为了将速度元素的值限制在 [0,1] 范围内,使用了 S 形函数:
|

| 9.6 |
|---|
位置更新方程因此变为
|

| 9.7 |
|---|
其中 r 是在 [0, 1] 范围内随机生成的数字。图 9.7 展示了 S 形函数和更新位置为 1 的概率。例如,如果 v = 0.3,这意味着更新位置为 1 的概率是 30%,而为 0 的概率是 70%。

图 9.7 二进制粒子群优化(BPSO)中的位置和速度表示。每个粒子代表二进制空间中的一个位置。速度是根据位变化的概率定义的。
如您所注意到的,速度分量将保持为使用原始方程中的实数值,但在更新位置向量之前,这些值将通过 S 形函数。以下方程是 BPSO 中的速度更新方程:
|

| 9.8 |
|---|
|

| 9.9 |
|---|
位置更新根据以下方程进行:
|

| 9.10 |
|---|
其中
-
ϕ[1] 和 ϕ[2] 代表从均匀分布中抽取的不同随机数。有时这些参数是从 0–2 的均匀分布中选择的,使得它们的两个极限之和为 4.0。
-
vid[k] [+1] 是个体 i 在二进制字符串的第 d^(th) 位选择 1 的概率。
-
x[k]^(id) 是字符串 i 在位 d 的当前状态。
-
v[k]^(id) 是字符串当前选择 1 的概率的度量。
-
pbest[k]^(id) 是个体 i 中位 d 的迄今为止找到的最佳状态(即,1 或 0)。
-
gbest[k]^d 取决于迄今为止最佳邻居中位 d 的值是 1 还是 0。
BPSO 示例
为了说明 BPSO 的工作原理,假设我们有一个由五个二进制粒子组成的种群,其中每个粒子由 6 位组成。让我们假设粒子由以下二进制字符串表示:101101,110001,011110,100010,和 001011。我们想要更新粒子 4(由二进制字符串 100010 表示)在位 3(当前值为 0)的值。假设这个位变为 1 的当前倾向(速度)是 0.23。此外,我们假设这个粒子迄今为止找到的最佳值是 101110,而整个种群找到的最佳值是 101111。让我们还假设ϕ[1] = 1.5 和ϕ[2] = 1.9。使用方程 9.8 和 9.9,我们可以得到粒子 4 中位 3 的更新速度如下:
粒子 4:100010,v[k]⁴³ = 0.23,x[k]⁴³ = 0,pbest[k]⁴³ = 1,gbest[k]³ = 1,ϕ[1] = 1.5,ϕ[2] = 1.9
v[k] [+ 1]⁴³ = 0.23 + 1.5(1–0) + 1.9(1–0) = 3.63
sig(v[k] [+ 1]⁴³) = sig(3.63) = 1/(1 + e^(–3.63)) = 0.974
生成一个随机数 r⁴³ = 0.6,并使用方程 9.10 更新位置如下:
x[k] [+ 1]⁴³ = 1 因为 sig(v[k] [+ 1]⁴³) > r⁴³
更新粒子 4:100110
更多关于 BPSO 的信息,请参阅 Kennedy 和 Eberhart 的文章“粒子群算法的离散二进制版本” [2]。
9.4 基于排列的 PSO
已经进行了许多努力来将 PSO 应用于解决排列问题。将 PSO 适应这些问题的挑战源于速度和方向的概念本身不适用于排列问题。为了克服这个障碍,需要重新定义像加法和乘法这样的算术运算。
在 M. Clerc 的 2004 年文章“离散粒子群优化,以旅行商问题为例” [3]中,PSO 被应用于解决 TSP 问题。粒子的位置是问题的解(城市的排列)。粒子的速度被定义为对粒子执行的一组交换。正如你所看到的,方程 9.1 的右侧包含三个算术运算:乘法、减法和加法。这些运算在新的搜索空间中被重新定义为以下内容:
- 乘法—速度向量限制城市之间的交换次数。将这个向量乘以一个常数 c,结果得到另一个具有不同长度的速度向量,这取决于常数的值。如果 c = 0,则速度向量的长度(即包含的交换次数)设置为 0。这意味着不会执行任何交换。如果 c < 1,则速度被截断。如果 c > 1,则速度如图 9.8 所示增加。增加意味着将来自当前速度向量顶部的交换添加到新速度向量的末尾。

图 9.8 基于排列的 PSO 重新定义的乘法
- 减法—两个位置相减应产生一个速度。此操作产生将一个位置转换为另一个位置的交换序列。例如,让我们考虑一个 8 城市 TSP 问题。该 TSP 的一个候选解由排列表示,例如[2, 4, 6, 1, 5, 3, 8, 7]。图 9.9 展示了如何通过减去两个位置来生成一个新的速度向量。

图 9.9 基于排列的 PSO 重新定义的减法操作
- 加法—该操作通过应用由速度定义的交换序列到位置向量来执行。图 9.10 展示了如何通过将速度交换向量加到当前位置来生成一个新的位置(即一个新的候选解)。

图 9.10 基于排列的 PSO 重新定义的加法操作
这些重新定义的算术运算使我们能够更新 PSO 粒子的速度和位置。
9.5 自适应 PSO
惯性、认知和社会成分是 PSO 的主要参数,可以在优化过程中实现探索和开发的平衡。这三个因素显著影响算法的行为,如以下小节所述。
9.5.1 惯性权重
惯性参数表示粒子保持其当前轨迹的趋势。通过调整惯性值,算法可以在广泛搜索解空间(探索)和专注于迄今为止找到的最佳解(开发)之间取得平衡。较大的ω值促进探索,而较小的值促进开发,如图 9.11 所示。过小的值可能会阻碍群体的探索能力。随着ω值的减小,认知和社会成分对位置更新的影响变得更加突出。

图 9.11 PSO 参数对搜索行为的影响。较大的惯性促进探索,而较小的值促进开发。c1 > c2 会导致个体在搜索空间中过度徘徊。相比之下,c2 > c1 可能会导致粒子过早地向局部最优解冲去。
当 ω > 1 时,粒子速度倾向于随时间增加,加速向最大速度(假设使用了速度钳位),最终导致群体发散。在这种情况下,粒子难以改变方向返回有希望的领域。另一方面,当 ω < 1 时,粒子可能会逐渐减速,直到它们的速度接近 0,这取决于加速度系数的值。可以通过设置速度的最大(和最小)限制来考虑速度钳位。如果粒子的计算速度超过此限制,则将其设置为最大(或最小)值。这防止粒子在问题空间中走得太远或在搜索空间的特定区域卡住。
可以使用以下方法来更新惯性权重:
- 随机选择 (RS)—这涉及到在每次迭代中选择不同的惯性权重。权重可以从具有您选择的平均值和标准差的分布中选择,但重要的是要确保尽管存在随机性,群体仍然收敛。以下公式可以用来:
|

| 9.11 |
|---|
其中 rand(.) 是 [0,1] 范围内均匀分布的随机数。因此,惯性权重的平均值是 0.75。
- 线性时变 (LTV)—这涉及到逐渐减小 𝜔 的值,从起始的高值 𝜔[max] 下降到最终的低值 𝜔[min],遵循以下方程:
|

| 9.12 |
|---|
其中 𝑡[max] 是迭代次数,t 是当前迭代,𝜔[t] 是第 t 次迭代的惯性权重值。通常,约定将 𝜔[max] 和 𝜔[min] 设置为 0.9 和 0.4。
- 非线性时变 (NLTV)—这种方法也涉及到从初始高值减小惯性权重,但这种减小可以是非线性的,如下方程所示:
|

| 9.13 |
|---|
其中 𝜔[𝑡][=0] = 0.9 是 𝜔 的初始选择。通过允许更多时间下降到动态范围的较低端,NLTV 可以增强局部搜索或开发。
图 9.12 展示了这三种更新方法。

图 9.12 不同惯性权重更新方法
如您所见,在随机选择中,每次迭代都会随机选择不同的惯性权重。惯性权重的平均值是 0.75。LTV 线性减小惯性权重。在 NLTV 中,惯性权重的减小比 LTV 更渐进。总之,惯性权重在 PSO 算法的收敛速度和解决方案质量中起着至关重要的作用。高惯性权重促进探索,而低惯性权重鼓励开发。
9.5.2 认知和社会成分
认知组件c1 是与粒子个体学习能力相关的参数,其中粒子受其自身经验的影响。社会组件c2 是与群体中所有粒子的集体学习能力相关的参数。它表示粒子受其邻居找到的最佳解决方案影响的程度。如果c1 > c2,算法将表现出探索行为,如果c2 > c1,算法将倾向于利用局部搜索空间,如图 9.11 所示。将c1 = 0 将速度模型简化为仅社会模型或无私模型(粒子都被吸引到nbest)。另一方面,将c2 = 0 将其简化为仅认知模型(粒子是独立的,如爬山算法的情况)。
通常,c1 和c2 在 PSO 中保持不变。经验上,c1 和c2 的和应小于或等于 4,任何与此有显著差异的情况可能导致发散行为。在自适应 PSO 中,建议随着时间的推移逐渐降低c1 的值,并使用线性公式[4]同时增加c2 的值,如下所示:
|

| 9.14 |
|---|
|

| 9.15 |
|---|
其中t是迭代索引,c1[max]和c2[max]分别是最大认知和社会参数,c1[min]和c2[min]分别是最小认知和社会参数,t[max]是最大迭代次数。
图 9.13 显示了线性变化的c1 和c2。如图所示,我们开始时c1 > c2,以利于探索。随着搜索的进行,c2 开始高于c1,以利于利用。

图 9.13 认知和社会加速系数更新。c1>c2 导致更多的探索,而 c2>c1 可能导致更多的利用。
让我们看看如何使用 PSO 处理连续和离散优化问题。
9.6 解决旅行商问题
在上一章中,您看到了如何使用遗传算法从纽约市开始解决美国 20 个主要城市的 TSP 问题。现在,让我们使用 PSO 解决相同的问题,如下所示。我们首先定义二十个美国城市的纬度和经度,并计算它们之间的城市距离。
列表 9.2 使用 PSO 解决 TSP
import numpy as np
import pandas as pd
from collections import defaultdict
from haversine import haversine
import networkx as nx
import matplotlib.pyplot as plt
import pyswarms as ps
cities = {
'New York City': (40.72, -74.00),
'Philadelphia': (39.95, -75.17),
'Baltimore': (39.28, -76.62),
'Charlotte': (35.23, -80.85),
'Memphis': (35.12, -89.97),
'Jacksonville': (30.32, -81.70),
'Houston': (29.77, -95.38),
'Austin': (30.27, -97.77),
'San Antonio': (29.53, -98.47),
'Fort Worth': (32.75, -97.33),
'Dallas': (32.78, -96.80),
'San Diego': (32.78, -117.15),
'Los Angeles': (34.05, -118.25),
'San Jose': (37.30, -121.87),
'San Francisco': (37.78, -122.42),
'Indianapolis': (39.78, -86.15),
'Phoenix': (33.45, -112.07),
'Columbus': (39.98, -82.98),
'Chicago': (41.88, -87.63),
'Detroit': (42.33, -83.05)
} ①
distance_matrix = defaultdict(dict) ②
for ka, va in cities.items(): ②
for kb, vb in cities.items(): ②
distance_matrix[ka][kb] = 0.0 if kb == ka ②
➥ else haversine((va[0],va[1]), (vb[0], vb[1])) ②
distances = pd.DataFrame(distance_matrix) ③
distance=distances.values ③
city_names=list(distances.columns) ③
①为二十个主要美国城市定义纬度和经度。
②根据纬度和经度坐标创建哈夫曼距离矩阵。
③将距离字典转换为具有距离值和城市名称作为标题的数据框。
接下来,我们可以计算城市的数量并设置决策变量的整数边界,这些变量代表访问城市的顺序。第一个函数tsp_distance接受两个参数:position和distance。position是一个一维数组,表示访问城市的顺序。distance是一个二维数组,包含所有城市对之间的距离。该函数首先将tour定义为表示访问城市顺序的索引排列。然后,通过计算相邻城市之间的距离以及路线中最后一个城市与起始城市之间的距离来计算路线的总距离。
第二个函数tsp_cost接受两个参数:x和distance。x是一个二维数组,包含 TSP 问题的决策变量,其中每一行代表群体中的不同粒子。distance是一个二维数组,包含所有城市对之间的距离。该函数通过在x的每一行上调用tsp_distance函数来计算每个粒子的成本,并返回一个成本列表:
n_cities = len(city_names) ①
bounds = (np.zeros(n_cities), np.ones(n_cities)*(n_cities-1)) ①
def tsp_distance(position, distance): ②
tour = np.argsort(position) ③
total_distance = distance[0, tour[0]] ④
for i in range(n_cities-1): ④
total_distance += distance[tour[i], tour[i+1]] ④
total_distance += distance[tour[-1], 0] ④
return total_distance
def tsp_cost(x, distance): ⑤
n_particles = x.shape[0] ⑤
cost=0 ⑤
cost = [tsp_distance(x[i], distance) for i in range(n_particles)] ⑤
return cost ⑤
① 将旅行商问题(TSP)定义为具有整数边界的排列优化问题。
② 定义 TSP 距离函数
③ 将排列转换为 TSP 路线。
④ 从纽约市作为第一个城市开始计算路线的总距离,并添加最后一个城市返回纽约市的距离。
⑤ 计算并返回每个粒子在群体中的成本。
作为 9.2 列表的延续,以下代码设置了 PSO 优化器的参数。options是一个字典,包含惯性权重(w)、认知(c1)和社会(c2)加速度系数、要考虑的邻居数量(k)以及 Minkowski 距离的 p 值。n_particles代表优化中使用的粒子数量,dimensions代表决策变量的数量,等于 TSP 问题中的城市数量。优化器找到的最佳解通过按升序排序解的索引,并使用它们以相同的顺序索引city_names列表来转换为 TSP 路线。这创建了一个按最佳路线访问顺序排列的城市名称列表。然后,我们打印最佳路线及其长度:
options = {'w': 0.79, 'c1': 2.05, 'c2': 2.05, 'k': 10, 'p': 2} ①
optimizer = ps.discrete.BinaryPSO(n_particles=100, dimensions=n_cities,
➥ options=options) ②
cost, solution = optimizer.optimize(tsp_cost, iters=150, verbose=True,
➥ distance=distance) ③
tour = np.argsort(solution) ④
city_names_tour = [city_names[i] for i in tour] ④
Route = " → ".join(city_names_tour) ⑤
print("Route:", Route) ⑤
print("Route length:", np.round(cost, 3)) ⑤
① 设置 PSO 参数。
② 实例化 PSO 优化器。
③ 解决问题。
④ 将最佳解转换为 TSP 路线。
⑤ 打印最佳路线及其长度。
列表 9.2 生成以下输出:
Route: New York City → Columbus → Indianapolis → Memphis → San Francisco → San Jose → Los Angeles → San Diego → Phoenix → Dallas → Fort Worth → San Antonio → Austin → Houston → Jacksonville → Charlotte → Baltimore → Philadelphia → Chicago → Detroit
Route length: 12781.892
图 9.14 显示了获得的路线。9.2 列表的完整版本可在本书的 GitHub 仓库中找到,它展示了将路线可视化为 NetworkX 图的过程。

图 9.14 展示了 20 城市 TSP 的 PSO 解决方案
根据您的需要调整代码,例如修改问题数据、初始城市或算法参数。
9.7 使用 PSO 进行神经网络训练
机器学习(ML)是人工智能(AI)的一个子领域,它赋予人工系统或过程从经验观察中学习的能力,而不需要明确编程。许多机器学习方法已经被提出,并且仍在被提出,更多关于机器学习的细节将在第十一章中提供。现在,让我们考虑神经网络,这是最常用且最成功的统计机器学习方法之一。人工神经网络(ANN 或 NN)方法受生物大脑的启发,可以被认为是一个高度简化的计算模型,因为神经网络与大脑的复杂性相去甚远。神经网络是深度学习模型的核心,如今它是许多成功应用的基础,这些应用触及到每个人的生活,如文本、音频、图像和视频生成、语音助手和推荐引擎等,仅举几例。
人类大脑
亚里士多德(公元前 384-322 年)写道:“在所有动物中,人类的大脑相对于其体型来说最大。”人类大脑由平均 86 亿个相互连接的神经细胞或神经元组成。每个生物神经元连接到几千个其他神经元。它非常节能,因为它只需 20 瓦的功率就能完成相当于一个艾弗洛普(每秒十亿亿次数学运算)的工作。
为了简化,我们可以将机器学习视为一种美化的曲线拟合,其目的是在自变量和因变量之间找到一个映射函数。例如,假设一个基于视觉的对象识别模型以车辆前摄像头拍摄的数字图像作为输入——输出将是识别出的对象,如汽车、行人、骑自行车的人、车道、交通灯等。实际上,在模型、评分标准和搜索策略方面,机器学习与曲线拟合有相同的成分。然而,机器学习方法,如神经网络,是一种创建人类无法编写的函数的方法。它们倾向于创建非线性、非单调、非多项式,甚至非连续的函数,这些函数近似地表示数据集中自变量和因变量之间的关系。
神经网络是一个由简单非线性计算元素(称为神经元)组成的巨大并行自适应网络,这些神经元排列在输入、隐藏和输出层中。每个节点,或人工神经元,都与另一个节点相连,并具有相关的权重和阈值,允许节点模拟神经元放电。每个单独的节点都有自己的线性回归模型,由输入数据、偏差、阈值和输出组成,如图 9.15 所示。一个神经元 k 可以用以下方程来描述:
|

| 9.16 |
|---|
其输出是
|

| 9.17 |
|---|
其中 x[i] 是输入,ω[ki] 是权重,b 是偏差项,它定义了在没有外部输入到节点的情况下节点能够激活的能力,而 φ 是激活函数。这个激活函数使得当输入 z[k] 达到阈值 θ[k] 时,神经元会输出。激活函数有不同的形式(也称为压缩函数),例如符号、阶跃、tanh、反正切、S 形 sigmoid(也称为逻辑回归)、softmax、径向基函数和修正线性单元(ReLU)。
就像曲线拟合的情况一样,使用评分标准或损失函数来估计估计值和实际值之间的偏差。在这种情况下,训练神经网络本质上是一个优化问题。训练的目标是找到最优参数(权重和偏差),以最小化网络输出和预期输出之间的差异。这种差异通常使用损失或成本函数来量化,例如这些:
-
均方误差 (MSE)—MSE 常用于回归问题。它计算预测值和实际值之间的差的平方,然后在整个数据集上平均这些值。这个函数对大误差进行重罚。
-
交叉熵损失—交叉熵损失通常用于二进制和多类分类问题。它衡量预测概率分布和实际分布之间的不相似性。换句话说,它比较模型对其预测的信心与实际结果。
-
负对数似然 (NLL)—NLL 是多类分类中的另一个损失函数。如果 y 是真实标签,而 p(y) 是该标签的预测概率,则负对数似然定义为 –log(p(y))。对数函数将介于 0 和 1 之间的概率转换为介于正无穷大到 0 的尺度。当预测概率对于正确类别较高(接近 1)时,对数值更接近 0,但随着预测概率对于正确类别的降低,对数值增加到无穷大。因此,取对数值的相反数给出一个量,当预测概率对于正确类别最大化时,该量被最小化。

图 9.15 神经网络节点演示
训练神经网络涉及以下步骤:
-
初始化—在训练开始之前,网络中的权重和偏差通常使用小的随机数进行初始化。
-
前馈—在这个阶段,输入通过网络产生输出。这个输出是通过使用初始或当前权重、偏差和激活函数转换对输入进行计算而生成的。一个层的输出成为下一层的输入,直到产生最终输出。
-
误差计算—在正向传播阶段之后,输出与期望输出进行比较,使用损失函数计算误差。这个函数量化了网络的预测值与实际值之间的距离。
-
反向传播—计算出的误差随后通过网络反向传播,从输出层开始,移动到输入层。这个过程计算了网络中损失函数相对于权重和偏差的梯度或导数。
-
权重调整—在这个最终阶段,网络权重被更新以减少误差。这通常使用称为梯度下降的技术来完成。权重调整的方向是减少误差最多的方向,这是通过反向传播期间计算的梯度确定的。
通过多次迭代(或称为 epoch)重复这些步骤,网络逐渐学会产生更接近期望的输出,从而“学习”输入数据。
现在你已经对神经网络有了基本了解,让我们使用 PSO(粒子群优化)按照监督学习的方法训练一个简单的神经网络。在监督训练过程中,神经网络通过最初处理标记的数据集来学习。通过在标记的数据集上训练,网络可以在训练后的推理阶段预测新、未标记数据集的标签。
对于这个例子,我们将使用企鹅数据集。这是数据科学社区中一个流行的数据集,包含有关企鹅大小、性别和种类的信息。该数据集由来自南极洲帕默群岛三个岛屿的 344 个观测值组成。它包括以下七个变量:
-
species—企鹅的种类(阿德利企鹅、帝企鹅或金图企鹅) -
island—观察到企鹅的岛屿(比斯科、梦想或托格森) -
bill_length_mm—企鹅喙的长度(毫米) -
bill_depth_mm—企鹅喙的深度(毫米) -
flipper_length_mm—企鹅鳍的长度(毫米) -
body_mass_g—企鹅身体的重量(克) -
sex—企鹅的性别(雄性或雌性)
我们在 PySwarms 用例中描述的简单神经网络具有以下特征:
-
输入层大小—4
-
隐藏层大小—10(激活函数:tanh(x))。双曲正切激活函数(又称 Tanh、tanh 或 TanH)将输入值映射到-1 和 1 之间,并用于在神经网络中引入非线性。记住,sigmoid 函数将输入值映射到 0 和 1 之间。tanh 函数以 0 为中心,这有助于减轻梯度消失问题,与 sigmoid 函数相比。然而,tanh 和 sigmoid 激活都存在一定程度的梯度消失问题。像 ReLU 及其变体这样的替代方案通常更受欢迎。
-
输出层大小—3(激活函数:softmax(x))。Softmax 是 sigmoid 函数的推广。该函数接受作为输入的logits,这些logits代表网络最后一层在应用 softmax 函数将其转换为概率之前的未归一化输出。这些 logits 可以解释为衡量“证据”的指标,表明某个输入属于特定类。特定类的 logits 值越高,输入属于该类的可能性就越大。
以下列表展示了使用 PSO 训练这个简单神经网络(NN)的步骤。我们首先导入所需的库并读取 penguin 数据集。
列表 9.3 使用 PSO 进行神经网络训练
import seaborn as sns ①
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from sklearn.preprocessing import LabelEncoder ②
from sklearn.decomposition import PCA ②
import pyswarms as ps ③
penguins = sns.load_dataset('penguins') ④
penguins.head() ⑤
① 用于加载数据集
② 用于目标标签编码
③ 用于降维
④ 加载 Penguins 数据集。
⑤ 显示数据集的行和列。
这会产生如图 9.16 所示的输出。

图 9.16 鹦鹉螺数据集
作为 9.3 列表的延续,我们可以使用 seaborn 库如下可视化这个数据集:
plt.figure(figsize=(8, 6))
sns.scatterplot(data=penguins, x='bill_length_mm', y='body_mass_g',
➥ hue='species', style="species")
plt.title('Bill Length vs. Body Mass by Species')
plt.show()
输出如图 9.17 所示。

图 9.17 鹦鹉螺数据集中不同物种的喙长与体重
接下来,我们定义一个logits_function,它接受一个参数向量p,用于神经网络,并返回网络的最后一层的 logits(预激活值)。如图 9.18 所示,该函数首先使用索引和重塑操作从参数向量p中提取网络第一层和第二层的权重和偏差。然后,该函数通过计算第一层的预激活值z¹(输入数据X和第一组权重W¹的点积加上偏差项b¹)来执行前向传播。然后,它将 tanh 激活函数应用于z¹以获得第一层的激活值a¹。最后,该函数通过将a¹与第二组权重W²的点积加上偏差项b²来计算第二层的预激活值。这些结果作为网络的最后一层的 logits 返回:
def logits_function(p):
W1 = p[0: n_inputs * n_hidden].reshape((n_inputs, n_hidden))
b1 = p[n_inputs * n_hidden: (n_inputs + 1) * n_hidden].reshape((n_hidden,)) ①
W2 = p[(n_inputs +1) * n_hidden: -n_classes].reshape((n_hidden, n_classes)) ②
b2 = p[-n_classes:].reshape((n_classes,)) ③
z1 = X.dot(W1) + b1 ④
a1 = np.tanh(z1) ④
logits = a1.dot(W2) + b2 ⑤
return logits ⑤
① 提取第一层的权重。
② 提取第二层的权重。
③ 提取第二层的偏差
④ 计算第一层的预激活值。
⑤ 计算并返回 logits。

图 9.18 神经网络层
接下来,我们定义forward_prop函数,通过具有两层 NN 进行正向传播。此函数计算给定一组参数params的输出 softmax 概率和负对数似然损失。函数首先调用logits_function来获取网络最后一层的 logits,给定参数params。然后,该函数使用np.exp函数对 logits 应用 softmax 函数,并使用带有axis=1参数的np.sum函数通过将每个样本的指数化 logits 的总和除以这些值来归一化结果。这给出了每个样本的类别概率分布。然后,该函数通过使用包含真实类别标签的y变量索引probs数组来计算每个样本正确类别的概率的负对数,该变量包含真实类别标签。使用np.sum函数计算这些负对数概率的总和,并将结果除以样本总数以获得每个样本的平均损失。最后,该函数返回计算出的损失:
def forward_prop(params):
logits = logits_function(params) ①
exp_scores = np.exp(logits) ②
probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True) ②
correct_logprobs = -np.log(probs[range(num_samples), y]) ③
loss = np.sum(correct_logprobs) / num_samples ④
return loss ④
① 获取 softmax 的 logits。
② 应用 softmax 计算类别的概率分布。
③ 计算负对数似然。
④ 计算并返回损失。
为了在整个粒子群上执行正向传播,我们定义以下particle_loss()函数。此函数计算 PSO 群中每个粒子的损失,给定其在搜索空间中的位置。值得注意的是,每个位置代表 NN 参数(w1,b1,w2,b2),其dimension计算如下:
dimension = (n_inputs * n_hidden) + (n_hidden * n_classes) + n_hidden + n_classes = 4 * 10 + 10 * 3 + 10 * 3 + 1 * 10 + 1 * 3 = 83.
一个 NN 参数候选设置(即 PSO 术语中的位置)的例子如下所示:
[ 3.65105185e-01 -9.57472869e-02 4.99475198e-01 2.33703047e-01
5.56295931e-01 6.95323783e-01 8.76045204e-02 5.52892675e-01
3.33363337e-01 5.60680304e-01 3.24233602e-01 3.40402243e-01
2.28940991e-01 6.47396295e-01 2.49476898e-01 -2.15041386e-01
6.61749764e-01 4.50805880e-01 7.31521923e-01 4.55724886e-01
5.81614992e-01 4.21303249e-01 3.10417945e-01 2.80091464e-01
3.63352355e-01 7.21593713e-01 4.11009136e-01 3.50489680e-01
6.82325768e-01 3.60945155e-01 3.34258781e-01 5.53845122e-01
5.39748679e-01 8.45310205e-01 7.38728229e-01 5.44408893e-01
4.22464042e-01 4.45099192e-01 4.36914661e-01 -2.40298612e-02
4.68795601e-01 4.58388074e-01 2.29566792e-01 5.18783606e-01
1.21226363e-01 2.80730816e-01 4.13249634e-01 1.91229505e-01
6.30829496e-01 -4.52777424e-01 1.62066215e-01 3.07603861e-01
1.54565454e-01 5.39974173e-01 4.48241886e-01 -2.81999490e-04
2.93907050e-01 2.58571312e-01 7.87784363e-01 5.06092352e-01
1.85010537e-01 8.06641243e-01 8.30985197e-01 4.06314407e-01
2.20795704e-01 3.25405213e-01 6.02993839e-01 4.21051295e-01
5.24352428e-01 2.49710316e-01 4.99212007e-01 4.48000964e-01
4.90888329e-01 3.94908331e-01 6.35997377e-01 5.91192453e-01
6.16639300e-01 6.85748919e-01 5.40805197e-01 -1.51195135e+00
3.21751027e-01 3.93555680e-01 5.23679003e-01]
然后,PSO 算法可以使用这些损失值来更新粒子的位置,并搜索 NN 的最佳参数集。
def particle_loss(x):
n_particles = x.shape[0] ①
j = [forward_prop(x[i]) for i in range(n_particles)] ②
return np.array(j) ②
① 确定粒子数量。
② 计算并返回每个粒子的损失。
我们需要的最后一个函数是predict,它使用 PSO 群中粒子的位置对应的 NN 参数来预测数据集中每个样本的类别标签。此函数首先调用logits_function来获取网络最后一层的 logits,给定搜索空间中粒子的位置pos。然后,该函数通过使用np.argmax函数计算 logits 的argmax值(即沿第二轴或axis=1),来计算预测的类别标签。这给出了每个样本具有最高概率的类别的索引。最后,该函数将预测的类别标签作为 numpy 数组y_pred返回:
def predict(pos):
logits = logits_function(pos) ①
y_pred = np.argmax(logits, axis=1) ②
return y_pred ②
① 获取网络最后一层的 logits。
② 计算并返回预测的类别标签。
我们现在可以使用 PySwarms 中可用的不同 PSO 来训练神经网络。代码首先设置几个训练样本、输入以及隐藏层和输出的数量。然后根据输入数量、隐藏节点和输出类别计算维度。定义了三种 PSO 变体:globalBest、localBest和binaryPSO。使用名为options的字典设置 PSO 超参数。这些超参数包括惯性权重w、认知参数c1、社会参数c2、要考虑的邻居数量k和 Minkowski 距离参数p(p=1是绝对值之和[或 L1 距离],而p=2是欧几里得[或 L2]距离):
X = penguins[['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm',
➥ 'body_mass_g']].to_numpy() ①
num_samples = X.shape[0] ②
n_inputs = X.shape[1] ②
n_hidden = 10 ②
n_classes = len(np.unique(y)) ②
dimensions = (n_inputs * n_hidden) + (n_hidden * n_classes) + n_hidden + n_
classes ③
PSO_varaints = ['globalBest', 'localBest', 'binaryPSO'] ④
options = {'w':0.79, 'c1': 0.9, 'c2': 0.5, 'k': 8, 'p': 2} ⑤
for algorithm in PSO_varaints: ⑥
if algorithm == 'globalBest':
optimizer = ps.single.GlobalBestPSO(n_particles=150,
➥ dimensions=dimensions, options=options)
cost, pos = optimizer.optimize(particle_loss, iters=2000)
print("#"*30)
print(f"PSO varaints: {algorithm}")
print(f"Best average accuracy: {100*round((predict(pos) == y).mean(),3)} %")
print()
elif algorithm == 'localBest':
optimizer = ps.single.LocalBestPSO(n_particles=150,
➥ dimensions=dimensions, options=options)
cost, pos = optimizer.optimize(particle_loss, iters=2000)
print("#"*30)
print(f"PSO varaints: {algorithm}")
print(f"Best average accuracy: {100*round((predict(pos) == y).mean(),3)} %")
print()
elif algorithm == 'binaryPSO':
optimizer = ps.discrete.BinaryPSO(n_particles=150,
➥ dimensions=dimensions, options=options)
cost, pos = optimizer.optimize(particle_loss, iters=2000)
print("#"*30)
print(f"PSO varaints: {algorithm}")
print(f"Best average accuracy: {100*round((predict(pos) == y).mean(),3)} %")
print()
① 获取特征向量
② 设置训练样本数量、输入、隐藏层和输出。
③ 设置问题的维度。
④ 定义 PSO 的变体。
⑤ 设置 PSO 超参数
⑥ 使用不同的 PSO 变体训练神经网络,并打印最佳准确率。
然后代码通过创建 PSO 优化器实例并调用optimize方法,传入损失函数和要运行的迭代次数iters,依次使用 PSO 的每个变体来训练一个神经网络。优化器找到的最佳损失和粒子位置分别存储在cost和pos中。然后代码打印出所使用的 PSO 变体以及使用相应的粒子位置进行预测并与之比较真实类别标签y所获得的最佳准确率。
运行完整的列表生成以下输出:
##############################
PSO variant: globalBest
Best average accuracy: 99.1 %
##############################
PSO variant: localBest
Best average accuracy: 69.1 %
##############################
PSO variant: binaryPSO
Best average accuracy: 43.8 %
如你所见,globalBest PSO 是训练此神经网络最有效的 PSO 变体。二进制 PSO 与神经网络参数的连续性不匹配。
你可以通过改变问题和算法参数来实验代码。例如,你可以使用如bill_length_mm和flipper_length_mm这样的减少特征集,而不是本代码中使用的四个特征。你也可以更改算法参数并应用速度限制。速度限制是 PySwarms 中启用的一个参数,用于设置速度限制的界限。它是一个大小为 2 的元组,其中第一个条目是最小速度,第二个条目是最大速度。
在下一章中,你将了解到蚁群优化(ACO)和人工蜂群(ABC)作为其他受群体智能启发的有效优化算法。
摘要
-
PSO 采用一种随机方法,利用粒子群体的集体智慧和运动。它基于社会互动的理念,允许高效地解决问题。
-
PSO 的基本原理是在引导群体向搜索空间中的最佳位置移动的同时,记住每个粒子的已知最佳位置,以及群体的全局已知最佳位置。
-
PSO 遵循一个简单的原则:模仿邻近个体的成功。
-
虽然 PSO 最初是为解决具有连续变量的问题而设计的,但许多现实世界的问题涉及离散或组合变量。在这些问题中,搜索空间是有限的,算法需要搜索一系列离散解。为了解决这类问题,已经开发出 PSO 的不同变体,例如二进制 PSO(BPSO)和基于排列的 PSO。
-
通过仔细调整惯性权重、认知和社会加速系数,PSO 可以有效地平衡探索和利用。
第十章:其他要探索的群智能算法
本章涵盖
-
熟悉蚁群优化元启发式算法
-
理解蚁群优化的不同变体
-
理解人工蜂群
-
将这些群智能算法应用于解决连续和离散优化问题
在上一章中,我们探讨了粒子群优化(PSO)算法,但蚁群优化(ACO)和人工蜂群(ABC)也是其他广泛使用的群智能算法,它们从蚂蚁和蜜蜂那里汲取灵感来解决各种优化问题。让我们重新审视寻宝任务,并假设你仍然想采用一种合作和迭代的途径来找到宝藏(在优化问题的情况下,这是最佳解决方案)。你和你的朋友们被分为两个小组:蚂蚁小组和蜜蜂小组。每个小组都有自己独特的寻找宝藏的方式,使用蚁群优化或人工蜂群算法。你可以加入这两个小组中的任何一个。
作为寻宝的蚂蚁,你和你的朋友们将从基地营出发,探索不同的路径以找到宝藏。在探索过程中,你们每个人都会在身后留下特殊的粉笔(信息素)痕迹。路径越有希望,你在这条路径上留下的粉笔就越多。当你的朋友们找到你的粉笔痕迹时,他们可以决定跟随它或者探索一条新的路径。随着时间的推移,最有希望的路径将会有最强的粉笔痕迹,最终整个群体将汇聚到通往宝藏的路径上。
作为寻宝的蜜蜂,你将采用不同的方法。你有觅食蜂和侦察蜂。觅食蜂专注于搜索附近区域,而侦察蜂飞出去随机探索岛屿,寻找通往宝藏的线索。当一只蜜蜂找到有希望的线索时,它会返回基地营并执行“摇摆舞”来向其他朋友(旁观蜂)传达线索的位置和质量。这个过程会一直持续到群体找到通往宝藏的最佳路径。
本章介绍了蚁群优化和人工蜂群优化作为群智能算法。本章讨论了开放旅行商问题、函数优化、路由问题、泵设计以及供需问题,并在附录 C 的补充练习中进行了讨论。
10.1 自然界的微小问题解决者
蚂蚁是微小的生物,它们解决复杂问题的能力甚至超过某些人类。蚂蚁可能体积小,但当他们作为一个群体工作时,他们可以完成一些令人难以置信的壮举。在觅食过程中,它们可以找到通往食物来源的最短路径,建造复杂的隧道,甚至捕食比它们自身大得多的猎物!在巢穴建造过程中,一些蚂蚁从植物和树木上剪下叶子,其他蚂蚁在距离巢穴数百米远的地方觅食叶子,以建造通往觅食地点的高速公路,还有其他蚂蚁用自己的身体形成链条,使它们能够跨越宽阔的缝隙,并将硬叶拉在一起形成巢穴。在后一种情况下,工蚁沿着叶子的边缘形成链条,并一次缩短一个蚂蚁的链条来拉紧叶子的边缘。一旦叶子边缘就位,织叶蚁用它们的颚各夹住一个幼虫,并轻轻挤压幼虫以产生丝线,这种丝线被用来粘合叶子的边缘。
关于强大的蚂蚁的有趣事实
-
蚂蚁在大约一亿年前出现在地球上,使它们成为地球上最古老的昆虫群体之一。
-
蚂蚁的当前总估计人口为 10 的 16 次方个体。据估计,蚂蚁的总重量与人类的总重量处于同一数量级。
-
蚂蚁对于它们的体型来说非常强壮。一些种类可以携带重量是它们自身体重 50 倍的物体!为了更直观地理解这一点,这就像一个人携带一辆汽车一样!
-
大约 2%的昆虫是社会的。大约有 12,000 种不同的蚂蚁,大多数蚂蚁都是社会性昆虫。
-
蚂蚁被认为是世界上人口密度最高的生物。它们生活在由 30 个到数百万个个体组成的群体中。一些群体,如 Formica Yesensis,大约有 1,080,000 个蚁后和 306,000,000 个工蚁,生活在 2.7 平方公里的区域内,45,000 个巢穴相互连接。
-
蚂蚁使用信息素作为它们主要的聚群通信媒介。然而,蚂蚁还使用其他形式的通信,包括视觉、听觉和触觉通信。例如,一些蚂蚁种类使用声音来相互沟通。这些声音可以从简单的点击声和爆裂声到更复杂的信号,这些信号传达有关食物来源、巢穴位置和其他重要信息。一些蚂蚁种类产生的声音在人类的可听范围内(20Hz 到 20kHz)。例如,切叶蚁在相互沟通时会产生点击声。这些点击声的频率可以从 1 到 10 千赫兹。其他蚂蚁种类产生的声音超出了人类的听觉范围。例如,一些军蚁产生超声波,可以用来定位猎物或相互沟通。如果你感兴趣,可以在 YouTube 上查看“蚂蚁发出什么声音?”的视频(
mng.bz/aEKo)。
蚂蚁是一种简单的刺激-反应生物,单独无法完成复杂任务。然而,作为一个群体,蚂蚁展现出惊人的能力,能够在没有任何计划、中央控制器、集中监督或直接沟通的情况下执行复杂任务。蚂蚁采用一种称为群居通信的间接沟通机制。群居性是法国生物学家皮埃尔-保罗·格雷斯在 1959 年提出的一个概念,它是一种涉及环境修改的社会性昆虫之间的间接沟通方法。这些环境修改作为昆虫之间的外部或共享记忆。
蚂蚁使用信息素作为它们主要的群居通信媒介。当它们往返于食物来源地时,会在路径上留下信息素。其他蚂蚁可以检测到这些信息素,这会影响它们在选择路径时的决策。这使得蚂蚁能够作为一个紧密的单元共同工作,完成诸如从巢穴到食物来源地以及相反方向的最短路径寻找等复杂任务。没有直接的沟通或中央控制器使得蚂蚁的行动看起来几乎像是某种形式的集体智慧所协调。本质上,群居通信的现象使得像蚂蚁这样的社会性昆虫能够利用它们的集体知识和行为来完成超出个体能力的任务。
蚂蚁群体优化(ACO)灵感来源于蚂蚁的觅食行为。当它们觅食时,蚂蚁最初会在巢穴区域周围随机探索。一旦蚂蚁发现食物来源,它会携带一些食物返回巢穴,同时在路径上留下信息素轨迹。然后其他蚂蚁会跟随信息素轨迹到达食物来源,如图 10.1 所示。随着越来越多的蚂蚁跟随信息素轨迹到达食物来源,信息素轨迹的强度增加,对其他蚂蚁更具吸引力。相反,由于信息素轨迹不是固定的,并且会随着时间的推移逐渐蒸发,较长路径上的信息素轨迹会蒸发。最终,一条单独的信息素轨迹变得主导,大部分蚂蚁都会跟随这条轨迹往返于食物来源地。通过这种方式,蚂蚁可以通过集体或群体智能的过程找到巢穴和食物来源地之间的最短路径。

图 10.1 蚂蚁觅食过程。觅食的蚂蚁在其返回巢穴的路径上留下信息素轨迹。其他蚂蚁很可能会跟随具有更强信息素轨迹的路径,以到达发现的食品来源。
正如我在上一章中解释的,对群体智能算法的研究大多数最初是基于实验观察。为了理解蚂蚁在觅食过程中的集体行为,并为 ACO 算法推导启发式方法,进行了两个著名的实验:二进制桥梁实验和长度不等桥梁实验。
二进制桥梁实验是为了观察蚂蚁在面临两条连接巢穴到食物源等长的桥梁时的行为(图 10.2a)。实验旨在研究蚂蚁如何确定最佳路径以及它们如何随时间调整行为。最初,蚂蚁随机选择两条桥梁中的一条。随着蚂蚁在巢穴和食物源之间往返,它们会在所走的路径上留下信息素。随着时间的推移,更多的蚂蚁跟随信息素浓度更高的路径,这使得路径对其他蚂蚁更具吸引力。最终,两条桥梁中的一条成为主导,大多数蚂蚁使用它来在巢穴和食物源之间旅行。蚂蚁的决策过程基于正反馈原理,蚂蚁强化了信息素浓度最高的路径,使其对其他蚂蚁更具吸引力。

图 10.2 a) 二进制桥梁实验;b) 长度不等桥梁实验
长度不等桥梁实验(图 10.2b)是二进制桥梁实验的扩展,其中桥梁的一支比另一支长。这个实验的目的是观察蚂蚁在面临两条不同长度的路径时的行为调整。实验表明,蚂蚁倾向于选择较短的路径而不是较长的路径。这是因为沿着较短路径旅行的蚂蚁比沿着较长路径旅行的蚂蚁更早返回巢穴。因此,较短路径上的信息素路径比较长路径上的信息素路径更快得到强化,使其对其他蚂蚁更具吸引力。这种强化行为被称为自催化行为。
信息素在蚂蚁的集体行为中的作用可以总结为以下要点:
-
信息素路径作为蚂蚁通过感知和记录它们的觅食经验来沟通的集体记忆。
-
随着时间的推移,信息素路径会蒸发,引入环境变化,这些变化可以影响蚂蚁的行为。
-
路径上的信息素浓度代表了一个反馈信号,它影响着蚂蚁的决策过程。
现在我们深入探讨 ACO 元启发式算法。
10.2 ACO 元启发式算法
蚂蚁群优化(ACO)通过一组“人工蚂蚁”搜索问题的最佳解决方案来模拟真实蚂蚁群体的行为。这些人工蚂蚁留下“信息素路径”来相互沟通,就像真实蚂蚁一样,最终收敛到最佳解决方案。
为了模拟蚂蚁的行为,让我们假设我们有一个巢穴和一个通过两条不同长度 L[1] 和 L[2] 连接的食物来源,就像长度不等的桥梁的情况。现在,让我们分配一个计算参数 τ 来表示蚂蚁留下的信息素。我们最初将相同的信息素值分配给每条路径:τ[1] = τ[2],如图 10.3 所示。然后,我们开始在巢穴放置 m 只蚂蚁。让我们假设这些人工蚂蚁完全模仿真实蚂蚁,并根据信息素浓度做出决策,但没有路径长度的任何知识。对于每只蚂蚁 k,这只蚂蚁以概率穿越路径 1:
|

| 10.1 |
|---|
因此,这只蚂蚁以概率 p[2] = 1 – p[1]穿越路径 2。

图 10.3 ACO 模拟
由于 τ[1] = τ[2],蚂蚁 k 将随机选择两条路径之一,因为两条路径被穿越的概率相同。在穿越所选路径后,每条路径上的信息素浓度需要更新。这种信息素更新包括两个阶段:蒸发和沉积。在蒸发阶段,信息素浓度 τ 按以下方式递减:
|

| 10.2 |
|---|
其中 ρ 指定了蒸发率。图 10.4 显示了基于 NetLogo 模拟的觅食过程中信息素蒸发率的影响。NetLogo 是一个多智能体可编程建模环境,用于模拟自然和社会现象。它允许用户创建、实验和分析复杂系统的模拟,如生态系统、经济和社会网络。蚂蚁的觅食行为在 NetLogo 的蚂蚁模型(ccl.northwestern.edu/netlogo/models/Ants)中显示。

图 10.4 食物觅食过程中蒸发率的影响。在模拟中,蚂蚁从中心巢穴开始寻找食物,巢穴周围有三个以块状表示的食物来源。信息素路径用白色表示。蚂蚁发现食物后,将其运回巢穴,留下化学路径。然后,其他蚂蚁跟随这条路径,嗅到气味,将其引导到食物来源。随着更多蚂蚁继续取回食物,它们会加强化学路径。
如您所见,如果蒸发率设置为 0,信息素路径将永远不会蒸发,蚂蚁将反复跟随相同的路径。这将导致蚂蚁陷入局部最优,它们将无法探索其他路径或找到更好的解决方案。另一方面,如果蒸发率设置为 1,信息素路径将以最大速率蒸发,这意味着蚂蚁将无法跟随任何路径,它们将被迫随机探索环境。这可能导致缓慢收敛到最优解。
在沉积阶段,每只蚂蚁在其经过的路径上留下更多的信息素。图 10.5 展示了用于信息素更新的不同方法:
- 在线逐步信息素更新—每只蚂蚁在其经过的路径上沉积一定量的信息素。这将增加另一只蚂蚁选择相同边的概率:
|

| 10.3 |
|---|
对于选择Δτ值的方法有多种。遵循蚂蚁密度模型,蚂蚁将一个常数量Q加到每个经过的边上。这意味着最终添加到边上的信息素将与选择该边的蚂蚁数量成比例。边上交通密度越高,该边作为最终解决方案组成部分的吸引力就越大。这种方法不考虑解决方案的质量(即边长)。在蚂蚁数量模型中,沉积的信息素量与蚂蚁获得的解决方案质量成比例。例如,蚂蚁在节点i和j之间穿越时,将沉积量Q/d[ij],其中d[ij]是i和j之间的距离。在这种情况下,只使用局部信息d[ij]来更新信息素浓度。成本较低的边被赋予更高的吸引力。
- 在线延迟信息素更新(或蚂蚁循环模型)—一旦蚂蚁构建了解决方案,它将重新走一遍路径,并根据解决方案的质量更新其经过的边上的信息素路径。沉积的信息素量由蚂蚁获得解决方案的质量决定如下:
|

| 10.4 |
|---|
其中Q是一个常数,L^k是蚂蚁k构建的路径长度。对于对应路径的每个边(i,j),在所有蚂蚁完成它们的巡回之后,沉积的总信息素量将是
|

| 10.5 |
|---|
其中m是蚂蚁的数量。

图 10.5 信息素更新方法
总结来说,在在线逐步信息素更新中,蚂蚁在从节点i移动到节点j时,会在边(i,j)上更新信息素轨迹τ[ij]。在在线延迟信息素更新中,一旦构建了一条路径,蚂蚁可以沿着相同的路径反向追踪并更新经过的边上的信息素轨迹。选择哪种方法取决于要解决的问题。在线逐步信息素更新和在线延迟信息素更新的任何组合也是可能的。
10.3 ACO 变体
ACO 已被用于解决各种优化问题,如车辆路径问题、调度问题和最优分配问题。多年来,已经开发了该算法的几个变体,如图 10.6 所示。

图 10.6 ACO 变体的示例
这些变体有不同的优缺点,变体的选择取决于要解决的问题的具体情况。在以下小节中,我们将讨论这些变体中的一些。
10.3.1 简单 ACO
简单 ACO(SACO)是双桥实验的算法实现。考虑在图上找到两个节点之间最短路径的问题,如图 10.7 所示。

图 10.7 最短路径问题
让我们使用 SACO 来解决这个问题。在每条边上,我们将分配一个小的随机值来表示初始信息素浓度,τ[ij](0)。然后,我们将放置一定数量的蚂蚁,k = 1,…,m在源节点上。
对于 SACO 的每一次迭代,每只蚂蚁都会逐步构建到目标节点的路径(解)。最初,蚂蚁会随机选择下一步要跟随的边。后来,每只蚂蚁将执行决策策略以确定路径的下一个边。在每个节点i,蚂蚁可以选择移动到与之相连的任何j个节点,基于以下转换概率:
|

| 10.6 |
|---|
其中
-
N[i]^k是相对于蚂蚁k与节点i相连的可行节点集合。
-
τ[ij]是从状态i到状态j沉积的信息素量。
-
η[ij]是一个启发式值,表示状态转换ij(先验知识,通常是 1/d[ij],其中d是距离)的期望性。
-
α ≥ 0 是一个参数,用于控制τ[ij]的影响。α用于放大信息素的影响。α的较大值会给信息素赋予过多的重视,尤其是初始的随机信息素,这可能导致快速收敛到次优路径。
-
β ≤ 1 是一个参数,用于控制边η[ij]的期望性的影响。
在最短路径问题中,假设我们使用五只蚂蚁,初始信息素值为 0.5,α = β = 1。第一只蚂蚁(k = 1),放置在源节点,有两个相邻节点{2,3},如图 10.8 所示。

图 10.8 第一只蚂蚁位于源节点 1,其邻居为 2 和 3。每条边上都有两个用冒号分隔的数字。第一个数字代表边的长度,第二个数字代表边上的当前信息素浓度。
考虑边长倒数作为边的吸引力,这只蚂蚁需要通过以下转换概率在节点 2 和 3 之间进行选择:
|

| 10.7 |
|---|
|

| 10.8 |
|---|
其中 p[12]¹ 是蚂蚁 1 在节点 1 选择节点 2 的概率,p[13]¹ 是选择节点 3 的概率。然后我们在 0 到 1 之间生成一个随机数 r。如果 p[13]¹ ≥ r,我们选择节点 3;否则,我们选择 2。由于节点 3 被选中的概率最高,它最有可能被选中。
继续前进,第一只蚂蚁现在位于节点 3,需要根据相同的转换概率在相邻节点 2 和 4 之间进行选择,这导致 p[32]¹ = 0.29 和 p[34]¹ = 0.71。假设选择了节点 4。蚂蚁现在位于节点 4,需要根据相同的转换概率在相邻节点 2 和 5 之间进行选择,这导致 p[42]¹ = 0.6 和 p[45]¹ = 0.4。假设根据生成的随机数选择了节点 5。图 10.9 显示了第一只蚂蚁在第一次迭代中完成的路径,长度 L¹(t = 1) = 3 + 2 + 6 = 11。每只蚂蚁将按照相同的步骤生成自己的路径。

图 10.9 第一只蚂蚁构建的路径。其他四只蚂蚁将类似地构建路径。
在开始新的迭代之前,需要更新信息素。根据方程 10.2 并假设蒸发率 ρ 为 0.7,新的信息素值将是
|

| 10.9 |
|---|
信息素也会被沉积。如果第一只蚂蚁 k = 1 根据每只蚂蚁找到的路径的成本来选择沉积信息素,它将根据在线延迟信息素更新模型,以 Q/L¹ = 1/11 的值强制执行边 {1,3}、{3,4} 和 {4,5}。图 10.10 显示了每条边上的更新信息素值。

图 10.10 更新的信息素浓度
在这个简单的例子中,经过三次迭代,蚂蚁找到了最短路径 1 → 3 → 4 → 5。在接下来的章节中,我们将讨论蚂蚁系统(AS)算法、蚁群系统(ACS)算法和最大-最小蚂蚁系统(MMAS)算法作为 ACO 变体,这些算法被提出以处理 SACO 的限制。
10.3.2 蚂蚁系统
蚂蚁系统(AS)算法通过添加一个禁忌列表来增加记忆能力,从而改进了 SACO 算法。这个列表,或者说是蚂蚁的记忆,标识了已经访问过的节点。AS 中使用的转移概率与方程 10.6 中的相同。当蚂蚁访问一个新节点时,该节点会被添加到蚂蚁的禁忌列表中,持续一定数量的迭代。和在 SACO 中一样,当蚂蚁完成一条路径后,每条边上的信息素都会被更新。蚂蚁密度、蚂蚁数量和蚂蚁循环模型可以用来更新信息素。正如之前解释的,在蚂蚁密度和蚂蚁数量模型中,蚂蚁在构建过程中会沉积信息素,而在蚂蚁循环模型中,蚂蚁在构建完整路径后会沉积信息素。
10.3.3 蚂蚁群体系统
蚂蚁群体系统(ACS)算法是 AS 算法的一个扩展,它使用了一个修改过的转移规则,并利用了精英策略。这种策略被称为伪随机比例行动规则,旨在提高算法的效率和有效性。ACS 中使用的伪随机比例行动规则基于这样的想法:蚂蚁找到的最佳解应该更多地影响决策过程。在 ACS 中,生成一个随机数 r,参数 r[o] ∈ [0,1] 是预定义的。位于节点 i 的蚂蚁 k 使用以下具有双重功能的决策规则选择下一个要移动到的节点 j:
- 如果 r ≤ r[o],蚂蚁会选择节点 j
|

| 10.10 |
|---|
- 否则,根据以下转移概率以概率方式选择一个节点(例如,使用在第七章中学到的轮盘赌方法):

注意,与 SACO 的转移概率(方程 10.6)相比,ACS 中控制信息素浓度影响的参数 α = 1。参数 r[0] 用于平衡探索-利用的权衡。当 r ≤ r[0] 时,决策规则通过优先选择最佳边来利用关于问题的现有知识,而当 r > r[0] 时,算法进行探索。适当地调整 r[0] 可以在探索和利用之间取得平衡。
在之前的 shortest path 示例中,假设蚂蚁位于节点 4,需要根据 ACS 决策规则(图 10.11)选择节点 2 或 5。假设我们有 r[o] = 0.5, β = 1, 和 η[ij] = 1 / d[ij]。现在让我们生成一个随机数 r。
如果 r ≤ r[o],蚂蚁将选择节点
|

| 10.11 |
|---|
如果 r > r[o],蚂蚁将选择具有最大转移概率的节点:p[45]¹ = 0.6 和 p[42]¹ = 0.4,如之前在 10.3.1 节中计算,α = 1。使用轮盘赌方法,节点 2 或节点 5 可能被选中。

图 10.11 蚂蚁在节点 4 根据 ACS 精英策略选择下一个节点(2 或 5)。
与 AS 不同,ACS 中的信息素强化过程仅由具有全局最佳解的蚂蚁执行,这对应于迄今为止找到的最佳路径。然而,仅依靠全局最佳解来指导信息素沉积可能会导致搜索过于迅速地收敛到迄今为止的全局最佳解,阻碍了对其他潜在更好解决方案的探索。为了解决这个问题,开发了最大-最小蚁群系统(MMAS)算法。
10.3.4 最大-最小蚁群系统
ACS 可能会遇到过早停滞的问题,这发生在所有蚂蚁都遵循相同路径且探索很少的情况下。这个问题在复杂问题中尤为普遍,其中搜索空间很大,最优解难以找到。为了克服这个问题,提出了最大-最小蚁群系统(MMAS)。
MMAS 使用迭代最佳路径而不是全局最佳路径来更新信息素。信息素路径仅使用在线延迟信息素更新模型进行更新,其中当前迭代中最佳蚂蚁所经过的边会接收额外的信息素。由于最佳路径在迭代之间可能差异很大,这种方法与 ACS 相比,在整个搜索空间中促进了更高程度的探索。也可以实现混合策略,其中迭代最佳路径主要用于鼓励探索,而全局最佳路径则定期纳入。
在 MMAS 中,信息素浓度被限制在上下界(τ[max])和(τ[min])之间,确保搜索既保持专注又具有灵活性。信息素路径被初始化为最大值τ[max],如果算法达到停滞点,所有信息素浓度将被重置为最大值。在此重置之后,仅使用迭代最佳路径进行有限的迭代次数。τ[min]和τ[max]的值通常通过实验确定,尽管如果已知最优解,它们也可以通过解析计算得出。
10.3.5 使用 ACO 解决开放 TSP 问题
现在我们来实现 ACO 算法来解决开放 TSP 问题,考虑 20 个主要美国城市。我们的目标是找到销售人员可以遵循的最短路线,以访问这 20 个城市中的每一个,从纽约市出发,且不返回到家乡城市。
我们将首先定义一个包含 20 个美国城市名称及其纬度和经度坐标的cities字典。然后,我们将使用嵌套循环计算每对城市之间的距离,使用haversine距离公式,并将结果存储在distance_matrix字典中。haversine距离被使用,因为它考虑了地球的曲率,提供了地球表面上两点之间准确的距离测量(有关更多详细信息,请参阅第 4.3.3 节中的“Haversine 距离”侧边栏)。cost_function被定义为计算路径的总距离。它接受城市索引列表(path)和距离矩阵(distances)作为输入参数。然后函数遍历路径,计算每对连续城市之间的距离。然后返回总路径距离。此代码在下一列表中展示。
列表 10.1 使用 ACO 解决最短路径问题
import numpy as np
import pandas as pd
from collections import defaultdict
from haversine import haversine
import networkx as nx
import matplotlib.pyplot as plt
import random
from tqdm import tqdm
cities = {
'New York City': (40.72, -74.00),
'Philadelphia': (39.95, -75.17),
'Baltimore': (39.28, -76.62),
'Charlotte': (35.23, -80.85),
'Memphis': (35.12, -89.97),
'Jacksonville': (30.32, -81.70),
'Houston': (29.77, -95.38),
'Austin': (30.27, -97.77),
'San Antonio': (29.53, -98.47),
'Fort Worth': (32.75, -97.33),
'Dallas': (32.78, -96.80),
'San Diego': (32.78, -117.15),
'Los Angeles': (34.05, -118.25),
'San Jose': (37.30, -121.87),
'San Francisco': (37.78, -122.42),
'Indianapolis': (39.78, -86.15),
'Phoenix': (33.45, -112.07),
'Columbus': (39.98, -82.98),
'Chicago': (41.88, -87.63),
'Detroit': (42.33, -83.05)
} ①
distance_matrix = defaultdict(dict) ②
for ka, va in cities.items(): ②
for kb, vb in cities.items(): ②
distance_matrix[ka][kb] = 0.0 if kb == ka else haversine((va[0], ②
➥ va[1]), (vb[0], vb[1])) ②
②
distances = pd.DataFrame(distance_matrix) ③
city_names=list(distances.columns) ④
city_indices = {city: idx for idx, city in enumerate(city_names)}
city_count = len(city_names)
def cost_function(path): ⑤
distance = 0
for i in range(len(path) - 1):
city1, city2 = city_names[path[i]], city_names[path[i + 1]]
distance += haversine(cities[city1], cities[city2])
return distance
① 定义 20 个主要美国城市的纬度和经度。
② 基于纬度和经度坐标创建一个哈夫曼距离矩阵。
③ 城市间值
④ 城市名称
⑤ 定义表示路径长度的成本函数。
作为列表 10.1 的延续,接下来的代码片段展示了一个名为ant_tour的函数,它接受两个参数:pheromones,表示城市间的信息素水平,以及distances,表示城市间的距离。它初始化一个paths数组以存储每个蚂蚁的路径,并遍历指定的蚂蚁范围。对于每个蚂蚁,它从纽约市开始初始化一个路径。它进入一个while循环,直到所有城市都被访问。在while循环中,它将当前城市作为路径中的最后一个城市。然后,它根据信息素水平和当前城市与未访问城市之间的距离的倒数计算选择下一个城市的概率。概率使用方程 10.6 计算。下一个城市使用基于归一化概率的random.choices函数选择。选定的下一个城市将从未访问城市列表中移除,并将其追加到路径中:
def ant_tour(pheromones):
paths = np.empty((ants, city_count), dtype=int) ①
for ant in range(ants):
path = [city_indices['New York City']] ②
unvisited_cities = set(range(city_count)) ③
unvisited_cities.remove(path[0]) ④
while unvisited_cities: ⑤
current_city = path[-1]
probabilities = []
for city in unvisited_cities: ⑥
tau = pheromones[current_city, city]
eta = (1 / distances[current_city, city])
probabilities.append((tau** alpha)*(eta ** beta))
probabilities /= sum(probabilities) ⑦
next_city = np.random.choice(list(unvisited_cities), ⑧
p=probabilities)
unvisited_cities.remove(next_city) ⑨
unvisited_cities.remove(next_city) ⑩
path.append(next_city) ⑪
paths[ant] = path
return paths
① 初始化一个数组以存储每个蚂蚁的路径。
② 从纽约市开始每个蚂蚁的路径。
③ 初始化一组未访问城市。
④ 从未访问城市中移除纽约市。
⑤ 继续构建路径,直到所有城市都被访问。
⑥ 计算移动到每个未访问城市的概率。
⑦ 归一化概率。
⑧ 根据概率选择下一个城市。
⑨ 从未访问城市集合中移除选定的城市。
⑩ 将选定的城市添加到路径中。
⑪ 为当前蚂蚁存储完成路径。
一旦所有城市都被访问过,当前蚂蚁的路径将被存储在paths数组中。当所有蚂蚁完成它们的路径后,函数返回包含每个蚂蚁找到的最优路线的paths数组。
下面的update_pheromones函数用于根据蚂蚁的距离和路径更新信息素水平:
def update_pheromones(paths, pheromones):
delta_pheromones = np.zeros_like(pheromones) ①
for i in range(ants): ②
for j in range(city_count - 1):
city1_idx, city2_idx = paths[i, j], paths[i, j + 1] ③
delta_pheromones[city1_idx, city2_idx] += Q / cost_ ④
function(paths[i])
return (1 - evaporation_rate) * pheromones + delta_pheromones ⑤
① 初始化一个矩阵以存储信息素水平的变化。
② 根据蚂蚁走过的路径更新信息素。
③ 获取当前路径中城市的索引。
④ 更新当前城市和下一个城市之间的信息素水平。
⑤ 蒸发现有信息素,添加信息素的变化,并返回更新后的信息素。
此函数接受两个参数:paths,表示蚂蚁走过的路径,以及pheromones,表示城市之间边上的当前信息素水平。它初始化一个名为delta_pheromones的矩阵来存储信息素水平的变化。此矩阵的形状与pheromones矩阵相同。它遍历指定范围内的每个蚂蚁。在循环内部,它遍历蚂蚁路径中的每个城市(除了最后一个城市)。对于每对连续城市,它通过添加基于蚂蚁路径成本的倒数来更新delta_pheromones矩阵。在内循环之后,它通过结合现有信息素、考虑蒸发并添加存储在delta_pheromones中的变化来计算更新的信息素。最后,它返回更新后的信息素矩阵。
作为延续,以下代码片段显示了run_ACO函数,它接受以下输入:
-
distances—一个二维数组(矩阵),存储城市之间的距离 -
ants—算法中使用的蚂蚁数量 -
iterations—迭代次数 -
alpha—一个参数,用于控制信息素轨迹对蚂蚁决策的影响 -
beta—一个参数,用于控制下一个城市距离对蚂蚁决策的影响 -
evaporation_rate—信息素从路径中蒸发的速率 -
Q—在计算蚂蚁沉积信息素量时使用的常数
此函数返回best_path和best_distance,代表蚁群优化算法找到的最优解:
def run_ACO(distances, ants, iterations, alpha, beta, evaporation_rate, Q):
pheromones = np.ones((city_count, city_count)) ①
best_path = None
best_distance = float('inf')
for _ in tqdm(range(iterations), desc="Running ACO", unit="iteration"):
paths =ant_tour(pheromones, distances) ②
distances_paths = np.array([cost_function(path) for path in paths]) ③
min_idx = distances_paths.argmin()
min_distance = distances_paths[min_idx]
if min_distance < best_distance: ④
best_distance = min_distance
best_path = paths[min_idx]
pheromones = update_pheromones(paths, pheromones) ⑤
return best_path, best_distance ⑥
① 初始化信息素数组。
② 为每个蚂蚁生成路径。
③ 计算每条路径的总距离。
④ 找到距离最短的路径索引。
⑤ 更新信息素。
⑥ 返回迭代过程中找到的最佳路径和距离。
现在让我们使用以下参数将蚁群优化算法(ACO)应用于解决最短路径问题:
ants = 30 ①
iterations = 100 ①
alpha = 1 ①
beta = 0.9 ①
evaporation_rate = 0.5 ①
Q = 100 ①
best_path, best_distance = run_ACO(distances.values, ants, iterations, alpha, beta,
➥ evaporation_rate, Q) ②
① 设置 ACO 参数。
② 使用定义的参数运行蚁群优化算法(ACO)。
由于算法中包含随机性,您的解决方案可能会有所不同。以下路径是在我运行求解器时生成的:
Route: New York City → Philadelphia → Baltimore → Detroit → Chicago → Indianapolis → Columbus → Charlotte → Jacksonville → Memphis → Fort Worth → Dallas → Houston → Austin → San Antonio → Phoenix → San Diego → Los Angeles → San Jose → San Francisco
Route length: 7937.115
前面的路径在图 10.12 中显示。列表 10.1 的完整版本可在本书的 GitHub 仓库中找到,其中还包含生成此可视化的代码。

图 10.12 蚁群优化算法获得的最短路径
与遗传算法和粒子群优化算法不同,ACO 元启发式算法没有成熟和全面的 Python 包。ACOpy 项目(https://acopy.readthedocs.io/en/latest/index.html)提供了一个 ACO 的实现,可以使用 pip 安装,如下所示:pip install acopy。作为列表 10.1 的延续,让我们使用 ACOpy 来解决最短路径问题。
我们首先导入acopy和networkx库。创建一个图G,其中节点代表城市,边代表它们之间的距离。distance_matrix包含每对城市之间的距离。循环遍历所有城市对,在图中为每对城市添加一条边,边的权重是城市之间的距离。然后从图中删除自环边(连接节点到自身的边):
import acopy
import networkx as nx
G=nx.Graph()
for ka, va in cities.items():
for kb, vb in cities.items():
G.add_weighted_edges_from({(ka,kb, distance_matrix[ka][kb])})
G.remove_edges_from(nx.selfloop_edges(G))
然后定义 ACO 算法的参数:evaporation_rate(蒸发率)、iterations(迭代次数)和Q,如前所述。创建一个具有指定evaporation_rate和Q的 ACO 求解器。使用alpha和beta参数初始化acopy.Colony对象。然后算法按照指定的迭代次数进行迭代。在每次迭代中,使用求解器的solve方法找到一个路径,该路径是一个边的列表。对于路径中的每条边,代码确定尚未添加到path_indices列表中的城市,并将其添加进去。最后,将旅行路径更新为path_indices列表,该列表是城市名称的列表,而不是边的列表:
evaporation_rate = 0.5 ①
iterations = 100 ①
Q = 100 ①
solver = acopy.Solver(rho=evaporation_rate, q=Q) ②
colony = acopy.Colony(alpha=1, beta=0.9) ③
for n_iter in range(iterations): ④
tour = solver.solve(G, colony, limit=4)
path_indices = ['New York City'] ⑤
for edge in tour.path:
next_city = edge[0] if edge[1] == path_indices[-1] else edge[1] ⑥
if next_city not in path_indices: ⑥
path_indices.append(next_city) ⑥
tour.path=path_indices ⑦
① ACO 参数
② 设置 ACO 求解器。
③ 使用 alpha 和 beta 参数设置 ACO 群体。
④ 运行 ACO 算法。
⑤ 从城市 0(纽约市)开始。
⑥ 添加路径中尚未包含的边的另一个节点。
⑦ 返回包含在路径中的城市名称的有序列表。
现在我们按照以下方式打印出获得路径及其长度:
best_path = tour.path
best_distance = tour.cost
Route = " → ".join(best_path)
print("Route:", Route)
print("Route length:", np.round(best_distance, 3))
best_path变量被设置为通过acopy求解器获得的tour对象的path属性。这条路径代表找到的最短路线的城市列表。best_distance变量被设置为tour对象的cost属性,即最佳路径的总距离(或成本)。Route变量是一个字符串,它将best_path中的所有城市通过箭头(→)连接起来,表示在最佳旅行中的访问城市顺序。最后,print语句显示最佳路线及其总距离。在运行求解器后,将生成如下路径:
Route: New York City → Columbus → Detroit → Philadelphia → Baltimore → Charlotte → Jacksonville → Memphis → Houston → Dallas → Fort Worth → Austin → San Antonio → Phoenix → San Diego → Los Angeles → San Jose → San Francisco → Chicago → Indianapolis
Route length: 11058.541
获得的路径如图 10.13 所示。书中 GitHub 仓库中可用的列表 10.1 的完整版本包含生成此可视化的代码。

图 10.13 ACOpy 获得的最短路径
值得注意的是,ACO(蚁群优化算法)与其他许多随机优化算法一样,包含随机性元素。ACO 中的随机性主要来源于两个方面:
-
初始条件——在算法开始时,蚂蚁通常被放置在随机位置,除非起始位置是预定义的,例如在 TSP 的情况下,蚂蚁从预定的家乡城市开始。这意味着在随机位置被使用的情况下,每只蚂蚁都会从不同的城市开始探索,导致路径的多样性。
-
路径选择——随着蚂蚁从一个城市移动到另一个城市,它们会概率性地选择下一个要访问的城市。这个选择受到通往城市路径上信息素的量和城市距离的影响。即使两只蚂蚁在同一城市并且拥有相同的信息,它们也可能因为这种概率性选择而选择不同的城市进行访问。
这种固有的随机性意味着 ACO 算法的每次运行都可以产生不同的结果。然而,在多次运行中,ACO 应该始终找到近似最优解,即使它们并不总是完全相同的解。
在下一节中,我们将深入探讨另一个由群体智能产生的迷人算法。这个算法再次从自然界中汲取灵感,特别是蜜蜂寻找食物的行为。你很快就会了解这个受蜜蜂启发的算法是如何运作的,以及它如何在计算环境中得到应用。
10.4 从蜂巢到优化
蜜蜂是著名的社交昆虫,以其非凡的合作而闻名。它们建造的蜂巢可以容纳大约 30,000 只蜜蜂,所有蜜蜂和谐共处。每只蜜蜂都有指定的任务,例如生产蜂蜡、制作蜂蜜、制作蜂粮、形成蜂房或把水带到蜂房并与蜂蜜混合。年轻的蜜蜂通常处理蜂巢外的任务,而年老的蜜蜂则专注于室内工作。
蜜蜂群体作为目标导向的决策系统运作,其功能由单个蜜蜂的分散控制和行动所指导。在觅食过程中,蜜蜂之间的合作产生了有利于蜂巢整体适应性的行为。通过使用单个觅食者,蜜蜂群体旨在最小化成本/收益比,而不是无差别地在所有方向上消耗能量进行搜索。他们将觅食努力集中在最有回报的区域,而忽略那些质量较差的区域。
观察表明,当蜂群食物资源稀缺时,觅食者会表现出增加招募行为的特征,这表现在它们返回蜂巢后舞蹈模式的变化。这种增强的招募有助于动员更多的巢居者去利用可用的食物来源。除了觅食外,蜜蜂还参与各种其他任务,如蜂巢建设、蜂巢温度调节和蜂群防御,展示了它们非凡的团队合作技能。
探索蜜蜂的迷人世界
-
蜜蜂是人们最熟知且最重要的生产人类食用食物的昆虫。
-
蜜蜂群体由一只蜂王、数百只雄性工蜂和 20,000 到 80,000 只雌性工蜂组成。
-
单个工蜂每天可能访问 50 到 1,000 朵花。同一蜂巢的蜜蜂一天内可以访问多达 225,000 朵花。蜜蜂可以以 21 到 28 公里/小时(13-17 英里/小时)的速度飞行,觅食区域可达 70 平方公里(27 平方英里)。
-
蜜蜂可以在蜂巢中维持大约 33°C(91°F)的恒定温度,无论外界温度如何。
-
蜜蜂选择六角形形状来建造蜂巢,以存放蜂王的卵和储存工蜂带到蜂巢的花粉和蜂蜜。
-
六角形结构有几个优点,例如空间利用效率高(在给定区域内能建造的最大数量的蜂房),结构强度大(坚固且稳定),材料效率高(使用较少的蜂蜡),以及最佳角度(略微倾斜,大约 13 度从水平线,以防止蜂蜜从蜂房中溢出,同时允许蜜蜂轻松移动)。
-
蜜蜂通过复杂的舞蹈动作“摇摆舞”相互沟通,这在乔治亚理工学院计算机学院的视频《蜜蜂的摇摆舞》(
mng.bz/gvxx)中有解释。
人工蜂群(ABC)算法是一种基于蜜蜂觅食行为的群体智能算法。具体来说,它受到蜜蜂寻找食物来源和将发现传达给其他蜜蜂以优化资源收集方式的影响。让我们首先看看蜜蜂是如何觅食食物的。图 10.14 展示了它们觅食行为的步骤。

图 10.14 蜜蜂的觅食行为
觅食行为可以总结为以下步骤:
-
初始化——觅食蜂(被雇佣的蜂)和侦察蜂开始寻找食物来源。觅食蜂通常从蜂巢周围的已知来源收集资源以满足蜂群的即时需求。侦察蜂定位新的食物来源以确保蜂群的长期生存,特别是如果蜂巢周围的食物来源开始枯竭。侦察蜂只占蜂群成员的一小部分,但它们为蜂群节省了许多寻找丰富新食物来源的飞行距离。值得注意的是,觅食蜂和侦察蜂都是工蜂(雌蜂)。根据蜂群的需求和食物来源的可用性,工蜂可以从觅食蜂转变为侦察蜂。总之,觅食蜂专注于利用现有资源,而侦察蜂则专注于探索以发现新资源。
-
探索——觅食蜂离开蜂巢开始寻找食物来源,如周围的花朵中的花蜜和花粉。侦察蜂探索更远的地方以发现新的食物来源。
-
检测—当找到合适食物来源时,工蜂降落在花朵上,并开始在她的蜜胃中收集花蜜或在她的后腿上收集花粉。
-
记忆—蜜蜂会注意食物来源的位置,包括其与蜂巢的距离和方向,以及花朵的类型和质量。
-
返回蜂巢—一旦工蜂收集了足够的资源或她的蜜胃已满,她就会飞回蜂巢。到达蜂巢后,觅食蜂将花蜜转移到巢蜂,然后巢蜂对其进行处理并储存为蜂蜜。花粉也会类似地卸载到其他蜜蜂,以储存并作为食物在以后使用。
-
沟通—工蜂在蜂巢的舞蹈地板上跳摇摆舞,与她的巢伴(即旁观蜂)分享位置信息。这种舞蹈传达了食物来源的方向、距离和质量。
-
招募—旁观蜂观察摇摆舞并解码关于食物来源位置的信息。然后这些蜜蜂飞出去收集资源。
-
重复—工蜂会继续访问同一个食物来源,直到其耗尽或另一只蜜蜂将她招募到更有希望的食物来源。在两种情况下,她都会重复觅食过程,以确保满足蜂群的需求。
现在让我们更详细地看看 ABC 算法。
10.5 探索人工蜂群算法
人工蜂群(ABC)算法是由 Dervis Karaboga 于 2005 年[1]提出的,它模拟了三种蜜蜂的角色:雇佣蜂(觅食者)、旁观蜂和侦察蜂。算法 10.1 展示了 ABC 算法的步骤。
算法 10.1 人工蜂群算法
Initialization Phase: population of candidate solutions (food sources) are initialized
REPEAT
Forager Bee Phase: Each forager bee goes to a food source in her memory and determines a closest source, then evaluates its nectar amount and dances in the hive
Onlooker Bee Phase: Each onlooker bee watches the dance of forager bees and chooses one of their sources depending on the dances, and then goes to that source. After choosing a neighbor around that, she evaluates its nectar amount.
Scout Bee Phase: Abandoned food sources are determined and are replaced with the new food sources discovered by scout bees.
Memorize the best food source (solution) achieved so far.
UNTIL (termination criteria are met)
如您所见,ABC 算法模拟了蜜蜂的觅食行为来探索和利用搜索空间,平衡全局探索(多样性)和局部利用(收敛),以有效地解决优化问题。在 ABC 算法中,三种类型的蜜蜂具有以下互补的角色:
-
雇佣蜂(觅食者)—这些蜜蜂利用当前的食物来源,这意味着它们在其当前位置周围搜索(搜索邻域)以找到更好的解决方案。这些蜜蜂执行局部搜索(强化),以细化当前的最佳解决方案。
-
旁观蜂—这些蜜蜂也参与了利用。它们根据雇佣蜂找到的解决方案的适应性概率选择食物来源。它们更有可能选择更好的解决方案(含有更多花蜜的食物来源)以进行进一步的利用。
-
侦察蜂—这些蜜蜂执行探索任务。如果一个食物源耗尽(如果在一定数量的迭代后无法改进解决方案),与该食物源相关的工蜂就会变成侦察蜂。侦察蜂通过放弃耗尽的食物源并在问题空间中随机搜索新的食物源来执行全局搜索(多样化)。这个过程通过探索搜索空间的新区域来防止算法陷入局部最优。
在 ABC 算法中,通过在工蜂和观察蜂之间共享解决方案的适应度值来模拟蜜蜂之间的通信,引导它们向更好的解决方案前进。ABC 算法采用了一种受蜜蜂根据食物质量选择食物源的方式启发的适应度比例选择过程。在算法中,工蜂和观察蜂以与它们的适应度成比例的概率选择解决方案,从而促进更好的解决方案被更频繁地探索。
为了理解我们如何使用 ABC 算法来解决优化问题,让我们考虑使用 ABC 算法最小化 Rosenbrock 函数。Rosenbrock 函数,也称为山谷或香蕉函数,是梯度优化算法的流行测试问题。该函数具有 n 个维度,具有以下一般形式:
|

| 10.12 |
|---|
该函数通常在超立方体 x[i] ∈ [–5, 10] 上对所有 i = 1,...,n 进行评估,但域可能限制为 x[i] ∈ [–2.048, 2.048] 对所有 i = 1,...,n。该函数的全局最小值位于 f(x^*) = 0.0,位置在 (1,...,1)。
让我们考虑以下形式的二维 Rosenbrock 函数:
|

| 10.13 |
|---|
图 10.15 显示了 Rosenbrock 函数的二维表面。

图 10.15 Rosenbrock 函数的二维表面图。点表示该函数的全局最小值。
让我们看看如何使用 ABC 算法来最小化这个函数:
- 初始化阶段—假设我们有一群 N = 6 只蜜蜂。每只蜜蜂都试图找到一个候选解,种群中的每个解决方案 i 由位置向量 X[mi] = {x[mi], y[mi]} 组成,其中 X[mi] ∈ [–2.048, 2.048] 且 m = 1,…,N。X[mi] 代表优化问题的潜在解。工蜂的位置在边界内随机确定。这些初始解可以使用以下公式生成:
|

| 10.14 |
|---|
其中 l[i] 和 u[i] 是决策变量的下界和上界。让我们假设初始位置(如表 10.1 所示)为 (x, y)。
表 10.1 初始食物源
| 候选解 X[m] | 目标函数 fm |
|---|---|
| X[1] = (–1.04,0.11) | 98.56 |
| X[2] = (–1.61,–1.98) | 2097.22 |
| X[3] = (1.82,1.22) | 438.49 |
| X[4] = (–1.64,1.92) | 66.20 |
| X[5] = (0.77,0.04) | 30.62 |
| X[6] = (–0.66,1.59) | 136.02 |
- 雇用蜂阶段—在雇用蜂阶段,每只蜂在其当前解的邻域内使用以下公式生成一个新的解:
|

| 10.15 |
|---|
其中 v[mj] 是新解,x[mi] 是当前解,ϕ[mi] 是介于 –1 和 1 之间的随机数,x[ki] 是与当前解不同的随机选择的解。为了简化,我们假设所有 ϕ[mi] 都是 –0.9,并且对于每只蜂,我们选择蜂 1 的解来计算新解。初始种群(表 10.1)中的最佳蜂,即蜂 5,也可以使用。然后我们计算表 10.2 中显示的新适应度值。
表 10.2 新食源
| 候选解 X[m] | 目标函数 f[m](X[m]) |
|---|---|
| X[1] = (–1.04,0.11) | 98.56 |
| X[2] = (–1.10,–0.10) | 174.02 |
| X[3] = (–0.75,0.22) | 15.15 |
| X[4] = (–1.10,0.29) | 88.87 |
| X[5] = (–0.86,0.10) | 43.76 |
| X[6] = (–1.00,0.26) | 59.66 |
- 观察蜂阶段—观察蜂观察雇用蜂的舞蹈,并根据花蜜量(适应度值)选择食源。如果新解有更好的适应度值,它将被记住作为全局变量,并更新位置。否则,保留旧位置。观察蜂选择 X[m] 的概率值 p[m] 可以通过以下公式计算:
|

| 10.16 |
|---|
其中 fit[m](X[m]) 是解的适应度值,可以使用以下表达式计算:
|

| 10.17 |
|---|
其中 f[m](X[m]) 是解 X[m] 的目标函数。表 10.3 显示了解适应度计算。
表 10.3 解适应度计算
| 候选解 X[m] | 目标函数 f[m](X[m]**) | 适应度 fit[m](X[m]) | 选择概率 p[m] |
|---|---|---|---|
| X[1] = (–1.04,0.11) | 98.56 | 0.010 | 0.08 |
| X[2] = (–1.10,–0.10) | 174.02 | 0.006 | 0.04 |
| X[3] = (–0.75,0.22) | 15.15 | 0.062 | 0.49 |
| X[4] = (–1.10,0.29) | 88.87 | 0.011 | 0.09 |
| X[5] = (–0.86,0.10) | 43.76 | 0.022 | 0.18 |
| X[6] = (–1.00,0.26) | 59.66 | 0.016 | 0.13 |
在本例中,蜂 3 发现的食源最有可能被选中。在为观察蜂选择一个食源 X[m] 后,通过使用方程 10.15 确定一个邻域源 v[m],并计算其适应度值。
- 侦察蜂阶段—如果通过预定的循环或试验次数(称为 限制)无法进一步改善位置,那么该位置将被放弃,蜂变成侦察蜂,寻找新的随机位置,这可以通过方程 10.14 生成。
让我们现在看看如何用 Python 实现 ABC 来解决这个问题。在下一个列表中,我们首先导入我们将使用的库,并定义rosenbrock_function。此函数将 Rosenbrock 函数的候选解(x,y)作为参数,并返回其值。
列表 10.2 使用 ABC 解决 Rosenbrock 函数优化
import numpy as np
import random
import matplotlib.pyplot as plt
def rosenbrock_function(cand_soln):
return (1 - cand_soln[0]) ** 2 + 100 * (cand_soln[1] - cand_soln[0] ** 2) ** 2
作为对列表 10.2 的延续,我们将创建一个包含以下属性的Bee:
-
position—蜜蜂在搜索空间中的位置(解) -
fitness—蜜蜂当前位置的适应度(当前位置 Rosenbrock 函数的值) -
counter—一个计数器,用于跟踪失败的尝试次数(蜜蜂适应度没有改进的迭代):
class Bee:
def __init__(self, position, fitness):
self.position = position
self.fitness = fitness
self.counter = 0
现在我们需要一个函数来生成具有随机位置的Bee,并使用 Rosenbrock 函数计算其适应度:
def generate_bee(dimensions):
position = np.array([random.uniform(-5, 5) for _ in range(dimensions)])
fitness = rosenbrock_function(position)
return Bee(position, fitness)
以下函数将使用伙伴蜜蜂的位置更新给定蜜蜂的位置。如果新位置具有更好的适应度值,蜜蜂的位置、适应度和计数器将更新。否则,计数器将增加:
def update_position(bee, partner, dimensions):
index = random.randrange(dimensions) ①
phi = random.uniform(-1, 1)
new_position = bee.position.copy()
new_position[index] += phi * (bee.position[index] - partner.position[index])
new_position = np.clip(new_position, -5, 5) ②
new_fitness = rosenbrock_function(new_position)
if new_fitness < bee.fitness:
bee.position = new_position
bee.fitness = new_fitness
bee.counter = 0
else:
bee.counter += 1
① 确定蜜蜂位置中哪个元素将被更新。
② 剪切以确保它保持在指定的范围内。
接下来,我们将定义一个abc_algorithm函数来实现 ABC 算法,以下为其输入参数:
-
dimensions—问题的维度数,对于 Rosenbrock 函数是 2 -
num_bees—蜂群中蜜蜂的总数 -
max_iter—算法应运行的最大迭代次数 -
max_trials—在蜜蜂成为侦察蜂之前允许的最大失败循环或尝试次数(没有改进的迭代):
def abc_algorithm(dimensions, num_bees, max_iter, max_trials):
bees = [generate_bee(dimensions) for _ in range(num_bees)] ①
best_bee = min(bees, key=lambda bee: bee.fitness) ②
for _ in range(max_iter):
for i in range(num_bees // 2): ③
employed_bee = bees[i]
partner_bee = random.choice(bees)
update_position(employed_bee, partner_bee, dimensions)
total_fitness = sum(1 / (1 + bee.fitness) if bee.fitness >= 0 else 1 ④
➥ + abs(bee.fitness) for bee in bees) ④
probabilities = [(1 / (1 + bee.fitness)) / total_fitness if bee. ④
➥ fitness >= 0 else (1 + abs(bee.fitness)) / total_fitness for bee in bees] ④
for i in range(num_bees // 2, num_bees): ⑤
onlooker_bee = random.choices(bees, weights=probabilities)[0]
partner_bee = min(bees[:num_bees // 2], key=lambda bee: bee.fitness)
update_position(onlooker_bee, partner_bee, dimensions)
for bee in bees: ⑥
if bee.counter > max_trials: ⑦
new_bee = generate_bee(dimensions)
bee.position = new_bee.position
bee.fitness = new_bee.fitness
bee.counter = 0
best_iter_bee = min(bees, key=lambda bee: bee.fitness) ⑧
if best_iter_bee.fitness < best_bee.fitness: ⑧
best_bee = best_iter_bee ⑧
return best_bee ⑨
① 生成蜜蜂的初始种群。
② 找到具有最佳适应度值的蜜蜂。
③ 执行雇佣蜜蜂阶段。
④ 执行观察者蜜蜂阶段。
⑤ 根据方程 16 和 17 计算选择概率。
⑥ 执行侦察蜜蜂阶段。
⑦ 检查每只蜜蜂的计数器是否超过 max_trials。
⑧ 使用新的最佳蜜蜂更新 best_bee。
⑨ 返回 best_bee,它代表最优解。
现在我们可以设置 ABC 算法的参数,并将其应用于解决问题:
dimensions = 2 ①
num_bees = 50 ②
max_iter = 1000 ③
max_trials = 100 ④
best_bee = abc_algorithm(dimensions, num_bees, max_iter, max_trials) ⑤
print(f"Best solution: {best_bee.position}") ⑥
print(f"Best fitness: {best_bee.fitness}") ⑥
① 定义问题维度。
② 设置蜜蜂的数量。
③ 设置最大迭代次数作为停止标准。
④ 设置在蜜蜂成为侦察蜂之前允许的最大失败尝试次数(没有改进的迭代)。
⑤ 使用指定的参数运行 ABC 算法,并将最佳蜜蜂(具有最小适应度值的蜜蜂)存储在 best_bee 变量中。
⑥ 打印 best_bee 的位置,它代表解及其适应度,即最佳解处的 Rosenbrock 函数值。
此代码将产生如下输出:
Best solution: [0.99766117 0.99542949]
Best fitness: 6.50385257086524e-06
与遗传算法和粒子群优化算法相比,专门为 ABC 算法设计的、建立良好且全面的 Python 包相对较少。然而,有一个名为 MEALPY 的 Python 库提供了基于种群的元启发式算法的实现,包括 ABC。您可以使用pip install mealpy来安装 MEALPY。
作为 10.2 列表的延续,以下代码片段展示了如何使用 MEALPY 库中的OriginalABC类来最小化 Rosenbrock 函数:
from mealpy.swarm_based.ABC import OriginalABC ①
problem_dict = {
"fit_func": rosenbrock_function,
"lb": [-5, -5],
"ub": [5, 5],
"minmax": "min",
} ②
epoch = 200 ③
pop_size = 50 ④
n_limits = 15 ⑤
model = OriginalABC(epoch, pop_size, n_limits) ⑥
best_position_mealpy, best_fitness_mealpy = model.solve(problem_dict) ⑦
print(f"Best solution: {best_position_mealpy}") ⑧
print(f"Best fitness: {best_fitness_mealpy}") ⑧
① 从 MEALPY 库导入求解器。
② 使用字典定义问题。
③ 设置 epoch(迭代)的数量。
④ 设置种群大小。
⑤ 设置触发侦察蜂之前的失败尝试次数限制。
⑥ 创建算法类的实例。
⑦ 运行算法。
⑧ 打印结果。
我们首先从mealpy.swarm_based.ABC模块导入OriginalABC类,这是 MEALPY 库提供的 ABC 算法的实现。然后我们定义问题字典,其中包含成本函数(fit_func)、下界(lb)、上界(ub)以及这是一个最小化还是最大化问题(minmax)。设置 epoch(迭代)的数量、种群大小以及触发侦察蜂之前的失败尝试次数限制。然后我们创建OriginalABC类的实例,并使用指定的参数进行初始化。在model对象上调用solve()方法,将problem_dict作为参数传递。它对定义的问题执行 ABC 算法优化过程,并返回最佳解和适应度值。
运行此代码将产生如下所示的解决方案:
Best solution: [1.07313697 1.04914444]
Cost at best solution: 0.0009197449137428784
图 10.16 显示了 ABC 求解器、ABC MEALPLY 和作为完整列表 10.2 一部分实现的 ACO 求解器获得的解决方案。

图 10.16 使用 ABC 和 ACO 算法的 Rosenbrock 函数等高线和解决方案
如您所见,ABC、ABC MEALPY 和 ACO 的解决方案都接近此函数的最优解。通过参数调整,这些算法可以达到最优解。您可以使用 10.2 列表中的代码来尝试不同的算法参数设置和不同的问题维度。
本章结束了本书的第四部分。在这一部分,我们深入探讨了群智能的迷人世界,探讨了像粒子群优化(PSO)中的鸟、蚁群优化(ACO)中的蚂蚁和人工蜂群(ABC)算法中的蜜蜂这样的简单实体如何集体执行复杂任务。这些受自然界启发的算法巧妙地平衡了探索和利用,以找到复杂优化问题的最优或近似最优解。
随着我们继续前进,我们将过渡到机器学习的领域。在这本书的最后一部分,我们将探讨专门针对搜索和优化的机器学习方法。我们将探索前沿技术,如图神经网络、注意力机制、自组织映射和强化学习,并研究它们在搜索和优化中的应用。
摘要
-
蚂蚁群优化(ACO)是一种受蚂蚁觅食行为启发的基于种群的算法。简单的蚁群优化(SACO)、蚂蚁系统(AS)、蚂蚁群系统(ACS)和最大-最小蚂蚁系统(MMAS)是 ACO 元启发式算法的例子。
-
在觅食过程中,蚂蚁发现好的解决方案,这会影响其他蚂蚁的决策。随着时间的推移,信息素路径沿着更好解决方案的路径增强,吸引更多蚂蚁探索这些路径。这被称为自催化行为。
-
信息素更新包括两个阶段:蒸发和沉积。在蒸发阶段,信息素浓度降低。蚂蚁可以在构建解决方案的过程中沉积信息素,使用在线逐步信息素更新方法,或者解决方案构建完成后,通过重新访问构建过程中访问的所有状态,使用在线延迟信息素更新方法。在某些情况下,两种方法可以同时使用。
-
蚂蚁系统(AS)通过添加禁忌表的形式的内存能力来改进简单的蚁群优化(ACO)。
-
蚂蚁群系统(ACS)算法是 AS 算法的扩展,它使用了一种修改后的转换规则,并利用了精英策略。
-
最大-最小蚂蚁系统(MMAS)通过使用迭代最佳路径进行信息素更新,鼓励探索并限制信息素值在最小和最大值之间,解决了蚂蚁系统(AS)和蚂蚁系统协同优化(ACS)的局限性。这种方法减少了过早停滞的风险,并通过平衡探索和利用来提高性能。
-
人工蜂群(ABC)算法是一种受蜜蜂觅食行为启发的基于种群的搜索算法。ABC 算法通过其三种蜜蜂(工蜂、观察蜂和侦察蜂)来管理探索和利用之间的平衡,每种蜜蜂都执行不同的互补角色。
-
由于初始条件和概率决策过程的不确定性,随机优化算法中的固有随机性并不一定是坏事。它可以帮助算法避免陷入局部最优——在它们 immediate vicinity 中是最好的解决方案,但不是整体上最好的解决方案。通过偶尔选择不太有希望的路径,算法可以探索更多的解决方案空间,并更有可能找到全局最优——最佳可能的解决方案。
第五部分:基于机器学习的方法
在本书的最后一部分,包括两个章节,你将深入研究机器学习技术的动态世界以及如何利用它们来解决复杂的优化问题。
在第十一章中,你将学习如何利用人工智能、机器学习和深度学习的力量来解决优化问题。我们将从这些基础概念的复习开始,确保你有一个坚实的基础。然后,你将深入探索图形机器学习、图嵌入、图卷积网络和注意力机制这一激动人心的领域,这些在解决具有图结构数据的优化问题中非常有价值。此外,你还将探索自组织图,揭示它们在优化任务中的作用。到本章结束时,你将准备好应用监督和无监督机器学习技术来处理优化问题。
第十二章深入探讨了强化学习(RL)这个迷人的领域。你将掌握 RL 的基本原理,理解马尔可夫决策过程的概念,并深入研究演员-评论家架构和近端策略优化算法。你还将熟悉多臂老丨虎丨机和上下文老丨虎丨机,并学习这些技术如何应用于解决优化问题,其中决策导致最优结果。
在这部分,你将弥合机器学习和优化之间的差距,深入了解机器学习如何被有效地利用来找到最优解。这正是机器学习和优化之间的协同作用开启了一个新的视野,并转向数据驱动和智能问题解决的方向。
第十一章:监督学习和无监督学习
本章涵盖
-
回顾人工智能、机器学习和深度学习的基础知识
-
理解图机器学习、图嵌入和图卷积网络
-
理解注意力机制
-
理解自组织映射
-
使用监督学习和无监督机器学习解决优化问题
人工智能(AI)是技术领域增长最快的领域之一,由计算能力的提升、大量数据的获取、算法的突破以及公共和私营部门的增加投资所驱动。AI 旨在创建能够表现出智能行为或机器,通常是通过模仿或从生物智能中汲取灵感。这些系统可以设计为自主运行或在一定程度上接受人类指导,理想情况下,它们可以适应具有不同结构、可观察性和动态的环境。AI 通过赋予我们分析大量多维、多模态数据的能力并识别人类难以识别的隐藏模式来增强我们的智能。AI 还通过提供相关见解和潜在的行动方案来支持我们的学习和决策。AI 包括各种子领域,如态势感知(包括感知、理解和预测)、知识表示、认知推理、机器学习、数据分析(包括描述性、诊断性、预测性和规范性分析)、问题解决(涉及约束满足和搜索与优化)、以及数字和物理自动化(如对话式 AI 和机器人)。
在本书的最后一部分,我们将探讨人工智能两个分支的融合:机器学习和优化。我们的重点是展示机器学习在解决优化问题中的实际应用。本章提供了机器学习基础知识的概述,作为必要的背景知识,然后深入探讨了监督学习和无监督机器学习在处理优化问题中的应用。强化学习将在下一章中介绍。
11.1 智能赋能日常生活的每一天
AI,尤其是机器学习,构成了许多成功颠覆性产业的基础,并成功推出了许多触及每个人日常生活的商业产品。从家庭开始,语音助手热切地等待您的命令,轻松控制智能设备并调整智能恒温器以确保舒适和便利。智能电表智能管理能源消耗,优化效率并降低成本。
在上学或上班的路上,具有位置智能的导航应用指引路线,考虑实时交通更新以提供最快和最有效的路线。共享出行服务提供按需的灵活交通选项,而先进的驾驶辅助系统在您决定驾驶时增强安全性和便利性。在不远的将来,我们将享受安全有趣的自动驾驶汽车作为第三个生活空间,在家庭和工作场所之后,拥有以消费者为中心的产品和服务。
一旦在学校或工作场所,人工智能成为个性化提升生产力的无价工具。个性化的学习平台满足个人需求,调整教学方法和内容以最大化理解和记忆。摘要和语法检查算法有助于制作无瑕疵的文档,而翻译工具则轻松跨越语言障碍。Excel 人工智能公式生成器简化了复杂的计算,节省了时间和精力。类似人类的文本生成使写作自然流畅,而从文本生成音频、图像和视频则开启了创意的可能性。优化算法确保资源分配和调度最优化,在各种场景中最大化效率,并处理不同的设计、规划和控制问题。
在购物过程中,人工智能以多种方式提升了购物体验。语音搜索实现了免提探索,而通过图像搜索则可以轻松发现所需商品。语义搜索理解上下文和意图,提供更准确的结果。推荐引擎根据个人偏好和在线购物行为提供个性化建议,而最后一英里或门到门的配送服务确保及时、透明和便利的包裹送达。
在健康领域,人工智能革命性地改变了个性化医疗保健,协助诊断、治疗规划和康复。实验室自动化加速了测试流程,提高了准确性和效率。人工智能驱动的药物发现和递送使创新治疗和靶向疗法的开发成为可能,改变人们的生活。
在休闲时间,人工智能有助于身心健康。健身计划应用根据个人目标和能力定制锻炼计划,提供个性化的指导和动力。行程规划工具推荐令人兴奋的目的地和行程,确保难忘的体验。人工智能驱动的冥想应用提供定制的放松体验,舒缓心灵并促进正念。
机器学习,作为人工智能的一个突出子领域,在将人工智能从高科技研究实验室的局限带到我们日常生活的便利中发挥了关键作用。
11.2 揭秘机器学习
学习的目的是创建外部世界的内部模型或抽象。更全面地说,Stanislas Dehaene 在 《我们如何学习》 [1] 中介绍了学习的关键定义,这些定义是当今机器学习算法的核心:
-
学习是调整心理模型的参数。
-
学习是探索组合爆炸。
-
学习是最小化错误。
-
学习是探索可能性空间。
-
学习是优化奖励函数。
-
学习是限制搜索空间。
-
学习是投射先验假设。
机器学习(ML)是人工智能的一个子领域,它赋予人工系统或过程从经验和观察中学习的能力,而不需要明确编程。Thomas Mitchell 在 《机器学习》 中将 ML 定义如下:“如果一个计算机程序在任务 T 中,根据性能度量 P,从经验 E 中学习,那么它的性能随着经验 E 的提高而提高” [2]。在他的书 《大师算法》 中,Pedro Domingos 将机器学习学派总结为五个主要学派 [3],如图 11.1 所示:
-
以概率推理作为主算法的贝叶斯主义者
-
以规则和树作为该范式主要核心算法的符号主义者
-
使用反向传播作为主算法的神经网络连接主义者
-
依赖于进化计算范式的进化论者
-
使用不同核的支持向量机等数学技术的类比主义者

图 11.1 根据 Domingos 的 《大师算法》 列出的不同机器学习学派
现在,连接主义学习方法因其几个具有挑战性的领域的感知和学习能力而吸引了大部分关注。这些统计机器学习算法遵循自下而上的归纳推理范式(即从一组示例中推断一般规则)来从大量数据中发现模式。
数据的不合理有效性
简单模型和大量数据胜过基于较少数据的更复杂模型 [4]。这意味着拥有大量数据来训练简单模型通常比使用只有少量数据的复杂模型更有效。例如,在自动驾驶汽车中,一个在数百万小时驾驶数据上训练过的简单模型,在识别和反应各种道路情况时,通常比在更小数据集上训练的更复杂模型更有效。这是因为大量数据帮助简单模型学习广泛的各种模式和场景,包括它可能遇到的对抗性和边缘情况,使其在现实世界的驾驶条件下更具适应性和可靠性。
这些基于连接主义学习或统计机器学习的方法基于实验发现,即使是非常复杂的人工智能问题也可能通过在大量数据集上训练的简单统计模型来解决[4]。统计机器学习是目前最著名的 AI 形式。这种形式机器学习的快速进步主要归因于大数据的广泛应用、开源工具的普及、增强的计算能力,如 AI 加速器,以及公共和私营部门的大量研发资金。
通常来说,机器学习算法可以分为监督学习、无监督学习、混合学习和强化学习算法,如图 11.2 所示。

图 11.2 机器学习作为人工智能子领域的分类
-
监督学习——这种方法使用归纳推理来近似数据与已知标签或类别之间的映射函数。这种映射是通过使用已经标记的训练数据来学习的。在监督学习中,分类(预测离散或分类值)和回归(预测连续值)是常见的任务。例如,分类寻求一个评分函数 f:Χ×C⟶R,其中 X 代表训练数据空间,C 代表标签或类别空间。这种映射可以使用 N 个训练示例来学习,形式为 {(x[11], x[21], …, x[m][1], c[1]), (x[12], x[22], …, x[m][2], c[2]), …, (x[1][N], x[2][N], …, x[mN], c[N])},其中 x[i] 是第 i 个示例的特征向量,m 是特征数量,c[i] 是相应的类别。预测的类别是给出最高评分的类别,即 c(x) = argmax[c]f(x,c)。在自动驾驶汽车的情况下,监督学习可能被用来训练一个模型来识别交通标志。输入数据将是各种交通标志的图像,正确的输出(标签)将是每个标志的类型。训练好的模型随后可以在驾驶时正确识别交通标志。前馈神经网络(FNNs)或多层感知器(MLPs)、卷积神经网络(CNNs)、循环神经网络(RNNs)、长短期记忆(LSTM)网络和序列到序列(Seq2Seq)模型是通常使用监督学习训练的常见神经网络架构的例子。使用监督机器学习解决组合问题的例子在 11.6、11.7 和 11.9 节中提供。
-
无监督学习—这种方法通过诸如聚类和降维等技术处理未标记数据。例如,在聚类中,给出n个对象(每个对象可能是一个d特征的向量),任务是根据某些相似性度量将它们分组到c个组(簇)中,使得单个组中的所有对象彼此之间有“自然”的关系,而不同组中的对象在某种程度上是不同的。例如,无监督学习可能用于自动驾驶车辆中聚类相似的驾驶场景或环境。使用无监督学习,汽车可能学会识别不同类型的交叉口或环岛,即使没有人明确地将这些类别标记为数据。自编码器、k-means、基于密度的空间聚类(DBSCAN)、主成分分析(PCA)和自组织映射(SOMs)是未监督学习方法的例子。SOM 在 11.4 节中解释。11.8 节提供了一个使用 SOM 的组合问题的例子。
-
混合学习—这种方法包括半监督学习和自监督学习技术。半监督学习是监督学习和无监督学习相结合的方法,其中只有一小部分输入数据被标记为相应的输出。在这种情况下,训练过程使用可用的少量标记数据,并对数据集的其余部分进行伪标记——例如,使用有限的标记驾驶场景集训练自动驾驶车辆的感知系统,然后使用大量的未标记驾驶数据来提高其识别和响应各种道路状况和障碍物的能力。自监督学习是一种机器学习过程,模型通过使用数据本身固有的结构或关系来学习输入数据的有效表示。这是通过从未标记数据中创建监督学习任务来实现的。例如,一个自监督模型可能被训练来根据前面的单词预测句子中的下一个单词,或者从打乱版本的图像中重建图像。这些学习到的表示可以用于各种下游任务,例如图像分类或目标检测。在自动驾驶车辆的情况下,感知系统可以被训练来识别未标记驾驶场景中的关键特征,例如车道标记、行人和其他车辆。然后,学习到的特征被用作伪标签,以监督方式对新的驾驶场景进行分类,使车辆能够根据其对道路环境的理解做出决策。
-
强化学习(RL)——这种方法通过反馈循环或通过试错来学习。学习代理通过在环境中采取行动以最大化某种累积奖励的概念来学习做出决策。对于自动驾驶汽车,强化学习可以在决策过程中使用。例如,汽车可能随着时间的推移学会在繁忙的高速公路上并入交通的最佳方式。它将因成功的并入而获得正面奖励,因危险的操作或失败的尝试而获得负面奖励。随着时间的推移,通过试错和最大化奖励的愿望,汽车将学会并入交通的最佳策略。关于强化学习的更多细节将在下一章提供。
深度学习(DL)是机器学习(ML)的一个子领域,它使用具有多层(因此称为“深度”)的神经网络来学习数据中的底层特征,使人工系统能够从更简单的概念中构建复杂的概念。深度学习能够学习具有判别性的特征或表示,并在不同抽象级别上进行学习。为了实现这一点,网络使用层次特征学习和采用少量卷积层。深度学习通过减少对大量数据预处理的需求,革新了机器学习领域。深度学习模型可以从原始数据中自动提取高度判别性的特征,从而消除了手动特征工程的需求。这种端到端的学习过程显著减少了对人专家的依赖,因为模型能够直接从输入数据中提取有意义的表示和模式。
与传统的机器学习算法不同,深度学习模型能够直接消费和处理各种形式的结构化和非结构化数据,如文本、音频、图像、视频,甚至图。图结构数据在组合优化领域尤为重要,因为它能够捕捉和表示优化问题中元素之间的关系和约束。几何深度学习是机器学习的一个子领域,它将图论与深度学习相结合。
以下两节更详细地介绍了图机器学习和自组织映射。它们是本章后面描述的使用案例的必要背景知识。
11.3 使用图的机器学习
如第 3.1 节所述,图是一种非线性数据结构,由称为顶点(或节点)的实体及其之间的关系组成,这些关系称为边(或弧或链接)。来自不同领域的数据可以使用图很好地捕捉。例如,社交媒体网络使用图来描绘用户之间的连接并分析社会互动,这反过来又推动内容传播和推荐。导航应用程序使用图来表示物理位置及其之间的路径,从而实现路线计算、实时交通更新和预计到达时间(ETA)预测。推荐系统依赖于图来模拟用户-项目交互和偏好,从而提供个性化推荐。搜索引擎使用网页图,其中网页是节点,超链接是边,以爬取和索引互联网并促进高效的信息检索。知识图谱提供了事实信息、关系和实体的结构化表示,并在从数字助手到企业数据集成等众多领域得到应用。问答引擎使用图来理解和分解复杂问题,并在结构化数据集中搜索相关答案。在化学领域,分子结构可以被视为图,其中原子是节点,键是边,支持发现化合物和预测性质等任务。
图结构化数据至关重要,因为它能够以直观、自描述、本质上可解释和自然的方式对实体之间的复杂关系和依赖进行建模。与传统表格数据不同,图允许表示感兴趣实体之间的网络关系和复杂相互关联,使它们成为建模众多现实世界系统的优秀工具。可以将表格数据转换为图结构化数据——节点和边的具体定义将取决于你想要在数据中检查哪些关系。例如,在 FIFA 数据集的背景下,我们可以根据该数据集中可用的信息定义节点和边:
-
节点—节点代表感兴趣的实体,可以是球员、他们所效力的俱乐部或他们的国籍。这些实体中的每一个都可以是图中的一个单独节点。例如,莱昂内尔·梅西、国际米兰和阿根廷都可以是图中的单独节点。
-
边缘—边缘代表节点之间的关系。例如,一条边可以连接一个球员和他们所效力的俱乐部,表示该球员是该俱乐部的一员。另一条边可以连接一个球员和他们的国籍,表明该球员属于那个国家。因此,例如,莱昂内尔·梅西可以通过一条边与国际米兰连接,表示梅西为国际米兰效力,另一条边可以将莱昂内尔·梅西与阿根廷连接,表示他的国籍。
下一个列表显示了如何使用 NetworkX 将 10 位选定足球运动员的表格数据转换为图。
列表 11.1 将表格数据转换为图
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
data={'Player':['L. Messi','R. Lewandowski','C. Ronaldo','Neymar Jr','K.
➥ Mbappé','E.Haaland','H. Kane','Luka Modrić','L. Goretzka','M. Salah'],
➥ 'Age':[36,34,38,22,24,35,29,37,28,31],
➥ 'Nationality':['Argentina','Poland','Portugal','Brazil','France','Norway',
➥ 'England','Croatia','Germany','Egypt'],
➥ 'Club':['Inter Miami','Barcelona','Al-Nassr','Al-Hilal ','PSG','Manchester
➥ City','Tottenham Hotspur','Real Madrid','Bayern Munich','Liverpool'],
➥ 'League':['Major League Soccer ','Spain Primera Division','Saudi Arabia
➥ League','Saudi Arabia League','French Ligue 1','English Premier
➥ League','English Premier League','Spain Primera Division','German 1.
➥ Bundesliga','English Premier League']}
df=pd.DataFrame.from_dict(data)
作为 11.1 列表的延续,我们可以创建一个 NetworkX 图,其节点代表球员姓名、俱乐部和国籍,而边则代表这些节点之间的语义关系。
G = nx.Graph() ①
for index, row in df.iterrows():
G.add_edge(row['Player'], row['Club'], relationship='plays_for') ②
for index, row in df.iterrows():
G.add_edge(row['Player'], row['Nationality'], relationship='belongs_to') ③
pos = nx.kamada_kawai_layout(G) ④
plt.figure(figsize=(20, 14)) ⑤
player_nodes = df['Player'].unique().tolist() ⑥
club_nodes = df['Club'].unique().tolist() ⑥
nationality_nodes = df['Nationality'].unique().tolist() ⑥
nx.draw_networkx_nodes(G, pos, nodelist=player_nodes, node_color='blue',
➥ label='Player Name', node_shape='o') ⑦
nx.draw_networkx_nodes(G, pos, nodelist=club_nodes, node_color='red', label='Club', ⑦
➥ node_shape='d') ⑦
nx.draw_networkx_nodes(G, pos, nodelist=nationality_nodes, node_color='gray', ⑦
➥ label='Nationality', node_shape='v') ⑦
nx.draw_networkx_edges(G, pos) ⑧
edge_labels = nx.get_edge_attributes(G, 'relationship') ⑨
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=12) ⑨
nx.draw_networkx_labels(G, pos) ⑩
plt.legend(fontsize=13, loc='upper right')
plt.show()
① 创建一个新的图。
② 为俱乐部添加节点和边。
③ 为国籍添加节点和边。
④ 创建布局
⑤ 设置图形的大小。
⑥ 获取球员、俱乐部和国籍节点的列表。
⑦ 用不同颜色绘制节点。
⑧ 绘制边。
⑨ 绘制边标签。
⑩ 绘制节点标签。
图 11.3 显示了 10 位选定足球运动员的图数据。此图显示了感兴趣的实体(球员、俱乐部和国籍)及其关系。例如,L.梅西是一名为 Inter Miami 效力的球员,来自阿根廷。

图 11.3 10 位选定足球运动员的图结构数据
图形数据与欧几里得数据根本不同,因为距离的概念不仅仅是两点之间的直线(欧几里得)距离。在图的情况下,重要的是节点和边的结构——两个节点是否通过边连接,以及它们如何连接到图中的其他节点。表 11.1 总结了欧几里得数据和非欧几里得图数据之间的差异。
表 11.1 欧几里得数据与非欧几里得图数据
| 方面 | 欧几里得数据 | 非欧几里得图数据 |
|---|---|---|
| 常见数据类型 | 数值、文本、音频、图像、视频 | 道路网络、社交网络、网页和分子结构 |
| 维度 | 可以为 1D(例如,数字、文本),2D(例如,图像、热图),或更高维(例如,RGB-D 图像或深度图,3D 点云数据) | 大维度(例如,Pinterest 图有 30 亿个节点和 180 亿条边) |
| 结构 | 固定结构(例如,在图像的情况下,结构通过像素邻近性嵌入) | 任意结构(每个节点都可以有不同的神经网络结构,因为其网络邻域不同,模型适应数据) |
| 空间局部性 | 是(即在输入空间中彼此靠近的数据点也可能会在输出空间中彼此靠近)。 | 否,“接近”由图结构决定,而不是空间排列(即彼此“接近”的两个节点可能不具有相似的性质或特征,例如在交通灯节点和人行横道节点的情况下)。 |
| 平移不变性 | 是(即在平移时保留数据固有的意义;例如,图片中猫的概念不会因为猫在图像的右上角或左下角而改变)。 | 否(在图中,节点的“位置”没有固有的意义,不能“平移”)。 |
| 序数或层次 | 是 | 否,图数据具有“排列不变性”——节点的特定顺序或标签通常不会影响图的基本关系和属性。 |
| 两个点之间的最短路径 | 直线 | 不一定是直线 |
| 机器学习模型示例 | 卷积神经网络(CNNs)、长短期记忆(LSTM)和循环神经网络(RNNs) | 图神经网络(GNNs)、图卷积网络(GCNs)、时序图网络(TGNs)、时空图神经网络(STGNNs) |
几何深度学习(GDL)是一个总称,用于描述旨在将(结构化)深度神经网络扩展到处理具有潜在几何结构的非欧几里得数据的新兴技术,例如图(连接实体的网络)、点云(3D 数据点的集合)、分子(化学结构)和流形(弯曲、高维表面)。图机器学习(GML)是机器学习的一个子领域,专注于开发能够从图结构化数据中学习的算法和模型。图嵌入或表示学习是执行机器学习任务(如节点分类(预测每个节点的类别)、链接预测(预测节点之间的连接)和社区检测(识别相互连接的节点组))的第一步。下一小节将描述不同的图嵌入技术。
11.3.1 图嵌入
图嵌入是一个旨在将离散的高维图域映射到低维连续域的任务。通过图嵌入的过程,图节点、边及其特征被转换为连续向量,同时保留图的结构信息。例如,如图 11.4 所示,编码器 ENC(v) 根据节点 v 在图中的位置、其局部邻域结构或其特征,或这三个的组合,将节点 v 从输入图空间 G 映射到嵌入或潜在空间 H 中的低维向量 h[v]。

图 11.4 图嵌入
此编码器需要优化以最小化图中一对节点相似性与它们在嵌入空间中相似性之间的差异。图中连接或靠近的节点在嵌入空间中应该靠近。相反,图中未连接或相距较远的节点在嵌入空间中应该相距较远。在更通用的编码器/解码器架构中,添加了解码器以从低维嵌入中提取用户指定的信息[5]。通过联合优化编码器和解码器,系统学习将关于图结构的信息压缩到低维嵌入空间中。
图嵌入有多种方法,可以大致分为归纳(浅层)嵌入和归纳嵌入:
-
归纳嵌入——在归纳学习范式下,模型仅在训练阶段学习图中存在的节点的嵌入。这些学习到的嵌入仅针对这些节点,模型不能为训练期间未出现的节点生成嵌入。这些方法难以扩展,适用于静态图。图嵌入的归纳方法示例包括随机游走(例如,node2vec 和 DeepWalk)和矩阵分解(例如,图分解和 HOPE)。
-
归纳嵌入——归纳学习方法可以推广到训练期间未出现的节点或整个图。它们通过学习一个函数来实现,该函数根据节点的特征和其局部邻域的结构生成节点的嵌入,这可以应用于任何节点,无论它是否在训练期间出现。这些方法适用于动态图。图嵌入的归纳方法示例包括图神经网络(GNN)和图卷积网络(GCNs)。
附录 A 包含了一些这些方法的示例。更多信息,请参阅 Broadwater 和 Stillman 的《图神经网络实战》[6]。我们将重点关注 GCN,因为它是最相关的组合优化应用方法,本章将介绍。
归纳学习与归纳学习比较
归纳学习旨在从特定的数据集中学习到特定的预测结果,而不对新的数据进行泛化。归纳学习旨在从观察到的训练案例中学习一般规则。这些一般规则可以应用于新的、未见过的数据。
卷积操作是许多结构化数据场景中表征学习的基础,它能够从原始数据中自动学习有意义的特征,从而避免了手动特征工程的需要。卷积是一种数学运算,它接受两个函数(输入数据和核、滤波器或特征检测器)并测量它们的重叠或合并这两组信息以生成特征图。卷积的一个关键方面是其能够尊重并利用数据点之间已知的结构关系,例如像素之间的位置关联、时间点的时序或网络中节点之间的边缘。在传统的机器学习中,卷积神经网络(CNNs)使用卷积算子作为识别图像中空间模式的关键工具。这是由图像数据的固有网格状结构所实现的,它允许模型在图像上滑动滤波器,利用空间规律性,并以类似于模式识别的方式提取特征。
然而,在图机器学习(GML)领域,情况发生了相当大的变化。在此背景下,数据是非欧几里得的,如前所述在表 11.1 中解释,这意味着它不是像图像中的像素或 3D 表面上的点那样排列在常规网格上。相反,它以网络或图的形式表示,可以捕捉复杂的关系。此外,这种数据表现出顺序不变性,这意味着输出不会随着节点的重新排列而改变。
与在常规网格上运行的卷积神经网络(CNNs)不同,图卷积网络(GCNs)被设计用来处理以图结构组织的数据,这种结构可以表示各种不规则和复杂的结构。每个节点都与其邻居通过没有任何预定义模式的连接相连,卷积操作应用于图中的节点及其直接邻居。
谷歌 DeepMind 是如何预测到达时间的?
你是否曾经想过,当你计划旅行时,谷歌地图是如何预测到达时间(ETA)的?谷歌 DeepMind 使用 GML 方法来做到这一点。传统的机器学习方法是将路线分解成多个路段,使用前馈神经网络预测穿越每个路段所需的时间,并将它们加起来得到 ETA。然而,前馈神经网络的基本假设是路段之间是相互独立的。在现实中,路段交通很容易影响相邻路段的 ETA,因此样本不是独立的。
例如,考虑这种情况:一条次要道路上的拥堵会影响主路的交通流量。当模型包含多个交叉口时,它自然会发展出预测交叉口减速、因交通汇聚导致的延误以及停车和行驶交通条件下所需总时间的预测能力。一个更好的方法是使用 GML 来考虑相邻路段的影响。
在这种情况下,道路网络首先将被转换为图,其中每个路段被表示为一个节点。如果两个路段相互连接,它们对应的节点将在图中通过边连接。然后,通过 GNN 生成图嵌入,将节点特征和图结构从高维离散图空间映射到低维连续潜在空间。通过称为“消息传递”的技术在图中传播和聚合信息,在最后,每个节点的嵌入向量包含并编码了其自身的信息以及所有相邻节点的网络信息,根据邻域的程度。相邻节点相互传递消息。在第一次传递中,每个节点了解其邻居。在第二次传递中,每个节点了解其邻居的邻居,并将这些信息编码到嵌入中,依此类推。这使得我们能够表示相邻路段中交通的影响。
使用这种方法,在柏林、雅加达、圣保罗、悉尼、东京和华盛顿特区等地,实时 ETAs 的准确性提高了高达 50% [7]。
如图 11.5 所示,给定一个输入图,该图包括节点特征x[v]和邻接矩阵A,GCN 将每个节点的特征转换为一个潜在或嵌入空间H,同时保留由邻接矩阵A表示的图结构。这些潜在向量提供了每个节点的丰富表示,使得独立进行节点分类成为可能。

图 11.5 图嵌入和节点、链接和图分类
此外,GCN 还能够预测与边相关的特征,例如两个节点之间是否存在链接。一旦生成了节点嵌入,就可以根据节点v和u的嵌入h[v],h[u]预测它们之间边的可能性。一种常见的方法是计算两个节点嵌入之间的相似度度量(例如,点积)。然后,可以将这种相似度通过 sigmoid 函数传递,以预测边的概率。预测上的错误(损失)将反向传播并更新神经网络中的权重。
最后,GCN 能够在整个图的层面上进行分类。这可以通过聚合所有节点的所有潜在或嵌入向量(H)来实现。所使用的聚合函数必须是排列不变的,这意味着输出应该与节点的顺序无关。此类函数的常见例子是求和、平均或最大化。一旦将潜在向量聚合为单一表示,就可以将此表示输入到一个模块(例如,一个神经网络层)中,以预测整个图的输出。本质上,GCN 允许进行节点级、边级和图级预测。
为了更好地理解 GCN 的工作原理,让我们考虑一个包含五个节点的图,如图 11.6 所示。对于图中的每个节点,第一步是找到其相邻节点。假设我们想检查节点 5 的嵌入是如何生成的。如图 11.6 左上角的原图所示,节点 2 和 4 是节点 5 的相邻节点。第二步是消息传递,这是节点向其相邻节点发送、接收和聚合消息的过程,以迭代更新其特征。这使得 GCN 能够学习每个节点的表示,该表示既捕获了其自身的特征,也捕获了其在图中的上下文。学习到的表示可以用于下游任务,如节点分类、链接预测或图分类。

图 11.6 GCN 中的消息传递和更新
考虑到节点 v 的 N(v)个相邻节点,经过 t 层邻域聚合后的节点 v 的嵌入基于图 11.7 所示的公式。初始的 0 层嵌入 h[v]⁰等于节点特征 x[v]。

图 11.7 GCN 中的嵌入函数
该公式递归应用于每个时间步得到另一个更好的向量 h,其中 h 是节点在潜在空间中的向量表示。权重矩阵通过在给定数据上的训练学习得到。一开始,图中的每个节点只知道其自身的初始特征。在 GCN 的第一层中,每个节点与其直接相邻的节点进行通信,聚合其自身的特征并接收来自这些邻居的特征。当我们移动到第二层时,每个节点再次与其邻居进行通信。然而,因为邻居已经在第一层中从其自身的邻居那里获取了信息,所以原始节点现在间接地访问了来自两个跳远的图中的信息——其邻居的邻居。随着这个过程在 GCN 的更多层中重复,信息在图中传播和聚合。最后,每个节点的嵌入向量包含并编码了其自身的信息以及所有相邻节点的网络信息,根据邻域的程度,或其k-跳邻域,以创建上下文嵌入。节点的k-跳邻域,或半径为k的邻域,是一组距离小于或等于k的相邻节点。
列表 11.2 展示了如何使用 GCN 生成 Cora 数据集的节点嵌入。Cora 数据集包含 2,708 篇科学出版物,分为七个类别之一。引用网络由 5,429 个链接组成。数据集中的每篇出版物都由一个 0/1 值的词向量描述,表示字典中相应单词的存在/不存在。该字典包含 1,433 个独特的单词。
使用 PyG(PyTorch Geometric)并可以按照以下方式安装:
$conda install pytorch torchvision -c pytorch
$conda install torch_scatter
$conda install torch_sparse
$conda install torch_cluster
$conda install torch-spline-conv
$conda install torch_geometric
更多关于 PyG CUDA 安装的详细信息可在 PyG 文档中找到(pytorch-geometric.readthedocs.io/en/latest/notes/installation.html)。
我们将首先导入我们将使用的库。
列表 11.2 使用 GCN 进行节点嵌入
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.pipeline import Pipeline
import torch
import torch.nn.functional as F
from torch_geometric.datasets import Planetoid
from torch_geometric.nn import GCNConv
from torch_geometric.utils import to_networkx
PyG 提供了多个可以直接加载的数据集,例如 KarateClub、Cora、Amazon、Reddit 等。Cora 数据集是 Planetoid 数据集的一部分,可以按以下方式加载:
dataset = Planetoid(root='/tmp/Cora', name='Cora')
如以下代码所示,GCN 模型使用两个 GCNConv 层(GCNConv)和一个 torch.nn.Dropout 层定义。GCNConv 是一个图卷积层,torch.nn.Dropout 是一个 dropout 层,在训练期间以概率 0.5 随机将输入张量的一些元素置零,作为一种简单的防止过拟合的方法。
forward 函数定义了模型的正向传递。它接受一个数据对象作为输入,表示图,并从输入数据中提取节点的特征和图的邻接列表。节点特征(x)通过第一个 GCN 层 conv1、一个 relu 激活函数、一个 dropout 层,最后通过第二个 GCN 层 conv2。邻接列表 edge_index 是 GCN 层中卷积操作所必需的。然后返回网络的输出:
class GCN(torch.nn.Module):
def __init__(self):
super(GCN, self).__init__()
self.conv1 = GCNConv(dataset.num_node_features, 16)
self.conv2 = GCNConv(16, dataset.num_classes)
self.dropout = torch.nn.Dropout(0.5)
def forward(self, data):
x, edge_index = data.x, data.edge_index
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, training=self.training)
x = self.conv2(x, edge_index)
return x
作为 11.2 列表的延续,以下代码片段在单个图上训练 GCN 模型并从训练模型中提取节点嵌入。model 在 200 个时期内进行训练。其梯度首先被置零,然后计算前向传递,并在训练节点(由 data.train_mask 标记的节点)上计算负对数似然损失。然后计算反向传递以获取梯度,并优化器执行一步以更新模型参数。模型被设置为评估模式,并在图上再次运行以获得最终的节点嵌入:
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') ①
model = GCN().to(device) ②
data = dataset[0].to(device) ③
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) ④
model.train() ⑤
for epoch in range(200): ⑤
optimizer.zero_grad() ⑤
out = model(data) ⑤
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask]) ⑤
loss.backward() ⑤
optimizer.step() ⑤
model.eval() ⑥
embeddings_pyg = model(data).detach().cpu().numpy() ⑦
① 如果 CUDA 可用,代码将使用 GPU;否则,它将使用 CPU。
② 创建 GCN 模型的一个实例,并将其移动到选定的设备上。
③ 加载数据集中的第一个图,并将其移动到设备上。
④ 使用学习率为 0.01 且权重衰减(一种正则化形式)为 0.0005 的 Adam 优化器。
⑤ 对模型进行 200 个时期的训练。
⑥ 设置评估模式。
⑦ 获取最终的节点嵌入。
使用 .detach() 函数将输出从计算图中分离出来,并返回一个新的不需要梯度的张量。然后,将嵌入从 GPU(如果它们在 GPU 上)移动到 CPU。这样做是为了使数据可用于进一步处理,例如将其转换为 NumPy 数组。生成的嵌入大小为 (2708, 7),其中节点数为 2,708,类别或主题数为 7。使用主成分分析(PCA)进行降维,以便如图 11.8 所示在 2D 中可视化嵌入。

图 11.8 使用 PyG 中的 GCN 进行节点嵌入
如您所见,节点嵌入使得属于同一类的节点聚集在一起。这意味着特征的可区分性增强,从而提高了预测的准确性。
书中 GitHub 仓库中可用的列表 11.2 的完整版本还展示了如何使用 StellarGraph 中的 GCN 生成节点嵌入。StellarGraph 是一个用于图和网络上的机器学习的 Python 库。
11.3.2 注意力机制
正如您在图 11.7 中看到的,GCN 中的嵌入函数包括消息传递、聚合和更新函数。消息传递函数主要根据可学习的权重矩阵 W^t 整合节点邻居的消息。这个权重矩阵并不反映相邻节点的重要性程度。卷积操作将相同的可学习权重作为线性变换应用于节点的所有邻居,而没有明确考虑它们的重要性或相关性。这可能不是理想的,因为某些部分可能需要比其他部分更多的注意力。
深度学习(DL)中“注意力”的概念本质上允许模型在生成输出序列时,有选择性地集中关注输入数据的特定部分。这种机制确保了上下文从初始阶段到末尾的维持和传播。它还允许模型通过在每个时间步集中关注输入的最重要部分来动态分配其资源。从广义上讲,深度学习中的注意力可以可视化为一个由重要性或相关性分数组成的向量。这些分数有助于量化图中节点与图中所有其他节点之间的关系或关联。
注意力即一切
具有里程碑意义的论文“Attention Is All You Need” [8] 提出了一种新的 Transformer 模型,用于处理如文本之类的序列数据。在语言处理和翻译的世界里,模型通常逐字逐句地读取整个句子或文档,按照顺序(就像我们读书时一样),然后基于此做出预测。这些模型在理解长句子和从文本中回忆信息方面存在一些困难。在长序列的情况下,到序列末尾时初始上下文可能会丢失。这被称为遗忘问题。
论文的作者们提出了一种处理这个任务的不同方法。他们的模型不是按顺序读取所有内容,而是在不同时间关注输入的不同部分,几乎就像在文本中跳跃一样。这就是他们所说的“注意力”。注意力机制允许模型动态地优先考虑对于它试图预测的每个单词来说,输入的哪些部分是最相关的,使其在理解上下文和减少长句子或复杂短语引起的混淆方面更加有效。更多细节,请参阅“Annotated Transformer” [9]。
图 11.9b 展示了图注意力网络(GAT),其中添加了一个权重因子或注意力系数α到嵌入方程中,以反映邻近节点的重要性。GAT 使用加权邻接矩阵而不是 GCN 中使用的非加权邻接矩阵(图 11.9a)。使用一个注意力机制a来计算节点对v和u之间的未归一化系数e[vu],基于它们的特征:
|

| 11.1 |
|---|
这种注意力机制的例子可以是点积注意力,它衡量两个节点特征的相似性或对齐度,提供了节点v应该给予节点u多少注意力的定量指示。其他机制可能涉及学习到的注意力权重、非线性变换或节点特征之间更复杂的交互。遵循图结构,节点v只能在其邻域内的节点i ∈ N[v]上关注。
注意力系数通常使用 softmax 函数进行归一化,以便它们是可比较的,无论原始分数在不同邻域或上下文中的规模或分布如何。请注意,在图 11.9b 中,为了简化,注意力系数α[vu]被表示为α[u]。
|

| 11.2 |
|---|

图 11.9 图卷积网络(GCN)与图注意力网络(GAT)的比较
多头注意力是 GAT 和“注意力即一切”论文中讨论的 Transformer 模型的关键组成部分。在多头注意力机制中,模型拥有多组注意力权重。每一组(或称为“头”)可以学习关注输入的不同部分。模型不再只有一个关注点,而是可以有多个关注点,使其能够捕捉数据中的不同类型的关系和模式。在 GAT 的上下文中,多头注意力机制允许图中的每个节点以不同的方式关注不同的邻近节点,如图 11.10 所示。

图 11.10 展示了具有 3 个头的多头注意力机制,节点 5 的α[52],α[54],和α[55]是节点之间的注意力系数。每个头的聚合特征被平均以获得节点的最终嵌入。
当多个头完成它们各自的关注操作后,通常会对它们的结果进行平均。这个过程将多个注意力头捕获的多样化视角压缩成一个单一的输出。在多头注意力操作的结果结合之后,随后应用一个最终的非线性变换。这一步通常涉及使用 softmax 函数或逻辑 sigmoid 函数,尤其是在分类问题中。这些函数的作用是将模型的最终输出转换为概率,使得输出更容易解释,并更适用于预测任务。
11.3.3 指针网络
序列机器学习涉及处理观察顺序重要的数据,如时间序列数据、句子或排列。根据输入和输出的数量,序列机器学习任务可以分类,如表 11.2 所示。序列到序列(seq2seq)模型接收一系列项目并输出另一个序列的项目。循环神经网络(RNN)和长短期记忆(LSTM)已被确立为 seq2seq 建模中的最先进方法。
表 11.2 序列机器学习
| 任务 | 示例 |
|---|---|
| 一对一 | 图像分类。我们提供一个单独的图像作为输入,模型输出分类或类别,如“狗”或“猫”,作为单个输出。 |
| 一对多 | 图像描述。我们将单个图像输入模型,它生成描述该图像的一系列单词。 |
| 多对一 | 情感分析。我们输入一系列单词(如句子或推文),模型输出一个单一的情感评分(如“积极”、“消极”或“中性”)。 |
| 多对多(类型 1) | 序列输入和序列输出,如命名实体识别(NER)的情况。我们输入一个句子(单词序列),模型输出识别的实体,如人、组织、地点等。 |
| 多对多(类型 2),称为同步序列模型 | 同步序列输入和输出。该模型接收一系列输入,但在读取整个序列之前不输出任何内容。然后它输出一个序列。此类的一个例子是视频分类,其中模型接收一系列视频帧作为输入,然后输出这些帧的标签序列。 |
在离散组合优化问题,如旅行商问题、排序任务或凸包问题中,输入和输出数据都是序列化的。然而,传统的 seq2seq 模型在有效解决这些问题上存在困难。这主要是因为输出元素的离散类别不是预先确定的。相反,它们取决于输入的变量大小(例如,输出字典依赖于输入长度)。指针网络(Ptr-Net)模型[10]通过利用注意力机制来指向或选择输入序列中的一个成员作为输出,来解决此问题。该模型不仅提高了配备输入注意力的传统 seq2seq 模型的性能,而且还使我们能够推广到可变大小的输出字典。
当传统的注意力机制在输入序列上分配注意力以生成输出元素时,Ptr-Net 则将注意力用作指针。这个指针用于从输入序列中选择一个元素,并将其包含在输出序列中。让我们以凸包问题作为一个离散组合优化问题的例子。凸包是一种几何形状,具体来说是一个多边形,它完全包围了一个给定的点集。它是通过优化两个不同的参数来实现的:它最大化了形状覆盖的面积,同时最小化形状的边界或周长,如图 11.11 所示。为了理解这个概念,可以想象将橡皮筋拉伸到集合的极点或顶点周围。当你释放橡皮筋时,它会自动以可能的最小周长包围整个集合,这正是凸包所做的事情。

图 11.11 凸包问题。a) 一个有效的凸包,包围了所有点,同时最大化面积和最小化周长。注意,多边形的输出序列中包含的点数可能少于给定的点数。b) 一个无效的凸包,因为周长没有最小化。c) 一个无效的凸包,因为并非所有点都被包围。
凸包在各个学科中有着多种应用。例如,在图像识别领域,凸包可以帮助确定图像中物体的形状和边界。同样,在机器人领域,它们可以通过定义机器人周围的“可达”空间来协助障碍物检测和导航。
给定点集,寻找或计算凸包的问题已经通过各种算法得到解决。例如,Graham 扫描算法根据点与凸包底部点的角度对点进行排序,然后处理它们以找到凸包[11]。Jarvis 行进(或礼物包装算法)从最左边的点开始,将剩余的点像包装礼物一样包裹起来[12]。Quickhull 算法通过递归地将集合划分为子集,选择离两个极点之间的线最远的点,并消除形成三角形内的点,直到识别出凸包的顶点[13]。
如图 11.12 所示,Ptr-Net 以平面点集 P = {P[1], P[2], …, P[n]} 作为输入,其中每个元素有 n 个,P[j] = (x[j], y[j]) 是点的笛卡尔坐标。输出 C[P] = {C[1], C[2],…, C[m][(][P][)]} 是代表与点集 P 相关的解决方案的序列。在此图中,Ptr-Net 从输入数据点 [1 2 3 4] 估计输出序列 [1 4 2]。此输出序列表示包含所有输入点、面积最大和周长最小的凸包。如图所示,凸包是通过连接 P[1],P[2],和 P[4] 形成的。第三个点 P[3] 在此凸包内部。

图 11.12 Pointer network (Prt-Net) 从输入数据点 [1 2 3 4] 估计输出序列 [1 4 2]
Ptr-Net 由三个主要组件组成:
-
编码器—编码器是一个循环神经网络(RNN),通常使用长短期记忆(LSTM)单元或门控循环单元(GRUs)实现。编码器的目的是处理输入序列,将每个输入元素转换为相应的隐藏状态。这些隐藏状态 (e[1],…, e[n]) 封装了输入序列中元素的上下文相关表示。
-
解码器—与编码器一样,解码器也是一个 RNN。它负责生成输出序列 (d[1],…, d[m])。对于每个输出步骤,它将前一个输出及其自身的隐藏状态作为输入。
-
注意力机制(指针)—Ptr-Net 中的注意力机制作为一个指针操作。它计算编码器输出的隐藏状态上的分布,指示每个输出步骤在输入序列中的“指向”位置。本质上,它决定哪些输入应该是下一个输出。注意力机制是学习到的注意力分数上的 softmax 函数,它给出了输入序列上的概率分布,表示每个元素被指向的可能性。
在每个输出时间 i 计算注意力向量使用以下方程:
|

| 11.3 |
|---|
|

| 11.4 |
|---|
|

| 11.5 |
|---|
其中
-
u[j] 是表示解码器和编码器隐藏状态之间相似性的注意力向量或对齐分数。v,W[1],和 W[2] 是模型的可学习参数。如果编码器和解码器使用相同的隐藏维度(通常为 512),则 v 是一个向量,而 W[1] 和 W[2] 是方阵。
-
a[j] 是对输入或通过应用 softmax 操作到对齐分数计算出的权重上的注意力掩码。
-
d[i]'是每个时间步输入到解码器中的上下文向量。换句话说,d[i]和d[i]'被连接起来,用作预测的隐藏状态。所有编码器隐藏状态的加权总和允许解码器灵活地将注意力集中在输入序列的最相关部分。
Ptr-Net 可以处理可变长度的序列并解决复杂的组合问题,特别是那些涉及排序或排序任务的问题,其中输出是输入的排列,正如你将在第 11.9 节中看到的。
11.4 自组织映射
自组织映射(SOM),也称为自组织特征映射(SOFM)或Kohonen 映射,是一种人工神经网络(ANN),通过无监督学习进行训练,以产生训练样本输入空间的低维(通常是二维)离散表示,称为映射。SOMs 与传统 ANN 的区别在于其学习过程的性质,称为竞争学习。在这些算法中,处理元素或神经元为了响应输入数据的一个子集而竞争。输出神经元激活的程度随着神经元权重向量和输入之间的相似性增加而增强。权重向量和输入之间的相似性,导致神经元激活,通常通过计算欧几里得距离来衡量。在响应特定输入时,表现出最高激活水平或等效的最短距离的输出单元被认为是最佳匹配单元(BMU)或“获胜”神经元,如图 11.13 所示。然后,通过调整其权重,将这个获胜者逐渐调整到输入数据点更近的位置。

图 11.13 带高斯邻域函数的自组织映射(SOM)
SOM 的一个关键特性是邻域函数的概念,它确保不仅获胜神经元,而且其邻居也会从每个新的输入中学习,从而创建相似数据的簇。这使得网络能够保留输入空间的整体拓扑属性。方程 11.6 展示了邻域函数的一个例子:
|

| 11.6 |
|---|
其中 v 是地图中节点的索引,u 是获胜神经元的索引,LDist(u,v) 代表 u 和 v 之间的晶格距离,而 σ 是高斯核的带宽。在 SOMs 中,σ* 代表邻域的半径或宽度,并决定了获胜神经元的权重更新阶段对其邻居的影响范围。大的 σ 意味着影响范围更广的邻域。另一方面,小的 σ 意味着受影响的邻近神经元更少。当 σ 设置为极小值时,邻域实际上缩小到只包括获胜神经元本身。这意味着只有获胜神经元的权重会因输入而显著更新,而其他神经元的权重几乎或根本不受影响。这种只更新获胜神经元的行为被称为“胜者全得”学习。
算法 11.1 展示了 SOM 的步骤,假设 D[t] 是目标输入数据向量,W[v] 是节点 v 的当前权重向量,θ(u,v,s*) 是表示获胜神经元距离引起的约束的邻域函数,而 α 是学习率,其中 α ∈ (0,1)。
算法 11.1 自组织映射(SOM)
Randomly initialize the weights of each neuron
For each step s=1 to iteration limit:
Randomly pick an input vector from the dataset
Traverse each node in the map
Calculate Euclidean distance as a similarity measure
Determine the node that produces the smallest distance (winning neuron)
Adapt the weights of each neuron v according to the following rule
Wv(s+1)=Wv(s)+ α(s).θ(u,v,s).‖Dt-Wv(s)‖
SOMs 最初被用作数据可视化和聚类任务的数据降维方法。例如,Kohonen 的 SOM 算法的早期应用之一是神经语音打字机。这是一个系统,其中可以识别和将语音音素(可以区分一个词与另一个词的最小语音单位)转换为符号。当有人向系统说话时,SOM 会分类输入的音素并输入相应的符号。SOMs 可以应用于不同的问题,如特征提取、自适应控制和旅行商问题(见第 11.8 节)。
SOMs 的一个显著优势在于它们保留了输入空间内计算出的点之间的相对距离。在输入空间中靠近的点被映射到 SOM 中的邻近单元,这使得 SOM 成为分析高维数据中簇的有效工具。当使用主成分分析(PCA)等技术处理高维数据时,在将维度降低到二维时可能会发生数据丢失。如果数据包含许多维度,并且每个维度都携带有价值的信息,那么 SOMs 在降维方面可能优于 PCA。除此之外,SOMs 还具有泛化的能力。通过这个过程,网络可以识别或分类它以前未遇到的输入数据。这种新的输入与地图上的特定单元相关联,因此相应地映射。
前几节提供了机器学习的基本基础,为你提供了必要的背景知识。接下来的几节将深入探讨监督和无监督机器学习在解决优化问题中的实际应用。
11.5 优化问题的机器学习
利用机器学习技术解决组合优化问题代表了一个新兴且令人兴奋的研究领域。"神经组合优化"指的是将机器学习和神经网络模型,特别是 seq2seq 监督模型、无监督模型和强化学习,应用于解决组合优化问题。在此背景下,Yoshua Bengio 及其合作者[14]全面描述了机器学习在组合优化中的应用。作者描述了三种利用机器学习解决组合优化问题的独特方法(见图 11.14):

图 11.14 机器学习(ML)在组合优化(CO)问题中的应用
-
端到端学习—为了使用机器学习解决优化问题,我们需要指导机器学习模型直接从输入实例中制定解决方案。Ptr-Net 是这种方法的例子,它在m个点上训练,在n个点上验证欧几里得平面对称 TSP [10]。使用端到端学习解决组合优化问题的例子在 11.6、11.7 和 11.9 节中提供。
-
学习配置算法—第二种方法涉及将机器学习模型应用于组合优化算法,并利用相关信息进行增强。在这方面,机器学习可以提供算法的参数化。此类参数的例子包括但不限于梯度下降方法中的学习率或步长;模拟退火中的初始温度或冷却计划;遗传算法中高斯变异的标准差或选择性交叉;粒子群优化(PSO)中的惯性权重或认知和社会加速系数;或者蚁群优化(ACO)中蒸发率、信息素沉积的影响或状态转换的期望影响。
-
与优化算法结合的机器学习——第三种方法要求使用组合优化算法反复咨询相同的机器学习模型进行决策。机器学习模型接受算法当前状态作为输入,这可能包括问题定义。与另外两种方法的基本区别在于,组合优化算法重复使用相同的机器学习模型来做出相同类型的决策,大约与算法的总迭代次数一样多。这种方法的例子是深度学习辅助启发式树搜索(DLTS),其中决策树搜索中关于探索哪些分支以及如何界定节点由深度神经网络(DNNs)[15]做出。
Vesselinova 等人的一篇引人入胜的研究论文深入探讨了机器学习和组合优化交叉的一些相关问题[16]。具体来说,该论文研究了以下问题:
-
能否利用机器学习技术自动化组合优化任务的学习启发式过程,从而更有效地解决这些问题?
-
解决这些现实世界问题都使用了哪些基本的机器学习方法?
-
这些方法在实用领域中的适用性如何?
本文对监督学习和强化学习策略在解决优化问题中的应用进行了全面综述。作者通过考察这些学习方法在一系列优化问题中的应用来分析这些学习途径:
-
背包问题(KP),目标是最大化所选物品的总价值,同时不超过背包的容量
-
最大团(MC)和最大独立集(MIS)问题,这两个问题都涉及识别具有特定属性的图子集
-
最大覆盖问题(MCP),需要选择一个子集以最大化覆盖范围
-
最大切割(MaxCut)和最小顶点覆盖(MVC)问题,这些问题涉及以特定方式划分图
此外,该论文还讨论了机器学习方法在可满足性问题(SAT)中的应用,这是一个涉及布尔逻辑的决策问题;经典的旅行商问题(TSP),需要找到访问给定城市集并返回起点城市的最短路径;以及车辆路径问题(VRP),这是 TSP 的推广版本,允许有多个“销售人员”(车辆)。关于基准优化问题的更多信息请参见附录 B。
Chengrun 等人在“大型语言模型作为优化器”文章中描述了通过提示(OPRO)进行优化,这是一种简单而有效的方法,用于使用大型语言模型(LLMs)作为优化器,其中优化任务以自然语言描述 [17]。更多展示机器学习在解决优化问题中应用的示例可以通过 AI for Smart Mobility 发布中心获取(medium.com/ai4sm)。为了进一步激发探索并吸引更多研究人员进入这个新兴领域,作为神经信息处理系统(NeurIPS)会议的一部分,组织了一场名为机器学习组合优化(ML4CO)的比赛。比赛为参与者提出了一个独特的命题,要求他们设计针对解决三个不同挑战的机器学习模型或算法。这些挑战中的每一个都反映了在传统优化求解器中常见的一个特定控制任务。这场比赛提供了一个平台,研究人员可以在其中探索和测试新的机器学习策略,从而推动组合优化领域的发展。
11.6 使用监督机器学习解决函数优化
摊销优化,或 学习优化,是一种使用机器学习模型快速预测优化问题解的方法。摊销优化方法试图学习决策变量空间与最优或近似最优解空间之间的映射。学习到的模型可以用来预测目标函数的最优值,从而实现快速求解器。优化过程的计算成本在学习和推理之间分散。这就是“摊销优化”这个名称的由来,因为“摊销”一词通常指的是分散成本。
B. Amos 在他的教程 [18] 中展示了如何使用摊销优化来解决优化问题的几个示例。例如,一个监督机器学习方法可以学习解决球面上的优化问题。在这里,目标是找到定义在地球或其他空间上、可以用球面形式近似的函数的极值
|

| 11.7 |
|---|
其中 S² 是嵌入在实数空间 R³ 中的单位 2-球面,表示为 S²:= {y ∈ R³ | ||y||[2] =1},而 x 是函数 f : S² × X → R 的某种参数化。||y||[2] 指的是向量 y 的欧几里得范数(也称为 L2 范数 或 2-范数)。关于摊销目标函数的更多细节可以在 Amos 的“关于在连续域上学习优化的摊销优化教程”中找到 [18]。
列表 11.3 展示了使用基于监督学习的摊销优化来解决在地球或其他空间上定义的函数的极值问题的步骤。我们将首先定义两个转换函数,celestial_to_euclidean() 和 euclidean_to_celestial(),它们将天球坐标(赤经 ra 和赤纬 dec)与欧几里得坐标(x, y, z)之间进行转换。
天球坐标系
天文或天球坐标系是用于指定天空中物体位置(如卫星、恒星、行星、星系和其他天体)的参考系。有几个天球坐标系,其中最常见的是赤道系统。在赤道系统中,赤经(RA)和赤纬(Dec)是用于确定天空中物体位置的两个数字。这些坐标与地球地理坐标系中使用的纬度和经度相似。
如下图中所示,赤经(RA)是以小时、分钟和秒(hⓂ️s)来测量的,并且与地球坐标系中的经度相似。赤经是从春分点(太阳在 3 月春分时穿过天赤道的点)沿天赤道向东测量的一个物体的角距离。天赤道是位于天球上的一个想象的大圆,位于与地球赤道相同的平面上。赤纬(Dec)是以度来测量的,表示一个物体在天赤道北或南的角距离。它与地球坐标系中的纬度相似。

具有赤经 10 小时和赤纬 30 度的示例点的天球坐标系
正赤纬用于天赤道以上的物体,负赤纬用于天赤道以下的物体。
sphere_dist(x, y) 函数计算欧几里得空间中球面上两点之间的黎曼距离(大圆距离)。这个距离代表两点之间最短(测地线)路径,沿着球面测量而不是穿过球体内部。该函数断言输入向量是二维的。然后它计算 x 和 y 的点积,并返回结果的反余弦值,这对应于 x 和 y 之间的角度。
列表 11.3 使用监督学习解决函数优化问题
import torch
from torch import nn
import numpy as np
from tqdm import tqdm
import matplotlib.pyplot as plt
def celestial_to_euclidean(ra, dec): ①
x = np.cos(dec)*np.cos(ra)
y = np.cos(dec)*np.sin(ra)
z = np.sin(dec)
return x, y, z
def euclidean_to_celestial(x, y, z): ②
sindec = z
cosdec = (x*x + y*y).sqrt()
sinra = y / cosdec
cosra = x / cosdec
ra = torch.atan2(sinra, cosra)
dec = torch.atan2(sindec, cosdec)
return ra, dec
def sphere_dist(x,y): ③
if x.ndim == 1:
x = x.unsqueeze(0)
if y.ndim == 1:
y = y.unsqueeze(0)
assert x.ndim == y.ndim == 2
inner = (x*y).sum(-1)
return torch.arccos(inner)
① 将天球坐标转换为欧几里得坐标。
② 将欧几里得坐标转换为天球坐标。
③ 计算球面上两点之间的黎曼距离。
我们接着定义一个c-convex类作为nn.Module的子类,这使得它成为 PyTorch 中的一个可训练模型。Cohen 及其合作者在他们的“黎曼凸势映射”文章中将c-凸定义为在球面上定义的优化问题的合成类 [19]。c-convex类在球面上模拟一个具有n_components个分量的 c-凸函数,我们可以从中采样数据用于训练。gamma参数控制函数分量的聚合,seed用于初始化随机数生成器以实现可重复性。它还为 c-凸函数的每个分量生成随机参数ys(它们是 3D 空间中的单位向量)和alphas(它们是介于 0 和 0.7 之间的标量)。参数被连接成一个单一的params向量。forward(xyz)方法计算在点xyz处的 c-凸函数的值:
class c_convex(nn.Module): ①
def __init__(self, n_components=4, gamma=0.5, seed=None):
super().__init__()
self.n_components = n_components
self.gamma = gamma
if seed is not None: ②
torch.manual_seed(seed) ②
self.ys = torch.randn(n_components, 3) ②
self.ys = self.ys / torch.norm(self.ys, 2, dim=-1, keepdim=True) ②
self.alphas = .7*torch.rand(self.n_components) ②
self.params = torch.cat((self.ys.view(-1), self.alphas.view(-1))) ②
def forward(self, xyz): ③
cs = [] ③
for y, alpha in zip(self.ys, self.alphas): ③
ci = 0.5*sphere_dist(y, xyz)**2 + alpha ③
cs.append(ci)
cs = torch.stack(cs)
if self.gamma == None or self.gamma == 0.:
z = cs.min(dim=0).values
else:
z = -self.gamma*(-cs/self.gamma).logsumexp(dim=0)
return z
① 定义一个 c-凸函数。
② 样本随机参数。
③ 计算在球面上给定输入坐标 xyz 的 c-凸函数的输出。
作为前面代码的延续,我们定义了一个摊销模型,它接受一个参数向量作为输入,并输出表示球面上一点的 3D 向量。摊销模型使用神经网络来学习从参数空间到球面上点的 3D 空间的映射。代码还初始化了一个具有不同种子的c_convex对象列表,并设置了摊销模型的参数数量:
seeds = [8,9,2,31,4,20,16,7] ①
fs = [c_convex(seed=i) for i in seeds] ②
n_params = len(fs[0].params) ③
① 创建一个表示不同种子的整数列表。
② 创建一个包含 c_convex 类不同实例的 fs 列表。
③ 设置第一个 c_convex 对象(fs[0])的参数数量。
在以下代码中,摊销模型表示为nn.Module。神经网络被定义为包含三个具有 ReLU 激活函数的全连接(线性)层的前馈神经网络或多层感知器:
class AmortizedModel(nn.Module):
def __init__(self, n_params): ①
super().__init__()
self.base = nn.Sequential(
nn.Linear(n_params, n_hidden),
nn.ReLU(inplace=True),
nn.Linear(n_hidden, n_hidden),
nn.ReLU(inplace=True),
nn.Linear(n_hidden, 3)
) ②
def forward(self, p): ③
squeeze = p.ndim == 1
if squeeze:
p = p.unsqueeze(0)
assert p.ndim == 2
z = self.base(p)
z = z / z.norm(dim=-1, keepdim=True)
if squeeze:
z = z.squeeze(0)
return z
① 将用作神经网络输入的 c-凸函数中的参数数量
② 按顺序定义神经网络的层。
③ 定义摊销模型的前向传递,它将输入 p(参数向量)映射到球面上的一个点。
我们现在可以训练一个摊销模型来学习从参数向量到球面上点的映射。它使用具有不同随机种子的 c_convex 函数(fs)列表来生成训练数据。摊销模型使用 Adam 优化器进行训练,其进度使用 tqdm 进度条进行可视化。球面上的输出点存储在张量 xs 中:
n_hidden = 128 ①
torch.manual_seed(0) ②
model = AmortizedModel(n_params=n_params) ③
opt = torch.optim.Adam(model.parameters(), lr=5e-4) ④
xs = [] ⑤
num_iterations = 100
pbar = tqdm(range(num_iterations), desc="Training Progress")
for i in pbar: ⑥
losses = [] ⑦
xis = [] ⑦
for f in fs: ⑧
pred_opt = model(f.params)
xis.append(pred_opt)
losses.append(f(pred_opt))
with torch.no_grad(): ⑧
xis = torch.stack(xis)
xs.append(xis)
loss = sum(losses)
opt.zero_grad()
loss.backward()
opt.step()
pbar.set_postfix({"Loss": loss.item()})
xs = torch.stack(xs, dim=1)
① 设置 AmortizedModel 神经网络的隐藏单元数量。
② 设置随机种子以确保训练过程的可重复性。
③ 创建 AmortizedModel 的一个实例。
④ 创建一个学习率为 0.0005 的 Adam 优化器来更新参数。
⑤ 存储每次训练迭代的球面上的输出点。
⑥ 训练循环
⑦ 存储每个 c_convex 函数及其对应的球面输出点(xis)的损失。
⑧ 遍历列表 fs 中的每个 c_convex 函数(f)。
训练完成后,所有预测的输出点沿新维度堆叠在球面上,从而得到一个形状为“迭代次数,c_convex 函数数量,3”的张量 xs。这个张量中的每个元素代表在训练的不同阶段由摊销模型预测的球面上的一个点。它生成了摊销模型和 c_convex 函数的训练进度可视化表示,如图 11.15 所示。

图 11.15 训练的摊销模型输出的示例
列表 11.3 的完整版本可在本书的 GitHub 仓库中找到。它创建了一个天体坐标网格,评估网格上的 c_convex 函数和摊销模型,然后绘制函数、预测路径和球面上的最优点的等高线图。最优点是给出最小损失的点,前提是使用监督学习来训练摊销模型。
11.7 使用监督图机器学习解决 TSP
Joshi、Laurent 和 Bresson 在他们的“图神经网络用于旅行商问题”文章[20]中提出了一种通用的端到端流程来解决组合优化问题,如旅行商问题(TSP)、车辆路径问题(VRP)、可满足性问题(SAT)、最大切割(MaxCut)和最大独立集(MIS)。图 11.16 显示了使用机器学习解决 TSP 的步骤。

图 11.16 组合优化问题的端到端流程
按照这种方法,我们首先以节点特征和节点之间的邻接矩阵的形式定义图问题。然后,基于消息传递方法,使用 GNN 或 GCN 生成低维图嵌入。使用多层感知器(MLPs)预测节点或边属于解决方案的概率。然后,应用图搜索,如束搜索(见第四章),通过边的概率分布搜索图,以找到可行的候选解决方案。应用模仿学习(监督学习)和探索学习(强化学习)。监督学习最小化最优解(在 TSP 的情况下,由 Concorde 等知名求解器获得)与模型预测之间的损失。强化学习方法使用策略梯度来最小化解码结束时模型预测的旅行长度。强化学习将在下一章讨论。
从头开始训练一个机器学习模型并将其应用于解决 TSP 需要大量的代码和数据预处理。列表 11.4 展示了如何使用预训练模型来解决 TSP 的不同实例。我们首先导入我们将使用的库和模块。这些库提供了处理数据、执行计算、可视化和优化的功能。Gurobi 库用于优化过程中的子回路消除以及计算一组点的降低成本(见附录 A)。我们设置CUDA_DEVICE_ORDER和CUDA_VISIBLE_DEVICES环境变量以控制 GPU 设备的可见性。
列表 11.4 使用监督机器学习解决 TSP
import os
import math
import itertools
import numpy as np
import networkx as nx
from scipy.spatial.distance import pdist, squareform
import seaborn as sns
import matplotlib.pyplot as plt
import torch
from torch.utils.data import DataLoader
from torch.nn import DataParallel
from learning_tsp.problems.tsp.problem_tsp import TSP
from learning_tsp.utils import load_model, move_to
from gurobipy import *
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
作为延续,以下opts类包含几个类级别属性,定义了以下选项和配置:
-
dataset path—书中 GitHub 仓库中可用的 TSP 数据集。 -
batch size—这决定了在训练或评估期间同时处理的 TSP 实例(问题)数量。它指定了将多少个 TSP 实例分组在一起并行处理。 -
number of samples—这是每个 TSP 大小下的样本数量。 -
neighbors—在 TSP 数据处理管道中使用,用于指定用于图稀疏化的最近邻的比例(百分比)。它通过为每个节点选择最近邻的子集来控制 TSP 图的连通性。 -
knn strategy—这是在执行图稀疏化时确定最近邻数量的策略。在代码中,'percentage'值表示最近邻的数量由neighbors参数确定,该参数指定了要考虑的邻居百分比。 -
model—这是预训练机器学习模型的路径。所使用的模型是书中 GitHub 仓库中可用的预训练 GNN 模型。 -
use_cuda—这检查系统上是否有 CUDA 可用。CUDA 是一个并行计算平台和编程模型,允许在 NVIDIA GPU 上高效执行计算。torch.cuda.is_available()返回一个布尔值(true 或 false),指示 CUDA 是否可用。如果 CUDA 可用,则表示系统上存在兼容的 NVIDIA GPU,并且可以用于加速计算。 -
device—这是用于计算的设备:
class opts:
dataset_path = "learning_tsp/data/tsp20-50_concorde.txt"
batch_size = 16
num_samples = 1280
neighbors = 0.20
knn_strat = 'percentage'
model =
➥ "learning_tsp/pretrained/tspsl_20-50/sl-ar-var-20pnn-gnn-max_20200308T172931"
use_cuda = torch.cuda.is_available()
device = torch.device("cuda:0" if use_cuda else "cpu")
下一步是使用 TSP 类和以下参数创建一个数据集对象:
-
filename—要使用的数据集的路径或文件名,由opts .dataset_path指定 -
batch_size—每个批次中包含的样本数量,由opts.batch_size指定 -
num_samples—要在数据集中包含的总样本数量,由opts.num_samples指定 -
neighbors—表示用于图稀疏化的最近邻数量的值,由opts.neighbors指定 -
knn_strat—选择最近邻的策略('percentage'或None),由opts.knn_strat指定 -
supervised—一个布尔值,指示数据集是否用于监督学习,设置为True
make_dataset方法创建 TSP 数据集类的实例,并用提供的参数初始化它,返回dataset对象:
dataset = TSP.make_dataset(
filename=opts.dataset_path, batch_size=opts.batch_size,
➥ num_samples=opts.num_samples,
➥ neighbors=opts.neighbors, knn_strat=opts.knn_strat, supervised=True
)
以下行创建了一个数据加载器对象,它允许以批处理方式方便地迭代数据集,这在评估期间处理数据时很有用。上一行创建的dataset对象将用作数据源。您还可以提供其他可选参数来自定义数据加载器的行为,例如shuffle(用于打乱数据)和num_workers(用于指定数据加载的工作进程数):
dataloader = DataLoader(dataset, batch_size=opts.batch_size, shuffle=False,
➥ num_workers=0)
现在我们可以加载训练好的模型并将其分配给model变量。如果模型被torch.nn.DataParallel包装,它将通过访问model.module提取底层模块。DataParallel是 PyTorch 的一个包装器,它允许在多个 GPU 上并行执行模型。如果模型确实是DataParallel的实例,它将通过访问module属性提取底层模型模块。这一步是必要的,以确保在访问模型属性和方法时保持一致的行为。然后设置模型的解码类型为"greedy"。这意味着在推理或评估期间,模型应使用贪婪解码策略来生成输出预测:
model, model_args = load_model(opts.model, extra_logging=True) ①
model.to(opts.device)
if isinstance(model, DataParallel): ②
model = model.module ②
model.set_decode_type("greedy") ③
model.eval() ④
① 加载预训练模型。
② 提取底层模块。
③ 将模型的解码类型设置为"greedy"。
④ 将模型的模式设置为评估模式
列表 11.4 的完整版本,包括可视化代码,可在本书的 GitHub 仓库中找到。图 11.17 显示了预训练 ML 模型为 TSP50 实例生成的输出。

图 11.17 使用预训练 ML 模型的 TSP50 解决方案
该图显示了与 TSP 实例和模型预测相关的以下七个图表:
-
Concorde—左上角的图表显示了 Concorde 求解器生成的真实解决方案,Concorde 是解决 TSP 实例以最优性为目的的分支和切割算法的高效实现。它显示 TSP 问题的节点作为由边连接的圆圈,代表 Concorde 计算出的最优回路。图表的标题表明了从 Concorde 获得的路程(成本)长度。
-
1 - (降低成本)—第二个图表包含最短子回路,并使用 Gurobi 优化库显示了这些子回路中点的降低成本。它以红色线条显示 TSP 的边,边颜色表示降低的成本值。
-
预测热图—第三个图表展示了模型对 TSP 问题的预测的热图可视化。它使用颜色尺度来表示边的预测概率,较高的概率以较深的阴影显示。
-
贪婪解—第四幅图展示了 ML 模型使用贪婪解码策略生成的解决方案。它显示了通过边连接的 TSP 问题的节点,代表模型获得的路线。图的标题显示了模型计算出的路线长度(成本)。
-
欧几里得距离(最大值归一化)—左下角的图是 TSP 问题中节点之间欧几里得距离的热力图可视化。它使用颜色尺度来表示距离,较浅的色调表示较小的距离。
-
降低成本—中间下方的图是 TSP 问题中边降低成本的热力图表示。它以颜色尺度显示降低的成本,较低值以较浅的色调显示。
-
1 - (模型预测)—右下角的图展示了模型对 TSP 问题预测的热力图可视化,类似于第三幅图。然而,在这种情况下,热力图通过从 1 减去模型的预测概率来显示“1 - (模型预测)”。较深的色调代表较低的概率,表明对边选择的信心更强。
本例演示了如何使用预训练的 GNN 模型来解决 TSP。图 11.17 显示了模型解决方案与 Concorde TSP 求解器对包含 50 个兴趣点的 TSP 实例结果的并排显示。更多信息及完整代码,包括模型训练步骤,可在“Learning the Travelling Salesperson Problem Requires Rethinking Generalization” GitHub 仓库[21]中找到。
11.8 使用无监督机器学习解决 TSP
作为无监督 ML 方法的一个例子,列表 11.5 展示了我们如何使用自组织映射(SOMs)来解决 TSP。我们首先导入我们将使用的库。一些辅助函数是从 Vicente 的博客文章[22]中描述的 som-tsp 实现导入的,用于读取 TSP 实例、获取邻域、获取路线、选择最近的候选者以及计算路线距离和绘制路线。我们从提供的 URL 读取 TSP 实例,并获取城市并将它们的坐标归一化到[0, 1]范围内。
列表 11.5 使用无监督学习解决 TSP
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
import requests
import os
from tqdm import tqdm
from som_tsp.helper import read_tsp, normalize, get_neighborhood, get_route,
➥ select_closest, route_distance, plot_network, plot_route
url = 'https://raw.githubusercontent.com/Optimization-Algorithms-Book/Code-
➥Listings/256207c4a8badc0977286c48a6e1cfd33237a51d/Appendix%20B/data/TSP/' ①
tsp='qa194.tsp' ②
response = requests.get(url+tsp) ③
response.raise_for_status() ③
problem_text = response.text ③
with open(tsp, 'w') as file: ③
file.write(problem_text) ③
problem = read_tsp(tsp) ④
cities = problem.copy() ⑤
cities[['x', 'y']] = normalize(cities[['x', 'y']]) ⑤
① 定义 TSP 实例所在的 URL。
② TSP 实例
③ 如果不存在,则下载文件。
④ 读取 TSP 问题。
⑤ 获取归一化的城市集合(坐标在[0,1]范围内)。
我们现在可以设置各种参数并初始化 SOM 的神经元网络:
number_of_neurons = cities.shape[0] * 8 ①
iterations = 12000 ②
learning_rate=0.8 ③
network = np.random.rand(number_of_neurons, 2) ④
① 种群大小是城市数量的 8 倍。
② 设置迭代次数。
③ 设置学习率。
④ 生成足够的神经元网络。
作为延续,以下代码片段实现了 SOM 的训练循环。此循环使用tqdm显示进度条,遍历指定的训练迭代次数:
route_lengths = [] ①
paths_x = [] ②
paths_y = [] ②
for i in tqdm(range(iterations)): ③
if not i % 100:
print('\t> Iteration {}/{}'.format(i, iterations), end="\r") ④
city = cities.sample(1)[['x', 'y']].values ⑤
winner_idx = select_closest(network, city) ⑥
gaussian = get_neighborhood(winner_idx, number_of_neurons // 10,
➥ network.shape[0]) ⑦
network += gaussian[:, np.newaxis] * learning_rate * (city - network) ⑧
paths_x.append(network[:, 0].copy()) ⑨
paths_y.append(network[:, 1].copy()) ⑨
learning_rate = learning_rate * 0.99997 ⑩
number_of_neurons = number_of_neurons * 0.9997 ⑩
if not i % 1000: ⑪
plot_network(cities, network, name='diagrams/{:05d}.png'.format(i))
if number_of_neurons < 1: ⑫
print('Radius has completely decayed, finishing execution',
➥ 'at {} iterations'.format(i))
break
if learning_rate < 0.001:
print('Learning rate has completely decayed, finishing execution',
➥ 'at {} iterations'.format(i))
break
route = get_route(cities, network) ⑬
problem = problem.reindex(route) ⑬
distance = route_distance(problem) ⑬
route_lengths.append(distance) ⑬
else:
print('Completed {} iterations.'.format(iterations)) ⑭
① 在 SOM 训练迭代过程中存储 TSP 路线的长度。
② 在训练迭代过程中存储网络中神经元的 x 和 y 坐标。
③ 训练循环
④ 只有当当前迭代索引是 100 的倍数时才打印。
⑤ 选择一个随机城市。
⑥ 在 SOM 网络中找到最接近随机选择城市的神经元的索引(获胜者)。
⑦ 生成一个应用于获胜者高斯滤波器的过滤器。
⑧ 更新网络的权重。
⑨ 将当前坐标附加到路径上。
⑩ 在每次迭代中衰减学习率和邻域半径 n,以逐渐减少高斯滤波器随时间对高斯滤波器的影响。
⑪ 检查绘图间隔。
⑫ 检查是否有任何参数已经完全衰减。
⑬ 计算距离,并将其存储在 route_lengths 列表中。
⑭ 指示已完成的指定数量的训练迭代次数。
以下代码片段绘制了每次迭代的路线长度。
plt.figure(figsize=(8, 6))
plt.plot(range(len(route_lengths)), route_lengths, label='Route Length')
plt.xlabel('Iterations')
plt.ylabel('Route Length')
plt.title('Route Length per Iteration')
plt.grid(True)
plt.show()
图 11.18 展示了每次迭代的路线长度。最终路线长度为 9,816,卡塔尔 TSP 实例使用的最优长度为 qa194.tsp,为 9,352。

图 11.18 SOM 对卡塔尔 TSP 的迭代路线长度。最终路线长度为 9,816,最优解为 9,352。
列表 11.5 的完整版本可在本书的 GitHub 仓库中找到,它包含基于 MiniSom 的实现。MiniSom 是 SOM 的最小化、基于 Numpy 的实现。您可以使用 !pip install minisom 安装此库。然而,MiniSom 获得的路线为 11,844.47,这远远低于此 TSP 实例的最优长度 9,352。为了提高结果,您可以尝试提供的代码并尝试调整 SOM 参数,如神经元数量、sigma、学习率和迭代次数。
11.9 寻找凸包
Ptr-Net 可以使用监督学习方法来解决凸包问题,如 Vinyals 及其合著者在他们的“指针网络”文章 [10] 中所述。Ptr-Net 有两个关键组件:一个编码器和一个解码器,如图 11.19 所示。

图 11.19 使用 Ptr-Net 解决凸包问题。每一步的输出是指向最大化概率分布的输入的指针。
编码器,一个循环神经网络(RNN),将原始输入序列转换为。在这种情况下,它将我们想要确定凸包的点协调到更易于管理的表示。
此编码向量随后传递给解码器。该向量作为基于内容注意力机制的内容调节器,该机制应用于输入。基于内容的注意力机制可以比作一个聚光灯,在不同时间突出显示输入数据的各个部分,专注于手头任务的最相关部分。
此注意力机制的输出是一个具有与输入长度相等的字典大小的 softmax 分布。这个 softmax 分布为输入序列中的每个点分配概率。这种设置允许 Ptr-Net 在每一步以概率方式决定下一个应该添加到凸包的点。这是基于当前输入状态和网络内部状态确定的。训练过程重复进行,直到网络为每个点做出决定,从而得到凸包问题的完整解决方案。
列表 11.6 展示了使用指针网络解决凸包问题的步骤。我们首先导入几个必要的库和模块,如 torch、numpy 和 matplotlib。根据 McGough 在“Transformer 中的指针网络”文章 [23] 中提供的实现,导入三个辅助类 Data、ptr_net 和 Disp。它们包含生成训练和验证数据、定义指针网络架构和可视化结果的功能。此代码为训练和验证分别生成两个数据集。这些数据集由随机的 2D 点组成,每个样本中的点数(凸包问题的输入)介于 min_samples 和 max_samples 之间。Scatter2DDataset 是一个自定义数据集类,用于生成这些随机的 2D 点数据集。
列表 11.6 使用指针网络解决凸包问题
import numpy as np
import torch
from torch.utils.data import DataLoader
import matplotlib.pyplot as plt
from scipy.spatial import ConvexHull
from ptrnets.Data import display_points_with_hull, cyclic_permute,
➥ Scatter2DDataset,Disp_results
from ptrnets.ptr_net import ConvexNet, AverageMeter, masked_accuracy,
➥ calculate_hull_overlap
min_samples = 5
max_samples = 50
n_rows_train = 100000
n_rows_val = 1000
torch.random.manual_seed(231)
train_dataset = Scatter2DDataset(n_rows_train, min_samples, max_samples)
val_dataset = Scatter2DDataset(n_rows_val, min_samples, max_samples)
运行此代码生成 100,000 个训练点和 1,000 个验证点。然后我们可以设置指针网络的参数。这些参数包括一个包含以下标记的 TOKENS 字典:
-
<eos>—索引为 0 的序列结束标记 -
c_inputs—模型输入特征的数量 -
c_embed—嵌入维度数量 -
c_hidden—模型中的隐藏单元数量 -
n_heads—多头自注意力机制中的注意力头数量 -
n_layers—模型中的层数 -
dropout—Dropout 概率,用于正则化 -
use_cuda—一个布尔标志,指示是否使用 CUDA(GPU)或 CPU -
n_workers—DataLoader 中数据加载的工作线程数量
训练参数包括 n_epochs(训练轮数)、batch_size(训练期间使用的批量大小)、lr(优化器的学习率)和 log_interval(记录训练进度的间隔)。代码检查 CUDA(GPU)是否可用,并相应地设置 device 变量:
TOKENS = {'<eos>': 0 }
c_inputs = 2 + len(TOKENS)
c_embed = 16
c_hidden = 16
n_heads = 4
n_layers = 3
dropout = 0.0
use_cuda = True
n_workers = 2
n_epochs = 5
batch_size = 16
lr = 1e-3
log_interval = 500
device = torch.device("cuda" if torch.cuda.is_available() and use_cuda else "cpu")
作为延续,我们使用指定的 batch_size 和 num_workers 加载训练和验证数据:
train_loader = DataLoader(train_dataset, batch_size=batch_size,
➥ num_workers=n_workers)
val_loader = DataLoader(val_dataset, batch_size=batch_size,
➥ num_workers=n_workers)
ConvexNet 模型是一个 Ptr-Net 模型,它被实现为一个具有编码器和解码器的转换器架构,使用 nn.TransformerEncoderLayer 并应用多头自注意力。完整的代码在书籍的 GitHub 仓库中的 ptr_net.py 类中可用。该模型使用预定义的超参数初始化。AverageMeter 类用于在训练和验证期间跟踪平均损失和准确率:
model = ConvexNet(c_inputs=c_inputs, c_embed=c_embed, n_heads=n_heads,
➥ n_layers=n_layers, dropout=dropout, c_hidden=c_hidden).to(device) ①
optimizer = torch.optim.Adam(model.parameters(), lr=lr) ②
criterion = torch.nn.NLLLoss(ignore_index=TOKENS['<eos>']) ③
train_loss = AverageMeter() ④
train_accuracy = AverageMeter() ④
val_loss = AverageMeter() ④
val_accuracy = AverageMeter() ④
① 创建 ConvexNet 模型。
② 使用 Adam 优化器来训练模型。
③ 使用负对数似然损失作为损失函数。
④ 在训练和验证期间跟踪平均损失和准确率。
我们现在可以使用 PyTorch 对模型(ConvexNet)执行训练和评估循环。模型在具有已知标签的 train_loader 数据集上训练,并在 val_loader 数据集上评估:
for epoch in range(n_epochs):
model.train() ①
for bat, (batch_data, batch_labels, batch_lengths) in enumerate(train_loader): ②
batch_data = batch_data.to(device)
batch_labels = batch_labels.to(device)
batch_lengths = batch_lengths.to(device)
optimizer.zero_grad() ③
log_pointer_scores, pointer_argmaxs = model(batch_data, batch_lengths,
➥ batch_labels=batch_labels)
loss = criterion(log_pointer_scores.view(-1, log_pointer_scores. ④
➥ shape[-1]), batch_labels.reshape(-1))
assert not np.isnan(loss.item()), 'Model diverged with loss = NaN' ⑤
loss.backward() ⑥
optimizer.step() ⑥
train_loss.update(loss.item(), batch_data.size(0)) ⑦
mask = batch_labels != TOKENS['<eos>'] ⑦
acc = masked_accuracy(pointer_argmaxs, batch_labels, mask).item() ⑦
train_accuracy.update(acc, mask.int().sum().item()) ⑦
if bat % log_interval == 0: ⑧
print(f'Epoch {epoch}: '
f'Train [{bat * len(batch_data):9d}/{len(train_dataset):9d} '
f'Loss: {train_loss.avg:.6f}\tAccuracy: {train_accuracy.avg:3.4%}')
① 训练模型。
② 迭代训练数据批次。
③ 将模型的参数梯度设置为零,以避免来自先前批次的累积。
④ 计算损失
⑤ 一个安全检查,以确保训练过程中的损失值不是 NaN。
⑥ 执行反向传播和优化步骤。
⑦ 更新训练损失和准确率。
⑧ 打印训练进度。
作为延续,训练好的模型(model)在验证数据集(val_dataset)上评估,以计算验证损失、准确率和输入数据凸包与预测指针序列之间的重叠。我们首先将模型设置为评估模式,其中模型的参数被冻结,批量归一化或 dropout 层的行为与训练期间不同。然后代码通过 val_loader 迭代验证数据集,val_loader 提供数据批次(batch_data)、真实标签(batch_labels)和每个序列的长度(batch_lengths):
model.eval() ①
hull_overlaps = [] ②
for bat, (batch_data, batch_labels, batch_lengths)
➥ in enumerate(val_loader): ③
batch_data = batch_data.to(device)
batch_labels = batch_labels.to(device)
batch_lengths = batch_lengths.to(device)
log_pointer_scores, pointer_argmaxs = model(batch_data, batch_lengths,
➥ batch_labels=batch_labels) ④
loss = criterion(log_pointer_scores.view(-1, log_pointer_scores. ⑤
➥ shape[-1]),batch_labels.reshape(-1))
assert not np.isnan(loss.item()), 'Model diverged with loss = NaN'
val_loss.update(loss.item(), batch_data.size(0)) ⑥
mask = batch_labels != TOKENS['<eos>'] ⑦
acc = masked_accuracy(pointer_argmaxs, batch_labels, mask).item() ⑧
val_accuracy.update(acc, mask.int().sum().item()) ⑨
for data, length, ptr in zip(batch_data.cpu(), batch_lengths.cpu(),
➥ pointer_argmaxs.cpu()): ⑩
hull_overlaps.append(calculate_hull_overlap(data, length, ptr)) ⑪
print(f'Epoch {epoch}: Val\tLoss: {val_loss.avg:.6f} '
f'\tAccuracy: {val_accuracy.avg:3.4%} '
f'\tOverlap: {np.mean(hull_overlaps):3.4%}') ⑫
train_loss.reset() ⑬
train_accuracy.reset() ⑬
val_loss.reset() ⑬
val_accuracy.reset() ⑬
① 将模型设置为评估模式。
② 初始化一个空列表来存储输入数据凸包与预测指针序列之间的重叠值。
③ 迭代验证数据集。
④ 生成指针得分和 argmax 预测。
⑤ 计算验证损失。
⑥ 更新验证损失。
⑦ 忽略 batch_labels 中存在 <eos> 标记的位置的损失贡献。
⑧ 计算掩码准确率。
⑨ 更新验证准确率。
⑩ 迭代每个批次的 数据、长度和指针 argmax 预测。
⑪ 计算输入数据凸包与预测指针序列之间的重叠。
⑫ 打印每个时期的验证损失、准确率和平均重叠。
⑬ 重置指标。
您可以使用 Disp_results 辅助函数显示训练和验证损失以及准确率的结果:
Disp_results(train_loss, train_accuracy, val_loss, val_accuracy, n_epochs)
上述代码行将生成如下输出:
Best Scores:
train_loss: 0.0897 (ep: 9)
train_accuracy 96.61% (ep: 9)
val_loss: 0.0937 (ep: 7)
val_accuracy: 96.54% (ep: 7)
在模型训练和验证之后,我们可以测试模型。以下测试函数将评估训练好的模型(model)在测试数据集上的表现。该函数评估模型在不同测试样本大小下的准确率和与凸包的重叠。此测试函数接受模型、测试样本数量和每个样本中的点数作为输入。代码通过从 5 到 45 以 5 为步长迭代每个样本中的点数(i)来执行测试。AverageMeter类用于在测试期间跟踪平均损失和准确率:
n_rows_test = 1000 ①
def test(model, n_rows_test, n_per_row): ②
test_dataset = Scatter2DDataset(n_rows_test, n_per_row, n_per_row) ③
test_loader = DataLoader(test_dataset, batch_size=batch_size, ③
➥ num_workers=n_workers) ③
test_accuracy = AverageMeter()
hull_overlaps = []
model.eval()
for _, (batch_data, batch_labels, batch_lengths) in enumerate(test_loader): ④
batch_data = batch_data.to(device)
batch_labels = batch_labels.to(device)
batch_lengths = batch_lengths.to(device)
_, pointer_argmaxs = model(batch_data, batch_lengths)
val_loss.update(loss.item(), batch_data.size(0)) ⑤
mask = batch_labels != TOKENS['<eos>'] ⑤
acc = masked_accuracy(pointer_argmaxs, batch_labels, mask).item() ⑤
test_accuracy.update(acc, mask.int().sum().item()) ⑤
for data, length, ptr in zip(batch_data.cpu(), batch_lengths.cpu(), ⑥
➥ pointer_argmaxs.cpu()): ⑥
hull_overlaps.append(calculate_hull_overlap(data, length, ptr)) ⑥
print(f'# Test Samples: {n_per_row:3d}\t ' ⑦
f'\tAccuracy: {test_accuracy.avg:3.1%} ' ⑦
f'\tOverlap: {np.mean(hull_overlaps):3.1%}') ⑦
for i in range(5,50,5): ⑧
test(model, n_rows_test, i) ⑧
① 设置每个测试要生成的测试样本数量。
② 测试函数
③ 生成测试数据集。
④ 遍历测试数据批次。
⑤ 跟踪损失和准确率。
⑥ 更新凸包和预测指针序列之间的重叠。
⑦ 打印准确率和重叠率。
⑧ 遍历并打印不同样本大小的结果。
此代码将产生如下输出:
# Test Samples: 5 Accuracy: 54.8% Overlap: 43.7%
# Test Samples: 10 Accuracy: 72.1% Overlap: 79.1%
# Test Samples: 15 Accuracy: 79.0% Overlap: 90.1%
# Test Samples: 20 Accuracy: 84.8% Overlap: 92.7%
# Test Samples: 25 Accuracy: 80.6% Overlap: 92.3%
# Test Samples: 30 Accuracy: 80.3% Overlap: 91.6%
# Test Samples: 35 Accuracy: 77.8% Overlap: 91.9%
# Test Samples: 40 Accuracy: 75.8% Overlap: 92.1%
# Test Samples: 45 Accuracy: 72.4% Overlap: 90.4%
现在让我们测试训练好的模型,看看这个模型对新未见数据的泛化能力如何。我们将使用包含 50 个点的数据集来测试训练和验证好的模型,并计算预测轮廓和通过 SciPy 获得的真实轮廓之间的凸包重叠。我们将输入数据批次及其长度通过模型来获得指针网络的预测分数(log_pointer_scores)和 argmax 索引(pointer_argmaxs)。真实值是通过从scipy.spatial的ConvexHull函数获得的凸包:
idx = 0
n_per_row = 50 ①
test_dataset = Scatter2DDataset(n_rows_test, n_per_row, n_per_row) ②
test_loader = DataLoader(test_dataset, batch_size=batch_size, ③
➥ num_workers=n_workers) ③
batch_data, batch_labels, batch_lengths = next(iter(test_loader))
print(batch_data.shape,batch_lengths.shape)
log_pointer_scores, pointer_argmaxs = model(batch_data.to(device),
➥ batch_lengths.to(device)) ④
pred_hull_idxs = pointer_argmaxs[idx].cpu() ⑤
pred_hull_idxs = pred_hull_idxs[pred_hull_idxs > 1] – 2 ⑥
points = batch_data[idx, 2:batch_lengths[idx], :2] ⑦
points1 = batch_data[idx, 1:batch_lengths[idx], :2] ⑦
print(points.shape,) ⑦
true_hull_idxs = ConvexHull(points).vertices.tolist() ⑧
true_hull_idxs = cyclic_permute(true_hull_idxs, np.argmin(true_hull_idxs)) ⑧
overlap = calculate_hull_overlap(batch_data[idx].cpu(), batch_lengths[idx].cpu(),
➥ pointer_argmaxs[idx].cpu()) ⑨
print(f'Predicted: {pred_hull_idxs.tolist()}') ⑩
print(f'True: {true_hull_idxs}') ⑩
print(f'Hull overlap: {overlap:3.2%}') ⑩
① 设置每个样本中的点数。
② 创建测试数据集。
③ 从测试数据集中加载第一批次的数据。
④ 获得指针网络的预测分数和 argmax 索引。
⑤ 从批次中提取所选样本的预测 argmax 索引。
⑥ 过滤掉特殊标记(例如,
⑦ 从批次中提取并打印所选样本的 2D 点。
⑧ 真实凸包
⑨ 计算凸包重叠。
⑩ 打印预测凸包索引、凸包索引和重叠百分比的列表。
运行代码将产生如下输出。您可以多次运行前面的代码片段以获得高重叠率:
torch.Size([16, 51, 3]) torch.Size([16])
torch.Size([49, 2])
Predicted: [0, 3, 5, 31, 45, 47, 48, 40, 10]
True: [0, 3, 5, 31, 45, 47, 48, 40, 10]
Hull overlap: 100.00%
以下代码片段可用于可视化由指针网络(ConvexNet)生成的凸包与由scipy.spatial作为真实值生成的凸包之间的比较:
plt.rcParams['figure.figsize'] = (10, 6) ①
plt.subplot(1, 2, 1) ①
true_hull_idxs = ConvexHull(points).vertices.tolist() ②
display_points_with_hull(points, true_hull_idxs) ③
_ = plt.title('SciPy Convex Hull') ③
plt.subplot(1, 2, 2) ④
display_points_with_hull(points, pred_hull_idxs) ⑤
_ = plt.title('ConvexNet Convex Hull') ⑤
① 设置默认的图形大小,并创建第一个子图。
② 使用scipy.spatial的ConvexHull函数计算一组点(points)的凸包。
③ 在第一个子图中显示点和它们的凸包。
④ 创建第二个子图。
⑤ 显示由 ConvexNet 生成的点和凸包。
图 11.20 显示了 SciPy 和 ConvexNet 生成的凸包。在某些情况下,这些凸包是相同的(即,凸包重叠=100.00%),但要实现这种一致性需要适当的训练和仔细调整 ConvexNet 参数。

图 11.20 SciPy 和 Ptr-Net 为 50 个点生成的凸包
本章在机器学习方面提供了一个基本的基础,并讨论了监督和无监督机器学习在处理优化问题中的应用。下一章将重点介绍强化学习,并深入探讨其在解决优化问题中的实际应用。
摘要
-
机器学习(ML)是人工智能(AI)的一个分支,它赋予人工系统或过程从经验和观察中学习的能力,而不是通过明确的编程。
-
深度学习(DL)是机器学习的一个子集,它通过使用深度神经网络来检测数据中的固有特征。这使得人工系统能够从更简单的概念中形成复杂的概念。
-
几何深度学习(GDL)扩展(结构化)深度神经网络以处理具有潜在几何结构的非欧几里得数据,例如图、点云和流形。
-
图机器学习(GML)是机器学习的一个子领域,专注于开发能够从图结构数据中学习的算法和模型。
-
图嵌入表示将离散、高维图域转换为低维连续域的过程。
-
注意力机制允许模型在生成输出序列的过程中有选择性地关注输入数据的一定部分。
-
指针网络(Ptr-Net)是一种具有注意力机制的序列到序列模型变体,旨在处理可变大小的输入数据序列。
-
自组织图(SOM),也称为 Kohonen 图,是一种用于无监督学习的人工神经网络(ANN)。SOM 与其他类型的 ANN 不同,因为它们应用竞争学习而不是错误纠正学习(如梯度下降的反向传播)。
-
神经组合优化是指将机器学习应用于解决组合优化问题。
-
利用机器学习(ML)进行组合优化可以通过三种主要方法实现:端到端学习,其中模型直接制定解决方案;使用机器学习来配置和改进优化算法;以及将机器学习与优化算法集成,其中模型根据其当前状态持续引导优化算法。
第十二章:强化学习
本章涵盖
-
掌握强化学习的基本原理
-
理解马尔可夫决策过程
-
理解演员-评论家架构和近端策略优化
-
熟悉非上下文和上下文多臂老丨虎丨机
-
将强化学习应用于解决优化问题
强化学习(RL)是一种强大的机器学习方法,它使智能代理能够通过与环境的交互来学习最优或近似最优的行为。本章深入探讨了强化学习中的关键概念和技术,揭示了其基本原理作为必要背景知识。在理论阐述之后,本章将接着阐述使用强化学习策略解决优化问题的实际例子。
12.1 揭秘强化学习
强化学习(RL)是机器学习的一个子领域,它处理代理如何通过试错学习方法在环境中做出决策和采取行动以实现特定目标。强化学习的核心思想是代理通过与环境的交互来学习,通过其动作的结果获得奖励或惩罚作为反馈。代理的目标是在时间上最大化累积奖励。
强化学习
“强化学习问题涉及学习做什么——如何将情况映射到动作——以最大化数值奖励信号”(理查德·萨顿和安德鲁·巴特奥在他们的书《强化学习》[1]中)。
图 12.1 概述了文献中常见的强化学习算法。这种分类将强化学习问题分为两大类:马尔可夫决策过程(MDP)问题和多臂老丨虎丨机(MAB)问题。两者之间的区别在于代理的动作如何与环境和影响环境。

图 12.1 强化学习算法分类
在基于 MDP 的问题中,代理的动作影响环境,代理必须考虑其动作在多个时间步长的后果,包括状态和转换的概念。另一方面,MAB 问题涉及代理面临一系列选择(臂)并旨在最大化时间上的累积奖励的场景。这类问题通常在没有明确的状态表示或长期规划要求时使用。在上下文多臂老丨虎丨机(CMAB)问题中,代理被提供上下文或辅助信息,这些信息用于做出更明智的决策。臂(动作)的预期奖励是动作和当前上下文的函数。这意味着最佳动作可以随着提供的上下文而改变。值得一提的是,MDP 是一个更全面的框架,它考虑了更广泛情况下动态决策。
在我们探讨如何将强化学习应用于解决优化问题之前,理解几个相关的强化学习技术是至关重要的。以下小节将详细概述这些方法,接下来的章节将展示它们在解决优化问题中的应用。
12.1.1 马尔可夫决策过程(MDP)
学习的目的是形成对外部世界的内部模型。这个外部世界,或环境,可以使用确定性或非确定性(随机)模型进行抽象。
考虑一种情况,你计划从你的初始状态(例如,你的家)通勤到一个指定的目标状态(例如,你的工作场所)。一种确定性路径规划算法,如 A*(在第四章中讨论),可能会为你提供多种选择:乘坐火车,大约需要 1 小时 48 分钟;开车,大约需要 1 小时 7 分钟;或者骑自行车,大约需要 3 小时 16 分钟(图 12.2a)。这些算法在假设行动及其结果完全确定性的前提下运行。

图 12.2 确定性与非确定性状态转换模型
然而,如果在你的旅程规划过程中出现不确定性,你应该求助于基于非确定性状态转换模型的随机规划算法,以实现不确定条件下的规划。在这些情况下,状态之间的转换概率成为你决策过程的一个组成部分。
例如,让我们考虑初始状态(你的家),如图 12.2b 所示。如果你选择乘坐火车通勤,有 60%的几率你能按时赶上火车,并在预期的 1 小时 48 分钟内到达目的地(你的工作场所)。然而,有 40%的几率你可能会错过火车,需要等待下一班。如果你最终需要等待,有 90%的几率火车会准时到达,你将赶上火车并在总共 2 小时 25 分钟内到达目的地。另一方面,有 10%的几率火车不会到达,导致等待时间延长,甚至可能需要寻找替代方案。另一方面,如果你选择开车,有 30%的几率你会遇到轻微的交通拥堵,只需 50 分钟就能到达办公室。然而,也有 50%的可能性中等交通会延误你的到达时间至 1 小时 7 分钟。在最坏的情况下,有 20%的几率严重交通拥堵可能会将你的旅行时间延长至 2 小时。如果你选择骑自行车,预计的旅行时间为 3 小时 16 分钟,更加可预测且变化较小。
这种场景描述了一个完全可观察的环境,其中当前状态和采取的行动完全决定了下一个状态的概率分布。这被称为马尔可夫决策过程(MDP)。
12.1.2 从 MDP 到强化学习
MDP 为在不确定性下进行规划提供了一个数学框架。它用于描述强化学习中的环境,其中智能体通过执行动作并接收奖励来学习做出决策。学习过程涉及尝试和错误,智能体发现哪些动作在一段时间内能产生最高的期望累积奖励。
如图 12.3 所示,智能体通过与环境的交互来采取行动,在某个状态 s[t] ∈ S 下,根据观察 o 和应用策略 π,在时间 t 采取动作 a[t] ∈ A,然后根据状态转移概率 T 接收奖励 r[t] ∈ R 并过渡到新的状态 s[t][+1] ∈ S。

图 12.3 智能体通过与环境的交互来学习。
以下术语在 RL 中常用:
-
状态, s—这代表在特定时间步长关于环境的完整和未过滤的信息。观察 o 是智能体在给定时间步长从环境中感知到的部分或有限信息。当智能体能够观察到环境的完整状态时,我们说环境是全可观察的,可以建模为 MDP。当智能体只能看到环境的一部分时,我们说环境是部分可观察的,应该建模为部分可观察马尔可夫决策过程 (POMDP)。
-
动作集, A—这代表智能体在给定环境中可以采取的可能或允许的动作集合。这些动作可以是离散的,如棋类游戏的情况,也可以是连续的,如机器人控制或辅助或自动驾驶车辆的车道保持辅助。
-
策略—这可以看作是智能体的“大脑”。它是智能体的决策策略,或从状态或观察到动作的映射。这种策略可以是确定性的,通常用 μ 表示,也可以是随机的,用 π 表示。一个随机的策略 π 主要是指在某个状态 s ∈ S 下选择动作 a ∈ A 的概率。一个随机的策略也可以参数化,并用 π[Θ] 表示。这种参数化策略是一个可计算的函数,它依赖于一组参数(例如,神经网络的权重和偏差),我们可以通过优化算法调整这些参数来改变行为。
-
轨迹, τ (又称事件或滚动)—这是世界中状态和动作的序列,τ = (s[0], a[0], s[1], a[1], ...) .
-
期望回报—这指的是智能体在未来时间范围内可以期望接收的累积奖励总和。它是特定状态-动作序列或策略的整体吸引力或价值的度量。
让我们考虑一个简单的“到达宝藏”游戏,其中智能体试图获取宝藏然后退出。在这个游戏中,只有四个状态,如图 12.4 所示。其中,状态 0 代表火坑,状态 3 代表出口——两者都是终端状态。状态 1 包含宝藏,用钻石表示。游戏开始时,智能体位于状态 2,可以选择向左或向右移动作为动作。到达宝藏后,智能体获得 +10 的奖励。然而,掉入火坑会导致 -10 的惩罚。成功退出会获得 +4 的奖励。

图 12.4 “到达宝藏”游戏
状态 s 提供了关于环境的完整信息,包括智能体的位置以及火坑、宝藏和出口的位置(状态 0 是火坑,状态 1 是宝藏,状态 2 是当前位置,状态 3 是出口)。游戏的观察结果被收集并呈现为一个观察向量,例如 obs = [2,0],表示智能体处于状态 2,并且没有感知到宝藏的存在。这是环境的一个局部视图,因为智能体无法访问完整的州信息,例如火坑或出口的位置。策略表示基于观察移动左或右的概率。例如,策略(obs)为[0.4, 0.6]表示有 40% 的概率向左移动,有 60% 的概率向右移动。在图 12.4 所示的单个轨迹中,我们可以计算期望回报如下:期望回报 (R) = 0.4 * (10) + 0.6 * (4) + 0.4 * 0.2 * (–10) + 0.4 * 0.8 * (0) + 0.4 * 0.8 * 0.9 * (0) + 0.4 * 0.8 * 0.1 * (4) = 5.728。
在强化学习 (RL) 中,目标是学习一个最优策略,该策略最大化期望累积折现奖励。值迭代、策略迭代和策略梯度是强化学习中用于实现此目标的不同迭代方法。在强化学习中,价值函数定义了智能体从特定状态或状态-动作对开始,遵循一定策略的期望累积奖励。有两种类型的价值函数:状态价值函数 V(s) 和动作价值函数 Q(s,a)。状态价值函数 V(s) 估计智能体从特定状态 s 开始并遵循策略 π 可能获得的期望累积未来奖励。它根据其预测的未来奖励来量化状态的可取性。状态价值函数 V(s) 由以下公式给出:
|

| 12.1 |
|---|
其中
-
V^π(s) 是从状态 s 开始并随后遵循 π 的期望回报。
-
E[π][] 表示在智能体遵循策略 π 的情况下,期望值。
-
t 是任意时间步。
-
γ 是折扣因子,通常是一个介于 0 和 1 之间的值,表示未来奖励相对于即时奖励的现值。折扣的目的在于更重视即时奖励,反映了对尽早获得奖励而非延迟获得奖励的偏好。接近 0 的折扣因子会使代理变得 短视(即专注于即时奖励),而接近 1 的折扣因子会使代理更加 远见(即考虑未来奖励)。
动作价值函数,Q(s, a),估计代理从给定状态 s 采取特定动作 a 并遵循策略 π 可以实现的期望累积未来奖励。它量化了在给定策略下,在特定状态下采取特定动作的“好”的程度。动作价值函数 Q(s, a) 由以下公式给出:
|

| 12.2 |
|---|
在 策略迭代方法 中,我们首先计算初始策略下每个状态的价值 V^π(s),并使用这些价值估计来改进策略。这意味着从初始策略开始,我们反复在策略评估和策略改进步骤之间交替(直到收敛)。
策略梯度方法 学习一个参数化的策略,该策略由代理用于选择动作。目标是找到最大化随时间累积期望奖励的策略参数值。
策略梯度
政策梯度是一种无模型的政策方法,不需要显式地表示价值函数。核心思想是偏好导致更高回报的动作,同时贬低导致较低奖励的动作。这个迭代过程随着时间的推移不断优化策略,旨在找到高性能的策略。
而不是显式地估计价值函数,策略梯度方法通过计算策略梯度的估计值并将其插入到随机梯度上升算法中来实现。策略梯度损失 L^(PG) 是最常用的梯度估计器之一:
|

| 12.3 |
|---|
其中
-
期望 E[t] 表示对有限批次样本的经验平均。
-
π[θ] 是一个随机策略,它将环境观察到的状态作为输入,并建议采取的动作作为输出。
-
Â[t] 是在时间步 t 时刻优势函数的估计。这个估计基本上试图评估当前状态下所选动作的相对价值。优势函数表示在给定状态下采取特定动作相对于期望值的优势。它通过执行建议的动作(通常具有最高的 Q 值)的期望奖励与当前状态的估计价值函数之间的差异来计算:
|

| 12.4 |
|---|
其中
-
Q(s, a)是动作值函数(也称为 Q 值),它表示按照策略在状态s采取动作a的期望累积奖励。
-
V(s)是状态值函数,它表示从状态s按照策略获得的期望累积奖励。
如您在方程 12.4 中看到的,如果优势函数是正的,表示观察到的回报高于预期值,梯度将是正的。这个正梯度意味着在该状态下采取的动作在未来被增加其可能性的概率。另一方面,如果优势函数是负的,梯度将是负的。这个负梯度意味着如果在未来遇到类似的状态,选择动作的概率将会降低。
12.1.3 基于模型的 RL 与无模型 RL
强化学习分为两种主要类型:基于模型的 RL(MBRL)和无模型 RL(MFRL)。这种分类基于 RL 代理是否拥有环境的模型。术语模型指的是环境的内部表示,包括其转移动力学和奖励函数。表 12.1 总结了这两类之间的差异。
表 12.1 基于模型的 RL(MBRL)与无模型 RL(MFRL)
| 方面 | 基于模型的 RL(MBRL) | 无模型 RL(MFRL) |
|---|---|---|
| 环境模型 | 使用已知模型或学习环境的模型(即转移概率) | 跳过模型,直接学习何时采取动作(不一定找出动作的确切模型) |
| 奖励 | 通常已知或学习到 | 未知或部分已知。无模型 RL 直接从与环境交互中获得的奖励中学习。 |
| 动作 | 使用模型选择以最大化期望累积奖励 | 根据经验的历史选择以最大化期望累积奖励 |
| 策略 | 通过学习环境动态模型来实现策略学习。 | 通过试错来实现策略学习,直接根据观察到的经验优化策略。 |
| 设计和调整 | 由于模型复杂性,MBRL 可能需要更高的初始设计和调整努力。然而,进步正在简化这一过程。 | 需要较少的初始努力。然而,MFRL 的超参数调整也可能具有挑战性,尤其是在复杂任务中。 |
| 示例 | AlphaZero、世界模型和想象增强代理(I2A) | Q 学习、优势演员-评论家(A2C)、异步优势演员-评论家(A3C)和近端策略优化(PPO) |
根据强化学习算法如何从收集的经验中学习和更新其策略,强化学习算法也可以分为离策略和在线策略强化学习。离策略方法从不同于正在更新的策略生成的经验中学习,而在线策略方法从正在更新的当前策略生成的经验中学习。在线策略和离策略方法通常被认为是无模型的,因为它们直接从经验中学习策略或价值函数,而没有明确构建环境动态的模型,这使它们与基于模型的方法区分开来。表 12.2 总结了离策略和无模型在线策略强化学习方法之间的差异。
表 12.2 离策略与在线策略强化学习方法
| 方面 | 离策略强化学习方法 | 在线策略强化学习方法 |
|---|---|---|
| 学习方法 | 从不同于正在更新的策略生成的经验中学习 | 从正在更新的当前策略生成的经验中学习 |
| 样本效率 | 通常由于重用过去经验(记录的数据)而具有更高的样本效率 | 通常样本效率较低,因为每次策略更新后都会丢弃经验批次(过去经验没有明确存储) |
| 策略评估 | 可以分别学习价值函数和策略,使不同的算法(例如,Q 学习、DDPG、TD3)成为可能 | 在在线策略算法(例如,REINFORCE、A2C、PPO)中,策略评估和改进通常是交织在一起的。 |
| 优点 | 更高的样本效率,可以从多样化的经验中学习,允许重用过去的数据,如果可用的先前数据量很大则很有用 | 更简单、更直接,避免了离策略校正,可以收敛到更好的局部最优解,适用于数据有限或在线学习的场景 |
| 缺点 | 需要仔细的离策略校正,不太适合在线学习或数据有限的任务 | 样本效率较低,丢弃过去经验,探索多样性有限,可能收敛到次优策略 |
以下两个小节提供了更多关于 A2C 和 PPO 的细节,作为本章中使用的在线策略方法的示例。
12.1.4 行为者-评论家方法
图 12.5 展示了优势演员-评论家(A2C)架构,作为演员-评论家方法的例子。正如其名所示,这个架构由两个模型组成:演员和评论家。演员负责学习和更新策略。它以当前状态作为输入,输出表示策略的动作概率分布。另一方面,评论家专注于评估演员建议的动作。它以状态和动作作为输入,并估计在该特定状态下采取该动作的优势。优势表示与该状态下平均动作相比,该动作有多好(或有多坏),这是基于预期的未来奖励。来自评论家的这种反馈有助于演员学习和更新策略,以优先考虑具有更高优势的动作。


A2C 是一种同步、无模型的算法,旨在同时学习策略(演员)和值函数(评论家)。它通过迭代改进演员和评论家网络来学习最优策略。通过估计优势,该算法可以为演员采取的动作的质量提供反馈。评论家网络帮助估计值函数,为优势计算提供基准。这种组合使得算法能够以更稳定和高效的方式更新策略。
12.1.5 近端策略优化
近端策略优化(PPO)算法是由 OpenAI [2] 设计的一种基于策略的无模型强化学习(RL)算法,它已在视频游戏和机器人控制等许多应用中取得了成功。PPO 基于演员-评论家架构。
在强化学习(RL)中,智能体通过与环境的交互生成自己的训练数据。与依赖于静态数据集的监督机器学习不同,RL 的训练数据动态地依赖于当前策略。这种动态特性导致数据分布不断变化,在训练过程中引入了潜在的不稳定性。在之前解释的政策梯度方法中,如果你对收集到的单个经验批次连续应用梯度上升,可能会导致更新将网络的参数推离数据收集范围,从而使得提供真实优势估计的优势函数变得不准确,并严重破坏策略。为了解决这个问题,已经提出了 PPO 的两个主要变体:PPO-penalty 和 PPO-clip。
PPO-penalty
在 PPO-penalty 中,目标函数中包含了一个约束,以确保策略更新不会过多偏离旧策略。这个想法是信任域策略优化 (TRPO) 的基础。通过强制执行信任域约束,TRPO 将策略更新限制在可管理的区域内,并防止策略发生大的变化。PPO-penalty 主要受 TRPO 启发,并使用以下无约束目标函数,该函数可以使用随机梯度上升进行优化:
|

| 12.5 |
|---|
其中 θ[old] 是更新前的策略参数向量,β 是一个固定的惩罚系数,而 Kullback–Leibler 距离 (KL) 表示更新策略与旧策略之间的差异。这个约束被整合到目标函数中,以避免远离旧策略的风险。
Kullback–Leibler 距离
Kullback–Leibler (KL) 距离,也称为 相对熵,是一个度量,用于量化两个概率分布之间的差异。两个概率分布 P 和 Q 之间的 KL 距离定义为 KL(P || Q) = ∫ P(x) ⋅ log(P(x) / Q(x)) dx,其中 P(x) 和 Q(x) 代表两个分布的概率密度函数 (PDFs)。积分是在随机变量 x 的整个支持集上进行的(即 PDF 非零的随机变量的值域)。以下图显示了具有不同均值和方差的两个高斯分布之间的 KL 距离。

两个高斯分布之间的 KL 距离
如果,并且仅当 P 和 Q 是相同的分布时,KL 距离等于零。
PPO-clip
在 PPO-clip 中,定义了一个比率 r(θ),它是更新策略与旧策略版本之间的概率比率。给定一系列样本动作和状态,如果动作现在比旧策略版本更有可能,则 r(θ) 值将大于 1,如果动作现在比上次梯度步之前更不可能,则它将在 0 和 1 之间。
在 PPO-clip 中要最大化的中心目标函数具有以下形式:
|

| 12.6 |
|---|
其中 L^(CLIP) 是剪裁代理目标,它是一个在轨迹批次上计算的 期望算子,epsilon ϵ 是一个超参数(例如,ϵ = 0.2)。正如方程 12.6 所示,期望算子是在两个项的最小值上取的。第一项代表在正常策略梯度中使用的默认目标,它鼓励策略优先选择与基线相比具有高正优势的动作。第二项是正常策略梯度的剪裁或截断版本。它应用剪裁操作以确保更新保持在指定的范围内,具体是在 1 – ϵ 和 1 + ϵ 之间。
PPO 中的剪辑代理目标在不同的区域定义了基于优势估计 Â[t] 和概率比率的客观函数的行为,如图 12.6 所示。

图 12.6 PPO 中的剪辑代理目标
在图 12.6 的左侧,优势函数为正时,目标函数表示所选行为对结果有比预期更好的影响。当比率(r)变得过高时,目标函数会变得平坦。这发生在当前策略下行为比旧策略更有可能发生的情况下。剪辑操作将更新限制在一个范围内,新策略不会与旧策略有显著偏差,从而防止过大的策略更新,这可能会破坏训练稳定性。
在图 12.6 的右侧,优势函数为负时,它表示行为对结果有估计的负面影响。当比率(r)接近零时,目标函数会变得平坦。这对应于在当前策略下比旧策略发生可能性小得多的行为。这种平坦化效应防止了过度更新,否则可能会将这些行为的概率降低到零。
12.1.6 多臂老丨虎丨机(MAB)
多臂老丨虎丨机(MAB)是一类具有单个状态的强化学习问题。在 MAB 中,一个代理面对一组可供选择的行为或“臂”,每个行为都有一个相关的奖励分布。代理的目标是在一系列行为中最大化累积的总奖励。代理不会通过其行为修改其环境,不考虑状态转换,并且始终保持在同一单个状态。它专注于在每个时间步选择最有利的行为,而不考虑对环境状态的影响。
老丨虎丨机(单臂强盗)
老丨虎丨机(又称水果机、老丨虎丨机或单臂强盗)是一种流行的赌博设备,通常在赌场中找到。它是一种机械或电子游戏机,具有多个带有各种符号的旋转滚筒。玩家将硬币或代币投入机器,然后拉动杠杆(因此得名“单臂强盗”)或按按钮以启动滚筒的旋转。玩老丨虎丨机的目的是将旋转滚筒上的符号排列成获胜组合。术语“强盗”源于与老丨虎丨机的类比,其中代理拉动一个臂(就像拉动老丨虎丨机的杠杆一样)以获得不同的奖励。这突显了人们如何看待老丨虎丨机,尤其是较老的机械老丨虎丨机,就像是一个窃贼或强盗在夺取玩家的钱。
MAB 代理面临着探索与利用的困境。为了学习最佳动作,它必须探索各种选项(探索)。然而,它还需要根据其当前信念快速收敛并迅速关注最有希望的动作(利用)。在监督学习和无监督学习中,主要目标是拟合预测模型(在监督学习中)或发现给定数据中的模式(在无监督学习中),没有明确的探索概念,正如在强化学习中所见,代理与环境交互以通过试错学习最佳行为。MAB 问题为从有限的反馈中学习以及平衡探索与发现高奖励动作提供了宝贵的见解。在 MAB 中,代理的目标是在历史上产生高奖励的动作中利用,同时探索以收集有关可能更有奖励的动作的信息。
要理解 MAB,想象你身处一家赌场,面前是一排老丨虎丨机(单臂老丨虎丨机)。每台老丨虎丨机代表 MAB 的一个“臂”。让我们假设你面前有三台老丨虎丨机,如图 12.7 所示。每次你决定玩老丨虎丨机时,你的情况(或状态)都是相同的:“我应该玩哪台老丨虎丨机?”环境不会改变或提供不同的条件;你始终在这个单一状态下做出决定。没有上下文,例如“如果赌场拥挤,就玩机器 A”或“如果外面下雨,就玩机器 B。”总是只是“我应该玩哪台机器?”你的动作是选择一台老丨虎丨机来玩——你投入硬币并拉动杠杆。你被允许拉动这些机器的杠杆总共 90 次,每台老丨虎丨机都有自己的收益分布,由其均值和标准差来表征。然而,一开始,你并不知道这些分布的具体细节。拉动杠杆后,机器可能会给你支付(正奖励)或什么也没有(零奖励)。随着时间的推移,你试图判断是否有哪台机器比其他机器更频繁地给出更高的支付。

图 12.7 三台老丨虎丨机作为一个非上下文的多臂老丨虎丨机(MAB)问题。这三台机器的收益分布的均值和标准差分别为(8,0.5),(10,0.7)和(5,0.45)。
在这个多臂老丨虎丨机(MAB)问题中的目标是,通过选择最有回报的老丨虎丨机,在 90 次试验中最大化获得的总回报或累积奖励。换句话说,你正在尝试学习机器的回报分布。术语回报分布指的是每个机器(或臂)提供的奖励(或回报)的概率分布。由于你对这些回报分布没有先验知识,你需要通过尝试不同的选项来探索老丨虎丨机,以收集有关它们性能的信息。挑战在于在探索和利用之间找到平衡。探索涉及尝试不同的机器以学习它们的回报分布,而利用则涉及根据可用信息选择被认为能带来最高回报的机器。
你可以应用各种老丨虎丨机算法或策略来确定选择老丨虎丨机和最大化 90 次试验累积奖励的最佳方法。这些策略包括但不限于仅探索、仅利用贪婪、ε-贪婪(ε-greedy)和上置信界(UCB):
-
仅探索策略—在这种策略中,代理在每次试验中随机选择一台老丨虎丨机进行游戏,而不考虑过去的结果。在 MAB 中,“遗憾”是一个常见的衡量标准,计算为最大可能奖励与从每个选定的机器获得的奖励之间的差异。例如,如果我们应用仅探索策略进行 90 次试验,并考虑每台老丨虎丨机回报分布的平均值,我们将得到总平均回报为 30 × 8 + 30 × 10 + 30 × 5 = 690。如果你在 90 次试验中使用机器 2,可以获得最大奖励。在这种情况下,最大奖励将是 90 × 10 = 900。这意味着遗憾ρ = 900 – 690 = 210。
-
仅利用贪婪策略—在这种情况下,代理尝试每台机器一次,然后选择具有最高估计平均回报的老丨虎丨机。例如,假设在第一次试验中,代理从机器 1、2 和 3 分别获得 7、6 和 3 的回报。代理将随后专注于使用机器 1,认为它是回报最高的。这可能导致代理由于缺乏探索而陷入困境。
-
ε-贪婪策略—在这里,智能体通过以一定概率(epsilon)随机选择老丨虎丨机来尝试在探索和利用之间取得平衡。这是探索部分,其中智能体偶尔尝试所有三台机器以收集更多关于它们的信息。以 1 – ε的概率,智能体根据过去的经验选择具有最高估计奖励的机器。这是利用部分,其中智能体根据迄今为止收集的数据选择看似最好的动作。例如,对于 90 次试验,如果ε = 10%,智能体将随机选择老丨虎丨机大约 9 次(90 的 10%)。其他 81 次试验(90%的 90)将看到智能体选择根据到目前为止的试验,产生了最高平均奖励的老丨虎丨机。
-
上置信界(UCB)策略—在这种策略中,智能体根据其估计的平均奖励和估计的不确定性或置信度之间的权衡来选择机器。它优先探索具有高潜在奖励但估计不确定性高的机器,以减少不确定性并最大化长期奖励。在 UCB 中,选择臂(动作 A[t])在时间步 t 使用以下公式:
|

| 12.7 |
|---|
其中 Q[t](a) 是在试验 t 时动作 a 的估计值。Q[t](a) = 奖励总和/N[t](a)。N[t](a) 是在试验 t 之前选择动作 a 的试验次数。方程 12.7 右侧的第一个项代表利用部分。如果你总是拉动具有最高 Q[t](a) 的臂,你将总是利用当前的知识而不探索其他臂。第二个项是探索部分。随着试验次数 t 的增加,探索项通常会增加,但会被动作 a 已经被选择的频率所减少。乘数 c 调整了此探索项的影响。
每种策略采用不同的方法来平衡探索和利用,导致不同的遗憾水平。这种遗憾水平量化了智能体由于不总是选择最佳动作而造成的累积损失。直观上,它衡量了智能体通过始终拉动最佳臂(即通过选择最佳动作)所能获得的奖励与智能体实际通过遵循某种策略所获得的奖励之间的差异。
让我们看看四种 MAB 策略的 Python 实现。我们将首先设置臂(动作)的数量、每个老丨虎丨机的收益分布和试验次数。我们还将定义一个sample_payoff来从老丨虎丨机中采样收益并计算最大可能奖励。
列表 12.1 多臂老丨虎丨机(MAB)策略
import numpy as np
K = 3 ①
payoff_params = [
{"mean": 8, "std_dev": 0.5},
{"mean": 10, "std_dev": 0.7},
{"mean": 5, "std_dev": 0.45}
] ②
num_trials = 90 ③
def sample_payoff(slot_machine):
return np.random.normal(payoff_params[slot_machine]["mean"],
➥ payoff_params[slot_machine]["std_dev"]) ④
max_reward = max([payoff_params[i]["mean"] for i in range(K)]) ⑤
① 设置老丨虎丨机(臂)的数量。
② 指定收益分布。
③ 设置试验次数。
④ 从老丨虎丨机中采样收益的函数
⑤ 计算最大可能奖励。
作为 12.1 列表的延续,我们将定义一个名为explore_only()的函数,该函数实现了仅探索的策略。在这个函数中,初始化total_regret,然后遍历指定的试验次数。通过生成一个介于 0 和K - 1(包含)之间的随机整数来随机选择一台投币机,其中K代表投币机的数量。然后,通过调用sample_payoff()函数从所选的投币机中采样收益,该函数根据所选机器的收益分布返回一个奖励值。遗憾值通过从最大可能奖励(max_reward)中减去获得的奖励来计算。在这里,我们考虑平均值的最大值作为三个机器收益分布中最大概率的点。平均遗憾值作为输出返回:
def explore_only():
total_regret = 0
for _ in range(num_trials):
selected_machine = np.random.randint(K) ①
reward = sample_payoff(selected_machine) ②
regret = max_reward - reward ③
total_regret += regret ④
average_regret = total_regret / num_trials ⑤
return average_regret
① 随机选择一台投币机。
② 从所选的投币机中采样收益。
③ 计算遗憾值。
④ 计算总遗憾值。
⑤ 计算平均遗憾值。
第二种策略在exploit_only_greedy()函数中定义。该函数通过从每个机器的收益平均值列表(payoff_params[i]["mean"])中找到最大平均收益值的索引来选择具有最高平均收益率的投币机。np.argmax()函数返回最大平均收益值的索引,代表被认为能提供最高预期奖励的机器:
def exploit_only_greedy():
total_regret = 0
for _ in range(num_trials):
selected_machine = np.argmax([payoff_params[i]["mean"] for i in
range(K)]) ①
reward = sample_payoff(selected_machine) ②
regret = max_reward – reward ③
total_regret += regret ④
average_regret = total_regret / num_trials ⑤
return average_regret
① 选择具有最高平均收益率的投币机进行利用。
② 从所选的投币机中采样收益。
③ 计算遗憾值。
④ 计算总遗憾值。
⑤ 计算平均遗憾值。
以下epsilon_greedy(epsilon)函数实现了 epsilon-greedy 策略。该函数检查生成的介于 0 和 1 之间的随机数是否小于 epsilon 值。如果是,算法通过随机选择一台投币机进行探索来执行探索。如果不满足此条件,算法通过选择具有最高平均收益率的投币机来执行利用:
def epsilon_greedy(epsilon):
total_regret = 0
for _ in range(num_trials):
if np.random.random() < epsilon:
selected_machine = np.random.randint(K) ①
else:
selected_machine = np.argmax([payoff_params[i]["mean"]
➥ for i in range(K)]) ②
reward = sample_payoff(selected_machine) ③
regret = max_reward - reward
total_regret += regret
average_regret = total_regret / num_trials
return average_regret
① 随机选择一台投币机进行探索。
② 选择具有最高平均收益率的投币机进行利用。
③ 从所选的投币机中采样收益。
以下ucb(c)函数实现了上置信界(UCB)策略。该函数首先初始化一个数组来跟踪每个投币机的游戏次数。该函数还初始化一个数组来累计从每个投币机获得的奖励总和,并初始化一个变量来累计总遗憾值。代码包括一个循环,每次玩一次每个投币机以收集初始奖励并更新计数和奖励总和:
def ucb(c):
num_plays = np.zeros(K) ①
sum_rewards = np.zeros(K) ②
total_regret = 0 ③
for i in range(K): ④
reward = sample_payoff(i)
num_plays[i] += 1
sum_rewards[i] += reward
for t in range(K, num_trials): ⑤
ucb_values = sum_rewards / num_plays + c * np.sqrt(np.log(t) / num_plays) ⑥
selected_machine = np.argmax(ucb_values)
reward = sample_payoff(selected_machine)
num_plays[selected_machine] += 1
sum_rewards[selected_machine] += reward
optimal_reward = max_reward
regret = optimal_reward - reward
total_regret += regret
average_regret = total_regret / num_trials
return average_regret
① 初始化一个数组来跟踪每个投币机的游戏次数。
② 初始化一个数组来累计从每个投币机获得的奖励总和。
③ 初始化每个老丨虎丨机的总后悔值。
④ 每次玩一次老丨虎丨机以初始化。
⑤ 继续使用 UCB 策略进行游戏。
⑥ 计算 UCB 值。
以下代码片段用于运行策略,计算平均后悔值,并打印结果:
avg_regret_explore = explore_only()
avg_regret_exploit = exploit_only_greedy()
avg_regret_epsilon_greedy = epsilon_greedy(0.1) ①
avg_regret_ucb = ucb(2) ②
print(f"Average Regret - Explore only Strategy: {round(avg_regret_ ③
explore,4)}") ③
print(f"Average Regret - Exploit only Greedy Strategy:
➥ {round(avg_regret_exploit,4)}") ③
print(f"Average Regret - Epsilon-greedy Strategy:
➥ {round(avg_regret_epsilon_greedy,4)}") ③
print(f"Average Regret - UCB Strategy: {round(avg_regret_ucb,4)}") ③
① 设置 epsilon-greedy 策略的 epsilon 值,并运行策略。
② 设置 UCB 策略的探索参数 c 的值,并运行策略。
③ 打印结果
由于代码中包含随机抽样,您的结果可能会有所不同,但运行代码将产生类似以下的结果:
Average Regret - Explore only Strategy: 2.246
Average Regret - Exploit only Greedy Strategy: 0.048
Average Regret - Epsilon-greedy Strategy: 0.3466
Average Regret - UCB Strategy: 0.0378
MAB 算法和概念在各种现实场景中找到应用,这些场景涉及不确定性下的决策和探索-利用权衡。MABs 的现实应用示例包括但不限于资源分配(动态分配资源到不同选项以最大化性能)、在线广告(动态分配广告印象到不同选项,并学习哪些广告产生最高的点击率,这是用户点击广告的概率)、实验设计和临床试验(优化患者分配到不同治疗选项)、内容推荐(为用户个性化内容推荐)和网站优化(优化不同的设计选项)。
如您在图 12.1 中看到的,多臂老丨虎丨机(MABs)可以分为非上下文和上下文 MABs。与之前解释的非上下文 MABs 相比,上下文多臂老丨虎丨机(CMAB)使用环境中包含的上下文信息。在 CMAB 中,学习器反复观察上下文,选择一个动作,并接收与所选动作相关的奖励或损失作为反馈。CMAB 算法使用补充信息,称为侧信息或上下文,在现实场景中做出明智的决策。例如,在卡车选择问题中,共享的上下文是配送路线的类型(城市或州际)。第 12.5 节展示了如何使用 CMAB 来解决这个问题,作为组合动作的示例。
上下文多臂老丨虎丨机应用
上下文多臂老丨虎丨机(CMABs)在个性化推荐和在线广告等领域找到了应用,其中上下文可以是一个用户的信息。例如,亚马逊展示了如何使用内置的 Vowpal Wabbit(VW)容器在 SageMaker 上开发和部署 CMAB 工作流程(mng.bz/y8rE)。这些 CMABs 可以用于为用户个性化内容,如内容布局、广告、搜索、产品推荐等。此外,基于 CMAB 并使用 VW 进行营销决策优化的可扩展算法决策平台 WayLift 被开发出来(mng.bz/MZom)。
12.2 基于强化学习的优化
强化学习(RL)可以通过将问题构造成马尔可夫决策过程(MDP)并应用强化学习算法来寻找一个最优策略,从而用于组合优化问题,以获得最佳可能的解决方案。在强化学习中,智能体通过在一个环境中学习做出连续决策,以最大化累积奖励的概念。这个过程涉及到找到一个最优策略,将状态映射到动作,以最大化预期的长期奖励。
强化学习与优化的收敛性最近已成为一个活跃的研究领域,吸引了学术界和工业界的广泛关注。研究人员正在积极探索将强化学习的优势应用于高效有效地解决复杂优化问题的方法。例如,第 11.7 节中提出的通用端到端管道可以用于解决组合优化问题,如旅行商问题(TSP)、车辆路径问题(VRP)、可满足性问题(SAT)、最大切割(MaxCut)、最大独立集(MIS)等。此管道包括使用监督学习或强化学习训练模型。列表 11.4 展示了如何使用 Joshi、Laurent 和 Bresson [3]所描述的监督学习或强化学习方法预训练的机器学习模型来解决 TSP。
一篇由 Nazari 等人撰写的论文中提出了一种使用强化学习解决车辆路径问题(VRP)的端到端框架[4]。在 Delarue、Anderson 和 Tjandraatmadja 的论文[5]中,也使用强化学习处理了带能力的车辆路径问题(CVRP)。另一篇名为 RLOR 的框架由 Wan、Li 和 Wang 在论文[6]中描述,它是一个适用于 CVRP 和 TSP 等路由问题的深度强化学习灵活框架。Alabbasi、Ghosh 和 Aggarwal 在论文[7]中描述了一个无模型的分布式强化学习算法 DeepPool,通过与环境交互来学习最优调度策略,用于拼车应用。DeepFreight 是另一个用于货运交付问题的无模型强化学习算法,由 Jiayu 等人在论文[8]中描述。它将问题分解为两个紧密协作的组件:卡车调度和包裹匹配。货运交付系统的关键目标是最大化在特定时间限制内服务的请求数量,并在此过程中最小化整个车队的总燃油消耗。MOVI 是另一种用于大规模出租车调度问题的无模型方法,由 Oda 和 Joe-Wong 在论文[9]中描述。强化学习也被用来优化交通信号控制(TSC),作为一种缓解拥堵的方法。在 Ruan 等人撰写的论文[10](我是合著者之一)中,模拟了一个收集于中国杭州的真实路口的真实交通数据模型,并使用不同的基于强化学习的交通信号控制器进行模拟。我们还提出了一种多智能体强化学习模型,以在混合交通场景中提供宏观和微观控制[11]。实验结果表明,所提出的方法在吞吐量、平均速度和安全性等几个指标上优于其他基线。
李和 Malik 在论文[12]中描述了一个名为“学习优化”的优化算法框架。问题被表述为一个强化学习问题,其中任何优化算法都可以表示为一个策略。使用引导策略搜索,并针对不同类别的凸和非凸目标函数训练了自主优化器。这些自主优化器比手工设计的优化器收敛更快或达到更好的最优解。这与第 11.6 节中描述的摊销优化概念有些相似,但使用了强化学习。
Toll 等人在论文[13]中描述了基于强化学习的调度方法,用于一个 10 层建筑中的四电梯系统。电梯调度问题是一个组合优化问题,涉及在多层建筑中高效调度多个电梯来服务乘客的请求。如第 1.4.2 节所述,在这种情况下可能的状态数量为 10⁴(电梯位置)× 2⁴⁰(电梯按钮)× 2¹⁸(大厅呼叫按钮)= 2.88 x 10²¹ 种不同状态。
Stable-Baselines3 (SB3)为几个与 OpenAI Gym 兼容的自定义 RL 环境提供了强化学习算法的可靠实现。要使用 pip 安装 Stable Baselines3,请执行pip install stable-baselines3。SB3 中 RL 算法实现的示例包括优势演员-评论家(A2C)、软演员-评论家(SAC)、深度确定性策略梯度(DDPG)、深度 Q 网络(DQN)、事后经验重放(HER)、双延迟 DDPG(TD3)和近端策略优化(PPO)。SB3 中可用的环境和项目包括以下内容:
-
移动环境—一个用于无线移动网络中自主协调的体育馆环境。它允许模拟涉及多个基站中移动用户的各种场景。此环境在第 12.4 节中使用。
-
gym-electric-motor—一个用于模拟和控制电动传动系统的 OpenAI Gym 环境。此环境在本章的练习 6 中使用(见附录 C)。
-
高速公路环境—一个用于在不同场景中(如高速公路、合并、环岛、停车、交叉口和赛道)进行自动驾驶决策的环境。
-
机器人深度强化学习中的广义状态依赖探索(gSDE)—一种直接在真实机器人上训练 RL 代理的探索方法。
-
RL Reach—一个用于运行可定制机器人抓取任务的 RL 实验的平台。
-
RL Baselines3 动物园—一个用于训练、评估 RL 代理、调整超参数、绘制结果和记录视频的框架。
-
Furuta 悬臂机器人—一个构建和训练旋转倒立摆(也称为 Furuta 悬臂)的项目。
-
UAV_Navigation_DRL_AirSim—一个用于在复杂未知环境中训练无人机导航策略的平台。
-
触觉健身房—RL 环境,主要使用模拟触觉传感器作为观察的主要来源。
-
SUMO-RL—一个用于实例化带有城市交通模拟(SUMO)的交通信号控制的 RL 环境的接口。
-
PyBullet Gym—用于单代理和多代理四旋翼机控制强化学习的环境。
以下章节提供了如何使用 RL 方法处理组合动作控制问题的示例。
12.3 使用 A2C 和 PPO 平衡 CartPole
让我们考虑一个经典控制任务,其目标是通过左右移动车辆来平衡杆在车上,如图 12.8 所示。这个任务可以被认为是一个优化问题,其目标是通过找到一种最佳策略,尽可能长时间地平衡杆在车上,来最大化累积奖励。智能体需要学习如何做出决策(采取行动)以最大化奖励信号。智能体在不同的状态下探索不同的行动,并随着时间的推移学习哪些行动能带来更高的奖励。通过根据观察到的奖励迭代更新其策略,智能体旨在优化其决策过程并找到每个状态的最佳行动。

图 12.8 CartPole 平衡问题
这个环境的描述由四个变量组成:
-
车辆位置(连续)—这表示车辆沿x轴的位置。值范围从 –4.8 到 4.8。
-
车辆速度(连续)—这表示车辆沿x轴的速度。值范围从 –inf 到 inf。
-
杆角度(连续)—这表示杆从垂直位置的角度。值范围从 –0.418 到 0.418 弧度或 –23.95° 到 23.95° 度。
-
杆角速度(连续)—这表示杆的角速度。值范围从 –inf 到 inf。
CartPole 环境中的动作空间是离散的,由两个可能的动作组成:
-
行动 0—将车辆向左移动。
-
行动 1—将车辆向右移动。
每当杆保持直立时,智能体在每个时间步长都会获得 +1 的奖励。如果满足以下条件之一,则剧集结束:
-
杆的角度超过 ±12 度远离垂直位置。
-
车辆位置超过 ±2.4 个单位远离中心。
-
这一幕达到最大时间步长限制(通常是 200 步)。
在 CartPole 环境中,目标是尽可能长时间地平衡杆在车上,以最大化累积奖励。让我们看看使用第 12.1.4 节中讨论的收益演员-评论员(A2C)算法学习平衡 CartPole 的最佳策略的代码。
如列表 12.2 所示,我们首先导入必要的库:
-
gym是 OpenAI Gym 库,用于处理强化学习环境。 -
torch是用于构建和训练神经网络的 PyTorch 库。 -
torch.nn是一个模块,提供定义神经网络所需的工具。 -
torch.nn.functional包含各种激活和损失函数。 -
torch.optim包含用于训练神经网络的优化算法。 -
tqdm提供进度条以跟踪训练进度。 -
使用
seaborn进行可视化。
列表 12.2 使用 A2C 算法平衡 CartPole
import gym
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
import matplotlib.pyplot as plt
from tqdm import tqdm
import pandas as pd
import seaborn as sns
接下来,我们使用 PyTorch 创建演员和评论家网络。Actor 类是 PyTorch 中的 nn.Module 的子类,代表策略网络,而 __init__ 方法定义了演员网络的架构。使用了三个全连接层(fc1、fc2、fc3)。forward 方法通过网络进行前向传递,应用激活函数(ReLU)并使用 softmax 函数返回动作概率:
class Actor(nn.Module):
def __init__(self, state_dim, action_dim):
super(Actor, self).__init__()
self.fc1 = nn.Linear(state_dim, 64)
self.fc2 = nn.Linear(64, 32)
self.fc3 = nn.Linear(32, action_dim)
def forward(self, state):
x1 = F.relu(self.fc1(state))
x2 = F.relu(self.fc2(x1))
action_probs = F.softmax(self.fc3(x2), dim=-1)
return action_probs
Critic 类也是 nn.Module 的子类,代表价值网络。__init__ 方法定义了评论家网络的架构,类似于演员网络。前向方法通过网络进行前向传递,应用激活函数(ReLU)并返回预测的价值:
class Critic(nn.Module):
def __init__(self, state_dim):
super(Critic, self).__init__()
self.fc1 = nn.Linear(state_dim, 64)
self.fc2 = nn.Linear(64, 32)
self.fc3 = nn.Linear(32, 1)
def forward(self, state):
x1 = F.relu(self.fc1(state))
x2 = F.relu(self.fc2(x1))
value = self.fc3(x2)
return value
作为延续,以下代码片段用于使用 OpenAI Gym 库创建 CartPole 环境的实例,并检索有关环境的重要信息:
env = gym.make("CartPole-v1") ①
env.seed(0) ②
state_dim = env.observation_space.shape[0] ③
n_actions = env.action_space.n ④
① 创建 CartPole 环境。
② 设置随机种子以帮助使环境的行为可重复。
③ 获取观察空间的维度。
④ 获取动作空间的动作数量。
state_dim 代表状态空间或观察空间,在这个例子中其值为 4(四个状态是滑车位置、滑车速度、杆角度和杆角速度)。n_actions 代表动作空间的维度或动作数量,在这个例子中是 2(向左推和向右推)。现在我们可以使用学习率 lr=1e-3 初始化演员和评论家模型以及演员和评论家模型的 Adam 优化器。折扣因子 gamma 决定了在强化学习算法中未来奖励与即时奖励的重要性:
actor = Actor(state_dim, n_actions) ①
critic = Critic(state_dim) ②
adam_actor = torch.optim.Adam(actor.parameters(), lr=1e-3) ③
adam_critic = torch.optim.Adam(critic.parameters(), lr=1e-3) ④
gamma = 0.99 ⑤
① 创建 Actor 类的实例。
② 创建评论家类的实例。
③ 为演员模型初始化 Adam 优化器。
④ 为评论家模型初始化 Adam 优化器。
⑤ 设置折扣率。
在此初始化之后,我们可以开始训练过程,其中智能体与环境进行指定数量的剧集交互。在训练过程中,智能体计算优势函数,更新演员和评论家模型,并跟踪训练统计信息。以下代码片段用于初始化训练过程:
num_episodes=500 ①
episode_rewards = [] ②
stats={'actor loss':[], 'critic loss':[], 'return':[]} ③
pbar = tqdm(total=num_episodes, ncols=80, bar_format='{l_bar}{bar}| {n_fmt}/
➥ {total_fmt}') ④
① 设置要运行的剧集总数。
② 创建一个空列表来存储每个剧集获得的总奖励。
③ 创建一个字典来存储训练统计信息,包括每个剧集的演员损失、评论家损失和总回报。
④ 初始化 tqdm 进度条。
训练循环遍历指定的剧集数量。在每个剧集期间,环境重置到其初始状态,并初始化随机种子以确保环境生成的随机数序列在不同代码运行中保持一致:
for episode in range(num_episodes):
done = False
total_reward = 0
state = env.reset()
env.seed(0)
然后,智能体通过根据其策略采取动作、累积奖励并更新其参数以改善其在平衡小车上的平衡性能,直到场景完成与环境进行交互。动作网络用于确定给定当前状态的行动概率,并使用这些概率创建一个分类分布。然后从这个分布中随机采样一个动作并在环境中执行。从环境中接收到的结果下一个状态、奖励、完成标志和附加信息完成智能体-环境交互循环的一步:
while not done:
probs = actor(torch.from_numpy(state).float()) ①
dist = torch.distributions.Categorical(probs=probs) ②
action = dist.sample() ③
next_state, reward, done, info = env.step(action.detach().data.numpy()) ④
① 根据当前状态获取动作概率。
② 创建一个分类分布。
③ 从分类分布中采样一个动作。
④ 将动作传递给环境。
然后使用奖励、下一个状态值和当前状态值计算优势函数。
advantage = reward + (1-
➥ done)*gamma*critic(torch.from_numpy(next_state).float()) -
➥ critic(torch.from_numpy(state).float()) ①
total_reward += reward ②
state = next_state ③
① 计算优势函数。
② 更新总奖励。
③ 移动到下一个状态。
这个优势函数用于更新评论家模型。评论家损失是通过优势的均方误差来计算的。评论家参数使用 Adam 优化器进行更新:
critic_loss = advantage.pow(2).mean()
adam_critic.zero_grad()
critic_loss.backward()
adam_critic.step()
动作损失是通过所选动作的负对数概率乘以优势来计算的。动作参数使用 Adam 优化器进行更新:
actor_loss = -dist.log_prob(action)*advantage.detach()
adam_actor.zero_grad()
actor_loss.backward()
adam_actor.step()
然后将场景的总奖励追加到episode_rewards列表中:
episode_rewards.append(total_reward)
将场景的动作损失、评论家损失和总奖励添加到stats字典中。每个场景都会打印出统计数据:
stats['actor loss'].append(actor_loss)
stats['critic loss'].append(critic_loss)
stats['return'].append(total_reward)
print('Actor loss= ', round(stats['actor loss'][episode].item(), 4), 'Critic
➥ loss= ', round(stats['critic loss'][episode].item(), 4), 'Return= ',
➥ stats['return'][episode]) ①
pbar.set_description(f"Episode {episode + 1}") ②
pbar.set_postfix({"Reward": episode_rewards}) ②
pbar.update(1) ②
pbar.close() ③
① 打印跟踪统计数据。
② 更新 tqdm 进度条。
③ 关闭 tqdm 进度条。
现在让我们通过散点图和趋势线来可视化学习过程,该散点图显示了每个训练场景中获得的场景奖励:
data = pd.DataFrame({"Episode": range(1, num_episodes + 1), "Reward":
➥ episode_rewards}) ①
plt.figure(figsize=(12,6)) ②
sns.set(style="whitegrid") ③
sns.regplot(data=data, x="Episode", y="Reward", scatter_kws={"alpha": 0.5}) ④
plt.xlabel("Episode")
plt.ylabel("Reward")
plt.title("Episode Rewards with Trend Line")
plt.show()
① 为场景奖励创建一个 DataFrame。
② 设置要显示的图形大小。
③ 将 seaborn 图表的样式设置为白色网格背景。
④ 创建带有趋势线的散点图。
当您运行此代码时,您将得到一个类似于图 12.9 的散点图和趋势线。

图 12.9 带趋势线的场景奖励
如您所见,在学习过程中奖励有波动,如图 12.9 所示的散点图显示了不同场景中奖励的增加和减少。然而,奖励在场景中的整体趋势或模式正在改善。在学习过程中奖励的波动是预期的,并且在强化学习中被认为是正常行为。最初,智能体可能会探索不同的动作,这可能导致成功和失败的场景,从而产生不同的奖励。随着训练的进行,智能体会细化其策略,并倾向于利用更有希望的动作,从而产生更一致的奖励。
Stable Baselines3 (SB3)基于 PyTorch 提供了更抽象和可靠的强化学习算法实现。作为列表 12.2 的延续,以下代码片段展示了使用 SB3 中的 A2C 实现处理 CartPole 环境的步骤。A2C 智能体使用 MlpPolicy 作为特定类型的策略网络,该网络反过来又使用多层感知器(MLP)架构。创建的智能体将与环境交互总共 10,000 个时间步来学习最优策略:
import gymnasium as gym ①
from stable_baselines3 import A2C ②
from stable_baselines3.common.evaluation import evaluate_policy ③
env = gym.make("CartPole-v1", render_mode="rgb_array") ④
model = A2C("MlpPolicy", env, verbose=1) ⑤
model.learn(total_timesteps=10000, progress_bar=True) ⑥
mean_reward, std_reward = evaluate_policy(model, model.get_env(),
➥ n_eval_episodes=10) ⑦
vec_env = model.get_env() ⑦
obs = vec_env.reset() ⑦
for i in range(1000): ⑦
action, _states = model.predict(obs, deterministic=True) ⑦
obs, rewards, dones, info = vec_env.step(action) ⑦
vec_env.render("human") ⑧
① 导入 gym 模块。
② 导入 A2C 模型
③ 评估训练模型的性能
④ 创建 CartPole-v1 环境。
⑤ 使用 MlpPolicy(多层感知器)和环境创建 A2C 模型。
⑥ 开始模型的训练过程。
⑦ 测试和评估训练模型。
⑧ 以人类可以可视化的方式渲染环境。
此代码片段设置环境,训练一个 A2C 模型,对其性能进行 1,000 步评估,然后可视化模型在环境中的动作。
我们可以使用 PPO 而不是 A2C 来平衡 CartPole。下一个列表与上一个列表非常相似,但它使用 PPO 而不是 A2C。
列表 12.3 使用 PPO 算法平衡 CartPole
import gymnasium as gym
from stable_baselines3 import PPO ①
from stable_baselines3.common.evaluation import evaluate_policy
env = gym.make("CartPole-v1", render_mode="rgb_array") ②
model = PPO("MlpPolicy", env, verbose=1) ③
model.learn(total_timesteps=10000, progress_bar=True) ④
① 从 SB3 导入 PPO 模型。
② 创建 CartPole-v1 环境的实例。
③ 使用 MlpPolicy 初始化 PPO 模型以处理代理网络。
④ 开始 10,000 个时间步的训练过程。
在训练期间,代码以下格式渲染日志输出:
------------------------------------------
| rollout/ | |
| ep_len_mean | 61.8 |
| ep_rew_mean | 61.8 |
| time/ | |
| fps | 362 |
| iterations | 5 |
| time_elapsed | 28 |
| total_timesteps | 10240 |
| train/ | |
| approx_kl | 0.0064375857 |
| clip_fraction | 0.051 |
| clip_range | 0.2 |
| entropy_loss | -0.61 |
| explained_variance | 0.245 |
| learning_rate | 0.0003 |
| loss | 26.1 |
| n_updates | 40 |
| policy_gradient_loss | -0.0141 |
| value_loss | 65.2 |
------------------------------------------
日志输出显示以下信息:
-
rollout/-
ep_len_mean—在回放过程中的平均回合长度 -
ep_rew_mean—在回放过程中的平均回合训练奖励
-
-
time/-
fps—训练期间每秒达到的帧数,表示算法的计算效率 -
iterations—完成的训练迭代次数 -
time_elapsed—从训练开始到现在的秒数 -
total_timesteps—代理在训练期间经历的总时间步数(环境中的步骤)
-
-
train/-
approx_kl—旧政策和新政策分布之间的近似 Kullback-Leibler (KL)散度,衡量训练期间政策变化程度 -
clip_fraction—被剪裁的代理损失的均值分数(超过clip_range阈值) -
clip_range—PPO 代理损失剪裁因子的当前值 -
entropy_loss—熵损失的均值(平均策略熵的负值) -
explained_variance—由价值函数解释的回报方差的比例 -
learning_rate—当前的学习率值 -
loss—当前的总损失值 -
n_updates—到目前为止应用的梯度更新次数 -
policy_gradient_loss—当前的政策梯度损失的值 -
value_loss—对于策略算法,当前的价值函数损失的值
-
我们通常会关注奖励和损失值。作为延续,以下代码片段展示了如何评估策略和渲染环境状态:
mean_reward, std_reward = evaluate_policy(model, model.get_env(),
➥ n_eval_episodes=10) ①
vec_env = model.get_env() ②
obs = vec_env.reset() ③
for i in range(1000): ④
action, _states = model.predict(obs, deterministic=True)
obs, rewards, dones, info = vec_env.step(action)
vec_env.render("human")
① 评估训练模型的策略。
② 获取与模型关联的向量化环境。
③ 将环境重置为其初始状态并获取初始观察。
④ 测试训练好的智能体。
如前述代码所示,在训练模型后,我们对训练模型的策略在 10 个回合中进行评估,并返回奖励的平均值和标准差。然后,我们允许智能体与环境交互 1000 步,并渲染环境。输出将是一个动画版本的第 12.8 图,展示了 CartPole 学习平衡策略的行为。
12.4 使用 PPO 在移动网络中进行自主协调
施耐德等人将 mobile-env 环境描述为无线移动网络中强化学习的开放平台[14]。此环境能够表示用户在指定区域内移动并可能连接到一个或多个基站。它支持多智能体和集中式强化学习策略。
在 mobile-env 环境中,我们有一个由多个基站或小区(BSs)和用户设备(UE)组成的移动网络,如图 12.10 所示。我们的目标是决定 UE 和 BS 之间应建立哪些连接,以实现全局体验质量(QoE)的最大化。对于单个 UE,通过尽可能多地与 BS 建立连接,实现更高的数据速率,从而获得更高的 QoE。然而,由于 BS 在连接的 UE 之间分配资源(例如,调度物理资源块),UE 最终会争夺有限的资源,导致目标冲突。

图 12.10 带有多个基站和用户设备的 mobile-env 环境
要实现全局最大化的 QoE,策略必须考虑两个关键因素:
-
每个连接的数据速率(DR 以 GB/s 计)由 UE 和 BS 之间的信道质量(例如,信噪比)决定。
-
单个 UE 的 QoE 并不一定随着数据速率的提高而线性增加。
让我们创建一个移动网络环境并训练一个 PPO 智能体来学习协调策略。我们将在列表 12.4 中开始,导入gymnasium,它是 OpenAI 的 Gym 库的维护分支。导入mobile_env以创建与移动网络相关的环境。IPython.display使得在 IPython 或 Jupyter Notebook 环境中使用交互式显示功能成为可能。我们将创建一个包含三个基站和五个用户的mobile_env的小实例。
列表 12.4 使用 PPO 进行移动网络协调
import gymnasium
import matplotlib.pyplot as plt
import mobile_env
from IPython import display
from stable_baselines3 import PPO
from stable_baselines3.ppo import MlpPolicy
env = gymnasium.make("mobile-small-central-v0", render_mode="rgb_array") ①
print(f"\nSmall environment with {env.NUM_USERS} users and {env.NUM_STATIONS}
➥ cells.") ②
① 创建环境的实例。
② 打印用户数量和基站数量。
接下来,我们将使用多层感知器(MLP)策略(MlpPolicy)和移动环境(env)创建 PPO 代理的实例。训练进度将记录到results_sb目录,以便 TensorBoard 可视化。您可以按照以下方式安装和设置tensorboard logdir:
pip install tensorboard
tensorboard --logdir .
代理总共训练了 30,000 个时间步:
model = PPO(MlpPolicy, env, tensorboard_log='results_sb', verbose=1)
model.learn(total_timesteps=30000, progress_bar=True)
以下代码片段可用于渲染训练后的环境状态。在剧集期间,训练好的模型根据当前观察预测要采取的动作。然后在该环境中执行该动作,环境响应下一个观察(obs)、收到的奖励(reward)、一个布尔标志表示剧集是否结束(terminated)、一个标志表示剧集是否因剧集时间限制而结束(truncated),以及环境信息(info):
obs, info = env.reset() ①
done = False ②
while not done:
action, _ = model.predict(obs) ③
obs, reward, terminated, truncated, info = env.step(action) ④
done = terminated or truncated ⑤
plt.imshow(env.render()) ⑥
display.display(plt.gcf()) ⑥
display.clear_output(wait=True) ⑥
① 重置环境,返回初始观察和环境信息。
② 标志以跟踪剧集是否完成。
③ 根据当前观察预测要采取的动作。
④ 执行动作并获取环境响应。
⑤ 更新标志
⑥ 渲染环境状态。
输出是图 12.10 的更新版本,显示了三个站点、五个用户以及它们之间的动态变化连接。还渲染了已建立连接的平均数据速率和平均效用。
与单个 RL 代理集中控制所有用户的细胞选择相比,一种替代方法是采用多代理 RL。在这个设置中,多个 RL 代理并行工作,每个代理负责特定用户的细胞选择。例如,在 mobile-small-ma-v0 环境中,我们将使用五个 RL 代理,每个代理满足单个用户的细胞选择需求。这种方法允许更分布式和去中心化的控制,提高了系统的可扩展性和效率。在本例中,我们将使用 Ray 和 Ray RLlib。Ray 是一个开源的统一框架,用于扩展 AI 和 Python 应用程序,如机器学习。Ray RLlib 是一个开源库,用于 RL,提供对生产级、高度分布式 RL 工作负载的支持,同时为各种行业应用保持统一和简单的 API。要使用 pip 安装 RLlib,请执行pip install -U "ray[rllib]"。
作为列表 12.4 的延续,我们可以导入 Ray 库,以多代理方法学习最佳协调策略:
import ray
from ray.tune.registry import register_env
import ray.air
from ray.rllib.algorithms.ppo import PPOConfig
from ray.rllib.policy.policy import PolicySpec
from ray.tune.stopper import MaximumIterationStopper
from ray.rllib.algorithms.algorithm import Algorithm
from mobile_env.wrappers.multi_agent import RLlibMAWrapper
以下函数将创建并返回一个适合 RLlib 多代理设置的包装环境。在这里,我们创建了一个小的mobile_env实例:
def register(config):
env = gymnasium.make("mobile-small-ma-v0")
return RLlibMAWrapper(env)
现在,我们将使用以下函数初始化 Ray。
ray.init(
num_cpus=2, ①
include_dashboard=False, ②
ignore_reinit_error=True, ③
log_to_driver=False, ④
)
① 指定 CPU 数量。
② 禁用 Ray 基于 Web 的仪表板。
③ 如果 Ray 已经初始化,则忽略重新初始化错误。
④ 忽略转发日志。
我们现在可以配置 RLlib 训练设置,在多智能体环境中使用mobile-env的小场景进行近端策略优化(PPO)算法:
config = (
PPOConfig()
.environment(env="mobile-small-ma-v0") ①
.multi_agent(
policies={"shared_policy": PolicySpec()},
policy_mapping_fn=lambda agent_id, episode, worker, **kwargs:
➥ "shared_policy",
) ②
.resources(num_cpus_per_worker=1) ③
.rollouts(num_rollout_workers=1) ④
)
① 设置环境。
② 配置所有智能体共享相同的策略。
③ 指定每个工作器应使用一个 CPU 核心。
④ 指示应有一个工作器专门执行回放。
以下代码片段配置并启动了一个使用 RLlib 框架的训练会话。它为 PPO 算法设置了一个调谐器(训练器)并执行了训练:
tuner = ray.tune.Tuner(
"PPO", ①
run_config=ray.air.RunConfig(
storage_path="./results_rllib", ②
stop=MaximumIterationStopper(max_iter=10), ③
checkpoint_config=ray.air.CheckpointConfig(checkpoint_at_end=True), ④
),
param_space=config, ⑤
)
result_grid = tuner.fit() ⑥
① 指定 PPO
② 指定训练结果和检查点(保存的模型状态)的存储位置。
③ 定义训练的停止条件。
④ 配置检查点的保存方式。
⑤ 指定训练参数。
⑥ 启动训练过程。
训练完成后,我们可以从结果中加载最佳训练智能体:
best_result = result_grid.get_best_result(metric="episode_reward_mean",
➥ mode="max") ①
ppo = Algorithm.from_checkpoint(best_result.checkpoint) ②
① 根据平均回合奖励的指标从 result_grid 中提取最佳训练结果。
② 从训练中获得的最佳检查点(模型状态)中加载智能体。
最后,我们可以在给定环境中评估训练好的模型并渲染结果:
env = gymnasium.make("mobile-small-ma-v0", render_mode="rgb_array") ①
obs, info = env.reset() ②
done = False
while not done: ③
action = {} ④
for agent_id, agent_obs in obs.items(): ⑤
action[agent_id] = ppo.compute_single_action(agent_obs,
➥ policy_id="shared_policy") ⑤
obs, reward, terminated, truncated, info = env.step(action) ⑥
done = terminated or truncated ⑦
plt.imshow(env.render()) ⑧
display.display(plt.gcf()) ⑧
display.clear_output(wait=True) ⑧
① 初始化环境。
② 将环境重置为其初始状态,并获取初始观察和附加信息。
③ 启动一个循环,使用训练好的模型运行一个回合。
④ 初始化一个空字典以保存多智能体环境中的每个智能体的动作。
⑤ 遍历每个智能体的观察结果。
⑥ 返回新的观察结果、奖励、终止标志、截断标志和附加信息。
⑦ 确定是否结束一个回合。如果回合被终止或截断为 True,则回合结束。
⑧ 可视化环境的当前状态。
运行此代码将产生如图 12.11 所示的环境的动画渲染。

图 12.11 使用多智能体 PPO 协调移动环境
此渲染显示了与五个用户单元和三个基站建立的连接以及获得的平均数据速率和效用。有关基于 PPO 的协调器的去中心化多智能体版本的更多信息,请参阅 Schneider 等人撰写的文章“mobile-env:无线移动网络强化学习的开源平台”以及相关的 GitHub 仓库[14]。
12.5 使用上下文赌博解决卡车选择问题
让我们考虑一个场景,即一个配送服务提供商正在计划为不同的配送路线分配卡车,如图 12.12 所示。目标是根据配送路线类型最大化车队效率。配送路线分为城市配送或州际配送,公司必须根据以下决策变量选择最佳类型的卡车:尺寸、发动机类型和轮胎类型。每个决策变量的可用选项如下:
-
尺寸—小号、中号或大号
-
发动机类型—汽油、柴油或电动
-
轮胎类型——全季、雪地或越野

图 12.12 基于指定上下文选择配送卡车大小、引擎类型和轮胎类型的 CMAB 基于推荐系统
奖励基于卡车选择对给定配送路线的适宜性。奖励函数接收配送路线类型和每个变量的所选动作作为参数。为了反映现实世界条件并增加问题的复杂性,我们在奖励值中添加噪声,以表示天气、路况等的不确定性。目标是选择最佳组合,以最大化总奖励。这意味着为每种类型的配送路线选择最合适的卡车大小、引擎类型和轮胎类型。
上下文多臂带式(CMAB)可以用来处理这个问题。Vowpal Wabbit(VW),一个最初在雅虎开发,目前在微软的开放源代码机器学习库,支持广泛的机器学习算法,包括 CMAB。您可以使用 pip install vowpalwabbit 安装 VW。
我们将使用 CMAB 来找到决策变量的最优值,以根据给定上下文最大化奖励。接下来的列表首先导入必要的库,并定义上下文相关带式问题的变量。
列表 12.5 上下文相关带式选择配送卡车
import vowpalwabbit
import torch
import matplotlib.pyplot as plt
import pandas as pd
import random
import numpy as np
from tqdm import tqdm
shared_contexts = ['city', 'interstate'] ①
size_types = ['small', 'medium', 'large'] ②
engine_types = ['petrol', 'diesel', 'electric'] ②
tire_types = ['all_season', 'snow', 'performance', 'all_terrain'] ②
① 设置共享上下文。
② 设置动作选项或臂。
我们然后定义以下奖励函数,它接受共享上下文和表示所选大小、引擎和轮胎选项的索引作为输入。它返回奖励值作为输出:
def reward_function(shared_context, size_index, engine_index, tire_index):
size_value = [0.8, 1.0, 0.9] ①
engine_value = [0.7, 0.9, 1.0] ②
tire_value = [0.9, 0.8, 1.0, 0.95] ③
reward = (
size_value[size_index]
* engine_value[engine_index]
* tire_value[tire_index]
) ④
noise_scale = 0.05 ⑤
noise_value = np.random.normal(loc=0, scale=noise_scale) ⑤
reward += noise_value ⑤
return reward ` ⑥
① 值越高表示燃油效率越好。
② 值越高表示性能越好。
③ 值越高表示舒适性越好。
④ 初始奖励基于所选选项。
⑤ 向奖励中添加噪声,以表示天气、路况等的不确定性。
⑥ 返回奖励。
作为延续,下面的 generate_combinations 函数生成了上下文相关带式问题的动作和示例组合。该函数接受四个输入:共享上下文和三个列表,分别代表动作的大小、引擎和轮胎选项。函数在处理完所有组合后返回示例列表和描述列表。使用嵌套循环遍历大小、引擎和轮胎选项的每个组合。使用 enumerate 函数同时检索索引(i、j、k)和相应的选项(大小、引擎、轮胎):
def generate_combinations(shared_context, size_types, engine_types, tire_types):
examples = [f"shared |User {shared_context}"]
descriptions = []
for i, size in enumerate(size_types):
for j, engine in enumerate(engine_types):
for k, tire in enumerate(tire_types):
examples.append(f"|Action truck_size={size} engine={engine}
➥ tire={tire}")
descriptions.append((i, j, k))
return examples, descriptions
我们现在需要从一个表示卡车动作的概率质量函数(PMF)中进行采样。sample_truck_pmf 函数从一个给定的 PMF 中采样一个索引,并返回索引及其概率。索引用于从索引列表中检索对应的大小、引擎和轮胎索引:
def sample_truck_pmf(pmf):
pmf_tensor = torch.tensor(pmf) ①
index = torch.multinomial(pmf_tensor, 1).item() ②
chosen_prob = pmf[index] ③
return index, chosen_prob ④
① 将 pmf 转换为 Torch 张量。
② 进行多项式采样。
③ 捕获所选动作的概率。
④ 返回采样的索引及其相应的概率。
现在我们需要创建一个 VW 工作空间来训练上下文赌博机模型:
cb_vw = vowpalwabbit.Workspace(
"--cb_explore_adf --epsilon 0.2 --interactions AA AU AAU -l 0.05 --power_t 0",
quiet=True,
)
工作空间使用以下参数定义:
-
--cb_explore_adf—此参数指定了使用动作相关特征(ADF)进行上下文赌博机学习的探索算法。在许多实际应用中,可能存在与每个动作(或臂)相关的特征,这些是动作相关特征。这使模型能够根据观察到的上下文探索不同的动作。 -
--epsilon 0.2—此参数将探索率或 epsilon 值设置为 0.2。它决定了模型探索随机动作而不是选择具有最高预测奖励的动作的概率。较高的 epsilon 值鼓励更多的探索。 -
--interactions AA AU AAU—此参数在 VW 中的命名空间之间创建特征交互。这些交互有助于模型捕捉特征之间的更复杂关系。 -
-l 0.05—此参数将学习率设置为 0.05。它决定了模型在学习过程中更新内部参数的速度。较高的学习率会使模型更快收敛,但如果学习率调整得太高,则可能存在过拟合的风险,平均效果会更差。 -
--power_t 0—此参数将功率值设置为 0。它影响学习率随时间衰减。功率值为 0 表示恒定的学习率。 -
quiet=True—此参数将quiet模式设置为True,在训练过程中抑制显示不必要的信息或进度更新。这有助于保持输出简洁和清晰。
以下代码片段用于训练 CMAB 模型:
num_iterations = 2500
cb_rewards = []
with tqdm(total=num_iterations, desc="Training") as pbar:
for _ in range(num_iterations):
shared_context = random.choice(shared_contexts) ①
examples, indices = generate_combinations(
shared_context, size_types, engine_types, tire_types
) ②
cb_prediction = cb_vw.predict(examples) ③
chosen_index, prob = sample_truck_pmf(cb_prediction) ④
size_index, engine_index, tire_index = indices[chosen_index] ⑤
reward = reward_function(shared_context, size_index, engine_index, ⑥
➥ tire_index) ⑥
cb_rewards.append(reward) ⑥
examples[chosen_index + 1] = f"0:{-1*reward}:{prob} {examples[chosen_
➥ index + 1]}" ⑦
cb_vw.learn(examples) ⑧
pbar.set_postfix({'Reward': reward})
pbar.update(1)
cb_vw.finish() ⑨
① 选择一个随机的共享上下文。
② 生成示例和索引。
③ 获取模型对所选动作的预测。
④ 获取所选索引及其相应的概率。
⑤ 检索相应的尺寸、引擎和轮胎索引。
⑥ 获取并附加奖励。
⑦ 更新与所选索引对应的示例。
⑧ 根据观察到的奖励学习和更新模型的内部参数。
⑨ 最终化 VW 工作空间。
训练完成后,我们可以使用以下函数来测试训练好的 CMAB 模型。此代码通过生成示例、进行预测、采样动作和基于给定的共享上下文计算期望奖励来评估训练好的模型:
def test_model(shared_context, size_types, engine_types, tire_types):
examples, indices = generate_combinations(shared_context, size_types,
➥ engine_types, tire_types)
cb_prediction = cb_vw.predict(examples)
chosen_index, prob = sample_truck_pmf(cb_prediction)
chosen_action = examples[chosen_index]
size_index, engine_index, tire_index = indices[chosen_index]
expected_reward = reward_function(shared_context, size_index,
➥ engine_index, tire_index)
print("Chosen Action:", chosen_action)
print("Expected Reward:", expected_reward)
test_shared_context = 'city'
test_model(test_shared_context, size_types, engine_types, tire_types)
代码将产生类似以下输出:
Chosen Action: Action truck_size=medium engine=electric tire=snow
Expected Reward: 1.012
这个输出表示在测试期间基于给定的共享上下文所选择的行为。在这种情况下,所选择的行为指定了一辆中等大小的卡车,一个电动引擎和一条雪地轮胎。获得的奖励是 1.012。请注意,带噪声的最大奖励约为 1.0 + 0.15 = 1.15。鉴于size_value、engine_value和tire_value的最大值都是 1,考虑到噪声是一个标准差为 0.05 的随机值,+3 个标准差(3 × 0.05 = 0.15)将覆盖正常分布中大约 99.7%的情况。
12.6 旅程的终点:最后的反思
在这本书中,我们开始了一段穿越搜索和优化算法多样景观的全面旅程。我们首先探索了确定性搜索算法,它们不知疲倦地穿越问题空间,通过盲法和信息法寻求最优解。然后我们攀登了基于轨迹的算法的峰谷,见证了模拟退火的力量和禁忌搜索逃离局部最优的巧妙设计。继续我们的旅程,我们进入了进化计算算法的领域,见证了遗传算法及其变体在解决复杂连续和离散优化问题中的力量。沿途,我们开始了与群体智能算法的迷人之旅,从粒子群优化开始,并提供了对蚁群优化和人工蜂群算法等其他算法的简要了解。最后,我们拥抱了基于机器学习的方法的领域,其中监督学习、无监督学习和强化学习算法被用来处理组合优化问题。本书中涵盖的每个算法都有其自身的优势和劣势。记住,技术选择取决于手头的任务、问题的特征和可用的资源。
我希望您从这本书中获得的知识能够让您有能力解决现实世界的问题,并拥抱不同领域中搜索和优化的无限潜力。搜索和优化算法的迷人世界仍在不断扩展和演变。利用这一知识,提升我们的能力,解决当今的问题,塑造未来,这取决于我们。
摘要
-
强化学习(RL)可以表述为一个优化问题,其中智能体旨在学习并/或改进其策略,以在特定环境中最大化预期的累积奖励。
-
强化学习问题可以分为两大类:马尔可夫决策过程(MDP)和多臂老丨虎丨机(MAB)问题。MDP 问题涉及智能体的动作影响环境和其未来状态的环境。MAB 问题侧重于最大化从一组独立选择(通常称为“臂”)中获得的累积奖励,这些选择可以在一段时间内重复进行。与 MDP 不同,MABs 不考虑选择对未来选项的影响。
-
在基于 MDP 的问题中,强化学习使用 MDP 作为基础数学框架来模拟不确定性下的决策问题。MDP 用于描述一个环境,其中智能体通过在环境中执行动作以实现目标来学习做出决策。
-
根据环境模型的存在与否,RL 被分为基于模型(model-based)和无模型(model-free)RL。模型指的是对环境行为的内部表示或理解——具体来说,是状态转移动力学和奖励函数。
-
根据 RL 算法如何从收集到的经验中学习和更新其策略,RL 算法可以分为离线策略(off-policy)和在线策略(on-policy)RL。
-
优势演员-评论家(A2C)和近端策略优化(PPO)是无模型的在线强化学习(RL)方法。
-
通过使用截断目标函数,PPO 在鼓励探索和保持策略更新时的稳定性之间取得平衡。截断操作将更新限制在有限范围内,防止可能导致性能下降的大规模策略变化。这种机制确保策略更新始终保持在先前策略的合理且可控距离内,从而促进更平滑和更稳定的学習。
-
与 MDP 不同,多臂老丨虎丨机(MABs)不考虑选择对未来状态的影响,智能体不需要担心在不同状态之间转换,因为只有一个状态。仅探索、仅利用贪婪、ε-贪婪和上置信界(UCB)是多臂老丨虎丨机策略的例子,用于确定选择动作的最佳方法以最大化时间累积奖励。
-
上下文多臂老丨虎丨机(CMABs)是多臂老丨虎丨机(MAB)的扩展,其中决策受到关于每个选择或环境的额外上下文信息的影響。
-
强化学习可以应用于解决各种组合优化问题,包括旅行商问题、交通信号控制、电梯调度、共享出行最优调度策略、货运配送问题、个性化推荐、CartPole 平衡、在移动网络中协调自动驾驶车辆以及卡车选择。
附录 A. Python 中的搜索和优化库
本附录涵盖了设置 Python 环境,以及数学编程、图形可视化、元启发式优化和机器学习的基本库。
A.1 设置 Python 环境
本书假设您已经在系统上安装了 Python 3.6 或更高版本。有关您操作系统的特定安装说明,请参阅此入门指南:wiki.python.org/moin/BeginnersGuide/。
对于 Windows 系统,您可以按照以下步骤安装 Python:
-
前往官方网站:www.python.org/downloads/。
-
选择要安装的 Python 版本。
-
下载 Python 可执行安装程序。
-
运行可执行安装程序。请确保选中“为所有用户安装启动器”和“将 Python 3.8 添加到 PATH”复选框。
-
通过在命令提示符中输入
python –V来验证 Python 是否成功安装。 -
通过在命令提示符中输入
pip -V来验证 pip 是否已安装。 -
在命令提示符中输入
pip install virtualenv来安装virtualenv。
如果您是 Linux 用户,请在终端中执行以下命令:
$ sudo apt update
$ sudo apt install python3-pip
安装 venv 并创建 Python 虚拟环境:
$ sudo apt install python3.8-venv
$ mkdir <new directory for venv>
$ python -m venv <path to venv directory>
确保将 python3.8 替换为你正在使用的 Python 版本。
您现在可以使用以下命令访问您的虚拟环境:
$ source <path to venv>/bin/activate
在 macOS 的情况下,Python 已经预安装,但如果您需要升级或安装特定版本,可以使用以下 macOS 终端操作:
$ python -m ensurepip --upgrade
venv 包含在 Python 3.8 及更高版本中。您可以使用以下命令创建虚拟环境:
$ mkdir <new directory>
$ python -m venv <path to venv directory>
您现在可以使用以下命令访问您的虚拟环境:
$ source <path to venv>/bin/activate
更好的选择是按照下一小节所述安装 Python 发行版。
A.1.1 使用 Python 发行版
Python 发行版,如 Anaconda 或 Miniconda,包含一个名为 conda 的包管理器,允许您安装广泛的 Python 包并管理不同的 Python 环境。使用以下指南为您操作系统安装 conda:conda.io/projects/conda/en/latest/user-guide/install/index.html。
Conda 环境用于管理不同版本的 Python 包及其依赖项的多个安装。您可以使用以下命令创建一个 conda 环境:
$ conda create --name <name of env> python=<your version of python>
按照以下方式访问新创建的环境:
$ conda activate <your env name>
此命令允许您在环境之间切换或移动。
A.1.2 安装 Jupyter Notebook 和 JupyterLab
Jupyter 是一个多语言、开源的基于网络的交互式编程平台 (jupyter.org/)。名称“Jupyter”是一个松散的首字母缩略词,代表 Julia、Python 和 R。本书中的所有代码都存储在 Jupyter 笔记本 (.ipynb 文件) 中,可以使用 JupyterLab 或 Jupyter Notebook 打开和编辑。Jupyter Notebook 感觉更独立,而 JupyterLab 则更像是一个集成开发环境 (IDE)。
您可以使用 pip 按照以下方式安装 JupyterLab:
$ pip install jupyterlab
$ pip install notebook
或者按照以下方式使用 conda:
$ conda install -c conda-forge jupyterlab
$ conda install -c conda-forge notebook
您可以使用 pip 按照以下方式安装 Python ipywidgets 软件包,以自动配置经典 Jupyter Notebook 和 JupyterLab 3.0 以显示 ipywidgets:
$ pip install ipywidgets
$ conda install -c conda-forge ipywidgets
如果您安装了旧版本的 Jupyter Notebook,您可能需要手动使用以下命令启用 ipywidgets 笔记本扩展:
$ jupyter nbextension install --user --py widgetsnbextension
$ jupyter nbextension enable --user --py widgetsnbextension
Google Colaboratory (Colab) 也可以使用。这个基于云的工具允许您通过浏览器编写、执行和分享 Python 代码。它还提供免费访问 GPU 和 TPU 以增强计算能力。您可以通过以下链接访问 Colab:colab.research.google.com/.
A.1.3 克隆书籍的仓库
您可以按照以下步骤克隆此书籍的代码仓库:
$git clone https://github.com/Optimization-Algorithms-Book/Code-Listings.git
本书中的许多操作都很长且难以从头编写代码。通常,它们高度标准化,并可以从具有辅助函数处理各种复杂性的情况下受益。optalgotools 是为此目的开发的 Python 软件包。
您可以在不安装此软件包的情况下本地使用这些支持工具。为此,您需要将 optalgotools 下载到本地文件夹,并将此文件夹添加到系统路径中。如果您使用 Jupyter notebook 或 Jupyter lab,可以按照以下步骤操作:
import sys
sys.path.insert(0, '../')
如果您使用 Colab,可以使用以下命令将您的 Google Drive 挂载:
from google.colab import drive
drive.mount('/content/drive')
然后,将 optalgotools 文件夹复制到您的 Google Drive。
此软件包也可在 Python 包索引 (PyPI) 仓库中找到:pypi.org/project/optalgotools/. 您可以按照以下步骤进行安装:
$pip install optalgotools
然后,您可以使用 import 命令来使用这些工具。以下是一个示例:
from optalgotools.problems import TSP
from optalgotools.algorithms import SimulatedAnnealing
第一行从 problems 模块导入 TSP 实例,第二行从 algorithms 模块导入模拟退火求解器。
A.2 数学规划求解器
数学规划,也称为数学优化,是寻找可以用数学术语表示的问题的最佳解决方案的过程。它涉及制定问题的数学模型,确定模型的参数,并使用数学和计算技术找到解决方案,该解决方案在满足一系列约束条件下最大化或最小化特定的目标函数或一组目标函数。线性规划(LP)、混合整数线性规划(MILP)和非线性规划(NLP)是数学优化问题的例子。可以使用几个 Python 库来解决数学优化问题。
让我们考虑 Guerte 等人《线性规划》[1]中的以下生产计划示例。一个小型木工车间生产两种尺寸的黄杨木棋套装。制作较小的套装需要 3 小时的车削加工,而较大的套装则需要 2 小时。有四个熟练的操作员,每人每周工作 40 小时,车间每周有总共 160 小时的车削时间可用。较小的棋套装消耗 1 公斤黄杨木,而较大的套装则需要 3 公斤。然而,由于稀缺,每周只能获得 200 公斤黄杨木。出售时,每个大型棋套装产生 12 美元的利润,而每个小型套装产生 5 美元的利润。目标是确定每个套装的最佳每周生产数量以最大化利润。
假设 x[1] 和 x[2] 是决策变量,分别代表要制作的中小型棋套装的数量。总利润是制作和销售 x[1] 套小型棋套装和 x[2] 套大型棋套装的个体利润之和:利润 = 5x[1] + 12x[2]。然而,这种利润受到以下约束:
-
我们将使用机器的总小时数为 3x[1] + 2x[2]。这个时间不应超过每周可用的最大 160 小时机器时间。这意味着 3x[1] + 2x[2] ≤ 160 (车削小时)。
-
每周只有 200 公斤的黄杨木可用。由于小套装每套需要 1 公斤,而大套装则需要 3 公斤,因此 x[1] + 3x[2] ≤ 200 (公斤黄杨木)。
-
木工车间不能生产负数的棋套装,因此我们有两个额外的非负约束:x[1] 和 x[2] >= 0。
这个线性规划问题可以总结如下。找到 x[1] 和 x[2],使得 5x[1] + 12x[2] 最大化,同时满足
-
加工时间约束:3x[1] + 2x[2] ≤ 160
-
重量约束:x[1] + 3x[2] ≤ 200
-
非负约束:x[1] 和 x[2] >= 0
让我们看看如何使用不同的求解器来解决这个线性规划问题。
A.2.1 SciPy
SciPy 是一个开源的科学计算 Python 库,提供了优化、线性代数和统计的工具。SciPy optimize 是 SciPy 库的一个子模块,包括非线性问题的求解器(支持局部和全局优化算法)、线性规划、约束和非线性最小二乘、根查找和曲线拟合。
要使用 SciPy,您需要安装它及其依赖项。您可以使用 pip 包管理器安装 SciPy:
$pip install scipy
或者,您可以使用预装了 SciPy 和其他科学库的 Python 发行版,如 Anaconda 或 Miniconda。
列表 A.1 展示了使用 SciPy 解决汽车制造问题的步骤。代码定义了系数向量c和约束方程的左侧(lhs)和右侧(rhs)。目标函数代表要最大化的利润。由于 SciPy 中的许多优化算法是为最小化设计的,因此利润最大化问题通常通过最小化利润函数的负值来转换为最小化问题。此外,使用大于或等于符号的约束不能直接定义。必须使用小于或等于代替。
列表 A.1 使用 SciPy 解决棋盘问题
import numpy as np
import scipy
from scipy.optimize import linprog
c = -np.array([5,12]) ①
lhs_constraints=([3,2], ②
[1,3]) ③
rhs_constraints=([160, ④
200]) ⑤
bounds = [(0, scipy.inf), (0, scipy.inf)] ⑥
results = linprog(c=c, A_ub=lhs_constraints, b_ub=rhs_constraints,
➥ bounds=bounds, method='highs-ds') ⑦
print('LP Solution:') ⑧
print(f'Profit: = {-round(results.fun,2)} $') ⑧
print(f'Make {round(results.x[0],0)} small sets, and make ⑧
{round(results.x[1],0)} large sets') ⑧
① 声明目标函数的系数。
② 加工时间约束的左侧
③ 权重约束的左侧
④ 加工时间约束的右侧
⑤ 权重约束的右侧
⑥ 决策变量的界限
⑦ 解决线性规划问题。
⑧ 打印解决方案。
运行此代码将得到以下结果:
LP Solution:
Profit: = 811.43 $
Make 11.0 small sets, and make 63.0 large sets
在前面的代码中使用的linprog()函数返回一个包含多个属性的数据结构,例如x(当前解向量)、fun(目标函数的当前值)和success(当算法成功完成时为true)。
A.2.2 PuLP
PuLP 是一个 Python 线性规划库,允许您定义和解决线性优化问题。PuLP 中有两个主要类:LpProblem和LpVariable。PuLP 变量可以单独声明或作为“字典”(在另一个集合上索引的变量)。
您可以使用以下 pip 命令安装 PuLP:
$pip install pulp
以下代码(列表 A.1 的延续)展示了如何使用 PuLP 解决棋盘问题:
#!pip install pulp
from pulp import LpMaximize, LpProblem, LpVariable, lpSum, LpStatus
model = LpProblem(name='ChessSet', sense=LpMaximize) ①
x1 = LpVariable('SmallSet', lowBound = 0, upBound = None, cat='Integer') ②
x2 = LpVariable('LargeSet', lowBound = 0, upBound = None, cat='Integer') ②
model += (3*x1 + 2*x2 <=160, 'Machining time constraint') ③
model += ( x1 + 3*x2 <= 200, 'Weight constraint') ③
profit= 5*x1 + 12*x2 ④
model.setObjective(profit) ④
model.solve() ⑤
print('LP Solution:') ⑥
print(f'Profit: = {model.objective.value()} $') ⑥
print(f'Make {x1.value()} small sets, and make {x2.value()} large sets') ⑥
① 定义模型。
② 定义决策变量。
③ 添加约束。
④ 将利润设置为目标函数。
⑤ 解决优化问题。
⑥ 打印解决方案。
PuLP 实现了多种算法来解决线性规划(LP)和混合整数线性规划(MILP)问题。例如包括 COIN-OR(运筹学研究计算基础设施)、CLP(COIN-OR 线性规划)、Cbc(COIN-OR 分支和切割)、CPLEX(Cplex)、GLPK(GNU 线性规划库)、SCIP(解决约束整数规划)、HiGHS(高度可扩展的全局求解器)、Gurobi LP/MIP 求解器、Mosek 优化器以及 XPRESS LP 求解器。
A.2.3 其他数学编程求解器
Python 中还有几个用于解决数学优化问题的库。以下是一个不完整的其他可用库列表:
-
OR-Tools—这是一个由 Google 开发的用于优化和约束编程的开源软件套件。它包括解决运筹学、运输、调度和物流等领域问题的各种算法和工具。OR-Tools 可用于建模和解决线性规划、整数规划以及约束编程问题。OR-Tools 求解器的例子包括 GLOP(Google 线性规划)、Cbc(COIN-OR 分支和切割)、CP-SAT(约束编程-可满足性)求解器、最大流和最小成本流求解器、最短路径求解器以及 BOP(二进制优化问题)求解器。它用 C++编写,并为包括 Python、C#和 Java 在内的多种编程语言提供了接口。有关更多详细信息及示例,请参阅 A.4.5 节。
-
Gurobi—这是一款商业优化软件,提供线性规划、二次规划和混合整数规划的尖端求解器。它提供了一个 Python 接口,可用于定义和解决优化问题。
-
CasADi—这是一个用于非线性优化和算法微分的开源工具。
-
Python-MIP—这是一个用于解决混合整数规划问题的 Python 库。它建立在 Cbc 开源优化库之上,并允许用户使用高级、数学化的编程语言表达优化模型。
-
Pyomo—这是一个开源的优化建模语言,可用于在 Python 中定义和解决数学优化问题。它支持广泛的优化求解器,包括线性规划、混合整数规划和非线性优化。
-
GEKKO—这是一个用于机器学习和混合整数及微分代数方程优化的 Python 包。
-
CVXPY—这是一个用于凸优化问题的开源 Python 嵌入式建模语言。它允许您以遵循数学的方式自然地表达问题,而不是遵循求解器要求的限制性标准形式。
-
PyMathProg—这是一个为 Python 设计的数学编程环境,允许建模、求解、分析、修改和操作线性规划问题。
-
Optlang—这是一个用于建模和求解数学优化问题的 Python 库。它提供了一系列优化工具的通用接口,以便可以以透明的方式更改不同的求解器后端。它与大多数流行的优化求解器兼容,如 Gurobi、Cplex 和 Ipopt(内点优化器)。
-
Python 接口到锥优化求解器 (PICOS)—这是一个用于建模和求解优化问题的 Python 库。它可以处理具有多个目标的复杂问题,并支持局部和全局优化方法。PICOS 具有与不同求解器(如 Gurobi、CPLEX、SCS(分割锥求解器)、ECOS(嵌入式锥求解器)和 MOSEK)的接口。
-
CyLP—这是一个到 COIN-OR 的线性混合整数规划求解器(CLP、Cbc 和 CGL)的 Python 接口。COIN-OR(运筹学计算基础设施)是运筹学和计算优化开源软件包的集合。它包括线性规划和整数规划、约束规划以及其他优化技术的库。
-
SymPy—这是一个用于符号数学的 Python 库。它可以用来解方程、处理组合数学、在 2D/3D 中绘图,或者处理多项式、微积分、离散数学、矩阵、几何、解析、物理、统计学和密码学。
书籍的 GitHub 仓库中包含的 Jupyter 笔记本“Listing A.1_Mathematical_programming_solvers.ipynb”展示了如何使用这些求解器解决棋盘问题。
A.3 图和映射库
本书使用以下 Python 库来处理和可视化图、网络和地理空间数据。
A.3.1 NetworkX
NetworkX 是一个用于在 Python 中处理图和网络库。它提供了创建、操作和分析图数据以及可视化图结构的工具。NetworkX 还包含图属性的近似和优化问题的启发式方法。你可以按照以下方式安装 NetworkX:
$pip install networkx
让我们考虑旅行商问题(TSP)。列表 A.2 显示了为该问题创建随机无向图的步骤。每个随机散布的节点代表销售员要访问的城市,连接城市的每条边的权重是基于节点之间的欧几里得距离使用hypot函数计算的,该函数计算平方和的平方根。使用 Christofides 算法解决此 TSP 实例——此算法提供 TSP 的 3/2 近似。这意味着其解决方案将在最优解长度的 1.5 倍以内。
列表 A.2 使用 NetworkX 解决 TSP
import matplotlib.pyplot as plt
import networkx as nx
import networkx.algorithms.approximation as nx_app
import math
plt.figure(figsize=(10, 7))
G = nx.random_geometric_graph(20, radius=0.4, seed=4) ①
pos = nx.get_node_attributes(G, "pos")
pos[0] = (0.5, 0.5) ②
H = G.copy() ③
for i in range(len(pos)): ④
for j in range(i + 1, len(pos)): ④
dist = math.hypot(pos[i][0] - pos[j][0], pos[i][1] - pos[j][1]) ④
dist = dist ④
G.add_edge(i, j, weight=dist) ④
cycle = nx_app.christofides(G, weight="weight") ⑤
edge_list = list(nx.utils.pairwise(cycle))
nx.draw_networkx_edges(H, pos, edge_color="blue", width=0.5) ⑥
nx.draw_networkx( ⑦
G, ⑦
pos, ⑦
with_labels=True, ⑦
edgelist=edge_list, ⑦
edge_color="red", ⑦
node_size=200, ⑦
width=3, ⑦
) ⑦
print("The route of the salesman is:", cycle) ⑧
plt.show()
① 创建一个包含 20 个节点的随机几何图。
② 将(0,0)设置为家乡城市/仓库。
③ 创建一个独立的浅拷贝的图和属性。
④ 将节点之间的距离作为边的权重计算。
⑤ 使用 Christofides 算法解决 TSP。
⑥ 仅在每个节点上突出显示最近的边。
⑦ 绘制路线。
⑧ 打印路线。
图 A.1 显示了此 TSP 的解决方案。

图 A.1 使用 NetworkX 中实现的 Christofides 算法解决 TSP。找到的路线是 0, 10, 7, 2, 6, 1, 15, 14, 5, 17, 4, 9, 12, 18, 3, 19, 16, 8, 11, 13, 0。
NetworkX 支持多种图搜索算法,并可以使用地理空间 Python 生态系统中的包执行网络分析。
A.3.2 OSMnx
OSMnx 是一个 Python 库,旨在简化从 OpenStreetMap (OSM) 获取和操作数据的过程。它提供了从 OSM 下载数据(过滤后)并返回作为 NetworkX 图数据结构网络的能力。它是全球免费开源的地理数据。
您可以使用conda安装 OSMnx:
$ conda config --prepend channels conda-forge
$ conda create -n ox --strict-channel-priority osmnx
$ conda activate ox
OSMnx 可以将地点的文本描述符转换为 NetworkX 图。以下列表 A.2 的后续内容中,我们将以纽约市的时报广场为例。
import osmnx as ox
place_name = "Times Square, NY" ①
graph = ox.graph_from_address(place_name, network_type='drive') ②
ox.plot_graph(graph,figsize=(10,10)) ③
① 地点名称或兴趣点
② 命名地点的 NetworkX 图
③ 绘制图。
图 A.2 显示了基于驾驶模式的时报广场图。

图 A.2 可行驶街道的时报广场图
network_type 允许您根据移动模式选择街道网络类型:all_private、all、bike、drive、drive_service或walk。您可以使用这两行代码突出显示时报广场街道网络中的所有单行边:
ec = ['y' if data['oneway'] else 'w' for u, v, key, data in graph.edges(keys=True, data=True)]
fig, ax = ox.plot_graph(graph, figsize=(10,10), node_size=0, edge_color=ec, edge_linewidth=1.5, edge_alpha=0.7)
可以检查图的各种属性,例如图类型、边(道路)类型、CRS 投影等。例如,您可以使用type(graph)打印图类型,并如下提取图的节点和边作为单独的结构:
nodes, edges = o.graph_to_gdfs(graph)
nodes.head(5)
我们可以进一步深入到每个单独的节点或边进行考察。
list(graph.nodes(data=True))[1]
list(graph.edges(data=True))[0]
您还可以检索图的街道类型:
print(edges['highway'].value_counts())
执行前面的代码行给出了以下关于时报广场道路网络的统计数据:
secondary 236
residential 120
primary 83
unclassified 16
motorway_link 12
tertiary 10
motorway 7
living_street 3
[unclassified, residential] 1
[motorway_link, primary] 1
trunk 1
可以使用osmnx.basic_stats(graph)生成更多统计信息。
使用osmnx.graph_from_gdfs可以将 GeoDataFrames 轻松转换为 MultiDiGraphs,如下所示:
new_graph = ox.graph_from_gdfs(nodes,edges)
ox.plot_graph(new_graph,figsize=(10,10))
这导致了与图 A.2 中显示相同的道路网络。您还可以按以下方式将街道网络保存为不同的格式:
ox.plot_graph(graph, figsize=(10,10), show=False, save=True, close=True,
filepath='./data/TimesSquare.png') ①
ox.plot_graph(graph, figsize=(10,10), show=False, save=True, close=True,
filepath='./data/TimesSquare.svg') ②
ox.save_graph_xml(graph, filepath='./data/TimesSquare.osm') ③
ox.save_graph_geopackage(graph, filepath='./data/TimesSquare.gpkg') ④
ox.save_graphml(graph, filepath='./data/TimesSquare.graphml') ⑤
ox.save_graph_shapefile(graph, filepath='./data/TimesSquare') ⑥
① 将街道网络保存为 PNG。
② 将街道网络保存为 SVG。
③ 将图形保存为磁盘上的.osm XML 文件。
④ 将街道网络保存为 GeoPackage 文件用于 GIS
⑤ 将街道网络保存为 OSMnx、NetworkX 或 Gephi 的 GraphML 文件。
⑥ 将图形保存为 shapefile。
A.3.3 GeoPandas
GeoPandas 是 Pandas 的一个扩展,通过扩展 Pandas 的数据类型和查询、操作空间数据的能力来处理地理空间数据。它提供了读取、写入和操作地理空间数据的工具,以及可视化并在地图上绘制数据的工具。您可以使用 pip 或 conda 安装 GeoPandas,如下所示:
$conda install geopandas or $pip install geopandas
GeoPandas 可以处理不同的地理空间数据格式,如 shapefiles (.shp)、CSV(逗号分隔值)、GeoJSON、ESRI JSON、GeoPackage (.gpkg)、GML、GPX(GPS 交换格式)和 KML(Keyhole 标记语言)。例如,假设我们想根据书中 GitHub 仓库(附录 B 数据文件夹中)包含的安大略省数据目录中的 shapefile 读取安大略省的健康区域数据。shapefile 是一种流行的地理空间数据格式,用于存储矢量数据(如点、线和多边形)。它是存储 GIS 数据广泛使用的格式,并且被许多 GIS 软件包支持,包括 ArcGIS 和 QGIS。实际上,shapefile 是一组具有不同扩展名的文件集合,包括以下内容:
-
.shp—主文件,包含地理空间数据(点、线或多边形)
-
.shx—索引文件,允许更快地访问.shp 文件中的数据
-
.dbf—属性文件,包含.shp 文件中每个特征的属性数据(非地理信息)
-
.prj—投影文件,定义.shp 文件中数据的坐标系和投影信息
-
.sbx—特征的空间索引
列表 A.2 的以下续集显示了如何从data.ontario.ca/dataset/ontario-s-health-region-geographic-data读取此地理空间数据,该数据存储在本书的 GitHub 仓库中:
import geopandas as gpd
import requests
import os
base_url = "https://raw.githubusercontent.com/Optimization-Algorithms-
➥Book/Code-Listings/05766c64c5e83dcd6788cc4415b462e2f82e0ccf/
➥Appendix%20B/data/OntarioHealth/" ①
files = ["Ontario_Health_Regions.shp", "Ontario_Health_Regions.shx",
➥ "Ontario_Health_Regions.dbf", "Ontario_Health_Regions.prj"] ②
for file in files: ③
response = requests.get(base_url + file) ③
with open(file, 'wb') as f: ③
f.write(response.content) ③
ontario = gpd.read_file("Ontario_Health_Regions.shp") ④
for file in files: ⑤
os.remove(file) ⑤
print(ontario.head()) ⑥
① 定义原始文件的基 URL。
② 与 shapefile 相关的文件。
③ 从指定的 URL 临时下载文件。
④ 使用 geopandas 读取 shapefile。
⑤ 清理/删除下载的文件。
⑥ 打印前 n 行。
列表 A.2 的完整版本可在本书的 GitHub 仓库中找到。
A.3.4 上下文相关
contextily 是一个 Python 库,用于向使用 Matplotlib、Plotly 等库创建的图表添加上下文底图。例如,contextily 可以用于在渲染安大略省卫生区域数据时添加上下文,如下所示:
#!pip install contextily
import contextily as ctx
ax=ontario.plot(cmap='jet', edgecolor='black', column='REGION', alpha=0.5,
➥ legend=True, figsize=(10,10))
ax.set_title("EPSG:4326, WGS 84")
ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik,
➥ crs=ontario.crs.to_string())
contextily 支持多个不同的底图源,包括以下常用源:
-
OpenStreetMap (OSM)—这是 contextily 的默认源。它是一个免费的开源地图服务,提供多种不同的样式,包括默认的 Mapnik 样式以及其他如人道主义和自行车样式。
-
Stamen—本源提供多种不同的地图样式,包括 Toner、地形和水彩。
-
Mapbox—本源提供多种不同的地图样式,包括街道、户外和卫星图。使用时需要 API 密钥。
-
MapQuest—本源提供多种不同的地图样式,包括 OSM 和航空图。使用时需要 API 密钥。
-
这里—本源提供多种不同的地图样式,包括正常日和正常夜。使用时需要 API 密钥。
-
Google Maps—本源提供多种不同的地图样式,包括路线图、卫星图和地形图。使用时需要 API 密钥。
A.3.5 Folium
Folium 是一个用于在 Python 中使用 Leaflet.js 库创建交互式地图的库。它提供了读取、写入和操作地理空间数据、以及可视化并在地图上绘制数据的工具。Folium 可以用来创建静态或动态地图,还可以自定义地图的外观和行为。以下 A.2 列表的延续展示了如何使用 Folium 在地图上可视化安大略省卫生区域:
#!pip install folium
import folium
ontario = ontario.to_crs(epsg=4326) ①
m = folium.Map(location=[43.67621,-79.40530],zoom_start=7,
➥ tiles='cartodbpositron', scrollWheelZoom=False, dragging=True) ②
for index, row in ontario.iterrows(): ③
sim_geo = gpd.GeoSeries(row['geometry']).simplify(tolerance=0.001)
geo_j = sim_geo.to_json()
geo_j = folium.GeoJson(data=geo_j,
➥ name=row['REGION'],style_function=lambda x: {'fillColor': 'black'})
folium.Popup(row['REGION']).add_to(geo_j)
geo_j.add_to(m)
m ④
① 将几何形状转换为新的坐标参考系统(CRS)。
② 设置起始位置、初始缩放和底图源。
③ 简化每个区域的 polygon,因为复杂的细节是不必要的。
④ 渲染地图。
书籍 GitHub 仓库中可用的“附录 A.2_ 图形库.ipynb”笔记本提供了不同方式可视化地理空间数据的示例,例如渐变地图、人口分布地图、气泡地图、六边形分箱、热图和聚类地图。
A.3.6 其他库和工具
以下是非详尽的列表,列出了其他用于处理地理空间数据、图形和网络的有用库和工具:
-
Pyrosm—这是另一个用于从 Protocolbuffer Binary Format 文件 (.osm.pbf) 读取 OpenStreetMap 的 Python 库。它可以用于下载和读取 OpenStreetMap 数据,提取诸如道路、建筑和兴趣点等特征,并分析和可视化数据。Pyrosm 与 OSMnx 的主要区别在于,OSMnx 使用 OverPass API 读取数据,而 Pyrosm 从本地从 Protocolbuffer Binary Format 文件 (.osm.pbf) 数据提供者(Geofabrik、BBBike)下载的 OSM 数据转储中读取数据,并将其转换为 GeoPandas GeoDataFrames。这使得解析 OSM 数据更快,并使得提取覆盖大范围的数据更加可行。
-
Pandana—这是一个使用收缩层次结构来计算超级快速旅行可达性指标和最短路径的 Python 网络分析库。
-
GeoPy—这是一个用于几个流行的地理编码 Web 服务的 Python 客户端。
-
Graphviz—这是一个用于在 Python 中创建图和树结构可视化的库。它提供了定义图结构以及以各种格式(如 PNG、PDF 和 SVG)渲染图的工具。Graphviz 是可视化在图上操作的算法(如图搜索算法和图遍历算法)的有用工具。
-
Gephi—这是一个用于可视化和分析图和网络的工具。它提供了一个图形用户界面来定义和自定义图形和图表的外观,以及可视化算法和数据结构。Gephi 可以用于可视化在图数据上操作的算法,例如图搜索算法和最短路径算法。
-
Cytoscape—这是一个用于可视化复杂网络并将其与任何类型的属性数据集成的开源软件平台。
-
ipyleaflet—这是一个基于 ipywidgets 的交互式小部件库。ipywidgets,也称为 jupyter-widgets 或简称为 widgets,是 Jupyter 笔记本和 IPython 内核的交互式 HTML 小部件。Ipyleaflet 将地图功能带到了笔记本和 JupyterLab 中。
-
hvPlot—这是一个基于 HoloViews 库构建的 Python 库,提供高级绘图 API。它可以与 GeoPandas 一起使用,创建地理空间数据的交互式可视化。
-
mplleaflet—这是一个基于
leaflet的库,但它与matplotlib配合得很好。 -
Cartopy—Cartopy 是一个用于在 Python 中创建地图和地理空间绘图的库。
-
geoplotlib—geoplotlib 是一个用于在 Python 中创建地图和可视化的库。它提供了样式化和定制地图元素的工具,以及将数据叠加到地图上的工具。geoplotlib 可以用于创建静态或交互式地图,并支持多种地图投影和坐标系。
-
Shapely—这是一个用于在笛卡尔平面上对对象执行几何操作的 Python 开源库。
-
deck.gl—这是一个用于 WebGL 驱动的开放式 JavaScript 库,用于大规模数据集的可视化。
-
kepler.gl—这是一个强大的开源地理空间分析工具,用于大规模数据集。
A.4 元启发式优化库
Python 中有几个库提供了不同元启发式优化算法的实现。以下小节涵盖了某些常用库。
A.4.1 PySwarms
PySwarms 是一个用于在 Python 中实现群体智能算法的库。它提供了定义、训练和评估群体智能模型以及可视化优化过程的工具。PySwarms 支持多种群体智能算法,包括粒子群优化(PSO)和蚁群优化(ACO)。下面的列表显示了使用 PySwarms 实现的 PSO 解决函数优化问题的步骤。
列表 A.3 使用 PySwarms 实现的 PSO 解决函数优化问题
#!pip install pyswarms
import pyswarms as ps
from pyswarms.utils.functions import single_obj as fx
from pyswarms.utils.plotters import plot_cost_history, plot_contour,
➥ plot_surface
from pyswarms.utils.plotters.formatters import Mesher, Designer
import matplotlib.pyplot as plt
from IPython.display import Image ①
options = {'c1':0.5, 'c2':0.3, 'w':0.9} ②
optimizer = ps.single.GlobalBestPSO(n_particles=50, dimensions=2,
➥ options=options) ③
optimizer.optimize(fx.sphere, iters=100) ④
plot_cost_history(optimizer.cost_history) ⑤
plt.show()
m = Mesher(func=fx.sphere, limits=[(-1,1), (-1,1)]) ⑥
d = Designer(limits=[(-1,1), (-1,1), (-0.1,1)], label=['x-axis', 'y-axis',
➥ 'z-axis']) ⑦
animation = plot_contour(pos_history=optimizer.pos_history, mesher=m,
➥ designer=d, mark=(0,0)) ⑧
animation.save('solution.gif', writer='imagemagick', fps=10)
Image(url='solution.gif') ⑨
① 导入 Image 类以在笔记本环境中显示图像。
② 设置 PSO 作为具有 50 个粒子和预定义参数的优化器。
③ 使用 PSO 解决函数优化问题。
④ 设置使用 PSO 优化的单峰球面函数,并设置迭代次数。
⑤ 绘制成本。
⑥ 绘制球面函数的网格以获得更好的图形。
⑦ 调整图形限制。
⑧ 在等高线上生成解的历史动画。
⑨ 渲染动画。
A.4.2 Scikit-opt
Scikit-opt 是一个优化库,它提供了一个简单灵活的接口来定义和运行使用各种元启发式(如遗传算法、粒子群优化、模拟退火、蚁群算法、免疫算法和人工鱼群算法)的优化问题。Scikit-opt 可以用于解决连续和离散问题。以下列表 A.3 的延续显示了使用 scikit-opt 解决函数优化问题的步骤:
#!pip install scikit-opt
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sko.SA import SA
obj_func = lambda x: np.sin(x[0]) * np.cos(x[1]) ①
sa = SA(func=obj_func, x0=np.array([-3, -3]), T_max=1, T_min=1e-9, L=300,
➥ max_stay_counter=150) ②
best_x, best_y = sa.run()
print('best_x:', best_x, 'best_y', best_y)
plt.plot(pd.DataFrame(sa.best_y_history).cummin(axis=0)) ③
plt.show()
① 定义一个多模态函数。
② 使用模拟退火(SA)解决问题。
③ 打印结果。
让我们考虑图 A.3 中显示的 TSP 实例。在这个 TSP 中,一位旅行推销员必须从一个特定的城市开始访问 20 个主要的美国城市。

图 A.3 20 个主要美国城市的旅行商问题(TSP)
以下列表 A.3 的后续部分展示了使用 scikit-opt 解决此问题的步骤。
import numpy as np
import matplotlib.pyplot as plt
from sko.PSO import PSO_TSP
num_points = len(city_names) ①
points_coordinate = city_names ①
pairwise_distances = distances ①
def cal_total_distance(routine): ②
num_points, = routine.shape
return sum([pairwise_distances[routine[i % num_points], routine[(i + 1)
➥ % num_points]] for i in range(num_points)])
pso_tsp = PSO_TSP(func=cal_total_distance, n_dim=num_points, size_pop=200,
➥ max_iter=800, w=0.8, c1=0.1, c2=0.1) ③
best_points, best_distance = pso_tsp.run() ③
best_points_ = np.concatenate([best_points, [best_points[0]]])
print('best_distance', best_distance) ④
print('route', best_points_) ④
① 定义 TSP 问题。
② 计算总距离。
③ 使用 PSO(粒子群优化)解决问题。
④ 打印解决方案。
A.4.3 NetworkX
在上一节中介绍的 NetworkX 提供了图属性的近似和优化启发式方法。这些启发式算法的例子包括模拟退火。以下列表 A.3 的后续部分展示了使用 NetworkX 中实现的模拟退火解决 TSP 的步骤。
#!pip install networkx
import matplotlib.pyplot as plt
import networkx as nx
from networkx.algorithms import approximation as approx
G=nx.Graph() ①
for i in range(len(city_names)): ②
for j in range(1,len(city_names)): ②
G.add_weighted_edges_from({(city_names[i], city_names[j], ②
➥ distances[i][j])}) ②
G.remove_edges_from(nx.selfloop_edges(G)) ②
pos = nx.spring_layout(G) ③
cycle = approx.simulated_annealing_tsp(G, "greedy", source=city_names[0]) ④
edge_list = list(nx.utils.pairwise(cycle)) ④
cost = sum(G[n][nbr]["weight"] for n, nbr in nx.utils.pairwise(cycle)) ④
print("The route of the salesman is:", cycle, "with cost of ", cost) ⑤
① 创建一个图。
② 向图中添加加权边,并移除自环边。
③ 使用 Fruchterman-Reingold 力导向算法定义位置为位置的字典。
④ 使用模拟退火解决 TSP。
⑤ 打印路线和成本。
A.4.4 Python 中的分布式进化算法(DEAP)
DEAP是一个用于在 Python 中实现遗传算法的库。它提供了定义、训练和评估遗传算法模型以及可视化优化过程的工具。DEAP 支持多种遗传算法技术,包括选择、交叉和变异。以下列表 A.3 的后续部分展示了使用 DEAP 中实现的模拟退火解决 TSP 的步骤:
#!pip install deap
from deap import base, creator, tools, algorithms
import random
import numpy as np
creator.create("FitnessMin", base.Fitness, weights=(-1.0,)) ①
creator.create("Individual", list, fitness=creator.FitnessMin) ①
toolbox = base.Toolbox() ②
toolbox.register("permutation", random.sample, range(len(city_names)), ②
➥ len(city_names)) ②
toolbox.register("individual", tools.initIterate, creator.Individual, ②
➥ toolbox.permutation) ②
toolbox.register("population", tools.initRepeat, list, toolbox.individual) ②
def eval_tsp(individual): ③
total_distance = 0
for i in range(len(individual)):
city_1 = individual[i]
city_2 = individual[(i + 1) % len(individual)]
total_distance += distances[city_1][city_2]
return total_distance,
toolbox.register("evaluate", eval_tsp) ④
toolbox.register("mate", tools.cxOrdered) ⑤
toolbox.register("mutate", tools.mutShuffleIndexes, indpb=0.05) ⑥
toolbox.register("select", tools.selTournament, tournsize=3) ⑦
pop = toolbox.population(n=50) ⑧
hof = tools.HallOfFame(1) ⑨
stats = tools.Statistics(lambda ind: ind.fitness.values)
stats.register("avg", np.mean)
stats.register("min", np.min)
stats.register("max", np.max)
pop, log = algorithms.eaSimple(pop, toolbox, cxpb=0.5, mutpb=0.2, ngen=50,
➥ stats=stats, halloffame=hof,verbose=True) ⑩
best_individual = hof[0]
print("Best solution:") ⑪
print(" - Fitness: ", eval_tsp(best_individual)) ⑪
print(" - Route: ", [city_names[i] for i in best_individual]) ⑪
① 创建一个最小化总距离的适应度函数。
② 创建遗传算子函数。
③ 计算路线长度。
④ 设置评估函数。
⑤ 设置有序交叉。
⑥ 设置以 0.05 的概率进行洗牌变异。
⑦ 在三个随机选择的个体中选出最佳个体。
⑧ 设置种群大小。
⑨ 设置名人堂以选择在进化过程中种群中曾经存活过的最佳个体。
⑩ 使用简单的进化算法解决问题。
⑪ 打印解决方案。
DEAP 包含几个内置算法,如遗传算法(GA)、进化策略(ES)、遗传编程(GA)、估计分布算法(EDA)和粒子群优化(PSO)。
A.4.5 OR-Tools
如前所述,OR-Tools(运筹学工具)是由 Google 开发的开源优化和约束编程库。以下列表 A.3 的后续部分展示了使用 OR-Tools 中实现的禁忌搜索解决 TSP 的步骤:
#!pip install --upgrade --user ortools
import numpy as np
import matplotlib.pyplot as plt
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2
distances2=np.asarray(distances, dtype = 'int') ①
data = {} ②
data['distance_matrix'] = distances ②
data['num_vehicles'] = 1 ②
data['depot'] = 0 ②
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
➥ data['num_vehicles'], data['depot']) ③
routing = pywrapcp.RoutingModel(manager) ③
def distance_callback(from_index, to_index): ④
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return data['distance_matrix'][from_node][to_node]
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.TABU_SEARCH) ⑤
search_parameters.time_limit.seconds = 30
search_parameters.log_search = True
def print_solution(manager, routing, solution): ⑥
print('Objective: {} meters'.format(solution.ObjectiveValue()))
index = routing.Start(0)
plan_output = 'Route for vehicle 0:\n'
route_distance = 0
while not routing.IsEnd(index):
plan_output += ' {} ->'.format(manager.IndexToNode(index))
previous_index = index
index = solution.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(previous_index,
➥ index, 0)
plan_output += ' {}\n'.format(manager.IndexToNode(index))
print(plan_output)
plan_output += 'Route distance: {}meters\n'.format(route_distance)
solution = routing.SolveWithParameters(search_parameters)
if solution:
print_solution(manager, routing, solution)
① 将浮点数组转换为整数数组以供 OR_Tools 使用。
② 定义问题数据。
③ 定义约束规划求解器。
④ 获取城市之间的距离。
⑤ 设置禁忌搜索作为局部搜索元启发式算法。
⑥ 打印解决方案。
OR-Tools 库也实现了某些元启发式算法,但不如 DEAP 等专门的元启发式框架多。例如,包括模拟退火(SA)、禁忌搜索(TS)和引导局部搜索(GLS)。
A.4.6 其他库
以下是非详尽的列表,列出了使用元启发式方法解决优化问题的其他有用库和工具:
-
simanneal—这是一个开源的 Python 模块,用于模拟退火。附录 A.3 展示了使用 simanneal 中实现的模拟退火解决 TSP 问题的步骤。
-
非支配排序遗传算法 (NSGA-II)—这是一个稳健的多目标算法,广泛应用于许多现实世界的应用。该算法旨在找到一组解决方案,称为帕累托前沿,它代表了多个冲突目标之间的权衡。NSGA-II 在 pymoo 和 DEAP 中都有实现。
-
Python 遗传算法 (PyGAD)—这是一个用于在 Python 中实现遗传算法和差分演化的库。它提供了定义、训练和评估遗传算法模型以及可视化优化过程的工具。PyGAD 支持多种遗传算法技术,包括选择、交叉和变异。
-
Python 进化算法库 (LEAP)—这是一个简单易用的通用进化算法(EA)包。它为定义和运行 EA 提供了高级抽象。
-
Pyevolve—这是一个提供简单且灵活 API 的 Python 库,用于实现和运行遗传算法。
-
遗传算法变得简单 (EasyGA)—这是另一个具有几个内置遗传算子的 Python 遗传算法库,如选择、交叉和变异。EasyGA 和 Pyevolve 是功能较少、预定义问题较少的简单库,与 DEAP 和 Pymoo 等其他库相比。
-
MEAPLY—这是一个用于种群元启发式算法的 Python 库。
-
swarmlib—这个库实现了几个群优化算法,并可视化它们的(中间)解决方案。
-
Hive—这是一个基于蜜蜂智能觅食行为的群优化算法。Hive 实现了人工蜂群(ABC)算法。
-
Pants—这是一个 Python3 实现的蚁群优化(ACO)元启发式方法。
-
mlrose—这个库提供了爬山法、随机爬山法、模拟退火和遗传算法的实现。
-
混合整数分布式蚁群优化 (MIDACO)—这是一个基于蚁群算法的数值高性能库,用于解决单目标和多目标优化问题。
-
cuOpt:这是由 NVIDIA 提供的 Python SDK 和云服务,它提供了访问基于元启发式计算复杂车辆路径问题的 GPU 加速物流求解器的权限,这些问题具有广泛的约束条件。
“列表 A.3_Metaheuristics_libraries.ipynb” 包含在本书的 GitHub 存储库中,展示了如何使用这些元启发式库的一些方法。
A.5 机器学习库
机器学习可以用于解决离散优化问题,其中 ML 模型被训练以直接从输入输出解决方案,通常表示为图。为了训练模型,问题图需要首先使用图嵌入/表示学习方法转换为特征向量。可以用于图嵌入和解决优化问题的 Python 库有多个。以下小节将介绍一些常用的库。
A.5.1 node2vec
node2vec 是一个用于学习图节点低维表示的算法框架。对于任何图,它都可以学习节点的连续特征表示,这些表示可以用于各种下游 ML 任务。
要安装 node2vec,请使用以下命令:
$ pip install node2vec
或者,您可以通过从 GitHub 克隆存储库并运行 setup.py 文件来安装 node2vec:
$ git clone https://github.com/aditya-grover/node2vec
$ cd node2vec
$ pip install -e .
以下代码演示了如何使用 node2vec 根据 Zachary 的空手道俱乐部数据集学习图节点的低维表示。这是一个在网络安全分析和基于图的机器学习算法中常用的基于图的数据库。它代表了一个包含 34 个空手道俱乐部成员之间关系信息的社会网络。它由 Wayne W. Zachary 在 1977 年撰写的论文“An Information Flow Model for Conflict and Fission in Small Groups”中创建并首次描述,并自那时起成为评估基于图的机器学习算法的流行基准数据集。
列表 A.4 node2vec 示例
import networkx as nx
from node2vec import Node2Vec
G = nx.karate_club_graph() ①
node2vec = Node2Vec(G, dimensions=64, walk_length=30, num_walks=200,
➥ workers=4) ②
model = node2vec.fit(window=10, min_count=1, batch_words=4) ③
representations_all = model.wv.vectors ④
representations_specific = model.wv['1'] ⑤
print(representations_specifi) ⑥
① 创建一个示例图
② 创建 Node2Vec 类的实例。
③ 学习表示。
④ 获取所有节点的表示。
⑤ 获取特定节点的表示。
⑥ 打印特定节点的表示。
您可以使用 t-SNE 等降维技术将生成的低维表示可视化,将表示投影到 2D 或 3D 空间,然后使用 Matplotlib 等可视化库来绘制该空间中的节点。t-distributed stochastic neighbor embedding (t-SNE) 是一种统计方法,通过在 2D 或 3D 地图中为每个数据点指定一个位置来可视化高维数据。以下是一个示例:
from sklearn.manifold import TSNE ①
import matplotlib.pyplot as plt
tsne = TSNE(n_components=2, learning_rate='auto', init='random',
➥ perplexity=3) ②
reduced_representations = tsne.fit_transform(representations_all) ②
plt.scatter(reduced_representations[:, 0], reduced_representations[:, 1]) ③
plt.show() ③
① 导入所需的库。
② 执行 t-SNE 降维。
③ 绘制节点。
运行此代码将在图 A.4 中给出可视化。

图 A.4 基于 t-SNE 的 node2vec 生成的低维表示的可视化
A.5.2 DeepWalk
DeepWalk 是一种基于表示学习的图嵌入随机游走方法。对于这个例子,我们将使用由Karate Club库提供的 DeepWalk 模块。这个库是 NetworkX 的无监督机器学习扩展。要使用 DeepWalk,你可以按照以下方式安装 Karate Club:
$ pip install karateclub
列表 A.4 的以下部分说明了如何使用 DeepWalk:
from karateclub import DeepWalk, Node2Vec ①
from sklearn.decomposition import PCA ②
import networkx as nx
import matplotlib.pyplot as plt
G=nx.karate_club_graph() ③
model=DeepWalk(dimensions=128, walk_length=100) ④
model.fit(G) ④
embedding=model.get_embedding() ⑤
officer=[] ⑥
mr=[] ⑥
for i in G.nodes: ⑥
t=G.nodes[i]['club']
officer.append(True if t=='Officer' else False)
mr.append(False if t=='Officer' else True)
nodes=list(range(len(G)))
X=embedding[nodes]
pca=PCA(n_components=2) ⑦
pca_out=pca.fit_transform(X) ⑦
plt.figure(figsize=(15, 10)) ⑧
plt.scatter(pca_out[:,0][officer],pca_out[:,1][officer]) ⑧
plt.scatter(pca_out[:,0][mr],pca_out[:,1][mr]) ⑧
plt.show() ⑧
① 导入 DeepWalk。node2vec 也是可用的。
② 导入主成分分析(PCA)。
③ 创建 Karate Club 图。
④ 定义 DeepWalk 模式,并拟合图。
⑤ 图嵌入。
⑥ 获取每个节点的俱乐部会员属性。
⑦ 定义 DeepWalk 模式,并拟合图。
⑧ 可视化嵌入。
A.5.3 PyG
PyG(PyTorch Geometric)是一个使用 PyTorch 深度学习框架在 Python 中实现图神经网络的库。它提供了定义、训练和评估图神经网络(GNN)模型以及可视化优化过程的工具。PyG 支持多种 GNN 架构,包括图卷积网络(GCN)和图注意力网络(GATs)。
你可以按照以下方式安装 PyG:
$pip install torch-scatter torch-sparse torch-cluster torch-spline-conv torch-geometric -f https://data.pyg.org/whl/torch-1.13.0+cpu.html
列表 A.4 的以下部分展示了如何使用 PyG 通过 GCN 生成 Karate Club 图嵌入:
import networkx as nx
import matplotlib.pyplot as plt
import torch
from torch_geometric.datasets import KarateClub
from torch_geometric.utils import to_networkx
from torch.nn import Linear
from torch_geometric.nn import GCNConv
dataset = KarateClub() ①
data = dataset[0]
class GCN(torch.nn.Module): ②
def __init__(self):
super().__init__()
torch.manual_seed(1234)
self.conv1 = GCNConv(dataset.num_features, 4)
self.conv2 = GCNConv(4, 4)
self.conv3 = GCNConv(4, 2)
self.classifier = Linear(2, dataset.num_classes)
def forward(self, x, edge_index):
h = self.conv1(x, edge_index)
h = h.tanh()
h = self.conv2(h, edge_index)
h = h.tanh()
h = self.conv3(h, edge_index)
h = h.tanh() ③
out = self.classifier(h) ④
return out, h
model = GCN() ⑤
criterion = torch.nn.CrossEntropyLoss() ⑥
optimizer = torch.optim.Adam(model.parameters(), lr=0.01) ⑦
optimizer.zero_grad()
for epoch in range(401):
out, h = model(data.x, data.edge_index) ⑧
loss = criterion(out[data.train_mask], data.y[data.train_mask]) ⑨
loss.backward() ⑩
optimizer.step() ⑪
h = h.detach().cpu().numpy() ⑫
plt.figure(figsize=(15, 10)) ⑬
plt.scatter(h[:, 0], h[:, 1], s=140, c=data.y, cmap="Set2") ⑬
① 使用 Karate Club 数据集。
② 图卷积网络类
应用一个最终的(线性)分类器。
④ 最终 GNN 嵌入空间
⑤ 定义模型。
⑥ 定义损失准则。
⑦ 定义一个优化器并清除梯度。
⑧ 执行单次前向传递。
仅根据训练节点计算损失。
⑩ 求导梯度。
⑪ 根据梯度更新参数。
⑫ 将张量格式的'h'转换为 numpy 格式以进行可视化。
⑬ 可视化嵌入。
PyG 是一个支持良好的库,它提供了多种功能,例如常见的基准数据集(例如 KarateClub、CoraFull、Amazon、Reddit、Actor),图的数据处理、小批量、数据转换以及图上的学习方法(例如 node2vec、MLP、GCN、GAT、GraphSAGE、GraphUNet、DeepGCNLayer、GroupAddRev 和 MetaLayer)。
A.5.4 OpenAI Gym
OpenAI Gym是一个用于开发和比较强化学习算法的工具包。它为你提供了访问各种环境的方式,例如
-
经典控制—各种经典控制任务
-
Box2d—一个二维物理引擎
-
MuJoCo—一个可以进行详细、高效模拟接触的物理引擎
-
算法—各种算法任务,例如学习复制一个序列
-
Atari—各种 Atari 视频游戏
-
gym-maze—一个简单的二维迷宫环境,其中智能体从起始位置找到目标
Gymnasium 是 OpenAI 的 Gym 库的维护分支。以下列表 A.4 的延续说明了如何使用 OpenAI Gym:
#!pip install gym[all] ①
import gym
env = gym.make('MountainCar-v0') ②
① 安装所有包含的环境。
② 创建一个环境。
在这个简单的例子中,MountainCar-v0 有离散的动作。你也可以使用 MountainCarCoutinous-V0,它具有与汽车被推的力相对应的连续动作。完整的代码列表可在本书的 GitHub 仓库中找到,在 “列表 A.4_ML_libraries.ipynb”。
A.5.5 流
Flow 是一个用于混合自主交通的深度强化学习框架。它允许你运行基于深度强化学习的交通微观模拟控制实验。
你可以按照以下步骤安装 Flow:
$git clone https://github.com/flow-project/flow.git
$cd flow
$conda env create –f environment.yml
$conda activate flow
$python setup.py develop
在环境中安装 Flow:
$pip install –e .
Flow 可以用于研究复杂、大规模和现实的多机器人控制场景。它可以用来开发在不同类型的车辆、模型噪声和道路网络(如单车道环形轨道、多车道环形轨道、八字形网络、合并网络和交叉路口)存在的情况下优化系统级速度或其他目标的控制器。
A.5.6 其他库
以下是非详尽的其它有用机器学习库列表:
-
深度图库 (DGL)—一个用于在 Python 中实现图神经网络的库。它提供了定义、训练和评估 GNN 模型以及可视化优化过程的工具。DGL 支持多种 GNN 架构,包括 GCN 和 GAT。
-
斯坦福网络分析平台 (SNAP)—一个用于分析和操作大型、复杂网络的通用、高性能系统。SNAP 包含了网络分析的一系列算法,如中心性度量、社区检测和图生成。它特别适合大规模网络分析,并在计算机科学、物理学、生物学和社会科学等多个领域得到应用。
-
Spektral—一个基于 TensorFlow 和 Keras 的开源 Python 库,用于构建图神经网络 (GNNs)。它旨在使在研究和生产中实现 GNNs 变得容易。它提供了一个高级、用户友好的 API 用于构建 GNNs,以及用于图深度学习常见任务的预构建层和模型。该库还包括用于加载和预处理图数据以及可视化评估 GNNs 性能的实用工具。
-
Jraph(发音为“giraffe”)—一个用于处理图神经网络的轻量级库,同时也提供了用于处理图的轻量级数据结构。你可以轻松地使用这个库来构建和可视化你的图。
-
GraphNets—一个用于在 Python 中实现图神经网络(GNN)的库。它提供了定义、训练和评估 GNN 模型以及可视化优化过程的工具。GraphNets 支持多种 GNN 架构,包括 GCN 和 GAT。
-
Stable-Baselines3 (SB3)—PyTorch 中强化学习算法的一组可靠实现。
-
Vowpal Wabbit (VW)—一个专门为大规模在线学习设计的开源机器学习库。最初由 Yahoo Research 开发,目前由 Microsoft Research 开发,广泛用于解决减少和上下文赌博问题。
-
Ray RLlib—一个开源的强化学习库,为生产级、高度分布式的多智能体强化学习工作负载提供支持,同时为各种行业应用保持统一和简单的 API。
-
TF-Agents—一个用于在 TensorFlow 中开发强化学习算法的库,其中包括一系列环境、算法和用于训练强化学习智能体的工具。
-
Keras-RL—一个基于 Keras 构建的深度强化学习库。它提供了一个易于使用的接口来开发和测试强化学习算法。Keras-RL 支持多种强化学习算法,如深度 Q 网络(DQN)和演员-评论家方法。还包括用于测试强化学习算法的内置环境。
-
pyqlearning—一个 Python 库,用于实现强化学习和深度强化学习,特别是针对 Q 学习、深度 Q 网络和多智能体深度 Q 网络,可以通过退火模型如模拟退火、自适应模拟退火和量子蒙特卡洛方法进行优化。
-
Python Optimal Transport (POT)—一个开源的 Python 库,提供了解决与信号、图像处理和机器学习相关的最优传输优化问题的多个求解器。
书籍 GitHub 仓库中可用的“A.6 项目”笔记本提供了如何安装和使用这些库的一些示例。
A.6 项目
我在多伦多大学的课程“ECE1724H:智能移动的生物启发算法”,在“AI for Smart Mobility (AI4SM)”出版中心提供了大量示例项目(medium.com/ai4sm),其中包含 Python 代码实现。这些项目涵盖了本书中涉及的各种优化算法及其在智能移动领域的实际应用。其中涉及的主题包括
-
在智能驾驶中采用大型语言模型进行强化学习
-
预测交通流量
-
制定电动汽车充电站布局策略
-
优化食品配送服务
-
预测预计到达时间
-
改进交通传感器的部署
-
定制骑行路线
-
重组城市消防区域
-
共享出行定价策略
-
精细学校巴士路线
-
自行车可用性预测
-
为高效垃圾收集划区
-
提升公交车站布局
-
实施公共交通动态定价
-
优化学生交通和住宿
-
将酒店推荐与路线规划整合
-
确定公共包裹柜的理想位置
-
医疗设施的健康响应和可达性评分预测
附录 B. 基准和数据集
本附录提供了优化基本资源的概述,包括测试函数、组合优化数据集、地理空间数据和机器学习数据集。
B.1 优化测试函数
优化测试函数,也称为基准函数,是用于评估优化算法性能的数学函数。以下是一些这些测试函数的例子:
-
Ackley——这是一个广泛用于测试优化算法的函数。在其二维形式中,它以几乎平坦的外围区域和中心的大洞为特征。
-
Bohachevsky——这是一个具有碗形形状的二维单峰函数。这个函数已知是连续的、凸的、可分离的、可微的、非多模态的、非随机的和非参数的,因此基于导数的求解器可以有效地处理它。请注意,变量可以分离的函数被称为可分离函数。非随机函数不包含随机变量。非参数函数假设数据分布不能通过有限参数集来定义。
-
Bukin——这个函数有许多局部最小值,所有这些最小值都位于脊上,并且在 x[] = f(–10,1) 处有一个全局最小值 f(x[]) = 0。这个函数是连续的、凸的、不可分离的、不可微的、多模态的、非随机的和非参数的。这需要使用无导数求解器(也称为黑盒求解器)如模拟退火。
-
Gramacy & Lee——这是一个具有多个局部最小值以及局部和全局趋势的一维函数。这个函数是连续的、非凸的、可分离的、可微的、非多模态的、非随机的和非参数的。
-
Griewank 1D、2D 和 3D 函数——这些函数有许多广泛存在的局部最小值。这些函数是连续的、非凸的、可分离的、可微的、多模态的、非随机的和非参数的。
“B.1_Optimization_test_functions.ipynb”列表 在本书的 GitHub 仓库中展示了不同测试函数的示例,这些函数可以从零开始实现或从 Python 框架如 DEAP、pymoo 和 PySwarms 中检索。
B.2 组合优化基准数据集
“B.2_CO_datasets.ipynb”列表 在本书的 GitHub 仓库中提供了组合优化问题的基准数据集示例,例如以下这些:
-
旅行商问题 (TSP)——给定一组 n 个节点以及每对节点之间的距离,找到一次访问每个节点且总长度最小的环形旅行。基准数据集可在
github.com/coin-or/jorlib/tree/master/jorlib-core/src/test/resources/tspLib/tsp找到。 -
车辆路径问题(VRP)—确定一支车队为服务一组客户或位置的最优路线和调度。基准数据集可在
github.com/coin-or/jorlib/tree/master/jorlib-core/src/test/resources/tspLib/vrp和neumann.hec.ca/chairedistributique/data/找到。 -
车间作业调度(JSS)—JSS 涉及在一组机器上对一组作业进行调度,其中每个作业由多个必须在特定顺序上不同机器上处理的操作组成。目标是确定一个最优调度方案,以最小化所有作业的完工时间或总完成时间。基准数据集可在
people.brunel.ac.uk/~mastjjb/jeb/orlib/files/jobshop1.txt和people.brunel.ac.uk/~mastjjb/jeb/orlib/files/jobshop2.txt找到。 -
装配线平衡问题(ALBP)—ALBP 涉及将任务(工作元素)分配到工作站,以最小化生产线上的空闲时间,同时满足特定约束。ALBP 通常包括在开始实际装配过程之前与给定生产过程的生产单元装备和调整相关的所有任务和决策。这包括设置系统产能,包括周期时间、工作站数量和工作站设备,以及将工作内容分配给生产单元,包括任务分配和确定操作顺序。基准数据集可在
assembly-line-balancing.de/找到。 -
二次分配问题(QAP)—QAP 涉及确定一组设施到一组位置的优化分配。它在运筹学中得到广泛研究,并在设施布局设计、制造、物流和电信等各个领域有应用。基准数据集可在
mistic.heig-vd.ch/taillard/problemes.dir/qap.dir/qap.html找到。 -
背包问题—给定一个包含n个物品的集合,每个物品i有一个重量w[i]和一个价值v[i]。你想要选择这些物品的子集,使得所选物品的总重量小于或等于给定的重量限制W,并且所选物品的总价值尽可能大。基准数据集可在
people.brunel.ac.uk/~mastjjb/jeb/orlib/mknapinfo.html找到。 -
集合覆盖问题(SCP)—给定一个包含 n 个元素的集合 U 和一个包含 m 个集合的集合 S,其并集等于集合 U,集合覆盖问题是要找到 S 的最小子集,使得这个子集仍然覆盖集合 U 中的所有元素。基准数据集可在
people.brunel.ac.uk/~mastjjb/jeb/orlib/scpinfo.html找到。 -
桶装问题—给定一个包含 n 个项目的集合,每个项目的大小为 s[i],以及一个容量为 C 的桶,问题是将每个项目分配到一个桶中,使得每个桶中项目的总大小不超过 C,并且使用的桶的数量最小化。基准数据集可在
people.brunel.ac.uk/~mastjjb/jeb/orlib/binpackinfo.html找到。
B.3 地理空间数据集
空间数据是指任何直接或间接引用特定地理位置的数据。此类数据的例子包括但不限于
-
人员、企业、资产、自然资源、新发展、服务和其他建筑基础设施的位置
-
空间分布变量,如交通、健康统计数据、人口统计和天气
-
与环境变化相关的数据—生态、海平面上升、污染、温度等
-
与协调应对紧急情况和自然灾害以及人为灾害的反应相关的数据—洪水、流行病、恐怖主义
如果你的优化问题包括地理空间数据,你可以从多个在线资源和开放数据仓库中检索这些数据。书中 GitHub 仓库的列表 B.3 (“Listing B.3_Geospatial_data.ipynb”) 和 B.4 (“Listing B.4_Geospatial_data_TBS.ipynb”) 展示了如何从开源数据源(如 OpenStreetMap (OSM)、Overpass API、Open-Elevation API 等)获取数据的示例。
B.4 机器学习数据集
神经组合优化已在各种数据集上得到应用。然而,由于这个领域通常关注解决经典的优化问题,基准数据集通常是这些问题的标准实例。除了列表 B.2 中包含的数据集(“Listing B.2_CO_datasets.ipynb”),书中 GitHub 仓库的列表 B.5 (“Listing B.5_ML_datasets.ipynb”)提供了神经组合优化数据集的示例,例如这些:
-
凸包—给定一组点,寻找或计算凸包的问题。凸包是一个几何形状,具体来说是一个多边形,它完全包围了给定的一组点。它通过优化两个不同的参数来实现这一点:它最大化形状覆盖的面积,同时最小化形状的边界或周长。数据可在本书的 GitHub 仓库中找到(在附录 B 数据文件夹中)。
-
TSP—用于训练和测试 TSP 的指针网络数据集。数据可在本书的 GitHub 仓库中找到(在附录 B 数据文件夹中)。
-
TLC 行程记录数据—黄色和绿色出租车行程记录包括捕获接车和下车日期和时间、接车和下车地点、行程距离、详细费用、费率类型、支付类型和驾驶员报告的乘客计数字段。数据可在www.nyc.gov/site/tlc/about/tlc-trip-record-data.page找到。
B.5 数据文件夹
书籍 GitHub 仓库中包含的data文件夹(github.com/Optimization-Algorithms-Book/Code-Listings/tree/main/Appendix%20B/data)包括以下示例数据:
-
ALBP—第六章中用于生产线平衡问题的数据集
-
行政边界—世界各地不同感兴趣区域的行政边界,以 geoJSON 格式提供,可用于地图可视化
-
自行车共享—包含匿名行程数据的多伦多自行车共享(TBS)乘客数据,包括行程开始日期和时间、行程结束日期和时间、行程持续时间、行程开始站点、行程结束站点和用户类型(参见附录 B.4:附录 B.4_Geospatial_data_TBS.ipynb)
-
加拿大交通事故—来自加拿大统计局 2018 年机动车碰撞数据集,包括每 10 万人死亡人数、每十亿车辆公里死亡人数、每十亿车辆公里受伤人数、每 10 万持牌驾驶员死亡人数和每 10 万持牌驾驶员受伤人数(参见附录 A.2:附录 A.2_ 图库.ipynb)
-
安大略省健康—加拿大安大略省的健康区域(参见附录 A.2:附录 A.2_ 图库.ipynb)
-
警察—多伦多警察局公共服务数据(参见附录 A.2:附录 A.2_ 图库.ipynb)
-
政治区划—第八章中使用到的政治区划数据
-
PtrNets—指针网络(第十一章)使用的凸包和 TSP 数据
-
TSP—旅行商问题实例
附录 C. 练习和解答
在本附录中,您将找到一套全面的练习及其相应的解答,按章节组织,以增强您对本书中所呈现材料的概念、理论和实际技能的理解和应用。这些练习旨在加强全书各章节中涵盖的概念、理论和实践技能。
C.1 第二章:深入探讨搜索和优化
C.1.1 练习
1. 多项选择题:为以下每个问题选择正确的答案。
1.1. _________ 是一类可以通过非确定性多项式算法解决的问题,其解决方案难以找到但易于验证。
a. P
b. NP
c. co-NP
d. NP-complete
e. NP-hard
1.2. 以下哪个基准(玩具)问题不是 NP-complete?
a. 二进制装箱
b. 背包问题
c. 最小生成树
d. 汉密尔顿回路
e. 顶点覆盖问题
1.3. _________ 是一类决策问题,其“否”答案可以在多项式时间内验证。
a. P
b. NP
c. co-NP
d. NP-complete
e. NP-hard
1.4. 以下哪个现实世界问题是 NP-hard?
a. 图像匹配
b. 单机调度
c. 组合等价性检查
d. 容量车辆路径问题(CVRP)
e. 容器/卡车装载
1.5. _________ 是一种理论,它专注于根据资源使用情况对计算问题进行分类,并将这些类别相互关联。
a. 优化复杂度
b. 时间复杂度
c. 计算复杂度
d. 运筹学
e. 决策复杂度
2. 用决策变量(单变量,双变量,多变量);目标函数(单目标,多目标,无目标函数,或约束满足问题);约束;(硬约束,软约束,硬软约束,无约束);以及线性(线性规划(LP),二次规划(QP),非线性规划(NLP))来描述以下搜索和优化问题。
a. 最小化 y + cos(x²), sin(x) – x × y,以及 1 / (x + y)²
b. 在 –3 ≤ x < 10 的约束下最大化 2 – e^((1 –) *x*)
c. 在 –2 ≤ x < 3, 0 < y ≤ 3, 和 x + y = 4 的约束下最大化 3 × x – y / 5
d. 学校区域划分问题包括确定在给定区域内学校委员会所属的每个学校的学生群体,以最大化学校区域连续性,同时考虑一系列硬约束,如每个年级的学校容量和班级容量。在这个问题中,步行可达性和保持学生每年在同一个学校被认为是软约束。
e. 背包问题是一个组合问题示例,其解决方案形式为组合,其中顺序不重要。如图 C.1 所示,给定一组物品,每个物品都有效用和重量,任务是选择物品的子集以最大化总效用,同时确保所选物品的总重量不超过预定义的容量。是否包含或排除每个物品的决定是二元的。

图 C.1 每个物品都有效用和重量,我们希望最大化背包内容的效用。该问题受背包容量的限制。
- 对于以下优化问题,根据解决问题的允许时间和预期解决方案的质量,基于问题类型(设计、规划或控制问题)提出适当的算法(离线或在线)来处理优化问题。
a. 找到最佳的风电场设计,其中需要根据风条件和风电场面积选择和放置风力涡轮机的数量和类型。
b. 找到多个车辆路线,这些路线从不同的仓库开始和结束,以便满足所有客户需求。
c. 为跑步者和骑自行车者创建一个健身助手,无缝自动化规划健身活动所涉及的多项任务。规划者将评估运动员当前的健身水平和个人训练目标,以便制定健身计划。规划者还将生成并推荐既受欢迎又符合用户目标、水平和预定时间的地理路线,从而减少规划阶段所涉及到的挑战。建议的健身计划将根据每个用户向健身目标进步的情况持续调整,从而保持运动员的挑战性和动力。
d. 在给定每班航班的需求和收入、路线信息(距离、时间)、运营限制、飞机特性和运营成本以及运营和管理约束的情况下,找到一组航班,其出发和到达时间以及飞机分配最大化利润。
e. 找到交付货物的自行车、半自动和全自动最后一英里配送卡车、自动驾驶配送机器人或配送无人机最优调度方案,以最大化客户满意度并最小化配送成本,同时考虑车辆容量、配送服务类型(几天配送、次日配送或加额外费用的当日配送)、配送时间、投放地点等因素。
f. 在疫情期间,计划按需响应式公共交通,以支持必要工作人员的运输以及公众(尤其是老年人)前往药店和杂货店的必要行程,同时考虑商店营业时间、容量和在线配送选项。
g. 在一组障碍物中,为车辆从起始位置到给定的目标位置找到一条无碰撞路径,以最小化估计的到达时间和消耗的能量。
h. 开发一个行程规划器,以最小化总通勤时间,最大化景点平均评分,最大化在每个景点停留的时间,并在有人访问城市时有效地最小化闲置时间。
i. 找到校车装载模式和日程安排,使得路线数量最小化,所有校车行驶的总距离保持在最低,没有校车超载,并且穿越任何路线所需的时间不超过最大时间政策。
j. 最小化共享移动公司(最小化无乘客驾驶的里程)或配送服务提供商(最小化无货物驾驶的里程)的空驶。
k. 规划或重新规划交通走廊和城市街道,以容纳更多的行人、自行车和共享交通的骑行者,以及更少的汽车。
l. 找到公交车站、交通传感器、微移动站、电动汽车充电站、空中出租车起飞和降落位置、步行路线和自行车道的最佳位置,以促进活跃的移动性。
4. 修改列表 2.6,使用 Python 字典定义动物饲料混合问题数据,或从 CSV 文件中读取问题数据。
C.1.2 解决方案
1. 多项选择题
1.1. b) NP
1.2. c) 最小生成树
1.3. c) co-NP
1.4. d) 容量车辆路径问题 (CVRP)
1.5. c) 计算复杂性
2. 优化问题描述
a. 二元,多目标,无约束,非线性规划
b. 单变量,单目标,硬约束,非线性规划
c. 二元,单目标,硬约束,线性规划
d. 多变量,单目标,既有硬约束又有软约束,线性规划(参见 Jacques A. Ferland 和 Gilles Guénette,“学校划区问题的决策支持系统” [1])
e. 二元,单目标,硬约束,线性规划
3. 优化问题和过程
a. 设计问题。离线优化。
b. 在生成路线过程中的规划问题,以及在重新路由过程中的控制问题。规划阶段的离线优化,以及重新路由阶段的在线优化。
c. 生成计划的规划问题,以及适应的控制问题。规划阶段的离线优化,以及适应阶段的在线优化。
d. 生成飞行计划的工程设计问题,以及在需要适应的情况下(例如,飞机故障或因天气条件取消)的规划问题。离线优化,以及在需要适应的情况下进行在线优化。
e. 在路径生成过程中的规划问题,以及在自适应运动规划中的重新路由过程中的控制问题。在线优化。
f. 调度问题的规划问题,以及涉及重新路由的控制问题。规划阶段的在线优化。
g. 规划问题以生成计划,控制问题以适应。在线优化。
h. 规划问题。离线优化。
i. 设计问题。离线优化。
j. 规划问题。在线优化。
k. 设计问题。离线优化。
l. 设计问题。离线优化。
4. 下一个列表显示了使用 Python 字典定义动物饲料混合问题数据或从 CSV 文件读取问题数据的步骤。
列表 C.1 动物饲料混合问题——使用字典定义数据
import pandas as pd
from pulp import *
Ingredients = ["Limestone", "Corn", "Soybean meal"] ①
Price = {"Limestone": 10.0, "Corn": 30.5, "Soybean meal": 90.0,} ②
Calcium = {"Limestone": 0.38, "Corn": 0.001, "Soybean meal": 0.002,} ③
Protein = {"Limestone": 0.0, "Corn": 0.09, "Soybean meal": 0.50,} ④
Fiber = {"Limestone": 0.0, "Corn": 0.02, "Soybean meal": 0.08,}
model = LpProblem("Animal_Feed_Mix_Problem", LpMinimize) ⑤
ingredient_vars = LpVariable.dicts("Ingr", Ingredients, 0) ⑥
model += (lpSum([Price[i] * ingredient_vars[i] for i in
➥ Ingredients]),"Total Cost of Ingredients per kg",) ⑦
model += (lpSum([Calcium[i] * ingredient_vars[i] for i in Ingredients]) >=
➥ 0.008, "Minimum calcium",) ⑧
model += (lpSum([Calcium[i] * ingredient_vars[i] for i in Ingredients]) <= ⑧
➥ 0.012, "Maximum calcium",) ⑧
model += (lpSum([Protein[i] * ingredient_vars[i] for i in Ingredients]) ⑧
➥ >=0.22, "Minimum protein",) ⑧
model += (lpSum([Fiber[i] * ingredient_vars[i] for i in Ingredients]) ⑧
➥ <=0.05, "MMaximum fiber",) ⑧
model += lpSum([ingredient_vars[i] for i in Ingredients]) == 1, ⑧
➥ "Conservation" ⑧
model.solve() ⑨
for v in model.variables(): ⑩
print(v.name, '=', round(v.varValue,2)*100, '%') ⑩
⑩
print('Total cost of the mixture per kg = ', round(value(model.objective), ⑩
➥ 2), '$') ⑩
① 创建一个成分列表。
② 单位成本(美分/kg)词典。
③ 钙(kg/kg)词典。
④ 蛋白质(kg/kg)词典。
⑤ 创建模型。
⑥ 创建名为'ingredient_vars'的字典以包含引用的变量。
⑦ 添加目标函数。
⑧ 添加五个约束条件。
⑨ 使用 PuLP 的求解器选择解决问题。
⑩ 打印结果。
我们还可以如下从 CSV 文件中读取问题数据。
列表 C.2 动物饲料混合问题——从 CSV 文件读取问题数据
df = pd.read_csv('Blending_problem_data.csv') ①
data = df.to_dict() ②
model = LpProblem("Animal_Feed_Mix_Problem", LpMinimize) ③
ingredient_vars = LpVariable.dicts("Ingr", data.get('Ingredients'), 0) ④
model += (lpSum([data.get('Price')[i] * ingredient_vars[i] for i in
➥ data.get('Ingredients')]),"Total Cost of Ingredients per kg",) ⑤
model += lpSum([ingredient_vars[i] for i in data.get('Ingredients')]) == 1,
➥ "Conservation" ⑥
model += (lpSum([data.get('Calcium')[i] * ingredient_vars[i] for i in
➥ data.get('Ingredients')]) >= 0.008, "Minimum calcium",) ⑥
model += (lpSum([data.get('Calcium')[i] * ingredient_vars[i] for i in
➥ data.get('Ingredients')]) <= 0.012, "Maximum calcium",) ⑥
model += (lpSum([data.get('Protein')[i] * ingredient_vars[i] for i in
➥ data.get('Ingredients')]) >=0.22, "Minimum protein",) ⑥
model += (lpSum([data.get('Fiber')[i] * ingredient_vars[i] for i in
➥ data.get('Ingredients')]) <=0.05, "MMaximum fiber",) ⑥
model.solve() ⑦
for v in model.variables(): ⑧
print(v.name, '=', round(v.varValue,2)*100, '%') ⑧
⑧
print('Total cost of the mixture per kg = ', round(value(model.objective),
➥ 2), '$') ⑧
① 读取 CSV 文件。
② 将数据帧转换为字典。
③ 创建模型。
④ 创建名为'ingredient_vars'的字典以包含引用的变量。
⑤ 添加目标函数。
⑥ 添加五个约束条件。
⑦ 使用 PuLP 的求解器选择解决问题。
⑧ 打印结果。
运行列表 C.2 产生以下结果:
Ingr_Corn = 65.0 %
Ingr_Limestone = 3.0 %
Ingr_Soybean_meal = 32.0 %
Total cost of the mixture per kg = 49.16 $
C.2 第三章:盲搜索算法
C.2.1 练习
1. 多项选择题和真/假题:为以下每个问题选择正确的答案。
1.1. 大 O 符号特别描述了当参数趋向于特定值或无穷大时函数的极限行为(最坏情况),通常用更简单的函数来表示。这个表达式的 big O 是什么:nlog(n) + log(2n)?
a. 线性对数
b. 对数线性
c. 准线性
d. 所有上述选项
1.2. 哪个盲搜索算法实现了状态搜索的栈操作?
a. 广度优先搜索(BFS)
b. 均匀代价搜索(UCS)
c. 双向搜索(BS)
d. 深度优先搜索(DFS)
e. 以上皆非
1.3. 树是一个无环无自环的连通图。
a. 正确
b. 错误
1.4. 在一个非常大的工作空间中,目标位于工作空间的深处,节点数量可能会呈指数级增长,深度优先搜索将需要非常大的内存需求。
a. 正确
b. 错误
1.5. 最佳优先搜索是一种混合深度和广度优先搜索,它使用启发式值并扩展最期望的未扩展节点。
a. 正确
b. 错误
1.6. 在设计问题或战略函数中,通常为了速度的提升而牺牲最优性。
a. 正确
b. 错误
1.7. 在图中边的权重都相等的应用中,图遍历算法优于最短路径算法。
a. 正确
b. 错误
1.8. 在 Dijkstra 算法中,优先队列使用哪种数据结构实现?
a. 栈
b. 队列
c. 堆
d. 数组
1.9. 广度优先搜索何时是最佳选择?
a. 当节点较少时
b. 当所有步骤成本相等时
c. 当所有步骤成本不相等时
d. 以上都不是
1.10. 哪个盲搜索算法通过增加深度限制直到达到目标,结合了 DFS 的空间效率和 BFS 的快速搜索?
a. 深度限制搜索(DLS)
b. 迭代加深搜索(IDS)
c. 均匀代价搜索(UCS)
d. 双向搜索(BS)
e. 以上都不是
1.11. 哪个术语描述了一个计算复杂度为 O(n logn) 的算法?
a. 对数
b. 指数
c. 准线性
d. 以上都不是
1.12. 哪个搜索算法使用一个空的前进先出队列实现?
a. 深度优先搜索
b. 广度优先搜索
c. 双向搜索
d. 以上都不是
- 考虑图 C.2 中显示的简化地图,其中边标有城市之间的实际距离。说明 BFS 和 DFS 产生的从城市 A 到城市 M 的路径。

图 C.2 一个简化的地图
- 找出以下函数的大 O 表示法:
a. 10n + nlog(n)
b. 4 + n/5
c. n⁵ – 20n³ + 170n + 208
d. n + 10log(n)
- 考虑图 C.3 中的搜索空间,其中 S 是起始节点,G1 和 G2 是目标节点。边标有成本函数的值;数字给出穿越弧的成本。每个节点上方是启发函数的值;数字给出到目标距离的估计。假设在存在选择时,无信息搜索算法总是首先选择左分支。对于深度优先搜索(DFS)和广度优先搜索(BFS)策略
a. 指出首先达到的目标状态(如果有)。
b. 按顺序列出从 OPEN 列表中弹出的所有状态。

图 C.3 一个图搜索练习
- 解决图 C.4 中的填字游戏。

图 C.4 盲搜索填字游戏
横向
-
一个具有预定深度限制的深度优先搜索
-
一个盲搜索算法,用于解决加权图的单源最短路径问题,其中边的成本为非负
-
一个结合正向和反向搜索的搜索算法
-
一个图遍历算法,首先探索通过根节点相邻的节点,然后是下一个相邻的节点,直到找到解决方案或达到死胡同
-
一个适用于大型图的 Dijkstra 算法的变体
-
一个比线性复杂度略快的函数
-
一个图中可能有多条边连接相同的顶点对
-
后进先出(LIFO)数据结构
向下
-
一个搜索算法,通过增加深度限制直到达到目标,结合了 DFS 的空间效率和 BFS 的快速搜索
-
Twitter 用来表示关注的图
3. 当树很深时,优先选择的图遍历搜索算法
4. 一种图的泛化,其中泛化边可以连接任意数量的节点
5. LinkedIn 用于表示用户、群组、未注册人员、帖子、技能和工作的图形类型
6. 用于描述算法性能或复杂性的符号
9. 通过遵循某个定义良好的规则访问节点来探索树或图形结构的过程
12. 一种先进先出(FIFO)数据结构
提示:如果答案由两个或多个单词组成,则必须使用空格或破折号。
C.2.2 解决方案
1. 多选题和判断题
1.1. d) 所有这些
1.2. d) 深度优先搜索 (DFS)
1.3. a) 正确
1.4. b) 错误
1.5. a) 正确
1.6. b) 错误
1.7. a) 正确
1.8. c) 堆
1.9. b) 当所有步骤成本相等时
1.10. b) 迭代加深搜索(IDS)
1.11. c) 近线性
1.12. b) 广度优先搜索
2. 从 A 到 M 通过 BFS 得到的路径是
A→B→H→C→E→I→L→D→F→J→K→M→H→G→J→K→M:
追踪回溯,最终路径是 A→H→L→M。
从 A 到 M 通过 DFS 得到的路径是 A→B→H→C→E→F→G→J→K→I→M。
3. 以下是一些解决方案:
a. O(nlogn)
b. O(n)
c. O(n⁵)
d. O(logn)
4. 以下是一些解决方案:
| 搜索策略 | a | b |
|---|---|---|
| 深度优先搜索 (DFS)假设节点根据它们的字典顺序扩展(即,在 S 的子节点中,A 首先扩展,因为在字典上,它排在 B 和 C 之前)。 | G2 | S, A, D, H, J, G2 |
| 广度优先搜索 (BFS)队列的状态演变如下[S],[S,A,B,C],[S,A,B,C,D,H],[S,A,B,C,D,H,G1]注意:由于 H 已经被 A 访问过,当 B 访问它时,它不会被再次推入队列。然而,它被访问了两次。 | G1 | S, A, B, C, D, H, H, G1 |
5. 跨字谜的解决方案显示在图 C.5 中。

图 C.5 盲搜索字谜解决方案
C.3 第四章:启发式搜索算法
C.3.1 练习
1. 多选题和判断题:为以下每个问题选择正确的答案。
1.1 如果我们决定在图 C.6 中合并节点 E,我们需要在增强图中添加多少条捷径?

图 C.6 合并节点 E
a. 0
b. 1
c. 2
d. 3
1.2 A*算法是以下哪种算法的特殊版本?
a. 广度优先搜索
b. 深度优先搜索
c. 爬山法
d. 最佳优先搜索
e. Dijkstra 算法
1.3 以下哪项不是爬山算法的变体?
a. 复杂的爬山法
b. 最陡上升爬山法
c. 随机重启爬山法
d. 最陡上升爬山法
e. 所有这些都是爬山算法的变体。
1.4 如果f(n)是通过n到目标路径的评估函数(成本),而h(n)是从n到目标的估计成本,例如从n到目标的目标距离,那么贪婪最佳优先搜索的启发式函数是什么?
a. f(n) ≠ h(n)
b. f(n) < h(n)
c. f(n) = h(n)
d. f(n) > h(n)
1.5 在图 C.7 中的有向加权图中,如果我们收缩白色节点,需要多少条捷径?

图 C.7 有向图
a. 0
b. 1
c. 2
d. 3
e. 4
1.6 使用特定问题知识的搜索策略被称为
a. 启发式搜索
b. 最佳优先搜索
c. A*搜索
d. 启发式搜索
e. 以上所有
1.7 以下哪个算法用于解决最小生成树(MST)问题?
a. 克鲁斯卡尔
b. 博鲁夫卡
c. 焦点-普里姆
d. 查泽勒
e. 以上所有
1.8 梯度上升是一种信息丰富的广度优先搜索,对内存和计算开销要求不高。
a. 正确
b. 错误
1.9 CH 的节点排序方法包括
a. 边差异
b. 迭代更新
c. 收缩邻居的数量
d. 捷径覆盖
e. 以上所有
1.10 如果h(n)是可接受的启发式,即h(n)从不高估到达目标成本,那么 A*是最佳的。
a. 正确
b. 错误
1.11 边差异是在收缩节点时引入的捷径数量减去进入节点的入边数量。
a. 正确
b. 错误
1.12 最佳优先搜索是一种混合深度和广度优先搜索,它使用启发式值并扩展最理想的未扩展节点。
a. 正确
b. 错误
1.13 A*搜索中的评估函数是什么?
a. 从当前节点到目标节点的估计成本
b. 通过当前节点到目标节点的路径成本
c. 通过当前节点到目标节点的路径成本和从当前节点到目标节点的估计成本之和
d. 通过当前节点到目标节点的路径成本和从当前节点到目标节点的估计成本的平均值
e. 以上都不是
1.14 当h(n)一致时,哪种搜索是完整的且最优的?
a. 最佳优先搜索
b. 深度优先搜索
c. 最佳优先搜索和深度优先搜索
d. A*搜索
1.15 在收缩层次(CH)算法中,我们根据以下哪个因素收缩节点?
a. 重要性递减顺序
b. 重要性递增顺序
1.16 A*搜索算法通过结合从给定状态到达目标的启发式成本估计来尝试减少探索的总状态数。
a. 正确
b. 错误
1.17 梯度上升算法是一种局部贪婪搜索算法,它试图提高广度优先搜索的效率。
a. 正确
b. 错误
1.18 在 CH 中,节点的重要性可能在收缩过程中发生变化,需要重新计算其重要性。
a. 正确
b. 错误
1.19 带有与每层节点数相等的波束宽度的波束搜索与
a. 广度优先搜索
b. 深度优先搜索
c. 爬山法
d. 最佳优先搜索
e. Dijkstra 算法
1.20 在 CH 中,收缩的顺序不影响查询性能。
a. 正确
b. 错误
2. 考虑图 C.8 中的搜索空间,其中 S 是起始节点,G1 和 G2 是目标节点。边标有成本函数的值;数字给出穿越弧的成本。每个节点上方是启发式函数的值;数字给出到目标距离的估计。使用 A*搜索策略
a. 指出首先达到哪个目标状态(如果有)
b. 按顺序列出所有弹出的状态,直到找到目标状态之一

图 C.8 搜索空间
3. 在图 C.9 所示的单词搜索谜题中,找到本章中使用的隐藏术语。你可以水平搜索(从左到右或从右到左),垂直搜索(从上到下或从下到上),或对角线搜索。

图 C.9 信息搜索单词搜索谜题
C.3.2 解决方案
1. 多项选择题和判断题:
1.1 a) 0(不需要捷径,因为 A 和 D 之间存在见证路径)
1.2 d) 最佳优先搜索
1.3 a) 复杂爬山法
1.4 c) f(n) = h(n)
1.5 c) 2
1.6 e) 所有上述选项
1.7 e) 所有上述选项
1.8 b) 错误(它是深度优先搜索的一个变体。)
1.9 e) 所有上述选项
1.10 a) 正确
1.11 b) 错误(边差异是在收缩节点时引入的捷径数减去节点的总度数;即,进入节点的边数之和加上从节点发出的边数。)
1.12 a) 正确
1.13 b) 从当前节点到目标节点的路径成本
1.14 d) A*搜索
1.15 b) 按重要性递增的顺序
1.16 a) 正确
1.17 b) 错误
1.18 a) 正确
1.19 a) 广度优先搜索
1.20 b) 错误(收缩的顺序不影响 CH 的成功,但会影响预处理时间和查询时间。一些收缩顺序系统最小化在增强图中添加的捷径数,从而影响总体运行时间。)
2. 扩展的顺序基于边权重之和 g(n) 和启发式估计 h(n);即,f(n) = g(n) + h(n)。例如,从 S 开始,f(A) = 8,f(B) = 18,f(C) = 9,因此队列将看起来像[A,B,C],因为f(A) < f(C) < f(B)。
在下一步,当 A 从队列中弹出时,节点 D 和 H 将被评估,f(D) = d(S,A) + d(A,D) + h(D) = 1 + 5 + 4 = 10,f(H) = d(S,A) + d(A,H) + h(H) = 19,按照顺序将它们推入队列将导致[A,C,D,B,H]。
因此,下一个要扩展的节点将是 C,它将 E 和 F 添加到队列中,其中f(E) = 11 和f(F) = 19: [A,C,D,E,B,H,F]
因此,下一个节点是 D,这导致 f(H) = d(S,A) + d(A,D) + d(D,H) + h(H) = 1 + 5 + 2 + 6 = 14 的更新,随后将其重新定位到队列中:[A,C,D,E,H,B,F]。应遵循相同的策略,直到找到其中一个目标。
a. G1
b. S, A, C, D, E, H, B, G1
3. 单词搜索谜题的解决方案如图 C.10 所示。

图 C.10 信息搜索单词搜索谜题解决方案
方向和起点以 (方向, X, Y) 的格式表示。
A-START (E, 7, 10)
BEAM SEARCH (E, 8, 6)
BEST-FIRST (E, 8, 9)
DEPTH-FIRST (E, 7, 12)
DOWNWARD GRAPH
(E, 3, 11)
EDGE DIFFERENCE (E, 2, 4)
HAVERSINE (E, 2, 8)
HILL CLIMBING (E, 5, 1)
INFORMED SEARCH (E, 3, 3)
KRUSKAL ALGORITHM (E, 2, 7)
WITNESS PATH (S,1, 1)
C.4 第五章:模拟退火
C.4.1 练习
1. 多项选择题和判断题:为以下每个问题选择正确的答案。
1.1 与爬山法不同,模拟退火结合了概率机制,允许它接受下降步骤,受当前温度和考虑的移动质量的影响。
a. 正确
b. 错误
1.2 模拟退火是一种优化技术,它始终保证找到全局最优解。
a. 正确
b. 错误
1.3 双重退火是广义爬山法的实现。
a. 正确
b. 错误
1.4 在完全自适应的模拟退火中,使用先前接受的一组步骤和参数的随机组合来估计新的步骤和参数。
a. 正确
b. 错误
1.5 当温度降低时,模拟退火算法探索更多的搜索空间。
a. 正确
b. 错误
1.6. 哪种冷却计划在渐近收敛到全局最小值时需要可观的计算时间?
a. 线性冷却计划
b. 几何冷却计划
c. 对数冷却计划
d. 指数冷却计划
e. 非单调自适应冷却计划
1.7. 模拟退火使用热跃迁来避免陷入局部最小值,而量子退火则依赖于量子隧穿。
a. 正确
b. 错误
2. 罗森布鲁克函数,通常被称为“山谷”或“香蕉”函数,是一个非凸函数,定义为 f(x,y) = (1 – x)² + 100(y – x²)²。这是一个标准的测试函数,对于大多数传统求解器来说相当困难。
a. 使用列表 5.1 或 5.2 或从头开始实现模拟退火算法的版本,或者使用附录 A 中 A.4 节提到的库来找到该函数的全局最小值。
b. 这个香蕉函数仍然相对简单,因为它有一个弯曲的狭窄山谷。其他函数,如蛋格函数,具有强烈的多个峰值和高度非线性。以下是一个高度非线性多峰函数的例子:f(x,y) = x² + y² + 25[sin²(x) + sin²(y)]。考虑域(x,y) ∈ [–5,5] × [–5,5]。要得到一个精确到小数点后三位的最佳解,大约需要 2,500 次评估。
c. 调查模拟退火对不同冷却计划的收敛速度。
d. 对于标准 SA,冷却计划是一个单调递减的函数。我们没有理由不使用其他形式的冷却。例如,我们可以使用 T(i) = T[o] cos²(i)e–*α^i*, α > 0。修改之前步骤中实现的代码,以研究各种函数作为冷却计划的行为。
3. 修改列表 5.5,或使用 ASA-GS [2],或实现自己的模拟退火版本以对不同 TSP 数据集进行对比研究(参见附录 B 中的列表 B.2 以了解如何获取 TSP 实例)。用模拟退火得到的行程长度填写表 C.1。
表 C.1 不同 TSP 数据集的 SA 解
| 数据集 | 已知最佳解 | SA 解 | CPU 时间(秒) |
|---|---|---|---|
| 缅甸 14 乌利塞斯 22 奥利弗 30 阿特 48 艾尔 51 艾尔 75 克罗 A100d198 | 30.875875.665142033,52442653521,28215,780 |
4. 解决图 C.11 所示的 20 个主要美国城市的 TSP 问题。在这个 TSP 中,旅行商必须从一个特定的城市开始访问多个美国城市。假设以下城市,由它们的名称和 GPS 纬度和经度坐标定义:纽约市(40.72, –74.00);费城(39.95, –75.17);巴尔的摩(39.28, –76.62);夏洛特(35.23, –80.85);孟菲斯(35.12, –89.97);杰克逊维尔(30.32, –81.70);休斯顿(29.77, –95.38);奥斯汀(30.27, –97.77);圣安东尼奥(29.53, –98.47);沃斯堡(32.75, –97.33);达拉斯(32.78, –96.80);圣地亚哥(32.78, –117.15);洛杉矶(34.05, –118.25);圣何塞(37.30, –121.87);旧金山(37.78, –122.42);印第安纳波利斯(39.78, –86.15);凤凰城(33.45, –112.07);哥伦布(39.98, –82.98);芝加哥(41.88, –87.63);底特律(42.33, –83.05)。使用每个城市的 GPS 坐标将城市和生成的解决方案可视化为一个 NetworkX 图。

图 C.11 20 个最大美国城市的旅行商问题(TSP)
5. 解决图 C.12 中的填字游戏

图 C.12 模拟退火填字游戏
横向
6. 通过应用量子效应在能量景观中搜索以找到最优或近似最优解的优化过程
7. 接受或拒绝邻近解的概率
10. 一种冷却过程,在最初的迭代中温度迅速降低,但随后指数衰减的速度减慢
11. 需要指定最大迭代次数的冷却计划
12. 与此搜索算法相比,主要区别在于 SA 概率性地允许由当前温度和移动的好坏控制的向下步骤。
13. 一种需要禁用计算时间的冷却计划
向下
1. 一种随机或概率模型,描述了一系列可能的移动,其中每个移动的概率仅取决于前一个移动达到的状态
2. 一种明确考虑搜索进展情况的冷却计划
3. 一种通过冷却因子降低温度的冷却计划
4. 基于物理退火过程的优化过程
5. 用于模拟退火转换概率的概率分布
8. 一种量子力学现象,其中波函数可以通过势垒传播
9. 系统状态,其中不接受更好或更差的移动
提示:如果答案由两个单词组成,则必须使用空格和破折号。
C.4.2 解决方案
1. 多项选择题和真/假题
1.1 a) 正确
1.2 b) 错误
1.3 b) 错误(双重退火是广义模拟退火算法的一种实现。)
1.4 a) 正确
1.5 b) 错误(模拟退火算法在温度降低时利用搜索空间。)
1.6 c) 对数冷却计划
1.7 a) 正确
2. 书中 GitHub 仓库提供的 __problem_base.py 和 _sa.py 的通用形式可以用来解决这个问题。您还可以修改列表 5.1 或 5.2 来解决这个问题。
3. 您可以使用列表 5.5 或 ASA-GS 实现[1]在不同的 TSP 实例上运行具有选定参数的模拟退火,并报告您的结果。在列表 5.5 中,将永久链接替换为对应于 TSP 实例的链接。例如,点击 burma14.tsp,点击右上角的三个点按钮,并选择复制永久链接。考虑调整算法的参数,以接近每个数据集已知的最佳行程长度(到目前为止)。
4. 以下列表显示了如何使用 optalgo-tools 包中实现的一般求解器来解决这个问题。
列表 C.3 使用 SA 解决 TSP 问题
from optalgotools.algorithms import SimulatedAnnealing
from optalgotools.problems import TSP
pairwise_distances = distances
tsp_US = TSP(dists=pairwise_distances, gen_method='insert',
➥ init_method='greedy')
sa=SimulatedAnnealing(max_iter=10000, max_iter_per_temp=10000,
➥ initial_temp=10000000, final_temp=0.0001,
➥ cooling_schedule='geometric', cooling_alpha=0.9, debug=1)
sa.run(tsp_US)
print(sa.s_best)
作为列表 C.3 的延续,以下代码展示了如何使用 simanneal Python 库来解决这个问题:
#!pip install simanneal
import math
import random
from simanneal import Annealer
class TravellingSalesmanProblem(Annealer): ①
def __init__(self, state, distance_matrix):
self.distance_matrix = distance_matrix
super(TravellingSalesmanProblem, self).__init__(state)
def move(self): ②
initial_energy = self.energy()
a = random.randint(0, len(self.state) - 1)
b = random.randint(0, len(self.state) - 1)
self.state[a], self.state[b] = self.state[b], self.state[a]
return self.energy() - initial_energy
def energy(self): ③
e = 0
for i in range(len(self.state)):
e += self.distance_matrix[self.state[i-1]][self.state[i]]
return e
init_state = list(cities) ④
random.shuffle(init_state) ④
tsp = TravellingSalesmanProblem(init_state, distance_matrix)
tsp.set_schedule(tsp.auto(minutes=0.2))
tsp.copy_strategy = "slice"
state, e = tsp.anneal()
while state[0] != 'New York City': ⑤
state = state[1:] + state[:1]
print("%i mile route:" % e) ⑥
print(" ➞ ".join(state)) ⑥
① 使用 simanneal 模块中的 TSP 测试退火器。
② 在路线上交换两个城市。
③ 计算路线的长度。
④ 初始状态,一个随机排序的行程
⑤ 将纽约市设为家乡城市。
⑥ 打印路线及其成本。
作为延续,以下代码可以用来可视化问题和解决方案:
fig, ax = plt.subplots(figsize=(15,10))
reversed_dict = {key: value[::-1] for key, value in cities.items()} ①
H = G.copy() ②
edge_list = list(nx.utils.pairwise(state))
nx.draw_networkx_edges(H, pos=reversed_dict, edge_color="gray", width=0.5) ③
ax=nx.draw_networkx(H, pos=reversed_dict, with_labels=True, edgelist=edge_list,
edge_color="red", node_size=200, width=3,) ④
plt.show() ⑤
① 反转经纬度以正确可视化。
② 创建一个独立的浅拷贝的图和属性。
③ 仅在每个节点上绘制最近的边。
④ 绘制路线。
⑤ 可视化。
运行此代码将产生图 C.13 所示的可视化。

图 C.13 使用 simanneal 解决 20 个主要美国城市 TSP 的解决方案
附录 A 展示了可用于解决此问题的 Python 库示例,例如 scikit-opt、DEAP、OR-Tools 和 simanneal,它们可以使用模拟退火和其他元启发式算法来解决这个问题。
5. 填字游戏的解决方案如图 C.14 所示。

图 C.14 模拟退火填字游戏解决方案
C.5 第六章:禁忌搜索
C.5.1 练习
1. 多项选择和判断题:为以下每个问题选择正确的答案。
1.1. 在 TS 中,为了逃离局部最优,非改进解在条件上被接受。
a. 正确
b. 错误
1.2. 基于频率的记忆维护有关搜索点最近访问的信息。
a. 正确
b. 错误
1.3. 为了提高解决某些问题的效率,TS 通过禁忌列表使用记忆来避免重新访问最近访问过的邻域。
a. 正确
b. 错误
1.4. 终止 TS 的停止标准可以是
a. 邻域为空
b. 迭代次数大于指定的阈值
c. 证据表明已获得最优解
d. 以上皆对(a, b, 和 c)
e. 以上皆非(a, b, 或 c)
1.5. 禁忌搜索的禁忌移动被存储在长期记忆中。
a. 正确
b. 错误
1.6. 对于大型和困难的问题(如调度、二次分配和车辆路径),禁忌搜索通常能获得代表全局最优或近似最优的解。
a. 正确
b. 错误
1.7. 使用近期记忆通过记住具有良好解决方案的邻居来增加搜索的强度。
a. 正确
b. 错误
1.8. 使用渴望标准来撤销禁忌搜索的禁忌移动,作为避免搜索停滞的一种方式。
a. 正确
b. 错误
1.9. TS 可以被视为随机搜索和记忆结构的组合。
a. 正确
b. 错误
1.10. 当禁忌列表的长度太短时,算法可能会陷入循环,而当它太长时,每次迭代可能会阻止许多移动,导致停滞。
a. 正确
b. 错误
2. 如附录 B 所示,Schwefel 函数复杂,具有许多局部最小值。图 C.15 展示了该函数的二维形式。

图 C.15 Schwefel 函数
将列表 6.1 修改为使用禁忌搜索解决 Schwefel 函数。
3. 苹果申请了美国专利 7790637 B2,该专利是一种复合材料层压板,包括七层叠加在一起,并有一个隔网层。将浸渍有树脂(例如,一种粘稠液体物质)的七层平面纤维放置在不同的方向上,以提高复合材料层的强度,如图 C.16 所示。隔网层是装饰层,它是一种不同的材料,并粘附在外部,以改善装饰外观和一致性。

图 C.16 复合层压板设计——苹果的美国专利 7790637 B2
七层排列的方式导致复合层压板具有不同的强度水平。假设成对强度增益或损失由图 C.17 中的经验值给出。正值表示两层接触时(向上或向下)的强度增益,负值表示强度损失。

图 C.17 成对强度增益或损失
假设我们想要找到七个纤维层最优的有序组合(即排列),以最大化复合层压板的强度。进行四次禁忌搜索的手动迭代,以展示算法解决问题的步骤。编写 Python 代码来解决这个问题。
4. 编写 Python 代码使用禁忌搜索来解决简单装配线平衡问题,类型 1(SALBP-1),其中机器和工人约束由 Kamarudin 和 Rashid 的论文“Modelling of Simple Assembly Line Balancing Problem Type 1 (SALBP-1) with Machine and Worker Constraints” [3] 描述。
5. 在图 C.18 的单词搜索谜题中,找到禁忌搜索中常用的隐藏术语。你可以水平搜索(从左到右或从右到左),垂直搜索(从上到下或从下到上),或对角线搜索。

图 C.18 TS 单词搜索谜题
C.5.2 解决方案
1. 多项选择题和判断题
1.1. a) 正确
1.2. b) 错误(基于最近性的记忆维护有关搜索点最近访问的信息。)
1.3. a) 正确
1.4. d) 所有上述选项(a、b 和 c)
1.5. b) 错误(禁忌活跃移动存储在短期记忆中。)
1.6. a) 正确
1.7. a) 正确
1.8. a) 正确
1.9. b) 错误(禁忌搜索可以被视为局部搜索和记忆结构的组合。)
1.10. a) 正确
2. 书籍 GitHub 仓库中的列表 C.4 展示了如何使用禁忌搜索解决 Schwefel 函数。
3. 图 C.19 显示了复合层压板问题的 TS 初始化和第一次迭代。不重复的可能排列数为n!。排列七个纤维层的可能解数为 7! = 5,040。要生成相邻解,可以使用纤维层交换。邻域定义为通过交换解中任意两个层而获得的任何其他解。如果我们让节点数n = 7,成对交换k = 2。邻域的数量是不重复的组合数C(n,k)或n-choose-k:C(n,k) = n! / (k!(n – k)!) = 21 个邻域。假设禁忌任期设置为 3 次迭代。

图 C.19 复合层压板设计的禁忌搜索初始化
图 C.20 至 C.22 分别显示了迭代 1、2 和 3。

图 C.20 复合层压板设计——迭代 1

图 C.21 复合材料层压板设计——第 2 次迭代

图 C.22 复合材料层压板设计——第 3 次迭代
在每次迭代中,我们通过层交换生成多个候选解,并选择导致最大 delta 值的移动,即前一个解和新的解在强度增益方面的差异。在下一个迭代中,如图 C.23 所示,没有具有正增益的移动,因此最佳(非禁忌)移动将是非改进的。选择移动(a,e),因为它与其他禁忌活跃的最佳移动(a,b)相比只有一期的禁忌期限。这意味着(a,e)的禁忌状态可以通过应用渴望标准来覆盖。

图 C.23 复合材料层压板设计——TS 迭代 4 和渴望标准
为了展示基于频率的记忆的多样化,假设在第 26 次迭代后达到的解是[a c f b g e d],强度值为 23。假设我们将根据其在使用中的频率来惩罚解(高度重复的解会受到更多的惩罚)。禁忌结构根据基于最近性(上三角)和基于频率(下三角)的记忆进行更新,如图 C.24a 所示。交换后的前五个候选解如图 C.24b 所示。

图 C.24 a) 基于最近性和频率的记忆,b) 交换后的前五个候选解及其惩罚值
当不存在可接受的改进步骤时,我们需要进行多样化。非改进步骤通过分配更大的惩罚给更频繁的交换来受到惩罚。根据惩罚值,选择交换(a,f)。列表 C.5 展示了使用禁忌搜索解决复合材料层压板问题的解决方案。
列表 C.5 使用禁忌搜索解决复合材料层压板问题
from optalgotools.problems import SHEETS ①
from optalgotools.algorithms import TabuSearch ②
import matplotlib.pyplot as plt
iphone_case = SHEETS(init_method='greedy') ③
ts = TabuSearch(max_iter=1000, tabu_tenure=10, neighbor_size=7,
➥ use_aspiration=True, aspiration_limit=3, use_longterm=False, debug=1,
➥ maximize=True, penalize=True) ④
ts.init_ts(iphone_case) ⑤
ts.val_cur
ts.run(iphone_case, repetition=1) ⑥
ts.val_allbest
① 导入 SHEETS 问题实例,其中包含复合材料层压板设计问题的描述。
② 导入禁忌搜索求解器。
③ 创建不同的 sheets 对象。有关支持的参数的完整列表,请参阅 optalgotools.problems 模块中 sheets.py 文件中的 SHEETS 类。
④ 创建一个 TS 对象以帮助解决复合材料层压板问题。
⑤ 获取一个初始随机解,并检查其成本。
⑥ 运行 TS,评估最佳解的成本。
- 列表 C.6 展示了使用禁忌搜索解决 SALBP-1 问题的代码片段。
列表 C.6 使用禁忌搜索解决 SALBP-1 问题
tasks = pd.DataFrame(columns=['Task', 'Duration']) ①
tasks= pd.read_csv("https://raw.githubusercontent.com/Optimization- ①
Algorithms-Book/Code-Listings/main/Appendix%20B/data/ALBP/ALB_TS_DATA2.txt", ①
sep =",") ①
Prec= pd.read_csv("https://raw.githubusercontent.com/Optimization-
➥Algorithms-Book/Code- ①
➥Listings/main/Appendix%20B/data/ALBP/ALB_TS_PRECEDENCE2.txt", sep =",") ①
Prec.columns=['TASK', 'IMMEDIATE_PRECEDESSOR'] ①
Cycle_time = 10 ②
tenure = 3
max_itr=100
solution = Initial_Solution(len(tasks)) ③
soln_init = Make_Solution_Feasible(solution, Prec) ④
sol_best, SI_best=tabu_search(max_itr, soln_init, SI_init, tenure, WS,
➥ tasks, Prec_Matrix, Cycle_time) ⑤
Smoothing_index(sol_best, WS, tasks, Cycle_time, True) ⑥
plt = Make_Solution_to_plot(sol_best, WS, tasks, Cycle_time) ⑦
plt.show() ⑦
① 直接使用 URL 从附录 B 读取数据。
② 定义周期时间。
③ 获取一个初始解。
④ 确保解决方案在考虑任务优先级约束下的可行性。
⑤ 运行禁忌搜索。
⑥ 计算最佳解的平滑指数。
⑦ 可视化解决方案。
运行完整代码产生以下输出:
The Smoothing Index value for ['T2', 'T7', 'T6', 'T1', 'T3', 'T4', 'T8',
➥ 'T9', 'T5'] solution sequence is: 1.0801234497346432
The number of workstations for ['T2', 'T7', 'T6', 'T1', 'T3', 'T4', 'T8',
➥ 'T9', 'T5'] solution sequence is: 6
The workloads of workstation for ['T2', 'T7', 'T6', 'T1', 'T3', 'T4', 'T8',
➥ 'T9', 'T5'] solution sequence are: [7\. 6\. 5\. 7\. 6\. 6.]
1.0801234497346432
完整代码可在本书的 GitHub 仓库中找到。
5. 单词搜索谜题的解决方案如图 C.25 所示。

图 C.25 TS 单词搜索谜题解决方案
方向和起点以 (方向, X, Y) 格式表示
适应性记忆 (W, 17, 13)
憧憬标准 (W, 23, 15)
多样化 (W, 23, 14)
基于频率的记忆 (W, 21, 7)
强化 (E, 8, 8)
局部搜索 (W, 16, 6)
长期记忆 (E, 1, 10)
近期记忆 (E, 12, 1)
反应性探索 (W, 23, 16)
感觉记忆 (W, 16, 3)
短期记忆 (W, 16, 4)
禁忌列表 (E, 6, 12)
禁忌搜索 (E, 1, 5)
禁忌结构 (W, 23, 2)
禁忌任期 (E, 12, 5)
工作记忆 (W, 20, 9)
C.6 第七章:遗传算法
C.6.1 练习
1. 多项选择和真/假:为以下每个问题选择正确答案。
1.1. 给定一个二进制字符串 1101001100101101 和另一个二进制字符串 yxyyxyxxyyyxyxxy,其中值 0 和 1 由 x 和 y 表示,在随机选择的重组点应用单点交叉后,这两个字符串会产生什么样的后代?
a. yxxyyyxyxxy11010 和 yxyyx01100101101
b. 11010yxxyyyxyxxy 和 yxyyx01100101101
c. 11010yxxyyyxyxxy 和 01100101101yxyyx
d. 以上皆非
1.2. 在二进制遗传算法中,位突变操作符是如何工作的?
a. 它交换两个随机选择的位的位置。
b. 它平均两个随机选择的位的值。
c. 它翻转二进制表示中的随机选择的位。
d. 它反转随机选择的位段顺序。
1.3. 每个基因在染色体上的位置名称称为
a. 等位基因
b. 突变位点
c. 基因型
d. 表型
e. 以上皆非
1.4. 在遗传算法的稳态模型中,如何将新的后代引入种群?
a. 通过替换整个种群
b. 通过替换种群的一小部分
c. 通过将它们添加到现有种群中
d. 通过替换种群中的最差个体
1.5. 假设你有一个表 C.2 中所示的种群。
表 C.2 给定种群
| 个人 | 个人 1 | 个人 2 | 个人 3 | 个人 4 | 个人 5 |
|---|---|---|---|---|---|
| 适应性 | 12 | 25 | 8 | 53 | 10 |
基于排名的选择试图通过基于相对适应性而不是绝对适应性来解决问题,以解决适应性比例选择(FPS)的问题。假设排名过程是线性排名,如下所示:

其中 N 是种群中个体的数量,r 是与种群中每个个体关联的排名(最不适应的个体有 r = 1,最适应的个体 r = N)。SP 是选择压力(假设 SP = 1.5)。如果我们使用基于线性排名的选择,哪两个个体将被选中?
a. 个人 1 和 2
b. 个人 1 和 3
c. 个人 2 和 3
d. 个人 2 和 4
e. 个人 3 和 4
f. 以上皆非
1.6. 在遗传算法中,哪种选择方法涉及随机选择固定数量的个体,并从中选择最佳者?
a. 轮盘赌选择
b. 排名选择
c. 征服选择
d. 随机均匀抽样(SUS)
1.7. 在 P 元启发式算法中,拉丁超立方策略的计算成本与伪随机初始化策略相同。
a. 正确
b. 错误
1.8. 在二进制遗传算法中,使用哪种类型的染色体编码?
a. 实数值
b. 排列
c. 二进制
d. 树
1.9. 以下哪种方法可以用于将优化问题中的最小化问题转换为最大化问题?
a. 将常数加到目标函数上
b. 取目标函数的倒数
c. 取反目标函数
d. 将目标函数按因子缩放
1.10. 与代际模型相比,稳态模型在遗传算法中的优势是什么?
a. 更快的收敛速度
b. 更好的多样性保持
c. 更低的计算成本
d. 更鲁棒的突变算子
1.11. 在二进制遗传算法中,突变算子对个体的基因做了什么?
a. 反转基因值(1 到 0 或 0 到 1)
b. 随机分配新的基因值(0 或 1)
c. 交换两个基因的位置
d. 结合来自不同个体的基因
1.12. 以下哪项是二进制遗传算法中常用的交叉方法?
a. 单点交叉
b. 双点交叉
c. 均匀交叉
d. 以上所有
1.13. 遗传算法中,代际模型与稳态模型之间的主要区别是什么?
a. 选择方法
b. 交叉算子
c. 突变算子
d. 替换策略
1.14. 在二进制遗传算法中,哪种交叉方法涉及根据每个基因预定义的概率在父代染色体之间交换遗传物质?
a. 单点交叉
b. 双点交叉
c. 均匀交叉
d. 算术交叉
1.15. 使用高突变率在二进制遗传算法中可能存在的潜在缺点是什么?
a. 种群多样性的丧失
b. 过早收敛
c. 优秀解决方案的破坏
d. 减少选择压力
1.16. 在遗传算法的代际模型中,每一代种群会发生什么变化?
a. 替换一小部分种群。
b. 完全替换整个种群。
c. 种群保持不变。
d. 种群大小逐渐减少。
1.17. 当使用转换将最小化问题转换为最大化问题时,必须保留最优解的哪个属性?
a. 可行性
b. 最优性
c. 领先
d. 凸性
1.18. 遗传算法中主要使用的算子有哪些?
a. 初始化、池化和反向传播
b. 选择、交叉和突变
c. 卷积、池化和激活
d. 前向传播、反向传播和优化
1.19. 与稳态模型相比,以下哪项是遗传算法中代际模型的优势?
b. 改善多样性保持
b. 收敛更快
c. 较低的计算成本
d. 更好地处理约束
1.20. 假设你需要使用二进制遗传算法来解决以下函数最大化问题:

其中
• O[i] 是个体 i 的目标函数值,且 O[i] = –(x – 6.4)²。
• N 是种群大小。
• V 是一个很大的值,以确保适应度值非负。
V 的值是适应度函数 f(x) 第二项的最大值,使得对应目标函数最大值的适应度值为 0。为了以 0.1 的精度表示解,需要多少位?
a. 6 位
b. 7 位
c. 8 位
d. 9 位
e. 10 位
f. 以上皆非
2. Ackley 函数是一个非线性、多模态函数,具有大量局部最小值,使其成为一个具有挑战性的优化问题。Ackley 函数的一般形式是

其中
• x = (x[1], x[2], ..., x[d]) 是输入向量。
• a, b, 和 c 是正常数(通常 a = 20, b = 0.2, 和 c = 2 × π).
• d 是输入向量的维度。
函数在原点有全局最小值(x[i] = 0),其中 f(x) = 0,并且它被几个局部最小值所包围,如图 C.26 所示。

图 C.26 Ackley 函数
这些局部最小值的存在使得优化算法难以找到全局最小值,尤其是那些可能陷入局部最小值的算法。请编写 Python 代码来解决一个 6D Ackley 函数。
C.6.2 解决方案
1. 多项选择题和是非题
1.1. b) 11010yxxyyyxyxxy and yxyyx01100101101
1.2. c) 随机翻转二进制表示中的某个位
1.3. b) 突变位点
1.4. b) 通过替换种群的一小部分
1.5. 考虑以下线性排名

| 个体 | 个体 1 | 个体 2 | 个体 3 | 个体 4 | 个体 5 |
|---|---|---|---|---|---|
| 适应度 | 12 | 25 | 8 | 53 | 10 |
| 排名 r | 3 | 4 | 1 | 5 | 2 |
| p(r) | 0.175 | 0.2 | 0.125 | 0.225 | 0.15 |
个体 4 和个体 2 将被选中,因为它们具有最高的概率。所以正确答案是 d。
1.6. c) 赛选
1.7. a) 正确
1.8. c) 二进制
1.9. b 和 c) 取目标函数的倒数,并取目标函数的相反数
1.10. c) 较低的计算成本
1.11. a) 反转基因值(1 到 0 或 0 到 1)
1.12. d) 所有上述选项
1.13. d) 替换策略
1.14. c) 均匀交叉
1.15. c) 破坏好的解
1.16. b) 替换整个种群
1.17. b) 最优性
1.18. b) 选择、交叉和变异
1.19. a) 改进的多样性保持
1.20. d) 9 位
给定 0.0 <= x <= 31.5,精度为 0.1
• 计算范围大小:31.5 – 0 = 31.5
• 将范围大小除以所需的精度:31.5 / 0.1 = 315
• 向上取整到最接近的整数:315
现在您有 315 个步骤(值)来表示从 0.0 到 31.5 的数字,精度为 0.1:
number_of_bits = ceil(log2) = ceil(log2) ≈ ceil(8.29) = 9
因此,您需要 9 位来表示从 0.0 到 31.5 的数字,精度为 0.1.2. 列表 C.8 显示了使用从头开始实现或基于 pymoo 开源库实现的遗传算法解决 Ackley 函数的代码片段。我们首先定义遗传算法和 Ackley 函数的常量如下:
列表 C.8 使用 GA 解决 Ackley 函数
import random
import math
POPULATION_SIZE = 100
GENOME_LENGTH = 30
MUTATION_RATE = 0.1
CROSSOVER_RATE = 0.8
GENERATIONS = 100
a = 20
b = 0.2
c = 2 * math.pi
我们随后定义decode函数,将二进制基因组转换为实数值:
def decode(genome):
x = []
for i in range(0, GENOME_LENGTH, 5):
binary_string = "".join([str(bit) for bit in genome[i:i+5]])
x.append(int(binary_string, 2) / 31 * 32 - 16)
return x
以下函数定义了适应度函数(Ackley 函数):
def fitness(genome):
x = decode(genome)
term1 = -a * math.exp(-b * math.sqrt(sum([(xi ** 2) for xi in x]) /
➥ len(x)))
term2 = -math.exp(sum([math.cos(c * xi) for xi in x]) / len(x))
return term1 + term2 + a + math.exp(1)
现在让我们定义交叉函数:
def crossover(parent1, parent2):
child1 = []
child2 = []
for i in range(GENOME_LENGTH):
if random.random() < CROSSOVER_RATE:
child1.append(parent2[i])
child2.append(parent1[i])
else:
child1.append(parent1[i])
child2.append(parent2[i])
return child1, child2
变异函数定义如下:
def mutate(genome):
for i in range(GENOME_LENGTH):
if random.random() < MUTATION_RATE:
genome[i] = 1 - genome[i]
现在是创建初始种群的时候了:
population = [[random.randint(0, 1) for _ in range(GENOME_LENGTH)] for _ in
➥ range(POPULATION_SIZE)]
现在让我们运行遗传算法:
for generation in range(GENERATIONS):
fitness_scores = [(genome, fitness(genome)) for genome in population] ①
fitness_scores.sort(key=lambda x: x[1])
print(f"Generation {generation}: Best fitness =
➥ {fitness_scores[0][1]}") ②
parents = [fitness_scores[i][0] for i in range(POPULATION_SIZE // 2)] ③
next_generation = [] ④
for i in range(POPULATION_SIZE // 2): ④
parent1 = random.choice(parents) ④
parent2 = random.choice(parents) ④
child1, child2 = crossover(parent1, parent2) ④
mutate(child1) ④
mutate(child2) ④
next_generation.append(child1) ④
next_generation.append(child2) ④
population = next_generation ⑤
① 计算种群中每个基因组的适应度分数。
② 打印出每一代的最佳适应度分数。
③ 选择种群中最佳的一半作为下一代的父母。
④ 通过应用交叉和变异生成下一代。
⑤ 用新的一代后代替换当前种群。
您可以按照以下方式打印出最佳基因组及其适应度:
fitness_scores = [(genome, fitness(genome)) for genome in population]
fitness_scores.sort(key=lambda x: x[1])
print(f"Generation {GENERATIONS}: Best fitness = {fitness_scores[0][1]}")
您可以打印出最佳基因组,解码最佳基因组,然后按照以下方式打印出决策变量的实数值:
print(f"Best genome = {fitness_scores[0][0]}")
print(f"Best genome decoded = {decode(fitness_scores[0][0])}")
print(f"Decision variables in real values = {decode(fitness_scores[0][0])}")
您可以使用 pymoo 中实现的遗传算法更快地解决这个问题:
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.problems import get_problem
from pymoo.optimize import minimize
problem = get_problem("ackley", n_var=6)
algorithm = GA(
pop_size=100,
eliminate_duplicates=True)
res = minimize(problem,
algorithm,
seed=1,
verbose=False)
print("Best solution found: \nX = %s\nF = %s" % (res.X, res.F))
运行此代码会产生以下输出:
Best solution found:
X = [ 0.00145857 -0.00011553 0.00033902 -0.00169267 -0.0005825 -0.00547546]
F = [0.01003609]
C.7 第八章:遗传算法变体
C.7.1 练习
1. 假设一家公司正在尝试为新设施选择最佳位置。决策变量 x[1] 和 x[2] 代表新设施在二维平面上的潜在位置坐标。公司需要最小化新设施与位于(2, 1)位置现有设施之间的距离平方,这可能代表了降低两个设施之间运输成本的需求。此外,公司旨在最大化新设施与位于(3, 4)位置的竞争对手设施之间的距离平方,这可能代表了通过将新设施与竞争对手保持距离来建立竞争优势的需求。由于分区法规或其他限制,新设施必须位于一定区域内。它还必须遵守环境限制或财产边界。这个多目标约束优化问题可以表述如下:
最小化 f1 = √((x[1] – 2)² + (x[2] – 1)²)(公司设施之间的距离)
最大化 f1 = √((x[1] – 3)² + (x[2] – 4)²)(新设施与竞争对手设施之间的距离)
such that
g1 = 2x[1] + x[2] – 6 ≤ 0(分区法规)
g2 = 2x[1] – x[2] – 2 ≤ 0(环境约束或属性边界)
0 ≤ x[1] ≤ 5(边界约束)
0 ≤ x[2] ≤ 5(边界约束)
编写 Python 代码以找到新设施的最佳位置。
-
全 1 问题,也称为最大 1 问题,是一个简单的目标是在固定长度的二进制字符串中最大化 1 的数量的问题。对于 10 位全 1 问题,最优解具有以下形式:[1111111111]。该问题由 K. Sutner 在他的文章“线性细胞自动机和伊甸园”中描述,如下:假设一个 n × n 国际象棋棋盘的每个方格都配备了一个指示灯和一个按钮。如果按下某个方格的按钮,该方格的灯将从关闭变为开启或反之亦然;相邻边界的所有灯也会发生同样的变化。最初所有灯都是关闭的。全 1 问题的目标是找到一个按钮按下的序列,使得最终所有灯都开启。这个问题本身可能没有直接的应用,但它被用作测试问题来比较各种算法的性能。编写 Python 代码以使用遗传算法找到 10 位全 1 问题的解决方案。
-
假设图 C.27 中有 10 个包裹需要装载到载货自行车上。

图 C.27 10 个物品的载货自行车装载问题,给定重量、利润和效率(利润/重量)
每个包裹都有自己的重量、利润和效率值(每公斤利润)。目标是选择要装载的包裹,以便使效用函数 f[1] 最大化,而重量函数 f[2] 最小化。
f[1] = ΣE[i], i = 0, 1, ..., n.
f[2] = |Σw[i] – C|,如果 Σw[i] > C,则添加 50。
where
• n 是包裹的总数。
• E[i] 是包裹 i 的效率。
• w[i] 是包裹 i 的重量。
• C 是自行车的最大容量。
如果添加的包裹总重量超过最大容量,则将添加 50 的惩罚。编写 Python 代码以确定哪些物品应装载以实现最大效率。
- 在 Guéret 等人在其著作《线性规划》[5]中描述的开采式采矿问题中,露天铀矿被划分为用于开采的块。如图 C.28 所示,有 18 个 10,000 吨的块分布在三个层面上,这些块是基于测试钻探的结果进行识别的。为了使卡车能够到达底部,需要将坑道梯田化,并且坑道在西部被一个村庄限制,在东部被一群山脉限制。由于坡度限制,提取一个块需要提取其上方三个层面的三个块:直接在其上方的块,以及右侧和左侧的块。根据块的水平不同,提取块的成本也不同。第 1 层面的块每吨提取成本为 100 美元,第 2 层面的块每吨提取成本为 200 美元,第 3 层面的块每吨提取成本为 300 美元。然而,由非常坚硬且富含石英的岩石构成的网状块,每吨提取成本为 1,000 美元。含有铀的块以灰色阴影显示:这些是块 0、6、9、11、16 和 17。这些块有不同的市场价值,其中块 17 富含矿石,但由与其他网状块相同的坚硬岩石制成。块 0、6、9、11、16 和 17 的市场价值分别为每吨 200 美元、300 美元、500 美元、200 美元、1,000 美元和 1,200 美元。

图 C.28 开采式采矿问题
a. 编写一个 Python 代码,以确定提取哪些块以最大化总利润,总利润由Σ(VALUE – COST) × x[i]给出,其中x = 0,1,…,17,x[i]是一个分配的二进制变量。
b. 假设处理块所需的时间长度取决于块的水平以及块的硬度,为[1, 1, 1, 1, 1, 1, 1, 1, 3, 2, 2, 2, 2, 3, 4, 4, 3, 4]。编写 Python 代码以确定提取哪些块以最大化折扣利润和增加的成本,由以下方程给出:

- 在书籍 GitHub 仓库的第八章/项目文件夹中,提供了以下样本研究项目以供审查和实验:
• 路由——使用遗传算法在多伦多北部的市政区 Vaughan 中寻找两个感兴趣点之间的最短路径。
• 公交车路线——解决学校公交车路线问题,将问题表述为一个包含多目标优化问题的方案。应用聚类优先、路线优先方案、遗传算法和自适应遗传算法来解决这个问题。使用美国弗吉尼亚州温彻斯特市的公立学校真实数据评估这些算法的性能。
• 位置分配——使用生物启发式优化算法处理无人机配送站的放置问题。解决方案框架包括两个阶段:第一阶段解决站点的位置规划问题,而第二阶段处理分配给已定位站点的配送需求。
C.7.2 解决方案
- 下一个列表显示了如何使用 NSGA-II 解决设施分配问题。
列表 C.9 使用 NSGA-II 解决设施分配问题
import numpy as np
import math
import matplotlib.pyplot as plt
from pymoo.core.problem import ElementwiseProblem
from pymoo.algorithms.moo.nsga2 import NSGA2
from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.operators.sampling.rnd import FloatRandomSampling
class FacilityProblem(ElementwiseProblem):
def __init__(self):
super().__init__(n_var=2,
n_obj=2,
n_ieq_constr=2,
xl=np.array([0,0]),
xu=np.array([5,5]))
def _evaluate(self, x, out, *args, **kwargs):
f1 = math.sqrt((x[0]-2)**2 + (x[1]-1)**2)
f2 = -math.sqrt((x[0]-3)**2 + (x[1]-4)**2)
g1 = x[0]+2*x[1]-6 ①
g2 = 2*x[0]-x[1]-2 ②
out["F"] = [f1, f2]
out["G"] = [g1, g2]
problem = FacilityProblem()
algorithm = NSGA2(
pop_size=50,
sampling=FloatRandomSampling(),
crossover=SBX(prob=0.8),
mutation = PolynomialMutation(prob=0.6, repair=RoundingRepair()),
eliminate_duplicates=True
)
from pymoo.termination import get_termination
termination = get_termination("n_gen", 50)
from pymoo.optimize import minimize
res = minimize(problem,
algorithm,
termination,
seed=1,
save_history=True,
verbose=False)
X = res.X
F = res.F
① 缩放约束
② 环境/属性约束
书籍 GitHub 仓库中的完整列表还显示了如何执行决策以选择最佳解决方案。
- 下一个列表显示了所有 1 问题的解决方案。
列表 C.10 使用 GA 解决所有 1 的问题
import numpy as np
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.crossover.sbx import SBX
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
class AllOnes(Problem):
def __init__(self, n_var):
super().__init__(n_var=n_var, n_obj=1, n_constr=0, xl=0, xu=1,
➥ vtype=int)
def _evaluate(self, x, out, *args, **kwargs):
out["F"] = -np.sum(x, axis=1)
problem = AllOnes(n_var=10)
algorithm = GA(
pop_size=100,
sampling=FloatRandomSampling(),
crossover=SBX(prob=1.0, eta=30, n_offsprings=2),
mutation=PolynomialMutation(prob=1.0),
eliminate_duplicates=True
)
res = minimize(problem, algorithm, ('n_gen', 100), seed=1, verbose=True)
print("Sum of the ones in the solution:", -res.F)
print("Solution: ", res.X)
此代码定义了一个扩展 pymoo 中Problem类的AllOnes类。AllOnes类的_evaluate方法通过计算二进制字符串中 1 的数量并返回该计数的负值(因为 pymoo 最小化目标函数)来计算个体的适应度。
- 下一个列表显示了货物自行车装载问题的解决方案。
列表 C.11 使用 GA 解决货物自行车装载问题
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.crossover.ux import UniformCrossover
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.termination.default import DefaultSingleObjectiveTermination
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
class CargoBike(Problem): ①
def __init__(self, weights, efficiency, capacity):
super().__init__(n_var=len(weights), n_obj=1, n_constr=1, xl=0,
➥ xu=1, vtype=bool)
self.weights = weights
self.efficiency = efficiency
self.capacity = capacity
def _evaluate(self, x, out, *args, **kwargs):
x = np.round(x) # Ensure X is binary
total_weight = np.sum(self.weights * x, axis=1)
total_profit = np.sum(self.efficiency * x, axis=1)
out["F"] = -total_profit[:, None]
out["G"] = np.where(total_weight <= self.capacity, 0, total_weight
➥ - self.capacity)[:, None]
weights = np.array([14.6, 20, 8.5, 10, 13, 9.6, 4.9, 16.5, 8.77, 7.8]) ②
profits = np.array([14.54, 15.26, 5.8, 12.12, 8.2, 7.4, 10.3, 13.5, 6.6,
➥ 20.9]) ②
efficiency = np.array([1.00, 0.76, 0.68, 1.21, 0.63, 0.77, 2.1, 0.82, 0.75,
➥ 2.68]) ②
capacity = 100 ②
df=pd.DataFrame({'Weight (kg)':weights,'Profit ($)':profits,'Efficiency ($/ ②
Kg)':efficiency}) ②
problem = CargoBike(weights, efficiency, capacity) ③
algorithm = GA(
pop_size=200,
sampling=FloatRandomSampling(),
crossover=UniformCrossover(prob=1.0),
mutation=PolynomialMutation(prob=0.5),
eliminate_duplicates=True
) ④
termination = DefaultSingleObjectiveTermination()
res = minimize(problem, algorithm, termination, verbose=True) ⑤
print(f"Best solution found: {res.X}") ⑥
print(f"Best objective value: {-res.F[0]}") ⑥
res_bool=np.round(res.X)
selected_items = df.loc[res_bool.astype(bool), :]
fig, ax1 = plt.subplots()
ax1.bar(1+selected_items.index, selected_items['Efficiency ($/Kg)'])
ax1.set_ylabel('Efficiency ($/Kg)')
ax1.set_ylim(0, 5)
ax1.legend(['Efficiency ($/Kg)'], loc="upper left")
ax2 = ax1.twinx()
ax2.bar(1+selected_items.index, selected_items['Weight (kg)'], width=0.5,
➥ alpha=0.5, color='orange')
ax2.grid(False)
ax2.set_ylabel('Weight (kg)')
ax2.set_ylim(0, 30)
ax2.legend(['Weight (kg)'], loc="upper right")
plt.show() ⑦
① 定义问题。
② 定义问题输入。
③ 创建问题实例。
④ 定义遗传算法。
⑤ 运行优化。
⑥ 打印结果。
⑦ 可视化解决方案。
运行此代码会产生图 C.29 所示的解决方案。

图 C.29 货物自行车装载解决方案
- 下一个列表显示了解决露天采矿问题的步骤。
列表 C.12 解决露天采矿问题
import numpy as np
BLOCKS = np.arange(0, 17) ①
LEVEL23 = np.array([8, 9, 10, 11, 12, 13, 14, 15, 16, 17]) ②
COST = np.array([100, 100, 100, 100, 100, 100, 100, 100,
200, 200, 200, 200, 200, 200,
1000, 1000, 1000, 1000]) ③
VALUE = np.array([200, 0, 0, 0, 0, 0, 300, 0,
0, 500, 0, 200, 0, 0,
0, 0, 1000, 0]) ④
Precedence = np.array([[0,1, 2],
[1, 2, 3],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6],
[5, 6, 7],
[8, 9, 10],
[9, 10, 11],
[10, 11, 12],
[11, 12, 13]]) ⑤
① 矿中的块
② 2 级和 3 级中的块
③ 块的成本。
④ 块的价值
⑤ 定义 LEVEL23 中每个块的成本。
作为延续,你可以如下可视化 2 级和 3 级中块提取的前置关系:
import networkx as nx
import matplotlib.pyplot as plt
G = nx.DiGraph() ①
G.add_nodes_from(BLOCKS) ②
for b, arc in zip(LEVEL23, Precedence): ③
for predecessor in arc: ③
G.add_edge(predecessor, b) ③
pos = nx.spring_layout(G) ④
pos.update({0: (0, 0), 1: (0, 1), 2: (0, 2), 3: (1, 0), 4: (1, 1), 5: (1,
➥ 2), 6: (2, 0), 7: (2, 1),
8: (2, 2), 9: (3, 0), 10: (3, 1), 11: (3, 2), 12: (4, 0), 13:
➥ (4, 1),
14: (4, 2), 15: (5, 0), 16: (5, 1), 17: (5, 2)}) ⑤
plt.figure(figsize=(10, 5)) ⑥
nx.draw(G, pos, with_labels=True, node_size=1500, node_color='skyblue', ⑥
➥ font_size=12, font_weight='bold') ⑥
plt.title("Precedence graph for extraction of blocks in Level 2 and 3") ⑥
plt.show() ⑥
① 创建有向图。
② 添加节点。
③ 添加前置边。
④ 创建一个具有所有节点默认位置的 pos 字典。
⑤ 更新具有特定位置的节点的位置。
⑥ 绘制图表。
图 C.30 显示了在 2 级和 3 级中提取块的前置图。

图 C.30 2 级和 3 级中提取块的前置图
作为列表 C.12 的延续,以下代码片段显示了如何将问题定义为单目标约束优化问题。
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.crossover.pntx import PointCrossover
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.core.problem import Problem
from pymoo.optimize import minimize
class OpencastMiningProblem(Problem):
def __init__(self):
super().__init__(n_var=18, n_obj=1, n_constr=len(LEVEL23), xl=0,
➥ xu=1)
def _evaluate(self, X, out, *args, **kwargs):
X = np.round(X)
profits = np.sum((VALUE - COST) * X, axis=1)
constraints = np.zeros((X.shape[0], len(LEVEL23)))
for i in range(X.shape[0]):
for j, b in enumerate(LEVEL23):
constraints[i, j] = min(X[i, Precedence[j]-1]) - X[i, b-1]
out["F"] = profits.reshape(-1, 1)
out["G"] = constraints
我们现在可以定义 GA 求解器并将其应用于以下问题求解:
problem = OpencastMiningProblem()
algorithm = GA(
pop_size=50,
sampling=FloatRandomSampling(),
crossover=PointCrossover(prob=0.8, n_points=2),
mutation = PolynomialMutation(prob=0.3, repair=RoundingRepair()),
eliminate_duplicates=True
)
res = minimize(problem, algorithm, ('n_gen', 50), seed=1, verbose=True)
print("Best solution found:\nX =", res.X)
print("Objective value =", -res.F[0])
运行此代码会产生以下输出:
Best solution found:
X = [0 1 1 0 1 1 0 1 1 0 1 0 1 1 1 1 0 1]
Objective value = 4300.0
书籍 GitHub 仓库中 C.12 列表的完整版本包括了帕累托优化实现,将问题视为一个多目标优化问题。
C.8 第九章:粒子群优化
C.8.1 练习
- 多项选择题和判断题:为以下每个问题选择正确答案。
1.1. 以下哪项是 PSO 中星型结构的缺点?
a. 可能会导致过早收敛到次优解。
b. 可能会导致算法收敛速度过慢。
c. 可能会导致算法陷入局部最优。
d. 不允许算法有效地探索搜索空间。
1.2. 异步 PSO 通常会产生更好的结果,因为它使粒子使用更及时的信息。
a. 正确
b. 错误
1.3. PSO 中认知组件和社会组件之间的区别是什么?
a. 认知组件基于粒子的自身经验,而社会组件基于整个群体的经验。
b. 认知组件基于粒子的邻居的经验,而社会组件基于粒子的自身经验。
c. 认知组件基于随机扰动,而社会组件基于梯度信息。
d. 认知组件和社会组件是同一件事。
1.4. 环形拓扑,或 lbest PSO,已被证明比其他网络结构收敛得更快,但容易陷入局部最小值。
a. 正确
b. 错误
1.5. 在二进制 PSO(BPSO)中,二进制字符串中的每个位都通过考虑其当前状态、迄今为止保持的最佳状态以及其相邻位的最优状态来更新。
a. 正确
b. 错误
1.6. 在 PSO 中,使粒子表现得像独立爬山者的速度模型是
a. 仅认知模型
b. 仅社会模型
1.7. PSO 保证找到函数的全局最优解。
a. 正确
b. 错误
1.8. 在 PSO 算法中,加速度系数的作用是什么?
a. 控制粒子的速度
b. 控制探索/利用权衡
c. 控制群体大小
d. 控制突变率
1.9. PSO 是一种局部搜索算法,只能找到局部最优解。
a. 正确
b. 错误
1.10. PSO 最初是为连续值空间开发的。
a. 正确
b. 错误
-
在 Martin 等人在其文章“局部终止标准用于群体智能:局部随机扩散搜索与蚂蚁巢穴选择之间的比较”中描述的餐厅游戏中,一群代表在一个不熟悉的城镇参加长时间的会议。每晚他们都必须找到吃饭的地方。有大量的餐厅可供选择,每家餐厅都提供大量的餐点。该群体面临的问题是找到最好的餐厅,即大多数代表愿意就餐的餐厅。即使通过并行穷举搜索餐厅和餐点组合也会花费太长时间。如果你使用 PSO 来解决这个问题,你将如何描述在这个问题背景下速度更新的三个组件?
-
共享出行是共享经济商业模式的一种成功实施,其中个人车辆由车主或驾驶员与具有相似行程和日程的个别旅客共享。共享出行问题是一个多目标约束优化问题。共享出行优化目标的不完整列表包括
• 最小化驾驶员行程的总旅行距离或时间
• 最小化乘客行程的总旅行时间
• 最大化匹配(服务)请求的数量
• 最小化驾驶员行程的成本
• 最小化乘客行程的成本
• 最大化驾驶员的收入
• 最小化乘客的等待时间
• 最小化所需驾驶员的总数
考虑图 C.31 所示的拼车问题,其目标是制定驾驶员的行程,以最小化车辆行程的总距离。该多目标优化的表述如下:
找到最小化 f 的 s

其中
• f 是为服务所有乘客而进行的车辆行程的总距离。
• s[i,j] 如果位置 j 被分配在驾驶员的行程中则为 1,否则为 0。
• d[i,j] 是路线内两点之间的距离,其中
d[i,j] = √((x[i] – x[j])² + (y[i] – y[j])²
• P 是乘客集合,具有已知的接车和送车点。
• V 是车辆集合,具有预定义的起始和终止位置。

图 C.31 拼车问题
给定五位乘客的接车和送车位置(表 C.3)以及两位驾驶员的起始和终止位置(表 C.4),定义一个合适的粒子表示(即候选解决方案),以表示每位驾驶员的行程并评估其适应性。
表 C.3 乘客的接车和送车位置
| 乘客 | 接车 x 坐标 | 接车 y 坐标 | 送车 x 坐标 | 送车 y 坐标 |
|---|---|---|---|---|
| P1 | 9 | 9 | 4 | 2 |
| P2 | 8 | 5 | 2 | 4 |
| P3 | 6 | 1 | 8 | 6 |
| P4 | 7 | 6 | 9 | 8 |
| P5 | 3 | 5 | 10 | 3 |
表 C.4 驾驶员的起始和终止位置
| 驾驶员 | 起始 x 坐标 | 起始 y 坐标 | 终止 x 坐标 | 终止 y 坐标 |
|---|---|---|---|---|
| A | 4 | 1 | 8 | 10 |
| B | 1 | 6 | 9 | 4 |
4. 考虑旅行行程规划问题,其目标是提供为访问新城市的游客提供的最佳旅行行程,考虑到要访问地点的质量、景点之间的邻近性以及一天中可以完全占用且空闲时间最少的时间。问题表述如下:
找到优化 f 的 X

遵循以下约束条件:
• 行程的持续时间不得超过给定一天的总时间,该时间为 480 分钟。这表示为行程中每个场所的持续时间与要访问的总通勤时间的总和。这意味着 Z[d] + Z[c] <= 480 分钟。
• 解决方案中不应重复相同的场所。
其中
• X = [Z[d] Z[c] Z[r]]^T
• Z[d] 表示行程的持续时间(分钟)。
• Z[c] 表示行程中的总通勤时间。
• Z[r] 代表行程中所有场所的平均评分。评分来自 Yelp、Google 评论或其他来源。
表 C.5 列出了 10 个景点之间的通勤时间。
表 C.5 景点之间的通勤时间
| Commute time (mins) | To |
|---|---|
| 1 | |
| From | 0 |
| 1 | |
| 2 | 12 |
| 3 | 14 |
| 4 | 1 |
| 5 | 7 |
| 6 | 12 |
| 7 | 10 |
| 8 | 22 |
| 9 | 22 |
不同景点的持续时间及评分列于表 C.6 中。
表 C.6 景点评分和持续时间
| ID | Rating | Duration (mins) |
|---|---|---|
| 0 | Starting hotel | |
| 1 | 2 | 120 |
| 2 | 3 | 60 |
| 3 | 3 | 180 |
| 4 | 0 | 180 |
| 5 | 5 | 120 |
| 6 | 1 | 60 |
| 7 | 4 | 60 |
| 8 | 0 | 60 |
| 9 | 2 | 120 |
定义一个合适的粒子表示(即候选解),并进行两次手动迭代,以展示如何使用 PSO 算法(假设群体大小为 4)来解决这个问题。
5. 三角测量法用于确定移动对象(如连接车辆或手机)的位置。这个过程使用车辆与三个或更多已知基站的距离来确定车辆的位置。通过测量每个基站设备信号的信号强度,可以计算出设备与每个基站之间的距离。这些距离测量产生的三个(或更多)圆的交点给出了设备位置的估计。如图 C.32 所示,三个基站发布它们的坐标并传输参考信号。

图 C.32 基站三角测量
连接车辆使用参考信号来估计到每个单元格 r[i] 的距离。这些距离测量值 r[i] 可能会受到噪声的影响。假设相关的误差由以下方程给出:

车辆位置(ϕ[0],λ[0])是使以下目标函数最小化的位置:

使用 PSO 算法编写 Python 代码来查找车辆的位置。
6. 咖啡店提供两种咖啡大小:小杯和大杯。制作一杯小咖啡的成本是 1 美元,制作一杯大咖啡的成本是 1.50 美元。咖啡店以每杯 2 美元的价格出售小杯咖啡,以每杯 3 美元的价格出售大杯咖啡。咖啡店希望最大化其利润,但同时也希望确保每天至少出售 50 杯小杯咖啡和 75 杯大杯咖啡,并且分别最多不超过 300 杯和 450 杯。这个问题可以表述为一个优化问题,如下所示:
最大化利润 = 2x[1] + 3x[2] - (x[1] + 1.5x[2])
约束条件:
50 ≤ x[1] ≤ 300
75 ≤ x[2] ≤ 450
其中
• x[1] 是制作的小杯咖啡数量。
• x[2] 是制作的大杯咖啡数量。
利润是总利润,其中方程中的第一项代表咖啡销售收入,第二项代表制作咖啡的成本。编写 Python 代码以找到每天制作小杯咖啡和大杯咖啡的最佳数量。
- 一家医院希望优化其医生的工作安排,以最小化劳动力总成本,同时确保有足够的医生满足患者需求。每位医生的时薪不同,而且一天中不同时间段的病人需求水平也不同。目标是找到最优的排班方案,以最小化劳动力总成本,同时满足高峰时段和非高峰时段的最低医生需求。
医院必须在高峰时段安排至少四名医生,在非高峰时段至少安排两名医生。医院可以以较低的时薪聘请兼职医生,但他们只能在非高峰时段工作。医院还有选择在高峰时段支付全职医生加班费以满足需求的选项,但时薪较高。
该问题可以用以下数学公式描述:
最小化劳动力总成本:f(x) = Σ(c[i] × x[i]) + Σ(c[i]^o × x[i]^o) + Σ(c[j]^p × x[j]^p)
约束条件如下:
• Σx[i] + Σx[i]^o ≥ 4:高峰时段至少安排四名医生。
• Σx[i]^p ≥ 2:非高峰时段至少安排两名医生。
• x[i], x[i]^o, x[j]^p ≥ 0:非负约束
其中
• i 是全职医生的索引,i = 1, 2, ..., m.
• j 是兼职医生的索引,j = 1, 2, ..., n。
预定义参数:
• c[i] 是全职医生 i 的时薪。
• c[i]^o 是全职医生 i 的加班时薪。
• c[j]^p 是兼职医生 j 的时薪。
决策变量:
• x[i] 是全职医生 i 在高峰时段工作的小时数。
• x[i]^o 是全职医生 i 在高峰时段加班工作的小时数。
• x[j]^p 是兼职医生 j 在非高峰时段工作的小时数。
假设全职医生的时薪为 [30, 35, 40, 45, 50],他们的加班费是全职时薪的 1.5 倍,兼职医生的时薪为 [25, 27, 29, 31, 33]。编写 Python 代码使用 PSO 求解此问题。
8. 在城市 X 的邻域 Y 中,有八所学校共同拥有 100 台显微镜用于生物课教学。然而,这些资源在学校之间并不是均匀分布的。随着学生入学人数的最近变化,四所学校拥有的显微镜多于所需,而其他四所学校则需要额外的显微镜。为了解决这个问题,负责城市 X 学校委员会生物系的 Rachel Carson 博士决定使用数学模型。她选择使用运输问题模型,这是一种旨在高效分配资源并最小化运输成本的策略,正如 R. Lovelace 在其文章“地理分析中的开源工具”中所述 [7]。该模型将供应 n 和需求 m 表示为网络中各个点的决策变量的单位权重,从供应点到需求点的单位运输成本等同于节点之间的时间或距离。这些数据被捕获在一个 n*m 成本矩阵中。该整数线性规划问题的正式陈述在 Daskin 的书籍“网络和离散定位:模型、算法和应用” [8] 中描述如下:

满足以下约束条件
• Σ[j][∈][J] x[ij] ≤ S[i] 对于所有 i ∈ I
• Σ[j][∈][J] x[ij] ≥ D[i] 对于所有 j ∈ J
• x[ij] ≥ 0 对于所有 i ∈ I 和所有 j ∈ J
其中
• i 是每个潜在的源节点。
• I 是所有潜在源节点的完整集合。
• j 是每个潜在的终点节点。
• J 是所有潜在节点的完整集合。
• x[ij] 是从所有 i ∈ I 到所有 j ∈ J 的运输量。
• c[ij] 是所有 i, j 对之间的单位运输成本。
• S[i] 是节点 i 的供应量,对于所有 i ∈ I。
• D[j] 是节点 i 的需求量,对于所有 j ∈ J。
编写 Python 代码使用 PSO 解决这个问题。在地理空间地图上可视化解决方案。
9. 在书籍 GitHub 存储库的第九章/项目文件夹中,提供了以下样本研究项目以供审查和实验。
• 路由——探讨使用 PSO 在多伦多两个感兴趣点之间找到最短路径。
• 公交站点的放置——探讨如何使用粒子群优化算法(PSO)在加拿大安大略省滑铁卢/基奇纳地区找到公交站点的最佳位置。
C.8.2 解决方案
1. 多项选择题和真/假题
1.1. c) 可能会导致算法陷入局部最优。
1.2. a) 正确
1.3. a) 认知组件基于粒子的自身经验,而社交组件基于整个群体的经验。
1.4. b) 错误
1.5. a) 正确
1.6. a) 认知模型
1.7. b) 错误
1.8. b) 为了控制探索/利用权衡
1.9. b) 错误(PSO 旨在全局探索搜索空间,并有可能找到全局最优解。)
1.10. a) 正确
2. 想象一群代表访问一个不熟悉的城市参加会议。他们试图使用 PSO 原则在该镇找到最好的餐厅。这个城镇很大,每位代表从一个不同的位置开始。每位代表都有一种偏好的探索餐厅的方式,比如沿着某些街道散步或访问特定的社区。这类似于 PSO 中的惯性成分,其中粒子保持它们当前的速度和方向,确保它们不会太突然地改变它们的探索模式。
随着每位代表访问不同的餐厅,他们会记住迄今为止他们去过最好的餐厅(他们的个人最佳)。如果他们遇到一个不那么吸引人的餐厅,他们更有可能回到他们最喜欢的餐厅,因为他们知道根据他们自己的经验,这是一个好的选择。这是认知成分,在 PSO 中,粒子被吸引到它们个人的最佳位置,遵循它们过去的经验和个人偏好。
代表们还通过群聊相互交流,分享他们找到的最好餐厅的经历和位置。如果有人发现一家杰出的餐厅,其他人可能会决定去那个地方亲自尝试,即使它不是他们的个人最爱。这是社交成分,在 PSO 中,粒子受到全局最佳位置或群体集体知识的影响。
3. 由于公式的离散性质,应使用基于排列的 PSO。在这个算法中,一个粒子代表每个司机要接车和下车的乘客的排序。在这个问题中,我们有两个车辆 A 和 B,以及五个乘客请求(P1–P5)需要匹配。例如,一个有五个乘客和两个司机的候选解决方案将具有以下格式:
| A^+ | P3^+ | P4^+ | P3^– | P5^+ | P4^– | P5^– | A^– |
|---|---|---|---|---|---|---|---|
| B^+ | P1^+ | P2^+ | P2^– | P1^– | B^– |
其中
• + 表示乘客请求的接车点和车辆的起点。
• – 表示乘客请求的交货点和车辆的终点。
这个解决方案可以读作如下:
• 车辆 A 的起点 → 乘客 3 接车 → 乘客 4 接车 → 乘客 3 下车 → 乘客 5 接车 → 乘客 4 下车 → 乘客 5 下车 → 车辆 A 的终点。
• 车辆 B 的起点 → 乘客 1 接车 → 乘客 1 接车 → 乘客 2 下车 → 乘客 2 下车 → 车辆 B 的终点。
这两个时间表也可以按以下方式连接:
| A^+ | P3^+ | P4^+ | P3^- | P5^+ | P4^- | P5^- | A^- | B^+ | P1^+ | P2^+ | P2^- | P1^- | B^- |
|---|
我们使用目标函数如下评估这个解决方案:

关于具有时间窗口的拼车问题(RMPTW)的更全面讨论以及该简化问题的扩展版本,请参阅 Herbawi 和 Weber 的文章“用于解决具有时间窗口的动态拼车问题的遗传和插入启发式算法” [9]。
- 二进制粒子群优化(BPSO)用于处理此问题。使用二进制字符串来描述要访问的吸引子。例如,[0 0 1 0 1 0 0 0 0] 表示访问吸引子 3 和 5。速度的计算使用以下公式:

其中
• i 是粒子编号。
• d 是维度或吸引子。
• v[k]^(id) 是第 k 次迭代中粒子 i 和维度 d 的速度。
• pbest[k]^(id) 是第 t 次迭代中粒子 i 和维度 d 的个人最佳值。
• gbest[k]^d 是第 k 次迭代中维度 d 的全局最优值。
• x[k]^(id) 是第 k 次迭代中粒子 i 和维度 d 的当前位置。
• ϕ[1], ϕ[2] 是在 0 和 2 之间均匀生成的随机数。
更新速度向量后,每个速度的 sigmoid 值更新如下:

使用速度的 sigmoid 值创建一个新的位置。接下来,粒子位置更新如下:

其中
• r 是在 0 和 1 之间均匀生成的随机数。
• sig(v[k][+1]^(id)) 是速度 v[k][+1]^(id) 的 sigmoid 值
表 C.7 初始化
| ϕ[1] | ϕ[2] | v[01] | v[02] | v[03] | v[04] | v[05] | v[06] | v[07] | v[08] | v[09] | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| x[1] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| x[2] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| x[3] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| x[4] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
令 p[i] 为粒子在表 C.8 中所示的吸引子 i 的当前位置的二进制值。
表 C.8 9 个吸引子的位置二进制值
| p[1] | p[2] | p[3] | p[4] | p[5] | p[6] | p[7] | p[8] | p[9] | |
|---|---|---|---|---|---|---|---|---|---|
| x[1] | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| x[2] | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| x[3] | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| x[4] | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
表 C.9 初始化:适应度评估
| p[1] | p[2] | p[3] | p[4] | p[5] | p[6] | p[7] | p[8] | p[9] | 总通勤时间 | 总访问地点 | 平均评分 | 总持续时间 | f(x[i]) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| x[1] | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 34 | 2 | 2.000 | 180 | 0.700 |
| x[2] | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 19 | 2 | 5.000 | 240 | 2.405 |
| x[3] | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 76 | 2 | 2.500 | 180 | 0.809 |
| x[4] | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 30 | 2 | 3.000 | 180 | 1.059 |
表 C.10 粒子初始化时的当前个人最佳
| pbest[01] | pbest [02] | pbest [03] | pbest [04] | pbest [05] | pbest [06] | pbest [07] | pbest [08] | pbest [09] | pbestVal | |
|---|---|---|---|---|---|---|---|---|---|---|
| x[1] | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0.700 |
| x[2] | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 2.405 |
| x[3] | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0.809 |
| x[4] | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1.059 |
表 C.11 迭代 1:速度更新
| ϕ[1] | ϕ[2] | v[11] | v[12] | v[13] | v[14] | v[15] | v[16] | v[17] | v[18] | v[19] | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| x[1] | 0.958 | 0.830 | -0.830 | -0.830 | 0.830 | 0.000 | 0.000 | 0.000 | 0.000 | 0.830 | 0.000 |
| x[2] | 1.347 | 1.517 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 | 0.000 |
| x[3] | 1.320 | 1.649 | 0.000 | -1.649 | 1.649 | 0.000 | -1.649 | 0.000 | 0.000 | 1.649 | 0.000 |
| x[4] | 0.757 | 0.678 | -0.678 | 0.000 | 0.678 | 0.000 | 0.000 | 0.000 | -0.678 | 0.678 | 0.000 |
表 C.12 迭代 1 后粒子的更新 sigmoid 速度值
| sig(v[11]) | sig(v[12]) | sig(v[13]) | sig(v[14]) | sig(v[15]) | sig(v[16]) | sig(v[17]) | sig(v[18]) | sig(v[19]) | |
|---|---|---|---|---|---|---|---|---|---|
| x[1] | 0.304 | 0.304 | 0.696 | 0.500 | 0.500 | 0.500 | 0.500 | 0.696 | 0.500 |
| x[2] | 0.500 | 0.500 | 0.500 | 0.500 | 0.500 | 0.500 | 0.500 | 0.500 | 0.500 |
| x[3] | 0.500 | 0.161 | 0.839 | 0.500 | 0.161 | 0.500 | 0.500 | 0.839 | 0.500 |
| x[4] | 0.337 | 0.500 | 0.663 | 0.500 | 0.500 | 0.500 | 0.337 | 0.663 | 0.500 |
表 C.13 迭代 1 后决定粒子更新的均匀生成随机数
| r[11] | r[12] | r[13] | r[14] | r[15] | r[16] | r[17] | r[18] | r[19] | |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 0.477 | 0.724 | 0.875 | 0.654 | 0.088 | 0.089 | 0.853 | 0.925 | 0.528 |
| 2 | 0.673 | 0.530 | 0.438 | 0.785 | 0.218 | 0.763 | 0.838 | 0.749 | 0.590 |
| 3 | 0.534 | 0.086 | 0.301 | 0.763 | 0.653 | 0.754 | 0.809 | 0.974 | 0.763 |
| 4 | 0.218 | 0.697 | 0.875 | 0.854 | 0.116 | 0.941 | 0.678 | 0.742 | 0.965 |
迭代 1:全局最优值为 2.405,最佳粒子为x[2] = [0 0 1 0 0 0 0 0 0]。
迭代 2:速度更新
表 C.14 迭代 2 后的粒子状态和适应度函数更新
| | p[1] | p[2] | p[3] | p[4] | p[5] | p[6] | p[7] | p[8] | p[9] | 总通勤时间 | 总访问地点 | 平均评分 | 总持续时间 | f(x[i]) |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| x[1] | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 64 | 2 | 3.500 | 180 | 1.158 |
| x[2] | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 37 | 2 | 5.000 | 300 | 2.901 |
| x[3] | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 70 | 2 | 2.500 | 240 | 1.091 |
| x[4] | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 24 | 2 | 4.500 | 240 | 2.143 |
表 C.15 迭代 2 后粒子的更新个人最佳
| pbest[11] | pbest [12] | pbest [13] | pbest [14] | pbest [15] | pbest [16] | pbest [17] | pbest [18] | pbest [19] | pbestVal | |
|---|---|---|---|---|---|---|---|---|---|---|
| x[1] | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1.158 |
| x[2] | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 2.901 |
| x[3] | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1.091 |
| x[4] | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 2.143 |
全局最佳值为 2.901,最佳粒子为 x[2] = [0 0 1 0 1 0 0 0 0]。重复执行,直到满足停止条件。
5. 下一个列表显示了使用 PSO 解决三角定位问题的步骤。
列表 C.13 使用 PSO 解决三角定位问题
import numpy as np
from pyswarms.single import GlobalBestPSO
import pyswarms as ps
def objective_function(pos): ①
r = np.array([6, 6.5, 8])
x0 = np.array([1, 3, 7])
y0 = np.array([5, 1, 7])
x = pos[:, 0]
y = pos[:, 1]
f = np.sum(r - np.sqrt((x0 - x.reshape(-1, 1)) ** 2 + (y0 - y.reshape(-
➥1, 1)) ** 2), axis=1)
return f
options = {"c1": 0.5, "c2": 0.5, "w": 0.79} ②
bounds = (np.array([0, 0]), np.array([8, 8])) ②
optimizer = GlobalBestPSO(n_particles=100, dimensions=2, options=options,
➥ bounds=bounds) ③
best_cost, best_position = optimizer.optimize(objective_function,
➥ iters=1000) ③
print("Best position:", np.round(best_position,2)) ④
print("Best cost:", np.round(best_cost,3)) ④
① 定义目标函数。
② 设置 PSO 算法。
③ 初始化 GlobalBestPSO,并最小化目标函数。
④ 打印结果。
6. 下一个列表显示了使用 PSO 解决咖啡店问题的步骤。
列表 C.14 使用 PSO 解决咖啡店问题
import numpy as np ①
import pyswarms as ps ①
def fitness_function(x): ②
n_particles = x.shape[0] ②
profit=0 ②
for i in range(n_particles): ②
profit=-(2*x[i][0] + 3*x[i][1] - (x[i][0] + 1.5*x[i][1])) ②
return profit ②
num_particles = 10 ③
num_iterations = 100 ③
lb = np.array([50, 75]) ④
ub = np.array([300, 450]) ④
bounds = (lb, ub) ④
options={'w':0.79, 'c1': 1.5, 'c2': 1.3} ⑤
optimizer = ps.single.GlobalBestPSO(n_particles=num_particles,
➥ dimensions=2, options=options, bounds=bounds) ⑥
best_cost, best_pos = optimizer.optimize(fitness_function,
➥ iters=num_iterations) ⑦
best_pos=np.asarray(best_pos, dtype = 'int')
print('###############################################')
print('Total profit: ', round(-best_cost, 2), '$')
print('Optimal number of small coffees to make: ',best_pos[0])
print('Optimal number of large coffees to make: ', best_pos[1])
① 导入所需的库。
② 定义适应度函数。
③ 设置粒子数量和迭代次数。
④ 设置变量的上下限。
⑤ 设置优化器选项。
⑥ 初始化优化器。
⑦ 执行优化。
7. 下一个列表显示了使用 PSO 解决医生排班问题的步骤。
列表 C.15 使用 PSO 解决医生排班问题
import matplotlib.pyplot as plt
import numpy as np
import pyswarms as ps
full_time_rates = np.array([30, 35, 40, 45, 50]) ①
overtime_rates = full_time_rates * 1.5 ①
part_time_rates = np.array([25, 27, 29, 31, 33]) ①
def objective_function(x):
x1 = x[:, :len(full_time_rates)] ②
x2 = x[:, len(full_time_rates):2 * len(full_time_rates)] ②
x3 = x[:, 2 * len(full_time_rates):] ②
total_cost = np.sum(x1 * full_time_rates) + np.sum(x2 * overtime_rates)
➥ + np.sum(x3 * part_time_rates) ③
penalty_x1_x2 = np.maximum(0, 4 - np.sum(x1) - np.sum(x2)) * 10000
penalty_x3 = np.maximum(0, 2 - np.sum(x3)) * 10000 ④
④
total_cost_with_penalty = total_cost + penalty_x1_x2 + penalty_x3 ④
return total_cost_with_penalty
min_bound = np.zeros(3 * len(full_time_rates)) ⑤
max_bound = np.full(3 * len(full_time_rates), 10) ⑤
bounds = (min_bound, max_bound) ⑤
options = {'w': 0.74, 'c1': 2.05, 'c2': 2.05, 'k': 5, 'p': 1} ⑥
optimizer = ps.single.LocalBestPSO(n_particles=30, dimensions=3 *
➥ len(full_time_rates), options=options, bounds=bounds) ⑦
cost, pos = optimizer.optimize(objective_function, iters=100, verbose=True) ⑧
optimal_x1 = pos[:len(full_time_rates)] ⑨
optimal_x2 = pos[len(full_time_rates):2 * len(full_time_rates)] ⑨
optimal_x3 = pos[2 * len(full_time_rates):] ⑨
① 设置全职小时费率、加班费率和兼职医生费率。
② 定义三个决策变量。
③ 将总成本定义为目标函数。
④ 定义约束并将其作为惩罚添加到成本函数中。
⑤ 初始化边界。
⑥ 设置 PSO 的选项。
⑦ 创建 PSO 的实例。
⑧ 执行优化。
⑨ 提取决策变量 x1、x2 和 x3 的最优值。
列表 C.15 的完整版本可在本书的 GitHub 仓库中找到,其中包含一个用于打印和可视化结果的函数。图 C.33 显示了每位医生的工作小时数。

图 C.33 每位医生的工作小时数
8. 下一个列表显示了使用 PSO 解决供应链优化问题的步骤。我们首先导入所需的库。
列表 C.16 使用 PSO 解决供应链优化问题
import numpy as np
import random
from pymoo.algorithms.soo.nonconvex.pso import PSO
from pymoo.optimize import minimize
from pymoo.algorithms.soo.nonconvex.ga import GA
from pymoo.operators.crossover.pntx import PointCrossover
from pymoo.operators.mutation.pm import PolynomialMutation
from pymoo.operators.repair.rounding import RoundingRepair
from pymoo.operators.sampling.rnd import FloatRandomSampling
from pymoo.core.problem import Problem
from geopy.geocoders import Nominatim
from geopy.distance import geodesic
import folium
我们接着定义如下问题数据:
supply_schools = [1, 6, 7, 8] ①
demand_schools = [2, 3, 4, 5] ②
amount_supply = [20, 30, 15, 35] ③
amount_demand = [5, 45, 10, 40] ④
n_var=len(supply_schools)*len(demand_schools) ⑤
① 可提供显微镜的学校
② 请求显微镜的学校
③ 每所学校可用的显微镜数量
④ 每所学校请求的显微镜数量
⑤ 变量数量
作为 C.16 列表的延续,以下函数生成中心点周围的随机位置,以表示所选城市中的供应和需求学校(以多伦多为例)。我们使用 geopy 计算学校之间的距离:
np.random.seed(0) ①
def generate_random(number, center_point):
lat, lon = center_point ②
coords = [(random.uniform(lat - 0.01, lat + 0.01), random.uniform(lon -
➥ 0.01, lon + 0.01)) for _ in range(number)]
return coords
supply_coords = generate_random(len(supply_schools), [location.latitude,
➥ location.longitude]) ③
demand_coords = generate_random(len(demand_schools), [location.latitude,
➥ location.longitude]) ④
distances = [] ⑤
for supply in supply_coords: ⑤
row = [] ⑤
for demand in demand_coords: ⑤
row.append(geodesic(supply, demand).kilometers) ⑤
distances.append(row) ⑤
distances = np.array(distances) ⑤
cost_matrix=50*distances
① 为了可重复性
② 设置地图的中心。
③ 为供应学校生成随机的 GPS 坐标(纬度,经度)。
④ 为需求学校生成随机的 GPS 坐标(纬度,经度)。
⑤ 计算学校之间的测地线距离(公里)。
以下类定义了与 pymoo 兼容的运输问题。它定义了决策变量、约束条件和目标函数:
class Transportation_problem(Problem):
def __init__(self,
cost_matrix,
amount_supply,
amount_demand
):
super().__init__(n_var=n_var,n_constr=1, vtype=int)
self.cost_matrix = cost_matrix
self.amount_supply = amount_supply
self.amount_demand = amount_demand
self.xl = np.array(np.zeros(n_var))
self.xu = np.repeat(amount_supply, len(amount_demand))
def _evaluate(self, X, out, *args, **kwargs):
loss = np.zeros((X.shape[0], 1))
g = np.zeros((X.shape[0], 1))
for i in range(len(X)):
soln = X[i].reshape(self.cost_matrix.shape)
cost_x = X[i].reshape(self.cost_matrix.shape)
cost = cost_x * cost_matrix.T
cost = cost.sum()
loss[i] = cost
total_supply = soln.sum(axis=1)
total_demand = soln.sum(axis=0)
print("total_supply: ", total_supply)
print("total_demand: ", total_demand)
g[i] = np.any(total_supply<self.amount_supply) or
➥ np.any(total_demand<self.amount_demand)
out["F"] = loss
out["G"] = g
现在我们可以创建一个问题对象和一个 PSO 求解器,如下所示:
problem = Transportation_problem(cost_matrix,amount_supply,amount_demand)
algorithm = PSO(pop_size=100,repair=RoundingRepair())
以下行用于运行 150 次迭代 PSO 求解器:
res = minimize(problem, algorithm, ('n_gen', 150), seed=1, verbose=False)
以下代码片段用于打印 PSO 求解器获得的解决方案:
soln = res.X ①
supply_num=len(amount_supply) ②
demand_num=len(amount_demand) ③
for i in range(supply_num): ④
print(f"Supply School({supply_schools[i]}): {' + [CA]'.join(['soln['+str(j)+']' for j in range(i*supply_num,
➥ (i+1)*supply_num)])} <= {amount_supply[i]} or {' + '.join(map(str,
➥ soln[i*supply_num:(i+1)*supply_num]))} <= {amount_supply[i]} or
➥ {sum(soln[i*supply_num:(i+1)*supply_num])} <= {amount_supply[i]}")
for j in range(demand_num): ⑤
print(f"Demand School({demand_schools[j]}): {' +
➥ '.join(['soln['+str(i*demand_num+j)+']' for i in range(demand_num)])}
➥ >= {amount_demand[j]} or {' + '.join(map(str, [soln[i*4+j] for i in
➥ range(demand_num)]))} >= {sum(soln[i*demand_num+j] for i in
➥ range(demand_num))} or {sum(soln[i*demand_num+j] for i in
➥ range(demand_num))} >= {amount_demand[j]}")
print(f"Shipping cost = {round(res.F[0], 2)} $") ⑥
① 提取解决方案。
② 供应点的数量
③ 需求点的数量
④ 打印每个供应点。
⑤ 打印每个需求点。
⑥ 打印运输成本。
考虑到实现中包含的随机性,代码将产生类似以下输出的结果:
Supply School(1): soln[0] + soln[1] + soln[2] + soln[3] <= 20 or 1 + 0 + 14 + 5 <= 20 or 20 <= 20
Supply School(6): soln[4] + soln[5] + soln[6] + soln[7] <= 30 or 0 + 30 + 0 + 0 <= 30 or 30 <= 30
Supply School(7): soln[8] + soln[9] + soln[10] + soln[11] <= 15 or 0 + 15 + 0 + 0 <= 15 or 15 <= 15
Supply School(8): soln[12] + soln[13] + soln[14] + soln[15] <= 35 or 4 + 0 + 0 + 35 <= 35 or 39 <= 35
Demand School(2): soln[0] + soln[4] + soln[8] + soln[12] >= 5 or 1 + 0 + 0 + 4 >= 5 or 5 >= 5
Demand School(3): soln[1] + soln[5] + soln[9] + soln[13] >= 45 or 0 + 30 + 15 + 0 >= 45 or 45 >= 45
Demand School(4): soln[2] + soln[6] + soln[10] + soln[14] >= 10 or 14 + 0 + 0 + 0 >= 14 or 14 >= 10
Demand School(5): soln[3] + soln[7] + soln[11] + soln[15] >= 40 or 5 + 0 + 0 + 35 >= 40 or 40 >= 40
Shipping cost = 3053.94 $
以下代码片段可用于使用 folium 可视化空间地图的解决方案:
def normalize(lst): ①
s = sum(lst)
return list(map(lambda x: (x/s)*10, lst))
soln_normalized = normalize(soln) ②
colors = ['cyan', 'brown', 'orange', 'purple'] ③
m = folium.Map(location=[location.latitude, location.longitude],
➥ zoom_start=15, scrollWheelZoom=False, dragging=False) ④
for i, coord in zip(supply_schools, supply_coords):
folium.Marker(location=coord, icon=folium.Icon(icon="home",
➥ color='red'), popup=f'Supply School {i+1}').add_to(m) ⑤
for i, coord in zip(demand_schools, demand_coords):
folium.Marker(location=coord, icon=folium.Icon(icon="flag",
➥ color='blue'), popup=f'Demand School {i+1}').add_to(m) ⑥
for i in range(len(supply_schools)): ⑦
for j in range(len(demand_schools)):
soln_value = soln[i*len(demand_schools) + j]
folium.PolyLine(locations=[supply_coords[i], demand_coords[j]],
➥ color=colors[i % len(colors)],
➥ weight=5*soln_normalized[i*len(demand_schools) + j],
➥ popup=folium.Popup(f'# of microscopes: {soln_value}')).add_to(m)
m ⑧
① 标准化函数
② 标准化解数组
③ 定义颜色列表。
④ 创建以多伦多市中心为中心的地图。
⑤ 为供应学校添加标记。
⑥ 为需求学校添加标记。
⑦ 在供应学校和需求学校之间添加线条(边)。
⑧ 显示地图。
图 C.34 显示了 PSO 可能产生的解决方案。

图 C.34 学校显微镜供应和需求
书中 C.16 列表的完整版本,可在 GitHub 仓库GitHub repo中找到,也展示了如何使用遗传算法解决这个问题。
C.9 第十章:其他群智能算法探索
C.9.1 练习
1. 匹配表 C.16 中显示的术语和描述。
表 C.16
| 术语 | 描述 |
|---|---|
| 1. 蚁群系统(ACS) | a. 在耗尽当前食物源后寻找新食物源的蜜蜂 |
| 2. 蚁群算法 | b. 食物路径的正反馈导致越来越多的蚂蚁跟随该路径 |
| 3. 巡视蜂 | c. 一种不考虑解决方案可行性的信息素更新方法 |
| 4. 最大-最小蚁群系统(MMAS) | d. 巡视蜂尝试寻找新食物源的最大失败次数 |
| 5. 自催化行为 | e. 根据工蜂找到的解决方案的适应性概率选择食物源的蜜蜂 |
| 6. 蚁群密度模型 | f. 社会昆虫通过环境修改进行间接交流,这些修改作为外部记忆 |
| 7. 观察者蜜蜂 | g. 一种使用局部信息更新信息素浓度的信息素更新方法 |
| 8. 蚂蚁循环 | h. 一种使用被称为伪随机比例行动规则的精英策略的蚁群优化(ACO)变体 |
| 9. 蚂蚁系统(AS) | i. 一种通过包括禁忌列表来增加记忆能力的蚁群优化(ACO)变体 |
| 10. 试验限制 | k. 一种克服停滞的蚁群优化(ACO)变体 |
2. 编写 Python 代码,使用蚁群优化算法找到源点与目的地点之间的最短路径。假设你现在站在多伦多市的国王爱德华七世骑马雕像处,GPS 坐标为(43.664527, –79.392442)。想象你是一名多伦多大学的学生,你正在赶往位于信息技术中心(Bahen Centre for Information Technology)的优化算法讲座,其 GPS 坐标为(43.659659, –79.397669)。在以国王学院圆环(GPS 坐标为 43.661667, –79.395)为中心的地图上可视化获得的路线,以便你能到达目的地。请随意使用 optalgotools 包中可用的帮助函数,例如Node、cost和draw_route。使用此代码来实验不同的搜索空间(不同的兴趣区域、不同的起点和目的地)以及不同的算法参数设置。
3. 反渗透(RO)是一种非常有效且重要的脱盐和废水回收过程。假设你需要最大化反渗透高压泵的功率。这个功率取决于以下方程式中的多个参数:

其中
• HP 是反渗透高压泵的功率,单位为 kW。
• M[d] 是反渗透的日产量,单位为 m³/d,其范围是 41.67 < M[d] <416.67 m³/d。
• N[v] 是压力容器的数量,其范围是 1 < N[v] < 200。
• ∆π 是跨膜的净渗透压,其范围是 1400 < ∆π < 2600 kPa。
• RR 是回收率,其范围是 1 < RR < 50%。
• η 是高压泵的效率,其范围是 0.70 < η < 0.85。
• ρ 是水的密度。
编写 Python 代码,使用蚁群优化(ACO)算法找到决策变量(Md, Nv, ∆π, RR, η, 和 ρ)的最优值以最大化 HP。使用 SciPy 中的optimize求解器解决相同的问题。
4. 使用蚁群优化(ACO)算法解决第九章第 8 题中引入的供需问题。使用混合整数分布式蚁群优化(MIDACO)来解决这个问题。MIDACO (www.midaco-solver.com) 是一种用于解决单目标和多目标优化问题的数值高性能软件。它基于蚁群优化,并扩展了混合整数搜索域。
要安装 MIDACO,请按照以下步骤操作:
-
下载 MIDACO Python 网关(midaco.py),并移除.txt 扩展名。
-
下载适当的库文件(midacopy.dll或 midacopy.so)。
-
在您的 PC 上的同一文件夹中存储所有文件。
-
将
midaco导入您的 Jupyter 笔记本。
MIDACO 是许可软件,具有有限的免费许可证,允许使用最多四个变量(两个供应学校和两个需求学校)进行优化。
假设以下简化问题数据:
supply_schools = [1, 3] ①
demand_schools = [2, 4] ②
amount_supply = [20, 30] ③
amount_demand = [5, 45] ④
① 有显微镜的学校
② 请求显微镜的学校
③ 每所学校可用的显微镜数量
④ 每所学校请求的显微镜数量
使用 MIDACO 编写 Python 代码来解决这个问题,并在地理空间地图上可视化解决方案。对于更多学校,您可以获得无限许可证或请求免费学术试用版,该版本可以支持多达 100,000 个变量。
C.9.2 解决方案
- 答案
1-h,2-f,3-a,4-k,5-b,6-c,7-e,8-g,9-i,10-d
- 列表 C.17 显示了在多伦多市找到并可视化两个感兴趣点之间最短路径的步骤。代码使用了 optalgotools 中的帮助函数,如 Node、cost 和 draw_route。我们首先定义并可视化搜索空间如下。
列表 C.17 使用 ACO 解决路由问题
import osmnx as ox
from optalgotools.structures import Node
from optalgotools.routing import cost, draw_route
import random
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
reference = (43.661667, -79.395) ①
G = ox.graph_from_point(reference, dist=300, clean_periphery=True,
➥ simplify=True) ②
origin = (43.664527, -79.392442) ③
destination = (43.659659, -79.397669) ④
origin_id = ox.distance.nearest_nodes(G, origin[1], origin[0]) ⑤
destination_id = ox.distance.nearest_nodes(G, destination[1], ⑤
➥ destination[0]) ⑤
origin_node = Node(graph=G, osmid=origin_id) ⑥
destination_node = Node(graph=G, osmid=destination_id) ⑥
highlighted = [destination_id, origin_id]
nc = ['red' if node in highlighted else '#336699' for node in G.nodes()] ⑦
ns = [70 if node in highlighted else 8 for node in G.nodes()] ⑦
fig, ax = ox.plot_graph(G, node_size=ns, node_color=nc, node_zorder=2) ⑧
① 将多伦多国王学院圆环,安大略省多伦多设为地图的中心。
② 创建一个图。
③ 将爱德华七世骑马雕像设为原点。
④ 将多伦多大学的信息技术中心设为目标。
⑤ 获取起点和目标点最近节点的 osmid。
⑥ 创建起点和终点节点。
⑦ 标记源节点和目标节点。
⑧ 可视化搜索空间。
作为列表 C.17 的延续,我们可以如下初始化 ACO 算法的参数:
alpha = 1 ①
beta = 1 ②
n = 500 ③
Q = 1 ④
pheremone_concentrations = dict()
known_routes = dict()
pheremone_concentrations = {(u,v):random.uniform(0,0.5) for [u,v] in
➥ G.edges()} ⑤
def pheremone(level, distance, alpha, beta): ⑥
return level ** alpha * ((1/distance)) ** beta
① 设置 alpha,一个控制信息素影响的参数。
② 设置 beta,一个控制城市转换吸引力的参数。
③ 设置迭代次数。
④ 随机化信息素。
⑤ 随机化信息素。
⑥ 一个计算信息素水平的函数
我们现在如下实现 ACO 过程:
for ant in tqdm(range(n)):
frontier = [origin_node] ①
explored = set()
route = []
found = False
while frontier and not found:
parent = frontier.pop(0)
explored.add(parent)
children = []
children_pheremones = []
for child in parent.expand():
if child == destination_node:
found = True
route = child.path()
continue
if child not in explored:
children.append(child)
children_pheremones.append(
pheremone(
pheremone_concentrations[(parent.osmid,
➥ child.osmid)],
child.distance,
alpha,
beta,
)
)
if len(children) == 0:
continue
transition_probability = [
children_pheremones[i] / sum(children_pheremones)
for i in range(len(children_pheremones))
]
chosen = random.choices(children, weights=transition_probability,
➥ k=1)[0] ②
children.pop(children.index(chosen)) ③
frontier.extend(children) ③
frontier.insert(0, chosen) ④
for u, v in zip(route[:-1], route[1:]): ⑤
length_of_edge = G[u][v][0]['length']
pheremone_concentrations[(u,v)] += Q/length_of_edge
route = tuple(route)
if route in known_routes: ⑥
known_routes[route] += 1
else:
known_routes[route] = 1
① 将蚂蚁放置在原点节点。
② 以概率选择一个子节点进行探索。
③ 添加所有未探索的子节点,以防我们以后需要探索它们。
④ 将选定的子节点设为下一个要探索的节点。
⑤ 更新信息素。
⑥ 如果路线是新发现的,则将其添加到列表中。
您现在可以打印最佳路线及其成本,并如下可视化获得的路线:
best_route = max(known_routes, key=known_routes.get)
times_used = known_routes[best_route]
route = list(best_route)
print("Cost:", cost(G,route))
print("Times used:",times_used)
draw_route(G,route)
图 C.35 显示了 ACO 生成的最优路线。

图 C.35 ACO 生成的最优路线
- 下一个列表显示了使用 ACO 解决反渗透(RO)高压泵功率最大化问题的步骤。
列表 C.18 使用 ACO 解决 RO 高压泵功率最大化问题
import random
md_range = (41.67, 416.67) ①
nv_range = (1, 200) ①
delta_range = (1400, 2600) ①
rr_range = (1, 50) ①
eta_range = (0.70, 0.85) ①
rho = 1000 ②
def ro_pump_power(X): ③
md, nv, delta, rr, eta=X
return (md ** 2 + 1200 * md * nv * delta) / (nv * rr * 3600 * eta *
➥ rho)
num_ants = 100 ④
num_iterations = 300 ④
evaporation_rate = 0.7 ④
pheromone_deposit = 1 ④
initial_pheromone = 0.25 ④
pheromone_matrix = [[initial_pheromone] * 5 for _ in range(num_ants)] ⑤
best_solution = None ⑥
best_power = float('-inf') ⑥
for _ in range(num_iterations):
solutions = []
powers = []
for ant in range(num_ants): ⑦
md = random.uniform(md_range[0], md_range[1])
nv = random.uniform(nv_range[0], nv_range[1])
delta = random.uniform(delta_range[0], delta_range[1])
rr = random.uniform(rr_range[0], rr_range[1])
eta = random.uniform(eta_range[0], eta_range[1])
soln=(md, nv, delta, rr, eta) ⑧
power = ro_pump_power(soln) ⑧
solutions.append((md, nv, delta, rr, eta)) ⑨
powers.append(power) ⑨
if power > best_power: ⑩
best_solution = (md, nv, delta, rr, eta)
best_power = power
for ant in range(num_ants): ⑪
for i in range(5):
pheromone_matrix[ant][i] *= evaporation_rate
if solutions[ant][i] == best_solution[i]:
pheromone_matrix[ant][i] += pheromone_deposit / powers[ant]
print("Optimal Solution:") ⑫
print("Md:", format(best_solution[0], '.2f')) ⑫
print("Nv:", format(best_solution[1], '.2f')) ⑫
print("Delta:", format(best_solution[2], '.2f')) ⑫
print("RR:", format(best_solution[3], '.2f')) ⑫
print("Eta:", format(best_solution[4], '.2f')) ⑫
print("Optimal HP:", format(best_power, '.2f')) ⑫
① 定义每个决策变量的范围。
② 水的密度为 kg/m³
③ 定义目标函数。
④ 定义 ACO 参数。
⑤ 初始化信息素矩阵。
⑥ 初始化最佳解决方案及其对应的功率。
为每只蚂蚁构建解决方案。
⑧ 计算当前解决方案的功率。
⑨ 存储解决方案及其功率。
⑩ 如有必要,更新最佳解决方案。
⑪ 根据每个解决方案的强度更新信息素路径。
⑫ 打印决策变量的最优值和最优 HP。
以下是一个生成的输出示例:
Optimal Solution:
Md: 404.10
Nv: 7.39
Delta: 2536.93
RR: 1.05
Eta: 0.76
Optimal HP: 425.75
作为 C.18 列表的延续,以下代码片段展示了使用 SciPy 中的optimize求解器解决此问题的步骤。
from scipy.optimize import minimize ①
def ro_pump_power(X): ②
md, nv, delta, rr, eta=X
return -(md ** 2 + 1200 * md * nv * delta) / (nv * rr * 3600 * eta *
➥ rho)
bounds = [md_range, nv_range, delta_range, rr_range, eta_range] ③
x0=[200, 100, 2000, 25, 0.75] ④
result = minimize(ro_pump_power, x0, bounds=bounds, method='SLSQP') ⑤
print("Optimal Solution:") ⑥
print("Md:", format(result.x[0], '.2f')) ⑥
print("Nv:", format(result.x[1], '.2f')) ⑥
print("Delta:", format(result.x[2], '.2f')) ⑥
print("RR:", format(result.x[3], '.2f')) ⑥
print("Eta:", format(result.x[4], '.2f')) ⑥
print("Optimal HP:", format(-result.fun, '.2f')) ⑥
① 导入 scipy 优化器。
② 根据 scipy 的要求,定义带有负号的优化目标函数。
③ 设置决策变量的界限。
④ 设置初始猜测值。
⑤ 使用顺序最小二乘编程(SLSQP)解决优化问题。
⑥ 打印决策变量的最优值和最优 HP。
生成的输出如下:
Optimal Solution:
Md: 416.67
Nv: 99.98
Delta: 2600.00
RR: 1.00
Eta: 0.70
Optimal HP: 515.88
您可以微调 ACO 参数以获得与 SciPy 优化器相当的结果。
- 下一个列表展示了使用 MIDACO 求解器解决供需问题的步骤。我们首先导入库并设置问题数据。
列表 C.19 使用 MIDACO 解决供需问题
import numpy as np
import random
import midaco ①
from geopy.geocoders import Nominatim
from geopy.distance import geodesic
import folium
supply_schools = [1, 3] ②
demand_schools = [2, 4] ③
amount_supply = [20, 30] ④
amount_demand = [5, 45] ⑤
n_var=len(supply_schools)*len(demand_schools)
n_constr=len(supply_schools)
① 导入 MIDACO 求解器。
② 设置有可用显微镜的学校。
③ 设置需要显微镜的学校。
④ 设置每所学校可用的显微镜数量。
⑤ 设置每所学校请求的显微镜数量。
作为延续,我们将为供应和需求学校生成随机的 GPS 坐标:
geolocator = Nominatim(user_agent="SupplyDemand") ①
location = geolocator.geocode("Toronto, Ontario") ②
def generate_random(number, center_point): ③
lat, lon = center_point
coords = [(random.uniform(lat - 0.01, lat + 0.01), random.uniform(lon -
➥ 0.01, lon + 0.01)) for _ in range(number)]
return coords
np.random.seed(0) ④
supply_coords = generate_random(len(supply_schools),
➥ [location.latitude, location.longitude]) ⑤
demand_coords = generate_random(len(demand_schools),
➥ [location.latitude, location.longitude]) ⑥
distances = [] ⑦
for supply in supply_coords: ⑦
row = [] ⑦
for demand in demand_coords: ⑦
row.append(geodesic(supply, demand).kilometers) ⑦
distances.append(row) ⑦
distances = np.array(distances) ⑦
cost_matrix=50*distances ⑧
① 创建一个地理定位对象。
② 获取多伦多的坐标。
③ 生成围绕中心点的随机位置的功能。
④ 设置随机种子以确保生成的随机数的可重复性。
⑤ 为供应学校生成随机的 GPS 坐标(纬度,经度)。
⑥ 为需求学校生成随机的 GPS 坐标(纬度,经度)。
⑦ 计算学校之间的测地线距离(公里)。
⑧ 计算成本矩阵。
以下函数定义了优化问题的主要成分,包括目标函数和约束。
def problem_function(x):
f = [0.0]*1 ①
g = [0.0]*n_constr ②
f[0] = np.sum(np.multiply(cost_matrix.flatten(), x)) ③
soln=np.reshape(x, (len(supply_schools), len(demand_schools))) ④
total_supply = soln.sum(axis=1)
total_demand = soln.sum(axis=0)
g[0] = (np.all(total_supply>=amount_supply) and np.all(total_demand>=amount_demand))-1 ⑤
return f,g ⑥
① 初始化目标函数 F(X)的数组。
② 初始化约束 G(X)的数组。
③ 目标函数 F(X)
④ 候选解决方案
⑤ 在 g[me:m-1]中,不等式约束 G(X) >= 0 必须排在第二位。
⑥ 返回目标函数和约束评估。
使用 MIDACO 的第一步是将问题定义为以下形式:
key = b'MIDACO_LIMITED_VERSION___[CREATIVE_COMMONS_BY-NC-ND_LICENSE]' ①
problem = {} ②
option = {} ③
problem['@'] = problem_function ④
① 免费有限许可证,支持多达 4 个变量。
② 初始化包含问题规范的字典。
③ 初始化包含 MIDACO 选项的字典。
④ 处理问题函数名称。
问题维度定义如下:
problem['o'] = 1 ①
problem['n'] = n_var ②
problem['ni'] = n_var ③
problem['m'] = n_constr ④
problem['me'] = 0 ⑤
① 目标数量
② 变量数量(总计)
③ 整数变量数量(0 <= ni <= n)
④ 约束数量(总计)
⑤ 等式约束数量(0 <= me <= m)
下界和上界xl和xu定义如下:
problem['xl'] = [0.0]*n_var
problem['xu'] = [30.0]*n_var
起始点x设置为下界:
problem['x'] = problem['xl']
停止标准定义如下:
option['maxeval'] = 10000 ①
option['maxtime'] = 60*60*24 ②
① 最大函数评估次数
② 最大时间限制(以秒为单位,例如,1 天 = 606024)
我们设置打印选项如下:
option['printeval'] = 1000 ①
option['save2file'] = 1 ②
① 打印当前最佳解的打印频率
② 保存屏幕和解决方案[0 为否,1 为是]
MIDACO 提供并行评估多个解候选人的选项(也称为协同评估或细粒度并行化)。根据 MIDACO 用户手册,对于并行化因子P = 10,潜在加速约为 10 倍,而对于并行化因子P = 100,潜在加速约为 70 倍。我们可以设置并行化因子如下:
option['parallel'] = 0 ①
① 序列:0 或 1,并行:2,3,4,5,6,7,8...
我们现在可以运行midaco来解决问题:
solution = midaco.run( problem, option, key)
以下代码片段用于打印 PSO 求解器获得的解:
soln = solution['x']
supply_num=len(amount_supply) ①
demand_num=len(amount_demand) ②
for i in range(supply_num): ③
print(f"Supply School({supply_schools[i]}): {' +
➥ '.join(['soln['+str(j)+']' for j in range(i*supply_num,
➥ (i+1)*supply_num)])} <= {amount_supply[i]} or {' + '.join(map(str,
➥ soln[i*supply_num:(i+1)*supply_num]))} <= {amount_supply[i]} or
➥ {sum(soln[i*supply_num:(i+1)*supply_num])} <= {amount_supply[i]}")
for j in range(demand_num): ④
print(f"Demand School({demand_schools[j]}): {' + '.join(['soln['+str(i*demand_num+j)+']' for i in range(demand_num)])} >=
➥ {amount_demand[j]} or {' + '.join(map(str, [soln[i*demand_num+j] for i
➥ in range(demand_num)]))} >= {sum(soln[i*demand_num+j] for i in
➥ range(demand_num))} or {sum(soln[i*demand_num+j] for i in
➥ range(demand_num))} >= {amount_demand[j]}")
print(f"Shipping cost = {solution['f']} $") ⑤
① 供应点数量
② 需求点数量
③ 打印每个供应点。
④ 打印每个需求点。
⑤ 打印运输成本。
代码将产生如下输出:
Supply School(1): soln[0] + soln[1] <= 20 or 0.0 + 30.0 <= 20 or 30.0 <= 20
Supply School(3): soln[2] + soln[3] <= 30 or 15.0 + 15.0 <= 30 or 30.0 <= 30
Demand School(2): soln[0] + soln[2] >= 5 or 0.0 + 15.0 >= 15.0 or 15.0 >= 5
Demand School(4): soln[1] + soln[3] >= 45 or 30.0 + 15.0 >= 45.0 or 45.0 >= 45
Shipping cost = [1919.3192442452619] $
以下代码片段可用于使用 folium 可视化空间地图的解决方案:
def normalize(lst): ①
s = sum(lst)
return list(map(lambda x: (x/s)*10, lst))
soln_normalized = normalize(soln) ②
colors = ['cyan', 'brown', 'orange', 'purple'] ③
m = folium.Map(location=[location.latitude, location.longitude],
➥ zoom_start=15, scrollWheelZoom=False, dragging=False) ④
for i, coord in zip(supply_schools, supply_coords):
folium.Marker(location=coord, icon=folium.Icon(icon="home",
➥ color='red'), popup=f'Supply School {i+1}').add_to(m) ⑤
for i, coord in zip(demand_schools, demand_coords):
folium.Marker(location=coord, icon=folium.Icon(icon="flag",
➥ color='blue'), popup=f'Demand School {i+1}').add_to(m) ⑥
for i in range(len(supply_schools)): ⑦
for j in range(len(demand_schools)):
soln_value = soln[i*len(demand_schools) + j]
folium.PolyLine(locations=[supply_coords[i], demand_coords[j]],
➥ color=colors[i % len(colors)],
➥ weight=5*soln_normalized[i*len(demand_schools) + j],
➥ popup=folium.Popup(f'# of microscopes: {soln_value}')).add_to(m)
m ⑧
① 标准化函数。
② 标准化解数组。
③ 定义颜色列表。
④ 创建以多伦多市中心为中心的地图。
⑤ 为供应学校添加标记。
⑥ 为需求学校添加标记。
⑦ 在供应和需求学校之间添加线条(边)。
⑧ 显示地图。
图 C.36 显示了使用 MIDACO 获得的解决方案。

图 C.36 MIDACO 获得的供需问题解决方案
对于更多学校,您可以获得无限许可证或请求免费的无限制版本学术试用,该版本可以支持多达 100,000 个变量。
C.10 第十一章:监督学习和无监督学习
C.10.1 练习
- 多项选择和真/假:为以下每个问题选择正确答案。
1.1. 机器学习算法的传统类别有哪些?
b. 监督的,无监督的,非增强的
b. 监督的,混合的,增强的
c. 监督的,无监督的,混合的,增强的
d. 无监督的,半监督的,混合的
1.2. Kohonen 图使用监督学习进行训练,以产生训练样本输入空间的二维表示。
a. 正确
b. 错误
1.3. 监督学习中常见的任务有哪些?
a. 聚类和数据降维
b. 分类和回归
c. 特征提取和异常检测
d. 维度约简和标准化
1.4. 无监督学习中的聚类任务是什么?
a. 根据某些相似性度量分组对象
b. 在自动驾驶汽车中识别交通标志
c. 将输入特征映射到已知标签或类别
d. 通过反馈循环优化模型性能
1.5. 指针网络(Ptr-Net)模型旨在解决传统序列到序列(seq2seq)模型的特定局限性,尤其是在涉及可变长度输出序列的任务中。
a. 正确
b. 错误
1.6. 在强化学习中,学习代理是如何学习做出决策的?
a. 通过最小化预测类别与实际类别之间的误差
b. 通过在输入数据中识别聚类
c. 通过在环境中通过行动最大化累积奖励
d. 通过将输入特征映射到已知标签或类别
1.7. 深度学习(DL)在机器学习中能够实现什么?
a. 在不同抽象层次上的特征表示学习
b. 基于标记数据对不同对象的分类
c. 基于某些度量将相似数据点分组
d. 在交互环境中基于奖励的决策
1.8. 图嵌入学习从低维连续域到离散高维图域的映射。
a. 正确
b. 错误
1.9. 深度学习(DL)是如何减少对大量数据预处理的需求的?
a. 通过使用大量未标记数据进行训练
b. 通过在反馈循环中的交互学习
c. 通过近似数据与已知标签之间的映射函数
d. 通过从原始数据中自动学习判别性特征
1.10. 为什么图结构数据在组合优化领域很重要?
a. 它有助于最大化累积奖励
b. 它有助于在数据与标签之间映射函数
c. 它捕捉并表示元素之间的关系和约束
d. 它根据相似性度量将对象分组
2. 将表 C.17 中显示的术语与描述进行匹配。
表 C.17
| 术语 | 描述 |
|---|---|
| 1. 自组织映射(SOM) | a. 一个多边形,它完全包围给定的一组点,具有最大面积和最小边界或周长。 |
| 2. 凸包 | b. 一种机制,允许模型动态地优先考虑输入中哪些部分对于它试图预测的每个输出是最相关的,使其在理解上下文和减少长输入序列的混淆方面更加有效。 |
| 3. 指针网络(Ptr-Net) | c. 一种使用无监督学习训练的人工神经网络,它产生输入空间训练样本的低维(通常是二维)离散表示。 |
| K-hop 邻域 | d. 距离小于或等于 K 的一组相邻节点 |
| 5. 注意力机制 | e. 一种用于处理可变大小输入数据序列的神经网络架构 |
3. 使用自组织映射(SOM)从纽约市出发访问 20 个主要美国城市的最短路径。城市名称和 GPS 纬度和经度坐标如下:
• 纽约市(40.72, –74.00)
• 费城(39.95, –75.17)
• 巴尔的摩(39.28, –76.62)
• 夏洛特(35.23, –80.85)
• 孟菲斯(35.12, –89.97)
• 杰克逊维尔(30.32, –81.70)
• 休斯顿(29.77, –95.38)
• 奥斯汀(30.27, –97.77)
• 圣安东尼奥(29.53, –98.47)
• 福特沃斯(32.75, –97.33)
• 达拉斯(32.78, –96.80)
• 圣地亚哥(32.78, –117.15)
• 洛杉矶(34.05, –118.25)
• 圣何塞(37.30, –121.87)
• 旧金山(37.78, –122.42)
• 印第安纳波利斯(39.78, –86.15)
• 凤凰城(33.45, –112.07)
• 辛辛那提(39.98, –82.98)
• 芝加哥(41.88, –87.63)
• 底特律(42.33, –83.05)
4. 调优超参数可以显著提高机器学习模型的表现。在列表 11.6 中调整超参数,并观察不同测试数据集对 ConvexNet 模型性能的影响。需要调整的超参数包括
• 模型的输入特征数量
• 嵌入维度数量
• 模型中隐藏单元的数量
• 多头自注意力机制中的注意力头数量
• 模型中的层数
• Dropout 概率
• 训练的迭代次数
• 训练过程中使用的批量大小
• 优化器的学习率
C.10.2 解决方案
1. 多选题和判断题
1.1. c) 监督的、无监督的、混合的、强化学习
1.2. b) 错误(Kohonen 图使用无监督学习进行训练,以产生训练样本输入空间的低维表示,而不是监督学习。)
1.3. b) 分类和回归
1.4. a) 基于某些相似性度量分组对象
1.5. a) 正确
1.6. c) 通过在环境中采取行动最大化累积奖励
1.7. a) 在不同抽象层次上的特征表示学习
1.8. b) 错误(图嵌入学习从离散的高维图域到低维连续域的映射,而不是相反。)
1.9. d) 通过从原始数据中自动学习判别特征
1.10. c) 它捕捉并表示元素之间的关系和约束
2. 1-c, 2-a, 3-e, 4-d, 和 5-b。
3. 本列表使用 MiniSom。MiniSom 是基于 Numpy 的最小化实现的自组织映射(SOM)。您可以使用以下命令安装此库:!pip install minisom。
下一个列表展示了如何使用自组织映射解决 20 个城市 TSP 问题。我们首先导入库和模块,定义感兴趣的城市,并计算城市对之间的哈夫曼距离。
列表 C.20 使用自组织映射(SOM)解决 TSP 问题
import numpy as np
import pandas as pd
from collections import defaultdict
from haversine import haversine
import networkx as nx
import matplotlib.pyplot as plt
import random
from minisom import MiniSom
cities = {
'New York City': (40.72, -74.00),
'Philadelphia': (39.95, -75.17),
'Baltimore': (39.28, -76.62),
'Charlotte': (35.23, -80.85),
'Memphis': (35.12, -89.97),
'Jacksonville': (30.32, -81.70),
'Houston': (29.77, -95.38),
'Austin': (30.27, -97.77),
'San Antonio': (29.53, -98.47),
'Fort Worth': (32.75, -97.33),
'Dallas': (32.78, -96.80),
'San Diego': (32.78, -117.15),
'Los Angeles': (34.05, -118.25),
'San Jose': (37.30, -121.87),
'San Francisco': (37.78, -122.42),
'Indianapolis': (39.78, -86.15),
'Phoenix': (33.45, -112.07),
'Columbus': (39.98, -82.98),
'Chicago': (41.88, -87.63),
'Detroit': (42.33, -83.05)
} ①
distance_matrix = defaultdict(dict) ②
for ka, va in cities.items(): ②
for kb, vb in cities.items(): ②
distance_matrix[ka][kb] = 0.0 if kb == ka else haversine((va[0],
➥ va[1]), (vb[0], vb[1])) ②
distances = pd.DataFrame(distance_matrix) ③
city_names=list(distances.columns) ③
distances=distances.values ④
① 定义 20 个主要美国城市的纬度和经度。
② 基于纬度和经度坐标创建哈夫曼距离矩阵。
③ 将距离字典转换为数据框。
④ 获取城市对之间的哈弗辛距离。
我们定义一个 SOM 来解决 TSP 实例。这个 SOM 是 1D 的,具有N个神经元。输入数据的维度是 2(纬度和经度坐标)。sigma 参数用于高斯邻域函数。此参数控制训练期间邻近神经元影响范围的扩散。学习率决定了训练期间权重更新的步长。训练期间使用的邻域函数设置为高斯,这意味着邻近神经元的影响随着距离的增加而减小。随机数生成器的种子设置为 50,以确保结果的再现性。
N_neurons = city_count*2 ①
som = MiniSom(1, N_neurons, 2, sigma=10, learning_rate=.5,
neighborhood_function='gaussia n', random_seed=50) ②
som.random_weights_init(points) ③
① 设置 1D SOM 的神经元(节点)数量。
② 创建一个具有 1xN 神经元网格的自组织图。
③ 初始化权重。
以下代码片段生成一组可视化,以显示 SOM 训练的进度:
plt.figure(figsize=(10, 9))
for i, iterations in enumerate(range(5, 61, 5)):
som.train(points, iterations, verbose=False, random_order=False)
plt.subplot(3, 4, i+1)
plt.scatter(x,y)
visit_order = np.argsort([som.winner(p)[1] for p in points])
visit_order = np.concatenate((visit_order, [visit_order[0]]))
plt.plot(points[visit_order][:,0], points[visit_order][:,1])
plt.title("iterations: {i};\nerror: {e:.3f}".format(i=iterations, ➥ e=som.quantization_error(points)))
plt.xticks([])
plt.yticks([])
plt.tight_layout()
plt.show()
图 C.37 显示了可视化的图表。

图 C.37 SOM 训练进度随迭代次数增加
C.11 第十二章:强化学习
C.11.1 练习
1. 多选题和判断题:为以下每个问题选择正确的答案。
1.1. 在强化学习中,“奖励”这个术语代表什么?
a. 执行动作的惩罚
b. 从环境中获得的即时反馈
c. 执行特定动作的概率
d. 代理采取的步数
1.2. 强化学习的目标是
a. 最小化累积奖励
b. 找到到达目标状态的最短路径
c. 学习最优策略以最大化累积奖励
d. 实现确定性环境
1.3. 以下哪个强化学习算法被认为是无模型的?
a. 专家迭代
b. 近端策略优化(PPO)
c. 想象力增强代理(I2A)
d. 以上皆非
1.4. 强化学习中“折扣因子”的概念用于
a. 减少状态空间的大小
b. 减少从环境中获得的奖励
c. 平衡即时奖励和未来奖励的重要性
d. 鼓励探索而非利用
1.5. 以下哪个强化学习算法被认为是离线策略 RL 方法?
a. Q 学习
b. 双延迟深度确定性策略梯度(TD3)
c. 深度确定性策略梯度(DDPG)
d. 近端策略优化(PPO)
1.6 PPO-clip 和 PPO-penalty 是策略梯度方法的两种变体,旨在解决训练过程中可能出现的潜在不稳定性。
a. 正确
b. 错误
1.7. 以下哪种多臂老丨虎丨机策略在每次试验中随机选择一个老丨虎丨机,而不考虑过去的结果?
a. 仅利用的贪婪策略
b. ε-贪婪策略
c. 上界置信(UCB)策略
d. 仅探索策略
1.8. 在ε-贪婪策略中,代理如何平衡探索和利用?
a. 它总是选择估计平均奖励最高的机器。
b. 它以一定的概率(ε)随机选择一台机器,否则选择估计平均奖励最高的机器。
c. 在每次试验中,它对所有机器进行平等探索。
d. 它只专注于利用当前最佳机器。
1.9. 在多臂老丨虎丨机问题中,“遗憾”衡量的是什么?
a. 最大可能奖励与从每个选定的机器获得的奖励之间的差异
b. 代理选择探索新机器的次数
c. 在相同单一状态下所花费的总时间
d. 代理可用的总臂或动作数量
1.10. 在强化学习的背景下,马尔可夫决策过程(MDP)代表什么?
a. 一种在决策时不考虑状态转换的过程
b. 使用标记数据集进行监督学习的方法
c. 一种在不确定性下的规划数学框架,其中动作以一定的概率影响未来的状态
d. 一种用于聚类数据的优化算法
2. 假设你是一位数字营销人员,正在运行在线广告活动。你可以向用户展示几个广告变体,每个广告变体都有自己的点击率(CTR)或转化率。点击率衡量用户点击链接的频率,而转化率衡量用户在点击链接后完成预期动作的频率,例如进行购买、注册通讯录或完成表格。你的目标是通过对最有效的广告变体进行选择,最大化用户参与度或转化率。
假设你有三种广告变体,分别用臂 A1、A2 和 A3 表示。每个广告变体都有一个与之相关的点击率或转化率的概率分布,分别表示为 Q1、Q2 和 Q3。这些概率分布代表了用户点击每个广告变体的可能性。在每一个时间步 t,你需要选择一个广告变体 A 来向用户展示。当广告变体 A 被展示时,用户会与之互动,你观察到结果,这可能是一个点击或转化。结果是从概率分布 Q(A)中抽取的,表示广告变体 A 的点击或转化可能性。假设三个概率分布 Q1、Q2 和 Q3 是具有均值{7, 10, 6}和标准差{0.45, 0.65, 0.35}的正态分布。你的目标是最大化一系列广告展示中的累积点击数(比如说 10,000 次广告展示)。请编写 Python 代码实现ε-贪婪策略,以确定在每一个时间步选择哪个广告变体进行展示,基于估计的点击率。
3. 出租车环境基于出租车或叫车问题,其中出租车必须从某个位置接乘客,并在另一个指定位置放下乘客。代理的目标是学习一种策略,通过网格导航出租车来接送乘客,同时最大化累积奖励。当场景开始时,出租车位于一个随机方块,乘客位于一个随机位置。出租车驶向乘客的位置,接上乘客,然后驶向乘客的目的地(四个指定位置之一),并放下乘客。一旦乘客被放下,场景结束。状态、动作和奖励如下:
• 状态(观察空间包括 500 个离散状态):25 个出租车位置(5×5 网格世界中的任何位置);5 个乘客位置(0:红;1:绿;2:黄;3:蓝;4:在出租车内)和 4 个目的地(0:红;1:绿;2:黄;3:蓝)。因此,此出租车环境共有 5×5×5×4=500 个可能的状态。
• 动作(动作空间包括 6 个离散动作):0 = 向南移动;1 = 向北移动;2 = 向东移动;3 = 向西移动;4 = 接乘客,5 = 放下乘客。
• 奖励:+20(成功放下乘客的高正奖励);–10(非法执行接送动作的惩罚,例如如果代理试图在错误的位置放下乘客),以及–1(每次时间步后未能到达目的地的小负奖励,以模拟延迟)。
编写 Python 代码以展示如何使用 A2C 学习此环境的最佳策略。尝试使用向量化环境,其中多个独立环境堆叠成一个单一环境。向量化环境允许你并行运行多个环境实例。与每步在一个环境中训练 RL 代理相比,它允许你每步在n个环境中训练。例如,如果你想运行四个并行环境,你可以在创建环境时指定此数字,如下所示:env = make_vec_env("Taxi-v3", n_envs=4, seed=0。
4. 在为计划中的航班做准备时,航空公司的航班运营团队负责根据共享上下文选择最佳飞行路线和服务。共享上下文代表飞行类型(国内或国际)和乘客类型(商务舱、经济舱或混合)。航班运营团队必须决定飞行路线、餐饮服务和机上娱乐的最佳策略。选项如下:
• 飞行路线—最直接路线、可能更长但更省油的路线,或避开湍流的路线,但可能需要更多时间
• 餐饮服务—包含多个选项的完整餐点、选项较少的简单餐点,或仅提供小吃和饮料
• 机上娱乐—电影和音乐、机上 Wi-Fi 服务,或两者的组合
奖励是针对给定航班(共享上下文)所选选项的满意度。奖励函数接收共享上下文(航班类型和乘客等级)以及每个选项所选动作(所选航班路线、餐食服务和机上娱乐)作为参数。为了模拟现实世界的场景和复杂性,我们在奖励值中注入了正常噪声。目标是选择最佳动作,从可用的组合动作中选择,以最大化总奖励。编写 Python 代码以训练和测试此问题的上下文赌博机。
5. 列表 11.4 展示了如何使用预先训练有监督方法或强化学习方法的 ML 模型解决 TSP 问题。将预训练的监督学习模型 sl-ar-var-20pnn-gnn-max_20200308T172931 替换为预训练的 RL 模型 rl-ar-var-20pnn-gnn-max_20200313T002243,并报告您的观察结果。
6. gym-electric-motor(GEM)包是一个 Python 工具箱,用于模拟和控制电机。它是基于 OpenAI Gym 环境构建的,适用于经典控制模拟和强化学习实验。使用 GEM 定义一个永磁同步电机(PMSM)环境,如图 C.38 所示。有关 PMSM 和 GEM 的更多信息,请参阅 Traue 等人撰写的文章“向智能电机控制强化学习环境工具箱迈进”[10]。

图 C.38 Traue 等人文章[9]中描述的 gym-electric-motor(GEM)环境
列表 C.24,在本书的 GitHub 仓库中提供,是 PPO 在电机控制中的简化实现。此代码用于训练一个控制模型(PPO 强化学习代理)以控制永磁同步电机的电流。此代理主要控制将供电电流转换为流入电机的电流的转换器。在此算法的不同参数上进行实验,并考虑尝试 Stable-Baselines3(SB3)中可用的其他 RL 模型。
C.11.2 解答
1. 多项选择题和判断题
1.1. b) 从环境中获得的即时反馈
1.2. c) 学习一个最优策略以最大化累积奖励。
1.3. b) 近端策略优化(PPO)
1.4. c) 平衡即时奖励和未来奖励的重要性。
1.5. d) 近端策略优化(PPO)
1.6. a) 正确
1.7. d) 探索性策略
1.8. b) 以一定的概率(epsilon)随机选择一台机器,否则选择估计平均奖励最高的机器。
1.9. a) 最大可能奖励与从每个所选机器获得的奖励之间的差异
1.10. c) 在不确定性下规划的一个数学框架,其中动作以一定的概率影响未来状态
2. 列表 C.21 展示了一种ε-greedy 策略的实现,该策略基于估计的点击率在每个时间步确定显示哪个广告变体。在此代码片段中,模拟了 10,000 次广告展示。每次展示后,每个广告变体的点击率估计都会更新。
列表 C.21 使用 MAB 进行在线广告
import numpy as np
num_arms = 3 ①
num_trials = 10000 ②
mu = [7, 10, 6] ③
sigma = [0.45, 0.65, 0.35] ③
counts = np.zeros(num_arms) ④
rewards = np.zeros(num_arms) ④
a = np.random.choice(num_arms) ⑤
eps = 0.1 ⑥
for t in range(num_trials):
if np.random.rand() > eps: ⑦
a = np.argmax(rewards / (counts + 1e-5)) ⑧
else: ⑨
a = np.random.choice(num_arms) ⑨
reward = np.random.normal(mu[a], sigma[a])
counts[a] += 1
rewards[a] += reward
estimates = rewards / counts ⑩
⑩
print("Estimated click-through rates: ", estimates) ⑩
① 初始化臂的数量(动作)。
② 初始化试验次数(广告)。
③ 每个臂的概率分布
④ 每个臂的计数器
⑤ 选择一个初始臂。
⑥ 设置ε-greedy 算法的ε值。
⑦ 利用
⑧ 添加一个小常数以避免除以零。
⑨ 探索
⑩ 计算并打印估计的点击率。
在此脚本中,counts跟踪每个广告变体显示的次数,而rewards跟踪每个广告变体的总点击次数。脚本结束时,计算并打印每个广告变体的估计点击率。
3. 列表 C.22 展示了使用 A2C 学习出租车问题最优策略的步骤。此代码使用 Stable-Baselines3(SB3),一个强化学习库,在 Taxi-v3 环境中使用 A2C 训练一个智能体。SB3 函数 make_vec_env 用于创建一个向量化环境,可以在同一进程中运行多个并行环境。SB3 函数 evaluate_policy 用于评估智能体的学习策略。
列表 C.22 使用 A2C RL 调度出租车
from stable_baselines3.common.env_util import make_vec_env
from stable_baselines3 import A2C
from stable_baselines3.common.evaluation import evaluate_policy
import matplotlib.pyplot as plt
from IPython.display import display, clear_output
env = make_vec_env("Taxi-v3", n_envs=1, seed=0) ①
print('Number of states:{}'.format(env.observation_space)) ②
print('Number of actions:{}'.format(env.action_space)) ②
model = A2C(policy="MlpPolicy", env=env, verbose=True) ③
model.learn(total_timesteps=10000, progress_bar=True) ④
① 创建一个包含单个并行环境(n_envs=1)的向量化环境。
② 打印环境的观察和动作空间。
③ 创建一个以 MlpPolicy 作为策略网络的 A2C 智能体。
④ 在 Taxi-v3 环境中训练 A2C 智能体 10,000 个时间步。
训练完成后,智能体将学会一个最优策略,用于在 Taxi-v3 环境中导航,以有效地在正确的位置接载和放下乘客。以下代码片段通过使用训练好的 A2C 智能体渲染 Taxi-v3 环境来可视化学习到的策略。
images = [] ①
vec_env = model.get_env() ②
obs = vec_env.reset() ③
for i in tqdm(range(300)):
action, _states = model.predict(obs, deterministic=True) ④
obs, rewards, dones, info = vec_env.step(action) ⑤
state_img = vec_env.render("rgb_array") ⑥
fig = plt.figure()
plt.imshow(state_img)
plt.axis('off')
display(fig)
images.append(fig)
clear_output(wait=True) ⑦
plt.close()
① 创建一个空列表以存储渲染环境的帧(图像)。
② 获取与模型关联的环境。
③ 重置环境并在重置后获取初始观察。
④ 根据当前观察预测一个动作。
⑤ 获取新的观察和奖励。
⑥ 将环境渲染为 RGB 图像。
⑦ 清除下一张图像的输出。
- 列表 C.23 展示了使用 Vowpal Wabbit Python 库实现航空公司航班运营上下文多臂老丨虎丨机的代码。共享上下文由两个列表定义:flight_types 和 passenger_classes。老丨虎丨机问题的可能选择或动作由 flight_routes、meal_services 和 entertainment_options 定义。reward_function 计算与特定航班路线、餐食服务和娱乐选项组合相关的奖励。奖励使用具有不同均值的正态分布生成。标准差(尺度)设置为 0.05,意味着奖励是从具有少量方差的正态分布中采样的。
列表 C.23 航空公司航班运营的上下文多臂老丨虎丨机
import vowpalwabbit
import torch
import matplotlib.pyplot as plt
import pandas as pd
import random
import numpy as np
flight_types = ['domestic', 'international'] ①
passenger_classes = ['business', 'economy', 'mix'] ①
flight_routes = ['direct', 'fuel_efficient', 'turbulence_avoidance'] ②
meal_services = ['full_meal', 'simple_meal', 'snacks_beverages'] ②
entertainment_options = ['movies_music', 'in_flight_wifi', 'combo'] ②
def reward_function(shared_context, flight_route, meal_service,
➥ entertainment_option): ③
if flight_route == 'direct':
route_reward = np.random.normal(loc=0.9, scale=0.05)
elif flight_route == 'fuel_efficient':
route_reward = np.random.normal(loc=0.8, scale=0.05)
else:
route_reward = np.random.normal(loc=0.7, scale=0.05)
if meal_service == 'full_meal':
meal_reward = np.random.normal(loc=0.9, scale=0.05)
elif meal_service == 'simple_meal':
meal_reward = np.random.normal(loc=0.8, scale=0.05)
else:
meal_reward = np.random.normal(loc=0.7, scale=0.05)
if entertainment_option == 'movies_music':
entertainment_reward = np.random.normal(loc=0.9, scale=0.05)
elif entertainment_option == 'in_flight_wifi':
entertainment_reward = np.random.normal(loc=0.8, scale=0.05)
else
entertainment_reward = np.random.normal(loc=0.7, scale=0.05)
reward = (route_reward + meal_reward + entertainment_reward) / 3.0
return reward
① 设置共享上下文。
② 设置可能的选择/动作选项。
③ 计算特定选项组合的奖励。
作为延续,定义以下两个效用函数。generate_combinations生成航班路线、餐食服务和娱乐选项的组合及其相关描述。sample_truck_pmf根据概率质量函数(PMF)进行采样:
def generate_combinations(shared_context, flight_routes, meal_services,
➥ entertainment_options):
examples = [f"shared |FlightType {shared_context[0]} PassClass
➥ {shared_context[1]}"]
descriptions = []
for route in flight_routes:
for meal in meal_services:
for entertainment in entertainment_options:
examples.append(f"|Action route={route} meal={meal}
➥ entertainment={entertainment}")
descriptions.append((route, meal, entertainment))
return examples, descriptions
def sample_truck_pmf(pmf):
pmf_tensor = torch.tensor(pmf)
index = torch.multinomial(pmf_tensor, 1).item()
chosen_prob = pmf[index]
return index, chosen_prob
我们现在可以使用 Vowpal Wabbit(VW)库创建上下文多臂老丨虎丨机,并在指定的迭代次数上评估其性能。这个上下文多臂老丨虎丨机将在不同航班类型和乘客类别的情况下做出决策(选择动作),以最大化预期奖励:
cb_vw = vowpalwabbit.Workspace(
"--cb_explore_adf --epsilon 0.2 --interactions AA AU AAU -l 0.05 –
➥power_t 0",
quiet=True,
)
创建上下文多臂老丨虎丨机的关键参数如下:
• --cb_explore_adf—启用基于动作相关特征的上下文多臂老丨虎丨机探索
• --epsilon 0.2—将探索率设置为 0.2,意味着老丨虎丨机将以 0.2 的概率(20%的时间)探索非贪婪动作
• --interactions AA AU AAU—指定特征 AA、AU 和 AAU 之间的三重交互
• -l 0.05—将学习率设置为 0.05,这控制了学习过程中的步长大小
• --power_t 0—指定学习率是常数(没有学习率衰减)
• num_iterations = 2500
以下代码片段允许我们运行创建的上下文多臂老丨虎丨机来做出决策:
num_iterations = 2500 ①
cb_rewards = [] ②
for _ in range(num_iterations):
shared_context = (random.choice(flight_types),
➥ random.choice(passenger_classes)) ③
examples, indices = generate_combinations(
shared_context, flight_routes, meal_services, entertainment_options
) ④
cb_prediction = cb_vw.predict(examples) ⑤
chosen_index, prob = sample_truck_pmf(cb_prediction) ⑥
route, meal, entertainment = indices[chosen_index] ⑦
reward = reward_function(shared_context, route, meal, entertainment)
cb_rewards.append(reward) ⑧
examples[chosen_index + 1] = f"0:{-1*reward}:{prob} {examples[chosen_index + 1]}" ⑨
cb_vw.learn(examples) ⑩
cb_vw.finish() ⑪
① 设置迭代次数。
② 初始化一个空列表以存储每个迭代的奖励。
③ 在每次迭代中随机选择一个共享上下文。
④ 根据所选共享上下文生成所有可能的航班路线、餐食服务和娱乐选项组合。
⑤ 预测每个动作(组合)的预期奖励。
⑥ 从预测的奖励中采样一个索引。
⑦ 获取航班路线、餐食服务和娱乐的个别选择。
⑧ 计算与所选动作(组合)和共享上下文相关的奖励。
⑨ 将奖励信息附加到示例中。
⑩ 从更新的示例中学习。
⑪ 关闭工作空间并最终化学习过程。
我们可以按以下方式打印训练过程中的平均奖励:
plt.plot(pd.Series(cb_rewards).expanding().mean())
plt.xlabel("Iterations")
plt.ylabel("Average reward")
plt.show()
图 C.39 展示了学习过程中每次迭代获得的平均奖励的进展情况。
图 C.39 学习过程中每次迭代的平均奖励
书中 GitHub 仓库提供的 C.23 完整列表定义了一个test_model函数,然后使用给定的共享上下文测试了上下文化多臂老丨虎丨机模型。test_model函数被定义为通过模拟给定共享上下文的一个决策实例来测试上下文化多臂老丨虎丨机模型。它接受四个参数——shared_context、flight_routes、meal_services和entertainment_options:
def test_model(shared_context, flight_routes, meal_services,
➥ entertainment_options):
examples, _ = generate_combinations(shared_context, flight_routes,
➥ meal_services, entertainment_options) ①
cb_prediction = cb_vw.predict(examples) ②
chosen_index, prob = sample_truck_pmf(cb_prediction) ③
chosen_action = examples[chosen_index + 1] ④
route, meal, entertainment = indices[chosen_index] ⑤
expected_reward = reward_function(shared_context, route, meal,
➥ entertainment) ⑥
print("Chosen Action:", chosen_action) ⑦
print("Expected Reward:", expected_reward) ⑦
test_shared_context = ('domestic', 'business') ⑧
test_model(test_shared_context, flight_routes, meal_services,
➥ entertainment_options) ⑨
① 根据给定的共享上下文,生成所有可能的航班路线、餐食服务和娱乐选项的组合。
② 根据提供的示例预测每个动作(组合)的预期奖励。
⑥ 计算与所选动作和共享上下文相关的预期奖励。
打印选定的动作和预期奖励。
⑨ 测试该特定情境下上下文老丨虎丨机模型的决策过程。
此代码将为给定上下文生成如下输出:
Chosen Action: |Action route=fuel_efficient meal=full_meal entertainment=movies_music
Expected Reward: 0.87
model = "learning_tsp/pretrained/tsp_20-50/rl-ar-var-20pnn-gnn-max_20200313T002243"
# model = "learning_tsp/pretrained/tspsl_20-50/sl-ar-var-20pnn-gnn-max_20200308T172931"
6. 列表 C.24,可在本书的 GitHub 仓库中找到,提供了用于电机控制的 PPO 算法的简化实现。尝试调整此算法的不同参数,并考虑尝试 SB3 中可用的其他强化学习模型,例如优势演员-评论家(A2C)、软演员-评论家(SAC)、深度确定性策略梯度(DDPG)、深度 Q 网络(DQN)、事后经验重放(HER)和双延迟 DDPG(TD3)。
第十三章:参考文献
第一章
[1] Herbert A. Simon, “不良结构问题的结构,” 人工智能 4, 第 3-4 期 (1973), 181–201.
第二章
[1] M. Held 和 R.M. Karp, “动态规划方法解决排序问题,” 工业与应用数学学会会刊 10, 第 1 期 (1962), 196–210.
[2] M.T. Goodrich 和 R. Tamassia, 算法设计与应用 (Wiley, 2015), 513–514.
[3] N. Damavandi 和 S. Safavi-Naeini, “一种用于电路优化的混合进化规划方法,” IEEE 电路与系统 I:常规论文 52, 第 5 期 (2005), 902–910.
[4] P. Jensen 和 B. Jonathan, 运筹学模型与方法 (John Wiley & Sons, 2002).
[5] A. Khamis 和 M. Ashraf, “一种基于微分演化的全地形地面车辆车轮设计方法,” 2017 年 IEEE 国际自主机器人系统与竞赛会议(ICARSC), (IEEE, 2017).
[6] K. Veselić, “有限悬链线和拉格朗日方法,” SIAM 评论 37, 第 2 期 (1995), 224–229.
[7] J. Kalcsics 和 Roger Z. Ríos-Mercado, “区域划分问题,” 在 Gilbert Laporte, Stefan Nickel 和 Francisco Saldanha da Gama (编辑), 位置科学 (Springer, 2019), 705–743.
[8] S.M. Almufti, “元启发式算法的历史综述,” 科学世界国际杂志 7, 第 1 期 (2019), 1.
第四章
[1] M. Leighton, R. Wheeler, 和 C. Holte, “更快的最优和次优分层搜索,” 第四年组合搜索年度研讨会论文集 (巴塞罗那, 2011).
[2] P. Sanders 和 D. Schultes, “高速公路层次结构加速精确最短路径查询,” 算法:ESA 2005 (Springer, 2005), 568–579.
[3] R. Geisberger 等人, “收缩层次结构:道路网络中更快且更简单的分层路由,” 实验算法:WEA 2008 (Springer, 2008), 319–333.
第五章
[1] S. Kirkpatrick, C. Gelatt, 和 M. Vecchi, “模拟退火优化,” 科学 220, 第 4598 期 (1983 年 5 月 13 日), 671–680.
[2] Y. Xiang 等人, “广义模拟退火算法及其在汤姆逊模型中的应用,” 物理学报 A 233, 第 3 期 (1997), 216–220.
[3] L. Ingber, “自适应模拟退火(ASA):经验教训,” arXiv cs/0001018 (2000).
[4] X. Geng, Z. Chen, W. Yang, D. Shi, and K. Zhao, “基于自适应模拟退火算法和贪婪搜索的旅行商问题求解,” 应用软计算 11, 第 4 期 (2011 年 6 月), 3680-3689.
[5] B. Felgenhauer 和 F. Jarvis, “枚举可能的数独网格” (2005).
[6] E. Russell 和 F. Jarvis, “数独数学 II,” 数学视野 39, 第 2 期 (2006), 54–58.
第六章
[1] F. Glover, “禁忌搜索和自适应记忆编程—进展、应用和挑战,” 在 计算机科学与运筹学研究接口,由 R. Barr, R. Helgason 和 J. Kennington 编辑 (Springer, 1997), 1–75.
[2] F. Glover, M. Laguna 和 R. Marti, “禁忌搜索原理,” 在 T.F. Gonzalez 编著的 逼近算法与元启发式手册(Chapman & Hall,2007 年),第二十三章。
[3] M. Gendreau, “禁忌搜索简介,” 在 Fred Glover,Gary A. Kochenberger 编著的 元启发式手册(Springer US,2003 年)。
[4] R. Battiti 和 G. Tecchiolli, “使用反应性禁忌搜索训练神经网络,” IEEE 神经网络学报 6,第 5 期(1995 年),1185–1200 页。
第七章
[1] El-Ghazali Talbi, 元启发式:从设计到实现(John Wiley & Sons,2009 年)。
[2] Julian Blank 和 Kalyanmoy Deb, “Pymoo: Python 中的多目标优化,” IEEE Access 8 (2020),89497–89509 页。
第八章
[1] K. Deb 和 R.B. Agrawal, “模拟二进制交叉用于连续搜索空间,” 复杂系统 9,第 2 期(1995 年),115–148 页。
[2] K. Deb,K. Sindhya 和 T. Okabe, “用于实参数优化的自适应模拟二进制交叉,” 在 第 9 届遗传和进化计算年度会议论文集(2007 年 7 月),第 1187–1194 页。
[3] K. Deb, “使用进化算法进行多目标优化:简介,” 在 L. Wang,A.H. Ng,K. Deb 编著的 多目标进化优化在产品设计制造中的应用(Springer,2011 年)。
[4] E. Zitzler, “多目标优化的进化算法:方法和应用,” Tik-Schriftenreihe 30(瑞士联邦理工学院苏黎世,1999 年)。
[5] A.P. Wierzbicki, “在多目标优化中使用参考目标,” 在 第三次会议的多标准决策理论与应用:会议论文集(Springer 1980 年),第 468–486 页。
[6] Z. Gaing, “用于 AVR 系统中 PID 控制器最优设计的粒子群优化方法,” IEEE 能源转换学报 19,第 2 期(2004 年):384–391 页。
第九章
[1] J. Kennedy 和 R. Eberhart, “粒子群优化,” 在 IEEE 国际神经网络会议(ICNN'95)论文集 4 (1995),1942–1948 页。
[2] J. Kennedy 和 R. Eberhart, “粒子群算法的离散二进制版本,” 在 1997 年 IEEE 国际系统、人类和自动化会议:计算控制论与仿真 第 5 卷(1997 年),第 4104–4108 页。
[3] M. Clerc, “通过旅行商问题展示的离散粒子群优化,” 在 G. Onwubolu 和 B. Babu 编著的 工程中的新优化技术:模糊性和软计算研究 第 141 卷(Springer,2004 年),第 219–239 页。
[4] A. Ratnaweera,S.K. Halgamuge 和 H.C. Watson, “具有时变加速度系数的自组织分层粒子群优化器,” IEEE 进化计算学报 8,第 3 期(2004 年),240–255 页。
第十章
[1] Dervis Karaboga, “基于蜜蜂群体智能的数值优化方法,” 技术报告 TR06,第 200 卷(埃尔齐耶斯大学,2005 年),1–10 页。
第十一章
[1] S. Dehaene,“我们如何学习:为什么现在大脑的学习比任何机器都要好...”(企鹅出版社,2021 年)。
[2] T.M. Mitchell,“机器学习”(麦格劳-希尔,1997 年)。
[3] P. Domingos,“终极学习机器的探索:将重塑我们的世界”(基础书籍,2015 年)。
[4] A. Halevy, P. Norvig, 和 F. Pereira,“数据的不合理有效性”,IEEE Intelligent Systems 24, no. 2 (2009), 8–12.
[5] W. Hamilton, R. Ying, 和 J. Leskovec,“图上的表示学习:方法和应用”,arXiv preprint arXiv:1709.05584 (2017).
[6] K. Broadwater 和 N. Stillman,“图神经网络实战”(Manning,2023 年)。
[7] Oliver Lange 和 Luis Perez,“使用高级图神经网络进行交通预测”,Google DeepMind博客,www.deepmind.com/blog/traffic-prediction-with-advanced-graph-neural-networks.
[8] A. Vaswani 等人,“注意力即一切”,Advances in neural information processing systems,30 (2017).
[9] “Transformer 的注释”,哈佛 NLP,https://nlp.seas.harvard.edu/2018/04/03/attention.html.
[10] O. Vinyals, M. Fortunato, 和 N Jaitly,“指针网络”,arXiv preprint arXiv:1506.03134 (2015).
[11] L. Graham,“确定有限平面集合凸包的高效算法”,Information Processing Letters 1 (1972), 132–133.
[12] A. Jarvis,“关于在平面上识别有限点集的凸包”,Information Processing Letters 2 (1973), 18–21.
[13] C. Barber, D.P. Dobkin, 和 H. Huhdanpaa,“凸包的快速凸算法”,ACM Transactions on Mathematical Software 22, no. 4 (1996), 469–483.
[14] Y. Bengio, A. Lodi, 和 A. Prouvost,“组合优化中的机器学习:一个方法论全景”,欧洲运筹学杂志 290, no. 2 (2021), 405–421.
[15] A. Hottung, S. Tanaka, 和 K. Tierney,“用于集装箱预装问题的深度学习辅助启发式树搜索”,Computers & Operations Research 113 (2020).
[16] N. Vesselinova 等人,“在图上学习组合优化:一个包含网络应用的综述”,IEEE Access 8 (2020), 120,388–120,416.
[17] Chengrun Yang 等人,“大型语言模型作为优化器”,arXiv preprint arXiv:2309.03409 (2023).
[18] B. Amos,“关于在连续域上学习优化的摊销优化教程”,arXiv: 2202.00665 (2022).
[19] S. Cohen, B. Amos, 和 Y. Lipman,“黎曼凸势映射”,第 38 届国际机器学习会议论文集,机器学习研究论文集(2021 年),2028–2038.
[20] Chaitanya K. Joshi, Thomas Laurent, 和 Xavier Bresson,“图神经网络用于旅行商问题”,来自 2019 年 INFORMS 年度会议的“利用机器学习提升组合优化”会议。
[21] Chaitanya K. Joshi, Quentin Cappart, Louis-Martin Rousseau, Thomas Laurent, “学习旅行商问题需要重新思考泛化”的代码,第 27 届约束规划原理与实践国际会议 (CP 2021), GitHub 仓库,https://github.com/chaitjo/learning-tsp。
[22] Diego Vicente, “使用自组织图解决旅行商问题," Diego Vicente 博客文章, https://diego.codes/post/som-tsp/。
[23] M. McGough, “带有 Transformer 的指针网络,” 数据科学向导 (2021), https://towardsdatascience.com/pointer-networks-with-transformers-1a01d83f7543。
第十二章
[1] R. Sutton 和 A. Barto, 强化学习:导论,第二版 (麻省理工学院出版社,2018)。
[2] J. Schulman 等人, “近端策略优化算法,” arXiv 预印本 (2017), arXiv:1707.06347。
[3] C. Joshi, T. Laurent, 和 X. Bresson, “图神经网络在旅行商问题中的应用,” 在使用机器学习提升组合优化,NFORMS 年度会议 2019。
[4] M. Nazari 等人, “解决车辆路径问题的强化学习,” arXiv 预印本 (2018), arXiv:1802.04240。
[5] A. Delarue, R. Anderson, 和 C. Tjandraatmadja, “组合动作的强化学习:车辆路径规划的应用,” arXiv 预印本 (2020), arXiv:2010.12001。
[6] C. Wan, Y. Li, 和 J. Wang, “RLOR:用于运筹学的灵活深度强化学习框架,” arXiv 预印本 (2023), arXiv:2303.13117。
[7] A. Alabbasi, A. Ghosh, 和 V. Aggarwal, “DeepPool: 基于深度强化学习的共享出行分布式无模型算法,” IEEE 智能交通系统杂志 20, 第 12 期 (2019), 4714–4727。
[8] C. Jiayu 等人, “DeepFreight: 基于深度强化学习的无模型多转运货算法,” 在 国际自动规划与调度会议论文集 31 (2021), 510–518。
[9] T. Oda 和 C. Joe-Wong, “MOVI:动态车队管理的无模型方法,” 在 IEEE INFOCOM 2018:IEEE 计算机通信会议 (2018), 2708–2716。
[10] J. Ruan 等人, “基于深度强化学习的交通信号控制,” 在 2023 IEEE 国际智能移动会议 (2023), 21–26。
[11] K. Lin 等人, “使用多智能体强化学习和进化策略进行合作速度限制控制,以改善混合交通中的通行能力,” 在 2023 IEEE 国际智能移动会议 (2023), 27–32。
[12] K. Li 和 J. Malik, “学习优化,” arXiv 预印本 (2016), arXiv:1606.01885。
[13] S. Toll 等人, “高效电梯算法,” 田纳西大学校长荣誉项目, 田纳西大学 (2020)。
[14] S. Schneider 等人,“mobile-env:无线移动网络中强化学习的开源平台”,在 NOMS 2022–2022 IEEE/IFIP 网络操作与管理研讨会 (2022)。GitHub 仓库:https://github.com/stefanbschneider/mobile-env/tree/main。
附录 A
[1] C. Guéret, C. Prins, 和 M. Sevaux,《线性规划》 (HAL, 2000)。
附录 C
[1] Jacques A. Ferland 和 Gilles Guénette,“学区划分问题的决策支持系统”,《运筹学》 38, 第 1 期 (1990), 15–21。
[2] X. Geng, Z. Chen, W. Yang, D. Shi, 和 K. Zhao,“基于自适应模拟退火算法和贪婪搜索解决旅行商问题”,《应用软计算》 11, 第 4 期 (2011 年 6 月), 3680-3689。
[3] N. Kamarudin 和 M. Ab Rashid,“考虑机器和工人约束的简单装配线平衡问题类型 1 (SALBP-1) 的建模”,《物理会议系列杂志》 1049, 第 1 期 (2018), 第 012037 页。
[4] K. Sutner,“线性细胞自动机和伊甸园花园”,《数学智能》 11, 第 2 期 (1989), 49–53。
[5] C. Guéret, C. Prins, 和 M. Sevaux,《线性规划》 (HAL, 2000)。
[6] A. Martin 等人,“群体智能的局部终止标准:局部随机扩散搜索与蚂蚁巢穴选择之间的比较”,在 《计算集体智能 XXXII 转录》 (2019), 140–166。
[7] R. Lovelace,“运输规划中地理分析的开源工具”,《地理系统杂志》 23 (2021), 547–578。
[8] M. Daskin,《网络与离散位置:模型、算法与应用》,第二版 (John Wiley, 2013)。
[9] W. Herbawi 和 M. Weber,“解决带时间窗口的动态拼车问题的遗传和插入启发式算法”,在 《第 14 届遗传与进化计算年度会议论文集》 (2012), 385–392。
[10] A. Traue 等人,“向智能电机控制强化学习环境工具箱迈进”,《IEEE 交易磁神经网络与学习系统》 33, 第 3 期 (2020), 919–928。另请参阅 gym-electric-motor (EGM) 软件包,可在 https://upb-lea.github.io/gym-electric-motor/index.html 获取。


浙公网安备 33010602011771号