diff --git a/minitorch/module.py b/minitorch/module.py index 0a66058c..b05cd1d1 100644 --- a/minitorch/module.py +++ b/minitorch/module.py @@ -32,12 +32,16 @@ def modules(self) -> Sequence[Module]: def train(self) -> None: """Set the mode of this module and all descendent modules to `train`.""" # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + self.training = True + for module in self.modules(): + module.train() def eval(self) -> None: """Set the mode of this module and all descendent modules to `eval`.""" # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + self.training = False + for module in self.modules(): + module.eval() def named_parameters(self) -> Sequence[Tuple[str, Parameter]]: """Collect all the parameters of this module and its descendents. @@ -48,12 +52,21 @@ def named_parameters(self) -> Sequence[Tuple[str, Parameter]]: """ # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + parameters_list = [] + for param_name, param_value in self._parameters.items(): + parameters_list.append((param_name, param_value)) + for module_name, module in self._modules.items(): + for sub_param_name, sub_param_value in module.named_parameters(): + parameters_list.append((f"{module_name}.{sub_param_name}", sub_param_value)) + return parameters_list def parameters(self) -> Sequence[Parameter]: """Enumerate over all the parameters of this module and its descendents.""" # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + params = list(self._parameters.values()) + for module in self.modules(): + params.extend(module.parameters()) + return params def add_parameter(self, k: str, v: Any) -> Parameter: """Manually add a parameter. Useful helper for scalar parameters. diff --git a/minitorch/operators.py b/minitorch/operators.py index 37cc7c09..18b55510 100644 --- a/minitorch/operators.py +++ b/minitorch/operators.py @@ -5,6 +5,7 @@ # ## Task 0.1 from typing import Callable, Iterable + # # Implementation of a prelude of elementary functions. @@ -34,6 +35,88 @@ # TODO: Implement for Task 0.1. +def mul(x: float, y: float) -> float: + """Multiplies two numbers.""" + return x * y + + +def id(x: float) -> float: + """Returns the input unchanged.""" + return x + + +def add(x: float, y: float) -> float: + """Adds two numbers.""" + return x + y + + +def neg(x: float) -> float: + """Negates a number.""" + return -x + + +def lt(x: float, y: float) -> bool: + """Checks if one number is less than another.""" + return x < y + + +def eq(x: float, y: float) -> bool: + """Checks if two numbers are equal.""" + return x == y + + +def max(x: float, y: float) -> float: + """Returns the larger of two numbers.""" + return x if x > y else y + + +def is_close(x: float, y: float) -> bool: + """Checks if two numbers are close in value.""" + return abs(x - y) < 1e-2 + + +def sigmoid(x: float) -> float: + """Calculates the sigmoid function.""" + if x >= 0: + return 1.0 / (1.0 + math.exp(-x)) + else: + return math.exp(x) / (1.0 + math.exp(x)) + + +def relu(x: float) -> float: + """Applies the ReLU activation function.""" + return max(0, x) + + +def log(x: float) -> float: + """Calculates the natural logarithm.""" + return math.log(x) + + +def exp(x: float) -> float: + """Calculates the exponential function.""" + return math.exp(x) + + +def inv(x: float) -> float: + """Calculates the reciprocal.""" + return 1 / x + + +def log_back(x: float, y: float) -> float: + """Computes the derivative of log times a second arg.""" + return y / x + + +def inv_back(x: float, y: float) -> float: + """Computes the derivative of reciprocal times a second arg.""" + return -y / (x ** 2) + + +def relu_back(x: float, y: float) -> float: + """Computes the derivative of ReLU times a second arg.""" + return y if x > 0 else 0 + # ## Task 0.3 @@ -50,5 +133,41 @@ # - sum: sum lists # - prod: take the product of lists +def map(func: Callable[[float], float], iterable: Iterable[float]) -> Iterable[float]: + """Applies a given function to each element of an iterable""" + return [func(item) for item in iterable] + + +def zipWith(func: Callable[[float, float], float], iterable1: Iterable[float], iterable2: Iterable[float]) -> Iterable[ + float]: + """Combines elements from two iterables using a given function.""" + return [func(a, b) for a, b in zip(iterable1, iterable2)] + + +def reduce(func: Callable[[float, float], float], iterable: Iterable[float], initial: float) -> float: + """Reduces an iterable to a single float value using a given function.""" + result = initial + for item in iterable: + result = func(result, item) + return result + + +def negList(iterable: Iterable[float]) -> Iterable[float]: + """Negate all elements in a list using map""" + return map(neg, iterable) + + +def addLists(iterable1: Iterable[float], iterable2: Iterable[float]) -> Iterable[float]: + """Add corresponding elements from two lists using zipWith""" + return zipWith(add, iterable1, iterable2) + + +def sum(iterable: Iterable[float]) -> float: + """Sum all elements in a list using reduce""" + return reduce(add, iterable, 0) + +def prod(iterable: Iterable[float]) -> float: + """Calculate the product of all elements in a list using reduce""" + return reduce(mul, iterable, 1) # TODO: Implement for Task 0.3. diff --git a/tests/test_operators.py b/tests/test_operators.py index f6e555af..3c68a9d1 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -10,9 +10,11 @@ add, addLists, eq, + exp, id, inv, inv_back, + log, log_back, lt, max, @@ -22,11 +24,12 @@ prod, relu, relu_back, - sigmoid, + sigmoid ) from .strategies import assert_close, small_floats + # ## Task 0.1 Basic hypothesis tests. @@ -108,7 +111,10 @@ def test_sigmoid(a: float) -> None: * It is strictly increasing. """ # TODO: Implement for Task 0.2. - raise NotImplementedError("Need to implement for Task 0.2") + assert 0.0 <= sigmoid(a) <= 1.0 + assert abs((1.0 - sigmoid(a)) - sigmoid(-a)) < 1e-6 + assert abs(sigmoid(0) - 0.5) < 1e-6 + assert sigmoid(a) <= sigmoid(a + 1) @pytest.mark.task0_2 @@ -116,32 +122,37 @@ def test_sigmoid(a: float) -> None: def test_transitive(a: float, b: float, c: float) -> None: """Test the transitive property of less-than (a < b and b < c implies a < c)""" # TODO: Implement for Task 0.2. - raise NotImplementedError("Need to implement for Task 0.2") + if lt(a, b) and lt(b, c): + assert (lt(a, c)) @pytest.mark.task0_2 -def test_symmetric() -> None: +@given(small_floats, small_floats) +def test_symmetric(a: float, b: float) -> None: """Write a test that ensures that :func:`minitorch.operators.mul` is symmetric, i.e. gives the same value regardless of the order of its input. """ # TODO: Implement for Task 0.2. - raise NotImplementedError("Need to implement for Task 0.2") + assert_close(mul(a, b), mul(b, a)) @pytest.mark.task0_2 -def test_distribute() -> None: +@given(small_floats, small_floats, small_floats) +def test_distribute(x: float, y: float, z: float) -> None: r"""Write a test that ensures that your operators distribute, i.e. :math:`z \times (x + y) = z \times x + z \times y` """ # TODO: Implement for Task 0.2. - raise NotImplementedError("Need to implement for Task 0.2") + assert_close(mul(z, add(x, y)), add(mul(z, x), mul(z, y))) @pytest.mark.task0_2 -def test_other() -> None: +@given(small_floats, small_floats) +def test_other(a: float, b: float) -> None: """Write a test that ensures some other property holds for your functions.""" + # checks that eq output is the same no matter the order of args # TODO: Implement for Task 0.2. - raise NotImplementedError("Need to implement for Task 0.2") + assert (eq(a, b) == eq(b, a)) # ## Task 0.3 - Higher-order functions @@ -168,8 +179,8 @@ def test_sum_distribute(ls1: List[float], ls2: List[float]) -> None: """Write a test that ensures that the sum of `ls1` plus the sum of `ls2` is the same as the sum of each element of `ls1` plus each element of `ls2`. """ + assert_close(sum(ls1) + sum(ls2), sum(addLists(ls1, ls2))) # TODO: Implement for Task 0.3. - raise NotImplementedError("Need to implement for Task 0.3") @pytest.mark.task0_3 @@ -210,7 +221,7 @@ def test_one_args(fn: Tuple[str, Callable[[float], float]], t1: float) -> None: @given(small_floats, small_floats) @pytest.mark.parametrize("fn", two_arg) def test_two_args( - fn: Tuple[str, Callable[[float, float], float]], t1: float, t2: float + fn: Tuple[str, Callable[[float, float], float]], t1: float, t2: float ) -> None: name, base_fn = fn base_fn(t1, t2)