Machine Learning como uma Função Serverless no Google Cloud Platform

Publicado em dom 10 maio 2020 na categoria tutorial • 13 min read

Introdução

Esse mês nos meus estudos de engenharia de machine learning tropecei em um tópico bem interessante: **machine learning como funções serverless**. Então, como se já não bastasse trabalhar umas 12 horas por dia (é sério), decidi construir uma pipeline BEM simples para o desenvolvimento de um modelo de regressão BEM básico e, em seguida, fazer o deploy deste modelo como uma função serveless na Google Cloud Platform. O resultado foi uma experiência bem interessante que me deu algum know how e um vislumbre de como poderia fazer o deploy de algo assim em produção com um sistema de maior escala. Talvez algum dia eu faça algo mais complexo envolvendo mais peças desse quebra cabeças.

Como sempre, espero que esse post possa dar uma luz para alguém fazendo algo parecido.

TL;DR: criei um modelo de machine learning e fiz o deploy como uma função serverless no GCP.

Objetivos do Post:

Suponhamos que você queira colocar seu apartamento para alugar, mas não sabe exatamente qual preço colocar no aluguel. Para isso você começa a procurar na internet por apartamentos mais ou menos igual ao seu na sua cidade e tenta tirar a média para gerar seu próprio valor de aluguel, cansativo, não?

Ia ser bem legal ter um modelo para tentar predizer o valor ideal para seu imóvel baseado em outros imóveis, né? Bem, é isso que vamos tentar fazer neste post.

  • Desenvolvimento de um modelo para predição de valor de aluguel baseado em características do apartamento;
  • Salvar o modelo treinado no Google Cloud Storage;
  • Deploy do modelo treinado no Google Cloud Functions;
  • Teste de funcionamento.

O que é uma Arquitetura Serverless?


Nos últimos anos, muito hype vem sendo criado em cima de arquiteturas serverless. Boa parte desse barulho é gerado por fornecedores de tais arquiteturas, onde principais são, obviamente: Google, Amazon e Microsoft, que investiram uma quantidade considerável de dinheiro nas suas plataformas serverless.

Ok, mas o que é uma arquitetura ou aplicação serverless? De acordo com o Martin Fowler, podemos dividir arquiteturas serverless em dois tipos: as BaaS (Backend as a Service) e as FaaS (Function as a Service). Em ambos os modelos, a parte de gerenciamento e arquitetura "server side" é toda tratada pelo provedor da plataforma, tirando essa responsabilidade do cliente. Arquiteturas serverless se beneficiam de uma redução de custo operacional, complexidade e necessidade de horas de engenharia. Para empresas com times reduzidos ou cientistas de dados ao estilo "faz tudo", esse tipo de aplicação é de grande ajuda, já que boa parte do processo de deploy é abstraída pelo provedor do serviço.

Para este post, vamos focar em aplicações FaaS. Neste tipo de aplicação, simplesmente criaremos uma função principal (main method) e usaremos as máquinas do Google Cloud Platform para hospedá-la. Essa função pode ser escrita em diferentes linguagens, obviamente utilizei Python3. Uma vez que a função tenha sido criada, sua execução será feita mediante requisições externas, ou seja, on demand. A parte de escalabilidade e distribuição também fica por conta do google, o que é uma mão na roda e tanto.

Pipeline do Mini Projeto

tile

Etapas para Desenvolvimento:

1 - EDA Básico com Pandas Profiling;

2 - Desenvolvimento e treinamento do modelo utilizando Scikit-Learn e Pandas;

3 - Upload do modelo salvo para um bucket no Google Cloud Storage;

4 - Desenvolvimento da API Flask para deploy no Google Cloud Function.

EDA Básico

Para criar nosso modelo de predição, vamos utilizar um dataset disponível no Kaggle com features de imóveis de algumas cidades brasileiras. Pelo o que pude perceber do dataset, o autor fez o scrapping das páginas do Quinto Andar para conseguir essas informações.

A primeira etapa de todo projeto de data science é fazer um EDA (Exploratory Data Analysis) do dataset com o qual se vai trabalhar. Muitas vezes esse tipo de análise é bem demorada, pois exige muita atenção. Por isso, hoje vamos utilizar uma ferramenta MUITO útil nessas análises iniciais o Pandas Profiling. Essa lib do Python gera um report bem detalhado sobre o dataset no qual é aplicada, ela nos ajuda a economizar bastante tempo.

