【强化学习系列】Gym库使用——创建自己的强化学习环境1:单一环境创建测试+简单环境可视化
本文基于gym官方文档尝试创建自己的单一强化学习环境
目录
在强化学习中实操中,有两个非常重要的设计模块,一个是模型网络和算法的设计,另一个则是用于智能体交互的强化学习环境搭建。Gym 库凭借其标准化和兼容性好的接口、丰富多样的环境库、易于定制和扩展的能力、完善的文档和易用性、高效的集成模拟器和图像处理支持、以及活跃的研究生态系统成为了强化学习研究和开发的首选工具。
Gym官方文档介绍地址:https://gymnasium.farama.org/
本文记录自建Gym强化学习环境中遇到的问题和解决,本文将按照源码Gym类的创建逻辑展开,并将单个环境过渡到矢量化多个环境同时运行以加速训练。
一、Gym类创建单一环境
1.gym类初始化 __init__()
在初始化gym自建环境类中,关键是定义环境的动作空间(self.action_space)和状态空间(self.observation_space)
在下图中,自定义一个关于目标检测框二次调整移动的强化学习gym环境,动作空间是四个对框的调整移动,状态空间是当前图片上的目标检测框四坐标。
在自定义 Gym 环境时,action_space(动作空间)定义了智能体(代理)可以选择的所有可能动作。动作空间决定了智能体能够做出的决策范围。observation_space(状态空间)定义了环境可以返回给智能体(代理)的所有可能状态或观测值。状态空间决定了智能体所能感知到的环境信息范围。
Gym 提供了多种动作空间和状态空间类型,每种类型都有其独特的参数和使用场景。两者本质上使用的都是gym.Space这个类的定义功能,下述将其合并记录。
此部分官方文档地址:Gym.Space官方文档介绍
① Discrete 空间
用于表示一个离散的动作或状态空间,有限个动作或状态,每个用一个整数索引表示。
参数 n 表示动作的总数。如定义一个在二维空间中移动的小球,可以定义其动作为移动 {上:0,下:1,左:2,右:3} 。或者定义一个3x3的网格状态空间,总共可能的格子数为9。
from gym import spaces
# 定义一个4动作离散动作空间, 如移动:上、下、左、右
action_space = spaces.Discrete(4)
# 定义一个9状态的状态空间,如3x3网格:九个格子对应9种情况
observation_space = spaces.Discrete(9)
② Box 空间
用于表示一个连续的动作或状态空间。用于需要连续动作的任务,如机器人手臂的操作,金融交易决策。或用于一个连续的状态空间,如空间物理位置、传感器读数、速度等。
参数 low 表示每个维度的最小值,high 表示每个维度的最大值。例如假设机器人手臂最大转动角度为一圈,那么此时动作空间中 low=0 代表0度;high=360 代表360度一圈。如温度传感器的最大温度限制为200摄氏度,最低为-50摄氏度,那么状态空间 low=-50,high=200。
参数 shape 表示动作空间维度, dtype 表示使用数据类型。
from gym import spaces
import numpy as np
# 定义一个二维连续动作空间,每个维度的取值范围是 [-1.0, 1.0]
action_space = spaces.Box(low=np.array([-1.0, -1.0]), high=np.array([1.0, 1.0]), dtype=np.float32)
# 定义一个三维连续状态空间,每个维度的取值范围分别是 [-1.0, 1.0], [0, 5], [0, 10]
observation_space = spaces.Box(low=np.array([-1.0, 0.0, 0.0]), high=np.array([1.0, 5.0, 10.0]), dtype=np.float32)
③ MultiDiscrete 空间
用于表示多个独立的离散动作或状态空间组合,如在环境中智能体需要同时控制多个变量——方向、速度(三维空间中小车移动),每个属性对应于一个不同的离散取值。或者环境存在多个维度,如三维离散空间。
参数 n 是一个整数数组,代表每个维度动作空间的取值范围。如移动方向四个(上下左右),速度四个(一倍速:0,两倍速:1,三倍速:2, 四倍速:3)。那么参数输入就是[4,4]。或者说对于3x3网格要分横竖两个方向的状态空间,那么可以理解为x,y轴两个方向分别对应三个可能情况,参数输入[3,3]
from gym import spaces
# 定义一个两个维度的多离散动作空间,每个维度有4种可能的动作
action_space = spaces.MultiDiscrete([4,4])
# 定义一个二维离散状态空间
observation_space = spaces.MultiDiscrete([3,3])
④ MultiBinary 空间
用于多个独立的二元变量动作或状态组合,如多个开关或布尔运算的组合。
参数 n 表示二元变量(开关)的数量
from gym import spaces
# 定义一个有3个二元变量的动作空间
action_space = spaces.MultiBinary(3)
# 定义一个有2个二元变量的状态空间
observation_space = spaces.MultiBinary(2)
⑤ Tuple 空间
这是一个更加自由的定义动作和状态空间的方式,允许多个不同类型的动作和状态空间组合,如一个动作可以由一个离散动作或状态和一个连续控制或连续空间组合。
参数是一个元组,元组包含多个 gym.Space 对象。
from gym import spaces
# 定义一个动作空间,由一个离散空间和一个连续空间组合而成
action_space = spaces.Tuple((spaces.Discrete(3), spaces.Box(low=0, high=1, shape=(2,), dtype=np.float32)))
# 定义一个状态空间,由一个离散空间和一个连续空间组合而成
observation_space = spaces.Tuple((spaces.Discrete(3), spaces.Box(low=0, high=1, shape=(2,), dtype=np.float32)))
⑥ Dict 空间
用于表示一个字典格式动作空间,可以更自由的灵活定义多维复杂动作结构。
参数也是 gym.Space 对象,用字典形式输入。
from gym import spaces
# 定义一个字典动作空间,包含一个离散空间和一个连续空间
action_space = spaces.Dict({
'discrete_action': spaces.Discrete(5),
'continuous_action': spaces.Box(low=-1, high=1, shape=(3,), dtype=np.float32)
})
# 定义一个字典状态空间,包含一个离散空间和一个连续空间
observation_space = spaces.Dict({
'discrete_state': spaces.Discrete(5),
'continuous_state': spaces.Box(low=-1, high=1, shape=(3,), dtype=np.float32)
})
上述是gym库支持的一般空间形式,在后续 gymnasium 分支更新中,又更新了新的空间表示形式以适应新的强化学习环境要求。记录在下。
⑦ Text 空间
在Gymnasium库中,Text空间专门用于处理文本数据的空间类型。用于自然语言处理相关的强化学习任务,如文字游戏、对话生成等。通常此类多用于状态空间表示。
参数min_length 和参数 max_length 表示文本的最小最大长度,是一个整数,指定空间中允许出现的最小最大字符数。参数charset表示文本的字符集或词汇表。
from gymnasium.spaces import Text
# 示例 1:创建一个最大长度为 10,字符集为默认 ASCII 的 Text 空间
text_space = Text(max_length=10)
# 示例 2:创建一个最大长度为 5,自定义字符集为 'abcdef' 的 Text 空间
text_space_custom = Text(max_length=5, charset="abcdef")
# 示例 3:创建一个最大长度为 7,词汇表为 ["hello", "world"] 的 Text 空间
text_space_words = Text(max_length=7, charset=["hello", "world"])
print("Default Text Space:", text_space)
print("Custom Character Set Text Space:", text_space_custom)
print("Custom Word Set Text Space:", text_space_words)
本文记录的是目标检测框的强化学习环境搭建,其中动作空间代表框的移动是一个四动作的离散空间,状态空间是图片中的目标框box坐标,因此是一个(图片框总数,4)的四维连续空间,其每一维度取值范围是0到图片的高或宽
import pygame
import gym
from gym import spaces
import numpy as np
class RL_Env(gym.Env):
def __init__(self, image_path, render_mode=None):
super(RL_Env, self).__init__()
# 读取当前图像信息
self.img = pygame.image.load(image_path)
self.width, self.height = self.img.get_width(), self.img.get_height()
# 创建动作和状态空间范围
self.action_space = spaces.Discrete(4)
self.observation_space = spaces.Box(low=np.array([0,0,0,0],dtype=np.float32), high=np.array([self.width, self.height,
self.width, self.height],dtype=np.float32), shape=(4,), dtype=np.float32)
# 定义模式:人类可视化or机器人训练
self.render_mode = render_mode
self.window = None # 可视化窗口
self.clock = None # 可视化时钟
self.window_size = (600,800) # 窗口大小
self.background = None # 背景图
self.scale_x = None # x横轴缩放比
self.scale_y = None # y竖轴缩放比
2.gym类初始状态 reset()
这里的关键就是初始化整个游戏的基础界面信息,对于每个不同的环境或项目,其定义可能是仁者见仁智者见智,此处仅记录基于目标检测框移动的项目代码。
因为状态空间就是对应当前图片的一个已知的框(目标是优化当前框,提供精度),因此只需从外部读取框信息并加载到状态空间中即可。对于其他的环境按住实际需求编写代码即可。
如果选择‘human’模式,就调用pygame可视化函数 self.render() 。
class RL_Env(gym.Env):
# 重置环境——读取图片和框信息
def reset(self, json_path):
# 读取外部数据
with open(box_path, 'r') as f:
box_dict_list = json.load(f)
box_list = []
for box_dict in box_dict_list:
box = box_dict['box']
box_list.append(box)
# 框构成当前状态空间
self.state = np.array(box_list,dtype=np.float32).reshape(-1,4)
# 如果是‘human’模式,则初始可视化
if self.render_mode == 'human':
self.background = pygame.transform.scale(self.img, self.window_size) # 设置背景图
# 计算x和y方向的缩放比例
self.scale_x = self.window_size[0]/ self.width
self.scale_y = self.window_size[1] / self.height
self.render()
return self.state
3.gym类渲染可视化 render()
在初始化函数中使用了可视化函数 render() ,可视化分为几个步骤:1.设置窗口,2.填充背景,3.描绘状态,4.完整展示。对于本文例子中,背景就是目标检测的图片,而变化的状态就是图上的框,因此可以编写一个十分简单的可视化函数。
其中self.clock.tick()控制每张图片显示的时长(实际是帧率,这里方便理解),参数调的越小,每一张图停留的时间越长。
class RL_Env(gym.Env):
def render(self):
# 初始化窗口和时钟
if self.window is None and self.render_mode == 'human':
pygame.init()
pygame.display.init()
self.window = pygame.display.set_mode(self.window_size)
if self.clock is None and self.render_mode == 'human':
self.clock = pygame.time.Clock()
# 重新绘制背景,以清除上一层
if self.background is not None:
self.window.blit(self.background,(0,0))
# 绘制框
for box in list(self.state):
rect = [box[0], box[1], abs(box[2]-box[0]), abs(box[3]-box[1])]
# 根据x和y方向的缩放比例计算每个矩形框的新位置
stretched_rectangle = [
rect[0] * self.scale_x, # x 坐标
rect[1] * self.scale_y, # y 坐标
rect[2] * self.scale_x, # 宽度
rect[3] * self.scale_y # 高度
]
pygame.draw.rect(self.window, (0, 255, 0), stretched_rectangle, 1) # 绘制矩形框,线宽为2
# 更新显示
pygame.display.flip()
self.clock.tick(2)
4.gym类运行核心 step()
本部分也是需要根据实际需求编写代码,step() 是整个环境更新的核心部分,影响着模型动作与环境的交互。总体来说,可以分为三部分思考此处代码架构。
a.输入动作与环境的交互设计——强化学习的模型输入的动作序列如何影响环境中的状态空间,如移动框中坐标信息的改变。
b.从动作之前状态到之后状态奖励设计——强化学习的关键就是奖励函数的设计,机器动作传入后是赢了游戏达到目标(正奖励)还是“一事无成”甚至使得状况更坏了(负奖励)。
c.终止条件设计——什么时候游戏到头了,或者哪些条件可以判断“角色挂了”,要设置当前环境的退出条件机制。
本文只简单实验环境搭建,占不写出具体的奖励逻辑等,终止条件也按最简单的到达一定步数时,退出环境。
class RL_Env(gym.Env):
def step(self, action): # 动作序列{0:上扩, 1:上缩, 2:下扩, 3:下缩}
# 根据action生成移动数组
movement_np = np.zeros_like(self.state,dtype=np.float32)
for i,act in enumerate(action):
if act == 0:
movement_np[i,1] = 100
elif act == 1:
movement_np[i,1] = -100
elif act == 2:
movement_np[i,3] = 100
elif act == 3:
movement_np[i,3] = -100
# 移动当前状态框
self.state += movement_np
self.state = np.clip(self.state, self.observation_space.low, self.observation_space.high)
return self.state
5.gym类运行
有了上述所有功能的准备,于是可以测试运行强化学习环境了。首先需要准备环境搭建的数据集——1.图片jpg数据;2.检测框json数据
图片上还是使用coco8中的图片,使用官方yolo8m模型预测目标框并保存为json格式。建立下图文件夹,运行下面代码得到结果。
from ultralytics import YOLO
import os
import json
jpg_file = './jpg'
save_file = './box'
model = YOLO(model='./yolo/yolov8m.pt')
for file in os.listdir(jpg_file):
base = file.split('.')[0]
savedir = os.path.join(save_file, base+'.json')
jpgdir = os.path.join(jpg_file, file)
pred = model.predict(jpgdir)
boxes = pred[0].boxes.xyxy
boxes_list = [[int(r[0]), int(r[1]), int(r[2]), int(r[3])] for r in boxes]
box_dict_list = []
for i,box in enumerate(boxes_list):
box_dict = dict()
box_dict['id'] = i
box_dict['box'] = box
box_dict_list.append(box_dict)
with open(savedir,'w') as f:
json.dump(box_dict_list, f)
有了数据支持,就可以实例化环境类并将本地图片和框传入环境并可视化展示了。这里选择随机生成动作序列,动作action是一个列表,其中元素与图片内的框一一对应。对框做50次修改以后退出环境。
if __name__=='__main__':
# 加载本地图片
image_path = './jpg/000000000025.jpg' # 替换为你的图片路径
# 加载本地框
box_path = './box/000000000025.json'
env = RL_Env(image_path, render_mode='human')
state = env.reset(box_path)
for epid in range(50):
action = np.random.choice([0,1,2,3],size=state.shape[0])
state = env.step(action)
env.render()
方便复制使用的完整代码如下。
import json
import pygame
import gym
from gym import spaces
import numpy as np
class RL_Env(gym.Env):
def __init__(self, image_path, render_mode=None):
super(RL_Env, self).__init__()
# 读取当前图像信息
self.img = pygame.image.load(image_path)
self.width, self.height = self.img.get_width(), self.img.get_height()
# 创建动作和状态空间范围
self.action_space = spaces.Discrete(4)
self.observation_space = spaces.Box(low=np.array([0,0,0,0],dtype=np.float32), high=np.array([self.width, self.height,
self.width, self.height],dtype=np.float32), shape=(4,), dtype=np.float32)
# 定义模式:人类可视化or机器人训练
self.render_mode = render_mode
self.window = None # 可视化窗口
self.clock = None # 可视化时钟
self.window_size = (600,600) # 窗口大小
self.background = None # 背景图
self.scale_x = None # x横轴缩放比
self.scale_y = None # y竖轴缩放比
# 重置环境——读取图片和框信息
def reset(self, json_path):
with open(box_path, 'r') as f:
box_dict_list = json.load(f)
box_list = []
for box_dict in box_dict_list:
box = box_dict['box']
box_list.append(box)
self.state = np.array(box_list,dtype=np.float32).reshape(-1,4)
if self.render_mode == 'human':
self.background = pygame.transform.scale(self.img, self.window_size) # 设置背景图
# 计算x和y方向的缩放比例
self.scale_x = self.window_size[0]/ self.width
self.scale_y = self.window_size[1] / self.height
self.render()
return self.state
def step(self, action): # 动作序列{0:上扩, 1:上缩, 2:下扩, 3:下缩}
# 根据action生成移动数组
movement_np = np.zeros_like(self.state,dtype=np.float32)
for i,act in enumerate(action):
if act == 0:
movement_np[i,1] = 10
elif act == 1:
movement_np[i,1] = -10
elif act == 2:
movement_np[i,3] = 10
elif act == 3:
movement_np[i,3] = -10
# 移动当前状态框
self.state += movement_np
self.state = np.clip(self.state, self.observation_space.low, self.observation_space.high)
return self.state
def render(self):
# 初始化窗口和时钟
if self.window is None and self.render_mode == 'human':
pygame.init()
pygame.display.init()
self.window = pygame.display.set_mode(self.window_size)
if self.clock is None and self.render_mode == 'human':
self.clock = pygame.time.Clock()
# 重新绘制背景,以清除上一层
if self.background is not None:
self.window.blit(self.background,(0,0))
# 绘制框
for box in list(self.state):
rect = [box[0], box[1], abs(box[2]-box[0]), abs(box[3]-box[1])]
# 根据x和y方向的缩放比例计算每个矩形框的新位置
stretched_rectangle = [
rect[0] * self.scale_x, # x 坐标
rect[1] * self.scale_y, # y 坐标
rect[2] * self.scale_x, # 宽度
rect[3] * self.scale_y # 高度
]
pygame.draw.rect(self.window, (0, 255, 0), stretched_rectangle, 1) # 绘制矩形框,线宽为2
# 更新显示
pygame.display.flip()
self.clock.tick(2)
def close(self):
pygame.quit()
if __name__=='__main__':
# 加载本地图片
image_path = './jpg/000000000025.jpg' # 替换为你的图片路径
# 加载本地框
box_path = './box/000000000025.json'
env = RL_Env(image_path, render_mode='human')
state = env.reset(box_path)
for epid in range(50):
action = np.random.choice([0,1,2,3],size=state.shape[0])
state = env.step(action)
env.render()
本篇到此结束。在下一篇中,将根据官方源码环境修改规范化此处环境代码。【强化学习系列】Gym库使用——创建自己的强化学习环境2:拆解官方标准模型源码/规范自定义类+打包自定义环境 后续也将记录使用矢量化环境的操作。在强化学习训练中如果一步只对应一张图片一个环境,运行是十分慢的,而且也没有发挥神经网络小批量处理的加速——即一次只输出批量为1的动作,因为当前一个环境只能接受一个动作。因此矢量化环境本质上就是同时加载多个相同或不同的图片环境与模型输出的多批次动作进行交互。
还有奖励函数也是强化学习中非常关键的设计,好的奖励函数才能引导agent的学习。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)