Sentiment Analyse

Sie erfahren in diesem Blogartikel, wie Sie ein Modell trainieren können, das eine Sentiment Analyse von Kommentaren selbstständig durchführt. Eine Sentiment Analyse bewertet die Stimmung oder das Gefühl eines Texts und nutzt Methoden der Textklassifizierung.
01_comment_classification
In [ ]:
# Import all pre-installed packages
import os
import pickle
import shutil
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import nltk
nltk.download('stopwords')
nltk.download('punkt')
nltk.download('wordnet')
from google.colab import drive
from IPython.display import display
from IPython.display import Image
from keras.optimizers import *
from keras.models import *
from keras.layers import * 
from keras.callbacks import * 
from keras.preprocessing.sequence import pad_sequences
from keras.preprocessing.text import Tokenizer
from keras.layers import *
import keras.backend as K
from sklearn.metrics import *
from sklearn.model_selection import train_test_split
from sklearn.utils.multiclass import unique_labels
from sklearn.utils.class_weight import compute_class_weight
from yellowbrick.target import ClassBalance
pd.set_option("display.max_columns", 100)

# Code necessary to run code in google colab
drive.mount('/content/gdrive')
os.chdir(os.path.join("gdrive", "Team Drives", "AI", "Blog-Posts", "toxic_comments"))

# Import my own functions
from utils.evaluate import *
from utils.preprocessing import *
from utils.models import *

# Creating folders for results
if not os.path.isdir("results"):
    os.mkdir("results")
if not os.path.isdir("models"):
    os.mkdir("models")
if not os.path.isdir("data"):
    os.mkdir("data")
if not os.path.isdir("images"):
    os.mkdir("images")
[nltk_data] Downloading package stopwords to /root/nltk_data...
[nltk_data]   Unzipping corpora/stopwords.zip.
[nltk_data] Downloading package punkt to /root/nltk_data...
[nltk_data]   Unzipping tokenizers/punkt.zip.
[nltk_data] Downloading package wordnet to /root/nltk_data...
[nltk_data]   Unzipping corpora/wordnet.zip.
Using TensorFlow backend.
Go to this URL in a browser: https://accounts.google.com/o/oauth2/auth?client_id=947318989803-6bn6qk8qdgf4n4g3pfee6491hc0brc4i.apps.googleusercontent.com&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdocs.test%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.photos.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fpeopleapi.readonly&response_type=code

Enter your authorization code:
··········
Mounted at /content/gdrive

1. Einleitung Sentiment Analyse

In [ ]:
Image(os.path.join("images", "sentiment_classification.jpg"))
Out[ ]:
# This is formatted as code

Sie erfahren in diesem Blogartikel, wie Sie ein Modell trainieren können, das eine Sentiment Analyse von Kommentaren selbstständig durchführt. Eine Sentiment Analyse bewertet die Stimmung oder das Gefühl eines Texts und nutzt Methoden der Textklassifizierung. Algorithmen zur Textklassifizierung sortieren schon heute Spam aus Ihren E-Mails, schätzen im Support die Dringlichkeit eines Kundenanliegens ein und ordnen für das Marketing auf Basis von Rezessionen die Kunden bestimmten Zielgruppen zu, um sie mit relevanteren Informationen zu versorgen. Textklassifizierung steigert schon heute Effizienz und Umsatz für ein Unternehmen.

Sentiment Analysen sind nicht nur für Social Media Plattformen wie Facebook oder Youtube interessant, sondern auch für jeden Online-Shop, der ein Kommentarforum bereitstellt. Durch eine gute Sentiment Analyse können Unternehmen beispielsweise einen unangemessenen Ton aufspüren und den Chatteilnehmer darauf hinweisen. So wird ein angemessener Ton eher eingehalten und konstruktive Chatteilnehmer werden nicht von sogenannten "Trollen" abgeschreckt.

Nun zur Praxis: Schauen wir uns die Sentiment Analyse an. Die Daten unseres Beispiels stammen von der Kaggle Challenge Jigsaw Unintended Bias in Toxicity Classification und wurden ursprünglich von Civil Comments erstellt. Kaggle ist eine Online Plattform, auf der Firmen Fragen zu ihrem Business Case stellen, die Machine Learning Algorithmen beantworten.

