Esse é o quarto 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

No último post segmentamos nossos dados em conjunto de treinamento, validação e teste que serão utilizados para diferentes finalidades ao longo do processo de treinamento do nosso modelo de aprendizado de máquina.

xTrain = pd.read_csv("./data/xTrain.csv", sep=';').set_index('legislationIdentifier')
yTrain = pd.read_csv("./data/yTrain.csv", sep=';').set_index('legislationIdentifier')

null_description = xTrain[xTrain['description'].isnull()].index
xTrain = xTrain.drop(null_description)
yTrain = yTrain.drop(null_description)

xValidation = pd.read_csv("./data/xValidate.csv", sep=';').set_index('legislationIdentifier')
yValidation = pd.read_csv("./data/yValidate.csv", sep=';').set_index('legislationIdentifier')

null_description = xValidation[xValidation['description'].isnull()].index
xValidation = xValidation.drop(null_description)
yValidation = yValidation.drop(null_description)

yTrain['keywords'] = yTrain['keywords'].apply(transf_str_into_list)
yValidation['keywords'] = yValidation['keywords'].apply(transf_str_into_list)

Vamos iniciar nosso processo de treinamento, instanciando o TfidfVectorizer que irá construir uma DTM e depois matrix TF-IDF das ementas das normas jurídicas que estamos trabalhando.

#pré processamento
vect = TfidfVectorizer(
    strip_accents='unicode',
    analyzer='word',
    tokenizer=word_tokenize,
    max_features=5000)
multilabel = MultiLabelBinarizer()

Aplicamos as transformações nos dados de treinamento e validação.

yValidation_transformed = multilabel.fit_transform(yValidation['keywords'])
xValidation_transformed = vect.fit_transform(xValidation['description'])
%%time
yTrain_transformed = multilabel.fit_transform(yTrain['keywords'])
xTrain_transformed = vect.fit_transform(xTrain['description'])
Wall time: 6.03 s

Em seguida, vamos criar um baseline para nosso problema de classificação multilabel. Escolhemos a Regressão Logística como ponto de partida do nosso trabalho.

Regressão Logística (baseline)

lr = LogisticRegression()
clf = OneVsRestClassifier(lr)
clf.fit(xTrain_transformed, yTrain_transformed)
OneVsRestClassifier(estimator=LogisticRegression())
y_pred = clf.predict(xValidation_transformed)

Para o caso de classificações multilabel o hamming loss, diferentemente, da função Zero one loss não penaliza um predição caso essa não seja idêntica ao set alvo, isto é, não precisamos de um match exato, portanto, ela apenas penaliza labels de forma individual. Por exemplo, um normativo que possui as seguintes tags: [CREDITO SUPLEMENTAR, REFORÇO, ORÇAMENTO DA SEGURIDADE SOCIAL, DOTAÇÃO ORÇAMENTARIA], mas foi classificado apenas como [CREDITO SUPLEMENTAR, REFORÇO, ORÇAMENTO DA SEGURIDADE SOCIAL] resultará num hamming_loss de 0.25. Os valores de hamming loss pertencem a um intervalo de 0 a 1 e quanto menor o valor, melhor. Para maiores informações. o leitor pode consultar a documentação do sklearn.

def print_hamming_loss(y_true, y_pred, clf):
    print("Clf: ", clf.__class__.__name__)
    print("Hamming loss: {}".format(hamming_loss(y_true, y_pred)))
print_hamming_loss(yValidation_transformed, y_pred, clf)
Clf:  OneVsRestClassifier
Hamming loss: 0.10763268211658625

Apesar de nossa métrica de avaliação ter resultado num valor baixo, que é o desejável, ela ainda pode ser considerada uma variante de uma métrica de acurácia. Portanto, é sabido que em cenários onde o modelo pode predizer apenas a classe majoritária, teremos um score razoável, apesar de não ser considerado um bom modelo. Dito isso, vamos determinar em quantas ocorrências o modelo não resultou em predição nenhuma, isto é, o normativo tem N tags, mas o retorno do modelo foi ausente (0 tags preditas).

len([data for data in y_pred if ~data.any()])/y_pred.shape[0]
0.9648079306071871

O modelo apesar de um hamming loss razoável, em 96,5% das predições não retorna tag alguma. Como exemplificado abaixo:

Exemplo de resultado esperado:

multilabel.inverse_transform(np.atleast_2d(yValidation_transformed[0]))
[('AREA PRIORITARIA',
  'DECLARAÇÃO',
  'DESAPROPRIAÇÃO',
  'DESTINAÇÃO',
  'ESTADO DE MINAS GERAIS MG',
  'IMOVEL RURAL',
  'INSTITUTO NACIONAL DE COLONIZAÇÃO E REFORMA AGRARIA INCRA',
  'INTERESSE SOCIAL',
  'MUNICIPIO',
  'REFORMA AGRARIA')]

Predição realizada:

multilabel.inverse_transform(np.atleast_2d(y_pred[1]))
[()]

Podemos realizar uma inspeção mais detalhada utilizando a função classification_report. Assim, temos evidenciado que a grande maioria das tags temos tanto precision como recall nulos, que no nosso caso em específico podemos interpretar que o modelo não teve capacidade de predição para nenhuma dessas ocorrências.

mapping_tags = list(multilabel.classes_)
print(classification_report(yValidation_transformed, y_pred, target_names=mapping_tags, zero_division=0))
                                                           precision    recall  f1-score   support

                                     ACORDO INTERNACIONAL       0.00      0.00      0.00       505
                                                ALTERAÇÃO       0.00      0.00      0.00      1018
                                                   AMBITO       0.00      0.00      0.00       406
                                                APROVAÇÃO       0.00      0.00      0.00      1944
                                         AREA PRIORITARIA       0.00      0.00      0.00       783
                                                      ATO       0.00      0.00      0.00      1465
                                              AUTORIZAÇÃO       0.00      0.00      0.00       835
                                                   BRASIL       0.00      0.00      0.00       413
                                              COMPETENCIA       0.00      0.00      0.00       445
                                               COMPOSIÇÃO       0.00      0.00      0.00       310
                                                CONCESSÃO       0.00      0.00      0.00      1890
                                               CORRELAÇÃO       0.00      0.00      0.00       263
                                      CREDITO SUPLEMENTAR       0.00      0.00      0.00      1302
                                                  CRIAÇÃO       0.00      0.00      0.00       608
                                                CRITERIOS       0.00      0.00      0.00       390
                                               DECLARAÇÃO       1.00      0.00      0.00      1197
                                           DESAPROPRIAÇÃO       0.00      0.00      0.00      1023
                                               DESTINAÇÃO       0.50      0.00      0.00      1730
                                             DISPOSITIVOS       0.00      0.00      0.00       268
                                     DOTAÇÃO ORÇAMENTARIA       0.00      0.00      0.00      1153
                              EMPRESA DE TELECOMUNICAÇÕES       0.00      0.00      0.00      1749
                                ESTADO DE MINAS GERAIS MG       0.00      0.00      0.00       396
                                   ESTADO DE SÃO PAULO SP       0.00      0.00      0.00       494
                                      ESTADO DO PARANA PR       0.98      0.50      0.66       260
                           ESTADO DO RIO GRANDE DO SUL RS       0.00      0.00      0.00       223
                                                EXECUTIVO       0.00      0.00      0.00       258
                                                 EXECUÇÃO       0.00      0.00      0.00      1870
                                                  FIXAÇÃO       0.00      0.00      0.00       365
                                            FUNCIONAMENTO       0.00      0.00      0.00       305
                                                 HIPOTESE       0.00      0.00      0.00       228
                                             IMOVEL RURAL       0.00      0.00      0.00       793
INSTITUTO NACIONAL DE COLONIZAÇÃO E REFORMA AGRARIA INCRA       0.00      0.00      0.00       794
                                         INTERESSE SOCIAL       0.00      0.00      0.00       899
                                                MUNICIPIO       0.72      0.00      0.01      3258
                                                   NORMAS       0.00      0.00      0.00      1045
                                                 OBJETIVO       0.00      0.00      0.00       511
                           ORÇAMENTO DA SEGURIDADE SOCIAL       0.00      0.00      0.00       323
                                         ORÇAMENTO FISCAL       0.00      0.00      0.00       651
                                         PAIS ESTRANGEIRO       0.00      0.00      0.00       430
                                             RADIODIFUSÃO       0.00      0.00      0.00      1749
                                          REFORMA AGRARIA       0.00      0.00      0.00       793
                                                  REFORÇO       0.00      0.00      0.00      1124
                                                RENOVAÇÃO       0.00      0.00      0.00       631
                                                  SERVIÇO       0.00      0.00      0.00      1832
                                                    TEXTO       0.00      0.00      0.00       348
                                            UNIÃO FEDERAL       0.00      0.00      0.00      1224
                                        UTILIDADE PUBLICA       0.00      0.00      0.00       320

                                                micro avg       0.49      0.00      0.01     40821
                                                macro avg       0.07      0.01      0.01     40821
                                             weighted avg       0.11      0.00      0.01     40821
                                              samples avg       0.02      0.00      0.00     40821

Pré-Processamento & Engenharia de Atributos (Feature Engineering)

Pré-Processamento

No presente momento, as nossas únicas variáveis preditoras são os tokens oriundos das ementas dos normativos que são submetidos a um pipeline que inicialmente os convertem a uma matrix DTM e por conseguinte os transformam em uma representação tf-idf. Para esse conjunto de operações a biblioteca sklearn fornece a classe TfidfVectorizer que realiza esse conjunto de operações.

Para que seja possível fazer uma inspeção das nossas variáveis preditoras, iremos recorrer a uma wordcloud para ter uma dimensão da frequência de ocorrência dos diferentes tokens no corpus em análise.

def make_word_cloud(series: pd.Series, **kwargs):
    stopwords = kwargs.get('stopwords')
    corpus_normativos = " ".join(series)
    tokenize_corpus = word_tokenize(corpus_normativos)
    if stopwords:
        print(len(stopwords))
        wordcloud = WordCloud(width=1024, height=768, stopwords=stopwords, margin=0).generate(corpus_normativos)
    else:
        wordcloud = WordCloud(width=1024, height=768, margin=0).generate(corpus_normativos)
    #show
    plt.figure(figsize=[15, 9])
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.axis("off")
    plt.show()
    return wordcloud
wc = make_word_cloud(xTrain['description'])

#collapse
def get_top_n_words(corpus, n=None):
    vec = CountVectorizer().fit(corpus)
    bag_of_words = vec.transform(corpus)
    sum_words = bag_of_words.sum(axis=0) 
    words_freq = [(word, sum_words[0, idx]) for word, idx in vec.vocabulary_.items()]
    words_freq = sorted(words_freq, key = lambda x: x[1], reverse=True)
    return words_freq[:n]
df_common_words = pd.DataFrame(get_top_n_words(xTrain['description']), columns = ['token' , 'frequencia'])

Pelo gráfico abaixo que inspecionamos os 30 tokens mais frequentes no corpus de treinamento, identificamos que muitos destes são considerados stop words, bem como há ocorrências de numerais. Dito isso, o próximo passo do nosso pipeline de pré-processamento será compostos pelas seguintas etapas:

  • Remoção de um conjunto delimitado de stopwords;
  • Excluir tokens que sejam compostos exclusivamente de dígitos;
  • Normalizar os tokens de forma a retirar acentos e caracteres especiais.
  • Correção de grafia de tokens

#collapse
alt.Chart(df_common_words[:30].sort_values(by='frequencia', ascending=False)).mark_bar().encode(
    alt.X('frequencia:Q', title='Frequência'),
    alt.Y("token:N", sort='-x'),
    tooltip=[alt.Tooltip('frequencia', title='Frequência')]
    )\
    .properties(height=500)\
    .configure_mark(color=default_color_2)    

a) Remoção de um conjunto delimitado de stopwords

Vamos utilizar uma lista compiladas de stopwords listadas nesse GIST.

b) Excluir tokens que sejam compostos exclusivamente de dígitos.

def hasNumbers(inputString):
    return any(char.isdigit() for char in inputString)

c) Normalizar os tokens de forma a retirar acentos e caracteres especiais.

def removerAcentosECaracteresEspeciais(palavra):
    # Unicode normalize transforma um caracter em seu equivalente em latin.
    nfkd = unicodedata.normalize('NFKD', palavra)
    palavraSemAcento = u"".join([c for c in nfkd if not unicodedata.combining(c)])
    #retirar espaços vazios múltiplos
    palavraSemAcento = re.sub(' +', ' ', palavraSemAcento)
    # Usa expressão regular para retornar a palavra apenas com números, letras e espaço
    return re.sub('[^a-zA-Z0-9 \\\]', '', palavraSemAcento).lower()

d) Correção de grafia de tokens

Nessa etapa faremos a correção da ocorrência de alguns tokens que estão com grafia incorreta, ou duas palavras que não estão separadas por espaço, ou mesmo palavras no plural que vamos transformá-la para o singular.

#collapse
def split_selected_tokens(texto: str):
    select_tokens = {
        'dedezembro' : 'de dezembro',
        'capitalsocial' : 'capital social',
        'decretosleis' : 'decretos leis',
        'nacionaldestinada' : 'nacional destinada',
        'professorequivalente' : 'professor equivalente',
        'tvsbt' : 'tv sbt',
        'orcamentariavigente' : 'orcamentaria vigente',
        'tecnicoprofissional' : 'tecnico profissional',
        'republicafederativa' : 'republica federativa', 
        'transbrasilianaconcessionaria' : 'transbrasiliana concessionaria',
        'publicoprivada' : 'publico privada',
        'procuradorgeral' : 'procurador geral',
        'auditoriafiscal' : 'auditoria fiscal',
        'decretoslei' : 'decretos lei',
        'segurancapublica' : 'seguranca publica',
        'novadutra' : 'nova dutra',
        'queespecifica' : 'que especifica',
        'agenciasreguladoras' : 'agencias reguladoras',
        'autopistalitoral' : 'autopista litoral',
        'goiasvigencia' : 'goias vigencia',
        'governadorcelso' : 'governador celso',
        'autopistaregis' :'autopista regis',
        'valecultura' : 'vale cultura',
        'programacaoorcamentaria' : 'programacao orcamentaria',
        'dopoder' : 'do poder',
        'ministeriodo' : 'ministerio do',
        'estadosmembros' : 'estados membros',
        'papelmoeda' :'papel moeda',
        'bolsaatleta' : 'bolsa atleta',
        'complementacaoeconomica' : 'complementacao economica',
        'artisticoculturais' :'artistico culturais',
        'conselhogeral' : 'conselho geral',
        'tecnicoadministrativos' : 'tecnico administrativos',
        'registradosacrescentando' : 'registrado sacrescentando',
        'ruralcontratadas' : 'rural contratadas',
        'estudantesconvenio' : 'estudantes convenio',
        'delegadode' : 'delegado de',
        'junhode' : 'junho de',
        'denovembro' : 'de novembro',
        'lavourapecuariafloresta' : 'lavoura pecuaria floresta',
        'autorizadaaltera' : 'autorizada altera',
        'grupodefesa' : 'grupo defesa',
        'contribuicoesprevidenciarias' : 'contribuicoes previdenciarias',
        'cpfgce' : 'cpf gce',
        'empregose' : 'empregos e',
        'dosanistiados' : 'dos anistiados',
        'antigomobilismo' : 'antigo mobilismo',
        'dogrupodirecao' : 'do grupo direcao',
        'milhoestrezentos' : 'milhoes trezentos',
        'deavaliacao' : 'de avaliacao',
        'salariominimo' : 'salario minimo',
        'imoveis' : 'imovel',
        'grupodirecao' : 'grupo direcao',
        'pispasep' : 'pis pasep',
        'rurais' : 'rural'
    }
    new_text = texto.split(" ")
    container =[]
    for word in new_text:
        if word in select_tokens.keys():
            container.append(select_tokens[word])
        else:
            container.append(word)
    return " ".join(container)

Função de pré-processamento

Por fim, vamos consolidar uma função que agrega as nossas etapas de pré-processamento.

def pre_processing_pipeline(texto:str):
    texto = " ".join([token for token in word_tokenize(removerAcentosECaracteresEspeciais(texto)) if not token.isdigit()])

    texto = " ".join([token for token in word_tokenize(texto) if not hasNumbers(token)])

    texto = " ".join([token for token in word_tokenize(texto) if len(token) > 2])
    texto = " ".join([split_selected_tokens(token) for token in word_tokenize(texto) if not token.isdigit()])
    return texto

Vamos retirar acentos e caracteres especiais do nosso conjunto de stopwords.

stopwords_2 = []
for word in stopwords: 
    stopwords_2.append(removerAcentosECaracteresEspeciais(word))

Feature Engineering

Uma vez determinado alguns passos adicionais de pré-processamento, podemos avaliar a construção de algumas novas features para serem incorporadas ao nosso modelo de dados. Inicialmente, vamos analisar como se distribuem o tamanho das ementas do nosso conjunto de treinamento.

text_size = pd.DataFrame(xTrain['description'].str.len()).rename(columns={'description' : 'size'}).reset_index()