import pathlib
import typing
import numpy
from .. import core
from ..core import graph_criteria
from ..utils import helpers
from . import anomaly_space
from . import graph_scorers
from . import meta_ml
logger = helpers.make_logger(__name__)
ModelsDict = dict[str, dict[graph_scorers.GraphScorer, list[meta_ml.MetaMLModel]]]
ScorerData = dict[str, dict[graph_scorers.GraphScorer, numpy.ndarray]]
def data_from_graph(
cluster_scores: graph_scorers.ClusterScores,
labels: numpy.ndarray,
) -> tuple[numpy.ndarray, numpy.ndarray]:
clusters = list(cluster_scores.keys())
scores = list(cluster_scores.values())
train_x = numpy.zeros(shape=(len(clusters), 6), dtype=numpy.float32)
train_y = numpy.zeros(shape=(len(clusters),))
for i, (cluster, score) in enumerate(zip(clusters, scores)):
train_x[i] = cluster.ratios
y_true = numpy.asarray(labels[cluster.indices], dtype=numpy.float32)
loss = (
float(numpy.sqrt(numpy.mean(numpy.square(score - y_true))))
/ cluster.cardinality
)
train_y[i] = 1.0 - loss
return train_x, train_y
def save_models(path: pathlib.Path, meta_models: ModelsDict):
model_codes: list[tuple[list[str], str]] = [
model.extract_python(metric_name, scorer.name)
for metric_name, scorer_models in meta_models.items()
for scorer, models in scorer_models.items()
for model in models
]
[import_lines_list, function_codes] = list(zip(*model_codes))
import_lines: list[str] = list(
{lines for import_lines in import_lines_list for lines in import_lines},
)
with open(path, "w") as writer:
writer.writelines(import_lines)
writer.write("\n")
for code in function_codes:
writer.write(f"\n\n{code}\n")
def train_meta_ml(
*,
spaces_criteria: list[
tuple[anomaly_space.AnomalySpace, list[core.ClusterCriterion]]
],
models_kwargs: list[tuple[type[meta_ml.MetaMLModel], dict[str, typing.Any]]],
scorers: list[graph_scorers.GraphScorer], out_dir: pathlib.Path,
num_epochs: int = 10,
save_frequency: int = 1,
only_train_fast_scorers: bool = False,
) -> pathlib.Path:
roots = []
for space, criteria in spaces_criteria:
logger.info(f"Building root cluster for {space.name} ...")
root = (
core.Cluster.new_root(space)
.build()
.iterative_partition(criteria)
.normalize_ratios("gaussian")
)
roots.append(root)
metric_names = list({s.distance_metric.name for s, _ in spaces_criteria})
meta_models: ModelsDict = {
metric_name: {
scorer: [model(**kwargs) for model, kwargs in models_kwargs]
for scorer in scorers
}
for metric_name in metric_names
}
full_train_x: ScorerData = {
metric_name: {scorer: None for scorer in scorers}
for metric_name in metric_names
}
full_train_y: ScorerData = {
metric_name: {scorer: None for scorer in scorers}
for metric_name in metric_names
}
for epoch in range(1, num_epochs + 1):
logger.info(f"Starting Epoch {epoch}/{num_epochs} ...")
for root, (space, _) in zip(roots, spaces_criteria):
root_name = root.metric_space.name
logger.info(f"Epoch {epoch}/{num_epochs}: Using root {root_name} ...")
metric_name = space.distance_metric.name
labels = space.data.labels
graphs: dict[graph_scorers.GraphScorer, list[core.Graph]]
if epoch == 1:
graphs_list = [
core.Graph(graph_criteria.Layer(d)(root)).build()
for d in range(5, root.max_leaf_depth, 5)
]
graphs = {scorer: graphs_list for scorer in scorers}
else:
graphs = {
scorer: [
core.Graph(
graph_criteria.MetaMLSelect(
lambda ratios: model.predict(ratios[None, :]),
name=model.name,
)(root),
).build()
for model in models
]
for scorer, models in meta_models[metric_name].items()
}
for scorer in scorers:
logger.info(
f"Epoch {epoch}/{num_epochs}: Using root "
f"{root_name} and scorer {scorer.name} ...",
)
for graph in graphs[scorer]:
if only_train_fast_scorers and (not scorer.should_be_fast(graph)):
continue
(cluster_scores, _) = scorer(graph)
train_x, train_y = data_from_graph(cluster_scores, labels)
if epoch == 1:
full_train_x[metric_name][scorer] = train_x
full_train_y[metric_name][scorer] = train_y
else:
full_train_x[metric_name][scorer] = numpy.concatenate(
[full_train_x[metric_name][scorer], train_x],
axis=0,
)
full_train_y[metric_name][scorer] = numpy.concatenate(
[full_train_y[metric_name][scorer], train_y],
axis=0,
)
new_models = []
for model in meta_models[metric_name][scorer]:
logger.info(
f"Epoch {epoch}/{num_epochs}: Fitting model "
f"{model.name}, scorer {scorer.name} with root "
f"{root_name} and scorer {scorer.name} ...",
)
new_models.append(
model.fit(
full_train_x[metric_name][scorer],
full_train_y[metric_name][scorer],
),
)
meta_models[metric_name][scorer] = new_models
if epoch % save_frequency == 0:
logger.info(f"Saving models after epoch {epoch}/{num_epochs} ...")
save_models(out_dir.joinpath(f"models_epoch_{epoch}.py"), meta_models)
final_path = out_dir.joinpath("models_final.py")
save_models(final_path, meta_models)
return final_path
__all__ = [
"train_meta_ml",
]