news 2026/10/14 10:19:42

基于DQN的赛车游戏连续控制实战:状态设计、奖励工程与训练调参

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于DQN的赛车游戏连续控制实战:状态设计、奖励工程与训练调参

1. 从像素到方向盘:为什么用DQN啃下赛车游戏这块硬骨头

很多人第一次接触深度强化学习,都是从打砖块、走迷宫这类离散动作空间的小游戏入手的。动作就那几个,状态也简单,训练几百轮就能看到智能体学会“往左往右”。但一旦把场景换成赛车游戏,事情就完全不一样了——方向盘转角是连续的,油门和刹车是连续的,速度、位置、朝向角全是连续变量,状态空间大到离谱。这时候如果还想着用表格法或者简单的Q-learning去硬解,基本等于拿算盘去跑深度学习。

我当初选这个Capstone题目,就是冲着这个“连续控制”的难点去的。DQN本身是为离散动作空间设计的,直接拿来做赛车游戏,天然就有一层矛盾:要么把连续动作离散化,要么换算法。但Capstone的要求是用DQN,所以核心工作就变成了——怎么在DQN的框架下,把赛车游戏这个连续控制问题“翻译”成DQN能吃的形式,同时还得让智能体真的学会开车,而不是原地打转或者一头撞墙。

这个项目适合谁呢?如果你已经学过Q-learning、知道神经网络的基本训练流程、能看懂PyTorch或TensorFlow的简单代码,那这个项目就是你的进阶跳板。它不会教你什么是卷积,也不会从头讲梯度下降,但它会逼着你面对真实项目里最头疼的问题:奖励怎么设计、状态怎么表示、训练不稳定怎么办、超参数怎么调。这些东西,看论文是看不出来的,只有自己跑过、崩过、调过,才能真正理解。

我做的这个赛车项目,环境用的是经典的2D俯视视角赛道,车辆有位置、速度、朝向角三个核心状态量,动作空间我最终选了5个离散动作:左转、右转、直行、加速、刹车。注意,这里我把转向和加减速拆成了独立动作,而不是组合动作,原因后面会详细说。状态表示上,我没有直接用原始像素,而是用了赛道传感器射线加车辆自身状态向量的混合表示。这个选择直接决定了训练能不能收敛,也是我踩坑最多的地方。

整篇文章我会按照我实际做项目的顺序来展开:先讲清楚DQN在赛车场景下的核心矛盾,再讲状态和动作空间怎么设计,然后是网络结构和训练流程,接着是奖励函数这个“玄学”部分,最后是我踩过的坑和调参经验。每一部分我都会给出具体的代码片段和参数,你可以直接拿去改改就用。

2. 状态表示与动作空间:DQN在赛车场景下的第一道坎

2.1 为什么不能直接用原始像素

很多人一上来就想复刻DQN打Atari的那套做法:把游戏画面缩到84x84,堆4帧,直接喂给卷积网络。我一开始也是这么干的,结果训练了整整两天,智能体连直线都跑不稳。问题出在哪儿?Atari游戏的画面里,物体边界清晰、背景简单,卷积网络很容易提取出“球在哪里”“挡板在哪里”这种空间特征。但赛车游戏的俯视画面里,赛道是灰的、车是小的、背景还有各种纹理,卷积网络要花大量容量去学“什么是赛道边缘”,而不是“怎么开车”。

更致命的是,原始像素里不包含速度信息。你看到两帧画面,知道车在哪儿,但不知道车跑多快。DQN的输入是单帧或堆叠帧,堆叠帧虽然能隐式提供一点运动信息,但对于赛车这种对速度极其敏感的任务来说,远远不够。我试过堆8帧,训练速度直接慢到无法接受,而且效果提升微乎其微。

所以我的结论很明确:对于2D赛车游戏,用传感器射线加状态向量,比用原始像素靠谱得多。这不是偷懒,而是把领域知识注入到状态表示里,让网络把容量花在决策上,而不是花在理解画面上。

2.2 传感器射线的设计与实现

我最终用的状态表示是一个混合向量,包含两部分:车辆自身状态和赛道传感器读数。

车辆自身状态有5个量:横向位置、纵向位置、速度大小、速度方向与车头朝向的夹角、朝向角的正弦和余弦。注意,朝向角我用的是sin和cos而不是角度值,原因是角度在0度和360度附近有跳变,直接输入网络会让网络学得很痛苦。用sin和cos表示,就变成了连续平滑的二维向量,网络处理起来舒服得多。

