bibliography: literatur/diplom.bib

d0f3e60e9e105e678e6eb9d5fb54ee0d817764ef.jpg

Bachelorarbeit
Vergleich zweier Ansätze zur Absatzprognose für Produkte eines Einzelhandelsunternehmens mit Hilfe von künstlichen neuronalen Netzen mit LSTM-Zellen
Sören Borgstedt

Gutachter:
Dr. Lars Hildebrand
Prof. Dr. Jakob Rehof

Technische Universität Dortmund
Fakultät für Informatik
Software Engineering (LS-14)
https://ls14-www.cs.tu-dortmund.de

Einleitung

Motivation und Hintergrund

Bei vielen Einzelhandelsunternehmen, wie TEDi, ist die sogenannte Absatzprognose ein grundlegender betriebswirtschaftlicher Prozess. Durch Trends, saisonale Effekte oder politische Entscheidungen unterliegt der Absatz permanenten Veränderungen. Das Ziel der Absatzprognose ist die Vorhersage von Verkaufszahlen beliebiger Produkte für einen bestimmten Zeitraum. Durch sie können im Anschluss weitere Prozesse, wie die Bestellung von Artikeln oder die Belieferung von Filialen, geplant werden. Diesen zeitintensiven Prozess der Absatzplanung zu optimieren, ist das Ziel aktueller Forschungen.

Seit einigen Jahren steht besonders der Bereich der künstlichen Intelligenz – insbesondere das Teilgebiet des Machine Learnings – im Fokus (siehe Studie (Brosset et al. 2019).). Laut dieser Studie ist es beispielsweise dem Unternehmen Danone gelungen, durch den Einsatz von Machine Learning die Prognosefehler um 20 Prozent sowie die Umsatzverluste um 30 Prozent zu senken.

Einordnung von Deep Learning (Seck, n.d.)

Seit den 1950er Jahren gibt es erste Ideen und Anwendungen von künstlicher Intelligenz. Im Laufe der Zeit wurde das Teilgebiet des maschinellen Lernens entwickelt. Das sogenannte Deep Learning ist ein noch recht junges Teilgebiet des maschinellen Lernens und löste in den letzten Jahren einen KI-Boom aus (vgl. Abbildung 1.1). Das Deep Learning erlaubt es Computern zu lernen, komplexe Aufgaben zu lösen, wie beispielsweise gesprochene Sprache in Text umzuwandeln, Text zu übersetzen, Tumore auf Bildern oder Spam in E-Mails zu erkennen (Deru and Ndiaye 2020).

Beim maschinellen Lernen wird dem Computer mithilfe von Algorithmen geholfen die Strukturen, die zur Lösung von Entscheidungs- und Bewertungsproblemen erforderlich sind, zu erkennen, sodass sie später auf einen spezifischen Anwendungsfall übertragen werden können (Deru and Ndiaye 2020). Diese Arbeit betrachtet Zeitreihenvorhersagen im Detail, die mithilfe von künstlichen rekurrenten neuronalen Netzen mit LSTM-Zellen erzeugt wurden. Zeitreihen bestehen aus einer Menge von zeitlich geordneten Messwerten (Schlittgen and Streitberg 2001). Künstliche neuronale Netze sind eine Abstraktion des Neuronennetzes des menschlichen Gehirns. Die künstlichen Neuronen werden, wie die Neuronen im Gehirn, durch mehrere Schichten miteinander verknüpft (Deru and Ndiaye 2020; Purkait 2019).

Der erste Ansatz, auf den in dieser Arbeit zurückgegriffen wird, stammt aus der offiziellen Tensorflow-Dokumentation1 und zeigt, wie Zeitreihenvorhersagen mit dem Tensorflow-Framework funktionieren. Dabei werden Wetterdaten im Zeitraum $t_0$ bis $t_n$ betrachtet, mit deren Hilfe dann die Temperatur für einen Zeitpunkt $t_{n+1}$ bestimmt wird. Zusätzlich gibt es eine überarbeitete Version (TensorFlow 2020b) des ersten Ansatzes ((TensorFlow 2020a)), die in dieser Arbeit ebenfalls betrachtet werden soll.

Im zweiten Ansatz wird durch eine Zeitreihenvorhersage der monatliche Absatz eines Produkts für die nächsten sechs Monate bestimmt, indem mithilfe der Differenzen der letzten zwölf Monate die Differenz für den nächsten Monat vorhergesagt wird. Dazu wird zunächst die Differenz der Absätze des aktuellen Monats, mit denen des vorherigen Monats gebildet ($\textrm{diff}{curr_i} = x_i - x{i-1}$). Anschließend wird die Differenz der Absätze des Vormonats mit denen des Vorvormonats gebildet usw., bis die Differenzen für die letzten zwölf Monate gebildet wurden ($\textrm{diff}{curr}=(\textrm{diff}{curr-1}, \dots, \textrm{diff}_{curr-12})$).

Ziel der Arbeit

Ziel der Arbeit ist es zu prüfen, ob der zeitaufwendige Prozess der Absatzplanung automatisiert werden kann. Dafür sollen mithilfe von rekurrenten neuronalen Netzen Absatzprognosen für einige Produkte generiert und im Detail näher betrachtet werden. Hauptsächlich wird sich hierbei auf zwei Ansätze für Zeitreihenvorhersagen bezogen.

Die Hauptaufgabe besteht hierbei zunächst darin, die beiden Ansätze zur Zeitreihenvorhersage ((TensorFlow 2020a, 2020b; Karaman 2019)) aufzuarbeiten und auf TEDi-Artikel anzupassen. Weiterhin muss ein Tool implementiert werden, mit dem Produkte aus dem TEDi-Sortiment, die für eine Vorhersage infrage kommen, gefunden und in ein eigenes Datenbankschema kopiert werden, sodass der Betrieb nicht gestört und die Datenabfrage schneller ausgeführt wird. Anschließend können die Ansätze miteinander verglichen werden, indem sie mit verschiedenen TEDi-Artikeln validiert werden.

Durch diese Rahmenbedingungen ergeben sich mehrere Fragestellungen, die in diesem Kontext bearbeitet werden sollen.

  1. Wie sehen die Ansätze zur Zeitreihenvorhersage im Detail aus?

  2. Kann man die Ansätze der Zeitreihenvorhersage miteinander hinsichtlich folgender Kriterien vergleichen?

    • Fehlerrate

    • Laufzeit des Trainings

    • Speicherbelegung während des Trainings

    • Erkennung von saisonalen Effekten

  3. Wie wirken sich Änderungen der Granularität der Daten aus?

  4. Lassen sich weitere Kriterien für einen Vergleich finden?

  5. Welcher Ansatz ist unter Berücksichtigung der Vergleichskriterien effizienter?

  6. Gibt es, abgesehen vom Absatz, weitere Kennzahlen, mit denen die Ansätze trainiert werden können?

Aufgabenstellung und Zielsetzung

In dieser Bachelorarbeit sollen zwei Ansätze zur Zeitreihenvorhersage mithilfe von künstlichen neuronalen Netzen mit LSTM-Zellen aufgearbeitet und näher betrachtet werden. Anschließend werden die Ansätze so angepasst, dass sie mit den zugrunde liegenden Absatzdaten von TEDi, einem Einzelhandelsunternehmen, eine Absatzprognose generieren können. Dafür ist es notwendig, dass passende Produktdaten gefunden und in eine zu den Ansätzen passende Form transformiert werden. Im Anschluss werden die Ansätze miteinander verglichen und untersucht.

Aufbau der Arbeit

Die Arbeit lässt sich in sieben Kapitel unterteilen. Das erste Kapitel (1) leitet in die Thematik ein und erläutert die Motivation, die Aufgabenstellung und die Struktur der Arbeit. Im zweiten Kapitel (2) werden Grundlagen, die im weiteren Verlauf der Arbeit benötigt werden, um Absatzprognosen mithilfe von neuronalen Netzen darstellen zu können, vorgestellt. Zunächst wird erklärt, was eine Absatzprognose (Abschnitt 2.1) ist. Anschließend folgen verschiedene Technologien und Frameworks (Abschnitt 2.3), die im Rahmen der Arbeit verwendet wurden. Dazu zählen beispielsweise Python (siehe Abschnitt 2.3.7), Node.js (siehe Abschnitt 2.3.2) und Angular (siehe Abschnitt 2.3.6). In Abschnitt 2.4 werden die für diese Arbeit vorliegenden Datenstrukturen erläutert. Das dritte Kapitel (3) greift die zuvor erklärten Datenstrukturen auf und stellt die Implementierung einer Software vor, mit der Produkte gefunden, analysiert und in eine andere Datenstruktur überführt werden können. Im vierten Kapitel (4) werden zwei Ansätze von Google (TensorFlow 2020a, 2020b) zur Zeitreihenvorhersage mit künstlichen neuronalen Netzen vorgestellt und auf eine Absatzprognose mit den zugrunde liegenden Daten angepasst. Das nachfolgende fünfte Kapitel (5) führt einen weiteren Ansatz (Karaman 2019) zur Absatzprognose mit künstlichen neuronalen Netzen ein. Das sechste Kapitel greift die beschriebenen Ansätze aus Kapitel vier (4) und fünf (5) auf und vergleicht diese. Kapitel sieben (7) fasst die Arbeit zusammen, zieht ein Fazit und gibt einen Ausblick.

Grundlagen

In diesem Kapitel wird in die für diese Arbeit erforderlichen Grundlagen eingeleitet. Zunächst wird erklärt, was eine Absatzprognose ist, wofür sie wichtig ist und wie sie erzeugt werden kann (Abschnitt 2.1). Anschließend erfolgt eine Einleitung in das Thema künstliche neuronale Netze (Abschnitt 2.2). In Abschnitt 2.3 werden die in dieser Arbeit eingesetzten Technologien näher erläutert. Der letzte Teil des Kapitels (Abschnitt 2.4) betrachtet die zugrunde liegenden Datenstrukturen.

Absatzprognose

Die Absatzprognose gehört zu den grundlegenden betriebswirtschaftlichen Prozessen von Unternehmen – besonders im Einzelhandel. Mit ihrer Hilfe wird versucht vorherzusagen, wie hoch die Anzahl der verkauften Produkte, wie z. B. Waren, Güter oder Dienstleistungen, eines Unternehmens in einem bestimmten Zeitraum sein wird. Eine Prognose ist als eine Einschätzung, basierend auf verschiedenen Daten, über zukünftig eintretende Ereignisse zu verstehen.

Weil die Zukunft jedoch nicht vorhergesagt werden kann, ist das Ziel der Absatzprognose potenzielle Schwankungen des Absatzes2 zu erkennen, sodass auf diese mit unternehmerischen Maßnahmen reagiert werden kann. Diese potenziellen Schwankungen des Absatzes werden durch interne und externe Faktoren beeinflusst. Zu den externen Faktoren, die nicht vom Unternehmen beeinflusst werden können, gehören beispielsweise Trends, saisonale Effekte oder politische Entscheidungen. Interne Faktoren, wie z. B. Ein- und Verkaufskonditionen oder Effizienz, werden vom Unternehmen selbst kontrolliert. Eine Absatzprognose kann sowohl mit quantitativen als auch qualitativen Methoden erstellt werden. Quantitative Methoden basieren auf mathematisch-statistischen Modellen, die auf Vergangenheitsdaten, wie Zeitreihen, aufbauen. Im Gegensatz dazu zeichnen sich qualitative Absatzprognosen dadurch aus, dass ihnen kein mathematisches Prognosemodell zugrunde liegt. Sie werden dort eingesetzt, wo Daten für eine quantitative Absatzprognose fehlen oder, um die quantitativen Prognosen bzw. die ihnen zugrunde liegenden Rechenmodelle zu hinterfragen. Eine Zeitreihe ist wie folgt definiert:

Definition 2.1 (Zeitreihen (Schlittgen and Streitberg 2001)). Eine Zeitreihe besteht aus zeitlich geordneten Folgen von Beobachtungen.

Ihr liegt eine Menge von Tupeln der Form $(t, v)$ zugrunde, wobei $t$ für einen Zeitpunkt und $v$ für den dazugehörigen Wert steht (siehe Definition 2.1).

3531f9990b086ba48f2636d15c315733cc5f9027.png Grafische Darstellung einer Zeitreihe (“Monatliche Dichte an Atmosphärischem Kohlendioxid in Volumenteilen Pro Million Für Mauna Loa,” n.d.)

Somit sind Zeitreihen sehr gut geeignet, um sie mit Graphen zu visualisieren, indem $t$ über die x-Achse und $v$ über die y-Achse dargestellt wird. In Abbildung 2.1 ist ein Graph zu sehen, der die monatliche Dichte an atmosphärischem Kohlendioxid in Volumenteilen pro Million für Mauna Loa zwischen 1959 und 2005 zeigt. Die x-Achse zeigt die Zeitpunkte und die y-Achse die zugehörige Dichte an atmosphärischem Kohlendioxid.

Wie im Vorfeld erläutert gibt es verschiedene Methoden eine Absatzprognose zu erstellen. Zu den am häufigsten genutzten Verfahren gehören die Auswertung von Zeitreihen (wie in Abbildung 2.1), Expertenurteile oder -prognosen, Testkäufe, Kundenbefragungen und Prognosen, die mithilfe von künstlichen neuronalen Netzen erzeugt werden.

Bei Absatzprognosen, denen Zeitreihen zugrunde liegen, werden Daten aus der Vergangenheit analysiert. Es wird davon ausgegangen, dass die erhobenen Daten der Vergangenheit sowie die einer zukünftigen Erhebung unter den gleichen Bedingungen entstanden sind.

Expertenurteile oder -prognosen werden von Mitarbeitern, die ein umfassendes Wissen in den jeweiligen Bereichen haben, durchgeführt. Dieses Wissen haben sie sich beispielsweise durch ein enges Kundenverhältnis oder durch ein intensives Studium des Marktes angeeignet. Dadurch ist es ihnen möglich die internen und externen Faktoren gut einzuschätzen und zudem Absatzprognosen zu validieren.

Bei Produkten, die neu in den Markt eingeführt werden und somit keine Vergangenheitsdaten besitzen, besteht die Möglichkeit Testkäufe durchzuführen, die dann als Referenz für eine Absatzprognose verwendet werden können. Bei Testkäufen werden kleinere Mengen der Ware in ausgewählten Märkten verkauft, um anschließend die Absätze zu analysieren.

Darüber hinaus werden Kundenbefragungen durchgeführt, um durch präzise Fragen, ein Feedback der Kunden zu den angebotenen Produkten zu erhalten, sodass daraus eine Absatzprognose erzeugt werden kann.

Bei Produkten, die bereits seit längerer Zeit auf dem Markt sind und über die somit eine große Menge an Daten gesammelt werden konnte, bietet es sich an eine Absatzprognose mit künstlichen neuronalen Netzen durchzuführen. Dabei werden verschiedene Variablen, wie Absatz oder die Filialzahl, in ein künstliches neuronales Netz gegeben, welches mithilfe der Daten nach Mustern in ihnen sucht, sodass eine Prognose erzeugt werden kann. Absatzprognosen, die mit neuronalen Netzen erzeugt wurden, sind menschlich erzeugten Absatzprognosen meist überlegen, da sie deutlich mehr Faktoren berücksichtigen können. (Gansser and Krol 2015; Meffert et al. 2015; Pepels 2016; Scheer 2013)

Künstliche neuronale Netze

Künstliche neuronale Netze (engl. artificial neural networks) sind Computerprogramme, die die Funktionsweise von biologischen neuronalen Netzen, wie sie in den Gehirnen von Tieren vorkommen, simulieren. Ein künstliches neuronales Netz besteht aus miteinander verbundenen Neuronen.

2807cea47aa5e1981e118a2303a38eb6c148b4d4.png Darstellung eines künstlichen Neurons (Chrislb 2005)

Ein einfaches künstliches Neuron, auch Perzeptron-Netz genannt, ist in Abbildung 2.2 zu sehen. Neuronen (auch Knoten, Unit oder Einheit genannt), die übereinander dargestellt werden, bilden eine Schicht (engl. Layer). Zu Beginn des Netzes sieht man die Eingabeneuronen (auch Inputs oder Eingabeschicht genannt), welche zu einem Vektor $X$ mit den Werten $x_1, \dots, x_n$ zusammengefasst werden können. Jede Eingabe hat eine Gewichtung (engl. weight) $w_{ij}$ (Eingang $i$ bei Neuron $j$), mit welcher der Einfluss der Eingabe auf den Rest des Netzes spezifiziert werden kann. Eine Gewichtung von $w_j = 0$ bedeutet, dass keine Verbindung zwischen den Knoten besteht. Eine negative Gewichtung wird als hemmend (inhibitorisch) und eine positive als erregend (exzitatorisch) bezeichnet.

Definition 2.2 (Übertragungsfunktion (Zaccone and Karim 2018)). Sei $\Sigma$ eine Übertragungsfunktion mit den Variablen $x$, $w$ und $b$: $\Sigma=\Sigma_{i=1}^{n}x_iw_i+b$*

Durch die Übertragungsfunktion (siehe Definition Definition 2.2) $\Sigma$ wird die Netzeingabe des Neurons mit der gegebenen Gewichtung $w$ berechnet. Oftmals wird anschließend ein sogenannter Bias $b$ addiert. Die Aktivierungsfunktion $\varphi$ berechnet anschließend mit der Eingabe $\alpha$ (Ergebnis der Übertragungsfunktion $\Sigma$) die Ausgabe $Y$ des Neurons ($y_i=\varphi(\alpha)$). Der Wertebereich von Aktivierungsfunktionen liegt meistens zwischen $-1$ und $1$ oder zwischen $0$ und $1$, wobei es auch Sonderfälle geben kann. Zu den einfacheren Funktionen zählen z. B. die Treppenfunktion oder die Sigmoid-Funktion.

Definition 2.3 (Heaviside-Funktion (Wartala 2018)). Sei $f$ eine Heaviside-Funktion mit der Eingabe $x$: $f(x)= \begin{cases} 1, x \geq 0\ 0, x < 0 \end{cases}$

In Abbildung [fig:heaviside_fig] ist der Funktionsgraph der Heaviside-Funktion, einer Treppenfunktion, zu sehen. Bei ihr wird ein Schwellenwert bestimmt, von dem aus abhängig die Ausgabe der Funktion bestimmt wird. Die Heaviside-Funktion der Abbildung gibt für jeden Wert $<0$ den Wert $0$ aus und für jeden Wert $\geq 0$ den Wert $1$ zurück (siehe Definition Definition 2.3).

Definition 2.4 (Sigmoid-Funktion (Chollet 2018)). $f(x)=\frac{1}{1+e^{-x}}$, wobei $x$ die Eingabe des Neurons ist.

Die Sigmoid-Funktion (siehe Definition Definition 2.4) produziert eine Sigmoid-Kurve (vgl. Abbildung [fig:sigmoid_fig]), die sich dadurch auszeichnet, dass sie S-förmig verläuft. Häufig bezieht sie sich auf einen Sonderfall der logistischen Funktion.

Definition 2.5 (Tanh-Funktion (Wartala 2018)). $tanh(x)=\frac{sinh(x)}{cosh(x)}=1-\frac{1}{1+e^{-x}}$, wobei $x$ die Eingabe ist.

