Skip to content

Explainable AI

(author: irene) Im Folgenden wird die Verwendung der Klassen TextPredictor, TextExplainer, TextExplanation erklärt.

TL;NR

  • TextPredictor: 1x instantiieren, n Texte predicten (nur Label, Wahrscheinlichkeiten, Vorsprung)
  • TextExplainer: 1x instantiieren, n Texte erklären (gibt u.a. TextExplanation-Instanz für einzelne Erklärung aus)
  • TextExplanation: erklärt 1 Prediction auf 1 Text (enthält Wortgewichte und Methoden zur Anzeige)

Klassifiziere Texte mit TextPredictor:

Hierzu wird einmalig ein TextPredictor mit der Pipeline instantiiert. Auf dieser Instanz werden Texte klassifiziert. Der TextPredictor validiert die Grundfunktionen der Pipeline, deshalb benutzen wir die Pipeline nicht direkt (zumindest nicht dann, wenn wir zusätzlich zur Prediction noch einen Grad der Wahrscheinlichkeit oder hinterher eine Erklärung für die Klassifikation haben möchten.)

from prediction.text_predictor import TextPredictor
from training.persistence import load_pipeline

# Beispieltickets
tickets = [
    ("The integration stopped working unexpectedly"),
    ("I recently observed an unanticipated charge on my monthly subscription bill."),
    ("The dashboard has crashed. This may be due to a software incompatibility")
]

# lade Pipeline
trained_pipeline = load_pipeline(Path("models") / "LR" / "LR_pipeline.pkl")

# instantiiere Predictor 1x mit der Pipeline
predictor: TextPredictor = TextPredictor(trained_pipeline, "Alias für die Pipeline")

# predicte mit dem TextPredictor k Texte mit Wahrscheinlichkeiten:
results_with_context = [predictor.predict_label_proba_advantage(text) for text in tickets]

# ...und erhalte predictetes Label, dessen Wahrscheinlichkeit sowie Vorsprung vor dem
# Zweitplatzierten als Dict für ggf. automatische Zuteilung zu einer Queue
for text, result in zip(tickets, results_with_context):
    print(f"Text: '{text}'")
    print(f" Label: {result['label']}")
    print(f" Wahrscheinlichkeit: {result['proba']}")
    print(f" Vorsprung: {result['advantage']}")
    print("---")

Erhalte Erklärung mit TextExplainer und TextExplanation

  • TextExplainer wird mit TextPredictor-Instanz initialisiert und prüft die enthaltene Pipeline tiefergehend (sind Vectorizer und Classifier bekannt, können Wortbeiträge extrahiert werden?)
  • TextExplainer greift intern auf verschiedene Inspector-Klassen zurück
  • TextExplainer gibt globale Analysedaten über das Modell, falls verfügbar
  • TextExplainer erstellt TextExplanation-Objekt mit Analysedaten für einzelne Prediction
  • TextExplanation enthält nur noch die Analysedaten für das einzelne Klassifizierungsergebnis und Methoden zur Filterung und Anzeige dieser Daten
from explanation.text_explainer import TextExplainer
from explanation.text_explanation import TextExplanation
from IPython.display import display, HTML # für anzeige von html in jupyter notebook

# Instantiiere 1x einen TextExplainer mit der TextPredictor-Instanz:
explainer = TextExplainer(predictor)

# Beispieltext
input_text = "The dashboard has crashed. This may be due to a software incompatibility"

# Erzeuge eine TextExplanation-Instanz für den konkreten Text:
analysis_data : TextExplanation = explainer.create_explanation(input_text)

# Hieraus kann man nun die Entscheidung für die Klassifizierung erklären:
top_label = analysis_data.get_top_label()
top_probability = analysis_data.get_probability(top_label)
all_probabilities = analysis_data.get_sorted_probabilities()

# ...oder Wortbeiträge pro Label ausgeben. 
# Die alphabetische Ausgabe macht die Ngramme gut sichtbar.
analysis_data.get_word_contributions_for_label(top_label)

# Wortbeiträge können auch gefiltert und (absteigend) nach Gewicht sortiert
# ausgegeben werden:
# optionen: "positive", "negative", "all" (default, wird unsigned sortiert)
analysis_data.get_top_k_word_contributions_for_label(
    label=top_label,
    top_k=20,
    sign_filter="positive"
    )

# Erzeugt html-String, bei dem Tokens nach Gewicht farblich gehighlighted sind 
# mit zusätzlichen Wortgewichten als Tooltips.
text_as_html = analysis_data.get_colorized_text_as_html(top_label)

# in .ipynb-Datei anzeigen
display(HTML(text_as_html))

# oder in .py-Datei normal printen
print(text_as_html)

# Gibt Plots aus (es wird ein png erzeugt, das aufgerufen werden kann)
path_to_png_contributions = plot_word_contributions(label=top_label, top_k=5)
path_to_png_probabilities = plot_top_k_class_probabilities(top_k=5)

# Die Erklärung kann auch mit den wesentlichen Einflüssen als Fließtext ausgegeben werden
print(get_prosa_explanation())

Hier die Docstrings der beschriebenen Klassen:

src.prediction.text_predictor

TextPredictor

Diese Klasse kapselt eine scikit-learn Pipeline, die mindestens einen Classifier und einen Vectorizer enthält. Ihre Aufgabe ist, Predictions und Wahrscheinlichkeiten einzelner Klassen zu liefern. Sie kann für die tiefergehende Analyse der Prediction an einen TextExplainer übergeben werden, für den sie dann schon die grundlegende Validierung der Pipeline übernommen hat und ihm direkt die wesentlichen Komponenten und Prädiktions-Methoden aus der Pipeline zur Verfügung stellt. Die Bezeichnungen der Pipeline-Steps werden zentral in der xai_config.yaml notiert.

Attributes:

Name Type Description
pipeline Pipeline

Eine scikit-learn-Pipeline mit trainiertem Classifier und Vectorizer.

alias str

Ein frei wählbarer Name zur Identifikation der Pipeline.

classifier ClassifierMixin

Der trainierte Classifier aus der Pipeline.

vectorizer Any

Der Vectorizer aus der Pipeline. Der Typ des Vectorizers ist für den Predictor unerheblich.

class_names List[str]

Liste der Klassen, die der Classifier kennt.

Raises:

Type Description
ValueError

Wenn:

  • die Pipeline keinen auffindbaren Classifier enthält,
  • die Pipeline keinen auffindbaren Vectorizer enthält,
  • der Classifier nicht trainiert wurde oder kein 'classes_'-Attribut besitzt,
  • die Pipeline keine Methode 'predict()' besitzt,
  • die Pipeline keine Methode 'predict_proba()' besitzt (z.B. unmodifizierter LinearSVC).
Author

irene

Source code in src\prediction\text_predictor.py
class TextPredictor:
    """
    Diese Klasse kapselt eine scikit-learn Pipeline, die mindestens einen Classifier
    und einen Vectorizer enthält. Ihre Aufgabe ist, Predictions und
    Wahrscheinlichkeiten einzelner Klassen zu liefern. Sie kann für die tiefergehende
    Analyse der Prediction an einen TextExplainer übergeben werden, für den sie dann
    schon die grundlegende Validierung der Pipeline übernommen hat und ihm direkt die
    wesentlichen Komponenten und Prädiktions-Methoden aus der Pipeline zur Verfügung
    stellt.
    Die Bezeichnungen der Pipeline-Steps werden zentral in der xai_config.yaml notiert.

    Attributes:
        pipeline (Pipeline): Eine scikit-learn-Pipeline mit trainiertem Classifier und
            Vectorizer.
        alias (str): Ein frei wählbarer Name zur Identifikation der Pipeline.
        classifier (ClassifierMixin): Der trainierte Classifier aus der Pipeline.
        vectorizer (Any): Der Vectorizer aus der Pipeline. Der Typ des Vectorizers ist
            für den Predictor unerheblich.
        class_names (List[str]): Liste der Klassen, die der Classifier kennt.

    Raises:
        ValueError : Wenn:

            - die Pipeline keinen auffindbaren Classifier enthält,
            - die Pipeline keinen auffindbaren Vectorizer enthält,
            - der Classifier nicht trainiert wurde oder kein 'classes_'-Attribut
                besitzt,
            - die Pipeline keine Methode 'predict()' besitzt,
            - die Pipeline keine Methode 'predict_proba()' besitzt (z.B. unmodifizierter
                LinearSVC).

    Author:
        irene
    """

    def __init__(
        self,
        pipeline: Pipeline,
        alias: str,
        config_path: str = "config/xai_config.yaml",
    ):
        """
        Initialisiert den TextPredictor mit einer Pipeline.

        Args:
            pipeline (Pipeline): Eine trainierte scikit-learn-Pipeline.
            alias (str): Bezeichner für die Pipeline.

        Raises:
            ValueError: Wenn die Pipeline unvollständig ist s.o.
        """
        self._pipeline: Pipeline = pipeline
        self._alias: str = alias

        # baut den übergebenen Pfad zu absolutem Pfad um (wichtig für Verwendung der
        # Klasse in Notebooks)
        absolute_path = self._create_absolute_path(config_path)
        # lädt die Defaultwerte aus der Konfigurationsdatei
        self._config = load_config(absolute_path)
        self._model_step_name = self.config["pipeline"]["model_step_name"]
        self._vectorizer_step_name = self.config["pipeline"]["vectorizer_step_name"]

        # holt den Classifier und prüft ihn auf Vollständigkeit, wirft ggf. ValueError
        self._classifier: ClassifierMixin = self._get_classifier_from_pipeline()
        self._check_is_trained()
        self._check_has_predict_method()
        self._check_has_predict_proba_method()
        self._class_names: List[str] = self.pipeline.classes_.tolist()
        self._num_class_names: int = len(self.class_names)
        self._classifier_type_name: str = type(self._classifier).__name__

        # holt vectorizer und wirft ggf. ValueError
        self._vectorizer = self._get_vectorizer_from_pipeline()

    # Ich möchte nicht, dass die Instanzvariablen unabhängig von der Prüfung im
    # Konstruktor neu gesetzt werden, deshalb hier die Zugriffsmethoden:
    @property
    def pipeline(self) -> Pipeline:
        return self._pipeline

    @property
    def alias(self) -> str:
        return self._alias

    @property
    def config(self) -> dict:
        return self._config

    @property
    def model_step_name(self) -> str:
        return self._model_step_name

    @property
    def vectorizer_step_name(self) -> str:
        return self._vectorizer_step_name

    @property
    def classifier(self) -> ClassifierMixin:
        return self._classifier

    @property
    def classifier_type_name(self) -> str:
        return self._classifier_type_name

    @property
    def class_names(self) -> List[str]:
        return self._class_names

    @property
    def num_class_names(self) -> int:
        return self._num_class_names

    @property
    def vectorizer(self) -> BaseEstimator:
        return self._vectorizer


    def predict_label(self, input_text: str) -> str:
        """
        Führt eine Prediction durch und gibt nur das wahrscheinlichste Label zurück.
        """
        series_input = pd.Series([input_text])
        return self.pipeline.predict(series_input)[0]

    def predict_label_and_proba(self, input_text: str) -> Dict[str, float]:
        """
        Führt eine Prediction durch und gibt ein Dict mit Label als Key
        und Wahrscheinlichkeit als Value zurück.
        """
        sorted_probas = self.predict_proba_all_sorted(input_text)
        top_label, top_proba = sorted_probas[0]

        return {"label": top_label, "proba": top_proba}

    def predict_proba_all_labels(self, input_text: str) -> Dict[str, float]:
        """
        Führt eine Prediction durch und gibt die Wahrscheinlichkeiten sämtlicher
        Klassen zurück. Im Fall des ComplementNB-Modells werden die Probabilities
        nachgeschärft, um Unterschiede deutlicher sehen zu können.

        Returns:
            Dict[str, float]: Dict mit Klassen als Keys und Wahrscheinlichkeiten als
                Values.
        """
        series_input = pd.Series([input_text])
        probas = self.pipeline.predict_proba(series_input)[0]
        probas_as_dict = dict(zip(self.class_names, probas))

        # Unterschiede in ComplementNB sind kaum erkennbar, deshalb ist dieses Modell
        # nur schwer vergleichbar mit den anderen. Hier muss ich die Unterschiede
        # verstärken.
        if self.classifier_type_name == "ComplementNB":
            return self._enhance_probability_contrast(probas_as_dict)
        else:
            return probas_as_dict

    def predict_proba_all_sorted(self, input_text: str) -> List[Tuple[str, float]]:
        """
        Führt eine Prediction durch und gibt eine nach Wahrscheinlichkeit
        absteigend sortierte Liste von (Label, Wahrscheinlichkeit)-Tupeln zurück.

        Returns:
            List[Tuple[str, float]]: Sortierte Liste aller Labels mit zugehöriger
                Wahrscheinlichkeit.
        """
        all_probas = self.predict_proba_all_labels(input_text)
        sorted_probas = sorted(
            all_probas.items(), key=lambda item: item[1], reverse=True
        )
        return sorted_probas

    def predict_label_proba_advantage(
        self, input_text: str
    ) -> Dict[str, Union[str, float]]:
        """
        Führt eine Prediction durch, sortiert die Klassen nach Wahrscheinlichkeit
        und berechnet den Vorsprung (Differenz) der höchsten Wahrscheinlichkeit
        zur zweithöchsten.

        Returns:
            Dict[str, float]:
                "label": das wahrscheinlichste Label,
                "proba": dessen Wahrscheinlichkeit,
                "advantage": Abstand zur zweithöchsten Wahrscheinlichkeit.
        """
        sorted_probas = self.predict_proba_all_sorted(input_text)

        # Top 1 und Top 2
        top_label, top_proba = sorted_probas[0]
        _, second_proba = sorted_probas[1]

        # Vorsprung des Top-Labels
        advantage = top_proba - second_proba

        return {
            "label": top_label,
            "proba": top_proba,
            "advantage": advantage,
        }

    def _get_classifier_from_pipeline(self) -> ClassifierMixin:
        """Gibt den Classifier aus der Pipeline zurück oder wirft ValueError."""

        # 1. prüft, ob etwas da ist, was wie ein Classifier aussieht
        if self.pipeline is None or not hasattr(self.pipeline, "named_steps"):
            raise ValueError(
                self._build_error_message(
                    "[Pr000] Die Pipeline besitzt keinen Classifier."
                )
            )

        # 2. versucht, etwas zu holen, das wie der Classifier heißt
        try:
            candidate = self.pipeline.named_steps[self.model_step_name]
        except KeyError:
            raise ValueError(
                self._build_error_message(
                    f"[Pr001] Pipeline enthält keinen Step namens "
                    f"'{self.model_step_name}'."
                )
            )

        # 3. prüft, ob der Kandidat wirklich ein Classifier ist
        if not isinstance(candidate, ClassifierMixin):
            raise ValueError(
                self._build_error_message(
                    f"[Pr003] Der Step '{self.model_step_name}' ist kein Classifier"
                    f" (ClassifierMixin)."
                )
            )

        # 4. alle Prüfungen bestanden, Classifier kann ausgegeben werden
        return candidate

    def _check_is_trained(self):
        """
        Prüft, ob die Pipeline trainiert wurde (Vorhandensein von 'classes_') und wirft
        ValueError bei Fehlschlag.
        """
        if not hasattr(self.pipeline, "classes_") or self.pipeline.classes_ is None:
            raise ValueError(
                self._build_error_message(
                    "[Pr004] Die Pipeline wurde nicht trainiert oder hat kein"
                    " 'classes_'-Attribut."
                )
            )

    def _check_has_predict_method(self):
        """
        Prüft, ob der Classifier eine predict-Methode besitzt und wirft ValueError bei
        Fehlschlag.
        """
        if not hasattr(self.classifier, "predict"):
            raise ValueError(
                self._build_error_message(
                    "[Pr005] Der Classifier besitzt keine 'predict()'-Methode."
                )
            )

    def _check_has_predict_proba_method(self):
        """
        Prüft, ob der Classifier eine predict_proba-Methode besitzt und wirft
        ValueError bei Fehlschlag.
        """
        if not hasattr(self.classifier, "predict_proba"):
            raise ValueError(
                self._build_error_message(
                    "[Pr006] Der Classifier besitzt keine 'predict_proba()'-Methode."
                )
            )

    def _get_vectorizer_from_pipeline(self) -> BaseEstimator:
        """Gibt den Vectorizer aus der Pipeline zurück oder wirft ValueError."""
        vectorizer = find_step_by_name(self.pipeline, self.vectorizer_step_name)
        if vectorizer is None:
            raise ValueError(
                self._build_error_message(
                    f"[Pr007] Pipeline enthält keinen Step"
                    f" '{self.vectorizer_step_name}'."
                )
            )
        return vectorizer

    def _enhance_probability_contrast(self,
                                      probas: Dict[str, float],
                                      min_target_contrast:float = 0.2
                                      ) -> Dict[str, float]:
        """
        Für Modelle, die kategorisch eine sehr schwache Varianz in ihren
        Prädiktionswahrscheinlichkeiten haben, sollen die Unterschiede verstärkt werden.
        Insbesondere ComplementNB kann hierdurch aussagekräftiger werden. Sollten
        die Wahrscheinlichkeiten bereits oberhalb einer Unterscheidbarkeitsschwelle
        liegen (hier 0.2), werden sie unverändert zurückgegeben.

        Args:
            probas (Dict[str, float]): Ein Dictionary der Form (label, probability)
            min_target_contrast (float): Der Schwellwert, unterhalb dessen die
                Probabilities verändert werden

        Returns:
            Dict[str, float]: Ein Dictionary der Form (label, probability) mit stärkeren
                Unterschieden in der Probability
        """

        all_probas = probas
        min_value: float = min(all_probas.values())
        max_value: float = max(all_probas.values())
        diff: float = max_value - min_value

        if diff > min_target_contrast:
            # werte sind gut genug unterscheidbar, unverändert zurückgeben
            return all_probas

        if  math.isclose(diff, 0.0, abs_tol=1e-12):
            # Werte sind so wenig unterscheidbar, dass ich sie nicht verstärken kann
            # (vielleicht sind auch alle 0.0 und deshalb nicht verschieden)
            # Deshalb lieber: unverändert zurückgeben
            return all_probas

        # Hier ziehe ich das Minimum von sämtlichen Wahrscheinlichkeiten ab, so dass
        # das kleinste Label den Wert 0.0 bekommt und die anderen Label ausschließlich
        # die Differenz bekommen.
        diff_probas = {label: proba - min_value for label, proba in all_probas.items()}

        # das ist mein neuer Wahrscheinlichkeitsraum
        sum_adjusted = sum(diff_probas.values())
        # auf den ich nun meine Werte aufziehe
        normalized_probas = {label: proba / sum_adjusted
                             for label, proba in diff_probas.items()}

        return normalized_probas

    def _create_absolute_path(self, rel_or_abs_path: str) -> str:
        """
        Prüft, ob der übergebene Pfad ein relativer Pfad ist und baut ihn zu einem
        absoluten Pfad um. Absolute Pfade werden hierbei nicht verändert. Diese Methode
        wird benötigt, um die Konfigurationsdatei laden zu können, falls diese Klasse
        im Jupyter Notebook instantiiert wird.

        Args:
            rel_or_abs_path (str): Ein absoluter oder relativer Pfad.

        Returns:
            str: Ein absoluter Pfad.

        Raises:
            FileNotFoundError: Falls die Zieldatei, auf die der Pfad weist, nicht
                existiert.
        """
        # absoluter Pfad wird unverändert zurückgegeben
        if os.path.isabs(rel_or_abs_path):
            absolute_path = rel_or_abs_path
        else:
            # berechnet den Pfad relativ zum Projektroot
            current_dir = os.path.dirname(__file__)
            # ich bin in Jafar/src/text_predictor.py, muss also zwei Ebenen hoch
            project_root = os.path.abspath(os.path.join(current_dir, "..", ".."))

            absolute_path = os.path.join(project_root, rel_or_abs_path)

        if not os.path.exists(absolute_path):
            raise FileNotFoundError(f"Datei nicht gefunden: {absolute_path}")

        return absolute_path

    def _build_error_message(self, message: str) -> str:
        """Ergänzt eine Fehlermeldung um Klassen- und Pipelineinformationen."""
        return (
            f"Klasse '{self.__class__.__name__}' mit Pipeline '{self.alias}': "
            + message
        )

__init__(pipeline, alias, config_path='config/xai_config.yaml')

Initialisiert den TextPredictor mit einer Pipeline.

Parameters:

Name Type Description Default
pipeline Pipeline

Eine trainierte scikit-learn-Pipeline.

required
alias str

Bezeichner für die Pipeline.

required

Raises:

Type Description
ValueError

Wenn die Pipeline unvollständig ist s.o.

Source code in src\prediction\text_predictor.py
def __init__(
    self,
    pipeline: Pipeline,
    alias: str,
    config_path: str = "config/xai_config.yaml",
):
    """
    Initialisiert den TextPredictor mit einer Pipeline.

    Args:
        pipeline (Pipeline): Eine trainierte scikit-learn-Pipeline.
        alias (str): Bezeichner für die Pipeline.

    Raises:
        ValueError: Wenn die Pipeline unvollständig ist s.o.
    """
    self._pipeline: Pipeline = pipeline
    self._alias: str = alias

    # baut den übergebenen Pfad zu absolutem Pfad um (wichtig für Verwendung der
    # Klasse in Notebooks)
    absolute_path = self._create_absolute_path(config_path)
    # lädt die Defaultwerte aus der Konfigurationsdatei
    self._config = load_config(absolute_path)
    self._model_step_name = self.config["pipeline"]["model_step_name"]
    self._vectorizer_step_name = self.config["pipeline"]["vectorizer_step_name"]

    # holt den Classifier und prüft ihn auf Vollständigkeit, wirft ggf. ValueError
    self._classifier: ClassifierMixin = self._get_classifier_from_pipeline()
    self._check_is_trained()
    self._check_has_predict_method()
    self._check_has_predict_proba_method()
    self._class_names: List[str] = self.pipeline.classes_.tolist()
    self._num_class_names: int = len(self.class_names)
    self._classifier_type_name: str = type(self._classifier).__name__

    # holt vectorizer und wirft ggf. ValueError
    self._vectorizer = self._get_vectorizer_from_pipeline()

predict_label(input_text)

Führt eine Prediction durch und gibt nur das wahrscheinlichste Label zurück.

Source code in src\prediction\text_predictor.py
def predict_label(self, input_text: str) -> str:
    """
    Führt eine Prediction durch und gibt nur das wahrscheinlichste Label zurück.
    """
    series_input = pd.Series([input_text])
    return self.pipeline.predict(series_input)[0]

predict_label_and_proba(input_text)

Führt eine Prediction durch und gibt ein Dict mit Label als Key und Wahrscheinlichkeit als Value zurück.

Source code in src\prediction\text_predictor.py
def predict_label_and_proba(self, input_text: str) -> Dict[str, float]:
    """
    Führt eine Prediction durch und gibt ein Dict mit Label als Key
    und Wahrscheinlichkeit als Value zurück.
    """
    sorted_probas = self.predict_proba_all_sorted(input_text)
    top_label, top_proba = sorted_probas[0]

    return {"label": top_label, "proba": top_proba}

predict_label_proba_advantage(input_text)

Führt eine Prediction durch, sortiert die Klassen nach Wahrscheinlichkeit und berechnet den Vorsprung (Differenz) der höchsten Wahrscheinlichkeit zur zweithöchsten.

Returns:

Type Description
Dict[str, Union[str, float]]

Dict[str, float]: "label": das wahrscheinlichste Label, "proba": dessen Wahrscheinlichkeit, "advantage": Abstand zur zweithöchsten Wahrscheinlichkeit.

Source code in src\prediction\text_predictor.py
def predict_label_proba_advantage(
    self, input_text: str
) -> Dict[str, Union[str, float]]:
    """
    Führt eine Prediction durch, sortiert die Klassen nach Wahrscheinlichkeit
    und berechnet den Vorsprung (Differenz) der höchsten Wahrscheinlichkeit
    zur zweithöchsten.

    Returns:
        Dict[str, float]:
            "label": das wahrscheinlichste Label,
            "proba": dessen Wahrscheinlichkeit,
            "advantage": Abstand zur zweithöchsten Wahrscheinlichkeit.
    """
    sorted_probas = self.predict_proba_all_sorted(input_text)

    # Top 1 und Top 2
    top_label, top_proba = sorted_probas[0]
    _, second_proba = sorted_probas[1]

    # Vorsprung des Top-Labels
    advantage = top_proba - second_proba

    return {
        "label": top_label,
        "proba": top_proba,
        "advantage": advantage,
    }

predict_proba_all_labels(input_text)

Führt eine Prediction durch und gibt die Wahrscheinlichkeiten sämtlicher Klassen zurück. Im Fall des ComplementNB-Modells werden die Probabilities nachgeschärft, um Unterschiede deutlicher sehen zu können.

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Dict mit Klassen als Keys und Wahrscheinlichkeiten als Values.

Source code in src\prediction\text_predictor.py
def predict_proba_all_labels(self, input_text: str) -> Dict[str, float]:
    """
    Führt eine Prediction durch und gibt die Wahrscheinlichkeiten sämtlicher
    Klassen zurück. Im Fall des ComplementNB-Modells werden die Probabilities
    nachgeschärft, um Unterschiede deutlicher sehen zu können.

    Returns:
        Dict[str, float]: Dict mit Klassen als Keys und Wahrscheinlichkeiten als
            Values.
    """
    series_input = pd.Series([input_text])
    probas = self.pipeline.predict_proba(series_input)[0]
    probas_as_dict = dict(zip(self.class_names, probas))

    # Unterschiede in ComplementNB sind kaum erkennbar, deshalb ist dieses Modell
    # nur schwer vergleichbar mit den anderen. Hier muss ich die Unterschiede
    # verstärken.
    if self.classifier_type_name == "ComplementNB":
        return self._enhance_probability_contrast(probas_as_dict)
    else:
        return probas_as_dict

predict_proba_all_sorted(input_text)

Führt eine Prediction durch und gibt eine nach Wahrscheinlichkeit absteigend sortierte Liste von (Label, Wahrscheinlichkeit)-Tupeln zurück.

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: Sortierte Liste aller Labels mit zugehöriger Wahrscheinlichkeit.

Source code in src\prediction\text_predictor.py
def predict_proba_all_sorted(self, input_text: str) -> List[Tuple[str, float]]:
    """
    Führt eine Prediction durch und gibt eine nach Wahrscheinlichkeit
    absteigend sortierte Liste von (Label, Wahrscheinlichkeit)-Tupeln zurück.

    Returns:
        List[Tuple[str, float]]: Sortierte Liste aller Labels mit zugehöriger
            Wahrscheinlichkeit.
    """
    all_probas = self.predict_proba_all_labels(input_text)
    sorted_probas = sorted(
        all_probas.items(), key=lambda item: item[1], reverse=True
    )
    return sorted_probas

src.explanation.text_explainer

TextExplainer

Der TextExplainer wird erzeugt, wenn man Informationen über das Modell selbst haben möchte oder wenn man eine einzelne Textklassifizierung erklären möchte. Der TextExplainer bekommt dazu den TextPredictor (mit darin enthaltener Pipeline) injiziert und gibt Analysedaten aus, die die einzelne Prädiktion nachvollziehbar machen, insbesondere den Beitrag von Wörtern zur Prediktionsentscheidung. Diese Daten können entweder separat ausgegeben werden, oder gesammelt in einer TextExplanation-Instanz (mit weiteren Auswertungsmethoden). Die Analysedaten werden nach Möglichkeit aus dem (in der Pipeline enthaltenen) Modell direkt gewonnen, aber falls dieses unmittelbar keine hinreichenden Erklärungen liefert (Blackbox), wird ein LIME-oder Shap-Explainer als Fallback aufgesetzt. Die Art der Berechnungen der Wortbeiträge wird den Inspector-Klassen überlassen (Strategy-Pattern).

Der TextExplainer kann aktuell die Classifier und Vectorizer aus der Pipeline auswerten, die in folgenden Klassenvariablen definiert sind: - für den Vectorizer in _ALLOWED_VECTORIZER_TYPES - für den Classifier in _ALLOWED_CLASSIFIER_TYPES Der Classifier LinearSVC ist nur kompatibel, wenn er vor Training mit einer predict_proba()-Methode ausgerüstet wurde

Attributes:

Name Type Description
alias str

Ein frei wählbarer Name zur Identifikation der Pipeline.

class_names List[str]

Liste der Klassenbezeichnungen, die der Klassifikator kennt.

Raises:

Type Description
TypeError

Wenn der Classifier oder der Vectorizer nicht zu den unterstützten Typen gehört.

Author

irene

Source code in src\explanation\text_explainer.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
class TextExplainer:
    """
    Der TextExplainer wird erzeugt, wenn man Informationen über das Modell
    selbst haben möchte oder wenn man eine einzelne Textklassifizierung erklären möchte.
    Der TextExplainer bekommt dazu den TextPredictor (mit darin enthaltener Pipeline)
    injiziert und gibt Analysedaten aus, die die einzelne Prädiktion nachvollziehbar
    machen, insbesondere den Beitrag von Wörtern zur Prediktionsentscheidung.
    Diese Daten können entweder separat ausgegeben werden, oder gesammelt in einer
    TextExplanation-Instanz (mit weiteren Auswertungsmethoden).
    Die Analysedaten werden nach Möglichkeit aus dem (in der Pipeline enthaltenen)
    Modell direkt gewonnen, aber falls dieses unmittelbar keine hinreichenden
    Erklärungen liefert (Blackbox), wird ein LIME-oder Shap-Explainer als Fallback
    aufgesetzt. Die Art der Berechnungen der Wortbeiträge wird den Inspector-Klassen
    überlassen (Strategy-Pattern).

    Der TextExplainer kann aktuell die Classifier und Vectorizer aus der Pipeline
    auswerten, die in folgenden Klassenvariablen definiert sind:
        - für den Vectorizer in _ALLOWED_VECTORIZER_TYPES
        - für den Classifier in _ALLOWED_CLASSIFIER_TYPES
        Der Classifier LinearSVC ist nur kompatibel, wenn er vor Training mit einer
        predict_proba()-Methode ausgerüstet wurde

    Attributes:
        alias (str): Ein frei wählbarer Name zur Identifikation der Pipeline.
        class_names (List[str]): Liste der Klassenbezeichnungen, die der Klassifikator
            kennt.

    Raises:
        TypeError: Wenn der Classifier oder der Vectorizer nicht zu den unterstützten
            Typen gehört.

    Author:
        irene
    """

    # NOTICE: Classifier LinearSVC ist nur kompatibel, wenn er vor Training mit einer
    #   predict_proba()-Methode ausgerüstet wurde
    _ALLOWED_CLASSIFIER_TYPES: ClassVar[Tuple[Type, ...]] = (
        SVC,
        LogisticRegression,
        MultinomialNB,
        ComplementNB,
        KNeighborsClassifier,
        RandomForestClassifier,
        DecisionTreeClassifier,
    )

    # die aktuell berücksichtigen Vectorizer
    _ALLOWED_VECTORIZER_TYPES: ClassVar[Tuple[Type, ...]] = (TfidfVectorizer,)

    def __init__(self, predictor: TextPredictor, force_agnostic: bool = False):
        """
        Initialisiert die Klasse mit dem übergebenen Predictor und prüft, ob der
        enthaltene Classifier und der Vectorizer zulässig sind.

        Die erlaubten Datentypen sind als Klassenvariablen definiert:
        - `_ALLOWED_VECTORIZER_TYPES` für den Vectorizer
        - `_ALLOWED_CLASSIFIER_TYPES` für den Classifier

        Args:
            predictor (TextPredictor): Ein TextPredictor-Objekt mit einer
                scikit-learn-Pipeline.
            force_agnostic (bool): Optional zum Testen, hier erzwinge ich die Benutzung
                des LIMETextExplainers oder SHAP (je nach config in xai-config.yaml)

        Raises:
            TypeError: Wenn der Classifier oder der Vectorizer nicht zu den
                unterstützten Typen gehört.
        """

        # testparameter, wenn ich dezidiert LIME/SHAP testen möchte, unabhängig von
        # der Erklärbarkeit des in der Pipeline enthaltenen Modells
        self._force_agnostic = force_agnostic

        # weist predictor zu, enthält Pipeline mit trainiertem Modell
        self._predictor = predictor
        self._pipeline: Pipeline = self.predictor.pipeline

        # ggf. für Kontrollausgaben
        self._alias: str = self.predictor.alias

        # lädt die Defaultwerte aus der Konfigurationsdatei
        self._config = self.predictor.config

        # prüft, ob Classifier zugewiesen werden darf
        self._validate_type(self.predictor.classifier, self._ALLOWED_CLASSIFIER_TYPES)

        # Prüfung bestanden: classifier darf zugewiesen werden
        self._classifier = self.predictor.classifier
        self._class_names: List[str] = self._classifier.classes_.tolist()
        self._num_class_names: int = len(self.class_names)
        self._classifier_type_name: str = type(self._classifier).__name__

        # prüft Vectorizer
        self._validate_type(self.predictor.vectorizer, self._ALLOWED_VECTORIZER_TYPES)

        # Prüfung bestanden: Vectorizer darf zugewiesen werden
        self._vectorizer = self.predictor.vectorizer

        # je nach Modell wird der passende Inspector erzeugt
        self._inspector: BaseInspector = self._create_inspector()

        # Baseline-Prognosen auf leerem Text:
        self._baseline_probs = self.predictor.predict_proba_all_labels("")

    @property
    def predictor(self) -> TextPredictor:
        return self._predictor

    @property
    def alias(self) -> str:
        return self._alias

    @property
    def class_names(self) -> List[str]:
        return self._class_names

    def _create_inspector(self) -> BaseInspector:
        if self._force_agnostic:
            print("TextExplainer erzeugt AgnosticInspector")
            return AgnosticInspector(self.predictor)
        elif self._has_linear_weights():
            print("TextExplainer erzeugt LinearModelInspector")
            return LinearModelInspector(self.predictor)
        elif self._has_logarithmic_weights():
            print("TextExplainer erzeugt LogarithmicModelInspector")
            return LogarithmicModelInspector(self.predictor)
        else:
            print("TextExplainer erzeugt AgnosticInspector")
            return AgnosticInspector(self.predictor)

    def _has_linear_weights(self) -> bool:
        """
        Prüft, ob das Modell lineare Gewichte hat.

        Returns:
            bool: True, wenn es ein lineares Modell mit den geforderten Attributen ist.
        """
        model = self._classifier
        # allowed_types = (LogisticRegression,LinearSVC)
        return (
            hasattr(model, "coef_")
            and hasattr(model, "intercept_")
            and model.coef_ is not None
            and model.intercept_ is not None
        )

    def _has_logarithmic_weights(self) -> bool:
        """
        Prüft, ob das Modell logarithmische Wahrscheinlichkeiten als Gewichte hat.

        Returns:
            bool: True, wenn es ein Naive Bayes Modell mit den geforderten Attributen
                ist.
        """
        model = self._classifier
        return (
            hasattr(model, "feature_log_prob_")
            and hasattr(model, "class_log_prior_")
            and model.feature_log_prob_ is not None
            and model.class_log_prior_ is not None
        )

    def get_label_index(self, label: str) -> int:
        """Gibt den Index für das Label zurück oder wirft ValueError"""
        if label not in self.class_names:
            raise ValueError(
                self._build_error_message(
                    f"[Er001] Kategorie {label!r} nicht in Klassen:"
                    f" {self.class_names!r}"
                )
            )
        return list(self.class_names).index(label)

    def get_vectorizer_ngram_range(self) -> tuple[int, int]:
        """
        Gibt zurück, aus wievielen Wörtern ein Token (für model_weights
        und word_contributions) zusammengesetzt sein darf.
        z.B.: (1, 3) maximal drei Wörter

        Returns:
            tuple[int, int]: Mindestzahl, Höchstzahl
        """
        return self._vectorizer.ngram_range

    def predict_proba_all_labels(self, input_text: str) -> Dict[str, float]:
        """
        Wrapper: Gibt für einen einzelnen Text die Wahrscheinlichkeiten sämtlicher
        Klassen zurück.

        Args:
            input_text (str): Ein Textstring, der klassifiziert werden soll.

        Returns:
            Dict[str, float]: Ein Dictionary mit Klassen als Keys und deren
            Wahrscheinlichkeiten als Values.
        """
        return self.predictor.predict_proba_all_labels(input_text=input_text)

    def inspect_model_weights_top_k(
        self, label: str, top_k: int = 20, sign_filter: str = "all"
    ) -> List[Tuple[str, float]]:
        """
        Gibt die Top-k Modellgewichte für ein bestimmtes Label zurück. Die Gewichte
        beziehen sich auf das Modell selbst und sind unabhängig von einzelnen
        Predictions. Falls das Modell keine Gewichte ausgibt, wird eine leere Liste
        zurückgegeben. Für die Sortierung der Gewichte sind die Inspector-Klassen
        zuständig.
        Für lineare Modelle gilt: positive = wichtig pro Label, negative = wichtig
        contra Label, all = nach wort alphabetisch sortiert
        Für logarithmische Modelle gilt: positive = wichtig pro Label, negative =
        unwichtig, all = nach wort alphabetisch sortiert

        Args:
            label (str): Das Label, für das die Top-Wörter ausgegeben werden.
            top_k (Optional, int): Anzahl der Top-Wörter nach Gewichtung.
            sign_filter (Optional, str): Filtert nach Vorzeichen der Modellgewichte
                mit erlaubten Werten "pro", "contra", "all"

        Returns:
            List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert
                nach Betrag. Ersatzweise eine leere Liste.

        Raises:
            ValueError:
                - Wenn falscher Wert für 'valid_filters' übergeben wird
                - Wenn das übergebene Label nicht existiert.
        """
        valid_filters = {"pro", "contra", "all"}
        if sign_filter not in valid_filters:
            raise ValueError(
                f"[Er001] Ungültiger Wert für sign_filter: {sign_filter!r}. "
                f"Erlaubt sind: {', '.join(valid_filters)}."
            )

        # prüft, ob das Label existiert und gibt index zurück
        label_index = self.get_label_index(label)

        # die zum Modell passende Inspector-Instanz führt aus
        return self._inspector.inspect_model_weights_top_k(label_index=label_index,
                                                           top_k=top_k,
                                                           sign_filter=sign_filter)


    def inspect_model_weights(self, label: str) -> List[Tuple[str, float | None]]:
        """
        Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche
        Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen
        sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions.
        Falls keine Gewichte berechnet werden können, wird None zurückgegeben.

        Args:
            label (str): Das Label, für das die gewichteten Wörter ausgegeben werden.

        Returns:
            List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel

        Raises:
            ValueError: Wenn das übergebene Label nicht existiert.

        """
        # prüfe vorab, ob label existiert
        label_index = self.get_label_index(label)
        # die zum Modell passende Inspector-Instanz führt aus
        return self._inspector.inspect_model_weights_by_index(label_index)

    def get_token_type(self) -> str:
        """
        Gibt entweder "raw" oder "processed" aus den Inspectoren aus, je nach dem, ob
        dort die Wortbeiträge auf dem Rohtext oder auf dem vorverarbeiteten Text
        ermittelt werden.

        Returns:
            str: "raw" oder "processed"
        """
        return self._inspector.get_token_type()

    def _eliminate_stopwords(self, input_text: str) -> str:
        """
        Gibt den Text ohne Stopwords zurück. Ich führe hier nicht das gesamte
        Preprocessig aus, sondern finde nur die Wörter aus dem Ausgangstext, die durch
        das Preprocessing als Stopwords erkannt und eliminiert werden. Der übrige Text
        soll möglichst Original bleiben.

        Returns:
            str: der Originaltext ohne Stopwords
        """
        # zerlegt Eingabe in Token
        original_tokens = input_text.split()

        # führt die Vorverarbeitung entsprechend der Pipeline aus
        preprocessed_tokens = [
            self._preprocess_before_vectorizer(t) for t in original_tokens]

        # entfernt alle Token, die nach Preprocessing leer sind: Stopwords
        tokens_without_stopwords = [
            orig for orig, prep in zip(original_tokens, preprocessed_tokens) if prep
        ]

        model_visible_string = " ".join(tokens_without_stopwords)

        return model_visible_string


    def compute_word_contributions(
        self, input_text: str
    ) -> Dict[str, Dict[str, float]]:
        """
        Ermittelt die Beiträge der Wörter des eingegebenen Textes zur Klassifikation
        sämtlicher Klassen mithilfe der jeweiligen, zum Modell passenden
        Inspector-Instanzen.

        Args:
            input_text (str): Der zu erklärende Text

        Returns:
            Dict[label_name, Dict[word, contribution]]: Ein Dictionary, das pro Label
                jedes einzelne Wort als Key und sein Beitrag zur Entscheidung als Value
                zuordnet. Positive Werte sprechen für die Klassifikation, negative
                dagegen.

        Raises:
            ValueError: Wenn der Text leer oder nur aus Leerzeichen besteht.
        """
        self._check_input_text(input_text)

        # Preprocessing (alle Schritte vor dem Vektorizer)
        clean_text = self._preprocess_before_vectorizer(input_text)

        # Vektorisieren
        feature_values, feature_names = self._vectorize(clean_text)

        # für agnostischen Explainer: alle Wörter rauswerfen, die das "echte Modell"
        # nie gesehen hat. Diese Wörter tauchen als relevant in LIME auf, obwohl das
        # echte Modell ihnen keinen Wert zuweisen kann.
        relevant_text = self._eliminate_stopwords(input_text)


        # wortbeiträge je nach methoden des Modells ermitteln
        # beachte: linear und log-Inspectoren brauchen nur den Vector, die agnostischen
        # den Rohtext (hier bereinigt von Stopwords)
        word_contributions = self._inspector.compute_word_contributions(
            relevant_text, feature_values, feature_names
        )

        return word_contributions

    def get_biases_all_classes(self) -> Dict[str, Dict[str, Optional[float]]]:
        """
        Gibt die Grundwahrscheinlichkeiten für jede Klasse zurück, die für die
        Entscheidung zusätzlich zu den Wortgewichten relevant sind (quasi als
        eingabeunabhängiges "Vorurteil").
        Da die Bias-/Intercept-Werte der logistischen Modelle One-vs.-All-
        Wahrscheinlichkeiten ausgeben, sind diese eher statistisch relevant.
        Wenn ein Modell keine Grundwahrscheinlichkeiten berechnet, wird ersatzweise für
        alle Klassen None zurückgegeben.

        Returns:
            Dict[str, Dict[str, float]]: Klassenname ('raw_bias': Bias,
            'prior_prob': Bias zur Wahrscheinlichkeit umgerechnet,
            'baseline_prob': Grundwahrscheinlichkeit auf Leerstring)

        Raises:
            ValueError: Wenn Anzahl der Bias-Werte nicht mit Anzahl Klassen
                übereinstimmt.
        """

        def safe_float(value):
            """
            Hier hab ich ggf. mit None-Werten als Float zu tun, das möchte ich abfangen.
            """
            return float(value) if value is not None else None

        labels = self.class_names

        # Faustkeil-Lösung:
        # Prediction auf Leerstring, um den realen Basis-Bias zu erhalten
        baseline_probs_dict = self.predict_proba_all_labels("")
        baseline_probs = [baseline_probs_dict[label] for label in labels]

        # die einzelnen Modelle liefern lineare, logarithmische oder keine Daten (LIME)
        raw_biases = self._inspector.get_raw_biases_per_class()
        prior_probs = self._inspector.get_prior_probs_per_class()

        if raw_biases:
            if len(raw_biases) != len(labels):
                raise ValueError(
                    f"[Er007] Anzahl der Bias-Werte ({len(raw_biases)}) "
                    f"stimmt nicht mit Anzahl der Klassen ({len(labels)}) überein."
                )

            result = {
                label: {
                    "raw_bias": safe_float(raw_biases[label]),
                    "prior_prob": safe_float(prior_probs[label]),
                    "baseline_prob": float(base),
                }
                for label, base in zip(labels, baseline_probs)
            }

        else:
            # Fallback: None als Platzhalter, da keine Bias-Information verfügbar ist
            result = {
                label: {"raw_bias": None, "prior_prob": None, "baseline_prob": None}
                for label in labels
            }

        return result

    def create_explanation(self, input_text: str) -> TextExplanation:
        """
        Erzeugt eine TextExplanation-Instanz für den input_text. Es werden die
        ermittelten Analysedaten gekapselt, des Weiteren wird die Preprocessing-
        Methode übergeben, um die Entscheidung rückverfolgen zu können.

        Raises:
            ValueError: Wenn der Text leer ist oder nur aus Leer-/Steuerzeichen besteht.
        """
        self._check_input_text(input_text)

        return TextExplanation(
            input_text=input_text,
            class_probabilities=self.predict_proba_all_labels(input_text),
            word_contributions=self.compute_word_contributions(input_text),
            class_biases=self.get_biases_all_classes(),
            preprocess_fn=self._preprocess_before_vectorizer,
            token_type=self.get_token_type(),
            display_config=self._config.get(
                "explanation_display", {}
            )
        )

    def _preprocess_before_vectorizer(self, input_text: str) -> str:
        """
        Führt alle Preprocessing-Schritte vor dem Vectorizer auf dem übergebenen Text
        aus. Wichtig für das Mapping des Input_text vor/nach der Verarbeitung

        Args:
            input_text (str): Der Originaltext, der vorverarbeitet werden soll.

        Returns:
            str: Der Text, der durch den Preprocessing-Schritt gelaufen ist.
        """

        # für den Durchlauf durch die Pipeline ist Pandas.Series erforderlich
        text = pd.Series([input_text])

        # relevante Pipelinesteps: von vorne bis vor den Vectorizer
        steps = self._pipeline.steps
        vectorizer_step_name = self.predictor.vectorizer_step_name
        vectorizer_index = [name for name, _ in steps].index(vectorizer_step_name)

        # Text durchläuft die einzelnen Steps
        for name, transformer in steps[:vectorizer_index]:
            text = transformer.transform(text)
        return text[0]

    def _vectorize(self, clean_text: str) -> tuple[np.ndarray, list[str]]:
        """
        Wandelt einen vorbereiteten Text in einen Feature-Vektor um und gibt die
        zugehörigen Merkmalsnamen zurück. Wird z.B. zur Erklärung einzelner Wörter
        benötigt.

        Args:
            clean_text (str): Vorverarbeiteter (bereits gereinigter) Text.

        Returns:
            Tuple[np.ndarray, list[str]]: Vektorisiertes Zahlenarray und zugehörige
                Feature-Namen (Wörter).
        """
        # vectorisiert den eingegebenen Text (der als Liste übergeben werden muss)
        X_vec = self._vectorizer.transform([clean_text])

        # holt die wörter aus dem vectorizer
        feature_names = self._vectorizer.get_feature_names_out()

        # gibt Werte als eindimensionales Array zurück sowie die zugehörigen Wörter
        # Reihenfolge stimmt, weil derselbe Vectorizer für fit() und transform()
        # benutzt wurde
        return X_vec.toarray()[0], feature_names

    def _build_error_message(self, message: str) -> str:
        """Ergänzt eine Fehlermeldung um Klassen- und Pipelineinformationen."""
        return (
            f"Klasse {(self.__class__.__name__)!r} mit Pipeline {self.alias!r}: "
            + message
        )

    def _validate_type(self, candidate: object, allowed_types: Tuple[Type, ...]):
        """
        Prüft, ob das Objekt einem der erlaubten Typen entspricht und wirft
        TypeError bei Fehlschlag.
        """
        if not isinstance(candidate, allowed_types):
            allowed_names = ", ".join([cls.__name__ for cls in allowed_types])
            raise TypeError(
                self._build_error_message(
                    f"[Er003] Pipeline enthält aktuell noch nicht unterstützten Typ:"
                    f" {(type(candidate).__name__)!r}."
                    f" Erlaubt sind bisher: {allowed_names!r}."
                )
            )

    def _check_input_text(self, input_text: str) -> None:
        """
        Interne Validierung für Eingabetexte. Während TextPredictor mit Leerstrings
        arbeiten kann, kommen Lime und Shap damit nicht zurecht. Deshalb muss ich
        Leerstrings für die Erklärung abfangen.

        Raises:
            ValueError: Wenn der Text leer oder nur aus Leerzeichen besteht.
        """
        if not input_text.strip():
            raise ValueError("[Er004] Eingabetext darf nicht leer sein.")


    def _get_preprocessing_from_pipeline(self) -> BaseEstimator:
        """
        Gibt den Preprocessing-Schritt aus der Pipeline zurück oder wirft
        ValueError.
        """
        preprocessing_step_name = self._config["pipeline"]["preprocessing_step_name"]
        preprocessing = find_step_by_name(self._pipeline, preprocessing_step_name)
        if preprocessing is None:
            raise ValueError(
                self._build_error_message(
                    f"[Er004] Pipeline enthält keinen Step {preprocessing_step_name!r}."
                )
            )
        return preprocessing

__init__(predictor, force_agnostic=False)

Initialisiert die Klasse mit dem übergebenen Predictor und prüft, ob der enthaltene Classifier und der Vectorizer zulässig sind.

Die erlaubten Datentypen sind als Klassenvariablen definiert: - _ALLOWED_VECTORIZER_TYPES für den Vectorizer - _ALLOWED_CLASSIFIER_TYPES für den Classifier

Parameters:

Name Type Description Default
predictor TextPredictor

Ein TextPredictor-Objekt mit einer scikit-learn-Pipeline.

required
force_agnostic bool

Optional zum Testen, hier erzwinge ich die Benutzung des LIMETextExplainers oder SHAP (je nach config in xai-config.yaml)

False

Raises:

Type Description
TypeError

Wenn der Classifier oder der Vectorizer nicht zu den unterstützten Typen gehört.

Source code in src\explanation\text_explainer.py
def __init__(self, predictor: TextPredictor, force_agnostic: bool = False):
    """
    Initialisiert die Klasse mit dem übergebenen Predictor und prüft, ob der
    enthaltene Classifier und der Vectorizer zulässig sind.

    Die erlaubten Datentypen sind als Klassenvariablen definiert:
    - `_ALLOWED_VECTORIZER_TYPES` für den Vectorizer
    - `_ALLOWED_CLASSIFIER_TYPES` für den Classifier

    Args:
        predictor (TextPredictor): Ein TextPredictor-Objekt mit einer
            scikit-learn-Pipeline.
        force_agnostic (bool): Optional zum Testen, hier erzwinge ich die Benutzung
            des LIMETextExplainers oder SHAP (je nach config in xai-config.yaml)

    Raises:
        TypeError: Wenn der Classifier oder der Vectorizer nicht zu den
            unterstützten Typen gehört.
    """

    # testparameter, wenn ich dezidiert LIME/SHAP testen möchte, unabhängig von
    # der Erklärbarkeit des in der Pipeline enthaltenen Modells
    self._force_agnostic = force_agnostic

    # weist predictor zu, enthält Pipeline mit trainiertem Modell
    self._predictor = predictor
    self._pipeline: Pipeline = self.predictor.pipeline

    # ggf. für Kontrollausgaben
    self._alias: str = self.predictor.alias

    # lädt die Defaultwerte aus der Konfigurationsdatei
    self._config = self.predictor.config

    # prüft, ob Classifier zugewiesen werden darf
    self._validate_type(self.predictor.classifier, self._ALLOWED_CLASSIFIER_TYPES)

    # Prüfung bestanden: classifier darf zugewiesen werden
    self._classifier = self.predictor.classifier
    self._class_names: List[str] = self._classifier.classes_.tolist()
    self._num_class_names: int = len(self.class_names)
    self._classifier_type_name: str = type(self._classifier).__name__

    # prüft Vectorizer
    self._validate_type(self.predictor.vectorizer, self._ALLOWED_VECTORIZER_TYPES)

    # Prüfung bestanden: Vectorizer darf zugewiesen werden
    self._vectorizer = self.predictor.vectorizer

    # je nach Modell wird der passende Inspector erzeugt
    self._inspector: BaseInspector = self._create_inspector()

    # Baseline-Prognosen auf leerem Text:
    self._baseline_probs = self.predictor.predict_proba_all_labels("")

compute_word_contributions(input_text)

Ermittelt die Beiträge der Wörter des eingegebenen Textes zur Klassifikation sämtlicher Klassen mithilfe der jeweiligen, zum Modell passenden Inspector-Instanzen.

Parameters:

Name Type Description Default
input_text str

Der zu erklärende Text

required

Returns:

Type Description
Dict[str, Dict[str, float]]

Dict[label_name, Dict[word, contribution]]: Ein Dictionary, das pro Label jedes einzelne Wort als Key und sein Beitrag zur Entscheidung als Value zuordnet. Positive Werte sprechen für die Klassifikation, negative dagegen.

Raises:

Type Description
ValueError

Wenn der Text leer oder nur aus Leerzeichen besteht.

Source code in src\explanation\text_explainer.py
def compute_word_contributions(
    self, input_text: str
) -> Dict[str, Dict[str, float]]:
    """
    Ermittelt die Beiträge der Wörter des eingegebenen Textes zur Klassifikation
    sämtlicher Klassen mithilfe der jeweiligen, zum Modell passenden
    Inspector-Instanzen.

    Args:
        input_text (str): Der zu erklärende Text

    Returns:
        Dict[label_name, Dict[word, contribution]]: Ein Dictionary, das pro Label
            jedes einzelne Wort als Key und sein Beitrag zur Entscheidung als Value
            zuordnet. Positive Werte sprechen für die Klassifikation, negative
            dagegen.

    Raises:
        ValueError: Wenn der Text leer oder nur aus Leerzeichen besteht.
    """
    self._check_input_text(input_text)

    # Preprocessing (alle Schritte vor dem Vektorizer)
    clean_text = self._preprocess_before_vectorizer(input_text)

    # Vektorisieren
    feature_values, feature_names = self._vectorize(clean_text)

    # für agnostischen Explainer: alle Wörter rauswerfen, die das "echte Modell"
    # nie gesehen hat. Diese Wörter tauchen als relevant in LIME auf, obwohl das
    # echte Modell ihnen keinen Wert zuweisen kann.
    relevant_text = self._eliminate_stopwords(input_text)


    # wortbeiträge je nach methoden des Modells ermitteln
    # beachte: linear und log-Inspectoren brauchen nur den Vector, die agnostischen
    # den Rohtext (hier bereinigt von Stopwords)
    word_contributions = self._inspector.compute_word_contributions(
        relevant_text, feature_values, feature_names
    )

    return word_contributions

create_explanation(input_text)

Erzeugt eine TextExplanation-Instanz für den input_text. Es werden die ermittelten Analysedaten gekapselt, des Weiteren wird die Preprocessing- Methode übergeben, um die Entscheidung rückverfolgen zu können.

Raises:

Type Description
ValueError

Wenn der Text leer ist oder nur aus Leer-/Steuerzeichen besteht.

Source code in src\explanation\text_explainer.py
def create_explanation(self, input_text: str) -> TextExplanation:
    """
    Erzeugt eine TextExplanation-Instanz für den input_text. Es werden die
    ermittelten Analysedaten gekapselt, des Weiteren wird die Preprocessing-
    Methode übergeben, um die Entscheidung rückverfolgen zu können.

    Raises:
        ValueError: Wenn der Text leer ist oder nur aus Leer-/Steuerzeichen besteht.
    """
    self._check_input_text(input_text)

    return TextExplanation(
        input_text=input_text,
        class_probabilities=self.predict_proba_all_labels(input_text),
        word_contributions=self.compute_word_contributions(input_text),
        class_biases=self.get_biases_all_classes(),
        preprocess_fn=self._preprocess_before_vectorizer,
        token_type=self.get_token_type(),
        display_config=self._config.get(
            "explanation_display", {}
        )
    )

get_biases_all_classes()

Gibt die Grundwahrscheinlichkeiten für jede Klasse zurück, die für die Entscheidung zusätzlich zu den Wortgewichten relevant sind (quasi als eingabeunabhängiges "Vorurteil"). Da die Bias-/Intercept-Werte der logistischen Modelle One-vs.-All- Wahrscheinlichkeiten ausgeben, sind diese eher statistisch relevant. Wenn ein Modell keine Grundwahrscheinlichkeiten berechnet, wird ersatzweise für alle Klassen None zurückgegeben.

Returns:

Type Description
Dict[str, Dict[str, Optional[float]]]

Dict[str, Dict[str, float]]: Klassenname ('raw_bias': Bias,

Dict[str, Dict[str, Optional[float]]]

'prior_prob': Bias zur Wahrscheinlichkeit umgerechnet,

Dict[str, Dict[str, Optional[float]]]

'baseline_prob': Grundwahrscheinlichkeit auf Leerstring)

Raises:

Type Description
ValueError

Wenn Anzahl der Bias-Werte nicht mit Anzahl Klassen übereinstimmt.

Source code in src\explanation\text_explainer.py
def get_biases_all_classes(self) -> Dict[str, Dict[str, Optional[float]]]:
    """
    Gibt die Grundwahrscheinlichkeiten für jede Klasse zurück, die für die
    Entscheidung zusätzlich zu den Wortgewichten relevant sind (quasi als
    eingabeunabhängiges "Vorurteil").
    Da die Bias-/Intercept-Werte der logistischen Modelle One-vs.-All-
    Wahrscheinlichkeiten ausgeben, sind diese eher statistisch relevant.
    Wenn ein Modell keine Grundwahrscheinlichkeiten berechnet, wird ersatzweise für
    alle Klassen None zurückgegeben.

    Returns:
        Dict[str, Dict[str, float]]: Klassenname ('raw_bias': Bias,
        'prior_prob': Bias zur Wahrscheinlichkeit umgerechnet,
        'baseline_prob': Grundwahrscheinlichkeit auf Leerstring)

    Raises:
        ValueError: Wenn Anzahl der Bias-Werte nicht mit Anzahl Klassen
            übereinstimmt.
    """

    def safe_float(value):
        """
        Hier hab ich ggf. mit None-Werten als Float zu tun, das möchte ich abfangen.
        """
        return float(value) if value is not None else None

    labels = self.class_names

    # Faustkeil-Lösung:
    # Prediction auf Leerstring, um den realen Basis-Bias zu erhalten
    baseline_probs_dict = self.predict_proba_all_labels("")
    baseline_probs = [baseline_probs_dict[label] for label in labels]

    # die einzelnen Modelle liefern lineare, logarithmische oder keine Daten (LIME)
    raw_biases = self._inspector.get_raw_biases_per_class()
    prior_probs = self._inspector.get_prior_probs_per_class()

    if raw_biases:
        if len(raw_biases) != len(labels):
            raise ValueError(
                f"[Er007] Anzahl der Bias-Werte ({len(raw_biases)}) "
                f"stimmt nicht mit Anzahl der Klassen ({len(labels)}) überein."
            )

        result = {
            label: {
                "raw_bias": safe_float(raw_biases[label]),
                "prior_prob": safe_float(prior_probs[label]),
                "baseline_prob": float(base),
            }
            for label, base in zip(labels, baseline_probs)
        }

    else:
        # Fallback: None als Platzhalter, da keine Bias-Information verfügbar ist
        result = {
            label: {"raw_bias": None, "prior_prob": None, "baseline_prob": None}
            for label in labels
        }

    return result

get_label_index(label)

Gibt den Index für das Label zurück oder wirft ValueError

Source code in src\explanation\text_explainer.py
def get_label_index(self, label: str) -> int:
    """Gibt den Index für das Label zurück oder wirft ValueError"""
    if label not in self.class_names:
        raise ValueError(
            self._build_error_message(
                f"[Er001] Kategorie {label!r} nicht in Klassen:"
                f" {self.class_names!r}"
            )
        )
    return list(self.class_names).index(label)

get_token_type()

Gibt entweder "raw" oder "processed" aus den Inspectoren aus, je nach dem, ob dort die Wortbeiträge auf dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

Returns:

Name Type Description
str str

"raw" oder "processed"

Source code in src\explanation\text_explainer.py
def get_token_type(self) -> str:
    """
    Gibt entweder "raw" oder "processed" aus den Inspectoren aus, je nach dem, ob
    dort die Wortbeiträge auf dem Rohtext oder auf dem vorverarbeiteten Text
    ermittelt werden.

    Returns:
        str: "raw" oder "processed"
    """
    return self._inspector.get_token_type()

get_vectorizer_ngram_range()

Gibt zurück, aus wievielen Wörtern ein Token (für model_weights und word_contributions) zusammengesetzt sein darf. z.B.: (1, 3) maximal drei Wörter

Returns:

Type Description
tuple[int, int]

tuple[int, int]: Mindestzahl, Höchstzahl

Source code in src\explanation\text_explainer.py
def get_vectorizer_ngram_range(self) -> tuple[int, int]:
    """
    Gibt zurück, aus wievielen Wörtern ein Token (für model_weights
    und word_contributions) zusammengesetzt sein darf.
    z.B.: (1, 3) maximal drei Wörter

    Returns:
        tuple[int, int]: Mindestzahl, Höchstzahl
    """
    return self._vectorizer.ngram_range

inspect_model_weights(label)

Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions. Falls keine Gewichte berechnet werden können, wird None zurückgegeben.

Parameters:

Name Type Description Default
label str

Das Label, für das die gewichteten Wörter ausgegeben werden.

required

Returns:

Type Description
List[Tuple[str, float | None]]

List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel

Raises:

Type Description
ValueError

Wenn das übergebene Label nicht existiert.

Source code in src\explanation\text_explainer.py
def inspect_model_weights(self, label: str) -> List[Tuple[str, float | None]]:
    """
    Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche
    Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen
    sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions.
    Falls keine Gewichte berechnet werden können, wird None zurückgegeben.

    Args:
        label (str): Das Label, für das die gewichteten Wörter ausgegeben werden.

    Returns:
        List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel

    Raises:
        ValueError: Wenn das übergebene Label nicht existiert.

    """
    # prüfe vorab, ob label existiert
    label_index = self.get_label_index(label)
    # die zum Modell passende Inspector-Instanz führt aus
    return self._inspector.inspect_model_weights_by_index(label_index)

inspect_model_weights_top_k(label, top_k=20, sign_filter='all')

Gibt die Top-k Modellgewichte für ein bestimmtes Label zurück. Die Gewichte beziehen sich auf das Modell selbst und sind unabhängig von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine leere Liste zurückgegeben. Für die Sortierung der Gewichte sind die Inspector-Klassen zuständig. Für lineare Modelle gilt: positive = wichtig pro Label, negative = wichtig contra Label, all = nach wort alphabetisch sortiert Für logarithmische Modelle gilt: positive = wichtig pro Label, negative = unwichtig, all = nach wort alphabetisch sortiert

Parameters:

Name Type Description Default
label str

Das Label, für das die Top-Wörter ausgegeben werden.

required
top_k (Optional, int)

Anzahl der Top-Wörter nach Gewichtung.

20
sign_filter (Optional, str)

Filtert nach Vorzeichen der Modellgewichte mit erlaubten Werten "pro", "contra", "all"

'all'

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert nach Betrag. Ersatzweise eine leere Liste.

Raises:

Type Description
ValueError
  • Wenn falscher Wert für 'valid_filters' übergeben wird
  • Wenn das übergebene Label nicht existiert.
Source code in src\explanation\text_explainer.py
def inspect_model_weights_top_k(
    self, label: str, top_k: int = 20, sign_filter: str = "all"
) -> List[Tuple[str, float]]:
    """
    Gibt die Top-k Modellgewichte für ein bestimmtes Label zurück. Die Gewichte
    beziehen sich auf das Modell selbst und sind unabhängig von einzelnen
    Predictions. Falls das Modell keine Gewichte ausgibt, wird eine leere Liste
    zurückgegeben. Für die Sortierung der Gewichte sind die Inspector-Klassen
    zuständig.
    Für lineare Modelle gilt: positive = wichtig pro Label, negative = wichtig
    contra Label, all = nach wort alphabetisch sortiert
    Für logarithmische Modelle gilt: positive = wichtig pro Label, negative =
    unwichtig, all = nach wort alphabetisch sortiert

    Args:
        label (str): Das Label, für das die Top-Wörter ausgegeben werden.
        top_k (Optional, int): Anzahl der Top-Wörter nach Gewichtung.
        sign_filter (Optional, str): Filtert nach Vorzeichen der Modellgewichte
            mit erlaubten Werten "pro", "contra", "all"

    Returns:
        List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert
            nach Betrag. Ersatzweise eine leere Liste.

    Raises:
        ValueError:
            - Wenn falscher Wert für 'valid_filters' übergeben wird
            - Wenn das übergebene Label nicht existiert.
    """
    valid_filters = {"pro", "contra", "all"}
    if sign_filter not in valid_filters:
        raise ValueError(
            f"[Er001] Ungültiger Wert für sign_filter: {sign_filter!r}. "
            f"Erlaubt sind: {', '.join(valid_filters)}."
        )

    # prüft, ob das Label existiert und gibt index zurück
    label_index = self.get_label_index(label)

    # die zum Modell passende Inspector-Instanz führt aus
    return self._inspector.inspect_model_weights_top_k(label_index=label_index,
                                                       top_k=top_k,
                                                       sign_filter=sign_filter)

predict_proba_all_labels(input_text)

Wrapper: Gibt für einen einzelnen Text die Wahrscheinlichkeiten sämtlicher Klassen zurück.

Parameters:

Name Type Description Default
input_text str

Ein Textstring, der klassifiziert werden soll.

required

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Ein Dictionary mit Klassen als Keys und deren

Dict[str, float]

Wahrscheinlichkeiten als Values.

Source code in src\explanation\text_explainer.py
def predict_proba_all_labels(self, input_text: str) -> Dict[str, float]:
    """
    Wrapper: Gibt für einen einzelnen Text die Wahrscheinlichkeiten sämtlicher
    Klassen zurück.

    Args:
        input_text (str): Ein Textstring, der klassifiziert werden soll.

    Returns:
        Dict[str, float]: Ein Dictionary mit Klassen als Keys und deren
        Wahrscheinlichkeiten als Values.
    """
    return self.predictor.predict_proba_all_labels(input_text=input_text)

src.explanation.text_explanation

TextExplanation

TextExplanation wird durch TextExplainer.create_explanation() für einen konkreten Eingabetext erstellt. Es enthält sämtliche ermittelten Analysedaten, die die Prediction des Modells erklären. Desweiteren erhält es die Preprocessing-Methode, die im Verarbeitungsprogess benutzt wurde. Es werden Methoden zur Verfügung gestellt, die diese Daten ausgeben.

Author

irene

