Kapitel 4. Textvektorisierung und Transformationspipelines

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Algorithmen für maschinelles Lernen arbeiten mit einem numerischen Merkmalsraum, der als Eingabe ein zweidimensionales Feld erwartet, in dem die Zeilen Instanzen und die Spalten die Merkmale sind. Um maschinelles Lernen mit Text durchführen zu können, müssen wir unsere Dokumente in Vektordarstellungen umwandeln, damit wir numerisches maschinelles Lernen anwenden können. Dieser Prozess wird als Merkmalsextraktion oder, einfacher gesagt, als Vektorisierung bezeichnet und ist ein wichtiger erster Schritt auf dem Weg zu einer sprachsensiblen Analyse.

Die numerische Darstellung von Dokumenten ermöglicht uns aussagekräftige Analysen und schafft die Instanzen, auf denen Algorithmen für maschinelles Lernen arbeiten. In der Textanalyse sind Instanzen ganze Dokumente oder Äußerungen, deren Länge von Zitaten oder Tweets bis zu ganzen Büchern variieren kann, deren Vektoren aber immer eine einheitliche Länge haben. Jede Eigenschaft der Vektordarstellung ist ein Feature. Bei Texten stellen Features Attribute und Eigenschaften von Dokumenten dar - einschließlich des Inhalts und der Meta-Attribute wie Dokumentlänge, Autor, Quelle und Veröffentlichungsdatum. Zusammen betrachtet beschreiben die Merkmale eines Dokuments einen mehrdimensionalen Merkmalsraum, auf den Methoden des maschinellen Lernens angewendet werden können.

Aus diesem Grund müssen wir jetzt einen entscheidenden Wandel in der Art und Weise vollziehen, wie wir über Sprache denken - von einer Abfolge von Wörtern zu Punkten, die einen hochdimensionalen semantischen Raum einnehmen. Die Punkte im Raum können nahe beieinander oder weit auseinander liegen, eng geclustert oder gleichmäßig verteilt sein. Der semantische Raum wird daher so abgebildet, dass Dokumente mit ähnlichen Bedeutungen näher beieinander liegen und solche, die sich unterscheiden, weiter voneinander entfernt sind. Indem wir Ähnlichkeit als Distanz kodieren, können wir die primären Komponenten von Dokumenten ableiten und Entscheidungsgrenzen in unserem semantischen Raum ziehen.

Die einfachste Kodierung des semantischen Raums ist das Bag-of-Words-Modell, dessen wichtigste Erkenntnis ist, dass Bedeutung und Ähnlichkeit im Wortschatz kodiert werden. Ein Beispiel: Die Wikipedia-Artikel über Baseball und Babe Ruth sind sich wahrscheinlich sehr ähnlich. Nicht nur, dass viele der gleichen Wörter in beiden Artikeln vorkommen, sie werden auch nicht viele Wörter mit Artikeln über Aufläufe oder quantitative Lockerung gemeinsam haben. Dieses Modell ist zwar einfach, aber äußerst effektiv und bildet den Ausgangspunkt für die komplexeren Modelle, die wir erkunden werden.

In diesem Kapitel werden wir zeigen, wie wir den Vektorisierungsprozess nutzen können, um linguistische Techniken aus dem NLTK mit Techniken des maschinellen Lernens in Scikit-Learn und Gensim zu kombinieren und so benutzerdefinierte Transformatoren zu erstellen, die in wiederholbaren und wiederverwendbaren Pipelines eingesetzt werden können. Am Ende dieses Kapitels werden wir in der Lage sein, unseren vorverarbeiteten Korpus zu nutzen und die Dokumente in den Modellraum zu transformieren, damit wir Vorhersagen treffen können.

Worte im Raum

Um einen Korpus mit einem Bag-of-Words (BOW)-Ansatz zu vektorisieren, stellen wir jedes Dokument aus dem Korpus als einen Vektor dar, dessen Länge dem Vokabular des Korpus entspricht. Wir können die Berechnung vereinfachen, indem wir die Tokenpositionen des Vektors in alphabetischer Reihenfolge sortieren, wie in Abbildung 4-1 dargestellt. Alternativ können wir auch ein Wörterbuch anlegen, das die Token den Vektorpositionen zuordnet. In beiden Fällen erhalten wir eine Vektorabbildung des Korpus, die es uns ermöglicht, jedes Dokument eindeutig zu repräsentieren.

Vector encoding is a basic representation of documents.
Abbildung 4-1. Dokumente als Vektoren kodieren

Was sollte jedes Element im Dokumentenvektor sein? In den nächsten Abschnitten werden wir verschiedene Möglichkeiten untersuchen, die jeweils das grundlegende Bag-of-Words-Modell erweitern oder modifizieren, um den semantischen Raum zu beschreiben. Wir werden uns vier Arten der Vektorkodierung ansehen - Frequenz-, One-Hot-, TF-IDF- und verteilte Repräsentationen - und ihre Implementierungen in Scikit-Learn, Gensim und NLTK diskutieren. Wir arbeiten mit einem kleinen Korpus aus den drei Sätzen in den Beispielbildern.

Um dies einzurichten, erstellen wir eine Liste unserer Dokumente und tokenisieren sie für die folgenden Vektorisierungsbeispiele. Die Methode tokenize führt eine leichte Normalisierung durch, indem sie Satzzeichen mit Hilfe des Zeichensatzes string.punctuation entfernt und den Text in Kleinbuchstaben umwandelt. Diese Funktion führt auch eine Merkmalsreduzierung mit SnowballStemmer durch, um Affixe wie Pluralität zu entfernen ("Fledermäuse" und "Fledermaus" sind das gleiche Token). Die Beispiele im nächsten Abschnitt verwenden diesen Beispielkorpus und einige verwenden die Tokenisierungsmethode.

import nltk
import string

def tokenize(text):
    stem = nltk.stem.SnowballStemmer('english')
    text = text.lower()

    for token in nltk.word_tokenize(text):
        if token in string.punctuation: continue
        yield stem.stem(token)

corpus = [
    "The elephant sneezed at the sight of potatoes.",
    "Bats can see via echolocation. See the bat sight sneeze!",
    "Wondering, she opened the door to the studio.",
]

Die Wahl einer bestimmten Vektorisierungstechnik wird weitgehend durch den Problemraum bestimmt. Auch die Wahl der Implementierung - ob NLTK, Scikit-Learn oder Gensim - sollte sich nach den Anforderungen der Anwendung richten. NLTK bietet zum Beispiel viele Methoden, die sich besonders gut für Textdaten eignen, ist aber auch sehr abhängig von der Anwendung. Scikit-Learn wurde nicht speziell für Textdaten entwickelt, bietet aber eine robuste API und viele andere Annehmlichkeiten (auf die wir später in diesem Kapitel eingehen werden), die in einem Anwendungskontext besonders nützlich sind. Gensim kann Wörterbücher und Referenzen im Matrixmarktformat serialisieren, was es für mehrere Plattformen flexibler macht. Im Gegensatz zu Scikit-Learn übernimmt Gensim jedoch keine Tokenisierung oder Stemming für deine Dokumente.

Aus diesem Grund werden wir bei jedem der vier Kodierungsansätze ein paar Optionen für die Implementierung aufzeigen - "Mit NLTK", "In Scikit-Learn" und "The Gensim Way".

Frequenz-Vektoren

Das einfachste Modell der Vektorkodierung besteht darin, den Vektor einfach mit der Häufigkeit jedes Worts auszufüllen, wie es im Dokument vorkommt. In diesem Kodierungsschema wird jedes Dokument als Multiset der Token dargestellt, aus denen es besteht, und der Wert für jede Wortposition im Vektor ist seine Anzahl. Diese Darstellung kann entweder eine reine Zählung (ganzzahlig) sein, wie in Abbildung 4-2 dargestellt, oder eine normalisierte Kodierung, bei der jedes Wort mit der Gesamtzahl der Wörter im Dokument gewichtet wird.

Bag of words encoding uses the frequency of words in the document to encode the vector.
Abbildung 4-2. Token-Häufigkeit als Vektor-Kodierung

Mit NLTK

NLTK erwartet Features als dict Objekt, dessen Schlüssel die Namen der Features sind und dessen Werte boolesch oder numerisch sind. Um unsere Dokumente auf diese Weise zu kodieren, erstellen wir eine vectorize Funktion, die ein Wörterbuch erstellt, dessen Schlüssel die Token im Dokument sind und dessen Werte die Anzahl der Vorkommen des Tokens im Dokument sind.

