Natural Language Processing, Classificação e Espaços Vetoriais

Alysson Guimarães
Data Hackers
Published in
37 min readJun 28, 2024
Generated with AI ∙ April 12, 2024 at 3:30 PM

1 Introdução

Os algoritmos de Machine Learning (ML) supervisionados são um tipo de algoritmo que aprende a partir de dados rotulados, ou seja, dados que já possuem um rótulo ou uma classificação pré-definida. Esses algoritmos usam esses dados rotulados para aprender a fazer previsões ou classificações em novos dados.

Na área de Processamento de Linguagem Natural (NLP), os algoritmos de ML supervisionados são usados para uma variedade de tarefas, como classificação de texto (ex. sentimento), identificação de entidades nomeadas (NER), análise de tópicos, tradução automática, entre outras.

Um exemplo de como esses algoritmos são usados em NLP é na classificação de sentimentos em textos. Nesse caso, um modelo de ML supervisionado seria treinado em um conjunto de dados rotulados que contém textos e suas respectivas classificações de sentimento (por exemplo, positivo, negativo ou neutro). O algoritmo usaria esses dados para aprender a reconhecer padrões nos textos e, em seguida, aplicaria esses padrões para classificar o sentimento em novos textos. Outro exemplo é na identificação de entidades nomeadas (NER), como já supracitado, que é uma tarefa que envolve a identificação de nomes de pessoas, locais, organizações e outras entidades em um texto. Nesse caso, um modelo de ML supervisionado seria treinado em um conjunto de dados rotulados que contém textos e suas respectivas entidades nomeadas. O algoritmo usaria esses dados para aprender a reconhecer os padrões de palavras e contextos que indicam a presença de uma entidade nomeada em um texto, e, em seguida, aplicaria esses padrões para identificar entidades em novos textos.

Esses algoritmos são uma técnica poderosa para resolver problemas em NLP, permitindo que modelos aprendam com dados rotulados e possam fazer previsões ou classificações em novos dados com base no que foi aprendido.

Um pipeline básico para a task ou tarefa de análise de sentimento (classificação) geralmente envolve as etapas de:

  • Pré-processamento de texto: Esta etapa envolve a limpeza e preparação dos dados. O texto é normalmente convertido em minúsculas, removidos caracteres especiais, removidos números e pontuações, e realizada a tokenização do texto para obter as palavras individuais.
  • Criação de features: Nesta etapa, são criadas as features que serão usadas pelo modelo de classificação. As features podem incluir a contagem de palavras, a frequência de palavras, o tipo de palavras, o uso de negação, entre outras.
  • Treinamento do modelo: O modelo de classificação é treinado em um conjunto de dados rotulados que contêm exemplos de texto e suas respectivas classificações de sentimento. Existem vários algoritmos de aprendizado de máquina que podem ser usados para treinar um modelo, como Árvores de Decisão, Naive Bayes, Regressão Logística, SVM, Redes Neurais, etc.
  • Avaliação do modelo: Após o treinamento, o modelo é avaliado em um conjunto de dados de teste para verificar sua precisão. É comum dividir o conjunto de dados em conjunto de treinamento, validação e teste.
  • Implementação/Deploy: Por fim, o modelo treinado é usado para classificar o sentimento de novos textos. Novos textos passam pelas mesmas etapas de pré-processamento e criação de features, e o modelo treinado é usado para prever a classificação de sentimento do texto.

Neste artigo, abordaremos alguns tópicos desse pipeline de tarefas de NLP, como Pré-processamento de texto, Criação de features, Treinamento e avaliação de modelos.

O artigo está organizado da seguinte forma: A seção 2 descreve como extrair features e realizar o preprocessamento de dados, na seção 3 são introduzidos os modelos de regressão logística e algoritmo naive bayes, e como aplicá-los na tarefa de classificação de texto para análise de sentimento. Na seção 4 são discutidos os Vector Space Models, como eles representam palavras e documentos e como construir matrizes de coocorrencia no formato word by word e word by document, como avaliar a similaridade de vetores com a distância euclidiana e similaridade de cosseno, como manipular palavras nos espaços vetoriais e o algoritmo Principal Component Analysis e como utilizá-lo. Por fim, Na seção 5 é apresentado a conclusão.

2 Vocabulary and Feature Extraction

Para representar textos de forma numérica, primeiro precisamos construir um vocabulário, com eles poderemos _encodar_ qualquer texto como um array de números. Um Vocabulário é uma lista com palavras únicas, não repetidas.

Uma forma simples de extrair features do texto é usando o vocabulário, verificando cada palavra do vocabulário que aparece no texto. Caso as palavras no texto que estamos extraindo a feature tenham apareca no vocabulário, atribuímos o valor 1 para ela, e zero para as palavras que do vocabulário que não aparecem no texto. Assim, estamos representando o texto usando one-hot encoding ou representação esparsa. Mas esse método pode ser problemático porquê o número de features é igual ao número de palavras no vocabulário e a grande maioria das features serão zero, aumentando excessivamente o tempo de treino e predição dos modelos.

Existem diversas técnicas para representar textos como vetores, sendo as mais comuns a Bag of Words (BoW) e a Representação Distribuída de Palavras (Word Embeddings). Vou explicar brevemente cada uma delas:

  • Bag of Words (BoW): Nessa técnica, o texto é representado como um vetor contendo a contagem de ocorrências de cada palavra presente no texto. Cada palavra é considerada como uma dimensão do vetor. Dessa forma, quanto mais vezes uma palavra aparecer no texto, maior será o valor correspondente na dimensão correspondente no vetor. Por exemplo, se um texto contém as palavras “gato”, “cão” e “casa”, a representação BoW seria um vetor com três dimensões, com valores correspondentes à contagem de ocorrências de cada palavra no texto.
  • Word Embeddings: Essa técnica é baseada em modelos de linguagem neural, que mapeiam cada palavra em um espaço vetorial de alta dimensão, onde palavras semelhantes têm representações próximas. A ideia é que cada palavra seja representada por um vetor de números reais que captura seu significado semântico. Esses vetores podem ser aprendidos a partir de grandes quantidades de textos usando técnicas de aprendizado de máquina, como Word2Vec, GloVe ou FastText. Os vetores resultantes podem ser usados para representar cada palavra em um texto como um vetor numérico. A representação distribuída de palavras pode capturar relações semânticas entre palavras, como sinonímia e antonímia, e pode ser usada para tarefas mais complexas, como análise de sentimento ou classificação de texto.

Ambas as técnicas são amplamente utilizadas em NLP, dependendo do objetivo e do contexto da tarefa em questão. A escolha da técnica de representação de texto pode influenciar significativamente o desempenho do modelo de aprendizado de máquina, e é importante escolher a técnica mais adequada para a tarefa específica.

Tanto a técnica de Bag of Words (BoW) quanto a Word Embeddings têm seus pontos fortes e fracos, e a escolha de qual usar depende do contexto e do objetivo da tarefa em questão.

A representação BoW pode ser uma escolha adequada para tarefas simples de classificação de texto, como classificação de spam ou análise de sentimento, onde a presença ou ausência de palavras específicas pode ser um indicador importante para a classificação. Além disso, a representação BoW é computacionalmente eficiente e fácil de interpretar, o que pode ser uma vantagem para problemas onde a transparência do modelo é importante. No entanto, a representação BoW não leva em consideração a ordem das palavras no texto, o que pode limitar sua capacidade de capturar nuances semânticas.

Já Word Embedding é mais adequada para tarefas que envolvem análise semântica, como tradução automática, classificação de tópicos ou análise de sentimento baseada em frases complexas. A representação Word Embedding leva em consideração a ordem e o contexto das palavras, e pode capturar a similaridade semântica entre palavras que não aparecem juntas com frequência. Além disso, a representação distribuída de palavras pode ser usada para inicializar redes neurais em tarefas de aprendizado profundo, melhorando o desempenho do modelo. No entanto, Word Embedding pode ser computacionalmente intensiva e requer grandes quantidades de dados de treinamento para obter bons resultados.

2.1 Feature Extraction with Frequencies

Numa task de classificação de textos (ex. sentimentos), podemos identificar as palavras positivas e negativas a partir da frequencia de ocorrencia em que elas ocorrem nos textos positivos e negativos. Usando essa contagem, podemos extrair features e usá-las no modelo de classificação, como a regressão logística.

A partir da contagem da frequencia das palavras em cada classe, chegamos a essa tabela. Na prática, essa tabela será um dicionário que mapeia a classe da palavra e sua frequência de ocorrência.

Podemos representar essa tabela de frequencia com um array com 3 features, aumentando a velocidade na implementação, porquê em vez de termos v features, teremos apenas 3 para que o modelo aprenda.

Aqui, a primeira feature é um bias, depois o somatório das palavras da label positiva e o somatório das palavras da label negativa. Assim, teremos o novo vetor com 3 features.

Podemos implementar essa extração de features com frequência de ocorrência de termos negativos ou positivos da seguinte forma:

import pandas as pd
import nltk
from nltk.tokenize import word_tokenize
from nltk.probability import FreqDist

positive_words = df[df["label"] == 0]["tweet"].to_list() # Lista de palavras positivas
positive_words = word_tokenize(' '.join(positive_words))

negative_words = df[df["label"] == 1]["tweet"].to_list()# Lista de palavras negativas
negative_words = word_tokenize(' '.join(negative_words))

# Função para mapear a frequência das palavras do subconjunto de palavras positivas (label = 0)
def positive_words_frequency(tweet):
tokens = word_tokenize(tweet.lower()) # Tokenização e conversão para minúsculas
freq = FreqDist(tokens)
positive_freq = sum([freq[word] for word in positive_words])
return positive_freq

# Função para mapear a frequência das palavras do subconjunto de palavras negativas (label = 1)
def negative_words_frequency(tweet):
tokens = word_tokenize(tweet.lower()) # Tokenização e conversão para minúsculas
freq = FreqDist(tokens)
negative_freq = sum([freq[word] for word in negative_words])
return negative_freq

# Aplicar as funções aos tweets e adicionar os resultados ao dataframe
df['bias'] = 1 # Coluna de bias com valor 1
df['pos_freq'] = df['tweet'].apply(positive_words_frequency)
df['neg_freq'] = df['tweet'].apply(negative_words_frequency)
df
Output do dataset sintético

2.2 Preprocessing

O pré-processamento para tarefas de NLP geralmente envolve as etapas de limpeza e preparação dos dados. O texto é normalmente convertido em minúsculas, removidos caracteres especiais, removidos números e pontuações, e realizada a tokenização do texto para obter as palavras individuais.

Exemplo em python:

import nltk
from nltk.corpus import stopwords
import string

nltk.download('stopwords')

def text_cleaning(text):
# Extrai as stopwords em português
stopwords_list = stopwords.words('portuguese')

# Remove caracteres especiais
text = text.translate(str.maketrans('', '', string.punctuation))

# Remove números e minimiza
text = ''.join(word for word in text if not word.isdigit()).lower()

# Converte para minúsculo e tokeniza as palavras
tokens = nltk.word_tokenize(text.lower())

# Remove as stopwords
tokens = [word for word in tokens if word not in stopwords_list]

return tokens

# Exemplo de uso
text = "Este é um exemplo de texto que será limpo e tokenizado!"
tokens = text_cleaning(text)
print(tokens)

# Output
# ['exemplo', 'texto', 'limpo', 'tokenizado']

Agora, podemos aplicar a extração de features do exemplo acima com o texto tratado.

# preprocessando o texto
df["ttweet"] = df["tweet"].apply(text_cleaning)
df["ttweet"] = df["ttweet"].apply(lambda x: " ".join(x)) # retornando a lista como string

# novo conjunto de palavras
positive_words = df[df["label"] == 0]["ttweet"].to_list() # Lista de palavras positivas
positive_words = word_tokenize(' '.join(positive_words))

negative_words = df[df["label"] == 1]["ttweet"].to_list()# Lista de palavras negativas
negative_words = word_tokenize(' '.join(negative_words))

# Função para mapear a frequência das palavras do subconjunto de palavras positivas (label = 0)
def positive_words_frequency(tweet):
tokens = word_tokenize(tweet.lower()) # Tokenização e conversão para minúsculas
freq = FreqDist(tokens)
positive_freq = sum([freq[word] for word in positive_words])
return positive_freq

# Função para mapear a frequência das palavras do subconjunto de palavras negativas (label = 1)
def negative_words_frequency(tweet):
tokens = word_tokenize(tweet.lower()) # Tokenização e conversão para minúsculas
freq = FreqDist(tokens)
negative_freq = sum([freq[word] for word in negative_words])
return negative_freq

# Aplicar as funções aos tweets e adicionar os resultados ao dataframe
df['bias'] = 1 # Coluna de bias com valor 1
df['pos_freq'] = df['ttweet'].apply(positive_words_frequency)
df['neg_freq'] = df['ttweet'].apply(negative_words_frequency)
df.head(20)
Note que, remover as stop words diminuiu drasticamente a frequencia de ocorrencia, já que estamos utilizando somente as palavras que carregam informações

Mas além disso também podemos fazer Stemming e lematização. Elas são técnicas de pré-processamento de texto com o objetivo de reduzir as palavras em sua forma base ou raiz, simplificando o processo de análise de texto.

O Stemming é um processo mais simples e rápido de normalização de palavras, que envolve a remoção de sufixos com o objetivo de transformar uma palavra em sua raiz, ou seja, em sua forma básica. Um exemplo de algoritmo de stemming é o Porter Stemmer, que é amplamente utilizado em NLP. O ponto positivo do stemming é sua simplicidade e velocidade de processamento, o que pode ser útil em projetos com grandes volumes de dados. No entanto, o stemming pode produzir algumas palavras raiz que não são facilmente reconhecidas, o que pode ser um problema em alguns casos.

Já a lematização é um processo mais complexo de normalização de palavras, que envolve a análise do contexto da palavra para determinar sua forma básica. A lematização utiliza um dicionário de palavras ou um algoritmo para mapear a palavra para sua forma base. O ponto positivo da lematização é que ela produz palavras que são facilmente reconhecíveis, o que pode ser importante em projetos que exigem maior precisão na análise de texto. No entanto, a lematização é um processo mais lento e computacionalmente mais caro do que o stemming, o que pode ser um problema em projetos com grandes volumes de dados.

