一、项目概述 本项目旨在使用飞桨框架 2.0 实现 AlphaZero 算法,构建一个能够玩五子棋的 AI 模型。通过纯粹的自我博弈方式进行训练,使 AI 在短时间内达到一定的棋力水平,能够与人类玩家进行有挑战性的对弈。
二、五子棋游戏简介 五子棋是一款经典的两人对弈棋类游戏,双方分别使用黑白棋子,在棋盘竖线与横线的交叉点上轮流落子,率先形成五子连线的一方获胜。其规则简单易懂,上手容易,适合各个年龄段的人群,具有很高的趣味性和竞技性。
三、本项目简介 本项目专注于运用 AlphaZero 算法来实现五子棋 AI。相较于复杂的围棋和象棋,五子棋的规则较为简洁,这使得我们能够将更多精力放在 AlphaZero 算法的训练和优化上。通过在一台普通 PC 机上进行几个小时的训练,即可获得一个具有一定实力的 AI 模型,在与人类玩家的对弈中展现出较强的竞争力。
四、为什么使用 MCTS(蒙特卡洛树搜索) 在传统的棋盘游戏决策过程中,玩家通常会思考多种走法及其可能的后续局面。类似 Minimax 这样的传统 AI 博弈树搜索算法,在做出决策前需要穷举所有可能的走法,这在面对复杂游戏时,其搜索空间会呈指数级增长,导致效率极其低下。例如,国际象棋的平均分支因子为 35,仅走两步就有 1,225(35²)种可能的棋面;围棋的平均分支因子更是高达 250,走两步就会产生 62,500(250²)种棋面。
而随着神经网络技术的发展,我们可以利用神经网络来指导搜索过程,筛选出更有价值的博弈路径进行探索,避免陷入大量无用的搜索分支中。蒙特卡洛树搜索(MCTS)算法在此背景下应运而生,它通过巧妙地结合神经网络的预测能力和树搜索的探索机制,有效地提高了搜索效率和决策质量。
五、训练算法流程 AlphaZero 算法的核心在于通过自我对弈不断收集数据,进而更新策略价值网络,而更新后的网络又会用于后续的自我对弈,形成一个相互促进、不断迭代的学习过程,从而实现 AI 棋力的稳步提升。
在本项目中,我们将训练流程封装在 TrainPipeline
类中,其中 run
方法是训练的主要执行逻辑。它会循环调用 collect_selfplay_data
方法来收集自我对弈产生的数据,当收集的数据量超过设定的 batch_size
时,就会调用 policy_update
方法对策略价值网络进行更新。在训练过程中,还可以根据实际需求调整各种超参数,如学习率、模拟次数、经验池大小等,以优化训练效果。
以下是 TrainPipeline
类的详细代码解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 class TrainPipeline (): def __init__ (self, init_model=None , is_shown=0 ): self .board_width = 9 self .board_height = 9 self .n_in_row = 5 self .board = Board(width=self .board_width, height=self .board_height, n_in_row=self .n_in_row) self .is_shown = is_shown self .game = Game_UI(self .board, is_shown) self .learn_rate = 2e-3 self .lr_multiplier = 1.0 self .temp = 1.0 self .n_playout = 400 self .c_puct = 5 self .buffer_size = 10000 self .batch_size = 512 self .data_buffer = deque(maxlen=self .buffer_size) self .play_batch_size = 1 self .epochs = 5 self .kl_targ = 0.02 self .check_freq = 100 self .game_batch_num = 1500 self .best_win_ratio = 0.0 self .pure_mcts_playout_num = 1000 if init_model: self .policy_value_net = PolicyValueNet(self .board_width, self .board_height, model_file=init_model) else : self .policy_value_net = PolicyValueNet(self .board_width, self .board_height) self .mcts_player = MCTSPlayer(self .policy_value_net.policy_value_fn, c_puct=self .c_puct, n_playout=self .n_playout, is_selfplay=1 )
在 __init__
方法中,首先初始化了与五子棋游戏逻辑和棋盘显示相关的参数,包括棋盘的宽度、高度、连成五子获胜的条件,以及创建了棋盘和游戏界面的实例。
接着定义了一系列训练参数,如学习率、学习率乘数(用于基于 KL 散度自适应调整学习率)、温度参数(用于控制探索程度)、每次移动的模拟次数、PUCT 算法中的 c_puct
参数、经验池大小、训练的小批次大小、训练轮数、KL 散度目标值、模型评估频率、总的游戏批次数量以及当前最佳胜率等。
根据是否提供初始模型文件路径,选择加载已有模型或创建新的策略价值网络实例,并创建基于该网络的 MCTS 玩家实例,用于自我对弈训练。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def get_equi_data (self, play_data ): """通过旋转和翻转来增加数据集 play_data: [(state, mcts_prob, winner_z),...,...] """ 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 .board_height, self .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
get_equi_data
方法用于数据增强,通过对原始的自我对弈数据进行旋转和翻转操作,扩充数据集的多样性,从而提高模型的泛化能力。
1 2 3 4 5 6 7 8 9 def collect_selfplay_data (self, n_games=1 ): """收集自我博弈数据进行训练""" 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)
collect_selfplay_data
方法负责收集自我对弈数据。在每次自我对弈中,通过 start_self_play
方法进行一局游戏,获取获胜者和游戏过程中的数据(包括状态、MCTS 概率和获胜者信息),然后对这些数据进行扩充,并添加到数据缓冲区 data_buffer
中,为后续的训练提供数据支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 def policy_update (self ): """更新策略价值网络""" mini_batch = random.sample(self .data_buffer, self .batch_size) state_batch = [data[0 ] for data in mini_batch] state_batch = np.array(state_batch).astype("float32" ) mcts_probs_batch = [data[1 ] for data in mini_batch] mcts_probs_batch = np.array(mcts_probs_batch).astype("float32" ) winner_batch = [data[2 ] for data in mini_batch] winner_batch = np.array(winner_batch).astype("float32" ) old_probs, old_v = self .policy_value_net.policy_value(state_batch) 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 : break if kl > self .kl_targ * 2 and self .lr_multiplier > 0.1 : self .lr_multiplier /= 1.5 elif kl < self .kl_targ / 2 and self .lr_multiplier < 10 : self .lr_multiplier *= 1.5 explained_var_old = (1 - np.var(np.array(winner_batch) - old_v.flatten()) / np.var(np.array(winner_batch))) explained_var_new = (1 - np.var(np.array(winner_batch) - new_v.flatten()) / np.var(np.array(winner_batch))) print (("kl:{:.5f}," "lr_multiplier:{:.3f}," "loss:{}," "entropy:{}," "explained_var_old:{:.3f}," "explained_var_new:{:.3f}" ).format (kl, self .lr_multiplier, loss, entropy, explained_var_old, explained_var_new)) return loss, entropy
policy_update
方法用于更新策略价值网络。首先从数据缓冲区中随机采样一个小批次的数据,包括状态、MCTS 概率和获胜者信息,并将其转换为适合网络输入的格式。然后通过多次调用 train_step
方法进行训练,计算价值损失和策略损失,并进行反向传播和参数优化。在训练过程中,还会计算新旧策略的 KL 散度,根据 KL 散度的值自适应地调整学习率乘数,同时计算并打印出 KL 散度、学习率乘数、损失、熵以及解释方差等训练信息,最后返回损失和熵的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def policy_evaluate (self, n_games=10 ): """ 通过与纯的MCTS算法对抗来评估训练的策略 注意:这仅用于监控训练进度 """ current_mcts_player = MCTSPlayer(self .policy_value_net.policy_value_fn, c_puct=self .c_puct, n_playout=self .n_playout) pure_mcts_player = MCTS_Pure(c_puct=5 , n_playout=self .pure_mcts_playout_num) win_cnt = defaultdict(int ) for i in range (n_games): winner = self .game.start_play(current_mcts_player, pure_mcts_player, start_player=i % 2 ) win_cnt[winner] += 1 win_ratio = 1.0 * (win_cnt[1 ] + 0.5 * win_cnt[-1 ]) / n_games print ("num_playouts:{}, win: {}, lose: {}, tie:{}" .format ( self .pure_mcts_playout_num, win_cnt[1 ], win_cnt[2 ], win_cnt[-1 ])) return win_ratio
policy_evaluate
方法用于评估训练的策略。通过创建基于当前策略价值网络的 MCTS 玩家和一个纯 MCTS 玩家进行对弈,统计在一定数量的游戏中双方的胜负情况,计算出当前策略的胜率,并打印相关信息,返回胜率值。该方法主要用于监控训练过程中策略的性能提升情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 def run (self ): """开始训练""" root = os.getcwd() dst_path = os.path.join(root, 'dist' ) if not os.path.exists(dst_path): os.makedirs(dst_path) try : 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() print ("loss :{}, entropy:{}" .format (loss, entropy)) if (i + 1 ) % 50 == 0 : self .policy_value_net.save_model(os.path.join(dst_path, 'current_policy_step.model' )) if (i + 1 ) % self .check_freq == 0 : print ("current self-play batch: {}" .format (i + 1 )) win_ratio = self .policy_evaluate() self .policy_value_net.save_model(os.path.join(dst_path, 'current_policy.model' )) if win_ratio > self .best_win_ratio: print ("New best policy!!!!!!!!" ) self .best_win_ratio = win_ratio self .policy_value_net.save_model(os.path.join(dst_path, 'best_policy.model' )) if (self .best_win_ratio == 1.0 and self .pure_mcts_playout_num < 8000 ): self .pure_mcts_playout_num += 1000 self .best_win_ratio = 0.0 except KeyboardInterrupt: print ('\n\rquit' )
run
方法是训练的主循环逻辑。首先创建保存模型的目录,如果不存在则自动创建。然后在循环中,不断地收集自我对弈数据,当数据量足够时进行策略更新,并定期保存当前的模型参数。每隔一定的批次数量,还会对当前模型进行性能评估,根据评估结果保存最佳模型参数,并根据胜率情况调整用于评估的纯 MCTS 玩家的模拟次数,以适应模型的不断进化。如果在训练过程中遇到键盘中断,则会打印退出信息并停止训练。
在 if __name__ == '__main__':
部分,首先获取当前的计算设备(CPU 或 GPU),并设置飞桨的计算设备。然后根据指定的模型路径(如果有)创建 TrainPipeline
实例,并调用 run
方法开始训练过程。