New model type declarations
Here is an example of a concrete supervised model type declaration, for a model with a single hyperparameter:
import MLJModelInterface
const MMI = MLJModelInterface
mutable struct RidgeRegressor <: MMI.Deterministic
lambda::Float64
end
Models (which are mutable) should not be given internal constructors. It is recommended that they be given an external lazy keyword constructor of the same name. This constructor defines default values for every field, and optionally corrects invalid field values by calling a clean!
method (whose fallback returns an empty message string):
function MMI.clean!(model::RidgeRegressor)
warning = ""
if model.lambda < 0
warning *= "Need lambda ≥ 0. Resetting lambda=0. "
model.lambda = 0
end
return warning
end
# keyword constructor
function RidgeRegressor(; lambda=0.0)
model = RidgeRegressor(lambda)
message = MMI.clean!(model)
isempty(message) || @warn message
return model
end
Important. Performing clean!(model)
a second time should not mutate model
. That is, this test should hold:
clean!(model)
clone = deepcopy(model)
clean!(model)
@test model == clone
Although not essential, try to avoid Union
types for model fields. For example, a field declaration features::Vector{Symbol}
with a default of Symbol[]
(detected with isempty
method) is preferred to features::Union{Vector{Symbol}, Nothing}
with a default of nothing
.
Hyperparameters for parallelization options
The section Acceleration and Parallelism of the MLJ manual indicates how users specify an option to run an algorithm using distributed processing or multithreading. A hyperparameter specifying such an option should be called acceleration
. Its value a
should satisfy a isa AbstractResource
where AbstractResource
is defined in the ComputationalResources.jl package. An option to run on a GPU is ordinarily indicated with the CUDALibs()
resource.
hyperparameter access and mutation
To support hyperparameter optimization (see the Tuning Models section of the MLJ manual) any hyperparameter to be individually controlled must be:
property-accessible; nested property access allowed, as in
model.detector.K
mutable
For an un-nested hyperparameter, the requirement is that getproperty(model, :param_name)
and setproperty!(model, :param_name, value)
have the expected behavior.
Combining hyperparameters in a named tuple does not generally work: although property-accessible (with nesting), an individual value cannot be mutated.
For a suggested way to deal with hyperparameters varying in number, see the implementation of Stack
, where the model struct stores a varying number of base models internally as a vector, but components are named at construction and accessed by overloading getproperty/setproperty!
appropriately.
Macro shortcut
An alternative to declaring the model struct, clean! method and keyword constructor, is to use the @mlj_model
macro, as in the following example:
@mlj_model mutable struct YourModel <: MMI.Deterministic
a::Float64 = 0.5::(_ > 0)
b::String = "svd"::(_ in ("svd","qr"))
end
This declaration specifies:
- A keyword constructor (here
YourModel(; a=..., b=...)
), - Default values for the hyperparameters,
- Constraints on the hyperparameters where
_
refers to a value passed.
For example, a::Float64 = 0.5::(_ > 0)
indicates that the field a
is a Float64
, takes 0.5
as default value, and expects its value to be positive.
You cannot use the @mlj_model
macro if your model struct has type parameters.
Known issue with @mlj_macro
Defaults with negative values can trip up the @mlj_macro
(see this issue). So, for example, this does not work:
@mlj_model mutable struct Bar
a::Int = -1::(_ > -2)
end
But this does:
@mlj_model mutable struct Bar
a::Int = (-)(1)::(_ > -2)
end