Mit dem Objekt defaultdict können wir angeben, was das Wörterbuch für einen Schlüssel zurückgeben soll, der ihm noch nicht zugewiesen wurde. Indem wir defaultdict(int) setzen, legen wir fest, dass 0 zurückgegeben werden soll, und erstellen so ein einfaches Zählwörterbuch. Mit der letzten Codezeile map können wir diese Funktion auf jedes Element im Korpus anwenden und so eine Iterable von vektorisierten Dokumenten erstellen.

from collections import defaultdict

def vectorize(doc):
    features = defaultdict(int)
    for token in tokenize(doc):
        features[token] += 1
    return features

vectors = map(vectorize, corpus)

In Scikit-Learn

Der CountVectorizer Transformator aus dem sklearn.feature_extraction Modell hat seine eigenen internen Tokenisierungs- und Normalisierungsmethoden. Die Methode fit des Vektorisierers erwartet eine iterable oder eine Liste von Zeichenketten oder Dateiobjekten und erstellt ein Wörterbuch des Vokabulars auf dem Korpus. Beim Aufruf von transform wird jedes einzelne Dokument in ein spärliches Array umgewandelt, dessen Index-Tupel die Zeile (die Dokument-ID) und die Token-ID aus dem Wörterbuch ist und dessen Wert die Anzahl ist:

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer()
vectors = vectorizer.fit_transform(corpus)
Hinweis

Vektoren können extrem spärlich werden, insbesondere wenn die Vokabulare größer werden, was sich erheblich auf die Geschwindigkeit und Leistung von Machine-Learning-Modellen auswirken kann. Für sehr große Korpora empfiehlt sich die Verwendung von Scikit-Learn HashingVectorizer, das einen Hash-Trick verwendet, um die Zuordnung von Token-String-Namen zu Feature-Indizes zu finden. Das bedeutet, dass es sehr wenig Speicherplatz benötigt und für große Datensätze skaliert werden kann, da nicht das gesamte Vokabular gespeichert werden muss und schneller gepickt und angepasst werden kann, da es keinen Zustand gibt. Allerdings gibt es keine inverse Transformation (vom Vektor zum Text), es kann zu Kollisionen kommen und es gibt keine inverse Gewichtung der Dokumentenhäufigkeit.

Der Gensim Weg

Der Frequenz-Encoder von Gensim heißt doc2bow. Um doc2bow zu verwenden, erstellen wir zunächst ein Gensim Dictionary, das die Token auf der Grundlage der beobachteten Reihenfolge auf Indizes abbildet (wodurch der Overhead der lexikografischen Sortierung entfällt). Das Wörterbuchobjekt kann geladen oder auf der Festplatte gespeichert werden und implementiert eine doc2bow Bibliothek, die ein mit Token versehenes Dokument akzeptiert und eine dünne Matrix von (id, count) Tupeln zurückgibt, wobei id die Kennung des Tokens im Wörterbuch ist. Da die Methode doc2bow nur ein einziges Dokument annimmt, verwenden wir das Listenverständnis, um den gesamten Korpus wiederherzustellen, indem wir die tokenisierten Dokumente in den Speicher laden, um unseren Generator nicht zu erschöpfen:

import gensim

corpus  = [tokenize(doc) for doc in corpus]
id2word = gensim.corpora.Dictionary(corpus)
vectors = [
    id2word.doc2bow(doc) for doc in corpus
]

One-Hot-Codierung

Da sie die Grammatik und die relative Position der Wörter in den Dokumenten außer Acht lassen, leiden die frequenzbasierten Kodierungsmethoden unter dem langen Schwanz oder der Zipf'schen Verteilung, die die natürliche Sprache kennzeichnet. Das hat zur Folge, dass Token, die sehr häufig vorkommen, um Größenordnungen "bedeutender" sind als andere, weniger häufig vorkommende Token. Dies kann erhebliche Auswirkungen auf einige Modelle (z. B. verallgemeinerte lineare Modelle) haben, die normalverteilte Merkmale erwarten.

Eine Lösung für dieses Problem ist die One-Hot-Kodierung, eine boolesche Vektorkodierungsmethode, die einen bestimmten Vektorindex mit dem Wert true (1) markiert, wenn das Token im Dokument vorhanden ist, und false (0), wenn es nicht vorhanden ist. Mit anderen Worten: Jedes Element eines One-Hot-Vektors spiegelt entweder das Vorhandensein oder das Nichtvorhandensein des Tokens im beschriebenen Text wider (siehe Abbildung 4-3).

Each element of a one-hot encoded vector reflects the presence or absence of the token in the described text.
Abbildung 4-3. One-Hot-Codierung

Die One-Hot-Kodierung reduziert das Problem der unausgewogenen Verteilung der Token und vereinfacht ein Dokument auf seine Bestandteile. Diese Reduktion ist am effektivsten bei sehr kleinen Dokumenten (Sätzen, Tweets), die nicht sehr viele sich wiederholende Elemente enthalten, und wird in der Regel auf Modelle angewendet, die sehr gute Glättungseigenschaften haben. Die One-Hot-Codierung wird auch häufig in künstlichen neuronalen Netzen verwendet, deren Aktivierungsfunktionen eine Eingabe im diskreten Bereich von [0,1] oder [-1,1] erfordern.

Mit NLTK

Die NLTK-Implementierung der One-Hot-Codierung ist ein Wörterbuch, dessen Schlüssel Token sind und dessen Wert True ist:

def vectorize(doc):
    return {
        token: True
        for token in doc
    }

vectors = map(vectorize, corpus)

Wörterbücher fungieren im Fall von NLTK als einfache spärliche Matrizen, da es nicht notwendig ist, jedes fehlende Wort zu markieren False. Zusätzlich zu den booleschen Wörterbuchwerten kann auch ein ganzzahliger Wert verwendet werden: 1 für vorhanden und 0 für abwesend.

In Scikit-Learn

In Scikit-Learn wird die One-Hot-Kodierung mit dem Binarizer Transformator im preprocessing Modul implementiert. Da Binarizer nur numerische Daten akzeptiert, müssen die Textdaten vor der One-Hot-Codierung mit CountVectorizer in einen numerischen Raum umgewandelt werden. Die Klasse Binarizer verwendet einen Schwellenwert (standardmäßig 0), so dass alle Werte des Vektors, die kleiner oder gleich dem Schwellenwert sind, auf Null gesetzt werden, während die Werte, die größer als der Schwellenwert sind, auf 1 gesetzt werden. Daher werden standardmäßig alle Binarizer alle Frequenzwerte in 1 um, wobei die Frequenzen mit dem Wert Null erhalten bleiben.

from sklearn.preprocessing import Binarizer

freq   = CountVectorizer()
corpus = freq.fit_transform(corpus)

onehot = Binarizer()
corpus = onehot.fit_transform(corpus.toarray())

Die Methode corpus.toarray() ist optional; sie wandelt die spärliche Matrixdarstellung in eine dichte um. In Korpora mit einem großen Wortschatz ist die spärliche Matrixdarstellung viel besser. Beachte, dass wir auch CountVectorizer(binary=True) verwenden könnten, um eine One-Hot-Kodierung zu erreichen, die Binarizer überflüssig macht.

Vorsicht

Trotz seines Namens ist der Transformator OneHotEncoder im Modul sklearn.preprocessing nicht genau die richtige Lösung für diese Aufgabe. OneHotEncoder behandelt jede Vektorkomponente (Spalte) als unabhängige kategoriale Variable und erweitert die Dimensionalität des Vektors für jeden beobachteten Wert in jeder Spalte. In diesem Fall würden die Komponenten (sight, 0) und (sight, 1) als zwei kategoriale Dimensionen und nicht als eine einzige binär kodierte Vektorkomponente behandelt werden.

Der Gensim Weg

Gensim verfügt zwar nicht über einen speziellen One-Hot-Encoder, aber die Methode doc2bow gibt eine Liste von Tupeln zurück, die wir im laufenden Betrieb verwalten können. Indem wir den Code aus dem Beispiel für die Frequenzvektorisierung in Gensim im vorigen Abschnitt erweitern, können wir unsere Vektoren mit unserem id2word Wörterbuch in einem Rutsch kodieren. Um unser vectors zu erhalten, wandelt ein inneres Listenverständnis die von der Methode doc2bow zurückgegebene Liste von Tupeln in eine Liste von (token_id, 1) Tupeln um und das äußere Verständnis wendet diesen Konverter auf alle Dokumente im Korpus an:

corpus  = [tokenize(doc) for doc in corpus]
id2word = gensim.corpora.Dictionary(corpus)
vectors = [
    [(token[0], 1) for token in id2word.doc2bow(doc)]
    for doc in corpus
]

Die One-Hot-Kodierung stellt Ähnlichkeit und Unterschiede auf Dokumentenebene dar, aber da alle Wörter gleich weit entfernt sind, kann sie die Ähnlichkeit pro Wort nicht kodieren. Da alle Wörter gleich weit entfernt sind, wird die Wortform unglaublich wichtig; die Token "versuchen" und "versuchen" werden gleich weit von nicht verwandten Token wie "rot" oder "Fahrrad" entfernt sein! Die Normalisierung von Token auf eine einzige Wortklasse, entweder durch Stemming oder Lemmatisierung, auf die wir später in diesem Kapitel eingehen werden, stellt sicher, dass verschiedene Formen von Token, die Pluralität, Groß- und Kleinschreibung, Geschlecht, Kardinalität, Zeitform usw. einschließen, als einzelne Vektorkomponenten behandelt werden, wodurch der Merkmalsraum reduziert und die Modelle leistungsfähiger werden.

Begriffshäufigkeit - Inverse Dokumenthäufigkeit

Die Bag-of-Words-Darstellungen, die wir bisher untersucht haben, beschreiben ein Dokument nur für sich allein, ohne den Kontext des Korpus zu berücksichtigen. Ein besserer Ansatz wäre es, die relative Häufigkeit oder Seltenheit von Token in einem Dokument im Vergleich zu ihrer Häufigkeit in anderen Dokumenten zu betrachten. Die zentrale Erkenntnis ist, dass die Bedeutung am ehesten in den selteneren Begriffen eines Dokuments kodiert ist. In einem Sporttextkorpus tauchen beispielsweise Token wie "Schiedsrichter", "Base" und "Unterstand" häufiger in Dokumenten auf, die sich mit Baseball beschäftigen, während andere Token, die im gesamten Korpus häufig vorkommen, wie "Run", "Score" und "Play", weniger wichtig sind.

Die TF-IDF-Kodierung ( Term Frequency-Inverse Document Frequency) normalisiert die Häufigkeit von Token in einem Dokument im Vergleich zum Rest des Korpus. Dieser Kodierungsansatz hebt Begriffe hervor, die für eine bestimmte Instanz sehr relevant sind, wie in Abbildung 4-4 zu sehen ist, wo das Token studio eine höhere Relevanz für dieses Dokument hat, da es nur dort erscheint.

Term frequency-inverse document frequency encodes documents relative to it's most unique and relevant terms.
Abbildung 4-4. TF-IDF-Kodierung

TF-IDF wird für jeden Begriff berechnet, so dass die Relevanz eines Tokens für ein Dokument anhand der skalierten Häufigkeit des Auftretens des Begriffs in dem Dokument gemessen wird, normiert durch die Umkehrung der skalierten Häufigkeit des Begriffs im gesamten Korpus.

Mit NLTK

Um Text auf diese Weise mit NLTK zu vektorisieren, verwenden wir die Klasse TextCollection, einen Wrapper für eine Liste von Texten oder einen Korpus, der aus einem oder mehreren Texten besteht. Diese Klasse unterstützt das Zählen, Konkordanzieren, Entdecken von Kollokationen und vor allem das Berechnen von tf_idf.

Da TF-IDF den gesamten Korpus benötigt, akzeptiert unsere neue Version von vectorize nicht ein einzelnes Dokument, sondern alle Dokumente. Nach der Anwendung unserer Tokenisierungsfunktion und der Erstellung der Textsammlung durchläuft die Funktion jedes Dokument im Korpus und liefert ein Wörterbuch, dessen Schlüssel die Terme sind und dessen Werte den TF-IDF-Score für den Term in diesem bestimmten Dokument darstellen.

from nltk.text import TextCollection

def vectorize(corpus):
    corpus = [tokenize(doc) for doc in corpus]
    texts  = TextCollection(corpus)

    for doc in corpus:
        yield {
            term: texts.tf_idf(term, doc)
            for term in doc
        }

In Scikit-Learn

Scikit-Learn bietet einen Transformator namens TfidfVectorizer im Modul feature_extraction.text zur Vektorisierung von Dokumenten mit TF-IDF-Scores. Unter der Haube verwendet TfidfVectorizer den CountVectorizer Schätzer, den wir zur Erstellung der Bag-of-Words-Kodierung verwendet haben, um das Vorkommen von Token zu zählen, gefolgt von einem TfidfTransformer, der die Anzahl der Vorkommen mit der inversen Dokumenthäufigkeit normalisiert.

Als Eingabe für TfidfVectorizer wird eine Folge von Dateinamen, dateiähnlichen Objekten oder Zeichenketten erwartet, die eine Sammlung von Rohdokumenten enthalten, ähnlich wie bei CountVectorizer. Daher wird eine Standard-Tokenisierung und Vorverarbeitungsmethode angewendet, sofern keine anderen Funktionen angegeben werden. Der Vektorisierer gibt eine spärliche Matrixdarstellung in Form von ((doc, term), tfidf) zurück, wobei jeder Schlüssel ein Dokument- und Termpaar und der Wert der TF-IDF-Score ist.

from sklearn.feature_extraction.text import TfidfVectorizer

tfidf  = TfidfVectorizer()
corpus = tfidf.fit_transform(corpus)

Der Gensim Weg

In Gensim ähnelt die Datenstruktur TfidfModel dem Dictionary Objekt, indem sie eine Zuordnung von Begriffen und ihren Vektorpositionen in der Reihenfolge speichert, in der sie beobachtet werden, aber zusätzlich die Korpushäufigkeit dieser Begriffe speichert, um Dokumente bei Bedarf vektorisieren zu können. Wie zuvor können wir mit Gensim unsere eigene Tokenisierungsmethode anwenden, die einen Korpus erwartet, der eine Liste von Token-Listen ist. Zunächst erstellen wir das Lexikon und verwenden es, um die TfidfModel zu instanziieren, die die normalisierte inverse Dokumentenhäufigkeit berechnet. Anschließend können wir die TF-IDF-Darstellung für jeden Vektor mit einer getitem wörterbuchähnlichen Syntax abrufen, nachdem wir die doc2bow Methode auf jedes Dokument mit Hilfe des Lexikons angewendet haben.

corpus  = [tokenize(doc) for doc in corpus]
lexicon = gensim.corpora.Dictionary(corpus)
tfidf   = gensim.models.TfidfModel(dictionary=lexicon, normalize=True)
vectors = [tfidf[lexicon.doc2bow(doc)] for doc in corpus]

Gensim bietet Hilfsfunktionen, um Wörterbücher und Modelle in einem kompakten Format auf die Festplatte zu schreiben. Das bedeutet, dass du sowohl das TF-IDF-Modell als auch das Lexikon bequem auf der Festplatte speichern kannst, um sie später zu laden und neue Dokumente zu vektorisieren. Es ist möglich (wenn auch etwas aufwändiger), das gleiche Ergebnis mit dem Modul pickle in Kombination mit Scikit-Learn zu erzielen. So speicherst du ein Gensim-Modell auf der Festplatte:

lexicon.save_as_text('lexicon.txt', sort_by_word=True)
tfidf.save('tfidf.pkl')

Dadurch wird das Lexikon als lexikografisch sortierte Textdatei und das TF-IDF-Modell als gepickte Sparse-Matrix gespeichert. Beachte, dass das Objekt Dictionary mit der Methode save auch kompakter in einem Binärformat gespeichert werden kann, aber save_as_text ermöglicht eine einfache Inspektion des Wörterbuchs für spätere Arbeiten. So lädst du die Modelle von der Festplatte:

lexicon = gensim.corpora.Dictionary.load_from_text('lexicon.txt')
tfidf = gensim.models.TfidfModel.load('tfidf.pkl')

Ein Vorteil von TF-IDF ist, dass es auf natürliche Weise das Problem der Stoppwörter löst, also der Wörter, die höchstwahrscheinlich in allen Dokumenten des Korpus vorkommen (z. B. "a", "der", "von" usw.) und daher bei diesem Kodierungsschema nur sehr geringe Gewichte erhalten. Dadurch wird das TF-IDF-Modell auf mäßig seltene Wörter ausgerichtet. Aus diesem Grund wird TF-IDF häufig für Bag-of-Words-Modelle verwendet und ist ein hervorragender Ausgangspunkt für die meisten Textanalysen.

Verteilte Repräsentation

Mit der Häufigkeits-, One-Hot- und TF-IDF-Kodierung können wir Dokumente in einen Vektorraum einordnen. Oft ist es sinnvoll, auch die Ähnlichkeiten zwischen Dokumenten im Kontext desselben Vektorraums zu kodieren. Leider erzeugen diese Vektorisierungsmethoden Dokumentvektoren mit nicht-negativen Elementen, was bedeutet, dass wir Dokumente, die keine gemeinsamen Terme haben, nicht vergleichen können (denn zwei Vektoren mit einem Kosinusabstand von 1 werden als weit voneinander entfernt angesehen, selbst wenn sie semantisch ähnlich sind).

Wenn die Ähnlichkeit von Dokumenten im Kontext einer Anwendung wichtig ist, kodieren wir den Text stattdessen entlang einer kontinuierlichen Skala mit einer verteilten Darstellung, wie in Abbildung 4-5 gezeigt. Das bedeutet, dass der resultierende Dokumentenvektor keine einfache Abbildung der Token-Position auf die Token-Bewertung ist. Stattdessen wird das Dokument in einem Merkmalsraum dargestellt, der zur Darstellung der Wortähnlichkeit eingebettet wurde. Die Komplexität dieses Raums (und die daraus resultierende Vektorlänge) ist das Produkt der Art und Weise, wie die Zuordnung zu dieser Darstellung gelernt wird. Die Komplexität dieses Raums (und die daraus resultierende Vektorlänge) ist das Produkt der Art und Weise, wie diese Repräsentation trainiert wird, und hängt nicht direkt mit dem Dokument selbst zusammen.

A distributed representation allots weight continuously along a vector to encode information about a word.
Abbildung 4-5. Verteilte Darstellung

Word2vec, das von einem Forscherteam bei Google unter der Leitung von Tomáš Mikolov entwickelt wurde, implementiert ein Modell zur Einbettung von Wörtern, mit dem wir diese Art von verteilten Repräsentationen erstellen können. Der word2vec-Algorithmus trainiert Wortrepräsentationen, die entweder auf einem kontinuierlichen Bag-of-Words- (CBOW) oder einem Skip-Gram-Modell basieren, so dass Wörter auf der Grundlage ihres Kontexts zusammen mit ähnlichen Wörtern im Raum eingebettet werden. Die Gensim-Implementierung verwendet zum Beispiel ein Feedforward-Netzwerk.

Der doc2vec1 Algorithmus ist eine Erweiterung von word2vec. Er schlägt einen Absatzvektorvor - einenunüberwachten Algorithmus, der Merkmalsrepräsentationen fester Länge aus Dokumenten variabler Länge lernt. Diese Repräsentation versucht, die semantischen Eigenschaften der Wörter zu übernehmen, so dass "rot" und "bunt" einander ähnlicher sind als "Fluss" oder "Regierung". Außerdem berücksichtigt der Absatzvektor die Reihenfolge der Wörter innerhalb eines engen Kontexts, ähnlich wie ein n-gram Modell. Das kombinierte Ergebnis ist viel effektiver als ein Bag-of-Words- oder Bag-of-n-Grams-Modell, weil es besser verallgemeinert und eine geringere Dimensionalität hat, aber immer noch eine feste Länge aufweist, so dass es in gängigen maschinellen Lernalgorithmen verwendet werden kann.

Der Gensim Weg

Weder NLTK noch Scikit-Learn bieten Implementierungen für diese Art von Worteinbettungen. Die Gensim-Implementierung ermöglicht es den Nutzern, sowohl word2vec- als auch doc2vec-Modelle auf benutzerdefinierten Korpora zu trainieren, und enthält außerdem ein Modell, das auf dem Google-Nachrichtenkorpus trainiert wurde.

Hinweis

Um die trainierten Modelle von Gensim zu verwenden, musst du die 1,5 GB große Modell-Bin-Datei herunterladen. Für Anwendungen, die extrem leichtgewichtige Abhängigkeiten benötigen (z. B. wenn sie auf einer AWS-Lambda-Instanz laufen müssen), ist das möglicherweise nicht praktikabel.

Wir können unser eigenes Modell wie folgt trainieren. Zuerst verwenden wir eine Listenverarbeitung, um unseren Korpus in den Speicher zu laden. (Gensim unterstützt Streaming, aber so können wir vermeiden, dass der Generator erschöpft wird.) Als Nächstes erstellen wir eine Liste von TaggedDocument Objekten, die die LabeledSentence und damit die verteilte Darstellung von word2vec erweitern. TaggedDocument Objekte bestehen aus Wörtern und Tags. Wir können das getaggte Dokument mit der Liste der Token zusammen mit einem einzelnen Tag instanziieren, das die Instanz eindeutig identifiziert. In diesem Beispiel haben wir jedes Dokument als "d{}".format(idx) gekennzeichnet, z. B. d0, d1, d2 und so weiter.

Sobald wir eine Liste der getaggten Dokumente haben, instanziieren wir das Doc2Vec Modell und legen die Größe des Vektors sowie die Mindestanzahl fest, die alle Token ignoriert, deren Häufigkeit unter dieser Zahl liegt. Der Parameter size hat in der Regel eine geringere Dimensionalität als 5; wir haben eine so kleine Zahl nur zu Demonstrationszwecken gewählt. Wir setzen auch den Parameter min_count auf Null, um sicherzustellen, dass alle Token berücksichtigt werden, aber in der Regel wird dieser Wert zwischen 3 und 5 gesetzt, je nachdem, wie viele Informationen das Modell erfassen soll. Sobald das Modell instanziiert ist, wird ein unbeaufsichtigtes neuronales Netz trainiert, um die Vektordarstellungen zu lernen, auf die dann über die Eigenschaft docvecs zugegriffen werden kann.

from gensim.models.doc2vec import TaggedDocument, Doc2Vec

corpus = [list(tokenize(doc)) for doc in corpus]
corpus = [
    TaggedDocument(words, ['d{}'.format(idx)])
    for idx, words in enumerate(corpus)
]

model = Doc2Vec(corpus, size=5, min_count=0)
print(model.docvecs[0])
# [ 0.01797447 -0.01509272  0.0731937   0.06814702 -0.0846546 ]

Verteilte Repräsentationen verbessern die Ergebnisse gegenüber TF-IDF-Modellen erheblich, wenn sie richtig eingesetzt werden. Das Modell selbst kann auf der Festplatte gespeichert und aktiv neu trainiert werden, was es für eine Vielzahl von Anwendungsfällen äußerst flexibel macht. Bei größeren Korpora kann das Training jedoch langsam und speicherintensiv sein, und es ist möglicherweise nicht so gut wie ein TF-IDF-Modell, bei dem die Hauptkomponentenanalyse (PCA) oder die Singulärwertzerlegung (SVD) angewendet wird, um den Merkmalsraum zu reduzieren. Letztendlich ist diese Darstellung jedoch ein Durchbruch, der in den letzten Jahren zu einer dramatischen Verbesserung der Textverarbeitungsmöglichkeiten von Datenprodukten geführt hat.

Auch hier ist die Wahl der Vektorisierungstechnik (wie auch die Bibliotheksimplementierung) in der Regel anwendungsspezifisch, wie in Tabelle 4-1 zusammengefasst ist.

Tabelle 4-1. Überblick über die Methoden der Textvektorisierung
Vektorisierungsmethode Funktion Gut für Überlegungen

Frequenz

Zählt Begriffshäufigkeiten

Bayes'sche Modelle

Die häufigsten Wörter sind nicht immer die informativsten

One-Hot-Codierung

Binarisiert das Auftreten von Begriffen (0, 1)

Neuronale Netze

Alle Wörter sind gleich weit entfernt, daher ist die Normalisierung besonders wichtig

TF-IDF

Normalisiert die Häufigkeit von Begriffen in den Dokumenten

Allgemeiner Zweck

Mäßig häufige Begriffe sind möglicherweise nicht repräsentativ für die Themen des Dokuments

Verteilte Vertretungen

Kontextbasierte, kontinuierliche Begriffsähnlichkeitskodierung

Modellierung komplexerer Beziehungen

Leistungsintensiv; ohne zusätzliche Tools (z. B. Tensorflow) schwer skalierbar

Später in diesem Kapitel werden wir uns mit dem Scikit-Learn Pipeline Objekt beschäftigen, mit dem wir die Vektorisierung zusammen mit späteren Modellierungsphrasen rationalisieren können. Daher bevorzugen wir oft Vektorisierer, die der Scikit-Learn-API entsprechen. Im nächsten Abschnitt werden wir besprechen, wie die API aufgebaut ist, und demonstrieren, wie wir die Vektorisierung in eine komplette Pipeline integrieren, um den Kern einer voll funktionsfähigen (und anpassbaren!) textuellen Machine-Learning-Anwendung zu erstellen.

Die Scikit-Learn API

Scikit-Learn ist eine Erweiterung von SciPy (ein Scikit), deren Hauptzweck darin besteht, Algorithmen für maschinelles Lernen sowie die Werkzeuge und Dienstprogramme bereitzustellen, die für eine erfolgreiche Modellierung erforderlich sind. Der Hauptbeitrag von Scikit-Learn ist eine "API für maschinelles Lernen", die die Implementierungen einer Vielzahl von Modellfamilien in einer einzigen, benutzerfreundlichen Schnittstelle bereitstellt. Das Ergebnis ist, dass mit Scikit-Learn eine Vielzahl von Modellen gleichzeitig trainiert, ausgewertet und verglichen werden kann, um dann das angepasste Modell für Vorhersagen auf neuen Daten zu verwenden. Da Scikit-Learn eine standardisierte API bereitstellt, ist dies mit geringem Aufwand möglich, und die Modelle können durch einfaches Auswechseln einiger Codezeilen erstellt und ausgewertet werden.

Die BaseEstimator-Schnittstelle

Die API selbst ist objektorientiert und beschreibt eine Hierarchie von Schnittstellen für verschiedene Aufgaben des maschinellen Lernens. Die Wurzel der Hierarchie ist ein Estimator, im Großen und Ganzen jedes Objekt, das aus Daten lernen kann. Die wichtigsten Estimator Objekte implementieren Klassifizierer, Regressoren oder Clustering-Algorithmen. Sie können aber auch ein breites Spektrum an Datenmanipulationen beinhalten, von der Dimensionalitätsreduktion bis zur Merkmalsextraktion aus Rohdaten. Estimator dient im Wesentlichen als Schnittstelle, und Klassen, die die Funktionalität von Estimator implementieren, müssen über zwei Methoden verfügen -fit und predict- wie hier gezeigt:

from sklearn.base import BaseEstimator

class Estimator(BaseEstimator):

    def fit(self, X, y=None):
        """
        Accept input data, X, and optional target data, y. Returns self.
        """
        return self

    def predict(self, X):
        """
        Accept input data, X and return a vector of predictions for each row.
        """
        return yhat

Die Methode Estimator.fit legt den Zustand des Schätzers auf der Grundlage der Trainingsdaten X und y fest. Die Trainingsdaten X sollten matrixartig sein - zum Beispiel ein zweidimensionales NumPy-Array der Form (n_samples, n_features) oder ein Pandas-Array DataFrame, dessen Zeilen die Instanzen und dessen Spalten die Merkmale sind. Überwachte Schätzer werden auch mit einem eindimensionalen NumPy-Array y angepasst, das die korrekten Bezeichnungen enthält. Durch den Anpassungsprozess wird der interne Zustand des Schätzers so verändert, dass er bereit oder in der Lage ist, Vorhersagen zu treffen. Dieser Zustand wird in Instanzvariablen gespeichert, die normalerweise mit einem Unterstrich versehen sind (z. B. Estimator.coefs_). Da diese Methode einen internen Zustand verändert, gibt sie self zurück, sodass die Methode verkettet werden kann.

Die Methode Estimator.predict erstellt Vorhersagen unter Verwendung des internen, angepassten Zustands des Modells auf den neuen Daten, X. Die Eingabe für die Methode muss die gleiche Anzahl von Spalten haben wie die Trainingsdaten, die an fit übergeben werden, und kann so viele Zeilen haben, wie Vorhersagen benötigt werden. Diese Methode gibt einen Vektor yhat zurück, der die Vorhersagen für jede Zeile der Eingabedaten enthält.

Hinweis

Durch die Erweiterung von Scikit-Learn's BaseEstimator erhält Estimator automatisch eine fit_predict Methode, mit der du fit und predict in einem einfachen Aufruf kombinieren kannst.

Estimator Objekte haben Parameter (auch Hyperparameter genannt), die festlegen, wie der Anpassungsprozess durchgeführt wird. Diese Parameter werden bei der Instanziierung von festgelegt (und wenn sie nicht angegeben werden, werden sie auf vernünftige Standardwerte gesetzt) und können mit den Methoden und geändert werden, die auch in der Superklasse verfügbar sind. Estimator get_param set_param BaseEstimator

Wir rufen die Scikit-Learn API auf, indem wir das Paket und den Typ des Schätzers angeben. Hier wählen wir die Naive Bayes-Modellfamilie und ein bestimmtes Mitglied der Familie, ein Multinomialmodell (das für die Textklassifizierung geeignet ist). Das Modell wird definiert, wenn die Klasse instanziiert wird und Hyperparameter übergeben werden. Hier übergeben wir einen Alpha-Parameter, der für die additive Glättung verwendet wird, sowie die Prioritätswahrscheinlichkeiten für jede unserer beiden Klassen. Das Modell wird mit bestimmten Daten trainiert (documents und labels) und wird dann zu einem angepassten Modell. Diese grundlegende Verwendung ist für jedes Modell (Estimator) in Scikit-Learn gleich, von Random-Forest-Entscheidungsbaum-Ensembles bis zu logistischen Regressionen und darüber hinaus.

from sklearn.naive_bayes import MultinomialNB

model = MultinomialNB(alpha=0.0, class_prior=[0.4, 0.6])
model.fit(documents, labels)

Erweitern von TransformerMixin

Scikit-Learn spezifiziert auch Dienstprogramme, um maschinelles Lernen auf wiederholbare Weise durchzuführen. Wir können nicht über Scikit-Learn sprechen, ohne auch die Schnittstelle Transformer zu erwähnen. Ein Transformer ist eine besondere Art von Estimator, die einen neuen Datensatz aus einem alten erstellt, und zwar auf der Grundlage von Regeln, die sie aus dem Anpassungsprozess gelernt hat. Die Schnittstelle ist wie folgt aufgebaut:

from sklearn.base import TransformerMixin

class Transfomer(BaseEstimator, TransformerMixin):

    def fit(self, X, y=None):
        """
        Learn how to transform data based on input data, X.
        """
        return self

    def transform(self, X):
        """
        Transform X into a new dataset, Xprime and return it.
        """
        return Xprime

Die Methode Transformer.transform nimmt einen Datensatz und gibt einen neuen Datensatz, X`, mit neuen Werten zurück, die auf dem Transformationsprozess basieren. Scikit-Learn enthält mehrere Transformatoren, darunter solche zur Normalisierung oder Skalierung von Merkmalen, zum Umgang mit fehlenden Werten (Imputation), zur Dimensionalitätsreduktion, zur Extraktion oder Auswahl von Merkmalen oder zur Abbildung von einem Merkmalsraum auf einen anderen.

Obwohl NLTK, Gensim und sogar neuere Textanalysebibliotheken wie SpaCy ihre eigenen internen APIs und Lernmechanismen haben, ist Scikit-Learn aufgrund seines Umfangs und seiner umfassenden Modelle und Methoden für maschinelles Lernen ein wesentlicher Bestandteil des Modellierungsworkflows. Daher schlagen wir vor, die API zu nutzen, um unsere eigenen Transformer und Estimator Objekte zu erstellen, die Methoden aus NLTK und Gensim implementieren. So können wir z. B. Topic Modeling-Schätzer erstellen, die die LDA- und LSA-Modelle von Gensim einschließen (die derzeit nicht in Scikit-Learn enthalten sind), oder Transformatoren erstellen, die die Part-of-Speech-Tagging- und Named-Entity-Chunking-Methoden von NLTK nutzen.

Einen benutzerdefinierten Gensim-Vektorisierungstransformator erstellen

Gensim-Vektorisierungstechniken sind eine interessante Fallstudie, weil Gensim-Korpora so gespeichert und von der Festplatte geladen werden können, dass sie von der Pipeline entkoppelt bleiben. Es ist jedoch möglich, einen eigenen Transformator zu erstellen, der die Gensim-Vektorisierung nutzt. Unser GensimVectorizer Transformator umhüllt ein Gensim Dictionary Objekt, das während fit() erzeugt wurde und dessen doc2bow Methode während transform(). Das Dictionary Objekt (wie auch das TfidfModel) kann gespeichert und von der Festplatte geladen werden, daher nutzt unser Transformator diese Methode, indem er bei der Instanziierung einen Pfad aufnimmt. Wenn eine Datei unter diesem Pfad existiert, wird sie sofort geladen. Außerdem können wir mit der Methode save() unsere Dictionary auf die Festplatte schreiben, was wir in fit() tun können.

Die Methode fit() konstruiert das Dictionary Objekt, indem sie bereits tokenisierte und normalisierte Dokumente an den Dictionary Konstruktor übergibt. Die Dictionary wird dann sofort auf der Festplatte gespeichert, so dass der Transformer ohne erneutes Laden geladen werden kann. Die Methode transform() verwendet die Methode Dictionary.doc2bow, die eine spärliche Darstellung des Dokuments als eine Liste von (token_id, frequency) Tupeln zurückgibt. Diese Darstellung kann jedoch mit Scikit-Learn Probleme bereiten, weshalb wir eine Gensim-Hilfsfunktion, sparse2full, verwenden, um die spärliche Darstellung in ein NumPy-Array umzuwandeln.

import os
from gensim.corpora import Dictionary
from gensim.matutils import sparse2full

class GensimVectorizer(BaseEstimator, TransformerMixin):

    def __init__(self, path=None):
        self.path = path
        self.id2word = None
        self.load()

    def load(self):
        if os.path.exists(self.path):
            self.id2word = Dictionary.load(self.path)

    def save(self):
        self.id2word.save(self.path)

    def fit(self, documents, labels=None):
        self.id2word = Dictionary(documents)
        self.save()
            return self

    def transform(self, documents):
        for document in documents:
            docvec = self.id2word.doc2bow(document)
            yield sparse2full(docvec, len(self.id2word))

Es ist leicht zu erkennen, wie die Vektorisierungsmethoden, die wir weiter oben in diesem Kapitel besprochen haben, von Scikit-Learn-Transformatoren ummantelt werden können. Das gibt uns mehr Flexibilität bei den Ansätzen, die wir wählen, und ermöglicht es uns gleichzeitig, die Hilfsmittel für maschinelles Lernen in jeder Bibliothek zu nutzen. Wir überlassen es dem Leser, dieses Beispiel zu erweitern und TF-IDF und verteilte Repräsentationstransformatoren zu untersuchen, die auf die gleiche Weise implementiert werden.

Erstellen eines benutzerdefinierten Textnormalisierungstransformators

Viele Modellfamilien leiden unter dem "Fluch der Dimensionalität": Je mehr Dimensionen der Merkmalsraum hat, desto spärlicher werden die Daten und desto weniger informativ sind sie für den zugrunde liegenden Entscheidungsraum. Die Textnormalisierung reduziert die Anzahl der Dimensionen und verringert damit die Sparsamkeit. Neben dem einfachen Filtern von Token (Entfernen von Satzzeichen und Stoppwörtern) gibt es zwei Hauptmethoden zur Textnormalisierung: Stemming und Lemmatisierung.

Beim Stemming wird eine Reihe von Regeln (oder ein Modell) verwendet, um eine Zeichenfolge in kleinere Teilzeichen zu zerlegen. Das Ziel ist es, Wortanhängsel (insbesondere Suffixe) zu entfernen, die die Bedeutung verändern. Zum Beispiel wird ein 's' oder 'es' entfernt, das in lateinischen Sprachen in der Regel eine Mehrzahl anzeigt. Bei der Lemmatisierung hingegen wird jedes Token in einem Wörterbuch nachgeschlagen und das kanonische "Hauptwort" im Wörterbuch, das sogenannte Lemma, zurückgegeben. Da die Token auf der Grundlage einer Grundwahrheit gesucht werden, können auch unregelmäßige Fälle und Token mit unterschiedlichen Wortarten behandelt werden. Zum Beispiel sollte das Verb 'gardening' zu 'to garden' lemmatisiert werden, während die Substantive 'garden' und 'gardener' unterschiedliche Lemmata sind. Das Stemming würde alle diese Token in einem einzigen 'garden' Token erfassen.

Stemming und Lemmatisierung haben ihre Vor- und Nachteile. Das Stemming ist schneller, weil wir nur die Wortfolgen zusammenfügen müssen. Bei der Lemmatisierung hingegen muss in einem Wörterbuch oder einer Datenbank nachgeschlagen werden, und es werden Part-of-Speech-Tags verwendet, um das Stammlemma eines Wortes zu identifizieren. Das macht die Lemmatisierung deutlich langsamer als das Stemming, aber auch effektiver.

Um die Textnormalisierung systematisch durchzuführen, schreiben wir einen eigenen Transformator, der diese Teile zusammenfügt. Unsere Klasse TextNormalizer nimmt als Eingabe eine Sprache, die zum Laden der richtigen Stoppwörter aus dem NLTK-Korpus verwendet wird. Wir könnten TextNormalizer auch so anpassen, dass die Benutzer zwischen Stemming und Lemmatisierung wählen können, und die Sprache an SnowballStemmer übergeben. Um fremde Token zu filtern, erstellen wir zwei Methoden. Die erste, is_punct(), prüft, ob jedes Zeichen im Token eine Unicode-Kategorie hat, die mit 'P' beginnt (für Interpunktion); die zweite, is_stopword(), bestimmt, ob das Token in unserem Satz von Stoppwörtern enthalten ist.

import unicodedata
from sklearn.base import BaseEstimator, TransformerMixin

class TextNormalizer(BaseEstimator, TransformerMixin):

    def __init__(self, language='english'):
        self.stopwords  = set(nltk.corpus.stopwords.words(language))
        self.lemmatizer = WordNetLemmatizer()

    def is_punct(self, token):
        return all(
            unicodedata.category(char).startswith('P') for char in token
        )

    def is_stopword(self, token):
        return token.lower() in self.stopwords

Wir können dann eine normalize() Methode hinzufügen, die ein einzelnes Dokument nimmt, das aus einer Liste von Absätzen besteht, die wiederum Listen von Sätzen sind, die wiederum Listen von (token, tag) Tupeln sind - das Datenformat, in das wir in Kapitel 3 rohes HTML vorverarbeitet haben.

    def normalize(self, document):
        return [
            self.lemmatize(token, tag).lower()
            for paragraph in document
            for sentence in paragraph
            for (token, tag) in sentence
            if not self.is_punct(token) and not self.is_stopword(token)
        ]

Diese Methode wendet die Filterfunktionen an, um unerwünschte Token zu entfernen und lemmatisiert sie dann. Die Methode lemmatize() wandelt zunächst die Part-of-Speech-Tags der Penn Treebank, die in der Funktion nltk.pos_tag als Standard-Tag gesetzt sind, in WordNet-Tags um und wählt standardmäßig Substantive aus.

    def lemmatize(self, token, pos_tag):
        tag = {
            'N': wn.NOUN,
            'V': wn.VERB,
            'R': wn.ADV,
            'J': wn.ADJ
        }.get(pos_tag[0], wn.NOUN)

        return self.lemmatizer.lemmatize(token, tag)

Zum Schluss müssen wir noch die Schnittstelle Transformer hinzufügen, damit wir diese Klasse zu einer Scikit-Learn-Pipeline hinzufügen können, die wir im nächsten Abschnitt untersuchen werden:

    def fit(self, X, y=None):
        return self

    def transform(self, documents):
        for document in documents:
            yield self.normalize(document)

Beachte, dass die Textnormalisierung nur eine Methode ist und außerdem NLTK sehr stark nutzt, was deine Anwendung unnötig belasten kann. Andere Optionen sind z. B. das Entfernen von Token, die über oder unter einem bestimmten Schwellenwert liegen, oder das Entfernen von Stoppwörtern und die Auswahl der ersten fünf- bis zehntausend häufigsten Wörter. Eine andere Möglichkeit ist, einfach die kumulative Häufigkeit zu berechnen und nur Wörter auszuwählen, die 10-50% der kumulativen Häufigkeitsverteilung enthalten. Mit diesen Methoden können wir sowohl die sehr seltenen Hapaxe (Begriffe, die nur einmal vorkommen) als auch die häufigsten Wörter ignorieren und so die potenziell aussagekräftigsten Begriffe im Korpus identifizieren.

Vorsicht

Der Vorgang der Textnormalisierung sollte optional sein und sorgfältig angewendet werden, denn er ist destruktiv, da er Informationen entfernt. Groß- und Kleinschreibung, Zeichensetzung, Stoppwörter und unterschiedliche Wortkonstruktionen sind für das Verständnis von Sprache entscheidend. Einige Modelle benötigen Indikatoren wie die Groß- und Kleinschreibung. Zum Beispiel ein Named-Entity-Recognition-Klassifikator, weil im Englischen Eigennamen großgeschrieben werden.

Ein alternativer Ansatz ist die Dimensionalitätsreduktion mit der Hauptkomponentenanalyse (PCA) oder der Singulärwertzerlegung (SVD), um den Merkmalsraum auf der Grundlage der Worthäufigkeit auf eine bestimmte Dimensionalität (z. B. fünf- oder zehntausend Dimensionen) zu reduzieren. Diese Transformatoren müssten nach einem Vektorisierungs-Transformator angewandt werden und würden bewirken, dass Wörter, die sich ähneln, im gleichen Vektor Raum zusammengeführt werden.

Pipelines

Der Prozess des maschinellen Lernens kombiniert oft eine Reihe von Transformatoren mit den Rohdaten, die den Datensatz Schritt für Schritt umwandeln, bis er an die Anpassungsmethode eines endgültigen Schätzers übergeben wird. Aber wenn wir unsere Dokumente nicht genau so vektorisieren, erhalten wir am Ende falsche oder zumindest unverständliche Ergebnisse. Das Scikit-Learn Pipeline Objekt ist die Lösung für dieses Dilemma.

Pipeline Objekte ermöglichen es uns, eine Reihe von Transformatoren zu integrieren, die Normalisierung, Vektorisierung und Merkmalsanalyse in einem einzigen, klar definierten Mechanismus kombinieren. Wie in Abbildung 4-6 dargestellt, bewegen die Objekte von Pipeline die Daten von einem Loader (einem Objekt, das unsere CorpusReader aus Kapitel 2 umhüllt) zu den Mechanismen der Merkmalsextraktion und schließlich zu einem Estimator-Objekt, das unsere Vorhersagemodelle implementiert. Pipelines sind gerichtete azyklische Graphen (DAGs), die einfache lineare Ketten von Transformatoren oder beliebig komplexe Verzweigungs- und Verbindungspfade sein können.

Pipelines implement a DAG of data from data loading through feature extraction to a final estimator. Pipelines can be arbitrarily complex or simple linear structures.
Abbildung 4-6. Pipelines für Textvektorisierung und Merkmalsextraktion

Pipeline-Grundlagen

Der Zweck von Pipeline ist es, mehrere Schätzer, die eine festgelegte Abfolge von Schritten darstellen, miteinander zu einer Einheit zu verknüpfen. Alle Schätzer in der Pipeline, mit Ausnahme des letzten, müssen Transformatoren sein, d. h. sie müssen die Methode transform implementieren, während der letzte Schätzer von beliebigem Typ sein kann, einschließlich prädiktiver Schätzer. Pipelines bieten Komfort: fit und transform können für einzelne Eingaben für mehrere Objekte gleichzeitig aufgerufen werden. Pipelines bieten außerdem eine einzige Schnittstelle für die Rastersuche nach mehreren Schätzern auf einmal. Vor allem aber ermöglichen Pipelines die Operationalisierung von Textmodellen, indem sie eine Vektorisierungsmethode mit einem prädiktiven Modell verbinden.

Pipelines werden erstellt, indem eine Liste von (key, value) Paaren beschrieben wird, wobei key eine Zeichenkette ist, die den Schritt benennt, und value das Schätzerobjekt. Pipelines können entweder mit der Hilfsfunktion make_pipeline erstellt werden, die die Namen der Schritte automatisch festlegt, oder indem sie direkt angegeben werden. Im Allgemeinen ist es besser, die Schritte direkt anzugeben, um eine gute Benutzerdokumentation zu erhalten, während make_pipeline häufiger für die automatische Erstellung von Pipelines verwendet wird.

Pipeline Objekte sind ein Scikit-Learn-spezifisches Hilfsmittel, aber sie sind auch der entscheidende Integrationspunkt mit NLTK und Gensim. Hier ist ein Beispiel, das die Objekte TextNormalizer und GensimVectorizer, die wir im letzten Abschnitt erstellt haben, im Vorfeld eines Bayes'schen Modells miteinander verbindet. Mithilfe der Transformer API, die wir bereits in diesem Kapitel besprochen haben, können wir TextNormalizer verwenden, um NLTK CorpusReader Objekte zu verpacken und die Vorverarbeitung und linguistische Merkmalsextraktion durchzuführen. Unser GensimVectorizer ist für die Vektorisierung zuständig und Scikit-Learn für die Integration über Pipelines, Dienstprogramme wie die Kreuzvalidierung und die vielen Modelle, die wir verwenden werden, von Naive Bayes bis zur logistischen Regression.

from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB

model = Pipeline([
    ('normalizer', TextNormalizer()),
    ('vectorizer', GensimVectorizer()),
    ('bayes', MultinomialNB()),
])

Die Pipeline kann dann als eine einzige Instanz eines vollständigen Modells verwendet werden. Der Aufruf von model.fit ist dasselbe wie der Aufruf von fit für jeden Schätzer der Reihe nach, wobei die Eingabe transformiert und an den nächsten Schritt weitergegeben wird. Andere Methoden wie fit_transform verhalten sich ähnlich. Die Pipeline enthält auch alle Methoden, die der letzte Schätzer in der Pipeline hat. Wenn der letzte Schätzer ein Transformator ist, ist es auch die Pipeline. Wenn der letzte Schätzer ein Klassifikator ist, wie im obigen Beispiel, dann verfügt die Pipeline auch über die Methoden predict und score, so dass das gesamte Modell als Klassifikator verwendet werden kann.

Die Schätzer in der Pipeline werden in einer Liste gespeichert und können über einen Index aufgerufen werden. Zum Beispiel gibt model.steps[1] das Tupel ('vectorizer', GensimVectorizer​(path=None)). In der Regel werden die Schätzer jedoch über ihre Namen identifiziert, indem man die Wörterbuch-Eigenschaft named_steps des Pipeline Objekts verwendet. Der einfachste Weg, auf das Vorhersagemodell zuzugreifen, ist die Verwendung von model.named_steps["bayes"] und die direkte Abfrage des Schätzers.

Rastersuche für Hyperparameter-Optimierung

In Kapitel 5 werden wir mehr über Modellabstimmung und Iteration sprechen, aber jetzt stellen wir einfach eine Erweiterung von Pipeline, GridSearch, vor, die für die Hyperparameter-Optimierung nützlich ist. Mit der Rastersuche können die Parameter aller Schätzer in der Pipeline so verändert werden, als wäre sie ein einziges Objekt. Um auf die Attribute der Schätzer zuzugreifen, würdest du die Pipeline-Methoden set_params oder get_params mit einer Dunderscore-Darstellung der Schätzer- und Parameternamen wie folgt verwenden: estimator__parameter.

Nehmen wir an, wir wollen nur die Begriffe kodieren, die mindestens dreimal im Korpus vorkommen. Dann können wir Binarizer wie folgt ändern:

model.set_params(onehot__threshold=3.0)

Nach diesem Prinzip können wir eine Rastersuche durchführen, indem wir die Suchparameter mit der dunderscore-Parametersyntax als Raster definieren. Betrachten wir die folgende Grid-Suche, um das beste ein-hot-kodierte Bayes'sche Textklassifizierungsmodell zu ermitteln:

from sklearn.model_selection import GridSearchCV

search = GridSearchCV(model, param_grid={
    'count__analyzer': ['word', 'char', 'char_wb'],
    'count__ngram_range': [(1,1), (1,2), (1,3), (1,4), (1,5), (2,3)],
    'onehot__threshold': [0.0, 1.0, 2.0, 3.0],
    'bayes__alpha': [0.0, 1.0],
})

Die Suche gibt drei Möglichkeiten für den Parameter CountVectorizer analyzer vor (Erstellung von N-Grammen an Wortgrenzen, Zeichengrenzen oder nur an Zeichen, die zwischen Wortgrenzen liegen) sowie mehrere Möglichkeiten für die N-Gramm-Bereiche, gegen die Tokenisierung durchgeführt werden soll. Wir legen auch den Schwellenwert für die Binarisierung fest, d. h., das N-Gramm muss eine bestimmte Anzahl von Malen vorkommen, bevor es in das Modell aufgenommen wird. Schließlich legt die Suche zwei Glättungsparameter fest (den Parameter bayes_alpha ): entweder keine Glättung (0,0 hinzufügen) oder Laplacian-Glättung (1,0 hinzufügen).

Bei der Grid-Suche wird für jede Merkmalskombination eine Pipeline unseres Modells instanziiert, dann wird das Modell mithilfe der Kreuzvalidierung bewertet und die beste Merkmalskombination ausgewählt (in diesem Fall die Kombination, die den F1-Score maximiert).

Anreicherung der Merkmalsextraktion mit Feature Unions

Pipelines müssen keine einfachen linearen Abfolgen von Schritten sein, sondern können durch die Implementierung von Feature-Unions beliebig komplex sein. Das FeatureUnion Objekt fasst mehrere Transformator-Objekte zu einem neuen, einzelnen Transformator zusammen, ähnlich wie das Pipline Objekt. Anstatt die Daten jedoch nacheinander durch jeden Transformator anzupassen und zu transformieren, werden sie unabhängig voneinander ausgewertet und die Ergebnisse zu einem zusammengesetzten Vektor verkettet.

Betrachte das in Abbildung 4-7 gezeigte Beispiel. Wir können uns einen HTML-Parser vorstellen, der BeautifulSoup oder eine XML-Bibliothek verwendet, um das HTML zu analysieren und den Textkörper jedes Dokuments zurückzugeben. Anschließend führen wir einen Feature-Engineering-Schritt durch, bei dem Entities und Keyphrases aus den Dokumenten extrahiert und die Ergebnisse in die Feature-Union übertragen werden. Die Häufigkeitskodierung für die Entitäten ist sinnvoller, da sie relativ klein sind, während TF-IDF für die Keyphrasen sinnvoller ist. Die Merkmalsvereinigung verknüpft dann die beiden resultierenden Vektoren so, dass unser Entscheidungsraum vor der logistischen Regression die Wortdimensionen im Titel von den Wortdimensionen im Text trennt.

Feature unions allow arbitrarily complex pipelines by implementing transformer methods in parallel, concatenating the resulting vectors as final output.
Abbildung 4-7. Merkmalsvereinigungen für die Verzweigungsvektorisierung

FeatureUnion Objekte werden auf ähnliche Weise als Pipeline Objekte mit einer Liste von (key, value) Paaren instanziiert, wobei key der Name des Transformators und value das Transformatorobjekt ist. Es gibt auch eine make_union Hilfsfunktion, die automatisch Namen bestimmen kann und ähnlich wie die make_pipeline Hilfsfunktion verwendet wird - für automatische oder generierte Pipelines. Auf die Parameter der Schätzer kann ebenfalls auf die gleiche Weise zugegriffen werden. Um eine Suche in einer Feature-Union zu implementieren, musst du nur den Dunderscore für jeden Transformator in der Feature-Union verschachteln.

Mit den oben erwähnten, nicht implementierten Transformatoren EntityExtractor und KeyphraseExtractor können wir unsere Pipeline wie folgt aufbauen:

from sklearn.pipeline import FeatureUnion
from sklearn.linear_model import LogisticRegression

model = Pipeline([
    ('parser', HTMLParser()),
    ('text_union', FeatureUnion(
        transformer_list = [
            ('entity_feature', Pipeline([
                ('entity_extractor', EntityExtractor()),
                ('entity_vect', CountVectorizer()),
            ])),
            ('keyphrase_feature', Pipeline([
                ('keyphrase_extractor', KeyphraseExtractor()),
                ('keyphrase_vect', TfidfVectorizer()),
            ])),
        ],
        transformer_weights= {
            'entity_feature': 0.6,
            'keyphrase_feature': 0.2,
        }
    )),
    ('clf', LogisticRegression()),
])

Beachte, dass die Objekte HTMLParser, EntityExtractor und KeyphraseExtractor derzeit nicht implementiert sind, aber zur Veranschaulichung verwendet werden. Die Merkmalsvereinigung wird in Bezug auf den Rest der Pipeline nacheinander angepasst, aber jeder Transformator innerhalb der Merkmalsvereinigung wird unabhängig angepasst, was bedeutet, dass jeder Transformator dieselben Daten als Eingabe für die Merkmalsvereinigung sieht. Während der Transformation wird jeder Transformator parallel angewandt und die Vektoren, die sie ausgeben, werden zu einem einzigen größeren Vektor zusammengefügt, der optional gewichtet werden kann, wie in Abbildung 4-8 dargestellt.

In this example, we see the process of extracting entities and keyphrases from the original documents, and then joining them in a feature union ahead of vectorization and modeling.
Abbildung 4-8. Merkmalsextraktion und Vereinigung

In diesem Beispiel gewichten wir den entity_feature Transformator stärker als den keyphrase_feature Transformator. Mithilfe von Kombinationen aus benutzerdefinierten Transformatoren, Feature-Unions und Pipelines ist es möglich, eine unglaublich umfangreiche Feature-Extraktion und Transformation auf wiederholbare Weise zu definieren. Indem wir unsere Methodik in einer einzigen Sequenz zusammenfassen, können wir die Transformationen wiederholbar anwenden, insbesondere auf neue Dokumente, wenn wir Vorhersagen in einer Produktionsumgebung machen wollen.

Fazit

In diesem Kapitel haben wir uns einen Überblick über Vektorisierungstechniken verschafft und ihre Einsatzmöglichkeiten für verschiedene Arten von Daten und verschiedene Algorithmen für maschinelles Lernen betrachtet. In der Praxis ist es am besten, ein Kodierungsschema auf der Grundlage des jeweiligen Problems auszuwählen; bestimmte Methoden sind anderen für bestimmte Aufgaben deutlich überlegen.

Für rekurrente neuronale Netzmodelle ist es zum Beispiel oft besser, eine Ein-Häufigkeits-Kodierung zu verwenden, aber um den Textraum aufzuteilen, könnte man einen kombinierten Vektor für die Dokumentzusammenfassung, den Dokumentenkopf, den Textkörper usw. erstellen. Die Frequenzkodierung sollte normalisiert werden, aber verschiedene Arten der Frequenzkodierung können probabilistische Methoden wie Bayes'sche Modelle begünstigen. TF-IDF ist eine exzellente Allzweckkodierung und wird oft zuerst bei der Modellierung verwendet, kann aber auch viele Sünden abdecken. Verteilte Repräsentationen sind der neue Trend, aber sie sind leistungsintensiv und schwer zu skalieren.

Bag-of-Words-Modelle haben eine sehr hohe Dimensionalität, d.h. der Raum ist extrem dünn, was zu Schwierigkeiten bei der Generalisierung des Datenraums führt. Wortreihenfolge, Grammatik und andere strukturelle Merkmale gehen von Natur aus verloren, und es ist schwierig, Wissen (z. B. lexikalische Ressourcen, ontologische Kodierungen) in den Lernprozess einzubringen. Lokale Kodierungen (z. B. nicht verteilte Repräsentationen) erfordern viele Stichproben, was zu Übertraining oder Unteranpassung führen kann, aber verteilte Repräsentationen sind komplex und fügen eine zusätzliche Ebene der "Repräsentationsmystik" hinzu.

Letztlich besteht ein Großteil der Arbeit für sprachsensitive Anwendungen in der Analyse domänenspezifischer Merkmale und nicht nur in der einfachen Vektorisierung. Im letzten Abschnitt dieses Kapitels haben wir uns mit der Verwendung von FeatureUnion und Pipeline beschäftigt, um durch die Kombination von Transformatoren sinnvolle Extraktionsmethoden zu entwickeln. Auch in Zukunft wird die Erstellung von Pipelines aus Transformatoren und Schätzern unser wichtigstes Instrument für maschinelles Lernen sein. In Kapitel 5 werden wir uns mit Klassifizierungsmodellen und Anwendungen befassen, in Kapitel 6 dann mit Clustering-Modellen, die in der Textanalyse oft als Topic Modeling bezeichnet werden. In Kapitel 7 werden wir uns mit komplexeren Methoden zur Merkmalsanalyse und -exploration befassen, die uns dabei helfen, unsere vektorbasierten Modelle zu verfeinern und bessere Ergebnisse zu erzielen. Nichtsdestotrotz sind einfache Modelle, die nur die Worthäufigkeiten berücksichtigen, oft sehr erfolgreich. Unserer Erfahrung nach funktioniert ein reines Bag-of-Words-Modell in etwa 85 % der Fälle!

1 Quoc V. Le und Tomas Mikolov, Distributed Representations of Sentences and Documents, (2014) http://bit.ly/2GJBHjZ

Get Angewandte Textanalyse mit Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.