传感器射线我设置了9条,从车头开始,均匀分布在车头前方180度范围内,每条射线检测到赛道边界的距离,归一化到0到1之间。9条射线加上5个自身状态,一共14维状态向量。这个维度不算高,全连接网络就能处理,训练速度也快。

import numpy as np def get_state(car, track): # 车辆自身状态 speed = car.speed / car.max_speed # 归一化 angle_diff = normalize_angle(car.velocity_angle - car.heading) sin_heading = np.sin(car.heading) cos_heading = np.cos(car.heading) # 传感器射线 ray_angles = np.linspace(-np.pi/2, np.pi/2, 9) ray_distances = [] for angle in ray_angles: dist = cast_ray(car.position, car.heading + angle, track) ray_distances.append(dist / track.max_ray_distance) state = np.array([ car.x / track.width, car.y / track.height, speed, angle_diff, sin_heading, cos_heading, *ray_distances ], dtype=np.float32) return state

这里有个细节:射线检测的距离要归一化。如果不归一化,距离值可能从0到几百,和前面那些0到1的状态量混在一起,网络训练时梯度会被大数值主导,小数值的特征就学不到了。归一化之后所有输入都在同一量级,训练稳定得多。

2.3 动作空间离散化的取舍

DQN只能处理离散动作,这是它的硬伤。赛车游戏里,方向盘转角是连续的,油门开度也是连续的。怎么离散化,直接决定了智能体能不能学会精细操控。

我试过三种方案:

第一种是粗粒度离散:左转、右转、直行、加速、刹车,5个动作。这是最简单的方案,训练快,但智能体开起来很“愣”,转向是一格一格的,没法走平滑的 racing line。

第二种是细粒度离散:把转向分成7个角度,油门分成3个档位,组合起来21个动作。动作精细了,但动作空间大了4倍多,DQN的探索效率急剧下降,训练时间翻了好几倍,而且很多动作组合在特定状态下是冗余的。

第三种是混合方案:转向和加减速分开,但转向用3个角度(左、直、右),加减速用3个档位(加速、滑行、刹车),组合成9个动作。这个方案在精细度和训练效率之间取得了平衡。

我最终选了第三种。9个动作,网络输出层9个节点,训练速度可以接受,智能体也能做出比较平滑的过弯动作。这里的关键经验是:动作空间不是越大越好,DQN的探索效率随动作空间增大而指数下降。在Capstone这种有限时间内要出结果的项目里,宁可动作粗一点,也要保证能训出来。

注意:动作离散化之后,一定要检查每个动作在物理上是否合理。比如“左转+加速”和“左转+刹车”在高速下可能导致车辆失控,如果环境物理引擎不支持这种组合,就要在动作设计时排除掉,否则智能体会学到一些莫名其妙的策略。

3. 网络结构与训练流程:把DQN跑起来只是第一步

3.1 全连接网络的层数与宽度选择

状态是14维向量,动作是9个,这个规模用全连接网络就够了,不需要上卷积。我一开始用了两层隐藏层,每层128个神经元,ReLU激活。训练下来发现收敛速度还可以,但最终策略不够稳定,有时候能跑完整圈,有时候跑一半就撞墙。

后来我把网络加宽到256个神经元,层数加到三层,情况有所改善,但提升不明显。真正带来质变的是加了Dropout和Layer Normalization。Dropout放在每层全连接之后,概率0.1,防止过拟合;Layer Normalization放在激活函数之前,稳定训练过程。这两个改动让训练曲线平滑了很多,最终策略的方差也小了。

import torch import torch.nn as nn class DQN(nn.Module): def __init__(self, state_dim=14, action_dim=9): super(DQN, self).__init__() self.net = nn.Sequential( nn.Linear(state_dim, 256), nn.LayerNorm(256), nn.ReLU(), nn.Dropout(0.1), nn.Linear(256, 256), nn.LayerNorm(256), nn.ReLU(), nn.Dropout(0.1), nn.Linear(256, 128), nn.LayerNorm(128), nn.ReLU(), nn.Linear(128, action_dim) ) def forward(self, x): return self.net(x)

