强化学习入门:环境与智能体解耦,轻松替换算法
简介这份资源面向强化学习初学者与需要快速上手实践的开发者提供一套可直接运行的乒乓球对战模拟实例帮助理解智能体如何通过与环境交互不断优化击球策略。压缩包共10个文件以6个pth模型权重、3个py脚本和1个pyc缓存文件为主整体约35.78MB其中pth文件保存不同训练阶段的网络参数py脚本承担环境封装、DQN算法实现与验证逻辑替换权重即可对比智能体在各阶段的真实表现。已有4371人学习下载说明其作为入门范例具备一定参考价值。读者可借此观察状态、动作、奖励与策略在具体游戏场景中的对应关系理解Q-learning与DQN的更新思路并在此基础上尝试替换算法或调整参数探索强化学习的更多可能。1. 强化学习实例从“跑不起来”到“改一行就能换算法”的最小闭环很多人第一次接触强化学习卡住的地方不是数学而是环境。装完框架跑官方示例报错换个环境接口对不上想改算法发现代码和环境的耦合深到无从下手。标题里说的“直接运行、直观、方法可替换、初学者代码友好”本质上是在解决一个很具体的问题能不能有一个最小闭环让环境、智能体、训练循环三层解耦跑通之后换算法只动一个文件。这篇文章面向的是想动手跑通第一个强化学习实例、并且希望后续能低成本替换算法的人。我会用一个虚构的“网格寻路”环境作为载体把环境接口、智能体接口、训练循环拆开先跑通一个能收敛的基线再演示怎么把 Q-learning 换成 SARSA 或策略梯度而不动环境代码。全程只依赖常见科学计算库不绑定任何特定框架。2. 环境与智能体解耦为什么你的第一个实例总是改不动2.1 耦合写法带来的三个具体麻烦初学者最常见的写法是把环境逻辑、动作选择、更新公式全塞在一个循环里。这种写法跑通很快但会带来三个麻烦。第一换算法时要重写整个循环容易把已经调好的环境逻辑改坏。第二没法对比不同算法在同一环境下的表现因为环境被硬编码进了算法。第三调试时无法单独验证环境是否正确只能通过最终回报反推排查成本高。解耦的核心思路是定义两个接口环境只负责“给定动作返回下一个状态和奖励”智能体只负责“给定状态返回动作并接收转移样本进行学习”。训练循环只做调度不包含任何算法细节。这样换算法时只需要替换智能体实现环境和训练循环原封不动。2.2 环境接口的最小定义下面是一个网格寻路环境的最小实现。状态用整数编号表示格子位置动作空间是上下左右四个离散动作到达目标格给正奖励撞墙或越界给轻微负奖励每走一步给一个小的负奖励促使尽快到达。import numpy as np class GridWorld: 最小网格环境状态为格子编号动作为0上1下2左3右 def __init__(self, rows5, cols5, goal(4, 4), max_steps100): self.rows rows self.cols cols self.goal goal self.max_steps max_steps self.n_states rows * cols self.n_actions 4 self._reset() def _reset(self): self.state (0, 0) self.steps 0 return self._to_index(self.state) def reset(self): return self._reset() def _to_index(self, pos): return pos[0] * self.cols pos[1] def step(self, action): r, c self.state # 动作到位移的映射 if action 0: r - 1 elif action 1: r 1 elif action 2: c - 1 elif action 3: c 1 # 边界处理越界则留在原地并给负奖励 if r 0 or r self.rows or c 0 or c self.cols: r, c self.state reward -1.0 else: reward -0.01 # 每步小惩罚鼓励最短路径 self.state (r, c) self.steps 1 done (self.state self.goal) or (self.steps self.max_steps) if self.state self.goal: reward 1.0 return self._to_index(self.state), reward, done这段代码里n_states和n_actions是智能体需要的元信息reset和step是标准交互接口。奖励设计里每步 -0.01 是为了让智能体在同样到达目标的情况下偏好更短路径撞墙 -1.0 是为了让它学会避开边界。max_steps防止无限循环这是初学者容易忽略的保护。2.3 智能体接口与训练循环的分离智能体只需要两个方法act根据当前状态选动作update接收一条转移样本并更新内部参数。下面用 Q-learning 实现一个最简智能体Q 表用二维数组存储。class QLearningAgent: def __init__(self, n_states, n_actions, lr0.1, gamma0.99, epsilon0.1): self.Q np.zeros((n_states, n_actions)) self.lr lr self.gamma gamma self.epsilon epsilon self.n_actions n_actions def act(self, state): # epsilon-贪婪以一定概率随机探索 if np.random.rand() self.epsilon: return np.random.randint(self.n_actions) return int(np.argmax(self.Q[state])) def update(self, state, action, reward, next_state, done): # Q-learning 使用下一状态的最大Q值作为目标 target reward if not done: target self.gamma * np.max(self.Q[next_state]) self.Q[state, action] self.lr * (target - self.Q[state, action])训练循环只负责调度不包含任何算法细节。这样换算法时只需要替换智能体实现环境和训练循环原封不动。def train(env, agent, episodes500): returns [] for ep in range(episodes): state env.reset() total_reward 0.0 done False while not done: action agent.act(state) next_state, reward, done env.step(action) agent.update(state, action, reward, next_state, done) state next_state total_reward reward returns.append(total_reward) return returnsepisodes控制训练轮数returns记录每轮累计回报用于观察收敛。这个循环里没有任何 Q-learning 特有的逻辑所有算法差异都被封在agent里。3. 跑通第一个收敛曲线参数怎么设、回报怎么看3.1 训练脚本与收敛判断把环境和智能体接起来跑 500 轮观察回报曲线。初学者最容易犯的错是只看最后一轮的回报而不看整体趋势。强化学习的回报波动很大单轮结果没有意义要看滑动平均。if __name__ __main__: env GridWorld() agent QLearningAgent(env.n_states, env.n_actions, lr0.1, gamma0.99, epsilon0.1) returns train(env, agent, episodes500) # 计算每50轮的平均回报观察趋势 window 50 for i in range(0, len(returns), window): chunk returns[i:iwindow] print(fepisode {i}-{ilen(chunk)-1}, avg return: {np.mean(chunk):.3f})运行后前 50 轮平均回报通常在 -5 到 -2 之间因为智能体还在随机探索。到 200 轮左右平均回报会明显上升接近 0.5 到 0.9 之间。如果 500 轮后平均回报仍然低于 -1说明参数或环境有问题需要排查。3.2 三个必调参数学习率、折扣因子、探索率学习率lr控制每次更新的步长。设太大Q 值震荡不收敛设太小收敛慢。网格这类小状态空间0.1 到 0.3 都合理。折扣因子gamma决定智能体有多看重未来奖励。0.99 适合目标较远的情况0.9 适合短视场景。探索率epsilon控制随机探索比例固定 0.1 是最简做法但更好的方式是随训练衰减。# 在 train 循环里加入 epsilon 衰减 def train_with_decay(env, agent, episodes500, eps_start1.0, eps_end0.05, decay0.995): returns [] eps eps_start for ep in range(episodes): agent.epsilon eps state env.reset() total_reward 0.0 done False while not done: action agent.act(state) next_state, reward, done env.step(action) agent.update(state, action, reward, next_state, done) state next_state total_reward reward returns.append(total_reward) eps max(eps_end, eps * decay) # 每轮衰减但不低于下限 return returnseps_start1.0表示一开始完全随机探索eps_end0.05保留少量探索防止陷入局部最优decay0.995控制衰减速度。500 轮后 epsilon 约降到 0.08 左右。这个设置比固定 epsilon 收敛更稳是实际项目里更常用的做法。3.3 回报曲线的三种典型形态与对应问题第一种回报快速上升后稳定说明参数合适。第二种回报长期在低位震荡通常是学习率过大或奖励设计有问题。第三种回报上升后突然崩塌往往是 epsilon 衰减过快导致过早停止探索。遇到第二种先把学习率降到 0.05 试试遇到第三种把 decay 调到 0.999 或提高 eps_end。4. 换算法只动一个文件从 Q-learning 到 SARSA 与策略梯度4.1 替换为 SARSA改一行目标值计算SARSA 和 Q-learning 的唯一区别在于目标值的计算方式。Q-learning 用下一状态的最大 Q 值SARSA 用实际采取的下一动作的 Q 值。这意味着 SARSA 是 on-policy 的需要知道下一步实际选了什么动作。class SarsaAgent(QLearningAgent): def update(self, state, action, reward, next_state, done): # SARSA 需要知道下一步实际动作这里用当前策略选一个 next_action self.act(next_state) target reward if not done: target self.gamma * self.Q[next_state, next_action] self.Q[state, action] self.lr * (target - self.Q[state, action])注意这里next_action是在update内部用当前策略选的而不是外部传入的。严格来说 SARSA 应该在 step 之前就选好下一步动作但在这个最小实现里用当前策略重新选一次效果等价因为策略只依赖当前 Q 表。替换时只需要把QLearningAgent换成SarsaAgent训练循环完全不用改。4.2 替换为策略梯度接口不变内部换实现策略梯度不再维护 Q 表而是直接参数化策略。对于离散动作可以用一个简单的 softmax 策略参数是每个状态对应的动作偏好值。class PolicyGradientAgent: def __init__(self, n_states, n_actions, lr0.01, gamma0.99): self.theta np.zeros((n_states, n_actions)) # 动作偏好 self.lr lr self.gamma gamma self.n_actions n_actions self.trajectory [] # 存储一轮的转移 def _softmax(self, state): logits self.theta[state] exp_logits np.exp(logits - np.max(logits)) # 数值稳定 return exp_logits / np.sum(exp_logits) def act(self, state): probs self._softmax(state) return int(np.random.choice(self.n_actions, pprobs)) def update(self, state, action, reward, next_state, done): # 先存轨迹回合结束后统一更新 self.trajectory.append((state, action, reward)) def finish_episode(self): # 计算折扣回报 returns [] G 0.0 for _, _, r in reversed(self.trajectory): G r self.gamma * G returns.insert(0, G) returns np.array(returns) # 标准化降低方差 if len(returns) 1: returns (returns - returns.mean()) / (returns.std() 1e-8) # 策略梯度更新 for (state, action, _), G in zip(self.trajectory, returns): probs self._softmax(state) # 对 log 概率求导得到梯度 grad -probs grad[action] 1.0 self.theta[state] self.lr * G * grad self.trajectory []这个实现里update只存轨迹真正的参数更新在finish_episode里做。训练循环需要在每轮结束后调用一次finish_episode。这是策略梯度和值方法在接口上的一个差异但通过增加一个方法就能兼容不需要改环境。4.3 三种算法的对比与选型建议算法更新时机是否 on-policy适合场景收敛速度Q-learning每步否离散状态、离线可用快SARSA每步是需要考虑探索安全中等策略梯度每轮是连续动作、大规模状态慢但可扩展网格这类小规模离散问题Q-learning 通常收敛最快。如果环境有风险动作比如某些动作会导致严重负奖励SARSA 因为考虑了实际探索策略表现更保守安全。策略梯度在这个小环境里优势不明显但它的接口可以自然扩展到连续动作这是值方法做不到的。5. 避坑与排查跑不通时先看这五个地方5.1 回报一直是负的且不上升现象训练几百轮后平均回报仍在 -5 以下。原因通常是奖励设计有问题比如每步惩罚过大导致智能体宁愿原地不动也不愿探索。解决把每步惩罚从 -0.01 降到 -0.001或者给到达目标更大的正奖励。另一个常见原因是学习率过大导致 Q 值发散检查 Q 表里是否有异常大的值。5.2 智能体学会绕圈但不走最短路径现象能到达目标但步数明显多于最短路径。原因是每步惩罚太小绕路的代价不足以影响策略。解决适当增大每步惩罚或者引入步数上限并在超时时给额外负奖励。注意惩罚不能太大否则会抑制探索。5.3 换了算法后完全不收敛现象Q-learning 能收敛换成 SARSA 或策略梯度后回报一直震荡。原因通常是新算法的超参数没有重新调。SARSA 对学习率更敏感策略梯度需要更小的学习率和更多的训练轮数。解决换算法后先把学习率降一个数量级试试策略梯度通常需要 1000 轮以上。5.4 训练速度突然变慢现象前 100 轮很快后面每轮耗时明显增加。原因可能是 epsilon 衰减后智能体走的路变长或者代码里有隐式的数组拷贝。检查update里是否有不必要的深拷贝Q 表更新应该是原地操作。另一个可能是max_steps设得太大智能体在后期仍然走很多步才结束。5.5 结果不可复现现象同样的代码跑两次收敛曲线差别很大。原因是随机种子没有固定。解决在训练开始前设置np.random.seed(42)并且确保环境和智能体用的是同一个随机源。如果用了多个随机源需要分别设置。6. 进阶技巧用状态聚合把表格方法扩展到更大空间网格环境的状态数是 25Q 表很小。但如果把网格扩大到 100x100状态数变成 10000Q 表仍然可以接受。再大到 1000x1000Q 表就太大了。这时候可以用状态聚合把相邻格子合并成一个聚合状态用聚合后的状态索引 Q 表。class AggregatedGridWorld(GridWorld): 把网格按 block_size 聚合减少状态数 def __init__(self, rows100, cols100, block_size10, **kwargs): super().__init__(rows, cols, **kwargs) self.block_size block_size self.n_states ((rows block_size - 1) // block_size) * \ ((cols block_size - 1) // block_size) def _to_index(self, pos): # 先算原始索引再映射到聚合索引 raw pos[0] * self.cols pos[1] br pos[0] // self.block_size bc pos[1] // self.block_size n_bc (self.cols self.block_size - 1) // self.block_size return br * n_bc bc聚合的代价是丢失了格子内的精确位置信息智能体在同一个聚合状态里可能采取不同动作但得到相同 Q 值。block_size越大状态数越少但精度越低。实际调的时候先从 10 开始试如果收敛不好再降到 5。这个技巧在状态连续但可以用离散区间近似时特别有用是表格方法向函数逼近过渡的一个中间步骤。我自己的习惯是每换一个环境先把状态数和动作数打印出来确认 Q 表大小在内存和训练速度上都能接受再开始调参。如果状态数超过一百万就别用表格了直接上函数逼近。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

C# async/await原理与高并发异步编程实战指南

C# async/await原理与高并发异步编程实战指南

1. 这不是语法糖,是C#并发能力的真正开关“C#异步task任务 await与async的正确打开方式”——这个标题里藏着一个被严重低估的事实:async/await不是让代码“看起来不卡”的美化技巧,而是C#运行时调度模型的一次底层重构。我带过不少刚从Java或…

2026/10/16 20:07:37 阅读更多 →
Spring Boot集成SAP RFC全攻略:JCo连接、Shiro权限与Quartz调度

Spring Boot集成SAP RFC全攻略:JCo连接、Shiro权限与Quartz调度

简介:这是一份基于Spring Boot构建的SAP RFC交互后台管理系统源码,适合需要对接SAP系统的Java开发者、后端工程师及项目架构师。项目完整集成MyBatis、MyBatis-Plus、Shiro、Quartz、thymeleaf以及SAPJCO3,通过调用SAP RFC函数实现SAP与外部系…

2026/10/18 12:55:32 阅读更多 →
哈夫曼编码文件压缩实战:从建树到解码的完整实现

哈夫曼编码文件压缩实战:从建树到解码的完整实现

简介:这份资源面向学习数据结构与算法的高校学生及C语言开发者,聚焦哈夫曼编码在文件压缩中的完整实现,可用于课程作业参考、算法理解与项目实践。压缩包共93个文件,约2.87MB,包含c与cpp源码、h头文件、exe可执行程序、…

2026/10/18 13:06:54 阅读更多 →

最新新闻

AI Agent 上线前最容易忽略的安全问题:提示词注入、工具越权与敏感数据泄露实战解析

AI Agent 上线前最容易忽略的安全问题:提示词注入、工具越权与敏感数据泄露实战解析

一、引言:Agent 把"模型风险"变成了"系统风险" 过去两年,绝大多数团队对 LLM 的安全认知还停留在"内容安全"层面——输出有没有违规、有没有幻觉、会不会骂人。这类问题最坏的结果是用户体验受损。 而 AI Agent 改变了风险…

2026/10/18 18:18:56 阅读更多 →
MMU内存管理单元深度解析:从虚拟地址到物理地址的完整过程

MMU内存管理单元深度解析:从虚拟地址到物理地址的完整过程

很多人第一次接触“MMU”这个词,是在学操作系统或者计算机组成原理的时候。老师在黑板上一笔带过,说它是“内存管理单元”,负责把虚拟地址翻译成物理地址。听起来很简洁,但你真去写代码、调bug、做性能优化的时候,会发…

2026/10/18 18:18:56 阅读更多 →
基于六维超混沌与DNA编码的图像加密算法及Matlab实现

基于六维超混沌与DNA编码的图像加密算法及Matlab实现

加密一直是信息安全里最让人上瘾的方向之一,尤其是图像加密——一张图片动辄几百KB甚至几MB,像素之间天然存在强相关性,传统文本加密算法直接套上去,效果往往一言难尽。我之前做过不少基于混沌系统的图像加密方案,从一…

2026/10/18 18:18:56 阅读更多 →
龙贝格观测器PMSM无传感器控制:建模、仿真与调参实战

龙贝格观测器PMSM无传感器控制:建模、仿真与调参实战

做电机控制的朋友应该都有体会:无传感器控制这五个字,听起来高级,做起来折磨。尤其是一上来就面对龙贝格观测器这种带人名的经典方案,教材里翻来覆去就是那几张状态方程和极点配置图,真到自己搭仿真、调参数的时候&…

2026/10/18 18:18:56 阅读更多 →
Octop:Python协作基建工具,实现可复现可审计的开发流程

Octop:Python协作基建工具,实现可复现可审计的开发流程

1. 项目概述:Octop 是什么?它解决的不是“Python安装问题”,而是开发者协作基建的隐形痛点你搜“Octop”时,首页跳出的全是“Python安装教程”“MIT thesis官网”“PyPI包 deprecated”这类关键词——这恰恰暴露了当前 Python 开发…

2026/10/18 18:18:56 阅读更多 →
前端JavaScript安全漏洞利用实战:从XSS到原型链污染

前端JavaScript安全漏洞利用实战:从XSS到原型链污染

我最近在整理一份安全学习系列v2023的学习笔记,第六十五讲正好落在JS应用安全漏洞利用这个主题上。说实话,这一讲的内容跟我平时做前端代码审查时的场景非常吻合:不需要等下一条注入请求,而是从浏览器里那堆看似平常的JavaScript文…

2026/10/18 18:17:55 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →