Exemple #1
0
    def set_trial(self,
                  trial: Optional[Trial] = None,
                  force: bool = False,
                  **kwargs):
        """Set a new trial

        Parameters
        ----------
        trial: Optional[Trial]
            project definition you can use to create or set the project

        force: bool
            by default once the trial is set it cannot be changed.
            use force to override this behaviour.

        kwargs: {uid, hash, revision}
            arguments used to create a `Trial` object if no Trial object were provided.
            You should specify `uid` or the pair `(hash, revision)`.
            See :func:`~track.structure.Trial` for possible arguments

        Returns
        -------
        returns a trial logger
        """
        if self.trial is not None and not force:
            info('Trial is already set, to override use force=True')
            return self.logger

        if trial is None:
            uhash = kwargs.pop('hash', None)
            uid = kwargs.pop('uid', None)
            version = kwargs.pop('version', self.version())

            trial = Trial(version=version, **kwargs)
            if uhash is not None:
                trial.hash = uhash

            if uid is not None:
                trial.uid = uid

        try:
            if trial.version is None:
                trial.version = self.version()

            trials = self.protocol.get_trial(trial)

            if trials is None:
                raise TrialDoesNotExist(
                    f'Trial (hash: {trial.hash}, v:{trial.version} rev: {trial.revision}) does not exist!'
                )

            self.trial = trials[0]
            self.logger = TrialLogger(self.trial, self.protocol)
            return self.logger

        except IndexError:
            raise TrialDoesNotExist(
                f'cannot set trial (id: {trial.uid}, hash:{hash}) it does not exist'
            )
Exemple #2
0
    def log_arguments(self,
                      args: Union[ArgumentParser, Namespace, Dict] = None,
                      show=False,
                      **kwargs) -> Namespace:
        """Store the arguments that was used to run the trial.

        Parameters
        ----------
        args: Union[ArgumentParser, Namespace, Dict]
            save up the trial's arguments

        show: bool
            print the arguments on the command line

        kwargs
            more trial's arguments

        Returns
        -------
        returns the trial's arguments
        """
        nargs = dict()
        if args is not None:
            nargs = args

        if isinstance(args, ArgumentParser):
            nargs = args.parse_args()

        if isinstance(nargs, Namespace):
            nargs = dict(**vars(nargs))

        kwargs.update(nargs)

        # if we have a pending trial create it now as we have all the information
        if is_delayed_call(self.trial):
            self.trial = self.trial(parameters=kwargs)
            self.logger = TrialLogger(self.trial, self.protocol)
        else:
            # we do not need to log the arguments they are inside the trial already
            self.logger.log_arguments(**kwargs)

        if show:
            print('-' * 80)
            for k, v in vars(args).items():
                print(f'{k:>30}: {v}')
            print('-' * 80)

        return args
Exemple #3
0
    def new_trial(self, force=False, **kwargs):
        """Create a new trial

        Parameters
        ----------
        force: bool
            by default once the trial is set it cannot be changed.
            use force to override this behaviour.

        kwargs:
            See :func:`~track.structure.Trial` for possible arguments

        Returns
        -------
        returns a trial logger
        """
        self.trial = self._new_trial(force, **kwargs)
        self.logger = TrialLogger(self.trial, self.protocol)
        return self.logger
Exemple #4
0
class TrackClient:
    """ TrackClient. A client tracks a single Trial being ran

    Parameters
    ----------
    backend: str
        Storage backend to use
    """
    def __init__(self, backend=options('log.backend.name', default='none')):
        self.project = None
        self.group = None
        self.trial = None

        self.protocol = get_protocol(backend)
        self.logger: TrialLogger = None
        self.set_version()

        self.version = None
        self.set_version()

        # Orion integration
        # -----------------
        orion_name = os.environ.get('ORION_PROJECT')
        if orion_name is not None:
            self.set_project(name=orion_name, get_only=True)

        orion_exp = os.environ.get('ORION_EXPERIMENT')
        if orion_exp is not None:
            self.set_group(name=orion_exp, get_only=True)

        orion_trial = os.environ.get('ORION_TRIAL_ID')
        if orion_trial is not None:
            self.set_trial(uid=orion_trial)

    def set_version(self, version=None, version_fun: Callable[[], str] = None):
        """Compute the version tag from the function call stack. Defaults to compute the hash of the executed file

        Parameters
        ----------
        version: str
            version string you want to use for the trial

        version_fun: Callable[[], str]
            version function to call to set the trial version
        """
        def version_compute():
            fun = version_fun
            if fun is None:
                fun = default_version_hash

            if version is None:
                return fun()
            else:
                return version

        self.version = version_compute
        return self

    def set_project(self,
                    project: Optional[Project] = None,
                    force: bool = False,
                    get_only: bool = False,
                    **kwargs):
        """Set or create a new project

        Parameters
        ----------
        project: Optional[Project]
            project definition you can use to create or set the project

        force: bool
            by default once the project is set it cannot be changed.
            use force to override this behaviour.

        get_only: bool
            if true does not insert the project if missing.
            default to false

        kwargs
            arguments used to create a `Project` object if no project object were provided
            See :func:`~track.structure.Project` for possible arguments

        Returns
        -------
        returns created project
        """
        if self.project is not None and not force:
            info('Project is already set, to override use force=True')
            return self.project

        if project is None:
            project = Project(**kwargs)

        assert project.name is not None, 'Project name cannot be none'

        # does the project exist ?
        self.project = self.protocol.get_project(project)

        if self.project is not None:
            return self.project

        if get_only:
            raise RuntimeError(
                f'Project (name: {project.name}) was not found!')

        self.project = self.protocol.new_project(project)

        debug(f'set project to (project: {self.project.name})')
        return self.project

    def set_group(self,
                  group: Optional[TrialGroup] = None,
                  force: bool = False,
                  get_only: bool = False,
                  **kwargs):
        """Set or create a new group

        Parameters
        ----------
        group: Optional[TrialGroup]
            project definition you can use to create or set the project

        force: bool
            by default once the trial group is set it cannot be changed.
            use force to override this behaviour.

        get_only: bool
            if true does not insert the group if missing.
            default to false

        kwargs
            arguments used to create a `TrialGroup` object if no TrialGroup object were provided.
            See :func:`~track.structure.TrialGroup` for possible arguments

        Returns
        -------
        returns created trial group
        """

        if self.group is not None and not force:
            info('Group is already set, to override use force=True')
            return self.group

        if group is None:
            group = TrialGroup(**kwargs)

        if group.project_id is None:
            group.project_id = self.project.uid

        self.group = self.protocol.get_trial_group(group)

        if self.group is not None:
            return self.group

        if get_only:
            raise RuntimeError(f'Group (name: {group.name}) was not found!')

        self.group = self.protocol.new_trial_group(group)
        return self.group

    def set_trial(self,
                  trial: Optional[Trial] = None,
                  force: bool = False,
                  **kwargs):
        """Set a new trial

        Parameters
        ----------
        trial: Optional[Trial]
            project definition you can use to create or set the project

        force: bool
            by default once the trial is set it cannot be changed.
            use force to override this behaviour.

        kwargs: {uid, hash, revision}
            arguments used to create a `Trial` object if no Trial object were provided.
            You should specify `uid` or the pair `(hash, revision)`.
            See :func:`~track.structure.Trial` for possible arguments

        Returns
        -------
        returns a trial logger
        """
        if self.trial is not None and not force:
            info('Trial is already set, to override use force=True')
            return self.logger

        if trial is None:
            uhash = kwargs.pop('hash', None)
            uid = kwargs.pop('uid', None)
            version = kwargs.pop('version', self.version())

            trial = Trial(version=version, **kwargs)
            if uhash is not None:
                trial.hash = uhash

            if uid is not None:
                trial.uid = uid

        try:
            if trial.version is None:
                trial.version = self.version()

            trials = self.protocol.get_trial(trial)

            if trials is None:
                raise TrialDoesNotExist(
                    f'Trial (hash: {trial.hash}, v:{trial.version} rev: {trial.revision}) does not exist!'
                )

            self.trial = trials[0]
            self.logger = TrialLogger(self.trial, self.protocol)
            return self.logger

        except IndexError:
            raise TrialDoesNotExist(
                f'cannot set trial (id: {trial.uid}, hash:{hash}) it does not exist'
            )

    def _new_trial(self, force=False, parameters=None, **kwargs):
        """Create a new trial if all the required arguments are satisfied.

        To provide a better user experience if not all arguments are provided a delayed trials is created
        that holds all the data provided and will create the trial once all arguments are ready.
        Currently only `arguments` i.e the parameters of the experience is required. This is
        because they are needed to compute the trial uid (which is a hash of the parameters).

        If no project is set, the trial is inserted in a catch all project named `orphan`

        Parameters
        ----------
        force: bool
            by default once the trial is set it cannot be changed.
            use force to override this behaviour.

        kwargs
            See :func:`~track.structure.Trial` for possible arguments

        Returns
        -------
        returns a trial
        """
        if isinstance(parameters, Namespace):
            parameters = dict(**vars(parameters))

        if self.trial is not None and not is_delayed_call(
                self.trial) and not force:
            info(f'Trial is already set, to override use force=True')
            return self.trial

        # if arguments are not specified do not create the trial just yet
        # wait for the user to be able to specify the parameters so we can have a meaningful hash
        if parameters is None:
            if is_delayed_call(self.trial):
                raise RuntimeError('Trial needs parameters')

            self.trial = delay_call(self._new_trial, **kwargs)
            # return the logger with the delayed trial
            return self.trial

        # replace the trial or delayed trial by its actual value
        if parameters or is_delayed_call(self.trial):
            self.trial = self._make_trial(parameters=parameters, **kwargs)

        assert self.trial is not None, f'Trial cant be None because {parameters} or {is_delayed_call(self.trial)}'

        if self.project is None:
            self.project = self.set_project(name='orphan')

        assert self.project is not None, 'Project cant be None'

        self.protocol.add_project_trial(self.project, self.trial)

        if self.group is not None:
            self.protocol.add_group_trial(self.group, self.trial)

        return self.trial

    def new_trial(self, force=False, **kwargs):
        """Create a new trial

        Parameters
        ----------
        force: bool
            by default once the trial is set it cannot be changed.
            use force to override this behaviour.

        kwargs:
            See :func:`~track.structure.Trial` for possible arguments

        Returns
        -------
        returns a trial logger
        """
        self.trial = self._new_trial(force, **kwargs)
        self.logger = TrialLogger(self.trial, self.protocol)
        return self.logger

    def _make_trial(self, parameters, name=None, **kwargs):
        project_id = None
        group_id = None
        if self.project is not None:
            project_id = self.project.uid

        if self.group is not None:
            group_id = self.group.uid

        trial = Trial(name=name,
                      version=self.version(),
                      project_id=project_id,
                      group_id=group_id,
                      parameters=parameters,
                      **kwargs)

        trial = self.protocol.new_trial(trial)
        assert trial is not None, 'Trial already exist!'
        return trial

    def add_tags(self, **kwargs):
        """Insert tags to current trials"""
        # We do not need to create the trial to add tags.
        # just append the tags to the trial call when it is going to be created
        if is_delayed_call(self.trial):
            self.trial.add_arguments(tags=kwargs)
        else:
            self.logger.add_tags(**kwargs)

    def get_arguments(self,
                      args: Union[ArgumentParser, Namespace, Dict] = None,
                      show=False,
                      **kwargs) -> Namespace:
        """See :func:`~track.client.log_arguments` for possible arguments"""
        return self.log_arguments(args, show, **kwargs)

    def log_arguments(self,
                      args: Union[ArgumentParser, Namespace, Dict] = None,
                      show=False,
                      **kwargs) -> Namespace:
        """Store the arguments that was used to run the trial.

        Parameters
        ----------
        args: Union[ArgumentParser, Namespace, Dict]
            save up the trial's arguments

        show: bool
            print the arguments on the command line

        kwargs
            more trial's arguments

        Returns
        -------
        returns the trial's arguments
        """
        nargs = dict()
        if args is not None:
            nargs = args

        if isinstance(args, ArgumentParser):
            nargs = args.parse_args()

        if isinstance(nargs, Namespace):
            nargs = dict(**vars(nargs))

        kwargs.update(nargs)

        # if we have a pending trial create it now as we have all the information
        if is_delayed_call(self.trial):
            self.trial = self.trial(parameters=kwargs)
            self.logger = TrialLogger(self.trial, self.protocol)
        else:
            # we do not need to log the arguments they are inside the trial already
            self.logger.log_arguments(**kwargs)

        if show:
            print('-' * 80)
            for k, v in vars(args).items():
                print(f'{k:>30}: {v}')
            print('-' * 80)

        return args

    def __getattr__(self, item):
        """Try to use the backend attributes if not available"""

        if is_delayed_call(self.trial):
            warning('Creating a trial without parameters!')
            self.logger = self.trial()
            self.trial = self.logger.trial

        # Look for the attribute in the top level logger
        if hasattr(self.logger, item):
            return getattr(self.logger, item)

        raise AttributeError(item)

    def report(self, short=True):
        """Print a digest of the logged metrics"""
        self.logger.finish()
        print(json.dumps(to_json(self.trial, short), indent=2))
        return self

    def save(self, file_name_override=None):
        """Saved logged metrics into a json file"""
        self.protocol.commit(file_name_override=file_name_override)

    @staticmethod
    def get_device():
        """Helper function that returns a cuda device if available else a cpu"""
        import torch

        if torch.cuda.is_available():
            return torch.device('cuda')
        return torch.device('cpu')

    def finish(self, exc_type=None, exc_val=None, exc_tb=None):
        return self.logger.finish(exc_type, exc_val, exc_tb)

    def start(self):
        return self.logger.start()

    def __enter__(self):
        return self.logger.__enter__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        return self.logger.__exit__(exc_type, exc_val, exc_tb)