提交 7608602e authored 作者: khaotik's avatar khaotik 提交者: khaotik

bug fix/cleanups

- Moved old docstring in OpFromGraph to function op_from_graph - Added an new example for op_from_graph docstring - now we can access `theano.op_from_graph` and `theano.OpFromGraph<Inline|Precompiled>` in `unittesttools.py`: ``` if check_topo: topo_shape = shapes_function.maker.fgraph.toposort() assert not any(isinstance(t.op, cls) for t in topo_shape) topo_out = outputs_function.maker.fgraph.toposort() assert any(isinstance(t.op, cls) for t in topo_out) ``` This seems to check existence of op in `topo_out` regardless `check_topo`. Since OpFromGraphInline will be replaced with its internal graph at compile time, this check will always cause error. Thus I moved them inside the `if` clause.
上级 85a1ae51
...@@ -68,7 +68,8 @@ from theano.compile import ( ...@@ -68,7 +68,8 @@ from theano.compile import (
SymbolicOutput, Out, SymbolicOutput, Out,
Mode, Mode,
predefined_modes, predefined_linkers, predefined_optimizers, predefined_modes, predefined_linkers, predefined_optimizers,
FunctionMaker, function, function_dump, OpFromGraph, FunctionMaker, function, function_dump,
OpFromGraph, OpFromGrpahInline, OpFromGraphPrecompiled, op_from_graph
ProfileStats, ProfileStats,
Param, shared, as_op) Param, shared, as_op)
......
...@@ -6,37 +6,17 @@ import theano ...@@ -6,37 +6,17 @@ import theano
from theano import gof from theano import gof
from theano.compat import izip from theano.compat import izip
from theano.compile.function_module import orig_function from theano.compile.function_module import orig_function
from theano.compile import SharedVariable, rebuild_collect_shared from theano.compile import SharedVariable, rebuild_collect_shared, optdb
from theano.compile import optdb
from theano.gof import ops_with_inner_function from theano.gof import ops_with_inner_function
from theano.gof.graph import io_connection_pattern from theano.gof.graph import io_connection_pattern
class BaseOpFromGraph(gof.Op): class OpFromGraphBase(gof.Op):
""" """
General syntax is similar to theano.function base class for Ops with custom inner graph
Currently does not support 'updates' or 'givens'argument
Parameters
----------
inputs: list of variables
outputs: list of variables
grad_overrides: None or function or list of [None|function]
Used to override default gradient routine.
Overriding function must take two list as inputs: original inputs
and upstream gradients
If is None, will use default gradient routine.
If is function, must return list of Variable.
If is list, each function must return a single Variable. The order
of the list must corresponds to inputs
Notes
-----
You can use regular function for grad_overrides, but the function
must take (list of) Variable as input and output.
""" """
# NOTE: if you make a subclass of this, make sure add it under:
# theano/compile/tests/test_builders.py
def __init__(self, inputs, outputs, grad_overrides=None, **kwargs): def __init__(self, inputs, outputs, grad_overrides=None, **kwargs):
if not isinstance(outputs, list): if not isinstance(outputs, list):
raise TypeError('outputs must be list', outputs) raise TypeError('outputs must be list', outputs)
...@@ -111,7 +91,7 @@ class BaseOpFromGraph(gof.Op): ...@@ -111,7 +91,7 @@ class BaseOpFromGraph(gof.Op):
disconnected_inputs='ignore'), disconnected_inputs='ignore'),
on_unused_input='ignore' on_unused_input='ignore'
) for go, inp in izip(grad_ops_l, self.internal_inputs)] ) for go, inp in izip(grad_ops_l, self.internal_inputs)]
# since BaseOpFromGraph only accepts and outputs list, # since OpFromGraphBase only accepts and outputs list,
# additional filtering is needed # additional filtering is needed
grad_ops = lambda inps:[ grad_ops = lambda inps:[
(go(inps) if ov else go(*inps)) (go(inps) if ov else go(*inps))
...@@ -184,8 +164,10 @@ class BaseOpFromGraph(gof.Op): ...@@ -184,8 +164,10 @@ class BaseOpFromGraph(gof.Op):
def perform(self, node, inputs, outputs): def perform(self, node, inputs, outputs):
raise NotImplementedError() raise NotImplementedError()
class OpFromGraphPrecompiled(BaseOpFromGraph): class OpFromGraphPrecompiled(OpFromGraphBase):
"""WRITEME""" """
The Op's inner graph is compiled into a theano function.
"""
def prepare_node(self, node, storage_map, compute_map, impl): def prepare_node(self, node, storage_map, compute_map, impl):
if not hasattr(self, "fn") and impl == 'py': if not hasattr(self, "fn") and impl == 'py':
self.fn = orig_function(self.internal_inputs, self.fn = orig_function(self.internal_inputs,
...@@ -200,11 +182,12 @@ class OpFromGraphPrecompiled(BaseOpFromGraph): ...@@ -200,11 +182,12 @@ class OpFromGraphPrecompiled(BaseOpFromGraph):
# we wont need this copy anymore # we wont need this copy anymore
output[0] = variable.copy() output[0] = variable.copy()
class OpFromGraphInline(BaseOpFromGraph): class OpFromGraphInline(OpFromGraphBase):
"""WRITEME""" """
The Op's inner graph is expanded into the outer graph at compile time
"""
def perform(self, node, inputs, outputs): def perform(self, node, inputs, outputs):
raise RuntimeError('OpFromGraphInline is not supposed to be executed at runtime') raise RuntimeError(type(self).__name__+' is not supposed to be executed at runtime')
pass
@gof.local_optimizer([OpFromGraphInline]) @gof.local_optimizer([OpFromGraphInline])
def inline_ofg_expansion(node): def inline_ofg_expansion(node):
...@@ -227,23 +210,37 @@ ops_with_inner_function[OpFromGraphPrecompiled] = 'fn' ...@@ -227,23 +210,37 @@ ops_with_inner_function[OpFromGraphPrecompiled] = 'fn'
# for backward compatibility # for backward compatibility
OpFromGraph = OpFromGraphPrecompiled OpFromGraph = OpFromGraphPrecompiled
# APIs for OpFromGraph*
# API for OpFromGraph*
def op_from_graph( def op_from_graph(
inputs, outputs, inline=False, grad_overrides=None, **kwargs): inputs, outputs, inline=False, grad_overrides=None, **kwargs):
cls_opfromgraph = OpFromGraphInline if inline else OpFromGraphPrecompiled
return cls_opfromgraph(
inputs, outputs, grad_overrides=grad_overrides, **kwargs)
#BELOW is the original OpFromGraph
'''
class OpFromGraph(gof.Op):
""" """
This creates an `Op` from inputs and outputs lists of variables. This creates an `Op` from inputs and outputs lists of variables.
The signature is similar to theano.function() and the resulting The signature is similar to theano.function() and the resulting
`Op`'s perform will do the same operation as:: `Op`'s perform will do the same operation as::
orig_function(inputs, outputs, **kwargs) orig_function(inputs, outputs, **kwargs)
Currently does not support 'updates' or 'givens' argument.
Parameters
----------
inputs: list of variables
outputs: list of variables
inline: bool
if True, will cause the Op's original graph being used during
compilation, otherwise will use a pre-compiled function inside.
grad_overrides: None | function | list of (None|function)
Used to override default gradient routine.
Overriding function must take two list as inputs: original inputs
and upstream gradients
If is None, will use default gradient routine.
If is function, must return list of Variable.
If is list, each function must return a single Variable. The order
of the list must corresponds to inputs
Notes
-----
TODO: TODO:
- examples for a multi-layer mlp. where? - examples for a multi-layer mlp. where?
...@@ -251,13 +248,14 @@ class OpFromGraph(gof.Op): ...@@ -251,13 +248,14 @@ class OpFromGraph(gof.Op):
gof.opt.is_same_graph_with_merge(op1.internal_outputs, op2, gof.opt.is_same_graph_with_merge(op1.internal_outputs, op2,
internal_outputs) internal_outputs)
- c_code() to remove the double overhead? - c_code() to remove the double overhead?
- opt to unfold it, work inplace on inputs
- grad() make it support DisconnectedType and the new interface - grad() make it support DisconnectedType and the new interface
- check how it works with updates. - check how it works with updates.
- add test with constant as input or inside the inner graph. - add test with constant as input or inside the inner graph.
- Add support for the GPU? Probably just need an opt to remove transfer - Add support for the GPU? Probably just need an opt to remove transfer
- Add support to pickle this Op. - Add support to pickle this Op.
- Add support/test with random generator - Add support/test with random generator
- Recursion detection to prevent Op "forkbomb", either set depth
limit or manually check them.
Notes Notes
----- -----
...@@ -265,6 +263,8 @@ class OpFromGraph(gof.Op): ...@@ -265,6 +263,8 @@ class OpFromGraph(gof.Op):
invisible to the user. They can be as input to the node or in the invisible to the user. They can be as input to the node or in the
inner graph. inner graph.
- We support unused inputs. This is needed for the grad. - We support unused inputs. This is needed for the grad.
- inline=True will cause better optimization at the cost of longer
compilation, only works with optimizer "fast_run" or "fast_compile"
Examples Examples
-------- --------
...@@ -273,10 +273,10 @@ class OpFromGraph(gof.Op): ...@@ -273,10 +273,10 @@ class OpFromGraph(gof.Op):
.. code-block:: python .. code-block:: python
from theano import function, OpFromGraph, tensor from theano import function, op_from_graph, tensor
x, y, z = tensor.scalars('xyz') x, y, z = tensor.scalars('xyz')
e = x + y * z e = x + y * z
op = OpFromGraph([x, y, z], [e]) op = op_from_graph([x, y, z], [e])
# op behaves like a normal theano op # op behaves like a normal theano op
e2 = op(x, y, z) + op(z, y, x) e2 = op(x, y, z) + op(z, y, x)
fn = function([x, y, z], [e2]) fn = function([x, y, z], [e2])
...@@ -287,134 +287,39 @@ class OpFromGraph(gof.Op): ...@@ -287,134 +287,39 @@ class OpFromGraph(gof.Op):
import numpy as np import numpy as np
import theano import theano
from theano import config, function, OpFromGraph, tensor from theano import config, function, op_from_graph, tensor
x, y, z = tensor.scalars('xyz') x, y, z = tensor.scalars('xyz')
s = theano.shared(np.random.rand(2, 2).astype(config.floatX)) s = theano.shared(np.random.rand(2, 2).astype(config.floatX))
e = x + y * z + s e = x + y * z + s
op = OpFromGraph([x, y, z], [e]) op = op_from_graph([x, y, z], [e])
# op behaves like a normal theano op # op behaves like a normal theano op
e2 = op(x, y, z) + op(z, y, x) e2 = op(x, y, z) + op(z, y, x)
fn = function([x, y, z], [e2]) fn = function([x, y, z], [e2])
""" Example 3 override gradient
def __init__(self, inputs, outputs, **kwargs):
if not isinstance(outputs, list):
raise TypeError('outputs must be list', outputs)
for i in inputs + outputs:
if not isinstance(i, gof.Variable):
raise TypeError(
'inputs and outputs must be Variable instances', i)
if 'updates' in kwargs or 'givens' in kwargs:
raise TypeError('updates and givens are not allowed in kwargs')
# To correctly support shared variables the inner fct should
# not see them. Otherwise there is a problem with the gradient.
self.shared_inputs = [var for var in gof.graph.inputs(outputs)
if isinstance(var, SharedVariable)]
shared_vars = [var.type() for var in self.shared_inputs]
new = rebuild_collect_shared(outputs, inputs=inputs + shared_vars,
replace=dict(izip(self.shared_inputs,
shared_vars)),
copy_inputs_over=False)
(internal_inputs, internal_outputs,
[clone_d, update_d, update_expr, shared_inputs]) = new
assert len(internal_inputs) == len(inputs) + len(self.shared_inputs)
assert len(internal_outputs) == len(outputs)
assert not update_d
assert not update_expr
assert not shared_inputs
self.internal_inputs = internal_inputs
self.internal_outputs = internal_outputs
self.inputs = inputs
self.outputs = outputs
self.kwargs = kwargs
self.input_types = [input.type for input in inputs]
self.output_types = [output.type for output in outputs]
def __eq__(self, other): .. code-block:: python
# TODO: recognize a copy
return self is other
def __hash__(self):
# TODO: use internal variables in hash
return hash(type(self))
def make_node(self, *inputs):
for input, type in zip(inputs, self.input_types):
if not type == input.type:
raise TypeError("Wrong type, expected %s but got %s" %
(type, input.type))
return gof.Apply(self,
list(inputs) + self.shared_inputs,
[type() for type in self.output_types])
def prepare_node(self, node, storage_map, compute_map, impl):
if not hasattr(self, "fn") and impl == 'py':
self.fn = orig_function(self.internal_inputs,
self.internal_outputs,
**self.kwargs)
def perform(self, node, inputs, outputs):
variables = self.fn(*inputs)
assert len(variables) == len(outputs)
for output, variable in zip(outputs, variables):
# TODO: when function's output-borrowing semantics are correct,
# we wont need this copy anymore
output[0] = variable.copy()
def connection_pattern(self, node):
"""
Return connection pattern of subfgraph defined by inputs and outputs.
"""
return io_connection_pattern(self.internal_inputs, self.internal_outputs)
def infer_shape(self, node, shapes):
out_shp = theano.scan_module.scan_utils.infer_shape(self.internal_outputs,
self.internal_inputs,
shapes)
# Clone the output shape so that shape are computed from outer inputs.
# Note:
# Here we can do it more simply like:
# ret = [theano.clone(shp, replace=repl) for shp in out_shp]
# But doing it multiple time could duplicate common subgraph between
# each shape call. Theano optimizer will clean this up later, but this
# will ask extra work to the optimizer.
repl = dict(zip(self.internal_inputs, node.inputs))
cloned = theano.clone(reduce(tuple.__add__, out_shp), replace=repl)
ret = []
used = 0
for i in range(len(out_shp)):
nb = len(out_shp[i])
ret.append(cloned[used: used + nb])
used += nb
return ret
def grad(self, inputs, output_grads): from thenao import funciton, op_from_graph, tensor, grad
if hasattr(self, "grad_ops"): x, y, z = tensor.scalars('xyz')
grad_ops = self.grad_ops e = x + y * z
else: def rescale_dy(inps, grads):
gs = theano.gradient.grad(cost=None, x, y, z = inps
known_grads=dict(izip(self.internal_outputs, g = grads
output_grads)), return z*2
wrt=self.internal_inputs,
disconnected_inputs='ignore') op = op_from_graph(
[x, y, z], [e], grad_overrides=[None, rescale_dy, None])
e2 = op(x, y, z)
dx, dy, dz = grad(e2, [x, y, z])
fn = function([x, y, z], [dx, dy, dz])
fn(2., 3., 4.) # [1., 8., 3.]
grad_ops = [] """
for g in gs: if inline and theano.config.optimizer in ['fast_run', 'fast_compile']:
if g is None: cls_opfromgraph = OpFromGraphInline
grad_ops.append(lambda *args: None) else:
else: cls_opfromgraph = OpFromGraphPrecompiled
# It is normal if some inputs are not needed in order return cls_opfromgraph(
# to compute the gradient, so we ignore them. inputs, outputs, grad_overrides=grad_overrides, **kwargs)
grad_ops.append(OpFromGraph(self.internal_inputs + output_grads,
[g],
on_unused_input='ignore'))
self.grad_ops = grad_ops
return [go(*(inputs + output_grads)) for go in grad_ops]
'''
...@@ -244,9 +244,9 @@ class InferShapeTester(unittest.TestCase): ...@@ -244,9 +244,9 @@ class InferShapeTester(unittest.TestCase):
# Check that the Op is removed from the compiled function. # Check that the Op is removed from the compiled function.
if check_topo: if check_topo:
topo_shape = shapes_function.maker.fgraph.toposort() topo_shape = shapes_function.maker.fgraph.toposort()
topo_out = outputs_function.maker.fgraph.toposort()
assert not any(isinstance(t.op, cls) for t in topo_shape) assert not any(isinstance(t.op, cls) for t in topo_shape)
topo_out = outputs_function.maker.fgraph.toposort() assert any(isinstance(t.op, cls) for t in topo_out)
assert any(isinstance(t.op, cls) for t in topo_out)
# Check that the shape produced agrees with the actual shape. # Check that the shape produced agrees with the actual shape.
numeric_outputs = outputs_function(*numeric_inputs) numeric_outputs = outputs_function(*numeric_inputs)
numeric_shapes = shapes_function(*numeric_inputs) numeric_shapes = shapes_function(*numeric_inputs)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论