150行代碼實現DQN算法玩CartPole

1 前言

終於到了DQN系列真正的實戰了。今天我們將一步一步的告訴大家如何用最短的代碼實現基本的DQN算法,並且完成基本的RL任務。這恐怕也將是你在網上能找到的最詳盡的DQN實戰教程,當然了,代碼也會是最短的。

在本次實戰中,我們不選擇Atari遊戲,而使用OpenAI Gym中的傳統增強學習任務之一CartPole作爲練手的任務。之所以不選擇Atari遊戲,有兩點原因:一個是訓練Atari要很久,一個是Atari的一些圖像的處理需要更多的tricks。而CartPole任務則比較簡單。

上圖就是CartPole的基本任務示意圖,基本要求就是控制下面的cart移動使連接在上面的杆保持垂直不倒。這個任務簡化到只有兩個離散動作,要麼向左用力,要麼向右用力。而state狀態就是這個杆的位置和速度。

今天我們就要用DQN來解決這個問題。

2 完成前提

雖然之前的文章已經說了很多,但是爲了完成這個練習,大家還是需要一定的基礎的:

  • 熟悉Python編程,能夠使用Python基本的語法
  • 對Tensorflow有一定的瞭解,知道基本的使用
  • 知道如何使用OpenAI Gym
  • 瞭解基本的神經網絡MLP
  • 理解DQN算法

看起來似乎是蠻有難度,但是如果你一步一步看過來的話,這些前提都很容易滿足。

3 開始

先上一下最後的測試效果圖:

也就是100%解決問題!

鏈接:gym.openai.com/evaluati

我們將要實現的是最基本的DQN,也就是NIPS 13版本的DQN:

面對CartPole問題,我們進一步簡化:
  1. 無需預處理Preprocessing。也就是直接獲取觀察Observation作爲狀態state輸入。
  2. 只使用最基本的MLP神經網絡,而不使用卷積神經網絡。

3.1 編寫主程序

按照至上而下的編程方式,我們先寫主函數用來執行這個實驗,然後再具體編寫DQN算法實現。

先import所需的庫:

import gym
import tensorflow as tf 
import numpy as np 
import random
from collections import deque

編寫主函數如下:

# Hyper Parameters
ENV_NAME = 'CartPole-v0'
EPISODE = 10000 # Episode limitation
STEP = 300 # Step limitation in an episode

def main():
  # initialize OpenAI Gym env and dqn agent
  env = gym.make(ENV_NAME)
  agent = DQN(env)

  for episode in xrange(EPISODE):
    # initialize task
    state = env.reset()
    # Train
    for step in xrange(STEP):
      action = agent.egreedy_action(state) # e-greedy action for train
      next_state,reward,done,_ = env.step(action)
      # Define reward for agent
      reward_agent = -1 if done else 0.1
      agent.perceive(state,action,reward,next_state,done)
      state = next_state
      if done:
        break

if __name__ == '__main__':
  main()

我們將編寫一個DQN的類,DQN的一切都將封裝在裏面。在主函數中,我們只需調用

agent.egreedy_action(state) # 獲取包含隨機的動作
agent.perceive(state,action,reward,next_state,done) # 感知信息

本質上就是一個輸出動作,一個輸入狀態。當然我們這裏輸入的是整個transition。

然後環境自己執行動作,輸出新的狀態:

next_state,reward,done,_ = env.step(action)

然後整個過程就反覆循環,一個episode結束,就再來一個。

這就是訓練的過程。

但只有訓練顯然不夠,我們還需要測試。因此,在main()的最後,我們再加上幾行的測試代碼:

# Test every 100 episodes
    if episode % 100 == 0:
      total_reward = 0
      for i in xrange(TEST):
        state = env.reset()
        for j in xrange(STEP):
          env.render()
          action = agent.action(state) # direct action for test
          state,reward,done,_ = env.step(action)
          total_reward += reward
          if done:
            break
      ave_reward = total_reward/TEST
      print 'episode: ',episode,'Evaluation Average Reward:',ave_reward
      if ave_reward >= 200:
        break

測試中唯一的不同就是我們使用

action = agent.action(state)

來獲取動作,也就是完全沒有隨機性,只根據神經網絡來輸出,沒有探索,同時這裏也就不再perceive輸入信息來訓練。

OK,這就是基本的主函數。接下來就是實現DQN

3.2 DQN實現

3.2.1 編寫基本DQN類的結構

class DQN():
  # DQN Agent
  def __init__(self, env): #初始化

  def create_Q_network(self): #創建Q網絡

  def create_training_method(self): #創建訓練方法

  def perceive(self,state,action,reward,next_state,done): #感知存儲信息

  def train_Q_network(self): #訓練網絡

  def egreedy_action(self,state): #輸出帶隨機的動作

  def action(self,state): #輸出動作

主要只需要以上幾個函數。上面已經註釋得很清楚,這裏不再加以解釋。

我們知道,我們的DQN一個很重要的功能就是要能存儲數據,然後在訓練的時候minibatch出來。所以,我們需要構造一個存儲機制。這裏使用deque來實現。

self.replay_buffer = deque()

3.2.2 初始化

  def __init__(self, env):
    # init experience replay
    self.replay_buffer = deque()
    # init some parameters
    self.time_step = 0
    self.epsilon = INITIAL_EPSILON
    self.state_dim = env.observation_space.shape[0]
    self.action_dim = env.action_space.n

    self.create_Q_network()
    self.create_training_method()

    # Init session
    self.session = tf.InteractiveSession()
    self.session.run(tf.initialize_all_variables())

這裏要注意一點就是egreedy的epsilon是不斷變小的,也就是隨機性不斷變小。怎麼理解呢?就是一開始需要更多的探索,所以動作偏隨機,慢慢的我們需要動作能夠有效,因此減少隨機。

3.2.3 創建Q網絡

我們這裏創建最基本的MLP,中間層設置爲20:

  def create_Q_network(self):
    # network weights
    W1 = self.weight_variable([self.state_dim,20])
    b1 = self.bias_variable([20])
    W2 = self.weight_variable([20,self.action_dim])
    b2 = self.bias_variable([self.action_dim])
    # input layer
    self.state_input = tf.placeholder("float",[None,self.state_dim])
    # hidden layers
    h_layer = tf.nn.relu(tf.matmul(self.state_input,W1) + b1)
    # Q Value layer
    self.Q_value = tf.matmul(h_layer,W2) + b2

  def weight_variable(self,shape):
    initial = tf.truncated_normal(shape)
    return tf.Variable(initial)

  def bias_variable(self,shape):
    initial = tf.constant(0.01, shape = shape)
    return tf.Variable(initial)

只有一個隱層,然後使用relu非線性單元。相信對MLP有了解的知友看上面的代碼很easy!要注意的是我們state 輸入的格式,因爲使用minibatch,所以格式是[None,state_dim]

3.2.4 編寫perceive函數

  def perceive(self,state,action,reward,next_state,done):
    one_hot_action = np.zeros(self.action_dim)
    one_hot_action[action] = 1
     self.replay_buffer.append((state,one_hot_action,reward,next_state,done))
    if len(self.replay_buffer) > REPLAY_SIZE:
      self.replay_buffer.popleft()

    if len(self.replay_buffer) > BATCH_SIZE:
      self.train_Q_network()

這裏需要注意的一點就是動作格式的轉換。我們在神經網絡中使用的是one hot key的形式,而在OpenAI Gym中則使用單值。什麼意思呢?比如我們輸出動作是1,那麼對應的one hot形式就是[0,1],如果輸出動作是0,那麼one hot 形式就是[1,0]。這樣做的目的是爲了之後更好的進行計算。

在perceive中一個最主要的事情就是存儲。然後根據情況進行train。這裏我們要求只要存儲的數據大於Batch的大小就開始訓練。

3.2.5 編寫action輸出函數

def egreedy_action(self,state):
    Q_value = self.Q_value.eval(feed_dict = {
      self.state_input:[state]
      })[0]
    if random.random() <= self.epsilon:
      return random.randint(0,self.action_dim - 1)
    else:
      return np.argmax(Q_value)

    self.epsilon -= (INITIAL_EPSILON - FINAL_EPSILON)/10000

def action(self,state):
    return np.argmax(self.Q_value.eval(feed_dict = {
      self.state_input:[state]
      })[0])

區別之前已經說過,一個是根據情況輸出隨機動作,一個是根據神經網絡輸出。由於神經網絡輸出的是每一個動作的Q值,因此我們選擇最大的那個Q值對應的動作輸出。

3.2.6 編寫training method函數

  def create_training_method(self):
    self.action_input = tf.placeholder("float",[None,self.action_dim]) # one hot presentation
    self.y_input = tf.placeholder("float",[None])
    Q_action = tf.reduce_sum(tf.mul(self.Q_value,self.action_input),reduction_indices = 1)
    self.cost = tf.reduce_mean(tf.square(self.y_input - Q_action))
    self.optimizer = tf.train.AdamOptimizer(0.0001).minimize(self.cost)

這裏的y_input就是target Q值。我們這裏採用Adam優化器,其實隨便選擇一個必然SGD,RMSProp都是可以的。可能比較不好理解的就是Q值的計算。這裏大家記住動作輸入是one hot key的形式,因此將Q_value和action_input向量相乘得到的就是這個動作對應的Q_value。然後用reduce_sum將數據維度壓成一維。

3.2.7 編寫training函數

def train_Q_network(self):
    self.time_step += 1
    # Step 1: obtain random minibatch from replay memory
    minibatch = random.sample(self.replay_buffer,BATCH_SIZE)
    state_batch = [data[0] for data in minibatch]
    action_batch = [data[1] for data in minibatch]
    reward_batch = [data[2] for data in minibatch]
    next_state_batch = [data[3] for data in minibatch]

    # Step 2: calculate y
    y_batch = []
    Q_value_batch = self.Q_value.eval(feed_dict={self.state_input:next_state_batch})
    for i in range(0,BATCH_SIZE):
      done = minibatch[i][4]
      if done:
        y_batch.append(reward_batch[i])
      else :
        y_batch.append(reward_batch[i] + GAMMA * np.max(Q_value_batch[i]))

    self.optimizer.run(feed_dict={
      self.y_input:y_batch,
      self.action_input:action_batch,
      self.state_input:state_batch
      })

首先就是進行minibatch的工作,然後根據batch計算y_batch。最後就是用optimizer進行優化。

4 整個程序

以上便是編寫DQN的全過程了。是不是很簡單呢,下面再把整個程序放出如下:

import gym
import tensorflow as tf
import numpy as np
import random
from collections import deque

# Hyper Parameters for DQN
GAMMA = 0.9 # discount factor for target Q
INITIAL_EPSILON = 0.5 # starting value of epsilon
FINAL_EPSILON = 0.01 # final value of epsilon
REPLAY_SIZE = 10000 # experience replay buffer size
BATCH_SIZE = 32 # size of minibatch

