Data Science, Machine Learning und KI
Kontakt

Warum wir KI-Prinzipien brauchen

Künstliche Intelligenz verändert unsere Welt grundlegend. Algorithmen beeinflussen zunehmend, wie wir uns verhalten, denken und fühlen. Unternehmen rund um den Globus werden KI-Technologien zunehmend nutzen und ihre derzeitigen Prozesse und Geschäftsmodelle neu erfinden. Unsere sozialen Strukturen, die Art und Weise, wie wir arbeiten und wie wir miteinander interagieren, werden sich mit den Fortschritten der Digitalisierung, insbesondere der KI, verändern.

Neben ihrem sozialen und wirtschaftlichen Einfluss spielt KI auch eine wichtige Rolle bei einer der größten Herausforderungen unserer Zeit: dem Klimawandel. Einerseits kann KI Instrumente bereitstellen, um einen Teil dieser dringenden Herausforderung zu bewältigen. Andererseits wird die Entwicklung und Implementierung von KI-Anwendungen viel Energie verbrauchen und große Mengen an Treibhausgasen ausstoßen.

Risiken der KI

Mit dem Fortschritt einer Technologie, die einen so großen Einfluss auf alle Bereiche unseres Lebens hat, gehen große Chancen, aber auch große Risiken einher. Um Euch einen Eindruck von den Risiken zu vermitteln, haben wir sechs Beispiele herausgegriffen:

  • KI kann zur Überwachung von Menschen eingesetzt werden, zum Beispiel durch Gesichtserkennungssysteme. Einige Länder setzen diese Technologie bereits seit einigen Jahren intensiv ein.
  • KI wird in sehr sensiblen Bereichen eingesetzt. In diesen können schon kleine Fehlfunktionen dramatische Auswirkungen haben. Beispiele dafür sind autonomes Fahren, robotergestützte Chirurgie, Kreditwürdigkeitsprüfung, Auswahl von Bewerber:innen oder Strafverfolgung.
  • Der Skandal um Facebook und Cambridge Analytica hat gezeigt, dass Daten und KI-Technologien zur Erstellung psychografischer Profile genutzt werden können. Diese Profile ermöglichen die gezielte Ansprache von Personen mit maßgeschneiderten Inhalten. Beispielsweise zur Beeinflussung von politischen Wahlen. Dieses Beispiel zeigt die enorme Macht der KI-Technologien und die Möglichkeit für Missbrauch und Manipulation.
  • Mit den jüngsten Fortschritten in der Computer Vision Technologie können Deep Learning Algorithmen nun zur Erstellung von Deepfakes verwendet werden. Deepfakes sind realistische Videos oder Bilder von Menschen, in denen diese etwas tun oder sagen, was sie nie in der Realität getan oder gesagt haben. Die Möglichkeiten für Missbrauch dieser Technologie sind vielfältig.
  • KI-Lösungen werden häufig entwickelt, um manuelle Prozesse zu verbessern oder zu optimieren. Es wird Anwendungsfälle geben, bei denen menschliche Arbeit ersetzt wird. Dabei entstehen unterschiedlichste Herausforderungen, die nicht ignoriert, sondern frühzeitig angegangen werden müssen.
  • In der Vergangenheit haben KI-Modelle diskriminierende Muster der Daten, auf denen sie trainiert wurden, reproduziert. So hat Amazon beispielsweise ein KI-System in seinem Rekrutierungsprozess eingesetzt, das Frauen eindeutig benachteiligte.

Diese Beispiele machen deutlich, dass jedes Unternehmen und jede Person, die KI-Systeme entwickelt, sehr sorgfältig darüber nachdenken sollte, welche Auswirkungen das System auf die Gesellschaft, bestimmte Gruppen oder sogar Einzelpersonen haben wird oder haben könnte.

Daher besteht die große Herausforderung für uns darin, sicherzustellen, dass die von uns entwickelten KI-Technologien den Menschen helfen und sie befähigen, während wir gleichzeitig potenzielle Risiken minimieren.

Warum gibt es im Jahr 2022 keine offizielle Regelung?

Vielleicht fragt Ihr euch, warum es keine Gesetze gibt, die sich mit diesem Thema befassen. Das Problem bei neuen Technologien, insbesondere bei künstlicher Intelligenz, ist, dass sie sich schnell weiterentwickeln, manchmal sogar zu schnell.

Die jüngsten Veröffentlichungen neuer Sprachmodelle wie GPT-3 oder Computer Vision Modelle, z. B. DALLE-2, haben selbst die Erwartungen vieler KI-Expert:innen übertroffen. Die Fähigkeiten und Anwendungen der KI-Technologien werden sich schneller weiterentwickeln, als die Regulierung es kann. Und wir sprechen hier nicht von Monaten, sondern von Jahren.

Dabei ist zu erwähnen, dass die EU einen ersten Versuch in diese Richtung unternommen hat, indem sie eine Regulierung von künstlicher Intelligenz vorgeschlagen hat. In diesem Vorschlag wird jedoch darauf hingewiesen, dass die Verordnung frühestens in der zweiten Hälfte des Jahres 2024 für die anwendenden Unternehmen gelten könnte. Das sind Jahre, nachdem die oben beschriebenen Beispiele Realität geworden sind.

Unser Ansatz: statworx AI Principles

Die logische Konsequenz daraus ist, dass wir uns als Unternehmen selbst dieser Herausforderung stellen müssen. Und genau deshalb arbeiten wir derzeit an den statworx AI Principles, einer Reihe von Prinzipien, die uns bei der Entwicklung von KI-Lösungen leiten und Orientierung geben sollen.

Was wir bisher getan haben und wie wir dazu gekommen sind

In unserer Arbeitsgruppe „AI & Society“ haben wir begonnen, uns mit diesem Thema zu beschäftigen. Zunächst haben wir den Markt gescannt und viele interessante Paper gefunden. Allerdings sind wir zu dem Schluss gekommen, dass sich keins davon 1:1 auf unser Geschäftsmodell übertragen lässt. Oft waren diese Prinzipien oder Richtlinien sehr schwammig oder zu detailliert und zusätzlich ungeeignet für ein Beratungsunternehmen, das im B2B-Bereich als Dienstleister tätig ist. Also beschlossen wir, dass wir selbst eine Lösung entwickeln mussten.

In den ersten Diskussionen darüber wurden vier große Herausforderungen deutlich:

  • Einerseits müssen die AI Principles klar und für das breite Publikum verständlich formuliert sein, damit auch Nicht-Expert:innen sie verstehen. Andererseits müssen sie konkret sein, um sie in unseren Entwicklungsprozess integrieren zu können.
  • Als Dienstleister haben wir nur begrenzte Kontrolle und Entscheidungsgewalt über einige Aspekte einer KI-Lösung. Daher müssen wir verstehen, was wir entscheiden können und was außerhalb unserer Kontrolle liegt.
  • Unsere AI Principles werden nur dann einen nachhaltigen Mehrwert schaffen, wenn wir auch nach ihnen handeln können. Deshalb müssen wir sie in unseren Kundenprojekten anwenden und bewerben. Wir sind uns darüber im Klaren, dass Budgetzwänge, finanzielle Ziele und andere Faktoren dem entgegenstehen könnten, da es zusätzlichen Zeit- und Geldaufwand erfordert.
  • Außerdem ist nicht immer klar, was falsch und richtig ist. Unsere Diskussionen haben gezeigt, dass es viele unterschiedliche Auffassungen darüber gibt, was richtig und notwendig ist. Das bedeutet, dass wir eine gemeinsame Basis finden müssen, auf die wir uns als Unternehmen einigen können.

Unsere zwei wichtigsten Erkenntnisse

Eine wichtige Erkenntnis aus diesen Überlegungen war, dass wir zwei Dinge brauchen.

In einem ersten Schritt brauchen wir übergeordnete Grundsätze, die verständlich und klar sind und bei denen alle mit an Bord sind. Diese Grundsätze dienen als Leitidee und geben Orientierung bei der Entscheidungsfindung. In einem zweiten Schritt wird daraus ein Framework abgeleitet, welches diese Grundsätze in allen Phasen unserer Projekte in konkrete Maßnahmen übersetzt.

Die zweite wichtige Erkenntnis ist, dass es durchaus schwierig ist, diesen Prozess zu durchlaufen und sich diese Fragen zu stellen. Aber gleichzeitig auch, dass dies für jedes Unternehmen, das KI-Technologie entwickelt oder einsetzt, unvermeidlich ist.

 

Was kommt als nächstes?

Bis jetzt sind wir fast am Ende des ersten Schritts angelangt. Wir werden die statworx AI Principles bald über unsere Kanäle kommunizieren. Wenn Ihr euch ebenfalls in diesem Prozess befindet, würden wir uns freuen, mit Euch in Kontakt zu treten, um zu verstehen, wie ihr vorgegangen seid und was ihr dabei gelernt habt.

Quellen

https://www.nytimes.com/2019/04/14/technology/china-surveillance-artificial-intelligence-racial-profiling.html

https://www.nytimes.com/2018/04/04/us/politics/cambridge-analytica-scandal-fallout.html

https://www.reuters.com/article/us-amazon-com-jobs-automation-insight-idUSKCN1MK08G

https://digital-strategy.ec.europa.eu/en/policies/european-approach-artificial-intelligence

https://www.bundesregierung.de/breg-de/themen/umgang-mit-desinformation/deep-fakes-1876736

https://www.welt.de/wirtschaft/article173642209/Jobverlust-Diese-Jobs-werden-als-erstes-durch-Roboter-ersetzt.html

Jan Fischer Jan Fischer Jan Fischer Jan Fischer Jan Fischer Jan Fischer Alexander Blaufuss

Recently, some colleagues and I attended the 2-day COVID-19 hackathon #wirvsvirus, organized by the German government. Thereby, we’ve developed a great application for simulating COVID-19 curves based on estimations of governmental measure effectiveness (FlatCurver). As there are many COVID-related dashboards and visualizations out there, I thought that gathering the underlying data from a single point of truth would be a minor issue. However, I soon realized that there are plenty of different data sources, mostly relying on the Johns Hopkins University COVID-19 case data. At first, I thought that’s great, but at a second glance, I revised my initial thought. The JHU datasets have some quirky issues to it that makes it a bit cumbersome to prepare and analyze it:

  • weird column names including special characters
  • countries and states „in the mix“
  • wide format, quite unhandy for data analysis
  • import problems due to line break issues
  • etc.

For all of you, who have been or are working with COVID-19 time series data and want to step up your data-pipeline game, let me tell you: we have an API for that! The API uses official data from the European Centre for Disease Prevention and Control and delivers a clear and concise data structure for further processing, analysis, etc.

Overview of our COVID-19 API

Our brand new COVID-19-API brings you the latest case number time series right into your application or analysis, regardless of your development environment. For example, you can easily import the data into Python using the requests package:

import requests
import json
import pandas as pd

# POST to API
payload = {'country': 'Germany'} # or {'code': 'DE'}
URL = 'https://api.statworx.com/covid'
response = requests.post(url=URL, data=json.dumps(payload))

# Convert to data frame
df = pd.DataFrame.from_dict(json.loads(response.text))

Or if you’re an R aficionado, use httr and jsonlite to grab the lastest data and turn it into a cool plot.

library(httr)
library(dplyr)
library(jsonlite)
library(ggplot2)

# Post to API
payload <- list(code = "ALL")
response <- httr::POST(url = "https://api.statworx.com/covid",
                       body = toJSON(payload, auto_unbox = TRUE), encode = "json")

# Convert to data frame
content <- rawToChar(response$content)
df <- data.frame(fromJSON(content))

# Make a cool plot
df %>%
  mutate(date = as.Date(date)) %>%
  filter(cases_cum > 100) %>%
  filter(code %in% c("US", "DE", "IT", "FR", "ES")) %>%
  group_by(code) %>%
  mutate(time = 1:n()) %>%
  ggplot(., aes(x = time, y = cases_cum, color = code)) +
  xlab("Days since 100 cases") + ylab("Cumulative cases") +
  geom_line() + theme_minimal()
covid-race

Developing the API using Flask

Developing a simple web app using Python is straightforward using Flask. Flask is a web framework for Python. It allows you to create websites, web applications, etc. right from Python. Flask is widely used to develop web services and APIs. A simple Flask app looks something like this.

from flask import Flask
app = Flask(__name__)

@app.route('/')
def handle_request():
  """ This code gets executed """
  return 'Your first Flask app!'

In the example above, app.route decorator defines at which URL our function should be triggered. You can specify multiple decorators to trigger different functions for each URL. You might want to check out our code in the Github repository to see how we build the API using Flask.

Deployment using Google Cloud Run

Developing the API using Flask is straightforward. However, building the infrastructure and auxiliary services around it can be challenging, depending on your specific needs. A couple of things you have to consider when deploying an API:

  • Authentification
  • Security
  • Scalability
  • Latency
  • Logging
  • Connectivity

We’ve decided to use Google Cloud Run, a container-based serverless computing framework on Google Cloud. Basically, GCR is a fully managed Kubernetes service, that allows you to deploy scalable web services or other serverless functions based on your container. This is how our Dockerfile looks like.

# Use the official image as a parent image
FROM python:3.7

# Copy the file from your host to your current location
COPY ./main.py /app/main.py
COPY ./requirements.txt /app/requirements.txt

# Set the working directory
WORKDIR /app

# Run the command inside your image filesystem
RUN pip install -r requirements.txt

# Inform Docker that the container is listening on the specified port at runtime.
EXPOSE 80

# Run the specified command within the container.
CMD ["python", "main.py"]

You can develop your container locally and then push it in to the container registry of your GCP project. To do so, you have to tag your local image using docker tag according to the following scheme: [HOSTNAME]/[PROJECT-ID]/[IMAGE]. The hostname is one of the following: gcr.io, us.gcr.io, eu.gcr.io, asia.gcr.io. Afterward, you can push using gcloud push, followed by your image tag. From there, you can easily connect the container to the Google Cloud Run service:

google cloud run

When deploying the service, you can define parameters for scaling, etc. However, this is not in scope for this post. Furthermore, GCR allows custom domain mapping to functions. That’s why we have the neat API endpoint https://api.statworx.com/covid.

Conclusion

Building and deploying a web service is easier than ever. We hope that you find our new API useful for your projects and analyses regarding COVID-19. If you have any questions or remarks, feel free to contact us or to open an issue on Github. Lastly, if you make use of our free API, please add a link to our website, https://statworx-1727.demosrv.dev to your project. Thanks in advance and stay healthy!

Once again, an amazing year at STATWORX is coming to an end. The frequency and magnitude of positive things happening to our company are continuously increasing with every new client, employee, or partner coming to our company. That is why the very first paragraph of this post ends with a massive THANK YOU to my whole team, our customers and partners. We would not be where we are now without you. Let’s keep up the awesome work!

STATWORX is constantly growing each year, both in revenue and profit, as well as in services and products – likewise in 2019. Our growth deeply embodies a high pace of change into our company’s DNA and all the things that surround us. That is why sometimes you can lose track of all the different things happening throughout the year. To keep track of our evolving company, I’ve decided to write down some of the most important things that moved our company this year, ranging from exciting projects, new partners to great festivities and special events. Let’s begin the time travel!

Projects & Academy

The various kinds of data science, machine learning, and AI projects, which we deliver for our cross-industry customers, are the cornerstone of our company. And by cross-industry, I really mean it: automotive, finance, pharma, retail, insurance, aviation – you name it. I would assume that there isn’t a single industry we have not worked in so far. That greatly enlarges our perspective on the field of data science and AI and helps us to translate solutions from different industries or functions into our project environments across our customers. This year, there have been so many fascinating projects, both end-2-end as well as prototypes / MVPs, that it’s tough to pick the gems. My personal favorites would most likely be the ground operations optimization project for one of our airline customers, as well as our various ML and NLP projects we’ve conducted for the purchasing department of one of our automotive customers. Besides that, we’ve delivered several forecasting products, customer analytics solutions, as well as different dashboards and ML fueled applications. Furthermore, in our strategy department, we’ve supported several new customers from the financial and insurance industry, who are just getting started with data science and AI by setting up their data and analytics platform strategies. Whew!

Besides our consulting projects, the “STATWORX Academy” brings a multitude of different data science, ML and AI trainings and workshops to our customers. One of the most exciting training projects this year was, for sure, a company-wide AI leadership training for one of our biggest customers. With a team from their professional development department, IT, and data science in finance, we’ve forged a manifold and interactive workshop, specifically targeted at middle to top management leaders. Besides multiple training sessions in Germany, this also led to cross-continental trainings in the USA as well as China. It was fascinating to discuss with leaders from various cultural backgrounds about AI and its implications on business, society and life in general. I am really looking forward to continue this training initiative in 2020.

Another great workshop event this year was our brand-new “Deep Learning Bootcamp”. Besides our battle-proven „Data Science Bootcamp”, we’ve created a 5-day workshop all around neural networks and deep learning. Thereby, we are digging into the basic concepts, such as MLPs, CNNs, and LSTMs, but also into more advanced topics such as GANs, autoencoders, and deep reinforcement learning. The first Bootcamp was a great success, for which we received loads of positive feedback from our participants. I can’t wait for the next session in the first quarter of 2020, with many new topics and programming exercises in Python and TensorFlow.

Investments

In 2019 we’ve also conducted our first financial investments into companies and products we believe in: first to mention is bamboolib, a product two former STATWORKERS created in their newly founded company. bamboolib is a low-code data preparation and visualization GUI for pandas in Jupyter (yes, a GUI for Jupyter). It allows data science professionals to carry out time-consuming data prep and manipulation steps 10x faster. Furthermore, it enables Python and pandas starters to learn the concepts of data prep more easily. bamboolib is currently making huge waves on LinkedIn, so I definitely recommend that you check it out!

Besides bamboolib, we’ve engaged in a joint-venture called ONEZERO-X that is bringing data science, machine learning, and AI to sports business. We’ve already had our first project for a German soccer club and created a great MVP for forecasting soccer stadium utilization by using historical sales data and many external effects. I will be presenting the new joint venture at IN-BETA AUDITIONS in Frankfurt in January 2020.

Partners

STATWORX is continually engaging with new partners to leverage synergies and to bring our services to a broader audience of customers. One of our longest and most fruitful partnerships is with Dataiku. The French software startup just reached unicorn-level company valuation and never ceases to surprise us with new powerful features on their innovative data science platform as well as with their excellent team spirit and collaborative work approach. Additionally, to support our clients on all major cloud platforms, we have further extended our cloud service provider network by entering partnerships with Microsoft and AWS, on top of our existing partnership with Google (GCP).

Depending on high-quality data in everything we do, we understand how important it is for all companies to build up a clean and structured data foundation. Therefore, on multiple occasions, we joined forces with the data management and data governance focused University of St. Gallen spin-off CDQ. Last but not least, we added renowned semiconductor manufacturer STMicroelectronics to the list of our partners. We are thrilled to leverage their STM32 microcontrollers’ capabilities, to run AI directly on edge controllers, which allows the integration of state-of-the-art neural networks in highly specialized industrial applications.

Events

When it comes to events and conferences, STATWORX is always ready to jump in. Not only to gather new knowledge but also to network, make new connections, and get new leads. One of our favorite events throughout the year is the Data Festival in Munich. This year, our Head of Data Science, Fabian Müller, gave an exciting talk about XAI (explainable AI), its current applications, and limitations. The Data Festival is getting bigger and bigger each year, which triggers the creativity of our marketing to come up with great new ideas for our STATWORX booth. This year, we launched our „Beat the AI“ campaign, where visitors of our booth could compete against a reinforcement learning agent that learned how to play Super Mario on NES.

“Beat the AI” was also our campaign at EGG Germany 2019 in Stuttgart, the 1-day AI conference organized by our friends at Dataiku. Here, we gave a talk on how to bring data science and AI to organizations using Dataiku together with our friends from Mercedes Benz.

image-03-egg-germany

Another exceptional event this year was the „KEX Forecasting Challenge“. Here, a selection of AI vendors was given a dataset of daily sales data that they were asked to forecast. The actual sales data was held back to ensure a fair competition. In an intense weekend session, the “STATWORX Black Ops” team managed to build a machine learning model for 10 different products that was able to forecast 365 days ahead. During the presentation of the results, we’ve received great feedback from the data supplying company and currently engage in a joint project.

Another highlight of 2019 was our participation in the Swiss AI Hackathon, where we competed against 10 other teams in the development of a machine learning model for turnaround delay prediction. This 2-day event in Zurich certainly was one of the most challenging events this year, since we had a great variety of data sources to deal with to make the model work. After 2-days of coffee, red bull, beer, and pizza, the STATWORX team made it on to the winners‘ podium and presented our modeling approach in front of several members of the Swiss BOM.

Data University

As you could read up until here, the last year was filled with new opportunities and challenges for us. One of those challenges for us as a team was the Data University, our first-ever open event. We created the event with the help of our partner BARC in early 2019. Together, we developed a marketing plan, designed the curriculum, built a website, and created the workshops. In October, we held the interactive 2-day workshop-event at the Goethe University in Frankfurt, with 40 participants, 8 trainers, and a whole lot of pizzas. Furthermore, our friends from Frankfurt Data Science organized an event in the evening about “How to become a Data Scientist” with over 100 participants that perfectly matched our topics at Data University. I want to thank everyone at BARC again for the great partnership. If you’re interested in our newest events and plans, then follow us on our various social media accounts and stay tuned.

