Visual Question Answering with Keras – Part 1

This is Part I of II of the Article Series Visual Question Answering with Keras

Making Computers Intelligent to answer from images

If we look closer in the history of Artificial Intelligence (AI), the Deep Learning has gained more popularity in the recent years and has achieved the human-level performance in the tasks such as Speech Recognition, Image Classification, Object Detection, Machine Translation and so on. However, as humans, not only we but also a five-year child can normally perform these tasks without much inconvenience. But the development of such systems with these capabilities has always considered an ambitious goal for the researchers as well as for developers.

In this series of blog posts, I will cover an introduction to something called VQA (Visual Question Answering), its available datasets, the Neural Network approach for VQA and its implementation in Keras and the applications of this challenging problem in real life. 

Table of Contents:

1 Introduction

2 What is exactly Visual Question Answering?

3 Prerequisites

4 Datasets available for VQA

4.1 DAQUAR Dataset

4.2 CLEVR Dataset

4.3 FigureQA Dataset

4.4 VQA Dataset

5 Real-life applications of VQA

6 Conclusion


  1. Introduction:

Let’s say you are given a below picture along with one question. Can you answer it?

I expect confidently you all say it is the Kitchen without much inconvenience which is also the right answer. Even a five-year child who just started to learn things might answer this question correctly.

Alright, but can you write a computer program for such type of task that takes image and question about the image as an input and gives us answer as output?

Before the development of the Deep Neural Network, this problem was considered as one of the difficult, inconceivable and challenging problem for the AI researcher’s community. However, due to the recent advancement of Deep Learning the systems are capable of answering these questions with the promising result if we have a required dataset.

Now I hope you have got at least some intuition of a problem that we are going to discuss in this series of blog posts. Let’s try to formalize the problem in the below section.

  1. What is exactly Visual Question Answering?:

We can define, “Visual Question Answering(VQA) is a system that takes an image and natural language question about the image as an input and generates natural language answer as an output.”

VQA is a research area that requires an understanding of vision(Computer Vision)  as well as text(NLP). The main beauty of VQA is that the reasoning part is performed in the context of the image. So if we have an image with the corresponding question then the system must able to understand the image well in order to generate an appropriate answer. For example, if the question is the number of persons then the system must able to detect faces of the persons. To answer the color of the horse the system need to detect the objects in the image. Many of these common problems such as face detection, object detection, binary object classification(yes or no), etc. have been solved in the field of Computer Vision with good results.

To summarize a good VQA system must be able to address the typical problems of CV as well as NLP.

To get a better feel of VQA you can try online VQA demo by CloudCV. You just go to this link and try uploading the picture you want and ask the related question to the picture, the system will generate the answer to it.


  1. Prerequisites:

In the next post, I will walk you through the code for this problem using Keras. So I assume that you are familiar with:

  1. Fundamental concepts of Machine Learning
  2. Multi-Layered Perceptron
  3. Convolutional Neural Network
  4. Recurrent Neural Network (especially LSTM)
  5. Gradient Descent and Backpropagation
  6. Transfer Learning
  7. Hyperparameter Optimization
  8. Python and Keras syntax
  1. Datasets available for VQA:

As you know problems related to the CV or NLP the availability of the dataset is the key to solve the problem. The complex problems like VQA, the dataset must cover all possibilities of questions answers in real-world scenarios. In this section, I will cover some of the datasets available for VQA.

4.1 DAQUAR Dataset:

The DAQUAR dataset is the first dataset for VQA that contains only indoor scenes. It shows the accuracy of 50.2% on the human baseline. It contains images from the NYU_Depth dataset.

Example of DAQUAR dataset

Example of DAQUAR dataset

The main disadvantage of DAQUAR is the size of the dataset is very small to capture all possible indoor scenes.

4.2 CLEVR Dataset:

The CLEVR Dataset from Stanford contains the questions about the object of a different type, colors, shapes, sizes, and material.

It has

  • A training set of 70,000 images and 699,989 questions
  • A validation set of 15,000 images and 149,991 questions
  • A test set of 15,000 images and 14,988 questions

Image Source:


4.3 FigureQA Dataset:

FigureQA Dataset contains questions about the bar graphs, line plots, and pie charts. It has 1,327,368 questions for 100,000 images in the training set.

4.4 VQA Dataset:

As comapred to all datasets that we have seen so far VQA dataset is relatively larger. The VQA dataset contains open ended as well as multiple choice questions. VQA v2 dataset contains:

  • 82,783 training images from COCO (common objects in context) dataset
  • 40, 504 validation images and 81,434 validation images
  • 443,757 question-answer pairs for training images
  • 214,354 question-answer pairs for validation images.

As you might expect this dataset is very huge and contains 12.6 GB of training images only. I have used this dataset in the next post but a very small subset of it.

This dataset also contains abstract cartoon images. Each image has 3 questions and each question has 10 multiple choice answers.

  1. Real-life applications of VQA:

There are many applications of VQA. One of the famous applications is to help visually impaired people and blind peoples. In 2016, Microsoft has released the “Seeing AI” app for visually impaired people to describe the surrounding environment around them. You can watch this video for the prototype of the Seeing AI app.

Another application could be on social media or e-commerce sites. VQA can be also used for educational purposes.

  1. Conclusion:

I hope this explanation will give you a good idea of Visual Question Answering. In the next blog post, I will walk you through the code in Keras.

If you like my explanations, do provide some feedback, comments, etc. and stay tuned for the next post.

Introduction to ROC Curve

The abbreviation ROC stands for Receiver Operating Characteristic. Its main purpose is to illustrate the diagnostic ability of classifier as the discrimination threshold is varied. It was developed during World War II when Radar operators had to decide if the blip on the screen is an enemy target, a friendly ship or just a noise.  For these purposes they measured the ability of a radar receiver operator to make these important distinctions, which was called the Receiver Operating Characteristic.

Later it was found useful in interpreting medical test results and then in Machine learning classification problems. In order to get an introduction to binary classification and terms like ‘precision’ and ‘recall’ one can look into my earlier blog  here.

True positive rate and false positive rate

Let’s imagine a situation where a fire alarm is installed in a kitchen. The alarm is supposed to emit a sound in case fire smoke is detected in the room. Unfortunately, there is a lot of cooking done in the kitchen and the alarm may trigger the sound too often. Thus, instead of serving a purpose the alarm becomes a nuisance due to a large number of false alarms. In statistical terms these types of errors are called type 1 errors, or false positives.

One way to deal with this problem is to simply decrease sensitivity of the device. We do this by increasing the trigger threshold at the alarm setting. But then, not every alarm should have the same threshold setting. Consider the same type of device but kept in a bedroom. With high threshold, the device might miss smoke from a real short-circuit in the wires which poses a real danger of fire. This kind of failure is called Type 2 error or a false negative. Although the two devices are the same, different types of threshold settings are optimal for different circumstances.

To specify this more formally, let us describe the performance of a binary classifier at a particular threshold by the following parameters:


These parameters take different values at different thresholds. Hence, they define the performance of the classifier at particular threshold. But we want to examine in overall how good a classifier is. Fortunately, there is a way to do that. We plot the True Positive Rate (TPR) and False Positive rate (FPR) at different thresholds and this plot is called ROC curve.

Let’s try to understand this with an example.

A case with a distinct population distribution

Let’s suppose there is a disease which can be identified with deficiency of some parameter (maybe a certain vitamin). The distribution of population with this disease has a mean vitamin concentration sharply distinct from the mean of a healthy population, as shown below.

This is result of dummy data simulating population of 2000 people,the link to the code is given  in the end of this blog.  As the two populations are distinctly separated (there is no  overlap between the two distributions), we can expect that a classifier would have an easy job distinquishing healthy from sick people. We can run a logistic regression classifier with a threshold of .5 and be 100% succesful in detecting the decease.

The confusion matrix may look something like this.

In this ideal case with a threshold  of  .5 we do not make a single wrong classification. The True positive rate and False positive rate are 1 and 0, respectively. But we can shift the threshold. In that case, we will  get different confusion matrices. First we plot threshold vs. TPR.

We see for most values of threshold the TPR is close to 1 which again proves data is easy to classify and the classifier is returning high probabilities  for the most of positives .

Similarly Let’s plot threshold vs. FPR.

For most of the data points FPR is close to zero. This is also good. Now its time to plot the ROC curve using these results (TPR vs FPR).

Let’s try to interpret  the results,  all the points lie on line x=0 and y=1, it means for all the points FPR is zero or TPR is one, making  the curve a square. which means the classifier does perfectly well.

Case with overlapping  population distribution

The above example was about a perfect classifer. However, life is often not so easy. Now let us consider another more realistic situation in which the parameter distribution of the population is not as distinct as in the previous case. Rather, the mean of the parameter with healthy and not healthy datapoints are close and the distributions overlap, as shown in the next figure.

If we set the threshold to 0.5, the confusion matrix may look like this.

Now, any new choice of threshold location will affect both false positives and false negatives. In fact, there is a trade-off. If we shift the threshold with the goal to reduce false negatives, false positives will increase. If we move the threshold to the other direction and reduce false positive, false negatives will increase.

The plots (TPR vs Threshold) , (FPR vs Threshold) are shown below

If we plot the ROC curve from these results, it looks like this:

From the curve we see the classifier does not perform as well as the earlier one.

What else can be infered from this curve? We first need to understand what the diagonal in this plot represent. The diagonal represents ‘Line of no discrimination’, which we obtain if we randomly guess. This is the ROC curve for the worst possible classifier. Therefore, by comparing the obtained ROC curve with the diagonal, we see how much better our classifer is from random guessing.

The further away ROC curve from the diagonal is (the closest it is to the top left corner) , better the classifier is.

Area Under the curve

The overall performance of the classifier is given by the area under the ROC curve and is usually denoted as AUC. Since TPR and FPR lie within the range of 0 to 1, the AUC also assumes values between 0 and 1. The higher the value of AUC, the better is the overall performance of the classifier.

Let’s see this for the two different distributions which we saw earlier.

As we know the classifier had worked perfectly in the first case with points at (0,1) the area under the curve is 1 which is perfect. In the latter case the classifier was not able to perform as good, the ROC curve is between the diagonal and left hand corner. The AUC as we can see is less than 1.

Some other general characteristics

There are still few points that needs to be discussed on a General ROC curve

  • The ROC curve does not provide information about the actual values of thresholds used for the classifier.
  • Performance of different classifiers can be compared using the AUC of different Classifier. The larger the AUC, the better the classifier.
  • The vertical distance of the ROC curve from the no discrimination line gives a measure of ‘INFORMEDNESS’. This is known as Youden’s J satistic. This statistics can take values between 0 and 1.

Youden’s  J statistic is defined for every point on the ROC curve . The point at which Youden’s  J satistics reaches its maximum for a given ROC curve can be used to guide the selection of the threshold to be used for that classifier.

I hope this post does the job of providing an understanding of ROC curves  and AUC. The  Python program for simulating the example given earlier can be found here .

Please feel free to adjust the mean of the distributions and see the changes in the plot.

Fehler-Rückführung mit der Backpropagation

Dies ist Artikel 4 von 6 der Artikelserie –Einstieg in Deep Learning.

Das Gradienten(abstiegs)verfahren ist der Schlüssel zum Training einzelner Neuronen bzw. deren Gewichtungen zu den Neuronen der vorherigen Schicht. Wer dieses Prinzip verstanden hat, hat bereits die halbe Miete zum Verständnis des Trainings von künstlichen neuronalen Netzen.

Der Gradientenabstieg wird häufig fälschlicherweise mit der Backpropagation gleichgesetzt, jedoch ist das nicht ganz richtig, denn die Backpropagation ist mehr als die Anwendung des Gradientenabstiegs.

Bevor wir die Backpropagation erläutern, nochmal kurz zurück zur Forward-Propagation, die die eigentliche Prädiktion über ein künstliches neuronales Netz darstellt:


Abbildung 1: Ein simples kleines künstliches neuronales Netz mit zwei Schichten (+ Eingabeschicht) und zwei Neuronen pro Schicht.

In einem kleinen künstlichen neuronalen Netz, wie es in der Abbildung 1 dargestellt ist, und das alle Neuronen über die Sigmoid-Funktion aktiviert, wird jedes Neuron eine Nettoeingabe z berechnen…

z = w^{T} \cdot x

… und diese Nettoeingabe in die Sigmoid-Funktion einspeisen…

\phi(z) = sigmoid(z) = \frac{1}{1 + e^{-z}}

… die dann das einzelne Neuron aktiviert. Die Aktivierung erfolgt also in der mittleren Schicht (N-Schicht) wie folgt:

N_{j} = \frac{1}{1 + e^{- \sum (w_{ij} \cdot x_{i}) }}

Die beiden Aktivierungsausgaben N werden dann als Berechnungsgrundlage für die Ausgaben der Ausgabeschicht o verwendet. Auch die Ausgabe-Neuronen berechnen ihre jeweilige Nettoeingabe z und aktivieren über Sigmoid(z).

Ausgabe eines Ausgabeknotens als Funktion der Eingänge und der Verknüpfungsgewichte für ein dreischichtiges neuronales Netz, mit nur zwei Knoten je Schicht, kann also wie folgt zusammen gefasst werden:

O_{k} = \frac{1}{1 + e^{- \sum (w_{jk} \cdot \frac{1}{1 + e^{- \sum (w_{ij} \cdot x_{i}) }}) }}

Abbildung 2: Forward-Propagation. Aktivierung via Sigmoid-Funktion.

Sollte dies die erste Forward-Propagation gewesen sein, wird der Output noch nicht auf den Input abgestimmt sein. Diese Abstimmung erfolgt in Form der Gewichtsanpassung im Training des neuronalen Netzes, über die zuvor erwähnte Gradientenmethode. Die Gradientenmethode ist jedoch von einem Fehler abhängig. Diesen Fehler zu bestimmen und durch das Netz zurück zu führen, das ist die Backpropagation.


Um die Gewichte entgegen des Fehlers anpassen zu können, benötigen wir einen möglichst exakten Fehler als Eingabe. Der Fehler berechnet sich an der Ausgabeschicht über eine Fehlerfunktion (Loss Function), beispielsweise über den MSE (Mean Squared Error) oder über die sogenannte Kreuzentropie (Cross Entropy). Lassen wir es in diesem Beispiel einfach bei einem simplen Vergleich zwischen dem realen Wert (Sollwert o_{real}) und der Prädiktion (Ausgabe o) bleiben:

e_{o} = o_{real} - o

Der Fehler e ist also einfach der Unterschied zwischen dem Ziel-Wert und der Prädiktion. Jedes Training ist eine Wiederholung von Prädiktion (Forward) und Gewichtsanpassung (Back). Im ersten Schritt werden üblicherweise die Gewichtungen zufällig gesetzt, jede Gewichtung unterschiedlich nach Zufallszahl. So ist die Wahrscheinlichkeit, gleich zu Beginn die “richtigen” Gewichtungen gefunden zu haben auch bei kleinen neuronalen Netzen verschwindend gering. Der Fehler wird also groß sein und kann über den Gradientenabstieg durch Gewichtsanpassung verkleinert werden.

In diesem Beispiel berechnen wir die Fehler e_{1} und e_{2} und passen danach die Gewichte w_{j,k} (w_{1,1} & w_{2,1} und w_{1,2} & w_{2,2}) der Schicht zwischen dem Hidden-Layer N und dem Output-Layer o an.

Abbildung 3: Anpassung der Gewichtungen basierend auf dem Fehler in der Ausgabe-Schicht.

Die Frage ist nun, wie die Gewichte zwischen dem Input-Layer X und dem Hidden-Layer N anzupassen sind. Es stellt sich die Frage, welchen Einfluss diese auf die Fehler in der Ausgabe-Schicht haben?

Um diese Gewichtungen anpassen zu können, benötigen wir den Fehler-Anteil der beiden Neuronen N_{1} und N_{2}. Dieser Anteil am Fehler der jeweiligen Neuronen ergibt sich direkt aus den Gewichtungen w_{j,k} zum Output-Layer:

e_{N_{1}} = e_{o1} \cdot \frac{w_{1,1}}{w_{1,1} + w_{1,2}} + e_{o2} \cdot \frac{w_{1,2}}{w_{1,1} + w_{1,2}}

e_{N_{2}} = e_{o1} \cdot \frac{w_{2,1}}{w_{2,1} + w_{2,2}} + e_{o2} \cdot \frac{w_{2,2}}{w_{2,1} + w_{2,2}}

Wenn man das nun generalisiert:

    \[ e_{N} = \left(\begin{array}{rr} \frac{w_{1,1}}{w_{1,1} + w_{1,2}} & \frac{w_{1,2}}{w_{1,1} + w_{1,2}} \\ \frac{w_{2,1}}{w_{2,1} + w_{2,2}} & \frac{w_{2,2}}{w_{2,1} + w_{2,2}} \end{array}\right) \cdot \left(\begin{array}{c} e_{1} \\ e_{2} \end{array}\right) \qquad \]

Dabei ist es recht aufwändig, die Gewichtungen stets ins Verhältnis zu setzen. Diese Berechnung können wir verkürzen, indem ganz einfach direkt nur die Gewichtungen ohne Relativierung zur Kalkulation des Fehleranteils benutzt werden. Die Relationen bleiben dabei erhalten!

    \[ e_{N} = \left(\begin{array}{rr} w_{1,1} & w_{1,2} \\ w_{2,1} & w_{2,2} \end{array}\right) \cdot \left(\begin{array}{c} e_{1} \\ e_{2} \end{array}\right) \qquad \]

Oder folglich in Kurzform: e_{N} = w^{T} \cdot e_{o}

Abbildung 4: Vollständige Gewichtsanpassung auf Basis der Fehler in der Ausgabeschicht und der Fehleranteile in der verborgenden Schicht.

Und nun können, basierend auf den Fehleranteilen der verborgenden Schicht N, die Gewichtungen w_{i,j} zwischen der Eingabe-Schicht I und der verborgenden Schicht N angepasst werden, entgegen dieser Fehler e_{N}.

Die Backpropagation besteht demnach aus zwei Schritten:

  1. Fehler-Berechnung durch Abgleich der Soll-Werte mit den Prädiktionen in der Ausgabeschicht und durch Fehler-Rückführung zu den Neuronen der verborgenden Schichten (Hidden-Layer)
  2. Anpassung der Gewichte entgegen des Gradientenanstiegs der Fehlerfunktion (Loss Function)


Die folgenden zwei Bücher haben mir sehr beim Verständnis und beim Verständlichmachen der Backpropagation in künstlichen neuronalen Netzen geholfen.

Neuronale Netze selbst programmieren: Ein verständlicher Einstieg mit Python Deep Learning. Das umfassende Handbuch: Grundlagen, aktuelle Verfahren und Algorithmen, neue Forschungsansätze (mitp Professional)

Training eines Neurons mit dem Gradientenverfahren

Dies ist Artikel 3 von 6 der Artikelserie –Einstieg in Deep Learning.

Das Training von neuronalen Netzen erfolgt nach der Forward-Propagation über zwei Schritte:

  1. Fehler-Rückführung über aller aktiver Neuronen aller Netz-Schichten, so dass jedes Neuron “seinen” Einfluss auf den Ausgabefehler kennt.
  2. Anpassung der Gewichte entgegen den Gradienten der Fehlerfunktion

Beide Schritte werden in der Regel zusammen als Backpropagation bezeichnet. Machen wir erstmal einen Schritt vor und betrachten wir, wie ein Neuron seine Gewichtsverbindungen zu seinen Vorgängern anpasst.


Der Gradientenabstieg ist ein generalisierbarer Algorithmus zur Optimierung, der in vielen Verfahren des maschinellen Lernens zur Anwendung kommt, jedoch ganz besonders als sogenannte Backpropagation im Deep Learning den Erfolg der künstlichen neuronalen Netze erst möglich machen konnte.

Der Gradientenabstieg lässt sich vom Prinzip her leicht erklären: Angenommen, man stünde im Gebirge im dichten Nebel. Das Tal, und somit der Weg nach Hause, ist vom Nebel verdeckt. Wohin laufen wir? Wir können das Ziel zwar nicht sehen, tasten uns jedoch so heran, dass unser Gehirn den Gradienten (den Unterschied der Höhen beider Füße) berechnet, somit die Steigung des Bodens kennt und sich entgegen dieser Steigung unser Weg fortsetzt.

Konkret funktioniert der Gradientenabstieg so: Wir starten bei einem zufälligen Theta \theta (Random Initialization). Wir berechnen die Ausgabe (Forwardpropogation) und vergleichen sie über eine Verlustfunktion (z. B. über die Funktion Mean Squared Error) mit dem tatsächlich korrekten Wert. Auf Grund der zufälligen Initialisierung haben wir eine nahe zu garantierte Falschheit der Ergebnisse und somit einen Verlust. Für die Verlustfunktion berechnen wir den Gradienten für gegebene Eingabewerte. Voraussetzung dafür ist, dass die Funktion ableitbar ist. Wir bewegen uns entgegen des Gradienten in Richtung Minimum der Verlustfunktion. Ist dieses Minimum (fast) gefunden, spricht man auch davon, dass der Lernalgorithmus konvergiert.

Das Gradientenabstiegsverfahren ist eine Möglichkeit der Gradientenverfahren, denn wollten wir maximieren, würden wir uns entlang des Gradienten bewegen, was in anderen Anwendungen sinnvoll ist.

Ob als “Cost Function” oder als “Loss Function” bezeichnet, in jedem Fall ist es eine “Error Function”, aber auf die Benennung kommen wir später zu sprechen. Jedenfalls versuchen wir die Fehlerrate zu senken! Leider sind diese Funktionen in der Praxis selten so einfach konvex (zwei Berge mit einem Tal dazwischen).


Aber Achtung: Denn befinden wir uns nur zwischen zwei Bergen, finden wir das Tal mit Sicherheit über den Gradienten. Befinden wir uns jedoch in einem richtigen Gebirge mit vielen Bergen und Tälern, gilt es, das richtige Tal zu finden. Bei der Optimierung der Gewichtungen von künstlichen neuronalen Netzen wollen wir die besten Gewichtungen finden, die uns zu den geringsten Ausgaben der Verlustfunktion führen. Wir suchen also das globale Minimum unter den vielen (lokalen) Minima.

Programmier-Beispiel in Python

Nachfolgend ein Beispiel des Gradientenverfahrens zur Berechnung einer Regression. Wir importieren numpy und matplotlib.pyplot und erzeugen uns künstliche Datenpunkte:

Nun wollen wir einen Lernalgorithmus über das Gradientenverfahren erstellen. Im Grunde haben wir hier es bereits mit einem linear aktivierten Neuron zutun:

Bei der linearen Regression, die wir durchführen wollen, nehmen wir zwei-dimensionale Daten (wobei wir die Regression prinzipiell auch mit x-Dimensionen durchführen können, dann hätte unser Neuron weitere Eingänge). Wir empfangen einen Bias (w_0) der stets mit einer Eingangskonstante multipliziert und somit als Wert erhalten bleibt. Der Bias ist das Alpha \alpha in einer Schulmathe-tauglichen Formel wie y = \beta \cdot x + \alpha.

Beta \beta ist die Steigung, der Gradient, der Funktion.

Sowohl \alpha als auch \beta sind uns unbekannt, versuchen wir jedoch über die Betrachtung unserer Prädiktion durch Berechnung der Formel \^y = \beta \cdot x + \alpha und den darauffolgenden Abgleich mit dem tatsächlichen y herauszufinden. Anfangs behaupten wir beispielsweise einfach, sowohl \beta als auch \alpha seien 0.00. Folglich wird \^y = \beta \cdot x + \alpha ebenfalls gleich 0.00 sein und die Fehlerfunktion (Loss Function) wird maximal sein. Dies war der erste Durchlauf des Trainings, die sogenannte erste Epoche!

Die Epochen (Durchläufe) und dazugehörige Fehlergrößen. Wenn die Fehler sinken und mit weiteren Epochen nicht mehr wesentlich besser werden, heißt es, das der Lernalogorithmus konvergiert.

Als Fehlerfunktion verwenden wir bei der Regression die MSE-Funktion (Mean Squared Error):

MSE = \sum(\^y_i - y_i)^2

Um diese Funktion wird sich nun alles drehen, denn diese beschreibt den Fehler und gibt uns auch die Auskunft darüber, ob wie stark und in welche Richtung sie ansteigt, so dass wir uns entgegen der Steigung bewegen können. Wer die Regeln der Ableitung im Kopf hat, weiß, dass die Ableitung der Formel leichter wird, wenn wir sie vorher auf halbe Werte runterskalieren. Da die Proportionen dabei erhalten bleiben und uns quadrierte Fehlerwerte unserem menschlichen Verstand sowieso nicht so viel sagen (unser Gehirn denkt nunmal nicht exponential), stört das nicht:

MSE = \frac{\frac{1}{2} \cdot \sum(\^y_i - y_i)^2}{n}

MSE = \frac{\frac{1}{2} \cdot \sum(w^T \cdot x_i - y_i)^2}{n}

Wenn die Mathematik der partiellen Ableitung (Ableitung einer Funktion nach jedem Gradienten) abhanden gekommen ist, bitte nochmal folgende Regeln nachschlagen, um die nachfolgende Ableitung verstehen zu können:

  • Allgemeine partielle Ableitung
  • Kettenregel

Ableitung der MSD-Funktion nach dem einen Gewicht w bzw. partiell nach jedem vorhandenen w_j:

\frac{\partial}{\partial w_j}MSE = \frac{\partial}{\partial w} \frac{1}{2} \cdot \sum(\^y - y_i)^2

\frac{\partial}{\partial w_j}MSE = \frac{\partial}{\partial w} \frac{1}{2} \cdot \sum(w^T \cdot x_i - y_i)^2

\frac{\partial}{\partial w_j}MSE = \frac{2}{n} \cdot \sum(w^T \cdot x_i - y_i) \cdot x_{ij}

Woher wir das x_{ij} am Ende her haben? Das ergibt sie aus der Kettenregel: Die äußere Funktion wurde abgeleitet, so wurde aus \frac{1}{2} \cdot \sum(w^T \cdot x_i - y_i)^2 dann \frac{2}{n} \cdot \sum(w^T \cdot x_i - y_i). Jedoch muss im Sinne eben dieser Kettenregel auch die innere Funktion abgeleitet werden. Da wir nach w_j ableiten, bleibt nur x_ij erhalten.

Damit können wir arbeiten! So kompliziert ist die Formel nun auch wieder nicht: \frac{2}{n} \cdot \sum(w^T \cdot x_i - y_i) \cdot x_{ij}

Mit dieser Formel können wir unsere Gewichte an den Fehler anpassen: (f\nabla ist der Gradient der Funktion!)

w_j = w_j - \nabla MSE(w_j)

Initialisieren der Gewichtungen

Die Gewichtungen \alpha und \beta müssen anfänglich mit Werten initialisiert werden. In der Regression bietet es sich an, die Gewichte anfänglich mit 0.00 zu initialisieren.

Bei vielen neuronalen Netzen, mit nicht-linearen Aktivierungsfunktionen, ist das jedoch eher ungünstig und zufällige Werte sind initial besser. Gut erprobt sind normal-verteilte Zufallswerte.


Nur eine Kleinigkeit haben wir bisher vergessen: Wir brauchen einen Faktor, mit dem wir anpassen. Hier wäre der Faktor 1. Das ist in der Regel viel zu groß. Dieser Faktor wird geläufig als Lernrate (Learning Rate) \eta (eta) bezeichnet:

w_j = w_j - \eta \cdot \nabla MSE(w_j)

Die Lernrate \eta ist ein Knackpunkt und der erste Parameter des Lernalgorithmus, den es anzupassen gilt, wenn das Training nicht konvergiert.

Die Lernrate \eta darf nicht zu groß klein gewählt werden, da das Training sonst zu viele Epochen benötigt. Ungeduldige erhöhen die Lernrate möglicherweise aber so sehr, dass der Lernalgorithmus im Minimum der Fehlerfunktion vorbeiläuft und diesen stets überspringt. Hier würde der Algorithmus also sozusagen konvergieren, weil nicht mehr besser werden, aber das resultierende Modell wäre weit vom Optimum entfernt.

Beginnen wir mit der Implementierung als Python-Klasse:

Die Klasse sollte so funktionieren, bevor wir sie verwenden, sollten wir die Input-Werte standardisieren:

Bei diesem Beispiel mit künstlich erzeugten Werten ist das Standardisieren bzw. das Fehlen des Standardisierens zwar nicht kritisch, aber man sollte es sich zur Gewohnheit machen. Testweise es einfach mal weglassen 🙂

Kommen wir nun zum Einsatz der Klasse, die die Regression via Gradientenabstieg absolvieren soll:

Was tut diese Instanz der Klasse LinearRegressionGD nun eigentlich?

Bildlich gesprochen, legt sie eine Gerade auf den Boden des Koordinatensystems, denn die Gewichtungen werden mit 0.00 initialisiert, y ist also gleich 0.00, egal welche Werte in x enthalten sind. Der Fehler ist dann aber sehr groß (sollte maximal sein, im Vergleich zu zukünftigen Epochen). Die Gewichte werden also angepasst, die Gerade somit besser in die Punktwolke platziert. Mit jeder Epoche wird die Gerade erneut in die Punktwolke gelegt, der Gesamtfehler (über alle x, da wir es hier mit dem Batch-Verfahren zutun haben) berechnet, die Werte angepasst… bis die vorgegebene Zahl an Epochen abgelaufen ist.

Schauen wir uns das Ergebnis des Trainings an:

Die Linie sieht passend aus, oder? Da wir hier nicht zu sehr in die Theorie der Regressionsanalyse abdriften möchten, lassen wir das testen und prüfen der Akkuratesse mal aus, hier möchte ich auf meinen Artikel Regressionsanalyse in Python mit Scikit-Learn verweisen.

Prüfen sollten wir hingegen mal, wie schnell der Lernalgorithmus mit der vorgegebenen Lernrate eta konvergiert:

Hier die Verlaufskurve der Cost Function:

Die Kurve zeigt uns, dass spätestens nach 40 Epochen kaum noch Verbesserung (im Sinne der Gesamtfehler-Minimierung) erreicht wird.

Wichtige Hinweise

Natürlich war das nun nur ein erster kleiner Einstieg und wer es verstanden hat, hat viel gewonnen. Denn erst dann kann man sich vorstellen, wie ein einzelnen Neuron eines künstlichen neuronalen Netzes grundsätzlich trainiert werden kann.

Folgendes sollte noch beachtet werden:

  • Lernrate \eta:
    Die Lernrate ist ein wichtiger Parameter. Wer das Programmier-Beispiel bei sich zum Laufen gebracht hat, einfach mal die Lernrate auf Werte zwischen 10.00 und 0.00000001 setzen, schauen was passiert 🙂
  • Globale Minima vs lokale Minima:
    Diese lineare zwei-dimensionale Regression ist ziemlich einfach. Neuronale Netze sind hingegen komplexer und haben nicht einfach nur eine simple konvexe Fehlerfunktion. Hier gibt es mehrere Hügel und Täler in der Fehlerfunktion und die Gefahr ist groß, in einem lokalen, nicht aber in einem globalen Minimum zu landen.
  • Stochastisches Gradientenverfahren:
    Wir haben hier das sogenannte Batch-Verfahren verwendet. Dieses ist grundsätzlich besser als die stochastische Methode. Denn beim Batch verwenden wir den gesamten Stapel an x-Werten für die Fehlerbestimmung. Allerdings ist dies bei großen Daten zu rechen- und speicherintensiv. Dann werden kleinere Unter-Stapel (Sub-Batches) zufällig aus den x-Werten ausgewählt, der Fehler daraus bestimmt (was nicht ganz so akkurat ist, wie als würden wir den Fehler über alle x berechnen) und der Gradient bestimmt. Dies ist schon Rechen- und Speicherkapazität, erfordert aber meistens mehr Epochen.


Die folgenden zwei Bücher haben mir bei der Erstellung dieses Beispiels geholfen und kann ich als hilfreiche und deutlich weiterführende Lektüre empfehlen:


Machine Learning mit Python und Scikit-Learn und TensorFlow: Das umfassende Praxis-Handbuch für Data Science, Predictive Analytics und Deep Learning (mitp Professional) Hands-On Machine Learning with Scikit-Learn and TensorFlow: Concepts, Tools, and Techniques for Building Intelligent Systems


Über die Integration symbolischer Inferenz in tiefe neuronale Netze

Tiefe neuronale Netze waren in den letzten Jahren eine enorme Erfolgsgeschichte. Viele Fortschritte im Bereich der KI, wie das Erkennen von Objekten, die fließende Übersetzung natürlicher Sprache oder das Spielen von GO auf Weltklasseniveau, basieren auf tiefen neuronalen Netzen. Über die Grenzen dieses Ansatzes gab es jedoch nur wenige Berichte. Eine dieser Einschränkungen ist die Unfähigkeit, aus einer kleinen Anzahl von Beispielen zu lernen. Tiefe neuronale Netze erfordern in der Regel eine Vielzahl von Trainingsbeispielen, während der Mensch aus nur einem einzigen Beispiel lernen kann. Wenn Sie eine Katze einem Kind zeigen, das noch nie zuvor eine gesehen hat, kann es eine weitere Katze anhand dieser einzigen Instanz erkennen. Tiefe neuronale Netze hingegen benötigen Hunderttausende von Bildern, um zu erlernen, wie eine Katze aussieht. Eine weitere Einschränkung ist die Unfähigkeit, Rückschlüsse aus bereits erlerntem Allgemeinwissen zu ziehen. Beim Lesen eines Textes neigen Menschen dazu, weitreichende Rückschlüsse auf mögliche Interpretationen des Textes zu ziehen. Der Mensch ist dazu in der Lage, weil er Wissen aus sehr unterschiedlichen Bereichen abrufen und auf den Text anwenden kann.

Diese Einschränkungen deuten darauf hin, dass in tiefen neuronalen Netzen noch etwas Grundsätzliches fehlt. Dieses Etwas ist die Fähigkeit, symbolische Bezüge zu Entitäten in der realen Welt herzustellen und sie in Beziehung zueinander zu setzen. Symbolische Inferenz in Form von formaler Logik ist seit Jahrzehnten der Kern der klassischen KI, hat sich jedoch als spröde und komplex in der Anwendung erwiesen. Gibt es dennoch keine Möglichkeit, tiefe neuronale Netze so zu verbessern, dass sie in der Lage sind, symbolische Informationen zu verarbeiten? Tiefe neuronale Netzwerke wurden von biologischen neuronalen Netzwerken wie dem menschlichen Gehirn inspiriert. Im Wesentlichen sind sie ein vereinfachtes Modell der Neuronen und Synapsen, die die Grundbausteine des Gehirns ausmachen. Eine solche Vereinfachung ist, dass statt mit zeitlich begrenzten Aktionspotenzialen nur mit einem Aktivierungswert gearbeitet wird. Aber was ist, wenn es nicht nur wichtig ist, ob ein Neuron aktiviert wird, sondern auch, wann genau. Was wäre, wenn der Zeitpunkt, zu dem ein Neuron feuert, einen relationalen Kontext herstellt, auf den sich diese Aktivierung bezieht? Nehmen wir zum Beispiel ein Neuron, das für ein bestimmtes Wort steht. Wäre es nicht sinnvoll, wenn dieses Neuron jedes Mal ausgelöst würde, wenn das Wort in einem Text erscheint? In diesem Fall würde das Timing der Aktionspotenziale eine wichtige Rolle spielen. Und nicht nur das Timing einer einzelnen Aktivierung, sondern auch das Timing aller eingehenden Aktionspotenziale eines Neurons relativ zueinander wäre wichtig. Dieses zeitliche Muster kann verwendet werden, um eine Beziehung zwischen diesen Eingangsaktivierungen herzustellen. Wenn beispielsweise ein Neuron, das ein bestimmtes Wort repräsentiert, eine Eingabesynapse für jeden Buchstaben in diesem Wort hat, ist es wichtig, dass das Wort Neuron nur dann ausgelöst wird, wenn die Buchstabenneuronen in der richtigen Reihenfolge zueinander abgefeuert wurden. Konzeptionell könnten diese zeitlichen Unterschiede als Relationen zwischen den Eingangssynapsen eines Neurons modelliert werden. Diese Relationen definieren auch den Zeitpunkt, zu dem das Neuron selbst im Verhältnis zu seinen Eingangsaktivierungen feuert. Aus praktischen Gründen kann es sinnvoll sein, der Aktivierung eines Neurons mehrere Slots zuzuordnen, wie z.B. den Anfang und das Ende eines Wortes. Andernfalls müssten Anfang und Ende eines Wortes als zwei getrennte Neuronen modelliert werden. Diese Relationen sind ein sehr mächtiges Konzept. Sie ermöglichen es, die hierarchische Struktur von Texten einfach zu erfassen oder verschiedene Bereiche innerhalb eines Textes miteinander in Beziehung zu setzen. In diesem Fall kann sich ein Neuron auf eine sehr lokale Information beziehen, wie z.B. einen Buchstaben, oder auf eine sehr weitreichende Information, wie z.B. das Thema eines Textes.

Eine weitere Vereinfachung im Hinblick auf biologische neuronale Netze besteht darin, dass mit Hilfe einer Aktivierungsfunktion die Feuerrate eines einzelnen Neurons angenähert wird. Zu diesem Zweck nutzen klassische neuronale Netze die Sigmoidfunktion. Die Sigmoidfunktion ist jedoch symmetrisch bezüglich großer positiver oder negativer Eingangswerte, was es sehr schwierig macht, ausssagenlogische Operationen mit Neuronen mit der Sigmoidfunktion zu modellieren. Spiking-Netzwerke hingegen haben einen klaren Schwellenwert und ignorieren alle Eingangssignale, die unterhalb dieses Schwellenwerts bleiben. Daher ist die ReLU-Funktion oder eine andere asymmetrische Funktion eine deutlich bessere Annäherung für die Feuerrate. Diese Asymmetrie ist auch für Neuronen unerlässlich, die relationale Informationen verarbeiten. Das Neuron, das ein bestimmtes Wort repräsentiert, muss nämlich für alle Zeitpunkte, an denen das Wort nicht vorkommt, völlig inaktiv bleiben.

Ebenfalls vernachlässigt wird in tiefen neuronalen Netzwerken die Tatsache, dass verschiedene Arten von Neuronen in der Großhirnrinde vorkommen. Zwei wichtige Typen sind die bedornte Pyramidenzelle, die in erster Linie eine exzitatorische Charakteristik aufweist, und die nicht bedornte Sternzelle, die eine hemmende aufweist. Die inhibitorischen Neuronen sind besonders, weil sie es ermöglichen, negative Rückkopplungsschleifen aufzubauen. Solche Rückkopplungsschleifen finden sich normalerweise nicht in einem tiefen neuronalen Netzwerk, da sie einen inneren Zustand in das Netzwerk einbringen. Betrachten wir das folgende Netzwerk mit einem hemmenden Neuron und zwei exzitatorischen Neuronen, die zwei verschiedene Bedeutungen des Wortes “August” darstellen.

Beide Bedeutungen schließen sich gegenseitig aus, so dass das Netzwerk nun zwei stabile Zustände aufweist. Diese Zustände können von weiteren Eingangssynapsen der beiden exzitatorischen Neuronen abhängen. Wenn beispielsweise das nächste Wort nach dem Wort ‘August’ ein potenzieller Nachname ist, könnte eine entsprechende Eingabesynapse für das Entitätsneuron August-(Vorname) das Gewicht dieses Zustands erhöhen. Es ist nun wahrscheinlicher, dass das Wort “August” als Vorname und nicht als Monat eingestuft wird. Aber bedenken Sie, dass beide Zustände evaluiert werden müssen. In größeren Netzwerken können viele Neuronen durch negative oder positive Rückkopplungsschleifen verbunden sein, was zu einer großen Anzahl von stabilen Zuständen im Netzwerk führen kann.

Aus diesem Grund ist ein effizienter Optimierungsprozess erforderlich, der den besten Zustand in Bezug auf eine Zielfunktion ermittelt. Diese Zielfunktion könnte darin bestehen, die Notwendigkeit der Unterdrückung stark aktivierter Neuronen zu minimieren. Diese Zustände haben jedoch den enormen Vorteil, dass sie es erlauben, unterschiedliche Interpretationen eines bestimmten Textes zu berücksichtigen. Es ist eine Art Denkprozess, in dem verschiedene Interpretationen bewertet werden und die jeweils stärkste als Ergebnis geliefert wird. Glücklicherweise lässt sich die Suche nach einem optimalen Lösungszustand recht gut optimieren.

Der Grund, warum wir in diesen Rückkopplungsschleifen hemmende Neuronen benötigen, ist, dass sonst alle gegenseitig unterdrückenden Neuronen vollständig miteinander verbunden sein müssten. Das würde zu einer quadratisch zunehmenden Anzahl von Synapsen führen.

Durch die negativen Rückkopplungsschleifen, d.h. durch einfaches Verbinden einer negativen Synapse mit einem ihrer Vorläuferneuronen, haben wir plötzlich den Bereich der nichtmonotonen Logik betreten. Die nichtmonotone Logik ist ein Teilgebiet der formalen Logik, in dem Implikationen nicht nur zu einem Modell hinzugefügt, sondern auch entfernt werden. Es wird davon ausgegangen, dass eine nichtmonotone Logik erforderlich ist, um Schlussfolgerungen für viele Common Sense Aufgaben ziehen zu können. Eines der Hauptprobleme der nichtmonotonen Logik ist, dass sie oft nicht entscheiden kann, welche Schlussfolgerungen sie ziehen soll und welche eben nicht. Einige skeptische oder leichtgläubige Schlussfolgerungen sollten nur gezogen werden, wenn keine anderen Schlussfolgerungen wahrscheinlicher sind. Hier kommt die gewichtete Natur neuronaler Netze zum Tragen. In neuronalen Netzen können nämlich eher wahrscheinliche Zustände weniger wahrscheinliche Zustände unterdrücken.

Beispielimplementierung innerhalb des Aika-Frameworks

An dieser Stelle möchte ich noch einmal das Beispielneuron für das Wort ‘der’ vom Anfang aufgreifen. Das Wort-Neuron besteht aus drei Eingabesynapsen, die sich jeweils auf die einzelnen Buchstaben des Wortes beziehen. Über die Relationen werden die Eingabesynapsen nun zueinander in eine bestimmte Beziehung gesetzt, so dass das Wort ‘der’ nur erkannt wird, wenn alle Buchstaben in der korrekten Reihenfolge auftreten.
Als Aktivierungsfunktion des Neurons wird hier der im negativen Bereich abgeschnittene (rectified) hyperbolische Tangens verwendet. Dieser hat gerade bei einem UND-verknüpfenden Neuron den Vorteil, dass er selbst bei sehr großen Werten der gewichteten Summe auf den Wert 1 begrenzt ist. Alternativ kann auch die ReLU-Funktion (Rectified Linear Unit) verwendet werden. Diese eignet sich insbesondere für ODER-verknüpfende Neuronen, da sie die Eingabewerte unverzerrt weiterleitet.
Im Gegensatz zu herkömmlichen neuronalen Netzen gibt es hier mehrere Bias Werte, einen für das gesamte Neuron (in diesem Fall auf 5.0 gesetzt) und einen für jede Synapse. Intern werden diese Werte zu einem gemeinsamen Bias aufsummiert. Es ist schon klar, dass dieses Aufteilen des Bias nicht wirklich gut zu Lernregeln wie der Delta-Rule und dem Backpropagation passt, allerdings eignen sich diese Lernverfahren eh nur sehr begrenzt für diese Art von neuronalem Netzwerk. Als Lernverfahren kommen eher von den natürlichen Mechanismen Langzeit-Potenzierung und Langzeit-Depression inspirierte Ansätze in Betracht.


Obwohl tiefe neuronale Netze bereits einen langen Weg zurückgelegt haben und mittlerweile beeindruckende Ergebnisse liefern, kann es sich doch lohnen, einen weiteren Blick auf das Original, das menschliche Gehirn und seine Schaltkreise zu werfen. Wenn eine so inhärent komplexe Struktur wie das menschliche Gehirn als Blaupause für ein neuronales Modell verwendet werden soll, müssen vereinfachende Annahmen getroffen werden. Allerdings ist bei diesem Prozess Vorsicht geboten, da sonst wichtige Aspekte des Originals verloren gehen können.


  1. Der Aika-Algorithm
    Lukas Molzberger
  2. Neuroscience: Exploring the Brain
    Mark F. Bear, Barry W. Connors, Michael A. Paradiso
  3. Neural-Symbolic Learning and Reasoning: A Survey and Interpretation
    Tarek R. Besold, Artur d’Avila Garcez, Sebastian Bader; Howard Bowman, Pedro Domingos, Pascal Hitzler, Kai-Uwe Kuehnberger, Luis C. Lamb, ; Daniel Lowd, Priscila Machado Vieira Lima, Leo de Penning, Gadi Pinkas, Hoifung Poon, Gerson Zaverucha
  4. Deep Learning: A Critical Appraisal
    Gary Marcus
  5. Nonmonotonic Reasoning
    Gerhard Brewka, Ilkka Niemela, Mirosław Truszczynski

Warning: file_get_contents( failed to open stream: Connection timed out in /var/www/datasciencehack/wp-content/plugins/nbconvert-master/nbconvert.php on line 38

IIIb. Einführung in TensorFlow: Realisierung eines Perzeptrons mit TensorFlow

In [1]:
import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

# Reset des TensorFlows

Daten laden und eigene Definitionen

In [2]:
data = pd.read_csv('data_train.csv')
input_X = data[['x0', 'x1']]
input_y = data.y

data_test = pd.read_csv('data_test.csv')
test_X = data_test[['x0', 'x1']]
test_y = data_test.y

Damit unser Modell schneller lernt, teilen wir unseren Datensatz in Stapel ein. Dafür erstellen wir eine Funktion, welche unseren Datensatz in Stapel teilt!

Je nach Datensatz und Modell empfehlt sich eine andere Stapelgröße.

In [3]:
def stapel_erstellen(X, Y, stapel_grosse, p_index):
    return X[stapel_grosse * p_index: stapel_grosse * (p_index + 1)], Y[stapel_grosse * p_index: stapel_grosse * (p_index + 1)]

Erstellen des Graphen

Formen der Tensoren

In [4]:
# Anzahl der Ergebnissspalten
anz_unit = 1
# Anzahl der Eingänge bzw. Merkmale 
anz_ein = 2
# Anzahl der Ausgänge
anz_aus = 1

Parameter zur Steuerung des Graphen

Die richtige Wahl der Parameter zur Steuerung des Graphen sind entscheidend, wenn es darum geht, wie schnell ein Modell lernt. Wenn wir zum Beispiel anz_stapel=10 statt anz_stapel=5 nutzen, dann brauch unser Modell länger um eine Genauigkeit von 100 % zu erreichen, wenn überhaupt.

In [5]:
# Lernrate
eta = 0.1
# Anzahl der der Pakete mit den zu analysierenden Datenwerte
anz_stapel = 5
# Anzahl der zu analysierenden Datenwerte
stapel_grosse = int(len(input_X)/anz_stapel)
# Anzahl der Wiederholungen
epochen = 50

Relevante Größen

In [6]:
# Eingangssignal
x = tf.placeholder(tf.float32, shape=[None, anz_ein],name='Input')  # Stapelgröße(k) x 2
# Ausgangssignal
y_true = tf.placeholder(tf.float32, shape=None, name='Labels')  # Stapelgröße(k) x 1
# Gewichte
w = tf.Variable(tf.random_normal([anz_ein, anz_unit]), name='Weights')  # 2x1


In der Theorie sind wir immer nur einen Datenpunkt in Betracht gezogen. In TensorFlow wollen wir jedoch einen Stapel betrachten. Dadurch ändert sich die Berechnung ein wenig. Wir berechnen für alle Punkte eine Fehlerfunktion. Der Mittelwert aller Fehlerfunktionen, die Kostenfunktion, soll dann optimiert werden.

In [7]:
# z = xw
z = tf.matmul(x, w, name='Z')
# H = y * -log(sigmoid(z)) + (1 - y) * -log(1 - sigmoid(z)) -> Kreuzentropie
err = tf.reduce_mean(
    tf.nn.sigmoid_cross_entropy_with_logits(labels=y_true, logits=z),name='Costfunction')
# Minimieren der Fehlerfunktion
opt = tf.train.GradientDescentOptimizer(learning_rate=eta).minimize(err)

# Berechnung der Genauigkeit
eins = tf.reshape(tf.round(tf.sigmoid(z)),[len(test_X), 1])
zwei = tf.reshape(y_true,[len(test_X), 1])
acc = tf.equal(eins, zwei)
acc = tf.reduce_mean(tf.cast(acc, tf.float32), name='Accuracy')

Ausführung des Graphen

Bei der Ausführung ist es wichtig, dass wir die Variablen initialisieren. Auch ist es vorteilhaft, wenn wir die Session mit with starten.

In [8]:
# Größen zur späteren Datenvisualisierung
W_set = []
Err_set = []
Acc_set = []
# Initialisierung der Variablen
init = tf.global_variables_initializer()
# Ausführung des Graphen
with tf.Session() as sess:
    # Wichtig für TensorBoard
    writer = tf.summary.FileWriter('./graphs/perceptron', sess.graph)
    # Schleife für Epoche
    for e in range(epochen):
        # Schleife für Stapel
        for i in range(anz_stapel):
            # Einteilen unserer Daten in Stapel
            stapel_x, stapel_y = stapel_erstellen(X=input_X,
            # Ausführung der Berechnung
            Z, W, _, Err =[z, w, opt, err],
                                    feed_dict={x: stapel_x, y_true: stapel_y})

        # Datenspeicherung für Visualisierung über die Epochen
        Acc =[acc],
                       feed_dict={x: test_X, y_true: test_y})
        print('{:}. Epoche Genauigkeit: {:.2f} %'.format(e, Acc[0]*100))
0. Epoche Genauigkeit: 50.75 %
1. Epoche Genauigkeit: 65.00 %
2. Epoche Genauigkeit: 80.75 %
3. Epoche Genauigkeit: 93.00 %
4. Epoche Genauigkeit: 97.75 %
5. Epoche Genauigkeit: 98.75 %
6. Epoche Genauigkeit: 99.75 %
7. Epoche Genauigkeit: 100.00 %
8. Epoche Genauigkeit: 100.00 %
9. Epoche Genauigkeit: 100.00 %
10. Epoche Genauigkeit: 100.00 %
11. Epoche Genauigkeit: 100.00 %
12. Epoche Genauigkeit: 100.00 %
13. Epoche Genauigkeit: 100.00 %
14. Epoche Genauigkeit: 100.00 %
15. Epoche Genauigkeit: 100.00 %
16. Epoche Genauigkeit: 100.00 %
17. Epoche Genauigkeit: 100.00 %
18. Epoche Genauigkeit: 100.00 %
19. Epoche Genauigkeit: 100.00 %
20. Epoche Genauigkeit: 100.00 %
21. Epoche Genauigkeit: 100.00 %
22. Epoche Genauigkeit: 100.00 %
23. Epoche Genauigkeit: 100.00 %
24. Epoche Genauigkeit: 100.00 %
25. Epoche Genauigkeit: 100.00 %
26. Epoche Genauigkeit: 100.00 %
27. Epoche Genauigkeit: 100.00 %
28. Epoche Genauigkeit: 100.00 %
29. Epoche Genauigkeit: 100.00 %
30. Epoche Genauigkeit: 100.00 %
31. Epoche Genauigkeit: 100.00 %
32. Epoche Genauigkeit: 100.00 %
33. Epoche Genauigkeit: 100.00 %
34. Epoche Genauigkeit: 100.00 %
35. Epoche Genauigkeit: 100.00 %
36. Epoche Genauigkeit: 100.00 %
37. Epoche Genauigkeit: 100.00 %
38. Epoche Genauigkeit: 100.00 %
39. Epoche Genauigkeit: 100.00 %
40. Epoche Genauigkeit: 100.00 %
41. Epoche Genauigkeit: 100.00 %
42. Epoche Genauigkeit: 100.00 %
43. Epoche Genauigkeit: 100.00 %
44. Epoche Genauigkeit: 100.00 %
45. Epoche Genauigkeit: 100.00 %
46. Epoche Genauigkeit: 100.00 %
47. Epoche Genauigkeit: 100.00 %
48. Epoche Genauigkeit: 100.00 %
49. Epoche Genauigkeit: 100.00 %
In [9]:
w_0, w_1 = zip(*W_set)
fig, ax = plt.subplots(3,1, figsize=(15,30), sharex='all')
ax[0].plot(range(len(W_set)), w_0, label='w0')
ax[0].plot(range(len(W_set)), w_1, label='w1')

ax[1].plot(range(len(W_set)), Err_set, c='r', label='err')

ax[2].plot(range(len(W_set)), Acc_set, c='g', label='acc')


Nun haben wir unser Perzeptron erfolgreich mit TensorFlow realisiert. Um ein Gefühl zu bekommen, könnt ihr gerne mit den "Parameter zur Steuerung des Graphen" herumexperimentieren. Je nach Auswahl der Parameter ändert sich die Optimierung und sogar die Genauigkeit unseres Modells. Bei so einfachen Daten, sollte unser Modell definitiv 100% Genauigkeit erreichen. Dies ist jedoch nur möglich, wenn wir die richtigen Parameter wählen. Probiert es also einfach mal aus.

PS: Wenn ihr die Trainings- und Testdaten sucht, dann werdet ihr auf Github fündig.

IIIa. Einführung in TensorFlow: Realisierung eines Perzeptrons mit TensorFlow

1. Einleitung

1.1. Was haben wir vor?

Im zweiten Artikel dieser Serie sind wir darauf eingegangen, wie man TensorFlow prinzipiell nutzt. Wir wollen das Gelernte an einem einfachen Modell anwenden. Bevor wir dies jedoch tun, müssen wir die Theorie hinter dem Modell verstehen um TensorFlow richtig anwenden zu können.

Dafür bietet sich ein Adaline-Perzeptron sehr gut an. Es ist ein einfaches Modell mit nur einer Schicht, wo die Theorie verständlich ist.

1.2. Aufgabenstellung

Abb.1 Trainingsdaten: Grün \rightarrow Label 0, Rot
\rightarrow Label 1

In Abb.1 sehen wir unsere Trainingsdaten, die
zufällig generiert wurden. Alle grün markierten Datenpunkte haben das Label 0 und die rot markierten Punkte erhalten das Label 1. 

Wir möchten einen Adaline-Perzeptron entwickeln, der unsere Daten  je nach Position in die richtige Klasse zuordnet. Somit haben wir eine Aufgabe mit binärer Klassifikation

2. Grundlagen

2.1. Funktionsweise eines Perzeptrons

Ein Perzeptron ist ein mathematisches Modell, welches eine Nervenzelle beschreiben soll.

Abb.2 Schematische Darstellung einer Nervenzelle und ihren Bestandteilen

Vereinfacht funktioniert eine Nervenzelle, auch Neuron genannt, folgendermaßen: Eine Vielzahl von Reizen bzw. Eingabesignalen wird von den Dendriten aufgenommen, die dann im Kern verarbeitet werden. Wenn die verschiedenen Eingabesignale die ’richtige’ Dosis an Reizen erreichen und einen Schwellwert erreichen, dann feuert das Neuron ab und leitet ein Signal weiter. 

Für eine detaillierte Beschreibung, wie ein Perzeptron mathematisch beschrieben wird, möchte ich auf diesen Artikel hinweisen.

Wir wollen uns in diesem Artikel auf den Adaline-Algorithmus (ADAptive LINear Element) konzentrieren. Dieser ist eine Weiterentwicklung des Perzeptron. Die Besonderheit an diesem Algorithmus liegt darin, dass das Konzept der Fehlerminimierung durch Minimierung der Straffunktion der berechneten und der tatsächlichen Ergebnisse enthält. Ein weiter wesentlicher Unterschied zu einem einfachen Perzeptron ist vor allem, dass wir bei Adaline keine einfache Sprungfunktion als Aktivierungsfunktion haben, sondern eine stetige Funktion nutzen und somit eine Differenzierung/Ableitung der Aktivierungsfunktion durchführen können. Dieser Punkt ist für die Optimierung der Gewichte und des Lernens unseres Modells ein entscheidender Vorteil.

Das Schema in Abb.3 zeigt uns die Funktionsweise, wie unser Adaline-Algorithmus funktionieren soll.

Abb.3 Schematische Darstellung des Adaline-Perzeptrons

  1. Eingang: In dieser Schicht werden unsere Daten ein gepfangen und weitergeleitet
  2. Die Gewichte geben an, welchen Einfluss unsere Eingangssignale haben. Sie sind auch unsere Größe, die in unserem Algorithmus optimiert werden.
  3. Die Nettoeingabefunktion wird durch die Zusammenführung von Eingangssignalen und Gewichten erzeugt. Je nachdem wie die Eingänge und Gewichte verbunden sind,  müssen diese mathematisch korrekt multipliziert werden.
  4. Die Nettoeingabe wird dann, in die Aktivierungsfunktion eingebunden. Je nachdem welche Aktivierungsfunktion man nutzt, ändert sich die Ausgabe nach der Aktivierungsfunktion. 
  5.  In der Fehlerrückgabe werden die vorhergesagten Ausgaben mit den tatsächlichen Werten/Labels verglichen. Auch hier gibt es verschiedene Verfahren, um eine Fehlerfunktion zu bilden. 
  6. In der Optimierung werden dann auf Basis der Fehlerfunktion die Gewichte so optimiert, dass der Fehler zwischen unseren Label und den vorhergesagten Werten minimiert wird.
  7. Der Quantisierer ist ein optionales Element. Bei einer kategorischen Problemstellung bekommen wir nach der Aktivierungsfunktion eine Wahrscheinlichkeit zu der die Daten zu welchem Label zugeteilt werden. Der Quantisierer wandelt diese Wahrscheinlichkeiten zu Labeln um. Zum Beispiel haben wir einen Datensatz und unser Modell sagt voraus, dass dieser Datensatz zu 88 % das Label 1 hat. Je nachdem welche Grenze dem Quantisierer gegeben wird, teilt dieser dann den Datensatz in die entsprechende Klasse ein. Wenn wir sagen die Grenze soll 50% sein, dann sagt der Quantisierer, dass unser Datensatz Label 1 ist.

2.2. Aktivierungsfunktionen

Die Aktivierungsfunktion ist ein sehr wichtiger Bestandteil bei neuronalen Netzen. Diese bestimmen, wie sich das Ausgangssignal verhält. Es gibt eine Vielzahl von Aktivierungsfunktionen, die ihre Vor- und Nachteile haben. Wir wollen uns erstmal auf die Sigmoidfunktion konzentrieren.

Eigentlich haben wir bei der Sprungfunktion alles was wir brauchen. Wenn wir einen Schwellenwert erreichen z \geq 0, dann feuert die Sprungfunktion und das sehr abrupt. Die Sigmoidfunktion hingegen hat einen sanfteren und natürlicheren Verlauf als die Sprungfunktion. Außerdem ist sie eine stetig und differenzierbare Funktion, was sehr vorteilhaft für das Gradientenverfahren (Optimierung) ist. Daher wollen wir die Sigmoidfunktion für unsere Problemstellung nutzen.

    \begin{align*} \text{sig}(z) = \frac{1}{1 + e^{-z}}\end{align*}

Abb.4 Sigmoid-Funktion mit ihrer Ableitung und deren Sättigungsbereichen

2.3. Optimierungsverfahren

2.3.1. Fehlerfunktion

Die wohl am häufigsten genutzten Fehlerfunktionen (oder auch Ziel-, Kosten-, Verlust-, Straffunktion) sind wohl der mittlere quadratische Fehler bei Regressionen und die Kreuzentropie bei kategorischen Daten.

In unserem Beispiel haben wir Daten kategorischer Natur und eine binäre Thematik, weshalb wir uns auf die Kreuzentropie in Kombination mit der Sigmoidfunktion konzentrieren wollen.

Aus der Matrizenrechnung t (z =\boldsymbol{xw}^T) erhalten wir ein Skalar (eindimensional). Geben wir diese in die Sigmoidfunktion ein, kommen wir auf folgende Gleichung.

    \begin{align*} \text{sig}(z=\boldsymbol{xw}^T) = \frac{1}{1 + e^{-\boldsymbol{xw}^T}} \end{align*}

Hinweis: Wie in Abb.4 kann die Sigmoidfunktion nur Werte zwischen 0 und 1 erreichen, ohne diese jemals zu erreichen. Außerdem ändert sich die Funktion bei sehr großen Beträgen nur noch minimal, man spricht auch von Sättigung. Dieser Fakt ist sehr wichtig, wenn um die Optimierung der Gewichte geht. Wenn wir unsere Nettoeingabe nicht skalieren, dann kann es passieren, dass unser Modell sehr langsam lernt, da der Gradient der Sigmoidfunktion bei großen Beträgen sehr klein ist.

Bei Aufgaben mit binärer Klassifizierung hat sich die Kreuzentropie als Fehlerfunktion etabliert. Sie ist ein Maß für die Qualität eines Modells, welche eine Wahrscheinlichkeitsverteilung angibt. Je kleiner diese Größe ist, desto besser unser Modell. Es gilt also unsere Fehlerfunktion zu minimieren!

Wir wollen in einem separaten Artikel genauer auf die Kreuzentropie eingehen. Für den jetzigen Zeitpunkt soll es reichen, wenn wir die Formel vor Augen haben und was sie grob bedeutet.

P = \{p_1,p_2,\dots,p_N\} sei die ‘wahre’ Wahrscheinlichkeitsverteilung aus der Menge X = \{x_1,x_2,\dots,x_N\}, in unserem Fall, die Wahrscheinlichkeitsverteilung, ob ein Datenpunkt dem Label 0 oder 1 zugehört. Wenn wir nun unser Eingangssignal durch die Aktivierungsfunktion fließen lassen, dann erhalten wir ebenfalls eine ‘berechnete’ Wahrscheinlichkeitsverteilung die Q = \{q_1,q_2,\dots,q_N\} genannt werden soll. Um die Wahrscheinlichkeitsverteilungen p und q zu vergleichen, nutzen wir die Kreuzentropie, welche wie folgt für diskrete Daten definiert ist:

    \begin{align*}\log_2{x}&= \operatorname{ld}(x) \\H(P;Q) &= - \sum{P \cdot \operatorname{ld}(Q)}\\H(P;Q) &= -p_1 \operatorname{ld}(q_1) - p_2  \operatorname{ld}(q_2)\end{align*}

Beispiel einer binären Problemstellung. Wir haben unsere Label 0 und 1. p1 ist die Wahrscheinlichkeit, inwiefern unser Datenpunkt das Label 0 hat. Da wir die Trainingsdaten kennen, wissen wir auch das dieser Punkt zu 100 %, welches Label hat. Unser Modell hat zum Beispiel im ersten Durchgang eine Wahrscheinlichkeit von 0.8 und später 0.9 berechnet.

Fall I : P = Q Die Wahrscheinlichkeitsverteilungen P und Q sind identisch:

    \begin{align*}P &= \{p_1 = 1.0, p_2 = 0.0 \} \\Q_0 &= \{q_1 = 1.0, q_2 = 0.0 \} \\ \\H_{0}(P;Q_I) &= -1.0 \operatorname{ld}(1) -0.0 \operatorname{ld}(0.0) = 0.0\\\end{align*}

Fall II: P \neq Q Die Wahrscheinlichkeitsverteilungen P und Q sind nicht identisch:

    \begin{align*}P &= \{p_1 = 1.0, p_2 = 0.0 \} \\Q_{1} &= \{q_1 = 0.8, q_2 = 0.2 \} \\ Q_{2} &= \{q_1 = 0.9, q_2 = 0.1 \} \\ Q_{3} &= \{q_1 = 0.99, q_2 = 0.01 \} \\ \\H_{1}(P;Q_{1}) &= -1.0 \operatorname{ld}(0.8) -0.0 \operatorname{ld}(0.2) = 0.3219 \\H_{2}(P;Q_{2}) &= -1.0 \operatorname{ld}(0.9) -0.0 \operatorname{ld}(0.1) = 0.1520 \\ H_{3}(P;Q_{3}) &= -1.0 \operatorname{ld}(0.99) -0.0 \operatorname{ld}(0.01) = 0.0144\\\end{align*}

In der oberen Berechnung haben wir zum einfachen Verständnis der Kreuzentropie ein einfaches Beispiel. p_1 ist eine 100 % ige  Wahrscheinlichkeit, dass zum Beispiel unser Datensatz das Label 0 hat. Unser perfektes Modell mit Q_0 hat eine Kreuzentropie-Wert von 0. Unser zweites Modell  H_1(P;Q1) hat eine gewisse Unbestimmtheit, die sich durch eine größere Kreuzentropie H_1 = 0.1520 bemerkbar macht. Je mehr sich also unser Modell von den wirklichen Daten abweicht, desto größer ist die Kreuzentropie.

2.3.2. Optimierung nach dem Gradientenverfahren

Wenn wir es also schaffen die Kreuzentropie zu minimieren, dann erhalten wir auch ein besseres Modell! Bei der Optimierung nach dem Gradientenverfahren versuchen wir uns schrittweise an das Minimum zu bewegen.

    \begin{align*}H(P;Q) &= H(y; \varPhi(z)) \\            &= H(y; \text{sig}(z))\\             &= H(y; \text{sig}(xw))\\H' &= \frac{\partial H}{\partial w} \rightarrow Min.\end{align*}

Ziel der Optimierung ist es, dass unsere Gewichte so angepasst werden, dass sich der Fehler in unserer Fehlerfunktion minimiert. Wir leiten also die Fehlerfunktion nach w ab. 

Diese Aufgabe wird zum Glück von TensorFlow übernommen und wir müssen die Randbedingungen nur dem System geben.

Neben dem Gradientenverfahren, gibt es auch noch eine Menge anderer Optimierer, auf die wir später nochmal eingehen werden.

3. Zusammenfassung

Bevor wir TensorFlow nutzen, ist es wichtig, dass wir unser Modell verstehen. TensorFlow ist wie vieles nur ein Werkzeug, wenn man die Grundlagen nicht verstanden hat. Daher haben wir uns in diesem Artikel erstmal auf die Theorie konzentriert und ich habe dabei versucht mich auf das Wesentliche zu beschränken. 

Im nächsten Artikel werden wir dann unser Modell in TensorFlow realisieren.

PS: In einem separaten Artikel wollen später nochmal detaillierter auf Aktivierungsfunktion, Kreuzentropie und das Gradientenverfahren eingehen.

