Introdução ao Processamento de Linguagem Natural (PLN) Usando Python

Professor Fernando Vieira da Silva MSc.

Técnicas para Pré-Processamento - Parte 2

Uma vez que o texto já foi devidamente tratado, removendo stopwords e pontuações, e aplicando stemming ou lemmatization, agora precisamos contar a frequência das palavras (ou n-grams) que utilizaremos em seguida como atributos para as técnicas de aprendizado de máquina.

1. TF-IDF (Term Frequency - Inverse Document Frequency)

Term Frequency: um termo que aparece muito em um documento, tende a ser um termo importante. Em resumo, divide-se o número de vezes em que um termo apareceu pelo maior número de vezes em que algum outro termo apareceu no documento.

tfwd = fwd / mwd

onde:
fwd é o número de vezes em que o termo w aparece no documento d.
mwd é o maior valor de fwd obtido para algum termo do documento d

Inverse Document Frequency: um termo que aparece em poucos documentos pode ser um bom descriminante. Obtem-se dividindo o número de documentos pelo número de documentos em que o termo aparece.

idfw = log2(n / nw)

onde:
n é o número de documentos no corpus nw é o número de documentos em que o termo w aparece.

Na prática, usa-se:

tf-idf = tfwd * idfw

Podemos calcular o TF-IDF de um corpus usando o pacote scikit-learn. Primeiramente, vamos abrir novamente o texto de Hamlet e armazenar as sentenças em uma ndarray do numpy (como se cada sentença fosse um documento do corpus):


In [1]:
import nltk
import numpy as np
from nltk.tokenize import sent_tokenize

hamlet_raw = nltk.corpus.gutenberg.raw('shakespeare-hamlet.txt')

sents = sent_tokenize(hamlet_raw)

hamlet_np = np.array(sents)

print(hamlet_np.shape)


(2355,)

Agora vamos definir uma função para tokenização pelo scikit-learn.


In [2]:
from nltk import pos_tag
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from nltk.tokenize import word_tokenize
import string
from nltk.corpus import wordnet

stopwords_list = stopwords.words('english')

lemmatizer = WordNetLemmatizer()

def my_tokenizer(doc):
    words = word_tokenize(doc)
    
    pos_tags = pos_tag(words)
    
    non_stopwords = [w for w in pos_tags if not w[0].lower() in stopwords_list]
    
    non_punctuation = [w for w in non_stopwords if not w[0] in string.punctuation]
    
    lemmas = []
    for w in non_punctuation:
        if w[1].startswith('J'):
            pos = wordnet.ADJ
        elif w[1].startswith('V'):
            pos = wordnet.VERB
        elif w[1].startswith('N'):
            pos = wordnet.NOUN
        elif w[1].startswith('R'):
            pos = wordnet.ADV
        else:
            pos = wordnet.NOUN
        
        lemmas.append(lemmatizer.lemmatize(w[0], pos))

    return lemmas

E essa função será chamada pelo objeto TfidfVectorizer


In [4]:
from sklearn.feature_extraction.text import TfidfVectorizer

hamlet_raw = nltk.corpus.gutenberg.raw('shakespeare-hamlet.txt')

sents = sent_tokenize(hamlet_raw)

hamlet_np = np.array(sents)

tfidf_vectorizer = TfidfVectorizer(tokenizer=my_tokenizer)

tfs = tfidf_vectorizer.fit_transform(hamlet_np)

print(tfs.shape)


(2355, 4305)

In [5]:
print([k for k in tfidf_vectorizer.vocabulary_.keys()][:20])


['tragedie', 'hamlet', 'william', 'shakespeare', '1599', 'actus', 'primus', 'scoena', 'prima', 'enter', 'barnardo', 'francisco', 'two', 'centinels', "'s", 'fran', 'nay', 'answer', 'stand', 'vnfold']

In [6]:
print(tfs[:50,:50])


  (0, 17)	0.40984054295
  (4, 5)	1.0
  (12, 6)	0.255695651027
  (13, 6)	0.241831885737
  (22, 5)	0.58657679876
  (27, 0)	0.226773205328
  (29, 5)	0.284581755506
  (37, 5)	0.264020196593
  (37, 0)	0.236027784598
  (39, 6)	0.138754555877
  (40, 13)	0.352972670444
  (41, 25)	0.314756261552
  (43, 5)	0.144160249461
  (46, 5)	0.322996303502

2. TF-IDF de N-gramas

Opcionalmente, podemos obter os atributos tf-idf de n-grams, combinando as classes CountVectorizer e TfidfTransformer. Em nosso exemplo, vamos utilizar apenas trigramas:


In [7]:
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer

count_vect = CountVectorizer(ngram_range=(3,3))

n_gram_counts = count_vect.fit_transform(hamlet_np)

tfidf_transformer = TfidfTransformer()

tfs_ngrams = tfidf_transformer.fit_transform(n_gram_counts)

print(tfs_ngrams.shape)


(2355, 23483)

3. Redução de Dimensionalidade

A transformação do corpus em atributos contendo as frequências TF-IDF em geral resultará numa ndarray bastante esparsa, ou seja, com muitas dimensões. Porém, além de isso tornar o treinamento de algoritmos mais demorado e custoso (computacionalmente falando), muitas dessas dimensões provavelmente são pouco representativas ou mesmo podem causar ruído durante o treinamento. Para resolver esse problema, podemos aplicar uma técnica de redução de dimensionalidade simples chamada Singular Value Decomposition (SVD).

Essa técnica transformará os vetores da matriz original, rotacionando e escalando-os, resultando em novas representações. A redução de dimensionalidade é feita ao manter apenas as k dimensões mais representativas que escolhermos. Outra vantagem dessa técnica é que as dimensões originais são, de certa forma, "combinadas", o que resulta em uma nova forma de representar a combinação de termos. No contexto de PLN, essa técnica é conhecida como Latent Semantic Analysis (LSA)


In [8]:
from sklearn.decomposition import TruncatedSVD

svd_transformer = TruncatedSVD(n_components=1000)

svd_transformer.fit(tfs)

print(sorted(svd_transformer.explained_variance_ratio_)[::-1][:30])


[0.04319143964864218, 0.018619648885290416, 0.015991826821457188, 0.015984996712032705, 0.013744333318752369, 0.011771580238264153, 0.0096502136679727582, 0.0095168074249578714, 0.0095050502041705634, 0.0094605420641273308, 0.0093174536752533427, 0.0092700425653136059, 0.0077114068911813705, 0.0069839976227606569, 0.0066546888854344659, 0.0064195285763082675, 0.0058853020300615594, 0.0053722779003033136, 0.0051581711906631012, 0.0049312579563956863, 0.0048675271120016935, 0.0047897360656975214, 0.0044624928650002895, 0.0043399578651272148, 0.0042969022436024646, 0.0041605617284113843, 0.0040394806461863204, 0.0039137333696623025, 0.0038585289999962837, 0.0038298284368970051]

Agora vamos manter as dimensões até que a variância acumulada seja maior ou igual a 0.50.


In [9]:
cummulative_variance = 0.0
k = 0
for var in sorted(svd_transformer.explained_variance_ratio_)[::-1]:
    cummulative_variance += var
    if cummulative_variance >= 0.5:
        break
    else:
        k += 1
        
print(k)


143

Transformarmos novamente, mas desta vez com o número de k componentes que obtemos anteriormente.


In [11]:
svd_transformer = TruncatedSVD(n_components=k)
svd_data = svd_transformer.fit_transform(tfs)
print(sorted(svd_transformer.explained_variance_ratio_)[::-1])


[0.043191439648672884, 0.018619648883417345, 0.015991826858359582, 0.015984996739453274, 0.013744333249896268, 0.011771579960380696, 0.0096502135927737202, 0.0095168071742513065, 0.0095050503528928427, 0.0094605422360137161, 0.009317453944640482, 0.0092700426922784143, 0.0077114076961383978, 0.0069839949092168879, 0.0066546898302034321, 0.0064195263269203598, 0.0058852936980912296, 0.0053722793434196623, 0.0051581705034021499, 0.0049312538165894974, 0.0048675261535410042, 0.004789732565309881, 0.0044624842454884517, 0.0043399501248714599, 0.0042969006188773064, 0.00416055574593406, 0.0040394733566841579, 0.0039137298223758969, 0.0038585318797753182, 0.0038298140891522641, 0.0037136338808673257, 0.0036634048773248437, 0.0035824588636568515, 0.0035617299738693034, 0.0035351647924154865, 0.0034284465525109675, 0.0033450509316554542, 0.0032429644299145728, 0.0032341007248697803, 0.0031762097278202309, 0.003122680063865171, 0.0030422258444417227, 0.0029313655524463721, 0.0029183200989965912, 0.0028876636134582559, 0.0028503348809795774, 0.0028175644272924725, 0.0027991818138099834, 0.0027527395476587158, 0.0027256300169464873, 0.0027164841919452413, 0.0026896792077978175, 0.0026503661642997721, 0.0025962298502006922, 0.002580426654239893, 0.0025425759751854533, 0.0025131520227835839, 0.0024690983005458554, 0.0024557336447891226, 0.0024444697564095456, 0.0024311018942105578, 0.0023815125796919833, 0.0023652521385923763, 0.0023213582876591221, 0.002300604848378299, 0.0022748927077490891, 0.0022642557156375932, 0.0022370846338525398, 0.002233029037504016, 0.0022220768499206377, 0.0022049901807487619, 0.0021754762794294474, 0.002167783763361299, 0.0021351427338382749, 0.0021136077570968908, 0.002086403211099016, 0.0020661754018540964, 0.0020395447508786508, 0.0020237663975328094, 0.0019949618434850673, 0.0019808359060320899, 0.001976774892756029, 0.001953091906483127, 0.00193243419709186, 0.0019139163835452772, 0.0019059020392949142, 0.0018803721320935743, 0.0018699180135109388, 0.0018581135666782737, 0.0018500451021882822, 0.0018306892720007946, 0.0018183332181344871, 0.0017886229681730315, 0.0017703878313556753, 0.0017515012650277894, 0.0017411335559160772, 0.0017162083072760417, 0.0017057854865048917, 0.0016712761148612268, 0.0016523276814628919, 0.0016465803263344208, 0.0016405947817971862, 0.0016308584186387942, 0.0016041738649253217, 0.0015908995646463893, 0.0015723461908011133, 0.0015670337580166898, 0.0015456336390515973, 0.0015258583475587686, 0.0015057406313198122, 0.0015011421998867753, 0.0014854772669949828, 0.001479215257009449, 0.0014567317636996206, 0.0014425734521038795, 0.0014387985795931224, 0.0014345292502974515, 0.0014266481314770521, 0.0014173351276758662, 0.0014090718813207128, 0.0013907025649712238, 0.0013757041499918073, 0.0013642239911746995, 0.0013579698713574329, 0.0013481919595701209, 0.0013359093562873711, 0.0013284325575027773, 0.0013197899015350537, 0.0012927329912149727, 0.0012881099994049828, 0.001278277531331054, 0.0012734024758883241, 0.0012626069210877257, 0.0012460043499499232, 0.0012408665084539304, 0.0012318864460886803, 0.001218825155308972, 0.0012051389824519734, 0.0012023344312770683, 0.0011894988555160425, 0.0011881114695934411, 0.0011788391472506298, 0.0011713127928922565]

In [12]:
print(svd_data.shape)


(2355, 143)

In [ ]: