目录

1、核心理论:在线时序差分学习(On-policy TD Learning)

边试边学,走一步看一步

2、基本原理:“S-A-R-S'-A'” 的循环更新

具体过程:“走 - 看 - 评 - 改” 的循环

3、核心公式:Q 函数的更新规则

简单公式:怎么修正 “好坏评价”?

4、多步 Sarsa 算法核心

5、策略与收敛性

6、完整代码

6.1、一步Sarsa算法

6.1.1、完整代码

6.1.2、实验结果

6.2、多步Sarsa算法

6.2.1、完整代码

6.2.2、实验结果


1、核心理论:在线时序差分学习(On-policy TD Learning)

Sarsa 算法是强化学习中时序差分(TD)学习的一种经典实现,属于在线策略(On-policy) 算法。其核心思想是:在与环境交互的过程中,通过不断更新动作价值函数(Q 函数)来学习最优策略,且学习的策略与正在执行的策略保持一致(即 “在线” 更新)。

  • 动作价值函数(Q 函数):定义为Q(s,a),表示在状态s下执行动作a后,遵循当前策略所能获得的累积折扣奖励的期望。
  • 目标:通过学习使Q(s,a)收敛到最优动作价值函数Q^*(s,a),此时对应的策略即为最优策略。
边试边学,走一步看一步

Sarsa 算法是强化学习里一种 “边行动边学习” 的方法。它的核心逻辑很简单:你在做一件事的时候,每走一步,就根据接下来的情况调整对 “当前这一步好坏” 的判断,而且判断的依据和你实际要走的下一步是绑定的。

 

打个比方:你在迷宫里找出口,每走一步(比如向东),会得到反馈(比如吃到金币或撞到墙),然后你会看下一步打算往哪走(比如接下来向北),再根据 “当前步的反馈 + 下一步的计划”,来修正 “向东这一步到底好不好” 的想法。下次再到这个位置,就知道该怎么选了。

2、基本原理:“S-A-R-S'-A'” 的循环更新

Sarsa 算法的名称来源于其更新过程中涉及的五个关键要素: 当前状态(State, s)→ 当前动作(Action, a)→ 奖励(Reward, r)→ 下一状态(Next State, s')→ 下一动作(Next Action, a'),即 “s,a,r,s',a'” 的序列。

算法的核心流程如下:

  1. 初始化:设定初始状态s,并根据当前策略(如\epsilon- 贪婪策略)选择初始动作a。
  2. 交互与观察:执行动作a,获得环境反馈的奖励r和下一状态s'。
  3. 确定下一动作:根据当前策略,从下一状态s'中选择动作a'。
  4. 更新 Q 函数:利用时序差分误差(TD error)更新Q(s,a)。
  5. 迭代:将s'设为当前状态s,a'设为当前动作a,重复步骤 2-4,直到达到终止状态(如任务完成或失败)。
具体过程:“走 - 看 - 评 - 改” 的循环

Sarsa 的名字来源于它每次学习都离不开 5 个关键信息:
现在在哪(状态 s)→ 现在做什么(动作 a)→ 得到什么反馈(奖励 r)→ 接下来到哪(新状态 s')→ 接下来做什么(新动作 a')
整个过程像个循环:

  1. 开始:比如你在迷宫的某个位置(s),凭当前的经验选一个动作(a,比如向右走)。
  2. 行动与反馈:执行动作 a,得到奖励 r(比如 + 10 分),并走到新位置 s'。
  3. 计划下一步:根据当前的经验,从新位置 s' 再选一个下一步的动作 a'(比如接下来向上走)。
  4. 更新经验:用 “刚才的奖励 + 下一步的预期”,修正对 “在 s 做 a” 的评价(也就是 Q 值)。
  5. 重复:把新位置 s' 当成现在的位置,新动作 a' 当成现在要做的动作,继续循环,直到到达终点(比如走出迷宫)。

3、核心公式:Q 函数的更新规则

Sarsa 算法通过时序差分目标(TD target) 与当前 Q 值的差异来更新Q(s,a),公式如下:

  1. 时序差分目标(TD Target) 表示在状态s执行动作a后,基于 “当前策略” 所能预期的累积奖励,计算公式为:G_t^{(1)} = r + \gamma \cdot Q(s',a') 其中:

    • r:当前动作a获得的即时奖励;
    • \gamma \in [0,1]:折扣因子,用于权衡即时奖励和未来奖励的重要性;
    • Q(s',a'):下一状态s'和下一动作a'的当前 Q 值(基于当前策略)。
  2. 时序差分误差(TD Error) 衡量当前 Q 值与预期目标的差异:\delta = G_t^{(1)} - Q(s,a) = \left[ r + \gamma \cdot Q(s',a') \right] - Q(s,a)

  3. Q 函数更新公式 利用 TD 误差更新Q(s,a),公式为:Q(s,a) \leftarrow Q(s,a) + \alpha \cdot \delta其中:

    合并后可写为:\boxed{Q(s,a) \leftarrow Q(s,a) + \alpha \cdot \left[ r + \gamma \cdot Q(s',a') - Q(s,a) \right]}

    • \alpha \in (0,1]:学习率,控制每次更新的幅度(值越大,更新越激进;值越小,收敛越稳定)。
