第七章的代码:DDPG.ipynb及其涉及的其他代码的更新以及注解(gym版本 >= 0.26)
摘要
本系列知识点讲解基于蘑菇书EasyRL中的内容进行详细的疑难点分析!具体内容请阅读蘑菇书EasyRL!
# -*- coding: utf-8 -*-
"""1. 定义算法"""
"""
1.1. 定义模型
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
class Actor(nn.Module):
def __init__(self, n_states, n_actions, hidden_dim = 256, init_w=3e-3):
super(Actor, self).__init__()
self.linear1 = nn.Linear(n_states, hidden_dim)
self.linear2 = nn.Linear(hidden_dim, hidden_dim)
self.linear3 = nn.Linear(hidden_dim, n_actions)
'''
作用:对第三层的权重使用均匀分布初始化,范围为 [-0.003, 0.003]。
数值例子:每个权重值在 [-0.003, 0.003] 之间,这有助于输出层初始时较小的值,防止过大输出。
'''
self.linear3.weight.data.uniform_(-init_w, init_w)
'''
作用:对第三层的偏置同样使用均匀分布初始化,范围为 [-0.003, 0.003]。
数值例子:例如,偏置可能被初始化为 [0.001, -0.002]。
'''
self.linear3.bias.data.uniform_(-init_w, init_w)
def forward(self, x):
x = F.relu(self.linear1(x))
x = F.relu(self.linear2(x))
x = torch.tanh(self.linear3(x))
return x
"""
注意DDGP中critic网络的输入是state加上action。
"""
class Critic(nn.Module):
def __init__(self, n_states, n_actions, hidden_dim=256, init_w=3e-3):
super(Critic, self).__init__()
self.linear1 = nn.Linear(n_states + n_actions, hidden_dim)
self.linear2 = nn.Linear(hidden_dim, hidden_dim)
self.linear3 = nn.Linear(hidden_dim, 1)
# 随机初始化为较小的值
self.linear3.weight.data.uniform_(-init_w, init_w)
self.linear3.bias.data.uniform_(-init_w, init_w)
self.count = 0
def forward(self, state, action):
# 按维数1拼接
x = torch.cat([state, action], 1)
x = F.relu(self.linear1(x))
x = F.relu(self.linear2(x))
x = self.linear3(x)
return x
"""
1.2 定义经验回放
"""
from collections import deque
import random
class ReplayBuffer:
"""
作用:定义一个名为 ReplayBuffer 的类,用于存储强化学习中的经验(transition)。
说明:ReplayBuffer 常用于经验重放,存储训练过程中收集的状态、动作、奖励等数据。
"""
def __init__(self, capacity: int) -> None:
self.capacity = capacity
self.buffer = deque(maxlen=self.capacity)
def push(self,transitions):
'''
transitions 通常是一个元组,包含 (state, action, reward, next_state, done) 等数据。
'''
self.buffer.append(transitions)
def sample(self, batch_size: int, sequential: bool = False):
if batch_size > len(self.buffer):
batch_size = len(self.buffer)
if sequential: # sequential sampling
rand = random.randint(0, len(self.buffer) - batch_size)
batch = [self.buffer[i] for i in range(rand, rand + batch_size)]
# return zip(*batch)
return tuple(zip(*batch))
else:
batch = random.sample(self.buffer, batch_size)
# return zip(*batch)
return tuple(zip(*batch))
def clear(self):
self.buffer.clear()
def __len__(self):
return len(self.buffer)
import torch.optim as optim
import numpy as np
class DDPG:
"""
作用:定义一个名为 DDPG 的类,用于实现深度确定性策略梯度算法。
说明:该类封装了 actor/critic 网络、目标网络、优化器、经验回放以及相关更新操作。
"""
def __init__(self, models,memories,cfg):
self.device = torch.device(cfg['device'])
self.critic = models['critic'].to(self.device)
self.target_critic = models['critic'].to(self.device)
self.actor = models['actor'].to(self.device)
self.target_actor = models['actor'].to(self.device)
# 复制参数到目标网络
'''
作用:遍历目标 critic 和在线 critic 网络的每个参数,将在线网络参数复制到目标网络中,使其完全一致。
'''
for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()):
target_param.data.copy_(param.data)
'''
作用:同样对 actor 网络进行参数复制,使目标 actor 与在线 actor 完全一致
'''
for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()):
target_param.data.copy_(param.data)
self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=cfg['critic_lr'])
self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=cfg['actor_lr'])
self.memory = memories['memory']
self.batch_size = cfg['batch_size']
self.gamma = cfg['gamma']
self.tau = cfg['tau'] # 软更新参数
def sample_action(self, state):
"""
作用:定义方法 sample_action,根据当前 actor 网络采样一个动作用于执行。
"""
if isinstance(state, tuple):
state = state[0]
state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
action = self.actor(state)
'''
作用:
- detach():切断计算图,防止梯度传播;
- cpu():将张量移动到 CPU;
- numpy():转换为 NumPy 数组;
- [0, 0]:取第一行第一列的数值,作为最终动作数值返回。
'''
return action.detach().cpu().numpy()[0, 0]
@torch.no_grad()
def predict_action(self, state):
''' 用于预测,不需要计算梯度
'''
if isinstance(state, tuple):
state = state[0]
state = torch.FloatTensor(state).unsqueeze(0).to(self.device)
action = self.actor(state)
return action.cpu().numpy()[0, 0]
def update(self):
if len(self.memory) < self.batch_size: # 当memory中不满足一个批量时,不更新策略
return
# 从经验回放中中随机采样一个批量的transition
state, action, reward, next_state, done = self.memory.sample(self.batch_size)
# 转变为张量
processed_state = []
for s in state:
if isinstance(s, tuple):
# 如果元素是元组,则取元组的第一个元素
processed_state.append(s[0])
else:
processed_state.append(s)
state = torch.FloatTensor(np.array(processed_state)).to(self.device)
next_state = torch.FloatTensor(np.array(next_state)).to(self.device)
action = torch.FloatTensor(np.array(action)).to(self.device)
reward = torch.FloatTensor(reward).unsqueeze(1).to(self.device)
done = torch.FloatTensor(np.float32(done)).unsqueeze(1).to(self.device)
# 注意看伪代码,这里的actor损失就是对应策略即actor输出的action下对应critic值的负均值
'''
计算当前状态下,通过 actor 网络得到的动作,再传入 critic 网络评估其价值;
得到的输出是每个状态-动作对的 Q 值。
'''
actor_loss = self.critic(state, self.actor(state))
actor_loss = - actor_loss.mean()
next_action = self.target_actor(next_state)
target_value = self.target_critic(next_state, next_action.detach())
# 这里的expected_value就是伪代码中间的y_i
expected_value = reward + (1.0 - done) * self.gamma * target_value
expected_value = torch.clamp(expected_value, -np.inf, np.inf)
actual_value = self.critic(state, action)
critic_loss = nn.MSELoss()(actual_value, expected_value.detach())
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()
# 各自目标网络的参数软更新
'''作用:遍历目标 critic 网络与在线 critic 网络的每一对参数,准备进行软更新。'''
for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()):
'''new_target = (1−τ) × old_target + τ × current_parameter'''
target_param.data.copy_(
target_param.data * (1.0 - self.tau) +
param.data * self.tau
)
'''作用:同样遍历目标 actor 与在线 actor 的参数对。'''
for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()):
'''new_target = (1−τ) × old_target + τ × current_parameter'''
target_param.data.copy_(
target_param.data * (1.0 - self.tau) +
param.data * self.tau
)
'''
2. 定义训练
注意测试函数中不需要动作噪声
'''
class OUNoise(object):
'''Ornstein–Uhlenbeck噪声
文档字符串说明该类用于生成 Ornstein–Uhlenbeck 噪声,这种噪声常用于连续控制任务中的动作探索。
'''
def __init__(self, action_space, mu=0.0, theta=0.15, max_sigma=0.3, min_sigma=0.3, decay_period=100000):
"""
action_space:环境的动作空间对象。它包含两个关键属性:
action_space.shape:例如假设动作空间是 1 维,则 action_space.shape[0] 为 1。
action_space.low 与 action_space.high:分别表示动作的下界和上界(如低=-1,高=1)。
"""
'''作用:将均值参数保存到实例属性中。'''
self.mu = mu # OU噪声的参数
'''作用:保存 theta 参数,控制噪声回归到均值的速度。'''
self.theta = theta # OU噪声的参数
'''作用:初始 sigma(噪声标准差)设置为 max_sigma。'''
self.sigma = max_sigma # OU噪声的参数
'''作用:记录最大噪声标准差,用于后续衰减计算。'''
self.max_sigma = max_sigma
'''作用:记录最小噪声标准差。'''
self.min_sigma = min_sigma
'''作用:记录衰减周期,即在多少步内从 max_sigma 衰减到 min_sigma。'''
self.decay_period = decay_period
'''作用:从动作空间中获取动作数量(维度数),用于生成噪声向量。'''
self.n_actions = action_space.shape[0]
'''作用:保存动作空间的下界,用于后续对噪声后的动作进行剪切。'''
self.low = action_space.low
'''作用:保存动作空间的上界。'''
self.high = action_space.high
'''作用:调用 reset 方法,初始化噪声状态(内部状态 obs)。'''
self.reset()
def reset(self):
"""
作用:重置 OU 噪声的内部状态 obs。
- 使用 np.ones(self.n_actions) 创建一个全 1 数组,长度等于动作数。
- 乘以 mu,使得所有初始噪声均值为 mu。
数值例子:若 n_actions=1 且 mu=0.0,则 np.ones(1) → [1.0],
乘以 0.0 得到 [0.0],因此 self.obs = [0.0].
"""
self.obs = np.ones(self.n_actions) * self.mu
def evolve_obs(self):
"""作用:将当前噪声状态赋给局部变量 x。"""
x = self.obs
'''
作用:计算噪声变化量 dx。公式解释:
- self.theta * (self.mu - x):表示噪声向均值回归的部分;
- self.sigma * np.random.randn(self.n_actions):表示随机部分,
np.random.randn 生成标准正态分布的数值。
数值例子:
- 假设 self.mu=0.0,x = [0.0],self.theta=0.15,self.sigma=0.3;
- np.random.randn(self.n_actions) 生成 [0.5](假设随机值为 0.5);
- 则 dx = 0.15 * (0.0 - 0.0) + 0.3 * 0.5 = 0 + 0.15 = 0.15,即 dx = [0.15].
'''
dx = self.theta * (self.mu - x) + self.sigma * np.random.randn(self.n_actions)
'''作用:更新内部状态 obs 为原始 x 加上变化 dx'''
self.obs = x + dx
return self.obs
def get_action(self, action, t=0):
"""
作用:定义 get_action 方法,用于给定一个原始动作,在其基础上添加 OU 噪声,并进行剪切。
"""
'''作用:调用 evolve_obs 生成更新后的噪声向量 ou_obs。'''
ou_obs = self.evolve_obs()
'''
作用:更新 sigma,使其随着时间步 t 逐渐衰减到 min_sigma。
- 计算 t / self.decay_period,取最小值和 1.0,确保衰减比例不超过 1。
- 用公式:sigma = max_sigma - (max_sigma - min_sigma) * (衰减比例)。
数值例子:
- 假设 max_sigma = 0.3,min_sigma = 0.3(此处两者相等,噪声不会衰减),
t=50,decay_period=100000。
- t / decay_period = 50/100000 = 0.0005,min(1.0, 0.0005)=0.0005,
- sigma = 0.3 - (0.3 - 0.3) * 0.0005 = 0.3 - 0*0.0005 = 0.3。
- 如果 min_sigma 与 max_sigma 不同(例如 max_sigma=0.3, min_sigma=0.1),
那么当 t=50000 时,
- t / decay_period = 50000/100000 = 0.5,
- sigma = 0.3 - (0.3 - 0.1) * 0.5 = 0.3 - 0.2*0.5 = 0.3 - 0.1 = 0.2。
'''
self.sigma = self.max_sigma - (self.max_sigma - self.min_sigma) * min(1.0, t / self.decay_period) # sigma会逐渐衰减
'''
作用:
- 将原始 action 加上噪声 ou_obs 得到扰动后的动作;
- 使用 np.clip 将结果限制在动作空间的下界和上界之间,确保动作有效。
'''
return np.clip(action + ou_obs, self.low, self.high) # 动作加上噪声后进行剪切
def train(cfg, env, agent):
"""
作用:定义 train 函数,接收配置字典 cfg、环境 env 和智能体 agent。实现训练过程。
参数说明:
- cfg:包含训练参数,如 'train_eps'(训练回合数)、'max_steps'(每回合最大步数)等。
- env:Gym 环境实例。
- agent:强化学习智能体,具有 sample_action、update 以及 memory 属性。
"""
print("开始训练!")
print("Using "+cfg['device']+"!")
'''作用:利用环境的动作空间创建一个 OU 噪声对象,用于后续在动作上添加噪声。'''
ou_noise = OUNoise(env.action_space) # 动作噪声
'''作用:初始化一个空列表,用于存储每个回合获得的累计奖励。'''
rewards = [] # 记录所有回合的奖励
for i_ep in range(cfg['train_eps']):
state = env.reset()
ou_noise.reset()
ep_reward = 0
for i_step in range(cfg['max_steps']):
action = agent.sample_action(state)
action = ou_noise.get_action(action, i_step+1)
result = env.step(action)
if len(result) == 5:
next_state, reward, done, truncated, info = result
done = done or truncated # 可合并 terminated 和 truncated 标志
else:
next_state, reward, done, info = result
# next_state, reward, done, truncated, info = env.step(action)
# next_state, reward, done, _ = env.step(action)
ep_reward += reward
agent.memory.push((state, action, reward, next_state, done))
'''
作用:调用 agent 的 update 方法,对智能体进行一次参数更新
(从经验回放中采样、计算损失、反向传播)。
说明:此处每步更新,更新策略依赖于采样到的批量经验。
'''
agent.update()
state = next_state
if done:
break
if (i_ep+1)%10 == 0:
print(f"回合:{i_ep+1}/{cfg['train_eps']},奖励:{ep_reward:.2f}")
rewards.append(ep_reward)
print("完成训练!")
return {'rewards':rewards}
def test(cfg, env, agent):
print("开始测试!")
rewards = [] # 记录所有回合的奖励
for i_ep in range(cfg['test_eps']):
state = env.reset()
ep_reward = 0
for i_step in range(cfg['max_steps']):
action = agent.predict_action(state)
result = env.step(action)
if len(result) == 5:
next_state, reward, done, truncated, info = result
done = done or truncated # 可合并 terminated 和 truncated 标志
else:
next_state, reward, done, info = result
# next_state, reward, done, truncated, info = env.step(action)
# next_state, reward, done, _ = env.step(action)
ep_reward += reward
state = next_state
if done:
break
rewards.append(ep_reward)
print(f"回合:{i_ep+1}/{cfg['test_eps']},奖励:{ep_reward:.2f}")
print("完成测试!")
return {'rewards':rewards}
"""3. 定义环境"""
import gym
import os
import torch
import numpy as np
import random
class NormalizedActions(gym.ActionWrapper):
''' 将action范围重定在[0.1]之间
'''
"""
作用:定义一个继承自 gym.ActionWrapper 的类,用于对环境的动作进行包装,
使其输出动作自动归一化到环境的动作范围内。
说明:该包装器可以将神经网络输出(通常在 [-1, 1] 范围内)映射到环境实际允许的动作范围。
"""
def action(self, action):
"""
作用:重写 gym.ActionWrapper 中的 action 方法,用于将神经网络输出的动作进行转换。
例子:输入参数 action 通常为一个在 [-1, 1] 范围内的数或数组,例如 0.5 或 [0.5].
"""
low_bound = self.action_space.low
upper_bound = self.action_space.high
'''
作用:将原始动作(假设在 [-1, 1] 范围内)线性映射到 [low_bound, upper_bound] 范围内。公式说明:
- 首先,action + 1.0 将区间 [-1, 1] 变为 [0, 2];
- 乘以 0.5 后变为 [0, 1];
- 再乘以 (upper_bound - low_bound) 得到 [0, upper_bound - low_bound];
- 最后加上 low_bound 使区间变为 [low_bound, upper_bound]。
数值例子:
- 若 action = 0.5,low_bound = -2,upper_bound = 2,则:
- action + 1.0 = 1.5;
- 1.5 * 0.5 = 0.75;
- (upper_bound - low_bound) = 4,所以 0.75 * 4 = 3;
- 最后 action = -2 + 3 = 1。
- 这样就将原始 0.5 映射到动作区间中的 1。
'''
action = low_bound + (action + 1.0) * 0.5 * (upper_bound - low_bound)
'''
作用:使用 np.clip 限制映射后的动作不超出环境的动作范围。
例子:如果计算结果超过上界或低于下界,将被剪切。例如计算结果为 2.5,则 clip 后为 2;
若为 -2.5,则 clip 后为 -2。
'''
action = np.clip(action, low_bound, upper_bound)
return action
def reverse_action(self, action):
"""
作用:重写 reverse_action 方法,用于将环境动作转换回包装器中原始动作空间。
通常在一些算法中需要逆向转换(例如,回放数据时)。
例子:输入 action 为环境动作,如 1.0,转换回 [-1, 1] 范围。
"""
low_bound = self.action_space.low
upper_bound = self.action_space.high
'''
作用:执行逆向变换,将动作从 [low_bound, upper_bound] 映射回 [-1, 1]。公式解释:
- 首先计算 (action - low_bound) / (upper_bound - low_bound),
将 [low_bound, upper_bound] 映射为 [0, 1];
- 乘以 2 得到 [0, 2],减去 1 则变成 [-1, 1]。
数值例子:
- 若 action = 1.0(环境动作),low_bound = -2,upper_bound = 2,则:
- (1.0 - (-2)) = 3.0, (upper_bound - low_bound) = 4, 3.0/4 = 0.75;
- 0.75 * 2 = 1.5, 1.5 - 1 = 0.5;
- 得到逆向映射后的值为 0.5。
'''
action = 2 * (action - low_bound) / (upper_bound - low_bound) - 1
'''
作用:对逆向映射后的动作再做一次 clip(注意:这里 clip 范围仍用原始动作空间的边界,
通常不改变结果,但可以保证数值稳定)。
例子:如果计算结果异常,仍确保在 [low_bound, upper_bound] 内。
'''
action = np.clip(action, low_bound, upper_bound)
return action
def all_seed(env,seed = 1):
''' 万能的seed函数
'''
if not hasattr(env, 'seed'):
def seed_fn(self, seed=None):
env.reset(seed=seed)
return [seed]
env.seed = seed_fn.__get__(env, type(env))
# env.seed(seed) # env config
np.random.seed(seed)
random.seed(seed)
torch.manual_seed(seed) # config for CPU
torch.cuda.manual_seed(seed) # config for GPU
os.environ['PYTHONHASHSEED'] = str(seed) # config for python scripts
# config for cudnn
torch.backends.cudnn.deterministic = True # 保证使用确定性算法;
torch.backends.cudnn.benchmark = False # 关闭 CuDNN 的自动优化搜索;
torch.backends.cudnn.enabled = False # 禁用 CuDNN,从而确保每次计算结果一致。
def env_agent_config(cfg):
"""
作用:定义一个函数,用于根据配置 cfg 创建环境和智能体,并更新 cfg 中状态和动作维度信息。
参数说明:cfg 是一个包含各种配置参数(例如环境名称、种子、隐藏层大小等)的字典。
"""
'''
作用:
- 调用 gym.make(cfg['env_name']) 创建指定名称的环境;
- 使用 NormalizedActions 包装该环境,使得动作输出被重新映射到环境允许的范围。
'''
env = NormalizedActions(gym.make(cfg['env_name'])) # 装饰action噪声
if cfg['seed'] !=0:
all_seed(env,seed=cfg['seed'])
n_states = env.observation_space.shape[0]
n_actions = env.action_space.shape[0]
'''
作用:将计算得到的 n_states 和 n_actions 更新到配置字典中,
以便后续使用(例如构造网络时需要知道状态和动作维度)。
数值例子:
- 若 n_states=3, n_actions=1,则 cfg 中新增键值对 {"n_states": 3, "n_actions": 1}。
'''
cfg.update({"n_states":n_states,"n_actions":n_actions}) # 更新n_states和n_actions到cfg参数中
'''
作用:
- 根据更新后的 n_states 和 n_actions 以及配置中指定的隐藏层大小,创建 Actor 和 Critic 模型;
- 将两个模型放入一个字典 models 中,键分别为 "actor" 和 "critic"。
'''
models = {"actor":Actor(n_states,n_actions,hidden_dim=cfg['actor_hidden_dim']),
"critic":Critic(n_states,n_actions,hidden_dim=cfg['critic_hidden_dim'])}
'''
作用:使用配置中给定的 memory_capacity 创建一个 ReplayBuffer 对象,并放入字典 memories 中,键为 "memory"。
'''
memories = {"memory":ReplayBuffer(cfg['memory_capacity'])}
'''
作用:利用上面创建的 models、memories 和配置 cfg 构造一个 DDPG 智能体实例。
例子:agent 将封装 actor、critic 网络、目标网络、经验重放及相应的优化器等,便于后续训练和更新。
'''
agent = DDPG(models,memories,cfg)
return env,agent
"""4. 设置参数"""
import argparse
import matplotlib.pyplot as plt
import seaborn as sns
def get_args():
""" 超参数
"""
parser = argparse.ArgumentParser(description="hyperparameters")
parser.add_argument('--algo_name',default='DDPG',type=str,help="name of algorithm")
parser.add_argument('--env_name',default='Pendulum-v1',type=str,help="name of environment")
parser.add_argument('--train_eps',default=300,type=int,help="episodes of training")
parser.add_argument('--test_eps',default=20,type=int,help="episodes of testing")
parser.add_argument('--max_steps',default=100000,type=int,help="steps per episode, much larger value can simulate infinite steps")
parser.add_argument('--gamma',default=0.99,type=float,help="discounted factor")
parser.add_argument('--critic_lr',default=1e-3,type=float,help="learning rate of critic")
parser.add_argument('--actor_lr',default=1e-4,type=float,help="learning rate of actor")
parser.add_argument('--memory_capacity',default=8000,type=int,help="memory capacity")
parser.add_argument('--batch_size',default=128,type=int)
parser.add_argument('--target_update',default=2,type=int)
parser.add_argument('--tau',default=1e-2,type=float)
parser.add_argument('--critic_hidden_dim',default=256,type=int)
parser.add_argument('--actor_hidden_dim',default=256,type=int)
parser.add_argument('--device',default='cuda',type=str,help=" or cuda")
parser.add_argument('--seed',default=1,type=int,help="random seed")
args = parser.parse_args([])
args = {**vars(args)} # 将args转换为字典
# 打印参数
print("训练参数如下:")
print(''.join(['=']*80))
tplt = "{:^20}\t{:^20}\t{:^20}"
print(tplt.format("参数名","参数值","参数类型"))
for k,v in args.items():
print(tplt.format(k,v,str(type(v))))
print(''.join(['=']*80))
return args
def smooth(data, weight=0.9):
'''用于平滑曲线,类似于Tensorboard中的smooth
Args:
data (List):输入数据
weight (Float): 平滑权重,处于0-1之间,数值越高说明越平滑,一般取0.9
Returns:
smoothed (List): 平滑后的数据
'''
last = data[0] # First value in the plot (first timestep)
smoothed = list()
for point in data:
smoothed_val = last * weight + (1 - weight) * point # 计算平滑值
smoothed.append(smoothed_val)
last = smoothed_val
return smoothed
def plot_rewards(rewards,cfg,path=None,tag='train'):
sns.set()
plt.figure() # 创建一个图形实例,方便同时多画几个图
plt.title(f"{tag}ing curve on {cfg['device']} of {cfg['algo_name']} for {cfg['env_name']}")
plt.xlabel('epsiodes')
plt.plot(rewards, label='rewards')
plt.plot(smooth(rewards), label='smoothed')
plt.legend()
plt.show()
# 获取参数
cfg = get_args()
# 训练
env, agent = env_agent_config(cfg)
res_dic = train(cfg, env, agent)
plot_rewards(res_dic['rewards'], cfg, tag="train")
# 测试
res_dic = test(cfg, env, agent)
plot_rewards(res_dic['rewards'], cfg, tag="test") # 画出结果