La web semántica (II): RDF, vocabularios, ontologías y consultas

0
1880
imagen con logo w3c

1 – Introducción

En el capítulo anterior del tutorial, vimos los problemas que planteaba la web para el procesamiento de datos y la necesidad de que las máquinas puedan entender su significado. En esta entrega, profundizaremos un poco más en RDF y crearemos nuestro primer dataset.

Antes de empezar, quisiera aclarar que existen herramientas que permiten crear vocabularios y ontologías con las tecnologías de la web semántica de una forma más cómoda. Sin embargo, dado que pretendo centrarme en los aspectos más técnicos de la materia, he creído conveniente llevar a cabo los ejemplos con tripletas RDF puras en un archivo de texto.

Para llevar a cabo el ejemplo, utilizaremos Apache Jena, un software con múltiples utilidades para trabajar con web semántica. Está escrito en Java, así que es multiplataforma.

2 – Entorno

  • Windows 10
  • SlimBook Pro X (Intel I7, 32 GB RAM)
  • JRE 8 o superior
  • Apache Jena

2.1 – Instalación de Apache Jena

Lo primero que debemos hacer es tener un JRE o JDK instalado. Una vez hecho esto, descargamos Apache jena desde su web. Descomprimimos el archivo y colocamos el directorio donde más nos plazca.

Aparte de esto, creamos la variable de entorno JENA_HOME apuntando al directorio. Además, podemos añadir al path las carpetas bat o bin, dependiendo de nuestro sistema, para una mayor comodidad. Como uso Windows, en mi caso añado el subdirectorio JENA_HOME/bat.

3 – Estructurar los datos en RDF

En primer lugar, vamos a definir de forma más técnica las tripletas. En la entrega pasada, hablábamos de tripletas sujeto-predicado-objeto. Estos nombres pueden llevar a engaños, pues no siempre coinciden con la función que desempeña cada elemento. Los predicados tienen como mínimo un verbo, pero esto no siempre es así en las tripletas. De hecho, casi nunca lo es. En su lugar, utilizaremos los términos domain-property-range a partir de ahora.

Un concepto fundamental que tenemos que asimilar es que todo en RDF está basado en clases. Como en los lenguajes orientados a objetos, cada clase puede tener un número indeterminado de instancias. Sin embargo, a diferencia de ellos, la jerarquía no es rígida. Esto se debe a que, mientras que en la POO trabajamos con árboles de clases, aquí estamos trabajando con grafos. Y eso trae muchas implicaciones curiosas.

La primera es que todos los tipos de RDF son clases. Tanto los recursos, como las propiedades o los literales, funcionan exactamente de la misma manera. Además, la forma de crear una nueva clase es como instancia de la clase base, rdfs:Class. Es para volverse loco, ¿no? Por lo menos nos queda algo familiar. Todos los nombres se escriben en CamelCase, siendo la inicial mayúscula para las clases.

Vamos a detallar un poco cada concepto y cómo se representa mediante los vocabularios de RDF y RDFS. Por comodidad, utilizaremos los prefijos típicos que se utilizan, y no el namespace completo:

  • rdfs:Class: Define el concepto de clase. Prácticamente todo se extiende de ella.
  • Rdfs:Resource: Representa los recursos.
  • rdfs:Property: Representa las propiedades.
  • Rdfs:Literal: Representa cualquier tipo de literal.
  • rdf:type: Establece una relación de instancia entre el domain y el range. Cualquier clase es instancia de la clase rdfs:class.
  • rdfs:subClassOf: Crea una jerarquía de clases entre el domain y el range.
  • rdfs:subPropertyOf: Crea una jerarquía de propiedades entre el domain y el range. Por ejemplo, la propiedad esAmigoDe podría ser una subpropiedad de conoceA.
  • rdfs:domain: Crea restricciones para el domain de una property. El domain de esta propiedad es una property, mientras que el range puede ser cualquier clase.
  • rdfs:range: Crea restricciones para el range de una property. El domain de esta propiedad es una property, mientras que el range puede ser cualquier clase.
  • rdfs:label: Proporciona un nombre legible para el recurso.

