第 96 课 - 强化学习入门:基本概念与 Q-Learning
1. 学习目标
- 理解强化学习(RL)的核心要素:代理、环境、状态、动作、奖励。
- 理解价值函数和策略的基本概念。
- 掌握 Q-Learning 算法的基本原理和更新规则。
- 能够实现一个简单的 Q-Learning 代理解决网格世界问题。
- 了解探索与利用(Exploration vs. Exploitation)的权衡。
2. 核心概念
想象你在玩一个迷宫游戏,你就是 代理(Agent)。你身处的迷宫就是 环境(Environment)。你在迷宫中的每个位置称为一个 状态(State)。你可以选择上下左右移动,这就是 动作(Action)。每当你到达宝藏,游戏给你加分,这个分数就是 奖励(Reward)。
强化学习 的目标是让代理通过与环境的交互(试错),学习一个 策略(Policy),即在每个状态下选择哪个动作,能使得长期累积的奖励最大化。
Q-Learning 是一种无模型(Model-Free)的强化学习算法。它学习一个 动作价值函数 Q(s, a),即在状态 s 下采取动作 a 后,期望能获得的未来累积折扣奖励总和。简单说,Q(s, a) 值越高,代表在状态 s 下执行动作 a 越“好”。
Q-Learning 更新规则(核心公式):
Q(s, a) ← Q(s, a) + α * [r + γ * max Q(s', a') - Q(s, a)]
α:学习率(Learning Rate),控制新信息覆盖旧信息的速度。γ:折扣因子(Discount Factor),表示未来奖励的重要程度(0-1之间)。r:立即奖励。s':执行动作后进入的新状态。max Q(s', a'):在新状态s'下所有可能动作中,Q值最大的那个。这代表了我们对未来最乐观的估计。
探索与利用:代理需要平衡“利用”已知的高价值动作和“探索”未知的动作以发现更好的路径。常用方法是 ε-贪婪策略(ε-greedy),即以概率 ε 随机探索,以概率 1-ε 选择当前Q值最高的动作。
3. 代码示例
我们将实现一个简单的 Q-Learning 代理,目标是从网格世界的左上角到达右下角的宝藏位置。我们使用一个 4x4 的网格。
import numpy as np
import random
import time
# --- 环境定义 ---
class GridWorld:
def __init__(self, rows=4, cols=4):
self.rows = rows
self.cols = cols
self.state = (0, 0) # 起始状态 (行, 列)
self.goal = (rows-1, cols-1) # 目标状态
# 定义动作:0-上,1-右,2-下,3-左
self.actions = [0, 1, 2, 3]
self.n_actions = len(self.actions)
def reset(self):
self.state = (0, 0)
return self.state
def step(self, action):
"""执行动作,返回新状态,奖励,是否结束"""
row, col = self.state
if action == 0: # 上
row = max(0, row - 1)
elif action == 1: # 右
col = min(self.cols - 1, col + 1)
elif action == 2: # 下
row = min(self.rows - 1, row + 1)
elif action == 3: # 左
col = max(0, col - 1)
self.state = (row, col)
# 到达目标奖励为1,否则为-0.1(惩罚每一步,鼓励快速到达)
reward = 1.0 if self.state == self.goal else -0.1
done = (self.state == self.goal)
return self.state, reward, done
def render(self):
"""在终端打印当前网格状态"""
grid = np.zeros((self.rows, self.cols), dtype=str)
grid[:] = '.'
grid[self.goal[0]][self.goal[1]] = 'G'
grid[self.state[0]][self.state[1]] = 'A'
print('\n'.join([' '.join(row) for row in grid]))
print('---')
# --- Q-Learning 代理 ---
class QLearningAgent:
def __init__(self, n_states, n_actions, lr=0.1, gamma=0.99, epsilon=1.0, epsilon_decay=0.995, epsilon_min=0.01):
self.n_actions = n_actions
self.lr = lr
self.gamma = gamma
self.epsilon = epsilon
self.epsilon_decay = epsilon_decay
self.epsilon_min = epsilon_min
# 初始化Q表为0,形状为 [状态数量, 动作数量]
# 注意:这里为了简化,我们将状态(row, col)映射到一个整数索引
self.q_table = np.zeros((n_states, n_actions))
def get_state_index(self, state):
"""将 (row, col) 状态映射为整数索引"""
return state[0] * 4 + state[1] # 假设网格大小为4x4
def choose_action(self, state_idx):
"""ε-贪婪策略选择动作"""
if random.random() < self.epsilon:
return random.randint(0, self.n_actions - 1) # 随机探索
else:
return np.argmax(self.q_table[state_idx]) # 选择Q值最大的动作
def learn(self, state_idx, action, reward, next_state_idx, done):
"""根据Q-Learning公式更新Q表"""
current_q = self.q_table[state_idx, action]
# 如果episode结束,没有未来奖励
if done:
target_q = reward
else:
target_q = reward + self.gamma * np.max(self.q_table[next_state_idx])
# 更新Q值
self.q_table[state_idx, action] += self.lr * (target_q - current_q)
# 衰减探索率
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
# --- 训练循环 ---
def train():
env = GridWorld()
agent = QLearningAgent(n_states=16, n_actions=4)
episodes = 1000
rewards_per_episode = []
for ep in range(episodes):
state = env.reset()
state_idx = agent.get_state_index(state)
total_reward = 0
done = False
while not done:
# 1. 代理选择动作
action = agent.choose_action(state_idx)
# 2. 环境执行动作,返回新状态和奖励
next_state, reward, done = env.step(action)
next_state_idx = agent.get_state_index(next_state)
# 3. 代理学习
agent.learn(state_idx, action, reward, next_state_idx, done)
# 4. 状态转移
state_idx = next_state_idx
total_reward += reward
rewards_per_episode.append(total_reward)
# 每100个episode打印一次进度
if (ep + 1) % 100 == 0:
avg_reward = np.mean(rewards_per_episode[-100:])
print(f"Episode {ep+1}, Average Reward: {avg_reward:.2f}, Epsilon: {agent.epsilon:.3f}")
# 训练结束,演示学到的策略
print("\n--- 训练完成,演示最优路径 ---")
state = env.reset()
state_idx = agent.get_state_index(state)
done = False
steps = 0
while not done and steps < 20:
action = np.argmax(agent.q_table[state_idx]) # 总是选择Q值最大的动作
print(f"Step {steps}: State {state}, Action {['上','右','下','左'][action]}")
env.render()
next_state, _, done = env.step(action)
state_idx = agent.get_state_index(next_state)
state = next_state
steps += 1
print(f"到达目标!总共用了 {steps} 步。")
if __name__ == "__main__":
train()
4. 实践练习
练习 1:基础运行与观察 直接运行上述代码。观察随着训练进行,平均奖励和探索率(epsilon)是如何变化的。最终代理是否能找到一条到达目标的有效路径?
练习 2:调整超参数
尝试修改 train() 函数中 QLearningAgent 的初始化参数:
- 将学习率
lr改为0.01和0.5,观察训练过程有何不同? - 将折扣因子
gamma改为0.5和0.9,这对代理的长期规划能力有何影响? - 去掉或减慢
epsilon_decay,观察代理的行为(它会不会一直原地打转?)。
练习 3:修改环境
修改 GridWorld 类:
- 在网格中间添加一堵墙(例如,状态
(1,1)和(2,1)不可到达),代理该如何绕行? - 改变奖励设计:将每步惩罚
-0.1改为0,或者将终点奖励1改为10。这会影响代理的学习速度和路径选择吗?
5. 常见错误
- 忽略探索:如果一开始就将
epsilon设为 0,代理只会利用当前已知信息,可能永远无法发现最优路径(特别是当初始Q值全为0时)。 - 折扣因子设置不当:
γ接近1会让代理更看重未来奖励(可能学得慢但长远规划好),γ接近0会让代理只关注眼前奖励。 - 学习率过高或过低:过高会导致Q值震荡,无法收敛;过低会导致学习速度过慢。
- 状态空间离散化不当:对于连续状态空间,需要更复杂的状态表示方法(如函数近似),直接使用表格型Q-Learning会失效。
- 忘记更新目标Q值:在实现更复杂的算法(如DQN)时,常见错误是错误地计算了目标
r + γ * max Q(s', a')中的Q值,应该使用旧的网络参数或独立的目标网络。
6. 小结
- 强化学习三要素:代理、环境、交互(状态、动作、奖励)。
- 目标:学习一个策略
π,以最大化长期累积奖励G。 - Q-Learning 核心:学习并更新一个动作价值表
Q(s, a),利用贝尔曼方程进行迭代。 - 关键更新公式:
Q(s, a) ← Q(s, a) + α * [r + γ * max Q(s', a') - Q(s, a)]。 - 探索与利用:ε-贪婪策略是平衡两者的最简单有效的方法之一。
- 代码实现要素:定义环境(状态、动作、转移、奖励)、初始化Q表、设计选择动作的策略、实现学习更新规则、循环训练。
Q-Learning 是理解强化学习的绝佳起点。它清晰地展示了价值迭代的思想,为后续学习更复杂的算法(如Deep Q-Networks, DQN)奠定了坚实的基础。接下来,你就可以尝试用神经网络来近似Q表,以解决状态空间巨大或连续的问题了。