HyperOpt gedemystificeerd

Modelafstemming automatiseren met HyperOpt

Ben je dol op het afstemmen van modellen? Als je antwoord "ja" is, is dit bericht niet voor jou

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Een cartoon van mijn opa - van de.

In deze blog bespreken we het extreem populaire geautomatiseerde algoritme voor het afstemmen van hyperparameters genaamd Op bomen gebaseerde Parzen-schatters (TPE). TPE wordt ondersteund door het open-sourcepakket HyperOpt. Door gebruik te maken van HyperOpt en TPE kunnen machine learning-engineers: snel zeer geoptimaliseerde modellen ontwikkelen zonder handmatige afstemming.

Zonder verder oponthoud, laten we erin duiken!

HyperOpt is een open-source python-pakket dat een algoritme genaamd Tree-based Parzen Esimtors (TPE) gebruikt om modelhyperparameters te selecteren die een door de gebruiker gedefinieerde doelfunctie optimaliseren. Door simpelweg de functionele vorm en grenzen van elke hyperparameter te definiëren, doorzoekt TPE grondig maar efficiënt de complexe hyperruimte om de optimale waarden te bereiken.

TPE is een sequentieel algoritme dat gebruikmaakt van bayesiaanse updates en de onderstaande volgorde volgt.

  1. Train een model met verschillende sets willekeurig geselecteerde hyperparameters, waarbij objectieve functiewaarden worden geretourneerd.
  2. Splits onze waargenomen objectieve functiewaarden in "goede" en "slechte" groepen, volgens een drempelgamma (γ).
  3. Bereken de 'veelbelovend'-score, die gewoon is P(x|goed) / P(x|slecht).
  4. Bepaal de hyperparameters die de belofte maximaliseren via mengselmodellen.
  5. Pas ons model aan met behulp van de hyperparameters uit stap 4.
  6. Herhaal stap 2-5 tot een stopcriterium.

Hier is een voorbeeld van snelle code.

Ok dat waren veel grote woorden. Laten we het rustiger aan doen en echt begrijpen wat er aan de hand is.

1.1 — Ons doel

Datawetenschappers hebben het druk. We willen echt goede modellen maken, maar dan wel op een efficiënte en idealiter hands-off manier.

Bepaalde stappen in de levenscyclus van ML-modellering zijn echter zeer moeilijk te automatiseren. Verkennende data-analyse (EDA) en feature engineering zijn bijvoorbeeld meestal onderwerpspecifiek en vereisen menselijke intuïtie. Aan de andere kant is het afstemmen van modellen een iteratief proces waarin computers kunnen uitblinken.

Ons doel in dit bericht is om te begrijpen hoe algoritmen kunnen worden gebruikt om het modelafstemmingsproces te automatiseren.

Om ons te helpen over dat doel na te denken, gebruiken we een analogie: we zijn piraten die op zoek zijn naar begraven schatten. Het is ook belangrijk op te merken dat we zeer efficiënte piraten zijn die onze tijd bij het zoeken naar de begraven schat willen minimaliseren. Dus, hoe moeten we de tijd die we besteden aan zoeken minimaliseren? Het antwoord is gebruik een kaart!

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 1: voorbeeld 3D hyperparameter zoekruimte. De locatie van de schatkist is een globaal optimum. Afbeelding door auteur.

In figuur 1 hebben we een fictieve kaart die laat zien waar onze schat zich bevindt. Na veel klimmen en graven zou het niet al te moeilijk zijn om die schat te bereiken, omdat we precies weten waar hij zich bevindt.

Maar wat gebeurt er als we geen kaart hebben?

Wanneer we worden belast met het afstemmen van een model, krijgen we helaas geen kaart. Ons terrein, dat overeenkomt met de hyperparmeter-zoekruimte, is onbekend. Bovendien is de locatie van onze schat, die overeenkomt met de optimale set hyperparameters, ook onbekend.

Laten we met die opstelling praten over enkele mogelijke manieren om deze ruimte efficiënt te verkennen en een schat te vinden!

1.2 — Mogelijke oplossingen

De originele methode voor het afstemmen van modellen is "handmatig" - de ingenieur zal in feite veel verschillende configuraties handmatig testen en zien welke hyperparametercombinatie het beste model produceert. Hoewel informatief, is dit proces inefficiënt. Er moet een betere manier zijn…

1.2.1 — Raster zoeken (slechtste)

Ons eerste optimalisatie-algoritme is zoeken naar rasters. Grid search test iteratief alle mogelijke combinaties van hyperparameters binnen een door de gebruiker gespecificeerd grid.

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 2: voorbeeld van rasterzoeklay-out. Afbeelding door auteur

In figuur 2 bijvoorbeeld, waar u een rode stip ziet, zullen we ons model opnieuw trainen en evalueren. Dit raamwerk is inefficiënt omdat het hergebruikt slechte hyperparameters. Als hyperparameter 2 bijvoorbeeld weinig invloed heeft op onze doelfunctie, zullen we toch alle combinaties van zijn waarden testen, waardoor het vereiste aantal iteraties met 10x wordt verhoogd (in dit voorbeeld).

Maar voordat u verder gaat, is het belangrijk op te merken dat het zoeken naar rasters nog steeds redelijk populair is, omdat het gegarandeerd een optimum vindt bij een correct gespecificeerd raster. Als u besluit de methode te gebruiken, zorg ervoor dat u uw raster transformeert om de functionele vorm van uw hyperparameters weer te geven. Bijvoorbeeld max_depth voor a willekeurige bosclassificatie is een geheel getal — laat het niet zoeken over een doorlopende spatie. Het is ook onwaarschijnlijk dat het een uniforme verdeling heeft - als u de functionele vorm van uw hyperparameter kent, transformeert u het raster om het weer te geven.

Samengevat, raster zoeken is onderhevig aan de vloek van dimensionaliteit en herberekent informatie tussen evaluaties, maar wordt nog steeds veel gebruikt.

1.2.2 — Willekeurig zoeken (goed)

Ons tweede algoritme is willekeurig zoeken. Willekeurig zoeken probeert willekeurige waarden binnen een door de gebruiker opgegeven raster. In tegenstelling tot het zoeken naar rasters, zijn we niet verplicht om elke mogelijke combinatie van hyperparameters te testen, wat de efficiëntie verhoogt.

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 3: voorbeeld van willekeurig zoeken. Afbeelding door auteur.

Hier is een cool feit: willekeurig zoeken vindt (gemiddeld) een top 5% hyperparameterconfiguratie binnen 60 herhalingen. Dat gezegd hebbende, moet u, net als bij het zoeken in een raster, uw zoekruimte transformeren om de functionele vorm van elke hyperparam weer te geven.

Willekeurig zoeken is een goede basis voor hyperparameteroptimalisatie.

1.2.3 — Bayesiaanse optimalisatie (beter)

Onze derde kandidaat is ons eerste Sequential Model-Based Optimization (SMBO) algoritme. Het belangrijkste conceptuele verschil met de eerdere technieken is dat we: iteratief eerdere runs gebruiken om toekomstige verkenningspunten te bepalen.

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 4: voorbeeld van bayesiaanse optimalisatie — src. Afbeelding door auteur.

Bayesiaanse hyperparameter-optimalisatie probeert een probabilistische verdeling van onze hyperparameter-zoekruimte te ontwikkelen. Van daaruit gebruikt het een acquisitiefunctie, zoals verwachte verwachte verbetering, om onze hyperspace te transformeren om meer "doorzoekbaar" te zijn. Ten slotte gebruikt het een optimalisatie-algoritme, zoals stochastische gradiëntafdaling, om de hyperparameters te vinden die onze acquisitiefunctie maximaliseren. Die hyperparameters worden gebruikt om in ons model te passen en het proces wordt herhaald tot convergentie.

Bayesiaanse optimalisatie presteert doorgaans beter dan willekeurig zoeken, maar heeft enkele kernbeperkingen, zoals het vereisen van numerieke hyperparameters.

1.2.4 - Boomgebaseerde Parzen-schatters (beste)

Laten we het tenslotte hebben over de ster van de show: Tree-Based Parzen Estimators (TPE). TPE is een ander SMBO-algoritme dat doorgaans beter presteert dan basale Bayesiaanse optimalisatie, maar het belangrijkste verkoopargument is dat het complexe hyperparameterrelaties via een boomstructuur afhandelt.

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 5: voorbeeld van hiërarchische structuur voor TPE — src. Afbeelding door auteur.

Laten we figuur 5 gebruiken om dit te begrijpen boomstructuur. Hier trainen we een Support Vector Machine (SVM) classifier. We zullen twee kernels testen: linear en RBF. Een linear kernel neemt geen parameter width maar RBF doet, dus door een genest woordenboek te gebruiken, kunnen we deze structuur coderen en daardoor de zoekruimte beperken.

TPE ondersteunt ook categorische variabelen die traditionele Bayesiaanse optimalisatie niet biedt.

Korte disclaimer voordat je verder gaat, er zijn veel andere afstemmingspakketten voor hyperparameters. Elk ondersteunt een verscheidenheid aan algoritmen, waarvan sommige willekeurig bos, gaussiaanse processen en genetische algoritmen bevatten. TPE is een zeer populair en algemeen algoritme, maar is niet noodzakelijk het beste.

Over het algemeen is TPE een zeer robuuste en efficiënte oplossing voor hyperparameteroptimalisatie.

Nu we een algemeen begrip hebben van enkele populaire algoritmen voor het optimaliseren van hyperparameters, gaan we dieper in op hoe TPE werkt.

Terugkerend naar onze analogie, we zijn piraten die op zoek zijn naar een begraven schat maar heb geen kaart. Onze kapitein heeft de schat zo snel mogelijk nodig, dus we moeten graven op strategische locaties met een grote kans op schatten, met behulp van eerdere opgravingen om de locatie van toekomstige opgravingen te bepalen.

2.1 — Initialisatie

Om te beginnen, wij de beperkingen van onze ruimte definiëren. Zoals hierboven opgemerkt, hebben onze hyperparameters vaak een functionele vorm, max/min-waarden en een hiërarchische relatie met andere hyperparameters. Met behulp van onze kennis over onze ML-algoritmen en onze gegevens, kunnen we onze zoekruimte definiëren.

Vervolgens moeten we definieer onze objectieve functie, die wordt gebruikt om te evalueren hoe "goed" onze hyperparametercombinatie is. Enkele voorbeelden zijn klassieke ML-verliesfuncties, zoals RMSE of AUC.

Super goed! Nu we een beperkte zoekruimte hebben en een manier om succes te meten, zijn we klaar om te beginnen met zoeken...

2.2 — Iteratieve Bayesiaanse optimalisatie

Bayesiaanse optimalisatie is een sequentieel algoritme dat volgens een objectieve functie punten in hyperspace vindt met een grote kans om "succesvol" te zijn. TPE maakt gebruik van bayesiaanse optimalisatie, maar gebruikt enkele slimme trucs om de prestaties te verbeteren en de complexiteit van de zoekruimte aan te pakken...

2.2.0 — De conceptuele opzet

De eerste truc is modelleren P(x|y) in plaats van P(y|x)…

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 6: voorwaardelijke kans dat TPE lijkt op te lossen. Afbeelding door auteur.

Bayesiaanse optimalisatie lijkt meestal te modelleren P(y|x), dat is de kans op een objectieve functiewaarde (y), gegeven hyperparameters (x). TPE doet het tegenovergestelde - het lijkt te modelleren P(x|y), wat de waarschijnlijkheid is van de hyperparameters (x), gegeven de objectieve functiewaarde (y).

