news 2026/8/31 21:06:45

DQN入门实践:从零实现MountainCar小车爬山

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DQN入门实践:从零实现MountainCar小车爬山

简介:本资源是一份面向强化学习初学者与Python开发者的实战项目,聚焦Deep Q-Network(DQN)算法在经典控制任务MountainCar中的完整实现,解决智能体如何通过试错学习克服物理约束、自主达成目标的核心问题。压缩包共2个文件(1个PyTorch/TensorFlow兼容的h5模型权重文件用于加载训练成果,1个结构清晰的Python主程序文件含环境交互、经验回放、双网络更新及ε-greedy策略实现),总大小仅6KB,轻量但功能完备。已有880人下载学习,适合高校课程实践、AI竞赛备赛或深度强化学习入门者快速复现并调试DQN核心机制。读者可直接运行脚本观察车辆从随机探索到稳定登顶的全过程,深入理解状态编码、Q值估计、目标网络同步与损失函数设计等关键环节,为拓展至更复杂游戏或机器人控制任务奠定扎实基础。 最近有个朋友问我,想入门强化学习,是不是得先拿Breakout或者Pong这类雅达利游戏练手。我的建议是反过来:先把MountainCar跑通,你对DQN的理解会比直接上雅达利游戏扎实得多。这个项目就是用Python从零实现DQN算法,让小车学会自己爬上山坡。它不需要GPU,CPU跑几分钟就能看到效果,非常适合有Python基础、正在入门强化学习的人作为第一个完整项目。

MountainCar这个环境很有意思:状态只有位置和速度两个连续值,动作只有三个离散选项,看起来比雅达利游戏简单太多。但它的奖励极其稀疏——每一步只给-1,小车到达山顶才算结束。这种设计让随机探索基本等于白给,恰好把DQN最核心的痛点暴露出来:如何在几乎没有正向反馈的情况下,让智能体学到“先倒车、再冲刺”这种需要长期规划的策略。理解了MountainCar,你再看DQN在雅达利游戏上的表现,背后的机制是一样的。

1. 项目背景与整体思路拆解

1.1 为什么选MountainCar作为DQN的练手环境

先把这个环境彻底说清楚。在OpenAI Gym(现在叫Gymnasium)里,MountainCar的任务描述非常朴素:一辆动力不足的小车被困在山谷里,靠自身引擎爬不上右边的山坡,必须通过来回摆动积蓄动能,像人荡秋千一样借着惯性冲上山顶。

环境的状态空间是二维的,一个是小车的位置,范围在[-1.2, 0.6],另一个是速度,范围在[-0.07, 0.07]。动作空间是离散的,三个动作:向左推、不施加力、向右推。每一步的奖励都是-1,直到小车到达目标位置(位置不小于0.5)或者超过回合步数上限,本回合结束。

严格来说,MountainCar并不是Atari 2600主机上的那批游戏,但它常和雅达利环境一起出现在强化学习基准套件里。DQN当年在雅达利游戏上打出一系列惊艳成绩,核心机制就是价值函数近似加经验回放。而MountainCar把这种机制压缩到了一个极小的规模里,可以随时看到训练曲线变化,方便你做各种实验。这种可调试性是雅达利游戏给不了的,后者动辄需要几十万帧才能看到明显进展。

选择MountainCar还有一个实际好处:训练速度快。雅达利环境通常要跑几万到几十万个step,MountainCar在你调好超参数之后,大约几百个episode就能看到策略明显改善。我自己的机器没有独立显卡,纯CPU训练,大概几分钟就能从完全随机变成“知道先往左边倒车”的智能体。对新手来说,反馈快就意味着试错成本低,你可以在一个晚上尝试多组参数组合,这种上手体感非常重要。

1.2 DQN解决MountainCar的核心逻辑与设计取舍

