UE4+AirSim无人机目标跟踪强化学习实战:从环境配置到PPO训练闭环

发布时间:2026/9/12 10:03:09
UE4+AirSim无人机目标跟踪强化学习实战:从环境配置到PPO训练闭环 简介面向无人机自主导航与目标跟踪方向的毕业设计源码包以 UE4 作为三维仿真环境、AirSim 作为高精度飞行模拟器完整实现了强化学习训练链路状态空间构建、动作定义、奖励设计、策略迭代并结合 YOLO 等检测模块完成目标识别与持续跟踪。工程代码体系完整共 646 个文件约 148.95MB其中 191 个 Python 脚本承载核心算法逻辑167 个 HPP 头文件及 16 个 CPP 源码用于 UE4/AirSim 交互接口与编译配置116 张图像和 65 篇 Markdown 记录训练可视化与实验笔记另附 PDF、PPTX、BibTeX 等说明与论文写作辅助文件。目录索引清晰环境脚本与文档生成脚本齐全有利于快速复现实验和跑通训练流程。目前已有 677 人学习下载适合机器人、计算机视觉方向的研究生或高年级本科生作为课题起步和基线参考。 训练日志里 Python 端每步决策 0.2 秒C 侧模拟器已经跑到 80 FPS奖励曲线在 3 万步后开始爬升无人机却总在贴近目标半圈后飞走——UE4AirSim 环境下做无人机自主导航与目标跟踪这是最常见的失败形态。这组毕业设计代码把 UE4 的渲染场景、微软 AirSim 的物理模拟以及强化学习算法串成一个完整闭环感知层用视觉模型提取目标策略层用强化学习输出飞行指令训练层在模拟器里反复试错。它适合正在搭同类实验环境的研究者也适合从 DQN 转 PPO 但一直调不动奖励的读者。这篇内容不贴概念只讲从环境配置、MDP 建模到训练闭环里真正影响结果的环节。2. UE4AirSim外设映射、物理模拟与场景构建2.1 UE4 查询和物理模拟器的区别训练日志里一半的诡异问题来自这里用户经常搜“ues4查询和物理模拟器的区别”这个细节在 AirSim 项目里决定了状态反馈是否可信。UE4 的物理系统分为两个阶段查询scene query和模拟simulation。射线检测raycast、重叠检测overlap属于查询它只对当前物理场景做碰撞判定不推进模拟步进而PhysScene::Simulate才真正把刚体位置、速度往前推一个 tick。AirSim 的无人机刚体控制走的是后者它把机体运动交给 UE4 物理引擎同时通过SimMode决定是否开启电机模型。训练时常见的“无人机贴着墙飞过去了但没收到碰撞惩罚”不是奖励函数写错而是你在查询阶段读的碰撞结果发生在模拟步进之前读到的是上一帧的物理状态。正确做法是把碰撞判定的读取放到物理子步结束之后的回调里或者在 AirSim 的PhysicsWorld里挂一个OnSubstepCompleted监听。以下是一段在 UE4 C 侧读取物理数据的写法// 在 AirSim 自建 Pawn 的 Tick 里做物理结果读取 void AMyUAVPawn::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 物理模拟器更新结束后再取状态避免拿到上一 tick 的脏数据 if (GetWorld()-GetPhysicsScene()-IsSubstepping()) { FPhysSubstepResults SubResults; if (GetWorld()-GetPhysicsScene()-GetSubstepResults(SubResults)) { FVector Velocity SubResults.LinearVelocity; CurrentVelocity FVector(Velocity.X, Velocity.Y, Velocity.Z); } } }这段代码只做一件事把速度读取锁定在物理子步完成之后。IsSubstepping()用于判断物理引擎当前是否处于子步迭代中GetSubstepResults()拿到的是本步刚结算完的刚体速度。如果不做这个判断Tick期间读取的位置可能混合了上一帧的旧值和新值输入给策略网络后表现为“状态跳变”PPO 对这方面比较敏感。2.2 UE4 外接设备映射遥控器通道到底进了哪个输入口项目文件里有AirSim_FrSkyTaranis.bin这是一个带 FrSky Taranis 遥控器配置的固件或设备映射文件。AirSim 对外部遥控器的支持不是直接从 UE4 的 Input 系统读取而是走AirSimRC组件遥控器摇杆信号经过 Joystick 驱动封装再按通道映射成 Roll、Pitch、Yaw、Throttle 四个控制量。如果你在 UE4 里改了默认输入映射AirSim 不会感知到因为它的遥控器逻辑独立于UInputComponent。在settings.json里要这样声明遥控器通道{ SettingsVersion: 1.2, SimMode: Multirotor, Vehicles: { UAV1: { VehicleType: SimpleFlight, RC: { RemoteControlID: 0, AllowAPIWhenDisconnected: true, JoystickChannelMap: { Roll: 0, Pitch: 1, Yaw: 2, Throttle: 3 } } } } }RemoteControlID对应接入系统的物理遥控器编号JoystickChannelMap把遥控器的物理通道映射到无人机控制通道。调用client.getRcState()时返回的roll/pitch/yaw/throttle就会是经过这张映射表换算后的归一化值。验证映射是否正确可以先把遥控器油门推到中间然后调用一次getRcState观察输出是否为 0.5 附近。另一个容易被忽略的点是AllowAPIWhenDisconnected。本项目的训练模式大概率不需要真遥控器脚本通过ioapi发指令时如果把这项设为falseAirSim 会拒绝 API 连接。毕业设计里调试“连接成功但控制不生效”大半是这项配置的问题。2.3 项目脚本与数据收集链路项目根目录的批处理文件其实划分了实验流水线我按项目里最常见的使用顺序列在下方脚本名作用触发时机check_cmake.bat检查 CMake 工具链版本与平台生成器首次构建前clean_rebuild.bat清理 UE4 工程中间缓存并全量重新构建 AirSim 插件插件或设定项变更后getData.bat启动数据采集流程调用 AirSim 的录制接口或自定义采集脚本每次训练前update_mavlibkcom.bat更新 MAVLink 通信库用于和 PX4 或真实飞控通信切换飞控固件时build_docs.bat生成references.bib引用的论文 PDF 或 API 文档写论文前clean_rebuild.bat是全流程中最耗时的一步。AirSim 插件更新 C 头文件后如果只做增量编译经常出现“头文件更新了但蓝图节点不刷新”的情况。用这个脚本把Binaries和Intermediate清空再全量编译问题基本消失。update_mavlibkcom.bat在纯 SimpleFlight 模式下不需要执行但如果你把模拟器切到 PX4 固件MAVLink 库版本对不上会导致第一条 TCP 连接就被对方关闭。换固件时跑一次这个脚本是最稳妥的。3. 状态空间构建、动作空间选择与奖励函数设计3.1 状态空间设计不要把原始传感器拼在一起直接喂网络这套代码里无人机状态由两部分组成自身飞行状态位置、速度、姿态角、角速度和目标状态检测框中心坐标、宽高、目标相对无人机的方位。我见过不少人把这些原始量直接concat后输入神经网络结果训练到后期梯度爆炸。原因在于量纲差的离谱位置是以米为单位的三位数目标框坐标是 0 到 1 的像素归一化值类别置信度又是另一个量级。网络前几层必须花大量样本去学“重新缩放”这件事。项目中实际使用的状态定义如下表状态分量维度来源归一化方式无人机位置3AirSim 的getMultirotorState()除以场景包围盒半径如 100m无人机线速度3同一接口返回的linear_velocity除以最大速度如 15m/s无人机姿态角3由四元数转换得到除以 π目标检测框中心2YOLO 输出图像像素坐标除以图像宽高目标检测框面积1框宽高乘积除以图像面积目标类别置信度1YOLO 输出自动在 0~1 之间给出一个简短的归一化处理片段def build_observation(airsim_state, bbox, img_size(640, 480)): pos np.array(airsim_state[position]) / ARENA_RADIUS vel np.array(airsim_state[linear_velocity]) / MAX_SPEED att np.array(airsim_state[attitude]) / np.pi cx (bbox[0] bbox[2] / 2) / img_size[0] cy (bbox[1] bbox[3] / 2) / img_size[1] area (bbox[2] * bbox[3]) / (img_size[0] * img_size[1]) conf np.array([bbox[4]]) return np.concatenate([pos, vel, att, [cx, cy, area], conf])ARENA_RADIUS要统一取场景里无人机可飞行的最大半径不是取某个具体坐标。如果训练中换过地图这个值必须同步修改否则状态分布发生偏移已经训练好的策略会立刻劣化。目标框里的坐标已经归一化区域面积则用来区分“目标在近处和远处”对判断无人机是否过于贴近目标很有用。3.2 动作空间连续动作与离散意图的取舍AirSim 环境下的无人机控制接口同时支持离散和连续动作设计中要二选一。离散动作空间前进、后退、左右转向、升降在训练早期更容易收敛因为探索空间小环境反馈直接但离散化的缺陷是“左转 30 度”和“左转 31 度”没有区别策略无法产生平滑的控制曲线目标跟踪场景里极易出现振荡。连续动作空间直接输出 Roll、Pitch、YawRate、Throttle 四个通道训练难度更高但学出来的跟踪轨迹是平滑的。这套代码选择的是连续动作空间这和机械臂强化学习实战里的选择逻辑一致机械臂关节角速度命令与无人机四个控制通道都是底层执行单元网络输出的是目标层面的运动意图物理层面的平滑由控制分配器完成。建议在训练初期先跑一个 2 万步的离散版基线确认环境接口和奖励函数没问题再切换到连续动作训练。纯连续控制下PPO 初始action_std建议设在 0.6~0.8随着训练衰减到 0.1这个参数影响探索能力。动作输出需要做数值截断。AirSim 的 API 对异常值不会报错但如果throttle连续输出大于 1.0无人机在物理模拟器里会表现出马达饱和长期的饱和输出会让训练样本分布偏向极端动作策略退化。标准做法是输出前接一层tanh或手工np.clip。3.3 奖励函数贴近目标不等于持续给正奖励目标跟踪场景里最自然的奖励是“离目标越近奖励越大”。但如果单纯把距离的负值作为奖励PPO 学到的策略会倾向于悬停在一个离目标不远不近的安全位置而不是持续逼近目标因为逼近目标的过程伴随着速度风险和姿态抖动。项目里采用势能塑形加稀疏触发的组合def compute_reward(state, bbox, prev_potential): # state 里包含无人机位置和目标位置的 3D 坐标 dist np.linalg.norm(state[target_pos] - state[pos]) potential -dist # 势能差奖励引导无人机向目标移动而不是停在原地吃距离奖励 reward 0.5 * (potential - prev_potential) # 稀疏触发进入目标附近 3 米范围给一次性奖励 if dist 3.0: reward 10.0 # 目标丢失惩罚视觉检测结果持续 N 帧为空视为丢失 if bbox is None: reward - 1.0 # 平滑性惩罚速度过大或姿态角速率过大时轻微扣分 reward - 0.01 * np.linalg.norm(state[linear_velocity]) return reward, potentialpotential - prev_potential是势能差它只在“这一步比上一步更接近目标”时为正。这个设计的要点是避免机器人停在原地不动却持续获得正奖励。0.5是塑形系数值调大收敛快但容易振荡调小了奖励信号被淹没建议范围在0.2~0.8。目标丢失惩罚用的是视觉检测bbox is None这个条件不是距离判断这能让策略同时学会“别把目标跟丢”和“快速寻找目标”两件事。平滑性惩罚的权重要设得很小权重过大会让策略输出永远为零。4. PPO 训练主循环与目标跟踪组合实现4.1 多机协同的 AirSim 客户端封装一个端口只属于一个进程AirSim 默认 RPC 端口是41451这个端口由运行 UE4 项目的进程独占。训练时如果同时启动两个 Python 进程去连同一个端口后连接的进程不会报错但请求会排队表现为训练速度骤降且越跑越慢。正确做法是在训练脚本外层包一层单例客户端一个训练进程只持有一个连接。import airsim class AirSimEnv: def __init__(self, ip127.0.0.1, port41451): self.client airsim.MultirotorClient(ip, port) self.client.confirmConnection() self.client.enableApiControl(True) self.client.armDisarm(True) def reset(self): # 注意AirSim 的 reset 会重置整个 UE4 场景不是只重置无人机 self.client.reset() self.client.enableApiControl(True) self.client.armDisarm(True) return build_observation(self._get_state(), None) def _get_state(self): state self.client.getMultirotorState() return { position: [state.kinematics_estimated.position.x_val, state.kinematics_estimated.position.y_val, state.kinematics_estimated.position.z_val], linear_velocity: [state.kinematics_estimated.linear_velocity.x_val, state.kinematics_estimated.linear_velocity.y_val, state.kinematics_estimated.linear_velocity.z_val], attitude: [state.kinematics_estimated.orientation.x_val, state.kinematics_estimated.orientation.y_val, state.kinematics_estimated.orientation.z_val] }reset()调用的是整个模拟器的重置它会把所有车辆、物体、时间戳都回到初始态。如果只想重置无人机而不动场景里的其他物体不能使用reset()得手动设置位置和速度。训练时推荐用全场景重置因为目标位置也要随机化这个随机化就在reset()后由你的初始化逻辑重新布置。confirmConnection()内部做了握手确认如果 UE4 场景还在加载这个调用会阻塞直到就绪。用它当训练脚本的启动等待逻辑比time.sleep(10)可靠得多。4.2 PPO 网络结构Actor-Critic 双头输出与 C/Python 场景边界项目使用的 PPO 实现是标准的 Actor-Critic 结构。Actor 负责根据当前状态输出动作分布Critic 负责估计状态价值两者共用特征提取层只在输出层分叉。这种结构比完全独立的两个网络参数利用率高训练时 Critic 的梯度也会帮助特征层学到更有区分度的表示。下面给出一个精简实现import torch import torch.nn as nn from torch.distributions import Normal class ActorCritic(nn.Module): def __init__(self, state_dim, action_dim, hidden256): super().__init__() self.feature nn.Sequential( nn.Linear(state_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU() ) self.actor_mean nn.Linear(hidden, action_dim) self.log_std nn.Parameter(torch.zeros(action_dim)) self.critic nn.Linear(hidden, 1) def forward(self, state): feat self.feature(state) mean torch.tanh(self.actor_mean(feat)) std torch.exp(self.log_std.clamp(-2.0, 0.5)) dist Normal(mean, std) value self.critic(feat) return dist, value def get_action(self, state, deterministicFalse): dist, _ self.forward(state) return dist.mean if deterministic else dist.sample()log_std是训练出来的参数它决定探索程度。初始对数标准差为零对应的实际标准差的数值由exp(log_std)计算。clamp(-2.0, 0.5)把探索幅度限制在合理范围太小了策略过早确定太大了奖励曲线要么上不去要么疯狂振荡。tanh把动作均值压缩到(-1, 1)区间与 AirSim 控制接口的输入范围对齐。关于“使用c训练强化学习actor-critic”这个搜索词需要说明一下C 里用 LibTorch 写 Actor-Critic 完全可行部署时也建议这么干但训练阶段不太推荐。项目里的update_mavlibkcom.bat是通信层更新不是训练层。训练适合在 Python 侧完成因为调试、可视化、数据记录都方便训练完成后把权重导出成 TorchScript 或 ONNX再在 C 端做推理。C 端的推理主要吃批量数据的延迟而训练阶段每步都要更新梯度Python 的灵活性远大于工程性能损耗。4.3 卡尔曼滤波目标跟踪策略网络不需要吃原始图像目标跟踪有很多路线可选SAM 类分割模型能输出像素级掩码跟踪精度最高但一帧推理耗时几十毫秒到几百毫秒在每步决策只有 0.2 秒的训练循环里会让数据采集速度直接腰斩。这套代码没有采用 SAM 目标跟踪路线而是用 YOLO 输出检测框再经卡尔曼滤波做跨帧轨迹平滑。策略网络不直接吃图像像素输入是从跟踪模块抽象出来的检测框中心和速度这也使得状态向量维度降到 12 维左右。from filterpy.kalman import KalmanFilter def make_target_filter(): kf KalmanFilter(dim_x4, dim_z2) kf.F np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]]) # 匀速运动模型 kf.H np.array([[1, 0, 0, 0], [0, 1, 0, 0]]) # 观测为中心坐标 kf.P * 1000 kf.R * 5 return kf状态向量是(cx, cy, vx, vy)观测是目标框中心坐标。F矩阵实现匀速运动预测H矩阵把状态映射到观测空间。P初始设为 1000 表示初始速度完全不确定R为 5 表示检测框中心噪声保持在像素级别。得到卡尔曼滤波输出后检测框的抖动被压平PPO 输入状态不会因为相邻帧检测框抖动而出现高频率噪声这对策略稳定性很重要。YOLO 的检测框丢失问题也要在跟踪层兜底。当某一帧 YOLO 没有输出任何目标时不要直接传空值给策略网络而是用卡尔曼滤波的预测输出作为当前观测。连续丢失超过 25 帧再判定目标丢失触发奖励函数里的丢失惩罚。5. 训练验证与三个维度的调参奖励曲线、崩溃检测与决策频率5.1 训练期必须盯三个指标第一是 Episodic Return每个回合累计奖励这个看的是整体收敛趋势。第二是目标距离误差这个值比奖励曲线更直观它不经过奖励塑形能反映真实任务完成度。第三是决策频率在 AirSim 里每步决策之间的间隔如果超过 0.5 秒模态会在控制层面出现断档这种情况下看成功率没有参考价值。建议每 1000 步往 CSV 里记录一次这三项数据。5.2 常见问题排查表现象可能原因排查命令或做法连接127.0.0.1:41451超时UE4 场景未启动或上一个进程占用端口运行clean_rebuild.bat后重启 UE4 editor训练几步后奖励直接 NaN奖励函数出现inf或log_std发散在compute_reward返回前加np.isfinite断言每步训练耗时超过 0.5 秒getData.bat在采集高分辨率图像并阻塞等待把图像读取放到异步线程或用 640x480 分辨率检测框频繁丢失导致奖励骤降DeepSORT 参数过严或卡尔曼R设置过小把max_age提高到 10R放宽到 5策略收敛后无人机动作振荡明显动作空间太大且奖励中缺少平滑惩罚增大 0.01 *5.3 C 与 Python 的部署交接点训练结束后的模型权重要固定。PyTorch 训练产生的.pt文件里包含网络结构与优化器状态部署时只需要推理部分应通过torch.jit.trace冻结图结构再放入 C 端加载#include torch/script.h // 加载训练好的 TorchScript 模型只保留前向推理 auto module torch::jit::load(policy.pt); std::vectortorch::jit::IValue inputs; inputs.push_back(torch::tensor(obs).unsqueeze(0)); at::Tensor action module.forward(inputs).toTensor();C 端控制频率建议与训练一致无人机每 0.2 秒读一次状态、执行一次推理。CUDA 推理的延迟在毫秒级瓶颈通常在图像解码和卡尔曼滤波的同步上。如果目标跟踪检测里用到 YOLOC 端用 ONNX Runtime 加载.onnx模型即可客户端只传目标框坐标给策略进程不共享像素数据这样两个进程解耦任何一个崩溃都不会拉垮另一端。奖励函数和模型权重在同一份文件里要有版本管理。毕业设计后期经常出现“昨天训的策略今天复现不出同样的结果”多半是某次调整了奖励系数但没保留旧权重。把reward_coef.json与模型版本号放一起比任何训练记录图都管用。本文还有配套的精品资源点击获取