Sacred

  • 모든 실험은 신성하다
  • 모든 실험은 훌륭함
  • 만약 실험이 낭비되면 신이 매우 화를 낼 것

  • Sacred는 실험을 구성, 기록 복제하는 도구

    • 최소한의 오버 헤드를 도입해 모듈화, 실험 구성을 도움
  • 기능

    • 실험의 모든 파라미터 추적
    • 여러 설정에서 실험을 쉽게 할 수 있음
    • 파일이나 데이터베이스에 실행 설정을 저장
    • 결과 재현
  • 메인 메커니즘

    • Config Scope는 @ex.config 데코레이터 함수를 사용해 쉽게 설정 가능
    • dependency injection을 통해 캡쳐된 기능을 사용할 수 있음. 시스템이 파라미터를 전달해 쉽게 config value를 사용할 수 있음
    • Command line interface가 쉽게 파라미터를 바꿀 수 있게 함
    • 실험 및 구성에 대한 모든 정보를 기록해 실험을 추적
    • Automatic seed는 무작위성을 제어해 재현성을 유지할 때 도움이 됨

In [28]:
from numpy.random import permutation
from sklearn import svm, datasets
from sacred import Experiment
ex = Experiment('iris_rbf_svm', interactive=True)
# jupyter notebook일 경우 interactive=True, python 스크립트라면 없어도 됨

@ex.config
def cfg():
    C = 1.0
    gamma = 0.7

# ex.automain은 python 스크립트일 때 사용
@ex.main
def run(C, gamma):
    iris = datasets.load_iris()
    per = permutation(iris.target.size)
    iris.data = iris.data[per]
    iris.target = iris.target[per]
    clf = svm.SVC(C, 'rbf', gamma=gamma)
    clf.fit(iris.data[:90],
          iris.target[:90])
    return clf.score(iris.data[90:],
                   iris.target[90:])

In [12]:
ex.run()


WARNING - iris_rbf_svm - No observers have been added to this run
INFO - iris_rbf_svm - Running command 'run'
INFO - iris_rbf_svm - Started
INFO - iris_rbf_svm - Result: 0.9833333333333333
INFO - iris_rbf_svm - Completed after 0:00:00
Out[12]:
<sacred.run.Run at 0x11f5970f0>

In [13]:
from sacred import Experiment
ex = Experiment('my_experiment', interactive=True)

@ex.config
def my_config():
    foo = 42
    bar = 'baz'

@ex.capture
def some_function(a, foo, bar=10):
    print(a, foo, bar)

@ex.main
def my_main():
    some_function(1, 2, 3)     #  1  2   3
    some_function(1)           #  1  42  'baz'
    some_function(1, bar=12)   #  1  42  12
    some_function()            #  TypeError: missing value for 'a'

In [14]:
ex.run()


WARNING - my_experiment - No observers have been added to this run
INFO - my_experiment - Running command 'my_main'
INFO - my_experiment - Started
ERROR - my_experiment - Failed after 0:00:00!
1 2 3
1 42 baz
1 42 12
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-106ea74a54b1> in <module>()
----> 1 ex.run()

/usr/local/lib/python3.6/site-packages/sacred/experiment.py in run(self, command_name, config_updates, named_configs, meta_info, options)
    207         run = self._create_run(command_name, config_updates, named_configs,
    208                                meta_info, options)
--> 209         run()
    210         return run
    211 

/usr/local/lib/python3.6/site-packages/sacred/run.py in __call__(self, *args)
    219                 self._start_heartbeat()
    220                 self._execute_pre_run_hooks()
--> 221                 self.result = self.main_function(*args)
    222                 self._execute_post_run_hooks()
    223                 if self.result is not None:

/usr/local/lib/python3.6/site-packages/sacred/config/captured_function.py in captured_function(wrapped, instance, args, kwargs)
     44         start_time = time.time()
     45     # =================== run actual function =================================
---> 46     result = wrapped(*args, **kwargs)
     47     # =========================================================================
     48     if wrapped.logger is not None:

<ipython-input-13-b76dd844dd54> in my_main()
     16     some_function(1)           #  1  42  'baz'
     17     some_function(1, bar=12)   #  1  42  12
---> 18     some_function()            #  TypeError: missing value for 'a'

/usr/local/lib/python3.6/site-packages/sacred/config/captured_function.py in captured_function(wrapped, instance, args, kwargs)
     39     bound = (instance is not None)
     40     args, kwargs = wrapped.signature.construct_arguments(args, kwargs, options,
---> 41                                                          bound)
     42     if wrapped.logger is not None:
     43         wrapped.logger.debug("Started")

/usr/local/lib/python3.6/site-packages/sacred/config/signature.py in construct_arguments(self, args, kwargs, options, bound)
    100         args, kwargs = self._fill_in_options(args, kwargs, options, bound)
    101 
--> 102         self._assert_no_missing_args(args, kwargs, bound)
    103         return args, kwargs
    104 

/usr/local/lib/python3.6/site-packages/sacred/config/signature.py in _assert_no_missing_args(self, args, kwargs, bound)
    158         if missing_args:
    159             raise TypeError("{} is missing value(s) for {}".format(
--> 160                 self.name, missing_args))

TypeError: some_function is missing value(s) for ['a']

Observing an Experiment

  • The main one is the Mongo Observer which stores all information in a MongoDB.
  • The File Storage Observer stores the run information as files in a given directory and will therefore only work locally.
  • The TinyDB Observer provides another local way of observing experiments by using tinydb to store run information in a JSON file.
  • The SQL Observer connects to any SQL database and will store the relevant information there.

In [17]:
from sacred.observers import MongoObserver

ex.observers.append(MongoObserver.create())

In [18]:
from pymongo import MongoClient

In [19]:
import urllib.parse

In [20]:
username = urllib.parse.quote_plus('user')

In [21]:
password = urllib.parse.quote_plus('password')

In [23]:
from sacred.observers import MongoObserver

ex.observers.append(MongoObserver.create(
    url='mongodb://user:password@example.com/the_database?authMechanism=SCRAM-SHA-1',
    db_name='MY_DB'))

Filt Storage Observer


In [27]:
from sacred.observers import FileStorageObserver

In [43]:
ex = Experiment('iris_rbf_svm', interactive=True)
# jupyter notebook일 경우 interactive=True, python 스크립트라면 없어도 됨

@ex.config
def cfg():
    C = 1.0
    gamma = 0.7

# ex.automain은 python 스크립트일 때 사용
@ex.main
def run(C, gamma):
    iris = datasets.load_iris()
    per = permutation(iris.target.size)
    iris.data = iris.data[per]
    iris.target = iris.target[per]
    clf = svm.SVC(C, 'rbf', gamma=gamma)
    clf.fit(iris.data[:90],
          iris.target[:90])
    return clf.score(iris.data[90:],
                   iris.target[90:])

ex.observers.append(FileStorageObserver.create('my_runs'))

In [44]:
ex.run()


INFO - iris_rbf_svm - Running command 'run'
INFO - iris_rbf_svm - Started run with ID "1"
INFO - iris_rbf_svm - Result: 0.95
INFO - iris_rbf_svm - Completed after 0:00:00
Out[44]:
<sacred.run.Run at 0x11f5ce5f8>

Slack Observer

실험과 logging을 합치고 싶은 경우

  • _log 사용

In [46]:
@ex.capture
def some_function(_log):
    _log.warning('My warning message!')

In [48]:
#!/usr/bin/env python
# coding=utf-8
""" An example showcasing the logging system of Sacred."""
from __future__ import division, print_function, unicode_literals
import logging
from sacred import Experiment

ex = Experiment('log_example', interactive=True)

# set up a custom logger
logger = logging.getLogger('mylogger')
logger.handlers = []
ch = logging.StreamHandler()
formatter = logging.Formatter('[%(levelname).1s] %(name)s >> "%(message)s"')
ch.setFormatter(formatter)
logger.addHandler(ch)
logger.setLevel('INFO')

# attach it to the experiment
ex.logger = logger


@ex.config
def cfg():
    number = 2
    got_gizmo = False


@ex.capture
def transmogrify(got_gizmo, number, _log):
    if got_gizmo:
        _log.debug("Got gizmo. Performing transmogrification...")
        return number * 42
    else:
        _log.warning("No gizmo. Can't transmogrify!")
        return 0


@ex.main
def main(number, _log):
    _log.info('Attempting to transmogrify %d...', number)
    result = transmogrify()
    _log.info('Transmogrification complete: %d', result)
    return result

In [49]:
ex.run()


[W] mylogger.log_example >> "No observers have been added to this run"
WARNING - mylogger.log_example - No observers have been added to this run
[I] mylogger.log_example >> "Running command 'main'"
INFO - mylogger.log_example - Running command 'main'
[I] mylogger.log_example >> "Started"
INFO - mylogger.log_example - Started
[I] mylogger.main >> "Attempting to transmogrify 2..."
INFO - mylogger.main - Attempting to transmogrify 2...
[W] mylogger.transmogrify >> "No gizmo. Can't transmogrify!"
WARNING - mylogger.transmogrify - No gizmo. Can't transmogrify!
[I] mylogger.main >> "Transmogrification complete: 0"
INFO - mylogger.main - Transmogrification complete: 0
[I] mylogger.log_example >> "Result: 0"
INFO - mylogger.log_example - Result: 0
[I] mylogger.log_example >> "Completed after 0:00:00"
INFO - mylogger.log_example - Completed after 0:00:00
Out[49]:
<sacred.run.Run at 0x102c54908>

In [50]:
from __future__ import division, print_function, unicode_literals
from sacred import Experiment

ex = Experiment('hello_config', interactive=True)


@ex.named_config
def rude():
    """A rude named config"""
    recipient = "bastard"
    message = "Fuck off you {}!".format(recipient)


@ex.config
def cfg():
    recipient = "world"
    message = "Hello {}!".format(recipient)


@ex.main
def main(message):
    print(__name__)
    print(message)

In [51]:
ex.run()


WARNING - hello_config - No observers have been added to this run
INFO - hello_config - Running command 'main'
INFO - hello_config - Started
INFO - hello_config - Completed after 0:00:00
__main__
Hello world!
Out[51]:
<sacred.run.Run at 0x11f8a6400>

Basic Example


In [ ]:
from __future__ import division, print_function, unicode_literals
from sacred import Experiment, Ingredient

# ============== Ingredient 0: settings =================
s = Ingredient("settings")


@s.config
def cfg1():
    verbose = True


# ============== Ingredient 1: dataset.paths =================
data_paths = Ingredient("dataset.paths", ingredients=[s])


@data_paths.config
def cfg2(settings):
    v = not settings['verbose']
    base = '/home/sacred/'


# ============== Ingredient 2: dataset =======================
data = Ingredient("dataset", ingredients=[data_paths, s])


@data.config
def cfg3(paths):
    basepath = paths['base'] + 'datasets/'
    filename = "foo.hdf5"


@data.capture
def foo(basepath, filename, paths, settings):
    print(paths)
    print(settings)
    return basepath + filename


# ============== Experiment ==============================
ex = Experiment('modular_example', ingredients=[data, data_paths])


@ex.config
def cfg(dataset):
    a = 10
    b = 17
    c = a + b
    out_base = dataset['paths']['base'] + 'outputs/'
    out_filename = dataset['filename'].replace('.hdf5', '.out')


@ex.automain
def main(a, b, c, out_base, out_filename, dataset):
    print('a =', a)
    print('b =', b)
    print('c =', c)
    print('out_base =', out_base, out_filename)
    # print("dataset", dataset)
    # print("dataset.paths", dataset['paths'])
    print("foo()", foo())