The JuliaConnectoR: a functionally oriented interface for integrating Julia in R
11 The JuliaConnectoR: a functionally oriented interface for integrating Julia in R
Stefan Lenz , Maren Hackenberg , Harald Binder Institute of Medical Biometry and Statistics, Faculty of Medicine and Medical Center – University of Freiburg, Freiburg * Corresponding author. E-mail: [email protected]
Abstract
Like many groups considering the new programming language Julia, we faced the challenge of accessing the algorithms that we develop in Julia from R. Therefore, we developed the R package JuliaConnectoR, available from the CRAN repository and GitHub (https://github.com/stefan-m-lenz/JuliaConnectoR), in particular for making advanced deep learning tools available. For maintainability and stability, we decided to base communication between R and Julia on TCP, using an optimized binary format for exchanging data. Our package also specifically contains features that allow for a convenient interactive use in R. This makes it easy to develop R extensions with Julia or to simply call functionality from Julia packages in R. With its functionally oriented design, the JuliaConnectoR enables a clean programming style by avoiding state in Julia that is not visible in the R workspace. We illustrate the further features of our package with code examples, and also discuss advantages over the two alternative packages JuliaCall and XRJulia. Finally, we demonstrate the usage of the package with a more extensive example for employing neural ordinary differential equations, a recent deep learning technique that has received much attention. This example also provides more general guidance for integrating deep learning techniques from Julia into R. Keywords: language bridge, R, Julia, deep learning, neural ordinary differential equations Introduction
R (R Core Team 2020) and Julia (Bezanson et al. 2017) are two high-level programming languages that are used in particular for statistics and numerical analysis. Connecting Julia and R is particularly interesting because the two languages can complement each other. While the history of R dates back to 1976 (Becker and Chambers 1984), the first Julia release was in 2013 (Shaw 2013). As can be expected with its long history, R offers a much larger number of packages for statistical methods than Julia. Yet, Julia also has packages that offer features that are not available in R. For example, training neural differential equations (Chen et al. 2018), which will be shown in an example later, is not directly possible in R at the moment. While there are already two packages, JuliaCall (Li 2019) and XRJulia (Chambers 2016), the requirements of convenient interactive use, e.g., of Julia deep learning packages, led us to develop the new package JuliaConnectoR for connecting Julia and R. Julia was created with a strong emphasis on computational speed as the authors were not satisfied with the performance of existing scientific computing languages (Bezanson et al. 2017). While Julia can get close to the performance of C, it offers the convenience of a high-level scripting language, which allows for fast code development. This makes connecting with Julia an alternative to using C extensions in R. Considering the similarities between R and Julia, this can further aid in making computationally demanding algorithms available in R. Also, there are already many bridges to different languages available (Dahl 2020; Urbanek 2009; Allaire et al. 2017), making R very suitable as a glue language. While R offers a wide range of features for classical statistics, access to promising new statistical methods and algorithms such as deep learning is mostly provided only by wrappers around packages from other languages. Examples for this are the R packages keras (Allaire and Chollet 2019), and rTorch (Reyes 2019), which wrap the Python libraries Keras (Chollet and others 2015) and PyTorch (Paszke et al. 2019), respectively. These two packages employ Python via the language bridge provided by the R package reticulate (Allaire et al. 2017). Julia also offers an innovative approach for deep learning via the native Julia package Flux (Innes 2018). Flux is based on differentiable programming, a technique for interpreting programs as functions which can be differentiated. The gradients of these functions are calculated via automatic differentiation, which can happen at execution time or even at compile time. Thus, it becomes less necessary for deep learning developers to adapt to a particular programming style that is enforced by a specific framework, e.g., computational graphs in TensorFlow (Abadi, Isard, and Murray 2017). Instead, the optimization can be performed automatically on typical code. Julia is particularly suited to support this (Innes et al. 2018). We designed our package having deep learning with Julia in mind. The goal is to be able to interact with Julia code in the most natural way in R. Julia and R both target users in the fields of statistics and machine learning. This is mirrored by the fact that both languages share more traits with each other than with languages such as C or Python, which have not been designed primarily for this user group. R and Julia both focus on functions rather than objects. Similar to R, Julia has features for object-oriented programming (OOP), but they are mainly focused on dispatching functions on types. This generic function OOP is different from an encapsulated object-oriented style (Wickham 2019), e.g., employed in Python or Java. The interface of the JuliaConnectoR reflects such commonalities between Julia and R, leveraging the parallels between the two languages as much as possible. Features
In the following, we describe the most important features of the JuliaConnectoR. In this process, we also highlight parallels and differences between Julia and R. We also compare the JuliaConnectoR package (version 0.6) to the packages JuliaCall (version 0.17.1) and XRJulia (version 0.9.0) with respect to each of the described features. A short overview of the comparison can be seen in Table 1.
Table 1: Comparison of features between the JuliaConnectoR (version 0.6), JuliaCall (version 0.17.1) and XRJulia (version 0.9.0).
Feature JuliaConnectoR JuliaCall XRJulia See Communication TCP/binary C-interface TCP/JSON 2.1 Automatic importing of packages Yes No No 2.2 Specification for type translation Yes No No 2.3 Reversible translation from Julia to R Yes No No 2.4 Callbacks Yes Yes No 2.5 Let-syntax Yes No No 2.6 Show standard (error) output Yes No Yes 2.7 Interruptible Yes No Yes 2.8 Missing values Yes Yes No 2.9 R data frames to Julia Tables Yes Yes No 2.10
Communication protocol
The JuliaConnectoR starts a Julia process in the background and communicates with it via a custom binary protocol that is based on TCP (Postel 1981). The protocol is based on messages, which may contain arbitrarily complex nested objects. The format is inspired by BSON (MongoDB, Inc. 2009), a format that is an alternative binary format to JavaScript Object Notation (JSON) (Bray 2017). Like BSON, the JuliaConnectoR serialization format uses the binary form of the objects directly, exploiting the commonalities of the binary representations between Julia and R. On the R side, the writeBin and readBin functions can directly write and read whole R arrays. In Julia the write and read methods for binary IO (input/output) connections can be used. This avoids transformations which are necessary for using text-based exchange formats like JSON, where numbers have to be converted to strings containing decimal representations. The protocol also allows streaming the messages, which means that messages do not have to be read completely, but a simultaneous writing and reading/parsing is possible. This allows for fast and efficient communication. XRJulia lets R and Julia communicate via JSON messages. Due to the disadvantage of JSON mentioned above, XRJulia also deviates from this strategy for large data by writing vectors in intermediate files (see largeVectors documentation item in the manual of XRJulia). Yet, writing files to the hard drive for communicating is still slower than sending files via TCP. Using files is also an obstacle for taking advantage of the potential of TCP-based communication, which is the ability to potentially run Julia as a server and R as a client on different machines or containers. JuliaCall connects Julia and R using the Julia package RCall (Bates et al. 2020). RCall integrates R and Julia using C interfaces. From a technical perspective, this is a tighter integration. Earlier versions were rather unstable and not always compatible with different Julia versions. This was one of the reasons why we started developing our own interface. Coupling Julia and R via their C interfaces makes communication faster, but the looser coupling via TCP also has benefits: First, it makes developing and maintaining the package much easier. In particular, the quick release cycles of Julia exacerbate problems of maintenance. With a coupling on a higher level of the interfaces, the compatibility has a higher chance of surviving an update. Additionally to supporting different versions, it is furthermore possible to support a wider range of configurations with this. For example, a particular configuration requirement of RCall is that R has to be compiled with the --enable-R-shlib option to build R as a dynamic library. But such a setup is not wanted in all cases, as it reduces the performance of R by 10-20 % (R Core Team 2018).
Automatic importing of whole packages and modules
One main feature of the JuliaConnectoR is that it can import whole packages from Julia conveniently in one command. The function juliaImport scans specified packages for functions and types and creates corresponding R functions, which are returned bundled in an enviroment. Types are also imported as functions, as they can be used as constructors or functors. The information that these functions are constructors for types is attached as attribute in R, such that these type-constructor functions can be passed as type arguments to other Julia functions. Translating Julia code into R code thus becomes straightforward. Consider the following snippet of Julia code, which defines a small neural network using the Flux package: julia> import Flux julia> model = Flux.Chain(Flux.Dense(4, 4, Flux.relu), Flux.Dense(4, 1))
This can be translated into the following R code:
R> Flux <- juliaImport("Flux")
R> model <- Flux$Chain(Flux$Dense(4, 4, Flux$relu), Flux$Dense(4, 1))
In addition to importing installed packages, it is also possible to load plain modules from source code. This is particularly useful when one wants to interactively develop Julia code in parallel to R code. A good workflow for developing Julia code is to put functions into modules. For loading a module in the current session, the corresponding (main) file can be executed via the Julia function include . This can be repeated in a Julia session multiple times, with the module being replaced completely every time. Thereby, the workspace can be kept clean. With the JuliaConnectoR, this workflow is also possible when working in an R session. If one follows this strategy and has, e.g., the Julia functions in a module
MyModule defined in the file mymodule.jl , importing the module can be done via using its relative module path like in Julia:
R> juliaCall("include", "/path/to/mymodule.jl"))
R> MyModule <- juliaImport(".MyModule")
If the module is the last thing that is defined in the file, it is returned as result of evaluating the file, and importing can be done in one line:
R> MyModule <- juliaImport(juliaCall("include", "/path/to/mymodule.jl")) https://github.com/JuliaInterop/RCall.jl/blob/v0.13.4/docs/src/installation.md JuliaCall can import functions of packages via the function julia_pkg_import . This function does not scan packages, but the names of the functions need to be specified. XRJulia has functions juliaImport and juliaUsing , but those behave differently and do not return or assign functions. The mechanism of connecting to Julia functionality also is more complex in XRJulia.
Translation from R to Julia
Julia is more sensitive to types than R. In contrast to R, Julia allows to specify types of arguments in functions. Functions that use this Julia feature then only accept arguments of specific types. On the one hand, being specific about types has advantages: It allows the Julia compiler to create efficient code. It also makes it possible to dispatch on the type. This means that one function can have multiple methods depending on the types of its arguments, which may be optimized for handling different types in the most efficient way. Julia then infers automatically which is the most specific method to pick. On the other hand, many R users may not be used to thinking about types, as most R functions handle types in a very relaxed way. So, it is worthwhile to take a look at how the JuliaConnectoR translates types between Julia and R. The basic R types and their counterparts in Julia are shown in Table 2. Vectors containing only one element are translated to the type shown in the table. R arrays with more than one element and/or having a dimension specified via the dim attribute are translated to Julia
Array s of the corresponding type and dimension. For example, the R integer vector c(1L, 2L) will be of type
Array{Int,1} in Julia. A double matrix such as matrix(c(1, 2, 3, 4), nrow = 2) will be of type
Array{Float64,2} . Table 2: Basic R types and their corresponding Julia types R → Julia integer → Int double → Float64 logical → Bool character → String complex → Complex{Float64 } raw → UInt8
More complex R data structures can also be translated: R list s are translated to Julia objects of type
Array{T, 1} , where T is the most specific Julia type of the translated elements contained in the list. This works with arbitrarily nested lists. Data frames are handled in a special way, see below in section 2.10. Even if this translation may be intuitive to users familiar with types in R and Julia, the clear specification of the translated types is a feature of the JuliaConnectoR that helps with common type issues, since it makes the type of arguments predictable. More details regarding this can be found in the package documentation. JuliaCall and XRJulia currently lack such a clear specification. From experiments it seems that JuliaCall and XRJulia use the same mapping of the types as specified in Table 2, although this is not documented. XRJulia 0.9.0 failed to translate complex values. (E.g., juliaCall("typeof", 1i) returned an error).
Translation from Julia to R
The type system of Julia is richer than that of R. The JuliaConnectoR follows the principle that data structures translated from Julia should be reconstructable with their original type if needed. This eases the integration of Julia code that relies on specific types. For example, the documentation of Flux recommends to use single-precision floating point values (Julia type
Float32 ) for performance reasons. If one were to translate a matrix with elements of type
Float32 to an R double array, add no type information, and then use it again with Flux, the inferred type for the call would be
Float64 : The code would lose its speed advantage unless the type was managed explicitly. For handling this, the JuliaConnectoR adds the type information as an attribute to translated objects. To ensure a minimal distraction on the command line output, the type is only added if it is needed. The translations resulting from following this principle are shown in Table 3.
Table 3: Julia types that are directly translated to R by the JuliaConnectoR
Julia → R Float64 → double Float16 , Float32 , UInt32 → double with type attribute Int64 that fits in 32 bits → integer Int64 not fitting in 32 bits → double with type attribute Int8 , Int16 , UInt16 , Int32 , Char → integer with type attribute UInt8 → raw UInt64 , Int128 , UInt128 , Ptr → raw with type attribute Complex{Float64 } → complex Complex{Int{X}} with
𝑋 ≤ 64 → complex with type attribute Complex{Float{X}} with
𝑋 ≤ 32 → complex with type attribute Julia functions are translated to R functions that call the respective Julia functions. With this, an anonymous function can be defined in Julia and assigned in R: R> f2 <- juliaEval("x -> x .^ 2")
The same can be done with a named function:
R> f2 <- juliaEval('function f2(x) + x .^ 2 + end')
If a named function exists already, it can be imported directly via juliaFun : R> f2 <- juliaFun("f2")
In any case, the resulting R function can be used like any R function:
R> f2(2) [1] 4
Objects that are more complex than
Array s of above types are returned to R in the form of proxy objects. Examples for such objects are Julia struct types or arrays of arrays. This behavior allows to get an easy access to simple objects, which are straightforward to use in R. It also allows to handle more complex objects in a performant and safe way, including objects having references to external resources, such as pointers to memory or file handles. A full translation of complex objects to Julia is possible via the juliaGet function. Julia objects that do not contain circular references or external pointers can be reconstructed from their translations to R. Such objects can thus also be serialized together with the R session. This is implemented based on the translation of complex Julia
Array s and struct s to R list s. In the case of struct s, the names of the list elements correspond to the field names of the struct. Retaining the type information as an attribute in R, the original objects can be assembled again. Consider the following Julia code in a file
MyLibrary.jl , defining a struct
Book . module MyLibrary export Book, cite struct Book author::String title::String year::Int end function cite(book::Book) "$(book.author): $(book.title) ($(book.year))" end end Such a struct can be fully translated to an R list , which can again be translated back to a Julia object when passed to a Julia function:
R> juliaCall("include", "/path/to/MyLibrary.jl") $name [1] "MyLibrary" attr(,"JLTYPE") [1] "Module"
R> juliaImport(".MyLibrary")
R> book <- juliaGet(MyLibrary.Book("Shakespeare", "Romeo and Julia", 1597L))
R> book $author [1] "Shakespeare" $title [1] "Romeo and Julia" $year [1] 1597 attr(,"JLTYPE") [1] "Main.MyLibrary.Book"
R> MyLibrary.cite(book) [1] "Shakespeare: Romeo and Julia (1597)"
For comparison, JuliaCall also creates proxy objects for more complex objects, e.g. also for an array of arrays such as [[1;2], [3;4]] . But there is no possibility for an automatic translation of such a structure to a native R data structure. XRJulia also has a function juliaGet , which can translate more complex structures to R. However, it does not annotate the translation with the original types, so an exact reconstruction is generally not possible.
Callbacks
R functions are translated to Julia functions that call the original R functions. This way, they can be passed to Julia functions as arguments. It is possible to nest callbacks, e.g., invoking a Julia function that calls an R function as a callback that again may call a Julia function, and so on. This feature makes the JuliaConnectoR a truly functional interface. This kind of communication becomes possible by the custom TCP-based protocol, which allows bidirectional communication. A simple example using the Julia function map (which is analogous to the R function
Map ) demonstrates this:
R> juliaCall("map", function(x) {x+1}, c(1,2,3)) [1] 2 3 4
Such callback functions are useful, e.g., for monitoring the training progress when training a neural network. We demonstrate this below with the neural ordinary differential equation example (see section 3). JuliaCall can also use R functions in place of Julia, functions. For example, julia_call("map", function(x) {x+1}, c(1,2,3)) returns the same result. XRJulia does not allow to pass R functions as arguments. It also does not translate Julia functions to R functions. let syntax
The usage of the keyword let in functionally oriented programming languages is inspired by mathematical language. In Julia, let allows to create a new scope, and declare variables in this scope. The value of the expression is the value of the final expression in the block. From this perspective, a let block is equivalent to defining an anonymous function and evaluating it only once. Using a let block in Julia, declaring (intermediate) global variables can be avoided, which allows for a clean programming style.
The function juliaLet of the JuliaConnectoR allows to create such a let block in a simple way. R variables can be passed as arguments. They are then inserted for the corresponding variables in the expression, which is given as a string. With this, one can evaluate complex Julia expressions and insert R variables in place of Julia variables. The following code demonstrates how juliaLet allows to a create a dictionary object, using distinct Julia syntax, in a straightforward way:
R> juliaLet('Dict("x" => x, "y" => y)', x = c(1,2), y = c(2,3))
Dict("x" => [1.0, 2.0],"y" => [2.0, 3.0])
JuliaCall and XRJulia do not have an equivalent function.
Output redirection
The standard output and standard error output from Julia are redirected and displayed in the R console. This is particularly useful for interactive work because warnings or output are needed to detect errors. The implementation of this is challenging because catching the output needs to be handled asynchronously from the function call itself. We implement this with Julia
Task s that collect the output. These tasks are synchronized in Julia, so that R can receive the messages synchronously. JuliaCall does not have this feature. Recent versions of XRJulia are able to display the output.
Interrupting
In interactive programming of machine learning algorithms, it is important to be able to interrupt long-running commands. The JuliaConnectoR catches interrupts that are signaled via
Ctrl + C key or esc in RStudio and terminates the running Julia process. To test this feature, we can run an infinite loop and try to interrupt it.
R> juliaEval('while true; end')
It is currently not possible to interrupt a command in JuliaCall. For example, RStudio as a development environment needed to be shut down forcefully after executing julia_eval('while true; end') . Interrupting commands in XRJulia is possible.
Missing values
Julia has a concept of missing values with a three-valued logic like R. The difference is that missing values are of the distinct type
Missing , which has the single value missing . This means an array that may contain missing values has a different type than an array without missing values. For example, [1.0; 2.0; 3.0] is of type
Array{Float64,1} and [1.0; missing; 3.0] is of type
Array{Union{Missing, Float64},1} . In R, on the other hand, c(1.0, 2.0, 3.0) and c(1.0, NA, 3.0) have the same type. Both behaviors are integrated by the JuliaConnectoR. Missing values in R (value NA ) are translated to missing values in Julia. R arrays with missing values are converted to Julia arrays of type Array{Union{Missing, T}} , where T stands for the translated type in Table 2. R> juliaCall("+", c(1, NA), c(1, 2)) [1] 2 NA
R> juliaCall("sqrt", NA) [1] NA
JuliaCall also supports missing values. The corresponding command julia_call("+", c(1, NA), c(1, 2)) yields the same result as the code above. But when calling julia_call("sqrt", NA) a fatal error occurs (on version 0.17.1), which terminates the R session. XRJulia does not handle NA s properly. The first command from the code snippet above returns a proxy object pointing to an object of type Array{Float64, 1} which cannot be retrieved via juliaGet . The second command prints a warning message and returns
NULL . Data frame support
Data of tabular shape is very common for statistics. Therefore, this is a very important type of data structure for languages like R and Julia, which both focus on making statistical and numerical computing easily accessible. Unsurprisingly, the implementation of such tabular data has some parallels in R and Julia. Base R provides data frames , but there are some packages providing alternative versions of such data structures, namely tibble and data.table. The implementations of tables provided by these packages extend the basic data frame interface, such that these tables can be used like a data.frame from base R. Similar to R, there are different takes on data frames in Julia, provided by different packages, most prominently DataFrames (Julia Data collaborators 2020) and JuliaDB (Julia Computing 2020). There is also a unifying interface for the data structures defined in these packages, which is defined in the Tables Julia package. These parallels are used in the JuliaConnectoR to make it simple to exchange tabular data. By enabling translations between the interfaces data.frame and Tables, it becomes possible to translate other kinds of tabular data structures extending data.frame s, in particular tibble s and data.table s, as well. For implementing the Tables interface, we use a custom type, which wraps the columns in a minimal way. As information about the internal structure of the original Julia objects gets lost with the translation to R, it is generally not possible to fully restore an object implementing the Tables interface from its data.frame translation. For this reason, the resulting tabular data structures are given back to R as proxy objects. Performance is another reason: By using proxy objects, it is possible to interactively create large subsets of Julia Tables without having to worry about heavy traffic between Julia and R because only references need to be transferred instead of the complete data. The translation of data structures from Julia to R can be requested via a call to as.data.frame . JuliaCall translates data frames to
DataFrame objects from the DataFrames package. This is also compatible with the Tables interface. We decided against DataFrames since it is more heavy-weight and also has not reached version 1.0 yet. XRJulia translates data frames to a proxy object encapsulating a
Dict{String, Any} . This has the disadvantage that the column order is lost and it is also not compatible to the Tables interface. An example using neural differential equations
As a further illustration, we use the JuliaConnectoR to reconstruct a deep learning approach proposed by Chen et al. (2018) in their work on neural ordinary differential equations, which won a best paper award at NeurIPS 2018 (https://nips.cc/Conferences/2018/Awards). Specifically, the authors present a new family of deep learning models by combining neural networks with ordinary differential equations. Many types of neural networks such as residual networks (He et al. 2015) or recurrent neural networks (RNNs) (Rumelhart, Hinton, and Williams 1986) are characterized by applying a finite sequence of discrete transformations to a hidden state ℎ 𝑡 : ℎ 𝑡+1 = ℎ 𝑡 + 𝑓(ℎ 𝑡 , 𝜃 𝑡 ), 𝑡 ∈ {0, … , 𝑇} The central idea in the work of Chen et al. (2018) is to generalize this discretized transformation to a continuous dynamic of the hidden units in the form of an ordinary differential equation (ODE): 𝑑ℎ(𝑡)𝑑𝑡 = 𝑓(ℎ(𝑡), 𝜃 𝑡 ), 𝑡 ∈ [0, 𝑇] (1) The function 𝑓(ℎ(𝑡), 𝜃 𝑡 ) that parameterizes the derivative of the hidden state is given by a neural network with the parameters 𝜃 𝑡 . To obtain the value of the hidden state at some time 𝑇 , instead of applying all transformations for 𝑡 = 0,1, … , 𝑇 , the initial value problem defined by equation (1) and a starting point ℎ(0) can be solved at 𝑇 using a black-box differential equation solver. In the paper, the authors propose a memory-efficient way of solving the ODE by using the adjoint sensitivity method, a technique for performing reverse-mode differentiation (aka backpropagation) through the ODE solver with constant memory cost with respect to the network depth. As a result, this allows to build continuous-depth residual networks and continuous-time latent variable models that can be trained efficiently. The model architecture is based on a variational autoencoder (VAE), a generative deep learning model first presented in (Kingma and Welling 2014). Model training is based on the framework of variational inference (Blei, Kucukelbir, and McAuliffe 2017) and aims at recovering the central factors of variation underlying the data in a low-dimensional latent representation defined by a random variable. The model from Chen et al. (2018) is trained to represent input time series data as latent trajectories, where each trajectory is obtained by solving an ODE system in the latent space of the VAE model. In our example here, we want to demonstrate the possibility of using such a model in R. This also more generally shows how the JuliaConnectoR can be used to bring capabilities for deep learning and related novel techniques from Julia to R. Our dataset consists of two-dimensional points 𝑥 𝑡 𝑖 that move with time 𝑡 inwards along the path of a spiral. The spirals are shown in Figure 1. The points are drawn from the trajectories of these spirals: Each of the two underlying spiral trajectories, one clockwise and one counter-clockwise, includes 100 two-dimensional points that can be thought of as a time series of a point on the 2d-plane that travels along a spiral-shaped trajectory. We generate 100 training observations from these trajectories by randomly sampling a starting point somewhere on the trajectory and adding gaussian noise with mean 0 and standard deviation 0.1 to the subsequent 20 points, corresponding to the next 20 time points of the trajectory. Figure 1: Learning of spirals with the latent time series VAE. The points on the spiral are moving inwards over time. Half of the spirals are clockwise and half counter-clockwise. Trajectories can be predicted by solving the differential equations using other time points.
To implement the model for capturing this pattern in Julia, we use the machine learning packages Flux (Innes 2018) and DiffEqFlux (Rackauckas et al. 2019). DiffEqFlux integrates the deep learning models from Flux with differential equations and realizes backpropagation through arbitrary ODE solvers with the adjoint sensitivity method. We use the same architecture as Chen et al. (2018), an RNN with 25 hidden units as encoder, a 4-dimensional latent space, a decoder with one hidden layer and 20 hidden units and another one-layer neural network with 20 hidden units parameterizing the latent dynamics. We use a standard Gaussian decoder and train the model as in standard VAE training by minimizing the evidence lower bound (ELBO) on the data likelihood (Kingma and Welling 2014; Blei, Kucukelbir, and McAuliffe 2017) using stochastic gradient descent with the ADAM optimiser (Kingma and Ba 2015) and a learning rate of 0.01. The complete code for creating the data set is available from https://github.com/stefan-m-lenz/JuliaConnectoR-SpiralExample. In the following, we highlight some parts which also may typically occur in code that performs deep learning. The Julia code for the example can be found in the file -20 -10 -5 0 5 10 15 - - x1 x Clockwise -4 -2 0 2 4 6 8 10 - x1 x Counter-clockwise
SamplePrediction SpiralExample.jl , which defines the Julia module
SpiralExample . The R code that uses the Julia functions of the module is contained in the file
SpiralExample.R . To ensure optimal reproducibility of the experiment, we use a Julia “project”. This simply is a directory containing a
Projects.toml file and a
Manifest.toml file. These files specify the exact versions of all packages used. With the Julia functions
Pkg.activate , we tell Julia to use the project. With
Pkg.instantiate , we can install all project dependencies with one call.
Pkg <- juliaImport("Pkg")
Pkg$activate(".")
Pkg$instantiate()
In the
SpiralExample
Julia module, a type
LatentTimeSeriesVAE is defined, which comprises the neural networks from the package Flux that are involved. After importing the module (see also section 2.2), we can call the constructor of this type to initialize the model, specifying the parameters of the architecture.
R> juliaCall("include", normalizePath("SpiralExample.jl"))
R> SpiralExample <- juliaImport(".SpiralExample")
R> model <- SpiralExample$LatentTimeSeriesVAE(latent_dim = 4L, + obs_dim = 2L, rnn_nhidden = 25L, f_nhidden = 20L, dec_nhidden = 20L)
It is very common in deep learning that many parameters are used. To avoid confusion, it is advised to use named arguments, as shown above. In contrast to R, named arguments and positional arguments are strictly separated in Julia. For named arguments, Julia requires the names and does not infer their values by their position. In the next step, the model can be trained on the data.
R> epochs <- 20
R> plotValVsEpoch <- function(epoch, val) { + if (epoch == 1) { + ymax <- max(val) + plot(x = 1, y = val, + xlim = c(0, epochs), ylim = c(0, ymax*1.1), + xlab = "Epoch", ylab = "Value") + } else { + points(x = epoch, y = val) + } + }
R> spiraldata <- SpiralExample$spiral_samples(nspiral = 100L, + ntotal = 150L, nsample = 30L, start = 0, stop = 6*pi, a = 0, b = 1)
R> SpiralExample$`train!`(model, spiraldata$samp_trajs, spiraldata$samp_ts, + epochs = epochs, learningrate = 0.01, monitoring = plotValVsEpoch)
The training function receives the model , the 𝑥 values in spiraldata$samp_trajs and the time values in spiraldata$samp_ts . Optional named arguments can be specified here as well. The example also demonstrates the use of a callback function, which is here specified as monitoring argument. The plotValVsEpoch defined above is used to plot the value of the loss function during the training. It is called after each training epoch . (An “epoch” is deep learning jargon for the update of the model parameters based on the loss function using all samples once.) A plot of the loss function allows to evaluate the training progress at one glance. Displaying output already during monitoring is especially useful if the training takes longer. After the model has been trained, we can evaluate the model performance. In our case, we take a look at the model predictions for the training observations. By parameterizing the latent space dynamics as a time-continuous ODE solution, we can inter- and extrapolate the time series by solving the ODE at other time points than the ones observed in the training data and decoding them to data space (see Figure 1). The prediction can be done, e.g., with the following code: R> predlength <- length(spiraldata$samp_ts) + 10
R> SpiralExample$predictspiral( + model, sample, spiraldata$orig_ts[1:predlength])
Here, the sample contains the 𝑥 values, and spiraldata$orig_ts contains all possible time values. Summary and outlook
We have introduced the JuliaConnectoR package for connecting R and Julia in a reliable and convenient way. The example with neural differential equations shows how the JuliaConnectoR can help to enable more flexible ways of deep learning in R. It also demonstrates some best practices for employing Julia, such as using Julia modules. The comparison of current features in Table 1 provides an overview over the different language bridges between Julia and R. It can be seen that the JuliaConnectoR is a new solution for connecting Julia and R that offers many features not available in other packages. The usage of the most important functions of the package has been exemplified in the small code snippets for illustrating the features. An overview of the functions that have been presented here can be seen in Table 4.
Table 4: Overview of most important exported functions
Function name Short description Usage see juliaImport
Load a Julia package in Julia via import and return its functions and data types as an environment, such that the functions can be called directly in R 2.2 juliaFun
Create an R function that wraps a Julia function 2.4 juliaCall
Call any Julia function by name. (Not needed for functions created via juliaImport or juliaFun .) 2.2, 2.9 juliaEval Evaluate a simple Julia expression (and return the result) 2.4, 2.8 juliaLet
Evaluate Julia expressions with R variables in place of Julia variables employing a let block (and return the result) 2.6 juliaGet
Fully translate a Julia object to an R object 2.4 There is also some potential that is not yet fully leveraged: A next step for developing the JuliaConnectoR further could be to harness the fact that the communication via TCP could in principle be used to run Julia sessions remotely from the computer running the R session. This might be useful when users want to use a convenient UI for programming on their machine, and at the same time, they would like to utilize resources on remote computing servers. As detailed in section 2.1, the JuliaConnectoR is the only one of the three packages connecting Julia and R whose design allows it to be used in such a way. It is technically not complicated to run a Julia server with the Julia part of the JuliaConnectoR on a remote server in the network, and connect to it from a different computer. Yet, additional security measures need to be implemented for putting this in practice since the execution of arbitrary code can be triggered via such a connection. The goal of the current version is to connect R and Julia in an intuitive way. The best example for this is the automatic importing of Julia packages. Also, the features for interactive use, such as the redirection of the standard (error) output and the possibility to interrupting running commands, make it easier to develop extensions for R in Julia. Additionally, the JuliaConnectoR comes with a design that aims to avoid state in Julia that is not visible in R: Julia functions can be translated to R functions and all variables returned from Julia are translated into R variables. It is not necessary or encouraged by the package to use global variables. In places where Julia and R do not align so easily, this is aided by the introduction of the function juliaLet to handle more complex Julia expressions. By that, the design of the JuliaConnectoR allows for a clean style of programming and minimizes the feeling of “remote - controlling” Julia. Acknowledgements
This work has been supported by the Federal Ministry of Education and Research (BMBF) in Germany in the MIRACUM project (FKZ 01ZZ1801B). References
Abadi, Martín, Michael Isard, and Derek G. Murray. 2017. “A Computational Model for TensorFlow: An Introduction.”
Proceedings of the 1st ACM SIGPLAN International Workshop on Machine Learning and Programming Languages , MAPL 2017,, 1 –
7. https://doi.org/10.1145/3088525.3088527. Allaire, Joseph J., and François Chollet. 2019.
Keras: R Interface to ’Keras’ . https://CRAN.R-project.org/package=keras. Allaire, Joseph J., Kevin Ushey, Yuan Tang, and Dirk Eddelbuettel. 2017.
Reticulate: R Interface to Python . https://github.com/rstudio/reticulate. Bates, Douglas, Randy Lai, Simon Byrne, and con tributors. 2020. “Julia Package RCall (GitHub Repository).” https://github.com/JuliaInterop/RCall.jl. Becker, Richard A., and John M. Chambers. 1984.
S: An Interactive Environment for Data Analysis and Graphics . Belmont, Calif. : Wadsworth Advanced Book Program.
Bezanson, Jeff, Alan Edelman, Stefan Karpinski, and Viral Shah. 2017. “Julia: A Fresh Approach to Numerical Computing.”
SIAM Review
59 (1): 65 –
98. https://doi.org/10.1137/141000671.
Blei, David M., Alp Kucukelbir, and Jon D. McAuliffe. 2017. “Variational Inference: A Review for Statisticians.”
Journal of the American Statistical Association
112 (518): 859 –
77. https://doi.org/10.1080/01621459.2017.1285773. Bray, Tim. 2017.
The JavaScript Object Notation (JSON) Data Interchange Format . https://tools.ietf.org/html/rfc8259.
Chambers, John M. 2016. “The XR Structure for Interfaces.” In
Extending R , 259 – Chen, Tian Qi, Yulia Rubanova, Jesse Bettencourt, and David Duvenaud. 2018. “Neural
Ordinary Differential Equation s.” In
Advances in Neural Information Processing Systems . Chollet, François, and others. 2015. “Keras: The Python Deep Learning Api.” https://keras.io.
Dahl, David. 2020. “Integration of R and Scala Using Rscala.”
Journal of Statistical Software
92 (4): 1 –
18. https://doi.org/10.18637/jss.v092.i04.
He, Kaiming, Xiangyu Zhang, Shaoqing Ren, and Jian Sun. 2015. “Deep Residual Learning for Image Recognition.” http://arxiv.org/abs/1512.03385.
Innes, Michael. 2018. “Flux: Elegant Machine Learning with Julia.”
Journal of Open Source Software
James Bradbury, et al. 2018. “On Machine Learning and Programming Languages.” In
SysML Conference 2018 . https://mlsys.org/Conferences/doc/2018/37.pdf.
Julia Computing, Inc. 2020. “Julia Package JuliaDB (GitHub Repository).” https://github.com/JuliaComputing/JuliaDB.jl. Ju lia Data collaborators. 2020. “Julia Package DataFrames (GitHub Repository).” https://github.com/JuliaData/DataFrames.jl.
Kingma, Diederik P., and Jimmy Ba. 2015. “Adam: A Method for Stochastic Optimization.” In , edited by Yoshua Bengio and Yann LeCun.
Kingma, Diederik P., and Max Welling. 2014. “Auto - Encoding Variational Bayes.” In , edited by Yoshua Bengio and Yann LeCun. Li, Changcheng. 2019. “JuliaCall: An R Package for Seamless Integration Between R and Julia.”
Journal of Open Source Software
MongoDB, Inc. 2009. “BSON (Binary JSON) Serialization.” http://bsonspec.org/. Paszke, Adam, Sam Gross, Francisco Massa, Adam Lerer, James Bradbury, Gregory Chanan,
Trevor Killeen, et al. 2019. “PyTorch: An Imperative Style, High -Performance Deep Learning
Library.” In
Advances in Neural Information Processing Systems 32 , edited by H. Wallach, H. Larochelle, A. Beygelzimer, F. dAlché-Buc, E. Fox, and R. Garnett, 8024 –
35. Curran Associates, Inc. http://papers.neurips.cc/paper/9015-pytorch-an-imperative-style-high-performance-deep-learning-library.pdf. Postel, Jon. 1981.
Transmission Control Protocol . https://tools.ietf.org/html/rfc793. Rackauckas, Chris, Michael Innes, Yingbo Ma, Jesse Bettencourt, Lyndon White, and Vaibhav
Dixit. 2019. “DiffEqFlux.jl - A Julia Library for Neural Differential Equations.” http://arxiv.org/abs/1902.02376. R Core Team. 2018.
R Installation and Administration . https://cran.r-project.org/doc/manuals/r-release/R-admin.pdf. ——— . 2020.
R: A Language and Environment for Statistical Computing rTorch: R Bindings to ’PyTorch’ . https://CRAN.R-project.org/package=rTorch.
Rumelhart, David, Geoffrey Hinton, and Ronald Williams. 1986. “Learning Representations by Back-
Propagating Errors.”
Nature –
36. https://doi.org/10.1038/323533a0. Shaw, Viral B. https://github.com/JuliaLang/julia/releases/tag/v0.1. Urbanek, Simon. 2009. rJava: Low-Level R to Java Interface . https://CRAN.R-project.org/package=rJava. Wickham, Hadley. 2019.