Eine besondere Form der Sigmoid-Funktion die die Tanh-Funktion (siehe Definition Definition 2.5). Die Ähnlichkeit der beiden Funktionen fällt sofort auf, wenn man die beiden Funktionsgraphen aus Abbildung [fig:sigmoid_fig] und Abbildung [fig:tanh_fig] miteinander vergleicht. Beide Graphen verlaufen S-förmig, wobei die Sigmoid-Funktion einen Wertebereich zwischen $0$ und $1$ und die Tanh Funktion einen Wertebereich zwischen $-1$ und $1$ hat.

Definition 2.6 (Rectifier (Chollet 2018)). $f(x)=max(0,x)$, wobei $x$ die Eingabe des Neurons ist.

Eine weitere Funktion, die häufig als Aktivierungsfunktion eingesetzt wird, ist die Rectifier-Funktion (siehe Definition 2.6). Diese Funktion gibt für alle positiven Eingaben diese unverändert aus, wohingegen bei negativen Eingaben der Wert $0$ zurückgegeben wird (vgl. Abbildung [fig:fig_relu]). Eine Einheit in einem künstlichen neuronalen Netz, die diese Funktion als Aktivierungsfunktion benutzen, wird ReLU (rectified linear unit) genannt.

Neuronale Netze bestehen normalerweise aus mehreren Neuronen (engl. units), die in mehrere Schichten aufgeteilt sind (vgl. Abbildung [fig:ann_example]). Die Schichten (engl. layer) gliedern sich in drei Arten. Die Eingabe-Schicht (engl. input layer) (rot) repräsentiert die Eingaben, die versteckte Schicht (engl. hidden layer) (blau), die aus beliebig vielen Schichten bestehen kann, steuert die Komplexität des Netzes und die Ausgabe-Schicht (engl. hidden layer) (grün) beinhaltet die Ausgabe des Netzes. Jeder Kreis der Abbildung steht für ein künstliches Neuron (vgl. Abbildung 2.2), wobei die einzelnen Schichten beliebig viele Neuronen enthalten können. Die Pfeile symbolisieren die Verbindungen und deren Gewichtungen zwischen den Neuronen. (Deru and Ndiaye 2020; Zaccone and Karim 2018; Vasilev et al. 2019)

Rekurrentes neuronales Netz (RNN)

Rekurrente neuronale Netze (RNN) werden häufig bei Problemen eingesetzt, die eine zeitliche Komponente beinhalten oder ein Gedächtnis brauchen, wie z. B. Satzvervollständigungen, Absatzprognosen oder Wettervorhersagen.

ea46a90e60fca7b4c4ea18a774ceff2ddd730c23.png Rekurrentes neuronales Netz (Mercyse 2016)

Im Gegensatz zu vorwärts-verketteten neuronalen Netzen (feed forward, Abbildung [fig:ann_example]), bei denen davon ausgegangen wird, dass jedes Neuron für sich unabhängig ist, sind bei RNNs die Neuronen zusätzlich mit sich selbst (blau), Neuronen derselben Schicht (rot) oder Neuronen einer vorhergehenden Schicht (grün) verbunden (vgl. Abbildung 2.3). Dadurch ist es möglich einen Wert über mehrere Schichten zu berücksichtigen.

Long Short-Term Memory (LSTM)

Long Short-Term Memory Netze, kurz LSTMs, sind spezielle RNNs, die neben einer Input- und Output-Zelle (engl. Gate) auch eine Merk- bzw. Vergessenszelle enthalten und es somit erlauben, Informationen über einen längeren Zeitraum zu speichern. Sie wurden 1997 von Sepp Hochreiter und Jürgen Schmidhuber vorgestellt. Im Gegensatz zu RNNs bestehen LSTMs aus vier statt einem Layer, wodurch sich LSTMs Informationen über einen längeren Zeitraum merken können. Aus diesem Grund werden sie häufig zur Sprachmodellierung oder zur Vorhersage von Zeitreihen eingesetzt. Außerdem ist es möglich, dass ein LSTM eine nicht länger benötigte Information vergessen kann.

3b43f7fc09afac27ec03bcf4b327e2f9bd5ae95f.png Abstrakte Darstellung einer LSTM-Zelle (Olah 2015)

In Abbildung 2.4 ist eine abstrakte Darstellung einer LSTM-Zelle zu sehen. Die innere Zelle $C_t$ speichert Informationen über vorherige Zeitschritte hinweg und kann deswegen als Herzstück dieser Architektur beschrieben werden. In jedem Zeitschritt kann das Netz mithilfe des Forget-Gates entscheiden, wie viel einer Information gespeichert werden soll. Dies geschieht mit einer Sigmoid-Funktion, die Werte $f_t \in [0,1]$ gibt. Dabei bedeuten Werte nahe der eins, dass die Information eher wichtig ist und Werte nahe der null, dass die Information eher unwichtig ist. Dabei erhält das Forget-Gate zwei Eingaben. Zum einen erhält es die Eingabe des aktuellen Zeitschritts, zum anderen die Ausgabe des vorherigen Zeitschritts $h_{t-1}$ (siehe Gleichung [eq:forget_gate]). Als Nächstes wird entschieden, welche neue Information in der Zelle gespeichert werden soll, wofür zwei Schritte nötig sind. Zuerst entscheidet das Input-Gate $i_t$ mit einer weiteren Sigmoid-Funktion welche Werte geändert werden (siehe Gleichung [eq:input_gate]), woraufhin mit der Tanh-Funktion ein neuer Kandidat, der der Zelle hinzugefügt werden kann, erstellt wird (siehe Gleichung [eq:c_t_in]). Anschließend werden die Ergebnisse des Input-Gates und der Tanh-Funktion multipliziert (siehe Gleichung [eq:c_t_out]) und in der inneren Zelle gespeichert. Folglich könnte $C_t$ keine neuen Informationen enthalten, weil das Input-Gate sie nicht zulässt. Die Ausgabe der LSTM-Zelle wird erneut mithilfe einer Sigmoid-Funktion, der aktuellen Eingabe und der Ausgabe des vorherigen Zeitschritts berechnet (siehe Gleichung [eq:output_gate]). Das Ergebnis ($o_t$) wird mit dem Wert der inneren Zelle ($C_t$), der von einer weiteren Tanh-Funktion berechnet wird, multipliziert (siehe Gleichung [eq:h_t]). Die Funktionen für eine Standard-LSTM-Zelle sind wie folgt definiert: $$\label{eq:forget_gate} f_t=\sigma(W_f \cdot [h_{t-1}, x_t]+b_f)$$ $$\label{eq:input_gate} i_t=\sigma(W_i \cdot [h_{t-1}, x_t]+b_i)$$ $$\label{eq:c_t_in} \tilde{C_t}=tanh(W_C \cdot [h_{t-1}, x_t]+b_C)$$ $$\label{eq:c_t_out} C_t=f_tC_{t-1}+i_t \tilde{C_t}$$ $$\label{eq:output_gate} o_t=\sigma(W_o \cdot [h_{t-1}, x_t]+b_o)$$ $$\label{eq:h_t} h_t=\sigma_t*tanh(C_t)$$

Mittlerweile gibt es einige Weiterentwicklungen der LSTM-Zelle. Eine von ihnen ist die bi-directional LSTM-Zelle (Graves and Schmidhuber 2005). Sie wurde ursprünglich zur Spracherkennung entwickelt, da Menschen Dialoge nicht linear erfassen. Stattdessen wird der gesamte Kontext benötigt, um einen Satz zu interpretieren. Eine bi-directional LSTM-Zelle erhält deswegen sowohl die reguläre Eingabe für sein sogenanntes forward network als auch eine umgekehrte Version für sein sogenanntes backward network. Anschließend werden beide Ausgaben durch eine Addition, Multiplikation oder Verkettung kombiniert, um die Ausgabe der bi-directional LSTM-Zelle zu erzeugen. (Zaccone and Karim 2018; Wartala 2018; Hochreiter and Schmidhuber 1997)

Convolutional Neuronal Network (CNN)

Ein convolutional neuronal network (CNN) (dt. faltendes neuronales Netz) ist ein künstliches neuronales Netz, welches hauptsächlich bei der Verarbeitung von Bild- oder Audiodateien eingesetzt wird, weil es effizienter mit großen Datenmengen umgehen kann als andere neuronalen Netze. Das liegt daran, dass die Eingabe in immer kleinere Stücke geteilt wird, weil nur bestimmte Teile betrachtet werden. CNNs basieren auf den Arbeiten der Hirnforscher David Hubel und Torsten Wiesel, die 1981 den Nobelpreis der Physiologie oder Medizin für ihre Arbeit, die Informationsverarbeitung im Sehwahrnehmungssystem beschreibt, erhielten. Sie beschrieben, wie Bilderkennung bei Säugetieren möglich ist. Dabei wurde die sogenannten Sehrinde (engl. Visual Cortex) als Anordnung unterschiedlicher Zelltypen beschrieben, von denen einer unter anderem kantenartige Muster innerhalb des Sehfelds identifizieren kann oder ein anderer die Position eines Objekts im Raum bestimmen kann.

28032c52d40fafe29e8ffd65f81c368b28acce3d.png Abstrakte Darstellung eines CNNs (vgl. (Zaccone and Karim 2018))

Abbildung 2.5 zeigt eine abstrakte Darstellung eines CNNs. Es wird der zuvor beschriebene Prozess der Bilderkennung mithilfe von CNNs aufgezeigt. Zunächst wird die Eingabe (grün) in den convolutional Layer (blau) überführt. Dort wird die Eingabe eingelesen, wobei jedoch nicht alle Daten verarbeitet werden. Angenommen man hat ein 5 x 5 Pixel großes Bild als Eingabe, dann läuft beispielsweise eine 3 x 3 Pixel große Faltungsmatrix von links oben nach rechts unten. Je häufiger dieser Schritt wiederholt wird, desto kompakter wird das Ergebnis und desto schmaler wird der convolutional Layer. Bevor der Output des Netzes berechnet wird, muss die Eingabe noch den Hidden Layer durchlaufen, der aus vollständig verknüpften Neuronen besteht. Mit ihnen ist es möglich die nicht lineare Kombination der Merkmale eines CNN zu lernen. Teilweise gibt es zwischen dem convolutional Layer und dem Hidden Layer noch die sogenannte Pooling-Schicht. Diese ist dafür verantwortlich auf dem Ergebnis des convolutional Layers eine Mustererkennung durchzuführen, wodurch überflüssige Informationen entfernt werden sollen. (Vasilev et al. 2019; Müller and Guido 2017; Zaccone and Karim 2018; Chollet 2018)

Lernprozess

Damit Prognosen mithilfe von neuronalen Netzen erzeugt werden können, muss das Netz zunächst historische Daten analysieren und Muster erkennen, was Lernen genannt wird. Dieses Lernen findet statt, indem eine Gewichtsmatrix, die die Werte der Gewichte zwischen den Neuronen im künstlichen neuronalen Netz enthält, bestimmt wird. Das Berechnen dieser Gewichtsmatrix findet mithilfe des Backpropagation of Error-Algorithmus (dt. Fehlerrückführungsalgorithmus) statt. Der Algorithmus läuft folgendermaßen ab:

Zunächst wird die Gewichtsmatrix mit Zufallswerten initialisiert, woraufhin die Eingabedaten vorwärts durch das Netz geleitet werden. Dabei werden die gewünschten Ausgabedaten, mit den Daten, die das Netz ausgibt, verglichen und die Differenzen der Werte als Fehler des Netzes gespeichert. Dieser Schritt läuft so lange, bis ein bestimmter Schwellenwert überschritten wird. Anschließend läuft die Fehlerrückführung ausgehend von der Ausgabeschicht in Richtung Eingabeschicht und minimiert nach und nach alle Fehlerwerte über Modifizierungen der Gewichtsmatrix. Dafür nutzt die Fehlerrückführung das Gradientenverfahren, weil ein Ausprobieren, bis alle Gewichte stimmen, einen zu hohen Zeitaufwand bedeuten würde. Das Verfahren versucht ein lokales Optimum (im Fall von Fehlerratenminimierung also ein lokales Minimum) zu finden, wobei zu Beginn festgelegt wird, wie viele Durchgänge berechnet werden sollen.

Dabei kann das Verfahren mit einigen Parametern gesteuert werden, wie der Lernrate, die angibt, wie stark die Gewichte pro Durchlauf geändert werden dürfen oder den Epochen, die angeben, wie oft das Verfahren ausgeführt werden soll. Je kleiner die Fehlerrate ist, desto besser hat das Verfahren funktioniert und das neuronale Netz berechnet Werte nahe der erwarteten Ausgabe.

Die Fehlerrate kann mit verschiedenen Verlustfunktionen (auch Loss-Function genannt) bestimmt werden. Zu den häufig genutzten zählen der Mean Square Error (dt. Mittlere quadratische Abweichung) und der Mean Absolute Error (dt. Mittlerer absoluter Fehler).

Definition 2.7 (Mean Squared Error). Der Mean Squared Error (MSE) ist definiert als: $$MSE=\frac{1}{n} \sum_{i=0}^{n}(y_i-\hat{y}_i)^2$$ wobei die Variablen folgende Bedeutung haben:

  • $n$: Anzahl der Beobachtungen

  • $y_i$: Beobachtungswerte

  • $\hat{y}_i$: Vorhersagewerte

Definition 2.8 (Mean Absolute Error). Der Mean Absolute Error (MAE) ist definiert als: $$MSE=\frac{1}{n} \sum_{i=0}^{n}|y_i-\hat{y}_i|$$ wobei die Variablen folgende Bedeutung haben:

  • $n$: Anzahl der Beobachtungen

  • $y_i$: Beobachtungswerte

  • $\hat{y}_i$: Vorhersagewerte

Der Mean Squared Error (siehe Definition Definition 2.7) berechnet den Mittelwert der quadratischen Differenz zwischen Beobachtungen und Vorhersagen. Aufgrund der quadratischen Differenz fallen hohe Differenzen stärker ins Gewicht und keine Abweichungen weniger stark, weil

Beim Mean Absolute Error (siehe Definition Definition 2.8) werden, im Gegensatz zum Mean Square Error, keine Quadrate gebildet, wodurch alle Differenzen zwischen Beobachtung und Vorhersage den Mittelwert gleich stark beeinflussen. Dadurch können eventuelle Messfehler in den Daten ignoriert werden, weil sie weniger ins Gewicht fallen.

Es gibt vier verschiedene Arten des Lernens, wobei die vierte eine Mischform der vorherigen Arten ist:

  • überwachtes Lernen(engl. supervised learning)

  • unüberwachtes Lernen(engl. unsupervised learning)

  • bestärkendes Lernen(engl. reinforcement learning)

  • teil-überwachtes Lernen(engl. semi-supervised learning)

Das überwachte Lernen arbeitet mit einer Menge vordefinierter Daten, die sich aus einem Tupel aus Ein- und Ausgaben zusammensetzen. Auf diese Weise ist für jede Eingabe eine Ausgabe definiert ist. Somit gibt es im Lernprozess eine direkte Rückmeldung, ob die gelernte Ausgabe richtig oder falsch ist. Diese Art des Lernens wird häufig bei Bilderkennung oder Zeitreihenvorhersagen verwendet und benutzt oftmals den Backpropagation-Algorithmus.

Beim unüberwachten Lernen hingegen sind die Daten nicht vordefiniert. Das bedeutet, dass das System die Daten selbst nach Mustern durchsuchen muss, um sie dann in Cluster bzw. Gruppen einzuordnen. Aus diesem Grund wird unüberwachtes Lernen häufig bei großen unstrukturierten Daten angewendet.

Das teil-überwachte Lernen ist eine Mischform aus überwachtem und unüberwachtem Lernen und betrachtet sowohl vordefinierte als auch nicht vordefinierte Daten. Dabei gibt es normalerweise weniger vordefinierte als nicht vordefinierte Daten. Bei dieser Art des Lernens wird zunächst mit den vordefinierten Daten und anschließend mit den übrigen Daten trainiert.

Das bestärkende Lernen belohnt richtige Antworten bzw. Aktionen, wohingegen es falsche Antworten bzw. Aktionen bestraft. Es wird hauptsächlich für Systeme eingesetzt, bei denen man bewerten kann, ob eine Aktion gut oder schlecht war, ohne dabei die richtigen Antworten vorzugeben. Bei Brett- oder Computerspielen können z. B. verschiedene Kombinationen von Spielzügen zum Erfolg führen, ohne dass vorher eine passende Reihenfolge bekannt ist. Besonderes Aufsehen erlangte Google mit ihrer KI “AlphaGo” die den besten “Go” Spieler der Welt mehrfach hintereinander schlagen konnte (vgl. (Silver et al. 2016; Menke 2016)). (Deru and Ndiaye 2020; Zaccone and Karim 2018; Wartala 2018; Vasilev et al. 2019)

Gradientenverfahren

Dieser Abschnitt erläutert das Gradientenverfahren näher, welches im vorherigen Abschnitt (siehe Abschnitt 2.2.4) erwähnt wurde. Das Gradientenverfahren wird dazu benutzt Optimierungsprobleme, wie die Minimierung einer reellen, differenzierbaren Funktion $J(\theta)$ mit den Funktionsparametern $\theta \in \mathbb{R}^d$, zu lösen, indem die Parameter entgegen der Gradientenrichtung der Funktion $\nabla_\theta J(\theta)$ aktualisiert werden. Bei künstlichen neuronalen Netzen wird versucht die Fehlerrate zu minimieren. Grundsätzlich basiert das Verfahren auf Gradienten, die als Pfeile betrachtet werden können, die auf das Minimum oder Maximum einer Funktion zeigen.

75b8c7dc27fdf24fb91629b244bc5b46ec84a1bf.png Topologische Karte zur Veranschaulichung eines Gradientenabstiegs (bfortuner and AyushSenapati 2017)

Ein häufig verwendetes Beispiel für Gradienten sind topografische Karten (Karten, auf denen ein Höhenprofil abgebildet ist), wie in Abbildung 2.6 zu sehen.

Angenommen eine Kugel befindet sich auf dem Gipfel des Berges in der rechten oberen Ecke der Abbildung (hohe Fehlerrate). Das Ziel ist es den tiefsten Punkt im Meer (niedrige Fehlerrate) am unteren Bildrand links von der Mitte zu erreichen. Die Pfeile innerhalb der Abbildung repräsentieren die Richtung in die der steilste Abstieg (negativer Gradient) von der aktuellen Position aus zu verzeichnen ist. Ausgehend vom Startpunkt folgt die Kugel immer dem Pfeil mit dem steilsten Abstieg, bis ein Punkt erreicht wird, von dem aus kein weiterer Abstieg möglich ist – die Kugel hat ein Tal erreicht.

0ebc70ff35afdf2a4a0f21d0921def8378a955b7.png Vergleich lokalem zwischen globalem Minimum (Wartala 2018)

Ein solcher Punkt wird entweder als lokales oder globales Minimum bezeichnet (siehe Abbildung 2.7). Das globale Minimum ist der absolut tiefste Punkt einer Funktion, wohingegen ein lokales Minimum ein Tiefpunkt ist der jedoch nicht niedriger als das globale Minimum sein kann. Für konvexe Funktionen gilt, dass jedes lokale Minimum auch ein globales Minimum ist, weil es nur ein einziges Minumum bzw. Maximum gibt.

Im Algorithmus wird die Richtung mithilfe der sogenannten Lernrate $\alpha$ bestimmt. Die Lernrate muss mit Bedacht gewählt werden, denn wird sie zu hoch gewählt, könnte das globale Minimum verfehlt werden, wohingegen eine zu kleine Lernrate dazu führt, dass es sehr zeitaufwendig wird die Werte für jeden Schritt zu berechnen.

Im Bereich des Deep Learnings wird das Gradientenverfahren oftmals in Form von sogenannten Optimierern (engl. optimizer) verwendet. Das liegt daran, dass die Eingaben in der Regel sehr groß sind und die Netze aus einer großen Anzahl von Schichten bestehen, weshalb das klassische Gradientenverfahren sehr zeitaufwändig ist. Optimierer sind Algorithmen bzw. Funktionen, die auf dem Gradientenverfahren aufbauen, es aber effizienter durchführen. Häufig eingesetze Optimierer für künstliche neuronale Netze sind SGD (Stochastic Gradient Descent), ADAM (Adaptive Moments) und RMSprop (Root Mean Squared Propagation). ADAM und RMSprop sind Variationen von SGD mit anpassbaren Lernraten.

SGD ist einer der grundlegendsten Optimierer. Es ist eine einfachere Version des Gradientenverfahrens.

ADAM ist eine Methode, bei der eine anpassbare Lernrate pro Parameter bestimmt werden kann. Er wurde foo von bar entwickelt…

RMSprop ist ein recht neuer Optimierungsalgorithmus, der foo von bar entwickelt wurde. Er ist besonders effizient … (Ruder 2016; Atienza 2018; Chollet 2018)

Eingesetzte Programmiersprachen und Frameworks

In diesem Abschnitt werden Programmiersprachen, Frameworks und Technologien vorgestellt, die im Laufe der Arbeit verwendet werden.

JavaScript

JavaScript ist eine Programmiersprache, die 1995 von Brendan Eich für die Firma Netscape, für ihren Webbrowser Netscape Navigator, unter dem Namen LiveScript entwickelt wurde. Ziel der Programmiersprache war es, für dynamisches HTML und CSS im Webbrowser zu sorgen, sodass z. B. Formvalidierungen möglich wurden. Im Unterschied zu anderen Sprachen wie C++ oder C, bei denen der Quelltext mithilfe eines Compilers kompiliert wird, letztendlich in maschinenlesbaren Code umgewandelt wird, ist JavaScript eine Skriptsprache, die von einem Interpreter validiert und interpretiert, also ausgeführt wird. Der heutige Name JavaScript entstand aus einer Kooperation von Netscape mit Sun Microsystems, den Entwicklern der Programmiersprache Java. Netscape wollte den Namen Java nutzen, um die Sprache populärer zu machen, weswegen sie in JavaScript umbenannt wurde. Beide Sprachen haben allerdings, abgesehen von einer ähnlichen Syntax, wenig miteinander gemein. JavaScript wird von ECMA International unter dem Namen ECMAScript (kurz ES) standardisiert3.

Mittlerweile gehört es zu den populärsten Programmiersprachen der Welt4. JavaScript nutzt im Gegensatz zu objektorientierten Sprachen (Java, C, C++) Prototypen, um Objekte zu realisieren und verzichtet auf Klassen. Prototypen lassen sich zur Laufzeit des Programms dynamisch anpassen, wodurch Attribute oder Methoden ergänzt bzw. entfernt werden können. Außerdem ist JavaScript eine nicht typisierte Sprache, was bedeutet, dass eine Variable beispielsweise sowohl Strings als auch Zahlen enthalten darf. Das bedeutet jedoch nicht, dass es keine Datentypen gibt. Aktuell umfasst JavaScript sechs primitive Datentypen, zu denen Boolean, Null, Undefined, Number, String und Symbol gehören und den Datentyp Object.5

Ein Vorteil von Skriptsprachen wie JavaScript liegt darin, dass sie sowohl für Menschen mit, als auch Menschen ohne Programmierkenntnisse recht einfach zu lernen ist, weil wenig Vorkenntnisse nötig sind, um erste Ergebnisse zu erzielen. Des Weiteren ist JavaScript sehr weit verbreitet, da es auf nahezu jeder Webseite eingesetzt wird. Ein Nachteil ist unter anderem, dass Skriptsprachen weniger effizient sind als kompilierte Sprachen wie C oder Java. Für JavaScript ist es ebenfalls von Nachteil, dass die Sprache, von verschiedenen Browsern unter Umständen unterschiedlich interpretiert wird, da jeder Browser seinen eigenen Interpreter (seine eigene Engine) hat. Mozilla benutzt für Firefox die SpiderMonkey Engine und bei Apple kommt für den Safari Browser die JavaScriptCore Engine, die Bestandteil der WebKit Browser Engine ist, zum Einsatz. Mittlerweile setzen allerdings viele Browserhersteller auf die Google V8-Engine6, die von Google unter einer Open-Source-Lizenz entwickelt wird und ursprünglich als Teil des Google Chrome Browsers ausgeliefert wurde. Browser, die auf der V8-Engine basieren, sind unter anderem Google Chrome, Microsoft Edge oder der Opera Browser. (Springer 2018; Zammetti 2020; Shute 2019)

Node.js

Node.js ist eine OpenSource Plattform, die das Ausführen von JavaScript ohne Webbrowser erlaubt. Es nutzt die Google V8-Engine, die JavaScript validieren und interpretieren kann. Ryan Dahl entwickelte Node.js im Jahr 2009 und stellte es der Öffentlichkeit auf der JsConfEu in Berlin vor7. Somit ist Node.js keine eigene Programmiersprache, sondern stellt nur eine Umgebung bereit, mit der JavaScript außerhalb des Webbrowsers (z. B. auf Servern) ausgeführt werden kann. Zudem beinhaltet die Node.js Plattform einige C Bibliotheken, die als Modul in ein Node.js Projekt eingebunden werden können, sodass unter anderem Zugriffe auf das Dateisystem oder die Bereitstellung eines Webservers möglich sind. Jedes Node.js Projekt kann als Modul anderen Node.js Projekten bereitgestellt werden. Dies ist vergleichbar mit dem Importieren von Bibliotheken in Java oder C. Bei der Bereitstellung hilft der Node Package Manager8 (kurz npm), mit dem jedes Node.js Projekt anderen Entwicklern zur Verfügung gestellt werden kann.

Die Architektur von Node.js ist ereignisgesteuert (auch unter nonblocking I/O bekannt), sodass effizienter mit externen Schnittstellen interagiert werden kann. Ereignisgesteuert bedeutet, dass nicht aktiv auf externe Ressourcen, wie z. B. I/O9, gewartet werden muss. In Node.js sind viele Aktionen, die ausgeführt werden können nonblocking. Beispielsweise wird beim Schreiben einer Datei nicht gewartet, bis das Betriebssystem die Datei geschrieben hat, sondern es wird ein Event registriert, dass ausgeführt wird, wenn das Betriebssystem meldet, dass die Datei geschrieben wurde. In der Zwischenzeit kann weiterer Node.js Code ausgeführt werden. So ist es möglich ohne zusätzliche Threads10 den Programmcode weiter auszuführen, wodurch deutlich weniger Arbeitsspeicher verwendet werden muss. Da Node.js hauptsächlich als Serveranwendung eingesetzt wird, müssen viele Anfragen von verschiedenen Klienten beantwortet werden. In Sprachen, die blocking I/O verwenden (z. B. C, Java, PHP), muss für jede Anfrage ein eigener Thread erzeugt werden, weil sonst das Programm so lange blockiert wäre, bis die Anfrage bearbeitet wurde. Somit können in dieser Zeit keine weiteren Anfragen bearbeitet werden. (Springer 2018; Zammetti 2020; Mardan 2018)

TypeScript

TypeScript ist eine OpenSource Programmiersprache, die von Microsoft entwickelt wurde, und die eine echte Obermenge von JavaScript ist. Dadurch ist JavaScript Quelltext gleichzeitig valider TypeScript Quelltext, was es erlaubt externe JavaScript Bibliotheken einzubinden. Der Name TypeScript wurde gewählt, weil TypeScript, im Gegensatz zu JavaScript, eine statische Typisierung erlaubt, sodass Variablen nur von einem bestimmten Typ sein können. Durch diese statische Typisierung wird es Entwicklern einfacher gemacht sich in fremden Code einzulesen, bzw. besser nachvollziehen zu können, wie Quelltext funktioniert. TypeScript wird nicht direkt ausgeführt, sondern von einem Compiler in JavaScript umgewandelt, sodass es anschließend von Browsern, Node.js oder anderen JavaScript Laufzeitumgebungen ausgeführt werden kann. Zusätzlich können mit TypeScript bekannte Techniken aus objektorientierten Programmiersprachen, wie z. B. Klassenobjekte (statt Prototypen), Vererbungen von Klassen, generische Klassen oder das Einbinden von Interfaces, umgesetzt werden. (“TypeScript Handbook” 2021; Zammetti 2020; Freeman 2019; O’Hanlon 2019; Kotaru 2020)

Relationale Datenbank

Datenbanken sind Computerprogramme, die helfen große Datenmengen effizient, persistent und widerspruchsfrei abzuspeichern. Die wesentlichen Bestandteile sind zum einen eine Verwaltungssoftware, die Datenbankmanagementsystem (DBMS) genannt wird und zum anderen die eigentliche Datenbank, die aus einer Menge verbundener Informationen besteht. Die Interaktion mit den in der Datenbank abgelegten Informationen übernimmt das DBMS. Es nutzt eine Abfragesprache mit dem Namen Structured Query Language (kurz: SQL), um auf die Daten zuzugreifen. Mit SQL ist es möglich Daten einzufügen, abzufragen, zu ändern oder zu löschen. Oftmals sind die Operationen unter dem Akronym CRUD (Create, Read, Update, Delete) zusammengefasst. Ein analoges Beispiel einer Datenbank ist ein Telefonbuch. In diesem befinden sich viele verschiedene Informationen, wie Namen, Telefonnummern und Adressen, zu Personen, die in einer bestimmten Region leben. Die Daten in einem Telefonbuch sind anhand der Nachnamen alphabetisch sortiert. Damit eine Adresse oder eine Telefonnummer gefunden wird, muss der Name der gesuchten Person bekannt sein.

Das Konzept der relationalen Datenbanken wurde 1970 von Dr. E. F. Codd, der bei IBM angestellt war, in einem Paper vorgestellt (Codd 1970). Es besagt, dass Daten in verschiedenen Tabellen gespeichert werden sollen, damit Redundanzen in den Daten vermieden werden können. Die Tabellen können miteinander verbunden werden, um eine Verknüpfung der Daten aus verschiedenen Tabellen zu ermöglichen. Das DBMS in relationalen Datenbanken wird RDBMS genannt. Zu bekannten RDBMS Systeme gehören Oracle, MySQL, Microsoft SQL Server und PostgreSQL.

ID Nachname Vorname ID_Adresse
1 Doe John 2
2 Doe Jane 2
3 Max Mustermann 1

Beispieltabelle für Personen in einem Telefonbuch

ID Telefonnummer
1 123456
2 02317551
3 43245122123

Beispieltabelle für Telefonnummern

ID Straße Hausnummer Plz Stadt Land
1 Otto-Hahn Straße 12 44227 Dortmund Deutschland
2 Main St 42 22162-1010 Anywhere USA

Beispieltabelle für Adressen

ID_Person ID_Telefonnummer
3 2
2 1
1 1
1 3

Beispieltabelle für Zuordnung von Personen zu Telefonnummern

Die Tabellen 2.12.2 und 2.3 beinhalten Beispieldaten aus einem Telefonbuch. In Tabelle 2.1 sind die Personen, in Tabelle 2.2 die Adressen und in Tabelle 2.3 die Telefonnummern gespeichert. Tabelle 2.4 gibt die Beziehung zwischen Personen und Telefonnummern an. Alle Datensätze in den Tabellen (jede Zeile beschreibt einen Datensatz) wurden zusätzlich um eine ID Spalte ergänzt, über die sie eindeutig identifizierbar sind. Die Eigenschaft, die eine eindeutige Zuordnung eines Datensatzes ermöglicht, wird Primary Key genannt und kann unter Umständen auch aus einer oder mehreren Spalten bestehen. Eine Spalte einer Tabelle, die auf den Primary Key einer anderen Tabelle verweist wird Foreign Key genannt.

Es ist möglich verschiedene Beziehungen zwischen den jeweiligen Tabellen auszudrücken. Zunächst gibt es die sogenannte OneToOne Beziehung, die aussagt, dass eine Zeile einer Tabelle maximal zu einer weiteren Zeile einer anderen Tabelle gehört. Dazu muss eine der beiden Tabellen einen Foreign Key enthalten, der auf den Primary Key der andere Tabelle verweist. Des Weiteren gibt es die ManyToOne bzw. OneToMany Beziehung, bei der eine Zeile der einen Tabelle mit beliebig vielen Zeilen einer anderen Tabelle verknüpft ist. Tabelle 2.3 hat eine OneToMany Beziehung zu Tabelle 2.1, wohingegen die Tabelle 2.1 eine ManyToOne Beziehung zur Tabelle 2.3 besitzt. Das bedeutet, dass es zu jeder Adresse beliebig viele Personen geben kann, umgekehrt zu einer Person jedoch nur eine Adresse. Zuletzt gibt es die ManyToMany Beziehung zwischen zwei Tabellen. Hierbei ist es möglich, dass sowohl beliebig viele Datensätze der ersten als auch beliebig viele Datensätze der zweiten Tabelle miteinander in Beziehung stehen. Diese Beziehung ist allerdings nur mit einer Hilfs- bzw. Zuordnungstabelle darzustellen. Die Zuordnungstabelle hat jeweils eine ManyToOne Beziehung zu den zu verknüpfenden Tabellen. Die Tabellen 2.1 und 2.2 stehen zueinander in einer ManyToMany Beziehung, weil zu einer Person beliebig viele Telefonnummern und zu einer Telefonnummer beliebig viele Personen gehören. Als Zuordnungstabelle wird Tabelle 2.4 verwendet. Zeile eins in dieser Tabelle gibt beispielsweise an, dass die Person mit dem Primary Key $=3$ (Max Mustermann) die Telefonnummer mit der ID $=2$ (02317551) hat. Außerdem kann aus ihr gelesen werden, dass sowohl die Person mit der ID $=1$ und die mit der ID $=2$ die Telefonnummer mit der ID $=1$ haben. Des Weiteren hat die Person mit der ID $=1$ die Telefonnummer mit der ID $=3$. Mithilfe der Relationen zwischen den Tabellen ist es möglich Daten abzuspeichern, ohne eine Redundanz zu erzeugen. Die Daten können durch SQL Befehle aus den verschiedenen Tabellen abgefragt und anhand der Foreign Keys verknüpft werden. Das Verknüpfen von mehreren Tabellen über einen SQL Befehl wird join genannt. Datenbanktabellen können in der Theorie beliebig viele Spalten11 und Zeilen beinhalten. (Bush 2020; Beaulieu 2020)

TypeORM

TypeORM ist ein in TypeScript geschriebenes Framework, mit dem Daten sowohl in eine Datenbank geschrieben als auch aus ihr ausgelesen werden können. Es wird unter einer Open-Source-Lizenz von verschiedene Personen entwickelt und auf GitHub12 veröffentlicht. TypeORM verwendet das Konzept der objektrelationalen Abbildung (engl. object-relational mapping, kurz ORM), welches eine Technik der Softwareentwicklung ist, mit der Objekte, die in einer objektorientierten Programmiersprache erzeugt wurden, in einer relationalen Datenbank (siehe Abschnitt 2.3.4) gespeichert werden können. Ein Vorteil von TypeORM ist die einfache Einbindung in Node.js bzw. JavaScript/TypeScript Anwendungen. Das Konzept der ORMs wird ebenfalls in anderen Programmiersprachen, wie Java (Hibernate) oder C# bzw. .Net (Entity Framework), verwendet.

9240ab2ad48cca4a6a9a1027c3ef0c9ff1509f16.png Beispielklassen in UML Darstellung

Jedes Objekt einer Klasse einer Programmiersprache bildet auf einen Datensatz (eine Zeile) in einer Datenbanktabelle ab, wobei jede Spalte eine Eigenschaft dieser Klasse darstellt. Eine Klasse, die auf eine Tabelle abbildet, wird Entity genannt. Besitzt eine Klasse eine Eigenschaft, die den Typ einer anderen Klasse hat, dessen Typ nicht vom DBMS unterstützt wird, muss diese Klasse ebenfalls als Entity angelegt werden. Damit ein Objekt in der Datenbank gefunden werden kann, benötigt es eine Eigenschaft, mit der es eindeutig identifiziert werden kann (Stichwort: Primary Key). Falls eine Entity keine Eigenschaft vorweist, wird ihr meistens die Eigenschaft ID hinzugefügt, die dann als Primary Key fungiert. In Abbildung 2.8 sind die Klassen Address, Person und Sport mithilfe der Unified Modeling Language (UML) dargestellt.

ada9c43826695b10f08d019186f0cd512a1b0336.png Beispielhaftes Datenbankmodel für das Beispiel aus Abbildung 2.8

Die Klassen stehen in unterschiedlichen Beziehungen zueinander. Jedes Personenobjekt kann auf genau ein Adressenobjekt und auf verschiedene Sportobjekte referenzieren. Ein Adressenobjekt und ein Sportobjekt können jeweils zu mehreren Personen gehören. Daraus lässt sich das in Abbildung 2.9 dargestellte Datenbankmodel erzeugen. Die Klasse Address wird über die Tabelle Addresses dargestellt, wobei jede Eigenschaft der Klasse Address als Spalte der Tabelle Addresses hinzugefügt wird. Dadurch, dass sowohl ein Personenobjekt zu mehreren Sportobjekten, als auch ein Sportobjekt mit mehreren Personenobjekten in Beziehung stehen kann, liegt zwischen ihnen eine ManyToMany Beziehung vor. In diesem Fall legt das ORM automatisch eine Zuordnungstabelle an, die im gegeben Beispiel Favorite_Sports heißt und zwei Foreign Keys enthält, die jeweils auf die Primary Keys der People- und der Sports-Tabelle zeigen. Die People-Tabelle enthält anstelle eines Addressobjekts einen Foreign Key auf die ID Spalte der Addresses-Tabelle.

Wird ein bestimmtes Objekt gesucht, muss dem ORM der Klassenname und seine ID mitgeteilt werden, woraufhin der benötigte SQL Befehl generiert und an die Datenbank gesendet wird. Falls der SQL-Befehl ein Ergebnis liefert, wird es vom ORM in das angefragte Objekt umgewandelt. Für den Fall, dass mehrere Objekte angefragt werden, weil nicht nach einem bestimmten Objekt gesucht wird, sondern beispielsweise nach allen Personen, die in einer bestimmten Stadt wohnen, gibt das ORM ein Array oder eine Liste mit den gefundenen Objekten zurück. Bei einer Schreiboperation (Einfügen, Ändern, Löschen) wandelt das ORM die Eigenschaften der Entity in den benötigten SQL Befehl um, sodass die Datenbank die gewünschte Operation ausführen kann. (Bauer and King 2016; Keith, Schincariol, and Nardone 2018; TypeORM 2021)

Angular

Angular ist eine OpenSource Software, mit der Webapplikationen, sogenannte Single Page Applications (SPA) entwickelt werden können. SPAs sind Webseiten, die aus einer einzelnen Seite bestehen, welche sich durch den Einsatz von JavaScript dynamisch ändern kann. Im Unterschied zu herkömmlichen Webseiten, bei denen mehrere HTML Dokumente miteinander verlinkt sind, existiert bei Angular ausschließlich ein einziges HTML Dokument, das mithilfe von JavaScript in verschiedene Seiten transformiert wird. Dadurch sind SPAs nach dem ersten Laden deutlich schneller, da weniger Daten vom Server geladen müssen, wodurch ebenfalls eine geringere Last auf dem Webserver erzeugt wird. Angular wurde von Google entwickelt und im Jahr 2014, als Nachfolger von AngularJS, veröffentlicht. Aktuell wird es von Googles Angular-Team und freiwilligen Entwicklern weiterentwickelt und basiert auf der Programmiersprache TypeScript (siehe 2.3.3), womit unter anderem die Vorteile der statischen Typisierung genutzt werden können. Der Quelltext des Angular Frameworks wird unter einer MIT Lizenz (Open-Source-Lizenz13) auf GitHub veröffentlicht14. Dadurch, dass der Inhalt der Webseite dynamisch durch JavaScript angepasst wird, ist es möglich bestimmte Elemente der Seite in sogenannte Components auszulagern, sodass diese auf verschiedenen Seiten wiederverwendet werden können. Zudem können Components zu Modulen zusammengefasst werden. Diese Module können wiederum anderen Entwicklern bzw. Projekten zur Verfügung gestellt werden. Dazu wird, wie bei Node.js (siehe 2.3.2), der Node Package Manager verwendet.

Angular wird von verschiedenen Unternehmen und Konzernen eingesetzt, zu diesen zählen unter anderem Delta Airlines15, Ryan Air16, Microsoft17 und Google18. (Angular 2021; Freeman 2020; Kotaru 2020)

Python

