Before starting, here are some imports, an ontology manipulator object, a small function that
removes the s of the plural of a word when it is present.
#!/usr/bin/env python
importre
importtime
importrospy
fromstd_msgs.msgimportString
fromontologeniusimportOntologyManipulator
_onto = None
defsingular(word):
ifword[len(word) - 1 ] == 's':
word = word[0 : len(word) - 1]
returnword
defsay(text):
print('[SAY]' + text)
#!/usr/bin/env python
importre
importtime
fromthreadingimportThreqd
importrclpy
fromrclpy.nodeimportNode
fromstd_msgs.msgimportString
fromontopyimportOntologyManipulator, Ontoros
_onto = None
defsingular(word):
ifword[len(word) - 1 ] == 's':
word = word[0 : len(word) - 1]
returnword
defsay(text):
Ontoros().get_logger().info('[SAY] %s' % text)
In the next step, we will define five regexes that correspond to the five types of sentences that our chatbot
will be able to understand. I'm not going to give you a lesson, but remember that an expression in
parentheses is what we call a match. When evaluating a regex on a string of characters, if the string
matches the regex, we will be able to recover each of these matches. Whenever you see (\w+) it
means that at this end you want to retrieve the set of characters until the next space: so a word.
For example, the regex "(a\s|the\s)?(\w+)\s(is\sa|is\san|are)\s(\w+)" accepts all the sentences of the type "bob is a man", "a chicken is
a bird" or "the birds are animals". This regex has four matches and the ones we are interested in
are the second and fourth. In the previous examples, the second match should be 'bob', 'chicken'
or 'birds' and the fourth 'man', 'bird' or 'animals'.
The regexp "(a\s)?(\w+)\scan\s(not\s)?(\w+)" will accepts sentences of type : "a bird can fly" and "a bird can not swim"
The regexp "can\s(a\s)?(\w+)\s(\w+)\s?\?" will accepts sentences of type : "can a bird fly ?"
The regexp "what\s(is|are)\s(a\s)?(\w+)\s?\?" will accepts sentences of type : "what are kiwis ?" and "what is a bird ?"
The regexp "(is\sa|are)\s(\w+)\s(is\sa\s|is\san\s|a\s|an\s)?(\w+)\s?\?" will accepts sentences of type : "is a kiwi a bird ?" and "are birds animals ?"
We will now write the callback function of a ROS topic that will determine the type of sentence and
perform the actions corresponding to each type of sentence.
deftestIsA(text):
global_onto
match = re.search(r"^(a\s|the\s)?(\w+)\s(is\sa|is\san|are)\s(\w+)$", text)
match = re.search(r"^can\s(a\s)?(\w+)\s(\w+)\s?\?$", text)
ifmatch != None:
res = _onto.individuals.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))
iflen(res) == 0:
res = _onto.classes.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))
iflen(res) != 0:
ifres[0] == 'bool#True':
return'yes'
else:
return'no'
else:
return'I do not know'
else:
returnNone
deftestWhatIs(text):
global_onto
match = re.search(r"^what\s(is|are)\s(a\s)?(\w+)\s?\?$", text)
ifmatch != None:
res = _onto.individuals.getUp(singular(match.group(3)), 1)
iflen(res) == 0:
res = _onto.classes.getUp(singular(match.group(3)), 1)
iflen(res) == 0:
return'I do not know'
else:
return'It is a : ' + str(res)
else:
returnNone
deftestIsItA(text):
global_onto
match = re.search(r"^(is\sa|are)\s(\w+)\s(is\sa\s|is\san\s|a\s|an\s)?(\w+)\s?\?$", text)
ifmatch != None:
res = _onto.individuals.getUp(singular(match.group(2)), selector = singular(match.group(4)))
iflen(res) == 0:
res = _onto.classes.getUp(singular(match.group(2)), selector = singular(match.group(4)))
iflen(res) == 0:
return'no'
else:
return'yes'
else:
returnNone
definputCallback(msg):
say(msg.data)
response = testIsA(msg.data)
ifresponse == None:
response = testProperty(msg.data)
ifresponse == None:
response = testQuestion(msg.data)
ifresponse == None:
response = testWhatIs(msg.data)
ifresponse == None:
response = testIsItA(msg.data)
ifresponse == None:
response = 'I do not understand'
ifresponse != '':
say('=> ' + response)
Let's break down the code:
match = re.search(r"^(a\s|the\s)?(\w+)\s(is\sa|is\san|are)\s(\w+)$", text)
We test here if the sentence sent to the topic corresponds to the first regexp. If so, then the
different matches will be put in the match variable. We will do the same for all the regexp.
The third match must contain "not" if the sentence is in its negative form and None if it is
in its positive form.
If the sentence is in its positive form, we remove the data property "can_verb" with the boolean value
false and add it with the value true. Although it seems strange, Ontologenius allows an individual to own
the same property with different values. To be sure that this does not happen here because it does not
make sense with booleans, we remove the value false before adding true. This is no problem removing a
property that does not exist on an individual, it will have no effect. However, if we add to it the
knowledge that a bird can not swim and that later we tell him who finally a bird can swim, we will be
sure that the first knowledge will be removed.
We do the same for the negative form by removing the value true and adding the value false.
Note, however, that we are applying a data property of the form "can_verb" to an individual or class
without ever having defined this property before. If Ontologenius does not find the said
property it will create it alone by determining its type with respect to the data pointed. For all
times after, the property will exist and will be reused.
res = _onto.individuals.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))
iflen(res) == 0:
res = _onto.classes.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))
iflen(res) != 0:
ifres[0] == 'bool#True':
return'yes'
else:
return'no'
else:
return'I do not know'
We are trying here to answer a question such as "can a kiwi fly?" We want to know if the concept
"kiwi" has the property "can_fly" with the value true or false. To get this information, we can
use the getOn function seen in the previous tutorial. However, we do not know if the concept we are
working on is of individual type or class type. The simplest way is to look at whether we have had
an answer on the individuals and to test on the classes otherwise.
If we got an answer, it will be in the form "type#value".
res = _onto.individuals.getUp(singular(match.group(3)), 1)
iflen(res) == 0:
res = _onto.classes.getUp(singular(match.group(3)), 1)
Here we try to answer a question such as "what is a kiwi?" You should directly think about using
the getUp! However, this function will give us all the inheritance trees, which is not relevant
to answer this question. This is why we add the second parameter to this function, defined here
as 1. This value corresponds to the depth of exploration of the inheritance tree. By setting this
parameter to 1, we ask to have only direct inheritances.
res = _onto.individuals.getUp(singular(match.group(2)), selector = singular(match.group(4)))
iflen(res) == 0:
res = _onto.classes.getUp(singular(match.group(2)), selector = singular(match.group(4)))
Finally, we finish with a question of the type "is a kiwi is an animal?". To answer this we could recover all the heritage tree and we look if the concept of
inheritance suppose is present. However, the getUp function has another optional parameter which is the selector.
It works in exactly the same way as we saw in the previous tutorial. When this parameter is defined, the function
retrieves only the concepts inheriting from the selector concept.
Finally, we can write our main function which creates the ontology manipulator, close the ontology,
and subscribes to the topic "feed_and_generalize/in".
The main function is a bit more complicated than usual as for the next step we will need to spin the Ontologenius API node in order to subscribe to some topics.
Before moving on, note that we add the concept "bird" before all. This is because to
build an ontology, Ontologenius must always know at least one concept. For example, if we want to
add the fact that a bird is an animal, Ontologenius must know either bird or animal and will
automatically create the second if it does not exist yet.
_onto.feeder.addConcept('bird')
Let's test!
Now that everything is ready, let's launch our program and in another terminal explain to it
that a kiwi is a bird and that it can not fly:
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a kiwi is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a kiwi can not fly'"
ros2 topic pub -1 feed_and_generalize/in std_msgs/msg/String "{data: 'a kiwi is a bird'}"
ros2 topic pub -1 feed_and_generalize/in std_msgs/msg/String "{data: 'a kiwi can not fly'}"
We can already ask it some questions to evaluate what it knows thanks to this new information:
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'what is a kiwi?'"
It should answer you (in the terminal where the program is launched) that a kiwi is a bird and
it can not fly. You should also have that a kiwi is not an animal (since it does not even know what
an animal is) and that it does not know if a bird can fly (since nothing has been said about it).
We can explain to it that a bird is an animal and ask it again if a kiwi is an animal:
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'birds are animals'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'is a kiwi an animal?'"
ros2 topic pub -1 feed_and_generalize/in std_msgs/msg/String "{data: 'birds are animals'}"
ros2 topic pub -1 feed_and_generalize/in std_msgs/msg/String "{data: 'is a kiwi an animal?'}"
It is obvious but now our program responds to us that a kiwi is an animal as a kiwi is a bird and a
bird is an animal.
So let's learn the concept of a penguin who is a bird that can not fly and then ask it again if
a bird can fly:
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a penguin is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a penguin can not fly'"
Surprisingly, our program now responds to the fact that birds can not fly, which is wrong, but
above all, that we have never told it!
This is in fact due to the generalization mechanism. Since all the birds it knows can not fly,
it has deduced by itself that all birds can not fly. To excuse this mistake, it must be
said that we did not help it.
The basic principle is that Ontologenius realizes a generalization if 60% of the individuals
inheriting from the same classes have the same property and that from two individuals.
Trying to remove this property with the call of the removeDataRelation function would have no effect
since it would be deduced again. To make this property accurate, it should be explicitly told that
birds can fly but we will not do that.
Before continuing, kill Ontologenius and so your program then restarts it and ask it again if the
birds can fly.
It should answer you again no. Yet you have stopped Ontologenius... This is where we understand its
long-term memory principle as well as the usefulness of the internal file. When stopping Ontologenius,
it stored all its knowledge in the internal file that is reloaded at the restart.
We will now teach it the principle of a parrot and a dove that are birds capable of flying and ask it again if
the birds can fly.
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a parrot is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a parrot can fly'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a dove is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a dove can fly'"
So we have seen here how to add new knowledge during the execution of the program and the impact of
this through the mechanism of generalization.
If you wish, you can delete the internal file Ontologenius has created and try again by adding
the following line just before closing the ontology in the main function.
_onto.reasoners.deactivate('ReasonerGeneralize')
In fact, the generalization mechanism is a reasoning plugin that like all these mechanisms can be
active or not through the reasoning manager.
However, if you do not delete the Ontologenius internal file, the generalization has already been
done and will remain so. It will just not be able to achieve new generalizations.