Model Wrappers

Lipschitz-Bounded Deep Networks

RobustNeuralNetworks.AbstractLBDNType
abstract type AbstractLBDN{T, L} end

Explicit parameterisation for Lipschitz-bounded deep networks.

(m::AbstractLBDN)(u::AbstractVecOrMat)

Call and AbstractLBDN model given inputs u.

If arguments are matrices, each column must be a vector of inputs (allows batch simulations).

Examples

This example creates a dense LBDN using DenseLBDNParams and calls the model with some randomly generated inputs.

using Random
using RobustNeuralNetworks

# Setup
rng = Xoshiro(42)
batches = 10
γ = 20.0

# Model with 4 inputs, 1 ouput, 4 hidden layers
nu, ny = 4, 1
nh = [5, 10, 5, 15]

lbdn_ps = DenseLBDNParams{Float64}(nu, nh, ny, γ; rng)
lbdn = LBDN(lbdn_ps)

# Evaluate model with a batch of random inputs
u = 10*randn(rng, nu, batches)
y = lbdn(u)

println(round.(y; digits=2))

# output

[-1.11 -1.01 -0.07 -2.25 -4.22 -1.76 -3.82 -1.13 -11.85 -3.01]
RobustNeuralNetworks.DiffLBDNType
DiffLBDN(ps::AbstractLBDNParams)

Construct a differentiable LBDN from its direct parameterisation.

DiffLBDN is an alternative to LBDN that computes the explicit parameterisation ExplicitLBDNParams each time the model is called, rather than storing it in the LBDN object.

This model wrapper is easiest to use if you plan to update the model parameters after every call of the model. It can be trained just like any other Flux.jl model and does not need to be re-created if the trainable parameters are updated (unlike LBDN).

However, it is slow and computationally inefficient if the model is called many times before updating the parameters (eg: in reinforcement learning).

Examples

The syntax to construct a DiffLBDN is identical to that of an LBDN.

using RobustNeuralNetworks

nu, nh, ny, γ = 1, [10, 20], 1
lbdn_params = DenseLBDNParams{Float64}(nu, nh, ny, γ)
model = DiffLBDN(lbdn_params)

See also AbstractLBDN, LBDN, SandwichFC.

RobustNeuralNetworks.SandwichFCType
SandwichFC((in, out), σ::F; <keyword arguments>) where F

Construct a non-expansive "sandwich layer" for a dense (fully-connected) LBDN.

A non-expensive layer is a layer with a Lipschitz bound of exactly 1. This layer is provided as a Lipschitz-bounded alternative to Flux.Dense. Its interface and usage was intentionally designed to be very similar to make it easy to use for anyone familiar with Flux.

Arguments

  • (in, out)::Pair{<:Integer, <:Integer}: Input and output sizes of the layer.
  • σ::F=identity: Activation function.

Keyword arguments

  • init::Function=Flux.glorot_normal: Initialisation function for all weights and bias vectors.
  • bias::Bool=true: Include bias vector or not.
  • output_layer::Bool=false: Just the output layer of a dense LBDN or regular sandwich layer.
  • T::DataType=Float32: Data type for weight matrices and bias vectors.
  • rng::AbstractRNG = Random.GLOBAL_RNG: rng for model initialisation.

Examples

We can build a dense LBDN directly using SandwichFC layers. The model structure is described in Equation 8 of Wang & Manchester (2023).

using Flux
using Random
using RobustNeuralNetworks

# Random seed for consistency
rng = Xoshiro(42)

# Model specification
nu = 1                  # Number of inputs
ny = 1                  # Number of outputs
nh = fill(16,2)         # 2 hidden layers, each with 16 neurons
γ = 5                   # Lipschitz bound of 5.0

# Set up dense LBDN model
model = Flux.Chain(
    (x) -> (√γ * x),
    SandwichFC(nu => nh[1], relu; T=Float64, rng),
    SandwichFC(nh[1] => nh[2], relu; T=Float64, rng),
    (x) -> (√γ * x),
    SandwichFC(nh[2] => ny; output_layer=true, T=Float64, rng),
)

# Evaluate on dummy inputs
u = 10*randn(rng, nu, 10)
y = model(u)

println(round.(y;digits=2))

# output

[4.13 4.37 3.22 8.38 4.15 3.71 0.7 2.04 1.78 2.64]

See also DenseLBDNParams, DiffLBDN.

Recurrent Equilibrium Networks

RobustNeuralNetworks.AbstractRENType
abstract type AbstractREN end

Explicit parameterisation for recurrent equilibrium networks.

(m::AbstractREN)(xt::AbstractVecOrMat, ut::AbstractVecOrMat)

Call an AbstractREN model given internal states xt and inputs ut.

If arguments are matrices, each column must be a vector of states or inputs (allows batch simulations).

Examples

This example creates a contracting REN using ContractingRENParams and calls the model with some randomly generated inputs.

using Random
using RobustNeuralNetworks

# Setup
rng = Xoshiro(42)
batches = 10
nu, nx, nv, ny = 4, 2, 20, 1

# Construct a REN
contracting_ren_ps = ContractingRENParams{Float64}(nu, nx, nv, ny; rng)
ren = REN(contracting_ren_ps)

# Some random inputs
x0 = init_states(ren, batches; rng)
u0 = randn(rng, ren.nu, batches)

# Evaluate the REN over one timestep
x1, y1 = ren(x0, u0)

println(round.(y1;digits=2))

# output

[-1.49 0.75 1.34 -0.23 -0.84 0.38 0.79 -0.1 0.72 0.54]

See also REN, WrapREN, and DiffREN.

RobustNeuralNetworks.DiffRENType
DiffREN(ps::AbstractRENParams{T}) where T

Construct a differentiable REN from its direct parameterisation.

DiffREN is an alternative to REN and WrapREN that computes the explicit parameterisation ExplicitRENParams every time the model is called, rather than storing it in the REN object.

This model wrapper is easiest to use if you plan to update the model parameters after every call of the model. It an be trained just like any other Flux.jl model (unlike WrapREN) and does not need to be re-created if the trainable parameters are updated (unlike REN).

However, it is slow and computationally inefficient if the model is called many times before updating the parameters (eg: in reinforcement learning).

Examples

The syntax to construct a DiffREN is identical to that of a REN.

using RobustNeuralNetworks

nu, nx, nv, ny = 1, 10, 20, 1
ren_params = ContractingRENParams{Float64}(nu, nx, nv, ny)
model = DiffREN(ren_params)

See also AbstractREN, REN, and WrapREN.

RobustNeuralNetworks.WrapRENType
WrapREN(ps::AbstractRENParams{T}) where T

[NOTE:] THIS IS A LEGACY WRAPPER AND WILL BE REMOVED IN A FUTURE RELEASE.

Construct REN wrapper from its direct parameterisation.

WrapREN is an alternative to REN that stores the AbstractRENParams and ExplicitRENParams within the same object. This means that a new REN object does not have to be created each time the parameters are updated. Explicit REN parameters must be updated by the user if the direct parameters have changed.

Note that WrapREN cannot be used with Flux.jl, since it relies on mutating the WrapREN instance.

Examples

In this example, we create a REN satisfying some generic behavioural constraints and demonstrate how to update the REN wrapper if model parameters are changed.

using LinearAlgebra
using Random
using RobustNeuralNetworks

# Setup
rng = Xoshiro(42)
batches = 10
nu, nx, nv, ny = 4, 10, 20, 2

Q = Matrix{Float64}(-I(ny))
R = 0.1^2 * Matrix{Float64}(I(nu))
S = zeros(Float64, nu, ny)

# Construct a REN
ren_ps = GeneralRENParams{Float64}(nu, nx, nv, ny, Q, S, R; rng)
ren = WrapREN(ren_ps)

# Some dummy inputs
x0 = init_states(ren, batches; rng)
u0 = randn(rng, ren.nu, batches)

# Evaluate the REN over one timestep
x1, y1 = ren(x0, u0) 

# Update the model after changing a parameter
ren.params.direct.B2 .*= rand(rng, size(ren.params.direct.B2)...)
update_explicit!(ren)

println(round(ren.explicit.B2[10];digits=4))

# output

-0.0034

See also AbstractREN, REN, and DiffREN.