当前位置: 首页 > news >正文

理解多智能体深度确定性策略梯度MADDPG算法:基于python从零实现

引言:多智能体强化学习(MARL)

多智能体强化学习(MARL)将强化学习拓展到多个智能体在共享环境中相互交互的场景。这些智能体可能相互合作、竞争,或者目标混杂。MARL 引入了单智能体设置中不存在的独特挑战。

MARL 中的非平稳性挑战

当多个智能体同时学习时,会出现一个主要的难题。从任何一个单一智能体的视角来看,环境显得非平稳,因为其他智能体的策略在学习过程中不断变化。当其他智能体的行为方式改变时,原本在它们先前行为下表现良好的动作,可能会变得糟糕。这违反了单智能体强化学习算法(如标准的 Q 学习或 DDPG)所依赖的平稳性假设,导致学习过程不稳定。

什么是 MADDPG?

多智能体深度确定性策略梯度(MADDPG)是 DDPG 算法的扩展,旨在解决多智能体场景中的非平稳性问题。它采用了集中式训练与分散式执行的范式。

核心思想:集中式训练,分散式执行

  1. 分散式执行:在执行阶段(在环境中行动时),每个智能体 i i i 根据自己的局部观察 o i o_i oi 使用自己的演员网络 μ i ( o i ; θ i ) \mu_i(o_i; \theta_i) μi(oi;θi) 来选择其动作 a i a_i ai。这一点对于实际部署至关重要,因为在实际中智能体可能无法获取全局信息。
  2. 集中式训练:在训练阶段(更新网络时),MADDPG 为每个智能体 i i i 使用一个集中式评论家 Q i Q_i Qi。这个评论家以所有智能体的联合观察(或状态) ( x = ( o 1 , . . . , o N ) x = (o_1, ..., o_N) x=(o1,...,oN)) 和联合动作 ( a = ( a 1 , . . . , a N ) a = (a_1, ..., a_N) a=(a1,...,aN)) 作为输入,来估计动作价值 Q i ( x , a 1 , . . . , a N ; ϕ i ) Q_i(x, a_1, ..., a_N; \phi_i) Qi(x,a1,...,aN;ϕi)

通过将评论家的条件设定为联合状态和动作,即使其他智能体的策略发生变化,每个智能体的学习目标也变得平稳,因为评论家知道其他智能体在训练过程中实际采取的动作。这使得学习过程得以稳定。

为什么选择 MADDPG?优势所在

  • 应对非平稳性:集中式评论家缓解了多智能体学习中的主要挑战。
  • 仅使用局部信息进行执行:策略可以在智能体仅具有部分可观察性的场景中部署。
  • 对环境结构无假设:与某些 MARL 方法不同,它不需要特定的通信协议或对奖励结构的了解(例如,差异奖励)。
  • 适用于合作、竞争或混合设置:该框架是通用的。

MADDPG 的应用领域和方式

MADDPG 已被应用于各种多智能体领域:

  1. 合作导航:多个智能体协调到达目标位置,同时避免碰撞。
  2. 捕食者-猎物场景:智能体学习追逐或躲避其他智能体。
  3. 交通信号控制:协调交通信号灯。
  4. 多机器人协调:任务分配和路径规划。

当满足以下条件时,它特别适用:

  • 智能体具有连续或离散的动作空间(尽管最初是为连续动作空间提出的)。
  • 可以进行集中式训练(在学习阶段可以访问联合信息)。
  • 需要分散式执行。
  • 环境动态复杂,从单个智能体的视角来看可能是非平稳的。

MADDPG 的数学基础

MADDPG 将 DDPG 框架扩展到 N N N 个智能体。设策略为 μ = { μ 1 , . . . , μ N } \boldsymbol{\mu} = \{\mu_1, ..., \mu_N\} μ={μ1,...,μN},参数为 θ = { θ 1 , . . . , θ N } \boldsymbol{\theta} = \{\theta_1, ..., \theta_N\} θ={θ1,...,θN},集中式动作价值函数为 Q i ( x , a 1 , . . . , a N ; ϕ i ) Q_i(x, a_1, ..., a_N; \phi_i) Qi(x,a1,...,aN;ϕi),每个智能体 i i i 的参数为 ϕ i \phi_i ϕi

集中式动作价值函数(评论家)

每个智能体 i i i 学习自己的评论家 Q i ( x , a 1 , . . . , a N ) Q_i(x, a_1, ..., a_N) Qi(x,a1,...,aN),其中 x = ( o 1 , . . . , o N ) x=(o_1, ..., o_N) x=(o1,...,oN) 表示联合观察(或完整状态,如果可用)。这个函数估计在给定联合上下文和动作的情况下智能体 i的预期回报。

评论家更新

每个智能体 i i i 的评论家 Q i ( x , a 1 , . . . , a N ; ϕ i ) Q_i(x, a_1, ..., a_N; \phi_i) Qi(x,a1,...,aN;ϕi) 通过最小化使用共享回放缓冲区 D \mathcal{D} D 中的样本计算的损失来进行更新。损失通常是均方误差(MSE):
L ( ϕ i ) = E ( x , a , r , x ′ ) ∼ D [ ( y i − Q i ( x , a 1 , . . . , a N ; ϕ i ) ) 2 ] L(\phi_i) = \mathbb{E}_{(x, a, r, x') \sim \mathcal{D}} [ (y_i - Q_i(x, a_1, ..., a_N; \phi_i))^2 ] L(ϕi)=E(x,a,r,x)D[(yiQi(x,a1,...,aN;ϕi))2]
目标值 y i y_i yi 使用目标网络(用撇号表示)计算:
y i = r i + γ Q i ′ ( x ′ , a 1 ′ , . . . , a N ′ ; ϕ i ′ ) ∣ a j ′ = μ j ′ ( o j ′ ) y_i = r_i + \gamma Q'_i(x', a'_1, ..., a'_N; \phi'_i)|_{a'_j = \mu'_j(o'_j)} yi=ri+γQi(x,a1,...,aN;ϕi)aj=μj(oj)
其中 a j ′ = μ j ′ ( o j ′ ; θ j ′ ) a'_j = \mu'_j(o'_j; \theta'_j) aj=μj(oj;θj) 是每个智能体的目标演员网络根据它们的下一个局部观察 o j ′ o'_j oj x ′ x' x 中预测的下一个动作。

演员更新(策略梯度)

每个智能体 i i i 的演员 μ i ( o i ; θ i ) \mu_i(o_i; \theta_i) μi(oi;θi) 使用确定性策略梯度定理进行更新,该定理已适应集中式评论家。演员 i i i 的梯度上升方向为:
∇ θ i J ( θ i ) ≈ E x , a ∼ D [ ∇ θ i μ i ( o i ; θ i ) ∇ a i Q i ( x , a 1 , . . . , a N ; ϕ i ) ∣ a i = μ i ( o i ) ] \nabla_{\theta_i} J(\theta_i) \approx \mathbb{E}_{x, a \sim \mathcal{D}} [ \nabla_{\theta_i} \mu_i(o_i; \theta_i) \nabla_{a_i} Q_i(x, a_1, ..., a_N; \phi_i)|_{a_i=\mu_i(o_i)} ] θiJ(θi)Ex,aD[θiμi(oi;θi)aiQi(x,a1,...,aN;ϕi)ai=μi(oi)]
在实际操作中,使用 PyTorch autograd,这通常是通过最小化损失来实现的:
L ( θ i ) = − E o i ∼ D [ Q i ( x , μ 1 ( o 1 ) , . . . , μ N ( o N ) ; ϕ i ) ] L(\theta_i) = - \mathbb{E}_{o_i \sim \mathcal{D}} [ Q_i(x, \mu_1(o_1), ..., \mu_N(o_N); \phi_i) ] L(θi)=EoiD[Qi(x,μ1(o1),...,μN(oN);ϕi)]
在为演员 i i i 计算这个损失及其梯度时,将 j ≠ i j \neq i j=i o j o_j oj a j = μ j ( o j ) a_j = \mu_j(o_j) aj=μj(oj) 视为从缓冲区或当前策略中获得的固定输入。

离散动作的适应:如果使用离散动作,演员通常输出一个概率分布的 logits(例如,Categorical 或 Gumbel-Softmax)。然后可以使用策略梯度方法进行更新,其中优势是从集中式评论家 Q i Q_i Qi 中得出的。为了简化,可以直接使用 Q i ( x , a 1 , . . . , a N ) Q_i(x, a_1, ..., a_N) Qi(x,a1,...,aN) 作为信号(类似于 DDPG 使用 Q 的方式),如果 Q 值较高,则增加采取该动作的概率,尽管也可以使用涉及基线的更复杂的策略梯度估计。

目标网络和软更新

