# Initialization

Make sure that the R6 and cgraph package are loaded. Both packages are avaiable on CRAN.

  library(R6)
library(cgraph)

A computational graph can be initialized by:

# Active Graph

All nodes are added to the computational graph that is currently active. This also applies to operator nodes that are created by operators like cg_mean or overloaded inflix operators like + or -. A cgraph object is set to be the active graph upon creation. Method $active() can be called on an existing cgraph object to change the active graph. x <- cgraph$new()
y <- cgraph$new() # this node is added to y a <- parm(10) x$active()

# this node is added to x
b <- parm(20)

NOTE

Only one computational graph can be active at a time.

# Plot a Graph

It can be useful to visualize the dependencies of nodes in a computational graph. This can be done by calling function plot on a cgraph object.

x <- cgraph$new() a <- const(2) b <- const(4) c <- const(6) d <- (a ^ b + b ^ c) e <- (b ^ a + c ^ b) plot(x) The plot function draws a node for each node in the graph. An edge is drawn from one node to the other if the former node is needed to compute the latter node. NOTE The RGraphviz package needs to be installed to plot a computational graph. This package can be downloaded from the Bioconductor repository. # Evaluating a Single Node A node can be evaluated by function val(). The behavior of var() depends on the type of node that is provided to the function. For a constant node, input node, or parameter node, the function retrieves the value that is assigned to the node. For an operation node, the function evaluates the node based on its parents. If these parents are unavailable, the function will traverse the graph and try to evaluate all ancestors that are needed to evaluate the parents. x <- cgraph$new()

a <- const(1)

b <- const(2)

c <- a + b

d <- c - b

val(d)
## [1] 1

NOTE

For computationally efficiency, nodes evaluated by val() are cached. Their values are temporary stored so that they do not have to be re-evaluated if the function is called again. For example, if we call val(d) again in the code block above, d is not evaluated by adding a to b and than substracting b from the result. Instead, d is evaluated by subtracting b from the cached value of c that is evaluated in the previous call of val(d).

# Dynamic Control Flow

Function val() can be used to dynamically build a computational graph. By combing val() with control statements of R, we can build a graph whose structure depends on the values of the nodes. This allows, for example, to construct a graph that calculates Catalan numbers.

x <- cgraph$new() c <- const(1) i <- 0 while(val(c) < 42) { c <- const((4 * i + 2) / (i + 2)) * c i <- i + 1 print(val(c)) } ## [1] 1 ## [1] 2 ## [1] 5 ## [1] 14 ## [1] 42 # Change the Value of a Node The value of a node can be changed by function set(). x <- cgraph$new()

a <- const(10)

set(a, 20)

val(a)
## [1] 20

However, one should be carefull when applying set() on an operation node. Consider the following example:

x <- cgraph$new() a <- const(2) b <- const(4) c <- a + b set(c, 5) val(c) ## [1] 6 Although, we fixed the value of c to 5, the node evaluates to 6 because it is re-evaluated based on a and b. Moreover, function set() removes the value assigned to all descendants of the provided node in the computational graph. The value of these descendants is no longer valid and needs to be re-evaluated. Hence, one should be carefull when applying set() subsequently on nodes that depend on each other. Consider the following example: x <- cgraph$new()

a <- const(2)

b <- a ^ const(2)

c <- b - const(2)

set(a, 3)
set(b, 4)

val(c)
## [1] 2
set(b, 4)
set(a, 3)

val(c)
## [1] 7

The order in which we change a and b matters. If we change a before b, than c is evaluated based on the fixed value of b. If, however, we change b before a, than c is evaluated based on the value of b, which in turn is evaluated based on a.

# Run a Graph

In some cases, it is desireable to evaluate a target node and also retrieve the value of all its ancestors in the computational graph. This procedure is called a forward-pass.

Function run() can be used to perform a forward-pass.

x <- cgraph$new() a <- input(name = "a") b <- const(4, name = "b") c <- cg_pow(a, b, name = "c") values <- run(c, list(a = 2)) as.list(values) ##$c
## [1] 16
##
## $a ## [1] 2 In order to perform a forward-pass, all ancestors of the target node must be available or they must be able to be computed at run-time. Argument values of run() can be used to substitute values for nodes that do not yet have a value (i.e. inputs). The output of run() is an environment containing the value of the target node and the values of all its ancestors that are (re)-evaluated. NOTE An important difference between run() and val() is that, when traversing trough the ancestors of the target node, val() will not evaluate a node if it is already available (e.g. when it has a fixed value or is cached), whereas run() always (re)-evaluates a node regardsless whether it is already available. # Differentiate a Graph A nice feature of a computational graph is that its nodes can be differentiated by reservse automatic differenation. This is done by traversing the ancestors of a given target node in the reverse direction and applying elementary rules of calculus to differentiate the nodes. The partial derivatives of the target node with respect to all its ancestors can be efficiently evaluated in a single backward-pass through the graph. Function gradients() can be used to perform a backward-pass. x <- cgraph$new()

a <- parm(2, name = "a")

b <- const(2, name = "b")

c <- cg_pow(a, b, name = "c")

values <- run(c)

as.list(grads)
## $a ## [1] 4 ## ##$c
## [1] 1
In order to perform a backward-pass, all ancestors of the target node must have a value. These values can be collected by first performing a forward-pass for the node using function run(). The environent obtained by run() can be supplied to the values argument of gradients().
Currently, the cgraph package can only differentiate scalar target nodes. In case the target node is a vector or array, argument index of gradients() can be used to specific which element of the vector or array is differentiated. The output of gradients() is an environment containing the partial derivative of the target node with respect to each ancestor of the node that is an operator node or parameter node. These derivatives have the same shape as the nodes.