强化学习算法系列(二):Model-Free类方法——蒙特卡洛算法(MC)和时序差分算法(TD)
强化学习算法
(一)动态规划方法——策略迭代算法(PI)和值迭代算法(VI)
(二)Model-Free类方法——蒙特卡洛算法(MC)和时序差分算法(TD)
(三)基于动作值的算法——Sarsa算法与Q-Learning算法
(四)深度强化学习时代的到来——DQN算法
(五)最主流的算法框架——AC算法(AC、A2C、A3C、SAC)
(六)应用最广泛的算法——PPO算法与TRPO算法
(七)更高级的算法——DDPG算法与TD3算法
(八)待续
文章目录
- 强化学习算法
- 前言
- 一、蒙特卡洛算法
- 1. 核心思想
- 2. 代码实战
- 3. 问题解释
- 二、时序差分算法
- 1. 核心思想
- 2. 代码实战
- 三、总结
前言
上一章学习的值迭代算法和策略迭代算法均要依赖已知环境模型,强化学习中的环境模型指能够显式使用的状态转移概率矩阵
P
(
s
′
∣
s
,
a
)
P(s′|s,a)
P(s′∣s,a)和奖励函数
R
(
s
,
a
)
R(s,a)
R(s,a)。在实际中,很多问题的状态变化概率是不确定,或者说是难以提前用准确的概率形式表达的。而奖励函数的设定在显示问题中更是难以直接得到。因此,我们需要在没有模型的情况下,找到问题的最优决策,这就需要我们本章要介绍的两大类算法——蒙特卡洛算法和时序差分算法。两种算法的思想均为“没有模型,就用数据”,以大量的测试数据
逼近真实模型。
一、蒙特卡洛算法
1. 核心思想
蒙特卡洛方法(Monte Carlo, MC)是一种免模型(Model-Free)学习算法,该算法通过与环境交互生成完整的轨迹(Episode),例如从起点到终点的全过程,并记录实际汇报进而更新价值函数,以此逼近真实的价值函数。这里我们介绍两种MC算法的实现方案
-
基于状态价值的方法
在一次交互轨迹中,我们可以直接通过累计奖励(Rerurn)的定义公式计算得到该轨迹的真实奖励
G t = R t + 1 + γ R t + 2 + γ 2 R t + 3 + . . . G_t = R_{t+1}+γR_{t+2}+γ^2R_{t+3}+... Gt=Rt+1+γRt+2+γ2Rt+3+...利用真实奖励更新值函数,直至值函数收敛
V ( s t ) ← V ( s t ) + α [ G t − V ( s t ) ] V(s_t)←V(s_t)+α[G_t-V(s_t)] V(st)←V(st)+α[Gt−V(st)]
该方法需要在算法运行阶段,不断的更新状态的价值函数 V ( s t ) V(s_t) V(st),需要额外计算才能得到策略。 -
基于动作价值的方法
我们先对动作价值函数做出定义:动作值函数 Q ( s , a ) Q(s,a) Q(s,a)表示在状态 s s s执行动作 a a a,并遵循策略 π \pi π的期望回报。蒙特卡洛方法可以通过轨迹采样直接估计 Q ( s , a ) Q(s,a) Q(s,a),即用历史回报的平均值更新 Q ( s , a ) Q(s,a) Q(s,a)
Q ( s , a ) ← E [ G t ∣ s , a ] Q(s,a)←E[G_t|s,a] Q(s,a)←E[Gt∣s,a]
该方法针对的是动作价值函数而非状态值函数,这样做就不需要显式的维护 V ( s ) V(s) V(s),直接将动作价值函数与动作选择关联,而基于状态价值函数的方法需要额外计算才能得到策略。在Model-Free中,直接维护 Q ( s , a ) Q(s,a) Q(s,a)更高效,避免了 V ( s ) V(s) V(s)到策略的转换步骤,若需要获取状态值函数,可以通过 V ( s ) = m a x a Q ( s , a ) V(s)=max_aQ(s,a) V(s)=maxaQ(s,a)计算,在蒙特卡洛算法的代码实现中多采用基于动作价值的方法。
2. 代码实战
同样在基于网格的迷宫问题中实现基于蒙特卡洛思想的代码实战,具体代码如下:
import numpy as np
import matplotlib.pyplot as plt
# 复用之前的网格世界定义
GRID_SIZE = 4
STATES = GRID_SIZE * GRID_SIZE
ACTIONS = 4 # 上(0)、右(1)、下(2)、左(3)
GOAL = (3, 3)
OBSTACLE = (1, 1)
ACTION_DELTA = [(-1, 0), (0, 1), (1, 0), (0, -1)]
# 构建环境模型(P和R)
def build_model():
P = np.zeros((STATES, ACTIONS, STATES))
R = np.full((STATES, ACTIONS), -1.0) # 默认每步奖励-1
for s in range(STATES):
x, y = s // GRID_SIZE, s % GRID_SIZE
if (x, y) == GOAL:
continue # 终点无动作
for a in range(ACTIONS):
dx, dy = ACTION_DELTA[a]
x_next = x + dx
y_next = y + dy
# 检查边界和障碍物
if x_next < 0 or x_next >= GRID_SIZE or y_next < 0 or y_next >= GRID_SIZE:
x_next, y_next = x, y
if (x_next, y_next) == OBSTACLE:
x_next, y_next = x, y
s_next = x_next * GRID_SIZE + y_next
P[s, a, s_next] = 1.0 # 确定性转移
# 到达终点的奖励为0
if (x_next, y_next) == GOAL:
R[s, a] = 0.0
return P, R
# 蒙特卡洛算法(首次访问,ε-贪婪策略)
def monte_carlo(P, R, gamma=0.9, epsilon=0.1, episodes=1000):
# 初始化动作值函数和策略
Q = np.zeros((STATES, ACTIONS)) # Q(s,a)
policy = np.random.randint(0, ACTIONS, size=STATES) # 初始随机策略
returns = {(s, a): [] for s in range(STATES) for a in range(ACTIONS)} # 存储回报
"""没有模型,就用数据;数据哪里来?测试获得轨迹数据"""
for _ in range(episodes):
# 生成轨迹(这里从起点(0,0)出发),也就是生成经验
trajectory = []
s = 0 # 起点 (0,0)
visited = set() # 记录首次访问的(s,a)
while True:
x, y = s // GRID_SIZE, s % GRID_SIZE
if (x, y) == GOAL:
break # 终止状态
# ε-贪婪策略选择动作
if np.random.rand() < epsilon:
a = np.random.randint(0, ACTIONS)
else:
a = policy[s]
# 执行动作,获取下一个状态和奖励
# TODO:问题①,Model-Free为什么仍然需要设定状态转移概率矩阵和奖励函数?
s_next = np.argmax(P[s, a]) # 确定性转移
reward = R[s, a]
# 记录(s,a,r)
trajectory.append((s, a, reward))
# 标记首次访问的(s,a)
if (s, a) not in visited:
visited.add((s, a))
s = s_next
# 计算累积回报(从后往前)
G = 0
# TODO:问题②:为什么从后往前计算真实奖励值?
for t in reversed(range(len(trajectory))):
s, a, r = trajectory[t]
G = gamma * G + r
# 仅更新首次访问的(s,a)
if (s, a) in visited:
returns[(s, a)].append(G)
# TODO:问题③:这里怎么更好的理解?
Q[s, a] = np.mean(returns[(s, a)]) # 用历史回报的平均值更新
visited.remove((s, a)) # 防止重复更新
# 策略改进(贪婪策略)
for s in range(STATES):
if s == GOAL[0] * GRID_SIZE + GOAL[1]:
continue
policy[s] = np.argmax(Q[s])
return Q, policy
def plot_value(V, title):
plt.figure(figsize=(8, 6))
grid = V.reshape((GRID_SIZE, GRID_SIZE))
# 绘制网格和状态值
plt.imshow(grid, cmap='viridis', origin='upper')
plt.colorbar()
plt.title(title)
# 标注特殊状态
for i in range(GRID_SIZE):
for j in range(GRID_SIZE):
if (i, j) == GOAL:
plt.text(j, i, 'GOAL', ha='center', va='center', color='white')
elif (i, j) == OBSTACLE:
plt.text(j, i, 'BLOCK', ha='center', va='center', color='white')
else:
plt.text(j, i, f'{V[i * GRID_SIZE + j]:.1f}', ha='center', va='center', color='white')
plt.xticks([])
plt.yticks([])
plt.show()
# 可视化策略(与之前代码复用)
def plot_policy(policy, title):
plt.figure(figsize=(8, 6))
action_symbol = ['↑', '→', '↓', '←']
for i in range(GRID_SIZE):
for j in range(GRID_SIZE):
s = i * GRID_SIZE + j
if (i, j) in [GOAL, OBSTACLE]:
plt.text(j, i, 'GOAL' if (i, j) == GOAL else 'BLOCK', ha='center', va='center')
else:
plt.text(j, i, action_symbol[policy[s]], ha='center', va='center', fontsize=20)
plt.xlim(-0.5, GRID_SIZE - 0.5)
plt.ylim(-0.5, GRID_SIZE - 0.5)
plt.gca().invert_yaxis()
plt.title(title)
plt.grid(True)
plt.show()
if __name__ == '__main__':
P, R = build_model()
# 运行蒙特卡洛算法
Q_mc, policy_mc = monte_carlo(P, R, gamma=0.9, epsilon=0.1, episodes=10000)
# 这里的np.max(Q_mc, axis=1)其实就是最优策略下贝尔曼方程求得的状态价值
plot_value(np.max(Q_mc, axis=1), "Monte Carlo - Optimal State Values")
plot_policy(policy_mc, "Monte Carlo - Optimal Policy")
运行结果:


