简介:面向计算机相关专业的课设、大作业与毕业设计场景,这份资源提供了基于UE4和AirSim环境的无人机自主导航与目标跟踪强化学习完整实现,解决了从仿真环境搭建到算法训练验证的链路问题,适合有Python和C++基础、希望快速上手项目实战的学生使用。压缩包共646个文件,含191个Python脚本、167个C++头文件、16个cpp源文件,以及116张PNG图像、65个Markdown说明文档,另有构建脚本、参考配置与配置文件等,整体148.98MB,结构清晰便于按模块检索。核心代码经历过功能测试,可放心下载运行;图像与文档可用于理解训练过程和结果分析,脚本工具辅助环境部署与数据获取。目前已有190人学习下载,作为初期项目立项演示或毕设基础均有较高借鉴价值。
1. 这个课设/毕设方向,难点根本不只在你以为的那个RL算法
如果你选的是“UE4 + AirSim 环境下无人机自主导航与目标跟踪的强化学习算法”这类课设/大作业/毕设题,我猜你最初的预期是:把 DQN 或者 PPO 调通,模型学会飞过去找目标,就大功告成。但实际做过的人都知道,真正花费 60% 时间的地方是前两步——UE4 与 AirSim 的版本匹配,以及如何把“导航”和“跟踪”翻译成强化学习能理解的观测与奖励。这篇笔记就是把这三段路完整走一遍:先是环境搭建,再是状态/动作/奖励设计,然后是 PPO 训练的最小可跑代码,最后是训练翻车时的排查思路。适合正在做课设、打算拿这套方案当毕设主体、或者想从零快速验证“强化学习能不能驱动无人机”的同学。
2. 先把 UE4 和 AirSim 这套环境搭稳:版本匹配与模式选择
2.1 为什么这套仿真架子比算法本身更难搭
AirSim 不是一个独立可执行的程序,它本质上是 UE4 的一个插件。也就是说,你必须先有一个能打开的 UE4 工程,再把 AirSim 插件塞进去编译,最后运行出来的那个窗口才是一个“能飞的四旋翼仿真环境”。这里最容易卡住的是 UE4 与 AirSim 版本锁死的问题:AirSim 的每个 Release 都明确写了它基于哪个 UE4 版本构建,你如果拿 UE4.26 的工程强行配一个只支持 UE4.27 的 AirSim 版本,最常见的结局是编译到一半插件动态库崩溃,或者 UE4 编辑器里出现一堆重定向的模型断链。
我一般会先定 UE4 版本,再按这个版本挑对应的 AirSim 版本。课设场景我建议选 UE4.27 这条线,因为它的编辑器比较稳定,网上能搜到的踩坑记录也最多。另一个需要提前做的决定是操作系统:AirSim 在 Windows 下的构建链最顺,Linux 下如果强行走 UE4 原生编译会很痛苦,需要处理大量的 vcpkg 依赖。不是不能跑,是没必要在生产环境里折腾自己。
2.2 最小可飞环境:从新建 UE 工程到取到第一帧图像
常见做法是把 AirSim 仓库克隆到本地,然后把它的 Unreal 插件目录塞进一个新的 UE4 工程里,再让 UE4 重新编译生成项目文件。下面是 Windows 下我实际会走的步骤。
# 假设你已经通过 Epic Games Launcher 装好 UE4.27 # 1. 克隆 AirSim 仓库到本地 git clone https://github.com/microsoft/AirSim.git cd AirSim # 2. 生成并编译 AirSim 插件 python build.py --target=UE4这里做的事情是:第一步拿到完整源码;第二步用仓库自带脚本生成插件并编译。编译时间取决于机器,一般 10 到 30 分钟,期间会输出大量 C++ 编译日志,看到“Build succeeded”才算插件这一侧完成。
然后新建一个 UE4 的 C++ 工程,把 AirSim 仓库里的Unreal/Plugins/AirSim文件夹整体复制到新工程的Plugins目录下;如果没有这个目录,手动新建一个即可。接着右键工程文件,执行“Generate Visual Studio project files”,再用 Visual Studio 打开工程并编译一次。这一步的作用是让 UE4 识别这个第三方插件,并把它的编辑器模块加载进当前工程。编译完成后,在编辑器的 Play 按钮旁边选择你要启动的地图,建议先用默认的空白地图跑通,而不是一上来就加载城市街区场景。
第一次启动后,如果一切正常,你会看到右上角出现 AirSim 的 API 状态浮层,默认是一辆车的视角。因为我们的目标是无人机,所以下一步是修改settings.json,把仿真模式切到Multirotor。
2.3 settings.json:仿真模式和跟踪场景的关键参数
settings.json是 AirSim 在启动时读取的配置文件,通常在Documents/AirSim下。它决定了整个仿真环境以什么模式运行、传感器开哪些、渲染分辨率多少。这是我的最小可飞配置:
{ "SettingsVersion": 1.2, "SimMode": "Multirotor", "ClockSpeed": 1.0, "ViewMode": "Fpv", "CameraDefaults": { "CaptureSettings": [ { "ImageType": 0, "Width": 640, "Height": 480, "FOV_Degrees": 90 } ] }, "SubWindows": [ { "WindowID": 0, "ImageType": 0, "CameraName": "0", "Visible": false } ] }这里的SimMode决定载具类型,必须写成Multirotor,否则你会拿到一辆车而不是四旋翼。ClockSpeed是仿真时钟倍率,训练时可以调到 1.0 甚至更高,但要注意它在物理碰撞和动力学采样上会带来额外开销,不是越高越好。ViewMode里的Fpv是指把主窗口绑到无人机前视相机视角,方便观察目标跟踪效果。CameraDefaults中的ImageType: 0代表场景彩色图,1 是深度图,2 是分割图,视觉阶段经常用深度图训练,因为深度图对颜色不敏感。SubWindows这一段是可选的,调试时可以在 UE4 窗口里额外挂一个信号画面,但训练阶段建议保持Visible: false,否则渲染开销会让帧率掉得很惨。
改完配置后重启 UE4 工程,你应该能在场景里看到一架无人机,按F5进入仿真后螺旋桨开始旋转,APIControl 状态变为可连接。到这一步,环境侧就算立住了。
3. 把“自主导航+目标跟踪”拆成 RL 能学的状态、动作和奖励
3.1 先决定学什么:目标跟踪任务的马尔可夫化过程
自主导航和目标跟踪在强化学习框架里不是两个独立任务,它们可以被统一成一个“给定相对位置与速度,输出速度指令”的序贯决策问题。导航阶段的目标是静态的,跟踪阶段的目标是运动的,但两者在数学形式上都满足马尔可夫性质:当前时刻的观测足够决定下一时刻的最优动作,不需要回看历史状态。
我见过有的同学把“导航”和“跟踪”分成两个完全不同的模型来训练,其实没有必要。同一个策略网络可以先用静态目标点做预训练,让模型学会“接近目标、悬停、防止越界”,然后再把目标点改成随时间移动的轨迹,做跟踪的微调。这样做的收敛速度比直接端到端学跟踪快很多,因为跟踪任务里目标移动造成的观测变化会让早期奖励非常不稳定。
这里可以顺带提一句离线强化学习的热点。现在像 IQL 这类的离线强化学习算法很流行,但在这个课设场景里我不建议一开始就上。AirSim 仿真本身足够便宜,在线采样成本低,没必要先收集数据集再做离线训练;在线 PPO 起步往往是最稳的。
3.2 观测与动作空间:用相对位置而不是全局坐标
观测空间设计有一个很关键的原则:尽量让策略对不同起点具有平移不变性。如果观测给的是无人机在 UE4 世界坐标系下的全局坐标,那么你在位置 A 学会的策略,换到位置 B 可能完全不适用,因为神经网络的输入分布变了。反过来,如果观测给的是目标相对无人机的偏移量和自身速度,那么无论起点在哪,输入的数值范围都能维持在相对稳定的区间。
我一般会把观测设计成 6 维向量,依次是目标在 NED 坐标系下的 x、y、z 相对偏移,以及机体当前速度的三分量。注意 AirSim 对外提供的位置会有一个坐标系陷阱:AirSim 对外 API 用的是 NED 坐标系,x 指向北,y 指向东,z 向下为正。也就是说,当无人机实际高度是 5 米时,它的 z 值是 -5。很多第一次做 AirSim 强化学习的人就是在这上面翻车:他们用正数表示高度,配出来的结果要么是模型一直在往下扎,要么是把高度控制完全学反了。
动作空间方面,为了降低控制复杂度,不直接输出四路电机转速,而是输出三个通道的速度指令:x 方向速度、y 方向速度、z 方向速度。这三个量通过moveByVelocityAsync发送给底层控制器,由 AirSim 自带的无人机姿态控制器去反解油门和姿态角。这样整体动作空间是连续三维向量,范围先归一化到 -1 到 1 之间,训练时再缩放成实际速度范围。
3.3 奖励设计:导航阶段用距离势场,跟踪阶段加“视野惩罚”
在强化学习里,奖励设计几乎决定了训练上限。下面这张奖励参数表是我在自动导航任务里经常拿来当起点的配置,多数情况下能先把“从随机位置飞到目标附近”学会。
| 奖励项 | 表达式 | 建议初始参数 |
|---|---|---|
| 距离势场 | -0.1 * dist | dist 指当前位置到目标点的 NED 距离 |
| 到达奖励 | +10 | 当dist < 1.0时触发并回合结束 |
| 越界/低高惩罚 | -20 | 当无人机撞地或飞离任务区域时触发 |
| 步数代价 | -0.01 | 每一步扣一点,防止模型原地打转 |
这个奖励函数的思路是:用负距离当作持续的压力信号,让模型每走一步都知道自己离目标更近还是更远;到达奖励是一次性事件,用于强化“最终收敛”这个事实;越界惩罚是安全约束,防止模型为了减少距离势场而直接撞地或飞出范围。
跟踪任务需要在上面这张表上再加两项。第一项是“动态目标距离变化率”,也就是这一帧的相对距离减去上一帧的相对距离。如果距离在缩小,就给一点正反馈;如果距离在拉大,就惩罚。这个项比单纯的距离势场更能帮助模型学会“保持跟随”,因为跟踪时目标自身也在运动,只看绝对距离会导致模型一直在追滞后值。第二项是“视野惩罚”。目标跟踪如果建立在视觉基础上,你需要让奖励与目标是否出现在画面中心挂钩;如果目标在画面外,给一个明显的负奖励。这样模型学的不只是“靠近目标”,而是“在视野里维持目标”。
3.4 训练模式选择:在线 PPO 起步,别急着上复杂 SAC
对于这个领域,深度强化学习算法其实有不少选择。DDPG 适合连续控制,SAC 在样本效率和稳定性上常优于 DDPG,还有基于模型强化学习或者多智能体强化学习等更高级的方向。但我自己的建议是:如果目的是把课设/毕设跑通并拿出令人信服的效果,请从 PPO 开始。
原因有三条。第一,PPO 对超参数的敏感度相对低,在奖励函数不完美的时候也能稳定推进。第二,PPO 是稳定性较好的在线策略算法,不需要回放缓冲区里的旧数据,降低了环境状态分布变化带来的训练震荡。第三,社区资料最多,出问题时你能搜到同场景的解决方案。SAC 虽然样本效率上限更高,但需要调的参数更多,而且目标跟踪任务里奖励信号本身就带有噪声,SAC 的熵项如果平衡不好,会出现长时间不收敛的“假死”状态。先让 PPO 出结果,再考虑换 SAC 做对比,是性价比最高的路线。
4. 用 PPO 把模型跑起来:训练脚本、参数表和第一轮调优
4.1 把 AirSim 环境包成 Gym 接口
无论你用什么库来写 PPO,第一步都是把 AirSim 环境包装成 OpenAI Gym 的接口格式。这一步的价值在于把“和仿真器通信”与“算法优化”解耦开。算法端只认obs、reward、done,至于这些值是从 UE4 里怎么来的,算法不关心。我自己习惯用gymnasium这个库,因为它的 API 更规范,配合 Stable-Baselines3 可以直接进入训练。
下面是一段可以拿去改的基础环境封装代码:
import gymnasium import airsim import numpy as np from gymnasium import spaces from stable_baselines3 import PPO class AirSimNavEnv(gymnasium.Env): def __init__(self, target_ned=(10.0, 10.0, -5.0)): super().__init__() self.client = airsim.MultirotorClient() self.client.confirmConnection() self.client.reset() self.client.enableApiControl(True) self.client.armDisarm(True) self.target = np.array(target_ned, dtype=np.float32) self.max_dist = 100.0 # 观测: 目标相对位置(3) + 自身速度(3) self.observation_space = spaces.Box( low=-1, high=1, shape=(6,), dtype=np.float32 ) # 动作: 三轴速度指令, 范围归一化到 [-1, 1] self.action_space = spaces.Box( low=-1, high=1, shape=(3,), dtype=np.float32 ) def _get_obs(self): state = self.client.getMultirotorState() pos = state.kinematics_estimated.position vel = state.kinematics_estimated.linear_velocity rel = np.array([pos.x_val, pos.y_val, pos.z_val]) - self.target rel_normed = rel / self.max_dist vel_normed = np.array([vel.x_val, vel.y_val, vel.z_val]) / 10.0 return np.clip(np.concatenate([rel_normed, vel_normed]), -1, 1).astype(np.float32) def step(self, action): a = np.clip(action, -1, 1) vx, vy, vz = a * 3.0 # 映射到 [-3, 3] m/s self.client.moveByVelocityAsync(vx, vy, vz, duration=0.5).join() obs = self._get_obs() rel = np.array([obs[0], obs[1], obs[2]]) * self.max_dist dist = np.linalg.norm(rel) reward = -0.1 * dist - 0.01 done = False if dist < 1.0: reward += 10.0 done = True if dist > self.max_dist: reward -= 20.0 done = True return obs, float(reward), done, False, {} def reset(self, seed=None, options=None): self.client.reset() self.client.enableApiControl(True) self.client.armDisarm(True) return self._get_obs(), {}这段代码里有几个细节值得说明。第一个是target_ned里的 z 写成了-5.0,对应海拔 5 米,千万别写成正数。第二个是moveByVelocityAsync的最后一个参数duration=0.5,表示速度指令的执行时长。0.5 秒是一个比较稳妥的折中:太短会导致控制指令频繁切换,仿真器物理求解不稳定;太长会让无人机在碰到障碍之前无法及时转向。第三个是观测归一化:相对位置除以max_dist,速度除以 10,让输入落在相对稳定的范围内。这一步看着不起眼,但极大影响 PPO 的收敛质量。
4.2 PPO 训练的最小代码骨架
环境类写好以后,训练代码会非常短。这里我直接用 Stable-Baselines3 的 PPO,因为它的实现足够可靠,课设阶段没必要重复造轮子。
env = AirSimNavEnv(target_ned=(10.0, 10.0, -5.0)) model = PPO( "MlpPolicy", env, learning_rate=3e-4, n_steps=4096, batch_size=256, n_epochs=10, gamma=0.99, gae_lambda=0.95, clip_range=0.2, verbose=1 ) model.learn(total_timesteps=500_000) model.save("airsim_nav_ppo")这里的n_steps=4096表示每次更新策略前先收集 4096 步经验。由于每一步step对应仿真中 0.5 秒,一轮更新相当于积累了约 2000 秒的飞行数据。batch_size=256表示从这 4096 步里分批采样做梯度更新;n_epochs=10则是对同一批数据复用 10 次,PPO 通过这个多次小步更新来保证策略不会瞬间偏离旧版本太远。gamma=0.99是对未来奖励的折扣,目标跟踪这类任务中,你希望模型稍微看重远期收益,所以设置得大一点没问题。clip_range=0.2是 PPO 截断范围,这个参数通常不需要动,它是 PPO 稳定性的核心保障。
4.3 超参表与第一轮调优的三个看得见的指标
第一次跑的时候,不要一上来就追求在 500 万步内收敛。先用 50 万步观察趋势,重点看三个指标。第一是平均奖励曲线:如果曲线整体在往上走,说明奖励函数和观测设计方向是对的。第二是回合长度曲线:导航任务里如果回合长度在逐步下降,说明模型越来越快地接近目标。第三是每次更新后新旧策略的 KL 散度:如果 KL 散度一直很大,说明学习率设高了,策略更新太激进,需要把learning_rate调小到1e-4或者把clip_range降到 0.1;如果 KL 散度趋近于零,说明训练停滞,可以把学习率提上去一点。
第一次调参最大的坑是只看总奖励曲线,不看回合长度。这两个指标在某些情况下是矛盾的:奖励曲线可能在上升,但无人机其实学会了“在原地打转以规避越界惩罚”。所以要交叉验证,不能只盯 TensorBoard 里的单条曲线。
5. 训练避坑记录:五个高频翻车现场与排查方法
5.1 UE4 工程打开以后黑屏:只有场景没有无人机
现象:工程能启动,场景也在,但看不到无人机,按 F5 也没有飞行器出现;有时看到无人机但相机视角固定在原地,无法控制。
原因:大概率是settings.json里的SimMode写错了,或者工程根本没有加载 AirSim 的运行时模块。更隐蔽的一种情况:你改了配置,但 UE4 启动时读取的是打包目录下残留的配置,不是文档目录里那份。
解决:先检查文档目录下AirSim/settings.json是否存在且格式合法,确认SimMode是Multirotor。然后在 UE4 编辑器的 Output Log 里搜索SimMode,看启动时实际加载的值是什么。如果配置正确但依然没有无人机,就把DefaultEngine.ini里关于游戏默认 Pawn 的设置清空,让 AirSim 的载具生成器接管场景。
5.2 无人机起飞后立刻掉高度或者原地打转:坐标系写反了
现象:代码明确给了“向上”的速度指令,但无人机却往下掉;或者轨迹不是直线,而是绕圈扩大。
原因:AirSim 对外 API 的世界坐标系是 NED,z 向下为正。你在代码里写了moveByVelocityAsync(0, 0, 5),在 NED 坐标里这个 5 是向下的速度,无人机自然往下扎。绕圈扩大则说明方向控制的分量顺序不匹配,x/y 和目标的实际方向对不上。
解决:统一约定在奖励计算和动作发送时都用 NED 坐标。给高度目标时写成负数;如果实在不习惯,就在环境接口内部做一个正负号转换,只在外层保留“正数代表高度”的语义。坐标系的坑是这套方向里最常见的翻车原因,没有任何捷径,建议每写一行坐标代码前先明确现在处在哪个坐标系。
5.3 训练到后期奖励震荡:奖励函数太陡或回合太长
现象:平均奖励前期上升,训练到几万步后开始大幅震荡,有时上一轮更新后效果突然变差。
原因:奖励绝对值太大。比如-0.5 * dist,无人机距目标 30 米时每步奖励 -15,PPO 的价值网络要拟合的这种负数值区间很宽,更新时会因为梯度方向不一致而震荡。另一个原因是回合长度过长,导致价值回报跨度过大,GAE 估计不稳定。
解决:把奖励压缩。常见做法是给距离奖励换一个更平坦的函数,比如-0.1 * log(1 + dist),或者直接用tanh把奖励压在[-1, 1]范围内。同时把回合最大步数限制在 200 步左右,超时强制结束。这样价值网络的回归目标范围小很多,训练会稳定得多。
5.4 训练时帧率掉到个位数:图像分辨率和渲染开销
现象:代码能跑,但一帧step耗时几秒,一个回合收集完需要几分钟,训练几乎无法推进。
原因:最常见的误用是让 UE4 视口保持高质量实时渲染,同时又开了SubWindows里的额外相机画面。图像模式下如果输入给策略的是 1280x720 的彩色图,预处理和网络前向都会消耗大量时间。
解决:训练阶段不要用图像观测,先用 6 维状态向量跑通,这是最高效的方案。如果你确实需要做视觉目标跟踪,就把图像分辨率降到 84x84 或者 64x64,并把Settings里的CaptureSettings分辨率调到对应值。另一个技巧是在 UE4 里把视口窗口缩小,甚至切到后台运行;AirSim 的渲染如果完全不可见,帧率会明显改善。
5.5 改完 settings.json 不生效:配置文件缓存与进程残留
现象:明明把SimMode从Car改成了Multirotor,启动后仍然是车;或者改了相机参数,画面完全没变化。
原因:UE4 可能还在运行旧的仿真进程,新配置没有机会被载入。更深层的原因是 AirSim 读配置时有时会落在不同路径:有的版本优先读工程目录下的配置,有的版本读Documents/AirSim,两个文件并存时你就会看到“改了但没生效”的诡异现象。
解决:每次修改配置后,先彻底退出 UE4 编辑器,再从任务管理器里确认没残留进程。然后统一只保留一个settings.json,删除另一处的重复配置。最后在启动日志里搜索 settings 的完整路径,确认当前加载的是哪一份文件,这一步能省掉大量无效配置时间。
6. 验证你的算法真的能飞:回放、泛化和视觉扩展
6.1 用轨迹回放评估收敛
训练结束不能只看 TensorBoard,要在 UE4 里实际跑一次推理。写一段单独的评估脚本,让训练好的模型从几个固定起点出发,记录每一帧的位置和速度,然后用轨迹曲线来判断行为合理性。一个合格的策略应该满足:路径平滑,没有反复画圈;到达目标点附近后能悬停,而不是在目标点上冲过头;全程高度维持在设定高度附近,不出现突然爬升或下跌。轨迹记录用 AirSim 的仿真时钟同步,加一点误差分析,绘制相对距离随时间下降的曲线,比奖励曲线更有说服力。
6.2 泛化测试:换地图和随机起点
只在一个固定起点和目标点上跑通,不代表算法真的学到了导航能力。我会把评估起点从固定坐标改成随机数,再跑 10 次统计成功率。如果目标点是固定的,模型可能学到了“先朝某个方向飞多少米”这种短期记忆,换起点后直接失效。更好的测试条件是同时更换 UE4 的地图场景,让策略面对新的纹理和障碍布局。AirSim 自带了几套不同场景,哪怕只是切换一个环境,都会显著暴露仿真过拟合问题。课设答辩时拿出多个场景的成功率结果,说服力远高于单场景动画。
6.3 向视觉目标跟踪扩展
状态向量版跑通以后,和“目标跟踪”这个关键词接轨的常见扩展是视觉跟踪。你可以先用 OpenCV 目标跟踪或者 YOLOv11 这类检测器得到目标在图像中的包围框,把框的归一化中心坐标作为观测的一部分;也可以直接把下采样图像交给一个 CNN 编码器,再接 PPO 策略网络。后者的训练成本更高,但视觉特征更鲁棒。我这里更推荐折中方案:检测器给框,策略网络继续吃向量状态,也就是把目标框中心偏移作为跟踪误差信号加入观测,这样既保留了视觉能力,又不至于让训练难度翻倍。如果目标是另一台无人机或地面车辆,则可以在 AirSim 里通过 API 读取目标真值坐标,再把相对位置喂给策略,验证纯跟踪制导逻辑。
这一整套走下来,你会发现真正的收获不见得是那个模型,而是你习惯了“先验证环境、再设计奖励、最后才调算法”的排错顺序。我自己后来每次搭新仿真环境,第一件事永远是花半小时检查坐标方向对不对,第二件事才是写训练循环——这两个习惯帮我省掉的返工时间,足够再跑完一个完整课题。希望帮到你。
本文还有配套的精品资源,点击获取