Files
wehub-resource-sync dc6079821b
Docs Tests / Check for file changes (push) Has been cancelled
Docs Tests / Test Documentation (push) Has been cancelled
Docs Tests / Documentation Linting Checks (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.10, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.8, test-policies) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-performance) (push) Has been cancelled
Continuous Integration / Run Tests (windows-2022, 3.9, test-policies) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (ubuntu-24.04, 3.9) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.10) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.8) (push) Has been cancelled
Continuous Integration / Run Flaky Tests (windows-2022, 3.9) (push) Has been cancelled
Continuous Integration / Check for file changes (push) Has been cancelled
Continuous Integration / Wait for docs tests (push) Has been cancelled
Continuous Integration / Code Quality (push) Has been cancelled
Continuous Integration / Check for changelog (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-cli) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-core-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-full-model-training) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-featurizers) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-nlu-predictors) (push) Has been cancelled
Continuous Integration / Run Tests (ubuntu-24.04, 3.10, test-other-unit-tests) (push) Has been cancelled
Continuous Integration / Upload coverage reports to codeclimate (push) Has been cancelled
Continuous Integration / Run Non-Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Run Broker Integration Tests (push) Has been cancelled
Continuous Integration / Run Sequential Integration Tests (push) Has been cancelled
Continuous Integration / Build Docker base images and setup environment (push) Has been cancelled
Continuous Integration / Build Docker (default) (push) Has been cancelled
Continuous Integration / Build Docker (full) (push) Has been cancelled
Continuous Integration / Build Docker (mitie-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-de) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-en) (push) Has been cancelled
Continuous Integration / Build Docker (spacy-it) (push) Has been cancelled
Continuous Integration / Deploy to PyPI (push) Has been cancelled
Continuous Integration / Notify Slack & Publish Release Notes (push) Has been cancelled
Publish Documentation / Evaluate release tag (push) Has been cancelled
Publish Documentation / Prebuild Docs (push) Has been cancelled
Publish Documentation / Preview Docs (push) Has been cancelled
Publish Documentation / Check for file changes (push) Has been cancelled
Publish Documentation / Publish Docs (push) Has been cancelled
Automatic PR Merger / mergepal (push) Has been cancelled
CI Github Actions / Run Tests (push) Has been cancelled
Semgrep / Semgrep Workflow Security Scan (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:47 +08:00

1888 lines
71 KiB
Python

from __future__ import annotations
import copy
import logging
from collections import defaultdict
from pathlib import Path
from typing import Any, Dict, List, Optional, Text, Tuple, Union, TypeVar, Type
import numpy as np
import scipy.sparse
import tensorflow as tf
from rasa.exceptions import ModelNotFound
from rasa.nlu.featurizers.featurizer import Featurizer
from rasa.engine.graph import ExecutionContext, GraphComponent
from rasa.engine.recipes.default_recipe import DefaultV1Recipe
from rasa.engine.storage.resource import Resource
from rasa.engine.storage.storage import ModelStorage
from rasa.nlu.extractors.extractor import EntityExtractorMixin
from rasa.nlu.classifiers.classifier import IntentClassifier
import rasa.shared.utils.io
import rasa.nlu.utils.bilou_utils as bilou_utils
from rasa.shared.constants import DIAGNOSTIC_DATA
from rasa.nlu.extractors.extractor import EntityTagSpec
from rasa.nlu.classifiers import LABEL_RANKING_LENGTH
from rasa.utils import train_utils
from rasa.utils.tensorflow import rasa_layers
from rasa.utils.tensorflow.feature_array import (
FeatureArray,
serialize_nested_feature_arrays,
deserialize_nested_feature_arrays,
)
from rasa.utils.tensorflow.models import RasaModel, TransformerRasaModel
from rasa.utils.tensorflow.model_data import (
RasaModelData,
FeatureSignature,
)
from rasa.nlu.constants import TOKENS_NAMES, DEFAULT_TRANSFORMER_SIZE
from rasa.shared.nlu.constants import (
SPLIT_ENTITIES_BY_COMMA_DEFAULT_VALUE,
TEXT,
INTENT,
INTENT_RESPONSE_KEY,
ENTITIES,
ENTITY_ATTRIBUTE_TYPE,
ENTITY_ATTRIBUTE_GROUP,
ENTITY_ATTRIBUTE_ROLE,
NO_ENTITY_TAG,
SPLIT_ENTITIES_BY_COMMA,
)
from rasa.shared.exceptions import InvalidConfigException
from rasa.shared.nlu.training_data.training_data import TrainingData
from rasa.shared.nlu.training_data.message import Message
from rasa.utils.tensorflow.constants import (
DROP_SMALL_LAST_BATCH,
LABEL,
IDS,
HIDDEN_LAYERS_SIZES,
RENORMALIZE_CONFIDENCES,
SHARE_HIDDEN_LAYERS,
TRANSFORMER_SIZE,
NUM_TRANSFORMER_LAYERS,
NUM_HEADS,
BATCH_SIZES,
BATCH_STRATEGY,
EPOCHS,
RANDOM_SEED,
LEARNING_RATE,
RANKING_LENGTH,
LOSS_TYPE,
SIMILARITY_TYPE,
NUM_NEG,
SPARSE_INPUT_DROPOUT,
DENSE_INPUT_DROPOUT,
MASKED_LM,
ENTITY_RECOGNITION,
TENSORBOARD_LOG_DIR,
INTENT_CLASSIFICATION,
EVAL_NUM_EXAMPLES,
EVAL_NUM_EPOCHS,
UNIDIRECTIONAL_ENCODER,
DROP_RATE,
DROP_RATE_ATTENTION,
CONNECTION_DENSITY,
NEGATIVE_MARGIN_SCALE,
REGULARIZATION_CONSTANT,
SCALE_LOSS,
USE_MAX_NEG_SIM,
MAX_NEG_SIM,
MAX_POS_SIM,
EMBEDDING_DIMENSION,
BILOU_FLAG,
KEY_RELATIVE_ATTENTION,
VALUE_RELATIVE_ATTENTION,
MAX_RELATIVE_POSITION,
AUTO,
BALANCED,
CROSS_ENTROPY,
TENSORBOARD_LOG_LEVEL,
CONCAT_DIMENSION,
FEATURIZERS,
CHECKPOINT_MODEL,
SEQUENCE,
SENTENCE,
SEQUENCE_LENGTH,
DENSE_DIMENSION,
MASK,
CONSTRAIN_SIMILARITIES,
MODEL_CONFIDENCE,
SOFTMAX,
RUN_EAGERLY,
)
logger = logging.getLogger(__name__)
SPARSE = "sparse"
DENSE = "dense"
LABEL_KEY = LABEL
LABEL_SUB_KEY = IDS
POSSIBLE_TAGS = [ENTITY_ATTRIBUTE_TYPE, ENTITY_ATTRIBUTE_ROLE, ENTITY_ATTRIBUTE_GROUP]
DIETClassifierT = TypeVar("DIETClassifierT", bound="DIETClassifier")
@DefaultV1Recipe.register(
[
DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER,
DefaultV1Recipe.ComponentType.ENTITY_EXTRACTOR,
],
is_trainable=True,
)
class DIETClassifier(GraphComponent, IntentClassifier, EntityExtractorMixin):
"""A multi-task model for intent classification and entity extraction.
DIET is Dual Intent and Entity Transformer.
The architecture is based on a transformer which is shared for both tasks.
A sequence of entity labels is predicted through a Conditional Random Field (CRF)
tagging layer on top of the transformer output sequence corresponding to the
input sequence of tokens. The transformer output for the ``__CLS__`` token and
intent labels are embedded into a single semantic vector space. We use the
dot-product loss to maximize the similarity with the target label and minimize
similarities with negative samples.
"""
@classmethod
def required_components(cls) -> List[Type]:
"""Components that should be included in the pipeline before this component."""
return [Featurizer]
@staticmethod
def get_default_config() -> Dict[Text, Any]:
"""The component's default config (see parent class for full docstring)."""
# please make sure to update the docs when changing a default parameter
return {
# ## Architecture of the used neural network
# Hidden layer sizes for layers before the embedding layers for user message
# and labels.
# The number of hidden layers is equal to the length of the corresponding
# list.
HIDDEN_LAYERS_SIZES: {TEXT: [], LABEL: []},
# Whether to share the hidden layer weights between user message and labels.
SHARE_HIDDEN_LAYERS: False,
# Number of units in transformer
TRANSFORMER_SIZE: DEFAULT_TRANSFORMER_SIZE,
# Number of transformer layers
NUM_TRANSFORMER_LAYERS: 2,
# Number of attention heads in transformer
NUM_HEADS: 4,
# If 'True' use key relative embeddings in attention
KEY_RELATIVE_ATTENTION: False,
# If 'True' use value relative embeddings in attention
VALUE_RELATIVE_ATTENTION: False,
# Max position for relative embeddings. Only in effect if key- or value
# relative attention are turned on
MAX_RELATIVE_POSITION: 5,
# Use a unidirectional or bidirectional encoder.
UNIDIRECTIONAL_ENCODER: False,
# ## Training parameters
# Initial and final batch sizes:
# Batch size will be linearly increased for each epoch.
BATCH_SIZES: [64, 256],
# Strategy used when creating batches.
# Can be either 'sequence' or 'balanced'.
BATCH_STRATEGY: BALANCED,
# Number of epochs to train
EPOCHS: 300,
# Set random seed to any 'int' to get reproducible results
RANDOM_SEED: None,
# Initial learning rate for the optimizer
LEARNING_RATE: 0.001,
# ## Parameters for embeddings
# Dimension size of embedding vectors
EMBEDDING_DIMENSION: 20,
# Dense dimension to use for sparse features.
DENSE_DIMENSION: {TEXT: 128, LABEL: 20},
# Default dimension to use for concatenating sequence and sentence features.
CONCAT_DIMENSION: {TEXT: 128, LABEL: 20},
# The number of incorrect labels. The algorithm will minimize
# their similarity to the user input during training.
NUM_NEG: 20,
# Type of similarity measure to use, either 'auto' or 'cosine' or 'inner'.
SIMILARITY_TYPE: AUTO,
# The type of the loss function, either 'cross_entropy' or 'margin'.
LOSS_TYPE: CROSS_ENTROPY,
# Number of top intents for which confidences should be reported.
# Set to 0 if confidences for all intents should be reported.
RANKING_LENGTH: LABEL_RANKING_LENGTH,
# Indicates how similar the algorithm should try to make embedding vectors
# for correct labels.
# Should be 0.0 < ... < 1.0 for 'cosine' similarity type.
MAX_POS_SIM: 0.8,
# Maximum negative similarity for incorrect labels.
# Should be -1.0 < ... < 1.0 for 'cosine' similarity type.
MAX_NEG_SIM: -0.4,
# If 'True' the algorithm only minimizes maximum similarity over
# incorrect intent labels, used only if 'loss_type' is set to 'margin'.
USE_MAX_NEG_SIM: True,
# If 'True' scale loss inverse proportionally to the confidence
# of the correct prediction
SCALE_LOSS: False,
# ## Regularization parameters
# The scale of regularization
REGULARIZATION_CONSTANT: 0.002,
# The scale of how important is to minimize the maximum similarity
# between embeddings of different labels,
# used only if 'loss_type' is set to 'margin'.
NEGATIVE_MARGIN_SCALE: 0.8,
# Dropout rate for encoder
DROP_RATE: 0.2,
# Dropout rate for attention
DROP_RATE_ATTENTION: 0,
# Fraction of trainable weights in internal layers.
CONNECTION_DENSITY: 0.2,
# If 'True' apply dropout to sparse input tensors
SPARSE_INPUT_DROPOUT: True,
# If 'True' apply dropout to dense input tensors
DENSE_INPUT_DROPOUT: True,
# ## Evaluation parameters
# How often calculate validation accuracy.
# Small values may hurt performance.
EVAL_NUM_EPOCHS: 20,
# How many examples to use for hold out validation set
# Large values may hurt performance, e.g. model accuracy.
# Set to 0 for no validation.
EVAL_NUM_EXAMPLES: 0,
# ## Model config
# If 'True' intent classification is trained and intent predicted.
INTENT_CLASSIFICATION: True,
# If 'True' named entity recognition is trained and entities predicted.
ENTITY_RECOGNITION: True,
# If 'True' random tokens of the input message will be masked and the model
# should predict those tokens.
MASKED_LM: False,
# 'BILOU_flag' determines whether to use BILOU tagging or not.
# If set to 'True' labelling is more rigorous, however more
# examples per entity are required.
# Rule of thumb: you should have more than 100 examples per entity.
BILOU_FLAG: True,
# If you want to use tensorboard to visualize training and validation
# metrics, set this option to a valid output directory.
TENSORBOARD_LOG_DIR: None,
# Define when training metrics for tensorboard should be logged.
# Either after every epoch or for every training step.
# Valid values: 'epoch' and 'batch'
TENSORBOARD_LOG_LEVEL: "epoch",
# Perform model checkpointing
CHECKPOINT_MODEL: False,
# Specify what features to use as sequence and sentence features
# By default all features in the pipeline are used.
FEATURIZERS: [],
# Split entities by comma, this makes sense e.g. for a list of ingredients
# in a recipie, but it doesn't make sense for the parts of an address
SPLIT_ENTITIES_BY_COMMA: True,
# If 'True' applies sigmoid on all similarity terms and adds
# it to the loss function to ensure that similarity values are
# approximately bounded. Used inside cross-entropy loss only.
CONSTRAIN_SIMILARITIES: False,
# Model confidence to be returned during inference. Currently, the only
# possible value is `softmax`.
MODEL_CONFIDENCE: SOFTMAX,
# Determines whether the confidences of the chosen top intents should be
# renormalized so that they sum up to 1. By default, we do not renormalize
# and return the confidences for the top intents as is.
# Note that renormalization only makes sense if confidences are generated
# via `softmax`.
RENORMALIZE_CONFIDENCES: False,
# Determines whether to construct the model graph or not.
# This is advantageous when the model is only trained or inferred for
# a few steps, as the compilation of the graph tends to take more time than
# running it. It is recommended to not adjust the optimization parameter.
RUN_EAGERLY: False,
# Determines whether the last batch should be dropped if it contains fewer
# than half a batch size of examples
DROP_SMALL_LAST_BATCH: False,
}
def __init__(
self,
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
execution_context: ExecutionContext,
index_label_id_mapping: Optional[Dict[int, Text]] = None,
entity_tag_specs: Optional[List[EntityTagSpec]] = None,
model: Optional[RasaModel] = None,
sparse_feature_sizes: Optional[Dict[Text, Dict[Text, List[int]]]] = None,
) -> None:
"""Declare instance variables with default values."""
if EPOCHS not in config:
rasa.shared.utils.io.raise_warning(
f"Please configure the number of '{EPOCHS}' in your configuration file."
f" We will change the default value of '{EPOCHS}' in the future to 1. "
)
self.component_config = config
self._model_storage = model_storage
self._resource = resource
self._execution_context = execution_context
self._check_config_parameters()
# transform numbers to labels
self.index_label_id_mapping = index_label_id_mapping or {}
self._entity_tag_specs = entity_tag_specs
self.model = model
self.tmp_checkpoint_dir = None
if self.component_config[CHECKPOINT_MODEL]:
self.tmp_checkpoint_dir = Path(rasa.utils.io.create_temporary_directory())
self._label_data: Optional[RasaModelData] = None
self._data_example: Optional[Dict[Text, Dict[Text, List[FeatureArray]]]] = None
self.split_entities_config = rasa.utils.train_utils.init_split_entities(
self.component_config[SPLIT_ENTITIES_BY_COMMA],
SPLIT_ENTITIES_BY_COMMA_DEFAULT_VALUE,
)
self.finetune_mode = self._execution_context.is_finetuning
self._sparse_feature_sizes = sparse_feature_sizes
# init helpers
def _check_masked_lm(self) -> None:
if (
self.component_config[MASKED_LM]
and self.component_config[NUM_TRANSFORMER_LAYERS] == 0
):
raise ValueError(
f"If number of transformer layers is 0, "
f"'{MASKED_LM}' option should be 'False'."
)
def _check_share_hidden_layers_sizes(self) -> None:
if self.component_config.get(SHARE_HIDDEN_LAYERS):
first_hidden_layer_sizes = next(
iter(self.component_config[HIDDEN_LAYERS_SIZES].values())
)
# check that all hidden layer sizes are the same
identical_hidden_layer_sizes = all(
current_hidden_layer_sizes == first_hidden_layer_sizes
for current_hidden_layer_sizes in self.component_config[
HIDDEN_LAYERS_SIZES
].values()
)
if not identical_hidden_layer_sizes:
raise ValueError(
f"If hidden layer weights are shared, "
f"{HIDDEN_LAYERS_SIZES} must coincide."
)
def _check_config_parameters(self) -> None:
self.component_config = train_utils.check_deprecated_options(
self.component_config
)
self._check_masked_lm()
self._check_share_hidden_layers_sizes()
self.component_config = train_utils.update_confidence_type(
self.component_config
)
train_utils.validate_configuration_settings(self.component_config)
self.component_config = train_utils.update_similarity_type(
self.component_config
)
self.component_config = train_utils.update_evaluation_parameters(
self.component_config
)
@classmethod
def create(
cls,
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
execution_context: ExecutionContext,
) -> DIETClassifier:
"""Creates a new untrained component (see parent class for full docstring)."""
return cls(config, model_storage, resource, execution_context)
@property
def label_key(self) -> Optional[Text]:
"""Return key if intent classification is activated."""
return LABEL_KEY if self.component_config[INTENT_CLASSIFICATION] else None
@property
def label_sub_key(self) -> Optional[Text]:
"""Return sub key if intent classification is activated."""
return LABEL_SUB_KEY if self.component_config[INTENT_CLASSIFICATION] else None
@staticmethod
def model_class() -> Type[RasaModel]:
return DIET
# training data helpers:
@staticmethod
def _label_id_index_mapping(
training_data: TrainingData, attribute: Text
) -> Dict[Text, int]:
"""Create label_id dictionary."""
distinct_label_ids = {
example.get(attribute) for example in training_data.intent_examples
} - {None}
return {
label_id: idx for idx, label_id in enumerate(sorted(distinct_label_ids))
}
@staticmethod
def _invert_mapping(mapping: Dict) -> Dict:
return {value: key for key, value in mapping.items()}
def _create_entity_tag_specs(
self, training_data: TrainingData
) -> List[EntityTagSpec]:
"""Create entity tag specifications with their respective tag id mappings."""
_tag_specs = []
for tag_name in POSSIBLE_TAGS:
if self.component_config[BILOU_FLAG]:
tag_id_index_mapping = bilou_utils.build_tag_id_dict(
training_data, tag_name
)
else:
tag_id_index_mapping = self._tag_id_index_mapping_for(
tag_name, training_data
)
if tag_id_index_mapping:
_tag_specs.append(
EntityTagSpec(
tag_name=tag_name,
tags_to_ids=tag_id_index_mapping,
ids_to_tags=self._invert_mapping(tag_id_index_mapping),
num_tags=len(tag_id_index_mapping),
)
)
return _tag_specs
@staticmethod
def _tag_id_index_mapping_for(
tag_name: Text, training_data: TrainingData
) -> Optional[Dict[Text, int]]:
"""Create mapping from tag name to id."""
if tag_name == ENTITY_ATTRIBUTE_ROLE:
distinct_tags = training_data.entity_roles
elif tag_name == ENTITY_ATTRIBUTE_GROUP:
distinct_tags = training_data.entity_groups
else:
distinct_tags = training_data.entities
distinct_tags = distinct_tags - {NO_ENTITY_TAG} - {None}
if not distinct_tags:
return None
tag_id_dict = {
tag_id: idx for idx, tag_id in enumerate(sorted(distinct_tags), 1)
}
# NO_ENTITY_TAG corresponds to non-entity which should correspond to 0 index
# needed for correct prediction for padding
tag_id_dict[NO_ENTITY_TAG] = 0
return tag_id_dict
@staticmethod
def _find_example_for_label(
label: Text, examples: List[Message], attribute: Text
) -> Optional[Message]:
for ex in examples:
if ex.get(attribute) == label:
return ex
return None
def _check_labels_features_exist(
self, labels_example: List[Message], attribute: Text
) -> bool:
"""Checks if all labels have features set."""
return all(
label_example.features_present(
attribute, self.component_config[FEATURIZERS]
)
for label_example in labels_example
)
def _extract_features(
self, message: Message, attribute: Text
) -> Dict[Text, Union[scipy.sparse.spmatrix, np.ndarray]]:
(
sparse_sequence_features,
sparse_sentence_features,
) = message.get_sparse_features(attribute, self.component_config[FEATURIZERS])
dense_sequence_features, dense_sentence_features = message.get_dense_features(
attribute, self.component_config[FEATURIZERS]
)
if dense_sequence_features is not None and sparse_sequence_features is not None:
if (
dense_sequence_features.features.shape[0]
!= sparse_sequence_features.features.shape[0]
):
raise ValueError(
f"Sequence dimensions for sparse and dense sequence features "
f"don't coincide in '{message.get(TEXT)}'"
f"for attribute '{attribute}'."
)
if dense_sentence_features is not None and sparse_sentence_features is not None:
if (
dense_sentence_features.features.shape[0]
!= sparse_sentence_features.features.shape[0]
):
raise ValueError(
f"Sequence dimensions for sparse and dense sentence features "
f"don't coincide in '{message.get(TEXT)}'"
f"for attribute '{attribute}'."
)
# If we don't use the transformer and we don't want to do entity recognition,
# to speed up training take only the sentence features as feature vector.
# We would not make use of the sequence anyway in this setup. Carrying over
# those features to the actual training process takes quite some time.
if (
self.component_config[NUM_TRANSFORMER_LAYERS] == 0
and not self.component_config[ENTITY_RECOGNITION]
and attribute not in [INTENT, INTENT_RESPONSE_KEY]
):
sparse_sequence_features = None
dense_sequence_features = None
out = {}
if sparse_sentence_features is not None:
out[f"{SPARSE}_{SENTENCE}"] = sparse_sentence_features.features
if sparse_sequence_features is not None:
out[f"{SPARSE}_{SEQUENCE}"] = sparse_sequence_features.features
if dense_sentence_features is not None:
out[f"{DENSE}_{SENTENCE}"] = dense_sentence_features.features
if dense_sequence_features is not None:
out[f"{DENSE}_{SEQUENCE}"] = dense_sequence_features.features
return out
def _check_input_dimension_consistency(self, model_data: RasaModelData) -> None:
"""Checks if features have same dimensionality if hidden layers are shared."""
if self.component_config.get(SHARE_HIDDEN_LAYERS):
num_text_sentence_features = model_data.number_of_units(TEXT, SENTENCE)
num_label_sentence_features = model_data.number_of_units(LABEL, SENTENCE)
num_text_sequence_features = model_data.number_of_units(TEXT, SEQUENCE)
num_label_sequence_features = model_data.number_of_units(LABEL, SEQUENCE)
if (0 < num_text_sentence_features != num_label_sentence_features > 0) or (
0 < num_text_sequence_features != num_label_sequence_features > 0
):
raise ValueError(
"If embeddings are shared text features and label features "
"must coincide. Check the output dimensions of previous components."
)
def _extract_labels_precomputed_features(
self, label_examples: List[Message], attribute: Text = INTENT
) -> Tuple[List[FeatureArray], List[FeatureArray]]:
"""Collects precomputed encodings."""
features = defaultdict(list)
for e in label_examples:
label_features = self._extract_features(e, attribute)
for feature_key, feature_value in label_features.items():
features[feature_key].append(feature_value)
sequence_features = []
sentence_features = []
for feature_name, feature_value in features.items():
if SEQUENCE in feature_name:
sequence_features.append(
FeatureArray(np.array(feature_value), number_of_dimensions=3)
)
else:
sentence_features.append(
FeatureArray(np.array(feature_value), number_of_dimensions=3)
)
return sequence_features, sentence_features
@staticmethod
def _compute_default_label_features(
labels_example: List[Message],
) -> List[FeatureArray]:
"""Computes one-hot representation for the labels."""
logger.debug("No label features found. Computing default label features.")
eye_matrix = np.eye(len(labels_example), dtype=np.float32)
# add sequence dimension to one-hot labels
return [
FeatureArray(
np.array([np.expand_dims(a, 0) for a in eye_matrix]),
number_of_dimensions=3,
)
]
def _create_label_data(
self,
training_data: TrainingData,
label_id_dict: Dict[Text, int],
attribute: Text,
) -> RasaModelData:
"""Create matrix with label_ids encoded in rows as bag of words.
Find a training example for each label and get the encoded features
from the corresponding Message object.
If the features are already computed, fetch them from the message object
else compute a one hot encoding for the label as the feature vector.
"""
# Collect one example for each label
labels_idx_examples = []
for label_name, idx in label_id_dict.items():
label_example = self._find_example_for_label(
label_name, training_data.intent_examples, attribute
)
labels_idx_examples.append((idx, label_example))
# Sort the list of tuples based on label_idx
labels_idx_examples = sorted(labels_idx_examples, key=lambda x: x[0])
labels_example = [example for (_, example) in labels_idx_examples]
# Collect features, precomputed if they exist, else compute on the fly
if self._check_labels_features_exist(labels_example, attribute):
(
sequence_features,
sentence_features,
) = self._extract_labels_precomputed_features(labels_example, attribute)
else:
sequence_features = None
sentence_features = self._compute_default_label_features(labels_example)
label_data = RasaModelData()
label_data.add_features(LABEL, SEQUENCE, sequence_features)
label_data.add_features(LABEL, SENTENCE, sentence_features)
if label_data.does_feature_not_exist(
LABEL, SENTENCE
) and label_data.does_feature_not_exist(LABEL, SEQUENCE):
raise ValueError(
"No label features are present. Please check your configuration file."
)
label_ids = np.array([idx for (idx, _) in labels_idx_examples])
# explicitly add last dimension to label_ids
# to track correctly dynamic sequences
label_data.add_features(
LABEL_KEY,
LABEL_SUB_KEY,
[
FeatureArray(
np.expand_dims(label_ids, -1),
number_of_dimensions=2,
)
],
)
label_data.add_lengths(LABEL, SEQUENCE_LENGTH, LABEL, SEQUENCE)
return label_data
def _use_default_label_features(self, label_ids: np.ndarray) -> List[FeatureArray]:
if self._label_data is None:
return []
feature_arrays = self._label_data.get(LABEL, SENTENCE)
all_label_features = feature_arrays[0]
return [
FeatureArray(
np.array([all_label_features[label_id] for label_id in label_ids]),
number_of_dimensions=all_label_features.number_of_dimensions,
)
]
def _create_model_data(
self,
training_data: List[Message],
label_id_dict: Optional[Dict[Text, int]] = None,
label_attribute: Optional[Text] = None,
training: bool = True,
) -> RasaModelData:
"""Prepare data for training and create a RasaModelData object."""
from rasa.utils.tensorflow import model_data_utils
attributes_to_consider = [TEXT]
if training and self.component_config[INTENT_CLASSIFICATION]:
# we don't have any intent labels during prediction, just add them during
# training
attributes_to_consider.append(label_attribute)
if (
training
and self.component_config[ENTITY_RECOGNITION]
and self._entity_tag_specs
):
# Add entities as labels only during training and only if there was
# training data added for entities with DIET configured to predict entities.
attributes_to_consider.append(ENTITIES)
if training and label_attribute is not None:
# only use those training examples that have the label_attribute set
# during training
training_data = [
example for example in training_data if label_attribute in example.data
]
training_data = [
message
for message in training_data
if message.features_present(
attribute=TEXT, featurizers=self.component_config.get(FEATURIZERS)
)
]
if not training_data:
# no training data are present to train
return RasaModelData()
(
features_for_examples,
sparse_feature_sizes,
) = model_data_utils.featurize_training_examples(
training_data,
attributes_to_consider,
entity_tag_specs=self._entity_tag_specs,
featurizers=self.component_config[FEATURIZERS],
bilou_tagging=self.component_config[BILOU_FLAG],
)
attribute_data, _ = model_data_utils.convert_to_data_format(
features_for_examples, consider_dialogue_dimension=False
)
model_data = RasaModelData(
label_key=self.label_key, label_sub_key=self.label_sub_key
)
model_data.add_data(attribute_data)
model_data.add_lengths(TEXT, SEQUENCE_LENGTH, TEXT, SEQUENCE)
# Current implementation doesn't yet account for updating sparse
# feature sizes of label attributes. That's why we remove them.
sparse_feature_sizes = self._remove_label_sparse_feature_sizes(
sparse_feature_sizes=sparse_feature_sizes, label_attribute=label_attribute
)
model_data.add_sparse_feature_sizes(sparse_feature_sizes)
self._add_label_features(
model_data, training_data, label_attribute, label_id_dict, training
)
# make sure all keys are in the same order during training and prediction
# as we rely on the order of key and sub-key when constructing the actual
# tensors from the model data
model_data.sort()
return model_data
@staticmethod
def _remove_label_sparse_feature_sizes(
sparse_feature_sizes: Dict[Text, Dict[Text, List[int]]],
label_attribute: Optional[Text] = None,
) -> Dict[Text, Dict[Text, List[int]]]:
if label_attribute in sparse_feature_sizes:
del sparse_feature_sizes[label_attribute]
return sparse_feature_sizes
def _add_label_features(
self,
model_data: RasaModelData,
training_data: List[Message],
label_attribute: Text,
label_id_dict: Dict[Text, int],
training: bool = True,
) -> None:
label_ids = []
if training and self.component_config[INTENT_CLASSIFICATION]:
for example in training_data:
if example.get(label_attribute):
label_ids.append(label_id_dict[example.get(label_attribute)])
# explicitly add last dimension to label_ids
# to track correctly dynamic sequences
model_data.add_features(
LABEL_KEY,
LABEL_SUB_KEY,
[
FeatureArray(
np.expand_dims(label_ids, -1),
number_of_dimensions=2,
)
],
)
if (
label_attribute
and model_data.does_feature_not_exist(label_attribute, SENTENCE)
and model_data.does_feature_not_exist(label_attribute, SEQUENCE)
):
# no label features are present, get default features from _label_data
model_data.add_features(
LABEL, SENTENCE, self._use_default_label_features(np.array(label_ids))
)
# as label_attribute can have different values, e.g. INTENT or RESPONSE,
# copy over the features to the LABEL key to make
# it easier to access the label features inside the model itself
model_data.update_key(label_attribute, SENTENCE, LABEL, SENTENCE)
model_data.update_key(label_attribute, SEQUENCE, LABEL, SEQUENCE)
model_data.update_key(label_attribute, MASK, LABEL, MASK)
model_data.add_lengths(LABEL, SEQUENCE_LENGTH, LABEL, SEQUENCE)
# train helpers
def preprocess_train_data(self, training_data: TrainingData) -> RasaModelData:
"""Prepares data for training.
Performs sanity checks on training data, extracts encodings for labels.
"""
if (
self.component_config[BILOU_FLAG]
and self.component_config[ENTITY_RECOGNITION]
):
bilou_utils.apply_bilou_schema(training_data)
label_id_index_mapping = self._label_id_index_mapping(
training_data, attribute=INTENT
)
if not label_id_index_mapping:
# no labels are present to train
return RasaModelData()
self.index_label_id_mapping = self._invert_mapping(label_id_index_mapping)
self._label_data = self._create_label_data(
training_data, label_id_index_mapping, attribute=INTENT
)
self._entity_tag_specs = self._create_entity_tag_specs(training_data)
label_attribute = (
INTENT if self.component_config[INTENT_CLASSIFICATION] else None
)
model_data = self._create_model_data(
training_data.nlu_examples,
label_id_index_mapping,
label_attribute=label_attribute,
)
self._check_input_dimension_consistency(model_data)
return model_data
@staticmethod
def _check_enough_labels(model_data: RasaModelData) -> bool:
return len(np.unique(model_data.get(LABEL_KEY, LABEL_SUB_KEY))) >= 2
def train(self, training_data: TrainingData) -> Resource:
"""Train the embedding intent classifier on a data set."""
model_data = self.preprocess_train_data(training_data)
if model_data.is_empty():
logger.debug(
f"Cannot train '{self.__class__.__name__}'. No data was provided. "
f"Skipping training of the classifier."
)
return self._resource
if not self.model and self.finetune_mode:
raise rasa.shared.exceptions.InvalidParameterException(
f"{self.__class__.__name__} was instantiated "
f"with `model=None` and `finetune_mode=True`. "
f"This is not a valid combination as the component "
f"needs an already instantiated and trained model "
f"to continue training in finetune mode."
)
if self.component_config.get(INTENT_CLASSIFICATION):
if not self._check_enough_labels(model_data):
logger.error(
f"Cannot train '{self.__class__.__name__}'. "
f"Need at least 2 different intent classes. "
f"Skipping training of classifier."
)
return self._resource
if self.component_config.get(ENTITY_RECOGNITION):
self.check_correct_entity_annotations(training_data)
# keep one example for persisting and loading
self._data_example = model_data.first_data_example()
if not self.finetune_mode:
# No pre-trained model to load from. Create a new instance of the model.
self.model = self._instantiate_model_class(model_data)
self.model.compile(
optimizer=tf.keras.optimizers.Adam(
self.component_config[LEARNING_RATE]
),
run_eagerly=self.component_config[RUN_EAGERLY],
)
else:
if self.model is None:
raise ModelNotFound("Model could not be found. ")
self.model.adjust_for_incremental_training(
data_example=self._data_example,
new_sparse_feature_sizes=model_data.get_sparse_feature_sizes(),
old_sparse_feature_sizes=self._sparse_feature_sizes,
)
self._sparse_feature_sizes = model_data.get_sparse_feature_sizes()
data_generator, validation_data_generator = train_utils.create_data_generators(
model_data,
self.component_config[BATCH_SIZES],
self.component_config[EPOCHS],
self.component_config[BATCH_STRATEGY],
self.component_config[EVAL_NUM_EXAMPLES],
self.component_config[RANDOM_SEED],
drop_small_last_batch=self.component_config[DROP_SMALL_LAST_BATCH],
)
callbacks = train_utils.create_common_callbacks(
self.component_config[EPOCHS],
self.component_config[TENSORBOARD_LOG_DIR],
self.component_config[TENSORBOARD_LOG_LEVEL],
self.tmp_checkpoint_dir,
)
self.model.fit(
data_generator,
epochs=self.component_config[EPOCHS],
validation_data=validation_data_generator,
validation_freq=self.component_config[EVAL_NUM_EPOCHS],
callbacks=callbacks,
verbose=False,
shuffle=False, # we use custom shuffle inside data generator
)
self.persist()
return self._resource
# process helpers
def _predict(
self, message: Message
) -> Optional[Dict[Text, Union[tf.Tensor, Dict[Text, tf.Tensor]]]]:
if self.model is None:
logger.debug(
f"There is no trained model for '{self.__class__.__name__}': The "
f"component is either not trained or didn't receive enough training "
f"data."
)
return None
# create session data from message and convert it into a batch of 1
model_data = self._create_model_data([message], training=False)
if model_data.is_empty():
return None
return self.model.run_inference(model_data)
def _predict_label(
self, predict_out: Optional[Dict[Text, tf.Tensor]]
) -> Tuple[Dict[Text, Any], List[Dict[Text, Any]]]:
"""Predicts the intent of the provided message."""
label: Dict[Text, Any] = {"name": None, "confidence": 0.0}
label_ranking: List[Dict[Text, Any]] = []
if predict_out is None:
return label, label_ranking
message_sim = predict_out["i_scores"]
message_sim = message_sim.flatten() # sim is a matrix
# if X contains all zeros do not predict some label
if message_sim.size == 0:
return label, label_ranking
# rank the confidences
ranking_length = self.component_config[RANKING_LENGTH]
renormalize = (
self.component_config[RENORMALIZE_CONFIDENCES]
and self.component_config[MODEL_CONFIDENCE] == SOFTMAX
)
ranked_label_indices, message_sim = train_utils.rank_and_mask(
message_sim, ranking_length=ranking_length, renormalize=renormalize
)
# construct the label and ranking
casted_message_sim: List[float] = message_sim.tolist() # np.float to float
top_label_idx = ranked_label_indices[0]
label = {
"name": self.index_label_id_mapping[top_label_idx],
"confidence": casted_message_sim[top_label_idx],
}
ranking = [(idx, casted_message_sim[idx]) for idx in ranked_label_indices]
label_ranking = [
{"name": self.index_label_id_mapping[label_idx], "confidence": score}
for label_idx, score in ranking
]
return label, label_ranking
def _predict_entities(
self, predict_out: Optional[Dict[Text, tf.Tensor]], message: Message
) -> List[Dict]:
if predict_out is None:
return []
predicted_tags, confidence_values = train_utils.entity_label_to_tags(
predict_out, self._entity_tag_specs, self.component_config[BILOU_FLAG]
)
entities = self.convert_predictions_into_entities(
message.get(TEXT),
message.get(TOKENS_NAMES[TEXT], []),
predicted_tags,
self.split_entities_config,
confidence_values,
)
entities = self.add_extractor_name(entities)
entities = message.get(ENTITIES, []) + entities
return entities
def process(self, messages: List[Message]) -> List[Message]:
"""Augments the message with intents, entities, and diagnostic data."""
for message in messages:
out = self._predict(message)
if self.component_config[INTENT_CLASSIFICATION]:
label, label_ranking = self._predict_label(out)
message.set(INTENT, label, add_to_output=True)
message.set("intent_ranking", label_ranking, add_to_output=True)
if self.component_config[ENTITY_RECOGNITION]:
entities = self._predict_entities(out, message)
message.set(ENTITIES, entities, add_to_output=True)
if out and self._execution_context.should_add_diagnostic_data:
message.add_diagnostic_data(
self._execution_context.node_name, out.get(DIAGNOSTIC_DATA)
)
return messages
def persist(self) -> None:
"""Persist this model into the passed directory."""
if self.model is None:
return None
with self._model_storage.write_to(self._resource) as model_path:
file_name = self.__class__.__name__
tf_model_file = model_path / f"{file_name}.tf_model"
rasa.shared.utils.io.create_directory_for_file(tf_model_file)
if self.component_config[CHECKPOINT_MODEL] and self.tmp_checkpoint_dir:
self.model.load_weights(self.tmp_checkpoint_dir / "checkpoint.tf_model")
# Save an empty file to flag that this model has been
# produced using checkpointing
checkpoint_marker = model_path / f"{file_name}.from_checkpoint.pkl"
checkpoint_marker.touch()
self.model.save(str(tf_model_file))
# save data example
serialize_nested_feature_arrays(
self._data_example,
model_path / f"{file_name}.data_example.st",
model_path / f"{file_name}.data_example_metadata.json",
)
# save label data
serialize_nested_feature_arrays(
dict(self._label_data.data) if self._label_data is not None else {},
model_path / f"{file_name}.label_data.st",
model_path / f"{file_name}.label_data_metadata.json",
)
rasa.shared.utils.io.dump_obj_as_json_to_file(
model_path / f"{file_name}.sparse_feature_sizes.json",
self._sparse_feature_sizes,
)
rasa.shared.utils.io.dump_obj_as_json_to_file(
model_path / f"{file_name}.index_label_id_mapping.json",
self.index_label_id_mapping,
)
entity_tag_specs = (
[tag_spec._asdict() for tag_spec in self._entity_tag_specs]
if self._entity_tag_specs
else []
)
rasa.shared.utils.io.dump_obj_as_json_to_file(
model_path / f"{file_name}.entity_tag_specs.json", entity_tag_specs
)
@classmethod
def load(
cls: Type[DIETClassifierT],
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
execution_context: ExecutionContext,
**kwargs: Any,
) -> DIETClassifierT:
"""Loads a policy from the storage (see parent class for full docstring)."""
try:
with model_storage.read_from(resource) as model_path:
return cls._load(
model_path, config, model_storage, resource, execution_context
)
except ValueError:
logger.debug(
f"Failed to load {cls.__class__.__name__} from model storage. Resource "
f"'{resource.name}' doesn't exist."
)
return cls(config, model_storage, resource, execution_context)
@classmethod
def _load(
cls: Type[DIETClassifierT],
model_path: Path,
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
execution_context: ExecutionContext,
) -> DIETClassifierT:
"""Loads the trained model from the provided directory."""
(
index_label_id_mapping,
entity_tag_specs,
label_data,
data_example,
sparse_feature_sizes,
) = cls._load_from_files(model_path)
config = train_utils.update_confidence_type(config)
config = train_utils.update_similarity_type(config)
model = cls._load_model(
entity_tag_specs,
label_data,
config,
data_example,
model_path,
finetune_mode=execution_context.is_finetuning,
)
return cls(
config=config,
model_storage=model_storage,
resource=resource,
execution_context=execution_context,
index_label_id_mapping=index_label_id_mapping,
entity_tag_specs=entity_tag_specs,
model=model,
sparse_feature_sizes=sparse_feature_sizes,
)
@classmethod
def _load_from_files(
cls, model_path: Path
) -> Tuple[
Dict[int, Text],
List[EntityTagSpec],
RasaModelData,
Dict[Text, Dict[Text, List[FeatureArray]]],
Dict[Text, Dict[Text, List[int]]],
]:
file_name = cls.__name__
# load data example
data_example = deserialize_nested_feature_arrays(
str(model_path / f"{file_name}.data_example.st"),
str(model_path / f"{file_name}.data_example_metadata.json"),
)
# load label data
loaded_label_data = deserialize_nested_feature_arrays(
str(model_path / f"{file_name}.label_data.st"),
str(model_path / f"{file_name}.label_data_metadata.json"),
)
label_data = RasaModelData(data=loaded_label_data)
sparse_feature_sizes = rasa.shared.utils.io.read_json_file(
model_path / f"{file_name}.sparse_feature_sizes.json"
)
index_label_id_mapping = rasa.shared.utils.io.read_json_file(
model_path / f"{file_name}.index_label_id_mapping.json"
)
entity_tag_specs = rasa.shared.utils.io.read_json_file(
model_path / f"{file_name}.entity_tag_specs.json"
)
entity_tag_specs = [
EntityTagSpec(
tag_name=tag_spec["tag_name"],
ids_to_tags={
int(key): value for key, value in tag_spec["ids_to_tags"].items()
},
tags_to_ids={
key: int(value) for key, value in tag_spec["tags_to_ids"].items()
},
num_tags=tag_spec["num_tags"],
)
for tag_spec in entity_tag_specs
]
index_label_id_mapping = {
int(key): value for key, value in index_label_id_mapping.items()
}
return (
index_label_id_mapping,
entity_tag_specs,
label_data,
data_example,
sparse_feature_sizes,
)
@classmethod
def _load_model(
cls,
entity_tag_specs: List[EntityTagSpec],
label_data: RasaModelData,
config: Dict[Text, Any],
data_example: Dict[Text, Dict[Text, List[FeatureArray]]],
model_path: Path,
finetune_mode: bool = False,
) -> "RasaModel":
file_name = cls.__name__
tf_model_file = model_path / f"{file_name}.tf_model"
label_key = LABEL_KEY if config[INTENT_CLASSIFICATION] else None
label_sub_key = LABEL_SUB_KEY if config[INTENT_CLASSIFICATION] else None
model_data_example = RasaModelData(
label_key=label_key, label_sub_key=label_sub_key, data=data_example
)
model = cls._load_model_class(
tf_model_file,
model_data_example,
label_data,
entity_tag_specs,
config,
finetune_mode=finetune_mode,
)
return model
@classmethod
def _load_model_class(
cls,
tf_model_file: Text,
model_data_example: RasaModelData,
label_data: RasaModelData,
entity_tag_specs: List[EntityTagSpec],
config: Dict[Text, Any],
finetune_mode: bool,
) -> "RasaModel":
predict_data_example = RasaModelData(
label_key=model_data_example.label_key,
data={
feature_name: features
for feature_name, features in model_data_example.items()
if TEXT in feature_name
},
)
return cls.model_class().load(
tf_model_file,
model_data_example,
predict_data_example,
data_signature=model_data_example.get_signature(),
label_data=label_data,
entity_tag_specs=entity_tag_specs,
config=copy.deepcopy(config),
finetune_mode=finetune_mode,
)
def _instantiate_model_class(self, model_data: RasaModelData) -> "RasaModel":
return self.model_class()(
data_signature=model_data.get_signature(),
label_data=self._label_data,
entity_tag_specs=self._entity_tag_specs,
config=self.component_config,
)
class DIET(TransformerRasaModel):
def __init__(
self,
data_signature: Dict[Text, Dict[Text, List[FeatureSignature]]],
label_data: RasaModelData,
entity_tag_specs: Optional[List[EntityTagSpec]],
config: Dict[Text, Any],
) -> None:
# create entity tag spec before calling super otherwise building the model
# will fail
super().__init__("DIET", config, data_signature, label_data)
self._entity_tag_specs = self._ordered_tag_specs(entity_tag_specs)
self.predict_data_signature = {
feature_name: features
for feature_name, features in data_signature.items()
if TEXT in feature_name
}
# tf training
self._create_metrics()
self._update_metrics_to_log()
# needed for efficient prediction
self.all_labels_embed: Optional[tf.Tensor] = None
self._prepare_layers()
@staticmethod
def _ordered_tag_specs(
entity_tag_specs: Optional[List[EntityTagSpec]],
) -> List[EntityTagSpec]:
"""Ensure that order of entity tag specs matches CRF layer order."""
if entity_tag_specs is None:
return []
crf_order = [
ENTITY_ATTRIBUTE_TYPE,
ENTITY_ATTRIBUTE_ROLE,
ENTITY_ATTRIBUTE_GROUP,
]
ordered_tag_spec = []
for tag_name in crf_order:
for tag_spec in entity_tag_specs:
if tag_name == tag_spec.tag_name:
ordered_tag_spec.append(tag_spec)
return ordered_tag_spec
def _check_data(self) -> None:
if TEXT not in self.data_signature:
raise InvalidConfigException(
f"No text features specified. "
f"Cannot train '{self.__class__.__name__}' model."
)
if self.config[INTENT_CLASSIFICATION]:
if LABEL not in self.data_signature:
raise InvalidConfigException(
f"No label features specified. "
f"Cannot train '{self.__class__.__name__}' model."
)
if self.config[SHARE_HIDDEN_LAYERS]:
different_sentence_signatures = False
different_sequence_signatures = False
if (
SENTENCE in self.data_signature[TEXT]
and SENTENCE in self.data_signature[LABEL]
):
different_sentence_signatures = (
self.data_signature[TEXT][SENTENCE]
!= self.data_signature[LABEL][SENTENCE]
)
if (
SEQUENCE in self.data_signature[TEXT]
and SEQUENCE in self.data_signature[LABEL]
):
different_sequence_signatures = (
self.data_signature[TEXT][SEQUENCE]
!= self.data_signature[LABEL][SEQUENCE]
)
if different_sentence_signatures or different_sequence_signatures:
raise ValueError(
"If hidden layer weights are shared, data signatures "
"for text_features and label_features must coincide."
)
if self.config[ENTITY_RECOGNITION] and (
ENTITIES not in self.data_signature
or ENTITY_ATTRIBUTE_TYPE not in self.data_signature[ENTITIES]
):
logger.debug(
f"You specified '{self.__class__.__name__}' to train entities, but "
f"no entities are present in the training data. Skipping training of "
f"entities."
)
self.config[ENTITY_RECOGNITION] = False
def _create_metrics(self) -> None:
# self.metrics will have the same order as they are created
# so create loss metrics first to output losses first
self.mask_loss = tf.keras.metrics.Mean(name="m_loss")
self.intent_loss = tf.keras.metrics.Mean(name="i_loss")
self.entity_loss = tf.keras.metrics.Mean(name="e_loss")
self.entity_group_loss = tf.keras.metrics.Mean(name="g_loss")
self.entity_role_loss = tf.keras.metrics.Mean(name="r_loss")
# create accuracy metrics second to output accuracies second
self.mask_acc = tf.keras.metrics.Mean(name="m_acc")
self.intent_acc = tf.keras.metrics.Mean(name="i_acc")
self.entity_f1 = tf.keras.metrics.Mean(name="e_f1")
self.entity_group_f1 = tf.keras.metrics.Mean(name="g_f1")
self.entity_role_f1 = tf.keras.metrics.Mean(name="r_f1")
def _update_metrics_to_log(self) -> None:
debug_log_level = logging.getLogger("rasa").level == logging.DEBUG
if self.config[MASKED_LM]:
self.metrics_to_log.append("m_acc")
if debug_log_level:
self.metrics_to_log.append("m_loss")
if self.config[INTENT_CLASSIFICATION]:
self.metrics_to_log.append("i_acc")
if debug_log_level:
self.metrics_to_log.append("i_loss")
if self.config[ENTITY_RECOGNITION]:
for tag_spec in self._entity_tag_specs:
if tag_spec.num_tags != 0:
name = tag_spec.tag_name
self.metrics_to_log.append(f"{name[0]}_f1")
if debug_log_level:
self.metrics_to_log.append(f"{name[0]}_loss")
self._log_metric_info()
def _log_metric_info(self) -> None:
metric_name = {
"t": "total",
"i": "intent",
"e": "entity",
"m": "mask",
"r": "role",
"g": "group",
}
logger.debug("Following metrics will be logged during training: ")
for metric in self.metrics_to_log:
parts = metric.split("_")
name = f"{metric_name[parts[0]]} {parts[1]}"
logger.debug(f" {metric} ({name})")
def _prepare_layers(self) -> None:
# For user text, prepare layers that combine different feature types, embed
# everything using a transformer and optionally also do masked language
# modeling.
self.text_name = TEXT
self._tf_layers[
f"sequence_layer.{self.text_name}"
] = rasa_layers.RasaSequenceLayer(
self.text_name, self.data_signature[self.text_name], self.config
)
if self.config[MASKED_LM]:
self._prepare_mask_lm_loss(self.text_name)
# Intent labels are treated similarly to user text but without the transformer,
# without masked language modelling, and with no dropout applied to the
# individual features, only to the overall label embedding after all label
# features have been combined.
if self.config[INTENT_CLASSIFICATION]:
self.label_name = TEXT if self.config[SHARE_HIDDEN_LAYERS] else LABEL
# disable input dropout applied to sparse and dense label features
label_config = self.config.copy()
label_config.update(
{SPARSE_INPUT_DROPOUT: False, DENSE_INPUT_DROPOUT: False}
)
self._tf_layers[
f"feature_combining_layer.{self.label_name}"
] = rasa_layers.RasaFeatureCombiningLayer(
self.label_name, self.label_signature[self.label_name], label_config
)
self._prepare_ffnn_layer(
self.label_name,
self.config[HIDDEN_LAYERS_SIZES][self.label_name],
self.config[DROP_RATE],
)
self._prepare_label_classification_layers(predictor_attribute=TEXT)
if self.config[ENTITY_RECOGNITION]:
self._prepare_entity_recognition_layers()
def _prepare_mask_lm_loss(self, name: Text) -> None:
# for embedding predicted tokens at masked positions
self._prepare_embed_layers(f"{name}_lm_mask")
# for embedding the true tokens that got masked
self._prepare_embed_layers(f"{name}_golden_token")
# mask loss is additional loss
# set scaling to False, so that it doesn't overpower other losses
self._prepare_dot_product_loss(f"{name}_mask", scale_loss=False)
def _create_bow(
self,
sequence_features: List[Union[tf.Tensor, tf.SparseTensor]],
sentence_features: List[Union[tf.Tensor, tf.SparseTensor]],
sequence_feature_lengths: tf.Tensor,
name: Text,
) -> tf.Tensor:
x, _ = self._tf_layers[f"feature_combining_layer.{name}"](
(sequence_features, sentence_features, sequence_feature_lengths),
training=self._training,
)
# convert to bag-of-words by summing along the sequence dimension
x = tf.reduce_sum(x, axis=1)
return self._tf_layers[f"ffnn.{name}"](x, self._training)
def _create_all_labels(self) -> Tuple[tf.Tensor, tf.Tensor]:
all_label_ids = self.tf_label_data[LABEL_KEY][LABEL_SUB_KEY][0]
sequence_feature_lengths = self._get_sequence_feature_lengths(
self.tf_label_data, LABEL
)
x = self._create_bow(
self.tf_label_data[LABEL][SEQUENCE],
self.tf_label_data[LABEL][SENTENCE],
sequence_feature_lengths,
self.label_name,
)
all_labels_embed = self._tf_layers[f"embed.{LABEL}"](x)
return all_label_ids, all_labels_embed
def _mask_loss(
self,
outputs: tf.Tensor,
inputs: tf.Tensor,
seq_ids: tf.Tensor,
mlm_mask_boolean: tf.Tensor,
name: Text,
) -> tf.Tensor:
# make sure there is at least one element in the mask
mlm_mask_boolean = tf.cond(
tf.reduce_any(mlm_mask_boolean),
lambda: mlm_mask_boolean,
lambda: tf.scatter_nd([[0, 0, 0]], [True], tf.shape(mlm_mask_boolean)),
)
mlm_mask_boolean = tf.squeeze(mlm_mask_boolean, -1)
# Pick elements that were masked, throwing away the batch & sequence dimension
# and effectively switching from shape (batch_size, sequence_length, units) to
# (num_masked_elements, units).
outputs = tf.boolean_mask(outputs, mlm_mask_boolean)
inputs = tf.boolean_mask(inputs, mlm_mask_boolean)
ids = tf.boolean_mask(seq_ids, mlm_mask_boolean)
tokens_predicted_embed = self._tf_layers[f"embed.{name}_lm_mask"](outputs)
tokens_true_embed = self._tf_layers[f"embed.{name}_golden_token"](inputs)
# To limit the otherwise computationally expensive loss calculation, we
# constrain the label space in MLM (i.e. token space) to only those tokens that
# were masked in this batch. Hence the reduced list of token embeddings
# (tokens_true_embed) and the reduced list of labels (ids) are passed as
# all_labels_embed and all_labels, respectively. In the future, we could be less
# restrictive and construct a slightly bigger label space which could include
# tokens not masked in the current batch too.
return self._tf_layers[f"loss.{name}_mask"](
inputs_embed=tokens_predicted_embed,
labels_embed=tokens_true_embed,
labels=ids,
all_labels_embed=tokens_true_embed,
all_labels=ids,
)
def _calculate_label_loss(
self, text_features: tf.Tensor, label_features: tf.Tensor, label_ids: tf.Tensor
) -> tf.Tensor:
all_label_ids, all_labels_embed = self._create_all_labels()
text_embed = self._tf_layers[f"embed.{TEXT}"](text_features)
label_embed = self._tf_layers[f"embed.{LABEL}"](label_features)
return self._tf_layers[f"loss.{LABEL}"](
text_embed, label_embed, label_ids, all_labels_embed, all_label_ids
)
def batch_loss(
self, batch_in: Union[Tuple[tf.Tensor, ...], Tuple[np.ndarray, ...]]
) -> tf.Tensor:
"""Calculates the loss for the given batch.
Args:
batch_in: The batch.
Returns:
The loss of the given batch.
"""
tf_batch_data = self.batch_to_model_data_format(batch_in, self.data_signature)
sequence_feature_lengths = self._get_sequence_feature_lengths(
tf_batch_data, TEXT
)
(
text_transformed,
text_in,
mask_combined_sequence_sentence,
text_seq_ids,
mlm_mask_boolean_text,
_,
) = self._tf_layers[f"sequence_layer.{self.text_name}"](
(
tf_batch_data[TEXT][SEQUENCE],
tf_batch_data[TEXT][SENTENCE],
sequence_feature_lengths,
),
training=self._training,
)
losses = []
# Lengths of sequences in case of sentence-level features are always 1, but they
# can effectively be 0 if sentence-level features aren't present.
sentence_feature_lengths = self._get_sentence_feature_lengths(
tf_batch_data, TEXT
)
combined_sequence_sentence_feature_lengths = (
sequence_feature_lengths + sentence_feature_lengths
)
if self.config[MASKED_LM] and self._training:
loss, acc = self._mask_loss(
text_transformed, text_in, text_seq_ids, mlm_mask_boolean_text, TEXT
)
self.mask_loss.update_state(loss)
self.mask_acc.update_state(acc)
losses.append(loss)
if self.config[INTENT_CLASSIFICATION]:
loss = self._batch_loss_intent(
combined_sequence_sentence_feature_lengths,
text_transformed,
tf_batch_data,
)
losses.append(loss)
if self.config[ENTITY_RECOGNITION]:
losses += self._batch_loss_entities(
mask_combined_sequence_sentence,
sequence_feature_lengths,
text_transformed,
tf_batch_data,
)
return tf.math.add_n(losses)
def _batch_loss_intent(
self,
combined_sequence_sentence_feature_lengths_text: tf.Tensor,
text_transformed: tf.Tensor,
tf_batch_data: Dict[Text, Dict[Text, List[tf.Tensor]]],
) -> tf.Tensor:
# get sentence features vector for intent classification
sentence_vector = self._last_token(
text_transformed, combined_sequence_sentence_feature_lengths_text
)
sequence_feature_lengths_label = self._get_sequence_feature_lengths(
tf_batch_data, LABEL
)
label_ids = tf_batch_data[LABEL_KEY][LABEL_SUB_KEY][0]
label = self._create_bow(
tf_batch_data[LABEL][SEQUENCE],
tf_batch_data[LABEL][SENTENCE],
sequence_feature_lengths_label,
self.label_name,
)
loss, acc = self._calculate_label_loss(sentence_vector, label, label_ids)
self._update_label_metrics(loss, acc)
return loss
def _update_label_metrics(self, loss: tf.Tensor, acc: tf.Tensor) -> None:
self.intent_loss.update_state(loss)
self.intent_acc.update_state(acc)
def _batch_loss_entities(
self,
mask_combined_sequence_sentence: tf.Tensor,
sequence_feature_lengths: tf.Tensor,
text_transformed: tf.Tensor,
tf_batch_data: Dict[Text, Dict[Text, List[tf.Tensor]]],
) -> List[tf.Tensor]:
losses = []
entity_tags = None
for tag_spec in self._entity_tag_specs:
if tag_spec.num_tags == 0:
continue
tag_ids = tf_batch_data[ENTITIES][tag_spec.tag_name][0]
# add a zero (no entity) for the sentence features to match the shape of
# inputs
tag_ids = tf.pad(tag_ids, [[0, 0], [0, 1], [0, 0]])
loss, f1, _logits = self._calculate_entity_loss(
text_transformed,
tag_ids,
mask_combined_sequence_sentence,
sequence_feature_lengths,
tag_spec.tag_name,
entity_tags,
)
if tag_spec.tag_name == ENTITY_ATTRIBUTE_TYPE:
# use the entity tags as additional input for the role
# and group CRF
entity_tags = tf.one_hot(
tf.cast(tag_ids[:, :, 0], tf.int32), depth=tag_spec.num_tags
)
self._update_entity_metrics(loss, f1, tag_spec.tag_name)
losses.append(loss)
return losses
def _update_entity_metrics(
self, loss: tf.Tensor, f1: tf.Tensor, tag_name: Text
) -> None:
if tag_name == ENTITY_ATTRIBUTE_TYPE:
self.entity_loss.update_state(loss)
self.entity_f1.update_state(f1)
elif tag_name == ENTITY_ATTRIBUTE_GROUP:
self.entity_group_loss.update_state(loss)
self.entity_group_f1.update_state(f1)
elif tag_name == ENTITY_ATTRIBUTE_ROLE:
self.entity_role_loss.update_state(loss)
self.entity_role_f1.update_state(f1)
def prepare_for_predict(self) -> None:
"""Prepares the model for prediction."""
if self.config[INTENT_CLASSIFICATION]:
_, self.all_labels_embed = self._create_all_labels()
def batch_predict(
self, batch_in: Union[Tuple[tf.Tensor, ...], Tuple[np.ndarray, ...]]
) -> Dict[Text, tf.Tensor]:
"""Predicts the output of the given batch.
Args:
batch_in: The batch.
Returns:
The output to predict.
"""
tf_batch_data = self.batch_to_model_data_format(
batch_in, self.predict_data_signature
)
sequence_feature_lengths = self._get_sequence_feature_lengths(
tf_batch_data, TEXT
)
sentence_feature_lengths = self._get_sentence_feature_lengths(
tf_batch_data, TEXT
)
text_transformed, _, _, _, _, attention_weights = self._tf_layers[
f"sequence_layer.{self.text_name}"
](
(
tf_batch_data[TEXT][SEQUENCE],
tf_batch_data[TEXT][SENTENCE],
sequence_feature_lengths,
),
training=self._training,
)
predictions = {
DIAGNOSTIC_DATA: {
"attention_weights": attention_weights,
"text_transformed": text_transformed,
}
}
if self.config[INTENT_CLASSIFICATION]:
predictions.update(
self._batch_predict_intents(
sequence_feature_lengths + sentence_feature_lengths,
text_transformed,
)
)
if self.config[ENTITY_RECOGNITION]:
predictions.update(
self._batch_predict_entities(sequence_feature_lengths, text_transformed)
)
return predictions
def _batch_predict_entities(
self, sequence_feature_lengths: tf.Tensor, text_transformed: tf.Tensor
) -> Dict[Text, tf.Tensor]:
predictions: Dict[Text, tf.Tensor] = {}
entity_tags = None
for tag_spec in self._entity_tag_specs:
# skip crf layer if it was not trained
if tag_spec.num_tags == 0:
continue
name = tag_spec.tag_name
_input = text_transformed
if entity_tags is not None:
_tags = self._tf_layers[f"embed.{name}.tags"](entity_tags)
_input = tf.concat([_input, _tags], axis=-1)
_logits = self._tf_layers[f"embed.{name}.logits"](_input)
pred_ids, confidences = self._tf_layers[f"crf.{name}"](
_logits, sequence_feature_lengths
)
predictions[f"e_{name}_ids"] = pred_ids
predictions[f"e_{name}_scores"] = confidences
if name == ENTITY_ATTRIBUTE_TYPE:
# use the entity tags as additional input for the role
# and group CRF
entity_tags = tf.one_hot(
tf.cast(pred_ids, tf.int32), depth=tag_spec.num_tags
)
return predictions
def _batch_predict_intents(
self,
combined_sequence_sentence_feature_lengths: tf.Tensor,
text_transformed: tf.Tensor,
) -> Dict[Text, tf.Tensor]:
if self.all_labels_embed is None:
raise ValueError(
"The model was not prepared for prediction. "
"Call `prepare_for_predict` first."
)
# get sentence feature vector for intent classification
sentence_vector = self._last_token(
text_transformed, combined_sequence_sentence_feature_lengths
)
sentence_vector_embed = self._tf_layers[f"embed.{TEXT}"](sentence_vector)
_, scores = self._tf_layers[
f"loss.{LABEL}"
].get_similarities_and_confidences_from_embeddings(
sentence_vector_embed[:, tf.newaxis, :],
self.all_labels_embed[tf.newaxis, :, :],
)
return {"i_scores": scores}