我们在“基础算法篇(四)值函数逼近方法解决强化学习问题”中介绍了经典的DQN算法,今天,我们就来点实际的,正式实现一下相关算法。
我们这次代码实现中,使用的对象,是Gym中的活动杆游戏,如下图所示:
游戏的规则非常简单,即操控黑色小车左右移动,使它上面的木棒能够保持平衡。当小车偏离中心4.8个单位,或杆的倾斜超过24度,任务失败。
这个游戏的观测空间是一个Box(4,)对象,即四个float组成的数组;而行动空间是一个Discrete(2),即0或1。这两个值其实就决定了我们后续建立神经网络的输入和输出。
下面,我们正式进入代码编写环节。
备注一下:关于编程环境的搭建,请大家参考“番外篇,强化学习基础环境搭建”,本篇使用的tensorflow安装,使用pip install 命令即可,在这里不再赘述。
首先,我们可以建立一个Python文件,并按照前面我们介绍的DQN算法,写出主函数:
env = gym.make(ENV_NAME)
agent = DQN(env.observation_space, env.action_space)
for episode in range(EPISODES):
# get the initial state
state = env.reset()
for step in range(STEPS):
# get the action by state
action = agent.Choose_Action(state)
# step the env forward and get the new state
next_state, reward, done, info = env.step(action)
# store the data in order to update the network in future
agent.Store_Data(state, action, reward, next_state, done)
if len(agent.replay_buffer) > BATCH_SIZE:
agent.Train_Network(BATCH_SIZE)
if step % UPDATE_STEP == 0:
agent.Update_Target_Network()
state = next_state
if done:
break
我们逐行进行介绍:
由上面可以看出,算法中需要重点关注的,就是由DQN类生成的agent对象,下面我们对这个对象进行详细介绍。
本算法中的agent,就是由DQN类实例化后的对象,这个对象主要完成的功能,就是根据环境状态生成行动,同时优化网络参数,最终实现DQN算法中介绍的TD差分的最小化,下面详细介绍相关部分。
DQN 类的初始化函数主要是通过输入的状态空间和行动空间参数,生成拟合Q值的深度神经网络,并设计网络的参数更新方法,如下:
def __init__(self, observation_space, action_space):
# the state is the input vector of network, in this env, it has four dimensions
self.state_dim = observation_space.shape[0]
# the action is the output vector and it has two dimensions
self.action_dim = action_space.n
# init experience replay, the deque is a list that first-in & first-out
self.replay_buffer = deque()
# you can create the network by the two parameters
self.create_Q_network()
# after create the network, we can define the training methods
self.create_updating_method()
# set the value in choose_action
self.epsilon = INITIAL_EPSILON
# Init session
self.session = tf.InteractiveSession()
self.session.run(tf.global_variables_initializer())
具体的神经网络建立方法和网络参数更新方法,我们在下面进行介绍。
DQN算法中设计了两个网络,current_net和target_net,两个网络结构相同,都是四层的全连接网络(state–>50–>20–>action),在后续算法中current_net用来生成行动,target_net用来计算TD目标值,具体代码如下:
def create_Q_network(self):
# first, set the input of networks
self.state_input = tf.placeholder("float", [None, self.state_dim])
# second, create the current_net
with tf.variable_scope('current_net'):
# first, set the network's weights
W1 = self.weight_variable([self.state_dim, 50])
b1 = self.bias_variable([50])
W2 = self.weight_variable([50, 20])
b2 = self.bias_variable([20])
W3 = self.weight_variable([20, self.action_dim])
b3 = self.bias_variable([self.action_dim])
# second, set the layers
# hidden layer one
h_layer_one = tf.nn.relu(tf.matmul(self.state_input, W1) + b1)
# hidden layer two
h_layer_two = tf.nn.relu(tf.matmul(h_layer_one, W2) + b2)
# the output of current_net
self.Q_value = tf.matmul(h_layer_two, W3) + b3
# third, create the current_net
with tf.variable_scope('target_net'):
# first, set the network's weights
t_W1 = self.weight_variable([self.state_dim, 50])
t_b1 = self.bias_variable([50])
t_W2 = self.weight_variable([50, 20])
t_b2 = self.bias_variable([20])
t_W3 = self.weight_variable([20, self.action_dim])
t_b3 = self.bias_variable([self.action_dim])
# second, set the layers
# hidden layer one
t_h_layer_one = tf.nn.relu(tf.matmul(self.state_input, t_W1) + t_b1)
# hidden layer two
t_h_layer_two = tf.nn.relu(tf.matmul(t_h_layer_one, t_W2) + t_b2)
# the output of current_net
self.target_Q_value = tf.matmul(t_h_layer_two, t_W3) + t_b3
# at last, solve the parameters replace problem
# the parameters of current_net
e_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='current_net')
# the parameters of target_net
t_params = tf.get_collection(tf.GraphKeys.GLOBAL_VARIABLES, scope='target_net')
# define the operation that replace the target_net's parameters by current_net's parameters
with tf.variable_scope('soft_replacement'):
self.target_replace_op = [tf.assign(t, e) for t, e in zip(t_params, e_params)]
在这里我们还定义了一个拷贝网络参数的操作,self.target_replace_op,即用current_net的参数替换target_net的参数。
在DQN算法中,我们需要设置TD目标值为 y j y_j yj,然后将 ( y j − Q ( ϕ t , a ; θ ) ) 2 \left(y_j-Q\left(\phi_t,a;\theta\right)\right)^2 (yj−Q(ϕt,a;θ))2作为误差,利用梯度下降法更新 Q Q Q函数的参数 θ \theta θ。在这里我们代码实现如下:
def create_updating_method(self):
# this the input action, use one hot presentation
self.action_input = tf.placeholder("float", [None, self.action_dim])
# this the TD aim value
self.y_input = tf.placeholder("float", [None])
# this the action's Q_value
Q_action = tf.reduce_sum(tf.multiply(self.Q_value, self.action_input), reduction_indices=1)
# this is the lost
self.cost = tf.reduce_mean(tf.square(self.y_input - Q_action))
# use the loss to optimize the network
self.optimizer = tf.train.AdamOptimizer(0.0001).minimize(self.cost)
上面代码中:
在DQN算法中,我们使用current_net,根据 ε − g r e e d y \varepsilon-greedy ε−greedy方法,在状态state基础上,输出行为,代码如下所示:
def Choose_Action(self, state):
# the output is a tensor, so the [0] is to get the output as a list
Q_value = self.Q_value.eval(feed_dict={
self.state_input: [state]
})[0]
# use epsilon greedy to get the action
if random.random() <= self.epsilon:
# if lower than epsilon, give a random value
self.epsilon -= (INITIAL_EPSILON - FINAL_EPSILON) / 10000
return random.randint(0, self.action_dim - 1)
else:
# if bigger than epsilon, give the argmax value
self.epsilon -= (INITIAL_EPSILON - FINAL_EPSILON) / 10000
return np.argmax(Q_value)
上面代码中,首先通过self.Q_value得到输出,这里需要注意的是self.Q_value的输出是一个tensor,因此,这里使用[0]来获取其输出的list。然后根据随机值进行判断,如果小于 ε \varepsilon ε,那就随机一个行为作为输出,否则就是用argmax来输出Q_value最大值对应的索引,作为输出。
DQN算法中,为了实现更新,要初始化存储单元 D D D,存储大小为 N N N,并将每一步生成的 { ϕ t , a t , r t , ϕ t + 1 } \left\{\phi_t,a_t,r_t,\phi_{t+1}\right\} {ϕt,at,rt,ϕt+1}存入 D D D中,具体代码如下:
def Store_Data(self, state, action, reward, next_state, done):
# generate a list with all 0,and set the action is 1
one_hot_action = np.zeros(self.action_dim)
one_hot_action[action] = 1
# store all the elements
self.replay_buffer.append((state, one_hot_action, reward, next_state, done))
# if the length of replay_buffer is bigger than REPLAY_SIZE
# delete the left value, make the len is stable
if len(self.replay_buffer) > REPLAY_SIZE:
self.replay_buffer.popleft()
上面的self.replay_buffer就是我们的存储单元 D D D,它是一个deque的对象,我们可以将其看作一个列表,存储方式是“先进先出”,如果大于我们设置的 N N N(即REPLAY_SIZE),则从左边插入新数据。
我们在前面设定网络参数更新方法中,已经将更新需要进行的计算公式进行了定义,那么在具体的计算中,就需要我们将数据流(flow)代入公式即可。这种使用flow的计算方式,也是我们使用的tensorflow的独特之处。具体代码如下:
def Train_Network(self, BATCH_SIZE):
# 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 TD aim value
y_batch = []
# give the next_state_batch flow to target_Q_value and caculate the next state's Q_value
Q_value_batch = self.target_Q_value.eval(feed_dict={self.state_input: next_state_batch})
# caculate the TD aim value by the formulate
for i in range(0, BATCH_SIZE):
done = minibatch[i][4]
if done:
y_batch.append(reward_batch[i])
else:
# the Q value caculate use the max directly
y_batch.append(reward_batch[i] + GAMMA * np.max(Q_value_batch[i]))
# step 3: update the network
self.optimizer.run(feed_dict={
self.y_input: y_batch,
self.action_input: action_batch,
self.state_input: state_batch
})
上面的训练网络主要分为三步:
在DQN算法中,需要每隔C步,用current_net的参数替换target_net参数,具体代码如下:
def Update_Target_Network(self):
# update target Q netowrk
self.session.run(self.target_replace_op)
由于我们在建立网络时,就进行了self.target_replace_op操作的定义,在这里只需要调用相关操作即可。
以上就是经典DQN算法的实现,具体的完整代码,参见:https://github.com/samurasun/RL。
本篇中的代码参考了刘建平相关博客和代码中的内容,在这里对刘老师表示感谢。