1. 项目概述:从“纸上谈兵”到“真刀真枪”的强化学习跃迁
你有没有试过学开车?教练在副驾上给你讲了一堆离合、油门、转向的原理,你听得头头是道,可一坐上驾驶座,手忙脚乱,方向盘打反,油门当刹车——理论和实操之间,隔着一个真实的驾驶舱。强化学习(Reinforcement Learning, RL)也一样。前两讲我们聊了马尔可夫决策过程(MDP)的骨架、聊了策略评估与改进的数学推导,那些公式像交通规则手册,清晰、严谨,但翻完手册你依然不会开车。真正让你学会开车的,不是背诵《道交法》,而是挂上一档、松开离合、感受车身的颤抖、观察后视镜里车流的移动、在一次次微调中建立肌肉记忆。本讲的核心,就是带你把RL从“纸面模型”推进到“真实交互”——Learning from Experience(从经验中学习)。它彻底甩开了“必须知道环境全貌”的幻想,直面一个最朴素的事实:现实世界没有水晶球,我们无法预知每一步行动的全部后果;我们能做的,只有迈出一步,看看发生了什么,再据此调整下一步。这正是TD-Learning(时序差分学习)的立身之本,也是ϵ-greedy策略存在的根本理由。它不追求一步到位的完美规划,而信奉“小步快跑、快速迭代”的工程哲学。如果你正在用Python写一个简单的游戏AI,或者想让一个机器人学会在迷宫里找出口,又或者只是想搞懂为什么AlphaGo能下赢人类,那么你此刻面对的,就是那个从“知道规则”迈向“真正会玩”的临界点。本讲内容完全基于真实教学场景中的代码、练习和踩坑记录,所有概念都配有可运行的Python片段和直观的数值演算,不堆砌公式,不空谈哲学,只讲一个资深从业者在带新人时,最常被问到、也最需要立刻解决的那几个问题:值函数怎么一步步更新出来?为什么不能只挑最好的路走?终端状态的值为什么非得是0?以及,最关键的一点——当你把代码敲进编辑器,按下回车,看到的是一串报错还是一次成功的收敛?接下来的内容,就是帮你把那串报错,变成一次漂亮的收敛。
2. 核心思路拆解:为什么“边做边学”是RL的唯一正解?
2.1 从“完美规划”到“经验驱动”的范式转移
强化学习的终极目标,是让一个智能体(Agent)在未知环境中,通过与环境(Environment)的持续互动,学会一套最优的行为策略(Policy),从而最大化长期累积奖励(Return)。这个目标听起来很宏大,但实现它的路径却异常务实。在教程第二讲中,我们详细推导了贝尔曼方程(Bellman Equation),它揭示了状态价值函数(State Value Function)v(s) 的本质:v(s) = E[R_{t+1} + γR_{t+2} + γ²R_{t+3} + ... | S_t = s]。这个公式告诉我们,一个状态的价值,等于从该状态出发,未来所有奖励的折现和的期望值。它像一张完美的航海图,标出了每一处暗礁、每一股洋流,只要你知道这张图,就能规划出一条通往宝藏的最优航线。
然而,这张图在现实中几乎不存在。让我们以自动驾驶汽车为例。要精确计算“在当前十字路口左转”这个动作的价值,你需要知道:左转后,你的车会精确停在哪个位置;紧接着,前方那辆SUV是否会突然变道;右边那辆自行车会不会加速抢行;甚至,路边那只狗会不会突然窜出马路。这些信息,共同构成了环境的“状态转移函数”(State Transition Function)和“奖励函数”(Reward Function)。在理想化的棋类游戏中,这个函数是确定性的、可穷举的——你落子后,棋盘状态必然按规则变化,胜负结果也必然随之确定。但在开放世界里,它是一个庞大、混沌、充满随机性的黑箱。你无法写出一个数学公式,来精确描述“一辆特斯拉在雨天高速公路上,对一辆突然切入的卡车做出反应”的全部物理过程。因此,任何依赖于“先验知识”的完美规划算法,在这里都会碰壁。这就是为什么,RL的整个技术栈,其设计哲学的底层逻辑,就是放弃对“全知全能”的幻想,拥抱“有限认知”的现实。它不试图去破解那个黑箱,而是选择绕过它,直接与黑箱对话:我做一件事,你给我一个反馈(reward),并把我带到下一个地方(next state),我就根据这个反馈,来修正我对“这件事值不值得做”的判断。这种“试错-反馈-修正”的闭环,就是Learning from Experience的全部内涵。它不是一种退而求其次的妥协,而是一种面向复杂世界的、更鲁棒、更具扩展性的工程智慧。
2.2 TD-Learning:在线学习的“即时反馈”机制
既然我们放弃了“先建模、再规划”的老路,那么新的学习方式就必须满足几个硬性要求:第一,它必须能从零开始,不需要任何关于环境的先验知识;第二,它必须是增量式的,因为现实世界的数据是源源不断地涌来的,你不可能等收集完所有数据再开始学习;第三,它必须足够高效,能在有限的交互次数内,给出一个足够好的近似解。TD-Learning(Temporal-Difference Learning)正是为满足这三个要求而生的。它的核心思想,可以用一句话概括:用“刚刚发生的事情”,来修正“刚刚做出的预测”。这听起来非常朴素,甚至有点“事后诸葛亮”,但它恰恰抓住了学习的本质——预测误差(Prediction Error)才是驱动学习的真正燃料。
我们来看一个具体的例子。假设你正在训练一个飞行导航AI,它的任务是从纽约飞往香港。在某次训练中,AI处于“伦敦”状态,它根据当前的值函数估计,认为飞往“开罗”的价值最高,于是选择了这个动作。随后,环境反馈:它收到了-8的奖励(飞行耗时8小时),并进入了“开罗”这个新状态。此时,TD-Learning的魔法就发生了。它会立刻计算一个叫做“TD误差”(TD Error)的东西:δ = r + γ * v̂(s') - v̂(s)。代入数字:δ = (-8) + 0.99 * v̂(开罗) - v̂(伦敦)。这个δ,就是AI的预测与现实之间的差距。如果δ是正的,说明AI低估了“伦敦”这个状态的价值(因为实际得到的回报比它预想的要好);如果δ是负的,则说明它高估了。TD-Learning的更新规则,就是用这个误差,按一个很小的比例(由学习率α控制),去微调v̂(伦敦)的值。这个过程,就是“在线学习”(Online Learning)的精髓。它不像蒙特卡洛(Monte Carlo)方法那样,需要等到整个飞行旅程(episode)结束,拿到最终的总回报,才能回头去更新所有中间状态的值;它也不像动态规划(Dynamic Programming)那样,需要一个完整的环境模型。它就像一个经验丰富的飞行员,每一次转弯、每一次爬升,他都在根据仪表盘上实时跳动的数据,微调自己的操作杆力度。这种“边飞边学”的能力,使得TD-Learning成为工业界应用最广泛的RL算法之一,从推荐系统到机器人控制,无处不在。它的成功,不是因为它有多“聪明”,而是因为它足够“务实”,足够“接地气”。
2.3 ϵ-greedy:探索与利用的“黄金分割点”
如果说TD-Learning解决了“如何从经验中学习”的问题,那么ϵ-greedy策略则解决了“如何获取高质量经验”的问题。这是一个看似简单、实则深刻的认知陷阱:一个只做“看起来最好”的事情的AI,永远无法发现“真正最好”的事情。这就像一个美食家,如果他永远只去自己最喜欢的那家餐厅,那么他将永远错过街角那家新开的、可能更惊艳的米其林新秀。在RL中,这个现象被称为“探索-利用困境”(Exploration-Exploitation Trade-off)。
- 利用(Exploitation),就是执行当前已知的、能带来最高预期回报的动作。这是“稳赚不赔”的保守策略,它能保证你获得当前认知下的最大收益。
- 探索(Exploration),则是主动尝试那些你了解甚少、甚至完全未知的动作。这是一场有风险的投资,它可能带来灾难性的低回报(比如选了一条死胡同),但也可能带来颠覆性的高回报(比如发现了一条捷径)。
一个只利用的AI,会很快陷入局部最优,它可能永远学不会如何从“纽约”飞往“香港”,因为它在第一次尝试时,偶然发现了一条经由“阿姆斯特丹”的路径,并且这条路径的初始估计值还不错,于是它就再也不去尝试“伦敦”这条路径了,哪怕后者才是真正的最优解。一个只探索的AI,则会像一个永远长不大的孩子,在无数条岔路上反复横跳,永远无法沉淀下任何有效的知识。因此,我们必须在两者之间找到一个平衡点。ϵ-greedy策略提供了一个极其简洁、极其有效的解决方案:它设定一个概率阈值ϵ(通常取0.1或0.05),在每次决策时,以ϵ的概率,完全随机地选择一个动作(强制探索);以1-ϵ的概率,选择当前估计值最高的那个动作(理性利用)。这个策略的精妙之处在于,它把一个复杂的、需要动态权衡的哲学问题,转化成了一个简单的、可编程的随机数比较。它不关心“现在该不该探索”,它只关心“现在有没有轮到我探索”。这种机械的、近乎冷酷的随机性,反而成为了打破认知僵局、跳出思维定势的最可靠工具。在后续的“Barry的停车场难题”练习中,你将亲手看到,如果没有ϵ-greedy,你的AI会像一只无头苍蝇,在迷宫的角落里无限循环;而一旦加入了它,那个小小的随机扰动,就会像投入湖面的一颗石子,激起一圈圈涟漪,最终引导AI找到通往终点的光明大道。
3. 核心细节解析:值函数、动作值与终端状态的“三重奏”
3.1 值函数(v̂(s))与动作值函数(q̂(s,a)):两个视角,一个世界
在RL的术语体系中,“值”(Value)这个词承载着至关重要的意义,但它并非一个单一的概念。初学者最容易混淆的,就是状态值函数v(s)和动作值函数q(s,a)。它们就像同一枚硬币的两面,分别从不同的视角,描述了同一个决策问题。
状态值函数 v̂(s),回答的是:“如果我现在身处状态s,并且从此刻起,我将严格遵循某个特定的策略π(比如‘总是选估计值最高的动作’),那么我从s开始,未来所能获得的平均回报,会是多少?” 它衡量的是一个位置的好坏。例如,在飞行路径中,“伦敦”这个状态的v̂值,代表了从伦敦出发,按照当前策略(比如总是飞向估计值最高的下一站),最终抵达香港所能获得的总时间(负值,越小越好)的期望值。它不关心你具体会采取哪一步行动,它只关心你“站在哪里”,以及你“打算怎么走”。
动作值函数 q̂(s,a),回答的是:“如果我现在身处状态s,并且我立即采取动作a,然后从此刻起,我将严格遵循策略π,那么我未来所能获得的平均回报,会是多少?” 它衡量的是一个决策的好坏。它比v̂更进一步,它不仅告诉你“站在这里怎么样”,还告诉你“如果我往左走/往右走/往前走,分别会怎么样”。在飞行路径中,q̂(伦敦, 开罗)的值,就代表了“我现在在伦敦,如果我立刻决定飞往开罗,然后从开罗开始继续按我的策略飞,那么我最终抵达香港的总时间期望值是多少”。
这两者的关系,由贝尔曼方程的“动作版本”所定义:q̂(s,a) = r(s,a,s') + γ * v̂(s')。这个公式,就是所谓的“一步前瞻”(1-step lookahead)。它非常直观:一个动作的价值,等于你立刻能拿到的奖励r,加上你到达下一个状态s'后,那个状态本身的价值v̂(s'),再乘以一个折扣因子γ(用来体现“远期回报不如眼前回报值钱”的经济学常识)。注意,这里用的是v̂(s'),而不是q̂(s',a'),因为我们只向前看一步,对于s'之后会发生什么,我们直接采用对s'的最新估值v̂(s')来代替。这正是TD-Learning“自举”(bootstrapping)思想的体现:它用一个估计值(v̂(s'))来更新另一个估计值(v̂(s)),形成一个自我强化的学习闭环。
在实际编程中,我们通常只需要维护其中一个。在本教程的“FlightPathEnv”示例中,由于环境是确定性的(从一个城市飞往另一个城市,结果是唯一的),并且动作空间很小,我们选择维护v̂(s),并在选择动作时,通过遍历所有可能的下一状态,计算q̂(s,a) = r + γ * v̂(s'),然后选出最大的那个。这是一种“隐式”地使用q̂的方式。而在更复杂的、动作空间巨大的环境中(比如围棋,每一步有数百种可能),我们往往会直接维护一个q̂(s,a)表,因为这样可以避免在每个状态下都要进行一次“遍历-计算”的开销。
3.2 终端状态(Terminal State)的“零值”之谜
在所有关于RL的讨论中,有一个看似微不足道、实则至关重要的细节,常常被初学者忽略,那就是终端状态(Terminal State)的值,必须被定义为0。在“FlightPathEnv”中,当AI抵达“香港”时,游戏结束,这个状态就是终端状态。为什么它的v̂值必须是0?这背后有着深刻的数学和逻辑必然性。
首先,从定义出发。状态值函数v(s)的定义是:“从状态s开始,遵循策略π所能获得的未来回报的期望值”。请注意关键词——“未来”。当AI已经抵达“香港”,旅程宣告结束,它不会再采取任何动作,也不会再收到任何奖励。因此,它从“香港”这个状态开始,所能获得的未来回报,就是一个空集,其期望值自然为0。这是定义层面的铁律,不容置疑。
其次,从算法实现的角度看,这个“0”是TD-Learning更新方程能够自洽运行的基石。回顾TD更新方程:v̂(s) ← v̂(s) + α * [r + γ * v̂(s') - v̂(s)]。当s'是一个终端状态时,s'之后没有下一个状态,因此v̂(s')这一项必须有一个确定的、有意义的值,否则整个方程就无法计算。如果我们随意给它赋一个非零值,比如100,那么算法就会错误地认为,抵达终端状态后,还能“凭空”获得额外的100点奖励,这显然违背了环境的物理事实。将v̂(s')设为0,意味着我们诚实地告诉算法:“到这里就结束了,后面什么都没有了。” 这确保了整个学习过程的因果链条是完整且可信的。
最后,这个“0”也完美地服务于我们的决策逻辑。在“Barry的停车场”练习中,假设AI距离终点只差一步。它有两个选择:A. 向前走,直接进入终端状态(奖励+1);B. 向左走,进入一个普通格子(奖励0)。根据q̂的计算:
- q̂(当前, 向前) = 1 + γ * v̂(终端) = 1 + 0.99 * 0 = 1
- q̂(当前, 向左) = 0 + γ * v̂(左格) = 0 + 0.99 * (某个小于1的数) < 1
因此,贪婪策略(greedy action)会毫不犹豫地选择“向前”,因为它能立刻获得最大的即时价值。这个决策的正确性,完全依赖于v̂(终端) = 0这个前提。如果v̂(终端)被错误地设为100,那么q̂(当前, 向前) = 1 + 0.99 * 100 = 100,而q̂(当前, 向左) = 0 + 0.99 * 100 = 99,两者相差无几,算法将无法做出明确、果断的最优决策。所以,“终端状态值为0”不是一个可以商量的编程技巧,而是整个RL理论大厦的地基之一,是连接数学定义、算法实现和实际决策的黄金纽带。
3.3 学习率α(Alpha):收敛速度与学习稳定性的“双刃剑”
在TD-Learning的更新方程中,学习率α(Alpha)是一个看似简单、实则威力巨大的超参数。它的取值范围被严格限定在0到1之间(0 < α < 1),而这个小小的数字,却直接决定了你的AI是会“一夜暴富”还是“细水长流”,是会“稳步前进”还是“原地踏步”。
我们可以把v̂(s)想象成一个不断被修正的“信念”。每一次与环境的交互,都是一次对这个信念的“投票”。α,就是这次投票的权重。当α=0.1时,意味着你只愿意用本次新获得的经验(r + γ * v̂(s')),来替换掉你旧信念(v̂(s))的10%;而剩下的90%,你依然固执地相信自己过去的经验。当α=0.9时,情况则截然相反,你几乎完全抛弃了过去的信念,全盘接受这一次的新经验。
提示:α的选择,本质上是在“记忆”与“遗忘”之间做权衡。一个高α的AI,学习速度快,能迅速响应环境的变化,但它也像一个情绪化的人,容易被一次偶然的糟糕经历(比如一次意外的高负奖励)所左右,导致其价值估计剧烈震荡,难以收敛。一个低α的AI,则像一个沉稳的老者,它对新信息持怀疑态度,需要多次重复的验证才会缓慢地改变自己的看法。它的学习曲线平滑、稳定,但收敛速度极慢,可能需要成千上万次的交互,才能学到一个还算靠谱的策略。
在本教程的练习中,推荐的α值是0.2。这是一个经过大量实践检验的“甜点”(sweet spot)。它既保证了AI有足够的“弹性”去吸收新知识,又不至于让它变得过于“善变”。你可以把它理解为一种“温和的修正主义”:既不顽固守旧,也不盲目激进。在你自己的项目中,如果环境变化剧烈(比如股票价格),可以尝试将α调高至0.3或0.4;如果环境相对稳定(比如一个固定的迷宫),则可以将α调低至0.05或0.1,以换取更高的最终精度。记住,没有放之四海而皆准的α,它永远是你与你的特定环境之间,一场需要耐心调试的对话。
4. 实操过程详解:从零开始,手把手复现TD-Learning
4.1 环境搭建:理解Env类的接口契约
在开始编写学习算法之前,我们必须先理解与之交互的对象——环境(Environment)。本教程采用了与OpenAI Gym高度兼容的Env类设计。这不是一个随意的约定,而是一个经过工业界千锤百炼的、标准化的接口契约。它规定了所有环境必须提供的三个核心方法,这就像USB接口的物理标准,确保了任何符合标准的“充电器”(算法)都能为任何符合标准的“手机”(环境)充电。
from typing import Dict, Tuple, Any class Env: def __init__(self): """初始化环境。这是对象创建时的“出生仪式”,负责设置所有初始状态。""" self.reset() # 调用reset,完成初始化 def step(self, action: Any) -> Tuple[Any, float, bool, Dict]: """ 执行一个动作,推动环境向前演化一步。 Args: action: 一个任意类型的对象,代表智能体想要执行的动作。 Returns: 一个四元组 (state, reward, done, info): - state: 执行动作后,环境所处的新状态。 - reward: 执行此动作所获得的即时奖励。 - done: 一个布尔值,表示此次“回合”(episode)是否已经结束。 - info: 一个字典,用于存放任何额外的、调试用的信息(如碰撞检测详情)。 """ raise NotImplementedError() # 子类必须实现此方法 def reset(self) -> Tuple[Any, float, bool, Dict]: """ 重置环境,使其回到一个全新的、可重复的初始状态。 Returns: 一个四元组 (state, reward, done, info),其含义与step相同。 注意:reward在此处通常为None,因为重置本身不产生奖励。 """ raise NotImplementedError()这个接口的设计,蕴含着深刻的工程智慧。reset()方法确保了实验的可重复性——你可以无数次地将环境拉回同一起点,进行公平的对比测试。step()方法则封装了环境的所有复杂性,它对外部世界(即你的学习算法)而言,是一个纯粹的“黑箱”。你不需要知道step()内部是如何计算奖励、如何模拟物理引擎的,你只需要知道:我给它一个action,它就一定会给我一个确定的(state, reward, done, info)。这种清晰的职责分离,是构建大型、可维护RL系统的基础。在后续的“FlightPathEnv”中,你将看到这个抽象接口是如何被一个具体的、有血有肉的飞行路径问题所实现的。
4.2 核心算法:TD-Learning的Python实现
现在,让我们将前面所有的理论,凝结成一段可运行的、逐行注释的Python代码。这段代码,就是TD-Learning的灵魂所在。
import numpy as np from typing import Dict, List, Tuple, Any def td_learning( env: Env, num_episodes: int = 1000, alpha: float = 0.2, gamma: float = 0.99, epsilon: float = 0.1, policy: str = "epsilon_greedy" ) -> Dict[Any, float]: """ 使用时序差分学习(TD-Learning)来估计给定环境的状态值函数。 Args: env: 一个符合Env接口的环境对象。 num_episodes: 训练的回合总数。 alpha: 学习率,控制更新步长。 gamma: 折扣因子,控制远期奖励的重要性。 epsilon: epsilon-greedy策略中的探索概率。 policy: 选择的策略类型,目前仅支持"epsilon_greedy"。 Returns: 一个字典,键为状态,值为该状态的估计值v̂(s)。 """ # 1. 初始化值函数估计。我们使用一个字典,key为状态,value为v̂(s)。 # 初始值设为0.0,这是一种常见且安全的起点。 value_function = {} # 2. 我们需要一个方法来获取环境中的所有可能状态。 # 在FlightPathEnv中,我们可以预先定义一个状态列表。 # 在更通用的环境中,这可能需要通过探索来发现。 all_states = ["New York", "London", "Amsterdam", "Tel Aviv", "Cairo", "Bangkok", "Hong Kong"] for state in all_states: value_function[state] = 0.0 # 3. 主训练循环:运行num_episodes个回合 for episode_num in range(num_episodes): # 4. 每个回合开始前,必须重置环境,获得初始状态 state, _, done, _ = env.reset() # 5. 在一个回合内,持续与环境交互,直到done为True while not done: # 6. 根据当前策略,选择一个动作 # 这里我们实现了epsilon-greedy策略的核心逻辑 if np.random.random() < epsilon: # 探索:以epsilon概率,随机选择一个动作 # 对于FlightPathEnv,动作就是所有可能的下一状态 possible_actions = env.POSS_STATE_ACTION.get(state, []) action = np.random.choice(possible_actions) if possible_actions else state else: # 利用:以1-epsilon概率,选择能带来最高q值的动作 # 计算所有可能动作的q值:q(s,a) = r(s,a) + gamma * v̂(s') q_values = [] possible_actions = env.POSS_STATE_ACTION.get(state, []) for a in possible_actions: # 获取执行动作a后的奖励和下一状态(注意:这里是确定性的) # 在FlightPathEnv中,reward是预定义的,s'就是a本身 reward = env.REWARDS.get((state, a), 0.0) next_state = a # 计算q值 q_val = reward + gamma * value_function[next_state] q_values.append((a, q_val)) # 选择q值最大的动作 if q_values: action, _ = max(q_values, key=lambda x: x[1]) else: action = state # 如果没有可能的动作,保持原地 # 7. 执行动作,与环境交互,获取反馈 next_state, reward, done, _ = env.step(action) # 8. 关键步骤:TD-Learning更新! # 使用TD更新方程:v̂(s) ← v̂(s) + α * [r + γ * v̂(s') - v̂(s)] # 注意:如果next_state是终端状态,v̂(next_state)应为0 if done: # 终端状态的值为0 value_function[state] = value_function[state] + alpha * ( reward + gamma * 0.0 - value_function[state] ) else: # 非终端状态,使用v̂(next_state)进行更新 value_function[state] = value_function[state] + alpha * ( reward + gamma * value_function[next_state] - value_function[state] ) # 9. 将当前状态更新为下一状态,准备下一次循环 state = next_state return value_function # 使用示例 if __name__ == "__main__": from flight_path_env import FlightPathEnv # 假设这是你导入的环境模块 env = FlightPathEnv() learned_v = td_learning(env, num_episodes=5000, alpha=0.2, gamma=0.99, epsilon=0.1) print("Learned Value Function:") for state, value in learned_v.items(): print(f"{state}: {value:.2f}")这段代码的每一行,都对应着我们前面讲解的每一个核心概念。它不是一个黑盒,而是一张清晰的地图。当你运行它时,你会看到learned_v字典中的数值,从最初的全0,逐渐演化成一个有意义的、反映各城市“战略价值”的分布。你会发现,“香港”的值始终是0(因为它是终端状态),“纽约”的值会是一个很大的负数(因为从它出发,要经历漫长的旅程),而“伦敦”和“阿姆斯特丹”的值,则会根据它们在最优路径上的位置,呈现出微妙的差异。这个过程,就是“从经验中学习”的具象化呈现。
4.3 “Barry的停车场”:网格世界的完整实战演练
现在,让我们将视野从抽象的飞行路径,拉回到一个更直观、更易可视化的场景——一个二维网格世界。这不仅是对TD-Learning的又一次巩固,更是对ϵ-greedy策略必要性的终极验证。
class ParkingLotEnv(Env): """一个简化的停车场环境,用于演示TD-Learning和epsilon-greedy。""" def __init__(self, width=5, height=5): super().__init__() self.width = width self.height = height # 定义障碍物坐标(红色方块) self.obstacles = {(1, 1), (1, 2), (2, 1), (2, 2)} # 左上角的一个2x2障碍区 # 定义起点和终点 self.start = (0, 0) # 左下角 self.goal = (width-1, height-1) # 右上角 def reset(self) -> Tuple[Tuple[int, int], float, bool, Dict]: self.state = self.start self.done = False return self.state, 0.0, self.done, {} def step(self, action: str) -> Tuple[Tuple[int, int], float, bool, Dict]: # 定义四个基本动作 actions = { 'up': (0, 1), 'down': (0, -1), 'left': (-1, 0), 'right': (1, 0) } dx, dy = actions[action] new_x = max(0, min(self.width-1, self.state[0] + dx)) new_y = max(0, min(self.height-1, self.state[1] + dy)) new_state = (new_x, new_y) # 检查是否撞墙或撞障碍物 if new_state in self.obstacles: # 撞障碍物,惩罚,并停留在原地 reward = -10 new_state = self.state elif new_state == self.goal: # 到达终点,大奖励 reward = 100 self.done = True else: # 普通行走,小惩罚(鼓励尽快结束) reward = -1 self.state = new_state return self.state, reward, self.done, {} # 创建环境并运行学习 env = ParkingLotEnv(width=5, height=5) learned_v = td_learning( env, num_episodes=10000, alpha=0.1, gamma=0.95, epsilon=0.1 ) # 可视化学习结果(伪代码,实际需用matplotlib) print("Parking Lot Value Function (approximate):") for y in range(env.height-1, -1, -1): # 从上到下打印,符合坐标系直觉 row = "" for x in range(env.width): state = (x, y) if state in env.obstacles: row += " X " # 障碍物 elif state == env.goal: row += " G " # 终点 else: # 显示该状态的值,四舍五入到整数 val = int(learned_v.get(state, 0)) row += f"{val:3d}" print(row)运行这段代码,你将看到一个5x5的网格,其中障碍物被标记为X,终点为G,而其他格子则显示着它们的估计值。你会清晰地看到,值最高的格子,会沿着一条避开障碍物、通向终点的路径,形成一个“价值高地”。而那些被障碍物包围、远离终点的格子,其值则会是很大的负数。这个可视化结果,就是TD-Learning在你眼前,一笔一划绘制出的“认知地图”。它不再是一个抽象的数学概念,而是一个你可以触摸、可以理解、可以信任的决策依据。而这一切的起点,正是那个小小的、被很多人忽视的epsilon=0.1。
5. 常见问题与排查技巧实录:那些只有亲手敲过代码才知道的坑
5.1 问题速查表:从报错到收敛的“通关秘籍”
在将上述代码付诸实践的过程中,你几乎必然会遇到一系列典型问题。这些问题,往往不是源于算法的错误,而是源于对RL概念的细微误解,或是编程实现中的一个疏忽。以下是我整理的、在真实教学和项目中高频出现的问题清单,附带了精准的定位方法和解决思路。
| 问题现象 | 可能原因 | 排查与解决技巧 |
|---|---|---|
| 值函数完全不更新,所有状态的v̂值始终保持为初始值(如0) | alpha被错误地设为0,或者alpha的更新逻辑被写在了if done:分支之外,导致只有在终端状态才更新。 | 检查td_learning函数中value_function[state] = ...这一行代码,确认它位于while not done:循环内部,并且在step()调用之后。打印alpha的值,确保它是一个大于0的浮点数。 |
值函数发散(数值变得极大或极小,甚至出现inf或nan) | alpha设置得过大(如>0.5),或者gamma设置为1.0,导致误差被不断放大。 | 将alpha降低到0.1或0.05,将gamma设置为0.99。在更新前添加一个检查:if np.isnan(value_function[state]) or np.isinf(value_function[state]): print("NaN detected!"); break。 |
| 算法学到了一个“死循环”策略,AI在两个状态间反复横跳 | 环境的奖励设计不合理,或者epsilon设置过小,导致AI过早地陷入了局部最优,且没有足够的探索来打破它。 | 检查step()函数返回的reward。确保“无效动作”(如撞墙)有显著的负奖励(如-10),而“有效行走”有轻微的负奖励(如-1),以鼓励尽快到达终点。将epsilon临时提高到0.3,观察是否能打破循环。 |
| 终端状态(如"香港")的值不是0,而是其他数值 | 在step()函数中,当done=True时,没有正确地将v̂(s')设为0,而是错误地使用了value_function[next_state],而next_state可能尚未被初始化。 | 在td_learning的更新逻辑中,务必区分done为True和False两种情况。当done=True时,v̂(s')必须硬编码为0.0,绝不能去查询value_function字典。 |
| **学习过程极其缓慢,运行数千回合后, |