# specs -- simple specifications for TensorFlow networks This library implements a simple domain-specific language for specifying deep neural networks in TensorFlow. From a high level, there are a set of standard operators and ways of combining them: - operator `|` takes the output from one layer and "pipes" it into the next - operator `**` repeats a layer multiple times Naming conventions: - single character names are reserved to users - built-in layers are capitalized, not CamelCase (Relu, Fs, etc.) - built-in layers that are common are usually two letters (Cr, Fs, etc.) - less common operations are longer (Relu, Conc, etc.) - temporary names should end in _ Common layers: Common layers are defined by short, capitalized abbreviations. For layers that take an activation function (fully_connected, conv2d), the acronym is a conjunction of a base layer and the activation. For example, `Fs` represents a fully connected layer followed by a sigmoid, whereas `Ft` represents a fully connected layer followed by a Tanh. - `Fx` = tf.contrib.layers.fully_connected; x = activation function, one of s/t/r/l/m - `Cx` = tf.contrib.layers.conv2d; x = activation function, one of s/t/r/l/m - `Mp` = tf.contrib.layers.max_pool2d - `Ap` = tf.contrib.layers.avg_pool2d - `Bn` = tf.contrib.layers.batch_norm Nonlinearities (suffixes for C/F, so Cs = convolutional layer + sigmoid): - `s` = sigmoid - `t` = tanh - `r` = relu - `l` = linear (i.e., None) - `m` = softmax Positional and keyword arguments are the same as for the underlying slim and TensorFlow functions. Therefore, common usage patterns are: Cr(64, [5, 5]) # conv2d with a 5x5 footprint and 64 outputs Mp([2, 2]) # max pooling using [2, 2] steps Explicit nonlinearities: - `Relu` = tf.nn.relu - `Sig` = tf.nn.sigmoid - `Tanh` = tf.nn.tanh - `Smax` = tf.nn.softmax Reshaping: - `Flat` = slim.flatten - `Reshape` = tf.reshape - `Squeeze` = tf.squeeze - `Expand` = tf.expand_dims Other: - `Id` = identity - `Do` = tf.contrib.layers.dropout - `Lrn` = tf.nn.local_response_normalization - `Unit` = tf.contrib.layers.unit_norm - `Conc` is roughly tf.nn.concat Binding external functions: - `External` - import an external function using module path - `Import` - import an external function using statements A network specification is a sequence of `name = expression` Python statements, with the `net` variable holding the network that is being defined. That is, your specification must have a statement of the form `net = ...` as its last statement. So, a simple MNIST network might look like: net = Cr(64, [5, 5]) | Fs(10) More complicated: net = (Cr(64, [5, 5]) | Mp([2, 2])) ** 3 | Fs(10) With temporary names: cmp_ = Cr(64, [5, 5]) | Mp([2, 2]) net = cmp_ ** 3 | Fs(10) (You can also separate statements with `;` instead of `\n`) General model structure: - Models are sequences of `name = expression` statements in Python syntax. - Other kinds of statements are not allowed (with a few exceptions, like calling `debug()`) - Names should be assigned only once. These constraints are only partially enforced by the library right now, but may be strictly enforced in the future. # More Details The spec language is intended for rapid experimentation with common layer types; it's not a replacement for the standard TensorFlow or slim APIs. If you have some complex layer type or construct that's difficult to represent in `spec`, you can implement it directly in Python and then easily make it available as a `spec` operator. Since partial application with positional arguments can be a little confusing, you can also specify positional arguments with keywords like `_1`: cr5_ = Cr(_1=[5, 5]); net = cr5_(64) ** 3 | Fs(10) You can enable debugging by putting `debug()` at the beginning of your network definition: debug(); net = Cr(64, [5, 5]) | Fs(10) The module is a "combinator library". To make the syntax work nicely with Python, the `__call__` operator is overloaded to perform partial application. To create a network from Python, you just call the following: inputs = tf.placeholder(...) spec = "net = (Cr(64, [5, 5]) | Mp([2, 2])) ** 3 | Fs(10)" outputs = specs.create_net(spec, inputs) You can pass variable bindings into `create_net`: inputs = tf.placeholder(...) spec = "net = (Cr(64, [5, 5]) | Mp([2, 2])) ** depth | Fs(10)" outputs = specs.create_net(spec, inputs, dict(depth=3)) # Using `specs` in Code The specs operators are defined in the module `specs_ops`. To facilitate using the `specs` DSL in your code without namespace pollution, you can use the `specs.ops` context manager, which will temporarily make the `specs` operators available in your code: import tensorflow as tf import numpy.random as npr specs = tf.contrib.specs.python with specs.ops: net = (Cr(64, [2, 2]) | Mp([2, 2])) ** 3 | Flat | Fs(10) inputs = tf.placeholder(tf.float32, [None, 28, 28, 1]) outputs = net.funcall(inputs) sess = tf.InteractiveSession() tf.global_variables_initializer().run() sess.run([outputs], feed_dict={inputs: npr.uniform(size=(17, 28, 28, 1))}) # Sharing and Variables You can share variables among subnets by wrapping them with `Shared`: f = Shared(Fr(100)) g = f | f | f | f This will stack four fully connected ReLU layers, sharing the same weights and biases. You can also create variables explicitly: v = Var("v") You can use this to write expressions like this: net = Cl(100, 3) + Var("b", shape=[128, 128, 100])) Note that, under the covers, both the `Cl` operator and the `Var` operator generate functions that are eventually applied via `funcall` to an input tensor; the function generated by the `Var` operator ignores its argument and calls `tf.get_variable` with the supplied arguments. # Pulling in New Primitives If you need some special function in your spec language, you can make it available using `External` or `Import`. The following two statements are equivalent: Sig = External("some_module", "some_op") Sig = Import("import tensorflow as tf; f = tf.nn.sigmoid") You probably will want to use `Import` because TensorFlow contains a number of imports that look like they are in modules, but they are actually just values placed in the namespace somehow. The `Import` function takes an arbitrary Python statement that eventually needs to assign a value to the variable `f` that is then wrapped up as a function. # Summaries There are a number of functions that give you information about the structure of specs (and other, similarly structured, TensorFlow graphs); the first number is the number of parameters, followed by the op, and the shape. >>> summaries.tf_spec_summary("net = Cr(100, [3,3]) | Flat | Fs(10)", input_shape=(17, 28, 28, 1)) 0 Placeholder [17, 28, 28, 1] 1000 Conv [17, 28, 28, 100] 0 Flatten [17, 78400] 784010 fully_connected [17, 10] >>> # ToDo More documentation, comments. The following features are intended to be added soon (names subject to change): - add sequence processing layers - add named point cuts - Seq(a, b, c).add(name=layer).add(name=layer) for explicit seq. structures - S2d, D2s (space/depth operators) - `Shared(...)` -- variable sharing - `Mix(...)` -- weighted convex combination of layer outputs - `Lincom(...)` -- weighted linear combination of layer outputs - `SameDepth(A)` -- makes output depth same as input - summary ops - slim's `arg_scope` - automatic wrapping of long-name slim layers - depth-to-space, etc. Eventually, there may be a similar spec language for input layers and pipelines.