Publicidad:
Terra
La Coctelera

Categoría: Computación

Procesamiento del lenguaje natural para Abredatos 2010

El fin de semana del 17 y 18 de Abril de 2010 tuvo lugar una de las primeras iniciativas españolas para promover el Open Government en España, el desafío Abredatos 2010.

Tuve el placer de participar en este desafío junto a  mis buenos amigos Javi Santana, Edu Lanchares y Félix López. Como buenos hijos de la diáspora castellano-leonesa por el país intentando buscarse la vida (por lo menos en mi caso) nos apetecía hacer algo cercano a nuestra tierra, así fue como decidimos trabajar con los datos ofrecidos las cortes de Castilla y León.

El problema de estos datos, como el de otros muchos datos públicos, es que la posibilidad de que alguien tenga la intención de reutilizarlos (esa visión de los sistemas de la administración como una plataforma abierta) es, en el mejor de los casos, remota.

De esta manera, nos encontramos con una buena cantidad de datos, no estructurados o semiestructurados, situados en un remoto sitio web y con un formato propietario, más por obligación legislativa, que por una verdadera intención de servicio público.

Procesar estos datos y transformarlos en información es un problema en absoluto trivial.

No existen soluciones algorítmicas que funcionen. La forma más frecuente de afrontar esta dificultad entre los equipos del desafío, así como en los últimos años a raíz de la explosión de la web, consiste en trasladar el problema de las máquinas a las personas, transformando un problema clásico de inteligencia artificial, el procesamiento de texto en lenguaje natural, en un problema de inteligencia colectiva: que sea la multitud de usuarios que visitan esa información los que la filtren.

Nosotros sin embargo, con un claro gusto por lo retro (que podéis esperar de un amante de Lisp), decidimos darle una nueva oportunidad a las técnicas clásicas de inteligencia artificial. El desprestigio de la inteligencia artificial después del conocido como AI Winter es tal, que ahora mismo, nadie que se dedique a la AI puede decirlo si quiere conseguir financiación, así que se utilizan todo tipo de eufemismos para las diferentes parcelas del campo: web semántica, minería de datos, machine learning o procesamiento del lenguaje natural (NLP) que es el que nos interesa.

La idea era utilzar algún tipo de técnica que, dado un volumen de datos textuales fuese capaz de extraer el significado de esos datos y mostrarlos con alguna visualización atractiva en la web.

Afortunadamente, numerosos hombres con barba se han enfrentado a este problema desde los 60 y existen herramientas que nos permiten hacer cosas asombrosas con el texto sin mancharse las manos con matemáticas. Entre ellas está la herramienta que elegimos: GATE un proyecto Java de código abierto que permite construir de forma sencilla sistemas para procesar lenguaje natural. (El proyecto UIMA de Apache también es interesante).

GATE se compone de diferentes componentes que se deben conectar unos con otros en un flujo y que van realizando transformaciones en el texto de entrada.

Nuestro flujo se componía de los siguientes componentes:

- SentenceSplitter: un componente que dado texto de entrada, es capaz de descomponerlo en frases
- DefaultTokeniser: un componente que es capaz de dividir las frases en palabras, teniendo en cuenta guiones y otros accidentes léxicos
- Gazetter: un componente que dado un diccionario de tokens y anotaciones anota todos los tokens producidos por el tokeniser. Utilizamos este componente para marcar palabras sin significado semantico, conocidas como stop words en la jerga del gremio, por ejemplo, preoposiciones, artículos etc.
- TreeTagger: un componente que es capaz de realizar el análisis gramátical de los token, anotándolos con su categoría: verbos, sustantivos, adjetivos, artículos, etc.
- Stemmer: el último componente que dado tokens anotados por el tagger, es capaz de reducir la palabra a su raíz o lexema y forma base o lema. Por ejemplo, para la palabra amaría, la raíz sería am, y la forma base, el infinitvo amar

Al final de este flujo, hemos transformado un montón de texto opaco en una secuencia de palabras con información gramatical. WIN.

El siguiente paso fue agrupar estas palabras con su frecuencia de aparición para pasar a deducir su relevancia. Con un poquito de código Java llevamos a cabo esta labor, aunque tomando algunas decisiones importantes:

- Sólo tendríamos en cuenta sustantivos, adjetivos y verbos que no fuesen stop words
- No contaríamos ocurrencias por su aparición literal, sino que contaríamos como éxito dos palabras con el mismo stem y la misma categoría gramatical.

La siguiente dificultad fue de carácter más práctico, cómo procesar todo el volumen de información a través del sistema construido. Teniendo en cuenta nuestra asombrosa capacidad de cómputo (1 portatil) tuvimos que descartar las herramientas más idóneas y arreglarnoslas poniendo el sistema de procesado en un tomcat con un sencillo servicio web, un script ruby para hacer el scrapping y MongoDB como almacén temporal de datos. Tras conseguir que Java no se comiese toda la memoria disponible por fugas de todo tipo, tuvimos un sistema capaz de procesar una sesión de las cortes en aproximadamente 20 minutos. No es el mejor código que hemos escrito en nuestra vida, pero sirvió para bordear del problema.

Una nueva dificultad apareció debido a las numerosas transformaciones de formato a las que sometimos a los datos: HTML, YAML, documento MongoDB, JSON, tabla SQL, objeto Ruby, pero el verdadero problema surgió cuando visualizamos finalmente los resultados y encontramos que las palabras mas frecuentes eran... totalmente irrelevantes.

Rápidamente nos dimos cuenta, que la relevancia de una palabra desaparecía si por ejemplo se encontraba en todos los documentos indexados (e.g. gracias) o si aparecía con frecuencia muy baja. Con esta idea construimos un algoritmo con dos criterios:

