Skip to content

Commit

Permalink
Merge pull request #141 from JuliaAI/dev
Browse files Browse the repository at this point in the history
For a 1.4.0 release
  • Loading branch information
ablaom authored Feb 28, 2022
2 parents 4e1a382 + 8b11617 commit 4845e17
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 90 deletions.
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "MLJModelInterface"
uuid = "e80e1ace-859a-464e-9ed9-23947d8ae3ea"
authors = ["Thibaut Lienart and Anthony Blaom"]
version = "1.3.6"
version = "1.4.0"

[deps]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Expand All @@ -18,9 +18,10 @@ CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
ScientificTypes = "321657f4-b219-11e9-178b-2701a2544e81"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["CategoricalArrays", "DataFrames", "Distances", "InteractiveUtils", "ScientificTypes", "Tables", "Test"]
test = ["CategoricalArrays", "DataFrames", "Distances", "InteractiveUtils", "Markdown", "ScientificTypes", "Tables", "Test"]
1 change: 1 addition & 0 deletions src/MLJModelInterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const MODEL_TRAITS = [
:supports_online,
:docstring,
:name,
:human_name,
:is_supervised,
:prediction_type,
:abstract_type,
Expand Down
240 changes: 191 additions & 49 deletions src/metadata_utils.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,3 @@
"""
docstring_ext
Internal function to help generate the docstring for a package. See
[`metadata_model`](@ref).
"""
function docstring_ext(T; descr::String="")
package_name = MLJModelInterface.package_name(T)
package_url = MLJModelInterface.package_url(T)
model_name = MLJModelInterface.name(T)
# the message to return
message = "$descr"
message *= "\n→ based on [$package_name]($package_url)."
message *= "\n→ do `@load $model_name pkg=\"$package_name\"` to " *
"use the model."
message *= "\n→ do `?$model_name` for documentation."
end

"""
metadata_pkg(T; args...)
Expand Down Expand Up @@ -72,19 +54,30 @@ function metadata_pkg(
parentmodule(T).eval(ex)
end

# Extend `program` (an expression) to include trait definition for
# specified `trait` and type `T`.
function _extend!(program::Expr, trait::Symbol, value, T)
if value !== nothing
push!(program.args, quote
MLJModelInterface.$trait(::Type{<:$T}) = $value
end)
return nothing
end
end

"""
metadata_model(`T`; args...)
Helper function to write the metadata for a model `T`.
## Keywords
* `input_scitype=Unknown` : allowed scientific type of the input data
* `target_scitype=Unknown`: allowed sc. type of the target (supervised)
* `output_scitype=Unknown`: allowed sc. type of the transformed data (unsupervised)
* `supports_weights=false` : whether the model supports sample weights
* `docstring=""` : short description of the model
* `load_path=""` : where the model is (usually `PackageName.ModelName`)
* `input_scitype=Unknown`: allowed scientific type of the input data
* `target_scitype=Unknown`: allowed scitype of the target (supervised)
* `output_scitype=Unkonwn`: allowed scitype of the transformed data (unsupervised)
* `supports_weights=false`: whether the model supports sample weights
* `supports_class_weights=false`: whether the model supports class weights
* `load_path="unknown"`: where the model is (usually `PackageName.ModelName`)
## Example
Expand All @@ -93,43 +86,192 @@ metadata_model(KNNRegressor,
input_scitype=MLJModelInterface.Table(MLJModelInterface.Continuous),
target_scitype=AbstractVector{MLJModelInterface.Continuous},
supports_weights=true,
docstring="K-Nearest Neighbors classifier: ...",
load_path="NearestNeighbors.KNNRegressor")
```
"""
function metadata_model(
T;
# aliases:
input=Unknown,
target=Unknown,
output=Unknown,
weights::Bool=false,
descr::String="",
path::String="",
input=nothing,
target=nothing,
output=nothing,
weights::Union{Nothing,Bool}=nothing,
class_weights::Union{Nothing,Bool}=nothing,
descr::Union{Nothing,String}=nothing,
path::Union{Nothing,String}=nothing,

# preferred names, corresponding to trait names:
input_scitype=input,
target_scitype=target,
output_scitype=output,
supports_weights=weights,
docstring=descr,
load_path=path,
supports_weights::Union{Nothing,Bool}=weights,
supports_class_weights::Union{Nothing,Bool}=class_weights,
docstring::Union{Nothing,String}=descr,
load_path::Union{Nothing,String}=path,
human_name::Union{Nothing,String}=nothing
)
if isempty(load_path)
pname = MLJModelInterface.package_name(T)
mname = MLJModelInterface.name(T)
load_path = "MLJModels.$(pname)_.$(mname)"
end
ex = quote
MLJModelInterface.input_scitype(::Type{<:$T}) = $input_scitype
MLJModelInterface.output_scitype(::Type{<:$T}) = $output_scitype
MLJModelInterface.target_scitype(::Type{<:$T}) = $target_scitype
MLJModelInterface.supports_weights(::Type{<:$T}) = $supports_weights
MLJModelInterface.load_path(::Type{<:$T}) = $load_path

function MLJModelInterface.docstring(::Type{<:$T})
return MLJModelInterface.docstring_ext($T; descr=$docstring)

program = quote end

# Note: Naively using metaprogramming to roll up the following
# code does not work. Only change this if you really know what
# you're doing.
_extend!(program, :input_scitype, input_scitype, T)
_extend!(program, :target_scitype, target_scitype, T)
_extend!(program, :output_scitype, output_scitype, T)
_extend!(program, :supports_weights, supports_weights, T)
_extend!(program, :supports_class_weights,supports_class_weights, T)
_extend!(program, :docstring, docstring, T)
_extend!(program, :load_path, load_path, T)
_extend!(program, :human_name, human_name, T)

parentmodule(T).eval(program)
end

# TODO: After `human_name` trait is added as model trait, include in
# example given in the docstring for `doc_header`.

"""
MLJModelInterface.doc_header(SomeModelType)
Return a string suitable for interpolation in the document string of
an MLJ model type. In the example given below, the header expands to
something like this:
> `FooRegressor`
>
>Model type for foo regressor, based on [FooRegressorPkg.jl](http://existentialcomics.com/).
>
>From MLJ, the type can be imported using
>
>
> `FooRegressor = @load FooRegressor pkg=FooRegressorPkg`
>
>Construct an instance with default hyper-parameters using the syntax
>`model = FooRegressor()`. Provide keyword arguments to override
>hyper-parameter defaults, as in `FooRegressor(a=...)`.
Ordinarily, `doc_header` is used in document strings defined *after*
the model type definition, as `doc_header` assumes model traits (in
particular, `package_name` and `package_url`) to be defined; see also
[`MLJModelInterface.metadata_pkg`](@ref).
### Example
Suppose a model type and traits have been defined by:
```
mutable struct FooRegressor
a::Int
b::Float64
end
metadata_pkg(FooRegressor,
name="FooRegressorPkg",
uuid="10745b16-79ce-11e8-11f9-7d13ad32a3b2",
url="http://existentialcomics.com/",
)
metadata_model(FooRegressor,
input=Table(Continuous),
target=AbstractVector{Continuous},
descr="La di da")
```
Then the docstring is defined post-facto with the following code:
```
const HEADER = MLJModelInterface.doc_header(FooRegressor)
@doc \"\"\"
\$HEADER
### Training data
In MLJ or MLJBase, bind an instance `model` ...
<rest of doc string goes here>
\"\"\" FooRegressor
```
"""
function doc_header(SomeModelType)
name = MLJModelInterface.name(SomeModelType)
human_name = MLJModelInterface.human_name(SomeModelType)
package_name = MLJModelInterface.package_name(SomeModelType)
package_url = MLJModelInterface.package_url(SomeModelType)
params = MLJModelInterface.hyperparameters(SomeModelType)

ret =
"""
```
$name
```
Model type for $human_name, based on
[$(package_name).jl]($package_url), and implementing the MLJ
model interface.
From MLJ, the type can be imported using
```
$name = @load $name pkg=$package_name
```
Do `model = $name()` to construct an instance with default hyper-parameters.
""" |> chomp

ret *= " "

isempty(params) && return ret

p = first(params)
ret *=
"""
Provide keyword arguments to override hyper-parameter defaults, as in
`$name($p=...)`.
""" |> chomp

return ret
end

"""
synthesize_docstring
Private method.
Generates a value for the `docstring` trait for use with a model which
does not have a standard document string, to use as the fallback. See
[`metadata_model`](@ref).
"""
function synthesize_docstring(M)
package_name = MLJModelInterface.package_name(M)
package_url = MLJModelInterface.package_url(M)
model_name = MLJModelInterface.name(M)
human_name = MLJModelInterface.human_name(M)
hyperparameters = MLJModelInterface.hyperparameters(M)

# generate text for the section on hyperparameters
text_for_params = ""
if !is_wrapper(M)
model = M()
isempty(hyperparameters) || (text_for_params *= "# Hyper-parameters")
for p in hyperparameters
value = getproperty(model, p)
text_for_params *= "\n\n- `$p = $value`"
end
end
parentmodule(T).eval(ex)

ret = doc_header(M)
if !isempty(text_for_params)
ret *=
"""
$text_for_params
"""
end
return ret
end
9 changes: 5 additions & 4 deletions src/model_traits.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ const DeterministicDetector = Union{

const StatTraits = StatisticalTraits

StatTraits.docstring(M::Type{<:MLJType}) = name(M)

function StatTraits.docstring(M::Type{<:Model})
return "$(name(M)) from $(package_name(M)).jl.\n" *
"[Documentation]($(package_url(M)))."
docstring = Base.Docs.doc(M) |> string
if occursin("No documentation found", docstring)
docstring = synthesize_docstring(M)
end
return docstring
end

StatTraits.is_supervised(::Type{<:Supervised}) = true
Expand Down
Loading

0 comments on commit 4845e17

Please sign in to comment.