前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >独家 | 教你用Q学习算法训练神经网络玩游戏(附源码)

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

作者头像
数据派THU
发布2018-01-29 19:47:46
1.1K0
发布2018-01-29 19:47:46
举报
文章被收录于专栏:数据派THU数据派THU数据派THU

原文标题: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'

class Player
  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'

class QLearningPlayer
  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
 sleep 0.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_game and @old_score < @game.score
  r = 1 # reward is 1 if our score increased
elsif !@game.new_game and @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<@replay_memory_size) ? @replay_memory_pointer+1 : 0

然后检查内存是否已满。如果已满,提取一个随机的批样本,计算更新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.each do |m|
 # To get entire q table row of the current state run the network once for every posible action
      q_table_row = []
 @actions.length.times do |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.times do |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语言编程等,希望能在数据派平台上熟识更多爱好相同的伙伴,今后能在数据科学的道路上走的更远,飞的更远。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-12-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据派THU 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档