Existen otras clases y propiedades, pero su uso excede de lo que pretendemos por ahora. Con lo poco que tenemos, vamos a ver cómo construir el ejemplo que dábamos en el capítulo anterior del tutorial.

3.1 – Modelando a Jose y a Juan

En resumen, lo que teníamos era un modelo de personas en el que estas podían ser a su vez un Familiar, con la relación padre-hijo como propiedad, un Profesor o un Alumno. Además, había Asignaturas que los profesores podían impartir y los alumnos estudiar.

Para no modelar todas las características de una persona, vamos a utilizar el vocabulario Friend of a Friend (FOAF). Este vocabulario es ampliamente utilizado para establecer relaciones entre individuos y grupos, y ya dispone de una clase Person con una serie de características típicas de cualquier persona.


@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.

@prefix foaf: <http://xmlns.com/foaf/0.1/>.

@prefix :<http://www.autentia.com/vocabularies/ejemplo/>.

# Primero modelamos las clases.

:Asignatura rdf:type rdfs:class.

:Familiar rdfs:subClassOf foaf:Person.

:Profesor rdfs:subClassOf foaf:Person.

:Alumno rdfs:subClassOf foaf:Person.

# Vamos a definir las properties de cada clase.

:esPadreDe rdf:type rdfs:Property;

rdfs:domain :Familiar;

rdfs:range :Familiar.

:imparte rdf:type rdfs:Property;

rdfs:domain :Profesor;

rdfs:range :Asignatura.

:estudia rdfs:type rdfs:Property;

rdfs:domain :Alumno;

rdfs:range :Asignatura.

# Ahora instanciamos los recursos.

:juan rdf:type :Familiar, :Alumno.

:jose rdf:type :Familiar, :Profesor.

:matematicas rdf:type :Asignatura.

:lengua rdf:type :Asignatura.

#Y seleccionamos algunas de sus propiedades.

:juan :estudia :matematicas, :lengua.

:jose :imparte :lengua.

:matematicas rdfs:label "Matemáticas"@es.

:matematicas rdfs:label "Maths"@en.

:lengua rdfs:label "Lengua".

:jose foaf:surname "García";

foaf:firstName "José";

:esPadreDe :juan.

Aquí hemos seguido un orden para las tripletas, pero este podría haber sido cualquiera. El caso es que el resultado final tenga sentido y no viole ninguna de las restricciones que hemos impuesto. Como veis, reutilizar vocabularios es tan sencillo como utilizar su namespace, siempre que estén publicados de una forma adecuada.

Por otra parte, podemos utilizar un namespace por defecto. Será este namespace contra el que se resuelvan todos los nombres en los que no se especifica un prefijo.

También hay que destacar la posibilidad de internacionalizar las cadenas de texto, como hemos hecho con la propiedad label del recurso matematicas.

3.2 – Validar el dataset

Podemos validar el dataset con Apache Jena. Para ello, guardamos el archivo como ‘ejemplo_rdfs.ttl’ ejecutamos el siguiente comando:


Riot.bat –validate ./ejemplo_rdfs.ttl

En caso de encontrar alguna inconsistencia en el dataset, como que no encuentre una clase o propiedad, el programa lanzará un error indicándonos el motivo y la localización.

4 – Las ontologías y las relaciones lógicas

Como habréis notado, en el dataset RDF que hemos creado mediante el uso de RDFS, no hemos incluido el resto de las relaciones familiares. Por ejemplo, Juan no tiene ninguna propiedad que indique de Jose es su padre. Si recordáis de la entrega anterior, dijimos que algunas relaciones podían inferirse de forma lógica.

Para poder incluir esta semántica, necesitamos convertir nuestro vocabulario en una ontología. Las ontologías son datasets que añaden ciertos axiomas, reglas y restricciones lógicas. Podemos expresar estas ontologías gracias al uso del Web Ontology Language (OWL).

Sin embargo, OWL no es una única especificación, sino que tiene tres categorías:

  • OWL Lite: Ofrece características para la clasificación de conceptos en clases y restricciones simples.
  • OWL DL: Ofrece una gran expresividad, pero garantiza la completitud computacional. Es decir, asegura que las conclusiones son computables en un tiempo finito. Es la especificación que vamos a utilizar.
  • OWL Full: Ofrece el máximo de expresividad, pero no garantiza la completitud computacional. No es muy usado en la práctica.

OWL Full, por tanto, puede verse como una extensión de RDFS. Recordemos la estructura de grafo y las cosas rocambolescas que nos permite este vocabulario. En cambio, las otras dos opciones pueden verse como una forma restringida de RDF para mantener la coherencia semántica.

Dado que OWL DL es más restrictivo que RDFS, el concepto de clase se redefine. Aun así, veremos que tenemos que apoyarnos en otras propiedades que ya hemos usado con RDF y RDFS.

Podéis ver una lista completa de propiedades y clases de OWL con ejemplos si estáis interesados. A continuación, vamos a ver sólo algunas que nos importan para nuestro caso:

  • wl:TransitiveProperty: Establece la transitividad para una propiedad.
  • wl:inverseOf: Establece la propiedad domain como la inversa del range.
  • wl:SimetricProperty: Establece una propiedad como simétrica. En otras palabras, da igual el sentido de la relación.
  • wl:disjointWith: Establece que la clase domain y range son disjuntas. Es decir, una instancia de A no puede ser a la vez instancia de B.

4.1 – Aplicar OWL a nuestro dataset

Vamos a ver cómo aplicar esto en nuestro ejemplo para el caso de las relaciones familiares:


@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.

@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.

@prefix owl: <http://www.w3.org/2002/07/owl#>.

@prefix foaf: <http://xmlns.com/foaf/0.1/>.

@prefix : <http://www.autentia.com/vocabularies/ejemplo/>.

# Primero modelamos las clases.

:Asignatura rdf:type owl:Class.

:Familiar rdf:type owl:Class.

:Profesor rdf:type owl:Class.

:Alumno rdf:type owl:Class.

:Familiar rdfs:subClassOf foaf:Person.

:Profesor rdfs:subClassOf foaf:Person.

:Alumno rdfs:subClassOf foaf:Person.

# Vamos a hacer que un Profesor no pueda ser a la vez Alumno.

:Profesor owl:disjointWith :Alumno.

# Vamos a definir las properties de cada clase.

:esPadreDe rdf:type owl:ObjectProperty;

rdfs:domain :Familiar;

rdfs:range :Familiar.

:imparte rdf:type owl:ObjectProperty;

rdfs:domain :Profesor;

rdfs:range :Asignatura.

:estudia rdfs:type owl:ObjectProperty;

rdfs:domain :Alumno;

rdfs:range :Asignatura.

# Definimos el resto de propiedades de familiar mediante axiomas lógicos.

:esHijoDe rdf:type owl:ObjectProperty;

owl:inverseOf :esPadreDe.

:parejaDe rdf:type owl:ObjectProperty, owl:SimetricProperty;

rdfs:domain :Familiar;

rdfs:range :Familiar.

:descendienteDe rdf:type owl:ObjectProperty, owl:TransitiveProperty.

:ancestroDe owl:inverseOf :descendienteDe.

# Ahora instanciamos los recursos.

:juan rdf:type :Familiar, :Alumno.

:jose rdf:type :Padre, :Profesor.

:ana rdf:type :Profesor.

:matematicas rdf:type :Asignatura.

:lengua rdf:type :Asignatura.

#Y seleccionamos algunas de sus propiedades.

:juan :estudia :matematicas, :lengua.

:jose :imparte :lengua.

:ana :imparte :matematicas, :Lengua.

:matematicas rdfs:label "Matemáticas"@es.

:matematicas rdfs:label "Maths"@en.

:lengua rdfs:label "Lengua".

:jose foaf:surname "García";

foaf:firstName "José";

:esPadreDe :juan.

:ana foaf:surname "Pérez";

foaf:firstName "Ana".

Como vemos, de esta forma, no hace falta indicar explícitamente que Juan es hijo de Jose. Esta relación se deduce al indicar que es la inversa de ser padre de alguien. La relación de pareja, en cambio, es simétrica. Esto quiere decir que, si uno de los dos recursos la tiene, el otro también la tendrá. Además, el caso de descendiente y ancestro es un ejemplo claro de cómo se puede aprovechar la transitividad.

Hemos incluido aquí a Ana como profesora para tener algo más de juego en las consultas que haremos sobre nuestro dataset.

