Bei statworx befassen wir uns intensiv damit, wie wir von großen Sprachmodellen (LLMs) die bestmöglichen Resultate erhalten. In diesem Blogbeitrag stelle ich fünf Ansätze vor, die sich sowohl in der Forschung als auch in unseren eigenen Experimenten mit LLMs bewährt haben. Während sich dieser Text auf das manuelle Design von Prompts zur Textgenerierung beschränkt, werden Bildgenerierung und automatisierte Promptsuche das Thema zukünftiger Beiträge sein.

Mega-Modelle läuten neues Paradigma ein

Die Ankunft des revolutionären Sprachmodells GPT-3 stellte nicht nur für das Forschungsfeld der Sprachmodellierung (NLP) einen Wendepunkt dar, sondern hat ganz nebenbei einen Paradigmenwechsel in der KI-Entwicklung eingeläutet: Prompt-Learning. Vor GPT-3 war der Standard das Fine-Tuning von mittelgroßen Sprachmodellen wie BERT, was dank erneutem Training mit eigenen Daten das vortrainierte Modell an den gewünschten Anwendungsfall anpassen sollte. Derartiges Fine-Tuning erfordert exemplarische Daten für die gewünschte Anwendung, sowie die rechnerischen Möglichkeiten das Modell zumindest teilweise neu zu trainieren.

Die neuen großen Sprachmodelle wie OpenAIs GPT-3 und BigSciences BLOOM hingegen wurden von ihren Entwicklerteams bereits mit derart großen Mengen an Ressourcen trainiert, dass diese Modelle ein neues Maß an Unabhängigkeit in ihrem Anwendungszweck erreicht haben: Diese LLMs benötigen nicht länger aufwändiges Fine-Tuning, um ihren spezifischen Zweck zu erlernen, sondern erzeugen bereits mithilfe von gezielter Instruktion („Prompt“) in natürlicher Sprache beeindruckende Resultate.

Wir befinden uns also inmitten einer Revolution in der KI-Entwicklung: Dank Prompt-Learning findet die Interaktion mit Modellen nicht länger über Code statt, sondern in natürlicher Sprache. Für die Demokratisierung der Sprachmodellierung bedeutet dies einen gigantischen Schritt vorwärts. Texte zu generieren oder neustens sogar Bilder zu erstellen, erfordert dadurch nicht mehr als rudimentäre Sprachkenntnisse. Allerdings heißt das nicht, dass damit auch überzeugende oder beeindruckende Resultate allen zugänglich sind. Hochwertige Outputs verlangen nach hochwertigen Inputs. Für uns Nutzer:innen bedeutet dies, dass die technische Planung in NLP nicht mehr der Modellarchitektur oder den Trainingsdaten gilt, sondern dem Design der Anweisungen, die Modelle in natürlicher Sprache erhalten. Willkommen im Zeitalter des Prompt-Engineerings.

Abbildung 1: Vom Prompt zur Prognose mit einem großen Sprachmodell

Prompts sind mehr als nur Textschnippsel

Templates erleichtern den häufigen Umgang mit Prompts

Da LLMs nicht auf einen spezifischen Anwendungsfall trainiert wurden, liegt es am Promptdesign dem Modell die genaue Aufgabenstellung zu bestimmen. Dazu dienen sogenannte „Prompt Templates“. Ein Template definiert die Struktur des Inputs, der an das Modell weitergereicht wird. Dadurch übernimmt das Template die Funktion des Fine-Tunings und legt den erwarteten Output des Modells für einen bestimmten Anwendungsfall fest. Am Beispiel einer einfachen Sentiment-Analyse könnte ein Prompt-Template so aussehen:

The expressed sentiment in text [X] is: [Z]

Das Modell sucht so nach einem Token z der, basierend auf den trainierten Parametern und dem Text in Position [X], die Wahrscheinlichkeit des maskierten Tokens an Position [Z] maximiert. Das Template legt so den gewünschten Kontext des zu lösenden Problems fest und definiert die Beziehung zwischen dem Input an Stelle [X] und dem zu vorhersagenden Output an Stelle [Z]. Der modulare Aufbau von Templates ermöglicht die systematische Verarbeitung einer Vielzahl von Texten für den erwünschten Anwendungsfall.

Abbildung 2: Prompt Templates definieren die Struktur eines Prompts.

Prompts benötigen nicht zwingend Beispiele

Das vorgestellte Template ist ein Beispiel eines sogenannten , da ausschließlich eine Anweisung, ohne jegliche Demonstration mit Beispielen im Template, vorhanden ist. Ursprünglich wurden LLMs von den Entwickler:innen von GPT-3 als „Few-Shot Learners“ bezeichnet, also Modelle, deren Leistung mit einer Auswahl von gelösten Beispielen des Problems maximiert werden kann (Brown et al., 2020). Eine Folgestudie konnte aber zeigen, dass mit strategischem Promptdesign auch 0-Shot Prompts, ohne einer Vielzahl von Beispielen, vergleichbare Leistung erzielen können (Reynolds & McDonell, 2021). Da also auch in der Forschung mit unterschiedlichen Ansätzen gearbeitet wird, stellt der nächste Abschnitt 5 Strategien für effektives Design von Prompt Templates vor.

5 Strategien für effektives Promptdesign

Task Demonstration

Im herkömmlichen Few-Shot Setting wird das zu lösende Problem durch die Bereitstellung von mehreren Beispielen eingegrenzt. Die gelösten Beispielsfälle sollen dabei eine ähnliche Funktion einnehmen wie die zusätzlichen Trainingssamples während des Fine-Tuning Prozesses und somit den spezifischen Anwendungsfall des Modells definieren. Textübersetzung ist ein gängiges Beispiel für diese Strategie, die mit folgendem Prompt Template repräsentiert werden kann:

French: „Il pleut à Paris“

German: „Es regnet in Paris“

French: „Copenhague est la capitale du Danemark“

German: „Kopenhagen ist die Hauptstadt von Dänemark“

[…]

French: [X]
German: [Z]

Die gelösten Beispiele sind zwar gut dazu geeignet das Problemsetting zu definieren, können aber auch für Probleme sorgen. „Semantische Kontamination“ bezeichnet das Problem, dass die Inhalte der übersetzten Sätze als relevant für die Vorhersage interpretiert werden können. Beispiele im semantischen Kontext der Aufgabe produzieren bessere Resultate – und solche außerhalb des Kontexts können dazu führen, dass die Prognose Z inhaltlich „kontaminiert“ wird (Reynolds & McDonell, 2021). Verwendet man das obige Template für die Übersetzung komplexer Sachverhalte, so könnte das Modell in unklaren Fällen wohl den eingegebenen Satz als Aussage über eine europäische Großstadt interpretieren.

Task Specification

Jüngste Forschung zeigt, dass mit gutem Promptdesign auch der 0-Shot Ansatz kompetitive Resultate liefern kann. So wurde demonstriert, dass LLMs gar keine vorgelösten Beispiele benötigen, solange die Problemstellung im Prompt möglichst genau definiert wird (Reynolds & McDonell, 2021). Diese Spezifikation kann unterschiedlich Formen annehmen, ihr liegt aber immer den gleichen Gedanken zugrunde: So genau wie möglich zu beschreiben was gelöst werden soll, aber ohne zu demonstriere wie.

Ein einfaches Beispiel für den Übersetzungsfall wäre folgender Prompt:

Translate from French to German [X]: [Z]

Dies mag bereits funktionieren, die Forscher empfehlen aber den Prompt so deskriptiv wie möglich zu gestalten und auch explizit die Übersetzungsqualität zu nennen:

A French sentence is provided: [X]. The masterful French translator flawlessly translates the sentence to German: [Z]

Dies helfe dem Modell dabei die gewünschte Problemlösung im Raum der gelernten Aufgaben zu lokalisieren.

Abbildung 3: Ein klarer Aufgabenbeschrieb kann die Prognosegüte stark erhöhen.

Auch in Anwendungsfällen ausserhalb von Übersetzungen ist dies empfehlenswert. Ein Text lässt mit einem einfachen Befehl zusammenfassen:

Summarize the following text: [X]: [Z]

Mit konkreterem Prompt sind aber bessere Resultate zu erwarten:

Rephrase this sentence with easy words so a child understands it,
emphasize practical applications and examples: [X]: [Z]

Je genauer der Prompt, desto grösser die Kontrolle über den Output.

Prompts als Einschränkungen

Zu Ende gedacht bedeutet der Ansatz von Kontrolle des Modells schlicht die Einschränkung des Modellverhaltens durch sorgfältiges Promptdesign. Diese Perspektive ist nützlich, denn während dem Training lernen LLMs viele verschiedene Texte fortzuschreiben und können dadurch eine breite Auswahl an Problemen lösen. Mit dieser Designstrategie verändert sich das grundlegende Herangehen ans Promptdesign: vom Beschrieb der Problemstellung zum Ausschluss unerwünschter Resultate durch die Einschränkung des Modellverhaltens. Welcher Prompt führt zum gewünschten Resultat und ausschliesslich zum gewünschten Resultat? Folgender Prompt weist zwar auf eine Übersetzungsaufgabe hin, beinhaltet aber darüber hinaus keine Ansätze zur Verhinderung, dass der Satz durch das Modell einfach zu einer Geschichte fortgesetzt wird.

Translate French to German Il pleut à Paris

Ein Ansatz diesen Prompt zu verbessern ist der Einsatz von sowohl semantischen als auch syntaktischen Mitteln:

Translate this French sentence to German: “Il pleut à Paris.”

Durch die Nutzung von syntaktischen Elementen wie dem Doppelpunkt und den Anführungszeichen wird klar gemacht, wo der zu übersetzende Satz beginnt und endet. Auch drückt die Spezifikation durch sentence aus, dass es nur um einen einzelnen Satz geht. Diese Mittel verringern die Wahrscheinlichkeit, dass dieser Prompt missverstanden und nicht als Übersetzungsproblem behandelt wird.

Nutzung von “memetic Proxies”

Diese Strategie kann genutzt werden, um die Informationsdichte in einem Prompt zu erhöhen und lange Beschreibungen durch kulturell verstandenen Kontext zu vermeiden. Memetic Proxies können in Taskbeschreibungen genutzt werden und nutzen implizit verständliche Situationen oder Personen anstelle von ausführlichen Anweisungen:

A primary school teacher rephrases the following sentence: [X]: [Z]

Dieser Prompt ist weniger deskriptiv als das vorherige Beispiel zur Umformulierung in einfachen Worten. Allerdings besitzt die beschriebene Situation eine viel höhere Dichte an Informationen: Die Nennung eines impliziert bereits, dass das Resultat für Kinder verständlich sein soll und erhöht dadurch hoffentlich auch die Wahrscheinlichkeit von praktischen Beispielen im Output. Ähnlich können Prompts fiktive Gespräche mit bekannten Persönlichkeiten beschreiben, so dass der Output deren Weltbild oder Sprechweise widerspiegelt:

In this conversation, Yoda responds to the following question: [X]

Yoda: [Z]

Dieser Ansatz hilft dabei einen Prompt durch implizit verstandenen Kontext kurz zu halten und die Informationsdichte im Prompt zu erhöhen.  Memetic Proxies finden auch im Promptdesign für andere Modalitäten Verwendung. Bei Modellen zur Bildgenerierung wie DALL-e 2 führt das Suffix „Trending on Artstation“ häufig zu Ergebnissen von höherer Qualität, obwohl semantisch eigentlich keine Aussagen über das darzustellende Bild gemacht werden.

Metaprompting

Als Metaprompting beschreibt das Forscherteam einer Studie den Ansatz Prompts durch Anweisungen anzureichern, die auf die jeweilige Aufgabe zugeschnitten sind. Dies beschreiben sie als Weg ein Modell durch klarere Anweisungen einzuschränken, sodass die gestellte Aufgabe besser gelöst werden kann (Reynolds & McDonell, 2021). Folgendes Beispiel kann dabei helfen mathematische Probleme zuverlässiger zu lösen und den Argumentationsweg nachvollziehbar zu machen:

[X]. Let us solve this problem step-by-step: [Z]

Ähnlich lassen sich Multiple Choice Fragen mit Metaprompts anreichern, sodass das Modell im Output auch tatsächlich eine Option wählt, anstatt die Liste fortzusetzen:

[X] in order to solve this problem, let us analyze each option and choose the best: [Z]

Metaprompts stellen somit ein weiteres Mittel dar Modellverhalten und Resultate einzuschränken.

Abbildung 4: Mithilfe von Metaprompts lassen sich Vorgehensweisen zur Problemlösung festlegen.

Ausblick

Prompt Learning ist ein noch sehr junges Paradigma und das damit eng verbundene Prompt Engineering befindet sich noch in den Kinderschuhen. Allerdings wird die Wichtigkeit von fundierten Fähigkeiten zum Schreiben von Prompts zweifellos nur noch zunehmen. Nicht nur Sprachmodelle wie GPT-3, sondern auch neuste Modelle zur Bildgenerierung verlangen von ihren User:innen solide Kenntnisse im Designen von Prompts, um gute Ergebnisse zu kreieren. Die vorgestellten Strategien sind sowohl in der Forschung als auch in der Praxis erprobte Ansätze zum systematischen Schreiben von Prompts, die dabei helfen, bessere Resultate von großen Sprachmodellen zu erhalten.

In einem zukünftigen Blogbeitrag nutzen wir diese Erfahrungen mit Textgenerierung, um Best Practices für eine weitere Kategorie generativer Modelle zu erschliessen: modernste Diffusionsmodelle zur Bildgenerierung, wie DALL-e 2, Midjourney und Stable Diffusion.

 

Quellen

Brown, Tom B. et al. 2020. “Language Models Are Few-Shot Learners.” arXiv:2005.14165 [cs]. http://arxiv.org/abs/2005.14165 (March 16, 2022).

Reynolds, Laria, and Kyle McDonell. 2021. “Prompt Programming for Large Language Models: Beyond the Few-Shot Paradigm.” http://arxiv.org/abs/2102.07350 (July 1, 2022).

Oliver Guggenbühl Oliver Guggenbühl Oliver Guggenbühl Oliver Guggenbühl

Du willst Python lernen? Oder bist du ein R-Profi und dir entfallen bei der Arbeit mit Python regelmäßig die wichtigen Funktionen und Befehle? Oder vielleicht brauchst du von Zeit zu Zeit eine kleine Gedächtnisstütze beim Programmieren? Genau dafür wurden Cheatsheets erfunden!

Cheatsheets helfen dir in all diesen Situationen weiter. Unser erstes Cheatsheet mit den Grundlagen von Python ist der Start einer neuen Blog-Serie, in der weitere Cheatsheets in unserem einzigartigen STATWORX Stil folgen werden.

Du kannst also neugierig sein auf unsere Serie von neuen Python-Cheatsheets, die sowohl Grundlagen als auch Pakete und Arbeitsfelder, die für Data Science relevant sind, behandeln werden.

Unsere Cheatsheets stehen euch zum kostenfreien Download frei zur Verfügung, ohne Anmeldung oder sonstige Paywall.

 

Warum haben wir neue Cheatsheets erstellt?

Als erfahrene R User sucht man schier endlos nach entsprechend modernen Python Cheatsheets, ähnlich denen, die du von R Studio kennst.

Klar, es gibt eine Vielzahl von Cheatsheets für jeden Themenbereich, die sich aber in Design und Inhalt stark unterscheiden. Sobald man mehrere Cheatsheets in unterschiedlichen Designs verwendet, muss man sich ständig neu orientieren und verliert so insgesamt viel Zeit. Für uns als Data Scientists ist es wichtig, einheitliche Cheatsheets zu haben, anhand derer wir schnell die gewünschte Funktion oder den Befehl finden können.

Diesem nervigen Zusammensuchen von Informationen wollen wir entgegenwirken. Daher möchten wir auf unserem Blog zukünftig regelmäßig neue Cheatsheets in einer Designsprache veröffentlichen – und euch alle an dieser Arbeitserleichterung teilhaben lassen.

 

Was enthält das erste Cheatsheet?

Unser erstes Cheatsheet in dieser Reihe richtet sich in erster Linie an Python-Neulinge, an R-Nutzer, die Python seltener verwenden, oder an Leute, die gerade erst anfangen, mit Python zu arbeiten.

Es erleichtert den Einstieg und Überblick in Python. Die grundlegende Syntax, die Datentypen und der Umgang mit diesen werden vorgestellt und grundlegende Kontrollstrukturen eingeführt. So kannst du schnell auf die Inhalte zugreifen, die du z.B. in unserer STATWORX Academy gelernt hast oder dir die Grundlagen für dein nächstes Programmierprojekt ins Gedächtnis rufen.

Was behandelt das STATWORX Cheatsheet Episode 2?

Das nächste Cheatsheet behandelt den ersten Schritt eines Data Scientists in einem neuen Projekt: Data Wrangling. Außerdem erwartet dich ein Cheatsheet für pandas über das Laden, Auswählen, Manipulieren, Aggregieren und Zusammenführen von Daten. Happy Coding!

Niklas Junker Niklas Junker

Der Anfang der Adventszeit brachte dieses Jahr nicht nur den ersten Schnee in der Rhein-Main Region (Abb. 1), sondern leitete auch bei vielen nach einem Jahr voller turbulenter Ereignisse und endlosen Stunden im Homeoffice die alljährliche Zeit der Besinnlichkeit ein. Obgleich sich die angesprochene Besinnung bei vielen eher euphemistisch äußert, in Form von Geschenk- und Planungsstress oder der alljährlichen Jahresendrallye bei STATWORX, dürfen gerne auch Erinnerungen an gutes Essen, kitschige Musik und gemütliche Abende heraufbeschworen werden.

Abb. 1: Schnee in Rhein-Main
Abb. 1: Für einen kurzen Moment scheint der Weltenbrand durch unsere Lieblingsniederschlagsform erloschen, und die Welt wirkt restlos in unschuldiges und reines Weiß getaucht.
Anm. der Redaktion: Der Autor hatte eine schwierige Woche. Nichtsdestotrotz gestehen wir ein, dass seine Worte ein positives Stimmungsbild inspirieren könnten.

Mit dem Advent bricht bekanntlich auch die Zeit der Weihnachtsplätzchen an. Eine exzellente Gelegenheit, bei meinen STATWORX-Kolleg*innen unter dem Vorwand der Nächstenliebe Sympathie und professionelles Ansehen durch unschuldig wirkende, zuckerhaltige Kalorienbomben inklusive Deep-Learning-Kniff zu erlangen. In Ergänzung zu meinem vorangegangenen Artikel zum Thema Kochen mit Deep Learning, möchte ich in diesem Beitrag eine kleine Web-App vorstellen, mit der sich alternative Zutaten für Rezepte vorschlagen lassen, untermalt von KI-generierten Weihnachtsliedern. Ziel dieser Artikel (und anderer, wie zum Beispiel unsere Super Mario spielende KI) ist es, die Grundlagen hinter den oftmals etwas mystisch wirkenden Deep Learning Methoden anschaulich zu vermitteln. In vielen Gesprächen entstand bei uns der Eindruck, dass durch solch einen Überblick spannende Anwendungs- und Geschäftsfälle wesentlich besser erschlossen werden können, indem man sich zunächst bewusst macht, welche grundsätzlichen Anwendungen mittels Deep Learning erschlossen werden können.

Alternative Zutaten für Rezepte finden mittels Word Embeddings

Schon länger dominieren Anwendungen und Publikationen zum Thema Natural Language Processing die Deep Learning Forschung. Regelmäßig ermöglichen bahnbrechende Ergebnisse neue Anwendungsfälle. Eines der Schlagworte war hier zunächst Word Embeddings mittels Word2Vec und nun brandaktuell Transformer-Modelle und ihr Attention-Mechanismus, wie beispielsweise BERT oder GPT-3.

Zur genaueren Veranschaulichung dieser Methode werden im Folgenden zunächst die Grundlagen von Word Embeddings erläutert. Anschließend wird aufgezeigt, wie diese Methode genutzt wurde, um Zutaten in Rezepten durch andere, möglichst ähnlich-verwendete Lebensmittel auszutauschen. Dabei werden nicht einfach „blind“ Vorschläge gemacht sondern aus ähnlichen, kontextuell verwandten Rezepten Vorschläge erkannt und abgeleitet. Somit kann (hoffentlich) sichergestellt werden, dass das alternative Rezept zur kulinarischen Freude wird und nicht unmittelbar im Mülleimer landet.

Grundlagen von Word Embeddings

Word2Vec ist ein ausgezeichnetes Beispiel, um eine der zentralen Konzepte des Deep Learnings zu veranschaulichen: das Representation Learning. Sehr vereinfacht dem Gehirn nachempfunden, durchlaufen Daten auch in künstlichen neuronalen Netzen mehrere Schichten. Jede Schicht verändert und kombiniert die Daten vorhergehender Schichten dahingehend, dass eine dichtere und abstraktere Repräsentation der Daten entsteht. Diese Ansammlungen an Zahlen, die aus menschlicher Sicht keine intuitive Bedeutung mehr haben, enthalten nicht mehr alle Informationen der Ursprungsdaten sondern nur noch jene, die für das Modell zur Lösung der jeweiligen Aufgabe als relevant eingestuft werden. Beim Training der Modelle wird die Repräsentationen also dahingehend optimiert, die Fehler der Modell-Vorhersage zu minimieren.

Möchte man also Word Embeddings nutzen, um Worte hinsichtlich ihrer Bedeutung zu vergleichen oder Ähnlichkeiten zu erkennen, wird ein Deep Learning Modell zum allgemeinen Sprachverständnis trainiert. Dies beinhaltet meistens Aufgaben, wie die Vorhersage benachbarter Worte oder des nächsten Satzes. Um diese Aufgaben lösen zu können, muss das Modell also die grundsätzliche Verwendung von Worten, insb. im Kontext von anderen Worten erlernen. Da man im Falle unserer Word Embeddings aber nur an den internen Repräsentierung der einzelnen Worte interessiert ist, ist dieses Vorgehen (Abb. 2) eigentlich nur Mittel zum Zweck.

Abb.2: Word2Vec Verfahren zur Vorhersage von benachbarten Worten

Beim Word2Vec Verfahren wird im Training versucht, ausgehend vom mittleren Wort die links und rechts benachbarten Worte vorherzusagen. Wichtig ist, dass die Repräsentierung des mittleren Wortes durch das Umfeld bzw. den Kontext geprägt wird. Treten Worte in ähnlichen Kontexten auf, ähnelt sich ihre numerische Repräsentierung. Daraus erwarten wir beispielsweise, dass die Embeddings von Feldsalat und Radicchio sich wesentlich näher sind (häufiger Kontext: Öl, Zwiebeln, Essig, Schüssel…), als die von Feldsalat und Dosenananas (seltener Kontext: trauriger verkaterter Sonntag im Studium).

Anwendung auf ein konkretes Beispiel: Pätzchen-Rezepte mit Bake2Vec

Wie auch im vorangegangenen Artikel zu diesem Beitrag wurden die Embeddings auf einer großen Ansammlung an Texten, genannt Corpus, trainiert. In diesem Fall ca. eine Million Rezepte aus Büchern und vor allem bekannten (englischen) Rezept-Webseiten, wie von der BBC oder Epicurious.

Hierbei prägen zwei verschiedene Kontexte die Embeddings:

  1. Die Auflistung aller Zutaten eines Gerichts
  2. Die Fließtext-Anleitung zur Zubereitung, also die angeleiteten Schritte im Kochrezept selbst

Nach dem Training entsteht eine Datenbank an Wort- und Embedding-Paaren, die mittels bestimmter Metriken verglichen werden können. Für gewöhnlich wird die Kosinus-Ähnlichkeit zwischen den Embeddings berechnet, womit man einen normierten Wert zwischen 0 und 1 enthält, der sozusagen die Güte einer Zutat als Ersatz einer anderen abbildet.  (Abb. 3)

Abb.3: Screenshot Web-App
Abb. 3: Screenshot aus der Web-App zur Recherche von Alternativzutaten

Für jede Zutat lassen sich Alternativen anzeigen. Beurteilen Sie die Vorschläge im Vergleich zu Ihren eigenen Ideen! Auch können Sie die Zutaten im Textfeld ändern/überschreiben, und sich Vorschläge für diese anzeigen lassen. Hier gehts zur Web-App.

Mit Generativen Modellen Weihnachtslieder generieren

Generative Modelle beweisen schon länger, zu welchen eindrucksvollen Resultaten Deep Learning abseits klassischer Machine Learning Anwendungen, wie Klassifikation und Regression, fähig sind. Anwendungsfälle wie das Generieren von fehlenden Datenpunkten oder das Ausfüllen von verdeckten Flächen in Bildern finden in der Geschäftswelt Anwendung. Künstler*innen und Enthusiasten erstellen dagegen Kunstwerke, Gedichte oder Fotos von nicht-existierenden Gesichtern. Vor 2 Jahren erzielte sogar das erste KI-generierte Porträtgemälde über $400.000 bei einer Auktion.

Neben der bildenden Kunst haben generative Modelle auch in der Musikszene ersten Anklang gefunden. Im folgenden Abschnitt betrachten wir die Generierung von Weihnachtsliedern am Beispiel einfacher Jingles und Ausschnitte wie „Oh Tannenbaum oder „Alle Jahre wieder. Damit schaffen wir zum Wohle der Lesenden und Hörenden einen Mariah Carey- und Michael Bublé-freien Safe Space, dem aber leider auch die George Michaels, Crosbys und Sinatras zum Opfer fallen.

Tensorflow Magenta

Das Magenta Projekt möchte „die Rolle von Machine Learning in kreativen Prozessen“ erforschen und bietet dabei eine Vielzahl an Veröffentlichungen, Programmier-Bibliotheken und Tools zur Generierung von Musik. Die Dance-Punk Band YACHT arbeitete daraufhin mit Google zusammen und erstellte einen Katalog aus den MIDI Tonspuren ihrer bisherigen Lieder, die dann einzeln oder gepaart (z.B. Gitarren- und Bass-Riff zusammen) verwendet wurden, um neue Musik im Stil des bisherigen Songs der Band zu generieren. Ähnlich wurde auch mit den Liedtexten verfahren. Selbst das Album Cover und die Videos im Hintergrund der Konzerte wurden mit Deep Learning Modellen generiert und verfeinert.

Die im nächsten Abschnitt beschriebenen Rekurrenten Neuronale Netze bieten eine etablierte Methode im Umgang mit einspurigen Riffs oder Jingles. Verschiedene Ausführungen und Varianten davon sind im „Magenta-Werkzeugkasten“ als MelodyRNN enthalten.

Rekurrente Modelle

Vor der Einführung von Transformer-Modellen wurden im Bereich NLP größtenteils Rekurrente Neuronale Netze (RNNs) verwendet, die von Natur aus besonders geeignet sind, sequenziellen Daten zu verarbeiten. Dies bedeutet, dass Rekurrente Netze es ermöglichen, Entscheidungen oder Vorhersagen bezüglich aufeinander folgender, abhängiger Datenpunkte zu treffen. Hierzu zählen bspw. aufeinanderfolgende Worte in einem Satz, Werte aus Sensoren im Zeitverlauf, oder eben Noten einer Partitur, die im Zeitverlauf betrachtet eine Melodie ergeben. Am Beispiel einer Wortsequenz ist es also wichtig, dass ein Machine Learning Modell einordnen kann, welche Worte in welcher Abfolge im Text erscheinen.

Um diese Informationen aus den Daten ableiten zu können, pflegen RNNs einen internen Zustand, der zusätzlich zur oben beschriebenen Repräsentation nicht nur vom aktuell betrachteten Wort, sondern auch vom internen Zustand des RNNs bei vorherigen Worten abhängt. (Abb. 4)

Abb. 4: RNN
Abb. 4: RNN Beispiel

Das RNN pflegt einen internen Zustand vorangegangener Inputs. Der neue Zustand ergibt sich aus einer Gewichtung des alten Zustands und des aktuellen Inputs.

Als Output wird in diesem Fall versucht, das folgende Wort vorherzusagen. Das RNN sollte erkennen, dass im Kontext des Wortes Weihnachten eher das Wort „backen“ vorhergesagt werden muss, und nicht etwa „kaufen“.

Um ein generatives Modell zu entwickeln, trainiert man das RNN darauf, für eine gegebene Sequenz an Worten stets das nächste, passende Wort vorherzusagen. Wendet man das Modell anschließend an, präsentiert man eben eine solche Sequenz an Worten als vorgegebenen Input, nimmt den vorhergesagten Output des Modells hinzu, füttert diesen als folgenden Input wieder in das Modell und wiederholt diesen Vorgang beliebig oft. So macht das Modell Vorhersagen auf dessen eigene Vorhersagen und generiert somit einen Text.

Musik generieren

Da es sich bei Noten von Volks- und Weihnachtsliedern auch um eine sequenzielle Abfolge von Daten handelt, lässt sich die zuvor beschriebene Methodik anschaulich auf musikalische Daten übertragen.

Möchte man ein Lied in einem bestimmten Stil oder Genre eines anderen Liedes weiterführen oder generieren, kann man im einfachsten Falle ein von Magenta publiziertes (und vortrainiertes) MelodyRNN verwenden. Dort gibt man die gewünschte Anfangsmelodie als MIDI Datei vor und definiert ggf. noch weitere Parameter wie Länge oder Freiheitsgrade in der Generierung. Anschließend wird das Lied, analog zum Text-Beispiel, Ton für Ton synthetisiert.

Abb. 5: Generierte Musik "oh Tannenbaum"
Abb.5: Kein Vivaldi, dafür aber eine Flasche Primitivo; der musikalisch gänzlich untalentierte Autor schafft es eines Nachts, Noten in einspurige MIDI Dateien ohne Taktwechsel zu exportieren, um sie mit Magenta verarbeiten zu können.

Für den interessierten Lesenden gibt es an dieser Stelle noch einiges zu entdecken! Um den Bogen zum Thema Representation Learning zu schlagen, empfehlen wir einen Blick auf MusicVAE [https://magenta.tensorflow.org/music-vae]. Mit einem Variational Autoencoder lassen sich Embeddings (merke: nicht nur auf Worte beschränkt, sondern auch ganze Texte, Bilder, Lieder,…) erlernen, welche einfach und strukturiert manipulierbar sind. Somit lässt sich beispielsweise eine Komponente eines Embeddings ändern, die das Musikgenre darstellt, aber nicht die Melodie. (Re)konstruiert man anschließend das Embedding, erhält man ein Lied mit der gleichen Melodie, jedoch im Stil einer anderen Musikrichtung.

Beispiele für State-of-the-Art Musikgenerierung finden sich auch in der OpenAI Jukebox, welche sich auch eines VAE-Modells bedient.

Fazit

Bei den in diesem Artikel beschriebenen Methoden muss natürlich meist etwas Auslese oder Feinabstimmung betrieben werden, um die gewünschten Ergebnissen zu erzielen. Im Falle der kreativen Anwendung, wie bei der Band YACHT, wurden tausende, wenn nicht zehntausend Melodien generiert und durchsucht, um zufriedenstellende Resultate zu erhalten. Bei den Plätzchenzutaten hingegen ging es eher um die Exploration von Modell-Varianten und -Parametern, welche dann zu beständig guten Ergebnissen führen.

Der Autor hofft, dass Ihnen der kleine Einblick in Methoden des Natural Language Processing gefallen hat und möchte Sie animieren, die Plätzchen Web-App auszuprobieren, oder diese zu missbrauchen, um sich Alternativen zu deftigen Zutaten oder Gemüse auszugeben.

Bleiben Sie so artig, wie nötig! Frohe Weihnachten! J.

Anmerkungen & Quellen

A) Hallo und danke Mama!

Abb. 6: Plaetzchen
Abb. 6: Eine unvollständige Auswahl der Plätzchen der Familie des Autors.

Während mütterlicherseits jährlich über 20 Sorten gebacken werden, schafft es die Tante nur auf mickrige acht. Die Begeisterung und das Interesse am Kochen und Backen ist im Kindesalter bereits auf den Autor übergesprungen. Im Gegenzug jedoch wundert sich dessen Familie des Öfteren, „was dieses Deep Learning denn genau ist“ und was der Junge auf der Arbeit eigentlich macht. Anwendungsbeispiele wie diese Web-App sind dabei wunderbare Gelegenheiten, Methoden des Deep Learning anschaulich zu erklären, Alltagsbezug zu schaffen und sich für die vielen kommenden fachlichen Herausforderungen zu motivieren!

B) Die Rezepte dieser kleinen Exkursion wurden Chefkoch.de entnommen, zu den Originalen gelangen Sie über die folgenden Links:

 

Jonas Braun Jonas Braun

Did you ever want to make your machine learning model available to other people, but didn’t know how? Or maybe you just heard about the term API, and want to know what’s behind it? Then this post is for you!

Here at STATWORX, we use and write APIs daily. For this article, I wrote down how you can build your own API for a machine learning model that you create and the meaning of some of the most important concepts like REST. After reading this short article, you will know how to make requests to your API within a Python program. So have fun reading and learning!

What is an API?

API is short for Application Programming Interface. It allows users to interact with the underlying functionality of some written code by accessing the interface. There is a multitude of APIs, and chances are good that you already heard about the type of API, we are going to talk about in this blog post: The web API.

This specific type of API allows users to interact with functionality over the internet. In this example, we are building an API that will provide predictions through our trained machine learning model. In a real-world setting, this kind of API could be embedded in some type of application, where a user enters new data and receives a prediction in return. APIs are very flexible and easy to maintain, making them a handy tool in the daily work of a Data Scientist or Data Engineer.

An example of a publicly available machine learning API is Time Door. It provides Time Series tools that you can integrate into your applications. APIs can also be used to make data available, not only machine learning models.

API Illustration

And what is REST?

Representational State Transfer (or REST) is an approach that entails a specific style of communication through web services. When using some of the REST best practices to implement an API, we call that API a „REST API“. There are other approaches to web communication, too (such as the Simple Object Access Protocol: SOAP), but REST generally runs on less bandwidth, making it preferable to serve your machine learning models.

In a REST API, the four most important types of requests are:

  • GET
  • PUT
  • POST
  • DELETE

For our little machine learning application, we will mostly focus on the POST method, since it is very versatile, and lots of clients can’t send GET methods.

It’s important to mention that APIs are stateless. This means that they don’t save the inputs you give during an API call, so they don’t preserve the state. That’s significant because it allows multiple users and applications to use the API at the same time, without one user request interfering with another.

The Model

For this How-To-article, I decided to serve a machine learning model trained on the famous iris dataset. If you don’t know the dataset, you can check it out here. When making predictions, we will have four input parameters: sepal length, sepal width, petal length, and finally, petal width. Those will help to decide which type of iris flower the input is.

For this example I used the scikit-learn implementation of a simple KNN (K-nearest neighbor) algorithm to predict the type of iris:

# model.py
from sklearn import datasets
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.externals import joblib
import numpy as np


def train(X,y):

    # train test split
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)

    knn = KNeighborsClassifier(n_neighbors=1)

    # fit the model
    knn.fit(X_train, y_train)
    preds = knn.predict(X_test)
    acc = accuracy_score(y_test, preds)
    print(f'Successfully trained model with an accuracy of {acc:.2f}')

    return knn

if __name__ == '__main__':

    iris_data = datasets.load_iris()
    X = iris_data['data']
    y = iris_data['target']

    labels = {0 : 'iris-setosa',
              1 : 'iris-versicolor',
              2 : 'iris-virginica'}

    # rename integer labels to actual flower names
    y = np.vectorize(labels.__getitem__)(y)

    mdl = train(X,y)

    # serialize model
    joblib.dump(mdl, 'iris.mdl')

As you can see, I trained the model with 70% of the data and then validated with 30% out of sample test data. After the model training has taken place, I serialize the model with the joblib library. Joblib is basically an alternative to pickle, which preserves the persistence of scikit estimators, which include a large number of numpy arrays (such as the KNN model, which contains all the training data). After the file is saved as a joblib file (the file ending thereby is not important by the way, so don’t be confused that some people call it .model or .joblib), it can be loaded again later in our application.

The API with Python and Flask

To build an API from our trained model, we will be using the popular web development package Flask and Flask-RESTful. Further, we import joblib to load our model and numpy to handle the input and output data.

In a new script, namely app.py, we can now set up an instance of a Flask app and an API and load the trained model (this requires saving the model in the same directory as the script):

from flask import Flask
from flask_restful import Api, Resource, reqparse
from sklearn.externals import joblib
import numpy as np

APP = Flask(__name__)
API = Api(APP)

IRIS_MODEL = joblib.load('iris.mdl')

The second step now is to create a class, which is responsible for our prediction. This class will be a child class of the Flask-RESTful class Resource. This lets our class inherit the respective class methods and allows Flask to do the work behind your API without needing to implement everything.

In this class, we can also define the methods (REST requests) that we talked about before. So now we implement a Predict class with a .post() method we talked about earlier.

The post method allows the user to send a body along with the default API parameters. Usually, we want the body to be in JSON format. Since this body is not delivered directly in the URL, but as a text, we have to parse this text and fetch the arguments. The flask _restful package offers the RequestParser class for that. We simply add all the arguments we expect to find in the JSON input with the .add_argument() method and parse them into a dictionary. We then convert it into an array and return the prediction of our model as JSON.

class Predict(Resource):

    @staticmethod
    def post():
        parser = reqparse.RequestParser()
        parser.add_argument('petal_length')
        parser.add_argument('petal_width')
        parser.add_argument('sepal_length')
        parser.add_argument('sepal_width')

        args = parser.parse_args()  # creates dict

        X_new = np.fromiter(args.values(), dtype=float)  # convert input to array

        out = {'Prediction': IRIS_MODEL.predict([X_new])[0]}

        return out, 200

You might be wondering what the 200 is that we are returning at the end: For APIs, some HTTP status codes are displayed when sending requests. You all might be familiar with the famous 404 - page not found code. 200 just means that the request has been received successfully. You basically let the user know that everything went according to plan.

In the end, you just have to add the Predict class as a resource to the API, and write the main function:

API.add_resource(Predict, '/predict')

if __name__ == '__main__':
    APP.run(debug=True, port='1080')

The '/predict' you see in the .add_resource() call, is the so-called API endpoint. Through this endpoint, users of your API will be able to access and send (in this case) POST requests. If you don’t define a port, port 5000 will be the default.

You can see the whole code for the app again here:

# app.py
from flask import Flask
from flask_restful import Api, Resource, reqparse
from sklearn.externals import joblib
import numpy as np

APP = Flask(__name__)
API = Api(APP)

IRIS_MODEL = joblib.load('iris.mdl')


class Predict(Resource):

    @staticmethod
    def post():
        parser = reqparse.RequestParser()
        parser.add_argument('petal_length')
        parser.add_argument('petal_width')
        parser.add_argument('sepal_length')
        parser.add_argument('sepal_width')

        args = parser.parse_args()  # creates dict

        X_new = np.fromiter(args.values(), dtype=float)  # convert input to array

        out = {'Prediction': IRIS_MODEL.predict([X_new])[0]}

        return out, 200


API.add_resource(Predict, '/predict')

if __name__ == '__main__':
    APP.run(debug=True, port='1080')

Run the API

Now it’s time to run and test our API!

To run the app, simply open a terminal in the same directory as your app.py script and run this command.

python run app.py

You should now get a notification, that the API runs on your localhost in the port you defined. There are several ways of accessing the API once it is deployed. For debugging and testing purposes, I usually use tools like Postman. We can also access the API from within a Python application, just like another user might want to do to use your model in their code.

We use the requests module, by first defining the URL to access and the body to send along with our HTTP request:

import requests

url = 'http://127.0.0.1:1080/predict'  # localhost and the defined port + endpoint
body = {
    "petal_length": 2,
    "sepal_length": 2,
    "petal_width": 0.5,
    "sepal_width": 3
}
response = requests.post(url, data=body)
response.json()

The output should look something like this:

Out[1]: {'Prediction': 'iris-versicolor'}

That’s how easy it is to include an API call in your Python code! Please note that this API is just running on your localhost. You would have to deploy the API to a live server (e.g., on AWS) for others to access it.

Conclusion

In this blog article, you got a brief overview of how to build a REST API to serve your machine learning model with a web interface. Further, you now understand how to integrate simple API requests into your Python code. For the next step, maybe try securing your APIs? If you are interested in learning how to build an API with R, you should check out this post. I hope that this gave you a solid introduction to the concept and that you will be building your own APIs immediately. Happy coding!

 

Jannik Klauke

Jannik Klauke

In meinem ersten Blogbeitrag dieser Reihe habe ich gezeigt, wie du deine R-Skripte in einem Docker-Container ausführen kannst. Für viele der Projekte, an denen wir hier bei STATWORX arbeiten, verwenden wir das RShiny-Framework, um unsere Produkte in interaktive Webapplikationen zu verwandeln.

Die Verwendung von Containern hat für die Bereitstellung von ShinyApps eine Vielzahl von Vorteilen. Zunächst sind es die üblichen Vorzüge wie einfache Cloud-Bereitstellung, Skalierbarkeit und praktisches Scheduling, aber es behebt auch einen der wesentlichen Nachteile von Shiny: Shiny erstellt nur eine einzige R-Sitzung pro App. Sollten also mehrere User auf dieselbe App zugreifen, dann arbeiten sie alle mit der selben R-Sitzung, was zu einer Vielzahl von Problemen führt. Mithilfe von Docker können wir dieses Problem umgehen und für jede:n User:in eine eigene Container-Instanz starten. Dadurch erhält jede:r User:in Zugriff auf eine eigene Instanz der App und somit eine eigene R-Sitzung. Ich gehe davon aus, dass du meinen vorigen Blogbeitrag über das Einbinden von R-Skripten in ein Docker-Image gelesen hast oder bereits über Grundkenntnisse der Docker-Terminologie verfügst.

Lassen wir also einfache R-Skripte hinter uns und führen wir jetzt ganze ShinyApps in Docker aus!

 

Das Setup

Einrichten eines Projekts

Für die Arbeit mit ShinyApps ist es ratsam, das Projekt-Setup von RStudio zu nutzen, insbesondere wenn man Docker verwendet. Projekte erleichtern nicht nur die Ordnung in RStudio, sondern ermöglichen es uns auch, mit dem Paket renv eine Paketbibliothek für unser spezifisches Projekt einzurichten. Dies ist besonders praktisch, um die benötigten Pakete für ein Programm in ein Docker-Image zu installieren.

Zu Demonstrationszwecken verwende ich eine Beispiel-App, die in einem früheren Blogbeitrag erstellt wurde und die du aus dem STATWORX GitHub Repository klonen kannst. Sie befindet sich im Unterordner „example-app“ und besteht aus den drei typischen Skripten, die von ShinyApps genutzt werden (global.R, ui.R und server.R) sowie aus Dateien, die zur Paketbibliothek renv gehören. Solltest du zur Übung ebenfalls die oben verlinkte Beispiel-App verwenden, musst du kein eigenes RStudio-Projekt einrichten. Stattdessen kannst du example-app.Rproj öffnen, das den von mir bereits eingerichteten Projektkontext startet. Falls du direkt mit einer eigenen App arbeiten möchtest und noch kein Projekt dafür erstellt hast, kannst du dein eigenes Projekt einrichten, indem du die von RStudio bereitgestellten Anweisungen befolgst.

Einrichten einer Paketbibliothek

Das RStudio-Projekt, das ich zur Verfügung gestellt habe, wird bereits mit einer Paketbibliothek geliefert, die in der Datei renv.lock gespeichert ist. Wenn du es vorziehst, mit deiner eigenen Anwendung zu arbeiten, kannst du deine eigene renv.lock Datei erstellen, indem du das renv Paket von deinem RStudio Projekt aus installierst und renv::init() ausführst. Dies initialisiert renv für dein Projekt und erstellt eine renv.lock Datei in deinem Projekt-Stammverzeichnis. Mehr Informationen über renv findest du unter RStudio’s Einführungsartikel.

Das Dockerfile

Das Dockerfile ist erneut das zentrale Element bei der Erstellung des Docker-Images. Während wir bisher nur ein einziges Skript in ein Image eingebaut haben, wollen wir diesen Prozess nun für eine ganze Anwendung wiederholen. Der Schritt von einem einzelnen Skript zu einem Ordner mit mehreren Skripten ist klein, aber es sind einige bedeutende Änderungen erforderlich, damit die App reibungslos läuft.

# Base image https://hub.docker.com/u/rocker/
FROM rocker/shiny:latest

# system libraries of general use
## install debian packages
RUN apt-get update -qq && apt-get -y --no-install-recommends install 
    libxml2-dev 
    libcairo2-dev 
    libsqlite3-dev 
    libmariadbd-dev 
    libpq-dev 
    libssh2-1-dev 
    unixodbc-dev 
    libcurl4-openssl-dev 
    libssl-dev

## update system libraries
RUN apt-get update && 
    apt-get upgrade -y && 
    apt-get clean

# copy necessary files
## app folder
COPY /example-app ./app
## renv.lock file
COPY /example-app/renv.lock ./renv.lock

# install renv & restore packages
RUN Rscript -e 'install.packages("renv")'
RUN Rscript -e 'renv::consent(provided = TRUE)'
RUN Rscript -e 'renv::restore()'

# expose port
EXPOSE 3838

# run app on container start
CMD ["R", "-e", "shiny::runApp('/app', host = '0.0.0.0', port = 3838)"]

Das Base-Image

Die erste Änderung betrifft das Base-Image. Da wir hier eine ShinyApp verdockern, können wir uns eine Menge Arbeit ersparen, indem wir das Base Image „rocker/shiny“ verwenden. Dieses Image kümmert sich um die notwendigen Dependencies für die Ausführung einer ShinyApp und enthält mehrere R-Pakete, die bereits vorinstalliert sind.

Erforderliche Dateien

Es ist natürlich notwendig, alle relevanten Skripte und Dateien für deine ShinyApp in dein Docker-Image einzubauen. Das Dockerfile erledigt genau das, indem es den gesamten Ordner, der die Anwendung enthält, in das Image kopiert.

Um die benötigten R-Pakete in ihrer korrekten Version in das Docker-Image zu installieren, verwenden wir am besten renv. Deshalb kopieren wir zuerst die Datei renv.lock separat in das Image. Das renv-Paket muss ebenfalls separat installiert werden, indem wir die Fähigkeit des Dockerfiles nutzen, mithilfe von RUN Rscript -e R-Code auszuführen. Diese Paketinstallation erlaubt es uns renv direkt aufzurufen und die kopierte Paketbibliothek in renv.lock innerhalb des Images mit renv::restore() wiederherzustellen. Dadurch wird die gesamte Paketbibliothek im Docker-Image installiert, mit genau der gleichen Version und dem gleichen Quellcode aller Pakete wie in der lokalen Projektumgebung. Und all dies mit nur ein paar Zeilen Code in unserem Dockerfile.

Starten der App zur Laufzeit

Ganz am Ende des Dockerfiles weisen wir den Container an, den folgenden R-Befehl auszuführen:

shiny::runApp('/app', host = '0.0.0.0', port = 3838)

Das erste Argument legt den Dateipfad zu den benötigten Skripten fest, welche in diesem Fall unter ./app abgelegt sind. Für den exponierten Port habe ich 3838 gewählt. Dies entspricht der Standardeinstellung für RStudio Server, kann aber nach Belieben geändert werden.

Wenn der letzte Befehl ausgeführt wird, startet jeder Container, der auf diesem Image basiert, die betreffende Anwendung automatisch zur Laufzeit (und schließt sie natürlich wieder, wenn sie beendet wurde).

Der letzte Schliff

Mit dem fertigen Dockerfile fehlen nur noch wenige Schritte bis zum laufenden Docker-Container. Dazu muss zunächst noch das Image erstellt werden, um anschließend einen Container basierend auf diesem Image zu starten.

Erstellen des Images

Also öffnen wir das Terminal, navigieren zu dem Ordner, der unser neues Dockerfile enthält und starten den Erstellungsprozess des Images:

docker build -t my-shinyapp-image . 

Starten eines Containers

Nachdem dieser Prozess abgeschlossen ist, können wir nun unser neu erstelltes Image testen, indem wir einen Container starten:

docker run -d --rm -p 3838:3838 my-shinyapp-image

Und da ist sie, die ShinyApp läuft auf localhost:3838!

docker-shiny-app-example

Ausblick

Jetzt, wo die ShinyApp in einem Docker-Container läuft, ist sie bereit für den Einsatz. Die Containerisierung unserer App macht diesen Prozess bereits sehr viel einfacher. Es gibt jedoch noch weitere Tools, die wir einsetzen, um Sicherheit, Skalierbarkeit und nahtloses Deployment auf dem neuesten Stand der Technik zu gewährleisten. Im nächsten Beitrag erwartet dich deshalb eine Einführung in ShinyProxy, womit du noch tiefer in das Spektrum an Möglichkeiten von RShiny und Docker eintauchen kannst.

Oliver Guggenbühl Oliver Guggenbühl

Aus meiner Erfahrung bei STATWORX weiß ich, dass der beste Weg zum Lernen das Probieren ist – mit etwas Hilfe von einem Freund! In diesem Artikel werde ich euch einen hands-on Guide zum Bauen eines Dashboards in Python geben. Als Framework verwenden wir Dash, mit dem Ziel ein einfaches Dashboard mit einer Dropdown-Liste und zwei reaktiven Graphen zu erstellen.

dash-app-final

Entwickelt als eine Open-Source-Bibliothek von Plotly, baut das Python Framework Dash auf Flask, Plotly.js und React.js auf. Das Framework ermöglicht die Erstellung von interaktiven Webapplikationen in purem Python und eignet sich besonders für das Teilen von Datenanalysen.

Solltest du Interesse an der Erstellung von interaktiven Grafiken mit Python haben, kann ich den Blog meines Kollegen Markus, Plotly – An Interactive Charting Library, sehr empfehlen.

Ein grundlegendes Verständnis von HTML und CSS ist für die Erstellung von Webapplikationen empfohlen. Um diesem Blog folgen zu können, werden wir alle notwendigen externen Ressourcen zur Verfügung stellen und die Rollen von HTML und CSS in einem Dash(board) erläutern.

Der Source Code ist auf GitHub verfügbar.

Vorraussetzungen

Das Projekt besteht aus einem Stylesheet style.css, den Beispieldaten stockdata2.csv und der eigentlichen Dash-Applikation app.py.

Laden des Stylesheet

Die Verwendung eines Stylesheets ist für die Funktionalitäten des Dashboards nicht notwendig. Damit das Dashboard jedoch so aussieht, wie in unseren Beispielen, kann die Datei style.css von unserem STATWORX GitHub heruntergeladen werden. Das verwendete Stylesheet is eine leicht veränderte Version des Stylesheets der Dash Uber Rides Demo. Dash lädt automatisch .css-Dateien, die sich im Unterordner assets befinden.

dashapp
    |--assets
        |-- style.css
    |--data
        |-- stockdata2.csv
    |-- app.py

Die Dokumentation zu externen Ressourcen (u.a. Stylesheets) kann unter folgendem Link gefunden werden: https://dash.plot.ly/external-resources

Laden der Daten

Für unser Dashboard verwenden wir den Datensatz stockdata2.csv. Der Datensatz hat folgende Struktur:

date stock value change
2007-01-03 MSFT 23.95070 -0.1667
2007-01-03 IBM 80.51796 1.0691
2007-01-03 SBUX 16.14967 0.1134
import pandas as pd

# Load data
df = pd.read_csv('data/stockdata2.csv', index_col=0, parse_dates=True)
df.index = pd.to_datetime(df['Date'])

Erste Schritte – Wie man eine Dash-App startet

Nach der Installation von Dash (Anleitung kann hier gefunden werden) können wir die App starten. Die folgenden Zeilen importieren die benötigten Pakete dash und dash_html_components. Ohne eine Definition eines Layouts der App, kann die Applikation nicht gestartet werden. Eine leere html.Div genügt, um die App zu starten.

import dash
import dash_html_components as html

Falls du bereits Erfahrung mit dem WSGI Web Applikation Framework Flask hast, werden dir die nächsten Schritte sehr bekannt sein. Da Dash auf Flask basiert, wird durch die folgenden Zeilen eine Flask App gestartet.

# Initialise the app
app = dash.Dash(__name__)

# Define the app
app.layout = html.Div()
# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)

Wie eine .css-Datei das Layout beeinflusst

Das Modul dash_html_components beinhaltet verschiedene HTML-Komponenten. Mehr Informationen zu den Komponenten unter: https://dash.plot.ly/dash-html-components

HTML-Komponenten können über das children-Attribut verschachtelt werden.

app.layout = html.Div(children=[
                      html.Div(className='row',  # Define the row element
                               children=[
                                  html.Div(className='four columns div-user-controls'),  # Define the left element
                                  html.Div(className='eight columns div-for-charts bg-grey')  # Define the right element
                                  ])
                                ])

Das erste html.Div() hat ein untergeordnetes Element (eng. child). Das Element is ein weiteres html.Div() mit dem Namen (className) row, welches der Container für unseren Inhalt sein wird. Die weiteren untergeordneten Elemente sind four-columns div-user-controls und eight columns div-for-charts bg-grey.

Der Stil der div-Komponenten stammt aus unserer style.css-Datei.

Wir starten damit, dass wir der App weitere Informationen zur Verfügung stellen, wie Titel und Beschreibung. Zum Element four columns div-user-controls fügen wir die Komponenten H2 für die Überschrift und P als Paragraph hinzu.

children = [
    html.H2('Dash - STOCK PRICES'),
    html.P('''Visualising time series with Plotly - Dash'''),
    html.P('''Pick one or more stocks from the dropdown below.''')
]

Wechsle ins Terminal und starte die App mit: python app.py

dash-app-first-layout

Die Basics des App-Layouts

Ein weiteres Feature von Flask (und somit Dash) ist das hot-reloading. Das Feature macht es möglich, eine Änderung im Code ohne einen Neustart der App sehen zu können.

Zusätzlich wird durch debug=True ein Feld rechts in der unteren Ecke der App angezeigt, worin wir auf Fehlermeldungen und den Callback Graph zugriff haben. Wir werden im letzten Abschnitt des Artikels auf Callback Graph zurück kommen, nachdem wir interaktive Funktionalitäten implementiert haben.

dash-app-layout

Erstellen von Grafiken in Dash – Wie man eine Plotly-Grafik anzeigt

Nachdem wir die grundlegenden Container für unsere App erstellt haben, erstellen wir jetzt einen Plotly-Graphen. Die Komponente dcc.Graph aus den dash_core_components nutzt das gleiche figure-Argument wie das plotly.py Paket. Dash übersetzt jeden Aspekt des Charts zu einem Schlüssel-Wert-Paar, welches von der darunterliegenden JavaScript-Bibliothek Plotly.js verarbeitet wird.

Im folgenden Abschnitt verwenden wir die Expressversion von plotly.py und das Paket Dash Core Components. Nach der Installation von Dash sollte plotly in der Entwicklungsumgebung bereits verfügbar sein.

import dash_core_components as dcc
import plotly.express as px

Dash Core Components beinhaltet eine Kollektion von nützlichen und einfach zu bedienenden Komponenten, welche zur Interaktivität und Funktionalität des Dashboard beitragen.

Plotly Express ist die Expressversion von plotly.py, welche die Erstellung von Plotly-Grafiken vereinfacht, mit der Einschränkung dass es die Flexibilität verringert.

Um einen Plot in der rechten Seite unserer App zu zeichnen, wird ein dcc.Graph() als ein Unterelement dem html.Div() mit dem Namen eight columns div-for-charts bg-grey hinzugefügt. Die Komponente dcc.Graph() kann für jede von plotly-gestützte Visualisierung genutzt werden. In unserem Fall wird die figure von px.line() aus dem Paket plotly.express erstellt. Das Layout der Grafik ändern wir durch die Methode update_layout(). Um den Hintergrund der Grafik transparent zu machen, setzen wir die Farbe auf rgba(0, 0, 0, 0). Ohne den Hintergrund transparent zu setzen, würde eine große weiße Box in der Mitte unserer App stehen. Da dcc.Graph() lediglich die Grafik anzeigt, können wir nach der Erstellung die Eigenschaften der Grafik nicht ohne weiteres ändern.

dcc.Graph(id='timeseries',
          config={'displayModeBar': False},
          animate=True,
          figure=px.line(df,
                         x='Date',
                         y='value',
                         color='stock',
                         template='plotly_dark').update_layout(
                                   {'plot_bgcolor': 'rgba(0, 0, 0, 0)',
                                    'paper_bgcolor': 'rgba(0, 0, 0, 0)'})
                                    )

Nachdem Dash die Applikation neu geladen hat (hot-reloading), sieht die App folgendermaßen aus:

dash-app-with-plot

Erstellen einer Dropdown-Liste

Eine weitere Komponente ist dcc.Dropdown(), welche genutzt wird – du kannst es dir vielleicht denken –, um eine Dropdown-Liste zu erstellen. Die verfügbaren Optionen in einem Dropdown werden entweder durch eine (Python) Liste gegeben oder innerhalb einer Funktion definiert.

Für unsere Dropdown-Liste erstellen wir eine Funktion, welche eine Liste von Dictionaries ausgibt. Die Liste besteht aus den Dictionaries mit den Schlüsseln label und value. Hierdurch werden die Optionen unserer Dropdown-Liste definiert. Der Wert von label ist der angezeigte Text in der App und der Wert von value kann innerhalb der App als Input für Funktionen verwendet werden. Solltest du beispielsweise den vollen Namen des Unternehmens anzeigen, kann der Wert von label auf Microsoft gesetzt werden. Der Einfachheit halber wählen wir für label und value die gleichen Werte.

Füge die folgende Funktion zum Skript hinzu, bevor du das Layout der App definierst:

# Creates a list of dictionaries, which have the keys 'label' and 'value'.
def get_options(list_stocks):
    dict_list = []
    for i in list_stocks:
        dict_list.append({'label': i, 'value': i})

    return dict_list

Nach unserer Funktion get_option, können wir das Element dcc.Dropdown() von den Dash Core Components zu unserer App hinzufügen. Füge html.Div() als untergeordenetes Element zur Liste der Elemente in four columns div-user-controls hinzu und setze das Argument className=div-for-dropdown. html.Div() hat ein Unterelement, dcc.Dropdown().

Wir möchten die Möglichkeit habe, nicht nur einen einzelnen Wert, sondern mehrere Werte zur gleichen Zeit auszuwählen und setzen daher das Argument multi=True. Da die App jedoch nicht leer erscheinen soll, setzen wir zudem einen initialen Wert für value.

 html.Div(className='div-for-dropdown',
          children=[
              dcc.Dropdown(id='stockselector',
                           options=get_options(df['stock'].unique()),
                           multi=True,
                           value=[df['stock'].sort_values()[0]],
                           style={'backgroundColor': '#1E1E1E'},
                           className='stockselector')
                    ],
          style={'color': '#1E1E1E'})

Die id und options Argumente des dcc.Dropdown() sind wichtig für den nächsten Abschnitt. Um unterschiedliche Styles der Dropdown-Liste auszuprobieren, folge diesem Link.

Mit Callbacks arbeiten

Wie man der App interaktive Funktionalitäten hinzufügt

Callbacks (eng. für Rückruffunktion) sind für die Interaktivität in einem Dashboard verantwortlich. Sie nehmen Inputs entgegen, z.B. die ausgewählten Optionen einer Dropdown-Liste und leiten die Werte zu einer Funktion weiter. Der Output der Funktion wird durch den Callback an ein definiertes Element geleitet. Wir werden im nächsten Schritt eine Funktion schreiben, welche eine Grafik basierend auf den Namen von Unternehmen ausgibt. In unserer Implementierung wird die erstellte Grafik an eine dcc.Graph()-Komponente geleitet.

Momenten hat die Auswahl der Dropdown-Liste noch keinen Einfluss auf die angezeigte Grafik. Um diese Funktionalität zu implementieren werden wir einen Callback nutzen. Der Callback übernimmt die Kommunikation zwischen der Dropdown-Liste mit der ID id='stockselector' und dem Graphen 'timeseries'. Zur Vorbereitung entfernen wir die zuvor erstellte Grafik, da wir die figure dynamisch erstellen möchten.

In unserer App möchten wir zwei Graphen haben, weshalb wir eine weitere dcc.Graph()-Komponente erstellen und mit dem Argument id='change' identifizierbar machen.

  • Entfernen der figure von der Komponente dcc.Graph(id='timeseries')
  • Hinzufügen der Komponente dcc.Graph(id='change')
  • Beide Komponenten sollten Unterelemente von eight columns div-for-charts bg-grey sein.
dcc.Graph(id='timeseries', config={'displayModeBar': False})
dcc.Graph(id='change', config={'displayModeBar': False})

Callbacks erhöhen die Interaktivität deiner Anwendung. Sie können Eingaben von Komponenten entgegennehmen, z. B. bestimmte Aktien, die über ein Dropdown-Menü ausgewählt werden, diese Eingaben an eine Funktion weitergeben und die von der Funktion zurückgegebenen Werte an Komponenten zurückgeben.

In unserer Implementierung wird ein Callback ausgelöst, sobald der Nutzer eine Aktie im Dropdown auswählt. Der Callback nimmt den Wert des dcc.Dropdown() entgegen (Input) und leitet den Wert an die Funktionen update_timeseries() und update_change() weiter. Die Funktionen filtern die Daten und erstellen eine auf den Inputs basierende Grafik. Der Callback leitet anschließend den Output der Funktionen (Grafik) an die als Output spezifizierten Elemente weiter.

Der Callback ist als Decorator für eine Funktion implementiert. Mehrere Inputs und Outputs sind möglich. Wir werden jedoch bei einem Input und einem Output bleiben. Wir importieren die Objekte dash.dependencies import Input, Output.

Füge die folgende Zeile zu deinemSkript hinzu.

from dash.dependencies import Input, Output

Input() und Output() nehmen die id einer Komponente (bspw. dcc.Graph(id='timeseries') hat die id 'timeseries') sowie die Eigenschaft einer Komponente (hier figure) als Argument entgegen.

Beispiel Callback:

# Update Time Series
@app.callback(Output('id of output component', 'property of output component'),
              [Input('id of input component', 'property of input component')])
def arbitrary_function(value_of_first_input):
    '''
    The property of the input component is passed to the function as value_of_first_input.
    The functions return value is passed to the property of the output component.
    '''
    return arbitrary_output

Wenn wir durch unseren stockselector eine Zeitreihe für einen oder mehrere Aktien anzeigen möchten, benötigen wir eine Funktion. Der value unseres Inputs ist die Liste von ausgewählten Unternehmen aus der Dropdown-Liste.

Implementieren von Callbacks

Die Funktion zeichnet die Linien (eng. traces) der Plotly-Grafik, basierend auf den übergebenen Aktiennamen werden die Linien gezeichnet und zusammen als eine figure ausgegeben, welche von dcc.Graph() angezeigt werden kann. Die Inputs für unsere Funktion werden in der Reihenfolge übergeben, in welcher Sie im Callback gesetzt wurden. (Bemerkung: Seit Dash 2.0 muss dies nicht immer der Fall sein.)

Update der Grafik (figure) für die time series:

@app.callback(Output('timeseries', 'figure'),
              [Input('stockselector', 'value')])
def update_timeseries(selected_dropdown_value):
    ''' Draw traces of the feature 'value' based one the currently selected stocks '''
    # STEP 1
    trace = []  
    df_sub = df
    # STEP 2
    # Draw and append traces for each stock
    for stock in selected_dropdown_value:   
        trace.append(go.Scatter(x=df_sub[df_sub['stock'] == stock].index,
                                 y=df_sub[df_sub['stock'] == stock]['value'],
                                 mode='lines',
                                 opacity=0.7,
                                 name=stock,
                                 textposition='bottom center'))  
    # STEP 3
    traces = [trace]
    data = [val for sublist in traces for val in sublist]
    # Define Figure
    # STEP 4
    figure = {'data': data,
              'layout': go.Layout(
                  colorway=["#5E0DAC", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'],
                  template='plotly_dark',
                  paper_bgcolor='rgba(0, 0, 0, 0)',
                  plot_bgcolor='rgba(0, 0, 0, 0)',
                  margin={'b': 15},
                  hovermode='x',
                  autosize=True,
                  title={'text': 'Stock Prices', 'font': {'color': 'white'}, 'x': 0.5},
                  xaxis={'range': [df_sub.index.min(), df_sub.index.max()]},
              ),

              }

    return figure

STEP 1

  • Ein trace wird für jede Aktie gezeichnet. Erstellen einer leeren list für jeden trace der Plotly-Grafik.

STEP 2

In einem for-loop, wird ein trace für die Plotly-Grafik von go.Scatter erstellt. (go stammt aus dem Import import plotly.graph_objects as go)

  • Iteriere über die selektierten Aktien im Drodown, zeichne eintrace und hänge dastrace an die Liste aus Step 3 an.

STEP 3

  • Ebne (eng. flatten) die Liste.

STEP 4

Plotly-Grafiken sind Dictionaries mit den Schlüsseln data und layout. Der Wert von data ist unsere Liste mit den erstellten traces. Das layout wird mit go.Layout() definiert.

  • Füge die traces zu unserer figure (Grafik) hinzu
  • Definiere das Layout der figure

Diese gleichen Schritte werden für unsere zweite Grafik durchgeführt, mit dem Unterschied, dass die Daten auf der y-Achse auf change gesetzt werden.

Update der Grafik change:

@app.callback(Output('change', 'figure'),
              [Input('stockselector', 'value')])
def update_change(selected_dropdown_value):
    ''' Draw traces of the feature 'change' based one the currently selected stocks '''
    trace = []
    df_sub = df
    # Draw and append traces for each stock
    for stock in selected_dropdown_value:
        trace.append(go.Scatter(x=df_sub[df_sub['stock'] == stock].index,
                                 y=df_sub[df_sub['stock'] == stock]['change'],
                                 mode='lines',
                                 opacity=0.7,
                                 name=stock,
                                 textposition='bottom center'))
    traces = [trace]
    data = [val for sublist in traces for val in sublist]
    # Define Figure
    figure = {'data': data,
              'layout': go.Layout(
                  colorway=["#5E0DAC", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'],
                  template='plotly_dark',
                  paper_bgcolor='rgba(0, 0, 0, 0)',
                  plot_bgcolor='rgba(0, 0, 0, 0)',
                  margin={'t': 50},
                  height=250,
                  hovermode='x',
                  autosize=True,
                  title={'text': 'Daily Change', 'font': {'color': 'white'}, 'x': 0.5},
                  xaxis={'showticklabels': False, 'range': [df_sub.index.min(), df_sub.index.max()]},
              ),
              }

    return figure

Starte die App erneut. In der App können nun über die Dropdown-Liste die verschiedenen Aktien ausgewählt werden. Für jedes selektierte Element wird ein Line-Plot gezeichnet und angezeigt. Standardmäßig hat die Dropdown-Komponente einige Funktionalitäten, wie bspw. die Suchfunktion, weshalb die Selektion von einzelnen Elementen auch bei vielen Auswahlmöglichkeiten sehr einfach ist.

dash-app-final

Visualisieren von Callbacks – Callback Graph

Mit den implementierten Callbacks ist die App fertig. Werfen wir nun einen Blick auf den sogenannten Callback Graph. Wenn die App mit debug=True gestartet wird, erscheint in der unteren rechten Ecke ein Feld. Hierüber kann eine visuelle Repräsentation des Callback Graph angezeigt werden, welchen wir im Skript definiert haben. Der Graph zeigt, dass die Komponenten timeseries und change eine figure anzeigen, die auf den ausgewählten Werten im stockselector basieren.

Sollten Callbacks nicht so funktionieren, wie es geplant wurde, ist dieses Tool sehr hilfreich beim Debuggen.

dash-app-final-callback

Fazit

Fassen wir die wichtigsten Elemente von Dash zusammen. Um die App zu starten, benötigt man nur ein paar Zeilen Code. Ein grundlegendes Verständnis von HTML und CSS genügt, um den Stil eines Dashboards anzupassen. Interaktive Grafiken können ohne Probleme implementiert werden, da Dash für das Arbeiten mit interaktiven Plotly-Grafiken entwickelt wurde. Über Callbacks, welche den Input von Nutzenden an Funktionen weiterleiten, werden unterschiedliche Komponenten miteinander verbunden.

Wenn dir der Blogartikel gefallen hat, kontaktiere mich gerne über LinkedIn oder per Email. Ich bin neugierig, welche weiteren Anwendungsmöglichkeiten das Framework bietet und freue mich auf Fragen zu Daten, Machine Learning und KI sowie den spannenden Lösungen, an denen wir bei STATWORX arbeiten.

Vielen Dank!

Alexander Blaufuss Alexander Blaufuss

Nearly one year ago, I analyzed how we use emojis in our Slack messages. Since then, STATWORX grew, and we are a lot more people now! So, I just wanted to check if something changed.

Last time, I did not show our custom emojis, since they are, of course, not available in the fonts I used. This time, I will incorporate them with geom_image(). It is part of the ggimage package from Guangchuang Yu, which you can find here on his Github. With geom_image() you can include images like .png files to your ggplot.

What changed since last year?

Let’s first have a look at the amount of emojis we are using. In the plot below, you can see that since my last analysis in October 2018 (red line) the amount of emojis is rising. Not as much as I thought it would, but compared to the previous period, we now have more days with a usage of over 100 emojis per day!

Like last time, our top emoji is ????, followed by ???? and ????. But sneaking in at number ten is one of our custom emojis: party_hat_parrot!

top-10-used-emojis

How to include custom images?

In my previous blogpost, I hid all our custom emojis behind❓since they were not part of the font. It did not occur to me to use their images, even though the package is from the same creator! So, to make up for my ignorance, I grabbed the top 30 custom emojis and downloaded their images from our Slack servers, saved them as .png and made sure they are all roughly the same size.

To use geom_image() I just added the path of the images to my data (the are just an abbreviation for the complete path).

                NAME COUNT REACTION IMAGE
1:          alnatura    25       63 .../custom/alnatura.png
2:              blog    19       20 .../custom/blog.png
3:           dataiku    15       22 .../custom/dataiku.png
4: dealwithit_parrot     3      100 .../custom/dealwithit_parrot.png
5:      deananddavid    31       18 .../custom/deananddavid.png

This would have been enough to just add the images now, but since I wanted the NAME attribute as a label, I included geom_text_repel from the ggrepel library. This makes handling of non-overlapping labels much simpler!

ggplot(custom_dt, aes( x = REACTION, y = COUNT, label = NAME)) +
  geom_image(aes(image = IMAGE), size = 0.04) +
  geom_text_repel(point.padding = 0.9, segment.alpha = 0) +
  xlab("as reaction") +
  ylab("within message") +
  theme_minimal()

Usually, if a label is „too far“ away from the marker, geom_text_repel includes a line to indicate where the labels belong. Since these lines would overlap the images, I used segment.alpha = 0 to make them invisible. With point.padding = 0.9 I gave the labels a bit more space, so it looks nicer. Depending on the size of the plot, this needs to be adjusted. In the plot, one can see our usage of emojis within a message (y-axis) and as a reaction (x-axis).

To combine the emoji font and custom emojis, I used the following data and code — really… why did I not do this last time? ???? Since the UNICODE is NA when I want to use the IMAGE, there is no „double plotting“.

                     EMOJI REACTION COUNT  SUM PLACE    UNICODE   IMAGE
 1:                    :+1:     1090     0 1090     1 U0001f44d
 2:                   :joy:      609   152  761     2 U0001f602
 3:                 :smile:       91   496  587     3 U0001f604
 4:                    :-1:      434     9  443     4 U0001f44e
 5:                  :tada:      346    38  384     5 U0001f389
 6:                  :fire:      274    17  291     6 U0001f525
 7: :slightly_smiling_face:        1   250  251     7 U0001f642
 8:                  :wink:       27   191  218     8 U0001f609
 9:                  :clap:      201    13  214     9 U0001f44f
10:      :party_hat_parrot:      192     9  201    10       <NA>  .../custom/party_hat_parrot.png
quartz()
ggplot(plotdata2, aes(x = PLACE, y = SUM, label = UNICODE)) +
  geom_bar(stat = "identity", fill = "steelblue") +
  geom_text(family="EmojiOne") +
  xlab("Most popular emojis") +
  ylab("Number of usage") +
  scale_fill_brewer(palette = "Paired") +
  geom_image(aes(image = IMAGE), size = 0.04) +
  theme_minimal()
ps = grid.export(paste0(main_path, "plots/top-10-used-emojis.svg"), addClass=T)
dev.off()

The meaning behind emojis

Now we know what our top emojis are. But what is the rest of the world doing? Thanks to Emojimore for providing me with this overview! On their site, you can find meanings for a lot more emojis.

Behind each of our custom emojis is a story as well. For example, all the food emojis are helping us every day to decide where to eat and provide information on what everyone is planning for lunch! And if you do not agree with the decision, just react with sadphan to let the others know about your feelings. If you want to know the whole stories behind all custom emojis or even help create new ones, then maybe you should join our team — check out our available job offers here!

 

Jakob Gepp

Jakob Gepp

In den meisten unserer Kundenprojekte bei statworx nutzen wir R oder Python. Auch in unserem Blog handelt es sich meist um Themen rund um die Nutzung dieser beiden Sprachen. Im Data Science Bereich gibt es allerdings eine große Anzahl anderer Tools, viele davon mit einer grafischen Benutzeroberfläche. Dazu zählen zum Beispiel KNIME, RapidMiner oder das hier vorgestellte Tool von Dataiku Data Science Studio (DSS). Allen gemeinsam ist, dass keine besonderen Programmierkenntnisse benötigt werden, um mit diesen zu arbeiten. Die Tools bieten also auch Data Science Einsteigern eine gute Möglichkeit schnelle Ergebnisse zu erzielen und diese mit Kollegen aus anderen Fachbereichen einfach zu teilen. Dies ist einer der Gründe warum auch wir in einigen Kundenprojekten mit Dataiku DSS arbeiten und zu diesem Tool auch Schulungen anbieten.

In dem ersten Teil des Artikels soll ein erster Überblick über die Funktionen von Dataiku DSS gegeben werden, im Folgenden werde ich die Möglichkeiten an einem Beispieldatensatz aufzeigen.

Das wichtigste allerdings zuerst. Wenn Sie sich selbst ein Bild von Dataiku machen wollen und das untere Beispiel nachvollziehen möchten, können Sie eine kostenlose Version auf Dataiku herunterladen.

Vorstellung Dataiku

Warum Dataiku Ihren Arbeitsablauf erleichtern wird?

Das im Jahr 2013 gegründete Unternehmen Dataiku bietet eine kollaborative Data Science Plattform, die die Bearbeitung des gesamten Workflows eines Data Sciences Projektes erlaubt. Dies umfasst die Integration von Hadoop oder Spark, die Datentransformation mit einer grafischen Benutzeroberfläche, dem Nutzen von diversen Algorithmen des maschinellen Lernens und die Datenvisualisierung mit Dashboards. Durch die Breite an Funktionen kann das Tool von Data Scientists, Data Analysts und Data Engineers genutzt werden. Weiterhin ist das Projektmanagement über Dataiku zum Beispiel durch To-do-Listen möglich.

Durch die diversen Funktionen wurde Dataiku im Jahr 2018 im Gartner Magic Quadrant for Data Science Platforms als Visionär eingeordnet.

Ein großer Vorteil an Dataiku ist die Click-or-Code Option. Diese Option ermöglicht es entweder die grafische Oberfläche oder R/Python Code zu nutzen, um zum Beispiel die Datenaufbereitung durchzuführen. Dadurch können beliebige Funktion selbst erstellt werden, sollte der vordefinierte Funktionsumfang nicht ausreichen. Die grafische Benutzeroberfläche bietet dabei eine große Menge an Funktionen und eine ähnliche Syntax wie Excel.

Der Aufbau von Dataiku DSS

Auf der Startseite befindet sich eine Übersicht über alle Projekte. Hier wurde neben zwei Beispielen von Dataiku DSS noch ein weiteres Testprojekt erstellt. Bei einer größeren Anzahl Projekte, ist es zudem möglich nach diesen zu suchen.

dataiku-projektuebersicht

Klickt man auf eines der Projekte, öffnet sich eine Zusammenfassung des Projektes mit der Anzahl genutzter Datensätze, Modelle, Dashboards usw..

dataiku-t-shirt-projekt

Zusätzlich kann eine kurze Zusammenfassung des Projektes, ähnlich einer Readme-Datei auf Github, sowie eine To-do-Liste erstellt werden. Weiterhin können über den Reiter „Changes“ Veränderungen nachverfolgt werden.

Klickt man auf den Button GO TO FLOW öffnet man den Workflow des Projektes.

Der angezeigte Workflow wird von links nach rechts durchlaufen und zeigt die Implementierung eines einfachen Modells. Zuerst werden dabei verschieden Datensätze geladen. In den nächsten Schritten erfolgt dann ein Join, die Datenaufbereitung, das Training sowie als letzter Schritt das Scoring der Modelle.

Ein Vorteil an dem Workflow ist neben der guten Übersicht, dass man die einzelnen Schritte schnell bearbeiten kann und Änderungen an den Datensätzen direkt beobachten kann. Durch einen Doppelklick auf den erstellten Datensatz web_last_month_enriched ist es zum Beispiel möglich zu sehen, was sich verändert hat und in welchem Format die einzelnen Spalten gespeichert sind. Gerade bei unbekannten Datensätzen hilft dies einen schnellen Überblick über die Daten zu bekommen.

dataiku-dataset-overview

Vorhersage der Weinqualität – ein Beispiel

Als Beispiel soll ein Problem gewählt werden, bei dem anhand von verschiedenen Einflussvariablen die Qualität eines Weines vorhergesagt werden muss. Die beiden genutzten Datensätze können auf der Seite vom UCI Machine Learning Repository unter UCI Datasets heruntergeladen werden. Ein Datensatz enthält dabei jeweils Daten einer Weinsorte.

Die Qualität eines Weines wird dabei anhand einer Skala von 0-10 bewertet, wobei die 10 der höchsten Kategorie entspricht. Als Einflussvariablen werden verschiedene sensorische Daten wie zum Beispiel der pH-Wert oder der Gesamtschwefeldioxidgehalt genutzt. Die Bedeutung der einzelnen Variablen kann genauer in Dataset Description nachgelesen werden. Für diesen Blog wird dabei die Qualität des Weines in nur zwei Gruppen eingeteilt. Dazu werden alle „schlechten“ Weine mit einer Wertung kleiner gleich 5 in einer Kategorie zusammengefasst und alle Weine mit einer Wertung größer als 5 in einer anderen Kategorie zusammengefasst.

Als erster Schritt müssen die Datensätze hochgeladen und zusammengeführt werden. Um einen neuen Datensatz in Dataiku zu nutzen kann das Datenbank Symbol genutzt werden. Danach werden die verschiedenen Möglichkeiten von Datenquellen angezeigt. Entweder werden verschiedene Datenbankanbindungen genutzt oder es können csv-Dateien geladen werden. Für das jetzige Problem müssen die beiden Datensätze für Rot- und Weißwein als csv-Dateien geladen werden. Direkt nach dem Laden des Datensatzes, wird dieser mit den entsprechenden Variablentypen angezeigt. In den meisten Fällen stimmen die vorgeschlagenen Werte, sollten diese doch abweichen, können die Werte auch direkt geändert werden.

Weiterhin kann durch einen Klick auf den Variablennamen und Analyze direkt eine Übersicht über die Variable gewonnen werden. Unter dem Reiter Charts gibt es zusätzlich die Möglichkeit durch Drag & Drop einfache Grafiken zu erstellen, um somit einen Überblick über die Daten zu erlangen, wobei zwischen verschiedenen Diagrammarten ausgewählt werden kann. Gerade bei unbekannten Datensätzen ist dies ein wichtiger Schritt, um die Daten zu verstehen und eventuell neue Features zu bilden oder Ausreißer zu erkennen.

Nachdem beide Datensätze geladen wurden, müssen diese zusammengeführt werden. Dazu muss zunächst über den kleinen abgebildeten Graphen oben links die Flow-Ansicht aufgerufen werden. In dieser Ansicht werden alle Schritte der Modellierung von der Datenaufbereitung bis zum letztendlichen Deployment grapfisch durch ein Flussdiagramm angezeigt. Dabei werden verschiedene Symbole für Datensätze und diverse Operationen genutzt. Für das Zusammenführen der Daten wird in der Flow-Ansicht das Stacking-Symbol genutzt.

In der darauffolgenden Ansicht muss der jeweils andere Datensatz ausgewählt werden sowie der Name des neuen Datensatzes gewählt werden.

Für unser Problem soll nur ein Modell gebildet werden und die Farbe des Weins als Einflussvariable mit aufgenommen werden. Dazu muss zunächst bei beiden Datensätzen eine neue Variable gebildet werden, in der die jeweilige Farbe, also weiß oder rot steht. Klickt man nun in der Flow Ansicht einmal auf den Datensatz, werden verschiedene Visual Recipes angezeigt, die für verschiedene Operationen stehen. Das Hinzufügen der Weinfarbe ist ein Datenvorbereitungsschritt, weshalb auf das Symbol mit dem Besen geklickt werden muss. Danach können einer oder mehrere Datenvorbereitungsschritte durchgeführt werden. Dazu kann über Add a new step ein neuer Schritt hinzugefügt werden. Um die neue Variable hinzuzufügen kann über ein Klick bei Strings und die Auswahl Formula eine Formel eingegeben werden. Die neue Variable wird wine_color genannt und als Expression wird die entsprechende Farbe, also entweder white oder red eingetragen.

new-variable

Durch eins Klick auf RUN wird die entsprechende Operation durchgeführt.

Nachdem die Datensätze zusammengeführt wurden, werden die neuen Kategorien gebildet. Dazu wird wieder das Datenaufbereitungssymbol genutzt. Darauf wird wieder über String > Formula der Formeleditor geöffnet. Die zugehörige Formel lautet if (quality <= 5, ‚0‘, ‚1‘). Die neue Variable wird quality_classification genannt. Um beim Training die Variable quality nicht als Einflussvariable zu nutzen, wird diese im nächsten Schritt ausgeschlossen. Dazu wird unter der Kategorie Filter und Delete/Keep columns by name die Variable quality eingesetzt. Durch einen Klick auf RUN werden die entsprechenden Schritte durchgeführt.

Im Folgenden erfolgt das eigentliche Training des Modells. Um ein Modell zu trainieren reicht ein einfacher Klick auf den Trainingsdatensatz und danach kann über Lab > Visual Analysis > New ein neues Modell trainiert werden. In der neuen Ansicht kann nun unter dem Reiter Models das erste Modell erstellt werden. Nach der Auswahl des Machine Learning Problems, in unserem Fall Prediction, und der Zielvariable, kann direkt ein einfaches Modell trainiert werden oder über den Reiter Design können eigene Modelle erstellt werden.

In dem Design können verschiedene Parameter verändert werden sowie die einzelnen Algorithmen ausgewählt werden. Es gibt dabei die vier Kategorien BASIC, FEATURES, MODELING und ADVANCED.

In der Kategorie BASIC können Parameter für den Train/Test Split sowie das zu optimierende Fehlermaß angegeben werden. Sollen weitere Feature Engineering Schritte durchgeführt oder einzelne Variablen ausgeschlossen werden, kann dies in der Kategorie FEATURES geschehen. Hierbei können auch verschiedene Interaktionen zwischen den verschiedenen Variablen sowie der Umgang mit fehlenden Werten definiert werden. Unter der Kategorie MODELING gibt es eine Auswahl nahezu aller gängigen Machine-Learning Modelle wie zum Beispiel verschiedene baumbasierte Verfahren, Regressionsverfahren und Neuronale Netze. Zu jedem Modell können verschiedene Hyperparameter angegeben werden, die zum Grid-Search genutzt werden sollen. Sollen weitere Modelle ausprobiert werden, können unter dem Punkt Add Custom Python Model auch eigene Modelle entwickelt werden. Über ADVANCED kann die Python Umgebung verändert werden sowie die Gewichte der einzelnen Obersevationen verändert werden.

In dem Beispiel wurden die beiden von Dataiku vorgeschlagenen Modelle Random Forest und Logistische Regression sowie der XGBoost getestet. Durch einen Klick auf Train werden die Modelle trainiert. Am Ende des Trainings werden zu jedem Modell die durch Grid Seach ermittelten optimalen Parameter, eine Liste der wichtigsten Parameter, die Aufteilung von Trainings- und Testset sowie die Trainingszeit angezeigt. Durch einen Klick auf ein Modell werden diverse Informationen zu dem Modell angezeigt, wie zum Beispiel Werte zur Performance oder zu genutzen Input Variablen.

model-results

In realen Problemen kann es natürlich noch andere Kriterien als die Accuracy für die Auswahl des Modells geben, wie zum Beispiel die Interpretierbarkeit eines Modells oder lineare Zusammenhänge. Zur Produktivsetzung kann in der Detailansicht des Modells der Deploy Button betätigt werden. Das Modell kann nun auf neue Daten angewandt werden.

Der gesamte Flow hat nun folgende Form:

final-flow

Resümee und Ausblick

Dieser Beitrag sollte einen ersten Überblick über das Tool geben. Natürlich gibt es noch viele weitere Funktionen auf die voerst nicht eingegangen wurde.

Ich hoffe jedoch, dass ich Ihr Interesse geweckt habe. Bei der Bearbeitung unserer Projekte erleichtert Dataiku DSS uns die Arbeit häufig stark, zusätzlich erkennen wir bei unseren Schulungen, dass das Tool leicht zu erlernen ist. Gerade für Personen, die vorher wenig mit Daten gearbeitet haben. Martin Albers Martin Albers

Im ersten Teil dieses Blogs haben wir darüber gesprochen, was ein DAG ist, wie man dieses mathematische Konzept in der Projektplanung und -programmierung anwendet und warum wir bei STATWORX beschlossen haben, Airflow statt anderer Workflow-Manager einzusetzen. In diesem Teil werden wir jedoch etwas technischer und untersuchen eine recht informative Hello-World-Programmierung und wie man Airflow für verschiedene Szenarien einrichtet, mit denen man konfrontiert werden könnte. Wenn du dich nur für den technischen Teil interessierst und deshalb den ersten Teil nicht lesen willst, aber trotzdem eine Zusammenfassung möchtest, findest du hier eine Zusammenfassung:

  • DAG ist die Abkürzung für „Directed Acyclic Graph“ und kann als solcher Beziehungen und Abhängigkeiten darstellen.
  • Dieser letzte Aspekt kann im Projektmanagement genutzt werden, um deutlich zu machen, welche Aufgaben unabhängig voneinander ausgeführt werden können und welche nicht.
  • Die gleichen Eigenschaften können in der Programmierung genutzt werden, da Software bestimmen kann, welche Aufgaben gleichzeitig ausgeführt werden können oder in welcher Reihenfolge die anderen beendet werden (oder fehlschlagen) müssen.

Warum haben wir Airflow gewählt:

  1. Kein Cron – Mit Airflows integriertem Scheduler müssen wir uns nicht auf Cron verlassen, um unsere DAG zu planen und verwenden nur ein Framework (nicht wie Luigi).
  2. Code Bases – In Airflow werden alle Workflows, Abhängigkeiten und das Scheduling in Python Code durchgeführt. Daher ist es relativ einfach, komplexe Strukturen aufzubauen und die Abläufe zu erweitern.
  3. Sprache – Python ist eine Sprache, die man relativ leicht erlernen kann und Python Kenntnisse war in unserem Team bereits vorhanden.

Vorbereitung

Der erste Schritt war die Einrichtung einer neuen, virtuellen Umgebung mit Python und virtualenv.

$pip install virtualenv # if it hasn't been installed yet   
$cd  # change into home 

# create a separated folder with all environments  
$mkdir env   
$cd env   
$virtualenv airflow

Sobald die Umgebung erstellt wurde, können wir sie immer dann verwenden, wenn wir mit Airflow arbeiten wollen, so dass wir nicht in Konflikt mit anderen Abhängigkeiten geraten.

$source ~/env/airflow/bin/activate  

Dann können wir alle Python-Pakete installieren, die wir benötigen.

$ pip install -U pip setuptools wheel \
psycopg2\
Cython \
pytz \
pyOpenSSL \
ndg-httpsclient \
pyasn1 \
psutil \
apache-airflow[postgres]\  

A Small Breeze

Sobald unser Setup fertig ist, können wir überprüfen, ob Airflow korrekt installiert ist, indem wir airflow version in Bash eingeben und du solltest etwas wie dieses sehen:

version-sequential

Anfänglich läuft Airflow mit einer SQLite-Datenbank, die nicht mehr als eine DAG-Aufgabe gleichzeitig ausführen kann und daher ausgetauscht werden sollte, sobald du dich ernsthaft damit befassen willst oder musst. Doch dazu später mehr. Beginnen wir nun mit dem typischen Hello-World-Beispiel. Navigiere zu deinem AIRFLOW_HOME-Pfad, der standardmäßig ein Ordner namens airflow in deinem Stammverzeichnis ist. Wenn du das ändern willst, editiere die Umgebungsvariable mit export AIRFLOW_HOME=/your/new/path und rufe airflow version noch einmal auf.

# ~/airflow/dags/HelloWorld.py  

from airflow import DAG  
from airflow.operators.dummy_operator import DummyOperator  
from airflow.operators.python_operator import PythonOperator  
from datetime import datetime, timedelta  

def print_hello():
    return 'Hello world!'  

dag = DAG('hello_world',
            description='Simple tutorial DAG',
            start_date= datetime.now() - timedelta(days= 4),  
            schedule_interval= '0 12 * * *'  
         )  

dummy_operator= DummyOperator(task_id= 'dummy_task', retries= 3, dag= dag)  

hello_operator= PythonOperator(task_id= 'hello_task', python_callable= print_hello, dag= dag)  

dummy_operator >> hello_operator # same as  dummy_operator.set_downstream(hello_operator)  

Die ersten neun Zeilen sollten einigermaßen selbsterklärend sein, nur der Import der notwendigen Bibliotheken und die Definition der Hello-World-Funktion passieren hier. Der interessante Teil beginnt in Zeile zehn. Hier definieren wir den Kern unseres Workflows, ein DAG-Objekt mit dem Identifier hello _world in diesem Fall und eine kleine Beschreibung, wofür dieser Workflow verwendet wird und was er tut (Zeile 10). Wie du vielleicht schon vermutet hast, definiert das Argument start_date das Anfangsdatum des Tasks. Dieses Datum sollte immer in der Vergangenheit liegen. Andernfalls würde die Aufgabe ausgelöst werden und immer wieder nachfragen, ob sie ausgeführt werden kann, und als solche bleibt sie aktiv, bis sie geplant ist. Das schedule_interval definiert die Zeiträume, in denen der Graph ausgeführt werden soll. Wir setzen sie entweder mit einer Cron-ähnlichen Notation auf (wie oben) oder mit einem syntaktischen Hilfsmittel, das Airflow übersetzen kann. Im obigen Beispiel definieren wir, dass die Aufgabe täglich um 12:00 Uhr laufen soll. Die Tatsache, dass sie täglich laufen soll, hätte auch mit schedule_interval='@daily ausgedrückt werden können. Die Cron-Notation folgt dem Schema Minute - Stunde - Tag (des Monats) - Monat - Tag (der Woche), etwa mi h d m wd. Mit der Verwendung von * als Platzhalter haben wir die Möglichkeit, in sehr flexiblen Intervallen zu planen. Nehmen wir an, wir wollen, dass ein Job jeden ersten Tag des Monats um zwölf Uhr ausgeführt wird. In diesem Fall wollen wir weder einen bestimmten Monat noch einen bestimmten Wochentag und ersetzen den Platzhalter durch eine Wildcard * ( min h d * *). Da es um 12:00 laufen soll, ersetzen wir mi mit 0 und h mit 12. Schließlich geben wir noch den Tag des Monats als 1 ein und erhalten unsere endgültige Cron-Notation 0 12 1 * *. Wenn wir nicht so spezifisch sein wollen, sondern lediglich täglich oder stündlich, beginnend mit dem Startdatum Ausführungen benötigen, können wir Airflows Hilfsmittel verwenden – @daily, @hourly, @monthly oder @yeary.

Sobald wir diese DAG-Instanz haben, können wir damit beginnen, sie mit einer Aufgabe zu füllen. Instanzen von Operatoren in Airflow repräsentieren diese. Hier initiieren wir einen DummyOperator und einen PythonOperator. Beiden muss eine eindeutige id zugewiesen werden, aber dieses Mal muss sie nur innerhalb des Workflows eindeutig sein. Der erste Operator, den wir definieren, ist ein DummyOperator, der überhaupt nichts tut. Wir wollen nur, dass er unseren Graphen füllt und dass wir Airflow mit einem möglichst einfachen Szenario testen können. Der zweite ist ein PythonOperator. Neben der Zuordnung zu einem Graphen und der id benötigt der Operator eine Funktion, die ausgeführt wird, sobald die Aufgabe ausgelöst wird. Nun können wir unsere Funktion hello_world verwenden und über den PythonOperator an unseren Workflow anhängen.

Bevor wir unseren Ablauf schließlich ausführen können, müssen wir noch die Beziehung zwischen unseren Aufgaben herstellen. Diese Verknüpfung wird entweder mit den binären Operatoren << und >> oder durch den Aufruf der Methoden set_upstream und set_downstream vorgenommen. Auf diese Weise können wir die Abhängigkeit einstellen, dass zuerst der DummyOperator laufen und erfolgreich sein muss, bevor unser PythonOperator ausgeführt wird.

Nun da unser Code in Ordnung ist, sollten wir ihn testen. Dazu sollten wir ihn direkt im Python-Interpreter ausführen, um zu prüfen, ob wir einen Syntaxfehler haben. Führe ihn also entweder in einer IDE oder im Terminal mit dem Befehl python hello_world.py aus. Wenn der Interpreter keine Fehlermeldung ausgibt, kannst du dich glücklich schätzen, dass du es nicht allzu sehr vermasselt hast. Als nächstes müssen wir überprüfen, ob Airflow unsere DAG mit airflow list_dags kennt. Jetzt sollten wir unsere hello_world id in der gedruckten Liste sehen. Wenn dies der Fall ist, können wir mit airflow list_task hello_world überprüfen, ob jede Aufgabe ihm zugewiesen ist. Auch hier sollten wir einige bekannte IDs sehen, nämlich dummy_task und hello_task. So weit so gut, zumindest die Zuweisung scheint zu funktionieren. Als nächstes steht ein Unit-Test der einzelnen Operatoren mit airflow test dummy_task 2018-01-01 und airflow test hello_task 2018-01-01 an. Hoffentlich gibt es dabei keine Fehler, und wir können fortfahren.

Da wir nun unseren Beispiel-Workflow bereitstellen konnten, müssen wir Airflow zunächst vollständig starten. Dazu sind drei Befehle erforderlich, bevor wir mit der manuellen Auslösung unserer Aufgabe fortfahren können.

  1. airflow initdb um die Datenbank zu initiieren, in der Airflow die Arbeitsabläufe und ihre Zustände speichert:
    initdb-sequential
  2. airflow webserver, um den Webserver auf localhost:8080 zu starten, von wo aus wir die Weboberfläche erreichen können:
    webserver-sequential
  3. airflow scheduler, um den Scheduling-Prozess der DAGs zu starten, damit die einzelnen Workflows ausgelöst werden können:scheduler-sequential
  4. airflow trigger_dag hello_world um unseren Workflow auszulösen und ihn in den Zeitplan aufzunehmen.

Jetzt können wir entweder einen Webbrowser öffnen und zu der entsprechenden Website navigieren oder open http://localhost:8080/admin/ im Terminal aufrufen, und es sollte uns zu einer Webseite wie dieser führen.

web-ui

Unten solltest du deine Kreation sehen und der hellgrüne Kreis zeigt an, dass unser Ablauf geplant ist und ausgeführt wird. Jetzt müssen wir nur noch warten, bis er ausgeführt wird. In der Zwischenzeit können wir über das Einrichten von Airflow sprechen und darüber, wie wir einige der anderen Executors verwenden können.

Das Backend

Wie bereits erwähnt – sobald wir uns ernsthaft mit der Ausführung unserer Graphen beschäftigen wollen, müssen wir das Backend von Airflow ändern. Anfänglich wird eine einfache SQLite-Datenbank verwendet, die Airflow darauf beschränkt, jeweils nur eine Aufgabe sequenziell auszuführen. Daher werden wir zunächst die angeschlossene Datenbank auf PostgreSQL umstellen. Falls du Postgres noch nicht installiert hast und Hilfe dabei brauchst, empfehle ich dir diesen Wiki-Artikel. Ich könnte den Prozess nicht so gut beschreiben wie die Seite. Für diejenigen, die mit einem Linux-basierten System arbeiten (sorry, Windows), versucht es mit sudo apt-get install postgresql-client oder mit homebrew auf einem Mac – brew install postgresql. Eine andere einfache Möglichkeit wäre die Verwendung eines Docker-Containers mit dem entsprechenden image.

Nun erstellen wir eine neue Datenbank für Airflow, indem wir im Terminal psql createdb airflow eingeben, in dem alle Metadaten gespeichert werden. Als nächstes müssen wir die Datei airflow.cfg bearbeiten, die in dem AIRFLOW_HOME-Ordner erscheinen sollte (der wiederum standardmäßig airflow in Ihrem Home-Verzeichnis ist) und die Schritte 1 – 4 von oben (initdb…) neu starten. Starte nun deinen Lieblingseditor und suche nach Zeile 32 sql_alchemy_conn =. Hier werden wir den SQLite Connection String durch den von unserem PostgreSQL-Server und einen neuen Treiber ersetzen. Diese Zeichenkette wird zusammengesetzt aus:

postgresql+psycopg2://IPADRESS:PORT/DBNAME?user=USERNAME&password=PASSWORD  

Der erste Teil teilt sqlalchemy mit, dass die Verbindung zu PostgreSQL führen wird und dass es den psycopg2-Treiber verwenden soll, um sich mit diesem zu verbinden. Falls du Postgres lokal installiert hast (oder in einem Container, der auf localhost mappt) und den Standard-Port von 5432 nicht geändert hast, könnte IPADRESS:PORT in localhost:5432 oder einfach localhost übersetzt werden. Der DBNAME würde in unserem Fall in airflow geändert werden, da wir ihn nur zu diesem Zweck erstellt haben. Die letzten beiden Teile hängen davon ab, was du als Sicherheitsmaßnahmen gewählt hast. Schließlich könnten wir eine Zeile erhalten haben, die wie folgt aussieht:

sql_alchemy_conn = postgresql+psycopg2://localhost/airflow?user=postgres&password=password  

Wenn wir dies getan haben, können wir auch unseren Executor in Zeile 27 von „Executor = SequentialExecutor“ in einen „Executor = LocalExecutor“ ändern. Auf diese Weise wird jede Aufgabe als Unterprozess gestartet und die Parallelisierung findet lokal statt. Dieser Ansatz funktioniert hervorragend, solange unsere Aufträge nicht zu kompliziert sind oder auf mehreren Rechnern laufen sollen.

Sobald wir diesen Punkt erreicht haben, brauchen wir Celery als Executor. Dabei handelt es sich um eine asynchrone Task/Job-Warteschlange, die auf verteilter Nachrichtenübermittlung basiert. Um den CeleryExecutor zu verwenden, benötigen wir jedoch ein weiteres Stück Software – einen Message Broker. Ein Message Broker ist ein zwischengeschaltetes Programmmodul, das eine Nachricht von der „Sprache“ des Senders in die des Empfängers übersetzt. Die beiden gängigsten Optionen sind entweder redis oder rabbitmq. Verwende das, womit du dich am wohlsten fühlst. Da wir rabbitmq verwendet haben, wird der gesamte Prozess mit diesem Broker fortgesetzt, sollte aber für redis mehr oder weniger analog sein.

Wiederum ist es für Linux- und Mac-Benutzer mit apt/homebrew ein Einzeiler, ihn zu installieren. Tippe einfach in dein Terminal sudo apt-get install rabbitmq-server oder brew install rabbitmq ein und fertig. Als nächstes brauchen wir einen neuen Benutzer mit einem Passwort und einen virtuellen Host. Beides – Benutzer und Host – kann im Terminal mit dem rabbitsmqs Kommandozeilen-Tool rabbitmqctl erstellt werden. Nehmen wir an, wir wollen einen neuen Benutzer namens myuser mit mypassword und einen virtuellen Host als myvhost erstellen. Dies kann wie folgt erreicht werden:

$ rabbitmqctl add_user myuser mypassword  
$ rabbitmqctl add_vhost myvhost  

Doch nun zurück zur Airflows-Konfiguration. Navigiere in deinem Editor zur Zeile 230, und du wirst hoffentlich broker_url = sehen. Dieser Connection-String ist ähnlich wie der für die Datenbank und wird nach dem Muster BROKER://USER:PASSWORD@IP:PORT/HOST aufgebaut. Unser Broker hat das Akronym amqp, und wir können unseren neu erstellten Benutzer, das Passwort und den Host einfügen. Sofern du nicht den Port geändert hast oder einen Remote Server verwendest, sollte deine Zeile in etwa so aussehen:

broker_url = amqp://myuser:mypassword@localhost:5672/myvhost  

Als nächstes müssen wir Celery Zugriff auf unsere airflow-Datenbank gewähren und die Zeile 232 mit:

db+postgresql://localhost:5432/airflow?user=postgres&password=password

Dieser String sollte im Wesentlichen dem entsprechen, den wir zuvor verwendet haben. Wir müssen nur den Treiber psycopg2 weglassen und stattdessen db+ am Anfang hinzufügen. Und das war’s! Du solltest nun alle drei Executors in der Hand haben und die Einrichtung ist abgeschlossen. Unabhängig davon, welchen Executor du gewählt hast, musst du, sobald du die Konfiguration geändert hast, die Schritte 1-4 – Initialisierung der DB, Neustart des Schedulers und des Webservers – erneut ausführen. Wenn du dies jetzt tust, wirst du feststellen, dass sich die Eingabeaufforderung leicht verändert hat, da sie anzeigt, welchen Executor du verwendest.

webserver-celery

Schluss & Ausblick

Airflow ist ein einfach zu bedienender, codebasierter Workflow-Manager mit einem integrierten Scheduler und mehreren Executors, die je nach Bedarf skaliert werden können.

Wenn du einen Ablauf sequenziell ausführen willst oder wenn es nichts gibt, was gleichzeitig laufen könnte, sollten die Standard-SQLite-Datenbank und der sequenzielle Executor die Aufgabe erfüllen.

Wenn du Airflow verwenden willst, um mehrere Aufgaben gleichzeitig zu starten und so die Abhängigkeiten zu verfolgen, solltest du zuerst die Datenbank und einen LocalExecutor für lokale Mehrfachverarbeitung verwenden. Dank Celery sind wir sogar in der Lage, mehrere Maschinen zu verwenden, um noch fortgeschrittenere und komplexere Workflows ohne viel Aufwand und Sorgen auszuführen. Marvin Taschenberger Marvin Taschenberger Marvin Taschenberger

Errors, Warnings und Meldungen erfassen, während Listenoperationen weiterlaufen

In den ersten beiden Teilen meiner Artikelserie über Textmining habe ich einige Lösungen für das Webscraping der Inhalte unseres STATWORX-Blogs mit Hilfe des purrr-Pakets vorgestellt. Bei den Vorbereitungen für die nächste Folge meiner Serie über Text Mining fiel mir jedoch ein kleines Gimmick ein, das ich auf dem Weg dorthin sehr hilfreich fand. Daher ein kleiner Umweg: Wie erfasse ich Side-Effects und Errors, wenn ich Operationen auf Listen mit purrr durchführe, anstatt eine Schleife zu verwenden?

meme blog loop

Zunächst einmal ein kurzes motivierendes Beispiel: Stell dir vor, wir wollen einen Parser für den Blog unserer STATWORX-Website entwickeln. Wir haben jedoch keine Ahnung, wie viele Einträge in der Zwischenzeit gepostet wurden (der Bienenstock von Data Scientists in unserem Büro ist in der Regel ziemlich schnell mit dem Schreiben solcher Artikel). Daher muss eine solche Funktion robuster sein, d. h. sie muss die Grausamkeiten von „404 – Not found“-Fehlermeldungen überstehen und auch nach einem Fehler noch weiter parsen können.

Wie könnte das possibly() funktionieren?

Lass uns also einige schöne purrr-Adverbien verwenden, um alle unsere Ausgaben, Errors und Warnings furchtlos aufzuzeichnen, anstatt anzuhalten und den User aufzufordern, sich um die Seiteneffekte zu kümmern, sobald Fehler auftauchen. Diese Adverbien erinnern an try(), sind aber etwas praktischer für Operationen auf Listen.

Betrachten wir zunächst ein komplexeres, motivierendes Beispiel, aber keine Sorge – weiter unten auf dieser Seite gibt es offensichtlichere Beispiele, die helfen, die Feinheiten zu erklären. Der folgende R-Code veranschaulicht unsere Verwendung von possibly() für die spätere Verwendung mit puurr::map(). Lass uns zunächst einen Blick darauf werfen, was wir mit unserer Funktion erreichen wollten. Genauer gesagt, was zwischen den geschweiften Klammern unten passiert: Unsere Funktion robust_parse() parst einfach HTML-Webseiten nach anderen Links unter Verwendung von URLs, die wir ihr zur Verfügung stellen. In diesem Fall verwenden wir einfach paste0(), um einen Vektor von Links zu unseren Blog-Übersichtsseiten zu erstellen, extrahieren die Weblinks von jeder dieser Seiten mit XML::xpathSApply(), leiten diese Weblinks in einen data_frame und bereinigen unsere Ergebnisse von Duplikaten mit dplyr::filter() – es gibt verschiedene Übersichtsseiten, die unsere Blogs nach Kategorien gruppieren – und dplyr::distinct().

robust_parse <- possibly(function(value){
        htmlParse(paste0("https://statworx-1727.demosrv.review/de/blog/page/",
                         value, "/")) %>%
          xpathSApply(., "//a/@href") %>%
          data_frame(.) %>%
          filter(., grepl("/blog", .)) %>%
          filter(., !grepl("/blog/|/blog/page/|/data-science/|/statistik/", .)) %>%           distinct()       }, otherwise = NULL) </code></pre>  Lass uns nun untersuchen, wie wir <code>possibly()</code> in diesem Zusammenhang verwenden. <code>possibly()</code> erwartet von uns eine zu modifizierende Funktion sowie das Argument <code>otherwise</code>, das angibt, was sie tun soll, wenn es schiefgeht. In diesem Fall wollen wir <code>NULL</code> als Ausgabewert. Eine andere beliebte Wahl wäre <code>NA</code>, um zu signalisieren, dass wir irgendwo eine Zeichenkette nicht wie vorgesehen erzeugt haben. In unserem Beispiel sind wir jedoch mit <code>NULL</code> zufrieden, da wir nur die existierenden Seiten analysieren wollen und keine spezifische Auflistung der nicht existierenden Seiten benötigen (oder was passiert ist, wenn wir eine Seite nicht gefunden haben).  <pre><code class="language-r" lang="r">webpages <- map_df(0:100, ~robust_parse(.)) %>%  			unlist  > webpages .1 "https://statworx-1727.demosrv.review/de/blog/strsplit-but-keeping-the-delimiter/" .2 "https://statworx-1727.demosrv.review/de/blog/data-science-in-python-vorstellung-von-nuetzlichen-datenstrukturen-teil-1/" .3 "https://statworx-1727.demosrv.review/de/blog/burglr-stealing-code-from-the-web/" .4 "https://statworx-1727.demosrv.review/de/blog/regularized-greedy-forest-the-scottish-play-act-i/"  ... </code></pre>  Drittens verwenden wir unsere neue Funktion <code>robust_parse()</code>, um mit einem Vektor oder einer Liste von ganzen Zahlen von 0 bis 100 (mögliche Anzahl von Unterseiten, die wir analysieren wollen) zu arbeiten, und werfen einen kurzen Blick auf die schönen Links, die wir extrahiert haben. Zur Erinnerung: Unten findest du den Code zum Extrahieren und Bereinigen der Inhalte der einzelnen Seiten mit Hilfe einer weiteren <code>map_df()</code>-basierten Schleife - die im Mittelpunkt eines <a href="https://statworx-1727.demosrv.review/de/blog/furchtlose-grammatiker-textmining-im-tidyverse-teil-2/">anderen Beitrags</a> steht. <pre><code class="language-r" lang="r">tidy_statworx_blogs <- map_df(webpages, ~read_html(.) %>%                                	htmlParse(., asText = TRUE) %>%                                 xpathSApply(., "//p", xmlValue) %>%                                 paste(., collapse = "\n") %>%                                 gsub("\n", "", .) %>%                                 data_frame(text = .) %>%                                 unnest_tokens(word, text) %>%                                 anti_join(data_frame(word = stopwords("de"))) %>%                                  anti_join(data_frame(word = stopwords("en"))) %>%                                  mutate(author = .word[2]))

Aber eigentlich wollen wir zu unseren purrr-Helfern zurückkehren und sehen, was sie für uns tun können. Um genauer zu sein, heißen sie nicht Helfer, sondern Adverbien, da wir sie verwenden, um das Verhalten einer Funktion (d.h. eines Verbs) zu ändern. Unsere derzeitige Funktion robust_parse() erzeugt keine Einträge, wenn die Schleife nicht erfolgreich eine Webseite findet, die sie nach Links durchsucht.

Stell dir die Situation vor, dass du erfolglose Operationen und Fehler, die auf dem Weg dorthin auftreten, verfolgen wilslt. Anstatt die purrr-Adverbien anhand des oben genannten Codes weiter zu erforschen, wollen wir uns ein viel einfacheres Beispiel ansehen, um die möglichen Zusammenhänge zu erkennen, in denen die Verwendung von purrr-Adverbien hilfreich sein kann.

Ein viel einfacheres Beispiel: Versuche, eine Zeichenkette durch 2 zu teilen

Angenommen, es gibt ein Element in unserer Liste, bei dem unsere erstaunlichen Divisionsfähigkeiten nutzlos sind: Wir werden versuchen, alle Elemente in unserer Liste durch 2 zu teilen – aber dieses Mal wollen wir, dass purrr beachtet, wo die Funktion i_divide_things sich weigert, bestimmte Elemente für uns zu teilen. Auch hier hilft uns das otherwise-Argument, unsere Ausgabe in Situationen zu definieren, die außerhalb des Anwendungsbereichs unserer Funktion liegen.

i_divide_things <- possibly(function(value){
						value /2},
                  		otherwise = "I won't divide this for you.")

# Let's try our new function

> purrr::map(list(1, 2, "a", 6), ~ i_divide_things(.))
[[1]]
[1] 0.5

[[2]]
[1] 1

[[3]]
[1] "I won't divide this for you."

[[4]]
[1] 3


Bedenke allerdings den Fall, bei dem „etwas hat nicht funktioniert“ nicht ausreicht und du sowohl mögliche Fehler als auch Warnungen im Auge behalten willst, während du die gesamte Ausgabe beibehältst. Das ist eine Aufgabe für safely(): Wie unten dargestellt, hilft uns die Umrahmung unserer Funktion durch safely(), eine verschachtelte Liste auszugeben. Für jedes Element der Eingabe liefert die Ausgabe zwei Komponenten, result</code> und <code>error. Für alle Iterationen, bei denen ein Listenelement numerisch ist, enthält result</code> eine numerische Ausgabe und ein leeres (<code>= NULL</code>) Fehler-Element. Nur für das dritte Listenelement - wenn unsere Funktion über eine Zeicheneingabe stolpert - haben wir eine Fehlermeldung sowie das Ergebnis, das wir mit <code>otherwise</code> definiert haben, erfasst. <pre><code class="language-r">i_divide_things <- safely(function(value){                       value /2},                       otherwise = "This did not quite work out.")  > purrr::map(list(1, 2, "a", 6), ~ i_divide_things(.)) [[1]] [[1]]result
[1] 0.5

[[1]]error NULL   [[2]] [[2]]result
[1] 1

[[2]]error NULL   [[3]] [[3]]result
[1] "This did not quite work out."

[[3]]error <simpleError in value/2: non-numeric argument to binary operator>   [[4]] [[4]]result
[1] 3

[[4]]error NULL</code></pre> Im vorstehenden Beispiel haben wir unsere Fehler erst aufgedeckt, nachdem wir eine Schleife über alle Elemente unserer Liste durchlaufen haben, indem wir die Ausgabeliste inspiziert haben. Allerdings hat <code>safely()</code> auch das Argument <code>quiet</code>, das standardmäßig auf <code>TRUE</code> gesetzt ist. Wenn wir dies auf <code>FALSE</code> setzen, erhalten wir unsere Fehler genau dann, wenn sie auftreten.  Nun wollen wir einen kurzen Blick auf <code>quietly()</code> werfen. Wir werden eine Warnung und eine Nachricht definieren und einen Output erstellen. Dies soll veranschaulichen, wo <em>purrr</em> die einzelnen Komponenten speichert, die unsere Funktion zurückgibt. Für jedes Element unserer Eingabe liefert die zurückgegebene Liste vier Komponenten: <ul>  	<li><code>result gibt wiederum das Ergebnis unserer Operation zurück

  • output</code> gibt die Ausgabe zurück, die in der Konsole ausgegeben wurde</li>  	<li><code>warnings und message</code> geben die von uns definierten Zeichenketten zurück</li> </ul> <pre><code class="language-r" lang="r">i_divide_things <- purrr::quietly(function(value){   if(is.numeric(value) == TRUE) {           print(value / 2)   }   else{            warning("Can't be done. Printing this instead.")           message("Why would you even try dividing this?")           print(value)   }   })  > purrr::map(list(1, "a", 6), ~i_divide_things(.)) [[1]] [[1]]result
    [1] 0.5

    [[1]]output [1] "[1] 0.5"  [[1]]warnings
    character(0)

    [[1]]messages character(0)   [[2]] [[2]]result
    [1] "a"

    [[2]]output [1] "[1] \"a\""  [[2]]warnings
    [1] "Can't be done. Printing this instead."

    [[2]]messages [1] "Why would you even try dividing this?\n"   [[3]] [[3]]result
    [1] 3

    [[3]]output [1] "[1] 3"  [[3]]warnings
    character(0)

    [[3]]$messages
    character(0)

    Schließlich gibt es noch auto_browse(), das es uns ermöglicht, den RStudio-Browser für die Fehlersuche auszulösen und die Usern an den ungefähren Ort des Fehlers zu bringen. Dieser Fall ist im folgenden Screenshot dargestellt.

    i_divide_things <- purrr::auto_browse(function(value){
    
        print(value / 2)
    })
    
    purrr::map(list(1, "a", 6), ~i_divide_things(.)) 
    

     

    output of auto_browse

     

    Wunderbar – das war eine schnelle Zusammenfassung, wie du deine Funktionen zur Behandlung von Nebenwirkungen in deinen Listenoperationen mit Adverbien von purrr verpackst. Happy Coding! David Schlepps David Schlepps

  • Errors, Warnings und Meldungen erfassen, während Listenoperationen weiterlaufen

    In den ersten beiden Teilen meiner Artikelserie über Textmining habe ich einige Lösungen für das Webscraping der Inhalte unseres STATWORX-Blogs mit Hilfe des purrr-Pakets vorgestellt. Bei den Vorbereitungen für die nächste Folge meiner Serie über Text Mining fiel mir jedoch ein kleines Gimmick ein, das ich auf dem Weg dorthin sehr hilfreich fand. Daher ein kleiner Umweg: Wie erfasse ich Side-Effects und Errors, wenn ich Operationen auf Listen mit purrr durchführe, anstatt eine Schleife zu verwenden?

    meme blog loop

    Zunächst einmal ein kurzes motivierendes Beispiel: Stell dir vor, wir wollen einen Parser für den Blog unserer STATWORX-Website entwickeln. Wir haben jedoch keine Ahnung, wie viele Einträge in der Zwischenzeit gepostet wurden (der Bienenstock von Data Scientists in unserem Büro ist in der Regel ziemlich schnell mit dem Schreiben solcher Artikel). Daher muss eine solche Funktion robuster sein, d. h. sie muss die Grausamkeiten von „404 – Not found“-Fehlermeldungen überstehen und auch nach einem Fehler noch weiter parsen können.

    Wie könnte das possibly() funktionieren?

    Lass uns also einige schöne purrr-Adverbien verwenden, um alle unsere Ausgaben, Errors und Warnings furchtlos aufzuzeichnen, anstatt anzuhalten und den User aufzufordern, sich um die Seiteneffekte zu kümmern, sobald Fehler auftauchen. Diese Adverbien erinnern an try(), sind aber etwas praktischer für Operationen auf Listen.

    Betrachten wir zunächst ein komplexeres, motivierendes Beispiel, aber keine Sorge – weiter unten auf dieser Seite gibt es offensichtlichere Beispiele, die helfen, die Feinheiten zu erklären. Der folgende R-Code veranschaulicht unsere Verwendung von possibly() für die spätere Verwendung mit puurr::map(). Lass uns zunächst einen Blick darauf werfen, was wir mit unserer Funktion erreichen wollten. Genauer gesagt, was zwischen den geschweiften Klammern unten passiert: Unsere Funktion robust_parse() parst einfach HTML-Webseiten nach anderen Links unter Verwendung von URLs, die wir ihr zur Verfügung stellen. In diesem Fall verwenden wir einfach paste0(), um einen Vektor von Links zu unseren Blog-Übersichtsseiten zu erstellen, extrahieren die Weblinks von jeder dieser Seiten mit XML::xpathSApply(), leiten diese Weblinks in einen data_frame und bereinigen unsere Ergebnisse von Duplikaten mit dplyr::filter() – es gibt verschiedene Übersichtsseiten, die unsere Blogs nach Kategorien gruppieren – und dplyr::distinct().

    robust_parse <- possibly(function(value){
            htmlParse(paste0("https://statworx-1727.demosrv.review/de/blog/page/",
                             value, "/")) %>%
              xpathSApply(., "//a/@href") %>%
              data_frame(.) %>%
              filter(., grepl("/blog", .)) %>%
              filter(., !grepl("/blog/|/blog/page/|/data-science/|/statistik/", .)) %>%           distinct()       }, otherwise = NULL) </code></pre>  Lass uns nun untersuchen, wie wir <code>possibly()</code> in diesem Zusammenhang verwenden. <code>possibly()</code> erwartet von uns eine zu modifizierende Funktion sowie das Argument <code>otherwise</code>, das angibt, was sie tun soll, wenn es schiefgeht. In diesem Fall wollen wir <code>NULL</code> als Ausgabewert. Eine andere beliebte Wahl wäre <code>NA</code>, um zu signalisieren, dass wir irgendwo eine Zeichenkette nicht wie vorgesehen erzeugt haben. In unserem Beispiel sind wir jedoch mit <code>NULL</code> zufrieden, da wir nur die existierenden Seiten analysieren wollen und keine spezifische Auflistung der nicht existierenden Seiten benötigen (oder was passiert ist, wenn wir eine Seite nicht gefunden haben).  <pre><code class="language-r" lang="r">webpages <- map_df(0:100, ~robust_parse(.)) %>%  			unlist  > webpages .1 "https://statworx-1727.demosrv.review/de/blog/strsplit-but-keeping-the-delimiter/" .2 "https://statworx-1727.demosrv.review/de/blog/data-science-in-python-vorstellung-von-nuetzlichen-datenstrukturen-teil-1/" .3 "https://statworx-1727.demosrv.review/de/blog/burglr-stealing-code-from-the-web/" .4 "https://statworx-1727.demosrv.review/de/blog/regularized-greedy-forest-the-scottish-play-act-i/"  ... </code></pre>  Drittens verwenden wir unsere neue Funktion <code>robust_parse()</code>, um mit einem Vektor oder einer Liste von ganzen Zahlen von 0 bis 100 (mögliche Anzahl von Unterseiten, die wir analysieren wollen) zu arbeiten, und werfen einen kurzen Blick auf die schönen Links, die wir extrahiert haben. Zur Erinnerung: Unten findest du den Code zum Extrahieren und Bereinigen der Inhalte der einzelnen Seiten mit Hilfe einer weiteren <code>map_df()</code>-basierten Schleife - die im Mittelpunkt eines <a href="https://statworx-1727.demosrv.review/de/blog/furchtlose-grammatiker-textmining-im-tidyverse-teil-2/">anderen Beitrags</a> steht. <pre><code class="language-r" lang="r">tidy_statworx_blogs <- map_df(webpages, ~read_html(.) %>%                                	htmlParse(., asText = TRUE) %>%                                 xpathSApply(., "//p", xmlValue) %>%                                 paste(., collapse = "\n") %>%                                 gsub("\n", "", .) %>%                                 data_frame(text = .) %>%                                 unnest_tokens(word, text) %>%                                 anti_join(data_frame(word = stopwords("de"))) %>%                                  anti_join(data_frame(word = stopwords("en"))) %>%                                  mutate(author = .word[2]))
    

    Aber eigentlich wollen wir zu unseren purrr-Helfern zurückkehren und sehen, was sie für uns tun können. Um genauer zu sein, heißen sie nicht Helfer, sondern Adverbien, da wir sie verwenden, um das Verhalten einer Funktion (d.h. eines Verbs) zu ändern. Unsere derzeitige Funktion robust_parse() erzeugt keine Einträge, wenn die Schleife nicht erfolgreich eine Webseite findet, die sie nach Links durchsucht.

    Stell dir die Situation vor, dass du erfolglose Operationen und Fehler, die auf dem Weg dorthin auftreten, verfolgen wilslt. Anstatt die purrr-Adverbien anhand des oben genannten Codes weiter zu erforschen, wollen wir uns ein viel einfacheres Beispiel ansehen, um die möglichen Zusammenhänge zu erkennen, in denen die Verwendung von purrr-Adverbien hilfreich sein kann.

    Ein viel einfacheres Beispiel: Versuche, eine Zeichenkette durch 2 zu teilen

    Angenommen, es gibt ein Element in unserer Liste, bei dem unsere erstaunlichen Divisionsfähigkeiten nutzlos sind: Wir werden versuchen, alle Elemente in unserer Liste durch 2 zu teilen – aber dieses Mal wollen wir, dass purrr beachtet, wo die Funktion i_divide_things sich weigert, bestimmte Elemente für uns zu teilen. Auch hier hilft uns das otherwise-Argument, unsere Ausgabe in Situationen zu definieren, die außerhalb des Anwendungsbereichs unserer Funktion liegen.

    i_divide_things <- possibly(function(value){
    						value /2},
                      		otherwise = "I won't divide this for you.")
    
    # Let's try our new function
    
    > purrr::map(list(1, 2, "a", 6), ~ i_divide_things(.))
    [[1]]
    [1] 0.5
    
    [[2]]
    [1] 1
    
    [[3]]
    [1] "I won't divide this for you."
    
    [[4]]
    [1] 3
    
    
    

    Bedenke allerdings den Fall, bei dem „etwas hat nicht funktioniert“ nicht ausreicht und du sowohl mögliche Fehler als auch Warnungen im Auge behalten willst, während du die gesamte Ausgabe beibehältst. Das ist eine Aufgabe für safely(): Wie unten dargestellt, hilft uns die Umrahmung unserer Funktion durch safely(), eine verschachtelte Liste auszugeben. Für jedes Element der Eingabe liefert die Ausgabe zwei Komponenten, result</code> und <code>error. Für alle Iterationen, bei denen ein Listenelement numerisch ist, enthält result</code> eine numerische Ausgabe und ein leeres (<code>= NULL</code>) Fehler-Element. Nur für das dritte Listenelement - wenn unsere Funktion über eine Zeicheneingabe stolpert - haben wir eine Fehlermeldung sowie das Ergebnis, das wir mit <code>otherwise</code> definiert haben, erfasst. <pre><code class="language-r">i_divide_things <- safely(function(value){                       value /2},                       otherwise = "This did not quite work out.")  > purrr::map(list(1, 2, "a", 6), ~ i_divide_things(.)) [[1]] [[1]]result
    [1] 0.5

    [[1]]error NULL   [[2]] [[2]]result
    [1] 1

    [[2]]error NULL   [[3]] [[3]]result
    [1] "This did not quite work out."

    [[3]]error <simpleError in value/2: non-numeric argument to binary operator>   [[4]] [[4]]result
    [1] 3

    [[4]]error NULL</code></pre> Im vorstehenden Beispiel haben wir unsere Fehler erst aufgedeckt, nachdem wir eine Schleife über alle Elemente unserer Liste durchlaufen haben, indem wir die Ausgabeliste inspiziert haben. Allerdings hat <code>safely()</code> auch das Argument <code>quiet</code>, das standardmäßig auf <code>TRUE</code> gesetzt ist. Wenn wir dies auf <code>FALSE</code> setzen, erhalten wir unsere Fehler genau dann, wenn sie auftreten.  Nun wollen wir einen kurzen Blick auf <code>quietly()</code> werfen. Wir werden eine Warnung und eine Nachricht definieren und einen Output erstellen. Dies soll veranschaulichen, wo <em>purrr</em> die einzelnen Komponenten speichert, die unsere Funktion zurückgibt. Für jedes Element unserer Eingabe liefert die zurückgegebene Liste vier Komponenten: <ul>  	<li><code>result gibt wiederum das Ergebnis unserer Operation zurück

  • output</code> gibt die Ausgabe zurück, die in der Konsole ausgegeben wurde</li>  	<li><code>warnings und message</code> geben die von uns definierten Zeichenketten zurück</li> </ul> <pre><code class="language-r" lang="r">i_divide_things <- purrr::quietly(function(value){   if(is.numeric(value) == TRUE) {           print(value / 2)   }   else{            warning("Can't be done. Printing this instead.")           message("Why would you even try dividing this?")           print(value)   }   })  > purrr::map(list(1, "a", 6), ~i_divide_things(.)) [[1]] [[1]]result
    [1] 0.5

    [[1]]output [1] "[1] 0.5"  [[1]]warnings
    character(0)

    [[1]]messages character(0)   [[2]] [[2]]result
    [1] "a"

    [[2]]output [1] "[1] \"a\""  [[2]]warnings
    [1] "Can't be done. Printing this instead."

    [[2]]messages [1] "Why would you even try dividing this?\n"   [[3]] [[3]]result
    [1] 3

    [[3]]output [1] "[1] 3"  [[3]]warnings
    character(0)

    [[3]]$messages
    character(0)

    Schließlich gibt es noch auto_browse(), das es uns ermöglicht, den RStudio-Browser für die Fehlersuche auszulösen und die Usern an den ungefähren Ort des Fehlers zu bringen. Dieser Fall ist im folgenden Screenshot dargestellt.

    i_divide_things <- purrr::auto_browse(function(value){
    
        print(value / 2)
    })
    
    purrr::map(list(1, "a", 6), ~i_divide_things(.)) 
    

     

    output of auto_browse

     

    Wunderbar – das war eine schnelle Zusammenfassung, wie du deine Funktionen zur Behandlung von Nebenwirkungen in deinen Listenoperationen mit Adverbien von purrr verpackst. Happy Coding! David Schlepps David Schlepps