Podemos implementar o stemming da seguinte forma:

import nltk
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer
import string

def text_cleaning_s(text):
# Extrai as stopwords em português
stopwords_list = stopwords.words('portuguese')

# Remove caracteres especiais
text = text.translate(str.maketrans('', '', string.punctuation))

# Remove números e minimiza
text = ''.join(word for word in text if not word.isdigit()).lower()

# Converte para minúsculo e tokeniza as palavras
tokens = nltk.word_tokenize(text.lower())

# Remove as stopwords
tokens = [word for word in tokens if word not in stopwords_list]

# Realiza o stemming dos tokens
stemmer = SnowballStemmer('portuguese')
stemmed_tokens = [stemmer.stem(token) for token in tokens]

return stemmed_tokens

# Exemplo de uso
text = "Este é um exemplo de texto que será limpo, tokenizado e stemmed!"
tokens = text_cleaning_s(text)
print(tokens)

# output
# ['exempl', 'text', 'limp', 'tokeniz', 'stemmed']

Enquanto que para lematização, podemos implementar da seguinte forma, apenas com pequenas alterações:

import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import string

nltk.download('wordnet')
nltk.download('omw-1.4')

def text_cleaning_l(text):
# Extrai as stopwords em inglês
stopwords_list = stopwords.words('english')

# Remove caracteres especiais
text = text.translate(str.maketrans('', '', string.punctuation))

# Remove números e minimiza
text = ''.join(word for word in text if not word.isdigit()).lower()

# Converte para minúsculo e tokeniza as palavras
tokens = nltk.word_tokenize(text.lower())

# Remove as stopwords
tokens = [word for word in tokens if word not in stopwords_list]

# Realiza a lematização dos tokens
lemmatizer = WordNetLemmatizer()
lemmatized_tokens = [lemmatizer.lemmatize(token) for token in tokens]

return lemmatized_tokens

3 Sentiment Analysis

Nesta seção falaremos sobre a tarefa de classificação de texto para analisar sentimentos utilizando os modelos Logistic Regression e Naive Bayes.

3.1 Sentiment Analysis With Logistic Regression

A regressão logística ou logistic regression é frequentemente utilizada em análise de sentimento porque é um algoritmo de classificação simples, rápido e eficiente para problemas de classificação binária, ou seja, quando há apenas duas classes, como positivo e negativo. Além disso, a regressão logística é facilmente interpretável, o que significa que é possível entender como o modelo chegou a uma determinada previsão.

Outra vantagem da regressão logística é que ela lida bem com problemas de dados desbalanceados, que é comum em análise de sentimento, onde muitas vezes há mais exemplos de uma classe do que outra. A regressão logística usa uma função sigmoide para calcular as probabilidades de cada classe, e essa função é bem adequada para casos de classes desbalanceadas.

Outro motivo para a popularidade da regressão logística em análise de sentimento é que ela é relativamente fácil de implementar e ajustar. É possível utilizar diferentes técnicas de regularização, como a regularização L1 ou L2, para evitar overfitting e melhorar o desempenho do modelo.

No entanto, é importante ressaltar que a regressão logística pode não ser adequada para problemas de classificação multiclasse, ou seja, quando há mais de duas classes, como em análise de tópicos. Nesses casos, outros algoritmos, como Árvores de Decisão, SVMs ou Redes Neurais, podem ser mais apropriados.

O modelo é treinado usando um conjunto de dados rotulados, onde cada texto é rotulado como tendo um sentimento positivo ou negativo. O objetivo do treinamento é ajustar os parâmetros do modelo para que ele possa fazer previsões precisas em novos dados.

Durante o treinamento, o modelo utiliza a função logística para calcular a probabilidade de um texto ter um sentimento positivo ou negativo com base em seus vetores de características. A função logística produz um valor entre 0 e 1, que representa a probabilidade de um texto ter um sentimento positivo. Por padrão, se a probabilidade for maior que 0,5, o modelo classifica o texto como tendo um sentimento positivo. Caso contrário, o texto é classificado como tendo um sentimento negativo. Além disso podemos ajustar o limiar de classificação do modelo.

Após o treinamento, o modelo pode ser usado para fazer previsões em novos dados. Para cada novo texto, o modelo converte-o em um vetor de características e utiliza a função logística para calcular a probabilidade de ter um sentimento positivo ou negativo. Em seguida, classifica o texto como tendo um sentimento positivo ou negativo com base no valor calculado.

Note que conforme 𝜃^T x^(i) se aproxima cada vez mais de -∞, o denominador da função sigmoidal fica cada vez maior e, como resultado, a sigmoidal se aproxima de 0. Por outro lado, conforme 𝜃^T x^(i) se aproxima cada vez mais de ∞, o denominador da função sigmoidal se aproxima de 1 e, como resultado, a sigmoidal também se aproxima de 1.

Dado um tweet, podemos transformá-lo em um vetor e passá-lo pela sua função sigmoidal para obter uma previsão da seguinte forma:

Agora, podemos comparar o desempenho do modelo com as duas estratégias de representação numérica de textos: A simples e da função text_cleaning_l definida acima, que remove caracteres especiais, stopwords e torna todas as palavras minúsculo.

Estratégia Simples:

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score, precision_score

# divisão em treino e teste
X = df[['bias', 'pos_freq', 'neg_freq']]
y = df['label']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# criação do modelo de regressão logística e treinamento
lr = LogisticRegression()
lr.fit(X_train, y_train)

# avaliação do modelo nos dados de teste
y_pred = lr.predict(X_test)

# avaliando o modelo
print("\n Classification Report: Estratégia Simples \n", classification_report(y_test, y_pred))

Com o pipeline de pré-processamento + Count Vectorizer:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# pré-processamento das frases
df['ttweet'] = df['tweet'].apply(text_cleaning_l)

# divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(df['ttweet'], df['label'], test_size=0.3, random_state=42)

# vetorização das frases com CountVectorizer
vectorizer = CountVectorizer()
X_train_vec = vectorizer.fit_transform(X_train.apply(lambda tokens: ' '.join(tokens)).to_list())
X_test_vec = vectorizer.transform(X_test.apply(lambda tokens: ' '.join(tokens)).to_list())

# criação do modelo de regressão logística e treinamento
lr = LogisticRegression()
lr.fit(X_train_vec, y_train)

# avaliação do modelo nos dados de teste
y_pred = lr.predict(X_test_vec)

# avaliando o modelo
print("\n Classification Report: Pipeline de Pré-processamento + CountVectorizer \n",
classification_report(y_test, y_pred))

Com o pipeline de pré-processamento + TF-IDF:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# pré-processamento das frases
df['ttweet'] = df['tweet'].apply(text_cleaning_l)

# divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(df['ttweet'], df['label'], test_size=0.3, random_state=42)

# vetorização das frases com CountVectorizer
vectorizer = TfidfVectorizer()
X_train_vec = vectorizer.fit_transform(X_train.apply(lambda tokens: ' '.join(tokens)).to_list())
X_test_vec = vectorizer.transform(X_test.apply(lambda tokens: ' '.join(tokens)).to_list())

# criação do modelo de regressão logística e treinamento
lr = LogisticRegression()
lr.fit(X_train_vec, y_train)

# avaliação do modelo nos dados de teste
y_pred = lr.predict(X_test_vec)

# avaliando o modelo
print("\n Classification Report: Pipeline de Pré-processamento +TF-IDF \n",
classification_report(y_test, y_pred))

Nesse teste exemplo com fins didáticos, com um conjunto de dados sintético e pequeno o processamento simples teve uma melhor performace. Mas num cenário real provavelmente esse não seria o caso. Mas mesmo assim, precisamos partir de um baseline e ir aumentando a complexidade do modelo e criando novas features até que o desempenho do modelo seja adequado para o objetivo da task.

3.2 Sentiment Analysis with Naive Bayes

Recaptulando Probabilidade…

Antes de abordar o algoritmo Naive bayes, é preciso relembrar alguns conceitos de probabilidade. A probabilidade é uma medida numérica que quantifica a incerteza associada a um evento. Em termos simples, é a chance de que algo aconteça. A probabilidade de um evento é sempre um número entre 0 e 1, onde 0 indica impossibilidade absoluta do evento ocorrer e 1 indica certeza absoluta de que o evento ocorrerá.

Já a probabilidade condicional é a probabilidade de um evento ocorrer dado que outro evento já ocorreu. É denotada por $ P(A|B) $, que lê-se como “a probabilidade de A dado B”. A fórmula para calcular a probabilidade condicional é:

Onde:
— P(A|B) é a probabilidade de A dado B,
— P(A ∩ B) é a probabilidade da interseção de A e B,
— P(B) é a probabilidade de B.

Em palavras simples, a probabilidade condicional é a proporção de vezes que o evento A ocorre quando o evento B ocorre.

Outro conceito importante a ser lembrado é a Regra de Bayes ou Bayes Rule. Ela é uma ferramenta fundamental na teoria das probabilidades que permite atualizar as probabilidades de uma hipótese à luz de novas evidências. Formalmente, a regra de Bayes é expressa como:

Onde:
— P(A|B) é a probabilidade de A dado B (posterior),
— P(B|A) é a probabilidade de B dado A (likelihood),
— P(A) é a probabilidade de A (prior),
— P(B) é a probabilidade de B.

A regra de Bayes nos permite calcular a probabilidade de uma hipótese (A) ser verdadeira dada uma evidência observada (B), usando a probabilidade da evidência dada a hipótese (likelihood), a probabilidade a priori da hipótese e a probabilidade marginal da evidência.A diferença principal entre a probabilidade condicional e a regra de Bayes é que a probabilidade condicional é uma medida da probabilidade de um evento ocorrer dado que outro evento já ocorreu, enquanto a regra de Bayes é uma ferramenta para atualizar a probabilidade de uma hipótese à luz de novas evidências. A regra de Bayes utiliza a probabilidade condicional como um de seus componentes para calcular a probabilidade posterior.

A probabilidade é a chance de um evento ocorrer, a probabilidade condicional é a probabilidade de um evento ocorrer dado que outro evento já ocorreu, e a regra de Bayes é uma ferramenta para atualizar a probabilidade de uma hipótese à luz de novas evidências. Baseado nesses conceitos, foi desenvolvido o algoritmo Naive Bayes.

O algoritmo Naive Bayes é um método de classificação probabilístico baseado no teorema de Bayes, com uma suposição “ingênua” de independência condicional entre os recursos.

1. Suposição de Independência Condicional: A primeira etapa do algoritmo Naive Bayes é a suposição de independência condicional entre os recursos. Isso significa que assumimos que os recursos são independentes entre si, dado o valor da variável de classe. Apesar de ser uma suposição forte e muitas vezes não ser verdadeira na prática, ela simplifica os cálculos e torna o algoritmo computacionalmente eficiente.

2. Construção do Modelo de Probabilidade: O próximo passo é construir o modelo de probabilidade. Isso envolve calcular a probabilidade de cada classe e a probabilidade de cada valor do recurso dado cada classe. Em outras palavras, para cada classe, calculamos a probabilidade a priori da classe P(C_k) e a probabilidade de cada recurso P(X_i | C_k).

3. Classificação: Depois que o modelo de probabilidade é construído, podemos usá-lo para fazer previsões sobre novos exemplos. Dada uma nova instância com valores de recursos x_1, x_2, …, x_n, queremos calcular a probabilidade de pertencer a cada classe e, em seguida, atribuir a classe com a maior probabilidade como a classe prevista para a instância. Isso é feito usando o teorema de Bayes:

Onde:
P(C_k | x_1, x_2, …, x_n) é a probabilidade da classe C_k dado os valores dos recursos/features
P(C_k) é a probabilidade a priori da classe C_k
P(x_i | C_k) é a probabilidade de cada valor do recurso dado a classe C_k
P(x_1, x_2, …, x_n) é a probabilidade dos valores dos recursos.

4. Estimação de Parâmetros: Durante a etapa de construção do modelo, precisamos estimar os parâmetros do modelo, ou seja, as probabilidades a priori das classes e as probabilidades condicionais dos recursos para cada classe. Isso geralmente é feito usando técnicas como a frequência relativa de ocorrência dos dados de treinamento.

5. Suavização de Laplace (Opcional): Em alguns casos, para evitar probabilidades condicionais iguais a zero para features não observados em uma classe particular, pode ser aplicada a suavização de Laplace, adicionando uma pequena quantidade aos contadores de frequência de cada valor de recurso para cada classe durante a estimativa dos parâmetros.

Essas são as etapas principais da função do algoritmo Naive Bayes, desde a suposição de independência condicional até a classificação de novas instâncias usando o teorema de Bayes.

Para construir um classificador, começaremos primeiro criando probabilidades condicionais, dada a tabela a seguir:

Isso nos permite calcular a seguinte tabela de probabilidades:

Depois de ter as probabilidades, podemos calcular a pontuação de probabilidade da seguinte forma

Uma pontuação maior que 1 indica que a classe é positiva, caso contrário é negativa.

Costumamos calcular a probabilidade de uma palavra dada uma classe da seguinte forma:

No entanto, se uma palavra não aparecer no treinamento, ela automaticamente recebe uma probabilidade de 0. Para corrigir isso, adicionamos suavização de laplace da seguinte forma:

Observe que adicionamos um 1 no numerador e, como há V palavras para normalizar, adicionamos V no denominador.

Onde:
- N_classe: frequência de todas as palavras na classe.
- V: número de palavras únicas no vocabulário.

Para calcular a log-verossimilhança (log likelihood), precisamos obter as razões e usá-las para calcular uma pontuação que nos permitirá decidir se um tweet é positivo ou negativo. Quanto maior a razão, mais positiva é a palavra:

Para fazer inferência, você pode calcular o seguinte:

A expressão acima começa com um logaritmo, log, que é aplicado a um produto de probabilidades condicionais. O produto é denotado pelo símbolo Π (pi) e representa o produto de todas as probabilidades condicionais das palavras w_i dadas as classes “negativo” e “positivo”. Isso significa que estamos multiplicando a probabilidade de cada palavra w_i ocorrer, dado que o tweet é negativo, e dividindo pelo mesmo para tweets positivos.

A expressão é comparada com o inverso do produto das probabilidades das classes “negativo” e “positivo”. Isso é feito usando o sinal >, indicando que queremos que a expressão à esquerda seja maior do que o inverso do produto das probabilidades das classes.

Em termos de interpretação, isso significa que estamos comparando a probabilidade conjunta de todas as palavras em um tweet serem negativas (ou positivas) com a probabilidade de um tweet ser classificado como negativo (ou positivo), independente do conteúdo do tweet. Se a probabilidade conjunta de todas as palavras serem negativas (ou positivas) for maior do que a probabilidade do tweet ser classificado como negativo (ou positivo) independentemente do conteúdo, então a inferência seria que o tweet é mais provavelmente negativo (ou positivo).

Essa expressão é uma maneira de inferir a polaridade (positiva ou negativa) de um tweet com base na probabilidade condicional de cada palavra em relação às classes “negativo” e “positivo”, em comparação com a probabilidade marginal das classes. Se a probabilidade conjunta das palavras sendo negativas (ou positivas) for maior do que a probabilidade do tweet ser classificado como negativo (ou positivo) independentemente do conteúdo, então a inferência seria que o tweet é mais provavelmente negativo (ou positivo).

Conforme o número de palavras do tweet (m) aumenta, podemos ter problemas numéricos, então introduzimos o logaritmo, que nos dá a seguinte equação:

Utilizando a propriedade do logaritmo de um produto, podemos reescrever a expressão como a soma dos logaritmos dos fatores dentro do produto. Esta transformação nos permite calcular a log-verossimilhança de forma mais eficiente e robusta, evitando o custo computacional de calcular o logaritmo de um produtório. A expressão agora é uma soma de logaritmos individuais, o que é mais estável numericamente. A nova expressão nos permite calcular a log-verossimilhança somando os logaritmos das razões das probabilidades condicionais de cada palavra em relação às classes “negativo” e “positivo”. Isso nos dá uma medida da probabilidade de observar as palavras em um tweet dado que ele é classificado como negativo, em comparação com a mesma probabilidade para tweets positivos. Essa transformação simplifica o cálculo da log-verossimilhança e reduz a chance de problemas numéricos, tornando a inferência mais eficiente e precisa.

O primeiro componente é chamado de log prior e o segundo componente é a log-verossimilhança. Introduzimos ainda λ (lambda) como segue:

Ter o dicionário λ ajudará muito ao fazer inferência, uma vez que o computemos, se torna simples fazer a inferência, simplesmente somando os lambdas e comparando com os thresholds de negativo, neutro e positivo.

O resultado foi 3.3, sendo > 0, classificaremos o documento como positivo. Se tivessemos um número negativo, classificaríamos como a classe negativa

Depois desse (nada) breve overview sobre probabilidade, podemos fazer os mesmos testes que fizemos com a regressão logística a fim de verificar o desempenho do modelo para cada estratégia de engenharia de features. Vamos seguir a mesma estrutura, testando a estratégia simples e depois a com o preprocessamento do texto aplicando Count Vectorizer e depois TF-IDF. Mas agora vamos utilizar o algoritmo Naive Bayes.

Implementação com a estratégia simples:

import pandas as pd
import nltk
from nltk.tokenize import word_tokenize
from nltk.probability import FreqDist
from sklearn.metrics import accuracy_score
from sklearn.naive_bayes import MultinomialNB
from sklearn.linear_model import LogisticRegression

# divisão em treino e teste
X = df[['pos_freq', 'neg_freq']]
y = df['label']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# criação do modelo
model = MultinomialNB() # Exemplo: usando Naive Bayes
model.fit(X_train, y_train)

# avaliação do modelo nos dados de teste
y_pred = model.predict(X_test)

# avaliando o modelo
print("\n Classification Report: Estratégia Simples \n", classification_report(y_test, y_pred))

Com o pipeline de pré-processamento + Count Vectorizer:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(df['ttweet'], df['label'], test_size=0.3, random_state=42)

# Pré-processamento
X_train_processed = X_train.apply(text_cleaning_l)
X_test_processed = X_test.apply(text_cleaning_l)

# Vetorização das frases processadas
vectorizer = CountVectorizer()
X_train_vectorized = vectorizer.fit_transform(X_train_processed.apply(lambda tokens: ' '.join(tokens)).to_list())
X_test_vectorized = vectorizer.transform(X_test_processed.apply(lambda tokens: ' '.join(tokens)).to_list()) # Use o vetorizador treinado

# Criação do modelo Naive Bayes e treinamento
nb_model = MultinomialNB()
nb_model.fit(X_train_vectorized, y_train)

# Avaliação do modelo nos dados de teste
y_pred = nb_model.predict(X_test_vectorized)

# Avaliação do modelo
print("\n Classification Report: Count Vectorizer \n", classification_report(y_test, y_pred))

Com o pipeline de pré-processamento + TF-IDF:

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

# Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(df['ttweet'], df['label'], test_size=0.3, random_state=42)

# Pré-processamento
X_train_processed = X_train.apply(text_cleaning_l)
X_test_processed = X_test.apply(text_cleaning_l)

# Vetorização das frases processadas
vectorizer = TfidfVectorizer()
X_train_vectorized = vectorizer.fit_transform(X_train_processed.apply(lambda tokens: ' '.join(tokens)).to_list())
X_test_vectorized = vectorizer.transform(X_test_processed.apply(lambda tokens: ' '.join(tokens)).to_list()) # Use o vetorizador treinado

# Criação do modelo Naive Bayes e treinamento
nb_model = MultinomialNB()
nb_model.fit(X_train_vectorized, y_train)

# Avaliação do modelo nos dados de teste
y_pred = nb_model.predict(X_test_vectorized)

# Avaliação do modelo
print("\n Classification Report: TF-IDF \n", classification_report(y_test, y_pred))

Novamente, precisamos identificar quais as métricas importantes e analisar os resultados de acordo com o objetivo do projeto ou do negócio. Com o modelo Naive Bayes, o processamento simples teve uma melhor performace. E em um cenário real provavelmente esse não seria o caso. Mas mesmo assim, precisamos partir de um baseline e ir aumentando a complexidade do modelo e criando novas features até que o desempenho do modelo seja adequado para o objetivo da task.

4. Vector Space Models

Os Modelos de Espaço Vetorial (VSMs) são uma técnica fundamental em processamento de linguagem natural (NLP) para representar o significado de palavras e documentos em um espaço matemático contínuo. Eles são usados para capturar e representar semanticamente o contexto e a similaridade entre palavras e documentos. Os VSMs representam palavras como vetores matemáticos em um espaço de alta dimensionalidade. Cada dimensão do vetor pode representar um aspecto diferente da palavra, como seu contexto, uso ou significado.

Essas dimensões são geralmente aprendidas automaticamente a partir de grandes conjuntos de dados textuais usando técnicas como word embeddings. Ou seja, é codificado uma representação numérica de palavras que captura o significado relativo de forma contextual. Exemplo: A palavra “gato” pode ser representada por um vetor como [0.2, -0.1, 0.5, …], onde cada valor numérico representa um aspecto diferente da palavra.

Além de representar palavras, os VSMs também podem representar documentos inteiros. Nesse caso, cada documento é representado como um vetor onde cada dimensão pode representar a frequência ou importância de uma palavra ou conceito dentro desse documento. Por exemplo, um artigo de notícias sobre tecnologia pode ser representado por um vetor onde a dimensão correspondente à palavra “tecnologia” tem um valor alto, enquanto as dimensões correspondentes a palavras irrelevantes têm valores baixos.

Os VSMs são úteis para medir a similaridade semântica entre palavras e documentos. Isso é feito calculando a proximidade entre os vetores no espaço vetorial. Palavras ou documentos semanticamente semelhantes estarão mais próximos no espaço vetorial. Os vetores das palavras “cachorro” e “animal” provavelmente estarão mais próximos no espaço vetorial do que os vetores das palavras “cachorro” e “carro”.

Eles são amplamente utilizados em tarefas de NLP, como recuperação de informações (information retrieval), classificação de texto, agrupamento de documentos, tradução automática e muito mais. Eles formam a base para muitas técnicas e algoritmos modernos de NLP. Um sistema de recomendação de filmes pode usar VSMs para encontrar filmes semelhantes com base nas sinopses dos filmes já assistidos pelo usuário.

4.1 Word by Word and Word by Doc

Construir vetores usando uma matriz de coocorrência com um design de word by word é uma abordagem para criar representações vetoriais de palavras com base em sua coocorrência em um corpus de texto.

Para construir uma Matriz de Coocorrência, precisamos:

  • Definir o Contexto: Para cada palavra no vocabulário, é necessário definir um contexto em torno dela. Por exemplo, para uma palavra “cachorro”, o contexto pode ser os N tokens à esquerda e à direita da palavra em um texto.
  • Contar Coocorrências: Para cada palavra no vocabulário, conte quantas vezes ela ocorre no contexto de outras palavras. Isso resulta em uma matriz de coocorrência, onde cada célula (i, j) representa quantas vezes a palavra i ocorre no contexto da palavra j.

A Construção dos Vetores envolve:

  • Normalização: Antes de construir os vetores, a matriz de coocorrência pode ser normalizada para considerar fatores como a frequência geral das palavras no corpus e a frequência dos próprios contextos.
  • Construção dos Vetores: Cada linha ou coluna na matriz de coocorrência representa um vetor para uma palavra específica. Isso pode ser feito tratando cada linha ou coluna como um vetor.

Suponha que temos o seguinte texto como nosso corpus:

O cachorro correu no parque.
O gato dormiu no sofá.
O cachorro e o gato são amigos.

Para construir a matriz de coocorrência para um contexto de duas palavras à esquerda e à direita, teríamos algo assim (excluindo palavras de parada como “o”, “e”, etc.):

Nesta matriz, cada linha representa uma palavra do vocabulário e cada coluna representa uma palavra do contexto. A célula (i, j) mostra quantas vezes a palavra i ocorre no contexto da palavra j.

Para construir vetores para cada palavra, normalizamos esta matriz e tratamos cada linha ou coluna como um vetor.

Uma vez que os vetores tenham sido construídos, eles podem ser usados para várias tarefas em NLP, como encontrar palavras semanticamente semelhantes, calcular distâncias entre palavras, etc.

Essa abordagem é uma maneira simples e eficaz de construir representações vetoriais de palavras usando a coocorrência em um corpus de texto. No entanto, ela pode ser limitada em capturar nuances semânticas mais complexas, especialmente em corpora muito grandes ou com vocabulários extensos.

Construir vetores usando uma matriz de coocorrência com um design de word by document é uma abordagem para criar representações vetoriais de palavras com base em sua coocorrência com documentos inteiros em um corpus de texto.

Novamente, para construir uma Matriz de Coocorrência, precisamos:

  • Definir o Contexto: Neste caso, o contexto é o documento inteiro em que uma palavra ocorre. Cada documento é tratado como uma “janela” em torno das palavras.
  • Contar as Coocorrências: Para cada palavra no vocabulário, conte quantas vezes ela ocorre em cada documento. Isso resulta em uma matriz de coocorrência, onde cada linha representa uma palavra e cada coluna representa um documento.

E para construir os vetores, também precisamos:

  • Normalização: Assim como na abordagem word by word, a matriz de coocorrência pode ser normalizada para considerar fatores como a frequência geral das palavras no corpus e a frequência dos próprios documentos.
  • Construção dos Vetores: Neste caso, cada linha na matriz de coocorrência representa um vetor para uma palavra específica. Cada elemento do vetor pode representar a frequência ou a importância relativa da palavra em cada documento.

Novamente, suponha que temos o seguinte corpus de três documentos:

1. Documento 1: “O cachorro correu no parque.”
2. Documento 2: “O gato dormiu no sofá.”
3. Documento 3: “O cachorro e o gato são amigos.”

A matriz de coocorrência seria algo como:

Para construir vetores para cada palavra, normalizamos esta matriz e tratamos cada linha como um vetor. Uma vez que os vetores tenham sido construídos, eles podem também ser usados para várias tarefas em NLP, como encontrar palavras semanticamente semelhantes, calcular distâncias entre palavras, classificação de texto, entre outros.

Esta abordagem é útil quando estamos mais interessados nas relações entre palavras e documentos do que nas relações entre palavras individuais. Ela pode capturar a essência de como palavras diferentes estão distribuídas em diferentes contextos de documentos.

No exemplo abaixo, podemos verificar a relação entre palavras e documentos, e temos que as palavras “data” estão mais relacionadas a Economy e ML do que Entertainment. Assim como a palavra “film” está mais relacionada à Entertainment que aos demais documentos. Aqui, a categoria Entertainment pode ser representada como um vetor v = [500, 700], assim como Economy como um vetor v = [6620, 4000] e ML como um vetor v = [9320, 1000]

Para medir o quão similiar os as palavras ou documentos são entre si, podemos utilizar a Distância Euclidiana e a Similaridade de Cosseno.

Euclidean Distance

A Distância Euclidiana é uma medida de distância entre dois pontos em um espaço euclidiano. É derivada do teorema de Pitágoras e é frequentemente usada em diversas áreas, como geometria, análise de dados, reconhecimento de padrões e aprendizado de máquina. Aqui está uma explicação detalhada:

A fórmula para calcular a distância euclidiana entre dois pontos P e Q em um espaço n-dimensional é dada por:

Onde:

  • P = (p_1, p_2, …, p_n) e Q = (q_1, q_2, …, q_n) são os pontos no espaço n-dimensional.
  • p_i e q_i são as coordenadas dos pontos P e Q ao longo da dimensão i.

Essa medida de distância possui as seguintes características:

  • Positividade: A distância euclidiana é sempre não negativa, ou seja, $ EuclideanDistance(P, Q) ≥ 0.
  • Identidade de Indiscerníveis: A distância entre dois pontos é zero se e somente se os pontos são idênticos.
  • Simetria: A distância entre dois pontos P e Q é a mesma que a distância entre Q e P.
  • Desigualdade Triangular: A distância de um ponto a outro ponto é sempre menor do que ou igual à soma das distâncias de um ponto a um terceiro ponto intermediário.

Suponha que tenhamos dois pontos no plano 2D: P(2, 3) e Q(5, 7). Para calcular a distância euclidiana entre esses dois pontos, usamos a fórmula:

Portanto, a distância euclidiana entre P e Q é 5 unidades.

A distância euclidiana é amplamente utilizada em algoritmos de agrupamento (como k-means), classificação (como k-NN), redução de dimensionalidade (como PCA), reconhecimento de padrões, entre outros. É uma métrica fundamental em muitos problemas de otimização, onde minimizar ou maximizar a distância entre pontos é um objetivo. Ela é é uma medida intuitiva e útil para quantificar a distância entre pontos em um espaço multidimensional.

Podemos generalizar a encontrar a distância entre dois pontos (x_1, y_1) e (x_2, y_2) para a distância entre um vetor v de n dimensões da seguinte forma:

Para calcular a distância euclidiana entre dois vetores n-dimensionais em Python, podemos usar a biblioteca NumPy, que fornece funções eficientes para operações matemáticas em vetores e matrizes.

import numpy as np

def euclidean_distance(vector1, vector2):
"""
Calcula a distância euclidiana entre dois vetores n-dimensionais.

Argumentos:
vector1 (np.array): O primeiro vetor.
vector2 (np.array): O segundo vetor.

Retorna:
float: A distância euclidiana entre os dois vetores.
"""
if len(vector1) != len(vector2):
raise ValueError("Os vetores devem ter o mesmo número de dimensões.")

# Converte as listas em arrays do NumPy para cálculos eficientes
vector1 = np.array(vector1)
vector2 = np.array(vector2)

# Calcula a diferença entre os dois vetores
difference = vector2 - vector1

# Calcula a soma dos quadrados das diferenças
squared_difference = np.sum(difference ** 2)

# Calcula a raiz quadrada da soma dos quadrados
euclidean_distance = np.sqrt(squared_difference)

return euclidean_distance

# Exemplo de uso
vector1 = [2, 3, 5]
vector2 = [5, 7, 1]

distance = euclidean_distance(vector1, vector2)
print("A distância euclidiana entre os vetores é:", distance)

Ou simplesmente

distance = np.linalg.norm(np.array(vector1) - np.array(vector2))

4.3 Cosine Similarity

A similaridade do cosseno é uma medida de similaridade entre dois vetores em um espaço vetorial, frequentemente usado em mineração de texto, recuperação de informação e aprendizado de máquina. A medida é baseada no ângulo formado entre os dois vetores no espaço vetorial e não na distância euclidiana entre eles. A similaridade do cosseno entre dois vetores $ \mathbf{A} $ e $ \mathbf{B} $ é dada pela fórmula:

Onde:
- 𝐀⋅𝐁 é o produto interno (ou escalar) entre os vetores 𝐀 e 𝐁.
- ‖𝐀‖‖‖ e ‖𝐁‖ são as normas (ou magnitudes) dos vetores 𝐀 e 𝐁, respectivamente.

Essa medida de similaridade possui as seguintes características:

  • Intervalo de Valores: A similaridade do cosseno varia de -1 a 1, onde 1 indica que os vetores têm a mesma direção, 0 indica que os vetores são ortogonais (semelhantes de forma nula) e -1 indica que os vetores têm direções opostas.
  • Independência de Escala: A similaridade do cosseno é independente da escala dos vetores. Isso significa que os vetores podem ser normalizados antes do cálculo sem afetar o resultado.
  • Eficiência Computacional: O cálculo da similaridade do cosseno é computacionalmente eficiente, especialmente para vetores de alta dimensionalidade.
  • Apropriado para Texto: É amplamente utilizado em NLP, pois lida bem com vetores de alta dimensão, como vetores de termos de documentos.

Suponha que temos dois vetores A = [2, 1] e B = [3, 4]. Para calcular a similaridade do cosseno entre esses dois vetores, usamos a fórmula:

Portanto, a similaridade do cosseno entre A e B é 0.4.

A similaridade do cosseno é amplamente utilizada em tarefas de recuperação de informação, onde é usada para encontrar documentos semelhantes com base em vetores de termos de documentos. Também é usado em sistemas de recomendação para calcular a similaridade entre perfis de usuários e itens. Além disso, é usado em classificação de texto e agrupamento de documentos. A similaridade do cosseno é uma medida eficaz de similaridade entre vetores que é amplamente utilizada em várias aplicações, especialmente em processamento de linguagem natural e mineração de texto.

Se os documentos tiverem tamanhos diferentes, a métrica de Euclidean Distance não é a ideal. Cosine Similarity não é enviesada pelo pelo tamanho da diferença entre os vetores.

Para calcular a similaridade de cosseno entre dois vetores em Python, você pode usar a biblioteca NumPy para facilitar os cálculos vetoriais

import numpy as np

def cosine_similarity(vector1, vector2):
"""
Calcula a similaridade de cosseno entre dois vetores.

Argumentos:
vector1 (np.array): O primeiro vetor.
vector2 (np.array): O segundo vetor.

Retorna:
float: A similaridade de cosseno entre os dois vetores.
"""
dot_product = np.dot(vector1, vector2)
norm_vector1 = np.linalg.norm(vector1)
norm_vector2 = np.linalg.norm(vector2)
similarity = dot_product / (norm_vector1 * norm_vector2)
return similarity

# Exemplo de uso
vector1 = np.array([2, 1])
vector2 = np.array([3, 4])

similarity = cosine_similarity(vector1, vector2)
print("A similaridade de cosseno entre os vetores é:", similarity)

Manipulating Words in Vector Spaces

Podemos manipular os espaços vetoriais para fazer algumas previsões, como prever países e suas capitais, essa é uma aplicação de vetores em aprendizado de máquina e processamento de linguagem natural. Graças ao relacionamento entre os vetores, podemos verificar esse tipo de associação.

Primeiro, precisaríamos de uma representação vetorial para as palavras envolvidas — neste caso, países e suas capitais. Uma maneira comum de fazer isso é usar embeddings de palavras pré-treinados, como os disponíveis em modelos como Word2Vec, GloVe ou FastText. Esses embeddings atribuem a cada palavra um vetor numérico denso em um espaço vetorial, onde palavras semanticamente semelhantes estão próximas umas das outras. Mas podemos usar os embeddings que criamos também.

Para cada país e capital, precisaríamos de seus respectivos vetores. E, ao usarmos embeddings de palavras pré-treinados, podemos simplesmente pegar os vetores correspondentes às palavras “país” e “capital” e somá-los aos vetores de palavras correspondentes ao país e à capital real. Por exemplo, para prever a capital do Brasil, você somaria o vetor correspondente a “Brasil” ao vetor correspondente a “capital”, obtendo assim um vetor que representa a capital prevista.

Agora que você tem vetores para países e capitais, você pode usar operações vetoriais para fazer previsões. Por exemplo, para prever a capital de um país, você poderíamos calcular a diferença entre o vetor do país de interesse e o vetor de outro país conhecido com sua capital conhecida. Em seguida, você adiciona esse vetor de diferença ao vetor da capital conhecida para obter a capital prevista.

Por exemplo, suponha que você tenha embeddings de palavras onde “Brasil” é representado pelo vetor v_Brasil e “capital” pelo vetor v_capital. Para prever-mos a capital do Brasil, faríamos o seguinte:

Outra forma de prever tokens usando as relações vetoriais, é usar as representações conhecidas. Sabendo que a capital dos USA é Washington, podemos ver a distância entre os dois vetores e somar ao vetor que representa Russia para descobrir sua capital, Como:

Depois de fazer as previsões, podemos avaliar a precisão do modelo usando um conjunto de dados de teste que contenha pares de países e suas capitais reais. Se a precisão não for satisfatória, você pode ajustar os vetores ou usar técnicas mais avançadas de aprendizado de máquina para melhorar o desempenho do modelo.

Manipular vetores usando aritmética para prever países e suas capitais (ou qualquer relacionamento parecido) envolve representar países e capitais como vetores, realizar operações vetoriais para fazer previsões e avaliar o desempenho do modelo.

Outro exemplo bem famoso é o do é o do “king — man + woman = queen”, quem ao somar e subtrair os valores correspondentes a eles, acabaríamos ficando próximo no espaço multidimensional do vetor que representa “queen”.

4.5 Principal Component Analysis

A Análise de Componentes Principais (PCA) é um algoritmo não supevisionado, uma técnica de redução de dimensionalidade amplamente utilizada para simplificar conjuntos de dados complexos, mantendo as informações mais importantes. Funciona encontrando as direções (ou componentes) de maior variância nos dados e projetando os dados nesses novos eixos, chamados de componentes principais. Ele funciona da seguinte forma:

  • Centralização dos Dados: Antes de aplicar o PCA, os dados são geralmente centralizados subtraindo a média de cada variável. Isso garante que o centro dos dados esteja na origem do espaço de características.
  • Cálculo da Matriz de Covariância: Em seguida, é calculada a matriz de covariância dos dados centralizados. A covariância entre duas variáveis mede como elas variam juntas. A matriz de covariância captura as relações lineares entre as variáveis originais.
  • Decomposição da Matriz de Covariância: A seguir, a matriz de covariância é decomposta em seus autovetores e autovalores. Os autovetores representam as direções dos eixos principais (ou componentes principais) dos dados, enquanto os autovalores representam a quantidade de variância explicada por cada componente principal.
  • Seleção dos Componentes Principais: Os autovetores são ordenados de acordo com seus autovalores associados, do maior para o menor. Os autovetores com os maiores autovalores capturam a maior parte da variância nos dados e são selecionados como os componentes principais mais importantes.
  • Projeção dos Dados: Finalmente, os dados são projetados nos novos eixos definidos pelos componentes principais selecionados. Isso reduz a dimensionalidade dos dados, substituindo as variáveis originais por uma combinação linear dos componentes principais.

Vamos considerar um conjunto de dados bidimensional com duas variáveis, como altura e peso de uma população. O PCA encontrará a direção ao longo da qual a variabilidade dos dados é máxima. Suponha que a direção seja dada pelo autovetor [0.8, 0.6], com um autovalor associado de 10. Isso significa que 80% da variabilidade dos dados está ao longo dessa direção. Os dados podem ser projetados nessa direção, reduzindo-os de duas dimensões para uma dimensão. Podemos usá-lo para:

  • Redução de dimensionalidade: PCA é usado para reduzir a dimensionalidade de dados complexos, preservando o máximo de informação possível.
  • Visualização de dados: PCA pode ser usado para visualizar dados de alta dimensionalidade em um espaço de menor dimensão.
  • Pré-processamento de dados: PCA é frequentemente usado como uma etapa de pré-processamento antes de aplicar algoritmos de aprendizado de máquina, para reduzir o tempo de treinamento e evitar a maldição da dimensionalidade.

O PCA é uma técnica poderosa e amplamente utilizada para redução de dimensionalidade e análise exploratória de dados, que ajuda a simplificar conjuntos de dados complexos, mantendo as informações mais importantes.

Sua implementação também é muito simples

import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA

# Passo 2: Gere um conjunto de dados de exemplo com vetores de alta dimensão (d = 10)
np.random.seed(42)
n_samples = 1000
d = 10
X = np.random.randn(n_samples, d) # Gera 1000 vetores de dimensão 10 com distribuição normal

# Passo 3: Aplique o PCA para reduzir a dimensionalidade dos vetores para 2 dimensões
pca = PCA(n_components=2)
X_2d = pca.fit_transform(X)

# Passo 4: Plote os vetores reduzidos em um gráfico de dispersão para visualização
plt.figure(figsize=(8, 6))
plt.scatter(X_2d[:, 0], X_2d[:, 1], alpha=0.5)
plt.title('Visualização dos Vetores Reduzidos com PCA em 2 Dimensões')
plt.xlabel('Componente Principal 1')
plt.ylabel('Componente Principal 2')
plt.grid(True)
plt.show()

PCA é comumente usada para reduzir a dimensão dos seus dados. Intuitivamente, o modelo colapsa os dados através dos componentes principais. Você pode pensar no primeiro componente principal (em um conjunto de dados 2D) como a linha onde há a maior quantidade de variância. Você pode então colapsar os pontos de dados nessa linha. Portanto, você passou de 2D para 1D. Podemos generalizar essa intuição para várias dimensões.

Com isso, acabamos gerando os Eigenvectors (autovetores) e Eigenvalues (autovalores), que são:

  • Eigenvectors: os vetores resultantes, também conhecidos como características não correlacionadas dos seus dados.
  • Eigenvalues: a quantidade de informação retida por cada nova característica. Você pode pensar nisso como a variância no vetor próprio.

Podemos seguir os seguintes passos para Calcular a PCA:

1. Normalizar a média dos seus dados.
2. Calcular a matriz de covariância.
3. Calcular SVD na sua matriz de covariância. Isso retorna [USV] = svd(Σ) (Sigma). As três matrizes U, S, V são desenhadas acima. U é rotulado com os vetores próprios, e S é rotulado com os autovalores.

Podemos então usar as primeiras n colunas do vetor U, para obter seus novos dados multiplicando XU[:, 0:n].

Conclusão

Este artigo focou somente no paradígma estatístico. Nele, foi abordado como extrair features e realizar o preprocessamento de dados, os modelos de regressão logística e algoritmo naive bayes, e como aplicá-los na tarefa de classificação de texto para análise de sentimento, foi descrito o que são Vector Space Models, como eles representam palavras e documentos e como construir matrizes de coocorrencia no formato word by word e word by document, como avaliar a similaridade de vetores com a distância euclidiana e similaridade de cosseno, como manipular palavras nos espaços vetoriais e o algoritmo Principal Component Analysis e como utilizá-lo.

Como Citar

Guimarães, Alysson. (Jun 2024). Natural Language Processing (NLP) Com Classificação e Espaços Vetoriais. https://medium.com/data-hackers/. https://medium.com/data-hackers/p-51291c08f29e

ou

@miscellaneous{guimaraes2024nlpvec,
title = {Natural Language Processing Com Classificação e Espaços Vetoriais},
author = {Guimarães, Alysson},
journal = {https://medium.com/data-hackers/},
year = {2024},
month = {Jun},
url = {https://k3ybladewielder.medium.com/p-51291c08f29e}
}

Referência

Licença

--

--

Alysson Guimarães
Data Hackers

Data Scientist. MBA Competitive Intelligence, Bachelor of Business Administration.