强化学习实战:从迷宫问题到策略优化
1. 从迷宫游戏开始,理解强化学习的“灵魂”
如果你玩过那种最简单的迷宫游戏,就是一只小老鼠在格子里找奶酪,那你其实已经摸到了强化学习的门把手。强化学习听起来高大上,但它的核心思想,和我们小时候玩游戏、学走路、甚至训练宠物,本质上是一样的:通过尝试和反馈来学习。
想象一下,你第一次走进一个陌生的迷宫。你不知道哪条路是死胡同,哪条路通向出口。你只能试探性地往前走。撞墙了,很疼,你知道这条路不行;走几步没撞墙,感觉还行;突然看到出口了,还有奖励,你开心极了。下次再进这个迷宫,你肯定会优先选择上次成功的路线。这个“尝试-反馈-学习”的循环,就是强化学习的灵魂。
在技术世界里,我们把上面这个场景抽象成几个核心角色:
- 智能体:就是那只找奶酪的小老鼠,或者游戏里的角色。它是做决策、执行动作的主体。
- 环境:就是整个迷宫,包括墙壁、通道、出口。它接收智能体的动作,并给出反馈。
- 状态:智能体在迷宫中的具体位置,比如“我在第3行第5列”。这是环境给智能体的“快照”。
- 动作:智能体在当前状态下能做的事,比如“向上”、“向左”。
- 奖励:环境给的即时反馈。撞墙是负奖励(比如-10分),走到出口是正奖励(+100分),在空地上走一步消耗时间,可能给个微小的负奖励(-0.1分),鼓励它尽快找到出口。
- 策略:这是智能体的“大脑”或“行为准则”。它根据当前状态,决定采取哪个动作。一开始可能是瞎蒙,但通过学习,它会形成一套“在A点就向右走,在B点就向下走”的最优策略。
- 价值:比即时奖励更长远的概念。它回答的是:“从当前这个状态出发,我未来总共能期望拿到多少奖励?”出口旁边的格子,价值就很高;死胡同深处的格子,价值就很低。智能体追求的是最大化长期累积价值,而不是眼前的一步奖励。
所以,强化学习的目标,就是让智能体(我们的程序)在和环境(比如迷宫)的不断互动中,学会一套最优策略,从而在任何状态下都能做出能获得最大长期回报的决策。迷宫问题之所以是绝佳的入门案例,就是因为它把所有这些抽象概念,都放到了一个我们直观能理解的、有边界的小世界里。
2. 为迷宫建模:马尔可夫决策过程
要把迷宫游戏交给计算机去学习,我们首先得用数学语言给它建立一个清晰的模型。这个模型就是马尔可夫决策过程。别被名字吓到,它其实就是一套描述我们上面所有概念的严谨公式。
MDP的核心假设是“未来只取决于现在”,也就是所谓的马尔可夫性质。在迷宫里,老鼠下一步能去哪、会得到什么奖励,只取决于它现在站在哪个格子里,跟它之前是怎么晃悠过来的没关系。这非常符合直觉,也大大简化了问题。
一个MDP通常由五个要素构成,我们用迷宫的例子一一对应:
- 状态集合 S:所有可能的位置。对于一个8x8的迷宫,状态就是(0,0), (0,1), ..., (7,7)这64个坐标。
- 动作集合 A:通常是 {上,下,左,右}。在边缘或墙边,可行动作会减少。
- 状态转移概率 P:在状态s下执行动作a,跑到状态s‘的概率。在标准迷宫问题里,这个概率是确定的。比如在(2,3)位置执行“向右”,100%会到达(2,4)(如果(2,4)不是墙)。如果(2,4)是墙,那就会100%留在原地。所以P通常是个非常简单的规则。
- 奖励函数 R:这是驱动学习的关键。我们需要精心设计。一个常见的设计是:
- 到达终点:+10
- 撞墙:-5(严厉惩罚,让它学会避障)
- 每走一步:-0.01(鼓励效率,避免它无限闲逛)
- 其他普通格子:0 这个设计就像训狗:做对了给零食(正奖励),做错了轻微呵斥(小负奖励),严重错误(撞墙)就严厉批评(大负奖励)。
- 折扣因子 γ:一个介于0和1之间的数,比如0.9。它决定了智能体有多“目光长远”。γ越接近1,它越重视未来的奖励;γ越接近0,它就越“短视”,只在乎眼前。设置γ是为了让无限时间步长的累积奖励有一个有限值,在数学上是必须的。
把这五样东西定义清楚,一个迷宫强化学习问题的“考场”就搭建好了。接下来,就是让智能体进场考试和学习。而最经典的两种“解题方法”就是策略迭代和值迭代,它们就像是两位风格不同的围棋教练。
3. 策略迭代:稳扎稳打的“改进派”
策略迭代的思路非常符合人类的学习方式:先评估自己当前的水平,然后找出不足进行改进,再评估改进后的水平,如此循环,直到自己满意为止。它分为两个核心步骤,交替进行:策略评估和策略改进。
3.1 第一步:策略评估
假设智能体初始有一个很烂的策略,比如完全随机乱走(每个方向概率都是25%)。策略评估要做的,就是计算在这个固定策略下,每一个状态的价值V(s)是多少。
怎么算?靠的是贝尔曼期望方程。这个方程是强化学习的基石,它表达了一个状态的价值和它后续状态价值之间的关系。公式看起来复杂,但道理很简单:
当前状态的价值 = 当前动作的即时奖励 + 未来可能状态价值的折扣平均。
具体来说,对于任意状态s,其价值V(s)等于:按照当前策略π,选择动作a的概率,乘以(执行a得到的即时奖励 + γ * 到达下一个状态s‘的价值V(s’)),然后对所有可能的动作a求和。
用代码来理解更直观。假设我们已经有了一个策略矩阵policy,形状是[状态数, 动作数],policy[s][a]就表示在状态s下选择动作a的概率。还有一个价值数组values,初始全为0。
def policy_evaluation(policy, values, rewards, gamma=0.9, theta=1e-6):
"""
策略评估:计算给定策略下的状态价值函数
policy: 策略矩阵 [num_states, num_actions]
values: 状态价值数组 [num_states]
rewards: 即时奖励,这里简化处理,假设只依赖于(s,a)
gamma: 折扣因子
theta: 收敛阈值
"""
while True:
delta = 0 # 记录本轮迭代中价值函数的最大变化
new_values = values.copy()
for s in range(num_states): # 遍历所有状态
v = 0
for a in range(num_actions): # 遍历所有可能动作
# 假设我们有一个函数 get_next_state_and_reward(s, a)
# 返回 (next_state, reward, done)
next_s, reward, _ = get_next_state_and_reward(s, a)
# 贝尔曼期望方程核心:按策略概率加权求和
v += policy[s][a] * (reward + gamma * values[next_s])
new_values[s] = v
delta = max(delta, abs(v - values[s]))
values = new_values
if delta < theta: # 如果价值函数变化很小,认为已收敛
break
return values
这个过程会不断迭代,直到所有状态的价值V(s)稳定下来。这时,我们就准确知道了在当前这个“乱走”策略下,每个位置到底值多少分。
3.2 第二步:策略改进
知道了每个状态的价值后,我们就可以做改进了。策略改进的思想很直接:在每个状态,我都看看有没有比当前策略更好的动作。
具体做法是,对每个状态s,我们计算所有可能动作a的“动作价值”Q(s, a)。Q(s, a)表示在状态s下执行动作a,然后此后一直遵循旧策略所能得到的总价值。计算公式是:Q(s, a) = 即时奖励 + γ * V(下一个状态)。
然后,我们比较所有Q(s, a),选出最好的那个动作a*。新的策略π‘在状态s下,就会变成一个贪婪策略:百分之百选择a*,其他动作概率为0。
def policy_improvement(values, policy, rewards, gamma=0.9):
"""
策略改进:根据当前价值函数,改进策略
values: 当前状态价值函数
policy: 待改进的策略
"""
policy_stable = True # 假设策略已经稳定
for s in range(num_states):
old_action = np.argmax(policy[s]) # 旧策略下最可能选的动作
# 计算所有动作的Q值
q_values = []
for a in range(num_actions):
next_s, reward, _ = get_next_state_and_reward(s, a)
q = reward + gamma * values[next_s]
q_values.append(q)
best_action = np.argmax(q_values) # 找到Q值最大的动作
# 将策略更新为贪婪策略(选择best_action的概率为1)
new_policy_s = np.zeros(num_actions)
new_policy_s[best_action] = 1.0
policy[s] = new_policy_s
# 检查最优动作是否发生变化
if old_action != best_action:
policy_stable = False
return policy, policy_stable
3.3 策略迭代的主循环
把评估和改进连起来,就是策略迭代算法:
def policy_iteration():
# 1. 初始化:随机策略或均匀策略
policy = np.ones((num_states, num_actions)) / num_actions
values = np.zeros(num_states)
iteration = 0
while True:
iteration += 1
print(f"策略迭代第 {iteration} 轮")
# 2. 策略评估
values = policy_evaluation(policy, values, rewards)
# 3. 策略改进
policy, policy_stable = policy_improvement(values, policy, rewards)
# 4. 检查策略是否已收敛(不再改变)
if policy_stable:
print("策略已收敛至最优!")
break
return policy, values
策略迭代的优点是非常稳定,每次迭代都保证策略不会变差,最终必然收敛到最优策略。但缺点也很明显:每次策略评估都需要遍历所有状态进行多次迭代直到收敛,计算量可能比较大。在实际编码时,策略评估不一定非要完全收敛,可以只迭代几次就进行改进,这被称为“截断策略迭代”,是常用的加速技巧。
4. 值迭代:一步到位的“优化派”
值迭代是另一种思路,它更直接,可以看作是策略迭代的一种极限情况。它想:既然最终我们要的是最优策略,而最优策略是由最优价值函数决定的,那我为什么不直接去逼近最优价值函数呢?
值迭代的核心是贝尔曼最优方程。它跳过了“评估某个策略”的步骤,直接假设我们采取的是当前“看起来最好”的动作。它的更新公式是:
V(s) = max_a [ 即时奖励(s, a) + γ * V(下一个状态) ]
看到了吗?它把策略评估中的“按概率加权求和”,换成了“取最大值”。这意味着,在更新某个状态的价值时,我直接看从它出发,哪个动作能带来最大的长期回报,并用这个最大值来更新当前状态的价值。
值迭代的算法流程非常简洁:
def value_iteration(rewards, gamma=0.9, theta=1e-6):
"""
值迭代:直接求解最优价值函数
"""
values = np.zeros(num_states)
while True:
delta = 0
new_values = values.copy()
for s in range(num_states):
# 如果是终止状态(如出口),其价值固定
if is_terminal_state(s):
continue
# 计算所有可能动作的Q值,并取最大值
q_values = []
for a in range(num_actions):
next_s, reward, _ = get_next_state_and_reward(s, a)
q = reward + gamma * values[next_s]
q_values.append(q)
best_value = max(q_values) # 贝尔曼最优方程核心:取max
new_values[s] = best_value
delta = max(delta, abs(best_value - values[s]))
values = new_values
if delta < theta:
print("价值函数已收敛!")
break
# 价值函数收敛后,根据最优价值函数导出确定性最优策略
policy = extract_policy(values, rewards, gamma)
return policy, values
def extract_policy(values, rewards, gamma):
"""从最优价值函数中提取最优策略"""
policy = np.zeros((num_states, num_actions))
for s in range(num_states):
q_values = []
for a in range(num_actions):
next_s, reward, _ = get_next_state_and_reward(s, a)
q = reward + gamma * values[next_s]
q_values.append(q)
best_action = np.argmax(q_values)
policy[s][best_action] = 1.0
return policy
值迭代通常比策略迭代更快,因为它每次迭代都直接进行“优化”操作,不需要等待一个中间策略完全评估好。在很多问题中,值迭代是更常用的选择。你可以把它理解为一个不断“传播”奖励的过程:出口的价值最高,然后它旁边的格子通过最大值更新,也获得了较高的价值,价值像波纹一样一层层扩散到整个迷宫,直到稳定。
5. 实战调优:避开局部最优的坑
理论很完美,但一写代码运行,你可能会发现智能体“卡住了”。比如,它总是走到某个死胡同前面就停住,或者在一个小圈子里来回转,就是找不到全局最优的出口路径。这就是陷入了局部最优。
在迷宫问题中,局部最优通常表现为智能体找到了一条能获得一些正奖励(比如避开了一面墙)但并非最短的路径,或者因为探索不足而错过了更好的路线。怎么解决呢?这里有几个我实战中常用的“杀手锏”。
5.1 调整奖励函数设计
奖励函数是智能体学习的“指挥棒”,设计不当是陷入局部最优的首要原因。
- 惩罚的设计:撞墙的惩罚(如-20)要足够大,让它深刻记住。但也要小心,如果惩罚过大,智能体可能会变得过于保守,躲在起点附近不敢动弹。
- 步数惩罚:每走一步给一个很小的负奖励(如-0.01到-0.1),这非常关键。它能鼓励智能体寻找更短的路径。如果没有步数惩罚,智能体找到一条能到终点的路后就没动力优化了,哪怕这条路绕了很远。
- “探索奖金”:对于长时间未被访问的状态,可以给予一个小的正奖励。这能主动引导智能体去探索未知区域,是解决局部最优的强力手段,但在简单迷宫问题中可能不必要。
5.2 利用ε-贪婪策略平衡探索与利用
这是强化学习中最著名、最实用的技巧。智能体不能永远贪婪(只选当前看来最好的动作),否则可能永远发现不了全局更好的选择;也不能永远随机探索,那样学不到东西。
ε-贪婪策略就是一个简单的平衡器:
- 以 1-ε 的概率,选择当前Q值最高的动作(利用已知知识)。
- 以 ε 的概率,随机选择一个动作(探索新可能性)。
def choose_action_epsilon_greedy(state, q_table, epsilon):
if np.random.uniform(0, 1) < epsilon:
# 探索:随机选一个动作
action = np.random.choice(possible_actions(state))
else:
# 利用:选择Q值最高的动作
action_values = q_table[state]
# 可能有多个动作Q值相同,随机选一个避免固定
max_actions = np.where(action_values == np.max(action_values))[0]
action = np.random.choice(max_actions)
return action
技巧:通常我们让ε随着训练过程衰减。训练初期,ε设得大一些(如0.5),鼓励多探索;训练后期,ε逐渐减小(如降到0.01),让智能体专注于利用学到的最优策略。这个简单的机制能极大提升找到全局最优解的概率。
5.3 调整学习率与折扣因子
这两个超参数对学习过程影响巨大。
- 学习率 α:控制新信息覆盖旧信息的程度。α太大(接近1),学习不稳定,像狗熊掰棒子,学了新的就忘了旧的;α太小(接近0),学习速度极慢。通常从0.1开始尝试,可以随着训练逐步减小。
- 折扣因子 γ:决定智能体的“远见”程度。在迷宫问题中,因为目标是找到出口,这是一个有明确终点的任务,γ可以设得比较高,比如0.9或0.99,让智能体愿意为长远回报(出口的大奖励)放弃眼前的微小负奖励(步数惩罚)。如果γ太低,智能体就会变得短视,可能因为不想承受步数惩罚而拒绝向远处探索。
5.4 从表格方法到函数逼近
我们上面讨论的策略迭代和值迭代,以及常用的Q-learning,都属于表格型方法。它们为每一个状态(或状态-动作对)维护一个价值估计值。这在8x8的迷宫中没问题(64个状态),但如果迷宫变成1000x1000呢?状态空间爆炸,表格根本存不下。
这时就需要函数逼近方法,比如用神经网络来近似价值函数或策略函数。输入是状态(如坐标),输出是价值或动作概率。这就是深度强化学习(如DQN、Policy Gradient)干的事情。虽然迷宫问题用不上,但这是你从入门迈向实战的必经之路。当智能体需要处理像图像(像素矩阵)这样高维的状态输入时,表格方法彻底失效,神经网络的价值就凸显出来了。
我在实际项目中,通常先用小规模迷宫和表格方法快速验证奖励函数和算法逻辑是否正确,然后再迁移到更复杂的环境和深度学习方法中。记住,调参和避坑的过程,本身就是在深入理解强化学习如何与环境交互、如何从反馈中学习的本质。多动手改改代码,观察智能体行为的变化,你会对这些理论有更“骨感”的认识。
更多推荐
所有评论(0)