mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-05-01 02:31:09 -05:00
This commit implements the pedagogically optimal "inevitable discovery" module progression based on expert validation and educational design principles. ## Module Reordering Summary **Previous Order (Problems)**: - 05_losses → 06_autograd → 07_dataloader → 08_optimizers → 09_spatial → 10_training - Issues: Autograd before optimizers, DataLoader before training, scattered dependencies **New Order (Beautiful Progression)**: - 05_losses → 06_optimizers → 07_autograd → 08_training → 09_spatial → 10_dataloader - Benefits: Each module creates inevitable need for the next ## Pedagogical Flow Achieved **05_losses** → "Need systematic weight updates" → **06_optimizers** **06_optimizers** → "Need automatic gradients" → **07_autograd** **07_autograd** → "Need systematic training" → **08_training** **08_training** → "MLPs hit limits on images" → **09_spatial** **09_spatial** → "Training is too slow" → **10_dataloader** ## Technical Changes ### Module Directory Renaming - `06_autograd` → `07_autograd` - `07_dataloader` → `10_dataloader` - `08_optimizers` → `06_optimizers` - `10_training` → `08_training` - `09_spatial` → `09_spatial` (no change) ### System Integration Updates - **MODULE_TO_CHECKPOINT mapping**: Updated in tito/commands/export.py - **Test directories**: Renamed module_XX directories to match new numbers - **Documentation**: Updated all references in MD files and agent configurations - **CLI integration**: Updated next-steps suggestions for proper flow ### Agent Configuration Updates - **Quality Assurance**: Updated module audit status with new numbers - **Module Developer**: Updated work tracking with new sequence - **Documentation**: Updated MASTER_PLAN_OF_RECORD.md with beautiful progression ## Educational Benefits 1. **Inevitable Discovery**: Each module naturally leads to the next 2. **Cognitive Load**: Concepts introduced exactly when needed 3. **Motivation**: Students understand WHY each tool is necessary 4. **Synthesis**: Everything flows toward complete ML systems understanding 5. **Professional Alignment**: Matches real ML engineering workflows ## Quality Assurance - ✅ All CLI commands still function - ✅ Checkpoint system mappings updated - ✅ Documentation consistency maintained - ✅ Test directory structure aligned - ✅ Agent configurations synchronized **Impact**: This reordering transforms TinyTorch from a collection of modules into a coherent educational journey where each step naturally motivates the next, creating optimal conditions for deep learning systems understanding.
266 lines
11 KiB
Python
266 lines
11 KiB
Python
"""
|
|
Integration Tests: Tensor ↔ CNN Operations
|
|
|
|
Tests the integration between core Tensor data structures and CNN operations:
|
|
- Conv2D operations with real tensors
|
|
- Flatten operations with real tensors
|
|
- CNN data flow with proper tensor shapes
|
|
- Error handling with real tensor inputs
|
|
|
|
These tests verify that CNN operations work correctly with real TinyTorch tensors,
|
|
not mocks or synthetic data.
|
|
"""
|
|
|
|
import pytest
|
|
import numpy as np
|
|
from tinytorch.core.tensor import Tensor
|
|
from tinytorch.core.cnn import Conv2D, conv2d_naive, flatten
|
|
|
|
|
|
class TestTensorCNNIntegration:
|
|
"""Test integration between Tensor and CNN components."""
|
|
|
|
def test_conv2d_naive_with_real_tensors(self):
|
|
"""Test conv2d_naive function with real tensor data."""
|
|
# Create real tensor data
|
|
input_data = np.array([[1.0, 2.0, 3.0],
|
|
[4.0, 5.0, 6.0],
|
|
[7.0, 8.0, 9.0]], dtype=np.float32)
|
|
|
|
kernel_data = np.array([[1.0, 0.0],
|
|
[0.0, -1.0]], dtype=np.float32)
|
|
|
|
# Test with real numpy arrays (function takes arrays, not tensors)
|
|
result = conv2d_naive(input_data, kernel_data)
|
|
|
|
# Verify correct shape
|
|
assert result.shape == (2, 2), f"Expected shape (2, 2), got {result.shape}"
|
|
|
|
# Verify correct computation
|
|
expected = np.array([[-4.0, -4.0],
|
|
[-4.0, -4.0]], dtype=np.float32)
|
|
np.testing.assert_array_almost_equal(result, expected, decimal=5)
|
|
|
|
def test_conv2d_layer_with_real_tensors(self):
|
|
"""Test Conv2D layer with real tensor inputs."""
|
|
# Create real tensor input
|
|
input_tensor = Tensor([[1.0, 2.0, 3.0],
|
|
[4.0, 5.0, 6.0],
|
|
[7.0, 8.0, 9.0]])
|
|
|
|
# Create Conv2D layer
|
|
conv_layer = Conv2D(kernel_size=(2, 2))
|
|
|
|
# Test forward pass
|
|
output = conv_layer(input_tensor)
|
|
|
|
# Verify output is a tensor
|
|
assert isinstance(output, Tensor), "Conv2D output should be a Tensor"
|
|
|
|
# Verify correct shape
|
|
assert output.shape == (2, 2), f"Expected shape (2, 2), got {output.shape}"
|
|
|
|
# Verify data type consistency
|
|
assert output.dtype == np.float32, f"Expected float32, got {output.dtype}"
|
|
|
|
def test_flatten_with_real_tensors(self):
|
|
"""Test flatten function with real tensor inputs."""
|
|
# Create real 2D tensor
|
|
input_tensor = Tensor([[1.0, 2.0], [3.0, 4.0]])
|
|
|
|
# Test flatten
|
|
output = flatten(input_tensor)
|
|
|
|
# Verify output is a tensor
|
|
assert isinstance(output, Tensor), "Flatten output should be a Tensor"
|
|
|
|
# Verify correct shape (batch dimension added)
|
|
assert output.shape == (1, 4), f"Expected shape (1, 4), got {output.shape}"
|
|
|
|
# Verify correct data
|
|
expected_data = np.array([[1.0, 2.0, 3.0, 4.0]], dtype=np.float32)
|
|
np.testing.assert_array_almost_equal(output.data, expected_data, decimal=5)
|
|
|
|
def test_cnn_pipeline_with_real_tensors(self):
|
|
"""Test complete CNN pipeline with real tensor data flow."""
|
|
# Create real input tensor (small image)
|
|
input_tensor = Tensor([[1.0, 2.0, 3.0, 4.0],
|
|
[5.0, 6.0, 7.0, 8.0],
|
|
[9.0, 10.0, 11.0, 12.0],
|
|
[13.0, 14.0, 15.0, 16.0]])
|
|
|
|
# Create CNN components
|
|
conv_layer = Conv2D(kernel_size=(2, 2))
|
|
|
|
# Test complete pipeline
|
|
conv_output = conv_layer(input_tensor)
|
|
flattened_output = flatten(conv_output)
|
|
|
|
# Verify shapes through pipeline
|
|
assert conv_output.shape == (3, 3), f"Conv output shape should be (3, 3), got {conv_output.shape}"
|
|
assert flattened_output.shape == (1, 9), f"Flattened shape should be (1, 9), got {flattened_output.shape}"
|
|
|
|
# Verify all outputs are tensors
|
|
assert isinstance(conv_output, Tensor), "Conv output should be a Tensor"
|
|
assert isinstance(flattened_output, Tensor), "Flattened output should be a Tensor"
|
|
|
|
# Verify data types
|
|
assert conv_output.dtype == np.float32, "Conv output should be float32"
|
|
assert flattened_output.dtype == np.float32, "Flattened output should be float32"
|
|
|
|
|
|
class TestTensorCNNShapeHandling:
|
|
"""Test CNN operations with various tensor shapes."""
|
|
|
|
def test_conv2d_with_different_input_shapes(self):
|
|
"""Test Conv2D with different input tensor shapes."""
|
|
# Test with different input sizes
|
|
test_cases = [
|
|
(Tensor([[1.0, 2.0], [3.0, 4.0]]), (2, 2), (1, 1)), # Minimal size
|
|
(Tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]), (2, 2), (2, 2)), # Standard size
|
|
(Tensor(np.random.rand(5, 5).astype(np.float32)), (3, 3), (3, 3)), # Larger input
|
|
]
|
|
|
|
for input_tensor, kernel_size, expected_shape in test_cases:
|
|
conv_layer = Conv2D(kernel_size=kernel_size)
|
|
output = conv_layer(input_tensor)
|
|
|
|
assert output.shape == expected_shape, f"For input {input_tensor.shape} and kernel {kernel_size}, expected {expected_shape}, got {output.shape}"
|
|
assert isinstance(output, Tensor), "Output should be a Tensor"
|
|
|
|
def test_flatten_with_different_shapes(self):
|
|
"""Test flatten with different tensor shapes."""
|
|
test_cases = [
|
|
(Tensor([[1.0, 2.0]]), (1, 2)), # 1x2 → 1x2
|
|
(Tensor([[1.0, 2.0], [3.0, 4.0]]), (1, 4)), # 2x2 → 1x4
|
|
(Tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]), (1, 6)), # 2x3 → 1x6
|
|
]
|
|
|
|
for input_tensor, expected_shape in test_cases:
|
|
output = flatten(input_tensor)
|
|
|
|
assert output.shape == expected_shape, f"For input {input_tensor.shape}, expected {expected_shape}, got {output.shape}"
|
|
assert isinstance(output, Tensor), "Output should be a Tensor"
|
|
|
|
|
|
class TestTensorCNNDataTypes:
|
|
"""Test CNN operations with different tensor data types."""
|
|
|
|
def test_conv2d_preserves_data_types(self):
|
|
"""Test that Conv2D preserves appropriate data types."""
|
|
# Create input with specific dtype
|
|
input_data = np.array([[1.0, 2.0, 3.0],
|
|
[4.0, 5.0, 6.0],
|
|
[7.0, 8.0, 9.0]], dtype=np.float32)
|
|
input_tensor = Tensor(input_data)
|
|
|
|
conv_layer = Conv2D(kernel_size=(2, 2))
|
|
output = conv_layer(input_tensor)
|
|
|
|
# Verify data type consistency
|
|
assert output.dtype == np.float32, f"Expected float32, got {output.dtype}"
|
|
assert input_tensor.dtype == np.float32, f"Input should be float32, got {input_tensor.dtype}"
|
|
|
|
def test_flatten_preserves_data_types(self):
|
|
"""Test that flatten preserves tensor data types."""
|
|
# Test with float32
|
|
input_float = Tensor(np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32))
|
|
output_float = flatten(input_float)
|
|
assert output_float.dtype == np.float32, f"Expected float32, got {output_float.dtype}"
|
|
|
|
# Test with int32
|
|
input_int = Tensor(np.array([[1, 2], [3, 4]], dtype=np.int32))
|
|
output_int = flatten(input_int)
|
|
assert output_int.dtype == np.int32, f"Expected int32, got {output_int.dtype}"
|
|
|
|
|
|
class TestTensorCNNErrorHandling:
|
|
"""Test error handling in CNN operations with real tensors."""
|
|
|
|
def test_conv2d_with_minimal_valid_tensor(self):
|
|
"""Test Conv2D with minimal valid tensor input."""
|
|
# Test with minimal valid input (2x2 with 2x2 kernel)
|
|
minimal_input = Tensor([[1.0, 2.0], [3.0, 4.0]])
|
|
conv_layer = Conv2D(kernel_size=(2, 2))
|
|
|
|
# Should produce 1x1 output
|
|
output = conv_layer(minimal_input)
|
|
assert output.shape == (1, 1), f"Expected (1, 1), got {output.shape}"
|
|
assert isinstance(output, Tensor), "Output should be a Tensor"
|
|
|
|
def test_conv2d_naive_with_edge_case_shapes(self):
|
|
"""Test conv2d_naive with edge case but valid shapes."""
|
|
# Test with minimal valid case
|
|
input_data = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float32)
|
|
kernel_data = np.array([[1.0, 0.0], [0.0, -1.0]], dtype=np.float32)
|
|
|
|
# Should produce 1x1 output
|
|
result = conv2d_naive(input_data, kernel_data)
|
|
assert result.shape == (1, 1), f"Expected (1, 1), got {result.shape}"
|
|
assert isinstance(result, np.ndarray), "Result should be numpy array"
|
|
|
|
|
|
class TestTensorCNNRealisticScenarios:
|
|
"""Test CNN operations with realistic tensor scenarios."""
|
|
|
|
def test_image_processing_pipeline(self):
|
|
"""Test CNN operations with image-like tensor data."""
|
|
# Create realistic image-like data (8x8 "image")
|
|
image_data = np.random.rand(8, 8).astype(np.float32)
|
|
image_tensor = Tensor(image_data)
|
|
|
|
# Apply 3x3 convolution (common in CNNs)
|
|
conv_layer = Conv2D(kernel_size=(3, 3))
|
|
features = conv_layer(image_tensor)
|
|
|
|
# Flatten for fully connected layer
|
|
flattened = flatten(features)
|
|
|
|
# Verify realistic shapes
|
|
assert features.shape == (6, 6), f"Expected (6, 6) feature map, got {features.shape}"
|
|
assert flattened.shape == (1, 36), f"Expected (1, 36) flattened, got {flattened.shape}"
|
|
|
|
# Verify realistic data ranges
|
|
assert np.all(np.isfinite(features.data)), "Features should be finite"
|
|
assert np.all(np.isfinite(flattened.data)), "Flattened data should be finite"
|
|
|
|
def test_multiple_convolutions(self):
|
|
"""Test multiple convolution operations in sequence."""
|
|
# Start with larger input
|
|
input_tensor = Tensor(np.random.rand(6, 6).astype(np.float32))
|
|
|
|
# Apply first convolution
|
|
conv1 = Conv2D(kernel_size=(3, 3))
|
|
features1 = conv1(input_tensor)
|
|
|
|
# Apply second convolution
|
|
conv2 = Conv2D(kernel_size=(2, 2))
|
|
features2 = conv2(features1)
|
|
|
|
# Verify shape progression
|
|
assert features1.shape == (4, 4), f"First conv should produce (4, 4), got {features1.shape}"
|
|
assert features2.shape == (3, 3), f"Second conv should produce (3, 3), got {features2.shape}"
|
|
|
|
# Verify all are tensors
|
|
assert isinstance(features1, Tensor), "First features should be Tensor"
|
|
assert isinstance(features2, Tensor), "Second features should be Tensor"
|
|
|
|
def test_conv_to_dense_integration_preparation(self):
|
|
"""Test CNN output preparation for dense layer integration."""
|
|
# Create input that will work with dense layers
|
|
input_tensor = Tensor(np.random.rand(5, 5).astype(np.float32))
|
|
|
|
# Apply convolution
|
|
conv_layer = Conv2D(kernel_size=(2, 2))
|
|
conv_output = conv_layer(input_tensor)
|
|
|
|
# Flatten for dense layer
|
|
flattened = flatten(conv_output)
|
|
|
|
# Verify shape is suitable for dense layer (batch_size, features)
|
|
assert len(flattened.shape) == 2, f"Flattened should be 2D, got {flattened.shape}"
|
|
assert flattened.shape[0] == 1, f"Batch size should be 1, got {flattened.shape[0]}"
|
|
|
|
# Verify data is ready for dense layer consumption
|
|
assert flattened.dtype == np.float32, "Data should be float32 for dense layers"
|
|
assert np.all(np.isfinite(flattened.data)), "Data should be finite for dense layers" |