UCT的实现
- template<uint T>
- class Node {
- static Pool<Node, uct_max_nodes> m_pool;
- public:
- Vertex<T> v;
- int win;
- uint count;
- Node* first_child;
- Node* sibling;
- static void * operator new(size_t t) {
- assertc(pool_ac, t == sizeof(Node));
- return Node::m_pool.malloc();
- }
- static void operator delete(void *p) {
- Node::m_pool.free((Node*)p);
- }
- };
- template<uint T>
- class UCBTree {
- public:
- union {
- Node<T>* root;
- Node<T>* history[uct_max_depth];
- };
- uint history_top;
- };
至于Pool的实现可以有多种方式,只要效率高就可以了。我目前用的是一个简单的定长数组,据说用标准库的list来做效率也不错,而且它是变长的。可惜物理内存不是无限大的,因此对于我的试验阶段,用定长数组,限定一个最大节点数更合适一些。
UCG的改进
博弈树并不是一棵数,因为可能由不同的走棋顺序走出来同样的局面,如果每个节点代表一个局面,那么这个局面就应该有不止一个父节点,那么这应该是图结构(Graph)而非树。
图的理解和编程要比树复杂一些,反正我大学时的数据结构课就没好好学图。不过这次我想到了一个技巧来避开图结构,但是同样达到图的目的。
这个想法来自设计模式中的享元,也就是我依然使用树作为存储结构,但是对于节点的值:
- Vertex<T> v;
- int win;
- uint count;
仅保留v,而win和count用一个指针代替。这个指针指向另一个结构:
- class Stat {
- public:
- int win;
- uint count;
- bool bexist;
- Hash hash;
- Player pl;
- };
这样,大家都看出来我是用hash表来存储共享的节点值,并且我解决冲突的方法是用移位。编程上是简单了,但是凭空浪费了一倍以上的额外空间,心痛呀!
pl和bexist可以压缩存储,能省点空间,或者用链表来解决冲突,也能省点空间。或者读者中有谁能告诉我一个更好的方法来实现UCG?
在更进一步优化UCG的实现前,我先试试我这个粗糙的UCG的效果,确实比起单纯的UCT来,感觉棋力立即改善了。
以五子棋为例,AI先行前几步竟然走出了花月必胜局的走法,吓了我一跳,一看log,思考深度达到9层。虽然许多五子棋程序都能走出开局定式,不过那是用了开局库,这个算法可是纯靠自己算出来的。
不过接下来它没能把胜势演变为胜局,这是我意料之中的,因为上一篇文章中提到,在五子棋中我人为设定了选点范围为已有棋子的邻点(严格说这算是给了它知识吧?),因此它是绝对发现不了跳二甚至跳一的妙手的,也会因此忽略对方的强防。这导致它不是一个一致的算法。
即使这样,它无论执白还是执黑的凌厉攻势还是让我叹服,只要你给它机会它就能抓住。不挡对方活三的问题已经解决的很好了,现在如果它不挡活三绝大多数情况下是因为挡了也会输。
不过它比起黑石还是差了很多。
在围棋上的19路盘,这个算法和UCT一样,还是没法下,因为AI老自己走自己的,根本不理你。换到9路棋盘上,测试了一下,比单纯的UCT好一些,可以和业余k级的人一战了。虽然它仍然没有解决好征子的问题,不过我试了一局它逃征子好像逃的还很有道理。但是和mogo的对局仍然是完败。
总结一下:UCG的引入会比UCT好一些,不过没有质的飞跃。在大棋盘或者较多选点的情况下,依然无法做到快速收敛。下次我该尝试一下RAVE/AMAF了,据说这是一个高速评估局面的方法,但愿它能模拟一局顶十局。