OpenAI Gym逐步学习笔记 -- 自定义场景(1)--先学会画出一个自定义图形并移动起来

490 阅读11分钟

关联文章:Pytorch强化学习初学者笔记 -- 实战 -- 使用强化学习(Reinforcement Learning)来让程序自动学习玩乒乓小游戏
(备注:要想顺利实现上面这篇文章功能,必须先一步一步把 OpenAI Gym 耍起来)

  • 友情提醒:
  • [本文涉及 OpenAI Gym 版本是 0.26.2]
  • [本文写于:2024年4月18日,由于 Gym 发展比较迅速,若你看到这篇文章的时间与此时间相隔较远,那本文中的部分代码可能已经不适用于最新的 Gym 了]

前言

  • 我们使用 Gym 目标是训练一个我们想玩的游戏,但是 Gym 使用方法中,最重要的就是先给游戏定义一个训练场景,这个训练场景目前来看并非是通用的,不同的游戏由于玩法规则和游戏对象的不同,那么需要定义的游戏场景也是不同的,现在 Gym 官方提供的游戏场景是有限的且单一的,而且都是一些非常老旧单一的像素游戏,所以我们需要自己为游戏开发训练场景,然后再加上 DQN(深度学习) 或者其他学习算法,才能最终训练出来一个模型,才能实现让程序协助我们玩游戏,当然这套思路同样适用于让程序协助我们做其他事情...;

  • 那么如何才能实现一个游戏训练场景呢,我们先不用想太多,我在摸索过程中,找到了一条适合小白逐步摸索学习的路线

  • 本文便是这条路线的第一步:先让 Gym 为我们绘制一个最简单的图形,并且动起来即可

  • (感兴趣的话,可以关注一波)

  • 参考链接:

【附】pip 国内镜像源记录:

  • 国内要想顺利安装某些国外的工具包,最好还是用国内镜像源比较方便
其他国内镜像源
中国科学技术大学 : https://pypi.mirrors.ustc.edu.cn/simple
豆瓣:http://pypi.douban.com/simple/
阿里云:http://mirrors.aliyun.com/pypi/simple/
# 使用方式如:
pip3 install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple

环境准备(这里不作详细展开,可自行网上搜索)

  • windows11
  • Anaconda -- 使用虚拟环境来控制安装 python 的不同版本如此比较方便,有的时候因为工具包的限制,同一个python版本不一定能适用所有场景
  • python3.9

1、官方案例运行起来

  • 我目前看到的官方文档中以及源码 README.md 中提供的案例都不是最新的,总之最后尝试出来以下代码是能够成功运行官方案例的
# 运行下面脚本之前,需要先安装相关工具包
pip install gym -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install gymnasium -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install gym[CartPole] -i https://pypi.tuna.tsinghua.edu.cn/simple
import gymnasium as gym

# 第二个参数用于控制显示游戏画面的渲染模式 render_mode="human"
env = gym.make("CartPole-v1", render_mode="human")
observation, info = env.reset()

for _ in range(1000):
    action = env.action_space.sample()
    observation, reward, terminated, truncated, info = env.step(action)
    env.render()  # 这行代码会显示游戏画面

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

env.close()
  • 运行效果截图
    image.png

2、使用自定义场景绘制一个蓝色矩形,并成功运行且显示出来

  • 注:在MAC系统上的话,脚本运行起来你会发现观察窗体会闪屏,这对于我们初学者来说并不重要,可以不做任何优化,因为训练过程中即使因为每次 render 导致画面重绘而出现闪屏的问题也是不影响训练结果的,这种无伤大雅的问题我们可以放到今后熟练了再来研究优化
# 运行以下脚本之前,安装如下工具包
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple
  • 【2.1】- 先尝试实现绘制一个最简单的蓝色矩形到画面上(程序能顺利运行即可)
import gymnasium as gym
from gymnasium.spaces import Box
import numpy as np
import pygame

# 注:在MAC系统上的话,运行起来你会发现观察窗体会闪屏,这对于我们初学者来说并不重要,可以不做任何优化,因为训练过程中即使因为每次 render 导致画面重绘而出现闪屏的问题也是不影响训练结果的,这种无伤大雅的问题我们可以放到今后熟练了再来研究优化

class NullActionSpace:
    def sample(self):
        return None

