mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-06-01 19:57:15 -05:00
Reorganize tests: Remove mocks, add real integration tests
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.
This commit is contained in:
6
tests/e2e/__init__.py
Normal file
6
tests/e2e/__init__.py
Normal file
@@ -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.
|
||||
"""
|
||||
306
tests/integration/test_layers_networks.py
Normal file
306
tests/integration/test_layers_networks.py
Normal file
@@ -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"])
|
||||
230
tests/integration/test_tensor_activations.py
Normal file
230
tests/integration/test_tensor_activations.py
Normal file
@@ -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"])
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
@@ -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"])
|
||||
Reference in New Issue
Block a user