How Expert System Rules Work in Animals.ipynb: A Deep Dive into Symbolic AI
Expert system rules in Animals.ipynb work through two inference paradigms: a custom backward-chaining shell that recursively evaluates If/AND/OR rule trees, and a forward-chaining engine using Experta's @Rule decorators that automatically fires rules as facts are asserted.
The Animals.ipynb notebook in Microsoft's AI-For-Beginners repository demonstrates classical symbolic AI through a practical animal identification system. Located at lessons/2-Symbolic/Animals.ipynb, this educational implementation shows how domain knowledge can be encoded as explicit rules and reasoned over without any machine learning—core principles of expert systems that dominated AI research before the neural network revolution.
Backward-Chaining: Goal-Driven Inference
Rule Representation with Python Objects
The backward-chaining implementation defines rules using lightweight Python classes that store structural information without embedded logic. Four primitives form the foundation:
Ask– Wraps user prompts for yes/no or multiple-choice questions (lines 38–50)If– Contains a list of sub-goals that must all be satisfiedAND/OR– Logical combinators inherited from aContentbase class
These classes appear in lessons/2-Symbolic/Animals.ipynb around lines 55–62. They function purely as containers; the actual reasoning logic resides in the KnowledgeBase evaluator.
The Rules Dictionary Structure
Rules map goal names to expression trees in a plain Python dictionary. Each key follows the pattern animal:species or names intermediate attributes:
rules = {
'animal:giraffe': If(['ungulate', 'long neck', 'long legs', 'pattern:dark spots']),
'animal:tiger': If(['mammal', 'carnivor', 'color:red-brown', 'pattern:dark stripes']),
'animal:cheetah': If(['mammal', 'carnivor', 'color:tawny', 'pattern:dark spots', 'fast']),
'ungulate': If(['mammal', 'hooves']),
'ungulate': If(['mammal', 'chew cud']),
'carnivor': If(['mammal', Ask('has sharp teeth?'), Ask('has claws?'), Ask('has forward-looking eyes?')]),
'carnivor': If(['mammal', Ask('eats meat?')])
}
This structure appears around lines 80–95. Multiple rules can share the same key (e.g., two paths to ungulate and carnivor), enabling disjunctive reasoning.
KnowledgeBase Implementation
The KnowledgeBase class (lines 119–146) provides the inference engine through two methods:
| Method | Purpose |
|---|---|
get(name) |
Retrieves facts from working memory or triggers rule evaluation |
eval(expr, field=None) |
Recursively interprets expression trees |
The core evaluation loop (lines 148–166) implements classic backward chaining:
class KnowledgeBase:
def __init__(self, rules):
self.rules = rules
self.memory = {} # Working memory for cached facts
def get(self, name):
if name in self.memory: # Return cached result
return self.memory[name]
# Find matching rules for this goal
for key, value in self.rules.items():
if key.startswith(name):
result = self.eval(value, field=name.split(':')[1] if ':' in name else None)
if result:
self.memory[name] = result
return result
return None
def eval(self, expr, field=None):
if isinstance(expr, Ask):
return expr.ask() # Prompt user interactively
elif isinstance(expr, If):
return all(self.eval(x) for x in expr.parts) # AND semantics
elif isinstance(expr, str):
return self.get(expr) # Recursively solve sub-goal
elif isinstance(expr, list):
return any(self.get(x) for x in expr) # OR semantics
When kb.get('animal') is called, the engine:
- Discovers all
animal:*keys in the rules dictionary - Attempts each in order until one succeeds
- Recursively evaluates prerequisites, asking the user only when encountering
Asknodes - Caches proven facts in
self.memoryto avoid redundant queries
Running the Backward-Chaining System
from ipywidgets import interact_manual
# Initialize with rule set
kb = KnowledgeBase(rules)
# Trigger inference—system will prompt for needed information
@interact_manual()
def find_animal():
animal = kb.get('animal')
print(f"\n*** Inferred animal: {animal} ***")
The interactive session prompts for characteristics only when required to distinguish between possible animals, demonstrating lazy, goal-driven reasoning.
Forward-Chaining: Data-Driven Inference with Experta
Declarative Rule Syntax
The same knowledge base is reimplemented using Experta, a Python production rule engine inspired by CLIPS. Rules are declared as decorated methods within a KnowledgeEngine subclass starting at line 304:
from experta import KnowledgeEngine, Fact, Rule, OR, AND, MATCH
class Animals(KnowledgeEngine):
@Rule(OR(
AND(Fact('sharp teeth'), Fact('claws'), Fact('forward looking eyes')),
Fact('eats meat')))
def carnivor(self):
self.declare(Fact('carnivor'))
@Rule(Fact('mammal'), Fact('carnivor'),
Fact(color='red-brown'), Fact(pattern='dark stripes'))
def tiger(self):
self.declare(Fact(animal='tiger'))
@Rule(Fact(animal=MATCH.a))
def print_result(self, a):
print(f"Animal is {a}")
Key Experta constructs include:
@Rule(...)– Decorator specifying activation conditionsFact(pattern)– Matches asserted facts; can usekey=valuefor fields orMATCH.variablefor bindingAND/OR– Logical combinations of conditionsself.declare(Fact(...))– Asserts new facts, potentially triggering additional rules
Forward Inference Execution
The engine maintains a FactList and executes the matching-selecting-acting cycle:
# Initialize engine
engine = Animals()
engine.reset() # Clear and prepare fact base
# Assert initial observations (data-driven start)
engine.declare(
Fact('gives milk'), # From observation
Fact('sharp teeth'), # From observation
Fact('claws'), # From observation
Fact('forward looking eyes'), # From observation
color='red-brown', # Field-style fact
pattern='dark stripes' # Field-style fact
)
# Run inference—rules fire automatically until quiescence
engine.run()
Output:
Animal is tiger
The run() method (implemented in Experta's base class) performs:
- Matching: Compare all rules against current facts
- Selection: Resolve conflicts using salience and recency
- Acting: Execute selected rule's method body, potentially asserting new facts
- Iteration: Repeat until no rules match (fixed-point reached)
Comparison: Backward vs. Forward Chaining
| Aspect | Custom Backward-Chaining | Experta Forward-Chaining |
|---|---|---|
| Rule format | Python objects (If, AND, OR, Ask) |
Decorated methods with @Rule |
| Control flow | Goal-driven; asks what's needed | Data-driven; processes all consequences |
| User interaction | Embedded via Ask nodes |
External; facts must be pre-asserted |
| Implementation | ~150 lines custom code | Library-based (experta dependency) |
| Traceability | Explicit recursive evaluation | Engine-managed matching cycle |
| Extensibility | Modify rules dictionary |
Add methods to KnowledgeEngine subclass |
Both implementations occupy the same notebook to illustrate that inference direction is an architectural choice, not inherent to the knowledge itself.
Extending the Rule Base
Adding a New Animal: Lion
Backward-chaining approach—extend the dictionary:
rules['animal:lion'] = If([
'mammal',
'carnivor',
'color:red-brown',
'pattern:dark spots',
Ask('has a mane?')
])
Forward-chaining approach—add a method to the Animals class:
@Rule(Fact('mammal'), Fact('carnivor'),
Fact(color='red-brown'), Fact(pattern='dark spots'),
Fact('mane'))
def lion(self):
self.declare(Fact(animal='lion'))
The structural equivalence demonstrates how expert systems separate knowledge (rules about lions) from control (how to apply those rules).
Source Locations and Dependencies
| Component | File Location | Lines |
|---|---|---|
Core primitives (Ask, If, AND, OR) |
lessons/2-Symbolic/Animals.ipynb |
38–62 |
| Rules dictionary definition | lessons/2-Symbolic/Animals.ipynb |
80–95 |
KnowledgeBase class |
lessons/2-Symbolic/Animals.ipynb |
119–166 |
Experta Animals engine |
lessons/2-Symbolic/Animals.ipynb |
304–339 |
experta dependency |
environment.yml |
package list |
The repository requires experta for forward-chaining functionality, specified in the project's environment configuration.
Summary
-
Expert system rules in Animals.ipynb encode domain knowledge as structured data—either as nested Python objects for backward chaining or as decorated methods for forward chaining.
-
Backward chaining uses a lightweight
KnowledgeBaseclass that recursively evaluatesIf/AND/ORexpressions, prompting users only for information needed to prove a specific goal. -
Forward chaining leverages the
expertalibrary's production rule engine, where@Ruledecorators declaratively specify conditions andrun()automatically propagates inferences from asserted facts. -
Both approaches demonstrate symbolic AI fundamentals: explicit knowledge representation, logical inference, and separation of domain rules from control procedures.
Frequently Asked Questions
What is the difference between backward and forward chaining in expert systems?
Backward chaining is goal-driven: it starts with a hypothesis (e.g., "is it a tiger?") and works backward to find supporting evidence, asking questions only when necessary. Forward chaining is data-driven: it starts with known facts and applies rules to derive new conclusions, continuing until no more inferences are possible. Animals.ipynb implements both to illustrate this architectural trade-off.
How does the KnowledgeBase.eval() method handle logical combinations?
The eval() method recursively interprets expression trees by type: Ask nodes trigger user interaction, If nodes require all sub-expressions to be true (AND semantics), and plain lists are evaluated with OR semantics (any element true). This simple dispatch logic—around 20 lines in lessons/2-Symbolic/Animals.ipynb lines 148–166—powers the entire backward-chaining inference.
Can the rule base be modified without changing the inference engine?
Yes. In the backward-chaining implementation, adding or modifying entries in the rules dictionary immediately changes system behavior without touching KnowledgeBase. Similarly, in the Experta forward-chaining version, adding new @Rule methods extends the engine's capabilities. This knowledge-engineering separation is a defining characteristic of expert systems.
Why does the notebook include two implementations of the same knowledge base?
The dual implementation serves pedagogical purposes: the custom backward-chaining shell (~150 lines) reveals how inference engines work internally, while the Experta version demonstrates production-grade features like automatic matching and conflict resolution. Students can compare explicit control flow against declarative specification, understanding that the same logical rules support multiple execution strategies.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →