|
6 | 6 |
|
7 | 7 | from numbers import Integral, Number
|
8 | 8 | from typing import (
|
| 9 | + TYPE_CHECKING, |
9 | 10 | Any,
|
10 | 11 | Callable,
|
11 | 12 | Dict,
|
|
66 | 67 |
|
67 | 68 | __all__ = ["Tensor", "asarray", "astensor"]
|
68 | 69 |
|
| 70 | +if TYPE_CHECKING: # pragma: no cover |
| 71 | + from mygrad.ufuncs._ufunc_creators import ufunc as mygrad_ufunc |
| 72 | + |
| 73 | + |
69 | 74 | CONSTANT_ONLY_DTYPES = (np.integer, np.bool_)
|
70 | 75 |
|
71 | 76 |
|
@@ -349,6 +354,53 @@ def astensor(
|
349 | 354 | return tensor(t, dtype=dtype, constant=constant, copy=False, ndmin=0)
|
350 | 355 |
|
351 | 356 |
|
| 357 | +_REGISTERED_UFUNC: Dict[np.ufunc, Type["mygrad_ufunc"]] = {} |
| 358 | +_REGISTERED_BOOL_ONLY_UFUNC: Set[np.ufunc] = { |
| 359 | + np.isnan, |
| 360 | + np.isfinite, |
| 361 | + np.isinf, |
| 362 | + np.isnat, |
| 363 | + np.signbit, |
| 364 | + np.logical_not, |
| 365 | + np.logical_and, |
| 366 | + np.logical_or, |
| 367 | + np.logical_xor, |
| 368 | + np.greater, |
| 369 | + np.greater_equal, |
| 370 | + np.less, |
| 371 | + np.less_equal, |
| 372 | + np.equal, |
| 373 | +} |
| 374 | + |
| 375 | +# These are ufuncs that users might mistake for being differentiable functions; |
| 376 | +# for this reason we make explicit the fact that only constant tensors are permitted |
| 377 | +# in these operations. |
| 378 | +_REGISTERED_CONST_ONLY_UFUNC = { |
| 379 | + np.floor_divide, |
| 380 | + np.remainder, |
| 381 | + np.mod, |
| 382 | + np.fmod, |
| 383 | + np.divmod, |
| 384 | + np.rint, |
| 385 | + np.sign, |
| 386 | + np.floor, |
| 387 | + np.ceil, |
| 388 | + np.trunc, |
| 389 | +} |
| 390 | + |
| 391 | + |
| 392 | +class _ConstantOnly(ValueError): |
| 393 | + pass |
| 394 | + |
| 395 | + |
| 396 | +def _as_constant_array(t: Union["Tensor", np.ndarray]) -> np.ndarray: |
| 397 | + if isinstance(t, Tensor): |
| 398 | + if t.constant is False: |
| 399 | + raise _ConstantOnly() |
| 400 | + return t.data |
| 401 | + return t |
| 402 | + |
| 403 | + |
352 | 404 | class Tensor:
|
353 | 405 | """A numpy-array-like object capable of serving as a node in a computational
|
354 | 406 | graph that supports back-propagation of derivatives via the chain rule.
|
@@ -505,6 +557,97 @@ class Tensor:
|
505 | 557 |
|
506 | 558 | __array_priority__ = 15.0
|
507 | 559 |
|
| 560 | + def __array_ufunc__( |
| 561 | + self, ufunc: Type[np.ufunc], method: str, *inputs: ArrayLike, **kwargs |
| 562 | + ) -> Union["Tensor", np.ndarray]: |
| 563 | + """An interface provided by NumPy to override the behavior of its ufuncs [1]_. |
| 564 | +
|
| 565 | + MyGrad implements its own ufuncs for all differentiable NumPy ufuncs. |
| 566 | +
|
| 567 | + Non-differentiable numpy ufuncs simply get called on the underlying arrays of tensors and |
| 568 | + will return ndarrays. |
| 569 | +
|
| 570 | + The differentiability - or lack thereof - of ufuncs may not be obvious to end users. |
| 571 | + Thus potentially ambiguous ufuncs (e.g. `numpy.ceil`) will be made to raise on non-constant |
| 572 | + tensors so that the lack of differentiability is made obvious to the users. This design decision |
| 573 | + is made in the same spirit as requiring integer-dtype tensors be constant. |
| 574 | +
|
| 575 | + References |
| 576 | + ---------- |
| 577 | + .. [1] https://numpy.org/doc/stable/reference/arrays.classes.html#numpy.class.__array_ufunc__ |
| 578 | +
|
| 579 | + Examples |
| 580 | + -------- |
| 581 | + NumPy ufuncs that represent differentiable operations are overloaded by MyGrad tensors |
| 582 | + so that they support backprop |
| 583 | +
|
| 584 | + >>> import mygrad as mg |
| 585 | + >>> import numpy as np |
| 586 | +
|
| 587 | + >>> x = mg.tensor([1., 2.]) |
| 588 | +
|
| 589 | + This calls ``mygrad.sin`` under the hood. |
| 590 | +
|
| 591 | + >>> np.sin(x) # returns a tensor |
| 592 | + Tensor([0.84147098, 0.90929743]) |
| 593 | +
|
| 594 | + >>> np.sin(x).backward() |
| 595 | + >>> x.grad # note: derivative of |
| 596 | + array([ 0.54030231, -0.41614684]) |
| 597 | +
|
| 598 | + Specifying a dtype, a ``where`` mask, an in-place target (via ``out``) as an array |
| 599 | + or a tensor, are all supported. |
| 600 | +
|
| 601 | + >>> x = mg.tensor([1., 2.]) |
| 602 | + >>> y = mg.tensor([-1., -1.]) |
| 603 | + >>> np.exp(x, where=[False, True], out=y) |
| 604 | + Tensor([-1. , 7.3890561]) |
| 605 | + >>> y.backward() |
| 606 | + >>> x.grad |
| 607 | + array([0. , 7.3890561]) |
| 608 | +
|
| 609 | + Non-differentiable NumPy ufuncs simply operate on the ndarrays that are wrapped |
| 610 | + by MyGrad tensors; these return ndarrays, which will appropriately and explicitly |
| 611 | + serve as constants elsewhere in a computational graph. |
| 612 | +
|
| 613 | + >>> x = mg.tensor([1., 2.]) |
| 614 | + >>> np.less_equal(x, 1) |
| 615 | + array([ True, False]) |
| 616 | + """ |
| 617 | + out = kwargs.pop("out", (None,)) |
| 618 | + if len(out) > 1: # pragma: no cover |
| 619 | + raise ValueError( |
| 620 | + "mygrad does not support in-place operations with more that one target" |
| 621 | + ) |
| 622 | + (out,) = out |
| 623 | + |
| 624 | + out: Optional[Union[np.ndarray, "Tensor"]] |
| 625 | + |
| 626 | + try: |
| 627 | + # differentiable ufunc implemented by mygrad |
| 628 | + return getattr(_REGISTERED_UFUNC[ufunc], method)(*inputs, **kwargs, out=out) |
| 629 | + except KeyError: |
| 630 | + pass |
| 631 | + |
| 632 | + # non-differentiable ufuncs get called on numpy arrays stored by tensors |
| 633 | + if ufunc in _REGISTERED_BOOL_ONLY_UFUNC: |
| 634 | + caster = asarray |
| 635 | + elif ufunc in _REGISTERED_CONST_ONLY_UFUNC: |
| 636 | + # the presence of non-constant tensors will raise |
| 637 | + caster = _as_constant_array |
| 638 | + else: # pragma: no cover |
| 639 | + return NotImplemented |
| 640 | + |
| 641 | + try: |
| 642 | + if out is not None: |
| 643 | + kwargs["out"] = caster(out) |
| 644 | + # returns ndarray |
| 645 | + return getattr(ufunc, method)(*(caster(t) for t in inputs), **kwargs) |
| 646 | + except _ConstantOnly: |
| 647 | + raise ValueError( |
| 648 | + f"{repr(ufunc)} cannot involve non-constant mygrad tensors." |
| 649 | + ) |
| 650 | + |
508 | 651 | def __array__(self, dtype: DTypeLike = None) -> np.ndarray:
|
509 | 652 | return np.array(self.data, dtype=dtype, copy=False)
|
510 | 653 |
|
@@ -787,11 +930,25 @@ def _op(
|
787 | 930 | -------
|
788 | 931 | mygrad.Tensor
|
789 | 932 | The tensor-result of the operation's forward-pass."""
|
790 |
| - if out is not None and isinstance(out, Tensor): |
791 |
| - out._in_place_op( |
792 |
| - Op, *input_vars, op_args=op_args, op_kwargs=op_kwargs, constant=constant |
793 |
| - ) |
794 |
| - return out |
| 933 | + if out is not None: |
| 934 | + if isinstance(out, tuple): |
| 935 | + if len(out) > 1: # pragma: no cover |
| 936 | + raise ValueError( |
| 937 | + "mygrad does not support in-place operations with more that one target" |
| 938 | + ) |
| 939 | + (out,) = out |
| 940 | + |
| 941 | + if isinstance(out, Tensor): |
| 942 | + out._in_place_op( |
| 943 | + Op, |
| 944 | + *input_vars, |
| 945 | + op_args=op_args, |
| 946 | + op_kwargs=op_kwargs, |
| 947 | + constant=constant, |
| 948 | + ) |
| 949 | + return out |
| 950 | + |
| 951 | + out: Optional[np.ndarray] |
795 | 952 |
|
796 | 953 | _uniques_bases_then_arrs = ()
|
797 | 954 |
|
@@ -1700,21 +1857,11 @@ def __truediv__(self, other: ArrayLike) -> "Tensor":
|
1700 | 1857 | def __rtruediv__(self, other: ArrayLike) -> "Tensor":
|
1701 | 1858 | return self._op(Divide, other, self)
|
1702 | 1859 |
|
1703 |
| - def __floordiv__(self, other: ArrayLike) -> "Tensor": |
1704 |
| - if not self.constant: |
1705 |
| - raise ValueError( |
1706 |
| - "Floor division cannot involve non-constant mygrad tensors." |
1707 |
| - ) |
1708 |
| - if isinstance(other, Tensor): |
1709 |
| - other = other.data |
1710 |
| - return type(self)(self.data.__floordiv__(other), constant=True) |
| 1860 | + def __floordiv__(self, other: ArrayLike) -> np.ndarray: |
| 1861 | + return np.floor_divide(self, other) |
1711 | 1862 |
|
1712 |
| - def __rfloordiv__(self, other: ArrayLike) -> "Tensor": |
1713 |
| - if not self.constant: |
1714 |
| - raise ValueError( |
1715 |
| - "Floor division cannot involve non-constant mygrad tensors." |
1716 |
| - ) |
1717 |
| - return type(self)(self.data.__rfloordiv__(other), constant=True) |
| 1863 | + def __rfloordiv__(self, other: ArrayLike) -> np.ndarray: |
| 1864 | + return np.floor_divide(other, self) |
1718 | 1865 |
|
1719 | 1866 | def __itruediv__(self, other: ArrayLike) -> "Tensor":
|
1720 | 1867 | self._in_place_op(Divide, self, other)
|
|
0 commit comments