【COMA】一种将团队回报拆分为独立回报的多智能体算法

文章目录

      • 1. COMA 解决了什么问题(Motivation)
      • 2. COMA 怎么解决独立回报分配问题(Method)
        • 2.1 核心思想 counterfactual baseline 的提出
        • 2.2 算法大框架 —— 基于 AC 框架的 CTDE(Centralized Training Distributed Execution)模式
        • 2.3 Actor 网络设计
        • 2.4 Critic 网络设计
        • 2.5 训练流程(Update Parameters)
      • 3. COMA 效果

COMA 是一种基于 Policy-Based 的多智能体算法,其核心思想是在于 “独立回报分配”。当多个智能体在执行任务时,获得的奖励分数是由所有智能体的行为共同决定的,那么怎么把一个奖励分数合理的分配给每一个智能体呢?这就是 COMA 算法要解决的关键问题。在网络结构上,COMA 沿用了 Actor-Critic 架构,其中 Actor 使用基于 Policy-Based 的 RNN 网络。

1. COMA 解决了什么问题(Motivation)

COMA 是一种解决多智能体强化学习问题的算法,对于大多数多智能体强化学习问题(MARL)都面临着同样一个问题:信度分配(也叫回报分配)

这是指,当多个 Agent 在同时执行任务时,我们应该怎样合理的去评价每一个 Agent 的行为效用,举个例子:

假设我们现在正在训练一个算法模型,使用该算法模型去玩 MOBA 类游戏(DOTA 或者 LOL),算法模型需要同时操控 5 个英雄。在训练过程中遇到了这样一个情况:我方 3 个英雄迎面撞上了敌方 1 个英雄。此时,算法模型控制 1 号英雄和 2 号英雄对敌方英雄发起进攻,但却让 3 号英雄撤退。那么最终,因为 2 打 1 的局面,我方成功击败对方英雄,获得了 10 分的奖励分(Reward),那么我们该怎样为我方的这 3 个英雄进行奖励分配?

在上面案例中,我们很明显能看出,在人数占优势的情况下,算法选择让 1 号和 2 号英雄一起发起进攻是一次正确的尝试,而让 3 号英雄尝试撤退显然就不那么明智了。由于对 1 号和 2 号的正确决策,使得整个指挥策略得到了正向的奖励分(Positive Reward),但显然我们不能直接将这个正向奖励分同时应用到这 3 个英雄上。

我们希望被正确决策的英雄(1 号和 2 号)获得较高的奖励分,而被错误决策的英雄(3 号)获得负的惩罚分,即最后的期望得分可能为:1 号(8分),2 号(8分),3 号(-6分)。

三个英雄的得分总和加起来还是 10 分,只是每个英雄能够按照自己的实际情况获得对应的合理奖励分。

这就是 回报分配 的概念。

为了解决 Individual Reward Assignment 的问题,COMA 引入了一种叫做 “反事实准则(counterfactual baseline)” 的概念,这个概念是整篇论文的重点。

反事实准则提出,每个 agent 应该拥有自己独立的 reward,这样才能知道在这一次的全局行为决策中单个 agent 的 action 贡献是多少。即,通过 counterfactual baseline 来解决 “回报分配” 的问题。


2. COMA 怎么解决独立回报分配问题(Method)

2.1 核心思想 counterfactual baseline 的提出

在第一章节中我们提到,要想成功进行模型训练,我们就需要知道每个 agent 的独立回报究竟是多少。那么单个 agent 的 reward 该怎么去计算呢?

COMA 提出:可以将当前情况下的全局 reward 和将该 agent 行为替换为一个 “默认行为” 后的全局 reward 做差。我们可以这样理解:该回报值其实计算的是 Agent aaa 采取行为 uuu 会比采取默认行为 cac_aca 要更好(DaD^aDa > 0)还是更坏(DaD^aDa < 0)。

我们用 DaD^aDa 来表示单个 Agent 采取行为后的独立回报,计算公式如下:

Da=r(s,u)−r(s,(u−a,ca))D^a = r(s, \textbf{u}) - r(s, (\textbf{u}^{-a}, c_a)) Da=r(s,u)r(s,(ua,ca))

其中,u−a\textbf{u}^{-a}ua 代表联合动作空间除去当前Agent aaa 这一时刻采取的行为。(u−a,ca)(\textbf{u}^{-a}, c_a)(ua,ca) 代表当前Agent aaa 采取"默认行为" cac_aca 后所有Agent的联合动作空间。

上述 特定agent特定动作rewardDaD^aDa)就被称为 counterfactual baseline,COMA 使得每一个 agent 的每一个 action 都会被计算出一个 counterfactual baseline。我们将在后面的章节结合代码来讲解这个 counterfactual baseline 究竟怎么计算。

2.2 算法大框架 —— 基于 AC 框架的 CTDE(Centralized Training Distributed Execution)模式

和主流多智能体强化学习算法一样,COMA 也利用 Center-Critic 和 Actor 来实现 “中心式学习,分布式执行” 的模式。即,在训练阶段输入全局信息给中心式 Critic,通过 中心 Critic 教会 Actor 如何来进行行为决策。

但和传统 AC 或是 QMIX 不一样,COMA 中的 Critic 并不直接估计状态值函数 VVV 或是全局总回报 QtotQ_{tot}Qtot,而是去估计一个 agent 在指定状态下所有可执行行为的 Q 值,并通过这些 Q 值完成 counterfactual baseline 的计算。在 2.4 章中我会详细讲解计算过程。

算法网络结构如下:

下面我们分别来看看 Actor 和 Critic 的详细设计。

2.3 Actor 网络设计

COMA 中每一个 Agent 都由 RNN 网络控制,在训练时你可以为每一个 Agent 个体都训练一个独立的 RNN 网络,同样也可以所有 Agent 复用同一个 RNN 网络,这取决于你自己的设计。

RNN 网络一共包含 3 层,输入层(MLP)→ 中间层(GRU)→ 输出层(MLP),实现代码如下:

class RNN(nn.Module):# 所有 Agent 共享同一网络, 因此 input_shape = obs_shape + n_actions + n_agents(one_hot_code)def __init__(self, input_shape, args):super().__init__()self.fc1 = nn.Linear(input_shape, args.rnn_hidden_dim)self.rnn = nn.GRUCell(args.rnn_hidden_dim, args.rnn_hidden_dim)     # GRUCell(input_size, hidden_size)self.fc2 = nn.Linear(args.rnn_hidden_dim, args.n_actions)def forward(self, obs, hidden_state):x = F.relu(self.fc1(obs))h_in = hidden_state.reshape(-1, self.args.rnn_hidden_dim)h = self.rnn(x, h_in)                # GRUCell 的输入要求(current_input, last_hidden_state)q = self.fc2(h)                      # h 是这一时刻的隐状态,用于输到下一时刻的RNN网络中去,q 是真实行为Q值输出return q, h

2.4 Critic 网络设计

在 2.1 中我们提出了独立行为回报 DaD^aDa 的计算方法,计算公式为:

Da=r(s,u)−r(s,(u−a,ca))D^a = r(s, \textbf{u}) - r(s, (\textbf{u}^{-a}, c_a)) Da=r(s,u)r(s,(ua,ca))

但是,如果按公式,如果想计算出所有动作的DaD^aDa值,就需要将每个动作都替换成默认行为cac_aca去与环境互动一次得到最终结果,这样采样次数会非常多。此外,到底选择哪一个行为当作默认行为才是最合适的呢?很难决定。

为此,COMA 就提出了使用 Critic 网络来计算 DaD^aDa。既然我们无法决定哪一个行为作为 “默认行为” 是最好的,那我们就干脆取所有行为的 “平均效用值” 作为 “默认行为” 的行为效用值吧,这样就不用纠结选择哪一个行为最合适了,并且 “均值” 本身听起来就很符合 baseline 的概念嘛。

注意:我在这里的 “平均效用” 上打了引号,准确来说应该是所有行为效用的 “期望”,即 行为效用 * 行为概率,而非简单的求平均。

这样一来,默认行为行为效用计算公式就变成了:

Q(s,ca)=∑ua′πa(u′a∣τa)Q(s,(u−a,u′a))Q(s, c_a) = \sum_{u_a'}\pi^a(u'^a|\tau^a)Q(s, (\textbf{u}^{-a}, u'^a)) Q(s,ca)=uaπa(uaτa)Q(s,(ua,ua))

那么我们就可以将 DaD^aDa 等效近似于 R(s,u)R(s, \textbf{u})R(s,u) 啦:

Ra(s,u)=Q(s,u)−∑ua′πa(u′a∣τa)Q(s,(u−a,u′a))R^a(s, \textbf{u}) = Q(s, \textbf{u}) - \sum_{u_a'}\pi^a(u'^a|\tau^a)Q(s, (\textbf{u}^{-a}, u'^a)) Ra(s,u)=Q(s,u)uaπa(uaτa)Q(s,(ua,ua))

说了这么多,好像根本没有跟中心 Critic 扯上啥关系咧?别急,现在就有关系了。

还记得我们要计算 Q(s,ca)Q(s, c_a)Q(s,ca) 吧?我们需要把一个 Agent 所有的行为的 Q 值拿出来乘以对应行为的选择概率才是最后的结果。行为概率简单,我们 Actor 网络最后的输出值走一遍 softmax 层就好了,但是行为的 Q 值怎么得到呢?

这时候就需要 Critic 出场了。

COMA 中的 Critic 网络接收 5 个输入:当前全局状态,当前 agent 的观测,除自身外其他 agent 的联合行为,自身 agent 的 one-hot 编码,所有 agent 上一时刻的行为。

输出:当前 agent 所有可执行行为的 Q 值。

具体对应这张图:

这样一来,我们每个 agent 对应的所有行为的 Q 值就能拿到了,再乘上 softmax 输出的行为概率,就得到 Q(s,ca)Q(s, c_a)Q(s,ca) 啦。

Critic 网络代码如下:

class ComaCritic(nn.Module):def __init__(self, input_shape, arglist):"""输入:当前的状态、当前agent的obs、其他agent执行的动作、当前agent的编号对应的one-hot向量、所有agent上一个timestep执行的动作输出:当前agent的所有可执行动作对应的联合Q值,一个n_actions维向量"""super(ComaCritic, self).__init__()self.arglist = arglistself.fc1 = nn.Linear(input_shape, arglist.critic_dim)self.fc2 = nn.Linear(arglist.critic_dim, arglist.critic_dim)self.fc3 = nn.Linear(arglist.critic_dim, self.arglist.n_actions)def forward(self, inputs):x = F.relu(self.fc1(inputs))x = F.relu(self.fc2(x))q = self.fc3(x)return q

2.5 训练流程(Update Parameters)

COMA 一共存在 2 个网络,Actor 和 Critic,因此整个模型训练中也包括了对应的 2 个模型更新过程。

我们先来看 COMA 更新函数的代码:

    def learn(self, batch, max_episode_len, train_step, epsilon):...# 计算每个 agent 的 Q 值更新 Criticq_values = self._train_critic(batch, max_episode_len, train_step)  # 训练critic网络,并且得到每个agent的所有动作的Q值action_prob = self._get_action_prob(batch, max_episode_len, epsilon)  # 走一遍softmax得到每个agent的所有动作的概率q_taken = torch.gather(q_values, dim=3, index=u).squeeze(3)  # 每个agent的选择的动作对应的Q值pi_taken = torch.gather(action_prob, dim=3, index=u).squeeze(3)  # 每个agent的选择的动作对应的概率log_pi_taken = torch.log(pi_taken)# 计算 baseline 和 R(s, u) 更新 actorbaseline = (q_values * action_prob).sum(dim=3, keepdim=True).squeeze(3).detach()advantage = (q_taken - baseline).detach()loss = - (advantage * log_pi_taken).sum()self.rnn_optimizer.zero_grad()loss.backward()torch.nn.utils.clip_grad_norm_(self.rnn_parameters, self.arglist.grad_norm_clip)self.rnn_optimizer.step()...

从上述代码可以看出,整个 learn 函数包含了两次 backward(),一次是 critic 网络的反向更新,一次是 actor 网络的反向更新。下面我们对这两个网络的更新分别做讲解。

Critic 网络更新

Critic 网络的更新同样使用 TD-Error 的方式进行更新,只是采用的是 TD(λ\lambdaλ) 的形式,Loss 函数如下:

Loss=(y(λ)−fθc(⋅))2Loss = (y^{(\lambda)} - f^{\theta^c}(·)) ^ 2 Loss=(y(λ)fθc())2

其中:

y(λ)=(1−λ)∑n=1∞λn−1Gt(n)Gt(n)=∑l=1nγl−1rt+l+γnfθc(⋅)y^{(\lambda)} = (1-\lambda)\sum_{n=1}^\infty \lambda^{n-1}G_t^{(n)} \\ G_t^{(n)} = \sum_{l=1}^n\gamma^{l-1}r_{t+l} + \gamma^nf^{\theta^c}(·) y(λ)=(1λ)n=1λn1Gt(n)Gt(n)=l=1nγl1rt+l+γnfθc()

也就是说,Loss 函数公式可以表示为:

Loss=((1−λ)∑n=1∞λn−1(∑l=1nγl−1rt+l+γnf(⋅))−f(⋅))2Loss = ((1-\lambda)\sum_{n=1}^\infty \lambda^{n-1}(\sum_{l=1}^n\gamma^{l-1}r_{t+l} + \gamma^n {\color{red}f(·)}) - f(·)) ^ 2 Loss=((1λ)n=1λn1(l=1nγl1rt+l+γnf())f())2

其中,红色部分的 f() 是使用 target critic network 预测的值,后一个 f() 是使用 evaluate critic network 预测的值。

整个 Critic 更新代码如下所示:

    def _train_critic(self, batch, max_episode_len, train_step):...# 得到每个 agent 对应的Q值,维度为 (episode个数, max_episode_len, n_agents,n_actions)# q_next_target为下一个状态-动作对应的 target 网络输出的Q值,没有包括rewardq_evals, q_next_target = self._get_q_values(batch, max_episode_len)q_values = q_evals.clone()  # 在函数的最后返回,用来计算advantage从而更新actor# 取每个agent动作对应的Q值,并且把最后不需要的一维去掉,因为最后一维只有一个值了q_evals = torch.gather(q_evals, dim=3, index=u).squeeze(3)q_next_target = torch.gather(q_next_target, dim=3, index=u_next).squeeze(3)targets = td_lambda_target(batch, max_episode_len, q_next_target.cpu(), self.arglist)td_error = targets.detach() - q_evalsloss = (masked_td_error ** 2).sum() / mask.sum()self.critic_optimizer.zero_grad()loss.backward()return q_values

Actor 网络更新

由于 COMA 是基于 Policy-Based 的方法,为此 Actor 更新使用传统的 PB 更新公式:

g=▽θπlogπ(u∣τta)A(s,u)g = \bigtriangledown_{\theta_\pi}log\pi(u|\tau_t^a)A(s, u) g=θπlogπ(uτta)A(s,u)

其中,这里的 A(s,u)A(s, u)A(s,u) 使用 COMA 提出的独立回报计算获得,使用 DaD^aDaR(s,u)R(s, u)R(s,u) 来进行等效代替。

此时,梯度(Loss)的计算公式变成如下所示:

g=▽θπlogπ(u∣τta)(Q(s,u)−∑ua′πa(u′a∣τa)Q(s,(u−a,u′a)))g = \bigtriangledown_{\theta_\pi}log\pi(u|\tau_t^a)(Q(s, \textbf{u}) - \sum_{u_a'}\pi^a(u'^a|\tau^a)Q(s, (\textbf{u}^{-a}, u'^a))) g=θπlogπ(uτta)(Q(s,u)uaπa(uaτa)Q(s,(ua,ua)))

actor 更新代码如下:

 # 计算 baseline 和 R(s, u) 更新 actorbaseline = (q_values * action_prob).sum().detach()advantage = (q_taken - baseline).detach()loss = - (advantage * log_pi_taken).sum()self.rnn_optimizer.zero_grad()loss.backward()

3. COMA 效果

实验结果如下图所示,其中3m,5m分别指代一个作战小队中包含3个,5个marine(一种兵种);2d_3z指代一个作战小队中包含2条龙和3个狂热者。


COMA 论文链接:https://arxiv.org/pdf/1705.08926.pdf
COMA 实现代码:https://github.com/oxwhirl/smac

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/290308.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#解析Markdown文档,实现替换图片链接操作

前言又是好久没写博客了其实也不是没写&#xff0c;是最近在「做一个博客」&#xff0c;从2月21日开始&#xff0c;大概一个多星期的时间&#xff0c;疯狂刷进度&#xff0c;边写代码边写了一整系列的博客开发笔记&#xff0c;目前为止已经写了16篇了&#xff0c;然后上3月之后…

LoadRunner测试下载功能点脚本(方法一)

性能需求&#xff1a;对系统某页面中&#xff0c;点击下载功能做并发测试&#xff0c;以获取在并发下载文件的情况下系统的性能指标。 备注&#xff1a;页面上点击下载时的文件可以是word、excel、pdf等。 问题1&#xff1a;录制完下载的场景后&#xff0c;发现脚本里面并没有包…

海南橡胶机器人成本_「图说」海垦看点:海南橡胶联合北京理工华汇智能科技首创我国林间智能割胶机器人...

1 海垦南繁产业集团长期以来高度重视改善职工居住条件&#xff0c;于去年启动了海燕队保障性住房项目&#xff0c;项目建成后将有效解决职工住房问题。图为近日正在加紧施工的建设工地。 蒙胜国 摄2 海南橡胶联合北京理工华汇智能科技有限公司&#xff0c;研发出来的最新一代林…

数据挖掘在轨迹信息上的应用实验

文章目录1. 实验概览2. 数据集下载3. 数据预处理3.1 异常点去除3.2 停留点检测与环绕点检测3.3 轨迹分段4. 基于轨迹信息的数据挖掘4.1 路口检测4.1.1 地图分割与轨迹点速度计算4.2 偏好学习通常&#xff0c;我们将一个连续的GPS信号点序列称为一个轨迹&#xff08;Trajectory&…

Avalonia跨平台入门第二十三篇之滚动字幕

在前面分享的几篇中咱已经玩耍了Popup、ListBox多选、Grid动态分、RadioButton模板、控件的拖放效果、控件的置顶和置底、控件的锁定、自定义Window样式、动画效果、Expander控件、ListBox折叠列表、聊天窗口、ListBox图片消息、窗口抖动、语音发送、语音播放、语音播放问题、玩…

oracle dba 手动创建数据实例

2019独角兽企业重金招聘Python工程师标准>>> 1.手动建库大致步骤 设置环境变量.bash_profile创建目录结构创建参数文件(位置:$ORACLE_HOME/dbs)生成密码文件执行建库脚本创建数据字典其他设置2.DBCA 脚本创建 2.1设置系统环境变量 ORACLE_HOME/app/oracle/11g/11.2.…

asp 强制转换浮点数值_C/C++中浮点数的编码存储

浮点数也称做实型数据(实数)&#xff0c;形式上就是数学中的小数。浮点型数据有两种表达方式&#xff1a; 一种是用数字和小数点表示的&#xff0c;如123.456&#xff1b; 另一种是用指数方式表示&#xff0c;如1.2e-6 或1.2E-6(1.2*10-6)。在计算机中实数是如何存储的呢&#…

PaddleNLP实战——信息抽取(InfoExtraction)

[ 文章目录 ]1. 信息抽取任务是什么&#xff1f;2. 基于PaddleNLP的信息抽取任务2.1 训练任务概览2.2 Predicate列表2.3 SPO列表2.4 代码解析1. 信息抽取任务是什么&#xff1f; 在NLP任务中&#xff0c;通常当我们拿到一段文本时&#xff0c;我们希望机器去理解这段文本描述的…

ThinkPad X220i 刷白名单BIOS,改装第三方无线网卡

ThinkPad X220i自带的网卡是REALTEK RTL8188CE&#xff0c;这张卡在Mac下目前是无解的.国外网站有该卡liunx、unix内核的驱动&#xff0c;但还是没有高人编译出来. 不等了,这卡没戏.正好手边有一台Dell E6400,E6400的无线网卡是DELL Wireless 1397 WLAN Mini-Card,具体型号是&a…

C# 离线人脸识别 ArcSoft

人脸识别&比对发展到今天&#xff0c;已经是一个非常成熟的技术了&#xff0c;而且应用在生活的方方面面&#xff0c;比如手机、车站、天网等。虹软人脸识别服务是免费的。最重要的是它还支持离线识别&#xff0c;并且提供Android、iOS、C、C#版SDK&#xff0c;现在已经升级…

【mongoDB运维篇③】replication set复制集

介绍 replicattion set 多台服务器维护相同的数据副本,提高服务器的可用性,总结下来有以下好处: 数据备份与恢复读写分离MongoDB 复制集的结构以及基本概念 正如上图所示&#xff0c;MongoDB 复制集的架构中&#xff0c;主要分为两部分&#xff1a;主节点&#xff08;Primary&a…

c++ long 转 short_C精品编程之——C语言的数据类型、运算符、表达式,精品课程...

在前边的文章分享中&#xff0c;我们已经看到程序中使用的各种变量都应预先加以说明&#xff0c;即先说明&#xff0c;后使用。对变量的说明可以包括三个方面&#xff1a;数据类型存储类型作用域在本课中&#xff0c;我们只介绍数据类型说明。其它说明在以后各章中陆续介绍。所…

李宏毅Reinforcement Learning强化学习入门笔记

文章目录Concepts in Reinforcement LearningDifficulties in RLA3C Method Brief IntroductionPolicy-based Approach - Learn an Actor (Policy Gradient Method)1. Decide Function of Actor Model (NN? ...)2. Decide Goodness of this Function3. Choose the best functi…

《BI项目笔记》数据源视图设置

原文:《BI项目笔记》数据源视图设置目的数据源视图是物理源数据库和分析维度与多维数据集之间的逻辑数据模型。在创建数据源视图时&#xff0c;需要在源数据库中指定包含创建维度和多维数据集所需要的数据表格和视图。BIDS与数据库连接&#xff0c;读取表格和视图定义&#xff…

201521123070 《JAVA程序设计》第13周学习总结

1. 本章学习总结 以你喜欢的方式&#xff08;思维导图、OneNote或其他&#xff09;归纳总结多网络相关内容。 2. 书面作业 Q1. 网络基础 1.1 比较ping www.baidu.com与ping cec.jmu.edu.cn&#xff0c;分析返回结果有何不同&#xff1f;为什么会有这样的不同&#xff1f; 1.2 t…

.NET 7 预览版2 的亮点之 NativeAOT 回顾

.NET 中备受追捧和期待已久的功能NativeAOT终于出现在本周的.NET 7 预览版2中&#xff0c;该项目的工作仍在继续&#xff0c;该版本将 NativeAOT 从实验性的 dotnet/runtimelab repo 中移出合并进入稳定的运行时库 dotnet/runtime repo&#xff0c;但尚未在 dotnet SDK 中添加足…

c语言十佳运动员有奖评选系统_2019年沃德十佳内饰解读

​2019年沃德十佳内饰解读​mp.weixin.qq.com在这个世界上&#xff0c;要判定一件事物的成功与否并不容易&#xff0c;仅靠主观判断远远不够&#xff0c;而是需要能够进行量化判断的标准和成果。正如运动员需要金牌和冠军积淀&#xff0c;导演和演员需要奖项傍身一样&#xff0…

Mybatis——返回类型为 集合嵌套集合 应该如何处理

2019独角兽企业重金招聘Python工程师标准>>> 最近在练习时 遇到了类似于 企鹅里的好友分组功能&#xff0c;使用的持久层框架是mybatis 第一次处理这种关系 记录一下 备忘。。 首先是表结构&#xff1a; <user_group > 好友分组 、 <t_group> 用户与好友…

为什么用 windbg 看 !address 显示出的Free是128T 大小?

总是被朋友问&#xff0c;用 !address -summary 显示出上面的 Free ≈ 128T 到底是什么意思&#xff1f;我的空闲内存不可能有这么大,不管怎么说&#xff0c;先上命令。0:009> !address -summary--- Usage Summary ---------------- RgnCount ----------- Total Size ------…

DeepMind 的马尔可夫决策过程(MDP)课堂笔记

DeepMind Teaching by David Silver 视频课程链接&#xff08;Youtube资源&#xff0c;需梯子&#xff09;&#xff1a;https://youtu.be/lfHX2hHRMVQ 文章目录DeepMind Teaching by David Silver1. 马尔可夫过程&#xff08;Markov Processes&#xff09;2. 马尔可夫回报过程…