This notebook will show you some simple inferences that are possible to do with an ontology, and how to do them with owlready2 -- however, the overall approach is applicable to other toolchains.
An ontology is a repository of knowledge formally expressed in some machine-readable language. Typically, this language is some flavor of "description logic", which is a mathematical language used to express definitions of classes and relations between individuals belonging to those classes.
For our example, we will have a small ontology that encodes knowledge about food -- specifically, what ingredients or substances go into various dishes, and whether those dishes are appropriate or not for people with various medical conditions and/or dietary preferences.
Consider the following hypothetical scenario: a smart block of flats provides assisted living services to its elderly inhabitants. Specifically, it keeps track of an inventory of ingredients and cooks food for them; it is also aware of medical histories and dietary preferences. The smart block of flats would be interested in knowing things such as,
- what sort of dishes are (in)appropriate for its residents? - what ingredients are needed to cook a particular dish? - what dishes use a particular ingredient that happens to be available?
We will look at how to implement such queries from an ontology.
But first, we need to load our ontology from its file.
import owlready2
dietOntology = owlready2.get_ontology("../owl/BAALLFragment.owl")
dietOntology.load()
An ontology contains information about "classes" -- groups of "individuals" -- and "properties". Collectively, classes, individuals, and properties are known as "entities". There can be many of them! Some ontologies contain several million entities, corresponding e.g. to various species or chemical substances.
Also, an ontology is meant to be reused and interoperate with other ontologies. Our example ontology uses parts of an ontology for chemistry, and parts of one for medicine.
Thus, it is important to keep some order in this knowledge base. Every entity then belongs to a "namespace", and to ease our access to them, we will explicitly create some namespace objects to retrieve entities from the ontology for us. The namespaces below are:
DUL -- for DOLCE Ultra Lite, these are very basic categories of the world, like Event or Object. FOD -- the Food related part of the Bremen Assisted Living Laboratory ontology (BAALL) Chemistry -- the Chemistry related part of BAALL Medicine -- the Medicine related part of BAALL
DUL = dietOntology.get_namespace("http://www.baall.de/ontologies/DUL.owl#")
FOD = dietOntology.get_namespace("http://www.baall.de/ontologies/FOD.owl#")
Chemistry = dietOntology.get_namespace("http://www.baall.de/ontologies/Chemistry.owl#")
Medicine = dietOntology.get_namespace("http://www.baall.de/ontologies/Medicine.owl#")
GradedQuality = dietOntology.get_namespace("http://www.baall.de/ontologies/GradedQuality.owl#")
We can now look at some classes. For example, we can see what sort of categories of food are known to the ontology ...
FOD.Food.descendants()
Of course, this is only a small fragment of the known foods in BAALL, but to make this example easier to run, we only look at a small subset.
So, we have seen an answer to a question like "what kinds of food are there?" by looking at subclasses of food. However, classes can also have superclasses. That is, we can also ask, "if I know something is food, what else do I know about it?"
FOD.Food.ancestors()
All classes will have Thing as the topmost ancestor.
You can also get a list of all the classes in the ontology:
list(dietOntology.classes())
In the printout produced by the classes() method, you will see class names of the form namespace.classname, where the namespace is compressed to a short string as opposed to its full expression. Usually, class names -- and entity names more generally -- are IRIs that resemble a URL. Thus, you may have something like
http://www.baall.de/ontologies/DUL.owl#Person
be the IRI for the "Person" concept from the foundational ontology DUL. Typically however, such a class would be referred to by a shorter name while programming, e.g. DUL.Person, assuming you have a namespace set up to point to the DUL IRI prefix (http://www.baall.de/ontologies/DUL.owl#).
So far we have looked at simple queries about what is already explicitly in the ontology. However, an ontology can be used for reasoning -- to derive more conclusions in a logical fashion from what is asserted. This allows us to look at relationships between more complicated concepts.
The approach is to define a "query concept": a concept that is defined in a way that is useful for some application, and then to ask the ontology what other concepts already existing in the ontology are subclasses (or superclasses) of it.
Lets take a specific example. Suppose our smart block of flats knows one of its inhabitants suffers from apoplexy. What sort of foods are absolutely to be avoided in this case?
We are then looking for all subclasses of food that are avoided by a person who suffers from apoplexy. To define such a query concept in owlready2, we can use the syntax below:
with dietOntology:
class NotSuitableForApoplexy(owlready2.Thing):
equivalent_to = [FOD.Food & FOD.avoidedBy.some(DUL.Person & Medicine.hasDisease.some(Medicine.Apoplexy))]
However, before this query concept can tell us anything, we must perform a reasoning step. The cell below shows how to do this in owlready2, which uses a description logic reasoner called Hermit. Faster reasoners exist, but they are not as well integrated as Hermit into python packages.
You will have to wait about a minute or so for the results.
with dietOntology:
owlready2.sync_reasoner()
NotSuitableForApoplexy.descendants()
So now that we have seen what a person with apoplexy should not eat, how about we check what is recommended for them to eat?
with dietOntology:
class FavoredForApoplexy(owlready2.Thing):
equivalent_to = [FOD.Food & FOD.favouredBy.some(DUL.Person & Medicine.hasDisease.some(Medicine.Apoplexy))]
owlready2.sync_reasoner()
FavoredForApoplexy.descendants()
Suppose then we wanted to cook some gefillte char for the resident. What sort of ingredients should be on hand for this? We will look at ingredients that are needed in large amounts.
with dietOntology:
class NeededForGefillteChar(owlready2.Thing):
equivalent_to = [owlready2.Inverse(FOD.hasFoodIngredient_atLeast_f004Major).some(FOD.GefillteChar)]
owlready2.sync_reasoner()
NeededForGefillteChar.descendants()
Finally, suppose we decided to buy some char. What else could we cook with it? We will look at dishes that use it in large amounts.
with dietOntology:
class DishWithChar(owlready2.Thing):
equivalent_to = [FOD.Food & FOD.hasFoodIngredient_atLeast_f004Major.some(FOD.Char)]
owlready2.sync_reasoner()
DishWithChar.descendants()
And that is it for this tutorial! But of course, there is lots more to learn about ontologies and description logic.
Feel free to download the repository associated to this notebook and open up the associated owl in a tool like Protege which offers you a graphical user interface in which you can see all the concepts, properties, and individuals in the ontology. You can also run queries in Protege by defining query concepts as above, as well as SPARQL queries for individuals in an ontology.
The formal logic aspect of description logic has also only been briefly touched upon in this tutorial. We have looked at sub- and superclasses and conjunctions (intersections) of classes, which may be intuitive already, but some other constructs appeared in our queries above: inverse properties and "existential" restrictions. A deeper look at the issues involved however will be presented in a different tutorial.
Hoping that the above made you curious for the possibilities of description logic and ontologies, see you at the other tutorial soon!