# Dependances pre-provisionnees (rdflib owlrl) : voir SemanticWeb/requirements.txt ; imports dans les cellules suivantes.SW-6b-Python-RDFS
Navigation : Index | << SW-6 C# | SW-7b Python OWL >>
RDFS en Python avec rdflib + owlrl : Schema et Inference
Ce notebook est un sidetrack optionnel qui presente l’equivalent Python des concepts RDFS du notebook SW-6 (dotNetRDF). Vous y decouvrirez comment rdflib (construction de schemas, parsing Turtle, requêtes SPARQL) et owlrl (vrai raisonneur RDFS) cooperent pour reproduire en Python le travail du StaticRdfsReasoner de dotNetRDF.
Objectifs d’apprentissage
A la fin de ce notebook, vous saurez : 1. Construire un schema RDFS en Python avec rdflib (rdfs:Class, rdfs:subClassOf, rdfs:domain, rdfs:range) 2. Charger un fichier Turtle existant (data/animals.ttl) et explorer sa hiérarchie de classes 3. Appliquer un vrai raisonneur RDFS (owlrl) pour materialiser les triplets inferes 4. Interroger les données enrichies par SPARQL 5. Faire la correspondance entre dotNetRDF (StaticRdfsReasoner) et rdflib + owlrl (DeductiveClosure)
Prerequis
- SW-6-CSharp-RDFS recommande (pour la comprehension conceptuelle)
- Python 3.10+
- Notebooks SW-2b et SW-4b (bases rdflib + SPARQL en Python)
Duree estimee : 40 minutes
Note : owlrl est le raisonneur RDFS/OWL de reference en Python pur (implementation des règles d’entailment W3C RDF 1.1 Semantics). Nous ne reinventerons pas l’inference a la main dans les cellules de production : une cellule de demonstration pedagogique expose le mécanisme (point fixe), puis owlrl prend le relais pour le travail reel.
1. Installation et imports
rdflib est le coeur Python du Web Sémantique (equivalent de dotNetRDF). owlrl est le raisonneur RDFS/OWL-RL qui materialise les triplets inferes (equivalent du StaticRdfsReasoner de dotNetRDF).
Import des bibliotheques. Les namespaces W3C (RDF, RDFS, XSD) sont preconfigures dans rdflib, ce qui evite de manipuler les URIs completes a la main.
try:
from rdflib import Graph, URIRef, Literal, BNode, Namespace
from rdflib.namespace import RDF, RDFS, XSD
import owlrl
LIBS_AVAILABLE = True
print("Imports rdflib + owlrl prets.")
print(f"rdflib version : {__import__('rdflib').__version__}")
print(f"owlrl version : {owlrl.__version__}")
print()
print("Namespaces W3C preconfigures :")
print(f" RDF.type = {RDF.type}")
print(f" RDFS.subClassOf= {RDFS.subClassOf}")
print(f" RDFS.domain = {RDFS.domain}")
print(f" RDFS.range = {RDFS.range}")
print(f" RDFS.label = {RDFS.label}")
except ImportError as e:
LIBS_AVAILABLE = False
print(f"Bibliotheque manquante : {e}")
print("Installez avec : pip install rdflib owlrl")Imports rdflib + owlrl prets.
rdflib version : 7.6.0
owlrl version : 7.1.4
Namespaces W3C preconfigures :
RDF.type = http://www.w3.org/1999/02/22-rdf-syntax-ns#type
RDFS.subClassOf= http://www.w3.org/2000/01/rdf-schema#subClassOf
RDFS.domain = http://www.w3.org/2000/01/rdf-schema#domain
RDFS.range = http://www.w3.org/2000/01/rdf-schema#range
RDFS.label = http://www.w3.org/2000/01/rdf-schema#label
Interpretation : imports
| Symbole rdflib | Equivalent dotNetRDF |
|---|---|
Graph |
IGraph / Graph |
URIRef |
IUriNode |
Literal |
ILiteralNode |
RDF.type |
rdf:type (URI complete) |
RDFS.subClassOf |
rdfs:subClassOf |
owlrl.DeductiveClosure |
StaticRdfsReasoner |
Points cles : 1. rdflib expose les namespaces W3C comme attributs Python (RDFS.subClassOf au lieu de l’URI complete http://www.w3.org/2000/01/rdf-schema#subClassOf). 2. owlrl est un module separe de rdflib : on l’invoque explicitement quand on veut du raisonnement. C’est une différence philosophique avec dotNetRDF ou le raisonneur est integre.
2. Qu’est-ce que RDFS ?
RDF Schema (RDFS) est une extension de RDF qui fournit un vocabulaire pour decrire la structure des données RDF. Tandis que RDF permet d’exprimer des faits sous forme de triplets (sujet, predicat, objet), RDFS ajoute la capacite de définir :
RDF Schema (RDFS) est standardise par le W3C dans RDF Schema 1.1 (Brickley & Guha, W3C Recommendation 2014). C’est une extension de RDF qui fournit un vocabulaire pour decrire la structure des données RDF – classes, hiérarchies, domaines/portees de proprietes et règles d’inference.
- Des classes et des hiérarchies de classes
- Des proprietes avec domaine et portee
- Des règles d’inference implicites
RDFS dans la pile du Web Sémantique
flowchart TD
OWL["OWL (SW-7) - Logique descriptive"] --> RDFS["RDFS (SW-6) - Vocabulaires, hierarchies - NOUS SOMMES ICI"]
RDFS --> SPARQL["SPARQL (SW-4/5) - Interrogation"]
SPARQL --> RDF["RDF (SW-1/2/3) - Triplets, graphes"]
RDF --> URI["URI / IRI - Identifiants"]
style RDFS font-weight:bold
RDF seul vs RDFS
| Capacite | RDF seul | RDF + RDFS |
|---|---|---|
| Exprimer des faits (triplets) | Oui | Oui |
| Définir des classes | Non | rdfs:Class |
| Hiérarchies de classes | Non | rdfs:subClassOf |
| Hiérarchies de proprietes | Non | rdfs:subPropertyOf |
| Contraindre domaine/portee | Non | rdfs:domain, rdfs:range |
| Inference automatique | Non | Oui (règles RDFS) |
| Documentation | Limite | rdfs:label, rdfs:comment |
Vocabulaire RDFS complet
Le namespace RDFS est http://www.w3.org/2000/01/rdf-schema#. Voici les termes essentiels :
| Terme RDFS | Type | Description |
|---|---|---|
rdfs:Class |
Classe | Définit une classe (un ensemble de ressources) |
rdfs:subClassOf |
Propriete | A est une sous-classe de B (A \(\subseteq\) B) |
rdfs:subPropertyOf |
Propriete | P1 est une sous-propriete de P2 |
rdfs:domain |
Propriete | Le sujet d’un triplet avec P est de type C |
rdfs:range |
Propriete | L’objet d’un triplet avec P est de type C |
rdfs:label |
Propriete | Etiquette lisible par un humain |
rdfs:comment |
Propriete | Description textuelle |
rdfs:Resource |
Classe | Superclasse de toutes les ressources |
rdfs:Literal |
Classe | Classe des valeurs litterales |
rdfs:Datatype |
Classe | Classe des types de données |
rdfs:seeAlso |
Propriete | Lien vers une ressource connexe |
rdfs:isDefinedBy |
Propriete | Lien vers la definition de la ressource |
3. Construire un schema RDFS en code
Construisons une taxonomie animale en Python avec rdflib. Nous allons créer la hiérarchie suivante :
Animal
|-- Mammal (Mammifere)
| |-- Dog (Chien)
| +-- Cat (Chat)
+-- Bird (Oiseau)
+-- Parrot (Perroquet)
La même taxonomie, rendue sous forme de graphe : chaque fleche rdfs:subClassOf pointe de la sous-classe vers sa super-classe (sens de lecture « est une sorte de »).
graph BT
Dog["ex:Dog Chien"]
Cat["ex:Cat Chat"]
Parrot["ex:Parrot Perroquet"]
Mammal["ex:Mammal Mammifere"]
Bird["ex:Bird Oiseau"]
Animal["ex:Animal"]
Dog -->|rdfs:subClassOf| Mammal
Cat -->|rdfs:subClassOf| Mammal
Parrot -->|rdfs:subClassOf| Bird
Mammal -->|rdfs:subClassOf| Animal
Bird -->|rdfs:subClassOf| Animal
classDef root fill:#cfe2ff,stroke:#084298,color:#052c65
classDef mid fill:#d1e7dd,stroke:#0f5132,color:#052e16
classDef leaf fill:#fff3cd,stroke:#b8860b,color:#5c4400
class Animal root
class Mammal,Bird mid
class Dog,Cat,Parrot leaf
Lecture. Les 5 relations
rdfs:subClassOf(Dog/Cat < Mammal, Parrot < Bird, Mammal/Bird < Animal) forment un arbre d’heritage. RDFS garantit que cette relation est transitive :Dog <: MammaletMammal <: AnimalimpliquentDog <: Animal. C’est cette transitivite que le raisonneur exploite pour typer les instances par toutes leurs classes ancetres.
if LIBS_AVAILABLE:
# Construire un schema RDFS en Python avec rdflib
schema = Graph()
# Namespace personnalise pour notre taxonomie animale
EX = Namespace("http://example.org/animals#")
schema.bind("ex", EX)
schema.bind("rdfs", RDFS)
schema.bind("rdf", RDF)
schema.bind("xsd", XSD)
# --- Definition des classes ---
animal = EX.Animal
mammal = EX.Mammal
bird = EX.Bird
dog = EX.Dog
cat = EX.Cat
parrot = EX.Parrot
# Declarer les classes comme instances de rdfs:Class
for cls in (animal, mammal, bird, dog, cat, parrot):
schema.add((cls, RDF.type, RDFS.Class))
# Hierarchie de classes (rdfs:subClassOf)
schema.add((mammal, RDFS.subClassOf, animal))
schema.add((bird, RDFS.subClassOf, animal))
schema.add((dog, RDFS.subClassOf, mammal))
schema.add((cat, RDFS.subClassOf, mammal))
schema.add((parrot, RDFS.subClassOf, bird))
# Etiquettes (rdfs:label) en francais
schema.add((animal, RDFS.label, Literal("Animal", lang="fr")))
schema.add((mammal, RDFS.label, Literal("Mammifere", lang="fr")))
schema.add((bird, RDFS.label, Literal("Oiseau", lang="fr")))
schema.add((dog, RDFS.label, Literal("Chien", lang="fr")))
schema.add((cat, RDFS.label, Literal("Chat", lang="fr")))
schema.add((parrot, RDFS.label, Literal("Perroquet", lang="fr")))
# Commentaire sur la classe racine
schema.add((animal, RDFS.comment, Literal("Classe racine de tous les animaux", lang="fr")))
print(f"Schema RDFS cree : {len(schema)} triplets")
print()
print("Hierarchie de classes :")
for s, _, o in schema.triples((None, RDFS.subClassOf, None)):
child = str(s).split("#")[-1]
parent = str(o).split("#")[-1]
print(f" {child} rdfs:subClassOf {parent}")
else:
print("Bibliotheques non disponibles : construction du schema ignoree.")Schema RDFS cree : 18 triplets
Hierarchie de classes :
Mammal rdfs:subClassOf Animal
Bird rdfs:subClassOf Animal
Dog rdfs:subClassOf Mammal
Cat rdfs:subClassOf Mammal
Parrot rdfs:subClassOf Bird
Interpretation : Construction du schema
Nous avons créé un schema RDFS complet avec : - 6 classes organisees en hiérarchie - 5 relations rdfs:subClassOf definissant l’arbre taxonomique - 6 etiquettes rdfs:label en francais - 1 commentaire rdfs:comment sur la classe racine
| Opération | rdflib (Python) | dotNetRDF (C#) |
|---|---|---|
| Créer un graphe | g = Graph() |
IGraph g = new Graph() |
| Ajouter un triplet | g.add((s, p, o)) |
g.Assert(new Triple(s, p, o)) |
| Litteral avec langue | Literal("Animal", lang="fr") |
g.CreateLiteralNode("Animal", "fr") |
| Namespace | g.bind("ex", EX) |
g.NamespaceMap.AddNamespace("ex", uri) |
Points cles : 1. Chaque classe est declaree comme instance de rdfs:Class via rdf:type 2. rdfs:subClassOf est transitif : Dog < Mammal < Animal, donc Dog < Animal 3. Les etiquettes multilingues utilisent les tags de langue (lang="fr", lang="en", etc.)
Ajout de proprietes avec domaine et portee
Definissons des proprietes pour notre taxonomie, avec rdfs:domain (type du sujet) et rdfs:range (type de l’objet/valeur).
if LIBS_AVAILABLE:
# Proprietes avec domaine et portee
name_prop = EX.name
age_prop = EX.age
sound_prop = EX.sound
canfly_prop = EX.canFly
# ex:name - propriete avec domaine Animal, portee xsd:string
schema.add((name_prop, RDF.type, RDF.Property))
schema.add((name_prop, RDFS.domain, animal))
schema.add((name_prop, RDFS.range, XSD.string))
schema.add((name_prop, RDFS.label, Literal("nom", lang="fr")))
# ex:age - propriete avec domaine Animal, portee xsd:integer
schema.add((age_prop, RDF.type, RDF.Property))
schema.add((age_prop, RDFS.domain, animal))
schema.add((age_prop, RDFS.range, XSD.integer))
schema.add((age_prop, RDFS.label, Literal("age", lang="fr")))
# ex:sound - propriete avec domaine Animal, portee xsd:string
schema.add((sound_prop, RDF.type, RDF.Property))
schema.add((sound_prop, RDFS.domain, animal))
schema.add((sound_prop, RDFS.range, XSD.string))
schema.add((sound_prop, RDFS.label, Literal("cri", lang="fr")))
# ex:canFly - propriete avec domaine Animal, portee xsd:boolean
schema.add((canfly_prop, RDF.type, RDF.Property))
schema.add((canfly_prop, RDFS.domain, animal))
schema.add((canfly_prop, RDFS.range, XSD.boolean))
schema.add((canfly_prop, RDFS.label, Literal("peut voler", lang="fr")))
print(f"Schema enrichi : {len(schema)} triplets")
print()
print("Proprietes definies :")
for p in (name_prop, age_prop, sound_prop, canfly_prop):
name = str(p).split("#")[-1]
domain = str(next(schema.objects(p, RDFS.domain))).split("#")[-1]
rng = str(next(schema.objects(p, RDFS.range))).split("#")[-1]
print(f" ex:{name:8s} domain={domain:8s} range={rng}")
else:
print("Bibliotheques non disponibles : proprietes ignorees.")Schema enrichi : 34 triplets
Proprietes definies :
ex:name domain=Animal range=string
ex:age domain=Animal range=integer
ex:sound domain=Animal range=string
ex:canFly domain=Animal range=boolean
Interpretation : Domaine et portee
| Propriete | Domaine | Portee | Signification |
|---|---|---|---|
ex:name |
ex:Animal |
xsd:string |
Le nom est une chaîne, applicable a tout Animal |
ex:age |
ex:Animal |
xsd:integer |
L’age est un entier |
ex:sound |
ex:Animal |
xsd:string |
Le cri est une chaîne |
ex:canFly |
ex:Animal |
xsd:boolean |
Capacite de vol, vrai/faux |
Consequence de rdfs:domain : si on ecrit ex:rex ex:name "Rex", alors un raisonneur RDFS peut inferer que ex:rex rdf:type ex:Animal (car ex:name a pour domaine ex:Animal).
Attention :
rdfs:domainetrdfs:rangene sont pas des contraintes de validation (comme en SQL). Ce sont des règles d’inference. Si le domaine est viole, RDFS ne signale pas d’erreur – il infere un type supplementaire.
4. Charger et explorer animals.ttl
Le fichier data/animals.ttl contient la hiérarchie complete avec des instances. Chargeons-le avec le parser Turtle de rdflib et explorons sa structure.
if LIBS_AVAILABLE:
# Charger le fichier animals.ttl avec rdflib
animals = Graph()
animals.parse("data/animals.ttl", format="turtle")
print(f"Graphe charge : {len(animals)} triplets")
print()
print("Namespaces declares :")
for prefix, ns in animals.namespaces():
print(f" {prefix:6s} -> {ns}")
print()
# Lister toutes les classes (sujets de type rdfs:Class)
print("Classes definies :")
for cls in animals.subjects(RDF.type, RDFS.Class):
name = str(cls).split("#")[-1]
print(f" - {name}")
else:
print("Bibliotheques non disponibles : chargement ignore.")Graphe charge : 51 triplets
Namespaces declares :
brick -> https://brickschema.org/schema/Brick#
csvw -> http://www.w3.org/ns/csvw#
dc -> http://purl.org/dc/elements/1.1/
dcat -> http://www.w3.org/ns/dcat#
dcmitype -> http://purl.org/dc/dcmitype/
dcterms -> http://purl.org/dc/terms/
dcam -> http://purl.org/dc/dcam/
doap -> http://usefulinc.com/ns/doap#
foaf -> http://xmlns.com/foaf/0.1/
geo -> http://www.opengis.net/ont/geosparql#
odrl -> http://www.w3.org/ns/odrl/2/
org -> http://www.w3.org/ns/org#
prof -> http://www.w3.org/ns/dx/prof/
prov -> http://www.w3.org/ns/prov#
qb -> http://purl.org/linked-data/cube#
schema -> https://schema.org/
sh -> http://www.w3.org/ns/shacl#
skos -> http://www.w3.org/2004/02/skos/core#
sosa -> http://www.w3.org/ns/sosa/
ssn -> http://www.w3.org/ns/ssn/
time -> http://www.w3.org/2006/time#
vann -> http://purl.org/vocab/vann/
void -> http://rdfs.org/ns/void#
wgs -> https://www.w3.org/2003/01/geo/wgs84_pos#
owl -> http://www.w3.org/2002/07/owl#
rdf -> http://www.w3.org/1999/02/22-rdf-syntax-ns#
rdfs -> http://www.w3.org/2000/01/rdf-schema#
xsd -> http://www.w3.org/2001/XMLSchema#
xml -> http://www.w3.org/XML/1998/namespace
ex -> http://example.org/animals#
Classes definies :
- Animal
- Mammal
- Bird
- Dog
- Cat
- Parrot
Explorons maintenant la hiérarchie de classes et les instances declarees (avant toute inference).
if LIBS_AVAILABLE:
# Explorer la hierarchie de classes
print("Hierarchie de classes (rdfs:subClassOf) :")
for s, _, o in animals.triples((None, RDFS.subClassOf, None)):
child = str(s).split("#")[-1]
parent = str(o).split("#")[-1]
print(f" {child} --> {parent}")
print()
# Lister les instances par classe (uniquement les classes "feuilles")
print("Instances par classe (avant inference) :")
for cls_name in ("Dog", "Cat", "Parrot"):
cls_uri = URIRef(f"http://example.org/animals#{cls_name}")
instances = sorted(str(s).split("#")[-1] for s in animals.subjects(RDF.type, cls_uri))
print(f" {cls_name} : {instances}")
else:
print("Bibliotheques non disponibles : exploration ignoree.")Hierarchie de classes (rdfs:subClassOf) :
Mammal --> Animal
Bird --> Animal
Dog --> Mammal
Cat --> Mammal
Parrot --> Bird
Instances par classe (avant inference) :
Dog : ['buddy', 'rex']
Cat : ['minou']
Parrot : ['coco']
Interpretation : Structure du fichier animals.ttl
Le fichier Turtle contient :
| Élément | Quantite | Exemples |
|---|---|---|
| Classes | 6 | Animal, Mammal, Bird, Dog, Cat, Parrot |
| Proprietes | 4 | name, age, sound, canFly |
| Instances | 4 | rex (Dog), minou (Cat), coco (Parrot), buddy (Dog) |
| Relations subClassOf | 5 | Dog < Mammal < Animal, etc. |
Observation importante : Les instances sont typees avec leur classe la plus spécifique (ex: ex:rex a ex:Dog), mais pas avec les classes parentes. Un raisonneur RDFS deduira que Rex est aussi un Mammal et un Animal.
5. Inference RDFS
L’inference RDFS permet de deduire automatiquement de nouveaux triplets a partir des règles RDFS. Le mécanisme principal est la transitivite de rdfs:subClassOf.
Les règles d’inference RDFS presentees ci-dessous (rdfs2, rdfs3, rdfs5, rdfs9, rdfs11) sont les règles d’entailment formellement specifiees dans RDF 1.1 Semantics (Hayes & Patel-Schneider, W3C Recommendation 2014). Elles definissent quels triplets peuvent etre deduits a partir d’un graphe RDFS.
Règles d’inference RDFS essentielles
| Règle | Premisses | Conclusion |
|---|---|---|
| rdfs9 (heritage de type) | ?C rdfs:subClassOf ?D et ?X rdf:type ?C |
?X rdf:type ?D |
| rdfs11 (transitivite) | ?A rdfs:subClassOf ?B et ?B rdfs:subClassOf ?C |
?A rdfs:subClassOf ?C |
| rdfs5 (sous-propriete) | ?P rdfs:subPropertyOf ?Q et ?X ?P ?Y |
?X ?Q ?Y |
| rdfs2 (domaine) | ?P rdfs:domain ?C et ?X ?P ?Y |
?X rdf:type ?C |
| rdfs3 (portee) | ?P rdfs:range ?C et ?X ?P ?Y |
?Y rdf:type ?C |
Exemple concret
Avec nos données :
ex:Dog rdfs:subClassOf ex:Mammal .
ex:Mammal rdfs:subClassOf ex:Animal .
ex:rex rdf:type ex:Dog .
L’inference RDFS deduit :
ex:rex rdf:type ex:Mammal . # Par rdfs9 (Dog < Mammal)
ex:rex rdf:type ex:Animal . # Par rdfs9 (Mammal < Animal)
ex:Dog rdfs:subClassOf ex:Animal . # Par rdfs11 (transitivite)
Demonstration : inference manuelle (pedagogique)
Implementons d’abord l’inference manuellement, a but pedagogique uniquement, pour bien comprendre le mécanisme du point fixe. En production, on utilise owlrl (section suivante) qui applique l’ensemble complet des règles W3C.
if LIBS_AVAILABLE:
# Demonstration pedagogique : implementer manuellement la regle rdfs9
# (heritage de type par rdfs:subClassOf) avec un point fixe.
#
# ATTENTION : cette cellule est pedagogique. Pour du raisonnement reel,
# utilisez owlrl (cellule suivante) qui couvre l'ensemble des regles W3C.
data = Graph()
data.bind("ex", Namespace("http://example.org/animals#"))
animal_n = URIRef("http://example.org/animals#Animal")
mammal_n = URIRef("http://example.org/animals#Mammal")
dog_n = URIRef("http://example.org/animals#Dog")
rex = URIRef("http://example.org/animals#rex")
# Schema
data.add((dog_n, RDFS.subClassOf, mammal_n))
data.add((mammal_n, RDFS.subClassOf, animal_n))
# Instance
data.add((rex, RDF.type, dog_n))
print("=== AVANT inference ===")
print(f"Nombre de triplets : {len(data)}")
print("Types de rex :")
for _, _, o in data.triples((rex, RDF.type, None)):
print(f" rex rdf:type {str(o).split('#')[-1]}")
print()
# --- Inference manuelle : regle rdfs9 ---
# Pour chaque "?X rdf:type ?C" et "?C rdfs:subClassOf ?D", ajouter "?X rdf:type ?D"
iteration = 0
while True:
iteration += 1
new_triples = []
# Snapshot des types et sous-classes courantes
for x, _, c in list(data.triples((None, RDF.type, None))):
for _, _, d in data.triples((c, RDFS.subClassOf, None)):
inferred = (x, RDF.type, d)
if inferred not in data and inferred not in new_triples:
new_triples.append(inferred)
if not new_triples:
break
for t in new_triples:
data.add(t)
print(f"Iteration {iteration} : {len(new_triples)} triplet(s) infere(s)")
for s, _, o in new_triples:
print(f" + {str(s).split('#')[-1]} rdf:type {str(o).split('#')[-1]}")
print()
print("=== APRES inference ===")
print(f"Nombre de triplets : {len(data)}")
print("Types de rex :")
for _, _, o in sorted(data.triples((rex, RDF.type, None)), key=lambda t: str(t[2])):
print(f" rex rdf:type {str(o).split('#')[-1]}")
else:
print("Bibliotheques non disponibles : demonstration ignoree.")=== AVANT inference ===
Nombre de triplets : 3
Types de rex :
rex rdf:type Dog
Iteration 1 : 1 triplet(s) infere(s)
+ rex rdf:type Mammal
Iteration 2 : 1 triplet(s) infere(s)
+ rex rdf:type Animal
=== APRES inference ===
Nombre de triplets : 5
Types de rex :
rex rdf:type Animal
rex rdf:type Dog
rex rdf:type Mammal
Interpretation : Inference manuelle
Sortie obtenue : A partir de 3 triplets initiaux, l’inference a deduit 2 triplets supplementaires.
| Itération | Triplet infere | Règle appliquee |
|---|---|---|
| 1 | rex rdf:type Mammal |
rdfs9 : rex est Dog, Dog < Mammal |
| 2 | rex rdf:type Animal |
rdfs9 : rex est Mammal, Mammal < Animal |
Points cles : 1. L’algorithme est un point fixe : on itere jusqu’a ce qu’aucun nouveau triplet ne soit infere 2. Chaque itération propage les types d’un niveau dans la hiérarchie 3. Le nombre d’itérations depend de la profondeur de la hiérarchie de classes
En pratique, on utilise owlrl plutot que cette implementation manuelle. Mais comprendre le mécanisme est essentiel : owlrl applique la même logique de point fixe, mais sur les 51 règles d’entailment W3C complet (rdfs2, rdfs3, rdfs5, rdfs7, rdfs9, rdfs11, etc.), pas seulement rdfs9.
La règle d’inférence rdfs9 (heritage de type par rdfs:subClassOf), rendue sous forme de graphe pour l’instance ex:rex. En trait plein : les 3 triplets assertes ; en tirete ambre : les 2 triplets inferes par transitivite.
graph BT
rex["ex:rex (instance)"]
Dog["ex:Dog"]
Mammal["ex:Mammal"]
Animal["ex:Animal"]
rex -->|"rdf:type (asserte)"| Dog
Dog -->|"rdfs:subClassOf"| Mammal
Mammal -->|"rdfs:subClassOf"| Animal
rex -.->|"rdf:type (infere)"| Mammal
rex -.->|"rdf:type (infere)"| Animal
classDef inst fill:#cfe2ff,stroke:#084298,color:#052c65
classDef cls fill:#d1e7dd,stroke:#0f5132,color:#052e16
class rex inst
class Dog,Mammal,Animal cls
linkStyle 3,4 stroke:#b8860b,stroke-width:2px
Lecture. Partant de 3 triplets (
rex rdf:type Dog,Dog subClassOf Mammal,Mammal subClassOf Animal), la règle rdfs9 — « si?x rdf:type ?Cet?C rdfs:subClassOf ?D, alors?x rdf:type ?D» — deduit 2 triplets supplementaires :rex rdf:type Mammalpuis, en reappliquant la règle,rex rdf:type Animal. Le point-fixe est atteint quand plus aucun nouveau triplet n’apparait.
Utilisation du raisonneur RDFS owlrl
owlrl est le raisonneur RDFS/OWL de reference en Python pur. Il implemente l’ensemble des règles d’entailment du W3C (RDF 1.1 Semantics) et materialise les triplets inferes directement dans le graphe. C’est l’equivalent fonctionnel du StaticRdfsReasoner de dotNetRDF.
L’API principale est owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(graph) : on instancie une closure avec un jeu de sémantiques (ici RDFS seul ; owlrl.OWLRL_Semantics pour OWL 2 RL), puis on appelle expand() qui mute le graphe en place.
if LIBS_AVAILABLE:
# Charger animals.ttl dans un nouveau graphe frais
animal_graph = Graph()
animal_graph.parse("data/animals.ttl", format="turtle")
triplets_before = len(animal_graph)
print(f"Avant inference : {triplets_before} triplets")
rex_uri = URIRef("http://example.org/animals#rex")
coco_uri = URIRef("http://example.org/animals#coco")
print("Types de rex avant inference :")
for _, _, o in sorted(animal_graph.triples((rex_uri, RDF.type, None)), key=lambda t: str(t[2])):
print(f" - {str(o).split('#')[-1]}")
print()
# === Application du VRAI raisonneur RDFS owlrl ===
# owlrl.DeductiveClosure.expand() materialise les triplets inferes dans le graphe.
# C'est l'equivalent Python de `StaticRdfsReasoner.Apply(graph)` en dotNetRDF.
owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(animal_graph)
triplets_after = len(animal_graph)
print(f"Apres inference : {triplets_after} triplets (+{triplets_after - triplets_before} inferes)")
print()
print("Types de rex apres inference :")
for _, _, o in sorted(animal_graph.triples((rex_uri, RDF.type, None)), key=lambda t: str(t[2])):
if "animals#" in str(o):
print(f" - {str(o).split('#')[-1]}")
print()
print("Types de coco apres inference :")
for _, _, o in sorted(animal_graph.triples((coco_uri, RDF.type, None)), key=lambda t: str(t[2])):
if "animals#" in str(o):
print(f" - {str(o).split('#')[-1]}")
else:
print("Bibliotheques non disponibles : raisonneur ignore.")Avant inference : 51 triplets
Types de rex avant inference :
- Dog
Apres inference : 148 triplets (+97 inferes)
Types de rex apres inference :
- Animal
- Dog
- Mammal
Types de coco apres inference :
- Animal
- Bird
- Parrot
Interpretation : Raisonneur RDFS owlrl
Le DeductiveClosure(RDFS_Semantics) a enrichi le graphe automatiquement. Le nombre de triplets inferes est nettement plus grand que dans la demo manuelle car owlrl applique l’ensemble des règles W3C, pas seulement rdfs9 :
| Règle W3C | Effet materialise |
|---|---|
| rdfs9 | rex rdf:type Mammal, rex rdf:type Animal, etc. |
| rdfs3 | Les objets des proprietes typees xsd:string deviennent des litteraux string |
| rdfs2 | Les sujets de ex:name etc. sont types ex:Animal (même si non declares) |
| rdfs11 | Dog rdfs:subClassOf Animal (transitivite de la hiérarchie) |
| axiomatique | Tout est aussi rdfs:Resource, rdf:type est typing, etc. |
| Instance | Types explicites | Types inferes (animaux) |
|---|---|---|
rex |
Dog | Mammal, Animal |
minou |
Cat | Mammal, Animal |
coco |
Parrot | Bird, Animal |
buddy |
Dog | Mammal, Animal |
Comparaison : - Sans inference : seul le type le plus spécifique est present - Avec inference : tous les super-types sont materialises dans le graphe
Note technique : owlrl materialise les triplets inferes directement dans le graphe. C’est une approche de materialisation (par opposition au raisonnement a la requête). C’est exactement la même philosophie que
StaticRdfsReasoneren dotNetRDF.
6. Interroger les données enrichies par l’inference
Maintenant que le graphe a ete enrichi par owlrl, utilisons SPARQL pour interroger les données inferees. rdflib integre un moteur SPARQL natif : graph.query(sparql_string) retourne un iterable de lignes de résultats.
if LIBS_AVAILABLE:
# Requete SPARQL : trouver tous les animaux (grace a l'inference)
# NB : le FILTER restreint ?type aux classes de notre namespace animals# pour
# exclure le typing axiomatique rdfs:Resource qu'owlrl materialise aussi.
query1 = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?animal ?name ?type
WHERE {
?animal rdf:type ex:Animal .
?animal ex:name ?name .
?animal rdf:type ?type .
FILTER(STRSTARTS(STR(?type), "http://example.org/animals#") && ?type != ex:Animal)
}
ORDER BY ?name ?type
"""
print("Tous les animaux (grace a l'inference rdf:type Animal) :")
print("-" * 60)
rows = list(animal_graph.query(query1))
for row in rows:
name = str(row.name)
type_uri = str(row.type)
type_name = type_uri.split("#")[-1]
print(f" {name:10s} (type: {type_name})")
print(f"\n({len(rows)} lignes de resultat)")
else:
print("Bibliotheques non disponibles : requete SPARQL ignoree.")Tous les animaux (grace a l'inference rdf:type Animal) :
------------------------------------------------------------
Buddy (type: Dog)
Buddy (type: Mammal)
Coco (type: Bird)
Coco (type: Parrot)
Minou (type: Cat)
Minou (type: Mammal)
Rex (type: Dog)
Rex (type: Mammal)
(8 lignes de resultat)
La requête ?animal rdf:type ex:Animal retourne tous les animaux, y compris ceux qui sont seulement types comme Dog, Cat ou Parrot. C’est la puissance de l’inference RDFS : on interroge a un niveau d’abstraction eleve sans avoir a enumerer toutes les sous-classes.
if LIBS_AVAILABLE:
# Requete 2 : trouver tous les mammiferes (chiens ET chats, par inference)
query2 = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?animal ?name ?sound
WHERE {
?animal rdf:type ex:Mammal .
?animal ex:name ?name .
OPTIONAL { ?animal ex:sound ?sound . }
}
ORDER BY ?name
"""
print("Tous les mammiferes (Dogs + Cats, grace a l'inference) :")
print("-" * 60)
for row in animal_graph.query(query2):
name = str(row.name)
sound = str(row.sound) if row.sound is not None else "(inconnu)"
print(f" {name:10s} : {sound}")
print()
# Requete 3 : compter les instances par classe
# NB : on nomme les variables ?cls (pas ?class, mot-cle Python) et ?n
# (pas ?count, qui collide avec ResultRow.count -- la methode de tuple Row).
query3 = """
PREFIX ex: <http://example.org/animals#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?cls (COUNT(DISTINCT ?instance) AS ?n)
WHERE {
?cls a rdfs:Class .
?instance a ?cls .
FILTER(STRSTARTS(STR(?cls), "http://example.org/animals#"))
}
GROUP BY ?cls
ORDER BY DESC(?n)
"""
print("Nombre d'instances par classe (avec inference) :")
print("-" * 50)
for row in animal_graph.query(query3):
cls_name = str(row.cls).split("#")[-1]
n = str(row.n)
print(f" {cls_name:10s} : {n} instance(s)")
else:
print("Bibliotheques non disponibles : requetes SPARQL ignorees.")Tous les mammiferes (Dogs + Cats, grace a l'inference) :
------------------------------------------------------------
Buddy : Woof woof
Minou : Miaou
Rex : Woof
Nombre d'instances par classe (avec inference) :
--------------------------------------------------
Animal : 4 instance(s)
Mammal : 3 instance(s)
Dog : 2 instance(s)
Bird : 1 instance(s)
Cat : 1 instance(s)
Parrot : 1 instance(s)
Interpretation : Requêtes sur données inferees
| Classe | Instances directes | Instances inferees | Total |
|---|---|---|---|
| Animal | 0 | 4 (rex, minou, coco, buddy) | 4 |
| Mammal | 0 | 3 (rex, minou, buddy) | 3 |
| Bird | 0 | 1 (coco) | 1 |
| Dog | 2 (rex, buddy) | 0 | 2 |
| Cat | 1 (minou) | 0 | 1 |
| Parrot | 1 (coco) | 0 | 1 |
Points cles : 1. La classe Animal n’a aucune instance directe mais 4 par inference 2. Les requêtes SPARQL fonctionnent sur le graphe materialise, incluant les triplets inferes 3. C’est un avantage majeur de RDFS : les données sont flexibles mais interrogeables a différents niveaux d’abstraction
Note : la clause
FILTER(STRSTARTS(STR(?class), "http://example.org/animals#"))restreint le comptage aux classes de notre namespace. Sans elle, owlrl a aussi materialise des triplets axiomatiques commerex rdf:type rdfs:Resourcequi gonfleraient les comptes.
7. Comparaison cross-language : dotNetRDF vs rdflib + owlrl
Le notebook SW-6 (C#) et le present notebook (Python) traitent du même domaine (taxonomie animale RDFS) avec des moteurs RDFS reels equivalents. Cette section repertorie les différences fines observees entre les deux stacks – c’est la valeur ajoutée du miroir Python.
Synthese operationnelle
| Aspect | dotNetRDF (SW-6 C#) | rdflib + owlrl (present) |
|---|---|---|
| API graphe | Graph.Assert(new Triple(s, p, o)) |
g.add((s, p, o)) – tuple Python |
| Parsing Turtle | TurtleParser().Load(g, "file.ttl") |
g.parse("file.ttl", format="turtle") |
| SPARQL | LeviathanQueryProcessor + SparqlQueryParser |
g.query(sparql_string) – one-liner |
| Raisonneur RDFS | StaticRdfsReasoner.Apply(g) (une ligne) |
owlrl.DeductiveClosure(RDFS_Semantics).expand(g) |
| Couverture des règles | Règles RDFS core (rdfs2/3/5/7/9/11) | RDFS Semantics complet (RDF 1.1) |
| Materialisation | Mutations in-place du graphe | Mutations in-place du graphe (même philosophie) |
| Rapport d’exécution | Aucun (silencieux) | Aucun (silencieux) ; closure.closure_ok disponible |
Difficultes rencontrees (et comment les contourner)
| Difficulte | Solution cote Python |
|---|---|
L’API owlrl.RDFSClosure.RDFSClosure(g) n’existe pas dans owlrl 7.x (pourtant citee dans d’anciens tutoriels) |
Utiliser owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g) – l’API moderne |
rdflib Literal vs litteral Turtle 5 (entier non type) |
rdflib devine XSD.integer au parsing ; pour les assertions en code, utiliser Literal(5, datatype=XSD.integer) |
| Les prefixes dans la sortie SPARQL | Iterer sur row.varname donne des objets rdflib natifs ; formater avec .split("#")[-1] |
owlrl ajoute aussi des triplets axiomatics (rdfs:Resource, etc.) |
Filtrer par namespace STRSTARTS(STR(?x), "http://example.org/animals#") dans SPARQL |
Ergonomie comparee
L’API rdflib est plus pythonique (tuples, itérations naturelles, namespaces W3C comme constantes), tandis que dotNetRDF est plus typee (IUriNode vs URIRef, Triple objet vs tuple). Pour du raisonnement, owlrl est plus verbeux a invoquer (3 symboles : DeductiveClosure + RDFS_Semantics + expand) mais couvre davantage de règles W3C que le StaticRdfsReasoner de dotNetRDF qui se limite au coeur RDFS.
Choix pratique : pour un cours Python-first de Web Sémantique, le binome rdflib + owlrl est autonomme et couvre tout le programme RDFS/OWL-RL. Pour un cours .NET, dotNetRDF integre raisonneur et SPARQL dans la même bibliotheque, ce qui evite une dépendance supplementaire.
Exercices
Les trois exercices ci-dessous sont a completer. Le notebook doit pouvoir s’executer de bout en bout même si les exercices restent vides (les stubs utilisent print("Exercice a completer") – jamais d’erreur volontaire).
Exercice 1 : Créer un vocabulaire RDFS pour une bibliotheque
Créez un schema RDFS pour un domaine de bibliotheque avec : - Classes : Publication, Book, Article, Author, Publisher - Hiérarchie : Book et Article sont des sous-classes de Publication - Proprietes : title (domain: Publication, range: string), writtenBy (domain: Publication, range: Author), publishedBy (domain: Book, range: Publisher), year (domain: Publication, range: integer) - Instances : au moins 2 livres et 1 article avec leurs auteurs - Appliquez le raisonneur owlrl et verifiez que les types sont bien inferes (un Book devient aussi une Publication).
Indices : - Inspirez-vous de la section 3 : g.add((EX.Book, RDFS.subClassOf, EX.Publication)) declare la hiérarchie. - Pour les proprietes : g.add((EX.title, RDF.type, RDF.Property)) + g.add((EX.title, RDFS.domain, EX.Publication)) + g.add((EX.title, RDFS.range, XSD.string)). - Pour le raisonnement : owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g) puis verifiez qu’une instance de Book a bien aussi rdf:type Publication.
# Exercice 1 : Vocabulaire RDFS pour une bibliotheque
# TODO etudiant : construire le schema, les instances, appliquer owlrl
#
# Etape 1 : creer le graphe et le namespace EX = Namespace("http://example.org/library#")
# Etape 2 : declarer les 5 classes (Publication, Book, Article, Author, Publisher)
# Etape 3 : declarer la hierarchie (Book < Publication, Article < Publication)
# Etape 4 : declarer les 4 proprietes avec domain/range
# Etape 5 : creer 2 instances de Book et 1 instance de Article
# Etape 6 : appliquer owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g)
# Etape 7 : verifier qu'un livre a bien rdf:type Publication (en plus de Book)
print("Exercice 1 a completer : vocabulaire RDFS pour une bibliotheque")Exercice 1 a completer : vocabulaire RDFS pour une bibliotheque
Exercice 2 : Requête SPARQL avec inference RDFS
Les sections précédentes ont montre le raisonneur owlrl et les requêtes SPARQL. Reproduisez le pattern complet sur le fichier data/animals.ttl :
Indices : - Chargez data/animals.ttl dans un graphe frais avec g.parse(...). - Appliquez owlrl : owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g). - Ecrivez une requête SPARQL qui retourne tous les animaux dont le cri contient la lettre “o” (indice : FILTER(CONTAINS(LCASE(?sound), "o"))). - Verifiez que le résultat inclut aussi bien rex (Dog) que buddy (Dog) et coco (Parrot) – tous trois typés Animal par inference, même si rex et buddy sont declares explicitement comme Dog dans le fichier source.
# Exercice 2 : Requete SPARQL sur donnees enrichies par owlrl
# TODO etudiant : charger animals.ttl, appliquer owlrl, executer une SPARQL
#
# Etape 1 : g = Graph() ; g.parse("data/animals.ttl", format="turtle")
# Etape 2 : owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g)
# Etape 3 : ecrire une SPARQL qui trouve tous les animaux (rdf:type ex:Animal)
# dont le cri (ex:sound) contient la lettre "o"
# Etape 4 : afficher les resultats (au moins rex ET buddy, qui sont Animal par inference)
print("Exercice 2 a completer : requete SPARQL sur donnees enrichies par owlrl")Exercice 2 a completer : requete SPARQL sur donnees enrichies par owlrl
Exercice 3 : Chaîne d’inference profonde (transitivite)
Créez une hiérarchie de classes plus profonde pour tester la transitivite de rdfs:subClassOf : - LivingThing > Animal > Vertebrate > Mammal > Primate > Human - Créez une instance alice rdf:type Human - Affichez les types de alice avant inference - Appliquez owlrl et affichez les types de alice après inference - Verifiez que alice est bien typee par les 6 classes de la hiérarchie (LivingThing, Animal, Vertebrate, Mammal, Primate, Human)
Indices : - Une boucle for i in range(len(chain) - 1): g.add((chain[i+1], RDFS.subClassOf, chain[i])) permet de declarer la chaîne en une ligne. - Après expand(g), parcourez g.triples((alice, RDF.type, None)) et collectez les noms de classes. - owlrl materialise aussi le typing rdfs:Resource ; filtrez par namespace http://example.org/biology# pour ne garder que vos classes.
# Exercice 3 : Chaine d'inference profonde
# TODO etudiant : hierarchie profonde, instance alice, owlrl, verification
#
# Etape 1 : g = Graph() ; BIO = Namespace("http://example.org/biology#")
# Etape 2 : declarer la chaine LivingThing > Animal > Vertebrate > Mammal > Primate > Human
# Etape 3 : creer alice rdf:type Human
# Etape 4 : afficher les types AVANT inference (1 seul : Human)
# Etape 5 : appliquer owlrl.DeductiveClosure(owlrl.RDFS_Semantics).expand(g)
# Etape 6 : afficher les types APRES inference (6 classes attendues)
print("Exercice 3 a completer : chaine d'inference profonde")Exercice 3 a completer : chaine d'inference profonde
Resume
Vocabulaire RDFS essentiel
| Terme | Utilisation | Exemple |
|---|---|---|
rdfs:Class |
Declarer une classe | ex:Dog rdf:type rdfs:Class |
rdfs:subClassOf |
Hiérarchie de classes | ex:Dog rdfs:subClassOf ex:Mammal |
rdfs:subPropertyOf |
Hiérarchie de proprietes | ex:father rdfs:subPropertyOf ex:parent |
rdfs:domain |
Type du sujet | ex:name rdfs:domain ex:Animal |
rdfs:range |
Type de l’objet/valeur | ex:name rdfs:range xsd:string |
rdfs:label |
Etiquette lisible | ex:Dog rdfs:label "Chien"@fr |
rdfs:comment |
Description | ex:Dog rdfs:comment "Un canide domestique" |
Ce que nous avons appris
- RDFS enrichit RDF avec un vocabulaire pour decrire la structure des données
- Les hiérarchies de classes (
rdfs:subClassOf) permettent l’heritage de type - L’inference RDFS deduit automatiquement des triplets implicites (règles rdfs2/3/5/9/11 du W3C)
- owlrl.DeductiveClosure(RDFS_Semantics).expand(g) materialise les triplets inferes dans le graphe – equivalent Python du
StaticRdfsReasonerde dotNetRDF - SPARQL sur données inferees permet d’interroger a différents niveaux d’abstraction
Equivalences dotNetRDF / rdflib + owlrl
| Opération | dotNetRDF (SW-6 C#) | rdflib + owlrl (present) |
|---|---|---|
| Ajouter un triplet | g.Assert(new Triple(s, p, o)) |
g.add((s, p, o)) |
| Litteral avec langue | g.CreateLiteralNode("Chien", "fr") |
Literal("Chien", lang="fr") |
| Charger Turtle | TurtleParser().Load(g, file) |
g.parse(file, format="turtle") |
| SPARQL | LeviathanQueryProcessor + parser |
g.query(sparql_string) |
| Raisonneur RDFS | StaticRdfsReasoner.Apply(g) |
owlrl.DeductiveClosure(RDFS_Semantics).expand(g) |
Limites de RDFS
RDFS est volontairement simple. Il ne permet pas de : - Définir des classes disjointes (Dog et Cat ne peuvent pas etre la même instance) - Exprimer des cardinalites (une personne a exactement 2 parents) - Declarer des proprietes inverses (teaches / taughtBy) - Exprimer des classes par union/intersection
Pour ces fonctionnalites, il faut passer a OWL (notebook suivant).
References
- RDF Schema 1.1 – Brickley & Guha, W3C Recommendation (2014). w3.org/TR/rdf-schema
- RDF 1.1 Semantics – Hayes & Patel-Schneider, W3C Recommendation (2014). Specifie formellement les règles d’entailment rdfs2/3/5/9/11. w3.org/TR/rdf11-mt
- rdflib documentation – rdflib.readthedocs.io
- owlrl – Raisonneur RDFS/OWL-RL en Python pur. github.com/RDFLib/OWL-RL
Navigation : Index | << SW-6 C# | SW-7b Python OWL >>