Esse é o quinto post de uma série de como construir um produto data-driven de ponta a ponta, caso você ainda não tenha acompanhado os demais, abaixo segue uma síntese com os respectivos links 😀.

  1. Em metadados de normas jurídicas federais coletamos dados do sistema LexML.
  2. Em um produto orientado a dados governamentais: parte 1 realizamos uma análise exploratória dos dados e definimos um recorte e um escopo para os dados do projeto.
  3. Em um produto orientado a dados governamentais: parte 2 realizamos a definição dos dos datasets de treino, validação e teste
  4. Em um produto orientado a dados governamentais: parte 3 detalhamos tudo que não deu certo no treinamento de modelos de machine learning.

No último post fizemos um registro de algumas tentativas de usar modelos de machine learning mais tradicionais para criar o nosso classificador de normas jurídicas. Depois de algumas tentativas frustadas, decidimos partir para uma abordagem de processamento de linguagem natural com deep learning. Para realizar o treinamento das redes neurais iremos usar a biblioteca fastai. Como sempre, iniciamos o projeto carregando as bibliotecas que faremos uso.

import pandas as pd
import numpy as np
from pathlib import Path
from fastai.text import *
from fastai import *
import fastai
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.metrics import classification_report, f1_score
import torch
import warnings

Utilizamos a plataforma colab por conta da sua disponibilidade de GPU/TPU para treinamento do nosso modelo. Dessa forma, será necessário sobrescrever alguns paths da biblioteca fastai para persistir alguns dados e modelos gerados dentro do nosso google drive.

Config.DEFAULT_CONFIG = {
        'data_path': './drive/My Drive/Colab Notebooks/datagovproduct/fastai/data',
        'model_path': './drive/My Drive/Colab Notebooks/datagovproduct/fastai/models'
    }

Config.create('./drive/My Drive/Colab Notebooks/datagovproduct/myconfig.yml')
Config.DEFAULT_CONFIG_PATH = './drive/My Drive/Colab Notebooks/datagovproduct/myconfig.yml'

path = Path("./drive/My Drive/Colab Notebooks/datagovproduct/")
data_path = Config.data_path()

O processo de escolha de uso da fast.ai teve duas grandes influências para esse projeto. Primeiramente, tenho uma admiração pessoal pelo Jeremy Howard que é o criador e mantenedor do projeto, que busca democratizar o uso de deep learning. Além disso, ele é uma das mentes por trás do ULMFit que é uma das aplicações pioneiras de uso de transfer learning para processamento de linguagem natural (NLP). Outra personalidade que contribuiu diretamente para essa escolha foi o Pierre Guillou que é uma referência em deep learning, além de ser um dos membros mais ativos da comunidade de Deep Learning Brasília.

O Pierre Guillou já realizou diversas contribuções quanto a treinamento de modelos de linguagem natural pra a língua portuguesa. Especificamente fizemos uso de um dos seus trabalhos que ele treinou um language model com o MultiFiT a partir dos dados da Wikipedia em português. Além disso, ele também demonstrou como fazer uso do language model treinado para a tarefa de classificadores de textos.

Apresentado os créditos necesários aos trabalhos que tornaram o nosso projeto viável, vamos iniciar carregando os dados necessários.

xTrain = pd.read_csv('drive/My Drive/Colab Notebooks/data/xTrain.csv', sep=';')
yTrain = pd.read_csv('drive/My Drive/Colab Notebooks/data/yTrain.csv', sep=';')
xValidate = pd.read_csv('drive/My Drive/Colab Notebooks/data/xValidate.csv', sep=';')
yValidate = pd.read_csv('drive/My Drive/Colab Notebooks/data/yValidate.csv', sep=';')
xTest = pd.read_csv('drive/My Drive/Colab Notebooks/data/xTest.csv', sep=';')
yTest = pd.read_csv('drive/My Drive/Colab Notebooks/data/yTest.csv', sep=';')

O language model que iremos utilizar foi treinado como bidirecional, isto é, uma rede neural é treinada predizer o token n+1, isto é, a palavra seguinte. Enquanto segundo modelo, é treinado para predizer a palavra antecessora, isto é, o token n-1. Para maiores detalhes do treinamento do language model sugiro consultar o post do Pierre Guillou.

Fine-tuning do language model

De posse do modelo treinado com os dados da Wikipedia, podemos realizar um ajuste fino com os nossos dados. Para essa etapa precisamos expor o modelo a maior quantidade de textos do nosso domínio específico. Assim, iremos consolidar os três datasets (treino, validação e teste) que segmentamos para treinamento. É válido ressaltar, que não iremos expor ao language model a nossa variável alvo, isto é, as tags. Portanto, não há vazamento de informação para o nosso problema de classificação, que é uma etapa posterior do nosso problema.

#collapse
def transf_str_into_list(texto: str) -> list:
    texto = str(texto)
    texto = texto.split('[')[1].split(']')[0].split(',')
    texto = [w.replace('[','').replace(']','').replace("'","").replace('(','').replace(')','').strip() for w in texto]
    return texto

ementas = pd.concat([xTrain, xValidate, xTest])
ementas.drop(ementas[ementas.description.isnull()].index, inplace=True)
ementas = ementas.merge(pd.concat([yTrain, yValidate, yTest]), on='legislationIdentifier', how='left')
ementas.drop(ementas[ementas.description.isnull()].index, inplace=True)
ementas['keywords'] = ementas['keywords'].apply(transf_str_into_list)

Apesar que nesse estágio não iremos treinar o nosso classificador, já podemos preparar os dados para como a biblioteca da fastai os consome. Assim realizamos uma transformação por one-hot-encoding em nossa variável alvo

one_hot = MultiLabelBinarizer()
mlabel_features = one_hot.fit_transform(ementas['keywords'])
mlabel_features_to_df = pd.DataFrame(mlabel_features, columns=one_hot.classes_)
mlabel_features_to_df.index = ementas.legislationIdentifier
ementas = ementas.set_index('legislationIdentifier')\
    .drop(['legislationType', 'datePublished', 'keywords'], axis='columns')\
    .join(mlabel_features_to_df)\
    .rename(columns={'description' : 'ementa'})
ementas.head(3)
ementa ACORDO INTERNACIONAL ... UNIÃO FEDERAL UTILIDADE PUBLICA
legislationIdentifier
urn:lex:br:federal:decreto.legislativo:2004-08-18;544 APROVA O ATO QUE AUTORIZA A ASSOCIAÇÃO BRASIL ... 0 ... 0 0
urn:lex:br:federal:decreto:1999-03-22;seq-sf-5 DISPÕE SOBRE A IMPLANTAÇÃO DO CENTRO FEDERAL D... 0 ... 0 0
urn:lex:br:federal:decreto:2008-01-23;seq-sf-4 DECLARA DE INTERESSE SOCIAL, PARA FINS DE REFO... 0 ... 0 0

3 rows × 48 columns

Em seguida criamos alguns diretórios e salvamos os nomes dos arquivos que armazenam os pesos dos modelos bem como seus respectivos vocabulários.

#collapse
lang = 'pt'
name = f'{lang}wiki'
path = data_path/name
path.mkdir(exist_ok=True, parents=True)
data_path_wiki = data_path / 'ptwiki'

#pesos do forward language model e vocabulário
lm_fns3 = [f'{lang}_wt_sp15_multifit', f'{lang}_wt_vocab_sp15_multifit']
#pesos do forward language model e vocabulário
lm_fns3_bwd = [f'{lang}_wt_sp15_multifit_bwd', f'{lang}_wt_vocab_sp15_multifit_bwd']

Em meados do ano passado, foi incluído na fast.ai uma implementação de um tokenizador faz uso de subwords units chamado SentencePiece.

Assim, precisamos garantir que os nossos dados sejam tokenizados no mesmo padrão utilizado no language model que iremos realizar o fine-tunning.

%%time
bs = 64   # batch size works for Kaggle Kernels/colab
data_lm = (TextList.from_df(ementas, data_path, cols='ementa', processor=SPProcessor.load(data_path_wiki))
                .split_by_rand_pct(0.1, seed=42)
                .label_for_lm()
                .databunch(bs=bs))
CPU times: user 3.14 s, sys: 166 ms, total: 3.3 s
Wall time: 5.68 s

Iremos realizar alguns ajustes nos parâmetros da ASGD Weight-Dropped LSTM que é arquitetura usada no treinamento do language model.

config = awd_lstm_lm_config.copy()
config['qrnn'] = True
config['n_hid'] = 1550 # default 1152
config['n_layers'] = 4 # default 3

Por conseguinte, instanciamos o Learner que irá armazenar o modelo que iremos importar e realizar o fine-tunning.

Forward language model

%%time
perplexity = Perplexity()
learn_lm = language_model_learner(data_lm, AWD_LSTM, config=config, pretrained_fnames=lm_fns3, drop_mult=1., 
                                  metrics=[error_rate, accuracy, perplexity]).to_fp16()
CPU times: user 662 ms, sys: 115 ms, total: 777 ms
Wall time: 16.8 s

O modelo que usaremos possui 46 milhões de parâmetros, mas colocando em contexto com o GPT-3 que possui os seus 175 Bilhões 🦾, não parece tanto, né? Mas o nosso propósito que é treinar um classificador de textos, é mais que suficiente 😄!

count_tags

# number of model parameters
sum([p.numel() for p in learn_lm.model.parameters()])
46020150

Avaliamos o gráfico abaixo para definir um learning rate para o nossa etapa de aprendizado.

learn_lm.recorder.plot(suggestion=True)
Min numerical gradient: 1.45E-01
Min loss divided by 10: 6.31E-02
lr = 2e-2
lr *= bs/48
wd = 0.1

Em problemas de otimização, há diversas abordagens, mas uma em particular que tem atraído atenção nos tempos mais recentes, é a chamada de super convergência. Para maiores detalhes do seu funcionamento recomendo a leitura do artigo do Sylvain Gugger.

