Feed and generalize (python)

Do the birds fly ?

Insert new knowledge

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

import re
import time

import rospy
from std_msgs.msg import String

from ontologenius import OntologyManipulator

_onto = None

def singular(word):
if word[len(word) - 1 ] == 's':
word = word[0 : len(word) - 1]
return word

def say(text):
print('[SAY]' + 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.

def testIsA(text):
global _onto
match = re.search(r"^(a\s|the\s)?(\w+)\s(is\sa|is\san|are)\s(\w+)$", text)
if match != None:
_onto.feeder.addInheritage(singular(match.group(2)), singular(match.group(4)))
return 'ok'
else:
return None

def testProperty(text):
global _onto
match = re.search(r"^(a\s)?(\w+)\scan\s(not\s)?(\w+)$", text)
if match != None:
if match.group(3) == None:
_onto.feeder.removeDataProperty(singular(match.group(2)), 'can_' + singular(match.group(4)), 'bool', 'False')
_onto.feeder.addDataProperty(singular(match.group(2)), 'can_' + singular(match.group(4)), 'bool', 'True')
else:
_onto.feeder.removeDataProperty(singular(match.group(2)), 'can_' + singular(match.group(4)), 'bool', 'True')
_onto.feeder.addDataProperty(singular(match.group(2)), 'can_' + singular(match.group(4)), 'bool', 'False')
return 'ok'
else:
return None

def testQuestion(text):
global _onto
match = re.search(r"^can\s(a\s)?(\w+)\s(\w+)\s?\?$", text)
if match != None:
res = _onto.individuals.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))
if len(res) == 0:
res = _onto.classes.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))

if len(res) != 0:
if res[0] == 'bool#True':
return 'yes'
else:
return 'no'
else:
return 'I do not know'
else:
return None

def testWhatIs(text):
global _onto
match = re.search(r"^what\s(is|are)\s(a\s)?(\w+)\s?\?$", text)
if match != None:
res = _onto.individuals.getUp(singular(match.group(3)), 1)
if len(res) == 0:
res = _onto.classes.getUp(singular(match.group(3)), 1)

if len(res) == 0:
return 'I do not know'
else:
return 'It is a : ' + str(res)

else:
return None

def testIsItA(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)
if match != None:
res = _onto.individuals.getUp(singular(match.group(2)), selector = singular(match.group(4)))
if len(res) == 0:
res = _onto.classes.getUp(singular(match.group(2)), selector = singular(match.group(4)))

if len(res) == 0:
return 'no'
else:
return 'yes'

else:
return None

def inputCallback(msg):
say(msg.data)

response = testIsA(msg.data)
if response == None:
response = testProperty(msg.data)
if response == None:
response = testQuestion(msg.data)
if response == None:
response = testWhatIs(msg.data)
if response == None:
response = testIsItA(msg.data)
if response == None:
response = 'I do not understand'

if response != '':
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.

_onto.feeder.addInheritage(singular(match.group(2)), singular(match.group(4)))

If the sentence matches a heritage relationship, we add the inheritance relationship via the member feeder of the ontology manipulator.

Note that we do not directly pass the matches in the parameters but their singular version.

if match.group(3) == None:
_onto.feeder.removeDataProperty(singular(match.group(2)), 'can_' + singular(match.group(4)), 'bool', 'False')
_onto.feeder.addDataProperty(singular(match.group(2)), 'can_' + singular(match.group(4)), 'bool', 'True')

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)))
if len(res) == 0:
res = _onto.classes.getOn(singular(match.group(2)), 'can_' + singular(match.group(3)))

if len(res) != 0:
if res[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)
if len(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)))
if len(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".

def main():
global _onto

rospy.init_node('feed_and_generalize')

_onto = OntologyManipulator()
_onto.close()
time.sleep(1)

_onto.feeder.addConcept('bird')

rospy.Subscriber('feed_and_generalize/in', String, inputCallback)

rospy.spin()

if __name__ == '__main__':
main()

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'"

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?'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a kiwi fly ?'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'is a kiwi an animal?'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a bird fly ?'"

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?'"

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'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a bird 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 removeDataProperty 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.

rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a bird 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'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a bird fly ?'"

You had to suspect that the 60% condition is no longer true and our program finds itself in the inability to know whether birds can fly or not.

Let's learn five new birds that can all fly to reach the threshold of 60%.

rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a heron is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a heron can fly'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a gull is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a gull can fly'"

By asking it one last time if the birds can fly you should finally have a good answer from it and this without ever having explicitly told it.

rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a bird fly ?'"

As I did in the introduction of this tutorial, simply teach it that a phalarope is a bird and ask it if it can fly:

rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'a phalarope is a bird'"
rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a phalarope fly ?'"

Since it has no information about this and because thinking that birds can fly in general, it estimates that the phalarope should be able to fly.

However, by asking if a kiwi can fly, you should always have the answer that not that you have explicitly given this knowledge.

rostopic pub -1 /feed_and_generalize/in std_msgs/String "data: 'can a kiwi 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.