Fictitious Cross-Play:团队竞争博弈中的全局Nash均衡学习

1. 引言

在多智能体强化学习中,团队竞争博弈(Team Competition Game)是一类重要的设置:智能体被划分为两个对立的团队,每个团队内部共享相同的奖励目标,但团队之间相互竞争。12

这类问题广泛存在于:

  • 游戏场景:如《星际争霸》中的团队对战
  • 机器人足球:两队机器人争夺进球
  • 军事模拟:对抗性策略规划
  • 金融市场:机构间的零和对弈

本章介绍 Xu 等人在 ICLR 2024 和 JMLR 2025 提出的 Fictitious Cross-Play (FCP) 及其广义版本 Generalized Fictitious Cross-Play (GFCP) 框架,该框架为团队竞争博弈中的全局Nash均衡学习提供了理论基础和高效算法。

2. 经典Fictitious Play回顾

2.1 Fictitious Play的基本思想

Fictitious Play (FP) 是一种经典的博弈论学习方法,最早由 Brown 于 1951 年提出。3 其核心思想是:

每个智能体假设对手的策略是通过历史平均得到的,然后针对这个”虚假”的对手计算最优反应。

2.2 数学形式化

在两人零和博弈中,经典FP的迭代过程为:

其中 是智能体 在时间 的平均历史策略。

每个智能体计算对对手平均策略的最优反应:

2.3 FP的理论保证

定理 2.1(FP收敛性)3
: 在两人零和博弈中,Fictitious Play 的平均策略序列收敛到 Nash 均衡。

然而,经典FP假设每个智能体独立优化,在团队竞争博弈中存在以下问题:

  1. 团队内协调缺失:同一团队的智能体可能学习到不一致的策略
  2. 全局目标忽视:个体最优不等于团队最优
  3. 计算复杂度高:需要为每个智能体计算最优反应

3. Fictitious Cross-Play(FCP)

3.1 团队竞争博弈形式化

FCP 考虑如下团队竞争博弈设置:

定义 3.1(团队竞争博弈)
: 一个团队竞争博弈由元组 定义,其中:

  • :智能体集合,划分为团队 和团队
  • :联合状态空间
  • :联合动作空间
  • :状态转移函数
  • :团队奖励函数(团队内共享)
  • :折扣因子

目标函数为团队极小极大(Minimax)优化:

其中 分别是团队 的联合策略。

3.2 FCP的核心思想

FCP 的核心创新是引入团队层面的虚拟玩家(Team-level Fictitious Players):

经典FP:                          FCP:
                                   
A₁ ←→ B₁                         [团队A] ←→ [团队B]
A₂ ←→ B₂                         (虚拟玩家)   (虚拟玩家)
  ...                             
An ←→ Bn                         A₁, A₂, ..., An 共同响应 B 的平均策略
                                   
                                   B₁, B₂, ..., Bn 共同响应 A 的平均策略

3.3 FCP迭代规则

定义 3.2(FCP迭代)
: 在每个迭代轮次 ,FCP 执行以下更新:

其中 分别是团队 的最优反应操作符。

3.4 最优反应计算

FCP 中的最优反应通过求解以下优化问题得到:

团队 的最优反应

3.5 PyTorch实现

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.distributions import Categorical
 
class TeamPolicy(nn.Module):
    """团队联合策略"""
    
    def __init__(self, state_dim, action_dims):
        super().__init__()
        self.n_agents = len(action_dims)
        self.action_dims = action_dims
        
        # 每个智能体的策略网络
        self.networks = nn.ModuleList([
            nn.Sequential(
                nn.Linear(state_dim, 128),
                nn.ReLU(),
                nn.Linear(128, 64),
                nn.ReLU(),
                nn.Linear(64, action_dim)
            ) for action_dim in action_dims
        ])
    
    def forward(self, states):
        """返回每个智能体的动作 logits"""
        return [net(states) for net in self.networks]
    
    def get_log_prob(self, states, actions):
        """计算动作的 log 概率"""
        logits = self.forward(states)
        log_probs = []
        for i, (logit, action) in enumerate(zip(logits, actions.t())):
            log_probs.append(F.cross_entropy(logit, action, reduction='none'))
        return torch.stack(log_probs, dim=1)
    
    def entropy(self, states):
        """计算策略熵"""
        entropies = []
        for i, net in enumerate(self.networks):
            probs = F.softmax(net(states), dim=-1)
            entropies.append(-torch.sum(probs * torch.log(probs + 1e-8), dim=-1))
        return torch.stack(entropies, dim=1)
 
 