- si una palabra aprecía, en más de N documentos era marcada como irrelevante
- si una palabra aperecía con una frecuencia menor de P era marcada como irrelevante
- El resto de palabras eran marcadas como relevantes.

Jugando con los dos parámetros ajustamos las palabras que se visualizaron finalmente.

Si queréis comprobar la diferencia entre aplicar el algoritmo o no, podéis consumir la API de cortesabiertas.org y comprobar la diferencia entre pedir todas las palabras del corpus:

curl http://cortesabiertas.org/api/words

O pedir las palabras filtradas:

curl http://cortesabiertas.org/api/words?relevant=true

El resultado ya lo conocéis. No es una maravilla, pero con los recursos que teníamos y nuestras carencias estamos bastante contentos. Hay muchas ideas que no nos dio tiempo a implementar, la más interesantes surgen del hecho de que dado el corpus global de palabras, y un objeto: sesión, intervención o procurador, se puede asignar un vector binario o numérico a ese objeto, en función de si dijo esas palabra o cuantas veces la dijo. Esto abre la puerta a utilizar numerosas técnicas de minería de datos sobre los objetos, clasificación o clusterización. Por ejemplo, clusterizar los procuradores por su léxico y ver si coinciden con sus agrupaciones políticas.

En cualquier caso, creemos que sería una herramienta interesante para este tipo de proyectos, y nos gustaría arreglar un poco el código y empaquetarlo de forma que fuese fácilmente usable para otras ideas. Mientras tanto podéis echarle un vistazo al código en github (¡mejoradlo por favo!r)

Haml en Clojure

