Den gesamten sogenannten Hyperparameterraum nach einer optimalen Parameterkonfiguration abzusuchen ist in der Regel nicht realisierbar. Gewöhnlich nutzen Data Scientists hierfür ihre Erfahrung und führen nur einen kleinen Gridsearch über einen Teil des Raums aus. Meistens ist diese Methode selbst für erfahrene Data Scientists nicht sehr effizient und führt oft zu einem falschen lokalen Minimum der Verlustfunktion. Beispielsweise ist bei der Einstellung der Hyperparameter für ein neuronales Netz in vielen Fällen ein Random Search zielführender als der manuelle Gridsearch.
Hyperopt ist eine Python Bibliothek, welche die Suche nach den optimalen Hyperparametern stark vereinfacht und gleichzeitig deutlich effizienter gestaltet als ein manueller Gridsearch. Entwickelt wurde diese Bibliothek von J. Bergstra et al. während eines Research-Projekts, um optimale Hyperparameter in einem über 200-dimensionalen Hyperparameterraum für ein Convolutional Neural Net zu finden.
Nehmen wir folgendes Beispiel:
Die Data Pipeline steht und der Gradient Boosting Decision Tree Classifier führt für den Business Use Case zu relativ guten Ergebnissen. Doch waren die maximale Tiefe und die Anzahl der Features pro Decision Tree optimal? Hätte man vielleicht doch eine andere Lernrate oder eine andere maximale Anzahl an Bäumen wählen sollen? All diese Fragen beziehen sich auf die Optimierung der Hyperparameter eines Machine Learning-Modells. Dies sind Parameter, welche nicht während des Trainingsprozesses eines Modells festgelegt werden, sondern bereits zuvor fixiert werden müssen.
Wir empfehlen eine systematische Optimierung der Hyperparameter spätestens vor dem Deployment des Modells durchzuführen, um optimale Ergebnisse für den Business Case zu erzielen. Die verschiedenen Möglichkeiten für die Einstellungen der Hyperparameter schrecken oftmals unerfahrene Data Scientist davor ab, etwas komplexere Modelle zu verwenden, welche ein besseres Verständnis aller Hyperparameter erfordern und mit Default Einstellungen nicht zu den gewünschten Ergebnissen führen.
Die Bibliothek Hyperopt ist flexibel einsetzbar – eine Suche besteht im Wesentlichen aus drei verschiedenen Komponenten:
- Definition einer Verlustfunktion, wie beispielsweise Mean Least Square bei Regressionsproblemen,
- Definition des Konfigurationsraums der Hyperparameter und
- die Anwendung der fmin Funktion für die eigentliche Suche.
- Definition einer Verlustfunktion, wie beispielsweise Mean Least Square bei Regressionsproblemen,
- Definition des Konfigurationsraums der Hyperparameter und
- die Anwendung der fmin Funktion für die eigentliche Suche.
In folgendem Codebeispiel ist ein einfaches Optimierungsproblem mit Hyperopt aufgeführt. Ziel ist es, für die Funktion f(x,y)=x² + y² diejenigen Werte für x und y zu finden, welche die Funktion f(x,y) minimieren. Die Funktion loss entspricht hier der Verlustfunktion und x und y können als Hyperparameter aufgefasst werden.
Code Snippet für ein einfaches Optimierungsproblem mit Hyperopt:
# import hyperopt
from hyperopt import hp, fmin, tpe, Trials, space_eval, rand
# loss function
def loss(params):
x = params['x']
y = params['y']
return x**2 + y**2
# search space
space = {}
space['x'] = hp.uniform('x',-100,100)
space['y'] = hp.uniform('y',-100,100)
# trial objects
trials_tpe = Trials()
trials_random = Trials()
# iypthon magic function
# %%time
fmin(loss, space, tpe.suggest, 500, trials=trials_tpe)
# Output:
# CPU times: user 1.71 s, sys: 0 ns, total: 1.71 s
# Wall time: 1.72 s
# {'x': 0.47358626968559747, 'y': -1.1612243423874569}
# iypthon magic function
# %%time
fmin(loss, space, rand.suggest, 500, trials=trials_random)
# Output
# CPU times: user 284 ms, sys: 8 ms, total: 292 ms
# Wall time: 278 ms
# {'x': -6.173861460812091, 'y': 2.4485079115627}
Im zweiten Schritt definieren wir den Konfigurationsraum für die Hyperparameter. Dieser setzt sich aus verschiedenen Wahrscheinlichkeitsverteilungen zusammen. In diesem Beispiel werden für die Evaluation der Verlustfunktion Stichproben für x und y aus einer Gleichverteilung mit der unteren Grenze ‑100 und der oberen Grenze +100 gezogen. Mit Hyperopt kann man zwischen verschiedenen kontinuierlichen und diskreten Verteilungen wählen. Die am meisten verwendeten Verteilungen sind die Gleichverteilung (hp.uniform), Log-Gleichverteilung (hp.loguniform), Normalverteilung (hp.normal) und Log-Normalverteilung (hp.lognormal). Mit hp.choice sind auch diskrete Verteilungen für Parameter, welche nur wenige Einstellungsmöglichkeiten aufweisen, möglich. Dies erlaubt es uns beispielsweise auch “String”-Parameter, wie verschiedene Distanzmetriken eines K‑Nearest Neighbor Klassifikators, dem Machine Learning Modell zu übergeben.
Nachdem der Konfigurationsraum definiert wurde, erstellen wir ein Trial Objekt, das es uns ermöglicht, Informationen wie den Wert der Verlustfunktion, die Dauer der Evaluation oder auftretende Fehlermeldungen für jeden Trial zu speichern. Das ist vor allem bei komplexeren Verlustfunktionen für die spätere Analyse des Optimierungsprozesses praktisch. Um diese weiteren Informationen zu speichern, muss die Verlustfunktion ein Python Dictionary übergeben, wobei eines der Key-Value Paare den Verlust mit dem Keyword loss widerspiegeln muss. Für Machine Learning-Modelle besteht ein Trial aus dem Training des Modells und der Berechnung von Evaluationsmetriken. In der Regel wird hierfür eine K‑Fold Cross Validation genutzt.
Zum Schluss initiieren wir zwei Suchen durch die Funktionsaufrufe von fmin. Als Argumente übergeben wir hier die Verlustfunktion, den Konfigurationsraum, den Suchalgorithmus, die maximale Anzahl an Evaluationen bzw. Trials und das Trial Objekt. Es sind noch viele weitere optionale Einstellungen möglich wie beispielsweise die maximale Evaluationszeit für einen Trial. In diesem Beispiel unterscheiden sich die beiden Suchen bzgl. des verwendeten Suchalgorithmus. Im ersten Funktionsaufruf nutzen wir den Tree Structured Parzen Estimator (TPE) Algorithmus, welcher zur Klasse der Bayesian Optimization Algorithmen zählt. Diese Art von Algorithmen verwenden mehr Zeit zwischen den Evaluationen der Verlustfunktionen als beispielsweise die bekannte Gradient Descent Methode, um die Anzahl der zumeist kostspieligen Evaluationen (Training und Berechnung der Metriken) zu reduzieren. Als Vergleich führen wir die gleiche Funktion mit einem Random Search (rand.suggest) aus.
Nach 500 Evaluationen sind wir mit dem TPE Algorithmus deutlich näher am Minimum (0,0) der Funktion angelangt als mit einem Random Search. Abbildung 1 zeigt die Werte für x, welche für die Evaluation der Verlustfunktion verwendet wurden, aufgetragen gegen den Wert der Verlustfunktion. Es ist deutlich zu erkennen, dass der TPE Algorithmus (rechts) mehr Werte für x in der Nähe des Minimums verwendet als der Random Search (links).
In Abbildung 2 sehen wir, dass sich mit dem TPE Algorithmus (rechts) bereits nach wenigen Trials eine Konvergenz andeutet und die Loss Funktionen nur noch für Parameterwerte evaluiert werden, welche einen geringeren Verlust aufweisen.
Das beschriebene Beispiel lässt sich recht einfach auf Machine Learning Probleme übertragen. Als reales Beispiel zeigen wir Ihnen hier das Ergebnis einer Kaggle Competition. Hierbei ging es darum vorherzusagen, ob eine Taxibuchung eingehalten wird. Der Datensatz beinhaltete beispielsweise Informationen über die Buchungs- und Abfahrtszeitpunkte, den Abfahrtsort und ob das Taxi online oder per Telefon vorbestellt wurde. Als Modell haben wir ein Balanced Bagging Modell mit einem Gradient Boosting Classifier verwendet. Die Suche wurde über einen 6‑dimensionalen Hyperparameterspace durchgeführt. Wichtige Parameter sind hier zum Beispiel die Lernrate des Modells und die maximale Tiefe der Entscheidungsbäume. Als Evaluationsmetrik bzw. Score wurde die Fläche (AUC) unter der Receiver Operating Characteristic (ROC) Kurve verwendet, welche den optimalen Wert 1 hat. Um dies in ein Minimierungsproblem umzuschreiben, gibt die Verlustfunktion den Wert 1‑score.mean() zurück, wobei der score ein Array von Flächen ist, welches von einer Cross Validation zurückgegeben wurde. Die genaue Implementierung der Verlustfunktion ist im nachfolgenden Codebeispiel dargestellt.
Code Snippet einer Implementierung der Verlustfunktion für die Optimierung eines Klassifizierungsmodells:
def loss(self, params):
self.model.set_params(**params)
shuffle = KFold(n_splits=3, shuffle=True)
score = cross_val_score(self.model, self.X, self.y,
cv=shuffle, scoring='roc_auc',
n_jobs=1, verbose=1)
return 1-score.mean()
Bereits nach wenigen Evaluationen (O(100)) war eine Konvergenz zu erkennen. In Abbildung 3 sind die beiden ROC Kurven für den Gradient Boosting Classifier mit Default-Einstellungen (grün) und optimierten Parametern (blau) und die zugehörigen Scores (Flächen unter den Kurven) zu sehen. Ein Wert von 0,5 (Fläche unter der schwarzen gestrichelten Linie) spiegelt hier einen Schätzer wider, der nicht besser ist als der Zufall. Mit den optimierten Hyperparametern konnte ein Modell erstellt werden, welches zum Zeitpunkt der Submission den 2. Platz der Kaggle Competition belegte. Ohne Optimierung belegten wir mit diesem Modell einen Platz im oberen Mittelfeld.
In diesem Blogbeitrag haben wir gesehen, dass Hyperopt sehr flexibel einsetzbar und nicht nur für Machine Learning-Probleme anwendbar ist. Diese Bibliothek ist ein Paket zum Auffinden von Parametern um Funktionen, welcher Art auch immer, zu minimieren. Man kann beispielsweise den Begriff Hyperparameter noch weiter fassen und sagen, dass verschiedene Preprocessing Schritte wie das Füllen von Null Values (Imputation) und die Skalierung der Daten oder gar der Machine Learning Algorithmus selbst ein Hyperparameter ist. Die Suche über diesen sehr abstrakten Parameterraum wird durch die Erweiterung hyperopt-sklearn vereinfacht. Für die Optimierung von Convolutional Neural Nets gibt es die Erweiterung hyperopt-convnet. Diese Erweiterungen werden in einem weiteren Blogbeitrag behandelt.
Zum Schluss möchten wir noch auf einen weiteren interessanten Anwendungsfall für Hyperopt jenseits von Machine Learning hinweisen: Fuzz Testing. Fuzz Testing hilft bei der Suche nach Bugs im eigenen Source Code, der durch unerwarteten User Input verursacht wurde. Manuell ist es nahezu unmöglich alle Eckpunkte des Konfigurationsraums für Input Parameter zu testen und manche Bugs bleiben daher unentdeckt. Bei der Suche mit Hyperopt wird das Testen der Software systematischer gestaltet und mögliche Bugs können gefunden werden.