Files
2026-07-13 13:17:40 +08:00

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