Scikit-learn: 使用 python 日志来报告长时间运行任务的收敛进度和级别信息

创建于 2011-02-12  ·  31评论  ·  资料来源: scikit-learn/scikit-learn

这是一个使用 python 的日志模块而不是在模型 API 中使用 stdout 和详细标志的建议。

使用日志记录模块将使用户更容易使用单个且记录良好的配置接口和日志记录 API 来控制 scikit 的详细程度。

http://docs.python.org/library/logging.html

New Feature

最有用的评论

第五个选项是删除详细标志,在任何地方使用日志记录,并让用户通过日志记录 API 调整详细程度。 毕竟,这就是日志记录的目的。

我会支持删除冗长,因为我发现每个估计器配置
令人沮丧,而且冗长的数值随意,不好
记录在案,

我认为摆脱verbose并使用日志记录级别会非常好。 我看到的唯一缺点是它会使日志稍微不易被发现。

所有31条评论

https://github.com/GaelVaroquaux/scikit-learn/tree/progress_logger 中的工作已经开始

剩下要做的很可能是相当机械的工作。

新的 Gradient Boosting 模块也有工作。

根据我的经验,日志记录实际上根本不是那么容易使用,因此在此为 -1。

有人在做这方面的工作吗?

我们添加一个默认打印到 STDOUT 的记录器怎么样? 那应该相当简单吧?

这个问题自 2011 年以来一直处于开放状态,所以我想知道这是否会得到解决。 我在使用 RFECV 时遇到了这个问题(https://github.com/scikit-learn/scikit-learn/blob/a24c8b464d094d2c468a16ea9f8bf8d42d949f84/sklearn/feature_selection/rfe.py#L273)。 我想打印进度,但默认的详细打印打印了太多消息。 我不想修补sys.stdout来完成这项工作,覆盖记录器将是简单而干净的解决方案。

sklearn 中还有其他发布,例如 #8105 和 #10973,它们将受益于 sklearn 中的真实日志记录。 总的来说,我认为日志记录将是 sklearn 的一个很好的补充。

欢迎您参与其中。 也许回调系统比
日志记录

我现在有点忙,但我支持以任何形式在 sklean 中自定义日志记录(尽管我更喜欢标准的 python 日志记录)。

有没有讨论过当 scikit-learn 开始使用日志记录时verbose=True意味着什么? 我们正在 dask-ml 中处理这个问题: https :

鉴于库不应该进行日志记录配置,用户可以配置他们的“应用程序”(可能只是脚本或交互式会话)以适当地记录事情。 要始终正确地做到这一点并不容易。

我在https://github.com/dask/dask-ml/pull/528 中的建议是verbose=True表示“为我临时配置日志记录”。 您可以使用上下文管理器来配置日志记录,并且 scikit-learn 会希望确保将INFO级别的消息打印到标准输出以匹配当前行为。

暂时还意味着设置的处理程序是特定于那个的
估计器或估计器类型?

我在 dask/dask-ml#528 中的建议是让 verbose=True 表示“暂时为我配置日志记录”。

这似乎是一个很好的平衡。 使用日志模块不是那么用户友好。 另一个“hack”是默认使用info ,但是当用户设置verbose=True ,日志可以提升到warning 。 这会起作用,因为默认情况下会显示警告。

我认为当用户要求更多时改变特定消息的级别
冗长与日志模块的意图完全相反
工作。 但是本地处理程序可能会从警告变为信息以进行调试
随着详细程度的增加,流中的级别

@jnothman 的评论符合我的想法。 scikit-learn 将始终发出消息,verbose 关键字控制记录器级别和处理程序。

但是本地处理程序可能会从警告变为信息以进行调试
随着详细程度的增加,流中的级别

好的,让我们开始吧。 当前日志级别为https://docs.python.org/3/library/logging.html#logging -levels 默认情况下,我们可以使用INFO ,默认情况下不会发出。 当verbose=1 ,我们有处理程序更改信息 -> 警告和调试 -> 信息。 当我们设置verbose>=2 ,我们仍然有 info -> warning 但也有 debug -> warning,并且估算器可以将verbose>=2为“随着详细增加发出更多调试消息”。 这个含义在不同的估计器之间可能不同。

你怎么认为?

你好,我对这个问题很感兴趣。 我对logging有一些经验,如果达成了一些共识和计划,我很乐意帮助在这里实施改进。

可能有助于回顾这里提到的想法:

  1. 使用回调模式
  2. 更改消息的级别,取决于verbose
    if verbose:
        logger.debug(message)
    else:
        logger.info(message)
  1. 改变logger的级别,取决于verbose
    if verbose:
        logger.selLevel("DEBUG")
  1. 添加级别DEBUG的处理程序,具体取决于详细信息
    if verbose:
        verbose_handler = logging.StreamHandler()
        verbose_handler.setLevel("DEBUG")
        logger.addHandler(verbose_handler)

我对这些选项的看法:

选项 1 或选项 4 可能是最好的。

  • 选项 1(回调)很好,因为它是最不可知的(人们可以随心所欲地记录事物)。 但从消息传递/状态捕获的角度来看,它可能不太灵活。 (回调不是每次内部循环迭代只调用一次或一次吗?)
  • 选项 2,正如这里所讨论的,我认为是滥用logging
  • 选项 3 有效,但我认为它违背了使用logging库的部分目的。 如果 sklearn 使用logging ,那么用户可以通过logging本身来调整详细程度,例如import logging; logging.getLogger("sklearn").setLevel("DEBUG")
  • 选项 4 可能是最规范的。 文档建议 _not_ 在NullHandler以外的库代码中创建处理程序,但我认为这是有道理的,因为 sklearn 有verbose标志。 在这种情况下,日志打印是库的“功能”。

第五个选项是删除verbose标志,在任何地方使用logging ,并让用户通过logging API 调整详细程度。 毕竟,这就是logging设计目的。

@grisaitis谢谢! 另请参阅https://github.com/scikit-learn/scikit-learn/issues/17439https://github.com/scikit-learn/scikit-learn/pull/16925#issuecomment -638956487 中更多最近的相关讨论(关于回调)。 非常感谢您的帮助,主要问题是我们还没有决定哪种方法最好:)

我会支持删除冗长,因为我发现每个估计器配置
令人沮丧,而且冗长的数值随意,不好
文档化等。每类配置将通过以下方式处理
多个 scikit-learn 记录器名称。

第五个选项是删除详细标志,在任何地方使用日志记录,并让用户通过日志记录 API 调整详细程度。 毕竟,这就是日志记录的目的。

我会支持删除冗长,因为我发现每个估计器配置
令人沮丧,而且冗长的数值随意,不好
记录在案,

我认为摆脱verbose并使用日志记录级别会非常好。 我看到的唯一缺点是它会使日志稍微不易被发现。

此外,日志记录提供的一件事是您可以将额外信息附加到每个日志记录消息,而不仅仅是字符串。 所以整个字典有用的东西。 所以如果你想在学习过程中报告丢失,你可以这样做并存储数值。 更重要的是,您既可以将数值存储为数字,也可以使用它来格式化用户友好的字符串,例如: logger.debug("Current loss: %(loss)s", {'loss': loss}) 。 我发现这在一般情况下非常有用,如果 sklearn 也公开它,我会很高兴。

我认为现在拥有模块或估算器级别的记录器有点过头了,我们应该从一些简单的东西开始,以便我们以后扩展它。
此外,无论我们做什么,都应该允许用户合理轻松地重现当前行为。

logging 和 joblib 如何交互? 不保留日志记录级别(我猜是预期的):

import logging
logger = logging.getLogger('sklearn')
logger.setLevel(2)

def get_level():
    another_logger = logging.getLogger('sklearn')
    return another_logger.level

results = Parallel(n_jobs=2)(
    delayed(get_level)() for _ in range(2)
)
results

``
[0, 0]

But that's probably not needed, since this works:
```python
import logging
import sys
logger = logging.getLogger('sklearn')
logger.setLevel(1)

handler = logging.StreamHandler(sys.stdout)
logger.addHandler(handler)


def log_some():
    another_logger = logging.getLogger('sklearn')
    another_logger.critical("log something")

results = Parallel(n_jobs=2)(
    delayed(log_some)() for _ in range(2)
)

老实说,我并不完全确定这是如何工作的。

stdout 和 stderr 都没有出现在 jupyter btw 中。

我的梦想:能够用一条线获得当前行为的近似值,而且能够轻松地使用进度条或绘图收敛。

reverbose:弃用verbose 可能更清晰,但是弃用verbose 并且没有估算器级别的日志记录会使记录一个估算器而不是另一个估算器变得有点棘手。 不过,我认为让用户过滤消息很好。

大家好,感谢您的友好回复和信息。 我阅读了其他问题并有一些想法。

joblib会很棘手。 不过我有一些想法。

@amueller这很奇怪。 我复制了你的例子。 事情确实适用于concurrent.futures.ProcessPoolExecutor ,我认为joblib使用...

似乎joblib正在logging破坏状态。 任何joblib专家对可能发生的事情有想法吗?

import concurrent.futures
import logging
import os

logger = logging.getLogger("demo🙂")
logger.setLevel("DEBUG")

handler = logging.StreamHandler()
handler.setFormatter(
    logging.Formatter("%(process)d (%(processName)s) %(levelname)s:%(name)s:%(message)s")
)
logger.addHandler(handler)

def get_logger_info(_=None):
    another_logger = logging.getLogger("demo🙂")
    print(os.getpid(), "another_logger:", another_logger, another_logger.handlers)
    another_logger.warning(f"hello from {os.getpid()}")
    return another_logger

if __name__ == "__main__":
    print(get_logger_info())

    print()
    print("concurrent.futures demo...")
    with concurrent.futures.ProcessPoolExecutor(2) as executor:
        results = executor.map(get_logger_info, range(2))
        print(list(results))

    print()
    print("joblib demo (<strong i="17">@amueller</strong>'s example #2)...")
    from joblib import Parallel, delayed
    results = Parallel(n_jobs=2)(delayed(get_logger_info)() for _ in range(2))
    print(results)

哪个输出

19817 another_logger: <Logger demo🙂 (DEBUG)> [<StreamHandler <stderr> (NOTSET)>]
19817 (MainProcess) WARNING:demo🙂:hello from 19817
<Logger demo🙂 (DEBUG)>

concurrent.futures demo...
19819 another_logger: <Logger demo🙂 (DEBUG)> [<StreamHandler <stderr> (NOTSET)>]
19819 (SpawnProcess-1) WARNING:demo🙂:hello from 19819
19819 another_logger: <Logger demo🙂 (DEBUG)> [<StreamHandler <stderr> (NOTSET)>]
19819 (SpawnProcess-1) WARNING:demo🙂:hello from 19819
[<Logger demo🙂 (DEBUG)>, <Logger demo🙂 (DEBUG)>]

joblib demo (<strong i="21">@amueller</strong>'s example #2)...
19823 another_logger: <Logger demo🙂 (WARNING)> []
hello from 19823
19823 another_logger: <Logger demo🙂 (WARNING)> []
hello from 19823
[<Logger demo🙂 (DEBUG)>, <Logger demo🙂 (DEBUG)>]

我认为您应该配置进程 joblib spawns 以将日志消息发送到主进程中的主记录器。 然后只能在主进程中控制日志记录。 像这样这样的东西。 所以有日志队列接收器和源,你可以将它们联系在一起。 我们在我们的集群上使用它,将所有机器上所有工作人员的所有日志发送到中心位置,以在用户的​​计算机上显示它。

@mitar我同意,我认为这可能是最好的选择。 (不一定是基于网络的队列,而是进程间通信队列)

我现在实际上正在使用loggingQueueHandler / QueueListener编写一个示例,以使用joblibconcurrent.futures 。 将在这里跟进。

也喜欢你的其他建议:

此外,日志记录提供的一件事是您可以将额外信息附加到每个日志记录消息,而不仅仅是字符串。 所以整个字典有用的东西。 所以如果你想在学习过程中报告丢失,你可以这样做并存储数值。 更重要的是,您既可以将数值存储为数字,也可以使用它来格式化用户友好的字符串,例如: logger.debug("Current loss: %(loss)s", {'loss': loss}) 。 我发现这在一般情况下非常有用,如果 sklearn 也公开它,我会很高兴。

我使用extra参数和自定义 Handler 类实现了高斯混合建模的可视化。 非常适合传递状态,并让用户决定如何处理状态。

还有我在上面注意到的joblib的特质......我将接受原样并超出范围。 无论如何,基于队列的设计将是最灵活的。

我能想到的使用 QueueHandler 的唯一限制是,任何extra状态( logger.debug("message", extra={...} )都是extra dict 必须可序列化队列。 所以没有numpy数组。 :/ 虽然想不出任何其他问题

我现在实际上正在使用 QueueHandler / QueueListener 编写一个示例,

是的,您应该始终使用队列处理程序,因为您永远不知道何时通过套接字发送阻塞,并且您不想因为日志记录阻塞而减慢模型速度。

此外,您甚至不必使用extra 。 我认为logger.debug("message %(foo)s", {'foo': 1, 'bar': 2})只是有效。

是的,您应该始终使用队列处理程序,因为您永远不知道何时通过套接字发送阻塞,并且您不想因为日志记录阻塞而减慢模型速度。

对于joblib情况,如果我们实现了QueueHandler / QueueListener ,我们必须将什么状态传递给进程池? 只是queue ,对吧?

此外,您甚至不必使用extra 。 我认为logger.debug("message %(foo)s", {'foo': 1, 'bar': 2})只是有效。

谢谢是的。 我发现记录状态而不将其转换为文本也很有用。 例如,在extra包含一个 numpy 数组,并在 jupyter notebook 中使用自定义日志处理程序进行实时数据可视化(以某种方式进行可视化日志记录)。 这对 sklearn 来说非常好,看起来@rth一直在用回调做类似的工作。

对于 joblib 的情况,如果我们实现了 QueueHandler / QueueListener,我们必须将什么状态传递给进程池? 只是排队,对吗?

我想是这样。 我没有在进程边界上使用它,但似乎它们对多处理有文档支持,因此它也应该与 joblib 一起使用。 我在同一个进程中使用 QueueHandler / QueueListener 。 将日志写入与日志传输分离。 QueueHandler -> QueueListener -> 发送到中央日志服务也是如此。 但从文档看来,它可以通过多处理队列工作。

我发现记录状态而不将其转换为文本也很有用

是的。 我要说的是,您不必使用extra ,而只需直接传递 dict,然后仅使用该 dict 中的少数项目进行消息格式化(请注意,格式字符串中使用的内容已决定由 sklearn 库的用户,而不是 sklearn 库的用户,您可以随时配置您想要的格式来设置您不期望的内容,因此究竟转换为文本的内容并不真正受 sklearn 控制)。 extra所有值也可用于消息格式化。 所以我不确定extra有多大用处。 但我也不是说我们不应该使用它。 左边字符串的有效载荷是什么,什么是额外的,这一点更加明确。 所以我们也可以同时使用两者。 我只是想确保这个替代方案是已知的。

@grisaitis FYI 如果你在提交中提到一个名字,每次你对提交做任何事情时(比如重新设置它或合并它或推送它),这个人会收到一封电子邮件,所以通常不鼓励;)

对不起安德烈亚斯! 😬 那太尴尬了......我只是想拥有有据可查的提交,哈哈。 以后会避而远之。

在那个 repo 中,我弄清楚了日志记录如何与joblib一起使用 QueueHandler / QueueListener 组合。 似乎工作得很好。

作为第一步,我将在使用joblib的 sklearn 的一部分中使用该方法实现日志记录。 也许是合奏模型之一。 将开启一个新的 PR。

对于 joblib 的情况,如果我们实现了 QueueHandler / QueueListener,

是的,如果在多处理的情况下使用日志记录模块和回调,听起来好像有必要启动/停止监视线程(此处QueueListener )(https:// 中具有多处理的回调的近似示例) github.com/scikit-learn/scikit-learn/pull/16925#issuecomment-656184396)

所以我想我上面所做的“工作”的唯一原因是它打印到标准输出,这是共享资源,而print在 python3 或类似的东西中是线程安全的?

所以我认为我在上面所做的“有效”的唯一原因是它打印到标准输出,这是共享资源,而打印在 python3 或类似的东西中是线程安全的?

打印不是线程安全的。 它们只是打印到相同的文件描述符。 可能运行更长的时间,您会看到消息有时会交错且行会混淆。

此页面是否有帮助?
0 / 5 - 0 等级