围棋AI之路(四):来自UCG的改进

UCT的实现

前文只顾抱怨,忘记讲述UCT的实现部分了,这里补上。UCT算法本身在第一篇已经详细阐述过了,具体实现上唯一的一个要点就是使用内存池来为Tree分配节点。树的结构如下所示:
  1. template<uint T>
  2. class Node {
  3.         static Pool<Node, uct_max_nodes> m_pool;
  4. public:
  5.         Vertex<T> v;
  6.         int    win;
  7.         uint    count;
  8.         Node*  first_child;
  9.         Node*  sibling;
  10.         static void * operator new(size_t t) {
  11.                 assertc(pool_ac, t == sizeof(Node));
  12.                 return Node::m_pool.malloc();
  13.         }
  14.         static void operator delete(void *p) {
  15.                 Node::m_pool.free((Node*)p);
  16.         }
  17. };
  18. template<uint T>
  19. class UCBTree {
  20. public:
  21.         union {
  22.                 Node<T>* root;
  23.                 Node<T>* history[uct_max_depth];
  24.         };
  25.         uint history_top;
  26. };
UCBTree中的history成员是用来跟踪树的当前节点的路径的,因为现在这棵树的节点是用孩子兄弟的二叉链表来做的,这种结构不方便得到父节点,如果增加一个父节点指针又嫌浪费内存,毕竟UCT算法的任何时刻只有一个分支需要访问父节点,而让所有节点都承受这个代码太不公平了,因此这里另设了一个数组来存储。

至于Pool的实现可以有多种方式,只要效率高就可以了。我目前用的是一个简单的定长数组,据说用标准库的list来做效率也不错,而且它是变长的。可惜物理内存不是无限大的,因此对于我的试验阶段,用定长数组,限定一个最大节点数更合适一些。

UCG的改进

博弈树并不是一棵数,因为可能由不同的走棋顺序走出来同样的局面,如果每个节点代表一个局面,那么这个局面就应该有不止一个父节点,那么这应该是图结构(Graph)而非树。

图的理解和编程要比树复杂一些,反正我大学时的数据结构课就没好好学图。不过这次我想到了一个技巧来避开图结构,但是同样达到图的目的。

这个想法来自设计模式中的享元,也就是我依然使用树作为存储结构,但是对于节点的值:
  1.         Vertex<T> v;
  2.         int    win;
  3.         uint    count;

仅保留v,而win和count用一个指针代替。这个指针指向另一个结构:
  1. class Stat {
  2. public:
  3.   int win;
  4.   uint count;
  5.   bool bexist;
  6.   Hash hash;
  7.   Player pl;
  8. };
这里hash是一个64位的数,由局面求出来的Zorbist Hash。基本可以保证不冲突(至少对现在的普通计算机内存而言不冲突),pl则是要区分是哪一个玩家所留下的局面,因为同一个局面轮谁走的结果是不一样的,因此把它看成是不同的局面。bexist则用于hash表中判断一个位置是否被占用过。

这样,大家都看出来我是用hash表来存储共享的节点值,并且我解决冲突的方法是用移位。编程上是简单了,但是凭空浪费了一倍以上的额外空间,心痛呀!

pl和bexist可以压缩存储,能省点空间,或者用链表来解决冲突,也能省点空间。或者读者中有谁能告诉我一个更好的方法来实现UCG?

在更进一步优化UCG的实现前,我先试试我这个粗糙的UCG的效果,确实比起单纯的UCT来,感觉棋力立即改善了。

以五子棋为例,AI先行前几步竟然走出了花月必胜局的走法,吓了我一跳,一看log,思考深度达到9层。虽然许多五子棋程序都能走出开局定式,不过那是用了开局库,这个算法可是纯靠自己算出来的。

不过接下来它没能把胜势演变为胜局,这是我意料之中的,因为上一篇文章中提到,在五子棋中我人为设定了选点范围为已有棋子的邻点(严格说这算是给了它知识吧?),因此它是绝对发现不了跳二甚至跳一的妙手的,也会因此忽略对方的强防。这导致它不是一个一致的算法。

即使这样,它无论执白还是执黑的凌厉攻势还是让我叹服,只要你给它机会它就能抓住。不挡对方活三的问题已经解决的很好了,现在如果它不挡活三绝大多数情况下是因为挡了也会输。

不过它比起黑石还是差了很多。

在围棋上的19路盘,这个算法和UCT一样,还是没法下,因为AI老自己走自己的,根本不理你。换到9路棋盘上,测试了一下,比单纯的UCT好一些,可以和业余k级的人一战了。虽然它仍然没有解决好征子的问题,不过我试了一局它逃征子好像逃的还很有道理。但是和mogo的对局仍然是完败。


总结一下:UCG的引入会比UCT好一些,不过没有质的飞跃。在大棋盘或者较多选点的情况下,依然无法做到快速收敛。下次我该尝试一下RAVE/AMAF了,据说这是一个高速评估局面的方法,但愿它能模拟一局顶十局。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值