现在的位置: 首页 > 综合 > 正文

围棋AI之路(五)自然选择过程

2013年03月19日 ⁄ 综合 ⁄ 共 6472字 ⁄ 字号 评论关闭
上次一个网友建议我用遗传算法,不过当时我没有找到合适的遗传因子,其实根本原因是我那时候正在手工测试程序的棋力,我自己和程序下,发现问题,然后看看怎么改进。这个过程中其实带有太强的主观色彩了,直到一周前我正式摒弃了UCG的想法,我才终于决定用自然选择的方式来测试程序的棋力。
摒弃UCG
前面的文章中提到过UCG的思想,也就是认为博弈树实际上应该看做是一个图,因为下棋时的每一个局面,都可以用不止一种顺序走出来,也就是每个节点不止一个父节点。我刚接触UCT算法时,就想到了这一点,而且奇怪为何没人这么做,总觉得如果按照图来处理,模拟的结果应该更逼近真实情况吧!因为图的处理比树麻烦,还是使用树结构去做了。
再后来,我发现计算机围棋论坛上也有人提出UCG,而我又想到了可以用置换表等效的实现后,很是兴奋,人都是会对自己的想法夸大优点,并且无意中忽略缺点。例如我对我实现的UCG多占用的一倍内存视而不见,速度比UCT慢我也能接受,并且出于良好的自我感觉,在与程序对战后,我做出了“UCG使得程序棋力立即有了提高”的判断。
幸好我在进一步改进UCG机制前,我写了个脚本让两个程序之间对战。并从对战结果中发现一个残酷的事实:使用UCG的程序完败。
在事实面前,我才想明白了原由:UCT算法本身是一个对所有节点公平的算法,如果某个节点在树中有同型节点存在,那么其任何一个兄弟节点也存在同型节点。而UCG的作用是使一个节点的模拟结果被其全部同型节点共享,共享或者不共享,影响的只是对节点过往胜率的重视程度,但是UCT算法本身就有调节这种重视程度的机制,还记得UCT的本质吗?就是在开发和探索之间寻求一种平衡,对过往胜率的重视就意味着把开发的权重加大。
这就是说我挖空心思做的一个UCG改进没有丝毫意义,它的效果可以通过调整UCB公式的参数模拟出来。
AMAF的使用
这是一种最大限度利用模拟结果的手段,All Moves As First,即所有的棋步都像第一步一样。例如,黑棋假设它在A1处下一步棋,然后进行模拟,模拟结果为黑胜,那么传统的UCT计分方法是为A1这个节点加一分,并把结果向A1的祖先节点汇总,也就是把黑棋胜利归功于A1这步棋。而按照AMAF的做法,则是把功劳均摊到A1以及模拟对局中黑方所下的每一个位置。
这个方法的优点在于能用少量的模拟局数获取大量的得分情况,从而加快局面评估速度。但是其缺点和优点一样突出,在棋类游戏中的一个常识是,棋步的顺序是与胜负有关系的。这样的快速评估很可能会得出错误的结论。因此,AMAF不是一个一致的算法。
一个折中的做法是设定一个分割比率,只对模拟对局前一部分的棋步计分,通常我们认为先下的棋步要比后下的棋重要。这个比率的取值从0到1,显而易见,如果取0,算法就还原为原始的UCT了;如果取1,则又相当于一个完整的AMAF了。取什么样的值,我用自然选择来决定。
自然选择
遗传因子我已经选好了,一共3个,分别是节点的成熟度(一个节点被模拟多少局后才为其创建子节点,going down的说法是ooxx多少次后才生娃),探险的权重,AMAF的分割比率。
维护一个种群数量不变,每次随机取出两个遗传因子不同的个体,让它们分先下两局棋,输了的杀掉,赢了的可以无性繁殖一个后代,繁殖的过程中有一定几率发生变异。如果没有分出胜负,那也不能让它们无限的活下去,无论胜负,每下一次棋,生命减1,生命到0也直接抹掉。然后我会再随机创造新的个体去补齐数量的。
听起来颇有造物主的感觉呀,进化将会是一个漫长的过程,为了让它们下的棋有意义,我每步棋给了它们足足一秒钟的思考时间。从本文开始写作时,它们的竞争已经开始了,到现在,预先设置的两个种子选手之一已经被淘汰掉了一个,不过离进化的终点还差的很远,按照游戏规则,这场进化的结束条件是全部的个体都拥有相同的遗传因子。
程序实现上,我采用了unix的传统,用C实现核心部分,脚本语言作为黏合剂。至于代码,实在没什么新东西,无非是测试和调整了些参数,我也懒得一遍遍上传了。实现自然选择的脚本我贴在下面,有兴趣可以参考。
  1. #! /usr/bin/perl -w
  2. # http://search.cpan.org/~jzucker/DBD-CSV-0.22/lib/DBD/CSV.pm
  3. use DBI;
  4. local $MAX_COUNT = 40;
  5. local $curid;
  6. local $prgname = "oygo.exe";
  7. my $time_to_die = 0;
  8. sub signal_handler {
  9.         $time_to_die = 1;
  10. }
  11. $SIG{INT} = $SIG{TERM} = $SIG{HUP} = /&signal_handler;
  12. my $dbh = DBI->connect("DBI:CSV:f_dir=./csv");
  13. $dbh->{'RaiseError'} = 1;
  14. $@ = '';
  15. eval {
  16.         $dbh->do("CREATE TABLE renju (id INTEGER, gid INTEGER, life INTEGER, gene1 INTEGER, gene2 char(50), gene3 char(50))");
  17. };
  18. sub insert_rand {
  19.         my $id = shift;
  20.         my ($gene1$gene2$gene3) = rand_gene();
  21.         return "INSERT INTO renju VALUES ($id, $id, 10, '$gene1', '$gene2', '$gene3')";
  22. }
  23. sub remove {
  24.         my $id = shift;
  25.         return "delete from renju where id=$id";
  26. }
  27. #mature [0,+infinite)
  28. #explore_rate [0, 16) float
  29. #aaf_fraction [0,1] float
  30. sub rand_gene {
  31.         return (int(rand 50000), rand 16, rand 1);
  32. }
  33. sub clone {
  34.         my $a = shift;
  35.         my ($id$gid$life$gene1$gene2$gene3) = @$a;
  36.         $curid++;
  37.         if(int(rand 5) == 1) {
  38.                 my $n = int(rand 3);
  39.                 if($n == 0) {
  40.                         $gene1 = int(mutate($gene1, 0, 50000));
  41.                 } elsif($n == 1) {
  42.                         $gene2 = mutate($gene2, 0, 16);
  43.                 } else {
  44.                         $gene3 = mutate($gene3, 0, 1);
  45.                 }
  46.                 unless(($gene1 == $a->[3] && 
  47.                                 $gene2 == $a->[4] && 
  48.                                 $gene3 == $a->[5])) {                      
  49.                         $gid = $curid;
  50.                 }
  51.         }
  52.         return "INSERT INTO renju VALUES ($curid, $gid, 10, '$gene1', '$gene2', '$gene3')";
  53. }
  54. # <1>步长变异
  55. # <2>高斯变异
  56. # 这里用的是步长变异
  57. sub mutate {
  58.         my $value = shift;
  59.         my $lower = shift;
  60.         my $upper = shift;
  61.         my $Boundary = 1/20;
  62.         my $Delta = 0;
  63.         for (my $i = 0; $i < 20; $i++) {
  64.                 if (rand 1 < $Boundary) {
  65.                         $Delta += 1/(1 << $i);
  66.                 }
  67.         }
  68.         if(rand 1 > 0.5) {
  69.                 $value += ($upper - $value) * $Delta * 0.5;
  70.         } else {
  71.                 $value -= ($value - $lower) * $Delta * 0.5;
  72.         }
  73.         return $value;
  74. }
  75. sub match {
  76.         my $a1 = shift;
  77.         my $a2 = shift;
  78.         my $gid1 = $a1->[1];
  79.         my $gid2 = $a2->[1];
  80.         my $str1 = join(",", (@$a1)[3,4,5]);
  81.         my $str2 = join(",", (@$a2)[3,4,5]);
  82.         #print "$str1 win $str2/n";
  83.         my $cmd = "perl towgo.pl --games 2 -b='$prgname --arg=$str1' -w='$prgname --arg=$str2' 2>/dev/null";
  84.         my $result = `$cmd`;
  85.         if($result =~ /winner:(.*)$/i) {
  86.                 $winner = $1;
  87.                 if($winner eq "$prgname --arg=$str1") {
  88.                         print "$gid1 -> $gid2/n";
  89.                         return 1;
  90.                 } else {
  91.                         print "$gid2 -> $gid1/n";
  92.                         return -1;
  93.                 }
  94.         }
  95.         print "--$gid1,$gid2/n";
  96.         return 0;
  97. }
  98. while(!$time_to_die) {
  99.         my $count;
  100.         $sth = $dbh->prepare("SELECT count(id) as n FROM renju");
  101.         $sth->execute();
  102.         $sth->bind_columns(undef, /$count);
  103.         $sth->fetch;
  104.         $sth->finish;
  105.         $sth = $dbh->prepare("SELECT max(id) as n FROM renju");
  106.         $sth->execute();
  107.         $sth->bind_columns(undef, /$curid);
  108.         $sth->fetch;
  109.         $sth->finish;
  110.         $curid = 0 unless($curid);
  111.         $count = 0 unless($count);
  112.         for(my $i = $count$i < $MAX_COUNT$i++) {
  113.                 $curid++;
  114.                 $dbh->do(insert_rand($curid));
  115.         }
  116.         my $randrow = int(rand($MAX_COUNT));
  117.         $sth = $dbh->prepare("SELECT * FROM renju limit $randrow,1");
  118.         $sth->execute();
  119.         my @a1 = $sth->fetchrow();
  120.         $sth->finish;
  121.         my $gid1 = $a1[1];
  122.         $sth = $dbh->prepare("SELECT count(id) as n FROM renju where gid <> $gid1");
  123.         my $numrows;
  124.         $sth->execute();
  125.         $sth->bind_columns(undef, /$numrows);
  126.         $sth->fetch;
  127.         $sth->finish;
  128.         die "selection finished!" unless($numrows);
  129.         $randrow = int(rand($numrows));
  130.         $sth = $dbh->prepare("SELECT * FROM renju where gid <> $gid1 limit $randrow,1");
  131.         $sth->execute();
  132.         my @a2 = $sth->fetchrow();
  133.         my $result = match(/@a1, /@a2);
  134.         if(!$time_to_die) {
  135.                 $a1[2]--;
  136.                 $a2[2]--;
  137.                 $dbh->do("update renju set life=".$a1[2]." where id=".$a1[0]);
  138.                 $dbh->do("update renju set life=".$a1[2]." where id=".$a2[0]);
  139.         }
  140.         if($result > 0) {
  141.                 $dbh->do(clone(/@a1));
  142.                 $dbh->do(remove($a2[0]));
  143.         } elsif ($result < 0) {
  144.                 $dbh->do(clone(/@a2));
  145.                 $dbh->do(remove($a1[0]));
  146.         } else {
  147.         }
  148.         $dbh->do("delete from renju where life<=0");
  149. } # end while
  150. $dbh->disconnect();
  151. print "exit.../n";

抱歉!评论已关闭.