这里有个坑:Dropout在训练时开启,推理时关闭。如果你忘了在推理前调用model.eval(),Dropout会继续随机丢弃神经元,导致策略随机性很大,车开得时好时坏。我当初就被这个坑了两天,一直以为是超参数问题,后来才发现是忘了切换模式。

3.2 经验回放池的大小与采样策略

经验回放是DQN的核心组件之一,它打破了样本之间的时间相关性,让训练更稳定。但回放池的大小很有讲究。

太小了,比如只存1000条经验,样本多样性不够,网络容易过拟合到最近的经验上,训练会震荡。太大了,比如存100万条,内存吃不消,而且旧经验可能已经过时了,采样到旧经验反而拖累训练。

我最终用的是10万条经验的回放池。这个大小在16GB内存的机器上完全没问题,采样时随机抽64条作为一个batch。采样策略我用的是均匀随机采样,没有用优先经验回放。原因很简单:优先经验回放虽然理论上更高效,但实现复杂,而且需要维护每个经验的TD误差,计算开销大。在Capstone项目里,均匀采样已经足够让智能体学会开车了。

from collections import deque import random class ReplayBuffer: def __init__(self, capacity=100000): self.buffer = deque(maxlen=capacity) def push(self, state, action, reward, next_state, done): self.buffer.append((state, action, reward, next_state, done)) def sample(self, batch_size=64): batch = random.sample(self.buffer, batch_size) states, actions, rewards, next_states, dones = zip(*batch) return ( np.array(states, dtype=np.float32), np.array(actions, dtype=np.int64), np.array(rewards, dtype=np.float32), np.array(next_states, dtype=np.float32), np.array(dones, dtype=np.float32) ) def __len__(self): return len(self.buffer)

回放池的预热也很重要。我一开始就让智能体边跑边训练,结果前几千步网络完全是在拟合随机噪声。后来改成先跑5000步纯随机动作,把回放池填到一定规模再开始训练,效果好了很多。这个预热阶段相当于给网络提供了一批“初始教材”,让它先对状态-动作空间有个大致了解。

3.3 目标网络的更新频率

DQN有两个网络:在线网络和目标网络。在线网络每步都更新,目标网络定期从在线网络复制参数。目标网络的作用是稳定TD目标,防止训练震荡。

更新频率是个关键超参数。我试过每100步更新一次,训练很稳定但收敛慢;试过每10步更新一次,收敛快但训练曲线抖动大。最终我选了每200步更新一次,配合学习率1e-4,训练曲线比较平滑,最终策略也稳定。

这里有个经验:目标网络更新频率和学习率要配合调整。学习率大,目标网络就要更新慢一点,否则TD目标变化太快,网络追不上;学习率小,目标网络可以更新快一点,加速收敛。我一般先固定学习率,然后调更新频率,找到训练曲线最平滑的那个点。

def update_target_network(online_net, target_net, tau=1.0): for target_param, online_param in zip(target_net.parameters(), online_net.parameters()): target_param.data.copy_(tau * online_param.data + (1 - tau) * target_param.data)

我用的不是硬更新,而是软更新,tau=0.005。软更新每步都做,但每次只把目标网络往在线网络挪一小步。这样目标网络的变化更平滑,训练更稳定。硬更新虽然简单,但每次更新都会造成TD目标的突变,容易引起训练震荡。

4. 奖励函数设计:决定智能体是赛车手还是碰碰车

4.1 稀疏奖励的致命问题

如果只给智能体一个“到达终点+1,撞墙-1,其他时候0”的稀疏奖励,会发生什么?答案是:智能体永远学不会开车。因为在随机探索阶段,它几乎不可能碰巧跑完整圈,大部分时候都是撞墙或者原地打转,拿到的奖励全是-1或0。网络从这些经验里学不到任何有用的东西,因为所有动作的Q值都差不多。

我一开始就犯了这个错误,训练了5000轮,智能体还是只会原地转圈。后来我意识到,稀疏奖励在赛车这种长序列任务里基本不可行,必须设计稠密奖励,让智能体每时每刻都能得到反馈。

4.2 稠密奖励的组成与权重

我的稠密奖励由四部分组成:

第一部分是速度奖励。每步给智能体一个正比于速度的奖励,鼓励它跑快。但这里有个陷阱:如果只奖励速度,智能体学会的就是直线加速然后撞墙,因为它不知道撞墙的代价。所以速度奖励必须和其他奖励配合。

第二部分是赛道中心奖励。每步给智能体一个负比于偏离赛道中心距离的惩罚,鼓励它沿着赛道中间跑。这个奖励让智能体学会了沿着赛道走,而不是到处乱窜。

第三部分是朝向奖励。每步给智能体一个正比于车头朝向与赛道切线方向一致性的奖励,鼓励它朝着正确的方向开。这个奖励让智能体学会了过弯时调整车头方向。

第四部分是撞墙惩罚。撞墙时给一个大的负奖励,并结束当前回合。这个惩罚让智能体学会了避障。

def compute_reward(car, track, prev_progress): # 速度奖励 speed_reward = car.speed / car.max_speed * 0.1 # 赛道中心奖励 center_dist = distance_to_center(car.position, track) center_reward = -center_dist / track.width * 0.2 # 朝向奖励 track_angle = get_track_angle(car.position, track) heading_diff = abs(normalize_angle(car.heading - track_angle)) heading_reward = (1 - heading_diff / np.pi) * 0.1 # 进度奖励 progress = get_progress(car.position, track) progress_reward = (progress - prev_progress) * 1.0 # 撞墙惩罚 if car.collision: return -10.0, True total_reward = speed_reward + center_reward + heading_reward + progress_reward return total_reward, False

注意进度奖励这一项。我加了“当前进度减去上一步进度”的奖励,鼓励智能体不断向前推进。这个奖励很关键,因为它给了智能体一个全局的目标感,让它知道“往前开是对的”。没有这个奖励,智能体可能会学会在原地高速转圈,因为速度奖励和朝向奖励都能拿到,但进度为零。

4.3 奖励权重的调参经验

奖励权重的调整是整个项目里最耗时间的部分。我前后调了十几版,才找到一组比较平衡的权重。

速度奖励权重0.1,不能太大,否则智能体只顾加速不顾方向。中心奖励权重0.2,让智能体有动力回到赛道中间。朝向奖励权重0.1,辅助过弯。进度奖励权重1.0,这是主导奖励,让智能体有全局目标。撞墙惩罚-10,足够大,让智能体把撞墙当成严重错误。

这里有个经验:奖励权重的比例比绝对值更重要。你把所有奖励都乘以10,训练效果基本不变,因为网络学的是相对大小。但如果你把速度奖励权重从0.1调到0.5,智能体的行为就会明显偏向加速,过弯质量下降。所以调参时先固定一组比例,然后微调,不要同时改多个权重。

注意:奖励函数设计完后,一定要可视化智能体的行为。我当初调了一组权重,训练曲线很好看,奖励一直在涨,但看回放发现智能体学会了“贴墙跑”——紧贴着赛道边缘高速行驶,因为这样速度奖励高,中心惩罚也不算太大。后来我把中心惩罚权重加大,才把这个行为纠正过来。训练曲线好看不代表策略合理,一定要看实际行为。

5. 训练不稳定与过估计:DQN在赛车任务里的典型病症

5.1 训练震荡的排查链路

训练不稳定是DQN的常见问题,但在赛车任务里尤其严重。我遇到的现象是:训练曲线在某个阶段突然崩掉,奖励从正变负,然后再也涨不回来。

排查过程是这样的:先看回放,发现智能体在崩掉之后学会了“原地转圈”,一直转,直到回合结束。然后看Q值,发现所有动作的Q值都变得很大,而且彼此之间差异很小。这说明网络过估计了,而且过估计得很均匀,导致智能体无法区分好坏动作。

根因是TD目标里的max操作。DQN的TD目标是r + gamma * max(Q_target(s', a')),这个max操作会系统性地高估Q值。在赛车任务里,因为状态空间大、奖励稠密,过估计问题比Atari游戏更严重。

5.2 Double DQN的引入与效果对比

解决过估计的标准方案是Double DQN。它的核心改动很小:用在线网络选动作,用目标网络评估动作值。这样就把“选择”和“评估”解耦了,过估计大大减轻。

def compute_td_target(rewards, next_states, dones, online_net, target_net, gamma=0.99): with torch.no_grad(): # 在线网络选动作 next_actions = online_net(next_states).argmax(dim=1, keepdim=True) # 目标网络评估动作值 next_q_values = target_net(next_states).gather(1, next_actions).squeeze(1) targets = rewards + gamma * next_q_values * (1 - dones) return targets

改成Double DQN之后,训练曲线明显稳定了,崩掉的次数少了很多。但也不是完全解决,偶尔还是会震荡。后来我又加了梯度裁剪,把梯度范数限制在10以内,训练才彻底稳定下来。

5.3 梯度裁剪与学习率调度

梯度裁剪是训练深度强化学习模型的标配。赛车任务里,奖励的方差比较大,偶尔会出现很大的TD误差,导致梯度爆炸。梯度裁剪把梯度范数限制在一个范围内,防止参数更新过猛。

optimizer.zero_grad() loss = nn.MSELoss()(q_values, targets) loss.backward() nn.utils.clip_grad_norm_(online_net.parameters(), max_norm=10.0) optimizer.step()

学习率调度我也加了。一开始用1e-3,训练快但震荡大;后来改成1e-4,稳定但收敛慢。最终我用的是余弦退火调度,从1e-3开始,逐渐降到1e-5。这样前期收敛快,后期稳定。

scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10000, eta_min=1e-5)

这套组合拳打下来,训练曲线终于变得比较平滑了。智能体从随机乱撞到能跑完整圈,大概用了8000个回合。这个数字不算快,但考虑到赛车任务的难度,我觉得可以接受。

6. 从训练到部署:模型保存、推理优化与效果评估

6.1 模型保存与加载的坑

模型保存看起来简单,但有几个坑。第一,保存时要同时保存网络参数和优化器状态,否则恢复训练时优化器的动量信息丢失,训练会有一个明显的性能下降。第二,保存时要记录当前的训练回合数和探索率,否则恢复训练时探索率从头开始,会破坏已经学到的策略。

def save_checkpoint(online_net, target_net, optimizer, episode, epsilon, path): torch.save({ 'online_net': online_net.state_dict(), 'target_net': target_net.state_dict(), 'optimizer': optimizer.state_dict(), 'episode': episode, 'epsilon': epsilon }, path) def load_checkpoint(path, online_net, target_net, optimizer): checkpoint = torch.load(path) online_net.load_state_dict(checkpoint['online_net']) target_net.load_state_dict(checkpoint['target_net']) optimizer.load_state_dict(checkpoint['optimizer']) return checkpoint['episode'], checkpoint['epsilon']

6.2 推理阶段的性能优化

训练完之后,推理阶段不需要计算梯度,可以用torch.no_grad()包裹,速度能快不少。另外,推理时要把网络设为eval模式,关闭Dropout和LayerNorm的训练行为。

online_net.eval() with torch.no_grad(): state_tensor = torch.FloatTensor(state).unsqueeze(0) q_values = online_net(state_tensor) action = q_values.argmax(dim=1).item()

如果要在CPU上跑推理,还可以把模型转成ONNX格式,用ONNX Runtime加速。我实测下来,ONNX Runtime比原生PyTorch在CPU上快大概30%,对于实时性要求高的场景很有用。

6.3 效果评估的指标与方法

评估强化学习模型不能只看训练奖励,因为训练奖励是在探索策略下得到的,不代表最终策略的性能。我用了三个指标:

第一个是平均完赛率。跑100个回合,看有多少回合能跑完整圈。我的模型最终完赛率是85%左右,剩下15%大部分是起步阶段撞墙。

第二个是平均完赛时间。跑完整圈的回合里,平均用了多少时间。这个指标反映的是策略的精细程度,时间越短说明 racing line 走得越好。

第三个是撞墙率。每100步里撞墙多少次。这个指标反映的是策略的安全性。

def evaluate(model, env, num_episodes=100): model.eval() completion_count = 0 total_time = 0 collision_count = 0 total_steps = 0 for _ in range(num_episodes): state = env.reset() done = False steps = 0 while not done: action = model.select_action(state, epsilon=0.0) state, reward, done, info = env.step(action) steps += 1 if info['collision']: collision_count += 1 total_steps += steps if info['completed']: completion_count += 1 total_time += steps completion_rate = completion_count / num_episodes avg_time = total_time / max(completion_count, 1) collision_rate = collision_count / total_steps return completion_rate, avg_time, collision_rate

评估的时候要注意,一定要用固定的随机种子,否则每次评估结果波动很大,没法比较不同模型的好坏。我一般跑3次评估,取平均值。

7. 踩坑实录与调参心得

7.1 状态归一化没做好,训练直接不收敛

