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

Praca z zasobami danych

W poprzednich ćwiczeniach dostęp do danych w chmurze zapewniał magazyn danych (ang. datastore). W tym ćwiczeniu poznasz zasoby danych (ang. data asset) - kolejny poziom abstrakcji, dzięki któremu łatwiej wskazywać zadaniom i trenowaniu konkretne dane.

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

W poprzednim ćwiczeniu powstał zasób danych typu uri_folder. Zasoby danych zwykle (choć nie zawsze) opierają się na danych wysłanych do magazynu danych.

Jeśli poprzednie ćwiczenie nie zostało wykonane, uruchom poniższy kod, aby zarejestrować dwa lokalne pliki CSV jako zasób danych. Jeśli było wykonane, ten sam zestaw danych zarejestruje się ponownie i powstanie po prostu nowa wersja tego samego zasobu.

In [ ]:
from azure.ai.ml.entities import Data
from azure.ai.ml.constants import AssetTypes

diabetes_data_folder = Data(
    path="./data",
    type=AssetTypes.URI_FOLDER,
    description="Diabetes data files (folder)",
    name="diabetes_data_folder",
)
diabetes_data_folder = ml_client.data.create_or_update(diabetes_data_folder)
print(f"Zasób danych gotowy: {diabetes_data_folder.name} (wersja {diabetes_data_folder.version})")

Utworzenie tabelarycznego zasobu danych

Do pracy z danymi tabelarycznymi w Azure ML służy mltable - definicja tabeli opisująca, jak odczytać jeden lub więcej plików rozdzielanych separatorem jako jeden zbiór tabelaryczny. Zbuduj mltable na podstawie wysłanych danych o cukrzycy i obejrzyj pierwsze 20 rekordów. Dane leżą w uporządkowanych plikach CSV, więc posłuży do tego mltable.from_delimited_files().

In [ ]:
# Pakiet mltable wraz z silnikiem odczytu danych. Wystarczy raz na instancję obliczeniową.
%pip install -q -U mltable "azureml-dataprep[pandas]"
In [ ]:
import mltable

# Definicja tabeli na podstawie plików CSV wysłanych do magazynu (chwilę to trwa)
sciezka = {"pattern": f"{diabetes_data_folder.path.rstrip('/')}/*.csv"}
tab_data_set = mltable.from_delimited_files(paths=[sciezka])

# Pierwsze 20 wierszy jako ramka danych Pandas
tab_data_set.to_pandas_dataframe().head(20)

Jak widać powyżej, mltable łatwo zmaterializować jako ramkę danych Pandas i dalej pracować z danymi zwykłymi technikami Pythona.

Utworzenie plikowego zasobu danych

Utworzony mltable pozwala odczytać wszystkie opisane przez niego pliki jako jedną ramkę danych. Do danych tabelarycznych sprawdza się to świetnie, ale w niektórych zastosowaniach uczenia maszynowego pracuje się z danymi nieustrukturyzowanymi albo po prostu chce się samodzielnie zdecydować, jak odczytać pliki. Wtedy te same dane wygodnie potraktować jako uri_folder - referencję do folderu w magazynie danych, po którym można się poruszać i który można czytać przez interfejs przypominający system plików, udostępniany przez pakiet azureml-fsspec.

In [ ]:
from azureml.fsspec import AzureMachineLearningFileSystem

# Widok systemu plików na tym samym folderze (może to chwilę potrwać)
fs = AzureMachineLearningFileSystem(diabetes_data_folder.path)
file_data_set = fs.glob(f"{diabetes_data_folder.path.rstrip('/')}/*.csv")

# Wypisz pliki wchodzące w skład zbioru
for file_path in file_data_set:
    print(file_path)

Rejestrowanie zasobów danych

Skoro powstał już mltable i widok plikowy na dane o cukrzycy, można je zarejestrować, aby były łatwo dostępne dla każdego zadania uruchamianego w obszarze roboczym.

Tabelę zarejestrujesz jako diabetes_mltable, a folder jako diabetes_file_dataset.

Dlaczego nie diabetes_dataset: zasób o tej nazwie powstał w Lab 1A przez kreator w studio, który idzie ścieżką starszego API (v1). SDK v2 potrafi taki zasób czytać, ale nie potrafi dopisać do niego nowej wersji - próba kończy się błędem Cannot create V2 Data Version in V1 Data Container. Zasobów danych w Azure ML nie da się usuwać (gwarantuje to odtwarzalność eksperymentów), więc ta nazwa pozostaje zajęta przez v1 na stałe. Dane rejestrowane z kodu dostają więc własną nazwę.

In [ ]:
# Zapisz definicję tabeli lokalnie, a potem zarejestruj ją jako nową wersję diabetes_mltable
tab_data_set.save("./diabetes-mltable", colocated=True, overwrite=True)

from azure.ai.ml.entities import Data
from azure.ai.ml.constants import AssetTypes

tabular_data_asset = Data(
    path="./diabetes-mltable",
    type=AssetTypes.MLTABLE,
    description="diabetes data (tabular, both CSV files)",
    name="diabetes_mltable",
    tags={"format": "CSV"},
)
tabular_data_asset = ml_client.data.create_or_update(tabular_data_asset)

# Zarejestruj plikowy zasób danych
file_data_asset = Data(
    path="./data",
    type=AssetTypes.URI_FOLDER,
    description="diabetes files",
    name="diabetes_file_dataset",
    tags={"format": "CSV"},
)
file_data_asset = ml_client.data.create_or_update(file_data_asset)

print(f"Zarejestrowano {tabular_data_asset.name} (wersja {tabular_data_asset.version}) oraz {file_data_asset.name} (wersja {file_data_asset.version})")

Zasoby danych obejrzysz i uporządkujesz na stronie Data assets swojego obszaru roboczego w Azure Machine Learning studio. Listę zasobów pobierzesz też z poziomu SDK:

In [ ]:
print("Zasoby danych w obszarze roboczym:")
for data_asset in ml_client.data.list():
    print(f"\t{data_asset.name}")

# Wersja i typ należą do konkretnej wersji zasobu, a nie do jego nazwy
for nazwa in ["diabetes_mltable", "diabetes_file_dataset"]:
    najnowszy = ml_client.data.get(name=nazwa, label="latest")
    print(f"\n{najnowszy.name}: najnowsza wersja {najnowszy.version}, typ {najnowszy.type}")

Powyższa rejestracja utworzyła zasób diabetes_mltable w wersji 1. Uruchom tę komórkę ponownie, a powstanie wersja 2 - wersja 1 pozostanie dostępna bez zmian. Wersjonowanie zasobów danych pozwala redefiniować dane bez psucia istniejących zadań i potoków, które opierają się na wcześniejszych definicjach. Domyślnie ml_client.data.get() bez podanej wersji zwraca wersję najnowszą, ale konkretną wersję pobierzesz, podając jej numer:

dataset_v1 = ml_client.data.get(name="diabetes_mltable", version="1")

Trenowanie modelu na tabelarycznym zasobie danych

Zasoby danych są już gotowe, więc można na nich trenować modele. Zasób danych przekazuje się do zadania jako wejście, przy użyciu klasy Input.

Uruchom dwie poniższe komórki, aby utworzyć:

  1. Folder o nazwie diabetes_training_from_tab_dataset
  2. Skrypt, który trenuje model klasyfikacji na tabelarycznym zasobie danych (mltable) przekazanym mu jako wejście.
In [ ]:
import os

# Utwórz folder na pliki zadania
experiment_folder = 'diabetes_training_from_tab_dataset'
os.makedirs(experiment_folder, exist_ok=True)
print(experiment_folder, '- folder utworzony')
In [ ]:
%%writefile $experiment_folder/diabetes_training.py
# Import bibliotek
import argparse
import os
import mlflow
import mltable
import pandas as pd
import numpy as np
import joblib
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve

# Ustaw hiperparametr regularyzacji (przekazany do skryptu jako argument)
parser = argparse.ArgumentParser()
parser.add_argument('--regularization', type=float, dest='reg_rate', default=0.01, help='wskaźnik regularyzacji')
parser.add_argument('--training-data', type=str, dest='training_data', help='ścieżka do danych uczących w formie mltable')
args = parser.parse_args()
reg = args.reg_rate

# Rozpocznij przebieg MLflow, aby zapisywać metryki (śledzenie MLflow jest wbudowane w zadania Azure ML)
mlflow.start_run()

# wczytaj dane o cukrzycy (przekazane jako wejście typu mltable)
print("Wczytywanie danych...")
tbl = mltable.load(args.training_data)
diabetes = tbl.to_pandas_dataframe()

# Rozdziel cechy (ang. features) i etykietę (ang. label)
X, y = diabetes[['Pregnancies','PlasmaGlucose','DiastolicBloodPressure','TricepsThickness','SerumInsulin','BMI','DiabetesPedigree','Age']].values, diabetes['Diabetic'].values

# Podziel dane na zbiór uczący 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 wskaźnikiem 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))

os.makedirs('outputs', exist_ok=True)
# pliki zapisane w folderze outputs są automatycznie dołączane do wyników zadania
joblib.dump(value=model, filename='outputs/diabetes_model.pkl')

mlflow.end_run()

Teraz utwórz zadanie, które uruchomi skrypt, i zdefiniuj nazwane wejście z zasobem danych uczących, odczytywane przez skrypt.

Uwaga: Skrypt wczytuje zasób danych pakietem mltable, więc pakiet ten musi być dostępny w środowisku wykonania zadania. Gotowe (ang. curated) środowiska - w tym użyte we wcześniejszych ćwiczeniach środowisko sklearn - go nie zawierają. Dlatego poniżej powstaje własne środowisko z tym pakietem. Przy pierwszym uruchomieniu Azure ML zbuduje z niego obraz kontenera, co potrwa kilka minut; kolejne zadania korzystają z gotowego obrazu.

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
from azure.ai.ml.constants import AssetTypes

job = command(
    code=experiment_folder,
    command="python diabetes_training.py --regularization 0.1 --training-data ${{inputs.training_data}}",
    inputs={
        "training_data": Input(type=AssetTypes.MLTABLE, path=f"azureml:{tabular_data_asset.name}:{tabular_data_asset.version}", mode="ro_mount")
    },
    environment=f"{mltable_env.name}:{mltable_env.version}",
    compute="aml-cluster",
    display_name="diabetes-training-tabular",
    experiment_name="diabetes-training",
)

# zleć zadanie
returned_job = ml_client.jobs.create_or_update(job)
ml_client.jobs.stream(returned_job.name)

Przy pierwszym uruchomieniu zadania zbudowanie środowiska może chwilę potrwać - kolejne uruchomienia są już szybsze.

Po zakończeniu zadania jego szczegóły obejrzysz w Azure Machine Learning studio, łącznie z kartą Outputs + logs i metrykami zapisanymi przez przebieg. Możesz je też pobrać z poziomu kodu:

In [ ]:
import mlflow

mlflow.set_tracking_uri(ml_client.workspaces.get(ml_client.workspace_name).mlflow_tracking_uri)
mlflow_run = mlflow.get_run(returned_job.name)

print("Metryki:")
for key, value in mlflow_run.data.metrics.items():
    print(key, value)

print(f"\nSzczegóły przebiegu w Studio: {returned_job.studio_url}")

Wytrenowany model został zapisany jako plik diabetes_model.pkl w folderze outputs, więc można go zarejestrować.

In [ ]:
from azure.ai.ml.entities import Model
from azure.ai.ml.constants import AssetTypes

model = Model(
    path=f"azureml://jobs/{returned_job.name}/outputs/artifacts/paths/outputs/diabetes_model.pkl",
    name="diabetes_model",
    type=AssetTypes.CUSTOM_MODEL,
    tags={"Training context": "Command job (tabular data asset)"},
)
ml_client.models.create_or_update(model)

for m in ml_client.models.list(name="diabetes_model"):
    print(m.name, 'wersja:', m.version)
    for tag_name in m.tags:
        print('\t', tag_name, ':', m.tags[tag_name])

Trenowanie modelu na plikowym zasobie danych

Trenowanie na tabelarycznym zasobie danych (mltable) masz już za sobą. A co z plikowym zasobem danych typu uri_folder?

Przy wejściu typu uri_folder skrypt dostaje punkt podmontowania (albo pobrany folder) ze ścieżkami do plików. Sposób odczytu zależy od tego, co jest w tych plikach i co chcesz z nimi zrobić. W przypadku plików CSV z danymi o cukrzycy wystarczy modułem glob wypisać pliki w folderze, wczytać każdy z nich do ramki danych Pandas i połączyć wszystko w jedną ramkę.

Uruchom dwie poniższe komórki, aby utworzyć:

  1. Folder o nazwie diabetes_training_from_file_dataset
  2. Skrypt, który trenuje model klasyfikacji na zasobie danych typu uri_folder przekazanym mu jako wejście.
In [ ]:
import os

# Utwórz folder na pliki zadania
experiment_folder = 'diabetes_training_from_file_dataset'
os.makedirs(experiment_folder, exist_ok=True)
print(experiment_folder, '- folder utworzony')
In [ ]:
%%writefile $experiment_folder/diabetes_training.py
# Import bibliotek
import argparse
import os
import glob
import mlflow
import pandas as pd
import numpy as np
import joblib
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve

# Ustaw hiperparametr regularyzacji (przekazany do skryptu jako argument)
parser = argparse.ArgumentParser()
parser.add_argument('--regularization', type=float, dest='reg_rate', default=0.01, help='wskaźnik regularyzacji')
parser.add_argument('--training-data', type=str, dest='training_data', help='ścieżka do folderu z plikami danych uczących')
args = parser.parse_args()
reg = args.reg_rate

# Rozpocznij przebieg MLflow, aby zapisywać metryki (śledzenie MLflow jest wbudowane w zadania Azure ML)
mlflow.start_run()

# wczytaj zbiór danych o cukrzycy
print("Wczytywanie danych...")
data_path = args.training_data
all_files = glob.glob(data_path + "/*.csv")
diabetes = pd.concat((pd.read_csv(f) for f in all_files))

# Rozdziel cechy (ang. features) i etykietę (ang. label)
X, y = diabetes[['Pregnancies','PlasmaGlucose','DiastolicBloodPressure','TricepsThickness','SerumInsulin','BMI','DiabetesPedigree','Age']].values, diabetes['Diabetic'].values

# Podziel dane na zbiór uczący 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 wskaźnikiem 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))

os.makedirs('outputs', exist_ok=True)
# pliki zapisane w folderze outputs są automatycznie dołączane do wyników zadania
joblib.dump(value=model, filename='outputs/diabetes_model.pkl')

mlflow.end_run()

Pozostaje wybrać, w jaki sposób wejście trafi na środowisko obliczeniowe. Przy dużych wolumenach danych używa się zwykle mode="ro_mount", czyli czytania plików prosto z magazynu. Dla tak małego zbioru jak ten równie dobrze sprawdza się mode="download", który najpierw kopiuje pliki na maszynę.

In [ ]:
from azure.ai.ml import command, Input
from azure.ai.ml.constants import AssetTypes

job = command(
    code=experiment_folder,
    command="python diabetes_training.py --regularization 0.1 --training-data ${{inputs.training_data}}",
    inputs={
        "training_data": Input(type=AssetTypes.URI_FOLDER, path=f"azureml:{file_data_asset.name}:{file_data_asset.version}", mode="download")
    },
    environment="azureml://registries/azureml/environments/sklearn-1.5/labels/latest",
    compute="aml-cluster",
    display_name="diabetes-training-file",
    experiment_name="diabetes-training",
)

# zleć zadanie
returned_job = ml_client.jobs.create_or_update(job)
ml_client.jobs.stream(returned_job.name)

Po zakończeniu zadania jego szczegóły obejrzysz w Azure Machine Learning studio, w tym kartę Outputs + logs - sprawdzisz tam, że plikowy zasób danych został przetworzony, a pliki odczytane. Zapisane metryki pobierzesz też z poziomu kodu:

In [ ]:
import mlflow

mlflow.set_tracking_uri(ml_client.workspaces.get(ml_client.workspace_name).mlflow_tracking_uri)
mlflow_run = mlflow.get_run(returned_job.name)

print("Metryki:")
for key, value in mlflow_run.data.metrics.items():
    print(key, value)

print(f"\nSzczegóły przebiegu w Studio: {returned_job.studio_url}")

Tak jak poprzednio, zarejestruj wytrenowany model.

In [ ]:
from azure.ai.ml.entities import Model
from azure.ai.ml.constants import AssetTypes

model = Model(
    path=f"azureml://jobs/{returned_job.name}/outputs/artifacts/paths/outputs/diabetes_model.pkl",
    name="diabetes_model",
    type=AssetTypes.CUSTOM_MODEL,
    tags={"Training context": "Command job (file data asset)"},
)
ml_client.models.create_or_update(model)

for m in ml_client.models.list(name="diabetes_model"):
    print(m.name, 'wersja:', m.version)
    for tag_name in m.tags:
        print('\t', tag_name, ':', m.tags[tag_name])

Więcej informacji: O odczycie i zapisie danych w zadaniach przeczytasz w artykule Read and write data in a job w dokumentacji Azure ML.