Awesome
ScalaStan
A Scala DSL for Stan.
Requirements
ScalaStan depends on Scala, SBT, and CmdStan.
The CMDSTAN_HOME
environment variable should be set to the location of the CmdStan installation.
Alternatively, ScalaStan will work if PATH
contains stanc
within a valid CmdStan installation.
ScalaStan Dependency
To use ScalaStan, add the following to your build:
libraryDependencies += "io.gihub.cibotech" %% "scalastan" % "<version>"
Project Structure
com.cibo.scalastan
contains the ScalaStan DSL (most importantly, theStanModel
trait).com.cibo.scalastan.analysis
contains analyses for ScalaStan models.com.cibo.scalastan.ast
contains the ScalaStan abstract syntax tree.com.cibo.scalastan.data
contains parsers for various data sources (R, for example).com.cibo.scalastan.models
contains reusable ScalaStan models.com.cibo.scalastan.run
contains the classes to handle compiling and running a model.com.cibo.scalastan.transform
contains ScalaStan transformations (optimizations, etc.).- Examples can be found in the
com.cibo.scalastan.examples
package in the integration test (it
) source directory. Run an example using the commandsbt it:run
and choosing from the available examples.
Usage
The ScalaStan DSL is accessed by extending the com.cibo.scalastan.StanModel
trait.
Here's a simple example of linear regression:
import com.cibo.scalastan.StanModel
object MyApp extends App {
object MyModel extends StanModel {
val n = data(int(lower = 0))
val x = data(vector(n))
val y = data(vector(n))
val b = parameter(real())
val m = parameter(real())
val sigma = parameter(real(lower = 0.0))
sigma ~ stan.cauchy(0, 1)
y ~ stan.normal(m * x + b, sigma)
}
val xs: Seq[Double] = ???
val ys: Seq[Double] = ???
val results = MyModel
.withData(MyModel.x, xs)
.withData(MyModel.y, ys)
.run(chains = 5)
results.summary(System.out)
}
For comparison, the equivalent Stan program (without the data setup/output) is:
data {
int<lower=0> n;
vector[n] x;
vector[n] y;
}
parameters {
real b;
real m;
real<lower = 0> sigma;
}
model {
sigma ~ cauchy(0, 1);
y ~ normal(m * x + b, sigma);
}
Data Declarations
Data declarations define the inputs to the model.
These go in the data
section in Stan:
data {
real<lower=0> x;
}
Using ScalaStan, this is encoded as:
val x = data(real(lower = 0))
The lower
and upper
bounds on values are optional in ScalaStan, just as in regular Stan. ScalaStan supports
the following data types:
int([lower], [upper])
// An integerreal([lower], [upper])
// A real/doublevector(length, [lower], [upper])
// A (column) vector of realsrowVector(length, [lower], [upper])
// A row vector of realsmatrix(rows, cols, [lower], [upper])
// A matrix of realscategorical()
// A categorical type (this is a ScalaStan extension to handle categorical data that turns into anint
).
Arrays can be created for any data type with multiple dimensions by calling apply
on the type.
For example, to create a 2-dimensional array of int
:
int()(j, k)
Parameter Declarations
Parameter declarations define the parameters to be inferred. In Stan, these go in the parameters
section:
parameters {
real y;
}
Using ScalaStan, this is encoded as:
val y = parameter(real())
The types for parameters are the same as those for data, however, Stan does not allow integral parameters.
Local Declarations
Local values can be declared using the local
function, which behaves like the data
and parameter
functions,
but is only available within the StanModel
DSL, for example:
val z = local(real())
The Model
The StanModel
trait supports a DSL to describe the model, which can be expressed directly in the body
of the subclass.
Operators
Most arithmetic operators behave as one would expect (and identically to Stan), for example +
, -
, *
, /
,
and %
.
The logical operators <
, <=
, >
, >=
operate as expected, however, note that test for equality
is ===
(three =
s instead of ==
) and test for inequality is =/=
(instead of !=
).
Stan supports element-wise multiplication and division, which are *:*
and /:/
in ScalaStan.
The power operator is ^
.
The transpose operator is .t
(for example, x.t
to transpose x
).
Assignment
The assignment operator is :=
(instead of =
).
For Loops and Slices
For loops use the standard Scala syntax, but using the range
function as a generator. For example:
for (i <- range(1, n)) {
// ...
}
This will cause i
to take on the values 1
through n
inclusive.
Note that arrays are indexed starting from one in Stan.
The range
function can also be used for array slicing, for example:
x(range(1, 5)) := y(range(2, 6))
While Loops
While loops are declared using the loop
function to avoid conflicting with the Scala while
statement.
For example:
loop(x < 10) {
// ...
}
Breaking and Continuing Loops
Stan supports breaking out of loops using the break
keyword and starting the next iteration using
the continue
keyword. Both of these are available in ScalaStan (since Scala does not support
break or continue, these functions are named the same as in Stan).
Conditionals
Conditions take the form when
and otherwise
to avoid conflicting with the Scala if
and else
statements.
For example:
when(x > 1) {
// ...
} otherwise {
// ...
}
The otherwise
section is optional. Additional when
statements can be added to provide an "else if"
structure, but note the required .
:
when(x > 1) {
// ...
}.when(x < 0) {
// ...
} otherwise {
// ...
}
Ternary Operator
Conditionals in ScalaStan/Stan are not expressions, so there is a ternary operator to
support conditionals as part of an expression. The ternary operator is supported as an overload
of when
:
y := when(x > 0, 1, 2)
This will evaluate the condition x > 0
and return 1
if true and 2
if false.
Distributions
The built-in Stan distributions are available in ScalaStan from the stan
object.
To indicate that a value is sampled from a distribution, the ~
operator is used, for example:
y ~ stan.normal(0.0, 1.0)
This is roughly equivalent to the following:
target += stan.normal(0.0, 1.0).lpdf(y)
Other Functions
The built-in Stan functions are available in ScalaStan from the stan
object.
Transformed Data and Parameters
Transformed versions of data and parameters can be declared via the transformed data
and transformed parameters
sections in Stan. In ScalaStan, this is accomplished by extending the TransformedData
or TransformedParameter
classes from within a StanModel
. These classes take a parameter that is the type of the transformed value.
The body of the class provides a DSL to encode the transformation using the same constructs as the model.
Inside this DSL, the value is accessed using result
.
Here is a simple data transform to add 1
to all elements of an array:
val xPlusOne = new TransformedParameter(vector(n)) {
result := x + 1
}
The transformed parameter can now be referenced instead of the actual parameter in the model. Note that a transformed parameter can be referenced in the output, however, if the transformed parameter is not used within the model, ScalaStan will not emit it. In this case, one should use a generated quantity instead.
User-Defined Functions
User-defined functions are created by extending the Function
class from within a StanModel
.
This class takes an optional parameter to determine the return type (it is assumed to return void
if not specified). The body of the class provides a DSL for implementing the function. Inputs to
the function are specified using the input
function, which works much like the local
function,
but creates an input parameter instead of a local variable.
The output
function sets the return value and returns from the function.
Here is an example to add 1
to all elements of an array:
val myFunc = new Function(vector(n)) {
val x = input(n)
output(x + 1)
}
Generated Quantities
Generated quantities provide a means of deriving quantities from parameters, data, and random number generation.
In ScalaStan, such quantities are created by extending the GeneratedQuantity
class within a StanModel
. This class
works like the data and parameter transform blocks, but is contained in the model. In addition to parameters and
data, a generated quantity can use random numbers drawn from a distribution.
Here is an example to draw a random number:
val rand = new GeneratedQuantity(real()) {
result := stan.normal(0.0, 1.0).rng
}
Assigning Inputs
The values for data declarations are specified using the withData
method on the model (technically, this method
is on the CompiledModel
class, but there is an implicit conversion to compile the model). So, for example,
given a data declaration for an integer called x
and a model called model
, one obtains an updated model
with the data filled in for x
via:
model.withData(x, data)
Note that data
in the above must be the appropriate Scala type for the model. For example, an int()
in
ScalaStan must be assigned an Int
and a real()
must be assigned a Double
. Vectors and higher
dimensional objects are represented Seq
in Scala, which each dimension adding another Seq
.
Note that Scala is indexed from zero whereas Stan (and ScalaStan) is indexed from one.
It is also possible to assign data from a DataSource
. The following data sources are available:
CsvDataSource
: A data source for parsing CSV.RDataSource
: A data source for parsing R-formatted data.TextDataSource
: A data source for parsing a text file with an array of values.
For example, given a file (data.R
) with R-formatted
data, we can assign x
from the value named X
in the R data file using the apply
method:
val source = com.cibo.scalastan.data.RDataSource.fromFile("data.R")
model.withData(source(x, "X"))
Using data sources, it is also possible to load the data in Scala format for manipulation using
the read
method:
val source = com.cibo.scalastan.data.CsvDataSource.fromFile("data.csv")
val data: Seq[Double] = source.read(x, "X")
Before the model can be run, all inputs must be assigned. To assign different inputs, the reset
method
must be called. Using the reset
method will clear all assigned inputs and initial values, allowing the
model to be run multiple times with different inputs. The reset
method, like the withData
method, returns an updated copy of the model.
Initial Values
Initial values can be set for parameter declarations using the withInitialValue
method on the model (like
withData
, this is actually on the CompiledModel
class). Here is an example to set the initial value
for b
:
model.withInitialValue(b, value)
It is also possible to set the initial value to a range [-x, x] for all parameters using:
model.withInitialValue(x)
Running the Model
Once all inputs are assigned, the model can be run using the run
method. This method takes the following
optional parameters:
- chains: An integer specifying the number of chains to run in parallel. This defaults to 4.
- seed: An integer specifying the first random number seed to use or
-1
to use system time to select one. With multiple chains, the chain index is added to the seed to ensure each chain gets a different, but deterministic seed. - cache: A boolean specifying whether results should be cached. This defaults to
true
. When the results are cached, re-running the model with the same data set and seed as before will cause the results from the previous run to be returned rather than re-executing the model. - method: The method to use (
RunMethod.Sample()
,RunMethod.Optimize()
,RunMethod.Variational()
, orRunMethod.Diagnose()
). This defaults toRunMethod.Sample()
. Note that eachRunMethod
is a case class that can accept additional parameters.
The return value of the run
method is a results object that can be used to extract values from the run.
The first run of a model will take a long time since the Stan code will need to be built. However,
the generated Stan executable is cached and reused, so additional runs will be fast. The SHA1 hash of the
generated code from ScalaStan is used to determine if the code has changed and needs to be rebuilt. The
cached models and results are stored in $HOME/.scalastan
. Old models and results are removed in a
least-recently used fashion.
Getting Results
The run
method on CompiledModel
returns a StanResults
object, which has several methods to extract
samples and statistics on the results of the run. These methods take a parameter (or transformed
parameter) as an argument.
For example, to get the mean of the samples for parameter y
:
val m = results.mean(model.y)
This will return the mean with the same shape as the input parameter. Note that the Scala data structure is indexed from zero (instead of one as it is in Stan).
For convenience, there is also a get
method on StanResults
to access the input data.
The results object also has a summary
method to output a summary of results like the stansummary
program:
results.summary(System.out)
External Stan Code
It is possible to use ScalaStan to generate regular Stan code using the emit
function on the model.
For example:
object MyModel extends StanModel {
// ...
}
val writer = new java.io.PrintWriter("myModel.stan")
MyModel.emit(writer)
writer.close()
It is also possible to use ScalaStan to run existing Stan models. To use an existing model, the data
and parameters must be set up in ScalaStan, then the loadFromFile
(or loadFromString
) method on
Model
can be used to load the model. Once loaded, the model can be used just as a model implemented
directly in ScalaStan. For example:
object MyModel extends StanModel {
val n = data(int(lower = 0))
val x = data(vector(n))
val mu = parameter(real())
val sigma = parameter(real(lower = 0))
loadFromFile("normal.stan")
}
// ...