Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "LearnAPI"
uuid = "92ad9a40-7767-427a-9ee6-6e577f1266cb"
authors = ["Anthony D. Blaom <[email protected]>"]
version = "0.1.0"
version = "0.2.0"

[compat]
julia = "1.10"
Expand Down
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ makedocs(
"predict/transform" => "predict_transform.md",
"Kinds of Target Proxy" => "kinds_of_target_proxy.md",
"obs and Data Interfaces" => "obs.md",
"target/weights/features" => "target_weights_features.md",
"features/target/weights" => "features_target_weights.md",
"Accessor Functions" => "accessor_functions.md",
"Learner Traits" => "traits.md",
],
Expand Down
214 changes: 129 additions & 85 deletions docs/src/anatomy_of_an_implementation.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/src/common_implementation_patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ which introduces the main interface objects and terminology.

Although an implementation is defined purely by the methods and traits it implements, many
implementations fall into one (or more) of the following informally understood patterns or
"tasks":
tasks:

- [Regression](@ref): Supervised learners for continuous targets

Expand Down
192 changes: 192 additions & 0 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# [Code for ridge example](@id code)

Below is the complete source code for the ridge implementations described in the tutorial,
[Anatomy of an Implementation](@ref).

- [Basic implementation](@ref)
- [Implementation with data front end](@ref)


## Basic implementation

```julia
using LearnAPI
using LinearAlgebra, Tables

struct Ridge{T<:Real}
lambda::T
end

"""
Ridge(; lambda=0.1)

Instantiate a ridge regression learner, with regularization of `lambda`.
"""
Ridge(; lambda=0.1) = Ridge(lambda)
LearnAPI.constructor(::Ridge) = Ridge

# struct for output of `fit`
struct RidgeFitted{T,F}
learner::Ridge
coefficients::Vector{T}
named_coefficients::F
end

function LearnAPI.fit(learner::Ridge, data; verbosity=1)
X, y = data

# data preprocessing:
table = Tables.columntable(X)
names = Tables.columnnames(table) |> collect
A = Tables.matrix(table, transpose=true)

lambda = learner.lambda

# apply core algorithm:
coefficients = (A*A' + learner.lambda*I)\(A*y) # vector

# determine named coefficients:
named_coefficients = [names[j] => coefficients[j] for j in eachindex(names)]

# make some noise, if allowed:
verbosity > 0 && @info "Coefficients: $named_coefficients"

return RidgeFitted(learner, coefficients, named_coefficients)
end

LearnAPI.predict(model::RidgeFitted, ::Point, Xnew) =
Tables.matrix(Xnew)*model.coefficients

# accessor functions:
LearnAPI.learner(model::RidgeFitted) = model.learner
LearnAPI.coefficients(model::RidgeFitted) = model.named_coefficients
LearnAPI.strip(model::RidgeFitted) =
RidgeFitted(model.learner, model.coefficients, nothing)

@trait(
Ridge,
constructor = Ridge,
kinds_of_proxy=(Point(),),
tags = ("regression",),
functions = (
:(LearnAPI.fit),
:(LearnAPI.learner),
:(LearnAPI.clone),
:(LearnAPI.strip),
:(LearnAPI.obs),
:(LearnAPI.features),
:(LearnAPI.target),
:(LearnAPI.predict),
:(LearnAPI.coefficients),
)
)

# convenience method:
LearnAPI.fit(learner::Ridge, X, y; kwargs...) = fit(learner, (X, y); kwargs...)
```

# Implementation with data front end

```julia
using LearnAPI
using LinearAlgebra, Tables

struct Ridge{T<:Real}
lambda::T
end

Ridge(; lambda=0.1) = Ridge(lambda)

# struct for output of `fit`:
struct RidgeFitted{T,F}
learner::Ridge
coefficients::Vector{T}
named_coefficients::F
end

# struct for internal representation of training data:
struct RidgeFitObs{T,M<:AbstractMatrix{T}}
A::M # `p` x `n` matrix
names::Vector{Symbol} # features
y::Vector{T} # target
end

# implementation of `RandomAccess()` data interface for such representation:
Base.getindex(data::RidgeFitObs, I) =
RidgeFitObs(data.A[:,I], data.names, y[I])
Base.length(data::RidgeFitObs) = length(data.y)

# data front end for `fit`:
function LearnAPI.obs(::Ridge, data)
X, y = data
table = Tables.columntable(X)
names = Tables.columnnames(table) |> collect
return RidgeFitObs(Tables.matrix(table)', names, y)
end
LearnAPI.obs(::Ridge, observations::RidgeFitObs) = observations

function LearnAPI.fit(learner::Ridge, observations::RidgeFitObs; verbosity=1)

lambda = learner.lambda

A = observations.A
names = observations.names
y = observations.y

# apply core learner:
coefficients = (A*A' + learner.lambda*I)\(A*y) # 1 x p matrix

# determine named coefficients:
named_coefficients = [names[j] => coefficients[j] for j in eachindex(names)]

# make some noise, if allowed:
verbosity > 0 && @info "Coefficients: $named_coefficients"

return RidgeFitted(learner, coefficients, named_coefficients)

end

LearnAPI.fit(learner::Ridge, data; kwargs...) =
fit(learner, obs(learner, data); kwargs...)

# data front end for `predict`:
LearnAPI.obs(::RidgeFitted, Xnew) = Tables.matrix(Xnew)'
LearnAPI.obs(::RidgeFitted, observations::AbstractArray) = observations # involutivity

LearnAPI.predict(model::RidgeFitted, ::Point, observations::AbstractMatrix) =
observations'*model.coefficients

LearnAPI.predict(model::RidgeFitted, ::Point, Xnew) =
predict(model, Point(), obs(model, Xnew))

# methods to deconstruct training data:
LearnAPI.features(::Ridge, observations::RidgeFitObs) = observations.A
LearnAPI.target(::Ridge, observations::RidgeFitObs) = observations.y
LearnAPI.features(learner::Ridge, data) = LearnAPI.features(learner, obs(learner, data))
LearnAPI.target(learner::Ridge, data) = LearnAPI.target(learner, obs(learner, data))

# accessor functions:
LearnAPI.learner(model::RidgeFitted) = model.learner
LearnAPI.coefficients(model::RidgeFitted) = model.named_coefficients
LearnAPI.strip(model::RidgeFitted) =
RidgeFitted(model.learner, model.coefficients, nothing)

@trait(
Ridge,
constructor = Ridge,
kinds_of_proxy=(Point(),),
tags = ("regression",),
functions = (
:(LearnAPI.fit),
:(LearnAPI.learner),
:(LearnAPI.clone),
:(LearnAPI.strip),
:(LearnAPI.obs),
:(LearnAPI.features),
:(LearnAPI.target),
:(LearnAPI.predict),
:(LearnAPI.coefficients),
)
)

```
45 changes: 45 additions & 0 deletions docs/src/features_target_weights.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# [`features`, `target`, and `weights`](@id input)

Methods for extracting certain parts of `data` for all supported calls of the form
[`fit(learner, data)`](@ref).

```julia
LearnAPI.features(learner, data) -> <training "features", suitable input for `predict` or `transform`>
LearnAPI.target(learner, data) -> <target variable>
LearnAPI.weights(learner, data) -> <per-observation weights>
```

Here `data` is something supported in a call of the form `fit(learner, data)`.

# Typical workflow

Not typically appearing in a general user's workflow but useful in meta-alagorithms, such
as cross-validation (see the example in [`obs` and Data Interfaces](@ref data_interface)).

Supposing `learner` is a supervised classifier predicting a vector
target:

```julia
model = fit(learner, data)
X = LearnAPI.features(learner, data)
y = LearnAPI.target(learner, data)
ŷ = predict(model, Point(), X)
training_loss = sum(ŷ .!= y)
```

# Implementation guide

| method | fallback return value | compulsory? |
|:-------------------------------------------|:---------------------------------------------:|--------------------------|
| [`LearnAPI.features(learner, data)`](@ref) | `first(data)` if `data` is tuple, else `data` | if fallback insufficient |
| [`LearnAPI.target(learner, data)`](@ref) | `last(data)` | if fallback insufficient |
| [`LearnAPI.weights(learner, data)`](@ref) | `nothing` | no |


# Reference

```@docs
LearnAPI.features
LearnAPI.target
LearnAPI.weights
```
21 changes: 10 additions & 11 deletions docs/src/fit_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
### Training

```julia
fit(learner, data; verbosity=LearnAPI.default_verbosity()) -> model
fit(learner; verbosity=LearnAPI.default_verbosity()) -> static_model
fit(learner, data; verbosity=1) -> model
fit(learner; verbosity=1) -> static_model
```

