Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore intent/entity prediction if input is empty #10604

Closed
wants to merge 9 commits into from
1 change: 1 addition & 0 deletions changelog/10504.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
If NLU gets an empty string, ignore predicting intents and entities. Some components can't handle empty strings (e.g `MitieFeaturizer`) which causes errors during intent prediction.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If NLU gets an empty string, ignore predicting intents and entities. Some components can't handle empty strings (e.g `MitieFeaturizer`) which causes errors during intent prediction.
Fixes a bug which caused machine learning components like `DIETClassifier`, `ResponseSelector`, `CRFEntityExtractor` and `SklearnIntentClassifier` to throw an error when a message with an empty text string was passed to them for processing.

3 changes: 1 addition & 2 deletions rasa/nlu/classifiers/diet_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,7 @@ def _predict_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)
out = self._predict(message) if message.get(TEXT) else None

if self.component_config[INTENT_CLASSIFICATION]:
label, label_ranking = self._predict_label(out)
Expand All @@ -1017,7 +1017,6 @@ def process(self, messages: List[Message]) -> List[Message]:
message.add_diagnostic_data(
self._execution_context.node_name, out.get(DIAGNOSTIC_DATA)
)

return messages

def persist(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion rasa/nlu/classifiers/sklearn_intent_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def _create_classifier(
def process(self, messages: List[Message]) -> List[Message]:
"""Return the most likely intent and its probability for a message."""
for message in messages:
if not self.clf:
if not self.clf or not message.get(TEXT):
# component is either not trained or didn't
# receive enough training data
intent = None
Expand Down
2 changes: 1 addition & 1 deletion rasa/nlu/extractors/crf_entity_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ def process(self, messages: List[Message]) -> List[Message]:

def extract_entities(self, message: Message) -> List[Dict[Text, Any]]:
"""Extract entities from the given message using the trained model(s)."""
if self.entity_taggers is None:
if self.entity_taggers is None or not message.get(TEXT):
return []

tokens = message.get(TOKENS_NAMES[TEXT])
Expand Down
2 changes: 1 addition & 1 deletion rasa/nlu/selectors/response_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ def process(self, messages: List[Message]) -> List[Message]:
the associated intent_response_key and its similarity to the input.
"""
for message in messages:
out = self._predict(message)
out = self._predict(message) if message.get(TEXT) else None
top_label, label_ranking = self._predict_label(out)

# Get the exact intent_response_key and the associated
Expand Down
11 changes: 11 additions & 0 deletions tests/nlu/classifiers/test_diet_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,17 @@ async def test_train_persist_load_with_only_intent_classification(
create_diet({MASKED_LM: True, EPOCHS: 1}, load=True, finetune=True)


async def test_process_empty_input(
create_train_load_and_process_diet: Callable[..., Message],
):
message = create_train_load_and_process_diet(
diet_config={EPOCHS: 1}, message_text="", expect_intent=False
)
assert message.get(TEXT) == ""
jupyterjazz marked this conversation as resolved.
Show resolved Hide resolved
assert message.get(INTENT)["name"] is None
assert message.get(INTENT)["confidence"] == 0.0


@pytest.mark.parametrize(
"classifier_params, data_path, output_length, output_should_sum_to_1",
[
Expand Down
31 changes: 31 additions & 0 deletions tests/nlu/classifiers/test_sklearn_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from rasa.engine.storage.storage import ModelStorage
from rasa.nlu.classifiers.sklearn_intent_classifier import SklearnIntentClassifier
from rasa.shared.nlu.training_data.training_data import TrainingData
from rasa.shared.nlu.constants import TEXT, INTENT
from rasa.shared.nlu.training_data.message import Message


@pytest.fixture()
Expand All @@ -33,6 +35,35 @@ def default_sklearn_intent_classifier(
)


def test_process_empty_input(
training_data: TrainingData,
default_sklearn_intent_classifier: SklearnIntentClassifier,
default_model_storage: ModelStorage,
default_execution_context: ExecutionContext,
train_and_preprocess: Callable[..., Tuple[TrainingData, List[GraphComponent]]],
spacy_nlp_component: SpacyNLP,
spacy_model: SpacyModel,
):
training_data = spacy_nlp_component.process_training_data(
training_data, spacy_model
)
training_data, loaded_pipeline = train_and_preprocess(
pipeline=[{"component": SpacyTokenizer}, {"component": SpacyFeaturizer},],
training_data=training_data,
)
default_sklearn_intent_classifier.train(training_data)
classifier = SklearnIntentClassifier.load(
SklearnIntentClassifier.get_default_config(),
default_model_storage,
Resource("sklearn"),
default_execution_context,
)
message = Message(data={TEXT: ""})
processed_message = classifier.process([message])[0]
assert processed_message.get(TEXT) == ""
assert not processed_message.get(INTENT)


def test_persist_and_load(
training_data: TrainingData,
default_sklearn_intent_classifier: SklearnIntentClassifier,
Expand Down
10 changes: 10 additions & 0 deletions tests/nlu/extractors/test_crf_entity_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,13 @@ def test_most_likely_entity(

assert actual_label == expected_label
assert actual_confidence == expected_confidence


def test_process_empty_input(
crf_entity_extractor: Callable[[Dict[Text, Any]], CRFEntityExtractor],
):
crf_extractor = crf_entity_extractor({})
message = Message(data={TEXT: ""})
processed_message = crf_extractor.process([message])[0]
assert processed_message.get(TEXT) == ""
assert processed_message.get(ENTITIES) == []
28 changes: 28 additions & 0 deletions tests/nlu/selectors/test_selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,34 @@ async def test_margin_loss_is_not_normalized(
assert len(response_ranking) == 9


async def test_process_empty_input(
create_response_selector: Callable[[Dict[Text, Any]], ResponseSelector],
train_and_preprocess: Callable[..., Tuple[TrainingData, List[GraphComponent]]],
process_message: Callable[..., Message],
):
pipeline = [
{"component": WhitespaceTokenizer},
{"component": CountVectorsFeaturizer},
]
training_data, loaded_pipeline = train_and_preprocess(
pipeline, "data/test_selectors"
)

response_selector = create_response_selector({EPOCHS: 1})
response_selector.train(training_data=training_data)

message = Message(data={TEXT: ""})
message = process_message(loaded_pipeline, message)

classified_message = response_selector.process([message])[0]
output = classified_message.get("response_selector").get("default").get("response")

assert classified_message.get(TEXT) == ""
assert not output.get("responses")
assert output.get("confidence") == 0.0
assert not output.get("intent_response_key")


@pytest.mark.parametrize(
"classifier_params, output_length, sums_up_to_1",
[
Expand Down