(图片来源网络,侵删)
原文标题: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,:xdef initialize@x = 0@y = 0enddef get_input input = STDIN.getchif input == 'a'return:leftelsif input == 'd'return:rightelsif input == 'w'return:upelsif input == 's'return:downelsif input == 'q'exitendreturn:nothingendend关于神经网络AI玩家,我们必须实现一个新的玩家类,它使用上面的算法大纲来确定get_input函数中的动作。我们首先需要的是Ruby-FANN工具包,它包含了用于FANN(快速人工神经网络,一个C语言的神经网络实现)的Ruby绑定。接下来,我们定义一个构造函数,该函数设置算法需要的玩家的属性和参数。我们的例子使用了一个大小为500的重放内存和大小为400的批训练样本。require'ruby-fann'classQLearningPlayer attr_accessor :y, :x, :gamedef 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.newend要注意那些用来支持动态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 runssleep0.05 + 0.01(@runs/400.0)@runs += 1if@first_run# If this is first run initialize the Q-neural network initialize_q_neural_network@first_run = falseelse如果这不是第一次运行,那么评估最后一次发生了什么,并计算相应的奖励(步骤2.c)。如果游戏得分增加则将奖励设置为1;如果游戏分数降低则将奖励设置为-1;如果没有事情发生则奖励为-0.1。在没有发生任何事情的情况下,给予一个负的奖励,这将鼓励算法直接去捉奶酪。# If this is not the first# Evaluate what happened on last action and calculate rewardr = 0# default is 0if !@game.new_gameand@old_score < @game.score r = 1# reward is 1 if our score increasedelsif !@game.new_gameand@old_score > @game.score r = -1# reward is -1 if our score decreasedelsif !@game.new_game r = -0.1end接下来要捕捉游戏的当前状态,并和奖励以及上一状态一起放到重放内存中。将捕捉到的状态作为神经网络的输入矢量。通过在玩家位置设置一个矢量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 positioninput_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 memoryif@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).firstend# 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)endend随着网络的更新我们开始思考下一步该做什么。首先在网络输入矢量中捕捉游戏的当前状态,然后根据算法的当前运行来计算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 positioninput_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 explorationepsilon_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).firstend# 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 actionreturn@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语言编程等,希望能在数据派平台上熟识更多爱好相同的伙伴,今后能在数据科学的道路上走的更远,飞的更远。
0 评论