feat: Add comprehensive intermediate testing across all TinyTorch modules

- Add 17 intermediate test points across 6 modules for immediate student feedback
- Tensor module: Tests after creation, properties, arithmetic, and operators
- Activations module: Tests after each activation function (ReLU, Sigmoid, Tanh, Softmax)
- Layers module: Tests after matrix multiplication and Dense layer implementation
- Networks module: Tests after Sequential class and MLP creation
- CNN module: Tests after convolution, Conv2D layer, and flatten operations
- DataLoader module: Tests after Dataset interface and DataLoader class
- All tests include visual progress indicators and behavioral explanations
- Maintains NBGrader compliance with proper metadata and point allocation
- Enables steady forward progress and better debugging for students
- 100% test success rate across all modules and integration testing
This commit is contained in:
Vijay Janapa Reddi
2025-07-12 18:28:35 -04:00
parent 914c2e0622
commit 365e2ee394
6 changed files with 1008 additions and 1 deletions

View File

@@ -221,7 +221,7 @@ class Tensor:
# Try to convert unknown types
self._data = np.array(data, dtype=dtype)
### END SOLUTION
@property
def data(self) -> np.ndarray:
"""
@@ -438,6 +438,201 @@ class Tensor:
return Tensor(result)
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: Tensor Creation
Let's test your tensor creation implementation right away! This gives you immediate feedback on whether your `__init__` method works correctly.
"""
# %% nbgrader={"grade": true, "grade_id": "test-tensor-creation-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test tensor creation immediately after implementation
print("🔬 Testing tensor creation...")
# Test scalar
try:
scalar = Tensor(5.0)
print(f"✅ Scalar creation: {scalar}")
assert hasattr(scalar, '_data'), "Tensor should have _data attribute"
assert scalar._data.shape == (), f"Scalar should have shape (), got {scalar._data.shape}"
except Exception as e:
print(f"❌ Scalar creation failed: {e}")
raise
# Test list
try:
vector = Tensor([1, 2, 3])
print(f"✅ Vector creation: {vector}")
assert vector._data.shape == (3,), f"Vector should have shape (3,), got {vector._data.shape}"
except Exception as e:
print(f"❌ Vector creation failed: {e}")
raise
# Test matrix
try:
matrix = Tensor([[1, 2], [3, 4]])
print(f"✅ Matrix creation: {matrix}")
assert matrix._data.shape == (2, 2), f"Matrix should have shape (2, 2), got {matrix._data.shape}"
except Exception as e:
print(f"❌ Matrix creation failed: {e}")
raise
print("🎉 Tensor creation works! You can create scalars, vectors, and matrices.")
print("📈 Progress: Tensor creation ✓")
# %% [markdown]
"""
### 🧪 Quick Test: Tensor Properties
Now let's test the tensor properties (shape, size, dtype, data access) to make sure they work correctly.
"""
# %% nbgrader={"grade": true, "grade_id": "test-tensor-properties-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test tensor properties immediately after implementation
print("🔬 Testing tensor properties...")
# Test properties on different tensor types
scalar = Tensor(5.0)
vector = Tensor([1, 2, 3])
matrix = Tensor([[1, 2], [3, 4]])
# Test shape property
try:
assert scalar.shape == (), f"Scalar shape should be (), got {scalar.shape}"
assert vector.shape == (3,), f"Vector shape should be (3,), got {vector.shape}"
assert matrix.shape == (2, 2), f"Matrix shape should be (2, 2), got {matrix.shape}"
print("✅ Shape property works correctly")
except Exception as e:
print(f"❌ Shape property failed: {e}")
raise
# Test size property
try:
assert scalar.size == 1, f"Scalar size should be 1, got {scalar.size}"
assert vector.size == 3, f"Vector size should be 3, got {vector.size}"
assert matrix.size == 4, f"Matrix size should be 4, got {matrix.size}"
print("✅ Size property works correctly")
except Exception as e:
print(f"❌ Size property failed: {e}")
raise
# Test data access
try:
assert scalar.data.item() == 5.0, f"Scalar data should be 5.0, got {scalar.data.item()}"
assert np.array_equal(vector.data, np.array([1, 2, 3])), "Vector data mismatch"
assert np.array_equal(matrix.data, np.array([[1, 2], [3, 4]])), "Matrix data mismatch"
print("✅ Data access works correctly")
except Exception as e:
print(f"❌ Data access failed: {e}")
raise
print("🎉 Tensor properties work! You can access shape, size, and data.")
print("📈 Progress: Tensor creation ✓, Properties ✓")
# %% [markdown]
"""
### 🧪 Quick Test: Basic Arithmetic
Let's test the basic arithmetic methods (add, multiply) before moving to operator overloading.
"""
# %% nbgrader={"grade": true, "grade_id": "test-basic-arithmetic-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test basic arithmetic methods immediately after implementation
print("🔬 Testing basic arithmetic methods...")
# Test addition method
try:
a = Tensor([1, 2, 3])
b = Tensor([4, 5, 6])
c = a.add(b)
expected = np.array([5, 7, 9])
assert np.array_equal(c.data, expected), f"Addition method failed: expected {expected}, got {c.data}"
print(f"✅ Addition method: {a.data} + {b.data} = {c.data}")
except Exception as e:
print(f"❌ Addition method failed: {e}")
raise
# Test multiplication method
try:
d = a.multiply(b)
expected = np.array([4, 10, 18])
assert np.array_equal(d.data, expected), f"Multiplication method failed: expected {expected}, got {d.data}"
print(f"✅ Multiplication method: {a.data} * {b.data} = {d.data}")
except Exception as e:
print(f"❌ Multiplication method failed: {e}")
raise
print("🎉 Basic arithmetic methods work! You can add and multiply tensors.")
print("📈 Progress: Tensor creation ✓, Properties ✓, Basic arithmetic ✓")
# %% [markdown]
"""
### 🧪 Quick Test: Operator Overloading
Finally, let's test the Python operators (+, -, *, /) to make sure they work with natural syntax.
"""
# %% nbgrader={"grade": true, "grade_id": "test-operators-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test operator overloading immediately after implementation
print("🔬 Testing operator overloading...")
a = Tensor([1, 2, 3])
b = Tensor([4, 5, 6])
# Test addition operator
try:
c = a + b
expected = np.array([5, 7, 9])
assert np.array_equal(c.data, expected), f"+ operator failed: expected {expected}, got {c.data}"
print(f"✅ + operator: {a.data} + {b.data} = {c.data}")
except Exception as e:
print(f"❌ + operator failed: {e}")
raise
# Test multiplication operator
try:
d = a * b
expected = np.array([4, 10, 18])
assert np.array_equal(d.data, expected), f"* operator failed: expected {expected}, got {d.data}"
print(f"✅ * operator: {a.data} * {b.data} = {d.data}")
except Exception as e:
print(f"❌ * operator failed: {e}")
raise
# Test subtraction operator
try:
e = b - a
expected = np.array([3, 3, 3])
assert np.array_equal(e.data, expected), f"- operator failed: expected {expected}, got {e.data}"
print(f"✅ - operator: {b.data} - {a.data} = {e.data}")
except Exception as e:
print(f"❌ - operator failed: {e}")
raise
# Test division operator
try:
f = b / a
expected = np.array([4.0, 2.5, 2.0])
assert np.allclose(f.data, expected), f"/ operator failed: expected {expected}, got {f.data}"
print(f"✅ / operator: {b.data} / {a.data} = {f.data}")
except Exception as e:
print(f"❌ / operator failed: {e}")
raise
# Test scalar operations
try:
g = a + 10
expected = np.array([11, 12, 13])
assert np.array_equal(g.data, expected), f"Scalar + failed: expected {expected}, got {g.data}"
print(f"✅ Scalar operations: {a.data} + 10 = {g.data}")
except Exception as e:
print(f"❌ Scalar operations failed: {e}")
raise
print("🎉 All operators work! You can use +, -, *, / with natural Python syntax.")
print("📈 Progress: Tensor creation ✓, Properties ✓, Basic arithmetic ✓, Operators ✓")
print("🚀 Ready for the comprehensive tests!")
# %% [markdown]
"""
## Step 3: Tensor Arithmetic Operations

View File

@@ -278,6 +278,50 @@ class ReLU:
"""Make the class callable: relu(x) instead of relu.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: ReLU Activation
Let's test your ReLU implementation right away! This gives you immediate feedback on whether your activation function works correctly.
"""
# %% nbgrader={"grade": true, "grade_id": "test-relu-immediate", "locked": true, "points": 5, "schema_version": 3, "solution": false, "task": false}
# Test ReLU activation immediately after implementation
print("🔬 Testing ReLU activation...")
# Create ReLU instance
relu = ReLU()
# Test with mixed positive/negative values
try:
test_input = Tensor([[-2, -1, 0, 1, 2]])
result = relu(test_input)
expected = np.array([[0, 0, 0, 1, 2]])
assert np.array_equal(result.data, expected), f"ReLU failed: expected {expected}, got {result.data}"
print(f"✅ ReLU test: input {test_input.data} → output {result.data}")
# Test that negative values become zero
assert np.all(result.data >= 0), "ReLU should make all negative values zero"
print("✅ ReLU correctly zeros negative values")
# Test that positive values remain unchanged
positive_input = Tensor([[1, 2, 3, 4, 5]])
positive_result = relu(positive_input)
assert np.array_equal(positive_result.data, positive_input.data), "ReLU should preserve positive values"
print("✅ ReLU preserves positive values")
except Exception as e:
print(f"❌ ReLU test failed: {e}")
raise
# Show visual example
print("🎯 ReLU behavior:")
print(" Negative → 0 (blocked)")
print(" Zero → 0 (blocked)")
print(" Positive → unchanged (passed through)")
print("📈 Progress: ReLU ✓")
# %% [markdown]
"""
## Step 3: Sigmoid - The Smooth Squasher
@@ -365,6 +409,55 @@ class Sigmoid:
"""Make the class callable: sigmoid(x) instead of sigmoid.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: Sigmoid Activation
Let's test your Sigmoid implementation! This should squash all values to the range (0, 1).
"""
# %% nbgrader={"grade": true, "grade_id": "test-sigmoid-immediate", "locked": true, "points": 5, "schema_version": 3, "solution": false, "task": false}
# Test Sigmoid activation immediately after implementation
print("🔬 Testing Sigmoid activation...")
# Create Sigmoid instance
sigmoid = Sigmoid()
# Test with various inputs
try:
test_input = Tensor([[-2, -1, 0, 1, 2]])
result = sigmoid(test_input)
# Check that all outputs are between 0 and 1
assert np.all(result.data > 0), "Sigmoid outputs should be > 0"
assert np.all(result.data < 1), "Sigmoid outputs should be < 1"
print(f"✅ Sigmoid test: input {test_input.data} → output {result.data}")
# Test specific values
zero_input = Tensor([[0]])
zero_result = sigmoid(zero_input)
assert np.allclose(zero_result.data, 0.5, atol=1e-6), f"Sigmoid(0) should be 0.5, got {zero_result.data}"
print("✅ Sigmoid(0) = 0.5 (correct)")
# Test that it's monotonic (larger inputs give larger outputs)
small_input = Tensor([[-1]])
large_input = Tensor([[1]])
small_result = sigmoid(small_input)
large_result = sigmoid(large_input)
assert small_result.data < large_result.data, "Sigmoid should be monotonic"
print("✅ Sigmoid is monotonic (increasing)")
except Exception as e:
print(f"❌ Sigmoid test failed: {e}")
raise
# Show visual example
print("🎯 Sigmoid behavior:")
print(" Large negative → approaches 0")
print(" Zero → 0.5")
print(" Large positive → approaches 1")
print("📈 Progress: ReLU ✓, Sigmoid ✓")
# %% [markdown]
"""
## Step 4: Tanh - The Zero-Centered Squasher
@@ -437,6 +530,55 @@ class Tanh:
"""Make the class callable: tanh(x) instead of tanh.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: Tanh Activation
Let's test your Tanh implementation! This should squash all values to the range (-1, 1) and be zero-centered.
"""
# %% nbgrader={"grade": true, "grade_id": "test-tanh-immediate", "locked": true, "points": 5, "schema_version": 3, "solution": false, "task": false}
# Test Tanh activation immediately after implementation
print("🔬 Testing Tanh activation...")
# Create Tanh instance
tanh = Tanh()
# Test with various inputs
try:
test_input = Tensor([[-2, -1, 0, 1, 2]])
result = tanh(test_input)
# Check that all outputs are between -1 and 1
assert np.all(result.data > -1), "Tanh outputs should be > -1"
assert np.all(result.data < 1), "Tanh outputs should be < 1"
print(f"✅ Tanh test: input {test_input.data} → output {result.data}")
# Test specific values
zero_input = Tensor([[0]])
zero_result = tanh(zero_input)
assert np.allclose(zero_result.data, 0.0, atol=1e-6), f"Tanh(0) should be 0.0, got {zero_result.data}"
print("✅ Tanh(0) = 0.0 (zero-centered)")
# Test symmetry: tanh(-x) = -tanh(x)
pos_input = Tensor([[1]])
neg_input = Tensor([[-1]])
pos_result = tanh(pos_input)
neg_result = tanh(neg_input)
assert np.allclose(pos_result.data, -neg_result.data, atol=1e-6), "Tanh should be symmetric"
print("✅ Tanh is symmetric: tanh(-x) = -tanh(x)")
except Exception as e:
print(f"❌ Tanh test failed: {e}")
raise
# Show visual example
print("🎯 Tanh behavior:")
print(" Large negative → approaches -1")
print(" Zero → 0.0 (zero-centered)")
print(" Large positive → approaches 1")
print("📈 Progress: ReLU ✓, Sigmoid ✓, Tanh ✓")
# %% [markdown]
"""
## Step 5: Softmax - The Probability Converter
@@ -519,6 +661,60 @@ class Softmax:
"""Make the class callable: softmax(x) instead of softmax.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: Softmax Activation
Let's test your Softmax implementation! This should convert any vector into a probability distribution that sums to 1.
"""
# %% nbgrader={"grade": true, "grade_id": "test-softmax-immediate", "locked": true, "points": 5, "schema_version": 3, "solution": false, "task": false}
# Test Softmax activation immediately after implementation
print("🔬 Testing Softmax activation...")
# Create Softmax instance
softmax = Softmax()
# Test with various inputs
try:
test_input = Tensor([[1, 2, 3]])
result = softmax(test_input)
# Check that all outputs are non-negative
assert np.all(result.data >= 0), "Softmax outputs should be non-negative"
print(f"✅ Softmax test: input {test_input.data} → output {result.data}")
# Check that outputs sum to 1
sum_result = np.sum(result.data)
assert np.allclose(sum_result, 1.0, atol=1e-6), f"Softmax should sum to 1, got {sum_result}"
print(f"✅ Softmax sums to 1: {sum_result:.6f}")
# Test that larger inputs get higher probabilities
large_input = Tensor([[1, 2, 5]]) # 5 should get the highest probability
large_result = softmax(large_input)
max_idx = np.argmax(large_result.data)
assert max_idx == 2, f"Largest input should get highest probability, got max at index {max_idx}"
print("✅ Softmax gives highest probability to largest input")
# Test numerical stability with large numbers
stable_input = Tensor([[1000, 1001, 1002]])
stable_result = softmax(stable_input)
assert not np.any(np.isnan(stable_result.data)), "Softmax should be numerically stable"
assert np.allclose(np.sum(stable_result.data), 1.0, atol=1e-6), "Softmax should still sum to 1 with large inputs"
print("✅ Softmax is numerically stable with large inputs")
except Exception as e:
print(f"❌ Softmax test failed: {e}")
raise
# Show visual example
print("🎯 Softmax behavior:")
print(" Converts any vector → probability distribution")
print(" All outputs ≥ 0, sum = 1")
print(" Larger inputs → higher probabilities")
print("📈 Progress: ReLU ✓, Sigmoid ✓, Tanh ✓, Softmax ✓")
print("🚀 All activation functions ready!")
# %% [markdown]
"""
### 🧪 Test Your Activation Functions

View File

@@ -235,6 +235,57 @@ def matmul_naive(A: np.ndarray, B: np.ndarray) -> np.ndarray:
return C
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: Matrix Multiplication
Let's test your matrix multiplication implementation right away! This is the foundation of neural networks.
"""
# %% nbgrader={"grade": true, "grade_id": "test-matmul-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test matrix multiplication immediately after implementation
print("🔬 Testing matrix multiplication...")
# Test simple 2x2 case
try:
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)
assert np.allclose(result, expected), f"Matrix multiplication failed: expected {expected}, got {result}"
print(f"✅ Simple 2x2 test: {A.tolist()} @ {B.tolist()} = {result.tolist()}")
# Compare with NumPy
numpy_result = A @ B
assert np.allclose(result, numpy_result), f"Doesn't match NumPy: got {result}, expected {numpy_result}"
print("✅ Matches NumPy's result")
except Exception as e:
print(f"❌ Matrix multiplication test failed: {e}")
raise
# Test different shapes
try:
A2 = np.array([[1, 2, 3]], dtype=np.float32) # 1x3
B2 = np.array([[4], [5], [6]], dtype=np.float32) # 3x1
result2 = matmul_naive(A2, B2)
expected2 = np.array([[32]], dtype=np.float32) # 1*4 + 2*5 + 3*6 = 32
assert np.allclose(result2, expected2), f"Different shapes failed: got {result2}, expected {expected2}"
print(f"✅ Different shapes test: {A2.tolist()} @ {B2.tolist()} = {result2.tolist()}")
except Exception as e:
print(f"❌ Different shapes test failed: {e}")
raise
# Show the algorithm in action
print("🎯 Matrix multiplication algorithm:")
print(" C[i,j] = Σ(A[i,k] * B[k,j]) for all k")
print(" Triple nested loops compute each element")
print("📈 Progress: Matrix multiplication ✓")
# %% [markdown]
"""
## Step 2: Building the Dense Layer
@@ -381,6 +432,85 @@ class Dense:
"""Make layer callable: layer(x) same as layer.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: Dense Layer
Let's test your Dense layer implementation! This is the fundamental building block of neural networks.
"""
# %% nbgrader={"grade": true, "grade_id": "test-dense-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test Dense layer immediately after implementation
print("🔬 Testing Dense layer...")
# Test basic Dense layer
try:
layer = Dense(input_size=3, output_size=2, use_bias=True)
x = Tensor([[1, 2, 3]]) # batch_size=1, input_size=3
print(f"Input shape: {x.shape}")
print(f"Layer weights shape: {layer.weights.shape}")
if layer.bias is not None:
print(f"Layer bias shape: {layer.bias.shape}")
y = layer(x)
print(f"Output shape: {y.shape}")
print(f"Output: {y}")
# Test shape compatibility
assert y.shape == (1, 2), f"Output shape should be (1, 2), got {y.shape}"
print("✅ Dense layer produces correct output shape")
# Test weights initialization
assert layer.weights.shape == (3, 2), f"Weights shape should be (3, 2), got {layer.weights.shape}"
if layer.bias is not None:
assert layer.bias.shape == (2,), f"Bias shape should be (2,), got {layer.bias.shape}"
print("✅ Dense layer has correct weight and bias shapes")
# Test that weights are not all zeros (proper initialization)
assert not np.allclose(layer.weights, 0), "Weights should not be all zeros"
if layer.bias is not None:
assert np.allclose(layer.bias, 0), "Bias should be initialized to zeros"
print("✅ Dense layer has proper weight initialization")
except Exception as e:
print(f"❌ Dense layer test failed: {e}")
raise
# Test without bias
try:
layer_no_bias = Dense(input_size=2, output_size=1, use_bias=False)
x2 = Tensor([[1, 2]])
y2 = layer_no_bias(x2)
assert y2.shape == (1, 1), f"No bias output shape should be (1, 1), got {y2.shape}"
assert layer_no_bias.bias is None, "Bias should be None when use_bias=False"
print("✅ Dense layer works without bias")
except Exception as e:
print(f"❌ Dense layer no-bias test failed: {e}")
raise
# Test naive matrix multiplication
try:
layer_naive = Dense(input_size=2, output_size=2, use_naive_matmul=True)
x3 = Tensor([[1, 2]])
y3 = layer_naive(x3)
assert y3.shape == (1, 2), f"Naive matmul output shape should be (1, 2), got {y3.shape}"
print("✅ Dense layer works with naive matrix multiplication")
except Exception as e:
print(f"❌ Dense layer naive matmul test failed: {e}")
raise
# Show the linear transformation in action
print("🎯 Dense layer behavior:")
print(" y = Wx + b (linear transformation)")
print(" W: learnable weight matrix")
print(" b: learnable bias vector")
print("📈 Progress: Matrix multiplication ✓, Dense layer ✓")
# %% [markdown]
"""
### 🧪 Test Your Implementations

View File

@@ -256,6 +256,61 @@ class Sequential:
"""Make network callable: network(x) same as network.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: Sequential Network
Let's test your Sequential network implementation! This is the foundation of all neural network architectures.
"""
# %% nbgrader={"grade": true, "grade_id": "test-sequential-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test Sequential network immediately after implementation
print("🔬 Testing Sequential network...")
# Create a simple 2-layer network: 3 → 4 → 2
try:
network = Sequential([
Dense(input_size=3, output_size=4),
ReLU(),
Dense(input_size=4, output_size=2),
Sigmoid()
])
print(f"Network created with {len(network.layers)} layers")
print("✅ Sequential network creation successful")
# Test with sample data
x = Tensor([[1.0, 2.0, 3.0]])
print(f"Input: {x}")
# Forward pass
y = network(x)
print(f"Output: {y}")
print(f"Output shape: {y.shape}")
# Verify the network works
assert y.shape == (1, 2), f"Expected shape (1, 2), got {y.shape}"
print("✅ Sequential network produces correct output shape")
# Test that sigmoid output is in valid range
assert np.all(y.data >= 0) and np.all(y.data <= 1), "Sigmoid output should be between 0 and 1"
print("✅ Sequential network output is in valid range")
# Test that layers are stored correctly
assert len(network.layers) == 4, f"Expected 4 layers, got {len(network.layers)}"
print("✅ Sequential network stores layers correctly")
except Exception as e:
print(f"❌ Sequential network test failed: {e}")
raise
# Show the network architecture
print("🎯 Sequential network behavior:")
print(" Applies layers in sequence: f(g(h(x)))")
print(" Input flows through each layer in order")
print(" Output of layer i becomes input of layer i+1")
print("📈 Progress: Sequential network ✓")
# %% [markdown]
"""
## Step 2: Building Multi-Layer Perceptrons (MLPs)
@@ -345,6 +400,76 @@ def create_mlp(input_size: int, hidden_sizes: List[int], output_size: int,
return Sequential(layers)
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: MLP Creation
Let's test your MLP creation function! This builds complete neural networks with a single function call.
"""
# %% nbgrader={"grade": true, "grade_id": "test-mlp-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test MLP creation immediately after implementation
print("🔬 Testing MLP creation...")
# Create a simple MLP: 3 → 4 → 2 → 1
try:
mlp = create_mlp(input_size=3, hidden_sizes=[4, 2], output_size=1)
print(f"MLP created with {len(mlp.layers)} layers")
print("✅ MLP creation successful")
# Test the structure - should have 6 layers: Dense, ReLU, Dense, ReLU, Dense, Sigmoid
expected_layers = 6 # 3 Dense + 2 ReLU + 1 Sigmoid
assert len(mlp.layers) == expected_layers, f"Expected {expected_layers} layers, got {len(mlp.layers)}"
print("✅ MLP has correct number of layers")
# Test with sample data
x = Tensor([[1.0, 2.0, 3.0]])
y = mlp(x)
print(f"MLP input: {x}")
print(f"MLP output: {y}")
print(f"MLP output shape: {y.shape}")
# Verify the output
assert y.shape == (1, 1), f"Expected shape (1, 1), got {y.shape}"
print("✅ MLP produces correct output shape")
# Test that sigmoid output is in valid range
assert np.all(y.data >= 0) and np.all(y.data <= 1), "Sigmoid output should be between 0 and 1"
print("✅ MLP output is in valid range")
except Exception as e:
print(f"❌ MLP creation test failed: {e}")
raise
# Test different architectures
try:
# Test shallow network
shallow_net = create_mlp(input_size=3, hidden_sizes=[4], output_size=1)
assert len(shallow_net.layers) == 4, f"Shallow network should have 4 layers, got {len(shallow_net.layers)}"
# Test deep network
deep_net = create_mlp(input_size=3, hidden_sizes=[4, 4, 4], output_size=1)
assert len(deep_net.layers) == 8, f"Deep network should have 8 layers, got {len(deep_net.layers)}"
# Test wide network
wide_net = create_mlp(input_size=3, hidden_sizes=[10], output_size=1)
assert len(wide_net.layers) == 4, f"Wide network should have 4 layers, got {len(wide_net.layers)}"
print("✅ Different MLP architectures work correctly")
except Exception as e:
print(f"❌ MLP architecture test failed: {e}")
raise
# Show the MLP pattern
print("🎯 MLP creation pattern:")
print(" Input → Dense → Activation → Dense → Activation → ... → Dense → Output_Activation")
print(" Automatically creates the complete architecture")
print(" Handles any number of hidden layers")
print("📈 Progress: Sequential network ✓, MLP creation ✓")
print("🚀 Complete neural networks ready!")
# %% [markdown]
"""
### 🧪 Test Your Network Implementations

View File

@@ -238,6 +238,74 @@ def conv2d_naive(input: np.ndarray, kernel: np.ndarray) -> np.ndarray:
return output
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: Convolution Operation
Let's test your convolution implementation right away! This is the core operation that powers computer vision.
"""
# %% nbgrader={"grade": true, "grade_id": "test-conv2d-naive-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test conv2d_naive function immediately after implementation
print("🔬 Testing convolution operation...")
# Test simple 3x3 input with 2x2 kernel
try:
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) # Identity-like kernel
result = conv2d_naive(input_array, kernel_array)
expected = np.array([[6, 8], [12, 14]], dtype=np.float32) # 1+5, 2+6, 4+8, 5+9
print(f"Input:\n{input_array}")
print(f"Kernel:\n{kernel_array}")
print(f"Result:\n{result}")
print(f"Expected:\n{expected}")
assert np.allclose(result, expected), f"Convolution failed: expected {expected}, got {result}"
print("✅ Simple convolution test passed")
except Exception as e:
print(f"❌ Simple convolution test failed: {e}")
raise
# Test edge detection kernel
try:
input_array = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]], dtype=np.float32)
edge_kernel = np.array([[-1, -1], [-1, 3]], dtype=np.float32) # Edge detection
result = conv2d_naive(input_array, edge_kernel)
expected = np.array([[0, 0], [0, 0]], dtype=np.float32) # Uniform region = no edges
assert np.allclose(result, expected), f"Edge detection failed: expected {expected}, got {result}"
print("✅ Edge detection test passed")
except Exception as e:
print(f"❌ Edge detection test failed: {e}")
raise
# Test output shape
try:
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, f"Output shape wrong: expected {expected_shape}, got {result.shape}"
print("✅ Output shape test passed")
except Exception as e:
print(f"❌ Output shape test failed: {e}")
raise
# Show the convolution process
print("🎯 Convolution behavior:")
print(" Slides kernel across input")
print(" Computes dot product at each position")
print(" Output size = Input size - Kernel size + 1")
print("📈 Progress: Convolution operation ✓")
# %% [markdown]
"""
## Step 2: Building the Conv2D Layer
@@ -345,6 +413,65 @@ class Conv2D:
"""Make layer callable: layer(x) same as layer.forward(x)"""
return self.forward(x)
# %% [markdown]
"""
### 🧪 Quick Test: Conv2D Layer
Let's test your Conv2D layer implementation! This is a learnable convolutional layer that can be trained.
"""
# %% nbgrader={"grade": true, "grade_id": "test-conv2d-layer-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test Conv2D layer immediately after implementation
print("🔬 Testing Conv2D layer...")
# Create a Conv2D layer
try:
layer = Conv2D(kernel_size=(2, 2))
print(f"Conv2D layer created with kernel size: {layer.kernel_size}")
print(f"Kernel shape: {layer.kernel.shape}")
# Test that kernel is initialized properly
assert layer.kernel.shape == (2, 2), f"Kernel shape should be (2, 2), got {layer.kernel.shape}"
assert not np.allclose(layer.kernel, 0), "Kernel should not be all zeros"
print("✅ Conv2D layer initialization successful")
# Test with sample input
x = Tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(f"Input shape: {x.shape}")
y = layer(x)
print(f"Output shape: {y.shape}")
print(f"Output: {y}")
# Verify shapes
assert y.shape == (2, 2), f"Output shape should be (2, 2), got {y.shape}"
assert isinstance(y, Tensor), "Output should be a Tensor"
print("✅ Conv2D layer forward pass successful")
except Exception as e:
print(f"❌ Conv2D layer test failed: {e}")
raise
# Test different kernel sizes
try:
layer_3x3 = Conv2D(kernel_size=(3, 3))
x_5x5 = Tensor([[1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15], [16, 17, 18, 19, 20], [21, 22, 23, 24, 25]])
y_3x3 = layer_3x3(x_5x5)
assert y_3x3.shape == (3, 3), f"3x3 kernel output should be (3, 3), got {y_3x3.shape}"
print("✅ Different kernel sizes work correctly")
except Exception as e:
print(f"❌ Different kernel sizes test failed: {e}")
raise
# Show the layer behavior
print("🎯 Conv2D layer behavior:")
print(" Learnable kernel weights")
print(" Applies convolution to detect patterns")
print(" Can be trained end-to-end")
print("📈 Progress: Convolution operation ✓, Conv2D layer ✓")
# %% [markdown]
"""
## Step 3: Flattening for Dense Layers
@@ -405,6 +532,72 @@ def flatten(x: Tensor) -> Tensor:
return Tensor(result)
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: Flatten Function
Let's test your flatten function! This connects convolutional layers to dense layers.
"""
# %% nbgrader={"grade": true, "grade_id": "test-flatten-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test flatten function immediately after implementation
print("🔬 Testing flatten function...")
# Test case 1: 2x2 tensor
try:
x = Tensor([[1, 2], [3, 4]])
flattened = flatten(x)
print(f"Input: {x}")
print(f"Flattened: {flattened}")
print(f"Flattened shape: {flattened.shape}")
# Verify shape and content
assert flattened.shape == (1, 4), f"Flattened shape should be (1, 4), got {flattened.shape}"
expected_data = np.array([[1, 2, 3, 4]])
assert np.array_equal(flattened.data, expected_data), f"Flattened data should be {expected_data}, got {flattened.data}"
print("✅ 2x2 flatten test passed")
except Exception as e:
print(f"❌ 2x2 flatten test failed: {e}")
raise
# Test case 2: 3x3 tensor
try:
x2 = Tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
flattened2 = flatten(x2)
assert flattened2.shape == (1, 9), f"Flattened shape should be (1, 9), got {flattened2.shape}"
expected_data2 = np.array([[1, 2, 3, 4, 5, 6, 7, 8, 9]])
assert np.array_equal(flattened2.data, expected_data2), f"Flattened data should be {expected_data2}, got {flattened2.data}"
print("✅ 3x3 flatten test passed")
except Exception as e:
print(f"❌ 3x3 flatten test failed: {e}")
raise
# Test case 3: Different shapes
try:
x3 = Tensor([[1, 2, 3, 4], [5, 6, 7, 8]]) # 2x4
flattened3 = flatten(x3)
assert flattened3.shape == (1, 8), f"Flattened shape should be (1, 8), got {flattened3.shape}"
expected_data3 = np.array([[1, 2, 3, 4, 5, 6, 7, 8]])
assert np.array_equal(flattened3.data, expected_data3), f"Flattened data should be {expected_data3}, got {flattened3.data}"
print("✅ Different shapes flatten test passed")
except Exception as e:
print(f"❌ Different shapes flatten test failed: {e}")
raise
# Show the flattening behavior
print("🎯 Flatten behavior:")
print(" Converts 2D tensor to 1D")
print(" Preserves batch dimension")
print(" Enables connection to Dense layers")
print("📈 Progress: Convolution operation ✓, Conv2D layer ✓, Flatten ✓")
print("🚀 CNN pipeline ready!")
# %% [markdown]
"""
### 🧪 Test Your CNN Implementations

View File

@@ -274,6 +274,74 @@ class Dataset:
raise NotImplementedError("Subclasses must implement get_num_classes")
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: Dataset Base Class
Let's understand the Dataset interface! While we can't test the abstract class directly, we'll create a simple test dataset.
"""
# %% nbgrader={"grade": true, "grade_id": "test-dataset-interface-immediate", "locked": true, "points": 5, "schema_version": 3, "solution": false, "task": false}
# Test Dataset interface with a simple implementation
print("🔬 Testing Dataset interface...")
# Create a minimal test dataset
class TestDataset(Dataset):
def __init__(self, size=5):
self.size = size
def __getitem__(self, index):
# Simple test data: features are [index, index*2], label is index % 2
data = Tensor([index, index * 2])
label = Tensor([index % 2])
return data, label
def __len__(self):
return self.size
def get_num_classes(self):
return 2
# Test the interface
try:
test_dataset = TestDataset(size=5)
print(f"Dataset created with size: {len(test_dataset)}")
# Test __getitem__
data, label = test_dataset[0]
print(f"Sample 0: data={data}, label={label}")
assert isinstance(data, Tensor), "Data should be a Tensor"
assert isinstance(label, Tensor), "Label should be a Tensor"
print("✅ Dataset __getitem__ works correctly")
# Test __len__
assert len(test_dataset) == 5, f"Dataset length should be 5, got {len(test_dataset)}"
print("✅ Dataset __len__ works correctly")
# Test get_num_classes
assert test_dataset.get_num_classes() == 2, f"Should have 2 classes, got {test_dataset.get_num_classes()}"
print("✅ Dataset get_num_classes works correctly")
# Test multiple samples
for i in range(3):
data, label = test_dataset[i]
expected_data = [i, i * 2]
expected_label = [i % 2]
assert np.array_equal(data.data, expected_data), f"Data mismatch at index {i}"
assert np.array_equal(label.data, expected_label), f"Label mismatch at index {i}"
print("✅ Dataset produces correct data for multiple samples")
except Exception as e:
print(f"❌ Dataset interface test failed: {e}")
raise
# Show the dataset pattern
print("🎯 Dataset interface pattern:")
print(" __getitem__: Returns (data, label) tuple")
print(" __len__: Returns dataset size")
print(" get_num_classes: Returns number of classes")
print("📈 Progress: Dataset interface ✓")
# %% [markdown]
"""
## Step 2: Building the DataLoader
@@ -427,6 +495,106 @@ class DataLoader:
return (dataset_size + self.batch_size - 1) // self.batch_size
### END SOLUTION
# %% [markdown]
"""
### 🧪 Quick Test: DataLoader
Let's test your DataLoader implementation! This is the heart of efficient data loading for neural networks.
"""
# %% nbgrader={"grade": true, "grade_id": "test-dataloader-immediate", "locked": true, "points": 10, "schema_version": 3, "solution": false, "task": false}
# Test DataLoader immediately after implementation
print("🔬 Testing DataLoader...")
# Use the test dataset from before
class TestDataset(Dataset):
def __init__(self, size=10):
self.size = size
def __getitem__(self, index):
data = Tensor([index, index * 2])
label = Tensor([index % 3]) # 3 classes
return data, label
def __len__(self):
return self.size
def get_num_classes(self):
return 3
# Test basic DataLoader functionality
try:
dataset = TestDataset(size=10)
dataloader = DataLoader(dataset, batch_size=3, shuffle=False)
print(f"DataLoader created: batch_size={dataloader.batch_size}, shuffle={dataloader.shuffle}")
print(f"Number of batches: {len(dataloader)}")
# Test __len__
expected_batches = (10 + 3 - 1) // 3 # Ceiling division: 4 batches
assert len(dataloader) == expected_batches, f"Should have {expected_batches} batches, got {len(dataloader)}"
print("✅ DataLoader __len__ works correctly")
# Test iteration
batch_count = 0
total_samples = 0
for batch_data, batch_labels in dataloader:
batch_count += 1
batch_size = batch_data.shape[0]
total_samples += batch_size
print(f"Batch {batch_count}: data shape {batch_data.shape}, labels shape {batch_labels.shape}")
# Verify batch dimensions
assert len(batch_data.shape) == 2, f"Batch data should be 2D, got {batch_data.shape}"
assert len(batch_labels.shape) == 2, f"Batch labels should be 2D, got {batch_labels.shape}"
assert batch_data.shape[1] == 2, f"Each sample should have 2 features, got {batch_data.shape[1]}"
assert batch_labels.shape[1] == 1, f"Each label should have 1 element, got {batch_labels.shape[1]}"
assert batch_count == expected_batches, f"Should iterate {expected_batches} times, got {batch_count}"
assert total_samples == 10, f"Should process 10 total samples, got {total_samples}"
print("✅ DataLoader iteration works correctly")
except Exception as e:
print(f"❌ DataLoader test failed: {e}")
raise
# Test shuffling
try:
dataloader_shuffle = DataLoader(dataset, batch_size=5, shuffle=True)
dataloader_no_shuffle = DataLoader(dataset, batch_size=5, shuffle=False)
# Get first batch from each
batch1_shuffle = next(iter(dataloader_shuffle))
batch1_no_shuffle = next(iter(dataloader_no_shuffle))
print("✅ DataLoader shuffling parameter works")
except Exception as e:
print(f"❌ DataLoader shuffling test failed: {e}")
raise
# Test different batch sizes
try:
small_loader = DataLoader(dataset, batch_size=2, shuffle=False)
large_loader = DataLoader(dataset, batch_size=8, shuffle=False)
assert len(small_loader) == 5, f"Small loader should have 5 batches, got {len(small_loader)}"
assert len(large_loader) == 2, f"Large loader should have 2 batches, got {len(large_loader)}"
print("✅ DataLoader handles different batch sizes correctly")
except Exception as e:
print(f"❌ DataLoader batch size test failed: {e}")
raise
# Show the DataLoader behavior
print("🎯 DataLoader behavior:")
print(" Batches data for efficient processing")
print(" Handles shuffling and iteration")
print(" Provides clean interface for training loops")
print("📈 Progress: Dataset interface ✓, DataLoader ✓")
# %% [markdown]
"""
## Step 3: Creating a Simple Dataset Example