Blog

ML Summit
Das große Trainingsevent für Machine Learning & Data Science
5
Aug

Jupyter Notebooks für Lehre und Entwicklung – alles im Blick:
Notizbuch für Entwickler

Denkt man an Themen, die sich mangels Laborausstattung nur schwer oder gar nicht veranschaulichen lassen, kommen einem vielleicht Elektronik und ähnliche Themen in den Sinn. Dass allerdings auch ein eben mal schnell erstellter Algorithmus angesichts des Fehlens von Compiler und Co. Probleme verursachen kann, stellt man oft erst bei schärferem Nachdenken fest. Grundlage aller Jupyter-bezogenen...
Read More

Denkt man an Themen, die sich mangels Laborausstattung nur schwer oder gar nicht veranschaulichen lassen, kommen einem vielleicht Elektronik und ähnliche Themen in den Sinn. Dass allerdings auch ein eben mal schnell erstellter Algorithmus angesichts des Fehlens von Compiler und Co. Probleme verursachen kann, stellt man oft erst bei schärferem Nachdenken fest.

Grundlage aller Jupyter-bezogenen Projekte ist ein als IPython bezeichneter Kernel, der im Prinzip eine um Komfortfunktionen erweiterte Variante der interaktiven Ansicht in REPL (Read-Eval-Print Loop) darstellt. Hintergedanke des meist nur als Komponente anzutreffenden Diensts war es, Entwicklern ein bequem ansprechbares Interface zu REPL zu geben, über das sich neuartige Interaktionsszenarien einfach realisieren lassen.

Mittlerweile wurde dieses Interface wie in Abbildung 1 gezeigt um nicht-pythonische Programmiersprachen ergänzt.

Abb. 1: Das Jupyter-Projekt beschränkt sich nicht auf Python [1]

Was ist das Notebook?

Lange Python-Programme wirken insbesondere auf Quereinsteiger, aber auch auf Entwickler, die mit Python wenig vertraut sind, nicht wirklich einladend. Ein klassisches Antipattern wäre beispielsweise die Hello-World-Applikation der YDLIDAR-Laserscanner.

Jupyter Notebooks dürfte das wahrscheinlich bekannteste Anwendungsszenario von IPython sein. Im Prinzip handelt es sich dabei um einen Server, der das Ansprechen der Logik aus einem beliebigen Webbrowser heraus erlaubt (Abb. 2).

Abb. 2: Notebooks machen den Terminal-Emulator arbeitslos

Jupyter Notebooks beschränken sich nicht auf das Einblenden einer an Azure erinnernden Web-Console. Erzeuger des Jupyter Notebooks, die auf der Festplatte übrigens unter der Endung .ipynb gespeichert werden, dürfen auch Erklärungsnotizen anlegen. Lohn der Mühen ist eine an interaktive Lehrbücher erinnernde Webseite, in denen Entwickler bzw. Lernende mit den Arbeits- und Lehrinhalten direkt interagieren können.

Vorbereitung mit Windows 10

Wegen der mittlerweile sehr hohen Verbreitung von Jupyter steht eine Vielzahl von Installationsangeboten zur Verfügung. Wir wollen in den folgenden Schritten mit der nativen Arbeitsumgebung interagieren – der einfachste Weg zur Bereitstellung ist das Herunterladen der Anaconda-Distribution, die neben einem Python-Interpreter auch Komfortfunktionen wie Paketverwaltung und Co. bereitstellt. Besuchen Sie im ersten Schritt die Seite von Anaconda [2] und scrollen Sie bis zum Abschnitt Anaconda Installers. Der Autor entschied sich in den folgenden Schritten für die Version Python 3.7; da ein 64-Bit-Betriebssystem zum Einsatz kommt, müssen Sie die 466 MB große 64-Bit-Version des Programms auf Ihren Rechner herunterladen.

Im Rahmen der Installation fragt das Set-up-Programm, ob Anaconda zur Path-Variable hinzugefügt werden soll: Wir wollen das in den folgenden Schritten ablehnen, weil sich das Produkt auch aus dem Startmenü heraus anwerfen lässt.

Nach der erfolgreichen Installation der Anaconda-Version für Python 3.7 finden Sie im Startmenü den Eintrag Jupyter Notebooks, der den direkten Einstieg in die Arbeitsumgebung ermöglicht. Nach seiner Eingabe erscheint am Bildschirm Ihrer Workstation das in Abbildung 3 gezeigte Terminalfenster, das zum Aufrufen eines bestimmten URL auffordert.

Abb. 3: Die Installation von Jupyter verlief erfolgreich

Wer den URL – am Rechner des Autors lautete er http://localhost:8888/?token=701007f8ab81716cede6c1989fc338b6cc1f6c05ac8fe3fd – in einen Browser seiner Wahl eingibt, findet sich im ersten Schritt in einer Art Dateimanager wieder. Der Autor entschied sich in den folgenden Schritten für die Verwendung von Google Chrome. Andere Browser dürften im Allgemeinen ebenfalls funktionieren; in User-Gruppen findet man allerdings immer wieder Berichte von massiven Problemen bei der Verwendung des klassischen Internet Explorer. Angemerkt sei noch, dass das hier verwendete lokale Ausführen von sowohl Server als auch Client nur ein Weg zum Ziel ist. Es ist erlaubt, Client und Server auf verschiedenen Maschinen auszuführen oder auf die unter [3] zur Verfügung stehenden Webdienste zu setzen.

Bei einem direkten Anwerfen des Servers aus dem Startmenü ist das Files-Tab von Haus aus auf den Inhalt des Verzeichnisses des Laufwerks C: eingestellt. Sie können einzelne Ordner oder Dateien anklicken, um mit ihnen zu interagieren.

Im Running-Tab finden Sie dann eine Liste aller Jupyter-bezogenen Aufgaben, die von Ihrer Workstation zum jetzigen Zeitpunkt ausgeführt werden. Haben Sie das Produkt eben erst frisch installiert, ist dieses Tab logischerweise noch leer.

Wer erste Experimente durchführen möchte, klickt im nächsten Schritt auf den auf der Oberseite der Liste befindlichen Button New. Jupyter reagiert darauf mit der Einblendung eines Kontextmenüs, in dem sie sich für die Option Notebook | Python3 entscheiden. Nach dem erfolgreichen Start des Kernels präsentiert sich das Fenster wie in Abbildung 4 gezeigt.

Abb. 4: Das Jupyter Notebook ist für erste Versuche bereit

Zelluläre Struktur

Jupyter Notebooks unterscheiden sich von klassischen Quellcodedateien dadurch, dass die einzelnen, in Abbildung 4 gezeigten Elemente als Zellen bezeichnet werden. Klicken Sie im ersten Schritt auf das Menü, um die Option Insert Cell Below zu aktivieren. Jupyter reagiert darauf mit der Einblendung einer weiteren Zelle.

Zur weiteren Vorführung der Möglichkeiten müssen wir die beiden Zellen mit Code beladen. In die erste Zelle platzieren wir folgende kleine numerische Anweisung:

3+3

Zelle Nummer 2 soll fürs Erste eine kleine for-Schleife aufnehmen, die Informationen über das print-Kommando nach außen trägt:

for x in range(0, 3): print("We're on time %d" % (x))

Für die Ausführung wählen wir danach die Option Cell | Run Cells, was zu dem in Abbildung 5 dargestellten Ergebnis führt, wenn die mit der for-Schleife beladene Zelle markiert ist.

Abb. 5: Die Ausgabe der Schleife erscheint direkt unter dem Quellcodeeingabefeld

Eine Ausführung der Zelle mit dem numerischen Additionsbefehl lässt sich dadurch anweisen, dass Sie ihn markieren und danach auf das Run-Symbol oder die Option Run Cells klicken. Alternativ dazu gibt es auch die Menüoption Cell | Run All, die alle im Jupyter Notebook befindlichen Zellen gleichzeitig zur Ausführung freigibt.

Ein interessanter Versuch betrifft das Anlegen einer Variablen. Hierzu wählen wir eine Zelle, in der wir folgenden Code platzieren und danach ausführen:

i=0

Da das Zuweisen eines Wertes an eine Variable keine Konsolenausgaben auslöst, sehen wir die erfolgreiche Abarbeitung nur durch eine Inkrementierung des in eckigen Klammern angezeigten Index. Zudem bewegt sich der Fokus (normalerweise) eine Zelle weiter nach unten.

Im nächsten Schritt wählen wir deshalb eine freie Zelle, und platzieren in ihr folgenden inkrementierenden und ausgebenden Code:

i=i+2 i

Im nächsten Schritt können Sie diese Zelle mehrfach markieren und durch Anklicken von Run je einmal ausführen. Wer das einige Male wiederholt, sieht das in Abbildung 6 gezeigte Ergebnis.

Abb. 6: Die in i gespeicherten Werte bleiben zwischen den Aufrufen der Zelle erhalten

Wer den im Hintergrund laufenden Kernel beenden möchte, klickt im Menü auf die Option Kernel | Restart and Clear Output. Wenn Sie die Zelle mit dem Code zur Variableninkrementierung danach nochmals markieren und zur Ausführung freigeben, bekommen Sie im Ausgabefenster statt einem numerischen Wert den Fehler: „NameError: name ‚i‘ is not defined“. Das ist logisch und richtig, weil das Neustarten des Kernels alle in ihm gehaltenen Zustandsinformationen zurückgesetzt hat.

Wer das neu angelegte Notebook in einem separaten Tab geöffnet hat, kann an dieser Stelle auf die Jupyter-Startseite zurückkehren und dort den Tab Running selektieren. Unser neu angelegtes Notebook erscheint in der Rubrik Notebooks, wo es sich durch Anklicken der Shut Down-Option herunterfahren lässt.

Elemente anreichern

Bis hierher ist das Jupyter Notebook eine im Webbrowser lebende Python-Kommandozeile: Wer einen Editor wie Visual Studio Code komfortabel konfiguriert, hat beim reinen Ausführen vom Quellcode ein ähnliches Erlebnis. Die Vorteile von Jupyter Notebooks kann das Produkt erst dann ausspielen, wenn man neben den Codezellen auch fortgeschrittene Funktionen einsetzt.

Der mit Abstand häufigste Kandidat ist das Markdown-Format. Erzeugen Sie zu seiner Demonstration im ersten Schritt eine neue Zelle, und klicken Sie danach im Menü auf die Option Cell | Cell Type | Markdown. Nach dem Festlegen der Option verschwindet der []-Callout aus dem Header – das weist darauf hin, dass der Inhalt dieser Zelle nicht durch den IPython-Kernel, sondern durch einen HTML-Konverter bearbeitet wird.

Zur Demonstration des Handlings platzieren wir im nächsten Schritt folgende Syntax, die neben fett gedruckten und kursiven Text auch das Anlegen einer Überschrift der dritten Klasse demonstriert:

### Heading dritter Klasse **bold text** *italicized text* Normaler Text

Nach dem Deselektieren, aber auch schon während der Eingabe der Syntax sehen Sie, dass Jupyter Notebooks wie in Abbildung 7 gezeigt eine Syntax-Highlighting-Unterstützung anbietet.

Abb. 7: Der Editor hilft bei der Bearbeitung

Wer den Inhalt der Zelle danach selektiert und auf Run klickt, stellt fest, dass die gesamte Zelle durch den in Abbildung 8 gezeigten Output ersetzt wird. Das lässt sich dadurch rückgängig machen, dass man den Output doppelt anklickt – Jupyter Notebook wechselt in diesem Fall automatisch wieder in den Bearbeitungsmodus.

Abb. 8: Der Parser hat seine Schuldigkeit getan

Markdown-Zellen kann das Anlegen von Dokumentationskommentaren ermöglichen. In der Praxis ist das allerdings nur ein kleiner Teil der Lösung.

Anschaulich lernen

Die langjährige Lehrerfahrung des Autors zeigt, dass der Einfluss von Parametern Lernenden am einfachsten dadurch sichtbar gemacht werden kann, wenn sie mit dem betreffenden Wert spielen können. Ein gutes Beispiel dafür wäre das in Abbildung 9 gezeigte Schirmbild eines DPOs. Die Linie ist dabei der Triggerwert, der die Welle zwecks Stabilisierung der Anzeige „touchieren“ muss. Der einfachste Weg, um dieses Verfahren Lernenden klarzumachen, besteht darin, die Linie durch Nutzung des Trigger-Level-Rheostats über den Bildschirm verschieben zu lassen. Sieht man, dass die Welle nun steht, tritt umgehend ein positiver Lerneffekt ein.

Abb. 9: Die richtige Einstellung des Triggers will gelernt sein

Erfordert das Spielen mit Parametern manuelle Anpassungen des Quellcodes sowie eine Rekompilation, lehrt die Erfahrung, dass die direkte Feedbackschleife unterbrochen wird und die Lerneffizienz rapide abnimmt. Eine effizientere Maßnahme wäre das Einbinden von Steuerelementen wie beispielsweise eines Schiebereglers, der das „direkte“ Beeinflussen der Parameter erlaubt. In der Welt von Jupyter wird das über die ipywidgets-Bibliothek realisiert, deren grundlegender Aufbau in Abbildung 10 gezeigt ist.

Abb. 10: Widgets leben sowohl im Kernel als auch im Frontend [4]

Auf Kernelebene liegt dabei eine Widget-Klasse, die man in der Data-Binding-Welt von XAML und Co. als Modell bezeichnen würde. Im Frontend findet sich eine Manipulationsklasse, die einerseits die im Modell lebenden Werte justiert und sich andererseits darum kümmert, die in den Browsern angezeigten Steuerelemente zu aktualisieren.

Im Fortgang wollen wir mit einem neuen Notebook weitermachen. Klicken Sie deshalb auf File | New Notebook | Python 3, um ein weiteres Notebook zu erzeugen. Das vorherige Speichern der schon geöffneten Arbeitsmappe ist übrigens nicht erforderlich, da der Jupyter-Notebook-Server automatisch einen neuen Namen zuweist.

Für erste Gehversuche benötigen wir zwei Zellen. Die erste davon statten wir mit folgendem Code aus:

import ipywidgets as widgets tamswidget = widgets.FloatSlider()

Da der im Hintergrund des Notebooks lebende Kernel eine mehr oder weniger gewöhnliche Python Runtime darstellt, ist der Autor nicht von der Verwendung von import-Deklarationen befreit. Nach dem Laden des relevanten Moduls rufen wir den Konstruktor auf, um eine Instanz eines mit Float-Werten parametrisierten Schiebers zu erzeugen und in die Variable TamsWidget zu speichern.

In der zweiten Zelle setzen wir im ersten Schritt einen Verweis auf die display-Methode, um ihr daraufhin die weiter oben erzeugte Instanz zuzuweisen:

from IPython.display import display display(tamswidget)

Bei Betrachtung des weiter oben im Diagramm gezeigten architektonischen Aufbaus müsste an dieser Stelle ein Float Slider entstehen, der seine Informationen aus dem Modell bezieht.

Zur Verifikation dieses Zusammenhangs klicken wir im Menü auf die Option Run All, und sehen daraufhin auch tatsächlich in der zweiten Zelle einen Slider, dessen Wert von Haus aus von 0 bis 100 läuft.

Um das Beispiel spielerisch zu halten, erzeugen wir daraufhin weiter unten noch eine weitere Zelle, die nun aber nur noch den Anzeigecode übergeben bekommt. Führen Sie danach alles über das Run-Symbol in der Toolbar aus, um sich am Aufscheinen eines weiteren Sliders zu erfreuen. Veränderungen des Werts des einen Sliders führen dann dazu, dass der Wert der anderen Instanz ebenfalls angepasst wird. Ein abermaliger Import der Klasse ist übrigens nicht erforderlich, weil unser Kernel – analog zu den weiter oben durchgeführten Experimenten mit Variablen – auch in Bezug auf die Importe zustandsbehaftet ist.

Die Data Binding Engine von Jupyter zeigt sich auch sonst sehr flexibel. In der Dokumentation findet sich beispielsweise folgendes Element, das eine Textbox und einen Slider über eine vom Entwickler manuell ins Leben gerufene Data-Binding-Beziehung miteinander verbindet:

a = widgets.FloatText() b = widgets.FloatSlider() display(a,b) mylink = widgets.jslink((a, 'value'), (b, 'value'))

Datenübertragung an Diagramme und Variablen

Die Grenzen des Data Bindings lassen sich durch eine Zelle anzeigen, die als Inhalt print(tamswidget.value) eingeschrieben bekommt. Wer den Inhalt der Zelle erstmals zur Ausführung freigibt, bekommt wie weiter oben bei der Ausgabe der Variable den aktuellen Wert des Modells angezeigt. Spätere Änderungen des Werts beeinflussen die Anzeige der print-Zelle allerdings nicht mehr, weil Jupyter nicht in der Lage ist, die Abhängigkeit der Werte voneinander zu erkennen.

Jupyter trifft hierbei auf ein Problem, das Nutzern von JavaScript Frameworks hinreichend bekannt vorkommen dürfte. So gut wie alle Programmiersprachen statten ihre grundlegenden Variablen im Interesse besserer Systemperformance nicht mit einem Notification Handler aus, weshalb zur Überwachung der Variablenwerte Handarbeit erforderlich ist. Im Fall von Jupyter Notebook müssen wir den primitiven Aufruf von print im ersten Schritt in eine Funktion verpacken, die über das Schlüsselwort def eingeleitet wird:

def on_change(v): print(v['new'])

Geben Sie den Code in die Zelle ein und klicken Sie danach auf das Run-Symbol. Wundern Sie sich nicht darüber, wenn das Notebook keine grafische Ausgabe präsentiert – die Funktion ist nun Teil des Zustands geworden und kann in der darauffolgenden Zelle nach dem folgenden Schema mit dem on_change-Ereignis Kontakt aufnehmen:

tamswidget.observe(on_change, names='value')

Klicken Sie auch in dieser Zelle auf Run, um eine Beziehung herzustellen. Ab diesem Moment können Sie nach Belieben die Widget-Werte verändern, was zum in Abbildung 11 gezeigten Verhalten führt. Beachten Sie allerdings, dass die Werteliste sehr schnell anwächst, und bald den Bildschirm füllt.

Abb. 11: Nach etwas Handarbeit funktioniert die Ausgabe der Slider-Werte automatisch

Darstellung von Funktionen

Das Python-Ökosystem hat mit der Matplotlib ein interessantes Alleinstellungsmerkmal – wer sich einmal in die Möglichkeiten der Diagrammerzeugung mit Matplotlib eingearbeitet hat, will das Produkt nicht mehr missen. Aus der Logik folgt, dass man in Jupyter Notebook ebenfalls Matplotlib-Diagramme erstellen kann. Als erstes und einfachstes Beispiel greifen wir uns eine neue leere Zelle, in der wir folgenden Code platzieren:

%matplotlib inline import matplotlib.pyplot as plt plt.style.use('seaborn-whitegrid') import numpy as np fig = plt.figure() ax = plt.axes() x = np.linspace(0, 10, 1000) ax.plot(x, np.sin(x));

Im Prinzip handelt es sich dabei um eine bekannte Kombination aus Matplotlib und NumPy. Neuartig ist in diesem Zusammenhang vor allem das Setzen des Umgebungsparameters, der die Matplotlib dazu anweist, statt dem hauseigenen Diagrammfenster auf die Ausgabemöglichkeiten der vorhandenen Runtime zurückzugreifen.

Wenn Sie eine mit Matplotlib ausgestattete Zelle ausführen, erscheint sofort eine leere Zelle darunter. Das Auftauchen des Diagramms, dass sich beim vorgegebenen Code wie Abbildung 12 präsentiert, nimmt derweil bis zu 5 Sekunden in Anspruch.

Abb. 12: Jupyter Notebooks arbeiten im Bereich der Diagramme synchron

plot nimmt dabei analog zum vollwertigen Python mehr oder weniger beliebige Funktionen entgegen. Ein dankbarer Kandidat für eine erste Visualisierung wäre eine lineare Funktion, die sich vom Ursprung entfernt. Hierzu müssen wir eine neue Zelle erstellen, deren plot-Methode folgenden Korpus aufweist:

%matplotlib inline import matplotlib.pyplot as plt . . . ax.plot(x, x+tamswidget.value)

Die Königslösung wäre nun natürlich, wenn sich das Diagramm dynamisch anpassen würde. In der Theorie könnten wir nach dem weiter oben besprochenen Schema vorgehen, und die Aufbaumethode in eine weitere Helferfunktion verpacken:

