强化学习实战4——自定义环境的搭建

我们之前写了自定义环境下的Predator游戏,但是如果要用BaseLine3的库来训练,就需要满足一些接口规范。接下来我们严格参考BL3的定义规范我们的环境,方便接下来的DQN训练。

【课程14:编写强化学习环境进阶】 https://www.bilibili.com/video/BV1bY41147Fz/?share_source=copy_web\&vd_source=2c56c6a2645587b49d62e5b12b253dca

Gymnasium 环境简介

https://gymnasium.farama.org/api/env/

Gymnasium 环境(Env)的核心标准 API 非常简洁,主要由 4 个核心函数2 个核心属性 组成。这是所有强化学习任务交互的基础。

🧠 1. 核心交互函数

这些是你在训练循环(Training Loop)中必须调用的函数。

函数名 参数/说明 返回值 (Tuple)
.reset() (重置环境) 作用 :在每个回合(Episode)开始前调用,将环境恢复到初始状态。 参数seed (可选,用于复现实验结果), options (特定环境的额外参数)。 1. observation :初始环境状态(符合 observation_space 定义)。 2. info:辅助诊断信息(字典)。
.step(action) (执行动作) 作用 :这是强化学习的核心。将 Agent 的动作(Action)传入环境,环境反馈下一步的状态和奖励。 参数action (Agent 选择的动作)。 1. observation :执行动作后的新环境状态。 2. reward :该动作获得的奖励(浮点数)。 3. terminated :布尔值。True 表示回合正常结束(如到达目标/坠毁)。 4. truncated :布尔值。True 表示回合被强制截断(如超时/出界)。 5. info:辅助诊断信息。
.render() (渲染画面) 作用 :将环境的当前状态可视化。 注意 :在 gymnasium.make() 时需指定 render_mode(如 "human", "rgb_array")。 根据 render_mode 不同而不同: * "human":通常返回 None (直接在窗口显示)。 * "rgb_array":返回图像帧 np.ndarray。 * "ansi":返回文本字符串。
.close() (关闭环境) 作用 :释放环境占用的资源(如关闭 Pygame 窗口、数据库连接等)。 建议:在脚本结束或训练完成后调用。 None

📏 2. 核心属性 (Spaces)

在编写代码前,你需要通过这两个属性来了解环境的输入输出规格:

  • .action_space

    • 含义:定义了 Agent 可以采取的所有合法动作的范围。
    • 用途 :用于构建 Agent 的输出层。例如,如果是 Discrete(4),说明有 4 个离散动作(如 Lunar Lander 的 0, 1, 2, 3)。
    • 常用方法sample() (随机采样一个动作)。
  • .observation_space

    • 含义:定义了环境状态(观测值)的数据结构和范围。
    • 用途 :用于构建 Agent 的输入层。例如,Box(4,) 表示一个包含 4 个浮点数的数组。
    • 常用方法sample() (随机采样一个观测值,常用于测试)。

🛠️ 3. 辅助属性与函数

  • .metadata :包含环境的元信息,比如支持的渲染模式 (render_modes) 和帧率 (render_fps)。
  • .spec :环境的配置规格,通常在通过 gymnasium.make() 创建时生成。
  • .np_random:环境内部的随机数生成器,用于保证实验的可复现性 (Reproducibility)。

📝 总结代码模板

一个标准的 自定义环境类 模板长这样:

https://stable-baselines3.readthedocs.io/en/master/guide/custom_env.html

python 复制代码
import gymnasium as gym
import numpy as np
from gymnasium import spaces


class CustomEnv(gym.Env):
    """Custom Environment that follows gym interface."""

    metadata = {"render_modes": ["human"], "render_fps": 30}

    def __init__(self, arg1, arg2, ...):
        super().__init__()
        # Define action and observation space
        # They must be gym.spaces objects
        # Example when using discrete actions:
        self.action_space = spaces.Discrete(N_DISCRETE_ACTIONS)
        # Example for using image as input (channel-first; channel-last also works):
        self.observation_space = spaces.Box(low=0, high=255,
                                            shape=(N_CHANNELS, HEIGHT, WIDTH), dtype=np.uint8)

    def step(self, action):
        ...
        return observation, reward, terminated, truncated, info

    def reset(self, seed=None, options=None):
        ...
        return observation, info

    def render(self):
        ...

    def close(self):
        ...

官方提供的完整例子:

python 复制代码
class IdentityEnv(gym.Env, Generic[T]):
    def __init__(self, dim: int | None = None, space: spaces.Space | None = None, ep_length: int = 100):
        """
        Identity environment for testing purposes

        :param dim: the size of the action and observation dimension you want
            to learn. Provide at most one of ``dim`` and ``space``. If both are
            None, then initialization proceeds with ``dim=1`` and ``space=None``.
        :param space: the action and observation space. Provide at most one of
            ``dim`` and ``space``.
        :param ep_length: the length of each episode in timesteps
        """
        if space is None:
            if dim is None:
                dim = 1
            space = spaces.Discrete(dim)
        else:
            assert dim is None, "arguments for both 'dim' and 'space' provided: at most one allowed"

        self.action_space = self.observation_space = space
        self.ep_length = ep_length
        self.current_step = 0
        self.num_resets = -1  # Becomes 0 after __init__ exits.
        self.reset()

    def reset(self, *, seed: int | None = None, options: dict | None = None) -> tuple[T, dict]:
        if seed is not None:
            super().reset(seed=seed)
        self.current_step = 0
        self.num_resets += 1
        self._choose_next_state()
        return self.state, {}

    def step(self, action: T) -> tuple[T, float, bool, bool, dict[str, Any]]:
        reward = self._get_reward(action)
        self._choose_next_state()
        self.current_step += 1
        terminated = False
        truncated = self.current_step >= self.ep_length
        return self.state, reward, terminated, truncated, {}

    def _choose_next_state(self) -> None:
        self.state = self.action_space.sample()

    def _get_reward(self, action: T) -> float:
        return 1.0 if np.all(self.state == action) else 0.0

    def render(self, mode: str = "human") -> None:
        pass

那么根据模板,我们一会要构建envCube这个环境类,并完成reset() render() step()这几个函数的编写。

编写环境类

参数引入

python 复制代码
class envCube:
    # 设定三个部分的颜色分别是蓝、绿、红
    d = {1: (255, 0, 0),  # blue
        2: (0, 255, 0),  # green
        3: (0, 0, 255)}  # red
    PLAYER_N = 1
    FOOD_N = 2
    ENEMY_N = 3
    def __init__(self,SIZE=10,
                 ACTION_SPACE_VALUES = 9,
                 RETURN_IMAGE = False,
                 MAX_STEP=200,
                 FOOD_REWARD = 25,
                 ENEMY_PENALITY = -300,
                 MOVE_PENALITY = -1):
        self.SIZE=SIZE
        self.OBSERVATION_SPACE_VALUES=(SIZE,SIZE,3)
        self.ACTION_SPACE_VALUES=ACTION_SPACE_VALUES
        self.RETURN_IMAGE = RETURN_IMAGE  # 考虑返回值是否图像
        self.MAX_STEP=MAX_STEP
    
        self.FOOD_REWARD = FOOD_REWARD  # agent获得食物的奖励
        self.ENEMY_PENALITY = ENEMY_PENALITY  # 遇上对手的惩罚
        self.MOVE_PENALITY = MOVE_PENALITY  # 每移动一步的惩罚

reset()函数定义

环境的初始化要做什么呢?

| 函数名 | 参数/说明 | 返回值 (Tuple) |

.reset() (重置环境) 作用 :在每个回合(Episode)开始前调用,将环境恢复到初始状态。 参数seed (可选,用于复现实验结果), options (特定环境的额外参数)。 1. observation :初始环境状态(符合 observation_space 定义)。 2. info:辅助诊断信息(字典)。

看到官方的代码,需要初始化所有的角色,返回一个观测值。

reset()

  1. 初始化实体:创建玩家、食物和敌人,并随机放置它们。
  2. 避免重叠 :通过 while 循环确保它们不会在同一个格子上开始,这是游戏规则的一部分。
  3. 生成观测 :根据 RETURN_IMAGE 的设置,返回不同形式的初始观测。
  4. 重置步数 :将 self.episode_step 归零,用于后续判断回合是否结束。

初始化好说,就是这个观测值,由于我们定义参数的时候有个RETURN_IMAGE,这就意味着我们需要能输出图片,也要能输出观测向量(状态)。

那么此后但凡遇到observation都需要分类讨论是否是图像输出。

对于图片输出,我们先定义一个get_image函数,之后实现。

对于状态向量输出,就直接输出player与food和enemy的坐标差即可。

python 复制代码
if self.RETURN_IMAGE:
    observation = np.array(self.get_image())
else:
    observation = (self.player - self.food)+(self.player - self.enemy)
python 复制代码
# 环境重置
    def reset(self):
        self.player = Cube(self.SIZE)
        self.food = Cube(self.SIZE)
        self.enemy = Cube(self.SIZE)

        # 如果玩家和食物初始位置相同,重置食物的位置,直到位置不同
        while self.player == self.food:
            self.food = Cube(self.SIZE)
        # 如果敌人和玩家或食物的初始位置相同,重置敌人的位置,直到位置不同
        while self.player == self.enemy or self.food == self.enemy:
            self.enemy = Cube(self.SIZE)

        # 判断观测是图像和数字
        if self.RETURN_IMAGE:
            observation = np.array(self.get_image())
        else:
            observation = (self.player - self.food)+(self.player - self.enemy)

        self.episode_step = 0

        return observation

注意,我们预设了(A==B)是检测两个Cube对象是否重合,但是我们并没有在之前的Cube类中定义__eq__,因此我们还需要补充Cube类的规范,并把Cube类放在envCube类之前。

规范化对象类

首先,我们将size改为Cube类的一个参数,无需一开始硬编码SIZE。

然后,我们增加了__eq__函数,用来检测两个Cube对象是否在同一个位置。

最后,我们拓展action函数,补充了上下左右和静止不动几个动作。

python 复制代码
#为三个对象创建类
class Cube:
    def __init__(self,size):#初始位置
        self.size=size
        self.x=np.random.randint(0,self.size-1)
        self.y=np.random.randint(0,self.size-1)

    def __str__(self): #打印当前位置
        return f'{self.x},{self.y}'

    def __sub__(self,other):#这个类的另一个实体
        return (self.x-other.x,self.y-other.y)

    def __eq__(self,other):
        return self.x == other.x and self.y == other.y

    def action(self,choise):
        if choise == 0:
            self.move(x=1,y=1)
        elif choise == 1:
            self.move(x=-1,y=1)
        elif choise == 2:
            self.move(x=1,y=-1)
        elif choise == 3:
            self.move(x=-1,y=-1)
        elif choise == 4:
            self.move(x=0,y=1)
        elif choise == 5:
            self.move(x=0,y=-1)
        elif choise == 6:
            self.move(x=1,y=0)
        elif choise == 7:
            self.move(x=-1,y=0)
        elif choise == 8:
            self.move(x=0,y=0)

    def move(self,x=False,y=False):
        if not x:#如果x没有给值
            self.x += np.random.randint(-1,2)
        else:
            self.x += x

        if not y:#如果y没有给值
            self.y += np.random.randint(-1,2)
        else:
            self.y += y

        #考虑边界
        if self.x<0:
            self.x=0
        elif self.x>=self.size:
            self.x=self.size-1
        if self.y<0:
            self.y=0
        elif self.y>=self.size:
            self.y=self.size-1
            

修改后将其放在envCube之前。

step()函数定义

| 函数名 | 参数/说明 | 返回值 (Tuple) |

.step(action) (执行动作) 作用 :这是强化学习的核心。将 Agent 的动作(Action)传入环境,环境反馈下一步的状态和奖励。 参数action (Agent 选择的动作)。 1. observation :执行动作后的新环境状态。 2. reward :该动作获得的奖励(浮点数)。 3. terminated :布尔值。True 表示回合正常结束(如到达目标/坠毁)。 4. truncated :布尔值。True 表示回合被强制截断(如超时/出界)。 5. info:辅助诊断信息。

格式如下,那么接下来就是如何通过action得到下面的输出了

python 复制代码
    def step(self,action):
        #TODO
        return new_observation,reward,terminated,truncated

遇到observation,立马想到要分类讨论。由于是需要新的new_obs因此要先执行动作再获取状态/图像。

python 复制代码
    def step(self,action):
        self.episode_step+=1
        self.player.action(action)
        self.food.move()
        self.enemy.move()

        # 分类讨论输出new_obs
        if self.RETURN_IMAGE:
            new_observation = np.array(self.get_image())
        else:
            new_observation = (self.player - self.food)+(self.player - self.enemy)

接着获取reward的值,直接按定义写,吃到食物、遇到敌人、普通移动的消耗。

python 复制代码
        #获取reward值
        if self.player == self.food:
            reward=self.FOOD_REWARD
        elif self.player==self.enemy:
            reward=self.ENEMY_PENALITY

根据终止条件返回terminated和truncated

python 复制代码
        #检测截止情况
        terminated = False
        truncated = False

        # 4. 判断结束条件
        if self.player == self.food or self.player == self.enemy:
            terminated = True
        
        if self.episode_step >= self.MAX_STEP:
            truncated = True

完整代码:

python 复制代码
    def step(self,action):
        self.episode_step+=1
        self.player.action(action)
        self.food.move()
        self.enemy.move()

        # 分类讨论输出new_obs
        if self.RETURN_IMAGE:
            new_observation = np.array(self.get_image())
        else:
            new_observation = (self.player - self.food)+(self.player - self.enemy)
            
        #获取reward值
        if self.player == self.food:
            reward=self.FOOD_REWARD
        elif self.player==self.enemy:
            reward=self.ENEMY_PENALITY
        else:
            reward=self.MOVE_PENALITY

        #检测截止情况
        terminated = False
        truncated = False

        # 4. 判断结束条件
        if self.player == self.food or self.player == self.enemy:
            terminated = True
        
        if self.episode_step >= self.MAX_STEP:
            truncated = True
            
        return new_observation,reward,terminated,truncated

            

写一段代码来测试,首先创建envCube类的实例,然后reset一下,最后获取返回值

python 复制代码
env=envCube()
print(env.reset())
obs,reward,terminated,truncated=env.step(3)
print(obs)
print(reward)
print(terminated)
print(truncated)

没毛病。

render()函数定义

| 函数名 | 参数/说明 | 返回值 (Tuple) |

.render() (渲染画面) 作用 :将环境的当前状态可视化。 注意 :在 gymnasium.make() 时需指定 render_mode(如 "human", "rgb_array")。 根据 render_mode 不同而不同: * "human":通常返回 None (直接在窗口显示)。 * "rgb_array":返回图像帧 np.ndarray。 * "ansi":返回文本字符串。

要求返回类型是**"返回图像帧 np.ndarray"**

我们可以参考之前写的画面渲染函数:

python 复制代码
        # 图像显示
        if show:
            env = np.zeros((SIZE,SIZE,3),dtype= np.uint8)
            env[food.x][food.y] = d[FOOD_N]
            env[player.x][player.y] = d[PLAYER_N]
            env[enemy.x][enemy.y] = d[ENEMY_N]
            img = Image.fromarray(env,'RGB')
            img = img.resize((800,800))
            cv2.imshow('',np.array(img))
            if reward == FOOD_REWARD or reward == -ENEMY_PENALITY:
                if cv2.waitKey(500) & 0xFF == ord('q'):
                    break
            else:
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break

这个类型正是我们需要的!

python 复制代码
cv2.imshow('',np.array(img))

现在正好把之前定义的get_image函数补上:

python 复制代码
    def get_image(self):
        # 图像显示
        env = np.zeros(self.OBSERVATION_SPACE_VALUES,dtype= np.uint8)
        env[self.food.x][self.food.y] = self.d[self.FOOD_N]
        env[self.player.x][self.player.y] = self.d[self.PLAYER_N]
        env[self.enemy.x][self.enemy.y] = self.d[self.ENEMY_N]
        img = Image.fromarray(env,'RGB')
        return img

然后在render中调用,用np.array输出。

python 复制代码
    def render(self):
        img=self.get_image()
        img = img.resize((800,800))
        cv2.imshow('Predator',np.array(img))
        cv2.waitKey(1)

测试一下

python 复制代码
env=envCube()
print(env.reset())
obs,reward,terminated,truncated=env.step(3)
print(obs)
print(reward)
print(terminated)
print(truncated)
env.render()

没毛病。

q_table函数编写

q_table也可以作为环境的一部分,我们定义get_qtable来获取/初始化一个q_table,直接引用之前的初始化代码,允许输入之前训练好的qtable,如果为空就初始化,否则就打开,然后返回q_table

python 复制代码
    def get_qtable(self,qtable_name=None):
        # 初始化Q表格
        if qtable_name is None:   # 如果没有实现提供,就随机初始化一个Q表格
            q_table = {}
            for x1 in range(-self.SIZE+1,self.SIZE):
                for y1 in range(-self.SIZE + 1, self.SIZE):
                    for x2 in range(-self.SIZE + 1, self.SIZE):
                        for y2 in range(-self.SIZE + 1, self.SIZE):
                            q_table[(x1,y1,x2,y2)] = [np.random.randint(-5,0) for i in range(self.ACTION_SPACE_VALUES)]
            #这是值,一个包含 4 个元素的列表。
            #np.random.uniform(-5,0):给这 4 个动作随机赋一个负数的初始价值(比如 -2.3, -1.5, -4.0, -0.5)。
            #注意:这里存的是价值,不是概率。如果想看概率,需要额外的数学运算(如 Softmax),但 Q-Learning 通常直接用 argmax 拿最大值的索引来作为动作。
        else:                # 提供了,就使用提供的Q表格
            with open(qtable_name,'rb') as f:
                q_table= pickle.load(f)
        return q_table

测试一下

python 复制代码
env=envCube()
print(env.reset())
obs,reward,terminated,truncated=env.step(3)
print(obs)
print(reward)
print(terminated)
print(truncated)
env.render()
q_table1=env.get_qtable()
q_table1[obs]

没毛病。

当然,你也可以把初始q设计为uniform,而不是randint。

python 复制代码
[np.random.uniform(-5,0) for i in range(self.ACTION_SPACE_VALUES)]

主函数的编写

一个标准的 Gymnasium 交互循环通常长这样:

python 复制代码
import gymnasium as gym

# 1. 创建环境 (指定渲染模式)
env = gym.make("LunarLander-v3", render_mode="human")

# 2. 重置环境
obs, info = env.reset(seed=42)

# 3. 交互循环
for _ in range(1000):
    # 渲染 (可选)
    env.render()
    
    # 采样随机动作 (这里换成你的 AI 模型预测)
    action = env.action_space.sample() 
    
    # 执行动作
    obs, reward, terminated, truncated, info = env.step(action)
    
    # 检查回合是否结束
    if terminated or truncated:
        obs, info = env.reset() # 重新开始

# 4. 关闭环境
env.close()

我们也依照这个格式修改我们的旧循环

先看一下我们的旧循环:

