Scikit-learn: Utilisez la journalisation python pour signaler les progrès de la convergence sur les informations de niveau pour les tâches de longue durée

Créé le 12 févr. 2011  ·  31Commentaires  ·  Source: scikit-learn/scikit-learn

Il s'agit d'une proposition d'utiliser le module de journalisation de python au lieu d'utiliser les indicateurs stdout et verbose dans l'API des modèles.

L'utilisation du module de journalisation permettrait à l'utilisateur de contrôler plus facilement la verbosité du scikit à l'aide d'une interface de configuration et d'une API de journalisation uniques et bien documentées.

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

New Feature

Commentaire le plus utile

Une cinquième option serait de supprimer les indicateurs détaillés, d'utiliser la journalisation partout et de laisser les utilisateurs ajuster la verbosité via l'API de journalisation. C'est pour cela que la journalisation a été conçue, après tout.

Je soutiendrais la suppression du verbeux, car je trouve la configuration par estimateur
frustrant, et les valeurs numériques de verbeux arbitraires, mal
documenté,

Je pense que se débarrasser de verbose et utiliser les niveaux de journalisation serait très bien. Le seul inconvénient que je vois est que cela rendrait la journalisation légèrement moins détectable.

Tous les 31 commentaires

Les travaux ont commencé à ce sujet dans https://github.com/GaelVaroquaux/scikit-learn/tree/progress_logger

Ce qui reste à faire est très probablement un travail assez mécanique.

Il y a aussi du travail dans le nouveau module Gradient Boosting.

La journalisation n'est en fait pas si facile à utiliser, d'après mon expérience, donc -1 à ce sujet.

Est-ce que quelqu'un travaille là-dessus ?

Que diriez-vous d'ajouter un enregistreur qui par défaut imprime sur STDOUT ? Cela devrait être assez simple, non?

Ce problème est ouvert depuis 2011 et je me demande donc si cela va être corrigé. J'ai rencontré ce problème avec RFECV (https://github.com/scikit-learn/scikit-learn/blob/a24c8b464d094d2c468a16ea9f8bf8d42d949f84/sklearn/feature_selection/rfe.py#L273). Je voulais imprimer la progression mais l'impression détaillée par défaut imprime trop de messages. Je ne voulais pas patcher sys.stdout pour que cela fonctionne et remplacer l'enregistreur serait la solution simple et propre.

Il existe d'autres publications dans sklearn telles que #8105 et #10973 qui bénéficieraient d'une connexion réelle dans sklearn. Dans l'ensemble, je pense que la journalisation serait un excellent ajout à sklearn.

vous êtes invités à y travailler. peut-être qu'un système de rappel est mieux que
enregistrement

Je suis un peu occupé en ce moment, mais je prends en charge la journalisation personnalisable dans sklean sous quelque forme que ce soit (bien que je préfère la journalisation python standard).

Y a-t-il eu une discussion sur ce que verbose=True signifiera lorsque scikit-learn commencera à utiliser la journalisation ? Nous traitons un peu de cela dans dask-ml : https://github.com/dask/dask-ml/pull/528.

Étant donné que les bibliothèques ne sont pas censées effectuer la configuration de la journalisation, il appartient à l'utilisateur de configurer son "application" (qui peut être simplement un script ou une session interactive) pour enregistrer les choses de manière appropriée. Ce n'est pas toujours facile à faire correctement.

Ma proposition dans https://github.com/dask/dask-ml/pull/528 est que verbose=True signifie "configurer temporairement la journalisation pour moi". Vous pouvez utiliser un gestionnaire de contexte pour configurer la journalisation , et scikit-learn voudrait s'assurer que les messages de niveau INFO sont imprimés sur stdout pour correspondre au comportement actuel.

Est-ce que temporairement signifie également que la configuration du gestionnaire est spécifique à ce
estimateur ou type d'estimateur?

Ma proposition dans dask/dask-ml#528 est que verbose=True signifie "configurer temporairement la journalisation pour moi".

Cela semble être un bon équilibre. L'utilisation du module de journalisation n'est pas si conviviale que cela. Un autre "hack" consisterait à utiliser info par défaut, mais lorsqu'un utilisateur définit verbose=True les journaux peuvent être élevés à warning . Cela fonctionnerait car les avertissements sont affichés par défaut.

Je pense changer le niveau des messages spécifiques lorsque l'utilisateur en demande plus
la verbosité est exactement le contraire de la façon dont le module de journalisation est censé
travail. Mais le gestionnaire local pourrait passer de l'avertissement à l'information pour déboguer
niveau en cours au fur et à mesure que le verbeux augmente

Le commentaire de @jnothman correspond à mes pensées. scikit-learn émettra toujours le message et le mot-clé verbose contrôle le niveau de consignation et les gestionnaires.

Mais le gestionnaire local pourrait passer de l'avertissement à l'information pour déboguer
niveau en cours au fur et à mesure que le verbeux augmente

D'accord, allons-y avec ça. Actuellement, les niveaux de journalisation sont https://docs.python.org/3/library/logging.html#logging -levels Par défaut, nous pouvons utiliser le INFO , qui n'émet pas par défaut. Lorsque verbose=1 , nous avons le gestionnaire change info -> warning, et debug -> info. Lorsque nous définissons verbose>=2 , nous avons toujours des informations -> avertissement mais également de débogage -> avertissement, et l'estimateur peut interpréter le verbose>=2 comme signifiant "émettre plus de messages de débogage au fur et à mesure que la verbose augmente". Cette signification peut être différente selon les différents estimateurs.

Qu'est-ce que tu penses?

Bonjour, je suis très intéressé par ce problème. J'ai une certaine expérience avec logging et j'aimerais aider à mettre en œuvre une amélioration ici si un consensus et un plan sont atteints.

pourrait être utile pour récapituler les idées mentionnées ici :

  1. utiliser un modèle de rappel
  2. changer le niveau du message, en fonction de verbose
    if verbose:
        logger.debug(message)
    else:
        logger.info(message)
  1. changer le niveau du logger , en fonction de verbose
    if verbose:
        logger.selLevel("DEBUG")
  1. ajouter un gestionnaire avec le niveau DEBUG , selon le verbeux
    if verbose:
        verbose_handler = logging.StreamHandler()
        verbose_handler.setLevel("DEBUG")
        logger.addHandler(verbose_handler)

Mon avis sur ces options :

L'option 1 ou l'option 4 serait probablement la meilleure.

  • L'option 1 (rappels) est bonne dans la mesure où elle est la plus agnostique (les gens peuvent enregistrer les choses comme ils le souhaitent). Mais cela pourrait être moins flexible du point de vue de la messagerie / capture d'état. (Les rappels ne sont-ils pas appelés une ou une fois par itération de boucle interne ?)
  • L'option 2, comme discuté ici, je pense qu'elle utilise à mauvais escient la bibliothèque logging
  • L'option 3 fonctionne mais, je pense, va à l'encontre du but de l'utilisation de la bibliothèque logging . Si sklearn utilise logging , les utilisateurs peuvent ajuster la verbosité via logging lui-même, par exemple import logging; logging.getLogger("sklearn").setLevel("DEBUG") .
  • L'option 4 est probablement la plus canonique. La documentation suggère de _ne pas_ créer des gestionnaires dans le code de la bibliothèque autres que NullHandler s, mais je pense que cela a du sens, étant donné que sklearn a des indicateurs verbose . Dans ce cas, l'impression des journaux est une "fonctionnalité" de la bibliothèque.

Une cinquième option serait de supprimer les drapeaux verbose , d'utiliser logging partout et de laisser les utilisateurs ajuster la verbosité via l'API logging . C'est pour cela que logging été conçu, après tout.

@grisaitis merci ! Voir également les discussions connexes plus récentes dans https://github.com/scikit-learn/scikit-learn/issues/17439 et https://github.com/scikit-learn/scikit-learn/pull/16925#issuecomment -638956487 (concernant les rappels). Votre aide serait très appréciée, le problème principal est que nous n'avons pas encore décidé quelle approche serait la meilleure :)

Je soutiendrais la suppression du verbeux, car je trouve la configuration par estimateur
frustrant, et les valeurs numériques de verbeux arbitraires, mal
documenté, etc. La configuration par classe serait gérée en ayant
plusieurs noms d'enregistreurs scikit-learn.

Une cinquième option serait de supprimer les indicateurs détaillés, d'utiliser la journalisation partout et de laisser les utilisateurs ajuster la verbosité via l'API de journalisation. C'est pour cela que la journalisation a été conçue, après tout.

Je soutiendrais la suppression du verbeux, car je trouve la configuration par estimateur
frustrant, et les valeurs numériques de verbeux arbitraires, mal
documenté,

Je pense que se débarrasser de verbose et utiliser les niveaux de journalisation serait très bien. Le seul inconvénient que je vois est que cela rendrait la journalisation légèrement moins détectable.

En outre, une chose que la journalisation fournit est que vous pouvez joindre des informations supplémentaires à chaque message de journalisation, pas seulement des chaînes. Donc tout un dict de trucs utiles. Donc, si vous souhaitez signaler une perte pendant l'apprentissage, vous pouvez le faire et stocker une valeur numérique. Encore plus, vous pouvez à la fois stocker la valeur numérique sous forme de nombre et l'utiliser pour formater une chaîne conviviale, comme : logger.debug("Current loss: %(loss)s", {'loss': loss}) . Je trouve cela très utile en général et j'aimerais que sklearn expose cela également.

Je pense qu'avoir des enregistreurs de niveau de module ou d'estimateur est un peu exagéré pour le moment et nous devrions commencer par quelque chose de simple qui nous permette de l'étendre plus tard.
De plus, tout ce que nous faisons devrait permettre aux utilisateurs de reproduire raisonnablement facilement le comportement actuel.

Comment la journalisation et la joblib interagissent-elles ? Le niveau de journalisation n'est pas conservé (comme prévu, je suppose):

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)
)

Honnêtement, je ne suis pas tout à fait sûr de savoir comment cela fonctionne.

les deux stdout et stderr n'apparaissent pas dans jupyter d'ailleurs.

Mon rêve : pouvoir obtenir une approximation du comportement actuel avec une seule ligne, mais aussi pouvoir utiliser facilement des barres de progression ou tracer la convergence à la place.

re verbose : il est probablement plus propre de déprécier verbose, mais déprécier verbose et ne pas avoir de journalisation au niveau de l'estimateur rendra un peu plus difficile la journalisation d'un estimateur mais pas d'un autre. Je pense que c'est bien que l'utilisateur filtre les messages, cependant.

salut à tous, merci pour les réponses amicales et les informations. J'ai lu les autres problèmes et j'ai quelques réflexions.

joblib sera délicat. j'ai quand même quelques idées.

@amueller c'est très bizarre. j'ai reproduit ton exemple. les choses fonctionnent avec concurrent.futures.ProcessPoolExecutor , je pense que joblib utilisations ...

semble que joblib détruit l'état dans logging . des joblib experts ont une idée de ce qui pourrait se passer ?

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)

quelles sorties

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)>]

Je pense que vous devriez configurer les processus générés par joblib pour envoyer un message de journalisation à l'enregistreur principal dans le processus principal. Ensuite, on peut contrôler la journalisation dans le processus principal uniquement. Quelque chose comme ceci ou cela . Il existe donc des récepteurs et des sources de file d'attente de journalisation et vous pouvez les lier ensemble. Nous l'utilisons sur notre cluster, pour envoyer tous les journaux de tous les travailleurs sur toutes les machines vers un emplacement central, pour les afficher sur l'ordinateur de l'utilisateur.

@mitar je suis d'accord, je pense que cela pourrait être le meilleur pari. (pas nécessairement des files d'attente basées sur le réseau, mais des files d'attente de communication inter-processus)

Je suis en train de coder un exemple en utilisant les logging de QueueHandler / QueueListener ce moment, pour tester avec joblib et concurrent.futures . suivra ici.

j'adore aussi ton autre suggestion :

En outre, une chose que la journalisation fournit est que vous pouvez joindre des informations supplémentaires à chaque message de journalisation, pas seulement des chaînes. Donc tout un dict de trucs utiles. Donc, si vous souhaitez signaler une perte pendant l'apprentissage, vous pouvez le faire et stocker une valeur numérique. De plus, vous pouvez à la fois stocker la valeur numérique sous forme de nombre et l'utiliser pour formater une chaîne conviviale, telle que : logger.debug("Current loss: %(loss)s", {'loss': loss}) . Je trouve cela très utile en général et j'aimerais que sklearn expose cela également.

J'ai implémenté une visualisation de la modélisation de mélanges gaussiens à l'aide du paramètre extra et d'une classe Handler personnalisée. fonctionne très bien pour transmettre l'état et laisser l'utilisateur décider comment gérer l'état.

aussi concernant les particularités de joblib que j'ai remarquées ci-dessus... je vais accepter cela tel quel et hors de portée. une conception basée sur une file d'attente serait de toute façon la plus flexible.

la seule limitation de l'utilisation d'un QueueHandler, à laquelle je peux penser, est que tout état extra ( logger.debug("message", extra={...} ) est que le dict extra doit être sérialisable pour la file d'attente. donc pas de tableaux numpy. :/ je ne peux pas penser à d'autres problèmes cependant

je suis en train de coder un exemple en utilisant QueueHandler / QueueListener en ce moment,

Oui, vous devez toujours utiliser le gestionnaire de file d'attente, car vous ne savez jamais quand l'envoi sur le socket se bloque et vous ne voulez pas ralentir le modèle à cause du blocage de la journalisation.

De plus, vous n'avez même pas besoin d'utiliser extra . Je pense que logger.debug("message %(foo)s", {'foo': 1, 'bar': 2}) fonctionne.

Oui, vous devez toujours utiliser le gestionnaire de file d'attente, car vous ne savez jamais quand l'envoi sur le socket se bloque et vous ne voulez pas ralentir le modèle à cause du blocage de la journalisation.

pour le cas joblib , si nous implémentions QueueHandler / QueueListener , quel état devrions-nous passer au pool de processus ? juste le queue , n'est-ce pas ?

De plus, vous n'avez même pas besoin d'utiliser extra . Je pense que logger.debug("message %(foo)s", {'foo': 1, 'bar': 2}) fonctionne.

merci oui. Je trouve qu'il est également utile de consigner l'état sans le convertir en texte. par exemple, inclure un tableau numpy dans extra et effectuer une visualisation des données en temps réel (enregistrement visuel en quelque sorte) avec un gestionnaire de journalisation personnalisé dans un cahier jupyter. ce serait SUPER sympa avec sklearn, et on dirait que @rth a fait un travail similaire avec des rappels.

pour le cas joblib, si nous implémentions QueueHandler / QueueListener, quel état devrions-nous passer au pool de processus ? juste la file d'attente, non?

Je pense que oui. Je ne l'ai pas utilisé au-delà des limites de processus, mais il semble qu'ils aient un support documenté pour le multitraitement, donc cela devrait également fonctionner avec joblib. J'utilise QueueHandler / QueueListener dans le même processus. Pour découpler les écritures de journalisation du transport de journalisation. Il en va de même pour QueueHandler -> QueueListener -> Envoyer au service de journalisation central. Mais d'après la documentation, il semble que cela puisse fonctionner via une file d'attente de multitraitement.

je trouve qu'il est également utile de consigner l'état sans le convertir en texte

Oui. Ce que je dis, c'est que vous n'avez pas besoin d'utiliser extra , mais passez simplement dict directement, puis vous n'utilisez que quelques éléments de ce dict pour le formatage du message (notez que ce qui est utilisé dans les chaînes de format est décidé par les utilisateurs de la bibliothèque sklearn, pas par la bibliothèque sklearn, on peut toujours configurer ce que vous voulez en formatant quelque chose auquel vous ne vous attendiez pas, donc ce qui est exactement converti en texte n'est pas vraiment sous le contrôle de sklearn). Toutes les valeurs de extra peuvent également être utilisées pour le formatage des messages. Je ne sais donc pas à quel point ce extra est utile. Mais je ne dis pas non plus que nous ne devrions pas l'utiliser. Il est beaucoup plus explicite quelle était la charge utile pour la chaîne de gauche et ce qui est supplémentaire. On peut donc aussi utiliser les deux. Je voulais juste m'assurer que cette alternative est connue.

@grisaitis Pour info, si vous mentionnez un nom dans un commit, chaque fois que vous faites quelque chose avec le commit (comme le rebaser, le fusionner ou le pousser), la personne reçoit un e-mail, donc c'est généralement déconseillé ;)

Désolé pour ça Andreas ! 😬 C'est embarrassant... J'essayais juste d'avoir des commits bien documentés lol. A éviter à l'avenir.

Dans ce référentiel, j'ai compris comment la journalisation pouvait fonctionner avec joblib avec un combo QueueHandler / QueueListener. Semble bien fonctionner.

Dans un premier temps, je vais implémenter la journalisation avec cette approche dans une partie de sklearn où joblib est utilisé. Peut-être l'un des modèles d'ensemble. Ouvrera un nouveau PR.

pour le cas joblib, si nous avons implémenté QueueHandler / QueueListener,

Oui, il semble qu'il soit nécessaire de démarrer/arrêter un thread de surveillance (ici QueueListener ) à la fois si vous utilisez le module de journalisation et les rappels dans le cas du multitraitement (exemple approximatif de rappels avec multitraitement dans https:// github.com/scikit-learn/scikit-learn/pull/16925#issuecomment-656184396)

Donc, je suppose que la seule raison pour laquelle ce que j'ai fait ci-dessus "a fonctionné" était qu'il s'imprimait sur stdout qui était la ressource partagée et que print est threadsafe dans python3 ou quelque chose comme ça?

Donc, je suppose que la seule raison pour laquelle ce que j'ai fait ci-dessus "a fonctionné" était qu'il s'imprimait sur stdout qui était la ressource partagée et que l'impression était threadsafe dans python3 ou quelque chose comme ça?

L'impression n'est pas thread-safe. Ils impriment simplement sur le même descripteur de fichier. En cours d'exécution probablement plus longtemps, vous verriez que les messages s'entrelacent parfois et que les lignes se mélangent.

Cette page vous a été utile?
0 / 5 - 0 notes