前言

还记得我们上次聊的PPO吗?那个被誉为"强化学习工业界标准"的算法,以其简单、稳定、高效的特点,几乎统治了从游戏AI到机器人控制的所有强化学习应用场景。

所有人都告诉你,PPO的成功归功于它那天才般的裁剪代理目标——用一个简单的min和clip函数,就完美解决了TRPO复杂的信任域约束问题。但今天,我要给你讲一个颠覆认知的故事:

PPO比TRPO好,根本不是因为裁剪机制!

2020年,MIT和Two Sigma的研究人员发表了一篇石破天惊的论文,他们通过严谨的实验发现:PPO相对于TRPO的性能优势,90%以上都来自于那些论文里一笔带过、甚至根本没提的"代码级优化"。而那个被吹上天的裁剪机制,其实可有可无。

这篇论文不仅揭开了PPO成功的真正秘密,更揭示了深度强化学习领域一个令人不安的真相:我们以为自己在做算法创新,其实很多时候只是在调代码的小细节。


论文信息

  • 标题:Implementation Matters in Deep Policy Gradients: A Case Study on PPO and TRPO
  • 会议:ICLR 2020
  • 单位:麻省理工学院、Two Sigma
  • 代码:github.com/MadryLab/implementation-matters
  • 论文:https://arxiv.org/pdf/2005.12729.pdf

1 问题背景:强化学习的"复现危机"

在深度强化学习领域,有一个公开的秘密:论文里的结果很难复现

你可能有过这样的经历:照着论文里的算法描述,一字不差地写了代码,调了无数次超参数,但性能就是达不到论文里的水平。甚至有时候,同一个算法,用不同的代码库实现,性能会差好几倍。

为什么会这样?

以前大家都觉得,是自己超参数调得不好,或者是框架的问题。但这篇论文的作者们提出了一个大胆的假设:真正决定性能的,不是论文里描述的核心算法,而是那些藏在代码里、被认为是"无关紧要"的小优化

为了验证这个假设,他们选择了两个最具代表性的策略梯度算法:PPO和TRPO,进行了一场深入骨髓的解剖。

通俗解释:这就像两家餐厅做同一道菜——红烧肉。A餐厅的厨师说,他的秘诀是用了一种特殊的酱油;B餐厅的厨师不信,照着做了,结果味道差远了。后来才发现,A餐厅的厨师在炖肉的时候,偷偷加了冰糖、八角、桂皮,还控制了火候和时间,而这些他都没告诉别人。


2 什么是"代码级优化"?

作者们仔细对比了OpenAI baselines中PPO的实现和原始PPO论文的描述,发现了9个在论文中几乎没有提到,但对性能至关重要的代码级优化。

这些优化看起来都是些微不足道的小细节,但组合起来,却能让PPO的性能提升一倍以上。

2.1 价值函数裁剪

原始PPO论文中,价值网络的损失函数就是简单的均方误差:
LV=(Vθt−Vtarg)2L^V = (V_{\theta_t} - V_{targ})^2LV=(VθtVtarg)2

公式符号全解释

  • LVL^VLV:价值网络的损失函数
  • VθtV_{\theta_t}Vθt:当前价值网络对状态sts_tst的估值
  • VtargV_{targ}Vtarg:状态sts_tst的目标回报值

但在实际实现中,PPO对价值网络也采用了类似策略网络的裁剪机制:
LV=max[(Vθt−Vtarg)2,(clip(Vθt,Vθt−1−ϵ,Vθt−1+ϵ)−Vtarg)2]L^V = max\left[ (V_{\theta_t} - V_{targ})^2, \left( clip(V_{\theta_t}, V_{\theta_{t-1}} - \epsilon, V_{\theta_{t-1}} + \epsilon) - V_{targ} \right)^2 \right]LV=max[(VθtVtarg)2,(clip(Vθt,Vθt1ϵ,Vθt1+ϵ)Vtarg)2]

公式符号全解释

  • Vθt−1V_{\theta_{t-1}}Vθt1:上一次迭代时价值网络对状态sts_tst的估值
  • ϵ\epsilonϵ:裁剪参数,通常和策略裁剪的ϵ\epsilonϵ相同,取0.2
  • maxmaxmax:取两个项的最大值

通俗解释:这就像给价值网络也加了一个"安全带",防止它在一次更新中变化太大。如果新的估值和旧的估值相差超过20%,我们就用裁剪后的值来计算损失。这样可以避免价值网络的剧烈波动,从而稳定整个训练过程。

2.2 奖励缩放

原始论文中,奖励是直接从环境中获取然后使用的。但在实际实现中,PPO会对奖励进行一种特殊的缩放:

  1. 维护一个滚动的折扣回报统计量
  2. 每次收到新的奖励rtr_trt,更新滚动统计量:Rt=γRt−1+rtR_t = \gamma R_{t-1} + r_tRt=γRt1+rt
  3. 将奖励除以滚动统计量的标准差:rt′=rt/std(R)r_t' = r_t / std(R)rt=rt/std(R)

通俗解释:不同的环境,奖励的尺度可能相差很大。比如在CartPole中,每步奖励是1;而在HalfCheetah中,每步奖励可能是几十甚至上百。奖励缩放可以自动将奖励调整到一个合适的范围,让学习率更容易选择。

2.3 正交初始化与层缩放

原始论文中,网络权重使用默认的Xavier初始化。但在实际实现中,PPO使用正交初始化,并且对最后一层的权重进行缩放。

正交初始化的好处是:

  • 可以保持梯度的范数在反向传播过程中不变
  • 避免梯度消失或爆炸
  • 提高训练的稳定性

2.4 Adam学习率退火

原始论文中,Adam的学习率是固定的。但在实际实现中,PPO会随着训练的进行,线性地降低学习率。

通俗解释:这就像开车,刚开始的时候油门踩大一点,开得快;快到目的地的时候,油门踩小一点,慢慢停。学习率退火可以让算法在训练后期更精细地调整参数,避免在最优点附近震荡。

2.5 其他重要优化

除了上面四个,还有五个同样重要的优化:

  1. 奖励裁剪:将奖励限制在一个固定范围内,通常是[-10, 10],防止异常奖励值破坏训练
  2. 观测归一化:将观测值归一化为均值为0、方差为1的向量
  3. 观测裁剪:将归一化后的观测值限制在[-10, 10]范围内
  4. tanh激活函数:在所有隐藏层使用tanh激活函数,而不是ReLU
  5. 全局梯度裁剪:将所有参数的梯度的L2范数限制在0.5以内,防止梯度爆炸

有趣的案例:OpenAI在训练Dota2 AI OpenAI Five的时候,使用了超过100个这样的代码级优化。这些优化加起来,让OpenAI Five的性能提升了一个数量级。而这些优化,几乎没有一个在论文里被详细描述过。


3 消融实验:哪些优化最重要?

为了搞清楚这些优化各自的贡献,作者们做了一个全面的消融实验。他们测试了所有24=162^4=1624=16种前四个优化的组合,在Humanoid-v2和Walker2d-v2两个任务上进行了训练。
在这里插入图片描述

【图片1 代码级优化的消融实验结果,出处:论文原文图1】

结果分析

  • 奖励归一化是最重要的优化,没有它,算法几乎无法学习
  • 正交初始化学习率退火也有非常显著的影响
  • 价值函数裁剪的影响相对较小,但仍然是正的
  • 所有优化都打开的组合,取得了最好的性能
  • 任何一个优化的缺失,都会导致性能的显著下降

这个实验清楚地表明:这些看似微不足道的代码级优化,才是PPO性能的真正来源。


4 惊天发现:代码优化 > 算法本身!

接下来,作者们做了一个最具颠覆性的实验:他们把PPO的所有代码级优化,原封不动地搬到了TRPO上,得到了一个新的算法——TRPO+

然后,他们对比了四个算法的性能:

  1. PPO:标准的PPO实现,包含所有代码级优化
  2. PPO-M:最小化的PPO实现,只保留核心的裁剪机制,去掉所有代码级优化
  3. TRPO:标准的TRPO实现,没有任何代码级优化
  4. TRPO+:TRPO的核心算法,加上PPO的所有代码级优化

【表格1 算法对比概览,出处:论文原文表1】

算法 核心步骤 使用PPO裁剪? 使用PPO优化?
PPO PPO
PPO-M PPO
TRPO TRPO
TRPO+ TRPO

【表格2 四个算法在三个任务上的性能对比,出处:论文原文表2】

