216 lines
7.3 KiB
Python
216 lines
7.3 KiB
Python
import warnings
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
Any,
|
|
List,
|
|
Protocol,
|
|
Sequence,
|
|
Union,
|
|
)
|
|
|
|
import numpy as np
|
|
|
|
from ray.data.constants import TENSOR_COLUMN_NAME
|
|
from ray.util import PublicAPI
|
|
from ray.util.annotations import DeveloperAPI
|
|
|
|
if TYPE_CHECKING:
|
|
from pandas.core.dtypes.generic import ABCSeries
|
|
|
|
|
|
@DeveloperAPI(stability="beta")
|
|
class ArrayLike(Protocol):
|
|
"""Protocol matching ndarray-like objects (like torch.Tensor)"""
|
|
|
|
def __array__(self):
|
|
...
|
|
|
|
def __len__(self):
|
|
...
|
|
|
|
|
|
@DeveloperAPI(stability="beta")
|
|
def is_ndarray_like(value: ArrayLike) -> bool:
|
|
"""Checks whether objects are ndarray-like (for ex, torch.Tensor)
|
|
but NOT and ndarray itself"""
|
|
return (
|
|
hasattr(value, "__array__")
|
|
and hasattr(value, "__len__")
|
|
and not isinstance(value, np.ndarray)
|
|
)
|
|
|
|
|
|
def _is_arrow_array(value: ArrayLike) -> bool:
|
|
import pyarrow as pa
|
|
|
|
return isinstance(value, (pa.Array, pa.ChunkedArray))
|
|
|
|
|
|
def _should_convert_to_tensor(
|
|
column_values: Union[List[Any], np.ndarray, ArrayLike], column_name: str
|
|
) -> bool:
|
|
assert len(column_values) > 0
|
|
|
|
# We convert passed in column values into a tensor representation (involving
|
|
# Arrow/Pandas extension types) in either of the following cases:
|
|
return (
|
|
# - Column name is `TENSOR_COLUMN_NAME` (for compatibility)
|
|
column_name == TENSOR_COLUMN_NAME
|
|
# - Provided column values are already represented by a Numpy tensor (ie
|
|
# ndarray with ndim > 1)
|
|
or _is_ndarray_tensor(column_values)
|
|
# - Provided collection is already implementing Ndarray protocol like
|
|
# `torch.Tensor`, `pd.Series`, etc (but *excluding* `pyarrow.Array`,
|
|
# `pyarrow.ChunkedArray`)
|
|
or _is_ndarray_like_not_pyarrow_array(column_values)
|
|
# - Provided column values is a list of a) ndarrays or b) ndarray-like objects
|
|
# (excluding Pyarrow arrays). This is done for compatibility with previous
|
|
# existing behavior where all column values were blindly converted to Numpy
|
|
# leading to list of ndarrays being converted a tensor):
|
|
or isinstance(column_values[0], np.ndarray)
|
|
or _is_ndarray_like_not_pyarrow_array(column_values[0])
|
|
)
|
|
|
|
|
|
def _is_ndarray_like_not_pyarrow_array(column_values):
|
|
return is_ndarray_like(column_values) and not _is_arrow_array(column_values)
|
|
|
|
|
|
def _is_ndarray_tensor(t: Any) -> bool:
|
|
"""Return whether provided ndarray is a tensor (ie ndim > 1).
|
|
|
|
NOTE: Tensor is defined as a NumPy array such that `len(arr.shape) > 1`
|
|
"""
|
|
|
|
if not isinstance(t, np.ndarray):
|
|
return False
|
|
|
|
# Case of uniform-shaped (ie non-ragged) tensor
|
|
if t.ndim > 1:
|
|
return True
|
|
|
|
# Case of ragged tensor (as produced by `create_ragged_ndarray` utility)
|
|
elif t.dtype.type is np.object_ and len(t) > 0 and isinstance(t[0], np.ndarray):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _is_ndarray_variable_shaped_tensor(arr: np.ndarray) -> bool:
|
|
"""Return whether the provided NumPy ndarray is comprised of variable-shaped
|
|
tensors.
|
|
|
|
NOTE: This is an O(rows) check.
|
|
"""
|
|
if arr.dtype.type is not np.object_:
|
|
return False
|
|
if len(arr) == 0:
|
|
return False
|
|
if not isinstance(arr[0], np.ndarray):
|
|
return False
|
|
shape = arr[0].shape
|
|
for a in arr[1:]:
|
|
if not isinstance(a, np.ndarray):
|
|
return False
|
|
if a.shape != shape:
|
|
return True
|
|
|
|
# All shapes are identical
|
|
return False
|
|
|
|
|
|
def _create_possibly_ragged_ndarray(
|
|
values: Union[np.ndarray, "ABCSeries", Sequence[Any]]
|
|
) -> np.ndarray:
|
|
"""
|
|
Create a possibly ragged ndarray.
|
|
Using the np.array() constructor will fail to construct a ragged ndarray that has a
|
|
uniform first dimension (e.g. uniform channel dimension in imagery). This function
|
|
catches this failure and tries a create-and-fill method to construct the ragged
|
|
ndarray.
|
|
"""
|
|
try:
|
|
with warnings.catch_warnings():
|
|
# For NumPy < 1.24, constructing a ragged ndarray directly via
|
|
# `np.array(...)` without the `dtype=object` parameter will raise a
|
|
# VisibleDeprecationWarning which we suppress.
|
|
# More details: https://stackoverflow.com/q/63097829
|
|
if np.lib.NumpyVersion(np.__version__) >= "2.0.0":
|
|
copy_if_needed = None
|
|
warning_type = np.exceptions.VisibleDeprecationWarning
|
|
else:
|
|
copy_if_needed = False
|
|
warning_type = np.VisibleDeprecationWarning
|
|
|
|
warnings.simplefilter("ignore", category=warning_type)
|
|
arr = np.array(values, copy=copy_if_needed)
|
|
return arr
|
|
except ValueError as e:
|
|
# Constructing a ragged ndarray directly via `np.array(...)`
|
|
# without the `dtype=object` parameter will raise a ValueError.
|
|
# For NumPy < 1.24, the message is of the form:
|
|
# "could not broadcast input array from shape..."
|
|
# For NumPy >= 1.24, the message is of the form:
|
|
# "The requested array has an inhomogeneous shape..."
|
|
# More details: https://github.com/numpy/numpy/pull/22004
|
|
error_str = str(e)
|
|
if (
|
|
"could not broadcast input array from shape" in error_str
|
|
or "The requested array has an inhomogeneous shape" in error_str
|
|
):
|
|
# Fall back to strictly creating a ragged ndarray.
|
|
return create_ragged_ndarray(values)
|
|
else:
|
|
# Re-raise original error if the failure wasn't a broadcast error.
|
|
raise e from None
|
|
|
|
|
|
@PublicAPI(stability="alpha")
|
|
def create_ragged_ndarray(values: Sequence[Any]) -> np.ndarray:
|
|
"""Create an array that contains arrays of different length
|
|
|
|
If you're working with variable-length arrays like images, use this function to
|
|
create ragged arrays instead of ``np.array``.
|
|
|
|
.. note::
|
|
``np.array`` fails to construct ragged arrays if the input arrays have a uniform
|
|
first dimension:
|
|
|
|
.. testsetup::
|
|
|
|
import numpy as np
|
|
from ray.data._internal.tensor_extensions.utils import create_ragged_ndarray
|
|
|
|
.. doctest::
|
|
|
|
>>> values = [np.zeros((3, 1)), np.zeros((3, 2))]
|
|
>>> np.array(values, dtype=object)
|
|
Traceback (most recent call last):
|
|
...
|
|
ValueError: could not broadcast input array from shape (3,1) into shape (3,)
|
|
>>> create_ragged_ndarray(values)
|
|
array([array([[0.],
|
|
[0.],
|
|
[0.]]), array([[0., 0.],
|
|
[0., 0.],
|
|
[0., 0.]])], dtype=object)
|
|
|
|
Or if you're creating a ragged array from a single array:
|
|
|
|
.. doctest::
|
|
|
|
>>> values = [np.zeros((3, 1))]
|
|
>>> np.array(values, dtype=object)[0].dtype
|
|
dtype('O')
|
|
>>> create_ragged_ndarray(values)[0].dtype
|
|
dtype('float64')
|
|
|
|
``create_ragged_ndarray`` avoids the limitations of ``np.array`` by creating an
|
|
empty array and filling it with pointers to the variable-length arrays.
|
|
""" # noqa: E501
|
|
# Create an empty object-dtyped 1D array.
|
|
arr = np.empty(len(values), dtype=object)
|
|
# Try to fill the 1D array of pointers with the (ragged) tensors.
|
|
arr[:] = list(values)
|
|
return arr
|