鱼群算法路径寻优:原理、Python实现与物流配送优化

鱼群算法路径寻优:原理、Python实现与物流配送优化 简介这是一份基于鱼群算法实现路径寻优的MATLAB完整工程面向需要学习群体智能优化算法或解决路径规划问题的本科生及科研人员。资源将鱼群算法的觅食、聚群、追尾等核心行为封装为独立函数配合主程序、距离计算与食物浓度模块可直接运行并观察寻优过程也便于在此基础上扩展改进。压缩包共14个文件包含8个m脚本、4个asv备份及2个mat数据文件整体仅7KB结构轻量清晰适合算法初学者对照阅读或二次开发。目前已有96人学习下载。通过完整代码、注释和配套坐标数据读者可以快速掌握鱼群算法在路径优化中的建模思路与编程实现还可借助asv文件追溯修改记录降低调试门槛。若运行或定制算法有疑问可通过私信或联系博主获得支持。1. 鱼群算法路径寻优里被低估的群体智能解法接手物流配送路径优化时最头疼的不是模型复杂而是传统启发式算法太容易在局部最优里打转。几轮迭代后所有配送路径都交叉着解却再也不动了。换用鱼群算法后聚群和追尾两个行为让搜索过程自带多样性在中小规模的路径寻优问题上往往比遗传算法更早找到优质解。鱼群算法是智能优化算法里比较特殊的一员。它不靠交叉变异而是用觅食、聚群、追尾和随机移动四种行为在解空间里寻找食物浓度最高的位置。对路径优化来说这些行为天然能平衡全局搜索和局部开采。下面从原理讲到 Python 实现再落到物流配送路径的容量约束和调试技巧。适合不熟悉组合优化编码、想快速验证鱼群算法路径寻优效果的人参考。2. 人工鱼群算法原理从觅食行为到路径寻优的建模2.1 四个行为与两个关键参数人工鱼群算法的基本单元是一条鱼每条鱼代表解空间里的一个位置 XX 处的食物浓度记为 Yf(X)。在路径寻优里X 是一个连续向量Y 越大表示路径越短。鱼的行为有四种觅食是向视野内更优的随机位置移动聚群是向视野内伙伴中心移动追尾是向视野内最优伙伴移动当没有更优方向或拥挤度过高时执行随机移动。四个行为都由视野 Visual、步长 Step、拥挤度因子 δ、尝试次数 try_number 控制。Visual 决定一条鱼能看到多大范围Step 决定每次移动多远δ 决定允许聚集多少伙伴try_number 是觅食无果时随机尝试的次数。鱼群算法对参数不像粒子群那样敏感但这四个值会影响收敛速度和解质量。为什么路径寻优要把最小化距离转换成最大化适应度因为鱼群算法的聚群和追尾判断基于“食物浓度越高越好”这一假设。把路径距离 d 转为 1/(1d)距离越短适应度越高才能直接套用标准判断公式。如果不做转换判断条件要反过来写容易在方向和步长上出错。2.2 路径寻优的编码方式随机键把离散路径变成连续空间路径寻优TSP、CVRP的解是置换序列而鱼群算法中的觅食、聚群、追尾都是连续向量的加减法直接处理置换会带来大量非法解。常见做法是用随机键编码一条鱼的位置是长度为城市数的连续向量每个维度是一个 0 到 1 之间的实数把向量排序得到的不重复下标序列就是访问顺序。比如有 5 个城市一条鱼的位置为 [0.62,0.31,0.85,0.12,0.44]按从小到大排序后对应下标 [3,1,4,0,2]即先访问第 3 个城市然后第 1 个再第 4、第 0、第 2最后回到起点。移动操作在键值上做排序结果自然始终是一条合法路径。这种编码把组合优化问题平滑映射到连续空间鱼群算法的所有行为算子都无需改动。import numpy as np def random_key_to_route(x): # x: 一条鱼在 [0,1] 空间里的随机键向量 return np.argsort(x) def evaluate_route(x, dist_mat): # 用随机键解码并计算闭合路径总距离 order np.argsort(x) d 0.0 prev order[-1] # 从最后一个城市回到第一个形成闭环 for city in order: d dist_mat[prev, city] prev city return d def fitness_from_distance(d): # 最小化距离转为最大化适应度 return 1.0 / (1.0 d)逻辑很简单argsort 返回排序后的下标下标就是城市编号dist_mat[prev, city] 取两个城市之间的运输距离每次移动更新 prev最后把闭环算进去。适应度加 1 是为了防止距离为 0 时除零同时做平滑缩放对寻优方向没有影响。还需要注意边界每次位置更新后要把所有随机键裁回 [0,1]。随机键编码依赖键的相对大小超出边界不仅没有意义还会在后续排序时产生不可控的偏移。这个边界约束在后面所有代码里都会出现。2.3 与遗传算法、粒子群算法的对比和选型很多人在做路径寻优时第一反应是遗传算法或粒子群算法但鱼群算法在实现成本上有自己的优势。差别可以从下表看出来。算法核心机制路径寻优常见问题鱼群算法在场景中的优势遗传算法选择/交叉/变异交叉算子需要针对性设计路径编码容易产生非法解早熟收敛没有交叉算子随机键天然合法实现简单粒子群算法个体历史最优全局最优依赖惯性权重和学习因子容易聚集到局部最优点聚群/追尾双机制拥挤度因子对聚集做约束人工鱼群算法觅食/聚群/追尾/随机参数较多需要调 Visual 和 δ四种行为互补全局探索和局部开采同时进行需要说明的是这不是否定遗传算法和粒子群算法。在带时间窗等强约束场景里遗传算法的置换交叉反而更容易利用邻域结构。鱼群算法的价值在于编码统一、实现直观适合先快速搭出可工作的解再根据项目需要混合其他算子。参数含义再强调一下。在随机键编码下Visual 控制局部邻域范围取 0.2~0.4 比较常见Step 取 Visual 的 1/5 到 1/3δ 通常取 0.6~0.8try_number 取 20 左右。这些参数会直接影响后面的实现和最终路径质量。3. 用 Python 实现鱼群算法路径寻优编码、三个行为与可运行代码3.1 初始化鱼群和基础方法基于上一章的随机键编码先定义 AFSA 类的基本骨架。初始化函数里生成鱼群位置并计算所有鱼的初始适应度。import numpy as np class AFSA: def __init__(self, dist_mat, fish_num30, max_iter200, visual0.25, step0.05, delta0.8, try_num20): self.dist_mat dist_mat self.n dist_mat.shape[0] self.fish_num fish_num self.max_iter max_iter self.visual visual self.step step self.delta delta self.try_num try_num self.fishes np.random.rand(fish_num, self.n) # 每条鱼一个随机键向量 self.fitness np.array([self.fitness_of(f) for f in self.fishes]) self.best_fish self.fishes[np.argmax(self.fitness)].copy() self.best_fitness self.fitness.max() self.record [] # 记录每代最优距离 def route_distance(self, x): order np.argsort(x) d 0.0 prev order[-1] for city in order: d self.dist_mat[prev, city] prev city return d def fitness_of(self, x): return 1.0 / (1.0 self.route_distance(x))鱼的位置是 fish_num 行 n 列的矩阵行是不同鱼列是随机键。fitness_of 负责解码、算闭合距离、转适应度。best_fish 保存全局最优record 用于画收敛曲线。所有参数都有默认值对距离矩阵直接实例化即可。3.2 实现觅食、聚群、追尾在类内继续补三个行为方法。move_towards 负责把一条鱼向某个方向移动 Step 距离prey 是觅食swarm 是聚群follow 是追尾。def move_towards(self, x, target): d target - x norm np.linalg.norm(d) if norm 1e-12: return x.copy() return np.clip(x self.step * d / norm, 0, 1) def prey(self, i): x self.fishes[i] for _ in range(self.try_num): xj np.clip(x self.visual * (np.random.rand(self.n) - 0.5), 0, 1) if self.fitness_of(xj) self.fitness_of(x): return self.move_towards(x, xj) return np.clip(x self.step * (np.random.rand(self.n) - 0.5), 0, 1) def swarm(self, i): x self.fishes[i] neighbors [j for j in range(self.fish_num) if j ! i and np.linalg.norm(self.fishes[j] - x) self.visual] if not neighbors: return self.prey(i) center np.mean(self.fishes[neighbors], axis0) nf len(neighbors) center_fit self.fitness_of(center) if center_fit / nf self.delta * self.fitness_of(x): return self.move_towards(x, center) return self.prey(i) def follow(self, i): x self.fishes[i] neighbors [j for j in range(self.fish_num) if j ! i and np.linalg.norm(self.fishes[j] - x) self.visual] if not neighbors: return self.prey(i) best_j max(neighbors, keylambda j: self.fitness_of(self.fishes[j])) nf len(neighbors) if self.fitness_of(self.fishes[best_j]) / nf self.delta * self.fitness_of(x): return self.move_towards(x, self.fishes[best_j]) return self.prey(i)move_towards 使用归一化方向乘以 Step保证每次最多移动 Step。prey 在视野内随机跳点找到更优就移动试完 try_number 次仍没有就随机走一步这就是随机行为。swarm 收集视野内伙伴计算伙伴中心 center当 center 的适应度经过拥挤度折算仍高于当前个体时朝 center 移动否则转觅食。follow 是追尾方向改为视野内适应度最高的伙伴。center 不一定在鱼群里但随机键在连续空间fitness_of 可以直接对它解码。主循环里每个个体同时尝试聚群和追尾取适应度更高的作为候选比只随机选一种行为的效果稳定。def optimize(self): for _ in range(self.max_iter): for i in range(self.fish_num): x_swarm self.swarm(i) x_follow self.follow(i) cand x_swarm if self.fitness_of(x_swarm) self.fitness_of(x_follow) else x_follow cand_fit self.fitness_of(cand) if cand_fit self.fitness[i]: self.fishes[i] cand self.fitness[i] cand_fit if cand_fit self.best_fitness: self.best_fitness cand_fit self.best_fish cand.copy() self.record.append(1.0 / self.best_fitness - 1.0) return self.best_fish, 1.0 / self.best_fitness - 1.0候选经过适应度比较才替换当前鱼这是一种精英保留让每代最优距离单调不增。record 存每代最优距离用于观察收敛。返回值是最好鱼的随机键和对应距离需要用 np.argsort 还原成路径。跑一个最小示例rng np.random.default_rng(42) coords rng.random((20, 2)) # 20个二维坐标点 dmat np.linalg.norm(coords[:, None, :] - coords[None, :, :], axis2) solver AFSA(dmat, fish_num30, max_iter200, visual0.25, step0.05) best_route_raw, best_dist solver.optimize() route np.argsort(best_route_raw) print(best_dist, route)dmat 用广播计算 20x20 距离矩阵。运行后打印最优闭合回路长度和访问顺序。如果解不理想优先调大 max_iter 或 fish_num。3.3 鱼群算法的参数表参数取值建议作用与调整方向fish_num20~50鱼群规模越大探索越充分每轮评估次数也越高max_iter100~500路径规模越大迭代越多record 走平即可停止visual0.1~0.4编码在 [0,1]过大变全局随机过小退化成局部爬山step0.02~0.10步长过大在最优附近震荡过小收敛慢delta0.6~0.9越大越允许扎堆路径寻优建议 0.7~0.8try_num10~30觅食尝试次数影响单次评估成本visual 和 step 的合适区间与城市数量有一定关系。城市多时visual 偏小会让伙伴判定过于局部可以适当调大step 保持 visual 的 1/5 到 1/3。delta 不是严格上限而是聚群引力与个体保守之间的权衡值。注意这些推荐值是起点。路径规模变化后正确做法是先看 record 曲线的形状再决定增大还是缩小步长。4. 物流配送路径寻优实战带容量约束的鱼群算法与参数调优4.1 从 TSP 到带容量约束的配送路径真实物流场景里一辆车跑完全部点不现实车辆有容量限制客户有需求量这是 CVRP。用鱼群算法解决时最省事的做法是保持随机键编码不变只换解码器随机键排序得到客户序列然后按顺序把客户装入车辆当前车剩余容量不足就换下一辆最后得到多辆车路径。这种“序列贪心分割”的编码方式在遗传算法和粒子群里都很常见鱼群算法同样适用。def decode_cvrp(x, demand, capacity): # x: 随机键向量 # demand: 每个客户需求量 # capacity: 单辆车容量 order np.argsort(x) routes [] load 0.0 current [] for c in order: if load demand[c] capacity 1e-9: routes.append(current) current [c] load demand[c] else: current.append(c) load demand[c] if current: routes.append(current) return routes逻辑说明当前车放不下下一个客户时把 current 追加到 routes 并新建车辆。1e-9 用于消除浮点误差。这个解码得到的 routes 是若干客户列表配送中心不在编码里实际距离计算要单独加上每辆车从仓库出发和返回仓库的里程。计算总距离时每辆车都从配送中心出发def cvrp_distance(routes, dist_mat, depot0): total 0.0 for route in routes: prev depot for c in route: total dist_mat[prev, c] prev c total dist_mat[prev, depot] return total然后把总距离和车辆数惩罚并入适应度。物流场景里的时间窗约束也可以同样处理比如把超时量乘系数加进目标函数。4.2 约束处理和罚函数设计罚函数是鱼群算法处理 VRP 约束最常见的做法。相比硬性丢弃超容量解罚函数能让搜索更平滑鱼会自然远离高惩罚区域。def cvrp_fitness(x, demand, capacity, dist_mat, vehicle_penalty1000): routes decode_cvrp(x, demand, capacity) total cvrp_distance(routes, dist_mat) overload max(0.0, len(routes) - vehicle_num) return 1.0 / (1.0 total vehicle_penalty * overload)参数说明vehicle_penalty 不能太小否则宁可多发一辆车也不优化路径罚函数失去意义也不能太大否则适应度差异被车辆数主导路径距离的梯度信号变弱。一般取当前平均单条路径距离的 10 到 100 倍。例如 dist_mat 均值是 5020 个客户平均每条路径 200vehicle_penalty 取 1000 比较合理。解码时如果需求总和超过车辆总容量decode_cvrp 仍然会生成路径但车辆数会明显增加罚函数会把这类解压下去。这比在解码阶段直接返回无穷大更利于维持鱼群的多样性。4.3 物流路径寻优的 3 个必调参数与踩坑点参数/环节建议值常见坑visual0.15~0.35VRP 点数量多visual 过大鱼群过早聚合到同一个解delta0.7~0.8过小导致不敢聚群很多鱼单独随机游走vehicle_penalty平均路径距离×50过小会允许超车结果里程虚低try_num20~30过大只增加评估次数不改善解质量踩坑点之一随机键是连续值排序后得到路径但两条重新生成的鱼如果键值整体偏移可能对应完全不同的路径。这意味着鱼群算法在随机键空间里的邻域关系和路径空间里的邻域关系并不完全一致。visual 和 step 不能照搬连续优化经验要先跑 50 代看 record 是否持续下降再调整。踩坑点之二CVRP 的距离矩阵可能不对称或者配送中心编号不是 0。decode_cvrp 只保留客户编号cvrp_distance 固定把 depot 当作起点和终点实现时需要保证编号一致。如果 dist_mat 里的仓库不是 0 索引结果会整体错位。另一个常见做法是先用纯 TSP 跑通最小用例再加容量约束。原因是容量约束会让目标函数包含罚函数表面收敛更好看但可能是在惩罚边界上。先把 capacity 放宽到总需求的 10 倍确认路径形态合理后再逐步收紧容量。5. 让鱼群算法路径寻优更稳2-opt 局部搜索与收敛性验证5.1 在鱼群行为后挂一个 2-opt 局部搜索鱼群算法的聚群和追尾在随机键空间里是连续移动但路径质量最终由离散排序决定。一个常见的改进是在候选路径上做 2-opt 局部搜索再把局部最优路径反写回随机键。2-opt 通过逆序一段子路径消除路径中的交叉和自绕。def two_opt(route, dist_mat, max_passes3): best route.copy() for _ in range(max_passes): improved False for i in range(len(best) - 1): for j in range(i 2, len(best)): new_route best[:i] best[i:j][::-1] best[j:] if route_length(new_route, dist_mat) route_length(best, dist_mat): best new_route improved True if not improved: break return best这里需要准备一个 route_length 函数直接复用第 3 章的 route_distance 逻辑。max_passes 限制扫描轮数避免每次迭代都陷入过长的局部精修。嵌入方式在鱼群主循环里对全局最优鱼做一次局部搜索如果局部搜索后的距离更短就把这条路径反写回随机键再更新全局最优。best_route np.argsort(solver.best_fish) improved_route two_opt(best_route, dmat) if route_length(improved_route, dmat) route_length(best_route, dmat): new_key np.empty(solver.n) for rank, city in enumerate(improved_route): new_key[city] rank solver.best_fish new_key / solver.n # 归一化到 [0,1] solver.best_fitness 1.0 / (1.0 route_length(improved_route, dmat))这是把离散路径映射回随机键的最简单方式第 rank 个访问的城市键值设为 rank然后整体除以 n排序后正好得到局部最优路径。反写后后续迭代可以继续在这个路径周围做连续搜索。验证方法就是比较纯 AFSA 和 AFSA2-opt 的 record 曲线多跑几个随机种子取中位数。常见的观察是纯 AFSA 在 150 代后下降变缓混合 2-opt 能在同样迭代次数下把最优距离再压低 5%~15%。计算改进比例用(pure_best - hybrid_best) / pure_best * 100。如果比例很低先检查 2-opt 扫的次数是否太少再检查 visual 是否过大导致鱼群早已收敛到同一个解上。本文还有配套的精品资源点击获取