Training Reservoir Computing Models

Training reservoir computing (RC) models usually means solving a linear regression problem. ReservoirComputing.jl offers multiple stratedies to provide a readout; in this page we will show the basics, while also pointing out the possible extensions.

Training in ReservoirComputing.jl: Ridge Regression

The most simple training of RC models is through ridge regression. Given the widepread adoption of this training mechanism, ridge regression is the default training algorithm for RC models in the library.

using ReservoirComputing
using Random
Random.seed!(42)
rng = MersenneTwister(42)

input_data = rand(Float32, 3, 100)
target_data = rand(Float32, 5, 100)

model = ESN(3, 100, 5)
ps, st = setup(rng, model)
ps, st = train!(model, input_data, target_data, ps, st,
    StandardRidge(); # default
    solver = QRSolver()) # default
((reservoir = (input_matrix = Float32[0.086408615 0.0998888 -0.013271046; -0.06170206 -0.06359978 0.0921186; … ; -0.027093245 -0.04336114 -0.04771917; 0.07098396 0.015740562 0.019439578], reservoir_matrix = Float32[0.0 0.0 … 0.0 0.38458103; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.07436356 … 0.0 0.0]), states_modifiers = (), readout = (weight = [155.35108756323686 -135.8712657819953 … 155.88074896713223 -17.109907041437385; 269.94220128782416 -308.8849300590367 … 308.3685883855184 -132.17113965976264; … ; 278.4173811801401 -303.6919995502757 … 244.19595651464243 -197.58585602683507; -3.5393982571778024 37.21017225452454 … -1.2648540501081846 31.08003819647461],)), (reservoir = (cell = (rng = Random.MersenneTwister(42, (0, 25050, 24048, 870)),), carry = (Float32[0.14156468; -0.036177248; … ; -0.38727927; 0.09736941;;],)), states_modifiers = (), readout = NamedTuple(), states = Float32[0.67556715 0.16518015 … 0.1620921 0.14156468; 0.0003713667 -0.30405453 … 0.0013688962 -0.036177248; … ; -0.2935047 0.20566761 … -0.345226 -0.38727927; -0.7566881 -0.592658 … 0.047118302 0.09736941]))

In this call you can see that there are two possible knobs to be modified: the loss function, in this case ridge, and the solver, in this case the build in QR factorization. In the remaining part of this tutorial we will see how it is possible to change either.

Changing Ridge Regression Solver

Building on SciML's LinearSolve.jl, it is possible to leverage multiple solvers for the ridge problem. For instance, building on the previous example:

using LinearSolve

ps, st = train!(model, input_data, target_data, ps, st,
    StandardRidge(); # default
    solver = SVDFactorization()) # from LinearSolve
((reservoir = (input_matrix = Float32[0.086408615 0.0998888 -0.013271046; -0.06170206 -0.06359978 0.0921186; … ; -0.027093245 -0.04336114 -0.04771917; 0.07098396 0.015740562 0.019439578], reservoir_matrix = Float32[0.0 0.0 … 0.0 0.38458103; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.07436356 … 0.0 0.0]), states_modifiers = (), readout = (weight = Float32[-0.42554864 2.830525 … -10.93533 9.562259; -1.7472765 11.085349 … -2.2991953 -6.7392015; … ; 0.51117396 16.642477 … 3.350666 1.195735; 4.609715 -1.8165002 … -16.057856 6.02251],)), (reservoir = (cell = (rng = Random.MersenneTwister(42, (0, 25050, 24048, 870)),), carry = (Float32[0.14502607; -0.030527564; … ; -0.38960707; 0.09629541;;],)), states_modifiers = (), readout = NamedTuple(), states = Float32[0.24012297 0.09647721 … 0.15855159 0.14502607; 0.070599996 -0.07172731 … -0.0055311597 -0.030527564; … ; -0.3597568 -0.3799437 … -0.34301454 -0.38960707; 0.038221236 0.031300593 … 0.048109535 0.09629541]))

or

ps, st = train!(model, input_data, target_data, ps, st,
    StandardRidge(); # default
    solver = QRFactorization()) # from LinearSolve
((reservoir = (input_matrix = Float32[0.086408615 0.0998888 -0.013271046; -0.06170206 -0.06359978 0.0921186; … ; -0.027093245 -0.04336114 -0.04771917; 0.07098396 0.015740562 0.019439578], reservoir_matrix = Float32[0.0 0.0 … 0.0 0.38458103; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.07436356 … 0.0 0.0]), states_modifiers = (), readout = (weight = Float32[122.6651 11.0168705 … -262.47482 122.44317; 170.44348 -230.26588 … -110.79341 272.9332; … ; 46.673862 70.66685 … -362.86142 471.12817; -93.76712 82.558914 … 130.86298 -78.306595],)), (reservoir = (cell = (rng = Random.MersenneTwister(42, (0, 25050, 24048, 870)),), carry = (Float32[0.14514889; -0.030315312; … ; -0.38968256; 0.09626353;;],)), states_modifiers = (), readout = NamedTuple(), states = Float32[0.2366648 0.099875934 … 0.15842067 0.14514889; 0.06441728 -0.0663825 … -0.0057809157 -0.030315312; … ; -0.3572218 -0.38247722 … -0.34294105 -0.38968256; 0.039269898 0.030111087 … 0.048138604 0.09626353]))

For a detailed explanation of the different solvers, as well as a complete list of them, we suggest visiting the appropriate page in LinearSolve's documentation

Changing Linear Regression Problem

Linear regression is a general problem, which can be espressed through multiple different loss functions. While ridge regression is the most common in RC, due to its closed form, there are multiple other available. ReservoirComputing.jl leverages MLJLinearModels.jl to access all the methods available from that library.

Warn

Currently MLJLinearModels.jl only supports Float64. If a certain precision is of the upmost importance to you, please refrain from using this external package

The train function can be called as before, only this time you can specify different models and different solvers for the linear regression problem:

using MLJLinearModels

ps, st = train!(model, input_data, target_data, ps, st,
    LassoRegression(fit_intercept=false); # from MLJLinearModels
    solver = ProxGrad()) # from MLJLinearModels
((reservoir = (input_matrix = Float32[0.086408615 0.0998888 -0.013271046; -0.06170206 -0.06359978 0.0921186; … ; -0.027093245 -0.04336114 -0.04771917; 0.07098396 0.015740562 0.019439578], reservoir_matrix = Float32[0.0 0.0 … 0.0 0.38458103; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 0.0; 0.0 0.07436356 … 0.0 0.0]), states_modifiers = (), readout = (weight = Float32[0.0 -0.0 … -0.0 0.0; 0.0 -0.0 … -0.0 0.0; … ; 0.0 -0.0 … -0.0 0.0; 0.0 -0.0 … -0.0 0.0],)), (reservoir = (cell = (rng = Random.MersenneTwister(42, (0, 25050, 24048, 870)),), carry = (Float32[0.1451532; -0.030307855; … ; -0.3896852; 0.09626243;;],)), states_modifiers = (), readout = NamedTuple(), states = Float32[0.23654257 0.09999292 … 0.15841615 0.1451532; 0.06419469 -0.066186875 … -0.0057896706 -0.030307855; … ; -0.35713658 -0.3825614 … -0.34293845 -0.3896852; 0.039303705 0.030072607 … 0.048139594 0.09626243]))

Make sure to check the MLJLinearModels documentation pages for the available models and solvers. Please note that not all solvers can be used on all the models.

Note

Currently the support for MLJLinearModels.jl is limited to regressors with fit_intercept=false. We are working on a solution, but until then you will always need to specify it on the regressor.

Support Vector Regression

ReservoirComputing.jl also allows users to train RC models with support vector regression through LIBSVM.jl. However, the majority of builtin models in the library uses a LinearReadout by default, which can only be trained with linear regression. In order to use support vector regression, one needs to build a model with SVMReadout

using LIBSVM

model = ReservoirComputer(
    StatefulLayer(ESNCell(3=>100)),
    SVMReadout(100=>5)
)

ps, st = setup(rng, model)
((reservoir = (input_matrix = Float32[0.036005475 -0.07744928 0.00549283; -0.027602648 -0.03803761 0.029640054; … ; 0.020895792 -0.04784565 -0.09396603; -0.05864432 0.05747981 0.08333655], reservoir_matrix = Float32[0.0 0.0 … -0.056890085 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 -0.06416772; 0.0 0.0 … 0.009671479 0.0]), states_modifiers = (), readout = NamedTuple()), (reservoir = (cell = (rng = Random.MersenneTwister(42, (0, 50100, 49098, 357)),), carry = nothing), states_modifiers = (), readout = NamedTuple()))

We can now train our new model similarly to before:

ps, st = train!(model, input_data, target_data, ps, st,
    EpsilonSVR() # from LIBSVM
    )
((reservoir = (input_matrix = Float32[0.036005475 -0.07744928 0.00549283; -0.027602648 -0.03803761 0.029640054; … ; 0.020895792 -0.04784565 -0.09396603; -0.05864432 0.05747981 0.08333655], reservoir_matrix = Float32[0.0 0.0 … -0.056890085 0.0; 0.0 0.0 … 0.0 0.0; … ; 0.0 0.0 … 0.0 -0.06416772; 0.0 0.0 … 0.009671479 0.0]), states_modifiers = (), readout = (models = Any[LIBSVM.EpsilonSVR(LIBSVM.Kernel.RadialBasis, 0.01, 0.1, 1.0, 3, 0.0, 0.001, true, false, LIBSVM.SVM{Float64, LIBSVM.Kernel.KERNEL}(LIBSVM.EpsilonSVR, LIBSVM.Kernel.RadialBasis, nothing, 100, 100, 2, Float64[], Int32[], Float64[], Int32[], LIBSVM.SupportVectors{Vector{Float32}, Matrix{Float32}}(0, Int32[], Float32[], Matrix{Float32}(undef, 100, 0), Int32[], LIBSVM.SVMNode[]), 0.0, Matrix{Float64}(undef, 0, 1), Float64[], Float64[], [-0.0036369473622437157], 3, 0.01, 200.0, 0.001, 1.0, 0.5, 0.1, true, false)), LIBSVM.EpsilonSVR(LIBSVM.Kernel.RadialBasis, 0.01, 0.1, 1.0, 3, 0.0, 0.001, true, false, LIBSVM.SVM{Float64, LIBSVM.Kernel.KERNEL}(LIBSVM.EpsilonSVR, LIBSVM.Kernel.RadialBasis, nothing, 100, 100, 2, Float64[], Int32[], Float64[], Int32[], LIBSVM.SupportVectors{Vector{Float32}, Matrix{Float32}}(0, Int32[], Float32[], Matrix{Float32}(undef, 100, 0), Int32[], LIBSVM.SVMNode[]), 0.0, Matrix{Float64}(undef, 0, 1), Float64[], Float64[], [-0.0036369473622437157], 3, 0.01, 200.0, 0.001, 1.0, 0.5, 0.1, true, false)), LIBSVM.EpsilonSVR(LIBSVM.Kernel.RadialBasis, 0.01, 0.1, 1.0, 3, 0.0, 0.001, true, false, LIBSVM.SVM{Float64, LIBSVM.Kernel.KERNEL}(LIBSVM.EpsilonSVR, LIBSVM.Kernel.RadialBasis, nothing, 100, 100, 2, Float64[], Int32[], Float64[], Int32[], LIBSVM.SupportVectors{Vector{Float32}, Matrix{Float32}}(0, Int32[], Float32[], Matrix{Float32}(undef, 100, 0), Int32[], LIBSVM.SVMNode[]), 0.0, Matrix{Float64}(undef, 0, 1), Float64[], Float64[], [-0.0036369473622437157], 3, 0.01, 200.0, 0.001, 1.0, 0.5, 0.1, true, false)), LIBSVM.EpsilonSVR(LIBSVM.Kernel.RadialBasis, 0.01, 0.1, 1.0, 3, 0.0, 0.001, true, false, LIBSVM.SVM{Float64, LIBSVM.Kernel.KERNEL}(LIBSVM.EpsilonSVR, LIBSVM.Kernel.RadialBasis, nothing, 100, 100, 2, Float64[], Int32[], Float64[], Int32[], LIBSVM.SupportVectors{Vector{Float32}, Matrix{Float32}}(0, Int32[], Float32[], Matrix{Float32}(undef, 100, 0), Int32[], LIBSVM.SVMNode[]), 0.0, Matrix{Float64}(undef, 0, 1), Float64[], Float64[], [-0.0036369473622437157], 3, 0.01, 200.0, 0.001, 1.0, 0.5, 0.1, true, false)), LIBSVM.EpsilonSVR(LIBSVM.Kernel.RadialBasis, 0.01, 0.1, 1.0, 3, 0.0, 0.001, true, false, LIBSVM.SVM{Float64, LIBSVM.Kernel.KERNEL}(LIBSVM.EpsilonSVR, LIBSVM.Kernel.RadialBasis, nothing, 100, 100, 2, Float64[], Int32[], Float64[], Int32[], LIBSVM.SupportVectors{Vector{Float32}, Matrix{Float32}}(0, Int32[], Float32[], Matrix{Float32}(undef, 100, 0), Int32[], LIBSVM.SVMNode[]), 0.0, Matrix{Float64}(undef, 0, 1), Float64[], Float64[], [-0.0036369473622437157], 3, 0.01, 200.0, 0.001, 1.0, 0.5, 0.1, true, false))],)), (reservoir = (cell = (rng = Random.MersenneTwister(42, (0, 50100, 49098, 457)),), carry = (Float32[-0.07993154; -0.056282256; … ; -0.2977938; -0.016758788;;],)), states_modifiers = (), readout = NamedTuple(), states = Float32[0.77942973 -0.61240965 … -0.038224187 -0.07993154; -0.87535733 0.0026252489 … 0.07984722 -0.056282256; … ; -0.85399914 -0.89032173 … -0.25453427 -0.2977938; 0.27237374 -0.12041608 … -0.07133142 -0.016758788]))