算法 Walker2d-v2 Hopper-v2 Humanoid-v2
PPO 3292 [3157, 3426] 2513 [2391, 2632] 806 [785, 827]
PPO-M 2735 [2602, 2866] 2142 [2008, 2279] 674 [656, 695]
TRPO 2791 [2709, 2873] 2043 [1948, 2136] 586 [576, 596]
TRPO+ 3050 [2976, 3126] 2466 [2381, 2549] 1030 [979, 1083]

为了量化对比,作者们定义了两个指标:

  • 平均算法改进(AAI):在固定优化的情况下,切换算法带来的最大性能提升
  • 平均代码级改进(ACLI):在固定算法的情况下,加入代码优化带来的最大性能提升

AAI=max{∣PPO−TRPO+∣,∣PPO−M−TRPO∣}AAI = max\{ |PPO - TRPO+|, |PPO-M - TRPO| \}AAI=max{PPOTRPO+,PPOMTRPO}
ACLI=max{∣PPO−PPO−M∣,∣TRPO+−TRPO∣}ACLI = max\{ |PPO - PPO-M|, |TRPO+ - TRPO| \}ACLI=max{PPOPPOM,TRPO+TRPO}

结果分析

  • 在Walker2d-v2上,AAI=242,ACLI=557
  • 在Hopper-v2上,AAI=421,ACLI=99
  • 在Humanoid-v2上,AAI=224,ACLI=444

结论:在所有三个任务上,代码级改进都大于算法改进

这意味着:你选择用PPO还是TRPO,对你的最终性能影响很小;而你是否使用了这些代码级优化,才是决定性能的关键

更令人震惊的是,TRPO+的性能超过了标准的PPO!在Humanoid-v2任务上,TRPO+的得分是1030,而PPO只有806。这说明,如果给TRPO同样的优化,它的性能其实比PPO更好。


5 裁剪机制真的有用吗?

既然代码优化这么重要,那PPO最引以为傲的裁剪机制,到底还有没有用?

为了回答这个问题,作者们又做了一个实验:他们创建了一个PPO-NOCLIP算法,去掉了PPO的裁剪机制,但保留了所有其他代码级优化。

【表格3 PPO与PPO-NOCLIP的性能对比,出处:论文原文表3】

算法 Walker2d-v2 Hopper-v2 Humanoid-v2
PPO 3292 [3157, 3426] 2513 [2391, 2632] 806 [785, 827]
PPO-NOCLIP 2867 [2701, 3024] 2371 [2316, 2424] 831 [798, 869]

结果分析

  • 在Walker2d-v2和Hopper-v2上,PPO-NOCLIP的性能略低于标准PPO
  • 在Humanoid-v2上,PPO-NOCLIP的性能超过了标准PPO
  • 总体来说,两者的性能差距非常小,远小于代码优化带来的差距

这说明:裁剪机制确实有一定的作用,但它的重要性远不如那些代码级优化。甚至在某些任务上,没有裁剪机制的PPO,性能反而更好。

通俗解释:这就像你买了一辆跑车,商家告诉你,这辆车跑得快是因为它有一个特别的尾翼。但后来你发现,把尾翼拆了,车还是跑得很快;而如果你把轮胎换成普通的轮胎,车就跑不动了。原来,真正决定速度的是轮胎,而不是尾翼。


6 代码优化如何改变算法行为?

这些代码级优化不仅影响了最终的性能,还从根本上改变了算法的行为。

PPO的裁剪机制本来是为了限制新旧策略的概率比在[1−ϵ,1+ϵ][1-\epsilon, 1+\epsilon][1ϵ,1+ϵ]之间,从而保证策略更新不会太大。但作者们发现,在实际训练中,最大概率比经常远远超过这个范围
在这里插入图片描述

【图片2 不同算法的信任域行为对比,出处:论文原文图2】

结果分析

  • TRPO精确地控制了KL散度在设定的阈值附近
  • PPO和PPO-M的最大概率比都经常超过1+ϵ1+\epsilon1+ϵ,违反了裁剪的初衷
  • 但令人惊讶的是,PPO的平均KL散度反而被控制得很好,甚至比TRPO更稳定

这说明:PPO的裁剪机制并没有真正起到限制概率比的作用。实际上,是学习率退火、梯度裁剪等其他优化,共同起到了控制策略更新幅度的作用。


7 核心代码:包含所有优化的PPO实现

下面是一个完整的、包含了论文中所有9个代码级优化的PPO实现。这个实现可以达到和OpenAI baselines相当的性能。

import gym
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import numpy as np
from torch.distributions import Normal

# 超参数(完全按照论文中的设置)
GAMMA = 0.99
GAE_LAMBDA = 0.95
CLIP_EPS = 0.2
BATCH_SIZE = 64
UPDATE_EPOCHS = 10
HORIZON = 2048
LEARNING_RATE = 3e-4
ENTROPY_COEF = 0.0
VALUE_COEF = 0.5
MAX_GRAD_NORM = 0.5  # 全局梯度裁剪
REWARD_CLIP = (-10.0, 10.0)  # 奖励裁剪
OBS_CLIP = (-10.0, 10.0)  # 观测裁剪

# 设备配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 正交初始化(代码级优化3)
def orthogonal_init(layer, gain=np.sqrt(2)):
    nn.init.orthogonal_(layer.weight, gain=gain)
    nn.init.constant_(layer.bias, 0)
    return layer

# 策略-价值共享网络
class ActorCritic(nn.Module):
    def __init__(self, state_dim, action_dim, action_std=0.5):
        super(ActorCritic, self).__init__()
        # 共享特征层(使用tanh激活函数,代码级优化8)
        self.shared = nn.Sequential(
            orthogonal_init(nn.Linear(state_dim, 64)),
            nn.Tanh(),
            orthogonal_init(nn.Linear(64, 64)),
            nn.Tanh()
        )
        # 策略头(最后一层增益为0.01,层缩放)
        self.actor_mean = orthogonal_init(nn.Linear(64, action_dim), gain=0.01)
        self.actor_log_std = nn.Parameter(torch.zeros(1, action_dim))
        # 价值头(最后一层增益为1.0)
        self.critic = orthogonal_init(nn.Linear(64, 1), gain=1.0)
    
    def forward(self, x):
        x = self.shared(x)
        mean = self.actor_mean(x)
        log_std = self.actor_log_std.expand_as(mean)
        std = torch.exp(log_std)
        value = self.critic(x)
        return mean, std, value
    
    def get_action_and_value(self, x, action=None):
        mean, std, value = self(x)
        dist = Normal(mean, std)
        if action is None:
            action = dist.sample()
        log_prob = dist.log_prob(action).sum(-1)
        entropy = dist.entropy().sum(-1)
        return action, log_prob, entropy, value

# 运行统计量,用于观测和奖励归一化
class RunningMeanStd:
    def __init__(self, shape=()):
        self.mean = np.zeros(shape, np.float32)
        self.var = np.ones(shape, np.float32)
        self.count = 1e-4
    
    def update(self, x):
        batch_mean = np.mean(x, axis=0)
        batch_var = np.var(x, axis=0)
        batch_count = x.shape[0]
        
        delta = batch_mean - self.mean
        total_count = self.count + batch_count
        
        self.mean = self.mean + delta * batch_count / total_count
        m_a = self.var * self.count
        m_b = batch_var * batch_count
        M2 = m_a + m_b + np.square(delta) * self.count * batch_count / total_count
        self.var = M2 / total_count
        self.count = total_count

# 计算广义优势估计(GAE)
def compute_gae_and_returns(rewards, values, dones, next_value, next_done):
    advantages = np.zeros_like(rewards)
    last_advantage = 0
    for t in reversed(range(len(rewards))):
        if t == len(rewards) - 1:
            next_non_terminal = 1.0 - next_done
            next_val = next_value
        else:
            next_non_terminal = 1.0 - dones[t+1]
            next_val = values[t+1]
        
        delta = rewards[t] + GAMMA * next_val * next_non_terminal - values[t]
        last_advantage = delta + GAMMA * GAE_LAMBDA * next_non_terminal * last_advantage
        advantages[t] = last_advantage
    
    returns = advantages + values
    return advantages, returns

# 主函数
def main():
    env = gym.make("HalfCheetah-v4")
    state_dim = env.observation_space.shape[0]
    action_dim = env.action_space.shape[0]
    
    model = ActorCritic(state_dim, action_dim).to(device)
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE, eps=1e-5)
    
    # 初始化运行统计量(代码级优化2和6)
    obs_rms = RunningMeanStd(shape=state_dim)
    ret_rms = RunningMeanStd(shape=())
    
    # 初始化存储
    states = []
    actions = []
    log_probs = []
    rewards = []
    dones = []
    values = []
    
    state, _ = env.reset()
    done = False
    episode_reward = 0
    episode_count = 0
    total_steps = 0
    max_steps = 1000000
    
    # 学习率退火(代码级优化4)
    def update_learning_rate(step):
        frac = 1.0 - step / max_steps
        lr = frac * LEARNING_RATE
        for param_group in optimizer.param_groups:
            param_group['lr'] = lr
    
    while total_steps < max_steps:
        # 收集数据
        for _ in range(HORIZON):
            # 观测归一化和裁剪(代码级优化6和7)
            state_norm = np.clip((state - obs_rms.mean) / np.sqrt(obs_rms.var + 1e-8), *OBS_CLIP)
            state_tensor = torch.tensor(state_norm, dtype=torch.float32).unsqueeze(0).to(device)
            
            with torch.no_grad():
                action, log_prob, _, value = model.get_action_and_value(state_tensor)
            
            next_state, reward, done, truncated, _ = env.step(action.cpu().numpy()[0])
            done = done or truncated
            
            # 奖励裁剪(代码级优化5)
            reward = np.clip(reward, *REWARD_CLIP)
            
            # 存储数据
            states.append(state)
            actions.append(action.cpu().numpy()[0])
            log_probs.append(log_prob.cpu().numpy()[0])
            rewards.append(reward)
            dones.append(done)
            values.append(value.cpu().numpy()[0])
            
            episode_reward += reward
            state = next_state
            total_steps += 1
            
            if done:
                episode_count += 1
                if episode_count % 10 == 0:
                    print(f"Episode {episode_count} | 总步数: {total_steps} | 平均奖励: {episode_reward:.1f}")
                
                state, _ = env.reset()
                episode_reward = 0
        
        # 更新运行统计量
        obs_rms.update(np.array(states))
        
        # 奖励缩放(代码级优化2)
        rewards_np = np.array(rewards)
        for t in range(len(rewards_np)):
            ret_rms.update(np.array([rewards_np[t] + GAMMA * (0 if dones[t] else ret_rms.mean)]))
        rewards_scaled = rewards_np / np.sqrt(ret_rms.var + 1e-8)
        
        # 计算最后一个状态的价值
        next_state_norm = np.clip((next_state - obs_rms.mean) / np.sqrt(obs_rms.var + 1e-8), *OBS_CLIP)
        next_state_tensor = torch.tensor(next_state_norm, dtype=torch.float32).unsqueeze(0).to(device)
        with torch.no_grad():
            _, _, _, next_value = model.get_action_and_value(next_state_tensor)
        next_value = next_value.cpu().numpy()[0]
        
        # 转换为numpy数组
        states_np = np.array(states)
        actions_np = np.array(actions)
        log_probs_np = np.array(log_probs)
        dones_np = np.array(dones)
        values_np = np.array(values)
        
        # 归一化观测
        states_norm_np = np.clip((states_np - obs_rms.mean) / np.sqrt(obs_rms.var + 1e-8), *OBS_CLIP)
        
        # 计算优势和回报
        advantages_np, returns_np = compute_gae_and_returns(
            rewards_scaled, values_np, dones_np, next_value, done
        )
        
        # 转换为张量
        states_tensor = torch.tensor(states_norm_np, dtype=torch.float32).to(device)
        actions_tensor = torch.tensor(actions_np, dtype=torch.float32).to(device)
        old_log_probs_tensor = torch.tensor(log_probs_np, dtype=torch.float32).to(device)
        advantages_tensor = torch.tensor(advantages_np, dtype=torch.float32).to(device)
        returns_tensor = torch.tensor(returns_np, dtype=torch.float32).to(device)
        old_values_tensor = torch.tensor(values_np, dtype=torch.float32).to(device)
        
        # 标准化优势
        advantages_tensor = (advantages_tensor - advantages_tensor.mean()) / (advantages_tensor.std() + 1e-8)
        
        # 更新学习率
        update_learning_rate(total_steps)
        
        # 优化K个epoch
        for epoch in range(UPDATE_EPOCHS):
            # 随机打乱数据
            indices = torch.randperm(HORIZON)
            for start in range(0, HORIZON, BATCH_SIZE):
                end = start + BATCH_SIZE
                batch_indices = indices[start:end]
                
                # 获取小批量数据
                batch_states = states_tensor[batch_indices]
                batch_actions = actions_tensor[batch_indices]
                batch_old_log_probs = old_log_probs_tensor[batch_indices]
                batch_advantages = advantages_tensor[batch_indices]
                batch_returns = returns_tensor[batch_indices]
                batch_old_values = old_values_tensor[batch_indices]
                
                # 前向传播
                _, new_log_probs, entropy, new_values = model.get_action_and_value(
                    batch_states, batch_actions
                )
                
                # 计算概率比
                ratio = torch.exp(new_log_probs - batch_old_log_probs)
                
                # 计算策略裁剪损失
                surr1 = ratio * batch_advantages
                surr2 = torch.clamp(ratio, 1 - CLIP_EPS, 1 + CLIP_EPS) * batch_advantages
                policy_loss = -torch.min(surr1, surr2).mean()
                
                # 计算价值裁剪损失(代码级优化1)
                value_pred_clipped = batch_old_values + torch.clamp(
                    new_values.squeeze() - batch_old_values, -CLIP_EPS, CLIP_EPS
                )
                value_loss_unclipped = (new_values.squeeze() - batch_returns) ** 2
                value_loss_clipped = (value_pred_clipped - batch_returns) ** 2
                value_loss = 0.5 * torch.max(value_loss_unclipped, value_loss_clipped).mean()
                
                # 计算熵损失
                entropy_loss = entropy.mean()
                
                # 总损失
                loss = policy_loss + VALUE_COEF * value_loss - ENTROPY_COEF * entropy_loss
                
                # 反向传播和全局梯度裁剪(代码级优化9)
                optimizer.zero_grad()
                loss.backward()
                nn.utils.clip_grad_norm_(model.parameters(), MAX_GRAD_NORM)
                optimizer.step()
        
        # 清空存储
        states.clear()
        actions.clear()
        log_probs.clear()
        rewards.clear()
        dones.clear()
        values.clear()

if __name__ == "__main__":
    main()

代码亮点

  • 完整实现了论文中提到的所有9个代码级优化
  • 使用了正交初始化和层缩放,提高了训练稳定性
  • 实现了奖励和观测的归一化与裁剪
  • 加入了学习率退火和全局梯度裁剪
  • 实现了价值函数裁剪,这是很多PPO实现中漏掉的关键优化

8 总结与启示

这篇论文是近年来强化学习领域最重要的论文之一,它给我们带来了三个深刻的启示:

8.1 不要迷信"核心算法"

很多时候,我们以为算法的性能提升来自于某个天才的核心创新,但实际上,真正起决定作用的是那些看似微不足道的工程细节。在强化学习中,工程实现的重要性不亚于算法本身

8.2 复现论文结果的正确姿势

如果你复现论文结果不好,不要先怀疑自己的算法理解。先检查一下:

  • 你是否使用了和论文相同的网络结构和激活函数?
  • 你是否使用了相同的权重初始化方法?
  • 你是否对观测和奖励进行了归一化?
  • 你是否使用了梯度裁剪?
  • 你是否使用了学习率调度?

90%的复现问题,都出在这些细节上。

8.3 强化学习研究需要更严谨的实验

这篇论文也暴露了当前强化学习研究的一个严重问题:很多论文只强调核心算法的创新,而忽略了工程细节的影响。这导致很多研究结果无法复现,也阻碍了领域的进步。

未来的强化学习研究,应该更加注重实验的严谨性和可复现性。研究者应该公开完整的代码,详细描述所有的实现细节,并进行充分的消融实验,以证明核心算法的有效性。


9 写在最后

PPO的故事告诉我们:真正的大师,都注重细节

在机器学习领域,我们总是热衷于追逐新的算法、新的模型,以为掌握了这些就能解决所有问题。但实际上,那些看似平凡的工程细节,才是区分高手和新手的真正标准。

下次当你看到一篇论文宣称自己的算法比SOTA提升了多少的时候,不妨多问一句:如果给SOTA同样的代码优化,它还能赢吗?

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