首页 » 软件开发 » 一文学会人工智能强化学习的编程(行号迷宫表头价值策略)

一文学会人工智能强化学习的编程(行号迷宫表头价值策略)

神尊大人 2024-07-24 04:54:41 0

扫一扫用手机浏览

文章目录 [+]

我们不仅是从零开始学习强化学习,而且是从零开始学习编程,接着编写一个真实的“强化学习”应用程序,这个程序将会非常有趣。
让我们一起开始吧!

强化学习是机器学习领域中的一种重要方法。
它能够通过与环境的交互来不断学习和改进自己的行为,因此被广泛运用于人工智能领域。

近年来,强化学习在各个领域都取得了重要的进展,其中最为著名的例子是人工智能软件阿尔法狗在围棋领域的胜利。
通过强化学习的训练,阿尔法狗成功地战胜了围棋世界冠军,成为了人工智能领域的一项重要突破。

2023年备受瞩目的人工智能大语言模型,如ChatGPT等,已经具备了与人类自由对话的能力,并且可以进行诗歌创作、写文章、绘画、翻译、编程、游戏制作等多种任务,这些能力让它们在很多领域都可以替代人类完成工作。

我和ChatGPT简单聊了聊,问它使用了哪些学习方法?它回答说使用了包括强化学习在内的三种学习方式。

首先我们讲软件编程的快速入门,同时完成走迷宫程序的基础工作。

学习的最低要求是电脑中装有微软的Office办公软件,版本不限。
只要你会Excel表格的基本操作,就OK了。
不用安装任何编程语言和开发系统。

如果电脑中只有WPS Office,理论上也可以,但需要下载一个插件才能用VBA编程。
有需要请留言。

我们会从VBA编程语言的入门讲起,让大家用最短的时间编写出自己的第一个计算机程序。
然后开始编写真实的人工智能程序,采用强化学习算法。

强化学习是谷歌的围棋AI阿尔法狗、OpenAI的红蓝小人捉迷藏以及ChatGPT等都用到的人工智能算法。

我们会在Excel表格中画一个小型迷宫,构造三个表格作为人工智能的“参数”,老鼠要学会走迷宫,就是通过这些参数得到“智能”的。

这个教程也非常适合假期中的学生们,自学编程语言和人工智能编程。

下面我们开始:

01 在Excel中编写和运行程序

微软的表格软件Excel自带编程语言:VBA,这是一种可视化的BASIC语言,特别适合初学者学习编程。

第一步:打开Excel的编程窗口

打开Excel软件,新建一个空白表格。
然后按 Alt+F11 组合键,打开编程窗口。
组合键的按法是先按住Alt键不放,再按功能键F11。
结果如下图:

右侧的窗口就是用来编写程序的界面,请按照上图所示,适当调整编程窗口的大小,使其在占据屏幕的一半,同时露出一半的表格。

然后按照图中标示①②的顺序,点开小框中的“+”号变为“-”号,再双击“Sheet1”,把光标移到③处,就可以在里面写程序代码了。

第二步:编写第一个程序代码

每一个程序必须由“Sub”开头,空格,接『程序名称』及一对空括号,然后回车。
『程序名称』可以用英文及汉字。
用“End Sub”结束这段程序。
例如:

Sub 初始化()End Sub

如下图:

特别要强调的是,在程序中除了名称可以用汉字以外,其他的字符以及标点符号,都必须在英文状态下输入。
否则会提示“无效字符”。
最容易出错的是标点符号,如逗号『,』、句点『.』、单引号『'』、双引号『""』、括号『()』等,都必须是英文符号。

在一个程序中,可以写多个以“Sub”开头,并以“End Sub”结尾的程序,这段程序称为一个“过程”或“子程序”。

在“Sub”与“End Sub”之间,写入自编的程序代码。

第三步:在过程内写程序代码

把光标移到Sub过程内部,写入第一条语句:

Cells(1,1)=1

这条语句的功能是,向指定的单元格中填写指定内容。
格式如下:

例如:Cells(1,1)=1,这句话的意思是,向第1行第1列的单元格中写入数字1。

我们在这个Sub过程里写三个Cells()语句:

Sub 初始化()Cells(1,1)=1Cells(1,2)=2Cells(1,3)=3End Sub

注意,程序语句通常要比Sub向右缩进4个空格,这样使程序看起来层次分明。
按一下Tab键即可实现缩进功能。
完整代码如下图:

下面我们看看怎么运行这段程序。

第四步:运行程序

把光标移到Sub过程内部,然后按功能键F8,可以看到 Sub语句的背景变成黄色,代表程序将从此处开始执行。

如果运行程序时提示错误。
请跳到第五步 设置安全级别。

F8键的功能是按顺序执行一句程序,专业名称叫“单步执行”。

再按F8键,让黄色背景移到下一句 Cells(1,1)=1 上,代表此句将被执行。

再按F8键,黄色背景又下移一行。
此时上一句已经执行完毕,我们看看结果是什么:

我们看到表格中的第1行第1列的单元格中被填入数字1。

我们的第一句程序执行成功了。

再按两次F8键,让后两条语句也执行完毕。
我们看到,表格的第1行第2列和第3列中分别被填入数字2和3,如下图:

至此,这段小小的程序全部执行完毕。
我们编写的第一个程序大功告成了。

不过,这么一下一下地按F8执行程序也太累了吧,能不能一键执行到底呢?

当然可以!

按功能键F5可以一键执行程序。

之所以先讲F8单步执行,是为了能让大家能够直观看到程序的执行顺序。
以后在调试程序时,用单步执行也会非常方便。

注意,如果我们编写了多个Sub过程,一定要把光标移到想要运行的Sub过程内部,再按F5,这个过程才会被执行。
而其它过程则不会被执行,如下图:

最后总结一下,VBA编程常用的功能键如下图所示:

以上就是在Excel中用VBA语言编写和运行程序的全部步骤。
一点儿也不难吧?

第五步:设置安全级别

如果前四步一切正常,可以跳过这一步不看。

如果程序无法运行,提示程序被“禁止”,这是由于Excel的安全级别设置过高,不允许运行任何程序。
需要重新设置“宏安全性”。

请按下图4个步骤,将安全级别设置为:“禁用所有宏,并发出通知”。

如果在Excel主菜单找不到步骤①中的“开发工具”选项,不要着急。

点击主菜单的“文件”→“选项”→“自定义功能区”,在“主选项卡”下,选中“开发工具”复选框,“开发工具”即可出现。
如下图所示4个步骤:

有些较低版本的Excel软件,安全性设置在:“工具”→“宏”→“安全性”选项下,请把安全级别设为“中”即可。

02 编程基本操作

我们已经写了3句代码,在表格第1行前三列填入了1、2、3三个数字。
下面接着在后面依次填入4、5、6、……,直到20,一列填一个数字。

如果还用上面的方法,就得傻傻地写20条Cells()语句才能完成。
我们使用循环语句,几句代码就可以实现。
步骤如下:

(1)定义变量。

我们在中学就学过代数,可用字母表示数字,如x=8,y=0.5等,这些字母在程序中称为变量。

在使用变量前,需要先定义变量的类型,方法如下:

例如:(注:以下程序代码显示不全时,可左右滑动)

Dim 列号 As Integer '把变量『列号』定义为整型,只能表达整数Dim x As Single '把变量『x』定义为浮点型,可表达带小数点的数Dim 姓名 As String '把变量『姓名』定义为字符串,可表达“张三”等单词或汉字

通常把所有的定义变量的语句都集中在一起,放在Sub过程内的上方。

另外,我们看到在上面的Dim语句后面,还跟着一段由英文的单引号『‘ 』开头的一段话,这是注释语句,显示为绿色。
它是给阅读者看的,用于说明程序的功能,以方便理解。
这种由单引号开头的注释语句不是程序,不会被执行。

(2)For循环语句

For循环语句由For开头,Next结束。
中间是循环体,如下图:

例如:

Dim 列号 As Integer '把变量 列号 定义为整型For 列号 = 1 To 20 '列号从1到20循环Cells(1, 列号) = 列号 '循环体,向指定单元格写入数据Next 列号 '每循环一次,列号加1,大于20后退出循环

这段程序从『列号=1』开始执行,执行到『Next』语句后,自动给『列号』 加1,然后跳回到『For』语句重新判断『列号』。

如果『列号』没有超过20,则再次执行循环体中的代码。

一直重复循环以上过程,直到当列号超过20后,退出循环,执行Next后的下一句程序。

完整的代码如下图。
请把光标移到Sub过程内部,按F5执行这段程序。

结果是:在表格第1行的每一列,依次从1写到20,见下图。

这一串连续的数字就是表格的列号,我们留着它供以后使用。

如果你还没有搞明白循环语句的用法,建议按 F8键单步执行上面的程序,可以直观看到循环语句是如何工作的,还能看到循环变量『列号』的值的变化过程。
如下图:

(3)保存程序

程序编好后,要保存起来,以便下次使用。
方法是:

点选主菜单的“文件”→“另存为”→“其他格式”。

然后点选“文件格式”,选择“Excel 启用宏的工作簿(.xlsm)”,输入文件名“老鼠走迷宫.xlsm”,再点“保存”按钮即可。
如下图:

注意,必须选用后缀为.xlsm的文件格式存盘,选用其他文件格式会导致程序丢失。

有些较低版本的Excel 没有.xlsm文件类型选项,那么就按照正常方式存盘即可。

(4)打开已保存的程序

在Excel中点选主菜单的“文件”→“打开”,选择刚才保存的“老鼠走迷宫.xlsm”。
文件打开后,通常会有一个安全警告,如下图黄色背景部分:

这是为了防止未知文件中的宏病毒。
我们已知这个文件是安全的,所以点击“启用内容”按钮即可。

对于较低版本的Excel,安全警告可能是个对话框,同样选择“启用宏”按钮。

启用后,请按组合键 Alt + F11 打开编程窗口,即可看到上次编写的程序了。
尝试运行一下程序,检查是否正常。

