当时的AlphaGo大师版,在彻底击败柯洁九段后不久,就被后辈AlphaGo Zero(简称狗零)击败。
从一个完全不懂围棋的AI,到打败Master,狗零只用了21天。
而且不需要人类知识的喂养,成为顶尖棋手全靠自学。
如果能培养出这样的AI,就算不会下棋也可以骄傲。
于是,来自巴黎的少年迪伦Djian(简称小迪)就跟着苟零的论文实现了。
他将自己的AI棋手命名为SuperGo,还提供了代码(门户见文章底部)。
除此之外,还有教程 mdash mdash
一个身体两个头。
代理分为三个部分:
一个是特征提取器,一个是策略网络,第三个是价值网络。
所以狗零也被亲切地称为 双头怪物 。
特征提取器是身体,另外两个网络是大脑。
特征提取器
特征提取模型是残差网络(ResNet),在普通的CNN上增加了一个跳跃连接,使梯度传播更加平滑。
跳转的样子,用代码写就是:
1class BasicBlock(nn。模块):
2 quot""
具有2个卷积和一个跳跃连接的3个基本剩余块
4在最后一次ReLU激活之前。
5 quot""
六
7 def __init__(自身,平面内,平面,步幅=1,下采样=无):
8超(BasicBlock,self)。__init__()
九
10 self.conv1 = nn。Conv2d(inplanes,planes,kernel_size=3
11步幅=步幅,填充=1,偏移=假)
12 self.bn1 = nn。BatchNorm2d(平面)
13
14 self.conv2 = nn。Conv2d(planes,planes,kernel_size=3
15步幅=步幅,填充=1,偏移=假)
16 self.bn2 = nn。BatchNorm2d(平面)
17
18
19 def forward(自身,x):
20残差= x
21
22 out = self.conv1(x)
23 out = F.relu(self.bn1(out))
24
25 out = self.conv2(out)
26 out = self.bn2(out)
27
28输出+=剩余
29 out = F.relu(out)
30
31返回出去
然后,将其添加到特征提取模型中:
1级提取器(nn。模块):
2 def __init__(自身,内平面,外平面):
3超(提取器,自带)。__init__()
4 self.conv1 = nn。Conv2d(输入平面,输出平面,步幅=1,
5 kernel_size=3,填充=1,偏差=假)
6 self.bn1 = nn。BatchNorm2d(输出平面)
七
8表示范围内的块(块):
9 setattr(self, quotres { } quot。格式(块),\
10 BasicBlock(输出平面,输出平面))
11
12
13 def forward(自身,x):
14 x = f . relu(self . bn1(self . con v1(x)))
15代表范围内的块(块- 1):
16 x = getattr(self, quotres { } quot。格式(块))(x)
17
18 feature_maps = getattr(self, quotres { } quot。格式(块- 1))(x)
19返回特征_地图
战略网络
网络就是普通的CNN,有一个批量归一化,有一个输出概率分布的全连接层。
1类别政策网(nn。模块):
2 def __init__(自身,内平面,外平面):
3超(PolicyNet,self)。__init__()
4 self . output planes = output planes
5 self.conv = nn。Conv2d(inplanes,1,kernel_size=1)
6 self.bn = nn。BatchNorm2d(1)
7 self.logsoftmax = nn。LogSoftmax(dim=1)
8 self.fc = nn。线性(输出平面- 1,输出平面)
九
10
11定义向前(自身,x):
12 x = F.relu(self.bn(self.conv(x)))
13 x = x.view(-1,self.outplanes - 1)
14 x = self.fc(x)
15 probas = self.logsoftmax(x)。exp()
16
17返回probas
价值网络
这个网络稍微复杂一点。
除了标准,我们还需要添加另一个完整的连接层。
最后用双曲正切计算(-1,1)之间的值,以显示当前状态下赢面有多大。
代码看起来是这样的 mdash mdash
1类值网(nn。模块):
2 def __init__(自身,内平面,外平面):
3超(ValueNet,self)。__init__()
4 self . output planes = output planes
5 self.conv = nn。Conv2d(inplanes,1,kernel_size=1)
6 self.bn = nn。BatchNorm2d(1)
7 self.fc1 = nn。线性(输出平面- 1,256)
8 self.fc2 = nn。线性(256,1)
九
10
11定义向前(自身,x):
12 x = F.relu(self.bn(self.conv(x)))
13 x = x.view(-1,self.outplanes - 1)
14 x = F.relu(self.fc1(x))
15 winning = F.tanh(self.fc2(x))
16回赢
一棵树以备不时之需
狗,另一个重要组成部分,是蒙特卡罗树搜索(MCTS)。
可以让AI玩家提前发现胜率最高的落点。
模拟器里模拟对手的下一手和下一手,给出对策,所以提前的远不止一步。
节点(节点)
树中的每个节点代表一种具有不同统计数据的不同情况:
每个节点经过的次数n,总行动值w,经过这个点的先验概率p,平均行动值q (q=w/n),从其他地方到这个节点所走的步,以及从这个节点开始的所有可能的下一步。
1类节点:
2 def __init__(self,parent=None,proba=None,move=None):
3 self.p = proba
4 self.n = 0
5 self.w = 0
6 self.q = 0
7 self.children = []
8 self.parent =父母
9 self.move =移动
部署(推广)
第一步是PUCT(多项式上的信任树)算法,它选择可以最大化PUCT函数的一个变体的方式(如下)。
用代码编写 mdash mdash
1def select(节点,c_puct=C_PUCT):
2 quot基于PUCT公式 quot
三
4总计数= 0
对于范围内的I(nodes . shape[0]):
6 total_count += nodes[i][1]
七
8 action _ scores = NP . zeros(nodes . shape[0])
对于范围内的I(nodes . shape[0]):
10 action_scores[i] =节点[i][0] + c_puct *节点[i][2] * \
11(NP . sqrt(total _ count)/(1+nodes[I][1]))
12
13等于= NP . where(action _ scores = = NP . max(action _ scores))[0]
14 if equals.shape[0]>0:
15 return np.random.choice(等于)
16返回等于[0]
结束(结尾)
选择继续进行,直到到达尚未向下分支的叶节点。
1def is_leaf(self):
2 quot""检查节点是否是叶节点 quot""
三
4返回len(self.children) == 0
在叶节点处,将评估那里的随机状态,并且所有 下一步 的概率。
所有禁止落点的概率会变成零,然后总概率会再次归类为1。
然后这个叶节点会生出分支(都是可以掉的位置,概率不为零)。
代码如下 mdash mdash
1定义扩展(self,probas):
2 self . children =[Node(parent = self,move=idx,proba=probas[idx]) \
如果probas[idx] gt;则对于范围内的idx(probas . shape[0])为3;0]
更新它
在分支诞生后,这个叶节点及其母节点的统计数据将被更新,使用下面的两串代码。
1def更新(self,v):
2 quot""在卷展栏后更新节点统计""
三
4 self.w = self.w + v
5 self . q = self . w/self . n if self . n gt;0否则0
1当current_node.parent:
2当前节点更新(v)
3当前节点=当前节点.父节点
选择放置点
模拟器设置好了,一切可能的 下一步 ,有自己的统计。
根据这些数据,算法会选择其中一个步骤,在这个步骤中,它确实被留在了后面。
有两个选择。一种是选择模拟次数最多的点。
试试看测试和实战。
另一种是随机选择,它将节点被传递的次数转换成概率分布,使用下面的代码 mdash mdash
1 total = np.sum(action_scores)
2 probas =行动得分/总数
3 move = NP . random . choice(action _ scores . shape[0],p=probas)
后者适合训练,让AlphaGo探索更多可能的选项。
三位一体培养
养狗分三个流程,异步进行。
一个是自玩,用来生成数据。
1def self_play():
2虽然正确:
3新玩家,检查点=加载玩家()
4如果是新玩家:
5名玩家=新玩家
六
7 ##创建进程的自玩匹配队列
8结果= create_matches(玩家,核心=PARALLEL_SELF_PLAY,
9 match_number=SELF_PLAY_MATCH
10 for _ in范围(SELF_PLAY_MATCH):
11 result = results.get()
12 db.insert({
13 quot游戏 quot:结果,
14 quotid quot:游戏id
15 })
16 game_id += 1
第二种是训练,利用新产生的数据来改进当前的神经网络。
1定义训练():
2标准= AlphaLoss()
3 dataset = SelfPlayDataset()
4播放器,检查点= load_player(当前时间,已加载版本)
5优化器= create_optimizer(player,lr,
6 param =检查点[ # 39;优化器 # 39;])
7 best_player = deepcopy(player)
8 dataloader = DataLoader(数据集,collate_fn=collate_fn,\
9 batch_size=BATCH_SIZE,shuffle=True
10
11虽然正确:
12对于枚举(数据加载器)中的batch_idx,(state,move,winner):
13
14 ##评估当前网络的副本
15如果total_ite % TRAIN_STEPS == 0:
16 pending _ player = deep copy(player)
17结果=评估(待定_玩家,最佳_玩家)
18
19如果结果:
20最佳玩家=待定玩家
21
22示例= {
23 #39;国家 # 39;:状态,
24 #39;赢家 # 39;:赢家,
25 #39;动 # 39;:移动
26 }
27 optimizer.zero_grad()
28 winner,probas = pending_player.predict(示例[ # 39;国家 # 39;])
29
30损失=标准(赢家,示例[ # 39;赢家 # 39;], \
31 probas,示例[ # 39;动 # 39;])
32 loss.backward()
33 optimizer.step()
34
35 ##获取新游戏
36如果total_ite % REFRESH_TICK == 0:
37 last_id = fetch_new_games(集合,数据集,last_id)
训练的损失函数表示如下:
1类AlphaLoss(torch.nn.Module):
2 def __init__(self):
3超(AlphaLoss,self)。__init__()
四
5 def forward(self,pred_winner,winner,pred_probas,probas):
6 value _ error =(winner-pred _ winner)* * 2
7 policy _ error = torch . sum((-probas *
8 (1e-6 + pred_probas)。log()),1)
9 total _ error =(value _ error . view(-1)+policy _ error)。平均值()
10返回总计_错误
第三个是评估,看经过训练的代理是否比正在生成数据的代理更好(最好的一个回到第一步继续生成数据)。
1def evaluate(玩家,新玩家):
2结果=游戏(玩家,对手=新玩家)
3黑_胜= 0
4白色胜= 0
五
6对于结果中的结果:
7如果结果[0] == 1:
8白胜+= 1
9 elif结果[0] == 0:
10黑胜+= 1
11
12 ##检查受训球员(黑色)是否优于
13 ##当前最佳玩家取决于阈值
14 if black _ wins gt= EVAL _阈值*长度(结果):
15返回True
16返回假
第三部分非常重要。只有不断选择最好的网络,不断产生高质量的数据,才能提高AI的棋艺。
只有重复三个环节,才能培养出强大的棋手。
对AI Go感兴趣的也可以试试这个PyTorch实现。
最初取自量子位,原迪伦Djian。
代码实施门户:
网页链接
教程门户:
网页链接
AlphaZero纸质门户网站:
网页链接