Machine learning para detecção de discurso racista — Parte 2

Desenvolvendo modelo lemmatizado e sem stopwords

Bruno Carloto
11 min readMar 23, 2022

1 Introdução

O presente artigo é continuação da primeira parte do projeto de desenvolvimento de detector de discurso racista. Para compreendê-lo, é indicado iniciar a leitura a partir do primeiro artigo, acessível ao clicar aqui.

O primeiro artigo encerrou-se com o desenvolvimento de cinco modelos de machine learning, dos quais, dois foram selecionados para análise mais detalhada, dentre os quais, um foi colocado em produção.

Os modelos obtiveram as seguintes performances:

Para a presente etapa, serão inseridas a lematização e a remoção de stopwords, no momento de pré-processamento dos dados. Espera-se, com isso, obter resultados melhores do que os primeiros, reduzir overfitting e gerar um modelo mais generalista.

Lematização

A lematização é um processo em que determinadas palavras são convertidas para suas formas integrais, isso é, de onde derivam, conforme exemplifica a ilustração abaixo:

Dessa forma, tende-se a reduzir vocabulário e diversidade de discurso, o que pode auxiliar na generalização do aprendizado do modelo, além da redução da dimensão do vetor.

Stopwords

Stopwords são palavras frequentes em discursos de uma língua, que, de certa forma, não apresentam relevância no aprendizado de máquina. Em português, as stopwords são as seguintes:

Conteúdo

1 — Introdução

2 — Limpeza e pré-processamanto

3 — Geração de vetor

4 — Treinamento e teste de modelos de machine learning

Considerações finais

2 Limpeza e pré-processamento

As bibliotecas já importadas, no primeiro artigo, são:

--- Bibliotecas importadas
numpy, pandas, nltk, re e string

Para realizar o procedimento de lematização, importo a biblioteca spacy, a qual junta-se às já importadas, e carrego as configurações do português.

#Importando biblioteca para lematização
import spacy
#Definindo a configuração português
nlp = spacy.load("pt_core_news_sm")

O desenvolvimento dos modelos lematizados e sem stopwords inicia-se do dataset original, isso é, os dados em seus estados de importação. Para relembrá-lo, mostro as 10 primeiras linhas e as cinco últimas.

Inicio com as 10 primeiras linhas:

#Importando dataset
discursos_df = pd.read_csv(r"C:\Users\Usuario\OneDrive\Ciência_de_Dados\NLP\injuria\dataset_injuria.txt",
sep='\t', encoding='utf-8')#Visualizando
discursos_df.head(10)

Em seguida, com as cinco últimas:

#Cinco últimas linhas do dataset
discursos_df.tail()

Como o índice do dataframe começa em zero, o último índice é 22.316. Portanto, há 22.317, como pode-se conferir no código a seguir:

#Dimensão do dataset
discursos_df.shape

No tocante às classes, ou seja, os discursos racistas e os não racistas, há balanceamento. O balanceamento é importante, pois auxilia no aprendizado não tendencioso, porquanto, havendo excessivamente predominância de uma classe, o modelo de machine learning pode aprender mais sobre essa, sendo incapaz de generalizar frente à realidade, como uma criança que viu muitas vezes uma determinada raça de cachorro e pouquíssimas vezes, outra raça. Ao ver essa raça pouco vista, não terá facilidade em identificar que é um cachorro.

A confirmação acerca do balanceamento dos dados é proporcionada pelo código abaixo.

#Contagem das quantidades de discursos racistas e não racistas
np.unique(discursos_df.classificacao, return_counts=True)

Há 11.861 discursos não racistas, ou seja, da classe 0, e 10.456 discursos racistas, isso é, da classe 1. Portanto, pode-se considerar que os dados estão balanceados.

Novamente separo os discursos em uma variável, nomeada discurso. A partir dessa variável, realizo alguns tratamentos sobre os textos.

#Imprimindo os textos
discursos = discursos_df.discurso
discursos[:10]

Como pode-se observar, os discursos não apresentam vírgula, pontos, entre outras pontuações gramaticais, conforme já visto no primeiro artigo. A fim de que, após a lematização feita, as sentenças possam ser reconstruídas, utilizarei o recurso do ponto final ao fim de cada sentença. Dessa forma, o aplico pelo seguinte código:

