mirror of
https://github.com/MLSysBook/TinyTorch.git
synced 2026-05-31 07:41:12 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user