Recursive transition networks (RTN) are useful devices for representing
grammers in both human and computer languages. The kind of grammers that can
be represented with RTN's are called context-free. The consist of rules for
how words may be put together into structures and, in turn, how those
structures may be built into larger structures. For a particular grammer a
valid sentence
is a list of words that follow the rules of the grammer.
The computer language Pascal (among others) is formally defined using RTN's.
Here is a very simple example. The grammer in figure 1 consists of a single
sentence
, the word red
followed by the word book
.

For our purposes an alternative, but equivalent, format is more useful because it is easily translated into a Python dictionary. The network consists of nodes (1, 2, END) connected by named arcs. The name of an arc indicates what word(s) will carry one from one node to another, and in which direction. You traverse the network by starting at node 1 and finding a path to node END.

And here is one way to represent the network in Python
simple1 = {TITLE: "simple1", 1: [("red", 2) ], 2: [("book", END)] }
The nodes in the network are key/value pairs in a dictionary. The key is the name of the node and the value is a list of arcs. Each arc is a two valued tuple describing the word and destination. The END node does not need to be defined since no arcs emanate from it. A special key/value pair is used for a title. TITLE and END are simply defined constants.
Let's move on to a somewhat more interesting grammer. Figure 3 describes a
grammer that consists of the word book preceded by one or more
words red. So red book and
red red red book are both valid sentences in the grammer.

And here is the Python version.
simple2 = {TITLE: "simple2", 1: [("red", 2) ], 2: [("red", 2), ("book", END)] }
Generating sentences from a grammer
It's easy to write a python function that will generate a random sentence in a grammer. For our simple non-recursive grammers above the following function works well.
def gen1(network) : "Generate random sentence from network. Non recursive" p = network[1]; s = "" while 1 : choose = random.randrange(len(p)) s = s + " " + p[choose][0] next = p[choose][1] if next == END : return s p = network[next]
The variable p keeps track of the current node and
s is the sentence as it is being built. From each node we choose
an arc at random (line 5), add its word to the sentence (line 6) and advance to
the next node (line 9). The sentence is returned when the END node is reached
(line 8).
Let's play with this a bit. You can access code and grammer at rtn.py and simple.py
>>> import rtn >>> import simple >>> rtn.gen1(simple.simple1) ' red book' >>> rtn.gen1(simple.simple1) ' red book' >>> rtn.gen1(simple.simple2) ' red red book' >>> rtn.gen1(simple.simple2) ' red book' >>> rtn.gen1(simple.simple2) ' red red red book'
The network simple1 always produces the same sentence, but
simple2 generates a random number of red words
preceding book.
Nested Networks
The following is going to assume a rudimentary knowledge of english grammer. You need to at least know the difference between nouns, prepositions, verbs and adjectives. There are lots places to get this information. Try google.
Extending our simple networks to recursive networks requires just one change.
Allow the label on an arc to be either a word or another network. Here is an
example of 3 simple networks article, noun1 and
adjective along with a recursive network noun2. These
2nd order nouns
are preceded by the word the or
a followed by zero or more adjectives.

The recursive netword noun2 is defined in Python as you
probably already suspect. The subnetworks must be defined earlier. For a full
listing see english.py
noun2 = {TITLE: "Noun2", 1: [(article, 2)], 2: [(adjective, 2), (noun1, END)] }
A small modification in the generator function is needed to make it work recursively.
def gen2(item) : "Generate random sentence, allow recursive network" if type(item) == type("") : return item p = item[1]; s = "" while 1 : choose = random.randrange(len(p)) s = s + " " + gen2(p[choose][0]) next = p[choose][1] if next == END : return s p = item[next]
Now the item passed to the function may be either a word or a subnetwork. The third checks if the item is a string and, if so, simply returns it. Otherwise line 7 calls gen2 recursively to possibly expand a subnetwork. Let's generate some sentences.
>>> import english >>> import rtn >>> rtn.gen2(english.noun2) ' a red top' >>> rtn.gen2(english.noun2) ' a big book' >>> rtn.gen2(english.noun2) ' the table' >>> rtn.gen2(english.noun2) ' the small red table' >>>
Now suppose that instead of generating a random sentence, we want to generate all possible sentences in a grammer. We must be careful to choose a grammer that is finite. We'll use a grammer whose sentences consist of a single adjective followed by a simple noun.
from english import * simple3 = {TITLE: "Simple finite network", 1: [(adjective, 2)], 2: [(noun1, END)] }
But is there an easy way to generate all sentences in a grammer? This brings us to a new topic
Python generators
Generators are new to Python 2.2. You may think of them as functions that
return multiple values one at a time to the caller, but maintaining their
internal state (where they are and the value of local variables) between calls.
They are very convenient when used with a for
loop.
Here's a very simple example
>>> def gg() : ... yield 2 ... yield 4 ... yield 8 ... >>> for i in gg() : print i ... 2 4 8 >>>
What turns a function definition into a generator is the use of the keyword
yield.
yield returns
a value but keeps a bookmark of where it left off and values for local
variables. The for loop will keep
getting values from the generator until an explicit return in the
generator is encountered or the generator falls off the end.
Generators are actually objects with a next method that is used
by the for loop. They raise a specific exception when a
return is excecuted, explicitly or implicitly.
Here is a generator that will generate all the sentences in the little recursive grammer above.
def gen3(item, Start=1) : "Use generator to yield all possible sentences" if Start == END : yield "" elif type(item) == type("") : yield item else : p = item[Start]; s = "" for arc in p : for word in gen3(arc[0]) : for rest in gen3(item,Start=arc[1]) : yield word + " " + rest
This generator is doubly recursive and deserves a close study. When it is
entered item may be network, a partial network (where
Start is set), or terminal word (like red
). Basically,
from a given node, each arc is tried. For each word
(terminal or
subnetwork) the arc label produces, we check from the target node to see if
the END can be reached and yield the sentence accordingly. It is tricky.
Let's try this out.
>>> import simple >>> import rtn >>> for sent in rtn.gen3(simple.simple3) : print sent ... big book big table big top small book small table small top red book red table red top >>>
As an exercise, figure out why the words are separated by 2 spaces instead of 1.
Matching input sentences
Usually we want to do is see if a given sentence is valid in the grammer and,
if so, what are its constituent parts and how are they related to one another.
Sometimes there is more than one answer. For example, with the sentence,
The book on the table that is red
. You can't tell whether it is the
table or the book that is red. Of course we humans will rely on context for
resolving ambiguities. In the sentence, The man wearing the hat that is
green
, it is almost certain that it is the hat that is green since you
almost never see martians wearing hats. But computer programs cannot be
expected to know such things, at least not yet! So if our grammer allows
ambiguity, the matching algorithm should find multiple interpretations.
Our approach will be to generate all sentences that match the input sentence. If we are unable to find at least one match the sentence is not valid for our grammer. Later, as the match is generated we will track the structures built in subnetworks and combine them into nested list structures. The nesting will show the relationship of the parts in the same way that a sentence diagram does.
We are going to extend our noun phrase grammer to include prepositional
phrases. Like adjectives, these qualify (or zero in on) a noun. The book on
the table with a blue cover
has two prepositional phrases both of which
distinguish this book from, perhaps, others nearby. However, notice the
ambiguity. Is the book or the table covered?
Remember that a noun2 included an article (a,the) and zero or
more adjectives. We'll call a noun3 any noun2 follwed
by zero or more prepositional phrases.
preposition = {TITLE: "Preposition", 1: [("on",END), ("of",END), ("in",END)] } prepPhrase = {TITLE: "Prepositional Phrase", 1: [(preposition, 2)], 2: [(noun2, END)] } noun3 = {TITLE: "Noun-3", 1: [(noun2, END), (noun2, 2)], 2: [(prepPhrase, END), (prepPhrase, 2)] }
Of course, this grammer is not really good english, since the noun inside a
prepositional phrase itself may be qualified by a prepositional phrase. So
noun2 in line 7 really should be noun3. This can be
done only after noun3 itself has been defined with a statement
like
prepPhrase[2][0] = (noun3, END)
This is ok, although we need to be carefule because the grammer is now
circluar. If you were to print the dictionary prepPhrase, you
would get a recursion overflow. But this will let our match functions discover
the kind of ambiguities mentioned above.
The match generator
Our first match generator is very simliar to gen3 above.
def match1 (item, input, Start=1) : "Use generator to yield all possible matching sentences" if Start == END : yield input elif type(item) == type("") : if item == input[0] : yield input[1:] else : p = item[Start]; s = "" for arc in p : for input1 in match1 (arc[0], input) : for rest in match1 (item, input1, Start=arc[1]) : yield rest
One difference is the parameter input
which is the input sentence to
be matched. Its format is a list of words. Secondly, rather than yielding
(generating) sentences (or pieces of a sentence), match1 yields the rest of the
input left unmatched. This means that subnetworks can chop off pieces of the
input that they can use and pass back the remainder. Note that we are careful
not to modify the input list itself. In line 5,
yield input[1:] will create a new list each time. This allows
concurrent partial matches to be active.
Let's play with this a bit.
>>> import english >>> import rtn >>> for s in rtn.match1(english.noun1, ['the','red','book']) : print s ... >>> for s in rtn.match1(english.noun2, ['the','red','book']) : print s ... [] >>> for s in rtn.match1(english.noun2, ['the','red','book','bites']) : print s ... ['bites']
Matching a simple noun1 with noun2 input (line 3) generates no match because
the article and adjective just get in the way. Matching the input to the
noun2 network generates a single match comsuming all the input
(line 6). The 3rd match leaves the verb bites after the match.
Retrieving the sentence structure
Our match generator generates values that reflect the input that remains to be matched. We would like to also generate a data structure showing the sentence structure of the input that has been matched.
We'll generate nested tuples, one tuple for each network. An example should
make this fairly clear. The noun2 sentence the red book
is
represented by the set of nested tuples
('Noun2', ('Article', 'the'), ('Adjective', 'red'), ('Noun', 'book'))
The first element of each tuple contains the name of the network and the remaining elements the items that satisfied each arc on the path from node 1 to the END node. This is a format that makes it fairly easy to do semantic analysis. Here is a run for our second match generator.
>>> import rtn
>>> import english
>>> for s,t in rtn.match2(english.noun2,['the','red','book']) : print s,t
...
[] ('Noun2', ('Article', 'the'), ('Adjective', 'red'), ('Noun', 'book'))
>>>
And this is the python code:
def match2(item, input, Start=1, depth=0): "Use generator to yield all possible matching sentences" if depth > 10: return if Start == END: yield input, None elif not input: return elif type(item) == type(""): if item == input[0]: yield input[1:], item else: p = item[Start]; s = "" trace = [item[TITLE]] for arc in p: sav1 = trace[:] for input1,trace1 in match2(arc[0], input, depth=depth+1): if trace1: trace.append(trace1) sav2 = trace[:] for rest, trace2 in match2(item, input1, Start=arc[1], depth=depth+1): if trace2: trace = trace + list(trace2[1:]) yield rest, tuple(trace[:]) trace = sav2[:] trace = sav1[:]
This is essentially match1 extended to generate trace
tuples. At each
level a different trace is started at line 9, grown at line 14 when the first
arc is traversed and the rest of it gathered at line 17. The appends and
additions are then backed off in lines 19 and 20 in order not to corrupt
further possibilities. Incidentally, I spent a couple of hours getting this to
work correctly.
Notice the variable depth. With a circularly recursive network
it is possible for the code to get caught in an infinite loop. Aborting any
search that exceeds some maximum level of nesting stops this. Except for
artificially constructed sentences (involving a house that Jack built
)
this should not be a problem.
Here is a handy function to display the nested networks in an indented form that resembles sentence diagrams.
def display(trace, Indent="") : if type(trace) == type("") : print Indent, trace else : print Indent, trace[0] for item in trace[1:] : display(item, Indent=Indent+" ")
>>> for s,t in rtn.match2(english.noun2,['the','red','book']) : ... rtn.display(t) ... Noun2 Article the Adjective red Noun book >>>
Finally, just to make life a little easier, we'll introduce a 3rd match function. It will generate solutions only where the complete input is matched and simply return the trace.
def match3(item, input): "Use generator to yield all matches that consume input" import string for input, trace in match2(item, string.split(input)): if not input: yield trace
Recursive networks
Let's now play with a sentence that is purposely ambiguous.
>>> sentence = "a book on the table with a cover" >>> for t in rtn.match3(english.noun3,sentence) : rtn.display(t) ... ------------------- Noun-3 Noun2 Article a Noun book Prepositional Phrase Preposition on Noun2 Article the Noun table Prepositional Phrase Preposition with Noun2 Article a Noun cover
With our current grammer we don't allow the noun in a prepositional phrase to itself be modified by another prepositional phrase. So the only interpretaion possible is that the book is both covered and on the table.
Now let's change the network for prepPhrase so that the definition is circular. Running the same sentence we get
>>> english.prepPhrase[2] = [(english.noun3,english.END)] >>> for t in rtn.match3(english.noun3,sentence) : rtn.display(t) ... ------------------- Noun-3 Noun2 Article a Noun book Prepositional Phrase Preposition on Noun-3 Noun2 Article the Noun table Prepositional Phrase Preposition with Noun-3 Noun2 Article a Noun cover ------------------- Noun-3 Noun2 Article a Noun book Prepositional Phrase Preposition on Noun-3 Noun2 Article the Noun table Prepositional Phrase Preposition with Noun-3 Noun2 Article a Noun cover >>>
If you look carefully, you'll see that in the first interpretation it is the table that is covered and in the second, the book.
Parsing another language
Finally, let's contrast an english phrase with a japanese one and create a RTN grammer for japanese.
The phrase we will parse is in english the book on top of the table
.
Now in japanese there are no articles (the, a, an). Prepositional phrases precede the noun and the preposition itself comes at the end. So the sentence looks like the following
taberu no ue ni hon table of top on book
See japanese.py for the networks. Notice that the last line of the file updates the prepPhrase network for the circular definition.
>>> sentence = "taberu no ue ni hon" >>> for t in rtn.match3(japanese.noun3,sentence) : rtn.display(t) ... ------------------- Noun3 Prepositional Phrase Noun3 Noun2 Noun1 taberu Preposition no Prepositional Phrase Noun3 Noun2 Noun1 ue Preposition ni Noun2 Noun1 hon ------------------- Noun3 Prepositional Phrase Noun3 Prepositional Phrase Noun3 Noun2 Noun1 taberu Preposition no Noun2 Noun1 ue Preposition ni Noun2 Noun1 hon >>>
Again, two interpretations are possible although we humans would hardly
think so. Beside the obvious one, the book on top of a table
could be a
book consisting of a table (of logrithms, maybe) on top of some other books. Of
course, the computer is just finding the ambiguity in the structure itself.
But subtle ambiguities are absolutely rampant in natural languages which is
one reason why natural language translation by computer programs is very
difficult. A famous sentence time flies like an arrow
leads to several
interpretaions beside the one you are thinking of. For example, would you time
flies and arrows with a stopwatch? Why would time flies prefer to hang out with
(or is it eat?) arrows instead of whatever horse flies prefer? Or perhaps time
flies in parabolic arcs just like arrows. You can have almost as much fun with
the sentence fruit flies like a banana
.
Pratical considerations
We just used a network for lexicons of nouns, adjectives, etc. but this
would be impractical for anything other than the toy grammers shown here.
Instead, matching noun to (say) table would be better
done with a real Python dictionary where the key table might index
a structure indicating a number of things about the word, including that it is
a noun and its japanese translation is taberu. Of course,
table can also be a verb in which case it would have a different
japanese counterpart. Yet more ambiguity possibilities.