Der Datensatz dieser Kaggle Challenge beinhalt 45 Spalten und etwa 1,8 Millionen Zeilen/Kommentare. Von den 45 Spalten sind lediglich 3 Spalten für diesen Blogpost relevant, nämlich die Kommentar-ID, der Kommentar selbst und die Bewertung eines Kommentars ("toxic" / "OK") durch Menschen, welche in der "target" Spalte steht. In der target Spalte kann man daher den Prozentsatz an Menschen finden, die einen gewissen Kommentar als toxic bewertet haben. Für die Klassifizierung definieren wir alle Kommentare als toxic, die einen target-Wert von mindestens 50 Prozent haben und wir werden alle Kommentare als OK definieren, die einen target-Wert von weniger als 50 Prozent haben. Den Grenzwert bei 50 Prozent nennt man auch Klassengrenze#Klassengrenze).

In dem folgenden Codeblock führen wir eine erste Sichtung der Daten durch.

In [ ]:
# Read in and view the data
data = pd.read_csv(os.path.join("data", "data.csv"))
data = data[["id", "target", "comment_text"]]
data.head()
Out[ ]:
idtargetcomment_text
0598480.000000This is so cool. It's like, 'would you want yo...
1598490.000000Thank you!! This would make my life a lot less...
2598520.000000This is such an urgent design problem; kudos t...
3598550.000000Is this something I'll be able to install on m...
4598560.893617haha you guys are a bunch of losers.

2. Kurze explorative Datenanalyse

Zu Anfang einer Klassifikation sollte man zur Sicherheit erst die Klassenverteilung der Grundwahrheit (OK/toxic) plotten, da diese einen großen Einfluss auf die Performance-Evaluierung unseres Modells hat.

In [ ]:
# One-hot-encoding of the ground truth variable
ytrue = data.target.apply(lambda x: 0 if x < 0.5 else 1)
class_labels = data.target.apply(lambda x: "OK" if x < 0.5 else "toxic")

# Class distribution plot
class_balance_plot = ClassBalance()
class_balance_plot.fit(y_train=class_labels)
class_balance_plot.poof(outpath=os.path.join("results", "label_distribution_plot.png"))
OK_percentage = len(class_labels[class_labels == "OK"]) / len(class_labels) * 100
toxic_percentage = len(class_labels[class_labels == "toxic"]) / len(class_labels) * 100
print("Share of OK classifications: {:.2f}%, Share of toxic classifications: {:.2f}%".format(OK_percentage, toxic_percentage))
Share of OK classifications: 92.00%, Share of toxic classifications: 8.00%

In unserem Datensatz sind lediglich 8 Prozent aller Kommentare toxic, während die restlichen 92 Prozent der Kommentare OK sind. Wenn unser Modell alle Kommentare als OK bewerten wird und keinen einzigen Kommentar als toxic, dann wäre die Accuracy (Prozentsatz korrekter Vorhersagen von allen Vorhersagen) bereits bei 92 Prozent. Das hört sich auf den ersten Blick sehr gut an, allerdings wäre unser Modell dann nicht in der Lage, die toxic Kommentare herauszufiltern, was z. B. für einen Online-Shop viel wichtiger wäre.

In unserem Fall wäre eine Kombination aus Precision und Recall eine viel angebrachtere Evaluierungsmetrik, denn bei der Analyse von Precision und Recall kann man definieren, auf welche Klasse es wirklich ankommt (toxic in unserem Fall). Demzufolge, bedeutet ein hoher Precision-Wert, dass von den Kommentaren, die unser Modell als toxic bewertet hat auch die meisten Kommentare wirklich toxic sind, während ein hoher Recall-Wert bedeutet, dass von den meisten Kommentaren, die wirklich toxic sind, unser Modell auch die meisten als toxic identifiziert hat.

