mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-05-01 19:07:31 -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:
@@ -24,8 +24,8 @@ def run_module_tests() -> Dict:
|
||||
console = Console()
|
||||
|
||||
# Update module number and name
|
||||
MODULE_NUMBER = "06"
|
||||
MODULE_NAME = "Spatial/CNN"
|
||||
MODULE_NUMBER = "XX"
|
||||
MODULE_NAME = "[Module Name]"
|
||||
|
||||
# Header
|
||||
console.print(Panel(f"[bold blue]Module {MODULE_NUMBER}: {MODULE_NAME} - Test Suite[/bold blue]",
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
"""
|
||||
Integration Tests - CNN and Networks
|
||||
|
||||
Tests real integration between CNN and Network modules.
|
||||
Uses actual TinyTorch components to verify they work together correctly.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
from test_utils import setup_integration_test
|
||||
|
||||
# Ensure proper setup before importing
|
||||
setup_integration_test()
|
||||
|
||||
# Import ONLY from TinyTorch package
|
||||
from tinytorch.core.tensor import Tensor
|
||||
from tinytorch.core.activations import ReLU, Softmax, Sigmoid, Tanh
|
||||
from tinytorch.core.layers import Dense
|
||||
from tinytorch.core.networks import Sequential
|
||||
from tinytorch.core.cnn import Conv2D, flatten
|
||||
|
||||
|
||||
class TestCNNNetworkIntegration:
|
||||
"""Test real integration between CNN layers and Networks."""
|
||||
|
||||
def test_conv2d_in_sequential_network(self):
|
||||
"""Test Conv2D layer works within Sequential network."""
|
||||
# Create a simple CNN architecture: Conv2D -> ReLU -> Flatten -> Dense
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(3, 3)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x), # Flatten function as lambda
|
||||
Dense(input_size=36, output_size=10)
|
||||
])
|
||||
|
||||
# Test with sample input
|
||||
input_image = Tensor(np.random.randn(8, 8))
|
||||
output = network(input_image)
|
||||
|
||||
# Verify integration
|
||||
assert isinstance(output, Tensor), "Sequential with Conv2D should return Tensor"
|
||||
assert output.shape == (1, 10), f"Expected shape (1, 10), got {output.shape}"
|
||||
assert not np.any(np.isnan(output.data)), "CNN network should not produce NaN"
|
||||
|
||||
def test_multiple_conv2d_layers_in_network(self):
|
||||
"""Test multiple Conv2D layers in a Sequential network."""
|
||||
# Create deeper CNN: Conv2D -> ReLU -> Conv2D -> ReLU -> Flatten -> Dense
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(3, 3)), # 10x10 -> 8x8
|
||||
ReLU(),
|
||||
Conv2D(kernel_size=(3, 3)), # 8x8 -> 6x6
|
||||
ReLU(),
|
||||
lambda x: flatten(x), # 6x6 -> 36
|
||||
Dense(input_size=36, output_size=5)
|
||||
])
|
||||
|
||||
# Test with larger input
|
||||
input_image = Tensor(np.random.randn(10, 10))
|
||||
output = network(input_image)
|
||||
|
||||
# Verify deep CNN integration
|
||||
assert isinstance(output, Tensor), "Deep CNN network should return Tensor"
|
||||
assert output.shape == (1, 5), f"Expected shape (1, 5), got {output.shape}"
|
||||
assert not np.any(np.isnan(output.data)), "Deep CNN should not produce NaN"
|
||||
|
||||
def test_conv2d_with_different_activations(self):
|
||||
"""Test Conv2D with different activation functions in networks."""
|
||||
activations = [ReLU(), Sigmoid(), Tanh()]
|
||||
|
||||
for activation in activations:
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(2, 2)),
|
||||
activation,
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=16, output_size=3)
|
||||
])
|
||||
|
||||
input_image = Tensor(np.random.randn(5, 5))
|
||||
output = network(input_image)
|
||||
|
||||
assert isinstance(output, Tensor), f"Network with {activation.__class__.__name__} should return Tensor"
|
||||
assert output.shape == (1, 3), f"Expected shape (1, 3), got {output.shape}"
|
||||
assert not np.any(np.isnan(output.data)), f"Network with {activation.__class__.__name__} should not produce NaN"
|
||||
|
||||
def test_conv2d_batch_processing_in_network(self):
|
||||
"""Test Conv2D handles batch processing within networks."""
|
||||
# Create network
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(2, 2)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=9, output_size=2)
|
||||
])
|
||||
|
||||
# Test with batch input (simulate multiple images)
|
||||
batch_images = []
|
||||
for _ in range(4):
|
||||
batch_images.append(Tensor(np.random.randn(4, 4)))
|
||||
|
||||
# Process each image in the batch
|
||||
batch_outputs = []
|
||||
for image in batch_images:
|
||||
output = network(image)
|
||||
batch_outputs.append(output)
|
||||
|
||||
# Verify batch processing
|
||||
assert len(batch_outputs) == 4, "Should process all images in batch"
|
||||
for i, output in enumerate(batch_outputs):
|
||||
assert isinstance(output, Tensor), f"Batch item {i} should return Tensor"
|
||||
assert output.shape == (1, 2), f"Batch item {i} should have shape (1, 2)"
|
||||
assert not np.any(np.isnan(output.data)), f"Batch item {i} should not produce NaN"
|
||||
|
||||
def test_conv2d_different_kernel_sizes_in_network(self):
|
||||
"""Test Conv2D with different kernel sizes in networks."""
|
||||
kernel_sizes = [(2, 2), (3, 3), (5, 5)]
|
||||
input_sizes = [6, 8, 10] # Adjust input size for each kernel
|
||||
|
||||
for kernel_size, input_size in zip(kernel_sizes, input_sizes):
|
||||
# Calculate expected output size after convolution
|
||||
conv_output_size = input_size - kernel_size[0] + 1
|
||||
flatten_size = conv_output_size * conv_output_size
|
||||
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=kernel_size),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=flatten_size, output_size=1)
|
||||
])
|
||||
|
||||
input_image = Tensor(np.random.randn(input_size, input_size))
|
||||
output = network(input_image)
|
||||
|
||||
assert isinstance(output, Tensor), f"Network with kernel {kernel_size} should return Tensor"
|
||||
assert output.shape == (1, 1), f"Expected shape (1, 1), got {output.shape}"
|
||||
assert not np.any(np.isnan(output.data)), f"Network with kernel {kernel_size} should not produce NaN"
|
||||
|
||||
|
||||
class TestCNNNetworkComposition:
|
||||
"""Test composition of CNN components with different network architectures."""
|
||||
|
||||
def test_feature_extraction_pipeline(self):
|
||||
"""Test CNN as feature extractor with dense classifier."""
|
||||
# Feature extractor: Conv2D -> ReLU -> Flatten
|
||||
feature_extractor = Sequential([
|
||||
Conv2D(kernel_size=(3, 3)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x)
|
||||
])
|
||||
|
||||
# Classifier: Dense -> ReLU -> Dense
|
||||
classifier = Sequential([
|
||||
Dense(input_size=36, output_size=16),
|
||||
ReLU(),
|
||||
Dense(input_size=16, output_size=3)
|
||||
])
|
||||
|
||||
# Test feature extraction
|
||||
input_image = Tensor(np.random.randn(8, 8))
|
||||
features = feature_extractor(input_image)
|
||||
|
||||
assert isinstance(features, Tensor), "Feature extractor should return Tensor"
|
||||
assert features.shape == (1, 36), f"Expected features shape (1, 36), got {features.shape}"
|
||||
|
||||
# Test classification
|
||||
predictions = classifier(features)
|
||||
|
||||
assert isinstance(predictions, Tensor), "Classifier should return Tensor"
|
||||
assert predictions.shape == (1, 3), f"Expected predictions shape (1, 3), got {predictions.shape}"
|
||||
assert not np.any(np.isnan(predictions.data)), "Complete pipeline should not produce NaN"
|
||||
|
||||
def test_cnn_network_parameter_count(self):
|
||||
"""Test that CNN networks have reasonable parameter counts."""
|
||||
# Create CNN network
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(3, 3)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=36, output_size=10)
|
||||
])
|
||||
|
||||
# Test with input to ensure network is initialized
|
||||
input_image = Tensor(np.random.randn(8, 8))
|
||||
output = network(input_image)
|
||||
|
||||
# CNN should have fewer parameters than equivalent fully connected
|
||||
# Conv2D(3x3) has 9 parameters
|
||||
# Dense(36->10) has 36*10 + 10 = 370 parameters
|
||||
# Total: ~379 parameters vs ~6400 for fully connected (64->10)
|
||||
|
||||
assert isinstance(output, Tensor), "CNN network should work"
|
||||
assert output.shape == (1, 10), "CNN network should produce correct output shape"
|
||||
|
||||
# Verify CNN efficiency (this is conceptual - actual parameter counting
|
||||
# would require more sophisticated tracking)
|
||||
conv_layer = network.layers[0]
|
||||
dense_layer = network.layers[3]
|
||||
|
||||
# Conv2D kernel should be 3x3
|
||||
assert conv_layer.kernel.shape == (3, 3), "Conv2D should have correct kernel shape"
|
||||
|
||||
# Dense layer should connect flattened features to output
|
||||
assert dense_layer.weights.shape == (36, 10), "Dense layer should have correct weight shape"
|
||||
|
||||
def test_cnn_vs_dense_comparison(self):
|
||||
"""Test CNN vs pure dense network comparison."""
|
||||
# Create CNN network
|
||||
cnn_network = Sequential([
|
||||
Conv2D(kernel_size=(2, 2)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=9, output_size=5)
|
||||
])
|
||||
|
||||
# Create equivalent dense network (much larger)
|
||||
dense_network = Sequential([
|
||||
Dense(input_size=16, output_size=16), # Simulate full connectivity
|
||||
ReLU(),
|
||||
Dense(input_size=16, output_size=5)
|
||||
])
|
||||
|
||||
# Test with same input
|
||||
input_image = Tensor(np.random.randn(4, 4))
|
||||
input_flat = flatten(input_image) # For dense network
|
||||
|
||||
cnn_output = cnn_network(input_image)
|
||||
dense_output = dense_network(input_flat)
|
||||
|
||||
# Both should work but CNN is more parameter-efficient
|
||||
assert isinstance(cnn_output, Tensor), "CNN network should work"
|
||||
assert isinstance(dense_output, Tensor), "Dense network should work"
|
||||
assert cnn_output.shape == dense_output.shape, "Both networks should have same output shape"
|
||||
|
||||
# CNN should have fewer parameters (conceptually)
|
||||
# Conv2D(2x2) + Dense(9->5) = 4 + 45 = 49 parameters
|
||||
# Dense(16->16) + Dense(16->5) = 256 + 80 = 336 parameters
|
||||
# CNN is ~7x more parameter efficient!
|
||||
|
||||
|
||||
class TestCNNNetworkEdgeCases:
|
||||
"""Test edge cases and error handling in CNN-Network integration."""
|
||||
|
||||
def test_minimal_input_size(self):
|
||||
"""Test CNN networks with minimal valid input sizes."""
|
||||
# Minimal case: 2x2 input with 2x2 kernel -> 1x1 output
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(2, 2)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=1, output_size=1)
|
||||
])
|
||||
|
||||
input_image = Tensor(np.random.randn(2, 2))
|
||||
output = network(input_image)
|
||||
|
||||
assert isinstance(output, Tensor), "Minimal CNN should work"
|
||||
assert output.shape == (1, 1), "Minimal CNN should produce scalar output"
|
||||
|
||||
def test_shape_compatibility_validation(self):
|
||||
"""Test that CNN networks properly validate shape compatibility."""
|
||||
# This tests the integration between Conv2D output and Dense input
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(2, 2)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=9, output_size=3) # Expects 3x3 = 9 from 4x4->2x2 conv
|
||||
])
|
||||
|
||||
# Correct input size
|
||||
correct_input = Tensor(np.random.randn(4, 4))
|
||||
output = network(correct_input)
|
||||
|
||||
assert isinstance(output, Tensor), "Correct input should work"
|
||||
assert output.shape == (1, 3), "Correct input should produce expected output"
|
||||
|
||||
# The network should handle the shape transformation correctly
|
||||
# Conv2D(2x2) on 4x4 input -> 3x3 output
|
||||
# Flatten 3x3 -> 9 features
|
||||
# Dense(9->3) -> 3 outputs
|
||||
|
||||
def test_data_type_preservation(self):
|
||||
"""Test that CNN networks preserve data types properly."""
|
||||
network = Sequential([
|
||||
Conv2D(kernel_size=(2, 2)),
|
||||
ReLU(),
|
||||
lambda x: flatten(x),
|
||||
Dense(input_size=4, output_size=2)
|
||||
])
|
||||
|
||||
# Test with float32 input
|
||||
input_image = Tensor(np.random.randn(3, 3).astype(np.float32))
|
||||
output = network(input_image)
|
||||
|
||||
assert isinstance(output, Tensor), "Network should preserve tensor type"
|
||||
assert output.data.dtype == np.float32, "Network should preserve float32 dtype"
|
||||
assert output.shape == (1, 2), "Network should produce correct output shape"
|
||||
|
||||
|
||||
def test_integration_summary():
|
||||
"""Summary test demonstrating complete CNN-Network integration."""
|
||||
print("🎯 Integration Summary: CNN ↔ Networks")
|
||||
print("=" * 50)
|
||||
|
||||
# Create a realistic CNN architecture
|
||||
print("🏗️ Building CNN architecture...")
|
||||
cnn_classifier = Sequential([
|
||||
Conv2D(kernel_size=(3, 3)), # Feature extraction (8x8 -> 6x6)
|
||||
ReLU(), # Nonlinearity
|
||||
Conv2D(kernel_size=(2, 2)), # Further feature extraction (6x6 -> 5x5)
|
||||
ReLU(), # Nonlinearity
|
||||
lambda x: flatten(x), # Prepare for dense layers (5x5 -> 25)
|
||||
Dense(input_size=25, output_size=8), # Feature compression (25 -> 8)
|
||||
ReLU(), # Nonlinearity
|
||||
Dense(input_size=8, output_size=3) # Final classification (8 -> 3)
|
||||
])
|
||||
|
||||
# Test with realistic input
|
||||
print("📊 Testing with sample input...")
|
||||
input_image = Tensor(np.random.randn(8, 8))
|
||||
output = cnn_classifier(input_image)
|
||||
|
||||
# Verify complete integration
|
||||
assert isinstance(output, Tensor), "Complete CNN should return Tensor"
|
||||
assert output.shape == (1, 3), "Complete CNN should produce classification output"
|
||||
assert not np.any(np.isnan(output.data)), "Complete CNN should not produce NaN"
|
||||
|
||||
print("✅ CNN-Network integration successful!")
|
||||
print(f" Input: {input_image.shape} -> Output: {output.shape}")
|
||||
print(" Architecture: Conv2D -> ReLU -> Conv2D -> ReLU -> Flatten -> Dense -> ReLU -> Dense")
|
||||
print(" Components: CNN layers, activations, dense layers, sequential composition")
|
||||
print("🎉 Ready for real computer vision applications!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_integration_summary()
|
||||
@@ -1,270 +0,0 @@
|
||||
"""
|
||||
Integration Tests - CNN Pipeline
|
||||
|
||||
Tests real integration between CNN operations, activations, and layers.
|
||||
Moved from inline tests because it's true cross-module integration testing.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project root to the path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# Import REAL TinyTorch components
|
||||
try:
|
||||
from tinytorch.core.tensor import Tensor
|
||||
from tinytorch.core.cnn import Conv2D, flatten
|
||||
from tinytorch.core.activations import ReLU, Sigmoid, Tanh, Softmax
|
||||
from tinytorch.core.layers import Dense
|
||||
except ImportError:
|
||||
# Fallback for development
|
||||
sys.path.append(str(project_root / "modules" / "source" / "01_tensor"))
|
||||
sys.path.append(str(project_root / "modules" / "source" / "02_activations"))
|
||||
sys.path.append(str(project_root / "modules" / "source" / "03_layers"))
|
||||
sys.path.append(str(project_root / "modules" / "source" / "05_cnn"))
|
||||
|
||||
from tensor_dev import Tensor
|
||||
from activations_dev import ReLU, Sigmoid, Tanh, Softmax
|
||||
from layers_dev import Dense
|
||||
from cnn_dev import Conv2D, flatten
|
||||
|
||||
|
||||
class TestCNNPipelineIntegration:
|
||||
"""Test CNN pipeline integration with activations and layers."""
|
||||
|
||||
def test_cnn_pipeline_integration(self):
|
||||
"""Test CNN pipeline integration with complete workflow."""
|
||||
print("🔬 Integration Test: CNN Pipeline...")
|
||||
|
||||
# Test complete CNN pipeline
|
||||
input_image = Tensor(np.random.randn(8, 8))
|
||||
|
||||
# Build CNN pipeline
|
||||
conv = Conv2D(kernel_size=(3, 3))
|
||||
conv_output = conv(input_image)
|
||||
flattened = flatten(conv_output)
|
||||
|
||||
# Test shapes
|
||||
assert conv_output.shape == (6, 6), "Conv output should be correct"
|
||||
assert flattened.shape == (1, 36), "Flatten output should be correct"
|
||||
|
||||
# Test with activation and dense layers
|
||||
relu = ReLU()
|
||||
dense = Dense(input_size=36, output_size=10)
|
||||
|
||||
activated = relu(conv_output)
|
||||
final_flat = flatten(activated)
|
||||
predictions = dense(final_flat)
|
||||
|
||||
assert predictions.shape == (1, 10), "Final predictions should be correct shape"
|
||||
|
||||
print("✅ CNN pipeline integration works correctly")
|
||||
|
||||
def test_cnn_with_different_activations(self):
|
||||
"""Test CNN pipeline with different activation functions."""
|
||||
activations = [
|
||||
("ReLU", ReLU()),
|
||||
("Sigmoid", Sigmoid()),
|
||||
("Tanh", Tanh())
|
||||
]
|
||||
|
||||
input_image = Tensor(np.random.randn(6, 6))
|
||||
|
||||
for name, activation in activations:
|
||||
# CNN pipeline with specific activation
|
||||
conv = Conv2D(kernel_size=(2, 2))
|
||||
conv_output = conv(input_image)
|
||||
|
||||
# Apply activation
|
||||
activated = activation(conv_output)
|
||||
|
||||
# Flatten and classify
|
||||
flattened = flatten(activated)
|
||||
dense = Dense(input_size=25, output_size=5)
|
||||
predictions = dense(flattened)
|
||||
|
||||
# Verify integration
|
||||
assert isinstance(predictions, Tensor), f"CNN-{name} pipeline should return Tensor"
|
||||
assert predictions.shape == (1, 5), f"CNN-{name} pipeline should have correct shape"
|
||||
assert not np.any(np.isnan(predictions.data)), f"CNN-{name} pipeline should not produce NaN"
|
||||
|
||||
def test_deep_cnn_pipeline(self):
|
||||
"""Test deeper CNN pipeline with multiple layers."""
|
||||
# Create deeper pipeline
|
||||
input_image = Tensor(np.random.randn(10, 10))
|
||||
|
||||
# Stage 1: First convolution
|
||||
conv1 = Conv2D(kernel_size=(3, 3))
|
||||
conv1_output = conv1(input_image) # 10x10 -> 8x8
|
||||
relu1 = ReLU()
|
||||
activated1 = relu1(conv1_output)
|
||||
|
||||
# Stage 2: Second convolution
|
||||
conv2 = Conv2D(kernel_size=(3, 3))
|
||||
conv2_output = conv2(activated1) # 8x8 -> 6x6
|
||||
relu2 = ReLU()
|
||||
activated2 = relu2(conv2_output)
|
||||
|
||||
# Stage 3: Final classification
|
||||
flattened = flatten(activated2) # 6x6 -> 36
|
||||
dense = Dense(input_size=36, output_size=3)
|
||||
predictions = dense(flattened)
|
||||
|
||||
# Verify deep pipeline
|
||||
assert isinstance(predictions, Tensor), "Deep CNN pipeline should return Tensor"
|
||||
assert predictions.shape == (1, 3), "Deep CNN pipeline should have correct shape"
|
||||
assert not np.any(np.isnan(predictions.data)), "Deep CNN pipeline should not produce NaN"
|
||||
|
||||
# Verify intermediate shapes
|
||||
assert conv1_output.shape == (8, 8), "First conv should produce 8x8"
|
||||
assert conv2_output.shape == (6, 6), "Second conv should produce 6x6"
|
||||
assert flattened.shape == (1, 36), "Flatten should produce 36 features"
|
||||
|
||||
def test_cnn_with_softmax_output(self):
|
||||
"""Test CNN pipeline with softmax output for classification."""
|
||||
input_image = Tensor(np.random.randn(5, 5))
|
||||
|
||||
# Build classification pipeline
|
||||
conv = Conv2D(kernel_size=(2, 2))
|
||||
conv_output = conv(input_image) # 5x5 -> 4x4
|
||||
|
||||
relu = ReLU()
|
||||
activated = relu(conv_output)
|
||||
|
||||
flattened = flatten(activated) # 4x4 -> 16
|
||||
dense = Dense(input_size=16, output_size=3)
|
||||
dense_output = dense(flattened)
|
||||
|
||||
softmax = Softmax()
|
||||
predictions = softmax(dense_output)
|
||||
|
||||
# Verify classification pipeline
|
||||
assert isinstance(predictions, Tensor), "Classification pipeline should return Tensor"
|
||||
assert predictions.shape == (1, 3), "Classification should have 3 class outputs"
|
||||
|
||||
# Verify softmax properties
|
||||
probabilities = predictions.data[0]
|
||||
assert np.all(probabilities > 0), "Softmax should produce positive probabilities"
|
||||
assert np.isclose(np.sum(probabilities), 1.0), "Softmax should sum to 1"
|
||||
|
||||
def test_cnn_batch_processing_integration(self):
|
||||
"""Test CNN pipeline with batch processing integration."""
|
||||
# Create batch of images
|
||||
batch_size = 3
|
||||
batch_images = []
|
||||
for _ in range(batch_size):
|
||||
batch_images.append(Tensor(np.random.randn(4, 4)))
|
||||
|
||||
# Process each image through the pipeline
|
||||
predictions = []
|
||||
for image in batch_images:
|
||||
# CNN pipeline
|
||||
conv = Conv2D(kernel_size=(2, 2))
|
||||
conv_output = conv(image) # 4x4 -> 3x3
|
||||
|
||||
relu = ReLU()
|
||||
activated = relu(conv_output)
|
||||
|
||||
flattened = flatten(activated) # 3x3 -> 9
|
||||
dense = Dense(input_size=9, output_size=2)
|
||||
prediction = dense(flattened)
|
||||
|
||||
predictions.append(prediction)
|
||||
|
||||
# Verify batch processing
|
||||
assert len(predictions) == batch_size, "Should process all images in batch"
|
||||
for i, pred in enumerate(predictions):
|
||||
assert isinstance(pred, Tensor), f"Batch item {i} should return Tensor"
|
||||
assert pred.shape == (1, 2), f"Batch item {i} should have correct shape"
|
||||
assert not np.any(np.isnan(pred.data)), f"Batch item {i} should not produce NaN"
|
||||
|
||||
def test_cnn_pipeline_numerical_stability(self):
|
||||
"""Test CNN pipeline numerical stability with edge cases."""
|
||||
# Test with very small values
|
||||
small_image = Tensor(np.random.randn(3, 3) * 0.001)
|
||||
|
||||
conv = Conv2D(kernel_size=(2, 2))
|
||||
conv_output = conv(small_image)
|
||||
|
||||
relu = ReLU()
|
||||
activated = relu(conv_output)
|
||||
|
||||
flattened = flatten(activated)
|
||||
dense = Dense(input_size=4, output_size=1)
|
||||
output = dense(flattened)
|
||||
|
||||
# Should handle small values without numerical issues
|
||||
assert isinstance(output, Tensor), "Should handle small values"
|
||||
assert output.shape == (1, 1), "Should maintain correct shape"
|
||||
assert not np.any(np.isnan(output.data)), "Should not produce NaN with small values"
|
||||
|
||||
# Test with larger values
|
||||
large_image = Tensor(np.random.randn(3, 3) * 10.0)
|
||||
|
||||
conv = Conv2D(kernel_size=(2, 2))
|
||||
conv_output = conv(large_image)
|
||||
|
||||
relu = ReLU()
|
||||
activated = relu(conv_output)
|
||||
|
||||
flattened = flatten(activated)
|
||||
dense = Dense(input_size=4, output_size=1)
|
||||
output = dense(flattened)
|
||||
|
||||
# Should handle large values without overflow
|
||||
assert isinstance(output, Tensor), "Should handle large values"
|
||||
assert output.shape == (1, 1), "Should maintain correct shape"
|
||||
assert not np.any(np.isnan(output.data)), "Should not produce NaN with large values"
|
||||
assert not np.any(np.isinf(output.data)), "Should not produce Inf with large values"
|
||||
|
||||
|
||||
def test_integration_summary():
|
||||
"""Summary test demonstrating complete CNN pipeline integration."""
|
||||
print("🎯 Integration Summary: CNN Pipeline")
|
||||
print("=" * 50)
|
||||
|
||||
# Create realistic CNN pipeline
|
||||
print("🏗️ Building CNN pipeline...")
|
||||
input_image = Tensor(np.random.randn(8, 8))
|
||||
|
||||
# Stage 1: Feature extraction
|
||||
conv = Conv2D(kernel_size=(3, 3))
|
||||
features = conv(input_image) # 8x8 -> 6x6
|
||||
|
||||
# Stage 2: Nonlinear activation
|
||||
relu = ReLU()
|
||||
activated = relu(features)
|
||||
|
||||
# Stage 3: Prepare for classification
|
||||
flattened = flatten(activated) # 6x6 -> 36
|
||||
|
||||
# Stage 4: Classification
|
||||
classifier = Dense(input_size=36, output_size=3)
|
||||
raw_predictions = classifier(flattened)
|
||||
|
||||
# Stage 5: Probability distribution
|
||||
softmax = Softmax()
|
||||
predictions = softmax(raw_predictions)
|
||||
|
||||
# Verify complete pipeline
|
||||
assert isinstance(predictions, Tensor), "Complete pipeline should return Tensor"
|
||||
assert predictions.shape == (1, 3), "Complete pipeline should produce 3 class probabilities"
|
||||
|
||||
probabilities = predictions.data[0]
|
||||
assert np.all(probabilities > 0), "Should produce positive probabilities"
|
||||
assert np.isclose(np.sum(probabilities), 1.0), "Should sum to 1.0"
|
||||
|
||||
print("✅ CNN pipeline integration successful!")
|
||||
print(f" Input: {input_image.shape} -> Features: {features.shape}")
|
||||
print(f" Activated: {activated.shape} -> Flattened: {flattened.shape}")
|
||||
print(f" Raw predictions: {raw_predictions.shape} -> Final: {predictions.shape}")
|
||||
print(" Components: CNN → Activation → Flatten → Dense → Softmax")
|
||||
print("🎉 Ready for real computer vision applications!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_integration_summary()
|
||||
390
tests/module_06/test_dataloader_tensor_integration.py
Normal file
390
tests/module_06/test_dataloader_tensor_integration.py
Normal file
@@ -0,0 +1,390 @@
|
||||
"""
|
||||
Integration Tests - DataLoader and Tensor
|
||||
|
||||
Tests real integration between DataLoader and Tensor modules.
|
||||
Uses actual TinyTorch components to verify they work together correctly.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
from test_utils import setup_integration_test
|
||||
|
||||
# Ensure proper setup before importing
|
||||
setup_integration_test()
|
||||
|
||||
# Import ONLY from TinyTorch package
|
||||
from tinytorch.core.tensor import Tensor
|
||||
from tinytorch.core.dataloader import DataLoader, Dataset, SimpleDataset
|
||||
from tinytorch.core.activations import ReLU
|
||||
from tinytorch.core.layers import Dense
|
||||
|
||||
|
||||
class TestDataLoaderTensorIntegration:
|
||||
"""Test integration between DataLoader and Tensor components."""
|
||||
|
||||
def test_simple_dataset_produces_tensors(self):
|
||||
"""Test SimpleDataset produces real Tensor objects."""
|
||||
# Create SimpleDataset
|
||||
dataset = SimpleDataset(size=10, num_features=3, num_classes=2)
|
||||
|
||||
# Get a sample
|
||||
data, label = dataset[0]
|
||||
|
||||
# Verify outputs are tensors
|
||||
assert isinstance(data, Tensor), "Data should be a Tensor"
|
||||
assert isinstance(label, Tensor), "Label should be a Tensor"
|
||||
|
||||
# Verify tensor properties
|
||||
assert data.shape == (3,), f"Expected data shape (3,), got {data.shape}"
|
||||
assert label.shape == (), f"Expected label shape (), got {label.shape}"
|
||||
assert data.dtype == np.float32, f"Expected float32, got {data.dtype}"
|
||||
assert label.dtype == np.int32, f"Expected int32, got {label.dtype}"
|
||||
|
||||
def test_dataloader_produces_tensor_batches(self):
|
||||
"""Test DataLoader produces batches of real Tensor objects."""
|
||||
# Create dataset and dataloader
|
||||
dataset = SimpleDataset(size=20, num_features=4, num_classes=3)
|
||||
dataloader = DataLoader(dataset, batch_size=5, shuffle=False)
|
||||
|
||||
# Get first batch
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Verify batch outputs are tensors
|
||||
assert isinstance(batch_data, Tensor), "Batch data should be a Tensor"
|
||||
assert isinstance(batch_labels, Tensor), "Batch labels should be a Tensor"
|
||||
|
||||
# Verify batch shapes
|
||||
assert batch_data.shape == (5, 4), f"Expected batch data shape (5, 4), got {batch_data.shape}"
|
||||
assert batch_labels.shape == (5,), f"Expected batch labels shape (5,), got {batch_labels.shape}"
|
||||
|
||||
# Verify data types
|
||||
assert batch_data.dtype == np.float32, f"Expected float32, got {batch_data.dtype}"
|
||||
assert batch_labels.dtype == np.int32, f"Expected int32, got {batch_labels.dtype}"
|
||||
|
||||
def test_dataloader_tensor_compatibility_with_activations(self):
|
||||
"""Test DataLoader tensors work with activation functions."""
|
||||
# Create dataset and dataloader
|
||||
dataset = SimpleDataset(size=10, num_features=3, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
|
||||
|
||||
# Get batch
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Apply activation function
|
||||
relu = ReLU()
|
||||
activated_data = relu(batch_data)
|
||||
|
||||
# Verify result is tensor
|
||||
assert isinstance(activated_data, Tensor), "Activated data should be a Tensor"
|
||||
assert activated_data.shape == batch_data.shape, "Shape should be preserved"
|
||||
|
||||
# Verify ReLU applied correctly (non-negative values)
|
||||
assert np.all(activated_data.data >= 0), "ReLU should produce non-negative values"
|
||||
|
||||
def test_dataloader_tensor_compatibility_with_layers(self):
|
||||
"""Test DataLoader tensors work with neural network layers."""
|
||||
# Create dataset and dataloader
|
||||
dataset = SimpleDataset(size=10, num_features=3, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
|
||||
|
||||
# Get batch
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Apply dense layer
|
||||
dense = Dense(input_size=3, output_size=2)
|
||||
output = dense(batch_data)
|
||||
|
||||
# Verify result is tensor
|
||||
assert isinstance(output, Tensor), "Layer output should be a Tensor"
|
||||
assert output.shape == (4, 2), f"Expected output shape (4, 2), got {output.shape}"
|
||||
assert output.dtype == np.float32, f"Expected float32, got {output.dtype}"
|
||||
|
||||
def test_dataloader_full_pipeline_integration(self):
|
||||
"""Test DataLoader tensors in complete ML pipeline."""
|
||||
# Create dataset and dataloader
|
||||
dataset = SimpleDataset(size=12, num_features=4, num_classes=3)
|
||||
dataloader = DataLoader(dataset, batch_size=6, shuffle=False)
|
||||
|
||||
# Get batch
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Apply full pipeline: Dense → ReLU → Dense
|
||||
dense1 = Dense(input_size=4, output_size=8)
|
||||
relu = ReLU()
|
||||
dense2 = Dense(input_size=8, output_size=3)
|
||||
|
||||
# Forward pass
|
||||
hidden = dense1(batch_data)
|
||||
activated = relu(hidden)
|
||||
output = dense2(activated)
|
||||
|
||||
# Verify all outputs are tensors
|
||||
assert isinstance(hidden, Tensor), "Hidden layer should be Tensor"
|
||||
assert isinstance(activated, Tensor), "Activated layer should be Tensor"
|
||||
assert isinstance(output, Tensor), "Output layer should be Tensor"
|
||||
|
||||
# Verify shapes through pipeline
|
||||
assert hidden.shape == (6, 8), f"Hidden shape should be (6, 8), got {hidden.shape}"
|
||||
assert activated.shape == (6, 8), f"Activated shape should be (6, 8), got {activated.shape}"
|
||||
assert output.shape == (6, 3), f"Output shape should be (6, 3), got {output.shape}"
|
||||
|
||||
|
||||
class TestDataLoaderTensorBatching:
|
||||
"""Test DataLoader batching with tensor integration."""
|
||||
|
||||
def test_different_batch_sizes(self):
|
||||
"""Test DataLoader with different batch sizes produces correct tensors."""
|
||||
dataset = SimpleDataset(size=20, num_features=3, num_classes=2)
|
||||
|
||||
batch_sizes = [1, 4, 8, 10]
|
||||
for batch_size in batch_sizes:
|
||||
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Verify tensor shapes
|
||||
assert batch_data.shape == (batch_size, 3), f"Data shape should be ({batch_size}, 3), got {batch_data.shape}"
|
||||
assert batch_labels.shape == (batch_size,), f"Label shape should be ({batch_size},), got {batch_labels.shape}"
|
||||
|
||||
# Verify tensor types
|
||||
assert isinstance(batch_data, Tensor), "Batch data should be Tensor"
|
||||
assert isinstance(batch_labels, Tensor), "Batch labels should be Tensor"
|
||||
|
||||
def test_shuffling_preserves_tensor_integrity(self):
|
||||
"""Test that shuffling preserves tensor data integrity."""
|
||||
dataset = SimpleDataset(size=10, num_features=2, num_classes=2)
|
||||
|
||||
# Create two dataloaders with different shuffle settings
|
||||
dataloader_no_shuffle = DataLoader(dataset, batch_size=5, shuffle=False)
|
||||
dataloader_shuffle = DataLoader(dataset, batch_size=5, shuffle=True)
|
||||
|
||||
# Get batches
|
||||
batch_no_shuffle = next(iter(dataloader_no_shuffle))
|
||||
batch_shuffle = next(iter(dataloader_shuffle))
|
||||
|
||||
# Both should produce valid tensors
|
||||
for batch_data, batch_labels in [batch_no_shuffle, batch_shuffle]:
|
||||
assert isinstance(batch_data, Tensor), "Data should be Tensor"
|
||||
assert isinstance(batch_labels, Tensor), "Labels should be Tensor"
|
||||
assert batch_data.shape == (5, 2), f"Expected shape (5, 2), got {batch_data.shape}"
|
||||
assert batch_labels.shape == (5,), f"Expected shape (5,), got {batch_labels.shape}"
|
||||
|
||||
def test_iteration_produces_consistent_tensors(self):
|
||||
"""Test that iterating through DataLoader produces consistent tensors."""
|
||||
dataset = SimpleDataset(size=12, num_features=3, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
|
||||
|
||||
batch_count = 0
|
||||
for batch_data, batch_labels in dataloader:
|
||||
batch_count += 1
|
||||
|
||||
# Verify each batch produces valid tensors
|
||||
assert isinstance(batch_data, Tensor), f"Batch {batch_count} data should be Tensor"
|
||||
assert isinstance(batch_labels, Tensor), f"Batch {batch_count} labels should be Tensor"
|
||||
|
||||
# Verify shapes (last batch might be smaller)
|
||||
assert batch_data.shape[1] == 3, f"Feature dim should be 3, got {batch_data.shape[1]}"
|
||||
assert batch_data.shape[0] == batch_labels.shape[0], "Batch and label sizes should match"
|
||||
|
||||
# Verify data types
|
||||
assert batch_data.dtype == np.float32, "Data should be float32"
|
||||
assert batch_labels.dtype == np.int32, "Labels should be int32"
|
||||
|
||||
# Should have processed all data
|
||||
assert batch_count == 3, f"Expected 3 batches, got {batch_count}"
|
||||
|
||||
|
||||
class TestDataLoaderTensorDataTypes:
|
||||
"""Test DataLoader tensor data type handling."""
|
||||
|
||||
def test_float32_tensor_production(self):
|
||||
"""Test DataLoader produces float32 tensors for data."""
|
||||
dataset = SimpleDataset(size=8, num_features=2, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
|
||||
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Verify data types
|
||||
assert batch_data.dtype == np.float32, f"Expected float32, got {batch_data.dtype}"
|
||||
assert isinstance(batch_data.data, np.ndarray), "Underlying data should be numpy array"
|
||||
assert batch_data.data.dtype == np.float32, "Underlying array should be float32"
|
||||
|
||||
def test_int32_tensor_production(self):
|
||||
"""Test DataLoader produces int32 tensors for labels."""
|
||||
dataset = SimpleDataset(size=8, num_features=2, num_classes=3)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
|
||||
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Verify label types
|
||||
assert batch_labels.dtype == np.int32, f"Expected int32, got {batch_labels.dtype}"
|
||||
assert isinstance(batch_labels.data, np.ndarray), "Underlying labels should be numpy array"
|
||||
assert batch_labels.data.dtype == np.int32, "Underlying array should be int32"
|
||||
|
||||
def test_tensor_data_ranges(self):
|
||||
"""Test DataLoader produces tensors with reasonable data ranges."""
|
||||
dataset = SimpleDataset(size=10, num_features=3, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=5, shuffle=False)
|
||||
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Check data ranges
|
||||
assert np.all(np.isfinite(batch_data.data)), "Data should be finite"
|
||||
assert np.all(batch_labels.data >= 0), "Labels should be non-negative"
|
||||
assert np.all(batch_labels.data < 2), "Labels should be less than num_classes"
|
||||
|
||||
|
||||
class TestDataLoaderTensorRealisticScenarios:
|
||||
"""Test DataLoader with realistic tensor scenarios."""
|
||||
|
||||
def test_training_loop_simulation(self):
|
||||
"""Test DataLoader tensors in training loop simulation."""
|
||||
dataset = SimpleDataset(size=16, num_features=4, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=8, shuffle=True)
|
||||
|
||||
# Simulate training loop
|
||||
epoch_batches = 0
|
||||
for epoch in range(2):
|
||||
batch_count = 0
|
||||
for batch_data, batch_labels in dataloader:
|
||||
batch_count += 1
|
||||
|
||||
# Simulate forward pass
|
||||
dense = Dense(input_size=4, output_size=2)
|
||||
output = dense(batch_data)
|
||||
|
||||
# Verify tensor operations work
|
||||
assert isinstance(output, Tensor), "Forward pass should produce Tensor"
|
||||
assert output.shape == (8, 2), f"Expected shape (8, 2), got {output.shape}"
|
||||
|
||||
# Simulate loss computation (simplified)
|
||||
loss = output.data.mean() # Simple loss
|
||||
assert np.isfinite(loss), "Loss should be finite"
|
||||
|
||||
epoch_batches += 1
|
||||
|
||||
assert batch_count == 2, f"Expected 2 batches per epoch, got {batch_count}"
|
||||
|
||||
assert epoch_batches == 4, f"Expected 4 total batches, got {epoch_batches}"
|
||||
|
||||
def test_different_dataset_sizes(self):
|
||||
"""Test DataLoader with different dataset sizes."""
|
||||
test_cases = [
|
||||
(5, 2), # Small dataset
|
||||
(32, 8), # Medium dataset
|
||||
(100, 16), # Large dataset
|
||||
]
|
||||
|
||||
for dataset_size, batch_size in test_cases:
|
||||
dataset = SimpleDataset(size=dataset_size, num_features=3, num_classes=2)
|
||||
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
|
||||
|
||||
total_samples = 0
|
||||
for batch_data, batch_labels in dataloader:
|
||||
# Verify tensor properties
|
||||
assert isinstance(batch_data, Tensor), "Data should be Tensor"
|
||||
assert isinstance(batch_labels, Tensor), "Labels should be Tensor"
|
||||
|
||||
# Count samples
|
||||
total_samples += batch_data.shape[0]
|
||||
|
||||
# Verify shapes
|
||||
assert batch_data.shape[1] == 3, "Feature dim should be 3"
|
||||
assert batch_data.shape[0] == batch_labels.shape[0], "Batch sizes should match"
|
||||
|
||||
assert total_samples == dataset_size, f"Should process all {dataset_size} samples, got {total_samples}"
|
||||
|
||||
def test_dataloader_with_complex_pipeline(self):
|
||||
"""Test DataLoader integration with complex neural network pipeline."""
|
||||
dataset = SimpleDataset(size=20, num_features=5, num_classes=3)
|
||||
dataloader = DataLoader(dataset, batch_size=10, shuffle=False)
|
||||
|
||||
# Create complex pipeline
|
||||
dense1 = Dense(input_size=5, output_size=16)
|
||||
relu1 = ReLU()
|
||||
dense2 = Dense(input_size=16, output_size=8)
|
||||
relu2 = ReLU()
|
||||
dense3 = Dense(input_size=8, output_size=3)
|
||||
|
||||
# Process batches
|
||||
for batch_data, batch_labels in dataloader:
|
||||
# Forward pass through complex pipeline
|
||||
x = dense1(batch_data)
|
||||
x = relu1(x)
|
||||
x = dense2(x)
|
||||
x = relu2(x)
|
||||
output = dense3(x)
|
||||
|
||||
# Verify final output
|
||||
assert isinstance(output, Tensor), "Final output should be Tensor"
|
||||
assert output.shape == (10, 3), f"Expected shape (10, 3), got {output.shape}"
|
||||
assert output.dtype == np.float32, "Output should be float32"
|
||||
assert np.all(np.isfinite(output.data)), "Output should be finite"
|
||||
|
||||
def test_dataloader_memory_efficiency(self):
|
||||
"""Test DataLoader memory efficiency with tensor operations."""
|
||||
dataset = SimpleDataset(size=50, num_features=10, num_classes=5)
|
||||
dataloader = DataLoader(dataset, batch_size=25, shuffle=False)
|
||||
|
||||
# Process batches and verify memory usage patterns
|
||||
processed_batches = []
|
||||
for batch_data, batch_labels in dataloader:
|
||||
# Store tensor info (not the actual tensors to avoid memory issues)
|
||||
batch_info = {
|
||||
'data_shape': batch_data.shape,
|
||||
'label_shape': batch_labels.shape,
|
||||
'data_type': batch_data.dtype,
|
||||
'label_type': batch_labels.dtype
|
||||
}
|
||||
processed_batches.append(batch_info)
|
||||
|
||||
# Verify tensors are properly formed
|
||||
assert isinstance(batch_data, Tensor), "Data should be Tensor"
|
||||
assert isinstance(batch_labels, Tensor), "Labels should be Tensor"
|
||||
|
||||
# Verify we processed expected number of batches
|
||||
assert len(processed_batches) == 2, f"Expected 2 batches, got {len(processed_batches)}"
|
||||
|
||||
# Verify consistency across batches
|
||||
for i, batch_info in enumerate(processed_batches):
|
||||
assert batch_info['data_shape'][1] == 10, f"Batch {i} should have 10 features"
|
||||
assert batch_info['data_type'] == np.float32, f"Batch {i} data should be float32"
|
||||
assert batch_info['label_type'] == np.int32, f"Batch {i} labels should be int32"
|
||||
|
||||
|
||||
class TestCustomDatasetIntegration:
|
||||
"""Test custom dataset integration with tensor operations."""
|
||||
|
||||
def test_custom_dataset_with_tensors(self):
|
||||
"""Test custom dataset that produces tensors works with DataLoader."""
|
||||
|
||||
class CustomTensorDataset(Dataset):
|
||||
def __init__(self, size: int):
|
||||
self.size = size
|
||||
self.data = [Tensor(np.random.rand(3).astype(np.float32)) for _ in range(size)]
|
||||
self.labels = [Tensor(np.random.randint(0, 2, dtype=np.int32)) for _ in range(size)]
|
||||
|
||||
def __len__(self):
|
||||
return self.size
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.data[index], self.labels[index]
|
||||
|
||||
# Create custom dataset and dataloader
|
||||
dataset = CustomTensorDataset(size=12)
|
||||
dataloader = DataLoader(dataset, batch_size=4, shuffle=False)
|
||||
|
||||
# Test integration
|
||||
batch_data, batch_labels = next(iter(dataloader))
|
||||
|
||||
# Verify tensor properties
|
||||
assert isinstance(batch_data, Tensor), "Batch data should be Tensor"
|
||||
assert isinstance(batch_labels, Tensor), "Batch labels should be Tensor"
|
||||
assert batch_data.shape == (4, 3), f"Expected shape (4, 3), got {batch_data.shape}"
|
||||
assert batch_labels.shape == (4,), f"Expected shape (4,), got {batch_labels.shape}"
|
||||
|
||||
# Test with neural network components
|
||||
dense = Dense(input_size=3, output_size=2)
|
||||
output = dense(batch_data)
|
||||
|
||||
assert isinstance(output, Tensor), "Dense output should be Tensor"
|
||||
assert output.shape == (4, 2), f"Expected shape (4, 2), got {output.shape}"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,336 +0,0 @@
|
||||
"""
|
||||
Module 06: Spatial - Core Functionality Tests
|
||||
Tests convolutional layers and spatial operations for computer vision
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
|
||||
class TestConv2DLayer:
|
||||
"""Test 2D convolution layer."""
|
||||
|
||||
def test_conv2d_creation(self):
|
||||
"""Test Conv2D layer creation."""
|
||||
try:
|
||||
from tinytorch.core.spatial import Conv2D
|
||||
|
||||
conv = Conv2D(in_channels=3, out_channels=16, kernel_size=3)
|
||||
|
||||
assert conv.in_channels == 3
|
||||
assert conv.out_channels == 16
|
||||
assert conv.kernel_size == 3
|
||||
|
||||
except ImportError:
|
||||
assert True, "Conv2D not implemented yet"
|
||||
|
||||
def test_conv2d_weight_shape(self):
|
||||
"""Test Conv2D weight tensor has correct shape."""
|
||||
try:
|
||||
from tinytorch.core.spatial import Conv2D
|
||||
|
||||
conv = Conv2D(in_channels=3, out_channels=16, kernel_size=5)
|
||||
|
||||
# Weights should be (out_channels, in_channels, kernel_height, kernel_width)
|
||||
expected_shape = (16, 3, 5, 5)
|
||||
if hasattr(conv, 'weights'):
|
||||
assert conv.weights.shape == expected_shape
|
||||
elif hasattr(conv, 'weight'):
|
||||
assert conv.weight.shape == expected_shape
|
||||
|
||||
except ImportError:
|
||||
assert True, "Conv2D weights not implemented yet"
|
||||
|
||||
def test_conv2d_forward_shape(self):
|
||||
"""Test Conv2D forward pass output shape."""
|
||||
try:
|
||||
from tinytorch.core.spatial import Conv2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
conv = Conv2D(in_channels=3, out_channels=16, kernel_size=3)
|
||||
|
||||
# Input: (batch_size, height, width, channels) - NHWC format
|
||||
x = Tensor(np.random.randn(8, 32, 32, 3))
|
||||
output = conv(x)
|
||||
|
||||
# With kernel_size=3 and no padding, output should be 30x30
|
||||
# Output: (batch_size, new_height, new_width, out_channels)
|
||||
expected_shape = (8, 30, 30, 16)
|
||||
assert output.shape == expected_shape
|
||||
|
||||
except ImportError:
|
||||
assert True, "Conv2D forward pass not implemented yet"
|
||||
|
||||
def test_conv2d_simple_convolution(self):
|
||||
"""Test simple convolution operation."""
|
||||
try:
|
||||
from tinytorch.core.spatial import Conv2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
# Simple 1-channel convolution
|
||||
conv = Conv2D(in_channels=1, out_channels=1, kernel_size=3)
|
||||
|
||||
# Set known kernel for testing
|
||||
if hasattr(conv, 'weights'):
|
||||
conv.weights = Tensor(np.ones((1, 1, 3, 3))) # Sum kernel
|
||||
elif hasattr(conv, 'weight'):
|
||||
conv.weight = Tensor(np.ones((1, 1, 3, 3)))
|
||||
|
||||
# Simple input
|
||||
x = Tensor(np.ones((1, 5, 5, 1))) # All ones
|
||||
output = conv(x)
|
||||
|
||||
# With all-ones input and all-ones kernel, output should be 9 everywhere
|
||||
expected_value = 9.0
|
||||
if output.shape == (1, 3, 3, 1):
|
||||
assert np.allclose(output.data, expected_value)
|
||||
|
||||
except ImportError:
|
||||
assert True, "Conv2D convolution operation not implemented yet"
|
||||
|
||||
|
||||
class TestPoolingLayers:
|
||||
"""Test pooling layers."""
|
||||
|
||||
def test_maxpool2d_creation(self):
|
||||
"""Test MaxPool2D layer creation."""
|
||||
try:
|
||||
from tinytorch.core.spatial import MaxPool2D
|
||||
|
||||
pool = MaxPool2D(pool_size=2)
|
||||
|
||||
assert pool.pool_size == 2
|
||||
|
||||
except ImportError:
|
||||
assert True, "MaxPool2D not implemented yet"
|
||||
|
||||
def test_maxpool2d_forward_shape(self):
|
||||
"""Test MaxPool2D forward pass output shape."""
|
||||
try:
|
||||
from tinytorch.core.spatial import MaxPool2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
pool = MaxPool2D(pool_size=2)
|
||||
|
||||
# Input: (batch_size, height, width, channels)
|
||||
x = Tensor(np.random.randn(4, 28, 28, 32))
|
||||
output = pool(x)
|
||||
|
||||
# Pooling by 2 should halve spatial dimensions
|
||||
expected_shape = (4, 14, 14, 32)
|
||||
assert output.shape == expected_shape
|
||||
|
||||
except ImportError:
|
||||
assert True, "MaxPool2D forward pass not implemented yet"
|
||||
|
||||
def test_maxpool2d_operation(self):
|
||||
"""Test MaxPool2D actually finds maximum values."""
|
||||
try:
|
||||
from tinytorch.core.spatial import MaxPool2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
pool = MaxPool2D(pool_size=2)
|
||||
|
||||
# Create input with known pattern
|
||||
# 4x4 input with values [1,2,3,4] in each 2x2 block
|
||||
x_data = np.array([[[[1, 2],
|
||||
[3, 4]],
|
||||
[[5, 6],
|
||||
[7, 8]]]]) # Shape: (1, 2, 2, 2)
|
||||
|
||||
x = Tensor(x_data)
|
||||
output = pool(x)
|
||||
|
||||
# MaxPool should select [4, 8] - the max from each 2x2 region
|
||||
if output.shape == (1, 1, 1, 2):
|
||||
assert output.data[0, 0, 0, 0] == 4 # Max of [1,2,3,4]
|
||||
assert output.data[0, 0, 0, 1] == 8 # Max of [5,6,7,8]
|
||||
|
||||
except ImportError:
|
||||
assert True, "MaxPool2D operation not implemented yet"
|
||||
|
||||
def test_avgpool2d_operation(self):
|
||||
"""Test average pooling."""
|
||||
try:
|
||||
from tinytorch.core.spatial import AvgPool2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
pool = AvgPool2D(pool_size=2)
|
||||
|
||||
# 2x2 input with known values
|
||||
x_data = np.array([[[[1, 2],
|
||||
[3, 4]]]]) # Shape: (1, 2, 2, 1)
|
||||
|
||||
x = Tensor(x_data)
|
||||
output = pool(x)
|
||||
|
||||
# Average should be (1+2+3+4)/4 = 2.5
|
||||
if output.shape == (1, 1, 1, 1):
|
||||
assert np.isclose(output.data[0, 0, 0, 0], 2.5)
|
||||
|
||||
except ImportError:
|
||||
assert True, "AvgPool2D not implemented yet"
|
||||
|
||||
|
||||
class TestSpatialUtilities:
|
||||
"""Test spatial operation utilities."""
|
||||
|
||||
def test_padding_operation(self):
|
||||
"""Test padding functionality."""
|
||||
try:
|
||||
from tinytorch.core.spatial import pad2d
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
# Simple 2x2 input
|
||||
x = Tensor(np.array([[[[1, 2],
|
||||
[3, 4]]]])) # Shape: (1, 2, 2, 1)
|
||||
|
||||
# Pad with 1 pixel on all sides
|
||||
padded = pad2d(x, padding=1, value=0)
|
||||
|
||||
# Should become 4x4 with zeros around border
|
||||
expected_shape = (1, 4, 4, 1)
|
||||
assert padded.shape == expected_shape
|
||||
|
||||
# Center should contain original values
|
||||
assert padded.data[0, 1, 1, 0] == 1
|
||||
assert padded.data[0, 1, 2, 0] == 2
|
||||
assert padded.data[0, 2, 1, 0] == 3
|
||||
assert padded.data[0, 2, 2, 0] == 4
|
||||
|
||||
except ImportError:
|
||||
assert True, "Padding operation not implemented yet"
|
||||
|
||||
def test_im2col_operation(self):
|
||||
"""Test im2col operation for efficient convolution."""
|
||||
try:
|
||||
from tinytorch.core.spatial import im2col
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
# Simple 3x3 input
|
||||
x = Tensor(np.arange(9).reshape(1, 3, 3, 1))
|
||||
|
||||
# Extract 2x2 patches
|
||||
patches = im2col(x, kernel_size=2, stride=1)
|
||||
|
||||
# Should get 4 patches (2x2 sliding window on 3x3 input)
|
||||
# Each patch should have 4 values (2x2 kernel)
|
||||
expected_num_patches = 4
|
||||
expected_patch_size = 4
|
||||
|
||||
if hasattr(patches, 'shape'):
|
||||
assert patches.shape[1] == expected_patch_size
|
||||
|
||||
except ImportError:
|
||||
assert True, "im2col operation not implemented yet"
|
||||
|
||||
def test_spatial_dimensions(self):
|
||||
"""Test spatial dimension calculations."""
|
||||
try:
|
||||
from tinytorch.core.spatial import calc_output_size
|
||||
|
||||
# Common convolution size calculation
|
||||
input_size = 32
|
||||
kernel_size = 5
|
||||
stride = 1
|
||||
padding = 2
|
||||
|
||||
output_size = calc_output_size(input_size, kernel_size, stride, padding)
|
||||
|
||||
# Formula: (input + 2*padding - kernel) / stride + 1
|
||||
expected = (32 + 2*2 - 5) // 1 + 1 # = 32
|
||||
assert output_size == expected
|
||||
|
||||
except ImportError:
|
||||
# Manual calculation test
|
||||
input_size = 32
|
||||
kernel_size = 5
|
||||
stride = 1
|
||||
padding = 2
|
||||
|
||||
output_size = (input_size + 2*padding - kernel_size) // stride + 1
|
||||
assert output_size == 32
|
||||
|
||||
|
||||
class TestCNNArchitecture:
|
||||
"""Test CNN architecture components working together."""
|
||||
|
||||
def test_conv_relu_pool_chain(self):
|
||||
"""Test Conv -> ReLU -> Pool chain."""
|
||||
try:
|
||||
from tinytorch.core.spatial import Conv2D, MaxPool2D
|
||||
from tinytorch.core.activations import ReLU
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
# Build simple CNN block
|
||||
conv = Conv2D(3, 16, kernel_size=3)
|
||||
relu = ReLU()
|
||||
pool = MaxPool2D(pool_size=2)
|
||||
|
||||
# Input image
|
||||
x = Tensor(np.random.randn(1, 32, 32, 3))
|
||||
|
||||
# Forward pass
|
||||
h1 = conv(x) # (1, 30, 30, 16)
|
||||
h2 = relu(h1) # (1, 30, 30, 16)
|
||||
output = pool(h2) # (1, 15, 15, 16)
|
||||
|
||||
expected_shape = (1, 15, 15, 16)
|
||||
assert output.shape == expected_shape
|
||||
|
||||
except ImportError:
|
||||
assert True, "CNN architecture chaining not ready yet"
|
||||
|
||||
def test_feature_map_progression(self):
|
||||
"""Test feature map size progression through CNN."""
|
||||
try:
|
||||
from tinytorch.core.spatial import Conv2D, MaxPool2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
# Typical CNN progression: increase channels, decrease spatial size
|
||||
conv1 = Conv2D(3, 32, kernel_size=3) # 3 -> 32 channels
|
||||
pool1 = MaxPool2D(pool_size=2) # /2 spatial size
|
||||
conv2 = Conv2D(32, 64, kernel_size=3) # 32 -> 64 channels
|
||||
pool2 = MaxPool2D(pool_size=2) # /2 spatial size
|
||||
|
||||
x = Tensor(np.random.randn(1, 32, 32, 3)) # Start: 32x32x3
|
||||
|
||||
h1 = conv1(x) # 30x30x32
|
||||
h2 = pool1(h1) # 15x15x32
|
||||
h3 = conv2(h2) # 13x13x64
|
||||
h4 = pool2(h3) # 6x6x64 (or 7x7x64)
|
||||
|
||||
# Should progressively reduce spatial size, increase channels
|
||||
assert h1.shape[3] == 32 # More channels
|
||||
assert h2.shape[1] < h1.shape[1] # Smaller spatial
|
||||
assert h3.shape[3] == 64 # Even more channels
|
||||
assert h4.shape[1] < h3.shape[1] # Even smaller spatial
|
||||
|
||||
except ImportError:
|
||||
assert True, "Feature map progression not ready yet"
|
||||
|
||||
def test_global_average_pooling(self):
|
||||
"""Test global average pooling for classification."""
|
||||
try:
|
||||
from tinytorch.core.spatial import GlobalAvgPool2D
|
||||
from tinytorch.core.tensor import Tensor
|
||||
|
||||
gap = GlobalAvgPool2D()
|
||||
|
||||
# Feature maps from CNN
|
||||
x = Tensor(np.random.randn(1, 7, 7, 512)) # Typical CNN output
|
||||
output = gap(x)
|
||||
|
||||
# Should average over spatial dimensions
|
||||
expected_shape = (1, 1, 1, 512) # or (1, 512)
|
||||
assert output.shape == expected_shape or output.shape == (1, 512)
|
||||
|
||||
except ImportError:
|
||||
# Manual global average pooling
|
||||
x_data = np.random.randn(1, 7, 7, 512)
|
||||
output_data = np.mean(x_data, axis=(1, 2), keepdims=True)
|
||||
assert output_data.shape == (1, 1, 1, 512)
|
||||
@@ -1,266 +0,0 @@
|
||||
"""
|
||||
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