独家|教你用q学习算法训练神经网络玩游戏(附源码)

原文标题://要注意那些用来支持动态(__。__)]。______(@__。_(@.#__(##(([(:_(@__。_(@).#__(#_[#__size_y) @action_taken_index] = 1@old_input_state = input_state# take actionreturn@actions[@action_taken_index]end可以在这里找到完整的组合代码:。

原文标题:teaching a neuralnetwork to play a game using q-learning

作者:soren d

翻译:杨金鸿

本文长度为6000字,建议阅读12分钟

本文介绍如何构建一个基于神经网络和q学习算法的ai来玩电脑游戏。

我们之前介绍了使用q学习算法教ai玩简单游戏,但这篇博客因为引入了额外的维度会更加复杂。为了从这篇博客文章中获得最大的收益,我建议先阅读前一篇文章(https://www.practicalai .io/teaching-ai-play-simple-game-using-q-learning/)。

这个示例的完整源代码可以在github(https:// github.com/daugaard/q-learning-simple-game/tree/neuralnetwork)上获得。注意,神经网络版本的强化学习算法是在神经网络分支中。

游戏

我们的游戏是一个简单的“抓奶酪”游戏,玩家p必须移动去抓奶酪c,并避免掉进坑o里。

玩家p发现一个奶酪得一分,当玩家p掉到坑里的时候就会减去一分。如果用户得到5分或者-5分,游戏就会结束。

如上所述,我们正在用一个新的维度来扩展原始游戏,玩家可以上下左右移动。这张gif图显示了玩家正在玩这个新游戏。

基于神经网络的强化学习

在上一篇文章中,我们使用q学习算法得到一个q表来构建ai。该算法使用q表来查找当前状态下最优的下一个动作(想要了解q学习算法的工作原理可以查看这篇文章(https://www.practicalai.io /teaching-ai-play-simple-game-using-q-learning#q-learning-algorithm))。对于简单的游戏来说是很好的,随着游戏复杂性的增加,q表复杂度也在增加。这是因为在每一个可能的游戏状态s下,q表必须包含每种可能动作a 的q值。

一种替代方法是用神经网络替代q表查询。神经网络会将状态s和动作a作为输入,同时输出q值。q值是指在状态s下执行动作a的可能奖励。

随着神经网络的实现,我们就可以确定在状态s下执行哪个动作a。我们的ai会为每一个动作运行一次网络,并从中选择使得神经网络输出最高的的那个动作,这种做法将最大限度地提高ai的奖励。

为了训练我们的神经网络,我们将采用与原始的q学习算法相似的方法,但是我们对这个神经网络做了一些自定义的调整:

step 1:使用任意值初始化神经网络。

step 2:当玩游戏时执行如下循环。

step 2.a:在0和1之间生成任意数。

如果产生的数大于某个阈值e,那么随机选择一个动作,否则的话,在当前状态和每个可能动作的组合下运行神经网络,选择那个可以获得最高奖励的动作。

step 2.b:执行从步骤2.a获得的那个动作。

step 2.c:观察奖励r。

step 2.d:用奖励r和下面公式来训练神经网络。

通过这一过程,我们将得到一个ai,这个ai的神经网络是基于在线训练方式得到的,即在数据可用时立即培训神经网络。

灾难性干扰和经验重现

正如上文所解释的那样,在线训练算法很容易受到灾难性的干扰。当一个神经网络突然在学习新信息时忘记先前所学习到的东西时,就会产生灾难性的干扰。

例如,在游戏中有时会体验到向左走时出现奶酪,但是其他时候往左走会让你掉进坑里。灾难性干扰会使神经网络忘记先前学习的“往左走掉进坑里”。这使得神经网络很难找到一个好的游戏解决方案。

我们使用一种叫做经验回放的方法解决灾难性干扰。我们将大小r的重放内存引入到ai中,在每一次迭代中,我们从重放内存中随机提取大小为b的状态信息和动作信息来训练神经网络。使用这种方法,我们不断地使用新的批样本来对神经网络进行训练,而不是只使用某一段样本。从而解决了灾难性干扰。

现在我们的q学习算法如下:

step 1:使用任意值初始化神经网络。

step 2:当玩游戏时执行如下循环。

step 2.a:在0和1之间生成任意的数。

如果产生的数大于某个阈值e,那么随机选择一个动作,否则的话,在当前状态和每个可能动作的组合下运行神经网络,选择那个可以获得最高奖励的动作

step 2.b:执行从步骤2.a获得的那个动作。

step 2.c:观察奖励r。

step 2.d:在重放内存中添加当前状态、动作、奖励和新状态(如果内存满了,覆盖最早的那部分信息)。

step 2.e:如果重放内存是满的-抽取尺寸为b的批样本。

在批样本的每个例子中,使用下式计算目标q值:

使用批目标q值和输入状态对神经网络进行训练。

实现神经网络的ai

一旦我们定义了算法,就可以开始实现我们的ai玩家。游戏以玩家类的实例作为玩家对象。玩家类必须实现get_input函数。get_input函数在游戏循环的每次迭代中被调用一次,并返回玩家的行动方向。

下面给出了一个人类玩家类的例子:

require'io/console'

classplayer

attr_accessor :y,:x

def initialize

@x = 0

@y = 0

end

def get_input

input = stdin.getch

if input == 'a'

return:left

elsif input == 'd'

return:right

elsif input == 'w'

return:up

elsif input == 's'

return:down

elsif input == 'q'

exit

end

return:nothing

end

end

关于神经网络ai玩家,我们必须实现一个新的玩家类,它使用上面的算法大纲来确定get_input函数中的动作。

我们首先需要的是ruby-fann工具包,它包含了用于fann(快速人工神经网络,一个c语言的神经网络实现)的ruby绑定。

接下来,我们定义一个构造函数,该函数设置算法需要的玩家的属性和参数。我们的例子使用了一个大小为500的重放内存和大小为400的批训练样本。

require'ruby-fann'

classqlearningplayer

attr_accessor :y, :x, :game

def initialize

@x = 0

@y = 0

@actions = [:left, :right, :up, :down]

@first_run = true

@discount = 0.9

@epsilon = 0.1

@max_epsilon = 0.9

@epsilon_increase_factor = 800.0

@replay_memory_size = 500

@replay_memory_pointer = 0

@replay_memory = []

@replay_batch_size = 400

@runs = 0

@r = random.new

end

要注意那些用来支持动态e值的参数设置。e是算法中第2.a步骤用于选择动作的概率。如果e值很低,那么我们会以高概率随机选择一个动作,而不是选择最高奖励的那个动作。e值的实现将是动态的,从一个非常低的值开始探索,并在每一次迭代中增长,直到达到最大值。

接下来设置一个函数来初始化神经网络。我们设置网络的输入大小等于xy轴的映射数量加上可执行动作数量的和。我们有一个和输入层神经元数量一致的隐藏层和一个输出节点(q值)。另外,将学习速率设置为0.2,并将激活函数更改为s型对称以支持负值。

def initialize_q_neural_network

# setup model

# input is the size of the map number of actions

# output size is one

@q_nn_model = rubyfann::standard.new(

num_inputs: @game.map_size_x*@game.map_size_y @actions.length,

hidden_neurons: [(@game.map_size_x*@game.map_size_y @actions.length)],

num_outputs: 1)

@q_nn_model.set_learning_rate(0.2)

@q_nn_model.set_activation_function_hidden(:sigmoid_symmetric)

@q_nn_model.set_activation_function_output(:sigmoid_symmetric)

end

现在是实现get_input函数的时候了。先暂停几毫秒来帮助我们跟随ai玩家并增加跟踪运行次数的属性。然后检查是否是第一次运行,以及是否初始化了神经网络(步骤1)。

def get_input

# pause to make sure humans can follow along

# increase pause with the number of runs

sleep0.05 0.01*(@runs/400.0)

@runs = 1

if@first_run

# if this is first run initialize the q-neural network

initialize_q_neural_network

@first_run = false

else

如果这不是第一次运行,那么评估最后一次发生了什么,并计算相应的奖励(步骤2.c)。如果游戏得分增加则将奖励设置为1;如果游戏分数降低则将奖励设置为-1;如果没有事情发生则奖励为-0.1。在没有发生任何事情的情况下,给予一个负的奖励,这将鼓励算法直接去捉奶酪。

# if this is not the first

# evaluate what happened on last action and calculate reward

r = 0# default is 0

if !@game.new_gameand@old_score

r = 1# reward is 1 if our score increased

elsif !@game.new_gameand@old_score > @game.score

r = -1# reward is -1 if our score decreased

elsif !@game.new_game

r = -0.1

end

接下来要捕捉游戏的当前状态,并和奖励以及上一状态一起放到重放内存中。将捕捉到的状态作为神经网络的输入矢量。通过在玩家位置设置一个矢量1来编码输入矢量的当前位置(步骤2.d)。

# capture current state

# set input to network map_size_x * map_size_y actions length vector with a 1 on the player position

input_state = array.new(@game.map_size_x*@game.map_size_y @actions.length, 0)

input_state[@x (@game.map_size_x*@y)] = 1

# add reward, old_state and input state to memory

@replay_memory[@replay_memory_pointer] = {reward: r, old_input_state: @old_input_state, input_state: input_state}

# increment memory pointer

@replay_memory_pointer = (@replay_memory_pointer

然后检查内存是否已满。如果已满,提取一个随机的批样本,计算更新q值并对网络进行训练(步骤2.e)。

# if replay memory is full train network on a batch of states from the memory

if@replay_memory.length > @replay_memory_size

# randomly sample a batch of actions from the memory and train network with these actions

@batch = @replay_memory.sample(@replay_batch_size)

training_x_data = []

training_y_data = []

# for each batch calculate new q_value based on current network and reward

@batch.eachdo |m|

# to get entire q table row of the current state run the network once for every posible action

q_table_row = []

@actions.length.timesdo |a|

# create neural network input vector for this action

input_state_action = m[:input_state].clone

# set a 1 in the action location of the input vector

input_state_action[(@game.map_size_x*@game.map_size_y) a] = 1

# run the network for this action and get q table row entry

q_table_row[a] = @q_nn_model.run(input_state_action).first

end

# update the q value

updated_q_value = m[:reward] @discount * q_table_row.max

# add to training set

training_x_data.push(m[:old_input_state])

training_y_data.push([updated_q_value])

end

# train network with batch

train = rubyfann::traindata.new(:inputs=> training_x_data, :desired_outputs=>training_y_data );

@q_nn_model.train_on_data(train, 1, 1, 0.01)

end

end

随着网络的更新我们开始思考下一步该做什么。首先在网络输入矢量中捕捉游戏的当前状态,然后根据算法的当前运行来计算e值。越高的e值意味着以越高的概率选择那些奖励最高的动作,而不是随机动作。

接下来,要么选择一个随机动作,要么在当前状态s运行神经网络,执行每个动作a,并根据网络输出来决定要执行哪个动作。

# capture current state and score

# set input to network map_size_x * map_size_y vector with a 1 on the player position

input_state = array.new(@game.map_size_x*@game.map_size_y @actions.length, 0)

input_state[@x (@game.map_size_x*@y)] = 1

# chose action based on q value estimates for state

# if a random number is higher than epsilon we take a random action

# we will slowly increase @epsilon based on runs to a maximum of @max_epsilon – this encourages early exploration

epsilon_run_factor = (@runs/@epsilon_increase_factor) > (@max_epsilon-@epsilon) ? (@max_epsilon-@epsilon) : (@runs/@epsilon_increase_factor)

if@r.rand > (@epsilon epsilon_run_factor)

# select random action

@action_taken_index = @r.rand(@actions.length)

else

# to get the entire q table row of the current state run the network once for every posible action

q_table_row = []

@actions.length.timesdo |a|

# create neural network input vector for this action

input_state_action = input_state.clone

# set a 1 in the action location of the input vector

input_state_action[(@game.map_size_x*@game.map_size_y) a] = 1

# run the network for this action and get q table row entry

q_table_row[a] = @q_nn_model.run(input_state_action).first

end

# select action with highest posible reward

@action_taken_index = q_table_row.each_with_index.max[1]

end

最后,将当前的分数存储在旧的分数变量中,将当前状态存储在旧的状态变量中,并返回游戏能够执行的动作(步骤2.b)。

# save current state, score and q table row

@old_score = @game.score

# set action taken in input state before storing it

input_state[(@game.map_size_x*@game.map_size_y) @action_taken_index] = 1

@old_input_state = input_state

# take action

return@actions[@action_taken_index]

end

可以在这里找到完整的组合代码:

https://github.com/daugaard/q-learning-simple-game/blob/55748d5e821b34a531dba4d9c4b2683038db6b3d/q_learning_player.rb。

让ai玩

用训练好的ai运行代码,看看它是如何运行的。

我们能看到ai一开始在到处游走。这是由动态的e值导致的,在重放内存满之前,我们不会开始训练神经网络。这意味着开始的时候执行的所有动作都是随机的。但是在运行1和运行2结束时会看到ai已经学会了避免掉进陷坑,直接朝着奶酪去了。

更通用的方法

这篇文章展示了如何训练一个具有对称s形激活器的神经网络来玩一个简单的游戏,方法是通过编码游戏状态和动作作为神经网络的输入向量,同时将对奖励的某种测量值作为神经网络的输出。这个方案需要了解游戏的知识来建立一个网络,当然这对我们建立更通用的ai是一个限制。

更一般的方法是将作为输入的编码游戏状态替换成渲染游戏用的rbg值。deepmind公司的研究人员在《用深度强化学习玩雅达利游戏》这篇论文中详尽地讨论了这个方法。他们成功地训练了q学习,用一个神经网络q表来玩太空入侵者、pong、q伯特和其他雅达利2600游戏。

原文链接:

https://www.practicalai.io/teaching-a-neural-network-to-play-a-game-with-q-learning/

编辑:黄继彦

校对:谭佳瑶

杨金鸿,北京护航科技有限公司员工,在业余时间喜欢翻译一些技术文档。喜欢阅读有关数据挖掘、数据库之类的书,学习java语言编程等,希望能在数据派平台上熟识更多爱好相同的伙伴,今后能在数据科学的道路上走的更远,飞的更远。