├── 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 | 36 | 37 | 38 | 39 | 42 | 45 | 46 |
CartPole-v1PongNoFrameskip-v4
40 | 41 | 43 | 44 |
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 --------------------------------------------------------------------------------