提交 18deb7a0 authored 作者: James Bergstra's avatar James Bergstra

merge

......@@ -11,7 +11,6 @@ Structure
.. toctree::
:maxdepth: 2
graphstructures
env
features
optimization
......
.. _unittest:
===============
Unit Testing
===============
Theano relies heavily on unit testing. Its importance cannot be stressed enough !
Unit Testing revolves around the following principles:
* ensuring correctness: making sure that your Op, Type or Optimization works in the way you intended it to work. It is important for this testing to be as thorough as possible: test not only the obvious cases, but more importantly the corner cases which are more likely to trigger bugs down the line.
* test all possible failure paths. This means testing that your code fails in the appropriate manner, by raising the correct errors when in certain situations.
* sanity check: making sure that everything still runs after you've done your modification. If your changes cause unit tests to start failing, it could be that you've changed an API on which other users rely on. It is therefore your responsibility to either a) provide the fix or b) inform the author of your changes and coordinate with that person to produce a fix. If this sounds like too much of a burden... then good ! APIs aren't meant to be changed on a whim !
This page is in no way meant to replace tutorials on Python's unittest module, for this we refer the reader to the `official documentation <http://docs.python.org/library/unittest.html>`_. We will however adress certain specificities about how unittests relate to theano.
How to Run Unit Tests ?
=======================
Running all unit tests
>>> cd Theano/theano
>>> nosetests
Running unit tests with standard out
>>> nosetests -s
Running unit tests contained in a specific .py file
>>> nosetests <filename>.py
Running a specific unit test
>>> nosetests <filename>.py:<classname>.<method_name>
Folder Layout
=============
"tests" directories are scattered throughout theano. Each tests subfolder is
meant to contain the unittests which validate the .py files in the parent folder.
Files containing unittests should be prefixed with the word "test".
Optimally every python module should have a unittest file associated with it,
as shown below. Unittests testing functionality of module <module>.py should therefore be stored in tests/test_<module>.py
>>> Theano/theano/tensor/basic.py
>>> Theano/theano/tensor/elemwise.py
>>> Theano/theano/tensor/tests/test_basic.py
>>> Theano/theano/tensor/tests/test_elemwise.py
How to Write a Unittest
=======================
Test Cases and Methods
----------------------
Unittests should be grouped "logically" into test cases, which are meant to
group all unittests operating on the same element and/or concept. Test cases
are implemented as Python classes which inherit from unittest.TestCase
Test cases contain multiple test methods. These should be prefixed with the
word "test".
Test methods should be as specific as possible and cover a particular aspect
of the problem. For example, when testing the TensorDot Op, one test method
could check for validity, while another could verify that the proper errors
are raised when inputs have invalid dimensions.
Test method names should be as explicit as possible, so that users can see at
first glance, what functionality is being tested and what tests need to be added.
Example:
>>> import unittest
>>> class TestTensorDot(unittest.TestCase):
>>> def test_validity(self):
>>> # do stuff
>>> def test_invalid_dims(self):
>>> # do more stuff
Test cases can define a special setUp method, which will get called before
each test method is executed. This is a good place to put functionality which
is shared amongst all test methods in the test case (i.e initializing data,
parameters, seeding random number generators -- more on this later)
>>> class TestTensorDot(unittest.TestCase):
>>> def setUp(self):
>>> # data which will be used in various test methods
>>> self.avals = numpy.array([[1,5,3],[2,4,1]])
>>> self.bvals = numpy.array([[2,3,1,8],[4,2,1,1],[1,4,8,5]])
Similarly, test cases can define a tearDown method, which will be implicitely
called at the end of each test method.
Checking for correctness
------------------------
When checking for correctness of mathematical expressions, the user should
preferably compare theano's output to the equivalent numpy implementation.
Example:
>>> class TestTensorDot(unittest.TestCase):
>>> def setUp(self):
>>> ...
>>>
>>> def test_validity(self):
>>> a = T.dmatrix('a')
>>> b = T.dmatrix('b')
>>> c = T.dot(a,b)
>>> f = theano.function([a,b],[c])
>>> cmp = f(self.avals,self.bvals) == numpy.dot(self.avals,self.bvals)
>>> self.failUnless(numpy.all(cmp))
Avoid hard-coding results, as in the following case:
>>> self.failUnless(numpy.all(f(self.avals,self.bvals)==numpy.array([[25,25,30,28],[21,18,14,25]])))
This makes the test case less manageable and forces the user to update the
results each time the input is changed or possibly when the module being
tested changes (after a bug fix for example). It also constrains the test case
to specific input/output data pairs. The section on random values covers why this
might not be such a good idea.
Here is a list of useful functions, as defined by TestCase:
* checking the state of boolean variables: assert_, failUnless, assertTrue, failIf, assertFalse
* checking for (in)equality constraints: assertEqual, failUnlessEqual, assertNotEqual, failIfEqual
* checking for (in)equality constraints up to a given precision (very useful in theano): assertAlmostEqual, failUnlessAlmostEqual, assertNotAlmostEqual, failIfAlmostEqual
Checking for errors
-------------------
On top of verifying that your code provides the correct output, it is equally
important to test that it fails in the appropriate manner, raising the
appropriate exceptions, etc. Silent failures are deadly, as they can go unnoticed
for a long time and a hard to detect "after-the-fact".
Example:
>>> class TestTensorDot(unittest.TestCase):
>>> ...
>>> def test_3D_dot_fail(self):
>>> def func():
>>> a = T.NDArrayType('float64', (False,False,False)) # create 3d tensor
>>> b = T.dmatrix()
>>> c = T.dot(a,b) # we expect this to fail
>>> # above should fail as dot operates on 2D tensors only
>>> self.failUnlessRaises(TypeError, func)
Useful functions, as defined by TestCase:
* assertRaises, failUnlessRaises
Test Cases and Theano Modes
---------------------------
When compiling theano functions or modules, a mode parameter can be given to specify which linker and optimizer to use.
Example:
>>> f = T.function([a,b],[c],mode='FAST_RUN')
>>> m = theano.Module()
>>> minstance = m.make(mode='DEBUG_MODE')
Whenever possible, unit tests should omit this parameter. Leaving-out the mode will ensure that unit tests use the default mode (defined in compile.mode.default_mode). This default_mode is set to the THEANO_DEFAULT_MODE environment variable, if it is present. If not, it defaults to 'FAST_RUN'.
This allows the user to easily switch the mode in which unittests are run. For example the nightly-build system iterates over all modes.
>>> THEANO_DEFAULT_MODE=FAST_COMPILE nosetests
>>> THEANO_DEFAULT_MODE=FAST_RUN nosetests
>>> THEANO_DEFAULT_MODE=DEBUG_MODE nosetests
Using Random Values in Test Cases
---------------------------------
numpy.random is often used in unit tests to initialize large data structures,
for use as inputs to the function or module being tested. When
doing this, it is imperative that the random number generator be seeded at the
be beginning of each unit test. This will ensure that unittest behaviour is
consistent from one execution to another (i.e always pass or always fail).
Instead of using numpy.random.seed to do this, we encourage users to do the
following:
>>> from theano.tests import unittest_tools
>>>
>>> class TestTensorDot(unittest.TestCase):
>>> def setUp(self):
>>> unittest_tools.seed_rng()
>>> # OR ... call with an explicit seed
>>> unittest_tools.seed_rng(234234)
The behaviour of seed_rng is as follows:
* if the environment variable THEANO_UNITTEST_SEED is defined, it will be used to seed the random number generator (and override any seed provided by the user)
* if THEANO_UNITTEST_SEED is not defined, the user-supplied seed will be used to seed the rng
* if THEANO_UNITTEST_SEED is not defined and no seed is given, the rng will be seeded with a random seed.
The main advantage of using unittest_tools.seed_rng is that it allows us to
change the seed used in the unitests, without having to manually edit all the
files. For example, this allows the nightly build to run nosetests repeatedly,
changing the seed on every run (hence achieving a higher confidence that the
results are correct), while still making sure unittests are deterministic.
Users who prefer their unittests to be random (when run on their local machine)
can simply undefine THEANO_UNITTEST_SEED.
Similarly, to provide a seed to numpy.random.RandomState, simply use:
>>> rng = numpy.random.RandomState(unittest_tools.fetch_seed())
>>> # OR providing an explicit seed
>>> rng = numpy.random.RandomState(unittest_tools.fetch_seed(1231))
Note that the ability to change the seed from one nosetest to another, is incompatible with the method of hard-coding the baseline results (against which we compare the theano outputs). These must then be determined "algorithmically". Although this represents more work, the test suite will be better because of it.
......@@ -47,7 +47,7 @@ The following command will install the latest revision of Theano
on your system:
.. TODO: Does this install the latest package version, or the latest Mercurial
.. revision?
.. revision ?
.. code-block:: bash
......
......@@ -86,14 +86,14 @@ An Op is any object which defines the following methods:
gradient symbolically in this method.
- Both the inputs and output_gradients will be Results. This function must
return a list containg one Result (or None) for each input.
return a list containing one Result (or None) for each input.
Each returned Result represents the gradient wrt that input given the
symbolic gradients wrt each output.
- If the output is not differentiable with respect to any inputs, then this
function should be defined to return [None for i in inputs].
- If this method is not defined, then theano assumes it hsa been forgotten.
- If this method is not defined, then theano assumes it has been forgotten.
Symbolic differentiation will fail on a graph that includes this Op.
- For more information on the use of this method, see ``grad``.
......
......@@ -24,10 +24,10 @@ that declares the right data types and that contains the right number
of loops over the dimensions.
Note that a Theano :ref:`type` is not equivalent to a Python type or
class. Indeed, in Theano, :ref:`irow <predefinedtypes>` and
:ref:`dmatrix <predefinedtypes>` both use ``numpy.ndarray`` as the
working data type, yet they are different Theano Types. Indeed, the
constraints set by ``dmatrix`` are:
class. Indeed, in Theano, :ref:`irow <predefinedtypes>` and :ref:`dmatrix
<predefinedtypes>` both use ``numpy.ndarray`` as the underlying type
for doing computations and storing data, yet they are different Theano
Types. Indeed, the constraints set by ``dmatrix`` are:
#. Must be an instance of ``numpy.ndarray``: ``isinstance(x, numpy.ndarray)``
#. Must be an array of 64-bit floating point numbers: ``str(x.dtype) == 'float64'``
......@@ -45,13 +45,21 @@ Theano Type.
Type's contract
===============
In Theano's framework, a Type is any object which
defines the following methods:
In Theano's framework, a Type is any object which defines the following
methods. To obtain the default methods described below, the Type should
be an instance of :api:``theano.gof.Type`` or should be an instance of a
subclass of :api:``theano.gof.Type``. If you will write all methods yourself,
you need not use an instance of :api:``theano.gof.Type``.
- **filter(value, strict [= False])**
Methods with default arguments must be defined with the same signature,
i.e. the same default argument names and values. If you wish to add
extra arguments to any of these methods, these extra arguments must have
default values.
- This casts or wraps a value to match the Type and returns the
casted/wrapped value. If ``value`` is incompatible with the Type,
- **filter(value, strict=False)**
- This casts a value to match the Type and returns the
casted value. If ``value`` is incompatible with the Type,
the method must raise an exception. If ``strict`` is True, ``filter`` must return a
reference to ``value`` (i.e. casting prohibited)
......@@ -61,41 +69,51 @@ defines the following methods:
- **is_valid_value(value)**
- Returns True iff the value is exactly compatible with the Type.
- Returns True iff the value is compatible with the Type. If
``filter(value, strict = True)`` does not raise an exception, the
value is compatible with the Type.
- *Default*: defined in terms of ``filter(value, strict = True)``
- *Default*: True iff ``filter(value, strict = True)`` does not raise an
exception.
- **values_eq(a, b)**
- Returns True iff ``a`` and ``b`` are valid values of this Type and
are equal.
- Returns True iff ``a`` and ``b`` are equal.
- *Default*: a == b
- *Default*: ``a == b``
- **values_eq_approx(a, b)**
- Returns True iff ``a`` and ``b`` are valid values of this Type and
are approximately equal, for a definition of approximately which
- Returns True iff ``a`` and ``b``
are approximately equal, for a definition of "approximately" which
varies from Type to Type.
- *Default*: same as ``values_eq``
- *Default*: ``values_eq(a, b)``
- **make_result(name [= None])**
- **make_result(name=None)**
- Makes a :term:`Result` of this Type with the specified name. The
Result will have its ``type`` field set to the Type object.
- Makes a :term:`Result` of this Type with the specified name, if
``name is not None``. If ``name is ``None``, then the Result does
not have a name. The Result will have its ``type`` field set to the
Type object.
- *Default*: there is a generic definition of this in Type.
- *Default*: there is a generic definition of this in Type. The Result's
``type`` will be the object that defines this method (in other words,
``self``).
- **__call__()**:
- **__call__(name=None)**:
- Syntactic shortcut to make_result.
- Syntactic shortcut to ``make_result``.
- *Default*: this is done for you by Type.
- *Default*: ``make_result``
For each method, the *default* is what Type defines for you. This
means you will rarely need to define all of these methods.
For each method, the *default* is what :api:``theano.gof.Type`` defines
for you. So, if you create an instance of :api:``theano.gof.Type`` or an
instance of a subclass of :api:``theano.gof.Type``, you
must define ``filter``. You might want to override ``values_eq_approx``,
as well as ``values_eq``. The other defaults generally need not be
overridden.
For more details you can go see the documentation for :ref:`type`.
......@@ -103,7 +121,7 @@ For more details you can go see the documentation for :ref:`type`.
Defining double
===============
We are going to piggyback Type ``double`` on Python's ``float``. We are
We are going to base Type ``double`` on Python's ``float``. We are
must define ``filter`` and shall override ``values_eq_approx``.
......@@ -111,10 +129,16 @@ must define ``filter`` and shall override ``values_eq_approx``.
.. code-block:: python
def filter(x, strict=False):
if strict and not isinstance(x, float):
raise TypeError('Expected a float!')
return float(x)
# Note that we shadow Python's function ``filter`` with this
# definition.
def filter(x, strict=False):
if strict:
if isinstance(x, float):
return x
else:
raise TypeError('Expected a float!')
else:
return float(x)
If ``strict == True`` we need to return ``x``. If ``strict == True`` and ``x`` is not a
``float`` (for example, ``x`` could easily be an ``int``) then it is
......@@ -140,9 +164,14 @@ actually produce the exact same output as ``6 * a`` (try with a=0.1),
but with ``values_eq_approx`` we with don't necessarily mind.
We added an extra ``tolerance`` argument here. Since this argument is
not part of the API, it must have a default value which we reasonably
not part of the API, it must have a default value which we
chose to be 1e-4.
.. note::
``values_eq`` is never actually used by Theano, but it might be used
internally in the future. Currently, all equality testing is done
using ``values_eq_approx``.
**Putting them together**
......@@ -182,16 +211,35 @@ and define ``filter`` and ``values_eq_approx`` in the subclass:
``double`` is then an instance of Type ``Double``, which in turn is a
sublcass of ``Type``.
There is a small issue with defining ``double`` that way in that all
instances of ``Double`` are technically the same Type. Indeed, they all
filter in the same way. This is relevant because Theano often compares
Types using ``==`` to see if they are the same. For example, if the
inputs of two different :ref:`applications <apply>` have the same
Type and the operation applied on them is the same, they can be
:term:`merged <merge>`. The workarounds are to define
``Double.__eq__`` so that all instances of Double are equal *or* to
override ``Double.__new__`` to always return the same instance *or* to
hide Double and only publish a single instance of it.
There is a small issue with defining ``double`` this way. All
instances of ``Double`` are technically the same Type. However, different
``Double`` Type instances do not compare the same:
>>> double1 = Double()
>>> double2 = Double()
>>> double1 == double2
False
Theano often compares Types using ``==`` to see if they are the same. If
the inputs of two different :ref:`Applies <apply>` have the same Type
and the :ref:`op` applied on them is the same, they can be :term:`merged
<merge>`.
There are several ways to make it that instances of Type ``Double``
compare equal:
#. Define ``Double.__eq__`` so that all instances of type Double
are equal. For example:
.. code-block:: python
def __eq__(self, other):
return type(self) is Double and type(other) is Double
#. Override ``Double.__new__`` to always return the same instance.
#. Hide Double and only publish a single instance of it.
We prefer the final option, because it's the simplest.
Untangling some concepts
......@@ -218,14 +266,14 @@ attempt to clear up the confusion:
b;``, ``a``, ``b`` and ``c`` would all be Result instances.
* A **subclass of Type** represents a set of Type instances that share
structural similarities. In the ``double`` example that we are
doing, there is actually only one Type in that set, therefore the
subclass doesn't represent anything that one of its instances
doesn't. In this case it is a singleton. However, the NDArrayType
class which is a subclass of Type represents a set of types of
tensors parametrized by their data type or number of dimensions. We
could say that subclassing Type builds a hierarchy of Types which is
based upon structural similarity rather than compatibility.
structural similarities. In the ``double`` example that we are doing,
there is actually only one Type in that set, therefore the subclass
doesn't represent anything that one of its instances doesn't. In this
case it is a singleton, a set with one element. However, the NDArrayType
class which is a subclass of Type represents a set of types of tensors
parametrized by their data type or number of dimensions. We could say
that subclassing Type builds a hierarchy of Types which is based upon
structural similarity rather than compatibility.
Final version
......
......@@ -26,6 +26,7 @@ concepts at work here.
.. toctree::
graphstructures
ex1/type
ex1/op
ex1/ctype
......
......@@ -1193,7 +1193,7 @@ class StructuredDotGradCSR(gof.Op):
const npy_int32 * __restrict__ indptr = (npy_int32 *)%(_indptr)s->data;
const npy_int32 * __restrict__ indices = (npy_int32 *)%(_indices)s->data;
// loop over rows
// loop over columns of sparse matrix
for (npy_int32 i = 0; i < N; ++i)
{
// for each non-null value in the sparse row
......
......@@ -153,61 +153,65 @@ class T_conversion(unittest.TestCase):
import scipy.sparse as sp
class test_structureddot(unittest.TestCase):
def setUp(self):
unittest_tools.seed_rng()
def test_structuredot(self):
bsize = 2
spmat = sp.csc_matrix((5,5))
spmat[0,1] = 1
spmat[0,2] = 2
spmat[1,2] = 3
spmat[1,4] = 4
spmat[3,4] = 5
kerns = tensor.dvector('kerns')
images = tensor.dmatrix('images')
##
# Test compressed-sparse column matrices ###
##
# build symbolic theano graph
def buildgraphCSC(kerns,images):
csc = CSC(kerns, spmat.indices[:spmat.size], spmat.indptr, spmat.shape)
return structured_dot(csc, images.T)
out = buildgraphCSC(kerns,images)
f = theano.function([kerns,images], out)
# compute theano outputs
kernvals = spmat.data[:spmat.size]
imvals = 1.0 * numpy.arange(bsize*spmat.shape[1]).reshape(bsize,spmat.shape[1])
outvals = f(kernvals,imvals)
# compare to scipy
c = spmat.dot(imvals.T)
assert _is_dense(c)
assert numpy.all(outvals == c)
tensor.verify_grad(None, buildgraphCSC, [kernvals,imvals])
##
# Test compressed-sparse row matrices ###
##
spmat = spmat.tocsr()
# build theano graph
def buildgraphCSR(kerns,images):
csr = CSR(kerns, spmat.indices[:spmat.size], spmat.indptr, spmat.shape)
return structured_dot(csr, images.T)
out = buildgraphCSR(kerns,images)
f = theano.function([kerns,images], out)
# compute theano output
kernvals = spmat.data[:spmat.size]
imvals = 1.0 * numpy.arange(bsize*spmat.shape[1]).reshape(bsize,spmat.shape[1])
outvals = f(kernvals,imvals)
# compare to scipy
c = spmat.dot(imvals.T)
assert _is_dense(c)
assert numpy.all(outvals == c)
tensor.verify_grad(None, buildgraphCSR, [kernvals,imvals])
# iterate 10 times just to make sure (cannot get this wrong !)
for i in range(10):
spmat = sp.csc_matrix((4,6))
for i in range(5):
x = numpy.floor(numpy.random.rand()*spmat.shape[0])
y = numpy.floor(numpy.random.rand()*spmat.shape[1])
spmat[x,y] = numpy.random.rand()*10
kerns = tensor.dvector('kerns')
images = tensor.dmatrix('images')
##
# Test compressed-sparse column matrices ###
##
# build symbolic theano graph
def buildgraphCSC(kerns,images):
csc = CSC(kerns, spmat.indices[:spmat.size], spmat.indptr, spmat.shape)
return structured_dot(csc, images.T)
out = buildgraphCSC(kerns,images)
f = theano.function([kerns,images], out)
# compute theano outputs
kernvals = spmat.data[:spmat.size]
imvals = 1.0 * numpy.arange(bsize*spmat.shape[1]).reshape(bsize,spmat.shape[1])
outvals = f(kernvals,imvals)
# compare to scipy
c = spmat.dot(imvals.T)
assert _is_dense(c)
assert numpy.all(outvals == c)
tensor.verify_grad(None, buildgraphCSC, [kernvals,imvals])
##
# Test compressed-sparse row matrices ###
##
spmat = spmat.tocsr()
# build theano graph
def buildgraphCSR(kerns,images):
csr = CSR(kerns, spmat.indices[:spmat.size], spmat.indptr, spmat.shape)
return structured_dot(csr, images.T)
out = buildgraphCSR(kerns,images)
f = theano.function([kerns,images], out)
# compute theano output
kernvals = spmat.data[:spmat.size]
imvals = 1.0 * numpy.arange(bsize*spmat.shape[1]).reshape(bsize,spmat.shape[1])
outvals = f(kernvals,imvals)
# compare to scipy
c = spmat.dot(imvals.T)
assert _is_dense(c)
assert numpy.all(outvals == c)
tensor.verify_grad(None, buildgraphCSR, [kernvals,imvals])
if __name__ == '__main__':
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论