13 · Interlingua — Toy UNL Round-Trip#

Dependencies: stdlib only
Runtime: < 1 min CPU

Chapter stub — implementation coming in a future commit.

Time-machine sentences#

Every chapter runs the same 20 held-out test sentences so the quality arc across chapters is directly comparable.

Open In Colab

# This chapter is **stdlib only** — nothing to install.
# It runs on any Python 3.8+ with no pip dependencies.

13 · Interlingua MT — UNL Round-Trip#

Interlingua-based MT proposes a universal meaning representation that lies between all natural languages. Translate once into interlingua, then generate any target language — 2N components instead of N² translation pairs.

UNL (Universal Networking Language) was the most ambitious attempt, developed at the UN University in the 1990s. We implement a toy round-trip: EN -> UNL -> DE.

1 · Universal Networking Language — graph representation#

class UNLNode:
    def __init__(self, uw, attrs=None):
        self.uw = uw
        self.attrs = attrs or []
        self.relations = {}
    def add_rel(self, label, node):
        self.relations[label] = node
        return self
    def __repr__(self):
        rels = ", ".join(f"{k}: {v.uw}" for k, v in self.relations.items())
        return f"UNLNode({self.uw!r}{chr(44)+chr(32)+rels if rels else chr(41)}"        # chr(44)=comma chr(32)=space chr(41)=close-paren

def to_unl_str(node, depth=0):
    indent = "  " * depth
    attr_s = ("." + ".".join(f"@{a}" for a in node.attrs)) if node.attrs else ""
    lines = [f"{indent}{node.uw}{attr_s}"]
    for rel, child in node.relations.items():
        lines.append(f"{indent}  {rel}: {to_unl_str(child, depth+2)}")
    return "\n".join(lines)

park = UNLNode("park(icl>place)")
dog  = UNLNode("dog(icl>animal)")
man  = UNLNode("man(icl>person)")
walk = (UNLNode("walk(icl>action)", attrs=["entry","present"])
        .add_rel("agt", man).add_rel("obj", dog).add_rel("plc", park))
print("UNL graph for: 'A man is walking his dog in the park.'")
print(to_unl_str(walk))

2 · EN -> UNL encoder (analysis stage)#

CMAP = {
    "walk":"walk(icl>action)","walking":"walk(icl>action)",
    "play":"play(icl>action)","playing":"play(icl>action)",
    "read":"read(icl>action)","reading":"read(icl>action)",
    "wait":"wait(icl>action)","waiting":"wait(icl>action)",
    "ride":"ride(icl>action)","riding":"ride(icl>action)",
    "chase":"chase(icl>action)","chasing":"chase(icl>action)",
    "take":"take(icl>action)","taking":"take(icl>action)",
    "feed":"feed(icl>action)","feeding":"feed(icl>action)",
    "dance":"dance(icl>action)","dancing":"dance(icl>action)",
    "sleep":"sleep(icl>action)","sleeping":"sleep(icl>action)",
    "sail":"sail(icl>action)","sailing":"sail(icl>action)",
    "climb":"climb(icl>action)","climbing":"climb(icl>action)",
    "repair":"repair(icl>action)","repairing":"repair(icl>action)",
    "carry":"carry(icl>action)","carrying":"carry(icl>action)",
    "kick":"kick(icl>action)","kicking":"kick(icl>action)",
    "sit":"sit(icl>action)","sitting":"sit(icl>action)",
    "run":"run(icl>action)","running":"run(icl>action)",
    "prepare":"prepare(icl>action)","preparing":"prepare(icl>action)",
    "man":"man(icl>person)","men":"man(icl>person)",
    "woman":"woman(icl>person)","child":"child(icl>person)",
    "children":"child(icl>person)","tourist":"tourist(icl>person)",
    "tourists":"tourist(icl>person)","worker":"worker(icl>person)",
    "workers":"worker(icl>person)","firefighter":"firefighter(icl>person)",
    "firefighters":"firefighter(icl>person)",
    "person":"person(icl>person)","people":"person(icl>person)",
    "musician":"musician(icl>person)","couple":"couple(icl>person)",
    "chef":"chef(icl>person)","boy":"boy(icl>person)",
    "girl":"girl(icl>person)","dog":"dog(icl>animal)",
    "cat":"cat(icl>animal)","duck":"duck(icl>animal)",
    "ducks":"duck(icl>animal)","flower":"flower(icl>plant)",
    "flowers":"flower(icl>plant)",
    "park":"park(icl>place)","street":"street(icl>place)",
    "lake":"lake(icl>place)","field":"field(icl>place)",
    "beach":"beach(icl>place)","kitchen":"kitchen(icl>place)",
    "windowsill":"windowsill(icl>place)","cafe":"cafe(icl>place)",
    "road":"road(icl>place)","pond":"pond(icl>place)",
    "stop":"stop(icl>place)",
    "fountain":"fountain(icl>thing)","boat":"boat(icl>thing)",
    "bench":"bench(icl>thing)","violin":"violin(icl>thing)",
    "ladder":"ladder(icl>thing)","newspaper":"newspaper(icl>thing)",
    "wall":"wall(icl>thing)","book":"book(icl>thing)",
    "coat":"coat(icl>thing)","ball":"ball(icl>thing)",
    "groceries":"grocery(icl>thing)","photographs":"photograph(icl>thing)",
    "football":"football(icl>thing)","rain":"rain(icl>thing)",
    "chess":"chess(icl>thing)","stairs":"stair(icl>thing)",
    "food":"food(icl>thing)","group":"group(icl>thing)",
    "flight":"flight(icl>thing)",
}
PREP_ROLES = {"in":"plc","on":"plc","near":"plc","at":"plc",
              "by":"plc","through":"plc","up":"plc","against":"plc"}

def encode(sentence):
    tokens = sentence.lower().rstrip(" .").split()
    verb = next((t for t in tokens if t in CMAP and "action" in CMAP[t]), tokens[0])
    vnode = UNLNode(CMAP.get(verb, verb), attrs=["entry","present"])
    vidx = tokens.index(verb) if verb in tokens else 0
    subj = next((UNLNode(CMAP[t]) for t in tokens[:vidx]
                 if t in CMAP and "person" in CMAP[t]), None)
    if subj: vnode.add_rel("agt", subj)
    after = tokens[vidx+1:]
    for i, t in enumerate(after):
        if t in PREP_ROLES:
            loc = next((UNLNode(CMAP[t2]) for t2 in after[i+1:] if t2 in CMAP), None)
            if loc: vnode.add_rel(PREP_ROLES[t], loc); break
        elif t in CMAP and "action" not in CMAP[t] and "obj" not in vnode.relations:
            vnode.add_rel("obj", UNLNode(CMAP[t]))
    return vnode

sample = "A man is walking his dog in the park ."
unl = encode(sample)
print(f"Input: {sample}")
print("UNL:")
print(to_unl_str(unl))

3 · UNL -> DE generator (synthesis stage)#

DE = {
    "walk(icl>action)": ("gehen", "sg:geht pl:gehen"),
    "play(icl>action)": ("spielen","sg:spielt pl:spielen"),
    "read(icl>action)": ("lesen","sg:liest pl:lesen"),
    "wait(icl>action)": ("warten","sg:wartet pl:warten"),
    "ride(icl>action)": ("fahren","sg:faehrt pl:fahren"),
    "chase(icl>action)":("jagen","sg:jagt pl:jagen"),
    "take(icl>action)": ("aufnehmen","sg:nimmt pl:nehmen"),
    "feed(icl>action)": ("fuettern","sg:fuettert pl:fuettern"),
    "dance(icl>action)":("tanzen","sg:tanzt pl:tanzen"),
    "sleep(icl>action)":("schlafen","sg:schlaeft pl:schlafen"),
    "sail(icl>action)": ("segeln","sg:segelt pl:segeln"),
    "climb(icl>action)":("klettern","sg:klettert pl:klettern"),
    "repair(icl>action)":("reparieren","sg:repariert pl:reparieren"),
    "carry(icl>action)":("tragen","sg:traegt pl:tragen"),
    "kick(icl>action)": ("treten","sg:tritt pl:treten"),
    "sit(icl>action)":  ("sitzen","sg:sitzt pl:sitzen"),
    "run(icl>action)":  ("laufen","sg:laeuft pl:laufen"),
    "prepare(icl>action)":("zubereiten","sg:bereitet_zu pl:bereiten_zu"),
    "man(icl>person)":"ein Mann","woman(icl>person)":"eine Frau",
    "child(icl>person)":"ein Kind","tourist(icl>person)":"ein Tourist",
    "worker(icl>person)":"ein Arbeiter","firefighter(icl>person)":"Feuerwehrleute",
    "person(icl>person)":"eine Person","musician(icl>person)":"ein Musiker",
    "couple(icl>person)":"ein Paar","chef(icl>person)":"ein Koch",
    "boy(icl>person)":"ein Junge","girl(icl>person)":"ein Maedchen",
    "dog(icl>animal)":"ein Hund","cat(icl>animal)":"eine Katze",
    "duck(icl>animal)":"eine Ente","flower(icl>plant)":"eine Blume",
    "park(icl>place)":"einem Park","street(icl>place)":"einer Strasse",
    "lake(icl>place)":"einem See","field(icl>place)":"einem Feld",
    "beach(icl>place)":"einem Strand","kitchen(icl>place)":"einer Kueche",
    "windowsill(icl>place)":"einem Fensterbrett","cafe(icl>place)":"einem Cafe",
    "road(icl>place)":"einer Strasse","pond(icl>place)":"einem Teich",
    "stop(icl>place)":"einer Haltestelle",
    "fountain(icl>thing)":"einem Brunnen","boat(icl>thing)":"ein Boot",
    "bench(icl>thing)":"einer Bank","violin(icl>thing)":"eine Geige",
    "ladder(icl>thing)":"eine Leiter","newspaper(icl>thing)":"eine Zeitung",
    "wall(icl>thing)":"eine Mauer","book(icl>thing)":"ein Buch",
    "coat(icl>thing)":"einem Mantel","ball(icl>thing)":"einen Ball",
    "grocery(icl>thing)":"Einkaeufe","photograph(icl>thing)":"Fotos",
    "football(icl>thing)":"einen Fussball","rain(icl>thing)":"dem Regen",
    "chess(icl>thing)":"Schach","stair(icl>thing)":"einer Treppe",
    "food(icl>thing)":"Essen","group(icl>thing)":"eine Gruppe",
    "flight(icl>thing)":"einer Treppe",
}

