├── common
├── __init__.py
├── layers.py
├── wrappers.py
├── replay_buffer.py
└── replay_buffer_dtm.py
├── requirements.txt
├── README.md
├── LICENSE
├── .gitignore
├── controller.py
├── em.py
├── dqn_mbec.py
└── dqn.ipynb
/common/__init__.py:
--------------------------------------------------------------------------------
1 | from . import layers
2 | from . import wrappers
3 | from . import replay_buffer
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | ale_py==0.7.5
2 | gym==0.26.2
3 | ipython==8.7.0
4 | matplotlib==3.6.2
5 | numpy==1.19.2
6 | opencv_python==4.6.0.66
7 | pyflann==1.6.14
8 | pyflann_py3==0.1.0
9 | tensorboard_logger==0.1.0
10 | torch==1.4.0
11 | tqdm==4.63.0
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AJCAI22-Tutorial
2 | ### Demo code for AJCAI22-Tutorial
3 | [Tutorial website](https://thaihungle.github.io/talks/2022-12-05-AJCAI)
4 | [Conference website](https://ajcai2022.org/tutorials/)
5 |
6 | Install packages following requirements.txt
7 | ```
8 | pip install -r requirements.txt
9 | ```
10 | ### DQN
11 | Follow dqn.ipynb
12 | Reference: https://github.com/higgsfield/RL-Adventure/blob/master/1.dqn.ipynb
13 |
14 |
15 | ### MBEC
16 | - Create log and model folders to save training info
17 | ```
18 | mkdir log
19 | mkdir model
20 | ```
21 | - Run the script dqn_mbec.py using
22 |
23 | ```
24 | python dqn_mbec.py --task MountainCar-v0 --rnoise 0.5 --render 0 --task2 mountaincar --n_epochs 100000 --max_episode 1000000 --model_name DTM --update_interval 100 --decay 1 --memory_size 3000 --k 15 --write_interval 10 --td_interval 1 --write_lr .5 --rec_rate .1 --rec_noise .1 --batch_size_plan 4 --rec_period 9999999999 --num_warm_up -1 --lr 0.0005
25 | ```
26 | - Monitor training using tensorboard
27 | ```
28 | cd log
29 | tensorboard --logdir=./
30 | ```
31 | Reference: https://github.com/thaihungle/MBEC-plus
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Tony
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 | log/
131 | model/
--------------------------------------------------------------------------------
/common/layers.py:
--------------------------------------------------------------------------------
1 | import math
2 | import torch
3 | import torch.nn as nn
4 | import torch.nn.functional as F
5 | from torch.autograd import Variable
6 |
7 | class NoisyLinear(nn.Module):
8 | def __init__(self, in_features, out_features, use_cuda, std_init=0.4):
9 | super(NoisyLinear, self).__init__()
10 |
11 | self.use_cuda = use_cuda
12 | self.in_features = in_features
13 | self.out_features = out_features
14 | self.std_init = std_init
15 |
16 | self.weight_mu = nn.Parameter(torch.FloatTensor(out_features, in_features))
17 | self.weight_sigma = nn.Parameter(torch.FloatTensor(out_features, in_features))
18 | self.register_buffer('weight_epsilon', torch.FloatTensor(out_features, in_features))
19 |
20 | self.bias_mu = nn.Parameter(torch.FloatTensor(out_features))
21 | self.bias_sigma = nn.Parameter(torch.FloatTensor(out_features))
22 | self.register_buffer('bias_epsilon', torch.FloatTensor(out_features))
23 |
24 | self.reset_parameters()
25 | self.reset_noise()
26 |
27 | def forward(self, x):
28 | if self.use_cuda:
29 | weight_epsilon = self.weight_epsilon.cuda()
30 | bias_epsilon = self.bias_epsilon.cuda()
31 | else:
32 | weight_epsilon = self.weight_epsilon
33 | bias_epsilon = self.bias_epsilon
34 |
35 | if self.training:
36 | weight = self.weight_mu + self.weight_sigma.mul(Variable(weight_epsilon))
37 | bias = self.bias_mu + self.bias_sigma.mul(Variable(bias_epsilon))
38 | else:
39 | weight = self.weight_mu
40 | bias = self.bias_mu
41 |
42 | return F.linear(x, weight, bias)
43 |
44 | def reset_parameters(self):
45 | mu_range = 1 / math.sqrt(self.weight_mu.size(1))
46 |
47 | self.weight_mu.data.uniform_(-mu_range, mu_range)
48 | self.weight_sigma.data.fill_(self.std_init / math.sqrt(self.weight_sigma.size(1)))
49 |
50 | self.bias_mu.data.uniform_(-mu_range, mu_range)
51 | self.bias_sigma.data.fill_(self.std_init / math.sqrt(self.bias_sigma.size(0)))
52 |
53 | def reset_noise(self):
54 | epsilon_in = self._scale_noise(self.in_features)
55 | epsilon_out = self._scale_noise(self.out_features)
56 |
57 | self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in))
58 | self.bias_epsilon.copy_(self._scale_noise(self.out_features))
59 |
60 | def _scale_noise(self, size):
61 | x = torch.randn(size)
62 | x = x.sign().mul(x.abs().sqrt())
63 | return x
--------------------------------------------------------------------------------
/controller.py:
--------------------------------------------------------------------------------
1 | """LSTM Controller."""
2 | import torch
3 | from torch import nn
4 | from torch.nn import Parameter
5 | import numpy as np
6 |
7 |
8 | class FFWController(nn.Module):
9 | """An NTM controller based on LSTM."""
10 | def __init__(self, num_inputs, num_outputs, num_layers):
11 | super(FFWController, self).__init__()
12 |
13 | self.num_inputs = num_inputs
14 | self.num_outputs = num_outputs
15 | self.num_layers = num_layers
16 |
17 |
18 |
19 | def create_new_state(self, batch_size):
20 | h = torch.zeros(batch_size, self.num_outputs)
21 | if torch.cuda.is_available():
22 | h = h.cuda()
23 | return h
24 |
25 | def reset_parameters(self):
26 | pass
27 |
28 | def size(self):
29 | return self.num_inputs, self.num_outputs
30 |
31 | def forward(self, x, prev_state):
32 | return x, prev_state
33 |
34 | class LSTMController(nn.Module):
35 | """An NTM controller based on LSTM."""
36 | def __init__(self, num_inputs, num_outputs, num_layers):
37 | super(LSTMController, self).__init__()
38 |
39 | self.num_inputs = num_inputs
40 | self.num_outputs = num_outputs
41 | self.num_layers = num_layers
42 |
43 | self.lstm = nn.LSTM(input_size=num_inputs,
44 | hidden_size=num_outputs,
45 | num_layers=num_layers)
46 | # The hidden state is a learned parameter
47 | if torch.cuda.is_available():
48 | self.lstm_h_bias = Parameter(torch.randn(self.num_layers, 1, self.num_outputs).cuda() * 0, requires_grad=False)
49 | self.lstm_c_bias = Parameter(torch.randn(self.num_layers, 1, self.num_outputs).cuda() * 0, requires_grad=False)
50 |
51 | else:
52 | self.lstm_h_bias = Parameter(torch.randn(self.num_layers, 1, self.num_outputs) * 0, requires_grad=False)
53 | self.lstm_c_bias = Parameter(torch.randn(self.num_layers, 1, self.num_outputs) * 0, requires_grad=False)
54 |
55 | self.reset_parameters()
56 |
57 | def create_new_state(self, batch_size):
58 | # Dimension: (num_layers * num_directions, batch, hidden_size)
59 | lstm_h = self.lstm_h_bias.clone().repeat(1, batch_size, 1)
60 | lstm_c = self.lstm_c_bias.clone().repeat(1, batch_size, 1)
61 | # h = torch.zeros(self.num_layers, batch_size, self.num_outputs)
62 | # c = torch.zeros(self.num_layers, batch_size, self.num_outputs)
63 | # if torch.cuda.is_available():
64 | # h = h.cuda()
65 | # c = c.cuda()
66 | # return h,c
67 |
68 |
69 | return lstm_h, lstm_c
70 |
71 | def reset_parameters(self):
72 | for p in self.lstm.parameters():
73 | if p.dim() == 1:
74 | nn.init.constant_(p, 0)
75 | else:
76 | stdev = 5 / (np.sqrt(self.num_inputs + self.num_outputs))
77 | nn.init.uniform_(p, -stdev, stdev)
78 |
79 | def size(self):
80 | return self.num_inputs, self.num_outputs
81 |
82 | def forward(self, x, prev_state):
83 | x = x.unsqueeze(0)
84 | outp, state = self.lstm(x, prev_state)
85 | return outp.squeeze(0), state
--------------------------------------------------------------------------------
/common/wrappers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from collections import deque
3 | import gym
4 | from gym import spaces
5 | import cv2
6 | cv2.ocl.setUseOpenCL(False)
7 |
8 | class NoopResetEnv(gym.Wrapper):
9 | def __init__(self, env, noop_max=30):
10 | """Sample initial states by taking random number of no-ops on reset.
11 | No-op is assumed to be action 0.
12 | """
13 | gym.Wrapper.__init__(self, env)
14 | self.noop_max = noop_max
15 | self.override_num_noops = None
16 | self.noop_action = 0
17 | assert env.unwrapped.get_action_meanings()[0] == 'NOOP'
18 |
19 | def reset(self, **kwargs):
20 | """ Do no-op action for a number of steps in [1, noop_max]."""
21 | self.env.reset(**kwargs)
22 | if self.override_num_noops is not None:
23 | noops = self.override_num_noops
24 | else:
25 | noops = self.unwrapped.np_random.integers(1, self.noop_max + 1) #pylint: disable=E1101
26 | assert noops > 0
27 | obs = None
28 | for _ in range(noops):
29 | obs, _, done, _ = self.env.step(self.noop_action)
30 | if done:
31 | obs = self.env.reset(**kwargs)
32 | return obs
33 |
34 | def step(self, ac):
35 | return self.env.step(ac)
36 |
37 | class FireResetEnv(gym.Wrapper):
38 | def __init__(self, env):
39 | """Take action on reset for environments that are fixed until firing."""
40 | gym.Wrapper.__init__(self, env)
41 | assert env.unwrapped.get_action_meanings()[1] == 'FIRE'
42 | assert len(env.unwrapped.get_action_meanings()) >= 3
43 |
44 | def reset(self, **kwargs):
45 | self.env.reset(**kwargs)
46 | obs, _, done, _ = self.env.step(1)
47 | if done:
48 | self.env.reset(**kwargs)
49 | obs, _, done, _ = self.env.step(2)
50 | if done:
51 | self.env.reset(**kwargs)
52 | return obs
53 |
54 | def step(self, ac):
55 | return self.env.step(ac)
56 |
57 | class EpisodicLifeEnv(gym.Wrapper):
58 | def __init__(self, env):
59 | """Make end-of-life == end-of-episode, but only reset on true game over.
60 | Done by DeepMind for the DQN and co. since it helps value estimation.
61 | """
62 | gym.Wrapper.__init__(self, env)
63 | self.lives = 0
64 | self.was_real_done = True
65 |
66 | def step(self, action):
67 | obs, reward, done, info = self.env.step(action)
68 | self.was_real_done = done
69 | # check current lives, make loss of life terminal,
70 | # then update lives to handle bonus lives
71 | lives = self.env.unwrapped.ale.lives()
72 | if lives < self.lives and lives > 0:
73 | # for Qbert sometimes we stay in lives == 0 condtion for a few frames
74 | # so its important to keep lives > 0, so that we only reset once
75 | # the environment advertises done.
76 | done = True
77 | self.lives = lives
78 | return obs, reward, done, info
79 |
80 | def reset(self, **kwargs):
81 | """Reset only when lives are exhausted.
82 | This way all states are still reachable even though lives are episodic,
83 | and the learner need not know about any of this behind-the-scenes.
84 | """
85 | if self.was_real_done:
86 | obs = self.env.reset(**kwargs)
87 | else:
88 | # no-op step to advance from terminal/lost life state
89 | obs, _, _, _ = self.env.step(0)
90 | self.lives = self.env.unwrapped.ale.lives()
91 | return obs
92 |
93 | class MaxAndSkipEnv(gym.Wrapper):
94 | def __init__(self, env, skip=4):
95 | """Return only every `skip`-th frame"""
96 | gym.Wrapper.__init__(self, env)
97 | # most recent raw observations (for max pooling across time steps)
98 | self._obs_buffer = np.zeros((2,)+env.observation_space.shape, dtype=np.uint8)
99 | self._skip = skip
100 |
101 | def reset(self):
102 | return self.env.reset()
103 |
104 | def step(self, action):
105 | """Repeat action, sum reward, and max over last observations."""
106 | total_reward = 0.0
107 | done = None
108 | for i in range(self._skip):
109 | obs, reward, done, info = self.env.step(action)
110 | if i == self._skip - 2: self._obs_buffer[0] = obs
111 | if i == self._skip - 1: self._obs_buffer[1] = obs
112 | total_reward += reward
113 | if done:
114 | break
115 | # Note that the observation on the done=True frame
116 | # doesn't matter
117 | max_frame = self._obs_buffer.max(axis=0)
118 |
119 | return max_frame, total_reward, done, info
120 |
121 | def reset(self, **kwargs):
122 | return self.env.reset(**kwargs)
123 |
124 | class ClipRewardEnv(gym.RewardWrapper):
125 | def __init__(self, env):
126 | gym.RewardWrapper.__init__(self, env)
127 |
128 | def reward(self, reward):
129 | """Bin reward to {+1, 0, -1} by its sign."""
130 | return np.sign(reward)
131 |
132 | class WarpFrame(gym.ObservationWrapper):
133 | def __init__(self, env):
134 | """Warp frames to 84x84 as done in the Nature paper and later work."""
135 | gym.ObservationWrapper.__init__(self, env)
136 | self.width = 84
137 | self.height = 84
138 | self.observation_space = spaces.Box(low=0, high=255,
139 | shape=(self.height, self.width, 1), dtype=np.uint8)
140 |
141 | def observation(self, frame):
142 | frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
143 | frame = cv2.resize(frame, (self.width, self.height), interpolation=cv2.INTER_AREA)
144 | return frame[:, :, None]
145 |
146 | class FrameStack(gym.Wrapper):
147 | def __init__(self, env, k):
148 | """Stack k last frames.
149 | Returns lazy array, which is much more memory efficient.
150 | See Also
151 | --------
152 | baselines.common.atari_wrappers.LazyFrames
153 | """
154 | gym.Wrapper.__init__(self, env)
155 | self.k = k
156 | self.frames = deque([], maxlen=k)
157 | shp = env.observation_space.shape
158 | self.observation_space = spaces.Box(low=0, high=255, shape=(shp[0], shp[1], shp[2] * k), dtype=np.uint8)
159 |
160 | def reset(self):
161 | ob = self.env.reset()
162 | for _ in range(self.k):
163 | self.frames.append(ob)
164 | return self._get_ob()
165 |
166 | def step(self, action):
167 | ob, reward, done, info = self.env.step(action)
168 | self.frames.append(ob)
169 | return self._get_ob(), reward, done, info
170 |
171 | def _get_ob(self):
172 | assert len(self.frames) == self.k
173 | return LazyFrames(list(self.frames))
174 |
175 | class ScaledFloatFrame(gym.ObservationWrapper):
176 | def __init__(self, env):
177 | gym.ObservationWrapper.__init__(self, env)
178 |
179 | def observation(self, observation):
180 | # careful! This undoes the memory optimization, use
181 | # with smaller replay buffers only.
182 | return np.array(observation).astype(np.float32) / 255.0
183 |
184 | class LazyFrames(object):
185 | def __init__(self, frames):
186 | """This object ensures that common frames between the observations are only stored once.
187 | It exists purely to optimize memory usage which can be huge for DQN's 1M frames replay
188 | buffers.
189 | This object should only be converted to numpy array before being passed to the model.
190 | You'd not believe how complex the previous solution was."""
191 | self._frames = frames
192 | self._out = None
193 |
194 | def _force(self):
195 | if self._out is None:
196 | self._out = np.concatenate(self._frames, axis=2)
197 | self._frames = None
198 | return self._out
199 |
200 | def __array__(self, dtype=None):
201 | out = self._force()
202 | if dtype is not None:
203 | out = out.astype(dtype)
204 | return out
205 |
206 | def __len__(self):
207 | return len(self._force())
208 |
209 | def __getitem__(self, i):
210 | return self._force()[i]
211 |
212 | def make_atari(env_id):
213 | env = gym.make(env_id)
214 | assert 'NoFrameskip' in env.spec.id
215 | env = NoopResetEnv(env, noop_max=30)
216 | env = MaxAndSkipEnv(env, skip=4)
217 | return env
218 |
219 | def wrap_deepmind(env, episode_life=True, clip_rewards=True, frame_stack=False, scale=False):
220 | """Configure environment for DeepMind-style Atari.
221 | """
222 | if episode_life:
223 | env = EpisodicLifeEnv(env)
224 | if 'FIRE' in env.unwrapped.get_action_meanings():
225 | env = FireResetEnv(env)
226 | env = WarpFrame(env)
227 | if scale:
228 | env = ScaledFloatFrame(env)
229 | if clip_rewards:
230 | env = ClipRewardEnv(env)
231 | if frame_stack:
232 | env = FrameStack(env, 4)
233 | return env
234 |
235 |
236 |
237 | class ImageToPyTorch(gym.ObservationWrapper):
238 | """
239 | Image shape to num_channels x weight x height
240 | """
241 | def __init__(self, env):
242 | super(ImageToPyTorch, self).__init__(env)
243 | old_shape = self.observation_space.shape
244 | self.observation_space = gym.spaces.Box(low=0.0, high=1.0, shape=(old_shape[-1], old_shape[0], old_shape[1]), dtype=np.uint8)
245 |
246 | def observation(self, observation):
247 | return np.swapaxes(observation, 2, 0)
248 |
249 |
250 | def wrap_pytorch(env):
251 | return ImageToPyTorch(env)
--------------------------------------------------------------------------------
/common/replay_buffer.py:
--------------------------------------------------------------------------------
1 | #code from openai
2 | #https://github.com/openai/baselines/blob/master/baselines/deepq/replay_buffer.py
3 |
4 | import numpy as np
5 | import random
6 |
7 | import operator
8 |
9 |
10 | class SegmentTree(object):
11 | def __init__(self, capacity, operation, neutral_element):
12 | """Build a Segment Tree data structure.
13 | https://en.wikipedia.org/wiki/Segment_tree
14 | Can be used as regular array, but with two
15 | important differences:
16 | a) setting item's value is slightly slower.
17 | It is O(lg capacity) instead of O(1).
18 | b) user has access to an efficient `reduce`
19 | operation which reduces `operation` over
20 | a contiguous subsequence of items in the
21 | array.
22 | Paramters
23 | ---------
24 | capacity: int
25 | Total size of the array - must be a power of two.
26 | operation: lambda obj, obj -> obj
27 | and operation for combining elements (eg. sum, max)
28 | must for a mathematical group together with the set of
29 | possible values for array elements.
30 | neutral_element: obj
31 | neutral element for the operation above. eg. float('-inf')
32 | for max and 0 for sum.
33 | """
34 | assert capacity > 0 and capacity & (capacity - 1) == 0, "capacity must be positive and a power of 2."
35 | self._capacity = capacity
36 | self._value = [neutral_element for _ in range(2 * capacity)]
37 | self._operation = operation
38 |
39 | def _reduce_helper(self, start, end, node, node_start, node_end):
40 | if start == node_start and end == node_end:
41 | return self._value[node]
42 | mid = (node_start + node_end) // 2
43 | if end <= mid:
44 | return self._reduce_helper(start, end, 2 * node, node_start, mid)
45 | else:
46 | if mid + 1 <= start:
47 | return self._reduce_helper(start, end, 2 * node + 1, mid + 1, node_end)
48 | else:
49 | return self._operation(
50 | self._reduce_helper(start, mid, 2 * node, node_start, mid),
51 | self._reduce_helper(mid + 1, end, 2 * node + 1, mid + 1, node_end)
52 | )
53 |
54 | def reduce(self, start=0, end=None):
55 | """Returns result of applying `self.operation`
56 | to a contiguous subsequence of the array.
57 | self.operation(arr[start], operation(arr[start+1], operation(... arr[end])))
58 | Parameters
59 | ----------
60 | start: int
61 | beginning of the subsequence
62 | end: int
63 | end of the subsequences
64 | Returns
65 | -------
66 | reduced: obj
67 | result of reducing self.operation over the specified range of array elements.
68 | """
69 | if end is None:
70 | end = self._capacity
71 | if end < 0:
72 | end += self._capacity
73 | end -= 1
74 | return self._reduce_helper(start, end, 1, 0, self._capacity - 1)
75 |
76 | def __setitem__(self, idx, val):
77 | # index of the leaf
78 | idx += self._capacity
79 | self._value[idx] = val
80 | idx //= 2
81 | while idx >= 1:
82 | self._value[idx] = self._operation(
83 | self._value[2 * idx],
84 | self._value[2 * idx + 1]
85 | )
86 | idx //= 2
87 |
88 | def __getitem__(self, idx):
89 | assert 0 <= idx < self._capacity
90 | return self._value[self._capacity + idx]
91 |
92 |
93 | class SumSegmentTree(SegmentTree):
94 | def __init__(self, capacity):
95 | super(SumSegmentTree, self).__init__(
96 | capacity=capacity,
97 | operation=operator.add,
98 | neutral_element=0.0
99 | )
100 |
101 | def sum(self, start=0, end=None):
102 | """Returns arr[start] + ... + arr[end]"""
103 | return super(SumSegmentTree, self).reduce(start, end)
104 |
105 | def find_prefixsum_idx(self, prefixsum):
106 | """Find the highest index `i` in the array such that
107 | sum(arr[0] + arr[1] + ... + arr[i - i]) <= prefixsum
108 | if array values are probabilities, this function
109 | allows to sample indexes according to the discrete
110 | probability efficiently.
111 | Parameters
112 | ----------
113 | perfixsum: float
114 | upperbound on the sum of array prefix
115 | Returns
116 | -------
117 | idx: int
118 | highest index satisfying the prefixsum constraint
119 | """
120 | assert 0 <= prefixsum <= self.sum() + 1e-5
121 | idx = 1
122 | while idx < self._capacity: # while non-leaf
123 | if self._value[2 * idx] > prefixsum:
124 | idx = 2 * idx
125 | else:
126 | prefixsum -= self._value[2 * idx]
127 | idx = 2 * idx + 1
128 | return idx - self._capacity
129 |
130 |
131 | class MinSegmentTree(SegmentTree):
132 | def __init__(self, capacity):
133 | super(MinSegmentTree, self).__init__(
134 | capacity=capacity,
135 | operation=min,
136 | neutral_element=float('inf')
137 | )
138 |
139 | def min(self, start=0, end=None):
140 | """Returns min(arr[start], ..., arr[end])"""
141 |
142 | return super(MinSegmentTree, self).reduce(start, end)
143 |
144 |
145 | class ReplayBuffer(object):
146 | def __init__(self, size):
147 | """Create Replay buffer.
148 | Parameters
149 | ----------
150 | size: int
151 | Max number of transitions to store in the buffer. When the buffer
152 | overflows the old memories are dropped.
153 | """
154 | self._storage = []
155 | self._maxsize = size
156 | self._next_idx = 0
157 |
158 | def __len__(self):
159 | return len(self._storage)
160 |
161 | def push(self, state, action, reward, next_state, done):
162 | data = (state, action, reward, next_state, done)
163 |
164 | if self._next_idx >= len(self._storage):
165 | self._storage.append(data)
166 | else:
167 | self._storage[self._next_idx] = data
168 | self._next_idx = (self._next_idx + 1) % self._maxsize
169 |
170 | def _encode_sample(self, idxes):
171 | obses_t, actions, rewards, obses_tp1, dones = [], [], [], [], []
172 | for i in idxes:
173 | data = self._storage[i]
174 | obs_t, action, reward, obs_tp1, done = data
175 | obses_t.append(np.array(obs_t, copy=False))
176 | actions.append(np.array(action, copy=False))
177 | rewards.append(reward)
178 | obses_tp1.append(np.array(obs_tp1, copy=False))
179 | dones.append(done)
180 | return np.array(obses_t), np.array(actions), np.array(rewards), np.array(obses_tp1), np.array(dones)
181 |
182 | def sample(self, batch_size):
183 | """Sample a batch of experiences.
184 | Parameters
185 | ----------
186 | batch_size: int
187 | How many transitions to sample.
188 | Returns
189 | -------
190 | obs_batch: np.array
191 | batch of observations
192 | act_batch: np.array
193 | batch of actions executed given obs_batch
194 | rew_batch: np.array
195 | rewards received as results of executing act_batch
196 | next_obs_batch: np.array
197 | next set of observations seen after executing act_batch
198 | done_mask: np.array
199 | done_mask[i] = 1 if executing act_batch[i] resulted in
200 | the end of an episode and 0 otherwise.
201 | """
202 | idxes = [random.randint(0, len(self._storage) - 1) for _ in range(batch_size)]
203 | return self._encode_sample(idxes)
204 |
205 |
206 | class PrioritizedReplayBuffer(ReplayBuffer):
207 | def __init__(self, size, alpha):
208 | """Create Prioritized Replay buffer.
209 | Parameters
210 | ----------
211 | size: int
212 | Max number of transitions to store in the buffer. When the buffer
213 | overflows the old memories are dropped.
214 | alpha: float
215 | how much prioritization is used
216 | (0 - no prioritization, 1 - full prioritization)
217 | See Also
218 | --------
219 | ReplayBuffer.__init__
220 | """
221 | super(PrioritizedReplayBuffer, self).__init__(size)
222 | assert alpha > 0
223 | self._alpha = alpha
224 |
225 | it_capacity = 1
226 | while it_capacity < size:
227 | it_capacity *= 2
228 |
229 | self._it_sum = SumSegmentTree(it_capacity)
230 | self._it_min = MinSegmentTree(it_capacity)
231 | self._max_priority = 1.0
232 |
233 | def push(self, *args, **kwargs):
234 | """See ReplayBuffer.store_effect"""
235 | idx = self._next_idx
236 | super(PrioritizedReplayBuffer, self).push(*args, **kwargs)
237 | self._it_sum[idx] = self._max_priority ** self._alpha
238 | self._it_min[idx] = self._max_priority ** self._alpha
239 |
240 | def _sample_proportional(self, batch_size):
241 | res = []
242 | for _ in range(batch_size):
243 | # TODO(szymon): should we ensure no repeats?
244 | mass = random.random() * self._it_sum.sum(0, len(self._storage) - 1)
245 | idx = self._it_sum.find_prefixsum_idx(mass)
246 | res.append(idx)
247 | return res
248 |
249 | def sample(self, batch_size, beta):
250 | """Sample a batch of experiences.
251 | compared to ReplayBuffer.sample
252 | it also returns importance weights and idxes
253 | of sampled experiences.
254 | Parameters
255 | ----------
256 | batch_size: int
257 | How many transitions to sample.
258 | beta: float
259 | To what degree to use importance weights
260 | (0 - no corrections, 1 - full correction)
261 | Returns
262 | -------
263 | obs_batch: np.array
264 | batch of observations
265 | act_batch: np.array
266 | batch of actions executed given obs_batch
267 | rew_batch: np.array
268 | rewards received as results of executing act_batch
269 | next_obs_batch: np.array
270 | next set of observations seen after executing act_batch
271 | done_mask: np.array
272 | done_mask[i] = 1 if executing act_batch[i] resulted in
273 | the end of an episode and 0 otherwise.
274 | weights: np.array
275 | Array of shape (batch_size,) and dtype np.float32
276 | denoting importance weight of each sampled transition
277 | idxes: np.array
278 | Array of shape (batch_size,) and dtype np.int32
279 | idexes in buffer of sampled experiences
280 | """
281 | assert beta > 0
282 |
283 | idxes = self._sample_proportional(batch_size)
284 |
285 | weights = []
286 | p_min = self._it_min.min() / self._it_sum.sum()
287 | max_weight = (p_min * len(self._storage)) ** (-beta)
288 |
289 | for idx in idxes:
290 | p_sample = self._it_sum[idx] / self._it_sum.sum()
291 | weight = (p_sample * len(self._storage)) ** (-beta)
292 | weights.append(weight / max_weight)
293 | weights = np.array(weights)
294 | encoded_sample = self._encode_sample(idxes)
295 | return tuple(list(encoded_sample) + [weights, idxes])
296 |
297 | def update_priorities(self, idxes, priorities):
298 | """Update priorities of sampled transitions.
299 | sets priority of transition at index idxes[i] in buffer
300 | to priorities[i].
301 | Parameters
302 | ----------
303 | idxes: [int]
304 | List of idxes of sampled transitions
305 | priorities: [float]
306 | List of updated priorities corresponding to
307 | transitions at the sampled idxes denoted by
308 | variable `idxes`.
309 | """
310 | assert len(idxes) == len(priorities)
311 | for idx, priority in zip(idxes, priorities):
312 | assert priority > 0
313 | assert 0 <= idx < len(self._storage)
314 | self._it_sum[idx] = priority ** self._alpha
315 | self._it_min[idx] = priority ** self._alpha
316 |
317 | self._max_priority = max(self._max_priority, priority)
--------------------------------------------------------------------------------
/common/replay_buffer_dtm.py:
--------------------------------------------------------------------------------
1 | #code from openai
2 | #https://github.com/openai/baselines/blob/master/baselines/deepq/replay_buffer.py
3 |
4 | import numpy as np
5 | import random
6 |
7 | import operator
8 |
9 |
10 | class SegmentTree(object):
11 | def __init__(self, capacity, operation, neutral_element):
12 | """Build a Segment Tree data structure.
13 | https://en.wikipedia.org/wiki/Segment_tree
14 | Can be used as regular array, but with two
15 | important differences:
16 | a) setting item's value is slightly slower.
17 | It is O(lg capacity) instead of O(1).
18 | b) user has access to an efficient `reduce`
19 | operation which reduces `operation` over
20 | a contiguous subsequence of items in the
21 | array.
22 | Paramters
23 | ---------
24 | capacity: int
25 | Total size of the array - must be a power of two.
26 | operation: lambda obj, obj -> obj
27 | and operation for combining elements (eg. sum, max)
28 | must for a mathematical group together with the set of
29 | possible values for array elements.
30 | neutral_element: obj
31 | neutral element for the operation above. eg. float('-inf')
32 | for max and 0 for sum.
33 | """
34 | assert capacity > 0 and capacity & (capacity - 1) == 0, "capacity must be positive and a power of 2."
35 | self._capacity = capacity
36 | self._value = [neutral_element for _ in range(2 * capacity)]
37 | self._operation = operation
38 |
39 | def _reduce_helper(self, start, end, node, node_start, node_end):
40 | if start == node_start and end == node_end:
41 | return self._value[node]
42 | mid = (node_start + node_end) // 2
43 | if end <= mid:
44 | return self._reduce_helper(start, end, 2 * node, node_start, mid)
45 | else:
46 | if mid + 1 <= start:
47 | return self._reduce_helper(start, end, 2 * node + 1, mid + 1, node_end)
48 | else:
49 | return self._operation(
50 | self._reduce_helper(start, mid, 2 * node, node_start, mid),
51 | self._reduce_helper(mid + 1, end, 2 * node + 1, mid + 1, node_end)
52 | )
53 |
54 | def reduce(self, start=0, end=None):
55 | """Returns result of applying `self.operation`
56 | to a contiguous subsequence of the array.
57 | self.operation(arr[start], operation(arr[start+1], operation(... arr[end])))
58 | Parameters
59 | ----------
60 | start: int
61 | beginning of the subsequence
62 | end: int
63 | end of the subsequences
64 | Returns
65 | -------
66 | reduced: obj
67 | result of reducing self.operation over the specified range of array elements.
68 | """
69 | if end is None:
70 | end = self._capacity
71 | if end < 0:
72 | end += self._capacity
73 | end -= 1
74 | return self._reduce_helper(start, end, 1, 0, self._capacity - 1)
75 |
76 | def __setitem__(self, idx, val):
77 | # index of the leaf
78 | idx += self._capacity
79 | self._value[idx] = val
80 | idx //= 2
81 | while idx >= 1:
82 | self._value[idx] = self._operation(
83 | self._value[2 * idx],
84 | self._value[2 * idx + 1]
85 | )
86 | idx //= 2
87 |
88 | def __getitem__(self, idx):
89 | assert 0 <= idx < self._capacity
90 | return self._value[self._capacity + idx]
91 |
92 |
93 | class SumSegmentTree(SegmentTree):
94 | def __init__(self, capacity):
95 | super(SumSegmentTree, self).__init__(
96 | capacity=capacity,
97 | operation=operator.add,
98 | neutral_element=0.0
99 | )
100 |
101 | def sum(self, start=0, end=None):
102 | """Returns arr[start] + ... + arr[end]"""
103 | return super(SumSegmentTree, self).reduce(start, end)
104 |
105 | def find_prefixsum_idx(self, prefixsum):
106 | """Find the highest index `i` in the array such that
107 | sum(arr[0] + arr[1] + ... + arr[i - i]) <= prefixsum
108 | if array values are probabilities, this function
109 | allows to sample indexes according to the discrete
110 | probability efficiently.
111 | Parameters
112 | ----------
113 | perfixsum: float
114 | upperbound on the sum of array prefix
115 | Returns
116 | -------
117 | idx: int
118 | highest index satisfying the prefixsum constraint
119 | """
120 | assert 0 <= prefixsum <= self.sum() + 1e-5
121 | idx = 1
122 | while idx < self._capacity: # while non-leaf
123 | if self._value[2 * idx] > prefixsum:
124 | idx = 2 * idx
125 | else:
126 | prefixsum -= self._value[2 * idx]
127 | idx = 2 * idx + 1
128 | return idx - self._capacity
129 |
130 |
131 | class MinSegmentTree(SegmentTree):
132 | def __init__(self, capacity):
133 | super(MinSegmentTree, self).__init__(
134 | capacity=capacity,
135 | operation=min,
136 | neutral_element=float('inf')
137 | )
138 |
139 | def min(self, start=0, end=None):
140 | """Returns min(arr[start], ..., arr[end])"""
141 |
142 | return super(MinSegmentTree, self).reduce(start, end)
143 |
144 |
145 | class ReplayBuffer(object):
146 | def __init__(self, size):
147 | """Create Replay buffer.
148 | Parameters
149 | ----------
150 | size: int
151 | Max number of transitions to store in the buffer. When the buffer
152 | overflows the old memories are dropped.
153 | """
154 | self._storage = []
155 | self._maxsize = size
156 | self._next_idx = 0
157 |
158 | def __len__(self):
159 | return len(self._storage)
160 |
161 |
162 | def push(self, state, h_trj, action, reward, next_state, nh_trj, done):
163 | data = (state, h_trj, action, reward, next_state, nh_trj, done)
164 |
165 | if self._next_idx >= len(self._storage):
166 | self._storage.append(data)
167 | else:
168 | self._storage[self._next_idx] = data
169 | self._next_idx = (self._next_idx + 1) % self._maxsize
170 |
171 | def _encode_sample(self, idxes):
172 | states, h_trjs, actions, rewards, next_states, nh_trjs, dones = [], [], [], [], [], [], []
173 | for i in idxes:
174 | data = self._storage[i]
175 | state, h_trj, action, reward, next_state, nh_trj, done = data
176 | states.append(np.array(state, copy=False))
177 | h_trjs.append(np.array(h_trj, copy=False))
178 | actions.append(np.array(action, copy=False))
179 | rewards.append(reward)
180 | next_states.append(np.array(next_state, copy=False))
181 | nh_trjs.append(np.array(nh_trj, copy=False))
182 | dones.append(done)
183 |
184 | return np.array(states), np.array(h_trjs), np.array(actions), np.array(rewards), \
185 | np.array(next_states), np.array(nh_trjs), np.array(dones)
186 |
187 | def sample(self, batch_size):
188 | """Sample a batch of experiences.
189 | Parameters
190 | ----------
191 | batch_size: int
192 | How many transitions to sample.
193 | Returns
194 | -------
195 | obs_batch: np.array
196 | batch of observations
197 | act_batch: np.array
198 | batch of actions executed given obs_batch
199 | rew_batch: np.array
200 | rewards received as results of executing act_batch
201 | next_obs_batch: np.array
202 | next set of observations seen after executing act_batch
203 | done_mask: np.array
204 | done_mask[i] = 1 if executing act_batch[i] resulted in
205 | the end of an episode and 0 otherwise.
206 | """
207 | idxes = [random.randint(0, len(self._storage) - 1) for _ in range(batch_size)]
208 | return self._encode_sample(idxes)
209 |
210 |
211 | class PrioritizedReplayBuffer(ReplayBuffer):
212 | def __init__(self, size, alpha):
213 | """Create Prioritized Replay buffer.
214 | Parameters
215 | ----------
216 | size: int
217 | Max number of transitions to store in the buffer. When the buffer
218 | overflows the old memories are dropped.
219 | alpha: float
220 | how much prioritization is used
221 | (0 - no prioritization, 1 - full prioritization)
222 | See Also
223 | --------
224 | ReplayBuffer.__init__
225 | """
226 | super(PrioritizedReplayBuffer, self).__init__(size)
227 | assert alpha > 0
228 | self._alpha = alpha
229 |
230 | it_capacity = 1
231 | while it_capacity < size:
232 | it_capacity *= 2
233 |
234 | self._it_sum = SumSegmentTree(it_capacity)
235 | self._it_min = MinSegmentTree(it_capacity)
236 | self._max_priority = 1.0
237 |
238 | def push(self, *args, **kwargs):
239 | """See ReplayBuffer.store_effect"""
240 | idx = self._next_idx
241 | super(PrioritizedReplayBuffer, self).push(*args, **kwargs)
242 | self._it_sum[idx] = self._max_priority ** self._alpha
243 | self._it_min[idx] = self._max_priority ** self._alpha
244 |
245 | def _sample_proportional(self, batch_size):
246 | res = []
247 | for _ in range(batch_size):
248 | # TODO(szymon): should we ensure no repeats?
249 | mass = random.random() * self._it_sum.sum(0, len(self._storage) - 1)
250 | idx = self._it_sum.find_prefixsum_idx(mass)
251 | res.append(idx)
252 | return res
253 |
254 | def sample(self, batch_size, beta):
255 | """Sample a batch of experiences.
256 | compared to ReplayBuffer.sample
257 | it also returns importance weights and idxes
258 | of sampled experiences.
259 | Parameters
260 | ----------
261 | batch_size: int
262 | How many transitions to sample.
263 | beta: float
264 | To what degree to use importance weights
265 | (0 - no corrections, 1 - full correction)
266 | Returns
267 | -------
268 | obs_batch: np.array
269 | batch of observations
270 | act_batch: np.array
271 | batch of actions executed given obs_batch
272 | rew_batch: np.array
273 | rewards received as results of executing act_batch
274 | next_obs_batch: np.array
275 | next set of observations seen after executing act_batch
276 | done_mask: np.array
277 | done_mask[i] = 1 if executing act_batch[i] resulted in
278 | the end of an episode and 0 otherwise.
279 | weights: np.array
280 | Array of shape (batch_size,) and dtype np.float32
281 | denoting importance weight of each sampled transition
282 | idxes: np.array
283 | Array of shape (batch_size,) and dtype np.int32
284 | idexes in buffer of sampled experiences
285 | """
286 | assert beta > 0
287 |
288 | idxes = self._sample_proportional(batch_size)
289 |
290 | weights = []
291 | p_min = self._it_min.min() / self._it_sum.sum()
292 | max_weight = (p_min * len(self._storage)) ** (-beta)
293 |
294 | for idx in idxes:
295 | p_sample = self._it_sum[idx] / self._it_sum.sum()
296 | weight = (p_sample * len(self._storage)) ** (-beta)
297 | weights.append(weight / max_weight)
298 | weights = np.array(weights)
299 | encoded_sample = self._encode_sample(idxes)
300 | return tuple(list(encoded_sample) + [weights, idxes])
301 |
302 | def update_priorities(self, idxes, priorities):
303 | """Update priorities of sampled transitions.
304 | sets priority of transition at index idxes[i] in buffer
305 | to priorities[i].
306 | Parameters
307 | ----------
308 | idxes: [int]
309 | List of idxes of sampled transitions
310 | priorities: [float]
311 | List of updated priorities corresponding to
312 | transitions at the sampled idxes denoted by
313 | variable `idxes`.
314 | """
315 | assert len(idxes) == len(priorities)
316 | for idx, priority in zip(idxes, priorities):
317 | assert priority > 0
318 | assert 0 <= idx < len(self._storage)
319 | self._it_sum[idx] = priority ** self._alpha
320 | self._it_min[idx] = priority ** self._alpha
321 |
322 | self._max_priority = max(self._max_priority, priority)
--------------------------------------------------------------------------------
/em.py:
--------------------------------------------------------------------------------
1 | import torch
2 | import torch.optim as optim
3 | from torch.nn import Parameter
4 | import pyflann
5 | import numpy as np
6 | from torch.autograd import Variable
7 | import random
8 |
9 | def inverse_distance(d, epsilon=1e-3):
10 | return 1 / (d + epsilon)
11 |
12 | class DND:
13 | def __init__(self, kernel, num_neighbors, max_memory, lr):
14 | self.kernel = kernel
15 | self.num_neighbors = num_neighbors
16 | self.max_memory = max_memory
17 | self.lr = lr
18 | self.keys = None
19 | self.values = None
20 | pyflann.set_distance_type('euclidean') # squared euclidean actually
21 | self.kdtree = pyflann.FLANN()
22 | # key_cache stores a cache of all keys that exist in the DND
23 | # This makes DND updates efficient
24 | self.key_cache = {}
25 | # stale_index is a flag that indicates whether or not the index in self.kdtree is stale
26 | # This allows us to only rebuild the kdtree index when necessary
27 | self.stale_index = True
28 | # indexes_to_be_updated is the set of indexes to be updated on a call to update_params
29 | # This allows us to rebuild only the keys of key_cache that need to be rebuilt when necessary
30 | self.indexes_to_be_updated = set()
31 |
32 | # Keys and value to be inserted into self.keys and self.values when commit_insert is called
33 | self.keys_to_be_inserted = None
34 | self.values_to_be_inserted = None
35 |
36 | # Move recently used lookup indexes
37 | # These should be moved to the back of self.keys and self.values to get LRU property
38 | self.move_to_back = set()
39 |
40 | def get_mem_size(self):
41 | if self.keys is not None:
42 | return len(self.keys)
43 | return 0
44 |
45 | def get_index(self, key):
46 | """
47 | If key exists in the DND, return its index
48 | Otherwise, return None
49 | """
50 | # print(key.data.cpu().numpy().shape)
51 | if self.key_cache.get(tuple(key.data.cpu().numpy()[0])) is not None:
52 | if self.stale_index:
53 | self.commit_insert()
54 | return int(self.kdtree.nn_index(key.data.cpu().numpy(), 1)[0][0])
55 | else:
56 | return None
57 |
58 | def update(self, value, index):
59 | """
60 | Set self.values[index] = value
61 | """
62 | values = self.values.data
63 | values[index] = value[0].data
64 | self.values = Parameter(values)
65 | # self.optimizer = optim.RMSprop([self.keys, self.values], lr=self.lr)
66 |
67 | def insert(self, key, value):
68 | """
69 | Insert key, value pair into DND
70 | """
71 |
72 | if torch.cuda.is_available():
73 | if self.keys_to_be_inserted is None:
74 | # Initial insert
75 | self.keys_to_be_inserted = Variable(key.data.cuda(), requires_grad=True)
76 | self.values_to_be_inserted = value.data.cuda()
77 | else:
78 | self.keys_to_be_inserted = torch.cat(
79 | [self.keys_to_be_inserted.cuda(), Variable(key.data.cuda(), requires_grad=True)], 0)
80 | self.values_to_be_inserted = torch.cat(
81 | [self.values_to_be_inserted.cuda(), value.data.cuda()], 0)
82 | else:
83 | if self.keys_to_be_inserted is None:
84 | # Initial insert
85 | self.keys_to_be_inserted = Variable(key.data, requires_grad=True)
86 | self.values_to_be_inserted = value.data
87 | else:
88 | self.keys_to_be_inserted = torch.cat(
89 | [self.keys_to_be_inserted,Variable(key.data, requires_grad=True)], 0)
90 | self.values_to_be_inserted = torch.cat(
91 | [self.values_to_be_inserted, value.data], 0)
92 | self.key_cache[tuple(key.data.cpu().numpy()[0])] = 0
93 | self.stale_index = True
94 |
95 | def commit_insert(self):
96 | if self.keys is None:
97 | self.keys = self.keys_to_be_inserted
98 | self.values = self.values_to_be_inserted
99 | elif self.keys_to_be_inserted is not None:
100 | self.keys = torch.cat([self.keys.data, self.keys_to_be_inserted], 0)
101 | self.values = torch.cat([self.values.data, self.values_to_be_inserted], 0)
102 | # Move most recently used key-value pairs to the back
103 | if len(self.move_to_back) != 0:
104 | self.keys = torch.cat([self.keys.data[list(set(range(len(
105 | self.keys))) - self.move_to_back)], self.keys.data[list(self.move_to_back)]], 0)
106 | self.values = torch.cat([self.values.data[list(set(range(len(
107 | self.values))) - self.move_to_back)], self.values.data[list(self.move_to_back)]], 0)
108 | self.move_to_back = set()
109 |
110 | if len(self.keys) > self.max_memory:
111 | # Expel oldest key to maintain total memory
112 | for key in self.keys[:-self.max_memory]:
113 | del self.key_cache[tuple(key.data.cpu().numpy())]
114 | self.keys = self.keys[-self.max_memory:].data
115 | self.values = self.values[-self.max_memory:].data
116 | self.keys_to_be_inserted = None
117 | self.values_to_be_inserted = None
118 | # self.optimizer = optim.RMSprop([self.keys, self.values], lr=self.lr)
119 | self.kdtree.build_index(self.keys.data.cpu().numpy(), algorithm='kdtree')
120 | self.stale_index = False
121 |
122 |
123 | # def lookup_batch(self, lookup_key, update_flag=False):
124 | # """
125 | # Perform DND lookup
126 | # If update_flag == True, add the nearest neighbor indexes to self.indexes_to_be_updated
127 | # """
128 | # lookup_indexesb, dists = self.kdtree.nn_index(
129 | # lookup_key.data.cpu().numpy(), min(self.num_neighbors, len(self.keys)))
130 | #
131 | # self.values[lookup_indexesb]
132 | # outs = []
133 | # for b, lookup_indexes in enumerate(lookup_indexesb):
134 | # output = 0
135 | # kernel_sum = 0
136 | # for i, index in enumerate(lookup_indexes):
137 | # if i == 0 and self.key_cache.get(tuple(lookup_key[0].data.cpu().numpy())) is not None:
138 | # # If a key exactly equal to lookup_key is used in the DND lookup calculation
139 | # # then the loss becomes non-differentiable. Just skip this case to avoid the issue.
140 | # continue
141 | # if update_flag:
142 | # self.indexes_to_be_updated.add(int(index))
143 | # else:
144 | # self.move_to_back.add(int(index))
145 | # kernel_val = self.kernel(self.keys[int(index)], lookup_key[b])
146 | # output += kernel_val * self.values[int(index)]
147 | # kernel_sum += kernel_val
148 | # output = output / kernel_sum
149 | # outs.append(output)
150 | # return torch.stack(outs)
151 |
152 | def nearest(self, lookup_key):
153 | lookup_indexesb, distb = self.kdtree.nn_index(
154 | lookup_key.data.cpu().numpy(), 1)
155 | indexes = torch.LongTensor(lookup_indexesb).view(-1)
156 | values = torch.tensor(self.values).gather(0, indexes.to(lookup_key.device))
157 | return values.reshape(lookup_key.shape[0], -1)
158 |
159 |
160 |
161 | def lookup(self, lookup_key, update_flag=False, is_learning=False, p=0.7, K=0):
162 | """
163 | Perform DND lookup
164 | If update_flag == True, add the nearest neighbor indexes to self.indexes_to_be_updated
165 | """
166 | if K<=0:
167 | K=self.num_neighbors
168 | lookup_indexesb, distb = self.kdtree.nn_index(
169 | lookup_key.data.cpu().numpy(), min(K, len(self.keys)))
170 | indexes = torch.LongTensor(lookup_indexesb).view(-1)
171 |
172 | # print(self.kdtree.nn_index(
173 | # lookup_key.data.cpu().numpy(), min(self.num_neighbors, len(self.keys)))[0])
174 | # print("----")
175 | old_indexes = indexes
176 | vshape = torch.tensor(self.values).shape
177 | if len(vshape)==2:
178 | indexes=indexes.unsqueeze(-1).repeat(1, vshape[1])
179 |
180 | kvalues = inverse_distance(torch.tensor(distb).to(lookup_key.device))
181 | kvalues = kvalues/torch.sum(kvalues, dim=-1, keepdim=True)
182 | #print(torch.tensor(self.values))
183 | #print(indexes)
184 | values = torch.tensor(self.values).gather(0, indexes.to(lookup_key.device))
185 |
186 | #print(values)
187 | #raise False
188 | if len(vshape)==2:
189 | values = values.reshape(lookup_key.shape[0],vshape[1], -1)
190 | kvalues = kvalues.unsqueeze(1)
191 | else:
192 | values = values.reshape(lookup_key.shape[0],-1)
193 | if random.random() > p:
194 | return torch.max(values, dim=-1)[0].detach()
195 | if not is_learning:
196 | self.move_to_back.update(old_indexes.numpy())
197 |
198 | return torch.sum(kvalues*values, dim=-1).detach()
199 |
200 | # outs = []
201 | # for b, lookup_indexes in enumerate(lookup_indexesb):
202 | # output = 0
203 | # kernel_sum = 0
204 | # for i, index in enumerate(lookup_indexes):
205 | # if i == 0 and self.key_cache.get(tuple(lookup_key[0].data.cpu().numpy())) is not None:
206 | # # If a key exactly equal to lookup_key is used in the DND lookup calculation
207 | # # then the loss becomes non-differentiable. Just skip this case to avoid the issue.
208 | # continue
209 | # # if update_flag:
210 | # # self.indexes_to_be_updated.add(int(index))
211 | # # else:
212 | # # self.move_to_back.add(int(index))
213 | # # kernel_val = self.kernel(self.keys[int(index)], lookup_key[b])
214 | # kernel_val = inverse_distance(distb[b][i])
215 | # output += kernel_val * self.values[int(index)]
216 | # kernel_sum += kernel_val
217 | # output = output / kernel_sum
218 | # outs.append(output)
219 | # return torch.stack(outs)
220 |
221 | def lookup_grad(self, lookup_key, update_flag=False, is_learning=False):
222 | """
223 | Perform DND lookup
224 | If update_flag == True, add the nearest neighbor indexes to self.indexes_to_be_updated
225 | """
226 | lookup_indexesb, distb = self.kdtree.nn_index(
227 | lookup_key.data.cpu().numpy(), min(self.num_neighbors, len(self.keys)))
228 |
229 | train_var = []
230 | outs = []
231 | for b, lookup_indexes in enumerate(lookup_indexesb):
232 | output = 0
233 | kernel_sum = 0
234 | for i, index in enumerate(lookup_indexes):
235 | kkkk = self.keys[int(index)].detach().requires_grad_(True)
236 | train_var.append((kkkk, index))
237 | kernel_val = self.kernel(kkkk, lookup_key[b])
238 | #kernel_val = inverse_distance(distb[b][i])
239 | output += kernel_val * self.values[int(index)]
240 | kernel_sum += kernel_val
241 | output = output / kernel_sum
242 | outs.append(output)
243 | return torch.stack(outs), train_var
244 |
245 |
246 | def lookup2write(self, lookup_key, R, update_flag=False, K=0):
247 | """
248 | Perform DND lookup
249 | If update_flag == True, add the nearest neighbor indexes to self.indexes_to_be_updated
250 | """
251 | if K<=0:
252 | K=self.num_neighbors
253 | lookup_indexesb, distb = self.kdtree.nn_index(
254 | lookup_key.data.cpu().numpy(), min(K, len(self.keys)))
255 | #print(lookup_indexesb.shape)
256 |
257 | for b, lookup_indexes in enumerate(lookup_indexesb):
258 | ks = []
259 | kernel_sum = 0
260 | if K == 1 and len(lookup_indexes.shape)==1:
261 | lookup_indexes=[lookup_indexes]
262 | distb=[distb]
263 | if isinstance(lookup_indexes, np.int32):
264 | lookup_indexes = [lookup_indexes]
265 | distb=[distb]
266 |
267 | for i, index in enumerate(lookup_indexes):
268 | # if i == 0 and self.key_cache.get(tuple(lookup_key[0].data.cpu().numpy())) is not None:
269 | # If a key exactly equal to lookup_key is used in the DND lookup calculation
270 | # then the loss becomes non-differentiable. Just skip this case to avoid the issue.
271 | # continue
272 | if update_flag:
273 | self.indexes_to_be_updated.add(int(index))
274 | else:
275 | self.move_to_back.add(int(index))
276 | curv = self.values[int(index)]
277 | # kernel_val = self.kernel(self.keys[int(index)], lookup_key[b])
278 | kernel_val = inverse_distance(distb[b][i])
279 | kernel_sum += kernel_val
280 | ks.append((index,kernel_val, curv))
281 | # self.update((R - curv) * kernel_val * self.lr + curv, index)
282 | # kernel_val = 1
283 | for index, kernel_val, curv in ks:
284 | self.update((R-curv)*kernel_val/kernel_sum*self.lr + curv, index)
285 | #self.update(torch.max(R,curv), index)
286 |
287 |
288 | def update_params(self):
289 | """
290 | Update self.keys and self.values via backprop
291 | Use self.indexes_to_be_updated to update self.key_cache accordingly and rebuild the index of self.kdtree
292 | """
293 | for index in self.indexes_to_be_updated:
294 | del self.key_cache[tuple(self.keys[index].data.cpu().numpy())]
295 | # self.optimizer.step()
296 | # self.optimizer.zero_grad()
297 | for index in self.indexes_to_be_updated:
298 | self.key_cache[tuple(self.keys[index].data.cpu().numpy())] = 0
299 | self.indexes_to_be_updated = set()
300 | self.kdtree.build_index(self.keys.data.cpu().numpy())
301 | self.stale_index = False
302 |
--------------------------------------------------------------------------------
/dqn_mbec.py:
--------------------------------------------------------------------------------
1 | import math, random
2 |
3 | import gym
4 | import numpy as np
5 |
6 | import torch
7 | import torch.nn as nn
8 | import torch.optim as optim
9 | import torch.autograd as autograd
10 | import torch.nn.functional as F
11 | from torch.optim.lr_scheduler import StepLR
12 | import os, json, copy, pickle
13 | from IPython.display import clear_output
14 | import matplotlib.pyplot as plt
15 | from tensorboard_logger import configure, log_value
16 | from argparse import ArgumentParser
17 | from collections import deque
18 | from tqdm import tqdm
19 | import em as dnd
20 | import controller
21 |
22 | USE_CUDA = torch.cuda.is_available()
23 | # USE_CUDA= False
24 | Variable = lambda *args, **kwargs: autograd.Variable(*args, **kwargs).cuda() if USE_CUDA else autograd.Variable(*args, **kwargs)
25 |
26 |
27 |
28 |
29 |
30 | class ReplayBuffer(object):
31 | def __init__(self, capacity):
32 | self.buffer = deque(maxlen=capacity)
33 |
34 | def push(self, state, h_trj, action, reward, old_reward, next_state, nh_trj, done):
35 | state = np.expand_dims(state, 0)
36 | next_state = np.expand_dims(next_state, 0)
37 | h_trj = np.expand_dims(h_trj, 0)
38 | nh_trj = np.expand_dims(nh_trj, 0)
39 |
40 | self.buffer.append((state, h_trj, action, reward, old_reward, next_state, nh_trj, done))
41 |
42 | def sample(self, batch_size):
43 | state, h_trj, action, reward, old_reward, next_state, nh_trj, done = zip(*random.sample(self.buffer, batch_size))
44 | return np.concatenate(state), np.concatenate(h_trj), action, reward, old_reward, \
45 | np.concatenate(next_state), np.concatenate(nh_trj), done
46 |
47 | def __len__(self):
48 | return len(self.buffer)
49 |
50 |
51 |
52 |
53 | mse_criterion = nn.MSELoss()
54 |
55 |
56 | # plt.plot([epsilon_by_frame(i) for i in range(10000)])
57 | # plt.show()
58 | def inverse_distance(h, h_i, epsilon=1e-3):
59 | return 1 / (torch.dist(h, h_i) + epsilon)
60 |
61 | def gauss_kernel(h, h_i, w=0.5):
62 | return torch.exp(-torch.dist(h, h_i)**2/w)
63 |
64 | def no_distance(h, h_i, epsilon=1e-3):
65 | return 1
66 |
67 |
68 | cos = nn.CosineSimilarity(dim=0, eps=1e-6)
69 | def cosine_distance(h, h_i):
70 | return max(cos(h, h_i),0)
71 |
72 | def weights_init(m):
73 | classname = m.__class__.__name__
74 | if classname.find('Conv') != -1:
75 | weight_shape = list(m.weight.data.size())
76 | fan_in = np.prod(weight_shape[1:4])
77 | fan_out = np.prod(weight_shape[2:4]) * weight_shape[0]
78 | w_bound = np.sqrt(6. / (fan_in + fan_out))
79 | m.weight.data.uniform_(-w_bound, w_bound)
80 | m.bias.data.fill_(0)
81 | elif classname.find('Linear') != -1:
82 | weight_shape = list(m.weight.data.size())
83 | fan_in = weight_shape[1]
84 | fan_out = weight_shape[0]
85 | w_bound = np.sqrt(6. / (fan_in + fan_out))
86 | m.weight.data.uniform_(-w_bound, w_bound)
87 | m.bias.data.fill_(0)
88 |
89 | class DQN_DTM(nn.Module):
90 | def __init__(self, env, args):
91 | super(DQN_DTM, self).__init__()
92 | if "maze_img" in args.task2:
93 | if "cnn" not in args.task2:
94 | self.num_inputs = 8
95 | self.proj= nn.Linear(514, 8)
96 | else:
97 | self.cnn = img_featurize.CNN(64)
98 | self.num_inputs = 64
99 |
100 | elif "world" in args.task2:
101 | self.cnn = img_featurize.CNN2(256)
102 | self.num_inputs = 256
103 | for param in self.cnn.parameters():
104 | param.requires_grad = False
105 | else:
106 | self.num_inputs = env.observation_space.shape[0]
107 | if "trap" in args.task2:
108 | self.num_inputs = self.num_inputs + 2
109 | self.num_actions = env.action_space.n
110 | self.model_name=args.model_name
111 | self.num_warm_up=args.num_warm_up
112 | self.replay_buffer = args.replay_buffer
113 | self.gamma = args.gamma
114 | self.last_inserts=[]
115 | self.insert_size = args.insert_size
116 | self.args= args
117 |
118 | self.qnet = nn.Sequential(
119 | nn.Linear(self.num_inputs, args.qnet_size),
120 | nn.ReLU(),
121 | nn.Linear(args.qnet_size, args.qnet_size),
122 | nn.ReLU(),
123 | nn.Linear(args.qnet_size, env.action_space.n)
124 | )
125 |
126 | self.qnet_target = nn.Sequential(
127 | nn.Linear(self.num_inputs, args.qnet_size),
128 | nn.ReLU(),
129 | nn.Linear(args.qnet_size, args.qnet_size),
130 | nn.ReLU(),
131 | nn.Linear(args.qnet_size, env.action_space.n)
132 | )
133 |
134 |
135 | if args.write_interval<0:
136 | self.state2key = nn.Linear(self.num_inputs, args.hidden_size)
137 | self.dnd = dnd.DND(no_distance, num_neighbors=args.k, max_memory=args.memory_size, lr=args.write_lr)
138 | else:
139 | self.dnd = dnd.DND(inverse_distance, num_neighbors=args.k, max_memory=args.memory_size, lr=args.write_lr)
140 |
141 | self.emb_index2count = {}
142 | self.act_net = nn.Linear(self.num_actions, self.num_actions)
143 | self.act_net_target = nn.Linear(self.num_actions, self.num_actions)
144 |
145 | self.choice_net = nn.Sequential(
146 | nn.Linear(args.hidden_size, args.hidden_size),nn.ReLU(),
147 | nn.Linear(args.hidden_size, 1))
148 | self.choice_net_target = nn.Sequential(
149 | nn.Linear(args.hidden_size, args.hidden_size),nn.ReLU(),
150 | nn.Linear(args.hidden_size, 1))
151 | self.alpha = nn.Parameter(torch.tensor(1.0),
152 | requires_grad=True)
153 | self.alpha_target = nn.Parameter(torch.tensor(1.0),
154 | requires_grad=True)
155 | self.beta = nn.Parameter(torch.ones(self.num_actions),
156 | requires_grad=True)
157 | self.trj_model = controller.LSTMController(self.num_inputs+self.num_actions, args.hidden_size, num_layers=1)
158 | self.trj_out = nn.Linear(args.hidden_size, self.num_inputs+self.num_actions+1)
159 | self.reward_model = nn.Sequential(
160 | nn.Linear(self.num_inputs+self.num_actions+args.hidden_size, args.reward_hidden_size),
161 | nn.ReLU(),
162 | nn.Linear(args.reward_hidden_size,args.reward_hidden_size),
163 | nn.ReLU(),
164 | nn.Linear(args.reward_hidden_size, 1),
165 | )
166 | self.best_trj = []
167 | self.optimizer_dnd = torch.optim.Adam(self.trj_model.parameters())
168 | # self.future_net = nn.Sequential(
169 | # nn.Linear(args.hidden_size, args.hidden_size),
170 | # nn.ReLU(),
171 | # nn.Linear(args.hidden_size, args.hidden_size),
172 | # )
173 | self.future_net = nn.Sequential(
174 | nn.Linear(args.hidden_size, args.mem_dim),
175 | )
176 |
177 | for param in self.future_net.parameters():
178 | param.requires_grad = False
179 | if args.rec_period<0:
180 | for param in self.trj_model.parameters():
181 | param.requires_grad = False
182 |
183 | self.apply(weights_init)
184 |
185 | def forward(self, x, h_trj, episode=0, use_mem=1.0, target=0, r=None, a=None, is_learning=False):
186 |
187 | q_value_semantic = self.semantic_net(x, target)
188 | z = torch.zeros(x.shape[0], self.num_actions)
189 | if USE_CUDA:
190 | z = z.cuda()
191 | q_episodic = qt= z
192 | if episode > self.num_warm_up and random.random() Use Cuda
"
40 | ]
41 | },
42 | {
43 | "cell_type": "code",
44 | "execution_count": 17,
45 | "metadata": {},
46 | "outputs": [],
47 | "source": [
48 | "USE_CUDA = torch.cuda.is_available()\n",
49 | "Variable = lambda *args, **kwargs: autograd.Variable(*args, **kwargs).cuda() if USE_CUDA else autograd.Variable(*args, **kwargs)"
50 | ]
51 | },
52 | {
53 | "cell_type": "markdown",
54 | "metadata": {},
55 | "source": [
56 | "Replay Buffer
"
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": 18,
62 | "metadata": {},
63 | "outputs": [],
64 | "source": [
65 | "from collections import deque\n",
66 | "\n",
67 | "class ReplayBuffer(object):\n",
68 | " def __init__(self, capacity):\n",
69 | " self.buffer = deque(maxlen=capacity)\n",
70 | " \n",
71 | " def push(self, state, action, reward, next_state, done):\n",
72 | " state = np.expand_dims(state, 0)\n",
73 | " next_state = np.expand_dims(next_state, 0)\n",
74 | " \n",
75 | " self.buffer.append((state, action, reward, next_state, done))\n",
76 | " \n",
77 | " def sample(self, batch_size):\n",
78 | " state, action, reward, next_state, done = zip(*random.sample(self.buffer, batch_size))\n",
79 | " return np.concatenate(state), action, reward, np.concatenate(next_state), done\n",
80 | " \n",
81 | " def __len__(self):\n",
82 | " return len(self.buffer)"
83 | ]
84 | },
85 | {
86 | "cell_type": "markdown",
87 | "metadata": {},
88 | "source": [
89 | "Cart Pole Environment
"
90 | ]
91 | },
92 | {
93 | "cell_type": "code",
94 | "execution_count": 19,
95 | "metadata": {},
96 | "outputs": [],
97 | "source": [
98 | "env_id = \"MountainCar-v0\"\n",
99 | "env = gym.make(env_id)"
100 | ]
101 | },
102 | {
103 | "cell_type": "markdown",
104 | "metadata": {},
105 | "source": [
106 | "Epsilon greedy exploration
"
107 | ]
108 | },
109 | {
110 | "cell_type": "code",
111 | "execution_count": 20,
112 | "metadata": {},
113 | "outputs": [],
114 | "source": [
115 | "epsilon_start = 1.0\n",
116 | "epsilon_final = 0.01\n",
117 | "epsilon_decay = 500\n",
118 | "\n",
119 | "epsilon_by_frame = lambda frame_idx: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1. * frame_idx / epsilon_decay)"
120 | ]
121 | },
122 | {
123 | "cell_type": "code",
124 | "execution_count": 21,
125 | "metadata": {},
126 | "outputs": [
127 | {
128 | "data": {
129 | "text/plain": [
130 | "[Deep Q Network
"
159 | ]
160 | },
161 | {
162 | "cell_type": "code",
163 | "execution_count": 22,
164 | "metadata": {},
165 | "outputs": [],
166 | "source": [
167 | "class DQN(nn.Module):\n",
168 | " def __init__(self, num_inputs, num_actions):\n",
169 | " super(DQN, self).__init__()\n",
170 | " \n",
171 | " self.layers = nn.Sequential(\n",
172 | " nn.Linear(env.observation_space.shape[0], 128),\n",
173 | " nn.ReLU(),\n",
174 | " nn.Linear(128, 128),\n",
175 | " nn.ReLU(),\n",
176 | " nn.Linear(128, env.action_space.n)\n",
177 | " )\n",
178 | " \n",
179 | " def forward(self, x):\n",
180 | " return self.layers(x)\n",
181 | " \n",
182 | " def act(self, state, epsilon):\n",
183 | " if random.random() > epsilon:\n",
184 | " state = Variable(torch.FloatTensor(state).unsqueeze(0), volatile=True)\n",
185 | " q_value = self.forward(state)\n",
186 | " action = q_value.max(1)[1].data[0]\n",
187 | " else:\n",
188 | " action = random.randrange(env.action_space.n)\n",
189 | " return action"
190 | ]
191 | },
192 | {
193 | "cell_type": "code",
194 | "execution_count": 23,
195 | "metadata": {},
196 | "outputs": [],
197 | "source": [
198 | "model = DQN(env.observation_space.shape[0], env.action_space.n)\n",
199 | "\n",
200 | "if USE_CUDA:\n",
201 | " model = model.cuda()\n",
202 | " \n",
203 | "optimizer = optim.Adam(model.parameters())\n",
204 | "\n",
205 | "replay_buffer = ReplayBuffer(1000)"
206 | ]
207 | },
208 | {
209 | "cell_type": "markdown",
210 | "metadata": {},
211 | "source": [
212 | "Computing Temporal Difference Loss
"
213 | ]
214 | },
215 | {
216 | "cell_type": "code",
217 | "execution_count": 24,
218 | "metadata": {},
219 | "outputs": [],
220 | "source": [
221 | "def compute_td_loss(batch_size):\n",
222 | " state, action, reward, next_state, done = replay_buffer.sample(batch_size)\n",
223 | "\n",
224 | " state = Variable(torch.FloatTensor(np.float32(state)))\n",
225 | " next_state = Variable(torch.FloatTensor(np.float32(next_state)), volatile=True)\n",
226 | " action = Variable(torch.LongTensor(action))\n",
227 | " reward = Variable(torch.FloatTensor(reward))\n",
228 | " done = Variable(torch.FloatTensor(done))\n",
229 | "\n",
230 | " q_values = model(state)\n",
231 | " next_q_values = model(next_state)\n",
232 | "\n",
233 | " q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)\n",
234 | " next_q_value = next_q_values.max(1)[0]\n",
235 | " expected_q_value = reward + gamma * next_q_value * (1 - done)\n",
236 | " \n",
237 | " loss = (q_value - Variable(expected_q_value.data)).pow(2).mean()\n",
238 | " \n",
239 | " optimizer.zero_grad()\n",
240 | " loss.backward()\n",
241 | " optimizer.step()\n",
242 | " \n",
243 | " return loss"
244 | ]
245 | },
246 | {
247 | "cell_type": "code",
248 | "execution_count": 25,
249 | "metadata": {},
250 | "outputs": [],
251 | "source": [
252 | "def plot(frame_idx, rewards, losses):\n",
253 | " clear_output(True)\n",
254 | " plt.figure(figsize=(20,5))\n",
255 | " plt.subplot(131)\n",
256 | " plt.title('frame %s. reward: %s' % (frame_idx, np.mean(rewards[-10:])))\n",
257 | " plt.plot(rewards)\n",
258 | " plt.subplot(132)\n",
259 | " plt.title('loss')\n",
260 | " plt.plot(losses)\n",
261 | " plt.show()"
262 | ]
263 | },
264 | {
265 | "cell_type": "markdown",
266 | "metadata": {},
267 | "source": [
268 | "Training
"
269 | ]
270 | },
271 | {
272 | "cell_type": "code",
273 | "execution_count": 26,
274 | "metadata": {},
275 | "outputs": [
276 | {
277 | "data": {
278 | "image/png": "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\n",
279 | "text/plain": [
280 | "