【笔记】蒙特卡洛树搜素(MCTS)与三子棋

本文介绍蒙特卡洛方法及其在树搜索中的应用,包括基本步骤、置信区间上界公式及如何应用于三子棋游戏。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

蒙特卡洛方法与蒙特卡洛树

蒙特卡洛(Monter Carlo)是一座著名的赌城。既然是赌,那多多少少会带有随机的成分。借用其名的蒙特卡洛方法就是一种随机方法。

在概率论的课堂上有过利用蒙特卡洛方法计算圆周率的例子(投针法):

  1. 在地面上绘制等间距为L的平行直线
  2. 用若干根长度为d(d < L)的针投向地面
  3. 统计针与直线相交的频率p
  4. 计算圆周率 π = 2d / pL

当d = 2L时,π = 1 / p。证明方法主要有两种(课堂上用的是第二种):

  1. 如果投的是一个周长为d的圆环,则其直径应为:d / π,那么它与直线相交的概率为 d / πL,且相交时交点数为2(相切时因为是第一类间断点对积分没有影响;或可视为2个重合的交点)。由此,交点数的均值就是2d / πL。将圆环掰直以后,其实它们与直线相交的机会还是均等的,只不过此时交点最多为1,且概率P = 2d / πL,用频率逼近概率可以得到π = 2d / pL。
  2. 当然也可以用纯数学的公式推导。设针的中点到离它最近的一条直线的距离为a, 针与直线的夹角为θ,则a服从[0, L / 2]的随机分布,θ服从[0,π / 2]的随机分布,f(x) = 4 / πL。当针与直线相交时,a∈[0,d sinθ / 2],对a、θ积分可得相同结果。

当然用蒙特卡罗方法计算圆周率还有更加直观的方法,比如统计大量随机点落入圆形的概率,结合圆形的面积公式计算圆周率。

与这种方法类似,蒙特卡罗树搜索就是用大量的、随机的搜索次数寻找最佳的选择;但蒙特卡洛树搜素的方法并非完全随机——在利用置信区间选择出最佳节点之后,随机进行模拟而找到叶节点。利用置信区间选择时会用到相关的启发函数,因此,蒙特卡洛树搜素可以视为一种启发式算法。

基本博弈理论

完全信息博弈

完全信息博弈指博弈双方信息完全共享的博弈,其中,动态博弈是指博弈双方轮流决策,静态博弈是指博弈双方同时决策。

组合博弈

组合博弈是指满足以下条件的博弈:

  1. 有且仅有两个玩家
  2. 游戏双方轮流操作
  3. 游戏操作状态是个有限的集合
  4. 游戏必须在有限次内结束
  5. 当一方无法操作时,游戏结束。
零和博弈

若记博弈中胜利为1,平局为0,失败为-1,那么在零和博弈中博弈双方的总收益之和为零;换言之,在零和博弈中没有双赢和双输。

蒙特卡洛树搜素适用条件
  1. 完全信息博弈
  2. 零和博弈
  3. 搜索空间巨大(很难实现更高层次的穷举,如围棋等)
  4. 组合游戏
  5. 每一个操作没有随机因素且没有连续值

置信区间上界(Upper Confidence Bound,UCB)

这里采用UCB1策略衡量节点探索的价值:
V ‾ i + 2 l n N n i \overline V_i+ \sqrt \frac{2lnN}{n_i} Vi+ni2lnN
其中, V ‾ i \overline V_i Vi表示当前节点的平均价值;
N N N表示总的探索次数;
n i n_i ni表示当前节点的探索次数。

基本步骤

蒙特卡洛树搜索有四个基本步骤:选择、扩展、模拟、反向传播。

选择(Selection)

若该节点已经是叶子节点(博弈结束),则直接进入反向传播;否则,选择当前节点下UBC值最大的、未被完全扩展的子节点,若UCB值最大的节点已经被扩展,需要进一步选择其UCB值最大的子节点,直到找到未被完全扩展的节点。

扩展(Expansion)

从选择的节点出发,随机选择一个合法动作创建子节点(或多个子节点,再从中随机选一个)。

仿真(Simulation)

顺着这个动作,随机地模拟游戏,直到游戏结束,判断模拟的结果,胜记为1,负记为-1,平局记为0。

反向传播(Backpropagation)

将仿真的结果从叶子节点向上更新,所经过的节点的值加上该叶子节点的值(胜负结果),探索次数增加1.

利用蒙特卡洛树搜素实现三子棋

简单地用C语言实现了一下,但效果并不好……可能是因为随机数或搜索次数太少了……

创建棋局
#include<stdlib.h>
#include<stdio.h>
#include<math.h>
#include<time.h>
#define Inf 99999
#define TIME 3000 //搜索次数
#define C 1 //电脑走子命令
#define P 0 //玩家走子命令

char map[3][3] = {0};	//当前棋局
int T = 9, N = 0; //T是还可以走的空格数,N是总的访问次数

typedef struct NODE {
    int t;  //未被填满的空格数
    int loc; //loc=x+y*10; loc%10=x; loc/10=y
    char map[3][3];
    char vis[3][3];
    float val;
    int order; //该步操作的命令方,取值为P或C
    int N; //该节点的探索次数
    struct NODE *sib;
    struct NODE *fth;
    struct NODE *kid;
}Node, *Node_ptr;

Node_ptr new(Node_ptr ptr) {	//从ptr新建叶子节点并初始化
    Node_ptr p = (Node_ptr)malloc(sizeof(Node));
    p->loc = -1;
    p->t = 0;
    int i, j;
    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++){
            if(ptr){
                p->map[i][j] = ptr->map[i][j];
                if (ptr->map[i][j] != ' ') {
                    p->vis[i][j] = 1;
                    p->t++;
                }
                else
                    p->vis[i][j] = 0;
            }
            if(!ptr){
                p->map[i][j] = map[i][j];
                p->vis[i][j] = 0;
                if(map[i][j] != ' '){
                    p->vis[i][j] = 1;
                    p->t++;
                }
            }
        }
    p->val = 0;
    p->N = 0;
    p->sib = NULL;
    if (ptr) {
        p->sib = ptr->kid;
        ptr->kid = p;
    }
    if (ptr == NULL || ptr->order == P)
        p->order = C;
    else
        p->order = P;
    p->fth = ptr;
    p->kid = NULL;
    return p;
}

int win(int x, int y, char map[3][3]) {	//判断输赢
    if (map[x][0] == map[x][1] && map[x][1] == map[x][2])
        return 1;
    if (map[0][y] == map[1][y] && map[1][y] == map[2][y])
        return 1;
    if (x - y == 0 && (map[0][0] == map[1][1] && map[1][1] == map[2][2])) 
        return 1;
    if (x + y == 2 && map[0][2] == map[1][1] && map[1][1] == map[2][0])
        return 1;
    return 0;
}

void print(char map[3][3]) {	//打印棋盘
    int i = 0;
    printf(" ---  ---  ---\n");
    for (i = 0; i < 3;i++)
        printf(" | %c | %c | %c |\n ---  ---  ---\n", map[i][0], map[i][1], map[i][2]);
}

接下来定义电脑和玩家的走子

void computer_MCTS(){
    int t = TIME;   //重置需要搜索的次数限制
    N = 0;  //重置总访问次数
    Node_ptr p = new (NULL), root, sol;
    while(t--) {
        root = selection(p);
        root = expansion(root);
        rollout(root);
        N++;
    }
    root = p->kid;
    sol = p->kid;
    while(root){
        if (root->N > sol->N)
            sol = root;
        root = root->sib;
    }
    int x, y;
    x = sol->loc % 10;
    y = sol->loc / 10;
    map[x][y] = '-';
    printf("Computer's step: <%d, %d>\n", x + 1, y + 1);
    print(map);
    Dis(p);
    if (win(x, y, map)) 
        printf("Sorry, you lost!\n");
    else if(!--T){
        printf("Draw!\n");
        return;
    }
    else playerplay();
}

void computerplay(){
    computer_MCTS();
}

void playerplay(){
    int x, y;
    printf("Your step: ");
    scanf("%d%d", &x, &y);
    while (1){
        if(x < 1 || x > 3 || y < 1 || y > 3){
            printf("Illegal input!\n");
            scanf("%d%d", &x, &y);
        }
        else if (map[x - 1][y - 1]!=' ') {
            printf("Already existed!\n");
            scanf("%d%d", &x, &y);
        }
        else
            break;
    }
    map[x - 1][y - 1] = '+';
    print(map);
    if (win(x - 1, y - 1, map))
        printf("Congratulations, you win!\n");
    else if(!--T){
        printf("Draw!\n");
        return;
    }
    else computerplay();
}

选择

float UCB(Node_ptr p) {	//计算UCB值
    if (p->N == 0)
        return Inf;
    else
        return p->val / p->N + sqrt(2 * N / p->N);
}

Node_ptr selection(Node_ptr p){
    int i, j;
    //判断是否为叶子节点,若是返回当前节点进入反向传播
    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++)
            if (p->vis[i][j] != 0)
                return p;
    int max = -Inf;
    Node_ptr M;
    Node_ptr m = p;
    while(m){
    	M = m;
        while (m) {
            if (UCB(m) > max) {
                max = UCB(m);
                M = m;
            }
            m = m->sib;
        }
        m = M->kid;
    }
    return M;
}

扩展
Node_ptr expansion(Node_ptr p) {
    int i, j;
    Node_ptr n, g;
    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++) {
            if (p->map[i][j] != ' ' || p->vis[i][j])
                continue;
            n = new (p);
            if (n->order == P)
                n->map[i][j] = '-';
            else
                n->map[i][j] = '+';
            n->vis[i][j] = 1;
            n->t++;
            n->fth = p;
            n->loc = i + j * 10;
            if(p->vis[i][j] == 0)
                p->vis[i][j] = 1;
        }
    int r = rand() % T;
    g = p->kid;
    while(r--) {
        if (g == NULL)
            g = p->kid;
        else g = g->sib;
    }
    if (g == NULL)
        g = p->kid;
    return g;
}
仿真

定义随机走子的操作

Node_ptr playR(int order, Node_ptr p, int t) {
    if(!t){
        p->val = 0;
        return p;
    }
    Node_ptr q = new (p);
    //随机走子
    int r = rand() % t;
    int x, y;
    x = r / 3;
    y = r % 3;
    while (q->map[x][y] != ' ') {
        x = (++r / 3) % 3;
        y = r % 3;
    }
    p->vis[x][y] = 1;
    q->loc = x + y * 10;
    if(order == C){
        q->map[x][y] = '-';
        if (win(x, y, q->map)) {
            q->val = 1;
            return q;
        }
        else return playR(P, q, t - 1);
    }
    else if(order == P) {
        q->map[x][y] = '+';
        if (win(x, y, q->map)) {
            q->val = -1;
            return q;
        }
        else return playR(C, q, t-1);
    }
}

定义仿真

void rollout(Node_ptr p) {
    Node_ptr ptr = playR(p->order, p, 9 - p->t);
    backpropagation(ptr->fth, ptr->val);
}
反向传播
void backpropagation(Node_ptr p, int order) {
    if (p == NULL)
        return;
    else {
        backpropagation(p->fth, order);
        p->val += order;
        p->N++;
    }
}
主函数
int main ( ) {
    int i, j;
    char order;
	srand((unsigned)time(NULL));
    for (i = 0; i < 3; i++)
        for (j = 0; j < 3; j++)
            map[i][j] = ' ';
    printf("Do you wanna play first? y/n\n");
    scanf("%c", &order);
    getchar();
    if (order == 'y') {
        playerplay();
    }
    else if (order == 'n') {
        computerplay();
    }
    return 0;
}

结果的选择

完成蒙特卡罗树搜索后该如何作出最优决策呢?主要有三种选择:

  1. 选择UCB值最大的节点
  2. 选择访问次数最多的节点
  3. 选择胜率最高的节点

一般会采用访问次数最多的节点作为最终的决策。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值