如果打开程序或运行程序出现问题,请跳回到上面的第五步:设置安全级别。

以上是Excel VBA编程的基本入门。
下面我们进入实质编程阶段。

03 画迷宫

首先在Excel表格中,画一个三行三列的迷宫。

我们把迷宫的左上角,也就是起点,放在整张表格的第4行,第2列。

因为这两个数字在后面的程序中会经常用到,我们定义两个常量代表这两个数字。
定义常量的方法如下:

Const 迷宫头行 = 4 '迷宫左上角的行号Const 迷宫头列 = 2 '迷宫左上角的列号

这样定义完以后,只要在程序中使用[『迷宫头行』,就代表数字4,使用『迷宫头列』 就代表数字2。

我们把定义常量的代码放在Sub过程外面,编程窗口的最顶端。
如下图所示。

这样做的原因是:在Sub过程外面定义的常量或变量,所有Sub过程都可以使用。

然后,我们开始编写第二个过程“画迷宫”,写在第一个Sub 初始化()过程的下面(如上图):

Sub 画迷宫()End Sub

接着在这个新的Sub过程里面,写入以下代码:

Dim 迷宫行号 As Integer '迷宫内部的相对行号Dim 迷宫列号 As Integer '迷宫内部的相对列号Dim 迷宫编号 As Integer '迷宫内单元格的编号

这三条语句定义了三个变量,写在Sub过程内部,称为“局部变量”。
它们与定义在Sub过程外部的变量不同,局部变量只在Sub过程内有效,别的Sub过程不能使用。
另一方面,在不同的Sub过程内可以定义的相同名字的变量,它们之间互不影响。

变量的含义在后面的注释语句中已经写了。
我们以后用到它们的时候再详细解释。

接着写入以下代码:

'以下画迷宫边框,并设置迷宫区域的字体属性With Range(Cells(迷宫头行, 迷宫头列), Cells(迷宫头行 + 2, 迷宫头列 + 2)) .BorderAround LineStyle:=xlDouble '在迷宫四周画出双线边框.HorizontalAlignment = xlCenter '设字符居中显示.Font.ColorIndex = 15 '设字体为灰色.Font.FontStyle = "Bold" '设字体为粗体End With

以With开头的第一个语句中,Range()语句的功能是定义一个矩形区域,括号里的两个Cells()分别为矩形区域的左上角和右下角的单元格。

Cells()里使用了我们刚定义的两个常量『迷宫头行』和『迷宫头列』,分别代表4和2。
也就是说,它的实际含义是:

Range(Cells(4, 2), Cells(6, 4))

即以第4行第2列的单元格为左上角,以第6行第4列的单元格为右下角,组成的3行3列的矩形方块。
如下图:

可以看出,Cells()语句内的行号,列号可以写成数字、常量、变量或表达式,非常灵活。
这就是编程的特点,大部分代码都可以这样用。

有人会问:为什么在程序里不直接写Cells(4, 2), Cells(6, 4),而要用那么长的名字呢?

这样做是为了以后需要改变数字时,便于修改。

接着看With语句下面的4句代码,功能是画出这个矩形四个边框,并设置字体属性。
其中的英文字符,都是Excel手册规定的,查出来使用即可,没必要记。

代码的开头或中间有一个或多个小句点,其含义是:小句点后面的对象属于前者。

比如,中国.北京,含义是:中国的北京。

如果写成“中国.华盛顿”,就是错的,因为中国没有这个对象。

每个对象还可以有自己的子对象,因此,可以用多个句点连着写多级。
比如:

中国.北京.清华大学.法学院.某某班.某某人.年龄=20。

最后一级的对象通常是前面对象的属性,比如某人的年龄、身高、体重、学习成绩等等都是属性。

在VBA 中,单元格、矩形块、表格等都称为对象。
这种“面向对象”的表达方式,使归属关系非常清晰,即使有重名的对象也不会造成混乱。

通常,当一个对象有多个属性需要设置或使用时,就可以用With语句。

以With开头,以End With结束。
用法如下:

把以上的代码复制或录入到Sub 画迷宫()过程内,然后按F5键运行,看看是否画出一个矩形边框。

再强调一遍,按F5键运行前,要把光标移到Sub过程内,才能运行该过程。

如果光标在Sub过程外部,按F5键后会弹出菜单,让你选择要运行哪个过程。
如下图:

选择“画迷宫”,点击“运行”按钮即可执行。

下面4句代码画迷宫的内部的墙,方法是用Cells(行号,列号)语句指定某个单元格,然后画该单元格某一边的边框,一次画一条。

'以下画迷宫内部墙Cells(迷宫头行 + 0, 迷宫头列 + 0).Borders(xlEdgeRight).LineStyle = xlDouble '指定单元格右边画双线边框Cells(迷宫头行 + 1, 迷宫头列 + 0).Borders(xlEdgeBottom).LineStyle = xlDouble '指定单元格下边双线边框Cells(迷宫头行 + 1, 迷宫头列 + 2).Borders(xlEdgeBottom).LineStyle = xlDouble '指定单元格下边双线边框Cells(迷宫头行 + 1, 迷宫头列 + 1).Borders(xlEdgeTop).LineStyle = xlDouble '指定单元格上边双线边框

其中的行号,列号都是以常量『迷宫头行』和『迷宫头列』为基准的,即以第4行第2列为基准,分别+0、+1、+2来指定对应的单元格。

下一步画迷宫内部单元格的编号。

此处需要使用在Sub过程开头定义的三个局部变量。

其中『迷宫行号』和『迷宫列号』是迷宫内部相对于左上角的行号和列号,取值分别为0、1、2。
即在迷宫内部的第0行、1行和2行,或第0列、1列或2列。

迷宫左上角的位置由常量『迷宫头行』和『迷宫头列』决定。

例如:迷宫内的第0行第1列,即:迷宫行号=0,迷宫列号=1。
该单元格表示为:Cells(迷宫头行 + 迷宫行号, 迷宫头列 + 迷宫列号)

'以下画迷宫内部单元格的编号 迷宫编号 = 0 '设迷宫编号的初始值为0 For 迷宫行号 = 0 To 2 '迷宫当前行号=0,1,2 For 迷宫列号 = 0 To 2 '迷宫当前列号=0,1,2 Cells(迷宫头行 + 迷宫行号, 迷宫头列 + 迷宫列号) = 迷宫编号 '画迷宫内单元格的编号 迷宫编号 = 迷宫编号 + 1 '修改迷宫编号变为当前值+1 Next 迷宫列号 Next 迷宫行号

这是两个嵌套在一起的For循环语句。

外圈的For循环语句,一共循环三轮,『迷宫行号』分别取值0、1、2。

外圈的For语句每循环一轮,都要进入内圈For循环语句,并循环三轮,『迷宫列号』的取值分别为:0、1、2。

总共进行3x3=9轮循环。

『迷宫编号』初值为0,每循环一次加1,在9轮循环后,其值从0加到8 。

这两个For循环语句完整执行过程如下图的箭头所示:

蓝色箭头是内圈的列号循环,红色箭头是外圈的行号循环。
9次循环后。
迷宫内的9个单元格依次被写入0~8。

最后,我们在迷宫左上角写“老鼠”,右下角写“食物”。

注意,在单元格中所写的字符,必须用英文的双引号括起来,不能用中文的双引号:

'以下写"老鼠"和"食物" Cells(迷宫头行, 迷宫头列) = "老鼠" '在迷宫0号单元格写“老鼠” Cells(迷宫头行, 迷宫头列).Font.ColorIndex = 1 '字体设为黑色 Cells(迷宫头行 + 2, 迷宫头列 + 2) = "食物" '在迷宫8号单元格写“食物” Cells(迷宫头行 + 2, 迷宫头列 + 2).Font.ColorIndex = 3 '字体设为红色

结尾 End Sub,画迷宫结束。

请把光标移到Sub 画迷宫() 的内部,按F5键运行此程序。

一个Sub过程除了可以直接运行之外,在别的Sub过程内部,写这个过程的名字,也可以调用此过程。
调用时,不用写Sub,一般也不用写括号。

例如,我们在Sub 初始化()内部,加入俩行,如下所示:

Sub 初始化() Dim 列号 As Integer '把变量 列号 定义为整型 For 列号 = 1 To 20 '列号从1到20循环 Cells(1, 列号) = 列号 '循环体,向指定单元格写入数据 Next 列号 '每循环一次列号加1,大于20后退出循环 Range(Cells(2, 1), Cells(26, 12)).Clear '清空指定矩形区域 画迷宫 '调用“画迷宫()”过程 End Sub

最后两句是新增的,第一句用于清空一块矩形区域,区域大小由左上角和右下角的单元格确定。
第二句调用『画迷宫』过程。

这样,当我们运行『初始化』过程时,即可自动运行『画迷宫』过程。
运行结果如下图:

请把上面的代码复制或录入到编程窗口中,按F5键运行一下,看看是否能画出同样的迷宫。

如果对这段程序有不明白的地方,建议按F8键单步执行,观看程序的执行流程,可以帮助理解。

04 画参数表

迷宫画好后,我们要做的工作是让老鼠动起来。

怎么动呢?假设老鼠在迷宫内某一个单元格中,有上、下、左、右4个方向可以移动。
但是,迷宫中有墙,被墙挡住的方向不能移动。

所以,需要让老鼠知道,往哪个方向可以移动,哪个方向不能移动。
否则它可能会随意穿墙乱走。

为此,我们建一张表,把迷宫中每个单元格的4个方向都列出来,在有墙的方向写0,没有墙的方向写1。
这样,老鼠根据表中的数值是1还是0,就知道这个方向是能走还是不能走。

迷宫一共有9个单元格,每个格有4个方向。
所以我们建一张9行4列的表,就可以把所有可能的情况表示出来。
这个表称为“动作参数表”。

在人工智能中,直接建表是处理参数最简单的方式。
然而,不是所有的应用都可以直接建表的,或者是直接建表的参数过于巨大,不经济。
这种情况下,通常用神经网络模型来表达,例如在处理文字、图形、语言等复杂情况时。
ChatGPT处理自然语言时,就使用了神经网络技术,其中包含大量的参数。

我们把动作参数表的左上角,放在表格的第4行,第8列,我们定义两个常量代表这两个数字。
定义常量的代码如下:

Const 参数表头行 = 4 '动作参数表左上角行号Const 参数表头列 = 8 '动作参数表左上角列号

这两句代码与之前定义的两个迷宫常量要放在一起,放在整个程序的最上方,如下图:

然后我们创建一个新的过程:画动作参数表,并定义内部使用的局部变量。
如下:

Sub 画动作参数表() '画动作参数表(θ) Dim 迷宫行号 As Integer '迷宫内部行号 Dim 迷宫列号 As Integer '迷宫内部列号 Dim 迷宫编号 As Integer '迷宫位置编号 Dim 参数表行号 As Integer '动作参数表当前行号 Dim 参数表列号 As Integer '动作参数表当前列号End Sub

接着,在Dim语句下面写“画表头”的语句,如下:

'画动作参数表表头 Cells(参数表头行 - 2, 参数表头列 + 1).Value = "动作参数表θ(动作表格化)" Cells(参数表头行 - 2, 参数表头列 - 1) = "迷宫编号/" Cells(参数表头行 - 1, 参数表头列 - 1) = "移动方向" Cells(参数表头行 - 1, 参数表头列 + 0) = "上" Cells(参数表头行 - 1, 参数表头列 + 1) = "右" Cells(参数表头行 - 1, 参数表头列 + 2) = "下" Cells(参数表头行 - 1, 参数表头列 + 3) = "左" '画动作参数表边框 With Range(Cells(参数表头行, 参数表头列), Cells(参数表头行 + 8, 参数表头列 + 3)) .BorderAround LineStyle:=xlContinuous '画参数表四边单线边框 End With

然后,我们向动作参数表中填数据。

方法是从『迷宫编号』为0的单元格开始,一直到第8个迷宫单元格,依次判断该单元格的上、右、下、左四个方向有没有边框。

如果没有边框,代表可以通行,则向对应的参数表中填数字1 。

如果有边框,代表不能通行,则向对应的参数表中填数字0 。

如下图:

实现判断比较的功能需要使用IF分支语句,用法如下:

其中的『条件表达式』通常要比较两个常量或变量的大小,常用的比较运算符有:

比较运算符

描述

=

等于:前者的值是否等于后者的值?是则表达式为真。

<

小于:前者的值是否小于后者的值?是则表达式为真。

>

大于:前者的值是否大于后者的值?是则表达式为真。

<=

小于等于:前者的值是否小于等于后者的值?是则表达式为真。

>=

大于等于:前者的值是否大于等于后者的值?是则表达式为真。

<>

不等于:前者的值是否不等于后者的值?是则表达式为真。

表达式中也可以使用算数运算符,常用的如下:

算术运算符

描述

+

加:将两数相加,如a+b。

-

减:将两数相减,如a-b。

乘:将两数相乘,如ab。

/

除:将两数相除,如a/b。

^

幂:进行幂运算,如a^2表示a的2次方。

使用举例:

Sub 测试() '这是一个测试程序 Dim x As Integer '定义一个整型变量x x = 11 '给x赋值 If x <= 2 5 Then '如果x小于等于2乘以5(即10) Cells(2, 1) = "小" '则在第2行1列单元格中填入“小” 。
Else '其他情况,即x大于10 Cells(2, 1) = "大" '则在第2行1列单元格中填入“大”。
End If 'If语句结束End Sub

把这段代码复制或录入到编程窗口中,按F5或F8执行一下,看看结果如何。

试着修改 x=7 这句代码,把数字改成7以下的值,看看程序运行结果是否改变?

下面我们看往动作参数表中填写参数的代码:

'向表内填写动作参数 迷宫编号 = 0 For 迷宫行号 = 0 To 2 For 迷宫列号 = 0 To 2 参数表行号 = 参数表头行 + 迷宫编号 Cells(参数表行号, 参数表头列 - 1) = 迷宫编号 '动作参数表中左侧写迷宫编号 With Cells(迷宫头行 + 迷宫行号, 迷宫头列 + 迷宫列号) '指定迷宫某个单元格 '以下检查单元格上方有无边框 If .Borders(xlEdgeTop).LineStyle = -4142 Then '如果上方无边框 Cells(参数表行号, 参数表头列 + 0) = 1 '动作参数值填1 Else Cells(参数表行号, 参数表头列 + 0) = 0 '动作参数值填0 End If '以下检查单元格右方有无边框 If .Borders(xlEdgeRight).LineStyle = -4142 Then '如果右方无边框 Cells(参数表行号, 参数表头列 + 1) = 1 '动作参数值填1 Else Cells(参数表行号, 参数表头列 + 1) = 0 '动作参数值填0 End If '以下检查单元格下方有无边框 If .Borders(xlEdgeBottom).LineStyle = -4142 Then '如果下方无边框 Cells(参数表行号, 参数表头列 + 2) = 1 '动作参数值填1 Else Cells(参数表行号, 参数表头列 + 2) = 0 '动作参数值填0 End If '以下检查单元格左方有无边框 If .Borders(xlEdgeLeft).LineStyle = -4142 Then '如果左方无边框 Cells(参数表行号, 参数表头列 + 3) = 1 '动作参数值填1 Else Cells(参数表行号, 参数表头列 + 3) = 0 '动作参数值填0 End If End With 迷宫编号 = 迷宫编号 + 1 '迷宫编号+1 Next 迷宫列号 Next 迷宫行号

程序看起来很长,其实结构很简单。

程序的主体结构依然是两个嵌套在一起的For循环语句,用With语句依次选中迷宫中编号0到8的单元格,然后用4个IF分支语句分别判断该单元格的上、右、下、左四个方向有没有边框。

查手册得知,属性.LineStyle = -4142代表“无边框”。

把以上代码复制或录入到“Sub 画动作参数表()”过程中,完成该过程。

我们把调用“画动作参数表”的语句也加入到“Sub 初始化()”过程中。
如下:

Sub 初始化() Dim 列号 As Integer '把变量 列号 定义为整型 For 列号 = 1 To 20 '列号从1到20循环 Cells(1, 列号) = 列号 '循环体,向指定单元格写入数据 Next 列号 '每循环一次列号加1,大于20后退出循环 Range(Cells(2, 1), Cells(26, 12)).Clear '清空指定矩形区域 画迷宫 '调用“画迷宫()”过程 画动作参数表 '调用“画动作参数表()”过程End Sub

最后一句是新增的。
然后运行“初始化()”过程,结果如下图:

图中左上部分为迷宫,右上部分为动作参数表。
动作参数表共有9行4列。

其中,行的编号为0~8,每行代表迷宫内的一个单元格。
每列代表一个移动方向,共4列,分别代表“上、右、下、左”。

我们看表中红色方框圈住的第0行,它的四个参数值分别为:“0、0、1、0”,即“上、右、左”三个方向的参数为0 ,即不可移动。
而向“下”方向的参数为1,可以移动。

这四个参数与左下方的红色方框中“老鼠”的移动方向参数相同,如蓝色字所示。

迷宫中每个单元格的动作参数,以相同方式填入动作参数表中。

有了这张表,老鼠就知道往哪个方向能走,哪个方向不能走了。

下一步,我们的编程工作是让老鼠动起来,为此,还需要建立另一张表,称为“策略表”。

05 画策略表

前面我们已经画出“动作参数表”。
老鼠根据此表,无论到了哪个格子里,都能知道哪个方向可以走,哪个方向不能走。

有的格子,只有一个方向可走,有的格子,有两个或者多个方向可走。

那么,该如何选择方向呢?这是“策略表”负责的工作。

“策略表”与“动作参数表”大小相同,都是9行4列。
行号代表迷宫的编号,列号代表“上、右、下、左” 4个方向。

“策略表”中的值是每个可移动方向的平均概率。

例如,如果迷宫中一个格子的某个方向有墙,则向该方向移动的概率为0,所以“策略表”该方向的值填0。

如果某个格子只有一个方向可以移动,那么,向这个方向移动的概率是100%。

如果某个格子有两个方向可以移动,那么,每个方向的移动概率是1/2=50%。

如果某个格子有三个方向可以移动,那么,每个方向的移动概率是1/3=33.33%。

如果某个格子四个方向都可以移动,那么,每个方向的移动概率是1/4=25%。

这个概率值就是动作策略值。

可见,所谓“策略表”,就是取每个可移动方向的平均概率。

下面我们编程画策略表:

我们把策略表的左上角,放在表格的第16行,第2列,我们定义两个常量代表这两个数字。
定义常量的代码如下:

Const 策略表头行 = 16 '策略表左上角行号Const 策略表头列 = 2 '策略表左上角列号

这两句代码与之前定义的迷宫常量和参数表常量放在一起,放在整个程序的最上方,如下图:

然后我们创建一个新的过程:“画策略表”,并定义内部使用的局部变量。
如下:

Sub 画策略表() '设置动作方向的平均概率 Dim 参数之和 As Single '迷宫每个位置的动作参数之和 Dim 动作概率 As Single '迷宫每个位置动作概率(策略π) Dim 概率梯度 As Single '迷宫动作概率的增加梯度 Dim 行号 As Integer '参数表或策略表的行号 Dim 列号 As Integer '参数表或策略表的列号End Sub

接着,在Dim语句下面写“画表头”的语句,如下(代码显示不全可左右划动):

'画策略表表头 Cells(策略表头行 - 2, 策略表头列 + 1).Value = "策略表π(动作概率)" Cells(策略表头行 - 2, 策略表头列 - 1) = "位置编号/" Cells(策略表头行 - 1, 策略表头列 - 1) = "移动方向" Cells(策略表头行 - 1, 策略表头列 + 0) = "0-上" '画策略表动作的方向 Cells(策略表头行 - 1, 策略表头列 + 1) = "1-右" '画策略表动作的方向 Cells(策略表头行 - 1, 策略表头列 + 2) = "2-下" '画策略表动作的方向 Cells(策略表头行 - 1, 策略表头列 + 3) = "3-左" '画策略表动作的方向 '画策略表边框 With Range(Cells(策略表头行, 策略表头列), Cells(策略表头行 + 8, 策略表头列 + 3)) .BorderAround LineStyle:=xlContinuous '画策略表四边单线边框 End With

然后,我们向策略表中填数据。

实现方法是根据“动作参数表”,从第0行开始,首先对该行所有列的值求和,赋给变量“参数之和”。

然后用该行每一列的值除以“参数之和”,得到该方向的动作概率。

一般来说,“策略表”中可以直接填写动作概率值。
但是,为了后面编程方便,我们把动作概率值转成阶梯概率值,再填写。

举例来说,假设某一格子有三个方向可以移动,那么,每个方向的动作概率为1/3=0.3333。
如下表所示:

0-上

1-右

2-下

3-左

0

0.3333333

0.3333333

0.3333333

而“阶梯概率”,是把当前列的动作概率值与左边所有列的动作概率值累加。
例如,上表中第2列的值是0.3333,其左边两列的值分别为0和0.3333,三者累加和为0.6666。
因为累加值像阶梯一样逐渐上升,所以称为阶梯概率。
如下表:

0-上

1-右

2-下

3-左

0

0.3333333

0.666667

1

下图为阶梯概率的示意图:

下面是向策略表中填写阶梯概率的代码:

'填写策略表参数 For 行号 = 0 To 8 '策略表行号从0到8循环 Cells(策略表头行 + 行号, 策略表头列 - 1) = 行号 '在策略表左侧写迷宫编号 阶梯概率 = 0 '策略表每行的阶梯概率初值为0 For 列号 = 0 To 3 '策略表列号从0到3循环 '求参数表中每行的参数之和 参数之和 = Application.Sum(Range(Cells(参数表头行 + 行号, 参数表头列), Cells(参数表头行 + 行号, 参数表头列 + 3))) 动作概率 = Cells(参数表头行 + 行号, 参数表头列 + 列号) / 参数之和 '求单元格每个方向的动作概率 阶梯概率 = 阶梯概率 + 动作概率 Cells(策略表头行 + 行号, 策略表头列 + 列号) = 阶梯概率 '将阶梯概率填入策略表 Next 列号 Next 行号

程序的主体结构依然是两个嵌套在一起的For循环语句,外圈的For循环是策略表的行号,从0到8,共9行。
内圈的For循环是策略表的列号,从0到3,分别代表四个方向,即:0-上,1-右,2-下,3-左。

在循环体中,先调用Excel中的内部函数Sum()计算出当前行中的『参数之和』。

然后分别取每一列的参数值除以参数之和,得到『动作概率』。

最后分别按列累加『动作概率』得到『阶梯概率』,填写到策略表中。
结果如下图右下方表格:

有了策略表,就可以让老鼠按这个策略动起来了。

下面学一个新的编程方法:编写自定义函数。

06 自定义函数

举个例子,y=sin(x) 是一个三角函数。
我们任意给定一个x值,三角函数能计算出对应的y值。

比如,x=30(度),计算出:y=0.5

下图是用三角函数计算器计算sin(30)得出的结果:

所谓自定义函数,相当于编写这个计算器的某一个计算功能。

首先,定义一个函数,定义函数的方法如下:

Function 函数名(参数1 As 类型 , 参数2 As 类型 , ...... ) As 类型

[计算过程]

函数名=计算结果

End Function

第一个词Function表示要定义一个函数。

空一格后写函数名,可以用英文,也可以用中文。
函数名后跟一对括号。

括号里写函数的参数。
参数可以没有,也可以有一个或多个参数,多个参数用英文的逗号『,』隔开。

参数与变量一样,可以用英文,也可以用中文,参数名后面接As指定参数的类型。
参数的类型与变量的类型相同。

参数写完后,以括号结束。
括号外还有一个As用于指定函数返回值的类型。

例如:

Function 平方(数值 As Single) As Single

... ...

End Function

我们定义了一个叫做“平方”的函数,其中有一个参数,叫做“数值”,类型是浮点型。
函数的返回值类型也是浮点型。

当我们在编程环境中输入函数的定义后,按回车键,会自动出现End Function

,代表函数的结束。

我们在两句之间输入函数的内容,即计算这个“数值”的平方。
如下:

Function 平方(数值 As Single) As Single Dim 结果 As Single '定义一个浮点型变量 结果 = 数值 数值 '计算数值的平方 平方 = 结果 '把计算结果赋值给函数名End Function

这个函数很简单,功能是计算任意给定数值的平方。

在函数内部,首先定义一个变量“结果”,是浮点型变量。

然后把“数值”和“数值”相乘计算平方,并把计算结果负值给“结果”。

最后把“结果”赋值给函数名“平方”。
这一句必须写,否则,调用函数者将得不到返回的计算结果。

写好的函数需要在另一个过程或函数中调用,函数调用格式是:

接收者=函数名(参数1,参数2,......)

其中接收者可以是变量,也可以是某个输出对象,如单元格。

调用的函数名必须与自定义的函数名完全一样。

括号里的参数数量、参数类型和前后顺序必须与定义函数时的参数完全一样,否则会出错。
参数可以是变量,也可以直接写数值。
变量名称可以与函数定义的参数名称不同。

下面我们写一个测试过程,调用“平方”函数。
如下:

Sub 测试() Cells(2, 1) = 平方(2) '计算2的平方,并填入单元格 Cells(2, 2) = 平方(3) '计算3的平方,并填入单元格 Cells(2, 2) = 平方(4.6) '计算4.6的平方,并填入单元格End Sub

请把光标移到“Sub 测试()”过程内部,按F5键执行,计算结果如下:

4

9

21.15999985

可以看出,编写自定义函数的好处是,可以多次调用函数,计算不同的值,避免重复写代码。

除了自定义函数以外,前面介绍过的Sub过程,同样可以使用参数,定义和调用方法与Function函数完全一致。
后面我们会编写带参数的Sub过程。

07 策略选择函数

根据画好的策略表,老鼠可以进行策略选择,以确定下一步的移动方向。

策略选择的方式很简单,首先产生一个0~1之间的随机数,根据随机数的值查策略表,随机数落在哪个范围区间,就选择该区间所对应的列作为移动的方向。

比如,假设某行的策略表的值如下所示:

0-上

1-右

2-下

3-左

0

0.3333333

0.666667

1

如果得到一个随机数是0.5,根据上表,0.5处于0.3333和0.6666之间,我们统一向取右边界取值,取0.6666对应的方向,即取第2列,方向为“下”。

策略选择的程序是一个自定义函数,名为“策略选择”,包含一个参数“位置编号”。
这个参数是老鼠在迷宫中的当前单元格编号,也是参数表或策略表的当前行号。
函数代码如下:

Function 策略选择(位置编号 As Integer) As Integer '按策略表选择指定行号的动作方向 Dim 随机数 As Single '浮点型变量:随机数 Dim 策略值 As Single '浮点型变量:策略表中的值 Dim 策略表列号 As Integer '策略表列号,取值:0、1、2、3 Randomize (Timer) '初始化随机数 随机数 = Rnd() '取得一个0~1之间的随机数 '按照策略表中的动作概率确定移动方向 For 策略表列号 = 0 To 3 '依次指定策略表的四个方向 策略值 = Cells(策略表头行 + 位置编号, 策略表头列 + 策略表列号) '从策略表取出指定行列的策略值 If 随机数 <= 策略值 Then '如果随机数小于等于该策略值 策略选择 = 策略表列号 '返回策略表的当前列号 Cells(策略表头行 + 位置编号, 策略表头列 + 策略表列号).Font.ColorIndex = 7 '策略表被选中的参数变粉色 Exit For '跳出循环 End If Next 策略表列号 End Function

函数在内部产生一个0~1之间的随机数,然后根据参数传入的指定行,依次判断每一列的策略值,如果随机数小于等于某列的策略值,则把该列号返回给函数调用者,并跳出循环,函数结束。

08 求取下一步的行号和列号

函数“策略选择()”可以得到下一步的移动方向,接着我们需要根据这个方向求出老鼠移动后的位置,即在迷宫中的新的行号和列号。

我们创建一个Sub()过程实现这个功能。
过程名字叫“下一步位置”,这个过程与自定义函数Function()一样带有参数。

过程“下一步位置”的定义如下:

Sub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer)End Sub

这个过程的括号里有三个参数:“当前方向”、“当前行”、“当前列”,数据类型都是整型(Integer)。
但是,在“当前行”和“当前列”前面,还有一个特别的词“ByRef”,它的意思是“传地址”。

“传地址”是一种特殊的参数传递方式,可以简单理解为参数的值可以“双向传送”。

如果定义参数时,参数名称之前不写“ByRef”,如本例第一个参数“当前方向”前面就没有“ByRef”,那么这个参数只能单向传送数据,即只能从调用者向过程或函数内部传递,而不能反向传递。

反之,如果参数名称之前写了“ByRef”,比如第二和第三个参数“当前行”和“当前列”,那么这个参数可以双向传送数据,既可以从调用者向过程或函数内部传递,又可以反向从过程或函数向调用者传递数据。

下面看这个过程的实现方式。
假设当前老鼠在迷宫编号为4的位置,如下图,它可以向三个方向移动:右、下和左。

按照迷宫内部的行列号,如图中红色编号所示,老鼠所在的“当前行”为1,“当前行”为1。

如果下一步的方向是“左”,则新的“当前列”等于原“当前列”-1,即1-1=0;

如果下一步的方向是“右”,则新的“当前列”等于原“当前列”+1,即1+1=2;

如果下一步的方向是“下”,则新的“当前行”等于原“当前行”+1,即1+1=2;

当然,如果老鼠可以向上移动的话,则新的“当前行”等于原“当前行”-1,即1-1=0;

下面的代码即实现以上过程:

Sub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer)'计算出下一步的行号和列号(迷宫内)并返回 Select Case 当前方向 Case 0 '"0=上" 当前行 = 当前行 - 1 '新的“当前行”的值会返回给调用者 Case 1 '"1=右" 当前列 = 当前列 + 1 '新的“当前列”的值会返回给调用者 Case 2 '"2=下" 当前行 = 当前行 + 1 '新的“当前行”的值会返回给调用者 Case 3 '"3=左" 当前列 = 当前列 - 1 '新的“当前列”的值会返回给调用者 End Select End Sub

这段代码使用了一个新的语句:Select Case语句。

Select Case语句与If语句一样都是分支语句。
通常,当一件事情只有两种选择时,比如非黑即白,常用If语句分别对黑和白进行处理。

如果一件事情的选择多于两种,那么使用Select Case语句会更方便。

使用方法如下:

Select Case 条件表达式

Case 情况表达式1

[程序代码1]

Case 情况表达式2

[程序代码2]

Case 情况表达式3

... ...

Case Else

[程序代码n]

End Select

程序的执行过程是,当“条件表达式”的值等于“情况表达式1”时,即执行[程序代码1],当“条件表达式”的值等于“情况表达式2”时,即执行[程序代码2],... ...,以此类推。
如果,所有情况都不满足,则执行Case Else后面的[程序代码n]。

