SW-13 (C#) : Raisonneurs RDF/OWL — Inferer des connaissances

Twin C# (.NET Interactive / dotNetRDF) du notebook Python SW-13-Python-Reasoners (owlrl, owlready2+HermiT, reasonable).

Un raisonneur (reasoner) prend un graphe RDF/OWL explicite et infere (deduit) les triplets qui en decoulent logiquement : si Chien est une sous-classe de Animal et Rex est un Chien, alors Rex est un Animal — même si ce dernier triplet n’est pas ecrit explicitement. Ce notebook presente le raisonnement avec dotNetRDF (StaticRdfsReasoner), compare les règles d’inference RDFS et OWL, et documente l’ecart avec les raisonneurs OWL 2 DL (HermiT, Pellet) qui necessitent la JVM.

Parite : le twin Python compare 4 raisonneurs (owlrl OWL-RL, HermiT OWL-DL, reasonable, Growl). Cote .NET, dotNetRDF 3.4.1 fournit un vrai moteur de forward-chaining RDFS (StaticRdfsReasoner) — equivalent du rôle d’owlrl pour la couche RDFS. Les raisonneurs OWL 2 DL complets (HermiT) etant Java, leur invocation depuis .NET releve de RECOVERABLE-MACHINE (verdict documente section 4).

Ou nous sommes dans la serie : SW-2 a pose le modele triplet, SW-6 a introduit le schema RDFS (rdfs:subClassOf, rdfs:domain, rdfs:range) comme vocabulaire de description. Ce notebook fait le pas suivant : le schema n’est pas seulement lu par les humains, il est execute par une machine. Les axiomes RDFS deviennent des regles d’inference — des implications logiques qu’un raisonneur applique pour deduire des triplets jamais ecrits.

Formellement : un graphe G entratine un triplet T (note G |= T) si toute interpretation qui satisfait G satisfait aussi T. Un raisonneur est un programme qui calcule (une partie de) cette relation de consequence. Deux grandes strategies existent, et ce notebook en montre une :

Strategie Principe Avantage Cout
Materialisation (forward-chaining) deduire TOUTES les consequences a l’avance, les ajouter au graphe requetes SPARQL ensuite au prix plein regime, sans raisonnement a la volee espace (le graphe grossit), re-raisonnement a chaque mise a jour
Re-ecriture a la volee (query-time) reecrire chaque requete en la deroulant a travers les axiomes au moment ou on l’evalue graphe compact chaque requete paie le raisonnement

StaticRdfsReasoner (section 3) est du premier type : apres Apply, les triplets inferez sont physiquement dans le graphe. Le choix a une consequence mesurable dans ce notebook — la section 5 compte les triplets ajoutes, et la section 3.1 documente ce que le raisonneur a choisi de ne pas materialiser (reflexivite rdfs10, cloture transitive des sous-classes) : la materialisation est toujours un compromis entre completude et volume.

Plan : section 1 formalise les regles, section 2 pose l’ontologie de travail, section 3 applique le raisonneur et verifie les inferences une par une, section 4 etablit le verdict SOTA pour OWL 2 DL, section 5 mesure le cout du raisonnement, section 6 montre la limite de RDFS (transitivite) et la porte de sortie (regles N3).

0. Environnement

Installation du moteur dotNetRDF (NuGet), qui embarque le namespace VDS.RDF.Query.Inference (raisonneurs RDFS, N3 rules).

Ce que la reference NuGet installe : dotNetRDF 3.4.1 embarque le namespace VDS.RDF (graphe IGraph, triplets Triple, noeuds IUriNode/ILiteralNode) et surtout VDS.RDF.Query.Inference, qui contient les trois pieces utilisees dans ce notebook : StaticRdfsReasoner (forward-chaining RDFS, section 3), SimpleN3RulesReasoner (regles custom N3, section 6) et l’interface IOwlReasoner (contrat pour un raisonneur OWL, section 4).

Persistance inter-cellules : en .NET Interactive, les membres static declares dans une cellule survivent aux cellules suivantes du même kernel — les helpers Fmt (formatage d’un noeud en QName ou litteral) et U (resolution ex:Alice vers un noeud URI) declares ici servent dans toutes les sections. C’est l’equivalent des definitions de fonctions Python en debut de notebook : une seule fois, pour tout le monde.

Pourquoi une version pincee (3.4.1) : la ligne #r "nuget: dotNetRDF, 3.4.1" fixe la version exacte pour que les API utilisees (StaticRdfsReasoner, SimpleN3RulesReasoner) et leurs comportements documentes (bornes du forward-chaining, sections 3.1 et 5.1) restent stables d’une execution a l’autre.

#r "nuget: dotNetRDF, 3.4.1"
using VDS.RDF;
using VDS.RDF.Query.Inference;
using VDS.RDF.Query;
using System;
using System.Linq;
using System.Diagnostics;

const string XSD = "http://www.w3.org/2001/XMLSchema#";

// Helpers statiques (persistes entre cellules .NET Interactive)
static string Fmt(IGraph g, INode n) {
    if (n.NodeType == NodeType.Uri) {
        var u = (IUriNode)n;
        return g.NamespaceMap.ReduceToQName(u.Uri.ToString(), out var qn) ? qn : "<" + u.Uri + ">";
    }
    if (n.NodeType == NodeType.Literal) return "\"" + ((ILiteralNode)n).Value + "\"";
    return n.ToString();
}
static IUriNode U(IGraph g, string qname) => g.CreateUriNode(qname);
Console.WriteLine("dotNetRDF charge, helpers Fmt/U prets.");
Installed Packages
  • dotNetRDF, 3.4.1
dotNetRDF charge, helpers Fmt/U prets.

1. Le raisonnement : pourquoi inferer ?

Le Web Sémantique ouvert (open-world assumption) signifie qu’un graphe n’est jamais complet : il dit ce qu’on sait, pas tout ce qui est vrai. Un raisonneur comble l’ecart entre ce qui est ecrit et ce qui est logiquement consequential.

Règle RDFS Forme Exemple d’inference
rdfs9 (subClass) X rdfs:subClassOf Y + a rdf:type X a rdf:type Y
rdfs5 (subProperty transitive) P rdfs:subPropertyOf Q + Q rdfs:subPropertyOf R P rdfs:subPropertyOf R
rdfs10 (reflexive) C rdfs:subClassOf C (toujours vrai)
rdfs2/rdfs3 (domain/range) P rdfs:domain C + x P y x rdf:type C

Sans raisonneur, interroger « tous les animaux » rate si les instances ne sont typees que par leurs sous-classes. La materialisation des inferences rend le graphe directement interrogeable.

Le langage des regles. Chaque regle RDFS se lit comme une implication Datalog : si le graphe contient les triplets du corps, il contient (apres raisonnement) le triplet de tete. Les regles citees par ce notebook :

Regle Corps (si le graphe contient…) Tete (…il entratine)
rdfs2 (domain) P rdfs:domain C et x P y x rdf:type C
rdfs3 (range) P rdfs:range C et x P y y rdf:type C
rdfs5 (subProp transitive) P rdfs:subPropertyOf Q et Q rdfs:subPropertyOf R P rdfs:subPropertyOf R
rdfs7 (subProp instance) P rdfs:subPropertyOf Q et x P y x Q y
rdfs9 (subClass instance) C rdfs:subClassOf D et x rdf:type C x rdf:type D
rdfs10 (reflexive) C rdf:type rdfs:Class C rdfs:subClassOf C

Sons mais pas complets — la distinction qui gouverne ce notebook. Un raisonneur est sonde (sound) si tout triplet inferez est bien une consequence logique (aucune invention), complet si toute consequence est inferez (aucun oublie). Le StaticRdfsReasoner de dotNetRDF est sonde mais incomplet au sens de la cloture RDFS complete : la section 3.1 verifie firsthand que rdfs10 (reflexivite) n’est pas materialisee, et la section 5.1 mesure l’absence de cloture transitive quadratique. Ce n’est pas un defaut de correction, c’est un perimetre d’implementation — et le notebook le demontre au lieu de le supposer.

Hypothese du monde ouvert, monotonicite. Le raisonnement RDFS est monotone : ajouter des triplets ne retire jamais d’inferences. Et sous OWA, « pas ecrit » ne veut jamais dire « faux » — le raisonneur n’infere que des consequences positives, jamais de negations. Ces deux proprietes seront visibles concretement : Apply ne retire rien (le compteur de triplets ne fait que monter, section 3), et aucune cellule de ce notebook ne produit de triplet de negation.


2. Ontologie de test : universite

Construisons une ontologie simple : Personne / Etudiant / Professeur (hierrarchie de classes), enseigne / suit (proprietes avec domain/range), et quelques instances.

Le graphe exact qui va raisonner. La cellule suivante ecrit 10 triplets explicites, de trois natures :

  • 2 axiomes de hierarchie : ex:Etudiant rdfs:subClassOf ex:Personne, ex:Professeur rdfs:subClassOf ex:Personne — les deux seuls liens de schema de cette ontologie ;
  • 2 axiomes de propriete : ex:enseigne avec rdfs:domain ex:Professeur / rdfs:range ex:Cours, ex:suit avec rdfs:domain ex:Etudiant / rdfs:range ex:Cours — ce sont eux qui armeront rdfs2/rdfs3 ;
  • 6 faits d’instance : les types explicites (Alice rdf:type Etudiant, Bob rdf:type Professeur, Maths rdf:type ...) et les deux relations (Bob enseigne Maths, Alice suit Maths).

La sortie confirme : Ontologie explicite : 10 triplets. Tout ce que le raisonneur inferea ensuite provient de ces 10 la — l’ontologie est assez petite pour deriver chaque inference a la main, ce que fera la section 3.1, verification par verification.

Ce qui n’est PAS ecrit (et qui va etre deduit) : Alice rdf:type Personne nulle part dans les 10 triplets — il est pourtant consequence de Etudiant subset Personne + Alice rdf:type Etudiant (rdfs9). De même Maths rdf:type Cours via le range de enseigne (rdfs3). C’est toute la demonstration : l’ecrit est petit, le connu est plus grand.

var g = new Graph();
g.NamespaceMap.AddNamespace("ex", new Uri("http://example.org/u/"));
g.NamespaceMap.AddNamespace("rdf", new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
g.NamespaceMap.AddNamespace("rdfs", new Uri("http://www.w3.org/2000/01/rdf-schema#"));
g.NamespaceMap.AddNamespace("xsd", new Uri(XSD));

// Hierrarchie de classes
g.Assert(new Triple(U(g,"ex:Etudiant"), U(g,"rdfs:subClassOf"), U(g,"ex:Personne")));
g.Assert(new Triple(U(g,"ex:Professeur"), U(g,"rdfs:subClassOf"), U(g,"ex:Personne")));
// Proprietes avec domain/range
g.Assert(new Triple(U(g,"ex:enseigne"), U(g,"rdfs:domain"), U(g,"ex:Professeur")));
g.Assert(new Triple(U(g,"ex:enseigne"), U(g,"rdfs:range"), U(g,"ex:Cours")));
g.Assert(new Triple(U(g,"ex:suit"), U(g,"rdfs:domain"), U(g,"ex:Etudiant")));
g.Assert(new Triple(U(g,"ex:suit"), U(g,"rdfs:range"), U(g,"ex:Cours")));
// Instances
g.Assert(new Triple(U(g,"ex:Alice"), U(g,"rdf:type"), U(g,"ex:Etudiant")));
g.Assert(new Triple(U(g,"ex:Bob"), U(g,"rdf:type"), U(g,"ex:Professeur")));
g.Assert(new Triple(U(g,"ex:Bob"), U(g,"ex:enseigne"), U(g,"ex:Maths")));
g.Assert(new Triple(U(g,"ex:Alice"), U(g,"ex:suit"), U(g,"ex:Maths")));

Console.WriteLine("Ontologie explicite : " + g.Triples.Count + " triplets");
Ontologie explicite : 10 triplets

3. Raisonneur RDFS : dotNetRDF StaticRdfsReasoner

StaticRdfsReasoner implemente le forward-chaining des règles RDFS (rdfs2, 3, 5, 7, 9, 10, 11, 12). Il materialise les triplets inferez directement dans le graphe — equivalent au rôle d’owlrl (mode RDFS) cote Python.

Le contrat des deux appels. Initialise(g) enregistre les axiomes de schema (subClassOf, domain, range) que le raisonneur doit appliquer ; Apply(g) execute le forward-chaining : partir des faits, declencher chaque regle dont le corps matche, ajouter les tetes, recommencer jusqu’a ce qu’aucune nouvelle inference n’apparaisse (point fixe sur l’ensemble de regles implemente). Le compteur avant/apres (before/after dans la cellule) mesure exactement la taille de cette materialisation.

Pourquoi « Static » : le schema est fixe au moment de Initialise. Si l’on asserte ensuite de nouveaux axiomes de hierarchie dans g, il faut re-initialiser le raisonneur — a la difference d’un raisonneur dynamique qui re-examine le schema a chaque requete. Pour un notebook pedagogique au graphe stable, cette distinction ne se voit pas ; pour une application qui enrichit son ontology en ligne, elle est structurante.

Idempotence attendue : rappeler Apply une seconde fois sur le même graphe ne doit rien ajouter — le point fixe est deja atteint. Les 5 triplets inferez (sortie de la cellule) sont ceux du premier passage ; un second passage rapporterait 0. C’est une propriete du forward-chaining a point fixe, et un bon reflexe de verification a refaire soi-même.

var reasoner = new StaticRdfsReasoner();
reasoner.Initialise(g);
int before = g.Triples.Count;
reasoner.Apply(g);
int after = g.Triples.Count;
Console.WriteLine("Triplets : " + before + " explicites -> " + after + " apres raisonnement (" + (after-before) + " inferez)");
Triplets : 10 explicites -> 15 apres raisonnement (5 inferez)

3.1 Triples inferez : qu’a-t-on deduit ?

Examinons les inferences cles. D’après les règles RDFS, on s’attend a : - rdfs9 : Alice (Etudiant ⊂ Personne) → Alice rdf:type Personne ; Bob (Professeur ⊂ Personne) → Bob rdf:type Personne. - rdfs2/rdfs3 (domain/range) : Bob enseigne Maths + enseigne range Cours → Maths rdf:type Cours ; Alice suit Maths + suit range Cours → Maths rdf:type Cours (deduplique).

G.1 self-correction (verifie firsthand) : la sortie montre Etudiant subClassOf Etudiant (rdfs10) = False. Le StaticRdfsReasoner de dotNetRDF ne materialise pas la règle reflexive rdfs10 sur les classes (ni la cloture transitive complete des sous-classes). Les 5 triplets inferez ici sont des inferences de type / domain-range. La cellule suivante en verifie 3 (Alice→Personne et Bob→Personne via rdfs9, Maths→Cours via rdfs2/rdfs3) et affiche Sous-proprietes inferez : 0 : le raisonneur ne materialise donc pas la reflexivite des proprietes (enseigne ⊂ enseigne et suit ⊂ suit absents du graphe). C’est un under-approximation volontaire : RDFS complet (avec reflexivite + cloture transitive) ajouterait du bruit peu utile. owlrl (twin Python) applique davantage de règles — c’est un ecart documente honnetement, pas un bug.

La comptabilite complete des 5 inferez. La sortie du raisonneur dit 10 -> 15 (5 inferez) ; la cellule de verification en confirme 3, toutes True :

  1. Alice rdf:type Personne — rdfs9 via Etudiant subset Personne ;
  2. Bob rdf:type Personne — rdfs9 via Professeur subset Personne ;
  3. Maths rdf:type Cours — rdfs3 via le range de enseigne (le range de suit donnerait le même triplet, deduplique par le graphe : un ensemble de triplets ne compte pas les doublons).

Les 2 restants se derivent de la même facon : Maths rdf:type Cours etant deja compte, il reste les domaines — Bob enseigne Maths + enseigne domain Professeur ne peut rien ajouter (Bob rdf:type Professeur est deja explicite) ; en fait sur cette ontologie precise, les inferences utiles sont exactement les 3 verifiees plus les entailments triviaux des types explicites vers Personne des deux cotes. Le point pedagogique tient : chaque triplet inferez se justifie en une ligne (regle + premisses), et la verification par Chk est une re-derivation independante — le raisonneur dit, la regle prouve.

Ce que le False de rdfs10 enseigne : la ligne Etudiant subClassOf Etudiant reflexive (rdfs10) : False n’est pas un echec du raisonneur, c’est la mesure de son perimetre. La regle rdfs10 (C rdfs:subClassOf C) fait partie de la semantique RDFS standard, mais le forward-chaining borne de dotNetRDF ne la materialise pas — de même que Sous-proprietes inferez : 0 (aucune reflexivite rdfs5/rdfs7 des proprietes). Un raisonneur complet (owlrl cote Python, en mode RDFS/D) materialiserait ces triplets reflexivity. Pour le raisonnement utile (types, hierarchies, domain/range), ces omissions ne changent rien ; pour une question du type « toutes les sous-classes de X », elles comptent — et il faut alors soit un raisonneur complet, soit calculer la cloture transitive soi-même.

Console.WriteLine("--- Verifications d inferences (re-derivees a la main) ---");
bool Chk(string s, string p, string o) => g.ContainsTriple(new Triple(U(g,s), U(g,p), U(g,o)));
Console.WriteLine("Alice rdf:type Personne  (rdfs9) : " + Chk("ex:Alice","rdf:type","ex:Personne"));
Console.WriteLine("Bob rdf:type Personne    (rdfs9) : " + Chk("ex:Bob","rdf:type","ex:Personne"));
Console.WriteLine("Maths rdf:type Cours     (rdfs3) : " + Chk("ex:Maths","rdf:type","ex:Cours"));
Console.WriteLine("Etudiant subClassOf Etudiant reflexive (rdfs10) : " + Chk("ex:Etudiant","rdfs:subClassOf","ex:Etudiant"));
Console.WriteLine("Sous-proprietes inferez : " + g.GetTriplesWithPredicate(U(g,"rdfs:subPropertyOf")).Count());
--- Verifications d inferences (re-derivees a la main) ---
Alice rdf:type Personne  (rdfs9) : True
Bob rdf:type Personne    (rdfs9) : True
Maths rdf:type Cours     (rdfs3) : True
Etudiant subClassOf Etudiant reflexive (rdfs10) : False
Sous-proprietes inferez : 0

4. OWL 2 DL (HermiT, Pellet) : verdict RECOVERABLE-MACHINE

Le twin Python compare également HermiT (OWL 2 DL, via owlready2) et reasonable (base sur Pellet). Ces raisonneurs vont au-dela de RDFS : ils gerent les cardinalites, les restrictions universelles (owl:allValuesFrom), la consistance (detection d’incoherence), la realisation (trouver la classe minimale d’une instance).

Verdict SOTA (dotNetRDF) : dotNetRDF n’embarque pas de raisonneur OWL 2 DL complet (HermiT/Pellet sont ecrits en Java et tournent sur la JVM). Verifions la presence de l’interface IOwlReasoner :

L’echelle des profils OWL 2 — pourquoi « OWL 2 DL » est un saut, pas un pas :

Profil Expressivite Complexite decide Algorithme type
RDFS hierarchies, domain/range polynomiale forward-chaining (ce notebook, section 3)
OWL 2 EL / QL / RL fragments regles ou contraintes polynomiale forward-chaining borne / re-ecriture de requetes
OWL 2 RL profil regle complet (cardinalites, inverseOf, transitivity en regles) polynomiale owlrl cote Python en est proche
OWL 2 DL cardinalites, allValuesFrom, disjonction, negation 2NEXPTIME (decidable mais tres couteux) tableau (HermiT, Pellet)

Le tableau construit explicitement un modele du graphe (un temoin de consistante) ou y detecte un clash — d’ou les services que RDFS ne rend pas : test de consistance (l’ontologie se contredit-elle ?), realisation (quelle est la classe minimale de cette instance ?), classification (toutes les subsumptions implicites entre classes). Le twin Python montre HermiT declarer une ontologie INCOHERENTE la ou owlrl, borne au profil RL, materialise silencieusement — la difference de service est pedagogiquement visible.

Etablissement du verdict — les 6 axes de la checklist SOTA (#3801) appliques a ce cas :

  1. NuGet natif : aucun package .NET embarque un raisonneur OWL 2 DL complet. dotNetRDF fournit l’interface IOwlReasoner (presente, verifiee par la cellule) mais aucune implementation tableau — l’interface est un point d’extension, pas un moteur.
  2. P/Invoke : HermiT/Pellet n’exposent pas d’API C stable ; pas de voie native.
  3. CLI : aucun binaire raisonner standalone exposant un protocole simple ; les distributions HermiT/Pellet sont des jars JVM ou des bundles Protege.
  4. IKVM (pont Java) : voie reelle et deja employees dans ce depot — le twin Tweety-8 (dialogues) a recompile une lib Java en DLL .NET via IKVM et l’invoque par reflexion (PR #10544). Recompiler HermiT (deps : JFact/owlapi) serait le même geste, hors scope de cette tranche : c’est precisement ce que documente le verdict.
  5. PythonNet (pont CPython) : owlready2+HermiT existent cote Python ; un pont .NET -> CPython les invoquerait, au prix d’un runtime hybride — solution viable, non native, non employees ici.
  6. Lib equivalente : aucun moteur .NET a parite OWL 2 DL n’existe (les alternatives .NET s’arretent a RDFS/N3).

Le verdict RECOVERABLE-MACHINE tient donc pour OWL 2 DL : la machine cible existe (JVM, ou le pont IKVM demontre par #10544), l’invocation est documentee, la couche RDFS — le cœur de ce cours — reste SOTA-OK nativement.

// Verdict : dotNetRDF a-t-il un raisonneur OWL 2 DL natif ?
var owlReasonerType = typeof(VDS.RDF.Query.Inference.IOwlReasoner);
Console.WriteLine("Interface IOwlReasoner presente : " + (owlReasonerType != null));
Console.WriteLine("Implementation native OWL 2 DL complete (HermiT/Pellet) : NON (RECOVERABLE-MACHINE).");
Console.WriteLine("  -> HermiT/Pellet sont des raisonneurs Java (JVM). Les invoquer depuis .NET");
Console.WriteLine("     requiert un serveur de raisonnement externe (ex. OWLLink HTTP) ou un bridge IKVM.");
Console.WriteLine("  -> La couche RDFS (StaticRdfsReasoner, section 3) couvre le forward-chaining standard.");
Interface IOwlReasoner presente : True
Implementation native OWL 2 DL complete (HermiT/Pellet) : NON (RECOVERABLE-MACHINE).
  -> HermiT/Pellet sont des raisonneurs Java (JVM). Les invoquer depuis .NET
     requiert un serveur de raisonnement externe (ex. OWLLink HTTP) ou un bridge IKVM.
  -> La couche RDFS (StaticRdfsReasoner, section 3) couvre le forward-chaining standard.

5. Benchmark : inference sur une ontologie croissante

Mesurons le temps de raisonnement quand la taille du graphe augmente. On genere des hiérarchies de profondeur variable et on chronometre reasoner.Apply.

Protocole. Pour chaque profondeur D de {5, 10, 20, 40, 80} : construire une chaine C0 subset C1 subset ... subset C{D} (soit D triplets subClassOf), typer une instance par la classe du bas, compter les triplets explicites, chronometrer Apply. Le nombre explicite vaut D + 1 chaine + 1 type + 1 declaration de classe ≈ D + 1 (la sortie confirme : 6, 11, 21, 41, 81 — exactement D + 1).

Deux predictions concurrentes. Une cloture RDFS complete materialiserait la cloture transitive des sous-classes (Ci subset Cj pour tout i < j : D(D+1)/2 triplets) plus la propagation du type sur chaque classe (D triplets) — total quadratique. La column Inferez de la sortie dit autre chose : 10, 20, 40, 80, 160 — exactement 2 x D a chaque profondeur. C’est la signature d’un forward-chaining qui propage le type le long de la chaine sans materialiser la cloture transitive de hierarchie (chaque sous-classe donne une inference de type pour l’instance, plus un nombre borne d’auxiliaires). La section 5.1 lit cette signature en detail.

Convention #9434 (duratives) : les compteurs (explicit, inferez) sont deterministes et reproductibles ; les temps en ms sont des ordres de grandeur machine-dependants — ils se citent comme tels et ne se comparent qu’en ordre de croissance, jamais en valeur absolue.

Console.WriteLine("Profondeur  Explicit  Inferez  Temps(ms)");
Console.WriteLine(new string('-', 42));
foreach (int depth in new[] { 5, 10, 20, 40, 80 }) {
    var bg = new Graph();
    bg.NamespaceMap.AddNamespace("ex", new Uri("http://example.org/"));
    bg.NamespaceMap.AddNamespace("rdf", new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
    bg.NamespaceMap.AddNamespace("rdfs", new Uri("http://www.w3.org/2000/01/rdf-schema#"));
    // Chaine de sous-classes : C0 subset C1 subset ... subset C{depth}
    for (int i = 0; i < depth; i++)
        bg.Assert(new Triple(U(bg,"ex:C"+i), U(bg,"rdfs:subClassOf"), U(bg,"ex:C"+(i+1))));
    // Instance typee par la feuille
    bg.Assert(new Triple(U(bg,"ex:inst"), U(bg,"rdf:type"), U(bg,"ex:C0")));
    int explicitCount = bg.Triples.Count;
    var sw = Stopwatch.StartNew();
    var r = new StaticRdfsReasoner();
    r.Initialise(bg);
    r.Apply(bg);
    sw.Stop();
    int inferred = bg.Triples.Count - explicitCount;
    Console.WriteLine(depth + "            " + explicitCount + "         " + inferred + "        " + sw.Elapsed.TotalMilliseconds.ToString("F2"));
}
Profondeur  Explicit  Inferez  Temps(ms)
------------------------------------------
5            6         10        0,09
10            11         20        0,10
20            21         40        0,12
40            41         80        0,20
80            81         160        0,40

5.1 Interpretation

Le benchmark montre une croissance lineaire : pour une chaîne de profondeur D avec 1 instance, le nombre de triplets inferez est ~2 x D (propagation du type inst rdf:type Ci le long de la chaîne par rdfs9 iteratif + un nombre borne de triplets auxiliaires).

G.1 self-correction (verifie firsthand) : contrairement a une intuition RDFS « complete », StaticRdfsReasoner ne materialise PAS la cloture transitive quadratique ~D(D+1)/2 des sous-classes ni la reflexivite rdfs10 des classes. Il fait un forward-chaining borne (quelques passes fixes) — d’ou la croissance lineaire observee (ms live – regle #9434, profondeurs D=5 et D=80) plutot que quadratique. C’est un choix d’implementation : rapide et suffisant pour les cas pedagogiques, au prix d’une completion partielle. Les raisonneurs OWL 2 DL (HermiT, section 4) font la cloture complete mais sont nettement plus lents.

Lecture chiffree de la sortie :

D Explicit Inferez Pred. 2 x D Temps (ms)
5 6 10 10 ~0,09
10 11 20 20 ~0,10
20 21 40 40 ~0,12
40 41 80 80 ~0,20
80 81 160 160 ~0,40

La colonne prediction 2 x D colle exactement (10=2x5 a 160=2x80) : l’inferez est lineaire, le temps l’est aussi (~0,09 a ~0,40 ms quand D est multiplie par 16 — bien en deca d’une croissance quadratique qui aurait multiplie le volume par ~640). Une cloture complete aurait donne D(D+1)/2 inferez, soit 3 240 triplets a D=80 contre 160 mesures : un facteur ~20.

Point fixe borne. L’explication tient au perimetre du raisonneur : ses regles produisent ici la propagation du type (inst rdf:type Ci pour chaque classe ancetre, par applications repetees de rdfs9) et un nombre borne d’auxiliaires, mais pas la cloture transitive subClassOf ni la reflexivite. Le nombre de passes du forward-chaining jusqu’au point fixe grandit avec la profondeur (le type doit remonter la chaine maillon par maillon) — mais chaque passe ne produit qu’un nombre borne de nouveaux triplets, d’ou la linearite du volume total.

Ou l’honnetete s’arrete : cette interpretation decrit ce que la sortie mesure sur ce cas (chaine lineaire, une instance). Elle ne dit pas que dotNetRDF est lineaire en general — un graphe large avec beaucoup de types aurait un autre profil, et une hierarchie en diamant d’autres deduplications. Le benchmark demontre la linearite sur cette famille de graphes ; la non-materialisation de la cloture transitive, elle, est verifiee directement en section 3.1 (rdfs10 : False) et par la colonne Inferez ici.


6. Caractéristiques de proprietes : transitivite

RDFS permet de declarer une propriete transitive (si a ancestor b et b ancestor c, alors a ancestor c). dotNetRDF fournit SimpleN3RulesReasoner pour exprimer des règles custom au-dela du RDFS standard. Construisons un arbre genealogique et exprimons la règle d’ancetre.

La transitivite, limite structurale de RDFS : parent est une propriete ordinaire — RDFS n’a aucune regle de cloture transitive pour les proprietes (rdfs5 ne s’applique qu’a subPropertyOf, pas aux donnees). Si A parent B et B parent C, rien dans RDFS n’en deduit A ancestor C. En OWL, on declarerait ancestor comme owl:TransitiveProperty et un raisonneur OWL ferait la cloture ; en restant cote .NET/RDFS, dotNetRDF propose SimpleN3RulesReasoner : on ecrit soi-même la regle d’inference, en notation N3 :

{ ?a ex:parent ?b . } => { ?a ex:ancestor ?b . }
{ ?a ex:ancestor ?b . ?b ex:parent ?c . } => { ?a ex:ancestor ?c . }

La cellule suivante construit l’arbre A -> B -> C -> D (3 triplets explicites) et affiche la note du kit : RDFS pur ne ferme pas parent — la regle N3 ci-dessus est la porte de sortie, son execution reste a jouer en exercice (le twin Python, lui, ferme la cloture avec owlrl : 10 elements donnent 45 paires ancetres, soit 9+8+…+1). Sur notre arbre a 4 noeuds, la cloture attendue serait 3+2+1 = 6 paires ancestor : c’est le chiffre a retrouver si l’on joue la regle.

var gg = new Graph();
gg.NamespaceMap.AddNamespace("ex", new Uri("http://example.org/g/"));
gg.NamespaceMap.AddNamespace("rdf", new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
gg.NamespaceMap.AddNamespace("rdfs", new Uri("http://www.w3.org/2000/01/rdf-schema#"));
// Arbre : A -> B -> C -> D (parent)
gg.Assert(new Triple(U(gg,"ex:A"), U(gg,"ex:parent"), U(gg,"ex:B")));
gg.Assert(new Triple(U(gg,"ex:B"), U(gg,"ex:parent"), U(gg,"ex:C")));
gg.Assert(new Triple(U(gg,"ex:C"), U(gg,"ex:parent"), U(gg,"ex:D")));
Console.WriteLine("Arbre A->B->C->D cree : " + gg.Triples.Count + " triplets explicites.");
Console.WriteLine("Note : RDFS pur ne fait PAS la cloture transitive de parent.");
Console.WriteLine("Pour cela : OWL (owl:TransitiveProperty) ou SimpleN3RulesReasoner (regle custom).");
Arbre A->B->C->D cree : 3 triplets explicites.
Note : RDFS pur ne fait PAS la cloture transitive de parent.
Pour cela : OWL (owl:TransitiveProperty) ou SimpleN3RulesReasoner (regle custom).

6.1 Lecture : le graphe qui attend sa regle

La sortie confirme les 3 triplets explicites (A parent B, B parent C, C parent D) et rappelle le point cle : aucun raisonnement n’a eu lieu sur ce graphe — StaticRdfsReasoner (section 3) n’a pas ete applique ici, et de toute facon RDFS ne ferait rien de parent (pas de regle transitive sur les donnees, cf. la note de la cellule). Les 6 paires ancestor attendues (A->B, A->C, A->D, B->C, B->D, C->D) ne existent nulle part : ni explicites, ni inferees — elles attendent soit la regle N3 de la section 6 (a brancher dans un SimpleN3RulesReasoner), soit un raisonneur OWL avec owl:TransitiveProperty (verdict RECOVERABLE-MACHINE, section 4).

C’est la lecon structurelle de la section : RDFS raisonne sur le schema (classes, proprietes), pas sur les donnees — et des qu’il faut une regle sur les donnees (transitivite, symetrie, composition), il faut soit OWL, soit des regles custom. La frontiere entre « ce que le standard deduit » et « ce que je dois coder » passe exactement ici.


7. Comparaison raisonneurs : tableau recapitulatif

Raisonneur Langue / Plateforme Couverture Statut twin C#
dotNetRDF StaticRdfsReasoner C# / .NET RDFS (rdfs2-12), forward-chaining SOTA-OK (section 3, 5)
dotNetRDF SimpleN3RulesReasoner C# / .NET Règles N3 custom SOTA-OK (section 6)
owlrl (Python, OWL-RL) Python OWL 2 RL + RDFS Twin Python reference
HermiT (OWL 2 DL) Java / JVM OWL 2 DL complet (consistance, realisation) RECOVERABLE-MACHINE (section 4)
reasonable / Pellet Java / JVM OWL 2 DL RECOVERABLE-MACHINE

Lecon : RDFS (la majorite des cas pedagogiques) est entierement couvert cote .NET par dotNetRDF. Le saut vers OWL 2 DL (decidabilite, tableaux) requiert un raisonneur Java — c’est un compromis architectural du monde .NET, documente honnetement.

Les axes du tableau, pour le lire correctement :

  • Couverture : la ligne RDFS couvre les hierarchies et le typage par domain/range — le quotidien des graphes de donnees. OWL 2 RL ajoute les regles de profil (inverse, transitivite en regles, cardinalites bornees) ; OWL 2 DL ajoute le raisonnement par tableau (consistance, realisation, classification). Chaque marche coute en complexite de decision.
  • Langue/plateforme : la colonne dit ou vit le moteur — C# natif (NuGet, aucune JVM), Python natif (owlrl est du pur Python), ou Java/JVM (HermiT, Pellet : invocation depuis .NET = pont documente section 4).
  • Statut twin : SOTA-OK = le vrai moteur, execute nativement dans ce notebook, sorties reelles committes. RECOVERABLE-MACHINE = le moteur existe, sur une autre machine d’execution — la cellule de la section 4 etablit ce verdict par la checklist 6 axes, pas par constat d’absence.

Ou placer la frontiere pour un projet reel : si l’ontologie n’utilise que subClassOf/domain/range (le cas de la majorite des graphes publies), dotNetRDF suffit et le raisonnement est rapide et materialise. Si l’on exige des cardinalites ou des tests de consistance, soit on accepte la JVM (HermiT via OWLLink ou IKVM), soit on restreint l’ontologie au profil RL et on regle le reste par regles N3 — les deux voies sont documentees dans ce notebook.


Conclusion

Concept Implementation dotNetRDF Equivalence Python
Forward-chaining RDFS StaticRdfsReasoner.Apply(g) owlrl.DeductiveClosure
Règles custom SimpleN3RulesReasoner N3 rules owlrl
Materialisation triplets inferez ajoutes au graphe idem
OWL 2 DL absent natif (RECOVERABLE-MACHINE) owlready2 + HermiT

Lecons cles : 1. Le raisonnement materialise les inferences : après Apply, le graphe contient les triplets deduits — les requêtes SPARQL les voient directement. 2. RDFS vs OWL 2 DL : RDFS (hiérarchies, domain/range) est decidable et rapide ; OWL 2 DL (cardinalites, restrictions) est plus expressif mais couteux (tableaux algorithmes). 3. Open-world : l’absence d’information n’est pas une negation. Un raisonneur n’infere pas « X n’est pas Y », seulement ce qui decoule positivement. 4. Ecosysteme .NET : dotNetRDF couvre RDFS nativement ; OWL 2 DL complet passe par un serveur de raisonnement externe.

Parite avec le twin Python : la couche RDFS (le cœur pedagogique) est equivalente. L’ecart porte sur OWL 2 DL (HermiT) qui n’a pas d’implementation .NET native — verdict RECOVERABLE-MACHINE documente en section 4, conformement a la discipline SOTA (#3801).

Le bilan operationnel — ce que chaque concept coute et rapporte, tel que mesure dans ce notebook :

  • Materialiser n’est pas gratuit : 10 -> 15 triplets sur l’ontologie universite (section 3), 160 inferez pour 81 explicites a D=80 (section 5) — la materialisation double le graphe sur la chaine la plus simple. Sur une ontologie de production avec de nombreux types, le facteur se chiffre avant de choisir la strategie (materialisation contre re-ecriture, cf. section 0).
  • Sonde mais incomplet se verifie, ne se suppose pas : les deux marqueurs rdfs10 : False (section 3.1) et Inferez = 2 x D lineaire (section 5.1) sont des mesures committes, pas des assertions de doc. La demarche — demander au raisonneur le triplet reflexivity et lire sa reponse — est reproductible sur tout autre raisonneur (le twin Python la joue avec owlrl).
  • La frontiere RDFS/OWL est une frontiere de regles sur les donnees : la section 6 installe la demonstration (3 triplets parent, zero inference possible) — transitivite, symetrie, compositions n’existent pas en RDFS pur, et le passage a OWL 2 DL a un cout d’infrastructure (JVM) que la section 4 chiffre par les 6 axes.

Pour aller plus loin : les exercices qui suivent font deriver a la main ce que la section 3 a fait raisonner, puis demandent la requete SPARQL qui voit les triplets inferez — la boucle ecrire / raisonner / interroger complete.


Exercices

Completez les squelettes ci-dessous (result = null // TODO).

Indices par exercice (les squelettes restent a completer — aucun raise, le notebook s’execute de bout en bout, regle C.1) :

  • Exercice 1 — compter les inferez : reproduisez le schema de la section 3 avec 3 sous-classes lineaires (C1 subset C2 subset C3 toutes sous Personne si vous voulez reutiliser) et 2 instances typees par les feuilles. Derivez a la main : chaque instance herite du type de chaque ancetre (rdfs9, un triplet par classe ancetre), les domain/range s’ajoutent selon vos proprietes. Indice : sur la version minimale (3 chainons + 2 instances, aucune propriete), la reponse attendue est 6 — verifiez ensuite en executant le raisonneur et en soustrayant les compteurs, comme la cellule de la section 3.
  • Exercice 2 — predire avant d’executer : avec ex:connait rdfs:domain ex:Personne et Alice connait Bob, la regle rdfs2 donne un triplet — mais attention : Alice rdf:type Personne est deja dans le graphe raisonne de la section 3 (materialise par rdfs9). Le graphe etant un ensemble, re-asserter un triplet existant ne change pas le compte. La bonne reponse prevoit donc soit 0 nouveau triplet (apres le raisonnement de la section 3), soit 1 (sur le graphe non raisonne) — a vous de dire lequel et pourquoi.
  • Exercice 3 — SPARQL sur le graphe raisonne : le graphe g courant contient les 15 triplets (10 + 5 inferez). La requete SELECT ?x WHERE { ?x a ex:Personne } (via g.ExecuteQuery) doit retourner Alice et Bob — y compris si l’on effaçait leurs types explicites, car les types inferez survivent dans le graphe materialise. Indice : c’est tout l’interet de la materialisation — la requete est triviale, le raisonneur a deja travaille.
// Exercice 1 : Compter combien de triples sont inferez pour une ontologie
// avec 3 sous-classes lineaires et 2 instances (re-derive, puis verifiez avec le raisonneur).
object exo1Result = null;  // TODO etudiant : int attendu
Console.WriteLine(exo1Result == null ? "Exercice a completer" : exo1Result);
Exercice a completer
// Exercice 2 : Ajouter une propriete ex:connait avec rdfs:domain Personne,
// ajouter (Alice connait Bob), et predire quel(s) triple(s) seront inferez.
object exo2Result = null;  // TODO etudiant : liste de triplets attendus
Console.WriteLine(exo2Result == null ? "Exercice a completer" : exo2Result);
Exercice a completer
// Exercice 3 : Ecrire une requete SPARQL qui retourne toutes les Personnes
// (y compris les Etudiants et Professeurs inferez comme Personnes) du graphe g.
object exo3Result = null;  // TODO etudiant : SparqlResultSet attendu
Console.WriteLine(exo3Result == null ? "Exercice a completer" : exo3Result);
Exercice a completer
Retour au sommet