来自:http://deeplearning.net/software/theano/tutorial/extending_theano.html
Extending Theano
该教程覆盖了如何使用新颖的ops来扩展theano。它主要关注哪些能够提供一个python实现的ops。而Extending Theano with a C Op 是基于c的op实现。该教程的第一部分介绍了theano的graphs,因为提供一个新颖的theano op需要对theano graphs有个基本的理解。然后在概括的介绍了定义一个op的最重要的方法。
正如一个图文说明需要的,该教程会介绍如何编写一个简单的基于python的op,该op是基于double上执行的。同样会介绍如何实现测试来确保之前op的工作的有效性。
note:
该教程不会介绍如何编写得到输入值的视图或者对其修改的op。所以这里介绍的所有ops必须返回新的分配的内存或者重用由函数perform() 的参数output_storage 所提供的内存。 想了解这些过程可以查阅 Views and inplace operations 。
如果你的op返回针对输入的视图或者修改,却不是如介绍的那样实现的,那么theano就还是会运行,并且返回某些graphs的正确的值和其他graphs的错误结果。
强烈建议在DebugMode (Theano flag mode=DebugMode) 下运行你的测试,因为它会验证你的op行为是否正确。
note:查阅 Developer Start Guide 来了解版本框架的信息,即git 和GitHub,和开发工作流程以及如何进行有质量的贡献。
一、Theano Graphs
Theano会将符号数学化计算表示成graph。这些graph都是二分图 (有2种类型node的图),它们由内连接的Apply 和 Variable 节点组成。 Variable 节点在graph中表示数据,例如:输入、输出或者中间值。所以一个graph的输入和输出也就是theano Variable 节点的列表. Apply 节点通过在这些变量上执行计算来生成新的值。每个 Apply 节点会连接到 Op 的一个实例,从而来表示所执行的计算。该教程详细介绍了如何编写这样一个op实例。关于图结构的更详细信息,查阅: Graph Structures 翻译。
二、Op 结构
op就是继承自gof.Op的某个python对象。该部分提供了一些方法的概述,可以用来实现一个新的op。。不过没有介绍你可能遇到或者需要的所有可能的情况。不过更详细的可以查阅: Op’s contract.
import theano class MyOp(theano.Op): # Properties attribute __props__ = () def make_node(self, *inputs): pass # Python implementation: def perform(self, node, inputs_storage, output_storage): pass # Other type of implementation # C implementation: [see theano web site for other functions] def c_code(...): # ... pass # Other implementations (pycuda, ...): def make_thunk(self, node, storage_map, _, _2): pass # optional: check_input = True def __init__(self, ...): pass def grad(self, inputs, g): pass def R_op(self, inputs, eval_points): pass def infer_shape(node, (i0_shapes, ...)): pass
op需要执行在gof.Op接口中定义的一些方法。对于op来说,首先需要定义方法make_node() 然后在实现该方法,例如perform(), Op.c_code() 或 make_thunk()。
make_node() 方法生成一个Apply节点来表示在提供的输入上op的应用。该方法需要对下面三个事情负责:
- 首先检查输入变量类型是否与当前的op兼容。如果在当前输入类型上没法应用,那么必须抛出一个异常 (例如 TypeError).
- 在theano的符号语言中,它会对*inputs上找到的变量进行操作,从而推断出符号输出变量的类型。并创建一个适合的符号类型的输出变量来作为该操作的输出。
- 它创建一个有着输入和输出变量的apply实例,然后返回该apply实例。
perform() 方法用来定义一个op的python实现。它有以下几个参数:
- node 是对一个Apply节点的引用,从前面的 Op‘s make_node() 方法获得。它通常不会用在简单的ops中,不过它包含的符号信息需要复杂的ops。
- inputs 是对数据的引用列表,会被非符号语句所操作(即,python,Numpy中的语句)。
- output_storage 是存储单元的列表,其中存储的是输出数据。每一个单元存储着op的一个输出。放入 output_storage 的数据必须匹配符号输出的类型。禁止改变output_storage中的列表的长度。函数模式可以允许output_storage 中的元素在执行过程中保持不变,或者将output_storage单元的值重设为None。而且允许对op所需要用到的一些内存进行预分配。该特性允许 perform 重用调用期间的内存,例如,假设在output_storage中预先分配了一些内存,其中有着正确的dtype,不过shape却是错的,而且存在某些stride模式。
perform() 方法必须由输入里决定。也就是说,当用在同一个输入的时候,该方法必须返回相同的输出。
gof.Op 允许使用其他的方法来定义op的实现。例如可以通过定义 Op.c_code() 来提供一个对该op的c实现。查阅 Extending Theano with a C Op 来获得关于 Op.c_code() 和其他相关的c方法更详细的介绍。注意到一个op可以提供c和pythong两种实现。
make_thunk() 方法是另一个可以用来代替 perform()的方法。它返回一个thunk.。该thunk被定义成一个0参数函数,该函数很好的封装了op在对应节点的参数上的计算过程。该方法有以下几个参数:
- node 是Apply的实例,其中会请求一个thunk。
- storage_map 是列表的字典,通过将变量映射成一个元素列表(one-element lists)来处理变量的当前的值。这个一元素列表可以作为指向值的指针,可以与其他节点和实例来共享该”指针“。
- compute_map 同样也是列表的字典。将变量映射成一元素列表来处理布尔值。如果该值是0,那么这个变量就表示还未计算,该值是无效的。如果该值是1,那么这个变量已经被计算过了,那么是有效的。如果该值是2,那么这个变量是已经被垃圾回收了,不再有效,而且无法被这次调用所请求。返回的函数必须确保计算值在compute_map中计算过。
make_thunk() 在你自己想要生成代码和编译的时候是很有用的。例如,这可以让你使用PyCUDA来编译GPU代码。
如果 make_thunk() 被一个op所定义,那么它会被theano所使用来获得该op的实现。 perform() 和 Op.c_code()会被忽略掉。
其他通过op定义的可选的方法:
__str__() 方法对你的op提供了一个有意义的字符串表示。
__eq__() 和 __hash__() 分别定义了两个ops是否相等和计算一个op实例的哈希。它们会在优化阶段被用来融合节点,那些有着相同计算的节点(相同的输入,相同的操作)。两个ops可以通过 __eq__() 来计算是否相等,即在有着相同输入的时候是否有着相同的输出。
__props__ 列出的属性影响着计算是如何执行的 (通常来说,它们就是你在__init__()中设置的那样). 它必须是一个元组。如果你没有任何属性,那么你应该将这个属性设置成空元组( tuple ())。
__props__ 能够自动的生成合适的 __eq__() 和 __hash__()。 给定从__props__生成的方法 __eq__(), 两个ops如果它们对于__props__中列出的所有属性都有有着相同的值,那么这两个ops就是相等的。给定从__props__ 生成的方法 __hash__() ,两个ops如果对于 __props__中列出的所有属性都有着相同的值,那么这两个ops有着相同的hash。 __props__ 同样会对你的op生成一个合适的 __str__() 。这需要theano的版本为 September 1st, 2014 or version 0.7.
infer_shape() 方法允许在没有实际计算输出的情况下,推断op输出变量的shape。它可以作为输入 node(一个对Apply节点的引用)和theano符号变量的列表 (i0_shape, i1_shape, ...)(op输入变量的shape)。 infer_shape() 返回一个列表,其中每个元素都是一个元组,用来标识一个输出的shape。这在当你只需要输出的shape而无需计算实际的输出的时候是很有用的,例如在优化的时候。
grad() 方法适用于你想要对一些包含你op的表达式的损失函数进行微分的时候。在这个方法中梯度可以具体的符号化。它有两个参数inputs 和 output_gradients ,这两个都是符号的theano变量的列表,必须被theano符号化语言所操作。该grad方法必须返回一个列表,其中对应每个输入都有一个变量。每个返回的变量表示关于输入的梯度,而该梯度是基于关于每个输出的符号化梯度上计算得到的(个人:这句话估计理解有误)。如果输出不是关于一个输入的微分,那么该方法就会被定义成针对这个输入返回一个NullType类型的变量。同样的,如果你没有对某些输入实现grad计算,你需要返回关于这个输入的一个NullType类型的变量。查阅 grad()来得到更详细的说明。
R_op() 方法当你想要让 theano.tensor.Rop 和你的op一起工作的时候是需要使用的。该函数实现了op表示的函数的R-操作。假设函数为 f ,输入为 x 。使用R-操作,即计算f 的jacobian然后通过v 与其右乘,表达式即: .
可选的布尔 check_input 属性用来指定你是否想要在你的op中使用这个类型来在c_code中检查它们的输入。它可以用来加速计算,减少开销(特别是在标量上)和减少生成的c文件的数量。
三、Op 例子
-
import theano class DoubleOp(theano.Op): __props__ = () def make_node(self, x): # check that the theano version has support for __props__ assert hasattr(self, ‘_props‘) x = theano.tensor.as_tensor_variable(x) return theano.Apply(self, [x], [x.type()]) def perform(self, node, inputs, output_storage): x = inputs[0] z = output_storage[0] z[0] = x * 2 def infer_shape(self, node, i0_shapes): return i0_shapes def grad(self, inputs, output_grads): return [output_grads[0] * 2] def R_op(self, inputs, eval_points): # R_op can receive None as eval_points. # That mean there is no diferientiable path through that input # If this imply that you cannot compute some outputs, # return None for those. if eval_points[0] is None: return eval_points return self.grad(inputs, eval_points)
可以按照如下形式来测试:
-
x = theano.tensor.matrix() f = theano.function([x], DoubleOp()(x)) import numpy inp = numpy.random.rand(5, 4) out = f(inp) assert numpy.allclose(inp * 2, out) print inp print out
四、如何进行测试
Theano有很多函数来简化测试。这些可以用来测试 infer_shape, grad 和 R_op 方法。将下面的代码放入一个文件中,然后用theano-nose
程序来执行该文件。
4.1 基本的测试
基本的测试就是通过使用op,然后检查返回的是否正确。如果你检测到一个错误,那么你就需要抛出一个异常。你可以使用关键字assert来自动的抛出一个AssertionError。
-
from theano.tests import unittest_tools as utt from theano import config class test_Double(utt.InferShapeTester): def setUp(self): super(test_Double, self).setUp() self.op_class = DoubleOp self.op = DoubleOp() def test_basic(self): x = theano.tensor.matrix() f = theano.function([x], self.op(x)) inp = numpy.asarray(numpy.random.rand(5, 4), dtype=config.floatX) out = f(inp) # Compare the result computed to the expected value. utt.assert_allclose(inp * 2, out)
我们调用 utt.assert_allclose(expected_value, value) 来对比NumPy ndarray。这抛出一个有着更多信息的错误消息。同样的,这个默认的容忍可以通过theano flags config.tensor.cmp_sloppy 来改变,其值为0, 1 和 2. 默认的值会有着最严格的比较,而1 和2 就比较不那么严格了。
4.2 测试infer_shape
当一个类继承自 InferShapeTester 类,那么就可以通过 self._compile_and_check 来测试op的 infer_shape方法。在当只需要输出的shape而无需计算实际的输出的时侯,用来测试该op在graph中的优化情况。另外,它通过优化后的graph计算得到的shape与实际计算的输出的shape进行对比来检查当前的shape是否正确。 self._compile_and_check 会编译一个theano函数。它会将输入和输出的theano变量的列表作为参数(正如theano.function那样),然后将真实值传递给编译后的函数。 它同样会将op类作为一个参数,来保证在优化后的shape graph中没有实例出现。
如果有错误,那么该函数抛出一个异常。如果你想要看错误的时候的行为,可以使用一个非正常的 infer_shape.
在测试的时候,如果在不同的维度上shape的值相同 (例如,一个方阵或者有着shape为 (n, n, n),或(m, n, m)的tensor3 ),那么就没法检测。例如,如果 infer_shape 使用矩阵的 width来作为它的height,那么在方阵的情况下就没法检测是否出错了。这就是为什么 self._compile_and_check 在这种情况下会输出一个警告的原因。如果你的操作只在这种矩阵下工作,那么就可以通过 warn=False 参数来禁用该警告。
-
from theano.tests import unittest_tools as utt from theano import config class test_Double(utt.InferShapeTester): # [...] as previous tests. def test_infer_shape(self): x = theano.tensor.matrix() self._compile_and_check([x], # theano.function inputs [self.op(x)], # theano.function outputs # Always use not square matrix! # inputs data [numpy.asarray(numpy.random.rand(5, 4), dtype=config.floatX)], # Op that should be removed from the graph. self.op_class)
4.3 测试梯度
函数 verify_grad 用来验证一个op或者theano graph的梯度。它通过对比分析梯度 (通过符号计算得到的)梯度和数值梯度 (通过有限的微分方法得到的)。
如果出错了,那么该函数抛出一个异常。如果你想要看出错的情况,你可以执行一个错误的梯度 (例如,移除前面的2)。
-
def test_grad(self): theano.tests.unittest_tools.verify_grad(self.op, [numpy.random.rand(5, 7, 2)])
4.4 测试Rop
类 RopLop_checker 定义了函数RopLop_checker.check_mat_rop_lop(),RopLop_checker.check_rop_lop() 和RopLop_checker.check_nondiff_rop(). 这些函数可以用来测试一个具体的op的Rop方法的实现。
例如,为了验证DoubleOp的Rop方法,你可以如下来测试:
-
import numpy import theano.tests from theano.tests.test_rop import RopLop_checker class test_DoubleRop(RopLop_checker): def setUp(self): super(test_DoubleRop, self).setUp() def test_double_rop(self): self.check_rop_lop(DoubleRop()(self.x), self.in_shape)
4.5 测试 GPU Ops
在GPU上执行的Ops应该继承自 theano.sandbox.cuda.GpuOp 而不是 theano.Op。这可以给theano区分它们的机会。目前,我们使用这个在GPU上的和约间代码来测试NVIDIA驱动是否正确工作。
五、运行你的测试
为了执行你的测试程序,你需要选择下面的某种方法 :
5.1 theano-nose
选择要进行测试的方法就是运行 theano-nose. 在常规的theano安装中,后面的参数会读取操作系统的path,然后直接访问其中的文件夹。而且它也可以在Theano/bin文件夹中被访问。对应于不同的目的,可以使用下面不同的命令:
- theano-nose --theano: 运行任何在theano的path上找到的测试。
- theano-nose folder_name: 运行任何在文件夹folder_name内找到的测试。
- theano-nose test_file.py: 运行任何在文件test_file.py内的测试。
下面的命令对于开发者来说特别有用,因为它们可以针对具体的类或者具体的测试被调用
- theano-nose test_file.py:test_DoubleRop: 运行在类 test_DoubleRop中的测试。
- theano-nose test_file.py:test_DoubleRop.test_double_op: 只运行在类test_DoubleRop中的测试test_double_op 。
针对于 theano-nose 的用法和功能的详细介绍,可以使用带有参数为 --help (-h)的命令。(个人:在windows下运行theano-nose会显示找不到该命令,在ubuntu下没问题,即该教程的所有命令其实都是基于linux的,而非windows的。)
5.2 nosetests
命令 nosetests 也是很有用的。虽然它缺少和 theano-nose 提供的信息, nosetests 的用法和theano-nose 相似,可以测试任何在python的路径上的文件夹:
nosetests [suffix similar to the above].
更多有关 nosetests 的资料,可查阅: nosetests.
5.3 In-file
可以增加一块代码在文件的最后,然后运行该文件。在这个例子中,在类test_double_op中的测试 test_DoubleRop 会运行的。
-
if __name__ == ‘__main__‘: t = test_DoubleRop("test_double_rop") t.setUp() t.test_double_rop()
我们建议如果在运行一个文件的时候,最好运行那个文件中所有的测试,这可以通过在测试文件的最后加上如下的代码:
-
if __name__ == ‘__main__‘: unittest.main()
六、练习
运行上面的 DoubleOp e例子的代码。
修改并执行: x * y.
修改并执行: x + y and x - y.
你可以省略Rop函数。 试着实现上面的测试过程。
(注意到当前的theano的逐元素风格的优化只能用在涉及到单一输出的情况下。所以,为了解决这个问题,需要在代码中对这两个操作进行联合的显式优化。)
七、as_op
as_op是一个pythong修饰符,用来将一个python函数转换成一个基础theano op,该op会在执行的时候调用所提供的函数。
不推荐用该方法来建立一个op,不过这却是一个快速实现的方法。
它有一个可选的 infer_shape() 参数,该参数必须有如下的形式:
-
def infer_shape(node, input_shapes): # ... return output_shapes
- input_shapes 和 output_shapes 是元组列表,用来表示对应的输入/输出的shape。
note:不提供 infer_shape方法是为了防止对这个op的相关的shape优化 。例如, your_op(inputs, ...).shape 会需要执行该op从而得到shape。
note:没有grad被定义,也就是不能对包含该op的路径中的函数进行微分。
note:它将python函数转换成可调用的对象,该对象的输入是已经声明过的theano变量。
7.1 as_op 例子
-
import theano import numpy from theano.compile.ops import as_op def infer_shape_numpy_dot(node, input_shapes): ashp, bshp = input_shapes return [ashp[:-1] + bshp[-1:]] @as_op(itypes=[theano.tensor.fmatrix, theano.tensor.fmatrix], otypes=[theano.tensor.fmatrix], infer_shape=infer_shape_numpy_dot) def numpy_dot(a, b): return numpy.dot(a, b)
如下进行测试:
-
x = theano.tensor.fmatrix() y = theano.tensor.fmatrix() f = function([x, y], numpy_dot(x, y)) inp1 = numpy.random.rand(5, 4) inp2 = numpy.random.rand(4, 7) out = f(inp1, inp2)
7.2 练习
运行上的numpy_dot 例子的代码。
修改并执行: numpy.add 和 numpy.subtract.
- 修改并执行: x + y和 x - y.
八、测试中的随机数
测试错误可重复在实际中是非常具有说服力的,为了让测试可以重复,就需要得到相同的随机树。你可以通过对Numpy的随机数生成器传入相同的种子来得到。
更方便的方法就是使用类 InferShapeTester 和 RopLop_checker 。如果你实现了自己的 setUp 函数,别忘了调用其父类的setUp函数。
更多信息请查阅: Using Random Values in Test Cases.
答案(Solution)
-
#!/usr/bin/env python # Theano tutorial # Solution to Exercise in section ‘Extending Theano‘ import unittest import theano # 1. Op returns x * y class ProdOp(theano.Op): def __eq__(self, other): return type(self) == type(other) def __hash__(self): return hash(type(self)) def __str__(self): return self.__class__.__name__ def make_node(self, x, y): x = theano.tensor.as_tensor_variable(x) y = theano.tensor.as_tensor_variable(y) outdim = x.ndim output = (theano.tensor.TensorType (dtype=theano.scalar.upcast(x.dtype, y.dtype), broadcastable=[False] * outdim)()) return theano.Apply(self, inputs=[x, y], outputs=[output]) def perform(self, node, inputs, output_storage): x, y = inputs z = output_storage[0] z[0] = x * y def infer_shape(self, node, i0_shapes): return [i0_shapes[0]] def grad(self, inputs, output_grads): return [output_grads[0] * inputs[1], output_grads[0] * inputs[0]] # 2. Op returns x + y and x - y class SumDiffOp(theano.Op): def __eq__(self, other): return type(self) == type(other) def __hash__(self): return hash(type(self)) def __str__(self): return self.__class__.__name__ def make_node(self, x, y): x = theano.tensor.as_tensor_variable(x) y = theano.tensor.as_tensor_variable(y) outdim = x.ndim output1 = (theano.tensor.TensorType (dtype=theano.scalar.upcast(x.dtype, y.dtype), broadcastable=[False] * outdim)()) output2 = (theano.tensor.TensorType (dtype=theano.scalar.upcast(x.dtype, y.dtype), broadcastable=[False] * outdim)()) return theano.Apply(self, inputs=[x, y], outputs=[output1, output2]) def perform(self, node, inputs, output_storage): x, y = inputs z1, z2 = output_storage z1[0] = x + y z2[0] = x - y def infer_shape(self, node, i0_shapes): return [i0_shapes[0], i0_shapes[0]] def grad(self, inputs, output_grads): og1, og2 = output_grads if og1 is None: og1 = theano.tensor.zeros_like(og2) if og2 is None: og2 = theano.tensor.zeros_like(og1) return [og1 + og2, og1 - og2] # 3. Testing apparatus import numpy from theano.gof import Op, Apply from theano import tensor, function, printing from theano.tests import unittest_tools as utt class TestProdOp(utt.InferShapeTester): rng = numpy.random.RandomState(43) def setUp(self): super(TestProdOp, self).setUp() self.op_class = ProdOp # case 1 def test_perform(self): x = theano.tensor.matrix() y = theano.tensor.matrix() f = theano.function([x, y], self.op_class()(x, y)) x_val = numpy.random.rand(5, 4) y_val = numpy.random.rand(5, 4) out = f(x_val, y_val) assert numpy.allclose(x_val * y_val, out) def test_gradient(self): utt.verify_grad(self.op_class(), [numpy.random.rand(5, 4), numpy.random.rand(5, 4)], n_tests=1, rng=TestProdOp.rng) def test_infer_shape(self): x = tensor.dmatrix() y = tensor.dmatrix() self._compile_and_check([x, y], [self.op_class()(x, y)], [numpy.random.rand(5, 6), numpy.random.rand(5, 6)], self.op_class) class TestSumDiffOp(utt.InferShapeTester): rng = numpy.random.RandomState(43) def setUp(self): super(TestSumDiffOp, self).setUp() self.op_class = SumDiffOp def test_perform(self): x = theano.tensor.matrix() y = theano.tensor.matrix() f = theano.function([x, y], self.op_class()(x, y)) x_val = numpy.random.rand(5, 4) y_val = numpy.random.rand(5, 4) out = f(x_val, y_val) assert numpy.allclose([x_val + y_val, x_val - y_val], out) def test_gradient(self): def output_0(x, y): return self.op_class()(x, y)[0] def output_1(x, y): return self.op_class()(x, y)[1] utt.verify_grad(output_0, [numpy.random.rand(5, 4), numpy.random.rand(5, 4)], n_tests=1, rng=TestSumDiffOp.rng) utt.verify_grad(output_1, [numpy.random.rand(5, 4), numpy.random.rand(5, 4)], n_tests=1, rng=TestSumDiffOp.rng) def test_infer_shape(self): x = tensor.dmatrix() y = tensor.dmatrix() # adapt the choice of the next instruction to the op under test self._compile_and_check([x, y], self.op_class()(x, y), [numpy.random.rand(5, 6), numpy.random.rand(5, 6)], self.op_class) # as_op exercice import theano import numpy from theano.compile.ops import as_op def infer_shape_numpy_dot(node, input_shapes): ashp, bshp = input_shapes return [ashp[:-1] + bshp[-1:]] @as_op(itypes=[theano.tensor.fmatrix, theano.tensor.fmatrix], otypes=[theano.tensor.fmatrix], infer_shape=infer_shape_numpy_dot) def numpy_add(a, b): return numpy.add(a, b) def infer_shape_numpy_add_sub(node, input_shapes): ashp, bshp = input_shapes # Both inputs should have that same shape, so we just return one of them. return [ashp[0]] @as_op(itypes=[theano.tensor.fmatrix, theano.tensor.fmatrix], otypes=[theano.tensor.fmatrix], infer_shape=infer_shape_numpy_add_sub) def numpy_add(a, b): return numpy.add(a, b) @as_op(itypes=[theano.tensor.fmatrix, theano.tensor.fmatrix], otypes=[theano.tensor.fmatrix], infer_shape=infer_shape_numpy_add_sub) def numpy_sub(a, b): return numpy.sub(a, b) if __name__ == "__main__": unittest.main()
九、文档
Documentation Documentation AKA Meta-Documentation中更详细的介绍了如何生成文档。
下面这个例子介绍如何将docstring加到一个类中:
-
import theano class DoubleOp(theano.Op): """ Double each element of a tensor. :param x: input tensor. :return: a tensor of the same shape and dtype as the input with all values doubled. :note: this is a test note :seealso: You can use the elemwise op to replace this example. Just execute `x * 2` with x being a Theano variable. .. versionadded:: 0.6 """
- 下面就是在库文档中的说明形式:
- class theano.misc.doubleop.DoubleOp(use_c_code=‘/usr/bin/g++‘)
-
Double each element of a tensor.Parameters: x – input tensor. Returns: a tensor of the same shape and dtype as the input with all values doubled. Note : this is a test note Seealso : You can use the elemwise op to replace this example. Just execute x * 2 with x being a Theano variable. -
在0.6版本中新加入的特性。
十、最后的笔记
这部分更深的讨论可以查阅高级教程: Extending Theano.
Other ops 介绍了下面这几个特殊情况:
参考资料:
[1]官网:http://deeplearning.net/software/theano/tutorial/extending_theano.html