凯鲁嘎吉
用书写铭记日常,最迷人的不在远方

Deep Reinforcement Learning Hands-On——Higher-Level RL Libraries (PTAN)

作者:凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/

更多请看:Reinforcement Learning - 随笔分类 - 凯鲁嘎吉 - 博客园 https://www.cnblogs.com/kailugaji/category/2038931.html

本文代码下载:https://github.com/kailugaji/Hands-on-Reinforcement-Learning/tree/main/02%20Higher-Level%20RL%20Libraries%20(PTAN)

    这一篇博文参考了书目《Deep Reinforcement Learning Hands-On Second Edition》第7章内容,主要介绍一个高级强化学习库:PyTorch Agent Net (PTAN)。用Python从头实现DQN及其他强化学习算法是复杂的,代码量较大,而且不同算法可能会一次又一次地编写相同的代码,调试起来困难。PTAN库将常用的强化学习代码封装起来,从而简化代码量,便于调试。下面通过6个Python程序来学会使用PTAN。前5个程序告诉我们如何调用PTAN库函数,为第6个程序做铺垫,第6个程序以gym中的CartPole游戏为例,结合PTAN库实现DQN算法,这里只是简易版的DQN(网络架构不是三卷积两全连接,简化为两全连接)。DQN的算法流程参见:2.4.3 深度Q网络(Deep Q-Networks,DQN)

    PTAN的详细代码见:https://github.com/Shmuma/ptan

    PTAN的安装(PTAN需要与Torch匹配):

pip install torch==1.7.0+cpu torchvision==0.8.1+cpu torchaudio===0.7.0 -f https://download.pytorch.org/whl/torch_stable.html
pip install ptan

    CartPole是推杆子游戏,争取让杆子立起来,有两个动作:向左和向右,有四个状态变量:小车在轨道上的位置,杆子与竖直方向的夹角,小车速度,角度变化率。杆子能越长时间保持平衡,得分越高。

1. 01_actions.py

1.1 程序

#!/usr/bin/env python3
# -*- coding=utf-8 -*-
# The PTAN library——Action selectors 动作选择器
# 从网络输出(Q值)到具体的动作值
# https://www.cnblogs.com/kailugaji/
import ptan
import numpy as np

if __name__ == "__main__":
    print("方法1:基于值函数的方法 (网络输出的是Q值)")
    q_vals_1 = np.array([
        [1, 2, 3],
        [1, -1, 0]
    ]) # 事先定义网络输出的Q值
    print("Q值:\n", q_vals_1)

    selector = ptan.actions.ArgmaxActionSelector()
    print("具有最大Q值的动作索引:", selector(q_vals_1))
    # 返回具有最大Q值的动作的索引——[列,行]

    print("采用epsilon贪心策略的动作索引:")
    selector = ptan.actions.EpsilonGreedyActionSelector(epsilon=0.0) # 以epsilon的概率随机选择值
    print("当epsilon=0.0:", selector(q_vals_1)) # no random actions

    selector.epsilon = 1.0 # will be random
    print("当epsilon=1.0:", selector(q_vals_1))

    selector.epsilon = 0.5
    print("当epsilon=0.5:", selector(q_vals_1))

    selector.epsilon = 0.1
    print("当epsilon=0.1:", selector(q_vals_1))

    print("-----------------------------------------------------")
    print("方法2:基于策略函数的方法 (网络输出的是标准化概率分布)")
    print("从三个概率分布中采样得到的动作:")
    q_vals_2 = np.array([
        [0.1, 0.8, 0.1], # 分布0 # 行归一化
        [0.0, 0.0, 1.0], # 分布1
        [0.5, 0.5, 0.0]  # 分布2
    ]) # 事先定义网络输出的概率分布
    # 从三个分布中进行抽样:
    # 在第一个分布中,选择索引为1的动作的概率为80%
    # 在第二个分布中,总是选择索引为2的动作
    # 在第三个分布中,选择索引为0的动作和索引为1的动作是等可能的
    selector = ptan.actions.ProbabilityActionSelector()
    # 从概率分布中采样 (输入必须是一个标准化的概率分布)
    for i in range(8): # 采样8次
        acts = selector(q_vals_2)
        print('第 %d 次: ' %(i+1), acts)
        # acts的三个值分别是从三个分布中采样的动作的索引
        # 可以看到第二个值始终是2,这是因为第二个分布中索引为2的动作的概率为1

1.2 结果

方法1:基于值函数的方法 (网络输出的是Q值)
Q值:
 [[ 1  2  3]
 [ 1 -1  0]]
具有最大Q值的动作索引: [2 0]
采用epsilon贪心策略的动作索引:
当epsilon=0.0: [2 0]
当epsilon=1.0: [2 2]
当epsilon=0.5: [2 1]
当epsilon=0.1: [2 0]
-----------------------------------------------------
方法2:基于策略函数的方法 (网络输出的是标准化概率分布)
从三个概率分布中采样得到的动作:
第 1 次:  [1 2 1]
第 2 次:  [1 2 1]
第 3 次:  [1 2 1]
第 4 次:  [1 2 0]
第 5 次:  [1 2 0]
第 6 次:  [1 2 0]
第 7 次:  [2 2 0]
第 8 次:  [1 2 0]

