打败人类,Pluribus第一课,如何抽象德扑的牌组——Kmeans with EMD distance
最近一直在看Pluribus相关的文献,开始研究实现德扑ai的第一步将牌组分类简化,根据Sam Ganzfried和Tuomas Sandholm的文章Potential-Aware Imperfect-RecallAbstraction with EarthMover’s Distance in Imperfect-Information Games,用的方法就是Kmeans聚类,聚类的时候使用的距离并不是常见的欧式距离,而是使用了判断向量相似性的emd距离,将概率分布相近的牌组视为同一类,emd距离的计算比较费时,所以在对亿级数据处理的时候,提升计算速度是程序执行的关键,我的解决方案就是并行加上优化emd的算法。本文首先介绍下的文章讲解对牌组聚类的方法,文章后面附优化后的就算emd距离的代码。
分类思路简介
对牌组的分类,有四种不同的情况 ,对应的是 hand,flop,turn,river。 常见的牌组分类方法就是把在仅考虑牌组打到river时胜率相近的牌组归为同一类别,即expected hand strength(EHS) metric 方法 。举例来说,如果一个玩家有对A作为手牌,那么发到河牌他获胜的概率是84.93%,打平的概率是0.55%,那么给予对A的评分是0.852。同样的对K的胜率是0.824, 这两手牌得分类似,因此把他们归类为同一类别。但是这种方法没有考虑一手牌的发展情况。忽略了某些手牌虽然初始胜率低,但是潜力大的情况。number of clusters, using the EMD 在德扑领域,前沿的分类方法是,首先,对于翻前手牌不抽象,对于翻后和转牌,根据发到河牌的胜率分布的情况进行Kmeans聚类,但是这种方法,被未考虑整个分布随着发牌的变化,因此更好的方法是首先对河牌分类,然后转牌的牌组,根据分布到下一轮可能的概率分布进行分类,翻后的牌组,根据下一轮可能分到的转牌类别的概率分布进行分类,手牌不抽象。下图展示了翻后牌组在河牌的概率分布,和对下一轮转牌的概率分布的情况,对于 5c9d和TcQd,他们收到的翻牌是7h9hQh,可以看出在河牌上这两手牌分布类似,但是对于下一轮转牌的分布就明显不同,所以要考虑下一步发牌后的分布,来进行分类。
代码
普通的emd算法计算速度,并不适用于德扑的分类,如python里面的pyemd包,平均计算一次要在ms的级别,文章介绍了一种emd的算法,适用于德扑的emd计算,因为德扑的分布矩阵大都是稀疏矩阵,有一定特殊性,可以提高算法的运行速度,我初步试了下分成100种的情况下,可以提高计算速度6倍,除了算法的优化,后期还可以multiprocessing并行的方法提高计算速度。
这个算法在计算emd之前多了一步,就是
给定一组质心mean的分布meanarr,那么的的分布的每个下标i就是对应的下一回合对应的类别,meanarr[i]就是分布到下一回合i的概率,因为meanarr很稀疏,他有很多的0,预先计算sortedDistances,sortedDistances[i][j]就是与分布到下一回合的i集合第j相邻的meanarr非零的集合的距离 orderedClusters[i][j] 就是i集合第j相邻的meanarr非零的集合的index。计算sortedDistances的代码的函数calOrderedCluster如下:
meanarr=[0,0,5,0,3,0,0,0]
pointarr=[2,0,2,0,3,0,1,0]
def calOrderedCluster(meanarr,pointarr):
arrQ=[index for index,i in enumerate(meanarr) if i!=0]
distant=np.zeros( (len(pointarr),len(arrQ)) )
martix=np.zeros( (len(pointarr),len(arrQ)),dtype=np.int32 )
for idealCluster in range(len(pointarr)):
arrtemp=[i-idealCluster for i in arrQ]
distancearr=list(map(lambda x:abs(x),arrtemp))
sortedDistances[idealCluster,:]=np.sort(distancearr)
s= np.argsort(distancearr)
orderedClusters[idealCluster,:]=[arrQ[i] for i in s ]
return orderedClusters,sortedDistances
计算emd的函数代码如下:
def calEmd(meanarr,pointarr,orderedClusters,sortedDistances):
targets=pointarr.copy()
meanRemaining=meanarr.copy()
done=[False]*len(pointarr)
totCost=0
for i in range(orderedClusters.shape[1]):
for j in range(len(pointarr)):
if done[j] :
continue
pointCluster=j
meanCluster=orderedClusters[pointCluster][i]
amtRemaining=meanRemaining[meanCluster]
if amtRemaining == 0:
continue
d=sortedDistances[pointCluster][i]
if amtRemaining<targets[j]:
totCost += amtRemaining * d
targets[j] -= amtRemaining
meanRemaining[meanCluster]=0
else:
totCost += targets[j] * d
targets[j]=0
meanRemaining[meanCluster] -= targets[j]
done[j]=True
return totCost