使用50行Python代码从零开始实现一个AI平衡小游戏

yipeiwu_com5年前Python基础

 

集智导读:

本文会为大家展示机器学习专家 Mike Shi 如何用 50 行 Python 代码创建一个 AI,使用增强学习技术,玩耍一个保持杆子平衡的小游戏。所用环境为标准的 OpenAI Gym,只使用 Numpy 来创建 agent。

各位看官好,我(作者 Mike Shi——译者注)将在本文教大家如何用 50 行 Python 代码,教会 AI 玩一个简单的平衡游戏。我们会用到标准的 OpenAI Gym 作为测试环境,仅用 Numpy 创建我们的 AI,别的不用。

这个小游戏就是经典的 Cart Pole 任务,它是 OpenAI Gym 中一个经典的传统增强学习任务。游戏玩法如下方动图所示,就是尽力保持这根杆子始终竖直向上。杆子由于重力原因,会出现倾斜,到了一定程度就会倒下,AI 的任务就是在此时向左或向右移动杆子,不让它倒下。这就跟我们在手指尖上树立一支铅笔玩“金鸡独立”一样,只不过我们这里是个一维的简单游戏(但是还是很有挑战性的)。

你可能好奇最终实现怎样的结果,可以在repl.it 上查看 demo:

 https:// repl.it/@MikeShi42/Cart Pole 

增强学习速览

如果这是你第一次接触机器学习或增强学习,别担心,我下面介绍一些基础知识,这样你就可以了解本文使用的术语了:)。如果已经熟悉了,大可跳过这部分,直接看看编写 AI 的部分。

增强学习(RL)是一个研究领域:教 agent(我们的算法/机器)执行某些任务/动作,但明确告诉它该怎样做。把它想象成一个婴儿,以随机的方式伸腿,如果宝宝偶然间走运站立起来,我们会给它一个糖果作为奖励。同样,Agent 的目标就是在其生命周期内得到最多的奖励,而且我们会根据是否和要完成的任务相符来决定奖励的类型。对于婴儿站立的例子,站立时奖励 1,否则为0。

增强学习 agent 的一个著名例子是 AlphaGo,其中的 agent 已经学会了如何玩围棋以最大化其奖励(赢得游戏)。在本教程中,我们将创建一个 agent,或者说 AI,可以向左或向右移动小车,让杆子保持平衡。

状态

状态是目前游戏的样子。我们通常处理游戏的多种数字表示。在乒乓球比赛中,它可能是每个球拍的垂直位置和 x,y 坐标和球的速度。在我们这个游戏中,我们的状态由 4 个数字组成:底部小车的位置,小车的速度,杆的位置(以角度表示)和杆的角速度。这 4 个数字都是给定的数组(或向量)。这个很重要,理解状态是一个数字数组意味着我们可以对它进行一些数学运算来决定我们根据状态采取什么行动。

策略

策略是一种函数,其输入是游戏的状态(例如棋盘的位置,或小车和杆的位置),输出 agent应该在该位置采取的动作(例如,将小车向左边移动)。在 agent 采取我们选择的操作后,游戏将使用下一个状态进行更新,我们会再次将其纳入策略以做出决策。这种情况一直持续到游戏结束。策略非常重要,也是我们一直追求的,因为代表了 agent 背后的决策能力。

点积

两个数组(向量)之间的点积简单地将第一个数组的每个元素乘以第二个数组的对应元素,并将它们全部加在一起。假设我们想找到数组 A 和 B 的点积,只需计算是 A [0] * B [0] + A [1] * B [1] ......我们将使用这种运算将状态(一个数组)乘以另一个数组(我们的策略)。

创建我们的策略

为了完成这个推车平衡游戏,我们希望让我们的 agent(或者说 AI)学习策略赢得比赛或获得最大奖励。

对于我们今天要开发的 agent,我们将策略表示为 4 个数字的数组,分别代表状态的各个部分的“重要性”(小车位置,杆子的位置等)然后我们会计算状态和策略数组的点积,得到一个数字。根据数字是正数还是负数,我们将向左或向右推动小车。

如果这听起来有点抽象,那么我们选择一个具体的例子,看看会发生什么。

假设小车在游戏中居中并且静止,杆子向右倾斜且可能倒向右边。它看起来像这样:

相关状态可能如下所示:

那么状态数组将是 [0,0,0.2,0.05]。

