122 lines
4.4 KiB
Python
Raw Normal View History

2020-03-17 11:37:31 +08:00
import torch
import numpy as np
2020-05-12 11:31:47 +08:00
from typing import Dict, List, Union, Optional
2020-03-17 11:37:31 +08:00
from tianshou.policy import BasePolicy
2020-06-10 12:06:56 +08:00
from tianshou.data import Batch, ReplayBuffer, to_torch_as
2020-03-17 11:37:31 +08:00
2020-03-18 21:45:41 +08:00
class PGPolicy(BasePolicy):
2020-04-06 19:36:59 +08:00
"""Implementation of Vanilla Policy Gradient.
:param torch.nn.Module model: a model following the rules in
:class:`~tianshou.policy.BasePolicy`. (s -> logits)
:param torch.optim.Optimizer optim: a torch.optim for optimizing the model.
:param torch.distributions.Distribution dist_fn: for computing the action.
:param float discount_factor: in [0, 1].
.. seealso::
Please refer to :class:`~tianshou.policy.BasePolicy` for more detailed
explanation.
2020-04-06 19:36:59 +08:00
"""
2020-03-17 11:37:31 +08:00
2020-05-12 11:31:47 +08:00
def __init__(self,
model: torch.nn.Module,
optim: torch.optim.Optimizer,
2020-05-16 20:08:32 +08:00
dist_fn: torch.distributions.Distribution
2020-05-12 11:31:47 +08:00
= torch.distributions.Categorical,
2020-05-16 20:08:32 +08:00
discount_factor: float = 0.99,
reward_normalization: bool = False,
2020-05-12 11:31:47 +08:00
**kwargs) -> None:
2020-04-08 21:13:15 +08:00
super().__init__(**kwargs)
2020-03-17 11:37:31 +08:00
self.model = model
self.optim = optim
2020-03-17 15:16:30 +08:00
self.dist_fn = dist_fn
2020-04-06 19:36:59 +08:00
assert 0 <= discount_factor <= 1, 'discount factor should in [0, 1]'
2020-03-17 11:37:31 +08:00
self._gamma = discount_factor
2020-04-26 16:13:51 +08:00
self._rew_norm = reward_normalization
2020-03-17 11:37:31 +08:00
2020-05-12 11:31:47 +08:00
def process_fn(self, batch: Batch, buffer: ReplayBuffer,
indice: np.ndarray) -> Batch:
2020-04-06 19:36:59 +08:00
r"""Compute the discounted returns for each frame:
.. math::
G_t = \sum_{i=t}^T \gamma^{i-t}r_i
, where :math:`T` is the terminal time step, :math:`\gamma` is the
discount factor, :math:`\gamma \in [0, 1]`.
"""
2020-04-14 21:11:06 +08:00
# batch.returns = self._vanilla_returns(batch)
2020-04-03 21:28:12 +08:00
# batch.returns = self._vectorized_returns(batch)
2020-04-14 21:11:06 +08:00
# return batch
return self.compute_episodic_return(
batch, gamma=self._gamma, gae_lambda=1.)
2020-03-17 11:37:31 +08:00
2020-05-12 11:31:47 +08:00
def forward(self, batch: Batch,
state: Optional[Union[dict, Batch, np.ndarray]] = None,
**kwargs) -> Batch:
2020-04-06 19:36:59 +08:00
"""Compute action over the given batch data.
:return: A :class:`~tianshou.data.Batch` which has 4 keys:
* ``act`` the action.
* ``logits`` the network's raw output.
* ``dist`` the action distribution.
* ``state`` the hidden state.
.. seealso::
2020-04-10 10:47:16 +08:00
Please refer to :meth:`~tianshou.policy.BasePolicy.forward` for
more detailed explanation.
2020-04-06 19:36:59 +08:00
"""
2020-03-17 11:37:31 +08:00
logits, h = self.model(batch.obs, state=state, info=batch.info)
2020-04-06 19:36:59 +08:00
if isinstance(logits, tuple):
dist = self.dist_fn(*logits)
else:
dist = self.dist_fn(logits)
2020-03-18 21:45:41 +08:00
act = dist.sample()
2020-03-17 11:37:31 +08:00
return Batch(logits=logits, act=act, state=h, dist=dist)
2020-05-12 11:31:47 +08:00
def learn(self, batch: Batch, batch_size: int, repeat: int,
**kwargs) -> Dict[str, List[float]]:
2020-03-17 11:37:31 +08:00
losses = []
2020-03-26 11:42:34 +08:00
r = batch.returns
2020-05-27 11:02:23 +08:00
if self._rew_norm and not np.isclose(r.std(), 0):
2020-04-26 16:13:51 +08:00
batch.returns = (r - r.mean()) / r.std()
2020-03-20 19:52:29 +08:00
for _ in range(repeat):
for b in batch.split(batch_size):
self.optim.zero_grad()
dist = self(b).dist
2020-06-10 12:06:56 +08:00
a = to_torch_as(b.act, dist.logits)
r = to_torch_as(b.returns, dist.logits)
loss = -(dist.log_prob(a).flatten() * r).sum()
2020-03-20 19:52:29 +08:00
loss.backward()
self.optim.step()
2020-04-03 21:28:12 +08:00
losses.append(loss.item())
2020-03-19 17:23:46 +08:00
return {'loss': losses}
2020-03-17 11:37:31 +08:00
2020-04-14 21:11:06 +08:00
# def _vanilla_returns(self, batch):
# returns = batch.rew[:]
# last = 0
# for i in range(len(returns) - 1, -1, -1):
# if not batch.done[i]:
# returns[i] += self._gamma * last
# last = returns[i]
# return returns
# def _vectorized_returns(self, batch):
# # according to my tests, it is slower than _vanilla_returns
# # import scipy.signal
# convolve = np.convolve
# # convolve = scipy.signal.convolve
# rew = batch.rew[::-1]
# batch_size = len(rew)
# gammas = self._gamma ** np.arange(batch_size)
# c = convolve(rew, gammas)[:batch_size]
# T = np.where(batch.done[::-1])[0]
# d = np.zeros_like(rew)
# d[T] += c[T] - rew[T]
# d[T[1:]] -= d[T[:-1]] * self._gamma ** np.diff(T)
# return (c - convolve(d, gammas)[:batch_size])[::-1]