Strojenie hiperparametrów¶
Wiele algorytmów uczenia maszynowego wymaga podania hiperparametrów (ang. hyperparameter) - wartości, które wpływają na przebieg trenowania, ale których nie da się wyznaczyć z samych danych treningowych. Przy regresji logistycznej takim hiperparametrem jest siła regularyzacji, przeciwdziałająca nadmiernemu dopasowaniu modelu. Przy splotowej sieci neuronowej są to na przykład współczynnik uczenia i rozmiar porcji danych, decydujące o tym, jak mocno korygowane są wagi i ile obserwacji przetwarza się naraz. Dobór hiperparametrów potrafi znacząco zmienić zarówno jakość gotowego modelu, jak i czas jego trenowania, więc zwykle trzeba sprawdzić wiele kombinacji, żeby znaleźć tę najlepszą.
W tym ćwiczeniu pracujesz na prostym przykładzie: regresja logistyczna z jednym hiperparametrem. Te same zasady stosują się jednak do dowolnego modelu trenowanego w Azure Machine Learning.
Połączenie z obszarem roboczym¶
Na początek połącz się z obszarem roboczym przy użyciu Azure ML SDK v2.
Uwaga: Jeśli od poprzedniego ćwiczenia wygasła uwierzytelniona sesja z subskrypcją Azure, pojawi się prośba o ponowne zalogowanie.
from azure.ai.ml import MLClient
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()
ml_client = MLClient.from_config(credential=credential)
print(f"Azure ML gotowe do pracy z obszarem roboczym {ml_client.workspace_name}")
Przygotowanie danych dla zadania¶
W tym ćwiczeniu użyjesz zasobu danych z wynikami badań pacjentów pod kątem cukrzycy. Uruchom poniższą komórkę, aby go utworzyć (jeśli powstał już we wcześniejszym ćwiczeniu, zarejestrowana zostanie jego nowa wersja).
# Pakiet mltable wraz z silnikiem odczytu danych. Wystarczy raz na instancję obliczeniową.
%pip install -q -U mltable "azureml-dataprep[pandas]"
import mltable
from azure.ai.ml.entities import Data
from azure.ai.ml.constants import AssetTypes
# Połącz oba pliki CSV w jedną tabelę
sciezki = [
{'file': './data/diabetes.csv'},
{'file': './data/diabetes2.csv'},
]
tbl = mltable.from_delimited_files(paths=sciezki)
# Usuń identyfikator pacjenta. To numer z rejestracji, a nie wynik badania - nie niesie
# żadnej informacji o cukrzycy, ale model potrafi go zapamiętać i wyglądać na dokładny
# na danych, które już widział.
tbl = tbl.drop_columns(['PatientID'])
tbl.save('./diabetes-mltable', colocated=True, overwrite=True)
# Zarejestruj tabelę jako zasób danych
data_asset = Data(
path='./diabetes-mltable',
type=AssetTypes.MLTABLE,
description='diabetes data',
name='diabetes_mltable',
)
ml_client.data.create_or_update(data_asset)
print('Zasób danych gotowy.')
Przygotowanie skryptu treningowego¶
Zacznij od utworzenia folderu na skrypt, który trenuje model regresji logistycznej.
import os
experiment_folder = 'diabetes_training-hyperdrive'
os.makedirs(experiment_folder, exist_ok=True)
print('Folder gotowy.')
Teraz utwórz skrypt Pythona trenujący model. Musi on zawierać:
- parametr dla każdego strojonego hiperparametru (tutaj jest tylko jeden - siła regularyzacji),
- kod zapisujący metrykę, którą chcesz optymalizować (tutaj przez MLflow zapisywane są zarówno AUC, jak i skuteczność, więc optymalizację można oprzeć na dowolnej z nich).
%%writefile $experiment_folder/diabetes_training.py
# Import bibliotek
import argparse
import mlflow
import mlflow.sklearn
import mltable
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
# Odczytaj parametr regularyzacji z wiersza poleceń
parser = argparse.ArgumentParser()
parser.add_argument('--input-data', type=str, dest='training_data', help='mltable z danymi treningowymi')
parser.add_argument('--regularization', type=float, dest='reg_rate', default=0.01, help='współczynnik regularyzacji')
parser.add_argument('--model_output', type=str, dest='model_output',
required=True, help='katalog na gotowy model')
args = parser.parse_args()
reg = args.reg_rate
# wczytaj dane o cukrzycy
print("Wczytywanie danych...")
tbl = mltable.load(args.training_data)
diabetes = tbl.to_pandas_dataframe()
# Rozdziel cechy i etykiety
X, y = diabetes[['Pregnancies','PlasmaGlucose','DiastolicBloodPressure','TricepsThickness','SerumInsulin','BMI','DiabetesPedigree','Age']].values, diabetes['Diabetic'].values
# Podziel dane na zbiór treningowy i testowy
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.30, random_state=0)
# Wytrenuj model regresji logistycznej
print('Trenowanie modelu regresji logistycznej ze współczynnikiem regularyzacji', reg)
mlflow.log_metric('Regularization Rate', float(reg))
model = LogisticRegression(C=1/reg, solver="liblinear").fit(X_train, y_train)
# policz skuteczność
y_hat = model.predict(X_test)
acc = np.average(y_hat == y_test)
print('Skuteczność:', acc)
mlflow.log_metric('Accuracy', float(acc))
# policz AUC
y_scores = model.predict_proba(X_test)
auc = roc_auc_score(y_test, y_scores[:,1])
print('AUC: ' + str(auc))
mlflow.log_metric('AUC', float(auc))
# Zapisujemy model do nazwanego wyjscia zadania. Przez log_model() sie nie
# da: azureml-mlflow obsluguje MLflow najwyzej w wersji 2.16.
mlflow.sklearn.save_model(sk_model=model, path=args.model_output)
print('Model zapisany w:', args.model_output)
Przygotowanie środowiska obliczeniowego¶
Zaletą chmury jest to, że zasoby obliczeniowe przydziela się na żądanie. Dzięki temu można uruchomić wiele przebiegów eksperymentu równolegle, każdy z inną kombinacją hiperparametrów.
Użyjesz klastra obliczeniowego aml-cluster utworzonego we wcześniejszym ćwiczeniu (jeśli nie istnieje, zostanie utworzony).
from azure.ai.ml.entities import AmlCompute
cluster_name = "aml-cluster"
try:
# Pobierz klaster, jeśli już istnieje
training_cluster = ml_client.compute.get(cluster_name)
print('Klaster już istnieje - używamy go.')
except Exception:
# Jeśli nie istnieje, utwórz go
compute_config = AmlCompute(
name=cluster_name,
type="amlcompute",
size="Standard_D2as_v4",
min_instances=0,
max_instances=4,
idle_time_before_scale_down=300,
)
training_cluster = ml_client.compute.begin_create_or_update(compute_config).result()
print(f"Środowisko obliczeniowe '{training_cluster.name}' jest gotowe do pracy.")
Uruchomienie zadania przeglądu¶
Azure Machine Learning SDK v2 stroi hiperparametry za pomocą zadania przeglądu (ang. sweep job). Takie zadanie bierze bazowe zadanie typu command i uruchamia je wielokrotnie - raz dla każdej kombinacji hiperparametrów z przestrzeni przeszukiwania. Na podstawie zapisanej metryki docelowej można potem wskazać próbę (zadanie podrzędne), która dała najlepszy model, i właśnie ten model zarejestrować oraz wdrożyć.
Uwaga: Skrypt odczytuje dane uczące pakietem mltable, którego nie ma w gotowych środowiskach. Poniżej powstaje więc własne środowisko z tym pakietem - obraz zbuduje się przy pierwszym zadaniu i posłuży wszystkim próbom przeglądu.
from azure.ai.ml.entities import Environment
# Skrypt odczytuje wejście pakietem mltable, więc pakiet musi być w środowisku
# wykonania zadania. Gotowe środowisko sklearn go nie zawiera - definiujemy własne.
conda_spec = {
"name": "diabetes-mltable-env",
"channels": ["conda-forge"],
"dependencies": [
"python=3.10",
"scikit-learn",
"pandas",
"numpy",
"pip",
{
"pip": [
"mltable",
"azureml-dataprep[pandas]",
# Skrypt zapisuje model przez mlflow.sklearn, więc potrzebny jest
# pełny mlflow. Przypięty do górnej granicy obsługiwanej przez
# azureml-mlflow - nowszy zrywa logowanie artefaktów.
"mlflow<=3.15.0",
"azureml-mlflow",
]
},
],
}
mltable_env = Environment(
name="diabetes-mltable-env",
description="Środowisko z pakietem mltable do odczytu tabelarycznych zasobów danych",
conda_file=conda_spec,
image="mcr.microsoft.com/azureml/openmpi5.0-ubuntu24.04",
)
# Rejestracja w obszarze roboczym. Sam obiekt przekazany do zadania dałby
# środowisko anonimowe - bez nazwy, osobne dla każdego zadania.
mltable_env = ml_client.environments.create_or_update(mltable_env)
print(f"{mltable_env.name}:{mltable_env.version} - środowisko zarejestrowane.")
from azure.ai.ml import command, Input, Output
from azure.ai.ml.constants import AssetTypes
from azure.ai.ml.sweep import Choice
# Pobierz zarejestrowany zasób danych treningowych
diabetes_data_asset = ml_client.data.get(name="diabetes_mltable", label="latest")
# Skonfiguruj bazowe zadanie typu command
job = command(
code=experiment_folder,
command="python diabetes_training.py --input-data ${{inputs.diabetes}} --regularization ${{inputs.regularization}} --model_output ${{outputs.model_output}}",
inputs={
"diabetes": Input(type=AssetTypes.MLTABLE, path=diabetes_data_asset.id),
"regularization": 0.01,
},
outputs={
# Nazwane wyjscie - stad zarejestrujemy model.
"model_output": Output(type=AssetTypes.MLFLOW_MODEL),
},
environment=f"{mltable_env.name}:{mltable_env.version}",
compute="aml-cluster",
display_name="diabetes-training-hyperdrive",
experiment_name="diabetes-training-hyperdrive",
)
# Określ przestrzeń przeszukiwania dla hiperparametru regularyzacji
# Parametr jest tylko jeden, więc próbkowanie siatką sprawdzi każdą wartość - przy wielu parametrach sprawdziłoby każdą kombinację
command_job_for_sweep = job(
regularization=Choice(values=[0.001, 0.005, 0.01, 0.05, 0.1, 1.0]),
)
# Skonfiguruj zadanie przeglądu
sweep_job = command_job_for_sweep.sweep(
# Klaster, na którym wykonują się wszystkie próby
compute="aml-cluster",
# Jak wybierane są kombinacje: "grid" sprawdza każdą wartość z przestrzeni
# przeszukiwania. Alternatywy to "random" i "bayesian" - przydatne, gdy
# kombinacji jest za dużo, by sprawdzić wszystkie.
sampling_algorithm="grid",
# Metryka, po której porównywane są próby. Nazwa musi być dokładnie taka,
# jak w mlflow.log_metric() w skrypcie treningowym.
primary_metric="AUC",
# Czy metryka ma być jak największa, czy jak najmniejsza. Dla AUC - największa,
# dla metryki błędu byłoby "Minimize".
goal="Maximize",
)
# Ustaw ograniczenia przeglądu:
# max_total_trials - ile prób łącznie. Tu 6, bo tyle wartości ma Choice,
# a przy siatce więcej prób nie wniosłoby nic nowego.
# max_concurrent_trials - ile prób naraz. Nie ma sensu przekraczać liczby węzłów
# klastra (max_instances=4) - nadmiarowe czekałyby w kolejce.
# timeout - po ilu sekundach przerwać całe zadanie (tu 2 godziny).
sweep_job.set_limits(max_total_trials=6, max_concurrent_trials=4, timeout=7200)
# Nazwa eksperymentu nie przenosi się z zadania bazowego - trzeba ją ustawić tutaj,
# inaczej próby trafią do eksperymentu domyślnego.
sweep_job.experiment_name = "diabetes-training-hyperdrive"
# Uruchom zadanie przeglądu
returned_sweep_job = ml_client.create_or_update(sweep_job)
print(f"Zlecono zadanie przeglądu: {returned_sweep_job.name}")
# Wyświetlaj na bieżąco logi zadania w trakcie jego działania
ml_client.jobs.stream(returned_sweep_job.name)
Stan zadania przeglądu śledzisz w logach wyświetlanych powyżej. Możesz też obejrzeć zadanie nadrzędne i jego próby (zadania podrzędne) w Azure Machine Learning studio - otwórz zadanie i przejdź na kartę Child jobs.
Wybór najlepszego przebiegu¶
Gdy wszystkie próby się zakończą, możesz wskazać najlepszą z nich według wybranej metryki - tutaj będzie to najwyższe AUC. MLflow śledzi każdą próbę jako przebieg podrzędny zadania przeglądu, więc da się je odpytać bezpośrednio.
import mlflow
import pandas as pd
mlflow.set_tracking_uri(ml_client.workspaces.get(ml_client.workspace_name).mlflow_tracking_uri)
# Zadanie przeglądu samo zapisuje, która próba wypadła najlepiej
ukonczone = ml_client.jobs.get(returned_sweep_job.name)
best_run_id = ukonczone.properties["best_child_run_id"]
# Próby podrzędne bierzemy z Azure ML, a ich metryki z MLflow
wiersze = []
for proba in ml_client.jobs.list(parent_job_name=returned_sweep_job.name):
przebieg = mlflow.get_run(proba.name)
wiersze.append({
"run_id": proba.name,
"AUC": przebieg.data.metrics.get("AUC"),
"Accuracy": przebieg.data.metrics.get("Accuracy"),
# Skrypt zapisuje tę wartość jako metrykę, nie jako parametr
"Regularization Rate": przebieg.data.metrics.get("Regularization Rate"),
})
child_runs = pd.DataFrame(wiersze).sort_values("AUC", ascending=False)
print(child_runs.to_string(index=False))
best_run = mlflow.get_run(best_run_id)
best_run_metrics = best_run.data.metrics
print('\nIdentyfikator najlepszego przebiegu:', best_run_id)
print(' -AUC:', best_run_metrics['AUC'])
print(' -Skuteczność:', best_run_metrics['Accuracy'])
print(' -Współczynnik regularyzacji:', best_run_metrics['Regularization Rate'])
Najlepszy przebieg jest już znany, więc możesz zarejestrować wytrenowany w nim model.
from azure.ai.ml.entities import Model
from azure.ai.ml.constants import AssetTypes
# Zarejestruj model zapisany przez MLflow w najlepszej próbie
model = Model(
path=f"azureml://jobs/{best_run_id}/outputs/model_output",
name="diabetes_model",
description="Model trained using a hyperparameter sweep",
type=AssetTypes.MLFLOW_MODEL,
properties={'AUC': best_run_metrics['AUC'], 'Accuracy': best_run_metrics['Accuracy']},
)
registered_model = ml_client.models.create_or_update(model)
# Wypisz zarejestrowane modele
for m in ml_client.models.list(name="diabetes_model"):
print(m.name, 'wersja:', m.version)
Więcej informacji: O strojeniu hiperparametrów przy użyciu zadań przeglądu przeczytasz w dokumentacji Azure ML.