A "static" algorithm is one that does not generalize to new observations (e.g., some
clustering algorithms); there is no training data and the algorithm is executed by
clustering algorithms); there is no training data and heavy lifting is carried out by
`predict` or `transform` which receive the data. See example below.


Expand Down Expand Up @@ -101,18 +101,18 @@ See also [Density Estimation](@ref).

Exactly one of the following must be implemented:

| method | fallback |
|:-----------------------------------------------------------------------|:---------|
| [`fit`](@ref)`(learner, data; verbosity=LearnAPI.default_verbosity())` | none |
| [`fit`](@ref)`(learner; verbosity=LearnAPI.default_verbosity())` | none |
| method | fallback |
|:--------------------------------------------|:---------|
| [`fit`](@ref)`(learner, data; verbosity=1)` | none |
| [`fit`](@ref)`(learner; verbosity=1)` | none |

### Updating

| method | fallback | compulsory? |
|:-------------------------------------------------------------------------------------|:---------|-------------|
| [`update`](@ref)`(model, data; verbosity=..., hyperparameter_updates...)` | none | no |
| [`update_observations`](@ref)`(model, new_data; verbosity=..., hyperparameter_updates...)` | none | no |
| [`update_features`](@ref)`(model, new_data; verbosity=..., hyperparameter_updates...)` | none | no |
| [`update`](@ref)`(model, data; verbosity=1, hyperparameter_updates...)` | none | no |
| [`update_observations`](@ref)`(model, new_data; verbosity=1, hyperparameter_updates...)` | none | no |
| [`update_features`](@ref)`(model, new_data; verbosity=1, hyperparameter_updates...)` | none | no |

There are some contracts governing the behaviour of the update methods, as they relate to
a previous `fit` call. Consult the document strings for details.
Expand All @@ -124,5 +124,4 @@ fit
update
update_observations
update_features
LearnAPI.default_verbosity
```
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Suppose `forest` is some object encapsulating the hyperparameters of the [random
algorithm](https://en.wikipedia.org/wiki/Random_forest) (the number of trees, etc.). Then,
a LearnAPI.jl interface can be implemented, for objects with the type of `forest`, to
enable the basic workflow below. In this case data is presented following the
"scikit-learn" `X, y` pattern, although LearnAPI.jl supports other patterns as well.
"scikit-learn" `X, y` pattern, although LearnAPI.jl supports other data pattern.

```julia
# `X` is some training features
Expand Down
14 changes: 5 additions & 9 deletions docs/src/obs.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ obs(learner, data) # can be passed to `fit` instead of `data`
obs(model, data) # can be passed to `predict` or `transform` instead of `data`
```

- [Data interfaces](@ref data_interfaces)


## [Typical workflows](@id obs_workflows)

LearnAPI.jl makes no universal assumptions about the form of `data` in a call
Expand Down Expand Up @@ -93,18 +96,11 @@ A sample implementation is given in [Providing a separate data front end](@ref).
obs
```

### [Data interfaces](@id data_interfaces)

New implementations must overload [`LearnAPI.data_interface(learner)`](@ref) if the
output of [`obs`](@ref) does not implement [`LearnAPI.RandomAccess()`](@ref). Arrays, most
tables, and all tuples thereof, implement `RandomAccess()`.

- [`LearnAPI.RandomAccess`](@ref) (default)
- [`LearnAPI.FiniteIterable`](@ref)
- [`LearnAPI.Iterable`](@ref)
### [Available data interfaces](@id data_interfaces)


```@docs
LearnAPI.DataInterface
LearnAPI.RandomAccess
LearnAPI.FiniteIterable
LearnAPI.Iterable
Expand Down
6 changes: 2 additions & 4 deletions docs/src/patterns/transformers.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# [Transformers](@id transformers)

Check out the following examples:
Check out the following examples from the TestLearnAPI.jl test suite:

- [Truncated
SVD]((https://github.com/JuliaAI/LearnTestAPI.jl/blob/dev/test/patterns/dimension_reduction.jl
(from the TestLearnAPI.jl test suite)
- [Truncated SVD](https://github.com/JuliaAI/LearnTestAPI.jl/blob/dev/test/patterns/dimension_reduction.jl)
Loading
Loading