Source code in src\explanation\text_explanation.py
  11
  12
  13
  14
  15
  16
  17
  18
  19
  20
  21
  22
  23
  24
  25
  26
  27
  28
  29
  30
  31
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
class TextExplanation:
    """
    TextExplanation wird durch TextExplainer.create_explanation() für einen konkreten
    Eingabetext erstellt. Es enthält sämtliche ermittelten Analysedaten, die die
    Prediction des Modells erklären. Desweiteren erhält es die Preprocessing-Methode,
    die im Verarbeitungsprogess benutzt wurde. Es werden Methoden zur Verfügung
    gestellt, die diese Daten ausgeben.

    Author:
        irene
    """

    def __init__(
        self,
        input_text: str,
        class_probabilities: Dict[str, float],
        word_contributions: Dict[str, Dict[str, float]],
        class_biases: Dict[str, Dict[str, float]],
        token_type: str,
        preprocess_fn: Callable[[str], str],
        display_config=None,
    ):
        self._input_text = input_text
        self._class_probabilities = class_probabilities
        self._word_contributions = word_contributions
        self._class_biases = class_biases
        self._token_type = token_type
        self._preprocess_fn = preprocess_fn

        # Übergabe der Konfigurationsdatei ist optional, damit Klasse möglichst
        # unabhängig bleibt. Deshalb setze ich hier Fallback-Werte.
        # Contra-Highlight ist lila, damit ich sofort sehe, ob externe yaml geladen
        # wurde.
        config = display_config or {}
        self._text_color_pro = config.get("text_color_pro", "0,128,0")
        self._text_color_contra = config.get("text_color_contra", "128,0,128")
        self._min_alpha = config.get("min_alpha", 0.05)
        self._max_alpha = config.get("max_alpha", 0.85)
        self._float_precision = config.get("float_precision", 5)
        self._alpha_smoothing_factor = config.get("alpha_float_precision", 0.6)
        self._alpha_curve_exponent = config.get("alpha_curve_exponent", 0.5)
        self._bar_color_pro = config.get("bar_color_pro", "skyblue")
        self._bar_color_contra = config.get("bar_color_contra", "grey")
        self._diagram_path = config.get("diagram_path", "")

    # Ich möchte nicht, dass die Instanzvariablen neu gesetzt werden, deshalb hier die
    # Zugriffsmethoden:
    @property
    def input_text(self) -> str:
        return self._input_text

    @property
    def class_probabilities(self) -> Dict[str, float]:
        return self._class_probabilities

    @property
    def word_contributions(self) -> Dict[str, Dict[str, float]]:
        return self._word_contributions

    @property
    def class_biases(self) -> Dict[str, Dict[str, float]]:
        return self._class_biases

    @property
    def preprocess_fn(self) -> Callable[[str], str]:
        return self._preprocess_fn

    @property
    def token_type(self) -> str:
        return self._token_type

    def get_biases_raw_all_classes(self) -> Dict[str, float]:
        """
        Gibt die Roh-Bias-/Log-Prior-Werte pro Klasse zurück.

        Returns:
            Dict[str, float]:{'klassenname': Bias-Wert}
        """
        return {
            label: values["raw_bias"] for label, values in self.class_biases.items()
        }

    def get_prior_prob_all_classes(self) -> Dict[str, float]:
        """
        Gibt die a-priori-Wahrscheinlichkeit pro Klasse in Range [0, 1] zurück.

        Returns:
            Dict[str, float]:{'klassenname': a-priori-Wahrscheinlichkeit}
        """
        return {
            label: values["prior_prob"] for label, values in self.class_biases.items()
        }

    def get_baseline_prob_all_classes(self) -> Dict[str, float]:
        """
        Gibt die Basiswahrscheinlichkeiten in Range [0, 1] pro Klasse
        zurück. Die Basiswahrscheinlichkeit basiert auf einer Prediction auf dem
        Leerstring.

        Returns:
            Dict[str, float]: {'klassenname': Basiswahrscheinlichkeit}
        """
        return {
            label: values["baseline_prob"]
            for label, values in self.class_biases.items()
        }

    def get_baseline_prob_for_class(self, class_name: str) -> float:
        """
        Gibt die Basiswahrscheinlichkeit in Range [0, 1] für die übergebene
        Klasse aus.

        Args:
            class_name (str): Der Klassenname.

        Raises:
            ValueError: Wenn es für den Klassennamen keine Basiswahrscheinlichkeit gibt.

        Returns:
            float: Die Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig
                vom zu klassifizierenden Text. Range [0, 1]
        """
        baseline_probs = self.get_baseline_prob_all_classes()
        try:
            return baseline_probs[class_name]
        except KeyError:
            raise ValueError(
                f"[En001] Klasse '{class_name}' ist nicht vorhanden."
                f"Bekannt: {list(baseline_probs.keys())}"
            )

    def get_prior_prob_for_class(self, class_name: str) -> float:
        """
        Gibt die umgerechnete Basiswahrscheinlichkeit für die Klassenentscheidung
        unabhängig vom zu klassifizierenden Text in Range [0, 1] für die übergebene
        Klasse.

        Args:
            class_name (str): Der Klassenname.

        Raises:
            ValueError: Wenn es für den Klassennamen keine Basiswahrscheinlichkeit gibt.

        Returns:
            float: Die Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig
                vom zu klassifizierenden Text.
        """
        prior_probs = self.get_prior_prob_all_classes()
        try:
            return prior_probs[class_name]
        except KeyError:
            raise ValueError(
                f"[En002] Klasse '{class_name}' ist nicht vorhanden."
                f"Bekannt: {list(prior_probs.keys())}"
            )

    def get_bias_raw_for_class(self, class_name: str) -> float:
        """
        Gibt den Bias als Rohdatum für die Klassenentscheidung für die übergebene
        Klasse.

        Args:
            class_name (str): Der Klassenname.

        Raises:
            ValueError: Wenn es für den Klassennamen keinen Bias gibt.

        Returns:
            float: Den Bias für die Klassenentscheidung unabhängig vom zu
                klassifizierenden Text.
        """
        raw_bias = self.get_biases_raw_all_classes()
        try:
            return raw_bias[class_name]
        except KeyError:
            raise ValueError(
                f"[En003] Klasse '{class_name}' ist nicht vorhanden."
                f"Bekannt: {list(raw_bias.keys())}"
            )

    def has_baseline_variation(self) -> bool:
        """
        Prüft, ob die Baseline-Werte sich zwischen den Klassen unterscheiden.

        Returns:
            bool: True, wenn mindestens zwei Klassen sich mehr als der Schwellwert
                unterscheiden.
        """
        baseline_probs = list(self.get_baseline_prob_all_classes().values())
        return self._has_variation(baseline_probs)

    def has_bias_variation(self) -> bool:
        """
        Prüft, ob die Bias-Werte sich zwischen den Klassen unterscheiden.

        Returns:
            bool: True, wenn mindestens zwei Klassen sich mehr als der Schwellwert
                unterscheiden.
        """
        biases = list(self.get_biases_raw_all_classes().values())
        return self._has_variation(biases)

    def _has_variation(self, list_of_values: list, min_variation: float = 1e-6) -> bool:
        """
        Gibt True zurück, wenn die Varianz der übergebenen Werte den übergebenen
        Schwellwert übersteigt, sonst False. False wird auch zurückgegeben, wenn die
        Liste der Werte leer ist

        Args:
            list_of_values (list): die zu prüfende Liste
            threshold (float, optional): der Schwellwert. Defaults to 1e-6.

        Returns:
            bool: True, wenn die übergebenen Werte eine Varianz oberhalb des
                Schwellwerts aufweisen, sonst False.
        """
        numeric_values = [value for value in list_of_values if value is not None]
        if not numeric_values:
            return False
        return (max(numeric_values) - min(numeric_values)) > min_variation

    def get_top_label(self) -> str:
        """Gibt das Label mit dem höchsten Score zurück"""
        return self.get_sorted_probabilities()[0][0]

    def get_probability(self, label: str) -> float:
        """
        Gibt den Score für das übergebene Label aus oder ValueError

        Args:
            label (str): Das Label, für das ein Score gesucht wird

        Raises:
            ValueError: wenn das Label unbekannt ist

        Returns:
            float: den Score des übergebenen Labels
        """
        self._check_label(label)
        return self.class_probabilities[label]

    def get_top_label_and_probability(self) -> Tuple[str, float]:
        """Gibt Label und mit höchster Wahrscheinlichkeit zurück"""
        return self.get_sorted_probabilities()[0]

    def get_top_advantage(self) -> float:
        # holt die absteigend sortierten Wahrscheinlichkeiten
        sorted_probas = self.get_sorted_probabilities()
        # Vorsprung des Top-Labels
        advantage = sorted_probas[0][1] - sorted_probas[1][1]
        return advantage

    def get_probabilities(self) -> Dict[str, float]:
        """
        Gibt sämtliche Labels und Wahrscheinlichkeiten als Dict aus

        Returns:
            Dict[str, float]: Label, Score
        """
        return self.class_probabilities

    def get_sorted_probabilities(self) -> List[Tuple[str, float]]:
        """
        Gibt sämtliche Labels und Wahrscheinlichkeiten als absteigend sortierte Liste
        zurück

        Returns:
            List[Tuple[str, float]]: Label, Score
        """
        return sorted(
            self.class_probabilities.items(), key=lambda item: item[1], reverse=True
        )

    def get_top_k_probabilities(self, k: int = 5) -> List[Tuple[str, float]]:
        """
        Gibt eine Anzahl der Labels und Wahrscheinlichkeiten mit den höchsten Scores
        zurück.

        Args:
            k (int, optional): Anzahl der höchstplatzierten Scores. Defaults to 5.

        Returns:
            List[Tuple[str, float]]: Liste der höchstplatzierten Label, Score
        """
        return self.get_sorted_probabilities()[:k]

    def get_top_k_word_contributions_for_label(
        self,
        label: str,
        top_k: int = 10,
        sign_filter: str = "all",
        normalize: bool = False,
    ) -> List[Tuple[str, float]]:
        """
        Gibt die wichtigsten k Wörter zurück, die für die Klassifizierung des
        Labels entscheidend sind. Als Filterkriterien dürfen übergeben werden:
        "positive", "negative", (default) "all"

        Args:
            label (str): Das Label, für das die Wortbeiträge ermittelt werden.
            top_k (Optional, int): Anzahl der wichtigsten Beiträge. Defaults to 10.
            sign_filter: (Optional, str): Filtert nach Vorzeichen der Wortbeiträge,
                zulässige Kriterien sind: "positive", "negative", (default) "all"
            normalize (Optional, bool): Ermöglicht zusätzliche Normalisierung der
                Wortbeiträge innerhalb einer Range von [-1, 1]. Defaults to False.

        Raises:
            ValueError:
                - Wenn falscher Wert für 'sign_filter' übergeben wird
                - Wenn das übergebene Label nicht existiert.

        Returns:
            List[Tuple[str, float]]: _description_
        """

        # prüft Zulässigkeit des Filterkriteriums
        valid_filters = {"positive", "negative", "all"}
        if sign_filter not in valid_filters:
            raise ValueError(
                f"[En004] Ungültiger Wert für sign_filter: {sign_filter!r}. "
                f"Erlaubt sind: {', '.join(valid_filters)}."
            )

        # prüft Zulässigkeit des übergebenen Labels
        self._check_label(label)

        # holt Contributions für das gegebene Label
        contribs = self.get_word_contributions_for_label(label, normalize=normalize)

        # filtert Contributions
        if sign_filter == "positive":
            filtered = [(word, score) for word, score in contribs.items() if score > 0]
            sorted_contribs = sorted(filtered, key=lambda x: x[1], reverse=True)
        elif sign_filter == "negative":
            filtered = [(word, score) for word, score in contribs.items() if score < 0]
            sorted_contribs = sorted(filtered, key=lambda x: x[1])
        else:  # "all": die größten absoluten Werte
            sorted_contribs = sorted(
                contribs.items(), key=lambda x: abs(x[1]), reverse=True
            )

        # Top-k zurückgeben
        return sorted_contribs[:top_k]

    def get_word_contributions_for_label(
        self, label: str, normalize: bool = False
    ) -> Dict[str, float]:
        """
        Gibt alle Wortbeiträge zurück, die pro/contra das übergebene Label sprechen

        Args:
            label (str): Das Label, für/gegen das die Wortbeiträge sprechen
            normalize (Optional, bool): Ermöglicht zusätzliche Normalisierung der
                Wortbeiträge innerhalb einer Range von [-1, 1]. Defaults to False.

        Raises:
            ValueError: Falls das Label nicht in word_contributions gefunden wird
        """

        # prüft, ob Label existiert
        self._check_label(label)

        # holt die Wortbeiträge für das Label
        raw_contribs = self.word_contributions[label]

        # stellt sicher, dass die Beiträge float sind
        typed_contribs = {
            word: float(contribs) for word, contribs in raw_contribs.items()
        }

        # normalisiert optional oder gibt Daten aus
        if normalize:
            return self._normalize_contributions(typed_contribs)
        else:
            return typed_contribs

    def get_colorized_text_as_html(self, label: str) -> str:
        """
        Wandelt einen Text in HTML um, wobei Wörter farblich nach ihren Beiträgen
        (einzeln und als Teil von ngrammen summiert) hervorgehoben werden. Die
        einzelnen Beiträge werden in den Tooltipps genannt.

        Args:
            label (str): das Label, für dessen Prädiktion die wichtigen Worte gezeigt
                werden sollen

        Raises:
            ValueError: falls das Label nicht gefunden wird

        Returns:
            str: ein html-<p>-Tag mit farblich hervorgehobenen Wörtern und Tooltips mit
                den Wortgewichten
        """

        # gibt es das Label?
        self._check_label(label)

        # holt die Wortgewichte für die Entscheidung
        contributions = self.get_word_contributions_for_label(
            label=label, normalize=True
        )

        # falls es keine Gewichte gibt, gib zumindest ordentlichen html-Tag aus
        if not contributions:
            return f"<p>{escape(self.input_text)}</p>"

        separated_words = self._split_text_to_list(self.input_text)

        token_matches = self._collect_token_matches(
            separated_words, contributions, self._preprocess_fn, self.token_type
        )

        # print("erhaltene token_matches aus _collect_token_matches")
        # pprint(token_matches)

        # maximum für farbskalierung holen
        max_abs_weight = self._calculate_max_abs_weight(token_matches)

        html_words = []

        for i, word in enumerate(separated_words):
            _, _, _, match_dict = token_matches[i]

            if match_dict:
                # summiere alle gewichte für dieses wort
                total_weight = sum(match_dict.values())

                # baue den tooltip mit einzelgewichten und summe
                matches = list(match_dict.items())
                tooltip = self._build_tooltip(matches, total_weight)

                # Skaliere Alpha-Wert, um besser sichtbare farbliche Kontraste zu
                # erstellen
                scaled_alpha = self._calculate_scaled_alpha(
                    abs(total_weight), max_abs_weight
                )

                # Farbe je nach Vorzeichen
                color = (
                    f"rgba({self._text_color_pro}, {scaled_alpha:.2f})"
                    if total_weight > 0
                    else f"rgba({self._text_color_contra}, {scaled_alpha:.2f})"
                )

                # HTML mit Tooltip und Farbe, Padding + border-radius für bessere Optik
                html = (
                    f"<span title='{tooltip}' "
                    f"style='background-color:{color};"
                    f" padding:2px; border-radius:3px;'>"
                    f"{escape(word)}</span>"
                )
            else:
                # Kein Wortgewicht für das Wort gespeichert: Wort ohne Tooltip ausgeben
                html = escape(word)

            html_words.append(html)

        return (
            '<p style="background-color: #fff; color:#000; font-family:sans-serif; '
            'line-height:1.5;">' + " ".join(html_words) + "</p>"
        )

    def _calculate_max_abs_weight(
            self,
            token_matches: List[Tuple[int, str, str, Dict[str, float]]]
            ) -> float:
        """
        Gibt das größte absolute Wortgewicht aus den gesammelten Wortgewichten des
        Textes aus, ersatzweise 1.0. Wird benötigt zur Skalierung der Farbwerte.

        Args:
            token_matches (List[Tuple[int, str, str, Dict[str, float]]]): Die Liste
                mit den gesammelten Wortgewichten des Eingabetextes.

        Returns:
            float: Das größte absolute Wortgewicht aus token_matches
        """
        max_abs_weight = 0.0

        for _, _, _, match_dict in token_matches:
            weight_sum = sum(match_dict.values())
            abs_sum = abs(weight_sum)
            if abs_sum > max_abs_weight:
                max_abs_weight = abs_sum

        # Fallback, falls keine Gewichte vorhanden sind
        if max_abs_weight == 0.0:
            max_abs_weight = 1.0

        return max_abs_weight


    def _split_text_to_list(self, input_text: str) -> List[str]:
        """
        Diese Funktion separiert die Wörter eines einen eingegebenen Textstrings nach
        Leerzeichen. Zusätzlich werden Wörter mit Bindestrichen vor und nach dem
        Bindestrich separiert. Dies dient der leichteren Erkennung von Tokengewichten in
        zusammengesetzten Wörtern.

        Args:
            input_text (str): Der Text, der gesplittet werden soll, üblicherweise der
                originale input_text des Tickets

        Returns:
            List[str]: Liste der einzelnen Wörter, separiert nach den Leerzeichen im
                Text, mit ersetzten Bindestrichen für zusammengesetzte Wörter.
        """
        # frische zu füllende Liste
        token_list = []

        # übergebener Text wird wortweise durchlaufen
        for word in input_text.split():
            # Bindestrichwörter werden zusätzlich unterteilt, Bindestrich wird als
            # eigenes Wort übergeben
            if '-' in word:
                parts = word.split('-')
                for i, part in enumerate(parts):
                    if part:
                        token_list.append(part)
                    if i < len(parts) - 1:
                        token_list.append('-')
            else:
                # hängt das einzelne Wort an die Liste
                token_list.append(word)
        return token_list

    def _calculate_scaled_alpha(
        self, abs_weight: float, max_abs_weight: float
    ) -> float:
        """
        Berechnet einen skalierten Transparenzwert (Alpha) für ein Token basierend
        auf dessen absolutem Beitragsgewicht.

        Der normalisierte Gewichtswert wird mit einer S-Kurve transformiert, um
        kleinere Werte nicht zu blass aussehen zu lassen. Sättigung und Steilheit der
        Kurve können in xai_config.yaml konfiguriert werden.

        Args:
            abs_weight (float): Absoluter Beitragswert des Tokens.
            max_abs_weight (float): Maximaler absoluter Beitragswert aller Tokens.

        Returns:
            float: Skalierter Alpha-Wert im Bereich zwischen min_alpha
                und max_alpha, geeignet für die Farbgebung des Tokens im html-span.
        """

        # Glättungsfaktor und Kurvenexponent aus der Konfiguration laden
        smoothing_factor: float = self._alpha_smoothing_factor
        curve_exponent: float = self._alpha_curve_exponent

        # Gewicht auf den Bereich [0, 1] normieren
        normalized_contribution = abs_weight / max_abs_weight

        # S-Kurve anwenden:
        # - Hebt kleine Werte stärker an
        # - curve_exponent bestimmt die Steilheit der Übergänge
        curve_scale_term = (
            normalized_contribution + smoothing_factor
        ) ** curve_exponent
        s_curve = normalized_contribution / curve_scale_term

        # Bereich der zulässigen Alpha-Werte berechnen
        alpha_range = self._max_alpha - self._min_alpha

        # skalierten Wert ermitteln
        scaled_alpha = self._min_alpha + (alpha_range * s_curve)

        return scaled_alpha

    def _format_highlighted_phrase(
        self,
        phrase_tokens: list[str],
        phrase: str,
        weight: float,
        max_abs_weight: float,
    ) -> str:
        """
        Gibt eine HTML-formatierte Phrase mit farblicher Hervorhebung basierend auf dem
        Beitrag zurück.

        Args:
            phrase_tokens (list[str]): Tokenisierte Originalphrase aus dem Text.
            phrase (str): Normalisierte Phrase als Schlüssel in den Beiträgen.
            weight (float): Gewicht der Phrase.
            max_abs_weight (float): Maximaler absoluter Beitrag aller Phrasen zur
                Skalierung der Farbintensität.

        Returns:
            str: HTML-Span gehighlighted und mit Gewicht als Mouseover-Tooltip.
        """
        scaled_alpha = self._calculate_scaled_alpha(abs(weight), max_abs_weight)
        color = (
            f"rgba({self._text_color_pro}, {scaled_alpha:.2f})"
            if weight > 0
            else f"rgba({self._text_color_contra}, {scaled_alpha:.2f})"
        )
        phrase_text = " ".join(phrase_tokens)
        return (
            f'<span title="{weight:+.{self._float_precision}f}" '
            f'style="background-color:{color}; padding:2px; border-radius:3px;">'
            f"{escape(phrase_text)}</span>"
        )

    def _build_tooltip(
        self, matches: list[tuple[str, float]], total_weight: float
    ) -> str:
        """
        Baut den Tooltip-Text mit allen N-Grammen und Gewichten,
        gefolgt von der Gesamtsumme am Ende.

        Args:
            matches (list of (phrase, weight)): Liste der Phrase-Gewicht-Paare
            total_weight (float): Summe der Gewichte aller Treffer

        Returns:
            str: Tooltip-Text mit Zeilenumbrüchen
        """
        lines = [f"{phrase}: {weight:.{self._float_precision}f}"
                 for phrase, weight in matches]
        if len(matches) > 1:
            lines.append(f"gesamt: {total_weight:.{self._float_precision}f}")
        return "\n".join(lines)

    def _check_label(self, label: str):
        if label not in self.word_contributions:
            raise ValueError(
                f"[En005] Keine Wortbeiträge für Label '{label}' vorhanden."
            )

    def _normalize_contributions(
        self, contributions: Dict[str, float]
    ) -> Dict[str, float]:
        """
        Normalisiert Wortbeiträge relativ zur Gesamtsumme aller Beträge. Gibt Beiträge
        im Bereich [-1, 1] zurück.

        Args:
            contributions (Dict[str, float]): Dictionary mit Wörtern und ihren (ggf.
                gewichteten) Beiträgen.

        Returns:
            Dict[str, float]: Normalisiertes Dictionary mit proportionalen Beiträgen
                im Bereich [-1, 1].
        """
        total = sum(abs(value) for value in contributions.values())
        if total == 0:
            return contributions
        return {word: (value / total) for word, value in contributions.items()}

    def _minimal_text_cleaning(self, input_text: str) -> str:
        """
        Da Lime Satzzeichen ignoriert, muss man auch den Rohtext minimal vorverarbeiten.
        Hier füge ich nur die absoluten Standards ein, insbesondere die Satzzeichen
        nehme ich raus.

        Args:
            input_text (str): der Text, der minimal gereinigt werden soll

        Returns:
            str: der gereinigte Text
        """
        clean_text = input_text.translate(str.maketrans('', '', string.punctuation))

        return clean_text

    def _collect_token_matches(
            self, input_text_tokens, contributions, preprocess_fn, token_type
    ) -> List[Tuple[int, str, str, Dict[str, float]]]:
        """
        Findet für jedes Wort des Eingabetextes die Liste von (N-Gramm, Gewicht)-Paaren,
        die sich auf dieses Token auswirken.

        Stop-Wörter im Text werden beim Matching übersprungen und erhalten ein Gewicht
        von 0.

        Args:
            input_text_tokens (List[str]): Liste der Tokens (Wörter) des Eingabetexts.
            contributions (Dict[str, float]): Wörter oder Phrasen (N-Gramme) mit
                zugehörigem Gewicht.
            preprocess_fn (Callable[[str], str]): Funktion zur Vorverarbeitung eines
                Tokens.
            token_type (str): entweder "raw" oder "processed". Falls "raw", stammen
                die Gewichte aus einem agnostischen Explainer. Die Gewichte wurden auf
                dem Rohtext ermittelt, also sind die Token in den contributions Rohtext.
                Bei "processed" muss der Originaltext erst mit der preprocess_fn auf
                die Token in den contributions gemappt werden.

        Returns:
            List[Tuple[int, str, str, Dict[str, float]]]: Eine Liste, die für jedes
                Wort des Input-Textes folgende Werte enthält:
                (int): die Position des Wortes im Text (Hausnummer quasi)
                (str): das Original-Wort aus dem Text
                (str): das preprocessierte Wort
                Dict[str, float]: ein Dictionary, das die zugeordneten Phrasen (str) aus
                    dem Classifier mit ihren Wortgewichten (float) enthält
        """

        # bereite wortbeiträge vor, ich möchte nur nonzero
        filtered_contributions = {
            phrase: weight
            for phrase, weight in contributions.items()
            if weight != 0
        }

        # Mapping der Eingabewörter auf ihre Grundversionen
        # wenn "processed", dann müssen erst die bereinigten Token erstellt werden
        # wenn token_type == "raw", bleiben diese weitgehend unverändert
        if self.token_type == "processed":
            token_mapping = [
                (i, token, self._preprocess_fn(token))
                for i, token in enumerate(input_text_tokens)
            ]
        else:
            token_mapping = [
                (i, token, self._minimal_text_cleaning(token))  # Token unverändert
                for i, token in enumerate(input_text_tokens)
            ]


        # hole die stopwords
        removed_tokens = set(self.get_removed_tokens())

        # entferne alle (preprocessten) Stoppwörter aus dem token mapping
        filtered_token_mapping = [
            (i, token, pre_token)
            for i, token, pre_token in token_mapping
            if pre_token not in removed_tokens
        ]

        # initialisiere Ergebnisliste
        enriched_token_mapping = []

        # baue struktur: (i, token, pre_token, {learned_phrase: weight, ...})
        for i, token, pre_token in filtered_token_mapping:
            match_dict = {}

            # prüfe: gibt es einen Beitrag, der dem pre_token entspricht?
            if pre_token in filtered_contributions:
                match_dict[pre_token] = filtered_contributions[pre_token]

            # schreibe Eintrag in Zielstruktur
            enriched_token_mapping.append((i, token, pre_token, match_dict))

        # initialisiere match_dicts als defaultdict, damit ich es erweitern kann:
        for idx, (i, token, pre_token, match_dict) in enumerate(enriched_token_mapping):
            enriched_token_mapping[idx] = (
                i, token, pre_token, defaultdict(float, match_dict))

        # Länge der Tokenliste
        n = len(enriched_token_mapping)

        # Hilfsfunktion: baue Phrase aus pre_tokens zusammen
        def build_phrase(start, end):
            return " ".join(enriched_token_mapping[j][2] for j in range(start, end))

        # Prüfe Bi- und Trigramme
        for ngram_size in [2, 3]:
            for start_idx in range(n - ngram_size + 1):
                phrase = build_phrase(start_idx, start_idx + ngram_size)
                if phrase in filtered_contributions:
                    weight = filtered_contributions[phrase]
                    # Ergänze match_dict in allen beteiligten Token
                    for j in range(start_idx, start_idx + ngram_size):
                        enriched_token_mapping[j][3][phrase] = weight

        # hier match_dicts zurück in normale dicts
        enriched_token_mapping = [
            (i, token, pre_token, dict(match_dict))
            for (i, token, pre_token, match_dict) in enriched_token_mapping
        ]

        # beide listen wieder zusammenführen
        # Erzeuge Lookup-Table aus enriched_token_mapping
        match_lookup = {
            i: match_dict
            for i, token, pre_token, match_dict in enriched_token_mapping
        }

        # alle Tokens behalten, Treffer ergänzen
        final_mapping = [
            (i, token, pre_token, match_lookup.get(i, {}))
            for i, token, pre_token in token_mapping
        ]

        return final_mapping

    def get_removed_tokens(self) -> set:
        """
        Gibt alle Token des Eingabestrings zurück, die vom Preprocessing entfernt
        wurden.

        Returns:
            set: Set sämtlicher Token, die vom Preprocessing entfernt wurden.
        """
        # zerlegt Eingabe in Token
        original_tokens = self.input_text.split()

        # führt die Vorverarbeitung für jedes Token entsprechend der Pipelinefunktion
        # durch
        preprocessed_tokens = [self._preprocess_fn(t) for t in original_tokens]

        # finde sämtliche Token, die nach dem Preprocessing leer sind
        removed_tokens = {
            orig for orig, prep in zip(original_tokens, preprocessed_tokens) if not prep
        }

        return removed_tokens

    def plot_word_contributions(
        self, label: str, top_k: int = 10, filename="diagramm_wortgewichte.png"
    ) -> str:
        """
        Zeichnet ein horizontales Balkendiagramm mit den wichtigsten Wortbeiträgen zu
        einer Klassenvorhersage. Positive Beiträge werden farblich von negativen
        unterschieden, Farben können in der xai-config.yaml eingestellt werden.

        Args:
            label (str): Klassenlabel, für das die Wortbeiträge dargestellt werden.
            top_k (int, optional): Anzahl der Wörter mit den größten (absoluten)
                Gewichtungen, die angezeigt werden sollen. Default ist 10.

        Returns:
            str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
        """
        title = f"Top {top_k} Wortgewichte für die Entscheidung {label!r}"
        x_label = "Wortgewicht"
        y_label = "Wort"

        sorted_items = self.get_top_k_word_contributions_for_label(label, top_k)

        tokens = [item[0] for item in sorted_items]
        weights = np.array([item[1] for item in sorted_items])
        y_pos = np.arange(len(tokens))
        plt.figure(figsize=(8, 6))

        colors = [
            self._bar_color_pro if w > 0 else self._bar_color_contra for w in weights
        ]

        plt.barh(y_pos, weights, color=colors)
        plt.title(title)
        plt.xlabel(x_label)
        plt.ylabel(y_label)
        plt.yticks(y_pos, tokens)

        plt.axvline(x=0, color="black", linewidth=0.8)
        # Höchstes Gewicht oben
        plt.gca().invert_yaxis()

        plt.tight_layout()

        full_save_path = self._build_diagram_path(filename)
        plt.savefig(full_save_path)
        plt.close()

        return full_save_path

    def plot_top_k_class_probabilities(
        self, top_k: int = 5, filename="diagramm_wahrscheinlichkeiten.png"
    ) -> str:
        """
        Zeichnet ein Balkendiagramm mit Klassifizierungswahrscheinlichkeiten. Die
        wahrscheinlichste Klasse wird farblich hervorgehoben, Farben können in der
        xai-config.yaml eingestellt werden.

        Args:
            top_k (int, optional): Anzahl der Klassen mit den höchsten
                Wahrscheinlichkeiten, die angezeigt werden sollen. Default ist 5.

        Returns:
            str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
        """
        title = "Klassifikations-Wahrscheinlichkeiten"
        x_label = "Wahrscheinlichkeit"
        y_label = "Klassifikation"

        sorted_items = self.get_sorted_probabilities()

        top_items = sorted_items[:top_k]
        labels = [item[0] for item in top_items]
        probs = [item[1] for item in top_items]

        y_pos = np.arange(len(labels))

        # Farbliste definieren: erste Bar = pro, Rest = contra
        colors = [self._bar_color_pro] + [self._bar_color_contra] * (len(probs) - 1)

        plt.figure(figsize=(8, 6))
        bars = plt.barh(y_pos, probs, color=colors)
        plt.yticks(y_pos, labels)
        plt.title(title)
        plt.xlabel(x_label)
        plt.ylabel(y_label)
        plt.xlim(0, 1.0)

        # Höchste Wahrscheinlichkeit oben
        plt.gca().invert_yaxis()

        # Werte werden mit angezeigt
        for bar in bars:
            width = bar.get_width()
            plt.text(
                width + 0.01,
                bar.get_y() + bar.get_height() / 2,
                f"{width:.{self._float_precision}f}",
                va="center",
            )

        plt.tight_layout()

        full_save_path = self._build_diagram_path(filename)
        plt.savefig(full_save_path)
        plt.close()

        return full_save_path

    def plot_class_biases(
            self,
            filename: str = "diagramm_class_biases.png"
            ) -> str:
        """
        Zeichnet ein vertikales Balkendiagramm zur Visualisierung der jeweiligen
        Klassen-Biase. Da die zu erwartenden Werte sehr verschieden sein können,
        wird hier keine Skala festgelegt.

        Args:
            filename (Optional, str): Dateiname, unter dem das Diagramm gespeichert
                wird. Defaultwert gesetzt. (Der Pfad kann in der xai_config.yaml
                geändert werden.)

        Returns:
            str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
        """
        title = "Class Biases"
        x_label = "Klasse"
        y_label = "Bias-Wert"

        # Anzeigewerte holen
        biases_raw = self.get_biases_raw_all_classes()  # Methode aufrufen!
        labels = list(biases_raw.keys())
        biases_orig = list(biases_raw.values())

        # Maske: welche Werte waren None?
        bias_none_mask = [b is None for b in biases_orig]

        # None-Werte für den Plot durch 0 ersetzen
        biases = [0 if b is None else b for b in biases_orig]

        x_pos = np.arange(len(labels))

        plt.figure(figsize=(8, 6))

        bars = plt.bar(x_pos, biases, color=self._bar_color_contra)

        plt.xticks(x_pos, labels, rotation=45, ha="right")
        plt.title(title)
        plt.xlabel(x_label)
        plt.ylabel(y_label)

        # Werte auf den Balken anzeigen
        for idx, bar in enumerate(bars):
            height = bar.get_height()

            # Beschriftung für None-Werte setzen
            if bias_none_mask[idx]:
                label = "None"
            else:
                label = f"{height:.{self._float_precision}f}"

            plt.text(
                bar.get_x() + bar.get_width() / 2,
                height + 0.01,
                label,
                ha="center",
                va="bottom",
            )

        plt.tight_layout()

        full_save_path = self._build_diagram_path(filename)
        plt.savefig(full_save_path)

        plt.close('all')

        # hier wird zusätzlich der Pfad zur png-Datei zurückgegeben
        return full_save_path

    def plot_prior_probs(self, filename: str = "diagramm_prior_prob.png") -> str:
        """
        Zeichnet ein vertikales Balkendiagramm zur Visualisierung der unsortierten
        A-Priori-Wahrscheinlichkeiten, die aus dem Bias errechnet werden.

        Returns:
            str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
        """
        title = "A-priori-Wahrscheinlichkeiten"
        x_label = "Klasse"
        y_label = "A-priori-Wahrscheinlichkeit"

        # Anzeigewerte holen
        probs_all = self.get_prior_prob_all_classes()
        labels = list(probs_all.keys())
        probs_orig = list(probs_all.values())

        # Maske: welche Werte waren None?
        probs_none_mask = [b is None for b in probs_orig]

        # None-Werte für den Plot durch 0 ersetzen
        probs = [0 if prob is None else prob for prob in probs_orig]

        x_pos = np.arange(len(labels))

        plt.figure(figsize=(8, 6))

        bars = plt.bar(x_pos, probs, color=self._bar_color_contra)

        plt.xticks(x_pos, labels, rotation=45, ha="right")
        plt.ylim(0, 1.0)
        plt.title(title)
        plt.xlabel(x_label)
        plt.ylabel(y_label)

        for idx, bar in enumerate(bars):
            height = bar.get_height()

            # Beschriftung für None-Werte setzen
            if probs_none_mask[idx]:
                label = "None"
            else:
                label = f"{height:.{self._float_precision}f}"
            plt.text(
                bar.get_x() + bar.get_width() / 2,
                height + 0.01,
                label,
                ha="center",
                va="bottom",
            )

        plt.tight_layout()

        full_save_path = self._build_diagram_path(filename)
        plt.savefig(full_save_path)
        plt.close('all')

        return full_save_path

    def plot_baseline(self, filename: str = "diagramm_baseline.png") -> str:
        """
        Zeichnet ein vertikales Balkendiagramm zur Visualisierung der
        Baseline-Wahrscheinlichkeiten.

        Returns:
            str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
        """
        title = "Baseline-Wahrscheinlichkeiten"
        x_label = "Klasse"
        y_label = "Baseline-Wahrscheinlichkeit"

        # class_biases unsortiert extrahieren
        probs_all = self.get_baseline_prob_all_classes()
        labels = list(probs_all.keys())
        probs = list(probs_all.values())

        x_pos = np.arange(len(labels))

        plt.figure(figsize=(8, 6))

        bars = plt.bar(x_pos, probs, color=self._bar_color_contra)

        plt.xticks(x_pos, labels, rotation=45, ha="right")
        plt.ylim(0, 1.0)
        plt.title(title)
        plt.xlabel(x_label)
        plt.ylabel(y_label)


        # Werte auf den Balken anzeigen
        for bar in bars:
            height = bar.get_height()
            plt.text(
                bar.get_x() + bar.get_width() / 2,
                height + 0.01,
                f"{height:.{self._float_precision}f}",
                ha="center",
                va="bottom",
            )

        plt.tight_layout()

        full_save_path = self._build_diagram_path(filename)
        plt.savefig(full_save_path)
        plt.close('all')

        return full_save_path

    def get_prosa_explanation(self) -> str:
        top_label = self.get_top_label()
        probability = self.get_probability(top_label)
        prob_formatted = f"{probability * 100:.1f}%"

        # wörter liste
        most_important_words = self.get_top_k_word_contributions_for_label(
            top_label, 3, "positive"
        )
        words = [word for word, _ in most_important_words]

        # setze jedes Wort in Hochkommata
        words = [f"'{word}'" for word in words]

        # Erzeuge eine kommaseparierte Aufzählung mit "und" am Ende
        if len(words) == 0:
            words_as_string = "(keine)"
        elif len(words) == 1:
            words_as_string = words[0]
        elif len(words) == 2:
            words_as_string = f"{words[0]} und {words[1]}"
        else:
            words_as_string = ", ".join(words[:-1]) + f" und {words[-1]}"

        prosa_text = (
            f"Für dieses Ticket ist die Abteilung {top_label!r}"
            f" mit einer Gesamt-Wahrscheinlichkeit von {prob_formatted} zuständig."
            f" Besonders stark zugunsten der Einordnung sprachen die Wörter"
            f" {words_as_string}."
        )

        if self.has_baseline_variation():
            baseline = self.get_baseline_prob_for_class(top_label)
            baseline_formatted = f"{baseline * 100:.1f}%"
            bias_text = (
                f" Zusätzlich zum analysierten Text-Inhalt wurde eine"
                f" Grundwahrscheinlichkeit berücksichtigt, die mit {baseline_formatted}"
                f" für die gewählte Abteilung sprach."
            )
        else:
            bias_text = ""

        advantage = self.get_top_advantage()
        advantage_formatted = f"{advantage * 100:.1f}%"

        if advantage >= 0.3:
            advatage_text = "sehr deutlichen"
        elif advantage >= 0.2:
            advatage_text = "merklichen"
        else:
            advatage_text = "schwachen"

        advantage_text = (
            f" Die thematische Zuordnung zur Abteilung {top_label!r} ist mit einem"
            f" {advatage_text} Vorsprung von {advantage_formatted} gegenüber den"
            f" anderen Abteilungen erfolgt."
        )

        return prosa_text + advantage_text + bias_text

    def _build_diagram_path(self, filename: str) -> str:
        """
        Baut einen absoluten Dateipfad für Diagramme aus dem Pfad aus
        self._diagram_path und dem übergebenen Dateinamen.
        Erzwingt .png-Endung und erstellt erforderlichenfalls das Verzeichnis neu.

        Args:
            filename (str): Dateiname der Bilddatei.

        Returns:
            str: Vollständiger Pfad zur Zieldatei.
        """
        fallback_filename = "diagramm.png"

        # prüft, ob filename gültig ist und weist Ersatzwert zu
        if not isinstance(filename, str) or not filename.strip():
            filename = fallback_filename

        # prüft Dateiendung auf .png
        if not filename.lower().endswith(".png"):
            filename += ".png"

        # bereinigt Pfad je nach System (keine Doppelbackslashes)
        diagram_path_clean = os.path.normpath(self._diagram_path)

        # Da ich ggf. die Klasse im Notebook lade, brauche ich absoluten Pfad
        diagram_path_absolute = self._create_absolute_path(diagram_path_clean)

        # erstellt ggf. Zielordner
        if not os.path.isdir(diagram_path_absolute):
            os.makedirs(diagram_path_absolute, exist_ok=True)

        # Pfad kombinieren und zurückgeben
        return os.path.join(diagram_path_absolute, filename)

    def _create_absolute_path(self, rel_or_abs_path: str) -> str:
        """
        Prüft, ob der übergebene Pfad ein relativer Pfad ist und baut ihn zu einem
        absoluten Pfad um. Absolute Pfade werden hierbei nicht verändert. Diese Methode
        wird benötigt, falls diese Klasse im Jupyter Notebook instantiiert wird.

        Args:
            rel_or_abs_path (str): Ein absoluter oder relativer Pfad.

        Returns:
            str: Ein absoluter Pfad.

        Raises:
            FileNotFoundError: Falls die Zieldatei, auf die der Pfad weist, nicht
                existiert.
        """
        # absoluter Pfad wird unverändert zurückgegeben
        if os.path.isabs(rel_or_abs_path):
            absolute_path = rel_or_abs_path
        else:
            # berechnet den Pfad relativ zum Projektroot
            current_dir = os.path.dirname(__file__)
            # ich bin in Jafar/src/text_explanation.py, muss also zwei Ebenen hoch
            project_root = os.path.abspath(os.path.join(current_dir, "..", ".."))
            # aneinanderhängen
            absolute_path = os.path.join(project_root, rel_or_abs_path)

        return absolute_path

get_baseline_prob_all_classes()

Gibt die Basiswahrscheinlichkeiten in Range [0, 1] pro Klasse zurück. Die Basiswahrscheinlichkeit basiert auf einer Prediction auf dem Leerstring.

Returns:

Type Description
Dict[str, float]

Dict[str, float]: {'klassenname': Basiswahrscheinlichkeit}

Source code in src\explanation\text_explanation.py
def get_baseline_prob_all_classes(self) -> Dict[str, float]:
    """
    Gibt die Basiswahrscheinlichkeiten in Range [0, 1] pro Klasse
    zurück. Die Basiswahrscheinlichkeit basiert auf einer Prediction auf dem
    Leerstring.

    Returns:
        Dict[str, float]: {'klassenname': Basiswahrscheinlichkeit}
    """
    return {
        label: values["baseline_prob"]
        for label, values in self.class_biases.items()
    }

get_baseline_prob_for_class(class_name)

Gibt die Basiswahrscheinlichkeit in Range [0, 1] für die übergebene Klasse aus.

Parameters:

Name Type Description Default
class_name str

Der Klassenname.

required

Raises:

Type Description
ValueError

Wenn es für den Klassennamen keine Basiswahrscheinlichkeit gibt.

Returns:

Name Type Description
float float

Die Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig vom zu klassifizierenden Text. Range [0, 1]

Source code in src\explanation\text_explanation.py
def get_baseline_prob_for_class(self, class_name: str) -> float:
    """
    Gibt die Basiswahrscheinlichkeit in Range [0, 1] für die übergebene
    Klasse aus.

    Args:
        class_name (str): Der Klassenname.

    Raises:
        ValueError: Wenn es für den Klassennamen keine Basiswahrscheinlichkeit gibt.

    Returns:
        float: Die Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig
            vom zu klassifizierenden Text. Range [0, 1]
    """
    baseline_probs = self.get_baseline_prob_all_classes()
    try:
        return baseline_probs[class_name]
    except KeyError:
        raise ValueError(
            f"[En001] Klasse '{class_name}' ist nicht vorhanden."
            f"Bekannt: {list(baseline_probs.keys())}"
        )

get_bias_raw_for_class(class_name)

Gibt den Bias als Rohdatum für die Klassenentscheidung für die übergebene Klasse.

Parameters:

Name Type Description Default
class_name str

Der Klassenname.

required

Raises:

Type Description
ValueError

Wenn es für den Klassennamen keinen Bias gibt.

Returns:

Name Type Description
float float

Den Bias für die Klassenentscheidung unabhängig vom zu klassifizierenden Text.

Source code in src\explanation\text_explanation.py
def get_bias_raw_for_class(self, class_name: str) -> float:
    """
    Gibt den Bias als Rohdatum für die Klassenentscheidung für die übergebene
    Klasse.

    Args:
        class_name (str): Der Klassenname.

    Raises:
        ValueError: Wenn es für den Klassennamen keinen Bias gibt.

    Returns:
        float: Den Bias für die Klassenentscheidung unabhängig vom zu
            klassifizierenden Text.
    """
    raw_bias = self.get_biases_raw_all_classes()
    try:
        return raw_bias[class_name]
    except KeyError:
        raise ValueError(
            f"[En003] Klasse '{class_name}' ist nicht vorhanden."
            f"Bekannt: {list(raw_bias.keys())}"
        )

get_biases_raw_all_classes()

Gibt die Roh-Bias-/Log-Prior-Werte pro Klasse zurück.

Returns:

Type Description
Dict[str, float]

Dict[str, float]:{'klassenname': Bias-Wert}

Source code in src\explanation\text_explanation.py
def get_biases_raw_all_classes(self) -> Dict[str, float]:
    """
    Gibt die Roh-Bias-/Log-Prior-Werte pro Klasse zurück.

    Returns:
        Dict[str, float]:{'klassenname': Bias-Wert}
    """
    return {
        label: values["raw_bias"] for label, values in self.class_biases.items()
    }

get_colorized_text_as_html(label)

Wandelt einen Text in HTML um, wobei Wörter farblich nach ihren Beiträgen (einzeln und als Teil von ngrammen summiert) hervorgehoben werden. Die einzelnen Beiträge werden in den Tooltipps genannt.

Parameters:

Name Type Description Default
label str

das Label, für dessen Prädiktion die wichtigen Worte gezeigt werden sollen

required

Raises:

Type Description
ValueError

falls das Label nicht gefunden wird

Returns:

Name Type Description
str str

ein html-

-Tag mit farblich hervorgehobenen Wörtern und Tooltips mit den Wortgewichten

Source code in src\explanation\text_explanation.py
def get_colorized_text_as_html(self, label: str) -> str:
    """
    Wandelt einen Text in HTML um, wobei Wörter farblich nach ihren Beiträgen
    (einzeln und als Teil von ngrammen summiert) hervorgehoben werden. Die
    einzelnen Beiträge werden in den Tooltipps genannt.

    Args:
        label (str): das Label, für dessen Prädiktion die wichtigen Worte gezeigt
            werden sollen

    Raises:
        ValueError: falls das Label nicht gefunden wird

    Returns:
        str: ein html-<p>-Tag mit farblich hervorgehobenen Wörtern und Tooltips mit
            den Wortgewichten
    """

    # gibt es das Label?
    self._check_label(label)

    # holt die Wortgewichte für die Entscheidung
    contributions = self.get_word_contributions_for_label(
        label=label, normalize=True
    )

    # falls es keine Gewichte gibt, gib zumindest ordentlichen html-Tag aus
    if not contributions:
        return f"<p>{escape(self.input_text)}</p>"

    separated_words = self._split_text_to_list(self.input_text)

    token_matches = self._collect_token_matches(
        separated_words, contributions, self._preprocess_fn, self.token_type
    )

    # print("erhaltene token_matches aus _collect_token_matches")
    # pprint(token_matches)

    # maximum für farbskalierung holen
    max_abs_weight = self._calculate_max_abs_weight(token_matches)

    html_words = []

    for i, word in enumerate(separated_words):
        _, _, _, match_dict = token_matches[i]

        if match_dict:
            # summiere alle gewichte für dieses wort
            total_weight = sum(match_dict.values())

            # baue den tooltip mit einzelgewichten und summe
            matches = list(match_dict.items())
            tooltip = self._build_tooltip(matches, total_weight)

            # Skaliere Alpha-Wert, um besser sichtbare farbliche Kontraste zu
            # erstellen
            scaled_alpha = self._calculate_scaled_alpha(
                abs(total_weight), max_abs_weight
            )

            # Farbe je nach Vorzeichen
            color = (
                f"rgba({self._text_color_pro}, {scaled_alpha:.2f})"
                if total_weight > 0
                else f"rgba({self._text_color_contra}, {scaled_alpha:.2f})"
            )

            # HTML mit Tooltip und Farbe, Padding + border-radius für bessere Optik
            html = (
                f"<span title='{tooltip}' "
                f"style='background-color:{color};"
                f" padding:2px; border-radius:3px;'>"
                f"{escape(word)}</span>"
            )
        else:
            # Kein Wortgewicht für das Wort gespeichert: Wort ohne Tooltip ausgeben
            html = escape(word)

        html_words.append(html)

    return (
        '<p style="background-color: #fff; color:#000; font-family:sans-serif; '
        'line-height:1.5;">' + " ".join(html_words) + "</p>"
    )

get_prior_prob_all_classes()

Gibt die a-priori-Wahrscheinlichkeit pro Klasse in Range [0, 1] zurück.

Returns:

Type Description
Dict[str, float]

Dict[str, float]:{'klassenname': a-priori-Wahrscheinlichkeit}

Source code in src\explanation\text_explanation.py
def get_prior_prob_all_classes(self) -> Dict[str, float]:
    """
    Gibt die a-priori-Wahrscheinlichkeit pro Klasse in Range [0, 1] zurück.

    Returns:
        Dict[str, float]:{'klassenname': a-priori-Wahrscheinlichkeit}
    """
    return {
        label: values["prior_prob"] for label, values in self.class_biases.items()
    }

get_prior_prob_for_class(class_name)

Gibt die umgerechnete Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig vom zu klassifizierenden Text in Range [0, 1] für die übergebene Klasse.

Parameters:

Name Type Description Default
class_name str

Der Klassenname.

required

Raises:

Type Description
ValueError

Wenn es für den Klassennamen keine Basiswahrscheinlichkeit gibt.

Returns:

Name Type Description
float float

Die Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig vom zu klassifizierenden Text.

Source code in src\explanation\text_explanation.py
def get_prior_prob_for_class(self, class_name: str) -> float:
    """
    Gibt die umgerechnete Basiswahrscheinlichkeit für die Klassenentscheidung
    unabhängig vom zu klassifizierenden Text in Range [0, 1] für die übergebene
    Klasse.

    Args:
        class_name (str): Der Klassenname.

    Raises:
        ValueError: Wenn es für den Klassennamen keine Basiswahrscheinlichkeit gibt.

    Returns:
        float: Die Basiswahrscheinlichkeit für die Klassenentscheidung unabhängig
            vom zu klassifizierenden Text.
    """
    prior_probs = self.get_prior_prob_all_classes()
    try:
        return prior_probs[class_name]
    except KeyError:
        raise ValueError(
            f"[En002] Klasse '{class_name}' ist nicht vorhanden."
            f"Bekannt: {list(prior_probs.keys())}"
        )

get_probabilities()

Gibt sämtliche Labels und Wahrscheinlichkeiten als Dict aus

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Label, Score

Source code in src\explanation\text_explanation.py
def get_probabilities(self) -> Dict[str, float]:
    """
    Gibt sämtliche Labels und Wahrscheinlichkeiten als Dict aus

    Returns:
        Dict[str, float]: Label, Score
    """
    return self.class_probabilities

get_probability(label)

Gibt den Score für das übergebene Label aus oder ValueError

Parameters:

Name Type Description Default
label str

Das Label, für das ein Score gesucht wird

required

Raises:

Type Description
ValueError

wenn das Label unbekannt ist

Returns:

Name Type Description
float float

den Score des übergebenen Labels

Source code in src\explanation\text_explanation.py
def get_probability(self, label: str) -> float:
    """
    Gibt den Score für das übergebene Label aus oder ValueError

    Args:
        label (str): Das Label, für das ein Score gesucht wird

    Raises:
        ValueError: wenn das Label unbekannt ist

    Returns:
        float: den Score des übergebenen Labels
    """
    self._check_label(label)
    return self.class_probabilities[label]

get_removed_tokens()

Gibt alle Token des Eingabestrings zurück, die vom Preprocessing entfernt wurden.

Returns:

Name Type Description
set set

Set sämtlicher Token, die vom Preprocessing entfernt wurden.

Source code in src\explanation\text_explanation.py
def get_removed_tokens(self) -> set:
    """
    Gibt alle Token des Eingabestrings zurück, die vom Preprocessing entfernt
    wurden.

    Returns:
        set: Set sämtlicher Token, die vom Preprocessing entfernt wurden.
    """
    # zerlegt Eingabe in Token
    original_tokens = self.input_text.split()

    # führt die Vorverarbeitung für jedes Token entsprechend der Pipelinefunktion
    # durch
    preprocessed_tokens = [self._preprocess_fn(t) for t in original_tokens]

    # finde sämtliche Token, die nach dem Preprocessing leer sind
    removed_tokens = {
        orig for orig, prep in zip(original_tokens, preprocessed_tokens) if not prep
    }

    return removed_tokens

get_sorted_probabilities()

Gibt sämtliche Labels und Wahrscheinlichkeiten als absteigend sortierte Liste zurück

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: Label, Score

Source code in src\explanation\text_explanation.py
def get_sorted_probabilities(self) -> List[Tuple[str, float]]:
    """
    Gibt sämtliche Labels und Wahrscheinlichkeiten als absteigend sortierte Liste
    zurück

    Returns:
        List[Tuple[str, float]]: Label, Score
    """
    return sorted(
        self.class_probabilities.items(), key=lambda item: item[1], reverse=True
    )

get_top_k_probabilities(k=5)

Gibt eine Anzahl der Labels und Wahrscheinlichkeiten mit den höchsten Scores zurück.

Parameters:

Name Type Description Default
k int

Anzahl der höchstplatzierten Scores. Defaults to 5.

5

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: Liste der höchstplatzierten Label, Score

Source code in src\explanation\text_explanation.py
def get_top_k_probabilities(self, k: int = 5) -> List[Tuple[str, float]]:
    """
    Gibt eine Anzahl der Labels und Wahrscheinlichkeiten mit den höchsten Scores
    zurück.

    Args:
        k (int, optional): Anzahl der höchstplatzierten Scores. Defaults to 5.

    Returns:
        List[Tuple[str, float]]: Liste der höchstplatzierten Label, Score
    """
    return self.get_sorted_probabilities()[:k]

get_top_k_word_contributions_for_label(label, top_k=10, sign_filter='all', normalize=False)

Gibt die wichtigsten k Wörter zurück, die für die Klassifizierung des Labels entscheidend sind. Als Filterkriterien dürfen übergeben werden: "positive", "negative", (default) "all"

Parameters:

Name Type Description Default
label str

Das Label, für das die Wortbeiträge ermittelt werden.

required
top_k (Optional, int)

Anzahl der wichtigsten Beiträge. Defaults to 10.

10
sign_filter str

(Optional, str): Filtert nach Vorzeichen der Wortbeiträge, zulässige Kriterien sind: "positive", "negative", (default) "all"

'all'
normalize (Optional, bool)

Ermöglicht zusätzliche Normalisierung der Wortbeiträge innerhalb einer Range von [-1, 1]. Defaults to False.

False

Raises:

Type Description
ValueError
  • Wenn falscher Wert für 'sign_filter' übergeben wird
  • Wenn das übergebene Label nicht existiert.

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: description

Source code in src\explanation\text_explanation.py
def get_top_k_word_contributions_for_label(
    self,
    label: str,
    top_k: int = 10,
    sign_filter: str = "all",
    normalize: bool = False,
) -> List[Tuple[str, float]]:
    """
    Gibt die wichtigsten k Wörter zurück, die für die Klassifizierung des
    Labels entscheidend sind. Als Filterkriterien dürfen übergeben werden:
    "positive", "negative", (default) "all"

    Args:
        label (str): Das Label, für das die Wortbeiträge ermittelt werden.
        top_k (Optional, int): Anzahl der wichtigsten Beiträge. Defaults to 10.
        sign_filter: (Optional, str): Filtert nach Vorzeichen der Wortbeiträge,
            zulässige Kriterien sind: "positive", "negative", (default) "all"
        normalize (Optional, bool): Ermöglicht zusätzliche Normalisierung der
            Wortbeiträge innerhalb einer Range von [-1, 1]. Defaults to False.

    Raises:
        ValueError:
            - Wenn falscher Wert für 'sign_filter' übergeben wird
            - Wenn das übergebene Label nicht existiert.

    Returns:
        List[Tuple[str, float]]: _description_
    """

    # prüft Zulässigkeit des Filterkriteriums
    valid_filters = {"positive", "negative", "all"}
    if sign_filter not in valid_filters:
        raise ValueError(
            f"[En004] Ungültiger Wert für sign_filter: {sign_filter!r}. "
            f"Erlaubt sind: {', '.join(valid_filters)}."
        )

    # prüft Zulässigkeit des übergebenen Labels
    self._check_label(label)

    # holt Contributions für das gegebene Label
    contribs = self.get_word_contributions_for_label(label, normalize=normalize)

    # filtert Contributions
    if sign_filter == "positive":
        filtered = [(word, score) for word, score in contribs.items() if score > 0]
        sorted_contribs = sorted(filtered, key=lambda x: x[1], reverse=True)
    elif sign_filter == "negative":
        filtered = [(word, score) for word, score in contribs.items() if score < 0]
        sorted_contribs = sorted(filtered, key=lambda x: x[1])
    else:  # "all": die größten absoluten Werte
        sorted_contribs = sorted(
            contribs.items(), key=lambda x: abs(x[1]), reverse=True
        )

    # Top-k zurückgeben
    return sorted_contribs[:top_k]

get_top_label()

Gibt das Label mit dem höchsten Score zurück

Source code in src\explanation\text_explanation.py
def get_top_label(self) -> str:
    """Gibt das Label mit dem höchsten Score zurück"""
    return self.get_sorted_probabilities()[0][0]

get_top_label_and_probability()

Gibt Label und mit höchster Wahrscheinlichkeit zurück

Source code in src\explanation\text_explanation.py
def get_top_label_and_probability(self) -> Tuple[str, float]:
    """Gibt Label und mit höchster Wahrscheinlichkeit zurück"""
    return self.get_sorted_probabilities()[0]

get_word_contributions_for_label(label, normalize=False)

Gibt alle Wortbeiträge zurück, die pro/contra das übergebene Label sprechen

Parameters:

Name Type Description Default
label str

Das Label, für/gegen das die Wortbeiträge sprechen

required
normalize (Optional, bool)

Ermöglicht zusätzliche Normalisierung der Wortbeiträge innerhalb einer Range von [-1, 1]. Defaults to False.

False

Raises:

Type Description
ValueError

Falls das Label nicht in word_contributions gefunden wird

Source code in src\explanation\text_explanation.py
def get_word_contributions_for_label(
    self, label: str, normalize: bool = False
) -> Dict[str, float]:
    """
    Gibt alle Wortbeiträge zurück, die pro/contra das übergebene Label sprechen

    Args:
        label (str): Das Label, für/gegen das die Wortbeiträge sprechen
        normalize (Optional, bool): Ermöglicht zusätzliche Normalisierung der
            Wortbeiträge innerhalb einer Range von [-1, 1]. Defaults to False.

    Raises:
        ValueError: Falls das Label nicht in word_contributions gefunden wird
    """

    # prüft, ob Label existiert
    self._check_label(label)

    # holt die Wortbeiträge für das Label
    raw_contribs = self.word_contributions[label]

    # stellt sicher, dass die Beiträge float sind
    typed_contribs = {
        word: float(contribs) for word, contribs in raw_contribs.items()
    }

    # normalisiert optional oder gibt Daten aus
    if normalize:
        return self._normalize_contributions(typed_contribs)
    else:
        return typed_contribs

has_baseline_variation()

Prüft, ob die Baseline-Werte sich zwischen den Klassen unterscheiden.

Returns:

Name Type Description
bool bool

True, wenn mindestens zwei Klassen sich mehr als der Schwellwert unterscheiden.

Source code in src\explanation\text_explanation.py
def has_baseline_variation(self) -> bool:
    """
    Prüft, ob die Baseline-Werte sich zwischen den Klassen unterscheiden.

    Returns:
        bool: True, wenn mindestens zwei Klassen sich mehr als der Schwellwert
            unterscheiden.
    """
    baseline_probs = list(self.get_baseline_prob_all_classes().values())
    return self._has_variation(baseline_probs)

has_bias_variation()

Prüft, ob die Bias-Werte sich zwischen den Klassen unterscheiden.

Returns:

Name Type Description
bool bool

True, wenn mindestens zwei Klassen sich mehr als der Schwellwert unterscheiden.

Source code in src\explanation\text_explanation.py
def has_bias_variation(self) -> bool:
    """
    Prüft, ob die Bias-Werte sich zwischen den Klassen unterscheiden.

    Returns:
        bool: True, wenn mindestens zwei Klassen sich mehr als der Schwellwert
            unterscheiden.
    """
    biases = list(self.get_biases_raw_all_classes().values())
    return self._has_variation(biases)

plot_baseline(filename='diagramm_baseline.png')

Zeichnet ein vertikales Balkendiagramm zur Visualisierung der Baseline-Wahrscheinlichkeiten.

Returns:

Name Type Description
str str

Pfad zur gespeicherten Bilddatei mit dem Diagramm.

Source code in src\explanation\text_explanation.py
def plot_baseline(self, filename: str = "diagramm_baseline.png") -> str:
    """
    Zeichnet ein vertikales Balkendiagramm zur Visualisierung der
    Baseline-Wahrscheinlichkeiten.

    Returns:
        str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
    """
    title = "Baseline-Wahrscheinlichkeiten"
    x_label = "Klasse"
    y_label = "Baseline-Wahrscheinlichkeit"

    # class_biases unsortiert extrahieren
    probs_all = self.get_baseline_prob_all_classes()
    labels = list(probs_all.keys())
    probs = list(probs_all.values())

    x_pos = np.arange(len(labels))

    plt.figure(figsize=(8, 6))

    bars = plt.bar(x_pos, probs, color=self._bar_color_contra)

    plt.xticks(x_pos, labels, rotation=45, ha="right")
    plt.ylim(0, 1.0)
    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)


    # Werte auf den Balken anzeigen
    for bar in bars:
        height = bar.get_height()
        plt.text(
            bar.get_x() + bar.get_width() / 2,
            height + 0.01,
            f"{height:.{self._float_precision}f}",
            ha="center",
            va="bottom",
        )

    plt.tight_layout()

    full_save_path = self._build_diagram_path(filename)
    plt.savefig(full_save_path)
    plt.close('all')

    return full_save_path

plot_class_biases(filename='diagramm_class_biases.png')

Zeichnet ein vertikales Balkendiagramm zur Visualisierung der jeweiligen Klassen-Biase. Da die zu erwartenden Werte sehr verschieden sein können, wird hier keine Skala festgelegt.

Parameters:

Name Type Description Default
filename (Optional, str)

Dateiname, unter dem das Diagramm gespeichert wird. Defaultwert gesetzt. (Der Pfad kann in der xai_config.yaml geändert werden.)

'diagramm_class_biases.png'

Returns:

Name Type Description
str str

Pfad zur gespeicherten Bilddatei mit dem Diagramm.

