Den gesam­ten soge­nann­ten Hyper­pa­ra­me­ter­raum nach einer opti­ma­len Para­me­ter­kon­fi­gu­ra­tion abzu­su­chen ist in der Regel nicht rea­li­sier­bar. Gewöhn­lich nut­zen Data Sci­en­tists hier­für ihre Erfah­rung und füh­ren nur einen klei­nen Grid­se­arch über einen Teil des Raums aus. Meis­tens ist diese Methode selbst für erfah­rene Data Sci­en­tists nicht sehr effi­zi­ent und führt oft zu einem fal­schen loka­len Mini­mum der Ver­lust­funk­tion. Bei­spiels­weise ist bei der Ein­stel­lung der Hyper­pa­ra­me­ter für ein neu­ro­na­les Netz in vie­len Fäl­len ein Ran­dom Search ziel­füh­ren­der als der manu­elle Gridsearch.

Hyper­opt ist eine Python Biblio­thek, wel­che die Suche nach den opti­ma­len Hyper­pa­ra­me­tern stark ver­ein­facht und gleich­zei­tig deut­lich effi­zi­en­ter gestal­tet als ein manu­el­ler Grid­se­arch. Ent­wi­ckelt wurde diese Biblio­thek von J. Berg­stra et al. wäh­rend eines Rese­arch-Pro­jekts, um opti­male Hyper­pa­ra­me­ter in einem über 200-dimen­sio­na­len Hyper­pa­ra­me­ter­raum für ein Con­vo­lu­tio­nal Neu­ral Net zu finden.

Neh­men wir fol­gen­des Beispiel:

Die Data Pipe­line steht und der Gra­di­ent Boos­ting Decis­ion Tree Clas­si­fier führt für den Busi­ness Use Case zu rela­tiv guten Ergeb­nis­sen. Doch waren die maxi­male Tiefe und die Anzahl der Fea­tures pro Decis­ion Tree opti­mal? Hätte man viel­leicht doch eine andere Lern­rate oder eine andere maxi­male Anzahl an Bäu­men wäh­len sol­len? All diese Fra­gen bezie­hen sich auf die Opti­mie­rung der Hyper­pa­ra­me­ter eines Machine Lear­ning-Modells. Dies sind Para­me­ter, wel­che nicht wäh­rend des Trai­nings­pro­zes­ses eines Modells fest­ge­legt wer­den, son­dern bereits zuvor fixiert wer­den müssen.

Wir emp­feh­len eine sys­te­ma­ti­sche Opti­mie­rung der Hyper­pa­ra­me­ter spä­tes­tens vor dem Deploy­ment des Modells durch­zu­füh­ren, um opti­male Ergeb­nisse für den Busi­ness Case zu erzie­len. Die ver­schie­de­nen Mög­lich­kei­ten für die Ein­stel­lun­gen der Hyper­pa­ra­me­ter schre­cken oft­mals uner­fah­rene Data Sci­en­tist davor ab, etwas kom­ple­xere Modelle zu ver­wen­den, wel­che ein bes­se­res Ver­ständ­nis aller Hyper­pa­ra­me­ter erfor­dern und mit Default Ein­stel­lun­gen nicht zu den gewünsch­ten Ergeb­nis­sen führen.

Die Biblio­thek Hyper­opt ist fle­xi­bel ein­setz­bar – eine Suche besteht im Wesent­li­chen aus drei ver­schie­de­nen Komponenten:

  • Defi­ni­tion einer Ver­lust­funk­tion, wie bei­spiels­weise Mean Least Square bei Regressionsproblemen,
  • Defi­ni­tion des Kon­fi­gu­ra­ti­ons­raums der Hyper­pa­ra­me­ter und
  • die Anwen­dung der fmin Funk­tion für die eigent­li­che Suche.
  1. Defi­ni­tion einer Ver­lust­funk­tion, wie bei­spiels­weise Mean Least Square bei Regressionsproblemen,
  2. Defi­ni­tion des Kon­fi­gu­ra­ti­ons­raums der Hyper­pa­ra­me­ter und
  3. die Anwen­dung der fmin Funk­tion für die eigent­li­che Suche.

In fol­gen­dem Code­bei­spiel ist ein ein­fa­ches Opti­mie­rungs­pro­blem mit Hyper­opt auf­ge­führt. Ziel ist es, für die Funk­tion f(x,y)=x² + y² die­je­ni­gen Werte für x und y zu fin­den, wel­che die Funk­tion f(x,y) mini­mie­ren. Die Funk­tion loss ent­spricht hier der Ver­lust­funk­tion und x und y kön­nen als Hyper­pa­ra­me­ter auf­ge­fasst werden.

Code Snip­pet für ein ein­fa­ches Opti­mie­rungs­pro­blem 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 zwei­ten Schritt defi­nie­ren wir den Kon­fi­gu­ra­ti­ons­raum für die Hyper­pa­ra­me­ter. Die­ser setzt sich aus ver­schie­de­nen Wahr­schein­lich­keits­ver­tei­lun­gen zusam­men. In die­sem Bei­spiel wer­den für die Eva­lua­tion der Ver­lust­funk­tion Stich­pro­ben für x und y aus einer Gleich­ver­tei­lung mit der unte­ren Grenze ‑100 und der obe­ren Grenze +100 gezo­gen. Mit Hyper­opt kann man zwi­schen ver­schie­de­nen kon­ti­nu­ier­li­chen und dis­kre­ten Ver­tei­lun­gen wäh­len. Die am meis­ten ver­wen­de­ten Ver­tei­lun­gen sind die Gleich­ver­tei­lung (hp.uniform), Log-Gleich­ver­tei­lung (hp.loguniform), Nor­mal­ver­tei­lung (hp.normal) und Log-Nor­mal­ver­tei­lung (hp.lognormal). Mit hp.choice sind auch dis­krete Ver­tei­lun­gen für Para­me­ter, wel­che nur wenige Ein­stel­lungs­mög­lich­kei­ten auf­wei­sen, mög­lich. Dies erlaubt es uns bei­spiels­weise auch “String”-Parameter, wie ver­schie­dene Distanz­me­tri­ken eines K‑Nearest Neigh­bor Klas­si­fi­ka­tors, dem Machine Lear­ning Modell zu übergeben.

Nach­dem der Kon­fi­gu­ra­ti­ons­raum defi­niert wurde, erstel­len wir ein Trial Objekt, das es uns ermög­licht, Infor­ma­tio­nen wie den Wert der Ver­lust­funk­tion, die Dauer der Eva­lua­tion oder auf­tre­tende Feh­ler­mel­dun­gen für jeden Trial zu spei­chern. Das ist vor allem bei kom­ple­xe­ren Ver­lust­funk­tio­nen für die spä­tere Ana­lyse des Opti­mie­rungs­pro­zes­ses prak­tisch. Um diese wei­te­ren Infor­ma­tio­nen zu spei­chern, muss die Ver­lust­funk­tion ein Python Dic­tion­ary über­ge­ben, wobei eines der Key-Value Paare den Ver­lust mit dem Key­word loss wider­spie­geln muss. Für Machine Lear­ning-Modelle besteht ein Trial aus dem Trai­ning des Modells und der Berech­nung von Eva­lua­ti­ons­me­tri­ken. In der Regel wird hier­für eine K‑Fold Cross Vali­da­tion genutzt.

Zum Schluss initi­ie­ren wir zwei Suchen durch die Funk­ti­ons­auf­rufe von fmin. Als Argu­mente über­ge­ben wir hier die Ver­lust­funk­tion, den Kon­fi­gu­ra­ti­ons­raum, den Such­al­go­rith­mus, die maxi­male Anzahl an Eva­lua­tio­nen bzw. Tri­als und das Trial Objekt. Es sind noch viele wei­tere optio­nale Ein­stel­lun­gen mög­lich wie bei­spiels­weise die maxi­male Eva­lua­ti­ons­zeit für einen Trial. In die­sem Bei­spiel unter­schei­den sich die bei­den Suchen bzgl. des ver­wen­de­ten Such­al­go­rith­mus. Im ers­ten Funk­ti­ons­auf­ruf nut­zen wir den Tree Struc­tu­red Par­zen Esti­ma­tor (TPE) Algo­rith­mus, wel­cher zur Klasse der Baye­sian Opti­miza­tion Algo­rith­men zählt. Diese Art von Algo­rith­men ver­wen­den mehr Zeit zwi­schen den Eva­lua­tio­nen der Ver­lust­funk­tio­nen als bei­spiels­weise die bekannte Gra­di­ent Des­cent Methode, um die Anzahl der zumeist kost­spie­li­gen Eva­lua­tio­nen (Trai­ning und Berech­nung der Metri­ken) zu redu­zie­ren. Als Ver­gleich füh­ren wir die glei­che Funk­tion mit einem Ran­dom Search (rand.suggest) aus.

Nach 500 Eva­lua­tio­nen sind wir mit dem TPE Algo­rith­mus deut­lich näher am Mini­mum (0,0) der Funk­tion ange­langt als mit einem Ran­dom Search. Abbil­dung 1 zeigt die Werte für x, wel­che für die Eva­lua­tion der Ver­lust­funk­tion ver­wen­det wur­den, auf­ge­tra­gen gegen den Wert der Ver­lust­funk­tion. Es ist deut­lich zu erken­nen, dass der TPE Algo­rith­mus (rechts) mehr Werte für x in der Nähe des Mini­mums ver­wen­det als der Ran­dom Search (links).

Hyperopt Bild1
Abbil­dung 1 Ver­wen­dete Werte für x mit­tels Ran­dom Search (links) und TPE Search (rechts)  auf­ge­tra­gen gegen die Verlustfunktion.

In Abbil­dung 2 sehen wir, dass sich mit dem TPE Algo­rith­mus (rechts) bereits nach weni­gen Tri­als eine Kon­ver­genz andeu­tet und die Loss Funk­tio­nen nur noch für Para­me­ter­werte eva­lu­iert wer­den, wel­che einen gerin­ge­ren Ver­lust aufweisen.

Hyperopt Bild2
Abbil­dung 2 Ver­lust­funk­tion in Abhän­gig­keit der Tri­als für eine Ran­dom Search (links) und für eine TPE Search (rechts).

Das beschrie­bene Bei­spiel lässt sich recht ein­fach auf Machine Lear­ning Pro­bleme über­tra­gen. Als rea­les Bei­spiel zei­gen wir Ihnen hier das Ergeb­nis einer Kaggle Com­pe­ti­tion. Hier­bei ging es darum vor­her­zu­sa­gen, ob eine Taxi­bu­chung ein­ge­hal­ten wird. Der Daten­satz beinhal­tete bei­spiels­weise Infor­ma­tio­nen über die Buchungs- und Abfahrts­zeit­punkte, den Abfahrts­ort und ob das Taxi online oder per Tele­fon vor­be­stellt wurde. Als Modell haben wir ein Balan­ced Bag­ging Modell mit einem Gra­di­ent Boos­ting Clas­si­fier ver­wen­det. Die Suche wurde über einen 6‑dimensionalen Hyper­pa­ra­me­ter­space durch­ge­führt. Wich­tige Para­me­ter sind hier zum Bei­spiel die Lern­rate des Modells und die maxi­male Tiefe der Ent­schei­dungs­bäume. Als Eva­lua­ti­ons­me­trik bzw. Score wurde die Flä­che (AUC) unter der Recei­ver Ope­ra­ting Cha­rac­te­ristic (ROC) Kurve ver­wen­det, wel­che den opti­ma­len Wert 1 hat. Um dies in ein Mini­mie­rungs­pro­blem umzu­schrei­ben, gibt die Ver­lust­funk­tion den Wert 1‑score.mean() zurück, wobei der score ein Array von Flä­chen ist, wel­ches von einer Cross Vali­da­tion zurück­ge­ge­ben wurde. Die genaue Imple­men­tie­rung der Ver­lust­funk­tion ist im nach­fol­gen­den Code­bei­spiel dargestellt.

Code Snip­pet einer Imple­men­tie­rung der Ver­lust­funk­tion für die Opti­mie­rung 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 weni­gen Eva­lua­tio­nen (O(100)) war eine Kon­ver­genz zu erken­nen. In Abbil­dung 3 sind die bei­den ROC Kur­ven für den Gra­di­ent Boos­ting Clas­si­fier mit Default-Ein­stel­lun­gen (grün) und opti­mier­ten Para­me­tern (blau) und die zuge­hö­ri­gen Scores (Flä­chen unter den Kur­ven) zu sehen. Ein Wert von 0,5 (Flä­che unter der schwar­zen gestri­chel­ten Linie) spie­gelt hier einen Schät­zer wider, der nicht bes­ser ist als der Zufall. Mit den opti­mier­ten Hyper­pa­ra­me­tern konnte ein Modell erstellt wer­den, wel­ches zum Zeit­punkt der Sub­mis­sion den 2. Platz der Kaggle Com­pe­ti­tion belegte. Ohne Opti­mie­rung beleg­ten wir mit die­sem Modell einen Platz im obe­ren Mittelfeld. 

Hyperopt Bild3
Abbil­dung 3 Recei­ver Ope­ra­ting Cha­rac­te­ristic für die Default Ein­stel­lun­gen (grün) und für die opti­mier­ten Para­me­ter (blau) des Gra­di­ent Boos­ted Classifiers.

In die­sem Blog­bei­trag haben wir gese­hen, dass Hyper­opt sehr fle­xi­bel ein­setz­bar und nicht nur für Machine Lear­ning-Pro­bleme anwend­bar ist. Diese Biblio­thek ist ein Paket zum Auf­fin­den von Para­me­tern um Funk­tio­nen, wel­cher Art auch immer, zu mini­mie­ren. Man kann bei­spiels­weise den Begriff Hyper­pa­ra­me­ter noch wei­ter fas­sen und sagen, dass ver­schie­dene Prepro­ces­sing Schritte wie das Fül­len von Null Values (Impu­ta­tion) und die Ska­lie­rung der Daten oder gar der Machine Lear­ning Algo­rith­mus selbst ein Hyper­pa­ra­me­ter ist. Die Suche über die­sen sehr abs­trak­ten Para­me­ter­raum wird durch die Erwei­te­rung hyper­opt-sklearn ver­ein­facht. Für die Opti­mie­rung von Con­vo­lu­tio­nal Neu­ral Nets gibt es die Erwei­te­rung hyper­opt-convnet. Diese Erwei­te­run­gen wer­den in einem wei­te­ren Blog­bei­trag behandelt.

Zum Schluss möch­ten wir noch auf einen wei­te­ren inter­es­san­ten Anwen­dungs­fall für Hyper­opt jen­seits von Machine Lear­ning hin­wei­sen: Fuzz Test­ing. Fuzz Test­ing hilft bei der Suche nach Bugs im eige­nen Source Code, der durch uner­war­te­ten User Input ver­ur­sacht wurde. Manu­ell ist es nahezu unmög­lich alle Eck­punkte des Kon­fi­gu­ra­ti­ons­raums für Input Para­me­ter zu tes­ten und man­che Bugs blei­ben daher unent­deckt. Bei der Suche mit Hyper­opt wird das Tes­ten der Soft­ware sys­te­ma­ti­scher gestal­tet und mög­li­che Bugs kön­nen gefun­den werden.