从零开始理解贝尔曼方程:强化学习中的价值函数推导与实战示例

如果你刚开始接触强化学习,可能会被那些复杂的数学公式和抽象概念搞得晕头转向。别担心,这很正常。强化学习的核心思想其实很直观:一个智能体(Agent)在环境(Environment)中通过试错来学习,目标是最大化长期累积的奖励。但如何量化这个“长期累积的奖励”?如何评估在某个状态下采取某个动作的“好坏”?这就是价值函数(Value Function)要解决的问题。而贝尔曼方程(Bellman Equation),正是连接当前价值与未来价值、将看似遥不可及的长期目标分解为可计算步骤的关键桥梁。它不仅是理解Q-learning、策略梯度等现代算法的基石,更是我们设计高效智能体的理论核心。

本文将彻底抛开晦涩的纯理论推导,带你从零开始,用代码和直观的网格世界(GridWorld)示例,亲手“搭建”出贝尔曼方程。我们会看到,这个看似复杂的方程,本质上是一个优雅的递归关系:一个状态的价值,等于即时奖励加上所有可能未来状态的折现价值。我们将通过Python和Gym库,一步步实现它,并观察不同折扣因子(γ)如何像“远见眼镜”一样,深刻改变智能体的决策策略。无论你是希望夯实理论基础的研究者,还是渴望快速上手的工程师,这篇文章都将为你提供一条清晰、可操作的路径。

1. 价值函数:为未来收益定价

在深入方程之前,我们必须先理解它要计算的对象——价值函数。想象一下,你站在一个迷宫的某个格子里。价值函数要回答的问题是:“从我这个格子出发,遵循一套既定的行动规则(策略),平均能获得多少总分?” 这个“总分”,就是回报(Return),它是未来所有奖励的加权和。

1.1 回报与折扣因子

回报 G_t 的定义很简单:从时刻 t 开始,把所有未来奖励加起来。但这里有个关键问题:明天的100块和今天的100块,价值一样吗?在经济学和强化学习中,我们通常认为即时奖励比未来奖励更有价值。因此,我们引入一个折扣因子 γ(Gamma),它是一个介于0和1之间的数。

# 一个简单的回报计算示例
rewards = [1, 2, 3, 4, 0]  # 从t时刻起,未来5个时间步的奖励
gamma = 0.9
G_t = 0
for i, r in enumerate(rewards):
    G_t += (gamma ** i) * r
print(f"折扣回报 G_t: {G_t:.2f}")
# 输出:G_t = 1*1 + 0.9*2 + 0.81*3 + 0.729*4 + 0.6561*0 ≈ 8.24

如果 γ=0,智能体完全“短视”,只关心下一步的即时奖励。如果 γ 接近1,智能体变得非常有“远见”,几乎平等地看待近期和远期的奖励。γ 的选择直接决定了智能体的“性格”,我们会在后面的实战中看到它的巨大影响。

1.2 状态价值函数 V(s)

回报 G_t 依赖于一条具体的行动轨迹,而轨迹有随机性(比如环境本身有随机性,或者策略是随机的)。因此,我们更关心它的期望值(平均值)。状态价值函数 V(s) 正是这个期望值:

V(s) = E[ G_t | 当前状态是 s ]

它衡量了从状态s出发,长期来看的平均收益V(s) 越高,说明状态s“越好”。但这里有个循环依赖:要计算 V(s),我需要知道从s出发后所有可能未来的价值,而那些未来的价值本身又依赖于更远的未来……这似乎是个死循环。

贝尔曼方程的绝妙之处,就在于它巧妙地打破了这个循环。它告诉我们,V(s) 可以通过其后继状态的价值 V(s') 来递归地表示,从而将一个无限求和问题,转化为一个可求解的(线性)方程。

2. 贝尔曼方程的直观拆解

让我们暂时忘掉复杂的求和符号。贝尔曼期望方程的核心思想可以概括为一句话:

当前状态的价值 = 平均即时奖励 + γ * 平均未来状态的价值

更具体地,对于给定的策略 π,其状态价值函数的贝尔曼方程如下:

V(s) = Σ_a π(a|s) * Σ_{s', r} p(s', r | s, a) * [ r + γ * V(s') ]

这个公式虽然紧凑,但包含了所有关键信息。我们来逐一拆解:

  • π(a|s):在状态s下,策略π选择动作a的概率。这体现了策略的随机性。
  • p(s', r | s, a):环境模型。表示在状态s执行动作a后,转移到状态s'并获得奖励r的概率。这描述了环境的动态特性。
  • r:执行动作a后获得的即时奖励。
  • γ * V(s'):下一个状态s'的价值的折现值。γ 决定了我们有多重视未来。
  • Σ_{s', r}:对所有可能的下一个状态和奖励求和,计算期望。
  • Σ_a:对所有可能的动作求和,计算在策略π下的期望。

这个方程建立了一个所有状态价值之间的关联网络。每个状态的价值都依赖于其可能的后继状态的价值。为了更清晰地展示这种关系,我们来看一个状态转移的抽象表示:

当前状态 (s)可选动作 (a)概率 π(a|s)可能结果 (s', r)转移概率 p(s', r|s, a)贡献部分 [r + γ*V(s')]
状态A向上0.8(状态B, +5)0.75 + γ*V(B)
(状态A, -1)0.3-1 + γ*V(A)
向左0.2(状态C, 0)1.00 + γ*V(C)

V(A) 的值,就是最后两列所有可能性的加权平均(先按动作概率加权,再按转移概率加权)。

注意:这个方程是“期望方程”,因为它基于一个固定的策略π来计算价值。还有另一个更强大的变体——贝尔曼最优方程,它直接寻找能使价值最大化的最优策略,其形式是将对动作的求和 Σ_a π(a|s) 替换为对动作取最大值 max_a。我们稍后会触及。

3. 实战演练:在网格世界中实现贝尔曼方程

理论说得再多,不如一行代码。让我们用经典的 GridWorld 环境来具象化这一切。假设一个4x4的网格,智能体从左上角(0,0)出发,目标是到达右下角(3,3)的终点,到达终点获得+1奖励并结束。如果撞到边界,则停留在原地并获得-0.1奖励。其他移动奖励为0。

3.1 环境设置与策略定义

首先,我们定义一个简单的确定性策略:总是试图向右或向下移动(如果可能),以尽快到达终点。

import numpy as np

# 定义网格世界参数
GRID_SIZE = 4
ACTIONS = ['UP', 'DOWN', 'LEFT', 'RIGHT']
ACTION_MAP = {'UP': (-1, 0), 'DOWN': (1, 0), 'LEFT': (0, -1), 'RIGHT': (0, 1)}
TERMINAL_STATE = (3, 3)

# 初始化状态价值表 V(s), 全零
V = np.zeros((GRID_SIZE, GRID_SIZE))

# 定义一个简单的确定性策略:在非终点的状态,优先向右,其次向下
def simple_policy(state):
    if state == TERMINAL_STATE:
        return None  # 终止状态无动作
    row, col = state
    if col < GRID_SIZE - 1:  # 可以向右
        return 'RIGHT'
    elif row < GRID_SIZE - 1: # 可以向下
        return 'DOWN'
    else:
        # 在最右下方但不是终点的格子(本例中不存在),随机选一个
        return np.random.choice(['UP', 'LEFT'])

# 环境动态函数:给定状态和动作,返回(下一个状态,奖励,是否终止)
def step(state, action):
    if state == TERMINAL_STATE:
        return state, 0, True
    row, col = state
    drow, dcol = ACTION_MAP[action]
    new_row, new_col = row + drow, col + dcol
    # 检查边界
    if 0 <= new_row < GRID_SIZE and 0 <= new_col < GRID_SIZE:
        next_state = (new_row, new_col)
    else:
        next_state = state  # 撞墙,留在原地
        reward = -0.1       # 撞墙惩罚
        return next_state, reward, False
    # 判断是否到达终点
    if next_state == TERMINAL_STATE:
        reward = 1.0
        done = True
    else:
        reward = 0.0
        done = False
    return next_state, reward, done

3.2 实现策略评估(迭代法求解贝尔曼方程)

我们无法直接解那个巨大的方程组,但可以利用贝尔曼方程本身进行迭代。这种方法称为策略评估(Policy Evaluation)。其核心思想是:不断用方程右边(基于当前价值估计的更新值)来更新方程左边的价值估计,直到收敛。

def policy_evaluation(policy, V, gamma=0.9, theta=1e-4, max_iter=1000):
    """
    迭代策略评估
    policy: 函数,输入状态返回动作
    V: 当前价值估计矩阵
    gamma: 折扣因子
    theta: 收敛阈值
    max_iter: 最大迭代次数
    返回:收敛后的价值函数 V
    """
    for i in range(max_iter):
        delta = 0
        # 遍历所有状态
        for row in range(GRID_SIZE):
            for col in range(GRID_SIZE):
                state = (row, col)
                if state == TERMINAL_STATE:
                    V[state] = 0  # 终止状态价值通常定义为0
                    continue
                # 根据当前策略选择动作(本例是确定性策略)
                action = policy(state)
                # 计算该动作下的期望价值(对于确定性环境,就是唯一结果)
                next_state, reward, done = step(state, action)
                # 贝尔曼更新
                new_v = reward + gamma * V[next_state]
                # 记录本次迭代中所有状态价值变化的最大值
                delta = max(delta, abs(new_v - V[state]))
                V[state] = new_v
        # 如果价值变化很小,认为已收敛
        if delta < theta:
            print(f"策略评估在 {i+1} 次迭代后收敛。")
            break
    return V

# 执行策略评估
gamma = 0.9
V_evaluated = policy_evaluation(simple_policy, V.copy(), gamma=gamma)
print("评估后的状态价值表 (V(s)):")
print(np.round(V_evaluated, 3))

运行这段代码,你会得到一个4x4的价值表。靠近终点的格子价值较高,因为从那里出发很快就能获得+1奖励。左上角的价值虽然为正,但因为距离远,经过γ的多次折现,价值会低很多。这就是贝尔曼方程在起作用:它通过递归的折现,将终点的奖励“传播”到了整个网格。

3.3 探索折扣因子 γ 的魔力

现在,让我们看看改变“远见眼镜”的度数(γ)会带来什么影响。我们将分别用 γ=0.1(极度短视)、γ=0.5(中等眼光)和 γ=0.99(极具远见)来评估同一个策略。

def evaluate_with_gamma(gamma_value):
    V_temp = np.zeros((GRID_SIZE, GRID_SIZE))
    V_result = policy_evaluation(simple_policy, V_temp, gamma=gamma_value, theta=1e-6)
    return V_result

gammas = [0.1, 0.5, 0.99]
results = {}
for g in gammas:
    print(f"\n=== 折扣因子 γ = {g} ===")
    V_g = evaluate_with_gamma(g)
    results[g] = V_g
    print(np.round(V_g, 3))

观察输出,你会发现:

  • γ = 0.1:价值衰减极快。只有紧挨着终点的格子有显著的正价值(因为下一步就能拿到奖励)。智能体几乎只关心下一步的即时奖励,不会为了长远目标进行规划。
  • γ = 0.5:价值的传播范围更广一些,但衰减仍然明显。距离终点两步的格子,其价值大约是终点奖励的 0.5^2 = 0.25倍。
  • γ = 0.99:价值衰减非常缓慢。即使是从最远的左上角出发,其价值也相对较高(大约0.9),因为智能体几乎同等地看待所有未来的奖励。它非常愿意为了最终的大奖励而忍受前期较长的无奖励移动。

提示:在实际项目中,γ 是一个至关重要的超参数。对于回合制任务(如棋类游戏,有明确结束),γ 常设为1或接近1。对于持续任务(如机器人平衡),γ 必须小于1以保证回报有限,其具体值需要通过实验调整,以平衡短期收益与长期规划。

4. 从策略评估到策略改进:动态规划循环

仅仅评估一个给定策略的价值还不够,我们的目标是找到最优策略。这就是策略迭代(Policy Iteration) 的用武之地。它包含两个交替进行的步骤:

  1. 策略评估(Policy Evaluation):如上所述,计算当前策略π下的价值函数 V_π
  2. 策略改进(Policy Improvement):基于评估出的价值函数 V_π,在每个状态贪婪地(greedily) 选择能带来最高期望价值的动作,从而生成一个更好的新策略 π‘。

策略改进的依据是动作价值函数 Q(s, a),它衡量在状态s下采取特定动作a,然后遵循策略π所能获得的期望回报。其贝尔曼方程与状态价值类似:

Q(s, a) = Σ_{s', r} p(s', r | s, a) * [ r + γ * V_π(s') ]

注意,这里 V_π(s') 是已知的(从上一步策略评估得到)。策略改进就是为每个状态s选择使得 Q(s, a) 最大的动作a:

π'(s) = argmax_a Q(s, a)

让我们在网格世界上实现一个简单的策略迭代演示。为了增加探索性,我们假设环境有10%的概率会执行一个随机动作(而不是指令动作)。

def policy_iteration(gamma=0.9, theta=1e-4):
    """简单的策略迭代算法"""
    # 初始化:随机策略和零价值函数
    policy = {}
    V = np.zeros((GRID_SIZE, GRID_SIZE))
    # 为每个状态随机初始化一个动作
    for row in range(GRID_SIZE):
        for col in range(GRID_SIZE):
            state = (row, col)
            if state != TERMINAL_STATE:
                policy[state] = np.random.choice(ACTIONS)
            else:
                policy[state] = None

    policy_stable = False
    iteration = 0

    while not policy_stable:
        iteration += 1
        # --- 策略评估 (简化版:只进行少量迭代) ---
        for _ in range(10): # 不完全收敛,近似评估
            delta = 0
            for row in range(GRID_SIZE):
                for col in range(GRID_SIZE):
                    state = (row, col)
                    if state == TERMINAL_STATE:
                        continue
                    action = policy[state]
                    # 考虑环境随机性:10%概率执行随机动作
                    if np.random.random() < 0.1:
                        actual_action = np.random.choice(ACTIONS)
                    else:
                        actual_action = action
                    next_state, reward, _ = step(state, actual_action)
                    # 简化计算,忽略随机性期望,用单次采样近似
                    new_v = reward + gamma * V[next_state]
                    delta = max(delta, abs(new_v - V[state]))
                    V[state] = new_v
            if delta < theta:
                break

        # --- 策略改进 ---
        policy_stable = True
        for row in range(GRID_SIZE):
            for col in range(GRID_SIZE):
                state = (row, col)
                if state == TERMINAL_STATE:
                    continue
                old_action = policy[state]
                # 计算每个动作的Q值(简化,用确定性近似)
                q_values = {}
                for a in ACTIONS:
                    next_state, reward, _ = step(state, a)
                    q_values[a] = reward + gamma * V[next_state]
                # 选择Q值最大的动作
                best_action = max(q_values, key=q_values.get)
                policy[state] = best_action
                if old_action != best_action:
                    policy_stable = False
        print(f"迭代 {iteration} 完成,策略是否稳定: {policy_stable}")

    print(f"\n策略迭代在 {iteration} 次迭代后收敛。")
    return policy, V

# 运行策略迭代
optimal_policy, optimal_V = policy_iteration(gamma=0.9)
print("\n最优价值函数:")
print(np.round(optimal_V, 3))
print("\n最优策略 (部分状态示例):")
for i in range(GRID_SIZE):
    for j in range(GRID_SIZE):
        s = (i, j)
        if s != TERMINAL_STATE:
            print(f"状态{s}: {optimal_policy.get(s)}", end=' | ')
    print()

运行后,你会看到策略从随机开始,经过几次迭代后稳定下来,最终生成一个指向终点的最优策略(例如,在左上角区域选择RIGHTDOWN)。这个过程完美诠释了贝尔曼方程如何作为动态规划的核心,驱动智能体从随机探索走向最优决策。

5. 超越表格型方法:函数近似与深度强化学习

我们上面的例子中,状态空间很小(16个格子),可以用一个表格来存储每个状态的价值 V(s)。这就是表格型方法。但在现实问题中,状态空间可能是天文数字(比如围棋的棋盘状态,或者游戏的一帧图像),根本无法用表格存储。

这时,我们就需要函数近似(Function Approximation)。我们用一个人工神经网络(或其他函数)来近似表示价值函数 V(s; θ)Q(s, a; θ),其中 θ 是神经网络的参数。贝尔曼方程的角色就从“需要精确求解的方程”转变为“提供监督信号的目标”。

以著名的 DQN(Deep Q-Network) 为例,其核心更新公式正是基于贝尔曼最优方程:

目标Q值 = r + γ * max_a' Q(s', a'; θ_target)
损失 = (目标Q值 - Q(s, a; θ))^2

这里,Q(s, a; θ) 是当前网络对Q值的估计,目标Q值则是根据贝尔曼最优方程计算出的“更准确”的估计。通过最小化两者之间的差距(损失),神经网络参数 θ 被不断调整,使其输出的Q值越来越满足贝尔曼方程,从而逼近最优Q函数。

# 伪代码展示DQN更新核心思想
import torch
import torch.nn as nn
import torch.optim as optim

class QNetwork(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(state_dim, 64),
            nn.ReLU(),
            nn.Linear(64, 64),
            nn.ReLU(),
            nn.Linear(64, action_dim)
        )
    def forward(self, state):
        return self.fc(state)

# 假设已有经验回放缓冲区 replay_buffer
q_net = QNetwork(state_dim=4, action_dim=2)
target_net = QNetwork(state_dim=4, action_dim=2)
target_net.load_state_dict(q_net.state_dict()) # 同步参数
optimizer = optim.Adam(q_net.parameters(), lr=1e-3)
criterion = nn.MSELoss()

# 训练循环中的一步
batch = replay_buffer.sample(batch_size=32)
states, actions, rewards, next_states, dones = batch

# 计算当前Q值
current_q_values = q_net(states).gather(1, actions.unsqueeze(1)).squeeze(1)

# 使用目标网络计算下一个状态的最大Q值 (贝尔曼最优方程)
with torch.no_grad():
    next_q_values = target_net(next_states).max(dim=1)[0]
    target_q_values = rewards + gamma * next_q_values * (1 - dones) # done时无未来奖励

# 计算损失并更新
loss = criterion(current_q_values, target_q_values)
optimizer.zero_grad()
loss.backward()
optimizer.step()

在这个框架下,贝尔曼方程从理论工具变成了驱动神经网络学习的损失函数。每一次参数更新,都是让网络对状态价值的预测更符合“当前奖励加上折现未来价值”这一递归关系。我最初实现DQN玩Atari游戏时,最让我震撼的就是这个简单的方程竟然能引导网络从像素中学会复杂的游戏策略。调试的关键往往在于如何稳定地计算这个“目标Q值”,比如使用目标网络、双Q学习等技巧,本质上都是为了更好地实现贝尔曼方程所描述的关系。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