Source Code für das das Masterarbeitsprojekt einer semantischen Suche für das Gesamtwerk von Immanuel Kant. Das Projekt basiert auf der online Ausgabe des Gesamtwerks von Immanuel Kant. Vielen Dank für die Bereitstellung der Daten!
Die Webseite mit der implementierten Suche ist unter folgendem Link zu finden: in-medias-res.wagnerhof.net
Enthalten sind:
Dateien mit Daten und Skripten zum Training und Vorbereiten der Daten.
Die Ergebnisse der Auswertung. Nähere Beschreibung dazu unter Auswertung.
Aus Lizenzrechtlichen Gründen können die Daten nicht veröffentlicht werden, jedoch bei den Rechteinhabern angefragt werden.
Bereits berechnete Vektoren getrennt nach Modellen für alle Bände. Erstellt durch Vektoren-Test.py
Es wurden folgende Modelle lokal installiert:
Dieses HowTo richtet sich vor allem an Neulinge der Natürlichen Sprachverarbeitung. Für Fortgeschrittenere ist wohl eher die Auswertung der Ergebnisse relevant, bzw. wie das Verfahren Implementiert wurde. Auf Feinheiten der Implementierung kann aufgrund des Platzes nicht in vollem Umfang eingegangen werden. Dazu gibt es auf der Webseite weitere Informationen.
Dieses Repositorium stellt alle Handwerkszeuge für die Aufbereitung des Korpus, das Training der Modelle und auch ein kleines Programm zur Durchführung einer Suche in dem Korpus dar. Es wird kurz auf die Einrichtung der Programmierumgebung eingegangen und enthält eine niederschwellige Schritt-für-Schritt-Anleitung, was jeweils gemacht wird (mit Hinweisen, wie der Code zur Anwendung an eigene Daten angepasst werden sollte). Eine allgemeine Einführung in das Projekt gibt es bei der überspannenden Organisation, oder auch auf der Webseite.
Grundsätzlich wurde versucht die Implementierung der Suche möglicht so zu gestalten, dass zumindest einzelne Funktionen oder vielleicht sogar ganze Programmteile auch für andere Projekte nutzbar sind. Jedoch ist jeder Korpus anders, sodass vor allem die Aufbereitung dessen nicht verallgemeinbar ist. Umso wichtiger ist die Dokumentation, in welcher Form die Daten vorliegen müssen um mit den restlichen Funktionen zusammen arbeiten zu können. So soll es möglich sein bereits aufbereitete Modelle zum Beispiel einfach zu benutzen um die Modelle zu trainieren, oder mit den eigenen trainierten Modellen oder auch Modellen anderer Institutionen darin zu suchen. Das eigene Modell mit den hier genutzten Daten zu trainieren ist leider momentan nicht möglich, wenden Sie sich dazu gerne an die Herausgeber des online-Kantkorpus.
Je nach Vorhaben kann direkt zu den entsprechenden Kapiteln gesprungen werden:
Für die Suche, wie sie hier umgesetzt wurde, sind drei verschiedene Versionen der Datei(en) nötig:
Die Dateien lagen dazu im TEI-Format vor und wurden für das Training und die Suche noch zusätzlich mittels CAB-Normalisierung aufbereitet -also die historische Schreibweise durch eine moderne ersetzt- da es sich bei dem Korpus um einen historischen Text handelt. Darum hatten sie vorerst zusätzliche Tags, die entfernt werden müssen. Die Vorgehensweise um diese Daten vorzubereiten unterteilt sich in verschiedene Schritte und wird mittels der Skripte Textprozess_Kant.py und Textprozess_functions.py umgesetzt:
Um die Aufbereitung möglichst simpel zu gestalten, werden alle unnötigen Zusätze wie Fußnoten, Marginalia, Appendix, Seiten- und Zeilenumbrüche entfernt. Außerdem werden bereits die Überschriften in h-tags gewandelt und doppelte Leerzeichen entfernt. Dies geschieht mittels der anpassen()-Funktion. In den normalisierten Dateien werden zusätzlich die Abkürzungen aufgelöst (mithilfe von abkürzungen_auflösen()) und die zusätzlichen w- und s-tags so entfernt, dass die neuen Begriffe stehen bleiben.
Nachdem die Dateien in Schritt 1 vorbereitet wurden, werden die IDs gesetzt mit strukturiereDIV(). Da die Absätze bereits vorgefiltert werden, ist hier zwingend darauf zu achten, dass ein Absatz in der originalen Datei die gleiche ID hat wie der gleiche Absatz in dem normalisierten Text, da sonst die Zuweisung der Ergebnisse auf die Ausgabedatei am Ende nicht korrekt erfolgen kann und nicht nur falsche Ergebnisse auftreten, sondern im schlimmsten Fall auch das Programm abbrechen kann.
Mithilfe der in Schritt 2 erstellten Texte wird nun mittels erstelle_plaintext() und der Hilfsfunktion plain() ein reiner Text erstellt, bei dem die Mithilfe von Zeilenumbrüchen Absätze getrennt werden. Dabei werden drei Dateien erzeugt: Eine Datei mit allen Absätzen, eine mit allen außer dem zehnten und eine mit nur allen zehnten Absätzen. Die Bereingigung verringert dabei den fremdsprachlichen Anteil, Nummern und Daten, überflüßige Leerzeichen, Satzzeichen und Klammern und entfernt alle verbleibenden Tags.
Grundsätzlich wird hier wie in Schritt 3 verfahren, jedoch werden in den normalisierten Dateien zu Beginn nicht die Satzunterteilungen entfernt, sodass diese dann als Teiler genutzt werden können und aus den resultierenden Sätzen drei Dateien nach dem oben genannten Schema erstellt werden.
In die Trainingspipeline sind zwei Arten des Trainings verbaut: Einmal das Training des grundlegenden Modells mittels Masked Language modeling (entsprechend dem offiziellen Paper von Bert) mit anschließendem Feintuning und zum anderen das Training mittels TSDAE und anschließendem Feintuning. Mit ein paar Anpassungen ist es auch möglich dies zu koppeln und auf das grundlegende Training mit TSDAE aufzubauen und danach feinzutunen. Auch eine Anpassung des Vokabulars ist bereits eingebaut, wird in Trainingspipeline.py allerdings nicht genutzt, da mit dem geringen Umfang des hier genutzten Koprus zu befürchten steht, dass die Gewichtungen der neuen Einträge nicht ausreichend trainiert werden.
Wie bei der Vorbereitung der Daten ist auch das Programm in Funktionen und Anwendung des Trainings geteilt. Das Training geschieht mittels HuggingFace Vorlagen und Benutzung von SentenceTransformer. Das Modell sollte eine dafür entsprechende Form haben. Für TSDAE sind die Vorgaben noch strikter (Distilbert und ConvBERT bspw. funktionieren nicht). Für das Feintuning wird ein deutsches Datenset von deepset, GermanDPR, verwendet und nicht extra eigene Information Retrieval Daten aus dem Kantkorpus extrahiert.
In Training_functions.py werden vier Funktionen bereitgestellt. Mithilfe von lade_Modell() kann das Modell enstprechend den Vorgaben eingelesen werden. Es deckt auch die neue Initialisierung eines SentenceTransformermodells ab. Die Funktion deckt für den Moment nur die Modelle svalabs/bi-electra-ms-marco-german-uncased, deepset/gelectra-large-germanquad, dbmdz/distilbert-base-german-europeana-cased und dbmdz/convbert-base-german-europeana-cased ab (Vielen Dank für die Bereitstellung dieser vortrainierten Modelle!). Die anderen drei Funktionen sind für das Laden der Daten. Auch wenn diese Funktionen in Sachen Vorbereitung einiges Abnehmen, müssen für das Training noch einige weitere Angaben gemacht werden. Dies ist allerdings Dank der entsprechenden Plattformen größtenteils vergleichsweise einfach und ist, vor allem für allgemeine Anwendungsfälle, recht gut dokumentiert.
Das grundlegende Transformer Modell wird hier mittels MaskedLM trainiert, entsprechend den Handlungsanweisungen bei HuggingFace für den HuggingFace Trainer. Als Datengrundlage werden zwei Textdateien benötigt, in denen die Textabschnitte mit Zeilenumbruch getrennt vorliegen und die bereits in Trainings- (Vorsilbe "train_") und Evaluierungsdate (Vorsilbe "eval_") unterteilt sind. Die Daten sollten aufbereitet sein, aber noch nicht tokenisiert. Mittels lese_dataset() werden die Daten dann in die Form eines Datensets gebracht. Für Testzwecke ist es auch möglich nur einen kleinen Auszug zu erhalten. Damit kann dann die eigene Implementierung ausführlich getestet werden, damit bei dem vollen Training alles optimal eingestellt ist und funktioniert. Wichtig ist jedoch immer, dass cased Modelle die Groß-/Kleinschreibung beachten, uncased benötigen kleinen Text. Es müssen dann nur noch die jeweils passenden Angaben für den eigenen Korpus und die eigene Hardware gemacht werden (Hierauf wird etwas ausführlicher eingegangen, weil diese Informationen teilweise etwas verstreut auf HuggingFace liegen):
Nachdem wahlweise ein neues SentenceTransformerModell als Aufsatz für das zugrundeliegende BertModell erstellt wurde oder ein schon existierendes geladen wurde, wird dieses Modell nun mithilfe von Textgruppen auf den konkreten Anwendungsfall hin trainiert. Das heißt es wird die Ähnlichkeit von Textstellen zu einer Frage berechnet. In dem Datenset GermanDPR sind Frage-Antwortkombinationen zu finden. Dabei gibt es zu jeder Frage immer mindestens eine gut passende (Positive) Antwort und eine nicht passende (negative) Antwort. Bei HuggingFace gibt es auch dazu eine Einführung. Mithilfe von lese_InputExample() werden die Daten in das richtige Format gebracht, auch für die Evaluierung (in dem Datenset ist sowohl ein Trainings- als auch ein Testset enthalten).
Entsprechend den Daten wird der loss mittels TripletLoss und die Evaluation mittels TripletEvaluator berechnet. Dabei wird der Abstand zwischen der Frage und der positiven Antword minimiert und umgekehrt der Abstand zwischen Frage und negativer Antwort maximiert. In dem Datenset handelt es sich tatsächlich immer um Fragen als Anker, sodass das Modell dahingehend trainiert wird und besonders gut mit Fragen als Eingaben funktioniert. Hinzu kommen auch hier noch spezifische Angaben:
Nachdem wahlweise ein neues SentenceTransformerModell als Aufsatz für das zugrundeliegende BertModell erstellt wurde oder ein schon existierendes geladen wurde, wird dieses Modell nun mithilfe von korrumpierten Sätzen aus dem Korpus vortrainiert. Der Computer muss mithilfe des Modells die Sätze wiederherstellen. Dazu müssen die Daten als Liste von Sätzen vorliegen. Das Einlesen aus einer Datei, in der sie mit Zeilenumbruch getrennt vorliegen wird mit der Funktion lese_datalisten() realisiert. Nun muss nur noch das Skript von den Erfindern umgesetzt werden. Bis auf den Rückgabe/Speicherangaben sowie der Batchgröße wurden die Angaben von dort übernommen.
Nachdem das SentenceTransformerModell als Aufsatz für das zugrundeliegende BertModell erstellt und vortrainiert wurde, wird es nun mithilfe von Textgruppen auf den konkreten Anwendungsfall hin trainiert. Dies geschieht analog zu Schritt 1.2. Es wird die Ähnlichkeit von Textstellen zu einer Frage berechnet. In dem Datenset GermanDPR sind Frage-Antwortkombinationen zu finden. Dabei gibt es zu jeder Frage immer mindestens eine gut passende (Positive) Antwort und eine nicht passende (negative) Antwort. Bei HuggingFace gibt es auch dazu eine Einführung. Mithilfe von lese_InputExample() werden die Daten in das richtige Format gebracht, auch für die Evaluierung (in dem Datenset ist sowohl ein Trainings- als auch ein Testset enthalten).
Entsprechend den Daten wird der loss mittels TripletLoss und die Evaluation mittels TripletEvaluator berechnet. Dabei wird der Abstand zwischen der Frage und der positiven Antword minimiert und umgekehrt der Abstand zwischen Frage und negativer Antwort maximiert. In dem Datenset handelt es sich tatsächlich immer um Fragen als Anker, sodass das Modell dahingehend trainiert wird und besonders gut mit Fragen als Eingaben funktioniert. Hinzu kommen auch hier noch spezifische Angaben:
Um die Suche anzuwenden müssen mithilfe eines SentenceTransformermodells die Vektorabbildung sowohl der Zielabsätze, als auch der Eingabe berechnet werden um anschließend den/die Zielabsatz/sätze herauszusuchen, die die größte Übereinstimmung mit der Sucheingabe aufweisen. Dazu werden die Python-Dateien Vektoren-Test.py und Anwendung_functions.py herangezogen.
Für die Suche sollten die zu durchsuchenden Absätze bereits in einer tei-Datei mit ausgezeichneten Ids vorliegen. Hier werden sogar zwei Versionen verwendet: zum einen die normalisierten Texte zum Berechnen mit dem Computer, zum anderen die originalen Texte, die für die Ausgabe verwendet werden. Die IDs müssen genau übereinstimmen, damit von dem normalisierten Absatz wieder auf die originale Textdatei geschlossen werden kann.
Mittels bereite_daten() werden diese Dateien eingelesen und Listen erstellt:
Zusätzlich werden noch Mappinglisten erstellt (eine pro Band), die von der AbsatzID auf die Seitenangabe in dem zugrundeliegenden Text rückschließen lassen. Mithilfe von Band und Seitenangabe ist es möglich auf der Webseite von korpora.org auf die entsprechende Textstelle im Gesamtkorpus zu verweisen.
Liegen die Listen in entsprechender Form vor, kann mithilfe von vektorenberechnen() die Vektorberechnung angestoßen werden. Liegen die Vektoren bereits vor, müssen sie lediglich eingelesen werden, ansonsten werden sie berechnet und für eine zukünftige Verwendung abgespeichert. Die Berechnung wird mithilfe eines SentenceTransformermodells durchgeführt, das bedeutet, wenn dieses noch nicht trainiert wurde, können auch bei einem gut trainierten Transformermodell schlechte Ergebnisse resultieren.
Zuletzt werden die Listen und die Vektoren mit einer Eingabe in Verbindung gesetzt. Es wird also für die Eingabe ein Vektor berechnet und dieser mit den normalisierten Vektoren verglichen. Der/die beste/n Ergebnisse werden nun mittels suche_absatz() aus den originalen Daten herausgesucht. Dabei wird zuerst die Id des Absatzes in dem normalisierten Datensatz herausgesucht, dann diese in der Id-Liste der originalen Absätze gesucht und mithilfe der so gewonnenen Indizes der Absatz gefunden und ausgegeben. Sollte die Id des Ergebnisses nicht in der originalen Id-Liste enthalten sein, so wird das Ergebnis übersprungen.
Für die Auswertung der Modelle gibt es zwei Verfahren: den Mean Reciprocal Rank (MRR) und den des gewichteten MRR (gMRR). Bei zweiterem werden die Ergebnisse nicht nur in Treffer und nicht Treffer unterteilt, sondern eine weitere Bewertung der Ergebnisse durchgeführt, um so auch Abschnitte einzubeziehen, die zwar kein perfekter Treffer sind, aber dennoch relevante oder spannende Informationen beinhalten. Dies geschieht mittels Auswertung.py und Auswertung_functions.py. Für beide Auswertungen muss eine manuelle Bewertung durchgeführt werden.
Hier die Berechnungsformeln mit |A| als Anzahl der Antworten, r als Rang der Antwort und b als Bewertung:
... für den MRR: $$\text{MRR} = \frac{ \sum\limits_{i=1}^{|\text{A}|} \frac{1}{\text{r}_i} }{ |\text{A}| }$$
... für den gewichteten MRR: $$\text{MRR}_{g} = \frac{ \sum\limits_{i=1}^{|\text{A}|} \frac{1}{ \text{min}\left(\text{r}_i \cdot \text{b}_i^4\right) } }{ |\text{A}| }$$
Zu Beginn müssen die Ergebnisse berechnet werden. Das geschieht analog zu der Implementierung der Suche und falls die Vektoren dort bereits berechnet wurden können sie wiederverwendet werden. Ansonsten werden sie berechnet und an dem angegebenen Ort abgespeichert. Dafür wird auf Funktionen von Anwendung_functions.py zurückgegriffen. Die Ergebnisse werden anschließend in zwei Formen abgespeichert: Einerseits für jedes Modell als Ergebnisliste pro Frage. Diese Datei ist nur für die anschließende Auswertung relevant. Andererseits wird für jede Frage ein Ergebniskatalog erstellt, in dem eine Bewertung durchgeführt werden muss, damit eine Auswertung durchgeführt werden kann. In diesen Dateien werden die Ergebnisse von allen Modellen duplikatfrei und ohne Angabe der Modelle gesichert.
Die Bewertung der einzelnen Ergebnisse ist händisch durchzuführen. Es bietet sich an für jede Frage-Antwortkombination eine Bewertung zwischen 0 und 10 durchzuführen. 0 steht dabei für kein Treffer (dieses Ergebnis wird nicht einbezogen in die Auswertung), eins für perfekter Treffer. Umso höher die Zahl weiterhin ist, desto schlechter ist das Ergebnis zu bewerten. Für den MRR werden nur Treffer der Wertung 1 herangezogen, während für den gMRR auch die weiteren Wertungen herangezogen werden. Die Tabelle sollte dann folgendermaßen aussehen:
| ID | Absatz | {Frage} |
|---|---|---|
| {ID1} | {Absatztext1} | {Bewertung1} |
| {ID2} | {Absatztext2} | {Bewertung2} |
| ... | ... | ... |
Die Bewertung muss also in die dritte Spalte hinter ID und Abschnitttext geschrieben werden (unter die Frage), damit das Programm die Auswertung durchführen kann. Die Formatierung muss erhalten bleiben (Delimiter muss ein Komma sein). Danach ist in der Konsole mit der Eingabe j zu bestätigen, dass die Bewertung erfolgt ist.
Nachdem die Bewertung erfolgt ist, werden die Dateien eingelesen und in Zusammenhang gestellt. Entscheidend ist dabei die ID, sowie die Frage und die Position. Aus diesen drei Faktoren zieht das Programm die Bewertung und den Rang und Kann so für jede Frage einen Wert ermitteln, deren Mittelwert dann den MRR/gMRR darstellt. Alle Werte werden nun pro Modell in eine Tabelle geschrieben. Es werden auch die Werte pro Frage gespeichert, sodass eine Mittelwertberechnung von nur bestimmten Fragen vereinfacht wird (Hierzu müssen nur die Werte der Fragen aufaddiert und durch die Anzahl dieser Frage geteilt werden). MRR und gMRR befinden sich in der untersten Zeile.
Nachdem die vollständige und allgemeine Auswertung erfolgt ist, kann die Betrachtung von Teilwerten relevant sein, um zum Beispiel zu erforschen, welche Kriterien besonders zielführende Sucheingaben erfüllen. Dazu kann das Skript Teilauswertung.py genutzt werden. Es wird zusätzlich eine Liste an Eingaben benötigt, die in der Gesamtauswertung bereits vorkamen sowie die Auswertungsdateien der Gesamtauswertung. Da bei der Gesamtauswertung die Teilergebnisse zu jeder Eingabe mitgespeichert werden, kann auf diese zurückgegriffen werden. Es wird also der Mittelwert aus diesen Teilergebnissen berechnet.
Bei der Auswertung kam es zu ambivalenten Ergebnissen. Grundsätzlich ist die Tendenz von besseren Ergebnissen bei längeren Eingaben klar erkennbar. Allerdings gibt es auch hier Ausreißer: Beispielsweise kommen die Versionen des bi-electra-Modells besonders gut mit der Begriffskombination "freier Wille" aus und gerade das Grundmodell lieferte die besten Ergebnisse, die sehr sauber und final waren, jedoch nicht so sehr zur weiteren Recherche einluden, weniger Hintergrundinformationen lieferten und sich weniger "im Sinne Kants" lasen. Allgemein wäre ein größerer Fragenkatalog sicher von Vorteil um positive und negative Ausreiser abzufangen und die "Glückskomponente" zu verringern. Nachfolgend befindet sich der Eingabekatalog:
| Eingabe-Nr. | Eingabetext |
|---|---|
| 1.1 | Determinismus |
| 1.2 | Willkürfreiheit, zureichender Grund, natürliche und sittliche Notwendigkeit, Vereinigung von Freiheit und Naturnotwendigkeit |
| 1.3 | Gibt es eine Verbindung von einem eindeutig deterministischen Grundansatz mit einer indeterministischen Verteidigung der formellen Zufälligkeit? |
| 1.4 | freier Wille |
| 1.5 | Grund für das Handeln, Willensbegrenzung |
| 1.6 | Gibt es einen Grund für das Handeln? |
| 2.1 | Linguistik |
| 2.2 | analytische Philosophie, Metakritik des Purismus der Vernunft, Formalismus |
| 2.3 | Welcher Sprachlichkeit oder Sprachbedingtheit unterliegt das Denken? |
| 2.4 | Sprachlichkeit |
| 2.5 | Unterschiede in der Sprache, Wortbedeutung, Aussage |
| 2.6 | Welchen Einfluß hat die Sprache auf die Gedanken? |
| 3.1 | ratio cognoscendi |
| 3.2 | Autonomie, Dualismus von Natur und Freiheit |
| 3.3 | Die Moral ist ein allgemeingültiges Gesetz und daher ein formales Prinzip. |
| 3.4 | Handeln |
| 3.5 | Wollen, Absicht, Handlung |
| 3.6 | Der Wille in der Welt existiert. |
| 4.1 | Künstliche Intelligenz |
| 4.2 | KI, Machinenlernen, Tiefe neuronale Netze, Worteinbettung |
| 4.3 | Wobei handelt es sich bei Künstlicher Intelligenz? Wird sie ein Segen für die Menschheit sein und intelligente Lösungen komplexer Probleme liefern? |
| 4.4 | Bundeskanzler |
| 4.5 | Bundeskanzler, Wahl, Bundesregierung, Bundestag |
| 4.6 | Welche Aufgaben hat der Bundeskanzler aktuell außer Deutschland zu repräsentieren? |
Außerdem die Auswertung in zusammengefasster Tabellenform:
| Eingabe-Nr. | Basis (MRR | gMRR) | Training (MRR | gMRR) | Training (MRR | gMRR) | TSDAE (MRR | gMRR) | TSDAE-Fein (MRR | gMRR) | Training-Fein-TSDAE (MRR | gMRR) |
|---|---|---|---|---|---|---|
| Bielectra | 0,25463 | 0,26647 | 0,06706 | 0,07062 | 0,21776 | 0,22342 | 0,07813 | 0,08733 | 0,13542 | 0,14159 | 0,07187 | 0,08380 |
| Distilbert | 0,00000 | 0,00545 | 0,00416 | 0,00938 | 0,12153 | 0,13133 | - | - | - | - | - | - |
| Converb | 0,00000 | 0,00085 | 0,00000 | 0,00333 | 0,00521 | 0,00628 | - | - | - | - | - | - |
| Gelectra | 0,00000 | 0,00104 | 0,00417 | 0,00886 | 0,00000 | 0,00352 | 0,00000 | 0,00090 | 0,00000 | 0,00330 | 0,06250 | 0,06530 |
Auswertung und Teilauswertungen finden sich graphisch aufbereitet auch auf der Webseite des Projektes.
Vorbereitung:
Training:
Anwendung:
Auswertung:
Kantkorpus und Modelle (wegen der Größe) werden (vorerst) nicht übertragen.
41 commits
2 commits
Python
100.0%
Source Code für das das Masterarbeitsprojekt einer semantischen Suche für das Gesamtwerk von Immanuel Kant. Das Projekt basiert auf der online Ausgabe des Gesamtwerks von Immanuel Kant. Vielen Dank für die Bereitstellung der Daten!
Die Webseite mit der implementierten Suche ist unter folgendem Link zu finden: in-medias-res.wagnerhof.net
Enthalten sind:
Dateien mit Daten und Skripten zum Training und Vorbereiten der Daten.
Die Ergebnisse der Auswertung. Nähere Beschreibung dazu unter Auswertung.
Aus Lizenzrechtlichen Gründen können die Daten nicht veröffentlicht werden, jedoch bei den Rechteinhabern angefragt werden.
Bereits berechnete Vektoren getrennt nach Modellen für alle Bände. Erstellt durch Vektoren-Test.py
Es wurden folgende Modelle lokal installiert:
Dieses HowTo richtet sich vor allem an Neulinge der Natürlichen Sprachverarbeitung. Für Fortgeschrittenere ist wohl eher die Auswertung der Ergebnisse relevant, bzw. wie das Verfahren Implementiert wurde. Auf Feinheiten der Implementierung kann aufgrund des Platzes nicht in vollem Umfang eingegangen werden. Dazu gibt es auf der Webseite weitere Informationen.
Dieses Repositorium stellt alle Handwerkszeuge für die Aufbereitung des Korpus, das Training der Modelle und auch ein kleines Programm zur Durchführung einer Suche in dem Korpus dar. Es wird kurz auf die Einrichtung der Programmierumgebung eingegangen und enthält eine niederschwellige Schritt-für-Schritt-Anleitung, was jeweils gemacht wird (mit Hinweisen, wie der Code zur Anwendung an eigene Daten angepasst werden sollte). Eine allgemeine Einführung in das Projekt gibt es bei der überspannenden Organisation, oder auch auf der Webseite.
Grundsätzlich wurde versucht die Implementierung der Suche möglicht so zu gestalten, dass zumindest einzelne Funktionen oder vielleicht sogar ganze Programmteile auch für andere Projekte nutzbar sind. Jedoch ist jeder Korpus anders, sodass vor allem die Aufbereitung dessen nicht verallgemeinbar ist. Umso wichtiger ist die Dokumentation, in welcher Form die Daten vorliegen müssen um mit den restlichen Funktionen zusammen arbeiten zu können. So soll es möglich sein bereits aufbereitete Modelle zum Beispiel einfach zu benutzen um die Modelle zu trainieren, oder mit den eigenen trainierten Modellen oder auch Modellen anderer Institutionen darin zu suchen. Das eigene Modell mit den hier genutzten Daten zu trainieren ist leider momentan nicht möglich, wenden Sie sich dazu gerne an die Herausgeber des online-Kantkorpus.
Je nach Vorhaben kann direkt zu den entsprechenden Kapiteln gesprungen werden:
Für die Suche, wie sie hier umgesetzt wurde, sind drei verschiedene Versionen der Datei(en) nötig:
Die Dateien lagen dazu im TEI-Format vor und wurden für das Training und die Suche noch zusätzlich mittels CAB-Normalisierung aufbereitet -also die historische Schreibweise durch eine moderne ersetzt- da es sich bei dem Korpus um einen historischen Text handelt. Darum hatten sie vorerst zusätzliche Tags, die entfernt werden müssen. Die Vorgehensweise um diese Daten vorzubereiten unterteilt sich in verschiedene Schritte und wird mittels der Skripte Textprozess_Kant.py und Textprozess_functions.py umgesetzt:
Um die Aufbereitung möglichst simpel zu gestalten, werden alle unnötigen Zusätze wie Fußnoten, Marginalia, Appendix, Seiten- und Zeilenumbrüche entfernt. Außerdem werden bereits die Überschriften in h-tags gewandelt und doppelte Leerzeichen entfernt. Dies geschieht mittels der anpassen()-Funktion. In den normalisierten Dateien werden zusätzlich die Abkürzungen aufgelöst (mithilfe von abkürzungen_auflösen()) und die zusätzlichen w- und s-tags so entfernt, dass die neuen Begriffe stehen bleiben.
Nachdem die Dateien in Schritt 1 vorbereitet wurden, werden die IDs gesetzt mit strukturiereDIV(). Da die Absätze bereits vorgefiltert werden, ist hier zwingend darauf zu achten, dass ein Absatz in der originalen Datei die gleiche ID hat wie der gleiche Absatz in dem normalisierten Text, da sonst die Zuweisung der Ergebnisse auf die Ausgabedatei am Ende nicht korrekt erfolgen kann und nicht nur falsche Ergebnisse auftreten, sondern im schlimmsten Fall auch das Programm abbrechen kann.
Mithilfe der in Schritt 2 erstellten Texte wird nun mittels erstelle_plaintext() und der Hilfsfunktion plain() ein reiner Text erstellt, bei dem die Mithilfe von Zeilenumbrüchen Absätze getrennt werden. Dabei werden drei Dateien erzeugt: Eine Datei mit allen Absätzen, eine mit allen außer dem zehnten und eine mit nur allen zehnten Absätzen. Die Bereingigung verringert dabei den fremdsprachlichen Anteil, Nummern und Daten, überflüßige Leerzeichen, Satzzeichen und Klammern und entfernt alle verbleibenden Tags.
Grundsätzlich wird hier wie in Schritt 3 verfahren, jedoch werden in den normalisierten Dateien zu Beginn nicht die Satzunterteilungen entfernt, sodass diese dann als Teiler genutzt werden können und aus den resultierenden Sätzen drei Dateien nach dem oben genannten Schema erstellt werden.
In die Trainingspipeline sind zwei Arten des Trainings verbaut: Einmal das Training des grundlegenden Modells mittels Masked Language modeling (entsprechend dem offiziellen Paper von Bert) mit anschließendem Feintuning und zum anderen das Training mittels TSDAE und anschließendem Feintuning. Mit ein paar Anpassungen ist es auch möglich dies zu koppeln und auf das grundlegende Training mit TSDAE aufzubauen und danach feinzutunen. Auch eine Anpassung des Vokabulars ist bereits eingebaut, wird in Trainingspipeline.py allerdings nicht genutzt, da mit dem geringen Umfang des hier genutzten Koprus zu befürchten steht, dass die Gewichtungen der neuen Einträge nicht ausreichend trainiert werden.
Wie bei der Vorbereitung der Daten ist auch das Programm in Funktionen und Anwendung des Trainings geteilt. Das Training geschieht mittels HuggingFace Vorlagen und Benutzung von SentenceTransformer. Das Modell sollte eine dafür entsprechende Form haben. Für TSDAE sind die Vorgaben noch strikter (Distilbert und ConvBERT bspw. funktionieren nicht). Für das Feintuning wird ein deutsches Datenset von deepset, GermanDPR, verwendet und nicht extra eigene Information Retrieval Daten aus dem Kantkorpus extrahiert.
In Training_functions.py werden vier Funktionen bereitgestellt. Mithilfe von lade_Modell() kann das Modell enstprechend den Vorgaben eingelesen werden. Es deckt auch die neue Initialisierung eines SentenceTransformermodells ab. Die Funktion deckt für den Moment nur die Modelle svalabs/bi-electra-ms-marco-german-uncased, deepset/gelectra-large-germanquad, dbmdz/distilbert-base-german-europeana-cased und dbmdz/convbert-base-german-europeana-cased ab (Vielen Dank für die Bereitstellung dieser vortrainierten Modelle!). Die anderen drei Funktionen sind für das Laden der Daten. Auch wenn diese Funktionen in Sachen Vorbereitung einiges Abnehmen, müssen für das Training noch einige weitere Angaben gemacht werden. Dies ist allerdings Dank der entsprechenden Plattformen größtenteils vergleichsweise einfach und ist, vor allem für allgemeine Anwendungsfälle, recht gut dokumentiert.
Das grundlegende Transformer Modell wird hier mittels MaskedLM trainiert, entsprechend den Handlungsanweisungen bei HuggingFace für den HuggingFace Trainer. Als Datengrundlage werden zwei Textdateien benötigt, in denen die Textabschnitte mit Zeilenumbruch getrennt vorliegen und die bereits in Trainings- (Vorsilbe "train_") und Evaluierungsdate (Vorsilbe "eval_") unterteilt sind. Die Daten sollten aufbereitet sein, aber noch nicht tokenisiert. Mittels lese_dataset() werden die Daten dann in die Form eines Datensets gebracht. Für Testzwecke ist es auch möglich nur einen kleinen Auszug zu erhalten. Damit kann dann die eigene Implementierung ausführlich getestet werden, damit bei dem vollen Training alles optimal eingestellt ist und funktioniert. Wichtig ist jedoch immer, dass cased Modelle die Groß-/Kleinschreibung beachten, uncased benötigen kleinen Text. Es müssen dann nur noch die jeweils passenden Angaben für den eigenen Korpus und die eigene Hardware gemacht werden (Hierauf wird etwas ausführlicher eingegangen, weil diese Informationen teilweise etwas verstreut auf HuggingFace liegen):
Nachdem wahlweise ein neues SentenceTransformerModell als Aufsatz für das zugrundeliegende BertModell erstellt wurde oder ein schon existierendes geladen wurde, wird dieses Modell nun mithilfe von Textgruppen auf den konkreten Anwendungsfall hin trainiert. Das heißt es wird die Ähnlichkeit von Textstellen zu einer Frage berechnet. In dem Datenset GermanDPR sind Frage-Antwortkombinationen zu finden. Dabei gibt es zu jeder Frage immer mindestens eine gut passende (Positive) Antwort und eine nicht passende (negative) Antwort. Bei HuggingFace gibt es auch dazu eine Einführung. Mithilfe von lese_InputExample() werden die Daten in das richtige Format gebracht, auch für die Evaluierung (in dem Datenset ist sowohl ein Trainings- als auch ein Testset enthalten).
Entsprechend den Daten wird der loss mittels TripletLoss und die Evaluation mittels TripletEvaluator berechnet. Dabei wird der Abstand zwischen der Frage und der positiven Antword minimiert und umgekehrt der Abstand zwischen Frage und negativer Antwort maximiert. In dem Datenset handelt es sich tatsächlich immer um Fragen als Anker, sodass das Modell dahingehend trainiert wird und besonders gut mit Fragen als Eingaben funktioniert. Hinzu kommen auch hier noch spezifische Angaben:
Nachdem wahlweise ein neues SentenceTransformerModell als Aufsatz für das zugrundeliegende BertModell erstellt wurde oder ein schon existierendes geladen wurde, wird dieses Modell nun mithilfe von korrumpierten Sätzen aus dem Korpus vortrainiert. Der Computer muss mithilfe des Modells die Sätze wiederherstellen. Dazu müssen die Daten als Liste von Sätzen vorliegen. Das Einlesen aus einer Datei, in der sie mit Zeilenumbruch getrennt vorliegen wird mit der Funktion lese_datalisten() realisiert. Nun muss nur noch das Skript von den Erfindern umgesetzt werden. Bis auf den Rückgabe/Speicherangaben sowie der Batchgröße wurden die Angaben von dort übernommen.
Nachdem das SentenceTransformerModell als Aufsatz für das zugrundeliegende BertModell erstellt und vortrainiert wurde, wird es nun mithilfe von Textgruppen auf den konkreten Anwendungsfall hin trainiert. Dies geschieht analog zu Schritt 1.2. Es wird die Ähnlichkeit von Textstellen zu einer Frage berechnet. In dem Datenset GermanDPR sind Frage-Antwortkombinationen zu finden. Dabei gibt es zu jeder Frage immer mindestens eine gut passende (Positive) Antwort und eine nicht passende (negative) Antwort. Bei HuggingFace gibt es auch dazu eine Einführung. Mithilfe von lese_InputExample() werden die Daten in das richtige Format gebracht, auch für die Evaluierung (in dem Datenset ist sowohl ein Trainings- als auch ein Testset enthalten).
Entsprechend den Daten wird der loss mittels TripletLoss und die Evaluation mittels TripletEvaluator berechnet. Dabei wird der Abstand zwischen der Frage und der positiven Antword minimiert und umgekehrt der Abstand zwischen Frage und negativer Antwort maximiert. In dem Datenset handelt es sich tatsächlich immer um Fragen als Anker, sodass das Modell dahingehend trainiert wird und besonders gut mit Fragen als Eingaben funktioniert. Hinzu kommen auch hier noch spezifische Angaben:
Um die Suche anzuwenden müssen mithilfe eines SentenceTransformermodells die Vektorabbildung sowohl der Zielabsätze, als auch der Eingabe berechnet werden um anschließend den/die Zielabsatz/sätze herauszusuchen, die die größte Übereinstimmung mit der Sucheingabe aufweisen. Dazu werden die Python-Dateien Vektoren-Test.py und Anwendung_functions.py herangezogen.
Für die Suche sollten die zu durchsuchenden Absätze bereits in einer tei-Datei mit ausgezeichneten Ids vorliegen. Hier werden sogar zwei Versionen verwendet: zum einen die normalisierten Texte zum Berechnen mit dem Computer, zum anderen die originalen Texte, die für die Ausgabe verwendet werden. Die IDs müssen genau übereinstimmen, damit von dem normalisierten Absatz wieder auf die originale Textdatei geschlossen werden kann.
Mittels bereite_daten() werden diese Dateien eingelesen und Listen erstellt:
Zusätzlich werden noch Mappinglisten erstellt (eine pro Band), die von der AbsatzID auf die Seitenangabe in dem zugrundeliegenden Text rückschließen lassen. Mithilfe von Band und Seitenangabe ist es möglich auf der Webseite von korpora.org auf die entsprechende Textstelle im Gesamtkorpus zu verweisen.
Liegen die Listen in entsprechender Form vor, kann mithilfe von vektorenberechnen() die Vektorberechnung angestoßen werden. Liegen die Vektoren bereits vor, müssen sie lediglich eingelesen werden, ansonsten werden sie berechnet und für eine zukünftige Verwendung abgespeichert. Die Berechnung wird mithilfe eines SentenceTransformermodells durchgeführt, das bedeutet, wenn dieses noch nicht trainiert wurde, können auch bei einem gut trainierten Transformermodell schlechte Ergebnisse resultieren.
Zuletzt werden die Listen und die Vektoren mit einer Eingabe in Verbindung gesetzt. Es wird also für die Eingabe ein Vektor berechnet und dieser mit den normalisierten Vektoren verglichen. Der/die beste/n Ergebnisse werden nun mittels suche_absatz() aus den originalen Daten herausgesucht. Dabei wird zuerst die Id des Absatzes in dem normalisierten Datensatz herausgesucht, dann diese in der Id-Liste der originalen Absätze gesucht und mithilfe der so gewonnenen Indizes der Absatz gefunden und ausgegeben. Sollte die Id des Ergebnisses nicht in der originalen Id-Liste enthalten sein, so wird das Ergebnis übersprungen.
Für die Auswertung der Modelle gibt es zwei Verfahren: den Mean Reciprocal Rank (MRR) und den des gewichteten MRR (gMRR). Bei zweiterem werden die Ergebnisse nicht nur in Treffer und nicht Treffer unterteilt, sondern eine weitere Bewertung der Ergebnisse durchgeführt, um so auch Abschnitte einzubeziehen, die zwar kein perfekter Treffer sind, aber dennoch relevante oder spannende Informationen beinhalten. Dies geschieht mittels Auswertung.py und Auswertung_functions.py. Für beide Auswertungen muss eine manuelle Bewertung durchgeführt werden.
Hier die Berechnungsformeln mit |A| als Anzahl der Antworten, r als Rang der Antwort und b als Bewertung:
... für den MRR: $$\text{MRR} = \frac{ \sum\limits_{i=1}^{|\text{A}|} \frac{1}{\text{r}_i} }{ |\text{A}| }$$
... für den gewichteten MRR: $$\text{MRR}_{g} = \frac{ \sum\limits_{i=1}^{|\text{A}|} \frac{1}{ \text{min}\left(\text{r}_i \cdot \text{b}_i^4\right) } }{ |\text{A}| }$$
Zu Beginn müssen die Ergebnisse berechnet werden. Das geschieht analog zu der Implementierung der Suche und falls die Vektoren dort bereits berechnet wurden können sie wiederverwendet werden. Ansonsten werden sie berechnet und an dem angegebenen Ort abgespeichert. Dafür wird auf Funktionen von Anwendung_functions.py zurückgegriffen. Die Ergebnisse werden anschließend in zwei Formen abgespeichert: Einerseits für jedes Modell als Ergebnisliste pro Frage. Diese Datei ist nur für die anschließende Auswertung relevant. Andererseits wird für jede Frage ein Ergebniskatalog erstellt, in dem eine Bewertung durchgeführt werden muss, damit eine Auswertung durchgeführt werden kann. In diesen Dateien werden die Ergebnisse von allen Modellen duplikatfrei und ohne Angabe der Modelle gesichert.
Die Bewertung der einzelnen Ergebnisse ist händisch durchzuführen. Es bietet sich an für jede Frage-Antwortkombination eine Bewertung zwischen 0 und 10 durchzuführen. 0 steht dabei für kein Treffer (dieses Ergebnis wird nicht einbezogen in die Auswertung), eins für perfekter Treffer. Umso höher die Zahl weiterhin ist, desto schlechter ist das Ergebnis zu bewerten. Für den MRR werden nur Treffer der Wertung 1 herangezogen, während für den gMRR auch die weiteren Wertungen herangezogen werden. Die Tabelle sollte dann folgendermaßen aussehen:
| ID | Absatz | {Frage} |
|---|---|---|
| {ID1} | {Absatztext1} | {Bewertung1} |
| {ID2} | {Absatztext2} | {Bewertung2} |
| ... | ... | ... |
Die Bewertung muss also in die dritte Spalte hinter ID und Abschnitttext geschrieben werden (unter die Frage), damit das Programm die Auswertung durchführen kann. Die Formatierung muss erhalten bleiben (Delimiter muss ein Komma sein). Danach ist in der Konsole mit der Eingabe j zu bestätigen, dass die Bewertung erfolgt ist.
Nachdem die Bewertung erfolgt ist, werden die Dateien eingelesen und in Zusammenhang gestellt. Entscheidend ist dabei die ID, sowie die Frage und die Position. Aus diesen drei Faktoren zieht das Programm die Bewertung und den Rang und Kann so für jede Frage einen Wert ermitteln, deren Mittelwert dann den MRR/gMRR darstellt. Alle Werte werden nun pro Modell in eine Tabelle geschrieben. Es werden auch die Werte pro Frage gespeichert, sodass eine Mittelwertberechnung von nur bestimmten Fragen vereinfacht wird (Hierzu müssen nur die Werte der Fragen aufaddiert und durch die Anzahl dieser Frage geteilt werden). MRR und gMRR befinden sich in der untersten Zeile.
Nachdem die vollständige und allgemeine Auswertung erfolgt ist, kann die Betrachtung von Teilwerten relevant sein, um zum Beispiel zu erforschen, welche Kriterien besonders zielführende Sucheingaben erfüllen. Dazu kann das Skript Teilauswertung.py genutzt werden. Es wird zusätzlich eine Liste an Eingaben benötigt, die in der Gesamtauswertung bereits vorkamen sowie die Auswertungsdateien der Gesamtauswertung. Da bei der Gesamtauswertung die Teilergebnisse zu jeder Eingabe mitgespeichert werden, kann auf diese zurückgegriffen werden. Es wird also der Mittelwert aus diesen Teilergebnissen berechnet.
Bei der Auswertung kam es zu ambivalenten Ergebnissen. Grundsätzlich ist die Tendenz von besseren Ergebnissen bei längeren Eingaben klar erkennbar. Allerdings gibt es auch hier Ausreißer: Beispielsweise kommen die Versionen des bi-electra-Modells besonders gut mit der Begriffskombination "freier Wille" aus und gerade das Grundmodell lieferte die besten Ergebnisse, die sehr sauber und final waren, jedoch nicht so sehr zur weiteren Recherche einluden, weniger Hintergrundinformationen lieferten und sich weniger "im Sinne Kants" lasen. Allgemein wäre ein größerer Fragenkatalog sicher von Vorteil um positive und negative Ausreiser abzufangen und die "Glückskomponente" zu verringern. Nachfolgend befindet sich der Eingabekatalog:
| Eingabe-Nr. | Eingabetext |
|---|---|
| 1.1 | Determinismus |
| 1.2 | Willkürfreiheit, zureichender Grund, natürliche und sittliche Notwendigkeit, Vereinigung von Freiheit und Naturnotwendigkeit |
| 1.3 | Gibt es eine Verbindung von einem eindeutig deterministischen Grundansatz mit einer indeterministischen Verteidigung der formellen Zufälligkeit? |
| 1.4 | freier Wille |
| 1.5 | Grund für das Handeln, Willensbegrenzung |
| 1.6 | Gibt es einen Grund für das Handeln? |
| 2.1 | Linguistik |
| 2.2 | analytische Philosophie, Metakritik des Purismus der Vernunft, Formalismus |
| 2.3 | Welcher Sprachlichkeit oder Sprachbedingtheit unterliegt das Denken? |
| 2.4 | Sprachlichkeit |
| 2.5 | Unterschiede in der Sprache, Wortbedeutung, Aussage |
| 2.6 | Welchen Einfluß hat die Sprache auf die Gedanken? |
| 3.1 | ratio cognoscendi |
| 3.2 | Autonomie, Dualismus von Natur und Freiheit |
| 3.3 | Die Moral ist ein allgemeingültiges Gesetz und daher ein formales Prinzip. |
| 3.4 | Handeln |
| 3.5 | Wollen, Absicht, Handlung |
| 3.6 | Der Wille in der Welt existiert. |
| 4.1 | Künstliche Intelligenz |
| 4.2 | KI, Machinenlernen, Tiefe neuronale Netze, Worteinbettung |
| 4.3 | Wobei handelt es sich bei Künstlicher Intelligenz? Wird sie ein Segen für die Menschheit sein und intelligente Lösungen komplexer Probleme liefern? |
| 4.4 | Bundeskanzler |
| 4.5 | Bundeskanzler, Wahl, Bundesregierung, Bundestag |
| 4.6 | Welche Aufgaben hat der Bundeskanzler aktuell außer Deutschland zu repräsentieren? |
Außerdem die Auswertung in zusammengefasster Tabellenform:
| Eingabe-Nr. | Basis (MRR | gMRR) | Training (MRR | gMRR) | Training (MRR | gMRR) | TSDAE (MRR | gMRR) | TSDAE-Fein (MRR | gMRR) | Training-Fein-TSDAE (MRR | gMRR) |
|---|---|---|---|---|---|---|
| Bielectra | 0,25463 | 0,26647 | 0,06706 | 0,07062 | 0,21776 | 0,22342 | 0,07813 | 0,08733 | 0,13542 | 0,14159 | 0,07187 | 0,08380 |
| Distilbert | 0,00000 | 0,00545 | 0,00416 | 0,00938 | 0,12153 | 0,13133 | - | - | - | - | - | - |
| Converb | 0,00000 | 0,00085 | 0,00000 | 0,00333 | 0,00521 | 0,00628 | - | - | - | - | - | - |
| Gelectra | 0,00000 | 0,00104 | 0,00417 | 0,00886 | 0,00000 | 0,00352 | 0,00000 | 0,00090 | 0,00000 | 0,00330 | 0,06250 | 0,06530 |
Auswertung und Teilauswertungen finden sich graphisch aufbereitet auch auf der Webseite des Projektes.
Vorbereitung:
Training:
Anwendung:
Auswertung:
Kantkorpus und Modelle (wegen der Größe) werden (vorerst) nicht übertragen.
41 commits
2 commits
Python
100.0%