Treinando Graph Neural Networks utilizando a Vertex AI

Luciano Martins
google-cloud-brasil
20 min readApr 14, 2023

--

Introdução

Sou um grande fã de estruturas de dados — em especial, de grafos e de todo o seu potencial de representação de dados do mundo real. Há algumas semanas, estava revisando conteúdo de grafos e postei a foto abaixo em uma rede social:

livros sobre teoria de grafos e redes neurais de grafos

Curiosamente, descobri uma guilda de estudantes e pesquisadores de grafos, que ficaram bastante interessados sobre o conteúdo de se utilizar grafos junto com aprendizado de máquina. Isso me trouxe uma dúvida: Será que há mais pessoas interessadas em conteúdos de machine learning que não sejam aqueles tópicos que estão mais na moda?

Partindo dessa dúvida, fiz uma pesquisa em uma rede social: quais conteúdos seriam interessantes para ter mais conteúdo técnico em português? O resultado acabou em um empate — e eu segui pela área de conforto técnico pessoal para construir essa postagem 🙂

pesquisa sobre interesse de temas para conteúdos de machine learning em português

Uma introdução a grafos

Importante: Se você já conhece ou utiliza grafos em seus estudos, pesquisas ou trabalho, essa sessão pode ser “mais do mesmo” para você. Também, esta postagem não é uma referência completa de grafos, então nem todas as propriedades de grafos estarão presentes aqui.

Mas o que são grafos? Numa conceituação livre, grafos são estruturas de dados onde representamos as informações e suas possíveis correlações através de um conjunto de de vértices (ou nós) e suas arestas (ou conexões). Podemos intuir como::

G = (V, A), onde:

G → o grafo em questão
V → o conjunto de vértices que compõe o grafo
A → o conjunto de arestas (ou conexões) — basicamente, pares ordenados a = (v,w), onde v e w ∈ V

Na prática, se considerarmos o grafo (não direcionado) abaixo:

grafo de exemplo, com três vértices e três arestas

Teremos:

V(G) = {A, B, C}
A(G) = {(A,B), (A,C), (C,B)

Se pensamos em uma representação em grafos, temos que considerar propriedades como:

a) Grafo não direcionado: quando a conexão entre dois vértices não possui direção: (A,B) = (B,A)

b) Grafo direcionado: quando há uma direção na conexão — ou seja, (A,B) ≠ (B,A)

c) Grafo completo: quanto há conexões entre todos os nós que compõe o grafo

d) Grafo valorado: quando há valores (ou pesos) em diferentes conexões

e) Multigrafo: quando há mais de uma conexão direta e possível entre dois vértices

Ainda, dependendo do tipo de dado que estamos representando na forma de grafos, podemos ter estruturas menos triviais, como grafos heterogêneos (quando diferentes tipos de entidades são representadas nos vértices do grafo), grafos k-partidos (como bipartidos, tripartidos, etc), onde podemos representar o mesmo grafo G em k diferentes conjuntos de vértices (V1, V2, … , Vk) e que cada aresta do conjunto E possua uma ponta em um conjunto de vértices diferente.

Visualizando estes conceitos:

exemplo de um multigrafo heterogêneo, direcoinado e valorado

Este grafo é:

  • Heterogêneo (há diferentes tipos de entidades nos vértices)
  • Multigrafo (há vértices com múltiplas conexões entre si)
  • Direcionado (há um sentido nas conexões entre os vértices
  • Valorado (há valores, ou pesos, em suas conexões)

Com essas características, e utilizando técnicas de como “percorrer” o grafo (ou seja, como traçar caminhos entre as arestas para cumprir algum objetivo — o caminho mais curto, ou o caminho com menor custo de peso, ou o caminho que percorra mais nós, etc) e com a utilização de atributos adicionais (sejam atributos das arestas ou de suas conexões), conseguimos utilizar e gerar valor de cenários não triviais e mais complexos.

Assim, conseguimos representar, sem perda de informações ou contexto, cenários como o hipotético abaixo — onde temos um grafo direcionado que representa transações financeiras, onde há vértices que representam os clientes, vértices que representam as máquinas de cartão — assim como seus atributos: tanto das pessoas (como idade e gênero), das máquinas de cartão (latitude, longitude, % de taxa das transações) e das arestas entre eles (valor da transação e se é débito ou crédito):

Para uma introdução mais formal e completa da teoria dos grafos, recomendo a aula das professoras Maria Cristina, Roseane e Josiane Bueno, do Instituto de Ciências Matemáticas e da Computação (ICMC) da USP São Carlos, que está disponibilizada no site do ICMC. Uma outra excelente referência sobre a utilização de grafos para a análise de cenários complexos é o livro Network Science, do professor Albert-László Barabási (disponível, gratuitamente, online).

Dados estruturados em forma de grafos

Com esse entendimento inicial sobre grafos, agora vamos imaginar como interpretar informações na forma de grafos. Mas para chegarmos nesta intuição, podemos nos perguntar: porquê utilizar grafos para representar dados? Bom, para isso, temos que pensar como os dados são organizados.

Aqui precisamos abraçar mais um fundamento matemático de um dos maiores matemáticos de todos os tempos — Euclides. Euclides é considerado um dos pais da geometria e, com base em seus estudos, leis e postulados, grande parte da organização que fazemos de informações acontece no que chamamos de Espaços Euclidianos (“espaços vetoriais reais e de dimensão finita”) onde podemos manipular vetores ou matrizes numéricas (reais) de múltiplas dimensões. Então, quando pegamos uma informação não estruturada e a representamos numericamente, em outras palavras, estamos tendo esta representação em um espaço euclidiano.

Exemplo, se considerarmos esta imagem (chamando localmente de “imagem.png”):

imagem de exemplo

E a lermos utilizando alguma biblioteca python, por exemplo — como abaixo:

import imageio as iio
img = iio.imread('/home/lucianomartins/Downloads/imagem.png')
print(img.shape)
print(img)
Temos como saída:
(45, 43, 3)
[[[255 255 255]
[255 255 255]
[255 255 255]

// várias linhas removidas aqui, pra facilitar a visualização

[255 255 255]
[255 255 255]
[242 245 253]]]

Ou seja, a imagem está representada como uma matriz de três dimensões (uma para cada camada de cor — pensando em RGB), com cada dimensão possuindo 45 linhas e 43 colunas. Com esta mesma lógica conseguimos representar dados estruturados (como leituras de sensores IoT, ou resultados de uma query SQL) e também outros dados não estruturados (além de imagens, áudio, textos, etc).

E onde esta estrutura, baseada nos conceitos de geometria euclidiana, nos deixa limitados? Em dados do mundo real, algumas complexidades de relacionamentos entre entidades não são facilmente — Por exemplo, representar a sua rede social de amigos, ou a malha rodoviária do Brasil, ou as ligações intermoleculares em um estudo de novos remédios.

Fonte: Ministério da Infraestrutura do Brasil

E, como muitos de vocês sabem (ou, pelo menos, estudaram em graduação, ou em preparação para entrevistas de código em empresas), existem muitas formas de explorarmos relacionamentos e extrairmos insights de grafos. Alguns exemplos (como uma lista não exaustiva):

  • Algoritmos de busca em grafo (como os famosos BFS e DFS)
  • Busca de caminhos mais curtos entre vértices (como o Dijkstra)
  • Cenários de spanning tree no grafo (como o algoritmo de Prim ou de Kruskal)

E o universo de áreas de pesquisa que utilizam grafos para interpretar cenários complexos é imensa. Deixo aqui alguns exemplos de coisas do nosso cotidiano (próximas a nós ou não) que só são possíveis graças aos grafos e análises de redes:

- Spread of Epidemic Disease on Edge-Weighted Graphs from a Database: A Case Study of COVID-19
- Integrating T cell receptor sequences and transcriptional profiles by clonotype neighbor graph analysis
- The anatomy of a large-scale hypertextual Web search engine

O que são redes neurais de grafo?

Com o entendimento de grafos, sua estrutura, características e aplicações práticas, seguimos para o próximo passo deste artigo: as redes neurais de grafo.

Começando com uma conceituação muito simples: uma rede neural de grafo é uma rede que recebe como dados de entrada informações organizadas numa estrutura de grafo. A grande vantagem desta abordagem é que, como revisamos anteriormente, muitas informações do “mundo real” possuem uma estrutura complexa e, para que não haja perda de detalhes destas informações, elas são melhor representadas por grafos — Assim, quando conseguimos ingerir os grafos de informações diretamente na rede neural, conseguimos manter o máximo possível dos detalhes e complexidade dos dados envolvidos.

A área de pesquisa de redes neurais de grafos é bem estabelecida e já existe há um certo tempo. Um dos primeiros estudos, A new model for learning in graph domains, foi publicado na IEEE em 2005.

Uma rede neural de grafo (ou Graph Neural Network, GNN) trabalha com vetores numéricos para cada vértice do grafo, chamado de estado do vértice (ou node state) — muito similar ao vetor de ativação de neurônios numa rede neural “clássica”. A transformação de dados de entrada dos vértices varia de acordo com a implementação da GNN — indo desde normalização e encoding (como em qualquer rede neural), até operações mais sofisticadas (como por exemplo a criação de embeddings de textos ou imagens).

Uma visão de uma rede neural convolucional de grafo (Graph Convolutional Network, GCN)

A essência da implementação de GNN gira em torno da ideia de ter ciclos de atualização desses node states, através de uma função de treinamento aplicada aos node states de cada vertex e de seus vizinhos. Há diversas formas na literatura de redes neurais de grafo, que podemos realizar essa atividade: como passagem de mensagens (message passing) entre as arestas existentes entre dois vértices; convoluções de grafo (graph convolutions) que conseguem enfatizar o uso dos mesmos pesos de treinamento em diversos vértices do grafo; ou ainda graph nets (disponível no github da DeepMind) onde se busca generalizar a manipulação de estados não só para vértices, mas para arestas e para o gráfico como um todo.

Mas que cenários lidamos com redes neurais de grafo?

Deixando um pouco de lado a representação do dado em si — quais cenários de machine learning podem ser implementados utilizando redes neurais de grafo? Diversos. Mas em uma visão simplificada, podemos agrupar os cenários de soluções com redes neurais de grafo em três grandes categorias:

1) classificação de vértices (ou node classification): Quando buscamos predizer a classe ou grupo que faz parte um determinado novo vértice. Um cenário do cotidiano que pode ser abordado desta forma é, por exemplo, dada uma rede de transações financeiras (lojas, ou máquinas de cartão, etc), identificar entidades potencialmente fraudulentas.

um exemplo visual de classificação de vértices

2) predição de arestas (ou link prediction): Quando buscamos predizer qual a probabilidade de dois vértices se conectarem. Este é o cenário mais comum que vivemos como usuários de redes sociais: quando o “motor de recomendação” da rede sugere novas conexões com pessoas (que não, necessariamente, conhecemos).

um exemplo de um cenário de predição de arestas

3) classificação do grafo (ou graph classification): Quando buscamos classificar um dado grafo em diferentes categorias pré-determinadas — de forma similar ao que fazemos, por exemplo, em classificação de imagens em visão computacional. Fazemos isso, por exemplo, quando queremos classificar uma molécula (representada em formato de grafo) como tóxica para humanos ou não.

um exemplo de classificação de grafos

A partir desta visão mais “generalista” das possibilidades, há diversas abordagens de implementação e não caberiam nesta postagem. Mas podemos ver um exemplo bem legal abaixo (parte deste artigo), onde a partir de uma descrição de uma cena representada em um grafo, conseguimos gerar uma imagem contextualmente fiel à descrição inicial:

Imagens geradas a partir de cenas em grafos — J. Johnson, A. Gupta, and L. Fei-Fei, “Image generation from scene graphs,” in Proc. of CVPR, 2018

Desenvolvendo o seu modelo de Machine Learning com dados em grafo

Agora vamos verificar isso tudo na prática. Como será que se desenvolve, treina e utiliza um modelo de redes neurais de grafo?

Para exercitar isso tudo, vamos criar um modelo de classificação de uma rede neural de grafo onde utilizaremos o dataset chamado Cora —é um dataset com dados de artigos científicos de computação incluindo suas referências cruzadas (ou qual artigo cita qual artigo). Ainda, para cada artigo há uma definição de qual "classe" (ou tópico de computação) esse artigo faz parte; podendo ser dos tipos: Neural_Networks, Probabilistic_Methods, Genetic_Algorithms, Theory, Case_Based, Reinforcement_Learning Rule_Learning.

O que buscamos é criar uma rede neural de classificação de arestas — ou seja, dado um deterNeste cenário, vamos:

  • visualizar os dados deste dataset
  • preparar os dados para serem ingeridos em uma rede neural de grafo
  • dividir o dataset em dados de treinamento e dados de teste
  • treinar e avaliar o modelo
  • validar a qualidade do modelo
  • implementar o modelo em uma API REST para realizar inferências

Criando uma instância de desenvolvimento na Vertex AI

Primeiramente vamos criar o nosso ambiente de desenvolvimento. Para isso, vamos usar a plataforma de desenvolvimento de machine learning na Google Cloud, chamada Vertex AI.

Para abrir a Vertex AI, acesse https://console.cloud.google.com e, na barra de busca, digite “Vertex AI”:

acessando o serviço da Vertex AI na Google Cloud

Quando já estiver na console da Vertex AI, clica no serviço chamado Workbench:

acessando o "Workbench" dentro da Vertex AI

No Workbench, podemos criar as máquinas virtuais onde iremos criar todo o nosso código e onde iremos interagir com as API da Vertex AI para acessar dados para a criação de datasets, treinar modelos e, até mesmo, produtizar modelos, servidos em API, para utilização por outros desenvolvedores.

Neste cenário, para criar nossa máquina virtual de desenvolvimento, devemos clicar em “New Notebook”:

iniciando a criação de uma nova instância de notebook na Vertex AI Workbench

Aqui no meu ambiente optei por uma máquina virtual já pré-configurada com o TensorFlow versão 2.10 sem nenhuma GPU conectada:

selecionando a configuração da instância de Vertex AI Workbench

Na nova tela que aparece, apenas configurei o nome da minha máquina virtual (que aqui chamei de “lab-grafos”) e cliquei em “Create”:

configurações da nova instância de Vertex AI Workbench

Depois de alguns segundos, sua instância de desenvolvimento estará pronta e iniciada. Basta clicar em “Open JupyterLab” e começar a desenvolver:

validando que a nossa instância de Vertex AI Workbench está em execução com sucesso

Considerando que você tem uma instância de Workbench com a mesma versão de TensorFlow que estou utilizando aqui (2.10), é importante validar que você tenha os seguintes módulos:

## modulos necessários
!pip install pandas numpy networkx matplotlib

Estou rodando com as seguintes versões:

(base) jupyter@lab-grafos:~$ python3
Python 3.7.12 | packaged by conda-forge | (default, Oct 26 2021, 06:08:21)
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import tensorflow as tf
>>> import pandas as pd
>>> import numpy as np
>>> import networkx as nx
im>>> import matplotlib as mpl
>>> print(tf.__version__)
2.10.1
>>> print(pd.__version__)
1.3.5
>>> print(np.__version__)
1.21.6
>>> print(nx.__version__)
2.6.3
>>> print(mpl.__version__)
3.5.3

Nas próximas sessões aqui podem ser verificados alguns snippets de código que foram blocos importantes para a criação do modelo final desenvolvido nesse blog. Por questão de simplicidade de leitura, incluí somente trechos importantes do código e seus resultados. O notebook com o código completo pode ser encontrado no github.

Importando datasets

Vamos realizar o download do dataset:

zip_file = keras.utils.get_file(
fname='cora.tgz',
origin='https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz',
extract=True)

data_dir = os.path.join(os.path.dirname(zip_file), 'cora')

E, depois disso, vamos importar as informações de artigos, citações e:

# artigos
column_names = ['paper_id'] + [f'term_{idx}' for idx in range(1433)] + ['subject']
papers = pd.read_csv(
os.path.join(data_dir, 'cora.content'), sep='\t', header=None, names=column_names,
)
print('shape das informações dos papers:', papers.shape)

# citações
citations = pd.read_csv(
os.path.join(data_dir, 'cora.cites'),
sep='\t',
header=None,
names=['target', 'source'],
)
print('shape da estrutura de citações:', citations.shape)

Após isso, podemos estruturar os dados para visualizarmos em formato de grafo:

# organização dos dados do dataset
class_values = sorted(papers['subject'].unique())
class_idx = {name: id for id, name in enumerate(class_values)}
paper_idx = {name: idx for idx, name in enumerate(sorted(papers['paper_id'].unique()))}

papers['paper_id'] = papers['paper_id'].apply(lambda name: paper_idx[name])
citations['source'] = citations['source'].apply(lambda name: paper_idx[name])
citations['target'] = citations['target'].apply(lambda name: paper_idx[name])
papers['subject'] = papers['subject'].apply(lambda value: class_idx[value])

Agora podemos, inclusive, verificar o dadaset em formato de grafo:

# visualizando o Cora dataset
plt.figure(figsize=(8, 8))
colors = papers['subject'].tolist() # aqui definimos a quantidade de "cores" do grafo, baseado nos tipos de papers
cora_graph = nx.from_pandas_edgelist(citations.sample(n=1500)) # aqui criamos um grafo de exemplo, com uma amostra de 1500 citações
subjects = list(papers[papers['paper_id'].isin(list(cora_graph.nodes))]['subject'])
nx.draw_spring(cora_graph, node_size=15, node_color=subjects)
visualização do Cora Dataset

Preparando os dados para serem usados pelo seu modelo

Com os dados importados, precisamos preparar esses dados para serem usados por um modelo de machine learning. Esta requisito não é diferente do que faríamos em dados que não estivessem em formato de grafo.

train_data, test_data = [], []

# realiza o split dos dados em treinamento e testes
for _, group_data in papers.groupby('subject'):
random_selection = np.random.rand(len(group_data.index)) <= 0.75
train_data.append(group_data[random_selection])
test_data.append(group_data[~random_selection])

train_data = pd.concat(train_data).sample(frac=1)
test_data = pd.concat(test_data).sample(frac=1)

print('shape do dataset treinamento:', train_data.shape)
print('shape do dataset teste:', test_data.shape)
print()

# informações sobre as features dos dataset
feature_names = set(papers.columns) - {'paper_id', 'subject'}
num_features = len(feature_names)
num_classes = len(class_idx)

# converte os datasets em arrays numpy
x_train = train_data[feature_names].to_numpy()
x_test = test_data[feature_names].to_numpy()

# converte os arrays das classes target em arrays numpy
y_train = train_data['subject']
y_test = test_data['subject']

# define a matrix de adjacência de shape [2, num_edges]
edges = citations[['source', 'target']].to_numpy().T

# define um array de 1's como um array de pesos
edge_weights = tf.ones(shape=edges.shape[1])

# cria o array de features dos vértices de shape [num_nodes, num_features]
node_features = tf.cast(
papers.sort_values('paper_id')[feature_names].to_numpy(), dtype=tf.dtypes.float32
)
# cria a tupla de informaçoes do grafo do dataset
graph_info = (node_features, edges, edge_weights)

Treinando seu modelo

Como estamos utilizando a Vertex AI, ela será responsável por simplificar toda a nossa infraestrutura — então, não é necessário nenhum trabalho manual de configuração de frameworks de machine learning, alocação de recursos (como CPU e memória), configuração de aceleradores, etc.

Para visualizar melhor o que acontece nesse treinamento, coloquei detalhes de como seria o treinamento manual do modelo no jupyter notebook disponível no github.

Para preparar o treinamento do modelo, precisamos preparar o ambiente de desenvolvimento — incluindo a definição de projeto da cloud, região e área de armazenamento (no serviço Cloud Storage).

# definição do projeto a ser utilizado
shell_output = ! gcloud config list --format 'value(core.project)' 2>/dev/null
PROJECT_ID = shell_output[0]
print("Project ID:", PROJECT_ID)

# definição da região onde o código será executado
REGION = "us-central1"

if REGION == "[your-region]":
REGION = "us-central1"

# configuração do Cloud Storage bucket que será utilizado
from google.cloud import storage
from google.cloud.storage import Bucket

BUCKET_NAME = "vertex-gnn-lab"
BUCKET_URI = f"gs://{BUCKET_NAME}"

client = storage.Client()
bucket = client.bucket(BUCKET_NAME)

if not bucket.exists():
bucket.create(location='us-central1')
del bucket, client
print('Criação de bucket realizada com sucesso. Bucket %s criado com sucesso' % BUCKET_NAME)
else:
print('Criação de bucket cancelada. Bucket %s já existe' % BUCKET_NAME)

# instanciamento do cliente da Vertex AI SDK
aiplatform.init(project=PROJECT_ID, location=REGION, staging_bucket=BUCKET_URI)

# preparando os diretórios locais que serão utilizados no processo
APPLICATION_DIR = "classificadorgnn"
TRAINER_DIR = f"{APPLICATION_DIR}/trainer"
PREDICTION_DIR = f"{APPLICATION_DIR}/prediction"
!mkdir -p $APPLICATION_DIR
!mkdir -p $TRAINER_DIR
!mkdir -p $PREDICTION_DIR

Para realizar o treinamento do modelo, vamos criar um arquivo em Python com todo o código necessário para baixar e preparar o dataset que utilizaremos (o Cora Dataset):

%%writefile {TRAINER_DIR}/dataset.py

import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
<...>

Também criaremos um arquivo que faça a definição do modelo, em TensorFlow, para o nosso modelo:

%%writefile {TRAINER_DIR}/model.py

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
<...>

E um arquivo que importe os dois primeiros (dataset.py e model.py) e realize as etapas de treinamento em si:

%%writefile {TRAINER_DIR}/train.py

import pickle
from google.cloud import storage

from trainer.dataset import *
from trainer.model import *
<...>

Com estes arquivos criados, o próximo passo é criar um Dockerfile que irá gerar a imagem de container que irá ser utilizada no treinamento do modelo:

%%writefile {APPLICATION_DIR}/Dockerfile

FROM gcr.io/deeplearning-platform-release/tf2-cpu.2-10

WORKDIR /

# copia o código de treinamento para dentro da imagem
COPY trainer /trainer

# configura o ENTRYPOINT do container
ENTRYPOINT ["python", "-m", "trainer.train"]

E, utilizando o serviços serviços de Google Cloud (Cloud Build para realizar a build do container e Artifacts Registry para armazenar a imagem), vamos ter o container para treinamento criado:

REPO_NAME_TRAINING='classificador-gnn-repo'

## as duas linhas abaixo só precisam ser executadas uma vez para este cenário
!gcloud artifacts repositories create $REPO_NAME_TRAINING --repository-format=docker --location=$REGION --description="Repositorio de treinamento de GNN"
!gcloud auth configure-docker {REGION}-docker.pkg.dev --quiet

# build da imagem de container
IMAGE_URI = (f"{REGION}-docker.pkg.dev/{PROJECT_ID}/{REPO_NAME_TRAINING}/classificador-gnn:latest")
! cd $APPLICATION_DIR; docker build ./ -t $IMAGE_URI; docker push $IMAGE_URI

Finalmente, utilizando a imagem de container criada, podemos iniciar o treinamento do modelo utilizando a Vertex AI. Primeiro criamos as instruções de treinamento, onde informaremos o tamanho da máquina de treinamento e qual imagem irá ser utilizada:

# instruções de infraestrutura para o treinamento
worker_pool_specs = [
{
"machine_spec": {
"machine_type": "n1-standard-8",
},
"replica_count": 1,
"container_spec": {
"image_uri": f"{REGION}-docker.pkg.dev/{PROJECT_ID}/{REPO_NAME_TRAINING}/classificador-gnn:latest"
},
}
]

my_custom_job = aiplatform.CustomJob(
display_name="classificador-gnn-job",
worker_pool_specs=worker_pool_specs,
staging_bucket=BUCKET_URI
)

# execução do treinamento
model = my_custom_job.run(sync=True)

<...>
<...>

CustomJob projects/48397268769/locations/us-central1/customJobs/9184012131920510976 current state:
JobState.JOB_STATE_RUNNING
CustomJob projects/48397268769/locations/us-central1/customJobs/9184012131920510976 current state:
JobState.JOB_STATE_SUCCEEDED
CustomJob run completed. Resource name: projects/48397268769/locations/us-central1/customJobs/9184012131920510976

Com o treinamento finalizado, podemos ver nos logs do treinamento todos os detalhes do treinamento, inclusive a avaliação do modelo que foi incluída ao final do código de treinamento (os logs podem ser encontrados na console da Vertex AI):

logs do treinamento de modelos na Vertex AI

Produtizando o modelo e realizando inferências

Que legal! Mas e agora? Bom, costuma-se dizer que "não existe machine learning se não vai pra produção", né? Ou seja, de que adianta termos um modelo que ficou bom, mas que não se tornou, além de útil, utilizável.

Como se trata de um cenário mais específico, onde usamos informações representadas em forma de grafos, é importante criarmos um container que realize as etapas que inferência necessárias para um modelo de grafo.

Felizmente, a Vertex AI apresenta um facilitador para esse processo chamado Custom Prediction Routines (CPR). Com as CPR você se preocupa só com o seu código de interação com o modelo e a Vertex AI ficará responsável por toda a parte de infraestrutura do container — imagem base, build do container, etc.

uma visão geral da arquitetura das Custom Prediction Routines da Vertex AI

Para utilizar as CPR, assim como fizemos para o treinamento, precisamos criar um arquivo Python que contenha todas as etapas da nossa predição — com alguns módulos requeridos e algumas funções que precisam existir. O início do nosso arquivo ficou assim:

%%writefile {PREDICTION_DIR}/predictor.py

import pickle
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from google.cloud import storage

# modulos requeridos pelas CPR
from google.cloud.aiplatform.utils import prediction_utils
from google.cloud.aiplatform.prediction.predictor import Predictor

<...>

Dentro desse mesmo arquivo, criamos as funções que realizarão a importação do modelo que treinamos com a Vertex AI, o pré-processamento dos dados enviados para inferência, a inferência em si e o retorno dos resultados:

class CprPredictor(Predictor):


# etapa de carregamento do modelo
def load(self, artifacts_uri):
"""Loads the model artifacts."""
prediction_utils.download_model_artifacts(artifacts_uri)
self._model = tf.keras.models.load_model(filepath='.',
custom_objects={
'GraphConvLayer': GraphConvLayer,
'GNNNodeClassifier': GNNNodeClassifier,
'graph_info': graph_info
})


# etapa de pré-processamento dos dados de inferência
def preprocess(self, prediction_input):
"""Prepare data for prediction."""
# dados de inferência
infer_data_path, new_edges, new_node_indices = prediction_input["instances"]
blob = bucket.blob(infer_data_path)
blob.download_to_filename('new_node_features.pickle')
localfile = open('new_node_features.pickle', 'rb')
new_node_features = pickle.load(localfile)
new_edges = np.asarray(new_edges)
return(new_node_features, new_edges, new_node_indices)


# etapa da predição em si
def predict(self, new_data):
"""Performs prediction."""
new_node_features, new_edges, new_node_indices = new_data
self._model.node_features = new_node_features
self._model.edges = new_edges
self._model.edge_weights = tf.ones(shape=new_edges.shape[1])
logits = self._model.predict([new_node_indices])
probabilities = keras.activations.softmax(tf.convert_to_tensor(logits)).numpy()
return([new_node_indices, probabilities.tolist()[0]])


# retorno dos resultados da predição
def postprocess(self, prediction_results):
return {"predictions": prediction_results}

Diferente da etapa de treinamento, aqui não realizaremos a criação de uma Dockerfile nem precisaremos nos preocupar com a criação de uma API REST no container ou o build desse container. A SDK da Vertex AI fará todo esse processo para nós:

import os
from google.cloud.aiplatform.prediction import LocalModel
from classificadorgnn.prediction.predictor import CprPredictor

REPO_NAME_SERVING='predictor-gnn-repo'

local_model = LocalModel.build_cpr_model(
src_dir=PREDICTION_DIR,
output_image_uri=f'{REGION}-docker.pkg.dev/{PROJECT_ID}/{REPO_NAME_SERVING}/predictor-gnn:latest',
predictor=CprPredictor,
requirements_path=os.path.join(PREDICTION_DIR, 'requirements.txt'))

Com a imagem de predição criada pelas CPR, agora podemos para a nossa registry de modelos (neste caso, a Model Registry da Vertex AI) e informando que a API com o modelo publicado será implantada utilizando a imagem que customizamos com as CPR:

model = aiplatform.Model.upload(
display_name='modelo-classificador-gnn',
artifact_uri=model_artifact[0] + 'model',
serving_container_image_uri=f'{REGION}-docker.pkg.dev/{PROJECT_ID}/{REPO_NAME_SERVING}/predictor-gnn:latest')

Com isso, estamos prontos para publicar o nosso modelo em uma API. Para isso, utilizaremos outra funcionalidade da Vertex AI, os Endpoints, que servem para, além de publicar o seu modelo dentro do seu perímetro de segurança da nuvem, também possui mecanismos de autoscaling da API do modelo, diminuindo a chance de indisponibilidades em caso de aumento abrupto de requisições.

A criação de um endpoint é muito simples — neste exemplo, definimos que nosso endpoint irá ser executado em uma configuração de máquinas n1-standard-8 e a configuração de autoscaling leva em consideração que sempre haverá, ao menos, uma instância executando a API do model (min_replicas_count=1) e a escalará até cinco instâncias em execução em paralelo (max_replicas_count=5). Além disso, por motivos de segurança, foi realizada a criação de uma service account específica para inferência — para garantir que a API do modelo só tenha acesso a recursos da Google Cloud que sejam explicitamente atribuídos.

endpoint = model.deploy(machine_type="n1-standard-8",
min_replica_count=1,
max_replica_count=5,
service_account='custom-gnn-sa@[...].gserviceaccount.com'
)

Após essa ação, poderemos ver na console da Vertex AI que o endpoint estará online e com um modelo implementado:

o endpoint implantado sendo verificado como online

Podemos ver também qual modelo está implantado no endpoint, clicando sobre o nome do endpoint:

verificando o modelo implantado dentro do endpoint

Com isso, temos a rede neural de grafo disponível em uma API REST que agora é acessível por todas as pessoas com acesso — não ficando limitado a times ou pessoas que realizam pesquisa e desenvolvimento de machine learning.

Utilizando a API com o modelo treinado implantado

Finalmente, podemos interagir com a API do nosso modelo de grafo e realizar inferências — O que torna um modelo de machine learning realmente util no mundo real.

Para realizar inferências, vamos simular o que aconteceria no mundo produtivo. Primeiro vamos criar dados sintéticos que representem novos dados do cenário — Em outras palavras, vamos criar novas informações de artigos científicos, incluindo citações entre esses novos artigos e os anteriores. Após isso, vamos enviar essas novas informações à API do nosso modelo e pediremos para que o nosso modelo classifique esses novos artigos dentro das possíveis classes (lembram? Neural_Networks, Probabilistic_Methods, Genetic_Algorithms, Theory, Case_Based, Reinforcement_Learning Rule_Learning):

# primeiro criamos os novos artigos fictícios
new_instances = generate_random_instances(num_classes)
new_node_features = np.concatenate([node_features, new_instances])

# depois criamos as novas citações entre os artigos
num_nodes = node_features.shape[0]
new_node_indices = [i + num_nodes for i in range(num_classes)]
new_citations = []
for subject_idx, group in papers.groupby("subject"):
subject_papers = list(group.paper_id)
# Select random x papers specific subject.
selected_paper_indices1 = np.random.choice(subject_papers, 5)
# Select random y papers from any subject (where y < x).
selected_paper_indices2 = np.random.choice(list(papers.paper_id), 2)
# Merge the selected paper indices.
selected_paper_indices = np.concatenate(
[selected_paper_indices1, selected_paper_indices2], axis=0
)
# Create edges between a citing paper idx and the selected cited papers.
citing_paper_indx = new_node_indices[subject_idx]
for cited_paper_idx in selected_paper_indices:
new_citations.append([citing_paper_indx, cited_paper_idx])

# preparamos essas novas informações
# como novas citações e novas arestas para o grafo
new_citations = np.array(new_citations).T
new_edges = np.concatenate([edges, new_citations], axis=1)

E, com estas novas informações, podemos fazer uma solicitação de inferência ao nosso modelo:

results = endpoint.predict(instances=[infer_data_path, 
new_edges.tolist(),
new_node_indices])

Para simplificar a interpretação desses resultados, podemos criar uma função que irá processar as respostas:

def display_class_probabilities(probabilities):
for instance_idx, probs in enumerate(probabilities):
print(f"Instance {instance_idx + 1}:")
for class_idx, prob in enumerate(probs):
print(f"- {class_values[class_idx]}: {round(prob * 100, 2)}%")

display_class_probabilities(results[0][1])

E podemos ver como o nosso modelo classificou cada um dos novos artigos:

Instance 1:
- Case_Based: 0.26%
- Genetic_Algorithms: 0.31%
- Neural_Networks: 80.14%
- Probabilistic_Methods: 1.17%
- Reinforcement_Learning: 0.99%
- Rule_Learning: 0.07%
- Theory: 17.06%

Instance 2:
- Case_Based: 0.2%
- Genetic_Algorithms: 0.11%
- Neural_Networks: 98.67%
- Probabilistic_Methods: 0.23%
- Reinforcement_Learning: 0.3%
- Rule_Learning: 0.01%
- Theory: 0.48%

Instance 3:
- Case_Based: 0.32%
- Genetic_Algorithms: 0.34%
- Neural_Networks: 97.2%
- Probabilistic_Methods: 0.28%
- Reinforcement_Learning: 0.99%
- Rule_Learning: 0.02%
- Theory: 0.86%

Instance 4:
- Case_Based: 0.15%
- Genetic_Algorithms: 94.22%
- Neural_Networks: 0.47%
- Probabilistic_Methods: 0.01%
- Reinforcement_Learning: 5.09%
- Rule_Learning: 0.01%
- Theory: 0.05%

Instance 5:
- Case_Based: 0.05%
- Genetic_Algorithms: 0.12%
- Neural_Networks: 96.56%
- Probabilistic_Methods: 0.86%
- Reinforcement_Learning: 0.65%
- Rule_Learning: 0.01%
- Theory: 1.74%

Instance 6:
- Case_Based: 0.01%
- Genetic_Algorithms: 0.02%
- Neural_Networks: 99.47%
- Probabilistic_Methods: 0.06%
- Reinforcement_Learning: 0.09%
- Rule_Learning: 0.0%
- Theory: 0.36%

Instance 7:
- Case_Based: 0.9%
- Genetic_Algorithms: 0.16%
- Neural_Networks: 28.22%
- Probabilistic_Methods: 61.46%
- Reinforcement_Learning: 3.04%
- Rule_Learning: 0.41%
- Theory: 5.8%

Conclusão e Próximos Passos

Parabéns!! Você percorreu toda essa longa jornada. Não se preocupe — este não é um cenário trivial mas, ao mesmo tempo, é bom começar a conhecê-lo pois traz muito poder de entedimento do relacionamento de dados mais complexos e não representáveis de uma forma simples.

Como já dito antes, todo o código e o detalhamento das etapas técnicas de treinamento e predição podem ser encontradas no notebook Redes Neurais de Grafo na Vertex AI de Google Cloud.

Você possui um cenário onde grafos podem enriquecer a qualidade de seus dados ou de suas predições? Executou o código deste blog, mas não entendeu bem algo? Sintam-se, também, super bem vindos caso queiram falar mais sobre essa abordagem, entrando em contato comigo nas redes sociais :)

--

--