MetricsLogger API#
MetricsLogger 允许 RLlib 实验跟踪指标。RLlib 中的大多数组件(例如:EnvRunner、Learner)都包含一个可以记录的 MetricsLogger 实例。所有记录的指标都会被汇总到位于 Algorithm 对象内部的根 MetricsLogger。该根 MetricsLogger 用于将指标报告给用户或 Ray Tune。当子组件沿层级向下报告指标时,它会“缩减”记录的结果,然后再发送。例如,通过求和进行缩减时,子组件会先计算总和,然后再将其发送给父组件。
我们建议使用此 API 来记录您希望 RLlib 报告的任何指标,尤其是当它们应该报告给 Ray Tune 或 WandB 时。要快速了解 RLlib 如何使用 MetricsLogger,请查看基于 EnvRunner 的 回调、自定义损失函数 或自定义 training_step 实现。
如果您的目标是在 RLlib 组件之间通信数据(例如,将损失从 Learner 传达给 EnvRunner),我们建议通过回调或覆盖 RLlib 组件的属性来传递这些值。这主要是因为 MetricsLogger 的设计目的是汇总指标,而不是随时随地提供它们,因此从中查询已记录的指标可能会导致意外结果。
RLlib 的 MetricsLogger 汇总概述:该图说明了并行组件中记录的指标如何汇总到根 MetricsLogger。 Algorithm 的并行子组件拥有自己的 MetricsLogger 实例,并使用它来本地记录值。当一个组件完成一个独立的任务时,例如,一个 EnvRunner 完成一个采样请求时,子组件(EnvRunner 或 Learner)的本地指标会被“缩减”,并向下游发送到根组件(Algorithm)。父组件将接收到的结果合并到其自己的 MetricsLogger 中。一旦 Algorithm 完成其自身的周期(step() 返回),它也会进行“缩减”以最终报告给用户或 Ray Tune。
MetricsLogger 的功能#
MetricsLogger API 提供以下功能
随时间记录标量值,例如损失、单个奖励或回合回报。
配置不同的缩减类型,特别是
ema(指数移动平均)、mean(平均)、min(最小值)、max(最大值)或sum(总和)。此外,用户可以通过使用item或item_series来选择不进行任何缩减,从而保持记录的值不变。指定缩减发生的滑动窗口,例如
window=100以对每个并行组件最近 100 个记录值进行平均。或者,指定指数移动平均 (EMA) 系数。通过方便的
with MetricsLogger.log_time(...)块记录不同代码块的执行时间。通过在记录值时设置
reduce="lifetime_sum"来累加生命周期总和。对于总和和生命周期总和,您还可以沿途计算相应的每秒吞吐量指标。
MetricsLogger 的内置用法#
RLlib 在现有代码库中广泛使用 MetricsLogger API。以下是由此产生的典型信息流的概述
每个
EnvRunner通过在其 RL 环境 中执行 step 来收集训练数据,并向其MetricsLogger记录标准统计信息,例如回合回报或回合长度。每个
EnvRunner缩减所有收集到的指标,并将它们返回给 Algorithm。Algorithm汇总来自 EnvRunner 的n块指标(这取决于选择的缩减方法,例如,如果 reduce="mean" 则为平均)。每个
Learner在执行模型更新的同时,将指标记录到其MetricsLogger中,例如总损失或平均梯度。每个
Learner缩减所有收集到的指标,并将它们返回给 Algorithm。Algorithm汇总来自 Learner 的m块指标(这同样取决于选择的缩减方法,例如,如果 reduce="sum" 则为求和)。Algorithm可能会在其自己的MetricsLogger实例中添加标准指标,例如并行采样请求的平均时间。Algorithm缩减所有收集到的指标,并将它们返回给用户或 Ray Tune。
MetricsLogger API 详解#
RLlib 的 MetricsLogger API:这是 RLlib 使用 MetricsLogger API 来记录和汇总指标的方式。我们使用 log_time() 和 log_value() 方法来记录指标。然后使用 reduce() 方法缩减指标。缩减后的指标使用 aggregate() 方法进行汇总。所有指标最终由 Algorithm 对象缩减并报告给用户或 Ray Tune。
记录标量值#
要在您的 MetricsLogger 中以某个字符串键记录一个标量值,请使用 log_value() 方法。
from ray.rllib.utils.metrics.metrics_logger import MetricsLogger
logger = MetricsLogger()
# Log a scalar float value under the `loss` key. By default, all logged
# values under that key are averaged, once `reduce()` is called.
logger.log_value("loss", 0.01, reduce="mean", window=2)
默认情况下,MetricsLogger 通过平均值来缩减值(reduce="mean")。
其他可用的缩减方法可以在字典 ray.rllib.utils.metrics.metrics_logger.DEFAULT_STATS_CLS_LOOKUP 中找到。
注意
您还可以通过扩展 ray.rllib.utils.metrics.metrics_logger.DEFAULT_STATS_CLS_LOOKUP 并将其传递给 reporting() 来提供自己的缩减方法。这些新的缩减方法将在运行时记录值时通过它们的键可用。例如,当使用键 "my_custom_reduce_method" 扩展字典并将其传递给 reporting() 时,您可以使用 reduce="my_custom_reduce_method"。
指定 window 会导致缩减发生在最近 window 个记录值上。例如,您可以继续在 loss 键下记录新值。
logger.log_value("loss", 0.02, reduce="mean", window=2)
logger.log_value("loss", 0.03, reduce="mean", window=2)
logger.log_value("loss", 0.04, reduce="mean", window=2)
logger.log_value("loss", 0.05, reduce="mean", window=2)
由于您指定了窗口大小为 2,MetricsLogger 只使用最后 2 个值来计算缩减结果。您可以通过 peek() 方法“查看”当前缩减的结果。
# Peek at the current, reduced value.
# Note that in the underlying structure, the internal values list still
# contains all logged values: 0.01, 0.02, 0.03, 0.04, and 0.05.
print(logger.peek("loss")) # Expect: 0.045, which is the average over the last 2 values
peek() 方法允许您查看某个键当前底层缩减的结果,而无需实际调用 reduce()。
警告
查看指标的一个限制是,当指标在下游聚合时,您通常无法有意义地查看它们。例如,如果您在每次调用 update() 时记录训练的步数,这些步数将被 Algorithm 的 MetricsLogger 缩减和聚合,而在 Learner 内部查看它们将不会得到聚合结果。
而不是提供一个扁平的键,您也可以通过传递一个元组来在某个嵌套键下记录一个值。
# Log a value under a deeper nested key.
logger.log_value(("some", "nested", "key"), -1.0)
print(logger.peek(("some", "nested", "key"))) # expect: -1.0
要使用除“mean”以外的缩减方法,请在 log_value() 调用中指定 reduce 参数。
# Log a maximum value.
logger.log_value(key="max_value", value=0.0, reduce="max")
最大值将在每次 reduce() 操作后重置。
for i in range(1000, 0, -1):
logger.log_value(key="max_value", value=float(i))
logger.peek("max_value") # Expect: 1000.0, which is the lifetime max (infinite window)
您也可以选择不进行任何缩减,而只是收集单独的值,例如您从环境中随时间接收到的一组图像,对于这些图像进行任何方式的缩减都没有意义。为此,请使用 reduce="item" 或 reduce="item_series" 参数。但是,请谨慎选择您记录的内容,因为 RLlib 将报告所有记录的值,除非您自己清理它们。
logger.log_value("some_items", value="a", reduce="item_series")
logger.log_value("some_items", value="b", reduce="item_series")
logger.log_value("an_item", value="c", reduce="item")
logger.log_value("an_item", value="d", reduce="item")
logger.peek("some_items") # expect a list: ["a", "b"]
logger.peek("an_item") # expect a string: "d"
logger.reduce()
logger.peek("some_items") # expect an empty list: []
logger.peek("an_item") # expect None: []
记录非标量数据#
警告
您可能会想使用 MetricsLogger 作为一种工具,将数据从 RLlib 的一个地方传递到另一个地方。例如,在 EnvRunner 回调之间存储数据,或将从环境中捕获的视频从 EnvRunner 移到 Algorithm 对象。这些情况需要格外小心,我们通常建议寻找其他解决方案。例如,回调可以在 EnvRunner 上创建自定义属性,您可能不希望将视频像指标那样处理。MetricsLogger 被 RLlib 设计并视为收集并行组件的指标并对其进行汇总的工具。它应该处理指标,并且这些指标应该单向流动——从并行组件到根组件。如果您的用例不符合此模式,请考虑使用 MetricsLogger 以外的其他方式。
MetricsLogger 不仅限于标量值。如果您决定仍然希望使用 MetricsLogger 将数据从 RLlib 的一个地方传递到另一个地方,您可以使用它来记录图像、视频或任何其他复杂数据。
例如,要记录来自 CartPole 环境的三个连续图像帧,请使用 reduce="item_series" 参数。
import gymnasium as gym
env = gym.make("CartPole-v1")
# Log three consecutive render frames from the env.
env.reset()
logger.log_value("some_images", value=env.render(), reduce="item_series")
env.step(0)
logger.log_value("some_images", value=env.render(), reduce="item_series")
env.step(1)
logger.log_value("some_images", value=env.render(), reduce="item_series")
计时器#
您可以将 MetricsLogger 用作上下文管理器来记录计时器结果。您可以通过一条 with MetricsLogger.log_time(...) 行来计时自定义代码中的所有代码块。
import time
from ray.rllib.utils.metrics.metrics_logger import MetricsLogger
logger = MetricsLogger()
# First delta measurement:
with logger.log_time("my_block_to_be_timed", reduce="ema", ema_coeff=0.1):
time.sleep(1.0)
# EMA should be ~1sec.
assert 1.1 > logger.peek("my_block_to_be_timed") > 0.9
# Second delta measurement:
with logger.log_time("my_block_to_be_timed"):
time.sleep(2.0)
# EMA should be ~1.1sec.
assert 1.15 > logger.peek("my_block_to_be_timed") > 1.05
计数器#
如果您想计数,例如在采样阶段执行的环境步数,并将这些计数在生命周期或某个特定阶段内累加,请在调用 log_value() 时使用 reduce="sum" 或 reduce="lifetime_sum" 参数。
from ray.rllib.utils.metrics.metrics_logger import MetricsLogger
logger = MetricsLogger()
logger.log_value("my_counter", 50, reduce="sum")
logger.log_value("my_counter", 25, reduce="sum")
logger.peek("my_counter") # expect: 75
logger.reduce()
logger.peek("my_counter") # expect: 0 (upon reduction, all values are cleared)
如果您使用 reduce="lifetime_sum" 记录生命周期指标,这些指标将在实验的整个生命周期内累加,甚至在从检查点恢复后也会如此。请注意,您无法有意义地在根 MetricsLogger 之外查看 lifetime_sum 值。另外请注意,生命周期总和是在根 MetricsLogger 中累加的,而我们在并行组件中只保留最近的值,这些值在每次缩减时都会被清除。
吞吐量测量#
使用 reduce="sum" 或 reduce="lifetime_sum" 设置记录的指标也可以测量吞吐量。吞吐量按每个指标报告周期计算一次。这意味着吞吐量始终相对于指标缩减周期的速度。
您可以通过传递 throughput=True 标志,使用 peek() 方法来访问吞吐量值。
import time
from ray.rllib.utils.metrics.metrics_logger import MetricsLogger
logger = MetricsLogger(root=True)
for _ in range(3):
logger.log_value("lifetime_sum", 5, reduce="sum", with_throughput=True)
time.sleep(1.0)
# Expect the throughput to be roughly 15/sec.
print(logger.peek("lifetime_sum", throughput=True))
示例 1:如何在 EnvRunner 回调中使用 MetricsLogger#
要演示如何在 EnvRunner 中使用 MetricsLogger,请查看此端到端示例,该示例利用了 RLlibCallback API 将自定义代码注入 RL 环境循环。
该示例计算了 Acrobot-v1 RL 环境 的平均“首次关节角度”,并通过 MetricsLogger API 记录结果。
请注意,此示例与此处描述的示例相同,但重点已转移到仅解释代码的 MetricsLogger 方面。
import math
import numpy as np
from ray.rllib.algorithms.ppo import PPOConfig
from ray.rllib.callbacks.callbacks import RLlibCallback
# Define a custom RLlibCallback.
class LogAcrobotAngle(RLlibCallback):
def on_episode_created(self, *, episode, **kwargs):
# Initialize an empty list in the `custom_data` property of `episode`.
episode.custom_data["theta1"] = []
def on_episode_step(self, *, episode, env, **kwargs):
# Compute the angle at every episode step and store it temporarily in episode:
state = env.envs[0].unwrapped.state
deg_theta1 = math.degrees(math.atan2(state[1], state[0]))
episode.custom_data["theta1"].append(deg_theta1)
def on_episode_end(self, *, episode, metrics_logger, **kwargs):
theta1s = episode.custom_data["theta1"]
avg_theta1 = np.mean(theta1s)
# Log the resulting average angle - per episode - to the MetricsLogger.
# Report with a sliding window of 50.
metrics_logger.log_value("theta1_mean", avg_theta1, reduce="mean", window=50)
config = (
PPOConfig()
.environment("Acrobot-v1")
.callbacks(
callbacks_class=LogAcrobotAngle,
)
)
ppo = config.build()
# Train n times. Expect `theta1_mean` to be found in the results under:
# `env_runners/theta1_mean`
for i in range(10):
results = ppo.train()
print(
f"iter={i} "
f"theta1_mean={results['env_runners']['theta1_mean']} "
f"R={results['env_runners']['episode_return_mean']}"
)
还可以查看这个更复杂的示例,关于如何生成 PacMan 热力图(图像)并将其记录到 WandB。
示例 2:如何在自定义损失函数中使用 MetricsLogger#
您可以在自定义损失函数中记录指标。为此,请使用 Learner 自身的 Learner.metrics 属性。
@override(TorchLearner)
def compute_loss_for_module(self, *, module_id, config, batch, fwd_out):
...
loss_xyz = ...
# Log a specific loss term.
# Each learner will sum up the loss_xyz value and send it to the root MetricsLogger.
self.metrics.log_value("special_loss_term", reduce="sum", value=loss_xyz)
total_loss = loss_abc + loss_xyz
return total_loss
请查看这个正在运行的关于在损失函数中记录自定义值的端到端示例。
示例 3:如何在自定义 Algorithm 中使用 MetricsLogger#
您可以在自定义 Algorithm 的 training_step() 方法中记录指标。为此,请使用 Algorithm 自身的 Algorithm.metrics 属性。
@override(Algorithm)
def training_step(self) -> None:
...
# Log some value.
self.metrics.log_value("some_mean_result", 1.5, reduce="mean", window=5)
...
with self.metrics.log_time(("timers", "some_code")):
... # time some code
请查看这个正在运行的关于在 training_step() 中记录的端到端示例。
迁移到 Ray 2.53#
如果您在 Ray 2.52 之前使用过 MetricsLogger API,以下内容需要您注意。
最重要的一点:- **指标现在会在每次 MetricsLogger.reduce() 调用时被清除。之后再查看它们会返回相应缩减类型的零元素(np.nan、None 或空列表)。** - 控制流应基于其他变量,而不是查看指标。
对于 MetricsLogger 的记录方法(log_value、log_time 等):- clear_on_reduce 参数已弃用。(见上一点) - 使用 reduce="sum" 和 clear_on_reduce=False 现在等同于 reduce="lifetime_sum"。 - throughput_ema_coeff 已弃用(我们不再对吞吐量使用 EMA)。 - reduce_per_index_on_aggregate 参数已弃用。所有指标现在都针对任何缩减周期叶节点收集的所有值进行汇总。
其他更改:- 升级到 2.52 后,许多指标看起来更“嘈杂”。这主要是因为它们不再被平滑。如果需要,平滑应在下游进行。- aggregate() 现在是汇总指标的唯一方法。- 您现在可以将自定义统计类传递给 (AlgorithmConfig.reporting(custom_stats_cls_lookup={…}))。这使您可以编写具有自己缩减逻辑的自定义统计类。如果您的自定义统计类构成了对 RLlib 的修复或有价值的补充,请考虑通过 PR 将其贡献给项目。- 在汇总指标时,我们现在可以使用 peek() 中的 latest_merged_only=True 参数来查看最近一次合并的指标。