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.
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.
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).
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.
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.
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
)
(
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.
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.
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.
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 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.