Acabo de subir a Github clj-haml, un apaño para tener algo parecido a Haml (http://haml.hamptoncatlin.com/) para clojure.


Haml:

%div#things
     %span#rice Chicken Fried
     %p.beans{ :food => 'true' } The magical fruit
     %h1.class.otherclass#id La La La
 

clj-haml:

(h= :div#things
              (h= :span#rice "Chicken Fried")
              (h= :p.beans {:food "true"} "The magical fruit")
              (h= :h1.class.otherclass#id "La La La"))
 

El HTML generado en ambos casos:

<div id='things'>
   <span id='rice'>Chicken Fried</span>
   <p class='beans' food='true'>The magical fruit</p>
   <h1 id='id' class='class otherclass'>La La La</h1>
  </div>

Sólo hay una función:

(h= :selector attributes contents /)
 

Todos los parámetros son opcionales, :selector es un keyword que puede incluir el nombre de una etiqueta HTML seguido de un id y algunas clases especificadas siguiendo la notación CSS: :p#myid.class1.clas2, :.class1#myid, :br

Attributes es un mapa asociativo con pares clave valor para nombre de atributo y valor, por ejemplo: {:href "/test.html"}.

Contents son unos cuantos forms, potencialmente conteniendo otras llamadas (h=) que se evaluarán y cuyo valor devuelto se insertará en la etiqueta que se está generando.

Por último, el / final opcional, si presente, hace que la etiqueta generada sea con autocierre (h= :br /) -> <br/>, (h= :br) -> <br></br>.

Como en Haml vamos.

La podéis descargar aquí (http://github.com/antoniogarrote/clj-haml/tree/master).

El núcleo (mónadico) de un framework web clojure

En las últimas semanas, mi pequeña biblioteca clojure para grizzly ha ido creciendo y ya puede aceptar cualquier petición web y renderizar una respuesta.

Desde el punto de vista de cliente, el proceso es ahora mismo este:

Enrutamos una petición a una función que procesará la petición:

  (route! (url-pattern GET "greetings" :name)
             say-hello)
 

Con la anterior expresión estamos enrutando las peticiones "GET /greetings/PARAMETRO" a la función say-hello, que además recibirá el valor de PARAMETRO con el nombre :name.

Definamos ahora la función say-hello:

 (defn say-hello
   ([] (render (if (= (parameter :name) "Helena")
                        "Hola chica guapa!"
                        (str "Hola " (parameter :name)))
                    200
                    {:Content-Type "text/plain"})))
 

En esta función básicamente decimos que queremos renderizar un mensaje u otro en función del valor del parámetro :name de la URL.


Hasta aquí todo sencillo, pero lo interesante es que hay debajo de todo esto ¿qué hacen las llamadas a render y parameter? Bien, la respuesta es que toda la lógica de servicio de una petición se encuentra encerrado dentro de una mónada, la Mónada WebIO. Esta mónada se define como:

 ;;
 ;; WebIO X = WebIOUnfinished X |
 ;;           WebIOFinished X
 ;;           WebIOtFailed
 ;;
 
 (defstruct web-request-processing :environment ;; The Rack request
                                   :response ;; The response of the request
                                   :parameters) ;; the parameters hash
 

La petición, la respuesta y los parámetros se guardan en la mónada que puede tener tres estados pendiente, terminada o fallida que establecen el estado de la respuesta.

La función de binding para la mónada WebIO, define como se deben combinar funciones monádicas según el resultado de aplicar la petición contenida en la mónda:

 (defmethod >>= :WebIO [f m]
   "Instance of the >>= function for the WebIO monad. If the computation
    result of m is WebIO Unfinished, the next function f is applied to the
    content of the m. If the result of m is WebIO Failed, next function
    f is not applied and m is returned. If the result of m is WebIO
    Finished, f is not invoked"
   (if (or (web-io-failed? m) (web-io-finished? m))
     m
     (f (:content m))))
 

Para poder realizar el procesamiento de la petición sólo se necesitan dos cosas una función que transforme una petición web, a través de la interfaz Rack que usamos, en una mónada WebIO y algún método para transformar una función normal como la anterior usada como ejemplo (say-hello), que no sabe nada sobre mónada alguna en una función monádica capaz de ser procesada por el operador de binding (>>=).

De lo primero se encarga la función (wrap-request) que usa la función general monádica (return) para envolver una petición Rack, además de protegerla con una referencia para poder ser usada facilmente mediante el mecanismo de memoria transaccional software (STM) que implementa Clojure:

 (defn wrap-request
   "Wrapper around return that builds the new request monad initializing it
    with the values of the rack-request"
   {:monad :WebIO}
   ([rack-request rack-response parameters subtype]
      (return :WebIO subtype
            (struct web-request-processing
                    rack-request
                    (if (= (class rack-response) clojure.lang.Ref)
                      rack-response
                      (ref rack-response))
                    parameters)))
   ([rack-request]
     (wrap-request rack-request (create-rack-response) {} :Unfinished))
   ([rack-request rack-response]
     (wrap-request rack-request rack-response {} :Unfinished))
   ([rack-request rack-response parameters]
     (wrap-request rack-request rack-response parameters :Unfinished)))
 

El segúndo requisito, transformar una función normal en una función monádica, lo realiza la función (with-web-io) y se conoce normalmente como "lifting". En este caso además, define unas variables accesibles sólo por el hilo que ejecuta esa función, para la petición, respuesta y parámetros, de forma que no sea necesario para el programador que implementa la función con la lógica de procesamiento, tener que pasar explícitamente estos parámetros a funciones como (parameter) o (render):

 (defn with-web-io
   "Lifts a call to a function for a web-io-processing struct into the web-io monad"
   {:monad :WebIO}
   ([f web-io-processing]
      (try
       (let [result (binding [*request* (:environment web-io-processing)
                              *response* (:response web-io-processing)
                              *parameters* (:parameters web-io-processing)]
                      (apply f []))]
         (if (web-io-monad? result)
           result
           (wrap-request (:environment web-io-processing)
                         (:response web-io-processing)
                         (:parameters web-io-processing)
                         :Unfinished)))
       (catch Exception ex (raise (:environment web-io-processing)
                                  500
                                  (. ex getMessage))))))
 

Sobre estas funciones, el mecanismo que sirve una petición resulta trivial, el proceso cuenta con los siguientes pasos:

  • Recibir la petición Rack
  • Extraer la ruta y método de la petición y comprobar si se puede enrutar a alguna función de respuesta
  • Transformar la petición Rack en una mónada WebIO
  • Crear una colección con los filtros anteriores, la función de respuesta, y los filtros posteriores a los que se va a enrutar la petición
  • Hacer un lift de cada una de estas funciones a funciones monádicas a través de (with-web-io)
  • Ejecutar la secuencia de funciones monádicas con el operador de binding (>>=)
  • Devolver el resultado a través de Rack

Programación concurrente en Clojure: agentes

Clojure es un lenguaje de programación pensado por y para la concurrencia. Las características funcionales del lenguaje, como la inmutabilidad de sus estructuras de datos, contribuyen a que mantener varios hilos de ejecución sea sencillo, cada uno con su propia copia inmutable de los datos, ya que de esta manera, es imposible que un hilo modifique el estado de la computación de otro hilo.

Sin embargo, hay veces que es necesario compartir el estado entre varios hilos o, dado que Clojure se ejecuta sobre la máquina virtual de Java y es posible acceder a todos los objetos y clases de Java, trabajamos con estructuras de datos no inmutables.

Clojure ofrece diferentes formas para tratar estas situaciones automáticamente, de forma que se eliminen los problemas de concurrencia que aparecen en estas circunstancias, los agentes son una de ellas.

Un agente es una abstracción que mantiene el estado de una computación, actualizándolo mediante la ejecución de forma asíncrona de funciones sobre ese estado que son enviadas por otros hilos mediante la función (send). Estas funciones se van almacenando en una estructura FIFO y van siendo aplicadas secuencialmente por el agente en su propio pool de hilos.
Si habéis trabajado con Erlang, podéis pensar en los agentes Clojure como una versión recortada de los servidores Erlang.

Un agente clojure se declara con la función (agent):

 (agent initial-state)
 

Donde initial-state puede ser cualquier estructura de datos. Por ejemplo, vamos a construir un logger asíncrono y thread-safe. Nuestro logger mantendrá pues un nivel de logging y un sitio donde escribir el log, por defecto le daremos el nivel "1" y la salida estándar:

(def *logger* (agent {:level 1 :output *out*}))
 

Para loguear los mensajes, necesitamos una función (log), que recibirá un nivel de logging y algo que loguear, como usar números para los niveles de log queda algo feo, definamos una función que transforma claves para los niveles de log en enteros:

 (defn level-name-to-int
   "Returns a numeric identifier for the levels of the logger"
   ([level]
      (let [levels {:debug 0
                    :info 1
                    :warning 2
                    :error 3
                    :critical 4}]
        (let [to-return (level levels)]
          (if (nil? to-return) 6 to-return)))))
 

Ahora que tenemos niveles de log, necesitamos alguna forma de cambiar el nivel de logging actual almacenado en el agente del logger. Como ya mencionamos, para cambiar el estado de un agente, necesitamos una función que será enviada al agente para que la ejecute en su pool de hilos sobre el estado actual almacenado. La función de actualización debe recibir como primer argumento el estado del agente, una serie de parámetros opcionales y devolver un estado del agente modificado. Usando notación Haskell:

 updateFunction :: AgentState -> [Args] -> AgentState
 

En nuestro caso, nuestra función para modificar el nivel de logging del agente puede ser:

 (defn reset-logger-level
   "Initializes the logger with a given level"
   ([level]
      (send *logger* (fn [a l] {:level (level-name-to-int l) :output (:output a)}) level)))
 

Como se puede ver, enviamos al agente *logger* una función lambda, donde se transforma la estructura de dats almacenada en el agente, sustituyendo el viejo nivel de logeo con el suministrado en el argumento "level".

Ahora, necesitamos la función de logueo. Crearemos dos versiones, una que recibe un nivel y el código a loguear, y si el nivel del agente logger es adecuado loguea el resultado del código pasado a la función de log y lo devuelve tal cual. La segunda, recibirá además una descripción que logueará junto al resultado de ejecutar el códigos suministrado como argumento:

 (defn log
   "logs something in the logger with the provided level of log"
   ([level to-log]
      (do
        (send *logger* (fn [a level msg]
                         (do
                           (when (<= (:level a)
                                     (level-name-to-int level))
                             (let [to-write (str (. (keyword-to-string level) toUpperCase) " "
                                                 (. (new java.util.Date) toString) " => "(str msg) "\n")
                                   writer (:output a)]
                               (do
                                 (. writer (write to-write 0 (. to-write length)))
                                 (. writer flush))))
                           a))
              level to-log)
        to-log))
   ([level desc to-log]
      (do
        (send *logger* (fn [a level msg]
                         (do
                           (when (<= (:level a)
                                     (level-name-to-int level))
                             (let [to-write (str (. (keyword-to-string level) toUpperCase) " "
                                                 (. (new java.util.Date) toString) " => " desc " " (str msg) "\n")
                                   writer (:output a)]
                               (do
                                 (. writer (write to-write 0 (. to-write length)))
                                 (. writer flush))))
                           a))
              level to-log)
        to-log)))
 

En este caso, como en el anterior, enviamos una función lambda al agente con send para que haga el trabajo, esta función loguea, el resultado de la ejecución comprobando el nivel de log y añadiendo alguna información extra como la hora.

Es interesante notar que la función log no es bloqueante, en cuanto se ha enviado la función lambda al agente, retorna y la ejecución de la computación continúa, se haya logueado el mensaje o no, además como la ejecución de funciones en el pool del agente es FIFO, los mensajes se van a loguear en el orden de ejecución, sin que se "cuelen" mensajes en el orden de ejecución (la función send es atómica).

Aquí se puede comprobar como funciona en ejecución nuestro flamante logger:

 user=> (reset-logger-level :debug)
 (reset-logger-level :debug)
 #=(clojure.lang.Agent. "clojure.lang.Agent@219009")
 
 user=> (log :info "hola?" (+ 1 2))
 (log :info "hola?" (+ 1 2))
 3
 
 user=> INFO Mon Jan 12 00:25:54 CET 2009 => hola? 3
 (reset-logger-level :error)
 (reset-logger-level :error)
 #=(clojure.lang.Agent. "clojure.lang.Agent@219009")
 
 user=> (log :info "ahora no... " (* 3 3))
 (log :info "ahora no... " (* 3 3))
 9
 user=>  
 

Como se puede ver, se puede meter llamadas a log en medio de la ejecución del código ya que es transparente para una llamada a función, además se puede comprobarla ejecución asíncrona del logger, comprobando como se cuela el logueo de la llamada para (log :info "hola?" (+ 1 2)) antes de que la shell de clojure muestre el cursor para la siguiente entrada de datos por parte del usuario.

Para terminar, vamos a hacer que en vez de loguear en la salida estándar, mande el log a un fichero. Como ya hemos dicho, Clojure es 100% java, y la variable especial de salida estándar *out*, no es más que un objeto java.io.OutputStreamWriter, así que vamos a hacer un par de funciones, una para cambiar el destino de logueo almacenado en el estado del agente, y otro para poder iniciarlo con un fichero de salida:

 (defn reset-logger
   "Initializes the logger with a given level and output writer"
   ([level writer]
      (send *logger* (fn [a l w] {:level (level-name-to-int l) :output w}) level writer)))
 
 (defn reset-logger-with-file-output
   "Resets the logger setting the output to the file whose path is provided as an argument
    with the level specified"
   ([level file-path]
      (reset-logger level (new FileWriter (new File file-path) true))))
 

Ya tenemos terminado nuestro logger, de ultimísima tecnología, con el que no tendremos problemas como el que te puedes encontrar en Merb, cuando el tiempo que tarda en mostrar el log de tu aplicación web acaba provocando un timeout por parte del cliente. ;)

Por último, queda comentar sobre los agentes que les ocurre cuando la función de actualización lanza una excepción: intentemos loguear en null, algo que seguro no es una buena idea:

 (send *logger* (fn [_] {:output nil :level 1}))
 (send *logger* (fn [_] {:output nil :level 1}))
 #=(clojure.lang.Agent. "clojure.lang.Agent@219009")
 user=> 
 (log :error "????")
 "????"
 

El mensaje no ha sido loguead, como esperábamos, pero tampoco parece que haya explotado nada, intentemos loguear algo más

 user=> (log :error "are you alive?")
  (log :error "are you alive?")
  java.lang.Exception: Agent has errors (NO_SOURCE_FILE:0)
  user=> 
 

Vaya, parece que nuestro agente se ha quedado en un estado de error. Este es el comportamiento de los agentes Clojure ante los errores, se quedan suspendidos hasta que reseteemos el estado del mismo con la función (clear-agent-errors)

 user=> (clear-agent-errors *logger*)
 (clear-agent-errors *logger*)
 nil
 

¡Listo para usar otra vez!

El código, como siempre, lo podéis descargar de Github

Creando individuos en la ABox: validaciones en una semántica de mundo abierto

Más cosas que voy implementando en mi biblioteca OWL para Clojure.

En otro post, mostré como se podía definir la TBox, con un procedimiento similar al de usar migraciones para definir el esquema de una base de datos relacional. Ahora vamos mostrar lo que sería el equivalente ontológico a insertar filas en las tablas del esquema relacional, es decir a insertar aserciones en la ABox.

Creamos una TBox de prueba:

 (describe-tbox
   (describe-owl-class :test "Person")
   (describe-owl-datatype-property :test "name" (xsd-string))
   (describe-owl-datatype-property :test "age" (xsd-decimal))
   (describe-owl-class-has-property :test "Person" :test "name")
   (describe-owl-class-has-property :test "Person" :test "age"))
 

Una vez guardada la TBox en un repositorio e iniciada (con alias :test-Person, :test-name y :test-age para los recursos), podremos crear individuos en la ABox que sean tipos de las clases OWL contenidas en la TBox, para ello usamos la función (abox-create-individual)

 (abox-create-individual :test-Person
                         "http://test.com/abox/people#"
                         { :test-name "Helena",
                           :test-age 24 })
 

Esta función crea un nuevo individuo con URI "http://test.com/abox/people#IDENTIFIER", donde IDENTIFIER es un identificador universal único (UUID) que se genera con la llamada a la función. La función inserta los tripletes RDF necesarios para asertar que el individuo recién creado es del tipo :test-Person, así como de todas sus superclases (en este caso owl:Thing). Además da valores a las propiedaded test:name y test:age.

Los tripletes RDF equivalentes serían (con notación Turtle):

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
 @prefix test: <http://test.com/tbox/people#> .
 @prefix owl: <http://www.w3.org/2002/07/owl#> .
 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 
 <"http://test.com/abox/people#IDENTIFIER"> rdf:type test:Person ;
                                            rdf:type owl:Thing ; 
                                            test:age "24"^^xsd:decimal ;
                                            test:name "Helena"^^xsd:string .
 

A la hora de crear individuos en la ABox, hay que tener en cuenta que se pueden crear individuos con valores sólo para algunas propiedades, o con propiedades no definidas en la clase OWL o ni siquiera en la TBox. Una de las características de las lógicas descriptivas, la base formal de OWL, es que tienen una semántica denominada de "mundo abierto" (ver el "Cambridge Handbook of DL"). Siguiendo una semántica "open world", si una información no se ha asertado en la ontología, no quiere decir que no exista, sino que todavía no se conoce. Lo mismo sucede para la información extra. Es interesante comparar esta semántica con una semántica de "mundo cerrado", como la que siguen las bases de datos relacionales, donde si un dato no está en la tabla, significa un error, no desconocimiento. Esta situación se agrava cuando contamos con pocos constructores OWL y no hay soporte para un razonador, si no que se trabaja solo con las aserciones explícitas de la ontología.

Una forma de poner restricciones a lo que insertamos en la ABox, sin recurrir a hacer un test de consistencia con un reasoner tras realizar las aserciones, es permitir añadir validaciones a los recursos descritos en la TBox. Estas validaciones se realizan cuando se quiere hacer una aserción en la ABox para ver si la aserción está permitida o no, controlando qué es válido y qué no es válido asertar. Aún así, esta solución no deja de ser una pequeña chapuza, ya que perdemos la información de las validaciones que no está contenida en la ontología, por lo que siempre que sea posible, se debe modelar correctamente la ontología, insertando la información posible y después añadir las validaciones.

Estas validaciones, son predicados y se pueden realizar en el registro de la TBox. y pueden ir asociadas a cualquier acción y recurso. Por ejemplo:

 (tbox-register-name! :owl-thing (owl-Thing) :connection)
 (tbox-register-name! :test-Person :test "Person"  :connection)
 (tbox-register-name! :test-age :test "age"  :connection)
 (tbox-register-name! :test-name :test "name"  :connection)
 
 (tbox-register-validation-on! :create 
                               :test-age 
                               (fn [age]
                                   (> (:value age) 18)))
 

En este caso, comprobamos que al crear un nuevo individuo que da valor a la propiedad test:age, el valor de la edad es mayor que 18. En este caso la validación es para un propiedad, y se comprobaría cuando se realizase una acción sobre cualquier clase con esa propiedad, de esta manera se pueden reutilizar las validaciones entre diferentes clases. Cuando la validación es a nivel de clase, pero no común para las propiedad compartida entre clases, se puede registrar la validación a nivel de clase:

 (tbox-register-name! :test-AdultPerson :test "AdultPerson" :connection)
 (tbox-register-validation-on! :create
                               :test-AdultPerson
                               (fn [properties] 
                                   (> (:value (:test-age properties)) 18)))
 

Así se comprobaría el predicado cuando se crea un individuo de la clase test:AdultPerson, pero no cuando ce crea cualquier otro individuo de una clase relacionada con la propiedad test:age.

Con las validaciones se pueden ofrecer una semántica para casi todo: propiedades obligatorias, formato correcto de valores ... siempre que no sean excusa para perder información que podría haberse asertado algún constructor de OWL, como allValuesFrom, etc.

Una capa de persistencia de datos semántica basada en OWL

Hoy en día la norma para desarrollar la capa de persistencia de una aplicación web consiste en desarrollar un esquema para algún sistema de base de datos relacional en el que almacenar los datos.
Esto supone básicamente darle golpes a tus objetos hasta convertirlos en conjuntos de enteros, cadenas, etc que después se vuelven a encajar en tablas, indicando relaciones entre esas tablas duplicando valores en columnas especiales de esas tablas. Estamos tan acostumbrados a hacerlo de esta manera que ni nos damos cuenta de la gran distancia que media entre nuestro modelo objetual, con el que trabajamos en nuestro código, y el nivel de abstracción mucho más bajo con el que almacenamos una representación persistente de esos datos.

Sin embargo, esto no tendría porque ser así. Cuando creamos y modelamos nuestro dominio de datos para la aplicación, no hacemos más que crear una ontología, una descripción de los recursos del dominio y sus relaciones, y en los últimos años se ha estandarizado un lenguaje para la descripción de ontologías por parte de la W3C: OWL.

OWL nace del mundo de la lógica descriptiva y permite describir las dos partes que componen una ontología:

  • Una TBox: o terminological box, donde describimos una terminología, los tipos de recursos y relaciones, por ejemplo, existen cosas llamadas personas, existe una relación entre una cosa y un número llamada tener edad, etc...
  • Una ABox: o assertional box, donde hacemos aserciones usando los términos de la TBox, por ejemplo: Juan es una persona, Juan tiene una edad de 15, etc...

A continuación mostraré una biblioteca para describir una TBox en Clojure lisp.

Nuestra primera tarea es tener un sitio donde almacenar la TBox, para ello debemos tener una conexión con un repositorio. Con el siguiente código creamos un repositorio en memoria y lo registramos con un nombre para usarlo posteriormente:

  (register-repository! :test (init-memory-repository!))
 

A continuación podemos pasar a describir nuestra TBox para la ontología que vamos a describir, la famosa aplicación para escribir blogs, para ello usamos las funciones (describe-owl-*):

 (write-graph-in-repository
    (describe-tbox
       ;; algunas propiedades de datos
       (describe-owl-datatype-property :superblog "title" (xsd-string))
       (describe-owl-datatype-property :superblog "content" (xsd-string))
       (describe-owl-datatype-property :superblog "name" (xsd-string))
                 
       ;; algunas clases
       (describe-owl-class :superblog "Blog")
       (describe-owl-class :superblog "Post")
       (describe-owl-class :superblog "Author")
                
       ;; asignamos propiedades a clases
       (describe-owl-class-has-property :superblog "Blog" :superblog "title")
       (describe-owl-class-has-property :superblog "Post" :superblog "title")
       (describe-owl-class-has-property :superblog "Post" :superblog "content")
       (describe-owl-class-has-property :superblog "Author" :superblog "name")
 
       ;; algunas propiedades de objetos
       (describe-owl-object-property :superblog "contains")
       (describe-owl-object-property :superblog "writes")
 
       ;; relaciones entre clases
       (describe-owl-class-has-property :superblog "Blog" :superblog "contains")
       (describe-owl-property-range :superblog "contains" :superblog "Post")
       (describe-owl-class-has-property :superblog "Author" :superblog "writes")
       (describe-owl-property-range :superblog "writes" :superblog "Post"))
 
    ;; seleccionamos la conexion con el repositorio anteriormente registrado
    (connection! :test))
 

El código anterior es equivalente a escribir una migración en Ruby on Rails, pero el hecho de usar OWL en vez de directamente una base de datos relacional, nos aporta una serie de beneficios, por ejemplo, la reutilización del esfuerzo empleado en modelar el dominio. En el ejemplo anterior podemos darnos cuenta de que las relaciones como título, ser autor de, etc, ya han sido modelados y estandarizados dentro del Dublin Core Metadata Initiative (http://en.wikipedia.org/wiki/Dublin_Core), ¿por qué no utilizar el soporte para el Dublin Core que incluye la biblioteca? La reutilización se basa en el concepto de kits de vocabularios, que incluyen clases y propiedades y funciones que trabajan sobre estas partículas OWL:

 (use 'com.agh.webserver.framework.persistence.rdf.vocabularies.dc)
 
 ;; cargamos la descripción del Dublin Core en el repositorio de test
 (import-vocabulary-kit :dc (connection! :test)) 
 
 ;; buscamos sinonimos entre nuestro dominio y el DC
 (write-graph-in-repository
    (describe-tbox
       (describe-owl-equivalent-properties :superblog "title" (dc-title))
       (describe-owl-equivalent-properties :superblog "writes" (dc-creator)))
    (connection! :test))
 

La descripción de la TBox de la ontología está en este momento almacenada en el repositorio, para poder usar la TBox de forma eficiente debemos iniciar la TBox al empezar a usar nuestra aplicación declarando que recursos de la ontología almacenada en el repositorio queremos utilizar:

   ;; iniciamos la TBox
  (tbox-register-name! :owl-thing (owl-Thing) :test)
  (tbox-register-name! :superblog-Post "http://superblog.com/Post" :test)
  (tbox-register-name! :superblog-Blog "http://superblog.com/Blog" :test)
  (tbox-register-name! :superblog-Author "http://superblog.com/Author" :test)
  (tbox-register-name! :superblog-content "http://superblog.com/content" :test)
  (tbox-register-name! :superblog-title "http://superblog.com/title" :test)
  (tbox-register-name! :superblog-name "http://superblog.com/name" :test)
  (tbox-register-name! :superblog-post "http://superblog.com/contains" :test)
  (tbox-register-name! :superblog-post "http://superblog.com/writes" :test)
 
  ;; iniciamos el kit de Dublin Core con los nombre por defecto que asigna la 
  :: biblioteca
  (tbox-register-vocabulary-dc :test)
 
   ;; el codigo de la aplicacion empezaria aqui
 

Ahora ya podríamos acceder a los recursos mediante su URI o mediante el nombre con el que se ha registrado. Un test que muestra la interacción entre las diferentes partes, además de la inferencia entre propiedades y subclases, podría ser:

 (deftest test-class-subclassing
   (let [orig-tbox @*tbox*
         orig-conn @*connections*
         orig-repos @*repositories-registry*
         repo (init-memory-repository!)
         graph (describe-tbox
                (describe-owl-datatype-property "http://test.com/prop_a" (xsd-float))
                (describe-owl-datatype-property "http://test.com/prop_b" (xsd-float))
                (describe-owl-class "http://test.com/class_a")
                (describe-owl-class "http://test.com/class_b")
                (describe-owl-class "http://test.com/class_c")
                (describe-owl-subclass "http://test.com/class_a" "http://test.com/class_b")
                (describe-owl-subclass "http://test.com/class_a" "http://test.com/class_c")
                (describe-owl-class-has-property "http://test.com/class_a" "http://test.com/prop_a")
                (describe-owl-class-has-property "http://test.com/class_b" "http://test.com/prop_b"))]
     (do (tbox-clear!)
         (register-repository! :test repo)
         (write-graph-in-repository graph (connection! :test))
         (tbox-register-name! :owl-thing (owl-Thing) :test)
         (tbox-register-name! :class_a "http://test.com/class_a" :test)
         (tbox-register-name! :class_b "http://test.com/class_b" :test)
         (tbox-register-name! :class_c "http://test.com/class_c" :test)
         (tbox-register-name! :prop_a "http://test.com/prop_a" :test)
         (tbox-register-name! :prop_b "http://test.com/prop_b" :test)
         (let [class-recovered (tbox-find-class-by-uri! "http://test.com/class_a")]
           (do 
               (is (= class-recovered
                      {:name :class_a
                       :uri "http://test.com/class_a"
                       :subclass-of [{:name :class_a
                                      :uri {:prefix "", :value "http://www.w3.org/2002/07/owl#Thing"}
                                      :subclass-of []
                                      :datatype-properties #{}
                                      :object-properties #{}
                                      :repository-name :test}
                                     {:name :class_b
                                      :uri {:prefix "", :value "http://test.com/class_b"}
                                      :subclass-of [{:name :owl-Thing
                                                     :uri {:prefix "", :value "http://www.w3.org/2002/07/owl#Thing"}
                                                     :subclass-of []
                                                     :datatype-properties #{}
                                                     :object-properties #{}
                                                     :repository-name :test}]
                                      :datatype-properties #{{:name nil
                                                              :uri
                                                              {:prefix "", :value "http://test.com/prop_b"}
                                                              :range {:prefix "", :value "http://www.w3.org/2001/XMLSchema#float"}
                                                              :equivalent-properties #{}
                                                              :repository-name :test}}
                                      :object-properties #{}
                                      :repository-name :test}
                                     {:name :class_c
                                      :uri {:prefix "", :value "http://test.com/class_c"}
                                      :subclass-of [{:name :owl-Thing
                                                     :uri {:prefix "", :value "http://www.w3.org/2002/07/owl#Thing"}
                                                     :subclass-of []
                                                     :datatype-properties #{}
                                                     :object-properties #{}
                                                     :repository-name :test}]
                                      :datatype-properties #{}
                                      :object-properties #{}
                                      :repository-name :test}]
                       :datatype-properties #{{:name :prop_b
                                               :uri {:prefix "", :value "http://test.com/prop_b"}
                                               :range {:prefix "", :value "http://www.w3.org/2001/XMLSchema#float"}
                                               :equivalent-properties #{}
                                               :repository-name :test}
                                              {:name :prop_a
                                               :uri
                                               {:prefix "", :value "http://test.com/prop_a"}
                                               :range {:prefix "", :value "http://www.w3.org/2001/XMLSchema#float"}
                                               :equivalent-properties #{}
                                               :repository-name :test}}
                       :object-properties #{}
                       :repository-name :test}
               (tbox-restore! orig-tbox)
               (repositories-register-restore! orig-repos)
               (connections-restore! orig-conn))))))
 

Al igual que OWL se situa sobre una pila de protocolos de la W3C para la web semántica, en concreto, una ontología OWL se codifica como un grafo RDF que a su vez se puede describir como un documento XML, la biblioteca OWL usa la bibliteca RDF que comenté anteriormente para describir los recursos en términos de constructores de un grafo RDF y recupera su información transformando ese grafo en consultas SPARQL.

En siguientes posts, veremos como usar esta TBox para hacer aserciones sobre objetos concretos dentro de la ABox.

Aunque todavía se encuentra muy lejos de ser usable para nada, si queréis hacer pruebas y juguetear con él, el código se encuentra disponible en mi repositorio de Github.

Una biblioteca RDF para Clojure

El código y los tests los podéis encontrar en mi repositorio de Github.
Usando Sesame como repositorio semántico. Describimos un grafo RDF:

 (def  *graph*
   (build-graph
     [(build-uri-node "http://test.com/whatever"
         [(build-relation :rdf "relation-1" (build-literal-node "testa1"))
          (build-relation :rdf "relation-2" (build-literal-node "testa2"))])
      (build-uri-node :rdf "test-subjectb"
         [(build-relation :rdf "relation-2" (build-blank-node "a" []))])])
 

El grafo que acabamos de describir es quivalente a los siguientes tripletes RDF (con notación turtle):

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
 <http://test.com/whatever> rdf:relation-1 "testa1" .
 <http://test.com/whatever> rdf:relation-2 "testa2" .
 rdf:test-subjectb rdf:relation-2 _:a .
 

El grafo que hemos guardado en *graph* podemos manipularlo en memoria, pero para almacenarlo, debemos obtener una conexión con un repositorio. Supongamos que tenemos una base de datos llamada "clojure_sesame", podemos usarla para almacenar el grafo en esa base de datos:

 (def *connection*
      (. (init-repository "com.mysql.jdbc.Driver" 
                          "jdbc:mysql://localhost:3306/clojure_sesame" 
                          "root" 
                          "root")
          getConnection)
 (write-graph-in-repository *graph* *connection*)
 

Una funcionalidad interesante es consultar un grafo RDF de una forma similar al uso que se hace de SQL para consultar un base de datos relacional.
Para dar solución a esta necesidad se ha estandarizado SPARQL (SPARQL Protocol and RDF Query Language) por parte de la W3C (http://www.w3.org/TR/rdf-sparql-query/).

La biblioteca ofrece esta funcionalidad mediante la construcción de plantillas: grafos RDF, donde se pueden insertar constructores para nodos variables y para relaciones variables:

 (def  *template*
   (build-graph-template 
      [{:template (build-graph
                            [(build-variable-node :x
                                 [(build-variable-relation :y 
                                      (build-variable-node :z []))])])
        :filters []}])]
 

La función (build-graph-template) acepta mapas con dos claves, :template que debe contener como valor un grafo con partes variables, y :filters, una colección de filtros que se pueden aplicar a los nodos variables. Por ejemplo:

 (build-filter :x "?x < 25")
 (build-filter :y "isURI(?y)")
 

También se pueden describir partes opcionales de la plantilla con la función (build-optional-graph).

Una vez que tenemos la plantilla debemos seleccionar que variables queremos que la consulta nos devuelva. En el ejemplo anterior, tenemos una plantilla con tres variables :x :y :z, si sólo nos interesan las variables :x e :y, construimos la consulta de la siguiente manera:

 (def *result*
   (query-template-in-repository
                              :x :y
                              *template*
                              *connection*)
 

La invocación anterior es equivalente a realizar la siguiente consulta SPARQL:

 SELECT ?x ?y WHERE { ?x ?y ?z .}
 

Los resultados se devuelven como colecciones de mapas, donde cada mapa contiene una solución para :x e :y:

 ;; El resultado de la consulta
  [{:y {:prefix "", :value "http://www.w3.org/1999/02/22-rdf-syntax-ns#relation-2"}
    :x {:prefix "", :value "http://test.com/test-subjecta/whatever"}}
   {:y {:prefix "", :value "http://www.w3.org/1999/02/22-rdf-syntax-ns#relation-1"}
    :x {:prefix "", :value "http://test.com/test-subjecta/whatever"}}
   {:y {:prefix "", :value "http://www.w3.org/1999/02/22-rdf-syntax-ns#relation-2"}
    :x {:prefix "", :value "http://www.w3.org/1999/02/22-rdf-syntax-ns#test-subjectb"}}]
 

Las plantillas no solo describen patrones para consultas SPARQL, sino que se pueden sustituir las partes variables de la plantilla, si queremos transformarla en un grafo de nuevo. Usando el primer resultado de la consulta, podemos hacer:

 (template-to-graph *template* (first *result*))
 

Y obtendríamos el grafo equivalente resolviendo :x e :y (en este caso todavía quedaría la parte variable referente a :z).

También podemos usar la capacidad de sustitución de las plantillas para dar valores parciales a variables al hacer una consulta:

 (query-template-in-repository
                            :x :y {:z (build-literal-node "testa1")}
                            *template*
                            *connection*)
 

De esta manera obtendríamos una consulta equivalente a:

 SELECT ?x ?y 
 WHERE { 
   ?x ?y "testa1" .
 }
 

Esta biblioteca, ofrece un acceso a RDF de bastante bajo nivel, manipulando tripletes de forma individual, pero puede servir de base para construir una capa de persistencia semántica para aplicaciones web, quizá construida sobre OWL, el lenguaje de descripción de ontologías de la W3C.

busque, compare y si encuentra algo mejor...

Un post rápido, comparando implementaciones de un algoritmo en su aproximación funcional e imperativa.

Cuando empezamos a programar, normalmente lo hacemos en un lenguaje imperativo, y el primer programa que tecleamos es nuestro querido "hola mundo". Bien, en el mundo funcional existe un equivalente al hola mundo que suele ser escribir una función que genere la serie de fibonacci, pero creo que será más interesante algo un poquito más complicado, usemos el quicksort:

1-pseudocódigo

 function quicksort(array)
      var list less, greater
      if length(array) ≤ 1  
          return array  
      select and remove a pivot value pivot from array
      for each x in array
          if x ≤ pivot then append x to less
          else append x to greater
      return concatenate(quicksort(less), pivot, quicksort(greater))
 

2.- C

 void Quicksort(int[] v, int prim, int ult) {
         if (prim < ult) {
                 int p = Pivot(v, prim, ult, ult);
 
                 Quicksort(v, prim, p - 1);
                 Quicksort(v, p + 1, ult);
         }
 }
  
 int Pivot(int[] v, int prim, int ult, int piv) {
         int p = v[piv];
         int j = prim;
 
         swap(v, piv, ult);
  
         for (int i = prim; i < ult; i++) {
                 if (v[i] <= p) {
                         swap(v, i, j);
                         j++;
                 }
         }
         swap(v, j, ult);
  
         return j;
 }
  
 void swap(int[] v, int a, int b) {
         if (a != b) {
                 int tmp = v[a];
                 v[a] = v[b];
                 v[b] = tmp;
         }
 }
 

3.-Java

 public class QuickSort {
    public static void swap (int A[], int x, int y) {
       int temp = A[x];
       A[x] = A[y];
       A[y] = temp;
    }
                
    public static int partition(int A[], int f, int l) {
       int pivot = A[f];
       while (f < l) {
          while (A[f] < pivot) f++;
          while (A[l] > pivot) l--;
          swap (A, f, l);
       }
       return f;
    }
 
    public static void Quicksort(int A[], int f, int l) {
       if (f >= l) return;
       int pivot_index = partition(A, f, l);
       Quicksort(A, f, pivot_index);
       Quicksort(A, pivot_index+1, l);
    }
 }
 

4.-Clojure Lisp

 (defn qsort
   ([] [])
   ([x & xs] (concat (apply qsort (filter #(< % x) xs))
 		    (cons x (apply qsort (filter #(>= % x) xs)))))) 
 

5.-Haskell

 qsort []     = []
 qsort (x:xs) = qsort smaller ++ [x] ++ qsort bigger
     where smaller = filter (<x)  xs
           bigger = filter (>=x) xs 
 

Los ejemplos están sacados de wikipedia, menos el de Clojure Lisp. Lo dicho, busque, compare, etc.