Materiały kursów
Materiały / Azure ML (polski)
Instrukcja do tego ćwiczeniaPobierz notatnik (.ipynb)

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.

In [ ]:
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).

In [ ]:
# Pakiet mltable wraz z silnikiem odczytu danych. Wystarczy raz na instancję obliczeniową.
%pip install -q -U mltable "azureml-dataprep[pandas]"
In [ ]:
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.

In [ ]:
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).
In [ ]:
%%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).

In [ ]:
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.

In [ ]:
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.")
In [ ]:
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.

In [ ]:
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.

In [ ]:
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.