3. 问题解释
这里解释几个代码中可能会产生疑惑的问题(已在代码中标注TODO),以进一步加深理解:
-
问题①:Model-Free为什么仍然需要设定状态转移概率矩阵和奖励函数?
免模型表示算法在更新策略或值函数时,不依赖环境模型(即转移概率 P ( s ′ ∣ s , a ) P(s'|s,a) P(s′∣s,a)和奖励函数 R ( s , a ) R(s,a) R(s,a)的显式知识)。具体来说:
动态规划(DP)(如值迭代、策略迭代)需要显式使用 P ( s ′ ∣ s , a ) P(s'|s,a) P(s′∣s,a)和 R ( s , a ) R(s,a) R(s,a)来计算期望值
V ( s ) ← ∑ a π ( a ∣ s ) [ R ( s , a ) + γ ∑ s ′ P ( s ′ ∣ s , a ) V ( s ′ ) ] V(s)← \sum_{a}π(a∣s)[R(s,a)+γ\sum_{s'}P(s′∣s,a)V(s')] V(s)←a∑π(a∣s)[R(s,a)+γs′∑P(s′∣s,a)V(s′)] Moder-Free方法通过直接与环境交互(或模拟交互)获取经验数据(状态、动作、奖励序列),并基于实际观测的回报更新值函数。
代码中的P和R,相当于模拟环境的反馈(模拟黑盒环境),即动作执行后转向哪里和获得了多少奖励,在下面状态价值或动作价值计算中并未使用P和R。 -
问题②:为什么从后往前计算真实奖励值?
蒙特卡洛的核心是计算从某一步开始的累计真实回报 G t G_t Gt,即
G t = R t + 1 + γ R t + 2 + γ 3 R t + 3 + . . . G_t = R_{t+1}+γR_{t+2}+γ^3R_{t+3}+... Gt=Rt+1+γRt+2+γ3Rt+3+...从后往前计算的本质是为了高效复用已计算的 G t + 1 G_{t+1} Gt+1,如果按顺序从前往后算,每一步的 G t G_t Gt都需要重新求从 R t + 1 R_{t+1} Rt+1到结束终点的所有奖励和;而从后往前计算,可以复用前一步已计算的 G t + 1 G_{t+1} Gt+1。直接通过公式计算
G t = R t + 1 + γ G t + 1 G_t=R_{t+1}+γG_{t+1} Gt=Rt+1+γGt+1这样便能在每一步只进行一次加法和一次乘法,极大减少计算量。 -
问题③:这里怎么更好的理解?
此处可以类比为策略迭代算法中的策略评估阶段,只不过此处是通过测试轨迹采样得到的动作价值 Q ( s , a ) Q(s,a) Q(s,a),而策略迭代算法是根据已知模型得到的状态价值 V ( s ) V(s) V(s),后面的策略改进方案也可用类似方法类比理解。
二、时序差分算法
1. 核心思想
时序差分算法(Temporal Difference,TD)与蒙特卡洛方法一样是一种不依赖模型环境的算法,该算法结合当前估计的状态价值与单步得到的奖励进行更新,而无需等待完整轨迹。其核心公式可以表示为
V
(
s
t
)
←
V
(
s
t
)
+
α
[
R
t
+
1
+
γ
V
(
s
t
+
1
)
−
V
(
s
t
)
]
V(s_t)←V(s_t)+α[R_{t+1}+γV(s_{t+1})-V(s_t)]
V(st)←V(st)+α[Rt+1+γV(st+1)−V(st)]我们可以将公式右侧拆开理解,
V
(
s
t
)
V(s_t)
V(st)为要更新的状态价值,
α
[
R
t
+
1
+
γ
V
(
s
t
+
1
)
−
V
(
s
t
)
]
α[R_{t+1}+γV(s_{t+1})-V(s_t)]
α[Rt+1+γV(st+1)−V(st)],其中
α
α
α表示学习率,
R
t
+
1
R_{t+1}
Rt+1表示此次单步推进得到的奖励值,我们将
R
t
+
1
+
γ
V
(
s
t
+
1
)
R_{t+1}+γV(s_{t+1})
Rt+1+γV(st+1)两项合称为单步目标值(TD target),此处有证明如下。进而
[
R
t
+
1
+
γ
V
(
s
t
+
1
)
−
V
(
s
t
)
]
[R_{t+1}+γV(s_{t+1})-V(s_t)]
[Rt+1+γV(st+1)−V(st)]就能够表示为当前状态价值与目标状态价值的差距,是一种从经验中获取到的信息。因此只要我们能够不断迭代地从环境中测试获取信息,就能逼近真实的状态价值。
具体到算法执行步骤:首先随机初始化
V
(
s
)
V(s)
V(s),接着交互采样,从状态
s
t
s_t
st执行动作
A
t
A_t
At,观察得到的
R
t
+
1
R_{t+1}
Rt+1和
s
t
+
1
s_{t+1}
st+1,然后利用上述公式更新
V
(
s
t
)
V(s_t)
V(st),重复上述步骤直至达到终止状态。
关于TD target的证明,即解释为什么 R t + 1 + γ V ( s t + 1 ) R_{t+1}+γV(s_{t+1}) Rt+1+γV(st+1)可以作为TD target?
一句话可以说明白,就是TD target中包含了此次单步测试得到的真实回应,我们要向真实靠近,下面我们以公式的角度看一下TD算法是怎么更新的。
首先,我们将TD算法公式改写并化简,步骤如下:
V ( s t ) ← V ( s t ) + α [ R t + 1 + γ V ( s t + 1 ) − V ( s t ) ] V(s_t)←V(s_t)+α[R_{t+1}+γV(s_{t+1})-V(s_t)] V(st)←V(st)+α[Rt+1+γV(st+1)−V(st)]将 R t + 1 + γ V ( s t + 1 ) R_{t+1}+γV(s_{t+1}) Rt+1+γV(st+1)记作 V ˉ ( s t ) \bar{V}(s_t) Vˉ(st),并在式子两边同时减去 V ˉ ( s t ) \bar{V}(s_t) Vˉ(st),得到如下式子
V ( s t ) − V ˉ ( s t ) ← V ( s t ) − V ˉ ( s t ) + α [ V ˉ ( s t ) − V ( s t ) ] V(s_t)-\bar{V}(s_t)←V(s_t)-\bar{V}(s_t)+α[\bar{V}(s_t)-V(s_t)] V(st)−Vˉ(st)←V(st)−Vˉ(st)+α[Vˉ(st)−V(st)]整合右边公式得到 V ( s t ) − V ˉ ( s t ) ← [ 1 − α ] ⋅ [ V ( s t ) − V ˉ ( s t ) ] V(s_t)-\bar{V}(s_t)←[1-α]\cdot [V(s_t)-\bar{V}(s_t)] V(st)−Vˉ(st)←[1−α]⋅[V(st)−Vˉ(st)]对两边同时取绝对值可以得到
∣ V ( s t ) − V ˉ ( s t ) ∣ ← ∣ 1 − α ∣ ⋅ ∣ V ( s t ) − V ˉ ( s t ) ∣ |V(s_t)-\bar{V}(s_t)|←|1-α|\cdot|V(s_t)-\bar{V}(s_t)| ∣V(st)−Vˉ(st)∣←∣1−α∣⋅∣V(st)−Vˉ(st)∣其中, α α α是一个大于0小于1的常数,因此右边的值小于左边的,这就代表我们在进行一次单步更新后能够更加靠近TD target。
2. 代码实战
与之前环境问题一致,TD算法的实现代码如下:
import numpy as np
import matplotlib.pyplot as plt
# 复用之前的网格世界定义
GRID_SIZE = 4
STATES = GRID_SIZE * GRID_SIZE
ACTIONS = 4 # 上(0)、右(1)、下(2)、左(3)
GOAL = (3, 3)
OBSTACLE = (1, 1)
ACTION_DELTA = [(-1, 0), (0, 1), (1, 0), (0, -1)]
def build_model():
"""构建网格世界的环境模型(状态转移矩阵P和奖励函数R)"""
P = np.zeros((STATES, ACTIONS, STATES))
R = np.full((STATES, ACTIONS), -1.0) # 默认每步奖励-1
for s in range(STATES):
x, y = s // GRID_SIZE, s % GRID_SIZE
if (x, y) == GOAL:
continue # 终点无动作
for a in range(ACTIONS):
dx, dy = ACTION_DELTA[a]
x_next = x + dx
y_next = y + dy
# 检查边界和障碍物
if x_next < 0 or x_next >= GRID_SIZE or y_next < 0 or y_next >= GRID_SIZE:
x_next, y_next = x, y
if (x_next, y_next) == OBSTACLE:
x_next, y_next = x, y
s_next = x_next * GRID_SIZE + y_next
P[s, a, s_next] = 1.0 # 确定性转移
# 到达终点的奖励为0
if (x_next, y_next) == GOAL:
R[s, a] = 0.0
return P, R
def td0_learning(P, R, gamma=0.9, alpha=0.1, epsilon=0.1, episodes=10000):
"""TD(0)算法实现(更新状态价值函数V)"""
V = np.zeros(STATES) # 初始化状态价值函数
policy = np.random.randint(0, ACTIONS, size=STATES) # 初始随机策略
for _ in range(episodes):
s = 0 # 起点 (0,0)
while True:
x, y = s // GRID_SIZE, s % GRID_SIZE
if (x, y) == GOAL:
break # 终止状态
# ε-贪婪策略选择动作
if np.random.rand() < epsilon:
a = np.random.randint(0, ACTIONS)
else:
a = policy[s]
# 执行动作,获取下一状态和奖励
s_next = np.argmax(P[s, a]) # 确定性转移
reward = R[s, a]
# TD(0)更新公式:V(s) ← V(s) + α [R + γV(s') - V(s)]
td_target = reward + gamma * V[s_next]
V[s] += alpha * (td_target - V[s])
# 策略改进(贪婪策略,基于当前V)
q_values = []
for a_candidate in range(ACTIONS):
s_next_candidate = np.argmax(P[s, a_candidate])
q_value = R[s, a_candidate] + gamma * V[s_next_candidate]
q_values.append(q_value)
policy[s] = np.argmax(q_values)
# 转移到下一状态
s = s_next
return V, policy
def plot_value(V, title):
plt.figure(figsize=(8, 6))
grid = V.reshape((GRID_SIZE, GRID_SIZE))
# 绘制网格和状态值
plt.imshow(grid, cmap='viridis', origin='upper')
plt.colorbar()
plt.title(title)
# 标注特殊状态
for i in range(GRID_SIZE):
for j in range(GRID_SIZE):
if (i, j) == GOAL:
plt.text(j, i, 'GOAL', ha='center', va='center', color='white')
elif (i, j) == OBSTACLE:
plt.text(j, i, 'BLOCK', ha='center', va='center', color='white')
else:
plt.text(j, i, f'{V[i * GRID_SIZE + j]:.1f}', ha='center', va='center', color='white')
plt.xticks([])
plt.yticks([])
plt.show()
# 复用之前的可视化函数
def plot_policy(policy, title):
plt.figure(figsize=(8, 6))
action_symbol = ['↑', '→', '↓', '←']
for i in range(GRID_SIZE):
for j in range(GRID_SIZE):
s = i * GRID_SIZE + j
if (i, j) in [GOAL, OBSTACLE]:
plt.text(j, i, 'GOAL' if (i, j) == GOAL else 'BLOCK', ha='center', va='center')
else:
plt.text(j, i, action_symbol[policy[s]], ha='center', va='center', fontsize=20)
plt.xlim(-0.5, GRID_SIZE - 0.5)
plt.ylim(-0.5, GRID_SIZE - 0.5)
plt.gca().invert_yaxis()
plt.title(title)
plt.grid(True)
plt.show()
if __name__ == '__main__':
P, R = build_model()
# 运行TD(0)算法
V_td0, policy_td0 = td0_learning(P, R, gamma=0.9, alpha=0.1, epsilon=0.1, episodes=10000)
plot_value(V_td0, "Value Iteration - Optimal State Values")
plot_policy(policy_td0, "TD(0) - Optimal Policy")
运行结果:


从结果可以看出,在本章设定的环境中,TD算法的性能还是要明显优于MC算法的。
三、总结
凭直觉不难发现MC算法和TD算法其实是两个方向的极端,MC算法每次更新是利用一条测试轨迹的所有数据,而TD算法则每次利用测试轨迹的单步轨迹。若将两种算法折中,就得到了TD(λ)算法,感兴趣可以了解一下。下表对比了两种算法的特点:
特性 | 蒙特卡洛(TD) | 时序差分(TD) |
---|---|---|
更新依据 | 完整轨迹的实际回报 G t G_t Gt | 单步奖励 + 下一状态的估计值 R t + 1 + γ V ( S t + 1 ) R_{t+1}+γV(S_{t+1}) Rt+1+γV(St+1) |
更新时机 | 回合结束后更新(离线学习) | 单步交互后更新(在线更新) |
偏差与方差 | 无偏估计,高方差 | 有偏估计,低方差 |
更新时机 | 回合结束后更新(离线学习) | 单步交互后更新(在线更新) |
适用场景 | 回合制任务(如游戏,棋类) | 连续任务(如机器人控制、实时决策) |