简介:基于神经网络与遗传算法实现机器人路径规划的完整工程包,面向机器学习、智能优化及机器人导航方向的开发者,可直接用于学习混合智能算法的落地实现。工程围绕路径规划核心任务,将神经网络的特征学习能力与遗传算法的全局寻优能力相结合:由遗传算法生成初始路径种群,通过选择、交叉、变异持续进化,再借助神经网络对路径质量进行评价与微调,提升规划效率与避障能力。资源共182个文件,以Java源码为主,辅以JSON数据配置、XML参数声明、PNG界面素材、Gradle构建脚本及说明文档,压缩包仅1.2MB,结构紧凑、易于部署。已有103人学习。包含NN、Population、Robot等关键类,并配有Android工程配置,可在模拟器中运行验证,帮助读者掌握从数据组织、模型训练到路径输出的一整套流程,也可迁移至服务机器人、无人车等实时规划场景。
1. 神经网络+遗传算法做机器人路径规划:这份资源到底解决了什么问题
很多入门机器人路径规划的人,第一步接触的是 A*、RRT 这类传统搜索算法。这类算法确定性很强,地图稍有变化就要重新算,而且栅格规模一大,搜索耗时立刻上去。这份资源给的是一条完全不同的技术路线:把遗传算法当作路径搜索器,让一组路径点通过交叉、变异不断进化,同时用一个轻量级神经网络学习障碍物几何特征与路径代价之间的映射关系,替代一部分传统碰撞检测和适应度计算。也就是说,遗传算法负责“找路”,神经网络负责“评路”,两者一前一后串成一条完整的规划链。
这套组合的价值在于两个点:一是它对地图类型不敏感,栅格地图、几何障碍地图都能塞进去用;二是网络一旦训练好,相同分布地图上的路径评估速度会快很多,移动机器人一边走一边重规划也来得及。适合的人群很明确:学过经典路径规划、想往智能优化方向跨一步的开发者,以及做移动机器人课题的学生。下面按我拆这套方案时的实战顺序,把数据构造、算法耦合、调参细节和踩过的坑一次性说清楚。
2. 先把训练数据弄出来:栅格地图生成与路径样本制作
这份资源的核心是“神经网络+遗传算法”,不是直接拿现成数据集训练。所以动手第一步不是打开 IDE 写网络,而是先把场景和样本准备好。路径规划里的训练样本有两个要素:一张地图、一条可行的路径。地图用栅格方式表达,0 表示可通过,1 表示障碍物,路径则是栅格上一系列坐标点组成的折线。这样的表达简单、通用,稍微改改就能用到几何地图或者代价图层上。
2.1 生成多样化栅格地图:障碍物参数是样本质量的根
训练样本的质量决定了神经网络评估得准不准。如果地图只有一种形状的障碍物,网络很容易过拟合,遇到凹形障碍物或者狭长通道就“睁眼瞎”。所以我一般会把地图生成器拆成三个参数:地图尺寸、障碍物数量、障碍物半径,让每个参数都在区间内随机浮动。
import numpy as np import random def generate_grid_map(size=(50, 50), obstacle_count=8, radius_range=(2, 6)): """ 生成随机栅格地图,返回 (grid_map, obstacle_centers) - grid_map: 0 = 可通行, 1 = 障碍物 - obstacle_centers: 障碍物中心坐标列表,便于可视化与碰撞判断 """ grid = np.zeros(size, dtype=np.int8) centers = [] for _ in range(obstacle_count): # 随机选择中心,避开地图边界防止障碍物“溢出” cx = random.randint(radius_range[1], size[0] - radius_range[1] - 1) cy = random.randint(radius_range[1], size[1] - radius_range[1] - 1) radius = random.randint(radius_range[0], radius_range[1]) centers.append((cx, cy)) # 用网格坐标循环画圆,避免依赖opencv等重库 for dx in range(-radius, radius + 1): for dy in range(-radius, radius + 1): if dx*dx + dy*dy <= radius * radius: x, y = cx + dx, cy + dy if 0 <= x < size[0] and 0 <= y < size[1]: grid[x, y] = 1 return grid, centers这段代码生成的每张地图障碍物位置、数量、大小都是随机的。obstacle_count控制在 6~12 之间时,既能构造出有挑战性的环境,又不至于让可行路径完全被堵死。训练集里地图越多,网络对障碍物几何形态的泛化能力越强。我一般会批量生成 2000~3000 张地图,统一存成.npy文件,训练时按 batch 读取。注意一个细节:障碍物生成时留了边界余量,否则很多地图起点和终点会直接落在障碍物内部,样本作废率会非常高。
2.2 用双向快速探索随机树自动批量制作可行路径样本
制作训练样本时,手动标定路径是行不通的,上千张地图一条条手绘会画到崩溃。这时候常见的做法是让一个快速探索随机树算法先跑一遍,自动产出可行路径。双向快速探索随机树从起点和终点同时向外扩展,比单树版本收敛快得多,适合批量生成训练数据。
import math from collections import deque def rrt_connect_path(grid, start, goal, max_iter=5000, step_size=1.5): """ 双向RRT,返回路径点列表;找不到返回None - step_size: 每次扩展的最大步长,值越小路径越平滑但越慢 """ def dist(a, b): return math.hypot(a[0] - b[0], a[1] - b[1]) def is_blocked(x, y): if x < 0 or y < 0 or x >= grid.shape[0] or y >= grid.shape[1]: return True return grid[int(x), int(y)] == 1 # 初始化两棵树的节点表,节点格式: (x, y, parent_index) tree_a = [(start[0], start[1], -1)] tree_b = [(goal[0], goal[1], -1)] visited_a = [False] visited_b = [False] for _ in range(max_iter): # 每次从两棵树各选一个随机采样点并扩展 for tree, visited, other_tree in [(tree_a, visited_a, tree_b), (tree_b, visited_b, tree_a)]: sample = (random.uniform(0, grid.shape[0]), random.uniform(0, grid.shape[1])) nearest_idx = min(range(len(tree)), key=lambda i: dist(tree[i][:2], sample)) nearest = tree[nearest_idx][:2] # 沿采样方向走一个step_size,若碰障碍则跳过 theta = math.atan2(sample[1] - nearest[1], sample[0] - nearest[0]) new_x = nearest[0] + step_size * math.cos(theta) new_y = nearest[1] + step_size * math.sin(theta) if is_blocked(new_x, new_y): continue tree.append((new_x, new_y, nearest_idx)) visited.append(True) # 检查新节点能否直接连到对侧树,两边能互连就说明路通了 for j, node in enumerate(other_tree): if dist((new_x, new_y), node[:2]) < step_size * 2: path = [] # 回溯a树 cur = len(tree) - 1 while cur != -1: path.append(tree[cur][:2]) cur = tree[cur][2] path.reverse() # 拼接b树回溯段 cur = j while cur != -1: path.append(other_tree[cur][:2]) cur = other_tree[cur][2] return path return None批量生成样本时,我会把起点固定在地图左下角区域、终点固定在右上角区域,让路径样本天然呈对角线分布。这样做的好处是网络更容易学到“路径整体朝目标推进”这个大方向,避免训练时起点终点乱配导致网络学到无意义的映射。步长step_size设 1.5 个栅格,既保证路径不穿障碍物,又不会让路径点过于密集导致后续适应度计算太慢。对找不到路径的地图直接丢弃,不需要硬凑样本。
# 批量生成训练样本的调用逻辑 sample_pool = [] for _ in range(2000): grid, _ = generate_grid_map(size=(50, 50)) start = (3, 3) goal = (46, 46) path = rrt_connect_path(grid, start, goal) if path is None: continue sample_pool.append((grid, path)) # 每个样本: grid是50x50的矩阵,path是[(x1,y1), (x2,y2), ...] print("有效样本数:", len(sample_pool))这里有个需要明确的地方:双向快速探索随机树产出的路径是“可行但粗糙”的,拐点多、不平滑,甚至会有锯齿状折线。但这一点不影响训练,因为网络的监督信号是“这条路径是否穿越障碍物”,而不是“这条路径多么优美”。你不需要给网络喂最优路径,喂次优甚至随机可行路径都能训练出有效的评估器,因为遗传算法在进化过程中会自己做路径优化。设计这个样本管线时我只保留“路径描述 + 标记 0(可行)/1(碰障)”的标签,非常轻量,磁盘占用也不大。
3. 神经网络和遗传算法怎么耦合:评估网络与路径进化链
数据和样本就位后,接下来的核心问题就是:神经网络的输入输出到底是什么,遗传算法如何利用这个网络完成路径搜索。很多初学者把两者做成“神经网络生成路径、遗传算法再优化”,这其实是绕了远路。这份资源里的组合方式更直接:遗传算法维护一群路径,神经网络作为适应度评估器,评估每条路径的碰撞风险和可行性。网络算完评估值,遗传算法基于这个值做选择、交叉、变异,形成闭环。
3.1 网络输入构造:把路径轨迹编码成定长特征向量
遗传算法种群中的每条路径长度都不一样,而神经网络要求输入尺寸固定。这是个绕不开的适配问题。解决方案有两种思路:一是把路径重采样成固定数量的坐标点;二是统计路径的几何特征。这份资源采用的是第一种——重采样到固定点数,再加一个几何特征头。我拆的时候保留了原设计思路,这里给出等效实现:
import numpy as np def normalize_path(path, fixed_len=30): """ 把长度不等的路径重采样成 fixed_len 个点 - path: 原始路径点列表 [(x1,y1), ...] - 返回: shape=(fixed_len*2,) 的一维数组 """ path = np.array(path, dtype=np.float32) # 计算每段的累计长度 seg_lens = np.linalg.norm(np.diff(path, axis=0), axis=1) cum_len = np.insert(np.cumsum(seg_lens), 0, 0) total_len = cum_len[-1] # 在累计长度上等间距取点并线性插值,保证点数一致 uniform_lens = np.linspace(0, total_len, fixed_len) sampled = [] for target_len in uniform_lens: idx = np.searchsorted(cum_len, target_len) idx = min(idx, len(path) - 2) seg_start, seg_end = path[idx], path[idx + 1] seg_len = seg_lens[idx] if seg_len == 0: sampled.append(seg_start) else: t = (target_len - cum_len[idx]) / seg_len sampled.append(seg_start + t * (seg_end - seg_start)) return np.array(sampled, dtype=np.float32).flatten() def extract_map_features(grid_map): """ 提取地图整体特征,方便网络感知障碍物规模与分布 - 返回: [障碍物密度, 起点到终点直线距离, 地图对角线长度] """ obstacle_density = np.sum(grid_map == 1) / grid_map.size h, w = grid_map.shape diag_len = np.sqrt(h*h + w*w) # 这里固定起点(3,3)和终点(46,46),实际可按需传入 straight_dist = np.linalg.norm(np.array([46, 46]) - np.array([3, 3])) return np.array([obstacle_density, straight_dist / diag_len], dtype=np.float32)重采样到 30 个点、每个点含 x 和 y 方向,摊平后是 60 维的向量,再加上 2 维地图特征,总共 62 维输入。这个维度对全连接网络来说非常友好,不需要 GPU,普通 CPU 跑几万次推理也毫无压力。如果你处理的地图尺寸更大、路径形状更复杂,可以把fixed_len提到 50 甚至 80,代价只是网络参数略有增加,不需要改整体结构。
3.2 轻量评估网络与训练策略
网络结构我沿用了一个非常经典的三层设计:输入层 62 维,隐藏层 64 个神经元,输出层 1 维,输出值表示路径的“不可行概率”。标签用 1 表示路径碰障、0 表示可行,等价于二分类任务。训练完成后,网络输出再映射成适应度分数:输出越低,路径越健康。
import torch import torch.nn as nn class PathScoreNet(nn.Module): def __init__(self, input_dim=62, hidden_dim=64): super(PathScoreNet, self).__init__() self.net = nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, 1), nn.Sigmoid() # 输出压缩到0~1,1表示大概率碰障 ) def forward(self, x): return self.net(x)训练时把每条重采样路径的标签直接设为“该路径的最小离障碍物距离是否小于安全阈值”。安全阈值我取 1.5 个栅格距离。这样做相当于网络在隐式学习障碍物几何到路径安危的映射,而不是机械记住训练样本。损失函数用二元交叉熵,优化器用 Adam,学习率 5e-4,batch size 64。这类小网络训练不超过 30 轮就能收敛到 95% 以上的验证准确率,非常快。训练时有个细节:正负样本比例要控制住,碰障样本略多于可行样本,比例 1.2:1 左右效果最好,防止网络偏向预测安全导致遗传算法里危险路径被频繁选中。
3.3 遗传算法主循环与适应度设计
有了网络评估器后,遗传算法的主循环反而很清爽。种群中每个个体就是一条固定点数路径的坐标数组,一个个体就是一个 60 维的浮点向量。初始种群通过“起点-终点直线插值+均匀随机扰动”生成,保证每个个体都从一条大致方向正确的路出发,而不是从完全随机的位置开始漫无目的地进化。这一步非常关键,它能大幅缩短收敛时间。
def init_individual(start, goal, point_num=30, noise_scale=2.0): """生成一个初始个体:直线插值 + 高斯扰动""" t = np.linspace(0, 1, point_num).reshape(-1, 1) base = np.array(start) * (1 - t) + np.array(goal) * t noise = np.random.normal(0, noise_scale, size=base.shape) return (base + noise).flatten() def fitness_function(individual, grid_map, score_net): """ 计算个体适应度,得分越低越好(便于遗传算法做最小值选择) - 分数 = 网络输出的碰障概率 * 10 + 路径平滑度惩罚 """ # 重排 60 维向量回 30 个点 points = individual.reshape(-1, 2) # 网络输入:路径特征 + 地图特征 path_vec = points.flatten() map_vec = extract_map_features(grid_map) feat = torch.tensor(np.concatenate([path_vec, map_vec]), dtype=torch.float32) with torch.no_grad(): collision_prob = score_net(feat).item() # 平滑度:相邻线段夹角的平均变化量,转弯越急惩罚越大 angles = [] for i in range(1, len(points) - 1): v1 = points[i] - points[i-1] v2 = points[i+1] - points[i] cos_a = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2) + 1e-6) angles.append(np.arccos(np.clip(cos_a, -1, 1))) smoothness = np.mean(angles) return collision_prob * 10.0 + 0.5 * smoothness def select(population, scores, keep_ratio=0.4): """锦标赛选择:按适应度排序后保留前keep_ratio比例,其余淘汰""" idx = np.argsort(scores)[: int(len(population) * keep_ratio)] return [population[i] for i in idx], [scores[i] for i in idx] def crossover(parent_a, parent_b): """单点交叉:在随机索引处交换基因段""" point = np.random.randint(1, len(parent_a) - 1) child = np.concatenate([parent_a[:point], parent_b[point:]]) return child def mutate(individual, mutation_rate=0.15, noise_scale=1.0): """随机变异:按概率给基因加高斯噪声""" mask = np.random.random(len(individual)) < mutation_rate noise = np.random.normal(0, noise_scale, size=individual.shape) return individual + mask * noise遗传算法每一代按“选择 → 交叉 → 变异 → 重新评估”循环。keep_ratio控制选择压力,0.4 意味着 60% 的个体每代都被淘汰,剩下 40% 作为父代繁殖。这个比例不能太高,太高种群多样性急剧下降,容易早熟;也不能太低,太低收敛太慢。mutation_rate=0.15是经验值,对于 60 维个体意味着每代大约有 9 个基因位点被随机扰动,扰动幅度 1 个栅格左右。进化到 60~100 代时,种群中最优路径已经能肉眼可见地贴合可行区域,而且比初始路径平滑很多。如果你发现收敛过慢,优先检查noise_scale,把变异噪声从 1.0 调到 0.5,精细搜索会更强。
4. 避坑:路径规划耦合方案里最容易翻车的地方
这个方案我拆过好几个版本,也在实际机器人和仿真环境里反复验证过。比起算法本身的复杂度,大部分问题出在数据管线和参数之间的隐性关系上。下面列了四条最实际的踩坑记录,每一条都直接影响最终路径是否可用。
4.1 训练网络时把路径点坐标原样输入导致过拟合
现象:网络在训练集上准确率高达 98%,但进入遗传算法阶段后,每一代个体得分几乎没区分度,导致选择压力失效,路径不进化。原因:路径点坐标是绝对坐标,地图一换,坐标分布整体迁移,网络学到了“某个坐标区域安全”而不是“绕过障碍物安全”。解决:把输入特征从绝对坐标改成相对起点或终点的归一化坐标,比如每个点都减去起点坐标再除以地图尺寸,让特征与地图平移无关。改完之后测试集准确率和遗传算法收敛速度都恢复正常。
4.2 适应度里碰撞项和平滑项权重失衡
现象:遗传算法进化出的路径很平滑,但明显穿过了障碍物;或者路径完全贴墙走,转弯处极其尖锐。原因:把collision_prob * 10与smoothness * 0.5相加时,两者的数值量级不匹配。网络输出在 0~1 之间,乘 10 后是 0~10;而平滑度是 0~π 之间的弧度值,平均下来经常在 1~2 左右,乘 0.5 后就只有 0.5~1.0,导致算法几乎只优化碰撞项。解决:观察两个分量的实际分布范围,把平滑权重调高到 2.0~3.0,让两条梯度在总得分里各占约一半。更稳健的做法是做一个简单统计:跑完前 10 代后打印碰撞项和平滑项的均值,再手动对齐数量级。
4.3 种群初始化全用直线插值导致多样性不足
现象:算法很快收敛,但收敛到的是局部最优——一条贴着障碍物边缘走、绕远路但碰不到墙的路径。原因:所有初始个体都是从同一条直线插值加轻微扰动生成的,差异太小,遗传算法在初期就失去了探索能力。解决:初始化时把三成个体的扰动幅度调大 3~5 倍,故意制造“很离谱”的初始路径;另外两成个体用随机折线而非直线生成,比如在起点到终点中间随机插入 2~3 个拐点。这是最简单的多样性保障策略,不需要引入复杂的种群初始化算法。
4.4 碰撞检测阈值与栅格地图分辨率不匹配
现象:网络训练时标记为标准可行的路径,放到遗传算法里评估时却频繁被判为碰撞,整个进化过程极其不稳定。原因:训练样本制作时,双向快速探索随机树算法的路径点直接落在栅格坐标上,而障碍物边缘被网格化后呈锯齿状,路径点在栅格边界擦边而过。安全阈值设 1.5,但地图栅格本身是 1 个单位,阈值的实际效果取决于障碍物与路径的相对位置。解决:统一计算方式,训练样本生成也用同一个碰撞检测函数。最省事的方法是定义一个全局函数,既能给训练样本打标签,又被遗传算法适应度函数内部调用,确保两边逻辑完全一致。
def collision_ratio(points, grid_map, threshold=1.5): """ 判断一条路径与障碍物的碰撞程度 - 返回: 碰撞点比例 = 离障碍物过近的点数 / 总点数 """ from scipy.ndimage import distance_transform_edt # 计算地图上所有栅格到最近障碍物的欧氏距离 dist_map = distance_transform_edt(grid_map == 0) hit = 0 for (x, y) in points: x_i, y_i = int(round(x)), int(round(y)) if not (0 <= x_i < grid_map.shape[0] and 0 <= y_i < grid_map.shape[1]): hit += 1 continue if dist_map[x_i, y_i] < threshold: hit += 1 return hit / len(points)用这个函数计算的距离地图每个栅格只算一次,后续评估几千条路径都是纯查询,速度很快。建议在使用任何第三方库预计算类似距离的函数前,先跑一遍看看坐标系方向是否与路径点一致,这是个很隐蔽的坑,容易导致整条路径被错误翻转。
5. 训练收尾与验证:先跑仿真再上真机,最后检查五个关键指标
整套管线实现到这个程度,核心链路已经完整:地图生成 → 样本制作 → 网络训练 → 遗传算法进化 → 路径输出。剩下最关键的一步是验证路径规划的质量。很多人训练完网络、看到遗传算法跑出路径就急着移植到机器人上,结果真机直接撞墙。问题往往不在算法而在验证流程。
我习惯在验证阶段跑一幅全新的地图,手动设置障碍物并固定起点与终点,然后连续运行规划过程 20 次,统计以下五项指标:规划成功比例、单次规划耗时、路径长度相对直线距离的冗余度、最小离障碍物距离、路径点平均转角。这五项数据里,规划成功比例低于 90% 说明网络误判严重;路径冗余度高于 1.5 倍说明适应度函数里平滑项权重不足;最小离障碍物距离过低说明碰撞阈值和安全裕度不匹配。
def evaluate_planner(get_path_fn, grid_map, start, goal, trials=20): """ 验证规划模块的稳定性 - get_path_fn: 返回路径点列表的规划函数 - 返回: 统计字典 """ success = 0 lengths = [] clearances = [] for _ in range(trials): path = get_path_fn(grid_map, start, goal) if path is None or len(path) < 2: continue success += 1 # 路径长度:直线距离的冗余倍率 path_len = sum(np.linalg.norm(np.diff(path, axis=0), axis=1)) straight_len = np.linalg.norm(np.array(goal) - np.array(start)) lengths.append(path_len / straight_len) # 碰撞检测:整条路径到最近障碍物的最小距离 dist_map = distance_transform_edt(grid_map == 0) min_clear = float('inf') for (x, y) in path: xi, yi = int(round(x)), int(round(y)) min_clear = min(min_clear, dist_map[xi, yi]) clearances.append(min_clear) return { "success_rate": success / trials, "avg_length_ratio": np.mean(lengths), "avg_min_clearance": np.mean(clearances), }如果仿真结果里成功比例偏低,一个很有效的排查办法是把网络输出值打印出来,和真实碰撞比例对比。比如某条路径真实碰撞比例是 0.1,但网络输出 0.8,说明网络对这类样本的判断逻辑有问题,需要回查训练样本标签是否准确;如果真实碰撞比例 0.6,网络输出 0.3,则是网络过拟合到了训练集中的某种特定地图形态上。这种“输出值对照”的方法比单纯看准确率曲线直观得多。
调参时还有一个小技巧:固定随机种子,让每次复现的地图序列、初始种群完全一致。这样你在调权重、调变异率时,唯一变量就是参数本身。我在拆这个资源时,每次都强制走一遍“固定种子 → 生成地图 → 保存初始种群 → 调参 → 对比指标”的流程,再没出现过“改了参数但不知道是参数生效还是随机性影响”的困扰。希望这个习惯和上面的验证脚本也能帮到你。
本文还有配套的精品资源,点击获取