diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 00000000..e69de29b diff --git a/.github/workflows/classroom.yml b/.github/workflows/classroom.yml new file mode 100644 index 00000000..694e0c44 --- /dev/null +++ b/.github/workflows/classroom.yml @@ -0,0 +1,67 @@ +name: Autograding Tests +'on': +- workflow_dispatch +- repository_dispatch +permissions: + checks: write + actions: read + contents: read +jobs: + run-autograding-tests: + runs-on: ubuntu-latest + if: github.actor != 'github-classroom[bot]' + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup + id: setup + uses: classroom-resources/autograding-command-grader@v1 + with: + test-name: Setup + setup-command: sudo -H pip3 install -qr requirements.txt; sudo -H pip3 install + flake8==5.0.4 + command: flake8 --ignore "N801, E203, E266, E501, W503, F812, E741, N803, + N802, N806" minitorch/ tests/ project/; mypy minitorch/* + timeout: 10 + - name: Task 0.1 + id: task-0-1 + uses: classroom-resources/autograding-command-grader@v1 + with: + test-name: Task 0.1 + setup-command: sudo -H pip3 install -qr requirements.txt + command: pytest -m task0_1 + timeout: 10 + - name: Task 0.2 + id: task-0-2 + uses: classroom-resources/autograding-command-grader@v1 + with: + test-name: Task 0.2 + setup-command: sudo -H pip3 install -qr requirements.txt + command: pytest -m task0_2 + timeout: 10 + - name: Task 0.3 + id: task-0-3 + uses: classroom-resources/autograding-command-grader@v1 + with: + test-name: Task 0.3 + setup-command: sudo -H pip3 install -qr requirements.txt + command: pytest -m task0_3 + timeout: 10 + - name: Task 0.4 + id: task-0-4 + uses: classroom-resources/autograding-command-grader@v1 + with: + test-name: Task 0.4 + setup-command: sudo -H pip3 install -qr requirements.txt + command: pytest -m task0_4 + timeout: 10 + - name: Autograding Reporter + uses: classroom-resources/autograding-grading-reporter@v1 + env: + SETUP_RESULTS: "${{steps.setup.outputs.result}}" + TASK-0-1_RESULTS: "${{steps.task-0-1.outputs.result}}" + TASK-0-2_RESULTS: "${{steps.task-0-2.outputs.result}}" + TASK-0-3_RESULTS: "${{steps.task-0-3.outputs.result}}" + TASK-0-4_RESULTS: "${{steps.task-0-4.outputs.result}}" + with: + runners: setup,task-0-1,task-0-2,task-0-3,task-0-4 diff --git a/README.md b/README.md index 62e4d6ba..1735329a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Open in Visual Studio Code](https://classroom.github.com/assets/open-in-vscode-2e0aaae1b6195c2367325f4f02e2d04e9abb55f0b24a779b69b11b9e10269abc.svg)](https://classroom.github.com/online_ide?assignment_repo_id=18378754&assignment_repo_type=AssignmentRepo) # MiniTorch Module 0 diff --git a/minitorch/module.py b/minitorch/module.py index 0a66058c..e3113da2 100644 --- a/minitorch/module.py +++ b/minitorch/module.py @@ -31,13 +31,21 @@ def modules(self) -> Sequence[Module]: def train(self) -> None: """Set the mode of this module and all descendent modules to `train`.""" + self.training = True + for module in self._modules.values(): + module.train() + # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + # raise NotImplementedError("Need to implement for Task 0.4") def eval(self) -> None: """Set the mode of this module and all descendent modules to `eval`.""" + self.training = False + for module in self._modules.values(): + module.eval() + # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + # raise NotImplementedError("Need to implement for Task 0.4") def named_parameters(self) -> Sequence[Tuple[str, Parameter]]: """Collect all the parameters of this module and its descendents. @@ -47,13 +55,24 @@ def named_parameters(self) -> Sequence[Tuple[str, Parameter]]: The name and `Parameter` of each ancestor parameter. """ + named_params = list(self._parameters.items()) + for module_name, module in self._modules.items(): + for param_name, param in module.named_parameters(): + named_params.append((f"{module_name}.{param_name}", param)) + return named_params + # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + # raise NotImplementedError("Need to implement for Task 0.4") def parameters(self) -> Sequence[Parameter]: """Enumerate over all the parameters of this module and its descendents.""" + params = list(self._parameters.values()) + for module in self._modules.values(): + params.extend(module.parameters()) + return params + # TODO: Implement for Task 0.4. - raise NotImplementedError("Need to implement for Task 0.4") + # raise NotImplementedError("Need to implement for Task 0.4") 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..5d0c0fb6 100644 --- a/minitorch/operators.py +++ b/minitorch/operators.py @@ -34,6 +34,82 @@ # 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, tol: float = 1e-2) -> bool: + """Checks if two numbers are close in value.""" + return abs(x - y) < tol + +def sigmoid(x: float) -> float: + """Calculates the sigmoid function.""" + if x >= 0: + return 1 / (1 + math.exp(-x)) + else: + exp_x = math.exp(x) + return exp_x / (1 + 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.""" + if x <= 0: + raise ValueError("log input must be positive") + 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.""" + if x == 0: + raise ValueError("Cannot divide by zero") + return 1 / x + +def log_back(x: float, d: float) -> float: + """Computes the derivative of log times a second argument.""" + if x <= 0: + raise ValueError("log_back input must be positive") + return d / x + +def inv_back(x: float, d: float) -> float: + """Computes the derivative of reciprocal times a second argument.""" + if x == 0: + raise ValueError("Cannot divide by zero") + return -d / (x * x) + +def relu_back(x: float, d: float) -> float: + """Computes the derivative of ReLU times a second argument.""" + return d if x > 0 else 0 + # ## Task 0.3 @@ -52,3 +128,27 @@ # TODO: Implement for Task 0.3. + +def map(fn: Callable[[float], float], iter: Iterable[float]) -> Iterable[float]: + return [fn(x) for x in iter] + +def zipWith(fn: Callable[[float, float], float], iter1: Iterable[float], iter2: Iterable[float]) -> Iterable[float]: + return [fn(x, y) for x, y in zip(iter1, iter2)] + +def reduce(fn: Callable[[float, float], float], iter: Iterable[float], start: float) -> float: + result = start + for x in iter: + result = fn(result, x) + return result + +def negList(lst: Iterable[float]) -> Iterable[float]: + return map(neg, lst) + +def addLists(list1: Iterable[float], list2: Iterable[float]) -> Iterable[float]: + return zipWith(add, list1, list2) + +def sum(lst: Iterable[float]) -> float: + return reduce(add, lst, 0) + +def prod(lst: Iterable[float]) -> float: + return reduce(mul, lst, 1) diff --git a/tests/test_operators.py b/tests/test_operators.py index f6e555af..7cb407f0 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -101,6 +101,9 @@ def test_eq(a: float) -> None: @pytest.mark.task0_2 @given(small_floats) def test_sigmoid(a: float) -> None: + assert 0.0 <= sigmoid(a) <= 1.0 + assert_close(1.0 - sigmoid(a), sigmoid(-a)) + assert_close(sigmoid(0), 0.5) """Check properties of the sigmoid function, specifically * It is always between 0.0 and 1.0. * one minus sigmoid is the same as sigmoid of the negative @@ -108,40 +111,46 @@ 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") + # raise NotImplementedError("Need to implement for Task 0.2") @pytest.mark.task0_2 @given(small_floats, small_floats, small_floats) def test_transitive(a: float, b: float, c: float) -> None: + if lt(a, b) and lt(b, c): + assert lt(a, c) """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") + # raise NotImplementedError("Need to implement for Task 0.2") @pytest.mark.task0_2 def test_symmetric() -> None: + assert mul(2, 3) == mul(3, 2) + """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") + # raise NotImplementedError("Need to implement for Task 0.2") @pytest.mark.task0_2 def test_distribute() -> None: - r"""Write a test that ensures that your operators distribute, i.e. + assert mul(2, add(3, 4)) == add(mul(2, 3), mul(2, 4)) + """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") + # raise NotImplementedError("Need to implement for Task 0.2") @pytest.mark.task0_2 def test_other() -> None: + assert eq(5, 5) """Write a test that ensures some other property holds for your functions.""" # TODO: Implement for Task 0.2. - raise NotImplementedError("Need to implement for Task 0.2") + # raise NotImplementedError("Need to implement for Task 0.2") # ## Task 0.3 - Higher-order functions @@ -169,7 +178,7 @@ def test_sum_distribute(ls1: List[float], ls2: List[float]) -> None: is the same as the sum of each element of `ls1` plus each element of `ls2`. """ # TODO: Implement for Task 0.3. - raise NotImplementedError("Need to implement for Task 0.3") + # raise NotImplementedError("Need to implement for Task 0.3") @pytest.mark.task0_3