Making the double type¶
Type’s contract¶
In Theano’s framework, a Type
(gof.type.Type
)
is any object which defines the following
methods. To obtain the default methods described below, the Type should
be an instance of Type
or should be an instance of a
subclass of Type
. If you will write all methods yourself,
you need not use an instance of Type
.
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.
-
class
PureType
¶ -
filter
(value, strict=False, allow_downcast=None)¶ This casts a value to match the Type and returns the cast value. If
value
is incompatible with the Type, the method must raise an exception. Ifstrict
is True,filter
must return a reference tovalue
(i.e. casting prohibited). Ifstrict
is False, then casting may happen, but downcasting should only be used in two situations:- if
allow_downcast
is True - if
allow_downcast
isNone
and the default behavior for this type allows downcasting for the givenvalue
(this behavior is type-dependent, you may decide what your own type does by default)
We need to define
filter
with three arguments. The second argument must be calledstrict
(Theano often calls it by keyword) and must have a default value ofFalse
. The third argument must be calledallow_downcast
and must have a default value ofNone
.- if
-
filter_inplace
(value, storage, strict=False, allow_downcast=None)¶ If filter_inplace is defined, it will be called instead of filter() This is to allow reusing the old allocated memory. As of this writing this is used only when we transfer new data to a shared variable on the gpu.
storage
will be the old value. i.e. The old numpy array, CudaNdarray, ...
-
is_valid_value
(value)¶ 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: True iff
filter(value, strict=True)
does not raise an exception.
-
values_eq
(a, b)¶ Returns True iff
a
andb
are equal.Default:
a == b
-
values_eq_approx
(a, b)¶ Returns True iff
a
andb
are approximately equal, for a definition of “approximately” which varies from Type to Type.Default:
values_eq(a, b)
-
make_variable
(name=None)¶ Makes a Variable of this Type with the specified name, if
name
is notNone
. Ifname
isNone
, then the Variable does not have a name. The Variable will have itstype
field set to the Type object.Default: there is a generic definition of this in Type. The Variable’s
type
will be the object that defines this method (in other words,self
).
-
__call__
(name=None)¶ Syntactic shortcut to
make_variable
.Default:
make_variable
-
__eq__
(other)¶ Used to compare Type instances themselves
Default:
object.__eq__
-
__hash__
()¶ Types should not be mutable, so it should be OK to define a hash function. Typically this function should hash all of the terms involved in
__eq__
.Default:
id(self)
-
get_shape_info
(obj)¶ Optional. Only needed to profile the memory of this Type of object.
Return the information needed to compute the memory size of
obj
.The memory size is only the data, so this excludes the container. For an ndarray, this is the data, but not the ndarray object and other data structures such as shape and strides.
get_shape_info()
andget_size()
work in tandem for the memory profiler.get_shape_info()
is called during the execution of the function. So it is better that it is not too slow.get_size()
will be called on the output of this function when printing the memory profile.Parameters: obj – The object that this Type represents during execution Returns: Python object that self.get_size()
understands
-
get_size
(shape_info)¶ Number of bytes taken by the object represented by shape_info.
Optional. Only needed to profile the memory of this Type of object.
Parameters: shape_info – the output of the call to get_shape_info() Returns: the number of bytes taken by the object described by shape_info
.
-
clone
(dtype=None, broadcastable=None)¶ Optional, for TensorType-alikes.
Return a copy of the type with a possibly changed value for dtype and broadcastable (if they aren’t None).
Parameters: - dtype – New dtype for the copy.
- broadcastable – New broadcastable tuple for the copy.
Optional to run, but mandatory for DebugMode. Return True if the Python objects a and b could share memory. Return False otherwise. It is used to debug when Ops did not declare memory aliasing between variables. Can be a static method. It is highly recommended to use and is mandatory for Type in Theano as our buildbot runs in DebugMode.
-
For each method, the default is what Type
defines
for you. So, if you create an instance of Type
or an
instance of a subclass of 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 Type.
Additional definitions¶
For certain mechanisms, you can register functions and other such things to plus your type into theano’s mechanisms. These are optional but will allow people to use you type with familiar interfaces.
transfer()¶
To plug in additional options for the transfer target, define a function which takes a theano variable and a target argument and returns eitehr a new transferred variable (which can be the same as the input if no transfer is nessecary) or returns None if the transfer can’t be done.
Then register that function by calling register_transfer()
with it as argument.
Defining double¶
We are going to base Type double
on Python’s float
. We
must define filter
and shall override values_eq_approx
.
filter
# Note that we shadow Python's function ``filter`` with this
# definition.
def filter(x, strict=False, allow_downcast=None):
if strict:
if isinstance(x, float):
return x
else:
raise TypeError('Expected a float!')
elif allow_downcast:
return float(x)
else: # Covers both the False and None cases.
x_float = float(x)
if x_float == x:
return x_float
else:
raise TypeError('The double type cannot accurately represent '
'value %s (of type %s): you must explicitly '
'allow downcasting if you want to do this.'
% (x, type(x)))
If strict
is True we need to return x
. If strict
is True and x
is not a
float
(for example, x
could easily be an int
) then it is
incompatible with our Type and we must raise an exception.
If strict is False
then we are allowed to cast x
to a float
,
so if x
is an int
it we will return an equivalent float
.
However if this cast triggers a precision loss (x != float(x)
) and
allow_downcast
is not True, then we also raise an exception.
Note that here we decided that the default behavior of our type
(when allow_downcast
is set to None
) would be the same as
when allow_downcast
is False, i.e. no precision loss is allowed.
values_eq_approx
def values_eq_approx(x, y, tolerance=1e-4):
return abs(x - y) / (abs(x) + abs(y)) < tolerance
The second method we define is values_eq_approx
. This method
allows approximate comparison between two values respecting our Type’s
constraints. It might happen that an optimization changes the computation
graph in such a way that it produces slightly different variables, for
example because of numerical instability like rounding errors at the
end of the mantissa. For instance, a + a + a + a + a + a
might not
actually produce the exact same output as 6 * a
(try with a=0.1),
but with values_eq_approx
we do not 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
chose to be 1e-4.
Note
values_eq
is never actually used by Theano, but it might be used
internally in the future. Equality testing in
DebugMode is done using values_eq_approx
.
Putting them together
What we want is an object that respects the aforementioned
contract. Recall that Type defines default implementations for all
required methods of the interface, except filter
. One way to make
the Type is to instantiate a plain Type and set the needed fields:
from theano import gof
double = gof.Type()
double.filter = filter
double.values_eq_approx = values_eq_approx
Another way to make this Type is to make a subclass of gof.Type
and define filter
and values_eq_approx
in the subclass:
from theano import gof
class Double(gof.Type):
def filter(self, x, strict=False, allow_downcast=None):
# See code above.
...
def values_eq_approx(self, x, y, tolerance=1e-4):
# See code above.
...
double = Double()
double
is then an instance of Type Double
, which in turn is a
subclass of Type
.
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 compares Types using ==
to see if they are the same.
This happens in DebugMode. Also, Ops can (and should) ensure that their inputs
have the expected Type by checking something like if x.type == lvector
.
There are several ways to make sure that equality testing works properly:
Define
Double.__eq__
so that instances of type Double are equal. For example:def __eq__(self, other): return type(self) is Double and type(other) is DoubleOverride
Double.__new__
to always return the same instance.Hide the Double class and only advertise a single instance of it.
Here we will prefer the final option, because it is the simplest.
Ops in the Theano code often define the __eq__
method though.
Untangling some concepts¶
Initially, confusion is common on what an instance of Type is versus
a subclass of Type or an instance of Variable. Some of this confusion is
syntactic. A Type is any object which has fields corresponding to the
functions defined above. The Type class provides sensible defaults for
all of them except filter
, so when defining new Types it is natural
to subclass Type. Therefore, we often end up with Type subclasses and
it is can be confusing what these represent semantically. Here is an
attempt to clear up the confusion:
- An instance of Type (or an instance of a subclass) is a set of constraints on real data. It is akin to a primitive type or class in C. It is a static annotation.
- An instance of Variable symbolizes data nodes in a data flow
graph. If you were to parse the C expression
int x;
,int
would be a Type instance andx
would be a Variable instance of that Type instance. If you were to parse the C expressionc = a + b;
,a
,b
andc
would all be Variable instances. - A subclass of Type is a way of implementing
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 does not represent anything that one of its instances does not. In this case it is a singleton, a set with one element. However, theTensorType
class in Theano (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¶
from theano import gof
class Double(gof.Type):
def filter(self, x, strict=False, allow_downcast=None):
if strict:
if isinstance(x, float):
return x
else:
raise TypeError('Expected a float!')
elif allow_downcast:
return float(x)
else: # Covers both the False and None cases.
x_float = float(x)
if x_float == x:
return x_float
else:
raise TypeError('The double type cannot accurately represent '
'value %s (of type %s): you must explicitly '
'allow downcasting if you want to do this.'
% (x, type(x)))
def values_eq_approx(self, x, y, tolerance=1e-4):
return abs(x - y) / (abs(x) + abs(y)) < tolerance
def __str__(self):
return "double"
double = Double()
We add one utility function, __str__
. That way, when we print
double
, it will print out something intelligible.