PLURAL = {"Feuerwehrleute","Einkaeufe","Fotos","Schach","Essen"}

def get_verb(uw, plural):
    entry = DE.get(uw, ("---","sg:--- pl:---"))
    if isinstance(entry, tuple):
        key = "pl:" if plural else "sg:"
        v = next((p.split(":")[1] for p in entry[1].split() if p.startswith(key)), entry[0])
        return v.replace("_", " ")
    return entry

def get_noun(uw):
    entry = DE.get(uw, uw)
    return entry if isinstance(entry, str) else uw

def generate_de(node):
    subj = node.relations.get("agt")
    obj  = node.relations.get("obj")
    plc  = node.relations.get("plc")
    subj_str = get_noun(subj.uw) if subj else ""
    is_pl = subj_str in PLURAL
    verb_str = get_verb(node.uw, is_pl)
    parts = [p for p in [subj_str, verb_str,
                         get_noun(obj.uw) if obj else "",
                         ("in " + get_noun(plc.uw)) if plc else ""] if p]
    return " ".join(parts).capitalize() + "."

sample = "A man is walking his dog in the park ."
unl = encode(sample)
de = generate_de(unl)
print("EN: " + sample)
a = unl.relations.get("agt", None)
o = unl.relations.get("obj", None)
p = unl.relations.get("plc", None)
print("UNL: " + unl.uw)
print("  agt: " + (a.uw if a else "-"))
print("  obj: " + (o.uw if o else "-"))
print("  plc: " + (p.uw if p else "-"))
print("DE: " + de)

4 · N languages, 2N components — the scaling argument#

cols = ("N langs", "Direct (N^2)", "Interlingua (2N)")
print(f"{cols[0]:>8}  {cols[1]:>14}  {cols[2]:>18}")
print("-" * 46)
for n in [2, 5, 10, 20, 50, 100, 200]:
    d, il = n*(n-1), 2*n
    print(f"{n:8d}  {d:14d}  {il:18d}")
print()
print("At 200 languages: 39,800 direct pairs vs 400 interlingua components.")
print("The catch: building a truly universal semantic representation")
print("is arguably AI-complete -- the core challenge UNL never solved.")

5 · Time-machine — EN -> UNL -> DE round-trip#

TM = [
    "A man is walking his dog in the park .",
    "Two children are playing near a fountain .",
    "A woman in a red coat is reading a book .",
    "Several people are waiting at a bus stop .",
    "A cyclist is riding through a crowded street .",
    "A dog is chasing a ball on the beach .",
    "A group of tourists is taking photographs .",
    "A young girl is feeding ducks by a pond .",
    "Two men are playing chess in a cafe .",
    "A street musician is playing the violin .",
    "Children are running through a field of flowers .",
    "An old man is sitting on a bench reading a newspaper .",
    "A woman is carrying groceries up a flight of stairs .",
    "A boy is kicking a football against a wall .",
    "A couple is dancing on an empty street .",
    "A chef is preparing food in an outdoor kitchen .",
    "A cat is sleeping on a warm windowsill .",
    "Workers are repairing a road in the rain .",
    "A small boat is sailing on a calm lake .",
    "Firefighters are climbing a tall ladder .",
]
print("=== Chapter 13 * Time-Machine: Interlingua EN -> UNL -> DE ===")
print()
for s in TM:
    de = generate_de(encode(s))
    print(f"  EN: {s}")
    print(f"  DE: {de}")
    print()
print("Quality verdict: coherent on simple SVO sentences;")
print("struggles with modifiers -- exactly what defeated real interlingua projects.")