python 复制代码
#建立实例-设计超时步数-初始化环境-得到环境状态-选择动作(e-greedy)-执行动作-计算奖励-
#得到new_q(计算当前q值、计算新状态、计算未来最大q)【用q new_obs max_future_q reward】更新-
#更新q_table[obs][action]=new_q(这里是当前obs,new_obs是为了估计max_future_q)的
episode_rewards=[]
for episode in range(EPISODES):
    player=Cube()
    food=Cube()
    enemy=Cube()

    # 每隔一段时间设定show为True,显示图像
    if episode % SHOW_EVERY == 0:
        print('episode ',episode,'  epsilon:',epsilon)
        print('mean_reward:',np.mean(episode_rewards[-SHOW_EVERY:]))
        show = True
    else:
        show = False

    episode_reward=0
    for i in range(200):#超过200步结束
        #拿到环境状态值
        obs=(player-food,player-enemy)#obs的状态 类型要和定义的q_table的状态一致
        #ε-Greedy
        if np.random.random()>epsilon:
            #Q-Learning就是在Q表中找到对应状态的最佳动作
            action=np.argmax(q_table[obs])#argmax就是取最大值的索引(0-3) 
        else:
            action = np.random.randint(0,4)#左闭右开

        #print(player)
        #print(obs)
        #print(action)
        #执行动作
        player.action(action)
        #food.action
        #print('after action:')
        #print(player)

        #计算奖励
        if player.x==food.x and player.y==food.y:
            reward=FOOD_REWARD
        elif player.x==enemy.x and player.y == enemy.y:
            reward = -ENEMY_PENALITY
        else:
            reward= -MOVE_PENALITY

        #print(f'reward:{reward}')

        #更新q_table
        #得到当前的q值
        current_q=q_table[obs][action]#从 Q 表中取出:在当前状态 obs 下,执行动作 action 对应的 Q 值
        #print(f'current_q:{current_q}')

        #更新表格需要 新obs和权衡后的q值
        #得到新的状态
        new_obs=(player-food,player-enemy)
        #print(f'new_obs{new_obs}')

        max_future_q=np.max(q_table[new_obs])
        #print(f'max_future_q:{max_future_q}')
        #逻辑:智能体当前可能因为 ϵ -greedy 探索策略而做了一个"傻事"(比如撞墙),或者做了一个次优动作。
        #目标:但我们更新 Q 值时,不应该参考那个"傻事",而是要参考如果我们在那个新位置(s')重新开始,最聪明的做法能拿多少分。
        #结果:这使得 Q-Learning 的更新目标(Target)总是朝着"最好"的方向修正,最终收敛到最优解。
        if reward==FOOD_REWARD:#如果吃到了食物,就用实际奖励更新
            new_q=FOOD_REWARD
        else:
            new_q=(1-LEARNING_RATE)*current_q + LEARNING_RATE*(reward + DISCOUNT*max_future_q)
        #print(f'new_q:{new_q}')

        q_table[obs][action]=new_q


        # 图像显示
        if show:
            env = np.zeros((SIZE,SIZE,3),dtype= np.uint8)
            env[food.x][food.y] = d[FOOD_N]
            env[player.x][player.y] = d[PLAYER_N]
            env[enemy.x][enemy.y] = d[ENEMY_N]
            img = Image.fromarray(env,'RGB')
            img = img.resize((800,800))
            cv2.imshow('',np.array(img))
            if reward == FOOD_REWARD or reward == -ENEMY_PENALITY:
                if cv2.waitKey(500) & 0xFF == ord('q'):
                    break
            else:
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break
        
        #加上这一步的奖励
        episode_reward+=reward

        if reward==FOOD_REWARD or reward==-ENEMY_PENALITY:
            break #如果吃到食物或被杀死就结束这一轮


    #轮批次处理
    episode_rewards.append(episode_reward)
    epsilon*=EPS_DECAY

        

首先,我们创建环境类实例,并初始化/导入q_table

python 复制代码
env=envCube()
q_table=env.get_qtable()
episode_rewards=[]

接着在每一个批外层循环中执行初始化,直接调用env的reset(),创建对象的部分我们已经囊括在reset()中了,因此可以删去:

python 复制代码
for episode in range(EPISODES):
    env.reset()
    terminated=False
    truncated=False

    # 每隔一段时间设定show为True,显示图像
    if episode % SHOW_EVERY == 0:
        print('episode ',episode,'  epsilon:',epsilon)
        print('mean_reward:',np.mean(episode_rewards[-SHOW_EVERY:]))
        show = True
    else:
        show = False

    episode_reward=0
    for i in range(200):#超过200步结束
        #每一步的操作

然后我们也不需要200步,而是可以用terminated和truncated判断

python 复制代码
while not terminated or truncated:

环境在一开始的env.reset()就获取了,因此可以删去。

python 复制代码
        #拿到环境状态值
        #obs=(player-food,player-enemy)#obs的状态 类型要和定义的q_table的状态一致

选择动作的空间也要从之前的4个改为env.ACTION_SPACE_VALUES了

python 复制代码
        #ε-Greedy
        if np.random.random()>epsilon:
            #Q-Learning就是在Q表中找到对应状态的最佳动作
            action=np.argmax(q_table[obs])#argmax就是取最大值的索引(0-3) 
        else:
            action = np.random.randint(0,env.ACTION_SPACE_VALUES)#左闭右开

然后执行动作也无需player.action(),而是引用环境类的env.step(),注意接收顺序。

python 复制代码
        #player.action(action)
        new_obs,reward,terminated,truncated=env.step(action)

那么此后获取new_obs和reward的代码都要删去:

reward我们在step计算过了,因此这部分也直接删去

python 复制代码
        #计算奖励
        #if player.x==food.x and player.y==food.y:
        #    reward=FOOD_REWARD
        #elif player.x==enemy.x and player.y == enemy.y:
        #    reward = -ENEMY_PENALITY
        #else:
        #    reward= -MOVE_PENALITY

计算new_obs的部分也可以删去

python 复制代码
        #更新表格需要 新obs和权衡后的q值
        #得到新的状态
        #new_obs=(player-food,player-enemy)
        #print(f'new_obs{new_obs}')

奖励部分记得给FOOD_REWARD加上env,然后最后别忘了更新obs(之前obs的获取在内层循环,但是现在给到外层reset()返回获取了,相对于每一步不会主动更新了,我们必须在内存循环设计一个更新obs的机制)

