26 de noviembre de 2011

10

Las matemáticas al servicio de la investigación literaria





¿Es posible conocer la autoría de una obra literaria por las palabras y la frecuencia de uso de éstas que contiene? Esta es una pregunta recurrente entre los estudios de obras que nos han llegado hasta hoy como anónimas y de las que se sospecha una autoría que no ha podido ser confirmada por estudios historico-literarios tradicionales.

Dos matemáticos y un físico, DaríoBenedetto, Emanuele Caglioti y Vittorio Loreto de la Universidad deLa Sapienza en Roma, decidieron poner a prueba el algoritmo Lempel-Ziv como método de identificación de creadores literarios. Su objetivo era identificar a los autores de obras literarias. Noventa textos escritos por 11 autores italianos (entre ellos Dante Alighieri y Pirandello) sirvieron como material de base. Se elegía el texto de un autor determinado y se le unían dos pequeños textos de igual tamaño: uno del mismo autor y otro de un autor diferente. Se introducían estos archivos en un programa de compresión, como el popular WinZip, y los científicos comparaban cuánto espacio de almacenamiento necesitaba cada uno. Conjeturaron que la entropía relativa del texto combinado les daría una idea sobre la autoría del texto anónimo. Si ambos eran obra del mismo autor, el algoritmo necesitaría menos espacio de almacenamiento que si el texto adjunto estaba escrito por otro diferente. En el segundo caso, la entropía relativa sería mayor, dado que el algoritmo tendría que considerar los distintos estilos y palabras usadas por ambos autores. En consecuencia, necesitaría más espacio para almacenar el archivo. Cuanto más pequeño fuera el archivo comprimido de los dos textos combinados, más probable era que el texto original y el adjunto pertenecieran al mismo autor. Los resultados del experimento fueron francamente increíbles. Cerca del 95% de las veces los programas de compresión permitieron identificar correctamente al autor.


Seguramente embargados por la emoción del éxito de su nuevo enfoque, los tres científicos no se dieron cuenta, o al menos olvidaron mencionar en su bibliografía, que su método no era tan original como creían. De hecho, no fueron los primeros en pensar que los métodos matemáticos se podrían usar para atribuir textos literarios a sus autores. George Zipf, profesor de Lingüística en Harvard, ya había abordado temas como la frecuencia de palabras en 1932. Y el escocés George Yule había demostrado en 1944, en un artículo titulado Estudio estadístico del vocabulario literario, cómo había podido atribuir el manuscrito De imítatione Christi al conocido místico Tomás de Kempis, que vivió en los Países Bajos en el siglo XV. Y por supuesto, hay que mencionar los papeles federalistas del siglo XVIII, cuya autoría por parte de Alexander Hamilton, Iames Madison y Iohn Iay fue determinada por los estadísticos americanos R. Prederick Mosteller y David L. Wallace

Dado que todo les había ido tan bien, Benedetto, Caglioti y Loreto decidieron llevar a cabo otro experimento. Analizaron los grados de afinidad entre lenguajes diferentes. Dos lenguas que pertenezcan a la misma familia lingüística deberían tener una entropía(1) relativa baja. Por tanto, podría comprimirse de forma más eficiente una combinación de dos textos escritos en lenguas que estén emparentadas que dos que pertenezcan a familias diferentes. Los científicos analizaron 52 lenguas europeas. De nuevo, tuvieron éxito. Usando el programa de compresión, pudieron clasificar cada lengua en su grupo lingüístico correspondiente. El italiano y el francés, por ejemplo, tienen una entropía relativa baja y por tanto pertenecen a la misma familia. El sueco y el croata, por otro lado, tienen una entropía relativa alta y por tanto han de provenir de grupos lingüísticos diferentes. WinZip consiguió incluso identificar el maltés, el vasco y el húngaro como lenguajes aislados que no pertenecían a ninguno de los grupos lingüísticos conocidos. 
El estudio completo se puede consultar en. Dario Benedetto, Emanuele Caglioti, Vittorio Loreto, "Language Trees and Zipping", Phys. Rev. Lett., 88, 048702 (2002) 

(1)
Para entender la compresión de datos, es necesario familiarizarse con el concepto de entropía. En física, la entropía es una medida del desorden de un sistema, por ejemplo un gas. En telecomunicaciones, la entropía es una medida del contenido en información de un mensaje. Un mensaje que consista, por ejemplo, en 1.000 repeticiones del número 0 tiene muy poco contenido en información y una entropía muy baja. Se puede comprimir a la pequeña formula 1000x0 Por otro lado, una secuencia totalmente aleatoria de unos y ceros tiene una entropía muy alta. No se puede comprimir en absoluto, y la única forma de almacenar dicha secuencia es repitiendo todos sus caracteres. La entropía relativa indica cuánto espacio de almacenamiento se ocupa si una secuencia de caracteres se comprime, con un método que se había optimizado para una secuencia diferente. El código morse, concebido para el inglés, puede ser un ejemplo. La letra que aparece mas frecuentemente en inglés, la e, obtuvo el código mas corto: un punto. Las letras que aparecen menos obtienen códigos mas largos, por ejemplo <<—.-» para la -q-. Para otras lenguas, el código morse no es idóneo, porque las longitudes de los códigos no corresponden con la frecuencia de las letras. La entropía relativa mide entonces cuántos puntos y guiones adicionales se necesitan para transmitir un texto, digamos en italiano, con un código que esta pensado para el inglés. 



La mayoría de las rutinas de compresión de datos estén basadas en algoritmos desarrollados a finales de los años setenta por dos científicos israelíes del Technion en Haifa. El método que desarrollaron Abraham Lempel, informático, y de Jacob Ziv, ingeniero electrónico, se basa en el hecho de que en un archivo aparecen secuencias idénticas de bits y bytes. La primera vez que una secuencia aparece en el texto, se introduce en una especie de diccionario. Cuando vuelve a aparecer la misma secuencia, un marcador señala el lugar adecuado del diccionario. Dado que el marcador ocupa menos espacio que la secuencia, el texto se comprime. Pero aún hay mas. La distribución de la tabla que lista todas las secuencias no sigue las reglas de clasificación de un diccionario normal, si no que se adapta al archivo en concreto que queremos comprimir. El algoritmo <<aprende>> a distinguir qué secuencias aparecen mas a menudo y adapta la compresión a ellas. Cuando el tamaño del archivo aumenta, el espacio necesario para almacenarlo crece hacia la entropía del texto.

Fuente: La vida secreta de los números. GEORGE G. SZPIRO, ALMUZARA, 2009 ISBN 9788492573288


Este artículo es mi primera colaboración con la Edición 2.8 del Carnaval de Matemáticas, que en esta ocasión organiza Ciencia Conjunta.


10 comentarios:

  1. Los números parecen vivir a escondidas (pero muy activos) detrás de la pura apariencia que nuestros defectuosos sentidos nos comunican. El "lenguaje matemático", que es una creación humana, muestra sus logros.

    El trabajo interesantísimo que aquí se comenta, encuentra (analizando los resultados de WinZip) lo que hay de similar entre lenguas en principio muy diferentes. Así también constatan el estilo que identifica a un autor literario.

    Es la capacidad del hombre de fugarse desde la entropía (entendida como este universo que no comprendemos, en “desorden”) hacia el orden (el mundo, creado por nosotros mismos, de lo que "comprendemos"). La humana necesidad de certezas.

    ¡Muy interesante!

    ResponderEliminar
  2. Genial artículo. Solo apuntar que la identificación de la autoría de obras, tanto literarias como musicales, a partir de frecuencias y otros análisis estadísticos (y algoritmos informacionales, como el de Lempel-Ziv) no es algo que hayan inventado recientemente.

    ResponderEliminar
  3. Ya es casualidad leer hoy este artículo... precisamente ayer, como pasatiempo escribí un pequeño script python que cuenta las palabras de un fichero de texto. Bajé algunos libros del proyecto Gutenberg en formato texto (como por ejemplo La Regenta, o El Lazarillo) y a contar...

    Por si a alguien (con un conocimiento mínimo de programación) le interesa, ahí va.

    -------------8<--------------------------
    # cuenta_palabras.py
    #
    # lee un fichero de texto nombre_de_fichero.txt
    # y genera un fichero de salida con dos listas:
    # 1) palabras - num.apariciones ordenado por palabras
    # 2) num.apariciones - palabras ordenado por apariciones
    #
    # Pasar como unico argumento el fichero de texto a leer
    #
    # -*- coding: cp1252 -*-

    import sys
    args = sys.argv

    #nombre de archivo
    fn = "la_regenta.txt" # valor por defecto
    if len(args) > 1:
    fn = args[1]

    #abrimos archivo
    f = open(fn, "rt");

    #lo leemos sobre una cadena
    fulltext = f.read();

    #y lo cerramos
    f.close()

    print "leido fichero " + fn + " - longitud = " + str(len(fulltext)) + " bytes."

    #eliminamos signos de puntuacion
    punctuation = ".,:;*_-()¡¿?!'«º»[]{}#"+'"'
    for i in range(len(punctuation)):
    fulltext = fulltext.replace(punctuation[i], ' ')

    #convertimos palabras a minuscula
    fulltext = fulltext.lower()

    #lineas crudas
    lines = fulltext.split("\n")

    print "numero de lineas (incluyendo vacias): " + str(len(lines))

    #lista de palabras
    words = [];

    #recorremos las lineas
    for line in lines:
    #palabras que hay en la linea
    lwords = line.split()
    for word in lwords:
    #cada palabra, la limpiamos y aniadimos a la lista
    word = word.strip(punctuation)
    if word:
    words.append(word)

    print "numero de palabras: " + str(len(words))

    #ordenamos las palabras
    words.sort()

    #vamos a contar las apariciones de palabras mediante un diccionario
    #que mapea palabra -> numero de apariciones
    dict = {}
    keys = []
    for word in words:
    #si la palabra no esta en el diccionario, la aniadimos con valor 1
    if not word in dict:
    dict[word] = 1
    keys.append(word)
    #si esta, incrementamos el valor
    else:
    dict[word] += 1

    #diccionario inversio, numero de apariciones -> lista de palabras que aparecen
    #ese numero de veces
    dict2 = {}
    for key in keys:
    key2 = dict[key]
    if not key2 in dict2:
    dict2[key2] = [key]
    else:
    dict2[key2].append(key)

    #ordenamos las claves (numero de apariciones) para visualizarlas ordenadas
    keys2 = []
    for key2 in dict2:
    keys2.append(key2)
    keys2.sort()

    fn2 = fn + ".wordcount.txt"

    f2 = open(fn2, "wt")

    f2.write("***********************************\n")
    f2.write("***********************************\n")
    f2.write("informe sobre "+fn+"\n")
    f2.write("numero de palabras: " + str(len(words))+"\n")
    f2.write("numero de palabras unicas: " + str(len(keys))+"\n")

    f2.write("***********************************\n")
    f2.write("***********************************\n")

    for key in keys:
    s = "" + key + " : " + str(dict[key]) + '\n'
    f2.write(s)

    f2.write("\n\n\n\n****************\n\n\n\n")

    for key2 in keys2:
    words2 = dict2[key2]
    for word in words2:
    s2 = "" + str(key2) + " : " + word + "\n"
    f2.write(s2)

    f2.close()

    print "numero de palabras distintas: " + str(len(keys))

    print "file " + fn2 + " written."


    Para que el script fun

    ResponderEliminar
  4. Vaya, en los comentarios no aparecen los tabuladores, y en python son necesarios. Vuelvo a pegar el código, cambiando los tabuladores por ·


    # -*- coding: cp1252 -*-
    # cuenta_palabras.py
    #
    # lee un fichero de texto nombre_de_fichero.txt
    # y genera un fichero de salida con dos listas:
    # 1) palabras - num.apariciones ordenado por palabras
    # 2) num.apariciones - palabras ordenado por apariciones
    #
    # Pasar como unico argumento el fichero de texto a leer

    import sys
    args = sys.argv

    #nombre de archivo
    fn = "la_regenta.txt" # valor por defecto
    if len(args) > 1:
    ·fn = args[1]

    #abrimos archivo
    f = open(fn, "rt");

    #lo leemos sobre una cadena
    fulltext = f.read();

    #y lo cerramos
    f.close()

    print "leido fichero " + fn + " - longitud = " + str(len(fulltext)) + " bytes."

    #eliminamos signos de puntuacion
    punctuation = ".,:;*_-()¡¿?!'«º»[]{}#"+'"'
    for i in range(len(punctuation)):
    ·fulltext = fulltext.replace(punctuation[i], ' ')

    #convertimos palabras a minuscula
    fulltext = fulltext.lower()

    #lineas crudas
    lines = fulltext.split("\n")

    print "numero de lineas (incluyendo vacias): " + str(len(lines))

    #lista de palabras
    words = [];

    #recorremos las lineas
    for line in lines:
    ·#palabras que hay en la linea
    ·lwords = line.split()
    ·for word in lwords:
    ··#cada palabra, la limpiamos y aniadimos a la lista
    ··word = word.strip(punctuation)
    ··if word:
    ···words.append(word)
    ····
    print "numero de palabras: " + str(len(words))

    #ordenamos las palabras
    words.sort()

    #vamos a contar las apariciones de palabras mediante un diccionario
    #que mapea palabra -> numero de apariciones
    dict = {}
    keys = []
    for word in words:
    ·#si la palabra no esta en el diccionario, la aniadimos con valor 1
    ·if not word in dict:
    ··dict[word] = 1
    ··keys.append(word)
    ·#si esta, incrementamos el valor
    ·else:
    ··dict[word] += 1

    #diccionario inversio, numero de apariciones -> lista de palabras que aparecen
    #ese numero de veces
    dict2 = {}
    for key in keys:
    ·key2 = dict[key]
    ·if not key2 in dict2:
    ··dict2[key2] = [key]
    ·else:
    ··dict2[key2].append(key)

    #ordenamos las claves (numero de apariciones) para visualizarlas ordenadas
    keys2 = []
    for key2 in dict2:
    ·keys2.append(key2)
    keys2.sort()

    fn2 = fn + ".wordcount.txt"

    f2 = open(fn2, "wt")

    f2.write("***********************************\n")
    f2.write("***********************************\n")
    f2.write("informe sobre "+fn+"\n")
    f2.write("numero de palabras: " + str(len(words))+"\n")
    f2.write("numero de palabras unicas: " + str(len(keys))+"\n")

    f2.write("***********************************\n")
    f2.write("***********************************\n")

    for key in keys:
    ·s = "" + key + " : " + str(dict[key]) + '\n'
    ·f2.write(s)

    f2.write("\n\n\n\n****************\n\n\n\n")

    for key2 in keys2:
    ·words2 = dict2[key2]
    ·for word in words2:
    ··s2 = "" + str(key2) + " : " + word + "\n"
    ··f2.write(s2)
    ·
    f2.close()

    print "numero de palabras distintas: " + str(len(keys))

    print "file " + fn2 + " written."

    ResponderEliminar
  5. Entra y descubre como ganar dinero por lo que haces en internet.
    Hay muchos métodos no te arrepentiras y todo sin invertir NADA! 100% efectivo
    http://adf.ly/3pqsZ

    ResponderEliminar
  6. Genial artículo, nunca lo hubiera imaginado.

    Valora en upnews.es: ¿Es posible conocer la autoría de una obra literaria por las palabras y la frecuencia de uso de éstas que contiene? Esta es una pregu...

    ResponderEliminar
  7. Hombre, antes de decir que no se habían dado cuenta hay que leer complejidad de Kolgomorov y métricas universales
    La justificación de la utilización del zip como máquina de turing .... por ejemplo http://cdsweb.cern.ch/record/1397873

    ResponderEliminar
  8. Buen artículo, también hay algoritmos que calculan la entropia de los textos que calculan la vericidad del texto.

    ResponderEliminar
  9. Hombre, antes de decir que no se habían dado cuenta hay que leer complejidad de Kolgomorov y métricas universales

    ResponderEliminar
  10. Que su método no era tan original? Lo dudo.

    ResponderEliminar

El tema está servido. ¿Ayudas a completarlo con tu punto de vista? por favor, intenta no responder como anónimo, será más fácil para los demás hacer referencia a lo que añadas. Gracias