聊聊强化学习:可自动玩游戏的AI技术,实战智能走迷宫案例

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

上一篇聊了聊AI之机器学习。这篇再聊聊机器学习之强化学习。

提到人工智能呀,大家普遍的认识,是应用在图像处理、文字生成这两类。其实,还有一个领域也挺有意思,那就是强化学习

一、强化学习

机器人AlphaGo打败人类围棋冠军这件事,就是强化学习的一个应用。除此之外,自动驾驶,自动游戏玩家,也是它的应用范畴。

为便于大家理解,举几个图例。

下面是机器狗学走路的例子。它一开始只能趔趔趄趄地站稳,1000次之后,能移动了。1800次之后,健步如飞。

下图是打砖块的游戏。这完全由机器人操作。它根据前4帧图像就可以判断出当前的形势,然后给出最佳行为。

下面这个,是强化学习经典的入门Hello Word。木板顶一跟棍子,棍子会随机倒下,你通过移动位置保持它不倒。

说白了,强化学习和马戏团训练狗熊类似。驯兽师会对狗熊的动作,给予奖励或者惩罚,让狗熊的行为按照趋利避害的方向发展,最后它就能走独木桥了。迁移到AI下棋的场景也是一样,如果下这一步棋能赢,那它就会依照能赢的方向进化。

说得这么玄乎,那具体如何实现呢?其实并不难。为此,我还写了个配套游戏来讲解。

如上图所示,话说有个古代人穿越到了现代。他很想快点回去结婚,不然新娘就改嫁了。因此,他得赶紧找到穿越之门。不过,寻门之路充满了艰辛。他需要吃鸡腿续命,也需要躲开雷电的伤害。

我通过强化学习,搞了自动玩这个游戏的算法。如下图所示,time是尝试次数,step表示走了多少步,reward表示得分,找到穿越之门可得100分。下图完全是机器自动操作。

我们看到,最初的几次尝试(time<3时),我们都替它着急。它围着门转,就是不进去,有点傻。但是从第7次开始,它基本可以做到直奔目标了。

到后面,随着地图复杂,他甚至可以自己学会吃鸡腿,躲避雷电,而且以最简路径找到大门。

下面,咱就连做带学,搞搞这个项目。经过这个项目,你可以了解强化学习中最基础的、入门级别的Q-Learning算法。

二、编写游戏

有游戏编程经验的朋友,可以直接略过这部分。没有基础的朋友,留下来看看。否则影响后面的理解。

我喜欢写游戏,我从2011年就写棋牌类游戏。我感觉,写游戏更考验编程逻辑能力。

比如,你把一个数组转化为地图。

2.1 地图

比如下面这组数据。

python 复制代码
game_map = [ 
    [1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1]
]

它是一个56列的二维数组。我们可以认为它就是个Excel表格。

如果用1表示围墙,0表示空地,那么你要做的,其实就是把数字换成图片。

python 复制代码
from tkinter import *
from PIL import ImageTk, Image

rows = len(game_map)  # 数组行数
cols = len(game_map[0]) # 数组列数
cell_size = 48 # 一个格子的尺寸
width = cell_size*cols # 画面总宽度
height = cell_size*rows # 画面总高度

def draw(canvas, game_map):
    # 白色幕布
    canvas.create_rectangle(0, 0, width, height, fill="white")
    for y in range(rows): # 循环格子
        for x in range(cols):
            v = game_map[y][x] # 根据元素画出图形
            cell_x = x*cell_size
            cell_y = y*cell_size
            # 画出围墙
            if v == 1:
                canvas.create_image(cell_x, cell_y, anchor='nw', image=wall_img)

root = Tk()
wall = Image.open("wall.png") # 加载一个围墙图片
wall_img = ImageTk.PhotoImage(wall)
canvas = Canvas(root, width=width, height=height)
canvas.pack()
draw(canvas, game_map)
root.mainloop()

如果用python实现,那就是上面的代码。其实,我用多种编程语言都写过,基本就是循环元素然后在画布上绘制。

结果如下:

而角色和道具的引入,其实是加一些定义。比如2代表人物,3代表鸡腿,4代表雷电,5代表大门。

python 复制代码
# # 0空,1墙,2人,3鸡腿,4雷电,5穿越之门 
game_map = [ 
    [1, 1, 1, 1, 1, 1],
    [1, 2, 0, 3, 0, 1],
    [1, 0, 0, 0, 0, 1],
    [1, 0, 4, 0, 5, 1],
    [1, 1, 1, 1, 1, 1]
]

反映到代码上,则是多一些ifelse的判断。

python 复制代码
def draw(canvas, game_map):
   ......
    # 画出围墙
    if v == 1:
        canvas.create_image(cell_x, cell_y, anchor='nw', image=wall_img)
    elif v == 2: # 画人

    elif v == 3: # 画鸡腿
    ......

你或许以为这没啥厉害的。实际上,只需要改一改数组,就可以让你大开眼界。

了解了吧,你花钱充的关卡和道具,只不过是人家捏造的数组。

那么,如果让人物动起来呢?

2.2 动作

地图贯穿游戏的核心。人物在地图上走,其实也是改变数组中的元素。

人物元素所在的位置,可以用它在数组中的下标来表示。

python 复制代码
# # 0空,1墙,2人
game_map = [ 
    [1, 1, 1, 1],
    [1, 0, 0, 1],
    [1, 2, 0, 1],
    [1, 1, 1, 1]
]

在上面的地图中,人物2的坐标是p=game_map[2][1]也就是(2, 1),表示第3行第2列。

如果人物要往上走,比如此时你按下了向上的方向键。表现在数据里,其实就是行的索引减去1,列的索引不变,变成下面这样。

python 复制代码
# # 0空,1墙,2人
game_map = [ 
    [1, 1, 1, 1],
    [1, 2, 0, 1],
    [1, 0, 0, 1],
    [1, 1, 1, 1]
]

这个变化,如果画出来,那就是游戏人物往上走了一格。其实,小数据量下,直接看数组也能看出来。

2.3 奖励和输赢

找到了目的地,或者吃了鸡腿、误入了雷电,这些如何判断与反馈呢?

python 复制代码
# # 0空,1墙,2人,5门
game_map = [ 
    [1, 1, 1, 1],
    [1, 2, 5, 1],
    [1, 0, 0, 1],
    [1, 1, 1, 1]
]

上面的数据中,5 大门的位置我们是知道的,是(1, 2)第2行的第3列。那么现在2 人物位于(1,1),如果它往右走一步,就变成了(1, 1+1),这样就和5 大门重叠了,其实就是进入了大门。如果我们设置一个game_win游戏胜利的变量,此时可以将它置为true

而吃到鸡腿、踏入雷区也是一样判断,只不过是给total_reward总奖励这个变量+10或者-20的区别。

说实话,我真的很想写游戏教程,很有意思。不过下面该讲人工智能啦。

三、强化学习实战

针对上面的游戏,我们来编写强化学习算法流程。

在介绍强化学习之前,首先要讲几个基本概念。也就是术语啊,不懂术语不算入门。

3.1 概念

我打算用一句话把几个概念说完,一说大家都懂。

强化学习,就是训练智能体 (Agent)在环境 (Environment)中,以奖励 (Reward)为引导信号,让它可以基于当前状态 (State)作出合理动作 (Action)的一种策略(Policy)学习。

这一句话,在书里得讲好几个章节。我用一段话解释。

比如你在公司里摸鱼......不是,上班。你就是智能体 ,公司就是环境 。你加班写完一个功能模块,总监给你表扬并加薪,这就是奖励 。下一次,当你再面临时间紧任务重的状态 时,你会选择继续加班这个动作 。你因之前的反馈,学会了策略。相反,如果你多承担还受到非议(负奖励),你下次就不这么做了。这也是策略。从这个角度看,你在公司的行为,完全取决于公司的制度与文化,是公司打造了每一个员工的状态。

好了,了解完这些虚的概念,我们将思路迁移到代码。

本文要讲的Q-Learning强化学习算法,是一种计算方法,就像是3×7=21一样,它不依赖于任何框架,python环境即可。

3.2 构建代码

首先,我们来建立一个类,这个类里面要有智能体的一些概念,比如环境、状态、动作等。

我们新建一个environment.py文件,这里面将包含那些元素与数据。

3.3 环境

游戏中的环境,就是地图。为了便于学习,我们先准备一个简单的地图。

如下所示,这是一个5行6列的地图数组。一圈围墙,里面就一个人和一个门。

python 复制代码
# 1围墙,0空地,2人物,5目的地
MAP = [ 
    [1, 1, 1, 1, 1, 1],
    [1, 2, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 1],
    [1, 0, 0, 5, 0, 1],
    [1, 1, 1, 1, 1, 1]
]

为了便于低成本观察,我们搞一个很有意思的替换。现在很多图形文字可以在控制台打印了,我们将数字替换成图形字符。

python 复制代码
vdict = {0:"🟩", 1:"🧱", 2:"😂", 3:"🍗", 4:"⚡", 5:"🏠"}
str_map = '\n'.join([''.join([vdict[i] for i in line]) for line in MAP])
print("当前地图为:")
print(str_map)

这时再打印这个地图数据,就是下面这样:

哈哈,这样就很直观了。

3.4 智能体

environment.py中,写一个EnvAgent类,算是智能体、环境的合集。里面包含很多信息。

python 复制代码
class EnvAgent():

    def __init__(self):
        self.map = copy.deepcopy(MAP) # 地图
        self.x = 1 # x坐标
        self.y = 1 # y坐标
        self.step = 0 # 走了多少步
        self.total_reward = 0 # 获得多少奖励
        self.is_end = False # 游戏是否结束
    ......

其中,self.xself.y就是智能体当前位置,我们把它初始化在(1, 1)这个点,也就是左上角的位置。

除此之外,step是走了多少步,total_reward是获得多少奖励。

3.5 动作

下面再来说一下动作。游戏里的人物有4个动作,分别是:向上、向下、向左、向右。

我们分别用0123表示上下左右。人物往任意方向移动,会触发一些变化,代码如下:

python 复制代码
DX = [-1, 1, 0, 0]
DY = [0, 0, -1, 1]

class EnvAgent():
    ......
    # 根据指令,更新坐标,计算奖励
    def interact(self, action):
        assert self.is_end is False
        new_x = self.x + DX[action] # 新坐标
        new_y = self.y + DY[action]
        new_pos = self.map[new_x][new_y]
        self.step += 1
        reward = 0 
        if new_pos == 1: # 新位置是墙,保持原地,奖励0
            return reward
        # 其他区域可走,更新人物坐标
        self.x = new_x 
        self.y = new_y
        if new_pos == 0: # 新位置是空地
            reward = 0
        elif new_pos == 5: # 新位置是大门,更新坐标,奖励分数
            self.map[new_x][new_y] = 0 
            reward = 100
            self.is_end = True # 游戏结束
        self.total_reward += reward
        return reward
    ......

interact这个函数,作用是根据传来的action进行改变,并获取走这一步的收益值。

DX = [-1, 1, 0, 0]DY = [0, 0, -1, 1]巧妙地对应上、下、左、右的0123。我们看,向上走action0,那DX[0]=-1说明行的索引-1DY[1]=0则表示列位置不变。同理,向右是3DX[3]=0, DY[3]=1就是行不变,列向右移动1格。

移动完毕之后,获得一个新位置new_pos。然后对新位置进行判断。

  • 如果new_pos是墙,根据规则,墙过不去,那么这一步白走,浪费一次选择step+1,返回0奖励。

  • 如果new_pos是空地,那么更新人物坐标到空地,因为它现在人走到那儿了。走空地没什么奖励。

  • 如果new_pos是大门,这说明是重大突破,除了更新坐标之外,本次行为给100奖励并通报表扬。

这是基础的设置,算是最小闭环。如果你想要拓展,其实就是多加几个判断,例如遇到鸡腿加分,遇到闪电减分。

python 复制代码
......
elif new_pos == 3: # 遇到鸡腿
    self.map[new_x][new_y] = 0 
    reward = 10
elif new_pos == 4: # 遇到闪电
    reward = -25

if self.total_reward < -100 and self.step > 100:
    self.is_end = True # 游戏结束
......

上面的代码表示,遇到鸡腿加10分,鸡腿消失。遇到闪电减25分,闪电不消失,下次还可能遇到。后面又加了一段,如果连续遇到4次闪电,并且走了100步也没找到出口,那么你太菜了,游戏结束。

3.6 状态

强化学习里的这个状态,我认为叫"局势"更为贴切。

这个游戏里面,刚开局时,人物距离大门很远,这是一种状态。人物和大门挨着,再走一步就胜利了,这也是一种状态。

两种状态下,同样是右移一格的动作,获得的奖励是不一样的。因此,脱离了环境的状态是没有意义的。这就是我觉得"局势"更贴切的原因。强化学习就是要在某种环境下,根据状态作出有最大奖励值的动作。

在这个游戏里面,有几种状态呢?

python 复制代码
class EnvAgent():

    @property
    def state_num(self): # 状态数
        rows = len(self.map)
        cols = len(self.map[0])
        return rows * cols

    @property
    def present_state(self): # 当前的状态
        cols = len(self.map[0])
        return self.x * cols + self.y
    ......

结论是有几个格子就有几种状态。要获取自己目前处于什么状态,计算一下当前的位置就可以了。

大家需要注意。这里的状态是一维数组。地图是5×6,而它的状态却需要用第29种状态来表示。

正常情况下,人物不会出现在围墙上。状态应该减去四面围墙,应该是(5-2)×(6-2)=12种状态。但实际上这对于结果影响不大,此处便于理解不做更细致处理。

到这里,准备工作就完成了。看我上面讲了那么多,其实就54行代码。

下面,就该强化学习上场啦。哈哈,整个项目,总共不超过100行代码。

3.7 Q-Learning算法

我还是先上代码吧。新建一个qlearning.py文件,代码如下:

python 复制代码
import numpy as np
import time
from environment import EnvAgent

EPSILON = 0.1
ALPHA = 0.1
GAMMA = 0.9
np.random.seed(0)

def epsilon_greedy(Q, state):
    if (np.random.uniform() > 1 - EPSILON) or ((Q[state, :] == 0).all()):
        action = np.random.randint(0, 4)  # 0~3
    else:
        action = Q[state, :].argmax()
    return action

e = EnvAgent()
Q = np.zeros((e.state_num, 4))

for i in range(80):
    e = EnvAgent()
    while (e.is_end is False) and (e.step < 40):
        action = epsilon_greedy(Q, e.present_state)
        state = e.present_state
        reward = e.interact(action)
        new_state = e.present_state
        Q[state, action] = (1 - ALPHA) * Q[state, action] + ALPHA * (reward + GAMMA * Q[new_state, :].max())
        e.print_map()
        time.sleep(0.1)
    print('次数:', i+1, '步数:', e.step, '得分:', e.total_reward)
    time.sleep(2)

54行的environment.py,加上这32行的qlearning.py,总共86行代码。放在同一个目录下,然后运行python qlearning.py。你可以从控制台看到如下结果:

我们看到,一开始它四处乱撞,但是从第6次开始便可以稳定在4步内直捣黄龙。

如果代码能跑通,你可以自己设计一下地图,扩大一下面积,加点其他道具,让它探索试试。

虽然只有几十行代码,但这也可能是本文最烧脑的地方。它的核心原理就在这儿。

Q-Learning算法首先讲一个Q。我们看代码中有一个Q = np.zeros((e.state_num, 4))。这里定义了一个全为0的矩阵。

这个矩阵的行数是状态数量,也就是5×6等于30个状态。列数是行为数量,就是上下左右4种行为。打印下来是这样的格式:

python 复制代码
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
        ......
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])

它究竟想要干什么呢?

其实它是想穷举所有状态下,每种动作的预测收益得分。

如果我们能知道人生在每个路口往哪个方向走,能得多少分。那么我们就能以它为参考,作出一个高分选择。这个Q就是一张十字路口预测表。

上图所示,18号位置,往左走一步就胜利了,因此这个动作的得分是100。相对而言,如果往上走就远离目标了,预计会减分。这就是Q-Learning算法的核心。

为了找到这些数值,把那些全0数据填满,代码中for i in range(80)进行80次尝试。每次尝试的条件是游戏没结束,或者e.step < 40还在40步内(不能无限寻找)。

那么重点来了,这里面的值是如何确定的呢?

还记得我们前面给智能体设置了个reward = e.interact(action),也就是走某一步返回得分。那是我们自己定义的,比如吃到鸡腿得10分,找到大门给100分。这种奖励是一次性的固定值,只针对某一步。状态16走到状态17进了大门,得100分。那从0开始到16这段距离就没有功劳吗?它得是连续更新才行。因此没法直接用这个reward,得处理一下。

看上面代码,Q[state, action]表示某个状态对于某一步的预测得分。知道了这个值,我们才能知道往哪个方向走最受益。它等于(1 - ALPHA) * Q[state, action] + ALPHA * (reward + GAMMA * Q[new_state, :].max())

ALPHA是学习率,前面定义为0.1Q[new_state, :].max()表示在Q矩阵中选择这个新状态下得分最高的一项。这一项的得分,加上规则定义的得分reward。这俩加起来还不能全信,只信10%。另外90%我信上一个状态的功劳,没有前面就没有后面。GAMMA是衰减系数,它和预测新状态的最高分相乘,表示也不能全信。

抽离出来这里面的关键点就是Q[预测得分] = 0.9*Q[旧状态]+0.1*Q[新状态]。这样做,整个链条的每一步,都会影响Q参考表,每一步都会向着得最高分去努力。

我的科普读者们,我知道你或许会有一丝儿听不懂。想要完全听懂,可能我还得再写这么一篇才行。你不用灰心,其他人也听不懂。如果你明白了一点,说明你很棒。

我们在每一次尝试后,打印Q的数值,格式如下:

python 复制代码
 [ 0.     0.     0.     0.   ]
 [ 0.     0.     0.     0.081]
 [ 0.     2.52   0.     0.   ]
    ......
 [ 0.    27.1    0.     0.   ]
 [ 0.     0.     0.     0.   ] 

其实从数据可以看到,并不是只有进大门那一瞬间有收益值。前面的几步,有个向右走得0.081分。后面还有个往下走得2.52分。虽然离目的地很远,尽管数值很微弱,但也是一步步引导向高分迈进的。先有它们,才有后面的27.1分。而那些墙壁位置的状态,则长期为0。因为游戏人物不与它交互,不会有奖励反馈(撞墙扣分除外)。这就是前面我说算上它们,对结果也没影响的原因。

好了,最难的原理也解释完了。最后做一下总结。

四、总结

这个Q-Learning算法的强化学习例子很初级。其实还有更多强化学习算法值得学习,希望大家看完本文能激发起学习的兴起。

比如Sarsa算法,和Q-Learning类似,是无模型的算法,但是它能在行动后更新状态值。相对于深度学习之于机器学习,强化学习也有个深度强化学习算法,比如Deep Q Learning (DQN),它用神经网络来估算Q值。此外,与基于值的方法不同,策略梯度算法(Policy Gradients)是直接优化策略本身。

可能是出于男孩的原因。我幼儿时期脑海中的人工智能,居然就是强化学习。它可以自动打游戏,可以帮我跟老头下棋,可以自己开车。

最后说一点,我的游戏是画面的,例子是命令行打印的字符。主要目的是让大家将注意力集中到学习算法。代码就86行,我就不上传Github了,自己敲更有意思。

我是掘金@TF男孩,是个怀旧的人,最近比较关注传统算法。

相关推荐
Kenneth風车1 分钟前
【机器学习(九)】分类和回归任务-多层感知机 (MLP) -Sentosa_DSML社区版
人工智能·算法·低代码·机器学习·分类·数据分析·回归
知来者逆5 分钟前
ChemChat——大语言模型与化学的未来,以及整合外部工具和聊天机器人的潜力
人工智能·gpt·语言模型·自然语言处理·机器人·llm·大语言模型
AI领航者5 分钟前
大型语言模型的结构性幻觉:不可避免的局限性
人工智能·语言模型·自然语言处理·llm·大语言模型·ai大模型·大模型幻觉
fydw_7155 分钟前
PyTorch 池化层详解
人工智能·深度学习
奥利给少年29 分钟前
深度学习——管理模型的参数
人工智能·深度学习
小羊在奋斗1 小时前
【C++】探秘二叉搜索树
c++·人工智能·神经网络·机器学习
m0_713344851 小时前
新能源汽车数据大全(产销数据\充电桩\专利等)
大数据·人工智能·新能源汽车
NewsMash1 小时前
平安养老险阜阳中心支公司开展金融教育宣传专项活动
人工智能·金融
白葵新1 小时前
PCL addLine可视化K近邻
c++·人工智能·算法·计算机视觉·3d
说私域2 小时前
开源 AI 智能名片 S2B2C 商城小程序与正能量融入对社群归属感的影响
人工智能·小程序