Warning: file_get_contents( failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in /var/www/datasciencehack/wp-content/plugins/nbconvert-master/nbconvert.php on line 87

Warning: DOMDocument::loadHTML(): Empty string supplied as input in /var/www/datasciencehack/wp-content/plugins/nbconvert-master/nbconvert.php on line 121

Warning: file_get_contents( failed to open stream: HTTP request failed! HTTP/1.1 404 Not Found in /var/www/datasciencehack/wp-content/plugins/nbconvert-master/nbconvert.php on line 38

Sentiment Analysis of IMDB reviews


Download the Data Sources

The data sources used in this article can be downloaded here:

Dem Wettbewerb voraus mit Künstlicher Intelligenz

Was KI schon heute kann und was bis 2020 auf deutsche Unternehmen zukommt

Künstliche Intelligenz ist für die Menschheit wichtiger als die Erfindung von Elektrizität oder die Beherrschung des Feuers – davon sind der Google-CEO Sundar Pichai und viele weitere Experten überzeugt. Doch was steckt wirklich dahinter? Welche Anwendungsfälle funktionieren schon heute? Und was kommt bis 2020 auf deutsche Unternehmen zu?

Big Data war das Buzzword der vergangenen Jahre und war – trotz mittlerweile etablierter Tools wie SAP Hana, Hadoop und weitere – betriebswirtschaftlich zum Scheitern verurteilt. Denn Big Data ist ein passiver Begriff und löst keinesfalls alltägliche Probleme in den Unternehmen.

Dabei wird völlig verkannt, dass Big Data die Vorstufe für den eigentlichen Problemlöser ist, der gemeinhin als Künstliche Intelligenz (KI) bezeichnet wird. KI ist ein Buzzword, dessen langfristiger Erfolg und Aktivismus selbst von skeptischen Experten nicht infrage gestellt wird. Daten-Ingenieure sprechen im Kontext von KI hier aktuell bevorzugt von Deep Learning; wissenschaftlich betrachtet ein Teilgebiet der KI.

Was KI schon heute kann

Deep Learning Algorithmen laufen bereits heute in Nischen-Anwendungen produktiv, beispielsweise im Bereich der Chatbots oder bei der Suche nach Informationen. Sie übernehmen ferner das Rating für die Kreditwürdigkeit und sperren Finanzkonten, wenn sie erlernte Betrugsmuster erkennen. Im Handel findet Deep Learning bereits die optimalen Einkaufsparameter sowie den besten Verkaufspreis.

Getrieben wird Deep Learning insbesondere durch prestigeträchtige Vorhaben wie das autonome Fahren, dabei werden die vielfältigen Anwendungen im Geschäftsbereich oft vergessen.

Die Grenzen von Deep Learning

Und Big Data ist das Futter für Deep Learning. Daraus resultiert auch die Grenze des Möglichen, denn für strategische Entscheidungen eignet sich KI bestenfalls für das Vorbereitung einer Datengrundlage, aus denen menschliche Entscheider eine Strategie entwickeln. KI wird zumindest in dieser Dekade nur auf operativer Ebene Entscheidungen treffen können, insbesondere in der Disposition, Instandhaltung, Logistik und im Handel auch im Vertrieb – anfänglich jeweils vor allem als Assistenzsystem für die Menschen.

Genau wie das autonome Fahren mit Assistenzsystemen beginnt, wird auch im Unternehmen immer mehr die KI das Steuer übernehmen.

Was sich hinsichtlich KI bis 2020 tun wird

Derzeit stehen wir erst am Anfang der Möglichkeiten, die Künstliche Intelligenz uns bietet. Das Markt-Wachstum für KI-Systeme und auch die Anwendungen erfolgt exponentiell. Entsprechend wird sich auch die Arbeitsweise für KI-Entwickler ändern müssen. Mit etablierten Deep Learning Frameworks, die mehrheitlich aus dem Silicon Valley stammen, zeichnet sich der Trend ab, der für die Zukunft noch weiter professionalisiert werden wird: KI-Frameworks werden Enterprise-fähig und Distributionen dieser Plattformen werden es ermöglichen, dass KI-Anwendungen als universelle Kernintelligenz für das operative Geschäft für fast alle Unternehmen binnen weniger Monate implementierbar sein werden.

Wir können bis 2020 also mit einer Alexa oder Cortana für das Unternehmen rechnen, die Unternehmensprozesse optimiert, Risiken berichtet und alle alltäglichen Fragen des Geschäftsführers beantwortet – in menschlich-verbal formulierten Sätzen.

Der Einsatz von Künstlicher Intelligenz zur Auswertung von Geschäfts- oder Maschinendaten ist auch das Leit-Thema der zweitägigen Data Leader Days 2018 in Berlin. Am 14. November 2018 sprechen renommierte Data Leader über Anwendungsfälle, Erfolge und Chancen mit Geschäfts- und Finanzdaten. Der 15. November 2018 konzentriert sich auf Automotive- und Maschinendaten mit hochrangigen Anwendern aus der produzierenden Industrie und der Automobilzuliefererindustrie. Seien Sie dabei und nutzen Sie die Chance, sich mit führenden KI-Anwendern auszutauschen.

II. Einführung in TensorFlow: Grundverständnis für TensorFlow

o. Installation von TensorFlow

Bevor wir richtig durchstarten können, müssen wir natürlich TensorFlow erstmal installieren. Auf dieser Seite findet ihr eine ausführliche Anleitung, wie man TensorFlow auf allen möglichen Systemen installiert. Die nächsten Schritte beschränken sich auf die Installation auf Windows.

o.1.  Installation mit pip

Um TensorFlow zu nutzen, müssen wir diesen Framework auch erstmal installieren. Am einfachsten ist die Installation, wenn ihr Python in reiner Form auf euren Rechner habt. Dann ist es vollkommen ausreichend, wenn ihr folgenden Befehl in eure Eingabeaufforderung(Windows: cmd) eingebt:

Stellt bei dieser Installation sicher, dass ihr keine ältere Version von Python habt als 3.5.x. Außerdem ist es erforderlich, dass ihr pip installiert habt und Python bei euch in der PATH-Umgebung eingetragen ist.Besitzt ihr eine NVIDIA® Grafikkarte so könnt ihr TensorFlow mit GPU Support nutzen. Dazu gebt ihr statt des oben gezeigten Befehls folgendes ein:

o.2. Installation mit Anaconda

Ein wenig aufwendiger wird es, wenn ihr die beliebte Anaconda Distribution nutzt, weil wir da eine Anaconda Umgebung einrichten müssen. Auch hier müssen wir wieder in den Terminal bzw. in die Eingabeaufforderung und folgenden Befehl eingeben:

Tauscht das mit eurer genutzten Version aus.(= 5, 6) Danach aktiviert ihr die erstellte Umgebung:

Nun installieren wir TensorFlow in unsere erstellte Umgebung. Ohne GPU Support

mit GPU Support

Es sei erwähnt, dass das Conda package nur von der Community unterstützt wird, jedoch nicht offiziell seitens Google.

o.3.  Validierung der Installation

Der einfachste Weg um zu überprüfen ob unsere Installation gefruchtet hat und funktioniert können wir anhand eines einfachen Beispiels testen. Dazu gehen wir wieder in den/die Terminal/Eingabeaufforderung und rufen python auf, indem wir python eingeben.


1. Grundverständnis für TensorFlow

1.1. Datenstrom-orientierte Programmierung

In diesem Artikel wollen wir näher auf die Funktionsweise von TensorFlow eingehen. Wie wir aus dem ersten Artikel dieser Serie wissen, nutzt TensorFlow das datenstrom-orientierte Paradigma. In diesem wird ein Datenfluss-Berechnungsgraph erstellt, welcher aus Knoten und Kanten besteht. Ein  Datenfluss-Berechnungsgraph, Datenflussgraph oder auch Berechnungsgraph kann mehrere Knoten haben, die wiederum durch die Kanten verbunden sind. In TensorFlow steht jeder Knoten für eine Operation, die Auswirkungen auf eingehende Daten haben.

Abb.1: Knoten und Kanten: Das Eingangssignal wird durch Kanten in den Knoten eingespeist, verändert und ausgegeben

Abb. 1.5: Achterbahn mit fehlender Verbindung [Quelle]

Analogie-Beispiel: Stellt euch vor ihr seid in einem Freizeitpark und habt Lust eine Achterbahn zu fahren. Am Anfang seid ihr vielleicht ein wenig nervös, aber euch geht es noch sehr gut. Wie jeder von euch weiß, hat eine Achterbahn verschiedene Fahrelemente eingebaut, die unsere Emotionen triggern und bei manchen vielleicht sogar auf den Magen schlagen. Diese Elemente sind äquivalent unsere Knoten. Natürlich müssen diese Elemente auch verbunden sein, sonst wäre eine Fahrt mit dieser Achterbahn in meinen Augen nicht empfehlenswert. Diese Verbindungsstücke sind unsere Kanten und somit sind wir die Daten/Signale, die von Knoten zu Knoten durch die Kanten weitergeleitet werden. Schauen wir uns Abb. 2 an, in der eine schematische Darstellung einer fiktiven Achterbahn zu sehen ist, welche mit 4 Fahrelementen dienen kann.

Abb. 2: Oben: Schematische Darstellung eines Datenflussgraphen anhand unserer fiktiven Achterbahn Unten: Unsere fiktive Achterbahn

  1. Airtime-Hügel: Ein Airtime-Hügel erzeugt bei der Überfahrt Schwerelosigkeit und in manchen Fällen ein Abheben aus dem Sitz. Ein guter Einstieg für die Mitfahrer, wie ich finde.
  2. Klassischer Looping: Wir kennen ihn alle, den Looping. Mit hoher Geschwindigkeit geht es in einen vertikalen Kreis hinein und man sich am höchsten Punkt kopfüber befindet.  Für Leute mit nicht so starken Nerven fragen sich spätestens jetzt, warum sie überhaupt mitgefahren sind.
  3. Korkenzieher/Schraube: Der Korkenzieher kann als auseinander gezogener Looping beschrieben werden.
  4. Schraubel-Looping : Und zu guter Letzt kombinieren wir  einen Looping mit einer Schraube! Ein Teil unserer Mitfahrer sucht den nächsten Busch auf, ein anderer Teil will am liebsten nochmal fahren und der Rest wird jetzt einen Pause brauchen.

Fakt ist, dass die Fahrelemente/Knoten unsere anfänglichen Emotionen/Eingangsdatensignale geändert haben.

1.2. Genereller Ablauf in TensorFlow

Anhand unser fiktiven Achterbahn haben wir das Prinzip der datenstrom-orientierten Programmierung eingefangen. Damit wir aber erst einmal Achterbahn fahren können, müssen wir diese konstruieren. Das gilt auch in TensorFlow und können die Arbeit in zwei wesentliche Phasen unterteilen:

  1. Erstellen eines Berechnungsgraphen: Wie auch bei einer Achterbahn müssen wir unser Modell erst einmal modellieren. Je nachdem welche Ressourcen uns zur Verfügung gestellt werden, welche Bedingungen wir folgen müssen, können wir unser Modell darauf aufbauen und gestalten.
  2. Ausführung des Berechnungsgraphen: Nachdem wir das Modell/den Graph fertig konstruiert haben, führen wir diese nun aus, d.h. für unsere Achterbahn, dass wir den Strom anschalten und losfahren können.

2. Erstellung eines Graphen

2.1. TensorFlow-Operatoren

Wie bereits erwähnt können Knoten verschiedene Operationen in sich tragen. Das können z.B. Addition, Substraktion oder aber auch mathematische Hyperbelfunktionen  à la Tangens Hyperbolicus Operatoren sein. Damit TensorFlow mit den Operatoren arbeiten kann, müssen wir diese mit den zur Verfügung gestellten Operatoren von TensorFlow auskommen. Eine vollständige Dokumentation findet ihr hier.

2.2. Platzhalter

Wenn in TensorFlow Daten aus externen Quellen in den Berechnungsgraph integriert werden sollen, dann wird eine eigens dafür entwickelte Struktur genutzt um die Daten einzulesen; dem Platzhalter. Ihr könnt euch den Platzhalter als Wagon unserer Achterbahn vorstellen, der die Mitfahrer (Daten bzw. Tensoren) durch die Achterbahn (Berechnungsgraph) jagt.

Es ist bei der Modellierung eines Berechnungsgraphen nicht notwendig, die Daten am Anfang einzuspeisen. Wie der Name schon sagt, setzt TensorFlow eine ‘leere Größe’ ein, die in der zweiten Phase gefüllt wird.

Eine Frage, die ich mir damals gestellt habe war, warum man einen Platzhalter braucht? Dazu können wir uns wieder unsere Achterbahn nehmen. Bei 2-3 Fahrgästen besteht kein Problem; wir hätten genug Platz/Ressourcen um diese unterzubringen. Aber was machen wir, wenn wir 10.000 Gäste haben, wie es auch in der Realität ist ? Das ist auch bei neuronalen Netzen der Fall, wenn wir zu viele Daten haben, dann stoßen wir irgendwann an unser Leistungslimit. Wir teilen unsere Daten/Gäste so auf, dass wir damit arbeiten können.

2.3. Variable

Stellen wir uns folgendes Szenario vor: Wir haben eine Achterbahn fertig konstruiert – wahrscheinlich die beste und verrückteste Achterbahn, die es jemals gegeben hat. Je nachdem welchen Effekt wir mit unserer Achterbahn erzielen wollen; z.B. ein einfacher Adrenalinschub, ein flaues Gefühl im Magen oder den vollständigen Verlust jeglicher Emotionen aus purer Angst um das eigene Leben, reicht es nicht nur ein schönes Modell zu bauen. Wir müssen zusätzlich verschiedene Größen anpassen um das Erlebnis zu maximieren. Eine wichtige Größe für unsere Achterbahn wäre die Geschwindigkeit (in neuronalen Netzen sind es die Gewichte), die über den Fahrspaß entscheidet. Um die optimale Geschwindigkeit zu ermitteln, müssen viele Versuche gemacht werden (sei es in der Realität oder in der Simulation) und nach jedem Test wird die Geschwindigkeit nach jedem Test angepasst. Zu diesem Zweck sind die Variablen da. Sie passen sich nach jedem Versuch an.

2.4. Optimierung

Damit die Variablen angepasst werden können, müssen wir TensorFlow Anweisungen geben, wie er die Variablen optimiert werden soll. Dafür müssen wir eine Formel an TensoFlow übermitteln, die dann optimiert wird. Auch hat man die Auswahl von verschiedenen Optimierer, die die Aufgabe anders optimieren. Die Wahl der richtigen Formel und des passenden Optimierer ist jedoch eine Sache, die ohne weiteres nicht zu beantworten ist. Wir wollen ein anderes Mal Bezug auf diese Frage nehmen.

3. Ausführung eines Graphen

Wie die Ausführung des Graphen von statten läuft, schauen wir uns im nächsten Abschnitt genauer an. Es sei so viel gesagt, dass um eine Ausführung einzuleiten wir den Befehl tf.Session() benötigen. Die Session wird mit tf.Session().run()gestartet und am Ende mit tf.Session().close() geschlossen. In der Methode .run()müssen die ausgeführten Größen stehen und außerdem der Befehl feed_dict= zum Befüllen der Platzhalter.

4. Beispiel: Achterbahn des Grauens – Nichts für schwache Nerven

4.1 Erklärung des Beispiels

Wir haben jetzt von so vielen Analogien gesprochen, dass es alles ein wenig verwirrend sein kann. Daher nochmal eine Übersicht zu den wesentlichen Punkten:

TensorFlow Neuronales Netz Achterbahn
Knoten Neuron Fahrelement
Variable Gewichte, Bias Geschwindigkeit
Kanten Signale Zustand der Fahrer
Platzhalter Wagon
Tab.1: Analogie unser fiktiven Achterbahn


Nun haben wir so viel Theorie gehört, jetzt müssen auch Taten folgen! Weshalb wir unsere Achterbahn modellieren wollen. Zu unserem Beispiel: Wir wollen eine Achterbahn bauen, welche ängstlichen Mitfahrer noch ängstlicher machen soll und diese sollen am Ende der Fahrt sich wünschen nie mitgefahren zu sein. (Es wird natürlich eine stark vereinfachte Variante werden, die aber auf all unsere Punkte eingehen soll, die wir im oberen Teil angesprochen haben.)

Wie im bereits beschrieben, unterteilt sich die Arbeit in TensorFlow in zwei Phasen:

  1. Erstellung des Graphen: In unserem Falle wäre das die Konstruktion unserer Achterbahn.
  2. Ausführung des Graphen: In dieser Phase lassen wir unsere Insassen einfach los und schauen mal was passiert.

Um die Zahlen zu verstehen, möchte ich euch zudem erklären, was überhaupt das Ziel unseres Modells ist. Wir haben 8 Probanden mit verschiedenen Angstzuständen. Der Angstzustand ist in unserem Beispiel ein quantitativer Wert, Menge der ganzen Zahlen  und je größer dieser Wert ist, desto ängstlicher sind unsere Probanden. Unser Ziel ist es alle Probanden in Angst und Schrecken zu versetzen, die einen Angstzustand >5 haben und sich nach der Fahrt wünschen unserer Achterbahn nie mitgefahren zu sein! Die Größe die wir dabei optimieren wollen, ist die Geschwindigkeit. Wenn die Geschwindigkeit zu schnell ist, dann fürchten sich zu viele, wenn wir zu langsam fahren, dann fürchtet sich womöglich niemand. Außerdem benötigen wir noch eine Starthöhe, die wir dem Modell zugeben müssen.

Wir haben somit eine Klassifikationsaufgabe mit dem Ziel die Geschwindigkeit und die Starthöhe zu optimieren, damit sich Fahrgäste mit einem Angstzustand > 5 so eine schlechte Erfahrung machen, dass sie am liebsten nie mitgefahren wären.

Wir benötigen außerdem für unser Beispiel folgende Module:

4.2. Eingangssignale: Zustände der Gäste

Wir sehen hier zwei Vektoren bzw. Tensoren die Informationen über unsere Gäste haben.

  • x_input ist der Angstzustand unserer Gäste
  • y_input ist unser gewünschtes Ausgangsssignal: 0  normal, 1  Wunsch nicht mitgefahren zu sein

4.3. Erstellung unseres Graphen: Konstruktion der Achterbahn

Nun konstruieren wir unsere Achterbahn des Grauens:

Eine Gleichrichter-Aktivierungsfunktion (engl. rectifier) mit einer Matrizenmultiplikation aus einem Vektor und einem Skalar mit anschließender Fehleroptimierung! MuhahahahaHAHAHAHA!

Auf den ersten Blick vielleicht ein wenig verwirrend, weshalb wir alles Schritt für Schritt durchgehen:

  • wag = tf.placeholder(tf.float32, shape = [8, 1]) ist unser Wagon, welcher die Achterbahn auf und ab fährt. Gefüllt mit unseren Probanden. Die Daten der Probanden (x_input)sind externe Daten und damit geeignet für einen Platzhalter.
    • Wichtig bei Platzhalter ist, dass ihr den Datentyp angeben müsst!
    • Optional könnt ihr auch die Form angeben. Bei einem so überschaubaren Beispiel machen wir das auch. (Form unseres Vektors: 8×1)
  • y_true = tf.placeholder(tf.float32, shape = [8, 1]) ist der gewünschte Endzustand unserer Gäste, den wir uns für die Probanden erhoffen, d.h. es ist unser y_input. Auch hier kommen die Daten von außerhalb und daher wird der Platzhalter genutzt.
  • v, h sind Geschwindigkeit und Starthöhe, die optimiert werden müssen; perfekt für eine Variable!
    • Variablen brauchen am Anfang immer einen Initialisierungswert. Für v soll es 1 sein und für h soll es -2 sein. Außerdem liegen diese Größen als Skalare (1×1) vor.

Abb.2: Schematische Darstellung unseres Berechnungsgraphen

Nun zum zweiten Teil der Modellierung in dem wir ein klein wenig Mathematik benötigen. Schauen wir uns folgende Gleichung an:

  • z = tf.matmul(wag, v) + h: ist unsere Matrizenmultiplikation -> Da unsere Größen in Vektoren/Tensoren vorliegen, können wir diese nicht einfach multiplizieren, wie z.B. 2*2 = 4. Bei der Multiplikation von Matrizen oder Vektoren müssen bestimmte Bedingungen herrschen, damit diese überhaupt multipliziert werden können. Eine ausführlichere Erklärungen soll demnächst folgen.
  • y_pred = tf.nn.relu(z): Für all diejenigen, die sich bereits mit neuronalen Netzen beschäftigt haben; relu ist in unserem Fall die Aktivierungsfunktion. Für alle anderen, die mit der Aktivierungsfunktion noch nichts anfangen können: Die Kombination (Matrizenmultiplikation) aus dem Angstzustand und der Geschwindigkeit ist der Wert Z. Je nachdem welche Aktivierungsfunktion genutzt wird, triggert der Wert unsere Emotionen, so dass wir den Wunsch verspüren, die Bahn nie gefahren zu sein.
  • err = tf.square(y_true - y_pred):Quadriert die Differenz der tatsächlichen und der ermittelten Werte. -> die zu optimierende Funktion
  • opt = tf.train.AdamOptimizer(learning_rate=0.01).minimize(err)Unser gewählter Optimierer mit der Lernrate 0.01.
  • init = tf.global_variables_initializer() Initialisierung der Variablen

Abb. 3: Aktivierungsfunktion ReLu

4.4. Ausführung des Graphen: Test der Achterbahn

Wenn wir den unten stehenden Code mal grob betrachten, dann fällt vor allem die Zeile mit dem with-(Python)Operator und dem tf.Session()-(TensorFlow)Operator auf. Der tf.Session()-Operator leitet unsere Ausführung ein. Warum wir with nutzen hat den Grund, dass dieser Operator uns das Leben einfacher macht, da dieser die nachfolgenden Befehle wieder schließt und damit wieder Leistungsressourcen frei werden. Werden zum Beispiel Daten aus externen Quellen benötigt – sei es eine Excel- oder eine SQL-Tabelle – dann schließt uns der with Operator die geöffneten Dateien, nachdem er alle unsere Befehle durchgeführt hat.

Durch die Methode .run() werden dann die in der Klammer befindenden Größen bearbeitet. Mit dem Parameter feed_dict= füllen wir den Graphen mit unseren gewünschten Dateien.

Wir lassen das Ganze 100 mal Testfahren um die optimalen Variablen zu finden. In Abb. 4 sehen wir die Verläufe der Fehlerfunktion, der Geschwindigkeit und der Höhe.


In Tab.2 sind nun zwei Fahrgäste zu sehen, die sich wünschen, die Bahn nie gefahren zu sein! Deren Angstlevel () ist über 0 und damit wird der Wunsch getriggert wurde; so wie wir es auch beabsichtigt haben!

Angstlvl berechnet: Fehler: Geschwindigkeit: Starthöhe:
 [0.       ] [0.        ] [0.4536] [-2.5187]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.       ]  [0.        ]
 [0.2060 ] -> Wunsch getriggert  [0.6304]
 [1.5685] -> Wunsch getriggert  [0.3231]
Tab.2: Endergebnisse der letzten Runde

Abb.4: Verläufe der Fehler-, Geschwindigkeits- und Höhenfunktion durch Optimierung

5. Zusammenfassung und Ausblick

Zugegeben ist dieser ganze Aufwand für ein mehr oder weniger linearen Zusammenhang etwas übertrieben und bestimmt ist dem einen oder anderen aufgefallen, dass unser Beispiel mit der Achterbahn an manchen Stellen hinkt. Dennoch hoffe ich, dass ich mit der Analogie das Verständnis von TensorFlow rüberbringen konnte. Lasst uns daher nochmal die wichtigsten Punkte zusammenfassen:

Die Arbeit mit TensorFlow unterteilt sich in folgende Phasen:

  1. Erstellung des Graphen: In dieser Phase konzentrieren wir uns darauf einen Berechnungsgraphen zu erstellen, welcher  so konzipiert wird, dass er uns am Ende das Ergebnis ausgibt, welches wir uns wünschen.
    • Platzhalter: Eine der wichtigsten Sturkturen in TensorFlow ist der Platzhalter. Er ist dafür zuständig, wenn es darum geht externe Daten in unseren Graph einfließen zu lassen. Bei der Erstellung eines Platzhalters müssen wir zumindest den Datentypen angeben.
    • Variable: Wenn es darum geht Größen für ein Modell zu optimieren, stellt TensorFlow Variablen zur Verfügung. Diese benötigen eine Angabe, wie die Form des Tensors aussehen soll.
  2. Ausführung des Graphen: Nachdem wir unseren Graphen entwickelt haben, ist der nächste Schritt diesen auszuführen.
    • Dies machen wir mit dem Befehl tf.Session() und führen diesen dann mit der Methode .run() aus
    • Ebenfalls hat die Optimierung einen wichtigen Bestand in dieser Phase
    • Um unseren Graphen mit den Daten zu füllen, nutzen wir den wird den Parameter feed_dict=

Um diesen Artikel nicht in die Länge zu ziehen, wurden die Themen der Matrizenmultiplikation, Aktivierungsfunktion und Optimierung erstmal nur angerissen. Wir wollen in einem separaten Artikel näher darauf eingehen. Für den Anfang genügen wir uns damit, dass wir von diesen Elementen wissen und dass sie einen wichtigen Bestandteil haben, wenn wir neuronale Netze aufbauen wollen.

In nächsten Artikel werden wir dann ein Perzeptron erstellen und gehen auch näher auf die Themen ein, die wir in diesem Teil nur angerissen haben. Bleibt gespannt!

6. Bonus-Material

Mit Tensorboard ist es möglich unseren entwickelten Graphen auch plotten und auszugeben zu lassen. So sieht unser Graph aus:

Abb.5.: Tensorboard Berechnungsgraphausgabe

Den Programmiercode könnt ihr in diesem Link auch als Ganzes betrachten.