Files
rasahq--rasa/rasa/nlu/classifiers/sklearn_intent_classifier.py
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

331 lines
12 KiB
Python

from __future__ import annotations
import logging
import typing
import warnings
from typing import Any, Dict, List, Optional, Text, Tuple, Type
import numpy as np
import rasa.shared.utils.io
from rasa.engine.graph import GraphComponent, ExecutionContext
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.classifiers import LABEL_RANKING_LENGTH
from rasa.nlu.classifiers.classifier import IntentClassifier
from rasa.nlu.featurizers.dense_featurizer.dense_featurizer import DenseFeaturizer
from rasa.shared.constants import DOCS_URL_TRAINING_DATA_NLU
from rasa.shared.exceptions import RasaException
from rasa.shared.nlu.constants import TEXT
from rasa.shared.nlu.training_data.message import Message
from rasa.shared.nlu.training_data.training_data import TrainingData
from rasa.utils.tensorflow.constants import FEATURIZERS
logger = logging.getLogger(__name__)
if typing.TYPE_CHECKING:
import sklearn
@DefaultV1Recipe.register(
DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER, is_trainable=True
)
class SklearnIntentClassifier(GraphComponent, IntentClassifier):
"""Intent classifier using the sklearn framework."""
@classmethod
def required_components(cls) -> List[Type]:
"""Components that should be included in the pipeline before this component."""
return [DenseFeaturizer]
@staticmethod
def get_default_config() -> Dict[Text, Any]:
"""The component's default config (see parent class for full docstring)."""
return {
# C parameter of the svm - cross validation will select the best value
"C": [1, 2, 5, 10, 20, 100],
# gamma parameter of the svm
"gamma": [0.1],
# the kernels to use for the svm training - cross validation will
# decide which one of them performs best
"kernels": ["linear"],
# We try to find a good number of cross folds to use during
# intent training, this specifies the max number of folds
"max_cross_validation_folds": 5,
# Scoring function used for evaluating the hyper parameters
# This can be a name or a function (cfr GridSearchCV doc for more info)
"scoring_function": "f1_weighted",
"num_threads": 1,
}
def __init__(
self,
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
clf: Optional["sklearn.model_selection.GridSearchCV"] = None,
le: Optional["sklearn.preprocessing.LabelEncoder"] = None,
) -> None:
"""Construct a new intent classifier using the sklearn framework."""
from sklearn.preprocessing import LabelEncoder
self.component_config = config
self._model_storage = model_storage
self._resource = resource
if le is not None:
self.le = le
else:
self.le = LabelEncoder()
self.clf = clf
@classmethod
def create(
cls,
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
execution_context: ExecutionContext,
) -> SklearnIntentClassifier:
"""Creates a new untrained component (see parent class for full docstring)."""
return cls(config, model_storage, resource)
@staticmethod
def required_packages() -> List[Text]:
"""Any extra python dependencies required for this component to run."""
return ["sklearn"]
def transform_labels_str2num(self, labels: List[Text]) -> np.ndarray:
"""Transforms a list of strings into numeric label representation.
:param labels: List of labels to convert to numeric representation
"""
return self.le.fit_transform(labels)
def transform_labels_num2str(self, y: np.ndarray) -> np.ndarray:
"""Transforms a list of strings into numeric label representation.
:param y: List of labels to convert to numeric representation"""
return self.le.inverse_transform(y)
def train(self, training_data: TrainingData) -> Resource:
"""Train the intent classifier on a data set."""
num_threads = self.component_config["num_threads"]
labels = [e.get("intent") for e in training_data.intent_examples]
if len(set(labels)) < 2:
rasa.shared.utils.io.raise_warning(
"Can not train an intent classifier as there are not "
"enough intents. Need at least 2 different intents. "
"Skipping training of intent classifier.",
docs=DOCS_URL_TRAINING_DATA_NLU,
)
return self._resource
y = self.transform_labels_str2num(labels)
training_examples = [
message
for message in training_data.intent_examples
if message.features_present(
attribute=TEXT, featurizers=self.component_config.get(FEATURIZERS)
)
]
X = np.stack(
[self._get_sentence_features(example) for example in training_examples]
)
# reduce dimensionality
X = np.reshape(X, (len(X), -1))
self.clf = self._create_classifier(num_threads, y)
with warnings.catch_warnings():
# sklearn raises lots of
# "UndefinedMetricWarning: F - score is ill - defined"
# if there are few intent examples, this is needed to prevent it
warnings.simplefilter("ignore")
self.clf.fit(X, y)
self.persist()
return self._resource
@staticmethod
def _get_sentence_features(message: Message) -> np.ndarray:
_, sentence_features = message.get_dense_features(TEXT)
if sentence_features is not None:
return sentence_features.features[0]
raise ValueError(
"No sentence features present. Not able to train sklearn policy."
)
def _num_cv_splits(self, y: np.ndarray) -> int:
folds = self.component_config["max_cross_validation_folds"]
return max(2, min(folds, np.min(np.bincount(y)) // 5))
def _create_classifier(
self, num_threads: int, y: np.ndarray
) -> "sklearn.model_selection.GridSearchCV":
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
C = self.component_config["C"]
kernels = self.component_config["kernels"]
gamma = self.component_config["gamma"]
# dirty str fix because sklearn is expecting
# str not instance of basestr...
tuned_parameters = [
{"C": C, "gamma": gamma, "kernel": [str(k) for k in kernels]}
]
# aim for 5 examples in each fold
cv_splits = self._num_cv_splits(y)
return GridSearchCV(
SVC(C=1, probability=True, class_weight="balanced"),
param_grid=tuned_parameters,
n_jobs=num_threads,
cv=cv_splits,
scoring=self.component_config["scoring_function"],
verbose=1,
)
def process(self, messages: List[Message]) -> List[Message]:
"""Return the most likely intent and its probability for a message."""
for message in messages:
if self.clf is None or not message.features_present(
attribute=TEXT, featurizers=self.component_config.get(FEATURIZERS)
):
# component is either not trained or didn't
# receive enough training data or the input doesn't
# have required features.
intent = None
intent_ranking = []
else:
X = self._get_sentence_features(message).reshape(1, -1)
intent_ids, probabilities = self.predict(X)
intents = self.transform_labels_num2str(np.ravel(intent_ids))
# `predict` returns a matrix as it is supposed
# to work for multiple examples as well, hence we need to flatten
probabilities = probabilities.flatten()
if intents.size > 0 and probabilities.size > 0:
ranking = list(zip(list(intents), list(probabilities)))[
:LABEL_RANKING_LENGTH
]
intent = {"name": intents[0], "confidence": probabilities[0]}
intent_ranking = [
{"name": intent_name, "confidence": score}
for intent_name, score in ranking
]
else:
intent = {"name": None, "confidence": 0.0}
intent_ranking = []
message.set("intent", intent, add_to_output=True)
message.set("intent_ranking", intent_ranking, add_to_output=True)
return messages
def predict_prob(self, X: np.ndarray) -> np.ndarray:
"""Given a bow vector of an input text, predict the intent label.
Return probabilities for all labels.
:param X: bow of input text
:return: vector of probabilities containing one entry for each label.
"""
if self.clf is None:
raise RasaException(
"Sklearn intent classifier has not been initialised and trained."
)
return self.clf.predict_proba(X)
def predict(self, X: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Given a bow vector of an input text, predict most probable label.
Return only the most likely label.
:param X: bow of input text
:return: tuple of first, the most probable label and second,
its probability.
"""
pred_result = self.predict_prob(X)
# sort the probabilities retrieving the indices of
# the elements in sorted order
sorted_indices = np.fliplr(np.argsort(pred_result, axis=1))
return sorted_indices, pred_result[:, sorted_indices]
def persist(self) -> None:
"""Persist this model into the passed directory."""
import skops.io as sio
with self._model_storage.write_to(self._resource) as model_dir:
file_name = self.__class__.__name__
classifier_file_name = model_dir / f"{file_name}_classifier.skops"
encoder_file_name = model_dir / f"{file_name}_encoder.json"
if self.clf and self.le:
# convert self.le.classes_ (numpy array of strings) to a list in order
# to use json dump
rasa.shared.utils.io.dump_obj_as_json_to_file(
encoder_file_name, list(self.le.classes_)
)
sio.dump(self.clf.best_estimator_, classifier_file_name)
@classmethod
def load(
cls,
config: Dict[Text, Any],
model_storage: ModelStorage,
resource: Resource,
execution_context: ExecutionContext,
**kwargs: Any,
) -> SklearnIntentClassifier:
"""Loads trained component (see parent class for full docstring)."""
from sklearn.preprocessing import LabelEncoder
import skops.io as sio
try:
with model_storage.read_from(resource) as model_dir:
file_name = cls.__name__
classifier_file = model_dir / f"{file_name}_classifier.skops"
if classifier_file.exists():
unknown_types = sio.get_untrusted_types(file=classifier_file)
if unknown_types:
logger.error(
f"Untrusted types ({unknown_types}) found when "
f"loading {classifier_file}!"
)
raise ValueError()
else:
classifier = sio.load(classifier_file, trusted=unknown_types)
encoder_file = model_dir / f"{file_name}_encoder.json"
classes = rasa.shared.utils.io.read_json_file(encoder_file)
encoder = LabelEncoder()
intent_classifier = cls(
config, model_storage, resource, classifier, encoder
)
# convert list of strings (class labels) back to numpy array of
# strings
intent_classifier.transform_labels_str2num(classes)
return intent_classifier
except ValueError:
logger.debug(
f"Failed to load '{cls.__name__}' from model storage. Resource "
f"'{resource.name}' doesn't exist."
)
return cls(config, model_storage, resource)