Source code in src\explanation\text_explanation.py
def plot_class_biases(
        self,
        filename: str = "diagramm_class_biases.png"
        ) -> str:
    """
    Zeichnet ein vertikales Balkendiagramm zur Visualisierung der jeweiligen
    Klassen-Biase. Da die zu erwartenden Werte sehr verschieden sein können,
    wird hier keine Skala festgelegt.

    Args:
        filename (Optional, str): Dateiname, unter dem das Diagramm gespeichert
            wird. Defaultwert gesetzt. (Der Pfad kann in der xai_config.yaml
            geändert werden.)

    Returns:
        str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
    """
    title = "Class Biases"
    x_label = "Klasse"
    y_label = "Bias-Wert"

    # Anzeigewerte holen
    biases_raw = self.get_biases_raw_all_classes()  # Methode aufrufen!
    labels = list(biases_raw.keys())
    biases_orig = list(biases_raw.values())

    # Maske: welche Werte waren None?
    bias_none_mask = [b is None for b in biases_orig]

    # None-Werte für den Plot durch 0 ersetzen
    biases = [0 if b is None else b for b in biases_orig]

    x_pos = np.arange(len(labels))

    plt.figure(figsize=(8, 6))

    bars = plt.bar(x_pos, biases, color=self._bar_color_contra)

    plt.xticks(x_pos, labels, rotation=45, ha="right")
    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)

    # Werte auf den Balken anzeigen
    for idx, bar in enumerate(bars):
        height = bar.get_height()

        # Beschriftung für None-Werte setzen
        if bias_none_mask[idx]:
            label = "None"
        else:
            label = f"{height:.{self._float_precision}f}"

        plt.text(
            bar.get_x() + bar.get_width() / 2,
            height + 0.01,
            label,
            ha="center",
            va="bottom",
        )

    plt.tight_layout()

    full_save_path = self._build_diagram_path(filename)
    plt.savefig(full_save_path)

    plt.close('all')

    # hier wird zusätzlich der Pfad zur png-Datei zurückgegeben
    return full_save_path

plot_prior_probs(filename='diagramm_prior_prob.png')

Zeichnet ein vertikales Balkendiagramm zur Visualisierung der unsortierten A-Priori-Wahrscheinlichkeiten, die aus dem Bias errechnet werden.

Returns:

Name Type Description
str str

Pfad zur gespeicherten Bilddatei mit dem Diagramm.

Source code in src\explanation\text_explanation.py
def plot_prior_probs(self, filename: str = "diagramm_prior_prob.png") -> str:
    """
    Zeichnet ein vertikales Balkendiagramm zur Visualisierung der unsortierten
    A-Priori-Wahrscheinlichkeiten, die aus dem Bias errechnet werden.

    Returns:
        str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
    """
    title = "A-priori-Wahrscheinlichkeiten"
    x_label = "Klasse"
    y_label = "A-priori-Wahrscheinlichkeit"

    # Anzeigewerte holen
    probs_all = self.get_prior_prob_all_classes()
    labels = list(probs_all.keys())
    probs_orig = list(probs_all.values())

    # Maske: welche Werte waren None?
    probs_none_mask = [b is None for b in probs_orig]

    # None-Werte für den Plot durch 0 ersetzen
    probs = [0 if prob is None else prob for prob in probs_orig]

    x_pos = np.arange(len(labels))

    plt.figure(figsize=(8, 6))

    bars = plt.bar(x_pos, probs, color=self._bar_color_contra)

    plt.xticks(x_pos, labels, rotation=45, ha="right")
    plt.ylim(0, 1.0)
    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)

    for idx, bar in enumerate(bars):
        height = bar.get_height()

        # Beschriftung für None-Werte setzen
        if probs_none_mask[idx]:
            label = "None"
        else:
            label = f"{height:.{self._float_precision}f}"
        plt.text(
            bar.get_x() + bar.get_width() / 2,
            height + 0.01,
            label,
            ha="center",
            va="bottom",
        )

    plt.tight_layout()

    full_save_path = self._build_diagram_path(filename)
    plt.savefig(full_save_path)
    plt.close('all')

    return full_save_path

plot_top_k_class_probabilities(top_k=5, filename='diagramm_wahrscheinlichkeiten.png')

Zeichnet ein Balkendiagramm mit Klassifizierungswahrscheinlichkeiten. Die wahrscheinlichste Klasse wird farblich hervorgehoben, Farben können in der xai-config.yaml eingestellt werden.

Parameters:

Name Type Description Default
top_k int

Anzahl der Klassen mit den höchsten Wahrscheinlichkeiten, die angezeigt werden sollen. Default ist 5.

5

Returns:

Name Type Description
str str

Pfad zur gespeicherten Bilddatei mit dem Diagramm.

Source code in src\explanation\text_explanation.py
def plot_top_k_class_probabilities(
    self, top_k: int = 5, filename="diagramm_wahrscheinlichkeiten.png"
) -> str:
    """
    Zeichnet ein Balkendiagramm mit Klassifizierungswahrscheinlichkeiten. Die
    wahrscheinlichste Klasse wird farblich hervorgehoben, Farben können in der
    xai-config.yaml eingestellt werden.

    Args:
        top_k (int, optional): Anzahl der Klassen mit den höchsten
            Wahrscheinlichkeiten, die angezeigt werden sollen. Default ist 5.

    Returns:
        str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
    """
    title = "Klassifikations-Wahrscheinlichkeiten"
    x_label = "Wahrscheinlichkeit"
    y_label = "Klassifikation"

    sorted_items = self.get_sorted_probabilities()

    top_items = sorted_items[:top_k]
    labels = [item[0] for item in top_items]
    probs = [item[1] for item in top_items]

    y_pos = np.arange(len(labels))

    # Farbliste definieren: erste Bar = pro, Rest = contra
    colors = [self._bar_color_pro] + [self._bar_color_contra] * (len(probs) - 1)

    plt.figure(figsize=(8, 6))
    bars = plt.barh(y_pos, probs, color=colors)
    plt.yticks(y_pos, labels)
    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.xlim(0, 1.0)

    # Höchste Wahrscheinlichkeit oben
    plt.gca().invert_yaxis()

    # Werte werden mit angezeigt
    for bar in bars:
        width = bar.get_width()
        plt.text(
            width + 0.01,
            bar.get_y() + bar.get_height() / 2,
            f"{width:.{self._float_precision}f}",
            va="center",
        )

    plt.tight_layout()

    full_save_path = self._build_diagram_path(filename)
    plt.savefig(full_save_path)
    plt.close()

    return full_save_path

plot_word_contributions(label, top_k=10, filename='diagramm_wortgewichte.png')

Zeichnet ein horizontales Balkendiagramm mit den wichtigsten Wortbeiträgen zu einer Klassenvorhersage. Positive Beiträge werden farblich von negativen unterschieden, Farben können in der xai-config.yaml eingestellt werden.

Parameters:

Name Type Description Default
label str

Klassenlabel, für das die Wortbeiträge dargestellt werden.

required
top_k int

Anzahl der Wörter mit den größten (absoluten) Gewichtungen, die angezeigt werden sollen. Default ist 10.

10

Returns:

Name Type Description
str str

Pfad zur gespeicherten Bilddatei mit dem Diagramm.

Source code in src\explanation\text_explanation.py
def plot_word_contributions(
    self, label: str, top_k: int = 10, filename="diagramm_wortgewichte.png"
) -> str:
    """
    Zeichnet ein horizontales Balkendiagramm mit den wichtigsten Wortbeiträgen zu
    einer Klassenvorhersage. Positive Beiträge werden farblich von negativen
    unterschieden, Farben können in der xai-config.yaml eingestellt werden.

    Args:
        label (str): Klassenlabel, für das die Wortbeiträge dargestellt werden.
        top_k (int, optional): Anzahl der Wörter mit den größten (absoluten)
            Gewichtungen, die angezeigt werden sollen. Default ist 10.

    Returns:
        str: Pfad zur gespeicherten Bilddatei mit dem Diagramm.
    """
    title = f"Top {top_k} Wortgewichte für die Entscheidung {label!r}"
    x_label = "Wortgewicht"
    y_label = "Wort"

    sorted_items = self.get_top_k_word_contributions_for_label(label, top_k)

    tokens = [item[0] for item in sorted_items]
    weights = np.array([item[1] for item in sorted_items])
    y_pos = np.arange(len(tokens))
    plt.figure(figsize=(8, 6))

    colors = [
        self._bar_color_pro if w > 0 else self._bar_color_contra for w in weights
    ]

    plt.barh(y_pos, weights, color=colors)
    plt.title(title)
    plt.xlabel(x_label)
    plt.ylabel(y_label)
    plt.yticks(y_pos, tokens)

    plt.axvline(x=0, color="black", linewidth=0.8)
    # Höchstes Gewicht oben
    plt.gca().invert_yaxis()

    plt.tight_layout()

    full_save_path = self._build_diagram_path(filename)
    plt.savefig(full_save_path)
    plt.close()

    return full_save_path

src.explanation.inspectors.base_inspector

BaseInspector

Bases: ABC

Diese abstrakte Basisklasse bietet die Schnittstelle, um Inspector-Klassen zu implementieren, die im Wege des Strategy Patterns Analysedaten aus verschiedenartigen Modellen extrahieren können (aktuell: lineare, logarithmische Modelle sowie ein agnostischer Inspector als Fallback für Blackbox-Modelle).

Author

irene

Source code in src\explanation\inspectors\base_inspector.py
class BaseInspector(ABC):
    """
    Diese abstrakte Basisklasse bietet die Schnittstelle, um Inspector-Klassen zu
    implementieren, die im Wege des Strategy Patterns Analysedaten aus
    verschiedenartigen Modellen extrahieren können (aktuell: lineare, logarithmische
    Modelle sowie ein agnostischer Inspector als Fallback für Blackbox-Modelle).

    Author:
        irene
    """
    def __init__(self, predictor: TextPredictor):
        self._predictor = predictor

    @property
    def predictor(self):
        return self._predictor

    @property
    def pipeline(self):
        return self._predictor.pipeline

    @property
    def classifier(self):
        return self._predictor.classifier

    @property
    def vectorizer(self):
        return self._predictor.vectorizer

    @property
    def predict_proba(self):
        return self._predictor.pipeline.predict_proba

    @property
    def class_names(self) -> List[str]:
        return self._predictor.class_names

    @property
    def num_class_names(self) -> int:
        return self._predictor.num_class_names

    @abstractmethod
    def inspect_model_weights_by_index(
        self, label_index: int
    ) -> List[Tuple[str, Optional[float]]]:
        raise NotImplementedError

    @abstractmethod
    def inspect_model_weights_top_k(
            self,
            label_index: int,
            top_k: int = 20,
            sign_filter: str = "all"
        ) -> List[Tuple[str, float]]:
            raise NotImplementedError

    @abstractmethod
    def compute_word_contributions(
        self,
        input_text: str,
        feature_values: np.ndarray,
        feature_names: list[str],
    ) -> Dict[str, Dict[str, float]]:
        raise NotImplementedError

    @abstractmethod
    def get_token_type(self) -> str:
        raise NotImplementedError

    @abstractmethod
    def get_raw_biases_per_class(self) -> Dict[str, float]:
        raise NotImplementedError

    @abstractmethod
    def get_prior_probs_per_class(self) -> Dict[str, float]:
        raise NotImplementedError

src.explanation.inspectors.linear_model_inspector

LinearModelInspector

Bases: BaseInspector

Diese Klasse dient dazu, Informationen über lineare Modelle (insb. LogisticRegression, LinearSVM) auszugeben. Sie stellt Methoden bereit, die die Wortgewichte für eine Textprediction ausgeben, aber auch die Biase und Wortgewichte, die für ein lineares Modell unabhängig von einer einzelnen Textpädiktion gelten. Die Berechnungen legen das Vorhandensein der Attribute .coef_ und .intercept_ zugrunde, die in linearen Modellen vorhanden sind. Der LinearModelInspector wird erst instantiiert, nachdem (vom TextExplainer) geprüft wurde, ob das Model die Voraussetzungen erfüllt. Das Modell selbst wird als Bestandteil der Pipeline im TextPredictor via Constructor Injection injiziert. Abstrakte Elternklasse ist BaseInspector, der den Konstruktor sowie die erforderlichen Auswertungsmethoden vorgibt.

Author

irene

Source code in src\explanation\inspectors\linear_model_inspector.py
class LinearModelInspector(BaseInspector):
    """
    Diese Klasse dient dazu, Informationen über lineare Modelle (insb.
    LogisticRegression, LinearSVM) auszugeben. Sie stellt Methoden bereit, die die
    Wortgewichte für eine Textprediction ausgeben, aber auch die Biase und Wortgewichte,
    die für ein lineares Modell unabhängig von einer einzelnen Textpädiktion gelten.
    Die Berechnungen legen das Vorhandensein der Attribute .coef_ und .intercept_
    zugrunde, die in linearen Modellen vorhanden sind. Der LinearModelInspector
    wird erst instantiiert, nachdem (vom TextExplainer) geprüft wurde, ob das Model
    die Voraussetzungen erfüllt. Das Modell selbst wird als Bestandteil der Pipeline im
    TextPredictor via Constructor Injection injiziert.
    Abstrakte Elternklasse ist BaseInspector, der den Konstruktor sowie die
    erforderlichen Auswertungsmethoden vorgibt.

    Author:
        irene
    """
    def inspect_model_weights_by_index(
        self, label_index: int
    ) -> List[Tuple[str, Optional[float]]]:
        """
        Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche
        Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen
        sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions.
        Falls keine Gewichte berechnet werden können, wird None zurückgegeben.

        Args:
            label_index (int): Index des Labels, für das die gewichteten Wörter
                ausgegeben werden.

        Returns:
            List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel
        """
        # holt alle wörter aus dem Vectorizer
        words = self.vectorizer.get_feature_names_out()

        # Gewichte sind erstmal None, außer sie werden sinnvoll gefüllt
        weights = None
        weights = self.classifier.coef_[label_index]

        # Konsistenzprüfung: Anzahl Wörter == Anzahl Gewichte?
        # Fallback auf None, falls keine gültigen Gewichte vorliegen oder die Daten
        # nicht konsistent sind
        if weights is None or len(words) != len(weights):
            weights = [None] * len(words)

        words_and_weights = list(zip(words, weights))

        return words_and_weights

    def inspect_model_weights_top_k(
        self, label_index: int, top_k: int = 20, sign_filter: str = "all"
    ) -> List[Tuple[str, float]]:
        """
        Gibt die Top-k Modellgewichte für Label zurück, dessen Index übergeben wurde.
        Die Gewichte beziehen sich auf das trainierte Modell selbst und sind unabhängig
        von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine
        leere Liste zurückgegeben.
        Es handelt sich hier um lineare Modelle, so dass ein hoher positiver Wert
        bedeutet, dass das Wort stark zugunsten der Klassifizierung spricht. Negative
        Werte sprechen folgerichtig stark zuungunsten der Klassifizierung. Werte nahe
        oder gleich 0 sind für die Klassifizierung irrelevant.

        Args:
            label_index (int): Das Label, für das die Top-Wörter ausgegeben werden.
            top_k (Optional, int): Anzahl der Top-Wörter nach Gewichtung.
            sign_filter (Optional, str): Filtert nach Vorzeichen der Modellgewichte
                mit erlaubten Werten "pro", "contra", "all"

        Returns:
            List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert
                nach Betrag. Ersatzweise eine leere Liste.

        Raises:
            ValueError:
                - Wenn falscher Wert für 'valid_filters' übergeben wird
        """
        valid_filters = {"pro", "contra", "all"}
        if sign_filter not in valid_filters:
            raise ValueError(
                f"[Er001] Ungültiger Wert für sign_filter: {sign_filter!r}. "
                f"Erlaubt sind: {', '.join(valid_filters)}."
            )

        weights = self.inspect_model_weights_by_index(label_index)
        # None-Werte rausfiltern
        weights = [item for item in weights if item[1] is not None]

        if sign_filter == "pro":
            filtered = [item for item in weights if item[1] > 0]
            sorted_weights = sorted(filtered, key=lambda x: x[1], reverse=True)
        elif sign_filter == "contra":
            filtered = [item for item in weights if item[1] < 0]
            sorted_weights = sorted(filtered, key=lambda x: x[1])
        else:
            # "all": vorzeichenunabhängig nach Betrag absteigend sortiert
            sorted_weights = sorted(weights, key=lambda x: abs(x[1]), reverse=True)


        return sorted_weights[:top_k]


    def get_raw_biases_per_class(self) -> Dict[str, float]:
        """
        Gibt die rohen Bias-Werte (Intercepts) für jede Klasse zurück.

        Die Intercepts des Klassifikators werden ausgelesen und als Dictionary
        zurückgegeben, das jedem Klassennamen den zugehörigen rohen Bias zuordnet.

        Returns:
            Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
            und die Werte die rohen Bias-Werte als Fließkommazahlen sind.
        """
        raw_biases = self.classifier.intercept_
        return {
            class_name: float(raw_bias)
            for class_name, raw_bias in zip(self.class_names, raw_biases)
        }

    def get_prior_probs_per_class(self) -> Dict[str, float]:
        """
        Berechnet die a-priori-Wahrscheinlichkeiten für jede Klasse basierend auf den
        rohen Bias-Werten.

        Die rohen Bias-Werte (Intercepts) werden mit der Softmax-Funktion
        transformiert, um die Wahrscheinlichkeiten für jede Klasse zu erhalten.
        Das Ergebnis wird als Dictionary zurückgegeben.

        Returns:
            Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
            und die Werte die a-priori-Wahrscheinlichkeiten (zwischen 0 und 1) sind.
        """
        raw_biases = self.classifier.intercept_
        prior_probs = softmax(raw_biases)
        return {
            class_name: float(prior_prob)
            for class_name, prior_prob in zip(self.class_names, prior_probs)
        }

    def get_token_type(self) -> str:
        """
        Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf
        dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden. Vorliegend
        "processed".

        Returns:
            str: "raw" oder "processed"
        """
        return "processed"

    def compute_word_contributions(
        self,
        input_text: str,
        feature_values: np.ndarray,
        feature_names: list[str],
    ) -> Dict[str, Dict[str, float]]:
        """
        Berechnet den Einfluss einzelner Wörter auf die Klassifikation bei linearen
        Modellen (z.B. LogisticRegression). Verwendet dazu die Modellgewichte
        (aus model.coeff_) und die Merkmalswerte (feature_values) des Eingabetexts
        (feature_names).
        Es werden die Einflüsse für alle Klassenlabels berechnet.

        Args:
            feature_values (np.ndarray): Vektorisierter Text als Zahlenarray.
            feature_names (list[str]): Namen der Merkmale (z.B. Wörter).
            input_text (str): Der ursprüngliche Text, der aber hier nicht verwendet
                wird (wird übergeben für die Einheitlichkeit der Signatur).

        Returns:
            Dict[label_name, Dict[word, contribution]]: pro Label: Wörter und ihre
                Beiträge.
        """

        contributions_all = {}

        # Modellgewichte abrufen (Form: n_classes x n_features)
        model_weights = self.classifier.coef_

        # Über alle Klassenlabels iterieren
        for label_index, label_name in enumerate(self.class_names):
            # Gewichte für die aktuelle Klasse holen
            weights_for_label = model_weights[label_index]

            # Beitrag jedes Merkmals berechnen (Gewicht * Merkmalswert)
            contributions = weights_for_label * feature_values

            # Wörter mit ihren Beiträgen sammeln, aber nur für vorkommende Wörter
            word_contributions = {}
            for index, value in enumerate(feature_values):
                if value != 0:
                    word = feature_names[index]
                    contribution = contributions[index]
                    word_contributions[word] = contribution

            # Ergebnis für dieses Label speichern
            contributions_all[label_name] = word_contributions

        return contributions_all

compute_word_contributions(input_text, feature_values, feature_names)

Berechnet den Einfluss einzelner Wörter auf die Klassifikation bei linearen Modellen (z.B. LogisticRegression). Verwendet dazu die Modellgewichte (aus model.coeff_) und die Merkmalswerte (feature_values) des Eingabetexts (feature_names). Es werden die Einflüsse für alle Klassenlabels berechnet.

Parameters:

Name Type Description Default
feature_values ndarray

Vektorisierter Text als Zahlenarray.

required
feature_names list[str]

Namen der Merkmale (z.B. Wörter).

required
input_text str

Der ursprüngliche Text, der aber hier nicht verwendet wird (wird übergeben für die Einheitlichkeit der Signatur).

required

Returns:

Type Description
Dict[str, Dict[str, float]]

Dict[label_name, Dict[word, contribution]]: pro Label: Wörter und ihre Beiträge.

Source code in src\explanation\inspectors\linear_model_inspector.py
def compute_word_contributions(
    self,
    input_text: str,
    feature_values: np.ndarray,
    feature_names: list[str],
) -> Dict[str, Dict[str, float]]:
    """
    Berechnet den Einfluss einzelner Wörter auf die Klassifikation bei linearen
    Modellen (z.B. LogisticRegression). Verwendet dazu die Modellgewichte
    (aus model.coeff_) und die Merkmalswerte (feature_values) des Eingabetexts
    (feature_names).
    Es werden die Einflüsse für alle Klassenlabels berechnet.

    Args:
        feature_values (np.ndarray): Vektorisierter Text als Zahlenarray.
        feature_names (list[str]): Namen der Merkmale (z.B. Wörter).
        input_text (str): Der ursprüngliche Text, der aber hier nicht verwendet
            wird (wird übergeben für die Einheitlichkeit der Signatur).

    Returns:
        Dict[label_name, Dict[word, contribution]]: pro Label: Wörter und ihre
            Beiträge.
    """

    contributions_all = {}

    # Modellgewichte abrufen (Form: n_classes x n_features)
    model_weights = self.classifier.coef_

    # Über alle Klassenlabels iterieren
    for label_index, label_name in enumerate(self.class_names):
        # Gewichte für die aktuelle Klasse holen
        weights_for_label = model_weights[label_index]

        # Beitrag jedes Merkmals berechnen (Gewicht * Merkmalswert)
        contributions = weights_for_label * feature_values

        # Wörter mit ihren Beiträgen sammeln, aber nur für vorkommende Wörter
        word_contributions = {}
        for index, value in enumerate(feature_values):
            if value != 0:
                word = feature_names[index]
                contribution = contributions[index]
                word_contributions[word] = contribution

        # Ergebnis für dieses Label speichern
        contributions_all[label_name] = word_contributions

    return contributions_all

get_prior_probs_per_class()

Berechnet die a-priori-Wahrscheinlichkeiten für jede Klasse basierend auf den rohen Bias-Werten.

Die rohen Bias-Werte (Intercepts) werden mit der Softmax-Funktion transformiert, um die Wahrscheinlichkeiten für jede Klasse zu erhalten. Das Ergebnis wird als Dictionary zurückgegeben.

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen

Dict[str, float]

und die Werte die a-priori-Wahrscheinlichkeiten (zwischen 0 und 1) sind.

Source code in src\explanation\inspectors\linear_model_inspector.py
def get_prior_probs_per_class(self) -> Dict[str, float]:
    """
    Berechnet die a-priori-Wahrscheinlichkeiten für jede Klasse basierend auf den
    rohen Bias-Werten.

    Die rohen Bias-Werte (Intercepts) werden mit der Softmax-Funktion
    transformiert, um die Wahrscheinlichkeiten für jede Klasse zu erhalten.
    Das Ergebnis wird als Dictionary zurückgegeben.

    Returns:
        Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
        und die Werte die a-priori-Wahrscheinlichkeiten (zwischen 0 und 1) sind.
    """
    raw_biases = self.classifier.intercept_
    prior_probs = softmax(raw_biases)
    return {
        class_name: float(prior_prob)
        for class_name, prior_prob in zip(self.class_names, prior_probs)
    }

get_raw_biases_per_class()

Gibt die rohen Bias-Werte (Intercepts) für jede Klasse zurück.

Die Intercepts des Klassifikators werden ausgelesen und als Dictionary zurückgegeben, das jedem Klassennamen den zugehörigen rohen Bias zuordnet.

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen

Dict[str, float]

und die Werte die rohen Bias-Werte als Fließkommazahlen sind.

Source code in src\explanation\inspectors\linear_model_inspector.py
def get_raw_biases_per_class(self) -> Dict[str, float]:
    """
    Gibt die rohen Bias-Werte (Intercepts) für jede Klasse zurück.

    Die Intercepts des Klassifikators werden ausgelesen und als Dictionary
    zurückgegeben, das jedem Klassennamen den zugehörigen rohen Bias zuordnet.

    Returns:
        Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
        und die Werte die rohen Bias-Werte als Fließkommazahlen sind.
    """
    raw_biases = self.classifier.intercept_
    return {
        class_name: float(raw_bias)
        for class_name, raw_bias in zip(self.class_names, raw_biases)
    }

get_token_type()

Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden. Vorliegend "processed".

Returns:

Name Type Description
str str

"raw" oder "processed"

Source code in src\explanation\inspectors\linear_model_inspector.py
def get_token_type(self) -> str:
    """
    Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf
    dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden. Vorliegend
    "processed".

    Returns:
        str: "raw" oder "processed"
    """
    return "processed"

inspect_model_weights_by_index(label_index)

Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions. Falls keine Gewichte berechnet werden können, wird None zurückgegeben.

Parameters:

Name Type Description Default
label_index int

Index des Labels, für das die gewichteten Wörter ausgegeben werden.

required

Returns:

Type Description
List[Tuple[str, Optional[float]]]

List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel

Source code in src\explanation\inspectors\linear_model_inspector.py
def inspect_model_weights_by_index(
    self, label_index: int
) -> List[Tuple[str, Optional[float]]]:
    """
    Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche
    Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen
    sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions.
    Falls keine Gewichte berechnet werden können, wird None zurückgegeben.

    Args:
        label_index (int): Index des Labels, für das die gewichteten Wörter
            ausgegeben werden.

    Returns:
        List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel
    """
    # holt alle wörter aus dem Vectorizer
    words = self.vectorizer.get_feature_names_out()

    # Gewichte sind erstmal None, außer sie werden sinnvoll gefüllt
    weights = None
    weights = self.classifier.coef_[label_index]

    # Konsistenzprüfung: Anzahl Wörter == Anzahl Gewichte?
    # Fallback auf None, falls keine gültigen Gewichte vorliegen oder die Daten
    # nicht konsistent sind
    if weights is None or len(words) != len(weights):
        weights = [None] * len(words)

    words_and_weights = list(zip(words, weights))

    return words_and_weights

inspect_model_weights_top_k(label_index, top_k=20, sign_filter='all')

Gibt die Top-k Modellgewichte für Label zurück, dessen Index übergeben wurde. Die Gewichte beziehen sich auf das trainierte Modell selbst und sind unabhängig von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine leere Liste zurückgegeben. Es handelt sich hier um lineare Modelle, so dass ein hoher positiver Wert bedeutet, dass das Wort stark zugunsten der Klassifizierung spricht. Negative Werte sprechen folgerichtig stark zuungunsten der Klassifizierung. Werte nahe oder gleich 0 sind für die Klassifizierung irrelevant.

Parameters:

Name Type Description Default
label_index int

Das Label, für das die Top-Wörter ausgegeben werden.

required
top_k (Optional, int)

Anzahl der Top-Wörter nach Gewichtung.

20
sign_filter (Optional, str)

Filtert nach Vorzeichen der Modellgewichte mit erlaubten Werten "pro", "contra", "all"

'all'

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert nach Betrag. Ersatzweise eine leere Liste.

Raises:

Type Description
ValueError
  • Wenn falscher Wert für 'valid_filters' übergeben wird
Source code in src\explanation\inspectors\linear_model_inspector.py
def inspect_model_weights_top_k(
    self, label_index: int, top_k: int = 20, sign_filter: str = "all"
) -> List[Tuple[str, float]]:
    """
    Gibt die Top-k Modellgewichte für Label zurück, dessen Index übergeben wurde.
    Die Gewichte beziehen sich auf das trainierte Modell selbst und sind unabhängig
    von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine
    leere Liste zurückgegeben.
    Es handelt sich hier um lineare Modelle, so dass ein hoher positiver Wert
    bedeutet, dass das Wort stark zugunsten der Klassifizierung spricht. Negative
    Werte sprechen folgerichtig stark zuungunsten der Klassifizierung. Werte nahe
    oder gleich 0 sind für die Klassifizierung irrelevant.

    Args:
        label_index (int): Das Label, für das die Top-Wörter ausgegeben werden.
        top_k (Optional, int): Anzahl der Top-Wörter nach Gewichtung.
        sign_filter (Optional, str): Filtert nach Vorzeichen der Modellgewichte
            mit erlaubten Werten "pro", "contra", "all"

    Returns:
        List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert
            nach Betrag. Ersatzweise eine leere Liste.

    Raises:
        ValueError:
            - Wenn falscher Wert für 'valid_filters' übergeben wird
    """
    valid_filters = {"pro", "contra", "all"}
    if sign_filter not in valid_filters:
        raise ValueError(
            f"[Er001] Ungültiger Wert für sign_filter: {sign_filter!r}. "
            f"Erlaubt sind: {', '.join(valid_filters)}."
        )

    weights = self.inspect_model_weights_by_index(label_index)
    # None-Werte rausfiltern
    weights = [item for item in weights if item[1] is not None]

    if sign_filter == "pro":
        filtered = [item for item in weights if item[1] > 0]
        sorted_weights = sorted(filtered, key=lambda x: x[1], reverse=True)
    elif sign_filter == "contra":
        filtered = [item for item in weights if item[1] < 0]
        sorted_weights = sorted(filtered, key=lambda x: x[1])
    else:
        # "all": vorzeichenunabhängig nach Betrag absteigend sortiert
        sorted_weights = sorted(weights, key=lambda x: abs(x[1]), reverse=True)


    return sorted_weights[:top_k]

src.explanation.inspectors.logarithmic_model_inspector

LogarithmicModelInspector

Bases: BaseInspector

Diese Klasse dient dazu, Informationen über logarithmisch konzipierte (Naive Bayes)- Modelle auszugeben. Sie stellt Methoden bereit, die die Wortgewichte für eine Textprediction ausgeben, aber auch die Biase und Wortgewichte, die für ein logarithmisches Modell unabhängig von einer einzelnen Textpädiktion gelten. Die Berechnungen legen das Vorhandensein des Attributs .feature_log_prob_ zugrunde. Der LogarithmicModelInspector wird erst instantiiert, nachdem (vom TextExplainer) geprüft wurde, ob das Modell dieses Attribut besitzt. Das Modell selbst wird als Bestandteil der Pipeline im TextPredictor via Constructor Injection injiziert. Abstrakte Elternklasse ist BaseInspector, der den Konstruktor sowie die erforderlichen Auswertungsmethoden vorgibt.

Author

irene

Source code in src\explanation\inspectors\logarithmic_model_inspector.py
class LogarithmicModelInspector(BaseInspector):
    """
    Diese Klasse dient dazu, Informationen über logarithmisch konzipierte (Naive Bayes)-
    Modelle auszugeben. Sie stellt Methoden bereit, die die Wortgewichte für eine
    Textprediction ausgeben, aber auch die Biase und Wortgewichte, die für ein
    logarithmisches Modell unabhängig von einer einzelnen Textpädiktion gelten.
    Die Berechnungen legen das Vorhandensein des Attributs .feature_log_prob_
    zugrunde. Der LogarithmicModelInspector wird erst instantiiert, nachdem (vom
    TextExplainer) geprüft wurde, ob das Modell dieses Attribut besitzt. Das Modell
    selbst wird als Bestandteil der Pipeline im TextPredictor via Constructor Injection
    injiziert.
    Abstrakte Elternklasse ist BaseInspector, der den Konstruktor sowie die
    erforderlichen Auswertungsmethoden vorgibt.

    Author:
        irene
    """
    def inspect_model_weights_by_index(
        self, label_index: int
    ) -> List[Tuple[str, Optional[float]]]:
        """
        Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche
        Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen
        sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions.
        Falls keine Gewichte berechnet werden können, wird None zurückgegeben.
        Auch bei ComplementNB wird nur None zurückgegeben, weil die Gewichte isoliert
        nicht sinnvoll interpretierbar sind.

        Args:
            label_index (int): Index des Labels, für das die gewichteten Wörter
                ausgegeben werden.

        Returns:
            List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel
        """
        # holt alle wörter aus dem Vectorizer
        words = self.vectorizer.get_feature_names_out()

        # Gewichte sind erstmal None, außer sie werden sinnvoll gefüllt
        weights = None

        # Für Naive Bayes Modelle: gibt logarithmierte bedingte Wahrscheinlichkeit
        weights = self.classifier.feature_log_prob_[label_index]

        # Konsistenzprüfung: Anzahl Wörter == Anzahl Gewichte?
        # Fallback auf None, falls keine gültigen Gewichte vorliegen oder die Daten
        # nicht konsistent sind
        if weights is None or len(words) != len(weights):
            weights = [None] * len(words)

        # weiteres Fallback für Typ CNB, weil Wortgewichte nicht interpretierbar
        #if type(self.classifier).__name__ == 'ComplementNB':
        #    weights = [None] * len(words)

        words_and_weights = list(zip(words, weights))

        return words_and_weights



    def inspect_model_weights_top_k(
        self, label_index: int, top_k: int = 20, sign_filter: str = "all"
    ) -> List[Tuple[str, float]]:
        """
        Gibt die Top-k Modellgewichte für Label zurück, dessen Index übergeben wurde.
        Die Gewichte beziehen sich auf das trainierte Modell selbst und sind unabhängig
        von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine
        leere Liste zurückgegeben.
        Es handelt sich hier um logarithmische Modelle, so dass ein Wert mit kleinem
        absoluten Betrag (also vorzeichenunabhängig) bedeutet, dass das Wort stark
        zugunsten der Klassifizierung spricht. Ein großer absoluter Betrag spricht
        somit stark gegen die Klassifizierung.
        MultinomialNB gibt ausschließlich negative Werte aus.
        CNB gibt keine unmittelbar interpretierbaren Werte aus, deshalb setze ich hier
        auf None.

        Args:
            label_index (int): Das Label, für das die Top-Wörter ausgegeben werden.
            top_k (Optional, int): Anzahl der Top-Wörter nach Gewichtung.
            sign_filter (Optional, str): Filtert die Modellgewichte mit erlaubten
                Werten "pro" (pro Label), "contra" (contra Label), "all"(default, wie
                pro)
        Returns:
            List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert
                nach Anweisung. Ersatzweise eine leere Liste.

        Raises:
            ValueError:
                - Wenn falscher Wert für 'valid_filters' übergeben wird
        """


        valid_filters = {"pro", "contra", "all"}
        if sign_filter not in valid_filters:
            raise ValueError(
                f"[Er001] Ungültiger Wert für sign_filter: {sign_filter!r}. "
                f"Erlaubt sind: {', '.join(valid_filters)}."
            )

        weights = self.inspect_model_weights_by_index(label_index)
        # None-Werte rausfiltern
        weights = [item for item in weights if item[1] is not None]

        # wir rechnen ausschließlich mit absoluten Werten, also ergibt sich der
        # # Filter rein durch die Sortierung:
        if sign_filter == "pro":
            # Werte nahe 0 sprechen stark für das Label, also asc sortieren
            sorted_weights = sorted(weights, key=lambda x: abs(x[1]))
        elif sign_filter == "contra":
            # hohe absolute Beträge sprechen stark gegen das Label, also desc
            sorted_weights = sorted(weights, key=lambda x: abs(x[1]), reverse=True)
        else:
            # "all": wie "pro", weil es keine klare pro/contra einteilung gibt
            sorted_weights = sorted(weights, key=lambda x: abs(x[1]))

        return sorted_weights[:top_k]

    def get_token_type(self) -> str:
        """
        Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf
        dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

        Returns:
            str: "raw" oder "processed"
        """
        return "processed"

    def compute_word_contributions(
        self,
        input_text: str,
        feature_values: np.ndarray,
        feature_names: list[str],
    ) -> Dict[str, Dict[str, float]]:
        """
        Berechnet den Einfluss einzelner Wörter auf die Klassifikation bei Naive-Bayes-
        Modellen, die mit logarithmischen Wahrscheinlichkeiten arbeiten. Verwendet den
        Unterschied zwischen Zielklasse und durchschnittlicher Klasse.

        Args:
            feature_values (np.ndarray): Vektorisierter Text als Zahlenarray.
            feature_names (list[str]): Namen der Merkmale (z.B. Wörter).
            input_text (str): Der ursprüngliche Text, der aber hier nicht verwendet
                wird (wird übergeben für die Einheitlichkeit der Signatur).

        Returns:
            Dict[label_name, Dict[word, contribution]] pro Label: Wörter und ihre
                Beiträge.
        """

        contributions_all = {}

        # Holt die logarithmierten Wahrscheinlichkeiten der Merkmale pro Klasse
        # Form: (Anzahl Klassen, Anzahl Merkmale)
        feature_log_prob = self.classifier.feature_log_prob_

        # Berechnet den Durchschnitt der logarithmierten Wahrscheinlichkeiten über alle
        # Klassen. Der Wert wird als Baseline genutzt, um relative Beiträge zu bestimmen
        baseline_log_prob = np.mean(feature_log_prob, axis=0)

        # Iteration über alle Klassenlabels (Index und Name)
        for label_index, label_name in enumerate(self.class_names):
            # Holt die logarithmierten Wahrscheinlichkeiten für die Zielklasse
            target_log_prob = feature_log_prob[label_index]

            # Berechnet die Differenz: Zielklasse - Baseline
            # Positive Werte, wenn das Wort für die Klassifizierung spricht
            diff_log_prob = target_log_prob - baseline_log_prob

            # Berechnet den Beitrag jedes Merkmals:
            # Merkmalswert * Unterschied in den Log-Wahrscheinlichkeiten
            contributions = feature_values * diff_log_prob

            # Dictionary zur Speicherung der Wortbeiträge für diese Klasse
            word_contributions = {}

            # Gehe alle Merkmale durch
            for i in range(len(contributions)):
                contribution_value = contributions[i]
                if contribution_value != 0:
                    word = feature_names[i]
                    word_contributions[word] = contribution_value

            # Speichere das Ergebnis für dieses Label
            contributions_all[label_name] = word_contributions

        # Rückgabe: Pro Klassenlabel ein Dictionary {Wort: Beitrag}
        return contributions_all

    def get_raw_biases_per_class(self) -> Dict[str, float]:
        """
        Gibt die rohen logarithmierten a-priori-Bias-Werte für jede Klasse zurück.

        Returns:
            Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
                und die Werte die logarithmierten a-priori-Bias-Werte als
                Fließkommazahlen sind.
        """
        raw_log_priors = self.classifier.class_log_prior_

        raw_biases_per_class = {
            class_name: float(raw_log_prior)
            for class_name, raw_log_prior in zip(self.class_names, raw_log_priors)
        }

        return raw_biases_per_class

    def get_prior_probs_per_class(self) -> Dict[str, float]:
        """
        Berechnet die a-priori-Wahrscheinlichkeiten für jede Klasse basierend auf den
        logarithmischen Bias-Werten.

        Returns:
            Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
            und die Werte die a-priori-Wahrscheinlichkeiten (zwischen 0 und 1) sind.
        """
        # log-Prio-Werte vom Klassifikator
        raw_log_priors = self.classifier.class_log_prior_
        # umgerechnet für wahrscheinlichkeiten
        prior_probs = np.exp(raw_log_priors)

        prior_probs_per_class = {
            class_name: float(prior_prob)
            for class_name, prior_prob in zip(self.class_names, prior_probs)
        }

        return prior_probs_per_class

compute_word_contributions(input_text, feature_values, feature_names)

Berechnet den Einfluss einzelner Wörter auf die Klassifikation bei Naive-Bayes- Modellen, die mit logarithmischen Wahrscheinlichkeiten arbeiten. Verwendet den Unterschied zwischen Zielklasse und durchschnittlicher Klasse.

Parameters:

Name Type Description Default
feature_values ndarray

Vektorisierter Text als Zahlenarray.

required
feature_names list[str]

Namen der Merkmale (z.B. Wörter).

required
input_text str

Der ursprüngliche Text, der aber hier nicht verwendet wird (wird übergeben für die Einheitlichkeit der Signatur).

required

Returns:

Type Description
Dict[str, Dict[str, float]]

Dict[label_name, Dict[word, contribution]] pro Label: Wörter und ihre Beiträge.

Source code in src\explanation\inspectors\logarithmic_model_inspector.py
def compute_word_contributions(
    self,
    input_text: str,
    feature_values: np.ndarray,
    feature_names: list[str],
) -> Dict[str, Dict[str, float]]:
    """
    Berechnet den Einfluss einzelner Wörter auf die Klassifikation bei Naive-Bayes-
    Modellen, die mit logarithmischen Wahrscheinlichkeiten arbeiten. Verwendet den
    Unterschied zwischen Zielklasse und durchschnittlicher Klasse.

    Args:
        feature_values (np.ndarray): Vektorisierter Text als Zahlenarray.
        feature_names (list[str]): Namen der Merkmale (z.B. Wörter).
        input_text (str): Der ursprüngliche Text, der aber hier nicht verwendet
            wird (wird übergeben für die Einheitlichkeit der Signatur).

    Returns:
        Dict[label_name, Dict[word, contribution]] pro Label: Wörter und ihre
            Beiträge.
    """

    contributions_all = {}

    # Holt die logarithmierten Wahrscheinlichkeiten der Merkmale pro Klasse
    # Form: (Anzahl Klassen, Anzahl Merkmale)
    feature_log_prob = self.classifier.feature_log_prob_

    # Berechnet den Durchschnitt der logarithmierten Wahrscheinlichkeiten über alle
    # Klassen. Der Wert wird als Baseline genutzt, um relative Beiträge zu bestimmen
    baseline_log_prob = np.mean(feature_log_prob, axis=0)

    # Iteration über alle Klassenlabels (Index und Name)
    for label_index, label_name in enumerate(self.class_names):
        # Holt die logarithmierten Wahrscheinlichkeiten für die Zielklasse
        target_log_prob = feature_log_prob[label_index]

        # Berechnet die Differenz: Zielklasse - Baseline
        # Positive Werte, wenn das Wort für die Klassifizierung spricht
        diff_log_prob = target_log_prob - baseline_log_prob

        # Berechnet den Beitrag jedes Merkmals:
        # Merkmalswert * Unterschied in den Log-Wahrscheinlichkeiten
        contributions = feature_values * diff_log_prob

        # Dictionary zur Speicherung der Wortbeiträge für diese Klasse
        word_contributions = {}

        # Gehe alle Merkmale durch
        for i in range(len(contributions)):
            contribution_value = contributions[i]
            if contribution_value != 0:
                word = feature_names[i]
                word_contributions[word] = contribution_value

        # Speichere das Ergebnis für dieses Label
        contributions_all[label_name] = word_contributions

    # Rückgabe: Pro Klassenlabel ein Dictionary {Wort: Beitrag}
    return contributions_all

get_prior_probs_per_class()

Berechnet die a-priori-Wahrscheinlichkeiten für jede Klasse basierend auf den logarithmischen Bias-Werten.

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen

Dict[str, float]

und die Werte die a-priori-Wahrscheinlichkeiten (zwischen 0 und 1) sind.

Source code in src\explanation\inspectors\logarithmic_model_inspector.py
def get_prior_probs_per_class(self) -> Dict[str, float]:
    """
    Berechnet die a-priori-Wahrscheinlichkeiten für jede Klasse basierend auf den
    logarithmischen Bias-Werten.

    Returns:
        Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
        und die Werte die a-priori-Wahrscheinlichkeiten (zwischen 0 und 1) sind.
    """
    # log-Prio-Werte vom Klassifikator
    raw_log_priors = self.classifier.class_log_prior_
    # umgerechnet für wahrscheinlichkeiten
    prior_probs = np.exp(raw_log_priors)

    prior_probs_per_class = {
        class_name: float(prior_prob)
        for class_name, prior_prob in zip(self.class_names, prior_probs)
    }

    return prior_probs_per_class

get_raw_biases_per_class()

Gibt die rohen logarithmierten a-priori-Bias-Werte für jede Klasse zurück.

Returns:

Type Description
Dict[str, float]

Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen und die Werte die logarithmierten a-priori-Bias-Werte als Fließkommazahlen sind.

Source code in src\explanation\inspectors\logarithmic_model_inspector.py
def get_raw_biases_per_class(self) -> Dict[str, float]:
    """
    Gibt die rohen logarithmierten a-priori-Bias-Werte für jede Klasse zurück.

    Returns:
        Dict[str, float]: Ein Dictionary, in dem die Schlüssel Klassennamen
            und die Werte die logarithmierten a-priori-Bias-Werte als
            Fließkommazahlen sind.
    """
    raw_log_priors = self.classifier.class_log_prior_

    raw_biases_per_class = {
        class_name: float(raw_log_prior)
        for class_name, raw_log_prior in zip(self.class_names, raw_log_priors)
    }

    return raw_biases_per_class

get_token_type()

Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

Returns:

Name Type Description
str str

"raw" oder "processed"

Source code in src\explanation\inspectors\logarithmic_model_inspector.py
def get_token_type(self) -> str:
    """
    Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf
    dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

    Returns:
        str: "raw" oder "processed"
    """
    return "processed"

inspect_model_weights_by_index(label_index)

Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions. Falls keine Gewichte berechnet werden können, wird None zurückgegeben. Auch bei ComplementNB wird nur None zurückgegeben, weil die Gewichte isoliert nicht sinnvoll interpretierbar sind.

Parameters:

Name Type Description Default
label_index int

Index des Labels, für das die gewichteten Wörter ausgegeben werden.

required

Returns:

Type Description
List[Tuple[str, Optional[float]]]

List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel

Source code in src\explanation\inspectors\logarithmic_model_inspector.py
def inspect_model_weights_by_index(
    self, label_index: int
) -> List[Tuple[str, Optional[float]]]:
    """
    Gibt alle Modellgewichte für ein bestimmtes Label zurück. Sie sagen, welche
    Wörter für die jeweiligen Labels wichtig sind. Die Gewichte beziehen
    sich auf das Modell selbst und sind Grundlage für die einzelnen Predictions.
    Falls keine Gewichte berechnet werden können, wird None zurückgegeben.
    Auch bei ComplementNB wird nur None zurückgegeben, weil die Gewichte isoliert
    nicht sinnvoll interpretierbar sind.

    Args:
        label_index (int): Index des Labels, für das die gewichteten Wörter
            ausgegeben werden.

    Returns:
        List[Tuple[str, float | None]]: Liste aller (Wort, Gewicht)-Tupel
    """
    # holt alle wörter aus dem Vectorizer
    words = self.vectorizer.get_feature_names_out()

    # Gewichte sind erstmal None, außer sie werden sinnvoll gefüllt
    weights = None

    # Für Naive Bayes Modelle: gibt logarithmierte bedingte Wahrscheinlichkeit
    weights = self.classifier.feature_log_prob_[label_index]

    # Konsistenzprüfung: Anzahl Wörter == Anzahl Gewichte?
    # Fallback auf None, falls keine gültigen Gewichte vorliegen oder die Daten
    # nicht konsistent sind
    if weights is None or len(words) != len(weights):
        weights = [None] * len(words)

    # weiteres Fallback für Typ CNB, weil Wortgewichte nicht interpretierbar
    #if type(self.classifier).__name__ == 'ComplementNB':
    #    weights = [None] * len(words)

    words_and_weights = list(zip(words, weights))

    return words_and_weights

inspect_model_weights_top_k(label_index, top_k=20, sign_filter='all')

Gibt die Top-k Modellgewichte für Label zurück, dessen Index übergeben wurde. Die Gewichte beziehen sich auf das trainierte Modell selbst und sind unabhängig von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine leere Liste zurückgegeben. Es handelt sich hier um logarithmische Modelle, so dass ein Wert mit kleinem absoluten Betrag (also vorzeichenunabhängig) bedeutet, dass das Wort stark zugunsten der Klassifizierung spricht. Ein großer absoluter Betrag spricht somit stark gegen die Klassifizierung. MultinomialNB gibt ausschließlich negative Werte aus. CNB gibt keine unmittelbar interpretierbaren Werte aus, deshalb setze ich hier auf None.

Parameters:

Name Type Description Default
label_index int

Das Label, für das die Top-Wörter ausgegeben werden.

required
top_k (Optional, int)

Anzahl der Top-Wörter nach Gewichtung.

20
sign_filter (Optional, str)

Filtert die Modellgewichte mit erlaubten Werten "pro" (pro Label), "contra" (contra Label), "all"(default, wie pro)

'all'

Returns: List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert nach Anweisung. Ersatzweise eine leere Liste.

Raises:

Type Description
ValueError
  • Wenn falscher Wert für 'valid_filters' übergeben wird
Source code in src\explanation\inspectors\logarithmic_model_inspector.py
def inspect_model_weights_top_k(
    self, label_index: int, top_k: int = 20, sign_filter: str = "all"
) -> List[Tuple[str, float]]:
    """
    Gibt die Top-k Modellgewichte für Label zurück, dessen Index übergeben wurde.
    Die Gewichte beziehen sich auf das trainierte Modell selbst und sind unabhängig
    von einzelnen Predictions. Falls das Modell keine Gewichte ausgibt, wird eine
    leere Liste zurückgegeben.
    Es handelt sich hier um logarithmische Modelle, so dass ein Wert mit kleinem
    absoluten Betrag (also vorzeichenunabhängig) bedeutet, dass das Wort stark
    zugunsten der Klassifizierung spricht. Ein großer absoluter Betrag spricht
    somit stark gegen die Klassifizierung.
    MultinomialNB gibt ausschließlich negative Werte aus.
    CNB gibt keine unmittelbar interpretierbaren Werte aus, deshalb setze ich hier
    auf None.

    Args:
        label_index (int): Das Label, für das die Top-Wörter ausgegeben werden.
        top_k (Optional, int): Anzahl der Top-Wörter nach Gewichtung.
        sign_filter (Optional, str): Filtert die Modellgewichte mit erlaubten
            Werten "pro" (pro Label), "contra" (contra Label), "all"(default, wie
            pro)
    Returns:
        List[Tuple[str, float]]: Liste der Top-k (Wort, Gewicht)-Tupel sortiert
            nach Anweisung. Ersatzweise eine leere Liste.

    Raises:
        ValueError:
            - Wenn falscher Wert für 'valid_filters' übergeben wird
    """


    valid_filters = {"pro", "contra", "all"}
    if sign_filter not in valid_filters:
        raise ValueError(
            f"[Er001] Ungültiger Wert für sign_filter: {sign_filter!r}. "
            f"Erlaubt sind: {', '.join(valid_filters)}."
        )

    weights = self.inspect_model_weights_by_index(label_index)
    # None-Werte rausfiltern
    weights = [item for item in weights if item[1] is not None]

    # wir rechnen ausschließlich mit absoluten Werten, also ergibt sich der
    # # Filter rein durch die Sortierung:
    if sign_filter == "pro":
        # Werte nahe 0 sprechen stark für das Label, also asc sortieren
        sorted_weights = sorted(weights, key=lambda x: abs(x[1]))
    elif sign_filter == "contra":
        # hohe absolute Beträge sprechen stark gegen das Label, also desc
        sorted_weights = sorted(weights, key=lambda x: abs(x[1]), reverse=True)
    else:
        # "all": wie "pro", weil es keine klare pro/contra einteilung gibt
        sorted_weights = sorted(weights, key=lambda x: abs(x[1]))

    return sorted_weights[:top_k]

src.explanation.inspectors.agnostic_inspector

AgnosticInspector

Bases: BaseInspector

Diese Klasse dient dazu, Informationen über diejenigen Modelle auszugeben, die nicht mittels des LinearModelInspector oder LogisticModelInspector erklärbar sind, insbesondere weil die Modelle die erforderlichen Attribute nicht zur Verfügung stellen. Der AgnosticInspector wird somit als Fallback genutzt. Er stellt Methoden bereit, die die Wortgewichte für eine Textprediction ausgeben. Er kann selbst keine Biase oder A-priori-Wahrscheinlichkeiten aus dem Modell extrahieren, die diesbezüglich geerbten Methoden geben None zurück. Der AgnosticInspector wird immer durch den TextExplainer instantiiert, somit ist gewährleistet, dass ein valider Vectorizer und Classifier vorhanden sind, mit denen der AgnosticInspector interagieren kann. Diese Komponenten werden als Bestandteil der Pipeline im TextPredictor via Constructor Injection injiziert. Abstrakte Elternklasse ist BaseInspector, der den Konstruktor sowie die erforderlichen Auswertungsmethoden vorgibt.

Author

irene

Source code in src\explanation\inspectors\agnostic_inspector.py
class AgnosticInspector(BaseInspector):
    """
    Diese Klasse dient dazu, Informationen über diejenigen Modelle auszugeben, die nicht
    mittels des LinearModelInspector oder LogisticModelInspector erklärbar sind,
    insbesondere weil die Modelle die erforderlichen Attribute nicht zur Verfügung
    stellen. Der AgnosticInspector wird somit als Fallback genutzt. Er stellt Methoden
    bereit, die die Wortgewichte für eine Textprediction ausgeben.
    Er kann selbst keine Biase oder A-priori-Wahrscheinlichkeiten aus dem Modell
    extrahieren, die diesbezüglich geerbten Methoden geben None zurück.
    Der AgnosticInspector wird immer durch den TextExplainer instantiiert, somit ist
    gewährleistet, dass ein valider Vectorizer und Classifier vorhanden sind, mit denen
    der AgnosticInspector interagieren kann. Diese Komponenten werden als Bestandteil
    der Pipeline im TextPredictor via Constructor Injection injiziert.
    Abstrakte Elternklasse ist BaseInspector, der den Konstruktor sowie die
    erforderlichen Auswertungsmethoden vorgibt.

    Author:
        irene
    """
    def __init__(self, predictor: TextPredictor):
        # initialisiert Eltern-Instanz
        super().__init__(predictor)

        # lädt Konfigurations-Infos (mittelbar aus xai_config.yaml)
        self._feature_selection = self.predictor.config["explainer"][
            "feature_selection"
        ]
        self._num_samples = self.predictor.config["explainer"]["num_samples"]
        self._explainer_method = self.predictor.config["explainer"]["method"]

        # Lime oder Shap (je nach Config) werden erst erzeugt, wenn gebraucht
        self._lime_explainer: Optional[LimeTextExplainer] = None
        self._shap_explainer: Optional[shap.Explainer] = None

    def _get_or_create_lime_explainer(self) -> LimeTextExplainer:
        """Erzeugt den LimeTextExplainer, falls noch nicht vorhanden."""
        if self._lime_explainer is None:
            self._lime_explainer = LimeTextExplainer(
                class_names=self.class_names, feature_selection=self._feature_selection
            )
        return self._lime_explainer

    def _create_lime_explanation(self, input_text: str) -> Explanation:
        """
        Erzeugt eine LIME-Erklärung für sämtliche Labels und verwendet automatisch die
        Wortanzahl als num_features.

        Args:
            input_text (str): zu erklärender Text

        Returns:
            Explanation: LIME-Erklärung
        """

        def classifier_for_lime_fn(texts: list[str]) -> np.ndarray:
            """erstellt die Klassifizierungsfunktion, hier ist auch pd.Series Pflicht"""
            texts_series = pd.Series(texts)
            return self.pipeline.predict_proba(texts_series)

        # erst bei Gebrauch wird der Lime-Explainer instantiiert
        explainer = self._get_or_create_lime_explainer()

        # Zähle Wörter im Input-Text als num_features:
        num_words = len(input_text.split())

        # jetzt wird die Klassifikationsentscheidung analysiert und erklärt
        explanation = explainer.explain_instance(
            text_instance=input_text,
            classifier_fn=classifier_for_lime_fn,
            num_features=num_words,
            top_labels=self.num_class_names,
            num_samples=self._num_samples,
        )

        return explanation

    def inspect_model_weights_by_index(
        self, label_index: int
    ) -> List[Tuple[str, Optional[float]]]:
        """
        LIME kann keine Modellgewichte ausgeben, deshalb werden hier alle Werte auf
        None gesetzt.
        Für die Zukunft kann man darüber nachdenken, hier SHAP zu implementieren. SHAP
        ist zur globalen Berechnung von Modellgewichten (important features) in der
        Lage, jedoch ist diese Berechnung vergleichsweise ressourcenintensiv, so dass
        ich es im Rahmen dieses Projekts bei dem Hinweis auf künftige
        Erweiterungsmöglichkeit belasse.

        Args:
            label_index (int): Der Label-Index.

        Returns:
            List[Tuple[str, Optional[float]]]: Liste von Tupeln bestehend aus
                Wörtern und Modellgewicht. Da LIME keine globalen Gewichte liefert, ist
                das Gewicht immer None.
        """
        words = self.vectorizer.get_feature_names_out()
        return [(w, None) for w in words]

    def inspect_model_weights_top_k(
        self, label_index: int, top_k: int = 20, sign_filter: str = "all"
    ) -> List[Tuple[str, float]]:
        """
        Meine Klasse kann noch keine globalen Modellgewichte ausgeben, deshalb wird hier
        lediglich die Signatur erfüllt.

        Args:
            label_index (int): Der Label-Index
            top_k (int, optional): Die Anzahl der auszugebenden Wörter. Defaults to 20.
            sign_filter (str, optional): Hier kann "positive", "negative", "all"
                (default) übergeben werden

        Returns:
            List[Tuple[str, float]]: Aktuell eine Liste von Wörtern mit None als
                Wortgewicht
        """
        # hier bekäme ich grundsätzlich meine Modellgewichte her, aktuell aber nur none
        model_weights = self.inspect_model_weights_by_index(label_index=label_index)

        # None-Werte rausfiltern (aktuell also alles)
        words_and_weights = [item for item in model_weights if item[1] is not None]

        # Falls künftig modellgewichte extrahiert werden können, kann hier eine
        # sinnvolle Sortierung und Filterung implementiert werden

        return words_and_weights[:top_k]

    def get_token_type(self) -> str:
        """
        Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf
        dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

        Returns:
            str: "raw" oder "processed"
        """
        return "raw"

    def compute_word_contributions(
        self,
        input_text: str,
        feature_values: np.ndarray,
        feature_names: list[str],
    ) -> Dict[str, Dict[str, float]]:
        """
        Erklärt ein beliebiges (Blackbox-)Modell mithilfe von LIME oder SHAP. Welches
        Modell die Erklärung übernimmt, wird aus der Konfigurationsdatei gelesen.
        Die Methode gibt die Gewichte aus, die die einzelnen Wörter des Eingabetextes
        zur Vorhersage beigetragen haben. Der Eingabetext wird roh verwendet, da für
        die Vorhersage die Decision-Function der kompletten Pipeline durchlaufen wird.


        Args:
            input_text (str): Der unbereinigte Text, der erklärt werden soll.
            feature_values: Wird hier nicht gebraucht, aber in den anderen Inspectoren.
            feature_names: Wird hier nicht gebraucht, aber in den anderen Inspectoren.

        Returns:
            Dict[str, float]: Wörter und ihre (nicht normalisierten) Beiträge.
        """

        if self._explainer_method == "lime":
            contributions_all = self.compute_word_contributions_lime(input_text)
        else:
            contributions_all = self.compute_word_contributions_shap(input_text)
        return contributions_all

    def compute_word_contributions_lime(
        self, input_text: str
    ) -> Dict[str, Dict[str, float]]:
        """
        Erklärt ein beliebiges (Blackbox-)Modell mithilfe von LIME. Bestimmt, welche
        Wörter am stärksten zur Vorhersage für eine bestimmte Klasse beitragen.
        Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function
        der kompletten Pipeline durchlaufen wird.

        Args:
            input_text (str): Der unbereinigte Text, der erklärt werden soll.

        Returns:
            Dict[str, float]: Wörter und ihre (nicht normalisierten) LIME-Beiträge.
        """
        # erzeugt Explanation-Objektinstanz aus LIME
        explanation = self._create_lime_explanation(input_text=input_text)

        # sammelt für alle verfügbaren Label die word_contributions
        contributions_all = {}

        # durchläuft jede in Modell enthaltene Klasse die Wortbeiträge
        for label_id in explanation.available_labels():
            # Holt Liste der Wortbeiträge für das gegebene Label (Klasse)
            contributions = explanation.as_list(label=label_id)
            # Erstellt ein Wörterbuch, das jedes Wort seinem Beitrag zuordnet
            contributions_for_label = {}
            for word, score in contributions:
                # es sind nur Wörter relevant, deren Beitrag ungleich 0 ist
                if score != 0:
                    contributions_for_label[str(word)] = float(score)

            # Ordnet die Wortbeiträge dieser Klasse (über den Klassennamen) zu
            class_name = self.class_names[label_id]
            contributions_all[class_name] = contributions_for_label

        return contributions_all

    def get_raw_biases_per_class(self) -> Dict[str, Optional[float]]:
        """
        Der agnostische Explainer kann keinen echten Bias zurückgeben, sondern würde ihn
        lediglich aus den Predictionswahrscheinlichkeiten herleiten.
        Diese Funktion gibt deshalb lediglich None-Werte zurück.

        Returns:
            Dict[str, Optional[float]]: Für jedes Label: None als Platzhalter
        """
        return dict.fromkeys(self.class_names)

    def get_prior_probs_per_class(self) -> Dict[str, Optional[float]]:
        """
        Der agnostische Explainer kann keinen echten Bias zurückgeben, sondern würde ihn
        lediglich auf einem Leerstring approximieren. Dies tu ich jedoch schon
        in der TextExplainer-Klasse und brauche den zusätzlichen Aufwand nicht.
        Die Funktion gibt deshalb lediglich None-Werte zurück.

        Returns:
            Dict[str, Optional[float]]: Für jedes Label: None als Platzhalter
        """
        return dict.fromkeys(self.class_names)

    def _get_or_create_shap_explainer(self) -> shap.Explainer:
        """
        Erstellt oder gibt eine vorhandene SHAP-Explainer-Instanz zurück.

        Diese Methode erzeugt bei erstmaligem Aufruf einen SHAP-Explainer für Textdaten
        mit dem internen Pipeline-Modell und einem Text-Masker. Bei wiederholtem Aufruf
        wird die bereits erstellte Instanz zurückgegeben, um Rechenzeit zu sparen.

        Returns:
            shap.Explainer: Eine SHAP-Explainer-Instanz, die Vorhersagen des Modells
                für Textdaten erklären kann.
        """
        if self._shap_explainer is None:
            # "masker" ist für Text die SHAP-eigene TextMasker-Klasse
            masker = shap.maskers.Text()

            self._shap_explainer = shap.Explainer(
                self.pipeline.predict_proba,
                masker,
            )
        return self._shap_explainer

    def compute_word_contributions_shap(
        self, input_text: str
    ) -> Dict[str, Dict[str, float]]:
        """
        Erklärt ein beliebiges (Blackbox-)Modell mithilfe von SHAP. Bestimmt, welche
        Wörter am stärksten zur Vorhersage für eine bestimmte Klasse beitragen.
        Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function
        der kompletten Pipeline durchlaufen wird.

        Args:
            input_text (str): Der ggf. bereinigte Text, der erklärt werden soll.

        Args:
            input_text (str): Der unbereinigte Text, der erklärt werden soll.

        Returns:
            Dict[str, Dict[str, float]]: Wörter (Features) und ihre SHAP-Beiträge pro
                Klasse.
        """
        # Explainer erzeugen (z. B. mit masker = vectorizer)
        explainer = self._get_or_create_shap_explainer()
        # SHAP erwartet eine Liste von Texten
        input_text_list = [input_text]
        # Berechne die SHAP-Erklärung für den gegebenen Text
        explanation = explainer(input_text_list)
        # Hole die tokenisierten Eingabedaten (Liste der Tokens für das erste Dokument)
        tokens = explanation.data[0]

        # Dictionary speichert für jede Klasse alle Wortbeiträge
        contributions_all = {}

        # Für jede vorhergesagte Klasse die SHAP-Werte sammeln
        for class_idx, class_name in enumerate(self.class_names):
            # SHAP-Werte für alle Tokens dieser Klasse extrahieren
            shap_values_for_class = explanation.values[0][:, class_idx]

            # Erstelle ein Wörterbuch, das jedes Wort seinem Beitrag zuordnet
            contributions_for_class = {}
            for token, shap_value in zip(tokens, shap_values_for_class):
                # Nur Tokens mit einem nicht-null SHAP-Wert berücksichtigen
                if shap_value != 0:
                    contributions_for_class[token] = float(shap_value)

            # Ergebnisse dieser Klasse speichern
            contributions_all[class_name] = contributions_for_class

        return contributions_all

compute_word_contributions(input_text, feature_values, feature_names)

Erklärt ein beliebiges (Blackbox-)Modell mithilfe von LIME oder SHAP. Welches Modell die Erklärung übernimmt, wird aus der Konfigurationsdatei gelesen. Die Methode gibt die Gewichte aus, die die einzelnen Wörter des Eingabetextes zur Vorhersage beigetragen haben. Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function der kompletten Pipeline durchlaufen wird.

Parameters:

Name Type Description Default
input_text str

Der unbereinigte Text, der erklärt werden soll.

required
feature_values ndarray

Wird hier nicht gebraucht, aber in den anderen Inspectoren.

required
feature_names list[str]

Wird hier nicht gebraucht, aber in den anderen Inspectoren.

required

Returns:

Type Description
Dict[str, Dict[str, float]]

Dict[str, float]: Wörter und ihre (nicht normalisierten) Beiträge.

Source code in src\explanation\inspectors\agnostic_inspector.py
def compute_word_contributions(
    self,
    input_text: str,
    feature_values: np.ndarray,
    feature_names: list[str],
) -> Dict[str, Dict[str, float]]:
    """
    Erklärt ein beliebiges (Blackbox-)Modell mithilfe von LIME oder SHAP. Welches
    Modell die Erklärung übernimmt, wird aus der Konfigurationsdatei gelesen.
    Die Methode gibt die Gewichte aus, die die einzelnen Wörter des Eingabetextes
    zur Vorhersage beigetragen haben. Der Eingabetext wird roh verwendet, da für
    die Vorhersage die Decision-Function der kompletten Pipeline durchlaufen wird.


    Args:
        input_text (str): Der unbereinigte Text, der erklärt werden soll.
        feature_values: Wird hier nicht gebraucht, aber in den anderen Inspectoren.
        feature_names: Wird hier nicht gebraucht, aber in den anderen Inspectoren.

    Returns:
        Dict[str, float]: Wörter und ihre (nicht normalisierten) Beiträge.
    """

    if self._explainer_method == "lime":
        contributions_all = self.compute_word_contributions_lime(input_text)
    else:
        contributions_all = self.compute_word_contributions_shap(input_text)
    return contributions_all

compute_word_contributions_lime(input_text)

Erklärt ein beliebiges (Blackbox-)Modell mithilfe von LIME. Bestimmt, welche Wörter am stärksten zur Vorhersage für eine bestimmte Klasse beitragen. Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function der kompletten Pipeline durchlaufen wird.

Parameters:

Name Type Description Default
input_text str

Der unbereinigte Text, der erklärt werden soll.

required

Returns:

Type Description
Dict[str, Dict[str, float]]

Dict[str, float]: Wörter und ihre (nicht normalisierten) LIME-Beiträge.

Source code in src\explanation\inspectors\agnostic_inspector.py
def compute_word_contributions_lime(
    self, input_text: str
) -> Dict[str, Dict[str, float]]:
    """
    Erklärt ein beliebiges (Blackbox-)Modell mithilfe von LIME. Bestimmt, welche
    Wörter am stärksten zur Vorhersage für eine bestimmte Klasse beitragen.
    Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function
    der kompletten Pipeline durchlaufen wird.

    Args:
        input_text (str): Der unbereinigte Text, der erklärt werden soll.

    Returns:
        Dict[str, float]: Wörter und ihre (nicht normalisierten) LIME-Beiträge.
    """
    # erzeugt Explanation-Objektinstanz aus LIME
    explanation = self._create_lime_explanation(input_text=input_text)

    # sammelt für alle verfügbaren Label die word_contributions
    contributions_all = {}

    # durchläuft jede in Modell enthaltene Klasse die Wortbeiträge
    for label_id in explanation.available_labels():
        # Holt Liste der Wortbeiträge für das gegebene Label (Klasse)
        contributions = explanation.as_list(label=label_id)
        # Erstellt ein Wörterbuch, das jedes Wort seinem Beitrag zuordnet
        contributions_for_label = {}
        for word, score in contributions:
            # es sind nur Wörter relevant, deren Beitrag ungleich 0 ist
            if score != 0:
                contributions_for_label[str(word)] = float(score)

        # Ordnet die Wortbeiträge dieser Klasse (über den Klassennamen) zu
        class_name = self.class_names[label_id]
        contributions_all[class_name] = contributions_for_label

    return contributions_all

compute_word_contributions_shap(input_text)

Erklärt ein beliebiges (Blackbox-)Modell mithilfe von SHAP. Bestimmt, welche Wörter am stärksten zur Vorhersage für eine bestimmte Klasse beitragen. Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function der kompletten Pipeline durchlaufen wird.

Parameters:

Name Type Description Default
input_text str

Der ggf. bereinigte Text, der erklärt werden soll.

required

Parameters:

Name Type Description Default
input_text str

Der unbereinigte Text, der erklärt werden soll.

required

Returns:

Type Description
Dict[str, Dict[str, float]]

Dict[str, Dict[str, float]]: Wörter (Features) und ihre SHAP-Beiträge pro Klasse.

Source code in src\explanation\inspectors\agnostic_inspector.py
def compute_word_contributions_shap(
    self, input_text: str
) -> Dict[str, Dict[str, float]]:
    """
    Erklärt ein beliebiges (Blackbox-)Modell mithilfe von SHAP. Bestimmt, welche
    Wörter am stärksten zur Vorhersage für eine bestimmte Klasse beitragen.
    Der Eingabetext wird roh verwendet, da für die Vorhersage die Decision-Function
    der kompletten Pipeline durchlaufen wird.

    Args:
        input_text (str): Der ggf. bereinigte Text, der erklärt werden soll.

    Args:
        input_text (str): Der unbereinigte Text, der erklärt werden soll.

    Returns:
        Dict[str, Dict[str, float]]: Wörter (Features) und ihre SHAP-Beiträge pro
            Klasse.
    """
    # Explainer erzeugen (z. B. mit masker = vectorizer)
    explainer = self._get_or_create_shap_explainer()
    # SHAP erwartet eine Liste von Texten
    input_text_list = [input_text]
    # Berechne die SHAP-Erklärung für den gegebenen Text
    explanation = explainer(input_text_list)
    # Hole die tokenisierten Eingabedaten (Liste der Tokens für das erste Dokument)
    tokens = explanation.data[0]

    # Dictionary speichert für jede Klasse alle Wortbeiträge
    contributions_all = {}

    # Für jede vorhergesagte Klasse die SHAP-Werte sammeln
    for class_idx, class_name in enumerate(self.class_names):
        # SHAP-Werte für alle Tokens dieser Klasse extrahieren
        shap_values_for_class = explanation.values[0][:, class_idx]

        # Erstelle ein Wörterbuch, das jedes Wort seinem Beitrag zuordnet
        contributions_for_class = {}
        for token, shap_value in zip(tokens, shap_values_for_class):
            # Nur Tokens mit einem nicht-null SHAP-Wert berücksichtigen
            if shap_value != 0:
                contributions_for_class[token] = float(shap_value)

        # Ergebnisse dieser Klasse speichern
        contributions_all[class_name] = contributions_for_class

    return contributions_all

get_prior_probs_per_class()

Der agnostische Explainer kann keinen echten Bias zurückgeben, sondern würde ihn lediglich auf einem Leerstring approximieren. Dies tu ich jedoch schon in der TextExplainer-Klasse und brauche den zusätzlichen Aufwand nicht. Die Funktion gibt deshalb lediglich None-Werte zurück.

Returns:

Type Description
Dict[str, Optional[float]]

Dict[str, Optional[float]]: Für jedes Label: None als Platzhalter

Source code in src\explanation\inspectors\agnostic_inspector.py
def get_prior_probs_per_class(self) -> Dict[str, Optional[float]]:
    """
    Der agnostische Explainer kann keinen echten Bias zurückgeben, sondern würde ihn
    lediglich auf einem Leerstring approximieren. Dies tu ich jedoch schon
    in der TextExplainer-Klasse und brauche den zusätzlichen Aufwand nicht.
    Die Funktion gibt deshalb lediglich None-Werte zurück.

    Returns:
        Dict[str, Optional[float]]: Für jedes Label: None als Platzhalter
    """
    return dict.fromkeys(self.class_names)

get_raw_biases_per_class()

Der agnostische Explainer kann keinen echten Bias zurückgeben, sondern würde ihn lediglich aus den Predictionswahrscheinlichkeiten herleiten. Diese Funktion gibt deshalb lediglich None-Werte zurück.

Returns:

Type Description
Dict[str, Optional[float]]

Dict[str, Optional[float]]: Für jedes Label: None als Platzhalter

Source code in src\explanation\inspectors\agnostic_inspector.py
def get_raw_biases_per_class(self) -> Dict[str, Optional[float]]:
    """
    Der agnostische Explainer kann keinen echten Bias zurückgeben, sondern würde ihn
    lediglich aus den Predictionswahrscheinlichkeiten herleiten.
    Diese Funktion gibt deshalb lediglich None-Werte zurück.

    Returns:
        Dict[str, Optional[float]]: Für jedes Label: None als Platzhalter
    """
    return dict.fromkeys(self.class_names)

get_token_type()

Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

Returns:

Name Type Description
str str

"raw" oder "processed"

Source code in src\explanation\inspectors\agnostic_inspector.py
def get_token_type(self) -> str:
    """
    Gibt entweder "raw" oder "processed" aus, je nach dem, ob die Wortbeiträge auf
    dem Rohtext oder auf dem vorverarbeiteten Text ermittelt werden.

    Returns:
        str: "raw" oder "processed"
    """
    return "raw"

inspect_model_weights_by_index(label_index)

LIME kann keine Modellgewichte ausgeben, deshalb werden hier alle Werte auf None gesetzt. Für die Zukunft kann man darüber nachdenken, hier SHAP zu implementieren. SHAP ist zur globalen Berechnung von Modellgewichten (important features) in der Lage, jedoch ist diese Berechnung vergleichsweise ressourcenintensiv, so dass ich es im Rahmen dieses Projekts bei dem Hinweis auf künftige Erweiterungsmöglichkeit belasse.

Parameters:

Name Type Description Default
label_index int

Der Label-Index.

required

Returns:

Type Description
List[Tuple[str, Optional[float]]]

List[Tuple[str, Optional[float]]]: Liste von Tupeln bestehend aus Wörtern und Modellgewicht. Da LIME keine globalen Gewichte liefert, ist das Gewicht immer None.

Source code in src\explanation\inspectors\agnostic_inspector.py
def inspect_model_weights_by_index(
    self, label_index: int
) -> List[Tuple[str, Optional[float]]]:
    """
    LIME kann keine Modellgewichte ausgeben, deshalb werden hier alle Werte auf
    None gesetzt.
    Für die Zukunft kann man darüber nachdenken, hier SHAP zu implementieren. SHAP
    ist zur globalen Berechnung von Modellgewichten (important features) in der
    Lage, jedoch ist diese Berechnung vergleichsweise ressourcenintensiv, so dass
    ich es im Rahmen dieses Projekts bei dem Hinweis auf künftige
    Erweiterungsmöglichkeit belasse.

    Args:
        label_index (int): Der Label-Index.

    Returns:
        List[Tuple[str, Optional[float]]]: Liste von Tupeln bestehend aus
            Wörtern und Modellgewicht. Da LIME keine globalen Gewichte liefert, ist
            das Gewicht immer None.
    """
    words = self.vectorizer.get_feature_names_out()
    return [(w, None) for w in words]

inspect_model_weights_top_k(label_index, top_k=20, sign_filter='all')

Meine Klasse kann noch keine globalen Modellgewichte ausgeben, deshalb wird hier lediglich die Signatur erfüllt.

Parameters:

Name Type Description Default
label_index int

Der Label-Index

required
top_k int

Die Anzahl der auszugebenden Wörter. Defaults to 20.

20
sign_filter str

Hier kann "positive", "negative", "all" (default) übergeben werden

'all'

Returns:

Type Description
List[Tuple[str, float]]

List[Tuple[str, float]]: Aktuell eine Liste von Wörtern mit None als Wortgewicht

Source code in src\explanation\inspectors\agnostic_inspector.py
def inspect_model_weights_top_k(
    self, label_index: int, top_k: int = 20, sign_filter: str = "all"
) -> List[Tuple[str, float]]:
    """
    Meine Klasse kann noch keine globalen Modellgewichte ausgeben, deshalb wird hier
    lediglich die Signatur erfüllt.

    Args:
        label_index (int): Der Label-Index
        top_k (int, optional): Die Anzahl der auszugebenden Wörter. Defaults to 20.
        sign_filter (str, optional): Hier kann "positive", "negative", "all"
            (default) übergeben werden

    Returns:
        List[Tuple[str, float]]: Aktuell eine Liste von Wörtern mit None als
            Wortgewicht
    """
    # hier bekäme ich grundsätzlich meine Modellgewichte her, aktuell aber nur none
    model_weights = self.inspect_model_weights_by_index(label_index=label_index)

    # None-Werte rausfiltern (aktuell also alles)
    words_and_weights = [item for item in model_weights if item[1] is not None]

    # Falls künftig modellgewichte extrahiert werden können, kann hier eine
    # sinnvolle Sortierung und Filterung implementiert werden

    return words_and_weights[:top_k]