From 7ccaf2bc1817841cab6633cadcc37bdcfe33731c Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 4 Jan 2021 10:16:09 +1300 Subject: [PATCH 01/20] add instructions on adding a data front-end oops --- docs/src/adding_models_for_general_use.md | 104 ++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/docs/src/adding_models_for_general_use.md b/docs/src/adding_models_for_general_use.md index 64183478b..9df85a68a 100755 --- a/docs/src/adding_models_for_general_use.md +++ b/docs/src/adding_models_for_general_use.md @@ -843,6 +843,109 @@ additional information required (for example, pre-processed versions of `X` and `y`), as this is also passed as an argument to the `update` method. +### Implementing a data-front + +```julia + MLJModelInterface.reformat(model, args...) -> data + MLJModelInterface.selectrows(::Model, I, data...) -> sampled_data +``` + +Models optionally overload `reformat` to define transformations of +user-supplied data into some model-specific representation (e.g., from +a table to a matrix). Computational overheads associated with multiple +`fit!`/`predict`/`transform` calls (on MLJ machines) are then avoided, +when memory resources allow. The fallback returns `args` (no +transformation). + +The `selectrows(::Model, I, data...)` method is overloaded to specify +how the model-specific data is to be subsampled, for some observation +indices `I`. In this way, implementing a data front-end also allow +more efficient resampling of data (in user calls to `evaluate!`). + +Here "user-supplied data" is what the MLJ user supplies when +constructing a machine, as in `machine(models, args...)`, which +coincides with the arguments expected by `fit(model, verbosity, +args...)` when `reformat` is not overloaded. + +Implementing a `reformat` data front-end is permitted for any `Model` +subtype, except for subtypes of `Static`. Here is a complete list of +responsibilities for such an implementation, for some +`model::SomeModelType` (a sample implementation follows after): + +- A `reformat(model::SomeModelType, args...) -> data` method must be + implemented for each form of `args...` appearing in a valid machine + construction `machine(model, args...)` (there will be one for each + possible signature of `fit(::SomeModelType, ...)`). + +- Additionally, if not included above, there must be a single argument + form of reformat, `reformat(model::SommeModelType, arg) -> (data,)`, + serving as a data front-end for operations like `predict`. It must + always hold that `reformat(model, args...)[1] = reformat(model, + args[1])`. + +*Important.* `reformat(model::SomeModelType, args...)` must always + return a tuple of the same length as `args`, even if this is one. + +- `fit(model::SomeModelType, verbosity, data...)` should be + implemented as if `data` is the output of `reformat(model, + args...)`, where `args` is the data an MLJ user has bound to `model` + in some machine. The same applies to any overloading of `update`. + +- Each implemented operation, such as `predict` and `transform` - but + excluding `inverse_transform` - must be defined as if its data + arguments are `reformat`ed versions of user-supplied data. For + example, in the supervised case, `data_new` in + `predict(model::SomeModelType, fitresult, data_new)` is + `reformat(model, Xnew)`, where `Xnew` is the data provided by the MLJ + user in a call `predict(mach, Xnew)` (`mach.model == model`). + +- To specify how the model-specific representation of data is to be + resampled, implement `selectrows(model::SomeModelType, I, data...) + -> resampled_data` for each overloading of `reformat(model::SomeModel, + args...) -> data` above. Here `I` is an arbitrary abstract integer + vector or `:` (type `Colon`). + +*Important.* `selectrows(model::SomeModelType, I, args...)` must always +return a tuple of the same length as `args`, even if this is one. + +The fallback for `selectrows` is described at [`selectrows`](@ref). + + +#### Sample implementation + +Suppose a supervised model type `SomeSupervised` supports sample +weights, leading to two different `fit` signatures, and that it has a +single operation `predict`: + + fit(model::SomeSupervised, verbosity, X, y) + fit(model::SomeSupervised, verbosity, X, y, w) + + predict(model::SomeSupervised, fitresult, Xnew) + +Without a data front-end implemented, suppose `X` is expected to be a +table and `y` a vector, but suppose the core algorithm always converts +`X` to a matrix with features as rows (features corresponding to +columns in the table). Then a new data-front end might look like +this: + + constant MMI = MLJModelInterface + + # for fit: + MMI.reformat(::SomeSupervised, X, y) = (MMI.matrix(X, transpose=true), y) + MMI.reformat(::SomeSupervised, X, y, w) = (MMI.matrix(X, transpose=true), y, w) + MMI.selectrows(::SomeSupervised, I, Xmatrix, y) = + (view(Xmatrix, :, I), view(y, I)) + MMI.selectrows(::SomeSupervised, I, Xmatrix, y, w) = + (view(Xmatrix, :, I), view(y, I), view(w, I)) + + # for predict: + MMI.reformat(::SomeSupervised, X) = (MMI.matrix(X, transpose=true),) + MMI.selectrows(::SomeSupervised, I, Xmatrix) = view(Xmatrix, I) + +With these additions, `fit` and `predict` are refactored, so that `X` +and `Xnew` represent matrices with features as rows. + + ### Supervised models with a `transform` method A supervised model may optionally implement a `transform` method, @@ -1183,3 +1286,4 @@ add a model, you need to follow these steps - An administrator will then review your implementation and work with you to add the model to the registry +> From 82de1c6e5b4b41abf46f11d7727466451463f119 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 4 Jan 2021 13:59:39 +1300 Subject: [PATCH 02/20] more instructions --- docs/src/adding_models_for_general_use.md | 33 ++++++++++++++++--- .../src/quick_start_guide_to_adding_models.md | 5 +++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/src/adding_models_for_general_use.md b/docs/src/adding_models_for_general_use.md index 9df85a68a..908841c55 100755 --- a/docs/src/adding_models_for_general_use.md +++ b/docs/src/adding_models_for_general_use.md @@ -301,7 +301,8 @@ MMI.is_pure_julia(::Type{<:SomeSupervisedModel}) = false MMI.package_license(::Type{<:SomeSupervisedModel}) = "unknown" ``` -If `SomeSupervisedModel` supports sample weights, then instead of the `fit` above, one implements +If `SomeSupervisedModel` supports sample weights or class weights, +then instead of the `fit` above, one implements ```julia MMI.fit(model::SomeSupervisedModel, verbosity, X, y, w=nothing) -> fitresult, cache, report @@ -320,6 +321,19 @@ Additionally, if `SomeSupervisedModel` supports sample weights, one must declare MMI.supports_weights(model::Type{<:SomeSupervisedModel}) = true ``` +Optionally, an implemenation may add a data front-end, for +transforming user data (such as a table) into some model-specific +format (such as a matrix), and for adding methods to specify how said +format is resampled. (This alters the meaning of `X`, `y` and `w` in +the signatures of `fit`, `update`, `predict`, etc; see [Implementing a +data front-end](@ref) for details). This can provide the MLJ user +certain performance advantages when fitting a machine. + +```julia + MLJModelInterface.reformat(model::SomeSupervisedModel, args...) = args + MLJModelInterface.selectrows(model::SomeSupervisedModel, I, data...) = data +``` + Optionally, to customized support for serialization of machines (see [Serialization](@ref)), overload @@ -409,7 +423,14 @@ MMI.fit(model::SomeSupervisedModel, verbosity, X, y) -> fitresult, cache, report It is not necessary for `fit` to provide type or dimension checks on `X` or `y` or to call `clean!` on the model; MLJ will carry out such -checks. +checks. + +The types of `X` and `y` are constrained by the `input_scitype` and +`target_scitype` trait declarations; see [Trait declarations](@ref) +below. (That is, unless a data front-end is implemented, in which case +these traits refer instead to the arguments of the overloaded +`reformat` method, and the types of `X` and `y` are determined by the +output of `reformat`.) The method `fit` should never alter hyperparameter values, the sole exception being fields of type `<:AbstractRNG`. If the package is able @@ -843,7 +864,7 @@ additional information required (for example, pre-processed versions of `X` and `y`), as this is also passed as an argument to the `update` method. -### Implementing a data-front +### Implementing a data front-end ```julia MLJModelInterface.reformat(model, args...) -> data @@ -859,8 +880,10 @@ transformation). The `selectrows(::Model, I, data...)` method is overloaded to specify how the model-specific data is to be subsampled, for some observation -indices `I`. In this way, implementing a data front-end also allow -more efficient resampling of data (in user calls to `evaluate!`). +indices `I` (a colon, `:`, or instance of +`AbstractVector{<:Integer}`). In this way, implementing a data +front-end also allow more efficient resampling of data (in user calls +to `evaluate!`). Here "user-supplied data" is what the MLJ user supplies when constructing a machine, as in `machine(models, args...)`, which diff --git a/docs/src/quick_start_guide_to_adding_models.md b/docs/src/quick_start_guide_to_adding_models.md index dfb018912..8c4281308 100644 --- a/docs/src/quick_start_guide_to_adding_models.md +++ b/docs/src/quick_start_guide_to_adding_models.md @@ -32,6 +32,11 @@ still handle this; you just need to define a non-tabular clarify the appropriate declaration. The discussion below assumes input data is tabular. +For simplicity, this document assumes no data front-end is to be +defined for your model. Adding a data front-end, which offers the MLJ +user some performances benefits, is easy to add post-facto, and is +described in [Implementing a data front-end](@ref). + ### Overview To write an interface create a file or a module in your package which From 5547c5c4c85e0d4c5d4d79b5703541a45a0dbef9 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 4 Jan 2021 17:38:13 +1300 Subject: [PATCH 03/20] unrelated machine doc update --- docs/src/machines.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/src/machines.md b/docs/src/machines.md index 8645ccd9b..2b3138e42 100644 --- a/docs/src/machines.md +++ b/docs/src/machines.md @@ -1,15 +1,18 @@ # Machines -Under the hood, calling `fit!` on a machine calls either `MLJBase.fit` -or `MLJBase.update`, depending on the machine's internal state (as -recorded in private fields `old_model` and -`old_rows`). These lower-level `fit` and `update` methods, which -are not ordinarily called directly by the user, dispatch on the model -and a view of the data defined by the optional `rows` keyword argument -of `fit!` (all rows by default). In this way, if a model `update` -method has been implemented for the model, calls to `fit!` can avoid -redundant calculations for certain kinds of model mutations (eg, -increasing the number of epochs in a neural network). +Recall from [Getting Started](@ref) that a machine binds a model +(i.e., a choice of algorithm + hyperparameters) to data (see more at +[Constructing machines](@ref) below). A machine is also the object +storing *learned* parameters. Under the hood, calling `fit!` on a +machine calls either `MLJBase.fit` or `MLJBase.update`, depending on +the machine's internal state (as recorded in private fields +`old_model` and `old_rows`). These lower-level `fit` and `update` +methods, which are not ordinarily called directly by the user, +dispatch on the model and a view of the data defined by the optional +`rows` keyword argument of `fit!` (all rows by default). In this way, +if a model `update` method has been implemented for the model, calls +to `fit!` can avoid redundant calculations for certain kinds of model +mutations (eg, increasing the number of epochs in a neural network). ```@example machines using MLJ; color_off() # hide From ea4712408dbff37d99772d6251732e5a0ca8d721 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Tue, 5 Jan 2021 18:49:52 +1300 Subject: [PATCH 04/20] bump MMI requirement specified in docs to 0.3.7 --- docs/src/adding_models_for_general_use.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/adding_models_for_general_use.md b/docs/src/adding_models_for_general_use.md index 908841c55..9862c7653 100755 --- a/docs/src/adding_models_for_general_use.md +++ b/docs/src/adding_models_for_general_use.md @@ -2,7 +2,7 @@ !!! note - Models implementing the MLJ model interface according to the instructions given here should import MLJModelInterface version 0.3.5 or higher. This is enforced with a statement such as `MLJModelInterface = "^0.3.5" ` under `[compat]` in the Project.toml file of the package containing the implementation. + Models implementing the MLJ model interface according to the instructions given here should import MLJModelInterface version 0.3.7 or higher. This is enforced with a statement such as `MLJModelInterface = "^0.3.7" ` under `[compat]` in the Project.toml file of the package containing the implementation. This guide outlines the specification of the MLJ model interface and provides detailed guidelines for implementing the interface for From 6049c66caf390f85a6daf2198a7cfdce8ae95514 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Fri, 29 Jan 2021 09:58:48 +1300 Subject: [PATCH 05/20] doc change --- docs/src/adding_models_for_general_use.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/src/adding_models_for_general_use.md b/docs/src/adding_models_for_general_use.md index 9862c7653..a38087c92 100755 --- a/docs/src/adding_models_for_general_use.md +++ b/docs/src/adding_models_for_general_use.md @@ -866,6 +866,10 @@ method. ### Implementing a data front-end +!!! note + + It is suggested that packages implementing MLJ's model API, that later implement a data front-end, should tag their changes in a breaking release. (The changes will not break use of models for the ordinary MLJ user, who interacts with models exlusively through the machine interface. However, it will break usage for some external packages that have chosen to depend directly on the model API.) + ```julia MLJModelInterface.reformat(model, args...) -> data MLJModelInterface.selectrows(::Model, I, data...) -> sampled_data From 878102dbea5ae331f5185fb434dbda83611c0f59 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 1 Feb 2021 12:40:42 +1300 Subject: [PATCH 06/20] bump compat MLJBase = "^0.17" --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 349f588e9..99e14e9c0 100644 --- a/Project.toml +++ b/Project.toml @@ -24,7 +24,7 @@ Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" CategoricalArrays = "^0.8,^0.9" ComputationalResources = "^0.3" Distributions = "^0.21,^0.22,^0.23, 0.24" -MLJBase = "^0.16" +MLJBase = "^0.17" MLJModels = "^0.12.1,^0.13" MLJScientificTypes = "^0.4.1" MLJTuning = "^0.5.1" From aa3103d92844d82e82ad4f8071cbde12fea19e2e Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 1 Feb 2021 12:41:20 +1300 Subject: [PATCH 07/20] bump compat MLJModels = "^0.14" --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 99e14e9c0..ce64003e8 100644 --- a/Project.toml +++ b/Project.toml @@ -25,7 +25,7 @@ CategoricalArrays = "^0.8,^0.9" ComputationalResources = "^0.3" Distributions = "^0.21,^0.22,^0.23, 0.24" MLJBase = "^0.17" -MLJModels = "^0.12.1,^0.13" +MLJModels = "^0.14" MLJScientificTypes = "^0.4.1" MLJTuning = "^0.5.1" ProgressMeter = "^1.1" From aa3bb741a4890dda4c5894f3b71955a84b8f94c8 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 1 Feb 2021 19:49:57 +1300 Subject: [PATCH 08/20] add Loading Model Code section; update index.md re @load changes --- docs/make.jl | 1 + docs/src/index.md | 34 +++++++----------- docs/src/loading_model_code.md | 66 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 docs/src/loading_model_code.md diff --git a/docs/make.jl b/docs/make.jl index 8a184b02b..9d3bc81bd 100755 --- a/docs/make.jl +++ b/docs/make.jl @@ -27,6 +27,7 @@ pages = [ "Common MLJ Workflows" => "common_mlj_workflows.md", "Working with Categorical Data" => "working_with_categorical_data.md", "Model Search" => "model_search.md", + "Loading Model Code" => "loading_model_code", "Machines" => "machines.md", "Evaluating Model Performance" => "evaluating_model_performance.md", "Performance Measures" => "performance_measures.md", diff --git a/docs/src/index.md b/docs/src/index.md index daa35ab1c..d8a81a042 100755 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -55,12 +55,12 @@ using MLJ X, y = @load_reduced_ames; ``` -Load and instantiate a gradient tree-boosting model: +Load and instantiate a gradient tree-boosting model type: ```julia -booster = @load EvoTreeRegressor -booster.max_depth = 2 -booster.nrounds=50 +Booster = @load EvoTreeRegressor +Booster = booster(max_depth=2) # specify hyperparamter at construction +booster.nrounds=50 # or mutate post facto ``` Combine the model with categorical feature encoding: @@ -227,28 +227,18 @@ julia> Pkg.test("MLJ") It is important to note that MLJ is essentially a big wrapper providing a unified access to _model providing packages_. For this reason, one generally needs to add further packages to your -environment to make model-specific code available. For instance, if -you want to use a **Decision Tree Classifier**, you need to have -[MLJDecisionTreeInterface.jl](https://github.com/bensadeghi/DecisionTree.jl) -installed: +environment to make model-specific code available. This +happens automatically when you use MLJ's interactive load command +`@iload`, as in ```julia -julia> Pkg.add("MLJDecisionTreeInterface"); -julia> using MLJ; -julia> @load DecisionTreeClassifier +julia> Tree = @iload DecisionTreeClassifier # load type +julis> tree = Tree() # instantiate ``` -However, if you try to use `@load` without adding the required package to your -environment, an error message will tell you what package needs adding. - -For a list of models and their packages run - -```julia -using MLJ -models() -``` - -or refer to [List of Supported Models](@ref model_list) +For more on identifying the name of an applicable model, see [`Model +Search`](@ref). For non-interactive loading of code (e.g., from a +module or function) see [`Loading Model Code`](@ref). It is recommended that you start with models marked as coming from mature packages such as DecisionTree.jl, ScikitLearn.jl or XGBoost.jl. diff --git a/docs/src/loading_model_code.md b/docs/src/loading_model_code.md new file mode 100644 index 000000000..e1662125c --- /dev/null +++ b/docs/src/loading_model_code.md @@ -0,0 +1,66 @@ +# Loading model code + +Once the name of a model, and the package providing that model, have +been identified (see [`Model Search`](@ref)) one can either import the +model type interactively with `@iload`, as shown under +[`Installation`](@ref), or use `@load` as shown below. The `@load` +macro works from within a module, a package or a function, provided +the relevant package providing the MLJ interface has been added to +your package environment. + +In general, the code providing core functionality for the model +(living in a packge you should consult for documentation) may be +different from the package providing the MLJ interface. Since the core +package is a dependency of the interface package, only the interface +package needs to be added to your environment. + +For instance, suppose you have activated a Julia package environment +`my_env` that you wish to use for your MLJ project; for example you +have run: + + +```julia +using Pkg +Pkg.activate("my_env", shared=true) +``` + +And, furthermore, suppose you want to use `DecisionTreeClassifier`, +provided by the +[DecisionTree.jl](https://github.com/bensadeghi/DecisionTree.jl) +package. Then, to determine which package provides the MLJ interface +you call `load_path`: + +```julia +julia> load_path("DecisionTreeClassifier", pkg="DecisionTree") +"MLJDecisionTreeInterface.DecisionTreeClassifier" +``` + +In this case we see that the package required is +MLJDecisionTreeInterface.jl. If this package is not in `my_env` (do +`Pkg.status()` to check) you add it by running + +```julia +julia> Pkg.add("MLJDecisionTreeInterface"); +``` + +So long as `my_env` is the active environment, this action need never +be repeated (unless you run `Pkg.rm("MLJDecisionTreeInterface")`). You +are now ready to instantiate a decision tree classifier: + +```julia +julia> Tree = @load DecisionTree pkg=DecisionTree +julia> tree = Tree() +``` + +which is equivalent to + +```julia +julia> import MLJDecisionTreeInterface.DecisionTreeClassifier +julia> Tree = MLJDecisionTreeInterface.DecisionTreeClassifier() +julia> tree = Tree() +``` + +*Tip.* The specification `pkg=DecisionTree` above can actually be +dropped, as only one package provides a model called +`DecisionTreeClassifier`. + From 67aa53bf2bcf971edf626130a9fbecdbf64f7fc0 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Wed, 3 Feb 2021 11:07:58 +1300 Subject: [PATCH 09/20] code-reorganization require by MLJBase/MLJModels changes --- src/MLJ.jl | 6 +--- src/model_matching.jl | 71 ------------------------------------------ src/scitypes.jl | 2 ++ test/model_matching.jl | 30 ------------------ test/runtests.jl | 4 --- test/scitypes.jl | 13 ++++---- 6 files changed, 10 insertions(+), 116 deletions(-) delete mode 100644 src/model_matching.jl delete mode 100644 test/model_matching.jl diff --git a/src/MLJ.jl b/src/MLJ.jl index ed477c3ea..b22ce8410 100644 --- a/src/MLJ.jl +++ b/src/MLJ.jl @@ -8,9 +8,6 @@ export MLJ_VERSION # ensembles.jl: export EnsembleModel -# model_matching.jl: -export matching - ## METHOD RE-EXPORT @@ -162,7 +159,7 @@ export models, localmodels, @load, @loadcode, load, info, FeatureSelector, UnivariateStandardizer, # builtins/Transformers.jl Standardizer, UnivariateBoxCoxTransformer, OneHotEncoder, ContinuousEncoder, UnivariateDiscretizer, - FillImputer + FillImputer, matching # re-export from ComputaionalResources: export CPU1, CPUProcesses, CPUThreads @@ -205,7 +202,6 @@ const srcdir = dirname(@__FILE__) include("version.jl") # defines MLJ_VERSION constant include("ensembles.jl") # homogeneous ensembles -include("model_matching.jl")# inferring model search criterion from data include("scitypes.jl") # extensions to ScientificTypes.scitype end # module diff --git a/src/model_matching.jl b/src/model_matching.jl deleted file mode 100644 index 272c0ad08..000000000 --- a/src/model_matching.jl +++ /dev/null @@ -1,71 +0,0 @@ -# Note. `ModelProxy` is the type of a model's metadata entry (a named -# tuple). So, `info("PCA")` has this type, for example. - - -## BASIC IDEA - -if false - - matching(model::MLJModels.ModelProxy, X) = - !(model.is_supervised) && scitype(X) <: model.input_scitype - - matching(model::MLJModels.ModelProxy, X, y) = - model.is_supervised && - scitype(X) <: model.input_scitype && - scitype(y) <: model.target_scitype - - matching(model::MLJModels.ModelProxy, X, y, w::AbstractVector{<:Real}) = - model.is_supervised && - model.supports_weights && - scitype(X) <: model.input_scitype && - scitype(y) <: model.target_scitype - -end - - -## IMPLEMENTATION - -matching(X) = Checker{false,false,scitype(X),missing}() -matching(X, y) = Checker{true,false,scitype(X),scitype(y)}() -matching(X, y, w) = Checker{true,true,scitype(X),scitype(y)}() - -(f::Checker{false,false,XS,missing})(model::MLJModels.ModelProxy) where XS = - !(model.is_supervised) && - XS <: model.input_scitype - -(f::Checker{true,false,XS,yS})(model::MLJModels.ModelProxy) where {XS,yS} = - model.is_supervised && - XS <: model.input_scitype && - yS <: model.target_scitype - -(f::Checker{true,true,XS,yS})(model::MLJModels.ModelProxy) where {XS,yS} = - model.is_supervised && - model.supports_weights && - XS <: model.input_scitype && - yS <: model.target_scitype - -(f::Checker)(name::String; pkg=nothing) = f(info(name, pkg=pkg)) -(f::Checker)(realmodel::Model) = f(info(realmodel)) - -matching(model::MLJModels.ModelProxy, args...) = matching(args...)(model) -matching(name::String, args...; pkg=nothing) = - matching(info(name, pkg=pkg), args...) -matching(realmodel::Model, args...) = matching(info(realmodel), args...) - - -## DUAL NOTION - -struct DataChecker - model::MLJModels.ModelProxy -end - -matching(model::MLJModels.ModelProxy) = DataChecker(model) -matching(name::String; pkg=nothing) = DataChecker(info(name, pkg=pkg)) -matching(realmodel::Model) = matching(info(realmodel)) - -(f::DataChecker)(args...) = matching(f.model, args...) - - - - - diff --git a/src/scitypes.jl b/src/scitypes.jl index 5f81b3770..314ce510d 100644 --- a/src/scitypes.jl +++ b/src/scitypes.jl @@ -1,5 +1,7 @@ ## SUPERVISED +# This implementation of scitype for models and measures is highly experimental + const MST = MLJScientificTypes # only used in this file struct SupervisedScitype{input_scitype, target_scitype, prediction_type} end diff --git a/test/model_matching.jl b/test/model_matching.jl deleted file mode 100644 index d296347a8..000000000 --- a/test/model_matching.jl +++ /dev/null @@ -1,30 +0,0 @@ -module TestModelMatching - -using MLJ -using Test - -X = (a = rand(5), b = categorical(1:5)) -y = rand(5) -w = rand(5) - -@test matching(X) == MLJ.Checker{false,false,scitype(X),missing}() -@test matching(X, y) == MLJ.Checker{true,false,scitype(X),scitype(y)}() -@test matching(X, y, w) == MLJ.Checker{true,true,scitype(X),scitype(y)}() - -@test !matching("RidgeRegressor", pkg="MultivariateStats", X) -@test matching("FeatureSelector", X) - -m1 = models(matching(X)) -@test issubset([info("FeatureSelector"), - info("OneHotEncoder"), - info("Standardizer")], m1) - -@test !("PCA" in m1) -@test !(info("PCA") in m1) - -m2 = models(matching(X, y)) -@test info("ConstantRegressor") in m2 -@test !(info("DecisionTreeRegressor") in m2) - -end -true diff --git a/test/runtests.jl b/test/runtests.jl index b6419ad71..5caf800f7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -17,10 +17,6 @@ end # @test include("ensembles.jl") # end -@testset "matching models to data" begin - @test include("model_matching.jl") -end - @testset "scitypes" begin @test include("scitypes.jl") end diff --git a/test/scitypes.jl b/test/scitypes.jl index 201d3d93a..15f29bd5e 100644 --- a/test/scitypes.jl +++ b/test/scitypes.jl @@ -13,12 +13,13 @@ U = scitype(FeatureSelector()) # M = scitype(rms) # @test_broken M().prediction_type == :deterministic -for handle in localmodels() - name = Symbol(handle.name) - eval(quote - scitype(($name)()) - end) -end +@test scitype(OneHotEncoder()) == + MLJ.UnsupervisedScitype{Table,Table} + +@test scitype(ConstantRegressor()) == + MLJ.SupervisedScitype{Table, + AbstractVector{Continuous}, + :probabilistic} end true From a00c4dbc2942735b989d5be823f5aa396fedcecf Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 3 Feb 2021 12:33:31 +0100 Subject: [PATCH 10/20] use `add` instead of `develop` when recommending old OpenSpecFun_jll --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf43f30ff..8b02f8cce 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ appropriate version, activate your MLJ environment and run ```julia using Pkg; - Pkg.develop(PackageSpec(url="https://github.com/tlienart/OpenSpecFun_jll.jl")) + Pkg.add(PackageSpec(url="https://github.com/tlienart/OpenSpecFun_jll.jl")) ``` #### Serialization for composite models with component models with custom serialization From 8862d08d09fb4daf140ac6ad4c17053fa1dac373 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Thu, 4 Feb 2021 11:24:17 +1300 Subject: [PATCH 11/20] re-export @iload --- src/MLJ.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MLJ.jl b/src/MLJ.jl index b22ce8410..765e284bd 100644 --- a/src/MLJ.jl +++ b/src/MLJ.jl @@ -154,7 +154,7 @@ export Grid, RandomSearch, Explicit, TunedModel, LatinHypercube, learning_curve!, learning_curve # re-export from MLJModels: -export models, localmodels, @load, @loadcode, load, info, +export models, localmodels, @load, @iload, load, info, ConstantRegressor, ConstantClassifier, # builtins/Constant.jl FeatureSelector, UnivariateStandardizer, # builtins/Transformers.jl Standardizer, UnivariateBoxCoxTransformer, From be666328b6cf6e4069d34feabad92ee76f5f59f5 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Thu, 4 Feb 2021 11:24:32 +1300 Subject: [PATCH 12/20] fix @load in docs --- docs/src/common_mlj_workflows.md | 173 +++++++++--------- docs/src/composing_models.md | 161 +++++++--------- docs/src/evaluating_model_performance.md | 2 +- docs/src/getting_started.md | 71 ++++--- docs/src/img/learning_curve_n.png | Bin 48433 -> 33942 bytes docs/src/learning_curves.md | 17 +- docs/src/machines.md | 19 +- docs/src/mlj_cheatsheet.md | 10 +- .../src/quick_start_guide_to_adding_models.md | 17 +- docs/src/transformers.md | 38 ++-- docs/src/tuning_models.md | 8 +- 11 files changed, 268 insertions(+), 248 deletions(-) diff --git a/docs/src/common_mlj_workflows.md b/docs/src/common_mlj_workflows.md index 43c0b1bcf..08054c203 100644 --- a/docs/src/common_mlj_workflows.md +++ b/docs/src/common_mlj_workflows.md @@ -87,16 +87,16 @@ info("RidgeRegressor", pkg="MultivariateStats") # a model type in multiple packa *Reference:* [Getting Started](index.md) ```@example workflows -@load DecisionTreeClassifier -model = DecisionTreeClassifier(min_samples_split=5, max_depth=4) +Tree = @load DecisionTreeClassifier +tree = Tree(min_samples_split=5, max_depth=4) ``` or ```@julia -model = @load DecisionTreeClassifier -model.min_samples_split = 5 -model.max_depth = 4 +tree = (@load DecisionTreeClassifier)() +tree.min_samples_split = 5 +tree.max_depth = 4 ``` ## Evaluating a model @@ -106,10 +106,16 @@ model.max_depth = 4 ```@example workflows X, y = @load_boston -model = @load KNNRegressor -evaluate(model, X, y, resampling=CV(nfolds=5), measure=[rms, mav]) +KNN = @load KNNRegressor +knn = KNN() +evaluate(knn, X, y, resampling=CV(nfolds=5), measure=[RootMeanSquaredError(), MeanAbsoluteError()]) ``` +Note `RootMeanSquaredError()` has alias `rms` and `MeanAbsoluteError()` has alias `mae`. + +Do `measures()` to list all losses and scores and their aliases. + + ## Basic fit/evaluate/predict by hand: *Reference:* [Getting Started](index.md), [Machines](machines.md), @@ -124,15 +130,15 @@ first(vaso, 3) ```@example workflows y, X = unpack(vaso, ==(:Y), c -> true; :Y => Multiclass) -tree_model = @load DecisionTreeClassifier -tree_model.max_depth=2; nothing # hide +Tree = @load DecisionTreeClassifier +tree = Tree(max_depth=2) # hide ``` Bind the model and data together in a *machine* , which will additionally store the learned parameters (*fitresults*) when fit: ```@example workflows -tree = machine(tree_model, X, y) +mach = machine(tree, X, y) ``` Split row indices into training and evaluation rows: @@ -144,49 +150,49 @@ train, test = partition(eachindex(y), 0.7, shuffle=true, rng=1234); # 70:30 spli Fit on train and evaluate on test: ```@example workflows -fit!(tree, rows=train) -yhat = predict(tree, X[test,:]) -mean(cross_entropy(yhat, y[test])) +fit!(mach, rows=train) +yhat = predict(mach, X[test,:]) +mean(LogLoss(tol=1e-4)(yhat, y[test])) ``` +Note `LogLoss()` has aliases `log_loss` and `cross_entropy`. + +Run `measures()` to list all losses and scores and their aliases ("instances"). + Predict on new data: ```@example workflows Xnew = (Volume=3*rand(3), Rate=3*rand(3)) -predict(tree, Xnew) # a vector of distributions +predict(mach, Xnew) # a vector of distributions ``` ```@example workflows -predict_mode(tree, Xnew) # a vector of point-predictions +predict_mode(mach, Xnew) # a vector of point-predictions ``` ## More performance evaluation examples -```@example workflows -import LossFunctions.ZeroOneLoss -``` - Evaluating model + data directly: ```@example workflows -evaluate(tree_model, X, y, +evaluate(tree, X, y, resampling=Holdout(fraction_train=0.7, shuffle=true, rng=1234), - measure=[cross_entropy, ZeroOneLoss()]) + measure=[LogLoss(), ZeroOneLoss()]) ``` If a machine is already defined, as above: ```@example workflows -evaluate!(tree, +evaluate!(mach, resampling=Holdout(fraction_train=0.7, shuffle=true, rng=1234), - measure=[cross_entropy, ZeroOneLoss()]) + measure=[LogLoss(), ZeroOneLoss()]) ``` Using cross-validation: ```@example workflows -evaluate!(tree, resampling=CV(nfolds=5, shuffle=true, rng=1234), - measure=[cross_entropy, ZeroOneLoss()]) +evaluate!(mach, resampling=CV(nfolds=5, shuffle=true, rng=1234), + measure=[LogLoss(), ZeroOneLoss()]) ``` With user-specified train/test pairs of row indices: @@ -194,18 +200,18 @@ With user-specified train/test pairs of row indices: ```@example workflows f1, f2, f3 = 1:13, 14:26, 27:36 pairs = [(f1, vcat(f2, f3)), (f2, vcat(f3, f1)), (f3, vcat(f1, f2))]; -evaluate!(tree, +evaluate!(mach, resampling=pairs, - measure=[cross_entropy, ZeroOneLoss()]) + measure=[LogLoss(), ZeroOneLoss()]) ``` Changing a hyperparameter and re-evaluating: ```@example workflows -tree_model.max_depth = 3 -evaluate!(tree, +tree.max_depth = 3 +evaluate!(mach, resampling=CV(nfolds=5, shuffle=true, rng=1234), - measure=[cross_entropy, ZeroOneLoss()]) + measure=[LogLoss(), ZeroOneLoss()]) ``` ## Inspecting training results @@ -219,22 +225,22 @@ x2 = rand(100) X = (x1=x1, x2=x2) y = x1 - 2x2 + 0.1*rand(100); -ols_model = @load LinearRegressor pkg=GLM -ols = machine(ols_model, X, y) -fit!(ols) +OLS = @load LinearRegressor pkg=GLM +ols = OLS() +mach = machine(ols, X, y) |> fit! ``` Get a named tuple representing the learned parameters, human-readable if appropriate: ```@example workflows -fitted_params(ols) +fitted_params(mach) ``` Get other training-related information: ```@example workflows -report(ols) +report(mach) ``` ## Basic fit/transform for unsupervised models @@ -249,16 +255,16 @@ train, test = partition(eachindex(y), 0.97, shuffle=true, rng=123) Instantiate and fit the model/machine: ```@example workflows -@load PCA -pca_model = PCA(maxoutdim=2) -pca = machine(pca_model, X) -fit!(pca, rows=train) +PCA = @load PCA +pca = PCA(maxoutdim=2) +mach = machine(pca, X) +fit!(mach, rows=train) ``` Transform selected data bound to the machine: ```@example workflows -transform(pca, rows=test); +transform(mach, rows=test); ``` Transform new data: @@ -266,18 +272,18 @@ Transform new data: ```@example workflows Xnew = (sepal_length=rand(3), sepal_width=rand(3), petal_length=rand(3), petal_width=rand(3)); -transform(pca, Xnew) +transform(mach, Xnew) ``` ## Inverting learned transformations ```@example workflows y = rand(100); -stand_model = UnivariateStandardizer() -stand = machine(stand_model, y) -fit!(stand) -z = transform(stand, y); -@assert inverse_transform(stand, z) ≈ y # true +stand = Standardizer() +mach = machine(stand, y) +fit!(mach) +z = transform(mach, y); +@assert inverse_transform(mach, z) ≈ y # true ``` ## Nested hyperparameter tuning @@ -291,40 +297,35 @@ X, y = @load_iris; nothing # hide Define a model with nested hyperparameters: ```@example workflows -tree_model = @load DecisionTreeClassifier -forest_model = EnsembleModel(atom=tree_model, n=300) -``` - -Inspect all hyperparameters, even nested ones (returns nested named tuple): - -```@example workflows -params(forest_model) +Tree = @load DecisionTreeClassifier +tree = Tree() +forest = EnsembleModel(atom=tree, n=300) ``` Define ranges for hyperparameters to be tuned: ```@example workflows -r1 = range(forest_model, :bagging_fraction, lower=0.5, upper=1.0, scale=:log10) +r1 = range(forest, :bagging_fraction, lower=0.5, upper=1.0, scale=:log10) ``` ```@example workflows -r2 = range(forest_model, :(atom.n_subfeatures), lower=1, upper=4) # nested +r2 = range(forest, :(atom.n_subfeatures), lower=1, upper=4) # nested ``` Wrap the model in a tuning strategy: ```@example workflows -tuned_forest = TunedModel(model=forest_model, +tuned_forest = TunedModel(model=forest, tuning=Grid(resolution=12), resampling=CV(nfolds=6), ranges=[r1, r2], - measure=cross_entropy) + measure=BrierScore()) ``` Bound the wrapped model to data: ```@example workflows -tuned = machine(tuned_forest, X, y) +mach = machine(tuned_forest, X, y) ``` Fitting the resultant machine optimizes the hyperparameters specified @@ -333,13 +334,13 @@ and performance `measure` (possibly a vector of measures), and retrains on all data bound to the machine: ```@example workflows -fit!(tuned) +fit!(mach) ``` Inspecting the optimal model: ```@example workflows -F = fitted_params(tuned) +F = fitted_params(mach) ``` ```@example workflows @@ -349,14 +350,19 @@ F.best_model Inspecting details of tuning procedure: ```@example workflows -report(tuned) +r = report(mach); +keys(r) +``` + +```@example workflows +r.history[[1,end]] ``` Visualizing these results: ```julia using Plots -plot(tuned) +plot(mach) ``` ![](img/workflows_tuning_plot.png) @@ -364,7 +370,7 @@ plot(tuned) Predicting on new data using the optimized model: ```@example workflows -predict(tuned, Xnew) +predict(mach, Xnew) ``` ## Constructing a linear pipeline @@ -376,11 +382,11 @@ transformation/inverse transformation: ```@example workflows X, y = @load_reduced_ames -@load KNNRegressor +KNN = @load KNNRegressor pipe = @pipeline(X -> coerce(X, :age=>Continuous), OneHotEncoder, - KNNRegressor(K=3), - target = UnivariateStandardizer) + KNN(K=3), + target = Standardizer) ``` Evaluating the pipeline (just as you would any other model): @@ -388,7 +394,7 @@ Evaluating the pipeline (just as you would any other model): ```@example workflows pipe.knn_regressor.K = 2 pipe.one_hot_encoder.drop_last = true -evaluate(pipe, X, y, resampling=Holdout(), measure=rms, verbosity=2) +evaluate(pipe, X, y, resampling=Holdout(), measure=RootMeanSquaredError(), verbosity=2) ``` Inspecting the learned parameters in a pipeline: @@ -403,10 +409,10 @@ Constructing a linear (unbranching) pipeline with a *static* (unlearned) target transformation/inverse transformation: ```@example workflows -@load DecisionTreeRegressor +Tree = @load DecisionTreeRegressor pipe2 = @pipeline(X -> coerce(X, :age=>Continuous), OneHotEncoder, - DecisionTreeRegressor(max_depth=4), + Tree(max_depth=4), target = y -> log.(y), inverse = z -> exp.(z)) ``` @@ -417,10 +423,11 @@ pipe2 = @pipeline(X -> coerce(X, :age=>Continuous), ```@example workflows X, y = @load_iris -tree_model = @load DecisionTreeClassifier -forest_model = EnsembleModel(atom=tree_model, bagging_fraction=0.8, n=300) -forest = machine(forest_model, X, y) -evaluate!(forest, measure=cross_entropy) +Tree = @load DecisionTreeClassifier +tree = Tree() +forest = EnsembleModel(atom=tree_model, bagging_fraction=0.8, n=300) +mach = machine(forest, X, y) +evaluate!(mach, measure=LogLoss()) ``` ## Performance curves @@ -431,13 +438,13 @@ Generate a plot of performance, as a function of some hyperparameter Single performance curve: ```@example workflows -r = range(forest_model, :n, lower=1, upper=1000, scale=:log10) -curve = learning_curve(forest, - range=r, - resampling=Holdout(), - resolution=50, - measure=cross_entropy, - verbosity=0) +r = range(forest, :n, lower=1, upper=1000, scale=:log10) +curve = learning_curve(mach, + range=r, + resampling=Holdout(), + resolution=50, + measure=LogLoss(), + verbosity=0) ``` ```julia @@ -450,10 +457,10 @@ plot(curve.parameter_values, curve.measurements, xlab=curve.parameter_name, xsca Multiple curves: ```@example workflows -curve = learning_curve(forest, +curve = learning_curve(mach, range=r, resampling=Holdout(), - measure=cross_entropy, + measure=LogLoss(), resolution=50, rng_name=:rng, rngs=4, diff --git a/docs/src/composing_models.md b/docs/src/composing_models.md index b0dc4883a..9dea1e98e 100644 --- a/docs/src/composing_models.md +++ b/docs/src/composing_models.md @@ -9,9 +9,18 @@ composite model types, that behave like any other model type. The main novelty of composite models is that they include other models as hyper-parameters. -That said, MLJ also provides dedicated syntax for the most common -composition use-cases, which are described first below. A description -of the general framework begins at [Learning Networks](@ref). +MLJ also provides dedicated syntax for the most common +composition use-cases, which are described first below. + +A description of the general framework begins at [Learning +Networks](@ref). For an in-depth high-level description of learning +networks, refer to the article linked below. + + + Anthony D. Blaom and Sebastian J. Voller (2020): Flexible model composition in machine learning and its implementation in MLJ Preprint, arXiv:2012.15505 + + ## Linear pipelines @@ -53,12 +62,13 @@ type called `pipe`, for performing the following operations: ``` ```julia> +KNNRegressor = @load KNNRegressor pipe = @pipeline(X -> coerce(X, :age=>Continuous), OneHotEncoder, KNNRegressor(K=3), - target = UnivariateStandardizer()) + target = Standardizer()) -Pipeline406( +Pipeline326( one_hot_encoder = OneHotEncoder( features = Symbol[], drop_last = false, @@ -71,8 +81,11 @@ Pipeline406( leafsize = 10, reorder = true, weights = :uniform), - target = UnivariateStandardizer()) @719 - + target = Standardizer( + features = Symbol[], + ignore = false, + ordered_factor = false, + count = false)) @287 ``` Notice that field names for the composite are automatically generated @@ -120,6 +133,12 @@ details. ## Learning Networks +Below is a practical guide to the MLJ implementantion of learning +networks, which have been described more abstractly in the article +[Anthony D. Blaom and Sebastian J. Voller (2020): Flexible model +composition in machine learning and its implementation in MLJ. +Preprint, arXiv:2012.15505](https://arxiv.org/abs/2012.15505). + Hand-crafting a learning network, as outlined below, is a relatively advanced MLJ feature, assuming familiarity with the basics outlined in [Getting Started](index.md). The syntax for building a learning @@ -179,7 +198,7 @@ this regressor can be changed to a different one (e.g., For testing purposes, we'll use a small synthetic data set: -```julia +```@example 7 using Statistics import DataFrames @@ -193,7 +212,7 @@ train, test = partition(eachindex(y), 0.8) ``` Step one is to wrap the data in *source nodes*: -``` +```@example 7 Xs = source(X) ys = source(y) ``` @@ -208,11 +227,8 @@ or call network nodes, as illustrated below. The contents of a source node can be recovered by simply calling the node with no arguments: -```julia -julia> ys()[1:2] -2-element Array{Float64,1}: - 0.12350299813414874 - 0.29425920370829295 +```@example 7 +ys()[1:2] ``` We label the nodes that we will define according to their outputs in @@ -222,7 +238,7 @@ machine, namely `box`, for different operations. To construct the `W` node we first need to define the machine `stand` that it will use to transform inputs. -```julia +```@example 7 stand_model = Standardizer() stand = machine(stand_model, Xs) ``` @@ -230,15 +246,8 @@ Because `Xs` is a node, instead of concrete data, we can call `transform` on the machine without first training it, and the result is the new node `W`, instead of concrete transformed data: -```julia -julia> W = transform(stand, Xs) -Node{Machine{Standardizer}} @325 - args: - 1: Source @085 - formula: - transform( - Machine{Standardizer} @709, - Source @085) +```@example 7 +W = transform(stand, Xs) ``` To get actual transformed data we *call* the node appropriately, which @@ -246,22 +255,13 @@ will require we first train the node. Training a node, rather than a machine, triggers training of *all* necessary machines in the network. -```julia +```@example 7 fit!(W, rows=train) W() # transform all data W(rows=test ) # transform only test data W(X[3:4,:]) # transform any data, new or old ``` -```julia -2×3 DataFrame -│ Row │ x1 │ x2 │ x3 │ -│ │ Float64 │ Float64 │ Float64 │ -├─────┼──────────┼───────────┼───────────┤ -│ 1 │ 0.113486 │ 0.732189 │ 1.4783 │ -│ 2 │ 0.783227 │ -0.425371 │ -0.113503 │ -``` - If you like, you can think of `W` (and the other nodes we will define) as "dynamic data": `W` is *data*, in the sense that it an be called ("indexed") on rows, but *dynamic*, in the sense the result depends on @@ -269,9 +269,8 @@ the outcome of training events. The other nodes of our network are defined similarly: -```julia -@load RidgeRegressor pkg=MultivariateStats - +```@example 7 +RidgeRegressor = @load RidgeRegressor pkg=MultivariateStats box_model = UnivariateBoxCoxTransformer() # for making data look normally-distributed box = machine(box_model, ys) z = transform(box, ys) @@ -280,51 +279,27 @@ ridge_model = RidgeRegressor(lambda=0.1) ridge =machine(ridge_model, W, z) zhat = predict(ridge, W) -yhat = inverse_transform(box, zhat) - +yhat = inverse_transform(box, zhat); ``` We are ready to train and evaluate the completed network. Notice that the standardizer, `stand`, is *not* retrained, as MLJ remembers that it was trained earlier: -```julia -fit!(yhat, rows=train) -``` - -```julia -[ Info: Not retraining Machine{Standardizer} @ 6…82. It is up-to-date. -[ Info: Training Machine{UnivariateBoxCoxTransformer} @ 1…09. -[ Info: Training Machine{RidgeRegressor} @ 2…66. -``` - -```julia +```@example 7 +fit!(yhat, rows=train); rms(y[test], yhat(rows=test)) # evaluate ``` - -```julia -0.022837595088079567 -``` - We can change a hyperparameters and retrain: -```julia +```@example 7 ridge_model.lambda = 0.01 -fit!(yhat, rows=train) -``` - -```julia -[ Info: Not retraining Machine{UnivariateBoxCoxTransformer} @ 1…09. It is up-to-date. -[ Info: Not retraining Machine{Standardizer} @ 6…82. It is up-to-date. -[ Info: Updating Machine{RidgeRegressor} @ 2…66. -Node @ 1…07 = inverse_transform(1…09, predict(2…66, transform(6…82, 3…40))) +fit!(yhat, rows=train); ``` - And re-evaluate: -```julia +```@example 7 rms(y[test], yhat(rows=test)) -0.039410306910269116 ``` > **Notable feature.** The machine, `ridge::Machine{RidgeRegressor}`, is retrained, because its underlying model has been mutated. However, since the outcome of this training has no effect on the training inputs of the machines `stand` and `box`, these transformers are left untouched. (During construction, each node and machine in a learning network determines and records all machines on which it depends.) This behavior, which extends to exported learning networks, means we can tune our wrapped regressor (using a holdout set) without re-computing transformations each time the hyperparameter is changed. @@ -345,11 +320,9 @@ supertype (`Deterministic`, `Probabilistic`, `Unsupervised` or Continuing with the example above: -```julia -julia> surrogate = Deterministic() -DeterministicSurrogate() @047 - -mach = machine(surrogate, Xs, ys; predict=yhat) +```@example 7 +surrogate = Deterministic() +mach = machine(surrogate, Xs, ys; predict=yhat); ``` Notice that a key-word argument declares which node is for making @@ -357,23 +330,24 @@ predictions, and the arguments `Xs` and `ys` declare which source nodes receive the input and target data. With `mach` constructed in this way, the code -```julia +```@example 7 fit!(mach) -predict(mach, X[test,:]) +predict(mach, X[test,:]); ``` is equivalent to -```julia +```@example 7 fit!(yhat) -yhat(X[test,:]) +yhat(X[test,:]); ``` While it's main purpose is for export (see below), this machine can actually be evaluated: -```julia -evaluate!(mach, resampling=CV(nfolds=3), measure=l2) +```@example 7 + +evaluate!(mach, resampling=CV(nfolds=3), measure=LPLoss(p=2)) ``` For more on constructing learning network machines, see @@ -417,7 +391,8 @@ end ``` -We can now create an instance of this type and apply the meta-algorithms that apply to any MLJ model: +We can now create an instance of this type and apply the +meta-algorithms that apply to any MLJ model: ```julia julia> composite = WrappedRegressor() @@ -433,7 +408,7 @@ Since our new type is mutable, we can swap the `RidgeRegressor` out for any other regressor: ``` -@load KNNRegressor +KNNRegressor = @load KNNRegressor composite.regressor = KNNRegressor(K=7) julia> composite WrappedRegressor(regressor = KNNRegressor(K = 7, @@ -521,11 +496,11 @@ Notes: calls `fit!` on the learning network machine `mach` and splits it into various pieces, as required by the MLJ model interface. See also the [`return!`](@ref) doc-string. - + - **Important note** An MLJ `fit` method is not allowed to mutate its - `model` argument. + `model` argument. -> **What's going on here?** MLJ's machine interface is built atop a more primitive *[model](simple_user_defined_models.md)* interface, implemented for each algorithm. Each supervised model type (eg, `RidgeRegressor`) requires model `fit` and `predict` methods, which are called by the corresponding *machine* `fit!` and `predict` methods. We don't need to define a model `predict` method here because MLJ provides a fallback which simply calls the `predict` on the learning network machine created in the `fit` method. +> **What's going on here?** MLJ's machine interface is built atop a more primitive *[model](simple_user_defined_models.md)* interface, implemented for each algorithm. Each supervised model type (eg, `RidgeRegressor`) requires model `fit` and `predict` methods, which are called by the corresponding *machine* `fit!` and `predict` methods. We don't need to define a model `predict` method here because MLJ provides a fallback which simply calls the `predict` on the learning network machine created in the `fit` method. #### A composite model coupling component model hyper-parameters @@ -538,8 +513,8 @@ depends on the number of clusters used (with less regularization for a greater number of clusters) and a user-specified "coupling" coefficient `K`. -```julia -@load RidgeRegressor pkg=MLJLinearModels +```@example 7 +RidgeRegressor = @load RidgeRegressor pkg=MLJLinearModels mutable struct MyComposite <: DeterministicComposite clusterer # the clustering model (e.g., KMeans()) @@ -571,8 +546,15 @@ function MLJ.fit(composite::Composite, verbosity, X, y) return!(mach, composite, verbosity) end + +kmeans = (@load KMeans pkg=Clustering)() +my_composite = MyComposite(kmeans, nothing, 0.5) ``` +```@example 7 +evaluate(my_composite, X, y, measure=MeanAbsoluteError(), verbosity=0) +``` + ## Static operations on nodes Continuing to view nodes as "dynamic data", we can, in addition to @@ -604,9 +586,9 @@ transforms (exponentiates) the blended predictions. Note, in particular, the lines defining `zhat` and `yhat`, which combine several static node operations. -```julia -@load RidgeRegressor pkg=MultivariateStats -@load KNNRegressor +```@example 7 +RidgeRegressor = @load RidgeRegressor pkg=MultivariateStats +KNNRegressor = @load KNNRegressor Xs = source() ys = source() @@ -755,5 +737,4 @@ node return! ``` -See more on fitting nodes at [`fit!`](@ref) and [`fit_only!`](@ref). - +See more on fitting nodes at [`fit!`](@ref) and [`fit_only!`](@ref). diff --git a/docs/src/evaluating_model_performance.md b/docs/src/evaluating_model_performance.md index bc89bb5a5..c8c2f5f34 100644 --- a/docs/src/evaluating_model_performance.md +++ b/docs/src/evaluating_model_performance.md @@ -23,7 +23,7 @@ MLJ.color_off() using MLJ X = (a=rand(12), b=rand(12), c=rand(12)); y = X.a + 2X.b + 0.05*rand(12); -model = @load RidgeRegressor pkg=MultivariateStats +model = (@load RidgeRegressor pkg=MultivariateStats verbosity=0)() cv=CV(nfolds=3) evaluate(model, X, y, resampling=cv, measure=l2, verbosity=0) ``` diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md index b96ad393d..1cfe34626 100644 --- a/docs/src/getting_started.md +++ b/docs/src/getting_started.md @@ -47,24 +47,32 @@ In MLJ a *model* is a struct storing the hyperparameters of the learning algorithm indicated by the struct name (and nothing else). Assuming the MLJDecisionTreeInterface.jl package is in your load path -(see [Installation](@ref)) we can use `@load` to load the code -defining the `DecisionTreeClassifier` model type. This macro also -returns an instance, with default hyperparameters. +(see [Installation](@ref)) we can use `@load` to import the +`DecisionTreeClassifier` model type, which we will be bind to `Tree`: ```@repl doda -tree_model = @load DecisionTreeClassifier +Tree = @load DecisionTreeClassifier ``` -*Important:* DecisionTree.jl and most other packages implementing machine -learning algorithms for use in MLJ are not MLJ dependencies. If such a -package is not in your load path you will receive an error explaining -how to add the package to your current environment. +Now we can instantiate a model with default hyperparameters: -Once loaded, a model's performance can be evaluated with the +```@repl doda +tree = Tree() +``` + +*Important:* DecisionTree.jl and most other packages implementing +machine learning algorithms for use in MLJ are not MLJ +dependencies. If such a package is not in your load path you will +receive an error explaining how to add the package to your current +environment. Alternatively, you can use the interactive version of +`@iload`. For more on importing model types, see [`Loading Model +Code`](@ref). + +Once instantiated, a model's performance can be evaluated with the `evaluate` method: ```@repl doda -evaluate(tree_model, X, y, +evaluate(tree, X, y, resampling=CV(shuffle=true), measure=cross_entropy, verbosity=0) ``` @@ -131,19 +139,24 @@ Wrapping the model in data creates a *machine* which will store training outcomes: ```@repl doda -tree = machine(tree_model, X, y) +mach = machine(tree, X, y) ``` Training and testing on a hold-out set: ```@repl doda train, test = partition(eachindex(y), 0.7, shuffle=true); # 70:30 split -fit!(tree, rows=train); -yhat = predict(tree, X[test,:]); +fit!(mach, rows=train); +yhat = predict(mach, X[test,:]); yhat[3:5] -cross_entropy(yhat, y[test]) |> mean +log_loss(yhat, y[test]) |> mean ``` +Here `log_loss` (and `cross_entropy`) is an alias for `LogLoss()` or, +more precisely, a built-in instance of the `LogLoss` type. Another +instance is `LogLoss(tol=0.0001)`. For a list of all losses and +scores, and their aliases, run `measures()`. + Notice that `yhat` is a vector of `Distribution` objects (because DecisionTreeClassifier makes probabilistic predictions). The methods of the [Distributions](https://github.com/JuliaStats/Distributions.jl) @@ -159,7 +172,7 @@ Or, one can explicitly get modes by using `predict_mode` instead of `predict`: ```@repl doda -predict_mode(tree, X[test[3:5],:]) +predict_mode(mach, X[test[3:5],:]) ``` Finally, we note that `pdf()` is overloaded to allow the retrieval of @@ -175,11 +188,11 @@ and may optionally implement an `inverse_transform` method: ```@repl doda v = [1, 2, 3, 4] -stand_model = UnivariateStandardizer() -stand = machine(stand_model, v) -fit!(stand) -w = transform(stand, v) -inverse_transform(stand, w) +stand = UnivariateStandardizer() # this type is built-in +mach = machine(stand, v) +fit!(mach) +w = transform(mach, v) +inverse_transform(mach, w) ``` [Machines](machines.md) have an internal state which allows them to @@ -195,16 +208,16 @@ added to the method name because machines are generally mutated when trained): ```@repl doda -evaluate!(tree, resampling=Holdout(fraction_train=0.7, shuffle=true), - measures=[cross_entropy, BrierScore()], +evaluate!(mach, resampling=Holdout(fraction_train=0.7, shuffle=true), + measures=[log_loss, brier_score], verbosity=0) ``` Changing a hyperparameter and re-evaluating: ```@repl doda tree_model.max_depth = 3 -evaluate!(tree, resampling=Holdout(fraction_train=0.7, shuffle=true), - measures=[cross_entropy, BrierScore()], +evaluate!(mach, resampling=Holdout(fraction_train=0.7, shuffle=true), + measures=[cross_entropy, brier_score], verbosity=0) ``` @@ -231,9 +244,8 @@ about the form of data expected by MLJ, as outlined below. The basic machines](@ref)): ``` -machine(model::Supervised, X, y) +machine(model::Supervised, X, y) machine(model::Unsupervised, X) - ``` Each supervised model in MLJ declares the permitted *scientific type* @@ -258,13 +270,14 @@ scientfic type; see [MLJScientificTypes.jl](https://alan-turing-institute.github.io/MLJScientificTypes.jl/dev/) or run `?coerce` for details. -Additionally, most data containers - such as tuples, -vectors, matrices and tables - have a scientific type. +Additionally, most data containers - such as tuples, vectors, matrices +and tables - have a scientific type. ![](img/scitypes.png) -*Figure 1. Part of the scientific type hierarchy in* [ScientificTypes.jl](https://alan-turing-institute.github.io/MLJScientificTypes.jl/dev/). +*Figure 1. Part of the scientific type hierarchy in* +[ScientificTypes.jl](https://alan-turing-institute.github.io/MLJScientificTypes.jl/dev/). ```@repl doda scitype(4.6) diff --git a/docs/src/img/learning_curve_n.png b/docs/src/img/learning_curve_n.png index c6c879a620d40d0196215b5344965d24f2bea351..c525ecb0175ba490e0db0e7072ca5c524f09cdbd 100644 GIT binary patch literal 33942 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 literal 48433 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 PhCuGzR8h#2GkWnKlHLE? diff --git a/docs/src/learning_curves.md b/docs/src/learning_curves.md index 3253ba7ff..dd35f93ff 100644 --- a/docs/src/learning_curves.md +++ b/docs/src/learning_curves.md @@ -12,18 +12,22 @@ instantiating a machine. You can choose to supply all available data, as performance estimates are computed using a resampling strategy, defaulting to `Holdout(fraction_train=0.7)`. -```julia +```@example hooking +using MLJ X, y = @load_boston; -atom = @load RidgeRegressor pkg=MultivariateStats +atom = (@load RidgeRegressor pkg=MLJLinearModels)() ensemble = EnsembleModel(atom=atom, n=1000) mach = machine(ensemble, X, y) -r_lambda = range(ensemble, :(atom.lambda), lower=10, upper=500, scale=:log10) +r_lambda = range(ensemble, :(atom.lambda), lower=1e-1, upper=100, scale=:log10) curve = MLJ.learning_curve(mach; range=r_lambda, resampling=CV(nfolds=3), - measure=mav) + measure=MeanAbsoluteError()) +``` + +```julia using Plots plot(curve.parameter_values, curve.measurements, @@ -44,11 +48,12 @@ generator, `rng_name`, and specify the random number generators to be used using `rngs=...` (an integer automatically generates the number specified): -```julia -atom.lambda=200 +```@example hooking +atom.lambda= 7.3 r_n = range(ensemble, :n, lower=1, upper=50) curves = MLJ.learning_curve(mach; range=r_n, + measure=MeanAbsoluteError(), verbosity=0, rng_name=:rng, rngs=4) diff --git a/docs/src/machines.md b/docs/src/machines.md index 2b3138e42..cd0293b50 100644 --- a/docs/src/machines.md +++ b/docs/src/machines.md @@ -16,7 +16,8 @@ mutations (eg, increasing the number of epochs in a neural network). ```@example machines using MLJ; color_off() # hide -forest = EnsembleModel(atom=(@load DecisionTreeClassifier), n=10); +tree = (@load DecisionTreeClassifier verbosity=0)() +forest = EnsembleModel(atom=tree, n=10); X, y = @load_iris; mach = machine(forest, X, y) fit!(mach, verbosity=2); @@ -70,7 +71,7 @@ training-related outcomes are inspected with `report(mach)`. ```@example machines X, y = @load_iris -pca = @load PCA +pca = (@load PCA verbosity=0)() mach = machine(pca, X) fit!(mach) ``` @@ -114,6 +115,18 @@ A machine is reconstructed from a file using the syntax `machine("my_machine.jlso")`, or `machine("my_machine.jlso", args...)` if retraining using new data. See [Saving machines](@ref) below. + +## Lowering memory demands + +For large data sets you may be able to save memory by suppressing data +caching that some models perform to increase speed. To do this, +specify `cache=false`, as in + +```julia +machine(model, X, y, cache=false) +``` + + ### Constructing machines in learning networks Instead of data `X`, `y`, etc, the `machine` constructor is provided @@ -129,7 +142,7 @@ machines](@ref). To save a machine to file, use the `MLJ.save` command: ```julia -tree = @load DecisionTreeClassifier +tree = (@load DecisionTreeClassifier verbosity=0)() mach = fit!(machine(tree, X, y)) MLJ.save("my_machine.jlso", mach) ``` diff --git a/docs/src/mlj_cheatsheet.md b/docs/src/mlj_cheatsheet.md index 3c576e417..55539d05f 100644 --- a/docs/src/mlj_cheatsheet.md +++ b/docs/src/mlj_cheatsheet.md @@ -33,12 +33,14 @@ models() do model end ``` -`tree = @load DecisionTreeClassifier` to load code and instantiate "DecisionTreeClassifier" model +`Tree = @load DecisionTreeClassifier` imports "DecisionTreeClassifier" type and binds it to `Tree` +`tree = Tree()` to instantiate a `Tree`. -`tree2 = DecisionTreeClassifier(max_depth=2)` instantiates a model type already in scope +`tree2 = Tree(max_depth=2)` instantiates a tree with different hyperparameter -`ridge = @load RidgeRegressor pkg=MultivariateStats` loads and -instantiates a model provided by multiple packages +`Ridge = @load RidgeRegressor pkg=MultivariateStats` imports a type for a model provided by multiple packages + +For interactive loading instead use `@iload` ## Scitypes and coercion diff --git a/docs/src/quick_start_guide_to_adding_models.md b/docs/src/quick_start_guide_to_adding_models.md index 8c4281308..8e3e4fe91 100644 --- a/docs/src/quick_start_guide_to_adding_models.md +++ b/docs/src/quick_start_guide_to_adding_models.md @@ -344,16 +344,17 @@ MLJModelInterface.metadata_pkg.(ALL_MODELS # Then for each model, MLJModelInterface.metadata_model(YourModel1, - input = MLJModelInterface.Table(MLJModelInterface.Continuous), # what input data is supported? - target = AbstractVector{MLJModelInterface.Continuous}, # for a supervised model, what target? - output = MLJModelInterface.Table(MLJModelInterface.Continuous), # for an unsupervised, what output? - weights = false, # does the model support sample weights? + input_scitype = MLJModelInterface.Table(MLJModelInterface.Continuous), # what input data is supported? + target_scitype = AbstractVector{MLJModelInterface.Continuous}, # for a supervised model, what target? + output_scitype = MLJModelInterface.Table(MLJModelInterface.Continuous), # for an unsupervised, what output? + supports_weights = false, # does the model support sample weights? descr = "A short description of your model" - path = "YourPackage.SubModuleContainingModelStructDefinition.YourModel1" + load_path = "YourPackage.SubModuleContainingModelStructDefinition.YourModel1" ) ``` -*Important.* Do not omit the `path` specification. +*Important.* Do not omit the `load_path` specification. Without a +correct `load_path` MLJ will be unable to import your model. **Examples**: @@ -370,8 +371,4 @@ MLJModelInterface.metadata_model(YourModel1, See [here](https://github.com/alan-turing-institute/MLJModels.jl/tree/master#instructions-for-updating-the-mlj-model-registry). -### Does it work? - -1. In new julia environment add `MLJ` and `YourPackage` -1. run `using MLJModels; @load YourModel pkg=YourPackage` diff --git a/docs/src/transformers.md b/docs/src/transformers.md index 0bdcf5e7e..522a7601e 100644 --- a/docs/src/transformers.md +++ b/docs/src/transformers.md @@ -51,13 +51,16 @@ the weighted average of two vectors (target predictions, for example). We suppose the weighting is normalized, and therefore controlled by a single hyper-parameter, `mix`. -```julia +```@setup boots +using MLJ +``` + +```@example boots mutable struct Averager <: Static mix::Float64 end -import MLJBase -MLJBase.transform(a::Averager, _, y1, y2) = (1 - a.mix)*y1 + a.mix*y2 +MLJ.transform(a::Averager, _, y1, y2) = (1 - a.mix)*y1 + a.mix*y2 ``` *Important.* Note the sub-typing `<: Static`. @@ -68,25 +71,21 @@ an `inverse_transform` can also be defined. Since they have no real learned parameters, you bind a static transformer to a machine without specifying training arguments. -```julia +```@example boots mach = machine(Averager(0.5)) |> fit! transform(mach, [1, 2, 3], [3, 2, 1]) -3-element Array{Float64,1}: - 2.0 - 2.0 - 2.0 ``` Let's see how we can include our `Averager` in a learning network (see [Composing Models](@ref)) to mix the predictions of two regressors, with one-hot encoding of the inputs: -```julia +```@example boots X = source() -y = source() #MLJ will automatically infer this a target node +y = source() -ridge = @load RidgeRegressor pkg=MultivariateStats -knn = @load KNNRegressor +ridge = (@load RidgeRegressor pkg=MultivariateStats)() +knn = (@load KNNRegressor)() averager = Averager(0.5) hotM = machine(OneHotEncoder(), X) @@ -102,13 +101,13 @@ averagerM= machine(averager) yhat = transform(averagerM, y1, y2) ``` -Now we export to obtain a `Deterministic` composite model and then +Now we export to obtain a `Deterministic` composite model and then instantiate composite model ```julia learning_mach = machine(Deterministic(), X, y; predict=yhat) Machine{DeterministicSurrogate} @772 trained 0 times. - args: + args: 1: Source @415 ⏎ `Unknown` 2: Source @389 ⏎ `Unknown` @@ -118,7 +117,7 @@ Machine{DeterministicSurrogate} @772 trained 0 times. regressor2=knn averager=averager end - + composite = DoubleRegressor() julia> composite = DoubleRegressor() DoubleRegressor( @@ -140,7 +139,6 @@ which can be can be evaluated like any other model: ```julia composite.averager.mix = 0.25 # adjust mix from default of 0.5 -evaluate(composite, (@load_reduced_ames)..., measure=rms) julia> evaluate(composite, (@load_reduced_ames)..., measure=rms) Evaluating over 6 folds: 100%[=========================] Time: 0:00:00 ┌───────────┬───────────────┬────────────────────────────────────────────────────────┐ @@ -149,6 +147,8 @@ Evaluating over 6 folds: 100%[=========================] Time: 0:00:00 │ rms │ 26800.0 │ [21400.0, 23700.0, 26800.0, 25900.0, 30800.0, 30700.0] │ └───────────┴───────────────┴────────────────────────────────────────────────────────┘ _.per_observation = [missing] +_.fitted_params_per_fold = [ … ] +_.report_per_fold = [ … ] ``` @@ -169,8 +169,9 @@ import Random.seed! seed!(123) X, y = @load_iris; -model = @load KMeans pkg=ParallelKMeans -mach = machine(model, X) |> fit! +KMeans = @load KMeans pkg=ParallelKMeans +kmeans = KMeans() +mach = machine(kmeans, X) |> fit! # transforming: Xsmall = transform(mach); @@ -223,4 +224,3 @@ compare[101:108] (3, "virginica") (2, "virginica") ``` - diff --git a/docs/src/tuning_models.md b/docs/src/tuning_models.md index 6e7168894..16e8c1774 100644 --- a/docs/src/tuning_models.md +++ b/docs/src/tuning_models.md @@ -29,7 +29,8 @@ below. using MLJ X = MLJ.table(rand(100, 10)); y = 2X.x1 - X.x2 + 0.05*rand(100); -tree = @load DecisionTreeRegressor verbosity=0; +Tree = @load DecisionTreeRegressor verbosity=0; +tree = Tree() ``` Let's tune `min_purity_increase` in the model above, using a @@ -123,7 +124,8 @@ info("KNNClassifier").prediction_type ```@example goof X, y = @load_iris -knn = @load KNNClassifier verbosity=0 +KNN = @load KNNClassifier verbosity=0 +knn = KNN() ``` We'll tune the hyperparameter `K` in the model above, using a @@ -235,7 +237,7 @@ The `forest` model below has another model, namely a `DecisionTreeRegressor`, as a hyperparameter: ```@example goof -tree = DecisionTreeRegressor() +tree = Tree() # defined above forest = EnsembleModel(atom=tree) ``` From 9f85d7805fe336be1e3b590df0e7568285a69adc Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Thu, 4 Feb 2021 11:36:06 +1300 Subject: [PATCH 13/20] update MLJ_stack images --- material/MLJ_stack.svg | 2 +- material/MLJ_stack.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/material/MLJ_stack.svg b/material/MLJ_stack.svg index d0adf8de3..50dca43f9 100644 --- a/material/MLJ_stack.svg +++ b/material/MLJ_stack.svg @@ -1,3 +1,3 @@ -
    MLJModelInterface
    MLJModelInterface
    ScientificTypes
    ScientificTypes
    MLJScientificTypes
    MLJScientificTypes
    MLJBase
    MLJBase
    MLJTuning
    MLJTuning
    MLJModels
    MLJModels
    MLJLinearModels
    MLJLinearModels
    ThirdPartyModelPkg
    ThirdPartyModelPkg
    MLJ
    MLJ
    StatisticalMeasures
    StatisticalMeasures
    MLJEnsembles
    MLJEnsembles
    DataScienceTutorials
    DataScienceTutorials
    MLJFlux
    MLJFlux
    satellite packages
    satellite packages
    A            B  means "A depends on B"
    A            B  means "A depends on...
     For general MLJ Users
     For general MLJ Users
    For model  API implementation/testing

    For model  API implementation/testin...
     Of interest outside MLJ
     Of interest outside MLJ
    For MLJ developers
    For MLJ developers
    Viewer does not support full SVG 1.1
    \ No newline at end of file +
    MLJModelInterface
    MLJModelInterface
    ScientificTypes
    ScientificTypes
    MLJScientificTypes
    MLJScientificTypes
    MLJBase
    MLJBase
    MLJTuning
    MLJTuning
    MLJModels
    MLJModels
    MLJLinearModels
    MLJLinearModels
    ThirdPartyModelPkg
    ThirdPartyModelPkg
    MLJ
    MLJ
    (StatisticalMeasures)
    (StatisticalMeasures)
    MLJEnsembles
    MLJEnsembles
    DataScienceTutorials
    DataScienceTutorials
    MLJFlux
    MLJFlux
    satellite packages
    satellite packages
    A            B  means "A depends on B"
    A            B  means "A depends on...
     For general MLJ Users
     For general MLJ Users
    For model  API implementation/testing

    For model  API implementation/testin...
     Of interest outside MLJ
     Of interest outside MLJ
    For MLJ developers
    For MLJ developers
    StatisticalTraits
    StatisticalTraits
    Viewer does not support full SVG 1.1
    \ No newline at end of file diff --git a/material/MLJ_stack.xml b/material/MLJ_stack.xml index a70853a7e..1f179905b 100644 --- a/material/MLJ_stack.xml +++ b/material/MLJ_stack.xml @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file From 999ee43dc2b1d6685367a280ee4b232c29b5c91b Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Fri, 5 Feb 2021 09:44:32 +1300 Subject: [PATCH 14/20] update docs around @load change and polish --- docs/Project.toml | 3 +- docs/make.jl | 2 +- docs/src/adding_models_for_general_use.md | 8 ++--- docs/src/common_mlj_workflows.md | 4 +-- docs/src/composing_models.md | 15 ++++----- docs/src/getting_started.md | 39 +++++++++++------------ docs/src/index.md | 19 ++++++----- docs/src/learning_curves.md | 3 +- docs/src/list_of_supported_models.md | 4 +-- docs/src/loading_model_code.md | 13 ++++++-- docs/src/third_party_packages.md | 4 +-- docs/src/working_with_categorical_data.md | 10 ++++-- 12 files changed, 68 insertions(+), 56 deletions(-) diff --git a/docs/Project.toml b/docs/Project.toml index 7411888f1..335aa4f16 100755 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -7,10 +7,11 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" LossFunctions = "30fc2ffe-d236-52d8-8643-a9d8f7c094a7" MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d" +MLJClusteringInterface = "d354fa79-ed1c-40d4-88ef-b8c7bd1568af" MLJDecisionTreeInterface = "c6f25543-311c-4c74-83dc-3ea6d1015661" MLJGLMInterface = "caf8df21-4939-456d-ac9c-5fefbfb04c0c" +MLJLinearModels = "6ee0df7b-362f-4a72-a706-9e79364fb692" MLJModelInterface = "e80e1ace-859a-464e-9ed9-23947d8ae3ea" -MLJModels = "d491faf4-2d78-11e9-2867-c94bc002c0b7" MLJMultivariateStatsInterface = "1b6a4a23-ba22-4f51-9698-8599985d3728" MLJScientificTypes = "2e2323e0-db8b-457b-ae0d-bdfb3bc63afd" MLJTuning = "03970b2e-30c4-11ea-3135-d1576263f10f" diff --git a/docs/make.jl b/docs/make.jl index 9d3bc81bd..f7bbc807b 100755 --- a/docs/make.jl +++ b/docs/make.jl @@ -27,7 +27,7 @@ pages = [ "Common MLJ Workflows" => "common_mlj_workflows.md", "Working with Categorical Data" => "working_with_categorical_data.md", "Model Search" => "model_search.md", - "Loading Model Code" => "loading_model_code", + "Loading Model Code" => "loading_model_code.md", "Machines" => "machines.md", "Evaluating Model Performance" => "evaluating_model_performance.md", "Performance Measures" => "performance_measures.md", diff --git a/docs/src/adding_models_for_general_use.md b/docs/src/adding_models_for_general_use.md index a38087c92..243280541 100755 --- a/docs/src/adding_models_for_general_use.md +++ b/docs/src/adding_models_for_general_use.md @@ -330,8 +330,8 @@ data front-end](@ref) for details). This can provide the MLJ user certain performance advantages when fitting a machine. ```julia - MLJModelInterface.reformat(model::SomeSupervisedModel, args...) = args - MLJModelInterface.selectrows(model::SomeSupervisedModel, I, data...) = data +MLJModelInterface.reformat(model::SomeSupervisedModel, args...) = args +MLJModelInterface.selectrows(model::SomeSupervisedModel, I, data...) = data ``` Optionally, to customized support for serialization of machines (see @@ -871,8 +871,8 @@ method. It is suggested that packages implementing MLJ's model API, that later implement a data front-end, should tag their changes in a breaking release. (The changes will not break use of models for the ordinary MLJ user, who interacts with models exlusively through the machine interface. However, it will break usage for some external packages that have chosen to depend directly on the model API.) ```julia - MLJModelInterface.reformat(model, args...) -> data - MLJModelInterface.selectrows(::Model, I, data...) -> sampled_data +MLJModelInterface.reformat(model, args...) -> data +MLJModelInterface.selectrows(::Model, I, data...) -> sampled_data ``` Models optionally overload `reformat` to define transformations of diff --git a/docs/src/common_mlj_workflows.md b/docs/src/common_mlj_workflows.md index 08054c203..3df82fe29 100644 --- a/docs/src/common_mlj_workflows.md +++ b/docs/src/common_mlj_workflows.md @@ -84,7 +84,7 @@ info("RidgeRegressor", pkg="MultivariateStats") # a model type in multiple packa ## Instantiating a model -*Reference:* [Getting Started](index.md) +*Reference:* [Getting Started](@ref), [Loading Model Code](@ref) ```@example workflows Tree = @load DecisionTreeClassifier @@ -425,7 +425,7 @@ pipe2 = @pipeline(X -> coerce(X, :age=>Continuous), X, y = @load_iris Tree = @load DecisionTreeClassifier tree = Tree() -forest = EnsembleModel(atom=tree_model, bagging_fraction=0.8, n=300) +forest = EnsembleModel(atom=tree, bagging_fraction=0.8, n=300) mach = machine(forest, X, y) evaluate!(mach, measure=LogLoss()) ``` diff --git a/docs/src/composing_models.md b/docs/src/composing_models.md index 9dea1e98e..2907d2280 100644 --- a/docs/src/composing_models.md +++ b/docs/src/composing_models.md @@ -14,12 +14,9 @@ composition use-cases, which are described first below. A description of the general framework begins at [Learning Networks](@ref). For an in-depth high-level description of learning -networks, refer to the article linked below. - -
    - Anthony D. Blaom and Sebastian J. Voller (2020): Flexible model composition in machine learning and its implementation in MLJ Preprint, arXiv:2012.15505 - +networks, refer to [Anthony D. Blaom and Sebastian J. Voller (2020): Flexible model +composition in machine learning and its implementation in MLJ. +Preprint, arXiv:2012.15505](https://arxiv.org/abs/2012.15505). ## Linear pipelines @@ -138,7 +135,7 @@ networks, which have been described more abstractly in the article [Anthony D. Blaom and Sebastian J. Voller (2020): Flexible model composition in machine learning and its implementation in MLJ. Preprint, arXiv:2012.15505](https://arxiv.org/abs/2012.15505). - +w Hand-crafting a learning network, as outlined below, is a relatively advanced MLJ feature, assuming familiarity with the basics outlined in [Getting Started](index.md). The syntax for building a learning @@ -333,6 +330,7 @@ this way, the code ```@example 7 fit!(mach) predict(mach, X[test,:]); +nothing # hide ``` is equivalent to @@ -340,6 +338,7 @@ is equivalent to ```@example 7 fit!(yhat) yhat(X[test,:]); +nothing # hide ``` While it's main purpose is for export (see below), this machine can @@ -554,7 +553,7 @@ my_composite = MyComposite(kmeans, nothing, 0.5) ```@example 7 evaluate(my_composite, X, y, measure=MeanAbsoluteError(), verbosity=0) ``` - + ## Static operations on nodes Continuing to view nodes as "dynamic data", we can, in addition to diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md index 1cfe34626..d2ac34a34 100644 --- a/docs/src/getting_started.md +++ b/docs/src/getting_started.md @@ -47,8 +47,8 @@ In MLJ a *model* is a struct storing the hyperparameters of the learning algorithm indicated by the struct name (and nothing else). Assuming the MLJDecisionTreeInterface.jl package is in your load path -(see [Installation](@ref)) we can use `@load` to import the -`DecisionTreeClassifier` model type, which we will be bind to `Tree`: +(see [Installation](@ref)) we can use `@load` to import the +`DecisionTreeClassifier` model type, which we will bind to `Tree`: ```@repl doda Tree = @load DecisionTreeClassifier @@ -64,9 +64,9 @@ tree = Tree() machine learning algorithms for use in MLJ are not MLJ dependencies. If such a package is not in your load path you will receive an error explaining how to add the package to your current -environment. Alternatively, you can use the interactive version of -`@iload`. For more on importing model types, see [`Loading Model -Code`](@ref). +environment. Alternatively, you can use the interactive macro +`@iload`. For more on importing model types, see [Loading Model +Code](@ref). Once instantiated, a model's performance can be evaluated with the `evaluate` method: @@ -126,7 +126,7 @@ and using `yint` in place of `y` in classification problems will fail. See also [Working with Categorical Data](@ref). For more on scientific types, see [Data containers and scientific -types](@ref) below. +types](@ref) below. ## Fit and predict @@ -153,8 +153,7 @@ log_loss(yhat, y[test]) |> mean ``` Here `log_loss` (and `cross_entropy`) is an alias for `LogLoss()` or, -more precisely, a built-in instance of the `LogLoss` type. Another -instance is `LogLoss(tol=0.0001)`. For a list of all losses and +more precisely, a built-in instance of the `LogLoss` type. For a list of all losses and scores, and their aliases, run `measures()`. Notice that `yhat` is a vector of `Distribution` objects (because @@ -189,10 +188,10 @@ and may optionally implement an `inverse_transform` method: ```@repl doda v = [1, 2, 3, 4] stand = UnivariateStandardizer() # this type is built-in -mach = machine(stand, v) -fit!(mach) -w = transform(mach, v) -inverse_transform(mach, w) +mach2 = machine(stand, v) +fit!(mach2) +w = transform(mach2, v) +inverse_transform(mach2, w) ``` [Machines](machines.md) have an internal state which allows them to @@ -205,7 +204,7 @@ as explained in [Composing Models](composing_models.md). There is a version of `evaluate` for machines as well as models. This time we'll add a second performance measure. (An exclamation point is added to the method name because machines are generally mutated when -trained): +trained.) ```@repl doda evaluate!(mach, resampling=Holdout(fraction_train=0.7, shuffle=true), @@ -215,7 +214,7 @@ evaluate!(mach, resampling=Holdout(fraction_train=0.7, shuffle=true), Changing a hyperparameter and re-evaluating: ```@repl doda -tree_model.max_depth = 3 +tree.max_depth = 3 evaluate!(mach, resampling=Holdout(fraction_train=0.7, shuffle=true), measures=[cross_entropy, brier_score], verbosity=0) @@ -229,8 +228,8 @@ Julia](https://alan-turing-institute.github.io/DataScienceTutorials.jl/) or try the [JuliaCon2020 Workshop](https://github.com/ablaom/MachineLearningInJulia2020) on MLJ (recorded -[here](https://www.youtube.com/watch?time_continue=27&v=qSWbCn170HU&feature=emb_title)) -returning to the manual as needed. +[here](https://www.youtube.com/watch?time_continue=27&v=qSWbCn170HU&feature=emb_title)) +returning to the manual as needed. *Read at least the remainder of this page before considering serious use of MLJ.* @@ -327,7 +326,7 @@ _.nrows = 2 ``` The matrix is *not* copied, only wrapped. To manifest a table as a -matrix, use [`MLJ.matrix`](@ref). +matrix, use [`MLJ.matrix`](@ref). ### Inputs @@ -335,7 +334,7 @@ matrix, use [`MLJ.matrix`](@ref). Since an MLJ model only specifies the scientific type of data, if that type is `Table` - which is the case for the majority of MLJ models - then any [Tables.jl](https://github.com/JuliaData/Tables.jl) format is -permitted. +permitted. Specifically, the requirement for an arbitrary model's input is `scitype(X) <: input_scitype(model)`. @@ -365,7 +364,7 @@ i.input_scitype i.target_scitype ``` -But see also [Model Search](@ref). +But see also [Model Search](@ref). ### Scalar scientific types @@ -388,7 +387,7 @@ are the key features of that convention: - In particular, *integers* (including `Bool`s) *cannot be used to represent categorical data.* Use the preceding `coerce` operations to coerce to a `Finite` scitype. - + - The scientific types of `nothing` and `missing` are `Nothing` and `Missing`, native types we also regard as scientific. diff --git a/docs/src/index.md b/docs/src/index.md index e419f990e..fe008e3af 100755 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -11,7 +11,7 @@ A Machine Learning Framework for Julia Tutorials  |  For Developers  |  Live Demo  |  - 3rd Party Packages + 3rd Party Packages ``` @@ -44,7 +44,6 @@ Julia installation instructions are using Pkg Pkg.activate("MLJ_tour", shared=true) Pkg.add("MLJ") -Pkg.add("MLJModels") Pkg.add("EvoTrees") ``` @@ -58,8 +57,8 @@ X, y = @load_reduced_ames; Load and instantiate a gradient tree-boosting model type: ```julia -Booster = @load EvoTreeRegressor -Booster = booster(max_depth=2) # specify hyperparamter at construction +Booster = @load EvoTreeRegressor +booster = Booster(max_depth=2) # specify hyperparamter at construction booster.nrounds=50 # or mutate post facto ``` @@ -178,11 +177,11 @@ do not exist in MLJ: - Hyper-parameters and/or learned parameters of component models are not easily inspected or manipulated (by tuning algorithms, for example) - + - Composite models cannot implement multiple opertations, for example, both a `predict` and `transform` method (as in clustering models) or both a `transform` and `inverse_transform` method. - + Some of these features are demonstrated in [this notebook](https://github.com/ablaom/MachineLearningInJulia2020/blob/master/wow.ipynb) @@ -223,7 +222,7 @@ julia> Pkg.add("MLJ") ```julia julia> Pkg.test("MLJ") ``` - + It is important to note that MLJ is essentially a big wrapper providing a unified access to _model providing packages_. For this reason, one generally needs to add further packages to your @@ -236,9 +235,9 @@ julia> Tree = @iload DecisionTreeClassifier # load type julis> tree = Tree() # instantiate ``` -For more on identifying the name of an applicable model, see [`Model -Search`](@ref). For non-interactive loading of code (e.g., from a -module or function) see [`Loading Model Code`](@ref). +For more on identifying the name of an applicable model, see [Model +Search](@ref). For non-interactive loading of code (e.g., from a +module or function) see [Loading Model Code](@ref). It is recommended that you start with models marked as coming from mature packages such as DecisionTree.jl, ScikitLearn.jl or XGBoost.jl. diff --git a/docs/src/learning_curves.md b/docs/src/learning_curves.md index dd35f93ff..3174b0114 100644 --- a/docs/src/learning_curves.md +++ b/docs/src/learning_curves.md @@ -26,7 +26,6 @@ curve = MLJ.learning_curve(mach; resampling=CV(nfolds=3), measure=MeanAbsoluteError()) ``` - ```julia using Plots plot(curve.parameter_values, @@ -57,6 +56,8 @@ curves = MLJ.learning_curve(mach; verbosity=0, rng_name=:rng, rngs=4) +``` +```julia plot(curves.parameter_values, curves.measurements, xlab=curves.parameter_name, diff --git a/docs/src/list_of_supported_models.md b/docs/src/list_of_supported_models.md index 38befdb27..0ecbee39f 100644 --- a/docs/src/list_of_supported_models.md +++ b/docs/src/list_of_supported_models.md @@ -27,9 +27,9 @@ models()`. [MLJFlux.jl](https://github.com/alan-turing-institute/MLJFlux.jl) | NeuralNetworkRegressor, NeuralNetworkClassifier, MultitargetNeuralNetworkRegressor, ImageClassifier | experimental | [MLJLinearModels.jl](https://github.com/alan-turing-institute/MLJLinearModels.jl) | LinearRegressor, RidgeRegressor, LassoRegressor, ElasticNetRegressor, QuantileRegressor, HuberRegressor, RobustRegressor, LADRegressor, LogisticClassifier, MultinomialClassifier | experimental | [MLJModels.jl](https://github.com/alan-turing-institute/MLJModels.jl) (built-in) | StaticTransformer, FeatureSelector, FillImputer, UnivariateStandardizer, Standardizer, UnivariateBoxCoxTransformer, OneHotEncoder, ContinuousEncoder, ConstantRegressor, ConstantClassifier, BinaryThreshholdPredictor | medium | -[MultivariateStats.jl](https://github.com/JuliaStats/MultivariateStats.jl) | LinearRegressor, RidgeRegressor, PCA, KernelPCA, ICA, LDA, BayesianLDA, SubspaceLDA, BayesianSubspaceLDA, FactorAnalysis, PPCA | high | +[MultivariateStats.jl](https://github.com/JuliaStats/MultivariateStats.jl) | LinearRegressor, MultitargetLinearRegressor, RidgeRegressor, MultitargetRidgeRegressor, PCA, KernelPCA, ICA, LDA, BayesianLDA, SubspaceLDA, BayesianSubspaceLDA, FactorAnalysis, PPCA | high | [NaiveBayes.jl](https://github.com/dfdx/NaiveBayes.jl) | GaussianNBClassifier, MultinomialNBClassifier, HybridNBClassifier | experimental | -[NearestNeighbors.jl](https://github.com/KristofferC/NearestNeighbors.jl) | KNNClassifier, KNNRegressor | high | +[NearestNeighborModels.jl](https://github.com/alan-turing-institute/NearestNeighborModels.jl) | KNNClassifier, KNNRegressor, MultitargetKNNClassifier, MultitargetKNNRegressor | high | [ParallelKMeans.jl](https://github.com/PyDataBlog/ParallelKMeans.jl) | KMeans | experimental | [PartialLeastSquaresRegressor.jl](https://github.com/lalvim/PartialLeastSquaresRegressor.jl) | PLSRegressor, KPLSRegressor | experimental | [ScikitLearn.jl](https://github.com/cstjean/ScikitLearn.jl) | ARDRegressor, AdaBoostClassifier, AdaBoostRegressor, AffinityPropagation, AgglomerativeClustering, BaggingClassifier, BaggingRegressor, BayesianLDA, BayesianQDA, BayesianRidgeRegressor, BernoulliNBClassifier, Birch, ComplementNBClassifier, DBSCAN, DummyClassifier, DummyRegressor, ElasticNetCVRegressor, ElasticNetRegressor, ExtraTreesClassifier, ExtraTreesRegressor, FeatureAgglomeration, GaussianNBClassifier, GaussianProcessClassifier, GaussianProcessRegressor, GradientBoostingClassifier, GradientBoostingRegressor, HuberRegressor, KMeans, KNeighborsClassifier, KNeighborsRegressor, LarsCVRegressor, LarsRegressor, LassoCVRegressor, LassoLarsCVRegressor, LassoLarsICRegressor, LassoLarsRegressor, LassoRegressor, LinearRegressor, LogisticCVClassifier, LogisticClassifier, MeanShift, MiniBatchKMeans, MultiTaskElasticNetCVRegressor, MultiTaskElasticNetRegressor, MultiTaskLassoCVRegressor, MultiTaskLassoRegressor, MultinomialNBClassifier, OPTICS, OrthogonalMatchingPursuitCVRegressor, OrthogonalMatchingPursuitRegressor, PassiveAggressiveClassifier, PassiveAggressiveRegressor, PerceptronClassifier, ProbabilisticSGDClassifier, RANSACRegressor, RandomForestClassifier, RandomForestRegressor, RidgeCVClassifier, RidgeCVRegressor, RidgeClassifier, RidgeRegressor, SGDClassifier, SGDRegressor, SVMClassifier, SVMLClassifier, SVMLRegressor, SVMNuClassifier, SVMNuRegressor, SVMRegressor, SpectralClustering, TheilSenRegressor | high | † diff --git a/docs/src/loading_model_code.md b/docs/src/loading_model_code.md index e1662125c..095199b4f 100644 --- a/docs/src/loading_model_code.md +++ b/docs/src/loading_model_code.md @@ -1,9 +1,9 @@ -# Loading model code +# Loading Model Code Once the name of a model, and the package providing that model, have -been identified (see [`Model Search`](@ref)) one can either import the +been identified (see [Model Search](@ref)) one can either import the model type interactively with `@iload`, as shown under -[`Installation`](@ref), or use `@load` as shown below. The `@load` +[Installation](@ref), or use `@load` as shown below. The `@load` macro works from within a module, a package or a function, provided the relevant package providing the MLJ interface has been added to your package environment. @@ -64,3 +64,10 @@ julia> tree = Tree() dropped, as only one package provides a model called `DecisionTreeClassifier`. +## API + +```@docs +load_path +@load +@iload +``` diff --git a/docs/src/third_party_packages.md b/docs/src/third_party_packages.md index 4a6f7a9f6..f609ded73 100644 --- a/docs/src/third_party_packages.md +++ b/docs/src/third_party_packages.md @@ -18,10 +18,10 @@ post an issue requesting this - [LIBSVM.jl](https://github.com/mpastell/LIBSVM.jl) - [EvoTrees.jl](https://github.com/Evovest/EvoTrees.jl) - [NaiveBayes.jl](https://github.com/dfdx/NaiveBayes.jl) -- [MLJFlux.jl](https://github.com/alan-turing-institute/MLJFlux.jl) +- [MLJFlux.jl](https://github.com/alan-turing-institute/MLJFlux.jl) (depending on [Flux.jl](https://github.com/FluxML/Flux.jl)) - [Clustering.jl](https://github.com/JuliaStats/Clustering.jl) - [ParallelKMeans.jl](https://github.com/PyDataBlog/ParallelKMeans.jl) -- [NearestNeighbors.jl](https://github.com/KristofferC/NearestNeighbors.jl) +- [NearestNeighborModels.jl](https://github.com/alan-turing-institute/NearestNeighborModels.jl) (depending on [NearestNeighbors.jl](https://github.com/KristofferC/NearestNeighbors.jl)) - [PartialLeastSquaresRegressor.jl](https://github.com/lalvim/PartialLeastSquaresRegressor.jl) - [LightGBM.jl](https://github.com/IQVIA-ML/LightGBM.jl) - [GLM.jl](https://github.com/JuliaStats/GLM.jl) diff --git a/docs/src/working_with_categorical_data.md b/docs/src/working_with_categorical_data.md index 631374da0..cb5cdb6a4 100644 --- a/docs/src/working_with_categorical_data.md +++ b/docs/src/working_with_categorical_data.md @@ -174,8 +174,14 @@ mach = machine(model, X) |> fit! # one-hot encode new data with missing classes: xproduction = coerce(["white", "white"], Multiclass) Xproduction = DataFrame(x=xproduction) -Xproduction == X[2:3,:] # true -transform(mach, Xproduction) == transform(mach, X[2:3,:]) +Xproduction == X[2:3,:] +``` + +So far, so good. But the following operation throws an error: + +```julia +julia> transform(mach, Xproduction) == transform(mach, X[2:3,:]) +ERROR: Found category level mismatch in feature `x`. Consider using `levels!` to ensure fitted and transforming features have the same category levels. ``` The problem here is that `levels(X.x)` and `levels(Xproduction.x)` are different: From 9e5c904c9f7b5090b4d9e58316c5316f9741c8a0 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 8 Feb 2021 14:26:08 +1300 Subject: [PATCH 15/20] NearestNeighbors -> NearestNeighborModels in doc project --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 335aa4f16..c69893462 100755 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -16,7 +16,7 @@ MLJMultivariateStatsInterface = "1b6a4a23-ba22-4f51-9698-8599985d3728" MLJScientificTypes = "2e2323e0-db8b-457b-ae0d-bdfb3bc63afd" MLJTuning = "03970b2e-30c4-11ea-3135-d1576263f10f" Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28" -NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" +NearestNeighborModels = "636a865e-7cf4-491e-846c-de09b730eb36" RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ScientificTypes = "321657f4-b219-11e9-178b-2701a2544e81" From b08f4b718ded27947fe1bf27ac591ef22f936218 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 8 Feb 2021 15:55:21 +1300 Subject: [PATCH 16/20] add evotrees example of data front-end to docs --- docs/src/adding_models_for_general_use.md | 5 +++++ docs/src/index.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/src/adding_models_for_general_use.md b/docs/src/adding_models_for_general_use.md index 243280541..a538ca0b4 100755 --- a/docs/src/adding_models_for_general_use.md +++ b/docs/src/adding_models_for_general_use.md @@ -889,6 +889,11 @@ indices `I` (a colon, `:`, or instance of front-end also allow more efficient resampling of data (in user calls to `evaluate!`). +After detailing formal requirments for implementing a data front-end, +we give a [Sample implementation](@ref). A simple implementation +[implementation](https://github.com/Evovest/EvoTrees.jl/blob/94b58faf3042009bd609c9a5155a2e95486c2f0e/src/MLJ.jl#L23) +also appears in the EvoTrees.jl package. + Here "user-supplied data" is what the MLJ user supplies when constructing a machine, as in `machine(models, args...)`, which coincides with the arguments expected by `fit(model, verbosity, diff --git a/docs/src/index.md b/docs/src/index.md index fe008e3af..0436bbee9 100755 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -232,7 +232,7 @@ happens automatically when you use MLJ's interactive load command ```julia julia> Tree = @iload DecisionTreeClassifier # load type -julis> tree = Tree() # instantiate +julis> tree = Tree() # instance ``` For more on identifying the name of an applicable model, see [Model From f87c707cbdb53cf54d839f70cb9dab79a6d97d76 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 8 Feb 2021 15:58:28 +1300 Subject: [PATCH 17/20] bump 0.16 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 36ae59d55..a565a697d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "MLJ" uuid = "add582a8-e3ab-11e8-2d5e-e98b27df1bc7" authors = ["Anthony D. Blaom "] -version = "0.15.2" +version = "0.16.0" [deps] CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597" From 02590243c649278985b9253a43732645f9211681 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 8 Feb 2021 16:14:34 +1300 Subject: [PATCH 18/20] bump [compat] to julia="1.1" and adjust ci accordingly --- .github/workflows/ci.yml | 2 +- Project.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3b92c53c..b68392474 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: version: - - '1.0' + - '1.1' - '1' # automatically expands to the latest stable 1.x release of Julia. os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index a565a697d..fd44d5793 100644 --- a/Project.toml +++ b/Project.toml @@ -31,7 +31,7 @@ MLJTuning = "^0.6" ProgressMeter = "^1.1" StatsBase = "^0.32,^0.33" Tables = "^0.2,^1.0" -julia = "1" +julia = "^1.1" [extras] NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce" From b9acd8cbdc6f4ce71503252f75e684edabcfd592 Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 8 Feb 2021 17:12:29 +1300 Subject: [PATCH 19/20] turn off pkg server in ci doc build --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b68392474..c9c105b9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,8 @@ jobs: docs: name: Documentation runs-on: ubuntu-latest + env: + JULIA_PKG_SERVER: "" steps: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 From aee3e4fa0b56b33a27d07e58ff0705023d97377d Mon Sep 17 00:00:00 2001 From: "Anthony D. Blaom" Date: Mon, 8 Feb 2021 17:19:59 +1300 Subject: [PATCH 20/20] return forgotten MLJModels to docs/Project.toml --- docs/Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Project.toml b/docs/Project.toml index c69893462..4ffbe718b 100755 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -12,6 +12,7 @@ MLJDecisionTreeInterface = "c6f25543-311c-4c74-83dc-3ea6d1015661" MLJGLMInterface = "caf8df21-4939-456d-ac9c-5fefbfb04c0c" MLJLinearModels = "6ee0df7b-362f-4a72-a706-9e79364fb692" MLJModelInterface = "e80e1ace-859a-464e-9ed9-23947d8ae3ea" +MLJModels = "d491faf4-2d78-11e9-2867-c94bc002c0b7" MLJMultivariateStatsInterface = "1b6a4a23-ba22-4f51-9698-8599985d3728" MLJScientificTypes = "2e2323e0-db8b-457b-ae0d-bdfb3bc63afd" MLJTuning = "03970b2e-30c4-11ea-3135-d1576263f10f"