Module livelossplot.main_logger

Expand source code
import re
from collections import OrderedDict, defaultdict
from typing import NamedTuple, Dict, Iterable, List, Pattern, Tuple, Optional, Union

# Value of metrics - for value later, we want to support numpy arrays etc
LogItem = NamedTuple('LogItem', [('step', int), ('value', float)])
COMMON_NAME_SHORTCUTS = {
    'acc': 'Accuracy',
    'nll': 'Log Loss (cost function)',
    'mse': 'Mean Squared Error',
    'loss': 'Loss'
}


class MainLogger:
    """
    Main logger - the aim of this class is to store every log from training
    Log is a float value with corresponding training engine step
    """
    def __init__(
        self,
        groups: Optional[Dict[str, List[str]]] = None,
        metric_to_name: Optional[Dict[str, str]] = None,
        from_step: int = 0,
        current_step: int = -1,
        auto_generate_groups_if_not_available: bool = True,
        auto_generate_metric_to_name: bool = True,
        group_patterns: Iterable[Tuple[Pattern, str]] = (
            (r'^(?!val(_|-))(.*)', 'training'),
            (r'^(val(_|-))(.*)', 'validation'),
        ),
        step_names: Union[str, Dict[str, str]] = 'epoch'
    ):
        """
        Args:
            groups: dictionary with grouped metrics for example the group 'accuracy' can contains different stages
                for example 'validation_accuracy', 'training_accuracy' etc.
            metric_to_name: transformation of metric name which can be used to display name
                we can have short name in the code (acc), but full name on charts (Accuracy)
            from_step: step to show in plots (positive: show steps from this one, negative: show only this many last steps)
            current_step: current step of the train loop
            auto_generate_groups_if_not_available: flag, that enable auto-creation of metric groups
                base on group patterns
            auto_generate_metric_to_name: flag, that enable auto-creation of metric long names
                base on common shortcuts
            group_patterns: you can put there regular expressions to match a few metric names with group
                and replace its name using second value
            step_names: dictionary with a name of x axis for each metrics group or one name for all metrics
        """
        self.log_history = {}
        self.groups = groups if groups is not None else {}
        self.metric_to_name = metric_to_name if metric_to_name else {}
        self.from_step = from_step
        self.current_step = current_step
        self.auto_generate_groups = all((not groups, auto_generate_groups_if_not_available))
        self.auto_generate_metric_to_name = auto_generate_metric_to_name
        self.group_patterns = tuple((re.compile(pattern), replace_with) for pattern, replace_with in group_patterns)
        if isinstance(step_names, str):
            self.step_names = defaultdict(lambda: step_names)
        else:
            self.step_names = defaultdict(lambda: 'epoch', step_names)

    def update(self, logs: Dict[str, float], current_step: Optional[int] = None) -> None:
        """
        Args:
            logs: dictionary with metric names and values
            current_step: current step of the training loop

        Notes:
            Loop step can be controlled outside or inside main logger with autoincrement of self.current_step
        """
        if current_step is None:
            self.current_step += 1
            current_step = self.current_step
        else:
            self.current_step = current_step
        for k, v in logs.items():
            if k not in self.log_history:
                self._add_new_metric(k)
            self.log_history[k].append(LogItem(step=current_step, value=v))

    def _add_new_metric(self, metric_name: str):
        """Add empty list for a new metric and extend metric name transformations
        Args:
            metric_name: name of metric that will be added to log_history as empty list
        """
        self.log_history[metric_name] = []
        if not self.metric_to_name.get(metric_name):
            self._auto_generate_metrics_to_name(metric_name)

    def _auto_generate_metrics_to_name(self, metric_name: str):
        """The function generate transforms for metric names base on patterns
        Args:
            metric_name: name of new appended metric

        Example:
            It can create transformation from val_acc to Validation Accuracy
        """
        suffix = self._find_suffix_with_group_patterns(metric_name)
        if suffix is None and suffix != metric_name:
            return
        similar_metric_names = [m for m in self.log_history.keys() if m.endswith(suffix)]
        if len(similar_metric_names) == 1:
            return
        for name in similar_metric_names:
            new_name = name
            for pattern_to_replace, replace_with in self.group_patterns:
                new_name = re.sub(pattern_to_replace, replace_with, new_name)
            if suffix in COMMON_NAME_SHORTCUTS.keys():
                new_name = new_name.replace(suffix, COMMON_NAME_SHORTCUTS[suffix])
            self.metric_to_name[name] = new_name

    def grouped_log_history(self,
                            raw_names: bool = False,
                            raw_group_names: bool = False) -> Dict[str, Dict[str, List[LogItem]]]:
        """
        Args:
            raw_names: flag, return raw names instead of transformed by metric to name (as in update() input dictionary)
            raw_group_names: flag, return group names without transforming them with COMMON_NAME_SHORTCUTS

        Returns:
            logs grouped by metric groups - groups are passed in the class constructor

        Notes:
            method use group patterns instead of groups if they are available
        """
        if self.auto_generate_groups:
            self.groups = self._auto_generate_groups()
        ret = {}
        sorted_groups = OrderedDict(sorted(self.groups.items(), key=lambda t: t[0]))
        for group_name, names in sorted_groups.items():
            group_name = group_name if raw_group_names else COMMON_NAME_SHORTCUTS.get(group_name, group_name)
            ret[group_name] = {
                name if raw_names else self.metric_to_name.get(name, name): self.history_shorter(name)
                for name in names
            }
        return ret

    def history_shorter(self, name: str, full: bool = False) -> List[LogItem]:
        """
        Args:
            name: metrics name, e.g. 'val_acc' or 'loss'
            full: flag, if True return all, otherwise as specified by the from_step parameter

        Returns:
            a list of log items
        """
        log_metrics = self.log_history[name]
        if full or self.from_step == 0:
            return log_metrics
        elif self.from_step > 0:
            return [x for x in log_metrics if x.step >= self.from_step]
        else:
            current_from_step = self.current_step + self.from_step
            return [x for x in log_metrics if x.step >= current_from_step]

    def _auto_generate_groups(self) -> Dict[str, List[str]]:
        """
        Returns:
            groups generated with group patterns

        Notes:
            Auto create groups base on val_ prefix - this step is skipped if groups are set
                or if group patterns are available
        """
        groups = {}
        for key in self.log_history.keys():
            abs_key = self._find_suffix_with_group_patterns(key)
            if not groups.get(abs_key):
                groups[abs_key] = []
            groups[abs_key].append(key)
        return groups

    def _find_suffix_with_group_patterns(self, metric_name: str) -> str:
        suffix = metric_name
        for pattern, _ in self.group_patterns:
            match = re.match(pattern, metric_name)
            if match:
                suffix = match.groups()[-1]
        return suffix

    def reset(self) -> None:
        """Method clears logs, groups and reset step counter"""
        self.log_history = {}
        self.groups = {}
        self.current_step = -1

    @property
    def groups(self) -> Dict[str, List[str]]:
        """groups getter"""
        return self._groups

    @groups.setter
    def groups(self, value: Dict[str, List[str]]) -> None:
        """groups setter - groups should be dictionary"""
        if value is None:
            self._groups = {}
        self._groups = value

    @property
    def log_history(self) -> Dict[str, List[LogItem]]:
        """logs getter"""
        return self._log_history

    @log_history.setter
    def log_history(self, value: Dict[str, List[LogItem]]) -> None:
        """logs setter - logs can not be overwritten - you can only reset it to empty state"""
        if len(value) > 0:
            raise RuntimeError('Cannot overwrite log history with non empty dictionary')
        self._log_history = value

Classes

class LogItem (step, value)

LogItem(step, value)

Ancestors

  • builtins.tuple

Instance variables

var step : int

Alias for field number 0

var value : float

Alias for field number 1

class MainLogger (groups: Optional[Dict[str, List[str]]] = None, metric_to_name: Optional[Dict[str, str]] = None, from_step: int = 0, current_step: int = -1, auto_generate_groups_if_not_available: bool = True, auto_generate_metric_to_name: bool = True, group_patterns: Iterable[Tuple[Pattern[~AnyStr], str]] = (('^(?!val(_|-))(.*)', 'training'), ('^(val(_|-))(.*)', 'validation')), step_names: Union[str, Dict[str, str]] = 'epoch')

Main logger - the aim of this class is to store every log from training Log is a float value with corresponding training engine step

Args

groups
dictionary with grouped metrics for example the group 'accuracy' can contains different stages for example 'validation_accuracy', 'training_accuracy' etc.
metric_to_name
transformation of metric name which can be used to display name we can have short name in the code (acc), but full name on charts (Accuracy)
from_step
step to show in plots (positive: show steps from this one, negative: show only this many last steps)
current_step
current step of the train loop
auto_generate_groups_if_not_available
flag, that enable auto-creation of metric groups base on group patterns
auto_generate_metric_to_name
flag, that enable auto-creation of metric long names base on common shortcuts
group_patterns
you can put there regular expressions to match a few metric names with group and replace its name using second value
step_names
dictionary with a name of x axis for each metrics group or one name for all metrics
Expand source code
class MainLogger:
    """
    Main logger - the aim of this class is to store every log from training
    Log is a float value with corresponding training engine step
    """
    def __init__(
        self,
        groups: Optional[Dict[str, List[str]]] = None,
        metric_to_name: Optional[Dict[str, str]] = None,
        from_step: int = 0,
        current_step: int = -1,
        auto_generate_groups_if_not_available: bool = True,
        auto_generate_metric_to_name: bool = True,
        group_patterns: Iterable[Tuple[Pattern, str]] = (
            (r'^(?!val(_|-))(.*)', 'training'),
            (r'^(val(_|-))(.*)', 'validation'),
        ),
        step_names: Union[str, Dict[str, str]] = 'epoch'
    ):
        """
        Args:
            groups: dictionary with grouped metrics for example the group 'accuracy' can contains different stages
                for example 'validation_accuracy', 'training_accuracy' etc.
            metric_to_name: transformation of metric name which can be used to display name
                we can have short name in the code (acc), but full name on charts (Accuracy)
            from_step: step to show in plots (positive: show steps from this one, negative: show only this many last steps)
            current_step: current step of the train loop
            auto_generate_groups_if_not_available: flag, that enable auto-creation of metric groups
                base on group patterns
            auto_generate_metric_to_name: flag, that enable auto-creation of metric long names
                base on common shortcuts
            group_patterns: you can put there regular expressions to match a few metric names with group
                and replace its name using second value
            step_names: dictionary with a name of x axis for each metrics group or one name for all metrics
        """
        self.log_history = {}
        self.groups = groups if groups is not None else {}
        self.metric_to_name = metric_to_name if metric_to_name else {}
        self.from_step = from_step
        self.current_step = current_step
        self.auto_generate_groups = all((not groups, auto_generate_groups_if_not_available))
        self.auto_generate_metric_to_name = auto_generate_metric_to_name
        self.group_patterns = tuple((re.compile(pattern), replace_with) for pattern, replace_with in group_patterns)
        if isinstance(step_names, str):
            self.step_names = defaultdict(lambda: step_names)
        else:
            self.step_names = defaultdict(lambda: 'epoch', step_names)

    def update(self, logs: Dict[str, float], current_step: Optional[int] = None) -> None:
        """
        Args:
            logs: dictionary with metric names and values
            current_step: current step of the training loop

        Notes:
            Loop step can be controlled outside or inside main logger with autoincrement of self.current_step
        """
        if current_step is None:
            self.current_step += 1
            current_step = self.current_step
        else:
            self.current_step = current_step
        for k, v in logs.items():
            if k not in self.log_history:
                self._add_new_metric(k)
            self.log_history[k].append(LogItem(step=current_step, value=v))

    def _add_new_metric(self, metric_name: str):
        """Add empty list for a new metric and extend metric name transformations
        Args:
            metric_name: name of metric that will be added to log_history as empty list
        """
        self.log_history[metric_name] = []
        if not self.metric_to_name.get(metric_name):
            self._auto_generate_metrics_to_name(metric_name)

    def _auto_generate_metrics_to_name(self, metric_name: str):
        """The function generate transforms for metric names base on patterns
        Args:
            metric_name: name of new appended metric

        Example:
            It can create transformation from val_acc to Validation Accuracy
        """
        suffix = self._find_suffix_with_group_patterns(metric_name)
        if suffix is None and suffix != metric_name:
            return
        similar_metric_names = [m for m in self.log_history.keys() if m.endswith(suffix)]
        if len(similar_metric_names) == 1:
            return
        for name in similar_metric_names:
            new_name = name
            for pattern_to_replace, replace_with in self.group_patterns:
                new_name = re.sub(pattern_to_replace, replace_with, new_name)
            if suffix in COMMON_NAME_SHORTCUTS.keys():
                new_name = new_name.replace(suffix, COMMON_NAME_SHORTCUTS[suffix])
            self.metric_to_name[name] = new_name

    def grouped_log_history(self,
                            raw_names: bool = False,
                            raw_group_names: bool = False) -> Dict[str, Dict[str, List[LogItem]]]:
        """
        Args:
            raw_names: flag, return raw names instead of transformed by metric to name (as in update() input dictionary)
            raw_group_names: flag, return group names without transforming them with COMMON_NAME_SHORTCUTS

        Returns:
            logs grouped by metric groups - groups are passed in the class constructor

        Notes:
            method use group patterns instead of groups if they are available
        """
        if self.auto_generate_groups:
            self.groups = self._auto_generate_groups()
        ret = {}
        sorted_groups = OrderedDict(sorted(self.groups.items(), key=lambda t: t[0]))
        for group_name, names in sorted_groups.items():
            group_name = group_name if raw_group_names else COMMON_NAME_SHORTCUTS.get(group_name, group_name)
            ret[group_name] = {
                name if raw_names else self.metric_to_name.get(name, name): self.history_shorter(name)
                for name in names
            }
        return ret

    def history_shorter(self, name: str, full: bool = False) -> List[LogItem]:
        """
        Args:
            name: metrics name, e.g. 'val_acc' or 'loss'
            full: flag, if True return all, otherwise as specified by the from_step parameter

        Returns:
            a list of log items
        """
        log_metrics = self.log_history[name]
        if full or self.from_step == 0:
            return log_metrics
        elif self.from_step > 0:
            return [x for x in log_metrics if x.step >= self.from_step]
        else:
            current_from_step = self.current_step + self.from_step
            return [x for x in log_metrics if x.step >= current_from_step]

    def _auto_generate_groups(self) -> Dict[str, List[str]]:
        """
        Returns:
            groups generated with group patterns

        Notes:
            Auto create groups base on val_ prefix - this step is skipped if groups are set
                or if group patterns are available
        """
        groups = {}
        for key in self.log_history.keys():
            abs_key = self._find_suffix_with_group_patterns(key)
            if not groups.get(abs_key):
                groups[abs_key] = []
            groups[abs_key].append(key)
        return groups

    def _find_suffix_with_group_patterns(self, metric_name: str) -> str:
        suffix = metric_name
        for pattern, _ in self.group_patterns:
            match = re.match(pattern, metric_name)
            if match:
                suffix = match.groups()[-1]
        return suffix

    def reset(self) -> None:
        """Method clears logs, groups and reset step counter"""
        self.log_history = {}
        self.groups = {}
        self.current_step = -1

    @property
    def groups(self) -> Dict[str, List[str]]:
        """groups getter"""
        return self._groups

    @groups.setter
    def groups(self, value: Dict[str, List[str]]) -> None:
        """groups setter - groups should be dictionary"""
        if value is None:
            self._groups = {}
        self._groups = value

    @property
    def log_history(self) -> Dict[str, List[LogItem]]:
        """logs getter"""
        return self._log_history

    @log_history.setter
    def log_history(self, value: Dict[str, List[LogItem]]) -> None:
        """logs setter - logs can not be overwritten - you can only reset it to empty state"""
        if len(value) > 0:
            raise RuntimeError('Cannot overwrite log history with non empty dictionary')
        self._log_history = value

Instance variables

var groups : Dict[str, List[str]]

groups getter

Expand source code
@property
def groups(self) -> Dict[str, List[str]]:
    """groups getter"""
    return self._groups
var log_history : Dict[str, List[LogItem]]

logs getter

Expand source code
@property
def log_history(self) -> Dict[str, List[LogItem]]:
    """logs getter"""
    return self._log_history

Methods

def grouped_log_history(self, raw_names: bool = False, raw_group_names: bool = False) ‑> Dict[str, Dict[str, List[LogItem]]]

Args

raw_names
flag, return raw names instead of transformed by metric to name (as in update() input dictionary)
raw_group_names
flag, return group names without transforming them with COMMON_NAME_SHORTCUTS

Returns

logs grouped by metric groups - groups are passed in the class constructor

Notes

method use group patterns instead of groups if they are available

Expand source code
def grouped_log_history(self,
                        raw_names: bool = False,
                        raw_group_names: bool = False) -> Dict[str, Dict[str, List[LogItem]]]:
    """
    Args:
        raw_names: flag, return raw names instead of transformed by metric to name (as in update() input dictionary)
        raw_group_names: flag, return group names without transforming them with COMMON_NAME_SHORTCUTS

    Returns:
        logs grouped by metric groups - groups are passed in the class constructor

    Notes:
        method use group patterns instead of groups if they are available
    """
    if self.auto_generate_groups:
        self.groups = self._auto_generate_groups()
    ret = {}
    sorted_groups = OrderedDict(sorted(self.groups.items(), key=lambda t: t[0]))
    for group_name, names in sorted_groups.items():
        group_name = group_name if raw_group_names else COMMON_NAME_SHORTCUTS.get(group_name, group_name)
        ret[group_name] = {
            name if raw_names else self.metric_to_name.get(name, name): self.history_shorter(name)
            for name in names
        }
    return ret
def history_shorter(self, name: str, full: bool = False) ‑> List[LogItem]

Args

name
metrics name, e.g. 'val_acc' or 'loss'
full
flag, if True return all, otherwise as specified by the from_step parameter

Returns

a list of log items

Expand source code
def history_shorter(self, name: str, full: bool = False) -> List[LogItem]:
    """
    Args:
        name: metrics name, e.g. 'val_acc' or 'loss'
        full: flag, if True return all, otherwise as specified by the from_step parameter

    Returns:
        a list of log items
    """
    log_metrics = self.log_history[name]
    if full or self.from_step == 0:
        return log_metrics
    elif self.from_step > 0:
        return [x for x in log_metrics if x.step >= self.from_step]
    else:
        current_from_step = self.current_step + self.from_step
        return [x for x in log_metrics if x.step >= current_from_step]
def reset(self) ‑> None

Method clears logs, groups and reset step counter

Expand source code
def reset(self) -> None:
    """Method clears logs, groups and reset step counter"""
    self.log_history = {}
    self.groups = {}
    self.current_step = -1
def update(self, logs: Dict[str, float], current_step: Optional[int] = None) ‑> None

Args

logs
dictionary with metric names and values
current_step
current step of the training loop

Notes

Loop step can be controlled outside or inside main logger with autoincrement of self.current_step

Expand source code
def update(self, logs: Dict[str, float], current_step: Optional[int] = None) -> None:
    """
    Args:
        logs: dictionary with metric names and values
        current_step: current step of the training loop

    Notes:
        Loop step can be controlled outside or inside main logger with autoincrement of self.current_step
    """
    if current_step is None:
        self.current_step += 1
        current_step = self.current_step
    else:
        self.current_step = current_step
    for k, v in logs.items():
        if k not in self.log_history:
            self._add_new_metric(k)
        self.log_history[k].append(LogItem(step=current_step, value=v))