深度强化学习训练掼蛋AI:LSTM+DMC 架构实战与踩坑

0 阅读5分钟

深度强化学习训练掼蛋AI:LSTM+DMC 架构实战与踩坑

掼蛋是近两年国内增长最快的牌类游戏,2024 年已被列为全国智力运动项目。但从AI角度看,掼蛋的决策难度远超大多数人的想象——它同时具备不完全信息、二人配合、复杂牌型、动态升级四大挑战。

本文分享我们用 LSTM + DMC 训练掼蛋AI的技术细节,包括难点分析、架构设计、训练过程和踩过的坑。


一、掼蛋AI的四大技术难点

难点具体问题对训练的影响
不完全信息看不到队友和对手的手牌需要从历史动作推断手牌分布
2v2 配合必须和队友形成默契不能只优化自己,要建模队友行为
牌型复杂炸弹、同花顺、三连对、钢板等动作空间巨大(~364种合法动作)
升级机制从2打到A,当前等级影响策略状态空间增加一个维度

对比斗地主(DouZero 定义了 309 种动作),掼蛋的动作空间更大、配合需求更高。


二、架构设计

class GuandanAI(nn.Module):
    """掼蛋AI决策模型"""
    def __init__(self):
        super().__init__()
        # 手牌编码:54张牌 × 4花色 one-hot
        self.hand_encoder = nn.Sequential(
            nn.Linear(216, 256),
            nn.ReLU(),
            nn.Linear(256, 128)
        )
        
        # 历史动作编码:LSTM 处理变长序列
        self.action_lstm = nn.LSTM(
            input_size=128,   # 每步动作的编码维度
            hidden_size=256,
            num_layers=2,
            batch_first=True
        )
        
        # 配合信号编码:队友近期动作的特征
        self.teammate_encoder = nn.Linear(128, 64)
        
        # 决策头:输出所有合法动作的概率
        self.policy_head = nn.Sequential(
            nn.Linear(256 + 128 + 64, 512),
            nn.ReLU(),
            nn.Linear(512, 364)  # 掼蛋动作空间
        )
    
    def forward(self, hand, action_history, teammate_actions):
        hand_feat = self.hand_encoder(hand)
        _, (h_n, _) = self.action_lstm(action_history)
        memory = h_n[-1]  # 取最后一层的隐状态
        team_feat = self.teammate_encoder(teammate_actions)
        
        combined = torch.cat([memory, hand_feat, team_feat], dim=-1)
        logits = self.policy_head(combined)
        return logits

关键设计决策

1) 为什么单独编码队友行为?

掼蛋是 2v2,队友的出牌隐含了大量信号(比如队友出了一对 K,可能暗示手中有炸弹需要你配合接风)。把队友动作单独编码后拼接,比混在一般历史序列里效果好约 5%。

2) 动作空间裁剪

掼蛋理论上的牌型组合极多,但大部分在特定局面下不合法。我们在模型前加了一个规则引擎过滤器

def get_legal_actions(hand, last_play, current_rank):
    """
    根据手牌、上家出牌和当前等级,
    枚举所有合法出牌组合
    """
    legal = []
    if last_play is None:
        # 自由出牌:枚举所有牌型
        legal = enumerate_all_plays(hand, current_rank)
    else:
        # 必须打过上家:枚举所有能压过的牌型
        legal = enumerate_beats(hand, last_play, current_rank)
    legal.append('pass')  # 永远可以选择不出
    return legal

# 模型只在合法动作上做 softmax
logits = model(hand, history, teammate)
mask = build_mask(legal_actions, action_space=364)
logits[~mask] = -float('inf')
probs = F.softmax(logits, dim=-1)

三、DMC 自对弈训练流程

初始化:随机策略模型 M_0

for episode in range(30_000_000):  # 三千万局
    # 1. 四个AI(2v2)用当前模型自对弈
    trajectory = self_play_guandan(M_current, num_players=4)
    
    # 2. 根据胜负标注每步价值
    #    赢的一方所有步骤 reward=+1
    #    输的一方所有步骤 reward=-1
    rewards = label_rewards(trajectory)
    
    # 3. 策略梯度更新
    for step in trajectory:
        log_p = log_prob(M_current(step.state), step.action)
        loss = -step.reward * log_p
        loss.backward()
    
    optimizer.step()
    
    # 4. 每 10000 局评估一次
    if episode % 10000 == 0:
        fitness = evaluate(M_current, baseline='greedy')
        log(f"Episode {episode}: fitness={fitness:.3f}")

训练在 V100 GPU 上跑了约 3 周,期间适应度曲线:

Episode 0:          fitness=0.35  (接近随机)
Episode 1,000,000:  fitness=0.48
Episode 5,000,000:  fitness=0.58
Episode 15,000,000: fitness=0.65
Episode 30,000,000: fitness=0.72  (当前水平)

四、踩坑记录

坑1:配合策略难收敛

最初没有单独编码队友行为,AI学出来的策略是"纯个人最优"——经常抢队友的牌权。加入 teammate_encoder 后,AI 开始学会"让牌""配合接风"等 2v2 配合策略。

坑2:炸弹使用时机

早期模型有两个极端:要么炸弹攒到最后全浪费,要么一开始就全扔完。

解决:在 reward 设计中加入了阶段性奖励——不仅看最终胜负,还奖励"关键时刻使用炸弹"(比如在对手即将出完牌时用炸弹拦截)。

坑3:升级等级带来的状态空间膨胀

掼蛋的升级机制意味着"当前打几"会影响哪些牌是万能牌(逢人配)。我们把当前等级作为一个额外的条件输入:

rank_embedding = nn.Embedding(13, 16)  # 2-A共13个等级
rank_feat = rank_embedding(current_rank)
combined = torch.cat([memory, hand_feat, team_feat, rank_feat], dim=-1)

五、小结

掼蛋AI的核心挑战在于不完全信息 + 2v2配合 + 复杂动作空间的叠加。LSTM+DMC 的组合在处理时序推理和策略优化上表现不错,但队友配合的建模仍有提升空间。

下一步我们计划:

  • 引入对手/队友风格分类(激进型/保守型),做针对性策略调整
  • 探索 Attention 机制替换 LSTM,看能否进一步提升长序列建模

如果你也在做类似的多人博弈AI,欢迎交流。


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