千嶂夹城
发布于 2026-01-18 / 5 阅读
0
0

学习记录#2:超小白的强化学习入门实践

首先声明:如果你正经学RL,去看书或者大佬的博客,比如https://github.com/wangshusen/DRLhttps://datawhalechina.github.io/easy-rl/#/,这里我只是在写一个学习记录。

强化学习,是一种学习策略,是与监督学习相对应的。它们的目标都是训练一个能解决问题的智能体。

监督学习有点像刷题:我们出一些互相没有显著关联的题目,智能体给出答案,我们将这个答案和正确答案比对,以此训练智能体。

强化学习有点像游戏:给一个上一帧和下一帧有明确关联的游戏,智能体给出操作,我们考虑这个操作的后果与期望的目标,以此训练智能体。

强化学习和监督学习需要的东西很不一样,监督学习需要输入输出数据集,强化学习需要环境。

智能体根据环境做出动作,环境给予智能体反馈,奖罚智能体,并告诉智能体当前世界的状态。

AE_loop.png

一言以蔽之,强化学习是这样一个模型:智能体从环境获得 Observation,做出 Action 改变环境,针对智能体做的怎么样,我们给出 Reward 进行评判,智能体要做的,是让 Reward 最大。

python 的 gymnasium 是一个实践强化学习的非常好的库,下面所有内容都基于这个库:

https://gymnasium.farama.org/

首先,创建 python 环境,这里使用 python3.12 是因为这是一个稳定的版本,有 tenserflow 库,方便之后搞事情……。注意这里装的是 gymnasium 因为 gym 已经停止维护了,安装 gymnasium 时可以带上 [all] 装上所有特性,后续使用各种环境的时候不用单独安装了,适合我这种空间足够的小白玩。去掉 [all] 也是可以的,后续可以根据报错安装对应的环境(QwQ。

conda create --name rl python=3.12 # 创建虚拟环境
conda activate rl # 切换环境
pip install "gymnasium[all]" # 安装 gymnasium,国内环境可以加一个加速镜像源 -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple

安装完成后,新建一个 .py 文件,写上这段例程,然后 python ./文件名.py 运行它:

# 对于这个案例,你可以使用 `pip install "gymnasium[classic-control]"` 安装 gym 需要的特性(像我一样 all 了之后可以忽略这行)
import gymnasium as gym

# 创建我们的训练环境 —— 一个小车顶着一个杆子,要平衡杆子不让它倒下
env = gym.make("CartPole-v1", render_mode="human")

# 重置环境到起始状态,每次 env.step 前都一定要 reset 哦
observation, info = env.reset()
# observation 是智能体从环境中能获得的信息,比如小车位置、小车速度、杆子角度、……
# info 提供了一些额外 debug 信息,在我们入门的旅程中,不需要在意它

print(f"Starting observation: {observation}")
# 一个可能的输出: [ 0.01234567 -0.00987654  0.02345678  0.01456789]
# 对应: [小车位置, 小车速度, 杆子角度, 杆子旋转角速度]

episode_over = False
total_reward = 0

while not episode_over:
    # 从所有可能的动作中选一个: 0 = 小车向左, 1 = 小车向右
    action = env.action_space.sample()  # 目前先随机一个动作吧

    # 采取这个动作,看看会发生什么
    observation, reward, terminated, truncated, info = env.step(action)

    # 在这个案例中:杆子每保持直立一步,reward 就增加 1
    # 当杆子落得太远,terminated 会置为真 (智能体失败了)
    # 当到达时间限制的时候 truncated 会置 (默认 500 步)

    total_reward += reward
    episode_over = terminated or truncated

print(f"Episode finished! Total reward: {total_reward}")
env.close()

运行后,会出现一个窗口,小车顶着杆子走,很快杆子会落下了,然后结束。

hint:将上面代码中的 action = env.action_space.sample() 改成 action = int(observation[0]*0.1 + observation[1] + observation[2] + observation[3] > 0) 可以获得一个更加智能的智能体。

在这个例子里,我们看到了 gymnasium 的主要特性:使用 gym.make 创建虚拟环境(以 env 为例),使用 env.reset() 重置环境,使用 env.step(action) 执行 Agent 的动作。

在这个例子中,我们实现了平衡着杆子的小车,但要谈到强化学习,我们的旅程才刚刚开始。

Q-Learning

为了方便讨论,我们假设智能体从环境得到的 Observation 是离散的,能采取的 Action 也是离散的。在这一节中,我们先以 is_slippery = falsehttps://gymnasium.farama.org/environments/toy_text/frozen_lake/ 环境入手,并进行这样的环境配置:

import gymnasium as gym
env = gym.make(
    'FrozenLake-v1',
    desc=None,
    map_name="4x4",
    is_slippery=False,
    reward_schedule=(10,-10,-1),
    render_mode="human"
)

智能体从环境获得 Observation,做出 Action 改变环境,针对智能体做的怎么样,我们给出 Reward 进行评判,智能体要做的,是让 Reward 最大。

对于环境的每一个状态(暂时假定这能在 Observation 中完全体现出来)与给定的 Action,智能体能得到一个针对性的 Reward,这一关系我们可以用一个表格来表示。表格的每一行表示不同的 Observation,每一列表示不同的 Action,表格中的数值表示 Reward。

这是 Reward 关于 Observation(行)与 Action(列)关系的表格

Reward

0(向左)

1(向下)

2(向右)

3(向上)

-1

-1

-1

-1

-1

-10 (TR)

-1

-1

-1

-1

-1

-1

-1

-10 (TR)

-1

-1

-1

-1

-10 (TR)

-1

/

/

/

/

-10 (TR)

-1

-10 (TR)

-1

/

/

/

/

-1

-10 (TR)

-1

-1

-1

-1

-1

-10 (TR)

-1

-1

-10 (TR)

-1

/

/

/

/

/

/

/

/

-10 (TR)

-1

-1

-1

-1

-1

10 (TE)

-1

/

/

/

/

诶,那还不简单,我们每次做使得我们马上获得的 Reward 最大的 Action 不就行了……了吗?不对啊不对,在智能体工作过程中,每一步 action 马上得到的 reward 几乎都为 0,在学习过程中也可能遇到为了 reward 总和更高暂时拿负 reward 的情况。

但是,我们可以定义,在某一个 Observation 下采取某个 Action,经过若干步后我们“期望”我们的智能体获得的总 Reward,我们称之为 Q。对于目前这个离散非随机案例,Q 可以是进行这一 Action 后能获得的最大 Reward。熟悉 OI 的小伙伴们马上就能发现,这件事情是可以动态规划求解的。

Q'(s,a) = W(s,a) + \max_{a'\in \mathbb {A}}Q(s',a')

实际 Q-learning 的公式中,会在 \max\limits_{a'\in\mathbb A}Q(s',a')前面加上一个折扣因子的在 0 到 1 之间系数。这个系数越接近 0,智能体越重视及时回报,越接近 1,智能体越重视长期回报,在最终得分上表现更好,最终公式如下:

Q'(s,a) = W(s,a) + \gamma \max_{a'\in \mathbb {A}}Q(s',a')

总之,我们重复进行这个若干遍来更新 Q,直到达到最大执行步骤数或 Q 表格稳定即可。

动态规划求解 Q 的过程
\begin{bmatrix} -1& -1& -1& -1&\\ -1& -10& -1& -1&\\ -1& -1& -1& -1&\\ -1& -10& -1& -1&\\ -1& -1& -10& -1&\\ 0& 0& 0& 0&\\ -10& -1& -10& -1&\\ 0& 0& 0& 0&\\ -1& -10& -1& -1&\\ -1& -1& -1& -10&\\ -1& -1& -10& -1&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& -1& -1& -1&\\ -1& -1& 10& -1&\\ 0& 0& 0& 0&\\ \end{bmatrix}\to \begin{bmatrix} -2& -2& -2& -2&\\ -2& -10& -2& -2&\\ -2& -2& -2& -2&\\ -2& -10& -2& -2&\\ -2& -2& -10& -2&\\ 0& 0& 0& 0&\\ -10& -2& -10& -2&\\ 0& 0& 0& 0&\\ -2& -10& -2& -2&\\ -2& -2& -2& -10&\\ -2& 9& -10& -2&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& -2& 9& -2&\\ -2& 9& 10& -2&\\ 0& 0& 0& 0&\\ \end{bmatrix}\to \begin{bmatrix} -3& -3& -3& -3&\\ -3& -10& -3& -3&\\ -3& -3& -3& -3&\\ -3& -10& -3& -3&\\ -3& -3& -10& -3&\\ 0& 0& 0& 0&\\ -10& 8& -10& -3&\\ 0& 0& 0& 0&\\ -3& -10& -3& -3&\\ -3& 8& 8& -10&\\ -3& 9& -10& -3&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& 8& 9& -3&\\ 8& 9& 10& 8&\\ 0& 0& 0& 0&\\ \end{bmatrix}
\to \begin{bmatrix} -4& -4& -4& -4&\\ -4& -10& -4& -4&\\ -4& 7& -4& -4&\\ -4& -10& -4& -4&\\ -4& -4& -10& -4&\\ 0& 0& 0& 0&\\ -10& 8& -10& -4&\\ 0& 0& 0& 0&\\ -4& -10& 7& -4&\\ -4& 8& 8& -10&\\ 7& 9& -10& 7&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& 8& 9& 7&\\ 8& 9& 10& 8&\\ 0& 0& 0& 0&\\ \end{bmatrix}\to \begin{bmatrix} -5& -5& -5& -5&\\ -5& -10& 6& -5&\\ -5& 7& -5& 6&\\ 6& -10& -5& -5&\\ -5& 6& -10& -5&\\ 0& 0& 0& 0&\\ -10& 8& -10& 6&\\ 0& 0& 0& 0&\\ 6& -10& 7& -5&\\ 6& 8& 8& -10&\\ 7& 9& -10& 7&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& 8& 9& 7&\\ 8& 9& 10& 8&\\ 0& 0& 0& 0&\\ \end{bmatrix}\to \begin{bmatrix} -6& 5& 5& -6&\\ -6& -10& 6& 5&\\ 5& 7& 5& 6&\\ 6& -10& 5& 5&\\ 5& 6& -10& -6&\\ 0& 0& 0& 0&\\ -10& 8& -10& 6&\\ 0& 0& 0& 0&\\ 6& -10& 7& 5&\\ 6& 8& 8& -10&\\ 7& 9& -10& 7&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& 8& 9& 7&\\ 8& 9& 10& 8&\\ 0& 0& 0& 0&\\ \end{bmatrix}
\to \begin{bmatrix} 4& 5& 5& 4&\\ 4& -10& 6& 5&\\ 5& 7& 5& 6&\\ 6& -10& 5& 5&\\ 5& 6& -10& 4&\\ 0& 0& 0& 0&\\ -10& 8& -10& 6&\\ 0& 0& 0& 0&\\ 6& -10& 7& 5&\\ 6& 8& 8& -10&\\ 7& 9& -10& 7&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& 8& 9& 7&\\ 8& 9& 10& 8&\\ 0& 0& 0& 0&\\ \end{bmatrix}\to...\to \begin{bmatrix} 4& 5& 5& 4&\\ 4& -10& 6& 5&\\ 5& 7& 5& 6&\\ 6& -10& 5& 5&\\ 5& 6& -10& 4&\\ 0& 0& 0& 0&\\ -10& 8& -10& 6&\\ 0& 0& 0& 0&\\ 6& -10& 7& 5&\\ 6& 8& 8& -10&\\ 7& 9& -10& 7&\\ 0& 0& 0& 0&\\ 0& 0& 0& 0&\\ -10& 8& 9& 7&\\ 8& 9& 10& 8&\\ 0& 0& 0& 0&\\ \end{bmatrix}