4.2 – Validar el dataset

Igual que con nuestro vocabulario confeccionado con RDFS, podemos guardar nuestra ontología y validarla. La archivamos como ‘ejemplo_owl.ttl’ y ejecutamos el siguiente comando:


Riot –validate ./ejemplo_owl.ttl

4.2 – El resto de las relaciones

Os habréis fijado en que no hemos implementado con OWL algunas de las relaciones de las que hablábamos en el capítulo anterior. Por ejemplo, dijimos que la relación de hermanos se basaba en comprobar si ambos recursos tenían un mismo padre. De igual forma, estaría bien poder decir que, si A es hijo de B, entonces también es descendiente de B, pero no al revés.

Este tipo de reglas no pueden implementarse con OWL. Son algo más complejas que las relaciones que maneja. Actualmente, no existe ningún vocabulario considerado un estándar oficial. Dependiendo del razonador que utilicemos para realizar la inferencia, deberemos utilizar uno u otro. Por eso no vamos a ver este tipo de reglas en este tutorial, pero que sepáis que existen y podemos aprovechar toda su potencia.

5 – Consultar nuestro dataset

Ahora que hemos construido un dataset, podemos utilizar SPARQL para realizar consultas y extraer conocimiento.

La sintaxis de SPARQL es similar a la de otros lenguajes de consultas como SQL. No obstante, tiene características propias que podemos asimilar a lenguajes de programación lógica como Prolog. SPARQL resuelve las variables mediante matching de las tripletas que tenemos en el dataset.

Una consulta SPARQL puede contener los siguientes elementos:

  • Declaración de prefijos los namespaces que utilicemos.
  • Consulta: El cuerpo de la consulta propiamente dicha. Se divide en:
    • Resultados: Determina los datos que se van a devolver y la forma en que s hará. Pueden ser:
    • SELECT: Devuelve una tabla de resultados.
    • CONSTRUCT: Devuelve un grafo RDF basado en la plantilla de la consulta.
    • DESCRIBE: Devuelve un grafo RDF basado en la configuración del procesador de consultas.
    • ASK: Realiza una consulta booleana.
    • Where: Tripletas que matchean las variables.
    • Filter: Permite establecer restricciones y filtros para los resultados.
    • Modificadores: Permiten modificar el conjunto de soluciones. Podemos encontrar modificadores típicos como:
    • OFFSET y LIMIT
    • ORDER BY
    • DISTINCT
    • GROUP BY

El resultado de SELECT puede serializarse en cuatro formatos diferentes: XML, JSON, CSV y TSV. Esto facilita su procesamiento y análisis posterior.

Una consulta puede constar de varias subconsultas. Además, estas consultas pueden estar federadas. Es decir, podemos delegar determinadas consultas en SPARQL end-points concretos. Para ello, utilizamos la palabra reservada SERVICE seguida de la URL del SPARQL end-point que queramos utilizar, antes de la consulta.

Para ver más en detalle todas las capacidades de SPARQL, recomiendo el tutorial de Apache Jena para SPARQL y la documentación del W3C sobre SPARQL 1.1. Aquí veremos cómo realizar una consulta simple sobre nuestro dataset.

5.1 – Obteniendo datos de nuestro dataset

Vamos a empezar con una consulta simple. Comprobaremos las asignaturas que estudia Juan:


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>

PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

PREFIX owl: <http://www.w3.org/2002/07/owl#>

PREFIX foaf: <http://xmlns.com/foaf/0.1/>

PREFIX : <http://www.autentia.com/vocabularies/ejemplo/>

SELECT ?nombreAsignatura

WHERE {

:juan :estudia ?asignatura.

?asignatura rdfs:label ?nombreAsignatura.

}

Las variables se caracterizan por el signo de interrogación (¿) que las precede.

Guardamos la consulta como ‘query1.rq’. Ahora vamos a extraer las asignaturas que enseña cada profesor:


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>

PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

PREFIX owl: <http://www.w3.org/2002/07/owl#>

PREFIX foaf: <http://xmlns.com/foaf/0.1/>

PREFIX : <http://www.autentia.com/vocabularies/ejemplo/>

SELECT ?nombre ?apellido ?asignatura

