斗地主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% |
| 平均决策延迟 | 12ms | 23ms |
| 适应度 | 0.71 | 0.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:
- LSTM 对历史信息的编码效果好于 concat flatten
- 多人配合需要显式建模,不能只靠隐式学习
- 自对弈的 CPU-GPU 异步流水线对训练效率影响很大
作者团队:长沙赢麻哒文化传播 | malinguo.com