python 复制代码
        if reward==env.FOOD_REWARD:#如果吃到了食物,就用实际奖励更新
            new_q=env.FOOD_REWARD
        else:
            new_q=(1-LEARNING_RATE)*current_q + LEARNING_RATE*(reward + DISCOUNT*max_future_q)
        #print(f'new_q:{new_q}')
        
        q_table[obs][action]=new_q
        obs=new_obs

以及边界条件,我们用terminated和truncated判断过了,不再需要这两行了:

python 复制代码
        #if reward==FOOD_REWARD or reward==-ENEMY_PENALITY:
        #    break #如果吃到食物或被杀死就结束这一轮

完整主函数:

python 复制代码
#建立实例-设计超时步数-初始化环境-得到环境状态-选择动作(e-greedy)-执行动作-计算奖励-
#得到new_q(计算当前q值、计算新状态、计算未来最大q)【用q new_obs max_future_q reward】更新-
#更新q_table[obs][action]=new_q(这里是当前obs,new_obs是为了估计max_future_q)的
env=envCube()
q_table=env.get_qtable()
episode_rewards=[]
for episode in range(EPISODES):
    obs=env.reset()
    terminated=False
    truncated=False
    # 每隔一段时间设定show为True,显示图像
    if episode % SHOW_EVERY == 0:
        print('episode ',episode,'  epsilon:',epsilon)
        print('mean_reward:',np.mean(episode_rewards[-SHOW_EVERY:]))
        show = True
    else:
        show = False

    episode_reward=0
    while not (terminated or truncated):
        #拿到环境状态值
        #obs=(player-food,player-enemy)#obs的状态 类型要和定义的q_table的状态一致
        
        #ε-Greedy
        if np.random.random()>epsilon:
            #Q-Learning就是在Q表中找到对应状态的最佳动作
            action=np.argmax(q_table[obs])#argmax就是取最大值的索引(0-3) 
        else:
            action = np.random.randint(0,env.ACTION_SPACE_VALUES)#左闭右开
        
        #player.action(action)
        new_obs,reward,terminated,truncated=env.step(action)

        #更新q_table
        #得到当前的q值
        current_q=q_table[obs][action]#从 Q 表中取出:在当前状态 obs 下,执行动作 action 对应的 Q 值
        #print(f'current_q:{current_q}')

        max_future_q=np.max(q_table[new_obs])
        #print(f'max_future_q:{max_future_q}')
        #逻辑:智能体当前可能因为 ϵ -greedy 探索策略而做了一个"傻事"(比如撞墙),或者做了一个次优动作。
        #目标:但我们更新 Q 值时,不应该参考那个"傻事",而是要参考如果我们在那个新位置(s')重新开始,最聪明的做法能拿多少分。
        #结果:这使得 Q-Learning 的更新目标(Target)总是朝着"最好"的方向修正,最终收敛到最优解。
        if reward==env.FOOD_REWARD:#如果吃到了食物,就用实际奖励更新
            new_q=env.FOOD_REWARD
        else:
            new_q=(1-LEARNING_RATE)*current_q + LEARNING_RATE*(reward + DISCOUNT*max_future_q)
        #print(f'new_q:{new_q}')
        
        q_table[obs][action]=new_q
        obs=new_obs

        # 图像显示
        if show:
            env.render()
        
        #加上这一步的奖励
        episode_reward+=reward

    #轮批次处理
    episode_rewards.append(episode_reward)
    epsilon*=EPS_DECAY

        

强化学习,启动!

同理,你可以在之前的数据上继续训练,找到最新的q表

填写到第一部分即可。

完整代码:

环境配置

python 复制代码
#将学习环境所需要的依赖库导入
import numpy as np
import cv2
from PIL import Image
import time
import pickle #对象到文件的处理
import matplotlib.pyplot as plt
from matplotlib import style
style.use('ggplot')

参数设定

python 复制代码
#环境参数设定
#SIZE=10 #3个对象在10*10的范围内
EPISODES=30000 #智能体玩的游戏轮数
SHOW_EVERY=3000 #每3000局展示一次游玩过程

#奖励与惩罚
#FOOD_REWARD=25#吃食物的奖励
#ENEMY_PENALITY=300 #被敌人抓住的惩罚
#MOVE_PENALITY=1 #移动惩罚

#环境计算参数
epsilon=0.6 #在强化学习时抽取随机动作的概率 40%使用最大价值期望动作
EPS_DECAY=0.9998 #每玩一局游戏就让随机动作概率乘以这个数,到最后基本就定性了
DISCOUNT=0.95 #折扣回报 未来奖励的折扣
LEARNING_RATE=0.1 #学习率 步长

#q_table_file = "q_table_save.pkl" 
#q_table = None
q_table = "qtable_1775227149.pickle"
#d = {1:(255,0,0), #蓝色------玩家
#     2:(0,255,0), #绿色------食物
#     3:(0,0,255)} #红色------敌人
#PLAYER_N=1
#FOOD_N=2
#ENEMY_N=3

对象类创建

python 复制代码
#为三个对象创建类
class Cube:
    def __init__(self,size):#初始位置
        self.size=size
        self.x=np.random.randint(0,self.size-1)
        self.y=np.random.randint(0,self.size-1)

    def __str__(self): #打印当前位置
        return f'{self.x},{self.y}'

    def __sub__(self,other):#这个类的另一个实体
        return (self.x-other.x,self.y-other.y)

    def __eq__(self,other):
        return self.x == other.x and self.y == other.y

    def action(self,choise):
        if choise == 0:
            self.move(x=1,y=1)
        elif choise == 1:
            self.move(x=-1,y=1)
        elif choise == 2:
            self.move(x=1,y=-1)
        elif choise == 3:
            self.move(x=-1,y=-1)
        elif choise == 4:
            self.move(x=0,y=1)
        elif choise == 5:
            self.move(x=0,y=-1)
        elif choise == 6:
            self.move(x=1,y=0)
        elif choise == 7:
            self.move(x=-1,y=0)
        elif choise == 8:
            self.move(x=0,y=0)

    def move(self,x=False,y=False):
        if not x:#如果x没有给值
            self.x += np.random.randint(-1,2)
        else:
            self.x += x

        if not y:#如果y没有给值
            self.y += np.random.randint(-1,2)
        else:
            self.y += y

        #考虑边界
        if self.x<0:
            self.x=0
        elif self.x>=self.size:
            self.x=self.size-1
        if self.y<0:
            self.y=0
        elif self.y>=self.size:
            self.y=self.size-1
            

环境类创建

python 复制代码
class envCube:
    # 设定三个部分的颜色分别是蓝、绿、红
    d = {1: (255, 0, 0),  # blue
        2: (0, 255, 0),  # green
        3: (0, 0, 255)}  # red
    PLAYER_N = 1
    FOOD_N = 2
    ENEMY_N = 3
    def __init__(self,SIZE=10,
                 ACTION_SPACE_VALUES = 9,
                 RETURN_IMAGE = False,
                 MAX_STEP=200,
                 FOOD_REWARD = 25,
                 ENEMY_PENALITY = -300,
                 MOVE_PENALITY = -1):
        self.SIZE=SIZE
        self.OBSERVATION_SPACE_VALUES=(SIZE,SIZE,3)
        self.ACTION_SPACE_VALUES=ACTION_SPACE_VALUES
        self.RETURN_IMAGE = RETURN_IMAGE  # 考虑返回值是否图像
        self.MAX_STEP=MAX_STEP
    
        self.FOOD_REWARD = FOOD_REWARD  # agent获得食物的奖励
        self.ENEMY_PENALITY = ENEMY_PENALITY  # 遇上对手的惩罚
        self.MOVE_PENALITY = MOVE_PENALITY  # 每移动一步的惩罚

    # 环境重置
    def reset(self):
        self.player = Cube(self.SIZE)
        self.food = Cube(self.SIZE)
        self.enemy = Cube(self.SIZE)
        # 如果玩家和食物初始位置相同,重置食物的位置,直到位置不同
        while self.player == self.food:
            self.food = Cube(self.SIZE)
        # 如果敌人和玩家或食物的初始位置相同,重置敌人的位置,直到位置不同
        while self.player == self.enemy or self.food == self.enemy:
            self.enemy = Cube(self.SIZE)
        # 判断观测是图像和数字
        if self.RETURN_IMAGE:
            observation = np.array(self.get_image())
        else:
            observation = (self.player - self.food)+(self.player - self.enemy)

        self.episode_step = 0

        return observation

    def step(self,action):
        self.episode_step+=1
        self.player.action(action)
        self.food.move()
        self.enemy.move()

        # 分类讨论输出new_obs
        if self.RETURN_IMAGE:
            new_observation = np.array(self.get_image())
        else:
            new_observation = (self.player - self.food)+(self.player - self.enemy)
            
        #获取reward值
        if self.player == self.food:
            reward=self.FOOD_REWARD
        elif self.player==self.enemy:
            reward=self.ENEMY_PENALITY
        else:
            reward=self.MOVE_PENALITY

        #检测截止情况
        terminated = False
        truncated = False

        # 4. 判断结束条件
        if self.player == self.food or self.player == self.enemy:
            terminated = True
        
        if self.episode_step >= self.MAX_STEP:
            truncated = True
            
        return new_observation,reward,terminated,truncated

    def render(self):
        img=self.get_image()
        img = img.resize((800,800))
        cv2.imshow('Predator',np.array(img))
        cv2.waitKey(1)

    def get_image(self):
        # 图像显示
        env = np.zeros(self.OBSERVATION_SPACE_VALUES,dtype= np.uint8)
        env[self.food.x][self.food.y] = self.d[self.FOOD_N]
        env[self.player.x][self.player.y] = self.d[self.PLAYER_N]
        env[self.enemy.x][self.enemy.y] = self.d[self.ENEMY_N]
        img = Image.fromarray(env,'RGB')
        return img

    def get_qtable(self,qtable_name=None):
        # 初始化Q表格
        if qtable_name is None:   # 如果没有实现提供,就随机初始化一个Q表格
            q_table = {}
            for x1 in range(-self.SIZE+1,self.SIZE):
                for y1 in range(-self.SIZE + 1, self.SIZE):
                    for x2 in range(-self.SIZE + 1, self.SIZE):
                        for y2 in range(-self.SIZE + 1, self.SIZE):
                            q_table[(x1,y1,x2,y2)] = [np.random.uniform(-5,0) for i in range(self.ACTION_SPACE_VALUES)]
            #这是值,一个包含 4 个元素的列表。
            #np.random.uniform(-5,0):给这 4 个动作随机赋一个负数的初始价值(比如 -2.3, -1.5, -4.0, -0.5)。
            #注意:这里存的是价值,不是概率。如果想看概率,需要额外的数学运算(如 Softmax),但 Q-Learning 通常直接用 argmax 拿最大值的索引来作为动作。
        else:                # 提供了,就使用提供的Q表格
            with open(qtable_name,'rb') as f:
                q_table= pickle.load(f)
        return q_table
            

