强化学习:用Q-Learning算法解决Cart Pole平衡问题

813 阅读3分钟

推车杆(Cart Pole)介绍

杆通过非驱动关节连接到推车上,推车沿着无摩擦轨道移动。摆锤直立在推车上,目标是通过在推车的左右方向施加力来平衡杆。类似我之前写的扫把平衡游戏。

cart_pole.gif

游戏规则

车可以左右推,杆子倾斜一定角度(±12°)游戏结束,每推动一次车奖励+1,奖励累计到500或者到达边界游戏结束。左右推动车调整杆子角度不让他倒下,尽可能的使奖励分数高些。

我们可以看到官方文档,推车杆给了4种状态。

观察值最小最大
车位置-4.84.8
车速度-∞
杆角度-0.418(-24°)-0.418(24°)
杆角速度-∞

游戏环境安装

通过Gymnasium去安装。

pip install gymnasium
# 安装游戏包
pip install gymnasium[classic-control]

python代码简单运行游戏。

import gymnasium as gym

if __name__ == '__main__':

    env = gym.make("CartPole-v1", render_mode="human")
    observation, info = env.reset()

    for _ in range(1000):
        action = env.action_space.sample()  # agent policy that uses the observation and info
        observation, reward, terminated, truncated, info = env.step(action)

        if terminated or truncated:
            observation, info = env.reset()

    env.close()

Q-Learning算法

我们可以使用机器学习的Q-Learning算法,让推车杆游戏的奖励最大化。Q-Learning算法简单来说就是用一个表格[状态,动作]记录在每个状态下,执行那个动作的期望累计奖励,训练完后,你可用通过查看表格看看当前状态,那个动作期望累计奖励最高,你就执行那个动作。

期望累计奖励计算公式

Q(s,a)Q(s,a)+α[r+γmaxQ(s,a)Q(s,a)]Q(s,a)←Q(s,a)+α[r+γmax​Q(s′,a′)−Q(s,a)]

其中:

  • s 是当前状态
  • a 是当前动作
  • r 是奖励
  • s′ 是下一个状态
  • a′ 是下一个状态
  • α 是学习率
  • γ 是折扣因子

python代码

我们要使用推车杆的状态得先数据离散化,就比如一杯奶茶0~600ml,你可以倒入300.1ml、300.2ml无限数,我们通过离散化分区间,就将这些倒入量分为大、小、中杯。

下面代码用5维数组(4个状态,1个动作)去存储Q值,训练时根据探索率来选择动作,每一次动作都会更改4个状态,Q值的更新使用上面公式。

import gymnasium as gym
import numpy as np
import random


class QLearning:
    def __init__(self, env):
        # 环境
        self.env = env
        # 区间个数
        self.num = 20
        # 创建4维元组(4个状态)
        states = tuple([self.num] * self.env.observation_space.shape[0])
        # 初始化Q表  合并元组(2个动作)变成5维数组
        self.q_table = np.zeros(states + (self.env.action_space.n,))

        # 定义离散化的区间
        self.bins = [
            np.linspace(-4.8, 4.8, self.num),
            np.linspace(-4, 4, self.num),
            np.linspace(-0.418, 0.418, self.num),
            np.linspace(-1, 1, self.num)
        ]

        self.alpha = 0.1  # 学习率
        self.gamma = 0.9  # 折扣因子
        self.epsilon = 0.95  # 初始探索率
        # self.epsilon_min = 0.01  # 最小探索率
        # self.epsilon_decay = 0.995  # 探索折扣率

    # 环境状态离散化
    def discretize_state(self, state):

        discretized = []
        for i in range(len(state)):
            discretized.append(np.digitize(state[i], self.bins[i]) - 1)

        return tuple(discretized)

    # 定义一个 ε-greedy 策略选择动作:
    def epsilon_greedy_policy(self, state):
        if random.uniform(0, 1) < self.epsilon:
            return self.env.action_space.sample()  # 随机选择动作

        else:
            return np.argmax(self.q_table[state])  # 选择最大 Q 值的动作

    # 更新Q值
    def update_q_table(self, state, next_state, reward, action):
        best_next_action = np.argmax(self.q_table[next_state])
        td_target = reward + self.gamma * self.q_table[next_state][best_next_action]
        td_error = td_target - self.q_table[state][action]
        self.q_table[state][action] += self.alpha * td_error

        # 衰减探索率
        # if self.epsilon > self.epsilon_min:
        #     self.epsilon *= self.epsilon_decay

if __name__ == '__main__':
    # env = gym.make("CartPole-v1", render_mode="human")
    env = gym.make("CartPole-v1")
    ql = QLearning(env)

    num_episodes = 10000
    # 训练代理
    for i in range(num_episodes):

        state, info = env.reset()
        # 离散化
        state = ql.discretize_state(state)
        # 表示回合自然结束
        terminated = False
        # 表示回合由于外部限制被截断
        truncated = False
        total_reward = 0
        while not terminated and not truncated:
            action = ql.epsilon_greedy_policy(state)
            next_state, reward, terminated, truncated, _ = env.step(action)
            # 离散化
            next_state = ql.discretize_state(next_state)
            # 更新Q
            ql.update_q_table(state,next_state,reward,action)
            state = next_state
            total_reward += reward

        print(f"遍历{i}: 奖励:{total_reward}")

    # 测试训练后的代理
    total_reward = 0
    num_test_episodes = 100
    for i in range(num_test_episodes):
        state, info = env.reset()
        state = ql.discretize_state(state)
        t_reward = 0
        terminated = False
        truncated = False
        while not terminated and not truncated:
            action = np.argmax(ql.q_table[state])
            state, reward, terminated,truncated, _ = env.step(action)
            state = ql.discretize_state(state)
            total_reward += reward
            t_reward += reward
        print(f"训练后-遍历{i} 奖励:{t_reward}")

    average_reward = total_reward / num_test_episodes
    print(f"训练后平均奖励: {average_reward:.2f}")

执行后结果,可以看到有的已经到达上限的500次。

训练后-遍历94 奖励:114.0
训练后-遍历95 奖励:142.0
训练后-遍历96 奖励:500.0
训练后-遍历97 奖励:178.0
训练后-遍历98 奖励:128.0
训练后-遍历99 奖励:500.0
训练后平均奖励: 254.01