Es gibt verschiedene Methoden, um eine Klassenungleichheit zu bekämpfen, die Sie weiter unten kennen lernen werden. Da sich der Erfolg der Methoden nicht vorhersehen lässt, werden wir erst ein "naives" Modell trainieren, welches keine dieser Methoden berücksichtigt. Erst nachdem wir dieses naive Modell evaluiert haben, werden wir die anderen Methoden ausprobieren.

3. Data Preprocessing

3.1 Bereinigung der Textdaten

Textdaten können sehr "messy" sein, also uneinheitlich, da Chatteilnehmer beispielsweise Schreibfehler machen oder zu viele Leerzeichen zwischen den Wörtern tippen. Wenn man diese "messy" Textdaten nicht bereinigt, könnte der Algorithmus Wörter mit gleicher Bedeutung und "anderer" Schriebweise als unterschiedliche Wörter behandeln. Um das zu verhindern, muss man die Textdaten bereinigen, also vereinheitlichen oder standardisieren. Wir werden unter anderem unnötige Satzzeichen und Stopwörter wie "a" oder "the" entfernen, alle Wörter kleinschreiben, überflüssige Leerzeichen entfernen und jedes Wort "lemmatisieren", das heißt auf eine Art Grundform reduzieren (z.B. wird das Wort "done" auf "do" reduziert). Diese Textbereinigungen finden in der von uns geschriebenen Funktion clean_comments() statt.

3.2 Vektorisierung der Textdaten

Da Machine Learning Algorithmen nichts mit reinen Buchstaben und Wörtern anfangen können, muss man diese zerst in ein numerisches Format umwandeln. Die Vektorisierung der Textdaten verläuft in groben Zügen so: Man findet die am häufigsten auftretenden Wörter in allen Kommentaren und weist jedem Wort einen einzigartigen Index zu. Dann wird in jedem Kommentar jedes Wort in seinen zugewisenden Index umgewandelt und danach werden diese Indizes in einen Vektor geschrieben. Da es häufig vorkommt, dass nicht alle Kommentare gleich viele Wörter beinhalten, muss man entweder den Anfang oder das Ende der Vektoren mit Nullen auffüllen, sodass alle Vektoren am Schluss die gleiche Läge haben.

In dem folgenden Codeblock werden wir

  1. die Textdaten bereinigen
  2. festlegen, wie viele der am häufigsten auftretenden Wörter wir verwenden wollen (vocab_size definieren)
  3. definieren, wie viele Wörter maximal in einem Kommentar stehen sollen (max_doc_len definieren). Das ist meinstens die Anzahl der Wörter in dem längsten Kommentar
  4. die einzelnen Kommentare in Vektoren umwandeln, die wir dann auf die gleiche Länge bringen
In [ ]:
# Cleaning the text data
max_doc_len = 220
vocab_size= 20000
clean_data = clean_comments(data["comment_text"])

# Vectorizing the text data
tokenizer = Tokenizer(num_words=vocab_size, lower=True)
tokenizer.fit_on_texts(clean_data)
X = tokenizer.texts_to_sequences(clean_data)
X = pad_sequences(X, maxlen=max_doc_len)

# Saving some data to disk which we need for future blog posts
with open(os.path.join("results", "tokenizer.pickledump"), "wb") as file:
    pickle.dump(tokenizer, file)
with open(os.path.join("results", "X.pickledump"), "wb") as file:
    pickle.dump(X, file)

comment_metadata_df = pd.DataFrame({
    "comment_text": clean_data,
    "class_label": class_labels,
    "target_score": data["target"]
})
comment_metadata_df["comment_text"] = comment_metadata_df["comment_text"].apply(lambda list_of_tokens: " ".join(list_of_tokens))
comment_metadata_df.to_csv(os.path.join("data", "comment_metadata_df.csv"), index=None)

# Displaying some results
print("Cleaned comments:")
display(comment_metadata_df.head())
print(" ")
print('Vectorized comments:')
print(X[:10])
print("Dataset has {} rows and {} columns".format(X.shape[0], X.shape[1]))
    
# Save some memory
del data
del clean_data
del comment_metadata_df
Cleaned comments:
comment_textclass_labeltarget_score
0cool 's like 'would want mother read really gr...OK0.000000
1thank would make life lot less anxiety-inducin...OK0.000000
2urgent design problem kudos take impressiveOK0.000000
3something 'll able install site releaseOK0.000000
4haha guy bunch loserstoxic0.893617
 
Vectorized comments:
[[   0    0    0 ...  289   34  146]
 [   0    0    0 ...  123    4   33]
 [   0    0    0 ... 3643   17 4022]
 ...
 [   0    0    0 ...    0    0    0]
 [   0    0    0 ...  580   78  300]
 [   0    0    0 ...  322   26  651]]
Dataset has 1804874 rows and 220 columns

4. Definition des naiven Modells

In unserem Modell werden wir zuerst ein Wort-Embedding erzeugen und dann ein Kommentar-Embedding. Unter Embedding versteht man das Mapping zwischen einer Kategorie, also einem Wort oder einem Kommentar in unserem Fall, und einem Vektor von Länge $x$. Durch die Wort- und Kommentar-Embeddings wollen wir erreichen, dass wir die Ähnlichkeiten der einzelnen Wörter/Kommentare zueinander modellieren können, sodass unser Modell in der Lage ist, Kontext zu modellieren. Kontext zu modellieren ist wichtig, da allein stehende Wörter in verschiedenen Kontexten komplett andere Bedeutungen haben können. Erst durch den Kontext wird klar, ob der Kommentar OK oder toxic ist. Das ideale Ergebnis wäre, dass Wörter, die häufig zusammen verwendet werden, und dass Kommentare, die in etwa das Gleiche aussagen, nah beieinander im $x$ dimensionalen Raum liegen. Hierunter kann man ein Beispiel eines erfolgreichen Wort-Embeddings sehen.

In [ ]:
Image(os.path.join("images", "word_embedding_pic.jpeg"))
Out[ ]:

Bild Quelle

Da die Bewertung eines Kommentars von allen seinen Wörtern abhängig ist, setzen wir einen speziellen rekurrenten Zellenblock ein (Bidirectional Gated Recurrent Unit), der Kommentar-Embeddings auf Basis des gesamten Kommentars erzeugt. Danach werden wir einen Convolutional Zellenblock anschließen, welches Nachbarschaftsbeziehungen zwischen den einzelnen Wörtern in jedem Kommentar hervorhebt. Dazwischen verwenden wir Dropout und Pooling Zellenblöcke, die ein "Overfitting" vermeiden sollen. Overfitting entsteht, wenn sich ein Modell extrem gut an die Trainingsdaten anpasst, aber ihm unbekannte Validierungs- und Testdaten schlecht generalisiert. Die Modellarchitektur wird in der von uns erstellten Funktion get_model() erstellt, die wiederum in unserer custom-Funktion train_model() aufgerufen wird (siehe im nächsten Codeblock).

5. Training des naiven Modells

Nachdem das Modell kompiliert wurde, starten wir den Trainingsprozess. Zuerst teilen wir den Datensatz auf in einen Trainingsdatensatz, einen Validierungsdatensatz und einen Testdatensatz. Auf dem Trainingsdatensatz trainieren wir das Modell für maximal 50 Epochen: Der gesamte Trainingsdatensatz wird maximal 50 Mal durch das Modell geschläust. Außderdem setzen wir ein EarlyStopping ein, damit das Modell nicht zu lange trainiert wird und eventuell anfängt zu overfitten. Danach identifizieren wir die optimale Anzahl von Epochen und evaluieren das Modell mit der optimalen Anzahl Epochen auf dem Testdatensatz.

In [ ]:
# Splitting the dataset into training, validation and test set
X_train, X_test, ytrue_train, ytrue_test = train_test_split(X, ytrue, test_size=0.2, random_state=42, stratify=ytrue)
X_train, X_val, ytrue_train, ytrue_val = train_test_split(X_train, ytrue_train, test_size=0.2, random_state=43, stratify=ytrue_train)

