Q学习深度指南
一句话总结
Q学习就是让智能体在”摸爬滚打”中学会判断每个状态下哪个动作最值得做——就像你家狗子学会在路口等红灯一样,靠的是不断试错和”这个动作值几分”的直觉积累。
关键词速览
| 核心概念 | 探索策略 | TD更新 | 收敛性 | Q表 |
|---|---|---|---|---|
| ε-greedy | 软更新 | 离策略学习 | Bellman方程 | 价值迭代 |
核心关键词表
术语 英文 符号 大白话解释 Q学习 Q-Learning 给每个”状态+动作”组合打分 时序差分 Temporal Difference 边走边学,不用等走完才知道对不对 ε-greedy Epsilon-Greedy 大部分时候选最优,偶尔瞎试试运气 TD误差 TD Error ”我觉得值5分,结果只得了3分,差2分” 学习率 Learning Rate 每次纠正多少,太大容易摇摆,太小学得慢 折扣因子 Discount Factor 未来的钱不值钱,打几折 Q值 Q-Value 最乐观情况下这个动作能拿多少分
第一部分:零基础入门——Q学习是什么?
零、一个牧羊犬的故事:让你彻底理解Q学习
想象你是个牧羊人,养了一只牧羊犬。你的目标是训练它把羊群从草地A赶到羊圈B。
问题来了:狗子怎么知道在该拐弯的时候拐弯,而不是傻愣愣直着跑?
狗子的”Q表”思维
你会发现,聪明的狗子脑子里其实有一张隐形的表格:
| 当前情况(状态) | 往左跑 | 往前跑 | 往右跑 | 往后跑 |
|---|---|---|---|---|
| 羊在左边 | 8分 | 3分 | 0分 | 1分 |
| 羊在右边 | 0分 | 3分 | 8分 | 1分 |
| 羊在正前方 | 2分 | 9分 | 2分 | 0分 |
| 羊四散 | 5分 | 5分 | 5分 | 3分 |
这个表格就是”Q表”,每个格子的数字就是Q值——狗子认为这个动作”值多少分”。
狗子是怎么学这张表的?
第一步:瞎跑试试 小狗刚出生啥也不懂,你就让它随便跑。一开始它可能往右跑,但羊其实在左边,结果羊被吓跑了。
第二步:打分纠错
- 狗子跑了5步终于把羊赶进羊圈了
- 狗子回看:最后一步(进羊圈)其实值10分
- 但倒数第二步(拐弯)只值5分
- 狗子就想:“哦,原来拐弯那个动作虽然当时看起来一般,但对最终成功很重要!”
- 所以狗子更新了对”拐弯”这个动作的评分
这就是TD学习的核心:不等走完全程才知道对不对,走一步就大概知道刚才那个动作值多少分。
关键洞察:为什么Q学习有效?
Q学习之所以有效,是因为它利用了递推关系——
一个动作的价值 = 立刻能拿到的奖励 + 未来可能拿到的最大奖励
用公式说就是:
翻译成人话就是:
“在路口往右转值多少分?” = “往右转后羊群不会乱跑(+3分奖励),然后我继续走最优路线还能再得10分” = 3 + 0.9 × 10 = 12分
这个递推关系就是Bellman方程的核心。理解了这个,你就理解了Q学习90%的精髓。
牧羊犬故事的启示
- 狗子不是背路线图:它学会的是”在什么情况下做什么动作最值”,而不是死记硬背每一步
- 狗子会犯错:有时候选了”8分”的动作,但羊突然发疯跑了,所以Q值要不断更新
- 狗子需要探索:如果狗子永远只走同一条路,它可能错过更高效的路线
- 狗子有耐心:它不是跑一趟就学会了,而是跑了几十趟才把Q表填准确
这就是Q学习的精髓:在试错中学习,在学习中改进。
一、Q学习算法原理
1.1 先问一个问题:什么是”好动作”?
在强化学习里,“好动作”不是指动作本身,而是指从这个动作开始,最终能拿到的总奖励有多少。
举个例子:
- 动作A:立刻给你1块钱,但后面啥也没有了
- 动作B:现在亏1块钱,但后面能慢慢赚10块钱
从长远来看,B才是好动作。Q学习就是要学会计算这个”长远价值”。
1.2 核心公式
Q学习的更新公式看起来吓人,但其实很简单:
让我拆开来讲:
Q(s,a) <—— 更新后的分数
↑
| + α × [TD误差]
|
| ┌─────────────────┬────────────────────┐
| │ │ │
| │ r + γ·max Q(s',a') - Q(s,a) │
| │ ↑ ↑ │
| │ │ │ │
| │ 这一刻的 之前认为 │
| │ 实际收获 这个动作值多少分 │
| │ │
| └────────────────────────────────┘
|
α = 学习率(0到1之间)
用人话说就是:
“我之前觉得这个动作值5分(Q(s,a)),现在走了一步发现其实值7分(r + γ·max Q(s’,a’)),差了2分。那我就把评分从5分更新到5 + α×2。“
1.3 离策略特性:这个特性很厉害
Q学习是个”离策略”算法。啥意思?
- 行为策略:你实际执行的策略(比如ε-greedy,到处乱跑)
- 目标策略:你学习的目标(比如总是选最优动作)
普通的学习方法:你用什么策略走路,就只能从这种走法中学东西
Q学习:你随便跑,但学到的是”如果我每次都选最优,会怎么样”
这就好比:
- 普通方法:只有跟着好老师学才能进步
- Q学习:看别人跑一遍,自己就知道”最好的跑法应该是这样”
这个特性让Q学习的样本利用率很高——你跑过的每一步都能拿来学习。
第二部分:手把手实现——从安装到训练
二、环境准备与依赖安装
2.1 安装必要的库
pip install numpy matplotlib gym torch torchvision或者一次性安装强化学习常用的库:
pip install numpy matplotlib gymnasium torch torchvision pyvirtualdisplay关于gym版本
gym==0.26.0及以上版本API有变化gymnasium是社区维护的新版本 新版本返回的是(state, info)而不是原来的state,注意区分
2.2 第一个Q学习程序:FrozenLake走迷宫
FrozenLake是一个经典入门环境:4x4的格子,冰面很滑(可能滑到相邻格子),目标是走到终点。
"""
第一个Q学习程序:让AI学会走FrozenLake
环境:4x4格子世界
目标:从起点走到终点G,不要掉进洞里
"""
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
from collections import defaultdict
# ============ 第一部分:Q-Learning Agent ============
class QLearningAgent:
"""
Q学习智能体
做的事情很简单:
1. 看到状态s,选个动作a(ε-greedy策略)
2. 做了动作a,得到奖励r和新状态s'
3. 更新Q(s,a)的值
4. 重复直到熟练
"""
def __init__(
self,
n_states: int,
n_actions: int,
learning_rate: float = 0.1,
gamma: float = 0.99,
epsilon: float = 1.0,
epsilon_decay: float = 0.995,
epsilon_min: float = 0.01
):
"""
初始化智能体
参数说明:
- n_states: 有多少个状态(FrozenLake是16个格子)
- n_actions: 有多少个动作(通常4个:上下左右)
- learning_rate (α): 每次更新走多远,0.1是常用的默认值
- gamma (γ): 折扣因子,0.99意味着100步后的奖励只值现在的0.37
- epsilon (ε): 探索率,1.0意味着100%随机探索
"""
self.n_states = n_states
self.n_actions = n_actions
self.alpha = learning_rate
self.gamma = gamma
self.epsilon = epsilon
self.epsilon_decay = epsilon_decay
self.epsilon_min = epsilon_min
# Q表:核心数据结构
# Q[s][a] = 在状态s下做动作a预计能拿多少分
self.Q = np.zeros((n_states, n_actions))
def choose_action(self, state: int) -> int:
"""
ε-greedy动作选择
有ε的概率随机选(探索),有(1-ε)的概率选当前最优(利用)
"""
if np.random.random() < self.epsilon:
# 随机选:探索新可能
return np.random.randint(self.n_actions)
else:
# 贪心选:选Q值最高的
return np.argmax(self.Q[state])
def update(self, state: int, action: int, reward: float,
next_state: int, done: bool) -> float:
"""
Q学习核心更新
公式:Q(s,a) += α × [r + γ × max Q(s',a') - Q(s,a)]
如果游戏结束了(next_state是终态),就不用加未来奖励了
"""
if done:
# 终态:下一步没有奖励了
td_target = reward
else:
# 非终态:还有未来奖励可以期待
td_target = reward + self.gamma * np.max(self.Q[next_state])
# TD误差:实际得到的和预期的差了多少
td_error = td_target - self.Q[state, action]
# 更新Q值
self.Q[state, action] += self.alpha * td_error
# ε衰减:越到后面越少探索,多利用已学到的知识
self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
return td_error
def get_policy(self) -> np.ndarray:
"""从Q表导出确定性策略"""
return np.argmax(self.Q, axis=1)
# ============ 第二部分:训练循环 ============
def train_frozenlake():
"""
训练Q学习智能体走FrozenLake
"""
# 创建环境
# FrozenLake-v1: 冰面会滑(有一定随机性)
# is_slippery=True: 动作有随机性,可能滑到相邻格子
env = gym.make('FrozenLake-v1', is_slippery=True)
# 获取状态和动作数量
n_states = env.observation_space.n # 16
n_actions = env.action_space.n # 4 (上下左右)
print(f"环境信息:{n_states}个状态,{n_actions}个动作")
print("动作映射:0=左, 1=下, 2=右, 3=上")
# 创建智能体
agent = QLearningAgent(
n_states=n_states,
n_actions=n_actions,
learning_rate=0.1,
gamma=0.99,
epsilon=1.0,
epsilon_decay=0.999,
epsilon_min=0.01
)
# 训练参数
num_episodes = 10000
# 记录训练曲线
episode_rewards = []
episode_lengths = []
success_count = 0
print("\n开始训练...")
print("-" * 50)
for episode in range(num_episodes):
# 重置环境,获取初始状态
state, info = env.reset()
total_reward = 0
steps = 0
done = False
while not done:
# 1. 选动作
action = agent.choose_action(state)
# 2. 执行动作
next_state, reward, terminated, truncated, info = env.step(action)
done = terminated or truncated
# 3. 更新Q表
agent.update(state, action, reward, next_state, done)
# 4. 准备下一步
state = next_state
total_reward += reward
steps += 1
episode_rewards.append(total_reward)
episode_lengths.append(steps)
if total_reward == 1:
success_count += 1
# 每1000个回合打印一次进度
if (episode + 1) % 1000 == 0:
recent_success = success_count / 1000 * 100
avg_reward = np.mean(episode_rewards[-1000:])
print(f"回合 {episode+1:5d} | "
f"成功率: {recent_success:5.1f}% | "
f"平均奖励: {avg_reward:.3f} | "
f"ε: {agent.epsilon:.3f}")
success_count = 0
env.close()
# ============ 第三部分:结果可视化 ============
print("\n" + "=" * 50)
print("训练完成!")
print("=" * 50)
# 绘制训练曲线
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 奖励曲线(用移动平均平滑)
window = 100
if len(episode_rewards) >= window:
smoothed = np.convolve(episode_rewards,
np.ones(window)/window, mode='valid')
axes[0].plot(smoothed)
else:
axes[0].plot(episode_rewards)
axes[0].set_xlabel('Episode')
axes[0].set_ylabel('Reward')
axes[0].set_title('Training Reward Curve')
axes[0].grid(True, alpha=0.3)
# 回合长度曲线
if len(episode_lengths) >= window:
smoothed_len = np.convolve(episode_lengths,
np.ones(window)/window, mode='valid')
axes[1].plot(smoothed_len)
else:
axes[1].plot(episode_lengths)
axes[1].set_xlabel('Episode')
axes[1].set_ylabel('Steps')
axes[1].set_title('Episode Length')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('frozenlake_training.png', dpi=150)
plt.show()
# 打印学到的策略
print("\n学到的策略(Q表):")
print(agent.Q.round(2))
print("\n最终策略(每个状态往哪走):")
policy = agent.get_policy()
action_names = ['← 左', '↓ 下', '→ 右', '↑ 上']
for s in range(n_states):
if s == 0:
print(f" 起点 → {action_names[policy[s]]}")
elif s == 15:
print(f" 终点 → {action_names[policy[s]]} (实际是终点)")
else:
print(f" 格子{s} → {action_names[policy[s]]}")
return agent, episode_rewards
# 运行训练
if __name__ == "__main__":
agent, rewards = train_frozenlake()2.3 运行结果解读
运行上面的代码,你会看到类似这样的输出:
环境信息:16个状态,4个动作
动作映射:0=左, 1=下, 2=右, 3=上
开始训练...
--------------------------------------------------
回合 1000 | 成功率: 10.2% | 平均奖励: 0.102 | ε: 0.368
回合 2000 | 成功率: 45.3% | 平均奖励: 0.453 | ε: 0.135
回合 3000 | 成功率: 58.7% | 平均奖励: 0.587 | ε: 0.050
回合 4000 | 成功率: 72.1% | 平均奖励: 0.721 | ε: 0.018
回合 5000 | 成功率: 76.8% | 平均奖励: 0.768 | ε: 0.010
...
你会发现:
- ε从1.0慢慢降到0.01:说明智能体从”瞎跑”逐渐变成”用经验”
- 成功率从10%提升到70%+:说明它真的学到了
- 训练曲线逐渐上升:证明学习在起作用
第三部分:深入原理
三、为什么Q学习有效?—— 从直觉到Bellman方程
3.1 一个简单的例子
假设你在玩一个游戏:
- 从A出发
- 每走一步扣1分
- 走到终点Z得100分
最短路线是 A → C → Z,共3步,得分 = 100 - 3 = 97分。
如果走错了绕远路,可能要10步,得分 = 100 - 10 = 90分。
Q学习要学会的就是:在每个格子,应该往哪个方向走,最终能拿最高分。
3.2 Bellman方程:递归的智慧
Bellman方程的核心洞察是:
当前位置的价值 = 现在的奖励 + 未来位置的价值
用公式写出来:
或者Q值版本:
这个方程好在哪?
它把一个大问题拆成了两个小问题:
- “现在能得到多少”(R)
- “从新位置出发能再得到多少”(γ·max Q)
3.3 迭代求解:一步一步逼近真相
我们不知道正确答案,但可以猜一个初始值,然后不断改进。
第0轮猜测:每个格子的Q值都猜0
第1轮:根据Bellman方程更新一遍
第2轮:再更新一遍
第3轮:再更新一遍
...
最终:Q值收敛到正确答案
这就是价值迭代。数学上可以证明,只要满足一些条件,这个过程一定会收敛到最优解。
3.4 为什么叫”时序差分”?
“差分”就是差别的意思,“时序差分”就是”时间上的差别”。
TD误差 = 实际得到的 - 之前的预期
之前觉得这个动作值 5 分
实际走了一步得到 3 分 + 未来还有 2 分的潜力
TD误差 = (3 + 2) - 5 = 0
但如果实际走了一步得到 8 分
TD误差 = 8 - 5 = +3
说明我之前低估了,应该把评分调高
边走边学,这就是”时序差分学习”的含义。
四、ε-greedy探索策略:怎么平衡”尝试新路线”和”走已知的路”?
4.1 探索的两难
想象你在找一家好吃的餐厅:
- 利用:去你吃过的、确定好吃的餐厅(稳妥但可能错过更好的)
- 探索:去一家没去过的餐厅(可能踩雷但也可能发现宝藏)
这就是强化学习里的探索-利用困境(Explore-Exploitation Dilemma)。
4.2 ε-greedy:最简单的解决方案
ε-greedy的思路很直接:
def choose_action(Q_values, epsilon):
"""
Q_values: 每个动作的Q值
epsilon: 探索概率
"""
if random.random() < epsilon:
# 探索:随机选一个
return random.choice(len(Q_values))
else:
# 利用:选当前最优
return argmax(Q_values)4.3 ε衰减:先广撒网,后精准钓鱼
一开始啥都不知道,应该多探索;后来经验丰富了,就主要用经验。
class EpsilonScheduler:
"""
ε调度器:控制探索率的变化
"""
def __init__(
self,
initial_epsilon: float = 1.0, # 一开始完全随机
final_epsilon: float = 0.01, # 最后保留1%探索
exploration_fraction: float = 0.1 # 用10%的训练时间从1降到0.01
):
self.initial_epsilon = initial_epsilon
self.final_epsilon = final_epsilon
self.exploration_fraction = exploration_fraction
def get_epsilon(self, current_step: int, total_steps: int) -> float:
"""
线性衰减:按训练进度从initial降到final
"""
fraction = min(current_step / (total_steps * self.exploration_fraction), 1.0)
return self.initial_epsilon + fraction * (self.final_epsilon - self.initial_epsilon)
class ExponentialEpsilonScheduler:
"""
指数衰减:每一步都乘一个小于1的数
"""
def __init__(
self,
initial_epsilon: float = 1.0,
final_epsilon: float = 0.01,
decay_steps: int = 10000
):
self.decay_rate = (final_epsilon / initial_epsilon) ** (1 / decay_steps)
self.initial_epsilon = initial_epsilon
def get_epsilon(self, current_step: int) -> float:
return max(self.initial_epsilon * (self.decay_rate ** current_step), 0.01)4.4 其他探索策略
Boltzmann探索:不用ε,而是看Q值的”温度”
def boltzmann_exploration(Q_values, temperature=1.0):
"""
温度高:所有动作概率差不多(高探索)
温度低:概率集中在高Q值动作(高利用)
"""
exp_values = np.exp(Q_values / temperature)
probs = exp_values / exp_values.sum()
return np.random.choice(len(Q_values), p=probs)UCB(Upper Confidence Bound):给访问少的动作加分
def ucb_action(Q_values, visit_counts, t, c=2.0):
"""
c: 探索常数,越大越鼓励探索
"""
ucb_values = Q_values + c * np.sqrt(np.log(t) / (visit_counts + 1))
return np.argmax(ucb_values)第四部分:完整可运行代码——从安装到训练到可视化
五、实战项目一:用Q学习走迷宫
5.1 自定义迷宫环境
"""
自定义迷宫环境
"""
import numpy as np
import gymnasium as gym
from gymnasium import spaces
class MazeEnv(gym.Env):
"""
自定义迷宫环境
地图:
S . . . #
. # . # .
. # . . .
. . . # .
# # . . G
S: 起点 (0,0)
G: 终点 (4,4)
#: 障碍物
.: 空地
"""
metadata = {'render_modes': ['human', 'ansi']}
def __init__(self, render_mode=None):
super().__init__()
self.render_mode = render_mode
# 地图:0=空地, 1=障碍, 2=起点, 3=终点
self.maze = np.array([
[2, 0, 0, 0, 1],
[0, 1, 0, 1, 0],
[0, 1, 0, 0, 0],
[0, 0, 0, 1, 0],
[1, 1, 0, 0, 3]
])
self.height, self.width = self.maze.shape
# 动作:0=上, 1=下, 2=左, 3=右
self.action_space = spaces.Discrete(4)
# 状态空间:位置 (y, x)
self.observation_space = spaces.Box(
low=0, high=max(self.height, self.width),
shape=(2,), dtype=np.int32
)
# 起点和终点
self.start_pos = np.argwhere(self.maze == 2)[0]
self.goal_pos = np.argwhere(self.maze == 3)[0]
# 初始状态
self.current_pos = self.start_pos.copy()
def reset(self, seed=None, options=None):
"""重置环境"""
super().reset(seed=seed)
self.current_pos = self.start_pos.copy()
return self.current_pos, {}
def step(self, action):
"""执行动作"""
# 动作转移动
moves = {
0: (-1, 0), # 上
1: (1, 0), # 下
2: (0, -1), # 左
3: (0, 1) # 右
}
dy, dx = moves[action]
new_pos = self.current_pos + np.array([dy, dx])
# 检查边界
if 0 <= new_pos[0] < self.height and 0 <= new_pos[1] < self.width:
# 检查障碍物
if self.maze[new_pos[0], new_pos[1]] != 1:
self.current_pos = new_pos
# 判断是否到达终点
at_goal = np.array_equal(self.current_pos, self.goal_pos)
# 计算奖励
if at_goal:
reward = 100
terminated = True
else:
# 每走一步扣1分,鼓励快速到达
reward = -1
terminated = False
truncated = False
info = {}
return self.current_pos.copy(), reward, terminated, truncated, info
def render(self):
"""渲染迷宫"""
if self.render_mode == 'ansi':
display = self.maze.copy().astype(str)
display[self.current_pos[0], self.current_pos[1]] = 'X'
display[self.maze == 1] = '#'
display[self.maze == 0] = '.'
display[self.maze == 2] = 'S'
display[self.maze == 3] = 'G'
print('\n'.join([' '.join(row) for row in display]))
print()
def get_state(self):
"""获取当前位置作为状态"""
return self.current_pos[0] * self.width + self.current_pos[1]
def state_to_pos(state, width):
"""状态转位置"""
return state // width, state % width
# ============ 完整可运行的Q学习走迷宫 ============
def train_maze_solver():
"""
训练Q学习智能体走迷宫
"""
env = MazeEnv(render_mode='ansi')
# 离散化状态空间:每个格子一个状态
n_states = env.height * env.width
n_actions = env.action_space.n
print(f"迷宫大小: {env.height}x{env.width}")
print(f"状态数: {n_states}, 动作数: {n_actions}")
print("动作: 0=上, 1=下, 2=左, 3=右")
print()
# Q表初始化
Q = np.zeros((n_states, n_actions))
# 超参数
alpha = 0.1 # 学习率
gamma = 0.95 # 折扣因子
epsilon = 1.0 # 初始探索率
epsilon_decay = 0.995
epsilon_min = 0.01
# 训练
num_episodes = 500
episode_rewards = []
steps_list = []
for episode in range(num_episodes):
state, _ = env.reset()
state = env.get_state()
total_reward = 0
steps = 0
done = False
while not done:
# ε-greedy选择动作
if np.random.random() < epsilon:
action = env.action_space.sample()
else:
action = np.argmax(Q[state])
# 执行
next_pos, reward, terminated, truncated, _ = env.step(action)
next_state = env.get_state()
done = terminated or truncated
# Q学习更新
Q[state, action] += alpha * (
reward + gamma * np.max(Q[next_state]) - Q[state, action]
)
state = next_state
total_reward += reward
steps += 1
episode_rewards.append(total_reward)
steps_list.append(steps)
epsilon = max(epsilon_min, epsilon * epsilon_decay)
if (episode + 1) % 100 == 0:
avg_reward = np.mean(episode_rewards[-100:])
avg_steps = np.mean(steps_list[-100:])
print(f"Episode {episode+1}: 平均奖励={avg_reward:.1f}, "
f"平均步数={avg_steps:.1f}, ε={epsilon:.3f}")
# 测试:让智能体走一次
print("\n" + "=" * 50)
print("测试:智能体走迷宫")
print("=" * 50)
state, _ = env.reset()
state = env.get_state()
env.render()
done = False
step = 0
while not done and step < 100:
action = np.argmax(Q[state])
next_pos, reward, terminated, truncated, _ = env.step(action)
next_state = env.get_state()
done = terminated or truncated
print(f"步骤{step+1}: 动作={action}, ", end="")
state = next_pos
state = env.get_state()
env.render()
step += 1
return Q, episode_rewards
if __name__ == "__main__":
Q, rewards = train_maze_solver()5.2 训练结果可视化
"""
绘制训练曲线和Q值热力图
"""
import matplotlib.pyplot as plt
import numpy as np
def visualize_results(rewards, Q, maze_shape=(5, 5)):
"""
可视化训练结果
"""
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 1. 训练奖励曲线
window = 50
if len(rewards) >= window:
smoothed = np.convolve(rewards, np.ones(window)/window, mode='valid')
axes[0].plot(smoothed, 'b-', alpha=0.8)
axes[0].set_xlabel('Episode')
axes[0].set_ylabel('Total Reward')
axes[0].set_title('Training Reward Curve')
axes[0].grid(True, alpha=0.3)
# 2. Q值热力图(每个状态的最高Q值)
max_q = np.max(Q, axis=1).reshape(maze_shape)
im = axes[1].imshow(max_q, cmap='YlOrRd')
axes[1].set_title('Max Q-Value per State')
axes[1].set_xlabel('X')
axes[1].set_ylabel('Y')
plt.colorbar(im, ax=axes[1], label='Max Q')
# 3. 最优动作可视化
best_actions = np.argmax(Q, axis=1).reshape(maze_shape)
action_arrows = ['↑', '↓', '←', '→']
action_chars = np.array([[action_arrows[a] for a in row]
for row in best_actions])
for i in range(maze_shape[0]):
for j in range(maze_shape[1]):
axes[2].text(j, i, action_chars[i, j],
ha='center', va='center', fontsize=14)
axes[2].set_xlim(-0.5, maze_shape[1] - 0.5)
axes[2].set_ylim(maze_shape[0] - 0.5, -0.5)
axes[2].set_title('Optimal Policy')
axes[2].set_xlabel('X')
axes[2].set_ylabel('Y')
axes[2].set_xticks([])
axes[2].set_yticks([])
plt.tight_layout()
plt.savefig('maze_results.png', dpi=150)
plt.show()六、实战项目二:用Q学习玩CartPole(倒立摆)
CartPole是控制论的经典问题:让小车上的杆子保持平衡。
"""
Q学习玩CartPole(连续状态空间的离散化处理)
"""
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
class DiscretizedQLearning:
"""
离散化Q学习:处理连续状态空间
核心思路:把连续空间分成很多小格子,每个格子当一个离散状态
"""
def __init__(self, n_bins=8):
"""
n_bins: 每个状态维度分成几个格子
"""
self.n_bins = n_bins
# CartPole的状态定义
# 0: 小车位置 (-2.4 ~ 2.4)
# 1: 小车速度 (-inf ~ inf)
# 2: 杆子角度 (-0.21 ~ 0.21 rad)
# 3: 杆子角速度 (-inf ~ inf)
self.state_bounds = [
[-2.4, 2.4], # 小车位置
[-3.0, 3.0], # 小车速度(截断)
[-0.21, 0.21], # 杆子角度
[-2.0, 2.0] # 杆子角速度(截断)
]
# Q表:4维离散状态 × 2个动作
shape = [n_bins] * 4 + [2]
self.Q = np.zeros(shape)
# 访问计数(用于计算学习率)
self.counts = np.zeros(shape)
def discretize(self, state):
"""
把连续状态转成离散索引
"""
indices = []
for i, (val, (low, high)) in enumerate(zip(state, self.state_bounds)):
# 归一化到[0, 1]
ratio = (val - low) / (high - low)
ratio = np.clip(ratio, 0, 1)
# 转成格子索引
index = int(ratio * (self.n_bins - 1))
indices.append(index)
return tuple(indices)
def choose_action(self, state):
"""ε-greedy动作选择"""
if np.random.random() < self.epsilon:
return np.random.randint(2)
return np.argmax(self.Q[self.discretize(state)])
def update(self, state, action, reward, next_state, done):
"""Q学习更新"""
s = self.discretize(state)
ns = self.discretize(next_state)
# 动态学习率:访问越多,学得越少
self.counts[s + (action,)] += 1
alpha = 1.0 / self.counts[s + (action,)]
if done:
target = reward
else:
target = reward + 0.99 * np.max(self.Q[ns])
td_error = target - self.Q[s + (action,)]
self.Q[s + (action,)] += alpha * td_error
# ε衰减
self.epsilon = max(0.01, self.epsilon * 0.995)
def train_cartpole():
"""训练CartPole"""
env = gym.make('CartPole-v1')
agent = DiscretizedQLearning(n_bins=8)
agent.epsilon = 1.0
num_episodes = 5000
episode_rewards = []
recent_rewards = []
print("开始训练CartPole...")
print("-" * 50)
for episode in range(num_episodes):
state, _ = env.reset()
total_reward = 0
done = False
while not done:
action = agent.choose_action(state)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
agent.update(state, action, reward, next_state, done)
state = next_state
total_reward += reward
episode_rewards.append(total_reward)
recent_rewards.append(total_reward)
if (episode + 1) % 500 == 0:
avg = np.mean(recent_rewards[-500:])
print(f"Episode {episode+1}: 最近500回合平均奖励 = {avg:.1f}")
env.close()
# 绘制结果
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
window = 100
smoothed = np.convolve(episode_rewards, np.ones(window)/window, mode='valid')
plt.plot(smoothed)
plt.xlabel('Episode')
plt.ylabel('Reward')
plt.title('Training Curve (Smoothed)')
plt.axhline(y=475, color='r', linestyle='--', label='Solved threshold')
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
plt.hist(episode_rewards[-500:], bins=30, edgecolor='black', alpha=0.7)
plt.xlabel('Episode Reward')
plt.ylabel('Count')
plt.title('Reward Distribution (Last 500 Episodes)')
plt.axvline(x=np.mean(episode_rewards[-500:]), color='r',
linestyle='--', label=f'Mean: {np.mean(episode_rewards[-500:]):.1f}')
plt.legend()
plt.tight_layout()
plt.savefig('cartpole_results.png', dpi=150)
plt.show()
# 测试:跑10回合看平均分
print("\n" + "=" * 50)
print("测试:跑10回合")
print("=" * 50)
test_env = gym.make('CartPole-v1')
for ep in range(10):
state, _ = test_env.reset()
total = 0
done = False
while not done:
action = agent.choose_action(state)
state, reward, terminated, truncated, _ = test_env.step(action)
done = terminated or truncated
total += reward
print(f"回合{ep+1}: 得分 = {total}")
test_env.close()
return agent, episode_rewards
if __name__ == "__main__":
agent, rewards = train_cartpole()第五部分:进阶内容
七、深度Q网络(DQN):当状态空间太大时
7.1 Q表解决不了的问题
FrozenLake只有16个状态,CartPole离散化后也只有8^4=4096个状态。
但如果是自动驾驶:输入是摄像头图像(640×480×3 = 92万个像素),每个像素取值0-255。
这种情况Q表根本没法用——没有足够的内存存储这么大的表,也没有足够的数据填满它。
7.2 用神经网络代替Q表
DQN的核心思想:用神经网络近似Q函数。
Q表:Q[state][action] = 值
DQN:Q_network(state) → [值1, 值2, 值3...] = 每个动作的值
PyTorch实现:
"""
DQN实现:用神经网络玩CartPole
"""
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
from collections import deque
import random
# ============ 第一部分:Q网络定义 ============
class QNetwork(nn.Module):
"""
简单的Q网络
输入:状态(4维向量)
输出:每个动作的Q值
"""
def __init__(self, state_dim=4, action_dim=2, hidden_dim=128):
super().__init__()
self.network = nn.Sequential(
nn.Linear(state_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, action_dim)
)
def forward(self, x):
"""前向传播"""
return self.network(x)
# ============ 第二部分:经验回放缓冲区 ============
class ReplayBuffer:
"""
经验回放:存储agent的经历,用于随机采样打破数据相关性
"""
def __init__(self, capacity=10000):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
"""存储一条经验"""
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
"""随机采样一批经验"""
batch = random.sample(self.buffer, batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
# 转成tensor
return (
torch.FloatTensor(np.array(states)),
torch.LongTensor(actions),
torch.FloatTensor(rewards),
torch.FloatTensor(np.array(next_states)),
torch.FloatTensor(dones)
)
def __len__(self):
return len(self.buffer)
# ============ 第三部分:DQN Agent ============
class DQNAgent:
"""
DQN智能体
和普通Q学习的区别:
1. 用神经网络代替Q表
2. 使用经验回放
3. 使用目标网络稳定训练
"""
def __init__(self, state_dim=4, action_dim=2):
self.state_dim = state_dim
self.action_dim = action_dim
# Q网络:用于选择动作
self.q_network = QNetwork(state_dim, action_dim)
# 目标网络:用于计算目标Q值
self.target_network = QNetwork(state_dim, action_dim)
self.target_network.load_state_dict(self.q_network.state_dict())
# 优化器
self.optimizer = optim.Adam(self.q_network.parameters(), lr=0.001)
# 经验回放
self.replay_buffer = ReplayBuffer(capacity=50000)
# 训练参数
self.gamma = 0.99 # 折扣因子
self.batch_size = 64 # 每次从buffer里取多少条经验
self.target_update_freq = 10 # 每多少步更新一次目标网络
self.train_step = 0
# 探索参数
self.epsilon = 1.0
self.epsilon_decay = 0.995
self.epsilon_min = 0.01
def choose_action(self, state):
"""ε-greedy动作选择"""
if np.random.random() < self.epsilon:
return np.random.randint(self.action_dim)
with torch.no_grad():
state_tensor = torch.FloatTensor(state).unsqueeze(0)
q_values = self.q_network(state_tensor)
return q_values.argmax().item()
def store_transition(self, state, action, reward, next_state, done):
"""存储经验"""
self.replay_buffer.push(state, action, reward, next_state, done)
def train(self):
"""训练一步"""
if len(self.replay_buffer) < self.batch_size:
return None
# 采样
states, actions, rewards, next_states, dones = \
self.replay_buffer.sample(self.batch_size)
# 当前Q值
current_q = self.q_network(states).gather(1, actions.unsqueeze(1)).squeeze()
# 目标Q值(用目标网络)
with torch.no_grad():
next_q = self.target_network(next_states).max(1)[0]
target_q = rewards + (1 - dones) * self.gamma * next_q
# 计算损失
loss = nn.MSELoss()(current_q, target_q)
# 反向传播
self.optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(self.q_network.parameters(), 1.0) # 梯度裁剪
self.optimizer.step()
# 更新目标网络
self.train_step += 1
if self.train_step % self.target_update_freq == 0:
self.target_network.load_state_dict(self.q_network.state_dict())
# ε衰减
self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
return loss.item()
# ============ 第四部分:训练主循环 ============
def train_dqn_cartpole():
"""训练DQN玩CartPole"""
env = gym.make('CartPole-v1')
agent = DQNAgent(state_dim=4, action_dim=2)
num_episodes = 500
episode_rewards = []
losses = []
print("开始训练DQN...")
print("-" * 50)
for episode in range(num_episodes):
state, _ = env.reset()
total_reward = 0
done = False
while not done:
# 选择动作
action = agent.choose_action(state)
# 执行
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
# 存储
agent.store_transition(state, action, reward, next_state, done)
# 训练
loss = agent.train()
if loss is not None:
losses.append(loss)
state = next_state
total_reward += reward
episode_rewards.append(total_reward)
if (episode + 1) % 50 == 0:
avg_reward = np.mean(episode_rewards[-50:])
avg_loss = np.mean(losses[-1000:]) if losses else 0
print(f"Episode {episode+1:4d} | "
f"奖励: {avg_reward:6.1f} | "
f"损失: {avg_loss:.4f} | "
f"ε: {agent.epsilon:.3f}")
env.close()
# 绘制结果
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
window = 20
smoothed = np.convolve(episode_rewards, np.ones(window)/window, mode='valid')
plt.plot(smoothed)
plt.xlabel('Episode')
plt.ylabel('Reward')
plt.title('DQN Training Curve')
plt.axhline(y=475, color='r', linestyle='--', label='Solved threshold')
plt.legend()
plt.grid(True, alpha=0.3)
plt.subplot(1, 2, 2)
if losses:
smoothed_loss = np.convolve(losses, np.ones(100)/100, mode='valid')
plt.plot(smoothed_loss)
plt.xlabel('Training Step')
plt.ylabel('Loss')
plt.title('Training Loss')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('dqn_results.png', dpi=150)
plt.show()
return agent, episode_rewards
if __name__ == "__main__":
agent, rewards = train_dqn_cartpole()7.3 DQN为什么work?
DQN有三个核心技巧:
-
经验回放(Experience Replay)
- 问题:连续的经验可能是相关的(比如一直往前走)
- 解决:从buffer里随机采样,打破相关性
-
目标网络(Target Network)
- 问题:用同一个网络计算当前Q和目标Q,会导致训练不稳定
- 解决:用一个”老”的网络算目标,定期更新
-
梯度裁剪(Gradient Clipping)
- 问题:梯度可能爆炸
- 解决:把梯度裁剪到[-1, 1]范围
八、调参实战:学习率、折扣因子、ε怎么调?
8.1 调参的基本思路
调参不是玄学,有一些基本规律:
| 参数 | 太大 | 太小 | 推荐范围 | 调节经验 |
|---|---|---|---|---|
| 学习率 α | 震荡不收敛 | 收敛太慢 | 0.001-0.1 | 常用0.01或0.001 |
| 折扣因子 γ | 可能过度考虑未来 | 只看眼前利益 | 0.9-0.999 | 任务越长,值越大 |
| 初始 ε | 探索不够 | 探索太多浪费 | 0.5-1.0 | 常用1.0 |
| ε衰减率 | 探索不充分 | 收敛太慢 | 0.99-0.9999 | 保证最后还有1%探索 |
8.2 实战调参建议
"""
调参经验总结
"""
TUNING_TIPS = {
"学习率": {
"问题": "Q值爆炸、振荡不收敛",
"检查": "梯度是否爆炸",
"解决": [
"降低学习率(×0.1)",
"使用学习率衰减",
"添加梯度裁剪",
"用Adam代替SGD"
]
},
"折扣因子": {
"问题": "短视行为 / 收敛慢",
"检查": "智能体是否只看眼前奖励",
"解决": [
"任务越长,γ越接近1",
"γ=0.9适合短任务",
"γ=0.99适合长任务(如Atari)",
"γ=0.999适合超长任务"
]
},
"探索率": {
"问题": "早熟收敛 / 探索不够",
"检查": "ε是否降得太快",
"解决": [
"增大衰减步数",
"增大最小ε(0.01→0.1)",
"使用线性衰减而非指数衰减",
"考虑Boltzmann探索"
]
},
"批量大小": {
"问题": "训练不稳定 / 收敛慢",
"检查": "batch内的TD误差方差",
"解决": [
"小batch(32):高方差但更新快",
"大batch(256):稳定但可能陷入局部最优",
"常用64或128",
"DQN建议至少32"
]
}
}8.3 推荐的起始参数
# 小型离散环境(FrozenLake, Taxi等)
default_params_small = {
"alpha": 0.1, # 表格型Q学习可以用较大的学习率
"gamma": 0.99,
"epsilon": 1.0,
"epsilon_decay": 0.999,
"epsilon_min": 0.01
}
# 连续环境 + 神经网络
default_params_dqn = {
"learning_rate": 0.001, # 神经网络用小一点
"gamma": 0.99,
"batch_size": 64,
"replay_buffer_size": 50000,
"target_update_freq": 10,
"epsilon": 1.0,
"epsilon_decay": 0.995,
"epsilon_min": 0.01
}九、Q学习的局限性:什么时候不该用它?
9.1 表格型Q学习的局限
只适用于离散、状态空间小的问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 状态空间爆炸 | 10个二值状态=1024种组合 | 函数逼近 |
| 连续动作空间 | Q表无法索引 | DDPG, SAC, TD3 |
| 部分可观测 | 看不全环境 | POMDP方法 |
| 延迟奖励 | TD误差传播慢 | 增加γ, 使用候选轨迹 |
9.2 函数逼近Q学习的局限
即使用了神经网络,Q学习也有根本性问题:
1. 不稳定甚至发散
- 理论:非线性函数逼近下的Q学习没有收敛保证
- 实践:可能学到完全错误的Q值
- 解决:DQN的双网络、梯度TD、ExpectedSARSA
2. Q值过估计
- 原理:max操作会系统性地高估Q值
- 表现:学到的策略可能不是最优
- 解决:Double DQN
3. 灾难性遗忘
- 原理:新经验覆盖旧经验
- 表现:学了新的忘了旧的
- 解决:经验回放、优先级回放
9.3 什么时候选其他方法?
| 场景 | 推荐方法 |
|---|---|
| 离散、小状态空间 | Q学习 ✓ |
| 高维连续输入(图像) | DQN, Dueling DQN, Rainbow |
| 连续动作空间 | DDPG, SAC, TD3 |
| 需要稳定训练 | PPO, A3C |
| 在线交互代价高 | 离线RL(CQL, IQL) |
| 需要学习随机策略 | 策略梯度(REINFORCE, PPO) |
9.4 Q学习 vs 策略梯度
| Q学习 | 策略梯度 | |
|---|---|---|
| 优点 | 样本效率高、收敛稳定 | 连续动作、自然随机策略 |
| 缺点 | 连续动作处理困难 | 高方差、需要大量样本 |
| 适用 | 离散、小空间 | 连续、大空间 |
十、常见问题排查
10.1 Q值不收敛
症状:Q值一直在波动,没有稳定趋势
检查清单:
-
学习率太大?
# 尝试:降低学习率 alpha = 0.001 # 从0.1降到0.001 # 或者:学习率衰减 alpha = initial_alpha / (1 + decay * episode) -
探索不够?
# 尝试:增大ε衰减步数 epsilon_decay = 0.9999 # 从0.999变得更慢 # 或者:增大最小ε epsilon_min = 0.1 # 从0.01增加到0.1 -
TD目标不稳定?
# DQN:检查目标网络是否正确更新 if step % target_update_freq == 0: target_network.load_state_dict(q_network.state_dict())
10.2 训练振荡
症状:奖励曲线上下剧烈波动
解决方案:
# 1. 降低学习率
agent.alpha *= 0.1
# 2. 增大经验回放缓冲区
buffer = ReplayBuffer(capacity=200000) # 从50000增大
# 3. 增大批量大小
batch_size = 128 # 从64增大到128
# 4. 添加梯度裁剪(DQN)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# 5. 减小γ
gamma = 0.95 # 从0.99减小10.3 早熟收敛
症状:很快收敛到一个次优策略,之后再也学不到更好的
诊断和解决:
# 1. 检查ε是否降太快
# 日志输出epsilon值
if episode % 100 == 0:
print(f"ε = {agent.epsilon:.4f}")
# 2. 增加探索时间
epsilon_decay = 0.9999 # 更慢的衰减
# 3. 使用Boltzmann探索替代ε-greedy
def boltzmann(Q, temperature=1.0):
exp_q = np.exp(Q / temperature)
probs = exp_q / exp_q.sum()
return np.random.choice(len(Q), p=probs)
# 4. 减少初始Q值(防止过早选择某个动作)
Q = np.random.uniform(-1, 0, (n_states, n_actions))10.4 Q值爆炸
症状:Q值变成inf或非常大的数
解决方案:
# 1. 检查奖励是否有异常值
print(f"奖励范围: min={rewards.min()}, max={rewards.max()}")
# 2. 归一化奖励
reward = (raw_reward - reward_mean) / reward_std
# 3. 限制Q值范围
Q = np.clip(Q, -100, 100)
# 4. 使用Huber损失代替MSE
loss = nn.SmoothL1Loss()(current_q, target_q)
# 5. 梯度裁剪
torch.nn.utils.clip_grad_norm_(parameters, max_norm=1.0)十一、进阶路线图:从Q学习到RainbowDQN
11.1 学习路径
第一阶段:基础(1-2周)
├── 理解MDP基础
├── 实现表格型Q学习
├── 实现ε-greedy
└── 在FrozenLake/Taxi上测试
第二阶段:深度Q学习(1-2周)
├── 实现DQN
├── 经验回放
├── 目标网络
└── 在CartPole上测试
第三阶段:DQN改进(1-2周)
├── Double DQN(解决过估计)
├── Dueling DQN(分离V和A)
├── Prioritized ER(优先级回放)
└── Noisy Networks(更好的探索)
第四阶段:现代方法(2-4周)
├── Rainbow DQN(集大成者)
├── DDPG(连续动作)
├── TD3(双延迟DDPG)
├── SAC(最大熵RL)
└── PPO(稳定策略梯度)
第五阶段:前沿与应用(持续)
├── 离线RL
├── 元学习
├── 多智能体RL
└── 模型预测控制
11.2 RainbowDQN:集大成者
Rainbow(2017)把7种DQN改进合在一起:
"""
RainbowDQN的组成部分
1. Double DQN:减少Q值过估计
2. Dueling DQN:分离状态价值和优势
3. Prioritized ER:智能采样重要经验
4. Multi-step bootstrap:TD(λ)思想
5. Distributional RL:预测奖励分布
6. Noisy Nets:可学习的探索
7. Categorical DQN:分布视角的DQN
"""11.3 各变体对比
| 变体 | 解决的问题 | 提升效果 |
|---|---|---|
| Double DQN | Q值过估计 | 稳定训练 |
| Dueling DQN | 学习效率 | 尤其状态多时有效 |
| Prioritized ER | 样本利用 | 2倍加速 |
| Noisy Nets | 探索效率 | 持续探索 |
| Distributional | 精确估计 | 更准确的值函数 |
第六部分:理论深化
十二、Bellman方程的直观理解
12.1 什么是Bellman方程?
Bellman方程(1957)是由动态规划大师Richard Bellman提出的。它把”长远利益”拆成两部分:
现在能拿到的 + 未来可能拿到的
12.2 为什么重要?
- 递归结构:把N步问题变成1步问题
- 最优子结构:最优策略由最优子策略组成
- 理论基础:Q学习、DQN都是这个方程的迭代求解
12.3 用例子理解
假设你在选择职业:
- 状态s:你的技能水平
- 动作a:选什么工作
- 奖励R:工资
- V(s):技能水平为s时,未来能赚多少钱
Bellman方程告诉你:
“选这份工作的价值 = 现在的工资 + 未来技能提升后能赚的钱”
12.4 压缩映射与收敛性
定义Bellman最优算子 :
核心性质: 是 -收缩映射
这意味着:反复应用 ,Q值必收敛到唯一不动点 。
十三、数学形式化总结
Q学习核心更新公式
等价形式(在线更新):
ε-greedy策略
TD误差
Bellman最优方程
Double DQN更新
Dueling DQN Q值分解
第七部分:参考与延伸
十四、相关文档
- MDP与Bellman方程 — Q学习的理论基础
- DQN深度指南 — Q学习的深度学习扩展
- 策略梯度方法 — 另一类强化学习方法
- PPO算法 — 现代策略优化方法
- RL应用场景 — Q学习的实际应用
十五、参考文献
-
Watkins, C. J. C. H. (1989). Learning from delayed rewards. PhD Thesis, Cambridge University.
-
Watkins, C. J., & Dayan, P. (1992). Q-learning. Machine Learning, 8(3-4), 279-292.
-
Sutton, R. S., & Barto, A. G. (2018). Reinforcement Learning: An Introduction (2nd ed.). MIT Press.
-
Mnih, V., et al. (2015). Human-level control through deep reinforcement learning. Nature, 518(7540), 529-533.
-
Mnih, V., et al. (2013). Playing Atari with deep reinforcement learning. arXiv:1312.5602.
-
Hasselt, H. V. (2010). Double Q-learning. Advances in Neural Information Processing Systems, 23.
-
Van Hasselt, H., Guez, A., & Silver, D. (2016). Deep reinforcement learning with double Q-learning. AAAI.
-
Wang, Z., et al. (2016). Dueling network architectures for deep reinforcement learning. ICML.
-
Schaul, T., et al. (2016). Prioritized experience replay. ICLR.
-
Fortunato, M., et al. (2018). Noisy networks for exploration. ICLR.
-
Hessel, M., et al. (2018). Rainbow: Combining improvements in deep reinforcement learning. AAAI.
-
Lillicrap, T. P., et al. (2016). Continuous control with deep reinforcement learning. ICLR.
-
Fujimoto, S., et al. (2018). Addressing function approximation error in actor-critic methods. ICML.
-
Haarnoja, T., et al. (2018). Soft actor-critic algorithms and applications. arXiv.
-
Schulman, J., et al. (2017). Proximal policy optimization algorithms. arXiv.
-
Kumar, A., et al. (2020). Conservative q-learning for offline reinforcement learning. NeurIPS.
Q学习是强化学习殿堂的入门钥匙——学会了它,你就能理解”智能”是怎么从”试错”中诞生的。从1989年Watkins的论文到今天的RainbowDQN,Q学习的思想一直在进化,但核心从未改变:让机器通过与环境交互,学会判断每个状态下哪个选择最值得。