Skip to content

Commit 6ab6c66

Browse files
authored
Merge pull request #31 from JuliaAI/dev
For a 0.4.0 release
2 parents 43f74c1 + e63eadc commit 6ab6c66

5 files changed

Lines changed: 93 additions & 70 deletions

File tree

Project.toml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,41 @@
11
name = "MLJEnsembles"
22
uuid = "50ed68f4-41fd-4504-931a-ed422449fee0"
33
authors = ["Anthony D. Blaom <anthony.blaom@gmail.com>"]
4-
version = "0.3.3"
4+
version = "0.4.0"
55

66
[deps]
77
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
88
CategoricalDistributions = "af321ab8-2d2e-40a6-b165-3d674595d28e"
99
ComputationalResources = "ed09eef8-17a6-5b46-8889-db040fac31e3"
1010
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
1111
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
12-
MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d"
1312
MLJModelInterface = "e80e1ace-859a-464e-9ed9-23947d8ae3ea"
1413
ProgressMeter = "92933f4c-e287-5a05-a399-4b506db050ca"
1514
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1615
ScientificTypesBase = "30f210dd-8aff-4c5f-94ba-8e64358c1161"
16+
StatisticalMeasuresBase = "c062fc1d-0d66-479b-b6ac-8b44719de4cc"
1717
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
1818

1919
[compat]
2020
CategoricalArrays = "0.8, 0.9, 0.10"
2121
CategoricalDistributions = "0.1.2"
2222
ComputationalResources = "0.3"
2323
Distributions = "0.21, 0.22, 0.23, 0.24, 0.25"
24-
MLJBase = "0.20, 0.21"
2524
MLJModelInterface = "0.4.1, 1.1"
2625
ProgressMeter = "1.1"
2726
ScientificTypesBase = "2,3"
27+
StatisticalMeasuresBase = "0.1"
2828
StatsBase = "0.32, 0.33, 0.34"
2929
julia = "1.6"
30+
31+
[extras]
32+
Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
33+
MLJBase = "a7f614a8-145f-11e9-1d2a-a57a1082229d"
34+
NearestNeighbors = "b8a86587-4115-5ab1-83bc-aa920d37bbce"
35+
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
36+
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
37+
StatisticalMeasures = "a19d573c-0a75-4610-95b3-7071388c7541"
38+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
39+
40+
[targets]
41+
test = ["Distances", "MLJBase", "NearestNeighbors", "Serialization", "StableRNGs", "StatisticalMeasures", "Test"]

src/MLJEnsembles.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ module MLJEnsembles
22

33
using MLJModelInterface
44
import MLJModelInterface: predict, fit, save, restore
5-
import MLJBase # still needed for aggregating measures in oob-estimates of error
65
using Random
76
using CategoricalArrays
87
using CategoricalDistributions
@@ -11,6 +10,7 @@ using Distributed
1110
import Distributions
1211
using ProgressMeter
1312
import StatsBase
13+
import StatisticalMeasuresBase
1414

1515
export EnsembleModel
1616

