斗地主AI实战:基于 LSTM+DMC 的深度强化学习方案与 DouZero 对比

0 阅读4分钟

斗地主AI实战:基于 LSTM+DMC 的深度强化学习方案与 DouZero 对比

斗地主是强化学习领域的经典 benchmark。DouZero(ICML 2021)证明了 DMC(Deep Monte-Carlo)在斗地主上的有效性。我们在 DouZero 的基础上做了进一步改进,引入 LSTM 编码历史动作,将适应度从 DouZero 的基线水平提升到 0.765,并集成到了线上 API 服务中。

本文分享改进思路、训练细节和工程化踩坑。


一、斗地主的AI难点

维度挑战
不完全信息看不到其他两人手牌和底牌
角色不对称地主 vs 两个农民,农民需配合
动作空间DouZero 定义了 309 种合法动作
炸弹博弈何时出炸弹是关键决策点

二、相比 DouZero 的改进

2.1 引入 LSTM 编码历史

DouZero 原版用的是将历史动作拼接成固定长度特征向量,信息压缩损失较大。我们用 LSTM 替换:

class ImprovedDoudizhuModel(nn.Module):
    def __init__(self):
        super().__init__()
        # DouZero 原版:手牌 + 历史 concat → MLP
        # 我们的改进:手牌 MLP + 历史 LSTM → concat → MLP
        
        self.hand_net = nn.Sequential(
            nn.Linear(162, 256),  # 54张牌 × 3种编码
            nn.ReLU(),
            nn.Linear(256, 128)
        )
        
        # 关键改进:LSTM 编码出牌历史
        self.history_lstm = nn.LSTM(
            input_size=54,     # 每步出牌的 one-hot
            hidden_size=128,
            num_layers=2,
            batch_first=True
        )
        
        self.policy = nn.Sequential(
            nn.Linear(256, 512),
            nn.ReLU(),
            nn.Linear(512, 309)  # 309种动作
        )
    
    def forward(self, hand_features, action_sequence):
        hand_feat = self.hand_net(hand_features)
        _, (h_n, _) = self.history_lstm(action_sequence)
        hist_feat = h_n[-1]
        combined = torch.cat([hand_feat, hist_feat], dim=-1)
        return self.policy(combined)

2.2 农民配合建模

DouZero 的三个角色(地主、地主上家、地主下家)用独立模型训练。我们给两个农民增加了队友行为编码:

# 农民模型额外输入:队友的近期出牌模式
teammate_recent = encode_recent_actions(teammate_history, window=5)
combined = torch.cat([hand_feat, hist_feat, teammate_recent], dim=-1)

这让两个农民学会了基本配合:

  • 上家出小牌"让路"给下家出炸弹
  • 下家在地主即将出完时优先拦截

三、训练配置

# 训练超参数
num_episodes: 30_000_000     # 三千万局
batch_size: 256
learning_rate: 0.0001
optimizer: Adam
hardware: V100 GPU × 1
training_time: ~18 days

# 自对弈配置
self_play_workers: 16        # 16个CPU进程并行生成对局
gpu_train_interval: 1000     # 每1000局更新一次模型
model_sync_interval: 5000    # 每5000局同步模型到self_play进程

适应度曲线

Episode 0:          fitness=0.31
Episode 5M:         fitness=0.52
Episode 10M:        fitness=0.63
Episode 20M:        fitness=0.72
Episode 30M:        fitness=0.765  ← 当前线上版本

适应度 0.765 意味着 AI 在随机对手池中的综合表现超过 76.5% 的基线策略。


四、与 DouZero 的对比实验

我们跑了 10 万局对比:

指标DouZero baseline我们的方案
地主胜率63.2%66.8%
农民配合成功率57.1%64.3%
平均决策延迟12ms23ms
适应度0.710.765

LSTM 带来了 ~20ms 的额外延迟,但仍远低于 300ms 的实时要求。农民配合建模带来的提升最明显。


五、工程化踩坑

坑1:自对弈的 CPU-GPU 流水线

自对弈阶段是 CPU 密集(模拟对局逻辑),训练阶段是 GPU 密集。早期两者串行跑,GPU 利用率只有 35%。

解决方案:用 multiprocessing + Queue 做异步流水线。

# 简化版异步训练架构
from multiprocessing import Process, Queue

def self_play_worker(model_weights, data_queue, num_games=1000):
    """CPU进程:生成对局数据"""
    model = load_model(model_weights)
    for _ in range(num_games):
        trajectory = play_one_game(model)
        data_queue.put(trajectory)

def gpu_trainer(data_queue, model):
    """GPU进程:消费数据并训练"""
    while True:
        batch = collect_batch(data_queue, batch_size=256)
        loss = train_step(model, batch)
        # 定期把新权重同步给 self_play workers

坑2:出牌合法性校验

模型偶尔会输出非法动作(比如手里没有的牌)。线上必须加一层硬约束:

legal_mask = get_legal_action_mask(hand, last_play)
logits[~legal_mask] = -float('inf')
action = sample(softmax(logits))

坑3:地主底牌信息泄露

早期版本中,两个农民的模型在编码时不小心把底牌信息也输入了(通过全局状态),导致农民"知道"地主有什么牌。修复后农民胜率下降了 5%,但这才是真实水平。


六、小结

在 DouZero 的 DMC 基础上加入 LSTM 历史编码和农民配合建模,适应度提升约 5.5 个百分点。系统已上线 API 服务,推理延迟 <300ms。

如果你也在做斗地主或类似的不完全信息博弈AI,几个 takeaway:

  1. LSTM 对历史信息的编码效果好于 concat flatten
  2. 多人配合需要显式建模,不能只靠隐式学习
  3. 自对弈的 CPU-GPU 异步流水线对训练效率影响很大

作者团队:长沙赢麻哒文化传播 | malinguo.com