learn_lm.fit_one_cycle(2, lr*10, wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss error_rate accuracy perplexity time
0 6.537959 4.602726 0.635126 0.364874 99.755928 03:58
1 3.026787 2.435508 0.416867 0.583133 11.421622 04:02
learn_lm.unfreeze()
learn_lm.fit_one_cycle(18, lr, wd=wd, moms=(0.8,0.7), callbacks=[ShowGraph(learn_lm)]) 
epoch train_loss valid_loss error_rate accuracy perplexity time
0 1.649410 1.420790 0.277500 0.722500 4.140390 05:10
1 1.252494 1.136566 0.238259 0.761741 3.116050 05:09
2 1.204118 1.083645 0.229663 0.770337 2.955434 05:09
3 1.235600 1.119952 0.236396 0.763604 3.064707 05:08
4 1.256426 1.152203 0.239959 0.760040 3.165157 05:08
5 1.279871 1.133477 0.237768 0.762232 3.106438 05:07
6 1.243827 1.122837 0.238243 0.761757 3.073564 05:08
7 1.221945 1.102359 0.230743 0.769257 3.011262 05:08
8 1.176175 1.063635 0.226591 0.773409 2.896883 05:08
9 1.126460 1.057538 0.225653 0.774346 2.879273 05:08
10 1.076421 0.994550 0.216968 0.783032 2.703506 05:08
11 1.026595 0.950709 0.209168 0.790832 2.587544 05:08
12 0.973859 0.905806 0.201510 0.798490 2.473926 05:09
13 0.909612 0.863579 0.194586 0.805414 2.371633 05:09
14 0.850613 0.832053 0.187739 0.812261 2.298033 05:09
15 0.796591 0.803084 0.181753 0.818247 2.232415 05:09
16 0.751498 0.789959 0.178718 0.821282 2.203307 05:10
17 0.740976 0.788238 0.177890 0.822110 2.199518 05:10

Backward language model

Preparamos nossa estrutura de dados orientadas ao language model invertido.

%%time
data_lm = (TextList.from_df(ementas, data_path, cols='ementa', processor=SPProcessor.load(data_path_wiki))
                .split_by_rand_pct(0.1, seed=42)
                .label_for_lm()
                .databunch(bs=bs, backwards=True))
CPU times: user 2.95 s, sys: 264 ms, total: 3.22 s
Wall time: 5.65 s

De forma análoga, instanciamos o Learner que irá armazenar o backward model que iremos importar e realizar o fine-tunning.

%%time
perplexity = Perplexity()
learn_lm = language_model_learner(data_lm, AWD_LSTM, config=config, pretrained_fnames=lm_fns3_bwd, drop_mult=1., 
                                  metrics=[error_rate, accuracy, perplexity]).to_fp16()
CPU times: user 602 ms, sys: 79 ms, total: 681 ms
Wall time: 8.25 s
learn_lm.recorder.plot(suggestion=True)
Min numerical gradient: 1.20E-01
Min loss divided by 10: 5.25E-02

Novamente, com auxílio do gráfico de Loss x Learning Rate definimos o lr que usaremos e também definimos um weight decay.

lr = 2e-2
lr *= bs/48
wd = 0.1
learn_lm.fit_one_cycle(2, lr*10, wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss error_rate accuracy perplexity time
0 9.154589 4.780515 0.682013 0.317987 119.165504 04:03
1 3.914568 3.539506 0.564140 0.435860 34.449905 04:02
learn_lm.unfreeze()
learn_lm.fit_one_cycle(18, lr, wd=wd, moms=(0.8,0.7), callbacks=[ShowGraph(learn_lm)])
epoch train_loss valid_loss error_rate accuracy perplexity time
0 1.867417 1.632737 0.277175 0.722824 5.117862 05:10
1 1.285496 1.168105 0.216185 0.783815 3.215894 05:09
2 1.188974 1.100847 0.208263 0.791737 3.006712 05:08
3 1.224316 1.117932 0.209505 0.790495 3.058522 05:08
4 1.264671 1.135131 0.211879 0.788121 3.111581 05:07
5 1.297721 1.143616 0.213592 0.786408 3.138094 05:06
6 1.266327 1.141421 0.212167 0.787833 3.131216 05:06
7 1.207644 1.104038 0.208109 0.791891 3.016322 05:05
8 1.172563 1.068903 0.203908 0.796092 2.912184 05:05
9 1.135526 1.041017 0.198811 0.801189 2.832095 05:05
10 1.072348 1.002103 0.194383 0.805617 2.724003 05:06
11 1.020304 0.966593 0.189387 0.810613 2.628973 05:06
12 0.993222 0.925114 0.181997 0.818003 2.522156 05:06
13 0.916666 0.886546 0.176096 0.823904 2.426733 05:07
14 0.877075 0.853486 0.170073 0.829927 2.347818 05:07
15 0.838313 0.828674 0.165605 0.834395 2.290279 05:07
16 0.788064 0.816227 0.163080 0.836920 2.261948 05:07
17 0.780965 0.814206 0.162468 0.837533 2.257382 05:08

Uma vez concluído as etapas de ajuste dos [forward & backward] language model. Iremos proceder com o treinamento dos classificadores multilabel.

Fine-Tuning do classificador forward

Iniciamos essa etapa carregando a TextList utilizada no fine-tuning do forward language model. Essa etapa será necessária para acessar o vocabulário existente nessa estrutura de dados.

%%time
data_lm = load_data(data_path, f'{lang}_databunch_lm_pylexml_metadados_sp15_multifit_v2', bs=bs)
CPU times: user 322 ms, sys: 91.1 ms, total: 413 ms
Wall time: 2.72 s

Podemos inspecionar essa estrutura de dados 👀.

data_lm.show_batch()
idx text
0 ▁implantação ▁xxup ▁do ▁xxup ▁centro ▁xxup ▁federal ▁xxup ▁de ▁xxup ▁educação ▁xxup ▁ tec no lo gica ▁xxup ▁do ▁xxup ▁e spirit o ▁xxup ▁santo . ▁xxbos ▁xxup ▁declara ▁xxup ▁de ▁xxup ▁interesse ▁xxup ▁social , ▁xxup ▁para ▁xxup ▁fins ▁xxup ▁de ▁xxup ▁reforma ▁xxup ▁a gra ria , ▁xxup ▁os ▁xxup ▁ imo ve is ▁xxup ▁rurais ▁xxup ▁que ▁xxup ▁menciona , ▁e ▁xxup ▁da ▁xxup ▁outras ▁xxup ▁providencia
1 ▁xxup ▁" fa zen da ▁xxup ▁alvo rada ", ▁xxup ▁situado ▁xxup ▁no ▁xxup ▁ muni ci pio ▁xxup ▁de ▁xxup ▁vila ▁xxup ▁rica , ▁xxup ▁estado ▁xxup ▁de ▁xxup ▁mato ▁xxup ▁grosso , ▁e ▁xxup ▁da ▁xxup ▁outras ▁xxup ▁providencia s . ▁xxbos ▁xxup ▁cria ▁xxup ▁cargos ▁xxup ▁em ▁xxup ▁comissão ▁xxup ▁do ▁xxup ▁grupo - dire ção ▁e ▁xxup ▁assessor amento ▁xxup ▁superiores ▁ - ▁xxup ▁das ▁xxup
2 ▁xxup ▁união , ▁xxup ▁em ▁xxup ▁favor ▁xxup ▁da ▁xxup ▁secretaria ▁xxup ▁da ▁xxup ▁ci encia ▁e ▁xxup ▁tecnologia , ▁xxup ▁ credit o ▁xxup ▁su ple menta r ▁xxup ▁no ▁xxup ▁valor ▁xxup ▁de ▁xxup ▁ cr $ ▁4 . 2 09 . 6 00.000 , 00 ▁xxup ▁para ▁xxup ▁os ▁xxup ▁fins ▁xxup ▁que ▁xxup ▁especifica . ▁xxbos ▁xxup ▁institui , ▁xxup ▁no ▁xxup ▁ ambi to ▁xxup
3 ▁xxup ▁do tação ▁xxup ▁constante ▁xxup ▁da ▁xxup ▁lei ▁xxup ▁o r ça menta ria ▁xxup ▁vigente . ▁xxbos ▁xxup ▁da ▁xxup ▁nova ▁xxup ▁redação ▁xxup ▁ao ▁xxup ▁artigo ▁1 ▁xxup ▁da ▁xxup ▁lei ▁9 . 5 30 , ▁xxup ▁de ▁10 ▁xxup ▁de ▁xxup ▁dezembro ▁xxup ▁de ▁1997 . ▁xxbos ▁xxup ▁re nova ▁a ▁xxup ▁concessão ▁xxup ▁outorga da ▁a ▁xxup ▁radio ▁xxup ▁brasil ▁xxup ▁sociedade ▁xxup ▁limitada ,
4 ▁xxup ▁do ▁xxup ▁sala rio - mini mo ▁xxup ▁do ▁xxup ▁ mes ▁xxup ▁de ▁xxup ▁agosto ▁xxup ▁de ▁1989, ▁xxup ▁na ▁xxup ▁forma ▁xxup ▁da ▁xxup ▁lei ▁7 . 7 89 , ▁xxup ▁de ▁3 ▁xxup ▁de ▁xxup ▁julho ▁xxup ▁de ▁1989 . ▁xxbos ▁xxup ▁a prova ▁o ▁xxup ▁ato ▁xxup ▁que ▁xxup ▁re nova ▁a ▁xxup ▁concessão ▁xxup ▁outorga da ▁a ▁xxup ▁radio ▁8 80 ▁xxup ▁ lt

A partir de agora como estamos trabalhando na parte de classificação, devemos novamente segregar os nossos dados que originalmente estavam dividos em conjunto de: treinamento, validação e teste. A classe TextList possui um método (split_by_rand_pct) que faz um recorte aleatório nos dados - baseado num percentual determinado pelo usuário - para fins de validação. Portanto, iremos consolidar os dados de treinamento e validação e essa definição de distribuição será delegada a estrutura do TextList.

Vamos selecionar as urns - que são os identificadores únicos das normas - dos conjunto de treinamento e validação originais.

xTrain = xTrain[~xTrain['legislationIdentifier'].isna()].copy()
train_id = list(xTrain['legislationIdentifier'])
train_id = set(ementas.index).intersection(set(train_id))
valid_id = list(xValidate['legislationIdentifier'])
valid_id = set(ementas.index).intersection(set(valid_id))

Em seguida vamos criar um set resultante da união dos dois conjuntos de urns.

new_train_id = train_id.union(valid_id)

Podemos conferir que temos 26900 normativos que serão utilizados para fins de treinamento e validação.

len(new_train_id)
26900

Por fim, iremos consolidar os dataframes de treinamento/validação e de teste.

train_ementas_df = ementas.loc[new_train_id]
test_ementas_df = ementas.drop(new_train_id)

Resultamos com dois dataframes com respectivamente 26900 e 6788 registros.

train_ementas_df.shape, test_ementas_df.shape
((26900, 48), (6788, 48))

Realizamos uma última inspeção para garantirmos que não há urns que estejam presentes em ambos conjuntos de dados.

set(train_ementas_df.index).intersection(test_ementas_df.index)
set()

Um último ajuste necessário é a segregação do test_ementas_df em dois dataframes distintos, um contendo apenas a variável preditora (ementa) e um segundo com as variáveis respostas.

test_ementas_df = test_ementas_df.reset_index()
y_test_ementas_df = test_ementas_df.drop(['ementa'], axis='columns')
y_test_ementas_df = y_test_ementas_df.set_index('legislationIdentifier')
test_ementas_df = test_ementas_df[['legislationIdentifier','ementa']]
test_ementas_df = test_ementas_df.set_index('legislationIdentifier')

Podemos fazer uma inspeção da estrutura do dataframe com os dados de teste.

test_ementas_df.head(2)
ementa
legislationIdentifier
urn:lex:br:federal:medida.provisoria:1995-06-27;1037 CRIA, A GRATIFICAçÃO DE DESEMPENHO E PRODUTIVI...
urn:lex:br:federal:decreto:1998-09-18;seq-sf-25 DECLARA DE INTERESSE SOCIAL, PARA FINS DE REFO...

Em seguida podemos criar uma estrutura TextList para os dados destinados para avaliação final. Além disso, armazenamos em uma lista todas as tags do conjunto de dados.

test_datalist = TextList.from_df(test_ementas_df, cols='ementa')
labels_cols = list(one_hot.classes_)

Agora podemos criar um novo objeto TextList a partir dos dataframe recém criado, informando o vocabulário que deverá ser utilizado (data_lm.vocab), além do tipo de tokenizador (SentencePiece), além disso determinar que 20% dos dados devem ser destinados para validação, bem como se trata de um problema de classificação multilabel e que os dados estão organizados na forma de one-hot-encoding.

%%time
data_cls = (TextList.from_df(train_ementas_df, cols='ementa', vocab=data_lm.vocab, path=data_path, processor=SPProcessor.load(data_path_wiki))
                .split_by_rand_pct(valid_pct=0.2, seed=42)
                .label_from_df(cols=labels_cols, label_cls=MultiCategoryList, one_hot=True)
                .databunch(bs=bs, num_workers=1))
CPU times: user 5.69 s, sys: 155 ms, total: 5.84 s
Wall time: 9.44 s

O próximo passo é instanciar um Learner (text_classifier_learner) com um classificador de textos, informando entre outras coisas qual arquitetura será utilizada. Em seguida devemos carregar o encoder do forward language model.

learn_c = text_classifier_learner(data_cls, AWD_LSTM, config=config, pretrained=False, drop_mult=0.3, 
                                  metrics=[fbeta]).to_fp16()
learn_c.load_encoder(f'{lang}fine_tuned2_enc_pylexml_metadados_sp15_multifit_v2');

Podemos inspecionar qual a função de loss setada pelo Learner e, aparentemente, parece ser a mais indicada para problemas do tipo multilabel, como relatado aqui.

learn_c.loss_func
FlattenedLoss of BCEWithLogitsLoss()

Novamente, fazendo a chamada no método lr_find do Learner podemos determinar o learning rate a ser utilizado.

learn_c.recorder.plot(suggestion=True)
Min numerical gradient: 1.00E-01
Min loss divided by 10: 7.59E-02

Definindo o learning rate e o weight decay podemos iniciar o processo de aprendizagem.

lr = 1e-1
lr *= bs/48
wd = 0.1

Vamos iniciar o nosso processo de aprendizado do classificador.

learn_c.fit_one_cycle(2, lr, wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.111955 0.098437 0.759704 01:29
1 0.080981 0.067770 0.832344 01:30
learn_c.fit_one_cycle(2, lr, wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.109070 0.085738 0.784912 01:40
1 0.078726 0.065300 0.836147 01:35
learn_c.freeze_to(-2)
learn_c.fit_one_cycle(2, slice(lr/(2.6**4),lr), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.099396 0.078938 0.784647 01:44
1 0.063894 0.055922 0.859471 01:40
learn_c.freeze_to(-3)
learn_c.fit_one_cycle(2, slice(lr/2/(2.6**4),lr/2), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.077960 0.068078 0.823664 02:18
1 0.064582 0.053703 0.863921 02:16
learn_c.unfreeze()
learn_c.fit_one_cycle(20, slice(lr/10/(2.6**4),lr/10), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.062994 0.052839 0.864633 03:57
1 0.057039 0.051858 0.870182 04:02
2 0.061062 0.051159 0.872716 04:00
3 0.060827 0.051522 0.873349 04:11
4 0.059825 0.051461 0.874872 04:00
5 0.056051 0.052700 0.868932 03:48
6 0.054872 0.049682 0.876805 04:00
7 0.058284 0.049812 0.878709 03:56
8 0.052459 0.049054 0.879648 04:14
9 0.051222 0.051716 0.869470 04:02
10 0.053088 0.047166 0.884372 04:06
11 0.045625 0.048138 0.885268 04:16
12 0.044459 0.047001 0.886889 04:01
13 0.047364 0.045902 0.889616 04:12
14 0.044861 0.045930 0.891286 03:48
15 0.035714 0.045713 0.892234 03:54
16 0.035460 0.045136 0.893838 03:52
17 0.033020 0.045786 0.892497 03:59
18 0.036324 0.046158 0.891285 04:08
19 0.031046 0.045884 0.891697 04:13
learn_c.fit_one_cycle(5, slice(lr/100/(2.6**4),lr/100), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.036481 0.024748 0.948045 00:37
1 0.033759 0.025118 0.946288 00:40
2 0.037918 0.025198 0.947669 00:39
3 0.040362 0.025391 0.946449 00:37
4 0.036300 0.025535 0.946922 00:37
learn_c.fit_one_cycle(2, slice(lr/1000/(2.6**4),lr/1000), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss fbeta time
0 0.037922 0.025300 0.947074 00:42
1 0.033986 0.025244 0.947335 00:39

Resultamos com um f1-score de 94.7% para a avaliação no conjunto de validação. Abaixo podemos em maiores detalhes a predição do modelo para alguns exemplos.

learn_c.show_results()
text target prediction
▁xxbos ▁xxup ▁transforma ▁xxup ▁cargos ▁xxup ▁ va gos ▁xxup ▁da ▁xxup ▁carreira ▁xxup ▁da ▁xxup ▁pre vi dência , ▁xxup ▁da ▁xxup ▁sau de ▁e ▁xxup ▁do ▁xxup ▁trabalho , ▁xxup ▁estrutura da ▁xxup ▁pela ▁xxup ▁lei ▁xxmaj ▁no ▁1 1.3 55 , ▁xxup ▁de ▁19 ▁xxup ▁de ▁xxup ▁outubro ▁xxup ▁de ▁2006, ▁xxup ▁em ▁xxup ▁cargos ▁xxup ▁de ▁xxup ▁analista ▁xxup ▁ambiental , ▁xxup ▁da ▁xxup ▁carreira ▁xxup ALTERAÇÃO;AMBITO ALTERAÇÃO;AMBITO
▁xxbos ▁xxup ▁autor iza ▁o ▁xxup ▁fundo ▁xxup ▁de ▁xxup ▁compensação ▁xxup ▁de ▁xxup ▁variações ▁xxup ▁sala ria is ▁ - ▁xxup ▁fc v s , ▁a ▁xxup ▁assumir , ▁xxup ▁na ▁xxup ▁forma ▁xxup ▁disciplina da ▁xxup ▁em ▁xxup ▁ato ▁xxup ▁do ▁xxup ▁conselho ▁xxup ▁curador ▁xxup ▁do ▁xxup ▁fundo ▁xxup ▁de ▁xxup ▁compensação ▁xxup ▁de ▁xxup ▁variações ▁xxup ▁sala ria is ▁ - ▁xxup ▁ cc fc v AUTORIZAÇÃO;CORRELAÇÃO;DESTINAÇÃO;OBJETIVO;PAIS ESTRANGEIRO;UNIÃO FEDERAL AUTORIZAÇÃO;CORRELAÇÃO;DESTINAÇÃO;OBJETIVO;PAIS ESTRANGEIRO;UNIÃO FEDERAL
▁xxbos ▁xxup ▁institui ▁o ▁xxup ▁regime ▁xxup ▁ diferencia do ▁xxup ▁de ▁xxup ▁contra ta ções ▁xxup ▁publica s ▁ - ▁xxup ▁ rd c ; ▁xxup ▁altera ▁a ▁xxup ▁lei ▁10 . 68 3, ▁xxup ▁de ▁28 ▁xxup ▁de ▁xxup ▁maio ▁xxup ▁de ▁2003, ▁xxup ▁que ▁xxup ▁dispõe ▁xxup ▁sobre ▁a ▁xxup ▁organização ▁xxup ▁da ▁xxup ▁pre side ncia ▁xxup ▁da ▁xxup ▁republica ▁e ▁xxup ▁dos ▁xxup ▁mini ster ALTERAÇÃO;AMBITO;CRIAÇÃO;NORMAS;OBJETIVO;SERVIÇO ALTERAÇÃO;AMBITO;CRIAÇÃO;OBJETIVO
▁xxbos ▁xxup ▁altera ▁xxup ▁as ▁xxup ▁leis ▁12 . 5 46 , ▁xxup ▁de ▁14 ▁xxup ▁de ▁xxup ▁dezembro ▁xxup ▁de ▁2011, ▁xxup ▁para ▁xxup ▁pro r ro gar ▁o ▁xxup ▁regime ▁xxup ▁especial ▁xxup ▁de ▁xxup ▁re integra ção ▁xxup ▁de ▁xxup ▁valores ▁xxup ▁ tribu tar ios ▁xxup ▁para ▁xxup ▁as ▁xxup ▁empresas ▁xxup ▁exporta dora s ▁ - ▁xxup ▁re integra , ▁e ▁xxup ▁para ▁xxup ▁de ALTERAÇÃO ALTERAÇÃO
▁xxbos ▁xxup ▁altera ▁xxup ▁as ▁xxup ▁leis ▁nos ▁8 . 21 2, ▁xxup ▁de ▁24 ▁xxup ▁de ▁xxup ▁julho ▁xxup ▁de ▁1991, ▁e ▁8 . 21 3, ▁xxup ▁de ▁24 ▁xxup ▁de ▁xxup ▁julho ▁xxup ▁de ▁1991, ▁xxup ▁para ▁xxup ▁tratar ▁xxup ▁da ▁xxup ▁associação ▁xxup ▁do ▁xxup ▁segura do ▁xxup ▁especial ▁xxup ▁em ▁xxup ▁cooperativa ▁xxup ▁de ▁xxup ▁crédito ▁xxup ▁rural ▁xxup ▁e , ▁xxup ▁ainda ▁xxup ▁essa ▁xxup ALTERAÇÃO ALTERAÇÃO

Avaliação da performance do modelo (forward classifier) com conjunto de testes.

test_ementas_df.head(2)
ementa
legislationIdentifier
urn:lex:br:federal:medida.provisoria:1995-06-27;1037 CRIA, A GRATIFICAçÃO DE DESEMPENHO E PRODUTIVI...
urn:lex:br:federal:decreto:1998-09-18;seq-sf-25 DECLARA DE INTERESSE SOCIAL, PARA FINS DE REFO...

Vamos escrever uma função para consolidar as predições do modelo em relação ao conjunto de testes.

def consolida_predicao_test_dataset(dataframe: pd.DataFrame, learner: fastai.text.learner.RNNLearner, multilabel: MultiLabelBinarizer):
    container_of_preds = []
    for row in dataframe.itertuples():
        ementa = row.ementa
        pred = learner.predict(ementa)[0].data.numpy()
        container_of_preds.append(pred)
    df = pd.DataFrame(container_of_preds, columns=multilabel.classes_, index=dataframe.index)
    return df

Por fim, podemos armazenar o resultado das predições em uma variável e avaliar o f1-score com os dados de teste.

df_test_predict = consolida_predicao_test_dataset(test_ementas_df, learn_c, one_hot)

Galera, marcamos 97.4% !!! 🥳🥳🥳🥳

f1_score(y_test_ementas_df.to_numpy(), df_test_predict.to_numpy(), average='micro')
0.9736836909955975

Confesso que estou aliviado 😌, sabia? Depois de várias abordagens fracassadas que foram relatadas aqui conseguimos um excelente desempenho! A seguir segue o relatório detalhado das métricas por tag.

print(classification_report(y_test_ementas_df.to_numpy(), df_test_predict.to_numpy(), target_names=one_hot.classes_))
                                                           precision    recall  f1-score   support

                                     ACORDO INTERNACIONAL       0.98      0.99      0.99       149
                                                ALTERAÇÃO       0.94      0.87      0.91       389
                                                   AMBITO       0.83      0.48      0.61       151
                                                APROVAÇÃO       1.00      0.99      1.00      3584
                                         AREA PRIORITARIA       1.00      0.99      0.99       979
                                                      ATO       1.00      1.00      1.00      3427
                                              AUTORIZAÇÃO       0.85      0.84      0.84       583
                                                   BRASIL       0.96      0.92      0.94       119
                                              COMPETENCIA       0.90      0.58      0.70       209
                                               COMPOSIÇÃO       0.85      0.62      0.72       114
                                                CONCESSÃO       0.98      0.98      0.98      3846
                                               CORRELAÇÃO       0.79      0.55      0.65       154
                                      CREDITO SUPLEMENTAR       1.00      1.00      1.00       432
                                                  CRIAÇÃO       0.91      0.65      0.76       208
                                                CRITERIOS       0.83      0.64      0.72       192
                                               DECLARAÇÃO       1.00      0.99      0.99      1231
                                           DESAPROPRIAÇÃO       0.99      0.98      0.99      1171
                                               DESTINAÇÃO       0.95      0.94      0.95      1404
                                             DISPOSITIVOS       0.78      0.59      0.67       139
                                     DOTAÇÃO ORÇAMENTARIA       0.97      0.97      0.97       362
                              EMPRESA DE TELECOMUNICAÇÕES       1.00      1.00      1.00      3980
                                ESTADO DE MINAS GERAIS MG       0.98      0.94      0.96       705
                                   ESTADO DE SÃO PAULO SP       0.98      0.96      0.97       758
                                      ESTADO DO PARANA PR       0.98      0.93      0.96       476
                           ESTADO DO RIO GRANDE DO SUL RS       0.95      0.95      0.95       433
                                                EXECUTIVO       0.89      0.76      0.82       127
                                                 EXECUÇÃO       0.99      0.99      0.99      4003
                                                  FIXAÇÃO       0.88      0.56      0.68       190
                                            FUNCIONAMENTO       0.95      0.85      0.90       144
                                                 HIPOTESE       0.95      0.60      0.73       129
                                             IMOVEL RURAL       1.00      0.98      0.99      1003
INSTITUTO NACIONAL DE COLONIZAÇÃO E REFORMA AGRARIA INCRA       0.99      0.98      0.99       980
                                         INTERESSE SOCIAL       0.99      0.98      0.99      1081
                                                MUNICIPIO       0.99      0.99      0.99      5379
                                                   NORMAS       0.88      0.72      0.79       492
                                                 OBJETIVO       0.80      0.57      0.66       286
                           ORÇAMENTO DA SEGURIDADE SOCIAL       0.98      0.98      0.98       127
                                         ORÇAMENTO FISCAL       0.99      0.97      0.98       275
                                         PAIS ESTRANGEIRO       0.96      0.93      0.94       141
                                             RADIODIFUSÃO       1.00      1.00      1.00      3972
                                          REFORMA AGRARIA       1.00      0.98      0.99       997
                                                  REFORÇO       0.97      0.96      0.96       357
                                                RENOVAÇÃO       0.98      0.97      0.98      1411
                                                  SERVIÇO       1.00      0.99      1.00      4048
                                                    TEXTO       0.99      0.97      0.98       105
                                            UNIÃO FEDERAL       0.97      0.88      0.92       515
                                        UTILIDADE PUBLICA       0.96      0.96      0.96       223

                                                micro avg       0.98      0.96      0.97     51180
                                                macro avg       0.95      0.87      0.90     51180
                                             weighted avg       0.98      0.96      0.97     51180
                                              samples avg       0.97      0.95      0.95     51180

Fine-Tuning do classificador backward

De forma análoga ao que foi realizado com o forward classifier iremos proceder com o backward.

data_lm = load_data(data_path, file=f'{lang}_databunch_lm_pylexml_metadados_sp15_multifit_bwd_v2', bs=bs, backwards=True)
data_cls_bwd = (TextList.from_df(train_ementas_df, cols='ementa', vocab=data_lm.vocab, path=data_path, processor=SPProcessor.load(data_path_wiki))
                .split_by_rand_pct(valid_pct=0.2, seed=42)
                .label_from_df(cols=labels_cols, label_cls=MultiCategoryList, one_hot=True)
                .add_test(test_datalist)
                .databunch(bs=bs, num_workers=1, backwards=True))

Vamos instanciar um Learner e carregar o encoder do backward language model que fizemos o fine-tunning.

learn_clf_bwd = text_classifier_learner(data_cls_bwd, AWD_LSTM, config=config, drop_mult=0.3, pretrained=False, metrics=[accuracy,fbeta]).to_fp16()
learn_clf_bwd.load_encoder(f'{lang}fine_tuned2_enc_pylexml_metadados_sp15_multifit_bwd_v2');

Como já procedido anteriormente, vamos determinar o learning rate a ser utilizado.

learn_clf_bwd.recorder.plot(suggestion=True)
Min numerical gradient: 1.00E-01
Min loss divided by 10: 6.31E-02

Não diferente do que já feito, setamos o learning rate e o weight decay.

lr = 2e-1
lr *= bs/48
wd = 0.1

Assim, podemos proceder com treinamento do classificador com o backward language model.

learn_clf_bwd.fit_one_cycle(2, lr, wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.142090 0.123915 0.040461 0.691918 01:34
1 0.086008 0.072039 0.055011 0.817218 01:31
learn_clf_bwd.fit_one_cycle(2, lr, wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.146105 0.114159 0.010164 0.686492 01:24
1 0.083387 0.073733 0.053516 0.809222 01:27
learn_clf_bwd.freeze_to(-2)
learn_clf_bwd.fit_one_cycle(2, slice(lr/(2.6**4),lr), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.138617 0.117763 0.053868 0.719503 01:44
1 0.067619 0.061657 0.041252 0.836847 01:41
learn_clf_bwd.freeze_to(-3)
learn_clf_bwd.fit_one_cycle(2, slice(lr/2/(2.6**4),lr/2), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.140213 0.125244 0.040635 0.705584 02:17
1 0.093416 0.074484 0.051772 0.794208 02:09
learn_clf_bwd.unfreeze()
learn_clf_bwd.fit_one_cycle(20, slice(lr/10/(2.6**4),lr/10), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.084641 0.072778 0.051167 0.805168 04:02
1 0.079943 0.069104 0.056632 0.816868 04:00
2 0.084409 0.066901 0.055149 0.822255 03:54
3 0.076037 0.068551 0.023416 0.821928 03:53
4 0.075007 0.064910 0.024951 0.823828 03:53
5 0.075617 0.071107 0.051056 0.819623 04:04
6 0.073620 0.067061 0.050724 0.822177 03:56
7 0.067108 0.060961 0.036135 0.845188 03:44
8 0.070245 0.075395 0.007973 0.798795 03:56
9 0.064585 0.063003 0.022546 0.850610 03:51
10 0.062305 0.058785 0.014423 0.848961 04:08
11 0.057394 0.057053 0.048094 0.849891 03:56
12 0.061946 0.055380 0.055770 0.863512 04:02
13 0.054074 0.053640 0.036495 0.866595 04:10
14 0.049562 0.053187 0.017179 0.871160 03:59
15 0.051621 0.050782 0.017342 0.873991 04:07
16 0.048581 0.049892 0.015807 0.875577 03:43
17 0.041890 0.049212 0.018267 0.882256 03:50
18 0.043432 0.048357 0.020264 0.880858 03:48
19 0.037916 0.048765 0.018485 0.880833 03:53
learn_clf_bwd.fit_one_cycle(5, slice(lr/100/(2.6**4),lr/100), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.045045 0.049856 0.015930 0.880618 04:01
1 0.041357 0.048746 0.019449 0.882349 04:09
2 0.045904 0.048975 0.013553 0.880791 04:08
3 0.045918 0.049144 0.019687 0.881828 04:10
4 0.036947 0.048953 0.018156 0.881553 03:49
learn_clf_bwd.unfreeze()
learn_clf_bwd.fit_one_cycle(10, slice(lr/1000/(2.6**4),lr/1000), wd=wd, moms=(0.8,0.7))
epoch train_loss valid_loss accuracy fbeta time
0 0.042905 0.033152 0.026845 0.920035 03:56
1 0.040445 0.032796 0.016365 0.921201 04:12
2 0.041783 0.032588 0.014925 0.921448 04:04
3 0.045822 0.032572 0.014692 0.921470 03:48
4 0.041142 0.033627 0.013379 0.920067 03:51
5 0.043025 0.032628 0.012335 0.920913 04:17
6 0.041950 0.033135 0.016638 0.920390 04:03
7 0.046244 0.032977 0.013395 0.919564 04:07
8 0.040205 0.032837 0.013644 0.920409 03:56
9 0.041679 0.032704 0.016800 0.921253 04:04

Terminamos o última época com um f1-score de 92.1% 🤗 para o conjunto de validação, um resultado um pouco menor que o obtido com forward classifier, mas igualmente satisfatório.

Avaliação da performance do modelo (backward classifier) com conjunto de testes.

Podemos aproveitar a função que escrevemos e consolidar as predições do estimador em um dataframe.

%%time
df_test_bwd_predict = consolida_predicao_test_dataset(test_ementas_df, learn_clf_bwd, one_hot)
CPU times: user 4min 13s, sys: 18.4 s, total: 4min 31s
Wall time: 4min 43s

E o modelo também obteve um resultado excelente (97.2%)!! Apenas 0.02% abaixo do resultado do forward classifier.

f1_score(y_test_ementas_df.to_numpy(), df_test_bwd_predict.to_numpy(), average='micro')
0.9724239460137732

Abaixo o relatório detalhado dos resultados.

print(classification_report(y_test_ementas_df.to_numpy(), df_test_bwd_predict.to_numpy(), target_names=one_hot.classes_))
                                                           precision    recall  f1-score   support

                                     ACORDO INTERNACIONAL       0.99      0.97      0.98       149
                                                ALTERAÇÃO       0.95      0.88      0.91       389
                                                   AMBITO       0.91      0.46      0.61       151
                                                APROVAÇÃO       1.00      0.99      1.00      3584
                                         AREA PRIORITARIA       1.00      0.99      0.99       979
                                                      ATO       1.00      1.00      1.00      3427
                                              AUTORIZAÇÃO       0.86      0.78      0.82       583
                                                   BRASIL       0.90      0.90      0.90       119
                                              COMPETENCIA       0.90      0.53      0.67       209
                                               COMPOSIÇÃO       0.79      0.57      0.66       114
                                                CONCESSÃO       0.98      0.98      0.98      3846
                                               CORRELAÇÃO       0.82      0.56      0.66       154
                                      CREDITO SUPLEMENTAR       1.00      1.00      1.00       432
                                                  CRIAÇÃO       0.92      0.71      0.80       208
                                                CRITERIOS       0.82      0.63      0.71       192
                                               DECLARAÇÃO       0.99      0.99      0.99      1231
                                           DESAPROPRIAÇÃO       0.99      0.99      0.99      1171
                                               DESTINAÇÃO       0.96      0.93      0.95      1404
                                             DISPOSITIVOS       0.64      0.53      0.58       139
                                     DOTAÇÃO ORÇAMENTARIA       0.96      0.97      0.96       362
                              EMPRESA DE TELECOMUNICAÇÕES       1.00      0.99      1.00      3980
                                ESTADO DE MINAS GERAIS MG       0.98      0.95      0.96       705
                                   ESTADO DE SÃO PAULO SP       0.98      0.95      0.96       758
                                      ESTADO DO PARANA PR       0.98      0.93      0.96       476
                           ESTADO DO RIO GRANDE DO SUL RS       0.93      0.94      0.94       433
                                                EXECUTIVO       0.90      0.71      0.79       127
                                                 EXECUÇÃO       0.99      0.99      0.99      4003
                                                  FIXAÇÃO       0.84      0.57      0.68       190
                                            FUNCIONAMENTO       0.97      0.84      0.90       144
                                                 HIPOTESE       0.91      0.56      0.69       129
                                             IMOVEL RURAL       1.00      0.98      0.99      1003
INSTITUTO NACIONAL DE COLONIZAÇÃO E REFORMA AGRARIA INCRA       0.98      0.98      0.98       980
                                         INTERESSE SOCIAL       0.99      0.98      0.98      1081
                                                MUNICIPIO       0.99      0.99      0.99      5379
                                                   NORMAS       0.88      0.77      0.82       492
                                                 OBJETIVO       0.82      0.49      0.61       286
                           ORÇAMENTO DA SEGURIDADE SOCIAL       0.98      0.97      0.97       127
                                         ORÇAMENTO FISCAL       0.99      0.97      0.98       275
                                         PAIS ESTRANGEIRO       0.98      0.90      0.94       141
                                             RADIODIFUSÃO       1.00      1.00      1.00      3972
                                          REFORMA AGRARIA       1.00      0.98      0.99       997
                                                  REFORÇO       0.97      0.96      0.96       357
                                                RENOVAÇÃO       0.98      0.96      0.97      1411
                                                  SERVIÇO       1.00      0.99      1.00      4048
                                                    TEXTO       0.94      0.96      0.95       105
                                            UNIÃO FEDERAL       0.97      0.86      0.91       515
                                        UTILIDADE PUBLICA       0.95      0.99      0.97       223

                                                micro avg       0.98      0.96      0.97     51180
                                                macro avg       0.94      0.86      0.89     51180
                                             weighted avg       0.98      0.96      0.97     51180
                                              samples avg       0.97      0.95      0.95     51180

Conclusão

Com muita satisfação terminamos a nossa prototipação de modelo de classificação de nomas jurídicas com a entrega de dois modelos baseados em MultiFiT que tiveram performance no conjunto de testes de 97.4% e 97.2%. Esse desempenho os confere potencial real de serem embarcados em sistemas em produção. Por fim, espero que aos que tiveram o interesse de acompanhar o desenvolvimento desse projeto até aqui estejam satisfeito com a solução, da mesma forma que eu. Por fim, no próximo post iremos apresentar o deploy 🖥️ da solução! Até mais!!