Reconstruindo a Caixa-Preta: Limpando os dados para um projeto de NLP

--

Um homem apresentando um trabalho, apontando para um slide projetado
Michel Gomes apresentando a oficina “Por dentro da caixa-preta: uma abordagem prática de IA e NLP”

No último final de semana participei da Conferência Brasileira de Jornalismo de Dados e Métodos Digitais (Coda.Br), o principal evento de jornalismo de dados da América Latina. O evento se iniciou no sábado (18) com um painel sobre como os dados podem ser importantes para reportagens de direitos humanos e continuou com muitas conferências, falando de mudanças climáticas até população LGBTQIA+. Além disso tiveram muitas oficinas. A que quero dar ênfase nesse texto é a “Por dentro da caixa-preta: uma abordagem prática de IA e NLP”, ministrada por Henrique Rieger e Michel Gomes, do Núcleo Jornalismo.

Em resumo, os dois explicaram como funciona uma Inteligência Artificial e ensinam a criar um modelo de Natural Language Process (Processamento de Linguagem Natural ou NLP, na sigla em inglês). Henrique e Michel construíram um notebook documentado as funções, explicaram o contexto da atividade e, no final, construíram uma IA que classificava a possibilidade de uma ementa ser aceita ou rejeitada.

Mas no final eles explicaram que tiveram um problema: muitos dos dados estavam duplicados e isso acabou acabou gerando um vazamento. Isso quer dizer que algumas das ementas usadas no treinamento acabaram sendo usadas novamente no teste, ou seja, testaram com algo que o modelo já conhecia.

O que fiz, então, foi limpar os dados e seguir novamente os passos para recriar o modelo. Também mudei alguns dos hiperparâmetros de treino para escolher o melhor, usando a cross validação ensinada na oficina. Aqui está o Notebook original, ideal para quem está começando. E aqui meu github com os dados, o Notebook com as alterações e com o método que fiz para limpar os dados e que explico com mais detalhes aqui.

Baixando os dados

O primeiro passo foi encontrar os dados no site da ALESP. É necessário buscar pelas Proposituras e pelos Pareceres. O site sempre tem atualizações, então se quiser usar os mesmos que eu, baixe do meu github.


# subindo os dados

# proposituras são as ementas
# proposituras_parecer tem a coluna para saber se foi classsificado com o parecer favorável ou contrário

df_1 = pd.read_xml('/content/proposituras.xml')
df_2 = pd.read_xml('/content/propositura_parecer.xml')

Com esses arquivos baixados é só subir e juntar. Os dois têm a coluna IdDocumento, então não é necessário fazer nenhuma mudança. Depois disso eu somente selecionei as colunas que quero: Ano, Ementa, Parecer e Id, as mesmas que estavam nos dados originais.

# juntando a ementa e seu parecer
# ambas tem a coluna 'IdDocumento'

df = df_1.merge(df_2)

# filtrando apenas as informações necessárias

df = df[['AnoLegislativo', 'Ementa', 'TipoParecer', 'IdDocumento']]

Depois excluo as ementas que estão duplicadas. Decido não manter nenhuma porque alguns dos textos estão classificados ao mesmo tempo como contrário e como favorável. Por isso o Keep=False.


# eliminando as ementas duplicadas

# muitos casos tinham ementas iguais e que, uma hora tinham sido consideradas
# contrárias e outra hora favorável. Por isso ambas são excluídas

df = df.drop_duplicates(subset='Ementa', keep=False)

E há muitos tipos de classificação (“favorável ao projeto com emenda”, “favorável ao substitutivo”, etc.), então decido manter apenas as favoráveis e as contrárias, como estava antes. Divido em dois DataFrames. E como o tamanho dos DataFrames é muito diferente, decido balancear por ano. Por exemplo: no ano de 2011 existem 44 favoráveis e 44 contrários. Cada ano tem o mesmo número de favorável e de contrário. Exceto 1996 que tem um a mais para o contrário, então excluo um deles.

# selecionando apenas quem tem aprecer favorável ou contrário

df_contrario = df.query('TipoParecer == "contrário"')
df_favoravel = df.query('TipoParecer == "favorável"')


# criando um dataframe
df_provisorio_fav = pd.DataFrame()


# Selecionando o mesmo número de favoráveis e não favoráveis por ano.
# exemplo: no ano de 2011 existem 44 favoráveis e 44 contrários

for index_ano, num_item in df_contrario.drop_duplicates(subset='Ementa')['AnoLegislativo'].value_counts().iteritems():
if len(df_favoravel.query(f'AnoLegislativo == {index_ano}')) >= num_item:
df_parte = df_favoravel.query(f'AnoLegislativo == {index_ano}').sample(num_item)
else:
df_parte = df_favoravel.query(f'AnoLegislativo == {index_ano}')
df_provisorio_fav = pd.concat([df_provisorio_fav, df_parte])


# juntando os contrários e os favoráveis
df = pd.concat([df_provisorio, df_contrario])


# excluindo uma linha que tinha a mais no contrário
df.drop(axis=0, index=82957, inplace=True)

Por fim mudo o nome das colunas e exporto os dados para serem usados novamente, no mesmo processo ensinado.


# mudando o nome da coluna para como foi usado anteriormente
df.rename(columns={'AnoLegislativo' : 'ano',
'Ementa' : 'ementa',
'TipoParecer' : 'classe',
'IdDocumento' : 'id_prop'}, inplace=True)

# exportando os dados
df.to_csv('ementas_classe.csv', index=False)

Bônus

Uma coisa que Henrique e Michel explicaram no finalzinho foi como escolher o melhor parâmetro. Já cheguei a trabalhar criando um modelo de Inteligência Artificial que classificava textos, mas não sabia da classificação cruzada. Ele cruza todos os hiperparâmetros que você seleciona e depois mostra qual foi o melhor resultado. Originalmente estava:

mlp = MLPClassifier(
early_stopping=True,
activation='tanh',
max_iter=100,
random_state=42,
verbose=True)

E mudei para:

mlp = MLPClassifier(
early_stopping=True,
activation='logistic',
max_iter=500,
random_state=42,
verbose=True)

Ainda tem mais coisas que quero falar sobre o Coda.Br e sobre dados, então acompanhe o blog. E se tiver alguma dúvida, pode entrar em contato comigo através do LinkedIn.

--

--