mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-05-02 13:28:45 -05:00
MAJOR: Implement beautiful module progression through strategic reordering
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.
This commit is contained in:
266
tests/module_07/test_tensor_cnn_integration.py
Normal file
266
tests/module_07/test_tensor_cnn_integration.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user