Robot Agents, Messages and The Society of Mind
One of the biggest challenges in both real and artificial minds
is how to keep track of all the voices—both outside and in the
head. The brain is continually bombarded by data from the outside
world, inside the body (proprioception), and even from other parts
of itself. How does such a system retain any kind of coherence
without simply spinning off into madness?
In the early 1970s, Marvin Minsky and Seymour Papert, pioneers in
the fields of cognitive psychology and artificial intelligence,
reasoned that such a complex system could not be controlled by some
single executive function. Of course, it sometimes seems like
that's how it works; for example, you decide to go to the
kitchen looking for a snack, you find your way to the kitchen
without stepping on the sleeping cat, you open the
refrigerator, etc. But of course, we are only aware of a tiny
fraction of all the neural activity involved in making this happen.
A path to the kitchen must be chosen from your current location,
hundreds of muscles must be commanded to contract in a particular
sequence, heart rate and breathing must be adjusted, balance must be
maintained, obstacles must be avoided, the fridge handle must be
grasped, foodstuffs must be identified, etc.
Minsky and Papert postulated that these various subtasks were
taken care of by semi-autonomous agents which
are themselves not particularly intelligent but are good at doing
one particular task very well. Maintaining balance is a good example
as is the task of reaching for an object. And even these tasks are
rather complex and would likely be the result of even more
specialized agents working together on even simpler tasks. Minsky
referred to this interaction among agents as "a society of
mind" and published a book by that title in 1986. In his view,
mind is not any one process or quality of the brain, but precisely
this complex interaction among mindless agents.
Minsky's and Papert's ideas
significantly influenced the development of artificial neural
networks, which as we have seen in earlier blog articles, are
composed of many simple artificial neurons or nodes connected
together into a larger network. Individually, a given neuron
computes a relatively simple function on its inputs, but taken
together, the entire network can solve more complex problems such as
recognizing faces in a picture. Even so, artificial neural networks
represent a fairly low-level form of agent processing since the
agents themselves are computing such simple functions and the
messages being passed are simple numbers. We can broaden the concept
of an agent or node and the types of messages they can exchange to
create even more intelligent systems.
The idea of a loosely
interconnected collection of agents has become a popular programming
model in robotics. This approach lends itself naturally to
constructing a modular system of simpler components that can be
combined and reused to produce more complex behaviors. In many of
these systems, the agents are referred to simply as nodes,
and nodes communicate with one another by passing messages.
For example a sonar sensor mounted on the head of your robot could
be considered a node while its message would consist of its current
distance reading to the nearest object. A more complex node might
consist of a head tracking routine (as we have seen in previous
articles) that reads visual messages from a head-mounted camera and
sends motor control messages to the head's pan and tilt servos.
Nodes can also represent remote data sources, such as a web server
where the messages are the contents of web pages, or a remote web
camera so that our robot's sense of vision can be expanded to other
locations.
Two of the more popular robot
programming frameworks are built around nodes and messages. These
are the Robot
Operating System (ROS) from Willow Garage, and Microsoft's
Robotics
Developer Studio (MSRDS). While ROS runs primarily on Linux
machines and is open source, MSRDS runs only on Windows computers.
Both systems are very powerful but also require a significant
investment in learning and practice. Fortunately, the key concepts
of nodes and messages can be constructed very simply without having
to adopt these entire frameworks. At the same time, our methods will
not preclude us from using these frameworks at a later time if
desired. What's more, the code developed below will run on all three
platforms (Linux, Windows, and MacOS X) without modification.
Programming with Nodes and Messages
Our nodes will need a way to
communicate with each other. For example, suppose one node monitors
our robot's video camera and tracks an object of interest by
recording its X-Y coordinates. A second node controls the pan and
tilt servos attached to the camera and we want those servos to move
in a way to keep the object centered in the field of view. When
programming in a multi-threaded environment, it is important to
prevent different nodes from overwriting a given variable at the
same time. Otherwise, unpredictable values and behaviors can result;
for example, a motor might suddenly start oscillating wildly or your
entire program could lock up. Avoiding these problems is called
making your code thread safe. Incidentally, even our own
brains require this kind of separation of threads. For imagine
trying to have a different conversation with two or more people at
the same time. With two conversations, we might just be able to keep
things straight as long as no one speaks too quickly. But throw in a
third conversation and we will quickly become confused and
frustrated.
There are many ways to achieve thread
safety in your programming code. The one we will follow in this
article requires that all nodes communicate with each other through
a common "message board". The idea is that each node will
either publish or read data in the form of messages that are posted
in a single place, much like a message board mounted on a wall in
school or the office. If one node needs data from another node, it
does not contact the node directly, but instead looks for the
appropriate message on the message board. By using a single location
to store and retrieve messages between nodes, we can easily make our
program thread safe.
A message has two simple parts: its
topic and its value. For example, the message from a
sonar sensor might look like this:
("head_sonar_reading", 33)
where the topic is
"head_sonar_reading" and its value is 33 inches. Any node
that wants to know the reading from the head sonar need only look up
this message on the message board.
Messages can also represent a request.
For example, to rotate the robot 60 degrees to the right, we don't
talk to the drive motors directly, but post the request on the
message board:
("rotate_base", 60)
If the robot's motor controller is set
to monitor the message board, it will see such a message and act on
it accordingly. Similarly, any other node that cares to know what
the robot base is up to can read this message.
Message values do not have to be
simple numbers. They can be text strings such as "Meet me at
the coffee shop at 10", sets of instructions, lists of numbers,
lists of other nodes, lists of lists, and so on. In our real-world
message board analogy, you could post a written note or nail a lost
shoe to the board. Either way, some one will get the point.
In the Python programming language,
our message board is a simple dictionary where the keys are topic
names and the values are the data or request values. The examples
above would then become:
MessageBoard
= dict({})
MessageBoard['head_sonar_reading']
= 33
MessageBoard['rotate_base']
= 60
For an example of a more complicated
message, we might have a list of fruits such as:
fruits = ["banana", "orange", "peach", "blueberry", "apple", "raspberry"]
which we could then post on the MessageBoard using the simple
statement:
MessageBoard['fruits']
= fruits
The Message Board and Working Memory
The message board concept is loosely
analogous to the concept of working memory in cognitive
psychology. By laying out all the messages before us in one place,
we can process them in a more orderly fashion, such as sorting them
into categories, removing messages we don't care about, or
re-ordering them into a sequence different from the order in which
they arrived. If we include a time stamp with each message, we can
even periodically clear out messages older than a certain date. We
will have much more to say about this in later articles.
Publishing and Reading Messages
The main role of our most abstract
node class is to publish and read messages to and from the message
board in a thread-safe manner as shown in the Python code below:
(PLEASE NOTE: these examples require Python 2.6.5 or above
to run.)
import threading
MessageBoard = dict({})
class Node(object):
""" Top Level Node Class """
def __init__(self, name="", uri=""):
self.name = name
self.uri = uri
# Publish a topic-value pair on the Message Board
def pub_message(self, topic, value):
self.messageLock = threading.Lock()
with self.messageLock:
MessageBoard[topic] = value
# Get the value of a topic from the Message Board.
# Return None if the topic does not exist.
def get_message(self, topic):
self.messageLock = threading.Lock()
with self.messageLock:
try:
return MessageBoard[topic]
except:
return None
The function pub_message()
takes two arguments, a topic and a value, sets a thread lock to keep
the MessageBoard safe for the moment, then sets the topic to
the given value. (The lock is released automatically as we fall
through the end of the function.) Similarly, get_message()
takes a topic, sets a lock, checks to see if that topic exists on
the message board, and returns the appropriate value.
Here is an example using our head
sonar.
HeadSonar = Node(name="Head Sonar")
HeadSonar.pub_message("head_sonar_reading", 33)
print HeadSonar.get_message("head_sonar_reading")
Any other node that would like to know the value coming from the
head sonar can read up on the "head_sonar_reading" topic
on the message board. For example, an obstacle avoidance routine
might check this message several times a second. In the meantime,
the obstacle avoidance routine does not need to know how to contact
the sonar sensor directly; it simply reads the appropriate
message(s) on the message board and assumes they are coming from a
reputable source.
Programming Examples
The following code snippet illustrates two nodes running in separate
threads and each publishing a message once a second while reading
and printing the message of the other node:
from pi.nodes.node import Node
import threading, time
Node1 = Node(name="Node 1")
Node2 = Node(name="Node 2")
class Thread1(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.interval = 1
self.count = 0
def run(self):
Node1.pub_message("node1_topic", "Hello from Node 1! Count: " + \
str(self.count))
print "Node 1 reads Node 2's topic: %s " % Node1.get_message("node2_topic")
self.count += 1
time.sleep(self.interval)
class Thread2(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.interval = 1
self.count = 0
def run(self):
Node1.pub_message("node2_topic", "Hello from Node 2! Count: " + \
str(self.count))
print "Node 2 reads Node 1's topic: %s " % Node2.get_message("node1_topic")
self.count += 1
time.sleep(self.interval)
thread1 = Thread1()
thread2 = Thread2()
thread1.start()
time.sleep(2)
thread2.start()
The output should look like this:
Node 1 reads Node 2's topic: None
Node 1 reads Node 2's topic: None
Node 1 reads Node 2's topic: None
Node 2 reads Node 1's topic: Hello from Node 1! Count: 2
Node 1 reads Node 2's topic: Hello from Node 2! Count: 1
Node 2 reads Node 1's topic: Hello from Node 1! Count: 3
Node 2 reads Node 1's topic: Hello from Node 1! Count: 4
Node 1 reads Node 2's topic: Hello from Node 2! Count: 2
Node 1 reads Node 2's topic: Hello from Node 2! Count: 3
Node 2 reads Node 1's topic: Hello from Node 1! Count: 5
etc.
Note that because we started Node 2's thread 2 seconds after Node 1,
the value of its topic is "None" until the thread is
started. Note also that the messages do not exactly alternate
between Node 1 and Node 2 even though they are both running on a 1
second interval. This is because the exact instant at which the
print command is executed in each thread is not under our control.
However, even so, note that the count values increment correctly for
each of the two topics so we are not actually missing any data.
Here
is a another example that mimics more closely what we might do with
our robot. The first node publishes a random number between 1 and 10
simulating a sensor reading. The second node reads this value on the
MessageBoard and commands the robot to turn left if the number is
odd or right if it is even. Furthermore, we set the publishing rate
for Node 1 to 100
times per second (interval
= 0.01 seconds) to illustrate that even very fast message updates
are no problem for our thread locking mechanism.
from pi.nodes.node import Node
import threading, time, random
Node1 = Node(name="Node 1")
Node2 = Node(name="Node 2")
class Thread1(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.finished = threading.Event()
self.interval = 0.01
self.daemon = False
def run(self):
while not self.finished.isSet():
Node1.pub_message("node1_topic", + random.randrange(11))
time.sleep(self.interval)
def stop(self):
print "Stopping Node 1 Thread ...",
self.finished.set()
self.join()
print "Done."
class Thread2(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.finished = threading.Event()
self.interval = 0.5
self.daemon = False
def run(self):
while not self.finished.isSet():
node1_message = Node2.get_message("node1_topic")
if node1_message % 2 == 0:
Node2.pub_message("node2_topic", "Turn Right")
else:
Node2.pub_message("node2_topic", "Turn Left")
print "Node 1 Message:", node1_message, "Node 2 Message:", \
Node2.get_message("node2_topic")
time.sleep(self.interval)
def stop(self):
print "Stopping Node 2 Thread ...",
self.finished.set()
self.join()
print "Done."
thread1 = Thread1()
thread2 = Thread2()
thread1.start()
thread2.start()
time.sleep(10)
thread1.stop()
thread2.stop()
And the output will look something like this:
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 8 Node 2 Message: Turn Right
Node 1 Message: 0 Node 2 Message: Turn Right
Node 1 Message: 3 Node 2 Message: Turn Left
Node 1 Message: 9 Node 2 Message: Turn Left
Node 1 Message: 1 Node 2 Message: Turn Left
Node 1 Message: 0 Node 2 Message: Turn Right
Node 1 Message: 3 Node 2 Message: Turn Left
Node 1 Message: 9 Node 2 Message: Turn Left
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 4 Node 2 Message: Turn Right
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 4 Node 2 Message: Turn Right
Node 1 Message: 10 Node 2 Message: Turn Right
Node 1 Message: 3 Node 2 Message: Turn Left
Node 1 Message: 6 Node 2 Message: Turn Right
Remote Messaging over a Network
So far we have designed our nodes and
message board to operate on a single computer. Some roboticists
prefer to work with a distributed computing architecture so that
nodes can run on multiple computers yet still exchange messages.
Fortunately, it is easy to plug in almost any function you like for
pub_message() and get_message() in our node
definition, including functions that pass the messages over a
network connection. Furthermore, Python makes working with network
communication fairly straightforward. Even so, this is a topic
outside the scope of the current article so we will have to come
back to it at a later time.
What Does Pi Robot Think of all This?
You'll be happy to hear that Pi Robot really likes the idea of using
nodes and messages to control his behavior. For one thing, it allows
him to take his mind off every little detail and spend more time
surfing the web. As you know, I have recently converted most of Pi's
programming code from C# to Python and I have also restructured his
control architecture to take advantage of the message board concept
described above. So how well does it work in real life? In short, it
is a smashing success. In fact, the combination of Python and
message-passing works much more smoothly than my older procedural
methods using C#. And most importantly of all, the voices in his
head are now making sense.