# Applying the model training function above
naive_model, naive_history = train_model(
    X_train, X_val, ytrue_train, ytrue_val, model_name="naive_model", patience=10
)
06/11/2019 10:57:25 AM Currently training naive_model
WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/op_def_library.py:263: colocate_with (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Colocations handled automatically by placer.
06/11/2019 10:57:25 AM From /usr/local/lib/python3.6/dist-packages/tensorflow/python/framework/op_def_library.py:263: colocate_with (from tensorflow.python.framework.ops) is deprecated and will be removed in a future version.
Instructions for updating:
Colocations handled automatically by placer.
WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/keras/backend/tensorflow_backend.py:3445: calling dropout (from tensorflow.python.ops.nn_ops) with keep_prob is deprecated and will be removed in a future version.
Instructions for updating:
Please use `rate` instead of `keep_prob`. Rate should be set to `rate = 1 - keep_prob`.
06/11/2019 10:57:25 AM From /usr/local/lib/python3.6/dist-packages/keras/backend/tensorflow_backend.py:3445: calling dropout (from tensorflow.python.ops.nn_ops) with keep_prob is deprecated and will be removed in a future version.
Instructions for updating:
Please use `rate` instead of `keep_prob`. Rate should be set to `rate = 1 - keep_prob`.
WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_ops.py:3066: to_int32 (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.
06/11/2019 10:57:28 AM From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_ops.py:3066: to_int32 (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.cast instead.
WARNING:tensorflow:From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_grad.py:102: div (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Deprecated in favor of operator or tf.math.divide.
06/11/2019 10:57:28 AM From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_grad.py:102: div (from tensorflow.python.ops.math_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Deprecated in favor of operator or tf.math.divide.
Train on 1155119 samples, validate on 288780 samples
Epoch 1/50
1155119/1155119 [==============================] - 157s 136us/step - loss: 0.1686 - val_loss: 0.1353
Epoch 2/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.1361 - val_loss: 0.1353
Epoch 3/50
1155119/1155119 [==============================] - 152s 131us/step - loss: 0.1268 - val_loss: 0.1369
Epoch 4/50
1155119/1155119 [==============================] - 151s 131us/step - loss: 0.1171 - val_loss: 0.1485
Epoch 5/50
1155119/1155119 [==============================] - 152s 131us/step - loss: 0.1072 - val_loss: 0.1532
Epoch 6/50
1155119/1155119 [==============================] - 151s 131us/step - loss: 0.0974 - val_loss: 0.1716
Epoch 7/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0879 - val_loss: 0.1836
Epoch 8/50
1155119/1155119 [==============================] - 150s 129us/step - loss: 0.0791 - val_loss: 0.2098
Epoch 9/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0722 - val_loss: 0.2193
Epoch 10/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0655 - val_loss: 0.2574
Epoch 11/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0603 - val_loss: 0.2573
Epoch 12/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0559 - val_loss: 0.2705
06/11/2019 11:27:52 AM Optimal number of epochs: 2

6. Evaluation des naiven Modells

Zuerst plotten wir die Performance des Modells während des Trainings, das heißt auf der x-Achse stellen wir die Epochen dar und auf der y-Achse plotten wir die Performance auf dem Validierungsdatensatz. Hier gilt: Je geringer der Wert auf der y-Achse, desto besser die Performance. Allerdings ist der Wert auf der y-Achse nicht direkt interpretierbar. Wir schauen uns diesen Plot an, um zu sehen, wie das Modell trainiert hat.

Anschließend evaluieren wir die Performance des Modells auf dem Testdatensatz, den wir dem Modell bislang vorenthalten haben. Wie bereits oben angesprochen, wäre es keine gute Idee, Accuracy als Evaluierungsmetrik zu nutzen. Deshalb verwenden wir die Fläche unterhalb der Precision-Recall-Kurve als Performance Metrik. Um die Performance unseres Modells besser einschätzen zu können, werden wir unser naives Modell mit einem simplen Baseline-Modell vergleichen, welches immer toxic Kommentare vorhersagt.

Es kann mathematisch bewiesen werden (Quelle), dass dieses Baseline-Modell eine Fläche unterhalb der Precision-Recall Kurve hat, die dem Anteil der toxic Kommentare von allen Kommentaren insgesamt entspricht: In unserem Fall also 8 Prozent. Sobald unser Modell eine Fläche unterhalb der Precision-Recall Kurve von mehr als 8 Prozent erreicht, können wir behaupten, dass unser naives Modell in der Lage ist, die toxic Kommentare von den OK Kommentaren zu unterscheiden.

Außerdem werden wir einen Classification Report erstellen und eine Confusion Matrix in den von uns erstellten Funktionen cls_report() und conf_matrix() ausrechnen lassen. Anhand des Classification Reports können wir nämlich direkt die Precision und Recall Werte der toxic Kommentare bei einer Klassengrenze von 50% ablesen und anhand der Confusion Matrix können wir die vorhergesagten Klassen ("predicted label") und die wirklichen Klassen ("True label") aller Datenpunkte des Testdatensatzes erkennen. Im Idealfall hat die Confusion Matrix alle Datenpunkte auf der Diagonalen von links oben nach rechts unten, d.h. dass von allen OK Kommentaren auch alle Kommentare als OK bewertet wurden und dass von allen toxic Kommentaren alle als toxic identifiziert wurden. Je mehr Streuung außerhalb dieser Diagonalen ist, desto schlechter ist die Performance des Modells.

In [ ]:
# Make predictions for the test set
ypred_test = naive_model.predict(X_test)
ypred_test_oh = pd.Series(ypred_test.ravel()).apply(lambda x: 0 if x < 0.5 else 1)
In [ ]:
# Evaluate the percormance of the naive model
epochs_loss_plot(
    histories=naive_history, model_names="naive_model"
)
precision_recall_plot(
    ytrue=ytrue_test, ypreds=ypred_test, model_names="naive_model", file_name="naive_model"
)
cls_report(
    ytrue=ytrue_test, ypreds_oh=ypred_test_oh, model_names="naive_model"
)
conf_matrix(
    ytrue=ytrue_test, ypreds_oh=ypred_test_oh, model_names="naive_model"
)
<Figure size 576x396 with 0 Axes>
<Figure size 576x396 with 0 Axes>
Classification report naive_model
OKtoxicaccuracymacro avgweighted avg
f1-score0.9720540.6324110.9480570.8022320.944893
precision0.9624060.7284680.9480570.8454370.943698
recall0.9818970.5587350.9480570.7703160.948057
support332108.00000028867.0000000.948057360975.000000360975.000000

7. Ergebnis Analyse des naiven Modells

In dem Plot "Epochen vs. Loss" kann man sehen, dass das Modell bereits nach zwei Epochen anfängt zu overfitten, da val_loss nach zwei Epochen sein Minimum erreicht und danach wieder steigt. Der kontinuierlich sinkende training_loss zeigt, dass das Modell nach zwei Epochen anfängt, die Trainingsdaten auswendig zu lernen. Es ist nicht mehr in der Lage, unbekannte Daten zu generalisieren.

Die Precision-Recall Kurve zeigt den Wert von Precision und Recall für verschiedene Klassengrenzen#Klassengrenze). Wir stellen fest, dass unser Modell zwar besser als das Baseline-Modell ist, aber wie man im Classification Report sehen kann, liegt der Recall Prozentsatz der toxic Kommentare bei etwa 56%. In der Confusion Matrix kann man auch sehen, dass eine große Mehrheit der Daten im links-oberen Eck liegen, aber relativ wenig im rechts-unteren Eck. Dass viele Datenpunkte im links-oberen Eck liegen ist nicht verwunderlich, da etwa 92% aller Kommentare OK sind. Dass allerdings nicht so viele Datenpunkte im rechts-unteren Eck liegen zeigt auch, dass unser Modell viele toxic Kommentare nicht als solche identifiziert hat. Wie oben bereits erwähnt, resultiert dies wahrscheinlich aus der starken Ungleichverteilung der OK und toxic Kommentare. Um dieses Problem zu bekämpfen gibt es u.a. folgende Methoden:

8. Alternative Methoden

  • Oversampling der toxic Kommentare: Es gibt z.B. den Algorithmus "Synthetic Minority Over-sampling Technique" (SMOTE), der künstliche toxic Kommentare erzeugen könnte. Allerdings ergibt es in unserem Fall nicht viel Sinn, diesen Algorithmus zu nutzen, da unsere Kommentardaten aus Kategorien (also Wörtern) und nicht stetigen Skalarwerten bestehen. SMOTE erzeugt nämlich syntetische Daten, die räumlich gesehen nah bei bestehenden Daten liegen. Dies könnte bei kategorischen Daten eventuell nicht gut funktionieren, da die Indizes von Wörtern, die räumlich nah beieinander liegen, nicht unbedingt zu Wörtern gehören, die eine ähnliche Bedeutung haben. In anderen Worten, SMOTE würde wahrscheinlich Kommentare erzeugen, die aus völlig zusammenhangslosen oder zufälligen Wörtern bestehen.

  • Undersampling der OK Kommentare: Man könnte einfach so viele OK Kommentare herausnehmen, bis es eine gleiche Verteilung von OK und toxic Kommentaren gibt. Der große Nachteil ist hier, dass man dem Modell sehr viele Informationen entzieht, die eventuell bräuchte, um eine gute Performance zu erzielen.

  • Die Klassnegrenze zugunsten der toxic Kommentare ändern: Wir könnten z.B. mehr toxic Kommentare erzeugen, indem wir Kommentare bereits als toxic definieren, die einen target-Wert von mindestens 30 Prozent haben. Wir werden dies hier erstmal nicht machen, da die Aufgabenstellung der Kaggle Challenge eine Klassengrenze von 50 Prozent vorschreibt.

  • Höhere Gewichtung von Fehlklassifikationen: Jeder Machine Learning Algorithmus optimiert intern den Wert einer gewissen Kostenfunktion, der sich in unserem Fall aus Bestrafungen von Fehlklassifikationen zusammensetzt. Je geringer der Wert der Kostenfunktion, je besser ist die Performance des Algorithmus. Da wir Fehlklassifikationen von den toxic Kommentaren vermeiden wollen, könnten wir Fehlklassifikationen der toxic Kommentare härter bestrafen als Fehlklassifiaktionen der OK Kommentare. Härter bestrafen heißt in diesem Fall, dass eine Fehlklassifikation eines toxic Kommentars den Wert der Kostenfunktion um mehr erhöht, als eine Fehlklassifikation eines OK Kommentars. Der einzige Nachteil diese Methode ist lediglich, dass man vorher nicht weiß, wie "hart" man genau Fehlklassifikationen der toxic Kommentare bestrafen muss.

Wir werden die am Schluss genante Mehtode verwenden, da diese einfach umzusetzen ist, und sie unseren Datensatz nicht verändern wird. Im folgenden Codeblock werden wir eine Gewichtskombination ausprobieren, bei der die Fehlklassifikationen der toxic Kommentare 9 mal härter bestraft werden, als die Fehlklassifikationen von OK Kommentaren.

In [ ]:
# Create a weight dictionary
weight_dict = {0: 0.1, 1: 0.9}

# Train the model with the above weighting combination 
weighted_model_name="{}_{}_model".format(weight_dict[0], weight_dict[1])
weighted_model, weighted_model_history = train_model(
    X_train, X_val, ytrue_train, ytrue_val, model_name=weighted_model_name,
    weight_dict=weight_dict, patience=10
)

# Make predictions for the test set
ypred_test = weighted_model.predict(X_test)
ypred_test_oh = pd.Series(ypred_test.ravel()).apply(lambda x: 0 if x < 0.5 else 1)
06/11/2019 11:29:43 AM Currently training 0.1_0.9_model
Train on 1155119 samples, validate on 288780 samples
Epoch 1/50
1155119/1155119 [==============================] - 151s 131us/step - loss: 0.0582 - val_loss: 0.2410
Epoch 2/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0474 - val_loss: 0.2592
Epoch 3/50
1155119/1155119 [==============================] - 151s 131us/step - loss: 0.0430 - val_loss: 0.2173
Epoch 4/50
1155119/1155119 [==============================] - 151s 131us/step - loss: 0.0391 - val_loss: 0.2399
Epoch 5/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0355 - val_loss: 0.2389
Epoch 6/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0323 - val_loss: 0.2710
Epoch 7/50
1155119/1155119 [==============================] - 149s 129us/step - loss: 0.0298 - val_loss: 0.2683
Epoch 8/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0276 - val_loss: 0.2589
Epoch 9/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0256 - val_loss: 0.2915
Epoch 10/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0241 - val_loss: 0.2938
Epoch 11/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0226 - val_loss: 0.3006
Epoch 12/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0215 - val_loss: 0.2986
Epoch 13/50
1155119/1155119 [==============================] - 150s 130us/step - loss: 0.0203 - val_loss: 0.2866
06/11/2019 12:02:26 PM Optimal number of epochs: 3
In [ ]:
# Evaluate the percormance of the weighted models
precision_recall_plot(
    ytrue=ytrue_test, ypreds=ypred_test, model_names=weighted_model_name, file_name="weighted_model"
)
cls_report(
    ytrue=ytrue_test, ypreds_oh=ypred_test_oh, model_names=weighted_model_name
)
epochs_loss_plot(
    histories=weighted_model_history, model_names=weighted_model_name
)
conf_matrix(
    ytrue=ytrue_test, ypreds_oh=ypred_test_oh, model_names=weighted_model_name
)
<Figure size 576x396 with 0 Axes>
Classification report 0.1_0.9_model
OKtoxicaccuracymacro avgweighted avg
f1-score0.9403290.5629090.8949930.7516190.910147
precision0.9852900.4218900.8949930.7035900.940235
recall0.8992920.8455330.8949930.8724120.894993
support332108.00000028867.0000000.894993360975.000000360975.000000
<Figure size 576x396 with 0 Axes>

Jeder Plot zeigt die Ergebnisse der unterschiedlichen Gewichtung. Dabei ist das erste Gewicht immer das Gewicht für die OK Kommentare und das zweite Gewicht ist das Gewicht für die toxic Kommentare. Also, z.B. 0.1_0.9_model ist das Modell mit einer Gewichtung, welche Fehlklassifikationen von toxic Kommentaren 9 mal härter als Fehlklassifikationen von OK Kommentaren.

9. Ergebnis Analyse des gewichteten Modells

Im "Epoch loss plot" kann man sehen, dass das Modell wieder früh anfängt zu overfitten und sehr schnell die Trainingsdaten "auswendig" lernt. Die Precision Recall-Kurven zeigen keine wesentlichen Veränderungen, aber anhand des Classification Reports kann man sehen, dass bei einer höheren Gewichtungen der toxic Kommentare der Recall ansteigt, während die Precision abnimmt. Dies spiegelt sich auch in der Confusion Matrix wieder, weil nun viel mehr Datenpunkte im rechts unteren Eck befinden, also viel mehr toxic Kommentare werden auch als solche bewertet.

Das heißt: Wenn Fehlklassifikationen von toxic Kommentaren härter bestraft werden, dann bewertet das Modell einen Kommentar früher als toxic, um somit einer hohen Bestrafung aus dem Weg zu gehen. Das hat allerdings dann zur Folge, dass auch mehrere OK Kommentare als toxic bewertet werden, wodurch man einen höheren "Beifang" hat. In unserem Fall ist das ein positives Ergebnis, da der Online-Shop toxic Kommentare möglichst nicht zulassen sollte.

10. Zusammenfassung

Wie so häufig bei Klassikationsproblemen, hatten wir eine sehr ungleiche Klassenverteilung von der Zielvariable (OK und toxic Klassifikationen in unserem Fall). Wir haben erst ein naives Modell trainiert und evaluiert und danach ein Modell mit einer anderen Gewichtung ausprobiert, um eine bessere Performance zu erreichen. Einerseitts haben wir es tatsächlich geschafft mehr von den toxic Kommentaren auch also solche zu identifizieren, andererseits wurden leider auch ein paar OK Kommentare als toxic bewertet. Unter dem Strich war das Modell mit der Gewichtung 0.1_0.9 erfolgreicher als das naive Modell, da das gewichtete Modell mehr toxic Kommentare identifiziert hat.

01_comment_classification