Executando verificação de segurança...
8

Vetorizando palavras - NLP - count vectorizing

Count vectorization

Nesse processo, alguns passos são executados:

  1. Transformar um parágrafo em várias frases.
  2. Limpar o texto (de forma simples)
    • Transformar tudo em lower case
    • substituir simbolos especiais por representações
    • remover espaços duplicados e antes e depois do texto
    • remover pontuação
  3. Pegamos o array de frases que são um list[str], uma lista de strings e convertemos para um list[list[str]] ou seja, um array onde cada array dentro dele representa uma frase e esse array terãos os tokens dessa frase
  4. Capturamos todos os tokens únicos de todas as frases e obtemos um list[str] que é uma lista de tokens únicos
  5. Então, criamos um hashset onde irá armazenar o número de ocorrências de cada token único para cada frase, então obtemos uma estrutura de dados no formato dict[int, dict[str, int]], onde a chave do dicionário superior é o índice da frase no array de frases, o valor é um dicionário, onde seu índice é um token único e seu valor a quantidade de aparições na frase em questão
  6. Transformamos essa estrutura em uma matrix no formato list[list[int]], onde cada linha representa uma frase e cada item dessa linha é a quantidade de aparições de uma determinada palavra na frase em questão

Implementação manual

Para prosseguir, você precisará instalar uma biblioteca chamada nltk.

Para instalar execute o seguinte comando:

pip install nltk

Então, vamos lá.

Primeiro vamos importar a biblioteca

import nltk

Agora, precisamos fazer o download de um modelo chamado 'punkt' do nltk para podermos usar a funcionalidade de tokenização (não queremos implementar essa parte na mão no momento).

nltk.download('punkt')

Após isso, vamos importar algumas dependências que usaremos mais tarde:

from io import BytesIO
from tokenize import tokenize
import re
from string import punctuation

Agora, vamos criar uma variável com uma frase que queremos vetorizar:

paragraph = 'I like to play football. Did you go outside to play tennis. John and I play tennis.'

Agora, vamos extrair as sentenças (frases) desse parágrafo:

raw_sentences = nltk.sent_tokenize(paragraph)

Uma parte importante do processo é limpar o nosso texto, removendo coisas como:

  • Pontuação
  • Emojis
  • Espaços duplicados (antes ou depois do texto)
  • entre outras técnicas

Nesse exemplo, nos contentaremos em:

  • Transformar tudo em lower case
  • substituir simbolos especiais por representações
  • remover espaços duplicados e antes e depois do texto
  • remover pontuação

Vamos criar uma função para isso:

def clear_text(sentences: list[str]) -> list[str]:
    result = []

    for sentence in sentences:
        string = ' '.join(sentence.lower().casefold().split())
        string = re.sub(f"[{re.escape(punctuation)}]", "", string)
        result.append(string)

    return result

Agora, vamos limpar nossas frases utilizando a função que acabamos de criar:

sentences = clear_text(raw_sentences)

E temos o seguinte resultado:

['i like to play football',
 'did you go outside to play tennis',
 'john and i play tennis']

Agora, precisamos tokenizar cada uma dessas frases e para isso, iremos criar mais uma função:

def sentence_into_words(sentences: list[str]) -> list[list[str]]:
   result = []

   for sentence in sentences:
       tokens = tokenize(BytesIO(sentence.encode('utf-8')).readline)
       tokens = [token.string for token in tokens if token.exact_type not in [0, 4, 63]]
       tokens = [token for token in tokens if len(token) > 1]

       result.append(tokens)

   return result

Essa função utiliza a biblioteca nltk para fazer a tokenização.
Podemos perceber que antes de chamar o método para tokenizar a frase em si, codificamos nossa string em "utf-8" e tranformamos ela em bytes.

Após isso aplicamos a função de tokenização.

Repare que logo após isso removemos os tokens que são identificados por "0", "4" e "63".
Isso é feito pelo simples motivo de que são tokens que representam o "encoding", "new-line" e "end", para nosso caso, não precisamos deles.

sentence_as_words = sentence_into_words(sentences)

Agora, vamos ter de extrair todas as palavras que são únicas entre todas as frases.
Para isso iremos criar outra função:

def get_unique_words_from_matrix(matrix: list[list[str]]) -> list[str]:
    unique_words = set()

    for row in matrix:
        for word in row:
            if word in unique_words:
                continue
            unique_words.add(word)

    return list(unique_words)

E chamamos a mesma:

unique_words = get_unique_words_from_matrix(sentence_as_words)

Com isso temos o seguinte resultado:

['did',
 'tennis',
 'john',
 'you',
 'to',
 'like',
 'go',
 'football',
 'outside',
 'play',
 'and']

Isso é um dos importantes passos para nossa vetorização, pois, precisamos contar o número de ocorrências de cada um desses tokens em cada uma de nossas frases.

Então, vamos começar fazer isso.

Para fazer isso, vou criar uma função que a princípio vai ser bem complicada de entender:

def create_count_table(unique_words: list[str], sentence_as_words: list[list[str]]) -> dict[int, dict[str, int]]:
   relation = { i: dict(zip(sorted(unique_words), [0] * len(unique_words))) for i in range(len(sentence_as_words))}

   for index, sentence in enumerate(sentence_as_words):
       for word in sentence:
           relation[index][word] += 1

   return relation

Tudo bem, o que raios está acontecendo nessa variável relation?

Bom, basicamente a estrutura que precisamos é a seguinte:

{
  "indice-da-frase" {
    "palavra-unica-1": "total-de-ocorrencia",
    "palavra-unica-2": "total-de-ocorrencia",
    "palavra-unica-3": "total-de-ocorrencia",
    ...
  }
}

Para facilitar esse processo resolvi encurtar um pouco o código em python fazendo:

{ i: dict(zip(sorted(unique_words), [0] * len(unique_words))) for i in range(len(sentence_as_words))}

Vamos dividir isso em partes:

Primeiro esse dict(zip(sorted(unique_words), [0] * len(unique_words))).

Nesse trecho estamos organizando as palavras únicas por ordem alfabetica e criando um zip dessas palavras com um array de 0's do tamanho da nossa lista de palavras únicas e transformando isso em um dicionário, o que vai nos dar uma estrutura nesse formato:

{
  "and":0,
  "did":0,
  "football":0,
  "go":0,
  "john":0,
  "like":0,
  "outside":0,
  "play":0,
  "tennis":0,
  "to":0,
  "you":0
}

Iniciando nosso hashmap onde tenho todos os tokens únicos com 0 ocorrências.

E então, replicamos isso para todas as frases e colocamos o índice da frase como chave e esse nosso hashmap como valor.

Agora que entendemos essa parte, vamos prosseguir.

unique_appearances = create_count_table(unique_words, sentence_as_words)

Agora, podemos visualizar quantas vezes cada palavra apareceu em cada frase:

for key in unique_appearances.keys():
    print(key, sentences[key])
    for word in unique_appearances[key].keys():
        print(f'\t"{word}" appears {unique_appearances[key][word]} times')

um output parecido com:

0 i like to play football
	"and" appears 0 times
	"did" appears 0 times
	"football" appears 1 times
	"go" appears 0 times
	"john" appears 0 times
	"like" appears 1 times
	"outside" appears 0 times
	"play" appears 1 times
	"tennis" appears 0 times
	"to" appears 1 times
	"you" appears 0 times

Se dermos uma olhada na variável "unique_appearances" teremos algo como:

{0: {'and': 0,
  'did': 0,
  'football': 1,
  'go': 0,
  'john': 0,
  'like': 1,
  'outside': 0,
  'play': 1,
  'tennis': 0,
  'to': 1,
  'you': 0},
 1: {'and': 0,
  'did': 1,
  'football': 0,
  'go': 1,
  'john': 0,
  'like': 0,
  'outside': 1,
  'play': 1,
  'tennis': 1,
  'to': 1,
  'you': 1},
 2: {'and': 1,
  'did': 0,
  'football': 0,
  'go': 0,
  'john': 1,
  'like': 0,
  'outside': 0,
  'play': 1,
  'tennis': 1,
  'to': 0,
  'you': 0}}

Agora, transformamos essa estrutura de dados em uma matrix:

def unique_appearances_to_matrix(appearances: dict[int, dict[str, int]]) -> list[list[int]]:
    return [[appearance for appearance in appearances[key].values()] for key in appearances.keys()]
# Document term matrix
matrix = unique_appearances_to_matrix(unique_appearances)

E boom, temos o seguinte resultado:

[[0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0],
 [0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1],
 [1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0]]

Nosso parágrafo vetorizado.

2