MLJ.jl icon indicating copy to clipboard operation
MLJ.jl copied to clipboard

Extract probability for a tresholded model

Open tpoisot opened this issue 3 years ago • 5 comments

When calling predict on a BinaryThresholdPredictor, is there a specific incantation that can give the probability of the positive class? Is there a way to "unwrap" the underlying model?

tpoisot avatar Nov 04 '22 18:11 tpoisot

No, I think if you want to get probabilisitic predictions for the original (atomic) model, then you need to train the atomic model on the data. You can't eat your cake and have it too.

I don't know your use-case, but I suppose you could roll your own wrapper that preserves the behaviour of predict (keeps it probabilistic) but overloads predict_mode to do the thresholding. You can specify predict_mode as the operation in things like evaluate! and TunedModel.

using MLJ
import CategoricalDistributions

mutable struct Thresholder <: ProbabilisticComposite
    model
    threshold::Float64
end

# to do the thresholding:
function deterministic(y_probabilistic, threshold)
    classes = CategoricalDistributions.classes(y_probabilistic)
    map(pdf.(y_probabilistic, classes[2]) .> threshold) do above
        above ? classes[2] : classes[1]
    end
end

function MLJ.fit(thresholder::Thresholder, verbosity, X, y)
    Xs = source(X)
    ys = source(y)
    mach = machine(thresholder.model, Xs, ys)
    y_probabilistic = predict(mach, Xs)
    y_deterministic = node(y -> deterministic(y, thresholder.threshold), y_probabilistic)

    network_machine = machine(Probabilistic(), Xs, ys;
                              predict=y_probabilistic,
                              predict_mode=y_deterministic)

    return!(network_machine, thresholder, verbosity)
end

X, y = make_moons()
DecisionTreeClassifier = @load DecisionTreeClassifier pkg=DecisionTree
thresholder = Thresholder(DecisionTreeClassifier(), 0.7)

mach = machine(thresholder, X, y) |> fit!
predict(mach, X)      # probabilistic predictions
predict_mode(mach, X) # thresholded predictions

julia> evaluate!(
       mach, 
       operation=[predict_mode, predict], 
       measure=[accuracy, log_loss],
       resampling=CV()
       )
Evaluating over 6 folds: 100%[=========================] Time: 0:00:00
PerformanceEvaluation object with these fields:
  measure, operation, measurement, per_fold,
  per_observation, fitted_params_per_fold,
  report_per_fold, train_test_rows
Extract:
┌────────────────────────────────┬──────────────┬─────────────┬─────────┬───────────────────
│ measure                        │ operation    │ measurement │ 1.96*SE │ per_fold         ⋯
├────────────────────────────────┼──────────────┼─────────────┼─────────┼───────────────────
│ Accuracy()                     │ predict_mode │ 0.967       │ 0.0345  │ [0.96, 0.92, 0.9 ⋯
│ LogLoss(                       │ predict      │ 1.2         │ 1.24    │ [1.44, 2.88, 2.8 ⋯
│   tol = 2.220446049250313e-16) │              │             │         │                  ⋯
└────────────────────────────────┴──────────────┴─────────────┴─────────┴───────────────────
                                                                            1 column omitted

ablaom avatar Nov 06 '22 21:11 ablaom

(After https://github.com/JuliaAI/MLJBase.jl/pull/853, the preferred way of exporting the learning network is to replace MLJ.fit definition above with the simpler

function MLJ.prefit(thresholder::Thresholder, verbosity, X, y)
    Xs = source(X)
    ys = source(y)
    mach = machine(:model, Xs, ys)
    y_probabilistic = predict(mach, Xs)
    y_deterministic = node(y -> deterministic(y, thresholder.threshold), y_probabilistic)

    (predict=y_probabilistic, predict_mode=y_deterministic)
end

and change the subtyping Thresholder <: ProbabilisticComposite to Thresholder <: ProbabilisticNetworkComposite.)

ablaom avatar Nov 06 '22 21:11 ablaom

No, I think if you want to get probabilisitic predictions for the original (atomic) model, then you need to train the atomic model on the data. You can't eat your cake and have it too.

In the specific case I used to explore this, the atomic model was wrapped into a BinaryThresholdPredictor, and then into a TunedModel to optimize the threshold -- so I think (if I understand correctly what TunedModel and BinaryThresholdPredictor do from the documentation) that the atomic model is trained.

tpoisot avatar Nov 07 '22 01:11 tpoisot

Yes, it is trained, but the public API doesn't give you a way to produce probabilistic predictions. It is possible to get them without retraining, but it's a hack (non-public API).

ablaom avatar Nov 07 '22 01:11 ablaom

Got it, thanks!!

tpoisot avatar Nov 07 '22 02:11 tpoisot

Closing as question resolved.

ablaom avatar Jun 09 '24 22:06 ablaom