Skip to content
Expedify
13 min

Expert systems — the machine that reasoned

Fifteen if-then rules that chain into conclusions, work out which questions are worth asking, and can print exactly why they decided. That last part is the one everything after this lesson loses.

Search you can afford ended on a machine that explores. This lesson is about the other half of classical AI. It does not explore anything. It deduces.

Search felt impressive. This felt like talking to an expert.

Fifteen rules, and some of them use the answers of others

Here is the whole system. Fifteen sentences of the form if all of these, then that. That is what an expert says when you ask how they know something.

IF hair                                   THEN mammal
IF gives milk                             THEN mammal
IF feathers                               THEN bird
IF lays eggs AND flies                    THEN bird

IF mammal AND eats meat                   THEN carnivore
IF mammal AND pointed teeth AND claws     THEN carnivore
IF mammal AND hooves                      THEN ungulate

IF carnivore AND tawny AND dark spots     THEN cheetah
IF carnivore AND tawny AND black stripes  THEN tiger
IF ungulate AND black stripes             THEN zebra
IF bird AND cannot fly AND swims          THEN penguin
Eleven of the fifteen. Read the middle block again.

That is the difference between this and a lookup table. Nothing about an animal is visibly mammal. You observe hair. The system concludes mammal, and then uses its own conclusion as an input.

Four observations become a cheetah through a chain three steps deep

Tell it four things you can see and let it run.

The highlighted boxes are not things anybody typed in.

Given hair, eats meat, tawny and dark spots, the engine fires 3 rules across 2 passes and concludes cheetah, through a chain 3 steps deep.

Nobody wrote a cheetah rule that mentions hair.

It asks 5.6 questions instead of 18

Now run it the other way. Give it a goal and let it work backwards. What would I need to know to prove this? Then ask only those things.

There are 18 different questions this system could ask you.

Every animal identified correctly, none of them by asking everything.introduction-to-ai/the-machine-that-reasoned.ipynb

An average of 5.6 questions instead of 18. That is 69%% never asked, and every animal identified correctly. A cheetah takes 4.

You have seen this idea already. Search you can afford spent a lesson on not looking everywhere. Do not ask everything. Ask the thing that could settle it.

Ask it how it knows and it prints the chain

It does not produce a confidence score or a heat map.

cheetah
  carnivore
    mammal
      hair            <- you told me
    eats meat         <- you told me
  tawny               <- you told me
  dark spots          <- you told me
The explanation is not a feature somebody added. It is the reasoning, printed.

Everything after this lesson loses that. A model later in this course will predict better than this system ever could. It will be unable to tell you why, beyond which inputs mattered most.

Systems built exactly this way ran real institutions

MYCIN: Yu et al., Stanford, 1979. XCON: built as R1 at Carnegie Mellon for DEC.

MYCIN

What it did
Diagnosed blood infections and recommended antibiotics, asking questions as it went. In a 1979 Stanford evaluation it prescribed correctly in 80% of test cases, against human specialists who scored between 42% and 65%.
How it ended
Never used on a patient. The obstacles were not technical. Liability, and who is responsible when a program prescribes.

XCON

What it did
Configured VAX computer orders at Digital Equipment Corporation, checking that thousands of parts would work together. It launched on about 500 rules, passed 10,000 by the late 1980s, and saved DEC an estimated $40 million a year.
How it ended
Drowned. Every new product meant new rules, the rules interacted, and eventually a team existed only to keep them consistent.

XCON did not get slower or less accurate. It got unmaintainable. The knowledge kept changing, and every change had to be found, written and reconciled with ten thousand others.

Both machines were built by people writing down what they knew

Classical AI ends with two impressive machines. One explores futures faster than any human. One reasons from expertise and shows its working.

The next two lessons are about what happened when writing it down stopped working. First because the knowledge outgrew anybody's ability to keep writing it. Then because of something worse.

Fifteen rules, forward and backward

content/notebooks/introduction-to-ai/the-machine-that-reasoned.ipynb

Add an animal and the rules to identify it, or remove a rule and see what stops working. Worth trying: ask it to identify something the rules cannot reach and read what it says.

Show the code4 cells
The rule base
RULES = [
    (['hair'],                                   'mammal'),
    (['gives milk'],                             'mammal'),
    (['feathers'],                               'bird'),
    (['lays eggs', 'flies'],                     'bird'),
    (['mammal', 'eats meat'],                    'carnivore'),
    (['mammal', 'pointed teeth', 'claws'],       'carnivore'),
    (['mammal', 'hooves'],                       'ungulate'),
    (['mammal', 'chews cud'],                    'ungulate'),
    (['carnivore', 'tawny', 'dark spots'],       'cheetah'),
    (['carnivore', 'tawny', 'black stripes'],    'tiger'),
    (['ungulate', 'long neck', 'long legs', 'dark spots'], 'giraffe'),
    (['ungulate', 'black stripes'],              'zebra'),
    (['bird', 'cannot fly', 'long neck', 'long legs'],     'ostrich'),
    (['bird', 'cannot fly', 'swims'],            'penguin'),
    (['bird', 'good flyer'],                     'albatross'),
]

ANIMALS = {'cheetah','tiger','giraffe','zebra','ostrich','penguin','albatross'}
DERIVED = {c for _, c in RULES}
ASKABLE = sorted({f for conds, _ in RULES for f in conds} - DERIVED)

record('n_rules', len(RULES))
record('n_animals', len(ANIMALS))
record('n_askable', len(ASKABLE))
Facts in, conclusions out — with the chain kept
def forward(facts):
    known = set(facts); why = {}; fired = 0; rounds = 0
    changed = True
    while changed:
        changed = False; rounds += 1
        for conds, concl in RULES:
            if concl not in known and all(c in known for c in conds):
                known.add(concl); why[concl] = conds; fired += 1; changed = True
    return known, why, fired, rounds

obs = ['hair', 'eats meat', 'tawny', 'dark spots']
known, why, fired, rounds = forward(obs)

record('demo_observations', ', '.join(obs))
record('demo_conclusion', next(k for k in known if k in ANIMALS))
record('demo_fired', fired)
record('demo_rounds', rounds)

def explain(fact, why, depth=0):
    lines = ['  ' * depth + fact]
    for c in why.get(fact, []):
        lines += explain(c, why, depth + 1)
    return lines

chain = explain('cheetah', why)
record('demo_chain_depth', max(l.count('  ') for l in chain))
print('\n'.join(chain))
Twenty questions, won in four
def backward(goal, oracle, asked):
    if goal in DERIVED:
        for conds, concl in RULES:
            if concl != goal: continue
            if all(backward(c, oracle, asked) for c in conds):
                return True
        return False
    if goal not in asked:
        asked[goal] = oracle(goal)
    return asked[goal]

TRUTH = {
    'cheetah':   ['hair','eats meat','tawny','dark spots'],
    'tiger':     ['hair','eats meat','tawny','black stripes'],
    'giraffe':   ['hair','hooves','long neck','long legs','dark spots'],
    'zebra':     ['hair','hooves','black stripes'],
    'ostrich':   ['feathers','cannot fly','long neck','long legs'],
    'penguin':   ['feathers','cannot fly','swims'],
    'albatross': ['feathers','good flyer'],
}

counts = {}
for animal, truth in TRUTH.items():
    asked = {}
    for candidate in ['cheetah','tiger','giraffe','zebra','ostrich','penguin','albatross']:
        if backward(candidate, lambda f: f in truth, asked):
            break
    counts[animal] = (candidate, len(asked))

avg = sum(n for _, n in counts.values()) / len(counts)
record('avg_questions', f'{avg:.1f}')
record('questions_cheetah', counts['cheetah'][1])
record('questions_albatross', counts['albatross'][1])
record('all_correct', 'yes' if all(a == g for g, (a, _) in counts.items()) else 'NO')
record('ask_everything', len(ASKABLE))
record('question_saving', f'{100*(1-avg/len(ASKABLE)):.0f}%')

def plot(ax):
    names = list(counts)
    vals = [counts[n][1] for n in names]
    ax.barh(names, vals)
    for i, v in enumerate(vals):
        ax.text(v + .25, i, str(v), va='center', fontsize=10)
    ax.axvline(len(ASKABLE), ls='--', lw=1.2)
    ax.text(len(ASKABLE) - .4, len(names) - 0.6,
            f'asking everything: {len(ASKABLE)}', ha='right', fontsize=9)
    ax.set_xlim(0, len(ASKABLE) + 1.5)
    ax.set_xlabel('questions the system actually asked')
save_fig('questions-asked', plot, figsize=(7, 3.6))
Depth of reasoning per animal
depths = {}
for animal, truth in TRUTH.items():
    _, w, _, _ = forward(truth)
    depths[animal] = max(l.count('  ') for l in explain(animal, w))
record('max_depth', max(depths.values()))
record('min_depth', min(depths.values()))

def plot(ax):
    names = list(depths)
    ax.bar(names, [depths[n] for n in names])
    for i, n in enumerate(names):
        ax.text(i, depths[n] + .06, str(depths[n]), ha='center', fontsize=10)
    ax.set_ylabel('steps of reasoning')
    ax.set_ylim(0, max(depths.values()) + .7)
    ax.tick_params(axis='x', rotation=30)
save_fig('chain-depth', plot, figsize=(7, 3.4))

Related lessons