你有没有好奇过,那些科幻电影里的机器人、游戏里的智能NPC,它们是如何在复杂的环境中穿梭自如,总能找到通往目标的路径的?这背后其实隐藏着强大的算法。
今天,我们将一起探索并亲手实现其中一种极具魅力的算法——粒子群优化(Particle Swarm Optimization, PSO)。我们将用它来解决一个经典的难题:在布满障碍的地图上,寻找一条从起点到终点的最优路径。
最棒的是,你不需要任何高深的数学背景,只需要一颗好奇的心和对Python的一点了解。让我们一起,把源于自然的鸟群智慧,变成触手可及的代码!
灵感来源:鸟群的觅食智慧
在深入代码之前,我们先来聊聊粒子群算法的灵感来源——鸟群觅食。
想象一下,在一个区域内有一群鸟在寻找食物,但区域里只有一个地方有食物。所有的鸟都不知道食物在哪里,但它们知道自己当前位置离食物有多远。那么,最有效的策略是什么呢?
- 相信自己:每只鸟都会记住自己到过的、离食物最近的位置。这个位置被称为“个体最优解 (pbest)”。
- 相信集体:所有鸟都会共享信息,它们都知道整个鸟群发现的、离食物最近的位置。这个位置被称为“全局最优解 (gbest)”。
- 调整飞行:在下一次飞行时,每只鸟都会结合三个因素来决定自己的方向和速度:
- 自己原来的飞行习惯(惯性)。
- 飞向“自己找到过的最佳位置”的趋势。
- 飞向“整个群体找到的最佳位置”的趋势。
通过这种简单的规则,整个鸟群不断迭代,逐步向食物的最终位置聚集,最终高效地找到目标。
在我们的路径规划问题中,我们将这个模型进行映射:
- 一只鸟(Particle):一条从起点到终点的潜在路径。
- 鸟的位置(Position):定义这条路径的一系列中间点坐标。
- 食物(Optimal Solution):一条最短且不碰撞障碍物的最佳路径。
- 离食物的距离(Fitness):一个衡量路径好坏的适应度值。路径越长、碰撞越多,适应度就越差。
用Python实现:代码全解析
现在,让我们把上面的思想转化为代码。整个项目结构清晰,主要分为环境构建、适应度函数、PSO主循环和可视化几个部分。
完整代码
下面是本次项目的完整Python代码。你可以先整体浏览一遍,我们将在后面分步解析它的每一部分。
import numpy as np
import matplotlib.pyplot as plt
import math
import random
# --- 1. 参数配置 ---
class PSOConfig:
"""粒子群算法的参数配置"""
num_particles = 100 # 粒子数量
max_iter = 200 # 最大迭代次数
grid_size = 30 # 地图大小 (30x30)
num_obstacles = 30 # 障碍物数量
# 路径表示: 路径由 N 个中间点构成
num_intermediate_points = 10 # 路径中间点的数量
# PSO参数
w = 0.8 # 惯性权重
c1 = 2.0 # 个体学习因子
c2 = 2.0 # 社会学习因子
# 适应度函数中的惩罚项
obstacle_penalty = 1000.0 # 障碍物碰撞惩罚
# --- 2. 环境与路径工具 ---
def create_grid(size, num_obstacles, start, end):
"""创建一个带有随机障碍物的栅格地图"""
grid = np.zeros((size, size))
# 随机放置障碍物
for _ in range(num_obstacles):
while True:
x, y = random.randint(0, size - 1), random.randint(0, size - 1)
# 确保障碍物不与起点或终点重合
if (x, y) != start and (x, y) != end and grid[x, y] == 0:
grid[x, y] = 1 # 1 代表障碍物
break
return grid
def decode_position_to_path(position, start, end):
"""将粒子的1D位置向量解码为2D路径点列表"""
path = [start]
# 将一维向量 position [x1, y1, x2, y2, ...] 转换成 [(x1, y1), (x2, y2), ...]
points = position.reshape(-1, 2)
for p in points:
path.append(tuple(p))
path.append(end)
return path
# --- 3. 适应度函数 (核心) ---
def calculate_fitness(path, grid):
"""
计算路径的适应度值(越小越好)
适应度 = 路径总长度 + 碰撞惩罚
"""
path_length = 0.0
collision_count = 0
# 遍历路径的每一段 (p1 -> p2)
for i in range(len(path) - 1):
p1 = np.array(path[i])
p2 = np.array(path[i+1])
# 计算段长度
path_length += np.linalg.norm(p2 - p1)
# 检查该段是否与障碍物碰撞
# 使用线性插值来检查路径段上的点
line_vec = p2 - p1
line_len = np.linalg.norm(line_vec)
if line_len > 0:
# 【核心修正】确保 num_samples 至少为 1,以防止除零错误
num_samples = max(1, int(line_len * 2)) # 采样密度
for j in range(num_samples + 1):
t = j / num_samples
check_point = p1 + t * line_vec
# 将浮点坐标转换为整数栅格索引
grid_x, grid_y = int(round(check_point[0])), int(round(check_point[1]))
# 检查边界和障碍物
if not (0 <= grid_x < grid.shape[0] and 0 <= grid_y < grid.shape[1]) or \
grid[grid_x, grid_y] == 1:
collision_count += 1
break # 一旦发现碰撞,就不再检查该段的其余部分
# 适应度 = 路径长度 + 巨大的碰撞惩罚
fitness = path_length + collision_count * PSOConfig.obstacle_penalty
return fitness
# --- 4. 粒子类 ---
class Particle:
def __init__(self, start_pos, end_pos, grid_size, num_points):
self.dim = num_points * 2 # 维度是中间点数 * 2 (x, y)
# 初始化位置:在网格内随机生成中间点
self.position = np.random.rand(self.dim) * grid_size
# 初始化速度:随机或为零
self.velocity = np.random.rand(self.dim) * 0.1
# 个体最优
self.pbest_position = self.position.copy()
path = decode_position_to_path(self.position, start_pos, end_pos)
self.pbest_fitness = calculate_fitness(path, grid)
self.current_fitness = self.pbest_fitness
# --- 5. 粒子群算法主体 ---
def pso_path_planning(config, start_pos, end_pos, grid):
"""执行粒子群算法进行路径规划"""
particles = [Particle(start_pos, end_pos, config.grid_size, config.num_intermediate_points)
for _ in range(config.num_particles)]
# 初始化全局最优
gbest_fitness = float('inf')
gbest_position = None
# 记录每次迭代的全局最优适应度,用于绘图
gbest_fitness_history = []
# 找到初始的全局最优
for p in particles:
if p.pbest_fitness < gbest_fitness:
gbest_fitness = p.pbest_fitness
gbest_position = p.pbest_position.copy()
print(f"初始全局最优适应度: {gbest_fitness:.2f}")
# --- 开始迭代 ---
for i in range(config.max_iter):
for p in particles:
# 更新速度
r1 = np.random.rand(p.dim)
r2 = np.random.rand(p.dim)
p.velocity = (config.w * p.velocity +
config.c1 * r1 * (p.pbest_position - p.position) +
config.c2 * r2 * (gbest_position - p.position))
# 更新位置
p.position += p.velocity
# 边界处理:确保粒子位置在网格范围内
p.position = np.clip(p.position, 0, config.grid_size - 1)
# 计算新位置的适应度
path = decode_position_to_path(p.position, start_pos, end_pos)
p.current_fitness = calculate_fitness(path, grid)
# 更新个体最优
if p.current_fitness < p.pbest_fitness:
p.pbest_fitness = p.current_fitness
p.pbest_position = p.position.copy()
# 更新全局最优
for p in particles:
if p.pbest_fitness < gbest_fitness:
gbest_fitness = p.pbest_fitness
gbest_position = p.pbest_position.copy()
gbest_fitness_history.append(gbest_fitness)
print(f"迭代 {i+1}/{config.max_iter}, 全局最优适应度: {gbest_fitness:.2f}")
return gbest_position, gbest_fitness_history
# --- 6. 可视化函数 ---
def plot_results(grid, path, start_pos, end_pos, fitness_history):
"""绘制栅格地图、路径和收敛曲线"""
plt.figure(figsize=(14, 6))
# 绘制栅格地图和路径
plt.subplot(1, 2, 1)
# 使用imshow绘制栅格,障碍物为黑色,通路为白色
plt.imshow(grid.T, cmap='Greys', origin='lower') # .T转置使(x,y)坐标系与数组索引匹配
# 绘制路径
path_points = np.array(path)
plt.plot(path_points[:, 0], path_points[:, 1], 'b-', linewidth=2, label='最优路径')
plt.plot(path_points[:, 0], path_points[:, 1], 'co') # 绘制中间点
# 标记起点和终点
plt.plot(start_pos[0], start_pos[1], 'go', markersize=10, label='起点')
plt.plot(end_pos[0], end_pos[1], 'ro', markersize=10, label='终点')
plt.title('PSO 路径规划结果')
plt.xlabel('X')
plt.ylabel('Y')
plt.legend()
plt.grid(True)
plt.xlim(0, grid.shape[0])
plt.ylim(0, grid.shape[1])
plt.gca().set_aspect('equal', adjustable='box')
# 绘制适应度收敛曲线
plt.subplot(1, 2, 2)
plt.plot(fitness_history)
plt.title('适应度收敛曲线')
plt.xlabel('迭代次数')
plt.ylabel('全局最优适应度')
plt.grid(True)
plt.tight_layout()
# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体
plt.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题
plt.show()
# --- 主函数 ---
if __name__ == '__main__':
# 定义起点和终点
start_point = (2, 2)
end_point = (28, 28)
# 1. 创建环境
config = PSOConfig()
grid = create_grid(config.grid_size, config.num_obstacles, start_point, end_point)
# 2. 运行PSO算法
best_position, history = pso_path_planning(config, start_point, end_point, grid)
# 3. 解码最优路径并可视化
if best_position is not None:
best_path = decode_position_to_path(best_position, start_point, end_point)
plot_results(grid, best_path, start_point, end_point, history)
else:
print("未能找到有效路径。")
代码分步解析
-
参数配置 (
PSOConfig
)
这里我们用一个类来集中管理所有的超参数,如粒子数、迭代次数、地图大小、障碍物数量以及PSO算法本身的学习因子等。这样做的好处是代码更整洁,调参更方便。 -
环境和工具 (
create_grid
,decode_position_to_path
)
create_grid
函数负责生成一个二维的numpy
数组作为地图,0
代表通路,1
代表障碍物。decode_position_to_path
则是一个转换函数,它将算法内部使用的一维位置向量,转换为我们容易理解的二维路径点列表。 -
适应度函数 (
calculate_fitness
) - 算法的灵魂
这是最核心的部分。它像一个裁判,给每一条路径打分。评分标准有两个:- 路径长度:用
np.linalg.norm
计算所有路径段的欧几里得距离之和。 - 碰撞检测:为了判断一条直线段是否穿过障碍物,我们在这条线上进行密集采样,检查每个采样点所在的栅格是否为障碍物。一旦发生碰撞,就记录一次,并给予一个巨大的惩罚值
obstacle_penalty
。
最终的适应度是 路径长度 + 碰撞惩罚。这个设计巧妙地引导算法去寻找既短又安全的路径。
- 路径长度:用
-
粒子类 (
Particle
)
这个类定义了每一只“鸟”的属性,包括它的当前位置(代表一条路径)、飞行速度、自己的历史最佳位置(pbest)和对应的适应度值。 -
PSO主算法 (
pso_path_planning
)
这里是算法迭代的舞台。它首先初始化一群粒子,然后进入主循环。在每次循环中,对每一个粒子:- 根据公式更新其速度和位置。
- 将更新后的位置(路径点)限制在地图边界内。
- 计算新路径的适应度,并更新其
pbest
。
在所有粒子都更新完后,遍历整个群体,找到并更新全局最优的gbest
。同时,我们记录下每次迭代的gbest
适应度,用于后续可视化。
-
可视化 (
plot_results
)
一个好的结果展示胜过千言万语。我们使用matplotlib
绘制两张图:- 路径图:直观地展示了地图、障碍物、起点、终点和算法找到的最佳路径。
- 收敛曲线:展示了适应度值随迭代次数的变化。一条平滑下降并最终收敛的曲线,有力地证明了我们的算法确实在“学习”和“进化”。
应用与拓展
你可能会问,这么酷的算法能用在哪些地方呢?
- 机器人导航:这是最直接的应用,无论是扫地机器人还是工业AGV小车,都需要在复杂环境中规划路径。
- 游戏开发:为游戏中的AI角色提供智能的移动逻辑,让它们能聪明地绕开障碍物。
- 无人机集群:可以扩展到三维空间,为无人机编队规划飞行轨迹。
- 函数优化:PSO的本质是寻找函数的最优解,因此在机器学习模型调参、工程设计等领域也有广泛应用。
结语
恭喜你!你已经完整地学习并实现了一个基于粒子群优化的路径规划器。我们从一个美丽的自然现象出发,通过清晰的逻辑和代码,解决了一个实际且有趣的问题。
这只是智能优化算法的冰山一角。我鼓励你亲手运行这段代码,并尝试修改PSOConfig
里的参数,看看会发生什么有趣的变化。比如,增加障碍物数量,或者改变路径中间点的数量,观察路径如何随之调整。
希望这篇文章能点燃你对算法的兴趣。如果你有任何想法或问题,欢迎在下方留言讨论!
感谢你的阅读,我们下期再见!