从直觉上,我们要把小车推向右边,将支杆拉直。我从训练中得到了一个很好的策略,其策略数据如下:[ - 0.116,0.332,0.207 0.352]。我们快速计算一下,看看该策略会输出怎样的动作。

这里,我们将状态数组 [0,0,0.2,0.05] 和上述策略数组结合计算点积。如果数字是正数,我们将车推向右边,如果数字是负数,我们向左推。

结果为正,意味着策略会向右推动小车,符合我们的预期。

现在比较明显了,我们需要 4 个像上面这样的神奇数字来帮我们解决问题。那么我们该如何获得这些数字?如果我们只是随机挑选它们会怎样?AI 的效果会怎样?我们来一起看代码!

启动你的编辑器!

首先在repl.it 上打开一个 Python 实例。Repl.it 能让我们快速启动大量不同编程环境的云实例,并在任何地方都能访问的强大云 IDE 中编辑代码!

安装软件包

我们首先安装这个项目所需的两个软件包:numpy 帮助进行数值计算;OpenAI Gym 作为我们代理的模拟器。

只需在编辑器左侧的包搜索工具中输入 gym 和 numpy,然后单击加号按钮即可安装包。

创建基础框架

我们首先将我们刚刚安装的两个依赖项导入到main.py 脚本中,并设置一个新的 gym 环境:

import gymimport numpy as npenv = gym.make('CartPole-v1')

接下来,我们定义一个名为“play”的函数,为该函数提供一个环境和一个策略数组,在环境中计算策略数组并返回分数,以及每个时步的游戏快照(用于观察)。我们将使用分数来判断策略的效果以及查看每个时步的游戏快照来判断策略的表现。这样我们就可以测试不同的策略,看看它们在游戏中的表现如何!

首先我们理解函数的定义,然后将游戏重置为开始状态。

def play(env, policy): observation = env.reset()

接下来,我们将初始化一些变量以跟踪游戏是否已经结束,包括策略的总分以及游戏中每个步骤的快照(供观察)。

done = False score = 0 observations = []

现在我们多次运行游戏,直到 gym 告诉我们游戏已经完成。

for _ in range(5000): 
observations += [observation.tolist()] 
# 记录用于正则化的观察值,并回放 
 if done:
 # 如果模拟在最后一次迭代中结束,则退出循环  
 break  
# 根据策略矩阵选择一种行为 
outcome = np.dot(policy, observation) 
 action = 1 if outcome > 0 else 0 
 # 创建行为,记录反馈 
observation, reward, done, info = env.step(action) 
 score += reward 
return score, observations

上面的大部分代码主要是玩游戏的过程以及记录的结果。实际上,我们的策略代码只需要两行:

outcome = np.dot(policy, observation) 
action = 1 if outcome > 0 else 0

我们在这里所做的只是策略数组和状态数组之间的点积运算,就像我们之前在具体例子中所示的那样。然后我们根据结果是正还是负,选择 1 或 0(左或右)的动作。

到目前为止,我们的main.py 应如下所示:

import gymimport numpy as npenv = gym.make('CartPole-v1')def play(env, policy): 
 observation = env.reset() 
done = False score = 0 
observations = [] for _ in range(5000): 
 observations += [observation.tolist()] 
# 如果模拟在最后一次迭代中结束,则退出循环  
if done: # 如果模拟在最后一次迭代中结束,则退出循环  break  
# 根据策略矩阵选择一种行为 
outcome = np.dot(policy, observation) 
action = 1 if outcome > 0 else 0  
# 创建行为,记录反馈 
observation, reward, done, info = env.step(action) 
score += reward 
return score, observations

现在,我们开始玩游戏,寻找我们的最佳策略!

第一局游戏

由于我们有了能够玩游戏的函数,并且能告诉我们的策略有多好,那么下面就创建一些策略,看看它们的效果怎样。

如果我们首先只想尝试随机策略呢?能达到怎样的效果?我们使用 numpy 来生成我们的策略,它是一个 4 元素数组或 1x4 矩阵。它会选择 0 到 1 之间的 4 个数字作为我们的策略。

policy = np.random.rand(1,4)

根据该策略和我们上面创建的环境,我们可以用它们来玩游戏,获得一个分数。

score, observations = play(env, policy)print('Policy Score', score)

点击运行,执行我们的脚本,然后会输出我们的策略得分:

游戏的最大得分是 500 分,你的策略有可能达不到这个水平。如果达到了,恭喜你!绝对是你的大日子!只是看一个数字并没有特别大的意义。如果能看到我们的 agent 是如何玩游戏的,那就太好了,下一步我们就会设置它!

查看我们的agent

要查看我们的 agent,我们会使用 Flask 设置一个轻量级服务器,以便我们可以在浏览器中查看代理的性能。Flask 是一个轻量级的 Python HTTP 服务器框架,可以为我们的 HTML UI 和数据伺服。这部分我就一笔带过了,因为渲染和 HTTP 服务器背后的细节对训练我们的 agent 并不重要。

我们首先将 Flask 安装为 Python 包,就像我们在前面安装 gym 和 Numpy 一样。

 

接着,在我们脚本的底部,我们将创建一个 Flask 服务器。它将在端点 / data 上显示游戏的每一帧的记录,并在/上托管UI。

from flask import Flaskimport jsonapp = Flask(__name__, static_folder='.')@app.route("/data")def data(): 
 return json.dumps(observations)@app.route('/')def root(): 
 return app.send_static_file('./index.html')
 app.run(host='0.0.0.0', port='3000')

另外,我们需要添加两个文件。一个是项目的空白 Python 文件。这是repl.it 如何检测 repl 是处于 eval 模式还是项目模式的专用术语。只需使用新文件按钮添加空白 Python 脚本即可。

之后我们还想创建一个用于渲染 UI 的 index.html。这里不再深入讲解,只需将此 index.html 上传到你的repl.it 项目即可。

现在你应该有一个如下所示的项目目录:

现在有了这两个新文件,当我们运行 repl 时,它应该能演示我们的策略。有了这个,我们尝试找到最佳策略!

策略搜索

在我们的第一局游戏中,我们只是随机选择了一个策略,但是如果我们选择了一批策略,并且只保留那个表现最好的策略呢?

我们回到发布策略的部分,这次不是仅生成一个,而是编写一个循环来生成多个策略,并跟踪每个策略的执行情况,最终仅保存最佳策略。

首先我们创建一个名为 max 的元组,它将存储我们迄今为止看到的最佳策略的得分,观察值和策略数组。

max = (0, [], [])

接着我们会生成和评估 10 个策略,并将最优策略保存在 max 中。

for _ in range(10): 
 policy = np.random.rand(1,4) 
 score, observations = play(env, policy) 
if score > max[0]: 
max = (score, observations, policy)print('Max Score', max[0])

我们还要让 /data 端点返回最优策略的回放。

该端点:

@app.route("/data")def data():return json.dumps(observations)

应该改为:

@app.route("/data")def data():return json.dumps(max[1])

你的main.py 应该如下所示:

import gymimport numpy as npenv = gym.make('CartPole-v1')def play(env, policy): 
 observation = env.reset() done = False score = 0 
 observations = [] for _ in range(5000): 
observations += [observation.tolist()]  
 if done:  
break  
outcome = np.dot(policy, observation) 
 action = 1 if outcome > 0 else 0 
 observation, reward, done, info = env.step(action) score += reward return score, observationsmax = (0, [], [])for _ in range(10): policy = np.random.rand(1,4) score, observations = play(env, policy) if score > max[0]: 
max = (score, observations, policy)print('Max Score', max[0])from flask import Flaskimport jsonapp = Flask(__name__, static_folder='.')@app.route("/data")def data(): return json.dumps(max[1])@app.route('/')def root(): 
 return app.send_static_file('./index.html') 
app.run(host='0.0.0.0', port='3000')

如果我们现在运行 repl,应该会得到最多为 500 分的分数,如果没有达到这个结果,那就再运行 repl 一遍。另外我们可以看到策略几乎完美地让推车上的杆子保持平衡。

不是那么快

不过实际上或许没有这么好,因为我们在第一部分稍微有一点作弊。首先,我们只是在 0 到 1 的范围内随机创建了策略数组。这恰好可行,但是如果我们修改一下运算符,就会看到 agent 出现灾难性的失败。你自己可以试试将 action = 1 if outcome > 0 else 0 改成 action = 1 if outcome < 0 else 0

但是效果仍然不稳定,因为如果我们恰好选择少于而不是大于 0,我们永远找不到最优的策略。为了解决这个问题,我们实际上应该生成对负数同样适用的策略。虽然这为我们的工作增加了难度,但我们再也不必通过将我们的特定算法拟合特定游戏来“作弊”了。不然,如果我们试图在 OpenAIgym 以外的其他环境中运行算法时,算法肯定会失败。

