[转载]用机器学习制作游戏AI

原作者: Yvan Scher
链接: https://medium.com/@yvanscher/making-a-game-ai-with-deep-learning-963bb549b3d5

用pysc2和Q Learning制作小型游戏AI

星际争霸2最强大的单位之一:The Laser Giraffe

要为游戏制作人工智能,您需要:

1 — 人工智能逻辑,不管是脚本行为还是人工智能

2 — 把你的游戏世界转换成你的人工智能可以理解和执行的东西

本文的目标是向您展示构建AL逻辑的方法,从脚本行为到可以学习几乎任何任务的方法。对于那些有编程技巧的人来说,这应该被认为是一篇介绍bot机器人构建的文章。我们将建立一个可以玩星际争霸2小游戏的人工智能。我们将使用python,numpy,pysc2。Let’s go!

设置pysc2

如果你想成为一个游戏AI,你需要为你的游戏创建一个接口api,一个AI可以用来在你的游戏世界中查看、玩和采取行动。我们将使用星际争霸2,特别是由deepmind和google发布的一个名为pysc2的环境。首先我们需要安装星际争霸2(它是免费的);我使用的是linux,所以:

 cd ~
 curl -O http://blzdistsc2-a.akamaihd.net/Linux/SC2.3.17.zip
 unzip -q SC2.3.17.zip

确保您得到了游戏的3.17版本,因为较新的版本似乎不适用于某些pysc2函数(查看 run_configs/platforms).解压后大约需要7GB的空间,解压密码是“iagreetotheeula”。我在ubuntu上渲染3D视图时遇到了一些问题。

如果使用mac,请确保将游戏安装在默认位置(~),并在主文件夹中创建“Maps”、“Replays”文件夹。使用安装程序现在让我们检查pysc2和pytorch:

conda create -n pysc2 python=3 -y
pip install pysc2
pip install numpy

现在我们需要得到sc2地图,我们将用它作为我们的AI的试验地: 在该链接上获取mini games地图

出于某种原因,pysc2的github上的迷你游戏zip文件在linux上不起作用。所以我在我的mac电脑上解压缩了它,然后把它移到了我的linux机器上。将mini_games文件夹放入StarcraftII安装文件夹中的Maps文件夹。小游戏地图实际上是随pysc2一起提供的,但谁知道deepmind是否会继续这样做。好了,现在我们有了所有的软件和地图,让我们编写第一个代理程序,并检查pysc2环境的详细信息。

实现随机AI

我们要做的人工智能将要玩移动到信标游戏。我们的人工智能将控制一个海军陆战队(小型作战部队),并将其移动到信标。

我要做一个简单的AI,可以与这个环境交互。它会在地图上随机移动:

import numpy as np
from pysc2.agents import base_agent
from pysc2.lib import actions
from pysc2.lib import features
from pysc2.env import sc2_env, run_loop, available_actions_printer
from pysc2 import maps
from absl import flags

# define the features the AI can seee
_AI_RELATIVE = features.SCREEN_FEATURES.player_relative.index
# define contstants for actions
_NO_OP = actions.FUNCTIONS.no_op.id
_MOVE_SCREEN = actions.FUNCTIONS.Attack_screen.id
_SELECT_ARMY = actions.FUNCTIONS.select_army.id
# define constants about AI's world
_BACKGROUND = 0
_AI_SELF = 1
_AI_ALLIES = 2
_AI_NEUTRAL = 3
_AI_HOSTILE = 4
# constants for actions
_SELECT_ALL = [0]
_NOT_QUEUED = [0]

def get_marine_location(ai_relative_view):
    '''get the indices where the world is equal to 1'''
    return (ai_relative_view == _AI_SELF).nonzero()

def get_rand_location(ai_location):
    '''gets a random location at least n away from current x,y point.'''
    return [np.random.randint(0, 64), np.random.randint(0, 64)]

class Agent1(base_agent.BaseAgent):
    # An agent for doing a simple movement form one point to another.
    def step(self, obs):
        # step function gets called automatically by pysc2 environment
        # call the parent class to have pysc2 setup rewards/etc for us
        super(Agent1, self).step(obs)
        # if we can move our army (we have something selected)
        if _MOVE_SCREEN in obs.observation['available_actions']:
            # get what the ai can see about the world
            ai_view = obs.observation['screen'][_AI_RELATIVE]
            # get the location of our marine in this world
            marine_x, marine_y = get_marine_location(ai_view)
            # it our marine is not on the screen do nothing.
            # this happens if we scroll away and look at a different
            # part of the world
            if not marine_x.any():
                return actions.FunctionCall(_NO_OP, [])
            target = get_rand_location([marine_x, marine_y])
            return actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, target])
        # if we can't move, we havent selected our army, so selecto ur army
        else:
            return actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])

下面是上面代码运行的视频;AI在地图上随机移动:

python -m pysc2.bin.agent --map MoveToBeacon --agent agent1.Agent1

功能图如下所示:

这里没发生什么疯狂的事。你可以看到一个海军陆战队(绿色)和信标(灰蓝色)的主视图。海军陆战队只是像我们说的那样随意移动。屏幕的右边是我们的机器人可以看到的所有不同的视图。屏幕上的单位类型不同,地形高度图也不同。要查看此部分的更多代码/说明,请参阅此笔记本。

具有更可读文本的要素图层的另一个示例:

实现脚本化人工智能

现在我们想做一些比随机更好的事情。在移动到灯塔迷你游戏中,目标是移动到灯塔。我们将编写一个执行此操作的机器人脚本:

import numpy as np
from pysc2.agents import base_agent
from pysc2.lib import actions
from pysc2.lib import features
from pysc2.env import sc2_env, run_loop, available_actions_printer
from pysc2 import maps
from absl import flags

_AI_RELATIVE = features.SCREEN_FEATURES.player_relative.index
_NO_OP = actions.FUNCTIONS.no_op.id
_MOVE_SCREEN = actions.FUNCTIONS.Attack_screen.id
_SELECT_ARMY = actions.FUNCTIONS.select_army.id
_BACKGROUND = 0
_AI_SELF = 1
_AI_ALLIES = 2
_AI_NEUTRAL = 3
_AI_HOSTILE = 4
_SELECT_ALL = [0]
_NOT_QUEUED = [0]

def get_beacon_location(ai_relative_view):
    '''returns the location indices of the beacon on the map'''
    return (ai_relative_view == _AI_NEUTRAL).nonzero()

class Agent2(base_agent.BaseAgent):
    """An agent for doing a simple movement form one point to another."""
    def step(self, obs):
        '''Step function gets called automatically by pysc2 environment'''
        super(Agent2, self).step(obs)
        if _MOVE_SCREEN in obs.observation['available_actions']:
            ai_view = obs.observation['screen'][_AI_RELATIVE]
            # get the beacon coordinates
            beacon_xs, beacon_ys = get_beacon_location(ai_view)
            if not beacon_ys.any():
                return actions.FunctionCall(_NO_OP, [])
            # get the middle of the beacon and move there
            target = [beacon_ys.mean(), beacon_xs.mean()]
            return actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, target])
        else:
            return actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])

下面是这个机器人的视频:

python -m pysc2.bin.agent --map MoveToBeacon --agent agent2.Agent2 --save_replay True

然后您可以在星际争霸2游戏客户端中查看重播:

正如你所看到的脚本人工智能玩灯塔游戏,并移动到灯塔的海军陆战队。这个脚本机器人在运行105集后平均每集获得25个奖励。这个奖励反映了我们的机器人在mingame计时器启动前(120秒)到达becaon的能力。我们开发的任何人工智能都应该至少和这个脚本机器人一样好,所以一次训练的平均分是25分。接下来,我们将使用强化学习实现一个实际的人工智能(学习如何玩)。

实现Q Learning人工智能

这种方法是一种称为“Q Learning”的方法的变体,它试图为游戏世界中的每一个状态学习一种称为“质量”的值,并将更高的质量归因于能够带来更多回报的状态。我们创建一个表(称为Qtable),其中游戏世界的所有可能状态都在y轴上,所有可能的操作都在x轴上。质量值存储在此表中,并告诉我们在任何可能的状态下应采取的操作。下面是Qtable的一个示例:

因此,当我们的AI选择了海军陆战队员,但它不在信标上时,state=(1,0),我们的AI将了解到移动到信标上的值(索引3处的动作)与处于相同状态的其他动作相比是最高的。当它没有选择海军陆战队员,并且它不在信标上时,state=(0,0),我们的AI了解到select海军陆战队员有最高的值(在索引1处的动作)。当它是一个信标,它选择了海军陆战队,state =(1,1),什么都不做是有价值的。

在update Q Table函数中更新Q表时,我们遵循以下公式:

它基本上是说把我们对采取行动的回报的估计值和实际采取行动的回报进行比较,然后把这个差异调整我们的Q值,以减少一点错误。我们的人工智能将获取状态信息并发出一个要采取的行动。我已经简化了这个世界状态和操作,以便更容易地学习Q表并保持代码简洁。我们给我们的代理人一个选择,而不是硬编码逻辑总是移动到信标。我们给了它6件可以做的事:

_NO_OP — 什么也不做。

_SELECT_ARMY — 选择海军陆战队。

__SELECT_POINT — 取消选择海军陆战队。

_MOVE_SCREEN — 移动到信标。

_MOVERAND — 移动到不是信标的随机点。

_MOVE_MIDDLE — 移到地图中间的一点。

这是我们的预培训代理Q表代码:

import math
import numpy as np
from pysc2.agents import base_agent
from pysc2.lib import actions
from pysc2.lib import features
from pysc2.env import sc2_env, run_loop, available_actions_printer
from pysc2 import maps
from absl import flags

_AI_RELATIVE = features.SCREEN_FEATURES.player_relative.index
_AI_SELECTED = features.SCREEN_FEATURES.selected.index
_NO_OP = actions.FUNCTIONS.no_op.id
_MOVE_SCREEN = actions.FUNCTIONS.Attack_screen.id
_SELECT_ARMY = actions.FUNCTIONS.select_army.id
_SELECT_POINT = actions.FUNCTIONS.select_point.id
_MOVE_RAND = 1000
_MOVE_MIDDLE = 2000
_BACKGROUND = 0
_AI_SELF = 1
_AI_ALLIES = 2
_AI_NEUTRAL = 3
_AI_HOSTILE = 4
_SELECT_ALL = [0]
_NOT_QUEUED = [0]
EPS_START = 0.9
EPS_END = 0.025
EPS_DECAY = 2500

possible_actions = [
    _NO_OP,
    _SELECT_ARMY,
    _SELECT_POINT,
    _MOVE_SCREEN,
    _MOVE_RAND,
    _MOVE_MIDDLE
]

def get_eps_threshold(steps_done):
    return EPS_END + (EPS_START - EPS_END) * math.exp(-1. * steps_done / EPS_DECAY)

def get_state(obs):
    ai_view = obs.observation['screen'][_AI_RELATIVE]
    beaconxs, beaconys = (ai_view == _AI_NEUTRAL).nonzero()
    marinexs, marineys = (ai_view == _AI_SELF).nonzero()
    marinex, mariney = marinexs.mean(), marineys.mean()

    marine_on_beacon = np.min(beaconxs) <= marinex <=  np.max(beaconxs) and np.min(beaconys) <= mariney <=  np.max(beaconys)

    ai_selected = obs.observation['screen'][_AI_SELECTED]
    marine_selected = int((ai_selected == 1).any())

    return (marine_selected, int(marine_on_beacon)), [beaconxs, beaconys]

class QTable(object):
    def __init__(self, actions, lr=0.01, reward_decay=0.9, load_qt=None, load_st=None):
        self.lr = lr
        self.actions = actions
        self.reward_decay = reward_decay
        self.states_list = set()
        self.load_qt = load_qt
        if load_st:
            temp = self.load_states(load_st)
            self.states_list = set([tuple(temp[i]) for i in range(len(temp))])

        if load_qt:
            self.q_table = self.load_qtable(load_qt)
        else:
            self.q_table = np.zeros((0, len(possible_actions))) # create a Q table

    def get_action(self, state):
        if not self.load_qt and np.random.rand() < get_eps_threshold(steps):
            return np.random.randint(0, len(self.actions))
        else:
            if state not in self.states_list:
                self.add_state(state)
            idx = list(self.states_list).index(state)
            q_values = self.q_table[idx]
            return int(np.argmax(q_values))

    def add_state(self, state):
        self.q_table = np.vstack([self.q_table, np.zeros((1, len(possible_actions)))])
        self.states_list.add(state)

    def update_qtable(self, state, next_state, action, reward):
        if state not in self.states_list:
            self.add_state(state)
        if next_state not in self.states_list:
            self.add_state(next_state)
        state_idx = list(self.states_list).index(state)
        next_state_idx = list(self.states_list).index(next_state)
        q_state = self.q_table[state_idx, action]
        q_next_state = self.q_table[next_state_idx].max()
        q_targets = reward + (self.reward_decay * q_next_state)
        loss = q_targets - q_state
        self.q_table[state_idx, action] += self.lr * loss
        return loss

    def get_size(self):
        print(self.q_table.shape)

    def save_qtable(self, filepath):
        np.save(filepath, self.q_table)

    def load_qtable(self, filepath):
        return np.load(filepath)

    def save_states(self, filepath):
        temp = np.array(list(self.states_list))
        np.save(filepath, temp)

    def load_states(self, filepath):
        return np.load(filepath)

class Agent3(base_agent.BaseAgent):
    def __init__(self, load_qt=None, load_st=None):
        super(Agent3, self).__init__()
        self.qtable = QTable(possible_actions, load_qt='agent3_qtable.npy', load_st='agent3_states.npy')

    def step(self, obs):
        '''Step function gets called automatically by pysc2 environment'''
        super(Agent3, self).step(obs)
        state, beacon_pos = get_state(obs)
        action = self.qtable.get_action(state)
        func = actions.FunctionCall(_NO_OP, [])

        if possible_actions[action] == _NO_OP:
            func = actions.FunctionCall(_NO_OP, [])
        elif state[0] and possible_actions[action] == _MOVE_SCREEN:
            beacon_x, beacon_y = beacon_pos[0].mean(), beacon_pos[1].mean()
            func = actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, [beacon_y, beacon_x]])
        elif possible_actions[action] == _SELECT_ARMY:
            func = actions.FunctionCall(_SELECT_ARMY, [_SELECT_ALL])
        elif state[0] and possible_actions[action] == _SELECT_POINT:
            ai_view = obs.observation['screen'][_AI_RELATIVE]
            backgroundxs, backgroundys = (ai_view == _BACKGROUND).nonzero()
            point = np.random.randint(0, len(backgroundxs))
            backgroundx, backgroundy = backgroundxs[point], backgroundys[point]
            func = actions.FunctionCall(_SELECT_POINT, [_NOT_QUEUED, [backgroundy, backgroundx]])
        elif state[0] and possible_actions[action] == _MOVE_RAND:
            beacon_x, beacon_y = beacon_pos[0].max(), beacon_pos[1].max()
            movex, movey = np.random.randint(beacon_x, 64), np.random.randint(beacon_y, 64)
            func = actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, [movey, movex]])
        elif state[0] and possible_actions[action] == _MOVE_MIDDLE:
            func = actions.FunctionCall(_MOVE_SCREEN, [_NOT_QUEUED, [32, 32]])
        return func


通过下载两个文件(agent3_qtable.npy, agent3_states.npy),使用预先准备好的代码运行此代理:

python -m pysc2.bin.agent --map MoveToBeacon --agent agent3.Agent3

这个人工智能可以匹配的奖励25每集我们的脚本人工智能可以得到一次训练。它在地图周围尝试了很多不同的移动,并注意到在海洋和信标位置相互重叠的州,它会得到奖励。然后,它试图在每一个导致这种结果的州采取行动,以获得最大的回报。这是一段人工智能早期播放的视频:

这是一个视频,一旦它知道移动到灯塔提供奖励:

您还可以使用notebook代码培训自己的AI。

结论和未来打算

在这篇文章中,我想向你们展示三种编程人工智能行为,随机的,脚本化的,和Q Learning AI。

正如《悖论发展》(Paradox development)杂志的马丁·安沃德所:“复杂游戏的机器学习在这个时候主要是科幻小说。”我同意马丁的一些观点。我仍然认为在游戏中机器学习是有潜力的。剩下的部分是如何使用加权列表来制作一个好的人工智能;神经网络和加权列表是一样的,但它是学习的。最困难的是,马丁就在这里,复杂的交互对于一台计算机来说是很难拼凑和推理的。

这是我们的Q学习信标AI,这次以正常速度播放:


在这篇文章中,我坚持使用一个迷你游戏,因为我想要一些易于实验和编程的东西。pysc2足够复杂,可以工作。我们还没有训练我们的Q学习代理来识别并移动到beacon(我们只是将其作为一个选项)。这是可能的,但超出了本文介绍的范围。在未来,我们将做一个类似DQN的Deepmind的论文,也许可以解决这个更复杂的任务。

最后请订阅Generation Machine。这里是代码和notebook

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 最近几次,跟各位老总一起拜访客户,接待客户,看到他们的厉害之处,也知反思了自己的不足,可是总感觉缺了点什么。就像不...
    Jason叶阅读 226评论 2 4
  • www.swordmaster.info
    SwordMaster阅读 284评论 0 0
  • 这是一片贫瘠的土地,这是一片富饶的土地,这,是一片神奇的土地。 这块土地贫瘠得养活不了自己的儿女。煤油灯下,主妇的...
    王小唐阅读 349评论 0 0
  • 返回json数据格式 django post 请求 403 在开头添加 from django.views.dec...
    Arnoux阅读 182评论 0 1
  • 早上想睡个懒觉,我突然兴奋的起床,因为昨晚受到同学的邀请去李晓涵家,我高兴的不得了,赶紧穿衣服洗刷吃早饭。 我一路...
    吕金金阅读 243评论 0 3