贝叶斯优化在车辆MPC参数整定中的实践 简介这份资源是本科毕业设计《基于贝叶斯优化的车辆模型预测控制》的完整项目包面向自动化、车辆工程或控制科学相关专业的学生也适合正在研究智能驾驶控制策略的开发者。内容围绕贝叶斯优化算法与车辆模型预测控制MPC的结合展开覆盖车辆动力学建模、代价函数设计、控制参数寻优及实验结果分析等关键环节核心目标是提升 MPC 在复杂非线性系统中的实时性与适用性。压缩包共 843 个文件约 3.86MB以 csv 数据文件为主602 个包含训练与仿真数据另有 py 脚本、log 日志、pdparams/pdmodel 模型参数、yaml 配置等可支撑算法复现与结果验证zip 与 zbak 文件则便于备份和恢复实验状态。资源结构较完整适合从零搭建贝叶斯优化车辆控制项目时参考。目前已有 59 人学习浏览对于毕业设计阶段的同学来说它提供了一套可直接对照的数据集、模型文件和代码框架能减少前期建模和参数调优的重复工作帮助理解贝叶斯优化在 MPC 中的实际落地路径。1. 贝叶斯优化与车辆MPC为什么这个毕设把调参变成黑盒模型预测控制MPC的参数整定是件苦差事预测时域、控制时域、权重矩阵每个都会影响跟踪效果手动调一轮要跑完整个轨迹换个车速又得重新来。常见做法是网格搜索但在车辆跟踪场景里一次闭环仿真就要几十秒网格点超过几百个就会等到怀疑人生。这个毕设把参数寻优交给贝叶斯优化用高斯过程对“参数→控制代价”的映射建模从一根参考轨迹、一个真值数据包里挑出几十个候选点就能找到能用的MPC参数。数据流也很直接bag文件转csvspline.csv提供参考线auto_*.csv记录每轮参数下的控制输出。适合正在做车辆控制、自动驾驶相关毕设的人参考。2. 车辆运动学模型与MPC参数化2.1 从运动学模型到离散状态方程我先解释为什么用运动学模型而不是整车动力学。毕业设计的核心是验证贝叶斯优化对MPC超参数的搜索能力底盘侧偏、轮胎滑移这些因素会引入太多模型不确定性让调参过程的代价面更不平滑。低速校园场景下自行车运动学模型已经能反映主要跟踪特性。状态取车辆位置 (x,y)、横摆角 (\psi) 和纵向速度 (v)控制量是前轮转角 (\delta) 和加速度 (a)状态方程写成[ \dot{x}v\cos\psi,\quad \dot{y}v\sin\psi,\quad \dot{\psi}\frac{v\tan\delta}{L},\quad \dot{v}a ]其中 (L) 是轴距。我一般把控制周期dt固定为 0.05s轴距L按试验车参数设成 2.8m如果换成其他车辆先从数据包里读车辆几何参数再改。下面是预测状态序列的实现import numpy as np def predict_states(x0, control_seq, dt, L, Np): x0: [x, y, psi, v] control_seq: 长度为 Np 的 [(delta, a), ...] dt: 控制周期秒 L: 轴距米 Np: 预测时域步数 x np.array(x0, dtypefloat) v x[3] traj [x.copy()] for k in range(Np): delta, a control_seq[k] x[0] v * np.cos(x[2]) * dt x[1] v * np.sin(x[2]) * dt x[2] v * np.tan(delta) / L * dt v np.clip(v a * dt, 0.0, 20.0) x[3] v traj.append(x.copy()) return np.array(traj)这里的control_seq是MPC内部优化要决定的控制序列而Np、权重矩阵等属于外层超参数两者不要混在一起。前向欧拉在dt0.05s时精度够用np.clip限制纵向速度非负且不超过20m/s否则预测轨迹会出现倒退这种明显不合理结果。2.2 代价函数与约束把控制目标写成可计算指标在MPC里每个控制周期会求解一个有限时域最优控制问题目标函数通常由四部分组成横向跟踪误差、航向误差、控制量幅值和控制增量。写出来就是[ J\sum_{k0}^{N_p-1} (q_{lat}e_{lat,k}^2q_{psi}e_{psi,k}^2)\sum_{k0}^{N_c-1}(r_\delta \delta_k^2r_a a_k^2) ]贝叶斯优化的目标函数不再是MPC内部的 (J)而是整段轨迹跑完后的累计代价。这里最容易犯的错是把内外两个代价函数搞混。我自己写外层评价函数时会这样组织def outer_cost(params, scenes): Np int(round(params[0])) q_lat, q_psi, r_delta, r_accel params[1:] total 0.0 for scene in scenes: state scene[init_state] ref scene[ref_points] for step in range(scene[total_steps]): control solve_mpc(state, ref, Np, q_lat, q_psi, r_delta, r_accel) state simulate_step(state, control) e_lat, e_psi compute_errors(state, ref) total q_lat * e_lat**2 q_psi * e_psi**2 total r_delta * control[0]**2 r_accel * control[1]**2 return total / len(scenes)solve_mpc返回当前周期的最优控制量simulate_step用 2.1 节的predict_states推进一步。这里故意把q_lat等参数同时放进MPC代价和外层代价优化器才能真正找到最小化“实际跟踪误差”的参数而不是只让MPC内部目标变小。超参数初值可以参考下表参数含义常见范围对行为的影响Np预测时域步数10~40太小弯道看不见太大模型失配累积Nc控制时域步数5~15控制自由度过大容易抖q_lat横向偏差权重0.1~10越大贴线越紧可能压过速度变化q_psi航向偏差权重0.1~10与q_lat耦合需要同数量级r_delta转角惩罚0.001~1越大转向变化越慢r_accel加速度惩罚0.001~1越大加减速越平缓注意这里的范围只是起点。横向误差单位是米转角单位是弧度直接把数字丢给优化器会让r_delta的搜索范围被q_lat的大数值淹没所以后续搜索空间要用对数均匀分布定义。2.3 为什么手动整定在车辆MPC里异常困难车辆MPC跟常见线性控制器调参不同状态方程里的 (v) 和 (\psi) 是耦合的车速越高相同预测时域下车辆走过的弧长越长错误的Np会被速度放大。传统网格搜索在5个超参数的维度上完全不可行有人用经验公式定初值但在路径曲率分布不均时经常失效。而且仿真评价本身有噪声手动比较两组参数时很难判断差异来自参数还是初始化扰动。手动调参时可以看到一些规律弯道入口处误差尖峰倾向于调大Np弯道内持续偏移倾向于调大q_lat转向反复抖动倾向于调大r_delta。但这些规则在多个参数互相影响时会自相矛盾所以需要一个能够用少量样本逼近高维非平滑函数的优化方法贝叶斯优化正好具备这种样本效率。3. 贝叶斯优化替代网格搜索高斯过程与采集函数3.1 高斯过程如何用稀疏样本估计代价面贝叶斯优化把参数 (p) 到外层代价 (c) 的映射看成黑盒函数。高斯过程GP给它一个分布式先验任意有限个点上的函数值服从联合高斯分布。核函数决定两个参数组合下的代价相关性我优先选Matern 5/2它在车辆控制这类代价面上比RBF表现更稳不会把远处的样本点假设得过于相关。再就是观测噪声。如果仿真完全确定noise参数可以给到 (10^{-6})GP会相信每个样本点后验方差在采样点附近非常小。但真实ROS回放或带随机障碍的仿真中同一参数跑两遍结果不完全一样此时要把noise设成重复实验的方差否则GP会把随机波动当成真实梯度采集函数会反复探索已经看过的区域。这个细节是新手最容易忽略的直接决定优化曲线最后是收敛还是原地抖动。3.2 EI与UCB采集函数决定下一组参数有了后验分布下一步是选择下一个采样点。期望改善EI计算候选点相对当前最优值的提升期望既看后验均值也看方差置信上界UCB则是均值加若干倍标准差(k) 越大越鼓励探索。对车辆跟踪这种单次评价几十秒的任务我一般用EI较少用UCB因为UCB对高斯过程的方差尺度更敏感写死在代码里容易调出非常激进或保守的 (k)。实际实现可以直接用scikit-optimize不必自己实现GP求逆。示例from skopt import gp_minimize from skopt.space import Real, Integer from skopt.utils import use_named_args space [ Integer(10, 40, nameNp), Real(0.1, 10.0, nameq_lat, priorlog-uniform), Real(0.1, 10.0, nameq_psi, priorlog-uniform), Real(0.001, 1.0, namer_delta, priorlog-uniform), Real(0.001, 1.0, namer_accel, priorlog-uniform), ] use_named_args(space) def outer_cost(**params): # 调用2.2节的closed loop评价返回标量 return run_closed_loop(params) res gp_minimize( outer_cost, space, n_calls80, n_initial_points15, acq_funcEI, noise1e-6, random_state7 ) print(res.x, res.fun)代码里用priorlog-uniform是因为权重参数通常跨越数量级线性均匀采样会让小数值区间几乎采不到点。n_calls80是总评价次数n_initial_points15是先随机探索的点数两者都按一次闭环仿真的耗时做预算。random_state固定之后同一份数据可以复现完整优化过程写报告时对比实验更有说服力。3.3 贝叶斯优化、随机搜索与遗传算法的取舍方法5维空间建议次数优势短板网格搜索10^5以上实现最简单维度灾难不能接受随机搜索200以上覆盖均匀并行容易不利用历史信息遗传/粒子群300以上容易写进并行种群大、早熟参数多贝叶斯优化50~100单次评价昂贵时最划算GP拟合代价随迭代上升这个取舍结论不能反过来。如果MPC的一次仿真只要0.1秒我可能直接用随机搜索跑5000次还能天然覆盖更多局部极小。贝叶斯优化适合单次评价贵到需要控制次数的场景这也是毕设选择它的理由。4. 从ROS bag到CSV的闭环数据流与优化实现4.1 数据导出的第一步把bag和csv对齐项目文件里2023-03-10-16-01-03.bag是原始记录spline.csv是参考轨迹auto_0.csv、auto_113.csv等是按优化轮次保存的控制输出。我按这个命名来理解每一轮贝叶斯优化选出一组参数跑一遍仿真后把状态和控制量写进auto_i.csv代价函数再读回这些csv计算指标。这样避免了同一进程里仿真和代价计算强耦合调试更简单。先处理bag文件rosbag info 2023-03-10-16-01-03.bag python -m rosbags.convert 2023-03-10-16-01-03.bag --dst . --format csvrosbags是纯Python库不用安装完整ROS环境就可以解包。转完之后每个topic一个csv先查看字段名把odom的位置、速度、姿态以及cmd_vel的期望速度、转角取出来然后按时间戳做最近邻插值对齐。不同topic的时间戳不能直接merge最好重采样到统一时刻序列通常是50Hz。接着加载参考轨迹和控制输出import pandas as pd ref pd.read_csv(spline.csv) auto pd.read_csv(auto_0.csv) print(ref.columns) print(auto.columns)如果列名不规范先做重命名不要靠位置索引。spline.csv的x,y是参考线的坐标点auto_0.csv里则是当前参数下仿真出来的实际轨迹。auto_*.csv的数量和优化轮次对应例如auto_0.csv是初始随机点的结果auto_113.csv是第113轮评价的结果这样代价函数不需要在优化进程里维护额外状态直接读文件就行。4.2 用横向误差计算代价从全局坐标到最近点投影离线评价误差最直接的方式是求实际位置到参考线最近点的横向偏差import numpy as np def lateral_error(px, py, ref_x, ref_y): diff_x px - ref_x diff_y py - ref_y dist2 diff_x ** 2 diff_y ** 2 idx int(np.argmin(dist2)) return float(np.sqrt(dist2[idx])), idx逻辑不复杂但它对参考线采样质量很敏感。如果spline.csv里点的间隔不均匀最近点索引会来回跳横向误差曲线出现毛刺。我一般先对参考线做样条重采样from scipy.interpolate import splprep, splev tck, u splprep([ref[x].values, ref[y].values], s0) ref_dense np.array(splev(np.linspace(0, 1, 5000), tck)).Tsplprep的s0表示完全插值如果原始点有噪声可以适当给一个非零s做平滑。重采样后再做最近点投影误差计算稳定得多。注意这里的“横向误差”是全局坐标下的欧氏距离近似不等于Frenet坐标系里的严格横向误差但在低速场景下已经足够用来比较不同MPC参数的优劣。4.3 优化主循环把仿真、评价、记录串起来主循环里只需要做三件事调用贝叶斯优化器、运行闭环仿真、记录中间结果。示例import json, time from skopt import gp_minimize history [] def run_closed_loop(params): t0 time.time() cost simulate_and_evaluate(params) history.append({ params: list(params), cost: cost, elapsed: time.time() - t0 }) return cost res gp_minimize(run_closed_loop, space, n_calls60, n_initial_points12, acq_funcEI, random_state42) with open(best_params.json, w) as f: json.dump({x: res.x.tolist(), fun: float(res.fun)}, f, indent2)history里保存了每次参数和耗时优化结束后画出cost随len(history)的变化可以看到初始阶段快速下降后面逐渐平缓。如果完全不下降先怀疑空间定义或代价函数写错再怀疑贝叶斯优化参数设得不对不要急着调采集函数。best_params.json用于后续局部搜索临时结果不要只放在 notebook 里。4.4 用finetune和test判断过拟合我把finetune.csv这一组数据交给优化器反复评价test.csv单独留到最后验证。原因很简单如果优化器在调参过程中见过test数据它的“最优”就可能是在使test拟合结果好而不是真正泛化。下面是常见现象和处理方式现象可能原因处理方法finetune代价持续下降test代价不降优化器在调参集上过拟合增加场景数量限制Np上限finetune与test代价都高搜索空间不在合理区域扩大范围检查单位归一化优化后期代价抖动仿真随机性、代价不平滑固定随机种子或同参数取3次平均test看起来比finetune更好数据集划分不合理按轨迹长度分层划分不要随机切提示不要在优化中途因为test差而修改搜索空间这会相当于把test信息透露给优化器最终报告里的数字没有说服力。5. 从仿真到实车参数迁移与鲁棒性验证技巧5.1 多场景联合评价不要只在一条spline上优化。我一般准备三段数据低速大曲率、中速直线、带加减速的工况。每段代价先除以各自参考值再求和避免高速段误差数值大而主导优化结果。如果只有finetune.csv和test.csv两份数据也可以把finetune.csv按轨迹长度切出三个子段分别计算代价后平均。5.2 单位归一化横向误差、航向、转角、加速度的数量级差异很大。我会给每个误差项设一个物理上合理的参考值比如横向偏差0.3m、航向偏差0.2rad、转角0.3rad、加速度1.0m/s²在代价函数里先除以这些参考值再加权。这样贝叶斯优化对每个参数的探索才公平否则横向误差以米为单位数值上天然比转角以弧度为单位大一个数量级搜索会被带偏。5.3 用贝叶斯优化结果做热启动再局部搜索GP到后期仍然在探索未知区域所以优化到60~80轮后不是最细粒度收敛。可以取res.x作为初值用scipy.optimize.minimize做局部精修from scipy.optimize import minimize def local_cost(params): params params.copy() params[0] int(round(params[0])) return run_closed_loop(params) x0 list(res.x) bounds [(10, 40), (0.1, 10), (0.1, 10), (0.001, 1), (0.001, 1)] res_local minimize(local_cost, x0, methodL-BFGS-B, boundsbounds, options{maxiter: 30})L-BFGS-B利用梯度如果代价面噪声大会出现原地踏步换成methodNelder-Mead并设置maxiter会更稳。Np这种整数参数我倾向于不动只精修四个权重参数结果更容易解释。5.4 固定随机种子与重复评价在ROS或仿真环境里车辆初始状态和路面扰动会让同一参数代价不同。优化前先固定np.random.seed(7)并在场景初始化阶段给随机发生器一个确定种子。如果随机源在C侧不好控制就每个参数重复3次取均值作为外层代价并把重复间方差一起存进history。重复次数增加到3次后GP的噪声估计会明显变小优化收敛速度比单纯增加n_calls更划算。所以我的建议是实车参数整定前先做好数据闭环和数据划分再让贝叶斯优化去跑夜间实验。本文还有配套的精品资源点击获取