Usar o ML automatizado em um pipeline do Azure Machine Learning no Python

APLICA-SE A:azureml do SDK do Python v1

O recurso ML automatizado do Azure Machine Learning ajuda a descobrir modelos de alto desempenho sem a necessidade de reimplementar todas as abordagens possíveis. Combinado com pipelines do Azure Machine Learning, você pode criar fluxos de trabalho implantáveis que podem descobrir rapidamente o algoritmo que funciona melhor para seus dados. Este artigo mostrará como unir com eficiência uma etapa de preparação de dados a uma etapa de ML automatizada. O ML automatizado pode descobrir rapidamente o algoritmo que funciona melhor para seus dados, enquanto coloca você no caminho de MLOps e operacionalização do ciclo de vida do modelo com pipelines.

Pré-requisitos

Examinar as classes centrais do ML automatizado

O ML automatizado em um pipeline é representado por um objeto AutoMLStep. A classe AutoMLStep é uma subclasse de PipelineStep. Um grafo de PipelineStep objetos define um Pipeline.

Há várias subclasses de PipelineStep. Além do AutoMLStep, este artigo mostrará um PythonScriptStep para a preparação de dados e outro para registrar o modelo.

A maneira preferida de mover dados inicialmente em um pipeline de ML é com objetos Dataset. Para mover dados entre etapas e salvar resultados de dados de execuções, a maneira preferida é com objetos OutputFileDatasetConfig e OutputTabularDatasetConfig. Para ser usado com AutoMLStep, o objeto PipelineData deve ser transformado em um objeto PipelineOutputTabularDataset. Para obter mais informações, consulte dados de entrada e saída de pipelines de ML.

O AutoMLStep é configurado por meio de um objeto AutoMLConfig. AutoMLConfig é uma classe flexível, conforme discutido em Configurar experimentos de ML automatizados no Python.

Um Pipeline é executado em um Experiment. O pipeline Run tem, para cada etapa, um StepRun filho. As saídas do ML automatizado StepRun são as métricas de treinamento e o modelo de maior desempenho.

Para tornar as coisas concretas, este artigo cria um pipeline simples para uma tarefa de classificação. A tarefa está prevendo a sobrevivência Titanic, mas não discutiremos os dados ou a tarefa, exceto na passagem.

Introdução

Recuperar conjunto de dados inicial

Geralmente, um fluxo de trabalho de ML começa com dados de linha de base pré-existentes. Esse é um bom cenário para um conjunto de dados registrado. Os conjuntos de dados são visíveis no espaço de trabalho, no controle de versão de suporte e podem ser explorados interativamente. Há muitas maneiras de criar e popular um conjunto de dados, conforme discutido em Criar conjuntos de dados de Azure Machine Learning. Como usaremos o SDK do Python para criar nosso pipeline, uso o SDK para baixar dados de linha de base e registrá-los com o nome ' titanic_ds'.

from azureml.core import Workspace, Dataset

ws = Workspace.from_config()
if not 'titanic_ds' in ws.datasets.keys() :
    # create a TabularDataset from Titanic training data
    web_paths = ['https://dprepdata.blob.core.windows.net/demo/Titanic.csv',
                 'https://dprepdata.blob.core.windows.net/demo/Titanic2.csv']
    titanic_ds = Dataset.Tabular.from_delimited_files(path=web_paths)

    titanic_ds.register(workspace = ws,
                                     name = 'titanic_ds',
                                     description = 'Titanic baseline data',
                                     create_new_version = True)

titanic_ds = Dataset.get_by_name(ws, 'titanic_ds')

O código primeiro faz logon no espaço de trabalho Azure Machine Learning definido em config.json (para obter uma explicação, consulte Criar um arquivo de configuração de espaço de trabalho. Se já não houver um conjunto de dados chamado 'titanic_ds' registrado, ele criará um. O código baixa dados CSV da Web, usa-os para instanciar um TabularDataset e, em seguida, registra o conjunto de dados com o espaço de trabalho. Por fim, a função Dataset.get_by_name() atribui o Dataset para titanic_ds.

Configurar seu armazenamento e o destino de computação

Recursos adicionais que serão necessários para o pipeline são o armazenamento e, geralmente, recursos de computação do Azure Machine Learning.

from azureml.core import Datastore
from azureml.core.compute import AmlCompute, ComputeTarget

datastore = ws.get_default_datastore()

compute_name = 'cpu-cluster'
if not compute_name in ws.compute_targets :
    print('creating a new compute target...')
    provisioning_config = AmlCompute.provisioning_configuration(vm_size='STANDARD_D2_V2',
                                                                min_nodes=0,
                                                                max_nodes=1)
    compute_target = ComputeTarget.create(ws, compute_name, provisioning_config)

    compute_target.wait_for_completion(
        show_output=True, min_node_count=None, timeout_in_minutes=20)

    # Show the result
    print(compute_target.get_status().serialize())

compute_target = ws.compute_targets[compute_name]

Os dados intermediários entre a preparação de dados e a etapa de ML automatizada podem ser armazenados no repositório de dados padrão do espaço de trabalho, portanto, não precisamos fazer mais do que chamar get_default_datastore() no Workspace objeto.

Depois disso, o código verifica se o destino de computação 'cpu-cluster' do Azure Machine Learning já existe. Caso contrário, especificamos que queremos um pequeno destino de computação baseado em CPU. Se você planeja usar os recursos de aprendizado profundo do ML automatizado (por exemplo, definição de recursos de texto com suporte a DNN), deverá escolher uma computação com suporte de GPU forte, conforme descrito em Tamanhos de máquina virtual com otimização de GPU.

O código é bloqueado até que o destino seja provisionado e, em seguida, imprima alguns detalhes do destino de computação recém-criado. Por fim, o destino de computação nomeado é recuperado do espaço de trabalho e atribuído a compute_target.

Configurar execução de treinamento

O contexto de tempo de execução é definido criando e configurando um objeto RunConfiguration. Aqui, definimos o destino de computação.

from azureml.core.runconfig import RunConfiguration
from azureml.core.conda_dependencies import CondaDependencies

aml_run_config = RunConfiguration()
# Use just-specified compute target ("cpu-cluster")
aml_run_config.target = compute_target

# Specify CondaDependencies obj, add necessary packages
aml_run_config.environment.python.conda_dependencies = CondaDependencies.create(
    conda_packages=['pandas','scikit-learn'], 
    pip_packages=['azureml-sdk[automl]', 'pyarrow'])

Preparar dados para aprendizado de máquina automatizado

Gravar o código de preparação de dados

O conjunto de dados Titanic de linha de base consiste em dados numéricos e de texto mistos, com alguns valores ausentes. Para prepará-lo para o Machine Learning automatizado, a etapa de pipeline de preparação de dados irá:

  • Preencher dados ausentes com dados aleatórios ou uma categoria correspondente a "desconhecido"
  • Transformar dados categóricos em inteiros
  • Remover colunas que não pretendemos usar
  • Dividir os dados em conjuntos de treinamento e de teste
  • Gravar os dados transformados nos caminhos de saída OutputFileDatasetConfig
%%writefile dataprep.py
from azureml.core import Run

import pandas as pd 
import numpy as np 
import argparse

RANDOM_SEED=42

def prepare_age(df):
    # Fill in missing Age values from distribution of present Age values 
    mean = df["Age"].mean()
    std = df["Age"].std()
    is_null = df["Age"].isnull().sum()
    # compute enough (== is_null().sum()) random numbers between the mean, std
    rand_age = np.random.randint(mean - std, mean + std, size = is_null)
    # fill NaN values in Age column with random values generated
    age_slice = df["Age"].copy()
    age_slice[np.isnan(age_slice)] = rand_age
    df["Age"] = age_slice
    df["Age"] = df["Age"].astype(int)
    
    # Quantize age into 5 classes
    df['Age_Group'] = pd.qcut(df['Age'],5, labels=False)
    df.drop(['Age'], axis=1, inplace=True)
    return df

def prepare_fare(df):
    df['Fare'].fillna(0, inplace=True)
    df['Fare_Group'] = pd.qcut(df['Fare'],5,labels=False)
    df.drop(['Fare'], axis=1, inplace=True)
    return df 

def prepare_genders(df):
    genders = {"male": 0, "female": 1, "unknown": 2}
    df['Sex'] = df['Sex'].map(genders)
    df['Sex'].fillna(2, inplace=True)
    df['Sex'] = df['Sex'].astype(int)
    return df

def prepare_embarked(df):
    df['Embarked'].replace('', 'U', inplace=True)
    df['Embarked'].fillna('U', inplace=True)
    ports = {"S": 0, "C": 1, "Q": 2, "U": 3}
    df['Embarked'] = df['Embarked'].map(ports)
    return df
    
parser = argparse.ArgumentParser()
parser.add_argument('--output_path', dest='output_path', required=True)
args = parser.parse_args()
    
titanic_ds = Run.get_context().input_datasets['titanic_ds']
df = titanic_ds.to_pandas_dataframe().drop(['PassengerId', 'Name', 'Ticket', 'Cabin'], axis=1)
df = prepare_embarked(prepare_genders(prepare_fare(prepare_age(df))))

df.to_csv(os.path.join(args.output_path,"prepped_data.csv"))

print(f"Wrote prepped data to {args.output_path}/prepped_data.csv")

O trecho de código acima é um exemplo completo, mas mínimo, de preparação de dados para os dados de Titanic. O trecho de código começa com um "comando mágico" Jupyter para gerar o código em um arquivo. Se você não estiver usando um notebook Jupyter, remova essa linha e crie o arquivo manualmente.

As várias funções prepare_ no trecho acima modificam a coluna relevante no conjunto de dados de entrada. Essas funções funcionam nos dados depois de serem alterados em um objeto Pandas DataFrame. Em cada caso, os dados ausentes são preenchidos usando dados aleatórios representativos ou dados categóricos que indicam "Desconhecido". Dados categóricos baseados em texto são mapeados para inteiros. Colunas não mais necessárias são substituídas ou descartadas.

Depois que o código define as funções de preparação de dados, o código analisa o argumento de entrada, que é o caminho para o qual desejamos escrever nossos dados. (Esses valores serão determinados por objetos OutputFileDatasetConfig que serão discutidos na próxima etapa). O código recupera o 'titanic_cs'Dataset registrado, converte-o em Pandas DataFrame e chama as várias funções de preparação de dados.

Como o output_path é um diretório, a chamada para to_csv() especifica o nome do arquivo prepped_data.csv.

Gravar a etapa do pipeline de preparação de dados (PythonScriptStep)

O código de preparação de dados descrito acima deve ser associado a um objeto PythonScripStep a ser usado com um pipeline. O caminho para o qual a saída CSV é gravada é gerado por um objeto OutputFileDatasetConfig. Os recursos preparados anteriormente, como ComputeTarget, RunConfig e 'titanic_ds' Dataset são usados para concluir a especificação.

from azureml.data import OutputFileDatasetConfig
from azureml.pipeline.steps import PythonScriptStep

prepped_data_path = OutputFileDatasetConfig(name="output_path")

dataprep_step = PythonScriptStep(
    name="dataprep", 
    script_name="dataprep.py", 
    compute_target=compute_target, 
    runconfig=aml_run_config,
    arguments=["--output_path", prepped_data_path],
    inputs=[titanic_ds.as_named_input('titanic_ds')],
    allow_reuse=True
)

O objeto prepped_data_path é do tipo OutputFileDatasetConfig que aponta para um diretório. Observe que ele está especificado no parâmetro arguments. Se você examinar a etapa anterior, verá que, dentro do código de preparação de dados, o valor do argumento '--output_path' será o caminho do diretório no qual o arquivo CSV foi gravado.

Treinar com o AutoMLStep

A configuração de uma etapa de pipeline de ML automatizada é feita com a classe AutoMLConfig. Essa classe flexível é descrita em Configurar experimentos de ML automatizados no Python. Entrada e saída de dados são os únicos aspectos da configuração que exigem atenção especial em um pipeline de ML. A entrada e a saída para AutoMLConfig em pipelines são discutidas em detalhes abaixo. Além dos dados, uma vantagem dos pipelines de ML é a capacidade de usar diferentes destinos de computação para diferentes etapas. Você pode optar por usar um ComputeTarget mais poderoso somente para o processo de ML automatizado. Fazer isso é tão simples quanto atribuir um RunConfiguration mais poderoso ao parâmetro do AutoMLConfig objeto run_configuration.

Enviar dados para AutoMLStep

Em um pipeline de ML, os dados de entrada devem ser um objeto Dataset. A maneira de desempenho mais alto é fornecer os dados de entrada na forma de objetos OutputTabularDatasetConfig. Você cria um objeto desse tipo com o read_delimited_files() em um OutputFileDatasetConfig, como prepped_data_path, como o objeto prepped_data_path.

# type(prepped_data) == OutputTabularDatasetConfig
prepped_data = prepped_data_path.read_delimited_files()

Outra opção é usar Dataset objetos registrados no espaço de trabalho:

prepped_data = Dataset.get_by_name(ws, 'Data_prepared')

Comparando as duas técnicas:

Técnica Benefícios e desvantagens
OutputTabularDatasetConfig Maior desempenho
Rota natural de OutputFileDatasetConfig
Os dados não são persistidos após a execução do pipeline
Registrado Dataset Desempenho inferior
Pode ser gerado de várias maneiras
Os dados persistem e ficam visíveis em todo o espaço de trabalho
Notebook mostrando Dataset técnica registrada

Especificar saídas de ML automatizadas

As saídas das AutoMLStep são as pontuações de métricas finais do modelo de maior desempenho e o próprio modelo. Para usar essas saídas em etapas de pipeline adicionais, prepare objetos OutputFileDatasetConfig para recebê-las.

from azureml.pipeline.core import TrainingOutput, PipelineData

metrics_data = PipelineData(name='metrics_data',
                            datastore=datastore,
                            pipeline_output_name='metrics_output',
                            training_output=TrainingOutput(type='Metrics'))

model_data = PipelineData(name='best_model_data',
                          datastore=datastore,
                          pipeline_output_name='model_output',
                          training_output=TrainingOutput(type='Model'))

O trecho acima cria os dois objetos PipelineData para as métricas e a saída do modelo. Cada um é nomeado, atribuído ao armazenamento de dados padrão recuperado anteriormente e associado com o específico type de TrainingOutput do AutoMLStep. Como atribuímos pipeline_output_name nesses objetos PipelineData, seus valores estarão disponíveis não apenas da etapa de pipeline individual, mas do pipeline como um todo, como serão discutidos abaixo na seção "Examinar os resultados do pipeline".

Configurar e criar a etapa de pipeline do ML automatizado

Depois que as entradas e saídas são definidas, é hora de criar AutoMLConfig e AutoMLStep. Os detalhes da configuração dependerão de sua tarefa, conforme descrito em Configurar experimentos de ML automatizados em Python. Para a tarefa de classificação de sobrevivência Titanic, o trecho a seguir demonstra uma configuração simples.

from azureml.train.automl import AutoMLConfig
from azureml.pipeline.steps import AutoMLStep

# Change iterations to a reasonable number (50) to get better accuracy
automl_settings = {
    "iteration_timeout_minutes" : 10,
    "iterations" : 2,
    "experiment_timeout_hours" : 0.25,
    "primary_metric" : 'AUC_weighted'
}

automl_config = AutoMLConfig(task = 'classification',
                             path = '.',
                             debug_log = 'automated_ml_errors.log',
                             compute_target = compute_target,
                             run_configuration = aml_run_config,
                             featurization = 'auto',
                             training_data = prepped_data,
                             label_column_name = 'Survived',
                             **automl_settings)

train_step = AutoMLStep(name='AutoML_Classification',
    automl_config=automl_config,
    passthru_automl_config=False,
    outputs=[metrics_data,model_data],
    enable_default_model_output=False,
    enable_default_metrics_output=False,
    allow_reuse=True)

O trecho mostra um idioma comumente usado com AutoMLConfig. Os argumentos que são mais fluidos (hiperparâmetro-ish) são especificados em um dicionário separado, enquanto os valores menos prováveis de serem alterados são especificados diretamente no construtor AutoMLConfig. Nesse caso, automl_settings especifica uma breve execução: a execução será interrompida após apenas 2 iterações ou 15 minutos, o que ocorrer primeiro.

O automl_settings dicionário é passado para o AutoMLConfig Construtor como kwargs. Os outros parâmetros não são complexos:

  • task é definido como classification para este exemplo. Outros valores válidos são regression e forecasting
  • path e debug_log descrevem o caminho para o projeto e um arquivo local no qual as informações de depuração serão gravadas
  • compute_target é o compute_target definido anteriormente que, neste exemplo, é um computador baseado em CPU de baixo custo. Se você estiver usando os recursos de aprendizado profundo do AutoML, você desejaria alterar o destino de computação para ser baseado em GPU
  • featurization é definido como auto. Mais detalhes podem ser encontrados na seção Definição de recursos de dados do documento de configuração do ML automatizado
  • label_column_name indica em qual coluna estamos interessados em prever
  • training_data é definido para os objetos OutputTabularDatasetConfig feitos das saídas da etapa de preparação de dados

O próprio AutoMLStep usa AutoMLConfig e tem, como saídas, os objetosPipelineData criados para manter as métricas e os dados do modelo.

Importante

Você deve definir enable_default_model_output e enable_default_metrics_output para True somente se estiver usando AutoMLStepRun.

Neste exemplo, o processo de ML automatizado executará validações cruzadas no training_data. Você pode controlar o número de validações cruzadas com o argumento n_cross_validations. Se você já tiver dividido os dados de treinamento como parte de suas etapas de preparação de dados, poderá definir validation_data como seu próprio Dataset.

Ocasionalmente, você pode ver o uso X para recursos de dados e y para rótulos de dados. Essa técnica foi preterida e você deve usar training_data para entrada.

Registrar o modelo gerado por ML automatizado

A última etapa em um pipeline ML simples é registrar o modelo criado. Ao adicionar o modelo ao registro de modelo do espaço de trabalho, ele estará disponível no portal e poderá ter controle de versão. Para registrar o modelo, escreva outro PythonScriptStep que receba a saída model_data do AutoMLStep.

Escreva o código para registrar o modelo

Um modelo é registrado em um Workspace. Você provavelmente está familiarizado com o uso de Workspace.from_config() para fazer logon no seu espaço de trabalho no computador local, mas há outra maneira de obter o espaço de trabalho em um pipeline de ML em execução. O Run.get_context() recupera o ativo Run. Este objeto run fornece acesso a muitos objetos importantes, incluindo o Workspace usado aqui.

%%writefile register_model.py
from azureml.core.model import Model, Dataset
from azureml.core.run import Run, _OfflineRun
from azureml.core import Workspace
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("--model_name", required=True)
parser.add_argument("--model_path", required=True)
args = parser.parse_args()

print(f"model_name : {args.model_name}")
print(f"model_path: {args.model_path}")

run = Run.get_context()
ws = Workspace.from_config() if type(run) == _OfflineRun else run.experiment.workspace

model = Model.register(workspace=ws,
                       model_path=args.model_path,
                       model_name=args.model_name)

print("Registered version {0} of model {1}".format(model.version, model.name))

Escrever o código PythonScriptStep

Aviso

Se você estiver usando o SDK v1 do Azure Machine Learning e seu espaço de trabalho estiver configurado para isolamento de rede (VNet), você pode receber um erro ao executar esta etapa. Para mais informações, consulte HyperdriveStep e AutoMLStep falham com o isolamento da rede.

O registro de modelo PythonScriptStep usa um PipelineParameter para um de seus argumentos. Os parâmetros de pipeline são argumentos para pipelines que podem ser facilmente definidos em tempo de envio de execução. Depois de declarado, eles são passados como argumentos normais.


from azureml.pipeline.core.graph import PipelineParameter

# The model name with which to register the trained model in the workspace.
model_name = PipelineParameter("model_name", default_value="TitanicSurvivalInitial")

register_step = PythonScriptStep(script_name="register_model.py",
                                       name="register_model",
                                       allow_reuse=False,
                                       arguments=["--model_name", model_name, "--model_path", model_data],
                                       inputs=[model_data],
                                       compute_target=compute_target,
                                       runconfig=aml_run_config)

Criar e executar seu pipeline de ML automatizado

Criar e executar um pipeline que contém um AutoMLStep não é diferente de um pipeline normal.

from azureml.pipeline.core import Pipeline
from azureml.core import Experiment

pipeline = Pipeline(ws, [dataprep_step, train_step, register_step])

experiment = Experiment(workspace=ws, name='titanic_automl')

run = experiment.submit(pipeline, show_output=True)
run.wait_for_completion()

O código acima combina as etapas de preparação de dados, ML automatizado e registro de modelo em um objeto Pipeline. Em seguida, ele cria um objeto Experiment. O construtor Experiment irá recuperar o experimento nomeado se ele existir ou criá-lo, se necessário. Ele envia o Pipeline para o Experiment, criando um objeto Run que irá executar o pipeline de forma assíncrona. A função wait_for_completion() é bloqueada até que a execução seja concluída.

Examinar os resultados do pipeline

Quando o run for concluído, você poderá recuperar objetos PipelineData que foram atribuídos a um pipeline_output_name. Você pode baixar os resultados e carregá-los para processamento adicional.

metrics_output_port = run.get_pipeline_output('metrics_output')
model_output_port = run.get_pipeline_output('model_output')

metrics_output_port.download('.', show_progress=True)
model_output_port.download('.', show_progress=True)

Os arquivos baixados são gravados no subdiretório azureml/{run.id}/. O arquivo de métricas é formatado em JSON e pode ser convertido em um dataframe do Pandas para exame.

Para o processamento local, talvez seja necessário instalar pacotes relevantes, como o Pandas, o Pickle, o SDK do Azure Machine Learning etc. Neste exemplo, é provável que o melhor modelo encontrado por ML automatizado dependerá do XGBoost.

!pip install xgboost==0.90
import pandas as pd
import json

metrics_filename = metrics_output._path_on_datastore
# metrics_filename = path to downloaded file
with open(metrics_filename) as f:
   metrics_output_result = f.read()
   
deserialized_metrics_output = json.loads(metrics_output_result)
df = pd.DataFrame(deserialized_metrics_output)
df

O trecho de código acima mostra o arquivo de métricas que está sendo carregado de seu local no armazenamento de dados do Azure. Você também pode carregá-lo do arquivo baixado, conforme mostrado no comentário. Depois de desserializá-lo e convertê-lo em um dataframe do Pandas, você poderá ver métricas detalhadas para cada uma das iterações da etapa de ML automatizada.

O arquivo de modelo pode ser desserializado em um objeto Model que você pode usar para inferência, análise de métricas adicionais e assim por diante.

import pickle

model_filename = model_output._path_on_datastore
# model_filename = path to downloaded file

with open(model_filename, "rb" ) as f:
    best_model = pickle.load(f)

# ... inferencing code not shown ...

Para obter mais informações sobre como carregar e trabalhar com modelos existentes, consulte Usar um modelo existente com Azure Machine Learning.

Baixar os resultados de uma execução de ML automatizada

Se estiver acompanhando o artigo, você terá um objeto instanciado run. Mas você também pode recuperar objetos concluídos Run do Workspace por meio de um objeto Experiment.

O espaço de trabalho contém um registro completo de todos os experimentos e execuções. Você pode usar o portal para localizar e baixar as saídas de experimentos ou usar o código. Para acessar os registros de uma execução histórica, use Azure Machine Learning para localizar a ID da execução em que você está interessado. Com essa ID, você pode escolher o específico run por meio do Workspace e do Experiment.

# Retrieved from Azure Machine Learning web UI
run_id = 'aaaaaaaa-bbbb-cccc-dddd-0123456789AB'
experiment = ws.experiments['titanic_automl']
run = next(run for run in ex.get_runs() if run.id == run_id)

Você precisaria alterar as cadeias de caracteres no código acima para as especificidades de sua execução histórica. O trecho acima pressupõe que você tenha atribuído ws ao relevante Workspace com o normal from_config(). O experimento de interesse é recuperado diretamente e, em seguida, o código encontra o Run de interesse correspondendo ao valorrun.id.

Depois de ter um objeto Run, você pode baixar as métricas e o modelo.

automl_run = next(r for r in run.get_children() if r.name == 'AutoML_Classification')
outputs = automl_run.get_outputs()
metrics = outputs['default_metrics_AutoML_Classification']
model = outputs['default_model_AutoML_Classification']

metrics.get_port_data_reference().download('.')
model.get_port_data_reference().download('.')

Cada objeto Run contém objetos StepRun que contêm informações sobre a execução da etapa de pipeline individual. O run é pesquisado para o objeto StepRun para o AutoMLStep. As métricas e o modelo são recuperados usando seus nomes padrão, que estão disponíveis mesmo se você não passar objetos PipelineData para o parâmetro outputs do AutoMLStep.

Por fim, as métricas e o modelo reais são baixados em seu computador local, como foi discutido na seção "Examinar resultados do pipeline" acima.

Próximas etapas