(7-3-02)TRPO算法核心知识与应用实践:综合实战:基于矩阵低秩分解的TRPO优化(2)

7.3.6  创建Agent

在机器学习中,"agents" 通常指代代理(agent)。代理是指一种具有感知和决策能力的实体,它可以与环境互动,采取行动以实现某些目标或最大化某些奖励信号。代理通常是强化学习问题的核心,其中代理的任务是学习一个策略,以在给定环境中最大化累积奖励。

编写文件src/agents.py,功能是创建Agent代理,实现与代理(Agent)和强化学习相关的功能。此文件涵盖了代理的策略和值函数更新,以及用于 TRPO 算法的一些重要计算步骤。这些功能用于训练强化学习代理,以在与环境互动时学习优化策略和值函数。代理可以基于高斯策略进行动作选择,同时还支持在离散状态空间中工作。文件src/agents.py的具体实现流程。

(1)创建代理类GaussianAgent,用于构建具有高斯策略的强化学习代理。这个代理具有一个策略网络(actor)和一个值函数网络(critic),可以通过策略网络生成动作并通过值函数评估状态值。可以选择性地提供离散化器(discretizer_actor 和 discretizer_critic),用于在离散状态空间中工作。代理可以生成动作、评估策略的对数概率、评估状态值以及执行策略。具体实现代码如下所示。

from typing import Tuple, List, Union, Optional
from copy import deepcopy

import numpy as np
import torch
import torch.nn as nn
from torch.nn.utils.convert_parameters import parameters_to_vector
from torch.nn.utils.convert_parameters import vector_to_parameters

from src.models import PolicyNetwork, PolicyLR, ValueNetwork, ValueLR
from src.utils import Buffer, Discretizer


class GaussianAgent(nn.Module):
    def __init__(
            self,
            actor: Union[PolicyNetwork, PolicyLR],
            critic: Union[ValueNetwork, ValueLR],
            discretizer_actor: Optional[Discretizer]=None,
            discretizer_critic: Optional[Discretizer]=None
        ) -> None:
        super(GaussianAgent, self).__init__()

        self.actor = actor
        self.critic = critic

        self.discretizer_actor = discretizer_actor
        self.discretizer_critic = discretizer_critic

    def pi(self, state: torch.Tensor) -> torch.distributions.Normal:
        # Parameters
        if self.discretizer_actor:
            state = state.numpy().reshape(-1, len(self.discretizer_actor.buckets))
            indices = self.discretizer_actor.get_index(state)
            mu, log_sigma = self.actor(indices)
        else:
            mu, log_sigma = self.actor(state)
        sigma = log_sigma.exp()

        # Distribution
        pi = torch.distributions.Normal(mu.squeeze(), sigma.squeeze())
        return pi

    def evaluate_logprob(self, state: torch.Tensor, action: torch.Tensor) -> torch.Tensor:
        # Actor
        dist = self.pi(state)
        action_logprob = dist.log_prob(action)
        return action_logprob.squeeze()

    def evaluate_value(self, state: torch.Tensor) -> torch.Tensor:
        # Critic
        if self.discretizer_critic:
            state = state.numpy().reshape(-1, len(self.discretizer_actor.buckets))
            indices = self.discretizer_critic.get_index(state)
            value = self.critic(indices)
            return value.squeeze()
        value = self.critic(state)
        return value.squeeze()

    def act(self, state: np.ndarray) -> Tuple[torch.Tensor, torch.Tensor]:
        dist = self.pi(state)
        action = dist.sample()
        action_logprob = dist.log_prob(action)
        return action.detach().flatten(), action_logprob.detach().flatten()

在上述代码中,类GaussianAgent中各个函数的具体说明如下所示:

  1. 构造函数__init__ 函数:用于初始化代理对象,接受以下参数:
  • actor:策略网络(可以是 PolicyNetwork 或 PolicyLR)。
  • critic:值函数网络(可以是 ValueNetwork 或 ValueLR)。
  • discretizer_actor:可选参数,用于在离散状态空间中工作的状态离散器。
  • discretizer_critic:可选参数,用于在离散状态空间中工作的值函数离散器。
  1. pi 函数:用于生成一个给定状态下的高斯分布策略。输入参数是当前状态 state,返回一个 torch.distributions.Normal 对象,表示在该状态下的策略。
  2. evaluate_logprob 函数:用于评估给定状态和动作下的对数概率。输入参数包括当前状态 state 和代理执行的动作 action。返回对数概率的张量。
  3. evaluate_value 函数:用于评估给定状态的值函数估计。输入参数是当前状态 state,返回状态值的张量估计。
  4. act 函数:用于执行代理的动作选择,输入参数是当前状态 state。返回一个包含两个张量的元组,第一个张量表示代理选择的动作,第二个张量表示选择该动作的对数概率。

上述函数共同构成了代理(agent)的主要功能,包括策略生成、对数概率估计、值函数估计和动作选择。这些功能用于代理与环境互动、学习和执行策略。根据输入的策略和值函数网络类型以及离散器,代理可以在不同问题和状态空间中工作。

(2)创建类TRPOGaussianNN,这是一个基于 Trust Region Policy Optimization(TRPO)算法的代理(agent)类,使用高斯策略进行探索。类TRPOGaussianNN包括了策略更新和值函数更新的功能,以及一些用于 TRPO 算法的计算方法,如共轭梯度方法。另外,还包括了用于执行线搜索和计算策略梯度的方法。具体实现代码如下所示。

class TRPOGaussianNN:
    def __init__(
        self,
        actor: Union[PolicyNetwork, PolicyLR],
        critic: Union[ValueNetwork, ValueLR],
        discretizer_actor: Optional[Discretizer]=None,
        discretizer_critic: Optional[Discretizer]=None,
        gamma: float=0.99,
        tau: float=0.97,
        delta: float=.01,
        cg_dampening: float=0.001,
        cg_tolerance: float=1e-10,
        cg_iteration: float=10,
    ) -> None:

        self.gamma = gamma
        self.delta = delta
        self.cg_dampening = cg_dampening
        self.cg_tolerance = cg_tolerance
        self.cg_iteration = cg_iteration
        self.discretizer_actor = discretizer_actor
        self.tau = tau

        self.buffer = Buffer()

        actor_old = deepcopy(actor)
        critic_old = deepcopy(critic)

        self.policy = GaussianAgent(actor, critic, discretizer_actor, discretizer_critic)
        self.policy_old = GaussianAgent(actor_old, critic_old, discretizer_actor, discretizer_critic)

        self.opt_critic = torch.optim.LBFGS(
            self.policy.critic.parameters(),
            history_size=100,
            max_iter=25,
            line_search_fn='strong_wolfe',
        )

    def select_action(self, state: np.ndarray) -> np.ndarray:
        with torch.no_grad():
            state = torch.as_tensor(state).double()
            action, action_logprob = self.policy_old.act(state)

        self.buffer.states.append(state)
        self.buffer.actions.append(action)
        self.buffer.logprobs.append(action_logprob)

        return action.numpy()

    def calculate_returns(
            self,
            values: np.ndarray
        ) -> Tuple[torch.Tensor, torch.Tensor]:
        returns = []
        advantages=[]

        prev_return = 0
        prev_value = 0
        prev_advantage = 0
        for i in reversed(range(len(self.buffer.rewards))):
            reward = self.buffer.rewards[i]
            mask = 1 - self.buffer.terminals[i]

            actual_return = reward + self.gamma*prev_return*mask
            actual_delta = reward + self.gamma*prev_value*mask - values[i]
            actual_advantage = actual_delta + self.gamma*self.tau*prev_advantage*mask        

            returns.insert(0, actual_return)
            advantages.insert(0, actual_advantage)

            prev_return = actual_return
            prev_value = values[i]
            prev_advantage = actual_advantage

        returns = torch.as_tensor(returns).double().detach().squeeze()
        advantages = torch.as_tensor(advantages).double().detach().squeeze()
        advantages = (advantages - advantages.mean())/advantages.std()

        return returns, advantages

    def kl_penalty(self, states: torch.Tensor) -> torch.Tensor:
        if self.discretizer_actor:
            states = states.numpy().reshape(-1, len(self.discretizer_actor.buckets))
            indices = self.discretizer_actor.get_index(states)
            mu1, log_sigma1 = self.policy_old.actor(indices)
            mu2, log_sigma2 = self.policy.actor(indices)

            mu1 = mu1.detach().unsqueeze(1)
            mu2 = mu2.unsqueeze(1)
            log_sigma1 = log_sigma1.detach()
        else:
            mu1, log_sigma1 = self.policy_old.actor(states)
            mu2, log_sigma2 = self.policy.actor(states)

            mu1 = mu1.detach()
            log_sigma1 = log_sigma1.detach()

        kl = ((log_sigma2 - log_sigma1) + 0.5 * (log_sigma1.exp().pow(2)
            + (mu1 - mu2).pow(2)) / log_sigma2.exp().pow(2) - 0.5)

        return kl.sum(1).mean()

    def loss_actor(
            self,
            states: torch.Tensor,
            actions: torch.Tensor,
            old_logprobs: torch.Tensor,
            advantages: torch.Tensor
    ):
        logprobs = self.policy.evaluate_logprob(states, actions)
        ratio = torch.exp(logprobs - old_logprobs)
        return torch.mean(ratio * advantages)

    def line_search(
        self,
        states: torch.Tensor,
        actions: torch.Tensor,
        old_logprobs: torch.Tensor,
        advantages: torch.Tensor,
        params: List[torch.nn.parameter.Parameter],
        params_flat: torch.Tensor,
        gradients: torch.Tensor,
        expected_improve_rate: float,
        max_backtracks: int=10,
        accept_ratio: float=.1
    ) -> torch.Tensor:
        with torch.no_grad():
            loss = self.loss_actor(states, actions, old_logprobs, advantages)

        weights = 0.5**np.arange(max_backtracks)
        for weight in weights:
            params_new = params_flat + weight*gradients
            vector_to_parameters(params_new, params)

            with torch.no_grad():
                loss_new = self.loss_actor(states, actions, old_logprobs, advantages)

            actual_improve = loss_new - loss
            expected_improve = expected_improve_rate*weight
            ratio = actual_improve/expected_improve

            if ratio.item() > accept_ratio and actual_improve.item() > 0:
                return params_new
        return params_flat

    def fvp(
            self,
            vector: torch.Tensor,
            states: torch.Tensor,
            params: List[torch.nn.parameter.Parameter]
        ) -> torch.Tensor:
        vector = vector.clone().requires_grad_()

        self.policy.actor.zero_grad()
        kl_penalty = self.kl_penalty(states)
        grad_kl = torch.autograd.grad(kl_penalty, params, create_graph=True)
        
        grad_kl = torch.cat([grad.view(-1) for grad in grad_kl])

        grad_vector_dot = grad_kl.dot(vector)
        fisher_vector_product = torch.autograd.grad(grad_vector_dot, params)
        fisher_vector_product = torch.cat([out.view(-1) for out in fisher_vector_product]).detach()

        return fisher_vector_product + self.cg_dampening*vector.detach()

    def conjugate_gradient(
            self,
            b: torch.Tensor,
            states: torch.Tensor,
            params: List[torch.nn.parameter.Parameter]
        ) -> torch.Tensor:    
        x = torch.zeros(*b.shape)
        d = b.clone()
        r = b.clone()
        rr = r.dot(r)
        for _ in range(self.cg_iteration):
            Hd = self.fvp(d, states, params)
            alpha = rr / (d.dot(Hd) + 1e-10)
            x = x + alpha * d
            r = r - alpha * Hd
            rr_new = r.dot(r)
            beta = rr_new / (rr + 1e-10)
            d = r + beta * d
            rr = rr_new
            if rr < self.cg_tolerance:
                break
        return x

    def zero_grad(self, model, idx: Optional[int]=None) -> None:
        if idx is None:
            return

        for i, param in enumerate(model.parameters()):
            if i != idx:
                param.grad.zero_()

    def update_critic(self, idx: Optional[int]=None) -> torch.Tensor:
        states = torch.stack(self.buffer.states, dim=0).detach()

        # GAE estimation
        values = self.policy.evaluate_value(states)
        rewards, advantages = self.calculate_returns(values.data.numpy())

        # LBFGS training
        def closure():
            self.opt_critic.zero_grad()
            values = self.policy.evaluate_value(states)
            loss = (values - rewards).pow(2).mean()
            loss.backward()
            self.zero_grad(self.policy.critic, idx)
            return loss
        self.opt_critic.step(closure)

        return advantages

    def update_actor(self, advantages: torch.Tensor, idx: Optional[int]=None) -> None:
        params = list(self.policy.actor.parameters())
        params_old = list(self.policy_old.actor.parameters())
        if idx is not None:
            params = [params[idx]]
            params_old = [params_old[idx]]

        states = torch.stack(self.buffer.states, dim=0).detach()
        actions = torch.stack(self.buffer.actions, dim=0).detach().squeeze()
        old_logprobs = torch.stack(self.buffer.logprobs, dim=0).detach().squeeze()

        # Actor - Gradient estimation
        self.loss_actor(states, actions, old_logprobs, advantages).backward()
        self.zero_grad(self.policy.actor, idx)

        grads = parameters_to_vector([param.grad for param in params])
        params_flat = parameters_to_vector([param for param in params])

        # Actor - Conjugate Gradient Ascent
        direction = self.conjugate_gradient(grads, states, params)
        direction_hessian_norm = direction.dot(self.fvp(direction, states, params))
        lagrange_multiplier = torch.sqrt(2*self.delta/(direction_hessian_norm + 1e-10))

        grads_opt = lagrange_multiplier*direction

        # Actor - Line search backtracking
        expected_improvement = grads.dot(grads_opt)
        params_flat = self.line_search(
            states,
            actions,
            old_logprobs,
            advantages,
            params,
            params_flat,
            grads_opt,
            expected_improvement
        )
        vector_to_parameters(params_flat, params)
        vector_to_parameters(params_flat, params_old)

在上述代码中,类TRPOGaussianNN中各个函数的具体说明如下所示:

  1. 构造函数__init__ :用于初始化 TRPO 代理对象,接受以下参数:
  • actor:策略网络(可以是 PolicyNetwork 或 PolicyLR)。
  • critic:值函数网络(可以是 ValueNetwork 或 ValueLR)。
  • discretizer_actor:可选参数,用于在离散状态空间中工作的状态离散器。
  • discretizer_critic:可选参数,用于在离散状态空间中工作的值函数离散器。
  • gamma:折扣因子,用于计算未来奖励的折现值。
  • tau:TRPO 算法中的控制参数,用于计算策略更新的步长。
  • delta:TRPO 算法中的控制参数,用于计算策略更新的步长。
  • cg_dampening:共轭梯度方法的阻尼参数。
  • cg_tolerance:共轭梯度方法的收敛容忍度。
  • cg_iteration:共轭梯度方法的迭代次数。
  1. select_action 函数:用于根据当前状态选择动作,并将状态、动作和对数概率添加到缓冲区中。

输入参数是当前状态 state,返回选择的动作。

  1. calculate_returns 函数:用于计算 GAE(Generalized Advantage Estimation)估计的奖励和优势。输入参数是值函数估计 values,返回一个包含奖励和优势的元组。
  2. kl_penalty 函数:用于计算策略分布的 KL 散度(Kullback-Leibler divergence)惩罚。输入参数是状态 states,返回 KL 散度的张量。
  3. loss_actor 函数:用于计算策略的损失,包括策略梯度损失。输入参数包括状态 states、动作 actions、旧的对数概率 old_logprobs 和优势 advantages。返回策略损失的张量。
  4. line_search 函数:用于执行线搜索来更新策略参数。输入参数包括状态 states、动作 actions、旧的对数概率 old_logprobs、优势 advantages、策略参数列表 params、参数张量 params_flat、梯度张量 gradients 以及期望的改进率 expected_improve_rate。返回更新后的参数张量。
  5. fvp 函数:用于计算 Fisher 向量乘积(Fisher Vector Product)。输入参数包括输入向量 vector、状态 states 以及参数列表 params。返回 Fisher 向量乘积的张量。
  6. conjugate_gradient 函数:用于执行共轭梯度方法来解决线性系统。输入参数包括右侧向量 b、状态 states 以及参数列表 params。返回解的张量。
  7. zero_grad 函数:用于将模型的梯度清零。输入参数是模型 model 和参数索引 idx。
  8. update_critic 函数:用于更新值函数网络(critic),返回优势估计的张量。
  9. update_actor 函数:用于更新策略网络(actor),输入参数包括优势 advantages 和参数索引 idx。

7.3.7  评估TRPO在Acrobot 环境中的性能

编写文件main_acrobot.py,用于训练和评估基于 TRPO 算法的强化学习代理(agent)在 Acrobot 环境中的性能,并保存训练结果以备将来使用。具体实现代码如下所示。

import pickle

from src.environments import CustomAcrobotEnv
from src.models import PolicyNetwork, PolicyLR, ValueNetwork, ValueLR
from src.agents import TRPOGaussianNN
from src.algorithms import Trainer
from src.utils import Discretizer


if __name__ == "__main__":
    res_nn, res_lr = [], []
    env = CustomAcrobotEnv()
    for _ in range(100):
        # NN
        actor = PolicyNetwork(4, [16], 1).double()
        critic = ValueNetwork(4, [16], 1).double()

        agent = TRPOGaussianNN(
            actor,
            critic,
            gamma=0.9,
            delta=0.01,
            tau=0.9,
            cg_dampening=0.1,
            cg_tolerance=1e-10,
            cg_iteration=10,
        )

        trainer = Trainer(actor_opt='sgd', critic_opt='sgd')
        _, totals, _ = trainer.train(
            env,
            agent,
            epochs=20000,
            max_steps=1000,
            update_freq=10000,
            initial_offset=-1,
        )
        res_nn.append(totals)

        # LR
        discretizer = Discretizer(
            min_points=[-1, -1, -1, -1],
            max_points=[1, 1, 1, 1],
            buckets=[2, 2, 2, 2],
            dimensions=[[0, 1], [2, 3]]
        )

        actor = PolicyLR(4, 4, 2, 0.1).double()
        critic = ValueLR(4, 4, 2, 0.1).double()

        agent = TRPOGaussianNN(
            actor,
            critic,
            discretizer,
            discretizer,
            gamma=0.9,
            delta=0.01,
            tau=0.9,
            cg_dampening=0.1,
            cg_tolerance=1e-10,
            cg_iteration=10,
        )

        trainer = Trainer(actor_opt='sgd', critic_opt='sgd')
        _, totals, _ = trainer.train(
            env,
            agent,
            epochs=20000,
            max_steps=1000,
            update_freq=10000,
            initial_offset=-1,
        )
        res_lr.append(totals)

    with open('results/acro_nn.pkl','wb') as f:
        pickle.dump(res_nn, f)

    with open('results/acro_lr.pkl','wb') as f:
        pickle.dump(res_lr, f)

上述代码的实现流程如下所示:

(1)导入必要的模块和类,具体说明如下:

  1. pickle:用于序列化和反序列化 Python 对象。
  2. 自定义的环境类 CustomAcrobotEnv,在 Acrobot 环境上的自定义变体。
  3. 自定义的神经网络模型类 PolicyNetwork、PolicyLR、ValueNetwork 和 ValueLR,用于构建代理的策略和值函数。
  4. 自定义的 TRPO 强化学习代理类 TRPOGaussianNN,包括策略更新和值函数更新的逻辑。
  5. 自定义的训练器类 Trainer,用于执行代理的训练和评估。

(2)创建两个空列表 res_nn 和 res_lr,用于存储训练结果。

(3)创建 Acrobot 环境:使用自定义的 CustomAcrobotEnv 创建 Acrobot 环境。

(4)循环训练代理:在循环中,首先创建一个使用神经网络的代理(agent),具体说明如下:

  1. 创建 PolicyNetwork 和 ValueNetwork 实例,并设置网络结构。
  2. 创建 TRPOGaussianNN 代理对象,指定网络、算法参数等。
  3. 创建 Trainer 训练器对象。

然后调用 trainer.train 函数,对代理在环境中进行训练,具体说明如下:

  1. epochs=20000:执行 20000 个训练周期。
  2. max_steps=1000:每个周期最多执行 1000 步。
  3. update_freq=10000:每 10000 步执行一次策略更新。
  4. initial_offset=-1:初始偏移值。

最后将每个周期的总奖励添加到 res_nn 或 res_lr 中,具体取决于代理类型。

(5)使用 pickle 模块将训练结果保存到文件中:分别将 res_nn 和 res_lr 保存到两个不同的 pickle 文件中,以供后续分析和可视化使用。

执行文件main_acrobot.py后会创建并训练两种不同类型的 TRPO 强化学习代理(res_nn 和 res_lr)在 Acrobot 环境中。然后将训练结果保存为两个不同的 pickle 文件('results/acro_nn.pkl' 和 'results/acro_lr.pkl'),这些文件将包含训练期间每个周期的总奖励等信息。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

感谢鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值