蒙特卡洛方法与蒙特卡洛树
蒙特卡洛(Monter Carlo)是一座著名的赌城。既然是赌,那多多少少会带有随机的成分。借用其名的蒙特卡洛方法就是一种随机方法。
在概率论的课堂上有过利用蒙特卡洛方法计算圆周率的例子(投针法):
- 在地面上绘制等间距为L的平行直线
- 用若干根长度为d(d < L)的针投向地面
- 统计针与直线相交的频率p
- 计算圆周率 π = 2d / pL
当d = 2L时,π = 1 / p。证明方法主要有两种(课堂上用的是第二种):
- 如果投的是一个周长为d的圆环,则其直径应为:d / π,那么它与直线相交的概率为 d / πL,且相交时交点数为2(相切时因为是第一类间断点对积分没有影响;或可视为2个重合的交点)。由此,交点数的均值就是2d / πL。将圆环掰直以后,其实它们与直线相交的机会还是均等的,只不过此时交点最多为1,且概率P = 2d / πL,用频率逼近概率可以得到π = 2d / pL。
- 当然也可以用纯数学的公式推导。设针的中点到离它最近的一条直线的距离为a, 针与直线的夹角为θ,则a服从[0, L / 2]的随机分布,θ服从[0,π / 2]的随机分布,f(x) = 4 / πL。当针与直线相交时,a∈[0,d sinθ / 2],对a、θ积分可得相同结果。
当然用蒙特卡罗方法计算圆周率还有更加直观的方法,比如统计大量随机点落入圆形的概率,结合圆形的面积公式计算圆周率。
与这种方法类似,蒙特卡罗树搜索就是用大量的、随机的搜索次数寻找最佳的选择;但蒙特卡洛树搜素的方法并非完全随机——在利用置信区间选择出最佳节点之后,随机进行模拟而找到叶节点。利用置信区间选择时会用到相关的启发函数,因此,蒙特卡洛树搜素可以视为一种启发式算法。
基本博弈理论
完全信息博弈
完全信息博弈指博弈双方信息完全共享的博弈,其中,动态博弈是指博弈双方轮流决策,静态博弈是指博弈双方同时决策。
组合博弈
组合博弈是指满足以下条件的博弈:
- 有且仅有两个玩家
- 游戏双方轮流操作
- 游戏操作状态是个有限的集合
- 游戏必须在有限次内结束
- 当一方无法操作时,游戏结束。
零和博弈
若记博弈中胜利为1,平局为0,失败为-1,那么在零和博弈中博弈双方的总收益之和为零;换言之,在零和博弈中没有双赢和双输。
蒙特卡洛树搜素适用条件
- 完全信息博弈
- 零和博弈
- 搜索空间巨大(很难实现更高层次的穷举,如围棋等)
- 组合游戏
- 每一个操作没有随机因素且没有连续值
置信区间上界(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;
}
结果的选择
完成蒙特卡罗树搜索后该如何作出最优决策呢?主要有三种选择:
- 选择UCB值最大的节点
- 选择访问次数最多的节点
- 选择胜率最高的节点
一般会采用访问次数最多的节点作为最终的决策。