Erklärbare Modelle schaffen Vertrauen

VS Code
1
Sep

Visualisieren von Machine-Learning-Daten in VS Code

Visualisierungen im Code haben in den letzten Jahren erfolgversprechende Innovationen hervorgebracht. Hierzu zählen die durchgehende Integration einiger Spezialtechnologien des Machine-Learning-Mappings wie die Einbindung des Jupyter-Notebook-Formats in VS Code, MS Power BI sowie das Aufrufen von Tensorboard zur Darstellung und Protokollierung der Trainingsergebnisse. Dieser Artikel verdeutlicht, wie weit die Codevisualisierung vorangeschritten ist und wie eigene Projekte davon profitieren.

Ein unmittelbarer Nutzen von Codevisualisierungen ist bereits heute klar: Bereiche wie die Robotik, Expertensysteme, mathematische Optimierung, Anomaliedetektion, Merkmalsreduktion oder modellbasierte Regelung wären besser erklärbar, wenn das Modell die gefundenen Merkmale zur Entscheidung direkt durch eine entsprechende Grafik aufzeigen könnte. Dazu dienen die in diesem Artikel beschriebenen Grundlagen.

Das Ziel ist es, so genau wie möglich zu verstehen, warum und wie eine KI bestimmte Entscheidungen trifft. Bei Bilderkennungsalgorithmen zeigt beispielsweise eine farbige Heatmap die Stellen eines Bildes, die besonders relevant für dessen Klassifizierung sind. Wir beginnen mit einem einfachen Datenset eines Klassifikationssystems und visualisieren die Entscheidung der Klassifikation mit einer Konfusionsmatrix und zugehöriger Heatmap. Als IDE nutzen wir Visual Studio Code mit den beiden Konfigurationsfiles tasks.json und dem projektspezifischen settings.json inklusive Test-Units und Pfadangaben (Listing 1 und 2).

{
  "python.pythonPath": 
"C:\\Users\\Max\\AppData\\Local\\Programs\\Python\\Python37\\python.exe",
  "python.testing.pytestArgs": [
    "freshonion"
  ],
  "python.testing.unittestEnabled": false,
  "python.testing.nosetestsEnabled": false,
  "python.testing.pytestEnabled": false,
  "python.testing.unittestArgs": [
    "-v",
    "-s",
    "./freshonion",
    "-p",
    "*test.py"
  ],
  "python.testing.promptToConfigure": false
}

 

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  // build from older win8.1. to win10.2 by max
  "version": "2.0.0",
  "tasks": [
    {
      "label": "buildpython",
      "type": "shell",
      "command": "C:\\Users\\Max\\AppData\\Local\\Programs\\Python\\Python37\\python.exe",
      "args": ["${file}"],
      "showOutput":"always",
      "problemMatcher": [],
      "group": {
        "kind": "build",
        "isDefault": true
      }
    }
  ]
}

Verschaffen Sie sich den Zugang zur .NET- und Microsoft-Welt mit unserem kostenlosen Newsletter!

 

Als Einstieg in VS Code mit Python empfiehlt sich das Tutorial, das Microsoft mit der aktuellen Version aus dem März 2020 (Version 1.44) publiziert hat: „Tutorials for creating Python containers and building Data Science models“. Wir starten mit den importierten Modulen in Listing 3 und nennen unser Skript logregclassifier2.py.

// get the modules we need
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report, confusion_matrix

 

Das Datenset

Die Daten unseres Beispiels sind bewusst neutral und einfach gehalten, um eine optimale Verständlichkeit zu gewährleisten. Als Trainingsdaten dient eine Datenreihe von 0 bis 9 (Samples), um ein Target mit 0 und 1 zu klassifizieren. Die Reihe könnte etwa zehn Patienten repräsentieren, die einen medizinischen Test absolvieren. Bei im Voraus bekannten Labels (Target) spricht man auch von Supervised Learning. Wir wollen nun das System soweit trainieren, dass die tiefen Zahlen wahrscheinlich mit 0 und die hohen Zahlen wahrscheinlich mit 1 zu klassifizieren sind (Listing 4).

X=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
y=[0, 0, 0, 0, 1, 1, 1, 1, 1, 1]

 

// arrays for the input (X) and output (y) values:
X = np.arange(10).reshape(-1, 1)
y = np.array([0, 0, 0, 0, 1, 1, 1, 1, 1, 1])

Wir können mit dem Befehl np.arange(10) ein Array erstellen, das alle ganzen Zahlen von 0 bis 9 enthält. Als Konvention betrachten wir x als zweidimensionales Array (Matrix) und y als eindimensionales Target (Vektor). Reshape (-1,1) bedeutet, dass wir nur ein Feature als Kolonne haben. Features sind die Merkmalsträger, die dem Modell helfen, unbekannte Muster zu finden. Anschließend definieren wir das Modell, das bereits mit der Fit-Methode die Daten trainiert, um eine Beziehung zwischen den Einflussvariablen (Determinanten) und dem Target herzustellen (Listing 5).

// Once you have input and output prepared, define your classification model.
model= LogisticRegression(solver='liblinear', random_state=0)
model.fit(X, y)
print(model)

Nachdem das Modell aufgebaut ist, können wir mit predict() eine erste Klassifikation mit einem Score versuchen und gleich die Konfusionsmatrix erstellen (Listing 6).

 

print(model.predict(X))
print(model.score(X, y))
// One false positive prediction: The fourth observation is a zero that was wrongly predicted as one.
print(confusion_matrix(y, model.predict(X)))

 

Real:    [0 0 0 0 1 1 1 1 1 1]
Predict: [0 0 0 1 1 1 1 1 1 1]
Score: 0.9
Confusion Matrix:
0: [[3 1]  :4
1:  [0 6]] :6

 

Und siehe da: Ein False Positive (Fehlalarm) hat sich eingeschlichen. Das Modell hat eine 0 irrtümlich als 1 klassifiziert, vergleichbar mit einem System, das fälschlicherweise eine ruhige Situation als Brandalarm bewertet. Die Konfusionsmatrix zeigt das als Fehlalarm an. Idealerweise zeigt die Matrix bei einem 100 Prozent Score folgendes Bild:

0: [[4 0]  :4
1:  [0 6]] :6

Das Datenset wird zum Bild

Im nächsten Schritt erfolgt die visuelle Aufbereitung der Matrix, um eine optische Beziehung zwischen den realen und den vorausgesagten Daten herzustellen (Listing 7).

 

plt.rcParams.update({'font.size': 16})
fig, ax = plt.subplots(figsize=(4, 4))
ax.imshow(cm)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('Predicted 0s', 'Predicted 1s'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('Actual 0s', 'Actual 1s'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
  for j in range(2):
    ax.text(j, i, cm[i, j], ha='center', va='center', color='red')
plt.show()

 

Abb. 1: Konfusionsmatrix mit Pyplot

 

Die Grafik in Abbildung 1 lässt sich auch einfacher und moderner mit einer zusätzlichen Bibliothek erzeugen. Wir benötigen dafür die Python Library seaborn (Abb. 2), die sich am besten über die integrierte Command Line Shell direkt in VS Code mit Pip Install installieren lässt (Listing 8).

 

import seaborn as sns
// get the instance of confusion_matrix:
cm = confusion_matrix(y, model.predict(X))
sns.heatmap(cm,  annot=True)
plt.title('heatmap confusion matrix')
plt.show()

 

Abb. 2: Konfusionsmatrix mit Seaborn

 

Die Klasse 0 hat also drei richtige Fälle (True Negatives), die Klasse 1 sechs richtige Fälle (True Positives) erkannt. Die Nutzergenauigkeit zeigt ebenfalls ein einziges falsches positives Ergebnis. Die Nutzergenauigkeit (Consumer Risk versus Producer Risk) wird auch als Überlassungsfehler oder Fehler vom Typ 1 bezeichnet. Fehler vom Typ 2 sind dann False Negatives.

Mit der Funktion .heatmap() aus der Bibliothek seaborn wird der Diagrammtyp definiert. Die folgenden Argumente parametrisieren das Aussehen des Diagramms. Werfen wir einen Blick in die Analyse des Fehlers, der durch den voreingestellten Schwellenwert der Wahrscheinlichkeit bei 0.5 definiert ist. Die Diskriminierung zwischen 0 und 1 hat zu früh stattgefunden, sodass unser Modell eine 0 zu früh als 1 klassifiziert hat. Natürlich lassen sich diese sogenannten Hyperparameter optimieren, um eine gerechtere Verteilung der Klassifikation zu finden. Dazu muss man sagen, dass die Wirkung auf diskrete, dichotome Variablen [0,1] sich nicht mit dem Verfahren der klassischen linearen Regressionsanalyse erklären und verifizieren lässt.

Hyperparameter

Die momentane Verteilung mit der zugehörigen Klassifikation sieht wie in Abb. 3 gezeigt aus.

 

Abb. 3: Die ersten drei Samples werden als 0, die restlichen als 1 gewertet

 

sns.set(style = 'whitegrid')
sns.regplot(X, model.predict_proba(X)[:,1], logistic=True,
                     scatter_kws={"color": "red"}, line_kws={"color": "blue"}) #label=model.predict(X))
plt.title('Logistic Probability Plot')
plt.show()

 

In Listing 9 ist in der Funktion regplot die geschätzte Wahrscheinlichkeit als Target enthalten. Nicht jeder Klassifizierer bietet die internen Wahrscheinlichkeiten an. Auch der Naive-Bayes-Klassifikator ist probabilistisch (Kasten: „Naive-Bayes-Klassifikator“). Die entsprechende Entscheidungsgrenze (Decision Boundary) ist für die Analyse optisch erkennbar und hilft, das Ergebnis zu interpretieren oder einen besseren Solver zu finden (Abb. 4).

 

Naive-Bayes-Klassifikator

Der Naive-Bayes-Klassifikator ist nach dem englischen Mathematiker Thomas Bayes benannt und leitet sich aus dem Bayes-Theorem ab. Die grundlegende Annahme besteht darin, von einer strengen Unabhängigkeit der verwendeten Merkmale auszugehen (deshalb „Naive“).

 

Abb. 4: Decision Boundary mit dem False Positive (blauer Punkt auf weißer Fläche)

 

     T precision    recall       f1-score    support      CM      
0 1.00 0.75 0.86 4 [[3 1
1 0.86 1.00 0.92 6 [0 6]]

Tabelle 1: Classification Report

Der Tabelle 1 können wir entnehmen, dass es einen Fall von Falsch-Positiv und keinen Fall von Falsch-Negativ gibt. Das bedeutet, dass nur in 86 Prozent aller Fälle ein positives Ergebnis auch einer Erkrankung entspricht. Die Precision errechnet sich dabei so:

Truepositiv / (Truepositiv + Falsepositiv) =
6 / (6+1) = 0.8571 = <strong>0.86</strong>

Es ist also entscheidend, in die Genauigkeit der Tests (Screening) auch die Falsch-Positiv-Fälle mit einzubinden. Ähnliche Beispiele zur bedingten Wahrscheinlichkeit befinden sich übrigens auf der Webseite „Lügen mit Statistik“. Auch hier habe ich einmal einen Fall nachgerechnet und visualisiert (aus dem Gebiet der Mammografie). Wie in Abbildung 5 gezeigt, sehen die Falsch-Positiv-Werte schon komplexer aus.

 

Abb. 5: Nichtlineare Analyse von Falsch-Positiv in einem Hyperplane (Support Vector Machine)

 

Optimieren der Optik

Nun wollen wir die erwähnten Hyperparameter ins Spiel bringen, von denen es einige gibt und die einen Teil der Modellevaluation ausmachen.

  • C ist eine positive Gleitkommazahl (standardmäßig 1,0), die die relative Stärke der Regularisierung definiert. Kleinere Werte zeigen eine stärkere Regularisierung an.
  • Solver ist eine Zeichenfolge (standardmäßig liblinear), die entscheidet, welchen Solver ich zum Anpassen des Modells verwende, und die Teil eines Kernels sein kann. Andere Optionen sind newton-cg, lbfgs, sag und saga.
  • max_iter ist eine Ganzzahl (standardmäßig 100), die die maximale Anzahl von Iterationen durch den Solver während der Modellanpassung definiert.

 

In Listing 10 erkennen wir die voreingestellten Modellparameter, die sich natürlich verändern lassen. Es ist allerdings nicht möglich, den besten Wert für einen Modellhyperparameter in Bezug auf ein bestimmtes Problem direkt zu ermitteln. Stattdessen kann man Erfahrungswerte nutzen, Werte kopieren, die bei anderen Problemen verwendet wurden, oder durch Ausprobieren nach dem besten Wert suchen. Ich benutze vor allem den Wert C (Regulator), um den Kernel oder den Solver zu optimieren.

model = LogisticRegression(solver='liblinear', C=1, random_state=0).fit(X, y) // show more model details
print(model)
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
  intercept_scaling=1, max_iter=100, multi_class='warn',
  n_jobs=None, penalty='l2', random_state=0, solver='liblinear',
  tol=0.0001, verbose=0, warm_start=False)

Die eigentliche Anpassung besteht darin, schlicht und ergreifend einen anderen Solver einzusetzen:

model = LogisticRegression(solver='<strong>lbfgs</strong>', C=1, random_state=0).fit(X, y)
  print(classification_report(y, model.predict(X)))

Die Ergebnisse sind nun optimal in Bezug auf die Güte des Algorithmus unserer Zahlenreihe und kann auch einem optischen Vergleich mit einem Decision Tree standhalten (Abb. 6).

 

Abb. 6: Optimale Entscheidung der Klassifikation

 

Das Entscheidungsbaumverfahren (Decision Tree) ist eine verbreitete Möglichkeit der Regression oder Klassifikation über ein multivariates Datenset. Das Verfahren kann beispielsweise verwendet werden, um die Zahlungsfähigkeit von Kunden zu klassifizieren oder eine Funktion zur Vorhersage von Falschmeldungen zu bilden. In der Praxis stellt das Verfahren den Data Scientist aber vor große Herausforderungen bezüglich Interpretation und Overfitting (Auswendiglernen der trainierten Beispiele), obwohl der Baum selbst eine transparente und lesbare Grafik bietet. Dazu nutzen wir in VS Code den installierten Graphviz 2.38 sowie eine zusätzliche Zeile im Code, die uns direkt die Pfadangaben im OS-Pfad setzt (Listing 11). Somit können wir direkt im Code Anpassungen an eine andere Version vornehmen oder die Plattform konfigurieren (Abb. 7).

<strong>from sklearn.tree import DecisionTreeClassifier</strong>
from converter import app, request
import unittest
import os
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import export_graphviz
os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
os.environ["PATH"] += os.pathsep + 'C:/Program Files/Pandoc/'

 

Abb. 7: Die Konfusionsmatrix hat keine Falschen mehr

 

Wichtig: Die Dimensionen der Konfusionsmatrix sind leider nicht normiert. Im Beispiel steht die Wahrheit „Real Actual“ in den Zeilen und die Schätzung „Predict“ in den Spalten. Je nach verwendeter Software können die Dimensionen aber vertauscht sein. Wichtig erscheint mir, die Matrix bei 0 zu beginnen, also True Negatives oben links zu normieren (Abb. 8). Für ein N-Klassen-Problem besteht die Konfusionsmatrix dann aus einer NxN-Matrix, ist also nicht auf eine binäre Klassifikation beschränkt.

 

Abb. 8: Eine normierte Konfusionsmatrix

 

Jupyter Notebook in VS Code

Zum Abschluss noch ein Ausblick in die Integration von Jupyter. Jupyter (ehemals IPython) Notebooks ist ein Open-Source-Projekt, mit dem sich interaktiver Markdown-Text und ausführbarer Python-Quellcode einfach auf einer Leinwand (Canvas) kombinieren lassen, die man als Notebook bezeichnet. Visual Studio Code unterstützt die native Arbeit mit Jupyter Notebooks sowie über Python-Codedateien. Auch meine Erfahrungen bezüglich Debugging oder Codemetriken sind gut. Um mit Jupyter Notebooks arbeiten zu können, ist eine Anaconda-Umgebung in VS Code oder eine andere Python-Umgebung erforderlich, jedoch ist vorgängig ein Jupyter-Paket zu installieren. Somit erhalten wir in VS Code direkt die Möglichkeit, Grafiken einzubinden, zu dokumentieren oder interaktiven Code auszuführen (Abb. 9, 10).

 

Abb. 9: Arbeiten mit Jupyter

 

Abb. 10: Mit dem Terminal lassen sich auch Images interaktiv im Code steuern

Ihr aktueller Zugang zur .NET- und Microsoft-Welt.
Der BASTA! Newsletter:

Behind the Tracks

.NET Framework & C#
Visual Studio, TFS, C# & mehr

Agile & DevOps
Best Practices & mehr

Web Development
Alle Wege führen ins Web

Data Access & Storage
Alles rund um´s Thema Data

HTML5 & JavaScript
Leichtegewichtig entwickeln

User Interface
Alles rund um UI- und UX-Aspekte

Microservices & APIs
Services, die sich über APIs via REST und JavaScript nutzen lassen

Security
Tools und Methoden für sicherere Applikationen

Cloud, Azure, Serverless
Cloud-basierte & Native Apps