class FictitiousCrossPlay:
    """
    Fictitious Cross-Play (FCP) 实现
    
    参考文献: Xu et al., "Fictitious Cross-Play for Multi-Agent Games"
    ICLR 2024
    """
    
    def __init__(self, state_dim, action_dims, lr=3e-4, gamma=0.99):
        self.gamma = gamma
        
        # 团队策略
        self.team_A_policy = TeamPolicy(state_dim, action_dims)
        self.team_B_policy = TeamPolicy(state_dim, action_dims)
        
        # 优化器
        self.optimizer_A = torch.optim.Adam(self.team_A_policy.parameters(), lr=lr)
        self.optimizer_B = torch.optim.Adam(self.team_B_policy.parameters(), lr=lr)
        
        # 历史策略(用于计算平均)
        self.history_A = []
        self.history_B = []
        self.t_A = 0  # 团队A的迭代计数器
        self.t_B = 0  # 团队B的迭代计数器
    
    def compute_team_reward(self, rewards):
        """
        计算团队奖励(团队内共享)
        """
        # 对于团队竞争,返回 (team_A_reward, team_B_reward)
        return rewards[:, 0], rewards[:, 1]
    
    def update_team_A(self, states, actions, rewards, next_states, dones):
        """
        更新团队A的策略
        
        计算 BR_A(bar_pi_B): 针对团队B的平均历史策略的最优反应
        """
        team_A_reward, _ = self.compute_team_reward(rewards)
        
        # 计算团队A的Q值估计
        with torch.no_grad():
            # 目标Q值(简化版本,实际中可用TD更新)
            next_q = torch.zeros_like(team_A_reward)
        
        # 策略梯度更新
        log_probs = self.team_A_policy.get_log_prob(states, actions[:, :self.team_A_policy.n_agents])
        
        # 最大化团队奖励
        loss = -(log_probs * team_A_reward.unsqueeze(-1)).mean()
        
        self.optimizer_A.zero_grad()
        loss.backward()
        self.optimizer_A.step()
        
        return loss.item()
    
    def update_team_B(self, states, actions, rewards, next_states, dones):
        """更新团队B的策略"""
        _, team_B_reward = self.compute_team_reward(rewards)
        
        # 团队B的策略梯度
        log_probs = self.team_B_policy.get_log_prob(states, actions[:, self.team_A_policy.n_agents:])
        
        loss = -(log_probs * team_B_reward.unsqueeze(-1)).mean()
        
        self.optimizer_B.zero_grad()
        loss.backward()
        self.optimizer_B.step()
        
        return loss.item()
    
    def update_average_strategy(self, states_A, states_B):
        """
        更新平均策略
        
        这是FCP的核心:记录当前策略到历史,用于后续的平均计算
        """
        self.t_A += 1
        self.t_B += 1
        
        # 保存当前策略的"快照"(简化为存储参数均值)
        with torch.no_grad():
            # 实际实现中可能需要存储更多统计量
            self.history_A.append({
                'policy': {k: v.clone() for k, v in self.team_A_policy.named_parameters()},
                'weight': 1.0 / self.t_A
            })
            self.history_B.append({
                'policy': {k: v.clone() for k, v in self.team_B_policy.named_parameters()},
                'weight': 1.0 / self.t_B
            })
    
    def get_average_strategy(self, team='A'):
        """
        获取平均策略(团队对手的历史平均策略)
        
        在实际更新中用于计算最优反应
        """
        history = self.history_A if team == 'B' else self.history_B
        
        if not history:
            return self.team_B_policy if team == 'B' else self.team_A_policy
        
        # 策略空间中的加权平均
        avg_params = {}
        for key in self.team_A_policy.state_dict().keys():
            avg_params[key] = sum(
                h['policy'][key] * h['weight'] 
                for h in history
            )
        
        return avg_params

4. Generalized Fictitious Cross-Play (GFCP)

4.1 从FCP到GFCP的动机

FCP 虽然在理论上有良好性质,但在实际应用中面临挑战:

  1. 平均策略存储开销:历史策略数量随迭代线性增长
  2. 最优反应计算复杂:需要多次调用对手的平均策略
  3. 收敛速率分析缺失:缺乏收敛速度的理论保证

GFCP 通过引入指数加权平均在线凸优化框架解决了这些问题。2

4.2 GFCP的数学形式化

定义 4.1(团队竞争博弈的Nash均衡)
: 联合策略 是团队竞争博弈 的 Nash 均衡,当:

4.3 GFCP迭代规则

定义 4.2(GFCP迭代)
: GFCP 采用指数加权平均维护历史策略:

其中 是指数衰减的权重()。

GFCP 更新规则

4.4 存在性证明

定理 4.1(全局Nash均衡存在性)2
: 在有限状态空间和有限动作空间的团队竞争博弈中,至少存在一个 Nash 均衡。

证明思路

  1. 团队竞争博弈是一个两人零和博弈的特殊形式
  2. 两人零和博弈必有 Nash 均衡(Von Neumann 极小极大定理)4
  3. 有限博弈的 Nash 均衡是纯策略或混合策略

定理 4.2(团队Nash均衡的等价性)
: 在团队竞争博弈中,以下三种均衡概念等价:

  • 全局Nash均衡
  • 团队极小极大均衡
  • 团队相关均衡

4.5 收敛性分析

定理 4.3(GFCP收敛速率)2
: 假设奖励函数有界 ,则 GFCP 的平均策略满足:

其中 是某个 Nash 均衡策略。

关键引理(后验遗憾界)

定义轮次 的即时遗憾为:

其中

引理 4.1 遗憾界)
: GFCP 对每个团队的遗憾界为

4.6 GFCP完整实现

class GeneralizedFictitiousCrossPlay:
    """
    Generalized Fictitious Cross-Play (GFCP) 实现
    
    参考文献: Xu et al., "Generalized Fictitious Cross-Play for 
    Multi-Agent Games with Convex Preferences", JMLR 2025
    """
    
    def __init__(self, state_dim, action_dims, beta=0.99, lr=3e-4, gamma=0.99):
        self.gamma = gamma
        self.beta = beta  # 指数衰减因子
        self.lr = lr
        
        # 团队策略
        self.team_A_policy = TeamPolicy(state_dim, action_dims)
        self.team_B_policy = TeamPolicy(state_dim, action_dims)
        
        # 目标网络(用于稳定训练)
        self.target_A_policy = TeamPolicy(state_dim, action_dims)
        self.target_B_policy = TeamPolicy(state_dim, action_dims)
        self._update_target(self.target_A_policy, self.team_A_policy, tau=1.0)
        self._update_target(self.target_B_policy, self.team_B_policy, tau=1.0)
        
        # 优化器
        self.optimizer_A = torch.optim.Adam(self.team_A_policy.parameters(), lr=lr)
        self.optimizer_B = torch.optim.Adam(self.team_B_policy.parameters(), lr=lr)
        
        # 指数加权平均估计器
        self.ema_A = ExponentialMovingAverage(beta)
        self.ema_B = ExponentialMovingAverage(beta)
        
        # 迭代计数器
        self.t = 0
    
    def _update_target(self, target_net, source_net, tau=0.005):
        """软更新目标网络"""
        for tp, sp in zip(target_net.parameters(), source_net.parameters()):
            tp.data.copy_(tau * sp.data + (1 - tau) * tp.data)
    
    def compute_best_response(self, opponent_avg_policy, team='A'):
        """
        计算对对手平均策略的最优反应
        
        这是一个简化实现,实际中可能需要更复杂的优化过程
        """
        if team == 'A':
            current_policy = self.team_A_policy
        else:
            current_policy = self.team_B_policy
        
        # 克隆当前策略作为最优反应候选
        br_policy = TeamPolicy(
            current_policy.networks[0].input_dim,
            current_policy.action_dims
        )
        
        # 复制参数
        with torch.no_grad():
            br_policy.load_state_dict(current_policy.state_dict())
        
        return br_policy
    
    def update(self, batch):
        """
        GFCP 主更新循环
        """
        states = batch['states']
        actions = batch['actions']
        rewards = batch['rewards']
        next_states = batch['next_states']
        dones = batch['dones']
        
        # 分离两个团队的数据
        n_agents_A = self.team_A_policy.n_agents
        states_A = states
        actions_A = actions[:, :n_agents_A]
        states_B = states
        actions_B = actions[:, n_agents_A:]
        
        # ===== 更新团队A =====
        # 计算团队A对B的平均策略的最优反应
        with torch.no_grad():
            # 获取团队B的EMA估计
            avg_B_params = self.ema_B.get_average()
            
            # 用平均策略计算目标
            target_q_A = self._compute_target_q(
                states_A, rewards[:, 0], next_states, 
                self.target_B_policy, dones
            )
        
        # 团队A的策略梯度
        log_probs_A = self.team_A_policy.get_log_prob(states_A, actions_A)
        
        # 使用TD优势
        with torch.no_grad():
            current_q_A = self._compute_q_values(states_A, actions_A)
            advantage_A = target_q_A - current_q_A
        
        loss_A = -(log_probs_A * advantage_A.unsqueeze(-1)).mean()
        
        self.optimizer_A.zero_grad()
        loss_A.backward()
        self.optimizer_A.step()
        
        # ===== 更新团队B =====
        with torch.no_grad():
            avg_A_params = self.ema_A.get_average()
            target_q_B = self._compute_target_q(
                states_B, rewards[:, 1], next_states,
                self.target_A_policy, dones
            )
        
        log_probs_B = self.team_B_policy.get_log_prob(states_B, actions_B)
        
        with torch.no_grad():
            current_q_B = self._compute_q_values(states_B, actions_B)
            advantage_B = target_q_B - current_q_B
        
        loss_B = -(log_probs_B * advantage_B.unsqueeze(-1)).mean()
        
        self.optimizer_B.zero_grad()
        loss_B.backward()
        self.optimizer_B.step()
        
        # ===== 更新EMA估计器 =====
        self.ema_A.update(self.team_A_policy.state_dict())
        self.ema_B.update(self.team_B_policy.state_dict())
        
        # 更新目标网络
        self._update_target(self.target_A_policy, self.team_A_policy)
        self._update_target(self.target_B_policy, self.team_B_policy)
        
        self.t += 1
        
        return {
            'loss_A': loss_A.item(),
            'loss_B': loss_B.item(),
            'step': self.t
        }
    
    def _compute_q_values(self, states, actions):
        """计算Q值估计"""
        # 简化为基于奖励的估计
        return torch.zeros(states.size(0))
    
    def _compute_target_q(self, states, rewards, next_states, opponent_policy, dones):
        """计算TD目标"""
        with torch.no_grad():
            next_logits = opponent_policy(next_states)
            next_q = torch.stack([
                torch.max(logit, dim=-1)[0] for logit in next_logits
            ], dim=1).mean(dim=1)
            
            target = rewards + self.gamma * (1 - dones) * next_q
        return target
 
 
class ExponentialMovingAverage:
    """
    指数加权移动平均
    
    用于维护GFCP中的历史策略加权平均
    """
    
    def __init__(self, beta=0.99):
        self.beta = beta
        self.ema = None
        self.normalization = 0.0
        self.t = 0
    
    def update(self, state_dict):
        """更新EMA"""
        self.t += 1
        
        # 当前时刻的权重
        weight = self.beta ** (self.t - 1)
        
        if self.ema is None:
            self.ema = {k: v.clone() * weight for k, v in state_dict.items()}
        else:
            for k, v in state_dict.items():
                self.ema[k] = self.beta * self.ema[k] + (1 - self.beta) * v * weight
        
        self.normalization = self.beta * self.normalization + (1 - self.beta) * weight
    
    def get_average(self):
        """获取归一化后的平均策略"""
        if self.ema is None:
            return None
        
        return {k: v / self.normalization for k, v in self.ema.items()}

5. 与Mean Field Games的联系

5.1 MFGs回顾

Mean Field Games (MFG) 研究大量智能体之间的博弈,其中每个智能体受”平均场”(其他智能体行为的统计平均)的影响。5

MFG的基本假设

  • 智能体数量趋于无穷大
  • 智能体之间是同质
  • 相互作用通过平均场传递

5.2 FCP/FCP vs MFG

特性FCP/GFCPMFG
智能体数量有限(两团队)趋于无穷
异质性支持(不同团队)通常同质
均衡概念Nash均衡Mean Field均衡
通信机制无显式通信平均场汇总
收敛性指数收敛

5.3 理论联系

定理 5.1(FCP作为有限团队MFG)
: 当团队内部智能体数量趋于无穷,且团队间通过团队平均策略交互时,FCP收敛到MFG的Mean Field均衡。

这为FCP提供了更广阔的理论视角:在大团队极限下,团队竞争博弈可以建模为两个”平均场”之间的博弈。

6. 实践应用

6.1 StarCraft II多智能体控制

在《星际争霸II》微操战斗中,FCP/GFCP已成功应用于:

  • 团队对战:两个队伍(人族 vs 神族)竞争
  • 资源分配:团队内协调单位行动
  • 战术学习:学习复杂战术配合
# StarCraft II 环境配置示例
SC2_CONFIG = {
    'map_name': 'corridor',
    'n_units_A': 10,  # 团队A单位数
    'n_units_B': 10,  # 团队B单位数
    'state_dim': 50,   # 单位状态维度
    'action_dims': [9, 9, 9, 9, 9, 9, 9, 9, 9, 9],  # 每个单位的动作空间
}

6.2 多智能体粒子环境

Multi-Agent Particle Environment (MPE) 是测试MARL算法的经典环境:

场景描述GFCP适用性
合作导航多个智能体协作到达目标★★★
竞争抢夺两队争夺资源★★★★★
参考坐标需要通信协调★★

6.3 收敛性监测

def monitor_convergence(algorithm, eval_env, n_episodes=100):
    """
    监测GFCP的收敛性
    """
    team_A_rewards = []
    team_B_rewards = []
    
    for _ in range(n_episodes):
        obs = eval_env.reset()
        done = False
        episode_reward_A = 0
        episode_reward_B = 0
        
        while not done:
            # 使用当前策略执行
            actions_A = algorithm.team_A_policy.sample(obs['team_A'])
            actions_B = algorithm.team_B_policy.sample(obs['team_B'])
            
            obs, rewards, done, info = eval_env.step(
                torch.cat([actions_A, actions_B], dim=1)
            )
            
            episode_reward_A += rewards[0]
            episode_reward_B += rewards[1]
        
        team_A_rewards.append(episode_reward_A)
        team_B_rewards.append(episode_reward_B)
    
    return {
        'mean_A_reward': np.mean(team_A_rewards),
        'mean_B_reward': np.mean(team_B_rewards),
        'std_A': np.std(team_A_rewards),
        'std_B': np.std(team_B_rewards),
        'reward_ratio': np.mean(team_A_rewards) / (np.mean(team_B_rewards) + 1e-8)
    }

7. 与PSRO/PSRO²的比较

7.1 PSRO回顾

Policy Space Response Oracle (PSRO) 是一种基于博弈论的多智能体学习方法。6

PSRO迭代

  1. 选择一个联合策略谱(mixture)
  2. 对每个智能体训练最优反应
  3. 扩展策略谱

7.2 PSRO²扩展

PSRO² 在PSRO基础上引入:

  • 双层优化:元游戏 + 基础游戏
  • 神经量化:连续策略的离散化
  • 分层学习:多尺度策略表示

7.3 对比分析

特性PSROPSRO²FCPGFCP
均衡类型NashNashNashNash
团队支持部分
收敛速率
计算开销很高
历史存储策略谱策略谱+元策略EMAEMA
适用场景一般博弈复杂博弈团队竞争团队竞争

7.4 核心差异

PSRO/PSRO²

  • 维护策略谱(所有历史策略的凸组合)
  • 每次迭代需要求解双层优化
  • 计算复杂度高,但理论保证强

FCP/GFCP

  • 维护团队层面的虚拟玩家
  • 只需计算单层最优反应
  • 计算效率高,专为团队竞争设计
# PSRO vs GFCP 对比示意
 
class PSROComparison:
    """PSRO vs GFCP 计算复杂度对比"""
    
    def __init__(self):
        self.psro_complexity = {
            'br_compute': 'O(T × |A| × |S|)',  # 最优反应计算
            'mixture_update': 'O(T²)',          # 策略谱更新
            'meta_game': 'O(T³)'                 # 元博弈求解
        }
        
        self.gfcp_complexity = {
            'ema_update': 'O(|θ|)',              # EMA更新
            'br_compute': 'O(|A| × |S|)',       # 最优反应(团队级)
            'total': 'O(T × |A| × |S|)'          # 总体
        }
    
    def summary(self):
        return f"""
        PSRO: 计算复杂度 ~O(T³)
        GFCP: 计算复杂度 ~O(T)
        
        GFCP在团队竞争场景下更加高效
        """

8. 总结与展望

8.1 主要贡献

  1. 理论框架:提出团队竞争博弈的完整形式化
  2. 收敛保证:证明GFCP的收敛速率
  3. 高效算法:通过EMA将存储复杂度从降至
  4. 实践验证:在StarCraft II和多智能体粒子环境中验证有效性

8.2 未来方向

  • 与LLM结合:利用大语言模型进行高层策略推理
  • 安全约束:在博弈过程中加入安全性约束
  • 课程学习:设计从简单到复杂的团队竞争课程
  • 开放世界:扩展到非平稳环境和未知对手

8.3 相关资源

参考文献

Footnotes

  1. Xu et al. “Fictitious Cross-Play: Learning Optimal Strategies in Team Competition Games” ICLR 2024

  2. Xu et al. “Generalized Fictitious Cross-Play for Multi-Agent Games with Convex Preferences” JMLR 2025 2 3 4

  3. Brown, G. W. “Iterative Solution of Games by Fictitious Play” 1951 2

  4. Von Neumann, J. “Zur Theorie der Gesellschaftsspiele” 1928

  5. Lasry, J. M., & Lions, P. L. “Mean field games” Japanese Journal of Mathematics 2007

  6. Muller et al. “A Unified Game-Theoretic Approach to Multi-Agent Reinforcement Learning” NeurIPS 2019