class DQN():
  # DQN Agent
  def __init__(self, env):
    # init experience replay
    self.replay_buffer = deque()
    # init some parameters
    self.time_step = 0
    self.epsilon = INITIAL_EPSILON
    self.state_dim = env.observation_space.shape[0]
    self.action_dim = env.action_space.n

    self.create_Q_network()
    self.create_training_method()

    # Init session
    self.session = tf.InteractiveSession()
    self.session.run(tf.initialize_all_variables())

  def create_Q_network(self):
    # network weights
    W1 = self.weight_variable([self.state_dim,20])
    b1 = self.bias_variable([20])
    W2 = self.weight_variable([20,self.action_dim])
    b2 = self.bias_variable([self.action_dim])
    # input layer
    self.state_input = tf.placeholder("float",[None,self.state_dim])
    # hidden layers
    h_layer = tf.nn.relu(tf.matmul(self.state_input,W1) + b1)
    # Q Value layer
    self.Q_value = tf.matmul(h_layer,W2) + b2

  def create_training_method(self):
    self.action_input = tf.placeholder("float",[None,self.action_dim]) # one hot presentation
    self.y_input = tf.placeholder("float",[None])
    Q_action = tf.reduce_sum(tf.mul(self.Q_value,self.action_input),reduction_indices = 1)
    self.cost = tf.reduce_mean(tf.square(self.y_input - Q_action))
    self.optimizer = tf.train.AdamOptimizer(0.0001).minimize(self.cost)

  def perceive(self,state,action,reward,next_state,done):
    one_hot_action = np.zeros(self.action_dim)
    one_hot_action[action] = 1
    self.replay_buffer.append((state,one_hot_action,reward,next_state,done))
    if len(self.replay_buffer) > REPLAY_SIZE:
      self.replay_buffer.popleft()

    if len(self.replay_buffer) > BATCH_SIZE:
      self.train_Q_network()

  def train_Q_network(self):
    self.time_step += 1
    # Step 1: obtain random minibatch from replay memory
    minibatch = random.sample(self.replay_buffer,BATCH_SIZE)
    state_batch = [data[0] for data in minibatch]
    action_batch = [data[1] for data in minibatch]
    reward_batch = [data[2] for data in minibatch]
    next_state_batch = [data[3] for data in minibatch]

    # Step 2: calculate y
    y_batch = []
    Q_value_batch = self.Q_value.eval(feed_dict={self.state_input:next_state_batch})
    for i in range(0,BATCH_SIZE):
      done = minibatch[i][4]
      if done:
        y_batch.append(reward_batch[i])
      else :
        y_batch.append(reward_batch[i] + GAMMA * np.max(Q_value_batch[i]))

    self.optimizer.run(feed_dict={
      self.y_input:y_batch,
      self.action_input:action_batch,
      self.state_input:state_batch
      })

  def egreedy_action(self,state):
    Q_value = self.Q_value.eval(feed_dict = {
      self.state_input:[state]
      })[0]
    if random.random() <= self.epsilon:
      return random.randint(0,self.action_dim - 1)
    else:
      return np.argmax(Q_value)

    self.epsilon -= (INITIAL_EPSILON - FINAL_EPSILON)/10000

  def action(self,state):
    return np.argmax(self.Q_value.eval(feed_dict = {
      self.state_input:[state]
      })[0])

  def weight_variable(self,shape):
    initial = tf.truncated_normal(shape)
    return tf.Variable(initial)

  def bias_variable(self,shape):
    initial = tf.constant(0.01, shape = shape)
    return tf.Variable(initial)
# ---------------------------------------------------------
# Hyper Parameters
ENV_NAME = 'CartPole-v0'
EPISODE = 10000 # Episode limitation
STEP = 300 # Step limitation in an episode
TEST = 10 # The number of experiment test every 100 episode

def main():
  # initialize OpenAI Gym env and dqn agent
  env = gym.make(ENV_NAME)
  agent = DQN(env)

  for episode in xrange(EPISODE):
    # initialize task
    state = env.reset()
    # Train
    for step in xrange(STEP):
      action = agent.egreedy_action(state) # e-greedy action for train
      next_state,reward,done,_ = env.step(action)
      # Define reward for agent
      reward_agent = -1 if done else 0.1
      agent.perceive(state,action,reward,next_state,done)
      state = next_state
      if done:
        break
    # Test every 100 episodes
    if episode % 100 == 0:
      total_reward = 0
      for i in xrange(TEST):
        state = env.reset()
        for j in xrange(STEP):
          env.render()
          action = agent.action(state) # direct action for test
          state,reward,done,_ = env.step(action)
          total_reward += reward
          if done:
            break
      ave_reward = total_reward/TEST
      print 'episode: ',episode,'Evaluation Average Reward:',ave_reward
      if ave_reward >= 200:
        break

if __name__ == '__main__':
  main()

上面的代碼就153行,我在github上加了網絡的存儲以及訓練曲線的顯示,代碼200行左右!

5 小結

分析代碼不是一件容易的事,這裏我主要就是介紹編寫的流程。具體代碼還需要大家去理解吧!相信大家如果看懂了這150行代碼,也就很清楚的知道DQN是怎麼回事了。謝謝大家!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章