mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-05-04 01:33:49 -05:00
- Create professional examples directory showcasing TinyTorch as real ML framework - Add examples: XOR, MNIST, CIFAR-10, text generation, autograd demo, optimizer comparison - Fix import paths in exported modules (training.py, dense.py) - Update training module with autograd integration for loss functions - Add progressive integration tests for all 16 modules - Document framework capabilities and usage patterns This commit establishes the examples gallery that demonstrates TinyTorch works like PyTorch/TensorFlow, validating the complete framework.
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" |