A Simple Differentiable Programming Language
Automatic differentiation plays a prominent role in scientific computing and in modern machine learning, often in the context of powerful programming systems. The relation of the various embodiments of automatic differentiation to the mathematical notion of derivative is not always entirely clear—discrepancies can arise, sometimes inadvertently. In order to study automatic differentiation in such programming contexts, we define a small but expressive programming language that includes a construct for reverse-mode differentiation. We give operational and denotational semantics for this language. The operational semantics employs popular implementation techniques, while the denotational semantics employs notions of differentiation familiar from real analysis. We establish that these semantics coincide.
Automatic differentiation is a set of techniques for calculating the derivatives of functions described by computer programs (e.g., (Pearlmutter and Siskind, 2008; Hascoët and Pascual, 2013; Baydin et al., 2018; Griewank, 2000)). These techniques are not required to produce symbolic representations for derivatives as in classic symbolic differentiation; on the other hand, neither do they employ finite-difference approximation methods common in numerical differentiation. Instead, they rely on the chain rule from calculus to obtain the desired derivatives from those of the programs’s basic operations. Thus, automatic differentiation is at the intersection of calculus and programming. However, the programs of interest are more than chains of operations: they may include control-flow constructs, data structures, and computational effects (e.g., side-effects or exceptions). Calculus does not provide an immediate justification for the treatment of such programming-language features.
In the present work we help bridge the gap between rules for automatic differentiation in expressive programming languages and their mathematical justification in terms of denotational semantics. Specifically, we consider automatic differentiation from a programming-language perspective by defining and studying a small but powerful functional first-order language. The language has conditionals and recursively defined functions (from which loops can be constructed), but only rudimentary data structures. Additionally, it contains a construct for reverse-mode differentiation, explained in detail below. Our language is thus inspired by modern systems for machine learning, which include standard programming constructs and support reverse-mode differentiation. Reverse-mode differentiation permits the computation of gradients, forward-mode derivatives, and more. Indeed as our differentiation construct is a language primitive, differentiations can be nested within differentiations, allowing the computation of higher-order derivatives.
In the setting of a language such as ours, we can consider some common approaches to implementing differentiation:
One approach relies on code transformation, whether on source code or intermediate representations. For example, for the derivative of a conditional expression , it would output , where and are the derivatives of and respectively. This approach is employed, for instance, in Theano (Bergstra et al., 2010), TensorFlow 1.0 (Abadi et al., 2016a; Yu et al., 2018), and Tangent (van Merrienboer et al., 2018).
Another approach relies on tracing, typically eliminating control structures to produce a simpler form of code, which we call an execution trace, that can more easily be differentiated. For example, to produce the derivative of , tracing would evaluate the conditional and produce a trace of the branch taken. Execution traces correspond to graphs of basic operations, and can be taken to be sequences of elementary assignments or else functional programs in A-normal form. Their derivatives can be calculated by applying the chain rule to those basic operations, perhaps via a code transformation (but now of a much simpler kind). Tracing may also record some intermediate values in an evaluation trace, to reduce, or eliminate, the need for recomputation.
This approach thereby conveniently avoids the problem of defining code transformations for conditionals and many other language constructs. It can also be implemented efficiently, sometimes in part with JIT compilation. For these reasons, trace-based differentiation is of growing importance. It is employed, for instance, in Autograd (Maclaurin et al., 2015), TensorFlow Eager Mode (Agrawal et al., 2019), Chainer (Tokui et al., 2015), PyTorch (Paszke et al., 2019), and JAX (Frostig et al., 2018)
We therefore focus on trace-based differentiation, and give our language an operational semantics using the trace-based approach. To do so, we define a sublanguage of execution trace terms (called simply trace terms below). These have no conditionals, function definitions or calls, or reverse-mode differentiations. They do have local definitions, corresponding to fanout in the graphs, but may not be in A-normal form. Tracing is modeled by a new kind of evaluation, called symbolic evaluation. This uses an environment for the free variables of a term to remove conditionals and function calls. Function derivatives at a given value are evaluated in three stages: first, the function is traced at that value; next, the resulting trace term is symbolically differentiated (largely just using the chain rule), resulting in another such trace term; and, finally, that term is evaluated.
We do not account for some of the optimizations used in practice. Doing so would have been a more complicated enterprise, possibly with more arbitrary choices tied to implementation details, and we wished to get a more straightforward formalization working first.
From a mathematical perspective, both approaches to implementing differentiation pose correctness problems. In particular, functions defined using conditionals need not be continuous, let alone differentiable. Consider, for example, the following definition
of the popular ReLU function (Goodfellow et al., 2016). This function is not differentiable at . Further, changing the function body to yields a non-continuous function. What is more, both approaches can produce wrong answers even for differentiable functions! Consider, for example, the following definition of the identity function on the reals:
The derivative of this function at is . However, differentiation “by branches” (whether by code transformation or tracing) would produce the wrong answer, .
In order to capture the mathematical perspective, in addition to its operational semantics we give our language a denotational semantics. This semantics is based on classical notions of differentiation from real analysis (see, for example, (Trench, 2003)). That theory concerns multivariate functions on the reals defined on open domains, i.e., partial such functions with open domains of definition. In our semantics, we make use of those that are smooth (that is, those that can be partially differentiated any number of times). A particularly pleasing aspect of this mathematical development is how well domain theory (needed to account for recursion) interacts with differentiation.
Partiality is necessary, as for any language with general recursion, but it also gives us useful flexibility in connection with differentiation. For example, let be the approximation to which is equal to it except on the diagonal (i.e., where both arguments are equal) where it is undefined. Then
defines an approximation to ReLU which is undefined at . The approximation to is (unlike ) continuous (i.e., the pre-images of and are open sets), and the approximation to ReLU is differentiable wherever it is defined. Therefore, we design the semantics of our language so that it forbids functions such as but allows related approximations such as . An interesting question is how satisfactory an idealization this is of programming practice (which in any case works with approximate reals). We return to this point in the final section.
Proceeding in this way, we obtain adequacy theorems (i.e., operational soundness and completeness theorems) connecting the operational semantics of our language with a denotational semantics based on the classical theory of differentiation of partially defined multivariate real functions. Our theorems apply not only to conditional expressions but to the full language.
In sum, the main contributions of this paper are: (1) a first-order language with conditionals, recursive function definitions, and a reverse-mode differentiation construct; (2) an operational semantics that models one form of trace-based differentiation; (3) a denotational semantics based on standard mathematical notions from real analysis and domain theory; and (4) theorems that show that the two semantics coincide, i.e., the derivatives computed by the operational semantics are indeed the correct derivatives in a mathematical sense. Beyond the specifics of these results, this paper aims to give some evidence of the relevance of ideas and techniques from the programming-languages literature for programming systems that include automatic differentiation, such as current systems for machine learning.
While traditionally associated with scientific computing, automatic differentiation is now a central component of many modern machine learning systems, and those for deep learning in particular (Goodfellow et al., 2016). These systems often employ automatic differentiation to compute the gradients of “loss functions” with respect to parameters, such as neural network weights. Loss functions measure the error resulting from particular values for the parameters. For example, when a machine-learning model is trained with a dataset consisting of pairs , aiming to learn a function that maps the ’s to the ’s, the loss function may be the distance between the ’s and the values the model predicts when presented with the ’s. By applying gradient descent to adjust the parameters, this error can be reduced, until convergence or (more commonly) until the error is tolerable enough. This simple approach has proven remarkably effective: it is at the core of many recent successes of machine learning in a variety of domains.
Whereas gradients are for functions of type , for , treating the more general functions of type , for , works better with function composition, and with the composite structures such as tensors of reals used in deep learning. The literature contains two basic “modes” for differentiating such functions. Forward-mode extends the computation of the function, step by step, with the computation of derivatives; it can be seen as evaluating the function on dual numbers of the form where is nilpotent. In contrast, reverse-mode propagates derivatives backwards from each output, typically after the computation of the function. Reverse-mode differentiation is often preferred because of its superior efficiency for functions of type with . In particular, systems for machine learning, which often deal with loss functions for which , generally rely on reverse-mode differentiation. We refer the reader to the useful recent survey (Baydin et al., 2018) for additional background on these two modes of differentiation; it also discusses the use of higher-order differentiation.
Applications to machine learning are our main motivation. Accordingly, our language is loosely inspired by systems for machine learning, and the implementation strategies that we consider are ones of current interest there. We also de-emphasize some concerns (e.g., numerical stability) that, at present, seem to play a larger role in scientific computing than in machine learning. As noted in (Baydin et al., 2016), the machine learning community has developed a mindset and a body of techniques distinct from those traditional in automatic differentiation.
The literature on scientific computing has addressed the correctness problem for conditionals (Beck and Fischer, 1994; Fischer, 2001), although not in the context of a formally defined programming language. In (Mayero, 2002) a formal proof of correctness for an algorithm for the automatic differentiation of straight-line sequences of Fortran assignments was given using the Coq theorem prover (Bertot and Castéran, 2013). Closer to machine learning, (Selsam et al., 2017) consider a stochastic graphical formalism where the nodes are random variables, and use the Lean theorem prover (de Moura et al., 2015) to establish the correctness of stochastic backpropagation. However, overall, the literature does not seem to contain semantics and theorems for a language of the kind we consider here.
Our work is also related to important papers by Ehrhard, Regnier, et al. (Ehrhard and Regnier, 2003), and by Di Gianantonio and Edalat (Di Gianantonio and Edalat, 2013). Ehrhard and Regnier introduce the differential -calculus; this is a simply-typed higher-order -calculus with a forward-mode differentiation construct which can be applied to functions of any type. It can be modeled using the category of convenient vector spaces and smooth functions between them (see (Blute et al., 2010; Kriegl and Michor, 1997)). Ehrhard and Regnier do not give an operational semantics but they do give rules for symbolic differentiation and it should not be too difficult to use them to give an operational semantics. However their language with its convenient vector space semantics only supports total functions. It therefore cannot be extended to include recursive function definitions or conditionals (even with total predicates, as continuous functions from to the booleans are constant). Di Gianantonio and Edalat prove adequacy theorems for their language, as do we, but their work differs from ours in several respects. In particular, their language has first-order forward-mode but no reverse-mode differentiation: our language effectively supports both, and at all orders. On the other hand, their language allows recursively-defined higher-order functions and accommodates functions, such as the ReLU function, which are differentiable in only a weaker sense. As far as we know, no other work on differentiable programming languages (e.g., (Pearlmutter and Siskind, 2008; Elliott, 2018; Wang et al., 2018; Shaikhha et al., 2018; Manzyuk, 2012)) gives operational and denotational semantics and proves adequacy theorems. Further afield, there is a good deal of work in the categorical literature on categories equipped with differential structure, for example (Blute et al., 2009; Bucciarelli et al., 2010).
Section 2 defines our language. Section 3 gives it an operational semantics with rules for symbolically evaluating general terms to trace terms, and for symbolically differentiating these terms. Sections 4 and 5 cover the needed mathematical material and the denotational semantics. Sections 6 establishes the correspondence between operational and denotational semantics. Section 7 concludes with discussion and some suggestions for future work.
2. A simple language
The types of our language are given by the grammar:
We will make use of iterated products , defined to be when , when , and, recursively, , when ; we write for the -fold iterated product of . Note that this type system includes the types of tensors (multidimensional arrays) of a given shape: the type of tensors of shape is the iterated product . The terms and boolean terms of the language are built from operation symbols and predicate symbols . An example operation symbol could be for dot product of vectors of dimension (for ); an example predicate symbol could be .
The terms are given by the following grammar, where and range over disjoint countably infinite alphabets of ordinary and function variables, respectively. We assume available a standard ordering of the function variables.
These constructs are all fairly standard, except for , which is for reverse-mode differentiation, and which we explain below. We treat addition separately from the operations to underline the fact that the commutative monoid it forms, together with zero, is basic for differentiation. For example, the rules for symbolic differentiation given below make essential use of this structure, but do not need any more of the available vector space structure.
Note the type subscripts on pairing and projection terms. Below, we rely on these subscripts for symbolic differentiation. In practice they could, if needed, be added when type-checking.
The sets and of free ordinary variables and free function variables of a term are understood as usual (and similarly for boolean terms). As is also usual, we do not distinguish -equivalent terms (or boolean terms).
The useful abbreviation
provides an elimination construct for iterated products. When this is
where ; when it is the above let construct; otherwise, it is defined recursively by:
(where is chosen not free in ).
We have zero and addition only at type . At other types we proceed inductively:
Skating over the difference between terms and their denotations, is the reverse-mode derivative at , evaluated at , of the function such that . Reverse-mode differentiation includes gradients as a special case. When and , the gradient of at is given by:
For definitions of gradients, Jacobians, and derivatives see Section 4.1 below, particularly equations (1), (2), (3), and (4). More generally, for an introduction to real analysis including vector-valued functions of several variables and their differentials and Jacobians, see, for example, (Trench, 2003).
So our language effectively also has forward-mode differentiation.
Function definitions can be recursive. Indeed a function can even be defined in terms of
its own derivative: in a recursive function
definition , the language allows
occurrences of within the term in a sub-term of . This generality may be useful—examples
have arisen in the context of Autograd (Maclaurin
et al., 2015)
Turning to typing, operation and predicate symbols have given arities, written and ; we write for the set of operation symbols of arity . For example, we would have and . Figure 1 gives typing rules for sequents
where (type) environments have the form
( all different) and where function (type) environments have the form
( all different). We adopt the usual overwriting notations and for type environments.
The typing rule for function definitions forbids any global variable occurrences (i.e., free variables in function definitions). This restriction involves no loss in expressiveness: as in lambda lifting, one can just add any global variables to a function’s parameters, and then apply the function to the global variables wherever it is called. The restriction enabled us to prove part (2) of Theorem 6.2 (below), but we conjecture it is not needed.
Our various abbreviations have natural admissible typing rules:
We may write (or ) instead of if has no free ordinary (or function) variables (and similarly for boolean terms). Typing is unique: for any , , and there is at most one type such that holds.
As an example, we use our language to program a miniature version of an algorithm for training a machine learning model by gradient descent, loosely based on (Goodfellow et al., 2016, Algorithm 8.1). In such training, one often starts with an untrained model, which is a function from inputs (for example, images) and parameter values to output “predictions” (for example, image labels). Relying on a dataset of input/output pairs, one then picks values of the parameters by gradient descent, as indicated in the Introduction. In our miniature version, we treat inputs, parameter values, and outputs as reals, and we assume that the training data consists of only one fixed input/output pair . We also assume that we have real constants (for the initial value for gradient descent), (for the learning rate, which is fixed) and (for the desired maximum loss on the dataset), and the infix predicate symbol . We can then define the trained model from the untrained model and a loss function as follows:
The example above is typical of what can be expressed in our language, and many variants of machine learning techniques that rely on gradient descent (e.g., as in (Goodfellow et al., 2016), and commonly used in systems like TensorFlow) are in scope as well. For instance, there is no difficulty in expressing optimization with momentum, or differentially private stochastic gradient descent (e.g., (Song et al., 2013; Abadi et al., 2016b)). Probabilistic choice may be treated via random number generators, as is done in practice. Architectures that rely on convolutions or RNN cells can be expressed, even conveniently, with a suitable choice of primitives.
3. Operational semantics
We give a big-step operational semantics, specified with Felleisen and Friedman’s method using evaluation contexts and redexes (Felleisen and Friedman, 1987). Other styles of operational semantics accommodating differentiation are surely also possible.
Terms and boolean terms are (ordinarily) evaluated to closed values and (necessarily) closed boolean values. The most original aspect of our operational semantics concerns the evaluation of differential terms; this is based on the trace-based approach outlined in the Introduction, and uses a second mode of evaluation: symbolic evaluation.
The core idea is that to evaluate a differential term
one first evaluates and , and then performs differentiation before evaluating further. There are two differentiation stages. First, using the closed value of for the differentiation variable , is symbolically evaluated to a trace term , thereby removing all control constructs from , but possibly keeping the variable free in , as the derivative may well depend on it. For example, when is , the value allows the guard of the conditional to be evaluated, but the occurrence of in the branch is not replaced by . Second, is symbolically differentiated at with respect to .
However, this idea is not enough by itself as the differential term may occur inside yet another differential term. One therefore also needs to be able to symbolically evaluate the differential term. That is done much as in the case of ordinary evaluation, but now symbolically evaluating redexes in and until one is left with the problem of symbolically evaluating a term of the form
where and are values that may contain free variables. One then proceeds as above, symbolically evaluating (now using the closed value of ) and then performing symbolic differentiation. As there is some duplication between these two symbolic and ordinary evaluation processes, our rule for ordinarily evaluating a differential term is designed, when executed, to first symbolically evaluate the term, and then ordinarily evaluate the resulting trace term.
The need to keep track of differentiation variables and their values for symbolic evaluation leads us to use value environments for ordinary variables. It is convenient to also use them for ordinary evaluation and to use function environments for function variables for both modes of evaluation.
Values are terms given by the grammar:
Note that, as indicated above, values may have free variables for the purposes of differentiation. Boolean values are boolean terms given by:
Closed values have unique types ; the set of closed values of type is ; and the set of boolean values is . We assume available operation and predicate symbol evaluation functions
We also assume that for every operator there is an operator . The idea is that is the reverse-mode derivative of at evaluated at . We write for . For example, for we would have:
We next define (value) environments , function environments , and (recursive function) closures , the last two mutually recursively:
Value environments are finite functions
from ordinary variables to closed values.
Every finite function
from function variables to closures is a function environment.
If and then is a closure, written .
For any and with , is the closed value obtained by substituting for all free occurrences of in .
Trace terms , are defined as follows:
They are the terms with no conditionals, function definitions or applications, or differentiations.
We will define two ordinary evaluation relations, and one symbolic one:
For all and we define evaluation relations between terms and closed values and between boolean terms and closed boolean values via rules establishing sequents of the forms:
For all and we define a symbolic evaluation relation between terms and trace terms via rules establishing sequents of the form:
Evaluation contexts (boolean evaluation contexts), ranged over by (resp. ), are terms with a unique hole :
We write for the term obtained by replacing the hole in by the term and similarly; a context is trivial if it is ; and and are extended to contexts. We have and and similarly for boolean contexts.
Redexes, ranged over by , and boolean redexes, ranged over by , are given by:
Note that boolean expressions are useful here in that they enable separate conditional and predicate redexes, and so evaluating predicates and making choices are distinct in the operational semantics.
The next lemma is the basis of a division into cases that supports operational semantics using evaluation contexts in the style of Felleisen and Friedman.
Lemma 3.1 (Evaluation context analysis).
Every term , other than a value, has exactly one of the following two forms:
for a unique evaluation context and redex, or
for a unique, and non-trivial, evaluation context and boolean redex.
Every boolean term , other than a boolean value, has exactly one of the following two forms:
for a unique, and non-trivial, boolean evaluation context and redex, or
for a unique boolean evaluation context and boolean redex.
The next lemma is useful to track types when proving theorems about the operational semantics.
Lemma 3.2 (Evaluation context polymorphism).
Suppose that . Then, for some type we have and, whenever , we have .
Analogous results hold for typings of any of the forms or or .
By the uniqueness of types, the types whose existence is claimed in the above lemma are unique.
The rules for ordinary evaluation are given in Figures 2 and 3; those for symbolic evaluation are given in Figures 4 and 5. The definitions are mutually recursive. They make use of the symbolic differentiation of trace terms: given a trace term , and values and (not necessarily closed), we define a trace term
intended to denote the reverse-mode derivative of the function , at , evaluated at . A definition is given in Figure 6; in the definition we assume that , and, as is common, that all binding variables are different.
Proposition 3.3 ().
The following typing rule is admissible:
In large part because of the restrictions on trace terms, their symbolic differentiation is just a systematic, formal application of the chain rule. In our setting, this application requires a fair amount of attention to detail, for instance the use of the type decorations when giving derivatives of pairing and projection terms.
The reader may wish to try the following two evaluation examples with nested differentiation: