From 4a1bc7c7f4c979d3dfddbd9ea04b2eb11e035ae2 Mon Sep 17 00:00:00 2001 From: Vijay Janapa Reddi Date: Sun, 13 Jul 2025 23:10:14 -0400 Subject: [PATCH] Reorganize tests: Remove mocks, add real integration tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REMOVED (Mock-based tests that duplicate inline tests): • test_activations.py - Used MockTensor instead of real Tensor • test_layers.py - Used MockTensor instead of real Tensor • test_networks.py - Used MockTensor/MockLayer instead of real components • test_cnn.py - Used MockTensor instead of real Tensor • test_dataloader.py - Used MockTensor/MockDataset instead of real components ADDED (Real integration tests with actual TinyTorch components): • integration/test_tensor_activations.py - Tests real Tensor ↔ Activations integration • integration/test_layers_networks.py - Tests real Dense ↔ Sequential/MLP integration • e2e/ directory structure for end-to-end tests RESULT: • Reduced test count from 209 → 70 (removed 139 redundant mock-based tests) • All 70 remaining tests use real components for true integration testing • Clear separation: inline tests (component validation) vs integration tests (cross-module) • Better QA structure following proper testing pyramid This follows QA best practices: since all modules are working and building on each other, integration tests should use real components, not mocks. Mocks were preventing us from catching actual integration issues. --- tests/e2e/__init__.py | 6 + tests/integration/test_layers_networks.py | 306 ++++++++++ tests/integration/test_tensor_activations.py | 230 ++++++++ tests/test_activations.py | 514 ----------------- tests/test_cnn.py | 467 --------------- tests/test_dataloader.py | 577 ------------------- tests/test_layers.py | 364 ------------ tests/test_networks.py | 552 ------------------ 8 files changed, 542 insertions(+), 2474 deletions(-) create mode 100644 tests/e2e/__init__.py create mode 100644 tests/integration/test_layers_networks.py create mode 100644 tests/integration/test_tensor_activations.py delete mode 100644 tests/test_activations.py delete mode 100644 tests/test_cnn.py delete mode 100644 tests/test_dataloader.py delete mode 100644 tests/test_layers.py delete mode 100644 tests/test_networks.py diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 00000000..000b702a --- /dev/null +++ b/tests/e2e/__init__.py @@ -0,0 +1,6 @@ +""" +End-to-End tests for TinyTorch. + +These tests verify complete workflows and real-world scenarios +using actual TinyTorch components throughout the entire pipeline. +""" \ No newline at end of file diff --git a/tests/integration/test_layers_networks.py b/tests/integration/test_layers_networks.py new file mode 100644 index 00000000..4206dc23 --- /dev/null +++ b/tests/integration/test_layers_networks.py @@ -0,0 +1,306 @@ +""" +Integration Tests - Layers and Networks + +Tests real integration between Dense layers and Network architectures. +Uses actual TinyTorch components to verify they work together correctly. +""" + +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.activations import ReLU, Sigmoid, Tanh + from tinytorch.core.layers import Dense + from tinytorch.core.networks import Sequential, MLP +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" / "04_networks")) + + from tensor_dev import Tensor + from activations_dev import ReLU, Sigmoid, Tanh + from layers_dev import Dense + from networks_dev import Sequential, MLP + + +class TestLayerNetworkIntegration: + """Test real integration between Dense layers and Networks.""" + + def test_dense_layer_with_real_tensors(self): + """Test Dense layer works with real Tensor objects.""" + # Create real layer and tensor + layer = Dense(input_size=3, output_size=2) + x = Tensor([[1.0, 2.0, 3.0]]) + + # Forward pass + result = layer(x) + + # Verify real integration + assert isinstance(result, Tensor) + assert result.shape == (1, 2) + assert hasattr(result, 'data') + assert not np.any(np.isnan(result.data)) + + def test_sequential_with_real_components(self): + """Test Sequential network with real Dense layers and activations.""" + # Create network with REAL components + network = Sequential([ + Dense(input_size=4, output_size=8), + ReLU(), + Dense(input_size=8, output_size=4), + Sigmoid(), + Dense(input_size=4, output_size=2) + ]) + + # Test with real tensor + x = Tensor([[1.0, 2.0, 3.0, 4.0]]) + result = network(x) + + # Verify real integration + assert isinstance(result, Tensor) + assert result.shape == (1, 2) + assert not np.any(np.isnan(result.data)) + assert not np.any(np.isinf(result.data)) + + def test_mlp_with_real_components(self): + """Test MLP network with real components.""" + # Create MLP with real components + mlp = MLP(input_size=5, hidden_size=10, output_size=3) + + # Test with real tensor + x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0]]) + result = mlp(x) + + # Verify real integration + assert isinstance(result, Tensor) + assert result.shape == (1, 3) + assert hasattr(mlp, 'network') + assert isinstance(mlp.network, Sequential) + + def test_deep_network_integration(self): + """Test deep network with multiple real layers.""" + # Create deep network + deep_network = Sequential([ + Dense(input_size=6, output_size=12), + ReLU(), + Dense(input_size=12, output_size=8), + Tanh(), + Dense(input_size=8, output_size=4), + ReLU(), + Dense(input_size=4, output_size=2), + Sigmoid() + ]) + + # Test with real tensor + x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) + result = deep_network(x) + + # Verify deep integration + assert isinstance(result, Tensor) + assert result.shape == (1, 2) + + # Verify final activation worked (sigmoid bounds) + assert np.all(result.data >= 0.0) + assert np.all(result.data <= 1.0) + + def test_batch_processing_integration(self): + """Test network works with batched real tensors.""" + network = Sequential([ + Dense(input_size=3, output_size=6), + ReLU(), + Dense(input_size=6, output_size=2) + ]) + + # Create batch of real tensors + batch_x = Tensor([ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0] + ]) + + result = network(batch_x) + + # Verify batch integration + assert isinstance(result, Tensor) + assert result.shape == (3, 2) + assert not np.any(np.isnan(result.data)) + + def test_layer_parameter_sharing(self): + """Test that layers maintain consistent parameters.""" + layer = Dense(input_size=4, output_size=3) + + # Store original parameters + original_weights = np.copy(layer.weights.data) + original_bias = np.copy(layer.bias.data) if layer.bias is not None else None + + # Multiple forward passes + x1 = Tensor([[1.0, 2.0, 3.0, 4.0]]) + x2 = Tensor([[5.0, 6.0, 7.0, 8.0]]) + + result1 = layer(x1) + result2 = layer(x2) + + # Parameters should be unchanged + np.testing.assert_array_equal(layer.weights.data, original_weights) + if original_bias is not None: + np.testing.assert_array_equal(layer.bias.data, original_bias) + + # Results should be different for different inputs + assert not np.allclose(result1.data, result2.data) + + def test_network_composition_integration(self): + """Test composing networks with real components.""" + # Create encoder + encoder = Sequential([ + Dense(input_size=8, output_size=4), + ReLU(), + Dense(input_size=4, output_size=2) + ]) + + # Create decoder + decoder = Sequential([ + Dense(input_size=2, output_size=4), + ReLU(), + Dense(input_size=4, output_size=8) + ]) + + # Compose autoencoder + autoencoder = Sequential([encoder, decoder]) + + # Test with real tensor + x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]]) + result = autoencoder(x) + + # Verify composition + assert isinstance(result, Tensor) + assert result.shape == x.shape # Should reconstruct input size + + +class TestMLClassificationPipeline: + """Test realistic ML classification pipelines.""" + + def test_binary_classifier_integration(self): + """Test binary classification with real components.""" + # Create binary classifier + classifier = Sequential([ + Dense(input_size=4, output_size=8), + ReLU(), + Dense(input_size=8, output_size=4), + ReLU(), + Dense(input_size=4, output_size=1), + Sigmoid() # Binary classification + ]) + + # Test with sample data + x = Tensor([[0.5, 1.5, -0.5, 2.0]]) + prediction = classifier(x) + + # Verify binary classification + assert isinstance(prediction, Tensor) + assert prediction.shape == (1, 1) + assert 0.0 <= prediction.data[0, 0] <= 1.0 # Probability + + def test_multiclass_classifier_integration(self): + """Test multi-class classification with real components.""" + # Create multi-class classifier (3 classes) + classifier = MLP( + input_size=6, + hidden_size=12, + output_size=3, + activation=ReLU, + output_activation=None # Will add softmax manually + ) + + # Add softmax for multi-class + from tinytorch.core.activations import Softmax + softmax = Softmax() + + # Test prediction + x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]]) + logits = classifier(x) + probabilities = softmax(logits) + + # Verify multi-class classification + assert isinstance(probabilities, Tensor) + assert probabilities.shape == (1, 3) + assert np.isclose(np.sum(probabilities.data), 1.0, atol=1e-6) + assert np.all(probabilities.data >= 0.0) + + def test_feature_extraction_pipeline(self): + """Test feature extraction with real components.""" + # Feature extractor (encoder part) + feature_extractor = Sequential([ + Dense(input_size=10, output_size=16), + ReLU(), + Dense(input_size=16, output_size=8), + ReLU(), + Dense(input_size=8, output_size=4) # Feature representation + ]) + + # Classifier head + classifier = Sequential([ + Dense(input_size=4, output_size=8), + ReLU(), + Dense(input_size=8, output_size=2) + ]) + + # Full pipeline + x = Tensor([[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]]) + + # Extract features + features = feature_extractor(x) + assert isinstance(features, Tensor) + assert features.shape == (1, 4) + + # Classify features + predictions = classifier(features) + assert isinstance(predictions, Tensor) + assert predictions.shape == (1, 2) + + +class TestErrorHandlingIntegration: + """Test error handling in real integration scenarios.""" + + def test_shape_mismatch_detection(self): + """Test that shape mismatches are properly detected.""" + layer = Dense(input_size=3, output_size=2) + + # Wrong input size should raise error + wrong_x = Tensor([[1.0, 2.0]]) # Should be size 3 + + with pytest.raises(Exception): + layer(wrong_x) + + def test_network_layer_compatibility(self): + """Test network layer compatibility checking.""" + # Create network with incompatible layers + try: + incompatible_network = Sequential([ + Dense(input_size=4, output_size=3), + Dense(input_size=5, output_size=2) # Expects 5, gets 3 + ]) + + x = Tensor([[1.0, 2.0, 3.0, 4.0]]) + result = incompatible_network(x) + + # If no error raised, should handle gracefully + assert isinstance(result, Tensor) + + except Exception: + # It's acceptable to raise an error for incompatible layers + pass + + +if __name__ == "__main__": + # Run integration tests + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/integration/test_tensor_activations.py b/tests/integration/test_tensor_activations.py new file mode 100644 index 00000000..5f9d773a --- /dev/null +++ b/tests/integration/test_tensor_activations.py @@ -0,0 +1,230 @@ +""" +Integration Tests - Tensor and Activations + +Tests real integration between Tensor and Activation modules. +Uses actual TinyTorch components to verify they work together correctly. +""" + +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.activations import ReLU, Sigmoid, Tanh, Softmax +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")) + + from tensor_dev import Tensor + from activations_dev import ReLU, Sigmoid, Tanh, Softmax + + +class TestTensorActivationIntegration: + """Test real integration between Tensor and Activation modules.""" + + def test_relu_with_real_tensors(self): + """Test ReLU activation with real Tensor objects.""" + relu = ReLU() + + # Test with negative, zero, and positive values + x = Tensor([[-2.0, -1.0, 0.0, 1.0, 2.0]]) + result = relu(x) + + # Verify it returns a real Tensor + assert isinstance(result, Tensor) + assert result.shape == x.shape + + # Verify ReLU behavior: max(0, x) + expected = np.array([[0.0, 0.0, 0.0, 1.0, 2.0]]) + np.testing.assert_allclose(result.data, expected) + + def test_sigmoid_with_real_tensors(self): + """Test Sigmoid activation with real Tensor objects.""" + sigmoid = Sigmoid() + + # Test with various inputs + x = Tensor([[-5.0, -1.0, 0.0, 1.0, 5.0]]) + result = sigmoid(x) + + # Verify it returns a real Tensor + assert isinstance(result, Tensor) + assert result.shape == x.shape + + # Verify sigmoid properties + assert np.all(result.data > 0.0) # All positive + assert np.all(result.data < 1.0) # All less than 1 + assert np.isclose(result.data[0, 2], 0.5, atol=1e-6) # sigmoid(0) = 0.5 + + def test_tanh_with_real_tensors(self): + """Test Tanh activation with real Tensor objects.""" + tanh = Tanh() + + x = Tensor([[-2.0, -1.0, 0.0, 1.0, 2.0]]) + result = tanh(x) + + # Verify it returns a real Tensor + assert isinstance(result, Tensor) + assert result.shape == x.shape + + # Verify tanh properties + assert np.all(result.data > -1.0) # All greater than -1 + assert np.all(result.data < 1.0) # All less than 1 + assert np.isclose(result.data[0, 2], 0.0, atol=1e-6) # tanh(0) = 0 + + def test_softmax_with_real_tensors(self): + """Test Softmax activation with real Tensor objects.""" + softmax = Softmax() + + # Test with logits + x = Tensor([[1.0, 2.0, 3.0]]) + result = softmax(x) + + # Verify it returns a real Tensor + assert isinstance(result, Tensor) + assert result.shape == x.shape + + # Verify softmax properties + assert np.all(result.data > 0.0) # All positive + assert np.all(result.data < 1.0) # All less than 1 + assert np.isclose(np.sum(result.data), 1.0, atol=1e-6) # Sums to 1 + + def test_activation_chaining_with_real_tensors(self): + """Test chaining activations with real Tensors.""" + relu = ReLU() + sigmoid = Sigmoid() + + # Start with mixed positive/negative values + x = Tensor([[-1.0, 0.0, 1.0, 2.0]]) + + # Apply ReLU first: negative values become 0 + relu_result = relu(x) + expected_after_relu = np.array([[0.0, 0.0, 1.0, 2.0]]) + np.testing.assert_allclose(relu_result.data, expected_after_relu) + + # Apply Sigmoid to ReLU output + final_result = sigmoid(relu_result) + + # Verify final result properties + assert isinstance(final_result, Tensor) + assert np.all(final_result.data > 0.0) + assert np.all(final_result.data < 1.0) + + # First two should be sigmoid(0) = 0.5 + assert np.isclose(final_result.data[0, 0], 0.5, atol=1e-6) + assert np.isclose(final_result.data[0, 1], 0.5, atol=1e-6) + + def test_batch_processing_integration(self): + """Test activation functions work with batched tensors.""" + activations = [ReLU(), Sigmoid(), Tanh()] + + # Create batch of samples + batch_x = Tensor([ + [-2.0, -1.0, 0.0, 1.0, 2.0], + [0.5, 1.5, -0.5, -1.5, 0.0], + [3.0, -3.0, 1.0, -1.0, 0.0] + ]) + + for activation in activations: + result = activation(batch_x) + + # Verify batch processing preserves shape + assert isinstance(result, Tensor) + assert result.shape == batch_x.shape + assert not np.any(np.isnan(result.data)) + assert not np.any(np.isinf(result.data)) + + def test_softmax_batch_integration(self): + """Test Softmax works correctly with batched tensors.""" + softmax = Softmax() + + # Create batch of logits + batch_x = Tensor([ + [1.0, 2.0, 3.0], + [0.0, 0.0, 0.0], + [10.0, 20.0, 30.0] + ]) + + result = softmax(batch_x) + + # Verify batch processing + assert isinstance(result, Tensor) + assert result.shape == batch_x.shape + + # Each row should sum to 1 + for i in range(batch_x.shape[0]): + row_sum = np.sum(result.data[i]) + assert np.isclose(row_sum, 1.0, atol=1e-6) + + def test_tensor_type_preservation(self): + """Test that activations preserve tensor type and properties.""" + activations = [ReLU(), Sigmoid(), Tanh(), Softmax()] + + # Test with different tensor shapes + test_tensors = [ + Tensor([5.0]), # Scalar + Tensor([1.0, 2.0, 3.0]), # 1D + Tensor([[1.0, 2.0], [3.0, 4.0]]), # 2D + ] + + for tensor in test_tensors: + for activation in activations: + result = activation(tensor) + + # Verify type preservation + assert isinstance(result, Tensor) + assert result.shape == tensor.shape + assert hasattr(result, 'data') + assert hasattr(result, 'shape') + + def test_numerical_stability_integration(self): + """Test numerical stability when using real tensors with activations.""" + # Test with extreme values + extreme_tensor = Tensor([[-1000.0, 1000.0, 0.0]]) + + # ReLU should handle extreme values + relu = ReLU() + relu_result = relu(extreme_tensor) + assert np.all(np.isfinite(relu_result.data)) + + # Sigmoid should handle extreme values + sigmoid = Sigmoid() + sigmoid_result = sigmoid(extreme_tensor) + assert np.all(np.isfinite(sigmoid_result.data)) + assert np.all(sigmoid_result.data >= 0.0) + assert np.all(sigmoid_result.data <= 1.0) + + # Tanh should handle extreme values + tanh = Tanh() + tanh_result = tanh(extreme_tensor) + assert np.all(np.isfinite(tanh_result.data)) + assert np.all(tanh_result.data >= -1.0) + assert np.all(tanh_result.data <= 1.0) + + +class TestActivationPolymorphism: + """Test that activations work with different tensor-like objects.""" + + def test_activation_type_preservation(self): + """Test that activations preserve input type.""" + relu = ReLU() + + # Test with real Tensor + tensor_input = Tensor([[1.0, -1.0, 2.0]]) + tensor_result = relu(tensor_input) + + # Should return same type as input + assert type(tensor_result) == type(tensor_input) + assert isinstance(tensor_result, Tensor) + + +if __name__ == "__main__": + # Run integration tests + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_activations.py b/tests/test_activations.py deleted file mode 100644 index 7723219f..00000000 --- a/tests/test_activations.py +++ /dev/null @@ -1,514 +0,0 @@ -""" -Mock-based module tests for Activations module. - -This test file uses simple mocks to avoid cross-module dependencies while thoroughly -testing the Activations module functionality. The MockTensor class provides a minimal -interface that matches the expected Tensor behavior without requiring the actual -Tensor implementation. - -Test Philosophy: -- Use simple, visible mocks instead of complex mocking frameworks -- Test interface contracts and behavior, not implementation details -- Avoid dependency cascade where activations tests fail due to tensor bugs -- Focus on the activation functions' mathematical correctness -- Ensure educational value with clear test structure -""" - -import pytest -import numpy as np -import sys -import os - -# Add the module source directory to the path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '02_activations')) - -from activations_dev import ReLU, Sigmoid, Tanh, Softmax - - -class MockTensor: - """ - Simple mock tensor for testing activations without tensor dependencies. - - This mock provides just enough functionality to test activation functions - without requiring the full Tensor implementation. It's intentionally simple - and visible to make test behavior clear. - """ - - def __init__(self, data): - """Initialize with numpy array data.""" - if isinstance(data, (list, tuple)): - self.data = np.array(data, dtype=np.float32) - elif isinstance(data, np.ndarray): - self.data = data.astype(np.float32) - else: - self.data = np.array([data], dtype=np.float32) - - @property - def shape(self): - """Return shape of the underlying data.""" - return self.data.shape - - def __repr__(self): - return f"MockTensor({self.data})" - - def __eq__(self, other): - """Check equality with another MockTensor.""" - if isinstance(other, MockTensor): - return np.allclose(self.data, other.data) - return False - - -class TestReLUActivation: - """Test ReLU activation function with mock tensors.""" - - def test_relu_initialization(self): - """Test ReLU can be initialized without parameters.""" - relu = ReLU() - assert relu is not None - assert hasattr(relu, 'forward') or hasattr(relu, '__call__') - - def test_relu_positive_values(self): - """Test ReLU preserves positive values.""" - relu = ReLU() - - # Test single positive value - input_tensor = MockTensor([5.0]) - output = relu(input_tensor) - assert isinstance(output, MockTensor) - assert np.allclose(output.data, [5.0]) - - # Test multiple positive values - input_tensor = MockTensor([1.0, 2.5, 10.0]) - output = relu(input_tensor) - assert np.allclose(output.data, [1.0, 2.5, 10.0]) - - def test_relu_negative_values(self): - """Test ReLU zeros out negative values.""" - relu = ReLU() - - # Test single negative value - input_tensor = MockTensor([-3.0]) - output = relu(input_tensor) - assert np.allclose(output.data, [0.0]) - - # Test multiple negative values - input_tensor = MockTensor([-1.0, -2.5, -10.0]) - output = relu(input_tensor) - assert np.allclose(output.data, [0.0, 0.0, 0.0]) - - def test_relu_mixed_values(self): - """Test ReLU with mixed positive and negative values.""" - relu = ReLU() - - input_tensor = MockTensor([-2.0, 0.0, 3.0, -1.5, 4.5]) - output = relu(input_tensor) - expected = [0.0, 0.0, 3.0, 0.0, 4.5] - assert np.allclose(output.data, expected) - - def test_relu_zero_value(self): - """Test ReLU behavior at zero (should return zero).""" - relu = ReLU() - - input_tensor = MockTensor([0.0]) - output = relu(input_tensor) - assert np.allclose(output.data, [0.0]) - - def test_relu_2d_input(self): - """Test ReLU with 2D input (matrices).""" - relu = ReLU() - - input_data = np.array([[-1.0, 2.0], [3.0, -4.0]]) - input_tensor = MockTensor(input_data) - output = relu(input_tensor) - expected = np.array([[0.0, 2.0], [3.0, 0.0]]) - assert np.allclose(output.data, expected) - - def test_relu_large_values(self): - """Test ReLU with very large values.""" - relu = ReLU() - - input_tensor = MockTensor([1000.0, -1000.0]) - output = relu(input_tensor) - expected = [1000.0, 0.0] - assert np.allclose(output.data, expected) - - -class TestSigmoidActivation: - """Test Sigmoid activation function with mock tensors.""" - - def test_sigmoid_initialization(self): - """Test Sigmoid can be initialized without parameters.""" - sigmoid = Sigmoid() - assert sigmoid is not None - assert hasattr(sigmoid, 'forward') or hasattr(sigmoid, '__call__') - - def test_sigmoid_zero_input(self): - """Test Sigmoid at zero (should return 0.5).""" - sigmoid = Sigmoid() - - input_tensor = MockTensor([0.0]) - output = sigmoid(input_tensor) - assert np.allclose(output.data, [0.5], atol=1e-6) - - def test_sigmoid_positive_values(self): - """Test Sigmoid with positive values (should be > 0.5).""" - sigmoid = Sigmoid() - - input_tensor = MockTensor([1.0, 2.0, 5.0]) - output = sigmoid(input_tensor) - - # All outputs should be > 0.5 - assert np.all(output.data > 0.5) - # All outputs should be < 1.0 - assert np.all(output.data < 1.0) - # Larger inputs should give larger outputs - assert output.data[0] < output.data[1] < output.data[2] - - def test_sigmoid_negative_values(self): - """Test Sigmoid with negative values (should be < 0.5).""" - sigmoid = Sigmoid() - - input_tensor = MockTensor([-1.0, -2.0, -5.0]) - output = sigmoid(input_tensor) - - # All outputs should be < 0.5 - assert np.all(output.data < 0.5) - # All outputs should be > 0.0 - assert np.all(output.data > 0.0) - # More negative inputs should give smaller outputs - assert output.data[0] > output.data[1] > output.data[2] - - def test_sigmoid_symmetry(self): - """Test Sigmoid symmetry: sigmoid(x) + sigmoid(-x) = 1.""" - sigmoid = Sigmoid() - - x_values = [1.0, 2.0, 3.0] - pos_input = MockTensor(x_values) - neg_input = MockTensor([-x for x in x_values]) - - pos_output = sigmoid(pos_input) - neg_output = sigmoid(neg_input) - - # sigmoid(x) + sigmoid(-x) should equal 1 - sum_output = pos_output.data + neg_output.data - assert np.allclose(sum_output, [1.0, 1.0, 1.0], atol=1e-6) - - def test_sigmoid_extreme_values(self): - """Test Sigmoid with extreme values.""" - sigmoid = Sigmoid() - - # Very large positive value should approach 1 - large_pos = MockTensor([100.0]) - output_pos = sigmoid(large_pos) - assert np.allclose(output_pos.data, [1.0], atol=1e-6) - - # Very large negative value should approach 0 - large_neg = MockTensor([-100.0]) - output_neg = sigmoid(large_neg) - assert np.allclose(output_neg.data, [0.0], atol=1e-6) - - def test_sigmoid_2d_input(self): - """Test Sigmoid with 2D input.""" - sigmoid = Sigmoid() - - input_data = np.array([[0.0, 1.0], [-1.0, 2.0]]) - input_tensor = MockTensor(input_data) - output = sigmoid(input_tensor) - - # Check that output has correct shape - assert output.shape == (2, 2) - # Check that all values are in (0, 1) - assert np.all(output.data > 0.0) - assert np.all(output.data < 1.0) - - -class TestTanhActivation: - """Test Tanh activation function with mock tensors.""" - - def test_tanh_initialization(self): - """Test Tanh can be initialized without parameters.""" - tanh = Tanh() - assert tanh is not None - assert hasattr(tanh, 'forward') or hasattr(tanh, '__call__') - - def test_tanh_zero_input(self): - """Test Tanh at zero (should return 0).""" - tanh = Tanh() - - input_tensor = MockTensor([0.0]) - output = tanh(input_tensor) - assert np.allclose(output.data, [0.0], atol=1e-6) - - def test_tanh_positive_values(self): - """Test Tanh with positive values (should be in (0, 1)).""" - tanh = Tanh() - - input_tensor = MockTensor([0.5, 1.0, 2.0]) - output = tanh(input_tensor) - - # All outputs should be > 0 - assert np.all(output.data > 0.0) - # All outputs should be < 1 - assert np.all(output.data < 1.0) - # Larger inputs should give larger outputs - assert output.data[0] < output.data[1] < output.data[2] - - def test_tanh_negative_values(self): - """Test Tanh with negative values (should be in (-1, 0)).""" - tanh = Tanh() - - input_tensor = MockTensor([-0.5, -1.0, -2.0]) - output = tanh(input_tensor) - - # All outputs should be < 0 - assert np.all(output.data < 0.0) - # All outputs should be > -1 - assert np.all(output.data > -1.0) - # More negative inputs should give more negative outputs - assert output.data[0] > output.data[1] > output.data[2] - - def test_tanh_antisymmetry(self): - """Test Tanh antisymmetry: tanh(-x) = -tanh(x).""" - tanh = Tanh() - - x_values = [1.0, 2.0, 3.0] - pos_input = MockTensor(x_values) - neg_input = MockTensor([-x for x in x_values]) - - pos_output = tanh(pos_input) - neg_output = tanh(neg_input) - - # tanh(-x) should equal -tanh(x) - assert np.allclose(neg_output.data, -pos_output.data, atol=1e-6) - - def test_tanh_extreme_values(self): - """Test Tanh with extreme values.""" - tanh = Tanh() - - # Very large positive value should approach 1 - large_pos = MockTensor([100.0]) - output_pos = tanh(large_pos) - assert np.allclose(output_pos.data, [1.0], atol=1e-6) - - # Very large negative value should approach -1 - large_neg = MockTensor([-100.0]) - output_neg = tanh(large_neg) - assert np.allclose(output_neg.data, [-1.0], atol=1e-6) - - def test_tanh_2d_input(self): - """Test Tanh with 2D input.""" - tanh = Tanh() - - input_data = np.array([[0.0, 1.0], [-1.0, 2.0]]) - input_tensor = MockTensor(input_data) - output = tanh(input_tensor) - - # Check that output has correct shape - assert output.shape == (2, 2) - # Check that all values are in (-1, 1) - assert np.all(output.data > -1.0) - assert np.all(output.data < 1.0) - - -class TestSoftmaxActivation: - """Test Softmax activation function with mock tensors.""" - - def test_softmax_initialization(self): - """Test Softmax can be initialized without parameters.""" - softmax = Softmax() - assert softmax is not None - assert hasattr(softmax, 'forward') or hasattr(softmax, '__call__') - - def test_softmax_probability_distribution(self): - """Test Softmax produces valid probability distribution.""" - softmax = Softmax() - - input_tensor = MockTensor([1.0, 2.0, 3.0]) - output = softmax(input_tensor) - - # All outputs should be positive - assert np.all(output.data > 0.0) - # All outputs should be less than 1 - assert np.all(output.data < 1.0) - # Outputs should sum to 1 - assert np.allclose(np.sum(output.data), 1.0, atol=1e-6) - - def test_softmax_uniform_input(self): - """Test Softmax with uniform input (should give uniform distribution).""" - softmax = Softmax() - - input_tensor = MockTensor([2.0, 2.0, 2.0]) - output = softmax(input_tensor) - - # Should be uniform distribution - expected = 1.0 / 3.0 - assert np.allclose(output.data, [expected, expected, expected], atol=1e-6) - - def test_softmax_max_element(self): - """Test Softmax emphasizes maximum element.""" - softmax = Softmax() - - input_tensor = MockTensor([1.0, 5.0, 2.0]) - output = softmax(input_tensor) - - # Maximum input should correspond to maximum output - max_input_idx = np.argmax([1.0, 5.0, 2.0]) - max_output_idx = np.argmax(output.data) - assert max_input_idx == max_output_idx - - # Maximum output should be significantly larger than others - assert output.data[max_output_idx] > 0.8 # Should be dominant - - def test_softmax_numerical_stability(self): - """Test Softmax numerical stability with large values.""" - softmax = Softmax() - - # Large values that could cause overflow in naive implementation - input_tensor = MockTensor([1000.0, 1001.0, 999.0]) - output = softmax(input_tensor) - - # Should still be valid probability distribution - assert np.all(output.data > 0.0) - assert np.all(output.data < 1.0) - assert np.allclose(np.sum(output.data), 1.0, atol=1e-6) - - # Maximum element should dominate - max_idx = np.argmax(output.data) - assert max_idx == 1 # 1001.0 is the maximum - - def test_softmax_2d_input(self): - """Test Softmax with 2D input (batch processing).""" - softmax = Softmax() - - # Test with 2D input where each row is a sample - input_data = np.array([[1.0, 2.0, 3.0], [3.0, 1.0, 2.0]]) - input_tensor = MockTensor(input_data) - output = softmax(input_tensor) - - # Check that output has correct shape - assert output.shape == (2, 3) - - # Each row should sum to 1 - row_sums = np.sum(output.data, axis=1) - assert np.allclose(row_sums, [1.0, 1.0], atol=1e-6) - - # All values should be positive - assert np.all(output.data > 0.0) - - def test_softmax_single_element(self): - """Test Softmax with single element (should return 1.0).""" - softmax = Softmax() - - input_tensor = MockTensor([5.0]) - output = softmax(input_tensor) - - # Single element should have probability 1.0 - assert np.allclose(output.data, [1.0], atol=1e-6) - - -class TestActivationIntegration: - """Test integration between different activation functions.""" - - def test_activation_chaining(self): - """Test chaining different activation functions.""" - # Create activations - relu = ReLU() - sigmoid = Sigmoid() - tanh = Tanh() - - # Test chaining: input -> ReLU -> Sigmoid - input_tensor = MockTensor([-1.0, 0.0, 1.0, 2.0]) - - # Apply ReLU first - relu_output = relu(input_tensor) - expected_relu = [0.0, 0.0, 1.0, 2.0] - assert np.allclose(relu_output.data, expected_relu) - - # Apply Sigmoid to ReLU output - sigmoid_output = sigmoid(relu_output) - - # Should be valid sigmoid values - assert np.all(sigmoid_output.data > 0.0) - assert np.all(sigmoid_output.data < 1.0) - - # First two should be 0.5 (sigmoid of 0) - assert np.allclose(sigmoid_output.data[:2], [0.5, 0.5], atol=1e-6) - - def test_activation_consistency(self): - """Test that activations are consistent across calls.""" - activations = [ReLU(), Sigmoid(), Tanh(), Softmax()] - - input_tensor = MockTensor([1.0, 2.0, 3.0]) - - for activation in activations: - # Apply activation twice - output1 = activation(input_tensor) - output2 = activation(input_tensor) - - # Should get identical results - assert np.allclose(output1.data, output2.data, atol=1e-10) - - def test_activation_shapes(self): - """Test that all activations preserve input shapes.""" - activations = [ReLU(), Sigmoid(), Tanh(), Softmax()] - - test_shapes = [ - [5], # 1D - [3, 4], # 2D - [2, 3, 4], # 3D - ] - - for shape in test_shapes: - input_data = np.random.randn(*shape) - input_tensor = MockTensor(input_data) - - for activation in activations: - output = activation(input_tensor) - assert output.shape == input_tensor.shape, f"Shape mismatch for {activation.__class__.__name__}" - - -class TestActivationEdgeCases: - """Test edge cases and error conditions for activation functions.""" - - def test_empty_input(self): - """Test activations with empty input.""" - activations = [ReLU(), Sigmoid(), Tanh(), Softmax()] - - empty_tensor = MockTensor([]) - - for activation in activations: - output = activation(empty_tensor) - assert output.shape == (0,), f"Empty input failed for {activation.__class__.__name__}" - - def test_very_small_values(self): - """Test activations with very small values.""" - activations = [ReLU(), Sigmoid(), Tanh()] # Exclude Softmax for this test - - small_tensor = MockTensor([1e-10, -1e-10, 1e-15]) - - for activation in activations: - output = activation(small_tensor) - # Should not crash and should produce finite values - assert np.all(np.isfinite(output.data)), f"Small values failed for {activation.__class__.__name__}" - - def test_inf_and_nan_handling(self): - """Test activation behavior with inf and nan values.""" - activations = [ReLU(), Sigmoid(), Tanh(), Softmax()] - - # Test with inf values - inf_tensor = MockTensor([np.inf, -np.inf, 0.0]) - - for activation in activations: - try: - output = activation(inf_tensor) - # Should either handle gracefully or raise appropriate exception - # At minimum, should not crash the program - assert output is not None - except (ValueError, RuntimeWarning): - # Acceptable to raise warnings/errors for inf values - pass - - -if __name__ == "__main__": - # Run tests if executed directly - pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_cnn.py b/tests/test_cnn.py deleted file mode 100644 index 53766d61..00000000 --- a/tests/test_cnn.py +++ /dev/null @@ -1,467 +0,0 @@ -""" -Mock-based module tests for CNN module. - -This test file uses simple mocks to avoid cross-module dependencies while thoroughly -testing the CNN module functionality. The MockTensor class provides a minimal -interface that matches expected behavior without requiring actual implementations. - -Test Philosophy: -- Use simple, visible mocks instead of complex mocking frameworks -- Test interface contracts and behavior, not implementation details -- Avoid dependency cascade where CNN tests fail due to tensor bugs -- Focus on convolution operations, Conv2D layers, and flatten functionality -- Ensure educational value with clear test structure -""" - -import pytest -import numpy as np -import sys -import os - -# Add the module source directory to the path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '05_cnn')) - -from cnn_dev import Conv2D, conv2d_naive, flatten - - -class MockTensor: - """ - Simple mock tensor for testing CNN operations without tensor dependencies. - - This mock provides just enough functionality to test CNN operations - without requiring the full Tensor implementation. - """ - - def __init__(self, data): - """Initialize with numpy array data.""" - if isinstance(data, (list, tuple)): - self.data = np.array(data, dtype=np.float32) - elif isinstance(data, np.ndarray): - self.data = data.astype(np.float32) - else: - self.data = np.array([data], dtype=np.float32) - - @property - def shape(self): - """Return shape of the underlying data.""" - return self.data.shape - - def __repr__(self): - return f"MockTensor({self.data})" - - def __eq__(self, other): - """Check equality with another MockTensor.""" - if isinstance(other, MockTensor): - return np.allclose(self.data, other.data) - return False - - -class TestConv2DNaive: - """Test conv2d_naive function with numpy arrays.""" - - def test_conv2d_naive_basic(self): - """Test basic convolution operation.""" - # Simple 3x3 input with 2x2 kernel - input_array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]], dtype=np.float32) - kernel_array = np.array([[1, 0], [0, 1]], dtype=np.float32) - - result = conv2d_naive(input_array, kernel_array) - expected = np.array([[6, 8], [12, 14]], dtype=np.float32) - - assert np.allclose(result, expected) - assert result.shape == (2, 2) - - def test_conv2d_naive_edge_detection(self): - """Test convolution with edge detection kernel.""" - # Create a simple edge pattern - input_array = np.array([[0, 0, 1], [0, 0, 1], [0, 0, 1]], dtype=np.float32) - edge_kernel = np.array([[-1, 1], [-1, 1]], dtype=np.float32) - - result = conv2d_naive(input_array, edge_kernel) - - # Should detect vertical edge - assert result.shape == (2, 2) - assert result[0, 0] == 0 # No edge at left - assert result[0, 1] > 0 # Edge detected at right - - def test_conv2d_naive_different_sizes(self): - """Test convolution with different kernel sizes.""" - # Test with 5x5 input and 3x3 kernel - input_5x5 = np.random.randn(5, 5).astype(np.float32) - kernel_3x3 = np.random.randn(3, 3).astype(np.float32) - - result = conv2d_naive(input_5x5, kernel_3x3) - expected_shape = (3, 3) # 5-3+1 = 3 - - assert result.shape == expected_shape - assert result.dtype == np.float32 - - def test_conv2d_naive_identity_kernel(self): - """Test convolution with identity-like kernel.""" - input_array = np.array([[1, 2], [3, 4]], dtype=np.float32) - identity_kernel = np.array([[1]], dtype=np.float32) - - result = conv2d_naive(input_array, identity_kernel) - expected = np.array([[1, 2], [3, 4]], dtype=np.float32) - - assert np.allclose(result, expected) - - def test_conv2d_naive_zero_kernel(self): - """Test convolution with zero kernel.""" - input_array = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) - zero_kernel = np.array([[0, 0], [0, 0]], dtype=np.float32) - - result = conv2d_naive(input_array, zero_kernel) - expected = np.zeros((1, 2), dtype=np.float32) - - assert np.allclose(result, expected) - - def test_conv2d_naive_large_kernel(self): - """Test convolution with large kernel.""" - input_array = np.ones((10, 10), dtype=np.float32) - large_kernel = np.ones((5, 5), dtype=np.float32) - - result = conv2d_naive(input_array, large_kernel) - expected_shape = (6, 6) # 10-5+1 = 6 - - assert result.shape == expected_shape - # All ones input with all ones kernel should give 25 (5*5) everywhere - assert np.allclose(result, 25.0) - - -class TestConv2DLayer: - """Test Conv2D layer class with mock tensors.""" - - def test_conv2d_layer_initialization(self): - """Test Conv2D layer initialization.""" - layer = Conv2D(kernel_size=(3, 3)) - - assert layer.kernel_size == (3, 3) - assert layer.kernel.shape == (3, 3) - assert layer.kernel.dtype == np.float32 - assert not np.allclose(layer.kernel, 0) # Should be randomly initialized - - def test_conv2d_layer_different_sizes(self): - """Test Conv2D layer with different kernel sizes.""" - sizes = [(2, 2), (3, 3), (5, 5), (1, 1)] - - for size in sizes: - layer = Conv2D(kernel_size=size) - assert layer.kernel_size == size - assert layer.kernel.shape == size - - def test_conv2d_layer_forward_pass(self): - """Test Conv2D layer forward pass.""" - layer = Conv2D(kernel_size=(2, 2)) - - # Test with 3x3 input - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - output = layer(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (2, 2) # 3-2+1 = 2 - - def test_conv2d_layer_batch_processing(self): - """Test Conv2D layer with batch input.""" - layer = Conv2D(kernel_size=(2, 2)) - - # Note: Current implementation might not support batches - # This test checks if it handles single images correctly - input_tensor = MockTensor(np.random.randn(5, 5)) - output = layer(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (4, 4) # 5-2+1 = 4 - - def test_conv2d_layer_kernel_consistency(self): - """Test that Conv2D layer uses consistent kernel.""" - layer = Conv2D(kernel_size=(2, 2)) - - # Store original kernel - original_kernel = layer.kernel.copy() - - # Forward pass shouldn't change kernel - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - output = layer(input_tensor) - - assert np.allclose(layer.kernel, original_kernel) - - def test_conv2d_layer_different_inputs(self): - """Test Conv2D layer with different input sizes.""" - layer = Conv2D(kernel_size=(3, 3)) - - input_sizes = [(5, 5), (8, 8), (10, 10)] - - for h, w in input_sizes: - input_tensor = MockTensor(np.random.randn(h, w)) - output = layer(input_tensor) - - expected_h, expected_w = h - 3 + 1, w - 3 + 1 - assert output.shape == (expected_h, expected_w) - - def test_conv2d_layer_callable(self): - """Test that Conv2D layer is callable.""" - layer = Conv2D(kernel_size=(2, 2)) - - # Should be callable both ways - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - - output1 = layer(input_tensor) - output2 = layer.forward(input_tensor) - - # Both should work and produce same output - assert isinstance(output1, MockTensor) - assert isinstance(output2, MockTensor) - assert output1.shape == output2.shape - - -class TestFlattenFunction: - """Test flatten function with mock tensors.""" - - def test_flatten_2d_tensor(self): - """Test flattening 2D tensor.""" - input_tensor = MockTensor([[1, 2], [3, 4]]) - flattened = flatten(input_tensor) - - assert isinstance(flattened, MockTensor) - assert flattened.shape == (1, 4) - expected = np.array([[1, 2, 3, 4]], dtype=np.float32) - assert np.allclose(flattened.data, expected) - - def test_flatten_3x3_tensor(self): - """Test flattening 3x3 tensor.""" - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - flattened = flatten(input_tensor) - - assert flattened.shape == (1, 9) - expected = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9]], dtype=np.float32) - assert np.allclose(flattened.data, expected) - - def test_flatten_different_shapes(self): - """Test flatten with different tensor shapes.""" - test_shapes = [ - (2, 3), # 2x3 -> (1, 6) - (4, 4), # 4x4 -> (1, 16) - (1, 5), # 1x5 -> (1, 5) - (6, 1), # 6x1 -> (1, 6) - ] - - for h, w in test_shapes: - input_data = np.random.randn(h, w) - input_tensor = MockTensor(input_data) - flattened = flatten(input_tensor) - - assert flattened.shape == (1, h * w) - assert np.allclose(flattened.data.flatten(), input_data.flatten()) - - def test_flatten_preserves_order(self): - """Test that flatten preserves row-major order.""" - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6]]) - flattened = flatten(input_tensor) - - expected = np.array([[1, 2, 3, 4, 5, 6]], dtype=np.float32) - assert np.allclose(flattened.data, expected) - - def test_flatten_single_element(self): - """Test flatten with single element tensor.""" - input_tensor = MockTensor([[5]]) - flattened = flatten(input_tensor) - - assert flattened.shape == (1, 1) - assert np.allclose(flattened.data, [[5]]) - - def test_flatten_batch_dimension(self): - """Test that flatten adds batch dimension correctly.""" - input_tensor = MockTensor([[1, 2], [3, 4]]) - flattened = flatten(input_tensor) - - # Should have batch dimension of 1 - assert flattened.shape[0] == 1 - assert len(flattened.shape) == 2 - - -class TestCNNIntegration: - """Test integration between CNN components.""" - - def test_conv2d_to_flatten_pipeline(self): - """Test pipeline from Conv2D to flatten.""" - # Create Conv2D layer - conv_layer = Conv2D(kernel_size=(2, 2)) - - # Apply convolution - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - conv_output = conv_layer(input_tensor) - - # Flatten the result - flattened = flatten(conv_output) - - # Should have correct shapes - assert conv_output.shape == (2, 2) - assert flattened.shape == (1, 4) - - def test_multiple_conv2d_layers(self): - """Test multiple Conv2D layers in sequence.""" - # Create two Conv2D layers - conv1 = Conv2D(kernel_size=(2, 2)) - conv2 = Conv2D(kernel_size=(2, 2)) - - # Apply them in sequence - input_tensor = MockTensor(np.random.randn(5, 5)) - - # First convolution: 5x5 -> 4x4 - h1 = conv1(input_tensor) - assert h1.shape == (4, 4) - - # Second convolution: 4x4 -> 3x3 - h2 = conv2(h1) - assert h2.shape == (3, 3) - - def test_conv2d_with_different_kernels(self): - """Test Conv2D layers with different kernel sizes.""" - input_tensor = MockTensor(np.random.randn(10, 10)) - - # Test different kernel sizes - kernel_sizes = [(3, 3), (5, 5), (7, 7)] - - for kernel_size in kernel_sizes: - conv_layer = Conv2D(kernel_size=kernel_size) - output = conv_layer(input_tensor) - - expected_h = 10 - kernel_size[0] + 1 - expected_w = 10 - kernel_size[1] + 1 - assert output.shape == (expected_h, expected_w) - - def test_cnn_feature_extraction_pipeline(self): - """Test complete CNN feature extraction pipeline.""" - # Simulate image classification pipeline - input_image = MockTensor(np.random.randn(8, 8)) - - # Feature extraction - conv1 = Conv2D(kernel_size=(3, 3)) # 8x8 -> 6x6 - features1 = conv1(input_image) - - conv2 = Conv2D(kernel_size=(2, 2)) # 6x6 -> 5x5 - features2 = conv2(features1) - - # Flatten for dense layer - flattened = flatten(features2) # 5x5 -> (1, 25) - - # Verify pipeline - assert features1.shape == (6, 6) - assert features2.shape == (5, 5) - assert flattened.shape == (1, 25) - - -class TestCNNEdgeCases: - """Test edge cases and error conditions for CNN operations.""" - - def test_conv2d_minimal_input(self): - """Test Conv2D with minimal input size.""" - # Test with input same size as kernel - layer = Conv2D(kernel_size=(2, 2)) - input_tensor = MockTensor([[1, 2], [3, 4]]) - - output = layer(input_tensor) - assert output.shape == (1, 1) # 2-2+1 = 1 - - def test_conv2d_large_kernel(self): - """Test Conv2D with large kernel relative to input.""" - layer = Conv2D(kernel_size=(3, 3)) - input_tensor = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - - output = layer(input_tensor) - assert output.shape == (1, 1) # 3-3+1 = 1 - - def test_flatten_edge_shapes(self): - """Test flatten with edge case shapes.""" - # Very wide tensor - wide_tensor = MockTensor([[1, 2, 3, 4, 5, 6, 7, 8]]) - flattened = flatten(wide_tensor) - assert flattened.shape == (1, 8) - - # Very tall tensor - tall_tensor = MockTensor([[1], [2], [3], [4]]) - flattened = flatten(tall_tensor) - assert flattened.shape == (1, 4) - - def test_conv2d_numerical_stability(self): - """Test Conv2D with extreme values.""" - layer = Conv2D(kernel_size=(2, 2)) - - # Test with very large values - large_input = MockTensor([[1000, 2000], [3000, 4000]]) - output = layer(large_input) - assert np.all(np.isfinite(output.data)) - - # Test with very small values - small_input = MockTensor([[1e-6, 2e-6], [3e-6, 4e-6]]) - output = layer(small_input) - assert np.all(np.isfinite(output.data)) - - def test_conv2d_zero_input(self): - """Test Conv2D with zero input.""" - layer = Conv2D(kernel_size=(2, 2)) - zero_input = MockTensor([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) - - output = layer(zero_input) - assert output.shape == (2, 2) - # Output should be finite (kernel is random, not zero) - assert np.all(np.isfinite(output.data)) - - -class TestCNNPerformance: - """Test CNN performance characteristics.""" - - def test_conv2d_consistency(self): - """Test Conv2D produces consistent results.""" - layer = Conv2D(kernel_size=(3, 3)) - input_tensor = MockTensor(np.random.randn(5, 5)) - - # Multiple forward passes should be consistent - output1 = layer(input_tensor) - output2 = layer(input_tensor) - - # Should be identical (deterministic) - assert np.allclose(output1.data, output2.data) - - def test_conv2d_different_instances(self): - """Test different Conv2D instances have different kernels.""" - layer1 = Conv2D(kernel_size=(3, 3)) - layer2 = Conv2D(kernel_size=(3, 3)) - - # Should have different random kernels - assert not np.allclose(layer1.kernel, layer2.kernel) - - def test_flatten_efficiency(self): - """Test flatten operation efficiency.""" - # Test with different sizes - sizes = [(5, 5), (10, 10), (20, 20)] - - for h, w in sizes: - input_tensor = MockTensor(np.random.randn(h, w)) - flattened = flatten(input_tensor) - - # Should preserve all data - assert flattened.shape == (1, h * w) - assert np.allclose(flattened.data.flatten(), input_tensor.data.flatten()) - - def test_conv2d_scalability(self): - """Test Conv2D with different scales.""" - kernel_sizes = [(2, 2), (3, 3), (5, 5)] - input_sizes = [(10, 10), (20, 20), (50, 50)] - - for kernel_size in kernel_sizes: - for input_size in input_sizes: - if input_size[0] >= kernel_size[0] and input_size[1] >= kernel_size[1]: - layer = Conv2D(kernel_size=kernel_size) - input_tensor = MockTensor(np.random.randn(*input_size)) - - output = layer(input_tensor) - expected_h = input_size[0] - kernel_size[0] + 1 - expected_w = input_size[1] - kernel_size[1] + 1 - assert output.shape == (expected_h, expected_w) - - -if __name__ == "__main__": - # Run tests if executed directly - pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_dataloader.py b/tests/test_dataloader.py deleted file mode 100644 index 946ae669..00000000 --- a/tests/test_dataloader.py +++ /dev/null @@ -1,577 +0,0 @@ -""" -Mock-based module tests for DataLoader module. - -This test file uses simple mocks to avoid cross-module dependencies while thoroughly -testing the DataLoader module functionality. The MockTensor class provides a minimal -interface that matches expected behavior without requiring actual implementations. - -Test Philosophy: -- Use simple, visible mocks instead of complex mocking frameworks -- Test interface contracts and behavior, not implementation details -- Avoid dependency cascade where dataloader tests fail due to tensor bugs -- Focus on Dataset interface, DataLoader functionality, and data pipeline patterns -- Ensure educational value with clear test structure -""" - -import pytest -import numpy as np -import sys -import os - -# Add the module source directory to the path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '06_dataloader')) - -from dataloader_dev import Dataset, DataLoader, SimpleDataset - - -class MockTensor: - """ - Simple mock tensor for testing dataloader operations without tensor dependencies. - - This mock provides just enough functionality to test data loading operations - without requiring the full Tensor implementation. - """ - - def __init__(self, data): - """Initialize with numpy array data.""" - if isinstance(data, (list, tuple)): - self.data = np.array(data, dtype=np.float32) - elif isinstance(data, np.ndarray): - self.data = data.astype(np.float32) - else: - self.data = np.array(data, dtype=np.float32) - - @property - def shape(self): - """Return shape of the underlying data.""" - return self.data.shape - - def __repr__(self): - return f"MockTensor({self.data})" - - def __eq__(self, other): - """Check equality with another MockTensor.""" - if isinstance(other, MockTensor): - return np.allclose(self.data, other.data) - return False - - -class MockDataset(Dataset): - """ - Simple mock dataset for testing without cross-module dependencies. - - This mock implements the Dataset interface with predictable, testable behavior. - """ - - def __init__(self, size=10, num_features=3, num_classes=2): - """Initialize mock dataset with synthetic data.""" - self.size = size - self.num_features = num_features - self.num_classes = num_classes - - # Generate predictable synthetic data - np.random.seed(42) # For reproducible tests - self.data = np.random.randn(size, num_features).astype(np.float32) - self.labels = np.random.randint(0, num_classes, size=size) - - def __getitem__(self, index): - """Get item by index.""" - if index < 0 or index >= self.size: - raise IndexError(f"Index {index} out of range for dataset of size {self.size}") - - data = MockTensor(self.data[index]) - label = MockTensor(self.labels[index]) - return data, label - - def __len__(self): - """Get dataset size.""" - return self.size - - def get_num_classes(self): - """Get number of classes.""" - return self.num_classes - - -class TestDatasetInterface: - """Test Dataset abstract base class and interface.""" - - def test_dataset_abstract_methods(self): - """Test that Dataset abstract methods raise NotImplementedError.""" - dataset = Dataset() - - with pytest.raises(NotImplementedError): - dataset[0] - - with pytest.raises(NotImplementedError): - len(dataset) - - with pytest.raises(NotImplementedError): - dataset.get_num_classes() - - def test_dataset_get_sample_shape(self): - """Test Dataset get_sample_shape method.""" - mock_dataset = MockDataset(size=5, num_features=4, num_classes=3) - - sample_shape = mock_dataset.get_sample_shape() - assert sample_shape == (4,) # Should match num_features - - def test_mock_dataset_basic_functionality(self): - """Test MockDataset basic functionality.""" - dataset = MockDataset(size=10, num_features=5, num_classes=3) - - # Test length - assert len(dataset) == 10 - - # Test get_num_classes - assert dataset.get_num_classes() == 3 - - # Test item access - data, label = dataset[0] - assert isinstance(data, MockTensor) - assert isinstance(label, MockTensor) - assert data.shape == (5,) - assert label.shape == () - - def test_mock_dataset_index_bounds(self): - """Test MockDataset index bounds checking.""" - dataset = MockDataset(size=5) - - # Valid indices should work - for i in range(5): - data, label = dataset[i] - assert isinstance(data, MockTensor) - assert isinstance(label, MockTensor) - - # Invalid indices should raise IndexError - with pytest.raises(IndexError): - dataset[5] - - with pytest.raises(IndexError): - dataset[-1] # Negative indices not supported - - def test_mock_dataset_consistency(self): - """Test MockDataset produces consistent results.""" - dataset = MockDataset(size=5, num_features=3, num_classes=2) - - # Multiple accesses should return same data - data1, label1 = dataset[0] - data2, label2 = dataset[0] - - assert np.allclose(data1.data, data2.data) - assert np.allclose(label1.data, label2.data) - - def test_mock_dataset_different_configurations(self): - """Test MockDataset with different configurations.""" - configs = [ - (5, 2, 2), # Small dataset - (100, 10, 5), # Medium dataset - (1000, 50, 10) # Large dataset - ] - - for size, num_features, num_classes in configs: - dataset = MockDataset(size=size, num_features=num_features, num_classes=num_classes) - - assert len(dataset) == size - assert dataset.get_num_classes() == num_classes - - data, label = dataset[0] - assert data.shape == (num_features,) - - -class TestDataLoaderBasic: - """Test DataLoader basic functionality.""" - - def test_dataloader_initialization(self): - """Test DataLoader initialization.""" - dataset = MockDataset(size=10) - dataloader = DataLoader(dataset, batch_size=4, shuffle=True) - - assert dataloader.dataset is dataset - assert dataloader.batch_size == 4 - assert dataloader.shuffle == True - - def test_dataloader_default_parameters(self): - """Test DataLoader with default parameters.""" - dataset = MockDataset(size=10) - dataloader = DataLoader(dataset) - - assert dataloader.batch_size == 32 # Default batch size - assert dataloader.shuffle == True # Default shuffle - - def test_dataloader_length_calculation(self): - """Test DataLoader length calculation (number of batches).""" - dataset = MockDataset(size=10) - - # Test different batch sizes - test_cases = [ - (10, 2, 5), # 10 samples, batch size 2 -> 5 batches - (10, 3, 4), # 10 samples, batch size 3 -> 4 batches (ceiling division) - (10, 5, 2), # 10 samples, batch size 5 -> 2 batches - (10, 10, 1), # 10 samples, batch size 10 -> 1 batch - (10, 15, 1), # 10 samples, batch size 15 -> 1 batch - ] - - for dataset_size, batch_size, expected_batches in test_cases: - dataset = MockDataset(size=dataset_size) - dataloader = DataLoader(dataset, batch_size=batch_size) - assert len(dataloader) == expected_batches - - def test_dataloader_iteration_basic(self): - """Test basic DataLoader iteration.""" - dataset = MockDataset(size=8, num_features=3, num_classes=2) - dataloader = DataLoader(dataset, batch_size=3, shuffle=False) - - batches = list(dataloader) - - # Should have 3 batches: [3, 3, 2] samples - assert len(batches) == 3 - - # Check batch shapes - batch_data, batch_labels = batches[0] - assert batch_data.shape == (3, 3) # 3 samples, 3 features each - assert batch_labels.shape == (3,) # 3 labels - - # Check last batch (partial) - batch_data, batch_labels = batches[2] - assert batch_data.shape == (2, 3) # 2 samples, 3 features each - assert batch_labels.shape == (2,) # 2 labels - - def test_dataloader_iteration_complete(self): - """Test that DataLoader iteration covers all samples.""" - dataset = MockDataset(size=10, num_features=4, num_classes=3) - dataloader = DataLoader(dataset, batch_size=3, shuffle=False) - - total_samples = 0 - all_batch_data = [] - all_batch_labels = [] - - for batch_data, batch_labels in dataloader: - batch_size = batch_data.shape[0] - total_samples += batch_size - - # Collect all data - all_batch_data.append(batch_data.data) - all_batch_labels.append(batch_labels.data) - - # Should process all samples - assert total_samples == 10 - - # Should have 4 batches: [3, 3, 3, 1] - assert len(all_batch_data) == 4 - assert all_batch_data[0].shape == (3, 4) - assert all_batch_data[1].shape == (3, 4) - assert all_batch_data[2].shape == (3, 4) - assert all_batch_data[3].shape == (1, 4) - - -class TestDataLoaderShuffling: - """Test DataLoader shuffling functionality.""" - - def test_dataloader_no_shuffle(self): - """Test DataLoader without shuffling.""" - dataset = MockDataset(size=6, num_features=2, num_classes=2) - dataloader = DataLoader(dataset, batch_size=2, shuffle=False) - - # Get first batch - batch_data, batch_labels = next(iter(dataloader)) - - # Should be samples 0 and 1 - expected_data_0, expected_label_0 = dataset[0] - expected_data_1, expected_label_1 = dataset[1] - - assert np.allclose(batch_data.data[0], expected_data_0.data) - assert np.allclose(batch_data.data[1], expected_data_1.data) - - def test_dataloader_with_shuffle(self): - """Test DataLoader with shuffling.""" - dataset = MockDataset(size=10, num_features=3, num_classes=2) - - # Create two dataloaders with different shuffle states - dataloader1 = DataLoader(dataset, batch_size=5, shuffle=True) - dataloader2 = DataLoader(dataset, batch_size=5, shuffle=True) - - # Get first batches - batch1 = next(iter(dataloader1)) - batch2 = next(iter(dataloader2)) - - # Should have same shapes - assert batch1[0].shape == batch2[0].shape - assert batch1[1].shape == batch2[1].shape - - # Note: Due to randomness, batches might be different - # This is a basic test that shuffling doesn't break functionality - - def test_dataloader_shuffle_consistency(self): - """Test that DataLoader shuffling is consistent within an epoch.""" - dataset = MockDataset(size=8, num_features=2, num_classes=2) - dataloader = DataLoader(dataset, batch_size=4, shuffle=True) - - # Collect all data from one epoch - epoch_data = [] - epoch_labels = [] - - for batch_data, batch_labels in dataloader: - epoch_data.append(batch_data.data) - epoch_labels.append(batch_labels.data) - - # Should have processed all samples - total_samples = sum(data.shape[0] for data in epoch_data) - assert total_samples == 8 - - # All data should be accounted for - assert len(epoch_data) == 2 # 8 samples / 4 batch_size = 2 batches - - -class TestDataLoaderEdgeCases: - """Test DataLoader edge cases and error conditions.""" - - def test_dataloader_empty_dataset(self): - """Test DataLoader with empty dataset.""" - dataset = MockDataset(size=0) - dataloader = DataLoader(dataset, batch_size=4) - - # Should have 0 batches - assert len(dataloader) == 0 - - # Iteration should produce no batches - batches = list(dataloader) - assert len(batches) == 0 - - def test_dataloader_single_sample(self): - """Test DataLoader with single sample.""" - dataset = MockDataset(size=1, num_features=3, num_classes=2) - dataloader = DataLoader(dataset, batch_size=4) - - # Should have 1 batch - assert len(dataloader) == 1 - - # Single batch should contain the one sample - batch_data, batch_labels = next(iter(dataloader)) - assert batch_data.shape == (1, 3) - assert batch_labels.shape == (1,) - - def test_dataloader_batch_size_larger_than_dataset(self): - """Test DataLoader with batch size larger than dataset.""" - dataset = MockDataset(size=5, num_features=4, num_classes=3) - dataloader = DataLoader(dataset, batch_size=10) - - # Should have 1 batch - assert len(dataloader) == 1 - - # Batch should contain all samples - batch_data, batch_labels = next(iter(dataloader)) - assert batch_data.shape == (5, 4) - assert batch_labels.shape == (5,) - - def test_dataloader_batch_size_one(self): - """Test DataLoader with batch size of 1.""" - dataset = MockDataset(size=5, num_features=2, num_classes=2) - dataloader = DataLoader(dataset, batch_size=1) - - # Should have 5 batches - assert len(dataloader) == 5 - - # Each batch should have 1 sample - for batch_data, batch_labels in dataloader: - assert batch_data.shape == (1, 2) - assert batch_labels.shape == (1,) - - def test_dataloader_multiple_epochs(self): - """Test DataLoader across multiple epochs.""" - dataset = MockDataset(size=6, num_features=3, num_classes=2) - dataloader = DataLoader(dataset, batch_size=2, shuffle=False) - - # Test 3 epochs - for epoch in range(3): - epoch_samples = 0 - batch_count = 0 - - for batch_data, batch_labels in dataloader: - batch_count += 1 - epoch_samples += batch_data.shape[0] - - # Each epoch should process all samples - assert epoch_samples == 6 - assert batch_count == 3 # 6 samples / 2 batch_size = 3 batches - - -class TestDataLoaderIntegration: - """Test DataLoader integration with different dataset types.""" - - def test_dataloader_with_simple_dataset(self): - """Test DataLoader with SimpleDataset.""" - # Note: This test assumes SimpleDataset exists and works - try: - dataset = SimpleDataset(size=20, num_features=5, num_classes=3) - dataloader = DataLoader(dataset, batch_size=4, shuffle=True) - - # Test basic functionality - assert len(dataloader) == 5 # 20 / 4 = 5 batches - - # Test iteration - total_samples = 0 - for batch_data, batch_labels in dataloader: - total_samples += batch_data.shape[0] - assert batch_data.shape[1] == 5 # num_features - - assert total_samples == 20 - - except (ImportError, NameError): - # SimpleDataset might not be available in all test environments - pytest.skip("SimpleDataset not available") - - def test_dataloader_with_custom_dataset(self): - """Test DataLoader with custom dataset implementation.""" - class CustomDataset(Dataset): - def __init__(self): - self.data = [(i, i % 2) for i in range(10)] - - def __getitem__(self, index): - value, label = self.data[index] - return MockTensor([value]), MockTensor([label]) - - def __len__(self): - return len(self.data) - - def get_num_classes(self): - return 2 - - dataset = CustomDataset() - dataloader = DataLoader(dataset, batch_size=3, shuffle=False) - - # Test that it works with custom dataset - batches = list(dataloader) - assert len(batches) == 4 # 10 / 3 = 4 batches (ceiling division) - - # Check first batch - batch_data, batch_labels = batches[0] - assert batch_data.shape == (3, 1) - assert batch_labels.shape == (3, 1) - - def test_dataloader_different_data_types(self): - """Test DataLoader with different data types.""" - class MultiTypeDataset(Dataset): - def __init__(self): - self.samples = [ - (np.array([1.0, 2.0]), 0), - (np.array([3.0, 4.0]), 1), - (np.array([5.0, 6.0]), 0), - (np.array([7.0, 8.0]), 1), - ] - - def __getitem__(self, index): - data, label = self.samples[index] - return MockTensor(data), MockTensor(label) - - def __len__(self): - return len(self.samples) - - def get_num_classes(self): - return 2 - - dataset = MultiTypeDataset() - dataloader = DataLoader(dataset, batch_size=2, shuffle=False) - - # Test batching different data types - batch_data, batch_labels = next(iter(dataloader)) - assert batch_data.shape == (2, 2) - assert batch_labels.shape == (2,) - - -class TestDataLoaderPerformance: - """Test DataLoader performance characteristics.""" - - def test_dataloader_functionality(self): - """Test DataLoader functionality with realistic datasets.""" - # Create reasonable dataset for educational context - dataset = MockDataset(size=100, num_features=10, num_classes=5) - dataloader = DataLoader(dataset, batch_size=16, shuffle=True) - - # Should iterate correctly - batch_count = 0 - for batch_data, batch_labels in dataloader: - batch_count += 1 - assert batch_data.shape[1] == 10 - assert batch_labels.shape[0] <= 16 - - # Process first few batches - if batch_count >= 3: - break - - assert batch_count == 3 - - def test_dataloader_iteration_completeness(self): - """Test DataLoader processes all samples correctly.""" - dataset = MockDataset(size=100, num_features=10, num_classes=5) - dataloader = DataLoader(dataset, batch_size=10, shuffle=False) - - # Should process all samples - total_samples = 0 - for batch_data, batch_labels in dataloader: - total_samples += batch_data.shape[0] - - # Should process all samples - assert total_samples == 100 - - def test_dataloader_different_scales(self): - """Test DataLoader with different reasonable scales.""" - sizes = [10, 50, 100] - batch_sizes = [1, 8, 16] - - for size in sizes: - for batch_size in batch_sizes: - dataset = MockDataset(size=size, num_features=5, num_classes=3) - dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True) - - # Should handle different scales - expected_batches = (size + batch_size - 1) // batch_size - assert len(dataloader) == expected_batches - - # Should iterate correctly - total_samples = 0 - for batch_data, batch_labels in dataloader: - total_samples += batch_data.shape[0] - - assert total_samples == size - - -class TestDataLoaderRobustness: - """Test DataLoader robustness and error handling.""" - - def test_dataloader_with_invalid_batch_size(self): - """Test DataLoader with invalid batch sizes.""" - dataset = MockDataset(size=10) - - # Zero batch size should raise error - with pytest.raises((ValueError, AssertionError)): - DataLoader(dataset, batch_size=0) - - # Negative batch size should raise error - with pytest.raises((ValueError, AssertionError)): - DataLoader(dataset, batch_size=-1) - - def test_dataloader_with_none_dataset(self): - """Test DataLoader with None dataset.""" - with pytest.raises((TypeError, AttributeError)): - DataLoader(None, batch_size=4) - - def test_dataloader_iteration_consistency(self): - """Test DataLoader iteration consistency.""" - dataset = MockDataset(size=12, num_features=3, num_classes=2) - dataloader = DataLoader(dataset, batch_size=5, shuffle=False) - - # Multiple iterations should be consistent - batches1 = list(dataloader) - batches2 = list(dataloader) - - assert len(batches1) == len(batches2) - - # Without shuffle, should be identical - for (batch1_data, batch1_labels), (batch2_data, batch2_labels) in zip(batches1, batches2): - assert np.allclose(batch1_data.data, batch2_data.data) - assert np.allclose(batch1_labels.data, batch2_labels.data) - - -if __name__ == "__main__": - # Run tests if executed directly - pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_layers.py b/tests/test_layers.py deleted file mode 100644 index a97acb91..00000000 --- a/tests/test_layers.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -Comprehensive Layers Module Tests - -Tests Dense layer functionality using simple mock objects. -Used for instructor grading and comprehensive validation. - -This file demonstrates the mock-based testing approach where we use -simple, visible mocks instead of depending on other TinyTorch modules. -""" - -import numpy as np -import pytest - - -# Simple Mock Objects - Visible and Educational -class MockTensor: - """ - Simple mock tensor for testing layers. - - Shows exactly what interface the Dense layer expects: - - .data (numpy array): The actual numerical data - - .shape (tuple): Dimensions of the data - """ - def __init__(self, data): - self.data = np.array(data, dtype=np.float32) - self.shape = self.data.shape - - def __repr__(self): - return f"MockTensor(shape={self.shape})" - - -# Import the student's implementation -# Note: In a real setup, this would import from the student's module -try: - from modules.source.layers.layers_dev import Dense, matmul_naive -except ImportError: - # Fallback for different import paths - try: - from tinytorch.core.layers import Dense, matmul_naive - except ImportError: - # Skip tests if module not found - pytest.skip("Layers module not found", allow_module_level=True) - - -class TestMatrixMultiplication: - """Comprehensive tests for matrix multiplication implementation.""" - - def test_basic_2x2_multiplication(self): - """Test basic 2x2 matrix multiplication.""" - A = np.array([[1, 2], [3, 4]], dtype=np.float32) - B = np.array([[5, 6], [7, 8]], dtype=np.float32) - - result = matmul_naive(A, B) - expected = np.array([[19, 22], [43, 50]], dtype=np.float32) - - np.testing.assert_array_almost_equal(result, expected) - assert result.shape == (2, 2) - - def test_different_shapes(self): - """Test matrix multiplication with different shapes.""" - # Test 1x3 × 3x1 = 1x1 - A = np.array([[1, 2, 3]], dtype=np.float32) - B = np.array([[4], [5], [6]], dtype=np.float32) - - result = matmul_naive(A, B) - expected = np.array([[32]], dtype=np.float32) # 1*4 + 2*5 + 3*6 = 32 - - np.testing.assert_array_almost_equal(result, expected) - assert result.shape == (1, 1) - - # Test 3x2 × 2x4 = 3x4 - A2 = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float32) - B2 = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.float32) - - result2 = matmul_naive(A2, B2) - expected2 = A2 @ B2 # Use NumPy for verification - - np.testing.assert_array_almost_equal(result2, expected2) - assert result2.shape == (3, 4) - - def test_edge_cases(self): - """Test matrix multiplication edge cases.""" - # Test with zeros - A_zero = np.zeros((2, 3), dtype=np.float32) - B_zero = np.zeros((3, 2), dtype=np.float32) - result_zero = matmul_naive(A_zero, B_zero) - expected_zero = np.zeros((2, 2), dtype=np.float32) - - np.testing.assert_array_almost_equal(result_zero, expected_zero) - - # Test with identity - A_id = np.array([[1, 2]], dtype=np.float32) - B_id = np.array([[1, 0], [0, 1]], dtype=np.float32) - result_id = matmul_naive(A_id, B_id) - expected_id = np.array([[1, 2]], dtype=np.float32) - - np.testing.assert_array_almost_equal(result_id, expected_id) - - def test_comparison_with_numpy(self): - """Test that our implementation matches NumPy.""" - # Random test cases - np.random.seed(42) - - for _ in range(5): - m, n, k = np.random.randint(1, 10, 3) - A = np.random.randn(m, n).astype(np.float32) - B = np.random.randn(n, k).astype(np.float32) - - our_result = matmul_naive(A, B) - numpy_result = A @ B - - np.testing.assert_array_almost_equal(our_result, numpy_result, decimal=5) - - -class TestDenseLayerInitialization: - """Test Dense layer initialization and parameter setup.""" - - def test_initialization_with_bias(self): - """Test Dense layer initialization with bias.""" - layer = Dense(input_size=3, output_size=2, use_bias=True) - - # Check shapes - assert layer.weights.shape == (3, 2) - assert layer.bias is not None - assert layer.bias.shape == (2,) - - # Check initialization - assert not np.allclose(layer.weights, 0), "Weights should not be all zeros" - assert np.allclose(layer.bias, 0), "Bias should be initialized to zeros" - - def test_initialization_without_bias(self): - """Test Dense layer initialization without bias.""" - layer = Dense(input_size=4, output_size=3, use_bias=False) - - # Check shapes - assert layer.weights.shape == (4, 3) - assert layer.bias is None - - # Check weight initialization - assert not np.allclose(layer.weights, 0), "Weights should not be all zeros" - - def test_different_sizes(self): - """Test Dense layer with different input/output sizes.""" - test_configs = [ - (1, 1), - (10, 5), - (100, 50), - (784, 128) # MNIST-like - ] - - for input_size, output_size in test_configs: - layer = Dense(input_size=input_size, output_size=output_size) - - assert layer.weights.shape == (input_size, output_size) - if layer.bias is not None: - assert layer.bias.shape == (output_size,) - - -class TestDenseLayerForward: - """Test Dense layer forward pass computation.""" - - def test_single_sample_forward(self): - """Test forward pass with single sample.""" - layer = Dense(input_size=3, output_size=2, use_bias=True) - - # Use mock tensor - x = MockTensor([[1, 2, 3]]) - y = layer(x) - - # Check output shape - assert y.shape == (1, 2) - - # Verify computation manually - expected = np.dot(x.data, layer.weights) + layer.bias - np.testing.assert_array_almost_equal(y.data, expected) - - def test_batch_forward(self): - """Test forward pass with batch of samples.""" - layer = Dense(input_size=3, output_size=2, use_bias=True) - - # Use mock tensor with batch - x = MockTensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - y = layer(x) - - # Check output shape - assert y.shape == (3, 2) - - # Verify computation manually - expected = np.dot(x.data, layer.weights) + layer.bias - np.testing.assert_array_almost_equal(y.data, expected) - - def test_forward_without_bias(self): - """Test forward pass without bias.""" - layer = Dense(input_size=2, output_size=3, use_bias=False) - - x = MockTensor([[1, 2]]) - y = layer(x) - - # Check output shape - assert y.shape == (1, 3) - - # Verify computation (should be just matrix multiplication) - expected = np.dot(x.data, layer.weights) - np.testing.assert_array_almost_equal(y.data, expected) - - def test_naive_vs_optimized_matmul(self): - """Test that naive and optimized matrix multiplication give same results.""" - layer_naive = Dense(input_size=2, output_size=2, use_naive_matmul=True) - layer_optimized = Dense(input_size=2, output_size=2, use_naive_matmul=False) - - # Set same weights for comparison - layer_optimized.weights = layer_naive.weights.copy() - if layer_naive.bias is not None: - layer_optimized.bias = layer_naive.bias.copy() - - x = MockTensor([[1, 2]]) - y_naive = layer_naive(x) - y_optimized = layer_optimized(x) - - # Both should give same results - np.testing.assert_array_almost_equal(y_naive.data, y_optimized.data) - - -class TestDenseLayerEdgeCases: - """Test Dense layer edge cases and robustness.""" - - def test_zero_input(self): - """Test layer with zero input.""" - layer = Dense(input_size=3, output_size=2, use_bias=True) - - x = MockTensor([[0, 0, 0]]) - y = layer(x) - - # Output should be just the bias - expected = layer.bias - np.testing.assert_array_almost_equal(y.data.flatten(), expected) - - def test_large_values(self): - """Test layer with large input values.""" - layer = Dense(input_size=2, output_size=2) - - x = MockTensor([[1000, -1000]]) - y = layer(x) - - # Should not produce NaN or Inf - assert not np.any(np.isnan(y.data)) - assert not np.any(np.isinf(y.data)) - assert y.shape == (1, 2) - - def test_negative_values(self): - """Test layer with negative input values.""" - layer = Dense(input_size=3, output_size=2) - - x = MockTensor([[-1, -2, -3]]) - y = layer(x) - - # Should handle negative values correctly - assert y.shape == (1, 2) - assert not np.any(np.isnan(y.data)) - - def test_single_neuron(self): - """Test layer with single input and output neuron.""" - layer = Dense(input_size=1, output_size=1) - - x = MockTensor([[5]]) - y = layer(x) - - assert y.shape == (1, 1) - - # Manual verification - expected = x.data * layer.weights + (layer.bias if layer.bias is not None else 0) - np.testing.assert_array_almost_equal(y.data, expected) - - -class TestDenseLayerIntegration: - """Test Dense layer integration scenarios.""" - - def test_layer_chaining(self): - """Test chaining multiple Dense layers.""" - layer1 = Dense(input_size=4, output_size=3) - layer2 = Dense(input_size=3, output_size=2) - layer3 = Dense(input_size=2, output_size=1) - - x = MockTensor([[1, 2, 3, 4]]) - - # Chain layers - h1 = layer1(x) - h2 = layer2(h1) - h3 = layer3(h2) - - # Check shapes - assert h1.shape == (1, 3) - assert h2.shape == (1, 2) - assert h3.shape == (1, 1) - - def test_parameter_counting(self): - """Test parameter counting for different layer configurations.""" - # Layer with bias - layer_bias = Dense(input_size=10, output_size=5, use_bias=True) - expected_params_bias = 10 * 5 + 5 # weights + bias - actual_params_bias = layer_bias.weights.size + (layer_bias.bias.size if layer_bias.bias is not None else 0) - assert actual_params_bias == expected_params_bias - - # Layer without bias - layer_no_bias = Dense(input_size=10, output_size=5, use_bias=False) - expected_params_no_bias = 10 * 5 # only weights - actual_params_no_bias = layer_no_bias.weights.size - assert actual_params_no_bias == expected_params_no_bias - - def test_batch_consistency(self): - """Test that batch processing is consistent with single sample processing.""" - layer = Dense(input_size=3, output_size=2) - - # Single samples - x1 = MockTensor([[1, 2, 3]]) - x2 = MockTensor([[4, 5, 6]]) - - y1 = layer(x1) - y2 = layer(x2) - - # Batch processing - x_batch = MockTensor([[1, 2, 3], [4, 5, 6]]) - y_batch = layer(x_batch) - - # Results should be consistent - np.testing.assert_array_almost_equal(y_batch.data[0], y1.data[0]) - np.testing.assert_array_almost_equal(y_batch.data[1], y2.data[0]) - - -class TestDenseLayerPerformance: - """Test Dense layer performance characteristics.""" - - def test_batch_processing(self): - """Test layer with reasonable batch sizes.""" - layer = Dense(input_size=10, output_size=5) - - # Realistic batch size for educational context - batch_size = 32 - x = MockTensor(np.random.randn(batch_size, 10)) - y = layer(x) - - assert y.shape == (batch_size, 5) - assert not np.any(np.isnan(y.data)) - assert not np.any(np.isinf(y.data)) - - def test_memory_efficiency(self): - """Test that layer doesn't create unnecessary copies.""" - layer = Dense(input_size=5, output_size=3) - - x = MockTensor([[1, 2, 3, 4, 5]]) - original_weights = layer.weights.copy() - original_bias = layer.bias.copy() if layer.bias is not None else None - - # Forward pass shouldn't modify weights - y = layer(x) - - np.testing.assert_array_equal(layer.weights, original_weights) - if original_bias is not None: - np.testing.assert_array_equal(layer.bias, original_bias) - - -# Test runner for command line execution -if __name__ == "__main__": - pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_networks.py b/tests/test_networks.py deleted file mode 100644 index 9d7b35e3..00000000 --- a/tests/test_networks.py +++ /dev/null @@ -1,552 +0,0 @@ -""" -Mock-based module tests for Networks module. - -This test file uses simple mocks to avoid cross-module dependencies while thoroughly -testing the Networks module functionality. The MockTensor and MockLayer classes provide -minimal interfaces that match expected behavior without requiring actual implementations. - -Test Philosophy: -- Use simple, visible mocks instead of complex mocking frameworks -- Test interface contracts and behavior, not implementation details -- Avoid dependency cascade where networks tests fail due to layer/tensor bugs -- Focus on the Sequential network architecture and MLP functionality -- Ensure educational value with clear test structure -""" - -import pytest -import numpy as np -import sys -import os - -# Add the module source directory to the path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'modules', 'source', '04_networks')) - -from networks_dev import Sequential, MLP - - -class MockTensor: - """ - Simple mock tensor for testing networks without tensor dependencies. - - This mock provides just enough functionality to test network architectures - without requiring the full Tensor implementation. - """ - - def __init__(self, data): - """Initialize with numpy array data.""" - if isinstance(data, (list, tuple)): - self.data = np.array(data, dtype=np.float32) - elif isinstance(data, np.ndarray): - self.data = data.astype(np.float32) - else: - self.data = np.array([data], dtype=np.float32) - - @property - def shape(self): - """Return shape of the underlying data.""" - return self.data.shape - - def __repr__(self): - return f"MockTensor({self.data})" - - def __eq__(self, other): - """Check equality with another MockTensor.""" - if isinstance(other, MockTensor): - return np.allclose(self.data, other.data) - return False - - -class MockLayer: - """ - Simple mock layer for testing networks without layer dependencies. - - This mock simulates a layer that transforms input dimensions in a predictable way. - """ - - def __init__(self, input_size, output_size, name="MockLayer"): - """Initialize mock layer with input/output sizes.""" - self.input_size = input_size - self.output_size = output_size - self.name = name - self.call_count = 0 - - def forward(self, x): - """Mock forward pass that transforms input shape.""" - self.call_count += 1 - - # Simulate layer computation: transform input to output size - if hasattr(x, 'data'): - input_data = x.data - else: - input_data = x - - # Create output with correct shape - if len(input_data.shape) == 1: - # 1D input -> 1D output - output_data = np.random.randn(self.output_size).astype(np.float32) - else: - # 2D input (batch) -> 2D output - batch_size = input_data.shape[0] - output_data = np.random.randn(batch_size, self.output_size).astype(np.float32) - - return MockTensor(output_data) - - def __call__(self, x): - """Make layer callable.""" - return self.forward(x) - - def __repr__(self): - return f"{self.name}({self.input_size} -> {self.output_size})" - - -class MockActivation: - """ - Simple mock activation function for testing networks. - """ - - def __init__(self, name="MockActivation"): - """Initialize mock activation.""" - self.name = name - self.call_count = 0 - - def forward(self, x): - """Mock activation that preserves input shape.""" - self.call_count += 1 - - # Simple activation: just add small noise to simulate processing - if hasattr(x, 'data'): - input_data = x.data - else: - input_data = x - - # Preserve shape, add small transformation - output_data = input_data + 0.01 * np.random.randn(*input_data.shape).astype(np.float32) - return MockTensor(output_data) - - def __call__(self, x): - """Make activation callable.""" - return self.forward(x) - - def __repr__(self): - return f"{self.name}()" - - -class TestSequentialNetwork: - """Test Sequential network architecture with mock layers.""" - - def test_sequential_initialization_empty(self): - """Test Sequential can be initialized without layers.""" - seq = Sequential() - assert seq is not None - assert hasattr(seq, 'layers') - assert len(seq.layers) == 0 - - def test_sequential_initialization_with_layers(self): - """Test Sequential can be initialized with layers.""" - layer1 = MockLayer(10, 5, "Layer1") - layer2 = MockLayer(5, 2, "Layer2") - - seq = Sequential([layer1, layer2]) - assert len(seq.layers) == 2 - assert seq.layers[0] is layer1 - assert seq.layers[1] is layer2 - - def test_sequential_add_layer(self): - """Test adding layers to Sequential network.""" - seq = Sequential() - - layer1 = MockLayer(10, 5, "Layer1") - layer2 = MockLayer(5, 2, "Layer2") - - seq.add(layer1) - assert len(seq.layers) == 1 - assert seq.layers[0] is layer1 - - seq.add(layer2) - assert len(seq.layers) == 2 - assert seq.layers[1] is layer2 - - def test_sequential_forward_single_layer(self): - """Test Sequential forward pass with single layer.""" - layer = MockLayer(5, 3, "TestLayer") - seq = Sequential([layer]) - - input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0, 5.0]) - output = seq(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (3,) # Should match layer output size - assert layer.call_count == 1 # Layer should be called once - - def test_sequential_forward_multiple_layers(self): - """Test Sequential forward pass with multiple layers.""" - layer1 = MockLayer(4, 6, "Layer1") - layer2 = MockLayer(6, 3, "Layer2") - layer3 = MockLayer(3, 2, "Layer3") - - seq = Sequential([layer1, layer2, layer3]) - - input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0]) - output = seq(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (2,) # Should match final layer output size - - # All layers should be called once - assert layer1.call_count == 1 - assert layer2.call_count == 1 - assert layer3.call_count == 1 - - def test_sequential_forward_with_activations(self): - """Test Sequential forward pass with layers and activations.""" - layer1 = MockLayer(3, 4, "Layer1") - activation1 = MockActivation("ReLU") - layer2 = MockLayer(4, 2, "Layer2") - activation2 = MockActivation("Sigmoid") - - seq = Sequential([layer1, activation1, layer2, activation2]) - - input_tensor = MockTensor([1.0, 2.0, 3.0]) - output = seq(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (2,) - - # All components should be called - assert layer1.call_count == 1 - assert activation1.call_count == 1 - assert layer2.call_count == 1 - assert activation2.call_count == 1 - - def test_sequential_batch_processing(self): - """Test Sequential with batch input.""" - layer1 = MockLayer(3, 5, "Layer1") - layer2 = MockLayer(5, 2, "Layer2") - - seq = Sequential([layer1, layer2]) - - # Batch input: 4 samples, 3 features each - batch_input = MockTensor(np.random.randn(4, 3)) - output = seq(batch_input) - - assert isinstance(output, MockTensor) - assert output.shape == (4, 2) # Batch size preserved, features transformed - - def test_sequential_empty_network(self): - """Test Sequential with no layers (identity function).""" - seq = Sequential() - - input_tensor = MockTensor([1.0, 2.0, 3.0]) - output = seq(input_tensor) - - # Should return input unchanged - assert isinstance(output, MockTensor) - assert np.allclose(output.data, input_tensor.data) - - def test_sequential_layer_order(self): - """Test that Sequential processes layers in correct order.""" - # Create layers that modify data in a traceable way - class TrackedLayer: - def __init__(self, multiplier, name): - self.multiplier = multiplier - self.name = name - self.call_count = 0 - - def forward(self, x): - self.call_count += 1 - return MockTensor(x.data * self.multiplier) - - def __call__(self, x): - return self.forward(x) - - layer1 = TrackedLayer(2.0, "Double") - layer2 = TrackedLayer(3.0, "Triple") - - seq = Sequential([layer1, layer2]) - - input_tensor = MockTensor([1.0]) - output = seq(input_tensor) - - # Should be: 1.0 * 2.0 * 3.0 = 6.0 - assert np.allclose(output.data, [6.0]) - assert layer1.call_count == 1 - assert layer2.call_count == 1 - - -class TestMLPNetwork: - """Test MLP (Multi-Layer Perceptron) network with mock components.""" - - def test_mlp_initialization_basic(self): - """Test MLP can be initialized with basic parameters.""" - mlp = MLP(input_size=10, hidden_size=20, output_size=5) - assert mlp is not None - assert hasattr(mlp, 'network') - assert isinstance(mlp.network, Sequential) - - def test_mlp_initialization_parameters(self): - """Test MLP stores initialization parameters.""" - mlp = MLP(input_size=8, hidden_size=16, output_size=3) - - # Should have stored parameters - assert mlp.input_size == 8 - assert mlp.hidden_size == 16 - assert mlp.output_size == 3 - - def test_mlp_network_structure(self): - """Test MLP creates correct network structure.""" - mlp = MLP(input_size=5, hidden_size=10, output_size=2) - - # Should have 3 layers: input->hidden, activation, hidden->output - assert len(mlp.network.layers) == 3 - - # Check layer types and sizes - hidden_layer = mlp.network.layers[0] - activation = mlp.network.layers[1] - output_layer = mlp.network.layers[2] - - # Verify layer properties (if available) - if hasattr(hidden_layer, 'input_size'): - assert hidden_layer.input_size == 5 - assert hidden_layer.output_size == 10 - - if hasattr(output_layer, 'input_size'): - assert output_layer.input_size == 10 - assert output_layer.output_size == 2 - - def test_mlp_forward_pass(self): - """Test MLP forward pass.""" - mlp = MLP(input_size=4, hidden_size=8, output_size=3) - - input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0]) - output = mlp(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (3,) # Should match output_size - - def test_mlp_batch_processing(self): - """Test MLP with batch input.""" - mlp = MLP(input_size=3, hidden_size=6, output_size=2) - - # Batch input: 5 samples, 3 features each - batch_input = MockTensor(np.random.randn(5, 3)) - output = mlp(batch_input) - - assert isinstance(output, MockTensor) - assert output.shape == (5, 2) # Batch size preserved - - def test_mlp_different_sizes(self): - """Test MLP with different size configurations.""" - configurations = [ - (2, 4, 1), # Small network - (10, 20, 5), # Medium network - (100, 50, 10) # Large network - ] - - for input_size, hidden_size, output_size in configurations: - mlp = MLP(input_size=input_size, hidden_size=hidden_size, output_size=output_size) - - # Test with appropriate input - input_tensor = MockTensor(np.random.randn(input_size)) - output = mlp(input_tensor) - - assert output.shape == (output_size,) - - def test_mlp_consistency(self): - """Test MLP produces consistent outputs for same input.""" - mlp = MLP(input_size=5, hidden_size=10, output_size=3) - - # Note: This test might be flaky with random mock layers - # In real implementation, should be deterministic - input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0, 5.0]) - - # Test that MLP can be called multiple times - output1 = mlp(input_tensor) - output2 = mlp(input_tensor) - - assert isinstance(output1, MockTensor) - assert isinstance(output2, MockTensor) - assert output1.shape == output2.shape - - -class TestNetworkIntegration: - """Test integration between Sequential and MLP networks.""" - - def test_sequential_as_mlp_component(self): - """Test using Sequential as a component in larger networks.""" - # Create a sub-network - sub_network = Sequential([ - MockLayer(5, 8, "SubLayer1"), - MockActivation("SubReLU"), - MockLayer(8, 3, "SubLayer2") - ]) - - # Use it in a larger Sequential - main_network = Sequential([ - MockLayer(10, 5, "MainLayer1"), - sub_network, - MockLayer(3, 2, "MainLayer2") - ]) - - input_tensor = MockTensor(np.random.randn(10)) - output = main_network(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (2,) - - def test_mlp_vs_sequential_equivalence(self): - """Test that MLP and equivalent Sequential produce similar structure.""" - # Create MLP - mlp = MLP(input_size=4, hidden_size=6, output_size=2) - - # Create equivalent Sequential - seq = Sequential([ - MockLayer(4, 6, "Hidden"), - MockActivation("ReLU"), - MockLayer(6, 2, "Output") - ]) - - # Both should have same number of layers - assert len(mlp.network.layers) == len(seq.layers) - - # Both should handle same input/output shapes - input_tensor = MockTensor([1.0, 2.0, 3.0, 4.0]) - - mlp_output = mlp(input_tensor) - seq_output = seq(input_tensor) - - assert mlp_output.shape == seq_output.shape - - def test_network_composition(self): - """Test composing multiple networks.""" - # Create encoder network - encoder = Sequential([ - MockLayer(10, 6, "Encoder1"), - MockActivation("ReLU"), - MockLayer(6, 3, "Encoder2") - ]) - - # Create decoder network - decoder = Sequential([ - MockLayer(3, 6, "Decoder1"), - MockActivation("ReLU"), - MockLayer(6, 10, "Decoder2") - ]) - - # Compose them - autoencoder = Sequential([encoder, decoder]) - - input_tensor = MockTensor(np.random.randn(10)) - output = autoencoder(input_tensor) - - assert isinstance(output, MockTensor) - assert output.shape == (10,) # Should reconstruct input size - - -class TestNetworkEdgeCases: - """Test edge cases and error conditions for networks.""" - - def test_sequential_with_incompatible_layers(self): - """Test Sequential behavior with dimension mismatches.""" - # Create layers with incompatible dimensions - layer1 = MockLayer(5, 3, "Layer1") - layer2 = MockLayer(10, 2, "Layer2") # Expects 10 inputs, gets 3 - - seq = Sequential([layer1, layer2]) - - input_tensor = MockTensor(np.random.randn(5)) - - # This should either work (if mocks are flexible) or raise appropriate error - try: - output = seq(input_tensor) - # If it works, output should have expected shape - assert isinstance(output, MockTensor) - except (ValueError, AssertionError): - # Acceptable to raise error for incompatible dimensions - pass - - def test_mlp_edge_sizes(self): - """Test MLP with edge case sizes.""" - # Very small network - mlp_small = MLP(input_size=1, hidden_size=1, output_size=1) - input_small = MockTensor([1.0]) - output_small = mlp_small(input_small) - assert output_small.shape == (1,) - - # Network with large hidden layer - mlp_large = MLP(input_size=2, hidden_size=100, output_size=1) - input_large = MockTensor([1.0, 2.0]) - output_large = mlp_large(input_large) - assert output_large.shape == (1,) - - def test_empty_sequential_behavior(self): - """Test Sequential with various empty states.""" - # Empty Sequential - empty_seq = Sequential() - - # Should handle empty input - empty_input = MockTensor([]) - output = empty_seq(empty_input) - assert np.array_equal(output.data, empty_input.data) - - # Should handle normal input (identity function) - normal_input = MockTensor([1.0, 2.0, 3.0]) - output = empty_seq(normal_input) - assert np.array_equal(output.data, normal_input.data) - - def test_network_with_none_layers(self): - """Test network robustness with None layers.""" - # Sequential should handle None layers gracefully - try: - seq = Sequential([None, MockLayer(5, 3, "Layer"), None]) - # Should either filter out None or raise appropriate error - assert True # If we get here, it handled None gracefully - except (ValueError, TypeError): - # Acceptable to raise error for None layers - pass - - -class TestNetworkPerformance: - """Test network performance characteristics.""" - - def test_sequential_call_efficiency(self): - """Test that Sequential doesn't add excessive overhead.""" - layers = [MockLayer(10, 10, f"Layer{i}") for i in range(5)] - seq = Sequential(layers) - - input_tensor = MockTensor(np.random.randn(10)) - - # Multiple calls should work efficiently - for _ in range(10): - output = seq(input_tensor) - assert isinstance(output, MockTensor) - assert output.shape == (10,) - - # Each layer should be called the expected number of times - for layer in layers: - assert layer.call_count == 10 - - def test_mlp_scalability(self): - """Test MLP with different scales.""" - scales = [ - (5, 10, 2), - (20, 50, 10), - (100, 200, 50) - ] - - for input_size, hidden_size, output_size in scales: - mlp = MLP(input_size=input_size, hidden_size=hidden_size, output_size=output_size) - - # Test single sample - single_input = MockTensor(np.random.randn(input_size)) - single_output = mlp(single_input) - assert single_output.shape == (output_size,) - - # Test batch - batch_input = MockTensor(np.random.randn(10, input_size)) - batch_output = mlp(batch_input) - assert batch_output.shape == (10, output_size) - - -if __name__ == "__main__": - # Run tests if executed directly - pytest.main([__file__, "-v"]) \ No newline at end of file