蒙特卡洛树搜索_从零开始用Golang实现AlphaZero下象棋——蒙特卡洛树

本文介绍了如何使用Golang实现AlphaZero的蒙特卡洛树搜索,探讨了蒙特卡洛搜索树的原理及其在策略游戏中应用,通过神经网络和强化学习进行棋局评估和策略优化。文中还展示了部分Golang代码,用于构建搜索树和反馈结果。
摘要由CSDN通过智能技术生成

d6b6a6974ab4572b9e578bb25b009532.png

关于AlphaGo和AlphaZero的消息已经不是新鲜事,似乎大家已经接受这样的东西的存在。这个“下棋机器人”的原理我也在下文中给出了简单的介绍。那么,到底这个“下棋机器人”是怎么运作的,我们就从本文开始,一起动手去实现一下它,帮助大家深入理解神经网络和强化学习。

一然:AlphaZero背后的算法原理解析​zhuanlan.zhihu.com
8acc1f403d2069f07d779a1a29b054f8.png

AlphaZero涉及以下几个关键点:

  • 蒙特卡洛搜索树,一个数据结构,相当于“记忆体”,用来搜索策略,并且探索结果
  • 神经网络,用来判断局势,选择可能的策略,并给出获胜的概率
  • 强化学习,通过自己跟自己下棋,来训练神经网络,更新其参数,同时优化蒙特卡洛搜索树

我们先从容易的地方着手,来搞一搞蒙特卡洛搜索树。顺便提一句,为什么我选择了Golang,一个是个人兴趣,一个是想用底层一点的语言来让大家看清楚细节,同时也想在Java、Python横行的时候给大家一点别的选择。并且,我不想用TensorFlow,虽然它真的很强大很不错,但是会选择Golang家族自己的机器学习库,也是为了带来一点新鲜感。

闲话少说,进入主题。

蒙特卡洛搜索树是2006年左右由 Rémi Coulom 等人提出的,当然“这棵树”的诞生是为了搜索。众所周知,在策略类游戏中,我们总是要预先考虑未来发生的状况,从而在现在选择较好的策略去应对,在极端状态下,当然是穷举所有的未来情况,一一给出应对策略,然后再选出一个最好的。不过这样做事不切实际的,因为搜索空间太大了,可能的情况太多了。这也就是为什么我们在博弈类游戏中感觉很累的原因:我们不可能穷尽那么大的搜索空间再给出策略。

这样,我们自然会想到,在不穷尽所有可能性的情况下,能不能给出比较好的策略呢?换句话说,有点像”走一步,算一步“,发现好的策略就记住,就很像是动态规划。基于这样的思路,有人找到了蒙特卡洛方法,这个方法基本就是”走着瞧,边看边算“,而把这种方法用在博弈类游戏上,自然就会想到树形结构嘛,于是就搞出来一个蒙特卡洛树,这棵树是用来搜索策略的,就有了蒙特卡洛搜索树。相对应的搜索方法就是蒙特卡洛树搜索法。

同时这类策略性游戏也很多,例如围棋,中国象棋等等,我们这次用AlphaGo来玩中国象棋。

09f5475dc8d3881232e6de86e54cec6b.png

如上图,我们这棵简单的树也是一棵策略树,用来搜索策略,它每个节点表示一个状态,从一个节点到另一个节点,表示状态的转移,也就是选择了一个策略。这棵树从根节点开始遍历,到达叶子节点的时候,也就是一局棋下完了,会有一个结果,从结果,我们就可以评判这个策略”路径“选择得好不好。

前文已经述及,这样一棵策略树的主要问题就是遍历(搜索)的空间太大了,没办法穷尽所有可能再做决策。虽然历史上有剪枝算法并且可以控制树的深度,但是那都是历史了,不做介绍。那么,对于如何选出最好的下一步策略,蒙特卡洛树就给出了一个相当简洁的答案:对于某一个节点,选最优秀的儿子节点,没有儿子的时候就创造一个儿子节点!

What?!简直没说一样,能知道谁最优秀,还整这些个干什么。对了,不知道哪个儿子节点最棒,那就找个明白人问一问。那么谁算是明白人呢?好了,假设大法搬出来,假设有一个神经网络明白哪个儿子节点最棒,问问这个网络不就行了。可是问题又来了,开天辟地之时,棋盘初立,两军对垒,神经网络里面的参数一片空白,怎么可能知道谁优秀呢。答案更简单了,随便选,选了看结果,把结果记住,下次不就知道好坏了。

上面这个过程提炼一下就是,策略树为构成 的时候,不知道选什么策略,就问神经网络;神经网络未经训练的时候,就给随机值;蒙特卡洛树得到结果后,就生长出一个子节点;但是每次出了结果,有了输赢,就反馈给策略树和神经网络,让这个系统无穷无尽的运转下去,就有了优秀的策略树和神经网络。非常简单吧。

下面来点真格的内容。

对于每一个节点,都代表一个策略,都设立一个积分q,分值越高表示这个节点所代表的策略越是优秀,同时设立一个访问次数n,代表这个节点被访问的次数。这时候就有,在选择儿子节点的时候,我们的原则是:分值高的并且访问次数少的。用数学表达一下就是:

上式中,q是分值,n是访问次数,

是父节点的访问次数。

c是什么?是一个折扣因子,代表第一项和第二项的权重,也就是地位不一样,后面的要打个折扣,免得都去选访问次数少的策略,而陷入无休止的探索中。

忍不住为这个公式上一段 Golang 代码来实现一下:

func (curNode *mcTreeNode) chooseBestChild(c float64) *mcTreeNode {
	idx := 0
	maxValue := -math.MaxFloat64
	var childValue float64
	for i, child := range curNode.children {
		childValue = (child.score / child.visitCount) +
			c*math.Sqrt(math.Log(curNode.visitCount)/child.visitCount)
		if childValue > maxValue {
			maxValue = childValue
			idx = i
		}
	}
	return curNode.children[idx]
}

下面介绍棋局结束以后反馈的过程。某一局棋结束以后,我们总要把结果反馈回去,告诉蒙特卡洛搜索树本次一系列的选择好不好。那么方法也是非常简单的,就是把结果的分数(例如胜:50;负:-50;和:0)逐个累加到路径上的各个节点上,直到根节点;同时把这条路径上各个节点的访问次数也加一。代码实现如下:

func (curNode *mcTreeNode) backPropagate(result float64) {
	nodeCursor := curNode
	for !nodeCursor.isRoot() {
		nodeCursor.score += result
		nodeCursor.visitCount++
		nodeCursor = nodeCursor.parent
	}
	nodeCursor.visitCount++
}

有一点好像落下了,那就是如何”生出“一个儿子节点:

func (curNode *mcTreeNode) expand() *mcTreeNode {
	child := getNewNode(curNode)
	curNode.addChild(&child)
	return &child
}

这样就有了这棵树的基本模样了,可以开始遍历了:

func MonteCarloTreeSearch(simulations int) {
	root := getRootNode()
	var leaf *mcTreeNode
	var result float64
	for i := 0; i < simulations; i++ {
		leaf = root.treePolicy()
		// TODO: calculate the result
		result = 50
		leaf.backPropagate(result)
	}
}

一个大大TODO写在那里,意思就是想要计算出结果,是要花点力气的,要给予棋盘现在的状态和下棋的规则,给出一个判断,本次实现中暂且略去。

接下来就是最后一个问题,虽然蒙特卡洛树是”边走边看边想“的,也是在搜索中慢慢生成出来的,但是,何时结束搜索,仍然是一个问题,因为如果搜索规模太大,可能导致每一步棋思考时间太长,如果搜索规模太小,也许结果太粗陋。综合上述情况,一般是把搜索规模做成可以配置的项目,让每一次搜索都在机器硬件资源可以支持的情况下最大化。

最后我们给出比较完整的代码:

package alphazerochinesechess

import "math"

const discountParamC = 1.4

type mcTreeNode struct {
	parent *mcTreeNode
	children []*mcTreeNode
	score float64
	visitCount float64
	chessBoard [9][9]int8
}

func MonteCarloTreeSearch(simulations int) {
	root := getRootNode()
	var leaf *mcTreeNode
	var result float64
	for i := 0; i < simulations; i++ {
		leaf = root.treePolicy()
		// TODO: calculate the result
		result = 50
		leaf.backPropagate(result)
	}
}

func getNewNode(parent *mcTreeNode) mcTreeNode {
	node := mcTreeNode{parent: parent}
	node.children = make([]*mcTreeNode, 0, 0)
	node.score = 0
	node.visitCount = 0
	return node
}

func getRootNode() mcTreeNode {
	return getNewNode(nil)
}

func (curNode *mcTreeNode) addChild(child *mcTreeNode) {
	curNode.children = append(curNode.children, child)
}

func (curNode *mcTreeNode) isRoot() bool {
	return curNode.parent == nil
}

func (curNode *mcTreeNode) chooseBestChild(c float64) *mcTreeNode {
	idx := 0
	maxValue := -math.MaxFloat64
	var childValue float64
	for i, child := range curNode.children {
		childValue = (child.score / child.visitCount) +
			c*math.Sqrt(math.Log(curNode.visitCount)/child.visitCount)
		if childValue > maxValue {
			maxValue = childValue
			idx = i
		}
	}
	return curNode.children[idx]
}

func (curNode *mcTreeNode) backPropagate(result float64) {
	nodeCursor := curNode
	for !nodeCursor.isRoot() {
		nodeCursor.score += result
		nodeCursor.visitCount++
		nodeCursor = nodeCursor.parent
	}
	nodeCursor.visitCount++
}

func (curNode *mcTreeNode) popPotentialAction() {
	// ask CNN for one action
}

func (curNode *mcTreeNode) expand() *mcTreeNode {
	child := getNewNode(curNode)
	curNode.addChild(&child)
	return &child
}

func (curNode *mcTreeNode) isFullyExpanded() bool {
	// ask CNN if there is another plan
	return true
}

func (curNode *mcTreeNode) isTerminal() bool {
	// check if the chess is finished by rules
	return true
}

func (curNode *mcTreeNode) treePolicy() *mcTreeNode {
	nodeCursor := curNode
	for !nodeCursor.isTerminal() {
		if !nodeCursor.isFullyExpanded() {
			return nodeCursor.expand()
		}
		nodeCursor = nodeCursor.chooseBestChild(discountParamC)
	}
	return nodeCursor
}

上面的代码并不完整,因为对于整个这个AlphaZero下象棋的问题,我们的实现才刚刚开始,还缺失关键的一些数据结构,比如棋盘的结构,神经网络的构成,蒙特卡洛树和神经网络的通信体等等,都需要在后续的过程中逐一完成。

可以参考上篇文章一起看:

一然:AlphaZero背后的算法原理解析​zhuanlan.zhihu.com
8acc1f403d2069f07d779a1a29b054f8.png

以上仅供大家参考,祝大家工作学习愉快,多谢啦 :0

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值