1. 从零理解AlphaZero的核心思想

第一次听说AlphaZero是在2017年,当时它仅用8小时训练就击败了国际象棋世界冠军程序Stockfish。作为一个棋类爱好者兼程序员,我立刻被这个"从零开始"的学习方式吸引了。后来我花了三个月时间,用Python复现了一个简化版的AlphaZero五子棋AI,今天就把这个实战经验完整分享给大家。

AlphaZero的核心思想可以用三个关键词概括:自对弈蒙特卡洛树搜索(MCTS)神经网络。想象一下教小朋友下棋的过程:首先他会胡乱下几盘(自对弈),然后慢慢记住哪些走法容易赢(神经网络),同时在下棋时会"脑补"几步之后的局面(MCTS)。AlphaZero的学习过程惊人地相似,只是用数学方法实现了这个过程。

与传统棋类AI最大的不同是,AlphaZero不需要任何人类棋谱。我刚开始也很怀疑:没有人类经验真能学会下棋吗?实测发现,通过设计合理的奖励机制(比如五子连珠得1分,被对手连珠得-1分),AI完全能自己发现经典开局策略。这就像给AI一本空棋谱,让它自己记录胜负经验。

2. 搭建五子棋基础环境

2.1 棋盘表示与规则实现

我们先从最基础的15×15棋盘开始。在Python中,我用numpy数组表示棋盘状态:

import numpy as np

class Board:
    def __init__(self):
        self.size = 15
        self.board = np.zeros((self.size, self.size))  # 0空 1黑棋 2白棋
        self.current_player = 1  # 黑棋先行
        
    def is_valid_move(self, x, y):
        return 0 <= x < self.size and 0 <= y < self.size and self.board[x,y] == 0
        
    def place_stone(self, x, y):
        if self.is_valid_move(x, y):
            self.board[x,y] = self.current_player
            self.current_player = 3 - self.current_player  # 切换玩家
            return True
        return False

判断胜负的算法需要检查四个方向(水平、垂直、两个对角线)。这里有个优化技巧:只需要检查最后落子位置的周边区域,不需要全盘扫描:

def check_win(self, x, y):
    directions = [(1,0), (0,1), (1,1), (1,-1)]  # 四个检查方向
    player = self.board[x,y]
    
    for dx, dy in directions:
        count = 1
        # 正向检查
        nx, ny = x + dx, y + dy
        while 0 <= nx < self.size and 0 <= ny < self.size and self.board[nx,ny] == player:
            count += 1
            nx += dx
            ny += dy
        # 反向检查
        nx, ny = x - dx, y - dy
        while 0 <= nx < self.size and 0 <= ny < self.size and self.board[nx,ny] == player:
            count += 1
            nx -= dx
            ny -= dy
        if count >= 5:
            return True
    return False

2.2 强化学习环境设计

强化学习需要定义三个核心要素:状态(State)、动作(Action)和奖励(Reward)。在五子棋中:

  • 状态:当前棋盘布局+当前执子方
  • 动作:在合法位置落子(15×15=225种可能)
  • 奖励:胜利+1,失败-1,平局0,未结束游戏0

我设计了一个Env类来管理游戏进程:

class Env:
    def __init__(self):
        self.board = Board()
        self.done = False
        self.winner = None
        
    def step(self, action):
        x, y = action // 15, action % 15  # 将动作编号转换为坐标
        if not self.board.place_stone(x, y):
            return self.board.board, -10, True, {}  # 非法走子惩罚
            
        if self.board.check_win(x, y):
            self.done = True
            self.winner = self.board.board[x,y]
            return self.board.board, 1, True, {}
            
        return self.board.board, 0, False, {}

3. 蒙特卡洛树搜索(MCTS)实现

3.1 节点与搜索树结构

MCTS的核心是不断扩展搜索树。每个节点需要存储以下信息:

class Node:
    def __init__(self, parent=None, action=None):
        self.parent = parent  # 父节点
        self.action = action  # 导致本节点的动作
        self.children = []  # 子节点列表
        self.visit_count = 0  # 访问次数
        self.total_value = 0  # 累计价值
        self.prior = 0  # 先验概率(来自神经网络)

搜索过程分为四个阶段,我把它比喻成城市规划:

  1. 选择(Selection):从根节点出发,选择最有"潜力"的子节点,就像规划师选择最有发展前景的区域
  2. 扩展(Expansion):当遇到未探索的节点时,扩展新节点,就像开发新城区
  3. 模拟(Simulation):用神经网络评估新节点价值,就像请专家评估地块价值
  4. 回溯(Backpropagation):将评估结果反向传播到路径上的所有节点,就像根据销售数据调整整个区域的估值

3.2 UCB算法实现

选择节点时使用UCB公式平衡探索与利用:

def ucb_score(node, parent_visit_count, c_puct=1.0):
    # Q值:节点平均价值
    q_value = node.total_value / (node.visit_count + 1e-6)
    # U项:探索项
    u_value = c_puct * node.prior * np.sqrt(parent_visit_count) / (node.visit_count + 1)
    return q_value + u_value

def select_child(node):
    # 选择UCB分数最高的子节点
    scores = [ucb_score(child, node.visit_count) for child in node.children]
    return node.children[np.argmax(scores)]

在实际项目中,我发现c_puct参数对性能影响很大。经过测试,设置在1.0-1.5之间效果最好。太小时AI过于保守,太大则容易冒险。

4. 神经网络设计与训练

4.1 网络架构

使用PyTorch实现的双头神经网络:

import torch
import torch.nn as nn
import torch.nn.functional as F

class AlphaZeroNet(nn.Module):
    def __init__(self, board_size=15):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 32, 3, padding=1)  # 输入通道3:当前玩家棋子,对手棋子,当前执子方
        self.conv2 = nn.Conv2d(32, 64, 3, padding=1)
        self.conv3 = nn.Conv2d(64, 128, 3, padding=1)
        
        # 策略头
        self.policy_conv = nn.Conv2d(128, 2, 1)
        self.policy_fc = nn.Linear(2*board_size*board_size, board_size*board_size)
        
        # 价值头
        self.value_conv = nn.Conv2d(128, 1, 1)
        self.value_fc1 = nn.Linear(board_size*board_size, 64)
        self.value_fc2 = nn.Linear(64, 1)

    def forward(self, x):
        # 共享特征提取
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = F.relu(self.conv3(x))
        
        # 策略头
        p = F.relu(self.policy_conv(x))
        p = p.view(p.size(0), -1)
        p = F.log_softmax(self.policy_fc(p), dim=1)
        
        # 价值头
        v = F.relu(self.value_conv(x))
        v = v.view(v.size(0), -1)
        v = F.relu(self.value_fc1(v))
        v = torch.tanh(self.value_fc2(v))  # 输出[-1,1]之间的估值
        
        return p, v

4.2 训练技巧

训练数据来自AI的自对弈记录。我总结了几个关键点:

  1. 数据增强:通过旋转和镜像增加数据多样性
def augment_data(state, policy):
    # 随机选择0-3次90度旋转
    k = np.random.randint(0, 4)
    state = np.rot90(state, k, axes=(1,2))
    policy = np.rot90(policy.reshape(15,15), k).flatten()
    
    # 50%概率水平翻转
    if np.random.random() > 0.5:
        state = np.flip(state, 2)
        policy = np.flip(policy.reshape(15,15), 1).flatten()
    
    return state, policy
  1. 学习率调整:使用余弦退火策略
optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=100)
  1. 正则化:添加L2正则和策略熵奖励
def loss_fn(policy_logits, value, target_policy, target_value):
    # 策略损失
    policy_loss = -torch.mean(torch.sum(target_policy * policy_logits, dim=1))
    
    # 价值损失
    value_loss = F.mse_loss(value.squeeze(), target_value)
    
    # 熵奖励(鼓励探索)
    entropy = -torch.mean(torch.sum(torch.exp(policy_logits) * policy_logits, dim=1))
    
    return policy_loss + value_loss - 0.01*entropy

5. 完整训练流程与对弈测试

5.1 自对弈数据生成

完整的训练循环如下:

def self_play(model, num_games=100):
    data = []
    for _ in range(num_games):
        env = Env()
        game_data = []
        state = env.board.board
        
        while not env.done:
            # 运行MCTS获取走子概率
            root = run_mcts(model, state, env.board.current_player)
            policy = get_policy(root)  # 根据访问次数计算概率
            
            # 保存训练数据
            game_data.append((state, env.board.current_player, policy))
            
            # 选择动作(添加探索噪声)
            action = select_action(root, temperature=1.0)
            state, reward, done, _ = env.step(action)
        
        # 为每一步数据添加最终奖励
        for i, (s, player, p) in enumerate(game_data):
            z = 0
            if env.winner is not None:
                z = 1 if env.winner == player else -1
            data.append((s, player, p, z))
    
    return data

5.2 人机对弈测试

实现一个简单的命令行对战界面:

def human_vs_ai(model):
    env = Env()
    while not env.done:
        print_board(env.board.board)
        if env.board.current_player == 1:  # 人类玩家
            x, y = map(int, input("输入你的走子坐标(x y): ").split())
            action = x * 15 + y
        else:  # AI玩家
            print("AI思考中...")
            root = run_mcts(model, env.board.board, 2)
            action = select_action(root, temperature=0.1)
        
        _, _, done, _ = env.step(action)
    
    print_board(env.board.board)
    if env.winner == 1:
        print("你赢了!")
    elif env.winner == 2:
        print("AI赢了!")
    else:
        print("平局!")

在训练过程中,我发现几个常见问题及解决方案:

  1. AI过于保守:增加UCB公式中的c_puct参数,鼓励更多探索
  2. 训练初期走子随机:适当降低初始温度参数(temperature)
  3. 过拟合:增加数据增强和L2正则化强度
  4. 训练速度慢:使用GPU加速,或减少MCTS的模拟次数

经过约10万局自对弈训练后,我的五子棋AI已经能达到业余高段水平。最有趣的是,AI自己发现了一些人类棋谱中常见的开局策略,比如"花月"、"浦月"等经典开局,完全是从零自学的。

更多推荐