跨境派

跨境派

跨境派,专注跨境行业新闻资讯、跨境电商知识分享!

当前位置:首页 > 跨境学堂 > 三子棋大师:用C语言打造无敌强化学习AI

三子棋大师:用C语言打造无敌强化学习AI

时间:2024-04-08 18:45:39 来源:网络cs 作者:淼淼 栏目:跨境学堂 阅读:

标签: 无敌  强化  学习  打造  语言  大师 
阅读本书更多章节>>>>

目录

三子棋

强化学习

状态

行动

分数

奖励

完整代码


写个三子棋的强化学习AI玩玩。写这玩意只需要有一点C语言基础就可以了,至于AI部分,也是很好理解的。

三子棋

在3*3的棋盘中,先手方画O,后手方画X,连成3个就赢了。事实上,只需要很简单的试验,你就会明白,如果双方都走最优解,最后一定是和棋

让电脑随机下棋显然没有什么意思,那能不能让电脑聪明点呢?

强化学习

强化学习的描述如下,看不太明白没关系,我会举例子的。

强化学习是机器学习的一个分支,它着重于如何让智能系统(称为代理)通过与环境的交互来学习做出最优的决策或者行动。在强化学习中,代理试图通过执行行动并接收环境反馈(通常是奖励)来最大化其累计获得的总奖励。这一过程涉及到学习行动的策略,即在给定的状态下应采取什么行动。

强化学习的核心组成部分包括:

代理(Agent):执行行动的实体,其目标是学习最佳行动序列(策略)以最大化奖励。环境(Environment):代理所处并与之交互的系统或问题域。环境根据代理的当前状态和执行的行动,反馈新的状态和奖励。状态(State):环境的一个描述,代理根据状态做出决策。行动(Action):代理可以执行的操作。奖励(Reward):环境对代理执行行动的即时反馈,指导学习过程。

强化学习的学习过程通常涉及探索(尝试新行动以了解它们的效果)和利用(使用已知的信息来获得最大奖励)之间的平衡。这一平衡的目标是发现最优策略,即一个从状态到行动的映射,使得累积奖励最大化。

强化学习在多个领域有广泛的应用,如自动驾驶汽车、游戏、机器人导航和控制、推荐系统等。与其他类型的机器学习不同,强化学习不是直接从数据集学习,而是通过试错和适应环境的反馈来学习。

下面我来谈谈最简单的强化学习,在三子棋中的应用。虽然有点杀鸡用牛刀的嫌疑,但这是个很好的例子。

电脑是很笨的,它只知道游戏规则:只有空位才能下棋、一人一步、连成3个获胜……如果你不告诉它下棋的思路,它就只会随机下。

状态

棋局在某个时刻,会有一个状态:

我们可以用3*3的二维数组来描述,即:

0 0 10 1 02 1 2

其中0表示空位,1表示先手方的O,2表示后手方的X。

如果我把这个二维数组从右向左、从下到上排成一排,得到212010100,这是一个只由012组成的数字,可以看作三进制,即(212010100)_3,再转换为十进制int即可。如果要从这个整数还原棋局的状态,只需要重新转换成三进制,再填到二维数组中。

%20

这样,我们成功地用int来描述棋局的状态。棋局所有可能的状态,不超过种,实际还要更少,因为有一些情况是达不到的。

%20
int%20GetState(int%20board[3][3]){int%20state%20=%200;//%20转换为3进制数for%20(int%20i%20=%202;%20i%20>=%200;%20--i){for%20(int%20j%20=%202;%20j%20>=%200;%20--j){state%20=%20state%20*%203%20+%20board[i][j];}}return%20state;}void%20StateToBoard(int%20state,%20int%20board[3][3]){//%20还原三进制整数for%20(int%20i%20=%200;%20i%20<%203;%20++i){for%20(int%20j%20=%200;%20j%20<%203;%20++j){board[i][j]%20=%20state%20%%203;state%20/=%203;}}}
%20行动%20

在某个状态下,比如:

%20

此时轮到X走,假设棋盘的9个位置分别是:

0 1 23 4 56 7 8

显然二维数组的第x行第y列(x、y从0开始)表示数字n=x*3+y,而x=n/3,y=n%3。

那么对于上图中的棋局状态,X所有可能的走法就是:0,1,3,5。这样,我们就用一个整数表示了某个状态下的行动

分数

某个状态下的某个行动都可以赋一个得分,这个分数越高,表示这个行动是越有利的。那么如何准确得到每个状态下的每个行动的分数呢?

我们可以这样初始化分数:

如果这步棋走完后能直接获胜,分数为1如果这步棋走完后和棋,或者棋局未结束,分数为0.5如果这步棋不能走(位置已被占用),分数为-1
static void _InitValue(value_t value){for (int state = 0; state < STATES_CNT; ++state){// 把状态转换为棋盘int board[3][3] = { 0 };StateToBoard(state, board);// 根据走棋后的状态,初始化分数for (int point = 0; point < 9; ++point){int tmpBoard[3][3] = { 0 };memcpy(tmpBoard, board, 9 * sizeof(int));if (Move(tmpBoard, point / 3, point % 3)){int res = IsWin(tmpBoard);// 赢 - 1// 和棋 - 0.5// 棋局未结束 - 0.5if (res == 1 || res == 2)value[state][point] = 1;elsevalue[state][point] = 0.5;}else{// 该位置非法value[state][point] = -1;}}}}

你可能会问,那如果这步棋走完后输了呢?emmm,这种可能不存在!注意这里只表示走一步棋的分数。如果这步棋走完后几步真的会输,那么分数应该为0,这点后面会讲。这样,如果这个位置没有违反规则,分数就在[0,1]的范围内。

我们可以用一个二维数组来存储所有状态下的所有行动的得分。二维数组的行标表示棋局状态(前面已经用整数描述状态了,为0~3^9-1),列标(0~8)表示行动,二维数组内存储分数。

%20

显然,这个分数是不准确的,有可能这步棋很烂,但分数却是0.5,这就需要让AI强化学习了。

%20奖励%20

我们让电脑自己和自己下棋,每一步都选择当前状态下分数最高的位置,如果分数相同(比如一开始的9个位置分数都是0.5),就随机选择一个位置。

%20
int%20BestMove(int%20board[3][3],%20value_t%20value){//%20选择value最大的走法//%20找到最大的valueint%20state%20=%20GetState(board);double%20maxVal%20=%20-1;int%20point%20=%200;for%20(int%20i%20=%200;%20i%20<%209;%20++i){if%20(value[state][i]%20>%20maxVal)maxVal%20=%20value[state][i];}//%20有可能的最优走法bool%20moves[9]%20=%20{%200%20};//%20找到所有和最大value接近的valuefor%20(int%20i%20=%200;%20i%20<%209;%20++i){if%20(value[state][i]%20>%20maxVal%20-%200.01)moves[i]%20=%20true;}//%20在moves里随机选择一个最优走法while%20(true){int%20point%20=%20rand()%20%%209;if%20(moves[point])return%20point;}}
%20

最终,会产生一个结果。有可能是先手方O赢了,也有可能是后手方X赢了,还有可能是和棋。

%20

让电脑吸取经验教训,也就是给奖励,也可以是惩罚。

%20

具体的做法是,从最后一步往前推,更新每一步的分数。我们规定:

%20如果是某一方赢了,那么最后一步棋的得分就是1(这点和前面分数的初始化保持一致),而倒数第二步棋是输的那方下的,这步棋的得分设置成0,因为是这步棋直接导致了输棋。如果是和棋,那么最后一步棋的得分就是0.5(这点和前面分数的初始化保持一致),而倒数第二步棋是另一方下的,这步棋的得分设置成0.5,因为是这步棋直接导致了和棋。%20

那么倒数第三步和倒数第一步是同一方下的。倒数第三步的新的分数=倒数第三步的旧的分数+0.1*(倒数第一步的新的分数-倒数第三步旧的分数)。

%20

同理,倒数第四步和倒数第二步是同一方下的。倒数第四步的新的分数=倒数第四步的旧的分数+0.1*(倒数第二步的新的分数-倒数第四步旧的分数)。

%20

接下来是倒数第五步、倒数第六步……一直到正数第一步。这样,这盘棋出现的状态中,对于走过的行为,就有了新的分数,这就是强化学习!

%20

根据前面的计算方式,很容易知道,如果这步棋是合法的,那么分数就在[0,1]之间。如果某一方赢了,该方最后一步的得分为1,那么前面的每一步分数都会增加,因为该方后一步的分一定比前一步高,这就是奖励。而如果某一方输了,该方最后一步的得分为0,那么前面的每一步分数都会减少,因为该方后一步的分一定比前一步低,这就是惩罚

%20

注意到更新的分数乘了0.1,这样越往前的分数,变动的幅度就越小,这也是合理的,因为越接近棋局开始,走棋的影响就越小。

%20

经过大量的对局后,所有状态下的行为的得分就会更加准确,无限趋近于理论值。然而,为了防止出现局部最优,也就是AI自我感觉良好,最好让AI有一定的概率随机走棋,而不是每次都选择最优走法。

%20

以下是一次强化学习:

%20
void%20QLearning(value_t%20value){//%20棋盘int%20board[3][3]%20=%20{%200%20};//%20下棋的状态数组int%20states[9]%20=%20{%200%20};//%20下棋的位置数组int%20points[9]%20=%20{%200%20};//%20states和point数组记录的状态数量int%20size%20=%200;//%20记录stateint%20state%20=%200;//%20记录胜负和//%200%20-%20未分胜负//%201%20-%20先手方获胜//%202%20-%20后手方获胜//%203%20-%20和棋int%20res%20=%200;//%20下一盘棋while%20(true){//%20计算并保存新的状态state%20=%20GetState(board);states[size]%20=%20state;//%20一定概率随机走棋//%20否则选择value最大的走法if%20(rand()%20%%2010%20<%203){while%20(true){//%20随机走棋int%20x%20=%20rand()%20%%203;int%20y%20=%20rand()%20%%203;if%20(Move(board,%20x,%20y)){//%20保存当前pointpoints[size++]%20=%20x%20*%203%20+%20y;break;}}}else{//%20选择value最大的走法//%20找到最大的value和对应的pointint%20point%20=%20BestMove(board,%20value);//%20在point位置下棋Move(board,%20point%20/%203,%20point%20%%203);//%20保存pointpoints[size++]%20=%20point;}//%20棋局是否结束if%20(res%20=%20IsWin(board))break;}//%20根据这盘棋的信息,总结经验//%20不考虑最后一步--size;//%20倒数第二步直接导致输棋或和棋//%20输棋分数设为0//%20和棋分数设为0.5value[states[size%20-%201]][points[size%20-%201]]=%20(res%20==%203%20?%200.5%20:%200);--size;//%20从倒数第三步开始,每一步的分数更新为//%20原来的分数%20+%200.1%20*%20(后两步的分数-原来的分数)while%20(size%20>%200){value[states[size%20-%201]][points[size%20-%201]]=%20value[states[size%20-%201]][points[size%20-%201]]+%200.1%20*%20(value[states[size%20+%201]][points[size%20+%201]]-%20value[states[size%20-%201]][points[size%20-%201]]);--size;}}
%20

这里举个实际的例子,比如下面的对局(这里是我跟电脑的对局,实际强化学习时可以是电脑自己和自己下,也可以是人类和电脑下):

%20

这盘棋总共下了7步,就分出了胜负。假设这场对局之前,除了最后一步棋的分数是1之外,其余的走法分数都是0.5。那么,value数组中,这7步的得分一开始是:

0.5 0.5 0.5 0.5 0.5 0.5 1

倒数第二步棋直接导致输棋,分数更新为0。

0.5 0.5 0.5 0.5 0.5 0 1

接着更新倒数第三步棋的分数,要加上倒数第一步棋的分数(1)和自己(0.5)的差的0.1倍,即0.05,更新后的分数为0.55。

0.5 0.5 0.5 0.5 0.55 0 1

接着更新倒数第四步棋的分数,要加上倒数第二步棋的分数(0)和自己(0.5)的差的0.1倍,即-0.05,更新后的分数为0.45。

0.5 0.5 0.5 0.45 0.55 0 1

接着更新倒数第五步棋的分数,要加上倒数第三步棋的分数(0.55)和自己(0.5)的差的0.1倍,即0.005,更新后的分数为0.505。 

0.5 0.5 0.505 0.45 0.55 0 1

同理更新前2步的分数:

0.5005 0.495 0.505 0.45 0.55 0 1

这就是一次强化学习。最终训练的结果,也就是value数组只需要保存到文件中,需要对局时再从文件中读取数据,这样就不用每次都重新训练了。

void SaveValue(value_t value){FILE* fin = fopen("value.dat", "wb");if (fin == NULL){perror("fopen");exit(2);}// 保存valuefwrite(value, sizeof(double), 177147, fin);fclose(fin);fin = NULL;}bool LoadValue(value_t value){FILE* fout = fopen("value.dat", "rb");// 没有这个文件if (fout == NULL)return false;// 加载valuefread(value, sizeof(double), 177147, fout);fclose(fout);fout = NULL;return true;}

完整代码

已上传至gitee链接

阅读本书更多章节>>>>

本文链接:https://www.kjpai.cn/xuetang/2024-04-08/155604.html,文章来源:网络cs,作者:淼淼,版权归作者所有,如需转载请注明来源和作者,否则将追究法律责任!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。

文章评论