Wie vergleicht man Textdokumente?
von Konstantina Lazaridou, Machine Learning Engineer
Die Arbeit mit Texten kann sehr viel Spaß machen, manchmal aber auch mühsam sein. Schriftliche Dokumente (entweder von Menschen oder von KI-Modellen) können in ihrer Struktur komplex sein, sie können kreative oder einzigartige Sprache und sogar Fehler enthalten. Die automatische Analyse eines Textes kann Zeit sparen. So können wir beispielsweise problemlos Nachrichtenartikel zu einem bestimmten Thema von verschiedenen Herausgebern vergleichen und verstehen, wie sich der ursprüngliche Inhalt im Laufe der Zeit von einer Medienquelle zur anderen verändert hat.
Dieser Beitrag ist ein Tutorial für den Vergleich von Texten (Strings) mit den Python-Librarys difflib und fuzzywuzzy.
Wir beginnen mit einem Showcase von difflib, der die Unterschiede (Deltas) zwischen zwei Sequenzen von Elementen berechnet. Diese Bibliothek kann auch für andere Datentypen verwendet werden, z. B. für Dateien.
from typing import List, Dict
import difflib
def get_changes_in_text(first_text: str, second_text: str) -> List[Dict]:
"""Compare the first with the second text and return the list of fine-grained changes.
The list contains one dictionary for every change.
The first text could be restored by applying the listed changes in the respective indices.
Args:
first_text (str): First sentence or document
second_text (str): Second sentence or document
Returns:
List[Dict]: List of dicts, where each dict describes a change: {start index, end index, token, diff}.
The diff is either an addition (+) or a deletion (-).
"""
changes = []
current_token = ""
start_index = 0
current_index = 0
current_diff = ""
# iterate over the descriptions of all differences between the first text and the second one
for i, s in enumerate(difflib.ndiff(first_text, second_text)):
if s[0] != " ":
print(f"i={i}, s={s}")
# if there is a diff indicated in `s[2]` and we are not still processing the current change
if s[2] != " " and (not current_diff or current_diff == s[0]):
current_token += s[2]
# newly seen change
if current_index == 0:
start_index = i
current_index = i
# consequent character changes
else:
current_index += 1
current_diff = s[0]
# if the description of the current change is complete, store it with spacy-like format and reset
elif s[2] == " ":
# there is added text to the input
if current_diff == "+":
changes.append(
{
"start_index": start_index,
"end_index": current_index,
"token": current_token,
"diff": "+",
}
)
# there is removed text from the input
elif current_diff == "-":
changes.append(
{
"start_index": start_index,
"end_index": current_index,
"token": current_token,
"diff": "-",
}
)
current_token = ""
start_index = 0
current_index = 0
current_diff = ""
# if the description of the current change is complete and a new description starts, store current one and move on to next one
elif current_diff != s[0]:
if current_diff == "+":
changes.append(
{
"start_index": start_index,
"end_index": current_index,
"token": current_token,
"diff": "+",
}
)
elif current_diff == "-":
changes.append(
{
"start_index": start_index,
"end_index": current_index,
"token": current_token,
"diff": "-",
}
)
current_token = s[2]
start_index = i
current_index = i
current_diff = s[0]
return changes
Der obige Codeschnipsel definiert die Funktion get_changes_in_text(), die alle Delta-Informationen zwischen zwei Inputtextenin einer Liste von Python-Dictionarys sammelt. Jedes Wörterbuch repräsentiert ein Delta und folgt einem spaCy-ähnlichen Format (dem bekannten Tool zur Verarbeitung natürlicher Sprache), um genau anzugeben, wo die Spanne des Deltas beginnt und endet.
Im Detail bedeutet das, dass wir zunächst die Funktion ndiff aus difflib aufrufen, welche die Deltas zwischen zwei Listen von Texten berechnet – in unserem vereinfachten Anwendungsfall vergleichen wir zwei einzelne Texte. Jedes zurückgegebeneDelta verarbeiten wir, um es in unser gewünschtes Format zu bringen, und führen dann alle Deltas zusammen. Zur Verdeutlichung, hier ein Funktionsaufruf von get_changes_in_text() mit einem einfachen Beispiel:
get_changes_in_text(
"“This is a good decision,” said Robert Habeck, the German vice-chancellor and economy minister",
"“This is not a good decision,” said Robert Habeck, the German vice-chancellor and economy minister.",
)
Die Ausgabe der Funktion ist unten zu sehen. Beim Vergleich der beiden Sätze, zeigt uns die Ausgabe des Skripts, dass wir an Position 9 des Textes das Wort „not“ (von Position 9 bis Position 11), ein Leerzeichen vor diesem neuen Wort und schließlich einen Punkt am Ende des Satzes eingefügt haben. Zu beachten ist, dass Positionen (Indizes) im Wesentlichen Zeichenpositionen im Text sind, wenn wir davon ausgehen, dass der Text eine geordnete Folge von Zeichen ist.
i=8, s=+
i=9, s=+ n
i=10, s=+ o
i=11, s=+ t
i=98, s=+ .
Im nächsten Beispiel sehen wir einen komplexeren Fall, bei dem zwei Wörter wegfallen („sollen“ und „werden“) und zwei Wörter in den Text eingefügt werden („werden“ und „Anfang“), zusammen mit einigen Änderungen der Interpunktion:
get_changes_in_text(
"Die Maßnahmen sollen Mitte Februar überprüft werden",
"Die Maßnahmen werden Anfang Februar überprüft.",
)
i=14, s=- s
i=15, s=- o
i=16, s=- l
i=17, s=- l
i=18, s=+ w
i=19, s=+ e
i=20, s=+ r
i=21, s=+ d
i=25, s=- M
i=26, s=- i
i=27, s=- t
i=28, s=- t
i=29, s=- e
i=30, s=+ A
i=31, s=+ n
i=32, s=+ f
i=33, s=+ a
i=34, s=+ n
i=35, s=+ g
i=54, s=+ .
i=55, s=-
i=56, s=- w
i=57, s=- e
i=58, s=- r
i=59, s=- d
i=60, s=- e
i=61, s=- n
Daher kann get_changes_in_text() angewendet werden, um den Unterschied in der Wortwahl zwischen zwei oder mehr Medien zu finden und außerdem, wo genau sich diese Änderungen im Text befinden.
Ein weiteres Python-Package, das zum Vergleich von Texten auf nicht strikte Weise verwendet werden kann, ist fuzzywuzzy, auch bekannt als TheFuzz und verwendet von RapidFuzz. Diese Bibliothek vergleicht zwei Texte mit der Levenshtein-Distanz (die Anzahl der einzelnen Zeichen-Änderungen, die notwendig sind, um einen String in einen andere umzuwandeln) und bietet außerdem mehrere Möglichkeiten, Texte auf Wort-Ebene (Token-Ebene) zu vergleichen. Das folgende Beispiel verwendet eine der einfacheren Funktionen, ratio(), und berechnet die Ähnlichkeit zwischen den beiden Sätzen. Es wird eine Ähnlichkeit von 97 % ausgegeben. Dies stimmt mit dem überein, was wir oben für dasselbe Satzpaar gesehen haben, d. h. der einzige kleine Unterschied zwischen den Sätzen ist das Wort „not“ und die Zeichensetzung.
fuzz.ratio(
"“This is a good decision,” said Robert Habeck, the German vice-chancellor and economy minister",
"“This is not a good decision,” said Robert Habeck, the German vice-chancellor and economy minister.",
)
Eine weitere Funktion von fuzzywuzzy ist partial_ratio(), die den ähnlichsten Substring zwischen zwei gegebenen Texten verwendet, um deren Ähnlichkeit zu berechnen. Diese Funktion kann nützlich sein, wenn wir zwei kurze Texte auf eine weniger restriktive Art und Weise vergleichen wollen, z. B. wenn sich zwei Artikel auf denselben Sachverhalt beziehen, auch wenn sie vielleicht nicht genau dieselben Worte verwenden, um sie zu beschreiben. Wenn wir beispielsweise die beiden folgenden Fakten mit der Funktion ratio() vergleichen, erhalten wir eine Ähnlichkeit von 86 %, aber wenn wir partial_ratio() verwenden, erreichen wir eine Ähnlichkeit von 98 %, was eher zu unserem Anwendungsfall passt. Würde der zweite Text das zusätzliche Zitat am Anfang nicht enthalten, läge die Ähnlichkeit bei 100 %, da einer der beiden Texte praktisch im anderen enthalten ist.
print(
fuzz.partial_ratio(
"“Robert Habeck, the German vice-chancellor and economy minister",
"“the German vice-chancellor and economy minister",
)
)
Fuzzywuzzy verfügt über weitere Optionen für den Vergleich von Texten auf der Grundlage ihrer Wortunterschiede, z. B. verwendet token_set_ratio() auch ratio(), um die Ähnlichkeit nach der Tokenisierung der Texte zu berechnen, und es gibt auch die teilweise Übereinstimmung mit partial_token_sort_ratio() sowie die sortierte Version token_sort_ratio(). Wenn die Reihenfolge der Wörter für eine bestimmte Anwendung keine Rolle spielt, können die Sortiermethoden geeignet sein. Insbesondere token_sort_ratio() kann hohe Ähnlichkeitswerte für Strings liefern, die sicher sehr ähnliche sind, aber deren Wörtern in unterschiedlicher Reihenfolge stehen. Darüber hinaus enthalten die tokenbasierten Methoden auch eine einfache Vorverarbeitung der Strings, bei der alles außer Buchstaben und Zahlen entfernt und kleingeschrieben wird.
Fazit
In diesem Beitrag wurden zwei Python-Librarys für den Vergleich von Strings vorgestellt. Der vorgestellte Code kann für jede Art von Text verwendet werden, z. B. für fiktionale Texte oder Sachartikel. Zudem kann es für verschiedene Anwendungen hilfreich sein, wie z. B. das Auffinden von Unterschieden im Schreibstil zweier Autoren oder die Entdeckung neuer Fakten, die einer Original-Geschichte hinzugefügt wurden. Insbesondere difflib kann für die data lineage und Versionierung nützlich sein, und fuzzywuzzy eignet sich hervorragend für den Datenabgleich und die Deduplizierung.
Eine Herausforderung, die es zu berücksichtigen und zu untersuchen gilt, ist die Frage, ob die Anwendung mit Dokumenten arbeitet, die verschiedene Alphabete enthalten, und ob diese Sprachen somit von den Librarys unterstützt werden oder nicht. Zu guter Letzt kann bei einer großen Datensammlung die Geschwindigkeit des String-Vergleichs erhöht werden, indem die Python-Levenshtein-Library installiert und zusammen mit fuzzywuzzy verwendet wird, oder alternativ kann die oben erwähnte Library RapidFuzz eingesetzt werden, die auf fuzzywuzzy basiert und eine höhere Leistung aufweist.
Artikel wurde aus dem englischen Original übersetzt.