Select Git revision
filter_images_V3.py
w2vMean.tex 10.41 KiB
\newpage
\subsection{Mean-Vektor-Klassifikationsmodell}
%Durchschnittsvektor-Klassifkationsmodell ??
Das Word2Vec-Modell bildet einen Vektorraum, indem ähnliche Wörter nahe
beieinander liegen.
Es ist somit denkbar, dass Wörter, die mit einer negativen Rezension assoziiert
werden, ein anderes Gebiet des Vektorraumes belegen als jene, die mit positiven Rezensionen
assoziiert werden.
Der Gedanke für dieses Modell ist es, den Mittelwert aller Wortvektoren in
einer Rezension zu ermitteln und somit eine Art „Satzvektor“ zu erhalten. Diese Satzvektoren
werden als Eingangswerte genutzt und mithilfe eines neuronalen Netzes eine
Klassifikation der Rezensionen durchzuführen.
\subsubsection{Implementierung}
Durch die Bildung der Satzvektoren verringert sich der benötigte Speicherbedarf,
somit ist es möglich, auf die Generatoren zu verzichten und die Trainingsdaten im
Arbeitsspeicher zu halten.
Dies hilft, die benötigte Trainingszeit zu reduzieren.
Sollte die Arbeitsspeicherkapazität nicht ausreichen, befindet sich eine Implementierung, die Generatoren benutzt im Anhang.
\wip{referenzieren und Tatsächlich einfügen}
Um das Word2Vec-Modell zu erhalten, importieren wir die im Listing \ref{list:gw2v} geschriebene Funktion
\lstinline{getWordVecModel}.
\begin{lstlisting}[caption={Satzvektorfunktion},label={list:mean1}]
from gensim import utils
from w2v_yelp_model import getWordVecModel
import json
model_path = "full_yelp_w2v_model"
data_path ="yelp_academic_dataset_review.json"
modelW2V = getWordVecModel(model_path)
def getSentenceVector(sentence):
split = utils.simple_preprocess(sentence)
wordVecs = []
for word in split:
try:
wordVecs.append(modelW2V.wv[word])
except:
pass
if wordVecs == []:
raise Exception('words not found in w2v model')
return np.mean(wordVecs,axis=0)
\end{lstlisting}
Die Funktion \lstinline{getSentenceVector} in Zeile 8 nimmt eine Rezension entgegen und
teilt diese in Zeile 9 in ihre Wörter auf. Danach wird jedes Wort geprüft,
ob es im Word2Vec-Modell hinterlegt ist, ist dies der Fall, wird der Wortvektor der
Liste \lstinline{wordVecs} hinzugefügt.
Ist kein einziges Wort der Rezension im Word2Vec-Modell enthalten, wird eine Exception geworfen.
In Zeile 18 wird der Durschnitt aller Wortvektoren zurückgeben.
Um die Daten für das Klassifikationsmodell zu generieren, muss der Datensatz in die Satzvektoren transformiert werden.
\begin{lstlisting}[caption={Daten},label={list:mean2},firstnumber=19]
import numpy as np
from sklearn.model_selection import train_test_split
try:
X = np.load("X.npy")
Y = np.load("Y.npy")
except:
X = []
Y = []
for index, line in enumerate(open(data_path,encoding="utf8")):
json_line = json.loads(line)
#X Data
try:
X.append(getSentenceVector(json_line["text"]))
except:
continue
y = float(json_line["stars"])
if(y <3):
Y.append(0)
elif(y==3):
Y.append(1)
else:
Y.append(2)
X = np.array(X)
Y = np.array(Y)
np.save("X.npy",X)
np.save("Y.npy",Y)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, random_state=42)
\end{lstlisting}
Falls die Daten bereits generiert wurden, werden diese geladen.
Sind keine Daten vorhanden,
wird der gesamte Datensatz durchlaufen und für jede einzelne Rezension der Satzvektor ermittelt.
Befindet sich kein Wort der Rezension im Word2Vec-Modell so wird diese Rezension in Zeile 33 übersprungen.
Die Bewertungen werden wie folg codiert: Negativ (0), Neutral (1) und Positiv (2).
Anschließend werden die generierten Daten gespeichert, um bei mehrfacher Ausführung zeit
zu sparen.
Danach werden die Daten in Zeile 45 in Trainings- und Testdaten aufgeteilt, hierfür wird die Funktion \lstinline{train_test_split} von \lstinline{sklearn} verwendet.
Die Rezensionen liegen jetzt in Form von Satzvektoren bzw. genau genommen Rezensionsvektoren vor, anhand dieser Daten kann nun ein neuronales Netz trainiert werden.
\begin{lstlisting}[caption={Neuronales Netz},label={list:mean3},firstnumber=46]
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow import keras
modelNN = Sequential()
modelNN.add(Dense(50,activation='relu',input_dim=X[0].size))
modelNN.add(Dense(15,activation='relu'))
modelNN.add(Dense(3,activation='softmax'))
modelNN.compile(optimizer='adam',loss='sparse_categorical_crossentropy',metrics=["sparse_categorical_accuracy"])
\end{lstlisting}
Hier wurde ein dichtes mehrschichtiges neuronales Netzwerk verwendet, wobei die
Eingangsdimension, der Dimension der Satzvektoren (in diesem Fall 100) entspricht.
Bis auf die Ausgangsschicht wurde für alle Schichten des neuronalen Netzwerks
die Aktivierungsfunktion \lstinline{relu} verwendet.
Die Ausgangsschicht selbst verwendet die Aktivierungsfunktion
\lstinline{softmax}.
In dieser Implementation wurden die hinteren Schichten, wie bei
Klassifikationen üblich, bewusst kleiner gewählt, um eine Verdichtung der Informationen zu erzwingen.
Nachdem die Struktur des neuronalen Netzwerkes nun feststeht, muss das Netz noch kompiliert werden. Hierzu werden in
Zeile 54 einige Parameter gesetzt. Für den Optimierungsalgorithmus wird \lstinline{adam} gewählt.
Für die Fehlerfunktion, hier loss genannt, wird \lstinline{sparse_categorical_crossentropy} benutzt.
Die Fehlerfunktion \lstinline{sparse_categorical_crossentropy} ermöglicht im Gegensatz zur
\lstinline{binary_crossentropy} mehr als zwei Klassen zu Klassifizieren. Eine weite
Mögliche Fehlerfunktion ist die \lstinline{categorical_crossentropy}, hierzu müssten
bloß die Zielwerte \noteable{One-Hot-Encoded} werden.
Zuletzt wird für den Parameter \lstinline{Metrics} der Wert \lstinline{sparse_categorical_accuracy}
übergeben.
Nach dem Kompilieren des neuronalen Netzes ist jetzt das Trainieren, also das Optimieren der Gewichte, der nächste Schritt.
\begin{lstlisting}[caption={Neuronales Netz - Trainieren},label={list:mean4},firstnumber=55]
earlystop = keras.callbacks.EarlyStopping(monitor='val_sparse_categorical_accuracy',patience=10,verbose=False,restore_best_weights=True)
cbList = [earlystop]
count = np.unique(Y_train,return_counts=True)[1]
cWeight = 1/(count/Y_train.size)
hist = modelNN.fit(X_train,Y_train,epochs=1000,validation_split=0.2,batch_size=2048,class_weight={0:cWeight[0],1:cWeight[1],2:cWeight[2]},callbacks=cbList
\end{lstlisting}
Um ein Overfitting zu vermeiden und die Trainingszeit zu minimieren, wird in Zeile 55
ein \noteable{Early-Stop-Callback} definiert.
Der vorzeitige Stopp des Trainierens tritt ein, wenn 10 Epochen
lang keine Verbesserung auf der Validierungsmenge aufzuzeigen ist.
Hierzu wird die Metrik \lstinline{val_sparse_categorical_accuracy} überwacht.
Zusätzlich werden die Gewichte, die zu dem besten Ergebnis geführt haben, nach
dem vorzeitigen Stopp wieder hergestellt.
In Zeile 59 findet das tatsächliche Training des neuronalen Netzes statt.
Hierfür wird der Methode \lstinline{fit} die Trainingsdaten Übergeben. Der Parameter
\lstinline{validation_split = 0.2} sorgt dafür das $20 \%$ der Trainingsdaten als
Validierungsmenge genommen werden, hierbei ist aber anzumerken das es sich bei den
Trainingsdaten ohnehin schon nur um $80 \%$ des Datensatzes handelt,
weswegen die tatsächliche Validierungsmenge $16 \%$ des Datensatzes beinhaltet.
Die \lstinline{batch_size} wird aufgrund des großen Datensatzes auf 2048 gesetzt, um das Optimieren der Gewichte zu
beschleunigen. Eine kleinere \lstinline{batch_size} hat beim Experimentieren in disem Fall nicht zu einer schnelleren Konvergenz
geführt.
Wie in Kapitel \ref{subsubsec:Dist} diskutiert gibt es verschiedene Umgänge mit unausgeglichenen Datensätzen. Die für diese Arbeit gewählte Methode der Gewichtung ist hier zu erkennen an den, an
die Methode \lstinline{fit} Übergebenen, Klassengewichte.
\wip{vll noch erklären wie die Gewichte entstehen}
\begin{lstlisting}[caption={Neuronales Netz - Evaluieren},label={list:mean5},firstnumber=60]
modelNN.evaluate(X_test,Y_test)
\end{lstlisting}
Um das Modell zu evaluieren, wird in Zeile 60 die Methode evaluate mit den Testdaten aufgerufen.
Das hier trainierte neuronale Netzwerk klassifiziert $80.02 \%$ der Testdaten richtig.
\subsubsection{Konfusionsmatrix}
Die bereits berechnete Genauigkeit ist ein gutes erstes Leistungsmerkmal des
Klassifikators, um den Klassifikator noch besser Einschätzen zu können,
lohnt es sich, eine Konfusionsmatrix aufzustellen.
Hierfür wird die Funktion \lstinline{confusion_matrix} von \lstinline{sklearn} benutzt.
\begin{lstlisting}[caption={Konfusionsmatrix},label={list:mean6},firstnumber=61]
from sklearn.metrics import confusion_matrix
y_pred = np.argmax(modelNN.predict(X_test),axis=-1)
confusion_matrix(Y_test,y_pred,normalize='true')
\end{lstlisting}
\begin{table}[ht]
\def\arraystretch{1.3}
\begin{center}
\begin{tabular}{*{4}{R}}
- &Negativ & Neutral & Positiv\\
Negativ &0.8046 & 0.1727 & 0.0228 \\
Neutral &0.1682 & 0.6844 & 0.1474 \\
Positiv &0.0242 & 0.1586 & 0.8172 \\
\end{tabular}
\end{center}
\caption{Konfusionsmatrix mit Klassengewichtung}
\label{tab:m_w}
\end{table}
Wie in Tabelle \ref{tab:m_w} zu sehen ist, werden negative Bewertungen zu $80.46\%$
richtig als negative Bewertung klassifiziert, $17.27\%$ werden als
neutral klassifiziert und $2.28\%$ werden als positiv klassifiziert.
Neutrale Bewertungen werden zu $68.44\%$ richtig klassifiziert,
jedoch werden $16.82\%$ der neutralen Bewertungen falsch als
negative klassifiziert und zu $14.74\%$ falsch als positiv klassifiziert.
Die positiven Bewertungen werden zu $81.72\%$ richtig als positive
Bewertungen klassifiziert, zu $15.86\%$ als neutral und zu
$2.42\%$ als negativ klassifiziert.
Das gleiche Modell ohne die Gewichtung der Klassen erreicht eine Genauigkeit
von $85.7\%$, betrachtet man jedoch die Konfusionsmatrix in Tabelle \ref{tab:conf_no_w}, so
sieht man das dort bloß $27\%$ der neutralen Rezensionen richtig klassifiziert wurden.
\begin{table}[ht]
\def\arraystretch{1.3}
\begin{center}
\begin{tabular}{*{4}{R}}
- &Negativ & Neutral & Positiv\\
Negativ &0.8567& 0.0602& 0.0831\\
Neutral &0.2571& 0.2708& 0.4720\\
Positiv &0.0249& 0.0235& 0.9516\\
\end{tabular}
\end{center}
\caption{Konfusionsmatrix ohne Klassengewichtung}
\label{tab:conf_no_w}
\end{table}