Select Git revision
w2vFromScratch.tex
w2vFromScratch.tex 8.42 KiB
\subsubsection{w2v from scratch
\label{subsec:w2vfs}}
Da Word2Vec ein großer Bestandteil dieser Ausarbeitung ist, jedoch durch einen Import gelöst wurde, wird in diesem Kapitel anhand eines simplen Beispiels das Prinzip dahinter erklärt. Dabei werden hier keine hochdimensionalen Wort-Vektoren verwendet, welche letztlich durch ihre Cluster ausgeprägte Bedeutungsräume bilden. Sondern es werden die Beziehungen der Wörter zu ihren Nachbarn dargestellt, indem anhand eines Wortes das Nachbarwort bestimmt werden soll.
\begin{lstlisting}[caption=Datensatz Word2Vec from scratch]
corpus = ['king is a strong man',
'queen is a wise woman',
'boy is a young man',
'girl is a young woman',
'prince is a young king',
'princess is a young queen',
'man is strong',
'woman is pretty',
'prince is a boy will be king',
'princess is a girl will be queen']
\end{lstlisting}
Unser Datensatz ist dabei recht überschaubar und auch in einer einfachen Struktur gehalten. Auf diese Weise lassen sich die Interaktionen und Einflüsse der Wort-Vektoren übersichtlicher darstellen.
\begin{lstlisting}[caption=Entfernen der Stoppwörter, firstnumber=10]
def remove_stop_words(corpus):
stop_words = ['is', 'a', 'will', 'be']
results = []
for text in corpus:
tmp = text.split(' ')
for stop_word in stop_words:
if stop_word in tmp:
tmp.remove(stop_word)
results.append(" ".join(tmp))
return results
corpus = remove_stop_words(corpus)
\end{lstlisting}
Zunächst entfernen wir die in Kapitel \ref{subsec:vorverarbeitung} bereits erklärten stop words. Dadurch, dass wir hier ein kurzes, akademisches Beispiel haben ist auch die Liste der stop words eher kurz. In der Regel sind Wörter wie die auch hier genutzten in diesen Listen zu finden, aber je nach Anwendungsfall kann es auch Sinn ergeben diese mit ungewöhnlicheren Wörtern zu füllen.
\begin{lstlisting}[caption=Nachbarwörter speichern, firstnumber=23]
from ordered_set import OrderedSet
words = OrderedSet()
for text in corpus:
for word in text.split(' '):
words.add(word)
word2int = {}
for i,word in enumerate(words):
word2int[word] = i
sentences = []
for sentence in corpus:
sentences.append(sentence.split())
WINDOW_SIZE = 2
data = []
for sentence in sentences:
for idx, word in enumerate(sentence):
for neighbor in sentence[max(idx - WINDOW_SIZE, 0) : min(idx + WINDOW_SIZE, len(sentence)) + 1] :
if neighbor != word:
data.append([word, neighbor])
import pandas as pd
df = pd.DataFrame(data, columns = ['input', 'neighbour'])
\end{lstlisting}
Als nächstes erstellen wir ein \lstinline{OrderedSet}{}, welches alle Wörter nur ein einziges Mal enthält und nummerieren dieses durch. Dies wird im späteren Verlauf noch benötigt, wobei die Reihenfolge wichtig für die korrekte Visualisierung ist. Anschließend erstellen wir ein Datenset, welches alle Wörter mit ihren jeweiligen Nachbarn enthält. Dabei entspricht die \lstinline{WINDOW_SIZE}{} in Zeile 17 wie viele Nachbarwörter betrachtet werden sollen. Schließlich haben in langen Sätzen Wörter, welche zum Ende eines Nebensatzes stehen, häufig eine eher schwache oder gar keine Bindung zu den ersten Wörtern des Hauptsatzes. Die ideale Größe der \lstinline{WINDOW_SIZE}{} hängt dabei individuell vom Text ab. Jedoch ist in der Regel eine eher kleine \lstinline{WINDOW_SIZE}{} zu empfehlen, +-2 bis 3 Wörter sind in vielen Fällen genug.
% Die +- 2 bis 3 hab ich mir ausgedacht und kann sie dementsprechend nicht belegen%
Abschließend wird das Datenset in ein pandas Dataframe überführt, wodurch das Arbeiten mit diesem erleichtert wird. Weiterhin sind noch 2 Aspekte des Datensets auffällig. Zum einen werden die Wörter mit ihren Nachbarn zwangsweise bidirektional abgespeichert. Die Erklärung dafür ist simpel: Die Beziehung zwischen den Wörtern ist schließlich auch bidirektional und so wird sichergestellt, dass im späteren Verlauf des Codes diese Bidirektionalität auch eingehalten wird, ohne dass darauf explizit geachtet werden muss. Zum anderen werden mehrfach auftretende Kombinationen auch mehrfach abgespeichert und nicht im Nachhinein wieder auf ein unique-dataset bereinigt. Das kommt daher, dass diese Häufigkeit essenzielle Daten enthält. Man geht letztlich davon aus, dass Wörter, die häufig zusammen vorkommen eine stärkere Bindung haben und auch außerhalb des benutzten Datensets verstärkt gemeinsam auftreten.
\begin{lstlisting}[caption=Beispielmodell,firstnumber=51]
import tensorflow as tf
import numpy as np
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense
from tensorflow import keras
ONE_HOT_DIM = len(words)
EMBEDDING_DIM = 2
# function to convert numbers to one hot vectors
def to_one_hot_encoding(data_point_index):
one_hot_encoding = np.zeros(ONE_HOT_DIM)
one_hot_encoding[data_point_index] = 1
return one_hot_encoding
X = [] # input word
Y = [] # target word
for x, y in zip(df['input'], df['neighbour']):
X.append(to_one_hot_encoding(word2int[ x ]))
Y.append(to_one_hot_encoding(word2int[ y ]))
# convert them to numpy arrays
X_train = np.asarray(X)
Y_train = np.asarray(Y)
model = Sequential()
model.add(Dense(EMBEDDING_DIM,activation='relu',input_dim=X_train.shape[1]))
model.add(Dense(X_train.shape[1], activation = "softmax"))
model.compile(optimizer='adam',loss='categorical_crossentropy',metrics=["accuracy"])
model.fit(X_train,Y_train,epochs=1000,verbose=True)
\end{lstlisting}
Bevor mit den Daten trainiert werden kann müssen die Wörter in eine dafür nutzbare Form übertragen werden. Dies geschieht in diesem Beispiel durch ein One-Hot-Encoding. Dabei wird das zuvor erstellte Dataset der durchnummerierten, einzigartigen Wörter zu Hilfe genommen. Entsprechend der ursprünglichen Zielsetzung entsprechen die Ausgangswörter den Eingangsdaten und die Nachbarwörter den Zieldaten. Zum Schluss werden diese Daten für das Training in numpy arrays übertragen und einem einfachen Modell fürs Training übergeben.
\begin{lstlisting}[caption=Plotten, firstnumber=84]
vectors = np.array(model.weights[0])
w2v_df = pd.DataFrame(vectors, columns = ['x1', 'x2'])
w2v_df.insert(0, "word", words)
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
for word, x1, x2 in zip(w2v_df['word'], w2v_df['x1'], w2v_df['x2']):
ax.annotate(word, (x1,x2 ))
PADDING = 1.0
x_axis_min = np.amin(vectors, axis=0)[0] - PADDING
y_axis_min = np.amin(vectors, axis=0)[1] - PADDING
x_axis_max = np.amax(vectors, axis=0)[0] + PADDING
y_axis_max = np.amax(vectors, axis=0)[1] + PADDING
plt.xlim(x_axis_min,x_axis_max)
plt.ylim(y_axis_min,y_axis_max)
plt.rcParams["figure.figsize"] = (10,10)
plt.show()
\end{lstlisting}
Die eigentliche Arbeit ist damit bereits getan. Da dieses Beispiel jedoch zur Veranschaulichung gedacht ist fehlt noch die Visualisierung. Dafür werden die Gewichte der jeweiligen Vektoren zunächst zusammen mit den entsprechenden Wörtern in ein Dataframe gespeichert. Anschließend wird der Plot vorbereitet und die Wörter werden anhand ihrer zweidimensionalen Vektoren auf einem Koordinatensystem verteilt. Das hier verwendete Beispiel ist insgesamt sehr akademisch gestaltet. Dadurch, dass zwei Dimensionen für Wortvektoren ziemlich gering ist, das Modell nicht ausführlich optimiert und auch die Ausgangsdaten relativ spärlich sind, können die Ergebnisse stark voneinander variieren. Eine sinnvolle Verteilung ist jedoch in Abbildung \ref{fig:scratch} zu sehen.
\begin{figure}
\includegraphics[width=\linewidth]{bilder/scratch.png}
\caption{Wort-Vektoren Verteilung}
\label{fig:scratch}
\end{figure}
Hier ist zu erkennen, dass die Begriffe, welche eher weiblich sind im oberen Bereich und dementsprechend die männlichen im unteren Bereich sind. Weiterhin sind die allgemeinen Begriffe \lstinline{man}{} und \lstinline{woman}{} eher rechts zu finden, während spezifische Wörter wie \lstinline{king}{} oder \lstinline{queen}{} sich eher links befinden. In dieser Darstellung funktioniert auch das berühmte Beispiel von:
\begin{equation}
\vec{king} - \vec{man} + \vec{woman} \approx \vec{queen}
\label{eqn:kingqueen}
\end{equation}
mit den grob abgelesenen Werten:
\begin{equation}
% king
\begin{pmatrix}
-0,5\\
-0,5
\end{pmatrix}
-
% man
\begin{pmatrix}
1,5\\
-0,5
\end{pmatrix}
+
% woman
\begin{pmatrix}
1,75\\
1
\end{pmatrix}
=
% queen
\begin{pmatrix}
-0.25\\
1
\end{pmatrix}
\end{equation}