简单公式:怎么修正 “好坏评价”?

我们用 Q (s,a) 表示 “在状态 s 做动作 a” 的好坏(值越大越好)。Sarsa 的核心就是更新这个 Q 值,公式可以通俗写成:

 

新的 Q (s,a) = 原来的 Q (s,a) + 一点点修正

 

这个 “修正” 怎么来?看两点:

 
  • 刚才的奖励 r(比如吃到金币 + 10,撞到墙 - 5);
  • 下一步的预期:也就是 “到了新位置 s',做动作 a'” 的好坏(Q (s',a')),再打个折扣(比如未来的奖励没现在重要,打 8 折)。
 

所以修正量是:(r + 折扣 × 下一步的 Q 值) - 原来的 Q 值

 

最后公式写成: Q(s,a) = Q(s,a) + α × [r + γ × Q(s',a') - Q(s,a)]

 

这里:

  • α 是 “学习率”(比如 0.1),控制每次修正的幅度(别改太猛);
  • γ 是 “折扣因子”(比如 0.9),表示未来的奖励有多重要(越接近 1,越看重长远)。

4、多步 Sarsa 算法核心

多步 Sarsa 与单步 Sarsa 的核心区别是:利用未来 n 步的累积奖励更新 Q 值,而非仅依赖单步奖励,从而更高效地学习策略。

  1. 核心思想:通过累积未来 n 步的奖励(而非单步奖励)计算更新目标,平衡偏差和方差,加速收敛。
  2. 关键公式:n 步回报(n-step return)G_t^{(n)} = R_{t+1} + \gamma R_{t+2} + \dots + \gamma^{n-1} R_{t+n} + \gamma^n Q(S_{t+n}, A_{t+n})其中,t为当前时间步,n为步数,R为奖励,\gamma为折扣因子。
  3. 更新规则: Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[ G_t^{(n)} - Q(S_t, A_t) \right]

5、策略与收敛性

  • 策略选择:Sarsa 通常采用\epsilon- 贪婪策略(\epsilon-greedy policy):以1-\epsilon的概率选择当前 Q 值最大的动作( exploitation),以\epsilon的概率随机选择动作(exploration),平衡探索与利用。
  • 收敛性:在满足一定条件(如学习率\alpha随时间递减且总和无穷、策略渐进收敛到贪婪策略)时,Sarsa 算法可保证Q(s,a)收敛到最优值Q^*(s,a)。

