Python实战:手把手教你用粒子群算法(PSO)实现机器人智能寻路

你有没有好奇过,那些科幻电影里的机器人、游戏里的智能NPC,它们是如何在复杂的环境中穿梭自如,总能找到通往目标的路径的?这背后其实隐藏着强大的算法。

今天,我们将一起探索并亲手实现其中一种极具魅力的算法——粒子群优化(Particle Swarm Optimization, PSO)。我们将用它来解决一个经典的难题:在布满障碍的地图上,寻找一条从起点到终点的最优路径。

最棒的是,你不需要任何高深的数学背景,只需要一颗好奇的心和对Python的一点了解。让我们一起,把源于自然的鸟群智慧,变成触手可及的代码!
左边是最有路径的优化结果,右边是适应度函数的收敛曲线


灵感来源:鸟群的觅食智慧

在深入代码之前,我们先来聊聊粒子群算法的灵感来源——鸟群觅食。

想象一下,在一个区域内有一群鸟在寻找食物,但区域里只有一个地方有食物。所有的鸟都不知道食物在哪里,但它们知道自己当前位置离食物有多远。那么,最有效的策略是什么呢?

  1. 相信自己:每只鸟都会记住自己到过的、离食物最近的位置。这个位置被称为“个体最优解 (pbest)”。
  2. 相信集体:所有鸟都会共享信息,它们都知道整个鸟群发现的、离食物最近的位置。这个位置被称为“全局最优解 (gbest)”。
  3. 调整飞行:在下一次飞行时,每只鸟都会结合三个因素来决定自己的方向和速度:
    • 自己原来的飞行习惯(惯性)。
    • 飞向“自己找到过的最佳位置”的趋势。
    • 飞向“整个群体找到的最佳位置”的趋势。

通过这种简单的规则,整个鸟群不断迭代,逐步向食物的最终位置聚集,最终高效地找到目标。

在我们的路径规划问题中,我们将这个模型进行映射:

  • 一只鸟(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("未能找到有效路径。")

代码分步解析
  1. 参数配置 (PSOConfig)
    这里我们用一个类来集中管理所有的超参数,如粒子数、迭代次数、地图大小、障碍物数量以及PSO算法本身的学习因子等。这样做的好处是代码更整洁,调参更方便。

  2. 环境和工具 (create_grid, decode_position_to_path)
    create_grid 函数负责生成一个二维的numpy数组作为地图,0代表通路,1代表障碍物。decode_position_to_path则是一个转换函数,它将算法内部使用的一维位置向量,转换为我们容易理解的二维路径点列表。

  3. 适应度函数 (calculate_fitness) - 算法的灵魂
    这是最核心的部分。它像一个裁判,给每一条路径打分。评分标准有两个:

    • 路径长度:用np.linalg.norm计算所有路径段的欧几里得距离之和。
    • 碰撞检测:为了判断一条直线段是否穿过障碍物,我们在这条线上进行密集采样,检查每个采样点所在的栅格是否为障碍物。一旦发生碰撞,就记录一次,并给予一个巨大的惩罚值 obstacle_penalty
      最终的适应度是 路径长度 + 碰撞惩罚。这个设计巧妙地引导算法去寻找既短又安全的路径。
  4. 粒子类 (Particle)
    这个类定义了每一只“鸟”的属性,包括它的当前位置(代表一条路径)、飞行速度、自己的历史最佳位置(pbest)和对应的适应度值。

  5. PSO主算法 (pso_path_planning)
    这里是算法迭代的舞台。它首先初始化一群粒子,然后进入主循环。在每次循环中,对每一个粒子:

    • 根据公式更新其速度和位置。
    • 将更新后的位置(路径点)限制在地图边界内。
    • 计算新路径的适应度,并更新其pbest
      在所有粒子都更新完后,遍历整个群体,找到并更新全局最优的gbest。同时,我们记录下每次迭代的gbest适应度,用于后续可视化。
  6. 可视化 (plot_results)
    一个好的结果展示胜过千言万语。我们使用matplotlib绘制两张图:

    • 路径图:直观地展示了地图、障碍物、起点、终点和算法找到的最佳路径。
    • 收敛曲线:展示了适应度值随迭代次数的变化。一条平滑下降并最终收敛的曲线,有力地证明了我们的算法确实在“学习”和“进化”。

应用与拓展

你可能会问,这么酷的算法能用在哪些地方呢?

  • 机器人导航:这是最直接的应用,无论是扫地机器人还是工业AGV小车,都需要在复杂环境中规划路径。
  • 游戏开发:为游戏中的AI角色提供智能的移动逻辑,让它们能聪明地绕开障碍物。
  • 无人机集群:可以扩展到三维空间,为无人机编队规划飞行轨迹。
  • 函数优化:PSO的本质是寻找函数的最优解,因此在机器学习模型调参、工程设计等领域也有广泛应用。

结语

恭喜你!你已经完整地学习并实现了一个基于粒子群优化的路径规划器。我们从一个美丽的自然现象出发,通过清晰的逻辑和代码,解决了一个实际且有趣的问题。

这只是智能优化算法的冰山一角。我鼓励你亲手运行这段代码,并尝试修改PSOConfig里的参数,看看会发生什么有趣的变化。比如,增加障碍物数量,或者改变路径中间点的数量,观察路径如何随之调整。

希望这篇文章能点燃你对算法的兴趣。如果你有任何想法或问题,欢迎在下方留言讨论!

感谢你的阅读,我们下期再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

电脑能手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值