Grafik von Milad Fakurian – https://unsplash.com/de/@fakurian

Klassische Optimierung mittels Optuna

Optimierung einer (komplexen) Funktion kann eine schwierige Aufgabe sein. Hier wird eine Bibliothek vorgestellt welche ich verwende und für eine gute Möglichkeit halte, entsprechende Aufgaben zu lösen und zusätzlich eine Implementierung einer vergleichsweise einfachen Optimierungsaufgabe als Nutzungsbeispiel.

Henrik Bartsch

Henrik Bartsch

Einordnung

Optimierungen sind in der modernen, digitalen Welt ein wichtiges Werkzeug geworden. In vielen Situationen sind Performanceerhöhungen wichtig - ob im privaten oder unternehmerischen Bereich. Vor allem die Anzahl der Parameter der immer komplexer werdenden Modellen macht eine händische oder analytische Optimierung immer komplexer. Entsprechend werde ich heute eine kurze Einführung in den Bereich der klassischen Optimierung in Python geben.

Grundlagen

Was ist das Ziel einer klassischen Optimierung?

Ziel einer Optimierung ist es, den Zielwert eines gegeben Modelles zu maximieren oder minimieren. Dies geschieht durch Variation verschiedener Parameter, welche im Modell definiert sind. Der Zielwert entsteht klassischerweise aus einer Zielfunktion, welche nicht unbedingt vorher bekannt sein muss. Anschließend können verschiedene Methoden verwendet werden, um sich langsam dem geforderten Optimum zu nähern. 1

Eine Liste verschiedener numerischer Verfahren, welche für eine solche Aufgabe verwendet werden können, können hier gefunden werden.

Abhängig von dem gegeben Problem, kann es sein das bestimmte Algorithmen keine Lösung finden können. Für verschiedene Probleme kann es sogar sein, dass keine Lösung - unabhängig von dem verwendeten Algorithmus - gefunden werden kann.

Implementierung eines Beispiels der klassischen Optimierung

Im Folgenden zeige ich nun ein Beispiel, wie eine Implementierung eines einfachen Beispiels aussehen könnte. Verwendet wird hierbei die Python-Library Optuna, welche eine Vereinfachung der Implementierung darstellt. Algorithmen zur Optimierung müssen hierbei nicht direkt selbst implementiert werden, sondern werden hierbei zur Verfügung gestellt. Eine effiziente Implementierung ist hierbei relevant, da hierdurch eine schnelle und effiziente Optimierung möglich gemacht wird. Diese effiziente Implementierung wird durch Optuna zur Verfügung gestellt.

Zusätzlich zeichnet sich Optuna durch die Kombatibilität mit allen aktuellen “Machine Learning”-Bibliotheken. Dies wird in einem anderen Artikel noch einmal betrachtet.

Einfaches Beispiel zum Einstieg

Zum Einstieg verwenden wir eine klassische Minimierungsaufgabe einer Funktion, in diesem Fall verwenden wir die Funktion

f:RR:x4x42x2+7.f: R \rightarrow R: x \mapsto 4x^4 - 2x^2 + 7.

Die Funktion kann wie folgt im Intervall x[4,4]x \in [-4, 4] visualisiert werden:

Analytisch ist das Minimum simpel zu berechnen:

dfdx(x)=16x34x=016x2=4x=±14=±12, \frac{df}{dx}(x) = 16x^3 - 4x = 0 \leftrightarrow 16x^2 = 4 \leftrightarrow x = \pm \sqrt{\frac{1}{4}} = \pm \frac{1}{2},

unter der Bedingung, dass x0.x \neq 0. Das Minimum liegt hierbei bei xm=12x_m = \frac{1}{2}, da dort d2fdx2(xm)>0\frac{d^2f}{dx^2}(x_m) > 0 gilt.

Die Implementierung läuft wie folgt:

import optuna

def value(x):
    return (4*(x**4) - 2*(x**2) + 7)

def objective(trial: optuna.trial.Trial):
    x = trial.suggest_float("x", -4, 4)
    return value(x)

study = optuna.create_study(study_name="Polynomial Example", direction="minimize")
max_trials = 250
study.optimize(objective, n_trials=max_trials)

Wichtig für Optuna ist die objective-Funktion im Code. Diese gibt einen skalaren Wert zurück, bei welchem bestimmt werden kann, ob dieser maximiert oder minimiert werden soll. Weiterhin wird dieser ein optuna.Trial.trial-Objekt übergeben, durch welches alle Hyperparameter des aktuellen Optimierungsschrittes angefordert werden können. Dies geschieht durch Funktionen wie trial.suggest_float, welche den Namen des Parameters als Argument nehmen, sowie die obere und untere Grenze des Parameters. Für weitere Funktionen zur Optimierung, siehe hier.

In dem Fall oben wird der Wert des Polynoms an der Stelle xx (welcher der Optimierungsparameter ist) zurückgegeben. Durch Ausführung des Codes beginnt Optuna sein Werk und minimiert den Funktionswert durch Anpassung unseres Parameters xx. Die Grenzen für den Parameter wurden als x[4,4]x \in [-4, 4] festgelegt. Nach Beendigung sollte ein Wert x0.5x \approx 0.5 herauskommen.

Für größere Projekte bietet es sich an dieser Stelle an, den besten Wert und die Werte der besten Parametervariationen auszulesen. Dies kann wie folgt geschehen:

trial = study.best_trial
print("Best value: ", trial.value)
for key, param in trial.params.items():
    print(f"{key}: {param}")

Eine Beispielausgabe sieht dann in dem oberen Beispiel so aus:

Best value:  6.750018072628766
x: 0.4978698668063428

In der nachfolgenden Grafik sind die entsprechenden Punkte angetragen, welche in der Optimierung verwendet wurden:

TL;DR

Optimierungsbibliotheken wie Optuna eignen sich hervoragend dazu, nicht nur Hyperparameteroptimierungen an neuronalen Netzen vorzunehmen, sondern auch klassischere, alltäglichere Optimierungsaufgaben durchzuführen. Hierfür sind keine großen Vorkenntnisse notwendig, sondern lediglich Daten des Optimierungsproblems notwendig. Durch seine Stabilität und Einfachheit in der Anwendung eignet sich in meinen Augen gerade Optuna sehr gut. Sollte Optuna aus einem bestimmten Grund nicht für die eigene Anwendung optimal sein, so kann auf eine Vielzahl anderer Optimierungsbibliotheken wie beispielsweise SciPy Optimize oder eine Reihe anderer Bibliotheken zurückgegriffen werden.

Viele Bibliotheken können hier verglichen werden.

Quellen

Footnotes

  1. wikipedia.org