A instalação da lib é bem simples em ambientes virtuais:

In [ ]:
pip install pandas-profiling[notebook]
In [1]:
import pandas as pd
from pandas_profiling import ProfileReport
In [3]:
#Carregando o dataset
path_data = 'caminho_para_dataset'
df_rent = pd.read_csv(path_data)

df_rent.head()
Out[3]:
city area rooms bathroom parking spaces floor animal furniture hoa (R$) rent amount (R$) property tax (R$) fire insurance (R$) total (R$)
0 São Paulo 70 2 1 1 7 acept furnished 2065 3300 211 42 5618
1 São Paulo 320 4 4 0 20 acept not furnished 1200 4960 1750 63 7973
2 Porto Alegre 80 1 1 1 6 acept not furnished 1000 2800 0 41 3841
3 Porto Alegre 51 2 1 0 2 acept not furnished 270 1112 22 17 1421
4 São Paulo 25 1 1 0 1 not acept not furnished 0 800 25 11 836


O dataset conta com as seguintes features:

  • city: cidade em que o imóvel se encontra;
  • area: área total do imóvel (m2);
  • rooms: quantidade de quartos do imóvel;
  • bathroom: quantidade de banheiros;
  • parking spaces: vagas de estacionamento;
  • floor: andar do imóvel;
  • furniture: se o imóvel é mobilhado;
  • hoa (R\$): valor do condomínio;
  • rent amount (R\$): valor do alguel;
  • property tax (R\$): IPTU;
  • fire insurance (R\$): seguro contra incêndio;
  • total (R\$): valor total do imóvel

A coluna total (R\$) trata do somatário dos valores de condomínio, aluguel, IPTU e seguro contra incêndio. Como queremos predizer apenas o valor de aluguel (rent amount (R\\$)), então, a coluna de total irá enviesar a análise. Por isso vamos excluí-la.

In [4]:
#O total amount pode ser descartado completamente dessa análise
df_train = df_rent.drop(columns=['total (R$)'])
df_train.head()
Out[4]:
city area rooms bathroom parking spaces floor animal furniture hoa (R$) rent amount (R$) property tax (R$) fire insurance (R$)
0 São Paulo 70 2 1 1 7 acept furnished 2065 3300 211 42
1 São Paulo 320 4 4 0 20 acept not furnished 1200 4960 1750 63
2 Porto Alegre 80 1 1 1 6 acept not furnished 1000 2800 0 41
3 Porto Alegre 51 2 1 0 2 acept not furnished 270 1112 22 17
4 São Paulo 25 1 1 0 1 not acept not furnished 0 800 25 11
In [ ]:
profile = ProfileReport(df_rent, title='Pandas Profiling Report', html={'style':{'full_width':True}})
In [6]:
profile
Out[6]:

Identificação de Anomalias


Vamos procurar alugueis muito mais caros que o normal, a regra será: Q3 + 3,5xIQR.

Como Pandas Profiling nos acusou, nosso Q3 = 5000 e nosso IQR = 3750

In [9]:
Q3 = 5000
IQR = 3750

df_train[df_train['rent amount (R$)'] >= Q3 + 3.5*IQR]
Out[9]:
city area rooms bathroom parking spaces floor animal furniture hoa (R$) rent amount (R$) property tax (R$) fire insurance (R$)
157 São Paulo 660 4 5 5 12 acept furnished 4800 20000 1750 254
1253 São Paulo 315 3 5 2 14 not acept not furnished 4300 20000 959 254
1743 São Paulo 410 4 5 5 1 acept not furnished 0 20000 0 254
2182 São Paulo 700 4 7 8 - acept not furnished 0 45000 8750 677
2521 Porto Alegre 318 4 3 0 - acept not furnished 0 19000 384 338
2619 São Paulo 80 2 1 1 1 acept not furnished 875 24000 0 305
2859 São Paulo 285 4 5 4 6 acept furnished 200000 20000 1834 254
5525 São Paulo 900 3 4 8 - acept not furnished 0 20000 3813 301
6185 São Paulo 455 4 5 4 5 acept not furnished 8500 19500 3334 248
6947 São Paulo 486 8 4 6 - acept not furnished 0 25000 2200 376
7748 São Paulo 350 3 3 3 - acept not furnished 0 30000 560 451

Bom, a maioria dos aluguéis absurdos estão em São Paulo (nenhuma novidade aí). Temos alguns valores que chegam a 45k/mês, porém, a área do imóvel é de 700 metros quadrados, 7 banheiros, 4 quartos e 8 vagas de estacionamento. Então, creio que esteja condizente. Como não temos informações relativas ao bairro do imóvel este tipo de análise fica um pouco complicada.

Porém, temos uma situação (id 2859) em que temos o aluguel e o valor de condomínio em 20k/mês, muito provavelmente é um erro. Vamos eliminar essa linha.

In [10]:
df_train = df_train.drop(2859)

Tratamento de Missing Values

Na coluna de floor algumas linhas que não possuem valor foram tratadas como '-', vamos substituir esses valores.

In [6]:
#Substituindo os valores de '-' na coluna floor
df_train = df_train.replace('-', 0)

Separação de Dados

In [7]:
X = df_train.drop(columns=['rent amount (R$)'])
y = df_train['rent amount (R$)']
In [8]:
from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(X, y, test_size=0.15)
In [9]:
x_train
Out[9]:
city area rooms bathroom parking spaces floor animal furniture hoa (R$) property tax (R$) fire insurance (R$)
6816 Porto Alegre 60 2 2 1 4 not acept not furnished 600 67 32
5074 São Paulo 70 3 2 0 0 acept not furnished 0 60 23
1853 São Paulo 43 1 1 0 6 acept not furnished 219 42 6
3878 São Paulo 50 1 1 1 7 acept furnished 2000 382 26
1722 São Paulo 120 3 2 1 1 not acept not furnished 870 160 32
... ... ... ... ... ... ... ... ... ... ... ...
7849 Belo Horizonte 180 3 2 2 2 acept not furnished 1502 231 31
9861 São Paulo 153 3 4 3 17 acept furnished 2000 534 80
9607 São Paulo 190 4 4 3 23 acept furnished 1100 589 91
1427 Belo Horizonte 70 2 1 1 2 acept not furnished 250 90 19
3626 São Paulo 200 3 5 3 5 acept furnished 1513 584 56

9088 rows × 11 columns

Construção do Modelo


Como vamos fazer o deploy do modelo como um único main method no Google Cloud Functions não é interessante para nós criarmos mais funções para tratamento de entradas do modelo. Para fazer esse pré processamento vamos utilizar algumas funções bem interessantes da biblioteca Scikit-Learn. A estratégia aqui é criar uma única pipeline que engloba tanto o modelo de regressão quanto as funções de pré processamento. Dessa forma podemos gerar tudo em um único arquivo .pkl, garantindo que toda a pipeline seja rodada da mesma forma, tanto para treinamento quanto para predição.

O nosso dataset é constituído tanto de dados numéricos quanto de dados categóricos, então, vamos construir duas pipelines diferentes e junta-las em uma única ao final. A primeira vai tratar os dados numéricos e a segunda os categóricos.

Para a pipeline numérica vamos apenas substituir os valores faltantes (nenhum até agora) pela mediana de todos os outros valores. Em seguida, vamos padronizar os inputs através do StandardScaler.

Já na pipeline categórica, vamos substituir os valores faltantes por uma string "missing_values" e em seguida vamos fazer o Hot Enconding das variáveis.

Para criar uma única função de pré processamento, vamos juntar as duas pipelines utilizando a função ColumnTransformer do Sklearn. Ela é útil para ser utlizada em datasets heterogêneos, ou seja, com dados categóricos e numéricos. Com essa função podemos agregar diferentes tipos de pipelines de pré processamento.

Por fim, criaremos a pipeline final com as funções de pré processamento e com o regressor.

In [10]:
#truque para facilitar nossa vida
from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression

#Pipeline para dados numéricos
numeric_features = ['area', 'rooms', 'bathroom', 'parking spaces', 'floor', 'hoa (R$)', 'property tax (R$)', 'fire insurance (R$)']
numeric_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')), #Usando a mediana para valores missing
    ('scaler', StandardScaler())]) #Padronizando os inputs