09 老鼠随机行走

首先,我们学习一个新的循环语句Do While ... Loop循环,格式如下:

Do While 逻辑表达式

[程序代码1]

( Exit Do ) '跳出循环,此句为选用

[程序代码2]

Loop

之前我们学过For循环语句,其中的循环次数是固定的,每当达到次数后,循环就会结束。
但是,有时候我们并不知道具体的循环次数,这时就要用Do While循环语句。

程序执行时,首先判断“逻辑表达式”是否为“真”。
在VBA中,所有非零的值都代表“真”,等于0的值为“假”。

如果“逻辑表达式”为“真”,则执行循环体中的[程序代码1];如果为“假”,则不循环,直接跳到Loop后面,执行下面程序。

如果程序中写有“Exit Do”,执行到此句时,结束循环,跳到Loop后面,执行下面的程序。

如果,没有“Exit Do”,则继续执行[程序代码2];

执行到“Loop”时,程序跳回到Do While语句,再次判断“逻辑表达式”是否为“真”。
如果为真,则继续执行循环体内的程序;如果为“假”,则结束循环,跳到Loop后面,执行下面的程序。

Do循环语句还有一种写法,把While判断放到Loop后面,格式如下:

Do

[程序代码1]

( Exit Do ) '跳出循环,此句为选用

[程序代码2]

Loop While 逻辑表达式

使用规则与Do While循环完全一致,唯一区别是Do循环先执行循环体里的程序代码,后判断逻辑表达式是否为“真”。
如果是“真”,则跳回到Do语句重新循环;如果为“假”,则结束循环,执行下面的语句。

下面我们看让老鼠随机行走的程序代码:

Sub 随机行走() Dim 老鼠行号 As Integer '老鼠在迷宫中的行号 Dim 老鼠列号 As Integer '老鼠在迷宫中的列号 Dim 当前编号 As Integer '老鼠在迷宫中的序号,也是参数表和策略表的行号。
Dim 表列号 As Integer '策略表的列号,对应下一步的移动方向 '设定老鼠的起始位置 老鼠行号 = 迷宫头行 老鼠列号 = 迷宫头列 Do While True '无条件循环 延时 (0.1) '延时0.1秒 Cells(老鼠行号, 老鼠列号) = "" '删除当前位置的“老鼠”,改为空字符 当前编号 = (老鼠行号 - 迷宫头行) 3 + (老鼠列号 - 迷宫头列) '根据行列号计算老当前位置的编号 表列号 = 策略选择(当前编号) '调用“策略选择”函数得到下一步的方向 下一步位置 表列号, 老鼠行号, 老鼠列号 '调用“下一步位置”过程取得下一步的行号和列号 If Cells(老鼠行号, 老鼠列号) = "食物" Then '如果下一步的单元格中有“食物” Cells(老鼠行号, 老鼠列号) = "老鼠" '在下一步单元格中填“老鼠” Cells(迷宫头行 + 2, 迷宫头列 + 2).Font.ColorIndex = 5 '字体颜色改为蓝色 延时 (0.5) '延时0.5秒 Exit Do '目标达成,退出循环 End If '老鼠还没有遇到食物 Cells(老鼠行号, 老鼠列号) = "老鼠" '在下一步单元格中填“老鼠” Cells(老鼠行号, 老鼠列号).Font.ColorIndex = 1 '字体颜色设为黑色 LoopEnd Sub

这个Sub过程名为“随机行走”,程序的每一行都有注释,我们就不对每一句都解释了。

程序的主体是 Do While True 开头的一个无限循环,其中“True”代表“真”,所以这个循环的条件永远满足,会一直循环。

其中调用了一个“延时”过程,用来等待指定的时间,单位是秒。
如果不使用延时,老鼠会运动得太快,肉眼难以识别。
这个“延时”过程也是自定义的,代码放在后面。

让老鼠产生移动效果的方法很简单,首先删除迷宫中当前位置的老鼠,即把老鼠当前所在的单元格写为空字符“”。

然后计算老鼠在迷宫中的编号,亦即参数表和策略表的当前行号。

根据当前行号,调用“策略选择”函数,得到下一步的方向,也就是策略表的“表列号”。

然后调用“下一步位置 ”过程,得到下一步的行号和列号。

注意,定义过程“Sub 下一步位置()”时,括号里有三个参数。
调用它时也需要给定三个参数,但是不用加括号,调用语句是:

下一步位置 表列号, 老鼠行号, 老鼠列号

前面说过,后两个参数“老鼠行号”和“老鼠列号”使用的是“ByRef”传地址模式,意味着数据是双向传递的。
也就是说,调用过程时,“老鼠行号”和“老鼠列号”的值是当前的值,调用过程后,“老鼠行号”和“老鼠列号”被赋予了新的值,即下一步的行号和列号。

所以接着用『Cells(老鼠行号, 老鼠列号) = "老鼠"』就可以把老鼠画在新的位置上。

反复如此循环就实现了老鼠的不断随机移动。

不过,死循环是不好的,需要设置一个退出机制。

程序中的If 语句即完成退出功能。
If语句判断老鼠是否移动到有“食物”的单元格中,如果是“食物”,则把该单元格写成蓝色的“老鼠”,任务完成,用Exit Do语句退出循环,程序结束。

其他情况下,即下一步的单元格中没有“食物”,则“老鼠”写成黑色,并继续循环。

下面是延时过程的代码:

Sub 延时(T As Single) Dim time1 As Single time1 = Timer '取得当前时间 Do DoEvents '等待 Loop While Timer - time1 < T '时间差满足条件则退出循环End Sub

最后编写一个“主程序”:

Sub 主程序() Range(Cells(迷宫头行, 迷宫头列), Cells(迷宫头行 + 4, 迷宫头列 + 3)).Clear '清空迷宫区域 初始化 '调用“初始化”过程 随机行走 '调用“随机行走”过程,随机走迷宫 End Sub

主程序只有三句话,首先清空一块迷宫区域,然后调用“初始化”过程,最后调用“随机行走”过程。

我们把“主程序”放到代码区域的最上方,是在定义常量的后面的第一个Sub过程。

请把以上的所有代码录入或复制到编程窗口中,为了避免丢失内容造成程序运行错误,我把程序所需所有过程和函数的定义部分罗列如下,大家自行把文章中的代码填进去即可:

'以下定义常量Const 迷宫头行 = 4 '迷宫左上角的行号Const 迷宫头列 = 2 '迷宫左上角的列号Const 参数表头行 = 4 '动作参数表左上角行号Const 参数表头列 = 8 '动作参数表左上角列号Const 策略表头行 = 16 '策略表左上角行号Const 策略表头列 = 2 '策略表左上角列号Sub 主程序() Range(Cells(迷宫头行, 迷宫头列), Cells(迷宫头行 + 4, 迷宫头列 + 3)).Clear '清空迷宫区域 初始化 '调用“初始化”过程 随机行走 '调用“随机行走”过程,随机走迷宫 End SubSub 初始化() Dim 列号 As Integer '把变量 列号 定义为整型 For 列号 = 1 To 20 '列号从1到20循环 Cells(1, 列号) = 列号 '循环体,向指定单元格写入数据 Next 列号 '每循环一次列号加1,大于20后退出循环 Range(Cells(2, 1), Cells(26, 12)).Clear '清空指定矩形区域 画迷宫 '调用“画迷宫()”过程 画动作参数表 '调用“画动作参数表()”过程 画策略表 '调用“画策略表()”过程End SubSub 画迷宫() ... ...End SubSub 画动作参数表() '画动作参数表(θ) ... ...End SubSub 画策略表() '设置动作方向的平均概率 ... ... End SubFunction 策略选择(位置编号 As Integer) As Integer '按策略表选择指定行号的动作方向 ... ... End FunctionSub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer) ... ... End SubSub 随机行走() ... ...End SubSub 延时(T As Single) ... ...End Sub

下面运行这段程序:将光标移到“主程序”过程内部,按F5功能键运行程序,就可以看到老鼠在迷宫中随机移动,像没头的苍蝇一样,直到移动到写有“食物”的单元格中时,程序结束。

至此,我们编出了一只“傻老鼠”,会随机乱动,然而,它不会学习经验也不会吸取教训。

下面 ,我们将赋予老鼠“强化学习”的能力,这样它就会逐步记住走哪条路能更快“吃”到食物,逐渐学会走最短的路径达成目标。

强化学习最早起源于心理学的研究成果。
巴甫洛夫揭示了动物形成条件反射的能力,桑代克在此基础上提出了学习理论,即:好的结果会强化对应的行为,坏的结果会削弱对应的行为,有奖励的重复练习会增强这种联结。
斯金纳通过对操作条件反射的研究,提出了强化理论,指出行为可以通过奖励和惩罚来塑造和改变。

在20世纪80年代兴起的人工智能研究中,研究者借鉴了心理学中的强化理论,并意识到,通过模拟动物学习的过程,可以为机器赋予学习和适应环境的能力。
他们从心理学中引入“强化”这个词,将这种学习方法命名为“强化学习”。

强化学习是一种试错学习方法,类似于动物学习过程。
例如,驯养员训练马戏团动物时,会对正确的动作给予食物奖励,以此引导动物逐渐学会复杂的动作。
类似地,强化学习算法也是通过奖励机制来指导学习过程,使其能够改进自己的行为并逐渐学会复杂任务。

所以,我们给强化学习下一个形象的定义:利用奖励或惩罚的手段,训练动物做复杂动作的学习方式,就是强化学习。

本篇不会过多介绍强化学习的理论和算法演变历程,而是尽快进入强化学习的编程阶段,通过实例让大家能够更好地体验和理解。

因为在电脑上进行实验和研究比研究大脑更为迅捷和方便,再加上数学工具的支持,使得人工智能的某些方面已经超越了神经科学和心理学。
因此,从事这两个领域研究的学者应更多关注人工智能的发展,以获得更多的借鉴和启示。

目前,大多数人工智能和强化学习程序都采用Python语言编写,因为它非常适合专业人士。
然而,对于初学者来说,Python语言并不够友好。
安装和配置Python编程环境的复杂程度足以使许多人望而却步,而学习Python语言和各种强化学习算法的原理则又会劝退多一半人。
实际上,留下10%的人能够真正开始编程,这已经相当不错了。

为了解决这一问题,我参考AI生成的代码,将强化学习程序移植到Excel中。
这样,学习者不需要安装或配置任何软件,只要电脑上已有办公软件Office,就能够开始编程了。

在Excel中内置的VBA编程语言简单易学。
我们之前已经通过两篇文章讲解了主要的语句,并通过编程绘制了一个迷宫。
此外,我们还绘制出一个动作参数表和一个策略表,使得迷宫中的老鼠能够实现随机移动。

接下来,我们会为这个“电子老鼠”赋予强化学习技能,很类似老鼠固有的条件反射学习能力。
这样,它就能够自主学习并探索最短的路径来获取“食物”。

我们先回顾一下前两篇教程《零基础入门,用Excel 编写人工智能程序:老鼠走迷宫》和《从零学起,用Excel VBA编程训练“人工智能老鼠”走迷宫(二)》中的主要内容(建议没读过的同学先点文章名字阅读),标题如下:

01 在Excel中编写和运行程序

02 编程基本操作

03 画迷宫

04 画参数表

05 画策略表

06 自定义函数

07 策略选择函数

08 求取下一步的行号和列号

09 老鼠随机行走

以下的标题序号接前文继续。

10 画价值表

在之前的教程中,我们创建了一个“策略表”,它存储了可采取动作的概率分布。
在强化学习中,这个概率值也被称为“策略”,通常用π来表示。
需要注意的是,这里的π并不是圆周率3.14的意思,而是来自英文单词“policy”(策略)的近似发音。

将要新建的“价值表”与“策略表”大小相同,都是9行4列。
其中,行号代表迷宫的编号,列号代表“上、右、下、左” 4个方向。

“价值表”中的值代表每个可移动方向的价值,在强化学习中常被称为Q值,字母Q取自“Quality”,表示动作的质量或价值。
其取值范围是0~1,Q值越大(越接近1)表示这个方向的价值越高,越有利于获得奖励。

我们把价值表的左上角,放在表格的第16行,第8列,我们定义两个常量代表这两个数字。
定义常量的代码如下:

Const 价值表头行 = 16 '价值表Q左上角行号Const 价值表头列 = 8 '价值表Q左上角列号

这两句代码与之前定义的常量放在一起,放在整个程序的最上方,

下面是画价值表的过程定义及实现代码(代码显示不全可左右划动):

Sub 画价值表() '画价值表(Q表) Dim 价值表初值 As Single '浮点型,价值表的初值 Dim 列号 As Integer '价值表的行号 Dim 行号 As Integer '价值表的列号 '画价值表表头 Cells(价值表头行 - 2, 价值表头列 + 1) = "价值表Q(动作的价值)" Cells(价值表头行 - 2, 价值表头列 - 1) = "位置编号/" Cells(价值表头行 - 1, 价值表头列 - 1) = "移动方向" Cells(价值表头行 - 1, 价值表头列 + 0) = "0-上" '写价值表动作的方向 Cells(价值表头行 - 1, 价值表头列 + 1) = "1-右" '写价值表动作的方向 Cells(价值表头行 - 1, 价值表头列 + 2) = "2-下" '写价值表动作的方向 Cells(价值表头行 - 1, 价值表头列 + 3) = "3-左" '写价值表动作的方向 With Range(Cells(价值表头行, 价值表头列), Cells(价值表头行 + 8, 价值表头列 + 3)) .BorderAround LineStyle:=xlContinuous '画价值表四边单线边框 End With Randomize (Timer) '初始化随机数 For 行号 = 0 To 8 '价值表行号从0~8循环 Cells(价值表头行 + 行号, 价值表头列 - 1) = 行号 '在价值表左侧写迷宫编号 For 列号 = 0 To 3 '价值表列号从0~3循环 If Cells(参数表头行 + 行号, 参数表头列 + 列号) <> 0 Then '如果参数表同行同列的值不等于0,则: Do While True '循环取一个非0随机数 价值表初值 = Rnd() '取得一个0~1之间的随机数作为价值表初值 If 价值表初值 <> 0 Then Exit Do '如果“价值表初值”不等于0,退出循环 Loop Cells(行号 + 价值表头行, 列号 + 价值表头列) = 价值表初值 0.5 '填价值表(乘0.1取较低初值) Else Cells(行号 + 价值表头行, 列号 + 价值表头列) = 0 '如果参数表为0,则价值表也填0 End If Next 列号 Next 行号 End Sub

画价值表的过程与画策略表的过程类似,开始是定义内部变量和画表头。

填表的程序的主体结构依然是两个嵌套在一起的For循环语句,外圈的For循环是价值表的行号,从0到8,共9行。
内圈的For循环是价值表的列号,从0到3,分别代表四个方向,即:0-上,1-右,2-下,3-左。

程序将向价值表中填初始值。
如果在策略表中的某个方向的值为0,表示这个方向有墙,不能移动。
填价值表时,该方向也填0,代表价值是0。

其他的方向是可移动的方向,填表时取一个0~1之间的随机数作为初始值。

当随机数的值过大时,可能会误导老鼠,使其误以为该方向有利于获得奖励,从而倾向于朝该方向前进,导致出现较多无效动作。

因此,在为价值表填写初值时,我们可以将得到的随机数乘以0.1,以便从一个较低的价值水平开始学习。
这样有利于减少无效动作的发生。

画价值表的程序写好后,把调用“画价值表”的代码加入到“初始化”过程中,放在最后一句,如下:

Sub 初始化() Range(Cells(2, 1), Cells(26, 12)).Clear '清空指定矩形区域 画迷宫 '调用“画迷宫()”过程 画动作参数表 '调用“画动作参数表()”过程 画策略表 '调用“画策略表()”过程 画价值表 '调用“画价值表()”过程End Sub

现在我们可以把光标放到“初始化”过程内部,按F5功能键执行这段程序,就可以看到画好的迷宫和三个表格。
其中价值表右下方,如下图:

11 价值选择

利用价值表可以选择下一步移动的方向,方法是:当老鼠移动到迷宫中的某个单元格时,到价值表中查询对应位置的Q值最大的方向,并以这个方向作为下一步的前进方向。

可见,价值表中Q值的大小会对老鼠的方向选择起到非常大的作用。

Q值的大小对老鼠的方向选择起到了极其重要的作用。
或许你会问,即使老鼠选择了Q值最大的方向,但是,我们在价值表中填写的初值是随机数,它会帮助老鼠吃到食物吗?

暂时不会。
不过,不用着急。
之后会根据得到的奖励来更新价值表。
随着时间的推移,正确方向对应的Q值将会越来越大,最终Q值最大的方向将会是离奖励最近的方向。

下面是价值选择过程的定义和实现代码。

Function 价值选择(位置编号 As Integer) As Integer '按价值表选择价值最大的方向 Dim 最大Q值 As Single '价值表中该行的最大价值 Dim 当前Q值 As Single '价值表的当前Q值 Dim 表列号 As Integer '价值表的当前列号(表内列号) 最大Q值 = Application.Max(Range(Cells(价值表头行 + 位置编号, 价值表头列), Cells(价值表头行 + 位置编号, 价值表头列 + 3))) '求价值表中最大值 For 表列号 = 0 To 3 '价值表列号从0~3循环 当前Q值 = Cells(位置编号 + 价值表头行, 表列号 + 价值表头列) '取当前列号的Q值 If 当前Q值 = 最大Q值 Then '如果当前Q值=最大Q值,则: 价值选择 = 表列号 '函数返回价值表中最大Q值对应的列号 Exit For '跳出For循环 End If Next 表列号 End Function12 探索与利用

在决策过程中,探索与利用是一个经典的难题,在一定条件下需要进行合理的权衡。

以挖矿或寻宝为例,一方面,往各处寻找和收集信息被称为探索,但过度的探索可能会浪费大量人力物力,却未能获得更多有用的信息。
另一方面,一旦得到少量信息,就停止探索并开始进行利用,有可能会错过附近更为丰富的矿藏。

对人工智能的学习过程来说,如何用最少的时间获得最大的奖励,也需要在探索与利用之间进行合理的平衡。

常用的解决方案是ε贪心策略,即选定一个探索系数ε,取值0~1。
每次决策时,以ε的概率去“探索”新的可能性;另一方面,以1-ε的概率“利用”已有的知识。
这种策略在探索与利用之间实现了平衡,既不会过于频繁地进行探索,也不会过于依赖已知的最佳动作。

大脑中也存在类似的机制。
研究表明,去甲肾上腺素可以影响运动系统,从而影响探索和利用之间的转换。
高浓度的去甲肾上腺素会促使人们更倾向于冒险的行为。

下面编写一个名为“探索与利用”的函数完成这项工作。
函数定义如下:

Function 探索与利用(ByRef 当前行 As Integer, ByRef 当前列 As Integer) As IntegerEnd Function

这个函数有两个参数:“当前行”与“当前列”,是指“价值表”的当前行列。
它们都使用“ByRef”传地址模式,即数据可以双向传送。

函数的代码如下:

Function 探索与利用(ByRef 当前行 As Integer, ByRef 当前列 As Integer) As Integer '获取下一步的方向 Dim 表列号 As Integer '表内列号 Dim 迷宫编号 As Integer '当前位置在价值表中的行号 Dim 随机数 As Single '随机数 Dim 探索系数ε As Single 'ε-贪心策略初值(探索的占比) 探索系数ε = 0.5 '探索系数ε取值0~1;取0全选价值表(利用),取1全选策略表(探索) Randomize (Timer) '初始化随机数 随机数 = Rnd() '取得一个0~1之间的随机数 迷宫编号 = (当前行 - 迷宫头行) 3 + (当前列 - 迷宫头列) If 随机数 < 探索系数ε Then '部分按照策略表中的动作概率随机选方向 表列号 = 策略选择(迷宫编号) Else '另一部分按照价值表中的最大价值选择方向 表列号 = 价值选择(迷宫编号) End If 下一步位置 表列号, 当前行, 当前列 '取得下一步的行列号 探索与利用 = 表列号 '函数返回表列号 End Function13 Q学习(Q-learning)

Q学习是一种基于奖励的强化学习算法,是时序差分(temporal difference,TD)算法的一种。
它主要通过预测不断地更新价值表来寻找最优策略。

价值表中的Q值代表了在该状态下采取某个移动方向所能获得的累计奖励。
通过重复训练和学习,正确的策略所对应的Q值不断增加,逐渐成为离奖励最近的方向。

在各类学习算法中,Q学习是最接近动物或人类建立条件反射的学习方式。
大脑很可能也使用了时序差分算法,通过释放多巴胺对正确的预测给予奖励,从而学会把刺激或行为与奖励建立联结。

Q学习更新价值表Q值的公式是:

新Q值=原Q值 + 学习率α (奖励R + 折扣因子γ 下步最大Q值 - 原Q值)

其中,括号里的内容叫做时序差分误差,简写为TD误差。

因此可以把上面的公式拆成两项,如下:

TD误差=(奖励R + 折扣因子γ 下步最大Q值 - 原Q值)

新Q值=原Q值 + 学习率α TD误差

其中:

奖励R:取值0或1,是达成目标时的奖励值,当老鼠找到食物时,奖励R的值为1,其他情况,奖励R的值为0。

折扣因子γ:取值0~1,是对下一步价值的折扣率,以权衡短期和长期奖励的重要性。
折扣因子γ取值0.1和0.9分别对应了“短视”和“看长远”的情况。
折扣因子γ越大,对未来奖励的重视程度越高,相反,折扣因子γ越小,越看中近期奖励。

经济学中的跨期选择和时间折扣与折扣因子γ 有类似的作用,都是用于衡量未来奖励或成本的重要性的参数。

有些人较有耐心,能放弃眼前的利益而去追求未来更大的收益,这些人的折扣因子γ较高。
而孩童的折扣因子γ通常较低,他们很难拒绝眼前奖赏的诱惑,倾向于获得即时满足。

学习率α :取值0~1,是学习的效率,表示新获取的信息在多大程度上覆盖旧信息。
学习率α值越大,学习效率越高。
但是学习率不一定越大越好,过大的学习率容易被局部小奖励困住,从而错过别处的更大奖励。

较低等的动物,比如蜜蜂,学习率较高,能够仅仅通过一次停留就能将一朵花和奖励(花蜜)关联起来,而高等动物如哺乳动物的学习率比较低,需要经过多次接触才会建立联结,这样它们可以拥有更多的机会。

下步最大Q值:下一步在价值表中所对应的最大的一个Q值。

原Q值:在进行下一步之前,在价值表中当前行和当前列的Q值。

新Q值:使用Q学习公式计算出的新Q值,用来替换原Q值以更新价值表。

下面是Q学习的代码:

Sub Q学习(奖励R As Integer, 当前编号 As Integer, 当前表列号 As Integer, 下一步编号 As Integer) Dim 学习率α As Single Dim 折扣因子γ As Single Dim 原Q值 As Single Dim 下步最大Q值 As Single Dim TD误差 As Single 学习率α = 0.1 '学习率,取值(0~1);越大学习越快,取较低的值0.1 折扣因子γ = 0.9 '折扣率,取值(0~1):越大越重视未来奖励,取较高的值0.9 原Q值 = Cells(价值表头行 + 当前编号, 价值表头列 + 当前表列号) '取得当前的价值Q '以下是Q-Learning算法 If 奖励R = 0 Then '如果没有遇到“食物” 下步最大Q值 = Application.Max(Range(Cells(下一步编号 + 价值表头行, 价值表头列), Cells(下一步编号 + 价值表头行, 价值表头列 + 3))) '求最大值 TD误差 = 奖励R + 折扣因子γ 下步最大Q值 - 原Q值 '计算TD误差 Else '如果遇到“食物” TD误差 = 奖励R - 原Q值 '遇到"食物"后,本轮结束,所以计算时去掉了“下步最大Q值” End If Cells(当前编号 + 价值表头行, 当前表列号 + 价值表头列) = 原Q值 + 学习率α TD误差 '更新价值表的Q值 Cells(当前编号 + 价值表头行, 当前表列号 + 价值表头列).Font.ColorIndex = 3 '把更新后的Q值改为红色 End Sub

“Q学习”的程序结构很简单,取出价值表中指定行和指定列的Q值,然后用Q-learning算法计算出新的Q值。

计算时,遇到“食物”和没有遇到“食物”的TD误差的计算式有所不同:

没有遇到“食物”时,奖励R=0。
由于任务还没有结束,需要继续走下一步,所以计算公式为:

TD误差=折扣因子γ 下步最大Q值 - 原Q值

而当遇到“食物”时,奖励R=1。
因为此轮任务已结束,无需走下一步了,所以公式中去掉“下步最大Q值”这一项,如下:

TD误差=奖励R - 原Q值

求得TD误差后,计算新Q值:

新Q值=原Q值 + 学习率α TD误差

用Cells()语句把新的Q值写回到价值表中,覆盖掉“原Q值”。
程序中把新写入的Q值改为红色,这样就可以直观看到价值表中有哪些值被更新了。

14 强化学习

在强化学习中,“Q学习”是一种时序差分算法,它的基本思想是,利用后续状态的价值估计来更新当前状态的价值估计,通过重复训练和反复迭代,从而使价值表中的Q值逐渐逼近真实值。

时序差分算法的核心在于差分计算,即计算当前状态和后续状态之间的差异,亦即Q-learning算法公式中的(下步最大Q值 - 原Q值)。
时序差分算法会根据这个差分来更新价值表中的Q值。

如何取得这两项Q值呢?

假设老鼠处于迷宫中编号为4的单元格,它可以选择向右、下和左三个方向移动,如下图左半部分。

在右半部分的价值表中,老鼠的位置对应编号为4的行,三个可能的移动方向对应三个有效的Q值(图中被蓝绿色圈起来的三个值)。

该取哪个值作为“原Q值”呢?只有在选定一个移动的方向后才能确定。

假设选择向下走,如左侧图中标号①的绿色箭头所指,则可以在右侧价值表中取得一个Q值,如图中绿色框圈住的数值,此值作为为“原Q值”。

老鼠从编号4向下移动一步后,将到达编号为7的单元格,对应价值表中行号为7的一行。
在此位置也可以向三个方向移动,对应于价值表中的三个非0的Q值。

需要再得到一个Q值,才能进行差分计算,所以必须再选定下一个移动方向。
根据Q学习算法,在该行三个Q值中选取最大的一个作为“下步Q值”,如右侧图中红色框圈住的数值,这个最大的Q值在价值表中第1列,对应着向右移动。
即左侧图中标号②的红色箭头所指的方向。

可见,在任何位置,需要移动两步得到两个Q值,才能进行价值的差分计算。

计算价值差分并更新Q值的程序流程框图如下:

以上流程的执行过程是:老鼠从第一个位置开始,先移动两步,取得两个Q值,一个为“原Q值”,另一个为“下步Q值”(下步Q值取的是当前行的最大值),通过计算价值差分得到“新Q值”,并用“新Q值”更新“原Q值”。

然后进入一个循环过程,先把上一步的“下步Q值”变成“原Q值”,然后再移动一步,得到一个新的“下步Q值”,重新计算“新Q值”,更新“原Q值”。
一直如此循环,直到遇到“食物”为止。

程序中通过调用“探索与利用”函数取得下一步的行号和列号。

调用“Q学习”过程计算新Q值并更新原Q值。

实现以上过程的代码如下:

Sub 强化学习() Dim 当前行号 As Integer '老鼠移动前位置行号 Dim 当前列号 As Integer '老鼠移动前位置列号 Dim 下步行号 As Integer '老鼠移动后位置行号 Dim 下步列号 As Integer '老鼠移动后位置列号 Dim 当前编号 As Integer '当前位置编号(=价值表行号) Dim 下步编号 As Integer '下一个位置编号(=价值表行号) Dim 当前表列号 As Integer '当前位置在Q表中的列号 Dim 下步表列号 As Integer '下一个位置在Q表中的列号 Dim 奖励R As Integer '奖励值 下步行号 = 迷宫头行 '把下一步的行号设为迷宫起始行号 下步列号 = 迷宫头列 '把下一步的列号设为迷宫起始列号 Cells(下步行号, 下步列号) = "" '擦掉初始位置的老鼠,写入空字符"" 下步编号 = (下步行号 - 迷宫头行) 3 + (下步列号 - 迷宫头列) '计算迷宫当前位置编号 下步表列号 = 探索与利用(下步行号, 下步列号) '调用“探索与利用”取得下一步的方向对应的列号 Do While True '无限循环 当前行号 = 下步行号 '把下一步行号转为当前行号 当前列号 = 下步列号 '把下一步列号转为当前列号 当前编号 = 下步编号 '把下一步迷宫编号转为当前迷宫编号 当前表列号 = 下步表列号 '把下一步价值表的列号转为当前表列号 If Cells(下步行号, 下步列号) = "食物" Then '如果到达食物奖励点(终点) 奖励R = 1 '设定奖励值为1 Q学习 奖励R, 当前编号, 当前表列号, 下步编号 '调用“Q学习”过程,更新价值表Q值 Cells(下步行号, 下步列号) = "老鼠" '在终点画出新老鼠 Cells(下步行号, 下步列号).Font.ColorIndex = 5 '把终点的老鼠变为蓝色 延时 (0.5) '延时0.5秒 Exit Do Else '如果没有“食物”,则: 奖励R = 0 '奖励值为0 下步编号 = (下步行号 - 迷宫头行) 3 + (下步列号 - 迷宫头列) '计算下一步位置编号 下步表列号 = 探索与利用(下步行号, 下步列号) '调用“探索与利用”取得再下一步的方向列号 Q学习 奖励R, 当前编号, 当前表列号, 下步编号 '调用“Q学习”过程,更新价值表Q值 Cells(当前行号, 当前列号) = "" '擦掉当前位置的老鼠 If Cells(下步行号, 下步列号) <> "食物" Then '如果该位置没有"食物",则画老鼠 Cells(下步行号, 下步列号) = "老鼠" '在下一步的位置画出新老鼠 Cells(下步行号, 下步列号).Font.ColorIndex = 1 '设老鼠颜色为黑色 End If End If 延时 (0.05) '延时0.05秒 Loop End Sub

15 主程序

主程序设定了一个“训练次数”变量,取值10次。
然后循环10次调用“强化学习”过程。

以下是初始化过程及主程序代码。
为了避免因缺少代码而导致程序无法运行,后面列出了所需的函数即过程的定义头部分。
大家在三篇教程中找到这些函数和过程,把代码录入或粘贴进去即可。

'以下定义常量Const 迷宫头行 = 4 '迷宫左上角的行号Const 迷宫头列 = 2 '迷宫左上角的列号Const 参数表头行 = 4 '动作参数表左上角行号Const 参数表头列 = 8 '动作参数表左上角列号Const 策略表头行 = 16 '策略表π左上角行号Const 策略表头列 = 2 '策略表π左上角列号Const 价值表头行 = 16 '价值表Q左上角行号Const 价值表头列 = 8 '价值表Q左上角列号Sub 初始化() Range(Cells(2, 1), Cells(26, 12)).Clear '清空指定矩形区域 画迷宫 '调用“画迷宫()”过程 画动作参数表 '调用“画动作参数表()”过程 画策略表 '调用“画策略表()”过程 画价值表 '调用“画价值表()”过程End SubSub 主程序() Dim 训练次数 As Integer '运行轮次数 Dim 当前次数 As Integer ' 初始化 '调用“初始化”过程 Cells(迷宫头行 - 1, 迷宫头列 + 0) = "训练次数:" '写字符串"训练次数:" 训练次数 = 10 '指定本轮训练次数 For 当前次数 = 1 To 训练次数 '循环训练10次 Range(Cells(2, 1), Cells(26, 12)).Font.ColorIndex = 1 '将指定矩形区域字符设为黑色 画迷宫 '调用“画迷宫”过程重绘迷宫 延时 (0.5) '延时0.5秒 Cells(迷宫头行 - 1, 迷宫头列 + 1) = 当前次数 '写当前训练次数' 随机行走 '随机走迷宫 强化学习 Next 当前次数 End SubSub 画迷宫()End SubSub 画策略表() '设置动作方向的平均概率End SubFunction 策略选择(位置编号 As Integer) As IntegerEnd FunctionSub 下一步位置(当前方向 As Integer, ByRef 当前行 As Integer, ByRef 当前列 As Integer)End SubSub 随机行走()End SubSub 延时(T As Single)End SubSub 画价值表()End SubFunction 价值选择(位置编号 As Integer) As IntegerEnd FunctionFunction 探索与利用(ByRef 当前行 As Integer, ByRef 当前列 As Integer) As IntegerEnd FunctionSub Q学习(奖励R As Integer, 当前编号 As Integer, 当前表列号 As Integer, 下一步编号 As Integer)End SubSub 强化学习()End Sub

我们按F5运行这个主程序,发现,一开始时,老鼠像没头苍蝇一样随机乱跑,但是,随着训练次数的增加,老鼠逐渐找到吃到食物的最短路径,并倾向于按这条路径行走。

从价值表中也能看出,最短路径所对应的Q值在逐渐增加,离食物越近,Q值增加得越快。
如下图:

我们编写的这个强化学习程序适应性很强,即使改变食物的位置,也无需修改程序,老鼠依然能找到最短的路径。

还可以随意改变迷宫内部墙的数量和位置,也可以增加食物的数量,同样无需修改程序,老鼠都能找到最佳的路径。

修改“画迷宫”过程的代码,可以改变“食物”和迷宫内墙的数量和位置。
下面是一个修改后的代码,画出一个新迷宫:

Sub 画迷宫() Dim 迷宫行号 As Integer '迷宫内部的相对行号 Dim 迷宫列号 As Integer '迷宫内部的相对列号 Dim 迷宫编号 As Integer '迷宫内单元格的编号 '以下画迷宫边框,并设置迷宫区域字体属性 With Range(Cells(迷宫头行, 迷宫头列), Cells(迷宫头行 + 2, 迷宫头列 + 2)) .BorderAround LineStyle:=xlDouble '在迷宫四边框画双线 .HorizontalAlignment = xlCenter '设字符居中 .Font.ColorIndex = 15 '设字体为灰色 .Font.FontStyle = "Bold" '设字体为粗体 End With '以下画迷宫内部墙 Cells(迷宫头行 + 0, 迷宫头列 + 0).Borders(xlEdgeRight).LineStyle = xlDouble '右侧画双线边框 Cells(迷宫头行 + 0, 迷宫头列 + 1).Borders(xlEdgeLeft).LineStyle = xlDouble '左侧画双线边框 Cells(迷宫头行 + 1, 迷宫头列 + 0).Borders(xlEdgeBottom).LineStyle = xlDouble '下边画双线边框 Cells(迷宫头行 + 2, 迷宫头列 + 0).Borders(xlEdgeTop).LineStyle = xlDouble '上边画双线边框 Cells(迷宫头行 + 1, 迷宫头列 + 1).Borders(xlEdgeTop).LineStyle = xlDouble '上边画双线边框 Cells(迷宫头行 + 0, 迷宫头列 + 1).Borders(xlEdgeBottom).LineStyle = xlDouble '下边画双线边框 Cells(迷宫头行 + 2, 迷宫头列 + 1).Borders(xlEdgeRight).LineStyle = xlDouble '右侧画双线边框 Cells(迷宫头行 + 2, 迷宫头列 + 2).Borders(xlEdgeLeft).LineStyle = xlDouble '左侧画双线边框 '以下画迷宫内部单元格的编号 迷宫编号 = 0 '设迷宫编号的初始值为0 For 迷宫行号 = 0 To 2 '迷宫当前行号=0,1,2 For 迷宫列号 = 0 To 2 '迷宫当前列号=0,1,2 Cells(迷宫头行 + 迷宫行号, 迷宫头列 + 迷宫列号) = 迷宫编号 '画迷宫内单元格的编号 迷宫编号 = 迷宫编号 + 1 '修改迷宫编号变为当前值+1 Next 迷宫列号 Next 迷宫行号 '以下写"老鼠"和"食物" Cells(迷宫头行, 迷宫头列) = "老鼠" '在迷宫0号单元格写“老鼠” Cells(迷宫头行, 迷宫头列).Font.ColorIndex = 1 '字体设为黑色 Cells(迷宫头行 + 0, 迷宫头列 + 1) = "食物" '在迷宫8号单元格写“食物” Cells(迷宫头行 + 0, 迷宫头列 + 1).Font.ColorIndex = 3 '字体设为红色 End Sub

画出的迷宫如下图,无需修改程序,老鼠依然能学会找到食物的最佳路径。

在你的电脑中,老鼠学会走迷宫了吗?请在评论区留言。

标签:

相关文章

语言中的借用,文化交融的桥梁

自古以来,人类社会的交流与发展离不开语言的传播。在漫长的历史长河中,各民族、各地区之间的文化相互碰撞、交融,产生了许多独特的语言现...

软件开发 2025-01-01 阅读1 评论0

机顶盒协议,守护数字生活的新卫士

随着科技的飞速发展,数字家庭逐渐走进千家万户。在这个时代,机顶盒成为了连接我们与丰富多彩的数字世界的重要桥梁。而机顶盒协议,作为保...

软件开发 2025-01-01 阅读1 评论0

语言基础在现代社会的重要性及方法步骤

语言是人类沟通的桥梁,是社会发展的基础。语言基础作为语言学习的基石,对于个人、社会乃至国家的发展具有重要意义。本文将从语言基础在现...

软件开发 2025-01-01 阅读2 评论0

粤语电影,传承文化,点亮时代之光

粤语电影,作为中国电影产业的一朵奇葩,以其独特的地域特色、丰富的文化内涵和鲜明的艺术风格,赢得了广大观众的喜爱。本文将从粤语电影的...

软件开发 2025-01-01 阅读3 评论0

苹果游戏语言,塑造未来娱乐体验的基石

随着科技的飞速发展,游戏产业逐渐成为全球娱乐市场的重要支柱。在我国,游戏产业更是蓬勃发展,吸引了无数玩家和投资者的目光。而在这其中...

软件开发 2025-01-01 阅读1 评论0