DQN全称是Deep Q-Network,它解决的问题可以概括为:在有限状态、有限动作的环境中,学习一个从状态到动作价值的映射。这里的“价值”指的是未来累计折扣奖励的期望,即Q值。MountainCar的状态虽然是连续的,但动作空间很小,只有三个动作,正好符合DQN的使用场景。

打个比方,你可以把Q网络理解成一个“动作打分器”。给定当前位置和速度,网络分别输出向左推、不推、向右推三个动作的分数。智能体每次决策时,挑选分数最高的动作执行。训练的目标就是让这个打分器越来越准:在某个状态,哪个动作能带来更少的负奖励,它的分就更高。

为什么这个任务不选策略梯度方法?不是说不行,而是对于入门来说,DQN的组件更直观,更符合“在已知环境中做最优选择”的直觉。策略梯度直接建模策略分布,处理连续动作空间有天然优势,但对于MountainCar这种离散动作、低维状态的任务,DQN的表达方式更简单,也更容易定位问题出在哪里。

整体架构包含四个核心部分:主Q网络负责输出当前状态的Q值,目标网络提供稳定的Q值估计用于计算损失,经验回放缓冲区存储历史转移数据,epsilon贪婪策略负责平衡探索与利用。这四个组件缺一不可,后面我会逐个拆解它们的设计细节和踩坑经验。

2. 核心细节解析:DQN三大关键机制与参数选择

2.1 经验回放:为什么必须打破样本时间关联

在我第一次写DQN的时候,曾经天真地以为直接用最近采集的数据进行梯度更新就行了。结果训练曲线剧烈震荡,甚至发散了。后来才意识到,强化学习的训练数据不是独立同分布的——智能体在环境里采集到的相邻样本高度相关,如果按时间顺序直接学习,网络会对最近的经历过拟合,导致灾难性遗忘。

经验回放的解决方案用一个词概括就是“攒起来随机抽”。我实现的ReplayBuffer是一个固定容量的双端队列,容量设为100000。每次环境交互产生的(state, action, reward, next_state, done)五元组都存入这个缓冲区,训练时从中随机采样一个小批量(batch size设为64)进行梯度下降。随机采样的意义在于打破样本之间的时间相关性,让每次更新面对的是一批来自不同时间段的经验,相当于把数据分布“打散”了。

缓冲区容量这个参数也值得琢磨。容量太大会让旧经验占据太多比例,训练初期的低质量样本迟迟无法被淘汰,拖慢学习速度;容量太小则采样多样性不足,网络容易震荡。100000对于MountainCar这种低维环境足够了,因为一个episode最长也就几百步,100000步相当于几百个完整episode的积累。如果是雅达利游戏那种高维图像输入,缓冲区容量通常要设到1000000,因为每帧图像的信息量远大于两个浮点数。

还有一个容易被忽略的细节:经验回放缓冲区里存的是转移元组,而不是单纯的样本。这是因为Q-learning的更新依赖“当前状态动作的Q值”和“下一个状态的最大Q值”之间的时间差分关系。如果不把完整的转移过程存下来,就无法计算这个时间差分目标。这也是DQN和普通监督学习在数据处理上最本质的区别。

2.2 目标网络:给训练一个稳定的靶子

DQN另一个出道即巅峰的创新点是目标网络。为什么需要它?因为Q-learning的更新本质上是自举的,它用当前网络的估计值去更新当前网络自身。如果只有一个网络,每一步更新都在移动靶子上瞄准,误差会不断累积放大,最终导致训练发散。这种自举带来的不稳定在MountainCar这种稀疏奖励环境下尤其致命,因为奖励信号太稀疏,误差几乎没有被修正的机会。

目标网络的做法是维护一份主网络参数的延迟副本。计算Q值目标时,用的是目标网络输出的下一代Q值,而目标网络自身不参与梯度下降,只在每隔固定步数后将主网络的参数同步过来。我在这个项目里把同步间隔设为1000步(这里的步指的是环境交互step,不是梯度更新次数)。这样做的好处是,在1000步的窗口内,训练目标保持不变,主网络可以稳定地朝这个目标优化,等到目标网络更新之后再调整新的靶子。

目标网络还有软更新和硬更新两种方式。硬更新就是上面说的每隔固定步数直接复制参数,简单直接;软更新则是每一步都把目标网络参数往主网络方向移动一小部分,公式是theta_target = tau * theta_main + (1 - tau) * theta_target,tau通常取0.005。对于MountainCar这个规模的问题,硬更新已经完全够用。但如果你后续做更复杂的连续控制任务,软更新的平滑性会明显更好,值得提前了解。

我实际调参时发现,目标网络更新频率对MountainCar的训练稳定性影响很大。更新太频繁,比如每隔100步就同步一次,目标网络和主网络过于接近,稳定性提升不明显;更新太稀疏,比如每隔10000步才同步一次,目标网络长期不更新,Q值估计会偏离真实价值。1000步是一个经过多次实验验证比较稳妥的取值,你可以在此基础上根据训练曲线微调。

2.3 探索策略与奖励信号的细节处理

探索与利用的平衡是强化学习永恒的话题。在MountainCar里,这个问题被稀疏奖励放大了:如果智能体从一开始就只选当前Q值最高的动作,那么它会一直困在“盲目向右推”的局部策略里,永远学不到“先倒车”的长期规划。因此,我采用了标准的epsilon贪婪策略:以epsilon的概率随机选择动作,以1-epsilon的概率选择Q值最大的动作。

epsilon的初始值设为1.0,意味着最开始完全随机探索。随着训练进行,epsilon按0.995的比例在每个step衰减,下限设为0.01。这个衰减速度是我反复试过的:太快的话,智能体还没积累足够的成功经验就进入纯利用阶段,策略定型在次优解;太慢的话,后期大量随机动作会干扰已学到的优秀策略,训练曲线会出现可见的震荡。0.995配合1000个episode的训练总量,大约在训练到300个episode时epsilon降到0.2左右,此时智能体基本已经掌握初级策略,开始进入精细优化阶段。

奖励信号的处理也有一个重要选择:是否对稀疏奖励做塑形。最常见的做法是在到达山顶前每一步给-1,到达后给0。这个设计简单且符合MountainCar的最小任务描述。有些人会在智能体接近山顶时给一个按距离缩放的奖励,比如r = -1 + 10 * |position - goal|之类的,来引导智能体快速靠近目标区域。我建议入门阶段先不做奖励塑形,因为塑形函数设计不当会引入新的偏置,智能体可能学到“在原地不动”这种钻空子的行为。先把原始版本跑通,再考虑扩展。

3. 实操过程:从环境安装到训练跑通

3.1 环境准备与依赖安装

先说环境版本。我用的Python是3.10,深度学习框架是PyTorch 2.x。强化学习环境库需要特别注意:老版本的gym库已经停止更新,现在推荐使用gymnasium,它是gym的继任者,API基本兼容,维护更活跃。如果你在网上下载老教程代码,看到“import gym”和“env.unwrapped”这些写法,建议直接替换成“import gymnasium as gym”。

安装命令很简单,在命令行执行:

pip install gymnasium torch numpy matplotlib

如果你的机器有NVIDIA显卡并装好了CUDA,PyTorch会自动使用GPU加速。但对这个项目来说,CPU已经足够,不用在环境配置上花太多时间。

装完之后跑一个简单验证脚本,确保环境能正常创建并交互:

import gymnasium as gym env = gym.make('MountainCar-v0', max_episode_steps=1000) state, info = env.reset() print("状态空间:", env.observation_space) print("动作空间:", env.action_space) print("初始状态:", state) for _ in range(10): action = env.action_space.sample() next_state, reward, terminated, truncated, info = env.step(action) print(f"动作: {action}, 奖励: {reward}, 是否结束: {terminated or truncated}")

这里有个很关键的参数:max_episode_steps=1000。MountainCar-v0默认的回合上限是200步,这对DQN训练来说太短了。训练初期智能体完全是随机探索,200步内到达山顶的概率极低,几乎每个episode都是-200的固定奖励,梯度信号非常微弱。把上限放宽到1000步,相当于给了智能体更多时间去尝试“倒车-加速-冲坡”的完整流程,学习效率会高很多。

3.2 完整代码实现与逐段解析

下面给出完整可运行的DQN训练代码。我按照模块拆开讲,每个部分说明设计意图。

首先是网络定义和超参数设置:

import random from collections import deque import gymnasium as gym import matplotlib.pyplot as plt import numpy as np import torch import torch.nn as nn import torch.optim as optim # 超参数 LEARNING_RATE = 1e-3 GAMMA = 0.99 EPSILON_START = 1.0 EPSILON_END = 0.01 EPSILON_DECAY = 0.995 MEMORY_SIZE = 100000 BATCH_SIZE = 64 TARGET_UPDATE = 1000 EPISODES = 1000 MAX_STEPS = 1000 class QNetwork(nn.Module): def __init__(self, state_dim, action_dim, hidden_dim=128): super().__init__() self.net = nn.Sequential( nn.Linear(state_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, action_dim), ) def forward(self, x): return self.net(x)

网络结构是两层128维隐藏层,输入是2维状态,输出是3维Q值。这个规模对MountainCar来说绰绰有余。不建议一上来就用特别宽或特别深的网络,因为这会显著增加训练时间,而且在小规模问题上容易过拟合到已有经验上,泛化能力反而变差。128个神经元的隐藏层在处理位置和速度两个连续输入时,非线性表达能力已经足够。

接下来是经验回放缓冲区:

class ReplayBuffer: def __init__(self, capacity): 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): 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)

这里的random.sample是不放回采样,确保同一批次内不会出现重复样本。dones用np.float32存储,方便后续在目标计算中和gamma直接相乘。一个常见错误是把done存成bool类型,做张量运算时类型不匹配会报错。

然后定义Agent类,把主网络、目标网络、优化器、探索策略都封装在一起:

class DQNAgent: def __init__(self, state_dim, action_dim): self.action_dim = action_dim self.q_net = QNetwork(state_dim, action_dim) self.target_net = QNetwork(state_dim, action_dim) self.target_net.load_state_dict(self.q_net.state_dict()) self.optimizer = optim.Adam(self.q_net.parameters(), lr=LEARNING_RATE) self.buffer = ReplayBuffer(MEMORY_SIZE) self.epsilon = EPSILON_START self.step_count = 0 def choose_action(self, state): if np.random.rand() < self.epsilon: return np.random.randint(self.action_dim) state_tensor = torch.FloatTensor(state).unsqueeze(0) with torch.no_grad(): q_values = self.q_net(state_tensor) return q_values.argmax().item() def update(self): if len(self.buffer) < BATCH_SIZE: return states, actions, rewards, next_states, dones = self.buffer.sample(BATCH_SIZE) states_tensor = torch.FloatTensor(states) actions_tensor = torch.LongTensor(actions).unsqueeze(1) rewards_tensor = torch.FloatTensor(rewards).unsqueeze(1) next_states_tensor = torch.FloatTensor(next_states) dones_tensor = torch.FloatTensor(dones).unsqueeze(1) q_values = self.q_net(states_tensor).gather(1, actions_tensor) with torch.no_grad(): max_next_q = self.target_net(next_states_tensor).max(1, keepdim=True)[0] targets = rewards_tensor + GAMMA * max_next_q * (1 - dones_tensor) loss = nn.MSELoss()(q_values, targets) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.epsilon = max(EPSILON_END, self.epsilon * EPSILON_DECAY) self.step_count += 1 if self.step_count % TARGET_UPDATE == 0: self.target_net.load_state_dict(self.q_net.state_dict())

这里有几个值得掰开说的点。gather(1, actions_tensor)是从Q网络输出的三维张量里,按动作索引取出对应Q值的标准写法,比先转one-hot再逐元素相乘更高效。计算target时用torch.no_grad()包裹,因为目标网络不参与反向传播,不用计算梯度,能节省显存和时间。(1 - dones_tensor)是为了在终止状态时不把未来的Q值计入目标,因为终止状态之后没有下一步了。

关于epsilon衰减的时机,我选择在update函数里每个step衰减一次,而不是每个episode衰减一次。原因是episode的长度差异很大,训练初期一个episode可能跑满1000步,后期可能一两百步就结束,按episode衰减会导致epsilon下降速度与训练效率脱钩。按step衰减更稳定,也更符合“交互次数决定探索量”的逻辑。

最后是训练主循环:

env = gym.make("MountainCar-v0", max_episode_steps=MAX_STEPS) agent = DQNAgent(state_dim=2, action_dim=3) episode_rewards = [] episode_lengths = [] for episode in range(EPISODES): state, _ = env.reset() total_reward = 0 step_count = 0 while True: action = agent.choose_action(state) next_state, reward, terminated, truncated, _ = env.step(action) done = terminated or truncated agent.buffer.push(state, action, reward, next_state, done) agent.update() state = next_state total_reward += reward step_count += 1 if done: break episode_rewards.append(total_reward) episode_lengths.append(step_count) if (episode + 1) % 50 == 0: avg_reward = np.mean(episode_rewards[-50:]) print(f"Episode {episode + 1}, 平均奖励: {avg_reward:.1f}, " f"平均步数: {np.mean(episode_lengths[-50:]):.1f}, " f"epsilon: {agent.epsilon:.3f}") torch.save(agent.q_net.state_dict(), "dqn_mountaincar.pth") plt.plot(episode_rewards) plt.xlabel("Episode") plt.ylabel("Total Reward") plt.title("DQN on MountainCar") plt.show()

一个实现细节:Gymnasium的新API中,env.step返回5个值,其中terminated表示是否到达终止状态(例如到达山顶),truncated表示是否因为步数超限而提前结束。我把两者用or合并成done,因为无论哪种情况,这个episode都结束了,都需要把done标志存入经验回放。如果你沿用老代码里的done单标志写法,在新版gymnasium里会漏掉truncated信息。

3.3 训练效果观测与收敛判断

训练过程中最直观的观测指标是每回合总奖励曲线。我训练了1000个episode,把每回合的奖励值画出来,曲线形态大致是这样的:前100到200个episode,奖励值基本稳定在-1000左右,因为智能体还在瞎逛,几乎没有成功到达山顶的情况。200到400个episode之间,开始出现零星的高奖励回合,说明智能体偶然学会了“先左后右”的摆动策略。大约500个episode之后,曲线明显抬升,平均奖励稳定在-200到-300之间,这对应着智能体可以稳定用两三百步以内完成任务。

如果你看到曲线在某个episode突然从-1000跳到-150,不用惊讶,这不是bug,而是探索过程中恰好触发了一次成功,这次成功经验被存入缓冲区,通过经验回放被反复学习,带动了网络参数质的飞跃。这是DQN训练中非常典型的现象,尤其在稀疏奖励任务中。

判断模型是否真正学会,可以跑一个纯利用(epsilon设为0)的测试循环,看看智能体是否每次都能用70到100步完成任务。MountainCar的最优策略大概需要80多步到达山顶,如果测试中智能体稳定做到这个水平,说明它学会了最优的摆动策略。如果测试中智能体仍然需要300步以上,大概率是探索不充分,可以考虑降低epsilon衰减速率或者增加训练episode数。

4. 常见问题与排查技巧实录

4.1 训练不收敛,奖励曲线纹丝不动

最让人头大的情况就是训练了几百个episode,奖励曲线一直平在-1000,毫无起色。我遇到过这种问题,排查思路从头到尾过一遍。

首先检查经验回放缓冲区里有没有出现“成功样本”。可以在训练循环里加一行打印:每次episode结束若total_reward > -200,打印成功episode的序号。如果训练了500个episode仍然零成功,说明探索严重不足,优先考虑三个调整:把max_episode_steps调大,比如从200改成1000;把epsilon的衰减速率调慢,比如从0.995改成0.998;把目标网络的更新间隔调快,比如从1000改成500。这三个改动都是在给智能体更多探索和试错的机会。

其次是检查Q值的波动范围。在训练早期用debug模式打印一批状态对应的Q值,如果Q值始终在零附近微小波动,说明网络没有有效学习信号。这时候着重检查reward是否成功传到网络,可以在计算loss后打印loss的值,如果loss恒定不变或者极小,大概率是target计算有误,比如dones没乘到gamma上。

4.2 训练中后期突然发散

这种情况是最诡异的:明明已经学会了一些有效策略,奖励曲线也在稳步上升,突然某几个episode奖励暴跌,然后训练彻底崩坏。我定位到的原因是无脑使用Adam优化器加上经验回放中大量低质量样本的耦合效应。

解决办法有两个。第一,给梯度更新加一个裁剪,在loss.backward()之后、optimizer.step()之前加一句torch.nn.utils.clip_grad_norm_(agent.q_net.parameters(), max_norm=10)。第二个是检查是否需要降低学习率,从1e-3降到5e-4通常能明显提升稳定性。MountainCar的奖励尺度本身就小(都是-1),Q值的绝对值不大,梯度爆炸的风险不算高,但一旦出现发散倾向,裁剪是最快的止血方法。

另外,如果你发现发散总是出现在某次成功的episode之后,还有一个可能:这次成功经验被反复采样,导致Q值对特定状态产生严重过拟合,破坏了已有策略。这时候可以考虑提高TARGET_UPDATE频率,让目标网络更快跟上主网络的变化,减少长期不一致带来的漂移。不过这个调整要谨慎,太频繁会重新引入不稳定。

4.3 复现结果不稳定

同一份代码,同一台机器,跑两次结果差别很大,这在强化学习训练里是常态,不是bug。随机性来源有三个:环境初始状态的随机采样、epsilon贪婪策略的随机动作、经验回放时的随机采样。为了尽可能提升复现性,你可以在代码开头固定随机种子:

np.random.seed(0) random.seed(0) torch.manual_seed(0) env = gym.make("MountainCar-v0", max_episode_steps=MAX_STEPS) env.reset(seed=0) env.action_space.seed(0)

但需要说明的是,即使固定了所有种子,不同机器上的浮点运算差异和CPU线程调度仍然可能导致结果不完全一致。这恰恰是强化学习调参的常态。不要追求像素级复现,重要的是评估模型最终性能的统计特征,比如跑10次测试算平均奖励,而不是单独看某一次的曲线。

4.4 训练太慢怎么办

MountainCar本身训练很快,但如果你觉得还是慢,先确认两件事:第一,是否在每次env.step时解压出所有返回值,用不到的变量不要重复计算;第二,是否在choose_action里频繁做FloatTensor转换。把转换放在一次推理前的嵌套训练循环里,可以小幅提升速度。

想要更大的加速效果,可以尝试向量化环境,用gymnasium.vector模块同时运行多个环境实例,每个step填充更多的经验。这样经验回放缓冲区能更快积累多样化的数据,训练前期探索效率提升非常明显。不过对于MountainCar这个规模,我建议先跑通单环境版本,把概念理顺了再考虑性能优化。很多新手一上来就追求分布式、并行训练,结果调试环境的时间比训练本身还长,实在不划算。

另外一个我常用的技巧是:训练进行到一半时,把当前Q网络画出来看看。用matplotlib画一张Q值分布的热力图,横轴是位置,纵轴是速度,颜色是三个动作中的最大Q值。你会非常直观地看到网络学到了什么:初始阶段颜色是杂乱的,等到训练后期,右边山坡对应区域的颜色会明显偏深,表示Q值在那里变高。这种可视化的成就感比单纯的奖励曲线强得多,也方便快速判断网络是否学到了合理的策略结构。

5. 写在最后:从MountainCar到更大的世界

训练完这个项目之后,我最大的感受是:DQN的核心思想并不复杂,但每个组件背后都对应着强化学习的本质困境。经验回放对应的是样本独立同分布假设的破与立,目标网络对应的是自举更新的不稳定性,epsilon贪婪对应的是探索与利用的永恒矛盾。MountainCar像是一个微缩模型,把所有困境压缩到最小的可操作规模,让你亲手调一遍、踩一遍坑,比看十篇理论文章都有用。

后续扩展的方向其实很多。你可以把网络结构换成Dueling DQN,把Q值拆成状态价值和动作优势两部分,这在动作空间更大的任务上往往见效;也可以换成Double DQN,用主网络选动作、用目标网络估价值,减少Q值过估计;还可以把目标网络更新方式改成软更新,体会不同平滑策略的差异。这些改进在MountainCar上都只需要改动十几行代码,却能让你的理论理解加深一个层次。

如果你顺利跑通了这个小车爬山,下一步挑战雅达利游戏就有了底气。把状态从二维数组换成屏幕图像,把网络从全连接换成卷积网络,再加上帧堆叠和奖励裁剪,整个训练框架不需要大改。到时候再回头看MountainCar,你可能会觉得它简单,但正是这个简单的环境,让你避开了很多弯路。

本文还有配套的精品资源,点击获取

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

Matlab通用MAP图绘制指南:从电机效率云图到任意二维数据场

简介&#xff1a;本资源是一套开箱即用的Matlab电机MAP绘制程序&#xff0c;面向电机控制工程师、电驱动系统研发人员及高校相关方向研究生&#xff0c;解决实际项目中电机效率、转矩、电流等多维性能数据难以高效可视化的问题。压缩包共3个文件&#xff08;116KB&#xff09;&…

作者头像 李华
网站建设 2026/8/31 21:02:02

JavaFX实现流程图设计器:开发实战与答辩指南

简介&#xff1a;这是一份面向Java初学者与课程设计学生的期末大作业实践资源&#xff0c;基于JavaFX开发轻量级流程图设计器&#xff0c;解决算法可视化、程序逻辑梳理及教学演示等实际需求。资源包共21个文件&#xff0c;含13个Java源码&#xff08;涵盖主界面、图形节点、连…

作者头像 李华
网站建设 2026/8/31 21:00:02

华为Ascend C算子开发认证中级:tanh_custom完整代码实战

简介&#xff1a;本资源是面向昇腾AI开发者与华为Ascend C算子开发能力认证&#xff08;中级&#xff09;备考人员的实战代码包&#xff0c;聚焦NPU异构计算场景下自定义Sigmoid算子的端到端实现。资源完整覆盖kernel侧核函数开发、host侧Tiling结构体设计、Float16数据类型适配…

作者头像 李华
网站建设 2026/8/31 20:53:27

STM32实战:基于RS485与Modbus RTU的土壤检测仪制作

简介&#xff1a;面向物联网与智慧农业开发者&#xff0c;一套基于STM32F103C8T6的土壤监测完整工程方案&#xff0c;通过RS485接口连接四合一综合土壤传感器&#xff0c;可实时采集土壤pH、氮、磷、钾含量并在OLED屏显示&#xff0c;适用于精准农业、环境监测及嵌入式教学实践…

作者头像 李华
网站建设 2026/8/31 20:53:24

基于YOLOv8的无人机交通监控设计与实现

简介&#xff1a;本资源是一套面向高校毕业设计、课程设计与期末大作业的无人机交通监控实战项目&#xff0c;基于最新YOLOv8目标检测算法&#xff0c;解决复杂环境下车辆与行人实时识别、跟踪及速度估计等核心问题&#xff0c;适用于智能交通、计算机视觉与嵌入式AI方向的学习…

作者头像 李华