def toiler(v): plt.style.use('seaborn-whitegrid') import numpy as np fig = plt.figure() ax = plt.axes() x = np.linspace(0, 10, 1000) ax.plot(x, x+tamswidget.value)

Ein zusätzliches Element kümmert sich dann darum, zwischen der neuen Funktion und dem Widget eine Beziehung herzustellen:

display(tamswidget) tamswidget.observe(toiler, names='value')

Der abermalige Aufruf von display dient nur dazu, dass lokal, also in räumlicher Nähe der Zelle mit dem Diagramm, auch eine Instanz des Sliders zur Verfügung steht. Wer die beiden Zellen zur Ausführung bringt, hat allerdings ein unangenehmes Erlebnis. Da die Methode toiler bei jedem Aufruf eine komplett neue Instanz des Diagramms erzeugt, „läuft“ die Zelle bald über, was der Browser mit dem Einblenden einer Scroll Bar quittiert und der Übersichtlichkeit nicht sonderlich zuträglich ist.

Zur Behebung dieses Problems könnten Sie auf Funktionen der Matplotlib setzen oder fortgeschrittene Manipulationen an den Diagrammobjekten durchführen. Das ist aber eine Aufgabe, die wir hier nicht im notwendigen Umfang darstellen können.

Blick in die Zukunft

Jupyter Notebooks mögen eine geradezu faszinierende Möglichkeit zur Visualisierung naturwissenschaftlicher Prozesse darstellen. In der Praxis ist das Benutzerinterface allerdings etwas sperrig. Wer mit Visual Studio und Co. aufgewachsen ist, wünscht sich eine stärker gestreamlinte Arbeitsoberfläche. Das Jupyter-Entwicklerteam begegnet diesem Wunsch mit einem als Lab bezeichneten Produkt, das (bis zu einem gewissen Grad) der spirituelle Nachfolger der nach wie vor voll unterstützten Jupyter Notebooks ist.

Die mit Abstand wichtigste Neuerung ist dabei ein mehr oder weniger vollwertiger Editor, der die Arbeit mit den nach wie vor unterstützen Dateien erleichtert.

Zur Nutzung der neuartigen Benutzerschnittstelle müssen wir unseren Jupyter-Notebook-Server im ersten Schritt beenden. Das geht am bequemsten, indem Sie einfach das Kommandozeilenfenster schließen. Suchen Sie im Startmenü danach nach der Option Anaconda Prompt, um eine mit den Anaconda-Pfaden vorgeladene Kommandozeile zu laden. Das eigentliche Anwerfen von Jupyter Labs erfolgt dann durch Eingabe des folgenden Kommandos:

(base) C:\Users\tamha>Jupyter lab [I 22:44:54.857 LabApp] JupyterLab extension loaded from C:\Users\tamha\anaconda3\lib\site-packages\jupyterlab

Analog zum „klassischen“ Jupyter Notebook gibt der Starter auch hier einen URL aus. Platzieren Sie ihn in einem Browser Ihrer Wahl, um das in Abbildung 13 gezeigte Startfenster zu öffnen.

Abb. 13: Ähnlichkeiten zu vollwertigen IDEs sind rein zufällig

Das Anklicken einer der beiden weiter oben verwendeten Dateien lädt diese dann in einen Tab, was die Arbeit wesentlich erleichtert. Das Anklicken des Konsolenfensters öffnet stattdessen eine REPL-Konsole, die allerdings um die Funktionen von IPython erweitert ist.

Lohnt es sich?

Ein gut gemachtes Notebook hilft immens, wenn es darum geht, die von Algorithmen repräsentierten Zusammenhänge schnell grafisch begreifbar zu machen. Wer die implementierte Infrastruktur von Hand nachzuprogrammieren sucht, hat einige Monate zu tun – die Beschäftigung mit dem Produkt zahlt sich aus. Angemerkt sei noch, dass die extrem hohe Popularität dazu führt, dass der Buchmarkt mittlerweile gut sortiert ist.

 

Tam Hanna befasst sich seit der Zeit des Palm IIIc mit der Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht für Fragen, Trainings und Vorträge gern zur Verfügung.