Python wurde Anfang der 1990er Jahren von Guido van Rossum am Stichting Mathematisch Centrum (CWI, siehe https://www.cwi.nl/) als Nachfolger der Sprache ABC19 entwickelt. Es ist eine Skriptsprache, die den Anspruch hat einen gut lesbaren, knappen Programmierstiel zu fördern, was sich z. B. durch relativ wenig Schlüsselwörter und das Fehlen von geschweiften Klammern für Blöcke äußert. Außerdem ist Python eine Multiparadigmensprache, was bedeutet, dass der Entwickler keinen bestimmten Programmierstil verwenden muss. Es ist möglich sowohl objektorientierte als auch funktionale Pythonprogramme zu entwickeln. Ähnlich wie in JavaScript verwaltet Python Datentypen dynamisch, weswegen eine Variable zur Laufzeit des Programms Werte verschiedener Typen enthalten kann. Des Weiteren gehört Python zu den weit verbreitetsten Programmiersprachen der Welt, wodurch es eine große Anzahl an externen Bibliotheken gibt, die in einem Pythonprojekt eingebunden werden können. Zudem ist Python sehr vielfältig und wird von vielen Unternehmen, wie Google, Netflix, Instagram oder YouTube in verschiedenen Bereichen eingesetzt. Es ist möglich Python in der Kommandozeile, auf Servern in Form von Skripten oder Webservern einzusetzen oder ein Programm mit graphischer Benutzeroberfläche für den Einsatz auf PCs zu programmieren. (Python 2021; Vasilev et al. 2019; Loy 2019; Lubanovic 2020; Browning and Alchin 2019)

Jupyter-Notebook

Das Jupyter-Notebook ist eine OpenSource Internetanwendung, mit der Dokumente, die Quelltext, mathematische Gleichungen, Visualisierungen und Fließtext enthalten, erstellt und mit anderen Personen geteilt werden können. Der Quelltext eines Jupyter-Notebook-Dokuments kann direkt in diesem ausgeführt werden, wodurch der Verfasser des Quelltextes eine sofortige Rückmeldung erhält. Das Ergebnis der Ausführung wird anschließend ebenfalls im Dokument dargestellt, wodurch sich Jupyter-Notebooks sehr gut dazu eignen, Daten auszuwerten, wie es häufig im Data Science Bereich der Fall ist. Das Project Jupyter20 wurde 2014 von Fernando Péres aus dem Projekt IPython21 ausgegliedert, welches jedoch weiterhin als Python-Shell existiert und als Kernel für Jupyter-Notebooks dient. Der Kernel der Jupyter-Notebooks fungiert als Interpreter für den Quelltext. Der Name Jupyter setzt sich aus den Namen der drei wesentlichen Programmiersprachen Julia22, Python23 und R24 zusammen. Jupyter unterstützt zusätzlich viele weitere Sprachen wie Haskell25 oder Ruby26, die in Form eines Kernels eingebunden werden können.

270b4d2e789a43aa5800e953b45dc5a7c5d88f13.png Beispieldokument eines Jupyter Projekts

Abbildung 2.10 zeigt ein Jupyter-Notebook-Dokument, das in zwei Arten von Zellen unterteilt ist. Bei diesen beiden Zellen handelt es sich um eine Textzelle und um eine Quelltextzelle. Textzellen werden mithilfe der Auszeichnungssprache Markdown geschrieben und dienen der Dokumentation des Notebooks. Mit Markdown lassen sich Texte über bestimmte Zeichen, wie # für eine Überschrift, formatieren. Die Quelltextzellen aus Abbildung 2.10 beinhalten Python-Quelltext, der in dem Notebook ausgeführt werden kann. Sie sind daran zu erkennen, dass links von ihnen ein In steht, welches angibt, wann der Quelltext ausgeführt wurde. Der Wert aus In wird bei jeder Quelltext-Ausführung um den Wert $1$ inkrementiert und symbolisiert somit die zeitliche Abfolge der Zellen. Das Dokument aus Abbildung 2.10 zeigt zunächst eine Textzelle, die aus einer Überschrift und einem Beschreibungstext für die folgenden beiden Quelltextzellen besteht. Unter den folgenden beiden Quelltextzellen befindet sich die jeweilige Ausgabe, die der Python-Quelltext erzeugt (Falls der Quelltext keine Ausgabe erzeugt, wird nichts angezeigt). Die letzten vier Blöcke bauen aufeinander auf und bestehen erst aus einer Textzelle, die beschreibt, was in der nachfolgenden Quelltextzelle passiert. Zunächst wird mithilfe der Python-Bibliothek Pandas27 ein Datensatz erstellt und anschließend in Form einer Tabelle ausgegeben. Danach wird mit Unterstützung der Bibliothek Plotly28 ein Graph auf Grundlage der zuvor erzeugten Daten gezeichnet. (Galea 2018; Toomey 2017; VanderPlay 2018; Wartala 2018)

TensorFlow

TensorFlow ist eine von Google entwickelte und 2017 unter einer Open-Source-Lizenz veröffentlichte Bibliothek für maschinelles Lernen. Es wird häufig eingesetzt, um mehrschichtige neuronale Netze zu trainieren. Der Name ergibt sich aufgrund der Rechenoperationen, die zur Berechnung der Gewichtsmatrix in einem neuronalen Netz erforderlich sind, die auf mehrdimensionalen Datenfeldern (sogenannten Tensoren) durchgeführt werden. Die TensorFlow Bibliothek beinhaltet zusätzlich die Deep Learning-Bibliothek Keras. Mithilfe von TensorFlow und Keras können verschiedene Modelle für künstliche neuronale Netze erstellt und trainiert werden. Das Framework ermöglicht z. B. Modelle mit LSTM-Zellen, CNNs oder Feedforward Netzen (mithilfe von Dense Layern) zu erstellen. Den Zellen des Modells kann entweder eine im Framework definierte29 oder eine individuelle (selbst definierte) Aktivierungsfunktion übergeben werden. Dem Modell wird ein Optimierer hinzugefügt, der das Gradientenverfahren optimiert und die Lernrate bestimmt. Zusätzlich wird durch das Modell bestimmt wie viele Epochen das Training des Modells andauern soll. Somit bietet TensorFlow alle wesentlichen Komponenten, die für das Trainieren eines neuronalen Netzes notwendig sind, da der Nutzer nur Ein- bzw. Ausgaben definieren, die Struktur des Netzes bestimmen und letztendlich eine Funktion des TensorFlow Frameworks aufrufen muss. Anschließend werden alle relevanten Berechnungen intern durchgeführt, ohne dass der Nutzer sich intensiver damit auseinandersetzen muss.

const X = [
        [
            [1],
            [2],
            [3]
            // ...
        ],
        [
            [4],
            [5],
            [6]
            // ...
        ]
        // ...
    ];
console.log(X.shape()) // output: shape(2,3,1)

Als Ein- bzw. Ausgabe erwartet TensorFlow Tensoren, die mehrdimensionale Arrays sind und wie in Listing [lst:input_tensor] visualisiert werden können. Die Länge eines Tensors wird als Shape (dt. Form oder Gestalt) bezeichnet und umfasst eine Zahl, die die Länge der jeweiligen Dimension angibt. Der Shape des Eingabetensors $X$ aus Listing [lst:input_tensor] besteht aus dem Tripel $(2,3,1)$. Das äußere Array (Zeile 1–15) hat die Länge $2$, die beiden folgenden (Zeile 2-7 und Zeile 8-14) die Länge $3$ und die innersten (Zeilen: 3,4,5,9,10,11) die Länge $1$. Je mehr Dimensionen eine Eingabe hat, desto tiefer ist sie bzw. das neuronale Netz. In vielen verschiedenen Googlediensten, wie der Google Suche, Gmail, Google Fotos oder der Spracherkennung wird TensorFlow derzeit produktiv eingesetzt. (TensorFlow 2021; Zaccone and Karim 2018; Deru and Ndiaye 2020; Wartala 2018)

Datenstrukturen

In diesem Abschnitt wird auf die zugrundeliegende Datenstruktur der in dieser Arbeit verwendeten Absatz-, Artikel- und Filialdaten eingegangen. Diese Absatzdaten sind in einer relationalen Datenbank gespeichert. In ihr sind viele unterschiedliche Tabellen vorhanden, von denen drei die relevanten Informationen enthalten. Zwei dieser vier Tabellen enthalten Stammdaten mit Informationen zu Filialen und Artikeln. Die dritte Tabelle baut auf die Filial- und Artikeltabelle auf und speichert die jeweiligen Artikelbewegungen pro Artikel, Filiale und Kassenbon. Sie ist die Basis zur Berechnung von Absätzen, Beständen und Retouren. Auf Basis der Tabelle, die die Artikelbewegungen speichert, existiert eine weitere Tabelle, in der die Absätze aggregiert gespeichert sind. Diese Tabelle beinhaltet die Absatzdaten pro Woche und pro Filiale.

Anwendung zum Analysieren, Finden und Kopieren von Artikeldaten

In diesem Kapitel wird der Entwicklungsprozess von zwei Anwendungen beschrieben. Die erste Anwendung wurde mit Node.js und TypeScript entwickelt und wird zum Extrahieren und Bereitstellen von Absatzdaten aus dem produktiven Datenbanksystem verwendet. Die zweite Anwendung wurde mit dem Angular-Framework entwickelt und wird zum Visualisieren der bereitgestellten Daten der ersten Anwendung genutzt. Zusätzlich soll die zweite Anwendung den Prozess der Datenextraktion steuern. Zunächst wird der Hintergrund der Anwendungsentwicklung in Abschnitt 3.1 erklärt. Anschließend wird der Entwicklungsprozess erörtert (siehe Abschnitt 3.2). Zum Schluss wird in Abschnitt 3.3 das Ergebnis vorgestellt.

Hintergrund

In diesem Abschnitt wird erklärt, warum die in der Einleitung beschriebenen Anwendungen entwickelt werden mussten, bevor Absatzprognosen für Artikel erzeugt werden können. Das Unternehmen TEDi verkauft über $15.000$ verschiedene Artikel in über $2.500$ Filialen in mehreren europäischen Ländern. Aus diesem Grund fallen sehr viele Daten an, sodass für die Entwicklung eines neuronalen Netzes, das eine Absatzprognose generiert, zunächst passende Artikel gesucht werden müssen, für die eine Prognose validiert werden kann. Diese Artikel müssen bestimmte Eigenschaften erfüllen, damit sie infrage kommen. Dazu zählt vor allem die Dauer im Sortiment, denn je länger ein Artikel verkauft wird, desto mehr Daten hat ein neuronales Netz zum Lernen. Außerdem sollte der Graph der Absätze nach Möglichkeit eine Art Muster beinhalten, sodass erste Prognosen generiert werden können und somit visuell die Vorhersage validiert werden kann.

Damit aus der Vielzahl der Artikel passende ausgewählt werden können, wäre es hilfreich eine Liste mit Bildern von Artikeln zu erhalten, die nach Dauer im Sortiment und Gesamtabsatz pro Tag gefiltert bzw. sortiert werden kann. Wird ein Artikel ausgewählt sollten die vergangenen Absätze in einem Graphen visualisiert werden, sodass nach einem Muster in den Absätzen gesucht werden kann. Ein weiteres Problem ist, dass das System keine taggenauen Absätze und Bestände beinhaltet, sondern entweder wochengenaue Absätze oder Kassenbon-genaue (siehe Abschnitt 2.4) Bewegungsdaten. Bei wochengenauen Absätzen hat ein neuronales Netz weniger Daten (pro Jahr ca. 52 zu ca. 365 bei taggenauen Daten) mit denen es lernen kann. Somit muss die Möglichkeit geschaffen werden, aus den Kassenbon-Daten taggenaue Absätze und Bestände zu berechnen, sobald ein passender Artikel gefunden wurde, für den eine Absatzprognose generiert werden soll. Die Bereitstellung der Daten für die Absatzprognose sollte über eine Schnittstelle erfolgen, sodass vergleichbare Prognosen für verschiedene Artikel mit minimaler Änderung generiert werden können. Hierbei sollte jedoch nicht das Tagesgeschäft des Unternehmens beeinträchtigt werden. Die Lösung dafür ist, dass die Anwendung mit der Testdatenbank interagiert, die in regelmäßigen Abständen mit dem Produktivsystem gespiegelt wird und zum Zeitpunkt der Entwicklung Daten bis Ende 2020 enthält. Ein weiteres Hindernis findet sich in den Öffnungszeiten, welche pro Filiale gespeichert sind. Es ist jedoch nicht bekannt, ob eine Filiale an einem Tag tatsächlich geöffnet hatte, weil unvorhergesehene Umstände, wie Umbaumaßnahmen oder Verkaufsverbote, die Filiale nicht öffnen ließen. Diese Daten lassen sich berechnen, indem geprüft wird, ob eine Filiale am betrachteten Tag mindestens einen Artikel verkauft hat. Sobald sie etwas verkauft hat, kann sie als geöffnet angesehen werden, da es sehr unwahrscheinlich ist, dass eine Filiale geöffnet war und nichts verkauft hat.

Zur Umsetzung werden zwei separate Anwendungen benötigt. Die erste soll auf die Datenbank zugreifen und die zuvor beschriebenen taggenauen Daten berechnen, sie in neuen Tabellen speichern und diese über eine REST-Schnittstelle zur Verfügung stellen. Die zweite Anwendung soll mit der ersten interagieren, die Daten abholen, sie anzeigen und bestimmen für welche Artikel die taggenauen Daten berechnet werden sollen.

Entwicklung

Der Entwicklungsprozess unterteilt sich in vier Phasen, in denen abwechselnd die beiden Anwendungen entwickelt werden. Die erste Anwendung wird mit Node.js umgesetzt, benutzt unter anderem das TypeScript und das TypeORM Framework und wird auf einem Server ausgeführt. Sie interagiert mit der Datenbank und stellt diese Daten über eine REST-Schnittstelle bereit. Die zweite Anwendung wird mithilfe des Angular Frameworks entwickelt und verarbeitet die bereitgestellten Daten.

Die erste Phase besteht darin die vorhandenen Datenbanktabellen mit dem Framework TypeORM abzubilden und diese anschließend über eine Schnittstelle verfügbar zu machen. In der Anwendung wird eine Schichtarchitektur benutzt, die aus einer Präsentationsschicht, einer Geschäftslogikschicht, einer Persistenzschicht und Datenbankschicht besteht. Bei einer validen Anfrage an die Anwendung werden nacheinander alle Schichten durchlaufen, wobei in der Präsentationsschicht begonnen wird. Von dort geht es von der Geschäftslogikschicht, über die Persistenzschicht in die Datenbankschicht, von der aus die Daten zurück bis in die Präsentationsschicht gereicht werden. In ihr werden HTTP Anfragen entgegengenommen und beantwortet, wobei geprüft wird, ob der Client berechtigt ist mit dem Server zu kommunizieren. Anschließend wird die Anfrage an die Geschäftslogikschicht weitergereicht, die prüft, ob der Client autorisiert ist diese Anfrage zu stellen und, ob sie valide ist. Falls ja, werden die benötigten Funktionen aus der Persistenzschicht aufgerufen. Diese übernimmt die Speicherlogik und wandelt dabei die Anwendungsobjekte in SQL bzw. Ergebnisse von SQL in Anwendungsobjekte um. In der Datenbankschicht werden die CRUD Operationen durchgeführt.

import {Column, Entity, JoinColumn, ManyToOne, PrimaryColumn} from 'typeorm';
import {Store} from './Store';
import {Article} from './Article';

$$@Entity({name: 'STORE_SALES_WEEKLY'})
export class StoreSalesArticle {

  $$@PrimaryColumn({name: 'START_OF_WEEK', type: 'date'})
  startOfWeek: Date;

  $$@ManyToOne(() => Article, {primary: true})
  $$@JoinColumn({name: 'ARTICLE_ID'})
  article: Article;

  $$@ManyToOne(() => Store, {primary: true})
  $$@JoinColumn({name: 'STORE_ID'})
  store: Store;

  $$@Column({name: 'QUANTITY'})
  quantity: number;
}

Dazu werden zunächst die benötigten Tabellen ausgewählt und als Entity-Objekte erstellt. Listing [lst:entity] zeigt beispielhaft, wie die Entity, die Daten der Tabelle, die Absätze pro Woche enthält, aussieht. Über die Annotations erstellt TypeORM eine Verknüpfung zwischen der TypeScript Klasse und der Datenbanktabelles. Die Annotation Entity gibt an zu welcher Tabelle die Klasse gehört, indem der Eigenschaft name ein Wert zugeordnet wird (SALES_WEEKLY). Mit PrimaryColumn und Column wird festgelegt welches Attribut der Klasse zu welcher Spalte gehört, wobei mithilfe des Wortes Primary angegeben wird, ob ein Attribut als Primary Key der Tabelle fungiert. Wie bei der Annotation Entity wird auch hier über die Eigenschaft name festgelegt, wie die Spalte der Tabelle heißt. Zudem wird die StoreSalesArticle-Entity mit den Entities für eine Filiale (store) und Artikel (article) verknüpft. Dazu wird die Annotation ManyToOne in Verbindung mit JoinColumn, welches die Spalte angibt mit der die andere Entity verknüpft ist (Foreign Key), verwendet. ManyToOne gibt an, dass zu jedem Objekt dieser Klasse ein Objekt der Filial- bzw. Artikelklasse und umgekehrt, dass es zu jedem Filial- bzw. Artikelobjekt mehrere Objekte dieser Klasse geben kann.

async getWeeklyStoreSales(skip: number, take: number): Promise<StoreSalesArticle[]> {
    return await getConnection(DBConnection.PRODUCTIVE)
      .getRepository(StoreSalesArticle)
      .find({
        order: {
          day: 'ASC'
        },
        skip,
        take
      });
}

async getWeeklyArticleSales(articleId: number, dateStart: Moment, dateUntil: Moment): Promise<WeeklyArticleSale[]> {
    const data: WeeklyArticleSale[] = await getConnection(DBConnection.PRODUCTIVE)
      .getRepository(StoreSalesArticle)
      .createQueryBuilder('sales')
      .select('sum(sales.quantity)', 'quantity')
      .addSelect('sales.startOfWeek', 'startOfWeek')
      .innerJoinAndSelect('sales.article', 'article')
      .where('sales.startOfWeek between :dateStart and :dateUntil')
      .andWhere('sales.article = :articleId')
      .groupBy('sales.startOfWeek')
      .addGroupBy('sales.article')
      .addGroupBy('article.id')
      .addGroupBy('article.name')
      .addGroupBy('article.description')
      .setParameters({
        articleId,
        dateStart: dateStart.toDate(),
        dateUntil: dateUntil.toDate()
      })
      .getRawMany();

    return data;
}

In Listing [lst:persitence] sind zwei Funktionen aus der Persistenzschicht zu sehen, die beide mithilfe des TypeORM Frameworks SQL generieren, welches Daten aus der Datenbankschicht abfragt. Die erste Funktion, die getWeeklyStoreSales-Funktion werden Daten der Tabelle der StoreSalesArticle abgefragt, wobei jeweils take viele und Zeilen zurückgegeben und skip viele übersprungen werden. Die getWeeklyArticleSales-Funktion summiert die Anzahl der Verkäufe pro Woche, sodass für jede Woche die Absätze aller Filialen summiert zurückgegeben werden.

async getWeeklyStoreSales(skip: number, take: number): Promise<StoreSalesArticle> {
    // do some checks here if needed
    return await getSalesRepo().getWeeklyStoreSales(skip, take);
  }

  async getWeeklyArticleSales(articleId: number, dateStart: Moment, dateUntil: Moment): Promise<WeeklyArticleSale[]> {
    // do some checks here if needed
    return await getSalesRepo().getWeeklyArticleSales(articleId, dateStart, dateUntil);
  }

In der Geschäftslogikschicht werden die Funktionen der Persistenzschicht aufgerufen (siehe Listing [lst:business]). Die aktuelle Fassung der Anwendung beinhaltet bisher keine Logik. Diese kann allerdings jeder Zeit hinzugefügt werden, um beispielsweise zu prüfen, ob der anfragende Nutzer berechtigt ist die Absatzdaten abzufragen.

- zunächst API/Schnittstelle in Node.js mit Typescript geschrieben, die auf Datenbank zugreift

  • Für Datenbankzugriff wird TypeORM eingesetzt
  • zunächst Artikeldaten und Absätze verfügbar machen
  • dann Filialdaten und Fotos der Artikel bereitstellen
    1. Tool zum Verarbeiten und Anzeigen der Daten entwickeln
  • benutzt Angular
  • zunächst liste von artikel anzeigen
  • dann für jeden Artikel eine art profilseite erstellen, auf der ein paar daten wie Name und Beschreibung angezeigt werden
  • dann absatzdaten laden und als graph anzeigen
  • Graph sowohl über die Jahre, als auch vergleichend (pro Monat, pro Woche)
  • API um Funktion zur Kopie ausgewählter Artikel erweitern
  • API um Schnittstelle zur Bereitstellung der kopierten Daten erweitern
  • Frontend-Tool, um Artikelliste der kopierten Daten erweitern
  • dann Profil Seite für kopierte Artikel erstellen
  • Seite zeigt berechnete Daten an
  • dazu gehören Tag genaue Absätze, Bestände und Retouren
  • welche Filiale an welchem Tag geöffnet war und ob sie Bestand hatte
  • Absätze pro Tag pro Land
  • etc.

Ergebnis

dd2de1917e419fc1d79409652e899b4ef89aa2b1.jpg Startseite der Programms

dd35eeef3c95ad649fe431c9380c6eebc2920643.jpg Allgemeine Übersicht der Artikel

a95cdc21a54b73718283fbf6a1353b91b524280f.jpg Die meistverkauften Artikel

842937136d36811098b41c914e52980a155f2244.jpg Übersichtsseite eines Artikels (Part I)

d24e65b77f204a2a38e2f0727de020988381ac9b.jpg Übersichtsseite eines Artikels (Part II)

f9b2b2cbe18d37afa7352bf55d67f5c11157499e.jpg Geöffnete Filialen nach Absatz und Öffnungszeiten zusammen

15fa5fd34f273baf543eda555627785d887e96a6.jpg Geöffnete Filialen nach Absatz und Öffnungszeiten einzeln

- Problem: Konnte nur über Absätze herausfinden, ob eine Filiale wirklich geöffnet war, da sonst nirgendwo gespeichert.

  • Man sieht deutlich die Differenz zwischen Filiale hätte geöffnet sein müssen und Filiale hatte tatsächlich geöffnet auf Basis von Verkäufen von Artikeln.

bf04482414e44f69b93f9dca06f2cc7b1972cd59.jpg Übersicht der kopierten Artikel

ffa4bae7e9589cd3a587b7029d924ee76d4c0c4e.jpg Übersicht eines kopierten Artikels

7af2aa961852141847f262fe948ace8ee2c59455.jpg Übersicht eines kopierten Artikels

f29c6efd7f37caa2de1ef54bf87f5ff017a04a52.jpg Übersicht eines kopierten Artikels

a7de3e10780db185ed2c3c433e004592dec79dd7.jpg Übersicht eines kopierten Artikels

Ansatz I – Zeitreihenprognose Wetterdaten

In diesem Kapitel werden zwei Ansätze zur Vorhersage von Zeitreihen vorgestellt, wobei der zweite Ansatz eine Weiterentwicklung des Ersten ist. Beide Ansätze prognostizieren mithilfe des TensorFlow-Frameworks (vgl. Abschnitt 2.3.9) die Lufttemperatur auf Basis von Wetterdaten, die das Max Planck Institut für Biogeochemie30 in Jena aufgezeichnet hat. Sie wurden in einem Jupyter-Notebook veröffentlicht. Der Datensatz beinhaltet 14 verschiedene Messwerte, wie Lufttemperatur, Luftdruck, Luftfeuchtigkeit und Luftdichte, die seit 2003 in 10-Minuten-Intervallen aufgezeichnet werden. Aus Effizienzgründen, damit das Trainieren des neuronalen Netzes nicht zu lange dauert, verwenden die Ansätze nur Messwerte zwischen 2009 und 2016, was $420.550$ Messungen bzw. Zeilen entspricht. Dieses Kapitel enthält zwei Abschnitte, die zunächst die Struktur und Funktionsweise der jeweiligen Ansätze erklären und anschließend die vorgenommenen Änderungen vorstellen, die nötig waren, um Absatzprognosen zu erstellen und die resultierenden Prognosen der verschiedenen Ansätze vergleichbar zu machen.

Version I

In diesem Abschnitt wird ein Ansatz zur Vorhersage von Zeitreihen mit dem Framework TensorFlow (vgl. Abschnitt 2.3.9) vorgestellt, der ein rekurrentes neuronales Netz mit LSTM-Zellen verwendet (TensorFlow 2020a).

Funktionsweise/Struktur

Der Ansatz gliedert sich in drei Teile: Zunächst werden $n$ Lufttemperaturmessungen betrachtet, mit denen die Temperatur zum Zeitpunkt $n+1$ vorhergesagt wird. Anschließend werden zusätzlich zu den $n$ Lufttemperaturmessungen auch die $n$ Luftdruck- und Luftdichtemessungen berücksichtigt, um eine Vorhersage der Lufttemperatur der $n+1$-ten Messung zu treffen. Zum Schluss wird statt der Vorhersage für den nächsten Wert $n+1$ eine Vorhersage für die Werte $n+1, \dots , n+k$ erzeugt.

zip_path = tf.keras.utils.get_file(
        origin='https://storage.googleapis.com/tensorflow/tf-keras-datasets/jena_climate_2009_2016.csv.zip',
        fname='jena_climate_2009_2016.csv.zip',
        extract=True
    )
    csv_path, _ = os.path.splitext(zip_path)

    df = pd.read_csv(csv_path)

Listing [lst:download_data] zeigt wie der Datensatz zunächst heruntergeladen (Zeilen 1-6) und eingelesen (Zeile 8) wird. Die Daten liegen im CSV-Format vor.

d6edc6f405eefd9533cc26db3dab1adcae0b1d04.png Auszug der Messwerte des Datensatzes

In Abbildung 4.1 sind die ersten fünf Zeilen des Datensatzes dargestellt. Die erste Spalte des Datensatzes beinhaltet den Zeitpunkt der Messung, gefolgt von Wettermessdaten, wie Lufttemperatur, Luftdruck und Luftdichte. Die Daten wurden in 10-Minuten-Intervallen erfasst, weswegen es sechs Messungen pro Stunde und daraus folgend $144$ ($6*24$) Messungen pro Tag gibt. Damit eine Vorhersage für die Lufttemperatur an Zeitpunkt $i$ getroffen werden kann, betrachtet dieser Ansatz die $n$ vorherigen Messungen. Daraus ergibt sich, dass die Eingabe $X$ aus $n$ Messwerten und die Ausgabe $Y$ aus der Lufttemperatur an Position $n+1$ besteht. Der Eingabevektor für die Vorhersage eines Wertes besteht aus einem Array der Länge $n$, wobei jeder Eintrag ein weiteres Array enthält, in dem die Temperaturen der letzten $n$ Messungen enthalten sind.

X = [
        [ // Eingabe 1
            [x_0_i], // Messwert an Zeitschritt i
            //...,
            [x_0_n], // Messwert an Zeitschritt n
        ],
        // ...
        [ // Eingabe n
            [x_k_i], // Messwert an Zeitschritt i
            // ...
            [x_k_n] // Messwert an Zeitschritt n
        ]
    ];
    Y = [
        y_1, // Ausgabe der 1. Eingabe
        // ...
        y_k // Ausgabe der n. Eingabe
    ];

In Listing [lst:example_input_data] sind Beispieltrainingseingabedaten (Zeile 1-13) und Beispieltrainingsausgabedaten (Zeile 14-18) zu sehen. Die Trainingseingabedaten bestehen aus einem dreidimensionalen Array, das in der ersten Dimension die $k$ Eingabedaten (Batches) (Zeile 1-13) enthält. In der zweiten Dimension sind die $n$ Messwerte (Zeile 2-6, Zeile 8-12) enthalten, die zur Vorhersage der Temperatur zum Zeitpunkt $n+1$ benötigt werden. Die dritte Dimension besteht aus einem Array, das jeweils einen der $n$ Messwerte enthält (siehe Zeile 3-5, Zeile 9-11). Das Array für die Trainingsausgabedaten beinhaltet zu jedem der $k$ Eingabevektoren eine Ausgabe (siehe Zeile 14-18). Die Länge eines Eingabevektors wird auch Fenstergröße genannt. Dadurch, dass für die Vorhersage eines Wertes eine Fenstergröße der Länge $n$ benötigt wird, kann der frühste Wert der Ausgabe der Messwert an der Position $n+1$ sein.

def univariate_data(dataset, start_index, end_index, history_size, target_size):
    data = []
    labels = []

    start_index = start_index + history_size
    if end_index is None:
        end_index = len(dataset) - target_size

    for i in range(start_index, end_index):
        indices = range(i - history_size, i)
        # Reshape data from (history_size,) to (history_size, 1)
        data.append(np.reshape(dataset[indices], (history_size, 1)))
        labels.append(dataset[i + target_size])
    return np.array(data), np.array(labels)

Mithilfe der Funktion aus Listing [lst:univariant_data] kann der Wetterdatensatz (siehe Abbildung 4.1) in die zuvor beschriebene Form für Ein- und Ausgabe gebracht werden. Die Funktion erwartet fünf Parameter, bestehend aus dem Datensatz, dem Bereich des Datensatzes, der in Eingabe- und Ausgabedaten aufgeteilt werden soll, der Fenstergröße und dem Index des Wertes, der vorhergesagt werden soll. Um den Wert an der Stelle $n+1$ vorherzusagen, muss eine target_size von $0$ gesetzt werden; für den Wert an Stelle $n+2$ muss eine target_size von $1$ gesetzt werden. Die Funktion gibt zwei Arrays zurück. Das erste Array enthält die Ein- und das zweite die Ausgabedaten.

uni_data = df['T (degC)']
    uni_data.index = df['Date Time']

Listing [lst:select_temp_data] zeigt, wie die Temperatur aus dem ursprünglichen Datensatz in ein neues Array kopiert (Zeile 1) und über den Zeitstempel indiziert (Zeile 2) wird.

6f681244adf0547220820db12219a77f786bafc4.png Zeitreihe der Temperaturmessungen

In Abbildung 4.2 wird das Array aus Listing [lst:select_temp_data] als Graph dargestellt, der die gemessenen Temperaturen auf der Y-Achse und den dazugehörigen Zeitpunkt auf der X-Achse abbildet.

TRAIN_SPLIT = 300000
    uni_train_mean = uni_data[:TRAIN_SPLIT].mean()
    uni_train_std = uni_data[:TRAIN_SPLIT].std()
    uni_data = (uni_data-uni_train_mean)/uni_train_std

Damit die Ergebnisse eines neuronalen Netzes überprüft werden können, teilt man einen Datensatz üblicherweise in Trainings- und Validierungsdaten auf. Die Trainingsdaten werden dem neuronalen Netz zum Lernen übergeben, wohingegen mit den Validierungsdaten das trainierte Modell überprüft werden soll. Des Weiteren ist es wichtig, dass die Daten normalisiert werden, da ein neuronales Netz so bessere Ergebnisse liefert, wenn möglichst kleine Ein- und Ausgaben verarbeitet werden. Nicht normalisierte Daten können zu sehr großen Gradienten führen, welche wiederum den Lernprozess – im Extremfall – scheitern lassen können. In Listing [lst:normalize_data] wird zunächst die Größe der Trainingsdaten festgelegt, welche $300.000$ Zeilen umfasst (Zeile 1), was ca. $71$ Prozent des Datensatzes sind. Anschließend werden der Mittelwert (Zeile 2) und die Standardabweichung (Zeile 3) der Trainingsdaten berechnet. Zum Schluss wird von jedem Messwert der Mittelwert abgezogen und das Ergebnis durch die Standardabweichung geteilt (Zeile 4).

$$norm: \mathbb{R} \rightarrow \mathbb{R},\ x \mapsto \frac{x-x_{mean}}{x_{std}} \label{eq:normalization}$$ Die Formel [eq:normalization] zeigt formal die Normalisierungsfunktion $norm$, mit der die Menge der Messwerte $X$ in die normalisierte Menge von Messwerten $Y$ überführt werden kann.

// Fenster von vergangenen Messdaten
    x = [[-1.99766294]
     [-2.04281897]
     [-2.05439744]
     [-2.0312405 ]
     [-2.02660912]
     [-2.00113649]
     [-1.95134907]
     [-1.95134907]
     [-1.98492663]
     [-2.04513467]
     [-2.08334362]
     [-2.09723778]
     [-2.09376424]
     [-2.09144854]
     [-2.07176515]
     [-2.07176515]
     [-2.07639653]
     [-2.08913285]
     [-2.09260639]
     [-2.10418486]]

    // Zieltemperatur, die vorhergesagt werden soll
    y = -2.1041848598100876

Listing [lst:normalized_data] zeigt die normalisierte Eingabe $x$ mit der normalisierten Ausgabe $y$ bei einer Fenstergröße von $20$.

Anschließend werden die Trainings- und Validierungsdaten gemischt, damit das Modell die Temperatur auf Basis der Messerwerte der Fenstergröße berechnet, statt ein Muster in der Reihenfolge der Eingabe zu suchen (was jedoch nicht ausgeschlossen werden kann).

simple_lstm_model = tf.keras.models.Sequential([
        tf.keras.layers.LSTM(8, input_shape=x_train_uni.shape[-2:]),
        tf.keras.layers.Dense(1)
    ])

    simple_lstm_model.compile(optimizer='adam', loss='mae')

In Listing [lst:app_1a_single_step_rnn] wird das Modell des neuronalen Netzes erstellt, welches aus zwei Schichten besteht (Zeile 1-4). Die erste Schicht beinhaltet LSTM-Zellen mit 8 Einheiten (Zeile 2) und die zweite enthält eine Dense-Schicht (Zeile 3), welche die Ausgabe $y$ bei Eingabe $x$ repräsentiert. Anschließend wird dem Modell der Optimierer ADAM und die Verlustfunktion Mean absolute error (Zeile 6) hinzugefügt, mit denen das Gradientenverfahren durchgeführt wird, welches die Gewichtsmatrix berechnet. Der Trainingsprozess des Modells wird durch die Funktion fit31 des TensorFlow-Frameworks gestartet.

EVALUATION_INTERVAL = 200
    EPOCHS = 10

    simple_lstm_model.fit(train_univariate,
                          epochs=EPOCHS,
                          steps_per_epoch=EVALUATION_INTERVAL,
                          validation_data=val_univariate,
                          validation_steps=50
    )

Im Listing [lst:app_1_start_training_example] ist der Funktionsaufruf zum Starten des Trainingsprozesses für das Modell aus Listing [lst:app_1a_single_step_rnn] dargestellt. Die Funktion erhält mehrere Parameter, wie die Trainingsdaten (Zeile 4), die Anzahl der Epochen (Zeile 5) und die Validierungsdaten (Zeile 7). Mit der Epochen-Anzahl wird angeben, über wie viele Iterationen das Gradientenverfahren laufen soll. Die Validierungsdaten werden nach Beendigung einer Epoche mit den berechneten Gewichte multipliziert, sodass eine weitere Fehlerrate berechnen kann, mit der die Qualität des Modells geprüft werden kann. Der Ansatz kürzt den Lernprozess jedoch aus Komplexitätsgründen ab, sodass pro Epoche nur $200$ Schritte (Zeile 1, Zeile 6) und zur Validierung nur $50$ Schritte (Zeile 8) durchgeführt werden.

pred_values = simple_lstm_model.predict(x_val)

Nachdem das Modell trainiert wurde, kann es eine Prognose berechnen, indem es eine Eingabe erhält, die in der Form der Trainingseingabedaten vorliegt. Diese Eingabe wird dann mit der berechneten Gewichtsmatrix des Modells multipliziert und erzeugt dann die im Modell festgelegte Ausgabe. In Listing [lst:prediction] werden dem Modell beispielsweise Eingaben der Validierungsmenge übergeben.

Im Folgenden wird der Ansatz erweitert, sodass die Temperatur nicht nur aufgrund vergangener Temperaturen vorhergesagt wird, sondern auch mithilfe anderer Messwerte.

features_considered = ['p (mbar)', 'T (degC)', 'rho (g/m**3)']
    features = df[features_considered]
    features.index = df['Date Time']
    features.head()

c01a911c0d98d8db1c90fc8296f6bcc55576d511.png Zeitreihe der Temperaturmessungen

Dazu werden zunächst die Messwerte, die als Eingabe berücksichtigt werden sollen, ausgewählt und wie zuvor beschrieben in ein neues Array kopiert (vgl. Listing [lst:multi_data]). Die Messwerte werden anhand des Zeitstempels indiziert (Zeile 3), sodass sie in einem Graphen dargestellt werden können (siehe Abbildung 4.3). Anschließend wird der Datensatz mithilfe des Mittelwerts und der Standardabweichung normalisiert.

def multivariate_data(
        dataset,
        target,
        start_index,
        end_index,
        history_size,
        target_size,
        step,
        single_step=False,
    ):
        data = []
        labels = []

        start_index = start_index + history_size
        if end_index is None:
            end_index = len(dataset) - target_size

        for i in range(start_index, end_index):
            indices = range(i - history_size, i, step)
            data.append(dataset[indices])

            if single_step:
                labels.append(target[i + target_size])
            else:
                labels.append(target[i : i + target_size])

        return np.array(data), np.array(labels)

Dafür muss die Funktion, die die Ein- und Ausgabedaten aus dem Datensatz extrahiert, angepasst werden. Statt, wie mit der Funktion aus Listing [lst:univariant_data], kann mithilfe der Funktion aus Listing [lst:multivariate_data] eine Netzeingabe erzeugt werden, die neben der Temperatur beliebig viele weitere Werte enthält. Diese Funktion erwartet acht Parameter (Zeile 2-9), die sich aus dem Datensatz, dem Ausgabedatensatz, dem zu betrachtenden Bereich des Datensatzes, der Fenstergröße, der Anzahl der vorherzusagenden Werte, der zu überspringenden Werte und ob mehrere Werte vorhergesagt werden sollen, zusammensetzt. Zudem ist es möglich nicht den Wert an Position $n+1$ als Ausgabe zu wählen, sondern einen Wert, der ferner in der Zukunft liegt. Außerdem ist es möglich Messwerte der Eingabedaten zu überspringen. Die Autoren des Ansatzes begründen dies damit, dass sich die Temperatur innerhalb einer Stunde nicht signifikant verändert, sodass sie nur stündliche, statt 10-minütige Messwerte im Eingabevektor verwenden. Die Funktion gibt ebenfalls sowohl ein Eingabevektorenarray als auch ein Ausgabevektorenarray zurück. Somit ändert sich die Struktur der Eingabe in der dritten Dimension, die mithilfe dieser Funktion nicht mehr die Länge eins, sondern die Länge der Messwerte hat. Da in diesem Ansatz drei Messwerte berücksichtigt werden, besteht die dritte Dimension aus diesen jeweiligen Messwerten.

single_step_model = tf.keras.models.Sequential()
    single_step_model.add(tf.keras.layers.LSTM(32,
                                               input_shape=x_train_single.shape[-2:]))
    single_step_model.add(tf.keras.layers.Dense(1))

    single_step_model.compile(optimizer=tf.keras.optimizers.RMSprop(), loss='mae')

Eine weitere Änderung wird im Modell des neuronalen Netzes vorgenommen. Dieses besteht erneut aus zwei Schichten, wobei die erste aus LSTM-Zellen und die zweite aus einer Dense-Ausgabeschicht besteht (siehe Listing [lst:app_1a_single_step_rnn_2]). Allerdings besitzt die LSTM-Schicht in diesem Modell $32$ statt acht Einheiten (Zeile 2). Darüber hinaus wird RMSprop als Optimierer (Zeile 6) verwendet. Die Verlustfunktion bleibt jedoch unverändert (Zeile 6). Dieses Modell wird durch Eingaben mit einer Fenstergröße von $720$ trainiert, wobei nur stündliche Messwerte (STEP von $6$) in der Eingabe vorhanden sind. Das bedeutet, dass von $720$ Messwerten nur jeder sechste als Eingabe genutzt wird. Als Ausgabe wird der Wert $n+72$ gewählt, der die Temperatur zwölf Stunden nach dem letzten Eingabewert enthält ($12*6$).

Zuletzt wird der Ansatz dahingehend geändert, dass nicht mehr nur ein Wert für eine Eingabe vorhergesagt wird, sondern beliebig viele. Dafür wird der letzte Parameter der Funktion aus Listing [lst:multivariate_data] auf False gesetzt, wodurch die Ausgabe alle Werte zwischen dem letzten Eingabewert eines Eingabevektors und der Variablen future_target enthält. Wählt man beispielsweise future_target $= 72$ und history_size $= 120$, dann besteht die Eingabe aus Messwerten $x_0, \dots, x_{119}$ und die Ausgabe aus den Messwerten $x_{120}, \dots, x_{191}$. Im Ansatz wird, wie zuvor, eine Fenstergröße von $720$ mit STEP von $6$ gewählt, sodass die Eingabe die stündlichen Messdaten der letzten fünf Tage berücksichtigt. Für die Ausgabe wird das history_target auf $72$ und single_step auf False gesetzt, sodass statt einer Ausgabe $72$ Ausgaben berechnet werden. Das Modell wird ebenfalls erweitert, indem eine weitere LSTM-Schicht hinzugefügt wird.

multi_step_model = tf.keras.models.Sequential()
    multi_step_model.add(tf.keras.layers.LSTM(32,
                                              return_sequences=True,
                                              input_shape=x_train_multi.shape[-2:]))
    multi_step_model.add(tf.keras.layers.LSTM(16, activation='relu'))
    multi_step_model.add(tf.keras.layers.Dense(72))

    multi_step_model.compile(optimizer=tf.keras.optimizers.RMSprop(clipvalue=1.0), loss='mae')

Daraus ergibt sich das Modell aus Listing [lst:app_1a_multi_step_rnn], welches aus einer LSTM-Schicht mit $32$ Einheiten, einer weiteren LSTM-Schicht mit $16$ Einheiten und einer Dense-Schicht mit $72$ Einheiten besteht. Die Anzahl der Einheiten der Dense-Schicht entspricht der Anzahl der vorherzusagenden Werte. Im Vergleich zum Modell aus Listing [lst:app_1a_single_step_rnn] und [lst:app_1a_single_step_rnn_2] hat sich die Verlustfunktion nicht verändert. Als Optimierer wird erneut, wie in Listing [lst:app_1a_single_step_rnn_2], RMSprop verwendet, wobei die clipvalue auf $1.0$ gesetzt wird, was dazu führt, dass Gradienten ignoriert werden, wenn ihr absoluter Wert den Wert von clipvalue überschreitet.

Anpassung auf TEDi Artikel

Der zuvor beschriebene Ansatz erzeugt eine Vorhersage für die Lufttemperatur auf Basis von Wetterdaten. Damit mit diesem Ansatz Absatzprognosen erzeugt werden können, muss das Einlesen der Daten verändert werden. Dazu wird die in Kapitel 3 vorgestellte REST-Schnittstelle verwendet, die Daten im JSON-Format bereitstellt. Zudem werden zu Beginn des Jupyter-Notebooks einige Variablen definiert, mit denen sich die verschiedenen Modelle des Ansatzes einfacher steuern lassen. Darüber hinaus wird die Aufteilung von Trainings- und Validierungsdaten so angepasst, dass prozentual angegeben werden kann, wie groß die Menge der beiden Datenmengen sein soll. Außerdem verfügt der Ansatz über keine Denormalisierung, welche ebenfalls hinzugefügt werden muss. Des Weiteren sind die Modelle des Ansatzes nicht miteinander vergleichbar, weil die ersten beiden Modelle nur einen Wert vorhersagen. Deswegen werden die Vorhersagen der Modelle so angepasst, dass sie mehrere Zeitpunkte vorhersagen können, indem sie den vorhergesagten Wert als Eingabe für die nächste Vorhersage berücksichtigen. Ein weiterer Unterschied zu den Absatzdaten ist, dass der Wetterdatensatz über $400.000$ Zeilen enthält, wohingegen die Absatzdaten ab 2015 täglich aufgezeichnet wurden, sodass im Schnitt pro Jahr ca. $365,25$ Absatztage gespeichert werden. Aufgrund der Menge an Daten wurden die Wetterdaten im Training in verschiedene Batches eingeteilt, um das Modell effizienter zu berechnen. Diese Einteilung ist bei den Absatzdaten, aufgrund der wenigen Messwerte, nicht nötig und kann entfernt werden.

Version II

Dieser Abschnitt beschreibt die zweite Version des Ansatzes zur Zeitreihenvorhersage der Tensorflow-Dokumentation. Er ist eine Weiterentwicklung der ersten Version (siehe Abschnitt 4.1) und vergleicht Vorhersagen von vorwärts-verketteten neuronalen Netzen (siehe Abbildung [fig:ann_example]), CNNs (siehe Abschnitt 2.2.3) und RNNs (siehe Abschnitt 2.2.1) miteinander.

Funktionsweise/Struktur

Der Ansatz gliedert sich in drei Teile und verwendet, wie die erste Version (siehe Abschnitt 4.1), den Wetterdatensatz des Max Plank Instituts in Jena. Im Gegensatz zur ersten Version legt dieser Ansatz ein größeres Augenmerk auf die Vorbereitung der Trainingsdaten und vergleicht im Anschluss verschiedene Modellstile. Zum einen wird ein rekurrentes neuronales Netz (siehe Abschnitt 2.2.1) mit LSTM-Zellen (siehe Abschnitt 2.2.2) und zum anderen ein faltendes neuronales Netz (siehe Abschnitt 2.2.3) verwendet. Zur besseren Einordnung werden zusätzlich Prognosen mithilfe von zwei neuronalen Netzen mit Dense-Schicht erstellt. Wie in Version I erzeugt dieser Ansatz zwei verschiedene Arten von Prognosen. Zunächst wird der nächste Zeitschritt vorhergesagt, bevor zum Schluss erneut $n$ Zeitschritte vorhergesagt werden.

Im Unterschied zur ersten Version werden in diesem Ansatz die 14 verschiedenen Messwerte des Datensatzes für die Vorhersage berücksichtigt, weswegen der Datenvorbereitungsprozess ausführlicher abläuft. Aus Effizienzgründen, damit das Trainieren der Modelle nicht zu lange dauert und weil sich die Temperatur innerhalb von zehn Minuten selten signifikant ändert, werden nur die stündlichen Messwerte des Datensatzes beachtet.

470f0f091f921e5834a4a34a69e6024519eef63b.png Statistische Kennzahlen des Wetterdatensatzes

Der erste Schritt, um die Daten für das Training vorzubereiten, besteht darin, einige statistische Kennzahlen, wie die Standardabweichung, den Mittelwert oder den maximalen Wert, zu berechnen (siehe Abbildung 4.4). Die Berechnung erfolgt über den Funktionsaufruf df.describe().transpose(), wobei das df-Objekt den Datensatz beinhaltet. Beim Lesen der Werte fällt auf, dass die min-Werte der Messwerte wv (m/s) und max. wv (m/s) fehlerhaft sind. Diese fehlerhaften Werte werden durch den Wert $0$ ersetzt.

Des Weiteren fällt auf, dass die Windrichtung als Winkel angegeben ist (siehe wd (deg)), was jedoch keine gute Modelleingabe ist, weil $360^\circ$ und $0^\circ$ in dieser Darstellung nicht nah beieinander liegen. Aus diesem Grund wird die Windrichtung in einen Vektor, bestehend aus $x$- und $y$-Koordinate, umgewandelt.

wd_rad = df.pop('wd (deg)')*np.pi / 180

    # Calculate the wind x and y components.
    df['Wx'] = wv*np.cos(wd_rad)
    df['Wy'] = wv*np.sin(wd_rad)

    # Calculate the max wind x and y components.
    df['max Wx'] = max_wv*np.cos(wd_rad)
    df['max Wy'] = max_wv*np.sin(wd_rad)

In Listing [lst:sin_cos_wind] ist die Umsetzung zu sehen. Zunächst wird die Windrichtung ins Bogenmaß umgerechnet (Zeile 1), woraufhin die $x$- und $y$-Koordinaten der Windgeschwindigkeiten berechnet werden (Zeile 4-5, Zeile 8-9).

Zuletzt werden die Zeitpunkte der Messwerte als Eingabedaten vorbereitet, da das Wetter eine tägliche und jährliche Periodizität aufweist. Dafür muss zunächst der Zeitstempel in eine Zahl umgewandelt werden, weil Strings eine invalide Eingabe für neuronale Netze sind. Deswegen wird jeder Zeitpunkt in einen UNIX-Timestamp umgewandelt, der die vergangenen Sekunden seit dem 01.01.1970 anzeigt. Danach kann der Timestamp mithilfe der Winkelfunktionen Sinus und Kosinus verwendet werden.

day = 24*60*60
    year = (365.2425)*day

    df['Day sin'] = np.sin(timestamp_s * (2 * np.pi / day))
    df['Day cos'] = np.cos(timestamp_s * (2 * np.pi / day))
    df['Year sin'] = np.sin(timestamp_s * (2 * np.pi / year))
    df['Year cos'] = np.cos(timestamp_s * (2 * np.pi / year))

Listing [lst:sin_cos_date] zeigt die Umsetzung mithilfe der Winkelfunktionen. Zunächst werden zwei Konstanten angelegt, in denen die Sekunden eines Tages und die eines Jahres gespeichert werden (Zeile 1-2). Danach wird für jede Zeile des Datensatzes df ein Wert zwischen $-1$ und $1$ für tägliche bzw. jährliche Periodizität mithilfe der Winkelfunktionen Sinus und Kosinus berechnet.

Anschließend werden die Daten in Trainings-, Validierungs- und Testmenge eingeteilt, wobei die ersten 70 Prozent des Datensatzes in der Trainings-, die nächsten 20 Prozent in der Validierungs- und die übrigen zehn Prozent in der Testmenge beinhaltet sind. Die Trainingsmenge wird als Modelleingabe verwendet. Zusätzlich wird die Validierungsmenge dem Modell im Trainingsprozess hinzugefügt, um das Modell auf Basis von im Trainingsprozess nicht berücksichtigten Daten zu prüfen und die Fehlerrate nach jeder Epoche zu berechnen, sodass diese in der nächsten Trainingsiteration indirekt einfließen kann. Zum Schluss wird mit der Testmenge überprüft, wie hoch die Fehlerrate bei gänzlich unbekannten Daten ist. Wie schon im ersten Ansatz werden die Daten, bevor sie als Eingabe in das Modell eingefügt werden, normalisiert. Dafür werden erneut der Mittelwert und die Standardabweichung des Trainingsdatensatzes berechnet und anschließend alle drei Mengen, analog zur Normalisierung aus Abschnitt 4.1.1, mithilfe der berechneten Werte und der norm-Funktion [eq:normalization] normalisiert.

3a2e853b140bd22a8c3be9babf9328857c13a4e9.png Verteilung der normalisierten Messwerte

In Abbildung 4.5 ist die Verteilung der normalisierten Messwerte in einem Violinendiagramm dargestellt. Auf der $X$-Achse befinden sich die Namen der verschiedenen Messwerte und auf der $Y$-Achse der Wertebereich aller normalisierten Messwerte. Je breiter ein Graph eines Messwerts an einer Stelle ist, desto häufiger tritt dieser Messwert auf. Umgekehrt, je schmaler ein Graph, desto seltener tritt dieser Messwert auf.

Zur Berechnung der Ein- und Ausgaben der Modelle dieses Ansatzes wird eine Klasse (Fenstergenerierungs-Klasse) definiert. Die Ein- und Ausgaben haben dieselbe Form, wie die des letzten Modells des ersten Ansatzes (vgl. Listing [lst:example_input_data] aus Abschnitt 4.1.1); das heißt, dass für jede Ausgabe $y$ die $k$ vorherigen Zeitschritte (Fenstergröße) berücksichtigt werden, die wiederum die verschiedenen Messwerte beinhalten. Im Unterschied zur Funktion aus Listing [lst:multivariate_data] bietet die Klasse die Möglichkeit mehrere Ausgaben zu erhalten, demnach mehr als einen Messwert (beispielsweise Lufttemperatur und Luftfeuchtigkeit).

51ce997789f05d02c2eafdd71eb540263c381cb5.png Einteilen der Daten in Ein- und Ausgabe (TensorFlow 2020b)

Abbildung 4.6 zeigt eine abstrakte Darstellung einer Aufteilung einer Sequenz (rot) in Eingabe (blau) und Ausgabe (grün), bei einer gegebenen Fenstergröße von sechs und einem Versatz und einer Vorhersage-Größe von eins. Die Länge der Sequenz berechnet sich aus der Addition der Fenstergröße mit dem Versatz ($6+1$), weshalb die Sequenz die sieben Elemente $t=0$ bis $t=6$ beinhaltet. Mithilfe des Versatzes wird bestimmt, welche Elemente zur Ausgabe gehören, indem die Ausgabe aus den letzten Elementen der Sequenz besteht. Die Anzahl der Elemente ist abhängig von der Vorhersage-Größe. Die Sequenz der Abbildung wird an der sechsten Stelle geteilt, sodass zwei neue Folgen entstehen. Die blaue Folge, mit einer Länge der Fenstergröße, enthält die Eingabedaten und die grüne die Ausgabedaten.

class WindowGenerator():
      def __init__(self, input_width, label_width, shift,
                   train_df=train_df, val_df=val_df, test_df=test_df,
                   label_columns=None):
        # Store the raw data.
        self.train_df = train_df
        self.val_df = val_df
        self.test_df = test_df
    
        # Work out the label column indices.
        self.label_columns = label_columns
        if label_columns is not None:
          self.label_columns_indices = {name: i for i, name in
                                        enumerate(label_columns)}
        self.column_indices = {name: i for i, name in
                               enumerate(train_df.columns)}
    
        # Work out the window parameters.
        self.input_width = input_width
        self.label_width = label_width
        self.shift = shift
    
        self.total_window_size = input_width + shift
    
        self.input_slice = slice(0, input_width)
        self.input_indices = np.arange(self.total_window_size)[self.input_slice]
    
        self.label_start = self.total_window_size - self.label_width
        self.labels_slice = slice(self.label_start, None)
        self.label_indices = np.arange(self.total_window_size)[self.labels_slice]

Ein Auszug der zuvor genannten Fenstergenerierungs-Klasse ist Listing [lst:window_generator_class] zu sehen. Objekte, die von dieser Klasse instanziiert werden, müssen Parameter angeben, mit denen die Modell Ein- und Ausgabe erzeugt wird. Diese Parameter sind die Fenstergröße (input_width), die Ausgabegröße (label_width), welche angibt wie viele Zeitschritte vorhergesagt werden sollen, der Versatz zwischen dem letzten Eingabe-Zeitschritt und dem letzten Ausgabe-Zeitschritt (shift), die drei Datenmengen (Training (train_df), Validierung (val_df) und Test (test_df)) und die Namen der Messwerte, die vorhergesagt werden sollen (label_columns) (vgl. Zeile 2-4). Anschließend werden die übergebenen Parameter innerhalb der Klasse gespeichert und weitere Hilfswerte berechnet (Zeile 6-30). Zu den berechneten Werten gehören die Indices der Messwerte der Ein- und Ausgabe des Datensatzes (Zeile 13-15), die Gesamtgröße einer Ein- und Ausgabe (Zeile 23) und die Indices der Ein- und Ausgabe, mit deren Hilfe die Struktur für die Ein- und Ausgabepaar berechnet wird (Zeile 25-30). Damit Paare von Ein- und Ausgaben in Form der Abbildung 4.6 erzeugt werden, muss der Wert $6$ für die Variable input_width und der Wert $1$ für die Variablen label_width und shift übergeben werden.

def split_window(self, features):
        inputs = features[:, self.input_slice, :]
        labels = features[:, self.labels_slice, :]
        if self.label_columns is not None:
            labels = tf.stack(
                [labels[:, :, self.column_indices[name]] for name in self.label_columns],
                axis=-1,
            )

        # Slicing doesn't preserve static shape information, so set the shapes
        # manually. This way the `tf.data.Datasets` are easier to inspect.
        inputs.set_shape([None, self.input_width, None])
        labels.set_shape([None, self.label_width, None])

        return inputs, labels

Das Listing [lst:window_generator_class_split] zeigt eine weitere Funktion der Fenstergenerierungs-Klasse. Diese Funktion spaltet die Messwerte der Zeitschritte (Sequenzen) in ein Paar aus Ein- und Ausgabe auf und bekommt beim Aufruf den Parameter features (Zeile 1) übergeben, der diese Sequenzen enthält. Das feature-Objekt besteht aus einem Array, in dem jedes Element aus einer Sequenz besteht. Zunächst werden das Eingabe- und das Ausgabe-Array, mithilfe der, in der Fenstergenerierungs-Klasse berechneten Werte, erstellt und mit den passenden Werten der Sequenzen gefüllt (Zeile 2-3; vgl. Abbildung 4.6). In den Zeilen 4-8 werden die einzelnen Messwerte der Ausgabe in das Ausgabe-Array geschrieben. Am Ende der Funktion werden erst die Strukturen der Arrays neu gesetzt, da sie während der Umwandlung gelöscht wurden (Zeile 12-13) und anschließend die beiden Arrays für Ein- und Ausgabe zurückgegeben (Zeile 15).

Im letzten Schritt muss der ganze Trainingsdatensatz in Paare, bestehend aus Ein- und Ausgabe, umgewandelt werden, sodass die Modelle damit trainiert werden können.

def make_dataset(self, data):
    data = np.array(data, dtype=np.float32)
    ds = tf.keras.preprocessing.timeseries_dataset_from_array(
        data=data,
        targets=None,
        sequence_length=self.total_window_size,
        sequence_stride=1,
        shuffle=True,
        batch_size=32,
    )

    ds = ds.map(self.split_window)

    return ds

Dies ist in Listing [lst:input_data_set] zu sehen. Zunächst wird der übergebene Datensatz (Zeile 1) in ein zweidimensionales Array vom Typ Float (Fließkommazahl) umgewandelt (Zeile 2), bei dem jede Zeile einen Zeitschritt und jede Spalte die dazugehörenden Messwerte beinhaltet. Danach wird mithilfe der timeseries_dataset_from_array-Funktion32 des TensorFlow-Frameworks das zuvor erstellte Array in sogenannte Batches eingeteilt (Zeile 3-11). Die Batches (dt. Bündel) bestehen aus batch_size-vielen (Zeile 9) Paaren der Ein- und Ausgaben und werden verwendet, damit der Trainingsprozess eines neuronalen Netzes nicht zu speicherintensiv wird. Die Funktion iteriert über die übergebenen Daten, kombiniert sequence_length-viele Zeilen zu einem Array (Sequenz) und fügt die Sequenz dem Batch hinzu. Sobald die Anzahl Sequenzen eines Batches mit der batch_size übereinstimmt, wird ein neues Batch erstellt. Dies geschieht allerdings nur, wenn das Batch komplett gefüllt werden kann; sonst verfallen die übrigen Zeilen. Nach jeder Iteration wird der Anfangswert der jeweiligen Sequenz durch die Inkrementierung des vorherigen Startwerts um den Wert der Variablen sequence_stride bestimmt. Das bedeutet, dass bei einer Sequenz-Größe von $10$ und einer sequence_stride von $1$ in der ersten Iteration die ersten zehn Elemente ($e_0$ bis $e_{9}$) zu einer Sequenz zusammengefasst werden. In der zweiten Iteration werden die Elemente $e_1$ bis $e_{10}$ zu einer Sequenz zusammengefügt usw. Mit der Variable shuffle (Zeile 8) wird bestimmt, ob die Sequenzen innerhalb der Batches gemischt (zufällig sortiert) werden sollen. Zum Schluss wird zunächst die zuvor beschriebene split_window-Funktion (siehe Listing 4.6) auf jede Sequenz der Batches angewendet (Zeile 12), sodass alle Sequenzen aus den Ein- und Ausgabe Paaren bestehen und anschließend der umgewandelte Datensatz ds zurückgegeben (Zeile 14).

const x = [
        [messwert_1, /*... ,*/ messwert_n], // Zeitschritt 1
        // ...
        [messwert_1, /*... ,*/ messwert_n]  // Zeitschritt k
    ];

    const y = [ausgabe_1, /*...,*/ ausgabe_n];

In Listing [lst:example_tensor_input] ist beispielhaft die Eingabe $x$ für eine Ausgabe $y$ in Form zweier Arrays dargestellt. Die Konstante x (Zeile 1-5) beinhaltet die Eingabe und die Konstante y (Zeile 7) die mögliche Ausgabe, die aus mehreren Messwerten bestehen kann. Innerhalb des Eingabearrays befinden sich $n$ weitere Arrays (Zeile 2-4), welche die $k$ Zeitschritte darstellen und die abhängig von der Fenstergröße sind. Die einzelnen Zeitschrittarrays beinhalten die verschiedenen Messwerte des Zeitschritts. Im Trainingsprozess, der den Trainingsdatensatz berücksichtigt, besteht die Eingabe der Modelle aus einem Array mit Elementen, die aufgebaut sind, wie das Array der Konstanten x und die Ausgabe aus einem Array mit Elementen der Form der Konstanten y haben. Das bedeutet, dass die Form der Eingabedaten für den Trainingsprozess des Modells dreidimensional sind, wobei die Ein- und Ausgaben der Eingabedaten dieselbe Form haben müssen. In der ersten Dimension befinden sich die Batches, in der zweiten die Zeitschritte der Batches und in der dritten die ausgewählten Messwerte des Zeitschritts ([Batches, Zeitschritte, Messwerte]).

Die im Folgenden beschriebenen Modelle verwenden alle den ADAM-Optimierer und den Mean squared error als Fehlerfunktion. Zunächst werden die Modelle vorgestellt, die genau einen Zeitschritt vorhersagen.

linear = tf.keras.Sequential([tf.keras.layers.Dense(units=1)])

Listing [lst:app1_b_single_step_linear] zeigt ein lineares Modell, welches aus einer Schicht mit einem Neuron besteht, das die Ausgabe beinhaltet. Dieses Modell erhält als Eingabe die Messwerte eines Zeitschritts und als Ausgabe den Messwert eines anderen Zeitschritts, der vorhergesagt werden soll. Die Struktur der Eingabe besteht aus den Batches, den Zeitschritten und den Messwerten der Zeitschritte ([Batches, Zeitschritte, Messwerte]). Eine Ausgabe muss dieselbe Anzahl an Zeitschritten beinhalten wie die Eingabe und darf maximal einen Messwert haben, weil das Modell aus nur einem Neuron besteht.

dense = tf.keras.Sequential(
        [
            tf.keras.layers.Dense(units=64, activation="relu"),
            tf.keras.layers.Dense(units=64, activation="relu"),
            tf.keras.layers.Dense(units=1),
        ]
    )

Das Modell aus Listing [lst:app1_b_single_step_dense] baut auf dem vorherigen Modell auf, hat jedoch mehrere Schichten mit mehreren Neuronen, wobei die letzte Schicht (Zeile 5) dem vorherigen Modell gleicht. Es verarbeitet die gleiche Art Eingaben wie das vorherige Modell.

multi_step_dense = tf.keras.Sequential(
        [
            # Shape: (time, features) => (time*features)
            tf.keras.layers.Flatten(),
            tf.keras.layers.Dense(units=32, activation="relu"),
            tf.keras.layers.Dense(units=32, activation="relu"),
            tf.keras.layers.Dense(units=1),
            # Add back the time dimension.
            # Shape: (outputs) => (1, outputs)
            tf.keras.layers.Reshape([1, -1]),
        ]
    )

Listing [lst:app1_b_single_step_multi_input_dense] erweitert das Modell erneut, sodass die Eingabe des Modells aus mehreren Zeitschritten bestehen kann, sodass beispielsweise die Messwerte der vergangenen drei Stunden zur Vorhersage der Temperatur der vierten Stunde verwendet werden können. Dabei kann die Anzahl der vergangenen Zeitschritte beliebig groß sein. Der Unterschied zu den vorherigen Modellen besteht darin, dass diesem Modell eine Flatten-Schicht (Zeile 4) hinzugefügt wird, welche die Messwerte von verschiedenen Zeitschritten miteinander kombiniert. Damit entsteht aus einer Eingabe der Form $(\text{Zeitschritte}, \text{Messwerte})$ die Form $(\text{Zeitschritte}*\text{Messwerte})$, womit es möglich ist mehrere Zeitschritte in der Eingabe zu berücksichtigen. Aufgrund der Strukturänderung innerhalb des Modells, muss die Ausgabe wieder zurückgewandelt werden, was mit der Reshape-Schicht (Zeile 10) umgesetzt wird. Dadurch hat die Ausgabe statt der Form $(\text{Ausgabe})$ die Form $(1,\text{Ausgabe})$, womit sie mit der Form der Ausgabe der Modelleingabe übereinstimmt (andernfalls könnten sie nicht verglichen werden).

conv_model = tf.keras.Sequential(
        [
            tf.keras.layers.Conv1D(
                filters=32, kernel_size=3, activation="relu"
            ),
            tf.keras.layers.Dense(units=32, activation="relu"),
            tf.keras.layers.Dense(units=1),
        ]
    )

In Listing [lst:app1_b_single_step_cnn] ist das Modell eines CNNs zu sehen. Es ist ähnlich aufgebaut wie das vorwärts-verkettete Netz aus Listing [lst:app1_b_single_step_multi_input_dense]. Der Unterschied besteht darin, dass anstelle der Flatten-Schicht eine faltende Schicht (Zeile 3-5) verwendet wird und deswegen die Ausgabe des Netzes nicht umgewandelt werden muss. Das liegt daran, dass die faltende Schicht die Eingabe in der ursprünglichen Form entgegennehmen kann. Die Variable filter gibt an, wie groß die Ausgabe der Conv1D-Schicht sein soll. Aus diesem Grund besteht die folgende Dense-Schicht aus 32 Neuronen (Zeile 6).

lstm_model = tf.keras.models.Sequential(
        [
            # Shape [batch, time, features] =>
            tf.keras.layers.LSTM(32, return_sequences=True),
            # Shape =>
            tf.keras.layers.Dense(units=1),
        ]
    )

In Listing [lst:app1_b_single_step_rnn] ist ein rekurrentes neuronales Netz mit LSTM-Zellen zu sehen. Es ist aufgebaut wie das Modell aus Listing [lst:app_1a_single_step_rnn_2] und erwartet somit dieselbe Art Eingabe.

Als nächstes werden Modelle vorgestellt, die statt eines Zeitschritts mehrere Zeitschritte vorhersagen.

multi_linear_model = tf.keras.Sequential(
        [
            # Take the last time-step.
            # Shape [batch, time, features] =>
            tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
            # Shape =>
            tf.keras.layers.Dense(
                OUT_STEPS * num_features, kernel_initializer=tf.initializers.zeros
            ),
            # Shape =>
            tf.keras.layers.Reshape([OUT_STEPS, num_features]),
        ]
    )

Listing [lst:app1_b_multi_step_linear] zeigt eine Erweiterung des linearen Modells aus Listing [lst:app1_b_single_step_linear], das durch das Hinzufügen der Lambda-Schicht (Zeile 5) und der Reshape-Schicht (Zeile 11) eine Vorhersage von mehreren Zeitschritten, für eine Eingabe, die ebenfalls aus mehreren Zeitschritten pro Batch besteht, treffen kann. Mithilfe der Lambda-Schicht werden die Zeitschritte in die Messwerte-Ebene der Eingabe verschoben, sodass die erweiterte Dense-Schicht (siehe Listing [lst:app1_b_multi_step_dense]) die Eingabe lesen kann. Die Reshape-Schicht des Modells ist dazu da, um die Ausgabe der Dense-Schicht in die Form der Eingabe umzuwandeln (Batches, Zeitschritte, Messwerte), sodass sie zur Ausgabe, die das Modell als Eingabe bekommen hat, passt.

multi_dense_model = tf.keras.Sequential(
        [
            # Take the last time step.
            # Shape [batch, time, features] =>
            tf.keras.layers.Lambda(lambda x: x[:, -1:, :]),
            # Shape =>
            tf.keras.layers.Dense(512, activation="relu"),
            # Shape =>
            tf.keras.layers.Dense(
                OUT_STEPS * num_features, kernel_initializer=tf.initializers.zeros
            ),
            # Shape =>
            tf.keras.layers.Reshape([OUT_STEPS, num_features]),
        ]
    )

Das in Listing [lst:app1_b_multi_step_dense] dargestellte Modell ist eine Erweiterung des zuvor vorgestellten linearen Modells um eine weitere Dense-Schicht. Der Unterschied zum vorwärts-verketteten neuronalen Netz, das einen Zeitschritt vorhersagt, liegt darin, dass die Dense-Schicht (Zeile 7) aus mehr Neuronen besteht und eine Dense-Schicht entfernt wurde. Die Erhöhung der Neuronen-Anzahl liegt daran, dass das Netz mehr Ausgaben produzieren muss.

CONV_WIDTH = 3
    multi_conv_model = tf.keras.Sequential(
        [
            # Shape [batch, time, features] =>
            tf.keras.layers.Lambda(lambda x: x[:, -CONV_WIDTH:, :]),
            # Shape =>
            tf.keras.layers.Conv1D(256, activation="relu", kernel_size=(CONV_WIDTH)),
            # Shape =>
            tf.keras.layers.Dense(
                OUT_STEPS * num_features, kernel_initializer=tf.initializers.zeros
            ),
            # Shape =>
            tf.keras.layers.Reshape([OUT_STEPS, num_features]),
        ]
    )

Listing [lst:app1_b_multi_step_cnn] zeigt eine Erweiterung das vorherigen CNN Modells. Wie die vorherigen Modelle, die größere Ausgaben berechnen sollen, wurde ebenfalls die Neuronen-Anzahl der Schichten erhöht.

multi_lstm_model = tf.keras.Sequential(
        [
            # Shape [batch, time, features] =>
            # Adding more `lstm_units` just overfits more quickly.
            tf.keras.layers.LSTM(32, return_sequences=False),
            # Shape =>
            tf.keras.layers.Dense(
                OUT_STEPS * num_features, kernel_initializer=tf.initializers.zeros
            ),
            # Shape =>
            tf.keras.layers.Reshape([OUT_STEPS, num_features]),
        ]
    )

Das letzte Modell dieses Ansatzes ist in Listing [lst:app1_b_multi_step_rnn] zu sehen. Der Unterschied zum RNN aus Listing [lst:app1_b_single_step_rnn] besteht ebenfalls in der Erhöhung der Neuronen der Dense-Schicht.

Anpassung auf TEDi Artikel

Die Anpassungen, die vorgenommen werden mussten, um die Modelle statt Wetterdaten Absätze vorhersagen zu lassen, überschneiden sich größtenteils mit den Änderungen aus Abschnitt 4.1.2. Zunächst müssen die passenden Daten geladen werden, was analog zu der Änderung der ersten Version abläuft. Anschließend wurden erneut globale Variablen definiert, mit denen die Modelle bzw. die Modell-Parameter gesteuert werden können. Die Bearbeitung der Messwerte, die in diesem Ansatz sehr ausführlich bearbeitet wurde, ist für die Absatzdaten in dieser Form nicht notwendig (Kein Windmesswert in den Absatzdaten vorhanden). Die Umwandlung der Zeitstempel mithilfe der Winkelfunktionen Sinus und Kosinus ist nur für die jährliche Periodizität notwendig, da keine stündlichen Absatzdaten betrachtet werden. Zudem wurde erneut eine Funktion zur Vorhersage von längeren Zeitintervallen hinzugefügt, welche die Ergebnisse der Modelle als Eingabe verwendet, sodass beliebig lange Intervalle vorhergesagt werden können. Außerdem wurde eine Funktion hinzugefügt, welche die Normalisierung rückgängig macht.

Ansatz II – Zeitreihenprognose Differenzen

In diesem Kapitel wird der zweite Ansatz zur Zeitreihenvorhersage vorgestellt. Dieser stellt ein Modell eines rekurrenten neuronalen Netzes vor, mit dem der monatliche Absatz für die nächsten sechs Monate vorhergesagt wird, indem mithilfe der Differenzen der letzten zwölf Monate die Differenz für den nächsten Monat vorhergesagt wird. Dazu wird zunächst die Differenz der Absätze des aktuellen Monats, mit der des vorherigen Monats gebildet ($\text{diff}{curr_i} = x_i - x{i-1}$). Anschließend wird die Differenz der Absätze des Vormonats mit der des Vorvormonats gebildet usw., bis die Differenzen für die letzten zwölf Monate gebildet wurden ($\text{diff}{curr}=(\text{diff}{curr-1}, \dots, \text{diff}_{curr-12})$). Zunächst wird die Funktionsweise des Ansatzes erklärt und anschließend werden die vorgenommenen Anpassungen vorgestellt. Der Ansatz wurde 2019 von Barış Karaman auf der Platform towards data science veröffentlicht. (Karaman 2019)

Funktionsweise/Struktur

In diesem Abschnitt wird der Aufbau der Modelleingabe und der des Modells erklärt. - Bibliotheken importieren

  • Daten laden (csv einbinden)
  • Daten ansehen bzw. plotten
  • Differenz zum Vormonat bilden
  • Differenz der letzten 12 Monate zum Vormonat bilden
  • Vorhersage mithilfe der Lags bewerten, indem adjustiertes Bestimmtheitsmaß eingesetzt wird
  • Daten in Trainings- und Testmenge aufteilen
  • Daten mit MinMax Scaler zwischen -1 und 1 normalisieren
  • Input hat shape (trainingsdatenlänge, Differenzenlänge)
  • Output ist differenz von aktueller Monat zu nächstem
model = Sequential()
    model.add(LSTM(4, batch_input_shape=(1, X_train.shape[1], X_train.shape[2]), stateful=True))
    model.add(Dense(1))

    model.compile(loss='mean_squared_error', optimizer='adam')

- Modell bauen mit Sequentiell, LSTM mit 4 units und Dense Layer

  • optimizer ADAM, loss Mean Squared Error
    ADAM ist ein Gradientenverfahren, welches eingesetzt wird um Optimierungsprobleme, wie den Gradientenabstieg, zu lösen.

- Dann Test validieren mit trainiertem Model

  • Werte denormalisieren, damit valide Absatzdaten
  • Daten plotten, um kurven besser vergleichen zu können

Anpassung auf TEDi Artikel

Dieser Abschnitt behandelt die Anpassungen, die nötig waren, um den Ansatz mit Absatzdaten von TEDi zu nutzen. Zunächst muss das Einbinden der Daten geändert werden. Im ursprünglichen Ansatz wird eine CSV-Datei eingelesen. Die TEDi Absatzdaten können allerdings aufgrund der Anwendung, die in Kapitel 3 vorgestellt wurde, dynamisch über REST-Schnittstelle abgefragt werden.
Dadurch, dass der Ansatz Prognosen für unterschiedliche Artikel generieren soll, wurden einige globale Variablen angelegt, die dem Netz als Parameter übergeben werden. Aufgrund dessen sind die Parameter des Netzes so anpassbar, dass sie für jeden Artikel, für den eine Vorhersage erzeugt wird, gleich eingestellt werden können, sodass diese Prognosen miteinander vergleichbar sind.
Abbildung [fig:ann_example] zeigt die globalen Variablen zur Steuerung des Fehleralorithmus, des Optimizers, der Units, der Epochen des Netzes, der Lernrate und der Aufteilung von Trainings- und Validierungsdaten.


Erstellung der Differenzen verallgemeinert, sodass nicht nur Jahres-, sondern beliebige Intervalle gewählt werden können. …
Funktion zur Vorhersage der Zukunft hinzugefügt.

Vergleich der Ansätze

In diesem Kapitel werden die Ergebnisse der angepassten Modelle der Ansätze aus den Kapiteln 4 und 5 vorgestellt und miteinander verglichen. Die Modelle wurden jeweils mit vier verschiedenen Artikeln aus unterschiedlichen Kategorien trainiert. Die Artikel kommen aus den Kategorien Küchen-Artikel, Sommer-Artikel, Halloween-Artikel und Elektro-Artikel. Dabei wurden die Zeiteinheiten angepasst, sodass die Modelle Prognosen, auf Basis von täglichen, wöchentlichen und monatlichen Absatzdaten, erstellen. Die Modelle aus Kapitel 4 nutzen dieselben Eingabedaten, wohingegen das Modell aus Kapitel 5, dem Ansatz geschuldet, andere Eingaben nutzt. Die Eingabedaten der ersten Modelle bestehen aus dem Absatz, der Anzahl geöffneter Filialen, den Absätzen des vorherigen Zeitpunkts, der Differenz der Absätze zwischen aktuellem und vorherigem Zeitpunkt und, abhängig von der Zeiteinheit, dem Tag im Jahr, dem Tag in der Woche und der Periodizität im Jahr. Das Modell des zweiten Ansatzes verwendet die Differenzen der Absätze innerhalb eines Jahres und die Anzahl der geöffneten Filialen, die den Artikel vorrätig hatten. Jedes Modell wird mit der gleichen Aufteilung der Daten trainiert (70 Prozent Trainingsmenge, 20 Prozent Validierungsmenge, 10 Prozent Testmenge). Ein zeitlicher Aspekt als Vergleichskriterium kann außen vor gelassen werden, weil die Modelle, wegen der geringen Datenmenge, schnell trainiert werden können.

Das Kapitel unterteilt sich in zwei Abschnitte. Der erste Abschnitt vergleicht die Modelle hinsichtlich ihrer Fehlerraten im Trainingsprozess und der Fehlerrate über den Validierungsdaten. Anschließend werden ihre Prognosen, welche die nächsten zwölf Monate vorhersagen, gegenüber gestellt.

Im zweiten Abschnitt werden erneut Prognosen mit den Modellen erzeugt, jedoch werden statt täglichen Absätzen wöchentliche und monatliche Absätze betrachtet.

Untersuchung der Ansätze im Hinblick auf Vergleichskriterien

  1. A2 ist schneller weil weniger Tief

  2. A2 liefert bessere Ergebnisse

  3. A2 erlaubt, dass Absatz unter 0 fällt, weil mit Differenzen gearbeitet wird

Anpassung der Zeiteinheit

Zusammenfassung und Ausblick

Fazit

  1. Datenvorbereitung sehr wichtig: je besser die Vorbereitung, desto zuverlässiger die Ergebnisse (siehe Ansatz 2)

  2. Problem, dass recht wenig Vergangenheitsdaten zur Verfügung stehen, sodass Modelle aus Ansatz I und II nicht so viel trainiert werden können.

  3. Google Ansätze benötigen viel Rechenzeit, weil größere Tiefe

  4. Google Ansatz zwei liefert bessere Ergebnisse als eins

  5. Überraschend, dass Ansatz mit sehr geringer Tiefe trotzdem gute Vorhersage liefert

Ausblick

Diese Arbeit betrachtete drei Ansätze zur Vorhersage von Zeitreihen und verglicht deren Vorhersagen zu verschiedenen Artikeln miteinander. Hauptsächlich wurde sich auf die Bereitstellung der Artikeldaten für die verschiedenen Ansätze konzentriert, sodass Prognosen für verschiedene Artikel erzeugt werden konnten. Dabei wurden zudem zeitlich unterschiedlich aggregierte Absatzdaten von jeweils einer Filiale oder allen Filialen zusammen betrachtet. Eine offene Frage ist, in wieweit die Ansätze angepasst werden können, um die Vorhersagen weiter zu verbessern, indem beispielsweise andere Fehlerfunktionen, Optimierer oder Modelle verwendet werden. Zudem ist es interessant zu erfahren was passiert, wenn die trainierten Modelle der verschiedenen Artikel für andere Artikel eingesetzt werden. Eine weitere Frage ist, welche Art Prognose erzeugt werden wird, wenn jeder Zeitschritt einer Eingabe die Absatzdaten aller Filialen einzeln enthält, statt wie bisher entweder die Summe der Absätze aller Filialen oder die Absätze einer einzelnen Filiale. Zuletzt kann in einer weiteren Arbeit vorhergesagt werden, für wie viele Tage in einer Filiale vorrätige Artikel ausreichen. Eine solche Prognose berücksichtigt, zusätzlich zu den Absatzdaten, ebenfalls die Bestandsdaten und die Eigenheiten einer Filiale.

Hiermit versichere ich, dass ich die vorliegende Arbeit selbstständig verfasst habe und keine anderen als die angegebenen Quellen und Hilfsmittel verwendet sowie Zitate kenntlich gemacht habe.

Dortmund, den

Sören Borgstedt

Angular. 2021. “Angular Documentation.” https://angular.io/docs.

Atienza, Rowel. 2018. Advanced Deep Learning with Keras: Apply Deep Learning Techniques, Autoencoders, GANs, Variational Autoencoders, Deep Reinforcement Learning, Policy Gradients, and More. Birmingham, UK: Packt Publishing Ltd.

Bauer, Christian, and Gavin King. 2016. Java Persistance with Hibernate (version 2). New York, USA: Manning Publications Co.

Beaulieu, Alan. 2020. Learning SQL: Master SQL Fundamentals (version 3). Sebastopol, CA, USA: O’Reilly.

bfortuner, and AyushSenapati. 2017. “Machine Learning Glossary.” https://ml-cheatsheet.readthedocs.io/en/latest/gradient_descent.html.

Brosset, Pascal, Anne-Laure Thieullent, Sergey Patsko, Jerome Buvat, Yashwardhan Khemka, Amol Khadikar, and Abhishek Jain. 2019. “Scaling AI in Manufacturing Operations: A Practitioners’ Perspective.” Paper. Capgemini Research Institute; Capgemini Research Institute. https://web.archive.org/web/20201028193046/https://www.capgemini.com/wp-content/uploads/2019/12/AI-in-manufacturing-operations.pdf.

Browning, J. Burton, and Marty Alchin. 2019. Pro Python 3: Features and Tools for Professional Development (version 3). New York, USA: Apress Media.

Bush, Josephine. 2020. Learn SQL Database Programming: Query and Manipulate Databases from Popular Relational Database Servers Using SQL (version 1). Birmingham, UK: Packt Publishing Ltd.

Chollet, Francois. 2018. Deep Learning with Python (version 1). New York, USA: Manning.

Chrislb. 2005. “Darstellung Eines Künstlichen Neurons Mit Seinen Elementen.” https://de.wikipedia.org/wiki/K%C3%BCnstliches_Neuron#/media/Datei:ArtificialNeuronModel_deutsch.png.

Codd, Edgar Frank. 1970. “Relational Model of Data for Large Shared Data Banks.”

Deru, Matthieu, and Alassane Ndiaye. 2020. Deep Learning Mit Tensorflow, Keras Und Tensorflow.js. 2nd ed. Bonn, Deutschland: Rheinwerk Computing.

Freeman, Adam. 2019. Essential TypeScript: From Beginner to Pro. Berkeley, USA: Apress.

———. 2020. Pro Angular 9: Build Powerful and Dynamic Web Apps. New York, USA: Aress Media.

Galea, Alex. 2018. Applied Data Science with Python and Jupyter: Use Powerful Industry-Standard Tools to Unlock New, Actionable Insights from Your Data. Birmingham, UK: Packt Publishing Ltd.

Gansser, Oliver, and Bianca Krol. 2015. Markt-Und Absatzprognosen. Heidelberg, Deutschland: Springer.

Graves, Alex, and Jürgen Schmidhuber. 2005. “Framewise Phoneme Classification with Bidirectional LSTM Networks.” In Proceedings. 2005 IEEE International Joint Conference on Neural Networks, 2005., 4:2047–52. IEEE.

Hochreiter, JS Sepp, and J Schmidhuber. 1997. “Long Short-Term Memory: Neural Computation 9(8):1735-1780.” Fakultät für Informatik, Technische Universität München, Licence Details: Https://Www. Bioinf. Jku. At/Publications/Older/2604. Pdf.

Karaman, Barış. 2019. “Predicting Sales: Forecasting the Monthly Sales with LSTM.” Medium.com. https://towardsdatascience.com/predicting-sales-611cb5a252de.

Keith, Mike, Merrick Schincariol, and Massimo Nardone. 2018. Pro JPA 2 in Java EE 8: An in-Depth Guide to Java Persistence APIs (version 3). Berkeley, USA: Apress.

Kotaru, Venkata Keerti. 2020. Angular for Material Design: Leverage Angular Material and TypeScript to Build a Rich User Interface for Web Apps. New York, USA: Aress Media.

Loy, James. 2019. Neural Network Projects with Python: The Ultimate Guide to Using Python to Explore the True Power of Neural Networks Through Six Projects. Birmingham, UK: Packt Publishing Ltd.

Lubanovic, Bill. 2020. Introducing Python (version 2). Sebastopol, CA, USA: O’Reilly.

Mardan, Azat. 2018. Full Stack JavaScript: Learn Backbone.js, Node.js, and MongoDB (version 2). New York, USA: Apress Media.

Meffert, Heribert, Christoph Burmann, Manfred Kirchgeorg, and Maik Eisenbeiß. 2015. Marketing: Grundlagen Marktorientierter Unternehmensführung Konzepte–Instrumente–Praxisbeispiele. 12th ed. Wiesbaden, Deutschland: Springer.

Menke, Birger. 2016. “Go-Duell Mensch Vs. Software: Technisches k.o.” Spiegel Online, March. https://www.spiegel.de/netzwelt/gadgets/go-duell-software-alphago-siegt-gegen-lee-sedol-a-1081975.html.

Mercyse. 2016. “Darstellung Es Künstlichen Neuronalen Netzes Mit Rückkopplung.” https://de.wikipedia.org/wiki/Datei:Neuronal-Networks-Feedback.png.

“Monatliche Dichte an Atmosphärischem Kohlendioxid in Volumenteilen Pro Million Für Mauna Loa.” n.d. NOAA Mauna Loa Laboratory. https://seos-project.eu/timeseries/timeseries-c02-p07.de.html.

Müller, Andreas C, and Sarah Guido. 2017. Einführung in Machine Learning Mit Python: Praxiswissen Data Science (version 1). Translated by Kristian Rother. Heidelberg, Deutschland: dpunkt.verlag GmbH.

O’Hanlon, Peter. 2019. Advanced TypeScript Programming Projects: Build 9 Different Apps with TypeScript 3 and JavaScript Frameworks Such as Angular, React and Vue. Birmingham, UK: Packt Publishing Ltd.

Olah, Christopher. 2015. “Darstellung Einer LSTM-Zelle.” https://colah.github.io/posts/2015-08-Understanding-LSTMs/.

Pepels, Werner. 2016. Produktmanagement. 7th ed. Berlin, Deutschland: Duncker & Humbold GmbH.

Purkait, Niloy. 2019. Hands-on Neural Networks with Keras: Design and Create Neural Networks Using Deep Learning and Artificial Intelligence Principles. 1st ed. Birmingham, UK: Packt Publishing Ltd.

Python. 2021. “Python Documentation.” https://docs.python.org/3/license.html.

Ruder, Sebastian. 2016. “An Overview of Gradient Descent Optimization Algorithms.” arXiv Preprint arXiv:1609.04747.

Scheer, A-W. 2013. Absatzprognosen. Berlin, Deutschland: Springer-Verlag.

Schlittgen, Rainer, and Bernd HJ Streitberg. 2001. Zeitreihenanalyse. 9th ed. München, Deutschland: Oldenbourg Verlag.

Seck, Friedrich. n.d. “KI Entwicklung.” https://medium.com/@friedrich.seck/forschungsfeld-ki-k%C3%BCnstliche-intelligenz-maschinelles-lernen-deep-learning-und-knn-959c21715b20.

Shute, Zachary. 2019. Advanced JavaScript: Speed up Web Development with the Powerful Features and Benefits of JavaScript. Birmingham, UK: Packt Publishing Ltd.

Silver, David, Aja Huang, Chris J Maddison, Arthur Guez, Laurent Sifre, George Van Den Driessche, Julian Schrittwieser, et al. 2016. “Mastering the Game of Go with Deep Neural Networks and Tree Search.” Nature 529 (7587): 484–89.

Springer, Sebastian. 2018. Node. Js: Das Umfassende Handbuch. Bonn, Deutschland: Rheinwerk Verlag.

TensorFlow. 2020a. “Time Series Forecasting V1.” Google. https://web.archive.org/web/20200423221841if_/https://www.tensorflow.org/tutorials/structured_data/time_series.

———. 2020b. “Time Series Forecasting V2.” Google. https://web.archive.org/web/20201213071636/https://www.tensorflow.org/tutorials/structured_data/time_series.

———. 2021. “TensorFlow Documentation.” https://www.tensorflow.org/learn.

Toomey, Dan. 2017. Jupyter for Data Science: Exploratory Analysis, Statistical Modeling, Machine Learning, and Data Visualization with Jupyter. Birmingham, UK: Packt Publishing Ltd.

TypeORM. 2021. “TypeORM Docs.” https://github.com/typeorm/typeorm/blob/master/README.md.

“TypeScript Handbook.” 2021. Microsoft. https://www.typescriptlang.org/docs/handbook/intro.html.

VanderPlay, Jake. 2018. Data Science Mit Python: Das Handbuch Für Den Einsatz von IPython, Jupyter, NumPy, Pandas, Matplotlib, Scikit-Learn (version 1). Translated by Knut Lorenzen. Bonn, Deutschland: Mitp Verlag.

Vasilev, Ivan, Daniel Slater, Gianmario Spacagna, Peter Roelants, and Valentino Zocca. 2019. Python Deep Learning: Exploring Deep Learning Techniques and Neural Network Architectures with Pytorch, Keras, and TensorFlow. Birmingham, UK: Packt Publishing Ltd.

Wartala, Ramon. 2018. Praxiseinstieg Deep Learning: Mit Python, Caffe, TensorFlow Und Spark Eigene Deep-Learning-Anwendungen Erstellen. 1st ed. Heidelberg, Deutschland: dpunkt.verlag GmbH.

Zaccone, Giancarlo, and Md Rezaul Karim. 2018. Deep Learning with TensorFlow: Explore Neural Networks and Build Intelligent Systems with Python. Birmingham, UK: Packt Publishing Ltd.

Zammetti, Frank. 2020. Modern Full-Stack Development: Using TypeScript, React, Node. Js, Webpack, and Docker. Berkeley, USA: Apress.


  1. TensorFlow Dokumentation (https://www.tensorflow.org/tutorials↩︎

  2. Anzahl der verkauften Produkte ↩︎

  3. weitere Informationen zu ECMAScript https://developer.mozilla.org/en-US/docs/Web/JavaScript/Language_Resources ↩︎

  4. Liste der populärsten Programmiersprachen: https://pypl.github.io/PYPL.html ↩︎

  5. https://developer.mozilla.org/en/docs/Web/JavaScript/Data_structures ↩︎

  6. siehe https://v8.dev/ ↩︎

  7. Video des Vortrags: https://www.youtube.com/watch?v=EeYvFl7li9E ↩︎

  8. https://www.npmjs.com ↩︎

  9. Input/Output bzw. Eingabe/Ausgabe eines Programms an seine Umgebung ↩︎

  10. https://en.wikipedia.org/wiki/Thread_(computing) ↩︎

  11. das DBMS entscheidet wie viele Spalten in der Praxis erlaubt sind ↩︎

  12. https://github.com/typeorm/typeorm/ ↩︎

  13. Übersicht verschiedener Open-Source-Lizenzen: https://ifross.github.io/ifrOSS/Lizenzcenter ↩︎

  14. siehe https://github.com/angular/angular ↩︎

  15. https://www.delta.com/ ↩︎

  16. https://www.ryanair.com/ ↩︎

  17. https://www.office.com/, wenn angemeldet ↩︎

  18. https://console.cloud.google.com/ ↩︎

  19. https://homepages.cwi.nl/~steven/abc/ ↩︎

  20. https://jupyter.org ↩︎

  21. https://ipython.readthedocs.io/en/stable/ ↩︎

  22. https://julialang.org/ ↩︎

  23. https://www.python.org/ ↩︎

  24. https://www.r-project.org/ ↩︎

  25. https://www.haskell.org/ ↩︎

  26. https://www.ruby-lang.org/ ↩︎

  27. https://pandas.pydata.org/ ↩︎

  28. https://plotly.com/ ↩︎

  29. TensorFlow Aktivierungsfunktionen: https://www.tensorflow.org/api_docs/python/tf/keras/activations ↩︎

  30. https://www.bgc-jena.mpg.de/wetter/ ↩︎

  31. API-Referenz der Funktion: https://www.tensorflow.org/api_docs/python/tf/keras/Mode#fit ↩︎

  32. API-Referenz der Funktion: https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/timeseries_dataset_from_array ↩︎