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
= slim.fully_connected; x = activation function, one of s/t/r/l/mCx
= slim.conv2d; x = activation function, one of s/t/r/l/mMp
= slim.max_pool2dAp
= slim.avg_pool2dBn
= slim.batch_norm
Nonlinearities (suffixes for C/F, so Cs = convolutional layer + sigmoid):
s
= sigmoidt
= tanhr
= relul
= 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.reluSig
= tf.nn.sigmoidTanh
= tf.nn.tanhSmax
= tf.nn.softmax
Reshaping:
Flat
= slim.flattenReshape
= tf.reshapeSqueeze
= tf.squeezeExpand
= tf.expand_dims
Multidimensional LSTM:
These are intended as alternatives to 2D convolutions. For sequence models, there will be other modeling primitives.
Lstm2
= Fun(lstm2d.separable_lstm) # 2D-to-2DLstm2to1
= Fun(lstm2d.reduce_to_sequence) # 2D-to-1DLstm2to0
= Fun(lstm2d.reduce_to_final) # 2D-to-vectorClstm2(n, m)
is aCl(n, [3,3])
followed byLstm2(m)
Dws(n)
is a depthwise convolutionCs(n, [1, 1])
Other:
Id
= identityDo
= slim.dropoutLrn
= tf.nn.local_response_normalizationUnit
= slim.unit_normConc
is roughly tf.nn.concat
Binding external functions:
External
- import an external function using module pathImport
- 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.
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))
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))})
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.
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.
Not all functions available in TensorFlow have corresponding short names in specs; in order to access other functions conveniently, you can refer to any function in a number of existing modules using the full function name in that module. Module shortcuts are defined for:
TF
=tf
NN
=tf.nn
SL
=slim
You can, of course, introduce more abbreviations in your own code.
These are defined as:
TF = specs_lib.AutoFunction(tf)
Using these definitions, SL.conv2d(64, 5)
is equivalent to Cr(64, 5)
.
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]
>>>
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 sharingMix(...)
-- weighted convex combination of layer outputsLincom(...)
-- weighted linear combination of layer outputsSameDepth(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.