LM 算法迭代过程动画演示(SLAM)

LM 算法迭代过程动画演示(SLAM) Levenberg-MarquardtLM算法四种优化方法对比总结表先看这个方法收敛速度稳定性Hessian 计算内存开销对初值敏感性典型适用场景最速下降法❌ 慢线性✅ 很稳定❌ 不需要✅ 极小✅ 不敏感简单问题、调试牛顿法✅✅ 极快二次❌ 不稳定✅ 需要完整 Hessian❌ 大❌ 非常敏感光滑凸问题、小规模高斯–牛顿法✅ 快近似二次⚠️ 一般❌ 只用 Jacobian✅ 较小❌ 较敏感非线性最小二乘LM 算法✅ 快 稳✅✅ 很稳定❌ 只用 Jacobian✅ 较小✅ 较鲁棒非线性拟合、SLAM、BA 项目简介本项目通过动画形式直观展示Levenberg-MarquardtLM算法​ 在非线性最小二乘优化中的迭代过程对比两种不同初始值下的收敛行为场景初始值结果 好的初始值[2.5, 1.8, 0.3]✅ 快速收敛到正确解[3.0, 2.0, 0.5] 差的初始值[1.0, 1.0, 0.0]❌ 陷入局部最优无法找到全局最优解 核心功能动画包含6 个子图全方位展示迭代过程左上 / 中上​ — 拟合曲线动态更新当前参数对应的拟合曲线逐步变化左下 / 中下​ — 参数收敛曲线展示 a、b、c 三个参数随迭代的变化趋势右上​ — 参数空间轨迹(a, b) 平面上的搜索路径右下​ — 代价函数下降曲线对数尺度展示目标函数的收敛过程 环境依赖pip install numpy matplotlib pillow ffmpeg-python注意导出 MP4 需要系统安装ffmpeg。 使用方法1. 屏幕实时播放python lm_animation.py按下右上角关闭按钮即可退出。2. 导出为 GIFpython lm_animation.py --save out.gif3. 导出为 MP4python lm_animation.py --save out.mp4可选参数参数默认值说明--fps2帧率帧/秒调大可加速播放--repeat关闭循环播放动画--save无保存路径支持.gif和.mp4 数据说明模型y a · sin(b · x c)真实参数a3.0, b2.0, c0.5数据100 个采样点加入高斯噪声σ0.3迭代历史来自实际 LM 算法运行结果 代码架构lm_animation.py ├── 数据与迭代历史定义 # 真实值、噪声数据、两组实验记录 ├── build_figure() # 创建 2×3 画布与子图布局 ├── make_history_equal_length() # 对齐两组迭代长度便于同步播放 ├── animate() # 构建动画返回 fig / update / n_frames │ └── update(frame) # 每帧刷新所有子图覆盖式重绘 └── main() # 命令行入口支持屏幕播放 文件导出 关键实现思路覆盖式刷新预先创建Line2D对象每帧仅调用set_data()更新数据避免重复创建/销毁图形对象等长对齐短的历史记录通过重复末帧补齐使左右两组实验可同步逐帧对比自适应范围参数曲线和代价函数的 y 轴范围随迭代动态缩放始终聚焦当前关注区域 效果预览动画将直观展示好的初始值如何稳步逼近真实参数差的初始值如何在参数空间中徘徊始终无法跳出局部最优# -*- coding: utf-8 -*- LMLevenberg-Marquardt算法迭代过程动画演示 通过逐步绘制 / 覆盖更新的方式动态展示 LM 算法在 - 好的初始值 - 快速收敛到正确解 - 差的初始值 - 陷入局部最优 两种情况下的迭代过程直观对比。 显示方式默认 matplotlib 内置动画逐帧在同一窗口覆盖刷新 - 按下右上角关闭按钮即可退出 - 也可改用 --save 把动画导出成 gif / mp4 文件 用法: python lm_animation.py # 屏幕实时播放 python lm_animation.py --save out.gif python lm_animation.py --save out.mp4 import argparse import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation # # 1. 数据与两组迭代历史来自你之前的实验 # x_data np.linspace(0, 4 * np.pi, 100) a_true, b_true, c_true 3.0, 2.0, 0.5 y_true a_true * np.sin(b_true * x_data c_true) np.random.seed(42) noise np.random.normal(0, 0.3, 100) y_data y_true noise # 实验1好的初始值 - 收敛 history_good [ [2.5, 1.8, 0.3], [0.3350, 1.7959, 1.2400], [0.4860, 1.8590, 1.5071], [1.5594, 1.9543, 0.8343], [2.8634, 2.0024, 0.4810], [3.0412, 1.9977, 0.5178], [3.0435, 1.9978, 0.5170], [3.0435, 1.9978, 0.5171], ] # 实验2差的初始值 - 局部最优 history_bad [ [1.0, 1.0, 0.0], [-0.1106, 0.7383, 1.6967], [0.5578, 0.7495, 0.7989], [0.3008, 0.6728, 2.1798], [0.4917, 0.8438, 1.2480], [0.4697, 0.6500, 2.4197], [0.4774, 0.7903, 1.5354], [0.5418, 0.6953, 2.1287], [0.5351, 0.7651, 1.6981], [0.5600, 0.7265, 1.9366], [0.5577, 0.7515, 1.7839], [0.5635, 0.7374, 1.8702], [0.5620, 0.7458, 1.8188], [0.5635, 0.7410, 1.8482], [0.5628, 0.7438, 1.8310], ] def cost_of(params): a, b, c params y_pred a * np.sin(b * x_data c) return 0.5 * np.sum((y_data - y_pred) ** 2) def make_history_equal_length(h_short, h_long): 把短的 history 补到和长的一样长最后一帧重复几次 这样左右两个实验可以同步逐帧播放。 if len(h_short) len(h_long): return h_short pad [h_short[-1]] * (len(h_long) - len(h_short)) return h_short pad # # 2. 准备画布一次创建后面只更新数据 - 覆盖式刷新 # def build_figure(): plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, DejaVu Sans] plt.rcParams[axes.unicode_minus] False fig, axes plt.subplots(2, 3, figsize(16, 9)) fig.suptitle(LM 迭代过程动画好的初始值 vs 差的初始值, fontsize15, fontweightbold) # ---- 散点 / 真实曲线左上、中上共用样式 ---- for col, title, color in [(0, 好的初始值逐步收敛, green), (1, 差的初始值陷入局部最优, red)]: ax axes[0, col] ax.scatter(x_data, y_data, s10, alpha0.3, colorgray, label数据点) ax.plot(x_data, y_true, g-, linewidth2, alpha0.7, label真实曲线) ax.set_title(title, colorcolor) ax.set_xlabel(x); ax.set_ylabel(y) ax.grid(True, alpha0.3) ax.set_ylim(-4.5, 4.5) # ---- 参数收敛子图左下、中下 ---- for col in (0, 1): ax axes[1, col] for tv, c in [(a_true, b), (b_true, r), (c_true, g)]: ax.axhline(ytv, colorc, linestyle--, alpha0.3) ax.set_xlabel(迭代次数); ax.set_ylabel(参数值) ax.grid(True, alpha0.3) ax.set_xlim(-0.5, len(history_bad) - 0.5) axes[1, 0].set_title(参数 a / b / c 变化好的) axes[1, 1].set_title(参数 a / b / c 变化差的) # ---- 右上参数空间轨迹a-b 平面 ---- ax axes[0, 2] ax.scatter(a_true, b_true, colorgreen, s180, markerX, zorder5, label真实值) ax.set_xlabel(参数 a); ax.set_ylabel(参数 b) ax.set_title(参数空间 (a, b) 轨迹) ax.grid(True, alpha0.3) ax.set_xlim(-1.0, 3.5); ax.set_ylim(0.4, 2.2) # ---- 右下代价函数 ---- ax axes[1, 2] ax.set_xlabel(迭代次数); ax.set_ylabel(代价函数值) ax.set_title(代价函数下降) ax.set_yscale(log) ax.grid(True, alpha0.3) ax.set_xlim(-0.5, len(history_bad) - 0.5) return fig, axes # # 3. 动画更新函数每一帧把所有曲线覆盖式刷新 # def animate(): fig, axes build_figure() history_good_padded make_history_equal_length(history_good, history_bad) n_frames len(history_bad) # ---- 预先创建空 Line2D后面只 set_data达到销毁前一帧、画新帧的效果 ---- # 左上当前拟合曲线好的 line_fit_g, axes[0, 0].plot([], [], b-, linewidth2.5, label当前拟合) axes[0, 0].legend(fontsize8, locupper right) # 中上当前拟合曲线差的 line_fit_b, axes[0, 1].plot([], [], colororange, linewidth2.5, label当前拟合) axes[0, 1].legend(fontsize8, locupper right) # 左下三条参数曲线好的 pa_g, axes[1, 0].plot([], [], b-o, markersize5, labela) pb_g, axes[1, 0].plot([], [], r-s, markersize5, labelb) pc_g, axes[1, 0].plot([], [], g-^, markersize5, labelc) axes[1, 0].legend(fontsize8, ncol3, loccenter right) # 中下三条参数曲线差的 pa_b, axes[1, 1].plot([], [], b-o, markersize4, labela) pb_b, axes[1, 1].plot([], [], r-s, markersize4, labelb) pc_b, axes[1, 1].plot([], [], g-^, markersize4, labelc) axes[1, 1].legend(fontsize8, ncol3, loccenter right) # 右上轨迹 traj_g, axes[0, 2].plot([], [], b-o, markersize5, linewidth1.5, alpha0.7, label好的初始值) traj_b, axes[0, 2].plot([], [], colororange, markero, markersize4, linewidth1, alpha0.7, label差的初始值) cur_g, axes[0, 2].plot([], [], D, colorblue, markersize10) cur_b, axes[0, 2].plot([], [], D, colororange, markersize10) axes[0, 2].legend(fontsize8, locupper left) # 右下代价 cost_g_line, axes[1, 2].plot([], [], b-o, markersize5, label好的初始值) cost_b_line, axes[1, 2].plot([], [], colororange, markero, markersize4, label差的初始值) axes[1, 2].legend(fontsize8) # 信息文字 info_g axes[0, 0].text(0.02, 0.02, , transformaxes[0, 0].transAxes, fontsize9, vabottom, bboxdict(boxstyleround, fcwhite, alpha0.8)) info_b axes[0, 1].text(0.02, 0.02, , transformaxes[0, 1].transAxes, fontsize9, vabottom, bboxdict(boxstyleround, fcwhite, alpha0.8)) step_text fig.text(0.5, 0.94, , hacenter, fontsize12, colordarkblue) def update(frame): 每帧重新设置所有 Line2D 的数据 覆盖式重绘。 params_g history_good_padded[frame] params_b history_bad[frame] # ---- 拟合曲线 ---- a, b, c params_g line_fit_g.set_data(x_data, a * np.sin(b * x_data c)) a, b, c params_b line_fit_b.set_data(x_data, a * np.sin(b * x_data c)) # ---- 参数曲线到当前帧为止 ---- g_so_far history_good_padded[:frame 1] b_so_far history_bad[:frame 1] xs list(range(len(g_so_far))) pa_g.set_data(xs, [h[0] for h in g_so_far]) pb_g.set_data(xs, [h[1] for h in g_so_far]) pc_g.set_data(xs, [h[2] for h in g_so_far]) xs_b list(range(len(b_so_far))) pa_b.set_data(xs_b, [h[0] for h in b_so_far]) pb_b.set_data(xs_b, [h[1] for h in b_so_far]) pc_b.set_data(xs_b, [h[2] for h in b_so_far]) # ---- 参数空间轨迹 ---- traj_g.set_data([h[0] for h in g_so_far], [h[1] for h in g_so_far]) traj_b.set_data([h[0] for h in b_so_far], [h[1] for h in b_so_far]) cur_g.set_data([g_so_far[-1][0]], [g_so_far[-1][1]]) cur_b.set_data([b_so_far[-1][0]], [b_so_far[-1][1]]) # ---- 代价 ---- cg [cost_of(h) for h in g_so_far] cb [cost_of(h) for h in b_so_far] cost_g_line.set_data(xs, cg) cost_b_line.set_data(xs_b, cb) # 自适应 y 轴范围参数曲线 all_vals ([h[0] for h in g_so_far] [h[1] for h in g_so_far] [h[2] for h in g_so_far]) lo, hi min(all_vals), max(all_vals) axes[1, 0].set_ylim(lo - 0.3, hi 0.3) all_vals_b ([h[0] for h in b_so_far] [h[1] for h in b_so_far] [h[2] for h in b_so_far]) lob, hib min(all_vals_b), max(all_vals_b) axes[1, 1].set_ylim(lob - 0.3, hib 0.3) # 代价 y 轴下界 if cg and cb: ymin min(min(cg), min(cb)) * 0.5 axes[1, 2].set_ylim(bottommax(ymin, 1e-3)) # ---- 文字 ---- a, b, c params_g info_g.set_text(fiter {frame}\na{a:.3f} b{b:.3f} c{c:.3f}\n fcost{cost_of(params_g):.3f}) a, b, c params_b info_b.set_text(fiter {frame}\na{a:.3f} b{b:.3f} c{c:.3f}\n fcost{cost_of(params_b):.3f}) step_text.set_text(f第 {frame} / {n_frames - 1} 步) artists [line_fit_g, line_fit_b, pa_g, pb_g, pc_g, pa_b, pb_b, pc_b, traj_g, traj_b, cur_g, cur_b, cost_g_line, cost_b_line, info_g, info_b, step_text] return artists return fig, update, n_frames # # 4. 入口 # def main(): parser argparse.ArgumentParser(descriptionLM 迭代过程动画) parser.add_argument(--save, typestr, defaultNone, help保存为文件如 out.gif 或 out.mp4) parser.add_argument(--fps, typeint, default2, help帧率默认 2便于看清每步) parser.add_argument(--repeat, actionstore_true, help循环播放) args parser.parse_args() fig, update, n_frames animate() anim FuncAnimation( fig, update, framesn_frames, interval1000 / args.fps, # 每帧间隔ms blitFalse, repeatargs.repeat, ) if args.save: # 保存到文件 if args.save.lower().endswith(.gif): anim.save(args.save, writerpillow, fpsargs.fps) else: anim.save(args.save, writerffmpeg, fpsargs.fps, dpi120) print(f已保存到: {args.save}) else: # 屏幕实时播放 plt.tight_layout(rect[0, 0, 1, 0.94]) plt.show() if __name__ __main__: main()