diff --git a/modules/source/01_tensor/tensor_dev.py b/modules/source/01_tensor/tensor_dev.py index c235972e..035c9188 100644 --- a/modules/source/01_tensor/tensor_dev.py +++ b/modules/source/01_tensor/tensor_dev.py @@ -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 diff --git a/modules/source/02_activations/activations_dev.py b/modules/source/02_activations/activations_dev.py index d0d1d7b6..29424fb5 100644 --- a/modules/source/02_activations/activations_dev.py +++ b/modules/source/02_activations/activations_dev.py @@ -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 diff --git a/modules/source/03_layers/layers_dev.py b/modules/source/03_layers/layers_dev.py index 0b075501..f21d24c4 100644 --- a/modules/source/03_layers/layers_dev.py +++ b/modules/source/03_layers/layers_dev.py @@ -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 diff --git a/modules/source/04_networks/networks_dev.py b/modules/source/04_networks/networks_dev.py index 4a54bd56..358c3654 100644 --- a/modules/source/04_networks/networks_dev.py +++ b/modules/source/04_networks/networks_dev.py @@ -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 diff --git a/modules/source/05_cnn/cnn_dev.py b/modules/source/05_cnn/cnn_dev.py index 4bef9ccc..d529b46c 100644 --- a/modules/source/05_cnn/cnn_dev.py +++ b/modules/source/05_cnn/cnn_dev.py @@ -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 diff --git a/modules/source/06_dataloader/dataloader_dev.py b/modules/source/06_dataloader/dataloader_dev.py index bfae9892..7dd5dd15 100644 --- a/modules/source/06_dataloader/dataloader_dev.py +++ b/modules/source/06_dataloader/dataloader_dev.py @@ -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