class CustomEnv(gym.Env):
    def __init__(self):
        super(CustomEnv, self).__init__()

        # 定义观测空间
        self.observation_space = Box(low=np.array([0, 0]), high=np.array([100, 20]), dtype=np.float32)

        # 定义动作空间(这里暂时不需要行为,整个空壳即可)
        self.action_space = NullActionSpace()

        # 定义游戏场景
        # 观察窗体尺寸
        self.screen_width = 300
        self.screen_height = 300
        # 矩形参数
        self.rect_width = 100
        self.rect_height = 20
        self.rect_x = 0
        self.rect_y = 0
        self.rect_color = (0, 0, 255) # 蓝色

        # 设置观察屏幕大小
        self.screen = None

    def reset(self):
        # 重置游戏场景,这里只需要返回一个初始观察值
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        self.render()
        return observation, {}

    def step(self, action=0):
        # 根据动作更新游戏状态,这里只是返回原始观察值
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        reward = 0
        terminated = False
        truncated = False
        self.render()
        return observation, reward, terminated, truncated, {}

    def render(self):
        # 渲染游戏场景
        self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        self.screen.fill((255, 255, 255))  # 白色背景
        pygame.draw.rect(self.screen, self.rect_color, (0, 0, self.rect_width, self.rect_height))
        pygame.display.flip()



# -----------------------------train-----------------------------------------
env = CustomEnv()
observation, info = env.reset()

for _ in range(1000):
    action = env.action_space.sample() # 这里不需要任何动作,只需要渲染
    observation, reward, terminated, truncated, info = env.step(action)
    env.render()

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


env.close()

    • 效果截图
      image.png
  • 【2.2】- 稍作优化:加入缓冲区,优化频繁初始化和渲染频率

    • 后面的代码也都是基于这份代码的基础上进行逐步扩展功能的
import gymnasium as gym
from gymnasium.spaces import Box
import numpy as np
import pygame
import time

# 注:在MAC系统上的话,运行起来你会发现观察窗体会闪屏,这对于我们初学者来说并不重要,可以不做任何优化,因为训练过程中即使因为每次 render 导致画面重绘而出现闪屏的问题也是不影响训练结果的,这种无伤大雅的问题我们可以放到今后熟练了再来研究优化

# 先不管行为空间,给一个空壳即可
class NullActionSpace:
    def sample(self):
        return None

# 自定义游戏场景
class CustomEnv(gym.Env):
    def __init__(self):
        super(CustomEnv, self).__init__()
        
        # 定义观测空间
        self.observation_space = Box(low=np.array([0, 0]), high=np.array([100, 20]), dtype=np.float32)
        
        # 定义动作空间
        self.action_space = NullActionSpace()
        
        # 定义游戏场景
        # 观察窗体尺寸
        self.screen_width = 300
        self.screen_height = 300
        # 矩形参数
        self.rect_width = 100
        self.rect_height = 20
        # 把矩形绘制到底部且位于中间位置
        self.rect_x = self.screen_width // 2 - self.rect_width // 2
        self.rect_y = self.screen_height - self.rect_height
        self.rect_color = (0, 0, 255) # 蓝色

        # 创建缓冲 Surface (用于渲染画面)
        self.buffer_surface = pygame.Surface((self.screen_width, self.screen_height))

        self.screen = None
        self.clock = pygame.time.Clock()
        self.fps = 30  # 设置帧率为 30 FPS
        
    def reset(self):
        # 重置游戏场景,这里只需要返回一个初始观察值
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        self.render()
        return observation, {}
    
    def step(self, action):
        # 根据动作更新游戏状态,这里只是返回原始观察值(这一步不涉及动作)
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        reward = 0
        terminated = False
        truncated = False
        self.render()
        return observation, reward, terminated, truncated, {}

    def render(self):
        if not pygame.get_init():
            pygame.init()
        if self.screen is None:
            self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        
        # 绘制蓝色矩形到缓冲 Surface 上
        pygame.draw.rect(self.buffer_surface, self.rect_color, (self.rect_x, self.rect_y, self.rect_width, self.rect_height))
        
        # 将缓冲 Surface 一次性绘制到屏幕上
        self.screen.blit(self.buffer_surface, (0, 0))
        pygame.display.update() # 更新画面
        time.sleep(0.05) # 强制控制渲染速度,当然也可以不做这一步
        self.clock.tick(self.fps)

# -----------------------------train-----------------------------------------
env = CustomEnv()
observation, info = env.reset()

for _ in range(1000):
    action = env.action_space.sample() # 这里不需要任何动作,只需要渲染
    observation, reward, terminated, truncated, info = env.step(action)
    env.render()

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

env.close()
  • 运行效果截图
    image.png

3、往自定义场景中添加行为,让矩形动起来

  • 以下代码中注释里带有 <====== 箭头的就是这次添加的内容
import gymnasium as gym
from gymnasium.spaces import Box, Discrete
import numpy as np
import pygame
import time

# 注:在MAC系统上,运行起来你会发现观察窗体会闪屏,这对于我们初学者来说并不重要,可以不做任何优化,因为训练过程中即使因为每次 render 导致画面重绘而出现闪屏的问题也是不影响训练结果的,这种无伤大雅的问题我们可以放到今后熟练了再来研究优化

# 这个空壳行为不需要了,我会使用到 Discrete 建立行为参数
# class NullActionSpace:
#     def sample(self):
#         return None

class CustomEnv(gym.Env):
    def __init__(self):
        super(CustomEnv, self).__init__()
        
        # 定义观测空间
        self.observation_space = Box(low=np.array([0, 0]), high=np.array([100, 20]), dtype=np.float32)
        
        # 定义动作空间 <======
        self.action_space = Discrete(3) # 0:左移, 1:右移, 2:不动
        
        # 定义游戏场景
        # 观察窗体尺寸
        self.screen_width = 300
        self.screen_height = 300
        # 矩形参数
        self.rect_width = 100
        self.rect_height = 20
        self.rect_x = self.screen_width // 2 - self.rect_width // 2
        self.rect_y = self.screen_height - self.rect_height
        self.rect_color = (0, 0, 255) # 蓝色

        # 创建缓冲 Surface (用于渲染画面)
        self.buffer_surface = pygame.Surface((self.screen_width, self.screen_height))

        self.screen = None
        self.clock = pygame.time.Clock()
        self.fps = 30  # 设置帧率为 30 FPS
        
    def reset(self):
        # 重置游戏场景,这里只需要返回一个初始观察值
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        self.render()
        return observation, {}
    
    def step(self, action=0):
        # 根据动作更新游戏状态,这里只是返回原始观察值

        # 根据动作更新蓝色矩形的位置(在 render 函数中会根据 rect_x rect_y rect_width rect_height 重绘) <======
        if action == 0:  # 左移
            self.rect_x = max(0, self.rect_x - 10)
        elif action == 1:  # 右移
            self.rect_x = min(self.screen_width - self.rect_width, self.rect_x + 10)

        # --
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        reward = 0
        terminated = False
        truncated = False
        self.render()
        return observation, reward, terminated, truncated, {}

    def render(self):
        if not pygame.get_init():
            pygame.init()
        if self.screen is None:
            self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
        
        # 绘制白色背景矩形 <====== 一定要绘制这个白色背景,否则默认的黑色属于无背景,重绘矩形时会产生图形滞留(即你会看到矩形在变长而不是在左右移动)
        self.buffer_surface.fill((255, 255, 255))
        
        # 绘制蓝色矩形到缓冲 Surface 上
        pygame.draw.rect(self.buffer_surface, self.rect_color, (self.rect_x, self.rect_y, self.rect_width, self.rect_height))
        
        # 将缓冲 Surface 一次性绘制到屏幕上
        self.screen.blit(self.buffer_surface, (0, 0))
        pygame.display.update()
        time.sleep(0.05) # 强制控制渲染速度,当然也可以不做这一步
        self.clock.tick(self.fps)

# -----------------------------train-----------------------------------------
env = CustomEnv()
observation, info = env.reset()

for _ in range(1000):
    action = env.action_space.sample() # 这里不需要任何动作,只需要渲染
    observation, reward, terminated, truncated, info = env.step(action)
    env.render()

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

env.close()
  • 运行效果截图(运行起来后会发现矩形在左右移动了)
  • 可能这里你会产生一点疑问:如果是实际训练的话左右移动怎么控制呢?
  • 我用目前的理解程度简单粗暴的解释一点点:行为控制是由程序自己控制的,我们后续要添加想加分或减分之类的算法之后,程序可以根据算法中的得分自动去计算得出该做什么行为最合适,而我们目前的例子并没有游戏规则(根本就是一个图形绘制而已),所以不用想这方面,一步一步来不用心急
    image.png

【附】:把白色背景替换成屏幕某一个区域截图

  • 其实本文的案例就是上面的内容了,这一部分附加内容是我个人的记录而已
  • 主要呢,我后续要做的游戏训练是通过截取已经运行起来的程序窗体的截图,再通过 yolov8 去识别出截图中的目标物体,然后让自定义的游戏场景中根据 yolov8 识别出的目标物体来绘制游戏场景中的相关图形
  • 所以呢,我这里尝试下如何在自定义场景中实现绘制截图的功能
import gymnasium as gym
from gymnasium.spaces import Box, Discrete
import numpy as np
import pygame
import time
# pip install pillow
from PIL import Image, ImageGrab

# 注:在MAC系统上,运行起来你会发现观察窗体会闪屏,这对于我们初学者来说并不重要,可以不做任何优化,因为训练过程中即使因为每次 render 导致画面重绘而出现闪屏的问题也是不影响训练结果的,这种无伤大雅的问题我们可以放到今后熟练了再来研究优化

class NullActionSpace:
    def sample(self):
        return None

class CustomEnv(gym.Env):
    def __init__(self):
        super(CustomEnv, self).__init__()
        
        # 定义观测空间
        self.observation_space = Box(low=np.array([0, 0]), high=np.array([100, 20]), dtype=np.float32)
        
        # 定义动作空间
        self.action_space = Discrete(3) # 0:左移, 1:右移, 2:不动
        
        # 定义游戏场景
        # 观察窗体尺寸
        self.screen_width = 300
        self.screen_height = 300
        # 矩形参数
        self.rect_width = 100
        self.rect_height = 20
        self.rect_x = self.screen_width // 2 - self.rect_width // 2
        self.rect_y = self.screen_height - self.rect_height
        self.rect_color = (0, 0, 255) # 蓝色

        # 加载背景图片
        # self.background_image = pygame.image.load("1.jpg")
        # self.background_image = pygame.transform.scale(self.background_image, (self.screen_width, self.screen_height))

        # 创建缓冲 Surface
        self.buffer_surface = pygame.Surface((self.screen_width, self.screen_height))

        self.screen = None
        self.clock = pygame.time.Clock()
        self.fps = 30  # 设置帧率为 30 FPS
        
    def reset(self):
        # 重置游戏场景,这里只需要返回一个初始观察值
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        self.render()
        return observation, {}
    
    def step(self, action=0):
        # 根据动作更新游戏状态,这里只是返回原始观察值

        # 根据动作更新蓝色矩形的位置
        if action == 0:  # 左移
            self.rect_x = max(0, self.rect_x - 10)
        elif action == 1:  # 右移
            self.rect_x = min(self.screen_width - self.rect_width, self.rect_x + 10)

        # --
        self.screen = None
        observation = np.array([0, 0], dtype=np.float32)
        reward = 0
        terminated = False
        truncated = False
        self.render()
        return observation, reward, terminated, truncated, {}

    def render(self):
        if not pygame.get_init():
            pygame.init()
        if self.screen is None:
            self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))

        # 截取屏幕区域
        screenshot = ImageGrab.grab(bbox=(0, 0, self.screen_width, self.screen_height))
        # 将截图转换为 Numpy 数组
        image_array = np.array(screenshot)
        # 确保数组的形状为 (width, height, 3)
        if len(image_array.shape) == 2:
            # 如果是灰度图,则扩展为三通道
            image_array = np.dstack((image_array, image_array, image_array))
        elif image_array.shape[2] == 4:
            # 如果是带 alpha 通道,则删除 alpha 通道
            image_array = image_array[:, :, :3]
        # 将 Numpy 数组转换为 Pygame Surface
        image_surface = pygame.surfarray.make_surface(image_array.transpose((1, 0, 2)))
        
        # 将背景图片绘制到缓冲 Surface 上
        # self.buffer_surface.blit(self.background_image, (0, 0))
        self.buffer_surface.blit(image_surface, (0, 0))
        
        # 绘制蓝色矩形到缓冲 Surface 上
        pygame.draw.rect(self.buffer_surface, self.rect_color, (self.rect_x, self.rect_y, self.rect_width, self.rect_height))
        
        # 将缓冲 Surface 一次性绘制到屏幕上
        self.screen.blit(self.buffer_surface, (0, 0))
        pygame.display.update()
        time.sleep(0.05)
        self.clock.tick(self.fps)

# -----------------------------train-----------------------------------------
env = CustomEnv()
observation, info = env.reset()

for _ in range(1000):
    action = env.action_space.sample() # 这里不需要任何动作,只需要渲染
    observation, reward, terminated, truncated, info = env.step(action)
    env.render()

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

env.close()
  • 运行效果截图
    image.png

总结

  • OpenAI Gym 这第一步顺利耍起来之后,就大体明白游戏训练大概是怎么一回事了,那接下来就是要把游戏场景真正建立起来了,往场景中添加其他物体,从而建立游戏规则,这就能让学习算法有所学习依据了
  • 感兴趣的话就关注起来吧!