要做到这一点,我们不再使用 policy = np.random.rand(1,4),而是改为 policy = np.random.rand(1,4) - 0.5。这样我们策略中的每个数字都在 -0.5 到 0.5 之间,而不是 0 到 1。但是因为这样难度更高,我们还想搜索更多的策略。在上面的 for 循环中,不是迭代 10 个策略,而是通过让代码改为读取 for _ in range(100): 来尝试 100 个策略。此外也鼓励大家尝试首先只迭代 10 个策略,看看现在用负数来获得好的策略的难度如何。

现在我们的main.py 应该如下所示:

import gym
import numpy as np
env = gym.make('CartPole-v1')
def play(env, policy):
 observation = env.reset()
 done = False
 score = 0
 observations = []
 for _ in range(5000):
  observations += [observation.tolist()] 
  if done: 
   break
  outcome = np.dot(policy, observation)
  action = 1 if outcome > 0 else 0
  observation, reward, done, info = env.step(action)
  score += reward
 return score, observations
max = (0, [], [])
# 修改接下来两行!
for _ in range(100):
 policy = np.random.rand(1,4) - 0.5
 score, observations = play(env, policy)
 if score > max[0]:
  max = (score, observations, policy)
print('Max Score', max[0])
from flask import Flask
import json
app = Flask(__name__, static_folder='.')
@app.route("/data")
def data():
  return json.dumps(max[1])
@app.route('/')
def root():
  return app.send_static_file('./index.html')
app.run(host='0.0.0.0', port='3000')

如果现在运行 repl,无论我们使用的值是否大于或小于 0,我们仍然可以为游戏找到一个好的策略。

但是等等,这还没完!即使我们的策略可以运行一次就达到最高分 500,但每次都能做到吗?当我们生成 100 个策略,并选择出在单一运行中表现最佳的策略时,该策略可能只是走运而已,甚至它可能是一个非常糟糕的策略,只是恰好运行效果很好。这是因为游戏本身具有随机性因素(起始位置每次都不同),因此策略可能只适用于一个起始位置,换成其他起始位置就不行了。

因此,为了解决这个问题,我们需要评估策略在多次试验中的表现。现在,我们使用之前找到的最优策略,看看它在 100 次试验中的表现如何。

scores = []for _ in range(100): score, _ = play(env, max[2])
 scores += [score] print('Average Score (100 trials)', np.mean(scores))

这里我们将该策略运行 100次,并且每次都记录它的得分。然后我们使用 numpy 计算平均分数并将其打印到我们的终端。没有严格的已发布的“已解决”定义,但它应该只有少数几个点。你可能会注意到最好的政策实际上可能实际上是低于平均水平。但是,我会把解决方案留给你决定!

当然,对于何为“最优”并没有严格的定义,但是至少比最高分 500 来说不应太差。你可能注意到最优策略有时是低于平均水平的,但是最终的最优策略如何,还是要靠大家根据自己的实际情况来定夺。

结语

恭喜!至此我们成功创建了一个 AI,能够很好地玩耍这个简单的平衡游戏。不过,仍然有很多需要改进的地方:

  • 找到一个“真正的”最优策略(每局游戏都能表现良好)
  • 减少我们寻找最优策略的搜索次数
  • 研究怎样找到正确的策略,而不是随机选择它们
  • 尝试其它开发环境

以上所述是小编给大家介绍的使用50行Python代码从零开始实现一个AI平衡小游戏,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对【听图阁-专注于Python设计】网站的支持!

相关文章

Python类的基础入门知识

复制代码 代码如下:class Account(object): "一个简单的类" account_type="Basic" def __init__(self,name,balance...

跟老齐学Python之正规地说一句话

小孩子刚刚开始学说话的时候,常常是一个字一个字地开始学,比如学说“饺子”,对他/她来讲,似乎有点难度,大人也聪明,于是就简化了,用“饺饺”来代替,其实就是让孩子学会一个字就能表达。当然,...

讲解Python中fileno()方法的使用

 fileno()方法返回所使用的底层实现,要求从操作系统I/O操作的整数文件描述符。 语法 以下是fileno()方法的语法: fileObject.fileno();...

使用Python的Twisted框架编写简单的网络客户端

Protocol   和服务器一样,也是通过该类来实现。先看一个简短的例程: from twisted.internet.protocol import Protocol...

python 排列组合之itertools

python 2.6 引入了itertools模块,使得排列组合的实现非常简单:复制代码 代码如下:import itertools  有序排列:e.g., 4个数内选2个排列...