image-05-DUgruppenfoto

Work events

With all the hard work we do here at STATWORX, we also like to relax and have some after-work fun. There have been multiple occasions during the past twelve months, where my team prepared parties, fun get-togethers and excursions to take our collective minds off of work. One of these occasions was the famous STATWORX summer barbecue, which took place on a very hot Friday evening in June. Everyone prepared something to eat, like a salad or tasty finger food, and we enjoyed the great weather with some fresh cocktails on our terrace.

But the barbecue wasn’t the only time we had great luck with the weather. In October, part of our crew went on a day trip to the picturesque Rheingau, the famous wine region known for its world-famous Riesling wines. With sunglasses on and a glass of delicious white wine in hand, the group went hiking for about four hours in the vineyards.

Last but not least, the Christmas spirit found its way into the STATWORX office. For our Christmas party in late November, the team prepared a large pot of hot wine, wrapped gifts for everyone, and showcased some very creative Ugly Sweaters. To finalize this amazing year, we also took a weekend trip to Belgium, where we time to unwind, talk, play games, laugh, and party.

As you can see, we had a lot of fun days and nights together. A big shout out to everyone who helped organize these events. Cheers!

Outlook on 2020

What a year! By writing all of this down, I started to realize how diverse and inspiring all of the different things were that happened at our company in the last year. And there is always more to come: in the first half of 2020, we will move into the brand-new STATWORX HQ in Frankfurt on 1’400 sqm on 2 floors. This will be our biggest and boldest move so far, which will enable our company and employees to grow, prosper, and develop even further. In terms of services, we will continue to strengthen our team and offerings in data and software engineering to accord with the skyrocketing demand for ML ops and industrialization of machine learning and AI applications. In addition to that, we will be launching our first own AI software product in 2020, which will be another massive step for our company on our collective journey along the road of STATWORX.

I can’t wait to see what 2020 holds for us! The only thing that remains constant in life is change. I am wishing you a happy holiday and a joyful new year.

Best wishes from your friends at STATWORX. This text was written by GPT-2 (just kidding).

It is June and nearly half of the year is over, marking the middle between Christmas 2018 and 2019. Last year in autumn, I’ve published a blog post about predicting Wham’s „Last Christmas“ search volume using Google Trends data with different types of neural network architectures. Of course, now I want to know how good the predictions were, compared to the actual search volumes.

The following table shows the predicted values by the different network architectures, the true search volume data in the relevant time region from November 2018 until January 2019, as well as the relative prediction error in brackets:

month MLP CNN LSTM actual
2018-11 0.166 (0.21) 0.194 (0.078) 0.215 (0.023) 0.21
2018-12 0.858 (0.057) 0.882 (0.031) 0.817 (0.102) 0.91
2019-01 0.035 (0.153) 0.034 (0.149) 0.035 (0.153) 0.03

There’s no clear winner in this game. For the month November, the LSTM model performs best with a relative error of only 2.3%. However, in the „main“ month December, the LSTM drops in accuracy in favor of the 1-dimensional CNN with 3.1% error and the MLP with 5.7% error. Compared to November and December, January exhibits higher prediction errors >10% regardless of the architecture.

To bring a little more data science flavor into this post, I’ve created a short R script that presents the results in a cool „heatmap“ style.

library(dplyr)
library(ggplot2)

# Define data frame for plotting
df_plot <- data.frame(MONTH=rep(c("2018-11", "2018-12", "2019-01"), 3),
                      MODEL = c(rep("MLP", 3), rep("CNN", 3), rep("LSTM", 3)),
                      PREDICTION = c(0.166, 0.858, 0.035, 0.194, 0.882, 0.034, 0.215, 0.817, 0.035),
                      ACTUAL = rep(c(0.21, 0.91, 0.03), 3))

# Do plot
df_plot %>%
  mutate(MAPE = round(abs(ACTUAL - PREDICTION) / ACTUAL, 3)) %>%
  ggplot(data = ., aes(x = MONTH, y = MODEL)) +
  geom_tile(aes(fill = MAPE)) +
  scale_fill_gradientn(colors = c('navyblue', 'darkmagenta', 'darkorange1')) +
  geom_text(aes(label = MAPE), color = "white") +
  theme_minimal()
prediction heat map

This year, I will (of course) redo the experiment using the newly acquired data. I am curious to find out if the prediction improves. In the meantime, you can sign up to our mailing list, bringing you the best data science, machine learning and AI reads and treats directly into your mailbox!

Reinforcement learning is currently one of the hottest topics in machine learning. For a recent conference we attended (the awesome Data Festival in Munich), we’ve developed a reinforcement learning model that learns to play Super Mario Bros on NES so that visitors, that come to our booth, can compete against the agent in terms of level completion time.

The promotion was a great success and people enjoyed the „human vs. machine“ competition. There was only one contestant who was able to beat the AI by taking a secret shortcut, that the AI wasn’t aware of. Also, developing the model in Python was a lot of fun. So, I decided to write a blog post about it that covers some of the fundamental concepts of reinforcement learning as well as the actual implementation of our Super Mario agent in TensorFlow (beware, I’ve used TensorFlow 1.13.1, TensorFlow 2.0 was not released at the time of writing this article).

Recap: reinforcement learning

Most machine learning models have an explicit connection between inputs and outputs that does not change during training time. Therefore, it can be difficult to model or predict systems, where the inputs or targets themselves depend on previous predictions. However, often,the world around the model updates itself with every prediction made. What sounds quite abstract is actually a very common situation in the real world: autonomous driving, machine control, process automation etc. – in many situations, decisions that are made by models have an impact on their surroundings and consequently on the next actions to be taken. Classical supervised learning approaches can only be used to a limited extend in such kinds of situations. To solve the latter, machine learning models are needed that are able to cope with time-dependent variation of inputs and outputs that are interdependent. This is where reinforcement learning comes into play.

In reinforcement learning, the model (called agent) interacts with its environment by choosing from a set of possible actions (action space) in each state of the environment that cause either positive or negative rewards from the environment. Think of rewards as an abstract concept of signalizing that the action taken was good or bad. Thereby, the reward issued by the environment can be immediate or delayed into the future. By learning from the combination of environment states, actions and corresponsing rewards (so called transitions), the agent tries to reach an optimal set of decision rules (the policy) that maximize the total reward gathered by the agent in each state.

Q-learning and Deep Q-learning

In reinforcement learning we often use a learning concept called Q-learning. Q-learning is based on so called Q-values, that help the agent determining the optimal action, given the current state of the environment. Q-values are „discounted“ future rewards, that our agent collects during training by taking actions and moving through the different states of the environment. Q-values themselves are tried to be approximated during training, either by simple exploration of the environment or by using a function approximator, such as a deep neural network (as in our case here). Mostly, we select in each state the action that has the highest Q-value, i.e. the highest discounuted future reward, givent the current state of the environment.

When using a neural network as a Q-function approximator we learn by computing the difference between the predicted Q-values and the „true“ Q-values, i.e. the representation of the optimal decision in the current state. Based on the computed loss, we update the network’s parameters using gradient descent, just like in any other neural network model. By doing this often, our network converges to a state, where it can approximate the Q-values of the next state, given the current state of the environment. If the approximation is good enough, we simple select the action that has the highest Q-value. By doing so, the agent is able to decide in each situation, which action generates the best outcome in terms of reward collection.

In most deep reinforcement learning models there are actually two deep neural networks involved: the online- and the target-network. This is done because during training, the loss function of a single neural network is computed against steadily changing targets (Q-values), that are based on the networks weights themselves. This adds increased difficulty to the optimization problem or might result in no convergence at all. The target network is basically a copy of the online network with frozen weights that are not directly trained. Instead the target network’s weights are synchronized with the online network after a certain amount of training steps. Enforcing „stable outputs“ of the target network that do not change after each training step makes sure that the computed target Q-values that are needed for computing the loss do not change steadily which supports convergence of the optimization problem.

Deep Double Q-learning

Another possible issue with Q-learning is, that due to the selection of the maximum Q-value for determining the best action, the model sometimes produces extraordinary high Q-values during training. Basically, this is not always a problem but might turn into one, if there is a strong concentration at certain actions that in return lead to the negletion of less favorable but „worth-to-try“ actions. If the latter are neglected all the time, the model might run into a locally optimal solution or even worse selects the same actions all the time. One way to deal with this problem is to introduce an updated version of Q-learning called double Q-learning.

In double Q-learning the actions in each state are not simply chosen by selecting the action with maximum Q-value of the target network. Instead, the selection process is split into three distinct steps: (1) first, the target network computes the target Q-values of the state after taking the action. Then, (2) the online network computes the Q-values of the state after taking the action and selects the best action by finding the maximum Q-value. Finally, (3) the target Q-Values are calculated using the target Q-values of the target network, but at the selected action indices of the online network. This assures, that there cannot occur an overestimation of Q-values because the Q-values are not updated based on themselves.

Gym environments

In order to build a reinforcement learning aplication, we need two things: (1) an environment that the agent can interact with and learn from (2) the agent, that observes the state(s) of the environment and chooses appropriate actions using Q-values, that (ideally) result in high rewards for the agent. An environment is typically provided as a so called gym, a class that contains the neecessary code to emulate the states and rewards of the environment as a function of the agent’s actions as well further information, e.g. about the possible action space. Here is an example of a simple environment class in Python:

class Environment:
    """ A simple environment skeleton """
    def __init__(self):
          # Initializes the environment
        pass

    def step(self, action):
          # Changes the environment based on agents action
        return next_state, reward, done, info

    def reset(self):
        # Resets the environment to its initial state
        pass

    def render(self):
          # Show the state of the environment on screen
        pass

The environment has three major class functions: (1) step() executes the environment code as function of the action selected by the agent and returns the next state of the environment, the reward with respect to action, a done flag indicating if the environment has reached its terminal state as well as a dictionary of additional information about the environment and its state, (2) reset() resets the environment in it’s original state and (3) render() print the current state on the screen (for example showing the current frame of the Super Mario Bros game).

For Python, a go-to place for finding gyms is OpenAI. It contains lots of diffenrent games and problems well suited for solving using reinforcement learning. Furthermore, there is an Open AI project called Gym Retro that contains hundrets of Sega and SNES games, ready to be tackled by reinforcement learning algorithms.

Agent

The agent comsumes the current state of the environment and selects an appropriate action based on the selection policy. The policy maps the state of the environment to the action to be taken by the agent. Finding the right policy is a key question in reinforcement learning and often involves the usage of deep neural networks. The following agent simply observes the state of the environment and returns action = 1 if state is larger than 0 and action = 0 otherwise.

class Agent:
    """ A simple agent """
    def __init__(self):
        pass

    def action(self, state):
        if state > 0:
            return 1
        else:
            return 0

This is of course a very simplistic policy. In practical reinforcement learning applications the state of the environment can be very complex and high-dimensional. One example are video games. The state of the environment is determined by the pixels on screen and the previous actions of the player. Our agent needs to find a policy that maps the screen pixels into actions that generate rewards from the environment.

Environment wrappers

Gym environments contain most of the functionalities needed to use them in a reinforcement learning scenario. However, there are certain features that do not come prebuilt into the gym, such as image downscaling, frame skipping and stacking, reward clipping and so on. Luckily, there exist so called gym wrappers that provide such kinds of utility functions. An example that can be used for many video games such as Atari or NES can be found here. For video game gyms it is very common to use wrapper functions in order to achieve a good performance of the agent. The example below shows a simple reward clipping wrapper.

import gym

class ClipRewardEnv(gym.RewardWrapper):
        """ Example wrapper for reward clipping """
    def __init__(self, env):
        gym.RewardWrapper.__init__(self, env)

    def reward(self, reward):
        # Clip reward to {1, 0, -1} by its sign
        return np.sign(reward)

From the example shown above you can see, that it is possible to change the default behavior of the environment by „overwriting“ its core functions. Here, rewards of the environment are clipped to [-1, 0, 1] using np.sign() based on the sign of the reward.

The Super Mario Bros NES environment

For our Super Mario Bros reinforcement learning experiment, I’ve used gym-super-mario-bros. The API ist straightforward and very similar to the Open AI gym API. The following code shows a random agent playing Super Mario. This causes Mario to wiggle around on the screen and – of course – does not lead to a susscessful completion of the game.

from nes_py.wrappers import BinarySpaceToDiscreteSpaceEnv
import gym_super_mario_bros
from gym_super_mario_bros.actions import SIMPLE_MOVEMENT


# Make gym environment
env = gym_super_mario_bros.make('SuperMarioBros-v0')
env = BinarySpaceToDiscreteSpaceEnv(env, SIMPLE_MOVEMENT)

# Play random
done = True
for step in range(5000):
    if done:
        state = env.reset()
    state, reward, done, info = env.step(env.action_space.sample())
    env.render()

# Close device
env.close()

The agent interacts with the environment by choosing random actions from the action space of the environment. The action space of a video game is actually quite large since you can press multiple buttons at the same time. Here, the action space is reduced to SIMPLE_MOVEMENT, which covers basic game actions such as run in all directions, jump, duck and so on. BinarySpaceToDiscreteSpaceEnv transforms the binary action space (dummy indicator variables for all buttons and directions) into a single integer. So for example the integer action 12 corresponds to pressing right and A (running).

Using a deep learning model as an agent

When playing Super Mario Bros on NES, humans see the game screen – more precisely – they see consecutive frames of pixels, displayed at a high speed on the screen. Our human brains are capable of transforming the raw sensorial input from our eyes into electrical signals that are processed by our brain that trigger corresponding actions (pressing buttons on the controller) that (hopefully) lead Mario to the finishing line.

When training the agent, the gym renders each game frame as a matrix of pixels, according to the respective action taken by the agent. Basically, those pixels can be used as an input to any machine learning model. However, in reinforcement learning we often use convolutional neural networks (CNNs) that excel at image recognition problems compared to other ML models. I won’t go into technical detail about CNNs here, there’s a plethora of great intro articles to CNNs like this one.

Instead of using only the current game screen as an input to the model, it is common to use multiple stacked frames as an input to the CNN. By doing so, the model can process changes and „movements“ on the screen between consecutive frames, which would not be possible when using only a single game frame. Here, the input tensor of our model is of size [84, 84, 4]. This corresponds to a stack of 4 grayscale frames, each frame of size 84×84 pixels. This corresponds to the default tensor size for 2-dimensional convolution.

The architecture of the deep learning model consists of three convolutional layers, followed by a flatten and one fully connected layer with 512 neurons as well as an output layer, consisting of actions = 6 nerons, which corresponds to the action space of the game (in this case RIGHT_ONLY, i.e. actions to move Mario to the right – enlarging the action space usually causes an increase in problem complexity and training time).

If you take a closer look at the TensorBoard image below, you’ll notice that the model actually consists of not only one but two identical convolutional branches. One is the online network branch, the other one is the target network branch. The online network is acutally trained using gradient descent. The target network is not directly trained but periodically synchronized every copy = 10000 steps by copying the weights from the online branch to the target branch of the network. The target network branch is excluded from gradient descent training by using the tf.stop_gradient() function around the output layer of the branch. This causes a stop in the flow of gradients at the output layer so that they cannot propagate along the branch and so the weights are not updated.

The agent learns by (1) taking random samples of historical transitions, (2) computing the „true“ Q-values based on the states of the environment after action, next_state, using the target network branch and the double Q-learning rule, (3) discounting the target Q-values using gamma = 0.9 and (4) run a batch gradient descent step based on the network’s internal Q-prediction and the true Q-values, supplied by target_q. In order to speed up the training process, the agent is not trained after each action but every train_each = 3 frames which corresponds to a training every 4 frames. In addition, not every frame is stored in the replay buffer but each 4th frame. This is called frame skipping. More specifically, a max pooling operation is performed that aggregates the information between the last 4 consecutive frames. This is motivated by the fact that consecutive frames contain nearly the same information which does not add new information to the learning problem and might introduce strongly autocorrelated datapoints.

Speaking of correlated data: our network is trained using adaptive moment estimation (ADAM) and gradient descent at a learning_rate = 0.00025, which requires i.i.d. datapoints in order to work well. This means, that we cannot simply use all new transition tuples subsequently for training since they are highly correlated. To solve this issue we use a concept called experience replay buffer. Hereby, we store every transition of our game in a ring buffer object (in Python the deque() function) which is then randomly sampled from, when we acquire our training data of batch_size = 32. By using a random sampling strategy and a large enough replay buffer, we can assume that the resulting datapoints are (hopefully) not correlated. The following codebox shows the DQNAgent class.

import time
import random
import numpy as np
from collections import deque
import tensorflow as tf
from matplotlib import pyplot as plt


class DQNAgent:
    """ DQN agent """
    def __init__(self, states, actions, max_memory, double_q):
        self.states = states
        self.actions = actions
        self.session = tf.Session()
        self.build_model()
        self.saver = tf.train.Saver(max_to_keep=10)
        self.session.run(tf.global_variables_initializer())
        self.saver = tf.train.Saver()
        self.memory = deque(maxlen=max_memory)
        self.eps = 1
        self.eps_decay = 0.99999975
        self.eps_min = 0.1
        self.gamma = 0.90
        self.batch_size = 32
        self.burnin = 100000
        self.copy = 10000
        self.step = 0
        self.learn_each = 3
        self.learn_step = 0
        self.save_each = 500000
        self.double_q = double_q

    def build_model(self):
        """ Model builder function """
        self.input = tf.placeholder(dtype=tf.float32, shape=(None, ) + self.states, name='input')
        self.q_true = tf.placeholder(dtype=tf.float32, shape=[None], name='labels')
        self.a_true = tf.placeholder(dtype=tf.int32, shape=[None], name='actions')
        self.reward = tf.placeholder(dtype=tf.float32, shape=[], name='reward')
        self.input_float = tf.to_float(self.input) / 255.
        # Online network
        with tf.variable_scope('online'):
            self.conv_1 = tf.layers.conv2d(inputs=self.input_float, filters=32, kernel_size=8, strides=4, activation=tf.nn.relu)
            self.conv_2 = tf.layers.conv2d(inputs=self.conv_1, filters=64, kernel_size=4, strides=2, activation=tf.nn.relu)
            self.conv_3 = tf.layers.conv2d(inputs=self.conv_2, filters=64, kernel_size=3, strides=1, activation=tf.nn.relu)
            self.flatten = tf.layers.flatten(inputs=self.conv_3)
            self.dense = tf.layers.dense(inputs=self.flatten, units=512, activation=tf.nn.relu)
            self.output = tf.layers.dense(inputs=self.dense, units=self.actions, name='output')
        # Target network
        with tf.variable_scope('target'):
            self.conv_1_target = tf.layers.conv2d(inputs=self.input_float, filters=32, kernel_size=8, strides=4, activation=tf.nn.relu)
            self.conv_2_target = tf.layers.conv2d(inputs=self.conv_1_target, filters=64, kernel_size=4, strides=2, activation=tf.nn.relu)
            self.conv_3_target = tf.layers.conv2d(inputs=self.conv_2_target, filters=64, kernel_size=3, strides=1, activation=tf.nn.relu)
            self.flatten_target = tf.layers.flatten(inputs=self.conv_3_target)
            self.dense_target = tf.layers.dense(inputs=self.flatten_target, units=512, activation=tf.nn.relu)
            self.output_target = tf.stop_gradient(tf.layers.dense(inputs=self.dense_target, units=self.actions, name='output_target'))
        # Optimizer
        self.action = tf.argmax(input=self.output, axis=1)
        self.q_pred = tf.gather_nd(params=self.output, indices=tf.stack([tf.range(tf.shape(self.a_true)[0]), self.a_true], axis=1))
        self.loss = tf.losses.huber_loss(labels=self.q_true, predictions=self.q_pred)
        self.train = tf.train.AdamOptimizer(learning_rate=0.00025).minimize(self.loss)
        # Summaries
        self.summaries = tf.summary.merge([
            tf.summary.scalar('reward', self.reward),
            tf.summary.scalar('loss', self.loss),
            tf.summary.scalar('max_q', tf.reduce_max(self.output))
        ])
        self.writer = tf.summary.FileWriter(logdir='./logs', graph=self.session.graph)

    def copy_model(self):
        """ Copy weights to target network """
        self.session.run([tf.assign(new, old) for (new, old) in zip(tf.trainable_variables('target'), tf.trainable_variables('online'))])

    def save_model(self):
        """ Saves current model to disk """
        self.saver.save(sess=self.session, save_path='./models/model', global_step=self.step)

    def add(self, experience):
        """ Add observation to experience """
        self.memory.append(experience)

    def predict(self, model, state):
        """ Prediction """
        if model == 'online':
            return self.session.run(fetches=self.output, feed_dict={self.input: np.array(state)})
        if model == 'target':
            return self.session.run(fetches=self.output_target, feed_dict={self.input: np.array(state)})

    def run(self, state):
        """ Perform action """
        if np.random.rand() < self.eps:
            # Random action
            action = np.random.randint(low=0, high=self.actions)
        else:
            # Policy action
            q = self.predict('online', np.expand_dims(state, 0))
            action = np.argmax(q)
        # Decrease eps
        self.eps *= self.eps_decay
        self.eps = max(self.eps_min, self.eps)
        # Increment step
        self.step += 1
        return action

    def learn(self):
        """ Gradient descent """
        # Sync target network
        if self.step % self.copy == 0:
            self.copy_model()
        # Checkpoint model
        if self.step % self.save_each == 0:
            self.save_model()
        # Break if burn-in
        if self.step < self.burnin:
            return
        # Break if no training
        if self.learn_step < self.learn_each:
            self.learn_step += 1
            return
        # Sample batch
        batch = random.sample(self.memory, self.batch_size)
        state, next_state, action, reward, done = map(np.array, zip(*batch))
        # Get next q values from target network
        next_q = self.predict('target', next_state)
        # Calculate discounted future reward
        if self.double_q:
            q = self.predict('online', next_state)
            a = np.argmax(q, axis=1)
            target_q = reward + (1. - done) * self.gamma * next_q[np.arange(0, self.batch_size), a]
        else:
            target_q = reward + (1. - done) * self.gamma * np.amax(next_q, axis=1)
        # Update model
        summary, _ = self.session.run(fetches=[self.summaries, self.train],
                                      feed_dict={self.input: state,
                                                 self.q_true: np.array(target_q),
                                                 self.a_true: np.array(action),
                                                 self.reward: np.mean(reward)})
        # Reset learn step
        self.learn_step = 0
        # Write
        self.writer.add_summary(summary, self.step)

Training the agent to play

First, we need to instantiate the environment. Here, we use the first level of Super Mario Bros, SuperMarioBros-1-1-v0 as well as a discrete event space with RIGHT_ONLY action space. Additionally, we use a wrapper that applies frame resizing, stacking and max pooling, reward clipping as well as lazy frame loading to the environment.

When the training starts, the agent begins to explore the environment by taking random actions. This is done in order to build up initial experience that serves as a starting point for the actual learning process. After burin = 100000 game frames, the agent slowly starts to replace random actions by actions determined by the CNN policy. This is called an epsilon-greedy policy. Epsilon-greeedy means, that the agent takes a random action with probability epsilon or a policy-based action with probability (1-epsilon). Here, epsilon diminisches linearly during training by a factor of eps_decay = 0.99999975 until it reaches eps = 0.1 where it remains constant for the rest of the training process. It is important to not completely eliminate random actions from the training process in order to avoid getting stuck on locally optimal solutions.

For each action taken, the environment returns a four objects: (1) the next game state, (2) the reward for taking the action, (3) a flag if the episode is done and (4) an info dictionary containing additional information from the environment. After taking the action, a tuple of the returned objects is added to the replay buffer and the agent performs a learning step. After learning, the current state is updated with the next_state and the loop increments. The while loop breaks, if the done flag is True. This corresponds to either the death of Mario or to a successful completion of the level. Here, the agent is trained in 10000 episodes.

import time
import numpy as np
from nes_py.wrappers import BinarySpaceToDiscreteSpaceEnv
import gym_super_mario_bros
from gym_super_mario_bros.actions import RIGHT_ONLY
from agent import DQNAgent
from wrappers import wrapper

# Build env (first level, right only)
env = gym_super_mario_bros.make('SuperMarioBros-1-1-v0')
env = BinarySpaceToDiscreteSpaceEnv(env, RIGHT_ONLY)
env = wrapper(env)

# Parameters
states = (84, 84, 4)
actions = env.action_space.n

# Agent
agent = DQNAgent(states=states, actions=actions, max_memory=100000, double_q=True)

# Episodes
episodes = 10000
rewards = []

# Timing
start = time.time()
step = 0

# Main loop
for e in range(episodes):

    # Reset env
    state = env.reset()

    # Reward
    total_reward = 0
    iter = 0

    # Play
    while True:

        # Show env (diabled)
        # env.render()

        # Run agent
        action = agent.run(state=state)

        # Perform action
        next_state, reward, done, info = env.step(action=action)

        # Remember transition
        agent.add(experience=(state, next_state, action, reward, done))

        # Update agent
        agent.learn()

        # Total reward
        total_reward += reward

        # Update state
        state = next_state

        # Increment
        iter += 1

        # If done break loop
        if done or info['flag_get']:
            break

    # Rewards
    rewards.append(total_reward / iter)

    # Print
    if e % 100 == 0:
        print('Episode {e} - +'
              'Frame {f} - +'
              'Frames/sec {fs} - +'
              'Epsilon {eps} - +'
              'Mean Reward {r}'.format(e=e,
                                       f=agent.step,
                                       fs=np.round((agent.step - step) / (time.time() - start)),
                                       eps=np.round(agent.eps, 4),
                                       r=np.mean(rewards[-100:])))
        start = time.time()
        step = agent.step

# Save rewards
np.save('rewards.npy', rewards)

After each game episode, the averagy reward in this episode is appended to the rewards list. Furthermore, different stats such as frames per second and the current epsilon are printed after every 100 episodes.

Replay

During training, the program checkpoints the current network at save_each = 500000 frames and keeps the 10 latest models on disk. I’ve downloaded several model versions during training to my local machine and produced the following video.

It is so awesome to see the learning progress of the agent! The training process took approximately 20 hours on a GPU accelerated VM on Google Cloud.

Summary and outlook

Reinforcement learning is an exciting field in machine learning that offers a wide range of possible applications in science and business likewise. However, the training of reinforcement learning agents is still quite cumbersome and often requires tedious tuning of hyperparameters and network architecture in order to work well. There have been recent advances, such as RAINBOW (a combination of multiple RL learning strategies) that aim at a more robust framework for training reinforcement learning agents but the field is still an area of active research. Besides Q-learning, there are many other interesting training concepts in reinforcement learning that have been developed. If you want to try different RL agents and training approaches, I suggest you check out Stable Baselines, a great way to easily use state-of-the-art RL agents and training concepts.

If you are a deep learning beginner and want to learn more, you should check our brandnew STATWORX Deep Learning Bootcamp, a 5-day in-person introduction into the field that covers everything you need to know in order to develop your first deep learning models: neural net theory, backpropagation and gradient descent, programming models in Python, TensorFlow and Keras, CNNs and other image recognition models, recurrent networks and LSTMs for time series data and NLP as well as advaned topics such as deep reinforcement learning and GANs.

If you have any comments or questions on my post, feel free to contact me!  Also, feel free to use my code (link to GitHub repo) or share this post with your peers on social platforms of your choice.

If you’re interested in more content like this, join our mailing list, constantly bringing you fresh data science, machine learning and AI reads and treats from me and my team at STATWORX right into your inbox!

Lastly, follow me on LinkedIn or my company STATWORX on Twitter, if you’re interested in more!

In a recent project at STATWORX, I’ve developed a large scale deep learning application for image classification using Keras and Tensorflow. After developing the model, we needed to deploy it in a quite complex pipeline of data acquisition and preparation routines in a cloud environment. We decided to deploy the model on a prediction server that exposes the model through an API. Thereby, we came across NVIDIA TensorRT Server (TRT Server), a serious alternative to good old TF Serving (which is an awesome product, by the way!). After checking the pros and cons, we decided to give TRT Server a shot. TRT Server has sevaral advantages over TF Serving, such as optimized inference speed, easy model management and ressource allocation, versioning and parallel inference handling. Furthermore, TensorRT Server is not „limited“ to TensorFlow (and Keras) models. It can serve models from all major deep learning frameworks, such as TensorFlow, MxNet, pytorch, theano, Caffe and CNTK.

Despite the load of cool features, I found it a bit cumbersome to set up the TRT server. The installation and documentation is scattered to quite a few repositories, documetation guides and blog posts. That is why I decided to write this blog post about setting up the server and get your predictions going!

NVIDIA TensorRT Server

TensorRT Inference Server is NVIDIA’s cutting edge server product to put deep learning models into production. It is part of the NVIDIA’s TensorRT inferencing platform and provides a scaleable, production-ready solution for serving your deep learning models from all major frameworks. It is based on NVIDIA Docker and contains everything that is required to run the server from the inside of the container. Furthermore, NVIDIA Docker allows for using GPUs inside a Docker container, which, in most cases, significantly speeds up model inference. Talking about speed – TRT Server can be considerably faster than TF Serving and allows for multiple inferences from multiple models at the same time, using CUDA streams to exploit GPU scheduling and serialization (see image below).

Visualization of model serialization and parallelism

With TRT Server you can specify the number of concurrent inference computations using so called instance groups that can be configured on the model level (see section „Model Configuration File“) . For example, if you are serving two models and one model gets significantly more inference requests, you can assign more GPU ressources to this model allowing you to compute more multiple requests in parallel. Furthermore, instance groups allow you to specify, whether a model should be executed on CPU or GPU, which can be a very interesting feature in more complex serving environments. Overall, TRT Server has a bunch of great features that makes it interesting for production usage.

NVIDIA architecture

The upper image illustrates the general architecture of the server. One can see the HTTP and gRPC interfaces that allow you to integrate your models into other applications that are connected to the server over LAN or WAN. Pretty cool! Furthermore, the server exposes a couple of sanity features such as health status checks etc., that also come in handy in production.

Setting up the Server

As mentioned before, TensorRT Server lives inside a NVIDIA Docker container. In order to get things going, you need to complete several installation steps (in case you are starting with a blank machine, like here). The overall process is quite long and requires a certain amount of „general cloud, network and IT knowledge“. I hope, that the following steps make the installation and setup process clear to you.

Launch a Deep Learning VM on Google Cloud

For my project, I used a Google Deep Learning VM that comes with preinstalled CUDA as well as TensorFlow libraries. You can launch a cloud VM using the Google Cloud SDK or in the GCP console (which is pretty easy to use, in my opinion). The installation of the GCP SDK can be found here. Please note, that it might take some time until you can connect to the server because of the CUDA installation process, which takes several minutes. You can check the status of the VM in the cloud logging console.

# Create project
gcloud projects create tensorrt-server

# Start instance with deep learning image
gcloud compute instances create tensorrt-server-vm 
	--project tensorrt-server 
	--zone your-zone 
	--machine-type n1-standard-4 
	--create-disk='size=50' 
	--image-project=deeplearning-platform-release 
	--image-family tf-latest-gpu 
	--accelerator='type=nvidia-tesla-k80,count=1' 
	--metadata='install-nvidia-driver=True' 
	--maintenance-policy TERMINATE

After successfully setting up your instance, you can SSH into the VM using the terminal. From there you can execute all the neccessary steps to install the required components.

# SSH into instance
gcloud compute ssh tensorrt-server-vm --project tensorrt-server --zone your-zone

Note: Of course, you have to adapt the script for your project and instance names.

Install Docker

After setting up the GCP cloud VM, you have to install the Docker service on your machine. The Google Deep Learning VM uses Debian as OS. You can use the following code to install Docker on the VM.

# Install Docker
sudo apt-get update
sudo apt-get install 
    apt-transport-https 
    ca-certificates 
    curl 
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository 
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu 
   $(lsb_release -cs) 
   stable"
sudo apt-get update
sudo apt-get install docker-ce

You can verify that Docker has been successfully installed by running the following command.

sudo docker run --rm hello-world

You should see a „Hello World!“ from the docker container which should give you something like this:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
d1725b59e92d: Already exists 
Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

Congratulations, you’ve just installed Docker successfully!

Install NVIDIA Docker

Unfortunately, Docker has no „out of the box“ support for GPUs connected to the host system. Therefore, the installation of the NVIDIA Docker runtime is required to use TensorRT Server’s GPU capabilities within a containerized environment. NVIDIA Docker is also used for TF Serving, if you want to use your GPUs for model inference. The following figure illustrates the architecture of the NVIDIA Docker Runtime.

NVIDIA docker

You can see, that the NVIDIA Docker Runtime is layered around the Docker engine allowing you to use standard Docker as well as NVIDIA Docker containers on your system.

Since the NVIDIA Docker Runtime is a proprietary product of NVIDIA, you have to register at NVIDIA GPU Cloud (NGC) to get an API key in order to install and download it. To authenticate against NGC execute the following command in the server command line:

# Login to NGC
sudo docker login nvcr.io

You will be prompted for username and API key. For username you have to enter $oauthtoken, the password is the generated API key. After you have successfully logged in, you can install the NVIDIA Docker components. Following the instructions on the NVIDIA Docker GitHub repo, you can install NVIDIA Docker by executing the following script (Ubuntu 14.04/16.04/18.04, Debian Jessie/Stretch).

# If you have nvidia-docker 1.0 installed: we need to remove it and all existing GPU containers
docker volume ls -q -f driver=nvidia-docker | xargs -r -I{} -n1 docker ps -q -a -f volume={} | xargs -r docker rm -f
sudo apt-get purge -y nvidia-docker

# Add the package repositories
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | 
  sudo apt-key add -
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | 
  sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt-get update

# Install nvidia-docker2 and reload the Docker daemon configuration
sudo apt-get install -y nvidia-docker2
sudo pkill -SIGHUP dockerd

# Test nvidia-smi with the latest official CUDA image
sudo docker run --runtime=nvidia --rm nvidia/cuda:9.0-base nvidia-smi

Installing TensorRT Server

The next step, after successfully installing NVIDIA Docker, is to install TensorRT Server. It can be pulled from the NVIDIA Container Registry (NCR). Again, you need to be authenticated against NGC to perform this action.

# Pull TensorRT Server (make sure to check the current version)
sudo docker pull nvcr.io/nvidia/tensorrtserver:18.09-py3

After pulling the image, TRT Server is ready to be started on your cloud machine. The next step is to create a model that will be served by TRT Server.

Model Deployment

After installing the required technical components and pulling the TRT Server container you need to take care of your model and the deployment. TensorRT Server manages it’s models in a folder on your server, the so called model repository.

Setting up the Model Repository

The model repository contains your exported TensorFlow / Keras etc. model graphs in a specific folder structure. For each model in the model repository, a subfolder with the corresponding model name needs to be defined. Within those model subfolders, the model schema files (config.pbtxt), label definitions (labels.txt) as well as model version subfolders are located. Those subfolders allow you to manage and serve different model versions. The file labels.txt contains strings of the target labels in appropriate order, corresponding to the output layer of the model. Within the version subfolder a file named model.graphdef (the exported protobuf graph) is stored. model.graphdef is actually a frozen tensorflow graph, that is created after exporting a TensorFlow model and needs to be named accordingly.

Remark: I did not manage to get a working serving from a tensoflow.python.saved_model.simple_save() or tensorflow.python.saved_model.builder.SavedModelBuilder() export with TRT Server due to some variable initialization error. We therefore use the „freezing graph“ approach, which converts all TensorFlow variable inside a graph to constants and outputs everything into a single file (which is model.graphdef).

/models
|-   model_1/
|--      config.pbtxt
|--      labels.txt
|--      1/
|---		model.graphdef

Since the model repository is just a folder, it can be located anywhere the TRT Server host has a network connection to. For exmaple, you can store your exported model graphs in a cloud repository or a local folder on your machine. New models can be exported and deployed there in order to be servable through the TRT Server.

Model Configuration File

Within your model repository, the model configuration file (config.pbtxt) sets important parameters for each model on the TRT Server. It contains technical information about your servable model and is required for the model to be loaded properly. There are sevaral things you can control here:

name: "model_1"
platform: "tensorflow_graphdef"
max_batch_size: 64
input [
   {
      name: "dense_1_input"
      data_type: TYPE_FP32
      dims: [ 5 ]
   }
]
output [
   {
      name: "dense_2_output"
      data_type: TYPE_FP32
      dims: [ 2 ]
      label_filename: "labels.txt"
   }
]
instance_group [
   {
      kind: KIND_GPU
      count: 4
   }
]

First, name defines the tag under the model is reachable on the server. This has to be the name of your model folder in the model repository. platform defines the framework, the model was built with. If you are using TensorFlow or Keras, there are two options: (1) tensorflow_savedmodel and tensorflow_graphdef. As mentioned before, I used tensorflow_graphdef (see my remark at the end of the previous section). batch_size, as the name says, controls the batch size for your predictions. input defines your model’s input layer node name, such as the name of the input layer (yes, you should name your layers and nodes in TensorFlow or Keras), the data_type, currently only supporting numeric types, such as TYPE_FP16, TYPE_FP32, TYPE_FP64 and the input dims. Correspondingly, output defines your model’s output layer name, it’s data_type and dims. You can specify a labels.txt file that holds the labels of the output neurons in appropriate order. Since we only have two output classes here, the file looks simply like this:

class_0
class_1

Each row defines a single class label. Note, that the file does not contain any header. The last section instance_group lets you define specific GPU (KIND_GPU)or CPU (KIND_CPU) ressources to your model. In the example file, there are 4 concurrent GPU threads assigned to the model, allowing for four simultaneous predictions.

Building a simple model for serving

In order to serve a model through TensorRT server, you’ll first need – well – a model. I’ve prepared a small script that builds a simple MLP for demonstration purposes in Keras. I’ve already used TRT Server successfully with bigger models such as InceptionResNetV2 or ResNet50 in production and it worked very well.

from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from keras.models import Sequential
from keras.layers import InputLayer, Dense
from keras.callbacks import EarlyStopping, ModelCheckpoint
from keras.utils import to_categorical

# Make toy data
X, y = make_classification(n_samples=1000, n_features=5)

# Make target categorical
y = to_categorical(y)

# Train test split
X_train, X_test, y_train, y_test = train_test_split(X, y)

# Scale inputs
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

# Model definition
model_1 = Sequential()
model_1.add(Dense(input_shape=(X_train.shape[1], ),
                  units=16, activation='relu', name='dense_1'))
model_1.add(Dense(units=2, activation='softmax', name='dense_2'))
model_1.compile(optimizer='adam', loss='categorical_crossentropy')

# Early stopping
early_stopping = EarlyStopping(patience=5)
model_checkpoint = ModelCheckpoint(filepath='model_checkpoint.h5',
                                   save_best_only=True,
                                   save_weights_only=True)
callbacks = [early_stopping, model_checkpoint]

# Fit model and load best weights
model_1.fit(x=X_train, y=y_train, validation_data=(X_test, y_test),
            epochs=50, batch_size=32, callbacks=callbacks)

# Load best weights after early stopping
model_1.load_weights('model_checkpoint.h5')

# Export model
model_1.save('model_1.h5')

The script builds some toy data using sklearn.datasets.make_classification and fits a single layer MLP to the data. After fitting, the model gets saved for further treatment in a separate export script.

Freezing the graph for serving

Serving a Keras (TensorFlow) model works by exporting the model graph as a separate protobuf file (.pb-file extension). A simple way to export the model into a single file, that contains all the weights of the network, is to „freeze“ the graph and write it to disk. Thereby, all the tf.Variables in the graph are converted to tf.constant which are stored together with the graph in a single file. I’ve modified this script for that purpose.

import os
import shutil
import keras.backend as K
import tensorflow as tf
from keras.models import load_model
from tensorflow.python.framework import graph_util
from tensorflow.python.framework import graph_io

def freeze_model(model, path):
    """ Freezes the graph for serving as protobuf """
    # Remove folder if present
    if os.path.isdir(path):
        shutil.rmtree(path)
        os.mkdir(path)
        shutil.copy('config.pbtxt', path)
        shutil.copy('labels.txt', path)
    # Disable Keras learning phase
    K.set_learning_phase(0)
    # Load model
    model_export = load_model(model)
    # Get Keras sessions
    sess = K.get_session()
    # Output node name
    pred_node_names = ['dense_2_output']
    # Dummy op to rename the output node
    dummy = tf.identity(input=model_export.outputs[0], name=pred_node_names)
    # Convert all variables to constants
    graph_export = graph_util.convert_variables_to_constants(
        sess=sess,
        input_graph_def=sess.graph.as_graph_def(),
        output_node_names=pred_node_names)
    graph_io.write_graph(graph_or_graph_def=graph_export,
                         logdir=path + '/1',
                         name='model.graphdef',
                         as_text=False)

# Freeze Model
freeze_model(model='model_1.h5', path='model_1')

# Upload to GCP
os.system('gcloud compute scp model_1 tensorrt-server-vm:~/models/ --project tensorrt-server --zone us-west1-b --recurse')

The freeze_model() function takes the path to the saved Keras model file model_1.h5 as well as the path for the graph to be exported. Furthermore, I’ve enhanced the function in order to build the required model repository folder structure containing the version subfolder, config.pbtxt as well as labels.txt, both stored in my project folder. The function loads the model and exports the graph into the defined destination. In order to do so, you need to define the output node’s name and then convert all variables in the graph to constants using graph_util.convert_variables_to_constants, which uses the respective Keras backend session, that has to be fetched using K.get_session(). Furthermore, it is important to disable the Keras learning mode using K.set_learning_phase(0) prior to export. Lastly, I’ve included a small CLI command that uploads my model folder to my GCP instance to the model repository /models.

Starting the Server

Now that everything is installed, set up and configured, it is (finally) time to launch our TRT prediciton server. The following command starts the NVIDIA Docker container and maps the model repository to the container.

sudo nvidia-docker run --rm --name trtserver -p 8000:8000 -p 8001:8001 
-v ~/models:/models nvcr.io/nvidia/tensorrtserver:18.09-py3 trtserver 
--model-store=/models

--rm removes existing containers of the same name, given by --name. -p exposes ports 8000 (REST) and 8001 (gRPC) on the host and maps them to the respective container ports. -v mounts the model repository folder on the host, which is /models in my case, to the container into /models, which is then referenced by --model-store as the location to look for servable model graphs. If everything goes fine you should see similar console output as below. If you don’t want to see the output of the server, you can start the container in detached model using the -d flag on startup.

===============================
== TensorRT Inference Server ==
===============================

NVIDIA Release 18.09 (build 688039)

Copyright (c) 2018, NVIDIA CORPORATION.  All rights reserved.
Copyright 2018 The TensorFlow Authors.  All rights reserved.

Various files include modifications (c) NVIDIA CORPORATION.  All rights reserved.
NVIDIA modifications are covered by the license terms that apply to the underlying
project or file.

NOTE: The SHMEM allocation limit is set to the default of 64MB.  This may be
   insufficient for the inference server.  NVIDIA recommends the use of the following flags:
   nvidia-docker run --shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864 ...

I1014 10:38:55.951258 1 server.cc:631] Initializing TensorRT Inference Server
I1014 10:38:55.951339 1 server.cc:680] Reporting prometheus metrics on port 8002
I1014 10:38:56.524257 1 metrics.cc:129] found 1 GPUs supported power usage metric
I1014 10:38:57.141885 1 metrics.cc:139]   GPU 0: Tesla K80
I1014 10:38:57.142555 1 server.cc:884] Starting server 'inference:0' listening on
I1014 10:38:57.142583 1 server.cc:888]  localhost:8001 for gRPC requests
I1014 10:38:57.143381 1 server.cc:898]  localhost:8000 for HTTP requests
[warn] getaddrinfo: address family for nodename not supported
[evhttp_server.cc : 235] RAW: Entering the event loop ...
I1014 10:38:57.880877 1 server_core.cc:465] Adding/updating models.
I1014 10:38:57.880908 1 server_core.cc:520]  (Re-)adding model: model_1
I1014 10:38:57.981276 1 basic_manager.cc:739] Successfully reserved resources to load servable {name: model_1 version: 1}
I1014 10:38:57.981313 1 loader_harness.cc:66] Approving load for servable version {name: model_1 version: 1}
I1014 10:38:57.981326 1 loader_harness.cc:74] Loading servable version {name: model_1 version: 1}
I1014 10:38:57.982034 1 base_bundle.cc:180] Creating instance model_1_0_0_gpu0 on GPU 0 (3.7) using model.savedmodel
I1014 10:38:57.982108 1 bundle_shim.cc:360] Attempting to load native SavedModelBundle in bundle-shim from: /models/model_1/1/model.savedmodel
I1014 10:38:57.982138 1 reader.cc:31] Reading SavedModel from: /models/model_1/1/model.savedmodel
I1014 10:38:57.983817 1 reader.cc:54] Reading meta graph with tags { serve }
I1014 10:38:58.041695 1 cuda_gpu_executor.cc:890] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
I1014 10:38:58.042145 1 gpu_device.cc:1405] Found device 0 with properties: 
name: Tesla K80 major: 3 minor: 7 memoryClockRate(GHz): 0.8235
pciBusID: 0000:00:04.0
totalMemory: 11.17GiB freeMemory: 11.10GiB
I1014 10:38:58.042177 1 gpu_device.cc:1455] Ignoring visible gpu device (device: 0, name: Tesla K80, pci bus id: 0000:00:04.0, compute capability: 3.7) with Cuda compute capability 3.7. The minimum required Cuda capability is 5.2.
I1014 10:38:58.042192 1 gpu_device.cc:965] Device interconnect StreamExecutor with strength 1 edge matrix:
I1014 10:38:58.042200 1 gpu_device.cc:971]      0 
I1014 10:38:58.042207 1 gpu_device.cc:984] 0:   N 
I1014 10:38:58.067349 1 loader.cc:113] Restoring SavedModel bundle.
I1014 10:38:58.074260 1 loader.cc:148] Running LegacyInitOp on SavedModel bundle.
I1014 10:38:58.074302 1 loader.cc:233] SavedModel load for tags { serve }; Status: success. Took 92161 microseconds.
I1014 10:38:58.075314 1 gpu_device.cc:1455] Ignoring visible gpu device (device: 0, name: Tesla K80, pci bus id: 0000:00:04.0, compute capability: 3.7) with Cuda compute capability 3.7. The minimum required Cuda capability is 5.2.
I1014 10:38:58.075343 1 gpu_device.cc:965] Device interconnect StreamExecutor with strength 1 edge matrix:
I1014 10:38:58.075348 1 gpu_device.cc:971]      0 
I1014 10:38:58.075353 1 gpu_device.cc:984] 0:   N 
I1014 10:38:58.083451 1 loader_harness.cc:86] Successfully loaded servable version {name: model_1 version: 1}

There is also a warning showing that you should start the container using the following arguments

--shm-size=1g --ulimit memlock=-1 --ulimit stack=67108864

You can do this of course. However, in this example I did not use them.

Installing the Python Client

Now it is time to test our prediction server. TensorRT Server comes with several client libraries that allow you to send data to the server and get predictions. The recommended method of building the client libraries is again – Docker. To use the Docker container, that contains the client libraries, you need to clone the respective GitHub repo using:

git clone https://github.com/NVIDIA/dl-inference-server.git

Then, cd into the folder dl-inference-server and run

docker build -t inference_server_clients .

This will build the container on your machine (takes some time). To use the client libraries within the container on your host, you need to mount a folder to the container. First, start the container in an interactive session (-it flag)

docker run --name tensorrtclient --rm -it -v /tmp:/tmp/host inference_server_clients

Then, run the following commands in the container’s shell (you may have to create /tmp/host first):

cp build/image_client /tmp/host/.
cp build/perf_client /tmp/host/.
cp build/dist/dist/tensorrtserver-*.whl /tmp/host/.
cd /tmp/host

The code above copies the prebuilt image_client and perf_client libraries into the mounted folder and makes it accessible from the host system. Lastly, you need to install the Python client library using

pip install tensorrtserver-0.6.0-cp35-cp35m-linux_x86_64.whl

on the container system. Finally! That’s it, we’re ready to go (sounds like it was an easy way)!

Inference using the Python Client

Using Python, you can easily perform predictions using the client library. In order to send data to the server, you need an InferContext() from the inference_server.api module that takes the TRT Server IP and port as well as the desired model name. If you are using the TRT Server in the cloud, make sure, that you have appropriate firewall rules allowing for traffic on ports 8000 and 8001.

from tensorrtserver.api import *
import numpy as np

# Some parameters
outputs = 2
batch_size = 1

# Init client
trt_host = '123.456.789.0:8000' # local or remote IP of TRT Server
model_name = 'model_1'
ctx = InferContext(trt_host, ProtocolType.HTTP, model_name)

# Sample some random data
data = np.float32(np.random.normal(0, 1, [1, 5]))

# Get prediction
# Layer names correspond to the names in config.pbtxt
response = ctx.run(
    {'dense_1_input': data}, 
    {'dense_2_output': (InferContext.ResultFormat.CLASS, outputs)},
    batch_size)

# Result
print(response)
{'output0': [[(0, 1.0, 'class_0'), (1, 0.0, 'class_1')]]}

Note: It is important that the data you are sending to the server matches the floating point precision, previously defined for the input layer in the model definition file. Furthermore, the names of the input and output layers must exactly match those of your model. If everything went well, ctx.run() returns a dictionary of predicted values, which you would further postprocess according to your needs.

Conclusion and Outlook

Wow, that was quite a ride! However, TensorRT Server is a great product for putting your deep learning models into production. It is fast, scaleable and full of neat features for production usage. I did not go into details regarding inference performance. If you’re interested in more, make sure to check out this blog post from NVIDIA. I must admit, that in comparison to TRT Server, TF Serving is much more handy when it comes to installation, model deployment and usage. However, compared to TRT Server it lacks some functionalities that are handy in production. Bottom line: my team and I will definitely add TRT Server to our production tool stack for deep learning models.

If you have any comments or questions on my story, feel free to comment below! I will try to answer them. Also, feel free to use my code or share this story with your peers on social platforms of your choice.

If you’re interested in more content like this, join our mailing list, constantly bringing you new data science, machine learning and AI reads and treats from me and my team at STATWORX right into your inbox!

Lastly, follow me on LinkedIn or Twitter, if you’re interested to connect with me.

References

Last Christmas is one of the most popular Christmas tunes that were, are and will be out there. The song is written by the brilliant musician George Michael and was released in 1984, when at that time, Epic Records quickly wanted to release a Christmas tune. According to Wikipedia, there are rumours going around that George Michael just changed the lyrics of an already composed tune named „last easter“ in a more „winterly“ manner. However, this has officially never been confirmed by the record company. Nonetheless, „Last Christmas“ remains at the top of all christmas pop songs – also at STATWORX, where „Last Christmas“ is on heavy rotation during the holiday season!

george michael meme

In a recent meme on the web I saw, that the Google Trends search volume for „last cristmas“ beginning to kick in (first week of october), indicating that Christmas (and the voice of George Michael, backed by christmas bells) is knocking at the door. In order to get ready for the „most wonderful time of the year“, I decided to build a small neural network in Keras that is able to perform a multistep forecast of the expected „last christmas“ search volume this year (that maybe correlates with the number of plays on TV, radio etc.).

google trends last christmas

The screenshot above shows the normalized search volume (range between 0 and 100) for last christmas for Germany from 2004 to October 2018. In winter 2017 there was the alltime high in search traffic for „Last Christmas“, maybe due to the tragic death of Michael on December 25th in 2016.

Data Preparation

I’ve downloaded the search volume data from Google Trends as a CSV file and manually formatted the file header (it included a description of the data as well as some blank lines) as well as string values of „<1“, which I’ve replace with numeric zeros. After preparing the file, I imported it into Python using pandas.read_csv().

Since neural networks work best with scaled data, i.e. data that ranges between a specific lower and upper bound, e.g. 0 and 1 or -1 and 1, I divided the normalized search volume by 100, y_{norm}=y/100.

There are many neural network architectures that can be used in order to perform time series forecasting. Since the data is modeled in a simple input-output-style, of course, MLPs can be used. Furthermore, 1-dimensional convolutional networks can be employed. Last but not least, LSTMs are also applicable.

Multistep forecasting can be done in several ways: (1) building a separate forecasting model for each forecast timestep, (2) building a recursive AR(p) model, that predicts the next value based on previous predictions and (3) building a model that is able to predict multiple values into the future at the same time. Since neural networks can easily handle multiple outputs, I decided to go with neural networks.

Preparing data for multistep forecasting can be a bit cumbersome, especially, when the input data consists on multiple timesteps and variables. In order to prepare my X and y data, I used the following snippet:

def prepare_data(target, window_X, window_y):
    """ Data preprocessing for multistep forecast """
    # Placeholders
    X, y = [], []
    n = len(target)
    # Iterators
    start_X = 0
    end_X = start_X + window_X
    start_y = end_X
    end_y = start_y + window_y
    # Build tensors
    for _ in range(n):
        if end_y < n:
            X.append(target[start_X:end_X])
            y.append(target[start_y:end_y])
        start_X += 1
        end_X = start_X + window_X
        start_y += 1
        end_y = start_y + window_y
    # Convert to array
    X = np.array(X)
    y = np.array(y)
    # Return
    return np.array(X), np.array(y)

The function transforms a single vector of values, target, into two 2-dimensional tensors X and y. The function arguments window_Xand window_y define the number of input lags per observation and the number of output values (timesteps to be predicted), respectively. Let’s take a look at the tensors. X[:3] yields:

array([[ 2,  1,  0,  0,  1,  1,  1,  1,  2,  4, 21, 69],
       [ 1,  0,  0,  1,  1,  1,  1,  2,  4, 21, 69,  1],
       [ 0,  0,  1,  1,  1,  1,  2,  4, 21, 69,  1,  1]])

whereas y[:3] yields:

array([[1, 1, 1, 1, 0, 0],
       [1, 1, 1, 0, 0, 1],
       [1, 1, 0, 0, 1, 1]])

One can see, that the tensors contain iterating data over the vector target of a fixed length. Specifically, I used window_x = 12 and window_y = 6 which means, that each row in our input tensor X consists of window_X = 12 elements that are used to forecast the next window_y = 6 timesteps. Note, that the original time series contains T=178 months of data, whereas X is of shape (160, 12). The reason for this is that values only get appended to the X and y tensors, if the current end_y iterator is smaller than the number of total obervations in the dataset. Otherwise, y would contain NaN values at the end of the series.

# Training and test
train = 100
X_train = X[:train]
y_train = y[:train]
X_valid = X[train:]
y_valid = y[train:]

For model training, I used the first 100 observations for training purposes and the remaining 60 observations for validation (early stopping). Furthermore, I built a tensor for prediction, X_test that contains the last 12 observations from the time series (November, 1st. 2017 – October, 1st. 2018) in order to compute a prediction for the following 6 months.

Model Building

The following Python snippet shows the function for fitting three neural network models: a simple MLP, a 1-dimensional CNN and a LSTM (I will not go into detail about how the models exactly work – there are tons of great tutorials on the web). I wrote a single function for all three architectures. Thereby, I exploited a litte trick I recently discovered: if you’re experimenting with different network archirectures you always have to make sure, that the input tensors are of the appropriate shape. Since MLPs, CNNs and LSTMs require different dimensions of input tensors, it can be helpful to provide a single input layer that takes the original input tensor and then add a Reshape() layer in order to reshape the input into the required dimensionality of the respective Dense(), Conv() or LSTM() layer. I know, that from a computational point of view, this is not very efficient, however, I do not care in this case 😉

def fit_model(type, X_train, y_train, X_test, y_test, batch_size, epochs):
    """ Training function for network """
    
    # Model input
    model = Sequential()
    model.add(InputLayer(input_shape=(X_train.shape[1], )))

    if type == 'mlp':
        model.add(Reshape(target_shape=(X_train.shape[1], )))
        model.add(Dense(units=64, activation='relu'))

    if type == 'cnn':
        model.add(Reshape(target_shape=(X_train.shape[1], 1)))
        model.add(Conv1D(filters=64, kernel_size=4, activation='relu'))
        model.add(MaxPool1D())
        model.add(Flatten())

    if type == 'lstm':
        model.add(Reshape(target_shape=(X_train.shape[1], 1)))
        model.add(LSTM(units=64, return_sequences=False))

    # Output layer
    model.add(Dense(units=64, activation='relu'))
    model.add(Dense(units=y_train.shape[1], activation='sigmoid'))

    # Compile
    model.compile(optimizer='adam', loss='mse')

    # Callbacks
    early_stopping = EarlyStopping(monitor='val_loss', patience=10)
    model_checkpoint = ModelCheckpoint(filepath='model.h5', save_best_only=True)
    callbacks = [early_stopping, model_checkpoint]

    # Fit model
    model.fit(x=X_train, y=y_train, validation_data=(X_test, y_test),
              batch_size=batch_size, epochs=epochs,
              callbacks=callbacks, verbose=2)

    # Load best model
    model.load_weights('model.h5')

    # Return
    return model

The InputLayer() consumes the prepared input tensor X_train. Depending on the type of model, distinct reshape operations are carried out in order to provide proper tensor dimensions for the model. Thereby, the MLP requires the input tensor to be 2-dimensional whereas the CNN and LSTM models require 3-dimensional input tensors. After the respective model architecture layers, another Dense() layer and the output layer are added. Note, that I’ve used a sigmoidal activation for the output. This makes sure, that the predicted values of the network range between 0 and 1, which corresponds to the normalized search volume data (another cool trick for predicting normalized outcomes or percentages). Dense activations are set to ReLU, optimization is performed using the ADAM optimizer. The model uses early stopping to stop model training when the validation error does not improve for 10 consecutive epochs. Then, thanks to model checkpointing, the best model is reloaded and returned.

Prediction

After model training, the predictions for the following 6 months are generated by feeding X_test into the respective model architecture. The following table shows the predicted values:

month MLP CNN LSTM
2018-11 0.165803 0.193593 0.214891
2018-12 0.857727 0.881791 0.817105
2019-01 0.034604 0.034484 0.034604
2019-02 0.007150 0.002432 0.007150
2019-03 0.012865 0.000508 0.012865
2019-04 0.013644 0.000502 0.013644

Overall, the predictions look quite similar between the models. However, there are certain systematic differences:

  • the CNN model predicts the highest search volume in december
  • the MLP seems to overestimate the search volume after the holiday season
  • the LSTM model shows the highest prediction in November and January but the lowest prediction in December

The following plot shows the search volume as well as the predictions for all three models from April 2016 to April 2019:

predictions last christmas

Conclusion and Outlook

Based on the predictions, there is an expected minor decline in search interest. This might be a predictor for lower „Last Christmas“ song penetration. However, I did not find any data on radio plays etc. Of course, the model can be improved by incorporating further information into the network. Anyways, I had great fun building the model and working with the search volume data. In the next step, I will check in November, which model performed best when new actual data comes in (beginning of November). Besides this dataset, Google Trends is a great ressource to include external information into your models. For example, in a recent use case at STATWORX, we used Google Trends data to incorporate search interest of specific products into our sales forecasting models. You should give it a try!

If you want to play around with the data or the model, you can find everything on our GitHub repository.

If you have any comments or questions on my blog post, contact me, I will try to answer them. Also, feel free to use my code or share this story with your peers on social platforms of your choice. Follow me on LinkedIn or Twitter, if you want to stay in touch.

Make sure, you frequently check the awesome STATWORX Blog for more interesting data science, ML and AI content straight from the our office in Frankfurt, Germany!

If you’re interested in more quality content like this, join my mailing list, constantly bringing you new data science, machine learning and AI reads and treats from me and my team right into your inbox!

Google AutoML Vision is a state-of-the-art cloud service from Google that is able to build deep learning models for image recognition completely fully automated and from scratch. In this post, Google AutoML Vision is used to build an image classification model on the Zalando Fashion-MNIST dataset, a recent variant of the classical MNIST dataset, which is considered to be more difficult to learn for ML models, compared to digit MNIST.

During the benchmark, both AutoML Vision training modes, „free“ (0 $, limited to 1 hour computing time) and „paid“ (approx. 500 $, 24 hours computing time) were used and evaluated:

Thereby, the free AutoML model achieved a macro AUC of 96.4% and an accuracy score of 88.9% on the test set at a computing time of approx. 30 minutes (early stopping). The paid AutoML model achieved a macro AUC of 98.5% on the test set with an accuracy score of 93.9%.

Introduction

Recently, there is a growing interest in automated machine learning solutions. Products like H2O Driverless AI or DataRobot, just to name a few, aim at corporate customers and continue to make their way into professional data science teams and environments. For many use cases, AutoML solutions can significantly speed up time-2-model cycles and therefore allow for faster iteration and deployment of models (and actually start saving / making money in production).

Automated machine learning solutions will transform the data science and ML landscape substantially in the next 3-5 years. Thereby, many ML models or applications that nowadays require respective human input or expertise will likely be partly or fully automated by AI / ML models themselves. Likely, this will also yield a decline in overall demand for „classical“ data science profiles in favor of more engineering and operations related data science roles that bring models into production.

A recent example of the rapid advancements in automated machine learning this is the development of deep learning image recognition models. Not too long ago, building an image classifier was a very challenging task that only few people were acutally capable of doing. Due to computational, methodological and software advances, barriers have been dramatically lowered to the point where you can build your first deep learning model with Keras in 10 lines of Python code and getting „okayish“ results.

Undoubtly, there will still be many ML applications and cases that cannot be (fully) automated in the near future. Those cases will likely be more complex because basic ML tasks, such as fitting a classifier to a simple dataset, can and will easily be automated by machines.

At this point, first attempts in moving into the direction of machine learning automation are made. Google as well as other companies are investing in AutoML research and product development. One of the first professional automated ML products on the market is Google AutoML Vision.

Google AutoML Vision

Google AutoML Vision (at this point in beta) is Google’s cloud service for automated machine learning for image classification tasks. Using AutoML Vision, you can train and evaluate deep learning models without any knowledge of coding, neural networks or whatsoever.

AutoML Vision operates in the Google Cloud and can be used either based on a graphical user interface or via, REST, command line or Python. AutoML Vision implements strategies from Neural Architecture Search (NAS), currently a scientific field of high interest in deep learning research. NAS is based on the idea that another model, typically a neural network or reinforcement learning model, is designing the architecture of the neural network that aims to solve the machine learning task. Cornerstones in NAS research were the paper from Zoph et at. (2017) as well as Pham et al. (2018). The latter has also been implemented in the Python package autokeras (currently in pre-release phase) and makes neural architecture search feasible on desktop computers with a single GPU opposed to 500 GPUs used in Zoph et al.

The idea that an algorithm is able to discover architectures of a neural network seems very promising, however is still kind of limited due to computational contraints (I hope you don’t mind that I consider a 500-1000 GPU cluster as as computational contraint). But how good does neural architecture search actually work in a pre-market-ready product?

Benchmark

In the following section, Google AutoML vision is used to build an image recognition model based on the Fashion-MNIST dataset.

Dataset

The Fashion-MNIST dataset is supposed to serve as a „drop-in replacement“ for the traditional MNIST dataset and has been open-sourced by Europe’s online fashion giant Zalando’s research department (check the Fashion-MNIST GitHub repo and the Zalando reseach website). It contains 60,000 training and 10,000 test images of 10 different clothing categories (tops, pants, shoes etc.). Just like in MNIST, each image is a 28×28 grayscale image. It shares the same image size and structure of training and test images. Below are some examples from the dataset:

The makers of Fashion-MNIST argue, that nowadays the traditional MNIST dataset is a too simple task to solve – even simple convolutional neural networks achieve >99% accuracy on the test set whereas classical ML algorithms easily score >97%. For this and other reasons, Fashion-MNIST was created.

The Fashion-MNIST repo contains helper functions for loading the data as well as some scripts for benchmarking and testing your models. Also, there’s a neat visualization of an ebmedding of the data on the repo. After cloning, you can import the Fashion-MNIST data using a simple Python function (check the code in the next section) and start to build your model.

Using Google AutoML Vision

Preparing the data

AutoML offers two ways of data ingestion: (1) upload a zip file that contains the training images in different folders, corresponding to the respective labels or (2) upload a CSV file that contains the Goolge cloud storage (GS) filepaths, labels and optionally the data partition for training, validation and test set. I decided to go with the CSV file because you can define the data partition (flag names are TRAIN, VALIDATION and TEST) in order to keep control over the experiment. Below is the required structure of the CSV file that needs to be uploaded to AutoML Vision (without the header!).

partition file label
TRAIN gs://bucket-name/folder/image_0.jpg 0
TRAIN gs://bucket-name/folder/image_1.jpg 2
VALIDATION gs://bucket-name/folder/image_22201.jpg 7
VALIDATION gs://bucket-name/folder/image_22202.jpg 9
TEST gs://bucket-name/folder/image_69998.jpg 4
TEST gs://bucket-name/folder/image_69999.jpg 1

Just like MNIST, Fashion-MNIST data contains the pixel values of the respective images. To actually upload image files, I developed a short python script that takes care of the image creation, export and upload to GCP. The script iterates over each row of the Fashion-MNIST dataset, exports the image and uploads it into a Google Cloud storage bucket.

import os
import gzip
import numpy as np
import pandas as pd
from google.cloud import storage
from keras.preprocessing.image import array_to_img


def load_mnist(path, kind='train'):
    """Load MNIST data from `path`"""
    labels_path = os.path.join(path,
                               '%s-labels-idx1-ubyte.gz'
                               % kind)
    images_path = os.path.join(path,
                               '%s-images-idx3-ubyte.gz'
                               % kind)

    with gzip.open(labels_path, 'rb') as lbpath:
        labels = np.frombuffer(lbpath.read(), dtype=np.uint8,
                               offset=8)

    with gzip.open(images_path, 'rb') as imgpath:
        images = np.frombuffer(imgpath.read(), dtype=np.uint8,
                               offset=16).reshape(len(labels), 784)

    return images, labels


# Import training data
X_train, y_train = load_mnist(path='data', kind='train')
X_test, y_test = load_mnist(path='data', kind='t10k')

# Split validation data
from sklearn.model_selection import train_test_split
X_train, X_valid, y_train, y_valid = train_test_split(X_train, y_train, test_size=10000)

# Dataset placeholder
files = pd.DataFrame({'part': np.concatenate([np.repeat('TRAIN', 50000),
                                              np.repeat('VALIDATION', 10000),
                                              np.repeat('TEST', 10000)]),
                      'file': np.repeat('file', 70000),
                      'label': np.repeat('label', 70000)})

# Stack training and test data into single arrays
X_data = np.vstack([X_train, X_valid, X_test])
y_data = np.concatenate([y_train, y_valid, y_test])

# GS path
gs_path = 'gs://secret/fashionmnist'

# Storgae client
storage_client = storage.Client.from_service_account_json(json_credentials_path='secret.json')
bucket = storage_client.get_bucket('secret-bucket')

# Fill matrix
for i, x in enumerate(X_data):
    # Console print
    if i % 1000 == 0:
        print('Uploading image {image}'.format(image=i))
    # Reshape and export image
    img = array_to_img(x=x.reshape(28, 28, 1))
    img.save(fp='fashionmnist' + '/' + 'image_' + str(i) + '.jpg')
    # Add info to data frame
    files.iloc[i, 1] = gs_path + '/' + 'image_' + str(i) + '.jpg'
    files.iloc[i, 2] = y_data[i]
    # Upload to GCP
    blob = bucket.blob('fashionmnist/' + 'image_' + str(i) + '.jpg')
    blob.upload_from_filename('fashionmnist/' + 'image_' + str(i) + '.jpg')
    # Delete image file
    os.remove('fashionmnist/' + 'image_' + str(i) + '.jpg')

# Export CSV file
files.to_csv(path_or_buf='fashionmnist.csv', header=False, index=False)

The function load_mnist is from the Fashion-MNIST repository and imports the training and test arrays into Python. After importing the training set, 10,000 examples are sampled and sotored as validation data using train_test_split from sklean.model_selection. The training, validation and test arrays are then stacked into X_data in order to have a single object for iteration. A placeholder DataFrame is initialized to store the required information (partition, filepath and label), required by AutoML Vision. storage from google.cloud connects to GCP using a service account json file (which I will, of course, not share here). Finally, the main process takes place, iterating over X_data, generating an image for each row, saving it to disk, uploading it to GCP and deleting the image since it is no longer needed. Lastly, I uploaded the exported CSV file into the Google Cloud storage bucket of the project.

Getting into AutoML

AutoML Vision is currently in Beta, which means that you have to apply before trying it out. Since me and my colleagues are currently exploring the usage of automated machine learning in a computer vision project for one of our customers, I already have access to AutoML Vision through the GCP console.

The start screen looks pretty unspectacular at this point. You can start by clicking on „Get started with AutoML“ or read the documentation, which is pretty basic so far but informative, especially when you’re not familiar with basic machine learning concepts such as train-test-splits, overfitting, prcision / recall etc.

After you started, Google AutoML takes you to the dataset dialog, which is the first step on the road to the final AutoML model. So far, nothing to report here. Later, you will find here all of your imported datasets.

Generating the dataset

After hitting „+ NEW DATASET“ AutoML takes you to the „Create dataset“ dialog. As mentioned before, new datasets can be added using two different methods, shown in the next image.

I’ve already uploaded the images from my computer as well as the CSV file containing the GS filepaths, partition information as well as the corresponding labels into the GS bucket. In order to add the dataset to AutoML Vision you must specify the filepath to the CSV file that contains the image GS-filepaths etc.

In the „Create dataset“ dialog, you can also enable multi-label classification, if you have multiple labels per image, which is also a very helpful feature. After hitting „CREATE DATASET“, AutoML iterates over the provided file names and builds the dataset for modeling. What exactly is does, is neither visible nor documented. This import process may take a while, so it is showing you the funky „Knight Rider“ progress bar.

After the import is finished, you will recieve an email from GCP, informing you that the import of the dataset is completed. I find this helpful because you don’t have to keep the browser window open and stare at the progress bar all the time.

The email looks a bit weird, but hey, it’s still beta…

Training a model

Back to AutoML. The first thing you see after building your dataset are the imported images. In this example, the images are a bit pixelated because they are only 28×28 in resolution. You can navigate through the different labels using the nav bar on the left side and also manually add labels so far unlabeled images. Furthermore, you can request a human labeling service if you do not have any labels that come with your images. Additionally, you can create new labels if you need to add a category etc.

Now let’s get serious. After going to the „TRAIN“ dialog, AutoML informs you about the frequency distribution of your labels. It recommends a minimum count of $n=100$ labels per class (which I find quite low). Also, it seems that it shows you the frequencies of the whole dataset (train, validation and test together). A grouped frquency plot by data partition would be more informative at this point, in my opinion.

A click on „start training“ takes you to a popup window where you can define the model name and the allocate a training budget (computing time / money) you are willing to invest. You have the choice between „1 compute hour“, whis is free for 10 models every month, or „24 compute hours (higher quality)“ that comes with a price tag of approx. 480 $ (1 hour of AutoML computing costs 20 $. Hovever, if the architecture search converges at an earlier point, you will only pay the amount of computing time that has been consumed so far, which I find reasonable and fair. Lastly, there is also the option to define a custom training time, e.g. 5 hours.

In this experiment, I tried both, the „free“ version of AutoML but I also went „all-in“ and seleced the 24 hours option to achieve the best model possible („paid model“). Let’s see, what you can expect from a 480 $ cutting edge AutoML solution. After hitting „START TRAINING“ the familiar Knight Rider screen appears telling you, that you can close the browser window and let AutoML do the rest. Naise.

Results and evaluation

First, let’s start with the free model. It took approx. 30mins of training and seemed to have converged a solution very quickly. I am not sure, what exactly AutoML does when it evaluates convergence criteria but it seems to be different between the free and paid model, because the free model converged already around 30 minutes of computing and the paid model did not.

The overall model metrics of the free model look pretty decent. An average precision of 96.4% on the testset at a macro class 1 presision of 90.9% and a recall of 87.7%. The current accuracy benchmark on the Fashion-MNIST dataset is at 96.7% (WRN40-4 8.9M parameters) followed by 96.3% (WRN-28-10 + Random Erasing) while the accuracy of the low budget model is only at 89.0%. So the free AutoML model is pretty far away from the current Fashion-MNIST benchmark. Below, you’ll find the screenshot of the free model’s metrics.

The model metrics of the paid model look significantly better. It achieved an average precision of 98.5% on the testset at a macro class 1 presision of 95.0% and a recall of 92.8% as well as an accuracy score of 93.9%. Those results are close to the current benchmark, however, not so close as I hoped. Below, you’ll find the screenshot of the paid model’s metrics.

The „EVALUATE“ tab also shows you further detailed metrics such as precision / recall curves as well as sliders for classification cutoffs that impact the model metrics respectively. At the bottom of the page you’ll find the confusion matrix with relative freuqencies of correct and misclassified examples. Furthermore, you can check images of false positives and negatives per class (which is very helpful, if you want to understand why and when your model is doing something wrong). Overall, the model evaluation functionalities are limited but user friendly. As a more profound user, of course, I would like to see more advanced features but considering the target group and the status of development I think it is pretty decent.

Prediction

After fitting and evaluating your model you can use several methods to predict new images. First, you can use the AutoML user interface to upload new images from your local machine. This is a great way for unexperienced users to apply their model to new images and get predictions. For advanced users and developers, AutoML vision exposes the model through an API on the GCP while taking care of all the technical infrastructure in the background. A simple Python script shows the basic usage of the API:

import sys
from google.cloud import automl_v1beta1


# Define client from service account json
client = automl_v1beta1.PredictionServiceClient.from_service_account_json(filename='automl-XXXXXX-d3d066fe6f8c.json')

# Endpoint
name = 'projects/automl-XXXXXX/locations/us-central1/models/ICNXXXXXX

# Import a single image
with open('image_10.jpg', 'rb') as ff:
    img = ff.read()

# Define payload
payload = {'image': {'image_bytes': img}}

# Prediction
request = client.predict(name=name, payload=payload, params={})
print(request)

# Console output
payload {
  classification {
    score: 0.9356002807617188
  }
  display_name: "a_0"
}

As a third method, it is also possible to curl the API in the command line, if you want to go full nerdcore. I think, the automated API exposure is a great feature because it lets you integrate your model in all kinds of scripts and applications. Furthermore, Google takes care of all the nitty-gritty things that come into play when you want to scale the model to hundrets or thousands of API requests simultaneously in a production environment.

Conclusion and outlook

In a nutshell, even the free model achieved pretty good results on the test set, given that the actual amount of time invested in the model was only a fraction of time it would have taken to build the model manually. The paid model achieved significantly better results, however at a cost note of 480 $. Obviously, the paid service is targeted at data science professionals and companies.

AutoML Vision is only a part of a set of new AutoML applications that come to the Google Cloud (check these announcements from Google Next 18), further shaping the positioning of the platform in the direction of machine learning and AI.

In my personal opinion, I am confident that automated machine learning solutions will continue to make their way into professional data science projects and applications. With automated machine learning, you can (1) build baseline models for benchmarking your custom solutions, (2) iterate use cases and data products faster and (3) get quicker to the point, when you actually start to make money with your data – in production.

Für Außenstehende umgeben neuronale Netze eine mystische Aura. Obwohl die Funktionsweise der elementaren Bausteine neuronaler Netze, Neuronen genannt, bereits seit vielen Jahrzehnten bekannt sind, stellt das Training von neuronalen Netzen Anwender auch heute noch vor Herausforderungen. Insbesondere im Bereich Deep Learning, in dem sehr tiefe oder anderweitig komplexe Netzarchitekturen geschätzt werden, spielt die Art und Weise wie das Netz aus den vorhandenen Daten lernt, eine zentrale Rolle für den Erfolg des Trainings.

In diesem Beitrag sollen die beiden grundlegenden Bausteine des Lernens von neuronalen Netzen beleuchtet werden: (1) Gradient Descent, eine iterative Methode zur Minimierung von Funktionen sowie (2) Backpropagation, ein Verfahren, mit dem in neuronalen Netzen die Stärke und Richtung der Anpassungen der Modellparameter berechnet werden können. Im Zusammenspiel beider Methoden sind wir heute in der Lage, verschiedenste Modelltypen und Architekturen zu entwickeln und auf vorhandenen Daten zu trainieren.

Formaler Aufbau eines Neurons

Ein Neuron j berechnet seinen Output o_j als gewichtete Summe der Eingangssignale x, die anschließend durch die sog. Aktivierungsfunktion des Neurons, g(x) transformiert wird. Je nachdem, welche Aktivierungsfunktion für g() gewählt wird, verändert sich der funktionale Output des Neurons. Die folgende Abbildung soll den Aufbau eines einzelnen Neurons schematisch darstellen.

Neuron

Neben den Inputs x_1,...,x_n beinhaltet jedes Neuron einen sog. Bias. Der Bias steuert das durchschnittliche Aktivierungsniveau und ist elementarer Bestandteil des Neurons. Jeder Input sowie der Bias fließen als gewichtete Summe in das Neuron ein. Anschließend wird die gewichtete Summe durch die Aktivierungsfunktion des Neurons nichtlinear transformiert. Heute wird insbesondere die sog. Rectified Linear Unit (ReLU) als Aktivierungsfunktion verwendet. Diese ist definiert als g(x)=max(0,x). Andere Aktivierungsfunktionen sind bspw. die Sigmoidfunktion, definiert als g(x)=frac{1}{1+e^{-x}} oder der Tangens Hyperbolicus, g(x)=1-frac{2}{e^{2x}+1}.

Der folgende Python Code soll exemplarisch den Ablauf zur Berechnung des Outputs eines Neurons aufzeigen.

# Imports
import numpy as np

# ReLU Aktivierungsfunktion
def relu(x):
    """ ReLU Aktivierungsfunktion """
    return(np.maximum(0, x))

# Funktion für ein einzelnes Neuron
def neuron(w, x):
    """ Neuron """
    # Gewichtete Summe von w und x
    ws = np.sum(np.multiply(w, x))
    # Aktivierungsfunktion (ReLU)
    out = relu(ws)
    # Wert zurückgeben
    return(out)

# Gewichtungen und Inputs
w = [0.1, 1.2, 0.5]
x = [1, 10, 10]

# Berechnung Output (ergibt 18.0)
p = neuron(w, x)
array([18.0])

Durch die Anwendung der ReLU Aktivierungsfunktion beträgt der Output des Neurons im obigen Beispiel 18. Selbstverständlich besteht ein neuronales Netz nicht nur aus einem, sondern sehr vielen Neuronen, die über ihre Gewichtungsfaktoren in Schichten miteinander verbunden sind. Durch die Kombination vieler Neuronen ist das Netz in der Lage, auch hochgradig komplexe Funktionen zu lernen.

Gradient Descent

Einfach gesprochen lernen Neuronale Netze, indem sie iterativ die Modellprognosen mit den tatsächlich beobachteten Daten vergleichen und die Gewichtungsfaktoren im Netz so anpassen, dass in jeder Iteration der Fehler zwischen Modellprognose und Istdaten reduziert wird. Zur Quantifizierung des Modellfehlers wird eine sog. Kostenfunktion (cost function) berechnet. Die Kostenfunktion hat i.d.R. zwei Funktionsargumente: (1) den Output des Modells, o sowie (2) die tatsächlich beobachteten Daten, y. Eine typische Kostenfunktion E, die in neuronalen Netzen häufig Anwendung findet, ist der mittlere quadratische Fehler (Mean Squared Error, MSE):

    \[E=frac{1}{n}sum (y_i-o_i)^2\]

Der MSE berechnet zunächst für jeden Datenpunkt die quadratische Differenz zwischen y und o und bildet anschließend den Mittelwert. Ein hoher MSE reflektiert somit eine schlechte Anpassung des Modells an die vorliegenden Daten. Ziel des Modelltrainings soll es sein, die Kostenfunktion durch Anpassung der Modellparameter (Gewichtungen) zu minimieren. Woher weiß das neuronale Netz, welche Gewichtungen im Netzwerk angepasst werden müssen, und vor allem, in welche Richtung?

Die Kostenfunktion hängt von allen Parametern im neuronalen Netz ab. Wird auch nur eine Gewichtung minimal verändert, hat dies eine unmittelbare Auswirkung auf alle folgenden Neuronen, den Output des Netzes und somit auch auf die Kostenfunktion. In der Mathematik können die Stärke und Richtung der Veränderung der Kostenfunktion durch Veränderung eines einzelnen Parameters im Netzwerk durch die Berechnung der partiellen Ableitung der Kostenfunktion nach dem entsprechenden Gewichtungsparameter bestimmt werden: frac{partial E}{partial w_{ij}}. Da die Kostenfunktion E im neuronalen Netz hierarchisch von den verschiedenen Gewichtungsfaktoren im Netz abhängt, kann nach Anwendung der Kettenregel für Ableitungen folgende Gleichung zur Aktualisierung der Gewichtungen im Netzwerk abgeleitet werden:

    \[w_{ijt}=w_{ijt-1}-eta frac{partial E}{partial w_{ij}}\]

Die Anpassung der Gewichtungsfaktoren von Iteration zu Iteration in Richtung der Minimierung der Kostenfunktion hängt also lediglich von dem Wert des jeweiligen Gewichts aus der vorhergehenden Iteration, der partiellen Ableitung von E nach w_{ij} sowie einer Lernrate (learning rate), eta ab. Die Lernrate steuert dabei, wie groß die Schritte in Richtung der Fehlerminimierung ausfallen. Das oben skizzierte Vorgehen, die Kostenfunktion iterativ auf Basis von Gradienten zu minimieren, wird als Gradient Descent bezeichnet. Die folgende Abbildung soll den Einfluss der Lernrate auf die Minimierung der Kostenfunktion verdeutlichen (in Anlehnung an Yun Le Cun):

Learning Rate

Idealerweise würde die Lernrate so gesetzt werden, dass mit nur einer Iteration das Minimum der Kostenfunktion erreicht wird (oben links). Da in der Anwendung der theoretisch korrekte Wert nicht bekannt ist, muss die Lernrate vom Anwender definiert werden. Im Falle einer kleinen Lernrate kann man relativ sicher sein, dass ein (zumindest lokales) Minimum der Kostenfunktion erreicht wird. Allerdings steigt in diesem Szenario die Anzahl der benötigten Iterationen bis zum Minimum deutlich an (oben rechts). Für den Fall, dass die Lernrate größer als das theoretische Optimum gewählt wird, destabilisiert sich der Pfad hin zum Minimum zusehends. Zwar sinkt die benötigte Anzahl der Iterationen, es kann aber nicht sichergestellt werden, dass tatsächlich ein lokales oder globales Optimum erreicht wird. Insbesondere in Regionen, in denen die Steigung bzw. Oberfläche der Fehlerkurve sehr flach wird, kann es vorkommen, dass durch eine zu große Schrittweite das Minimum verfehlt wird (unten links). Im Falle einer deutlich zu hohen Lernrate führt dies zu einer Destabilisierung der Minimierung und es wird keine sinnvolle Lösung für die Anpassung der Gewichtungen gefunden (unten rechts). In aktuellen Optimierungsschemata finden auch adaptive Lernraten Anwendung, die die Lernrate im Laufe des Trainings anpassen. So kann bspw. zu Beginn eine höhere Lernrate gewählt werden, die dann im Laufe der Zeit weiter reduziert wird, um stabiler am (lokalen) Optimum zu landen.

# Imports
import numpy as np

# ReLU Aktivierungsfunktion
def relu(x):
    """ ReLU Aktivierungsfunktion """
    return(np.maximum(0, x))


# Ableitung der Aktivierungsfunktion
def relu_deriv(x):
    """ Ableitung ReLU """
    return(1)


# Kostenfunktion
def cost(y, p):
    """ MSE Kostenfunktion """
    mse = np.mean(pow(y-p, 2))
    return(mse)


# Ableitung der Kostenfunktion
def cost_deriv(y, p):
    """ Ableitung MSE """
    return(y-p)


# Funktion für ein einzelnes Neuron
def neuron(w, x):
    """ Neuron """
    # Gewichtete Summe von w und x
    ws = np.sum(np.multiply(w, x))
    # Aktivierungsfunktion (ReLU)
    out = relu(ws)
    # Wert zurückgeben
    return(out)
 

# Initiales Gewicht
w = [5.5]

# Input
x = [10]

# Target
y = [100]

# Lernrate
eta = 0.01

# Anzahl Iterationen
n_iter = 100

# Neuron trainieren
for i in range(n_iter):
    # Ausgabe des Neurons berechnen
    p = neuron(w, x)
    # Ableitungen berechnen
    delta_cost = cost_deriv(p, y)
    delta_act = relu_deriv(x)
    # Gewichtung aktualisieren
    w = w - eta * delta_act * delta_cost
    
# Ergebnis des Trainings
print(w)
array([9.99988047])

Durch das iterative Training des Modells ist es also gelungen, die Gewichtung des Inputs so anzupassen, dass der Abstand zwischen der Modellprognose und dem tatsächlich beobachteten Wert nahe 0 ist. Dies ist einfach nachzurechnen:

    \[o_j=g(x)=g(w*x)=max(0, 9.9998*10) = max(0, 99.998) = 99.998\]

Das Ergebnis liegt also fast bei 100. Der mittlere quadratische Fehler beträgt 3.9999e-06 und ist somit ebenfalls nahe 0. Die folgende Abbildung visualisiert abschließend den MSE im Trainingsverlauf.

Verlauf MSE Training

Man sieht deutlich den monoton abnehmenden, mittleren quadratischen Fehler. Nach 30 Iterationen liegt der Fehler praktisch bei null.

Backpropagation

Das oben dargestellte Verfahren des steilsten Gradientenabstiegs wird auch heute zur Minimierung der Kostenfunktion von neuronalen Netzen eingesetzt. Allerdings gestaltet sich die Berechnung der benötigten Gradienten in komplexeren Netzarchitekturen deutlich schwieriger als im oben gezeigten Beispiel eines einzelnen Neurons. Der Grund hierfür ist, dass die Gradienten der einzelnen Neuronen voneinander abhängen. Es besteht also eine Verkettung der Wirkungen einzelner Neuronen im Netz. Zur Lösung dieses Problems wird der sog. Backpropagation Algorithmus eingesetzt, der es ermöglicht, die Gradienten in jeder Iteration rekursiv zu berechnen.

Ursprünglich wurde der Backpropagation Algorithmus in den 1970er Jahren entwickelt, fand aber erst deutlich später, im Jahre 1986, Anerkennung durch das bahnbrechende Paper von Rumelhart, Hinton und Williams (1986), in dem Backpropagation erstmalig zum Training von neuronalen Netzen verwendet wurde. Die gesamte formale Herleitung des Backpropagation Algorithmus ist zu komplex, um hier im Detail dargestellt zu werden. Im Folgenden soll der grundsätzliche Ablauf formal skizziert werden.

In Matrizenschreibweise ist der Vektor der Outputs o aller Neuronen eines Layers l definiert als

    \[o_l=g(w_lo_{l-1}+b_l)\]

wobei w_l die Gewichtungsmatrix zwischen den Neuronen der Layer l und l-1 ist und o_{l-1} den Output des vorhergehenden Layers bezeichnet. Der Term b_l repräsentiert die Biaswerte des Layers l. Die Funktion g ist die Aktivierungsfunktion des Layers. Der Term innerhalb der Klammer wir auch als gewichteter Input z bezeichnet. Es gilt also o_l=g(z) mit z=w_lo_{l-1}+b_l. Zudem nehmen wir an, dass das Netzwerk insgesamt aus L Layern besteht. Der Output des Netzes ist also o_L. Die Kostenfunktion E des neuronalen Netzes muss so definiert sein, dass sie als Funktion des Outputs o_L sowie der beobachteten Daten y geschreiben werden kann.

Die Veränderung der Kostenfunktion in Abhängigkeit des gewichteten Inputs eines Neurons j im Layer l ist definiert als delta_j=partial E / partial z_j. Umso größer dieser Wert, desto stärker ist die Kostenfunktion abhängig vom gewichteten Inputs dieses Neurons. Die Veränderung der Kostenfunktion in Abhängigkeit der Outputs des Layers L ist definiert als

    \[delta_L=frac{partial E}{partial o_L}g'(z_L)\]

Die genaue Form von delta_L ist abhängig von der Wahl der Kostenfunktion E. Die hier verwendete mittlere quadratische Abweichung lässt sich einfach ableiten nach o_L:

    \[frac{partial E}{partial o_L}=(y-o_l)\]

Somit kann der Vektor der Gradienten für die Outputschicht folgendermaßen geschrieben werden:

    \[delta_L=(y-o_L)g'(z_L)\]

Für die vorhergehenden Layer l=1,...,L-1 lässt sich der Gradientenvektor schreiben als

    \[delta_l=(w_{l+1})^T delta_{l+1} g'(z_l)\]

Wie man sieht, sind die Gradienten im l-ten Layer eine Funktion der Gradienten und Gewichtungen des folgenden Layers l+1. Somit ist es unabdingbar, die Berechnung der Gradienten im letzten Layer des Netzes zu beginnen und diese dann iterativ in die vorhergehenden Layer zu propagieren (=Backpropagation). Durch die Kombination von delta_L und delta_l lassen sich somit die Gradienten für alle Layer im neuronalen Netz berechnen.

Nachdem die Gradienten aller Layer im Netz bekannt sind, kann anschließend ein Update der Gewichtungen im Netz mittels Gradient Descent stattfinden. Dieses Update wird so gewählt, dass die Gewichtungen in entgegengesetzter Richtung zu den berechneten Gradienten stattfinden – und somit die Kostenfunktion reduzieren.

Für das folgende Programmierbeispiel, das exemplarisch den Backpropagation Algorithmus darstellt, erweitern wir unser neuronales Netz aus dem vorherigen Beispiel um ein weiteres Neuron. Somit besteht das Netz nun aus zwei Schichten – dem Hidden Layer sowie dem Output Layer. Der Hidden Layer wird weiterhin mit der ReLU aktiviert, der Output Layer verfügt über eine lineare Aktivierung, er gibt also nur die gewichtete Summe des Neurons weiter.

# Neuron
def neuron(w, x, activation):
    """ Neuron """
    # Gewichtete Summe von w und x
    ws = np.sum(np.multiply(w, x))
    # Aktivierungsfunktion (ReLU)
    if activation == 'relu':
        out = relu(ws)
    elif activation == 'linear':
        out = ws
    # Wert zurückgeben
    return(out)


# Initiale Gewichte
w = [1, 1]

# Input
x = [10]

# Target
y = [100]

# Lernrate
eta = 0.001

# Anzahl Iterationen
n_iter = 100

# Container für Gradienten
delta = [0, 0]

# Container für MSE
mse = []

# Anzahl Layer
layers = 2

# Neuron trainieren
for i in range(100):
    # Hidden layer
    hidden = neuron(w[0], x, activation='relu')
    # Output layer
    p = neuron(w[1], x=hidden, activation='linear')
    # Ableitungen berechnen
    d_cost = cost_deriv(p, y)
    d_act = relu_deriv(x)
    # Backpropagation
    for l in reversed(range(layers)):
        # Output Layer
        if l == 1:
            # Gradienten und Update
            delta[l] = d_act * d_cost
            w[l] = w[l] - eta * delta[l]
        # Hidden Layer
        else:
            # Gradienten und Update
            delta[l] = w[l+1] * delta[l+1] * d_act
            w[l] = w[l] - eta * delta[l]
    # Append MSE
    mse.append(cost(y, p))
    
# Ergebnis des Trainings
print(w)
[array([3.87498172]), array([2.58052067])]

Im Gegensatz zum letzten Beispiel ist die Schleife nun durch Hinzufügen einer weiteren Schleife nun etwas komplexer geworden. Die weitere Schleife bildet die Backpropagation im Netz ab. Über reversed(range(layers)) iteriert die Schleife rückwärts über die Anzahl der Layer und startet somit beim Output Layer. Danach werden die Gradienten gem. oben dargestellter Formel berechnet und anschließend die Gewichtungen mittels Gradient Descent aktualisiert. Multipliziert man die berechneten Gewichtungen erhält man

    \[3.87498172*10*2.58052067=99.99470\]

was wiederum fast genau dem Zielwert von y=100 entspricht. Durch die Anwendung von Backpropagation und Gradient Descent hat das (wenn auch sehr minimalistische) neuronale Netz den Zielwert gelernt.

Zusammenfassung und Ausblick

Neuronale Netze lernen heute mittels Gradient Descent (bzw. moderneren Abwandlungen davon) und Backpropagation. Die Entwicklung effizienterer Lernverfahren, die schneller, genauer und stabiler als bestehende Formen von Backpropagation und Gradient Descent sind, gehört zu den zentralen Forschungsbereichen in diesem Fachgebiet. Insbesondere für Deep Learning Modelle, in denen sehr tiefe Modellarchitekturen geschätzt werden müssen, spielt die Auswahl des Lernverfahrens eine zentrale Rolle für die Geschwindigkeit des Trainings sowie für die Genauigkeit des Modells. Die fundamentalen Mechanismen der Lernverfahren für neuronale Netze sind bisher weitestgehend unverändert geblieben. Vielleicht wird aber künftig, angetrieben durch die massive Forschung in diesem Bereich, ein neues Lernverfahren entwickelt, das genauso revolutionär und erfolgreich sein wird wie der Backpropagation Algorithmus.

Die meisten Machine Learning Algorithmen, die heute in der Praxis Anwendung finden, gehören zur Klasse des überwachten Lernens (Supervised Learning). Im Supervised Learning wird dem Machine Learning Modell ex post eine bereits bekannte Zielgröße y präsentiert, die auf Basis verschiedener Einflussfaktoren X in den Daten durch eine Funktion f möglichst genau vorhergesagt werden soll. Die Funktion f repräsentiert dabei abstrakt das jeweilige Machine Learning Modell, das ein Mapping zwischen den Inputs und den Outputs des Modells bereitstellt.

funktion

Die gelernten Zielgrößen einfacher ML Modelle sind somit i.d.R. statisch und verändern sich nur im historischen Zeitverlauf, der ex post bereits bekannt ist. Im Zeitverlauf können neue Datenpunkte gesammelt werden, diese fließen dann in Form eines „Retrainings“ in das gelernte Mapping f zwischen X und y ein.

Basierend auf den Prognosen eines Modells werden im Anschluss weitere, meist noch menschlich gesteuerte, Handlungen ausgelöst. Diese stellen oft die eigentlichen Entscheidungen dar, die geschäftsrelevante Auswirkungen bzw. Implikationen nach sich ziehen. Die eigentlichen Geschäftsentscheidungen, die auf Basis von ML Modellen getroffen werden, sind somit vielerorts noch nicht bzw. nur teilweise maschinengesteuert. Nur an sehr wenigen Stellen werden heute bereits vollständig autonom agierende Modelle angewendet, die in der Lage sind, autark Entscheidungen zu treffen und ihr Handeln zu überwachen bzw. modellbasiert anzupassen.

Auf dem Weg hin zu selbstlernenden, autonomen Algorithmen, wie sie im Bereich der künstilichen Intelligenz verwendet werden müssen, liefern Modelle aus dem supervised learning somit nur einen begrenzten Beitrag.

Auf dem Weg hin zu selbstlernenden, autonomen Algorithmen, wie sie im Bereich der künstlichen Intelligenz verwendet werden müssen, liefern Modelle aus dem Supervised Learning somit nur einen begrenzten Beitrag. Dies ist meist darin begründet, dass die von Machine Learning Modellen gelernte 1:1 Beziehung zwischen Inputs und Outputs in komplexeren Szenarien oder Handlungsumgebungen nicht mehr ausreichend ist. Beispielweise sind die meisten Machine Learning Modelle damit überfordert, mehrere Zielgrößen gleichzeitig oder eine Sequenz von Zielgrößen bzw. Handlungen zu erlernen. Weiterhin kann der Zusammenhang zwischen Einflussfaktoren und Zielgrößen in Abhängigkeit der Umgebung bzw. auf Basis bereits getroffenener Prognosen und Entscheidungen unmittelbar variieren, was ein fortlaufendes „Retraining“ der Modelle unter geänderten Rahmenbedingungen implizieren würde.

Damit Machine Learning Modelle auch in Umgebungen Anwendung finden können, in denen sie eigenständige Aktions-Reaktions Ereignisse erlernen können, werden Lernverfahren benötigt, die einer sich verändernden Dynamik der Umgebung Rechnung tragen. Ein populäres Beispiel für die erfolgreiche Anwendung von Algorithmen dieser Art ist der Sieg von Goolge’s KI AlphaGo über den weltbesten menschlichen Go-Spieler. AlphaGo wäre mit klassischen Methoden des Supervised Learning nicht darstellbar gewesen, da aufgrund der unendlichen Anzahl an Spielzügen und Szenarien kein Modell in der Lage gewesen wäre, die Komplexität der Aktions-Reaktions-Beziehungen als reines Input-Output-Mapping abzubilden. Stattdessen werden Methoden benötigt, die in der Lage sind, selbständig auf neue Gegebenheiten der Umgebung zu reagieren, mögliche zukünftige Handlungen zu antizipieren und diese in aktuelle Entscheidung mit einfließen zu lassen. Die Klasse der Lernverfahren, auf denen Systeme wie AlphaGo basieren werden als Reinforcement Learning bezeichnet.

Was ist Reinforcement Learing?

Reinforcement Learning (RL) bildet neben Supervised und Unsupervised Learning die dritte große Gruppe von Machine Learning Verfahren. RL ist eine am natürlichen Lernverhalten des Menschen orientierte Methode. Menschliches Lernen erfolgt, insbesondere in frühen Stadien des Lernens, häufig über eine einfache Exploration der Umwelt. Dabei sind unsere Handlungen im Rahmen des Lernproblems durch einen gewissen Aktionsraum definiert. Über „Trial and Error“ werden die Auswirkungen verschiedener Handlungen auf unsere Umwelt beobachtet und bewertet. Als Reaktion auf unsere Handlungen erhalten wir von unserer Umgebung ein Feedback, abstrakt dargestellt in Form einer Belohnung oder Bestrafung. Dabei ist das Konzept der Belohnung bzw. Bestrafung nur in den allerwenigsten Fällen monetär zu verstehen. In vielen Fällen wird die Belohnung in Form von sozialer Akzeptanz, Lob anderer Menschen aber auch durch persönliches Wohlbefinden oder Erfolgserlebnisse ausgezahlt. Vielfach zeigt sich auch eine zeitliche Latenz zwischen Handlung und Belohnung. Hierbei versucht der Mensch häufig, durch sein Handeln die erwartete „Gesamtbelohnung“ im Zeitverlauf zu maximieren und nicht nur unmittelbare Belohnungen zu generieren.

Ein Beispiel: Wenn wir lernen Gitarre zu spielen, umfasst unser Aktionsraum das Zupfen der Seiten sowie das Greifen am Bund. Über eine zunächst zufällige Exploration des Handlungsraums erhalten wir in Form von Tönen der Gitarre ein Feedback der Umwelt. Dabei werden wir belohnt, wenn die Töne „gerade“ sind bzw. bestraft, wenn die Nachbarn mit dem Besen von unten gegen die Decke klopfen. Wir versuchen, die erwartete Gesamtbelohnung in Form von richtig gespielten Noten und Akkorden im für uns relevanten Zeithorizont zu maximieren. Dies geschieht nicht dadurch, dass wir aufhören zu lernen sobald wir einen Akkord sauber spielen können, sondern manifestiert sich durch ein stetiges Training und immer wieder neue Belohnungen und Erfolge, die uns im Zeitverlauf erwarten. Natürlich kann die Dauer der Exploration der möglichen Handlungen und Belohnungen der Umwelt durch die Hinzunahme eines externen Trainers verbessert werden. Dieses Beispiel ist natürlich sehr vereinfacht, stellt aber das Grundprinzip im Kern gut dar.

Reinforcement Learning besteht formal betrachtet aus fünf wichtigen Komponenten, nämlich (1) dem Agenten (agent), (2) der Umgebung (environment), (3) dem Status (state), (4) der Aktion (action) sowie (5) der Belohnung (reward). Grundsätzlich lässt sich der Ablauf wie folgt beschreiben: Der Agent führt in einer Umgebung zu einem bestimmten Status (s_t) eine Aktion (a_t) aus dem zur Verfügung stehenden Aktionsraum A durch, die zu einer Reaktion der Umgebung in Form einer Belohnungen (r_{t}) führt.

reinforcement-learning

Die Reaktion der Umgebung auf die Aktion des Agenten beeinflusst nun wiederum die Wahl der Aktion des Agenten im nächsten Status (s_{t+1}). Über mehrere tausend, hunderttausend oder sogar millionen von Iterationen ist der Agent in der Lage, einen Zusammenhang zwischen seinen Aktionen und dem künftig zu erwartenden Nutzen in jedem Status zu approximieren und sich somit entsprechend optimal zu verhalten. Dabei befindet sich der Agent immer in einem Dilemma zwischen der Nutzung seiner bisher erworbenen Erfahrung auf der einen und der Exploration neuer Strategien zur Erhöhung der Belohnung auf der anderen Seite. Dies wird als „Exploration-Exploitation Dilemma“ bezeichnet.

Die Approximation des Nutzens kann dabei modellfrei, also über reine Exploration der Umgebung erfolgen oder durch die Anwendung von Machine Learning Modellen, die den Nutzen einer Aktion versuchen zu approximieren. Letztere Variante wird insbesondere dann angewendet, wenn der Status- und/oder Aktionsraum von hoher Dimensionalität ist.

Q-Learning

Um Reinforcement Learning Systeme zu trainieren, wird häufig eine Methode verwendet, die als Q-Learning bekannt ist. Den Namen erhält Q-Learning von der sog. Q-Funktion Q(s,a), die den erwarteten Nutzen Q einer Aktion a im Status s beschreibt. Die Nutzenwerte werden in der sog. Q-Matrix Q gespeichert, deren Dimensionalität sich über die Anzahl der möglichen Stati sowie Aktionen definiert. Während des Trainings versucht der Agent, die Q-Werte der Q-Matrix durch Exploration zu approximieren, um diese später als Entscheidungsregel zu nutzen. Die Belohnungsmatrix R enthält, korrespondierend zu Q, die entsprechenden Belohnungen, die der Agent in jedem Status-Aktions-Paar erhält.

Die Approximation der Q-Werte funktioniert im einfachsten Falle wie folgt: Der Agent startet in einem zufällig initialisierten Status s_t. Anschließend selektiert der Agent zufällig eine Aktion a_t aus A, beobachtet die entsprechende Belohnung r_t und den darauf folgenden Status s_{t+1}. Die Update-Regel der Q-Matrix ist dabei wie folgt definiert:

    \[Q(s_t,a_t)=(1-alpha)Q(s_t, a_t)+alpha(r_t+gamma max Q(s_{t+1},a))\]

Der Q-Wert im Status s_t bei Ausführung der Aktion a_t ist eine Funktion des bereits gelernten Q-Wertes (erster Teil der Gleichung) sowie der Belohnung im aktuellen Status zzgl. des diskontierten maximalen Q-Wertes aller möglichen Aktionen a im folgenden Status s_{t+1}.

Der Parameter alpha im ersten Teil der Gleichung wird als Lernrate (learning rate) bezeichnet und steuert, zu welchem Anteil eine neu beobachtete Information den Agenten in seiner Entscheidung eine bestimmte Aktion zu treffen beeinflusst.

Der Parameter gamma ist der sog. Diskontierungsfaktor (discount factor) und steuert den Trade-off zwischen der Präferenz von kurzfristigen oder zukünftigen Belohnungen in der Entscheidungsfindung des Agenten. Kleine Werte für gamma lassen den Agenten eher Entscheidungen treffen, die näher liegende Belohnungen in der Entscheidungsfindung priorisieren, während höhere Werte für gamma den Agenten langfristige Belohnungen in der Entscheidungsfindung priorisieren lassen.

In modellbasierten Q-Learning Umgebungen findet die Exploration der Umgebung nicht rein zufällig statt. Die Q-Werte der Q-Matrix werden basierend auf dem aktuellen Status durch Machine Learning Modelle, in der Regel neuronale Netze und Deep Learning Modelle, approximiert. Häufig wird während des Trainings von modellbasierten RL Systemen noch eine zufällige Handlungskomponente implementiert, die der Agent mit einer gewissen Wahrscheinlichkeit p < epsilon durchführt. Dieses Vorgehen wird als epsilon-greedy bezeichnet und soll verhindern, dass der Agent immer nur die gleichen Aktionen bei der Exploration der Umgebung durchführt.

Nach Abschluss der Lernphase wählt der Agent in jedem Status diejenige Aktion mit dem höchsten Q-Wert aus, max Q(s_t, a). Somit kann sich der Agent von Status zu Status bewegen und immer diejenige Aktion wählen, die den approximierten Nutzen maximiert.

Q-Learning eignet sich insbesondere dann als Lernverfahren, wenn die Anzahl der möglichen Stati und Aktionen überschaubar ist. Andernfalls wird das Problem aufgrund der kombinatorischen Komplexität mit reinen Explorationsmechanismen nur schwer lösbar. Aus diesem Grund findet in extrem hochdimensionalen Status- und Aktionsräumen die Approximation der Q-Werte häufig über modellbasierte Ansätze statt.

Eine weitere Schwierigkeit bei der Anwendung von Q-Learning zeigt sich, wenn die Belohnungen zeitlich sehr weit vom aktuellen Status- und Handlungsraum des Agenten entfernt liegen. Wenn in naheliegenden Stati keine Belohnungen vorhanden sind, kann der Agent erst nach einer lagen Explorationsphase weit in der Zukunft liegende Belohnungen in die naheliegenden Stati propagieren.

Minimalbeispiel für Q Learning

Der neue, autonome Stabsaugerroboter „Dusty3000“ der Firma STAUBWORX soll sich vollautomatisch in unbekannten Wohnungen zurecht finden. Dabei nutzt das Gerät einen Reinforcement Learning Ansatz, um herauszufinden, in welchen Räumen einer Wohnung sich Staubballen und Flusen anhäufen. Eine virtuelle Testwohnung, in der der Roboter kalibriert werden soll, hat den folgenden Grundriss:

grundriss-wohnung-1

Insgesamt verfügt die virtuelle Testwohnung über 5 Zimmer, wobei im Testszenario lediglich im Wohnzimmer Staub anzufinden ist. Findet der Roboter den Weg zum Staub, erhält er eine Belohnung von r = 1 andernfalls wird keine Belohnung angesetzt r=0. Räume, die der Roboter von seiner aktuellen Position aus nicht erreichen kann, werden in der Belohnungsmatrix mit r=-1 definiert. In Matrizenschreibweise stellen sich die Belohnungen sowie die möglichen Aktionen pro Raum wie folgt dar:

reward-matrix-1

Stellen wir uns vor, der Saugroboter startet seine Erkundung zufällig im Flur (Raum 0). Ausgehend vom aktuellen Raum (Status) s=0 bieten sich dem Roboter drei mögliche Aktionen: 1, 2 oder 4. Aktion 0 und 3 sind nicht möglich, da diese Räume vom Flur aus nicht erreichbar sind. Der Agent erhält keine Belohnung, wenn er Aktion a=1 wählt und sich vom Flur in Raum 1 (Bad) begibt. Das gleiche gilt für Aktion a=2 (Bewegung ins Schlafzimmer). Wählt der Roboter jedoch Aktion a=4 und fährt ins Wohnzimmer, so findet er dort den Staub und er erhält eine Belohnung von r=1. Für den externen Betrachter erscheint die Wahl der Aktion trivial, unser Roboter jedoch kennt seine Umgebung nicht und wählt zufällig aus den zur Verfügung stehenden Aktionen aus.

Der Roboter startet die zufällige Erkundung der Wohnung. Die Lernrate wird auf alpha=1 und der Diskontierungsfaktor auf gamma=0.95 gesetzt. Basierend auf dem Startpunkt im Flur wählt er per Zufallsgenerator aus den zur Verfügung stehenden Aktionen a=2 aus. Somit initiiert der Roboter zunächst eine Bewegung ins Schlafzimmer. Im vereinfachten Fall von alpha=0.8 is der Q Value für die Bewegung vom Flur ins Schlafzimmer ist definiert durch:

    \[Q(0,2)=(1-alpha)Q(0,2)+alpha(r_t+gamma max Q(s_{t+1},a))\]

    \[=(1-1)*0+1*(0+0.95max[Q(2,0),Q(2,4)])\]

    \[=0+1*(0+0.95 max[0, 1])=0.95\]

Hier zeigt sich auch die Bedeutung des Diskontierungsfaktors: Bei einem Wert von 0 würde die mögliche Bewegung vom Schlafzimmer aus ins Wohnzimmer bei der momentanen Bewegung vom Flur ins Schlafzimmer nicht berücksichtigt werden. Es ergäbe sich Q Value von Q(0,2)=0​. Da der Diskontierungsfaktor in unserem Beispiel aber größer 0 ist, wird auch die mögliche, zukünftige Belohnung in Q(0,2)​ mit eingepreist.

Im Schlafzimmer angekommen ergeben sich wiederum zwei mögliche Aktionen. Entweder der Roboter bewegt sich zurück in den Flur, a=0 oder er fährt weiter ins Wohnzimmer, a=4. Zufällig fällt die Wahl diesmal auf das Wohnzimmer, womit sich folgender Q Value ergibt

    \[Q(2,4)=(1-alpha)+alpha(r_t+gamma max Q(s_{t+1},a))\]

    \[=(1-1)*0+1*(1+0.95max[Q(4,0),Q(4,3)])\]

    \[=0+1*(1+0.95max[0,0])=1\]

Der gesamte Vorgang der Exploration bis hin zur Belohnung wird als eine Episode bezeichnet. Da der Roboter nun am Ziel angekommen ist, wird die Episode beendet. Während dieses Durchlaufs konnte der Agent zwei Q-Werte berechnen. Diese werden in die Q-Matrix eingetragen, die sozusagen das Gedächtnis des Roboters abbildet.

q-matrix-1

Im weiteren Trainingsverlauf werden nun Schritt für Schritt die Werte der Q-Matrix durch den Algorithmus aktualisiert. Der Roboter entscheidet sicht in jedem Raum für diejenige Aktion, die den höchsten Q-Wert aufweist. Die Exploration der Umgebung endet dann, wenn der Agent eine Belohnung erhalten hat.

Implementierung in Python

Zur Verdeutlichung des obigen Beispiels findet sich im Folgenden der Programmiercode zur Umsetzung in Python. Zunächst wird eine Funktion erstellt, die in Abhängigkeit der Belohnungsmatrix R der Lernrate alpha, dem Diskontierungsfaktor gamma sowie der Episodenzahl episodes den oben skizzierten Algorithmus durchführt.

# Imports
import numpy as np

# Funktion
def q_learning(R, gamma, alpha, episodes):

    """ Funktion für Q Learning """
    
    # Anzahl der Zeilen und Spalten der R-Matrix
    n, p = R.shape

    # Erstellung der Q Matrix (0-Werte)
    Q = np.zeros(shape=[n, p])
    
    # Loop Episoden
    for i in range(episodes):

        # Zufälliger Startpunkt des Roboters
        state = np.random.randint(0, n, 1)

        # Iteration
        for j in range(100):

            # Mögliche Rewards im aktuellen Status
            rewards = R[state]

            # Mögliche Bewegungen des Roboters im aktuellen Status
            possible_moves = np.where(rewards[0] > -1)[0]
            
            # Zufällige Bewegung des Roboters
            next_state = np.random.choice(possible_moves, 1)

            # Update der Q values berechnen
            Q[state, next_state] = (1 - alpha) * Q[state, next_state] + alpha * (
                R[state, next_state] + gamma * np.max(Q[next_state, :]))

            # Abbrechen der Episode wenn Ziel erreicht
            if R[state, next_state] == 1:
                break

    # Q-Matrix zurückgeben
    return Q

Zunächst wird die Anzahl der Stati n sowie die Anzahl der Aktionen p bestimmt. Diese werden aus der Belohnungsmatrix R abgeleitet. Anschließend wird die zunächst noch leere Matrix Q erstellt, die die Q-Werte während der Lernphase speichert. In der Schleife über die Anzahl der festgelegten Episoden episodes wird zunächst ein zufälliger Startpunkt für den Agenten gewählt. Anschließend wird in j=100 Iterationen der oben beschriebene Algorithmus durchgeführt: Aus der Menge möglicher Aktionen possible_moves für den aktuellen Status state wird eine zufällige Auswahl getroffen und in der Variable next_state gespeichert. Im Anschluss daran findet das Update der Q-Werte, gem. oben beschriebener Formel statt. Hierbei werden sowohl der aktuelle Q-Wert an der Stelle Q[state, next_state] als auch die Belohnung des aktuellen Status R[state, next_state] verarbeitet. Nach der Aktualisierung der Q-Werte wird noch geprüft, ob der Agent die Belohnung erhalten hat. Falls ja, wird der innere Loop beendet und die Simulation geht in die nächste Episode. Nach Beendigung aller Episoden wird die finale Q-Matrix zurückgegeben.

Eine praktische Anwendung der oben gezeigten Funktion findet sich in der unten stehenden Codebox. Zunächst wird die Belohnungsmatrix analog für das oben skizzierte Beispiel definiert, die dann mit den weiteren Funktionsargumenten an die Funktion q_learning übergeben wird. Die Lernrate wird auf alpha=0.8 und der Diskontierungsfaktor auf gamma=0.95 gesetzt. Insgesamt sollen n=1000 Episoden simuliert werden.

# Anzahl der Räume
rooms = 5

# Belohnungs-Matrix
R = np.zeros(shape=[rooms, rooms])
R[0, :] = [-1,  0,  0, -1,  1]
R[1, :] = [ 0, -1, -1, -1, -1]
R[2, :] = [ 0, -1, -1, -1,  1]
R[3, :] = [-1, -1, -1, -1,  1]
R[4, :] = [ 0, -1,  0,  0,  1]

# Q-Learning
Q = q_learning(R=R, gamma=0.95, alpha=0.8, episodes=1000)

# Finale Q Matrix normalisieren und anzeigen
np.round(Q / np.max(Q), 4)
array([[ 0.    ,  0.9025,  0.95  ,  0.    ,  1.    ],
       [ 0.95  ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.95  ,  0.    ,  0.    ,  0.    ,  1.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  1.    ],
       [ 0.95  ,  0.    ,  0.95  ,  0.95  ,  1.    ]])

An der finalen Q-Matrix kann man nun die gelernten Handlungen des Roboters ablesen. Dies geschieht durch Ablesen des maximalen Q-Wertes pro Status (Zeile). Beispiel: Befindet sich der Roboter im Flur (Zeile 1 der Matrix) so ist der maximale Q-Wert in der letzten Spalte zu finden. Dies korrespondiert zu einer Bewegung ins Wohnzimmer. Befindet sich der Agent im Bad (Zeile 2) bewegt er sich zunächst in den Flur (Zeile 1). Von dort bestimmt der maximale Q-Wert eine Bewegung ins Wohnzimmer.

Ein komplexeres Beispiel

Unser Dusty3000 hat sich in der einfachen Simulationsumgebung bereits bewährt. Nun soll geklärt werden, wie sich der Roboter in komplexeren Wohnungen zurecht finden kann. Zu diesem Zweck wurde die Wohnung um zwei weitere Zimmer ergänzt und der Staub vom Wohnzimmer ins Kinderzimmer verlegt:

grundriss-wohnung-2

Das Kinderzimmer ist im Vergleich zum vorherigen Beispiel deutlich schwieriger und nur über verschiedene Pfade zu erreichen. Somit wird der Roboter es schwerer haben, die Q-Werte richtig zu schätzen. Die Belohnungsmatrix verändert sich entsprechend wie folgt:

reward-matrix-2

Analgog zum vorhergehenden Beispiel wird die Lernrate auf alpha=0.8 und der Diskontierungsfaktor auf gamma=0.95 gesetzt. Insgesamt werden n=1000 Episoden simuliert.

# Anzahl der Räume
rooms = 7

# Belohnungs-Matrix
R = np.zeros(shape=[rooms, rooms])
R[0, :] = [-1,  0,  0, -1,  0, -1, -1]
R[1, :] = [ 0, -1, -1, -1, -1, -1, -1]
R[2, :] = [ 0, -1, -1, -1,  0,  1, -1]
R[3, :] = [-1, -1, -1, -1,  0, -1, -1]
R[4, :] = [ 0, -1,  0,  0, -1, -1,  0]
R[5, :] = [-1, -1,  0, -1, -1, -1,  0]
R[6, :] = [-1, -1, -1, -1,  0,  0, -1]

# Q-Learning!
Q = q_learning(R=R, gamma=0.95, alpha=0.8, episodes=1000)

# Normalize
np.round(Q / np.max(Q), 4)
array([[ 0.    ,  0.8571,  0.9499,  0.    ,  0.9023,  0.    ,  0.    ],
       [ 0.9023,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.9023,  0.    ,  0.    ,  0.    ,  0.9023,  1.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.9023,  0.    ,  0.    ],
       [ 0.9023,  0.    ,  0.9497,  0.857 ,  0.    ,  0.    ,  0.857 ],
       [ 0.    ,  0.    ,  0.9499,  0.    ,  0.    ,  0.    ,  0.8571],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.9023,  0.9023,  0.    ]])

Die resultierende Q-Matrix ist komplexer als im einfacheren Vorgängerbeispiel. Beispiel: Ausgehend vom Flur (Zeile 1) bewegt sich der Agent in Zimmer 2 (Schlafzimmer) und von dort aus dann ins Kinderzimmer, wo die Belohnung auf ihn wartet. Ausgehend von der Küche (Zeile 4) fährt der Roboter ins Wohnzimmer (Zeile 5) von dort ins Schlafzimmer (Zeile 3) und dann schlussendlich ins Kinderzimmer.

Fazit und Ausblick

Mit Reinforcement Learning und Q-Learning ist es möglich, Algorithmen und Systeme zu entwickeln, die autark in deterministischen als auch stochastischen Umgebungen Handlungen erlernen und ausführen können; ohne diese exakt zu kennen. Dabei versucht der Agent stets basierend auf seinen Handlungen, die für ihn von der Umgebung erzeugte Belohnung zu maximieren. Dabei kann über den Diskontierungsfaktor gesteuert werden, ob der Agent einen Fokus auf kurzfristige oder langfristige Belohnungen legen soll. Die Anwendungsgebiete für solche Agenten sind vielfältig und spannend: Vor einiger Zeit veröffentlichte Google ein Paper, in dem mittels modellbasiertem Reinforcement Learning ein Agent trainiert darauf wurde, verschiedenste Atari Computerspiele zu spielen. In der nächsten Entwicklungsstufe wurde das zuvor entwickelte RL System DQN (Deep Q-Network) auf den deutlich komplexeren Strategiespiel-Klassiker StarCraft angewendet. Im Gegensatz zum hier gezeigten Beispiel wurden dabei die Ableitung der jeweils optimalen Handlung nicht in Form einer matrizenbasierten Übersicht, sondern über Deep Learning Modelle gelöst, die die Q-Werte in s_{t+1} modellbasiert auf Basis der Pixel auf dem Bildschirm approximieren. Auf diesen Anwendungsfall werden wir im zweiten Teil der Reihe zum Thema Reinforcement Learning eingehen und zeigen, wie neuronale Netze und Deep Learning als Q-Approximatoren genutzt werden können. Dies eröffnet nochmals deutliche komplexere und realitätsnähere Anwendungsfälle, da die Anzahl der Stati beliebig hoch sein können.

Referenzen

  1. Sutton, Richard S.; Barto, Andrew G. (1998). Reinforcement Learning: An Introduction. MIT Press.
  2. Goodfellow, Ian; Bengio Yoshua; Courville, Cohan (2016). Deep Learning. MIT Press.

Die meisten Machine Learning Algorithmen, die heute in der Praxis Anwendung finden, gehören zur Klasse des überwachten Lernens (Supervised Learning). Im Supervised Learning wird dem Machine Learning Modell ex post eine bereits bekannte Zielgröße y präsentiert, die auf Basis verschiedener Einflussfaktoren X in den Daten durch eine Funktion f möglichst genau vorhergesagt werden soll. Die Funktion f repräsentiert dabei abstrakt das jeweilige Machine Learning Modell, das ein Mapping zwischen den Inputs und den Outputs des Modells bereitstellt.

funktion

Die gelernten Zielgrößen einfacher ML Modelle sind somit i.d.R. statisch und verändern sich nur im historischen Zeitverlauf, der ex post bereits bekannt ist. Im Zeitverlauf können neue Datenpunkte gesammelt werden, diese fließen dann in Form eines „Retrainings“ in das gelernte Mapping f zwischen X und y ein.

Basierend auf den Prognosen eines Modells werden im Anschluss weitere, meist noch menschlich gesteuerte, Handlungen ausgelöst. Diese stellen oft die eigentlichen Entscheidungen dar, die geschäftsrelevante Auswirkungen bzw. Implikationen nach sich ziehen. Die eigentlichen Geschäftsentscheidungen, die auf Basis von ML Modellen getroffen werden, sind somit vielerorts noch nicht bzw. nur teilweise maschinengesteuert. Nur an sehr wenigen Stellen werden heute bereits vollständig autonom agierende Modelle angewendet, die in der Lage sind, autark Entscheidungen zu treffen und ihr Handeln zu überwachen bzw. modellbasiert anzupassen.

Auf dem Weg hin zu selbstlernenden, autonomen Algorithmen, wie sie im Bereich der künstilichen Intelligenz verwendet werden müssen, liefern Modelle aus dem supervised learning somit nur einen begrenzten Beitrag.

Auf dem Weg hin zu selbstlernenden, autonomen Algorithmen, wie sie im Bereich der künstlichen Intelligenz verwendet werden müssen, liefern Modelle aus dem Supervised Learning somit nur einen begrenzten Beitrag. Dies ist meist darin begründet, dass die von Machine Learning Modellen gelernte 1:1 Beziehung zwischen Inputs und Outputs in komplexeren Szenarien oder Handlungsumgebungen nicht mehr ausreichend ist. Beispielweise sind die meisten Machine Learning Modelle damit überfordert, mehrere Zielgrößen gleichzeitig oder eine Sequenz von Zielgrößen bzw. Handlungen zu erlernen. Weiterhin kann der Zusammenhang zwischen Einflussfaktoren und Zielgrößen in Abhängigkeit der Umgebung bzw. auf Basis bereits getroffenener Prognosen und Entscheidungen unmittelbar variieren, was ein fortlaufendes „Retraining“ der Modelle unter geänderten Rahmenbedingungen implizieren würde.

Damit Machine Learning Modelle auch in Umgebungen Anwendung finden können, in denen sie eigenständige Aktions-Reaktions Ereignisse erlernen können, werden Lernverfahren benötigt, die einer sich verändernden Dynamik der Umgebung Rechnung tragen. Ein populäres Beispiel für die erfolgreiche Anwendung von Algorithmen dieser Art ist der Sieg von Goolge’s KI AlphaGo über den weltbesten menschlichen Go-Spieler. AlphaGo wäre mit klassischen Methoden des Supervised Learning nicht darstellbar gewesen, da aufgrund der unendlichen Anzahl an Spielzügen und Szenarien kein Modell in der Lage gewesen wäre, die Komplexität der Aktions-Reaktions-Beziehungen als reines Input-Output-Mapping abzubilden. Stattdessen werden Methoden benötigt, die in der Lage sind, selbständig auf neue Gegebenheiten der Umgebung zu reagieren, mögliche zukünftige Handlungen zu antizipieren und diese in aktuelle Entscheidung mit einfließen zu lassen. Die Klasse der Lernverfahren, auf denen Systeme wie AlphaGo basieren werden als Reinforcement Learning bezeichnet.

Was ist Reinforcement Learing?

Reinforcement Learning (RL) bildet neben Supervised und Unsupervised Learning die dritte große Gruppe von Machine Learning Verfahren. RL ist eine am natürlichen Lernverhalten des Menschen orientierte Methode. Menschliches Lernen erfolgt, insbesondere in frühen Stadien des Lernens, häufig über eine einfache Exploration der Umwelt. Dabei sind unsere Handlungen im Rahmen des Lernproblems durch einen gewissen Aktionsraum definiert. Über „Trial and Error“ werden die Auswirkungen verschiedener Handlungen auf unsere Umwelt beobachtet und bewertet. Als Reaktion auf unsere Handlungen erhalten wir von unserer Umgebung ein Feedback, abstrakt dargestellt in Form einer Belohnung oder Bestrafung. Dabei ist das Konzept der Belohnung bzw. Bestrafung nur in den allerwenigsten Fällen monetär zu verstehen. In vielen Fällen wird die Belohnung in Form von sozialer Akzeptanz, Lob anderer Menschen aber auch durch persönliches Wohlbefinden oder Erfolgserlebnisse ausgezahlt. Vielfach zeigt sich auch eine zeitliche Latenz zwischen Handlung und Belohnung. Hierbei versucht der Mensch häufig, durch sein Handeln die erwartete „Gesamtbelohnung“ im Zeitverlauf zu maximieren und nicht nur unmittelbare Belohnungen zu generieren.

Ein Beispiel: Wenn wir lernen Gitarre zu spielen, umfasst unser Aktionsraum das Zupfen der Seiten sowie das Greifen am Bund. Über eine zunächst zufällige Exploration des Handlungsraums erhalten wir in Form von Tönen der Gitarre ein Feedback der Umwelt. Dabei werden wir belohnt, wenn die Töne „gerade“ sind bzw. bestraft, wenn die Nachbarn mit dem Besen von unten gegen die Decke klopfen. Wir versuchen, die erwartete Gesamtbelohnung in Form von richtig gespielten Noten und Akkorden im für uns relevanten Zeithorizont zu maximieren. Dies geschieht nicht dadurch, dass wir aufhören zu lernen sobald wir einen Akkord sauber spielen können, sondern manifestiert sich durch ein stetiges Training und immer wieder neue Belohnungen und Erfolge, die uns im Zeitverlauf erwarten. Natürlich kann die Dauer der Exploration der möglichen Handlungen und Belohnungen der Umwelt durch die Hinzunahme eines externen Trainers verbessert werden. Dieses Beispiel ist natürlich sehr vereinfacht, stellt aber das Grundprinzip im Kern gut dar.

Reinforcement Learning besteht formal betrachtet aus fünf wichtigen Komponenten, nämlich (1) dem Agenten (agent), (2) der Umgebung (environment), (3) dem Status (state), (4) der Aktion (action) sowie (5) der Belohnung (reward). Grundsätzlich lässt sich der Ablauf wie folgt beschreiben: Der Agent führt in einer Umgebung zu einem bestimmten Status (s_t) eine Aktion (a_t) aus dem zur Verfügung stehenden Aktionsraum A durch, die zu einer Reaktion der Umgebung in Form einer Belohnungen (r_{t}) führt.

reinforcement-learning

Die Reaktion der Umgebung auf die Aktion des Agenten beeinflusst nun wiederum die Wahl der Aktion des Agenten im nächsten Status (s_{t+1}). Über mehrere tausend, hunderttausend oder sogar millionen von Iterationen ist der Agent in der Lage, einen Zusammenhang zwischen seinen Aktionen und dem künftig zu erwartenden Nutzen in jedem Status zu approximieren und sich somit entsprechend optimal zu verhalten. Dabei befindet sich der Agent immer in einem Dilemma zwischen der Nutzung seiner bisher erworbenen Erfahrung auf der einen und der Exploration neuer Strategien zur Erhöhung der Belohnung auf der anderen Seite. Dies wird als „Exploration-Exploitation Dilemma“ bezeichnet.

Die Approximation des Nutzens kann dabei modellfrei, also über reine Exploration der Umgebung erfolgen oder durch die Anwendung von Machine Learning Modellen, die den Nutzen einer Aktion versuchen zu approximieren. Letztere Variante wird insbesondere dann angewendet, wenn der Status- und/oder Aktionsraum von hoher Dimensionalität ist.

Q-Learning

Um Reinforcement Learning Systeme zu trainieren, wird häufig eine Methode verwendet, die als Q-Learning bekannt ist. Den Namen erhält Q-Learning von der sog. Q-Funktion Q(s,a), die den erwarteten Nutzen Q einer Aktion a im Status s beschreibt. Die Nutzenwerte werden in der sog. Q-Matrix Q gespeichert, deren Dimensionalität sich über die Anzahl der möglichen Stati sowie Aktionen definiert. Während des Trainings versucht der Agent, die Q-Werte der Q-Matrix durch Exploration zu approximieren, um diese später als Entscheidungsregel zu nutzen. Die Belohnungsmatrix R enthält, korrespondierend zu Q, die entsprechenden Belohnungen, die der Agent in jedem Status-Aktions-Paar erhält.

Die Approximation der Q-Werte funktioniert im einfachsten Falle wie folgt: Der Agent startet in einem zufällig initialisierten Status s_t. Anschließend selektiert der Agent zufällig eine Aktion a_t aus A, beobachtet die entsprechende Belohnung r_t und den darauf folgenden Status s_{t+1}. Die Update-Regel der Q-Matrix ist dabei wie folgt definiert:

    \[Q(s_t,a_t)=(1-alpha)Q(s_t, a_t)+alpha(r_t+gamma max Q(s_{t+1},a))\]

Der Q-Wert im Status s_t bei Ausführung der Aktion a_t ist eine Funktion des bereits gelernten Q-Wertes (erster Teil der Gleichung) sowie der Belohnung im aktuellen Status zzgl. des diskontierten maximalen Q-Wertes aller möglichen Aktionen a im folgenden Status s_{t+1}.

Der Parameter alpha im ersten Teil der Gleichung wird als Lernrate (learning rate) bezeichnet und steuert, zu welchem Anteil eine neu beobachtete Information den Agenten in seiner Entscheidung eine bestimmte Aktion zu treffen beeinflusst.

Der Parameter gamma ist der sog. Diskontierungsfaktor (discount factor) und steuert den Trade-off zwischen der Präferenz von kurzfristigen oder zukünftigen Belohnungen in der Entscheidungsfindung des Agenten. Kleine Werte für gamma lassen den Agenten eher Entscheidungen treffen, die näher liegende Belohnungen in der Entscheidungsfindung priorisieren, während höhere Werte für gamma den Agenten langfristige Belohnungen in der Entscheidungsfindung priorisieren lassen.

In modellbasierten Q-Learning Umgebungen findet die Exploration der Umgebung nicht rein zufällig statt. Die Q-Werte der Q-Matrix werden basierend auf dem aktuellen Status durch Machine Learning Modelle, in der Regel neuronale Netze und Deep Learning Modelle, approximiert. Häufig wird während des Trainings von modellbasierten RL Systemen noch eine zufällige Handlungskomponente implementiert, die der Agent mit einer gewissen Wahrscheinlichkeit p < epsilon durchführt. Dieses Vorgehen wird als epsilon-greedy bezeichnet und soll verhindern, dass der Agent immer nur die gleichen Aktionen bei der Exploration der Umgebung durchführt.

Nach Abschluss der Lernphase wählt der Agent in jedem Status diejenige Aktion mit dem höchsten Q-Wert aus, max Q(s_t, a). Somit kann sich der Agent von Status zu Status bewegen und immer diejenige Aktion wählen, die den approximierten Nutzen maximiert.

Q-Learning eignet sich insbesondere dann als Lernverfahren, wenn die Anzahl der möglichen Stati und Aktionen überschaubar ist. Andernfalls wird das Problem aufgrund der kombinatorischen Komplexität mit reinen Explorationsmechanismen nur schwer lösbar. Aus diesem Grund findet in extrem hochdimensionalen Status- und Aktionsräumen die Approximation der Q-Werte häufig über modellbasierte Ansätze statt.

Eine weitere Schwierigkeit bei der Anwendung von Q-Learning zeigt sich, wenn die Belohnungen zeitlich sehr weit vom aktuellen Status- und Handlungsraum des Agenten entfernt liegen. Wenn in naheliegenden Stati keine Belohnungen vorhanden sind, kann der Agent erst nach einer lagen Explorationsphase weit in der Zukunft liegende Belohnungen in die naheliegenden Stati propagieren.

Minimalbeispiel für Q Learning

Der neue, autonome Stabsaugerroboter „Dusty3000“ der Firma STAUBWORX soll sich vollautomatisch in unbekannten Wohnungen zurecht finden. Dabei nutzt das Gerät einen Reinforcement Learning Ansatz, um herauszufinden, in welchen Räumen einer Wohnung sich Staubballen und Flusen anhäufen. Eine virtuelle Testwohnung, in der der Roboter kalibriert werden soll, hat den folgenden Grundriss:

grundriss-wohnung-1

Insgesamt verfügt die virtuelle Testwohnung über 5 Zimmer, wobei im Testszenario lediglich im Wohnzimmer Staub anzufinden ist. Findet der Roboter den Weg zum Staub, erhält er eine Belohnung von r = 1 andernfalls wird keine Belohnung angesetzt r=0. Räume, die der Roboter von seiner aktuellen Position aus nicht erreichen kann, werden in der Belohnungsmatrix mit r=-1 definiert. In Matrizenschreibweise stellen sich die Belohnungen sowie die möglichen Aktionen pro Raum wie folgt dar:

reward-matrix-1

Stellen wir uns vor, der Saugroboter startet seine Erkundung zufällig im Flur (Raum 0). Ausgehend vom aktuellen Raum (Status) s=0 bieten sich dem Roboter drei mögliche Aktionen: 1, 2 oder 4. Aktion 0 und 3 sind nicht möglich, da diese Räume vom Flur aus nicht erreichbar sind. Der Agent erhält keine Belohnung, wenn er Aktion a=1 wählt und sich vom Flur in Raum 1 (Bad) begibt. Das gleiche gilt für Aktion a=2 (Bewegung ins Schlafzimmer). Wählt der Roboter jedoch Aktion a=4 und fährt ins Wohnzimmer, so findet er dort den Staub und er erhält eine Belohnung von r=1. Für den externen Betrachter erscheint die Wahl der Aktion trivial, unser Roboter jedoch kennt seine Umgebung nicht und wählt zufällig aus den zur Verfügung stehenden Aktionen aus.

Der Roboter startet die zufällige Erkundung der Wohnung. Die Lernrate wird auf alpha=1 und der Diskontierungsfaktor auf gamma=0.95 gesetzt. Basierend auf dem Startpunkt im Flur wählt er per Zufallsgenerator aus den zur Verfügung stehenden Aktionen a=2 aus. Somit initiiert der Roboter zunächst eine Bewegung ins Schlafzimmer. Im vereinfachten Fall von alpha=0.8 is der Q Value für die Bewegung vom Flur ins Schlafzimmer ist definiert durch:

    \[Q(0,2)=(1-alpha)Q(0,2)+alpha(r_t+gamma max Q(s_{t+1},a))\]

    \[=(1-1)*0+1*(0+0.95max[Q(2,0),Q(2,4)])\]

    \[=0+1*(0+0.95 max[0, 1])=0.95\]

Hier zeigt sich auch die Bedeutung des Diskontierungsfaktors: Bei einem Wert von 0 würde die mögliche Bewegung vom Schlafzimmer aus ins Wohnzimmer bei der momentanen Bewegung vom Flur ins Schlafzimmer nicht berücksichtigt werden. Es ergäbe sich Q Value von Q(0,2)=0​. Da der Diskontierungsfaktor in unserem Beispiel aber größer 0 ist, wird auch die mögliche, zukünftige Belohnung in Q(0,2)​ mit eingepreist.

Im Schlafzimmer angekommen ergeben sich wiederum zwei mögliche Aktionen. Entweder der Roboter bewegt sich zurück in den Flur, a=0 oder er fährt weiter ins Wohnzimmer, a=4. Zufällig fällt die Wahl diesmal auf das Wohnzimmer, womit sich folgender Q Value ergibt

    \[Q(2,4)=(1-alpha)+alpha(r_t+gamma max Q(s_{t+1},a))\]

    \[=(1-1)*0+1*(1+0.95max[Q(4,0),Q(4,3)])\]

    \[=0+1*(1+0.95max[0,0])=1\]

Der gesamte Vorgang der Exploration bis hin zur Belohnung wird als eine Episode bezeichnet. Da der Roboter nun am Ziel angekommen ist, wird die Episode beendet. Während dieses Durchlaufs konnte der Agent zwei Q-Werte berechnen. Diese werden in die Q-Matrix eingetragen, die sozusagen das Gedächtnis des Roboters abbildet.

q-matrix-1

Im weiteren Trainingsverlauf werden nun Schritt für Schritt die Werte der Q-Matrix durch den Algorithmus aktualisiert. Der Roboter entscheidet sicht in jedem Raum für diejenige Aktion, die den höchsten Q-Wert aufweist. Die Exploration der Umgebung endet dann, wenn der Agent eine Belohnung erhalten hat.

Implementierung in Python

Zur Verdeutlichung des obigen Beispiels findet sich im Folgenden der Programmiercode zur Umsetzung in Python. Zunächst wird eine Funktion erstellt, die in Abhängigkeit der Belohnungsmatrix R der Lernrate alpha, dem Diskontierungsfaktor gamma sowie der Episodenzahl episodes den oben skizzierten Algorithmus durchführt.

# Imports
import numpy as np

# Funktion
def q_learning(R, gamma, alpha, episodes):

    """ Funktion für Q Learning """
    
    # Anzahl der Zeilen und Spalten der R-Matrix
    n, p = R.shape

    # Erstellung der Q Matrix (0-Werte)
    Q = np.zeros(shape=[n, p])
    
    # Loop Episoden
    for i in range(episodes):

        # Zufälliger Startpunkt des Roboters
        state = np.random.randint(0, n, 1)

        # Iteration
        for j in range(100):

            # Mögliche Rewards im aktuellen Status
            rewards = R[state]

            # Mögliche Bewegungen des Roboters im aktuellen Status
            possible_moves = np.where(rewards[0] > -1)[0]
            
            # Zufällige Bewegung des Roboters
            next_state = np.random.choice(possible_moves, 1)

            # Update der Q values berechnen
            Q[state, next_state] = (1 - alpha) * Q[state, next_state] + alpha * (
                R[state, next_state] + gamma * np.max(Q[next_state, :]))

            # Abbrechen der Episode wenn Ziel erreicht
            if R[state, next_state] == 1:
                break

    # Q-Matrix zurückgeben
    return Q

Zunächst wird die Anzahl der Stati n sowie die Anzahl der Aktionen p bestimmt. Diese werden aus der Belohnungsmatrix R abgeleitet. Anschließend wird die zunächst noch leere Matrix Q erstellt, die die Q-Werte während der Lernphase speichert. In der Schleife über die Anzahl der festgelegten Episoden episodes wird zunächst ein zufälliger Startpunkt für den Agenten gewählt. Anschließend wird in j=100 Iterationen der oben beschriebene Algorithmus durchgeführt: Aus der Menge möglicher Aktionen possible_moves für den aktuellen Status state wird eine zufällige Auswahl getroffen und in der Variable next_state gespeichert. Im Anschluss daran findet das Update der Q-Werte, gem. oben beschriebener Formel statt. Hierbei werden sowohl der aktuelle Q-Wert an der Stelle Q[state, next_state] als auch die Belohnung des aktuellen Status R[state, next_state] verarbeitet. Nach der Aktualisierung der Q-Werte wird noch geprüft, ob der Agent die Belohnung erhalten hat. Falls ja, wird der innere Loop beendet und die Simulation geht in die nächste Episode. Nach Beendigung aller Episoden wird die finale Q-Matrix zurückgegeben.

Eine praktische Anwendung der oben gezeigten Funktion findet sich in der unten stehenden Codebox. Zunächst wird die Belohnungsmatrix analog für das oben skizzierte Beispiel definiert, die dann mit den weiteren Funktionsargumenten an die Funktion q_learning übergeben wird. Die Lernrate wird auf alpha=0.8 und der Diskontierungsfaktor auf gamma=0.95 gesetzt. Insgesamt sollen n=1000 Episoden simuliert werden.

# Anzahl der Räume
rooms = 5

# Belohnungs-Matrix
R = np.zeros(shape=[rooms, rooms])
R[0, :] = [-1,  0,  0, -1,  1]
R[1, :] = [ 0, -1, -1, -1, -1]
R[2, :] = [ 0, -1, -1, -1,  1]
R[3, :] = [-1, -1, -1, -1,  1]
R[4, :] = [ 0, -1,  0,  0,  1]

# Q-Learning
Q = q_learning(R=R, gamma=0.95, alpha=0.8, episodes=1000)

# Finale Q Matrix normalisieren und anzeigen
np.round(Q / np.max(Q), 4)
array([[ 0.    ,  0.9025,  0.95  ,  0.    ,  1.    ],
       [ 0.95  ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.95  ,  0.    ,  0.    ,  0.    ,  1.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  1.    ],
       [ 0.95  ,  0.    ,  0.95  ,  0.95  ,  1.    ]])

An der finalen Q-Matrix kann man nun die gelernten Handlungen des Roboters ablesen. Dies geschieht durch Ablesen des maximalen Q-Wertes pro Status (Zeile). Beispiel: Befindet sich der Roboter im Flur (Zeile 1 der Matrix) so ist der maximale Q-Wert in der letzten Spalte zu finden. Dies korrespondiert zu einer Bewegung ins Wohnzimmer. Befindet sich der Agent im Bad (Zeile 2) bewegt er sich zunächst in den Flur (Zeile 1). Von dort bestimmt der maximale Q-Wert eine Bewegung ins Wohnzimmer.

Ein komplexeres Beispiel

Unser Dusty3000 hat sich in der einfachen Simulationsumgebung bereits bewährt. Nun soll geklärt werden, wie sich der Roboter in komplexeren Wohnungen zurecht finden kann. Zu diesem Zweck wurde die Wohnung um zwei weitere Zimmer ergänzt und der Staub vom Wohnzimmer ins Kinderzimmer verlegt:

grundriss-wohnung-2

Das Kinderzimmer ist im Vergleich zum vorherigen Beispiel deutlich schwieriger und nur über verschiedene Pfade zu erreichen. Somit wird der Roboter es schwerer haben, die Q-Werte richtig zu schätzen. Die Belohnungsmatrix verändert sich entsprechend wie folgt:

reward-matrix-2

Analgog zum vorhergehenden Beispiel wird die Lernrate auf alpha=0.8 und der Diskontierungsfaktor auf gamma=0.95 gesetzt. Insgesamt werden n=1000 Episoden simuliert.

# Anzahl der Räume
rooms = 7

# Belohnungs-Matrix
R = np.zeros(shape=[rooms, rooms])
R[0, :] = [-1,  0,  0, -1,  0, -1, -1]
R[1, :] = [ 0, -1, -1, -1, -1, -1, -1]
R[2, :] = [ 0, -1, -1, -1,  0,  1, -1]
R[3, :] = [-1, -1, -1, -1,  0, -1, -1]
R[4, :] = [ 0, -1,  0,  0, -1, -1,  0]
R[5, :] = [-1, -1,  0, -1, -1, -1,  0]
R[6, :] = [-1, -1, -1, -1,  0,  0, -1]

# Q-Learning!
Q = q_learning(R=R, gamma=0.95, alpha=0.8, episodes=1000)

# Normalize
np.round(Q / np.max(Q), 4)
array([[ 0.    ,  0.8571,  0.9499,  0.    ,  0.9023,  0.    ,  0.    ],
       [ 0.9023,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.9023,  0.    ,  0.    ,  0.    ,  0.9023,  1.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.9023,  0.    ,  0.    ],
       [ 0.9023,  0.    ,  0.9497,  0.857 ,  0.    ,  0.    ,  0.857 ],
       [ 0.    ,  0.    ,  0.9499,  0.    ,  0.    ,  0.    ,  0.8571],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.9023,  0.9023,  0.    ]])

Die resultierende Q-Matrix ist komplexer als im einfacheren Vorgängerbeispiel. Beispiel: Ausgehend vom Flur (Zeile 1) bewegt sich der Agent in Zimmer 2 (Schlafzimmer) und von dort aus dann ins Kinderzimmer, wo die Belohnung auf ihn wartet. Ausgehend von der Küche (Zeile 4) fährt der Roboter ins Wohnzimmer (Zeile 5) von dort ins Schlafzimmer (Zeile 3) und dann schlussendlich ins Kinderzimmer.

Fazit und Ausblick

Mit Reinforcement Learning und Q-Learning ist es möglich, Algorithmen und Systeme zu entwickeln, die autark in deterministischen als auch stochastischen Umgebungen Handlungen erlernen und ausführen können; ohne diese exakt zu kennen. Dabei versucht der Agent stets basierend auf seinen Handlungen, die für ihn von der Umgebung erzeugte Belohnung zu maximieren. Dabei kann über den Diskontierungsfaktor gesteuert werden, ob der Agent einen Fokus auf kurzfristige oder langfristige Belohnungen legen soll. Die Anwendungsgebiete für solche Agenten sind vielfältig und spannend: Vor einiger Zeit veröffentlichte Google ein Paper, in dem mittels modellbasiertem Reinforcement Learning ein Agent trainiert darauf wurde, verschiedenste Atari Computerspiele zu spielen. In der nächsten Entwicklungsstufe wurde das zuvor entwickelte RL System DQN (Deep Q-Network) auf den deutlich komplexeren Strategiespiel-Klassiker StarCraft angewendet. Im Gegensatz zum hier gezeigten Beispiel wurden dabei die Ableitung der jeweils optimalen Handlung nicht in Form einer matrizenbasierten Übersicht, sondern über Deep Learning Modelle gelöst, die die Q-Werte in s_{t+1} modellbasiert auf Basis der Pixel auf dem Bildschirm approximieren. Auf diesen Anwendungsfall werden wir im zweiten Teil der Reihe zum Thema Reinforcement Learning eingehen und zeigen, wie neuronale Netze und Deep Learning als Q-Approximatoren genutzt werden können. Dies eröffnet nochmals deutliche komplexere und realitätsnähere Anwendungsfälle, da die Anzahl der Stati beliebig hoch sein können.

Referenzen

  1. Sutton, Richard S.; Barto, Andrew G. (1998). Reinforcement Learning: An Introduction. MIT Press.
  2. Goodfellow, Ian; Bengio Yoshua; Courville, Cohan (2016). Deep Learning. MIT Press.