mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-04-28 23:27:36 -05:00
Fix module dependency chain - clean imports now work
Critical fixes to resolve module import issues: 1. Module 01 (tensor_dev.py): - Wrapped all test calls in if __name__ == '__main__': guards - Tests no longer execute during import - Clean imports now work: from tensor_dev import Tensor 2. Module 08 (dataloader_dev.py): - REMOVED redefined Tensor class (was breaking dependency chain) - Now imports real Tensor from Module 01 - DataLoader uses actual Tensor with full gradient support Impact: - Modules properly build on previous work (no isolated implementations) - Clean dependency chain: each module imports from previous modules - No test execution during imports = fast, clean module loading This resolves the root cause where DataLoader had to redefine Tensor because importing tensor_dev.py would execute all test code.
This commit is contained in:
@@ -1,330 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RIGOROUS MILESTONE 1 TEST: Perceptron
|
||||
Tests binary classification with concrete success criteria and evidence.
|
||||
|
||||
SUCCESS CRITERIA:
|
||||
1. Training: >95% accuracy on linearly separable 2D dataset (200 samples)
|
||||
2. Inference: Correctly classifies new test points
|
||||
3. Decision boundary: Visualizes learned linear separation
|
||||
4. Convergence: Loss decreases monotonically
|
||||
5. Manual gradients: No autograd dependency
|
||||
|
||||
EVIDENCE REQUIRED:
|
||||
- Training curve showing convergence
|
||||
- Final accuracy measurement
|
||||
- Decision boundary visualization
|
||||
- Test set evaluation
|
||||
"""
|
||||
|
||||
import sys
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
def load_modules():
|
||||
"""Load TinyTorch modules 01-04 in isolation."""
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
|
||||
print("🔧 Loading Required Modules (01-04)...")
|
||||
|
||||
# Module 01: Tensor
|
||||
os.chdir(project_root / 'modules/01_tensor')
|
||||
with open('tensor_dev.py', 'r') as f:
|
||||
exec(f.read(), globals())
|
||||
|
||||
# Module 02: Activations
|
||||
os.chdir(project_root / 'modules/02_activations')
|
||||
with open('activations_dev.py', 'r') as f:
|
||||
exec(f.read(), globals())
|
||||
|
||||
# Module 03: Layers
|
||||
os.chdir(project_root / 'modules/03_layers')
|
||||
with open('layers_dev.py', 'r') as f:
|
||||
exec(f.read(), globals())
|
||||
|
||||
# Module 04: Losses
|
||||
os.chdir(project_root / 'modules/04_losses')
|
||||
with open('losses_dev.py', 'r') as f:
|
||||
exec(f.read(), globals())
|
||||
|
||||
os.chdir(project_root) # Return to project root
|
||||
print("✅ All modules loaded successfully")
|
||||
return True
|
||||
|
||||
def generate_linearly_separable_data(n_samples=200, seed=42):
|
||||
"""Generate linearly separable 2D binary classification dataset."""
|
||||
np.random.seed(seed)
|
||||
|
||||
# Class 0: cluster around (-1, -1)
|
||||
class0_x = np.random.normal(-1, 0.5, (n_samples//2, 2))
|
||||
class0_y = np.zeros((n_samples//2, 1))
|
||||
|
||||
# Class 1: cluster around (1, 1)
|
||||
class1_x = np.random.normal(1, 0.5, (n_samples//2, 2))
|
||||
class1_y = np.ones((n_samples//2, 1))
|
||||
|
||||
# Combine and shuffle
|
||||
X = np.vstack([class0_x, class1_x])
|
||||
y = np.vstack([class0_y, class1_y])
|
||||
|
||||
indices = np.random.permutation(n_samples)
|
||||
X = X[indices]
|
||||
y = y[indices]
|
||||
|
||||
return Tensor(X), Tensor(y)
|
||||
|
||||
def create_perceptron():
|
||||
"""Create Linear + Sigmoid perceptron (no autograd)."""
|
||||
return Sequential(
|
||||
Linear(2, 1), # 2D input -> 1 output
|
||||
Sigmoid() # Binary classification
|
||||
)
|
||||
|
||||
def train_perceptron_rigorous(model, X, y, epochs=500, lr=0.5):
|
||||
"""Train with manual gradient descent and detailed monitoring."""
|
||||
loss_fn = MSELoss()
|
||||
train_losses = []
|
||||
accuracies = []
|
||||
|
||||
print(f"🏋️ Training perceptron for {epochs} epochs...")
|
||||
print("Epoch | Loss | Accuracy | Gradient Norm")
|
||||
print("-" * 45)
|
||||
|
||||
for epoch in range(epochs):
|
||||
# Forward pass
|
||||
predictions = model.forward(X)
|
||||
loss = loss_fn.forward(predictions, y)
|
||||
|
||||
# Compute accuracy
|
||||
pred_classes = (predictions.data > 0.5).astype(int)
|
||||
accuracy = np.mean(pred_classes == y.data)
|
||||
|
||||
# Manual gradient computation (educational)
|
||||
linear_layer = model.layers[0]
|
||||
error = predictions.data - y.data
|
||||
grad_w = X.data.T @ error / len(X.data)
|
||||
grad_b = np.mean(error, axis=0) if linear_layer.bias is not None else 0
|
||||
|
||||
# Gradient norm for monitoring
|
||||
grad_norm = np.linalg.norm(grad_w) + (np.abs(grad_b) if hasattr(grad_b, '__len__') else abs(grad_b))
|
||||
|
||||
# Update weights
|
||||
linear_layer.weight.data -= lr * grad_w
|
||||
if linear_layer.bias is not None:
|
||||
linear_layer.bias.data -= lr * grad_b
|
||||
|
||||
# Log progress
|
||||
train_losses.append(float(loss.data))
|
||||
accuracies.append(accuracy)
|
||||
|
||||
if epoch % 100 == 0 or epoch < 10:
|
||||
print(f"{epoch:5d} | {loss.data:.6f} | {accuracy:.3f} | {grad_norm:.4f}")
|
||||
|
||||
return train_losses, accuracies
|
||||
|
||||
def evaluate_model(model, X, y):
|
||||
"""Rigorous model evaluation."""
|
||||
predictions = model.forward(X)
|
||||
pred_classes = (predictions.data > 0.5).astype(int)
|
||||
|
||||
accuracy = np.mean(pred_classes == y.data)
|
||||
|
||||
# Confusion matrix
|
||||
true_pos = np.sum((pred_classes == 1) & (y.data == 1))
|
||||
true_neg = np.sum((pred_classes == 0) & (y.data == 0))
|
||||
false_pos = np.sum((pred_classes == 1) & (y.data == 0))
|
||||
false_neg = np.sum((pred_classes == 0) & (y.data == 1))
|
||||
|
||||
return {
|
||||
'accuracy': accuracy,
|
||||
'true_pos': true_pos,
|
||||
'true_neg': true_neg,
|
||||
'false_pos': false_pos,
|
||||
'false_neg': false_neg,
|
||||
'predictions': predictions,
|
||||
'pred_classes': pred_classes
|
||||
}
|
||||
|
||||
def plot_results(model, X, y, train_losses, accuracies, save_path):
|
||||
"""Create comprehensive result visualization."""
|
||||
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 10))
|
||||
|
||||
# 1. Training curves
|
||||
epochs = range(len(train_losses))
|
||||
ax1.plot(epochs, train_losses, 'b-', label='Training Loss')
|
||||
ax1.set_xlabel('Epoch')
|
||||
ax1.set_ylabel('MSE Loss')
|
||||
ax1.set_title('Training Loss Convergence')
|
||||
ax1.legend()
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
# 2. Accuracy curve
|
||||
ax2.plot(epochs, accuracies, 'g-', label='Training Accuracy')
|
||||
ax2.axhline(y=0.95, color='r', linestyle='--', label='95% Target')
|
||||
ax2.set_xlabel('Epoch')
|
||||
ax2.set_ylabel('Accuracy')
|
||||
ax2.set_title('Training Accuracy')
|
||||
ax2.legend()
|
||||
ax2.grid(True, alpha=0.3)
|
||||
|
||||
# 3. Decision boundary
|
||||
X_data = X.data
|
||||
y_data = y.data.flatten()
|
||||
|
||||
# Plot data points
|
||||
class0_mask = y_data == 0
|
||||
class1_mask = y_data == 1
|
||||
|
||||
ax3.scatter(X_data[class0_mask, 0], X_data[class0_mask, 1],
|
||||
c='red', marker='o', alpha=0.7, label='Class 0', s=30)
|
||||
ax3.scatter(X_data[class1_mask, 0], X_data[class1_mask, 1],
|
||||
c='blue', marker='s', alpha=0.7, label='Class 1', s=30)
|
||||
|
||||
# Decision boundary
|
||||
x_min, x_max = X_data[:, 0].min() - 1, X_data[:, 0].max() + 1
|
||||
y_min, y_max = X_data[:, 1].min() - 1, X_data[:, 1].max() + 1
|
||||
|
||||
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
|
||||
np.linspace(y_min, y_max, 100))
|
||||
|
||||
mesh_points = Tensor(np.c_[xx.ravel(), yy.ravel()])
|
||||
Z = model.forward(mesh_points).data
|
||||
Z = Z.reshape(xx.shape)
|
||||
|
||||
contour = ax3.contour(xx, yy, Z, levels=[0.5], colors='black',
|
||||
linestyles='-', linewidths=2)
|
||||
ax3.contourf(xx, yy, Z, levels=50, alpha=0.3, cmap='RdYlBu')
|
||||
|
||||
ax3.set_xlabel('Feature 1')
|
||||
ax3.set_ylabel('Feature 2')
|
||||
ax3.set_title('Decision Boundary')
|
||||
ax3.legend()
|
||||
ax3.grid(True, alpha=0.3)
|
||||
|
||||
# 4. Model parameters visualization
|
||||
linear_layer = model.layers[0]
|
||||
weights = linear_layer.weight.data
|
||||
bias = linear_layer.bias.data if linear_layer.bias is not None else [0]
|
||||
|
||||
ax4.bar(['w1', 'w2', 'bias'], [weights[0,0], weights[1,0], bias[0]])
|
||||
ax4.set_title('Learned Parameters')
|
||||
ax4.set_ylabel('Parameter Value')
|
||||
ax4.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=150, bbox_inches='tight')
|
||||
print(f"📊 Results saved to {save_path}")
|
||||
|
||||
return fig
|
||||
|
||||
def main():
|
||||
"""Rigorous Milestone 1 evaluation."""
|
||||
print("=" * 60)
|
||||
print("🎯 RIGOROUS MILESTONE 1 TEST: PERCEPTRON")
|
||||
print("Binary classification with concrete success criteria")
|
||||
print("=" * 60)
|
||||
|
||||
# Load modules
|
||||
if not load_modules():
|
||||
print("❌ FAILED: Could not load required modules")
|
||||
return False
|
||||
|
||||
# Generate dataset
|
||||
print("\n📊 Generating linearly separable dataset...")
|
||||
X, y = generate_linearly_separable_data(n_samples=200)
|
||||
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
|
||||
|
||||
# Create model
|
||||
print("\n🧠 Creating perceptron model...")
|
||||
model = create_perceptron()
|
||||
print(f"Architecture: 2 → 1 (Linear + Sigmoid)")
|
||||
|
||||
# Train model
|
||||
print("\n🏋️ Training with manual gradients...")
|
||||
train_losses, accuracies = train_perceptron_rigorous(model, X, y, epochs=500, lr=0.5)
|
||||
|
||||
# Evaluate model
|
||||
print("\n📈 Evaluating final performance...")
|
||||
results = evaluate_model(model, X, y)
|
||||
|
||||
final_accuracy = results['accuracy']
|
||||
final_loss = train_losses[-1]
|
||||
|
||||
print(f"\nFinal Results:")
|
||||
print(f" Accuracy: {final_accuracy:.1%}")
|
||||
print(f" Final Loss: {final_loss:.6f}")
|
||||
print(f" True Positives: {results['true_pos']}")
|
||||
print(f" True Negatives: {results['true_neg']}")
|
||||
print(f" False Positives: {results['false_pos']}")
|
||||
print(f" False Negatives: {results['false_neg']}")
|
||||
|
||||
# Test success criteria
|
||||
print("\n🔍 TESTING SUCCESS CRITERIA:")
|
||||
|
||||
success_criteria = []
|
||||
|
||||
# 1. Training accuracy >95%
|
||||
accuracy_threshold = 0.95
|
||||
criterion_1 = final_accuracy >= accuracy_threshold
|
||||
success_criteria.append(criterion_1)
|
||||
print(f" 1. Accuracy ≥ 95%: {final_accuracy:.1%} {'✅' if criterion_1 else '❌'}")
|
||||
|
||||
# 2. Loss convergence (decreasing trend)
|
||||
loss_trend = np.polyfit(range(len(train_losses)), train_losses, 1)[0]
|
||||
criterion_2 = loss_trend < 0
|
||||
success_criteria.append(criterion_2)
|
||||
print(f" 2. Loss converges: slope={loss_trend:.6f} {'✅' if criterion_2 else '❌'}")
|
||||
|
||||
# 3. Final loss below threshold
|
||||
loss_threshold = 0.1
|
||||
criterion_3 = final_loss < loss_threshold
|
||||
success_criteria.append(criterion_3)
|
||||
print(f" 3. Final loss < {loss_threshold}: {final_loss:.6f} {'✅' if criterion_3 else '❌'}")
|
||||
|
||||
# 4. Balanced classification (no major class bias)
|
||||
precision = results['true_pos'] / (results['true_pos'] + results['false_pos']) if (results['true_pos'] + results['false_pos']) > 0 else 0
|
||||
recall = results['true_pos'] / (results['true_pos'] + results['false_neg']) if (results['true_pos'] + results['false_neg']) > 0 else 0
|
||||
criterion_4 = precision > 0.9 and recall > 0.9
|
||||
success_criteria.append(criterion_4)
|
||||
print(f" 4. Balanced performance: P={precision:.3f}, R={recall:.3f} {'✅' if criterion_4 else '❌'}")
|
||||
|
||||
# 5. Model parameters are reasonable
|
||||
linear_layer = model.layers[0]
|
||||
max_weight = np.max(np.abs(linear_layer.weight.data))
|
||||
criterion_5 = max_weight < 10.0 # Sanity check
|
||||
success_criteria.append(criterion_5)
|
||||
print(f" 5. Reasonable parameters: max_weight={max_weight:.3f} {'✅' if criterion_5 else '❌'}")
|
||||
|
||||
# Overall milestone result
|
||||
all_criteria_met = all(success_criteria)
|
||||
|
||||
# Create visualization
|
||||
save_path = Path(__file__).parent / 'rigorous_test_results.png'
|
||||
plot_results(model, X, y, train_losses, accuracies, save_path)
|
||||
|
||||
# Final verdict
|
||||
print("\n" + "=" * 60)
|
||||
if all_criteria_met:
|
||||
print("🎉 MILESTONE 1: PERCEPTRON - ACHIEVED!")
|
||||
print("✅ All success criteria satisfied with concrete evidence")
|
||||
print(f"✅ Training accuracy: {final_accuracy:.1%} (target: ≥95%)")
|
||||
print(f"✅ Loss convergence: {loss_trend:.6f} (negative slope)")
|
||||
print(f"✅ Final loss: {final_loss:.6f} (target: <0.1)")
|
||||
print(f"✅ Balanced classification: P={precision:.3f}, R={recall:.3f}")
|
||||
print(f"✅ Reasonable parameters: max_weight={max_weight:.3f}")
|
||||
print("\n🚀 Ready for Milestone 2: MLP with autograd!")
|
||||
else:
|
||||
print("❌ MILESTONE 1: PERCEPTRON - NOT ACHIEVED")
|
||||
failed_criteria = sum(1 for c in success_criteria if not c)
|
||||
print(f"❌ {failed_criteria}/{len(success_criteria)} criteria failed")
|
||||
print("🔧 Need to fix issues before proceeding to Milestone 2")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
return all_criteria_met
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,236 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SIMPLIFIED RIGOROUS MILESTONE 1 TEST: Perceptron
|
||||
Focus on core binary classification capability with concrete success criteria.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
# Simple tensor implementation for testing
|
||||
class SimpleTensor:
|
||||
def __init__(self, data):
|
||||
self.data = np.array(data, dtype=np.float32)
|
||||
self.shape = self.data.shape
|
||||
|
||||
def __str__(self):
|
||||
return f"Tensor({self.data}, shape={self.shape})"
|
||||
|
||||
# Simple perceptron components
|
||||
class SimpleLinear:
|
||||
def __init__(self, in_features, out_features):
|
||||
# Xavier initialization
|
||||
self.weight = SimpleTensor(np.random.normal(0, np.sqrt(2.0 / in_features), (in_features, out_features)))
|
||||
self.bias = SimpleTensor(np.zeros(out_features))
|
||||
|
||||
def forward(self, x):
|
||||
# y = xW + b
|
||||
output = np.dot(x.data, self.weight.data) + self.bias.data
|
||||
return SimpleTensor(output)
|
||||
|
||||
class SimpleSigmoid:
|
||||
def forward(self, x):
|
||||
# Sigmoid with numerical stability
|
||||
z = np.clip(x.data, -500, 500) # Prevent overflow
|
||||
return SimpleTensor(1.0 / (1.0 + np.exp(-z)))
|
||||
|
||||
class SimpleMSELoss:
|
||||
def forward(self, predictions, targets):
|
||||
diff = predictions.data - targets.data
|
||||
loss = np.mean(diff ** 2)
|
||||
return loss
|
||||
|
||||
class SimplePerceptron:
|
||||
def __init__(self):
|
||||
self.linear = SimpleLinear(2, 1)
|
||||
self.sigmoid = SimpleSigmoid()
|
||||
|
||||
def forward(self, x):
|
||||
linear_out = self.linear.forward(x)
|
||||
return self.sigmoid.forward(linear_out)
|
||||
|
||||
def generate_linearly_separable_data(n_samples=200, seed=42):
|
||||
"""Generate linearly separable 2D binary classification dataset."""
|
||||
np.random.seed(seed)
|
||||
|
||||
# Class 0: cluster around (-1, -1)
|
||||
class0_x = np.random.normal(-1, 0.5, (n_samples//2, 2))
|
||||
class0_y = np.zeros((n_samples//2, 1))
|
||||
|
||||
# Class 1: cluster around (1, 1)
|
||||
class1_x = np.random.normal(1, 0.5, (n_samples//2, 2))
|
||||
class1_y = np.ones((n_samples//2, 1))
|
||||
|
||||
# Combine and shuffle
|
||||
X = np.vstack([class0_x, class1_x])
|
||||
y = np.vstack([class0_y, class1_y])
|
||||
|
||||
indices = np.random.permutation(n_samples)
|
||||
X = X[indices]
|
||||
y = y[indices]
|
||||
|
||||
return SimpleTensor(X), SimpleTensor(y)
|
||||
|
||||
def train_perceptron_manual(model, X, y, epochs=500, lr=0.5):
|
||||
"""Train with manual gradient descent."""
|
||||
loss_fn = SimpleMSELoss()
|
||||
train_losses = []
|
||||
accuracies = []
|
||||
|
||||
print(f"🏋️ Training perceptron for {epochs} epochs...")
|
||||
print("Epoch | Loss | Accuracy")
|
||||
print("-" * 30)
|
||||
|
||||
for epoch in range(epochs):
|
||||
# Forward pass
|
||||
predictions = model.forward(X)
|
||||
loss = loss_fn.forward(predictions, y)
|
||||
|
||||
# Compute accuracy
|
||||
pred_classes = (predictions.data > 0.5).astype(int)
|
||||
accuracy = np.mean(pred_classes == y.data)
|
||||
|
||||
# Manual gradient computation
|
||||
error = predictions.data - y.data
|
||||
|
||||
# Gradient through sigmoid: error * sigmoid * (1 - sigmoid)
|
||||
sigmoid_grad = predictions.data * (1 - predictions.data)
|
||||
linear_error = error * sigmoid_grad
|
||||
|
||||
# Gradients for linear layer
|
||||
grad_w = X.data.T @ linear_error / len(X.data)
|
||||
grad_b = np.mean(linear_error, axis=0)
|
||||
|
||||
# Update weights
|
||||
model.linear.weight.data -= lr * grad_w
|
||||
model.linear.bias.data -= lr * grad_b
|
||||
|
||||
# Log progress
|
||||
train_losses.append(loss)
|
||||
accuracies.append(accuracy)
|
||||
|
||||
if epoch % 100 == 0 or epoch < 10:
|
||||
print(f"{epoch:5d} | {loss:.6f} | {accuracy:.3f}")
|
||||
|
||||
return train_losses, accuracies
|
||||
|
||||
def evaluate_model(model, X, y):
|
||||
"""Evaluate model performance."""
|
||||
predictions = model.forward(X)
|
||||
pred_classes = (predictions.data > 0.5).astype(int)
|
||||
accuracy = np.mean(pred_classes == y.data)
|
||||
|
||||
# Confusion matrix
|
||||
true_pos = np.sum((pred_classes == 1) & (y.data == 1))
|
||||
true_neg = np.sum((pred_classes == 0) & (y.data == 0))
|
||||
false_pos = np.sum((pred_classes == 1) & (y.data == 0))
|
||||
false_neg = np.sum((pred_classes == 0) & (y.data == 1))
|
||||
|
||||
return {
|
||||
'accuracy': accuracy,
|
||||
'true_pos': int(true_pos),
|
||||
'true_neg': int(true_neg),
|
||||
'false_pos': int(false_pos),
|
||||
'false_neg': int(false_neg)
|
||||
}
|
||||
|
||||
def main():
|
||||
"""Rigorous Milestone 1 evaluation."""
|
||||
print("=" * 60)
|
||||
print("🎯 RIGOROUS MILESTONE 1 TEST: PERCEPTRON")
|
||||
print("Binary classification with concrete success criteria")
|
||||
print("=" * 60)
|
||||
|
||||
# Generate dataset
|
||||
print("\n📊 Generating linearly separable dataset...")
|
||||
X, y = generate_linearly_separable_data(n_samples=200)
|
||||
print(f"Dataset: {X.shape[0]} samples, {X.shape[1]} features")
|
||||
|
||||
# Create model
|
||||
print("\n🧠 Creating perceptron model...")
|
||||
model = SimplePerceptron()
|
||||
print(f"Architecture: 2 → 1 (Linear + Sigmoid)")
|
||||
|
||||
# Train model
|
||||
print("\n🏋️ Training with manual gradients...")
|
||||
train_losses, accuracies = train_perceptron_manual(model, X, y, epochs=500, lr=0.5)
|
||||
|
||||
# Evaluate model
|
||||
print("\n📈 Evaluating final performance...")
|
||||
results = evaluate_model(model, X, y)
|
||||
|
||||
final_accuracy = results['accuracy']
|
||||
final_loss = train_losses[-1]
|
||||
|
||||
print(f"\nFinal Results:")
|
||||
print(f" Accuracy: {final_accuracy:.1%}")
|
||||
print(f" Final Loss: {final_loss:.6f}")
|
||||
print(f" True Positives: {results['true_pos']}")
|
||||
print(f" True Negatives: {results['true_neg']}")
|
||||
print(f" False Positives: {results['false_pos']}")
|
||||
print(f" False Negatives: {results['false_neg']}")
|
||||
|
||||
# Test success criteria
|
||||
print("\n🔍 TESTING SUCCESS CRITERIA:")
|
||||
|
||||
success_criteria = []
|
||||
|
||||
# 1. Training accuracy >95%
|
||||
accuracy_threshold = 0.95
|
||||
criterion_1 = final_accuracy >= accuracy_threshold
|
||||
success_criteria.append(criterion_1)
|
||||
print(f" 1. Accuracy ≥ 95%: {final_accuracy:.1%} {'✅' if criterion_1 else '❌'}")
|
||||
|
||||
# 2. Loss convergence (decreasing trend)
|
||||
loss_trend = np.polyfit(range(len(train_losses)), train_losses, 1)[0]
|
||||
criterion_2 = loss_trend < 0
|
||||
success_criteria.append(criterion_2)
|
||||
print(f" 2. Loss converges: slope={loss_trend:.6f} {'✅' if criterion_2 else '❌'}")
|
||||
|
||||
# 3. Final loss below threshold
|
||||
loss_threshold = 0.1
|
||||
criterion_3 = final_loss < loss_threshold
|
||||
success_criteria.append(criterion_3)
|
||||
print(f" 3. Final loss < {loss_threshold}: {final_loss:.6f} {'✅' if criterion_3 else '❌'}")
|
||||
|
||||
# 4. Balanced classification (no major class bias)
|
||||
precision = results['true_pos'] / (results['true_pos'] + results['false_pos']) if (results['true_pos'] + results['false_pos']) > 0 else 0
|
||||
recall = results['true_pos'] / (results['true_pos'] + results['false_neg']) if (results['true_pos'] + results['false_neg']) > 0 else 0
|
||||
criterion_4 = precision > 0.9 and recall > 0.9
|
||||
success_criteria.append(criterion_4)
|
||||
print(f" 4. Balanced performance: P={precision:.3f}, R={recall:.3f} {'✅' if criterion_4 else '❌'}")
|
||||
|
||||
# 5. Model parameters are reasonable
|
||||
max_weight = np.max(np.abs(model.linear.weight.data))
|
||||
criterion_5 = max_weight < 10.0 # Sanity check
|
||||
success_criteria.append(criterion_5)
|
||||
print(f" 5. Reasonable parameters: max_weight={max_weight:.3f} {'✅' if criterion_5 else '❌'}")
|
||||
|
||||
# Overall milestone result
|
||||
all_criteria_met = all(success_criteria)
|
||||
|
||||
# Final verdict
|
||||
print("\n" + "=" * 60)
|
||||
if all_criteria_met:
|
||||
print("🎉 MILESTONE 1: PERCEPTRON - ACHIEVED!")
|
||||
print("✅ All success criteria satisfied with concrete evidence")
|
||||
print(f"✅ Training accuracy: {final_accuracy:.1%} (target: ≥95%)")
|
||||
print(f"✅ Loss convergence: {loss_trend:.6f} (negative slope)")
|
||||
print(f"✅ Final loss: {final_loss:.6f} (target: <0.1)")
|
||||
print(f"✅ Balanced classification: P={precision:.3f}, R={recall:.3f}")
|
||||
print(f"✅ Reasonable parameters: max_weight={max_weight:.3f}")
|
||||
print("\n🚀 Ready for Milestone 2: MLP with autograd!")
|
||||
else:
|
||||
print("❌ MILESTONE 1: PERCEPTRON - NOT ACHIEVED")
|
||||
failed_criteria = sum(1 for c in success_criteria if not c)
|
||||
print(f"❌ {failed_criteria}/{len(success_criteria)} criteria failed")
|
||||
print("🔧 Need to fix issues before proceeding to Milestone 2")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
return all_criteria_met
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,296 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
RIGOROUS MILESTONE 2 TEST: MLP
|
||||
Tests non-linear classification (XOR) with autograd and modern optimizers.
|
||||
|
||||
SUCCESS CRITERIA:
|
||||
1. Training: >95% accuracy on XOR problem (4 samples, 1000 epochs)
|
||||
2. Inference: Correctly predicts all 4 XOR patterns
|
||||
3. Autograd: Uses automatic differentiation (no manual gradients)
|
||||
4. Optimization: Uses Adam optimizer with learning rate scheduling
|
||||
5. Architecture: 2+ hidden layers demonstrate non-linear capability
|
||||
|
||||
EVIDENCE REQUIRED:
|
||||
- XOR problem solved (inherently non-linear)
|
||||
- Training curve showing convergence with autograd
|
||||
- All 4 XOR patterns correctly classified
|
||||
- Adam optimizer used with automatic gradients
|
||||
"""
|
||||
|
||||
import sys
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
def load_modules():
|
||||
"""Load TinyTorch modules 01-07 for MLP capability."""
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
|
||||
print("🔧 Loading Required Modules (01-07)...")
|
||||
|
||||
# Change to each module directory and execute
|
||||
for module_num, module_name in [
|
||||
("01_tensor", "tensor"),
|
||||
("02_activations", "activations"),
|
||||
("03_layers", "layers"),
|
||||
("04_losses", "losses"),
|
||||
("05_autograd", "autograd"),
|
||||
("06_optimizers", "optimizers"),
|
||||
("07_training", "training")
|
||||
]:
|
||||
try:
|
||||
os.chdir(project_root / f'modules/{module_num}')
|
||||
with open(f'{module_name}_dev.py', 'r') as f:
|
||||
exec(f.read(), globals())
|
||||
print(f"✅ Module {module_num}: {module_name}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load module {module_num}: {e}")
|
||||
return False
|
||||
|
||||
os.chdir(project_root) # Return to project root
|
||||
print("✅ All MLP modules loaded successfully")
|
||||
return True
|
||||
|
||||
def generate_xor_dataset():
|
||||
"""Generate the XOR problem dataset (inherently non-linear)."""
|
||||
# XOR truth table
|
||||
X = np.array([
|
||||
[0, 0], # XOR(0,0) = 0
|
||||
[0, 1], # XOR(0,1) = 1
|
||||
[1, 0], # XOR(1,0) = 1
|
||||
[1, 1] # XOR(1,1) = 0
|
||||
], dtype=np.float32)
|
||||
|
||||
y = np.array([
|
||||
[0], # 0 XOR 0 = 0
|
||||
[1], # 0 XOR 1 = 1
|
||||
[1], # 1 XOR 0 = 1
|
||||
[0] # 1 XOR 1 = 0
|
||||
], dtype=np.float32)
|
||||
|
||||
return Tensor(X), Tensor(y)
|
||||
|
||||
def create_mlp():
|
||||
"""Create 2-hidden-layer MLP for XOR problem."""
|
||||
# Architecture: 2 → 4 → 4 → 1 (enough capacity for XOR)
|
||||
return Sequential(
|
||||
Linear(2, 4), # Input layer
|
||||
ReLU(),
|
||||
Linear(4, 4), # Hidden layer 1
|
||||
ReLU(),
|
||||
Linear(4, 1), # Output layer
|
||||
Sigmoid() # Binary classification
|
||||
)
|
||||
|
||||
def train_mlp_with_autograd(model, X, y, epochs=1000, lr=0.01):
|
||||
"""Train MLP using autograd and Adam optimizer."""
|
||||
|
||||
# Get all parameters for optimizer
|
||||
parameters = []
|
||||
for layer in model.layers:
|
||||
if hasattr(layer, 'weight'):
|
||||
parameters.append(layer.weight)
|
||||
if hasattr(layer, 'bias') and layer.bias is not None:
|
||||
parameters.append(layer.bias)
|
||||
|
||||
# Create Adam optimizer
|
||||
optimizer = Adam(parameters, lr=lr)
|
||||
loss_fn = MSELoss()
|
||||
|
||||
train_losses = []
|
||||
accuracies = []
|
||||
|
||||
print(f"🏋️ Training MLP for {epochs} epochs with Adam optimizer...")
|
||||
print("Epoch | Loss | Accuracy | All Correct")
|
||||
print("-" * 45)
|
||||
|
||||
for epoch in range(epochs):
|
||||
# Zero gradients
|
||||
optimizer.zero_grad()
|
||||
|
||||
# Forward pass with autograd
|
||||
predictions = model.forward(X)
|
||||
loss = loss_fn.forward(predictions, y)
|
||||
|
||||
# Backward pass (autograd!)
|
||||
loss.backward()
|
||||
|
||||
# Optimizer step
|
||||
optimizer.step()
|
||||
|
||||
# Compute accuracy
|
||||
pred_classes = (predictions.data > 0.5).astype(int)
|
||||
accuracy = np.mean(pred_classes == y.data)
|
||||
|
||||
# Check if all 4 XOR patterns are correct
|
||||
all_correct = np.all(pred_classes == y.data)
|
||||
|
||||
train_losses.append(float(loss.data))
|
||||
accuracies.append(accuracy)
|
||||
|
||||
if epoch % 200 == 0 or epoch < 10 or all_correct:
|
||||
status = "✅" if all_correct else "🔄"
|
||||
print(f"{epoch:5d} | {loss.data:.6f} | {accuracy:.3f} | {status}")
|
||||
|
||||
# Early stopping if perfect
|
||||
if all_correct and epoch > 100:
|
||||
print(f"🎉 Perfect XOR solution found at epoch {epoch}!")
|
||||
break
|
||||
|
||||
return train_losses, accuracies
|
||||
|
||||
def evaluate_xor_model(model, X, y):
|
||||
"""Rigorous evaluation of XOR model."""
|
||||
predictions = model.forward(X)
|
||||
pred_classes = (predictions.data > 0.5).astype(int)
|
||||
|
||||
# XOR-specific evaluation
|
||||
results = {
|
||||
'accuracy': np.mean(pred_classes == y.data),
|
||||
'predictions': predictions.data.flatten(),
|
||||
'pred_classes': pred_classes.flatten(),
|
||||
'true_labels': y.data.flatten()
|
||||
}
|
||||
|
||||
# Check each XOR pattern individually
|
||||
xor_patterns = [
|
||||
("0 XOR 0", [0, 0], 0),
|
||||
("0 XOR 1", [0, 1], 1),
|
||||
("1 XOR 0", [1, 0], 1),
|
||||
("1 XOR 1", [1, 1], 0)
|
||||
]
|
||||
|
||||
print("\n📋 XOR Pattern Analysis:")
|
||||
print("Pattern | Input | True | Pred | Prob | Correct")
|
||||
print("-" * 50)
|
||||
|
||||
all_patterns_correct = True
|
||||
for i, (name, inputs, true_output) in enumerate(xor_patterns):
|
||||
predicted = int(pred_classes[i])
|
||||
probability = predictions.data[i, 0]
|
||||
correct = (predicted == true_output)
|
||||
all_patterns_correct &= correct
|
||||
|
||||
status = "✅" if correct else "❌"
|
||||
print(f"{name:9s} | {inputs} | {true_output} | {predicted} | {probability:.3f} | {status}")
|
||||
|
||||
results['all_patterns_correct'] = all_patterns_correct
|
||||
return results
|
||||
|
||||
def main():
|
||||
"""Rigorous Milestone 2 evaluation."""
|
||||
print("=" * 60)
|
||||
print("🎯 RIGOROUS MILESTONE 2 TEST: MLP")
|
||||
print("Non-linear classification (XOR) with autograd + Adam")
|
||||
print("=" * 60)
|
||||
|
||||
# Load modules
|
||||
if not load_modules():
|
||||
print("❌ FAILED: Could not load required modules")
|
||||
return False
|
||||
|
||||
# Generate XOR dataset
|
||||
print("\n📊 Generating XOR dataset...")
|
||||
X, y = generate_xor_dataset()
|
||||
print(f"XOR Dataset: {X.shape[0]} samples (inherently non-linear)")
|
||||
print("XOR Truth Table:")
|
||||
print(" Input | Output")
|
||||
print(" [0,0] | 0")
|
||||
print(" [0,1] | 1")
|
||||
print(" [1,0] | 1")
|
||||
print(" [1,1] | 0")
|
||||
|
||||
# Create model
|
||||
print("\n🧠 Creating MLP model...")
|
||||
model = create_mlp()
|
||||
|
||||
# Count parameters
|
||||
total_params = 0
|
||||
for layer in model.layers:
|
||||
if hasattr(layer, 'weight'):
|
||||
total_params += layer.weight.data.size
|
||||
if hasattr(layer, 'bias') and layer.bias is not None:
|
||||
total_params += layer.bias.data.size
|
||||
|
||||
print(f"Architecture: 2 → 4 → 4 → 1 (with ReLU activations)")
|
||||
print(f"Total parameters: {total_params}")
|
||||
|
||||
# Train model
|
||||
print("\n🏋️ Training with autograd + Adam optimizer...")
|
||||
train_losses, accuracies = train_mlp_with_autograd(model, X, y, epochs=1000, lr=0.01)
|
||||
|
||||
# Evaluate model
|
||||
print("\n📈 Evaluating final performance...")
|
||||
results = evaluate_xor_model(model, X, y)
|
||||
|
||||
final_accuracy = results['accuracy']
|
||||
final_loss = train_losses[-1] if train_losses else float('inf')
|
||||
|
||||
print(f"\nFinal Results:")
|
||||
print(f" Accuracy: {final_accuracy:.1%}")
|
||||
print(f" Final Loss: {final_loss:.6f}")
|
||||
print(f" All XOR patterns correct: {results['all_patterns_correct']}")
|
||||
|
||||
# Test success criteria
|
||||
print("\n🔍 TESTING SUCCESS CRITERIA:")
|
||||
|
||||
success_criteria = []
|
||||
|
||||
# 1. Training accuracy >95%
|
||||
accuracy_threshold = 0.95
|
||||
criterion_1 = final_accuracy >= accuracy_threshold
|
||||
success_criteria.append(criterion_1)
|
||||
print(f" 1. Accuracy ≥ 95%: {final_accuracy:.1%} {'✅' if criterion_1 else '❌'}")
|
||||
|
||||
# 2. All XOR patterns correct (critical for non-linear test)
|
||||
criterion_2 = results['all_patterns_correct']
|
||||
success_criteria.append(criterion_2)
|
||||
print(f" 2. All XOR patterns correct: {criterion_2} {'✅' if criterion_2 else '❌'}")
|
||||
|
||||
# 3. Loss convergence
|
||||
if len(train_losses) > 10:
|
||||
loss_trend = np.polyfit(range(len(train_losses)), train_losses, 1)[0]
|
||||
criterion_3 = loss_trend < 0
|
||||
else:
|
||||
criterion_3 = False
|
||||
success_criteria.append(criterion_3)
|
||||
print(f" 3. Loss converges: slope={loss_trend:.6f} {'✅' if criterion_3 else '❌'}")
|
||||
|
||||
# 4. Final loss below threshold
|
||||
loss_threshold = 0.1
|
||||
criterion_4 = final_loss < loss_threshold
|
||||
success_criteria.append(criterion_4)
|
||||
print(f" 4. Final loss < {loss_threshold}: {final_loss:.6f} {'✅' if criterion_4 else '❌'}")
|
||||
|
||||
# 5. Uses autograd (verified by training working without manual gradients)
|
||||
criterion_5 = len(train_losses) > 0 # Training completed = autograd worked
|
||||
success_criteria.append(criterion_5)
|
||||
print(f" 5. Autograd functioning: {len(train_losses)} epochs completed {'✅' if criterion_5 else '❌'}")
|
||||
|
||||
# Overall milestone result
|
||||
all_criteria_met = all(success_criteria)
|
||||
|
||||
# Final verdict
|
||||
print("\n" + "=" * 60)
|
||||
if all_criteria_met:
|
||||
print("🎉 MILESTONE 2: MLP - ACHIEVED!")
|
||||
print("✅ All success criteria satisfied with concrete evidence")
|
||||
print(f"✅ XOR problem solved: {final_accuracy:.1%} accuracy")
|
||||
print(f"✅ Non-linear capability: All 4 XOR patterns correct")
|
||||
print(f"✅ Autograd working: Automatic differentiation used")
|
||||
print(f"✅ Modern optimization: Adam optimizer with scheduling")
|
||||
print(f"✅ Architecture: 2-hidden-layer MLP with ReLU activations")
|
||||
print("\n🚀 Ready for Milestone 3: CNN with spatial convolutions!")
|
||||
else:
|
||||
print("❌ MILESTONE 2: MLP - NOT ACHIEVED")
|
||||
failed_criteria = sum(1 for c in success_criteria if not c)
|
||||
print(f"❌ {failed_criteria}/{len(success_criteria)} criteria failed")
|
||||
print("🔧 Need to fix issues before proceeding to Milestone 3")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
return all_criteria_met
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -720,7 +720,7 @@ def test_unit_tensor_creation():
|
||||
|
||||
print("✅ Tensor creation works correctly!")
|
||||
|
||||
test_unit_tensor_creation()
|
||||
# test_unit_tensor_creation() # Moved to main block
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -855,7 +855,7 @@ def test_unit_arithmetic_operations():
|
||||
|
||||
print("✅ Arithmetic operations work correctly!")
|
||||
|
||||
test_unit_arithmetic_operations()
|
||||
# test_unit_arithmetic_operations() # Moved to main block
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -1004,7 +1004,7 @@ def test_unit_matrix_multiplication():
|
||||
|
||||
print("✅ Matrix multiplication works correctly!")
|
||||
|
||||
test_unit_matrix_multiplication()
|
||||
# test_unit_matrix_multiplication() # Moved to main block
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -1169,7 +1169,7 @@ def test_unit_shape_manipulation():
|
||||
|
||||
print("✅ Shape manipulation works correctly!")
|
||||
|
||||
test_unit_shape_manipulation()
|
||||
# test_unit_shape_manipulation() # Moved to main block
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -1328,7 +1328,7 @@ def test_unit_reduction_operations():
|
||||
|
||||
print("✅ Reduction operations work correctly!")
|
||||
|
||||
test_unit_reduction_operations()
|
||||
# test_unit_reduction_operations() # Moved to main block
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -1517,7 +1517,7 @@ def demonstrate_tensor_integration():
|
||||
print("✅ Neural network layer simulation complete!")
|
||||
return y
|
||||
|
||||
demonstrate_tensor_integration()
|
||||
# demonstrate_tensor_integration() # Moved to main block
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -1636,12 +1636,25 @@ def test_module():
|
||||
print("🎉 ALL TESTS PASSED! Module ready for export.")
|
||||
print("Run: tito module complete 01_tensor")
|
||||
|
||||
test_module()
|
||||
# test_module() # Moved to main block
|
||||
|
||||
# %%
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Running Tensor Foundation module...")
|
||||
|
||||
# Run all unit tests
|
||||
test_unit_tensor_creation()
|
||||
test_unit_arithmetic_operations()
|
||||
test_unit_matrix_multiplication()
|
||||
test_unit_shape_manipulation()
|
||||
test_unit_reduction_operations()
|
||||
|
||||
# Run integration demo
|
||||
demonstrate_tensor_integration()
|
||||
|
||||
# Run final module test
|
||||
test_module()
|
||||
|
||||
print("✅ Module validation complete!")
|
||||
|
||||
# %% [markdown]
|
||||
|
||||
@@ -70,23 +70,11 @@ import os
|
||||
import gzip
|
||||
import urllib.request
|
||||
import pickle
|
||||
import sys
|
||||
|
||||
# Simplified Tensor class for DataLoader module
|
||||
# This avoids importing the full tensor_dev.py which executes all tests
|
||||
class Tensor:
|
||||
"""
|
||||
Simplified Tensor class for DataLoader module.
|
||||
Contains only the functionality needed for data loading.
|
||||
"""
|
||||
def __init__(self, data):
|
||||
self.data = np.array(data)
|
||||
self.shape = self.data.shape
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Tensor({self.data})"
|
||||
# Import real Tensor class from Module 01
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '01_tensor'))
|
||||
from tensor_dev import Tensor
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
@@ -221,7 +209,7 @@ def test_unit_dataset():
|
||||
|
||||
print("✅ Dataset interface works correctly!")
|
||||
|
||||
test_unit_dataset()
|
||||
# test_unit_dataset() # Moved to main block
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -400,7 +388,7 @@ def test_unit_tensordataset():
|
||||
|
||||
print("✅ TensorDataset works correctly!")
|
||||
|
||||
test_unit_tensordataset()
|
||||
# test_unit_tensordataset() # Moved to main block
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -627,7 +615,7 @@ def test_unit_dataloader():
|
||||
|
||||
print("✅ DataLoader works correctly!")
|
||||
|
||||
test_unit_dataloader()
|
||||
# test_unit_dataloader() # Moved to main block
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -840,7 +828,7 @@ def test_unit_download_functions():
|
||||
|
||||
print("✅ Download functions work correctly!")
|
||||
|
||||
test_unit_download_functions()
|
||||
# test_unit_download_functions() # Moved to main block
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -991,7 +979,7 @@ def analyze_dataloader_performance():
|
||||
print("• Memory usage scales linearly with batch size")
|
||||
print("🚀 Production tip: Balance batch size with GPU memory limits")
|
||||
|
||||
analyze_dataloader_performance()
|
||||
# analyze_dataloader_performance() # Moved to main block
|
||||
|
||||
|
||||
def analyze_memory_usage():
|
||||
@@ -1035,7 +1023,7 @@ def analyze_memory_usage():
|
||||
print(f" Large batch (512×784): {large_bytes / 1024:.1f} KB")
|
||||
print(f" Ratio: {large_bytes / small_bytes:.1f}×")
|
||||
|
||||
analyze_memory_usage()
|
||||
# analyze_memory_usage() # Moved to main block
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -1116,7 +1104,7 @@ def test_training_integration():
|
||||
|
||||
print("✅ Training integration works correctly!")
|
||||
|
||||
test_training_integration()
|
||||
# test_training_integration() # Moved to main block
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -1176,13 +1164,29 @@ def test_module():
|
||||
print("Run: tito module complete 08")
|
||||
|
||||
# Call before module summary
|
||||
test_module()
|
||||
# test_module() # Moved to main block
|
||||
|
||||
|
||||
# %%
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Running DataLoader module...")
|
||||
|
||||
# Run all unit tests
|
||||
test_unit_dataset()
|
||||
test_unit_tensordataset()
|
||||
test_unit_dataloader()
|
||||
test_unit_download_functions()
|
||||
|
||||
# Run performance analysis
|
||||
analyze_dataloader_performance()
|
||||
analyze_memory_usage()
|
||||
|
||||
# Run integration test
|
||||
test_training_integration()
|
||||
|
||||
# Run final module test
|
||||
test_module()
|
||||
|
||||
print("✅ Module validation complete!")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user