主函数

python 复制代码
#建立实例-设计超时步数-初始化环境-得到环境状态-选择动作(e-greedy)-执行动作-计算奖励-
#得到new_q(计算当前q值、计算新状态、计算未来最大q)【用q new_obs max_future_q reward】更新-
#更新q_table[obs][action]=new_q(这里是当前obs,new_obs是为了估计max_future_q)的
env=envCube()
q_table=env.get_qtable()
episode_rewards=[]
for episode in range(EPISODES):
    obs=env.reset()
    terminated=False
    truncated=False
    # 每隔一段时间设定show为True,显示图像
    if episode % SHOW_EVERY == 0:
        print('episode ',episode,'  epsilon:',epsilon)
        print('mean_reward:',np.mean(episode_rewards[-SHOW_EVERY:]))
        show = True
    else:
        show = False

    episode_reward=0
    while not (terminated or truncated):
        #拿到环境状态值
        #obs=(player-food,player-enemy)#obs的状态 类型要和定义的q_table的状态一致
        
        #ε-Greedy
        if np.random.random()>epsilon:
            #Q-Learning就是在Q表中找到对应状态的最佳动作
            action=np.argmax(q_table[obs])#argmax就是取最大值的索引(0-3) 
        else:
            action = np.random.randint(0,env.ACTION_SPACE_VALUES)#左闭右开
        
        #player.action(action)
        new_obs,reward,terminated,truncated=env.step(action)

        #更新q_table
        #得到当前的q值
        current_q=q_table[obs][action]#从 Q 表中取出:在当前状态 obs 下,执行动作 action 对应的 Q 值
        #print(f'current_q:{current_q}')

        max_future_q=np.max(q_table[new_obs])
        #print(f'max_future_q:{max_future_q}')
        #逻辑:智能体当前可能因为 ϵ -greedy 探索策略而做了一个"傻事"(比如撞墙),或者做了一个次优动作。
        #目标:但我们更新 Q 值时,不应该参考那个"傻事",而是要参考如果我们在那个新位置(s')重新开始,最聪明的做法能拿多少分。
        #结果:这使得 Q-Learning 的更新目标(Target)总是朝着"最好"的方向修正,最终收敛到最优解。
        if reward==env.FOOD_REWARD:#如果吃到了食物,就用实际奖励更新
            new_q=env.FOOD_REWARD
        else:
            new_q=(1-LEARNING_RATE)*current_q + LEARNING_RATE*(reward + DISCOUNT*max_future_q)
        #print(f'new_q:{new_q}')
        
        q_table[obs][action]=new_q
        obs=new_obs

        # 图像显示
        if show:
            env.render()
        
        #加上这一步的奖励
        episode_reward+=reward

    #轮批次处理
    episode_rewards.append(episode_reward)
    epsilon*=EPS_DECAY

        

统计量绘图

python 复制代码
moving_avg = np.convolve(episode_rewards, np.ones((SHOW_EVERY,))/SHOW_EVERY,mode='valid')
plt.plot([i for i in range(len(moving_avg))], moving_avg)
plt.xlabel('episode #')
plt.ylabel(f'mean{SHOW_EVERY} reward')
plt.show()

保存q_table

python 复制代码
with open(f'qtable_{int(time.time())}.pickle','wb') as f:
    pickle.dump(q_table,f)
相关推荐
清水白石0086 小时前
Python 编程实战全景:从基础语法到插件架构、异步性能与工程最佳实践
开发语言·python·架构
AI木马人6 小时前
3.【Prompt工程实战】如何设计一个可复用的Prompt系统?(避免每次手写提示词)
linux·服务器·人工智能·深度学习·prompt
lwf0061646 小时前
导数学习日记
学习·算法·机器学习
yaoxin5211237 小时前
390. Java IO API - WatchDir 示例
java·前端·python
ydmy7 小时前
transformer超参数配置(个人理解)
人工智能·深度学习
武帝为此7 小时前
【数据清洗缺失值处理】
python·算法·数学建模
zhangchaoxies8 小时前
如何在 Go 中安全复制接口指针所指向的值
jvm·数据库·python
曲幽8 小时前
FastAPI + Pydantic 模型终极实战手册:从能跑就行到固若金汤,这些技巧你一定用得上
python·fastapi·web·model·field·pydantic·validator·basemodel
计算机软件程序设计9 小时前
Python Flask工程目录解读
python·flask·工程目录解读
Ares-Wang9 小时前
Flask》》 Flask-OpenID 认证、 OpenID Connect (OIDC)
后端·python·flask