这是我踩的第一个大坑。一开始我觉得状态量都是物理量,直接输入网络就行了。结果训练了几千轮,损失函数一直在震荡,Q值要么全正要么全负,智能体完全学不到东西。

后来我把所有状态量都归一化到0到1之间,训练立刻就正常了。原因很简单:神经网络对输入尺度很敏感,如果输入量有的在0到1,有的在0到1000,网络的第一层权重会被大数值主导,小数值的特征就学不到了。归一化之后所有输入在同一量级,网络才能均衡地学习每个特征。

注意:归一化要用训练集的统计量,不能用测试集的。但在强化学习里,状态分布是随策略变化的,所以最好用运行均值来动态归一化。我用的是一种简化方案:根据环境的物理边界做固定归一化,比如位置除以赛道宽度,速度除以最大速度。这样简单可靠,不需要维护运行统计量。

7.2 探索率衰减太快,智能体陷入局部最优

探索率epsilon控制智能体随机探索的概率。我一开始设的是从1.0线性衰减到0.1,用了5000步。结果智能体很快就学会了“直行加速”这个局部最优策略,因为直行能拿到速度奖励,而且不会撞墙(赛道是直的)。但它永远学不会过弯,因为过弯需要先减速再转向,这个动作序列在随机探索阶段很难碰巧发现。

后来我把衰减周期拉长到50000步,并且把最终epsilon从0.1降到0.01。这样智能体有足够的时间去探索各种动作组合,最终学会了过弯。这里的关键经验是:探索率衰减周期要和任务难度匹配。任务越难,需要的探索时间越长。赛车任务里,过弯是一个复杂的动作序列,需要大量探索才能发现,所以衰减周期不能太短。

7.3 回放池预热不足,前期训练全是噪声

前面提到过,我一开始让智能体边跑边训练,结果前几千步网络完全在拟合随机噪声。后来改成先跑5000步纯随机动作,把回放池填到一定规模再开始训练,效果好了很多。

这个预热阶段相当于给网络提供了一批“初始教材”。如果回放池里只有几百条经验,而且都是随机动作产生的,网络从这些经验里学不到任何有用的模式。预热到5000条之后,回放池里有了一定的多样性,网络才能开始学到一些基本规律。

7.4 奖励函数调参的“玄学”与科学

奖励函数调参是整个项目里最像“玄学”的部分。我前后调了十几版,才找到一组比较平衡的权重。但后来我总结出了一些科学的方法:

第一,先固定一组初始权重,跑一次训练,看回放。如果智能体学会了“贴墙跑”,说明中心惩罚太小;如果智能体学会了“原地转圈”,说明进度奖励太小;如果智能体学会了“直线加速然后撞墙”,说明撞墙惩罚太小。

第二,每次只调一个权重,其他保持不变。同时调多个权重,你根本不知道是哪个起了作用。

第三,用评估指标来指导调参,不要只看训练奖励。训练奖励高不代表策略好,要看完赛率、完赛时间、撞墙率这些客观指标。

第四,奖励权重的比例比绝对值更重要。你把所有奖励都乘以10,训练效果基本不变。但如果你把速度奖励权重从0.1调到0.5,智能体的行为就会明显偏向加速。

7.5 训练时间与硬件配置的平衡

赛车游戏的训练时间比Atari游戏长得多,因为状态空间大、奖励稠密、动作序列长。我在一台配置了中端GPU的机器上训练,跑完8000个回合大概用了6个小时。如果只用CPU,时间要翻好几倍。

这里有个经验:如果你的机器配置有限,可以适当减小网络规模、减小回放池大小、减少传感器射线数量。这些都会降低训练时间,但也会影响最终性能。我建议先在小规模配置上跑通流程,确认代码没问题,再放大配置跑完整训练。

另外,训练过程中要定期保存模型。我一般每500个回合保存一次,防止训练崩掉之后从头再来。保存的时候要同时保存优化器状态和探索率,方便恢复训练。

8. 这个项目还能怎么玩:扩展方向与进阶思路

8.1 从DQN到连续控制算法

DQN的离散动作空间是它的硬伤。如果你想让智能体做出更平滑的操控,可以换成DDPG、TD3或SAC这些连续控制算法。这些算法直接输出连续的动作值,不需要离散化,过弯会更平滑,racing line 会更接近人类车手。