求得 Q 表格之后,诶,我们就可以每次做使得我们马上获得的 Q 最大/并列最大的 Action 就行了。然而,更多实际强化学习问题中,Observation 与 Action 可能是连续的,智能体每进行一次 Action 后对环境的改变是有一定随机性的。

此外,在更多实际强化学习问题中,我们是无模型的,也就是说我们得不到全局的 W 表,我们只能通过智能体不断实验探索以获取 W,这就引出了 ε-贪婪策略。

ε-贪婪策略

ε-贪婪策略咋说呢,就是分为探索和利用,最开始探索多,到后面利用多。探索就是随机选一个动作执行,利用就是在 Q 值大的动作里选一个做。边做动作边更新 Q 矩阵。

这里基于上面的例子给一个使用 ε-贪婪策略更新 Q 矩阵的智能体的例子:

from collections import defaultdict
import gymnasium as gym
import numpy as np
class FrozenLakeAgent:
    def __init__(
        self,
        env: gym.Env,
        learning_rate: float = 0.1,
        initial_epsilon: float = 1,
        epsilon_decay: float = 0.999,
        final_epsilon: float = 0.1,
        discount_factor: float = 1,
    ):
        """初始化一个 Q-Learning 智能体
        
        参数:
            env: gym 环境
            learning_rate: 学习率
            initial_epsilon: 初始探索率
            epsilon_decay: 探索率衰减
            final_epsilon: 最终探索率
            discount_factor: 折扣因子
        """
        self.env = env
        # Q 值表,使用 defaultdict 初始化为 0 以便自动初始化未见过的状态
        self.qValues = defaultdict(lambda: np.zeros(env.action_space.n))
        
        self.lr = learning_rate # 学习率
        self.discount_factor = discount_factor # 折扣因子
        
        # 探索率相关参数
        self.epsilon = initial_epsilon
        self.epsilon_decay = epsilon_decay
        self.final_epsilon = final_epsilon
        
        # 跟踪训练过程
        self.training_error = []
    
    def get_action(self, obsv: int) -> int:
        """基于 ε-贪婪策略选择动作
        
        参数:
            obsv: 当前观察到的状态
            
        返回:
            选择的动作
        """
        if np.random.rand() < self.epsilon:
            return self.env.action_space.sample()  # 探索Explore:随机选择动作
        else:
            return np.argmax(self.qValues[obsv])  # 利用Exploit:选择 Q 值最高的动作
    
    def update(
        self,
        obsv: int,
        action: int,
        reward: float,
        terminated: bool,
        next_obsv: int
    ):
        """使用 Q-Learning 更新 Q 值
        
        参数:
            obsv: 当前观察到的状态
            action: 采取的动作
            reward: 获得的奖励
            terminated: 是否终止
            next_obsv: 下一个观察到的状态
        """
        current_q = self.qValues[obsv][action]
        max_future_q = np.max(self.qValues[next_obsv]) if not terminated else 0
        # Q-Learning 更新公式
        new_q = (1 - self.lr) * current_q + self.lr * (reward + self.discount_factor * max_future_q)
        self.qValues[obsv][action] = new_q
        
        # 记录训练误差
        self.training_error.append(abs(new_q - current_q))
    
    def decay_epsilon(self):
        """衰减探索率 ε"""
        self.epsilon = max(self.final_epsilon, self.epsilon * self.epsilon_decay) if self.epsilon > self.final_epsilon else self.epsilon

怎么训练这个智能体呢?这样:

n_episodes = 10000
env = gym.make(
    'FrozenLake-v1',
    desc=None,
    is_slippery=False,
    success_rate=0.9,
    reward_schedule=(10,-10,-1),
    map_name="4x4"
)
env = gym.wrappers.RecordEpisodeStatistics(env, buffer_length=n_episodes)
observation, info = env.reset()
tot_reward = 0

agent = FrozenLakeAgent(env)

from tqdm import tqdm
for episode in tqdm(range(n_episodes)):
    observation, info = env.reset()
    done = False
    while not done:
        action = agent.get_action(observation)
        next_observation, reward, terminated, truncated, info = env.step(action)
        agent.update(observation, action, reward, terminated, next_observation)
        done = terminated or truncated
        observation = next_observation
    agent.decay_epsilon()

这里获得的 agent.qValues 就是训练的成果了。然后可以将学习参数 ε 设置为 0,对这个模型进行测试:

total_rewards = []
old_epsilon = agent.epsilon
agent.epsilon = 0.0  # 禁用探索以测试学习到的策略

for _ in range(100):
    obs, info = env.reset()
    episode_reward = 0
    done = False
    while not done:
        action = agent.get_action(obs)
        obs, reward, terminated, truncated, info = env.step(action)
        episode_reward += reward
        done = terminated or truncated
    total_rewards.append(episode_reward)

win_rate = np.mean(np.array(total_rewards) > 0)
print(f"Win rate over 100 test episodes: {win_rate * 100}%")
print(f"Average reward over 100 test episodes: {np.mean(total_rewards)}")
print(f"Standard deviation of reward over 100 test episodes: {np.std(total_rewards)}")
# 下面是展示训练得到的 qValues
print(f"Trained Q-values:")
for state, actions in agent.qValues.items():
    print(f"State {state}: {actions}")

好了就写到这里因为哎我越写越发现我花半天写出来的东西好烂啊里面有用的部分完全是大佬博客的零测真子集哎不想写了不想写了就这样收个尾等我以后也成了大佬再写这种教程性的东西吧唉唉唉


评论