#Aplicando ponto final ao fim das sentenças
for i in range(len(discursos)):
sentenca = discursos[i].split()
discursos[i] = sentenca+['.']
#Resultado
discursos[0]

O procedimento, além de agregar um ponto final às sentenças, tokenizou suas palavras constituintes.

Para trabalhar a lematização, necessito transformar as sentenças em um documento da biblioteca spacy. Para isso, devo reunir as palavras tokenizadas anteriormente. Assim, as sentenças ficarão completas.

#Unificando os tokens dos discursos: formando sentenças e listas
discursos_unidos = []
for i in range(len(discursos)):
sentenca = ' '.join(discursos[i])
discursos_unidos.append(sentenca)
#Resultado
discursos_unidos[0], type(discursos_unidos[0])

O procedimento foi bem sucedido.

Para transformar as sentenças em documentos da biblioteca spacy, gero o programa a seguir, que além da transformação, tokeniza as sentenças:

#Documentando, por spacy, cada linha
doc = []
for i in range(len(discursos_unidos)):
doc.append(list(nlp((discursos_unidos[i]))))
#Resultado
print(doc[:10])

A lematização será sobre somente as classes verbos e advérbios. Sendo assim, crio o seguinte programa:

#Lemmatização
lista = []
for i in range(len(doc)):
for token in doc[i]:
if token.pos_ in ['VERB', 'ADV']:
lista.append(str(token.lemma_))
elif token.pos_ not in ['VERB', 'ADV']:
lista.append(str(token))

Começo o programa criando uma lista vazia. Essa lista vazia receberá todas as sentenças, porém, tokenizadas, e os verbos e advérbios lematizados.

Ao rodar o programa, houve sucesso.

#Resultado
print(lista[:50])

Como pode-se notar, as sentenças estão em apenas uma lista. O que garante agora a separação em sub-listas, ou por sentenças, é justamente o ponto final.

Para reconstrução das sentenças, já com os dados tratados, utilizo o código abaixo.

#Reconstruindo as sentenças
lista_definitiva = []
lista_provisoria = []
for token in lista:
tk = re.sub(r",'", '', token)
if tk == '.':
lista_provisoria.append(token.lower())
lista_definitiva.append(list(lista_provisoria))
lista_provisoria = []
elif tk != '.':
lista_provisoria.append(token.lower())

O resultado do processo está abaixo.

#Resultados
lista_definitiva[:10], type(lista_definitiva[0])

De acordo com o retornado, as sentenças foram reconstruídas.

O último tratamento é a remoção de stopwords. Para isso, crio o programa a seguir:

#Retirando stopwords
for i in range(len(lista_definitiva)):
for j in range(len(lista_definitiva[i])):
if lista_definitiva[i][j] in stopword:
lista_definitiva[i][j] = ''
#Resultado
lista_definitiva[:10]

Observando as sentenças, conforme definido, as stopwords foram substituídas por um espaço vazio. Esse espaço vazio será entendido como uma informação, pelo modelo, entretanto, não interferirá em seu aprendizado, já que está presente em todas ou quase todas as sentenças, e aumentará, em apenas uma dimensão, o vetor diminuído pela lematização e remoção de stopwords.

3 Geração de vetor

Para geração do vetor e compreensão de sua dimensionalidade, crio, inicialmente, um dicionário de palavras únicas.

#Criando o dicionário com as palavras únicas
#Definindo dicionário
dicionario = set()
#Adicionando palavras no dicionário
for lista in lista_definitiva:
dicionario.update(lista)
#Imprimindo palavras do dicionário
print(dicionario)

Criado o dicionário, observo a quantidade de palavras únicas.

#Buscando total de palavras
total_de_palavras = len(dicionario)
print(total_de_palavras)

Com a remoção de stopwords e a lematização, o dicionário de palavras únicas foi de 410 palavras para 329 palavras, uma redução de aproximadamente 20%. Portanto, de 410 dimensões, o vetor foi para 329 dimensões.

A segunda fase para criação do vetor, é a criação de um dicionário de posição de palavra. A partir da posição das palavras do dicionário de palavras únicas, o crio.

#Criando dicionário palavra-posição
palavra_posicao = dict(zip(dicionario, range(total_de_palavras)))
print(palavra_posicao)

Conforme o retornado, o procedimento foi bem efetuado.

Para criação do vetor, crio a seguinte função:

#Função para vetorização de discursos
def vetorizacao(texto):
vetor = [0] * total_de_palavras
for token in texto.split():
token = token.lower()
if token in palavra_posicao:
posicao = palavra_posicao[token]
vetor[posicao] += 1
return vetor

Testo a função da seguinte forma:

#Testando função
frase = 'preto preto preto é legal mal mal legal louco maldito maldito maldito maldito'
print(vetorizacao(frase))

Ao total, foram inseridas seis palavras, entretanto, no vetor, foram contabilizadas cinco. Isso demonstra o efeito da lematização e/ou da remoção de stopwords. O procedimento de vetorização foi efetuado devidamente.

Após isso, vetorizo todas as sentenças.

#Vetorizando os discursos da amostra
vetores_dos_discursos = [vetorizacao(discurso) for discurso in discursos_unidos]
#Testando eficácia do procedimento
print(vetores_dos_discursos[3])

O procedimento foi bem sucedido.

4 Treinamento e teste de modelos de machine learning

4.1 Separando os dados em dados de treinamento e de teste

Inicio o desenvolvimento dos modelos, propriamente dito, para seleção de um, separando os discursos vetorizados e suas respectivas classes em distintas variáveis para criação de um data frame, pelo qual possa-se observá-los conjuntamente.

#Separando as classes
classificacao_injuria = discursos_df.classificacao#Visualizando os vetores em conexão com classificação
pd.DataFrame({'vetor':vetores_dos_discursos, 'classe':classificacao_injuria}).head(10)

O resultado está apropriado.

Os procedimentos a seguir serão iguais aos aplicados para o treinamento e teste do modelo de base, sendo assim, “resseparo” os discursos (vetorizados) e as classes, estocando-os nas variáveis X e y.

#Separando variáveis preditoras e variável dependente
X = np.array(vetores_dos_discursos)
y = np.array(classificacao_injuria)

Após separação, importo funções para separação dos dados em treinamento e teste ( train_test_split ) e para validação de treinamento ( cross_val_score ).

#Importando train test split
from sklearn.model_selection import train_test_split, cross_val_score

Em seguida, separo os dados em dados de treinamento e dados de teste.

#Repartindo os dados
x_treinamento, x_teste, y_treinamento, y_teste = train_test_split(X, y, test_size=0.2, random_state=1)#Dimensões dos x's e dos y's
x_treinamento.shape, x_teste.shape, y_treinamento.shape, y_teste.shape

Aproximadamente 18 mil dados fazem parte da base de treinamento e em torno de 4.500 constituem os dados de teste.

4.2 Treinando e testando os modelos

Inicio criando função de treinamento. Através dessa função, observo o desempenho dos algoritmos de machine learning selecionados. Pela análise do desempenho, seleciono os melhores, de acordo com a complexidade do algoritmo e suas consequências (positivas e negativas), para análise mais detalhada.

#Definindo função de treinamento
def predicao(nome_do_modelo, modelo, x_train, y_train):
k = 10
scores = cross_val_score(modelo, x_train, y_train, cv=k)
taxa_media_de_acerto = np.mean(scores)
return print('Taxa média de acerto do {0}: {1}%'.format(nome_do_modelo, np.round(taxa_media_de_acerto, 2)*100))

Após criação da função, aplico-a conseguinte à importação dos algoritmos de machine learning.

#Definindo dicionário de resultados
#Modelo Gaussian NB
from sklearn.naive_bayes import GaussianNB
modelo_gaussiano = GaussianNB()
resultado_gaussiano = predicao('Gaussian NB', modelo_gaussiano, x_treinamento, y_treinamento)#Modelo Multinomial NB
from sklearn.naive_bayes import MultinomialNB
modelo_multinomial = MultinomialNB()
resultado_multinomial = predicao('Multinomial NB', modelo_multinomial, x_treinamento, y_treinamento)#Modelo Decision Tree
from sklearn.tree import DecisionTreeClassifier
modelo_arvore_de_decisao = DecisionTreeClassifier(max_depth=15)
resultado_arvore_de_decisao = predicao('Decision Tree Classifier', modelo_arvore_de_decisao, x_treinamento, y_treinamento)#Modelo Linear SVC
from sklearn.svm import LinearSVC
modelo_linear_svc = LinearSVC(random_state=0)
resultado_linear_svc = predicao('Linear SVC', modelo_linear_svc, x_treinamento, y_treinamento)#Modelo Linear Regression
from sklearn.linear_model import LogisticRegression
modelo_regressao_logistica = LogisticRegression()
resultado_regressao_logistica = predicao('Logistic Regression', modelo_regressao_logistica, x_treinamento, y_treinamento)

Aqui, podemos comparar o desempenho dos modelos, tendo como diferença os pré-processamentos de lematização e remoção de stopwords:

Observando cada modelo, antes e depois do pré-processamento com lematização e remoção de stopwords, pode-se perceber que os modelos lematizados e sem stopwrods desenvolvidos, a partir dos mesmos algoritmos de machine learning dos modelos de base, são menos acurados. Todavia, isso pode indicar não simplesmente em piora do modelo, mas, também em redução do overfitting.

Dentre esses modelos, os dois mais acurados são os de regressão logística e de support vector machine classifier. Entretanto, considerando a possibilidade de overfitting, seleciono os modelos multinomial e regressão logística para análise detalhada.

Para isso, treino novamente esses dois últimos modelos mencionados acima, contudo, separadamente da função.

#Treinando multinomial e regressão logística
modelo_multinomial.fit(x_treinamento, y_treinamento), modelo_regressao_logistica.fit(x_treinamento, y_treinamento)

Em seguida, realizo as predições armazenando-as em variáveis respectivas.

#Predizendo com ambos modelos
predicao_multinomial = modelo_multinomial.predict(x_teste)
predicao_logistica = modelo_regressao_logistica.predict(x_teste)

Para avaliação detalhada, importo o classification_report

#Importando classification report para análise dos modelos
from sklearn.metrics import classification_report

Por fim, avalio o desempenho dos modelos detalhadamente.

#Desempenho do modelo multinomial
print(classification_report(predicao_multinomial, y_teste))

O classification_report fornece algumas métricas. Qual olhar primordialmente? Nesse contexto, como mencionado na parte 1 desse projeto, é preferível classificar erroneamente um discurso não racista como racista do que um discurso racista como não racista. Portanto, o objetivo é reduzir falsos negativos, quando um discursos racista é definido como não racista, dessa forma, olha-se cautelosamente para o recall. O modelo multinomial alcançou um recall de 90% na identificação dos discursos não racistas e 81% na identificação dos discursos racistas. Em outras palavras, em 1 mil oportunidades de identificar discursos racistas, cobriu 810, deixando descobertos ao público, 190 casos de discursos racistas. Esse valor equivale a um aumento de erro em torno de 5%, em comparação ao modelo multinomial de base.

Olhando para o desempenho do modelo de regressão logística, tem-se:

#Desempenho do modelo de regressão logística
print(classification_report(predicao_logistica, y_teste))

Observando o recall, vê-se que, em 1 mil oportunidades, o modelo cobriu 880 discursos racistas, deixando descobertos ao público, 120 discursos racistas, o que equivale ao um aumento de erro em torno de 40%.

Considerações finais

Após a aplicação de lematização e remoção de stopwords, houve uma piora nos modelos. A identificação de discursos racistas sofre impacto negativo. Isso pode ser devido à quantidade de palavras únicas e de discursos únicos presentes no corpus (dataset). Após a aplicação desses procedimentos, a quantidade de palavras únicas foi reduzida ainda mais. O apropriado seria o desenvolvimento de um modelo com um corpus adequado, o que já foi salientado não ser o caso do usado para o desenvolvimento desse projeto.

Embora haja a piora dos modelos, o modelo multinomial lematizado e sem stopwords foi colocado em produção para teste. Após os testes, o desempenho do modelo é similar ao não lematizado e com stopwords.

Para experimentar o modelo, basta clicar aqui.

--

--

Bruno Carloto

Bem-vindo ao Deep Analytics, um blog que aborda de forma técnica o mundo Analytics | LinkedIn: www.linkedin.com/in/bruno-rodrigues-carloto