Sarsa 最特别的地方是:它学习的 “经验” 和实际 “行动” 是一致的。比如它计划下一步要向上走(a'),就会用 “向上走的预期” 来修正现在的判断,而不是空想 “如果下一步走最好的路会怎样”。这种 “说到做到” 的特点,让它在需要 “谨慎规划” 的任务(比如悬崖边走路,不能随便尝试危险动作)中更稳妥。

6、完整代码

6.1、一步Sarsa算法

6.1.1、完整代码

"""
文件名: 5.1
作者: 墨尘
日期: 2025/7/22
项目名: d2l_learning
备注: Sarsa算法在悬崖漫步环境中的实现与可视化
"""
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.table import Table


class CliffWalkingEnv:
    """悬崖漫步环境(确定性动作)"""

    def __init__(self, ncol=12, nrow=4):
        """初始化环境参数"""
        self.ncol = ncol  # 环境宽度(列数)
        self.nrow = nrow  # 环境高度(行数)
        self.reset()  # 重置环境状态到起点
        # 定义悬崖位置:最后一行的第1列到第10列
        self.cliff = [(nrow - 1, j) for j in range(1, ncol - 1)]
        self.goal = (nrow - 1, ncol - 1)  # 目标位置:右下角

    def reset(self):
        """重置环境到初始状态"""
        self.state = (self.nrow - 1, 0)  # 起点:左下角(3,0)
        return self._state_to_index(self.state)  # 返回一维状态索引

    def step(self, action):
        """执行动作并返回下一状态、奖励和终止标志"""
        # 动作编码:0=上, 1=右, 2=下, 3=左
        x, y = self.state  # 当前状态坐标
        if action == 0:     # 向上移动
            x = max(x - 1, 0)  # 防止越界
        elif action == 1:   # 向右移动
            y = min(y + 1, self.ncol - 1)  # 防止越界
        elif action == 2:   # 向下移动
            x = min(x + 1, self.nrow - 1)  # 防止越界
        elif action == 3:   # 向左移动
            y = max(y - 1, 0)  # 防止越界

        self.state = (x, y)  # 更新当前状态

        # 计算奖励和终止条件
        if self.state == self.goal:
            reward = 0  # 到达目标获得0奖励
            done = True  # 终止回合
        elif self.state in self.cliff:
            reward = -100  # 掉入悬崖获得-100惩罚
            done = False  # 回合不终止,但回到起点
            self.state = (self.nrow - 1, 0)  # 重置到起点
        else:
            reward = -1  # 每步移动获得-1奖励(鼓励最短路径)
            done = False

        return self._state_to_index(self.state), reward, done, {}

    def _state_to_index(self, state):
        """将二维坐标状态转换为一维索引"""
        return state[0] * self.ncol + state[1]


class SarsaAgent:
    """Sarsa算法实现"""

    def __init__(self, env, alpha=0.5, gamma=0.9, epsilon=0.1):
        """初始化Sarsa智能体参数"""
        self.env = env  # 环境实例
        self.n_states = env.nrow * env.ncol  # 状态总数
        self.n_actions = 4  # 动作总数(上右下左)
        self.alpha = alpha  # 学习率:控制更新步长
        self.gamma = gamma  # 折扣因子:权衡即时奖励和未来奖励
        self.epsilon = epsilon  # ε-贪婪策略参数
        # 初始化Q表:状态×动作矩阵,存储每个状态-动作对的价值
        self.Q = np.zeros((self.n_states, self.n_actions))

    def epsilon_greedy_policy(self, state):
        """ε-贪婪策略:以ε概率随机探索,以1-ε概率利用当前最优动作"""
        if np.random.random() < self.epsilon:
            return np.random.randint(0, self.n_actions)  # 随机动作(探索)
        else:
            return np.argmax(self.Q[state])  # 最优动作(利用)

    def update(self, state, action, reward, next_state, next_action, done):
        """Sarsa更新公式:基于五元组(s,a,r,s',a')更新Q表"""
        current_q = self.Q[state, action]  # 当前状态-动作对的Q值
        # 下一状态-动作对的Q值(若回合结束则为0)
        next_q = self.Q[next_state, next_action] if not done else 0
        # 计算TD目标:即时奖励 + 折扣后的下一状态Q值
        td_target = reward + self.gamma * next_q
        # 计算TD误差:TD目标与当前Q值的差
        td_error = td_target - current_q
        # 更新Q表:当前Q值向TD目标移动α步长
        self.Q[state, action] += self.alpha * td_error

    def train(self, episodes=500):
        """训练智能体指定轮数"""
        rewards_history = []  # 记录每轮总奖励
        steps_history = []    # 记录每轮步数

        for episode in range(episodes):
            state = self.env.reset()  # 重置环境,获取初始状态
            action = self.epsilon_greedy_policy(state)  # 选择初始动作
            done = False  # 终止标志
            total_reward = 0  # 累积奖励
            steps = 0  # 步数

            while not done:
                # 执行动作,获取下一状态、奖励和终止标志
                next_state, reward, done, _ = self.env.step(action)
                # 基于下一状态选择下一动作(ε-贪婪策略)
                next_action = self.epsilon_greedy_policy(next_state)
                # 使用五元组(s,a,r,s',a')更新Q表
                self.update(state, action, reward, next_state, next_action, done)
                # 转移到下一状态和动作
                state, action = next_state, next_action
                total_reward += reward  # 累积奖励
                steps += 1  # 增加步数

            # 记录本轮训练结果
            rewards_history.append(total_reward)
            steps_history.append(steps)

            # 每100轮打印训练进度
            if (episode + 1) % 100 == 0:
                print(f"Episode {episode + 1}/{episodes}, "
                      f"Total Reward: {total_reward}, "
                      f"Steps: {steps}")

        # 训练结束后,提取最优策略(每个状态下Q值最大的动作)
        self.policy = np.argmax(self.Q, axis=1)
        return rewards_history, steps_history


def visualize_policy(agent, env):
    """可视化最优策略(动作方向)"""
    action_symbols = ['↑', '→', '↓', '←']  # 动作符号映射
    grid = np.empty((env.nrow, env.ncol), dtype=object)

    # 构建策略网格
    for i in range(env.nrow):
        for j in range(env.ncol):
            state_idx = i * env.ncol + j
            if (i, j) == env.goal:
                grid[i, j] = 'G'  # 目标位置标记
            elif (i, j) in env.cliff:
                grid[i, j] = 'C'  # 悬崖位置标记
            else:
                # 根据Q表选择最优动作
                action = agent.policy[state_idx]
                grid[i, j] = action_symbols[action]

    # 打印策略网格
    print("\n最优策略:")
    for row in grid:
        print(' '.join([f"{cell:^3}" for cell in row]))


def plot_training_history(rewards, steps):
    """绘制训练历史曲线(奖励和步数随轮次的变化)"""
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

    # 绘制奖励曲线
    ax1.plot(rewards)
    ax1.set_title('Episode Rewards')
    ax1.set_xlabel('Episode')
    ax1.set_ylabel('Total Reward')

    # 绘制步数曲线
    ax2.plot(steps)
    ax2.set_title('Episode Steps')
    ax2.set_xlabel('Episode')
    ax2.set_ylabel('Steps')

    plt.tight_layout()  # 调整布局
    plt.show()


if __name__ == "__main__":
    # 创建悬崖漫步环境(12列×4行)
    env = CliffWalkingEnv(ncol=12, nrow=4)
    # 初始化Sarsa智能体
    agent = SarsaAgent(env, alpha=0.5, gamma=0.9, epsilon=0.1)

    # 训练智能体500轮
    print("开始训练Sarsa智能体...")
    rewards, steps = agent.train(episodes=500)

    # 可视化最优策略和训练历史
    visualize_policy(agent, env)
    plot_training_history(rewards, steps)

6.1.2、实验结果

6.2、多步Sarsa算法

6.2.1、完整代码

"""
文件名: 5.2
作者: 墨尘
日期: 2025/7/22
项目名: d2l_learning
备注: 多步Sarsa算法在悬崖漫步环境中的实现与可视化
"""
import numpy as np
import matplotlib.pyplot as plt


class CliffWalkingEnv:
    """悬崖漫步环境(确定性动作)"""

    def __init__(self, ncol=12, nrow=4):
        self.ncol = ncol  # 环境宽度(列数)
        self.nrow = nrow  # 环境高度(行数)
        self.reset()  # 重置环境状态到起点
        self.cliff = [(nrow - 1, j) for j in range(1, ncol - 1)]  # 悬崖位置
        self.goal = (nrow - 1, ncol - 1)  # 目标位置:右下角

    def reset(self):
        self.state = (self.nrow - 1, 0)  # 起点:左下角(3,0)
        return self._state_to_index(self.state)

    def step(self, action):
        # 动作编码:0=上, 1=右, 2=下, 3=左
        x, y = self.state
        if action == 0:     # 向上
            x = max(x - 1, 0)
        elif action == 1:   # 向右
            y = min(y + 1, self.ncol - 1)
        elif action == 2:   # 向下
            x = min(x + 1, self.nrow - 1)
        elif action == 3:   # 向左
            y = max(y - 1, 0)

        self.state = (x, y)

        # 奖励与终止条件
        if self.state == self.goal:
            reward = 0
            done = True
        elif self.state in self.cliff:
            reward = -100
            done = False
            self.state = (self.nrow - 1, 0)  # 回起点
        else:
            reward = -1
            done = False

        return self._state_to_index(self.state), reward, done, {}

    def _state_to_index(self, state):
        return state[0] * self.ncol + state[1]


class NStepSarsaAgent:
    """多步Sarsa算法实现"""

    def __init__(self, env, n=3, alpha=0.5, gamma=0.9, epsilon=0.1):
        self.env = env
        self.n = n  # 多步Sarsa的步数(核心参数)
        self.n_states = env.nrow * env.ncol
        self.n_actions = 4  # 上右下左
        self.alpha = alpha  # 学习率
        self.gamma = gamma  # 折扣因子
        self.epsilon = epsilon  # ε-贪婪策略
        self.Q = np.zeros((self.n_states, self.n_actions))  # Q表

    def epsilon_greedy_policy(self, state):
        """ε-贪婪策略:平衡探索与利用"""
        if np.random.random() < self.epsilon:
            return np.random.randint(0, self.n_actions)  # 随机探索
        else:
            return np.argmax(self.Q[state])  # 利用最优动作

    def update(self, trajectory, tau):
        """根据轨迹更新Q值(核心:计算n步回报)"""
        # trajectory格式:[(s0,a0,r0), (s1,a1,r1), ..., (st,at,rt)]
        # tau:待更新的时间步(t-n ≤ tau < t)
        G = 0.0  # n步回报

        # 累积n步奖励:R_{tau+1} + γR_{tau+2} + ... + γ^{n-1}R_{tau+n}
        for i in range(tau + 1, min(tau + self.n + 1, len(trajectory))):
            # 奖励的时间步为i,折扣因子为γ^(i - tau - 1)
            G += (self.gamma ** (i - tau - 1)) * trajectory[i][2]

        # 若未到轨迹终点,加上γ^n * Q(S_{tau+n}, A_{tau+n})
        if tau + self.n < len(trajectory) - 1:
            s_n = trajectory[tau + self.n][0]  # S_{tau+n}
            a_n = trajectory[tau + self.n][1]  # A_{tau+n}
            G += (self.gamma ** self.n) * self.Q[s_n, a_n]

        # 更新Q值:Q(S_tau, A_tau) += α[G - Q(S_tau, A_tau)]
        s_tau = trajectory[tau][0]
        a_tau = trajectory[tau][1]
        self.Q[s_tau, a_tau] += self.alpha * (G - self.Q[s_tau, a_tau])

    def train(self, episodes=500):
        """训练智能体(多步Sarsa核心逻辑)"""
        rewards_history = []
        steps_history = []

        for episode in range(episodes):
            trajectory = []  # 存储轨迹:[(s0,a0,r0), (s1,a1,r1), ...]
            state = self.env.reset()  # 初始状态
            action = self.epsilon_greedy_policy(state)  # 初始动作
            trajectory.append((state, action, 0.0))  # 初始奖励为0
            done = False
            total_reward = 0
            steps = 0

            while not done:
                # 执行动作,获取下一状态和奖励
                next_state, reward, done, _ = self.env.step(action)
                steps += 1
                total_reward += reward

                # 选择下一动作
                next_action = self.epsilon_greedy_policy(next_state)
                trajectory.append((next_state, next_action, reward))

                # 更新时间步(当前已执行steps步,时间步t=steps)
                t = steps
                # 待更新的时间步tau:从t-n开始,直到t-1
                tau = t - self.n
                if tau >= 0:
                    self.update(trajectory, tau)  # 用n步回报更新Q值

                # 转移到下一状态和动作
                state, action = next_state, next_action

            # 处理轨迹末尾剩余的tau(确保所有时间步都被更新)
            while tau < len(trajectory) - 1:
                tau += 1
                if tau >= self.n:
                    self.update(trajectory, tau)

            rewards_history.append(total_reward)
            steps_history.append(steps)

            if (episode + 1) % 100 == 0:
                print(f"Episode {episode + 1}/{episodes}, "
                      f"Total Reward: {total_reward}, "
                      f"Steps: {steps}")

        # 提取最优策略
        self.policy = np.argmax(self.Q, axis=1)
        return rewards_history, steps_history


def visualize_policy(agent, env):
    """可视化最优策略"""
    action_symbols = ['↑', '→', '↓', '←']
    grid = np.empty((env.nrow, env.ncol), dtype=object)

    for i in range(env.nrow):
        for j in range(env.ncol):
            state_idx = i * env.ncol + j
            if (i, j) == env.goal:
                grid[i, j] = 'G'
            elif (i, j) in env.cliff:
                grid[i, j] = 'C'
            else:
                action = agent.policy[state_idx]
                grid[i, j] = action_symbols[action]

    print("\n最优策略:")
    for row in grid:
        print(' '.join([f"{cell:^3}" for cell in row]))


def plot_training_history(rewards, steps):
    """绘制训练曲线"""
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
    ax1.plot(rewards)
    ax1.set_title('Episode Rewards (n-step Sarsa)')
    ax1.set_xlabel('Episode')
    ax1.set_ylabel('Total Reward')

    ax2.plot(steps)
    ax2.set_title('Episode Steps (n-step Sarsa)')
    ax2.set_xlabel('Episode')
    ax2.set_ylabel('Steps')

    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    # 创建环境
    env = CliffWalkingEnv(ncol=12, nrow=4)
    # 初始化多步Sarsa智能体(n=3步)
    agent = NStepSarsaAgent(env, n=3, alpha=0.5, gamma=0.9, epsilon=0.1)

    # 训练500轮
    print("开始训练多步Sarsa智能体...")
    rewards, steps = agent.train(episodes=500)

    # 可视化结果
    visualize_policy(agent, env)
    plot_training_history(rewards, steps)

6.2.2、实验结果

更多推荐