simonbb 0.0.3__tar.gz → 0.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simonbb-0.0.3 → simonbb-0.0.4}/PKG-INFO +5 -2
- {simonbb-0.0.3 → simonbb-0.0.4}/pyproject.toml +5 -2
- simonbb-0.0.4/src/simonbb/cis433/actions.py +76 -0
- simonbb-0.0.4/src/simonbb/cis433/agent.py +193 -0
- simonbb-0.0.4/src/simonbb/cis433/experience.py +604 -0
- simonbb-0.0.4/src/simonbb/cis433/ignite.py +137 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/LICENSE +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/README.md +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/__init__.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis433/__init__.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis433/bert.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis433/gpt.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis433/transformer.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis433/utils.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis438/__init__.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/cis438/agentic.py +0 -0
- {simonbb-0.0.3 → simonbb-0.0.4}/src/simonbb/utils.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simonbb
|
|
3
|
-
Version: 0.0.
|
|
4
|
-
Summary: A small package to facilitate teaching AI
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: A small package to facilitate teaching AI courses
|
|
5
5
|
Project-URL: Homepage, https://simonbb.com
|
|
6
6
|
Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
|
|
7
7
|
License-Expression: MIT
|
|
@@ -9,6 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: gymnasium~=1.3.0
|
|
12
13
|
Requires-Dist: langchain-chroma~=1.1.0
|
|
13
14
|
Requires-Dist: langchain-community~=0.4.1
|
|
14
15
|
Requires-Dist: langchain-core~=1.2.23
|
|
@@ -17,6 +18,8 @@ Requires-Dist: langchain~=1.2.13
|
|
|
17
18
|
Requires-Dist: langgraph~=1.1.3
|
|
18
19
|
Requires-Dist: matplotlib~=3.10.8
|
|
19
20
|
Requires-Dist: numpy~=2.4.0
|
|
21
|
+
Requires-Dist: pytorch-ignite~=0.5.4
|
|
22
|
+
Requires-Dist: pyyaml~=6.0.3
|
|
20
23
|
Requires-Dist: sentence-transformers~=5.3.0
|
|
21
24
|
Requires-Dist: torchvision~=0.24.1
|
|
22
25
|
Requires-Dist: torch~=2.9.1
|
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simonbb"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
|
|
10
10
|
]
|
|
11
|
-
description = "A small package to facilitate teaching AI
|
|
11
|
+
description = "A small package to facilitate teaching AI courses"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.9"
|
|
14
14
|
classifiers = [
|
|
@@ -30,6 +30,9 @@ dependencies = [
|
|
|
30
30
|
"langchain-chroma~=1.1.0",
|
|
31
31
|
"langchain-huggingface~=1.2.1",
|
|
32
32
|
"sentence-transformers~=5.3.0",
|
|
33
|
+
"gymnasium~=1.3.0",
|
|
34
|
+
"pytorch-ignite~=0.5.4",
|
|
35
|
+
"pyyaml~=6.0.3"
|
|
33
36
|
]
|
|
34
37
|
[project.urls]
|
|
35
38
|
Homepage = "https://simonbb.com"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import numpy as np
|
|
3
|
+
import typing as tt
|
|
4
|
+
|
|
5
|
+
class ActionSelector(abc.ABC):
|
|
6
|
+
"""
|
|
7
|
+
Abstract class which converts scores to the actions
|
|
8
|
+
"""
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def __call__(self, scores: np.ndarray) -> np.ndarray:
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ArgmaxActionSelector(ActionSelector):
|
|
15
|
+
"""
|
|
16
|
+
Selects actions using argmax
|
|
17
|
+
"""
|
|
18
|
+
def __call__(self, scores: np.ndarray) -> np.ndarray:
|
|
19
|
+
return np.argmax(scores, axis=1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EpsilonGreedyActionSelector(ActionSelector):
|
|
23
|
+
def __init__(self, epsilon: float = 0.05,
|
|
24
|
+
selector: tt.Optional[ActionSelector] = None):
|
|
25
|
+
self._epsilon = epsilon
|
|
26
|
+
self.selector = selector if selector is not None \
|
|
27
|
+
else ArgmaxActionSelector()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def epsilon(self) -> float:
|
|
31
|
+
return self._epsilon
|
|
32
|
+
|
|
33
|
+
@epsilon.setter
|
|
34
|
+
def epsilon(self, value: float):
|
|
35
|
+
if value < 0.0 or value > 1.0:
|
|
36
|
+
raise ValueError("Epsilon has to be between 0 and 1")
|
|
37
|
+
self._epsilon = value
|
|
38
|
+
|
|
39
|
+
def __call__(self, scores: np.ndarray) -> np.ndarray:
|
|
40
|
+
assert len(scores.shape) == 2
|
|
41
|
+
batch_size, n_actions = scores.shape
|
|
42
|
+
actions = self.selector(scores)
|
|
43
|
+
mask = np.random.random(size=batch_size) < self.epsilon
|
|
44
|
+
rand_actions = np.random.choice(n_actions, sum(mask))
|
|
45
|
+
actions[mask] = rand_actions
|
|
46
|
+
return actions
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ProbabilityActionSelector(ActionSelector):
|
|
50
|
+
"""
|
|
51
|
+
Converts probabilities of actions into action by sampling them
|
|
52
|
+
"""
|
|
53
|
+
def __call__(self, probs: np.ndarray) -> np.ndarray:
|
|
54
|
+
actions = []
|
|
55
|
+
for prob in probs:
|
|
56
|
+
actions.append(np.random.choice(len(prob), p=prob))
|
|
57
|
+
return np.array(actions)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EpsilonTracker:
|
|
61
|
+
"""
|
|
62
|
+
Updates epsilon according to linear schedule
|
|
63
|
+
"""
|
|
64
|
+
def __init__(self, selector: EpsilonGreedyActionSelector,
|
|
65
|
+
eps_start: tt.Union[int, float],
|
|
66
|
+
eps_final: tt.Union[int, float],
|
|
67
|
+
eps_frames: int):
|
|
68
|
+
self.selector = selector
|
|
69
|
+
self.eps_start = eps_start
|
|
70
|
+
self.eps_final = eps_final
|
|
71
|
+
self.eps_frames = eps_frames
|
|
72
|
+
self.frame(0)
|
|
73
|
+
|
|
74
|
+
def frame(self, frame: int):
|
|
75
|
+
eps = self.eps_start - frame / self.eps_frames
|
|
76
|
+
self.selector.epsilon = max(self.eps_final, eps)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent is something which converts states into actions and has state
|
|
3
|
+
"""
|
|
4
|
+
import abc
|
|
5
|
+
import copy
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
from torch import nn
|
|
9
|
+
import torch.nn.functional as F
|
|
10
|
+
import typing as tt
|
|
11
|
+
|
|
12
|
+
from . import actions
|
|
13
|
+
|
|
14
|
+
States = tt.List[np.ndarray] | np.ndarray
|
|
15
|
+
AgentStates = tt.List[tt.Any]
|
|
16
|
+
Preprocessor = tt.Callable[[States], torch.Tensor]
|
|
17
|
+
|
|
18
|
+
CPU_DEVICE = torch.device("cpu")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def default_states_preprocessor(states: States) -> torch.Tensor:
|
|
22
|
+
"""
|
|
23
|
+
Convert list of states into the form suitable for model
|
|
24
|
+
:param states: list of numpy arrays with states or numpy array
|
|
25
|
+
:return: torch.Tensor
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(states, list):
|
|
28
|
+
if len(states) == 1:
|
|
29
|
+
np_states = np.expand_dims(states[0], 0)
|
|
30
|
+
else:
|
|
31
|
+
np_states = np.asarray([np.asarray(s) for s in states])
|
|
32
|
+
else:
|
|
33
|
+
np_states = states
|
|
34
|
+
return torch.as_tensor(np_states)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def float32_preprocessor(states: States):
|
|
38
|
+
np_states = np.array(states, dtype=np.float32)
|
|
39
|
+
return torch.as_tensor(np_states)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BaseAgent(abc.ABC):
|
|
43
|
+
"""
|
|
44
|
+
Base Agent, sharing most of logic with concrete agent implementations.
|
|
45
|
+
"""
|
|
46
|
+
def initial_state(self) -> tt.Optional[tt.Any]:
|
|
47
|
+
"""
|
|
48
|
+
Should create initial empty state for the agent. It will be
|
|
49
|
+
called for the start of the episode
|
|
50
|
+
:return: Anything agent want to remember
|
|
51
|
+
"""
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def __call__(self, states: States, agent_states: AgentStates) -> tt.Tuple[np.ndarray, AgentStates]:
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class NNAgent(BaseAgent):
|
|
60
|
+
"""
|
|
61
|
+
Network-based agent
|
|
62
|
+
"""
|
|
63
|
+
def __init__(self, model: nn.Module, action_selector: actions.ActionSelector,
|
|
64
|
+
device: torch.device, preprocessor: Preprocessor):
|
|
65
|
+
"""
|
|
66
|
+
Constructor of base agent
|
|
67
|
+
:param model: model to be used
|
|
68
|
+
:param action_selector: action selector
|
|
69
|
+
:param device: device for tensors
|
|
70
|
+
:param preprocessor: states preprocessor
|
|
71
|
+
"""
|
|
72
|
+
self.model = model
|
|
73
|
+
self.action_selector = action_selector
|
|
74
|
+
self.device = device
|
|
75
|
+
self.preprocessor = preprocessor
|
|
76
|
+
|
|
77
|
+
@abc.abstractmethod
|
|
78
|
+
def _net_filter(self, net_out: tt.Any, agent_states: AgentStates) -> \
|
|
79
|
+
tt.Tuple[torch.Tensor, AgentStates]:
|
|
80
|
+
"""
|
|
81
|
+
Internal method, processing network output and states into selector's input and new states
|
|
82
|
+
:param net_out: output from the network
|
|
83
|
+
:param agent_states: agent states
|
|
84
|
+
:return: tuple with tensor to be fed into selector and new states
|
|
85
|
+
"""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
@torch.no_grad()
|
|
89
|
+
def __call__(self, states: States, agent_states: AgentStates = None) -> tt.Tuple[np.ndarray, AgentStates]:
|
|
90
|
+
"""
|
|
91
|
+
Convert observations and states into actions to take
|
|
92
|
+
:param states: list of environment states to process
|
|
93
|
+
:param agent_states: list of states with the same length as observations
|
|
94
|
+
:return: tuple of actions, states
|
|
95
|
+
"""
|
|
96
|
+
if agent_states is None:
|
|
97
|
+
agent_states = [None] * len(states)
|
|
98
|
+
if self.preprocessor is not None:
|
|
99
|
+
states = self.preprocessor(states)
|
|
100
|
+
if torch.is_tensor(states):
|
|
101
|
+
states = states.to(self.device)
|
|
102
|
+
q_v = self.model(states)
|
|
103
|
+
q_v, new_states = self._net_filter(q_v, agent_states)
|
|
104
|
+
q = q_v.data.cpu().numpy()
|
|
105
|
+
actions = self.action_selector(q)
|
|
106
|
+
return actions, new_states
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DQNAgent(NNAgent):
|
|
110
|
+
"""
|
|
111
|
+
DQNAgent is a memoryless DQN agent which calculates Q values
|
|
112
|
+
from the observations and converts them into the actions using action_selector
|
|
113
|
+
"""
|
|
114
|
+
def __init__(self, model: nn.Module, action_selector: actions.ActionSelector,
|
|
115
|
+
device: torch.device = CPU_DEVICE,
|
|
116
|
+
preprocessor: Preprocessor = default_states_preprocessor):
|
|
117
|
+
super().__init__(model, action_selector=action_selector, device=device,
|
|
118
|
+
preprocessor=preprocessor)
|
|
119
|
+
|
|
120
|
+
# not needed in DQN - we don't process Q-values returned
|
|
121
|
+
def _net_filter(self, net_out: tt.Any, agent_states: AgentStates) -> \
|
|
122
|
+
tt.Tuple[torch.Tensor, AgentStates]:
|
|
123
|
+
assert torch.is_tensor(net_out)
|
|
124
|
+
return net_out, agent_states
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TargetNet:
|
|
128
|
+
"""
|
|
129
|
+
Wrapper around model which provides copy of it instead of trained weights
|
|
130
|
+
"""
|
|
131
|
+
def __init__(self, model: nn.Module):
|
|
132
|
+
self.model = model
|
|
133
|
+
self.target_model = copy.deepcopy(model)
|
|
134
|
+
|
|
135
|
+
def sync(self):
|
|
136
|
+
self.target_model.load_state_dict(self.model.state_dict())
|
|
137
|
+
|
|
138
|
+
def alpha_sync(self, alpha):
|
|
139
|
+
"""
|
|
140
|
+
Blend params of target net with params from the model
|
|
141
|
+
:param alpha:
|
|
142
|
+
"""
|
|
143
|
+
assert isinstance(alpha, float)
|
|
144
|
+
assert 0.0 < alpha <= 1.0
|
|
145
|
+
state = self.model.state_dict()
|
|
146
|
+
tgt_state = self.target_model.state_dict()
|
|
147
|
+
for k, v in state.items():
|
|
148
|
+
tgt_state[k] = tgt_state[k] * alpha + (1 - alpha) * v
|
|
149
|
+
self.target_model.load_state_dict(tgt_state)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class PolicyAgent(NNAgent):
|
|
153
|
+
"""
|
|
154
|
+
Policy agent gets action probabilities from the model and samples actions from it
|
|
155
|
+
"""
|
|
156
|
+
def __init__(self, model: nn.Module,
|
|
157
|
+
action_selector: actions.ActionSelector = actions.ProbabilityActionSelector(),
|
|
158
|
+
device: torch.device = CPU_DEVICE, apply_softmax: bool = False,
|
|
159
|
+
preprocessor: Preprocessor = default_states_preprocessor):
|
|
160
|
+
super().__init__(model=model, action_selector=action_selector, device=device,
|
|
161
|
+
preprocessor=preprocessor)
|
|
162
|
+
self.apply_softmax = apply_softmax
|
|
163
|
+
|
|
164
|
+
def _net_filter(self, net_out: tt.Any, agent_states: AgentStates) -> \
|
|
165
|
+
tt.Tuple[torch.Tensor, AgentStates]:
|
|
166
|
+
assert torch.is_tensor(net_out)
|
|
167
|
+
if self.apply_softmax:
|
|
168
|
+
return F.softmax(net_out, dim=1), agent_states
|
|
169
|
+
return net_out, agent_states
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ActorCriticAgent(NNAgent):
|
|
173
|
+
"""
|
|
174
|
+
Policy agent which returns policy and value tensors from observations. Value are stored in agent's state
|
|
175
|
+
and could be reused for rollouts calculations by ExperienceSource.
|
|
176
|
+
"""
|
|
177
|
+
def __init__(self, model: nn.Module,
|
|
178
|
+
action_selector: actions.ActionSelector = actions.ProbabilityActionSelector(),
|
|
179
|
+
device: torch.device = CPU_DEVICE, apply_softmax: bool = False,
|
|
180
|
+
preprocessor: Preprocessor = default_states_preprocessor):
|
|
181
|
+
super().__init__(model=model, action_selector=action_selector, device=device, preprocessor=preprocessor)
|
|
182
|
+
self.apply_softmax = apply_softmax
|
|
183
|
+
|
|
184
|
+
def _net_filter(self, net_out: tt.Any, agent_states: AgentStates) -> \
|
|
185
|
+
tt.Tuple[torch.Tensor, AgentStates]:
|
|
186
|
+
assert isinstance(net_out, tuple)
|
|
187
|
+
policy_v, values_v = net_out
|
|
188
|
+
assert torch.is_tensor(policy_v)
|
|
189
|
+
assert torch.is_tensor(values_v)
|
|
190
|
+
agent_states = values_v.data.squeeze().cpu().numpy().tolist()
|
|
191
|
+
if self.apply_softmax:
|
|
192
|
+
return F.softmax(policy_v, dim=1), agent_states
|
|
193
|
+
return policy_v, agent_states
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import gymnasium as gym
|
|
2
|
+
import torch
|
|
3
|
+
import random
|
|
4
|
+
import collections
|
|
5
|
+
import typing as tt
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from collections import deque
|
|
11
|
+
|
|
12
|
+
from .agent import BaseAgent
|
|
13
|
+
|
|
14
|
+
State = np.ndarray
|
|
15
|
+
Action = int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Experience:
|
|
20
|
+
state: State
|
|
21
|
+
action: Action
|
|
22
|
+
reward: float
|
|
23
|
+
done_trunc: bool
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ExperienceSource:
|
|
27
|
+
"""
|
|
28
|
+
Simple n-step experience source using single or multiple environments
|
|
29
|
+
|
|
30
|
+
Every experience contains n list of Experience entries
|
|
31
|
+
"""
|
|
32
|
+
Item = tt.Tuple[Experience, ...]
|
|
33
|
+
|
|
34
|
+
def __init__(self, env: gym.Env | tt.Collection[gym.Env], agent: BaseAgent,
|
|
35
|
+
steps_count: int = 2, steps_delta: int = 1,
|
|
36
|
+
env_seed: tt.Optional[int] = None):
|
|
37
|
+
"""
|
|
38
|
+
Create simple experience source
|
|
39
|
+
:param env: environment or list of environments to be used
|
|
40
|
+
:param agent: callable to convert batch of states into actions to take
|
|
41
|
+
:param steps_count: count of steps to track for every experience chain
|
|
42
|
+
:param steps_delta: how many steps to do between experience items
|
|
43
|
+
:param env_seed: seed to be used in Env.reset() call
|
|
44
|
+
"""
|
|
45
|
+
assert steps_count >= 1
|
|
46
|
+
if isinstance(env, (list, tuple)):
|
|
47
|
+
self.pool = env
|
|
48
|
+
# do the check for the multiple copies passed
|
|
49
|
+
ids = set(id(e) for e in env)
|
|
50
|
+
if len(ids) < len(env):
|
|
51
|
+
raise ValueError("You passed single environment instance multiple times")
|
|
52
|
+
else:
|
|
53
|
+
self.pool = [env]
|
|
54
|
+
self.agent = agent
|
|
55
|
+
self.steps_count = steps_count
|
|
56
|
+
self.steps_delta = steps_delta
|
|
57
|
+
self.total_rewards = []
|
|
58
|
+
self.total_steps = []
|
|
59
|
+
self.agent_states = [agent.initial_state() for _ in self.pool]
|
|
60
|
+
self.env_seed = env_seed
|
|
61
|
+
|
|
62
|
+
def __iter__(self) -> tt.Generator[Item, None, None]:
|
|
63
|
+
states, histories, cur_rewards, cur_steps = [], [], [], []
|
|
64
|
+
for env in self.pool:
|
|
65
|
+
if self.env_seed is not None:
|
|
66
|
+
obs, _ = env.reset(seed=self.env_seed)
|
|
67
|
+
else:
|
|
68
|
+
obs, _ = env.reset()
|
|
69
|
+
states.append(obs)
|
|
70
|
+
histories.append(deque(maxlen=self.steps_count))
|
|
71
|
+
cur_rewards.append(0.0)
|
|
72
|
+
cur_steps.append(0)
|
|
73
|
+
|
|
74
|
+
iter_idx = 0
|
|
75
|
+
while True:
|
|
76
|
+
actions, self.agent_states = self.agent(states, self.agent_states)
|
|
77
|
+
for idx, env in enumerate(self.pool):
|
|
78
|
+
state = states[idx]
|
|
79
|
+
action = actions[idx]
|
|
80
|
+
history = histories[idx]
|
|
81
|
+
next_state, r, is_done, is_tr, _ = env.step(action)
|
|
82
|
+
cur_rewards[idx] += r
|
|
83
|
+
cur_steps[idx] += 1
|
|
84
|
+
history.append(Experience(state=state, action=action, reward=r, done_trunc=is_done or is_tr))
|
|
85
|
+
if len(history) == self.steps_count and iter_idx % self.steps_delta == 0:
|
|
86
|
+
yield tuple(history)
|
|
87
|
+
states[idx] = next_state
|
|
88
|
+
if is_done or is_tr:
|
|
89
|
+
# generate tail of history
|
|
90
|
+
if 0 < len(history) < self.steps_count:
|
|
91
|
+
yield tuple(history)
|
|
92
|
+
while len(history) > 1:
|
|
93
|
+
history.popleft()
|
|
94
|
+
yield tuple(history)
|
|
95
|
+
self.total_rewards.append(cur_rewards[idx])
|
|
96
|
+
self.total_steps.append(cur_steps[idx])
|
|
97
|
+
cur_rewards[idx] = 0.0
|
|
98
|
+
cur_steps[idx] = 0
|
|
99
|
+
if self.env_seed is not None:
|
|
100
|
+
states[idx], _ = env.reset(seed=self.env_seed)
|
|
101
|
+
else:
|
|
102
|
+
states[idx], _ = env.reset()
|
|
103
|
+
self.agent_states[idx] = self.agent.initial_state()
|
|
104
|
+
history.clear()
|
|
105
|
+
iter_idx += 1
|
|
106
|
+
|
|
107
|
+
def pop_total_rewards(self) -> tt.List[float]:
|
|
108
|
+
r = self.total_rewards
|
|
109
|
+
if r:
|
|
110
|
+
self.total_rewards = []
|
|
111
|
+
self.total_steps = []
|
|
112
|
+
return r
|
|
113
|
+
|
|
114
|
+
def pop_rewards_steps(self) -> tt.List[tt.Tuple[float, int]]:
|
|
115
|
+
res = list(zip(self.total_rewards, self.total_steps))
|
|
116
|
+
if res:
|
|
117
|
+
self.total_rewards, self.total_steps = [], []
|
|
118
|
+
return res
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class ExperienceFirstLast:
|
|
123
|
+
state: State
|
|
124
|
+
action: Action
|
|
125
|
+
reward: float
|
|
126
|
+
last_state: tt.Optional[State]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ExperienceSourceFirstLast(ExperienceSource):
|
|
130
|
+
"""
|
|
131
|
+
This is a wrapper around ExperienceSource to prevent storing full trajectory in replay buffer when we need
|
|
132
|
+
only first and last states. For every trajectory piece it calculates discounted reward and emits only first
|
|
133
|
+
and last states and action taken in the first state.
|
|
134
|
+
|
|
135
|
+
If we have partial trajectory at the end of episode, last_state will be None
|
|
136
|
+
"""
|
|
137
|
+
def __init__(self, env: gym.Env, agent: BaseAgent, gamma: float,
|
|
138
|
+
steps_count: int = 1, steps_delta: int = 1, env_seed: tt.Optional[int] = None):
|
|
139
|
+
super(ExperienceSourceFirstLast, self).__init__(env, agent, steps_count+1, steps_delta, env_seed=env_seed)
|
|
140
|
+
self.gamma = gamma
|
|
141
|
+
self.steps = steps_count
|
|
142
|
+
|
|
143
|
+
def __iter__(self) -> tt.Generator[ExperienceFirstLast, None, None]:
|
|
144
|
+
for exp in super(ExperienceSourceFirstLast, self).__iter__():
|
|
145
|
+
if exp[-1].done_trunc and len(exp) <= self.steps:
|
|
146
|
+
last_state = None
|
|
147
|
+
elems = exp
|
|
148
|
+
else:
|
|
149
|
+
last_state = exp[-1].state
|
|
150
|
+
elems = exp[:-1]
|
|
151
|
+
total_reward = 0.0
|
|
152
|
+
for e in reversed(elems):
|
|
153
|
+
total_reward *= self.gamma
|
|
154
|
+
total_reward += e.reward
|
|
155
|
+
yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action,
|
|
156
|
+
reward=total_reward, last_state=last_state)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def vector_rewards(rewards: tt.Deque[np.ndarray], dones: tt.Deque[np.ndarray], gamma: float) -> np.ndarray:
|
|
160
|
+
"""
|
|
161
|
+
Calculate rewards from vectorized environment for given amount of steps.
|
|
162
|
+
:param rewards: deque with observed rewards
|
|
163
|
+
:param dones: deque with bool flags indicating end of episode
|
|
164
|
+
:param gamma: gamma constant
|
|
165
|
+
:return: vector with accumulated rewards
|
|
166
|
+
"""
|
|
167
|
+
res = np.zeros(rewards[0].shape[0], dtype=np.float32)
|
|
168
|
+
for r, d in zip(reversed(rewards), reversed(dones)):
|
|
169
|
+
res *= gamma * (1. - d)
|
|
170
|
+
res += r
|
|
171
|
+
return res
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class VectorExperienceSourceFirstLast(ExperienceSource):
|
|
175
|
+
"""
|
|
176
|
+
ExperienceSourceFirstLast which supports VectorEnv from Gymnasium.
|
|
177
|
+
"""
|
|
178
|
+
def __init__(self, env: gym.vector.VectorEnv, agent: BaseAgent,
|
|
179
|
+
gamma: float, steps_count: int = 1, env_seed: tt.Optional[int] = None,
|
|
180
|
+
unnest_data: bool = True):
|
|
181
|
+
"""
|
|
182
|
+
Construct vectorized version of ExperienceSourceFirstLast
|
|
183
|
+
:param env: vectorized environment
|
|
184
|
+
:param agent: agent to use
|
|
185
|
+
:param gamma: gamma for reward calculation
|
|
186
|
+
:param steps_count: count of steps
|
|
187
|
+
:param env_seed: seed for environments reset
|
|
188
|
+
:param unnest_data: should we unnest data in the iterator. If True (default)
|
|
189
|
+
ExperienceFirstLast will be yielded sequentially. If False, we'll keep them in a list as we
|
|
190
|
+
got them from env vector.
|
|
191
|
+
"""
|
|
192
|
+
super().__init__(env, agent, steps_count+1, steps_delta=1, env_seed=env_seed)
|
|
193
|
+
self.env = env
|
|
194
|
+
self.gamma = gamma
|
|
195
|
+
self.steps = steps_count
|
|
196
|
+
self.unnest_data = unnest_data
|
|
197
|
+
self.agent_state = self.agent_states[0]
|
|
198
|
+
|
|
199
|
+
def _iter_env_idx_obs_next(self, b_obs, b_next_obs) -> tt.Generator[tt.Tuple[
|
|
200
|
+
int, tt.Any, tt.Any
|
|
201
|
+
], None, None]:
|
|
202
|
+
"""
|
|
203
|
+
Iterate over individual environment observations and next observations.
|
|
204
|
+
Take into account Tuple observation space (which is handled specially in Vectorized envs)
|
|
205
|
+
:param b_obs: vectorized observations
|
|
206
|
+
:param b_next_obs: vectorized next observations
|
|
207
|
+
:yields: Tuple of index, observation and next observation
|
|
208
|
+
"""
|
|
209
|
+
if isinstance(self.env.single_observation_space, gym.spaces.Tuple):
|
|
210
|
+
obs_iter = zip(*b_obs)
|
|
211
|
+
next_obs_iter = zip(*b_next_obs)
|
|
212
|
+
else:
|
|
213
|
+
obs_iter = b_obs
|
|
214
|
+
next_obs_iter = b_next_obs
|
|
215
|
+
yield from zip(range(self.env.num_envs), obs_iter, next_obs_iter)
|
|
216
|
+
|
|
217
|
+
def __iter__(self) -> tt.Generator[tt.List[ExperienceFirstLast] | ExperienceFirstLast, None, None]:
|
|
218
|
+
q_states = collections.deque(maxlen=self.steps+1)
|
|
219
|
+
q_actions = collections.deque(maxlen=self.steps+1)
|
|
220
|
+
q_rewards = collections.deque(maxlen=self.steps+1)
|
|
221
|
+
q_dones = collections.deque(maxlen=self.steps+1)
|
|
222
|
+
total_rewards = np.zeros(self.env.num_envs, dtype=np.float32)
|
|
223
|
+
total_steps = np.zeros_like(total_rewards, dtype=np.int64)
|
|
224
|
+
|
|
225
|
+
if self.env_seed is not None:
|
|
226
|
+
obs, _ = self.env.reset(seed=self.env_seed)
|
|
227
|
+
else:
|
|
228
|
+
obs, _ = self.env.reset()
|
|
229
|
+
env_indices = np.arange(self.env.num_envs)
|
|
230
|
+
|
|
231
|
+
while True:
|
|
232
|
+
q_states.append(obs)
|
|
233
|
+
actions, self.agent_state = self.agent(obs, self.agent_state)
|
|
234
|
+
q_actions.append(actions)
|
|
235
|
+
next_obs, r, is_done, is_tr, _ = self.env.step(actions)
|
|
236
|
+
total_rewards += r
|
|
237
|
+
total_steps += 1
|
|
238
|
+
done_or_tr = is_done | is_tr
|
|
239
|
+
q_rewards.append(r)
|
|
240
|
+
q_dones.append(done_or_tr)
|
|
241
|
+
|
|
242
|
+
# process environments which are done at this step
|
|
243
|
+
if done_or_tr.any():
|
|
244
|
+
indices = env_indices[done_or_tr]
|
|
245
|
+
self.total_steps.extend(total_steps[indices])
|
|
246
|
+
self.total_rewards.extend(total_rewards[indices])
|
|
247
|
+
total_steps[indices] = 0
|
|
248
|
+
total_rewards[indices] = 0.0
|
|
249
|
+
|
|
250
|
+
if len(q_states) == q_states.maxlen:
|
|
251
|
+
# enough data for calculation
|
|
252
|
+
results = []
|
|
253
|
+
rewards = vector_rewards(q_rewards, q_dones, self.gamma)
|
|
254
|
+
for i, e_obs, e_next_obs in self._iter_env_idx_obs_next(q_states[0], next_obs):
|
|
255
|
+
# if anywhere in the trajectory we have ended episode flag,
|
|
256
|
+
# the last state will be None
|
|
257
|
+
ep_ended = any(map(lambda d: d[i], q_dones))
|
|
258
|
+
last_state = e_next_obs if not ep_ended else None
|
|
259
|
+
results.append(ExperienceFirstLast(
|
|
260
|
+
state=e_obs,
|
|
261
|
+
action=q_actions[0][i],
|
|
262
|
+
reward=rewards[i],
|
|
263
|
+
last_state=last_state,
|
|
264
|
+
))
|
|
265
|
+
if self.unnest_data:
|
|
266
|
+
yield from results
|
|
267
|
+
else:
|
|
268
|
+
yield results
|
|
269
|
+
obs = next_obs
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def discount_with_dones(rewards, dones, gamma):
|
|
273
|
+
discounted = []
|
|
274
|
+
r = 0
|
|
275
|
+
for reward, done in zip(rewards[::-1], dones[::-1]):
|
|
276
|
+
r = reward + gamma*r*(1.-done)
|
|
277
|
+
discounted.append(r)
|
|
278
|
+
return discounted[::-1]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ExperienceSourceRollouts:
|
|
282
|
+
"""
|
|
283
|
+
TODO: need to be updated
|
|
284
|
+
N-step rollout experience source following A3C rollouts scheme. Have to be used with agent,
|
|
285
|
+
keeping the value in its state (for example, agent.ActorCriticAgent).
|
|
286
|
+
|
|
287
|
+
Yields batches of num_envs * n_steps samples with the following arrays:
|
|
288
|
+
1. observations
|
|
289
|
+
2. actions
|
|
290
|
+
3. discounted rewards, with values approximation
|
|
291
|
+
4. values
|
|
292
|
+
"""
|
|
293
|
+
def __init__(self, env, agent, gamma, steps_count=5):
|
|
294
|
+
"""
|
|
295
|
+
Constructs the rollout experience source
|
|
296
|
+
:param env: environment or list of environments to be used
|
|
297
|
+
:param agent: callable to convert batch of states into actions
|
|
298
|
+
:param steps_count: how many steps to perform rollouts
|
|
299
|
+
"""
|
|
300
|
+
assert isinstance(env, (gym.Env, list, tuple))
|
|
301
|
+
assert isinstance(agent, BaseAgent)
|
|
302
|
+
assert isinstance(gamma, float)
|
|
303
|
+
assert isinstance(steps_count, int)
|
|
304
|
+
assert steps_count >= 1
|
|
305
|
+
|
|
306
|
+
if isinstance(env, (list, tuple)):
|
|
307
|
+
self.pool = env
|
|
308
|
+
else:
|
|
309
|
+
self.pool = [env]
|
|
310
|
+
self.agent = agent
|
|
311
|
+
self.gamma = gamma
|
|
312
|
+
self.steps_count = steps_count
|
|
313
|
+
self.total_rewards = []
|
|
314
|
+
self.total_steps = []
|
|
315
|
+
|
|
316
|
+
def __iter__(self):
|
|
317
|
+
pool_size = len(self.pool)
|
|
318
|
+
states = [np.array(e.reset()) for e in self.pool]
|
|
319
|
+
mb_states = np.zeros((pool_size, self.steps_count) + states[0].shape, dtype=states[0].dtype)
|
|
320
|
+
mb_rewards = np.zeros((pool_size, self.steps_count), dtype=np.float32)
|
|
321
|
+
mb_values = np.zeros((pool_size, self.steps_count), dtype=np.float32)
|
|
322
|
+
mb_actions = np.zeros((pool_size, self.steps_count), dtype=np.int64)
|
|
323
|
+
mb_dones = np.zeros((pool_size, self.steps_count), dtype=np.bool)
|
|
324
|
+
total_rewards = [0.0] * pool_size
|
|
325
|
+
total_steps = [0] * pool_size
|
|
326
|
+
agent_states = None
|
|
327
|
+
step_idx = 0
|
|
328
|
+
|
|
329
|
+
while True:
|
|
330
|
+
actions, agent_states = self.agent(states, agent_states)
|
|
331
|
+
rewards = []
|
|
332
|
+
dones = []
|
|
333
|
+
new_states = []
|
|
334
|
+
for env_idx, (e, action) in enumerate(zip(self.pool, actions)):
|
|
335
|
+
o, r, done, _ = e.step(action)
|
|
336
|
+
total_rewards[env_idx] += r
|
|
337
|
+
total_steps[env_idx] += 1
|
|
338
|
+
if done:
|
|
339
|
+
o = e.reset()
|
|
340
|
+
self.total_rewards.append(total_rewards[env_idx])
|
|
341
|
+
self.total_steps.append(total_steps[env_idx])
|
|
342
|
+
total_rewards[env_idx] = 0.0
|
|
343
|
+
total_steps[env_idx] = 0
|
|
344
|
+
new_states.append(np.array(o))
|
|
345
|
+
dones.append(done)
|
|
346
|
+
rewards.append(r)
|
|
347
|
+
# we need an extra step to get values approximation for rollouts
|
|
348
|
+
if step_idx == self.steps_count:
|
|
349
|
+
# calculate rollout rewards
|
|
350
|
+
for env_idx, (env_rewards, env_dones, last_value) in enumerate(zip(mb_rewards, mb_dones, agent_states)):
|
|
351
|
+
env_rewards = env_rewards.tolist()
|
|
352
|
+
env_dones = env_dones.tolist()
|
|
353
|
+
if not env_dones[-1]:
|
|
354
|
+
env_rewards = discount_with_dones(env_rewards + [last_value], env_dones + [False], self.gamma)[:-1]
|
|
355
|
+
else:
|
|
356
|
+
env_rewards = discount_with_dones(env_rewards, env_dones, self.gamma)
|
|
357
|
+
mb_rewards[env_idx] = env_rewards
|
|
358
|
+
yield mb_states.reshape((-1,) + mb_states.shape[2:]), mb_rewards.flatten(), mb_actions.flatten(), mb_values.flatten()
|
|
359
|
+
step_idx = 0
|
|
360
|
+
mb_states[:, step_idx] = states
|
|
361
|
+
mb_rewards[:, step_idx] = rewards
|
|
362
|
+
mb_values[:, step_idx] = agent_states
|
|
363
|
+
mb_actions[:, step_idx] = actions
|
|
364
|
+
mb_dones[:, step_idx] = dones
|
|
365
|
+
step_idx += 1
|
|
366
|
+
states = new_states
|
|
367
|
+
|
|
368
|
+
def pop_total_rewards(self):
|
|
369
|
+
r = self.total_rewards
|
|
370
|
+
if r:
|
|
371
|
+
self.total_rewards = []
|
|
372
|
+
self.total_steps = []
|
|
373
|
+
return r
|
|
374
|
+
|
|
375
|
+
def pop_rewards_steps(self):
|
|
376
|
+
res = list(zip(self.total_rewards, self.total_steps))
|
|
377
|
+
if res:
|
|
378
|
+
self.total_rewards, self.total_steps = [], []
|
|
379
|
+
return res
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class ExperienceSourceBuffer:
|
|
383
|
+
"""
|
|
384
|
+
The same as ExperienceSource, but takes episodes from the buffer
|
|
385
|
+
"""
|
|
386
|
+
def __init__(self, buffer, steps_count=1):
|
|
387
|
+
"""
|
|
388
|
+
Create buffered experience source
|
|
389
|
+
:param buffer: list of episodes, each is a list of Experience object
|
|
390
|
+
:param steps_count: count of steps in every entry
|
|
391
|
+
"""
|
|
392
|
+
self.update_buffer(buffer)
|
|
393
|
+
self.steps_count = steps_count
|
|
394
|
+
|
|
395
|
+
def update_buffer(self, buffer):
|
|
396
|
+
self.buffer = buffer
|
|
397
|
+
self.lens = list(map(len, buffer))
|
|
398
|
+
|
|
399
|
+
def __iter__(self):
|
|
400
|
+
"""
|
|
401
|
+
Infinitely sample episode from the buffer and then sample item offset
|
|
402
|
+
"""
|
|
403
|
+
while True:
|
|
404
|
+
episode = random.randrange(len(self.buffer))
|
|
405
|
+
ofs = random.randrange(self.lens[episode] - self.steps_count - 1)
|
|
406
|
+
yield self.buffer[episode][ofs:ofs+self.steps_count]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class ExperienceReplayBuffer:
|
|
410
|
+
def __init__(self, experience_source: tt.Optional[ExperienceSource], buffer_size: int):
|
|
411
|
+
self.experience_source_iter = None if experience_source is None else iter(experience_source)
|
|
412
|
+
self.buffer: tt.List[ExperienceSource.Item] = []
|
|
413
|
+
self.capacity = buffer_size
|
|
414
|
+
self.pos = 0
|
|
415
|
+
|
|
416
|
+
def __len__(self):
|
|
417
|
+
return len(self.buffer)
|
|
418
|
+
|
|
419
|
+
def __iter__(self) -> tt.Iterator[ExperienceSource.Item]:
|
|
420
|
+
return iter(self.buffer)
|
|
421
|
+
|
|
422
|
+
def sample(self, batch_size: int) -> tt.List[ExperienceSource.Item]:
|
|
423
|
+
"""
|
|
424
|
+
Get one random batch from experience replay
|
|
425
|
+
:param batch_size: size of the batch to sample
|
|
426
|
+
:return: list of experience entries
|
|
427
|
+
"""
|
|
428
|
+
if len(self.buffer) <= batch_size:
|
|
429
|
+
return self.buffer
|
|
430
|
+
# Warning: replace=False makes random.choice O(n)
|
|
431
|
+
keys = np.random.choice(len(self.buffer), batch_size, replace=True)
|
|
432
|
+
return [self.buffer[key] for key in keys]
|
|
433
|
+
|
|
434
|
+
def _add(self, sample: ExperienceSource.Item):
|
|
435
|
+
if len(self.buffer) < self.capacity:
|
|
436
|
+
self.buffer.append(sample)
|
|
437
|
+
else:
|
|
438
|
+
self.buffer[self.pos] = sample
|
|
439
|
+
self.pos = (self.pos + 1) % self.capacity
|
|
440
|
+
|
|
441
|
+
def populate(self, samples: int):
|
|
442
|
+
"""
|
|
443
|
+
Populates samples into the buffer
|
|
444
|
+
:param samples: how many samples to populate
|
|
445
|
+
"""
|
|
446
|
+
for _ in range(samples):
|
|
447
|
+
entry = next(self.experience_source_iter)
|
|
448
|
+
self._add(entry)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class PrioReplayBufferNaive:
|
|
452
|
+
def __init__(self, exp_source, buf_size, prob_alpha=0.6):
|
|
453
|
+
self.exp_source_iter = iter(exp_source)
|
|
454
|
+
self.prob_alpha = prob_alpha
|
|
455
|
+
self.capacity = buf_size
|
|
456
|
+
self.pos = 0
|
|
457
|
+
self.buffer = []
|
|
458
|
+
self.priorities = np.zeros((buf_size, ), dtype=np.float32)
|
|
459
|
+
|
|
460
|
+
def __len__(self):
|
|
461
|
+
return len(self.buffer)
|
|
462
|
+
|
|
463
|
+
def populate(self, count):
|
|
464
|
+
max_prio = self.priorities.max() if self.buffer else 1.0
|
|
465
|
+
for _ in range(count):
|
|
466
|
+
sample = next(self.exp_source_iter)
|
|
467
|
+
if len(self.buffer) < self.capacity:
|
|
468
|
+
self.buffer.append(sample)
|
|
469
|
+
else:
|
|
470
|
+
self.buffer[self.pos] = sample
|
|
471
|
+
self.priorities[self.pos] = max_prio
|
|
472
|
+
self.pos = (self.pos + 1) % self.capacity
|
|
473
|
+
|
|
474
|
+
def sample(self, batch_size, beta=0.4):
|
|
475
|
+
if len(self.buffer) == self.capacity:
|
|
476
|
+
prios = self.priorities
|
|
477
|
+
else:
|
|
478
|
+
prios = self.priorities[:self.pos]
|
|
479
|
+
probs = np.array(prios, dtype=np.float32) ** self.prob_alpha
|
|
480
|
+
|
|
481
|
+
probs /= probs.sum()
|
|
482
|
+
indices = np.random.choice(len(self.buffer), batch_size, p=probs, replace=True)
|
|
483
|
+
samples = [self.buffer[idx] for idx in indices]
|
|
484
|
+
total = len(self.buffer)
|
|
485
|
+
weights = (total * probs[indices]) ** (-beta)
|
|
486
|
+
weights /= weights.max()
|
|
487
|
+
return samples, indices, np.array(weights, dtype=np.float32)
|
|
488
|
+
|
|
489
|
+
def update_priorities(self, batch_indices, batch_priorities):
|
|
490
|
+
for idx, prio in zip(batch_indices, batch_priorities):
|
|
491
|
+
self.priorities[idx] = prio
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class BatchPreprocessor:
|
|
496
|
+
"""
|
|
497
|
+
Abstract preprocessor class descendants to which converts experience
|
|
498
|
+
batch to form suitable to learning.
|
|
499
|
+
"""
|
|
500
|
+
def preprocess(self, batch):
|
|
501
|
+
raise NotImplementedError
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class QLearningPreprocessor(BatchPreprocessor):
|
|
505
|
+
"""
|
|
506
|
+
Supports SimpleDQN, TargetDQN, DoubleDQN and can additionally feed TD-error back to
|
|
507
|
+
experience replay buffer.
|
|
508
|
+
|
|
509
|
+
To use different modes, use appropriate class method
|
|
510
|
+
"""
|
|
511
|
+
def __init__(self, model, target_model, use_double_dqn=False, batch_td_error_hook=None, gamma=0.99, device="cpu"):
|
|
512
|
+
self.model = model
|
|
513
|
+
self.target_model = target_model
|
|
514
|
+
self.use_double_dqn = use_double_dqn
|
|
515
|
+
self.batch_dt_error_hook = batch_td_error_hook
|
|
516
|
+
self.gamma = gamma
|
|
517
|
+
self.device = device
|
|
518
|
+
|
|
519
|
+
@staticmethod
|
|
520
|
+
def simple_dqn(model, **kwargs):
|
|
521
|
+
return QLearningPreprocessor(model=model, target_model=None, use_double_dqn=False, **kwargs)
|
|
522
|
+
|
|
523
|
+
@staticmethod
|
|
524
|
+
def target_dqn(model, target_model, **kwards):
|
|
525
|
+
return QLearningPreprocessor(model, target_model, use_double_dqn=False, **kwards)
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def double_dqn(model, target_model, **kwargs):
|
|
529
|
+
return QLearningPreprocessor(model, target_model, use_double_dqn=True, **kwargs)
|
|
530
|
+
|
|
531
|
+
def _calc_Q(self, states_first, states_last):
|
|
532
|
+
"""
|
|
533
|
+
Calculates apropriate q values for first and last states. Way of calculate depends on our settings.
|
|
534
|
+
:param states_first: numpy array of first states
|
|
535
|
+
:param states_last: numpy array of last states
|
|
536
|
+
:return: tuple of numpy arrays of q values
|
|
537
|
+
"""
|
|
538
|
+
# here we need both first and last values calculated using our main model, so we
|
|
539
|
+
# combine both states into one batch for efficiency and separate results later
|
|
540
|
+
if self.target_model is None or self.use_double_dqn:
|
|
541
|
+
states_t = torch.tensor(np.concatenate((states_first, states_last), axis=0)).to(self.device)
|
|
542
|
+
res_both = self.model(states_t).data.cpu().numpy()
|
|
543
|
+
return res_both[:len(states_first)], res_both[len(states_first):]
|
|
544
|
+
|
|
545
|
+
# in this case we have target_model set and use_double_dqn==False
|
|
546
|
+
# so, we should calculate first_q and last_q using different models
|
|
547
|
+
states_first_v = torch.tensor(states_first).to(self.device)
|
|
548
|
+
states_last_v = torch.tensor(states_last).to(self.device)
|
|
549
|
+
q_first = self.model(states_first_v).data
|
|
550
|
+
q_last = self.target_model(states_last_v).data
|
|
551
|
+
return q_first.cpu().numpy(), q_last.cpu().numpy()
|
|
552
|
+
|
|
553
|
+
def _calc_target_rewards(self, states_last, q_last):
|
|
554
|
+
"""
|
|
555
|
+
Calculate rewards from final states according to variants from our construction:
|
|
556
|
+
1. simple DQN: max(Q(states, model))
|
|
557
|
+
2. target DQN: max(Q(states, target_model))
|
|
558
|
+
3. double DQN: Q(states, target_model)[argmax(Q(states, model)]
|
|
559
|
+
:param states_last: numpy array of last states from the games
|
|
560
|
+
:param q_last: numpy array of last q values
|
|
561
|
+
:return: vector of target rewards
|
|
562
|
+
"""
|
|
563
|
+
# in this case we handle both simple DQN and target DQN
|
|
564
|
+
if self.target_model is None or not self.use_double_dqn:
|
|
565
|
+
return q_last.max(axis=1)
|
|
566
|
+
|
|
567
|
+
# here we have target_model set and use_double_dqn==True
|
|
568
|
+
actions = q_last.argmax(axis=1)
|
|
569
|
+
# calculate Q values using target net
|
|
570
|
+
states_last_v = torch.tensor(states_last).to(self.device)
|
|
571
|
+
q_last_target = self.target_model(states_last_v).data.cpu().numpy()
|
|
572
|
+
return q_last_target[range(q_last_target.shape[0]), actions]
|
|
573
|
+
|
|
574
|
+
def preprocess(self, batch):
|
|
575
|
+
"""
|
|
576
|
+
Calculates data for Q learning from batch of observations
|
|
577
|
+
:param batch: list of lists of Experience objects
|
|
578
|
+
:return: tuple of numpy arrays:
|
|
579
|
+
1. states -- observations
|
|
580
|
+
2. target Q-values
|
|
581
|
+
3. vector of td errors for every batch entry
|
|
582
|
+
"""
|
|
583
|
+
# first and last states for every entry
|
|
584
|
+
state_0 = np.array([exp[0].state for exp in batch], dtype=np.float32)
|
|
585
|
+
state_L = np.array([exp[-1].state for exp in batch], dtype=np.float32)
|
|
586
|
+
|
|
587
|
+
q0, qL = self._calc_Q(state_0, state_L)
|
|
588
|
+
rewards = self._calc_target_rewards(state_L, qL)
|
|
589
|
+
|
|
590
|
+
td = np.zeros(shape=(len(batch),))
|
|
591
|
+
|
|
592
|
+
for idx, (total_reward, exps) in enumerate(zip(rewards, batch)):
|
|
593
|
+
# game is done, no final reward
|
|
594
|
+
if exps[-1].done:
|
|
595
|
+
total_reward = 0.0
|
|
596
|
+
for exp in reversed(exps[:-1]):
|
|
597
|
+
total_reward *= self.gamma
|
|
598
|
+
total_reward += exp.reward
|
|
599
|
+
# update total reward and calculate td error
|
|
600
|
+
act = exps[0].action
|
|
601
|
+
td[idx] = q0[idx][act] - total_reward
|
|
602
|
+
q0[idx][act] = total_reward
|
|
603
|
+
|
|
604
|
+
return state_0, q0, td
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from ignite.engine import Engine, State
|
|
4
|
+
from ignite.engine import Events, EventEnum
|
|
5
|
+
from ignite.handlers.timing import Timer
|
|
6
|
+
|
|
7
|
+
from .experience import ExperienceSource
|
|
8
|
+
|
|
9
|
+
class EpisodeEvents(EventEnum):
|
|
10
|
+
EPISODE_COMPLETED = "episode_completed"
|
|
11
|
+
BOUND_REWARD_REACHED = "bound_reward_reached"
|
|
12
|
+
BEST_REWARD_REACHED = "best_reward_reached"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EndOfEpisodeHandler:
|
|
16
|
+
def __init__(self, exp_source: ExperienceSource, alpha: float = 0.98,
|
|
17
|
+
bound_avg_reward: Optional[float] = None,
|
|
18
|
+
subsample_end_of_episode: Optional[int] = None):
|
|
19
|
+
"""
|
|
20
|
+
Construct end-of-episode event handler
|
|
21
|
+
:param exp_source: experience source to use
|
|
22
|
+
:param alpha: smoothing alpha param
|
|
23
|
+
:param bound_avg_reward: optional boundary for average reward
|
|
24
|
+
:param subsample_end_of_episode: if given, end of episode event will be subsampled by this amount
|
|
25
|
+
"""
|
|
26
|
+
self._exp_source = exp_source
|
|
27
|
+
self._alpha = alpha
|
|
28
|
+
self._bound_avg_reward = bound_avg_reward
|
|
29
|
+
self._best_avg_reward = None
|
|
30
|
+
self._subsample_end_of_episode = subsample_end_of_episode
|
|
31
|
+
|
|
32
|
+
def attach(self, engine: Engine):
|
|
33
|
+
engine.add_event_handler(Events.ITERATION_COMPLETED, self)
|
|
34
|
+
engine.register_events(*EpisodeEvents)
|
|
35
|
+
State.event_to_attr[EpisodeEvents.EPISODE_COMPLETED] = "episode"
|
|
36
|
+
State.event_to_attr[EpisodeEvents.BOUND_REWARD_REACHED] = "episode"
|
|
37
|
+
State.event_to_attr[EpisodeEvents.BEST_REWARD_REACHED] = "episode"
|
|
38
|
+
|
|
39
|
+
def __call__(self, engine: Engine):
|
|
40
|
+
for reward, steps in self._exp_source.pop_rewards_steps():
|
|
41
|
+
engine.state.episode = getattr(engine.state, "episode", 0) + 1
|
|
42
|
+
engine.state.episode_reward = reward
|
|
43
|
+
engine.state.episode_steps = steps
|
|
44
|
+
engine.state.metrics['reward'] = reward
|
|
45
|
+
engine.state.metrics['steps'] = steps
|
|
46
|
+
self._update_smoothed_metrics(engine, reward, steps)
|
|
47
|
+
if self._subsample_end_of_episode is None or engine.state.episode % self._subsample_end_of_episode == 0:
|
|
48
|
+
engine.fire_event(EpisodeEvents.EPISODE_COMPLETED)
|
|
49
|
+
if self._bound_avg_reward is not None and engine.state.metrics['avg_reward'] >= self._bound_avg_reward:
|
|
50
|
+
engine.fire_event(EpisodeEvents.BOUND_REWARD_REACHED)
|
|
51
|
+
if self._best_avg_reward is None:
|
|
52
|
+
self._best_avg_reward = engine.state.metrics['avg_reward']
|
|
53
|
+
elif self._best_avg_reward < engine.state.metrics['avg_reward']:
|
|
54
|
+
engine.fire_event(EpisodeEvents.BEST_REWARD_REACHED)
|
|
55
|
+
self._best_avg_reward = engine.state.metrics['avg_reward']
|
|
56
|
+
|
|
57
|
+
def _update_smoothed_metrics(self, engine: Engine, reward: float, steps: int):
|
|
58
|
+
for attr_name, val in zip(('avg_reward', 'avg_steps'), (reward, steps)):
|
|
59
|
+
if attr_name not in engine.state.metrics:
|
|
60
|
+
engine.state.metrics[attr_name] = val
|
|
61
|
+
else:
|
|
62
|
+
engine.state.metrics[attr_name] *= self._alpha
|
|
63
|
+
engine.state.metrics[attr_name] += (1-self._alpha) * val
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EpisodeFPSHandler:
|
|
67
|
+
FPS_METRIC = 'fps'
|
|
68
|
+
AVG_FPS_METRIC = 'avg_fps'
|
|
69
|
+
TIME_PASSED_METRIC = 'time_passed'
|
|
70
|
+
|
|
71
|
+
def __init__(self, fps_mul: float = 1.0, fps_smooth_alpha: float = 0.98):
|
|
72
|
+
self._timer = Timer(average=True)
|
|
73
|
+
self._fps_mul = fps_mul
|
|
74
|
+
self._started_ts = time.time()
|
|
75
|
+
self._fps_smooth_alpha = fps_smooth_alpha
|
|
76
|
+
|
|
77
|
+
def attach(self, engine: Engine, manual_step: bool = False):
|
|
78
|
+
self._timer.attach(engine, step=None if manual_step else Events.ITERATION_COMPLETED)
|
|
79
|
+
engine.add_event_handler(EpisodeEvents.EPISODE_COMPLETED, self)
|
|
80
|
+
engine.state.metrics[self.AVG_FPS_METRIC] = 0
|
|
81
|
+
|
|
82
|
+
def step(self):
|
|
83
|
+
"""
|
|
84
|
+
If manual_step=True on attach(), this method should be used every time we've communicated with environment
|
|
85
|
+
to get proper FPS
|
|
86
|
+
:return:
|
|
87
|
+
"""
|
|
88
|
+
self._timer.step()
|
|
89
|
+
|
|
90
|
+
def __call__(self, engine: Engine):
|
|
91
|
+
t_val = self._timer.value()
|
|
92
|
+
if engine.state.iteration > 1:
|
|
93
|
+
fps = self._fps_mul / t_val
|
|
94
|
+
avg_fps = engine.state.metrics.get(self.AVG_FPS_METRIC)
|
|
95
|
+
if avg_fps is None or avg_fps <= 0:
|
|
96
|
+
avg_fps = fps
|
|
97
|
+
else:
|
|
98
|
+
avg_fps *= self._fps_smooth_alpha
|
|
99
|
+
avg_fps += (1-self._fps_smooth_alpha) * fps
|
|
100
|
+
engine.state.metrics[self.AVG_FPS_METRIC] = avg_fps
|
|
101
|
+
engine.state.metrics[self.FPS_METRIC] = fps
|
|
102
|
+
engine.state.metrics[self.TIME_PASSED_METRIC] = time.time() - self._started_ts
|
|
103
|
+
self._timer.reset()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PeriodEvents(EventEnum):
|
|
107
|
+
ITERS_10_COMPLETED = "iterations_10_completed"
|
|
108
|
+
ITERS_100_COMPLETED = "iterations_100_completed"
|
|
109
|
+
ITERS_1000_COMPLETED = "iterations_1000_completed"
|
|
110
|
+
ITERS_10000_COMPLETED = "iterations_10000_completed"
|
|
111
|
+
ITERS_100000_COMPLETED = "iterations_100000_completed"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class PeriodicEvents:
|
|
115
|
+
"""
|
|
116
|
+
The same as CustomPeriodicEvent from ignite.contrib, but use true amount of iterations,
|
|
117
|
+
which is good for TensorBoard
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
INTERVAL_TO_EVENT = {
|
|
121
|
+
10: PeriodEvents.ITERS_10_COMPLETED,
|
|
122
|
+
100: PeriodEvents.ITERS_100_COMPLETED,
|
|
123
|
+
1000: PeriodEvents.ITERS_1000_COMPLETED,
|
|
124
|
+
10000: PeriodEvents.ITERS_10000_COMPLETED,
|
|
125
|
+
100000: PeriodEvents.ITERS_100000_COMPLETED,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def attach(self, engine: Engine):
|
|
129
|
+
engine.add_event_handler(Events.ITERATION_COMPLETED, self)
|
|
130
|
+
engine.register_events(*PeriodEvents)
|
|
131
|
+
for e in PeriodEvents:
|
|
132
|
+
State.event_to_attr[e] = "iteration"
|
|
133
|
+
|
|
134
|
+
def __call__(self, engine: Engine):
|
|
135
|
+
for period, event in self.INTERVAL_TO_EVENT.items():
|
|
136
|
+
if engine.state.iteration % period == 0:
|
|
137
|
+
engine.fire_event(event)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|