Procedura per addestrare una piccola rete neurale PyTorch su Azure Machine Learning, confrontare due addestramenti, registrare il modello e pubblicarlo su un endpoint in tempo reale, poi chiamarlo da una console .NET. Le risorse si creano, si provano e si eliminano dal portale e dallo studio di Azure Machine Learning; dal computer si scrive il programma di addestramento, che è un file Python, e la console .NET.

Occorre: un account Azure con una sottoscrizione attiva e .NET 10 sul computer. Durata: più di un'ora, quasi tutta di attesa: circa un quarto d'ora per ogni addestramento e venti minuti per la pubblicazione. Costo: meno di un dollaro, se elimini le risorse alla fine. Il portale indica 0,27 dollari l'ora per la macchina dell'addestramento e 0,12 dollari l'ora per quella dell'endpoint.

Il portale e lo studio delle schermate sono in inglese: i nomi dei comandi sono riportati come appaiono a schermo.

L'area di lavoro

1. Crea l'area di lavoro

Apri https://portal.azure.com/#create/Microsoft.MachineLearningServices e compila:

  • Resource group: Create new, rg-guide-ml-01, OK
  • Name: mlw-guide-extranet-01
  • Region: West Europe

Il portale propone da solo l'account di archiviazione, il Key Vault e Application Insights.

La scheda Basics dell'area di lavoro

Premi Review + create.

Il riepilogo prima della creazione

Premi Create e aspetta Your deployment is complete.

La creazione completata

2. Apri lo studio

Premi Go to resource e poi Launch studio. I passi seguenti, fino alla pubblicazione, si fanno nello studio.

L'area di lavoro nel portale, con Launch studio

L'addestramento

3. Scrivi il programma di addestramento

Sul computer crea il file train.py. Addestra una rete con uno strato nascosto sul dataset Iris di scikit-learn: 150 fiori, quattro misure per fiore, tre specie da riconoscere.

import argparse

import mlflow
import mlflow.pytorch
import pandas as pd
import torch
from mlflow.models import infer_signature
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from torch import nn

parser = argparse.ArgumentParser()
parser.add_argument("--epochs", type=int, default=200)
parser.add_argument("--hidden", type=int, default=8)
parser.add_argument("--lr", type=float, default=0.01)
args = parser.parse_args()

iris = load_iris(as_frame=True)
columns = ["sepal_length", "sepal_width", "petal_length", "petal_width"]
features = iris.data.set_axis(columns, axis=1).astype("float32")
x_train, x_test, y_train, y_test = train_test_split(
    features, iris.target, test_size=0.3, random_state=42, stratify=iris.target)

to_x = lambda frame: torch.tensor(frame.values, dtype=torch.float32)
to_y = lambda series: torch.tensor(series.values, dtype=torch.long)

torch.manual_seed(42)
network = nn.Sequential(nn.Linear(4, args.hidden), nn.ReLU(), nn.Linear(args.hidden, 3))
optimizer = torch.optim.Adam(network.parameters(), lr=args.lr)
loss_function = nn.CrossEntropyLoss()

mlflow.log_params(vars(args))

for epoch in range(args.epochs):
    optimizer.zero_grad()
    loss = loss_function(network(to_x(x_train)), to_y(y_train))
    loss.backward()
    optimizer.step()
    if epoch % 10 == 0:
        mlflow.log_metric("loss", loss.item(), step=epoch)

model = nn.Sequential(network, nn.Softmax(dim=1)).eval()
with torch.no_grad():
    probabilities = model(to_x(x_test))
accuracy = (probabilities.argmax(dim=1) == to_y(y_test)).float().mean().item()
mlflow.log_metric("accuracy", accuracy)
print(f"accuracy: {accuracy:.4f}")

signature = infer_signature(x_test, pd.DataFrame(probabilities.numpy()))
requirements = [
    f"mlflow=={mlflow.__version__}",
    f"torch=={torch.__version__.split('+')[0]}",
    "cloudpickle",
    "pandas<3",
    "azureml-ai-monitoring",
]
mlflow.pytorch.save_model(model, "model", signature=signature,
                          serialization_format="pickle", pip_requirements=requirements)
mlflow.log_artifacts("model", artifact_path="model")

I parametri e le metriche registrati con MLflow compaiono nello studio. Le ultime righe salvano il modello nel formato che Azure sa pubblicare senza script di inferenza. In questa prova ognuna di queste scelte ha evitato un errore:

  • save_model seguito da log_artifacts, perché mlflow.pytorch.log_model con MLflow 3.15 si è fermato su una chiamata che Azure Machine Learning non accetta.
  • serialization_format="pickle", perché con MLflow 3 il formato predefinito per PyTorch richiede un esempio di dati in ingresso.
  • pandas<3, perché senza questa riga il modello chiede pandas 3.1.1, MLflow 3.15 vuole pandas inferiore a 3 e Azure non riesce a costruire l'immagine dell'endpoint.
  • azureml-ai-monitoring, perché lo script di inferenza generato da Azure importa questo pacchetto e senza di esso l'endpoint non parte.

4. Imposta il primo addestramento

Nello studio apri Jobs, premi Create job, scegli Run a custom training script e premi Start configuring job.

La scelta del metodo di addestramento

In Job name scrivi hidden-8, in New experiment name iris-classification e premi Next.

Il nome dell'addestramento e dell'esperimento

Premi Browse > Local files e scegli train.py. In Command scrivi:

pip install scikit-learn && python train.py --epochs 200 --hidden 8 --lr 0.01

L'ambiente PyTorch scelto più avanti non contiene scikit-learn: lo installa la prima parte del comando. Premi Next.

Il programma caricato e il comando

5. Scegli la macchina e l'ambiente

Lascia Serverless, CPU, Dedicated e la macchina proposta, Standard_DS3_v2. Azure la accende per l'addestramento e la spegne alla fine. Premi Next.

La macchina dell'addestramento

In Curated environments cerca pytorch, seleziona acpt-pytorch-2.8-cuda12.6 e premi Next.

L'ambiente PyTorch selezionato

Controlla il riepilogo e premi Submit training job.

Il riepilogo dell'addestramento

6. Leggi l'esito

Nella prova l'addestramento è rimasto in coda poco più di due minuti ed è durato dodici, quasi tutti per preparare la macchina e l'ambiente. Al termine lo stato è Completed e a destra compaiono i parametri e le metriche: accuracy 1, cioè tutti i 45 fiori di prova riconosciuti.

L'addestramento completato, con parametri e metriche

7. Confronta un secondo addestramento

Ripeti i passi 4 e 5 con Job name hidden-2, di nuovo iris-classification in New experiment name, così lo studio lo aggiunge allo stesso esperimento, e questo comando, che usa una rete più piccola e un addestramento più breve:

pip install scikit-learn && python train.py --epochs 30 --hidden 2 --lr 0.001

Quando è finito, apri l'esperimento iris-classification, premi Columns, aggiungi accuracy e hidden e premi Save. Le due colonne si aggiungono in fondo alla tabella: scorrila verso destra.

Il confronto fra i due addestramenti

La rete con 2 neuroni riconosce un fiore su tre, come tirando a caso; quella con 8 li riconosce tutti.

Il modello

8. Registra le due versioni

Apri l'addestramento hidden-2 e premi Register model. Lo studio trova da solo la cartella model in formato MLflow.

La cartella del modello trovata nell'output

Premi Next, in Name scrivi iris-model, premi Next e Register: è la versione 1.

Ripeti dall'addestramento hidden-8 con lo stesso nome: lo studio propone la versione 2.

Il nome del modello e la versione proposta

La pubblicazione

9. Pubblica la versione 2

Apri Models > iris-model, versione 2, premi Use this model > Real-time endpoint. Lo studio avvisa che lo script di inferenza e l'ambiente vengono generati da soli.

Compila:

  • Instance count: 1. La procedura ne propone 3, e nella prova la quota della sottoscrizione non bastava.
  • Virtual machine: Standard_D2as_v4
  • Endpoint name: un nome tuo, unico nella regione, qui iris-guide-extranet-01

La pubblicazione della versione 2

Premi Deploy. Nella prova la pubblicazione ha richiesto 23 minuti, perché Azure costruisce l'immagine con i pacchetti indicati nel modello. Al termine iris-model-2 riceve il 100% del traffico.

L'endpoint con la pubblicazione riuscita

10. Prova dallo studio

Nella scheda Test incolla in Input:

{"input_data": {
  "columns": ["sepal_length", "sepal_width", "petal_length", "petal_width"],
  "index": [0, 1, 2],
  "data": [[5.1, 3.5, 1.4, 0.2], [6.0, 2.9, 4.5, 1.5], [6.9, 3.1, 5.4, 2.1]]
}}

Premi Test. Per ogni fiore la risposta riporta la probabilità delle tre specie: 0 setosa, 1 versicolor, 2 virginica.

La risposta dell'endpoint nella scheda Test

11. Copia indirizzo e chiave

Nella scheda Consume copia REST endpoint e Primary key. La chiave dà accesso al modello: non scriverla nel codice e non pubblicarla.

L'indirizzo e la chiave, nascosta

Il programma .NET

12. Chiama l'endpoint da .NET

dotnet new console -n iris-client
cd iris-client

Sostituisce Program.cs:

using System.Net.Http.Headers;
using System.Net.Http.Json;

string scoringUri = Environment.GetEnvironmentVariable("SCORING_URI")!;
string key = Environment.GetEnvironmentVariable("ENDPOINT_KEY")!;

var request = new
{
    input_data = new
    {
        columns = new[] { "sepal_length", "sepal_width", "petal_length", "petal_width" },
        index = new[] { 0, 1, 2 },
        data = new[]
        {
            new[] { 5.1, 3.5, 1.4, 0.2 },
            new[] { 6.0, 2.9, 4.5, 1.5 },
            new[] { 6.9, 3.1, 5.4, 2.1 },
        },
    },
};

using HttpClient http = new();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key);

HttpResponseMessage response = await http.PostAsJsonAsync(scoringUri, request);
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine(await response.Content.ReadAsStringAsync());

Imposta le due variabili con i valori copiati e avvia:

export SCORING_URI=https://iris-guide-extranet-01.westeurope.inference.ml.azure.com/score
export ENDPOINT_KEY=<chiave>
dotnet run

In PowerShell le prime due righe sono $env:SCORING_URI = "..." e $env:ENDPOINT_KEY = "...". Nella prova:

HTTP 200
[{"0": 0.9828304052352905, "1": 0.01716943085193634, "2": 7.715370742289451e-08}, {"0": 0.006401201244443655, "1": 0.7643778324127197, "2": 0.22922086715698242}, {"0": 0.00015558379527647048, "1": 0.21182303130626678, "2": 0.7880213856697083}]

Il primo fiore è una setosa al 98%, il secondo una versicolor al 76%, il terzo una virginica al 79%. Con una chiave sbagliata l'endpoint risponde HTTP 401.

Eliminare le risorse

13. Elimina il gruppo

Apri il gruppo rg-guide-ml-01. Oltre alle risorse create al passo 1 contiene l'endpoint, la sua pubblicazione e un registro di contenitori che Azure ha creato da solo per l'immagine dell'endpoint.

Le risorse dentro il gruppo

Premi Delete resource group, scrivi il nome del gruppo per confermare, premi Delete e, nella finestra che segue, di nuovo Delete.

La conferma dell'eliminazione del gruppo

14. Elimina in modo definitivo l'area di lavoro

Eliminato il gruppo, Azure tiene l'area di lavoro tra quelle eliminate per 14 giorni. Finché resta lì, la creazione di una nuova area di lavoro con lo stesso nome fallisce con Soft-deleted workspace exists. Please purge or recover it.

Sul portale cerca Azure Machine Learning, premi Recently deleted, scegli la sottoscrizione e, in Location, West Europe. Spunta mlw-guide-extranet-01 e premi Permanently delete.

L'area di lavoro tra quelle eliminate di recente

Nomi dei comandi, schermate e risposte rilevati il 10 novembre 2026, regione West Europe, con l'ambiente acpt-pytorch-2.8-cuda12.6 versione 21 (PyTorch 2.13.0, MLflow 3.15.0) e .NET 10.0.401; il passo 14 il 12 novembre 2026.