与 DDPG 类似,MADDPG 为所有演员( μ i ′ \mu'_i μi)和评论家( Q i ′ Q'_i Qi)使用目标网络,通过软更新进行更新:
θ i ′ ← τ θ i + ( 1 − τ ) θ i ′ \theta'_i \leftarrow \tau \theta_i + (1 - \tau) \theta'_i θiτθi+(1τ)θi
ϕ i ′ ← τ ϕ i + ( 1 − τ ) ϕ i ′ \phi'_i \leftarrow \tau \phi_i + (1 - \tau) \phi'_i ϕiτϕi+(1τ)ϕi

探索

探索通常通过在训练过程中为确定性演员输出的动作添加噪声来处理。这通常是为每个智能体独立完成的(例如,独立的 Ornstein-Uhlenbeck 或高斯噪声过程)。
对于我们的离散动作适应,探索可以通过从演员的输出分布中采样或基于演员的首选动作使用 ϵ \epsilon ϵ-贪婪策略来实现。

MADDPG 的逐步解释

  1. 初始化:对于每个智能体 i = 1... N i=1...N i=1...N
    • 演员网络 μ i ( θ i ) \mu_i(\theta_i) μi(θi),评论家网络 Q i ( ϕ i ) Q_i(\phi_i) Qi(ϕi)
    • 目标网络 μ i ′ ( θ i ′ ) \mu'_i(\theta'_i) μi(θi) Q i ′ ( ϕ i ′ ) Q'_i(\phi'_i) Qi(ϕi),并设置 θ i ′ ← θ i , ϕ i ′ ← ϕ i \theta'_i \leftarrow \theta_i, \phi'_i \leftarrow \phi_i θiθi,ϕiϕi
    • 探索噪声过程 N i \mathcal{N}_i Ni
  2. 初始化:共享回放缓冲区 D \mathcal{D} D
  3. 对于每个训练剧集
    a. 获取初始联合状态/观察 x = ( o 1 , . . . , o N ) x = (o_1, ..., o_N) x=(o1,...,oN)。重置噪声过程。
    b. 对于每个步骤 t t t
    i. 对于每个智能体 i i i,选择动作 a i = μ i ( o i ; θ i ) + N i a_i = \mu_i(o_i; \theta_i) + \mathcal{N}_i ai=μi(oi;θi)+Ni。如有必要,对动作进行裁剪。
    ii. 执行联合动作 a = ( a 1 , . . . , a N ) a = (a_1, ..., a_N) a=(a1,...,aN),观察联合奖励 r = ( r 1 , . . . , r N ) r=(r_1, ..., r_N) r=(r1,...,rN) 和下一个联合状态/观察 x ′ = ( o 1 ′ , . . . , o N ′ ) x'=(o'_1, ..., o'_N) x=(o1,...,oN),以及完成标志 d = ( d 1 , . . . , d N ) d=(d_1, ..., d_N) d=(d1,...,dN)
    iii. 将联合转换 ( x , a , r , x ′ , d ) (x, a, r, x', d) (x,a,r,x,d) 存储到 D \mathcal{D} D 中。
    iv. x ← x ′ x \leftarrow x' xx
    v. 更新步骤(如果缓冲区有足够的样本)
    1. 从 D \mathcal{D} D 中随机抽取一个大小为 B B B 的联合转换迷你批次。
    2. 对于每个智能体 i = 1... N i=1...N i=1...N
    - 更新评论家 Q i Q_i Qi:使用目标网络 μ j ′ , Q i ′ \mu'_j, Q'_i μj,Qi 计算目标 y i y_i yi。最小化 L ( ϕ i ) L(\phi_i) L(ϕi)
    - 更新演员 μ i \mu_i μi:使用主集中式评论家 Q i Q_i Qi 最小化演员损失 L ( θ i ) L(\theta_i) L(θi)
    3. 对于每个智能体 i = 1... N i=1...N i=1...N
    - 更新目标网络:对 θ i ′ \theta'_i θi ϕ i ′ \phi'_i ϕi 进行软更新。
    vi. 如果任何智能体完成(或全局终止),则退出步骤循环。
  4. 重复:直到收敛。

MADDPG 的关键组成部分

每个智能体的演员网络

  • 每个智能体 i i i 都有一个演员 μ i \mu_i μi,将它的局部观察 o i o_i oi 映射到它的动作 a i a_i ai
  • 使用其关联的集中式评论家的梯度进行训练。

每个智能体的集中式评论家网络

  • 每个智能体 i i i 都有一个评论家 Q i Q_i Qi,根据联合观察和联合动作 ( x , a 1 , . . . , a N ) (x, a_1, ..., a_N) (x,a1,...,aN) 来估计智能体 i i i 的动作价值。
  • 使用目标网络计算的目标值进行训练。

目标网络(演员和评论家)

  • 所有演员和评论家网络的慢速更新副本,用于稳定的目標计算。

回放缓冲区(共享)

  • 存储联合经验的元组: ( x , a , r , x ′ , d ) (x, a, r, x', d) (x,a,r,x,d)
  • 启用离线策略学习。

动作选择与探索

  • 演员在分散式执行中使用,仅基于局部 o i o_i oi
  • 在训练过程中,为每个智能体独立添加探索噪声(或采样策略)。

集中式训练逻辑

  • 更新步骤需要访问所有智能体的网络参数(用于目标和评论家输入)以及共享缓冲区中的数据。

超参数

  • 与 DDPG 类似(缓冲区大小、批次大小、学习率、 τ \tau τ γ \gamma γ、噪声参数),但可能需要每个智能体单独设置或共享。

实践示例:自定义多智能体网格世界

环境设计理由

我们将创建一个简单的 2 智能体合作导航任务,位于网格上:

  • 状态:两个智能体的位置以及它们各自目标的位置。
  • 观察(智能体 i i i:智能体 i i i 的位置,智能体 i i i 的目标位置,其他智能体的位置。
  • 动作:每个智能体的离散动作(上、下、左、右)。
  • 奖励:合作型。每步有较小的负奖励。如果两个智能体同时到达它们的目标,则有较大的正奖励。也许可以根据距离减少情况设置一个形状奖励。
  • 目标:两个智能体同时到达它们的目标。

这允许我们展示基于联合状态(所有位置)和联合动作的集中式 Q 值估计,而演员则基于局部观察进行操作。我们通过集中式 Q-评论家使用策略梯度来适应 MADDPG,以处理离散动作。

设置环境

导入库。

# 导入必要的库
import numpy as np
import matplotlib.pyplot as plt
import random
import math
from collections import namedtuple, deque
from itertools import count
from typing import List, Tuple, Dict, Optional, Callable, Any
import copy# 导入 PyTorch
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from torch.distributions import Categorical # 用于离散演员# 设置设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备:{device}")# 设置随机种子
seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():torch.cuda.manual_seed_all(seed)%matplotlib inline
使用设备:cpu

创建自定义多智能体环境

一个简单的 2 智能体合作网格导航任务。

class MultiAgentGridEnv:"""简单的 2 智能体合作网格世界。智能体必须同时到达各自的目标。观察包括智能体位置和目标位置。奖励是共享的。"""def __init__(self, size: int = 5, num_agents: int = 2):self.size: int = sizeself.num_agents: int = num_agentsself.action_dim: int = 4 # 上、下、左、右self.action_map: Dict[int, Tuple[int, int]] = {0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1)}self.actions: List[int] = list(self.action_map.keys())# 为简单起见,定义固定的起始和目标位置self.start_positions: List[Tuple[int, int]] = [(0, 0), (size - 1, size - 1)]self.goal_positions: List[Tuple[int, int]] = [(size - 1, 0), (0, size - 1)]if num_agents != 2:raise NotImplementedError("目前仅支持 2 个智能体。")self.agent_positions: List[Tuple[int, int]] = list(self.start_positions)# 智能体 i 的观察空间:(self_r, self_c, other_r, other_c, goal_i_r, goal_i_c)# 在 0 和 1 之间进行归一化self.observation_dim_per_agent: int = 6 self.max_coord = float(self.size - 1)def _normalize_pos(self, pos: Tuple[int, int]) -> Tuple[float, float]:""" 对网格位置进行归一化。 """r, c = posreturn (r / self.max_coord, c / self.max_coord) if self.max_coord > 0 else (0.0, 0.0)def _get_observation(self, agent_id: int) -> np.ndarray:""" 获取特定智能体的归一化局部观察。 """obs = []# 自身位置self_pos_norm = self._normalize_pos(self.agent_positions[agent_id])obs.extend(self_pos_norm)# 其他智能体位置other_id = 1 - agent_idother_pos_norm = self._normalize_pos(self.agent_positions[other_id])obs.extend(other_pos_norm)# 自身目标位置goal_pos_norm = self._normalize_pos(self.goal_positions[agent_id])obs.extend(goal_pos_norm)return np.array(obs, dtype=np.float32)def get_joint_observation(self) -> List[np.ndarray]:""" 获取所有智能体的观察列表。 """return [self._get_observation(i) for i in range(self.num_agents)]def reset(self) -> List[np.ndarray]:""" 重置环境,返回初始观察列表。 """self.agent_positions = list(self.start_positions)return self.get_joint_observation()def step(self, actions: List[int]) -> Tuple[List[np.ndarray], List[float], bool]:"""执行所有智能体的一步操作。参数:- actions (List[int]): 每个智能体的一个动作列表。返回:- Tuple[List[np.ndarray], List[float], bool]: - 每个智能体的下一个观察列表。- 每个智能体的奖励列表(共享奖励)。- 全局完成标志。"""if len(actions) != self.num_agents:raise ValueError(f"期望有 {self.num_agents} 个动作,但得到了 {len(actions)} 个")next_positions: List[Tuple[int, int]] = list(self.agent_positions) # 从当前开始total_dist_reduction = 0.0hit_wall_penalty = 0.0for i in range(self.num_agents):current_pos = self.agent_positions[i]if current_pos == self.goal_positions[i]: # 如果智能体已经在目标位置,则不移动continue action = actions[i]dr, dc = self.action_map[action]next_r, next_c = current_pos[0] + dr, current_pos[1] + dc# 检查边界并更新位置if 0 <= next_r < self.size and 0 <= next_c < self.size:next_positions[i] = (next_r, next_c)else:hit_wall_penalty -= 0.5 # 碰到墙壁的惩罚next_positions[i] = current_pos # 保持原位# 简单的碰撞处理:如果智能体移动到同一个位置,则反弹if self.num_agents == 2 and next_positions[0] == next_positions[1]:next_positions = list(self.agent_positions) # 恢复到先前的位置hit_wall_penalty -= 0.5 # 视为惩罚(类似于碰到墙壁)self.agent_positions = next_positions# 计算奖励和完成标志done = all(self.agent_positions[i] == self.goal_positions[i] for i in range(self.num_agents))if done:reward = 10.0 # 合作成功的大奖励else:# 基于距离的奖励(负的到目标的距离总和)dist_reward = 0.0for i in range(self.num_agents):dist = abs(self.agent_positions[i][0] - self.goal_positions[i][0]) + \abs(self.agent_positions[i][1] - self.goal_positions[i][1])dist_reward -= dist * 0.1 # 缩放后的负距离reward = -0.05 + dist_reward + hit_wall_penalty # 每步的小惩罚 + 距离 + 墙壁惩罚next_observations = self.get_joint_observation()# 共享奖励rewards = [reward] * self.num_agents return next_observations, rewards, done

实例化并测试多智能体环境。

maddpg_env = MultiAgentGridEnv(size=5, num_agents=2)
initial_obs_list = maddpg_env.reset()
print(f"MADDPG 环境大小:{maddpg_env.size}x{maddpg_env.size}")
print(f"智能体数量:{maddpg_env.num_agents}")
print(f"每个智能体的动作维度:{maddpg_env.action_dim}")
print(f"每个智能体的观察维度:{maddpg_env.observation_dim_per_agent}")
print(f"智能体 0 的初始观察:{initial_obs_list[0]}")
print(f"智能体 1 的初始观察:{initial_obs_list[1]}")# 示例步骤
actions_example = [1, 2] # 智能体 0 向下,智能体 1 向左
next_obs_list, rewards_list, done_flag = maddpg_env.step(actions_example)
print(f"\n执行动作 {actions_example} 后:")
print(f" 智能体 0 的下一个观察:{next_obs_list[0]}")
print(f" 智能体 1 的下一个观察:{next_obs_list[1]}")
print(f" 共享奖励:{rewards_list[0]}")
print(f" 完成:{done_flag}")
MADDPG 环境大小:5x5
智能体数量:2
每个智能体的动作维度:4
每个智能体的观察维度:6
智能体 0 的初始观察:[0. 0. 1. 1. 1. 0.]
智能体 1 的初始观察:[1. 1. 0. 0. 0. 1.]执行动作 [1, 2] 后:智能体 0 的下一个观察:[0.25 0.   1.   0.75 1.   0.  ]智能体 1 的下一个观察:[1.   0.75 0.25 0.   0.   1.  ]共享奖励:-0.8500000000000001完成:False

实现 MADDPG 算法

定义演员网络(离散动作)

输出一个分类分布的 logits,用于离散动作。

class ActorDiscrete(nn.Module):""" 离散动作的演员网络,用于 MADDPG。 """def __init__(self, obs_dim: int, action_dim: int):super(ActorDiscrete, self).__init__()self.fc1 = nn.Linear(obs_dim, 128)self.fc2 = nn.Linear(128, 128)self.fc3 = nn.Linear(128, action_dim)def forward(self, obs: torch.Tensor) -> Categorical:""" 将观察映射到动作 logits -> 分类分布。 """x = F.relu(self.fc1(obs))x = F.relu(self.fc2(x))action_logits = self.fc3(x)return Categorical(logits=action_logits)def select_action(self, obs: torch.Tensor, use_exploration=True) -> Tuple[torch.Tensor, torch.Tensor]:""" 根据策略选择动作(采样或 argmax)。 """dist = self.forward(obs)if use_exploration:action = dist.sample()else: # 测试或目标动作预测时使用确定性action = torch.argmax(dist.probs, dim=-1)log_prob = dist.log_prob(action)return action, log_prob

定义集中式评论家网络

以联合观察和联合动作为输入。

class CentralizedCritic(nn.Module):""" 集中式评论家网络,用于 MADDPG。 """def __init__(self, joint_obs_dim: int, joint_action_dim: int):super(CentralizedCritic, self).__init__()# 输入大小 = 所有观察的大小 + 所有动作的大小self.fc1 = nn.Linear(joint_obs_dim + joint_action_dim, 256) self.fc2 = nn.Linear(256, 256)self.fc3 = nn.Linear(256, 1) # 为特定智能体输出单个 Q 值def forward(self, joint_obs: torch.Tensor, joint_actions: torch.Tensor) -> torch.Tensor:""" 将联合观察和动作映射到 Q 值。 """# 确保动作格式正确(例如,离散动作的 one-hot)# 为了简化,这里我们假设直接传递动作索引(在实际中可能需要进行处理,例如嵌入)# 假设动作已经正确处理(例如,连续动作或已嵌入)x = torch.cat([joint_obs, joint_actions], dim=1)x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))q_value = self.fc3(x)return q_value

定义回放缓冲区

存储联合转换。

# 定义 MADDPG 缓冲区的转换结构
# 为了方便批量处理,通常将每个智能体的数据分别存储
Experience = namedtuple('Experience', ('obs_list', 'action_list', 'reward_list', 'next_obs_list', 'done_list'))# 多智能体回放缓冲区(修改为处理智能体数据列表)
class MultiAgentReplayBuffer:def __init__(self, capacity: int):self.memory = deque([], maxlen=capacity)def push(self, obs_list: List[np.ndarray], action_list: List[int], reward_list: List[float], next_obs_list: List[np.ndarray], done: bool) -> None:""" 保存联合转换。 """# 将 numpy 数组转换为张量(或直接存储为 numpy)# 在批量处理之前,存储为 numpy 可能会稍微节省一些内存obs_t = [torch.from_numpy(o).float() for o in obs_list]act_t = torch.tensor(action_list, dtype=torch.long) # 存储动作索引rew_t = torch.tensor(reward_list, dtype=torch.float32)next_obs_t = [torch.from_numpy(o).float() for o in next_obs_list]done_t = torch.tensor([float(done)] * len(obs_list), dtype=torch.float32) # 为每个智能体重复完成标志self.memory.append(Experience(obs_t, act_t, rew_t, next_obs_t, done_t))def sample(self, batch_size: int) -> Optional[Experience]:""" 采样一批经验。 """if len(self.memory) < batch_size:return Noneexperiences = random.sample(self.memory, batch_size)# 批量处理:对每个组件进行堆叠,跨越批次和智能体# 需要仔细处理结果形状,在更新步骤中使用obs_batch, act_batch, rew_batch, next_obs_batch, dones_batch = zip(*experiences)# 示例:堆叠观察 -> (batch_size, num_agents, obs_dim)obs_batch_stacked = torch.stack([torch.stack(obs) for obs in obs_batch])act_batch_stacked = torch.stack(act_batch) # (batch_size, num_agents)rew_batch_stacked = torch.stack(rew_batch) # (batch_size, num_agents)next_obs_batch_stacked = torch.stack([torch.stack(n_obs) for n_obs in next_obs_batch])dones_batch_stacked = torch.stack(dones_batch) # (batch_size, num_agents)# 返回一个包含批量张量的命名元组return Experience(obs_batch_stacked, act_batch_stacked, rew_batch_stacked, next_obs_batch_stacked, dones_batch_stacked)def __len__(self) -> int:return len(self.memory)

软更新函数

标准的软更新。

def soft_update(target_net: nn.Module, main_net: nn.Module, tau: float) -> None:for target_param, main_param in zip(target_net.parameters(), main_net.parameters()):target_param.data.copy_(tau * main_param.data + (1.0 - tau) * target_param.data)

MADDPG 智能体管理器类

一个类,用于管理多个智能体、它们的网络和优化器。

class MADDPGAgentManager:""" 管理多个智能体、它们的网络和更新,用于 MADDPG。 """def __init__(self, num_agents: int, obs_dim_list: List[int], action_dim: int,actor_lr: float, critic_lr: float, gamma: float, tau: float,device: torch.device):self.num_agents = num_agentsself.obs_dim_list = obs_dim_listself.action_dim = action_dimself.gamma = gammaself.tau = tauself.device = device# 计算评论家所需的联合维度joint_obs_dim = sum(obs_dim_list)# 对于离散动作,评论家需要适当的表示(例如,one-hot 或索引)# 如果传递索引,则需要嵌入或小心处理。如果使用 one-hot,则维度为 num_agents * action_dim。# 假设将动作索引传递给评论家。joint_action_dim_one_hot = num_agents * action_dimself.actors: List[ActorDiscrete] = []self.critics: List[CentralizedCritic] = []self.target_actors: List[ActorDiscrete] = []self.target_critics: List[CentralizedCritic] = []self.actor_optimizers: List[optim.Optimizer] = []self.critic_optimizers: List[optim.Optimizer] = []for i in range(num_agents):# 为智能体 i 创建网络actor = ActorDiscrete(obs_dim_list[i], action_dim).to(device)critic = CentralizedCritic(joint_obs_dim, joint_action_dim_one_hot).to(device)target_actor = ActorDiscrete(obs_dim_list[i], action_dim).to(device)target_critic = CentralizedCritic(joint_obs_dim, joint_action_dim_one_hot).to(device)# 初始化目标网络target_actor.load_state_dict(actor.state_dict())target_critic.load_state_dict(critic.state_dict())for p in target_actor.parameters(): p.requires_grad = Falsefor p in target_critic.parameters(): p.requires_grad = False# 创建优化器actor_optimizer = optim.Adam(actor.parameters(), lr=actor_lr)critic_optimizer = optim.Adam(critic.parameters(), lr=critic_lr)# 存储智能体组件self.actors.append(actor)self.critics.append(critic)self.target_actors.append(target_actor)self.target_critics.append(target_critic)self.actor_optimizers.append(actor_optimizer)self.critic_optimizers.append(critic_optimizer)def select_actions(self, obs_list: List[torch.Tensor], use_exploration=True) -> Tuple[List[int], List[torch.Tensor]]:""" 根据局部观察为所有智能体选择动作。 """actions = []log_probs = [] for i in range(self.num_agents):self.actors[i].eval()with torch.no_grad():act, log_prob = self.actors[i].select_action(obs_list[i].to(self.device), use_exploration)# 如果是单个观察,则转换为标量if act.dim() == 0:act = act.item()self.actors[i].train()actions.append(act)log_probs.append(log_prob)return actions, log_probsdef update(self, batch: Experience, agent_id: int) -> Tuple[float, float]:""" 为单个智能体 'agent_id' 执行更新。 """# 解包批量数据# 形状:obs/next_obs(B, N, O_dim),acts(B, N),rews(B, N),dones(B, N)obs_batch, act_batch, rew_batch, next_obs_batch, dones_batch = batchbatch_size = obs_batch.shape[0]# 为集中式评论家准备输入# 堆叠观察:(B, N, O) -> (B, N*O)joint_obs = obs_batch.view(batch_size, -1).to(self.device)joint_next_obs = next_obs_batch.view(batch_size, -1).to(self.device)# 将动作索引转换为 one-hot 编码,供评论家输入act_one_hot = F.one_hot(act_batch, num_classes=self.action_dim).float()joint_actions_one_hot = act_one_hot.view(batch_size, -1).to(self.device)# 获取当前智能体的奖励和完成标志rewards_i = rew_batch[:, agent_id].unsqueeze(-1).to(self.device)dones_i = dones_batch[:, agent_id].unsqueeze(-1).to(self.device)# --- 评论家更新 --- with torch.no_grad():# 从所有目标演员那里获取下一个动作target_next_actions_list = []for j in range(self.num_agents):# 输入:智能体 j 在批量中的下一个观察 (B, O_j)obs_j_next = next_obs_batch[:, j, :].to(self.device)# 从目标演员 j 获取确定性动作action_j_next, _ = self.target_actors[j].select_action(obs_j_next, use_exploration=False)# 将动作索引转换为 one-hotaction_j_next_one_hot = F.one_hot(action_j_next, num_classes=self.action_dim).float()target_next_actions_list.append(action_j_next_one_hot)# 堆叠目标下一个动作:(B, N*A_onehot)joint_target_next_actions = torch.cat(target_next_actions_list, dim=1).to(self.device)# 从目标评论家 i 获取目标 Q 值q_target_next = self.target_critics[agent_id](joint_next_obs, joint_target_next_actions)# 计算目标 y = r_i + gamma * Q'_i * (1 - d_i)y = rewards_i + self.gamma * (1.0 - dones_i) * q_target_next# 获取主评论家 i 的当前 Q 估计值q_current = self.critics[agent_id](joint_obs, joint_actions_one_hot)# 计算评论家损失critic_loss = F.mse_loss(q_current, y)# 优化评论家 iself.critic_optimizers[agent_id].zero_grad()critic_loss.backward()torch.nn.utils.clip_grad_norm_(self.critics[agent_id].parameters(), 1.0) # 可选的梯度裁剪self.critic_optimizers[agent_id].step()# --- 演员更新 --- # 冻结评论家梯度,用于演员更新for p in self.critics[agent_id].parameters():p.requires_grad = False# 根据批量观察,使用所有智能体的当前演员计算动作current_actions_list = []log_probs_i_list = [] # 仅存储智能体 i 的 log_probfor j in range(self.num_agents):obs_j = obs_batch[:, j, :].to(self.device)# 需要动作概率/logits 进行更新 - 使用类似 Gumbel-Softmax 或 REINFORCE 的更新dist_j = self.actors[j](obs_j) # 获取分类分布# 如果使用 DDPG 目标:Q(s, mu(s)),我们需要确定性动作# 适应使用策略梯度:最大化 E[log_pi_i * Q_i_detached]action_j = dist_j.sample() # 采样动作作为 Q 的输入if j == agent_id:log_prob_i = dist_j.log_prob(action_j) # 仅需要更新的智能体的 log_probaction_j_one_hot = F.one_hot(action_j, num_classes=self.action_dim).float()current_actions_list.append(action_j_one_hot)joint_current_actions = torch.cat(current_actions_list, dim=1).to(self.device)# 计算演员损失:- E[log_pi_i * Q_i_detached]# Q_i 在所有演员的当前动作下进行评估q_for_actor_loss = self.critics[agent_id](joint_obs, joint_current_actions)actor_loss = -(log_prob_i * q_for_actor_loss.detach()).mean() # 剥离 Q 值# 替代的 DDPG 风格损失(如果演员是确定性的):# actor_loss = -self.critics[agent_id](joint_obs, joint_current_actions).mean()# 优化演员 iself.actor_optimizers[agent_id].zero_grad()actor_loss.backward()torch.nn.utils.clip_grad_norm_(self.actors[agent_id].parameters(), 1.0) # 可选的梯度裁剪self.actor_optimizers[agent_id].step()# 解冻评论家梯度for p in self.critics[agent_id].parameters():p.requires_grad = Truereturn critic_loss.item(), actor_loss.item()def update_targets(self) -> None:""" 为所有目标网络执行软更新。 """for i in range(self.num_agents):soft_update(self.target_critics[i], self.critics[i], self.tau)soft_update(self.target_actors[i], self.actors[i], self.tau)

运行 MADDPG 算法

超参数设置

# MADDPG 在自定义多智能体网格世界中的超参数
BUFFER_SIZE_MADDPG = int(1e5)
BATCH_SIZE_MADDPG = 256
GAMMA_MADDPG = 0.99
TAU_MADDPG = 1e-3
ACTOR_LR_MADDPG = 1e-4
CRITIC_LR_MADDPG = 1e-3
EPSILON_MADDPG_START = 1.0 # 用于离散动作的 epsilon-greedy 探索
EPSILON_MADDPG_END = 0.05
EPSILON_MADDPG_DECAY = 50000 # 在这么多步中衰减NUM_EPISODES_MADDPG = 200
MAX_STEPS_PER_EPISODE_MADDPG = 100
UPDATE_EVERY_MADDPG = 4      # 每 N 个环境步执行一次更新
NUM_UPDATES_MADDPG = 1       # 每次更新间隔的更新次数

初始化

# 初始化环境
env_maddpg = MultiAgentGridEnv(size=5, num_agents=2)
num_agents = env_maddpg.num_agents
obs_dims = [env_maddpg.observation_dim_per_agent] * num_agents
action_dim = env_maddpg.action_dim# 初始化智能体管理器
maddpg_manager = MADDPGAgentManager(num_agents=num_agents,obs_dim_list=obs_dims,action_dim=action_dim,actor_lr=ACTOR_LR_MADDPG,critic_lr=CRITIC_LR_MADDPG,gamma=GAMMA_MADDPG,tau=TAU_MADDPG,device=device
)# 初始化回放缓冲区
memory_maddpg = MultiAgentReplayBuffer(BUFFER_SIZE_MADDPG)# 用于绘图的列表
maddpg_episode_rewards = [] # 每个剧集存储总奖励(智能体总和)
maddpg_critic_losses = [[] for _ in range(num_agents)] # 每个智能体的评论家损失列表
maddpg_actor_losses = [[] for _ in range(num_agents)] # 每个智能体的演员损失列表

训练循环

print("开始 MADDPG 训练...")total_steps_maddpg = 0
epsilon = EPSILON_MADDPG_STARTfor i_episode in range(1, NUM_EPISODES_MADDPG + 1):obs_list_np = env_maddpg.reset()obs_list = [torch.from_numpy(o).float().to(device) for o in obs_list_np]episode_reward = 0ep_critic_losses = [0.0] * num_agentsep_actor_losses = [0.0] * num_agentsnum_updates_ep = 0for t in range(MAX_STEPS_PER_EPISODE_MADDPG):# --- 动作选择 --- # 使用 epsilon-greedy 适应离散动作if random.random() < epsilon:actions_list = [random.randrange(action_dim) for _ in range(num_agents)]else:# 从演员那里获取动作(use_exploration=False 给出 argmax 用于测试)actions_list, _ = maddpg_manager.select_actions(obs_list, use_exploration=False)# --- 环境交互 --- next_obs_list_np, rewards_list, done = env_maddpg.step(actions_list)# --- 存储经验(联合) --- memory_maddpg.push(obs_list_np, actions_list, rewards_list, next_obs_list_np, done)# 更新状态表示obs_list_np = next_obs_list_npobs_list = [torch.from_numpy(o).float().to(device) for o in obs_list_np]# 累积共享奖励用于记录episode_reward += rewards_list[0] # 使用智能体 0 的奖励作为共享奖励total_steps_maddpg += 1# --- 更新网络 --- if len(memory_maddpg) > BATCH_SIZE_MADDPG and total_steps_maddpg % UPDATE_EVERY_MADDPG == 0:for _ in range(NUM_UPDATES_MADDPG):batch = memory_maddpg.sample(BATCH_SIZE_MADDPG)if batch:for agent_id in range(num_agents):c_loss, a_loss = maddpg_manager.update(batch, agent_id)ep_critic_losses[agent_id] += c_lossep_actor_losses[agent_id] += a_lossnum_updates_ep += 1# 在更新所有智能体后更新目标网络maddpg_manager.update_targets()# 衰减 epsilonepsilon = max(EPSILON_MADDPG_END, EPSILON_MADDPG_START - total_steps_maddpg / EPSILON_MADDPG_DECAY * (EPSILON_MADDPG_START - EPSILON_MADDPG_END))if done:break# --- 剧集结束 --- maddpg_episode_rewards.append(episode_reward)for i in range(num_agents):maddpg_critic_losses[i].append(ep_critic_losses[i] / num_updates_ep if num_updates_ep > 0 else 0)maddpg_actor_losses[i].append(ep_actor_losses[i] / num_updates_ep if num_updates_ep > 0 else 0)# 打印进度if i_episode % 50 == 0:avg_reward = np.mean(maddpg_episode_rewards[-50:])avg_closs = np.mean([np.mean(maddpg_critic_losses[i][-50:]) for i in range(num_agents)])avg_aloss = np.mean([np.mean(maddpg_actor_losses[i][-50:]) for i in range(num_agents)])print(f"剧集 {i_episode}/{NUM_EPISODES_MADDPG} | 步数:{total_steps_maddpg} | 平均奖励:{avg_reward:.2f} | 平均 C_Loss:{avg_closs:.4f} | 平均 A_Loss:{avg_aloss:.4f} | Epsilon:{epsilon:.3f}")print("训练完成 (MADDPG)。")
开始 MADDPG 训练...
剧集 50/200 | 步数:4130 | 平均奖励:-65.08 | 平均 C_Loss:0.1754 | 平均 A_Loss:-1.5070 | Epsilon:0.922
剧集 100/200 | 步数:7739 | 平均奖励:-57.36 | 平均 C_Loss:0.0337 | 平均 A_Loss:-2.1525 | Epsilon:0.853
剧集 150/200 | 步数:11024 | 平均奖励:-47.13 | 平均 C_Loss:0.0143 | 平均 A_Loss:-2.8037 | Epsilon:0.791
剧集 200/200 | 步数:14686 | 平均奖励:-54.68 | 平均 C_Loss:0.0153 | 平均 A_Loss:-3.7225 | Epsilon:0.721
训练完成 (MADDPG)。

可视化学习过程

绘制每个剧集的聚合奖励和每个智能体的平均损失。

# 绘制 MADDPG 的结果
plt.figure(figsize=(18, 4))# 剧集奖励(共享)
plt.subplot(1, 3, 1)
plt.plot(maddpg_episode_rewards)
plt.title('MADDPG 网格世界:剧集奖励(共享)')
plt.xlabel('剧集')
plt.ylabel('总奖励')
plt.grid(True)
if len(maddpg_episode_rewards) >= 50:rewards_ma_maddpg = np.convolve(maddpg_episode_rewards, np.ones(50)/50, mode='valid')plt.plot(np.arange(len(rewards_ma_maddpg)) + 49, rewards_ma_maddpg, label='50-剧集移动平均', color='orange')plt.legend()# 平均评论家损失 / 剧集
avg_critic_losses = np.mean(maddpg_critic_losses, axis=0)
plt.subplot(1, 3, 2)
plt.plot(avg_critic_losses)
plt.title('MADDPG 网格世界:平均评论家损失 / 剧集')
plt.xlabel('剧集')
plt.ylabel('平均 MSE 损失')
plt.grid(True)
if len(avg_critic_losses) >= 50:closs_ma_maddpg = np.convolve(avg_critic_losses, np.ones(50)/50, mode='valid')plt.plot(np.arange(len(closs_ma_maddpg)) + 49, closs_ma_maddpg, label='50-剧集移动平均', color='orange')plt.legend()# 平均演员损失 / 剧集
avg_actor_losses = np.mean(maddpg_actor_losses, axis=0)
plt.subplot(1, 3, 3)
plt.plot(avg_actor_losses)
plt.title('MADDPG 网格世界:平均演员损失 / 剧集')
plt.xlabel('剧集')
plt.ylabel('平均策略梯度损失')
plt.grid(True)
if len(avg_actor_losses) >= 50:aloss_ma_maddpg = np.convolve(avg_actor_losses, np.ones(50)/50, mode='valid')plt.plot(np.arange(len(aloss_ma_maddpg)) + 49, aloss_ma_maddpg, label='50-剧集移动平均', color='orange')plt.legend()plt.tight_layout()
plt.show()

在这里插入图片描述

MADDPG 学习曲线分析(网格世界 - 共享奖励):

  1. 剧集奖励(共享):

    • 观察结果: 每个剧集的总共享奖励在整个训练期间(200 个剧集)表现出极大的波动。尽管 50-剧集移动平均值(橙色线)表明有一些学习正在发生,但平均值仍然显著为负(在结束时大约为 -50),并且远未达到任何潜在的最优值(这在目标到达任务中可能是正的)。从一个剧集到下一个剧集,性能的大幅波动没有显示出稳定在高奖励水平的迹象。
    • 解释: 这个图表突出了遇到的主要挑战:实现稳定的协作行为。高方差表明分散的演员在一致协调动作以实现共享目标方面存在困难。虽然移动平均值显示一些改善,超出了随机行为,但缺乏收敛到高奖励的收敛表明在这些条件下发现一个稳健的协作联合策略是困难的。学习过程不稳定,这是多智能体交互引入的复杂性的特征。
  2. 平均评论家损失 / 剧集:

    • 观察结果: 与奖励形成鲜明对比的是,集中式评论家的平均均方误差(MSE)损失表现出极佳的收敛特性。在一些初始高尖峰之后(可能是由于随机初始策略和较大的预测误差),损失在大约前 50 个剧集内显著下降,并稳定地收敛到零,其余训练期间保持非常低的水平。
    • 解释: 这表明集中式评论家,它观察所有智能体的动作和状态,能够有效地学习预测联合状态-动作对的预期共享回报(Q 值)。损失的快速收敛到低水平表明价值估计组件的 MADDPG 正在正常工作,并能够根据它接收到的数据准确评估智能体集体动作的后果。
  3. 平均演员损失 / 剧集:

    • 观察结果: 平均演员损失,代表策略梯度目标(通常与评论家估计的 Q 值有关,用于演员选择的动作),显示出清晰且一致的下降趋势(变得更负)。这表明,平均而言,演员正在成功地根据评论家提供的梯度信息调整其策略,朝着评论家建议的更高预期回报的方向发展。
    • 解释: 这个图表证实了策略更新机制按预期工作。每个分散的演员都使用集中式评论家提供的梯度信息来改进其个人策略。稳步下降表明根据评论家的当前估计一致地改进策略。

总体结论:
MADDPG 的结果呈现出多智能体学习中常见的有趣画面。底层机制——集中式评论家学习价值函数以及分散式演员根据评论家指导更新策略——似乎正常工作,如收敛的损失图表所示。然而,这种内部学习进展并没有转化为稳定、高奖励的团队表现。共享奖励的极端波动表明在协调、信用分配或处理非平稳环境(每个智能体的策略变化影响其他智能体的最优策略)方面存在显著挑战。尽管评论家学会了什么是好的联合动作,但分散的演员在一致发现和执行这些协调动作方面存在困难,导致在观察到的训练期间内整体结果不稳定。

分析学习到的策略(测试)

使用训练好的演员分散地进行测试。

def test_maddpg_agents(manager: MADDPGAgentManager, env_instance: MultiAgentGridEnv, num_episodes: int = 5, seed_offset: int = 5000) -> None:""" 测试训练好的 MADDPG 智能体分散地行动。 """print(f"\n--- 测试 MADDPG 智能体 ({num_episodes} 个剧集) ---")all_episode_rewards = []for i in range(num_episodes):obs_list_np = env_instance.reset() # 如果环境支持,使用内部种子obs_list = [torch.from_numpy(o).float().to(manager.device) for o in obs_list_np]episode_reward = 0done = Falset = 0while not done and t < MAX_STEPS_PER_EPISODE_MADDPG: # 添加测试步数限制# 分散地获取动作,不添加探索噪声actions_list, _ = maddpg_manager.select_actions(obs_list, use_exploration=True)# 执行环境步骤next_obs_list_np, rewards_list, done = env_instance.step(actions_list)# 更新观察obs_list = [torch.from_numpy(o).float().to(manager.device) for o in next_obs_list_np]episode_reward += rewards_list[0] # 跟踪共享奖励t += 1print(f"测试剧集 {i+1}:奖励 = {episode_reward:.2f}, 长度 = {t}")all_episode_rewards.append(episode_reward)print(f"--- 测试完成。平均奖励:{np.mean(all_episode_rewards):.2f} ---")# 运行测试剧集
test_maddpg_agents(maddpg_manager, env_maddpg, num_episodes=5)
--- 测试 MADDPG 智能体 (5 个剧集) ---
测试剧集 1:奖励 = -78.40, 长度 = 100
测试剧集 2:奖励 = -119.80, 长度 = 100
测试剧集 3:奖励 = -88.30, 长度 = 100
测试剧集 4:奖励 = -35.60, 长度 = 53
测试剧集 5:奖励 = -80.10, 长度 = 100
--- 测试完成。平均奖励:-80.44 ---

MADDPG 的常见挑战和扩展

挑战:集中式评论家的可扩展性

  • 问题:集中式评论家的输入维度随着智能体数量线性增长(联合观察 + 联合动作)。对于大量智能体,这可能变得计算上不可行。
  • 解决方案
    • 参数共享:为相似的智能体使用共享网络参数。
    • 注意力机制:使用注意力机制让评论家专注于其他智能体的相关信息。
    • 近似方法:使用近似集中式评论家的方法,无需完整的联合信息(例如,平均场方法)。

挑战:合作设置中的信用分配

  • 问题:当使用共享团队奖励时,很难让单个智能体的评论家确定该智能体对团队成功的具体贡献(信用分配问题)。
    解决方案
    • 反事实基线(COMA):将当前联合 Q 值与仅当前智能体动作变化的基线进行比较。
    • 价值分解网络(VDN,QMIX):学习智能体的个体 Q 函数,这些函数组合(例如,求和)形成联合 Q 函数,强制执行对信用分配有用的关联。

挑战:对超参数的敏感性

  • 问题:与 DDPG 一样,MADDPG 对学习率、目标更新、探索噪声、缓冲区大小等超参数敏感。
    解决方案:需要仔细调整,通常针对特定的 MARL 问题。

挑战:连续动作空间问题

  • 问题:原始的 MADDPG 继承了 DDPG 在连续动作空间中的潜在不稳定性(例如,Q 值高估)。
    解决方案:将 TD3 的想法(例如,双集中式评论家、延迟策略更新、目标动作平滑)纳入 MADDPG 框架(结果是 MATD3)。

扩展:

  • MATD3:将 TD3 改进纳入 MADDPG。
  • MAPPO:将 PPO 适应于多智能体设置,通常使用集中式价值函数。
  • 价值分解方法(VDN,QMIX):专注于具有共享奖励的合作任务。

结论

MADDPG 为将深度演员-评论家方法应用于多智能体环境提供了一个有效的框架。通过利用集中式训练(特别是通过集中式评论家了解联合信息)和分散式执行(演员使用局部观察),它成功地缓解了 MARL 中固有的非平稳性挑战。

其能够处理各种智能体互动(合作、竞争、混合)和动作空间,使得它成为一个多功能的算法。尽管存在挑战,特别是关于扩展到大量智能体以及稳健的信用分配,MADDPG 代表了 MARL 的重要一步,并且是许多后续多智能体算法的基础。

相关文章:

理解多智能体深度确定性策略梯度MADDPG算法:基于python从零实现

引言&#xff1a;多智能体强化学习&#xff08;MARL&#xff09; 多智能体强化学习&#xff08;MARL&#xff09;将强化学习拓展到多个智能体在共享环境中相互交互的场景。这些智能体可能相互合作、竞争&#xff0c;或者目标混杂。MARL 引入了单智能体设置中不存在的独特挑战。…...

【AI大语言模型本质分析框架】

AI大语言模型本质分析框架 ——从教育危机到智能本质的七层递进式解构 第一层&#xff1a;现象观察——阴&#xff08;显性危机&#xff09;与阳&#xff08;隐性变革&#xff09;的共存 观点1&#xff08;阴&#xff09;&#xff1a;AI作弊泛滥&#xff0c;传统教育体系崩溃…...

算法模型部署后_python脚本API测试指南-记录3

API 测试指南 服务运行后&#xff0c;可以通过以下方式测试&#xff1a; Curl: curl -X POST -F "file./test_dataset/surface/surface57.png" http://<服务器IP>:9000/api/v1/predictPython 脚本: (参考 svm_request测试.py) import requestsurl http://…...

鸿蒙(HarmonyOS)应用开发入门教程

目录 第一章:鸿蒙系统简介 1.1 什么是鸿蒙系统? 1.2 鸿蒙系统架构 第二章:开发环境搭建 2.1 安装DevEco Studio 步骤1:下载与安装 步骤2:首次配置 步骤3:设备准备 2.2 创建第一个项目 第三章:鸿蒙应用开发基础 3.1 核心概念:Ability与AbilitySlice 示例代码…...

MIT XV6 - 1.6 Lab: Xv6 and Unix utilities -uptime

接上文 MIT XV6 - 1.5 Lab: Xv6 and Unix utilities - xargs 第一章持续有点久了&#xff0c;虽然肯定有些特点和细节还没注意到&#xff0c;但这次的主要目的是学习内核部分&#xff0c;决定水一篇然后进入第二章节 uptime 第一章的最后一个实验&#xff0c;选做性质&#xf…...

Python语言在地球科学交叉领域中的应用——从数据可视化到常见数据分析方法的使用【实例操作】

前言&#xff1a; Python是功能强大、免费、开源&#xff0c;实现面向对象的编程语言&#xff0c;Python能够运行在Linux、Windows、Macintosh、AIX操作系统上及不同平台&#xff08;x86和arm&#xff09;&#xff0c;Python简洁的语法和对动态输入的支持&#xff0c;再加上解释…...

flutter 的 json序列化和反序列化

一、json转实体 Instantly parse JSON in any language | quicktype 二、实体中的toJson和fromJson 实现 官方推荐的 两个插件(个人觉得一个实体会多一个.g.dart 文件太多了&#xff0c;不喜欢) json_annotation json_serializable 三、使用 dart_json_mapper 实现上面的功…...

什么是数据集市(Data Mart)?

数据集市&#xff08;Data Mart&#xff09;是数据仓库&#xff08;Data Warehouse&#xff09;的一个子集&#xff0c;专门针对某个特定业务部门、业务线或主题领域&#xff0c;存储和管理该部门或领域所需的特定数据。它通常包含从企业范围的数据仓库中抽取、筛选和汇总的部分…...

从攻击者角度来看Go1.24的路径遍历攻击防御

目录 一、具体攻击示例 程序 攻击步骤&#xff1a; 二、为什么攻击者能成功&#xff1f; 分析 类比理解 总结 三、TOCTOU 竞态条件漏洞 1、背景&#xff1a;符号链接遍历攻击 2. TOCTOU 竞态条件漏洞 3. 另一种变体&#xff1a;目录移动攻击 4. 问题的核心 四、防…...

[ARM][汇编] 01.基础概念

目录 1.全局标号 1.1.使用方法 1.1.1.声明全局标号 1.1.2.定义全局标号 1.1.3.引用全局标号 1.2.全局标号与局部标号的区别 1.3.注意事项 2.局部标号 2.1.使用方法 2.1.1.定义局部标号 2.1.2.跳转引用 2.2.局部标号与全局标号的对比 2.3.注意事项 3.符号定义伪指…...

杭州电商全平台代运营领军者——品融电商

杭州电商全平台代运营领军者——品融电商&#xff1a;以“效品合一”驱动品牌全域增长 在电商行业竞争日益白热化的当下&#xff0c;品牌如何突破流量焦虑、实现长效增长&#xff1f;作为中国领先的品牌化电商服务商&#xff0c;杭州品融电商&#xff08;PINKROON&#xff09;…...

02.Golang 切片(slice)源码分析(一、定义与基础操作实现)

Golang 切片&#xff08;slice&#xff09;源码分析&#xff08;一、定义与基础操作实现&#xff09; 注意当前go版本代码为1.23 一、定义 slice 的底层数据是数组&#xff0c;slice 是对数组的封装&#xff0c;它描述一个数组的片段。两者都可以通过下标来访问单个元素。 数…...

当生产了~/qt-arm/bin/qmake,可以单独编译其他-源码的某个模块,如下,编译/qtmultimedia

cd ~/qt-everywhere-src-5.15.2/qtmultimedia # 设置交叉编译器和 qmake 路径 export CC/usr/bin/aarch64-linux-gnu-gcc export CXX/usr/bin/aarch64-linux-gnu-g export QMAKE~/qt-arm/bin/qmake # 使用已安装的 qmake export QT_INSTALL_PREFIX~/qt-arm # 安装路径 # 配…...

WordPress 网站上的 jpg、png 和 WebP 图片插件

核心功能 1. 转换 AVIF 并压缩 AVIF 将您 WordPress 网站上的 jpg、png 和 WebP 图片转换为 AVIF 格式&#xff0c;并根据您设置的压缩级别压缩 AVIF 图片。如果原始图片已经是 WordPress 6.5 以上支持的 AVIF 格式&#xff0c;则原始 AVIF 图片将仅被压缩。 2. 转换 WebP 并…...

构造+简单树状

昨日的牛客周赛算是比较简单的&#xff0c;其中最后一道构造题目属实眼前一亮。 倒数第二个题目也是一个很好的模拟题目&#xff08;考验对二叉树的理解和代码的细节&#xff09; 给定每一层的节点个数&#xff0c;自己拟定一个父亲节点&#xff0c;构造一个满足条件的二叉树。…...

Flask支持哪些日志框架

目录 ✅ Flask 默认支持的日志框架 ✅ 默认推荐:logging(标准库) ✅ 进阶推荐:Loguru(更优雅的日志库) ✅ Flask 日志级别说明(与标准库一致) ✅ 生产环境建议 ✅ 总结推荐 在 Flask 中,默认的日志系统是基于 Python 标准库 logging 模块 构建的。 ✅ Flask 默认…...

健康养生指南:解锁活力生活的科学密码

健康是人生最珍贵的财富&#xff0c;在快节奏的现代生活中&#xff0c;掌握科学的养生方法至关重要。虽然不借助中医理念&#xff0c;我们依然可以从饮食、运动、睡眠等多个方面入手&#xff0c;打造健康生活方式。 合理的饮食是健康的基石。遵循均衡饮食原则&#xff0c;保证每…...

SAR图像压缩感知

SAR图像压缩感知 matlab代码 对应着汕大闫老师的那本压缩感知及其应用&#xff0c;有需要的可以看一下&#xff01;&#xff01; SAR图像压缩感知/baboon.bmp , 66616 SAR图像压缩感知/camera.bmp , 66616 SAR图像压缩感知/DWT.m , 1265 SAR图像压缩感知/Gauss.m , 373 SAR图像…...

定时器设计

定时器设计的必要性 服务器中的定时器设计具有多方面的必要性&#xff0c;主要体现在以下几个关键方面&#xff1a; 任务调度与管理 定时任务执行&#xff1a;服务器常常需要执行一些定时性的任务&#xff0c;如定时备份数据、定时清理缓存、定时更新系统日志等。通过定时器可…...

Spring Boot整合Kafka实战指南:从环境搭建到消息处理全解析

一、环境准备 安装 Kafka 下载 Kafka&#xff1a;从 Apache Kafka 官网下载对应版本的 Kafka。 解压并启动 Kafka&#xff1a; # 启动 Zookeeper&#xff08;Kafka 依赖 Zookeeper&#xff09; bin/zookeeper-server-start.sh config/zookeeper.properties# 启动 Kafka bin/ka…...

(done) 补充:xv6 的一个用户程序 init 是怎么启动的 ?它如何启动第一个 bash ?

先看 main.c 从函数名来看&#xff0c;比较相关的就 userinit() 和 scheduler() #include "types.h" #include "param.h" #include "memlayout.h" #include "riscv.h" #include "defs.h"volatile static int started 0;//…...

AI 搜索引擎 MindSearch

背景 RAG是一种利用文档减少大模型的幻觉&#xff0c;AI搜索也是 AI 搜索引擎 MindSearch 是一个开源的 AI 搜索引擎框架&#xff0c;具有与 Perplexity.ai Pro 相同的性能。您可以轻松部署它来构建您自己的搜索引擎&#xff0c;可以使用闭源 LLM&#xff08;如 GPT、Claude…...

HTML简单语法标签(后续实操:云备份项目)

以下是一些 HTML 的简单语法标签及其功能介绍&#xff1a; 基本结构标签 <!DOCTYPE html>&#xff1a;声明文档类型为 HTML5<html>&#xff1a;HTML 文档的根标签<head>&#xff1a;包含文档元数据&#xff08;如标题、字符编码等&#xff09;<title>…...

CentOS 和 RHEL

CentOS 和 RHEL&#xff08;Red Hat Enterprise Linux&#xff09;关系非常紧密&#xff0c;简而言之&#xff1a; CentOS 最初是 RHEL 的免费、开源克隆版&#xff0c;几乎与 RHEL 二进制兼容。 CentOS 原是 RHEL 的“免费双胞胎”&#xff0c;但已被放弃&#xff0c;现在推荐…...

java----------->代理模式

目录 什么是代理模式&#xff1f; 为什么会有代理模式&#xff1f; 怎么写代理模式&#xff1f; 实现代理模式总共需要三步&#xff1a; 什么是代理模式&#xff1f; 代理模式&#xff1a;给目标对象提供一个代理对象&#xff0c;并且由代理对象控制目标对象的引用 代理就是…...

Wpf学习片段

IRegionManager 和IContainerExtension IRegionManager 是 Prism 框架中用于管理 UI 区域&#xff08;Regions&#xff09;的核心接口&#xff0c;它实现了模块化应用中视图&#xff08;Views&#xff09;的动态加载、导航和生命周期管理。 IContainerExtension 是依赖注入&…...

智能手表测试用例文档

智能手表测试用例文档 产品名称&#xff1a;智能手表 A1 版本号&#xff1a;FW v1.0.0 测试负责人&#xff1a;[填写] 编写时间&#xff1a;2025-xx-xx 文档状态&#xff1a;初次版本 &#x1f4c1; 测试用例结构说明 字段描述用例编号测试用例唯一编号&#xff0c;如 TC-FUN…...

密码学--希尔密码

一、实验目的 1、通过实现简单的古典密码算法&#xff0c;理解密码学的相关概念 2、理解明文、密文、加密密钥、解密密钥、加密算法、解密算法、流密码与分组密码等。 二、实验内容 1、题目内容描述 ①定义分组字符长度 ②随机生成加密密钥&#xff0c;并验证密钥的可行性 …...

配置Hadoop集群-集群配置

以下是 Hadoop 集群的核心配置步骤&#xff0c;基于之前的免密登录和文件同步基础&#xff0c;完成 Hadoop 分布式环境的搭建&#xff1a; 1. 集群规划 假设集群包含 3 个节点&#xff1a; master&#xff1a;NameNode、ResourceManagerslave1&#xff1a;DataNode、NodeMana…...

第三方软件测评中心分享:软件功能测试类型和测试工具

在数字化时代&#xff0c;软件测试已成为确保产品质量的重要环节。功能测试作为软件测试中的核心部分&#xff0c;关注于软件产品是否按预期功能正常运作。 软件功能测试可以按不同的方式进行分类&#xff0c;主要包括以下几种类型&#xff1a;   1.正功能测试&#xff1a;验…...

Profibus DP主站与Modbus RTU/TCP网关与海仕达变频器轻松实现数据交互

Profibus DP主站与Modbus RTU/TCP网关与海仕达变频器轻松实现数据交互 Profibus DP主站转Modbus RTU/TCP&#xff08;XD-MDPBm20&#xff09;网关在Profibus总线侧实现主站功能&#xff0c;在Modbus串口侧实现从站功能。可将ProfibusDP协议的设备&#xff08;如&#xff1a;海…...

多视角系统,视角之间的切换,输入操作。无人机Pawn视角的实现

一.创建自己的PlayerController。它相当于是灵魂&#xff0c;穿梭在不同Pawn之间。也即是切换视角。不同输入的响应也写在这里。这样即使&#xff0c;都有鼠标操作&#xff0c;也能区分。避免了代码的重复耦合。也可以叫做视角系统。 class LZJGAMEMODE_API ALZJPlayerControl…...

[学习]RTKLib详解:ionex.c、options.c与preceph.c

RTKLib详解&#xff1a;ionex.c、options.c与preceph.c 本文是 RTKLlib详解 系列文章的一篇&#xff0c;目前该系列文章还在持续总结写作中&#xff0c;以发表的如下&#xff0c;有兴趣的可以翻阅。 [学习] RTKlib详解&#xff1a;功能、工具与源码结构解析 [学习]RTKLib详解&…...

【Linux笔记】——进程信号的保存

&#x1f525;个人主页&#x1f525;&#xff1a;孤寂大仙V &#x1f308;收录专栏&#x1f308;&#xff1a;Linux &#x1f339;往期回顾&#x1f339;&#xff1a;【Linux笔记】——进程信号的产生 &#x1f516;流水不争&#xff0c;争的是滔滔不 一、信号的相关概念二、信…...

教育机构教务管理系统哪个好?

在当今教育培训行业快速发展的背景下&#xff0c;一个高效、专业的教务管理系统已成为教育机构提升运营效率、优化教学质量的关键工具。本文将深入分析爱耕云教务管理系统的核心优势&#xff0c;通过具体功能解析和代码示例展示其技术实现方式&#xff0c;并对比市场上其他主流…...

ZYNQ笔记(二十):Clocking Wizard 动态配置

版本&#xff1a;Vivado2020.2&#xff08;Vitis&#xff09; 任务&#xff1a;ZYNQ PS端 通过 AXI4Lite 接口配置 Clocking Wizard IP核输出时钟频率 目录 一、介绍 二、寄存器定义 三、配置 四、PS端代码 一、介绍 Xilinx 的 Clock Wizard IP核 用于在 FPGA 中生成和管理…...

电商平台一站式网络安全架构设计指南

摘要&#xff1a;据 Gartner 统计&#xff0c;采用一体化安全方案的电商企业数据泄露成本降低 67%。本文从攻击链分析到防御体系构建&#xff0c;详解如何实现网络层、应用层、数据层的协同防护。 一、电商安全威胁全景图&#xff08;2024 攻击态势&#xff09; 1.1 攻击者完…...

烟花爆竹储存需要注意哪些问题

烟花爆竹储存需要注意哪些问题 烟花爆竹作为易燃易爆物品&#xff0c;其储存安全至关重要。不当的储存方式不仅可能导致产品失效&#xff0c;更可能引发火灾、爆炸等严重事故。以下是烟花爆竹储存需要注意的几个关键问题&#xff1a; 一、储存场所选择 必须选择专用仓库储存…...

C++11详解

文章目录 前言一、列表初始化1.1 {} 初始化1.2 initializer_list 类型 三、声明3.1 auto3.2 decltype 四、右值引用和移动语义4.1 左值引用和右值引用4.2 移动语义 五、可变参数模板六、lambda表达式各部分详细解释示例代码代码解释 七、包装器八、bind注意事项 前言 C11在系统…...

VLM-RL:用于安全自动驾驶的统一视觉语言模型和强化学习框架——论文阅读

《VLM-RL: A Unified Vision Language Models and Reinforcement Learning Framework for Safe Autonomous Driving》2024年12月发表&#xff0c;来自Wisconsin Madison分校和Purdue大学的论文。 近年来&#xff0c;基于强化学习&#xff08;RL&#xff09;的学习驾驶策略的方法…...

新手安装java所有工具(jdk、idea,Maven,数据库)

新手安装JAVA工具 介绍JDK11IDEA 2025.1Maven数据库&#xff08;Navicat Premium Lite&#xff09; 介绍 涉及安装JAVA所需的各种工具 JDK&#xff08;以JDK11为例&#xff09;IDEA&#xff08;以2025.1为例&#xff09;Maven&#xff08;以3.8.8为例&#xff09;数据库&…...

hive在配置文件中添加了hive.metastore.uris之后进入hive输入命令报错

在hive-site.xml文件中加入配置hive.metastore.uris启动hive后报错 <property><name>hive.metastore.uris</name><value>thrift://node154:9083</value></property> 加完属性就需要手动启动metastore服务&#xff0c;因为不使用 Zookeepe…...

Hive原理

Hive 是构建在 Hadoop 上的数据仓库工具,其核心原理是通过类 SQL 语言(HiveQL)将结构化数据查询转换为分布式计算任务(如 MapReduce、Tez、Spark),并利用 HDFS 存储数据。以下是 Hive 的核心原理和架构: 1. 核心设计思想‌ ‌数据仓库抽象‌:将 HDFS 上的文件抽象为‌…...

cursor 出现 unauthorized request

文档出自&#xff1a;https://www.kdocs.cn/l/csE3iuSauHoS...

uniapp|商品列表加入购物车实现抛物线动画效果、上下左右抛入、多端兼容(H5、APP、微信小程序)

以uniapp框架为基础,详细解析商品列表加入购物车抛物线动画的实现方案。通过动态获取商品点击位置与购物车坐标,结合CSS过渡动画模拟抛物线轨迹,实现从商品图到购物车图标的动态效果。 目录 核心实现原理坐标动态计算抛物线轨迹模拟​动画元素控制代码实现详解模板层设计脚本…...

点下4个Winform UI开源控件库

从零学习构建一个完整的系统 今天一起来盘点下4个Winform UI开源控件库&#xff0c;有.Net Framework&#xff0c;也有.Net Core。 1、支持.Net 7的开源UI组件框架 项目简介 这是一个基于.Net Framework、.Net 6开发的&#xff0c;WinForm开源UI框架&#xff0c;框架包含常…...

【AI】mcp server本质就是一个接口服务么

以下为元宝的回答&#xff1a; 你的理解非常准确&#xff01;​​MCP Server​​本质上是一个接口服务&#xff0c;但其设计目标、交互逻辑和使用场景与传统后端接口存在显著差异。以下是两者的对比分析&#xff1a; ​​1. 核心定位差异​​ ​​维度​​​​MCP Server​​…...

chalrs正常使用一段时间后开启代理访问网页 显示“不是私密链接”解决办法

chalrs正常使用一段时间后开启代理访问网页 显示“不是私密链接”解决办法 背景&#xff1a; charles用了好长时间了&#xff0c;最近发现打开charles有些软件无法上网&#xff0c;浏览器访问网页提示“您的连接不是私密链接”&#xff0c;按照网上的教程重装了几次证书&#x…...

如何通过DNS解析实现负载均衡?

在当今的互联网时代&#xff0c;随着网络应用的飞速发展&#xff0c;网站和各类在线服务面临着海量的用户请求。为了保障服务的高可用性和高性能&#xff0c;负载均衡技术应运而生。DNS&#xff08;域名系统&#xff09;负载均衡作为其中一种重要的实现方式&#xff0c;凭借其简…...

uni-app微信小程序登录流程详解

文章目录 uni-app微信小程序登录流程实战详解微信小程序登录流程概述1. 获取登录凭证&#xff08;code&#xff09;2. 发送登录请求3. 保存登录态4. 登录状态管理5. 应用登录状态请求拦截器中添加 token自动登录页面路由守卫 使用 Vuex 集中管理登录状态登录组件示例登录流程最…...