Skip to content

Commit 3c002e8

Browse files
authored
Merge pull request #29 from alan-turing-institute/dev
For a 0.3 release
2 parents c64bff7 + adae301 commit 3c002e8

File tree

8 files changed

+305
-108
lines changed

8 files changed

+305
-108
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "MLJTuning"
22
uuid = "03970b2e-30c4-11ea-3135-d1576263f10f"
33
authors = ["Anthony D. Blaom <anthony.blaom@gmail.com>"]
4-
version = "0.2.0"
4+
version = "0.3.0"
55

66
[deps]
77
ComputationalResources = "ed09eef8-17a6-5b46-8889-db040fac31e3"

README.md

Lines changed: 127 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -153,14 +153,17 @@ begin, on the basis of the specific strategy and a user-specified
153153
measures](https://alan-turing-institute.github.io/MLJ.jl/dev/performance_measures/)
154154
for details.
155155

156-
- The *history* is a vector of tuples generated by the tuning
157-
algorithm - one tuple per iteration - used to determine the optimal
158-
model and which also records other user-inspectable statistics that
159-
may be of interest - for example, evaluations of a measure (loss or
160-
score) different from one being explicitly optimized. Each tuple is
161-
of the form `(m, r)`, where `m` is a model instance and `r` is
162-
information
163-
about `m` extracted from an evaluation.
156+
- The *history* is a vector of tuples of the form `(m, r)` generated
157+
by the tuning algorithm - one tuple per iteration - where `m` is a
158+
model instance that has been evaluated, and `r` (called the
159+
*result*) contains three kinds of information: (i) whatever parts of
160+
the evaluation needed to determine the optimal model; (ii)
161+
additional user-inspectable statistics that may be of interest - for
162+
example, evaluations of a measure (loss or score) different from one
163+
being explicitly optimized; and (iii) any model "metadata" that a
164+
tuning strategy implementation may need to be recorded for
165+
generating the next batch of model candidates - for example an
166+
implementation-specific representation of the model.
164167

165168
- A *tuning strategy* is an instance of some subtype `S <:
166169
TuningStrategy`, the name `S` (e.g., `Grid`) indicating the tuning
@@ -179,8 +182,12 @@ begin, on the basis of the specific strategy and a user-specified
179182
iteration count are given - and is essentially the space of models
180183
to be searched. This definition is intentionally broad and the
181184
interface places no restriction on the allowed types of this
182-
object. For the range objects supported by the `Grid` strategy, see
183-
[below](#range-types).
185+
object. It may be generally viewed as the "space" of models being
186+
searched *plus* strategy-specific data explaining how models from
187+
that space are actually to be generated (e.g.,
188+
hyperparameter-specific grid resolutions or probability
189+
distributions). For the range objects supported by the `Grid`
190+
strategy, see [below](#range-types).
184191

185192

186193
### Interface points for user input
@@ -242,7 +249,7 @@ Several functions are part of the tuning strategy API:
242249
- `tuning_report`: for selecting what to report to the user apart from
243250
details on the optimal model
244251

245-
- `default_n`: to specify the number of models to be evaluated when
252+
- `default_n`: to specify the total number of models to be evaluated when
246253
`n` is not specified by the user
247254

248255
**Important note on the history.** The initialization and update of the
@@ -316,19 +323,22 @@ which is recorded in its `field` attribute, but for composite models
316323
this might be a be a "nested name", such as `:(atom.max_depth)`.
317324

318325

319-
#### The `result` method: For declaring what parts of an evaluation goes into the history
326+
#### The `result` method: For building each entry of the history
320327

321328
```julia
322-
MLJTuning.result(tuning::MyTuningStrategy, history, e)
329+
MLJTuning.result(tuning::MyTuningStrategy, history, state, e, metadata)
323330
```
324331

325-
This method is for extracting from an evaluation `e` of some model `m`
326-
the value of `r` to be recorded in the corresponding tuple `(m, r)` of
327-
the history. The value of `r` is also allowed to depend on previous
328-
events in the history. The fallback is:
332+
This method is for constructing the result object `r` in each tuple
333+
`(m, r)` written to the history. Here `e` is the evaluation of the
334+
model `m` (as returned by a call to `evaluation!`) and `metadata` is
335+
any metadata associated with `m` when this is included in the output
336+
of `models!` (see below), and `nothing` otherwise. The value of `r` is
337+
also allowed to depend on previous events in the history. The fallback
338+
is:
329339

330340
```julia
331-
MLJTuning.result(tuning, history, e) = (measure=e.measure, measurement=e.measurement)
341+
MLJTuning.result(tuning, history, state, e, metadata) = (measure=e.measure, measurement=e.measurement)
332342
```
333343

334344
Note in this case that the result is always a named tuple of
@@ -350,18 +360,18 @@ state = setup(tuning::MyTuningStrategy, model, range, verbosity)
350360
```
351361

352362
The `setup` function is for initializing the `state` of the tuning
353-
algorithm (needed, by the algorithm's `models!` method; see below). Be
354-
sure to make this object mutable if it needs to be updated by the
355-
`models!` method. The `state` generally stores, at the least, the
356-
range or some processed version thereof. In momentum-based gradient
357-
descent, for example, the state would include the previous
358-
hyperparameter gradients, while in GP Bayesian optimization, it would
359-
store the (evolving) Gaussian processes.
360-
361-
If a variable is to be reported as part of the user-inspectable
362-
history, then it should be written to the history instead of stored in
363-
state. An example of this might be the `temperature` in simulated
364-
annealing.
363+
algorithm (available to the `models!` method). Be sure to make this
364+
object mutable if it needs to be updated by the `models!` method.
365+
366+
The `state` is a place to record the outcomes of any necessary
367+
intialization of the tuning algorithm (performed by `setup`) and a
368+
place for the `models!` method to save and read transient information
369+
that does not need to be recorded in the history.
370+
371+
The `setup` function is called once only, when a `TunedModel` machine
372+
is `fit!` the first time, and not on subsequent calls (unless
373+
`force=true`). (Specifically, `MLJBase.fit(::TunedModel, ...)` calls
374+
`setup` but `MLJBase.update(::TunedModel, ...)` does not.)
365375

366376
The `verbosity` is an integer indicating the level of logging: `0`
367377
means logging should be restricted to warnings, `-1`, means completely
@@ -411,17 +421,24 @@ selection of `n - length(history)` models from the grid, so that
411421
non-deterministically (such as simulated annealing), `models!` might
412422
return a single model, or return a small batch of models to make use
413423
of parallelization (the method becoming "semi-sequential" in that
414-
case). In sequential methods that generate new models
415-
deterministically (such as those choosing models that optimize the
416-
expected improvement of a surrogate statistical model) `models!` would
417-
return a single model.
424+
case).
425+
426+
##### Including model metadata
427+
428+
If a tuning strategy implementation needs to pass additional
429+
"metadata" along with each model, to be passed to `result` for
430+
recording in the history, then instead of model instances, `models!`
431+
should returne a vector of *tuples* of the form `(m, metadata)`, where
432+
`m` is a model instance, and `metadata` the associated data. See the
433+
discussion above on `result`.
418434

419435
If the tuning algorithm exhausts it's supply of new models (because,
420436
for example, there is only a finite supply) then `models!` should
421-
return an empty vector. Under the hood, there is no fixed "batch-size"
422-
parameter, and the tuning algorithm is happy to receive any number
423-
of models.
424-
437+
return an empty vector or `nothing`. Under the hood, there is no fixed
438+
"batch-size" parameter, and the tuning algorithm is happy to receive
439+
any number of models. If `models!` returns a number of models
440+
exceeding the number needed to complete the history, the list returned
441+
is simply truncated.
425442

426443
#### The `best` method: To define what constitutes the "optimal model"
427444

@@ -483,52 +500,94 @@ MLJTuning.tuning_report(tuning, history, state) = (history=history,)
483500
MLJTuning.default_n(tuning::MyTuningStrategy, range)
484501
```
485502

486-
The `methods!` method (which is allowed to return multiple models) is
487-
called until a history of length `n` has been built, or `models!`
488-
returns an empty list or `nothing`. If the user does not specify a
489-
value for `n` when constructing her `TunedModel` object, then `n` is
490-
set to `default_n(tuning, range)` at construction, where `range` is
491-
the user specified range.
503+
The `models!` method (which is allowed to return multiple models) is
504+
called until one of the following occurs:
505+
506+
- The length of the history matches the number of iterations specified
507+
by the user, namely `tuned_model.n` where `tuned_model` is the user's
508+
`TunedModel` instance. If `tuned_model.n` is `nothing` (because the
509+
user has not specified a value) then `default_n(tuning, range)` is
510+
used instead.
511+
512+
- `models!` returns an empty list or `nothing`.
492513

493514
The fallback is
494515

495516
```julia
496-
MLJTuning.default_n(::TuningStrategy, range) = 10
517+
default_n(tuning::TuningStrategy, range) = DEFAULT_N
497518
```
498519

520+
where `DEFAULT_N` is a global constant. Do `using MLJTuning;
521+
MLJTuning.DEFAULT_N` to see check the current value.
499522

500-
### Implementation example: Search through an explicit list
501523

502-
The most rudimentary tuning strategy just evaluates every model in a
503-
specified list of models sharing a common type, such lists
504-
constituting the only kind of supported range. (In this special case
505-
`range` is an arbitrary iterator of models, which are `Probabilistic`
506-
or `Deterministic`, according to the type of the prototype `model`,
507-
which is otherwise ignored.) The fallback implementations for `setup`,
508-
`result`, `best` and `report_history` suffice. In particular, there
509-
is not distinction between `range` and `state` in this case.
524+
### Implementation example: Search through an explicit list
510525

511-
Here's the complete implementation:
526+
The most rudimentary tuning strategy just evaluates every model
527+
generated by some iterator, such iterators constituting the only kind
528+
of supported range. The models generated must all have a common type
529+
and, in th implementation below, the type information is conveyed by
530+
the specified prototype `model` (which is otherwise ignored). The
531+
fallback implementations for `result`, `best` and `report_history`
532+
suffice.
512533

513534
```julia
514535

515-
import MLJBase
516-
517536
mutable struct Explicit <: TuningStrategy end
518537

538+
mutable struct ExplicitState{R,N}
539+
range::R
540+
next::Union{Nothing,N} # to hold output of `iterate(range)`
541+
end
542+
543+
ExplicitState(r::R, ::Nothing) where R = ExplicitState{R,Nothing}(r,nothing)
544+
ExplictState(r::R, n::N) where {R,N} = ExplicitState{R,Union{Nothing,N}}(r,n)
545+
546+
function MLJTuning.setup(tuning::Explicit, model, range, verbosity)
547+
next = iterate(range)
548+
return ExplicitState(range, next)
549+
end
550+
519551
# models! returns all available models in the range at once:
520-
MLJTuning.models!(tuning::Explicit, model, history::Nothing,
521-
state, verbosity) = state
522-
MLJTuning.models!(tuning::Explicit, model, history,
523-
state, verbosity) = state[length(history) + 1:end]
524-
525-
function MLJTuning.default_n(tuning::Explicit, range)
526-
try
527-
length(range)
528-
catch MethodError
529-
10
530-
end
552+
function MLJTuning.models!(tuning::Explicit,
553+
model,
554+
history,
555+
state,
556+
n_remaining,
557+
verbosity)
558+
559+
range, next = state.range, state.next
560+
561+
next === nothing && return nothing
562+
563+
m, s = next
564+
models = [m, ]
565+
566+
next = iterate(range, s)
567+
568+
i = 1 # current length of `models`
569+
while i < n_remaining
570+
next === nothing && break
571+
m, s = next
572+
push!(models, m)
573+
i += 1
574+
next = iterate(range, s)
575+
end
576+
577+
state.next = next
578+
579+
return models
580+
531581
end
582+
583+
function default_n(tuning::Explicit, range)
584+
try
585+
length(range)
586+
catch MethodError
587+
DEFAULT_N
588+
end
589+
end
590+
532591
```
533592

534593
For slightly less trivial example, see

src/MLJTuning.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ import ComputationalResources: CPU1, CPUProcesses,
2323
CPUThreads, AbstractResource
2424
using Random
2525

26+
27+
## CONSTANTS
28+
29+
const DEFAULT_N = 10
30+
31+
2632
## INCLUDE FILES
2733

2834
include("utilities.jl")

src/strategies/explicit.jl

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,54 @@
1-
mutable struct Explicit <: TuningStrategy end
1+
mutable struct Explicit <: TuningStrategy end
2+
3+
mutable struct ExplicitState{R,N}
4+
range::R # a model-generating iterator
5+
next::Union{Nothing,N} # to hold output of `iterate(range)`
6+
end
7+
8+
ExplicitState(r::R, ::Nothing) where R = ExplicitState{R,Nothing}(r,nothing)
9+
ExplictState(r::R, n::N) where {R,N} = ExplicitState{R,Union{Nothing,N}}(r,n)
10+
11+
function MLJTuning.setup(tuning::Explicit, model, range, verbosity)
12+
next = iterate(range)
13+
return ExplicitState(range, next)
14+
end
215

316
# models! returns all available models in the range at once:
4-
MLJTuning.models!(tuning::Explicit, model, history::Nothing,
5-
state, verbosity) = state
6-
MLJTuning.models!(tuning::Explicit, model, history,
7-
state, verbosity) = state[length(history) + 1:end]
17+
function MLJTuning.models!(tuning::Explicit,
18+
model,
19+
history,
20+
state,
21+
n_remaining,
22+
verbosity)
23+
24+
range, next = state.range, state.next
25+
26+
next === nothing && return nothing
27+
28+
m, s = next
29+
models = [m, ]
830

9-
function MLJTuning.default_n(tuning::Explicit, range)
31+
next = iterate(range, s)
32+
33+
i = 1 # current length of `models`
34+
while i < n_remaining
35+
next === nothing && break
36+
m, s = next
37+
push!(models, m)
38+
i += 1
39+
next = iterate(range, s)
40+
end
41+
42+
state.next = next
43+
44+
return models
45+
46+
end
47+
48+
function default_n(tuning::Explicit, range)
1049
try
1150
length(range)
1251
catch MethodError
13-
10
52+
DEFAULT_N
1453
end
1554
end
16-

src/strategies/grid.jl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,13 @@ function setup(tuning::Grid, model, user_range, verbosity)
104104

105105
end
106106

107-
MLJTuning.models!(tuning::Grid, model, history::Nothing,
108-
state, verbosity) = state.models
109-
MLJTuning.models!(tuning::Grid, model, history,
110-
state, verbosity) =
111-
state.models[length(history) + 1:end]
107+
MLJTuning.models!(tuning::Grid,
108+
model,
109+
history,
110+
state,
111+
n_remaining,
112+
verbosity) =
113+
state.models[_length(history) + 1:end]
112114

113115
function tuning_report(tuning::Grid, history, state)
114116

0 commit comments

Comments
 (0)