提交 21789731 authored 作者: nouiz's avatar nouiz

Merge pull request #360 from lamblin/filter_variable

Add "filter_variable" mechanism in Type
...@@ -103,7 +103,7 @@ def rebuild_collect_shared( outputs ...@@ -103,7 +103,7 @@ def rebuild_collect_shared( outputs
# Do not use default_update if a "real" update was # Do not use default_update if a "real" update was
# provided # provided
if v not in update_d: if v not in update_d:
v_update = v.filter_update(v.default_update) v_update = v.type.filter_variable(v.default_update)
if v_update.type != v.type: if v_update.type != v.type:
raise TypeError( raise TypeError(
( 'an update must have the same type as ' ( 'an update must have the same type as '
...@@ -188,8 +188,8 @@ def rebuild_collect_shared( outputs ...@@ -188,8 +188,8 @@ def rebuild_collect_shared( outputs
'expression'), 'expression'),
(store_into, update_d[store_into])) (store_into, update_d[store_into]))
update_val = store_into.filter_update(update_val) # filter_variable ensure smooth conversion of cpu/gpu Types
# typically this might be a cast() update_val = store_into.type.filter_variable(update_val)
if update_val.type != store_into.type: if update_val.type != store_into.type:
err_msg = ( 'an update must have the same type as the ' err_msg = ( 'an update must have the same type as the '
'original shared variable(dest, dest.type, ' 'original shared variable(dest, dest.type, '
......
...@@ -118,29 +118,6 @@ class SharedVariable(Variable): ...@@ -118,29 +118,6 @@ class SharedVariable(Variable):
cp.tag = copy.copy(self.tag) cp.tag = copy.copy(self.tag)
return cp return cp
def filter_update(self, update):
"""
When this shared variable is updated by a pfunc, the update value will be run through this function.
This is a good spot to cast or convert the update expression as necessary.
Default behaviour is to return `update` unmodified if it is a Variable, otherwise to create a SharedVariable for it by calling ``shared(update)``.
:param update: the new value for this shared variable when updated by a pfunc.
:returns: a Variable whose value will be assigned to this SharedVariable by a pfunc.
:note: The return value of this function must match the self.type, or else pfunc()
will raise a TypeError.
"""
if not isinstance(update, Variable):
# The value for the update is not a Variable: we cast it into
# a shared Variable so that it can be used by 'function'. Note that
# it means the update value may change if it is mutable and its
# value is modified after the function is created.
update = shared(update)
return update
def __getitem__(self, *args): def __getitem__(self, *args):
# __getitem__ is not available for generic SharedVariable objects. # __getitem__ is not available for generic SharedVariable objects.
# We raise a TypeError like Python would do if __getitem__ was not # We raise a TypeError like Python would do if __getitem__ was not
......
...@@ -152,28 +152,33 @@ class Apply(utils.object2): ...@@ -152,28 +152,33 @@ class Apply(utils.object2):
:type strict: Bool :type strict: Bool
:param strict: :param strict:
If True, the type fields of all the inputs must be equal to the current ones, and If True, the type fields of all the inputs must be equal
returned outputs are guaranteed to have the same types as self.outputs. If False, to the current ones (or compatible, for instance Tensor /
then there's no guarantee that the clone's outputs will have the same types as CudaNdarray of the same dtype and broadcastable patterns,
self.outputs, and cloning may not even be possible (it depends on the Op). in which case they will be converted into current Type), and
returned outputs are guaranteed to have the same types as
self.outputs. If False, then there's no guarantee that the
clone's outputs will have the same types as self.outputs,
and cloning may not even be possible (it depends on the Op).
:returns: an Apply instance with the same op but different outputs. :returns: an Apply instance with the same op but different outputs.
""" """
remake_node = False remake_node = False
for curr, new in zip(self.inputs, inputs): new_inputs = inputs[:]
for i, (curr, new) in enumerate(zip(self.inputs, new_inputs)):
if not curr.type == new.type: if not curr.type == new.type:
if strict: if strict:
raise TypeError("Cannot change the type of this input.", ((curr, curr.type), # If compatible, casts new into curr.type
(new, new.type))) new_inputs[i] = curr.type.filter_variable(new)
else: else:
remake_node = True remake_node = True
if remake_node: if remake_node:
new_node = self.op.make_node(*inputs) new_node = self.op.make_node(*new_inputs)
new_node.tag = copy(self.tag).__update__(new_node.tag) new_node.tag = copy(self.tag).__update__(new_node.tag)
else: else:
new_node = self.clone() new_node = self.clone()
new_node.inputs = inputs new_node.inputs = new_inputs
return new_node return new_node
#convenience properties #convenience properties
......
...@@ -228,9 +228,35 @@ class PureType(object): ...@@ -228,9 +228,35 @@ class PureType(object):
# filter() This is to allow reusing the old allocated memory. As # 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 # of this writing this is used only when we transfer new data to a
# shared variable on the gpu. # shared variable on the gpu.
#def filter_inplace(value, storage, strict=False, allow_downcast=None) #def filter_inplace(value, storage, strict=False, allow_downcast=None)
def filter_variable(self, other):
"""Convert a symbolic variable into this Type, if compatible.
For the moment, the only Types compatible with one another are
TensorType and CudaNdarrayType, provided they have the same
number of dimensions, same broadcasting pattern, and same dtype.
If Types are not compatible, a TypeError should be raised.
"""
if not isinstance(other, graph.Variable):
# The value is not a Variable: we cast it into
# a Constant of the appropriate Type.
other = self.Constant(type=self, data=other)
if other.type != self:
raise TypeError(
'Cannot convert Type %(othertype)s '
'(of Variable %(other)s) into Type %(self)s. '
'You can try to manually convert %(other)s into a %(self)s.'
% dict(
othertype=other.type,
other=other,
self=self)
)
return other
def is_valid_value(self, a): def is_valid_value(self, a):
"""Required: Return True for any python object `a` that would be a legal value for a Variable of this Type""" """Required: Return True for any python object `a` that would be a legal value for a Variable of this Type"""
try: try:
......
...@@ -26,6 +26,7 @@ import logging ...@@ -26,6 +26,7 @@ import logging
from theano.gof import PureOp, Apply from theano.gof import PureOp, Apply
import theano.tensor import theano.tensor
from theano.tensor import TensorType
import gof import gof
from compile import optdb from compile import optdb
...@@ -312,7 +313,10 @@ def ifelse(condition, then_branch, else_branch, name=None): ...@@ -312,7 +313,10 @@ def ifelse(condition, then_branch, else_branch, name=None):
if type(else_branch) not in (list, tuple): if type(else_branch) not in (list, tuple):
else_branch = [else_branch] else_branch = [else_branch]
# Some of the elements might be converted into another type,
# we will store them in these new_... lists.
new_then_branch = []
new_else_branch = []
for then_branch_elem, else_branch_elem in zip(then_branch, else_branch): for then_branch_elem, else_branch_elem in zip(then_branch, else_branch):
if not isinstance(then_branch_elem, theano.Variable): if not isinstance(then_branch_elem, theano.Variable):
then_branch_elem = theano.tensor.as_tensor_variable(then_branch_elem) then_branch_elem = theano.tensor.as_tensor_variable(then_branch_elem)
...@@ -320,14 +324,32 @@ def ifelse(condition, then_branch, else_branch, name=None): ...@@ -320,14 +324,32 @@ def ifelse(condition, then_branch, else_branch, name=None):
else_branch_elem = theano.tensor.as_tensor_variable(else_branch_elem) else_branch_elem = theano.tensor.as_tensor_variable(else_branch_elem)
if then_branch_elem.type != else_branch_elem.type: if then_branch_elem.type != else_branch_elem.type:
raise ValueError(('The two branches should have identical types, ' # If one of them is a TensorType, and the other one can be
# converted into one, then we try to do that.
# This case happens when one of the elements has a GPU type,
# for instance a shared variable that was silently moved to GPU.
if (isinstance(then_branch_elem.type, TensorType)
and not isinstance(else_branch_elem.type, TensorType)):
else_branch_elem = then_branch_elem.type.filter_variable(
else_branch_elem)
elif (isinstance(else_branch_elem.type, TensorType)
and not isinstance(then_branch_elem.type, TensorType)):
then_branch_elem = else_branch_elem.type.filter_variable(
then_branch_elem)
if then_branch_elem.type != else_branch_elem.type:
# If the types still don't match, there is a problem.
raise ValueError(
('The two branches should have identical types, '
' but they are '+str(then_branch_elem.type)+' and '+ ' but they are '+str(then_branch_elem.type)+' and '+
str(else_branch_elem.type)+' respectively. ' str(else_branch_elem.type)+' respectively. '
'This error could be raised if for example ' 'This error could be raised if for example '
' you provided a one element list on the then ' ' you provided a one element list on the then '
' branch but a tensor on the else branch')) ' branch but a tensor on the else branch'))
new_then_branch.append(then_branch_elem)
new_else_branch.append(else_branch_elem)
if len(then_branch) != len(else_branch): if len(then_branch) != len(else_branch):
raise ValueError(('The number of values on the `then` branch' raise ValueError(('The number of values on the `then` branch'
...@@ -341,7 +363,7 @@ def ifelse(condition, then_branch, else_branch, name=None): ...@@ -341,7 +363,7 @@ def ifelse(condition, then_branch, else_branch, name=None):
gpu=False, gpu=False,
name=name) name=name)
ins = [condition] + list(then_branch) + list(else_branch) ins = [condition] + list(new_then_branch) + list(new_else_branch)
rval = new_ifelse.make_node(*ins).outputs rval = new_ifelse.make_node(*ins).outputs
if rval_type is None: if rval_type is None:
......
import unittest
import numpy import numpy
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
import theano
from theano import tensor
from theano.ifelse import ifelse
from theano import sparse
from theano.tensor import TensorType
from theano.tests import unittest_tools as utt
from theano.sandbox.cuda.var import float32_shared_constructor as f32sc from theano.sandbox.cuda.var import float32_shared_constructor as f32sc
from theano.sandbox.cuda import CudaNdarrayType, cuda_available from theano.sandbox.cuda import CudaNdarrayType, cuda_available
...@@ -34,3 +42,110 @@ def test_float32_shared_constructor(): ...@@ -34,3 +42,110 @@ def test_float32_shared_constructor():
assert eq( assert eq(
f32sc(numpy.zeros((2,3,4,5), dtype='float32')).type, f32sc(numpy.zeros((2,3,4,5), dtype='float32')).type,
CudaNdarrayType((False,)*4)) CudaNdarrayType((False,)*4))
def test_givens():
# Test that you can use a TensorType expression to replace a
# CudaNdarrayType in the givens dictionary.
# This test case uses code mentionned in #757
data = numpy.float32([1,2,3,4])
x = f32sc(data)
y = x**2
f = theano.function([x], y, givens={x:x+1})
class T_updates(unittest.TestCase):
# Test that you can use a TensorType expression to update a
# CudaNdarrayType in the updates dictionary.
def test_1(self):
data = numpy.float32([1,2,3,4])
x = f32sc(data)
y = x**2
f = theano.function([], y, updates={x:x+1})
def test_2(self):
# This test case uses code mentionned in #698
data = numpy.random.rand(10,10).astype('float32')
output_var = f32sc(name="output",
value=numpy.zeros((10,10), 'float32'))
x = tensor.fmatrix('x')
output_updates = {output_var:x**2}
output_givens = {x:data}
output_func = theano.function(inputs=[], outputs=[],
updates=output_updates, givens=output_givens)
output_func()
class T_ifelse(unittest.TestCase):
def setUp(self):
utt.seed_rng()
self.rng = numpy.random.RandomState(seed=utt.fetch_seed())
def test_cuda_tensor(self):
data = self.rng.rand(4).astype('float32')
x = f32sc(data)
y = x + 1
cond = theano.tensor.iscalar('cond')
assert isinstance(x.type, CudaNdarrayType)
assert isinstance(y.type, TensorType)
out1 = ifelse(cond, x, y)
out2 = ifelse(cond, y, x)
assert isinstance(out1.type, TensorType)
assert isinstance(out2.type, TensorType)
f = theano.function([cond], out1)
g = theano.function([cond], out2)
assert numpy.all(f(0) == data+1)
assert numpy.all(f(1) == data)
assert numpy.all(g(0) == data)
assert numpy.all(g(1) == data+1)
def test_dtype_mismatch(self):
data = self.rng.rand(5).astype('float32')
x = f32sc(data)
y = tensor.cast(x, 'float64')
cond = theano.tensor.iscalar('cond')
self.assertRaises(TypeError, ifelse, cond, x, y)
self.assertRaises(TypeError, ifelse, cond, y, x)
def test_ndim_mismatch(self):
data = self.rng.rand(5).astype('float32')
x = f32sc(data)
y = tensor.fcol('y')
cond = theano.tensor.iscalar('cond')
self.assertRaises(TypeError, ifelse, cond, x, y)
self.assertRaises(TypeError, ifelse, cond, y, x)
def test_broadcast_mismatch(self):
data = self.rng.rand(2,3).astype('float32')
x = f32sc(data)
print x.broadcastable
y = tensor.frow('y')
print y.broadcastable
cond = theano.tensor.iscalar('cond')
self.assertRaises(TypeError, ifelse, cond, x, y)
self.assertRaises(TypeError, ifelse, cond, y, x)
def test_sparse_tensor_error(self):
data = self.rng.rand(2,3).astype('float32')
x = f32sc(data)
y = sparse.matrix('csc', dtype='float32', name='y')
z = sparse.matrix('csr', dtype='float32', name='z')
cond = theano.tensor.iscalar('cond')
# Right now (2012-01-19), a ValueError gets raised, but I thing
# a TypeError (like in the other cases) would be fine.
self.assertRaises((TypeError, ValueError), ifelse, cond, x, y)
self.assertRaises((TypeError, ValueError), ifelse, cond, y, x)
self.assertRaises((TypeError, ValueError), ifelse, cond, x, z)
self.assertRaises((TypeError, ValueError), ifelse, cond, z, x)
self.assertRaises((TypeError, ValueError), ifelse, cond, y, z)
self.assertRaises((TypeError, ValueError), ifelse, cond, z, y)
...@@ -97,6 +97,32 @@ class CudaNdarrayType(Type): ...@@ -97,6 +97,32 @@ class CudaNdarrayType(Type):
% (self, self.dtype, data, converted_data, self.dtype), % (self, self.dtype, data, converted_data, self.dtype),
data) data)
def filter_variable(self, other):
"""Convert a Variable into a CudaNdarrayType, if compatible.
This Variable should either already be a CudaNdarrayType, or be
a TensorType. It has to have the right number of dimensions,
broadcastable pattern, and dtype.
"""
if hasattr(other, '_as_CudaNdarrayVariable'):
other = other._as_CudaNdarrayVariable()
if not isinstance(other, Variable):
# The value is not a Variable: we cast it into
# a Constant of the appropriate Type.
other = self.Constant(type=self, data=other)
if other.type == self:
return other
if not isinstance(other.type, tensor.TensorType):
raise TypeError('Incompatible type', (self, other.type))
if (other.type.dtype != self.dtype):
raise TypeError('Incompatible dtype', (self.dtype, other.type.dtype))
if (other.type.broadcastable != self.broadcastable):
raise TypeError('Incompatible broadcastable', (self.broadcastable,
other.type.broadcastable))
return theano.sandbox.cuda.basic_ops.GpuFromHost()(other)
@staticmethod @staticmethod
def bound(a): def bound(a):
......
...@@ -127,19 +127,6 @@ class CudaNdarraySharedVariable(SharedVariable, _operators): ...@@ -127,19 +127,6 @@ class CudaNdarraySharedVariable(SharedVariable, _operators):
value = copy.deepcopy(value) value = copy.deepcopy(value)
self.container.value = value # this will copy a numpy ndarray self.container.value = value # this will copy a numpy ndarray
def filter_update(self, other):
if hasattr(other, '_as_CudaNdarrayVariable'):
return other._as_CudaNdarrayVariable()
if not isinstance(other.type, tensor.TensorType):
raise TypeError('Incompatible type', (self, (self.type, other.type)))
if (other.type.dtype != self.dtype):
raise TypeError('Incompatible dtype', (self, (self.dtype, other.type.dtype)))
if (other.type.broadcastable != self.broadcastable):
raise TypeError('Incompatible broadcastable', (self, (self.broadcastable,
other.type.broadcastable)))
return GpuFromHost()(other)
def __getitem__(self, *args): def __getitem__(self, *args):
# Defined to explicitly use the implementation from `_operators`, since # Defined to explicitly use the implementation from `_operators`, since
# the definition in `SharedVariable` is only meant to raise an error. # the definition in `SharedVariable` is only meant to raise an error.
......
...@@ -633,6 +633,35 @@ class TensorType(Type): ...@@ -633,6 +633,35 @@ class TensorType(Type):
raise ValueError("non-finite elements not allowed") raise ValueError("non-finite elements not allowed")
return data return data
def filter_variable(self, other):
"""Convert a symbolic Variable into a TensorType, if compatible.
For the moment, only a TensorType or CudaNdarrayType will be
converted, provided they have the same number of dimensions,
broadcastable pattern, and dtype.
"""
if hasattr(other, '_as_TensorVariable'):
other = other._as_TensorVariable()
if not isinstance(other, Variable):
# The value is not a Variable: we cast it into
# a Constant of the appropriate Type.
other = self.Constant(type=self, data=other)
if other.type == self:
return other
raise TypeError(
'Cannot convert Type %(othertype)s '
'(of Variable %(other)s) into Type %(self)s. '
'You can try to manually convert %(other)s into a %(self)s.'
% dict(
othertype=other.type,
other=other,
self=self)
)
def value_validity_msg(self, a): def value_validity_msg(self, a):
try: try:
self.filter(a, strict=True) self.filter(a, strict=True)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论