2. 02_agents.py

2.1 程序

#!/usr/bin/env python3
# -*- coding=utf-8 -*-
# The PTAN library——The agent
# https://www.cnblogs.com/kailugaji/
import ptan
import torch
import torch.nn as nn

# 方法1:基于值函数的方法 (网络输出的是Q值)
# DQNAgent
class DQNNet(nn.Module):
    def __init__(self, actions: int):
        super(DQNNet, self).__init__()
        self.actions = actions # 为简单起见,网络输出和输入一致, f(x)=x

    def forward(self, x):
        return torch.eye(x.size()[0], self.actions)
    # 定义了返回对角线全1,其余部分全0的二维数组,大小为(batch_size=x.size()[0], actions)

# 方法2:基于策略函数的方法 (网络输出的是标准化概率分布)
# PolicyAgent
class PolicyNet(nn.Module):
    def __init__(self, actions: int):
        super(PolicyNet, self).__init__()
        self.actions = actions # 为简单起见,网络输出和输入一致, f(x)=x

    def forward(self, x):
        # Now we produce the tensor with first two actions having the same logit scores
        shape = (x.size()[0], self.actions) # 大小为(batch_size=x.size()[0], actions)
        res = torch.zeros(shape, dtype=torch.float32)
        res[:, 0] = 1
        res[:, 1] = 1 # 定义了返回前两列为1,后面为0的二维数组
        return res


if __name__ == "__main__":
    net_1 = DQNNet(actions=3) # 3个动作(3列/3维)

    print("方法1:基于值函数的方法 (网络输出的是Q值)")
    net_in = torch.zeros(2, 10) # 输入2*10的全0矩阵,样本个数2,维度10
    net_out = net_1(net_in)
    print("DQN Net 输入:\n", net_in)
    print("DQN Net 输出:\n", net_out)
    # 得到对角线全1,其余部分全0的矩阵,大小为(batch_size=2, actions=3)

    selector = ptan.actions.ArgmaxActionSelector()
    agent = ptan.agent.DQNAgent(dqn_model=net_1, action_selector=selector)
    # dqn_model换成自定义的DQNNet模型,action_selector保持不变,例子可见上一个程序01_actions.py
    ag_in = torch.zeros(2, 5) # 输入:2*5的全0矩阵,样本个数2,维度5 (a batch of two observations, each having five values)
    ag_out = agent(ag_in)
    print("DQN网络输入:\n", ag_in)
    print("具有最大Q值的动作与状态索引:", ag_out)
    # 输出动作与状态的索引
    # 1. 动作矩阵:网络输出中对应于1的动作索引,有2个样本,因此结果矩阵大小为1*2
    # 2. 状态列表:由于例子未涉及状态,因此为None

    print("采用epsilon贪心策略得到的动作索引:")
    selector = ptan.actions.EpsilonGreedyActionSelector(epsilon=0.0) # no random actions
    agent = ptan.agent.DQNAgent(dqn_model=net_1, action_selector=selector)
    ag_in = torch.zeros(10, 5) # 输入:10*5的全0矩阵,10个样本
    ag_out = agent(ag_in)[0] # [0]表示只返回动作的索引,不返回状态的索引
    print("当epsilon=0:", ag_out) # DQNNet中actions=3使得第4维及后面索引全为0

    selector.epsilon = 1.0 # 当epsilon为1时,所有的动作都是随机的,与网络的输出无关
    ag_out = agent(ag_in)[0]
    print("当epsilon=1:", ag_out)

    selector.epsilon = 0.5
    ag_out = agent(ag_in)[0]
    print("当epsilon=0.5:", ag_out)

    selector.epsilon = 0.1
    ag_out = agent(ag_in)[0]
    print("当epsilon=0.1:", ag_out)

    print("----------------------------------------------------------------")
    net_2 = PolicyNet(actions=5) # 5个动作(5列),0-4

    print("方法2:基于策略函数的方法 (网络输出的是标准化概率分布)")
    net_in = torch.zeros(6, 10) # 输入:6*10的全0矩阵,6个样本
    net_out = net_2(net_in)
    print("Policy Net 输入:\n", net_in)
    print("Policy Net 输出:\n", net_out)

    selector = ptan.actions.ProbabilityActionSelector()
    agent = ptan.agent.PolicyAgent(model=net_2, action_selector=selector, apply_softmax=True)
    # 对输出再采用softmax将数值归一化为[0, 1]的概率分布值
    ag_in = torch.zeros(6, 5) # 输入:6*5的全0矩阵,6个样本
    ag_out = agent(ag_in)[0]
    print("Policy网络输入:\n", ag_in)
    print("采样Policy方法得到的动作索引:", ag_out)
    # 采样索引为2-4的动作概率小于0与1

2.2 结果

方法1:基于值函数的方法 (网络输出的是Q值)
DQN Net 输入:
 tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
DQN Net 输出:
 tensor([[1., 0., 0.],
        [0., 1., 0.]])
DQN网络输入:
 tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])
具有最大Q值的动作与状态索引: (array([0, 1], dtype=int64), [None, None])
采用epsilon贪心策略得到的动作索引:
当epsilon=0: [0 1 2 0 0 0 0 0 0 0]
当epsilon=1: [1 1 0 2 1 0 2 0 0 0]
当epsilon=0.5: [2 2 2 0 0 1 0 0 0 0]
当epsilon=0.1: [0 1 2 0 0 0 0 0 0 0]
----------------------------------------------------------------
方法2:基于策略函数的方法 (网络输出的是标准化概率分布)
Policy Net 输入:
 tensor([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
Policy Net 输出:
 tensor([[1., 1., 0., 0., 0.],
        [1., 1., 0., 0., 0.],
        [1., 1., 0., 0., 0.],
        [1., 1., 0., 0., 0.],
        [1., 1., 0., 0., 0.],
        [1., 1., 0., 0., 0.]])
Policy网络输入:
 tensor([[0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0.]])
采样Policy方法得到的动作索引: [2 2 0 4 4 4]

3. 03_exp_sources.py

3.1 程序

#!/usr/bin/env python3
# -*- coding=utf-8 -*-
# The PTAN library——Experience source
# 是对智能体在环境中运行过程的一种封装,屏蔽了很多运行细节,最终只返回运行记录以用于训练模型
# 常用的两个封装类有:ExperienceSource,ExperienceSourceFirstLast(推荐使用)
# 部分参考: https://blog.csdn.net/HJJ19881016/article/details/105743835/
# https://www.cnblogs.com/kailugaji/
import gym
import ptan
from typing import List, Optional, Tuple, Any

# 构建Environment
class ToyEnv(gym.Env):
    """
    Environment with observation 0..4 and actions 0..2
    Observations are rotated sequentialy mod 5, reward is equal to given action.
    Episodes are having fixed length of 10
    """
    def __init__(self):
        super(ToyEnv, self).__init__()
        self.observation_space = gym.spaces.Discrete(n=5) # integer observation, which increases from 0 to 4
        self.action_space = gym.spaces.Discrete(n=3) # integer action, which increases from 0 to 2
        self.step_index = 0

    def reset(self): # 用于重置环境
        self.step_index = 0
        return self.step_index

    def step(self, action):
    # 输入:action
    # 输出:observation, reward, done, info
    # observation(object)一个特定的环境对象,代表了你从环境中得到的观测值
    # reward(float)由于之前采取的动作所获得的大量奖励,与环境交互的过程中,奖励值的规模会发生变化,但是总体的目标一直都是使得总奖励最大
    # done(boolean)决定是否将环境初始化,大多数,但不是所有的任务都被定义好了什么情况该结束这个回合
    # info(dict)调试过程中将会产生的有用信息,有时它会对我们的强化学习学习过程很有用
        is_done = self.step_index == 10 # 一局游戏走10步
        if is_done:
            return self.step_index % self.observation_space.n, 0.0, is_done, {}
        # Observation: mod 5, 0-4一循环,依次递增
        self.step_index += 1
        reward = float(action)
        return self.step_index % self.observation_space.n, reward, self.step_index == 10, {}
        # 这里定义了reward = action,info = {}, 玩够10步done=True

# 构建Agent
# 继承BaseAgent来自定义自己的Agent类,通过重写__call__()方法来实现Obervation到action的转换逻辑
class DullAgent(ptan.agent.BaseAgent):
    """
    Agent always returns the fixed action
    """
    def __init__(self, action: int):
        self.action = action

    def __call__(self, observations: List[Any], state: Optional[List] = None) -> Tuple[List[int], Optional[List]]:
    # "->"常常出现在python函数定义的函数名后面,为函数添加元数据,描述函数的返回类型,从而方便开发人员使用
    # 不管observations输入的是什么,结果都是输入的action的值
        return [self.action for _ in observations], state


if __name__ == "__main__":
    print("案例I:")
    env = ToyEnv()
    s = env.reset()
    print("env.reset() -> %s" % s)
    s = env.step(1) # action = 1
    print("env.step(1) -> %s" % str(s))
    s = env.step(2) # action = 2
    print("env.step(2) -> %s" % str(s))
    # 输出:observation, reward, done, info

    for i in range(10):
        r = env.step(0) # action = 0
        print("第 %d 次 env.step(0) -> %s" % (i, str(r)))
    # 重复10次,action的索引为0
    # 输出:observation, reward, done, info

    print("-------------------------------------------------------------------")
    print("案例II:")
    agent = DullAgent(action=1) # 生成固定动作,与action的取值保持一致,与observations取值无关
    print("agent:", agent(observations=[2, 1, 3, 1])[0])
    # [1, 2]: observations
    # [0]只输出动作索引

    print("-------------------------------------------------------------------")
    print("案例III:")
    env = ToyEnv()
    agent = DullAgent(action=1) # 生成固定动作,始终为1
    print("1. ExperienceSource (steps_count=2): ")
    exp_source_1 = ptan.experience.ExperienceSource(env, agent, steps_count=2)
    # ExperienceSource输入:
    # env: The Gym environment to be used. Alternatively, it could be the list of environments.
    # agent: The agent instance.
    # steps_count: 用于说明一条记录中包含的步(step)数 (sub-trajectories of length 2)
    # ExperienceSource输出:
    # 返回智能体在环境中每一步的交互信息,输出格式为:(state, action, reward, done)
    # 其中state为agent所处的状态,action为采取的动作,reward为采取action后获得的即时奖励,done用来标识episode是否结束。
    for idx, exp in enumerate(exp_source_1):
        if idx > 15:
            break
        print("第%d步" %(idx), exp)

    print("2. ExperienceSource (steps_count=4): ")
    exp_source_2 = ptan.experience.ExperienceSource(env, agent, steps_count=4)
    # print(next(iter(exp_source_2))) # 只一步
    # iter()返回迭代器对象
    # next()函数自动调用文件第一行并返回下一行
    for idx, exp in enumerate(exp_source_2):
        if exp[0].done:
            break
        print("第%d步" %(idx), exp)

    print("3. ExperienceSource (steps_count=2): ")
    exp_source_3 = ptan.experience.ExperienceSource([ToyEnv(), ToyEnv()], agent, steps_count=2)
    # 环境正在以循环的方式迭代,从两个环境中一步步获取轨迹。
    for idx, exp in enumerate(exp_source_3):
        if idx > 20:
            break
        print("第%d步" %(idx), exp)

    print("4. ExperienceSourceFirstLast (steps_count=1): ")
    exp_source_4 = ptan.experience.ExperienceSourceFirstLast(env, agent, gamma=1.0, steps_count=1)
    # 输出的信息格式为:(state, action, reward, last_state)
    # 并不会输出每一步的信息,而是把多步的交互结果综合(累计多步的reward;显示头尾的状态)到一条Experience输出
    # 多步rewards的累加是有衰退的,而其中的衰退系数由参数gamma(折扣率)指定,即reward=r1+gamma∗r2+(gamma^2)∗r3
    # 其中rn代表第n步操作获得的reward
    # last_state: the state we've got after executing the action. If our episode ends, we have None here
    for idx, exp in enumerate(exp_source_4):
        print("第%d步" %(idx), exp)
        if idx > 10:
            break

    print("5. ExperienceSourceFirstLast (steps_count=4): ")
    exp_source_5 = ptan.experience.ExperienceSourceFirstLast(env, agent, gamma=0.6, steps_count=4)
    # 输出的信息格式为:(state, action, reward, last_state)
    # 并不会输出每一步的信息,而是把多步的交互结果综合(累计多步的reward;显示头尾的状态)到一条Experience输出
    # 多步rewards的累加是有衰退的,而其中的衰退系数由参数gamma指定,即reward=r1+gamma∗r2+(gamma^2)∗r3
    # 其中rn代表第n步操作获得的reward
    # last_state: the state we've got after executing the action. If our episode ends, we have None here
    for idx, exp in enumerate(exp_source_5):
        print("第%d步" % (idx), exp)
        if idx > 10:
            break

3.2 结果

案例I:
env.reset() -> 0
env.step(1) -> (1, 1.0, False, {})
env.step(2) -> (2, 2.0, False, {})
第 0 次 env.step(0) -> (3, 0.0, False, {})
第 1 次 env.step(0) -> (4, 0.0, False, {})
第 2 次 env.step(0) -> (0, 0.0, False, {})
第 3 次 env.step(0) -> (1, 0.0, False, {})
第 4 次 env.step(0) -> (2, 0.0, False, {})
第 5 次 env.step(0) -> (3, 0.0, False, {})
第 6 次 env.step(0) -> (4, 0.0, False, {})
第 7 次 env.step(0) -> (0, 0.0, True, {})
第 8 次 env.step(0) -> (0, 0.0, True, {})
第 9 次 env.step(0) -> (0, 0.0, True, {})
-------------------------------------------------------------------
案例II:
agent: [1, 1, 1, 1]
-------------------------------------------------------------------
案例III:
1. ExperienceSource (steps_count=2): 
第0步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第1步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第2步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第3步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False))
第4步 (Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False))
第5步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第6步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第7步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第8步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=True))
第9步 (Experience(state=4, action=1, reward=1.0, done=True),)
第10步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第11步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第12步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第13步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False))
第14步 (Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False))
第15步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
2. ExperienceSource (steps_count=4): 
第0步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第1步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False))
第2步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False))
第3步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第4步 (Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第5步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第6步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=True))
第7步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=True))
第8步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=True))
3. ExperienceSource (steps_count=2): 
第0步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第1步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第2步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第3步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第4步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第5步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第6步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False))
第7步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=False))
第8步 (Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False))
第9步 (Experience(state=4, action=1, reward=1.0, done=False), Experience(state=0, action=1, reward=1.0, done=False))
第10步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第11步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
第12步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第13步 (Experience(state=1, action=1, reward=1.0, done=False), Experience(state=2, action=1, reward=1.0, done=False))
第14步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第15步 (Experience(state=2, action=1, reward=1.0, done=False), Experience(state=3, action=1, reward=1.0, done=False))
第16步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=True))
第17步 (Experience(state=4, action=1, reward=1.0, done=True),)
第18步 (Experience(state=3, action=1, reward=1.0, done=False), Experience(state=4, action=1, reward=1.0, done=True))
第19步 (Experience(state=4, action=1, reward=1.0, done=True),)
第20步 (Experience(state=0, action=1, reward=1.0, done=False), Experience(state=1, action=1, reward=1.0, done=False))
4. ExperienceSourceFirstLast (steps_count=1): 
第0步 ExperienceFirstLast(state=0, action=1, reward=1.0, last_state=1)
第1步 ExperienceFirstLast(state=1, action=1, reward=1.0, last_state=2)
第2步 ExperienceFirstLast(state=2, action=1, reward=1.0, last_state=3)
第3步 ExperienceFirstLast(state=3, action=1, reward=1.0, last_state=4)
第4步 ExperienceFirstLast(state=4, action=1, reward=1.0, last_state=0)
第5步 ExperienceFirstLast(state=0, action=1, reward=1.0, last_state=1)
第6步 ExperienceFirstLast(state=1, action=1, reward=1.0, last_state=2)
第7步 ExperienceFirstLast(state=2, action=1, reward=1.0, last_state=3)
第8步 ExperienceFirstLast(state=3, action=1, reward=1.0, last_state=4)
第9步 ExperienceFirstLast(state=4, action=1, reward=1.0, last_state=None)
第10步 ExperienceFirstLast(state=0, action=1, reward=1.0, last_state=1)
第11步 ExperienceFirstLast(state=1, action=1, reward=1.0, last_state=2)
5. ExperienceSourceFirstLast (steps_count=4): 
第0步 ExperienceFirstLast(state=0, action=1, reward=2.176, last_state=4)
第1步 ExperienceFirstLast(state=1, action=1, reward=2.176, last_state=0)
第2步 ExperienceFirstLast(state=2, action=1, reward=2.176, last_state=1)
第3步 ExperienceFirstLast(state=3, action=1, reward=2.176, last_state=2)
第4步 ExperienceFirstLast(state=4, action=1, reward=2.176, last_state=3)
第5步 ExperienceFirstLast(state=0, action=1, reward=2.176, last_state=4)
第6步 ExperienceFirstLast(state=1, action=1, reward=2.176, last_state=None)
第7步 ExperienceFirstLast(state=2, action=1, reward=1.96, last_state=None)
第8步 ExperienceFirstLast(state=3, action=1, reward=1.6, last_state=None)
第9步 ExperienceFirstLast(state=4, action=1, reward=1.0, last_state=None)
第10步 ExperienceFirstLast(state=0, action=1, reward=2.176, last_state=4)
第11步 ExperienceFirstLast(state=1, action=1, reward=2.176, last_state=0)

4. 04_replay_buf.py

4.1 程序

#!/usr/bin/env python3
# -*- coding=utf-8 -*-
# The PTAN library——Experience replay buffers 经验回放池
# 在DQN中,很少处理即时的经验样本,因为它们是高度相关的,这导致了训练中的不稳定性
# 构建一个很大的经验回放池,其中填充了经验片段
# 然后对回放池进行采样(随机或带优先级权重),得到训练批。
# 经验回放池通常有最大容量,所以当经验回放池达到极限时,旧的样本将被推出。
# 训练时,随机从经验池中抽取样本来代替当前的样本用来进行训练。
# 这样,就打破了和相邻训练样本的相似性,避免模型陷入局部最优
# https://www.cnblogs.com/kailugaji/
import gym
import ptan
from typing import List, Optional, Tuple, Any

# 构建Environment
class ToyEnv(gym.Env):
    """
    Environment with observation 0..4 and actions 0..2
    Observations are rotated sequentialy mod 5, reward is equal to given action.
    Episodes are having fixed length of 10
    """
    def __init__(self):
        super(ToyEnv, self).__init__()
        self.observation_space = gym.spaces.Discrete(n=5) # integer observation, which increases from 0 to 4
        self.action_space = gym.spaces.Discrete(n=3) # integer action, which increases from 0 to 2
        self.step_index = 0

    def reset(self):
        self.step_index = 0
        return self.step_index

    def step(self, action):
    # 输入:action
    # 输出:observation, reward, done, info
        is_done = self.step_index == 10 # 一局游戏走10步
        if is_done:
            return self.step_index % self.observation_space.n, 0.0, is_done, {}
        self.step_index += 1
        reward = float(action)
        return self.step_index % self.observation_space.n, reward, self.step_index == 10, {}
        # Observation: mod 5, 0-4一循环,依次递增

# 构建Agent
class DullAgent(ptan.agent.BaseAgent):
    """
    Agent always returns the fixed action
    """
    def __init__(self, action: int):
        self.action = action

    def __call__(self, observations: List[Any], state: Optional[List] = None) -> Tuple[List[int], Optional[List]]:
        # 不管observations输入的是什么,结果都是输入的action的值
        return [self.action for _ in observations], state


if __name__ == "__main__":
    env = ToyEnv()
    agent = DullAgent(action=1) # 生成固定动作,与action的取值保持一致,与observations取值无关
    exp_source = ptan.experience.ExperienceSourceFirstLast(env, agent, gamma=1.0, steps_count=1)
    # 输出的信息格式为:(state, action, reward, last_state)
    buffer = ptan.experience.ExperienceReplayBuffer(exp_source, buffer_size=100)
    # a simple replay buffer of predefined size with uniform sampling.
    # 构建buffer,容量为100,当前没东西,len(buffer) = 0

    for step in range(6): # 最大buffer进6个样本
        buffer.populate(1) # 从环境中获取一个新样本
        # The method populate(N) to get N samples from the experience source and put them into the buffer
        print("第%d次buffer大小:" %step, len(buffer))
        if len(buffer) < 5: # buffer里面还没超过5个样本
            continue # if buffer is small enough (<5), do nothing
        # buffer等于或超过5个后,从buffer里面均匀抽样一个批次的样本,一批4个样本
        batch = buffer.sample(4) # The method sample(N) to get the batch of N experience objects
        print("Train time, %d batch samples:" % len(batch))
        for s in batch:
            print(s)

4.2 结果

第0次buffer大小: 1
第1次buffer大小: 2
第2次buffer大小: 3
第3次buffer大小: 4
第4次buffer大小: 5
Train time, 4 batch samples:
ExperienceFirstLast(state=0, action=1, reward=1.0, last_state=1)
ExperienceFirstLast(state=1, action=1, reward=1.0, last_state=2)
ExperienceFirstLast(state=0, action=1, reward=1.0, last_state=1)
ExperienceFirstLast(state=3, action=1, reward=1.0, last_state=4)
第5次buffer大小: 6
Train time, 4 batch samples:
ExperienceFirstLast(state=2, action=1, reward=1.0, last_state=3)
ExperienceFirstLast(state=3, action=1, reward=1.0, last_state=4)
ExperienceFirstLast(state=4, action=1, reward=1.0, last_state=0)
ExperienceFirstLast(state=0, action=1, reward=1.0, last_state=1)

5. 05_target_net.py

5.1 程序

#!/usr/bin/env python3
# -*- coding=utf-8 -*-
# The PTAN library——The TargetNet class
# TargetNet允许我们同步具有相同架构的两个网络,其目的是为了提高训练稳定性
# https://www.cnblogs.com/kailugaji/
import ptan
import torch.nn as nn

# 创建网络
class DQNNet(nn.Module):
    def __init__(self):
        super(DQNNet, self).__init__()
        self.ff = nn.Linear(5, 3) # in_features=5, out_features=3, 权重大小:(3, 5)

    def forward(self, x):
        return self.ff(x)


if __name__ == "__main__":
    net = DQNNet()
    print("原网络架构:\n", net)
    tgt_net = ptan.agent.TargetNet(net)
    print("原网络权重:", net.ff.weight)
    print("目标网络权重:", tgt_net.target_model.ff.weight)
    # 上述原网络与目标网络权重相同

    # 然而,它们彼此独立,只是拥有相同的架构:
    net.ff.weight.data += 1.0
    print("-------------------------------------------------------------------")
    print("更新后:")
    print("原网络权重:", net.ff.weight)
    print("目标网络权重:", tgt_net.target_model.ff.weight)

    # 要再次同步它们,可以使用sync()方法
    tgt_net.sync() # weights from the source network are copied into the target network
    print("-------------------------------------------------------------------")
    print("同步后:")
    print("原网络权重:", net.ff.weight)
    print("目标网络权重:", tgt_net.target_model.ff.weight)

5.2 结果

原网络架构:
 DQNNet(
  (ff): Linear(in_features=5, out_features=3, bias=True)
)
原网络权重: Parameter containing:
tensor([[-0.0103,  0.4268,  0.2549,  0.1492,  0.2748],
        [ 0.0375, -0.0403,  0.0326,  0.0213,  0.1052],
        [-0.1674, -0.3298, -0.0271, -0.1609,  0.3070]], requires_grad=True)
目标网络权重: Parameter containing:
tensor([[-0.0103,  0.4268,  0.2549,  0.1492,  0.2748],
        [ 0.0375, -0.0403,  0.0326,  0.0213,  0.1052],
        [-0.1674, -0.3298, -0.0271, -0.1609,  0.3070]], requires_grad=True)
-------------------------------------------------------------------
更新后:
原网络权重: Parameter containing:
tensor([[0.9897, 1.4268, 1.2549, 1.1492, 1.2748],
        [1.0375, 0.9597, 1.0326, 1.0213, 1.1052],
        [0.8326, 0.6702, 0.9729, 0.8391, 1.3070]], requires_grad=True)
目标网络权重: Parameter containing:
tensor([[-0.0103,  0.4268,  0.2549,  0.1492,  0.2748],
        [ 0.0375, -0.0403,  0.0326,  0.0213,  0.1052],
        [-0.1674, -0.3298, -0.0271, -0.1609,  0.3070]], requires_grad=True)
-------------------------------------------------------------------
同步后:
原网络权重: Parameter containing:
tensor([[0.9897, 1.4268, 1.2549, 1.1492, 1.2748],
        [1.0375, 0.9597, 1.0326, 1.0213, 1.1052],
        [0.8326, 0.6702, 0.9729, 0.8391, 1.3070]], requires_grad=True)
目标网络权重: Parameter containing:
tensor([[0.9897, 1.4268, 1.2549, 1.1492, 1.2748],
        [1.0375, 0.9597, 1.0326, 1.0213, 1.1052],
        [0.8326, 0.6702, 0.9729, 0.8391, 1.3070]], requires_grad=True)

6. 06_cartpole.py

6.1 程序

#!/usr/bin/env python3
# -*- coding=utf-8 -*-
# The PTAN library——The PTAN CartPole solver
# 前述5个程序全部是为了CartPole实战做准备
# https://www.cnblogs.com/kailugaji/
import gym
import ptan
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import matplotlib.pylab as plt
from matplotlib import rcParams
config = {
    "font.family":'Times New Roman',
    "font.size": 12,
    "mathtext.fontset": 'stix',
    "font.serif": ['SimSun']
}
rcParams.update(config)


HIDDEN_SIZE = 128 # 隐层神经元个数
BATCH_SIZE = 16 # 一批16个样本
TGT_NET_SYNC = 10 #每隔10轮将参数从原网络同步到目标网络
GAMMA = 0.9 # 折扣率
REPLAY_SIZE = 1000 # 经验回放池容量
LR = 5e-3 # 学习率
EPS_DECAY=0.995 # epsilon因子线性衰减率

# 构建网络
class Net(nn.Module):
    def __init__(self, obs_size, hidden_size, n_actions):
        # obs_size:输入状态维度,hidden_size:隐层维度,n_actions:输出动作维度
        super(Net, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(obs_size, hidden_size), # 全连接层
            nn.ReLU(),
            nn.Linear(hidden_size, n_actions) # 全连接层
        )

    def forward(self, x):
    # CartPole is stupid -- they return double observations, rather than standard floats, so, the cast here
        return self.net(x.float())


@torch.no_grad() # 下面数据不需要计算梯度,也不会进行反向传播
def unpack_batch(batch, net, gamma):
# batch: 一批次的样本,16个,(state, action, reward, last_state)
    states = []
    actions = []
    rewards = []
    done_masks = []
    last_states = []
    for exp in batch:
        states.append(exp.state)
        actions.append(exp.action)
        rewards.append(exp.reward)
        done_masks.append(exp.last_state is None)
        if exp.last_state is None:
            last_states.append(exp.state)
        else:
            last_states.append(exp.last_state)

    states_v = torch.tensor(states)
    actions_v = torch.tensor(actions)
    rewards_v = torch.tensor(rewards)
    last_states_v = torch.tensor(last_states)
    last_state_q_v = net(last_states_v) # 将最后的状态输入网络,得到Q(s, a)
    best_last_q_v = torch.max(last_state_q_v, dim=1)[0] # 找最大的Q
    best_last_q_v[done_masks] = 0.0
    return states_v, actions_v, best_last_q_v * gamma + rewards_v
    # r + gamma * max Q(s, a)


if __name__ == "__main__":
    env = gym.make("CartPole-v0")
    obs_size = env.observation_space.shape[0]
    # observation大小(4个状态变量):小车在轨道上的位置,杆子与竖直方向的夹角,小车速度,角度变化率
    n_actions = env.action_space.n # action大小(2个动作,左或者右)

    net = Net(obs_size, HIDDEN_SIZE, n_actions) # 4->128->2
    tgt_net = ptan.agent.TargetNet(net) # 目标网络(与原网络架构一致)
    selector = ptan.actions.ArgmaxActionSelector() # 选Q值最大的动作索引
    selector = ptan.actions.EpsilonGreedyActionSelector(epsilon=1, selector=selector)
    # epsilon-greedy action selector,初始epsilon=1
    agent = ptan.agent.DQNAgent(net, selector) # 离散:输出具有最大Q值的动作与状态索引
    exp_source = ptan.experience.ExperienceSourceFirstLast(env, agent, gamma=GAMMA)
    # 返回运行记录以用于训练模型,输出格式为:(state, action, reward, last_state)
    buffer = ptan.experience.ExperienceReplayBuffer(exp_source, buffer_size=REPLAY_SIZE)
    # 经验回放池,构建buffer,容量为1000,当前没东西,len(buffer) = 0
    optimizer = optim.Adam(net.parameters(), LR) # Adam优化

    step = 0 # 迭代次数/轮数
    episode = 0 # 局数,几局游戏
    solved = False
    losses = []
    rewards = []

    while True:
        step += 1
        buffer.populate(1) # 从环境中获取一个新样本

        for reward, steps in exp_source.pop_rewards_steps():
        # pop_rewards_steps(): 返回一局游戏过后的(total_reword,total_steps)
            episode += 1
            print("第%d次: 第%d局游戏结束, 奖励为%.2f, 本局步数为%d, epsilon为%.2f" %(step, episode, reward, steps, selector.epsilon))
            # 杆子能越长时间保持平衡,得分越高。steps与reward一致
            rewards.append(reward)
            solved = reward > 100 # 最大奖励阈值,只有当reward>100时才结束游戏
        if solved:
            print("Victory!")
            break

        # print("第%d次buffer大小:" % step, len(buffer))
        if len(buffer) < 2*BATCH_SIZE: # # buffer里面还没超过2倍的批大小(32)个样本
            continue

        batch = buffer.sample(BATCH_SIZE)
        # buffer等于或超过2*BATCH_SIZE后,从buffer里面均匀抽样一个批次的样本,一批BATCH_SIZE个样本
        # batch:state, action, reward, last_state
        states_v, actions_v, tgt_q_v = unpack_batch(batch, tgt_net.target_model, GAMMA)
        # 输入目标网络
        # 得到tgt_q_v = r + gamma * max Q(s, a)
        optimizer.zero_grad()
        q_v = net(states_v) # 输入状态,得到Q(s, a)
        q_v = q_v.gather(1, actions_v.unsqueeze(-1)).squeeze(-1)
        '''
            torch.gather 作用:收集输入的特定维度指定位置的数值
            参数:input(tensor):   待操作数。不妨设其维度为(x1, x2, …, xn)
                  dim(int):   待操作的维度。
                  index(LongTensor):   如何对input进行操作。
                  其维度有限定,例如当dim=i时,index的维度为(x1, x2, …y, …,xn),既是将input的第i维的大小更改为y,且要满足y>=1(除了第i维之外的其他维度,大小要和input保持一致)。
                  out:   注意输出和index的维度是一致的
            squeeze(-1): 将输入张量形状中的1去除并返回。
            如果输入是形如(A×1×B×1×C×1×D),那么输出形状就为: (A×B×C×D)
        '''
        loss_v = F.mse_loss(q_v, tgt_q_v)
        # MSE Loss, min L = (r + gamma * max Q(s', a') - Q(s, a))^2
        loss_v.backward()
        optimizer.step()
        losses.append(loss_v.item())
        selector.epsilon *= EPS_DECAY # 贪心因子线性衰减

        if step % TGT_NET_SYNC == 0: # 每TGT_NET_SYNC(10)轮同步一次目标网络参数
            tgt_net.sync() # weights from the source network are copied into the target network

    # 画图
    # Loss曲线图
    plt.plot(losses)
    plt.xlabel('Iteration', fontsize=13) # 迭代次数
    plt.ylabel('Loss', fontsize=13)
    plt.title('CartPole-v0', fontsize=14)
    plt.savefig('损失函数曲线图.png', dpi=1000)
    plt.show()
    # reward曲线图
    plt.plot(rewards)
    plt.xlabel('Episode', fontsize=13) # 几局游戏
    plt.ylabel('Reward', fontsize=13)
    plt.title('CartPole-v0', fontsize=14)
    plt.savefig('奖励曲线图.png', dpi=1000)
    plt.show()

6.2 结果

第23次: 第1局游戏结束, 奖励为22.00, 本局步数为22, epsilon为1.00
第47次: 第2局游戏结束, 奖励为24.00, 本局步数为24, epsilon为0.93
第68次: 第3局游戏结束, 奖励为21.00, 本局步数为21, epsilon为0.83
第81次: 第4局游戏结束, 奖励为13.00, 本局步数为13, epsilon为0.78
第109次: 第5局游戏结束, 奖励为28.00, 本局步数为28, epsilon为0.68
第122次: 第6局游戏结束, 奖励为13.00, 本局步数为13, epsilon为0.64
第134次: 第7局游戏结束, 奖励为12.00, 本局步数为12, epsilon为0.60
第165次: 第8局游戏结束, 奖励为31.00, 本局步数为31, epsilon为0.51
第180次: 第9局游戏结束, 奖励为15.00, 本局步数为15, epsilon为0.48
第193次: 第10局游戏结束, 奖励为13.00, 本局步数为13, epsilon为0.45
第204次: 第11局游戏结束, 奖励为11.00, 本局步数为11, epsilon为0.42
第234次: 第12局游戏结束, 奖励为30.00, 本局步数为30, epsilon为0.36
第260次: 第13局游戏结束, 奖励为26.00, 本局步数为26, epsilon为0.32
第302次: 第14局游戏结束, 奖励为42.00, 本局步数为42, epsilon为0.26
第355次: 第15局游戏结束, 奖励为53.00, 本局步数为53, epsilon为0.20
第391次: 第16局游戏结束, 奖励为36.00, 本局步数为36, epsilon为0.17
第402次: 第17局游戏结束, 奖励为11.00, 本局步数为11, epsilon为0.16
第491次: 第18局游戏结束, 奖励为89.00, 本局步数为89, epsilon为0.10
第547次: 第19局游戏结束, 奖励为56.00, 本局步数为56, epsilon为0.08
第681次: 第20局游戏结束, 奖励为134.00, 本局步数为134, epsilon为0.04
Victory!

损失函数曲线图

奖励曲线图

7. 参考文献

[1] https://github.com/PacktPublishing/Deep-Reinforcement-Learning-Hands-On-Second-Edition

[2] https://github.com/Shmuma/ptan

posted on 2022-02-23 12:01  凯鲁嘎吉  阅读(376)  评论(0编辑  收藏  举报