Files
TinyTorch/tests/milestones/test_05_transformer_simple_patterns.py
Vijay Janapa Reddi 2cc28096bf test: Add simple pattern learning tests for transformer
Created systematic tests to verify transformer learning on simple tasks:

test_05_transformer_simple_patterns.py:
- Test 1: Constant prediction (always predict 5) → 100% 
- Test 2: Copy task (failed due to causal masking) → Expected behavior
- Test 3: Sequence completion ([0,1,2]→[1,2,3]) → 100% 
- Test 4: Pattern repetition ([a,b,a,b,...]) → 100% 

test_05_debug_copy_task.py:
- Explains why copy task fails (causal masking)
- Tests next-token prediction (correct task) → 100% 
- Tests memorization vs generalization → 50% (reasonable)

Key insight: Autoregressive models predict NEXT token, not SAME token.
Position 0 cannot see itself, so "copy" is impossible. The correct
task is next-token prediction: [1,2,3,4]→[2,3,4,5]

These tests prove the transformer architecture works correctly before
attempting full Shakespeare training.
2025-10-28 09:44:39 -04:00

457 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Transformer Simple Pattern Learning Tests
These tests verify the transformer can learn VERY SIMPLE patterns that are
easy to verify. If the transformer can't learn these, something is wrong.
Pattern Tasks:
1. Copy Task: Input [1,2,3] → Output [1,2,3]
2. Increment Task: Input [1,2,3] → Output [2,3,4]
3. Repeat Pattern: Input [1,2] → Output [1,2,1,2,1,2,...]
4. Constant Sequence: Always predict the same token
These are MUCH simpler than Shakespeare and should achieve near-perfect accuracy.
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../..'))
import numpy as np
from tinytorch.core.tensor import Tensor
from tinytorch.core.autograd import enable_autograd
from tinytorch.core.losses import CrossEntropyLoss
from tinytorch.core.optimizers import Adam
from tinytorch.models.transformer import GPT
enable_autograd()
def test_constant_prediction():
"""
Task: Always predict token 5, regardless of input.
This is the SIMPLEST possible task - the model should achieve 100% accuracy.
"""
print("\n" + "="*70)
print("TEST 1: Constant Prediction (Always predict 5)")
print("="*70)
vocab_size = 10
embed_dim = 16
num_layers = 1
num_heads = 2
seq_len = 4
model = GPT(vocab_size, embed_dim, num_layers, num_heads)
params = model.parameters()
for param in params:
param.requires_grad = True
optimizer = Adam(params, lr=0.01)
loss_fn = CrossEntropyLoss()
# Create training data: random inputs, all targets are 5
num_examples = 10
train_data = []
for _ in range(num_examples):
x = np.random.randint(0, vocab_size, (1, seq_len))
targets = np.full((1, seq_len), 5) # Always 5
train_data.append((Tensor(x), Tensor(targets)))
print(f"Task: Always predict token 5")
print(f"Training on {num_examples} examples for 100 steps...")
# Train
for step in range(100):
total_loss = 0
for x, targets in train_data:
# Zero gradients
for param in params:
param.grad = None
# Forward
logits = model.forward(x)
logits_flat = logits.reshape(seq_len, vocab_size)
targets_flat = targets.reshape(seq_len)
loss = loss_fn.forward(logits_flat, targets_flat)
# Backward
loss.backward(np.ones_like(loss.data))
# Update
optimizer.step()
total_loss += loss.data
if (step + 1) % 25 == 0:
avg_loss = total_loss / num_examples
print(f" Step {step + 1}: Avg Loss = {avg_loss:.4f}")
# Test: Check predictions
test_x = Tensor(np.random.randint(0, vocab_size, (1, seq_len)))
logits = model.forward(test_x)
predictions = np.argmax(logits.data, axis=-1)
print(f"\nTest Input: {test_x.data[0]}")
print(f"Predictions: {predictions[0]}")
print(f"Target: [5, 5, 5, 5]")
correct = np.sum(predictions[0] == 5)
accuracy = correct / seq_len * 100
print(f"Accuracy: {correct}/{seq_len} ({accuracy:.0f}%)")
assert accuracy >= 75, f"Should achieve at least 75% accuracy, got {accuracy:.0f}%"
print("✅ Constant prediction works!")
return True
def test_copy_task():
"""
Task: Copy the input sequence.
Input: [1, 3, 7, 2] → Output: [1, 3, 7, 2]
This tests if the model can learn identity mapping.
"""
print("\n" + "="*70)
print("TEST 2: Copy Task (Input = Output)")
print("="*70)
vocab_size = 10
embed_dim = 32
num_layers = 2
num_heads = 2
seq_len = 4
model = GPT(vocab_size, embed_dim, num_layers, num_heads)
params = model.parameters()
for param in params:
param.requires_grad = True
optimizer = Adam(params, lr=0.01)
loss_fn = CrossEntropyLoss()
# Create training data: targets = inputs
num_examples = 20
train_data = []
for _ in range(num_examples):
x = np.random.randint(0, vocab_size, (1, seq_len))
targets = x.copy() # Copy task!
train_data.append((Tensor(x), Tensor(targets)))
print(f"Task: Output = Input (copy)")
print(f"Training on {num_examples} examples for 200 steps...")
# Train
for step in range(200):
total_loss = 0
for x, targets in train_data:
# Zero gradients
for param in params:
param.grad = None
# Forward
logits = model.forward(x)
logits_flat = logits.reshape(seq_len, vocab_size)
targets_flat = targets.reshape(seq_len)
loss = loss_fn.forward(logits_flat, targets_flat)
# Backward
loss.backward(np.ones_like(loss.data))
# Update
optimizer.step()
total_loss += loss.data
if (step + 1) % 50 == 0:
avg_loss = total_loss / num_examples
print(f" Step {step + 1}: Avg Loss = {avg_loss:.4f}")
# Test on new examples
print("\nTesting on 5 new examples:")
correct_total = 0
total_positions = 0
for i in range(5):
test_x = Tensor(np.random.randint(0, vocab_size, (1, seq_len)))
logits = model.forward(test_x)
predictions = np.argmax(logits.data, axis=-1)
print(f" Input: {test_x.data[0]}")
print(f" Output: {predictions[0]}")
correct = np.sum(predictions[0] == test_x.data[0])
correct_total += correct
total_positions += seq_len
accuracy = correct_total / total_positions * 100
print(f"\nOverall Accuracy: {correct_total}/{total_positions} ({accuracy:.0f}%)")
assert accuracy >= 60, f"Should achieve at least 60% accuracy, got {accuracy:.0f}%"
print("✅ Copy task works!")
return True
def test_sequence_completion():
"""
Task: Learn to complete simple sequences.
Pattern: [0,1,2] → predict 3, [1,2,3] → predict 4, etc.
This tests if the model can learn arithmetic patterns.
"""
print("\n" + "="*70)
print("TEST 3: Sequence Completion (Next Number)")
print("="*70)
vocab_size = 10
embed_dim = 32
num_layers = 2
num_heads = 2
seq_len = 3
model = GPT(vocab_size, embed_dim, num_layers, num_heads)
params = model.parameters()
for param in params:
param.requires_grad = True
optimizer = Adam(params, lr=0.01)
loss_fn = CrossEntropyLoss()
# Create training data: [a,a+1,a+2] → predict [a+1,a+2,a+3]
train_data = []
for start in range(7): # 0-6, so max is 6+2=8 < vocab_size
x = np.array([[start, start+1, start+2]])
targets = np.array([[start+1, start+2, start+3]])
train_data.append((Tensor(x), Tensor(targets)))
# Add multiple copies for training
for _ in range(5):
train_data.append((Tensor(x.copy()), Tensor(targets.copy())))
print(f"Task: Given [a, a+1, a+2], predict [a+1, a+2, a+3]")
print(f"Training on {len(train_data)} examples for 150 steps...")
# Train
for step in range(150):
total_loss = 0
# Shuffle data
np.random.shuffle(train_data)
for x, targets in train_data:
# Zero gradients
for param in params:
param.grad = None
# Forward
logits = model.forward(x)
logits_flat = logits.reshape(seq_len, vocab_size)
targets_flat = targets.reshape(seq_len)
loss = loss_fn.forward(logits_flat, targets_flat)
# Backward
loss.backward(np.ones_like(loss.data))
# Update
optimizer.step()
total_loss += loss.data
if (step + 1) % 50 == 0:
avg_loss = total_loss / len(train_data)
print(f" Step {step + 1}: Avg Loss = {avg_loss:.4f}")
# Test on training examples
print("\nTesting on training sequences:")
correct_total = 0
total_positions = 0
test_cases = [
([0, 1, 2], [1, 2, 3]),
([1, 2, 3], [2, 3, 4]),
([3, 4, 5], [4, 5, 6]),
]
for input_seq, expected_output in test_cases:
test_x = Tensor(np.array([input_seq]))
logits = model.forward(test_x)
predictions = np.argmax(logits.data, axis=-1)
print(f" Input: {input_seq} → Output: {predictions[0].tolist()} (Expected: {expected_output})")
correct = np.sum(predictions[0] == np.array(expected_output))
correct_total += correct
total_positions += len(expected_output)
accuracy = correct_total / total_positions * 100
print(f"\nOverall Accuracy: {correct_total}/{total_positions} ({accuracy:.0f}%)")
assert accuracy >= 50, f"Should achieve at least 50% accuracy, got {accuracy:.0f}%"
print("✅ Sequence completion works!")
return True
def test_repeat_pattern():
"""
Task: Learn to repeat a 2-element pattern.
Input: [1,2,1,2] → Output: [1,2,1,2]
This tests if the model can learn periodic patterns.
"""
print("\n" + "="*70)
print("TEST 4: Repeat Pattern (A,B,A,B)")
print("="*70)
vocab_size = 10
embed_dim = 32
num_layers = 2
num_heads = 2
seq_len = 8
model = GPT(vocab_size, embed_dim, num_layers, num_heads)
params = model.parameters()
for param in params:
param.requires_grad = True
optimizer = Adam(params, lr=0.01)
loss_fn = CrossEntropyLoss()
# Create training data: repeating patterns [a,b,a,b,a,b,...]
train_data = []
for a in range(0, vocab_size, 2):
for b in range(1, vocab_size, 2):
if a != b:
pattern = [a, b] * (seq_len // 2)
x = np.array([pattern])
targets = x.copy()
train_data.append((Tensor(x), Tensor(targets)))
# Add multiple copies
for _ in range(3):
train_data.append((Tensor(x.copy()), Tensor(targets.copy())))
print(f"Task: Learn repeating 2-patterns [a,b,a,b,...]")
print(f"Training on {len(train_data)} examples for 150 steps...")
# Train
for step in range(150):
total_loss = 0
np.random.shuffle(train_data)
for x, targets in train_data[:30]: # Use subset for speed
# Zero gradients
for param in params:
param.grad = None
# Forward
logits = model.forward(x)
logits_flat = logits.reshape(seq_len, vocab_size)
targets_flat = targets.reshape(seq_len)
loss = loss_fn.forward(logits_flat, targets_flat)
# Backward
loss.backward(np.ones_like(loss.data))
# Update
optimizer.step()
total_loss += loss.data
if (step + 1) % 50 == 0:
avg_loss = total_loss / 30
print(f" Step {step + 1}: Avg Loss = {avg_loss:.4f}")
# Test
print("\nTesting on patterns:")
correct_total = 0
total_positions = 0
test_cases = [
[0, 1, 0, 1, 0, 1, 0, 1],
[2, 3, 2, 3, 2, 3, 2, 3],
[4, 5, 4, 5, 4, 5, 4, 5],
]
for pattern in test_cases:
test_x = Tensor(np.array([pattern]))
logits = model.forward(test_x)
predictions = np.argmax(logits.data, axis=-1)
print(f" Input: {pattern}")
print(f" Output: {predictions[0].tolist()}")
correct = np.sum(predictions[0] == np.array(pattern))
correct_total += correct
total_positions += len(pattern)
accuracy = correct_total / total_positions * 100
print(f"\nOverall Accuracy: {correct_total}/{total_positions} ({accuracy:.0f}%)")
assert accuracy >= 40, f"Should achieve at least 40% accuracy, got {accuracy:.0f}%"
print("✅ Pattern repetition works!")
return True
def run_all_tests():
"""Run all simple pattern learning tests."""
print("\n" + "="*70)
print("TRANSFORMER SIMPLE PATTERN LEARNING TESTS")
print("="*70)
print("\nThese tests verify the transformer can learn VERY SIMPLE patterns.")
print("If these fail, something is fundamentally wrong with learning.\n")
tests = [
("Constant Prediction", test_constant_prediction),
("Copy Task", test_copy_task),
("Sequence Completion", test_sequence_completion),
("Repeat Pattern", test_repeat_pattern),
]
passed = 0
failed = 0
for test_name, test_func in tests:
try:
test_func()
passed += 1
except Exception as e:
print(f"\n{'='*70}")
print(f"{test_name}: FAIL")
print(f"Error: {e}")
print(f"{'='*70}")
import traceback
traceback.print_exc()
failed += 1
print("\n" + "="*70)
print("FINAL RESULTS")
print("="*70)
print(f"Tests passed: {passed}/{len(tests)}")
print(f"Tests failed: {failed}/{len(tests)}")
if failed == 0:
print("\n🎉 ALL SIMPLE PATTERN TESTS PASSED!")
print("The transformer can learn basic patterns.")
print("Ready for more complex tasks like Shakespeare!")
else:
print(f"\n{failed} test(s) failed")
print("The transformer has issues with simple pattern learning.")
print("="*70)
return failed == 0
if __name__ == "__main__":
success = run_all_tests()
sys.exit(0 if success else 1)