ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Q-learning算法实战:从零构建倒立摆智能体

Q-learning算法实战:从零构建倒立摆智能体 1. 从直觉到代码为什么倒立摆是强化学习的“Hello World”如果你对强化学习感兴趣想找一个项目来练手那么“倒立摆”几乎是一个绕不开的经典案例。它不像下围棋的AlphaGo那样遥不可及也不像简单的“走迷宫”那样过于基础。倒立摆问题恰恰卡在一个非常精妙的位置它的状态空间连续且维度适中动作空间离散物理模型清晰但控制逻辑却并不直观。这就像一个完美的沙盒让你能亲手验证强化学习那些听起来有点“玄乎”的理论——比如智能体如何通过“试错”学会一个人类工程师需要复杂数学推导才能设计的控制器。我第一次接触这个项目时心里也犯嘀咕一个连PID控制器都需要仔细调参才能稳定的系统真的能靠一个“傻乎乎”的、只知道根据表格Q表选动作的Q-learning算法学会吗答案是肯定的而且过程极具启发性。它完美地展示了强化学习的核心范式智能体Agent通过与环境Environment交互根据环境反馈的奖励Reward来调整自己的策略Policy最终学会完成特定任务。在倒立摆任务中任务就是让摆杆保持竖直向上。我们将使用最经典的Q-learning算法这是一种无模型的、基于值函数的时序差分学习方法。它不依赖于环境的动力学模型而是通过不断地交互来估计在某个状态State下采取某个动作Action的长期价值Q值并据此做出决策。这个实战项目的目标非常明确我们不依赖任何现成的控制理论仅用Q-learning算法从零开始训练一个智能体让它学会控制小车或滑轨左右移动以保持顶部的摆杆不倒。完成这个项目你将彻底理解状态离散化、奖励函数设计、探索与利用的权衡这些核心概念并拥有一个可以随时运行和修改的代码框架。下面我们就从最核心的环境搭建开始。2. 环境构建将物理世界转化为算法可理解的“游戏”强化学习智能体生活在它自己的“世界”里这个世界就是环境。我们的第一步是创建一个标准化的Gymnasium环境。Gymnasium是OpenAI Gym的维护分支提供了大量标准化的环境接口极大方便了算法的测试与比较。虽然Gymnasium内置了经典的CartPole-v1环境但为了彻底理解其内部机制我强烈建议我们从零开始构建一个简化版本。这不仅有助于调试更能让你深刻理解状态、动作和奖励是如何定义的。2.1 定义环境动力学倒立摆的物理心脏一个倒立摆系统通常由几个关键物理量描述小车位置x、小车速度v、摆杆角度theta从竖直向上位置开始计算、摆杆角速度theta_dot。这些连续变量共同构成了我们的状态空间。环境的动力学由微分方程控制这里我们采用经典的近似动力学模型。在每一时间步智能体对环境施加一个力动作这个力会改变小车的加速度进而通过摆杆的铰链影响摆杆的角加速度。其核心微分方程可以简化表述为摆杆角加速度 (重力 * sin(角度) cos(角度) * (-外力 - 摆杆质量*长度*角速度^2 * sin(角度)) / 总质量) / (长度 * (4/3 - (摆杆质量 * cos(角度)^2) / 总质量)) 小车加速度 (外力 摆杆质量 * 长度 * (角速度^2 * sin(角度) - 角加速度 * cos(角度))) / 总质量其中总质量 小车质量 摆杆质量。外力即智能体选择的动作如向左-10N向右10N。在实际编程中我们会使用欧拉积分法来更新状态新角速度 当前角速度 角加速度 * 时间步长(dt) 新角度 当前角度 新角速度 * dt 新小车速度 当前小车速度 小车加速度 * dt 新小车位置 当前小车位置 新小车速度 * dt这个dt就是仿真步长通常设为0.02秒即50Hz与CartPole-v1环境保持一致。这里有一个关键细节在计算角度时我们通常将其归一化到[-π, π]区间这能避免角度值无限增长带来的数值问题使用(angle np.pi) % (2 * np.pi) - np.pi即可实现。注意对于首次实现我建议直接使用gymnasium.make(‘CartPole-v1’)来获取环境。它的动力学经过充分验证且被广泛用作基准。我们自制环境的主要目的是教学和深度定制。在本文的后续部分我们将以标准CartPole-v1环境为例进行讲解其状态是一个4维向量[车位置 车速 杆角度 杆角速度]。2.2 设计奖励函数与终止条件告诉智能体什么是“好”奖励函数是强化学习任务的“指挥棒”。一个设计不当的奖励函数会导致智能体学到奇怪甚至完全错误的行为。对于倒立摆我们的目标是让杆子直立同时小车尽量不要偏离中心太远。一个直观且有效的奖励设计是只要杆子没有倒下每一步都给予1的奖励。这就是CartPole-v1的标准设置。杆子“倒下”的定义就是终止条件通常包括角度阈值摆杆角度绝对值超过某个值如12度或15度即约0.2弧度。位置阈值小车位置绝对值超过轨道长度的一半如2.4个单位。步数限制为了防止智能体在局部最优中无限循环通常设置一个最大步数如500步。达到最大步数视为成功完成一个回合episode并触发终止但这不是因为“失败”。奖励函数的设计是门艺术。除了简单的每一步1你也可以尝试加入一些形奖励来引导学习对角度绝对值进行负奖励reward 1.0 - abs(theta) / angle_threshold。这样杆子越垂直奖励越高。对小车的偏离进行轻微惩罚reward 1.0 - 0.1 * abs(x)鼓励小车待在中心附近。 但请注意引入形奖励可能会让问题变得更复杂初期建议使用最简单的每步1奖励它已被证明是有效的。2.3 实现标准Gymnasium接口让智能体能“玩”起来为了让我们的环境能与各种强化学习算法库兼容必须实现Gymnasium的核心接口。主要需要实现三个方法reset(seedNone): 初始化环境将状态重置为随机初始值通常在小角度范围内并返回初始状态。step(action): 接收智能体的动作0或1代表向左或向右施力根据动力学方程计算下一状态判断是否终止计算奖励并返回(next_state, reward, terminated, truncated, info)这五个值。terminated表示因失败杆倒/车出界而终止truncated表示因步数限制而终止。render(): 可选方法用于可视化当前状态可以用matplotlib简单绘制小车和摆杆。完成这些一个功能完整的环境就搭建好了。智能体将通过调用env.step(action)来与之交互获得反馈从而学习。3. Q-learning算法核心构建智能体的“经验表格”有了环境我们来看看智能体的大脑——Q-learning算法。Q-learning是一种表格型方法其核心是维护一张Q表。这张表的行代表所有可能的状态离散化后列代表所有可能的动作。Q值Q(s, a)代表了在状态s下采取动作a并且此后一直遵循最优策略所能获得的期望累积奖励。3.1 状态离散化将连续世界装入离散表格CartPole-v1的状态是4维连续空间而Q表需要离散的索引。因此我们必须进行状态离散化State Discretization。这是本项目第一个关键技巧直接影响到学习效率和最终性能。一个简单粗暴的方法是将每个状态维度均匀地划分为若干个区间bin。例如车位置x: 范围[-2.4, 2.4]划分为 10 个区间。车速v: 范围[-3.0, 3.0]根据经验估计划分为 10 个区间。杆角度theta: 范围[-0.2, 0.2]弧度约±12度划分为 20 个区间因为角度对稳定性更敏感。杆角速度theta_dot: 范围[-2.0, 2.0]划分为 20 个区间。这样总的状态空间大小就是10 * 10 * 20 * 20 40,000。对于每个维度的具体范围你需要通过观察智能体交互时状态值的大致分布来调整。离散化粒度是一个重要的超参数区间太少状态区分度低智能体学不到精细控制区间太多Q表过于稀疏需要更多样本来填充学习变慢且容易过拟合。离散化的代码实现就是为每个连续值找到它所属的区间索引。我们可以用np.digitize函数或者自己写一个简单的映射函数def discretize_state(state, bins): 将连续状态state映射为离散索引元组 discretized [] for i in range(len(state)): # 将state[i]映射到bins[i]的某个索引 # 例如如果bins[i] np.linspace(-2.4, 2.4, 10)则找到state[i]落在哪个区间 discretized.append(np.digitize(state[i], bins[i]) - 1) # digitize返回1-based索引 # 确保索引在有效范围内 discretized[-1] max(0, min(discretized[-1], len(bins[i])-2)) return tuple(discretized)3.2 Q表更新与动作选择学习与决策的平衡有了离散状态智能体在每一步的决策和学习流程如下观察状态从环境获得当前连续状态s将其离散化为s_discrete。选择动作根据当前Q表和探索策略选择一个动作a。最常用的策略是ε-贪婪策略以概率ε随机选择一个动作探索以概率1-ε选择当前状态下Q值最大的动作利用。执行动作将动作a传递给环境得到下一个状态s‘、奖励r和终止标志done。更新Q表这是Q-learning的核心学习步骤。更新公式为Q(s, a) ← Q(s, a) α * [ r γ * max_a’ Q(s’, a’) - Q(s, a) ]α是学习率控制新信息覆盖旧信息的程度。γ是折扣因子表示对未来奖励的重视程度0更重视即时奖励1更重视长期回报。r γ * max_a’ Q(s’, a’)被称为目标值它是当前步骤实际获得的即时奖励加上对下一状态最佳估计的折现。r γ * max_a’ Q(s’, a’) - Q(s, a)是时序差分误差代表了当前估计与目标之间的差距。状态转移将s’设为新的当前状态重复步骤1。如果当前步骤是终止状态doneTrue那么目标值中就没有未来部分即max_a’ Q(s’, a’)为0更新公式简化为Q(s, a) ← Q(s, a) α * [ r - Q(s, a) ]。关于探索率ε通常我们使用衰减的ε例如ε max(ε_min, ε * ε_decay)。训练初期需要大量探索因此ε较高随着学习进行逐渐降低ε让智能体更多地利用已学到的知识。这是平衡探索与利用的常用手法。4. 训练流程与超参数调优让智能体从零学起现在我们将环境、离散化、Q-learning算法组合起来形成完整的训练循环。这个循环将运行多个回合直到智能体的性能达到满意水平。4.1 完整的训练循环架构一个典型的训练循环代码如下所示。请注意其中包含了关键的超参数和性能监控逻辑。import gymnasium as gym import numpy as np # 超参数 EPISODES 2000 # 训练总回合数 MAX_STEPS 500 # 每回合最大步数 ALPHA 0.1 # 学习率 GAMMA 0.99 # 折扣因子 EPSILON 1.0 # 初始探索率 EPSILON_MIN 0.01 # 最小探索率 EPSILON_DECAY 0.995 # 探索率衰减因子 # 创建环境 env gym.make(‘CartPole-v1’) # 定义离散化区间需要根据环境观察空间的范围调整 state_bins [ np.linspace(-2.4, 2.4, 10), # 车位置 np.linspace(-3.0, 3.0, 10), # 车速 np.linspace(-0.2, 0.2, 20), # 杆角度 np.linspace(-2.0, 2.0, 20) # 杆角速度 ] state_dims [len(bins)-1 for bins in state_bins] # 每个维度的离散状态数 action_dims env.action_space.n # 动作空间大小2 # 初始化Q表形状为 (state_dim1, state_dim2, state_dim3, state_dim4, action_dim) Q_table np.zeros(state_dims [action_dims]) # 训练记录 rewards_history [] epsilon_history [] for episode in range(EPISODES): state, _ env.reset() state_disc discretize_state(state, state_bins) total_reward 0 done False for step in range(MAX_STEPS): # 1. ε-贪婪策略选择动作 if np.random.random() EPSILON: action env.action_space.sample() # 随机探索 else: action np.argmax(Q_table[state_disc]) # 贪婪利用 # 2. 执行动作 next_state, reward, terminated, truncated, _ env.step(action) done terminated or truncated total_reward reward # 3. 离散化下一状态 next_state_disc discretize_state(next_state, state_bins) # 4. 更新Q表 current_q Q_table[state_disc (action,)] if done and terminated: # 因失败终止无未来奖励 target_q reward else: max_next_q np.max(Q_table[next_state_disc]) target_q reward GAMMA * max_next_q Q_table[state_disc (action,)] current_q ALPHA * (target_q - current_q) # 5. 状态转移 state_disc next_state_disc if done: break # 回合结束衰减探索率 EPSILON max(EPSILON_MIN, EPSILON * EPSILON_DECAY) rewards_history.append(total_reward) epsilon_history.append(EPSILON) # 每100回合输出一次平均奖励 if episode % 100 0: avg_reward np.mean(rewards_history[-100:]) print(f”Episode {episode}, Avg Reward (last 100): {avg_reward:.2f}, Epsilon: {EPSILON:.3f}”) env.close()4.2 超参数的影响与调优经验训练强化学习智能体很大程度上是在调优超参数。以下是几个关键参数的经验之谈学习率α控制学习速度。太大可能导致Q值震荡甚至发散太小则学习缓慢。典型范围在[0.01, 0.5]之间。对于稳定的CartPole0.1通常是个不错的起点。折扣因子γ决定了智能体对未来奖励的重视程度。越接近1智能体越有远见。对于倒立摆这种需要持续平衡的任务γ应该设得较高比如0.99。如果任务更看重即时奖励可以设低一些。探索率ε及其衰减初始ε通常设为1.0完全随机探索。衰减因子ε_decay控制探索减少的速度。0.995意味着每回合探索率乘以0.995。你需要观察训练曲线如果奖励很早就停滞不前可能是探索衰减太快智能体陷入了局部最优如果奖励一直波动很大可能是探索率始终太高智能体无法稳定利用学到的策略。离散化区间数这是最影响性能的参数之一。我建议从一个中等粒度开始如每个维度10个区间观察学习效果。如果学习很快但性能上限低比如始终达不到500步可能是状态区分度不够可以尝试增加角度和角速度的区间数。如果学习极其缓慢可能是状态空间太大可以适当减少区间或增大探索率。一个重要的训练技巧是观察“移动平均奖励”。单回合奖励波动很大看最近100回合的平均奖励更能反映智能体的真实学习进度。当这个平均奖励接近最大步数如500并保持稳定时说明智能体已经学会了。5. 结果可视化与策略分析解读智能体学到了什么训练完成后我们不仅要看它能不能平衡还要理解它到底学到了什么策略。可视化是最直观的工具。5.1 训练过程可视化学习曲线的秘密绘制奖励随训练回合的变化曲线以及探索率ε的衰减曲线放在同一张图上可以清晰地看到学习过程。import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) # 子图1奖励历史平滑处理 plt.subplot(1, 2, 1) # 计算每100回合的移动平均奖励使曲线更平滑 window_size 100 moving_avg np.convolve(rewards_history, np.ones(window_size)/window_size, mode‘valid’) plt.plot(range(window_size-1, len(rewards_history)), moving_avg, label‘Moving Avg Reward (100 episodes)’, color‘blue’) plt.xlabel(‘Episode’) plt.ylabel(‘Total Reward’) plt.title(‘Training Progress’) plt.legend() plt.grid(True) # 子图2探索率衰减 plt.subplot(1, 2, 2) plt.plot(epsilon_history, label‘Epsilon’, color‘red’) plt.xlabel(‘Episode’) plt.ylabel(‘Epsilon’) plt.title(‘Exploration Rate Decay’) plt.legend() plt.grid(True) plt.tight_layout() plt.show()从学习曲线中你通常能看到几个阶段初期奖励很低随机探索随后奖励快速上升智能体发现了有价值的动作最后奖励在高位波动并逐渐稳定策略趋于成熟。探索率曲线应平滑下降与奖励上升期相对应。5.2 策略可视化与Q表洞察看看智能体的“脑回路”我们可以运行一个训练好的智能体并录制其控制过程。更深入的是尝试可视化Q表的一部分来理解智能体的决策逻辑。由于Q表是高维的我们可以固定其中两个维度观察在另外两个维度构成的平面上不同动作的Q值分布。例如我们固定小车位置和速度在中间值观察摆杆角度和角速度构成的相位平面上的策略# 假设我们固定车位置索引为5接近中心车速索引为5接近零 fixed_x_idx, fixed_v_idx 5, 5 # 创建一个网格来绘制策略 theta_idx_range range(state_dims[2]) # 角度维度索引 theta_dot_idx_range range(state_dims[3]) # 角速度维度索引 policy_map np.zeros((len(theta_idx_range), len(theta_dot_idx_range))) for i, theta_idx in enumerate(theta_idx_range): for j, theta_dot_idx in enumerate(theta_dot_idx_range): state_idx (fixed_x_idx, fixed_v_idx, theta_idx, theta_dot_idx) # 选择Q值最大的动作作为策略 policy_map[i, j] np.argmax(Q_table[state_idx]) # 将离散索引映射回近似的物理值用于坐标轴 theta_vals state_bins[2][:-1] # 忽略最后一个边界值 theta_dot_vals state_bins[3][:-1] plt.figure(figsize(8, 6)) # 使用imshow或pcolormesh绘制策略图 plt.imshow(policy_map.T, origin‘lower’, aspect‘auto’, extent[theta_vals[0], theta_vals[-1], theta_dot_vals[0], theta_dot_vals[-1]], cmap‘coolwarm’) plt.colorbar(label‘Action (0Left, 1Right)’) plt.xlabel(‘Pole Angle (rad)’) plt.ylabel(‘Pole Angular Velocity (rad/s)’) plt.title(‘Learned Policy Slice (at center position zero cart velocity)’) plt.show()这张图会显示在角度-角速度平面上智能体在什么区域会选择向左推什么区域会选择向右推。你可能会看到一个清晰的分界线这类似于一个简化的控制律。例如当杆向右倒角度为正时智能体很可能选择向右移动小车以“接住”杆子这与直观物理是一致的。5.3 性能测试与鲁棒性验证最后关闭探索设置ε0让智能体纯粹利用学到的策略运行多个回合统计平均步数。一个训练良好的智能体应该能稳定达到最大步数500步。你还可以稍微修改环境参数如重力、杆长测试策略的鲁棒性。表格型Q-learning学到的策略通常对微小扰动有一定鲁棒性但较大变化可能需要重新训练或调整离散化区间。6. 从Q-learning出发局限性与进阶方向通过这个项目你已经成功用最经典的Q-learning算法解决了倒立摆问题。但我们必须清醒地认识到表格型Q-learning的局限性这也正是深度强化学习等更高级方法发展的动力。表格型Q-learning的核心局限维度灾难我们的状态空间被离散为4万个单元。如果状态维度再增加比如一个7自由度的机械臂或者离散化更精细Q表的大小会呈指数级爆炸根本无法存储和训练。无法处理连续动作Q-learning天然适用于离散动作空间。对于需要连续力输出的控制问题如油门、方向盘角度表格法无能为力。泛化能力差Q表只存储了见过的离散状态的值。对于未在训练中精确出现过的状态智能体无法做出很好的推断。由此引出的进阶方向函数逼近用参数化函数如线性函数、神经网络来近似Q值函数Q(s, a; θ)而不是用表格存储。这就是深度Q网络DQN的核心思想。DQN使用神经网络作为Q函数逼近器可以处理高维原始输入如图像并具备一定的泛化能力。策略梯度方法直接参数化策略π(a|s; θ)并通过梯度上升来优化策略参数以最大化期望回报。这类方法如REINFORCE, Actor-Critic, PPO天然适用于连续动作空间。结合模型上述都是无模型方法。基于模型的强化学习则尝试学习环境动力学模型然后利用这个模型进行规划或辅助学习可以大幅提升样本效率。倒立摆问题本身也有更复杂的变体如二阶倒立摆双摆其状态空间更复杂、更不稳定对算法是更大的挑战。从Q-learning到DQN再到策略梯度方法去解决连续控制版本的倒立摆如Pendulum-v1环境是一条清晰的学习路径。我个人在实践中的体会是这个Q-learning倒立摆项目最大的价值不在于实现了一个多强的控制器而在于它像一把钥匙帮你打通了强化学习从理论到实践的任督二脉。当你亲手调整超参数、观察学习曲线、可视化Q表并看到智能体从零开始学会平衡时那些抽象的概念——状态、动作、奖励、值函数、探索与利用——就变得无比具体和深刻。它留下的印象远比读十篇论文更牢固。在后续学习更复杂的深度强化学习算法时你会不断地回溯到这个最基础的模型上来理解新概念。所以尽管它简单但请务必重视这个起点把每个环节都吃透。
返回列表