Representación vectorial de textos (Parte 2) TF-IDF

NLP - Analítica Estratégica de Datos


Fundación Universitaria Konrad Lorenz
Docente: Viviana Márquez vivianam.penama@konradlorenz.edu.co
Clase #5: Marzo 18, 2021

Retroalimentación taller 3 \& 4

⌛ En la clase anterior

  • Herramientas específicas de pre-procesamiento de texto en NLP
    • Palabras vacías
    • Tokenización
    • Stemming
    • Lematización
    • Etiquetado gramatical
  • Repaso de Feature Engineering en Machine Learning
  • Representación de datos en forma numérica
  • Espacio semántico vectorial
  • Métodos de vectorización
    • One-Hot Encoding
    • Bag of Words
    • Bag of N-Grams

Flujo de datos en un proyecto de NLP (pipeline)


Flujo de datos en un proyecto de NLP (pipeline) --- En clases anteriores


Flujo de datos en un proyecto de NLP (pipeline) --- En clases anteriores


Flujo de datos en un proyecto de NLP (pipeline) --- Hoy


Feature Engineering para NLP

Representación vectorial de textos

  • Existen varios métodos
  • Lo que diferencia un método del otro es qué tan bien captura las propiedades lingüísticas del texto que representa y la cantidad de espacio que ocupa en memoria

  • Métodos más populares:
    • One-Hot Encoding
    • Bag of Words (Bolsa de palabras)
    • Bag of N-Grams (Bolsa de n-gramas)
    • TF-IDF
    • Word embeddings (word2vec)
      • CBOW (Bolsa de palabras continua)
      • SkipGram

🚀 Hoy veremos...

  • Continuación de los métodos de vectorización

    • TF-IDF
  • Medidas de similitud

    • Distancia Euclidiana
    • Distancia del coseno
    • Distancia de Jaccard
    • Distancia de Levenshtein

🛠️ TF-IDF

  • En los métodos que vimos en la clase pasada no hay ninguna noción de que algunas palabras del documento sean más importantes que otras
  • TF-IDF (Term Frequency, Inverse Document Frequency) se ocupa de este tema
  • Busca cuantificar la importancia de una palabra relativa a las otras palabras del documento y del corpus
  • Se usa frecuentemente en los sistemas de recuperación de información y algoritmos de agrupación
  • Entre más ayuda una palabra a distinguir un documento de los demás, más alta va a ser su puntuación TF-IDF
In [112]:
import re
import pandas as pd
import numpy as np 

corpus = {'D1': 'in the new york times in',
          'D2': 'the new york post',
          'D3': 'the los angeles times'}

corpus = pd.DataFrame.from_dict(corpus, orient='index', columns=['texto'])

corpus
Out[112]:
texto
D1 in the new york times in
D2 the new york post
D3 the los angeles times

TF: Term Frequency

  • Frecuencia de términos: Contar el número de ocurrencias de una palabra en un documento, dividido por el número de palabras en ese documento
$$tf(t,d) = \dfrac{count(t)}{|d|}$$
  • $t$ = término
  • $d$ = documento
In [113]:
corpus['d'] = corpus.texto.apply(lambda val: len(val.split()))

corpus
Out[113]:
texto d
D1 in the new york times in 6
D2 the new york post 4
D3 the los angeles times 4
In [114]:
from sklearn.feature_extraction.text import CountVectorizer

count_vect = CountVectorizer()
bow_rep = count_vect.fit_transform(corpus['texto'].values)
In [115]:
# Contar el número de cada una de las palabras en el documento
tf = pd.DataFrame(bow_rep.toarray())
tf.columns = count_vect.get_feature_names()
tf.index = corpus.index
tf = tf.T
In [116]:
tf = tf.div(corpus['d'], axis=1).round(3)

tf
Out[116]:
D1 D2 D3
angeles 0.000 0.00 0.25
in 0.333 0.00 0.00
los 0.000 0.00 0.25
new 0.167 0.25 0.00
post 0.000 0.25 0.00
the 0.167 0.25 0.25
times 0.167 0.00 0.25
york 0.167 0.25 0.00
In [ ]:
 
In [ ]:
 

👮 Pop Quiz

  • ¿Cuál es el valor máximo de $tf(t,d$)?
$$tf(t,d) = \dfrac{count(t)}{|d|}$$

DF: Document Frequency

  • La frecuencia de términos es más alta para palabras frecuentemente usadas en un documento
  • Frecuencia en documentos: Es el número de documentos que tienen esa palabra sobre el número total de documentos
$$df(t,N) = \dfrac{|\{d_i:t\in d_i, i=1,\cdots, N\}|}{N}$$
  • $t$ = término
  • $N$ = número de documentos en el corpus
In [117]:
# En cuántos documentos aparece cada una de las palabras, dividido por la cantidad de documentos
df = {}

for palabra in count_vect.get_feature_names():
    suma = corpus['texto'].apply(lambda val: palabra in val).sum()
    df[palabra] = suma

df = pd.DataFrame.from_dict(df, orient="index", columns=['doc_count'])

N = corpus.shape[0]

df['df'] = df['doc_count']/N

df
Out[117]:
doc_count df
angeles 1 0.333333
in 1 0.333333
los 1 0.333333
new 2 0.666667
post 1 0.333333
the 3 1.000000
times 2 0.666667
york 2 0.666667
  • La frecuencia en documentos es más alta para palabras usadas en muchos documentos
  • Por otro lado, una palabra específica a algún documento va a tener frecuencia de término muy baja
  • Como el objetivo es distinguir un documento del otro, queremos resaltar las palabras usadas frecuentemente en un documento pero penalizarlas si están presentes en todos los documentos. A esto se le llama la puntuación TF-IDF
  • Inicialmente, $$tfidf(t,d,N) = \dfrac{tf(t,d)}{df(t,N)}$$

  • Pero esto no nos da un buen puntaje. La fórmula mejorada es:

$$tfidf(t,d,N) = tf(t,d) \cdot \log\left(\dfrac{1}{df(t,N)}\right)$$
  • Cuando $t$ está en todos los documentos, $idf$ es $\log(1) = 0$

  • Esto tiene sentido ya que una palabra que está en todos los documentos es muy mala para distinguir entre documentos

In [125]:
df['idf'] = 1/df['df']
df['log_idf'] = np.log10(df['idf'])

df
Out[125]:
doc_count df idf log_idf
angeles 1 0.333333 3.0 0.477121
in 1 0.333333 3.0 0.477121
los 1 0.333333 3.0 0.477121
new 2 0.666667 1.5 0.176091
post 1 0.333333 3.0 0.477121
the 3 1.000000 1.0 0.000000
times 2 0.666667 1.5 0.176091
york 2 0.666667 1.5 0.176091
In [123]:
tfidf = df.join(tf)
tfidf["tfidf_d1"] = tfidf['D1'] * tfidf['log_idf']
tfidf["tfidf_d2"] = tfidf['D2'] * tfidf['log_idf']
tfidf["tfidf_d3"] = tfidf['D3'] * tfidf['log_idf']

tfidf[['tfidf_d1', 'tfidf_d2', 'tfidf_d3']]
Out[123]:
tfidf_d1 tfidf_d2 tfidf_d3
angeles 0.000000 0.000000 0.119280
in 0.158881 0.000000 0.000000
los 0.000000 0.000000 0.119280
new 0.029407 0.044023 0.000000
post 0.000000 0.119280 0.000000
the 0.000000 0.000000 0.000000
times 0.029407 0.000000 0.044023
york 0.029407 0.044023 0.000000
In [132]:
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vect = TfidfVectorizer()
tfidf = tfidf_vect.fit_transform(corpus['texto'].values)

tfidf_matrix = pd.DataFrame(tfidf.toarray(), columns=tfidf_vect.get_feature_names())
tfidf_matrix.index = corpus.index

tfidf_matrix.T.round(3)
Out[132]:
D1 D2 D3
angeles 0.000 0.000 0.584
in 0.811 0.000 0.000
los 0.000 0.000 0.584
new 0.308 0.480 0.000
post 0.000 0.632 0.000
the 0.239 0.373 0.345
times 0.308 0.000 0.445
york 0.308 0.480 0.000

Más info en 1, 2.

🔮 En la próxima clase

Pasando de TF-IDF a Word2Vec

  • Hasta el momento, las representaciones vectoriales de texto que hemos vistos tratan las unidades lingüísticas como unidades atómicas
  • Los vectores son dispersos
  • Tienen problema con palabras fuera del vocabulario

-Con representaciones distribuidas, como word2vec, podemos crear representaciones densas y bajas en dimensión que capturan similitudes distributivas entre palabras

Medidas de similitud

¿Qué tan parecidos son los documentos?

In [135]:
n1 = "La compañía Boring de Elon Musk construirá una conexión de alta velocidad en el aeropuerto de Chicago"
n2 = "La compañía Boring de Elon Musk construirá un enlace de alta velocidad al aeropuerto de Chicago"
n3 = "La empresa Boring de Elon Musk aprobó la construcción del tránsito de alta velocidad entre el centro de Chicago y el aeropuerto O'Hare."
n4 = "Tanto la manzana como la naranja son frutas"

corpus = {'n1': n1,
          'n2': n2,
          'n3': n3,
          'n4': n4}

corpus = pd.DataFrame.from_dict(corpus, orient='index', columns=['texto'])

corpus
Out[135]:
texto
n1 La compañía Boring de Elon Musk construirá una...
n2 La compañía Boring de Elon Musk construirá un ...
n3 La empresa Boring de Elon Musk aprobó la const...
n4 Tanto la manzana como la naranja son frutas
In [143]:
import re 
from nltk.corpus import stopwords
stopwords_sp = stopwords.words('spanish')

def pre_procesado(texto):
    texto = texto.lower()
    texto = re.sub(r"[\W\d_]+", " ", texto)
    texto = texto.split() # tokenización 
    texto = [palabra for palabra in texto if palabra not in stopwords_sp]
    texto = " ".join(texto)
    return texto 

corpus['pp'] = corpus['texto'].apply(lambda val: pre_procesado(val))
corpus
Out[143]:
texto pp
n1 La compañía Boring de Elon Musk construirá una... compañía boring elon musk construirá conexión ...
n2 La compañía Boring de Elon Musk construirá un ... compañía boring elon musk construirá enlace al...
n3 La empresa Boring de Elon Musk aprobó la const... empresa boring elon musk aprobó construcción t...
n4 Tanto la manzana como la naranja son frutas manzana naranja frutas
In [162]:
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf_vec = TfidfVectorizer()
tfidf = tfidf_vec.fit_transform(corpus['pp'].values)

tfidf_matrix = pd.DataFrame(tfidf.toarray())
tfidf_matrix.columns = tfidf_vec.get_feature_names()
tfidf_matrix.index = corpus.index

tfidf_matrix = tfidf_matrix.T

tfidf_matrix
Out[162]:
n1 n2 n3 n4
aeropuerto 0.282775 0.282775 0.214535 0.00000
alta 0.282775 0.282775 0.214535 0.00000
aprobó 0.000000 0.000000 0.336111 0.00000
boring 0.282775 0.282775 0.214535 0.00000
centro 0.000000 0.000000 0.336111 0.00000
chicago 0.282775 0.282775 0.214535 0.00000
compañía 0.349284 0.349284 0.000000 0.00000
conexión 0.443022 0.000000 0.000000 0.00000
construcción 0.000000 0.000000 0.336111 0.00000
construirá 0.349284 0.349284 0.000000 0.00000
elon 0.282775 0.282775 0.214535 0.00000
empresa 0.000000 0.000000 0.336111 0.00000
enlace 0.000000 0.443022 0.000000 0.00000
frutas 0.000000 0.000000 0.000000 0.57735
hare 0.000000 0.000000 0.336111 0.00000
manzana 0.000000 0.000000 0.000000 0.57735
musk 0.282775 0.282775 0.214535 0.00000
naranja 0.000000 0.000000 0.000000 0.57735
tránsito 0.000000 0.000000 0.336111 0.00000
velocidad 0.282775 0.282775 0.214535 0.00000

Medidas de similitud: Distancia euclidiana


In [165]:
from sklearn.metrics.pairwise import euclidean_distances

dist_euc = euclidean_distances(tfidf_matrix.T.values)
dist_euc = pd.DataFrame(dist_euc, columns = tfidf_matrix.columns, index=tfidf_matrix.columns)

print(dist_euc)
corpus
          n1        n2        n3        n4
n1  0.000000  0.626528  1.072701  1.414214
n2  0.626528  0.000000  1.072701  1.414214
n3  1.072701  1.072701  0.000000  1.414214
n4  1.414214  1.414214  1.414214  0.000000
Out[165]:
texto pp
n1 La compañía Boring de Elon Musk construirá una... compañía boring elon musk construirá conexión ...
n2 La compañía Boring de Elon Musk construirá un ... compañía boring elon musk construirá enlace al...
n3 La empresa Boring de Elon Musk aprobó la const... empresa boring elon musk aprobó construcción t...
n4 Tanto la manzana como la naranja son frutas manzana naranja frutas

Medidas de similitud: Distancia del coseno


In [170]:
from sklearn.metrics.pairwise import cosine_distances

dist_cos = cosine_distances(tfidf_matrix.T.values)
dist_cos = pd.DataFrame(dist_cos, columns = tfidf_matrix.columns, index = tfidf_matrix.columns)
dist_cos
Out[170]:
n1 n2 n3 n4
n1 0.000000 0.196269 0.575343 1.0
n2 0.196269 0.000000 0.575343 1.0
n3 0.575343 0.575343 0.000000 1.0
n4 1.000000 1.000000 1.000000 0.0

¿Cuándo usar la distancia del coseno en vez de la euclidiana?


Medidas de similitud: Distancia de Jaccard


  • Jaccard Similarity = (Intersection of A and B) / (Union of A and B)
In [176]:
def jaccard_distance(list1, list2):
    s1 = set(list1)
    s2 = set(list2)
    resultado = 1 - len(s1.intersection(s2)) / len(s1.union(s2))
    return resultado

jaccard_distance(corpus.iloc[0]['pp'].split(), corpus.iloc[3]['pp'].split())
Out[176]:
1.0

Medidas de similitud: Distancia de Levenshtein

  • Es el número mínimo de operaciones requeridas para transformar una cadena de caracteres en otra
  • Se usa en los correctores de ortografía

Ejemplo

La distancia de Levenshtein entre "casa" y "calle" es de 3 porque se necesitan al menos tres ediciones elementales para cambiar uno en el otro.

  • casa → cala (sustitución de 's' por 'l')
  • cala → calla (inserción de 'l' entre 'l' y 'a')
  • calla → calle (sustitución de 'a' por 'e')
In [180]:
import nltk

nltk.edit_distance(corpus.iloc[0]['pp'].split(), corpus.iloc[3]['pp'].split())
Out[180]:
10

🤓 Recapitulando: Hoy aprendímos...

  • TF-IDF

Medidas de similitud

  • Distancia Euclidiana
  • Distancia del coseno
  • Distancia de Jaccard
  • Distancia de Levenshtein

🚧 ✋ Expectativas del Proyecto Final

Modo:

  • Máximo 3 personas por grupo
  • Exposición ~10 min y repositorio de GitHub
  • ABRIL 15 - Plan de Proyecto (Documento con descripción del proyecto)
  • JUNIO 3 - Entrega Proyecto
  • JUNIO 3 y 10 - Exposiciones (La fecha puede cambiar)

Proyecto:

  • Los datos pueden ser personales, del internet, o de su empresa (pedir permiso).
  • Tener objetivo claro
  • Pre-procesamiento
  • Modelo de NLP/Machine Learning
  • Visualización

Ideas para conseguir datos:

¡Tiempo de taller!

Taller # 5: Representación vectorial de textos (Parte 2)

Fecha de entrega: Marzo 25, 2021. (Antes del inicio de la próxima clase)

Proxima clase(s): Representación vectorial de textos (Parte 3)