我们之前写了自定义环境下的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():
- 初始化实体:创建玩家、食物和敌人,并随机放置它们。
- 避免重叠 :通过
while循环确保它们不会在同一个格子上开始,这是游戏规则的一部分。 - 生成观测 :根据
RETURN_IMAGE的设置,返回不同形式的初始观测。 - 重置步数 :将
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)