54、强化学习:OpenAI Gym 与 Q - 学习实战指南(上)

强化学习:OpenAI Gym 与 Q - 学习实战指南(上)

1. OpenAI Gym 工具包介绍

OpenAI Gym 是一个专门用于促进强化学习(RL)模型开发的工具包。它自带了多个预定义的环境,例如:
- CartPole :任务是平衡一根杆子。
- MountainCar :任务是将一辆汽车推上山顶。

此外,还有许多高级的机器人环境,可用于训练机器人抓取、推动和够取长凳上的物品,或者训练机械手握持方块、球或笔等。而且,OpenAI Gym 为开发新环境提供了一个方便、统一的框架,更多信息可查看其官方网站:https://gym.openai.com/。

要使用后续章节中的 OpenAI Gym 代码示例,需要安装 gym 库,可使用 pip 轻松完成安装:

pip install gym

如果在安装过程中需要额外帮助,请参考官方安装指南:https://gym.openai.com/docs/#installation。

2. 使用 OpenAI Gym 中的现有环境

为了练习使用 Gym 环境,我们以 CartPole-v1 环境为例,该环境在 OpenAI Gym 中已经存在。在这个示例环境中,有一根杆子连接在一个可以水平移动的小车上。

以下是该环境在强化学习中的一些属性:
- 状态(或观察)空间 Box(4,) ,表示一个四维空间,对应四个实值数,分别是小车的位置、小车的速度、杆子的角度以及杆子顶端的速度。
- 动作空间 Discrete(2) ,是一个离散空间,有两个选择,即向左或向右推动小车。

以下是使用该环境的代码示例:

import gym
env = gym.make('CartPole-v1')
print(env.observation_space)  # 输出: Box(4,)
print(env.action_space)  # 输出: Discrete(2)

环境对象 env 有一个 reset() 方法,可用于在每一轮训练前重新初始化环境,调用该方法会设置杆子的起始状态:

print(env.reset())
# 示例输出: array([-0.03908273, -0.00837535,  0.03277162, -0.0207195 ])

上述数组中的值表示小车的初始位置为 -0.039,速度为 -0.008,杆子的角度为 0.033 弧度,顶端的角速度为 -0.021。调用 reset() 方法时,这些值会在 [-0.05, 0.05] 范围内以均匀分布随机初始化。

重置环境后,可以通过选择一个动作并将其传递给 step() 方法来与环境进行交互:

print(env.step(action=0))
# 示例输出: (array([-0.03925023, -0.20395158,  0.03235723,  0.28212046]), 1.0, False, {})
print(env.step(action=1))
# 示例输出: (array([-0.04332927, -0.00930575,  0.03799964, -0.00018409]), 1.0, False, {})

通过上述两个命令,分别将小车向左( action=0 )和向右( action=1 )推动。每次调用 env.step() 方法时,会返回一个包含四个元素的元组:
| 元素 | 描述 |
| ---- | ---- |
| 数组 | 新的状态(或观察值) |
| 奖励 | 一个浮点型标量值 |
| 终止标志 | 布尔值(True 或 False) |
| Python 字典 | 包含辅助信息 |

环境对象 env 还有一个 render() 方法,可在每一步(或一系列步骤)后执行,以可视化环境以及杆子和小车的运动。

当杆子相对于垂直轴的角度大于 12 度(从任一侧),或者小车的位置距离中心位置超过 2.4 个单位时,本轮训练结束。此示例中定义的奖励是最大化小车和杆子在有效区域内的稳定时间,即通过最大化训练轮次的长度来最大化总奖励。

3. 网格世界示例

在介绍了 CartPole 环境作为使用 OpenAI Gym 工具包的热身练习后,我们将切换到另一个环境——网格世界环境。这是一个简单的环境,有 m 行和 n 列,以 m = 4 n = 6 为例,该环境有 30 种不同的可能状态。其中有四个终端状态:
- 黄金状态(状态 16) :到达该状态可获得正奖励 +1。
- 陷阱状态(状态 10、15 和 22) :到达这些状态会获得负奖励 -1。

