dc6079821b
CI Github Actions / Run Tests (push) Waiting to run
Automatic PR Merger / mergepal (push) Waiting to run
Docs Tests / Check for file changes (push) Waiting to run
Docs Tests / Test Documentation (push) Blocked by required conditions
Docs Tests / Documentation Linting Checks (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Blocked by required conditions
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Blocked by required conditions
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Blocked by required conditions
Continuous Integration / Check for file changes (push) Waiting to run
Continuous Integration / Wait for docs tests (push) Blocked by required conditions
Continuous Integration / Code Quality (push) Blocked by required conditions
Continuous Integration / Check for changelog (push) Waiting to run
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Blocked by required conditions
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Blocked by required conditions
Continuous Integration / Upload coverage reports to codeclimate (push) Blocked by required conditions
Continuous Integration / Run Non-Sequential Integration Tests (push) Blocked by required conditions
Continuous Integration / Run Broker Integration Tests (push) Blocked by required conditions
Continuous Integration / Run Sequential Integration Tests (push) Blocked by required conditions
Continuous Integration / Build Docker base images and setup environment (push) Waiting to run
Continuous Integration / Build Docker (default) (push) Blocked by required conditions
Continuous Integration / Build Docker (full) (push) Blocked by required conditions
Continuous Integration / Build Docker (mitie-en) (push) Blocked by required conditions
Continuous Integration / Build Docker (spacy-de) (push) Blocked by required conditions
Continuous Integration / Build Docker (spacy-en) (push) Blocked by required conditions
Continuous Integration / Build Docker (spacy-it) (push) Blocked by required conditions
Continuous Integration / Deploy to PyPI (push) Blocked by required conditions
Continuous Integration / Notify Slack & Publish Release Notes (push) Blocked by required conditions
Publish Documentation / Evaluate release tag (push) Waiting to run
Publish Documentation / Prebuild Docs (push) Blocked by required conditions
Publish Documentation / Preview Docs (push) Blocked by required conditions
Publish Documentation / Check for file changes (push) Waiting to run
Publish Documentation / Publish Docs (push) Waiting to run
Semgrep / Semgrep Workflow Security Scan (push) Waiting to run
460 lines
15 KiB
Python
460 lines
15 KiB
Python
import copy
|
|
import logging
|
|
import os
|
|
from typing import (
|
|
Text,
|
|
Dict,
|
|
Optional,
|
|
List,
|
|
Any,
|
|
Iterable,
|
|
Tuple,
|
|
Union,
|
|
)
|
|
from pathlib import Path
|
|
|
|
from rasa.core.agent import Agent
|
|
from rasa.engine.storage.local_model_storage import LocalModelStorage
|
|
import rasa.shared.utils.cli
|
|
import rasa.shared.utils.common
|
|
import rasa.shared.utils.io
|
|
import rasa.utils.common
|
|
from rasa.constants import RESULTS_FILE, NUMBER_OF_TRAINING_STORIES_FILE
|
|
from rasa.exceptions import ModelNotFound
|
|
from rasa.shared.constants import DEFAULT_RESULTS_PATH
|
|
import rasa.shared.nlu.training_data.loading
|
|
from rasa.shared.data import TrainingType
|
|
from rasa.shared.nlu.training_data.training_data import TrainingData
|
|
import rasa.model
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ClassificationReportException(Exception):
|
|
"""Raised when clf_report doesn't correctly set accuracy and/or micro avg.
|
|
|
|
sklearn.metrics.classification_report should provide either accuracy or micro avg.
|
|
"""
|
|
|
|
|
|
async def test_core_models_in_directory(
|
|
model_directory: Text,
|
|
stories: Text,
|
|
output: Text,
|
|
use_conversation_test_files: bool = False,
|
|
) -> None:
|
|
"""Evaluates a directory with multiple Core models using test data.
|
|
|
|
Args:
|
|
model_directory: Directory containing multiple model files.
|
|
stories: Path to a conversation test file.
|
|
output: Output directory to store results to.
|
|
use_conversation_test_files: `True` if conversation test files should be used
|
|
for testing instead of regular Core story files.
|
|
"""
|
|
from rasa.core.test import compare_models_in_dir
|
|
|
|
model_directory = _get_sanitized_model_directory(model_directory)
|
|
|
|
await compare_models_in_dir(
|
|
model_directory,
|
|
stories,
|
|
output,
|
|
use_conversation_test_files=use_conversation_test_files,
|
|
)
|
|
|
|
story_n_path = os.path.join(model_directory, NUMBER_OF_TRAINING_STORIES_FILE)
|
|
number_of_stories = rasa.shared.utils.io.read_json_file(story_n_path)
|
|
plot_core_results(output, number_of_stories)
|
|
|
|
|
|
def plot_core_results(output_directory: Text, number_of_examples: List[int]) -> None:
|
|
"""Plot core model comparison graph.
|
|
|
|
Args:
|
|
output_directory: path to the output directory
|
|
number_of_examples: number of examples per run
|
|
"""
|
|
import rasa.utils.plotting as plotting_utils
|
|
|
|
graph_path = os.path.join(output_directory, "core_model_comparison_graph.pdf")
|
|
|
|
plotting_utils.plot_curve(
|
|
output_directory,
|
|
number_of_examples,
|
|
x_label_text="Number of stories present during training",
|
|
y_label_text="Number of correct test stories",
|
|
graph_path=graph_path,
|
|
)
|
|
|
|
|
|
def _get_sanitized_model_directory(model_directory: Text) -> Text:
|
|
"""Adjusts the `--model` argument of `rasa test core` when called with
|
|
`--evaluate-model-directory`.
|
|
|
|
By default rasa uses the latest model for the `--model` parameter. However, for
|
|
`--evaluate-model-directory` we need a directory. This function checks if the
|
|
passed parameter is a model or an individual model file.
|
|
|
|
Args:
|
|
model_directory: The model_directory argument that was given to
|
|
`test_core_models_in_directory`.
|
|
|
|
Returns: The adjusted model_directory that should be used in
|
|
`test_core_models_in_directory`.
|
|
"""
|
|
|
|
p = Path(model_directory)
|
|
if p.is_file():
|
|
if model_directory != rasa.model.get_latest_model():
|
|
rasa.shared.utils.cli.print_warning(
|
|
"You passed a file as '--model'. Will use the directory containing "
|
|
"this file instead."
|
|
)
|
|
model_directory = str(p.parent)
|
|
|
|
return model_directory
|
|
|
|
|
|
async def test_core_models(
|
|
models: List[Text],
|
|
stories: Text,
|
|
output: Text,
|
|
use_conversation_test_files: bool = False,
|
|
) -> None:
|
|
"""Compares multiple Core models based on test data.
|
|
|
|
Args:
|
|
models: A list of models files.
|
|
stories: Path to test data.
|
|
output: Path to output directory for test results.
|
|
use_conversation_test_files: `True` if conversation test files should be used
|
|
for testing instead of regular Core story files.
|
|
"""
|
|
from rasa.core.test import compare_models
|
|
|
|
await compare_models(
|
|
models, stories, output, use_conversation_test_files=use_conversation_test_files
|
|
)
|
|
|
|
|
|
async def test_core(
|
|
model: Optional[Text] = None,
|
|
stories: Optional[Text] = None,
|
|
output: Text = DEFAULT_RESULTS_PATH,
|
|
additional_arguments: Optional[Dict] = None,
|
|
use_conversation_test_files: bool = False,
|
|
) -> None:
|
|
"""Tests a trained Core model against a set of test stories."""
|
|
try:
|
|
model = rasa.model.get_local_model(model)
|
|
except ModelNotFound:
|
|
rasa.shared.utils.cli.print_error(
|
|
"Unable to test: could not find a model. Use 'rasa train' to train a "
|
|
"Rasa model and provide it via the '--model' argument."
|
|
)
|
|
return
|
|
|
|
metadata = LocalModelStorage.metadata_from_archive(model)
|
|
if metadata.training_type == TrainingType.NLU:
|
|
rasa.shared.utils.cli.print_error(
|
|
"Unable to test: no core model found. Use 'rasa train' to train a "
|
|
"Rasa model and provide it via the '--model' argument."
|
|
)
|
|
elif metadata.training_type == TrainingType.CORE and use_conversation_test_files:
|
|
rasa.shared.utils.cli.print_warning(
|
|
"No NLU model found. Using default 'RegexMessageHandler' for end-to-end "
|
|
"evaluation. If you added actual user messages to your test stories "
|
|
"this will likely lead to the tests failing. In that case, you need "
|
|
"to train a NLU model first, e.g. using `rasa train`."
|
|
)
|
|
|
|
if additional_arguments is None:
|
|
additional_arguments = {}
|
|
|
|
if output:
|
|
rasa.shared.utils.io.create_directory(output)
|
|
|
|
_agent = Agent.load(model_path=model)
|
|
|
|
if not _agent.is_ready():
|
|
rasa.shared.utils.cli.print_error(
|
|
"Unable to test: processor not loaded. Use 'rasa train' to train a "
|
|
"Rasa model and provide it via the '--model' argument."
|
|
)
|
|
return
|
|
|
|
from rasa.core.test import test as core_test
|
|
|
|
kwargs = rasa.shared.utils.common.minimal_kwargs(
|
|
additional_arguments, core_test, ["stories", "agent", "e2e"]
|
|
)
|
|
|
|
await core_test(
|
|
stories, _agent, e2e=use_conversation_test_files, out_directory=output, **kwargs
|
|
)
|
|
|
|
|
|
async def test_nlu(
|
|
model: Optional[Text],
|
|
nlu_data: Optional[Text],
|
|
output_directory: Text = DEFAULT_RESULTS_PATH,
|
|
additional_arguments: Optional[Dict] = None,
|
|
domain_path: Optional[Text] = None,
|
|
) -> None:
|
|
"""Tests the NLU Model."""
|
|
from rasa.nlu.test import run_evaluation
|
|
|
|
rasa.shared.utils.io.create_directory(output_directory)
|
|
|
|
try:
|
|
model = rasa.model.get_local_model(model)
|
|
except ModelNotFound:
|
|
rasa.shared.utils.cli.print_error(
|
|
"Could not find any model. Use 'rasa train nlu' to train a "
|
|
"Rasa model and provide it via the '--model' argument."
|
|
)
|
|
return
|
|
|
|
metadata = LocalModelStorage.metadata_from_archive(model)
|
|
|
|
if os.path.exists(model) and metadata.training_type != TrainingType.CORE:
|
|
kwargs = rasa.shared.utils.common.minimal_kwargs(
|
|
additional_arguments, run_evaluation, ["data_path", "model"]
|
|
)
|
|
_agent = Agent.load(model_path=model)
|
|
await run_evaluation(
|
|
nlu_data,
|
|
_agent.processor,
|
|
output_directory=output_directory,
|
|
domain_path=domain_path,
|
|
**kwargs,
|
|
)
|
|
else:
|
|
rasa.shared.utils.cli.print_error(
|
|
"Could not find any model. Use 'rasa train nlu' to train a "
|
|
"Rasa model and provide it via the '--model' argument."
|
|
)
|
|
|
|
|
|
async def compare_nlu_models(
|
|
configs: List[Text],
|
|
test_data: TrainingData,
|
|
output: Text,
|
|
runs: int,
|
|
exclusion_percentages: List[int],
|
|
) -> None:
|
|
"""Trains multiple models, compares them and saves the results."""
|
|
|
|
from rasa.nlu.test import drop_intents_below_freq
|
|
from rasa.nlu.utils import write_json_to_file
|
|
from rasa.utils.io import create_path
|
|
from rasa.nlu.test import compare_nlu
|
|
|
|
test_data = drop_intents_below_freq(test_data, cutoff=5)
|
|
|
|
create_path(output)
|
|
|
|
bases = [os.path.basename(nlu_config) for nlu_config in configs]
|
|
model_names = [os.path.splitext(base)[0] for base in bases]
|
|
|
|
f1_score_results: Dict[Text, List[List[float]]] = {
|
|
model_name: [[] for _ in range(runs)] for model_name in model_names
|
|
}
|
|
|
|
training_examples_per_run = await compare_nlu(
|
|
configs,
|
|
test_data,
|
|
exclusion_percentages,
|
|
f1_score_results,
|
|
model_names,
|
|
output,
|
|
runs,
|
|
)
|
|
|
|
f1_path = os.path.join(output, RESULTS_FILE)
|
|
write_json_to_file(f1_path, f1_score_results)
|
|
|
|
plot_nlu_results(output, training_examples_per_run)
|
|
|
|
|
|
def plot_nlu_results(output_directory: Text, number_of_examples: List[int]) -> None:
|
|
"""Plot NLU model comparison graph.
|
|
|
|
Args:
|
|
output_directory: path to the output directory
|
|
number_of_examples: number of examples per run
|
|
"""
|
|
import rasa.utils.plotting as plotting_utils
|
|
|
|
graph_path = os.path.join(output_directory, "nlu_model_comparison_graph.pdf")
|
|
|
|
plotting_utils.plot_curve(
|
|
output_directory,
|
|
number_of_examples,
|
|
x_label_text="Number of intent examples present during training",
|
|
y_label_text="Label-weighted average F1 score on test set",
|
|
graph_path=graph_path,
|
|
)
|
|
|
|
|
|
async def perform_nlu_cross_validation(
|
|
config: Dict[Text, Any],
|
|
data: TrainingData,
|
|
output: Text,
|
|
additional_arguments: Optional[Dict[Text, Any]],
|
|
) -> None:
|
|
"""Runs cross-validation on test data.
|
|
|
|
Args:
|
|
config: The model configuration.
|
|
data: The data which is used for the cross-validation.
|
|
output: Output directory for the cross-validation results.
|
|
additional_arguments: Additional arguments which are passed to the
|
|
cross-validation, like number of `disable_plotting`.
|
|
"""
|
|
from rasa.nlu.test import (
|
|
drop_intents_below_freq,
|
|
cross_validate,
|
|
log_results,
|
|
log_entity_results,
|
|
)
|
|
|
|
additional_arguments = additional_arguments or {}
|
|
folds = int(additional_arguments.get("folds", 3))
|
|
|
|
data = drop_intents_below_freq(data, cutoff=folds)
|
|
kwargs = rasa.shared.utils.common.minimal_kwargs(
|
|
additional_arguments, cross_validate
|
|
)
|
|
|
|
results, entity_results, response_selection_results = await cross_validate(
|
|
data, folds, config, output, **kwargs
|
|
)
|
|
logger.info(f"CV evaluation (n={folds})")
|
|
|
|
if any(results):
|
|
logger.info("Intent evaluation results")
|
|
log_results(results.train, "train")
|
|
log_results(results.test, "test")
|
|
if any(entity_results):
|
|
logger.info("Entity evaluation results")
|
|
log_entity_results(entity_results.train, "train")
|
|
log_entity_results(entity_results.test, "test")
|
|
if any(response_selection_results):
|
|
logger.info("Response Selection evaluation results")
|
|
log_results(response_selection_results.train, "train")
|
|
log_results(response_selection_results.test, "test")
|
|
|
|
|
|
def get_evaluation_metrics(
|
|
targets: Iterable[Any],
|
|
predictions: Iterable[Any],
|
|
output_dict: bool = False,
|
|
exclude_label: Optional[Text] = None,
|
|
) -> Tuple[Union[Text, Dict[Text, Dict[Text, float]]], float, float, float]:
|
|
"""Compute the f1, precision, accuracy and summary report from sklearn.
|
|
|
|
Args:
|
|
targets: target labels
|
|
predictions: predicted labels
|
|
output_dict: if True sklearn returns a summary report as dict, if False the
|
|
report is in string format
|
|
exclude_label: labels to exclude from evaluation
|
|
|
|
Returns:
|
|
Report from sklearn, precision, f1, and accuracy values.
|
|
"""
|
|
from sklearn import metrics
|
|
|
|
targets = clean_labels(targets)
|
|
predictions = clean_labels(predictions)
|
|
|
|
labels = get_unique_labels(targets, exclude_label)
|
|
if not labels:
|
|
logger.warning("No labels to evaluate. Skip evaluation.")
|
|
return {}, 0.0, 0.0, 0.0
|
|
|
|
report = metrics.classification_report(
|
|
targets, predictions, labels=labels, output_dict=output_dict
|
|
)
|
|
precision = metrics.precision_score(
|
|
targets, predictions, labels=labels, average="weighted"
|
|
)
|
|
f1 = metrics.f1_score(targets, predictions, labels=labels, average="weighted")
|
|
accuracy = metrics.accuracy_score(targets, predictions)
|
|
|
|
if output_dict:
|
|
report = make_classification_report_complete(report, accuracy)
|
|
|
|
return report, precision, f1, accuracy
|
|
|
|
|
|
def make_classification_report_complete(report: dict, accuracy: float) -> dict:
|
|
"""Completes the sklearn classification report with accuracy xor micro avg.
|
|
|
|
Args:
|
|
report: Report generated by metrics.classification_report with output_dict=True
|
|
accuracy: Model accuracy
|
|
|
|
Raises:
|
|
Exception: When sklearn.metrics.classification_report
|
|
behaves different to our expectation.
|
|
|
|
Returns:
|
|
report: Report generated by metrics.classification_report
|
|
enhanced with accuracy xor micro avg.
|
|
"""
|
|
report = copy.deepcopy(report)
|
|
if "accuracy" in report and "micro avg" not in report:
|
|
# micro avg corresponds to accuracy in this case
|
|
# and is the same for all metrics
|
|
acc = report["accuracy"]
|
|
support = report["macro avg"]["support"]
|
|
report["micro avg"] = {
|
|
"precision": acc,
|
|
"recall": acc,
|
|
"f1-score": acc,
|
|
"support": support,
|
|
}
|
|
elif "accuracy" not in report and "micro avg" in report:
|
|
# Due to provided labels, micro avg can have recall != precision
|
|
# The accuracy therefore has to be inferred separately
|
|
report["accuracy"] = accuracy
|
|
else:
|
|
raise ClassificationReportException(
|
|
"This cannot happen according to classification_report's docs"
|
|
)
|
|
return report
|
|
|
|
|
|
def clean_labels(labels: Iterable[Text]) -> List[Text]:
|
|
"""Remove `None` labels. sklearn metrics do not support them.
|
|
|
|
Args:
|
|
labels: list of labels
|
|
|
|
Returns:
|
|
Cleaned labels.
|
|
"""
|
|
return [label if label is not None else "" for label in labels]
|
|
|
|
|
|
def get_unique_labels(
|
|
targets: Iterable[Text], exclude_label: Optional[Text]
|
|
) -> List[Text]:
|
|
"""Get unique labels. Exclude 'exclude_label' if specified.
|
|
|
|
Args:
|
|
targets: labels
|
|
exclude_label: label to exclude
|
|
|
|
Returns:
|
|
Unique labels.
|
|
"""
|
|
labels = set(targets)
|
|
if exclude_label and exclude_label in labels:
|
|
labels.remove(exclude_label)
|
|
return list(labels)
|