Latu.ML.CrossValidator (latu_ml v0.2.0)

Copy Markdown View Source

K-fold cross-validation: fit every param map on every fold, and keep the best.

Not a server operator. Spark has no Fit for a validator — MlCommand.Fit takes an MlOperator and the Connect server exposes none for CrossValidator — so the search is a fold this package runs, as it is a loop PySpark runs. What reaches the server is one Fit, one Transform and one Evaluate per fold per param map, and one last Fit for the winner.

grid = ML.param_grid(lr, reg_param: [0.1, 0.01], max_iter: [10, 100])

cv =
  ML.cross_validator(
    estimator: lr,
    param_maps: grid,
    evaluator: Evaluation.binary_classification_evaluator(),
    num_folds: 3
  )

{:ok, model} = ML.fit(cv, training)

What it costs

num_folds * length(param_maps) fits, plus one. Each is a cache entry on the server while it is being scored, and Latu.ML.fit/2 deletes it as soon as its metric is read — so a 3-by-4 search holds one model at a time rather than twelve. collect_sub_models: true keeps them all instead, and they become yours to Latu.ML.delete/1.

The folds

A fold is cut by ranking rows on rand(seed) and taking a range of it, which is Spark's own way and not a random_split. Pass seed: for a cut that is the same twice. fold_col: names a column that already holds a fold number instead, in which case seed and num_folds only have to agree with it.

Summary

Types

What is being searched: one estimator, or a pipeline with one inside it.

t()

Types

searchable()

@type searchable() :: Latu.ML.Estimator.t() | Latu.ML.Pipeline.t()

What is being searched: one estimator, or a pipeline with one inside it.

t()

@type t() :: %Latu.ML.CrossValidator{
  collect_sub_models: boolean(),
  estimator: searchable(),
  evaluator: Latu.ML.Evaluator.t(),
  fold_col: String.t() | nil,
  num_folds: pos_integer(),
  param_maps: [Latu.ML.param_map()],
  seed: integer() | nil,
  uid: String.t()
}