#Pipeline categórica
categorical_features = ['city', 'animal', 'furniture']
categorical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
    ('onehot', OneHotEncoder(handle_unknown='ignore'))])

#Agregando as duas pipelines
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat', categorical_transformer, categorical_features)])

#Pipeline final
clf = Pipeline(steps=[('preprocessor', preprocessor),
                      ('regressor', LinearRegression())])
In [11]:
clf.fit(x_train, y_train)
Out[11]:
Pipeline(memory=None,
         steps=[('preprocessor',
                 ColumnTransformer(n_jobs=None, remainder='drop',
                                   sparse_threshold=0.3,
                                   transformer_weights=None,
                                   transformers=[('num',
                                                  Pipeline(memory=None,
                                                           steps=[('imputer',
                                                                   SimpleImputer(add_indicator=False,
                                                                                 copy=True,
                                                                                 fill_value=None,
                                                                                 missing_values=nan,
                                                                                 strategy='median',
                                                                                 verbose=0)),
                                                                  ('scaler',
                                                                   StandardScaler(copy=True,
                                                                                  with_mean...
                                                                                 fill_value='missing',
                                                                                 missing_values=nan,
                                                                                 strategy='constant',
                                                                                 verbose=0)),
                                                                  ('onehot',
                                                                   OneHotEncoder(categories='auto',
                                                                                 drop=None,
                                                                                 dtype=<class 'numpy.float64'>,
                                                                                 handle_unknown='ignore',
                                                                                 sparse=True))],
                                                           verbose=False),
                                                  ['city', 'animal',
                                                   'furniture'])],
                                   verbose=False)),
                ('regressor',
                 LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
                                  normalize=False))],
         verbose=False)
In [12]:
y_pred = clf.predict(x_test)
In [13]:
from sklearn.metrics import mean_squared_error
import numpy as np
rmse = np.sqrt(mean_squared_error(y_test.values, y_pred))

print('O RMSE é:', rmse)
O RMSE é: 721.1614257966016

Conseguimos um RMSE um pouco alto, mas como nosso foco não é a otimização do modelo em si, vamos deixar assim mesmo. Com ele pronto já podemos fazer os testes com o Google Cloud Functions.

Persistindo o Modelo


Antes de enviar o modelo para o Google Cloud Storage, devemos salvar o modelo localmente. Para isso vamos utilizar uma lib chamada joblib.

In [16]:
import joblib
model_path = './modelos'

joblib.dump(clf, 'modelos/rent_pred.pkl')
Out[16]:
['modelos/rent_pred.pkl']
In [28]:
loaded = joblib.load('modelos/rent_pred.pkl')

loaded.predict(x_test)
Out[28]:
array([ 3922.74963362,  3648.39585038,  3375.85630382, ...,
        2010.2315262 , 14005.40379719, 12822.54160072])

Configurações para Uso do Google Services


Para iniciar esta etapa, a primeira coisa a ser feita é criar um projeto no Google Cloud Platform. O Google oferece 300 dólares gratuitos para quem estiver começando a utilizar seus serviços, é uma boa ajuda para quem gosta de fazer PoCs com novas tecnologias.

Em seguida, devemos instalar o Google Cloud command line tools e depois setar nossas credenciais para conexão com as aplicações necessárias. A instalação do command lines tools pode ser feita assim (em máquinas linux, se você usa windows só posso lamentar):

In [ ]:
curl -O https://dl.google.com/dl/cloudsdk/channels/
    rapid/downloads/google-cloud-sdk-255.0.0-linux-x86_64.tar.gz


tar zxvf google-cloud-sdk-255.0.0-linux-x86_64.tar.gz
    google-cloud-sdk


./google-cloud-sdk/install.sh

Suas credenciais devem ser ajustadas assim:

In [ ]:
gcloud config set project projeto_que_voce_criou
gcloud auth login
gcloud init
gcloud iam service-accounts create nome_conta_de_serviço
gcloud projects add-iam-policy-binding id_do_seu_projeto
    --member "serviceAccount:nome_conta_de_serviço@id_do_seu_projeto.iam.gserviceaccount.com" --role "roles/owner"
gcloud iam service-accounts keys create nome_conta_de_serviço.json --iam-account
    nome_conta_de_serviço@id_do_seu_projeto.iam.gserviceaccount.com
export GOOGLE_APPLICATION_CREDENTIALS=/caminho/credenciais/nome_conta_de_serviço.json

Para enviarmos o modelo para o GCS devemos instalar a lib do google cloud para python, isso pode ser feito assim:

In [ ]:
pip install --user google-cloud-storage
In [17]:
#Ajustando credenciais
import os
os.environ["GOOGLE_APPLICATION_CREDENTIALS"]="/caminho/credenciais/nome_conta_de_serviço.json"

Após as configurações, o primeiro passo a ser tomado é criar um bucket no GCS para armazenar nosso modelo, isso é feito da seguinte maneira:

In [19]:
from google.cloud import storage

bucket_name='hype_storage'

storage_client = storage.Client()
storage_client.create_bucket(bucket_name)
In [20]:
for bucket in storage_client.list_buckets():
    print(bucket.name)
hype_storage
hype_store

Agora vamos salvar o nosso modelo no bucket que acabamos de criar.

In [24]:
#Fazendo o upload do modelo para o seu bucket no gcp
from google.cloud import storage
bucket_name = "hype_storage"
storage_client = storage.Client()
bucket = storage_client.get_bucket(bucket_name)
blob = bucket.blob("modelos/")
blob.upload_from_filename("./modelos/rent_pred.pkl")

Para confirmar que o modelo foi salvo corretamente no bucket, vamos fazer um teste localmente.

In [26]:
#Pegando o modelo do bucket
from google.cloud import storage

bucket_name = "hype_storage"
storage_client = storage.Client()
bucket = storage_client.get_bucket(bucket_name)

blob = bucket.blob('modelos/')
blob.download_to_filename('modelos/rent_pred_gcs2.pkl')
In [27]:
gcs_model = joblib.load('modelos/rent_pred_gcs2.pkl')

gcs_model.predict(x_test)
Out[27]:
array([ 8242.22860467,   881.97199847,  8007.4308632 , ...,
       13422.37960433,  3056.06423233,  1310.00163549])
In [61]:
x_test
Out[61]:
city area rooms bathroom parking spaces floor animal furniture hoa (R$) property tax (R$) fire insurance (R$)
2926 Porto Alegre 148 3 2 2 3 acept furnished 2200 167 59
5727 São Paulo 101 2 3 1 12 acept not furnished 900 267 45
10485 São Paulo 150 2 3 3 0 acept not furnished 0 213 47
5432 Belo Horizonte 80 2 2 1 0 not acept not furnished 0 61 14
2317 São Paulo 90 3 2 2 10 acept not furnished 850 300 21
... ... ... ... ... ... ... ... ... ... ... ...
6913 São Paulo 17 1 1 0 1 not acept furnished 300 50 28
10017 Belo Horizonte 53 2 1 1 2 acept not furnished 200 74 14
87 São Paulo 63 2 2 0 15 not acept not furnished 590 152 20
7899 Rio de Janeiro 400 3 4 2 12 acept not furnished 3800 1834 194
4008 Rio de Janeiro 405 4 3 2 1 acept furnished 4032 1649 181

1604 rows × 11 columns

Ok, tudo funcionando normalmente.

  • Configuramos o Google Cloud command line;
  • Ajustamos nossas credenciais;
  • Criamos o bucket no GCS;
  • Enviamos o modelo;

Agora, só nos resta fazer o deploy final para o Google Cloud Functions.

Criando as Funções no Google Cloud Functions


Como falei anteriormente, para fazermos o deploy do nosso modelo utilizando FaaS, nós precisamos apenas ajustar uma função (literalmente).

title

Para a criação de uma função Cloud Function no Google, siga os seguinte passos:

  1. Procure por "Cloud Function" na barra lateral;
  2. Clique em "Criar Função";
  3. Adicione o nome que desejar para a função;
  4. Para teste, clique em "Permitir invocações não autenticadas";
  5. Selecione Python 3.7 como Ambiente de execução.

A nossa função no GCF funciona basicamente como um aplicativo Flask. Ela vai receber um request, processar e gerar um resultado. Bom, nosso request será um json contendo as infomações do imóvel que queremos prever o aluguel e o nosso resultado será o valor predito do aluguel.

Quando a função for criada, você perceberá dois arquivos editáveis na plataforma. No MAIN.PY nós iremos criar nossa função para predição e no REQUIREMENTS.TXT iremos colocar as libs a serem instaladas.

title

O código da nossa função main.py é o seguinte:

In [1]:
model = None
def pred_rent(request):
    from google.cloud import storage
    import joblib
    import sklearn
    import pandas as pd
    from flask import jsonify
    import json

    global model

    data = {}
    params = request.get_json(force=True)

    request_pd = pd.DataFrame.from_dict(params, orient='index').transpose()

    if not model:
        # #Configurando bucket
        bucket_name = 'hype_storage'
        storage_client = storage.Client()
        bucket = storage_client.get_bucket(bucket_name)

        #download do modelo
        blob = bucket.blob('modelos/')
        blob.download_to_filename('/tmp/rent_pred.pkl')
        model = joblib.load('/tmp/rent_pred.pkl')

    data['valor aluguel'] = str(model.predict(request_pd)[0])
    
    return jsonify(data)


Com o Google Cloud Funtions não podemos criar pastar e salvar arquivos normalmente, mas podemos salvar arquivos na pasta /tmp. Por essa razão salvamos nosso modelo nela após pegarmos do bucket.

Também usamos uma variável global no nosso modelo para não termos a necessidade de leitura de arquivo do HD a cada requisição da API, isso agiliza muito o processo de inferência.

Nosso requirements.txt ficará das seguinte forma:

In [ ]:
# Function dependencies, for example:
# package>=version
google-cloud-storage
sklearn
pandas
flask

Pronto, tudo configurado. Só nos resta testar. O GCF nos fornece uma área de testes da função.

Vamos criar um JSON de exemplo e ver se nosso modelo nos retorna algo. O JSON para teste pode ser o seguinte:

In [1]:
request_example = {"city":"São Paulo", "area": 148, "rooms":2, "bathroom":1, "parking spaces": 1, 
                   "floor": 1, "animal": "accept", "furniture": "furnished", "hoa (R$)": 950, 
                   "property tax (R$)": 60, "fire insurance (R$)": 45}

Isso nos gera o seguinte resultado:

title

Também podemos fazer o teste enviando uma requisição diretamente para a URI gerado pela função:

In [9]:
import requests 

result = requests.post("https://us-central1-severlessml.cloudfunctions.net/teste3", json={"city":"Belo Horizonte", "area": 85, "rooms":2, "bathroom":1, "parking spaces": 1, "floor": 1, "animal": "accept", "furniture": "furnished", "hoa (R$)": 150, "property tax (R$)": 60, "fire insurance (R$)": 45})
result
Out[9]:
<Response [200]>
In [10]:
result.json()
Out[10]:
{'valor aluguel': '3097.3450854034586'}

Conclusões

Apesar dessa PoC ter sido bem pequena e simples, consegui simular uma pipeline de um modelo do treinamento ao deploy em um ambiente serverless. A parte mais interessante sem dúvida foi aprender coisas novas sobre as ferramentas do Google Cloud Platform.

As principais conclusões que tirei desse mini projeto foram:

  • A facilidade de trabalho no ambiente Cloud da Google é espantosa. A curva de aprendizado é muito boa;
  • As integrações entre serviços são bem fáceis também;
  • O deploy de um modelo como FaaS foi bem simples, mas como foi um modelo básico e tudo integrado em uma única pipeline acho que essa opinião pode estar um pouco enviesada;
  • Apesar de toda essa facilidade, me pergunto se, com o dólar a quase 6 reais, essa pipelines são viáveis para empresas de médio e pequeno porte. Já que você é cobrado por requisição;
  • Não pude testar a questão da escalabilidade do modelo, como eu disse, o dólar ta em quase 6 reais.

TODOs:

  • Utilizar modelos de Tensorflow ou Pytorch para testar com modelos mais complexos;
  • Utilizar múltiplas funções;
  • Criar pipelines mais complexas para teste.
In [ ]: