深度强化学习实践 Maxim Lapan 章节6:深度Q-Network

这本书的前面的章节主要是之前的学习过的内容,我就不再重复了,这一章开始正式的开始deep reinforcement的内容的研究,我们将会参考DQN的实现的来完成一个atari的游戏的学习,原来的代码对于新的环境的适配程度非常差,所以我这边会一边的学习,一边的修改的代码。这一章因为我已经看完了,所以只是简单的介绍里面的内容,因为DQN并不是一个非常好用的学习模型,后面应该还会有很多优化的方案,所以我们只是学习到了12分左右。

初始的问题描述

SGD优化

q-learning的核心是表格学习,其中有一个非常重要的点是需要保证的数据的独立性,显然一次游戏的连续的画面不是一个独立的场景,这里我们需要的是使用了一个非常重要的工具 , 回放缓冲区(replay buffer),回放缓冲区可以解决,一个优化的方法是使用带优先级的回放缓冲区(关于这个概念,可以参考我们的专栏前面的文章里面的优先级回放)

步骤的相关性

显然我们的需要一个目标网络,为什么需要目标网络,是因为我们的Q值的结果来源于附近的步骤的学习,但是我们的现在使用的是函数逼近(深度学习),换句话说,我们的自举的内容,来源于我们自己的学习,这样会导致目标函数追逐自己的尾巴,从而陷入到一些非常遭的学习状态。

我们使用了目标网络,来表示过去的这个s的Q值,从而保证学习的稳定性 ,1000或者10000次迭代以后一次性更新

部分马尔科夫性质

显然我们无法使用一张图片表示很多动态的性质(例如速度,方向,等等),我们叫做部分可观察的马尔科夫POMDP,对于Atari的游戏,我们使用多个(k)个后续帧堆叠来形成一个堆叠的观察值,经典的k为4

DQN的训练的最终形式

  1. 初始化 ϵ \epsilon ϵ-greedy得我,初始化Q,和 Q ^ \hat Q Q^参数,清空缓冲回放区
  2. 使用概率 ϵ \epsilon ϵ来选择动作a,否则执行max的q值对应的action
  3. 观察奖励r和下一个状态s`
  4. 转移过程存放在回放缓冲区
  5. 随机采样一个小批量的转移过程
  6. 如果结束状态,则使用r直接来获取y,否则使用 r + γ m a x ( s ′ , a ′ ) r + \gamma max(s',a') r+γmax(s′,a′)来实现
  7. 计算损失函数L=(Q(s,a) - y)^2
  8. SGD优化器更新Q值
  9. 每经过N步,使用Q更新 Q ^ \hat Q Q^
    10.重复步骤2,直到游戏收敛

遇到的坑

  • env直接不可用
  • ale的库加载完毕以后,千万不要使用原来的gym的库的内容,会导致不可用
  • gym已经被新的库gymnasium替代,包含的step,reset的执行和返回参数异常
  • 经过了一晚上的学习,越来越学回去了,直接被-21分击败
  • 注意,继承的函数的调用过程是从外到内,但是内部函数执行的顺序是从内到外
  • 新版本的继承自gym.ObservationWrapper的reset操作需要reset里面执行seed和options的参数
    解决方案:只在observation的库上面执行reset函数的传递,判断是否支持seed和option函数,注意所有的调用的函数都需要正确的执行reset操作,否则可能会导致无法正确的执行,
  • 不同的版本的step的返回值不同,我们需要从头到尾全部返回新的参数的个数5个
  • 注意使用--cuda操作,发送到GPU进行学习加速度、
  • 注意如果被中断,重新学习的时候需要load模型(而且使用的是最后的最高的分数)
  • 注意需要保存frame的值,否则一开始全是随机,会导致多次间断学习的效率非常的低
  • 发现显存使用的不够,训练的很慢,于是创建了一个memory的类来分析合适的参数用于更快的学习(提升了20多倍的学习速度)
  • 不过优化GPU的显存本来就是我们刚接触这个内容的一个重要的性能的指标()
  • 注意加载模型的时候,也需要同步的更新目标网络的模型,不然就会导致学习的异常

代码

修改后的代码使用了新的gym模式gymnasium,里面使用了新的环境和reset的的获取和执行的方法,同时我们使用了新的skip=1少跳过一些frame

python 复制代码
import cv2
import gym
import gymnasium as gym
#from gymnasium import spaces
import numpy as np
import collections
import ale_py

    
class MaxAndSkipEnv(gym.Wrapper):
    def __init__(self, env=None, skip=4):
        """Return only every `skip`-th frame"""
        super(MaxAndSkipEnv, self).__init__(env)
        # most recent raw observations (for max pooling across time steps)
        self._obs_buffer = collections.deque(maxlen=2)
        self._skip = skip

    def step(self, action):
        total_reward = 0.0
        done = None
        truncated = None
        for _ in range(self._skip):
            obs, reward, done, truncated, info = self.env.step(action)
            self._obs_buffer.append(obs)
            total_reward += reward
            if done:
                break
        max_frame = np.max(np.stack(self._obs_buffer), axis=0)
        return max_frame, total_reward, done, truncated, info

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

#用于在reset的时候执行fire操作
class FireResetEnv(gym.Wrapper):
    def __init__(self, env):
        #执行fire操作
        super(FireResetEnv, self).__init__(env)
        assert env.unwrapped.get_action_meanings()[1] == 'FIRE'
        assert len(env.unwrapped.get_action_meanings()) >= 3

    def step(self, action): 
        return self.env.step(action)
    
    def reset(self):
        obs, info = self.env.reset()
        obs, _, done, truncated, _ = self.env.step(1)
        if done:
            obs, info = self.env.reset()
        obs, _, done, truncated, _ = self.env.step(2)
        return obs, info

class ProcessFrame84(gym.ObservationWrapper):
    def __init__(self, env=None):
        super(ProcessFrame84, self).__init__(env)
        self.observation_space = gym.spaces.Box(
            low=0, high=255, shape=(84, 84, 1), dtype=np.uint8)

    def observation(self, obs):
        return ProcessFrame84.process(obs)

    @staticmethod
    def process(frame):
        if frame.size == 210 * 160 * 3:
            img = np.reshape(frame, [210, 160, 3]).astype(
                np.float32)
        elif frame.size == 250 * 160 * 3:
            img = np.reshape(frame, [250, 160, 3]).astype(
                np.float32)
        else:
            assert False, "Unknown resolution."
        img = img[:, :, 0] * 0.299 + img[:, :, 1] * 0.587 + \
              img[:, :, 2] * 0.114
        resized_screen = cv2.resize(
            img, (84, 110), interpolation=cv2.INTER_AREA)
        x_t = resized_screen[18:102, :]
        x_t = np.reshape(x_t, [84, 84, 1])
        return x_t.astype(np.uint8)

    def reset(self,seed=None,options=None):
        # 只在这一层处理seed和options
        if seed is not None or options is not None:
            obs, info = self.env.env.reset(seed=seed, options=options)  # 直接访问原始环境
            obs = self.observation(obs)
            return obs, info
        else:
            obs, info = self.env.reset()  # 不带参数的reset



class ImageToPyTorch(gym.ObservationWrapper):
    def __init__(self, env):
        super(ImageToPyTorch, self).__init__(env)
        old_shape = self.observation_space.shape

        new_shape = (old_shape[-1], old_shape[0], old_shape[1])
        self.observation_space = gym.spaces.Box(
            low=0.0, high=1.0, shape=new_shape, dtype=np.float32)

    def observation(self, observation):
        return np.moveaxis(observation, 2, 0)


class ScaledFloatFrame(gym.ObservationWrapper):
    def observation(self, obs):
        return np.array(obs).astype(np.float32) / 255.0

class BufferWrapper(gym.ObservationWrapper):
    def __init__(self, env, n_steps, dtype=np.float32):
        super(BufferWrapper, self).__init__(env)

        self.dtype = dtype
        old_space = env.observation_space
        self.observation_space = gym.spaces.Box(
            old_space.low.repeat(n_steps, axis=0),
            old_space.high.repeat(n_steps, axis=0), dtype=dtype)

    def observation(self, observation):
        self.buffer[:-1] = self.buffer[1:]
        self.buffer[-1] = observation
        return self.buffer

    def reset(self,seed=None,options=None):
        self.buffer = np.zeros_like(
            self.observation_space.low, dtype=self.dtype)
        if seed is not None or options is not None:
            obs, info = self.env.reset(seed=seed, options=options)
            obs = self.observation(obs)
            return obs, info
        else:
            obs, info = self.env.reset()


def make_env(env_name):
    env = gym.make(env_name)
    env = MaxAndSkipEnv(env,skip=1)
    env = FireResetEnv(env)
    env = ProcessFrame84(env)
    env = ImageToPyTorch(env)
    env = BufferWrapper(env, 4)
    return ScaledFloatFrame(env)

学习的代码,这些代码在我的电脑上是可以直接运行的,前面的分数比较稳定,但是一旦超过了10分以后,学习就开始变的非常的困难,我觉得和我的环境的配置也有一定的关系,我觉得强化学习的弊端在这也显示了,就是很难去追踪和学习我们需要的内容,没法debug. Memory分析的库可以直接删除

python 复制代码
#!/usr/bin/env python3
from lib import wrappers
from lib import dqn_model
from lib.memory_analyzer import MemoryAnalyzer

import argparse
import time
import numpy as np
import collections

import torch
import torch.nn as nn
import torch.optim as optim
import os
from tensorboardX import SummaryWriter


DEFAULT_ENV_NAME = "ALE/Pong-v5"
MEAN_REWARD_BOUND = 19

GAMMA = 0.99
BATCH_SIZE = 64
REPLAY_SIZE = 10000
LEARNING_RATE = 1e-4
SYNC_TARGET_FRAMES = 1000
REPLAY_START_SIZE = 10000

EPSILON_DECAY_LAST_FRAME = 150000
EPSILON_START = 1.0
EPSILON_FINAL = 0.01


Experience = collections.namedtuple(
    'Experience', field_names=['state', 'action', 'reward',
                               'done', 'new_state'])


class ExperienceBuffer:
    def __init__(self, capacity):
        self.buffer = collections.deque(maxlen=capacity)

    def __len__(self):
        return len(self.buffer)

    def append(self, experience):
        self.buffer.append(experience)

    def sample(self, batch_size):
        indices = np.random.choice(len(self.buffer), batch_size,
                                   replace=False)
        states, actions, rewards, dones, next_states = \
            zip(*[self.buffer[idx] for idx in indices])
        return np.array(states), np.array(actions), \
               np.array(rewards, dtype=np.float32), \
               np.array(dones, dtype=np.uint8), \
               np.array(next_states)


class Agent:
    def __init__(self, env, exp_buffer):
        self.env = env
        self.exp_buffer = exp_buffer
        self._reset()

    def _reset(self):
        self.state, _ = self.env.reset(seed=42)
        self.total_reward = 0.0

    @torch.no_grad()
    def play_step(self, net, epsilon=0.0, device="cpu"):
        done_reward = None

        if np.random.random() < epsilon:
            action = env.action_space.sample()
        else:
            state_a = np.asarray([self.state], dtype=np.float32)
            state_v = torch.tensor(state_a).to(device)
            q_vals_v = net(state_v)
            _, act_v = torch.max(q_vals_v, dim=1)
            action = int(act_v.item())

        # do step in the environment
        new_state, reward, is_done, _, _ = self.env.step(action)
        self.total_reward += reward

        exp = Experience(self.state, action, reward,
                         is_done, new_state)
        self.exp_buffer.append(exp)
        self.state = new_state
        if is_done:
            done_reward = self.total_reward
            self._reset()
        return done_reward


def calc_loss(batch, net, tgt_net, device="cpu"):
    states, actions, rewards, dones, next_states = batch

    states_v = torch.tensor(np.array(
        states, copy=False)).to(device)
    next_states_v = torch.tensor(np.array(
        next_states, copy=False)).to(device)
    actions_v = torch.tensor(actions,dtype=torch.int64).to(device)
    rewards_v = torch.tensor(rewards).to(device)
    done_mask = torch.BoolTensor(dones).to(device)

    state_action_values = net(states_v).gather(
        1, actions_v.unsqueeze(-1)).squeeze(-1)
    with torch.no_grad():
        next_state_values = tgt_net(next_states_v).max(1)[0]
        next_state_values[done_mask] = 0.0
        next_state_values = next_state_values.detach()

    expected_state_action_values = next_state_values * GAMMA + \
                                   rewards_v
    return nn.MSELoss()(state_action_values,
                        expected_state_action_values)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--cuda", default=False,
                        action="store_true", help="Enable cuda")
    parser.add_argument("--env", default=DEFAULT_ENV_NAME,
                        help="Name of the environment, default=" +
                             DEFAULT_ENV_NAME)
    args = parser.parse_args()
    device = torch.device("cuda" if args.cuda else "cpu")
    print(device)
    env = wrappers.make_env(args.env)

    net = dqn_model.DQN(env.observation_space.shape,
                        env.action_space.n).to(device)
    tgt_net = dqn_model.DQN(env.observation_space.shape,
                            env.action_space.n).to(device)
    writer = SummaryWriter(comment="-" + args.env)
    print(net)

    buffer = ExperienceBuffer(REPLAY_SIZE)
    agent = Agent(env, buffer)
    epsilon = EPSILON_START

    optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE)
    total_rewards = []
    frame_idx = 0
    ts_frame = 0
    ts = time.time()
    best_m_reward = None

    # 如果存在模型目录,加载最好的模型和训练状态
    if os.path.exists(args.env):
        # 查找分数最高的模型
        files = os.listdir(args.env)
        best_m_reward = None
        for file in files:
            if file.startswith("best_") and file.endswith(".dat"):
                score = float(file[5:-4])  # 从文件名提取分数
                if best_m_reward is None or score > best_m_reward:
                    best_m_reward = score
        print(f"best_m_reward: {best_m_reward}")
        if best_m_reward is not None:
            # 加载模型权重
            net.load_state_dict(torch.load(os.path.join(args.env, f"best_{best_m_reward:.0f}.dat")))
            #此时的target_net和net的参数是一样的,
            tgt_net.load_state_dict(net.state_dict())
            # 加载优化器状态
            if os.path.exists(os.path.join(args.env, f"optimizer_{best_m_reward:.0f}.dat")):
                print(f"加载优化器状态: {os.path.join(args.env, f'optimizer_{best_m_reward:.0f}.dat')}")
                optimizer.load_state_dict(torch.load(os.path.join(args.env, f"optimizer_{best_m_reward:.0f}.dat")))
            if best_m_reward > 0:
                frame_idx = 200000


    # 在创建环境和网络之后
    analyzer = MemoryAnalyzer(env, net, REPLAY_SIZE, BATCH_SIZE)
    analyzer.analyze()

    # 获取建议的批量大小
    suggested_batch_size, message = analyzer.suggest_batch_size(target_memory_usage=0.7)
    print(message)

    BATCH_SIZE = suggested_batch_size*8

    # 相应地调整优化器
    #optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE)

    while True:
        frame_idx += 1
        epsilon = max(EPSILON_FINAL, EPSILON_START -
                      frame_idx / EPSILON_DECAY_LAST_FRAME)

        reward = agent.play_step(net, epsilon, device=device)
        if reward is not None:
            total_rewards.append(reward)
            speed = (frame_idx - ts_frame) / (time.time() - ts)
            ts_frame = frame_idx
            ts = time.time()
            m_reward = np.mean(total_rewards[-100:])
            print("%d: done %d games, reward %.3f, "
                  "eps %.2f, speed %.2f f/s" % (
                frame_idx, len(total_rewards), m_reward, epsilon,
                speed
            ))
            writer.add_scalar("epsilon", epsilon, frame_idx)
            writer.add_scalar("speed", speed, frame_idx)
            writer.add_scalar("reward_100", m_reward, frame_idx)
            writer.add_scalar("reward", reward, frame_idx)
            if best_m_reward is None or best_m_reward < m_reward:
                os.makedirs(args.env, exist_ok=True)
                # 保存模型权重
                torch.save(net.state_dict(), os.path.join(args.env, f"best_{m_reward:.0f}.dat"))
                # 保存优化器状态
                torch.save(optimizer.state_dict(), os.path.join(args.env, f"optimizer_{m_reward:.0f}.dat"))
                if best_m_reward is not None:
                    print("Best reward updated %.3f -> %.3f" % (
                        best_m_reward, m_reward))
                best_m_reward = m_reward
            if m_reward > MEAN_REWARD_BOUND:
                print("Solved in %d frames!" % frame_idx)
                break

        if len(buffer) < REPLAY_START_SIZE:
            continue

        if frame_idx % SYNC_TARGET_FRAMES == 0:
            tgt_net.load_state_dict(net.state_dict())

        optimizer.zero_grad()
        batch = buffer.sample(BATCH_SIZE)
        loss_t = calc_loss(batch, net, tgt_net, device=device)
        loss_t.backward()
        optimizer.step()
    writer.close()

结果

我这边最多可以学习到+14分的样子,对于中断的后学习的效果很一般,有可能会导致loss函数出现回落的情况,其实我觉得这里依然有过拟合的问题,我们可以尝试加入一些深度学习常见的解决过拟合问题的方式,例如dropout或者其他,不同的学习的超参自然会带来不同的结果,我自己的显卡也很一般,学习一次的成本其实也挺高的。

我个人觉得学习的有点蠢 ,但是因为v5的版本是有一定的滑动的25%的概率action会失效,我觉得也是学习遇到瓶颈的问题之一。

相关推荐
格林威几秒前
Baumer工业相机堡盟工业相机使用不同内外同轴光源进行检测的不同效果
人工智能·数码相机·计算机视觉
Artificial Idiots4 分钟前
Computer Vision Arxiv Daily 2025.02.07
人工智能·深度学习·机器学习·计算机视觉·computer vision
xiao-chong5 分钟前
预训练语言模型(笔记)
人工智能·深度学习
MYT_flyflyfly6 分钟前
计算机视觉-拟合
人工智能·计算机视觉
源代码•宸6 分钟前
A Normalized Gaussian Wasserstein Distance for Tiny Object Detection(纯翻译)
人工智能·经验分享·算法·目标检测·计算机视觉·tod
选择不变6 分钟前
AI眼镜-推理成本降低将加速端侧硬件智能化-AI 眼镜、AI玩具、手机AI化
大数据·人工智能·通达信指标公式·炒股技巧·炒股指标·ai眼镜
苏州稳联6 分钟前
边缘计算网关驱动智慧煤矿智能升级——实时预警、低延时决策与数字孪生护航矿山安全高效运营
人工智能·安全·边缘计算
jndingxin7 分钟前
OpenCV2D 特征框架 (19)目标检测类cv::CascadeClassifier的使用
人工智能·目标检测·目标跟踪
小赖同学啊8 分钟前
人工智能应用-智能驾驶精确的目标检测和更高级的路径规划
人工智能·目标检测·计算机视觉
拓端研究室29 分钟前
【专题】2025年我国机器人产业发展形势展望:人形机器人量产及商业化关键挑战报告汇总PDF洞察(附原数据表)
大数据·人工智能