原文标题: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学习算法相似的方法,但是我们对这个神经网络做了一些自定义的调整:
通过这一过程,我们将得到一个AI,这个AI的神经网络是基于在线训练方式得到的,即在数据可用时立即培训神经网络。
灾难性干扰和经验重现
正如上文所解释的那样,在线训练算法很容易受到灾难性的干扰。当一个神经网络突然在学习新信息时忘记先前所学习到的东西时,就会产生灾难性的干扰。
例如,在游戏中有时会体验到向左走时出现奶酪,但是其他时候往左走会让你掉进坑里。灾难性干扰会使神经网络忘记先前学习的“往左走掉进坑里”。这使得神经网络很难找到一个好的游戏解决方案。
我们使用一种叫做经验回放的方法解决灾难性干扰。我们将大小R的重放内存引入到AI中,在每一次迭代中,我们从重放内存中随机提取大小为B的状态信息和动作信息来训练神经网络。使用这种方法,我们不断地使用新的批样本来对神经网络进行训练,而不是只使用某一段样本。从而解决了灾难性干扰。
现在我们的Q学习算法如下:
在批样本的每个例子中,使用下式计算目标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语言编程等,希望能在数据派平台上熟识更多爱好相同的伙伴,今后能在数据科学的道路上走的更远,飞的更远。