但换算法不是没有代价的。连续控制算法的训练通常比DQN更不稳定,超参数更敏感,调试时间更长。如果你时间有限,DQN加离散动作已经能跑出不错的效果了。

8.2 多传感器融合与视觉输入

我用的传感器射线加状态向量的方案,优点是训练快、可解释性强。但它的缺点是信息量有限,智能体看不到赛道全局,只能看到局部。如果你想进一步提升性能,可以加入视觉输入,用卷积网络提取赛道特征,和传感器特征融合。

这个方案我在后期尝试过,效果有提升,但训练时间翻了三倍。而且卷积网络的调参比全连接网络复杂得多,需要更多的耐心。如果你要做这个扩展,建议先把传感器方案跑通,再逐步加入视觉输入。

8.3 课程学习与迁移学习

课程学习是一种很实用的技巧:先让智能体在简单赛道上训练,学会基本操控,再迁移到复杂赛道上。这样可以大大缩短训练时间,因为智能体不需要从零开始探索。

我试过在直道赛道上预训练,然后迁移到弯道赛道。预训练用了2000个回合,迁移后只需要3000个回合就能跑完整圈,比直接从零训练快了将近一半。迁移的时候,网络的前几层可以冻结,只训练后面的层,这样能保留预训练学到的底层特征。

8.4 多智能体竞速与对抗

如果你想让项目更有趣,可以加入多个智能体,让它们在同一条赛道上竞速。这时候奖励函数要加入相对位置的考量,比如“超过前车+奖励,被后车超过-惩罚”。多智能体训练比单智能体复杂得多,因为环境是非平稳的,每个智能体的策略都在变化。但这也是一个很有挑战性的扩展方向,适合对多智能体强化学习感兴趣的人。

我个人在实际操作中的体会是,这个Capstone项目最大的价值不在于最终模型跑得多好,而在于你亲手经历了深度强化学习从环境搭建、状态设计、奖励工程到训练调参的完整流程。这些经验是看论文和跑Demo学不到的。如果你也在做类似的项目,我的建议是:先把最简单的版本跑通,哪怕性能很差,然后再逐步优化。不要一上来就追求完美,那样很容易卡在某个细节上,最后什么都做不出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 10:19:35

CSAPP第五章性能优化:从测量归因到硬件协同的实操指南

1. 项目概述:为什么《CSAPP》第五章答案值得单独深挖 《CSAPP》(Computer Systems: A Programmer’s Perspective,中文名《深入理解计算机系统》)第3版第五章——“优化程序性能”,是整本书里最让初学者头皮发紧、又让…

作者头像 李华
网站建设 2026/10/14 10:19:17

pikachu Cross-Site Scripting(XSS)

漏洞描述XSS漏洞描述 跨站脚本攻击(XSS)属于Web常见注入类漏洞,攻击者向网页输入点注入恶意JavaScript脚本,服务器未对用户输入做有效过滤、转义,导致恶意代码被直接输出到页面中,当其他用户访问该页面时&a…

作者头像 李华
网站建设 2026/10/14 10:18:50

SwiftUI Skill:纯本地可组合式异步能力封装体系

1. 项目概述:这不是一个“AI Agent”,而是一套 SwiftUI 原生技能封装体系“SwiftUI-Agent-Skill”这个标题,乍看容易让人联想到当前泛滥的“AI Agent”概念——大模型调度、工具调用、自主规划……但实际拆解下来,它完全不涉及任何…

作者头像 李华
网站建设 2026/10/14 10:18:33

Flutter集成端侧嵌入模型:EmbeddingGemma 2本地语义检索实战

做 Flutter 的开发者应该都有同感:产品想靠 AI 加分,但把能力接进来之后,最先撞上的往往不是模型效果,而是网络依赖、密钥管理、API 账单和飘忽不定的首字节延迟。我最近把一个模拟项目完整跑通了:在 Flutter 应用里集…

作者头像 李华
网站建设 2026/10/14 10:18:32

校园兼职系统SSM架构设计与高并发优化实践

1. 项目背景与核心价值 校园兼职系统是连接学生与企业的重要桥梁,传统的人工对接方式存在信息不对称、匹配效率低等问题。这个基于SSM框架的分布式系统,采用SpringBootVueSpringCloud技术栈,实现了兼职信息的智能匹配、信用评价和全流程管理。…

作者头像 李华