WHERE {

[] :imparte ?asignatura;

foaf:firstName ?nombre;

foaf:surname ?apellido.

}

Si no vamos a utilizar el valor en los resultados, podemos usar un nodo en blanco. Este puede no tener nombre si no vamos a usarlo más de una vez, como en el ejemplo. Entonces se puede abreviar con []. Si vamos a reutilizarlo en otra tripleta, tendremos que asignarle un nombre de la forma _:nombre.

Guardamos la consulta como ‘query2.ttl’. Ahora comprobaremos si hay algún profesor que imparta clase a algún hijo suyo. Como sabemos, Jose lo hace, así que debería ser un resultado positivo:


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>

PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

PREFIX owl: <http://www.w3.org/2002/07/owl#>

PREFIX foaf: <http://xmlns.com/foaf/0.1/>

PREFIX : <http://www.autentia.com/vocabularies/ejemplo/>

SELECT ?nombre ?apellido ?hijo ?asignatura

WHERE {

[] :imparte ?asignatura;

:esPadreDe ?hijo;

foaf:firstName ?nombre;

foaf:surname ?apellido.

?hijo :estudia ?asignatura.

}

Algo a destacar es que una consulta puede devolver literales o recursos. Por ejemplo, en este caso, estamos devolviendo los literales del nombre y el apellido del profesor, mientras que la asignatura y el hijo se devuelven como recursos.

Guardamos la consulta como ‘query3.rq’. Vamos con la última:


PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>

PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

PREFIX owl: <http://www.w3.org/2002/07/owl#>

PREFIX foaf: <http://xmlns.com/foaf/0.1/>

PREFIX : <http://www.autentia.com/vocabularies/ejemplo/>

SELECT ?padre

WHERE {

:juan :esHijoDe ?padre.

}

Aquí comprobamos que la propiedad inversa que hemos declarado funciona correctamente.

5.2 – Ejecutar la consulta

Para ejecutar las consultas, recurrimos de nuevo a una de las utilidades de línea de comando que nos ofrece Jena:


Sparql.bat –data=./ejemplo_owl.ttl –query=./query.rq

Comprobamos que todas las consultas devuelven lo que tienen que devolver… ¡excepto la cuarta! ¿Es que hemos hecho algo mal?

El problema es que el programa sparql que estamos utilizando se limita a consultar el dataset que le pasamos en el archivo. No realiza ningún tipo de inferencia. Para que la propiedad se interpretara de forma correcta, necesitaríamos cargar esta ontología en un datastore con soporte para inferencia de OWL. Dado que eso puede ser materia de un tutorial por sí mismo, no vamos a hacerlo aquí.

5.3 – Abriendo las miras

Evidentemente, este dataset es muy pequeño y la información que hemos extraído la conocíamos de sobra. Sin embargo, os planteo algunos ejercicios que podéis intentar resolver con ayuda de la DBPedia, y que espero que os den una idea de la potencia que nos ofrece la web semántica:

  • Listar todos los pintores italianos que nacieron en el siglo XVI. Sólo nos interesa conocer su nombre y su fecha de nacimiento.
  • Listar los pintores italianos del siglo XVI que vivieron más de 55 años. Ahora también nos interesa la fecha de defunción.
  • Listar todos los cuadros de los pintores italianos del siglo XVI que vivieron más de 55 años. Nos interesa el título del cuadro, el autor y la fecha en que se pintó.

Como veis, las posibilidades son enormes. Podemos generar una gran cantidad de conocimiento específico en un tiempo mucho más breve del que nos llevaría hacerlo a mano, incluso con la ayuda de buscadores. Y lo mejor es que no estamos limitados a una sola fuente de información, sino que podemos beber también de otros SPARQL end-points.

6 – Conclusiones

La web semántica es una herramienta muy potente. Permite que las máquinas puedan entender conceptos y cómo se relacionan entre ellos, de modo que pueden responder de una forma más inteligente a nuestras preguntas.

¡Espero que os haya gustado el tutorial y que os sirva como trampolín si os interesa el tema!

7 – Referencias

[1] Resource Description Language

[2] RDF Schema

[3] Web Ontology Language

[4] SPARQL

[5] Apache Jena

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad