CS 165: Natural Language Processing
Week 1 - Thursday X-Hour
By the end of this session, you will:
This notebook is designed to run in Google Colab or any Jupyter environment. No external dependencies required!
ELIZA (1966) by Joseph Weizenbaum was one of the first chatbots. It simulated a Rogerian psychotherapist using:
Let's build this step by step.
import re
import random
from typing import List, Tuple, Optional
ELIZA needs to swap pronouns when reflecting statements back to the user.
# Reflection dictionary: swaps first-person to second-person and vice versa
REFLECTIONS = {
"am": "are",
"was": "were",
"i": "you",
"i'd": "you would",
"i've": "you have",
"i'll": "you will",
"my": "your",
"are": "am",
"you've": "I have",
"you'll": "I will",
"your": "my",
"yours": "mine",
"you": "me",
"me": "you",
"myself": "yourself",
"yourself": "myself"
}
def reflect(text: str) -> str:
"""
Reflect pronouns in the text from first to second person and vice versa.
Args:
text: Input string to reflect
Returns:
Reflected string
"""
words = text.lower().split()
reflected_words = [REFLECTIONS.get(word, word) for word in words]
return ' '.join(reflected_words)
# Test the reflection function
print("Original: 'I am sad'")
print(f"Reflected: '{reflect('I am sad')}'")
print()
print("Original: 'my mother thinks you are crazy'")
print(f"Reflected: '{reflect('my mother thinks you are crazy')}'")
Try these inputs and predict the output before running:
# Your code here
test_sentences = [
"I think my father hates me",
"you are not listening to me",
"I've been feeling lonely"
]
for sentence in test_sentences:
print(f"Original: {sentence}")
print(f"Reflected: {reflect(sentence)}")
print()
ELIZA's power comes from its pattern-response database. Each pattern has:
# Pattern-response database
# Format: (pattern, [responses])
# Use {0}, {1}, etc. for captured groups
PATTERNS = [
# High priority: specific patterns
(r'quit|exit|bye|goodbye',
["Goodbye. It was nice talking to you.",
"Thank you for talking with me. Goodbye."]),
(r'hello|hi|hey',
["Hello. How are you feeling today?",
"Hi there. What brings you here today?"]),
# Family patterns
(r'.*\bmother\b.*',
["Tell me more about your mother.",
"How is your relationship with your mother?",
"What else comes to mind when you think of your mother?"]),
(r'.*\bfather\b.*',
["Tell me more about your father.",
"How do you feel about your father?",
"What role does your father play in your life?"]),
(r'.*\bfamily\b.*',
["Tell me more about your family.",
"How is your relationship with your family?"]),
# Emotion patterns with capture groups
(r'i am (sad|depressed|unhappy).*',
["I'm sorry to hear that you are {0}. Can you tell me why?",
"Why do you think you are {0}?",
"How long have you been {0}?"]),
(r'i am (happy|glad|excited).*',
["That's wonderful that you are {0}! What made you feel this way?",
"It's great to hear you are {0}. Tell me more."]),
(r'i (feel|felt) (.*)',
["Why do you feel {1}?",
"Tell me more about feeling {1}.",
"When did you start feeling {1}?"]),
# "I am" patterns
(r'i am (.*)',
["Why do you say you are {0}?",
"How long have you been {0}?",
"Do you believe it is normal to be {0}?",
"Do you enjoy being {0}?"]),
# "I want/need" patterns
(r'i (want|need) (.*)',
["Why do you {0} {1}?",
"What would it mean to you if you got {1}?",
"What if you never got {1}?"]),
# Dream patterns
(r'i (dream|dreamed|dreamt) (.*)',
["What does that dream suggest to you?",
"Do you dream often about {1}?",
"What do you think that dream means?"]),
# Question patterns
(r'why (.*)',
["Why do you think {0}?",
"What makes you ask that?",
"Does that question interest you?"]),
(r'how (.*)',
["How do you suppose {0}?",
"What makes you think about {0}?"]),
# "I remember" patterns
(r'i remember (.*)',
["Do you often think of {0}?",
"What else do you remember about {0}?",
"Why do you recall {0} right now?"]),
# "I think" patterns
(r'i think (.*)',
["Do you really think {0}?",
"What makes you think {0}?",
"Do you doubt that {0}?"]),
# "Yes/No" responses
(r'^yes$|^yeah$|^yep$',
["You seem quite certain.",
"I see. Can you elaborate?"]),
(r'^no$|^nope$',
["Why not?",
"Are you sure?",
"Can you explain?"]),
# Catch-all patterns
(r'.*',
["Can you elaborate on that?",
"I see. Please continue.",
"Very interesting. Tell me more.",
"How does that make you feel?",
"What does that suggest to you?"])
]
print(f"Loaded {len(PATTERNS)} pattern-response pairs")
Now we'll implement the core matching and response generation logic.
class Eliza:
"""
ELIZA chatbot implementation using pattern matching.
"""
def __init__(self, patterns: List[Tuple[str, List[str]]]):
"""
Initialize ELIZA with a pattern database.
Args:
patterns: List of (pattern, responses) tuples
"""
self.patterns = [(re.compile(p, re.IGNORECASE), r) for p, r in patterns]
self.history = [] # Store conversation history
def respond(self, user_input: str) -> str:
"""
Generate a response to user input.
Args:
user_input: User's message
Returns:
ELIZA's response
"""
# Store input in history
self.history.append(("User", user_input))
# Clean and normalize input
user_input = user_input.strip().lower()
# Try to match patterns in order
for pattern, responses in self.patterns:
match = pattern.match(user_input)
if match:
# Select a random response template
response = random.choice(responses)
# Fill in captured groups
if match.groups():
# Reflect the captured text
reflected_groups = [reflect(g) if g else '' for g in match.groups()]
response = response.format(*reflected_groups)
self.history.append(("ELIZA", response))
return response
# Should never reach here due to catch-all pattern
default = "I'm not sure I understand. Can you rephrase?"
self.history.append(("ELIZA", default))
return default
def print_history(self, n: Optional[int] = None):
"""
Print conversation history.
Args:
n: Number of recent exchanges to print (None for all)
"""
history = self.history[-n:] if n else self.history
for speaker, message in history:
print(f"{speaker}: {message}")
# Create an ELIZA instance
eliza = Eliza(PATTERNS)
print("ELIZA initialized and ready!")
# Test conversation
test_inputs = [
"Hello",
"I am feeling sad",
"I think my mother doesn't understand me",
"I want to be happy",
"Why do you ask so many questions?"
]
for user_input in test_inputs:
response = eliza.respond(user_input)
print(f"User: {user_input}")
print(f"ELIZA: {response}")
print()
Now let's have a real conversation with ELIZA!
def chat_with_eliza(max_turns: int = 10):
"""
Interactive chat session with ELIZA.
Args:
max_turns: Maximum number of conversation turns
"""
eliza_bot = Eliza(PATTERNS)
print("ELIZA: Hello. I am ELIZA. How can I help you today?")
print("(Type 'quit' to exit)\n")
for turn in range(max_turns):
user_input = input("You: ").strip()
if not user_input:
continue
response = eliza_bot.respond(user_input)
print(f"ELIZA: {response}\n")
if re.match(r'quit|exit|bye|goodbye', user_input.lower()):
break
return eliza_bot
# Uncomment to chat:
# my_eliza = chat_with_eliza()
Let's build tools to understand which patterns are matching and why.
def debug_pattern_match(user_input: str, patterns: List[Tuple[str, List[str]]]):
"""
Show which patterns match a given input and in what order.
Args:
user_input: Input text to test
patterns: List of (pattern, responses) tuples
"""
user_input = user_input.strip().lower()
print(f"Testing input: '{user_input}'\n")
print("Matching patterns (in priority order):")
print("-" * 80)
matches_found = 0
for i, (pattern_str, responses) in enumerate(patterns):
pattern = re.compile(pattern_str, re.IGNORECASE)
match = pattern.match(user_input)
if match:
matches_found += 1
print(f"\n✓ Pattern {i+1}: {pattern_str}")
print(f" Captured groups: {match.groups()}")
print(f" Possible responses ({len(responses)}):")
for j, resp in enumerate(responses[:3], 1): # Show first 3
print(f" {j}. {resp}")
# Show what the response would be
response = responses[0]
if match.groups():
reflected_groups = [reflect(g) if g else '' for g in match.groups()]
response = response.format(*reflected_groups)
print(f" → Example output: '{response}'")
if matches_found == 1:
print(" ⭐ This pattern would be used (first match)")
if matches_found == 0:
print("No patterns matched!")
else:
print(f"\n{matches_found} pattern(s) matched total")
# Test it
debug_pattern_match("I am feeling sad about my mother", PATTERNS)
Use debug_pattern_match() to see which patterns match these inputs:
For each, identify:
# Your code here
Now it's your turn to improve ELIZA!
Add patterns for these scenarios:
For each pattern, create 2-3 appropriate response templates.
# Extended patterns - add your new patterns here
EXTENDED_PATTERNS = PATTERNS.copy()
# TODO: Add work-related patterns
# Example:
# EXTENDED_PATTERNS.insert(10, (
# r'.*\bwork\b.*',
# ["Tell me more about your work.",
# "How do you feel about your job?"]
# ))
# Your patterns here:
# Test your extended ELIZA
extended_eliza = Eliza(EXTENDED_PATTERNS)
test_cases = [
"I hate my job",
"My boss is terrible",
"I'm worried about my relationship",
"I feel anxious all the time"
]
for test in test_cases:
print(f"User: {test}")
print(f"ELIZA: {extended_eliza.respond(test)}")
print()
ELIZA has no memory - it doesn't remember what was discussed earlier. Implement a simple memory system:
class ElizaWithMemory(Eliza):
"""
Extended ELIZA with simple topic memory.
"""
def __init__(self, patterns: List[Tuple[str, List[str]]]):
super().__init__(patterns)
self.topics_mentioned = set()
self.memory_keywords = ['mother', 'father', 'family', 'work', 'job',
'friend', 'relationship', 'dream']
def respond(self, user_input: str) -> str:
# TODO: Extract topics from user input
# TODO: Occasionally reference past topics
# TODO: Call parent respond method
# Starter code:
user_lower = user_input.lower()
# Extract topics
for keyword in self.memory_keywords:
if keyword in user_lower:
self.topics_mentioned.add(keyword)
# Your implementation here
return super().respond(user_input)
# Test your implementation
# memory_eliza = ElizaWithMemory(EXTENDED_PATTERNS)
Let's explore what makes ELIZA work and where it fails.
def analyze_pattern_coverage(test_sentences: List[str], patterns: List[Tuple[str, List[str]]]):
"""
Analyze which patterns are used most often on a test set.
"""
pattern_usage = {}
for sentence in test_sentences:
sentence = sentence.strip().lower()
for i, (pattern_str, _) in enumerate(patterns):
pattern = re.compile(pattern_str, re.IGNORECASE)
if pattern.match(sentence):
pattern_usage[i] = pattern_usage.get(i, 0) + 1
break # Only count first match
print("Pattern Usage Statistics:")
print("-" * 80)
sorted_patterns = sorted(pattern_usage.items(), key=lambda x: x[1], reverse=True)
for pattern_idx, count in sorted_patterns[:10]: # Top 10
pattern_str = patterns[pattern_idx][0]
print(f"Pattern {pattern_idx}: {pattern_str[:50]}... → {count} times")
catch_all_idx = len(patterns) - 1
catch_all_count = pattern_usage.get(catch_all_idx, 0)
print(f"\nCatch-all pattern used: {catch_all_count} times ({catch_all_count/len(test_sentences)*100:.1f}%)")
# Test with some realistic inputs
realistic_inputs = [
"Hello, how are you?",
"I'm feeling really depressed lately",
"My mother never understood me",
"I want to be successful",
"Why do I always feel this way?",
"I had a terrible dream last night",
"I think something is wrong with me",
"Nobody at work respects me",
"I need someone to talk to",
"Sometimes I just feel empty inside"
]
analyze_pattern_coverage(realistic_inputs, PATTERNS)
Find inputs that expose ELIZA's limitations:
Document your findings below:
# Test cases that break ELIZA
breaking_inputs = [
# Add your test cases here
"I am sad",
"I am sad", # Repetition
"I am sad",
# More tests...
]
test_eliza = Eliza(PATTERNS)
for inp in breaking_inputs:
print(f"User: {inp}")
print(f"ELIZA: {test_eliza.respond(inp)}")
print()
Discuss these questions with your group:
The ELIZA Effect: Why did people form emotional connections with ELIZA, even knowing it was a simple program? What does this tell us about human conversation?
Pattern Priority: Why does the order of patterns matter? Give an example where reordering patterns would change ELIZA's behavior.
Limitations: What are the fundamental limitations of pattern-matching approaches to NLP? What can they never do?
Modern Applications: Where do we still use pattern matching in modern NLP systems? (Think: preprocessing, rule-based components, etc.)
Evaluation: How would you evaluate whether ELIZA is "good"? What metrics would you use?
Ethics: Weizenbaum was disturbed when people became emotionally attached to ELIZA. What ethical considerations should we have when building conversational AI?
Write your answers here:
(Your discussion notes)
Create an ELIZA-style chatbot for a different domain:
Requirements:
# Your domain-specific ELIZA here
DOMAIN_PATTERNS = [
# Add your patterns
]
DOMAIN_REFLECTIONS = {
# Add domain-specific reflections if needed
}
# Implementation...
Today you learned:
We'll move from rule-based systems to statistical approaches - text classification using machine learning!
Questions? Bring them to office hours or post on Piazza!