Kapitel 4. Text-Klassifizierung

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

Eine der neueren Anwendungen für die binäre Klassifizierung ist die Stimmungsanalyse, bei der eine Textprobe wie eine Produktbewertung, ein Tweet oder ein Kommentar auf einer Website untersucht und auf einer Skala von 0,0 bis 1,0 bewertet wird, wobei 0,0 für eine negative und 1,0 für eine positive Stimmung steht. Eine Bewertung wie "tolles Produkt zu einem tollen Preis" könnte die Note 0,9 erhalten, während "überteuertes Produkt, das kaum funktioniert" die Note 0,1 erhalten könnte. Die Punktzahl gibt die Wahrscheinlichkeit an, dass der Text eine positive Stimmung ausdrückt. Modelle für die Stimmungsanalyse sind algorithmisch schwer zu erstellen, aber mit maschinellem Lernen relativ einfach zu realisieren. Beispiele dafür, wie die Stimmungsanalyse heute in der Wirtschaft eingesetzt wird, findest du in dem Artikel "8 Sentiment Analysis Real-World Use Cases" von Nicholas Bianchi.

Die Stimmungsanalyse ist ein Beispiel für eine Aufgabe, bei der es um die Klassifizierung von Textdaten und nicht von numerischen Daten geht. Da maschinelles Lernen mit Zahlen arbeitet, musst du Text in Zahlen umwandeln, bevor du ein Stimmungsanalysemodell, ein Modell zur Erkennung von Spam-E-Mails oder ein anderes Modell zur Klassifizierung von Text trainierst. Eine gängige Methode ist es, eine Tabelle mit Worthäufigkeiten zu erstellen, die Bag of Words genannt wird. Scikit-Learn bietet Klassen, die dabei helfen. Außerdem unterstützt es die Normalisierung von Text, damit zum Beispiel "geil" und "geil" nicht als zwei verschiedene Wörter zählen.

In diesem Kapitel wird zunächst beschrieben, wie du Text für die Verwendung in Klassifizierungsmodellen vorbereitest. Nachdem du ein Modell zur Stimmungsanalyse erstellt hast, lernst du einen anderen beliebten Lernalgorithmus namens Naive Bayes kennen, der besonders gut mit Text funktioniert, und verwendest ihn, um ein Modell zu erstellen, das zwischen legitimen E-Mails und Spam-Mails unterscheidet. Schließlich lernst du eine mathematische Technik kennen, mit der du die Ähnlichkeit von zwei Textproben messen kannst, und entwickelst damit eine App, die dir Filme empfiehlt, die dir auch gefallen.

Text für die Klassifizierung vorbereiten

Bevor ein Modell zur Klassifizierung von Text trainiert, muss der Text in Zahlen umgewandelt werden, ein Prozess, der als Vektorisierung bekannt ist. In Kapitel 1 wurde die Illustration in Abbildung 4-1 vorgestellt, die eine gängige Technik zur Vektorisierung von Text veranschaulicht. Jede Zeile steht für ein Textbeispiel, z. B. einen Tweet oder eine Filmkritik, und jede Spalte für ein Wort im Trainingstext. Die Zahlen in den Zeilen sind die Anzahl der Wörter, und die letzte Zahl in jeder Zeile ist ein Label: 0 für negativ und 1 für positiv.

Abbildung 4-1. Datensatz für die Stimmungsanalyse

Text wird normalerweise bereinigt, bevor er vektorisiert wird. Beispiele für die Bereinigung sind die Umwandlung von Buchstaben in Kleinbuchstaben (so dass z. B. "Ausgezeichnet" gleichbedeutend mit "exzellent" ist), das Entfernen von Satzzeichen und optional das Entfernen von Stoppwörtern - gebräuchlicheWörter wie das und und, die wahrscheinlich wenig Einfluss auf das Ergebnis haben. Nach der Bereinigung von werden die Sätze in einzelne Wörter unterteilt(tokenisiert) und die Wörter verwendet, um Datensätze wie den in Abbildung 4-1 zu erstellen.

Scikit-Learn hat drei Klassen, die den größten Teil der Arbeit beim Bereinigen und Vektorisieren von Text übernehmen:

CountVectorizer
Erstellt ein Wörterbuch(Vokabular) aus dem Korpus der Wörter im Trainingstext und erzeugt eine Matrix der Wortanzahl wie in Abbildung 4-1
HashingVectorizer
Verwendet Wort-Hashes anstelle eines speicherinternen Vokabulars, um die Wortanzahl zu ermitteln, und ist daher speichereffizienter
TfidfVectorizer
Erstellt ein Wörterbuch aus den ihm zur Verfügung gestellten Wörtern und erzeugt eine Matrix, die der in Abbildung 4-1 ähnelt. Die Matrix enthält jedoch keine ganzzahligen Wortzahlen, sondern TFIDF-Werte (Term Frequency-Inverse Document Frequency) zwischen 0,0 und 1,0, die die relative Bedeutung der einzelnen Wörter widerspiegeln

Alle drei Klassen von sind in der Lage, Text in Kleinbuchstaben umzuwandeln, Satzzeichen zu entfernen, Stoppwörter zu entfernen, Sätze in einzelne Wörter zu zerlegen und mehr. Sie unterstützen auch n-Gramme, d. h. Kombinationen aus zwei oder mehr aufeinanderfolgenden Wörtern (du gibst die Zahl n an), die als ein einziges Wort behandelt werden sollen. Die Idee dahinter ist, dass Wörter wie "Kredit" und " Punktzahl" bedeutungsvoller sein können, wenn sie in einem Satz nebeneinander stehen, als wenn sie weit voneinander entfernt sind. Ohne n-gramswird die relative Nähe der Wörter ignoriert. Der Nachteil der Verwendung von N-Grammen ist, dass sie den Speicherbedarf und die Trainingszeit erhöhen. Mit Bedacht eingesetzt, können sie die Textklassifizierungsmodelle jedoch genauer machen.

Hinweis

Neuronale Netze haben andere, leistungsfähigere Möglichkeiten, die Wortreihenfolge zu berücksichtigen, ohne dass verwandte Wörter nebeneinander stehen müssen. Ein herkömmliches maschinelles Lernmodell kann die Wörter blau und Himmel in dem Satz "Ich mag blau, denn es ist die Farbe des Himmels" nicht miteinander verbinden, aber ein neuronales Netz kann es. In Kapitel 13 werde ich das genauer erläutern.

Hier ist ein Beispiel, das zeigt, was CountVectorizer macht und wie es verwendet wird:

import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer

lines = [
    'Four score and 7 years ago our fathers brought forth,',
    '... a new NATION, conceived in liberty $$$,',
    'and dedicated to the PrOpOsItIoN that all men are created equal',
    'One nation\'s freedom equals #freedom for another $nation!'
]

# Vectorize the lines
vectorizer = CountVectorizer(stop_words='english')
word_matrix = vectorizer.fit_transform(lines)

# Show the resulting word matrix
feature_names = vectorizer.get_feature_names_out()
line_names = [f'Line {(i + 1):d}' for i, _ in enumerate(word_matrix)]

df = pd.DataFrame(data=word_matrix.toarray(), index=line_names,
                  columns=feature_names)

df.head()

Hier ist die Ausgabe:

Der Textkorpus besteht in diesem Fall aus vier Zeichenketten in einer Python-Liste. CountVectorizer hat die Zeichenketten in Wörter zerlegt, Stoppwörter und Symbole entfernt und alle verbleibenden Wörter in Kleinbuchstaben umgewandelt. Diese Wörter bilden die Spalten des Datensatzes und die Zahlen in den Zeilen geben an, wie oft ein bestimmtes Wort in jeder Zeichenkette vorkommt. Der Parameter stop_words='english' weist CountVectorizer an, Stoppwörter mit Hilfe eines eingebauten Wörterbuchs zu entfernen, das mehr als 300 englischsprachige Stoppwörter enthält. Wenn du möchtest, kannst du deine eigene Liste von Stoppwörtern in einer Python-Liste angeben. (Du kannst die Stoppwörter auch drin lassen, das ist oft egal.) Und wenn du mit Text trainierst, der in einer anderen Sprache geschrieben wurde, kannst du Listen mit mehrsprachigen Stoppwörtern aus anderen Python-Bibliotheken wie dem Natural Language Toolkit (NLTK) und Stop-words beziehen.

Beachte in der Ausgabe, dass equal und equals als separate Wörter zählen, obwohl sie eine ähnliche Bedeutung haben. Datenwissenschaftler gehen bei der Vorbereitung von Text für maschinelles Lernen manchmal noch einen Schritt weiter, indem sie Wörter stemmen oder lemmatisieren. Wenn der vorangehende Text gestämmt wäre, würden alle Vorkommen von equals in equal umgewandelt. Scikit bietet keine Unterstützung für Stemming und Lemmatisierung, aber du kannst sie von anderen Bibliotheken wie NLTK erhalten.

CountVectorizer entfernt Interpunktionszeichen, aber keine Zahlen. Es hat die 7 in Zeile 1 ignoriert, weil es einzelne Zeichen ignoriert. Aber wenn du 7 in 777 ändern würdest, würde der Begriff 777 im Vokabular erscheinen. Eine Möglichkeit, das zu beheben, besteht darin, eine Funktion zu definieren, die Zahlen entfernt, und diese über den Parameter preprocessor an CountVectorizer zu übergeben:

import re

def preprocess_text(text):
    return re.sub(r'\d+', '', text).lower()

vectorizer = CountVectorizer(stop_words='english', preprocessor=preprocess_text)
word_matrix = vectorizer.fit_transform(lines)

Beachte den Aufruf von lower, um den Text in Kleinbuchstaben zu konvertieren. CountVectorizer konvertiert den Text nicht in Kleinbuchstaben, wenn du eine Vorverarbeitungsfunktion angibst, also muss die Vorverarbeitungsfunktion ihn selbst konvertieren. Sie entfernt jedoch weiterhin Satzzeichen.

Ein weiterer nützlicher Parameter für CountVectorizer ist min_df, der Wörter ignoriert, die weniger als die angegebene Anzahl von Wörtern vorkommen. Dabei kann es sich um eine ganze Zahl handeln, die eine Mindestanzahl angibt (z. B. Wörter ignorieren, die weniger als fünfmal im Trainingstext vorkommen, min_df=5), oder um einen Fließkommawert zwischen 0,0 und 1,0, der den Mindestprozentsatz der Stichproben angibt, in denen ein Wort vorkommen muss - z. B. Wörter ignorieren, die in weniger als 10 % der Stichproben vorkommen (min_df=0.1). Diese Funktion eignet sich hervorragend, um Wörter herauszufiltern, die wahrscheinlich ohnehin keine Bedeutung haben, und sie reduziert den Speicherbedarf und die Trainingszeit, indem sie die Größe des Vokabulars verringert. Count​Vec⁠tor⁠izer unterstützt auch den Parameter max_df, um Wörter zu eliminieren, die zu häufig vorkommen .

In den vorangegangenen Beispielen wird CountVectorizer verwendet, was dich wahrscheinlich fragen lässt, wann (und warum) du stattdessen HashingVectorizer oder TfidfVectorizer verwenden solltest. HashingVectorizer ist nützlich, wenn du mit großen Datensätzen arbeitest. Anstatt Wörter im Speicher zu speichern, wird jedes Wort gehasht und der Hash als Index in einem Array mit den Wortzahlen verwendet. Es kann daher mehr mit weniger Speicher auskommen und ist sehr nützlich, um die Größe von Vektorisierern zu reduzieren, wenn du sie serialisierst, damit du sie später wiederherstellen kannst - ein Thema, auf das ich in Kapitel 7 näher eingehen werde. Der Nachteil von HashingVectorizer ist, dass du nicht vom vektorisierten Text zum ursprünglichen Text zurückgehen kannst. Count​Vec⁠tor⁠izer kann das und bietet dafür eine inverse_transform Methode.

TfidfVectorizer ist häufig verwendet, um Schlüsselwörter zu extrahieren: ein Dokument oder eine Reihe von Dokumenten zu untersuchen und Schlüsselwörter zu extrahieren, die deren Inhalt charakterisieren. Es weist den Wörtern eine numerische Gewichtung zu, die ihre Bedeutung widerspiegelt, und verwendet zwei Faktoren, um die Gewichtung zu bestimmen: wie oft ein Wort in einzelnen Dokumenten vorkommt und wie oft es in der gesamten Dokumentenmenge vorkommt. Wörter, die häufiger in einzelnen Dokumenten, aber in weniger Dokumenten vorkommen, werden höher gewichtet. Ich werde hier nicht weiter darauf eingehen, aber wenn du neugierig bist, findest du im GitHub-Repository dieses Buches ein Notizbuch, das Tfidf​Vec⁠tor⁠izer verwendet, um Schlüsselwörter aus dem Manuskript von Kapitel 1 zu extrahieren.

Stimmungsanalyse

Um ein Stimmungsanalysemodell zu trainieren, brauchst du einen gelabelten Datensatz. Es gibt mehrere solcher Datensätze, die öffentlich zugänglich sind. Einer davon ist der IMDB-Filmkritikdatensatz, der 25.000 Beispiele negativer und 25.000 Beispiele positiver Kritiken enthält, die auf der Website der Internet Movie Database veröffentlicht wurden. Jede Rezension ist sorgfältig mit einer 0 für negative Stimmung oder einer 1 für positive Stimmung gekennzeichnet. Um zu zeigen, wie die Stimmungsanalyse funktioniert, erstellen wir ein binäres Klassifizierungsmodell und trainieren es mit diesem Datensatz. Als Lernalgorithmus verwenden wir die logistische Regression. Das Ergebnis der Stimmungsanalyse ist einfach die Wahrscheinlichkeit, dass die Eingabe eine positive Stimmung ausdrückt. Diese lässt sich ganz einfach durch den Aufruf der LogisticRegres⁠sionpredict_proba Methode.

Lade zunächst den Datensatz herunter und kopiere ihn in das Unterverzeichnis Data des Verzeichnisses, in dem sich deine Jupyter-Notebooks befinden. Führe dann den folgenden Code in einem Notizbuch aus, um den Datensatz zu laden und die ersten fünf Zeilen anzuzeigen:

import pandas as pd
 
df = pd.read_csv('Data/reviews.csv', encoding='ISO-8859-1')
df.head()

Das Attribut encoding ist notwendig, weil die CSV-Datei die Zeichenkodierung ISO-8859-1 und nicht UTF-8 verwendet. Die Ausgabe sieht wie folgt aus:

Finde heraus, wie viele Zeilen der Datensatz enthält und bestätige, dass keine Werte fehlen:

df.info()

Verwende die folgende Aussage, um zu sehen, wie viele Instanzen es von jeder Klasse gibt (0 für negativ und 1 für positiv):

df.groupby('Sentiment').describe()

Hier ist die Ausgabe:

Es gibt eine gerade Anzahl positiver und negativer Proben, aber in jedem Fall ist die Anzahl der eindeutigen Proben geringer als die Anzahl der Proben für diese Klasse. Das bedeutet, dass der Datensatz doppelte Zeilen enthält, die ein maschinelles Lernmodell verfälschen könnten. Verwende die folgenden Anweisungen, um die doppelten Zeilen zu löschen und erneut auf Ausgewogenheit zu prüfen:

df = df.drop_duplicates()
df.groupby('Sentiment').describe()

Jetzt gibt es keine doppelten Zeilen mehr, und die Anzahl der positiven und negativen Proben ist ungefähr gleich.

Nach verwendest du CountVectorizer, um den Text in der Spalte Text vorzubereiten und zu vektorisieren. Setze min_df auf 20, um Wörter zu ignorieren, die im Trainingstext nur selten vorkommen. Das verringert die Wahrscheinlichkeit von Fehlern, die aus dem Speicher verschwinden, und macht das Modell wahrscheinlich auch genauer. Verwende auch den Parameter ngram_range, damit Count​Vec⁠tor⁠izer nicht nur einzelne Wörter, sondern auch Wortpaare berücksichtigen kann:

from sklearn.feature_extraction.text import CountVectorizer
 
vectorizer = CountVectorizer(ngram_range=(1, 2), stop_words='english',
                             min_df=20)

x = vectorizer.fit_transform(df['Text'])
y = df['Sentiment']

Teile nun den Datensatz für Training und Tests auf. Wir verwenden eine 50/50-Aufteilung, da es insgesamt fast 50.000 Proben gibt:

from sklearn.model_selection import train_test_split
 
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.5,
                                                    random_state=0)

Der nächste Schritt besteht darin, einen Klassifikator zu trainieren. Wir verwenden die Klasse LogisticRegression von Scikit, die eine logistische Regression verwendet, um ein Modell auf die Daten anzuwenden:

from sklearn.linear_model import LogisticRegression
 
model = LogisticRegression(max_iter=1000, random_state=0)
model.fit(x_train, y_train)

Validiere das trainierte Modell mit den 50 % des Datensatzes, die zum Testen vorgesehen sind, und zeige die Ergebnisse in einer Konfusionsmatrix:

%matplotlib inline
from sklearn.metrics import ConfusionMatrixDisplay as cmd

cmd.from_estimator(model, x_test, y_test,
                   display_labels=['Negative', 'Positive'],
                   cmap='Blues', xticks_rotation='vertical')

Die Konfusionsmatrix zeigt, dass das Modell 10.795 negative Bewertungen richtig identifiziert und 1.574 davon falsch klassifiziert hat. Es identifizierte 10.966 positive Bewertungen richtig und lag 1.456 Mal falsch:

Jetzt kommt der spaßige Teil: die Analyse des Textes auf seine Stimmung. Verwende die folgenden Aussagen, um eine Stimmungsbewertung für den Satz "Die langen Schlangen und der schlechte Kundenservice haben mich wirklich abgeschreckt" zu erstellen:

text = 'The long lines and poor customer service really turned me off'
model.predict_proba(vectorizer.transform([text]))[0][1]

Hier ist die Ausgabe:

0.09183447847778639

Jetzt mach das Gleiche für "Das Essen war großartig und der Service war ausgezeichnet!":

text = 'The food was great and the service was excellent!'
model.predict_proba(vectorizer.transform([text]))[0][1]

Wenn du hier eine höhere Punktzahl erwartet hast, wirst du nicht enttäuscht sein:

0.8536277207125618

Du kannst gerne deine eigenen Sätze ausprobieren und sehen, ob du mit den Stimmungswerten übereinstimmst, die das Modell vorhersagt. Es ist nicht perfekt, aber gut genug, um bei Hunderten von Bewertungen oder Kommentaren einen zuverlässigen Hinweis auf die Stimmung im Text zu erhalten.

Hinweis

Manchmal senkt die in CountVectorizereingebaute Liste der Stoppwörter die Genauigkeit eines Modells, weil die Liste so umfangreich ist. Entferne versuchsweise stop_words='english' aus CountVectorizer und führe den Code erneut aus. Überprüfe die Konfusionsmatrix. Nimmt die Genauigkeit zu oder ab? Du kannst auch andere Parameter wie min_df und ngram_range verändern. In der Praxis probieren Datenwissenschaftler oft viele verschiedene Parameterkombinationen aus, um herauszufinden, welche die besten Ergebnisse liefert .

Naive Bayes

Die logistische Regression ist ein beliebter Algorithmus für Klassifizierungsmodelle und ist oft sehr effektiv bei der Klassifizierung von Text. In Szenarien, in denen es um die Klassifizierung von Texten geht, greifen Datenwissenschaftler/innen jedoch oft auf einen anderen Lernalgorithmus namens Naive Bayes zurück. Dabei handelt es sich um einen Klassifizierungsalgorithmus, der auf dem Bayes'schen Theorem basiert, mit dem sich bedingte Wahrscheinlichkeiten berechnen lassen. Mathematisch wird das Bayes-Theorem folgendermaßen beschrieben:

P ( A | B ) = P(B|A)-P(A) P(B)

Das heißt, die Wahrscheinlichkeit, dass A wahr ist, wenn B wahr ist, ist gleich der Wahrscheinlichkeit, dass B wahr ist, wenn A wahr ist, multipliziert mit der Wahrscheinlichkeit, dass A wahr ist, dividiert durch die Wahrscheinlichkeit, dass B wahr ist. Das ist ein ganz schöner Brocken, und obwohl er genau ist, erklärt er nicht, warum Naive Bayes so nützlich für die Klassifizierung von Text ist - oder wie du ihn zum Beispiel auf eine Sammlung von E-Mails anwendest, um festzustellen, welche davon Spam sind.

Beginnen wir mit einem einfachen Beispiel. Angenommen, 10 % aller E-Mails, die du erhältst, sind Spam. Das ist P(A). Die Analyse zeigt, dass 5 % der Spam-E-Mails, die du erhältst, das Wort "Glückwunsch" enthalten, aber nur 1 % aller E-Mails, die du erhältst, das gleiche Wort. P(B|A) ist also 0,05 und P(B) ist 0,01. Die Wahrscheinlichkeit, dass eine E-Mail, die das Wort "Glückwunsch" enthält, Spam ist, ist P(A|B), also (0,05 x 0,10) / 0,01, also 0,50.

Natürlich muss ein Spamfilter alle Wörter in einer E-Mail berücksichtigen, nicht nur eines. Wenn du einige einfache (naive) Annahmen triffst - dass die Reihenfolge der Wörter in einer E-Mail keine Rolle spielt und dass jedes Wort gleich gewichtet ist -, kannst du die Bayes-Gleichung für einen Spam-Klassifikator auf diese Weise schreiben:

P ( S | m e s s a g e ) = P ( S ) - P ( w o r d 1 | S ) - P ( w o r d 2 | S ) ... P ( w o r d n | S )

Im Klartext: Die Wahrscheinlichkeit, dass eine Nachricht Spam ist, ist proportional zu dem Produkt aus:

  • Die Wahrscheinlichkeit, dass eine Nachricht im Datensatz Spam ist, oder P(S)

  • Die Wahrscheinlichkeit, dass jedes Wort in der Nachricht in einer Spam-Nachricht vorkommt, oder P(Wort|S)

P(S) lässt sich ganz einfach berechnen: Es ist einfach der Anteil der Spam-Nachrichten im Datensatz. Wenn du ein maschinelles Lernmodell mit 1.000 Nachrichten trainierst und 500 davon Spam sind, dann ist P(S) = 0,5. Für ein bestimmtes Wort ist P(Wort|S) einfach die Anzahl, wie oft das Wort in Spam-Nachrichten vorkommt, geteilt durch die Anzahl der Wörter in allen Spam-Nachrichten. Das gesamte Problem reduziert sich auf die Anzahl der Wörter. Du kannst eine ähnliche Berechnung durchführen, um die Wahrscheinlichkeit zu ermitteln, dass die Nachricht kein Spam ist, und dann die höhere der beiden Wahrscheinlichkeiten für eine Vorhersage verwenden.

Hier ist ein Beispiel mit vier Muster-E-Mails. Die Mails sind:

Text Spam
Verbessere deine Kreditwürdigkeit in wenigen Minuten 1
Hier ist das Protokoll der gestrigen Sitzung 0
Treffen morgen, um die Ergebnisse von gestern zu überprüfen 0
Hol dir die Medikamente von morgen zu den Preisen von gestern 1

Wenn du die Stoppwörter entfernst, die Buchstaben in Kleinbuchstaben umwandelst und die Wörter so formulierst, dass aus "tomorrow's" "tomorrow" wird, erhältst du folgendes Ergebnis:

Text Spam
Kreditwürdigkeit erhöhen Minute 1
Protokoll der gestrigen Sitzung 0
Treffen morgen Rückblick auf das Ergebnis von gestern 0
Ergebnis morgen med gestern Preis 1

Da zwei der vier Nachrichten Spam sind und zwei nicht, ist die Wahrscheinlichkeit, dass eine Nachricht Spam ist(P(S)) 0,5. Das Gleiche gilt für die Wahrscheinlichkeit, dass eine Nachricht kein Spam ist(P(N) = 0,5). Außerdem enthalten die Spam-Nachrichten neun einzigartige Wörter, während die Nicht-Spam-Nachrichten insgesamt acht enthalten.

Der nächste Schritt besteht darin, die folgende Tabelle mit den Worthäufigkeiten zu erstellen. Nehmen wir das Wort gestern als Beispiel. Es kommt einmal in einer als Spam gekennzeichneten Nachricht vor, also ist P(yesterday|S) 1/9 oder 0,111. In Nicht-Spam-Nachrichten kommt es zweimal vor, so dass P(yesterday|N) 2/8 oder 0,250 beträgt:

Wort P(Wort|S) P(Wort|N)
erhöhen 1/9 = 0.111 0/8 = 0.000
Kredit 1/9 = 0.111 0/8 = 0.000
punkten 2/9 = 0.222 1/8 = 0.125
Minute 1/9 = 0.111 1/8 = 0.125
gestern 1/9 = 0.111 2/8 = 0.250
Treffen 0/9 = 0.000 2/8 = 0.250
morgen 1/9 = 0.111 1/8 = 0.125
Rezension 0/9 = 0.000 1/8 = 0.125
med 1/9 = 0.111 0/8 = 0.000
Preis 1/9 = 0.111 0/8 = 0.000

Das funktioniert bis zu einem gewissen Punkt, aber die Nullen in der Tabelle sind ein Problem. Nehmen wir an, du willst herausfinden, ob "Die Ergebnisse müssen bis morgen überprüft werden" Spam ist. Wenn du die Stoppwörter entfernst, bleibt nur noch "Notenüberprüfung morgen" übrig. So kannst du die Wahrscheinlichkeit berechnen, dass die Nachricht Spam ist:

P ( S | s c o r e r e v i e w t o m o r r o w ) = P ( S ) - P ( s c o r e | S ) - P ( r e v i e w | S ) - P ( t o m o r r o w | S )
P ( S | s c o r e r e v i e w t o m o r r o w ) = 0 . 5 - 0 . 222 - 0 . 0 - 0 . 111 = 0
P ( S | s c o r e r e v i e w t o m o r r o w ) = 0

Das Ergebnis ist 0, weil eine Überprüfung nicht in einer Spam-Nachricht vorkommt, und 0 mal irgendetwas ist 0. Der Algorithmus kann der Aussage "Die Ergebnisse müssen bis morgen überprüft werden" einfach keine Spam-Wahrscheinlichkeit zuordnen.

Eine gängige Möglichkeit, dieses Problem zu lösen, ist die Laplace-Glättung, auch bekannt als additive Glättung. Dabei wird in der Regel 1 zu jedem Zähler und die Anzahl der eindeutigen Wörter im Datensatz (in diesem Fall 10) zu jedem Nenner addiert. P(Rezension|S) ist also (0 + 1) / (9 + 10), was 0,053 entspricht. Das ist nicht viel, aber besser als gar nichts (im wahrsten Sinne des Wortes). Hier sind die Worthäufigkeiten noch einmal, dieses Mal mit Laplace-Glättung überarbeitet:

Wort P(Wort|S) P(Wort|N)
erhöhen (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056
Kredit (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056
punkten (2 + 1) / (9 + 10) = 0.158 (1 + 1) / (8 + 10) = 0.111
Minute (1 + 1) / (9 + 10) = 0.105 (1 + 1) / (8 + 10) = 0.111
gestern (1 + 1) / (9 + 10) = 0.105 (2 + 1) / (8 + 10) = 0.167
Treffen (0 + 1) / (9 + 10) = 0.053 (2 + 1) / (8 + 10) = 0.167
morgen (1 + 1) / (9 + 10) = 0.105 (1 + 1) / (8 + 10) = 0.111
Rezension (0 + 1) / (9 + 10) = 0.053 (1 + 1) / (8 + 10) = 0.111
med (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056
Preis (1 + 1) / (9 + 10) = 0.105 (0 + 1) / (8 + 10) = 0.056

Jetzt kannst du mit zwei einfachen Berechnungen feststellen, ob "Die Ergebnisse müssen bis morgen überprüft werden" Spam ist:

P ( S | s c o r e r e v i e w t o m o r r o w ) = 0 . 5 - 0 . 158 - 0 . 053 - 0 . 105 = 0 . 000440
P ( N | s c o r e r e v i e w t o m o r r o w ) = 0 . 5 - 0 . 111 - 0 . 111 - 0 . 111 = 0 . 000684

Nach diesem Maßstab ist es wahrscheinlich, dass "Ergebnisse müssen bis morgen überprüft werden" kein Spam ist. Die Wahrscheinlichkeiten sind relativ, aber du könntest sie normalisieren und zu dem Schluss kommen, dass eine 40%ige Chance besteht, dass die Nachricht Spam ist, und eine 60%ige, dass sie es nicht ist, basierend auf den E-Mails, mit denen das Modell trainiert wurde.

Zum Glück musst du diese Berechnungen nicht von Hand durchführen. Scikit-Learn stellt mehrere Klassen zur Verfügung, die dir dabei helfen, darunter die KlasseMultinomialNB , die hervorragend mit den von CountVectorizer erstellten Tabellen der Wortanzahl funktioniert.

Spam-Filterung

Es ist kein Zufall, dass moderne Spam-Filter bei der Erkennung von Spam bemerkenswert geschickt sind. Praktisch alle von ihnen basieren auf maschinellem Lernen. Solche Modelle sind algorithmisch schwer zu implementieren, denn ein Algorithmus, der anhand von Schlüsselwörtern wie Kredit und Punktzahl feststellt, ob eine E-Mail Spam ist, lässt sich leicht täuschen. Das maschinelle Lernen hingegen betrachtet eine Reihe von E-Mails und nutzt das Gelernte, um die nächste E-Mail zu klassifizieren. Solche Modelle erreichen oft eine Genauigkeit von mehr als 99%. Und sie werden mit der Zeit immer schlauer, da sie mit immer mehr E-Mails trainiert werden.

Im vorherigen Beispiel wurde mithilfe der logistischen Regression vorhergesagt, ob der eingegebene Text eine positive oder negative Stimmung ausdrückt. Dabei wurde die Wahrscheinlichkeit, dass der Text eine positive Stimmung ausdrückt, als Stimmungswert verwendet, und du hast gesehen, dass Ausdrücke wie "Die langen Schlangen und der schlechte Kundenservice haben mich wirklich abgeschreckt" einen Wert nahe 0,0 haben, während Ausdrücke wie "Das Essen war großartig und der Service war ausgezeichnet" einen Wert nahe 1,0 haben. Erstellen wir nun ein binäres Klassifizierungsmodell, das E-Mails als Spam oder Nicht-Spam klassifiziert, und passen wir das Modell mit Naive Bayes an die Trainingsdaten an.

Es gibt mehrere öffentlich zugängliche Datensätze zur Spam-Klassifizierung. Jeder enthält eine Sammlung von E-Mails mit Proben, die mit 1en für Spam und 0en für keinen Spam gekennzeichnet sind. Wir werden einen relativ kleinen Datensatz mit 1.000 Proben verwenden. Lade den Datensatz herunter und kopiere ihn in das Unterverzeichnis Data deines Notizbuchs. Dann lädst du die Daten und zeigst die ersten fünf Zeilen an:

import pandas as pd
 
df = pd.read_csv('Data/ham-spam.csv')
df.head()

Prüfe nun, ob es doppelte Zeilen im Datensatz gibt:

df.groupby('IsSpam').describe()

Der Datensatz enthält eine doppelte Zeile. Entfernen wir sie und überprüfen wir das Gleichgewicht:

df = df.drop_duplicates()
df.groupby('IsSpam').describe()

Der Datensatz enthält nun 499 Proben, die kein Spam sind, und 500, die es sind. Im nächsten Schritt verwenden wir CountVectorizer, um die E-Mails zu vektorisieren. Auch hier lassen wir CountVectorizer sowohl Wortpaare als auch einzelne Wörter berücksichtigen und entfernen Stoppwörter mit dem in Scikit integrierten Wörterbuch für englische Stoppwörter:

from sklearn.feature_extraction.text import CountVectorizer
 
vectorizer = CountVectorizer(ngram_range=(1, 2), stop_words='english')
x = vectorizer.fit_transform(df['Text'])
y = df['IsSpam']

Teile den Datensatz so auf, dass 80% zum Trainieren und 20% zum Testen verwendet werden können:

from sklearn.model_selection import train_test_split
 
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2,
                                                    random_state=0)

Der nächste Schritt ist das Trainieren eines Naive Bayes-Klassifikators mit der Scikit-KlasseMultinomialNB:

from sklearn.naive_bayes import MultinomialNB
 
model = MultinomialNB()
model.fit(x_train, y_train)

Validiere das trainierte Modell mit den 20% des Datensatzes, die zum Testen vorgesehen sind, mithilfe einer Konfusionsmatrix:

%matplotlib inline
from sklearn.metrics import ConfusionMatrixDisplay as cmd

cmd.from_estimator(model, x_test, y_test,
                   display_labels=['Not Spam', 'Spam'],
                   cmap='Blues', xticks_rotation='vertical')

Das Modell identifizierte 101 von 102 legitimen E-Mails korrekt als Nicht-Spam und 95 von 98 Spam-E-Mails als Spam:

Verwende die Methode score, um ein grobes Maß für die Genauigkeit des Modells zu erhalten:

model.score(x_test, y_test)

Jetzt verwendet die Klasse RocCurveDisplay von Scikit, um die ROC-Kurve zu visualisieren:

from sklearn.metrics import RocCurveDisplay as rcd
import seaborn as sns
sns.set()

rcd.from_estimator(model, x_test, y_test)

Die Ergebnisse sind ermutigend. Die Fläche unter der ROC-Kurve (AUC) zeigt, dass das Modell mit einer Genauigkeit von mehr als 99,9 % die E-Mails als Spam oder nicht als Spam klassifiziert:

Schauen wir uns an, wie das Modell einige E-Mails klassifiziert, die es noch nicht gesehen hat, angefangen mit einer, die kein Spam ist. Die Methode predict des Modells sagt eine Klasse 0 für Nicht-Spam oder 1 für Spam voraus:

msg = 'Can you attend a code review on Tuesday to make sure the logic is solid?'
input = vectorizer.transform([msg])
model.predict(input)[0]

Das Modell sagt, dass diese Nachricht kein Spam ist, aber wie hoch ist die Wahrscheinlichkeit, dass es sich nicht um Spam handelt? Das kannst du mit predict_proba herausfinden, das ein Array mit zwei Werten zurückgibt: die Wahrscheinlichkeit, dass die vorhergesagte Klasse 0 ist, und die Wahrscheinlichkeit, dass die vorhergesagte Klasse 1 ist, in dieser Reihenfolge:

model.predict_proba(input)[0][0]

Das Model scheint sich sehr sicher zu sein, dass diese E-Mail legitim ist:

0.9999497111473539

Teste das Modell nun mit einer Spam-Nachricht:

msg = 'Why pay more for expensive meds when you can order them online ' \
      'and save $$$?'

input = vectorizer.transform([msg])
model.predict(input)[0]

Wie hoch ist die Wahrscheinlichkeit, dass die Nachricht kein Spam ist?

model.predict_proba(input)[0][0]

Die Antwort ist:

0.00021423891260677753

Wie hoch ist die Wahrscheinlichkeit, dass die Nachricht Spam ist?

model.predict_proba(input)[0][1]

Und die Antwort ist:

0.9997857610873945

Beachte, dass predict und predict_proba eine Liste von Eingaben akzeptieren. Könntest du auf dieser Grundlage einen ganzen Stapel von E-Mails mit einem einzigen Aufruf der beiden Methoden klassifizieren? Wie würdest du die Ergebnisse für jede E-Mail erhalten?

Empfehlungssysteme

Ein weiterer Zweig des maschinellen Lernens, der sich in den letzten Jahren bewährt hat, sind Empfehlungssysteme - Systeme, die den Kunden Produkte oder Dienstleistungen empfehlen. Das Empfehlungssystem von Amazon ist Berichten zufolge für 35% des Umsatzes des Unternehmens verantwortlich. Die gute Nachricht ist, dass du nicht Amazon sein musst, um von einem Empfehlungssystem zu profitieren, und dass du auch nicht über die Ressourcen von Amazon verfügen musst, um eines zu entwickeln. Sie sind relativ einfach zu erstellen, wenn du ein paar grundlegende Prinzipien kennst.

Empfehlungssysteme gibt es in vielen Formen. Beliebtheitsbasierte Systeme zeigen den Kunden an, welche Produkte und Dienstleistungen gerade beliebt sind, z. B. "Hier sind die Bestseller dieser Woche". Kollaborative Systeme geben Empfehlungen auf der Grundlage dessen, was andere ausgewählt haben, wie z. B. "Leute, die dieses Buch gekauft haben, haben auch diese Bücher gekauft." Keines dieser Systeme erfordert maschinelles Lernen.

Inhaltsbasierte Systeme hingegen profitieren stark vom maschinellen Lernen. Ein Beispiel für ein inhaltsbasiertes System ist eines, das sagt: "Wenn du dieses Buch gekauft hast, könnten dir diese Bücher auch gefallen." Diese Systeme benötigen eine Methode, um die Ähnlichkeit zwischen den Artikeln zu quantifizieren. Wenn dir der Film Stirb Langsam gefallen hat, könnte dir Monty Python und der Heilige Gral gefallen oder auch nicht. Wenn du Toy Story mochtest, wirst du wahrscheinlich auch A Bug's Life mögen. Aber wie kannst du das algorithmisch feststellen?

Inhaltsbasierte Empfehlungsprogramme benötigen zwei Komponenten: eine Möglichkeit, die Attribute, die eine Dienstleistung oder ein Produkt charakterisieren, zu vektorisieren - alsoin Zahlen umzuwandeln- und ein Mittel zur Berechnung der Ähnlichkeit zwischen den resultierenden Vektoren. Der erste Punkt ist einfach. Count​Vec⁠tor⁠izer wandelt Text in Tabellen mit Wortzahlen um. Alles, was du brauchst, ist eine Möglichkeit, die Ähnlichkeit zwischen den Zeilen zu messen, und du kannst ein Empfehlungssystem aufbauen. Eine der einfachsten und effektivsten Methoden dafür ist die sogenannte Kosinusähnlichkeit.

Kosinus-Ähnlichkeit

DieCosinus-Ähnlichkeit ist ein mathematisches Mittel zur Berechnung der Ähnlichkeit zwischen Paaren von Vektoren (oder Zahlenreihen, die als Vektoren behandelt werden). Die Grundidee besteht darin, jeden Wert in einer Stichprobe - zum Beispiel die Anzahl der Wörter in einer Zeile eines vektorisierten Textes - als Endpunktkoordinaten für einen Vektor zu verwenden, wobei der andere Endpunkt im Ursprung des Koordinatensystems liegt. Mache das für zwei Stichproben und berechne dann den Kosinus zwischen Vektoren im m-dimensionalen Raum, wobei m die Anzahl der Werte in jeder Stichprobe ist. Da der Kosinus von 0 gleich 1 ist, haben zwei identische Vektoren eine Ähnlichkeit von 1. Je unähnlicher die Vektoren sind, desto näher liegt der Kosinus bei 0.

Hier ein Beispiel im zweidimensionalen Raum zur Veranschaulichung. Angenommen, du hast drei Zeilen mit jeweils zwei Werten:

1 2
2 3
3 1

Du möchtest herausfinden, ob Zeile 2 der Zeile 1 oder der Zeile 3 ähnlicher ist. Es ist schwer, das allein anhand der Zahlen zu erkennen, und im echten Leben gibt es viel mehr Zahlen. Wenn du einfach die Zahlen in jeder Reihe addieren und die Summen vergleichen würdest, kämst du zu dem Schluss, dass Reihe 2 der Reihe 3 ähnlicher ist. Aber was wäre, wenn du jede Reihe als Vektor behandelst, wie in Abbildung 4-2 gezeigt?

  • Zeile 1: (0, 0) → (1, 2)

  • Reihe 2: (0, 0) → (2, 3)

  • Zeile 3: (0, 0) → (3, 1)

Abbildung 4-2. Kosinus-Ähnlichkeit

Jetzt kannst du jede Reihe als Vektor aufzeichnen, die Kosinuswerte der Winkel zwischen 1 und 2 und 2 und 3 berechnen und feststellen, dass die Reihe 2 der Reihe 1 ähnlicher ist als die Reihe 3. Das ist die Kosinusähnlichkeit auf den Punkt gebracht.

Die Kosinusähnlichkeit ist nicht auf zwei Dimensionen beschränkt, sondern funktioniert auch im höherdimensionalen Raum. Um die Kosinusähnlichkeit unabhängig von der Anzahl der Dimensionen zu berechnen, bietet Scikit die Funktioncosine_similarity . Der folgende Code berechnet die Kosinus-Ähnlichkeiten der drei Proben aus dem vorangegangenen Beispiel:

data = [[1, 2], [2, 3], [3, 1]]
cosine_similarity(data)

Der Rückgabewert ist eine Ähnlichkeitsmatrix, die die Kosinuswerte aller Vektorpaare enthält. Die Breite und Höhe der Matrix entspricht der Anzahl der Stichproben:

array([[1.        , 0.99227788, 0.70710678],
       [0.99227788, 1.        , 0.78935222],
       [0.70710678, 0.78935222, 1.        ]])

Daraus kannst du ersehen, dass die Ähnlichkeit der Zeilen 1 und 2 0,992 beträgt, während die Ähnlichkeit der Zeilen 2 und 3 0,789 beträgt. Mit anderen Worten: Reihe 2 ist Reihe 1 ähnlicher als Reihe 3. Auch zwischen den Reihen 2 und 3 (0,789) besteht eine größere Ähnlichkeit als zwischen den Reihen 1 und 3 (0,707).

Aufbau eines Filmempfehlungssystems

Lass uns die Cosinus- Ähnlichkeit nutzen, um ein inhaltsbasiertes Empfehlungssystem für Filme zu erstellen. Lade zunächst den Datensatz herunter, der einer von mehreren Filmdatensätzen ist, die bei Kaggle.com verfügbar sind. Dieser Datensatz enthält Informationen zu etwa 4.800 Filmen, darunter Titel, Budget, Genres, Schlüsselwörter, Besetzung und mehr. Lege die CSV-Datei im Unterverzeichnis Data deines Jupyter-Notebooks ab. Dann lade den Datensatz und sieh dir seinen Inhalt an:

import pandas as pd
 
df = pd.read_csv('Data/movies.csv')
df.head()

Der Datensatz enthält 24 Spalten, von denen nur einige wenige zur Beschreibung eines Films benötigt werden. Benutze die folgenden Anweisungen, um Schlüsselspalten wie title und genres zu extrahieren und fehlende Werte mit leeren Zeichenfolgen zu füllen:

df = df[['title', 'genres', 'keywords', 'cast', 'director']]
df = df.fillna('') # Fill missing values with empty strings
df.head()

Als Nächstes fügst du eine Spalte namens features hinzu, die alle Wörter in den anderen Spalten kombiniert :

df['features'] = df['title'] + ' ' + df['genres'] + ' ' + \
            df['keywords'] + ' ' + df['cast'] + ' ' + \
            df['director']

Verwende CountVectorizer, um den Text in der Spalte features zu vektorisieren:

from sklearn.feature_extraction.text import CountVectorizer
 
vectorizer = CountVectorizer(stop_words='english', min_df=20)
word_matrix = vectorizer.fit_transform(df['features'])
word_matrix.shape

Die Tabelle mit den Wortzahlen enthält 4.803 Zeilen - eine für jeden Film - und 918 Spalten. Die nächste Aufgabe besteht darin, die Kosinusähnlichkeiten für jedes Zeilenpaar zu berechnen:

from sklearn.metrics.pairwise import cosine_similarity
 
sim = cosine_similarity(word_matrix)

Das Ziel dieses Systems ist es, einen Filmtitel einzugeben und die n Filme zu identifizieren, die diesem Film am ähnlichsten sind. Zu diesem Zweck definierst du eine Funktion namens get​_recom⁠mendations, die einen Filmtitel, eine DataFrame mit Informationen über alle Filme, eine Ähnlichkeitsmatrix und die Anzahl der zurückzugebenden Filmtitel akzeptiert:

def get_recommendations(title, df, sim, count=10):
    # Get the row index of the specified title in the DataFrame
    index = df.index[df['title'].str.lower() == title.lower()]
     
    # Return an empty list if there is no entry for the specified title
    if (len(index) == 0):
        return []
 
    # Get the corresponding row in the similarity matrix
    similarities = list(enumerate(sim[index[0]]))
     
    # Sort the similarity scores in that row in descending order
    recommendations = sorted(similarities, key=lambda x: x[1], reverse=True)
     
    # Get the top n recommendations, ignoring the first entry in the list since
    # it corresponds to the title itself (and thus has a similarity of 1.0)
    top_recs = recommendations[1:count + 1]
 
    # Generate a list of titles from the indexes in top_recs
    titles = []
 
    for i in range(len(top_recs)):
        title = df.iloc[top_recs[i][0]]['title']
        titles.append(title)
 
    return titles

Diese Funktion sortiert die Cosinus-Ähnlichkeiten in absteigender Reihenfolge, um die count Filme zu finden, die dem durch den Parameter title identifizierten Film am ähnlichsten sind. Dann gibt sie die Titel dieser Filme zurück.

Verwende nun get_recommendations, um die Datenbank nach ähnlichen Filmen zu durchsuchen. Frag zuerst nach den 10 Filmen, die dem James Bond-Thriller Skyfall am ähnlichsten sind:

get_recommendations('Skyfall', df, sim)

Hier ist die Ausgabe:

['Spectre',
 'Quantum of Solace',
 'Johnny English Reborn',
 'Clash of the Titans',
 'Die Another Day',
 'Diamonds Are Forever',
 'Wrath of the Titans',
 'I Spy',
 'Sanctum',
 'Blackthorn']

Rufe erneut get_recommendations auf, um Filme aufzulisten, die wie Mulan sind:

get_recommendations('Mulan', df, sim)

Du kannst auch gerne andere Filme ausprobieren. Beachte, dass du nur Filmtitel eingeben kannst, die im Datensatz enthalten sind. Verwende die folgenden Anweisungen, um eine vollständige Liste der Titel zu drucken:

pd.set_option('display.max_rows', None)
print(df['title'])

Ich denke, du wirst mir zustimmen, dass das System ziemlich glaubwürdig ähnliche Filme auswählt. Nicht schlecht für etwa 20 Codezeilen !

Zusammenfassung

Modelle für maschinelles Lernen, die Texte klassifizieren, sind weit verbreitet und werden in der Industrie und im Alltag auf vielfältige Weise eingesetzt. Welcher vernünftige Mensch wünscht sich nicht einen Zauberstab, der zum Beispiel alle Spam-Mails auslöscht?

Der Text, der zum Trainieren eines Textklassifizierungsmodells verwendet wird, muss vor dem Training vorbereitet und vektorisiert werden. Zur Vorbereitung gehören die Umwandlung von Buchstaben in Kleinbuchstaben und das Entfernen von Satzzeichen sowie das Entfernen von Stoppwörtern, Zahlen und Stemming oder Lemmatisierung. Nach der Vorbereitung wird der Text vektorisiert, indem er in eine Tabelle mit Worthäufigkeiten umgewandelt wird. Die Klasse CountVectorizer von Scikit macht kurzen Prozess mit der Vektorisierung und übernimmt auch einen Teil der Vorbereitungsaufgaben.

Die logistische Regression und andere gängige Klassifizierungsalgorithmen können verwendet werden, um Text zu klassifizieren, sobald er in eine numerische Form umgewandelt wurde. Für Textklassifizierungsaufgaben ist der Naive Bayes Lernalgorithmus jedoch häufig besser geeignet als andere Algorithmen. Durch einige "naive" Annahmen, wie z. B., dass die Reihenfolge der Wörter in einer Textprobe keine Rolle spielt, reduziert sich Naive Bayes auf ein Verfahren zum Zählen von Wörtern. Die Scikit MultinomialNB Klasse bietet eine praktische Naive Bayes-Implementierung.

Die Kosinusähnlichkeit ist eine mathematische Methode zur Berechnung der Ähnlichkeit zwischen zwei Zahlenreihen. Eine Anwendung ist die Entwicklung von Systemen, die Produkte oder Dienstleistungen empfehlen, die auf anderen Produkten oder Dienstleistungen basieren, die ein Kunde gekauft hat. Worthäufigkeitstabellen, die aus Textbeschreibungen von CountVectorizer erstellt werden, können mit der Cosinus-Ähnlichkeit kombiniert werden, um intelligente Empfehlungssysteme zu erstellen, die das Endergebnis eines Unternehmens ergänzen.

Du kannst die Beispiele in diesem Kapitel als Ausgangspunkt für deine eigenen Experimente nutzen. Überprüfe zum Beispiel, ob du die Parameter, die du in einem der Beispiele an CountVectorizer übergibst, verändern kannst, um die Genauigkeit des resultierenden Modells zu erhöhen. Datenwissenschaftler nennen die Suche nach der optimalen Parameterkombination "Hyperparameter-Tuning", ein Thema, das du im nächsten Kapitel kennenlernen wirst.

Get Angewandtes maschinelles Lernen und KI für Ingenieure 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.