mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-07-16 15:27:08 -05:00
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.
457 lines
14 KiB
Python
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)
|
|
|