AlphaZero巧妙了使用MCTS搜索树和神经网络一起,通过MCTS搜索树优化神经网络参数,反过来又通过优化的神经网络指导MCTS搜索。两者一主一辅,非常优雅的解决了这类状态完全可见,信息充分的棋类问题。前面结合一个五子棋AI的案例代码实现了蒙特卡洛树搜索,还使用Tensorflow2实现神经网络的部分。并通过一个人机交互案例把AlphaZero的算法实现了,现在只剩最关键的模型训练部分了。现在我们来完成自对弈训练的部分。
AI人工智能(调包侠)速成之路九(AlphaZero代码实战1:强化学习介绍)
AI人工智能(调包侠)速成之路十(AlphaZero代码实战2:蒙特卡洛树搜索)
AI人工智能(调包侠)速成之路十一(AlphaZero代码实战3:神经网络实现)
AI人工智能(调包侠)速成之路十二(AlphaZero代码实战4:人机对战实现))
自我对弈
把上篇“人机对战实现”代码中人类玩家的部分去掉,甚至不需要窗口的输入界面,直接上AI玩家循环起来就可以实现自对弈(左右互搏)。AI玩家对象mcts_player负责根据输入局面信息配合神经网络对象给出的估值结合蒙特卡洛树搜索给出应对方法。AI可以不断重复的自己跟自己对弈。
def start_self_play(self, player, is_shown=False, temp=1e-3):
"""
自我对局,返回对局数据(state, mcts_probs, z),用来训练。
:param player: MCTSPlayer
:param is_shown: 是否显示棋盘
:param temp: 论文里面的温度值
:return: 赢了的玩家编号和对局数据(state, mcts_probs, z)列表
"""
self.board.init_board()
p1, p2 = self.board.players
states, mcts_probs, current_players = [], [], []
while True:
move, move_probs = player.get_action(self.board, temp=temp, return_prob=True)
states.append(self.board.current_state()) # 保存棋局状态
mcts_probs.append(move_probs) # 保存pi值
current_players.append(self.board.current_player)
self.board.do_move(move)
if is_shown:
self.graphic(p1, p2)
end, winner = self.board.game_end()
if end:
winners_z = np.zeros(len(current_players))
if winner != -1:
winners_z[np.array(current_players) == winner] = 1.0
winners_z[np.array(current_players) != winner] = -1.0
player.reset_player()
if is_shown:
if winner != -1:
print("Game end. Winner is player:", winner)
else:
print("Game end. Tie")
return winner, zip(states, mcts_probs, winners_z)
数据增强
上面自对弈的部分是非常消耗资源的(计算资源和时间成本),所以有必要使用数据增强来快速获得更多的对弈数据。在图片识别的应用里面数据增强也很常见,把输入图片做旋转,拉伸,裁剪,拼接等等操作把输入数据成倍的增加,让神经网络看到更多样的情况,识别效率也会得到很大提高。自对弈得到的数据是一串对弈局面从开始到结束的集合,可以把不同的棋子排列看成是不同图案,由于棋盘的对称性,可以转换角度或者反转图案来把转换后的局面收集起来,相当于不同角度看下同一盘棋结果肯定是一样的。这样相当于得到了更多自对弈的数据,大大节省了资源消耗。
def get_equi_data(self, play_data):
extend_data = []
for state, mcts_porb, winner in play_data:
for i in [1, 2, 3, 4]:
# 逆时针旋转
equi_state = np.array([np.rot90(s, i) for s in state])
equi_mcts_prob = np.rot90(np.flipud(mcts_porb.reshape(self.game.board.width, self.game.board.width)), i)
extend_data.append((equi_state, np.flipud(equi_mcts_prob).flatten(), winner))
# 水平翻转
equi_state = np.array([np.fliplr(s) for s in equi_state])
equi_mcts_prob = np.fliplr(equi_mcts_prob)
extend_data.append((equi_state, np.flipud(equi_mcts_prob).flatten(), winner))
return extend_data
def collect_selfplay_data(self, n_games=1):
"""
通过自我对局,为训练收集游戏数据。
:param n_games: 玩游戏的局数
"""
self.mcts_player = None
self.mcts_player = MCTSPlayer(self.policy_value_net.policy_value_fn, c_puct=self.c_puct, n_playout=self.n_playout, is_selfplay=True)
for i in range(n_games):
winner, play_data = self.game.start_self_play(self.mcts_player, temp=self.temp)
play_data = list(play_data)[:]
self.episode_len = len(play_data)
play_data = self.get_equi_data(play_data)
self.data_buffer.extend(play_data)
训练网络
def policy_update(self):
"""
根据在自我对局中收集的游戏数据,训练神经网络。
:return: 返回损失函数值和熵值
"""
mini_batch = random.sample(self.data_buffer, self.batch_size)
state_batch = [data[0] for data in mini_batch]
mcts_probs_batch = [data[1] for data in mini_batch]
winner_batch = [data[2] for data in mini_batch]
old_probs, old_v = self.policy_value_net.policy_value(state_batch)
winner_batch = np.expand_dims(winner_batch, 1)
state_batch = np.array(state_batch)
state_batch = state_batch.astype(np.float32)
if len(state_batch.shape) == 3:
sp = state_batch.shape
state_batch = np.reshape(state_batch, [1, sp[0], sp[1], sp[2]])
for i in range(self.epochs):
loss, entropy = self.policy_value_net.train_step(state_batch, mcts_probs_batch, winner_batch, self.learn_rate*self.lr_multiplier)
new_probs, new_v = self.policy_value_net.policy_value(state_batch)
kl = np.mean(np.sum(old_probs * (np.log(old_probs + 1e-10) - np.log(new_probs + 1e-10)), axis=1))
if kl > self.kl_targ * 4: # D_KL偏离太远
break
# 调整学习速率
if kl > self.kl_targ * 2 and self.lr_multiplier > 0.01:
self.lr_multiplier /= 1.5
elif kl < self.kl_targ / 2 and self.lr_multiplier < 100:
self.lr_multiplier *= 1.5
explained_var_old = (1 - np.var(np.array(winner_batch) - tf.squeeze(old_v)) / np.var(np.array(winner_batch)))
explained_var_new = (1 - np.var(np.array(winner_batch) - tf.squeeze(new_v)) / np.var(np.array(winner_batch)))
print("loss={}, accuracy={}, kl={:.5f}, lr_multiplier={:.3f}, explained_var_old={:.3f}, explained_var_new={:.3f}".format
(loss, entropy, kl, self.lr_multiplier, explained_var_old, explained_var_new))
return loss, entropy
自对弈训练
def run(self):
try:
start_time = time.time()
for i in range(self.game_batch_num):
self.collect_selfplay_data(self.play_batch_size)
print("batch_i={}, episode_len={}".format(i+1, self.episode_len))
if len(self.data_buffer) > self.batch_size:
loss, entropy = self.policy_update()
if (i+1) % self.check_freq == 0:
print("save model ")
self.policy_value_net.save_model(i)
except KeyboardInterrupt:
print('\n\rquit')
训练网络的步骤基本上都套路化了,但是决定成败和学习快慢的因素非常多,相关的技巧与理论指导也在不断发展,从入门到放弃也都发生在这个阶段。这个阶段也最能体现开发人员的基本素养和能力(概念清晰,灵活多变,善于尝试)。就像没有学不好的学生只有教不好的老师一样,每个神经网络模型就像是一个幼稚的学生,我们知道他/她/它一定可以成为“天才”,超越一切有机生物。但是我们是那个可以把他/她/它培养出来的好老师吗?
具体看下我们这个主体是7层残差网络的模型,需要训练的参数量是637485个。训练神经网络模型的本质就是按照一定的方法不断调整这637485个参数值,最后让这些参数合作起来输出的结果接近或者超越人类。人脑大约有120亿个脑细胞,最多不到10%是充分发展了的并常加以运用的,其余的仍处在未充分发展或完全没有发展的原始状态。对比起来看可以得出两个结论:1,以现在的网络规模来对抗人脑基本上就是不能完成的任务,所以很多模型训练失败是常态。2,人一生中浪费最大的资源或许就是我们的脑细胞吧......。
AlphaZero代码实战系列 源代码打包
下载地址:https://download.csdn.net/download/askmeaskyou/12931806