├── README.md
├── code
├── 1.dqn.ipynb
├── 2.double dqn.ipynb
├── 3.dueling dqn.ipynb
├── 4.prioritized dqn.ipynb
├── 5.noisy dqn.ipynb
├── 6.categorical dqn.ipynb
├── 7.rainbow dqn.ipynb
├── README.md
├── common
│ ├── __init__.py
│ ├── layers.py
│ ├── replay_buffer.py
│ └── wrappers.py
├── play_rainbow_dqn_CartPole-v1.py
├── play_rainbow_dqn_PongNoFrameskip-v4.py
├── pytorch0.4.1
│ ├── 4.prioritized dqn.ipynb
│ └── 5.noisy dqn.ipynb
├── save_gif
│ ├── CartPole-v1_RainbowDQN_10.gif
│ └── PongNoFrameskip-v4_RainbowDQN_3.gif
└── save_model
│ ├── 27586-CartPole-v1_RainbowDQN.pkl
│ └── 999398-PongNoFrameskip-v4_RainbowDQN.pkl
└── slide
├── pt1_dqn_double_duel.pdf
├── pt2_per_noisynet.pdf
├── pt3_C51_distributional.pdf
├── pt4_rainbow.pdf
└── template.pptx
/README.md:
--------------------------------------------------------------------------------
1 | # Pycon2018 "RL Adventure : DQN 부터 Rainbow DQN까지"
2 | 참조 reference : https://github.com/higgsfield/RL-Adventure
3 |
4 | ## 발표자료
5 | 그림을 클릭하시면 자료화면으로 넘어갑니다
6 |
7 | [
](https://www.slideshare.net/ssuserbd7730/pyconkr-2018-rladventureto-the-rainbow)
8 |
9 | ## 발표내용
10 | - 파트 1 : DQN, Double & Dueling DQN - 성태경
11 | - 파트 2 : PER and NoisyNet - 양홍선
12 | - 파트 3 : Distributed RL - 이의령
13 | - 파트 4 : RAINBOW - 김예찬
14 |
15 |
16 | ## 실험환경
17 | **openai gym** : atari
18 | - CartPole-v1
19 | - PongNoFrameskip-v4
20 |
21 |
22 | ## 구현체
23 | - DQN
24 | - DDQN
25 | - Dueling DQN
26 | - PER
27 | - Noisy DQN
28 | - Categorical DQN
29 | - Rainbow
30 |
31 |
32 | ## 실험결과 확인
33 |
34 |
35 | CartPole-v1 |
36 | PongNoFrameskip-v4 |
37 |
38 |
39 |
40 |
41 | |
42 |
43 |
44 | |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/code/7.rainbow dqn.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": 2,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import math, random\n",
10 | "\n",
11 | "import gym\n",
12 | "import numpy as np\n",
13 | "\n",
14 | "import torch\n",
15 | "import torch.nn as nn\n",
16 | "import torch.optim as optim\n",
17 | "import torch.autograd as autograd \n",
18 | "import torch.nn.functional as F\n",
19 | "\n",
20 | "from common.layers import NoisyLinear\n",
21 | "from common.replay_buffer import ReplayBuffer"
22 | ]
23 | },
24 | {
25 | "cell_type": "code",
26 | "execution_count": 3,
27 | "metadata": {},
28 | "outputs": [],
29 | "source": [
30 | "from IPython.display import clear_output\n",
31 | "import matplotlib.pyplot as plt\n",
32 | "%matplotlib inline"
33 | ]
34 | },
35 | {
36 | "cell_type": "markdown",
37 | "metadata": {},
38 | "source": [
39 | "Use Cuda
"
40 | ]
41 | },
42 | {
43 | "cell_type": "code",
44 | "execution_count": 4,
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 | "Cart Pole Environment
"
57 | ]
58 | },
59 | {
60 | "cell_type": "code",
61 | "execution_count": 5,
62 | "metadata": {},
63 | "outputs": [
64 | {
65 | "name": "stdout",
66 | "output_type": "stream",
67 | "text": [
68 | "\u001b[33mWARN: gym.spaces.Box autodetected dtype as . Please provide explicit dtype.\u001b[0m\n"
69 | ]
70 | }
71 | ],
72 | "source": [
73 | "env_id = \"CartPole-v0\"\n",
74 | "env = gym.make(env_id)"
75 | ]
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {},
80 | "source": [
81 | " Rainbow: Combining Improvements in Deep Reinforcement Learning
"
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": 12,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "class RainbowDQN(nn.Module):\n",
91 | " def __init__(self, num_inputs, num_actions, num_atoms, Vmin, Vmax):\n",
92 | " super(RainbowDQN, self).__init__()\n",
93 | " \n",
94 | " self.num_inputs = num_inputs\n",
95 | " self.num_actions = num_actions\n",
96 | " self.num_atoms = num_atoms\n",
97 | " self.Vmin = Vmin\n",
98 | " self.Vmax = Vmax\n",
99 | " \n",
100 | " self.linear1 = nn.Linear(num_inputs, 32)\n",
101 | " self.linear2 = nn.Linear(32, 64)\n",
102 | " \n",
103 | " self.noisy_value1 = NoisyLinear(64, 64, use_cuda=USE_CUDA)\n",
104 | " self.noisy_value2 = NoisyLinear(64, self.num_atoms, use_cuda=USE_CUDA)\n",
105 | " \n",
106 | " self.noisy_advantage1 = NoisyLinear(64, 64, use_cuda=USE_CUDA)\n",
107 | " self.noisy_advantage2 = NoisyLinear(64, self.num_atoms * self.num_actions, use_cuda=USE_CUDA)\n",
108 | " \n",
109 | " def forward(self, x):\n",
110 | " batch_size = x.size(0)\n",
111 | " \n",
112 | " x = F.relu(self.linear1(x))\n",
113 | " x = F.relu(self.linear2(x))\n",
114 | " \n",
115 | " value = F.relu(self.noisy_value1(x))\n",
116 | " value = self.noisy_value2(value)\n",
117 | " \n",
118 | " advantage = F.relu(self.noisy_advantage1(x))\n",
119 | " advantage = self.noisy_advantage2(advantage)\n",
120 | " \n",
121 | " value = value.view(batch_size, 1, self.num_atoms)\n",
122 | " advantage = advantage.view(batch_size, self.num_actions, self.num_atoms)\n",
123 | " \n",
124 | " x = value + advantage - advantage.mean(1, keepdim=True)\n",
125 | " x = F.softmax(x.view(-1, self.num_atoms)).view(-1, self.num_actions, self.num_atoms)\n",
126 | " \n",
127 | " return x\n",
128 | " \n",
129 | " def reset_noise(self):\n",
130 | " self.noisy_value1.reset_noise()\n",
131 | " self.noisy_value2.reset_noise()\n",
132 | " self.noisy_advantage1.reset_noise()\n",
133 | " self.noisy_advantage2.reset_noise()\n",
134 | " \n",
135 | " def act(self, state):\n",
136 | " state = Variable(torch.FloatTensor(state).unsqueeze(0), volatile=True)\n",
137 | " dist = self.forward(state).data.cpu()\n",
138 | " dist = dist * torch.linspace(self.Vmin, self.Vmax, self.num_atoms)\n",
139 | " action = dist.sum(2).max(1)[1].numpy()[0]\n",
140 | " return action"
141 | ]
142 | },
143 | {
144 | "cell_type": "code",
145 | "execution_count": 29,
146 | "metadata": {},
147 | "outputs": [],
148 | "source": [
149 | "num_atoms = 51\n",
150 | "Vmin = -10\n",
151 | "Vmax = 10\n",
152 | "\n",
153 | "current_model = RainbowDQN(env.observation_space.shape[0], env.action_space.n, num_atoms, Vmin, Vmax)\n",
154 | "target_model = RainbowDQN(env.observation_space.shape[0], env.action_space.n, num_atoms, Vmin, Vmax)\n",
155 | "\n",
156 | "if USE_CUDA:\n",
157 | " current_model = current_model.cuda()\n",
158 | " target_model = target_model.cuda()\n",
159 | " \n",
160 | "optimizer = optim.Adam(current_model.parameters(), 0.001)\n",
161 | "\n",
162 | "replay_buffer = ReplayBuffer(10000)"
163 | ]
164 | },
165 | {
166 | "cell_type": "code",
167 | "execution_count": 30,
168 | "metadata": {},
169 | "outputs": [],
170 | "source": [
171 | "def update_target(current_model, target_model):\n",
172 | " target_model.load_state_dict(current_model.state_dict())\n",
173 | " \n",
174 | "update_target(current_model, target_model)"
175 | ]
176 | },
177 | {
178 | "cell_type": "code",
179 | "execution_count": 31,
180 | "metadata": {},
181 | "outputs": [],
182 | "source": [
183 | "def projection_distribution(next_state, rewards, dones):\n",
184 | " batch_size = next_state.size(0)\n",
185 | " \n",
186 | " delta_z = float(Vmax - Vmin) / (num_atoms - 1)\n",
187 | " support = torch.linspace(Vmin, Vmax, num_atoms)\n",
188 | " \n",
189 | " next_dist = target_model(next_state).data.cpu() * support\n",
190 | " next_action = next_dist.sum(2).max(1)[1]\n",
191 | " next_action = next_action.unsqueeze(1).unsqueeze(1).expand(next_dist.size(0), 1, next_dist.size(2))\n",
192 | " next_dist = next_dist.gather(1, next_action).squeeze(1)\n",
193 | " \n",
194 | " rewards = rewards.unsqueeze(1).expand_as(next_dist)\n",
195 | " dones = dones.unsqueeze(1).expand_as(next_dist)\n",
196 | " support = support.unsqueeze(0).expand_as(next_dist)\n",
197 | " \n",
198 | " Tz = rewards + (1 - dones) * 0.99 * support\n",
199 | " Tz = Tz.clamp(min=Vmin, max=Vmax)\n",
200 | " b = (Tz - Vmin) / delta_z\n",
201 | " l = b.floor().long()\n",
202 | " u = b.ceil().long()\n",
203 | " \n",
204 | " offset = torch.linspace(0, (batch_size - 1) * num_atoms, batch_size).long()\\\n",
205 | " .unsqueeze(1).expand(batch_size, num_atoms)\n",
206 | "\n",
207 | " proj_dist = torch.zeros(next_dist.size()) \n",
208 | " proj_dist.view(-1).index_add_(0, (l + offset).view(-1), (next_dist * (u.float() - b)).view(-1))\n",
209 | " proj_dist.view(-1).index_add_(0, (u + offset).view(-1), (next_dist * (b - l.float())).view(-1))\n",
210 | " \n",
211 | " return proj_dist"
212 | ]
213 | },
214 | {
215 | "cell_type": "markdown",
216 | "metadata": {},
217 | "source": [
218 | "Computing Temporal Difference Loss
"
219 | ]
220 | },
221 | {
222 | "cell_type": "code",
223 | "execution_count": 32,
224 | "metadata": {},
225 | "outputs": [],
226 | "source": [
227 | "def compute_td_loss(batch_size):\n",
228 | " state, action, reward, next_state, done = replay_buffer.sample(batch_size) \n",
229 | "\n",
230 | " state = Variable(torch.FloatTensor(np.float32(state)))\n",
231 | " next_state = Variable(torch.FloatTensor(np.float32(next_state)), volatile=True)\n",
232 | " action = Variable(torch.LongTensor(action))\n",
233 | " reward = torch.FloatTensor(reward)\n",
234 | " done = torch.FloatTensor(np.float32(done))\n",
235 | "\n",
236 | " proj_dist = projection_distribution(next_state, reward, done)\n",
237 | " \n",
238 | " dist = current_model(state)\n",
239 | " action = action.unsqueeze(1).unsqueeze(1).expand(batch_size, 1, num_atoms)\n",
240 | " dist = dist.gather(1, action).squeeze(1)\n",
241 | " dist.data.clamp_(0.01, 0.99)\n",
242 | " loss = -(Variable(proj_dist) * dist.log()).sum(1)\n",
243 | " loss = loss.mean()\n",
244 | " \n",
245 | " optimizer.zero_grad()\n",
246 | " loss.backward()\n",
247 | " optimizer.step()\n",
248 | "\n",
249 | " current_model.reset_noise()\n",
250 | " target_model.reset_noise()\n",
251 | " \n",
252 | " return loss"
253 | ]
254 | },
255 | {
256 | "cell_type": "code",
257 | "execution_count": 33,
258 | "metadata": {},
259 | "outputs": [],
260 | "source": [
261 | "def plot(frame_idx, rewards, losses):\n",
262 | " clear_output(True)\n",
263 | " plt.figure(figsize=(20,5))\n",
264 | " plt.subplot(131)\n",
265 | " plt.title('frame %s. reward: %s' % (frame_idx, np.mean(rewards[-10:])))\n",
266 | " plt.plot(rewards)\n",
267 | " plt.subplot(132)\n",
268 | " plt.title('loss')\n",
269 | " plt.plot(losses)\n",
270 | " plt.show()"
271 | ]
272 | },
273 | {
274 | "cell_type": "markdown",
275 | "metadata": {},
276 | "source": [
277 | "Training
"
278 | ]
279 | },
280 | {
281 | "cell_type": "code",
282 | "execution_count": 35,
283 | "metadata": {},
284 | "outputs": [
285 | {
286 | "data": {
287 | "image/png": "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\nShWA/weLKyFwyGPfjwAAAABJRU5ErkJggg==\n",
288 | "text/plain": [
289 | ""
290 | ]
291 | },
292 | "metadata": {},
293 | "output_type": "display_data"
294 | }
295 | ],
296 | "source": [
297 | "num_frames = 15000\n",
298 | "batch_size = 32\n",
299 | "gamma = 0.99\n",
300 | "\n",
301 | "losses = []\n",
302 | "all_rewards = []\n",
303 | "episode_reward = 0\n",
304 | "\n",
305 | "state = env.reset()\n",
306 | "for frame_idx in range(1, num_frames + 1):\n",
307 | " action = current_model.act(state)\n",
308 | " \n",
309 | " next_state, reward, done, _ = env.step(action)\n",
310 | " replay_buffer.push(state, action, reward, next_state, done)\n",
311 | " \n",
312 | " state = next_state\n",
313 | " episode_reward += reward\n",
314 | " \n",
315 | " if done:\n",
316 | " state = env.reset()\n",
317 | " all_rewards.append(episode_reward)\n",
318 | " episode_reward = 0\n",
319 | " \n",
320 | " if len(replay_buffer) > batch_size:\n",
321 | " loss = compute_td_loss(batch_size)\n",
322 | " losses.append(loss.data[0])\n",
323 | " \n",
324 | " if frame_idx % 200 == 0:\n",
325 | " plot(frame_idx, all_rewards, losses)\n",
326 | " \n",
327 | " if frame_idx % 1000 == 0:\n",
328 | " update_target(current_model, target_model)"
329 | ]
330 | },
331 | {
332 | "cell_type": "markdown",
333 | "metadata": {},
334 | "source": [
335 | "
"
336 | ]
337 | },
338 | {
339 | "cell_type": "markdown",
340 | "metadata": {},
341 | "source": [
342 | "Atari Environment
"
343 | ]
344 | },
345 | {
346 | "cell_type": "code",
347 | "execution_count": 36,
348 | "metadata": {},
349 | "outputs": [],
350 | "source": [
351 | "from common.wrappers import make_atari, wrap_deepmind, wrap_pytorch"
352 | ]
353 | },
354 | {
355 | "cell_type": "code",
356 | "execution_count": 37,
357 | "metadata": {},
358 | "outputs": [],
359 | "source": [
360 | "env_id = \"PongNoFrameskip-v4\"\n",
361 | "env = make_atari(env_id)\n",
362 | "env = wrap_deepmind(env)\n",
363 | "env = wrap_pytorch(env)"
364 | ]
365 | },
366 | {
367 | "cell_type": "code",
368 | "execution_count": 38,
369 | "metadata": {},
370 | "outputs": [],
371 | "source": [
372 | "class RainbowCnnDQN(nn.Module):\n",
373 | " def __init__(self, input_shape, num_actions, num_atoms, Vmin, Vmax):\n",
374 | " super(RainbowCnnDQN, self).__init__()\n",
375 | " \n",
376 | " self.input_shape = input_shape\n",
377 | " self.num_actions = num_actions\n",
378 | " self.num_atoms = num_atoms\n",
379 | " self.Vmin = Vmin\n",
380 | " self.Vmax = Vmax\n",
381 | " \n",
382 | " self.features = nn.Sequential(\n",
383 | " nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),\n",
384 | " nn.ReLU(),\n",
385 | " nn.Conv2d(32, 64, kernel_size=4, stride=2),\n",
386 | " nn.ReLU(),\n",
387 | " nn.Conv2d(64, 64, kernel_size=3, stride=1),\n",
388 | " nn.ReLU()\n",
389 | " )\n",
390 | " \n",
391 | " self.noisy_value1 = NoisyLinear(self.feature_size(), 512, use_cuda=USE_CUDA)\n",
392 | " self.noisy_value2 = NoisyLinear(512, self.num_atoms, use_cuda=USE_CUDA)\n",
393 | " \n",
394 | " self.noisy_advantage1 = NoisyLinear(self.feature_size(), 512, use_cuda=USE_CUDA)\n",
395 | " self.noisy_advantage2 = NoisyLinear(512, self.num_atoms * self.num_actions, use_cuda=USE_CUDA)\n",
396 | " \n",
397 | " def forward(self, x):\n",
398 | " batch_size = x.size(0)\n",
399 | " \n",
400 | " x = x / 255.\n",
401 | " x = self.features(x)\n",
402 | " x = x.view(batch_size, -1)\n",
403 | " \n",
404 | " value = F.relu(self.noisy_value1(x))\n",
405 | " value = self.noisy_value2(value)\n",
406 | " \n",
407 | " advantage = F.relu(self.noisy_advantage1(x))\n",
408 | " advantage = self.noisy_advantage2(advantage)\n",
409 | " \n",
410 | " value = value.view(batch_size, 1, self.num_atoms)\n",
411 | " advantage = advantage.view(batch_size, self.num_actions, self.num_atoms)\n",
412 | " \n",
413 | " x = value + advantage - advantage.mean(1, keepdim=True)\n",
414 | " x = F.softmax(x.view(-1, self.num_atoms)).view(-1, self.num_actions, self.num_atoms)\n",
415 | " \n",
416 | " return x\n",
417 | " \n",
418 | " def reset_noise(self):\n",
419 | " self.noisy_value1.reset_noise()\n",
420 | " self.noisy_value2.reset_noise()\n",
421 | " self.noisy_advantage1.reset_noise()\n",
422 | " self.noisy_advantage2.reset_noise()\n",
423 | " \n",
424 | " def feature_size(self):\n",
425 | " return self.features(autograd.Variable(torch.zeros(1, *self.input_shape))).view(1, -1).size(1)\n",
426 | " \n",
427 | " def act(self, state):\n",
428 | " state = Variable(torch.FloatTensor(np.float32(state)).unsqueeze(0), volatile=True)\n",
429 | " dist = self.forward(state).data.cpu()\n",
430 | " dist = dist * torch.linspace(self.Vmin, self.Vmax, self.num_atoms)\n",
431 | " action = dist.sum(2).max(1)[1].numpy()[0]\n",
432 | " return action"
433 | ]
434 | },
435 | {
436 | "cell_type": "code",
437 | "execution_count": 39,
438 | "metadata": {},
439 | "outputs": [],
440 | "source": [
441 | "num_atoms = 51\n",
442 | "Vmin = -10\n",
443 | "Vmax = 10\n",
444 | "\n",
445 | "current_model = RainbowCnnDQN(env.observation_space.shape, env.action_space.n, num_atoms, Vmin, Vmax)\n",
446 | "target_model = RainbowCnnDQN(env.observation_space.shape, env.action_space.n, num_atoms, Vmin, Vmax)\n",
447 | "\n",
448 | "if USE_CUDA:\n",
449 | " current_model = current_model.cuda()\n",
450 | " target_model = target_model.cuda()\n",
451 | " \n",
452 | "optimizer = optim.Adam(current_model.parameters(), lr=0.0001)\n",
453 | "update_target(current_model, target_model)\n",
454 | "\n",
455 | "replay_initial = 10000\n",
456 | "replay_buffer = ReplayBuffer(100000)"
457 | ]
458 | },
459 | {
460 | "cell_type": "code",
461 | "execution_count": 41,
462 | "metadata": {},
463 | "outputs": [
464 | {
465 | "data": {
466 | "image/png": "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\n",
467 | "text/plain": [
468 | ""
469 | ]
470 | },
471 | "metadata": {},
472 | "output_type": "display_data"
473 | }
474 | ],
475 | "source": [
476 | "num_frames = 1000000\n",
477 | "batch_size = 32\n",
478 | "gamma = 0.99\n",
479 | "\n",
480 | "losses = []\n",
481 | "all_rewards = []\n",
482 | "episode_reward = 0\n",
483 | "\n",
484 | "state = env.reset()\n",
485 | "for frame_idx in range(1, num_frames + 1):\n",
486 | " action = current_model.act(state)\n",
487 | " \n",
488 | " next_state, reward, done, _ = env.step(action)\n",
489 | " replay_buffer.push(state, action, reward, next_state, done)\n",
490 | " \n",
491 | " state = next_state\n",
492 | " episode_reward += reward\n",
493 | " \n",
494 | " if done:\n",
495 | " state = env.reset()\n",
496 | " all_rewards.append(episode_reward)\n",
497 | " episode_reward = 0\n",
498 | " \n",
499 | " if len(replay_buffer) > replay_initial:\n",
500 | " loss = compute_td_loss(batch_size)\n",
501 | " losses.append(loss.data[0])\n",
502 | " \n",
503 | " if frame_idx % 10000 == 0:\n",
504 | " plot(frame_idx, all_rewards, losses)\n",
505 | " \n",
506 | " if frame_idx % 1000 == 0:\n",
507 | " update_target(current_model, target_model)"
508 | ]
509 | },
510 | {
511 | "cell_type": "code",
512 | "execution_count": null,
513 | "metadata": {},
514 | "outputs": [],
515 | "source": []
516 | }
517 | ],
518 | "metadata": {
519 | "kernelspec": {
520 | "display_name": "Python 2",
521 | "language": "python",
522 | "name": "python2"
523 | },
524 | "language_info": {
525 | "codemirror_mode": {
526 | "name": "ipython",
527 | "version": 2
528 | },
529 | "file_extension": ".py",
530 | "mimetype": "text/x-python",
531 | "name": "python",
532 | "nbconvert_exporter": "python",
533 | "pygments_lexer": "ipython2",
534 | "version": "2.7.13"
535 | }
536 | },
537 | "nbformat": 4,
538 | "nbformat_minor": 2
539 | }
540 |
--------------------------------------------------------------------------------
/code/README.md:
--------------------------------------------------------------------------------
1 | # https://github.com/higgsfield/RL-Adventure
2 |
3 |
4 |
5 | # DQN Adventure: from Zero to State of the Art
6 |
7 |
8 |
9 |
10 | This is easy-to-follow step-by-step Deep Q Learning tutorial with clean readable code.
11 |
12 | The deep reinforcement learning community has made several independent improvements to the DQN algorithm. This tutorial presents latest extensions to the DQN algorithm in the following order:
13 |
14 | 1. Playing Atari with Deep Reinforcement Learning [[arxiv]](https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/1.dqn.ipynb)
15 | 2. Deep Reinforcement Learning with Double Q-learning [[arxiv]](https://arxiv.org/abs/1509.06461) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/2.double%20dqn.ipynb)
16 | 3. Dueling Network Architectures for Deep Reinforcement Learning [[arxiv]](https://arxiv.org/abs/1511.06581) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/3.dueling%20dqn.ipynb)
17 | 4. Prioritized Experience Replay [[arxiv]](https://arxiv.org/abs/1511.05952) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/4.prioritized%20dqn.ipynb)
18 | 5. Noisy Networks for Exploration [[arxiv]](https://arxiv.org/abs/1706.10295) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/5.noisy%20dqn.ipynb)
19 | 6. A Distributional Perspective on Reinforcement Learning [[arxiv]](https://arxiv.org/pdf/1707.06887.pdf) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/6.categorical%20dqn.ipynb)
20 | 7. Rainbow: Combining Improvements in Deep Reinforcement Learning [[arxiv]](https://arxiv.org/abs/1710.02298) [[code]](https://github.com/higgsfield/RL-Adventure/blob/master/7.rainbow%20dqn.ipynb)
21 |
22 |
23 | # Environments
24 | First, I recommend to use small test problems to run experiments quickly. Then, you can continue on environments with large observation space.
25 |
26 | - **CartPole** - classic RL environment can be solved on a single cpu
27 | - **Atari Pong** - the easiest atari environment, only takes ~ 1 million frames to converge, comparing with other atari games that take > 40 millions
28 | - **Atari others** - change hyperparameters, target network update frequency=10K, replay buffer size=1M
29 |
30 | # If you get stuck…
31 | - Remember you are not stuck unless you have spent more than a week on a single algorithm. It is perfectly normal if you do not have all the required knowledge of mathematics and CS. For example, you will need knowledge of the fundamentals of measure theory and statistics, especially the [Wasserstein metric](https://en.wikipedia.org/wiki/Wasserstein_metric) and [quantile regression](https://en.wikipedia.org/wiki/Quantile_regression). Statistical inference: [importance sampling](https://en.wikipedia.org/wiki/Importance_sampling). Data structures: [Segment Tree](https://leetcode.com/tag/segment-tree/) and [K-dimensional Tree](https://en.wikipedia.org/wiki/K-d_tree).
32 | - Carefully go through the paper. Try to see what is the problem the authors are solving. Understand a high-level idea of the approach, then read the code (skipping the proofs), and after go over the mathematical details and proofs.
33 |
34 | # Best RL courses
35 | - David Silver's course [link](http://www0.cs.ucl.ac.uk/staff/d.silver/web/Teaching.html)
36 | - Berkeley deep RL [link](http://rll.berkeley.edu/deeprlcourse/)
37 | - Practical RL [link](https://github.com/yandexdataschool/Practical_RL)
38 |
--------------------------------------------------------------------------------
/code/common/__init__.py:
--------------------------------------------------------------------------------
1 | import layers
2 | import wrappers
3 | import replay_buffer
--------------------------------------------------------------------------------
/code/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
--------------------------------------------------------------------------------
/code/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)
--------------------------------------------------------------------------------
/code/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.randint(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)
--------------------------------------------------------------------------------
/code/play_rainbow_dqn_CartPole-v1.py:
--------------------------------------------------------------------------------
1 | from common.layers import NoisyLinear
2 |
3 | import torch
4 | import torch.nn as nn
5 | import torch.nn.functional as F
6 | import torch.autograd as autograd
7 | import imageio
8 | import gym
9 |
10 |
11 | class RainbowDQN(nn.Module):
12 | def __init__(self, num_inputs, num_actions, num_atoms, Vmin, Vmax):
13 | super(RainbowDQN, self).__init__()
14 |
15 | self.num_inputs = num_inputs
16 | self.num_actions = num_actions
17 | self.num_atoms = num_atoms
18 | self.Vmin = Vmin
19 | self.Vmax = Vmax
20 |
21 | self.linear1 = nn.Linear(num_inputs, 32)
22 | self.linear2 = nn.Linear(32, 64)
23 |
24 | self.noisy_value1 = NoisyLinear(64, 64, use_cuda=USE_CUDA)
25 | self.noisy_value2 = NoisyLinear(64, self.num_atoms, use_cuda=USE_CUDA)
26 |
27 | self.noisy_advantage1 = NoisyLinear(64, 64, use_cuda=USE_CUDA)
28 | self.noisy_advantage2 = NoisyLinear(64, self.num_atoms * self.num_actions, use_cuda=USE_CUDA)
29 |
30 | def forward(self, x):
31 | batch_size = x.size(0)
32 |
33 | x = F.relu(self.linear1(x))
34 | x = F.relu(self.linear2(x))
35 |
36 | value = F.relu(self.noisy_value1(x))
37 | value = self.noisy_value2(value)
38 |
39 | advantage = F.relu(self.noisy_advantage1(x))
40 | advantage = self.noisy_advantage2(advantage)
41 |
42 | value = value.view(batch_size, 1, self.num_atoms)
43 | advantage = advantage.view(batch_size, self.num_actions, self.num_atoms)
44 |
45 | x = value + advantage - advantage.mean(1, keepdim=True)
46 | x = F.softmax(x.view(-1, self.num_atoms)).view(-1, self.num_actions, self.num_atoms)
47 |
48 | return x
49 |
50 | def reset_noise(self):
51 | self.noisy_value1.reset_noise()
52 | self.noisy_value2.reset_noise()
53 | self.noisy_advantage1.reset_noise()
54 | self.noisy_advantage2.reset_noise()
55 |
56 | def action(self, state):
57 | state = Variable(torch.FloatTensor(state).unsqueeze(0), volatile=True)
58 | dist = self.forward(state).data.cpu()
59 | dist = dist * torch.linspace(self.Vmin, self.Vmax, self.num_atoms)
60 | action = dist.sum(2).max(1)[1].numpy()[0]
61 | return action
62 |
63 |
64 | USE_CUDA = torch.cuda.is_available() # False
65 | Variable = lambda *args, **kwargs: autograd.Variable(*args, **kwargs).cuda() if USE_CUDA else autograd.Variable(*args,
66 | **kwargs)
67 |
68 | env_id = "CartPole-v1"
69 | env = gym.make(env_id)
70 |
71 | num_atoms = 51
72 | Vmin = -10
73 | Vmax = 10
74 |
75 | current_model = RainbowDQN(env.observation_space.shape[0], env.action_space.n, num_atoms, Vmin, Vmax)
76 | current_model.load_state_dict(torch.load('./save_model/27586-CartPole-v1_RainbowDQN.pkl'))
77 |
78 | if USE_CUDA:
79 | current_model = current_model.cuda()
80 |
81 | EPISODE = 10
82 | rendering = True
83 | frames = []
84 |
85 | for episode in range(1, EPISODE + 1):
86 | done = False
87 | score = 0
88 | state = env.reset()
89 |
90 | # for gif
91 | obs = env.render(mode='rgb_array')
92 | frames.append(obs)
93 |
94 | while True:
95 | if rendering:
96 | env.render()
97 |
98 | frames.append(env.render(mode='rgb_array'))
99 | action = current_model.action(state)
100 |
101 | next_state, reward, done, _ = env.step(action)
102 |
103 | state = next_state
104 | score += reward
105 | print(len(frames))
106 |
107 | if done:
108 | break
109 |
110 | string = '{}_{}_{}.gif'
111 | imageio.mimsave('./save_gif/' + string.format(env_id, "RainbowDQN", episode), frames, duration=0.0286)
112 |
113 | env.close()
114 |
--------------------------------------------------------------------------------
/code/play_rainbow_dqn_PongNoFrameskip-v4.py:
--------------------------------------------------------------------------------
1 | from common.wrappers import make_atari, wrap_deepmind, wrap_pytorch
2 | from common.layers import NoisyLinear
3 |
4 | import math
5 | import numpy as np
6 | import torch
7 | import torch.nn as nn
8 | import torch.nn.functional as F
9 | import torch.autograd as autograd
10 | import imageio
11 |
12 |
13 | # model은 학습모형에 맞는 모델을 class로 설계해주시면 됩니
14 | class RainbowDQN(nn.Module):
15 | def __init__(self, input_shape, num_actions, num_atoms, Vmin, Vmax):
16 | super(RainbowDQN, self).__init__()
17 |
18 | self.input_shape = input_shape
19 | self.num_actions = num_actions
20 | self.num_atoms = num_atoms
21 | self.Vmin = Vmin
22 | self.Vmax = Vmax
23 |
24 | self.features = nn.Sequential(
25 | # ((84 - 8 - 2*0) / 4) + 1 = 20
26 | nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4), # batch_size x 32 x 20 x 20
27 | nn.ReLU(),
28 |
29 | # ((20 - 4 - 2*0) / 2) + 1 = 9
30 | nn.Conv2d(32, 64, kernel_size=4, stride=2), # batch_size x 64 x 9 x 9
31 | nn.ReLU(),
32 |
33 | # ((9 - 3 - 2*0) / 2) + 1 = 4
34 | nn.Conv2d(64, 64, kernel_size=3, stride=1), # batch_size x 64 x 4 x 4
35 | nn.ReLU()
36 | )
37 |
38 | self.noisy_value1 = NoisyLinear(self.feature_size(), 512, use_cuda=USE_CUDA)
39 | self.noisy_value2 = NoisyLinear(512, self.num_atoms, use_cuda=USE_CUDA)
40 |
41 | self.noisy_advantage1 = NoisyLinear(self.feature_size(), 512, use_cuda=USE_CUDA)
42 | self.noisy_advantage2 = NoisyLinear(512, self.num_atoms * self.num_actions, use_cuda=USE_CUDA)
43 |
44 |
45 | def forward(self, x):
46 | batch_size = x.size(0)
47 |
48 | x = x / 255.
49 | x = self.features(x)
50 | x = x.view(batch_size, -1)
51 |
52 | value = F.relu(self.noisy_value1(x))
53 | value = self.noisy_value2(value)
54 |
55 | advantage = F.relu(self.noisy_advantage1(x))
56 | advantage = self.noisy_advantage2(advantage)
57 |
58 | value = value.view(batch_size, 1, self.num_atoms)
59 | advantage = advantage.view(batch_size, self.num_actions, self.num_atoms)
60 |
61 | x = value + advantage - advantage.mean(1, keepdim=True)
62 | x = F.softmax(x.view(-1, self.num_atoms)).view(-1, self.num_actions, self.num_atoms)
63 |
64 | return x
65 |
66 | def reset_noise(self):
67 | self.noisy_value1.reset_noise()
68 | self.noisy_value2.reset_noise()
69 | self.noisy_advantage1.reset_noise()
70 | self.noisy_advantage2.reset_noise()
71 |
72 | def feature_size(self):
73 | return self.features(autograd.Variable(torch.zeros(1, *self.input_shape))).view(1, -1).size(1)
74 |
75 | def act(self, state):
76 | state = Variable(torch.FloatTensor(np.float32(state)).unsqueeze(0), volatile=True)
77 | dist = self.forward(state).data.cpu()
78 | dist = dist * torch.linspace(self.Vmin, self.Vmax, self.num_atoms)
79 | action = dist.sum(2).max(1)[1].numpy()[0]
80 | return action
81 |
82 |
83 | USE_CUDA = torch.cuda.is_available() # False
84 | Variable = lambda *args, **kwargs: autograd.Variable(*args, **kwargs).cuda() if USE_CUDA else autograd.Variable(*args,
85 | **kwargs)
86 |
87 | env_id = "PongNoFrameskip-v4"
88 | env = make_atari(env_id)
89 | env = wrap_deepmind(env)
90 | env = wrap_pytorch(env)
91 |
92 | num_atoms = 51
93 | Vmin = -10
94 | Vmax = 10
95 |
96 |
97 | # 모델을 초기화하고 load하는 부분
98 | current_model = RainbowDQN(env.observation_space.shape, env.action_space.n, num_atoms, Vmin, Vmax)
99 | current_model.load_state_dict(torch.load('./save_model/999398-PongNoFrameskip-v4_RainbowDQN.pkl'))
100 |
101 | if USE_CUDA:
102 | current_model = current_model.cuda()
103 |
104 |
105 | EPISODE = 3
106 | rendering = True
107 | frames = []
108 |
109 | for episode in range(1, EPISODE + 1):
110 | done = False
111 | score = 0
112 | state = env.reset()
113 |
114 | # for gif
115 | obs = env.render(mode='rgb_array')
116 | frames.append(obs)
117 |
118 | while True:
119 | if rendering:
120 | env.render()
121 |
122 | frames.append(env.render(mode='rgb_array'))
123 | action = current_model.act(state)
124 |
125 | next_state, reward, done, _ = env.step(action)
126 |
127 | state = next_state
128 | score += reward
129 |
130 | if done:
131 | break
132 |
133 | string = '{}_{}_{}.gif'
134 | imageio.mimsave('./save_gif/' + string.format(env_id, "RainbowDQN", episode), frames, duration=0.0286)
135 |
136 | env.close()
--------------------------------------------------------------------------------
/code/pytorch0.4.1/4.prioritized dqn.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "metadata": {},
7 | "outputs": [],
8 | "source": [
9 | "import math, random\n",
10 | "\n",
11 | "import gym\n",
12 | "import numpy as np\n",
13 | "\n",
14 | "import torch\n",
15 | "import torch.nn as nn\n",
16 | "import torch.optim as optim\n",
17 | "import torch.nn.functional as F"
18 | ]
19 | },
20 | {
21 | "cell_type": "code",
22 | "execution_count": null,
23 | "metadata": {},
24 | "outputs": [],
25 | "source": [
26 | "from IPython.display import clear_output\n",
27 | "import matplotlib.pyplot as plt\n",
28 | "%matplotlib inline"
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {},
34 | "source": [
35 | "Use Cuda
"
36 | ]
37 | },
38 | {
39 | "cell_type": "code",
40 | "execution_count": null,
41 | "metadata": {},
42 | "outputs": [],
43 | "source": [
44 | "use_cuda = torch.cuda.is_available()\n",
45 | "device = torch.device(\"cuda\" if use_cuda else \"cpu\")"
46 | ]
47 | },
48 | {
49 | "cell_type": "markdown",
50 | "metadata": {},
51 | "source": [
52 | "Prioritized Replay Buffer
"
53 | ]
54 | },
55 | {
56 | "cell_type": "markdown",
57 | "metadata": {},
58 | "source": [
59 | "Prioritized Experience Replay: https://arxiv.org/abs/1511.05952
"
60 | ]
61 | },
62 | {
63 | "cell_type": "code",
64 | "execution_count": null,
65 | "metadata": {},
66 | "outputs": [],
67 | "source": [
68 | "class NaivePrioritizedBuffer(object):\n",
69 | " def __init__(self, capacity, prob_alpha=0.6):\n",
70 | " self.prob_alpha = prob_alpha\n",
71 | " self.capacity = capacity\n",
72 | " self.buffer = []\n",
73 | " self.pos = 0\n",
74 | " self.priorities = np.zeros((capacity,), dtype=np.float32)\n",
75 | " \n",
76 | " def push(self, state, action, reward, next_state, done):\n",
77 | " assert state.ndim == next_state.ndim\n",
78 | " state = np.expand_dims(state, 0)\n",
79 | " next_state = np.expand_dims(next_state, 0)\n",
80 | " \n",
81 | " max_prio = self.priorities.max() if self.buffer else 1.0\n",
82 | " \n",
83 | " if len(self.buffer) < self.capacity:\n",
84 | " self.buffer.append((state, action, reward, next_state, done))\n",
85 | " else:\n",
86 | " self.buffer[self.pos] = (state, action, reward, next_state, done)\n",
87 | " \n",
88 | " self.priorities[self.pos] = max_prio\n",
89 | " self.pos = (self.pos + 1) % self.capacity\n",
90 | " \n",
91 | " def sample(self, batch_size, beta=0.4):\n",
92 | " if len(self.buffer) == self.capacity:\n",
93 | " prios = self.priorities\n",
94 | " else:\n",
95 | " prios = self.priorities[:self.pos]\n",
96 | " \n",
97 | " probs = prios ** self.prob_alpha\n",
98 | " probs /= probs.sum()\n",
99 | " \n",
100 | " indices = np.random.choice(len(self.buffer), batch_size, p=probs)\n",
101 | " samples = [self.buffer[idx] for idx in indices]\n",
102 | " \n",
103 | " total = len(self.buffer)\n",
104 | " weights = (total * probs[indices]) ** (-beta)\n",
105 | " weights /= weights.max()\n",
106 | " weights = np.array(weights, dtype=np.float32)\n",
107 | " \n",
108 | " batch = list(zip(*samples))\n",
109 | " states = np.concatenate(batch[0])\n",
110 | " actions = batch[1]\n",
111 | " rewards = batch[2]\n",
112 | " next_states = np.concatenate(batch[3])\n",
113 | " dones = batch[4]\n",
114 | " \n",
115 | " return states, actions, rewards, next_states, dones, indices, weights\n",
116 | " \n",
117 | " def update_priorities(self, batch_indices, batch_priorities):\n",
118 | " for idx, prio in zip(batch_indices, batch_priorities):\n",
119 | " self.priorities[idx] = prio\n",
120 | "\n",
121 | " def __len__(self):\n",
122 | " return len(self.buffer)"
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": null,
128 | "metadata": {},
129 | "outputs": [],
130 | "source": [
131 | "beta_start = 0.4\n",
132 | "beta_frames = 1000 \n",
133 | "beta_by_frame = lambda frame_idx: min(1.0, beta_start + frame_idx * (1.0 - beta_start) / beta_frames)"
134 | ]
135 | },
136 | {
137 | "cell_type": "code",
138 | "execution_count": null,
139 | "metadata": {},
140 | "outputs": [],
141 | "source": [
142 | "plt.plot([beta_by_frame(i) for i in range(10000)])"
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "metadata": {},
148 | "source": [
149 | "Cart Pole Environment
"
150 | ]
151 | },
152 | {
153 | "cell_type": "code",
154 | "execution_count": null,
155 | "metadata": {},
156 | "outputs": [],
157 | "source": [
158 | "env_id = \"CartPole-v0\"\n",
159 | "env = gym.make(env_id)"
160 | ]
161 | },
162 | {
163 | "cell_type": "code",
164 | "execution_count": null,
165 | "metadata": {},
166 | "outputs": [],
167 | "source": [
168 | "epsilon_start = 1.0\n",
169 | "epsilon_final = 0.01\n",
170 | "epsilon_decay = 500\n",
171 | "\n",
172 | "epsilon_by_frame = lambda frame_idx: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1. * frame_idx / epsilon_decay)"
173 | ]
174 | },
175 | {
176 | "cell_type": "code",
177 | "execution_count": null,
178 | "metadata": {},
179 | "outputs": [],
180 | "source": [
181 | "plt.plot([epsilon_by_frame(i) for i in range(10000)])"
182 | ]
183 | },
184 | {
185 | "cell_type": "markdown",
186 | "metadata": {},
187 | "source": [
188 | "Deep Q Network
"
189 | ]
190 | },
191 | {
192 | "cell_type": "code",
193 | "execution_count": null,
194 | "metadata": {},
195 | "outputs": [],
196 | "source": [
197 | "class DQN(nn.Module):\n",
198 | " def __init__(self, num_inputs, num_actions):\n",
199 | " super(DQN, self).__init__()\n",
200 | " \n",
201 | " self.layers = nn.Sequential(\n",
202 | " nn.Linear(env.observation_space.shape[0], 128),\n",
203 | " nn.ReLU(),\n",
204 | " nn.Linear(128, 128),\n",
205 | " nn.ReLU(),\n",
206 | " nn.Linear(128, env.action_space.n)\n",
207 | " )\n",
208 | " \n",
209 | " def forward(self, x):\n",
210 | " return self.layers(x)\n",
211 | " \n",
212 | " def act(self, state, epsilon):\n",
213 | " if random.random() > epsilon:\n",
214 | " with torch.no_grad():\n",
215 | " state = torch.FloatTensor(state).unsqueeze(0).to(device)\n",
216 | " q_value = self.forward(state)\n",
217 | " action = q_value.max(1)[1].item()\n",
218 | " else:\n",
219 | " action = random.randrange(env.action_space.n)\n",
220 | " return action"
221 | ]
222 | },
223 | {
224 | "cell_type": "code",
225 | "execution_count": null,
226 | "metadata": {},
227 | "outputs": [],
228 | "source": [
229 | "current_model = DQN(env.observation_space.shape[0], env.action_space.n).to(device)\n",
230 | "target_model = DQN(env.observation_space.shape[0], env.action_space.n).to(device)\n",
231 | " \n",
232 | "optimizer = optim.Adam(current_model.parameters())\n",
233 | "\n",
234 | "replay_buffer = NaivePrioritizedBuffer(100000)"
235 | ]
236 | },
237 | {
238 | "cell_type": "markdown",
239 | "metadata": {},
240 | "source": [
241 | "Synchronize current policy net and target net
"
242 | ]
243 | },
244 | {
245 | "cell_type": "code",
246 | "execution_count": null,
247 | "metadata": {},
248 | "outputs": [],
249 | "source": [
250 | "def update_target(current_model, target_model):\n",
251 | " target_model.load_state_dict(current_model.state_dict())"
252 | ]
253 | },
254 | {
255 | "cell_type": "code",
256 | "execution_count": null,
257 | "metadata": {},
258 | "outputs": [],
259 | "source": [
260 | "update_target(current_model, target_model)"
261 | ]
262 | },
263 | {
264 | "cell_type": "markdown",
265 | "metadata": {},
266 | "source": [
267 | "Computing Temporal Difference Loss
"
268 | ]
269 | },
270 | {
271 | "cell_type": "code",
272 | "execution_count": null,
273 | "metadata": {},
274 | "outputs": [],
275 | "source": [
276 | "def compute_td_loss(batch_size, beta):\n",
277 | " state, action, reward, next_state, done, indices, weights = replay_buffer.sample(batch_size, beta) \n",
278 | "\n",
279 | " state = torch.FloatTensor(np.float32(state)).to(device)\n",
280 | " next_state = torch.FloatTensor(np.float32(next_state)).to(device)\n",
281 | " action = torch.LongTensor(action).to(device)\n",
282 | " reward = torch.FloatTensor(reward).to(device)\n",
283 | " done = torch.FloatTensor(done).to(device)\n",
284 | " weights = torch.FloatTensor(weights).to(device)\n",
285 | "\n",
286 | " q_values = current_model(state)\n",
287 | " with torch.no_grad():\n",
288 | " next_q_values = target_model(next_state)\n",
289 | "\n",
290 | " q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)\n",
291 | " with torch.no_grad():\n",
292 | " next_q_value = next_q_values.max(1)[0]\n",
293 | " expected_q_value = reward + gamma * next_q_value * (1 - done)\n",
294 | " \n",
295 | " loss = (q_value - expected_q_value.detach()).pow(2) * weights\n",
296 | " prios = loss + 1e-5\n",
297 | " loss = loss.mean()\n",
298 | " \n",
299 | " optimizer.zero_grad()\n",
300 | " loss.backward()\n",
301 | " replay_buffer.update_priorities(indices, prios.data.cpu().numpy())\n",
302 | " optimizer.step()\n",
303 | " \n",
304 | " return loss"
305 | ]
306 | },
307 | {
308 | "cell_type": "code",
309 | "execution_count": null,
310 | "metadata": {},
311 | "outputs": [],
312 | "source": [
313 | "def plot(frame_idx, rewards, losses):\n",
314 | " clear_output(True)\n",
315 | " plt.figure(figsize=(20,5))\n",
316 | " plt.subplot(131)\n",
317 | " plt.title('frame %s. reward: %s' % (frame_idx, np.mean(rewards[-10:])))\n",
318 | " plt.plot(rewards)\n",
319 | " plt.subplot(132)\n",
320 | " plt.title('loss')\n",
321 | " plt.plot(losses)\n",
322 | " plt.show()"
323 | ]
324 | },
325 | {
326 | "cell_type": "markdown",
327 | "metadata": {},
328 | "source": [
329 | "Training
"
330 | ]
331 | },
332 | {
333 | "cell_type": "code",
334 | "execution_count": null,
335 | "metadata": {},
336 | "outputs": [],
337 | "source": [
338 | "num_frames = 10000\n",
339 | "batch_size = 32\n",
340 | "gamma = 0.99\n",
341 | "\n",
342 | "losses = []\n",
343 | "all_rewards = []\n",
344 | "episode_reward = 0\n",
345 | "\n",
346 | "state = env.reset()\n",
347 | "for frame_idx in range(1, num_frames + 1):\n",
348 | " epsilon = epsilon_by_frame(frame_idx)\n",
349 | " action = current_model.act(state, epsilon)\n",
350 | " \n",
351 | " next_state, reward, done, _ = env.step(action)\n",
352 | " replay_buffer.push(state, action, reward, next_state, done)\n",
353 | " \n",
354 | " state = next_state\n",
355 | " episode_reward += reward\n",
356 | " \n",
357 | " if done:\n",
358 | " state = env.reset()\n",
359 | " all_rewards.append(episode_reward)\n",
360 | " episode_reward = 0\n",
361 | " \n",
362 | " if len(replay_buffer) > batch_size:\n",
363 | " beta = beta_by_frame(frame_idx)\n",
364 | " loss = compute_td_loss(batch_size, beta)\n",
365 | " losses.append(loss.item())\n",
366 | " \n",
367 | " if frame_idx % 200 == 0:\n",
368 | " plot(frame_idx, all_rewards, losses)\n",
369 | " \n",
370 | " if frame_idx % 1000 == 0:\n",
371 | " update_target(current_model, target_model)"
372 | ]
373 | },
374 | {
375 | "cell_type": "markdown",
376 | "metadata": {},
377 | "source": [
378 | "
"
379 | ]
380 | },
381 | {
382 | "cell_type": "markdown",
383 | "metadata": {},
384 | "source": [
385 | "Atari Environment
"
386 | ]
387 | },
388 | {
389 | "cell_type": "code",
390 | "execution_count": null,
391 | "metadata": {},
392 | "outputs": [],
393 | "source": [
394 | "from common.wrappers import make_atari, wrap_deepmind, wrap_pytorch"
395 | ]
396 | },
397 | {
398 | "cell_type": "code",
399 | "execution_count": null,
400 | "metadata": {},
401 | "outputs": [],
402 | "source": [
403 | "env_id = \"PongNoFrameskip-v4\"\n",
404 | "env = make_atari(env_id)\n",
405 | "env = wrap_deepmind(env)\n",
406 | "env = wrap_pytorch(env)"
407 | ]
408 | },
409 | {
410 | "cell_type": "code",
411 | "execution_count": null,
412 | "metadata": {},
413 | "outputs": [],
414 | "source": [
415 | "class CnnDQN(nn.Module):\n",
416 | " def __init__(self, input_shape, num_actions):\n",
417 | " super(CnnDQN, self).__init__()\n",
418 | " \n",
419 | " self.input_shape = input_shape\n",
420 | " self.num_actions = num_actions\n",
421 | " \n",
422 | " self.features = nn.Sequential(\n",
423 | " nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),\n",
424 | " nn.ReLU(),\n",
425 | " nn.Conv2d(32, 64, kernel_size=4, stride=2),\n",
426 | " nn.ReLU(),\n",
427 | " nn.Conv2d(64, 64, kernel_size=3, stride=1),\n",
428 | " nn.ReLU()\n",
429 | " )\n",
430 | " \n",
431 | " self.fc = nn.Sequential(\n",
432 | " nn.Linear(self.feature_size(), 512),\n",
433 | " nn.ReLU(),\n",
434 | " nn.Linear(512, self.num_actions)\n",
435 | " )\n",
436 | " \n",
437 | " def forward(self, x):\n",
438 | " x = self.features(x)\n",
439 | " x = x.view(x.size(0), -1)\n",
440 | " x = self.fc(x)\n",
441 | " return x\n",
442 | " \n",
443 | " def feature_size(self):\n",
444 | " return self.features(torch.zeros(1, *self.input_shape)).view(1, -1).size(1)\n",
445 | " \n",
446 | " def act(self, state, epsilon):\n",
447 | " if random.random() > epsilon:\n",
448 | " with torch.no_grad():\n",
449 | " state = torch.FloatTensor(np.float32(state)).unsqueeze(0).to(device)\n",
450 | " q_value = self.forward(state)\n",
451 | " action = q_value.max(1)[1].item()\n",
452 | " else:\n",
453 | " action = random.randrange(env.action_space.n)\n",
454 | " return action"
455 | ]
456 | },
457 | {
458 | "cell_type": "code",
459 | "execution_count": null,
460 | "metadata": {},
461 | "outputs": [],
462 | "source": [
463 | "current_model = CnnDQN(env.observation_space.shape, env.action_space.n).to(device)\n",
464 | "target_model = CnnDQN(env.observation_space.shape, env.action_space.n).to(device)\n",
465 | " \n",
466 | "optimizer = optim.Adam(current_model.parameters(), lr=0.0001)\n",
467 | "\n",
468 | "replay_initial = 10000\n",
469 | "replay_buffer = NaivePrioritizedBuffer(100000)\n",
470 | "\n",
471 | "update_target(current_model, target_model)"
472 | ]
473 | },
474 | {
475 | "cell_type": "markdown",
476 | "metadata": {},
477 | "source": [
478 | "Epsilon greedy exploration
"
479 | ]
480 | },
481 | {
482 | "cell_type": "code",
483 | "execution_count": null,
484 | "metadata": {},
485 | "outputs": [],
486 | "source": [
487 | "epsilon_start = 1.0\n",
488 | "epsilon_final = 0.01\n",
489 | "epsilon_decay = 30000\n",
490 | "\n",
491 | "epsilon_by_frame = lambda frame_idx: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1. * frame_idx / epsilon_decay)"
492 | ]
493 | },
494 | {
495 | "cell_type": "code",
496 | "execution_count": null,
497 | "metadata": {},
498 | "outputs": [],
499 | "source": [
500 | "plt.plot([epsilon_by_frame(i) for i in range(1000000)])"
501 | ]
502 | },
503 | {
504 | "cell_type": "markdown",
505 | "metadata": {},
506 | "source": [
507 | "Beta Prioritized Experience Replay
"
508 | ]
509 | },
510 | {
511 | "cell_type": "code",
512 | "execution_count": null,
513 | "metadata": {},
514 | "outputs": [],
515 | "source": [
516 | "beta_start = 0.4\n",
517 | "beta_frames = 100000\n",
518 | "beta_by_frame = lambda frame_idx: min(1.0, beta_start + frame_idx * (1.0 - beta_start) / beta_frames)"
519 | ]
520 | },
521 | {
522 | "cell_type": "code",
523 | "execution_count": null,
524 | "metadata": {},
525 | "outputs": [],
526 | "source": [
527 | "plt.plot([beta_by_frame(i) for i in range(1000000)])"
528 | ]
529 | },
530 | {
531 | "cell_type": "markdown",
532 | "metadata": {},
533 | "source": [
534 | "Training
"
535 | ]
536 | },
537 | {
538 | "cell_type": "code",
539 | "execution_count": null,
540 | "metadata": {},
541 | "outputs": [],
542 | "source": [
543 | "from tqdm import trange\n",
544 | "\n",
545 | "num_frames = 1000000\n",
546 | "batch_size = 32\n",
547 | "gamma = 0.99\n",
548 | "\n",
549 | "losses = []\n",
550 | "all_rewards = []\n",
551 | "episode_reward = 0\n",
552 | "\n",
553 | "state = env.reset()\n",
554 | "for frame_idx in trange(1, num_frames + 1):\n",
555 | " epsilon = epsilon_by_frame(frame_idx)\n",
556 | " action = current_model.act(state, epsilon)\n",
557 | " \n",
558 | " next_state, reward, done, _ = env.step(action)\n",
559 | " replay_buffer.push(state, action, reward, next_state, done)\n",
560 | " \n",
561 | " state = next_state\n",
562 | " episode_reward += reward\n",
563 | " \n",
564 | " if done:\n",
565 | " state = env.reset()\n",
566 | " all_rewards.append(episode_reward)\n",
567 | " episode_reward = 0\n",
568 | " \n",
569 | " if len(replay_buffer) > replay_initial:\n",
570 | " beta = beta_by_frame(frame_idx)\n",
571 | " loss = compute_td_loss(batch_size, beta)\n",
572 | " losses.append(loss.item())\n",
573 | " \n",
574 | " if frame_idx % 10000 == 0:\n",
575 | " plot(frame_idx, all_rewards, losses)\n",
576 | " \n",
577 | " if frame_idx % 1000 == 0:\n",
578 | " update_target(current_model, target_model)\n",
579 | " "
580 | ]
581 | },
582 | {
583 | "cell_type": "code",
584 | "execution_count": null,
585 | "metadata": {},
586 | "outputs": [],
587 | "source": []
588 | }
589 | ],
590 | "metadata": {
591 | "kernelspec": {
592 | "display_name": "Python 3",
593 | "language": "python",
594 | "name": "python3"
595 | },
596 | "language_info": {
597 | "codemirror_mode": {
598 | "name": "ipython",
599 | "version": 3
600 | },
601 | "file_extension": ".py",
602 | "mimetype": "text/x-python",
603 | "name": "python",
604 | "nbconvert_exporter": "python",
605 | "pygments_lexer": "ipython3",
606 | "version": "3.6.4"
607 | }
608 | },
609 | "nbformat": 4,
610 | "nbformat_minor": 2
611 | }
612 |
--------------------------------------------------------------------------------
/code/save_gif/CartPole-v1_RainbowDQN_10.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/code/save_gif/CartPole-v1_RainbowDQN_10.gif
--------------------------------------------------------------------------------
/code/save_gif/PongNoFrameskip-v4_RainbowDQN_3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/code/save_gif/PongNoFrameskip-v4_RainbowDQN_3.gif
--------------------------------------------------------------------------------
/code/save_model/27586-CartPole-v1_RainbowDQN.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/code/save_model/27586-CartPole-v1_RainbowDQN.pkl
--------------------------------------------------------------------------------
/code/save_model/999398-PongNoFrameskip-v4_RainbowDQN.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/code/save_model/999398-PongNoFrameskip-v4_RainbowDQN.pkl
--------------------------------------------------------------------------------
/slide/pt1_dqn_double_duel.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/slide/pt1_dqn_double_duel.pdf
--------------------------------------------------------------------------------
/slide/pt2_per_noisynet.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/slide/pt2_per_noisynet.pdf
--------------------------------------------------------------------------------
/slide/pt3_C51_distributional.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/slide/pt3_C51_distributional.pdf
--------------------------------------------------------------------------------
/slide/pt4_rainbow.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/slide/pt4_rainbow.pdf
--------------------------------------------------------------------------------
/slide/template.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hongdam/pycon2018-RL_Adventure/d3dab89e1958d908ed399bdc873a7ed02d1fd157/slide/template.pptx
--------------------------------------------------------------------------------