src/ensembles.jl

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,10 @@ If a single measure or non-empty vector of measures is specified by
321321
written to the training report (call `report` on the trained
322322
machine wrapping the ensemble model).
323323
324-
*Important:* If sample weights `w` (not to be confused with atomic
325-
weights) are specified when constructing a machine for the ensemble
326-
model, as in `mach = machine(ensemble_model, X, y, w)`, then `w` is
327-
used by any measures specified in `out_of_bag_measure` that support
328-
sample weights.
324+
*Important:* If per-observation or class weights `w` (not to be confused with atomic
325+
weights) are specified when constructing a machine for the ensemble model, as in `mach =
326+
machine(ensemble_model, X, y, w)`, then `w` is used by any measures specified in
327+
`out_of_bag_measure` that support them.
329328
330329
"""
331330
function EnsembleModel(
@@ -395,34 +394,56 @@ function _fit(res::CPUProcesses, func, verbosity, stuff)
395394
if i != nworkers()
396395
func(atom, 0, chunk_size, n_patterns, n_train, rng, progress_meter, args...)
397396
else
398-
func(atom, 0, chunk_size + left_over, n_patterns, n_train, rng, progress_meter, args...)
397+
func(
398+
atom,
399+
0,
400+
chunk_size + left_over,
401+
n_patterns,
402+
n_train,
403+
rng,
404+
progress_meter,
405+
args...,
406+
)
399407
end
400408
end
401409
end
402410

403-
@static if VERSION >= v"1.3.0-DEV.573"
404-
function _fit(res::CPUThreads, func, verbosity, stuff)
405-
atom, n, n_patterns, n_train, rng, progress_meter, args = stuff
406-
if verbosity > 0
407-
println("Ensemble-building in parallel on $(Threads.nthreads()) threads.")
408-
end
409-
nthreads = Threads.nthreads()
410-
chunk_size = div(n, nthreads)
411-
left_over = mod(n, nthreads)
412-
resvec = Vector(undef, nthreads) # FIXME: Make this type-stable?
413-
414-
Threads.@threads for i = 1:nthreads
415-
resvec[i] = if i != nworkers()
416-
func(atom, 0, chunk_size, n_patterns, n_train, rng, progress_meter, args...)
417-
else
418-
func(atom, 0, chunk_size + left_over, n_patterns, n_train, rng, progress_meter, args...)
419-
end
420-
end
411+
function _fit(res::CPUThreads, func, verbosity, stuff)
412+
atom, n, n_patterns, n_train, rng, progress_meter, args = stuff
413+
if verbosity > 0
414+
println("Ensemble-building in parallel on $(Threads.nthreads()) threads.")
415+
end
416+
nthreads = Threads.nthreads()
417+
chunk_size = div(n, nthreads)
418+
left_over = mod(n, nthreads)
419+
resvec = Vector(undef, nthreads) # FIXME: Make this type-stable?
421420

422-
return reduce(_reducer, resvec)
421+
Threads.@threads for i = 1:nthreads
422+
resvec[i] = if i != nworkers()
423+
func(atom, 0, chunk_size, n_patterns, n_train, rng, progress_meter, args...)
424+
else
425+
func(
426+
atom,
427+
0,
428+
chunk_size + left_over,
429+
n_patterns,
430+
n_train,
431+
rng,
432+
progress_meter,
433+
args...,
434+
)
435+
end
423436
end
437+
438+
return reduce(_reducer, resvec)
424439
end
425440

441+
# for subsampling weights, which could be `nothing`, per-observation weights, or
442+
# class_weights:
443+
_view(class_weights::AbstractDict, rows) = class_weights
444+
_view(::Nothing, rows) = nothing
445+
_view(weights, rows) = view(weights, rows)
446+
426447
function MMI.fit(
427448
model::EitherEnsembleModel{Atom}, verbosity::Int, args...
428449
) where Atom<:Supervised
@@ -446,10 +467,14 @@ function MMI.fit(
446467
acceleration = CPU1()
447468
end
448469

470+
# we wrap the measures in `robust_measure` so they can be called with weights, even
471+
# when they don't support them, and just ignore them silently.
449472
if model.out_of_bag_measure isa Vector
450-
out_of_bag_measure = model.out_of_bag_measure
473+
out_of_bag_measure =
474+
StatisticalMeasuresBase.robust_measure.(model.out_of_bag_measure)
451475
else
452-
out_of_bag_measure = [model.out_of_bag_measure,]
476+
out_of_bag_measure =
477+
[StatisticalMeasuresBase.robust_measure(model.out_of_bag_measure),]
453478
end
454479

455480
if model.rng isa Integer
@@ -484,7 +509,7 @@ function MMI.fit(
484509

485510
if !isempty(out_of_bag_measure)
486511

487-
metrics=zeros(length(ensemble),length(out_of_bag_measure))
512+
measurements=zeros(length(ensemble),length(out_of_bag_measure))
488513
for i= 1:length(ensemble)
489514
#oob indices
490515
ooB_indices= setdiff(1:n_patterns, ensemble_indices[i])
@@ -493,42 +518,44 @@ function MMI.fit(
493518
"Data size too small or "*
494519
"bagging_fraction too close to 1.0. ")
495520
end
496-
yhat = predict(atom, ensemble[i], selectrows(atom, ooB_indices, atom_specific_X)...)
521+
yhat = predict(
522+
atom,
523+
ensemble[i],
524+
selectrows(atom, ooB_indices, atom_specific_X)...,
525+
)
497526
Xtest = selectrows(X, ooB_indices)
498527
ytest = selectrows(y, ooB_indices)
499528

500-
if w === nothing
501-
wtest = nothing
502-
else
503-
wtest = selectrows(w, ooB_indices)
504-
end
529+
# this could be class weights OR per-observation weights, OR `nothing`:
530+
wtest = _view(w, ooB_indices)
505531

506532
for k in eachindex(out_of_bag_measure)
507533
m = out_of_bag_measure[k]
508-
if MMI.reports_each_observation(m)
509-
s = MLJBase.aggregate(
510-
MLJBase.value(m, yhat, Xtest, ytest, wtest),
511-
m
512-
)
513-
else
514-
s = MLJBase.value(m, yhat, Xtest, ytest, wtest)
515-
end
516-
metrics[i,k] = s
534+
s = m(yhat, ytest, wtest)
535+
measurements[i,k] = s
517536
end
518537
end
519538

520-
# aggregate metrics across the ensembles:
521-
aggregated_metrics = map(eachindex(out_of_bag_measure)) do k
522-
MLJBase.aggregate(metrics[:,k], out_of_bag_measure[k])
539+
# aggregate measurements across the ensembles:
540+
aggregated_measurements = map(eachindex(out_of_bag_measure)) do k
541+
StatisticalMeasuresBase.aggregate(
542+
measurements[:,k],
543+
mode=StatisticalMeasuresBase.external_aggregation_mode(
544+
out_of_bag_measure[k],
545+
)
546+
)
523547
end
524548

525549
names = Symbol.(string.(out_of_bag_measure))
526550

527551
else
528-
aggregated_metrics = missing
552+
aggregated_measurements = missing
529553
end
530554

531-
report=(measures=out_of_bag_measure, oob_measurements=aggregated_metrics,)
555+
report=(
556+
measures=out_of_bag_measure,
557+
oob_measurements=aggregated_measurements,
558+
)
532559
cache = deepcopy(model)
533560

534561
return fitresult, cache, report
@@ -542,7 +569,7 @@ function MMI.update(model::EitherEnsembleModel,
542569

543570
n = model.n
544571

545-
if MLJBase.is_same_except(model.model, old_model.model,
572+
if MMI.is_same_except(model.model, old_model.model,
546573
:n, :atomic_weights, :acceleration)
547574
if n > old_model.n
548575
verbosity < 1 ||

test/Project.toml

Lines changed: 0 additions & 16 deletions
This file was deleted.

test/ensembles.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ using MLJBase
88
using ..Models
99
using CategoricalArrays
1010
import Distributions
11-
11+
using StatisticalMeasures
1212

1313
## HELPER FUNCTIONS
1414

0 commit comments

Comments
 (0)