Skip to content
Snippets Groups Projects
Select Git revision
  • d48edcd327479e7a230e1a34684f251a5cd47337
  • master default protected
  • 2018ws
  • 2017ws
  • 2016ws
5 results

side-effects-08.c

Blame
  • Forked from Peter Gerwinski / hp
    Source project has a limited visibility.
    Generatoren.tex 14.74 KiB
    \subsection{Generatoren
    \label{subsec:Generator}}
    %Problemstellung
    Spätestens wenn mit etwas größeren Datenmengen trainiert wird stellt sich bei einer unserer Methoden ein neues Problem dar. Während die mean-Methode ohne Probleme durchläuft bricht die CNN-Methode aufgrund mangelnden Arbeitsspeicher ab. %beschwert sich die CNN-Methode, dass nicht genug Arbeitsspeicher vorhanden ist. 
    Dies lässt sich auch ganz leicht nach folgender Formel nachrechnen:
    %Vlt mit Formelzeichen, welche vorher im text definiert werden?
    % => Kürzere Formel & Mehr Text
    \begin{equation}
    \text{Anzahl Einträge }\times \text{ Vektorenanzahl } \times \text{ Vektorendimension } \times \text{ Datentyp-Größe}
    \end{equation}
    Der Datentyp entspricht in unserem Fall float. Ein float wird in Python durch 64 Bit, also 8 Byte dargestellt. Gehen wir davon aus, dass wir alle 8 Millionen Einträge fürs fitten und somit gleichzeitig im Arbeitsspeicher benötigen. Für die mean-Vektor Methode ergibt sich somit folgende Rechnung:
    \begin{equation}
    8,000,000 \times 1 \times 100 \times 8 \approx 6\text{ GB}
    \end{equation}
    Dies könnte mit 8GB RAM etwas knapp werden, jedoch stellt es bei 16GB gar kein Problem dar. Bei der CNN-Methode sieht das jedoch wie folgt aus:
    \begin{equation}\label{eq:430GB}
    8,000,000 \times 72 \times 100 \times 8 \approx 430\text{ GB}
    \end{equation}
    Das Problem ist recht deutlich. Viele Personen hätten auf ihrem privaten PC vermutlich nicht einmal genug Festplattenspeicher für diese Datenmenge, geschweige denn Arbeitsspeicher. \\
    Die Daten müssen bei der CNN-Methode also nur stückweise in den Arbeitsspeicher geladen werden, dann wird mit diesen trainiert und anschließend werden sie wieder aus dem Arbeitsspeicher gelöscht um Platz für die nächsten Daten zu machen. Dabei gibt es wieder verschiedene Ansätze.
    
    \subsubsection{Erster Generator}\label{subsubsec:simpleGen}
    %Erster Ansatz, sehr lange validierungszeit
    Ein möglicher Ansatz ist die rohen Daten nur schrittweise einzulesen und diese dann erst dem word2vec model zu übergeben um daraus einen Vektor zu machen. Dafür wird eine Funktion geschrieben, welche dann der fit Methode des eigentlichen machine learning models als Datensatz-parameter übergeben wird. Dabei wird in der Funktion die batchsize berücksichtigt, sodass stets die gewünschte Menge an Daten gleichzeitig in das model geladen werden. 
    
    Um dies jedoch zu erfüllen müssen die Daten nach ihren Nutzen in verschiedene Dateien aufgeteilt werden. Dementsprechend eine Trainings-, Validierungs- und Test-Datei.
    \begin{lstlisting}[label={lst:Aufteilung}]
    val = open('val.json','w',encoding='utf8')
    test = open('test.json','w',encoding='utf8')
    train = open('train.json','w',encoding='utf8')
    
    i = 0
    for line in open(corpus_path,encoding="utf8"):
        if(i<3): train.write(line)
        elif(i==3): val.write(line)
        else:
            i = -1
            test.write(line) 
        i += 1
    
    val.close()
    test.close()
    train.close()
    \end{lstlisting}
    Das Aufteilen wird durch ein einfaches Zählen und Sortieren gelöst. Dabei zählt eine Variable, hier \lstinline{i}{}, hoch und je nachdem welchen Wert diese Variable hat werden die Daten in die entsprechende Datei sortiert. Das Ziel ist eine 60-20-20 Aufteilung der Daten. Dafür werden bei den Werten \lstinline{0, 1}{} und \lstinline{2}{} die Daten in die Trainingsdatei geschrieben und dementsprechend bei den Werten \lstinline{3}{} und \lstinline{4}{} in die Validierungs-, bzw.\@ Testdatei. 
    Wenn in die Testdatei geschrieben wird, wird auch gleichzeitig die Zählervariable zurückgesetzt, sodass mit den gleichen Werten von vorne begonnen werden kann. Hier wurde sich bewusst für ein Zurücksetzen der Variable entschieden und gegen ein kontinuierliches Hochzählen und der Sortierung durch Modulo: Auf diese Weise werden bedenkenlos Overflow- und Signifikanzprobleme verhindert ohne einen deutlichen Verlust der Lesbarkeit des Codes hinnehmen zu müssen. \wip{Da der genutzte Datensatz nur etwas über 8 Millionen Einträge besitzt und mit Ganzzahlen gearbeitet wird wären beide Probleme zu vernachlässigen gewesen.}
    Diese Art der Aufteilung ist jedoch nur bedenkenlos einzusetzen in diesem Projekt, da die Daten ohnehin unabhängig sind und ebenfalls unsortiert vorliegen. Aus diesen Gründen sind die drei Datensätze auch ohne extra shuffle zufällig genug.
    
    \wip{Überleitung zum Generator? Oder das Aufteilen der Daten in eigenes Kapitel?}
    
    \begin{lstlisting}
    import numpy as np
    import json
    def generate_arrays_from_file(path, batchsize):
        inputs = []
        targets = []
        batchcount = 0
        while True:
            with open(path,encoding="utf8") as f:
                for line in f:
                    json_line = json.loads(line)
    \end{lstlisting}
    Zunächst ein wenig overhead, bevor wir dann die entsprechende Datei zeilenweise durchgehen. Die Zeile 7 mit \lstinline{while True:}{} wird dabei vom Keras Modell erwartet, denn das Trainieren soll nicht mitten in einer Epoche enden, weil das Ende der Trainingsdatendatei erreicht wurde.
    \begin{lstlisting}
                    try:
                        inputs.append(getSentenceVectorCNN(json_line["text"]))
                        y = float(json_line["stars"])
                        if(y <3):
                            targets.append([0,0,1])
                        elif(y==3):
                            targets.append([0,1,0])
                        else:
                            targets.append([1,0,0])
                        batchcount += 1
                    except:
                        continue
    \end{lstlisting}
    Anschließend wird aus dem eigentlichen Text der review ein Wortvektor für das CNN gemacht, \wip{wie zuvor in dem Kapitel X.X (Referenz) beschrieben}. Das Ergebnis ob diese review als positiv, negativ oder neutral gelten soll wird in diesem Beispiel One-Hot-Encoded abgespeichert. Sollte eine Exception geschmissen werden, weil z.B. kein einziges Wort, welches in der review vorkommt im word2vec model vertreten sein wird diese review einfach übersprungen.
    \begin{lstlisting}
                    if batchcount > batchsize:
                      X = np.array(inputs)
                      y = np.array(targets)
                      yield (X, y)
                      inputs = []
                      targets = []
                      batchcount = 0
    \end{lstlisting}
    Abschließend werden die beiden Listen returned und die entsprechenden Variablen zurückgesetzt. Yield entspricht dabei in Python einem return, welches den dazugehörigen Code erst ausführt wenn die Daten gebraucht werden. Die Ansprüche an den Arbeitsspeicher werden auf diese Weise deutlich gesenkt. Es müssen für das Training also nur so viele Daten gleichzeitig im Arbeitsspeicher gehalten werden wie es die batchsize vorgibt. Wenn diese z.B. 512 entspricht fallen dabei, an folgender Formel zu erkennen, lediglich 28 MB an Daten an.
    \begin{equation}
    512 \times 72 \times 100 \times 8 \approx 28\text{ MB}
    \end{equation}
    Ein deutlicher Nachteil bei diesem Vorgehen ist jedoch, dass dies sehr zeitaufwändig ist. Nicht nur müssen die Daten während des Trainings von der Festplatte anstelle des Arbeitsspeichers gelesen werden. Sondern diese Daten müssen zunächst auch noch in das word2vec model überführt werden. Beides sind verhältnismäßig langsame Prozesse, wodurch die Trainingszeit enorm steigt. Aber auch die Validierung benötigt nun in etwa genau so viel Zeit wie das Training an sich.
    Dafür ist diese Methode recht simpel und sie benötigt keinen zusätzlichen Festplattenspeicher. Jedoch muss hierfür der Datensatz für Tranings-, Validierungs- und Testmenge in verschiedene Dateien aufgeteilt werden. Sollte also der Festplattenspeicher und nicht so sehr die Zeit ein begrenzender Faktor sein, ist dies ein durchaus valider Ansatz.
    
    \subsubsection{hdf5}
    %Bessere Zeiten, aber hoher Speicherplatz
    Ein weiterer Ansatz ist die bereits in das word2vec model überführten Wörter, also die Vektoren an sich abzuspeichern. Dafür eignen sich z.B. hdf5 files, welche einen weiteren Vorteil besitzen auf den später eingegangen wird. Das Prinzip ist bei dieser Methode nahezu identisch zu der zuvor besprochenen, auch hier wird der Fit-Methode ein Generator gegeben, dieser liest jedoch nur die entsprechende Datei. Zunächst muss aber der Datensatz in das word2vec model überführt und entsprechend abgespeichert werden. Dies beginnt auch hier wieder mit ein wenig overhead:
    \begin{lstlisting}
    import h5py
    
    path = "path/to/my/workspace/"
    sample_path = path + "sample.json"
    
    with h5py.File(path + "w2vCNN.hdf5", "w") as hf:
    	i = 0
        chunkSize = 10**4
        trainChunk = int(chunkSize * 0.6)
        valTestChunk = int(chunkSize * 0.2)
        xTrain = []
        yTrain = []
        xVal = []
        yVal = []
        xTest = []
        yTest = []
    \end{lstlisting}
    Zu Beginn initialisieren wir eine Zähler-variable auf 0, welche wir später für unsere Aufteilung in Trainings-, Validierungs- und Testmenge nutzen. Für diese Mengen und ihren jeweiligen Input- und Output-daten bereiten wir auch ein paar Listen vor. In hdf5 Dateien kann man die Daten in so genannte Chunks aufteilen, welche das schrittweise speichern möglich macht, aber auch das laden verbessern soll. Die Größe dieser Chunks legen wir zunächst auf 10.000 fest. Da wir einen 60-20-20 split unseres Datensatzes haben wollen speichern wir uns ebenfalls die Größe dieser einzelnen Chunks.
    \begin{lstlisting}
        for index, line in enumerate(open(corpus_path,encoding="utf8")):
            json_line = json.loads(line)
            #X Data
            xData = []
            try:
                xData = getSentenceVectorCNN(json_line["text"])
            except:
                continue
            y = float(json_line["stars"])
            if y < 3:
                yData = 0
            elif y == 3:
                yData = 1
            else:
                yData = 2
    \end{lstlisting}
    Anschließend wird quasi identisch \wip{zu der anderen Methode (Ref?)}der rohe Datensatz ausgelesen und entsprechend in nutzbare x und y Daten umgewandelt.
    \begin{lstlisting}
            if i < 3:
                xTrain.append(xData)
                yTrain.append(yData)
            elif i == 3:
                xVal.append(xData)
                yVal.append(yData)
            else:
                xTest.append(xData)
                yTest.append(yData)
                i = -1
            i += 1
    \end{lstlisting}
    Als nächstes werden die Daten den entsprechenden Listen angehangen. Das vorgehen hier orientiert sich dabei an der zuvor besprochenen Aufteilung aus aus dem Code-Schnipsel \ref{lst:Aufteilung}.
    \begin{lstlisting}
            if index == chunkSize - 1:      
                XTrain = hf.create_dataset("XTrain", data=xTrain, maxshape=(None, 72, 100), chunks=(trainChunk, 72, 100))
                YTrain = hf.create_dataset("YTrain", data=yTrain, maxshape=(None, 3), chunks=(trainChunk, 3))
            
                XVal = hf.create_dataset("XVal", data=xVal, maxshape=(None, 72, 100), chunks=(valTestChunk, 72, 100))
                YVal = hf.create_dataset("YVal", data=yVal, maxshape=(None, 3), chunks=(valTestChunk, 3))
            
                XTest = hf.create_dataset("XTest", data=xTest, maxshape=(None, 72, 100), chunks=(valTestChunk, 72, 100))
                YTest = hf.create_dataset("YTest", data=yTest, maxshape=(None, 3), chunks=(valTestChunk, 3))  
            
                #reset Buffer-Data
                xTrain = []
                yTrain = []
                xVal = []
                yVal = []
                xTest = []
                yTest = []
    \end{lstlisting}
    Wenn das erste mal genug Daten in den buffer-listen liegen können diese noch nirgends appended werden, da in der entsprechenden Datei noch nichts vorliegt wo diese appended werden können. Damit kommen wir auch zu einem weiteren Vorteil der hdf5 Dateien: Wir können verschiedene Datasets, mit entsprechenden Labeln, in der gleichen Datei speichern und diese später auch einfach wieder auslesen. \\
    Diese Datasets müssen aber zunächst erstellt werden. Dafür geben wir zum einen das Label des Datasets an; die dimensionen der maxshape, welche als Anzahl der Datensätze den Eintrag None bekommt, da wir einfach alle Daten die wir haben speichern wollen; die Dimensionen der chunks, wofür wir die eingangs berechneten Chunkgrößen nun nutzen; und zu guter letzt natürlich die tatsächlichen Daten, welche stets die gleichen Dimensionen haben müssen wie die Chunks. Abschließend resetten wir die buffer-listen um keine Daten mehrfach zu speichern.
    \begin{lstlisting}
            if (index+1) % chunkSize == 0 and index > chunkSize:
                XTrain.resize(XTrain.shape[0]+trainChunk, axis=0)
                XTrain[-trainChunk:] = xTrain
                
                YTrain.resize(YTrain.shape[0]+trainChunk, axis=0)
                YTrain[-trainChunk:] = yTrain
    
                XVal.resize(XVal.shape[0]+valTestChunk, axis=0)
                XVal[-valTestChunk:] = xVal
                
                YVal.resize(YVal.shape[0]+valTestChunk, axis=0)
                YVal[-valTestChunk:] = yVal        
    
                XTest.resize(XTest.shape[0]+valTestChunk, axis=0)
                XTest[-valTestChunk:] = xTest
                
                YTest.resize(YTest.shape[0]+valTestChunk, axis=0)
                YTest[-valTestChunk:] = yTest
                
                #reset Buffer-Data
                xTrain = []
                yTrain = []
                xVal = []
                yVal = []
                xTest = []
                yTest = []
    \end{lstlisting}
    Für das appenden der Daten im Folgenden Verlauf müssen die Datasets stets um die entsprechende Größe resized werden. Anschließend werden die neuen Felder entsprechend aufgefüllt und zu guter Letzt wieder die buffer-listen resetted.\\
    Das Auslesen der Daten erfolgt letztlich ähnlich wie bei dem anderen vorgestellten Generator:
    \begin{lstlisting}
    def hdf5Generator(filePath, batch_size, dataSet, loop=True):
        with h5py.File(filePath, 'r') as hf:
            L = len(hf["X" + dataSet])
            while True:
                batch_start = 0
                batch_end = batch_size
                
                while batch_end < L:
                    X = hf["X" + dataSet][batch_start:limit]
                    Y = hf["Y" + dataSet][batch_start:limit]
        
                    yield (X,Y)    
        
                    batch_start += batch_size   
                    batch_end += batch_size
                if not loop: break
    \end{lstlisting}
    Die Hauptunterschiede sind dabei das hier entsprechend anfallende Zugreifen des passenden Datasets, das Wegfallen der word2vec Überführung und der zusätzliche check ob \lstinline{batch_size}{} über das Ende der Datasets gehen würde.\\
    Der Vorteil bei diesem Vorgehen ist eine bessere Trainingszeit und vor allem eine bessere Validierungszeit gegenüber dem zuvor besprochenen Generator. Jedoch braucht diese Variante ein vielfaches mehr Festplattenspeicher und zwar so viel wie in der zuvor berechneten Gleichung \ref{eq:430GB}:
    \begin{equation*}
    8,000,000 \times 72 \times 100 \times 8 \approx 430\text{ GB}\tag{\ref{eq:430GB} revisited}
    \end{equation*}
    Das Erstellen dieser Datenmengen benötigt aber auch entsprechende Zeit. Dieses Vorgehen lohnt sich also besonders, wenn das Word2Vec Modell nicht mehr oder nur noch selten verändert wird und somit häufig auf dem gleichen Modell trainiert werden kann.