一、项目概述

本项目旨在使用飞桨框架 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):
# 五子棋逻辑和棋盘UI的参数
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 # 基于KL自适应地调整学习率
self.temp = 1.0 # 临时变量
self.n_playout = 400 # 每次移动的模拟次数
self.c_puct = 5
self.buffer_size = 10000 # 经验池大小 10000
self.batch_size = 512 # 训练的mini-batch大小 512
self.data_buffer = deque(maxlen=self.buffer_size)
self.play_batch_size = 1
self.epochs = 5 # 每次更新的train_steps数量
self.kl_targ = 0.02
self.check_freq = 100 # 评估模型的频率,可以设置大一些比如500
self.game_batch_num = 1500
self.best_win_ratio = 0.0
# 用于纯粹的mcts的模拟数量,用作评估训练策略的对手
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: # early stopping if D_KL diverges badly
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 方法开始训练过程。