mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-07-16 14:42:29 -05:00
refactor: implement strict progressive disclosure for autograd
- Module 01: Remove requires_grad, grad, backward() from Tensor class Students learn pure tensor math first without gradient concepts - Module 02: Remove requires_grad propagation from Softmax Activations are now forward-only until autograd is enabled - Module 03: Remove requires_grad=True from layer weights Layers store parameters without gradient flags - Module 05: Update enable_autograd() to ADD gradient infrastructure Now adds requires_grad, grad, and backward() to Tensor class Uses helper functions for tensors created before autograd - Module 09: Remove Conv2dBackward, MaxPool2dBackward classes Convolutions are now forward-only, no Module 05 import - Module 11: Remove EmbeddingBackward import and usage Embeddings are now forward-only - Modules 12, 13: Remove requires_grad from mask/param tensors This implements true progressive disclosure: concepts are introduced only when students are ready to learn them. Gradient tracking is now completely absent from Modules 01-04 and added in Module 05.
This commit is contained in:
@@ -33,8 +33,8 @@ NumPy Arrays → Tensor → Activations (Module 02)
|
||||
By the end of this module, you will:
|
||||
1. Implement a complete Tensor class with fundamental operations
|
||||
2. Understand tensors as the universal data structure in ML
|
||||
3. Test tensor operations with immediate validation
|
||||
4. Prepare for gradient computation in Module 05
|
||||
3. Master broadcasting, matrix multiplication, and shape manipulation
|
||||
4. Test tensor operations with immediate validation
|
||||
|
||||
Let's get started!
|
||||
|
||||
@@ -193,7 +193,7 @@ This memory layout affects performance in real ML workloads - algorithms that ac
|
||||
|
||||
Let's build our Tensor class step by step, testing each component as we go.
|
||||
|
||||
**Key Design Decision**: We'll include gradient-related attributes from the start, but they'll remain dormant until Module 05. This ensures a consistent interface throughout the course while keeping the cognitive load manageable.
|
||||
**Key Design Decision**: We focus on core tensor operations first. Gradient tracking will be added in Module 05 (Autograd) when you're ready to learn backpropagation.
|
||||
|
||||
### Tensor Class Architecture
|
||||
|
||||
@@ -204,21 +204,24 @@ Tensor Class Structure:
|
||||
│ • data: np.array (the numbers) │
|
||||
│ • shape: tuple (dimensions) │
|
||||
│ • size: int (total elements) │
|
||||
│ • dtype: type (float32, int64) │
|
||||
│ • dtype: type (float32) │
|
||||
├─────────────────────────────────┤
|
||||
│ Gradient Attributes (dormant): │
|
||||
│ • requires_grad: bool │
|
||||
│ • grad: None (until Module 05) │
|
||||
├─────────────────────────────────┤
|
||||
│ Operations: │
|
||||
│ Arithmetic Operations: │
|
||||
│ • __add__, __sub__, __mul__ │
|
||||
│ • matmul(), reshape() │
|
||||
│ • __truediv__, matmul() │
|
||||
├─────────────────────────────────┤
|
||||
│ Shape Operations: │
|
||||
│ • reshape(), transpose() │
|
||||
│ • sum(), mean(), max() │
|
||||
│ • __getitem__ (indexing) │
|
||||
├─────────────────────────────────┤
|
||||
│ Utility Methods: │
|
||||
│ • __repr__(), __str__() │
|
||||
│ • numpy(), memory_footprint() │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
The beauty of this design: **all methods are defined inside the class from day one**. No monkey-patching, no dynamic attribute addition. Clean, consistent, debugger-friendly.
|
||||
This clean design focuses on what tensors fundamentally do: store and manipulate numerical data efficiently.
|
||||
"""
|
||||
|
||||
# %% [markdown]
|
||||
@@ -255,31 +258,29 @@ Tensor wraps with: shape=(2,3), size=6, dtype=int64
|
||||
# %% nbgrader={"grade": false, "grade_id": "tensor-class", "solution": true}
|
||||
#| export
|
||||
class Tensor:
|
||||
"""Educational tensor that grows with student knowledge.
|
||||
"""Educational tensor - the foundation of machine learning computation.
|
||||
|
||||
This class starts simple but includes dormant features for future modules:
|
||||
- requires_grad: Will be used for automatic differentiation (Module 05)
|
||||
- grad: Will store computed gradients (Module 05)
|
||||
- backward(): Will compute gradients (Module 05)
|
||||
This class provides the core data structure for all ML operations:
|
||||
- data: The actual numerical values (NumPy array)
|
||||
- shape: Dimensions of the tensor
|
||||
- size: Total number of elements
|
||||
- dtype: Data type (float32)
|
||||
|
||||
For now, focus on: data, shape, and basic operations.
|
||||
All arithmetic, matrix, and shape operations are built on this foundation.
|
||||
"""
|
||||
|
||||
def __init__(self, data, requires_grad=False):
|
||||
def __init__(self, data):
|
||||
"""Create a new tensor from data."""
|
||||
### BEGIN SOLUTION
|
||||
self.data = np.array(data, dtype=np.float32)
|
||||
self.shape = self.data.shape
|
||||
self.size = self.data.size
|
||||
self.dtype = self.data.dtype
|
||||
self.requires_grad = requires_grad
|
||||
self.grad = None
|
||||
### END SOLUTION
|
||||
|
||||
def __repr__(self):
|
||||
"""String representation of tensor for debugging."""
|
||||
grad_info = f", requires_grad={self.requires_grad}" if self.requires_grad else ""
|
||||
return f"Tensor(data={self.data}, shape={self.shape}{grad_info})"
|
||||
return f"Tensor(data={self.data}, shape={self.shape})"
|
||||
|
||||
def __str__(self):
|
||||
"""Human-readable string representation."""
|
||||
@@ -389,8 +390,7 @@ class Tensor:
|
||||
result_data = self.data[key]
|
||||
if not isinstance(result_data, np.ndarray):
|
||||
result_data = np.array(result_data)
|
||||
result = Tensor(result_data, requires_grad=self.requires_grad)
|
||||
return result
|
||||
return Tensor(result_data)
|
||||
### END SOLUTION
|
||||
|
||||
def reshape(self, *shape):
|
||||
@@ -418,8 +418,7 @@ class Tensor:
|
||||
f"Total elements must match: {self.size} ≠ {target_size}"
|
||||
)
|
||||
reshaped_data = np.reshape(self.data, new_shape)
|
||||
result = Tensor(reshaped_data, requires_grad=self.requires_grad)
|
||||
return result
|
||||
return Tensor(reshaped_data)
|
||||
### END SOLUTION
|
||||
|
||||
def transpose(self, dim0=None, dim1=None):
|
||||
@@ -438,8 +437,7 @@ class Tensor:
|
||||
axes = list(range(len(self.shape)))
|
||||
axes[dim0], axes[dim1] = axes[dim1], axes[dim0]
|
||||
transposed_data = np.transpose(self.data, axes)
|
||||
result = Tensor(transposed_data, requires_grad=self.requires_grad)
|
||||
return result
|
||||
return Tensor(transposed_data)
|
||||
### END SOLUTION
|
||||
|
||||
def sum(self, axis=None, keepdims=False):
|
||||
@@ -463,12 +461,6 @@ class Tensor:
|
||||
return Tensor(result)
|
||||
### END SOLUTION
|
||||
|
||||
def backward(self):
|
||||
"""Compute gradients (implemented in Module 05: Autograd)."""
|
||||
### BEGIN SOLUTION
|
||||
pass
|
||||
### END SOLUTION
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
### 🧪 Unit Test: Tensor Creation
|
||||
@@ -490,8 +482,6 @@ def test_unit_tensor_creation():
|
||||
assert scalar.data == 5.0
|
||||
assert scalar.shape == ()
|
||||
assert scalar.size == 1
|
||||
assert scalar.requires_grad == False
|
||||
assert scalar.grad is None
|
||||
assert scalar.dtype == np.float32
|
||||
|
||||
# Test vector creation
|
||||
@@ -506,10 +496,10 @@ def test_unit_tensor_creation():
|
||||
assert matrix.shape == (2, 2)
|
||||
assert matrix.size == 4
|
||||
|
||||
# Test gradient flag (dormant feature)
|
||||
grad_tensor = Tensor([1, 2], requires_grad=True)
|
||||
assert grad_tensor.requires_grad == True
|
||||
assert grad_tensor.grad is None # Still None until Module 05
|
||||
# Test 3D tensor creation
|
||||
tensor_3d = Tensor([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
|
||||
assert tensor_3d.shape == (2, 2, 2)
|
||||
assert tensor_3d.size == 8
|
||||
|
||||
print("✅ Tensor creation works correctly!")
|
||||
|
||||
@@ -1128,76 +1118,6 @@ def test_unit_reduction_operations():
|
||||
if __name__ == "__main__":
|
||||
test_unit_reduction_operations()
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
## Gradient Features: Preparing for Module 05
|
||||
|
||||
Our Tensor includes dormant gradient features that will spring to life in Module 05. For now, they exist but do nothing - this design choice ensures a consistent interface throughout the course.
|
||||
|
||||
### Why Include Gradient Features Now?
|
||||
|
||||
```
|
||||
Gradient System Evolution:
|
||||
Module 01: Tensor with dormant gradients
|
||||
┌─────────────────────────────────┐
|
||||
│ Tensor │
|
||||
│ • data: actual values │
|
||||
│ • requires_grad: False │ ← Present but unused
|
||||
│ • grad: None │ ← Present but stays None
|
||||
│ • backward(): pass │ ← Present but does nothing
|
||||
└─────────────────────────────────┘
|
||||
↓ Module 05 activates these
|
||||
Module 05: Tensor with active gradients
|
||||
┌─────────────────────────────────┐
|
||||
│ Tensor │
|
||||
│ • data: actual values │
|
||||
│ • requires_grad: True │ ← Now controls gradient tracking
|
||||
│ • grad: computed gradients │ ← Now accumulates gradients
|
||||
│ • backward(): computes grads │ ← Now implements chain rule
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Design Benefits
|
||||
|
||||
**Consistency**: Same Tensor class interface throughout all modules
|
||||
- No confusing Variable vs. Tensor distinction (unlike early PyTorch)
|
||||
- Students never need to learn a "new" Tensor class
|
||||
- IDE autocomplete works from day one
|
||||
|
||||
**Gradual Complexity**: Features activate when students are ready
|
||||
- Module 01-04: Ignore gradient features, focus on operations
|
||||
- Module 05: Gradient features "turn on" magically
|
||||
- No cognitive overload in early modules
|
||||
|
||||
**Future-Proof**: Easy to extend without breaking changes
|
||||
- Additional features can be added as dormant initially
|
||||
- No monkey-patching or dynamic class modification
|
||||
- Clean evolution path
|
||||
|
||||
### Current State (Module 01)
|
||||
|
||||
```
|
||||
Gradient Features - Current Behavior:
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Feature │ Current State │ Module 05 State │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ requires_grad │ False │ True (when needed) │
|
||||
│ grad │ None │ np.array(...) │
|
||||
│ backward() │ pass (no-op) │ Chain rule impl │
|
||||
│ Operation chaining│ Not tracked │ Computation graph │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
Student Experience:
|
||||
• Can call .backward() without errors (just does nothing)
|
||||
• Can set requires_grad=True (just gets stored)
|
||||
• Focus on understanding tensor operations first
|
||||
• Gradients remain "mysterious" until Module 05 reveals them
|
||||
```
|
||||
|
||||
This approach matches the pedagogical principle of "progressive disclosure" - reveal complexity only when students are ready to handle it.
|
||||
"""
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
## Systems Analysis: Memory Layout and Performance
|
||||
@@ -1390,18 +1310,6 @@ def test_module():
|
||||
|
||||
print("✅ Two-layer neural network computation works!")
|
||||
|
||||
# Test gradient attributes are preserved and functional
|
||||
print("🧪 Integration Test: Gradient System Readiness...")
|
||||
grad_tensor = Tensor([1, 2, 3], requires_grad=True)
|
||||
result = grad_tensor + 5
|
||||
assert grad_tensor.requires_grad == True, "requires_grad not preserved"
|
||||
assert grad_tensor.grad is None, "grad should still be None"
|
||||
|
||||
# Test backward() doesn't crash (even though it does nothing)
|
||||
grad_tensor.backward() # Should not raise any exception
|
||||
|
||||
print("✅ Gradient system ready for Module 05!")
|
||||
|
||||
# Test complex shape manipulations
|
||||
print("🧪 Integration Test: Complex Shape Operations...")
|
||||
data = Tensor([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
|
||||
@@ -1624,19 +1532,18 @@ Congratulations! You've built the foundational Tensor class that powers all mach
|
||||
### Key Accomplishments
|
||||
- **Built a complete Tensor class** with arithmetic operations, matrix multiplication, and shape manipulation
|
||||
- **Implemented broadcasting semantics** that match NumPy for automatic shape alignment
|
||||
- **Created dormant gradient features** that will activate in Module 05 (autograd)
|
||||
- **Created reduction operations** (sum, mean, max) for loss computation and pooling
|
||||
- **Added comprehensive ASCII diagrams** showing tensor operations visually
|
||||
- **All methods defined INSIDE the class** (no monkey-patching) for clean, maintainable code
|
||||
- **All tests pass ✅** (validated by `test_module()`)
|
||||
|
||||
### Systems Insights Discovered
|
||||
- **Memory scaling**: Matrix operations create new tensors (3× memory during computation)
|
||||
- **Broadcasting efficiency**: NumPy's automatic shape alignment vs. explicit operations
|
||||
- **Cache behavior**: Row-wise access is faster than column-wise due to memory layout
|
||||
- **Shape validation trade-offs**: Clear errors vs. performance in tight loops
|
||||
- **Architecture decisions**: Dormant features vs. inheritance for clean evolution
|
||||
|
||||
### Ready for Next Steps
|
||||
Your Tensor implementation enables all future modules! The dormant gradient features will spring to life in Module 05, and every neural network component will build on this foundation.
|
||||
Your Tensor implementation enables all future modules! In Module 05 (Autograd), gradient tracking will be added to enable training. Every neural network component will build on this foundation.
|
||||
|
||||
Export with: `tito module complete 01_tensor`
|
||||
|
||||
|
||||
@@ -762,19 +762,18 @@ class Softmax:
|
||||
"""
|
||||
### BEGIN SOLUTION
|
||||
# Numerical stability: subtract max to prevent overflow
|
||||
# Use Tensor operations to preserve gradient flow!
|
||||
x_max_data = np.max(x.data, axis=dim, keepdims=True)
|
||||
x_max = Tensor(x_max_data, requires_grad=False) # max is not differentiable in this context
|
||||
x_shifted = x - x_max # Tensor subtraction!
|
||||
x_max = Tensor(x_max_data)
|
||||
x_shifted = x - x_max # Tensor subtraction
|
||||
|
||||
# Compute exponentials (NumPy operation, but wrapped in Tensor)
|
||||
exp_values = Tensor(np.exp(x_shifted.data), requires_grad=x_shifted.requires_grad)
|
||||
# Compute exponentials
|
||||
exp_values = Tensor(np.exp(x_shifted.data))
|
||||
|
||||
# Sum along dimension (Tensor operation)
|
||||
# Sum along dimension
|
||||
exp_sum_data = np.sum(exp_values.data, axis=dim, keepdims=True)
|
||||
exp_sum = Tensor(exp_sum_data, requires_grad=exp_values.requires_grad)
|
||||
exp_sum = Tensor(exp_sum_data)
|
||||
|
||||
# Normalize to get probabilities (Tensor division!)
|
||||
# Normalize to get probabilities
|
||||
result = exp_values / exp_sum
|
||||
return result
|
||||
### END SOLUTION
|
||||
@@ -783,10 +782,6 @@ class Softmax:
|
||||
"""Allows the activation to be called like a function."""
|
||||
return self.forward(x, dim)
|
||||
|
||||
def backward(self, grad: Tensor) -> Tensor:
|
||||
"""Compute gradient (implemented in Module 05)."""
|
||||
pass # Will implement backward pass in Module 05
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
### 🔬 Unit Test: Softmax
|
||||
|
||||
@@ -165,9 +165,9 @@ Let's build our layer system step by step. We'll implement two essential layer t
|
||||
|
||||
### Key Design Principles:
|
||||
- All methods defined INSIDE classes (no monkey-patching)
|
||||
- Parameter tensors have requires_grad=True (ready for Module 05)
|
||||
- Forward methods return new tensors, preserving immutability
|
||||
- parameters() method enables optimizer integration
|
||||
- Gradient tracking will be added in Module 05 (Autograd)
|
||||
"""
|
||||
|
||||
# %% [markdown]
|
||||
@@ -211,7 +211,7 @@ class Layer:
|
||||
Return list of trainable parameters.
|
||||
|
||||
Returns:
|
||||
List of Tensor objects with requires_grad=True
|
||||
List of Tensor objects (weights and biases)
|
||||
"""
|
||||
return [] # Base class has no parameters
|
||||
|
||||
@@ -282,7 +282,7 @@ class Linear(Layer):
|
||||
APPROACH:
|
||||
1. Create weight matrix (in_features, out_features) with Xavier scaling
|
||||
2. Create bias vector (out_features,) initialized to zeros if bias=True
|
||||
3. Set requires_grad=True for parameters (ready for Module 05)
|
||||
3. Store as Tensor objects for use in forward pass
|
||||
|
||||
EXAMPLE:
|
||||
>>> layer = Linear(784, 10) # MNIST classifier final layer
|
||||
@@ -303,12 +303,12 @@ class Linear(Layer):
|
||||
# Xavier/Glorot initialization for stable gradients
|
||||
scale = np.sqrt(XAVIER_SCALE_FACTOR / in_features)
|
||||
weight_data = np.random.randn(in_features, out_features) * scale
|
||||
self.weight = Tensor(weight_data, requires_grad=True)
|
||||
self.weight = Tensor(weight_data)
|
||||
|
||||
# Initialize bias to zeros or None
|
||||
if bias:
|
||||
bias_data = np.zeros(out_features)
|
||||
self.bias = Tensor(bias_data, requires_grad=True)
|
||||
self.bias = Tensor(bias_data)
|
||||
else:
|
||||
self.bias = None
|
||||
### END SOLUTION
|
||||
@@ -390,8 +390,6 @@ def test_unit_linear_layer():
|
||||
assert layer.out_features == 256
|
||||
assert layer.weight.shape == (784, 256)
|
||||
assert layer.bias.shape == (256,)
|
||||
assert layer.weight.requires_grad == True
|
||||
assert layer.bias.requires_grad == True
|
||||
|
||||
# Test Xavier initialization (weights should be reasonably scaled)
|
||||
weight_std = np.std(layer.weight.data)
|
||||
@@ -467,35 +465,32 @@ if __name__ == "__main__":
|
||||
|
||||
# %% [markdown]
|
||||
"""
|
||||
### 🔬 Gradient Preparation Tests: Linear Layer
|
||||
Tests to ensure Linear layer is ready for gradient-based training (Module 05).
|
||||
### 🔬 Parameter Collection Tests: Linear Layer
|
||||
Tests to ensure Linear layer parameters can be collected for optimization.
|
||||
"""
|
||||
|
||||
# %% nbgrader={"grade": true, "grade_id": "test-linear-grad-prep", "locked": true, "points": 5}
|
||||
def test_gradient_preparation_linear():
|
||||
"""🔬 Test Linear layer is ready for gradients (Module 05)."""
|
||||
print("🔬 Gradient Preparation Test: Linear Layer...")
|
||||
# %% nbgrader={"grade": true, "grade_id": "test-linear-params", "locked": true, "points": 5}
|
||||
def test_parameter_collection_linear():
|
||||
"""🔬 Test Linear layer parameter collection."""
|
||||
print("🔬 Parameter Collection Test: Linear Layer...")
|
||||
|
||||
layer = Linear(10, 5)
|
||||
|
||||
# Verify requires_grad is set
|
||||
assert layer.weight.requires_grad == True, "Weight should require gradients"
|
||||
assert layer.bias.requires_grad == True, "Bias should require gradients"
|
||||
|
||||
# Verify gradient placeholders exist (even if None initially)
|
||||
assert hasattr(layer.weight, 'grad'), "Weight should have grad attribute"
|
||||
assert hasattr(layer.bias, 'grad'), "Bias should have grad attribute"
|
||||
|
||||
# Verify parameter collection works
|
||||
params = layer.parameters()
|
||||
assert len(params) == 2, "Should return 2 parameters"
|
||||
assert all(p.requires_grad for p in params), "All parameters should require gradients"
|
||||
assert len(params) == 2, "Should return 2 parameters (weight and bias)"
|
||||
assert params[0].shape == (10, 5), "First param should be weight"
|
||||
assert params[1].shape == (5,), "Second param should be bias"
|
||||
|
||||
print("✅ Layer ready for gradient-based training!")
|
||||
# Test layer without bias
|
||||
layer_no_bias = Linear(10, 5, bias=False)
|
||||
params_no_bias = layer_no_bias.parameters()
|
||||
assert len(params_no_bias) == 1, "Should return 1 parameter (weight only)"
|
||||
|
||||
print("✅ Parameter collection works correctly!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_gradient_preparation_linear()
|
||||
|
||||
test_parameter_collection_linear()
|
||||
|
||||
|
||||
# %% [markdown]
|
||||
@@ -596,7 +591,7 @@ class Dropout(Layer):
|
||||
|
||||
APPROACH:
|
||||
1. If training=False or p=0, return input unchanged
|
||||
2. If p=1, return zeros (preserve requires_grad)
|
||||
2. If p=1, return zeros
|
||||
3. Otherwise: create random mask, apply it, scale by 1/(1-p)
|
||||
|
||||
EXAMPLE:
|
||||
@@ -616,8 +611,8 @@ class Dropout(Layer):
|
||||
return x
|
||||
|
||||
if self.p == DROPOUT_MAX_PROB:
|
||||
# Drop everything (preserve requires_grad for gradient flow)
|
||||
return Tensor(np.zeros_like(x.data), requires_grad=x.requires_grad)
|
||||
# Drop everything
|
||||
return Tensor(np.zeros_like(x.data))
|
||||
|
||||
# During training, apply dropout
|
||||
keep_prob = 1.0 - self.p
|
||||
@@ -625,9 +620,9 @@ class Dropout(Layer):
|
||||
# Create random mask: True where we keep elements
|
||||
mask = np.random.random(x.data.shape) < keep_prob
|
||||
|
||||
# Apply mask and scale using Tensor operations to preserve gradients!
|
||||
mask_tensor = Tensor(mask.astype(np.float32), requires_grad=False) # Mask doesn't need gradients
|
||||
scale = Tensor(np.array(1.0 / keep_prob), requires_grad=False)
|
||||
# Apply mask and scale
|
||||
mask_tensor = Tensor(mask.astype(np.float32))
|
||||
scale = Tensor(np.array(1.0 / keep_prob))
|
||||
|
||||
# Use Tensor operations: x * mask * scale
|
||||
output = x * mask_tensor * scale
|
||||
@@ -1015,7 +1010,7 @@ def test_module():
|
||||
print("Running unit tests...")
|
||||
test_unit_linear_layer()
|
||||
test_edge_cases_linear()
|
||||
test_gradient_preparation_linear()
|
||||
test_parameter_collection_linear()
|
||||
test_unit_dropout_layer()
|
||||
|
||||
print("\nRunning integration scenarios...")
|
||||
@@ -1055,10 +1050,6 @@ def test_module():
|
||||
expected_params = 6 # 3 weights + 3 biases from 3 Linear layers
|
||||
assert len(all_params) == expected_params, f"Expected {expected_params} parameters, got {len(all_params)}"
|
||||
|
||||
# Test all parameters have requires_grad=True
|
||||
for param in all_params:
|
||||
assert param.requires_grad == True, "All parameters should have requires_grad=True"
|
||||
|
||||
# Test individual layer functionality
|
||||
test_x = Tensor(np.random.randn(4, 784))
|
||||
# Test dropout in training vs inference
|
||||
|
||||
@@ -1366,6 +1366,19 @@ def enable_autograd(quiet=False):
|
||||
# Silently return if already enabled - no need to warn
|
||||
return
|
||||
|
||||
# ===== STEP 1: Add gradient infrastructure to Tensor =====
|
||||
# Store original __init__ to extend it
|
||||
_original_init = Tensor.__init__
|
||||
|
||||
def gradient_aware_init(self, data, requires_grad=False):
|
||||
"""Extended Tensor init that supports gradient tracking."""
|
||||
_original_init(self, data)
|
||||
self.requires_grad = requires_grad
|
||||
self.grad = None
|
||||
|
||||
# Replace __init__ with gradient-aware version
|
||||
Tensor.__init__ = gradient_aware_init
|
||||
|
||||
# Store original operations
|
||||
# These are guaranteed to exist from Module 01 (Tensor class)
|
||||
_original_add = Tensor.__add__
|
||||
@@ -1379,6 +1392,19 @@ def enable_autograd(quiet=False):
|
||||
_original_transpose = Tensor.transpose
|
||||
_original_reshape = Tensor.reshape
|
||||
|
||||
# Helper to safely check requires_grad (handles tensors created before enable_autograd)
|
||||
def _get_requires_grad(tensor):
|
||||
"""Safely get requires_grad, defaulting to False for pre-autograd tensors."""
|
||||
return getattr(tensor, 'requires_grad', False) if isinstance(tensor, Tensor) else False
|
||||
|
||||
def _ensure_grad_attrs(tensor):
|
||||
"""Ensure tensor has gradient attributes (for tensors created before enable_autograd)."""
|
||||
if isinstance(tensor, Tensor):
|
||||
if not hasattr(tensor, 'requires_grad'):
|
||||
tensor.requires_grad = False
|
||||
if not hasattr(tensor, 'grad'):
|
||||
tensor.grad = None
|
||||
|
||||
# Enhanced operations that track gradients
|
||||
def tracked_add(self, other):
|
||||
"""
|
||||
@@ -1387,15 +1413,20 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original __add__ method to build computation graphs
|
||||
when requires_grad=True for any input.
|
||||
"""
|
||||
# Ensure self has gradient attributes
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Convert scalar to Tensor if needed
|
||||
if not isinstance(other, Tensor):
|
||||
other = Tensor(other)
|
||||
_ensure_grad_attrs(other)
|
||||
|
||||
# Call original operation
|
||||
result = _original_add(self, other)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad or other.requires_grad:
|
||||
if _get_requires_grad(self) or _get_requires_grad(other):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = AddBackward(self, other)
|
||||
|
||||
@@ -1408,17 +1439,21 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original __mul__ method to build computation graphs
|
||||
when requires_grad=True for any input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Convert scalar to Tensor if needed for consistency
|
||||
if not isinstance(other, Tensor):
|
||||
other_tensor = Tensor(other)
|
||||
else:
|
||||
other_tensor = other
|
||||
_ensure_grad_attrs(other_tensor)
|
||||
|
||||
# Call original operation
|
||||
result = _original_mul(self, other)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad or (isinstance(other, Tensor) and other.requires_grad):
|
||||
if _get_requires_grad(self) or _get_requires_grad(other_tensor):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = MulBackward(self, other)
|
||||
|
||||
@@ -1431,11 +1466,15 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original matmul method to build computation graphs
|
||||
when requires_grad=True for any input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
_ensure_grad_attrs(other)
|
||||
|
||||
# Call original matmul from Module 01
|
||||
result = _original_matmul(self, other)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad or other.requires_grad:
|
||||
if _get_requires_grad(self) or _get_requires_grad(other):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = MatmulBackward(self, other)
|
||||
|
||||
@@ -1448,11 +1487,14 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original transpose method to build computation graphs
|
||||
when requires_grad=True for the input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Call original transpose from Module 01
|
||||
result = _original_transpose(self, dim0, dim1)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad:
|
||||
if _get_requires_grad(self):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = TransposeBackward(self, dim0, dim1)
|
||||
|
||||
@@ -1465,13 +1507,15 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original reshape method to build computation graphs
|
||||
when requires_grad=True for the input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
original_shape = self.shape
|
||||
|
||||
# Call original reshape from Module 01
|
||||
result = _original_reshape(self, *shape)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad:
|
||||
if _get_requires_grad(self):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = ReshapeBackward(self, original_shape)
|
||||
|
||||
@@ -1484,15 +1528,19 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original __sub__ method to build computation graphs
|
||||
when requires_grad=True for any input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Convert scalar to Tensor if needed
|
||||
if not isinstance(other, Tensor):
|
||||
other = Tensor(other)
|
||||
_ensure_grad_attrs(other)
|
||||
|
||||
# Call original operation
|
||||
result = _original_sub(self, other)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad or other.requires_grad:
|
||||
if _get_requires_grad(self) or _get_requires_grad(other):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = SubBackward(self, other)
|
||||
|
||||
@@ -1505,15 +1553,19 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original __truediv__ method to build computation graphs
|
||||
when requires_grad=True for any input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Convert scalar to Tensor if needed
|
||||
if not isinstance(other, Tensor):
|
||||
other = Tensor(other)
|
||||
_ensure_grad_attrs(other)
|
||||
|
||||
# Call original operation
|
||||
result = _original_div(self, other)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad or other.requires_grad:
|
||||
if _get_requires_grad(self) or _get_requires_grad(other):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = DivBackward(self, other)
|
||||
|
||||
@@ -1526,11 +1578,14 @@ def enable_autograd(quiet=False):
|
||||
Enhances the original __getitem__ method to build computation graphs
|
||||
when requires_grad=True for the input.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Call original __getitem__ from Module 01
|
||||
result = _original_getitem(self, key)
|
||||
_ensure_grad_attrs(result)
|
||||
|
||||
# Track gradient if needed
|
||||
if self.requires_grad:
|
||||
if _get_requires_grad(self):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = SliceBackward(self, key)
|
||||
|
||||
@@ -1543,10 +1598,12 @@ def enable_autograd(quiet=False):
|
||||
Creates a new sum method that builds computation graphs
|
||||
when requires_grad=True.
|
||||
"""
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
result_data = np.sum(self.data, axis=axis, keepdims=keepdims)
|
||||
result = Tensor(result_data)
|
||||
|
||||
if self.requires_grad:
|
||||
if _get_requires_grad(self):
|
||||
result.requires_grad = True
|
||||
result._grad_fn = SumBackward(self)
|
||||
|
||||
@@ -1573,8 +1630,11 @@ def enable_autograd(quiet=False):
|
||||
print(x.grad) # [3.0]
|
||||
```
|
||||
"""
|
||||
# Ensure gradient attributes exist
|
||||
_ensure_grad_attrs(self)
|
||||
|
||||
# Only compute gradients if required
|
||||
if not self.requires_grad:
|
||||
if not _get_requires_grad(self):
|
||||
return
|
||||
|
||||
# Initialize gradient if not provided (for scalar outputs)
|
||||
|
||||
@@ -65,7 +65,6 @@ import numpy as np
|
||||
import time
|
||||
|
||||
from tinytorch.core.tensor import Tensor
|
||||
from tinytorch.core.autograd import Function
|
||||
|
||||
# Constants for convolution defaults
|
||||
DEFAULT_KERNEL_SIZE = 3 # Default kernel size for convolutions
|
||||
@@ -298,110 +297,6 @@ This reveals why convolution is expensive: O(B×C_out×H×W×K_h×K_w×C_in) ope
|
||||
|
||||
#| export
|
||||
|
||||
class Conv2dBackward(Function):
|
||||
"""
|
||||
Gradient computation for 2D convolution.
|
||||
|
||||
Computes gradients for Conv2d backward pass:
|
||||
- grad_input: gradient w.r.t. input (for backprop to previous layer)
|
||||
- grad_weight: gradient w.r.t. filters (for weight updates)
|
||||
- grad_bias: gradient w.r.t. bias (for bias updates)
|
||||
|
||||
This uses explicit loops to show the gradient computation, matching
|
||||
the educational approach of the forward pass.
|
||||
"""
|
||||
|
||||
def __init__(self, x, weight, bias, stride, padding, kernel_size, padded_shape):
|
||||
# Register all tensors that need gradients with autograd
|
||||
if bias is not None:
|
||||
super().__init__(x, weight, bias)
|
||||
else:
|
||||
super().__init__(x, weight)
|
||||
self.x = x
|
||||
self.weight = weight
|
||||
self.bias = bias
|
||||
self.stride = stride
|
||||
self.padding = padding
|
||||
self.kernel_size = kernel_size
|
||||
self.padded_shape = padded_shape
|
||||
|
||||
def apply(self, grad_output):
|
||||
"""
|
||||
Compute gradients for convolution inputs and parameters.
|
||||
|
||||
Args:
|
||||
grad_output: Gradient flowing back from next layer
|
||||
Shape: (batch_size, out_channels, out_height, out_width)
|
||||
|
||||
Returns:
|
||||
Tuple of (grad_input, grad_weight, grad_bias)
|
||||
"""
|
||||
batch_size, out_channels, out_height, out_width = grad_output.shape
|
||||
_, in_channels, in_height, in_width = self.x.shape
|
||||
kernel_h, kernel_w = self.kernel_size
|
||||
|
||||
# Apply padding to input if needed (for gradient computation)
|
||||
if self.padding > 0:
|
||||
padded_input = np.pad(self.x.data,
|
||||
((0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)),
|
||||
mode='constant', constant_values=0)
|
||||
else:
|
||||
padded_input = self.x.data
|
||||
|
||||
# Initialize gradients
|
||||
grad_input_padded = np.zeros_like(padded_input)
|
||||
grad_weight = np.zeros_like(self.weight.data)
|
||||
grad_bias = None if self.bias is None else np.zeros_like(self.bias.data)
|
||||
|
||||
# Compute gradients using explicit loops (educational approach)
|
||||
for b in range(batch_size):
|
||||
for out_ch in range(out_channels):
|
||||
for out_h in range(out_height):
|
||||
for out_w in range(out_width):
|
||||
# Position in input
|
||||
in_h_start = out_h * self.stride
|
||||
in_w_start = out_w * self.stride
|
||||
|
||||
# Gradient value flowing back to this position
|
||||
grad_val = grad_output[b, out_ch, out_h, out_w]
|
||||
|
||||
# Distribute gradient to weight and input
|
||||
for k_h in range(kernel_h):
|
||||
for k_w in range(kernel_w):
|
||||
for in_ch in range(in_channels):
|
||||
# Input position
|
||||
in_h = in_h_start + k_h
|
||||
in_w = in_w_start + k_w
|
||||
|
||||
# Gradient w.r.t. weight
|
||||
grad_weight[out_ch, in_ch, k_h, k_w] += (
|
||||
padded_input[b, in_ch, in_h, in_w] * grad_val
|
||||
)
|
||||
|
||||
# Gradient w.r.t. input
|
||||
grad_input_padded[b, in_ch, in_h, in_w] += (
|
||||
self.weight.data[out_ch, in_ch, k_h, k_w] * grad_val
|
||||
)
|
||||
|
||||
# Compute gradient w.r.t. bias (sum over batch and spatial dimensions)
|
||||
if grad_bias is not None:
|
||||
for out_ch in range(out_channels):
|
||||
grad_bias[out_ch] = grad_output[:, out_ch, :, :].sum()
|
||||
|
||||
# Remove padding from input gradient
|
||||
if self.padding > 0:
|
||||
grad_input = grad_input_padded[:, :,
|
||||
self.padding:-self.padding,
|
||||
self.padding:-self.padding]
|
||||
else:
|
||||
grad_input = grad_input_padded
|
||||
|
||||
# Return gradients as numpy arrays (autograd system handles storage)
|
||||
# Following TinyTorch protocol: return (grad_input, grad_weight, grad_bias)
|
||||
return grad_input, grad_weight, grad_bias
|
||||
|
||||
#| export
|
||||
|
||||
class Conv2d:
|
||||
"""
|
||||
2D Convolution layer for spatial feature extraction.
|
||||
@@ -458,12 +353,11 @@ class Conv2d:
|
||||
|
||||
# Weight shape: (out_channels, in_channels, kernel_h, kernel_w)
|
||||
self.weight = Tensor(np.random.normal(0, std,
|
||||
(out_channels, in_channels, kernel_h, kernel_w)),
|
||||
requires_grad=True)
|
||||
(out_channels, in_channels, kernel_h, kernel_w)))
|
||||
|
||||
# Bias initialization
|
||||
if bias:
|
||||
self.bias = Tensor(np.zeros(out_channels), requires_grad=True)
|
||||
self.bias = Tensor(np.zeros(out_channels))
|
||||
else:
|
||||
self.bias = None
|
||||
### END SOLUTION
|
||||
@@ -558,18 +452,7 @@ class Conv2d:
|
||||
for out_ch in range(out_channels):
|
||||
output[:, out_ch, :, :] += self.bias.data[out_ch]
|
||||
|
||||
# Return Tensor with gradient tracking enabled
|
||||
result = Tensor(output, requires_grad=(x.requires_grad or self.weight.requires_grad))
|
||||
|
||||
# Attach backward function for gradient computation (following TinyTorch protocol)
|
||||
if result.requires_grad:
|
||||
result._grad_fn = Conv2dBackward(
|
||||
x, self.weight, self.bias,
|
||||
self.stride, self.padding, self.kernel_size,
|
||||
padded_input.shape
|
||||
)
|
||||
|
||||
return result
|
||||
return Tensor(output)
|
||||
### END SOLUTION
|
||||
|
||||
def parameters(self):
|
||||
@@ -799,84 +682,6 @@ For input (1, 64, 224, 224) with 2×2 pooling:
|
||||
|
||||
#| export
|
||||
|
||||
class MaxPool2dBackward(Function):
|
||||
"""
|
||||
Gradient computation for 2D max pooling.
|
||||
|
||||
Max pooling gradients flow only to the positions that were selected
|
||||
as the maximum in the forward pass.
|
||||
"""
|
||||
|
||||
def __init__(self, x, output_shape, kernel_size, stride, padding):
|
||||
super().__init__(x)
|
||||
self.x = x
|
||||
self.output_shape = output_shape
|
||||
self.kernel_size = kernel_size
|
||||
self.stride = stride
|
||||
self.padding = padding
|
||||
# Store max positions for gradient routing
|
||||
self.max_positions = {}
|
||||
|
||||
def apply(self, grad_output):
|
||||
"""
|
||||
Route gradients back to max positions.
|
||||
|
||||
Args:
|
||||
grad_output: Gradient from next layer
|
||||
|
||||
Returns:
|
||||
Gradient w.r.t. input
|
||||
"""
|
||||
batch_size, channels, in_height, in_width = self.x.shape
|
||||
_, _, out_height, out_width = self.output_shape
|
||||
kernel_h, kernel_w = self.kernel_size
|
||||
|
||||
# Apply padding if needed
|
||||
if self.padding > 0:
|
||||
padded_input = np.pad(self.x.data,
|
||||
((0, 0), (0, 0), (self.padding, self.padding), (self.padding, self.padding)),
|
||||
mode='constant', constant_values=-np.inf)
|
||||
grad_input_padded = np.zeros_like(padded_input)
|
||||
else:
|
||||
padded_input = self.x.data
|
||||
grad_input_padded = np.zeros_like(self.x.data)
|
||||
|
||||
# Route gradients to max positions
|
||||
for b in range(batch_size):
|
||||
for c in range(channels):
|
||||
for out_h in range(out_height):
|
||||
for out_w in range(out_width):
|
||||
in_h_start = out_h * self.stride
|
||||
in_w_start = out_w * self.stride
|
||||
|
||||
# Find max position in this window
|
||||
max_val = -np.inf
|
||||
max_h, max_w = 0, 0
|
||||
for k_h in range(kernel_h):
|
||||
for k_w in range(kernel_w):
|
||||
in_h = in_h_start + k_h
|
||||
in_w = in_w_start + k_w
|
||||
val = padded_input[b, c, in_h, in_w]
|
||||
if val > max_val:
|
||||
max_val = val
|
||||
max_h, max_w = in_h, in_w
|
||||
|
||||
# Route gradient to max position
|
||||
grad_input_padded[b, c, max_h, max_w] += grad_output[b, c, out_h, out_w]
|
||||
|
||||
# Remove padding
|
||||
if self.padding > 0:
|
||||
grad_input = grad_input_padded[:, :,
|
||||
self.padding:-self.padding,
|
||||
self.padding:-self.padding]
|
||||
else:
|
||||
grad_input = grad_input_padded
|
||||
|
||||
# Return as tuple (following Function protocol)
|
||||
return (grad_input,)
|
||||
|
||||
#| export
|
||||
|
||||
class MaxPool2d:
|
||||
"""
|
||||
2D Max Pooling layer for spatial dimension reduction.
|
||||
@@ -1000,16 +805,7 @@ class MaxPool2d:
|
||||
# Store result
|
||||
output[b, c, out_h, out_w] = max_val
|
||||
|
||||
# Return Tensor with gradient tracking
|
||||
result = Tensor(output, requires_grad=x.requires_grad)
|
||||
|
||||
# Attach backward function for gradient computation
|
||||
if result.requires_grad:
|
||||
result._grad_fn = MaxPool2dBackward(
|
||||
x, output.shape, self.kernel_size, self.stride, self.padding
|
||||
)
|
||||
|
||||
return result
|
||||
return Tensor(output)
|
||||
### END SOLUTION
|
||||
|
||||
def parameters(self):
|
||||
|
||||
@@ -66,7 +66,6 @@ from typing import List, Optional, Tuple
|
||||
|
||||
# Import from previous modules - following dependency chain
|
||||
from tinytorch.core.tensor import Tensor
|
||||
from tinytorch.core.autograd import EmbeddingBackward
|
||||
|
||||
# Constants for memory calculations
|
||||
BYTES_PER_FLOAT32 = 4 # Standard float32 size in bytes
|
||||
@@ -278,8 +277,7 @@ class Embedding:
|
||||
# Xavier initialization for better gradient flow
|
||||
limit = math.sqrt(6.0 / (vocab_size + embed_dim))
|
||||
self.weight = Tensor(
|
||||
np.random.uniform(-limit, limit, (vocab_size, embed_dim)),
|
||||
requires_grad=True
|
||||
np.random.uniform(-limit, limit, (vocab_size, embed_dim))
|
||||
)
|
||||
|
||||
def forward(self, indices: Tensor) -> Tensor:
|
||||
@@ -303,14 +301,7 @@ class Embedding:
|
||||
# This is equivalent to one-hot multiplication but much more efficient
|
||||
embedded = self.weight.data[indices.data.astype(int)]
|
||||
|
||||
# Create result tensor with gradient tracking
|
||||
result = Tensor(embedded, requires_grad=self.weight.requires_grad)
|
||||
|
||||
# Attach backward function for gradient computation (following TinyTorch protocol)
|
||||
if result.requires_grad:
|
||||
result._grad_fn = EmbeddingBackward(self.weight, indices)
|
||||
|
||||
return result
|
||||
return Tensor(embedded)
|
||||
|
||||
def __call__(self, indices: Tensor) -> Tensor:
|
||||
"""Allows the embedding to be called like a function."""
|
||||
@@ -355,7 +346,7 @@ def test_unit_embedding():
|
||||
|
||||
# Test 4: Parameter access
|
||||
params = embed.parameters()
|
||||
assert all(p.requires_grad for p in params), "All parameters should require gradients"
|
||||
assert len(params) == 1, "Should have 1 parameter"
|
||||
|
||||
print("✅ Embedding layer works correctly!")
|
||||
|
||||
@@ -451,8 +442,7 @@ class PositionalEncoding:
|
||||
# Smaller initialization than token embeddings since these are additive
|
||||
limit = math.sqrt(2.0 / embed_dim)
|
||||
self.position_embeddings = Tensor(
|
||||
np.random.uniform(-limit, limit, (max_seq_len, embed_dim)),
|
||||
requires_grad=True
|
||||
np.random.uniform(-limit, limit, (max_seq_len, embed_dim))
|
||||
)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
@@ -481,19 +471,13 @@ class PositionalEncoding:
|
||||
)
|
||||
|
||||
# Slice position embeddings for this sequence length using Tensor slicing
|
||||
# This now preserves gradient flow (as of Module 01 update with __getitem__)
|
||||
pos_embeddings = self.position_embeddings[:seq_len] # (seq_len, embed_dim) - gradients preserved!
|
||||
pos_embeddings = self.position_embeddings[:seq_len] # (seq_len, embed_dim)
|
||||
|
||||
# Reshape to add batch dimension: (1, seq_len, embed_dim)
|
||||
# Need to use .data for reshaping temporarily, then wrap in Tensor
|
||||
pos_data = pos_embeddings.data[np.newaxis, :, :]
|
||||
pos_embeddings_batched = Tensor(pos_data, requires_grad=pos_embeddings.requires_grad)
|
||||
pos_embeddings_batched = Tensor(pos_data)
|
||||
|
||||
# Copy gradient function if it exists (to preserve backward connection)
|
||||
if hasattr(pos_embeddings, '_grad_fn') and pos_embeddings._grad_fn is not None:
|
||||
pos_embeddings_batched._grad_fn = pos_embeddings._grad_fn
|
||||
|
||||
# Add positional information - gradients flow through both x and pos_embeddings!
|
||||
# Add positional information
|
||||
result = x + pos_embeddings_batched
|
||||
|
||||
return result
|
||||
@@ -931,7 +915,7 @@ class EmbeddingLayer:
|
||||
|
||||
# Reshape to add batch dimension
|
||||
pos_data = pos_embeddings.data[np.newaxis, :, :]
|
||||
pos_embeddings_batched = Tensor(pos_data, requires_grad=False) # Sinusoidal are fixed
|
||||
pos_embeddings_batched = Tensor(pos_data) # Sinusoidal are fixed
|
||||
|
||||
output = token_embeds + pos_embeddings_batched
|
||||
else:
|
||||
|
||||
@@ -327,7 +327,7 @@ def scaled_dot_product_attention(Q: Tensor, K: Tensor, V: Tensor, mask: Optional
|
||||
# Ensure mask is broadcastable
|
||||
mask_data = mask.data
|
||||
adder_mask = (1.0 - mask_data) * MASK_VALUE
|
||||
adder_mask_tensor = Tensor(adder_mask, requires_grad=False)
|
||||
adder_mask_tensor = Tensor(adder_mask)
|
||||
scores = scores + adder_mask_tensor
|
||||
|
||||
# Step 5: Apply softmax to get attention weights
|
||||
@@ -648,7 +648,7 @@ class MultiHeadAttention:
|
||||
# This allows the mask to broadcast across all attention heads
|
||||
batch_size_mask, seq_len_mask, _ = mask.shape
|
||||
mask_data = mask.data.reshape(batch_size_mask, 1, seq_len_mask, seq_len_mask)
|
||||
mask_reshaped = Tensor(mask_data, requires_grad=False)
|
||||
mask_reshaped = Tensor(mask_data)
|
||||
|
||||
attended, _ = scaled_dot_product_attention(Q, K, V, mask=mask_reshaped)
|
||||
|
||||
|
||||
@@ -435,8 +435,8 @@ class LayerNorm:
|
||||
self.eps = eps
|
||||
|
||||
# Learnable parameters: scale and shift
|
||||
self.gamma = Tensor(np.ones(normalized_shape), requires_grad=True) # Scale parameter
|
||||
self.beta = Tensor(np.zeros(normalized_shape), requires_grad=True) # Shift parameter
|
||||
self.gamma = Tensor(np.ones(normalized_shape)) # Scale parameter
|
||||
self.beta = Tensor(np.zeros(normalized_shape)) # Shift parameter
|
||||
### END SOLUTION
|
||||
|
||||
def forward(self, x):
|
||||
@@ -465,10 +465,8 @@ class LayerNorm:
|
||||
diff = x - mean
|
||||
variance = (diff * diff).mean(axis=-1, keepdims=True)
|
||||
|
||||
# Normalize - use Tensor operations to preserve gradients!
|
||||
# Add eps as a Tensor for proper gradient flow
|
||||
eps_tensor = Tensor(np.array(self.eps), requires_grad=False)
|
||||
std = Tensor(np.sqrt(variance.data + self.eps), requires_grad=variance.requires_grad)
|
||||
# Normalize
|
||||
std = Tensor(np.sqrt(variance.data + self.eps))
|
||||
normalized = (x - mean) / std
|
||||
|
||||
# Apply learnable transformation
|
||||
|
||||
Reference in New Issue
Block a user