Skip to content
This repository was archived by the owner on Jul 10, 2021. It is now read-only.

Commit 8341b72

Browse files
committed
Merge pull request #61 from aigamedev/logging
Automatically configuring logger if it has not been setup
2 parents 84fbaea + 65aaf82 commit 8341b72

File tree

5 files changed

+96
-60
lines changed

5 files changed

+96
-60
lines changed

docs/guide_intermediate.rst

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ Misc. Additions
44
Verbose Mode
55
------------
66

7-
To see the output of the neural network's training, you need to configure two things: first setting up the Python logger (mandatory), and secondly to specify a verbose mode if you want more information during training (optional).
8-
9-
The first step is to configure either the ``sknn`` logger specifically, or do so globally (easier) as follows:
7+
To see the output of the neural network's training, configure the Python logger called ``sknn`` or the default root logger. This is possible using the standard ``logging`` module which you can setup as follows:
108

119
.. code:: python
1210
@@ -18,20 +16,9 @@ The first step is to configure either the ``sknn`` logger specifically, or do so
1816
level=logging.DEBUG,
1917
stream=sys.stdout)
2018
21-
Then you can optionally create your neural networks using an additional ``verbose`` parameter to show the output during training:
22-
23-
.. code:: python
24-
25-
from sknn.mlp import Regressor, Layer
26-
27-
nn = Regressor(
28-
layers=[Layer("Linear")],
29-
n_iter=20,
30-
verbose=True,
31-
valid_size=0.25)
32-
nn.fit(X, y)
19+
Change the log level to ``logging.INFO`` for less information about each epoch, or ``logging.WARNING`` only to receive messages about problems or failures.
3320

34-
This code will output a table containing validation scores at each of the twenty epochs. The ``valid_size`` parameter is a ratio of the data to be used internally for validation; in short, the ``fit()`` function is automatically splitting the data into ``X_train`` and ``y_train`` as well as ``X_valid`` and ``y_valid``.
21+
Using the flag ``verbose=True`` on either :class:`sknn.mlp.Classifier` and :class:`sknn.mlp.Regressor` will setup a default logger at ``DEBUG`` level if it does not exist, and ``verbose=False`` will setup a default logger at level ``WARNING`` if no logging has been configured.
3522

3623

3724
Saving & Loading

examples/bench_cifar10.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,28 @@
33

44
import sys
55
import pickle
6-
import logging
7-
import numpy as np
86

9-
logging.basicConfig(format="%(message)s", level=logging.DEBUG, stream=sys.stdout)
7+
import numpy as np
108

119
PRETRAIN = False
1210

1311

1412
def load(name):
13+
# Pickle module isn't backwards compatible. Hack so it works:
14+
compat = {'encoding': 'latin1'} if sys.version_info[0] == 3 else {}
15+
1516
print("\t"+name)
1617
try:
1718
with open(name, 'rb') as f:
18-
return pickle.load(f) # , encoding='latin1')
19+
return pickle.load(f, **compat)
1920
except IOError:
2021
import gzip
2122
with gzip.open(name+'.gz', 'rb') as f:
22-
return pickle.load(f) # , encoding='latin1')
23+
return pickle.load(f, **compat)
24+
25+
26+
# Download and extract Python data for CIFAR10 manually from here:
27+
# http://www.cs.toronto.edu/~kriz/cifar.html
2328

2429
print("Loading...")
2530
dataset1 = load('data_batch_1')
@@ -38,18 +43,16 @@ def load(name):
3843
n_feat = data_train.shape[1]
3944
n_targets = labels_train.max() + 1
4045

41-
import sys
42-
import logging
43-
logging.basicConfig(format="%(message)s", level=logging.DEBUG, stream=sys.stdout)
4446

4547
from sknn import mlp
48+
4649
nn = mlp.Classifier(
4750
layers=[
48-
mlp.Layer("Sigmoid", units=128),
49-
mlp.Layer("Sigmoid", units=128),
51+
mlp.Layer("Tanh", units=n_feat*2/3),
52+
mlp.Layer("Sigmoid", units=n_feat*1/3),
5053
mlp.Layer("Softmax", units=n_targets)],
51-
n_iter=4,
52-
n_stable=4,
54+
n_iter=50,
55+
n_stable=10,
5356
learning_rate=0.001,
5457
valid_size=0.5,
5558
verbose=1)
@@ -58,8 +61,8 @@ def load(name):
5861
from sknn import ae
5962
ae = ae.AutoEncoder(
6063
layers=[
61-
ae.Layer("Sigmoid", units=128),
62-
ae.Layer("Sigmoid", units=128)],
64+
ae.Layer("Tanh", units=n_feat*2/3),
65+
ae.Layer("Sigmoid", units=n_feat*2/3)],
6366
learning_rate=0.002,
6467
n_iter=10,
6568
verbose=1)
@@ -68,6 +71,7 @@ def load(name):
6871

6972
nn.fit(data_train, labels_train)
7073

74+
7175
from sklearn.metrics import classification_report
7276
from sklearn.metrics import confusion_matrix
7377

examples/bench_mnist.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
import sys
55
import time
6-
import logging
76
import numpy as np
87

98
if len(sys.argv) == 1:
@@ -12,8 +11,6 @@
1211

1312
np.set_printoptions(precision=4)
1413
np.set_printoptions(suppress=True)
15-
logging.basicConfig(format="%(message)s", level=logging.DEBUG, stream=sys.stdout)
16-
1714

1815
from sklearn.base import clone
1916
from sklearn.cross_validation import train_test_split
@@ -52,8 +49,7 @@
5249
valid_size=0.0,
5350
n_stable=10,
5451
n_iter=10,
55-
verbose=1,
56-
)
52+
verbose=True)
5753
classifiers.append(('sknn.mlp', clf))
5854

5955
if 'lasagne' in sys.argv:
@@ -83,8 +79,7 @@
8379
batch_iterator_train=BatchIterator(batch_size=25),
8480

8581
max_epochs=10,
86-
verbose=1
87-
)
82+
verbose=1)
8883
classifiers.append(('nolearn.lasagne', clf))
8984

9085

sknn/nn.py

Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
__all__ = ['Regressor', 'Classifier', 'Layer', 'Convolution']
55

66
import os
7+
import sys
78
import time
89
import logging
910
import itertools
@@ -316,8 +317,10 @@ class NeuralNetwork(object):
316317
317318
loss_type: string, optional
318319
The cost function to use when training the network. There are two valid options:
320+
319321
* ``mse`` — Use mean squared error, for learning to predict the mean of the data.
320322
* ``mae`` — Use mean average error, for learning to predict the median of the data.
323+
321324
The default option is ``mse``, and ``mae`` can only be applied to layers of type
322325
``Linear`` or ``Gaussian`` and they must be used as the output layer.
323326
@@ -327,9 +330,16 @@ class NeuralNetwork(object):
327330
be caught more effectively. Default is off.
328331
329332
verbose: bool, optional
330-
If True, print the score at each epoch via the logger called 'sknn'. You can
331-
control the detail of the output by customising the logger level and formatter.
332-
The default is off.
333+
How to initialize the logging to display the results during training. If there is
334+
already a logger initialized, either ``sknn`` or the root logger, then this function
335+
does nothing. Otherwise:
336+
337+
* ``False`` — Setup new logger that shows only warnings and errors.
338+
* ``True`` — Setup a new logger that displays all debug messages.
339+
* ``None`` — Don't setup a new logger under any condition (default).
340+
341+
Using the built-in python ``logging`` module, you can control the detail and style of
342+
output by customising the verbosity level and formatter for ``sknn`` logger.
333343
"""
334344

335345
def __init__(
@@ -350,7 +360,7 @@ def __init__(
350360
valid_size=0.0,
351361
loss_type='mse',
352362
debug=False,
353-
verbose=False,
363+
verbose=None,
354364
**params):
355365

356366
self.layers = []
@@ -391,7 +401,9 @@ def __init__(
391401
self.loss_type = loss_type
392402
self.debug = debug
393403
self.verbose = verbose
394-
404+
405+
self._create_logger()
406+
395407
assert self.regularize in (None, 'L1', 'L2', 'dropout'),\
396408
"Unknown type of regularization specified: %s." % self.regularize
397409
assert self.loss_type in ('mse', 'mae'),\
@@ -432,6 +444,20 @@ def is_convolution(self):
432444
"""
433445
return isinstance(self.layers[0], Convolution)
434446

447+
def _create_logger(self):
448+
# If users have configured logging already, assume they know best.
449+
if len(log.handlers) > 0 or len(log.parent.handlers) > 0 or self.verbose is None:
450+
return
451+
452+
# Otherwise setup a default handler and formatter based on verbosity.
453+
lvl = logging.DEBUG if self.verbose else logging.WARNING
454+
fmt = logging.Formatter("%(message)s")
455+
hnd = logging.StreamHandler(stream=sys.stdout)
456+
457+
hnd.setFormatter(fmt)
458+
hnd.setLevel(lvl)
459+
log.addHandler(hnd)
460+
435461
def _create_matrix_input(self, X, y=None):
436462
if self.is_convolution:
437463
# Using `b01c` arrangement of data, see this for details:
@@ -478,23 +504,22 @@ def _train_layer(self, trainer, layer, dataset):
478504
layer.monitor.report_epoch()
479505
layer.monitor()
480506

481-
if self.verbose:
482-
objective = layer.monitor.channels.get('objective', None)
483-
if objective:
484-
avg_valid_error = objective.val_shared.get_value()
485-
best_valid_error = min(best_valid_error, avg_valid_error)
486-
else:
487-
# 'objective' channel is only defined with validation set.
488-
avg_valid_error = None
489-
490-
best_valid = bool(best_valid_error == avg_valid_error)
491-
log.debug("{:>5} {}{}{} {:>3.1f}s".format(
492-
i,
493-
ansi.GREEN if best_valid else "",
494-
"{:>10.6f}".format(float(avg_valid_error)) if avg_valid_error else " N/A ",
495-
ansi.ENDC if best_valid else "",
496-
time.time() - start
497-
))
507+
objective = layer.monitor.channels.get('objective', None)
508+
if objective:
509+
avg_valid_error = objective.val_shared.get_value()
510+
best_valid_error = min(best_valid_error, avg_valid_error)
511+
else:
512+
# 'objective' channel is only defined with validation set.
513+
avg_valid_error = None
514+
515+
best_valid = bool(best_valid_error == avg_valid_error)
516+
log.debug("{:>5} {}{}{} {:>3.1f}s".format(
517+
i,
518+
ansi.GREEN if best_valid else "",
519+
"{:>10.6f}".format(float(avg_valid_error)) if avg_valid_error else " N/A ",
520+
ansi.ENDC if best_valid else "",
521+
time.time() - start
522+
))
498523

499524
if not trainer.continue_learning(layer):
500525
log.debug("")

sknn/tests/test_training.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from nose.tools import (assert_in, assert_raises)
2+
from nose.tools import (assert_in, assert_raises, assert_equals)
33

44
import io
55
import logging
@@ -34,6 +34,31 @@ def test_FitAutomaticValidation(self):
3434
self.nn._fit(a_in, a_out)
3535

3636

37+
class TestCustomLogging(unittest.TestCase):
38+
39+
def setUp(self):
40+
self.log = logging.getLogger('sknn')
41+
self.log.handlers = []
42+
self.backup, self.log.parent.handlers = self.log.parent.handlers, []
43+
44+
def tearDown(self):
45+
self.log.parent.handlers = self.backup
46+
47+
def test_DefaultLogVerbose(self):
48+
nn = MLPR(layers=[L("Linear")], verbose=True)
49+
assert_equals(1, len(self.log.handlers))
50+
assert_equals(logging.DEBUG, self.log.handlers[0].level)
51+
52+
def test_DefaultLogQuiet(self):
53+
nn = MLPR(layers=[L("Linear")], verbose=False)
54+
assert_equals(1, len(self.log.handlers))
55+
assert_equals(logging.WARNING, self.log.handlers[0].level)
56+
57+
def test_VerboseNoneNoLog(self):
58+
nn = MLPR(layers=[L("Linear")], verbose=None)
59+
assert_equals(0, len(self.log.handlers))
60+
61+
3762
class TestTrainingOutput(unittest.TestCase):
3863

3964
def setUp(self):

0 commit comments

Comments
 (0)