Esquemas de pesado para representar documentos

Los modelos de espacio vectorial (vector space models (VSMs)) permiten representar palabras o términos dentro de un espacio vectorial continuo, de manera que las palabras que son similares desde el punto semántico se situan en puntos cercanos dentro de ese espacio común.

El uso de distintas aproximaciones de modelos de espacio vectorial tiene una larga tradición en PLN. Todas ellas comparten una misma hipótesis distribucional: las palabras o términos que comparten contextos tienen significados similares.


In [ ]:
# corpus ficticio con tres documentos de la misma longitud 
# y sin repeticiones de términos dentro del mismo documento

# cada doc es una lista de palabras
d1 = 'los angeles times'.split()
d2 = 'new york times'.split()
d3 = 'new york post'.split()

# nuestro corpus D es una lista de documentos
D = [d1, d2, d3]
print(D)

tf (term frequency)

tf es el peso que indica la frecuencia de un término, es decir, el número de veces que una determinada palabra aparece en un documento.

La aproximación más sencilla consiste consiste en asignar como peso para el término $t$ en el documento $d$ del corpus $D$ (denotado como $\mbox{tf}_{t,d}$) el número de ocurrencias de $t$ en $d$. Es recomendable normalizar esta frecuencia, diviendo el número de ocurrencias entre el número total de palabras de un documento, para no penalizar los documentos breves: $\mathrm{tf}(t,d) = \frac{\mathrm{f}(t, d)}{\max\{\mathrm{f}(w, d):w \in d\}}$

Vamos a calcularlo.

Calculando tf (1er intento)


In [ ]:
# calculamos los valores de tf para cada término t y cada docID 
# como un diccionario de diccionarios, tal que tf[t][docID] = valor

tf = {}

# iteramos sobre los documentos del corpus
for d in D:
    # iteramos sobre las palabras del documento
    for t in d:
        # si no he visto el término t antes, creo la clave en tf
        if t not in tf:
            tf[t] = {}
        # ¿cuál es el doc que estoy procesando?    
        docID = D.index(d) 
        # asigno el valor de tf para el término t y el documento actual
        # (número de veces que aparece t dividido entre el número de palabras de d)
        tf[t][docID] = d.count(t) / len(d) 
        
        
print(tf)

La aproximación anterior, tal cual está programada, arma un diccionario de diccionarios pero tiene varias desventajas:

  • no almaceno los valores de tf para aquellos documentos que no contienen ninguna ocurrencia de t.
  • si las claves de mi diccionario son números enteros correlativos, como es el caso, haría mejor en utilizar una estructura de datos ordenada: una lista.

Calculando tf (2º intento)


In [ ]:
# calculamos los valores de tf para cada término t y cada docID 
# como un diccionario de listas, tal que tf[t][i] = valor

tf = {}

# primera iteración, creo el esqueleto del diccionario de listas 
# iteramos sobre los documentos del corpus
for d in D:
    # iteramos sobre las palabras del documento
    for t in d:
        # relleno todas las casillas con 0
        tf[t] = [0] * len(D)
        
print('tf solo contiene 0s')       
print(tf)


# segunda iteración, reasigno los valores sólo en aquellas posiciones donde sea necesario
# iteramos sobre los documentos del corpus
for d in D:
    # iteramos sobre las palabras del documento
    for t in d:
        docID = D.index(d)
        tf[t][docID] = d.count(t) / len(d)
        
print('\ntf contiene los valores de tf que corresponden')       
print(tf)

En el caso de este corpus ficticio, todos los valores de tf son, o bien 0 (si el término no aparece en el documento), o bien $1/3$ si aparece una sola vez.

idf (inverse document frequency)

Trabajar unicamente con las frecuencias de los términos conlleva un problema: todos los términos presentes en la colección se consideran igualmente relevantes a la hora de discriminar la relevancia de los documentos, atendiendo a sus frecuencias. Y resulta que esto no es verdad.

Imaginemos un corpus en el que la frecuencia total de dos términos concretos, este y fonema, es similar en términos absolutos. La distribución de estos términos a lo largo de la coleccion es seguramente muy diferente. El primero aparece con una distribución uniforme a lo largo del corpus, su capacidad discriminativa es baja y debería penalizarse a la hora de asignar relevancia (como el resto de stopwords). El segundo, por el contrario, se concentra principalmente en documentos que hablan de fonología, su capacidad discriminativa es alta y debería ser premiado.

Existen mecanismos correctores para incorporar estas penalizaciones y premios en nuestros pesos. Los más habituales pasan por recurrir a la frecuencia de documento $\mbox{df}_t$, definida como el número de documentos de la colección $D$ que contienen el término $t$: $\mbox{df}_t = {|\{d \in D: t \in d\}|}$.

Más concretamente, se calcula la frecuencia inversa de documento, o idf (inverse document frequency), definida como: $\mbox{idf}_t = \log {|D|\over \mbox{df}_t}$, donde $|D|$ indica el número total de documentos de nuestra colección. De este modo, el idf de un término específico pero muy discriminativo será alto, mientras que el de un término muy frecuente a lo largo de la coleccion será bajo.

Calculando df


In [ ]:
# calculamos los valores de df para cada término t
df = {}

# iteramos sobre los término del vocabulario
for t in tf:
    # reiniciamos los valores a 0
    df[t] = 0
    for d in D:
        # para cada documento d que contenga a t, sumamos +1 al df correspondiente
        if t in d:
            df[t] += 1

print(df)

Los valores de df son números enteros: el número de documentos del corpus que contienen cada uno de los términos.

Calculando idf


In [ ]:
import math

# calculamos los valores de idf para cada término t
idf = {}

# iteramos sobre los término del vocabulario
for t in tf:
    idf[t] = math.log(len(D) / df[t])

print(idf)

Fíjate cómo interpretamos estos valores. Los términos que aparecen en un solo documento, tienen un idf más alto, son mejores descriptores del contenido de esos documentos, tienen más poder para discriminar temáticas. Los términos que se distribuyen en varios documentos tienen un idf más bajo, son peores descriptores.

tf.idf

td.idf (term frequency - inverse document frequency) es una medida numérica que expresa la relevancia de una palabra de un documento con respecto a una colección de documentos. Es uno de los esquemas de pesado más comunes en las tareas relacionadas con la recuperación de información y la minería de texto.

El objetivo de esta métrica es representar los documentos de texto como vectores, ignorando el orden concreto de las palabras pero manteniendo la información relativa a las frecuencias de aparición.

El valor de tf-idf de una palabra:

  • es mayor cuanto más frecuente sea esta palabra dentro de un documento concreto, pero;
  • es mayor cuando menos común sea la palabra en otros documentos de la colección.

Estas dos características premian a los términos que son muy frecuentes en determinados documentos concretos pero poco comunes en general: estos términos pueden considerarse buenos descriptores de un conjunto de documentos. Y a la vez, penalizan aquellos términos que aparecen con mucha frecuencia a lo largo de toda la colección, como las stopwords.

Calculando tf.idf

tf.idf se calcula como el producto de dos términos: $\mathrm{tf.idf}(t, d, D) = \mathrm{tf}(t, d) \times \mathrm{idf}(t, D)$

  • la frecuencia de un término (tf): el número de veces que una determinada palabra aparece en un documento.

  • la frecuencia inversa de documento (idf): el logaritmo del número total de documentos en el corpus dividido entre el número de documentos en los que el término aparece.

Ya hemos calculado previamente esos valores. Bastará con realizar los productos.


In [ ]:
# calculamos los valores de tf.idf para cada término t y cada docID 
# como un diccionario de listas, tal que tfidf[t][i] = valor
tfidf = {}

# iteramos sobre los términos del vocabulario
for t in tf:
    tfidf[t] = [] # inicializamos con una lista vacía
    # iteramos sobre los valores de tf del término t
    for d in tf[t]:
        # añadimos el nuevo valor multiplicando tf * idf
        tfidf[t].append( d * idf[t])
        
print(tfidf)

Repetimos el experimento con más documentos

Vamos a repetir todo lo visto hasta ahora en el cuaderno con otras colección ficticia de documentos.

Parte del código de las celdas anteriores lo voy a codificar como funciones, de manera que podamos ejecutar el cálculo de los distintos valores de manera más clara.

¡Allá vamos!


In [ ]:
def calcula_tf(corpus):
    """Calcula los valores de tf para cada término t de un corpus. 
    Devuelve un diccionario de listas tf[t][docID] = valor"""
    import math
    tf = {}
    # primera iteración, creo el esqueleto del diccionario de listas 
    # iteramos sobre los documentos del corpus
    for d in corpus:
        # iteramos sobre las palabras del documento
        for t in d:
            # rellenamos las casillas con casi el log de casi 0
            tf[t] = [math.log(0.00000001)] * len(D)
            
    # segunda iteración, reasigno los valores sólo en aquellas posiciones donde sea necesario
    # iteramos sobre los documentos del corpus
    for d in corpus:
        # iteramos sobre las palabras del documento
        for t in d:
            docID = corpus.index(d)
            tf[t][docID] = 1 + math.log(d.count(t) / len(d)) # log normalization
            
    return tf

In [ ]:
def calcula_idf(vocabulario, corpus):
    """Calcula los valores de idf para una lista de vocabulario y un corpus.
    Devuelve un diccionario idf[t] = valor"""
    import math    
    # primero, calculamos los valores de df para cada término t
    df = {}
    # iteramos sobre los término del vocabulario
    for t in vocabulario:
        # reiniciamos los valores a 0
        df[t] = 0
        for d in corpus:
            # para cada documento d que contenga a t, sumamos +1 al df correspondiente
            if t in d:
                df[t] += 1

    # después, calculamos los valores de idf para cada término t
    idf = {}
    # iteramos sobre los término del vocabulario
    for t in vocabulario:
        idf[t] = math.log(len(corpus) / df[t])

    return idf

In [ ]:
def calcula_tfidf(tf, idf):
    """Calcula los valores de tf.idf para un diccionario de valores tf y otro de valores idf.
    Devuelve un diccionario de listas tfidf[t][i] = valor
    """
    tfidf = {}
    # iteramos sobre los términos del vocabulario
    for t in tf:
        tfidf[t] = [] # inicializamos con una lista vacía
        # iteramos sobre los valores de tf del término t
        for d in tf[t]:
            # añadimos el nuevo valor multiplicando tf * idf
            tfidf[t].append( d * idf[t])

    return tfidf

In [ ]:
# construyo un nuevo corpus como una lista de docs, donde cada doc es una lista de palabras
# https://www.goodreads.com/author/quotes/272231.Eminem

eminem_quotes = """Love when spelled backwards and read phonetically reads evil|
Don’t do drugs don’t have unprotected sex don’t be violent Leave that to me|
If you have enemies good that means you stood up for something|
Somewhere deep down there's a decent man in me he just can't be found|
I can't tell you what it really is I can only tell you what it feels like|
Behind every sucessful person lies a pack of haters|
Sometimes I'm real cool but sometimes I could be a real asshole I think everyone is like that|
Love is just a word but you bring it definition|
Damn How much damage can you do with a pen|
Don't let them say you ain't beautiful They can all get fucked just stay true to you|
I come from Detroit where it's rough and I'm not a smooth talker|
If there's not drama and negativity in my life all my songs will be really wack and boring or something|
I always wished for this but it's almost turning into more of a nightmare than a dream|
Dealing with backstabbers there was one thing I learned They're only powerful when you got your back turned|
When I say I'll murder my baby's mother maybe I wanted to but I didn't Anybody who takes it literally is 10 times sicker than I am|
When you're a little kid you don't see color and the fact that my friends were black never crossed my mind It never became an issue until I was a teenager and started trying to rap|
It sometimes feels like a strange movie you know it’s all so weird that sometimes I wonder if it is really happening|
Personally I just think rap music is the best thing out there period If you look at my deck in my car radio you're always going to find a hip-hop tape; that's all I buy that's all I live that's all I listen to that's all I love|
I'm just a little bit sicker then the average individual I think|
Imma be what I set out to be without a doubt undoubtedly|
The truth is you don't know what is going to happen tomorrow Life is a crazy ride and nothing is guaranteed|
You'd have to walk a thousand miles in my shoes just to see what its like to be me|
Don't let them tell you ain't beautiful|
I act like shit don’t phase me inside it drives me crazy my insecurities could eat me alive|
But music is reflection of self we just explain it and then we get our checks in the mail|
Sometimes I feel like rap music is almost the key to stopping racism|
I might talk about killing people but that doesn't mean I do it|
Before I was famous when I was just working in Gilbert's Lodge everything was moving in slow motion|""".split('|\n')

# nuestro corpus D es una lista de documentos
# cada doc es una lista de palabras
D = []
for quote in eminem_quotes:    
    D.append( quote.lower().split() )

Ahora sí lo probamos :-)


In [ ]:
print('Calculando los valores tf... ', end='')
tf = calcula_tf(D)
print('¡ok!')

print('Calculando los valores idf... ', end='')
idf = calcula_idf(tf.keys(), D)
print('¡ok!')

print('Calculando los valores tf.idf... ', end='')
tfidf = calcula_tfidf(tf, idf)
print('¡ok!\n\n')

In [ ]:
# imprimimos los valores de algunos términos
print('love', tfidf['love'], '\n')
print('the', tfidf['the'], '\n')
print('backwards', tfidf['backwards'], '\n')
print('killing', tfidf['killing'], '\n')

In [ ]:
print('Los valores tf.idf para cada término del vocabulario son:')
for t in tfidf:
    print(t, '=>')
    print(tfidf[t], '\n\n')