其他所有状态的奖励为 0。智能体总是从状态 0 开始,每次重置环境时,智能体都会回到状态 0。动作空间包括四个方向:上、下、左、右。当智能体位于网格的外边界时,选择会导致其离开网格的动作不会改变状态。

4. 在 OpenAI Gym 中实现网格世界环境

为了通过 OpenAI Gym 试验网格世界环境,强烈建议使用脚本编辑器或集成开发环境(IDE),而不是交互式执行代码。

以下是实现步骤:
1. 创建一个名为 gridworld_env.py 的新 Python 脚本。
2. 导入必要的包和两个用于构建环境可视化的辅助函数。OpenAI Gym 库使用 Pyglet 库进行环境渲染,并提供了方便的包装类和函数,更多详细信息可查看:https://github.com/openai/gym/blob/master/gym/envs/classic_control/rendering.py。

以下是 gridworld_env.py 的代码示例:

import numpy as np
from gym.envs.toy_text import discrete
from collections import defaultdict
import time
import pickle
import os
from gym.envs.classic_control import rendering

CELL_SIZE = 100
MARGIN = 10

def get_coords(row, col, loc='center'):
    xc = (col+1.5) * CELL_SIZE
    yc = (row+1.5) * CELL_SIZE
    if loc == 'center':
        return xc, yc
    elif loc == 'interior_corners':
        half_size = CELL_SIZE//2 - MARGIN
        xl, xr = xc - half_size, xc + half_size
        yt, yb = xc - half_size, xc + half_size
        return [(xl, yt), (xr, yt), (xr, yb), (xl, yb)]
    elif loc == 'interior_triangle':
        x1, y1 = xc, yc + CELL_SIZE//3
        x2, y2 = xc + CELL_SIZE//3, yc - CELL_SIZE//3
        x3, y3 = xc - CELL_SIZE//3, yc - CELL_SIZE//3
        return [(x1, y1), (x2, y2), (x3, y3)]

def draw_object(coords_list):
    if len(coords_list) == 1:  # -> circle
        obj = rendering.make_circle(int(0.45*CELL_SIZE))
        obj_transform = rendering.Transform()
        obj.add_attr(obj_transform)
        obj_transform.set_translation(*coords_list[0])
        obj.set_color(0.2, 0.2, 0.2)  # -> black
    elif len(coords_list) == 3:  # -> triangle
        obj = rendering.FilledPolygon(coords_list)
        obj.set_color(0.9, 0.6, 0.2)  # -> yellow
    elif len(coords_list) > 3:  # -> polygon
        obj = rendering.FilledPolygon(coords_list)
        obj.set_color(0.4, 0.4, 0.8)  # -> blue
    return obj

class GridWorldEnv(discrete.DiscreteEnv):
    def __init__(self, num_rows=4, num_cols=6, delay=0.05):
        self.num_rows = num_rows
        self.num_cols = num_cols
        self.delay = delay
        move_up = lambda row, col: (max(row-1, 0), col)
        move_down = lambda row, col: (min(row+1, num_rows-1), col)
        move_left = lambda row, col: (row, max(col-1, 0))
        move_right = lambda row, col: (
            row, min(col+1, num_cols-1))
        self.action_defs={0: move_up, 1: move_right,
                          2: move_down, 3: move_left}
        nS = num_cols*num_rows
        nA = len(self.action_defs)
        self.grid2state_dict={(s//num_cols, s%num_cols):s
                              for s in range(nS)}
        self.state2grid_dict={s:(s//num_cols, s%num_cols)
                              for s in range(nS)}
        gold_cell = (num_rows//2, num_cols-2)
        trap_cells = [((gold_cell[0]+1), gold_cell[1]),
                       (gold_cell[0], gold_cell[1]-1),
                       ((gold_cell[0]-1), gold_cell[1])]
        gold_state = self.grid2state_dict[gold_cell]
        trap_states = [self.grid2state_dict[(r, c)]
                       for (r, c) in trap_cells]
        self.terminal_states = [gold_state] + trap_states
        print(self.terminal_states)
        P = defaultdict(dict)
        for s in range(nS):
            row, col = self.state2grid_dict[s]
            P[s] = defaultdict(list)
            for a in range(nA):
                action = self.action_defs[a]
                next_s = self.grid2state_dict[action(row, col)]
                if self.is_terminal(next_s):
                    r = (1.0 if next_s == self.terminal_states[0]
                         else -1.0)
                else:
                    r = 0.0
                if self.is_terminal(s):
                    done = True
                    next_s = s
                else:
                    done = False
                P[s][a] = [(1.0, next_s, r, done)]
        isd = np.zeros(nS)
        isd[0] = 1.0
        super(GridWorldEnv, self).__init__(nS, nA, P, isd)
        self.viewer = None
        self._build_display(gold_cell, trap_cells)

    def is_terminal(self, state):
        return state in self.terminal_states

    def _build_display(self, gold_cell, trap_cells):
        screen_width = (self.num_cols+2) * CELL_SIZE
        screen_height = (self.num_rows+2) * CELL_SIZE
        self.viewer = rendering.Viewer(screen_width,  
                                       screen_height)
        all_objects = []
        bp_list = [
            (CELL_SIZE-MARGIN, CELL_SIZE-MARGIN),
            (screen_width-CELL_SIZE+MARGIN, CELL_SIZE-MARGIN),
            (screen_width-CELL_SIZE+MARGIN,
             screen_height-CELL_SIZE+MARGIN),
            (CELL_SIZE-MARGIN, screen_height-CELL_SIZE+MARGIN)
        ]
        border = rendering.PolyLine(bp_list, True)
        border.set_linewidth(5)
        all_objects.append(border)
        for col in range(self.num_cols+1):
            x1, y1 = (col+1)*CELL_SIZE, CELL_SIZE
            x2, y2 = (col+1)*CELL_SIZE, (self.num_rows+1)*CELL_SIZE
            line = rendering.PolyLine([(x1, y1), (x2, y2)], False)
            all_objects.append(line)
        for row in range(self.num_rows+1):
            x1, y1 = CELL_SIZE, (row+1)*CELL_SIZE
            x2, y2 = (self.num_cols+1)*CELL_SIZE, (row+1)*CELL_SIZE
            line=rendering.PolyLine([(x1, y1), (x2, y2)], False)
            all_objects.append(line)
        for cell in trap_cells:
            trap_coords = get_coords(*cell, loc='center')
            all_objects.append(draw_object([trap_coords]))
        gold_coords = get_coords(*gold_cell,
                                 loc='interior_triangle')
        all_objects.append(draw_object(gold_coords))
        if (os.path.exists('robot-coordinates.pkl') and
                CELL_SIZE==100):
            agent_coords = pickle.load(
                open('robot-coordinates.pkl', 'rb'))
            starting_coords = get_coords(0, 0, loc='center')
            agent_coords += np.array(starting_coords)
        else:
            agent_coords = get_coords(
                0, 0, loc='interior_corners')
        agent = draw_object(agent_coords)
        self.agent_trans = rendering.Transform()
        agent.add_attr(self.agent_trans)
        all_objects.append(agent)
        for obj in all_objects:
            self.viewer.add_geom(obj)

    def render(self, mode='human', done=False):
        if done:
            sleep_time = 1
        else:
            sleep_time = self.delay
        x_coord = self.s % self.num_cols
        y_coord = self.s // self.num_cols
        x_coord = (x_coord+0) * CELL_SIZE
        y_coord = (y_coord+0) * CELL_SIZE
        self.agent_trans.set_translation(x_coord, y_coord)
        rend = self.viewer.render(
             return_rgb_array=(mode=='rgb_array'))
        time.sleep(sleep_time)
        return rend

    def close(self):
        if self.viewer:
            self.viewer.close()
            self.viewer = None

以下是该实现的详细说明:
- 使用 lambda 函数定义了四个不同的动作: move_up() move_down() move_left() move_right()
- NumPy 数组 isd 保存了起始状态的概率,当调用父类的 reset() 方法时,会根据这个分布随机选择一个起始状态。由于我们总是从状态 0(网格世界的左下角)开始,所以将状态 0 的概率设为 1.0,其他 29 个状态的概率设为 0.0。
- Python 字典 P 中定义的转移概率,决定了选择一个动作时从一个状态转移到另一个状态的概率,这使得环境具有概率性。为简单起见,我们只使用一个结果,即根据所选动作的方向改变状态。最终,这些转移概率将由 env.step() 函数用于确定下一个状态。
- _build_display() 函数用于设置环境的初始可视化, render() 函数用于显示智能体的移动。

可以在脚本末尾添加以下代码来测试这个实现:

if __name__ == '__main__':
    env = GridWorldEnv(5, 6)
    for i in range(1):
        s = env.reset()
        env.render(mode='human', done=False)
        while True:
            action = np.random.choice(env.nA)
            res = env.step(action)
            print('Action  ', env.s, action, ' -> ', res)
            env.render(mode='human', done=res[2])
            if res[2]:
                break
    env.close()

执行该脚本后,应该可以看到网格世界环境的可视化效果。

在学习过程中,我们并不知道转移概率,目标是通过与环境交互来学习,因此在类定义之外无法访问 P

强化学习:OpenAI Gym 与 Q - 学习实战指南(下)

5. 使用 Q - 学习解决网格世界问题

在了解了强化学习算法的理论和开发过程,并通过 OpenAI Gym 工具包设置好环境后,我们将实现当前最流行的强化学习算法——Q - 学习。这里我们使用之前在 gridworld_env.py 脚本中实现的网格世界示例。

6. 实现 Q - 学习算法

首先,我们创建一个名为 agent.py 的新脚本,在其中定义一个用于与环境交互的智能体:

## Script: agent.py
from collections import defaultdict
import numpy as np

class Agent(object):
    def __init__(
            self, env,
            learning_rate=0.01,
            discount_factor=0.9,
            epsilon_greedy=0.9,
            epsilon_min=0.1,
            epsilon_decay=0.95):
        self.env = env
        self.lr = learning_rate
        self.gamma = discount_factor
        self.epsilon = epsilon_greedy
        self.epsilon_min = epsilon_min
        self.epsilon_decay = epsilon_decay
        ## Define the q_table
        self.q_table = defaultdict(lambda: np.zeros(self.env.nA))

    def choose_action(self, state):
        if np.random.uniform() < self.epsilon:
            action = np.random.choice(self.env.nA)
        else:
            q_vals = self.q_table[state]
            perm_actions = np.random.permutation(self.env.nA)
            q_vals = [q_vals[a] for a in perm_actions]
            perm_q_argmax = np.argmax(q_vals)
            action = perm_actions[perm_q_argmax]
        return action

    def _learn(self, transition):
        s, a, r, next_s, done = transition
        q_val = self.q_table[s][a]
        if done:
            q_target = r
        else:
            q_target = r + self.gamma*np.max(self.q_table[next_s])
        ## Update the q_table
        self.q_table[s][a] += self.lr * (q_target - q_val)
        ## Adjust the epislon
        self._adjust_epsilon()

    def _adjust_epsilon(self):
        if self.epsilon > self.epsilon_min:
            self.epsilon *= self.epsilon_decay

以下是对上述代码的详细解释:
- __init__() 构造函数设置了各种超参数,如学习率、折扣因子( γ )以及 ε - 贪婪策略的参数。最初, ε 的值较高,但 _adjust_epsilon() 方法会逐渐减小它,直到达到最小值 ε_min
- choose_action() 方法根据 ε - 贪婪策略选择动作。通过选择一个随机均匀数来确定是随机选择动作,还是根据动作 - 值函数选择动作。
- _learn() 方法实现了 Q - 学习算法的更新规则。它接收每个转移的元组,包括当前状态( s )、选择的动作( a )、观察到的奖励( r )、下一个状态( s' )以及一个标志,用于确定是否已到达本轮训练的结束。如果标志为训练结束,目标值等于观察到的奖励( r );否则,目标值为 r + γ * max_a Q(s', a)

7. 训练智能体

接下来,我们创建一个新脚本 qlearning.py ,将所有内容整合在一起,并使用 Q - 学习算法训练智能体:

## Script: qlearning.py
from gridworld_env import GridWorldEnv
from agent import Agent
from collections import namedtuple
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(1)

Transition = namedtuple(
    'Transition', ('state', 'action', 'reward',
                   'next_state', 'done'))

def run_qlearning(agent, env, num_episodes=50):
    history = []
    for episode in range(num_episodes):
        state = env.reset()
        env.render(mode='human')
        final_reward, n_moves = 0.0, 0
        while True:
            action = agent.choose_action(state)
            next_s, reward, done, _ = env.step(action)
            agent._learn(Transition(state, action, reward,
                                    next_s, done))
            env.render(mode='human', done=done)
            state = next_s
            n_moves += 1
            if done:
                break
            final_reward = reward
        history.append((n_moves, final_reward))
        print('Episode %d: Reward %.1f #Moves %d'
              % (episode, final_reward, n_moves))
    return history

def plot_learning_history(history):
    fig = plt.figure(1, figsize=(14, 10))
    ax = fig.add_subplot(2, 1, 1)
    episodes = np.arange(len(history))
    moves = np.array([h[0] for h in history])
    plt.plot(episodes, moves, lw=4,
             marker='o', markersize=10)
    ax.tick_params(axis='both', which='major', labelsize=15)
    plt.xlabel('Episodes', size=20)
    plt.ylabel('# moves', size=20)
    ax = fig.add_subplot(2, 1, 2)
    rewards = np.array([h[1] for h in history])
    plt.step(episodes, rewards, lw=4)
    ax.tick_params(axis='both', which='major', labelsize=15)
    plt.xlabel('Episodes', size=20)
    plt.ylabel('Final rewards', size=20)
    plt.savefig('q - learning - history.png', dpi=300)
    plt.show()

if __name__ == '__main__':
    env = GridWorldEnv(num_rows=5, num_cols=6)
    agent = Agent(env)
    history = run_qlearning(agent, env)
    env.close()
    plot_learning_history(history)

执行这个脚本将运行 50 轮 Q - 学习程序,智能体的行为将被可视化。在学习过程开始时,智能体大多会陷入陷阱状态,但随着时间的推移,它会从失败中学习,最终找到黄金状态(例如,在第 7 轮首次找到)。

以下是整个过程的流程图:

graph TD;
    A[开始] --> B[创建网格世界环境];
    B --> C[创建智能体];
    C --> D[进行多轮训练];
    D --> E[每轮训练开始,重置环境];
    E --> F[选择动作];
    F --> G[执行动作,获取新状态和奖励];
    G --> H[更新 Q 表];
    H --> I{是否到达终止状态};
    I -- 否 --> F;
    I -- 是 --> J[记录本轮训练的移动次数和奖励];
    J --> K{是否完成所有轮次};
    K -- 否 --> E;
    K -- 是 --> L[绘制学习历史图];
    L --> M[结束];

总结

本文详细介绍了 OpenAI Gym 工具包,包括如何使用其预定义的环境(如 CartPole 和网格世界),以及如何实现自定义环境。同时,我们还实现了 Q - 学习算法,并使用它来训练智能体在网格世界环境中寻找黄金状态。通过这些示例,我们可以看到强化学习在解决复杂决策问题中的强大能力。

关键步骤总结

步骤 描述 脚本
1 安装 OpenAI Gym 库 pip install gym
2 实现网格世界环境 gridworld_env.py
3 定义 Q - 学习智能体 agent.py
4 训练智能体并绘制学习历史图 qlearning.py

通过按照这些步骤操作,你可以在自己的项目中应用强化学习和 Q - 学习算法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值