Kortom, TPE probeert de beste objectieve functiewaarden te vinden en vervolgens de bijbehorende hyperparameters te bepalen.

Laten we met die zeer belangrijke setup ingaan op het eigenlijke algoritme.

2.2.1 — Splits onze gegevens op in "goede" en "slechte" groepen

Onthoud dat ons doel is om de beste hyperparameterwaarden te vinden volgens een objectieve functie. Dus, hoe kunnen we gebruikmaken van P(x|y) om dat te doen?

Ten eerste splitst TPE onze waargenomen gegevenspunten in twee groepen: goed, aangegeven g(x), en slecht, aangegeven ik(x). De grens tussen goed en slecht wordt bepaald door een door de gebruiker gedefinieerde parameter gamma (γ), die overeenkomt met het objectieve functiepercentiel dat onze waarnemingen splitst (y*).

Dus, met γ = 0.5, onze objectieve functiewaarde die onze waarnemingen splitst (y*) zal de mediaan zijn van onze waargenomen punten.

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 7: verdeling van p(x|y) in twee sets. Afbeelding door auteur.

Zoals weergegeven in figuur 7, kunnen we formaliseren: p(x|y) met behulp van het bovenstaande kader. En om met de piratenanalogie te rollen ...

Piratenperspectief: kijkend naar de plaatsen die we al hebben verkend, geeft l(x) een lijst van plaatsen met heel weinig schatten en g(x) een lijst van plaatsen met veel schatten.

2.2.32— Bereken de “Belovend”-score

Ten tweede definieert TPE hoe we een niet-geobserveerde hyperparametercombinatie moeten evalueren via de "veelbelovend" score.

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 8: definitie van veelbelovendheidsscore. Afbeelding door auteur.

Figuur 8 definieert onze veelbelovendheidsscore (P), wat slechts een verhouding is met de volgende componenten...

  • Teller: de kans op het waarnemen van een reeks hyperparameters (x), gegeven de corresponderende objectieve functiewaarde is “goed. '
  • Noemer: de kans op het waarnemen van een reeks hyperparameters (x), gegeven de corresponderende objectieve functiewaarde is “slecht. '

Hoe groter de waarde 'veelbelovend', hoe groter de kans dat onze hyperparameters x zal een "goede" objectieve functie opleveren.

Piratenperspectief: veelbelovendheid laat zien hoe waarschijnlijk het is dat een bepaalde locatie in ons terrein veel schatten zal hebben.

Snel terzijde voordat u verder gaat, als u bekend bent met Bayesiaanse optimalisatie, deze vergelijking fungeert als een acquisitiefunctie en is evenredig met de Verwachte verbetering (EI).

2.2.3— Maak een schatting van de waarschijnlijkheidsdichtheid

Ten derde probeert TPE de score 'veelbelovend' te evalueren via: mengsel modellen. Het idee van mengselmodellen is om meerdere kansverdelingen te nemen en ze samen te voegen met behulp van een lineaire combinatie - src. Deze gecombineerde kansverdelingen worden vervolgens gebruikt om schattingen van de kansdichtheid te ontwikkelen.

Over het algemeen is het mengselmodelleringsproces ...

  1. Definieer het distributietype van onze punten. In ons geval, als onze variabele categorisch is, gebruiken we een opnieuw gewogen categorische verdeling en als het numeriek is, gebruiken we een gaussiaanse (dwz normale) of uniforme verdeling.
  2. Herhaal elk punt en voeg op dat punt een verdeling in.
  3. Tel de massa van alle verdelingen op om een ​​schatting van de kansdichtheid te krijgen.

Merk op dat dit proces voor beide sets afzonderlijk wordt uitgevoerd ik(x) en g(x).

Laten we een voorbeeld in figuur 9 doornemen…

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 9: voorbeeld van afgeknotte Gauss-verdelingen die passen bij 3 hyperparameterobservaties. Afbeelding door auteur.

Voor elke waarneming (blauwe stippen op de x-as), creëren we een normale verdeling ~N(μ, σ), waarbij...

  • μ (mu) is het gemiddelde van onze normale verdeling. Zijn waarde is de locatie van ons punt langs de x-as.
  • (sigma) is de standaarddeviatie van onze normale verdeling. De waarde is de afstand tot het dichtstbijzijnde naburige punt.

Als punten dicht bij elkaar liggen, zal de standaarddeviatie klein zijn en daardoor zal de verdeling erg groot zijn en omgekeerd, als punten uit elkaar liggen, zal de verdeling vlak zijn (figuur 10)…

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 10: voorbeeld van de impact van standaarddeviatie op de vorm van een normale verdeling. Afbeelding door auteur.

Piratenperspectief: NA — piraten zijn niet geweldig met mengmodellen.

Nog een korte kanttekening voordat je verder gaat: als je de literatuur leest, zul je merken dat TPE "afgekapte" gaussianen gebruikt, wat simpelweg betekent dat de gaussianen worden begrensd door het bereik dat we specificeren in onze hyperparameterconfiguratie in plaats van zich uit te breiden tot +/- oneindig .

2.2.4 — Het volgende te verkennen punt bepalen!

Laten we deze stukken samenbrengen. Tot dusver hebben we 1) objectieve functieobservaties verkregen, 2) onze formule voor 'veelbelovend' gedefinieerd en 3) een schatting van de waarschijnlijkheidsdichtheid gemaakt via mengselmodellen op basis van eerdere waarden. We hebben alle stukjes om een ​​bepaald punt te evalueren!

Onze eerste stap is het maken van een gemiddelde kansdichtheidsfunctie (PDF) voor beide g(x) en ik(x).

Hyperparameter hyper parameter tuning model tuning machine learning data science sklearn model mllib spark hyperopt tree parzen schatter tpe op boom gebaseerde parzen esimtator mlflow databricks
Figuur 11: overlay van de gemiddelde kansdichtheid gegeven 3 waargenomen punten. Afbeelding door auteur.

Een voorbeeldproces wordt getoond in figuur 11 — de rode lijn is onze gemiddelde PDF en is gewoon de som van alle PDF's gedeeld door het aantal PDF's.

Met behulp van de gemiddelde PDF kunnen we de waarschijnlijkheid van elke hyperparameterwaarde (x) binnen zijn g(x) or ik(x).

Laten we bijvoorbeeld zeggen dat de waargenomen waarden in figuur 11 behoren tot de "goede" verzameling, g(x). Op basis van onze gemiddelde PDF is het onwaarschijnlijk dat een hyperparameterwaarde van 3.9 of 0.05 tot de "goede" set behoort. Omgekeerd lijkt een hyperparameterwaarde van ~1.2 zeer waarschijnlijk tot de "goede" set te behoren.

Dit is nu nog maar de helft van de foto. We passen dezelfde methode toe voor de "slechte" set, ik(x). Omdat we willen maximaliseren g(x) / l(x), veelbelovende punten moeten zich daar bevinden: g(x) is hoog en ik(x) is laag.

Best cool, toch?

Met deze kansverdelingen kunnen we steekproeven nemen uit onze boomgestructureerde hyperparameters en de reeks hyperparameters vinden die 'veelbelovend' maximaliseren en daardoor het onderzoeken waard zijn.

Piratenperspectief: de volgende locatie die we graven is de locatie die de (kans op het hebben van veel schatten) / (kans op het hebben van weinig schatten) maximaliseert.

Nu je weet hoe het werkt, volgen hier enkele praktische tips voor het implementeren van TPE via het open source pakket HyperOpt.

3.1 — Structuur van een HyperOpt-app

Over het algemeen zijn er drie hoofdstappen bij het gebruik van HyperOpt...

  1. Definieer de zoekruimte, dat zijn alleen de bereiken en functionele vormen van de hyperparameters die u wilt optimaliseren.
  2. Definieer de pasfunctie, die je noemt model.fit() functie op een bepaalde trein/testsplitsing.
  3. Definieer de doelfunctie, wat een van de klassieke nauwkeurigheidsstatistieken is, zoals RMSE of AUC.

Helaas vereisen deze geautomatiseerde afstemmingsmethoden nog steeds ontwerpinput van de datawetenschapper - het is geen volledig gratis lunch. Anekdotisch is TPE echter behoorlijk robuust tegen onjuiste specificatie van hyperparameters (binnen redelijke grenzen).

3.2— Tips en trucs

  • HyperOpt is parallelleerbaar via beide Apache Spark en MongoDB. Als u met meerdere kernen werkt, of dit nu in de cloud of op uw lokale computer is, kan dit de runtime aanzienlijk verkorten.
  • Als u het afstemmingsproces via Apache Spark parallelliseert, gebruikt u a SparkTrialsobject voor ML-modellen met één knooppunt (sklearn) en a Trails object voor parallelle ML-modellen (MLlib). Code staat hieronder.
  • MLstroom is een open source-methode voor het volgen van uw modelruns. Het integreert eenvoudig met HyperOpt.
  • Beperk de zoekruimte niet te vroeg. Sommige combinaties van hyperparameters kunnen verrassend effectief zijn.
  • Het definiëren van de zoekruimte kan lastig zijn, vooral als u de functionele vorm van uw hyperparameters. Echter, uit persoonlijke ervaring is TPE behoorlijk robuust tegen verkeerde specificatie van die functionele vormen.
  • Het kiezen van een goede objectieve functie gaat een lange weg. In de meeste gevallen wordt de fout niet gelijk gemaakt. Als een bepaald type fout problematischer is, zorg er dan voor dat u die logica in uw functie inbouwt.

3.3— Een codevoorbeeld

Hier is wat code om de HyperOpt op een gedistribueerde manier uit te voeren. Het is aangepast van de code in het boek, Machine learning-engineering in actie - hier is de git-opslagplaats.

Enkele leuke functies van dit fragment zijn parallellisatie via Apache Spark en modelregistratie via MLstroom. Merk ook op dat dit fragment een sklearn RandomForestRegressor optimaliseert - u zult het model en de aanpassingsfunctie moeten wijzigen om aan uw behoeften te voldoen.

En daar heb je het - HyperOpt in al zijn glorie!

Om de belangrijkste punten te hopen, laten we het snel samenvatten.

Het afstemmen van hyperparameters is een noodzakelijk onderdeel van de levenscyclus van het ML-model, maar kost veel tijd. Sequential Model-Based Optimization (SMBO)-algoritmen blinken uit in het doorzoeken van complexe hyperruimten voor optimums, en ze kunnen worden toegepast op het afstemmen van hyperparameters. Tree-based Parzen Estimators (TPE) is een zeer efficiënte SMBO en presteert beter dan zowel Bayesiaanse optimalisatie als willekeurig zoeken.

TPE herhaalt de onderstaande stappen tot een stopcriterium:

  1. Verdeel waargenomen punten in "goede" en "slechte" sets, volgens een hyperparameter, gamma.
  2. Pas een mengselmodel aan zowel de "goede" als de "slechte" set aan om een ​​schatting van de gemiddelde kansdichtheid te ontwikkelen.
  3. Selecteer het punt dat de score 'veelbelovend' optimaliseert, waarbij stap 2 wordt gebruikt om de kans in te schatten dat je in de sets 'goed' en 'slecht' zit.

Ten slotte hebben we een echt cool codefragment dat laat zien hoe HyperOpt parallel kan worden gezet via SparkTrials. Het logt ook al onze iteraties in MLflow.

HyperOpt Demystified opnieuw gepubliceerd van bron https://towardsdatascience.com/hyperopt-demystified-3e14006eb6fa?source=rss—-7f60cf5620c9—4 via https://towardsdatascience.com/feed

<!–

->

Tijdstempel:

Meer van Blockchain-adviseurs