Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions nnf/layers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from nnf.layers.dense import Dense
from nnf.layers.dense import Layer
from nnf.layers.base import Layer
from nnf.layers.dropout import Dropout

__all__ = ['Dense', 'Layer']
__all__ = ['Dense', 'Layer', 'Dropout']
62 changes: 62 additions & 0 deletions nnf/layers/dropout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import numpy as np
from nnf.layers import Layer

class Dropout(Layer):
"""
Dropout layer implementing inverted dropout.

During training, it randomly sets a fraction `p` of input units to zero
at each update and scales the rest by `1 / (1 - p)` to maintain the expected output.
During inference, it passes the input through unchanged.

Attributes:
p (float): Dropout probability. Fraction of inputs to drop.
mask (np.ndarray): Binary mask used to drop/keep inputs during training.
training (bool): Flag indicating whether the layer is in training mode.
"""

def __init__(self, p: float = 0.5):
"""
Initializes the Dropout layer.

Args:
p (float): Probability of dropping a unit. Must be between 0 and 1.
"""
super().__init__()
if not (0 <= p < 1):
raise ValueError("Dropout probability p must be in the range [0, 1).")
self.p = p
self.mask = None
self.training = True

def forward(self, X: np.ndarray) -> np.ndarray:
"""
Applies dropout to the input during training, or passes it unchanged during inference.

Args:
X (np.ndarray): Input array of shape (batch_size, features).

Returns:
np.ndarray: Output after applying dropout (during training) or original input (during inference).
"""
if not self.training:
return X # No dropout during inference

self.mask = (np.random.rand(*X.shape) > self.p).astype(np.float32)
return (X * self.mask) / (1 - self.p)

def backward(self, doutput: np.ndarray) -> np.ndarray:
"""
Backward pass through the dropout layer. Only propagates gradients
through the neurons that were active during the forward pass.

Args:
doutput (np.ndarray): Upstream gradient.

Returns:
np.ndarray: Gradient of the loss with respect to the input.
"""
if self.mask is None:
raise ValueError("Must call forward() before backward().")

return (doutput * self.mask) / (1 - self.p)
63 changes: 63 additions & 0 deletions tests/test_layers/test_dropout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import numpy as np
import pytest

from nnf.layers import Dropout

@pytest.fixture
def sample_input():
return np.ones((4, 4), dtype=np.float32)

@pytest.fixture
def dropout_layer():
return Dropout()

def test_forward_training_shape_and_scale(dropout_layer, sample_input):
dropout_layer.training = True
out = dropout_layer.forward(sample_input)

assert out.shape == sample_input.shape

valid_vals = [0.0, 1.0 / (1 - dropout_layer.p)]
unique_vals = np.unique(out)
for val in unique_vals:
assert val in valid_vals

def test_forward_inference_no_change(dropout_layer, sample_input):
dropout_layer.training = False
out = dropout_layer.forward(sample_input)

np.testing.assert_array_equal(out, sample_input)

def test_mask_is_binary(dropout_layer, sample_input):
dropout_layer.training = True
_ = dropout_layer.forward(sample_input)
mask = dropout_layer.mask

assert mask is not None
assert mask.shape == sample_input.shape

unique_vals = np.unique(mask)
for val in unique_vals:
assert val in [0.0, 1.0]

def test_backward_gradient_masking(dropout_layer, sample_input):
dropout_layer.training = True
_ = dropout_layer.forward(sample_input)

upstream_grad = np.full_like(sample_input, 2.0)
dx = dropout_layer.backward(upstream_grad)

expected = (upstream_grad * dropout_layer.mask) / (1 - dropout_layer.p)
np.testing.assert_array_almost_equal(dx, expected)

def test_backward_raises_without_forward():
dropout = Dropout(p=0.5)
upstream_grad = np.ones((4, 4), dtype=np.float32)

with pytest.raises(ValueError):
dropout.backward(upstream_grad)

@pytest.mark.parametrize("invalid_p", [-0.1, 1.0, 1.5])
def test_invalid_p_raises(invalid_p):
with pytest.raises(ValueError):
Dropout(p=invalid_p)