Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

789 lines
31 KiB
Python

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import atexit
import inspect
import os
import time
import warnings
from collections import namedtuple
from itertools import islice
# Add this for extremely slow connection to hf sever even for local dataset.
os.environ["HF_UPDATE_DOWNLOAD_COUNTS"] = "False"
import datasets
from multiprocess import Pool, RLock
import paddlenlp
try:
import paddle.distributed as dist
except Exception:
warnings.warn("paddle.distributed is not contains in you paddle!")
import importlib
from functools import partial
from paddle.io import Dataset, IterableDataset
from paddle.utils.download import _get_unique_endpoints
from paddlenlp.utils.env import DATA_HOME
__all__ = ["MapDataset", "DatasetBuilder", "IterDataset", "load_dataset"]
DATASETS_MODULE_PATH = "paddlenlp.datasets."
# Patch for intranet
from datasets import load_dataset as origin_load_dataset # noqa: E402
def load_from_ppnlp(path, *args, **kwargs):
ppnlp_path = paddlenlp.datasets.__path__[0]
new_path = os.path.split(path)[-1]
new_path = os.path.join(ppnlp_path, "hf_datasets", new_path + ".py")
if os.path.exists(new_path):
return origin_load_dataset(new_path, trust_remote_code=True, *args, **kwargs)
else:
return origin_load_dataset(path, trust_remote_code=True, *args, **kwargs)
datasets.load_dataset = load_from_ppnlp
class DatasetTuple:
def __init__(self, splits):
self.identifier_map, identifiers = self._gen_identifier_map(splits)
self.tuple_cls = namedtuple("datasets", identifiers)
self.tuple = self.tuple_cls(*[None for _ in splits])
def __getitem__(self, key):
if isinstance(key, (int, slice)):
return self.tuple[key]
if isinstance(key, str):
return getattr(self.tuple, self.identifier_map[key])
def __setitem__(self, key, value):
self.tuple = self.tuple._replace(**{self.identifier_map[key]: value})
def _gen_identifier_map(self, splits):
identifier_map = {}
identifiers = []
for i in range(len(splits)):
identifiers.append("splits_" + str(i))
identifier_map[splits[i]] = "splits_" + str(i)
return identifier_map, identifiers
def __len__(self):
return len(self.tuple)
def import_main_class(module_path):
"""
Import a module at module_path and return its DatasetBuilder class.
"""
module_path = DATASETS_MODULE_PATH + module_path
module = importlib.import_module(module_path)
main_cls_type = DatasetBuilder
# Find the main class in our imported module
module_main_cls = None
for name, obj in module.__dict__.items():
if isinstance(obj, type) and issubclass(obj, main_cls_type):
if name == "DatasetBuilder":
continue
module_main_cls = obj
break
return module_main_cls
def load_from_hf(path, name=None, splits=None, **kwargs):
from datasets import DatasetDict, IterableDatasetDict
from datasets import load_dataset as load_hf_dataset
from datasets.features import ClassLabel
try:
if "split" in kwargs:
hf_datasets = load_hf_dataset(path, name=name, **kwargs)
else:
hf_datasets = load_hf_dataset(path, name=name, split=splits, **kwargs)
except FileNotFoundError:
raise FileNotFoundError("Couldn't find the dataset script for '" + path + "' on PaddleNLP or HuggingFace")
else:
label_list = []
if isinstance(hf_datasets, DatasetDict):
datasets = DatasetTuple(list(hf_datasets.keys()))
for split, ds in hf_datasets.items():
for feature in ds.features.values():
if isinstance(feature, ClassLabel):
label_list = feature.names
datasets[split] = MapDataset(ds, label_list=label_list)
elif isinstance(hf_datasets, IterableDatasetDict):
datasets = DatasetTuple(list(hf_datasets.keys()))
for split, ds in hf_datasets.items():
datasets[split] = IterDataset(ds)
elif isinstance(hf_datasets, list):
datasets = DatasetTuple(splits)
for i, split in enumerate(splits):
for feature in hf_datasets[i].features.values():
if isinstance(feature, ClassLabel):
label_list = feature.names
datasets[split] = MapDataset(hf_datasets[i], label_list=label_list)
else:
for feature in hf_datasets.features.values():
if isinstance(feature, ClassLabel):
label_list = feature.names
datasets = MapDataset(hf_datasets, label_list=label_list)
return datasets
def load_dataset(path_or_read_func, name=None, data_files=None, splits=None, lazy=None, **kwargs):
"""
This method will load a dataset, either form PaddleNLP library or from a
self-defined data loading script, by calling functions in `DatasetBuilder`.
For all the names of datasets in PaddleNLP library, see here: `dataset_list
<https://paddlenlp.readthedocs.io/zh/latest/data_prepare/dataset_list.html>`__.
Either `splits` or `data_files` must be specified.
Args:
path_or_read_func (str|callable): Name of the dataset processing script
in PaddleNLP library or a custom data reading function.
name (str, optional): Additional name to select a more specific dataset.
Defaults to None.
data_files (str|list|tuple|dict, optional): Defining the path of dataset
files. If None. `splits` must be specified. Defaults to None.
splits (str|list|tuple, optional): Which split of the data to load. If None.
`data_files` must be specified. Defaults to None.
lazy (bool, optional): Weather to return `MapDataset` or an `IterDataset`.
True for `IterDataset`. False for `MapDataset`. If None, return the
default type of this dataset. Defaults to None.
kwargs (dict): Other keyword arguments to be passed to the `DatasetBuilder`.
Returns:
A `MapDataset` or `IterDataset` or a tuple of those.
For how to use this function, please see `dataset_load
<https://paddlenlp.readthedocs.io/zh/latest/data_prepare/dataset_load.html>`__
and `dataset_self_defined
<https://paddlenlp.readthedocs.io/zh/latest/data_prepare/dataset_self_defined.html>`__
"""
if inspect.isfunction(path_or_read_func):
assert lazy is not None, "lazy can not be None in custom mode."
kwargs["name"] = name
kwargs["data_files"] = data_files
kwargs["splits"] = splits
custom_kwargs = {}
for name in inspect.signature(path_or_read_func).parameters.keys():
if name in kwargs.keys():
custom_kwargs[name] = kwargs[name]
reader_instance = SimpleBuilder(lazy=lazy, read_func=path_or_read_func)
return reader_instance.read(**custom_kwargs)
else:
try:
reader_cls = import_main_class(path_or_read_func)
except ModuleNotFoundError:
datasets = load_from_hf(
path_or_read_func, name=name, splits=splits, data_files=data_files, streaming=lazy, **kwargs
)
else:
reader_instance = reader_cls(lazy=lazy, name=name, **kwargs)
# Check if selected name and split is valid in this DatasetBuilder
if hasattr(reader_instance, "BUILDER_CONFIGS"):
if name in reader_cls.BUILDER_CONFIGS.keys():
split_names = reader_cls.BUILDER_CONFIGS[name]["splits"].keys()
else:
raise ValueError(
'Invalid name "{}". Should be one of {}.'.format(name, list(reader_cls.BUILDER_CONFIGS.keys()))
)
elif hasattr(reader_instance, "SPLITS"):
split_names = reader_instance.SPLITS.keys()
else:
raise AttributeError("Either 'SPLITS' or 'BUILDER_CONFIGS' must be implemented for DatasetBuilder.")
selected_splits = []
if isinstance(splits, list) or isinstance(splits, tuple):
selected_splits.extend(splits)
else:
selected_splits += [splits]
for split_name in selected_splits:
if split_name not in split_names and split_name is not None:
raise ValueError('Invalid split "{}". Should be one of {}.'.format(split_name, list(split_names)))
datasets = reader_instance.read_datasets(data_files=data_files, splits=splits)
return datasets
class MapDataset(Dataset):
"""
Wraps a map-style dataset-like object as an instance of `MapDataset`, and equips it
with `map` and other utility methods. All non-magic methods of the raw object
are also accessible.
Args:
data (list|Dataset): An object with `__getitem__` and `__len__` methods. It could
be a list or a subclass of `paddle.io.Dataset`.
kwargs (dict, optional): Other information to be passed to the dataset.
For examples of this class, please see `dataset_self_defined
<https://paddlenlp.readthedocs.io/zh/latest/data_prepare/dataset_self_defined.html>`__.
"""
def __init__(self, data, **kwargs):
self.data = data
self._transform_pipline = []
self.new_data = self.data
self.info = kwargs
self.label_list = self.info.pop("label_list", None)
self.vocab_info = self.info.pop("vocab_info", None)
def _transform(self, data):
for fn in self._transform_pipline:
data = fn(data)
return data
def __getitem__(self, idx):
"""
Basic function of `MapDataset` to get sample from dataset with a given
index.
"""
return self._transform(self.new_data[idx]) if self._transform_pipline else self.new_data[idx]
def __len__(self):
"""
Returns the number of samples in dataset.
"""
return len(self.new_data)
def filter(self, fn, num_workers=0):
"""
Filters samples by the filter function and uses the filtered data to
update this dataset.
Args:
fn (callable): A filter function that takes a sample as input and
returns a boolean. Samples that return False would be discarded.
num_workers(int, optional): Number of processes for multiprocessing. If
set to 0, it doesn't use multiprocessing. Defaults to `0`.
"""
assert num_workers >= 0, "num_workers should be a non-negative value"
if num_workers > 1:
shards = [
self._shard(num_shards=num_workers, index=index, contiguous=True) for index in range(num_workers)
]
kwds_per_shard = [dict(self=shards[rank], fn=fn) for rank in range(num_workers)]
pool = Pool(num_workers, initargs=(RLock(),))
results = [pool.apply_async(self.__class__._filter, kwds=kwds) for kwds in kwds_per_shard]
transformed_shards = [r.get() for r in results]
pool.close()
pool.join()
self.new_data = []
for i in range(num_workers):
self.new_data += transformed_shards[i].new_data
return self
else:
return self._filter(fn)
def _filter(self, fn):
self.new_data = [self.new_data[idx] for idx in range(len(self.new_data)) if fn(self.new_data[idx])]
return self
def shard(self, num_shards=None, index=None, contiguous=False):
self.new_data = self._shard(num_shards=num_shards, index=index, contiguous=contiguous).data
return self
def _shard(self, num_shards=None, index=None, contiguous=False):
"""
Split the dataset into `num_shards` pieces. Note that the size of each
shard might be different because the original dataset may not be evenly
divisible.
Args:
num_shards (int, optional): An integer representing the number of
data shards. If None, `num_shards` would be number of trainers.
Defaults to `None`.
index (int, optional): An integer representing the index of the
current shard. If None, `index` would be the current trainer rank
id. Defaults to `None`.
contiguous: (bool, optional): If true, contiguous chunks of data
will be select for sharding. And total number of examples will
be the same. Otherwise each shard will contain all examples of
dataset whose index mod `num_shards` = `index`. Defaults to `False`.
"""
if num_shards is None:
num_shards = dist.get_world_size()
if index is None:
index = dist.get_rank()
if contiguous:
div = len(self) // num_shards
mod = len(self) % num_shards
start = div * index + min(index, mod)
end = start + div + (1 if index < mod else 0)
new_data = [self.new_data[idx] for idx in range(start, end)]
else:
new_data = [self.new_data[idx] for idx in range(len(self.new_data)) if idx % num_shards == index]
return MapDataset(new_data)
def map(self, fn, lazy=True, batched=False, num_workers=0):
"""
Performs specific function on the dataset to transform and update every sample.
Args:
fn (callable): Transformations to be performed. It receives single
sample as argument if batched is False. Else it receives all examples.
lazy (bool, optional): If True, transformations would be delayed and
performed on demand. Otherwise, transforms all samples at once. Note that
if `fn` is stochastic, `lazy` should be True or you will get the same
result on all epochs. Defaults to False.
batched(bool, optional): If True, transformations would take all examples as
input and return a collection of transformed examples. Note that if set
True, `lazy` option would be ignored. Defaults to False.
num_workers(int, optional): Number of processes for multiprocessing. If
set to 0, it doesn't use multiprocessing. Note that if set to positive
value, `lazy` option would be ignored. Defaults to 0.
"""
assert num_workers >= 0, "num_workers should be a non-negative value"
if num_workers > 1:
shards = [
self._shard(num_shards=num_workers, index=index, contiguous=True) for index in range(num_workers)
]
kwds_per_shard = [
dict(self=shards[rank], fn=fn, lazy=False, batched=batched) for rank in range(num_workers)
]
pool = Pool(num_workers, initargs=(RLock(),))
results = [pool.apply_async(self.__class__._map, kwds=kwds) for kwds in kwds_per_shard]
transformed_shards = [r.get() for r in results]
pool.close()
pool.join()
self.new_data = []
for i in range(num_workers):
self.new_data += transformed_shards[i].new_data
return self
else:
return self._map(fn, lazy=lazy, batched=batched)
def _map(self, fn, lazy=True, batched=False):
if batched:
self.new_data = fn(self.new_data)
elif lazy:
self._transform_pipline.append(fn)
else:
self.new_data = [fn(self.new_data[idx]) for idx in range(len(self.new_data))]
return self
class IterDataset(IterableDataset):
"""
Wraps a dataset-like object as an instance of `IterDataset`, and equips it with
`map` and other utility methods. All non-magic methods of the raw object
also accessible.
Args:
data (Iterable): An object with `__iter__` function. It can be a Iterable or a
subclass of `paddle.io.IterableDataset`.
kwargs (dict, optional): Other information to be passed to the dataset.
For examples of this class, please see `dataset_self_defined
<https://paddlenlp.readthedocs.io/zh/latest/data_prepare/dataset_self_defined.html>`__.
"""
def __init__(self, data, **kwargs):
self.data = data
self._transform_pipline = []
self._filter_pipline = []
self.label_list = kwargs.pop("label_list", None)
self.vocab_info = kwargs.pop("vocab_info", None)
def _transform(self, data):
for fn in self._transform_pipline:
data = fn(data)
return data
def _shard_filter(self, num_samples):
return True
def _filter(self, data):
for fn in self._filter_pipline:
if not fn(data):
return False
return True
def __iter__(self):
"""
yields sample sequentially.
"""
num_samples = 0
if inspect.isfunction(self.data):
for example in self.data():
if (not self._filter_pipline or self._filter(self._filter_pipline)) and self._shard_filter(
num_samples=num_samples
):
yield self._transform(example) if self._transform_pipline else example
num_samples += 1
else:
if inspect.isgenerator(self.data):
warnings.warn("Receiving generator as data source, data can only be iterated once")
for example in self.data:
if (not self._filter_pipline or self._filter(self._filter_pipline)) and self._shard_filter(
num_samples=num_samples
):
yield self._transform(example) if self._transform_pipline else example
num_samples += 1
def skip(self, n):
if inspect.isfunction(self.data):
raise NotImplementedError("Function-based IterDataset does not support `.skip()`")
self.data = islice(self.data, n, None)
return self
def filter(self, fn):
"""
Filters samples by the filter function and uses the filtered data to
update this dataset.
Args:
fn (callable): A filter function that takes a sample as input and
returns a boolean. Samples that return False are discarded.
"""
self._filter_pipline.append(fn)
return self
def shard(self, num_shards=None, index=None):
"""
Split the dataset into `num_shards` pieces.
Args:
num_shards (int, optional): An integer representing the number of
data shards. If None, `num_shards` would be number of trainers.
Defaults to None.
index (int, optional): An integer representing the index of the
current shard. If None, `index` would be the current trainer rank
id. Defaults to None.
"""
if num_shards is None:
num_shards = dist.get_world_size()
if index is None:
index = dist.get_rank()
def sharder(num_shards, index, num_samples):
if num_samples % num_shards == index:
return True
else:
return False
fn = partial(sharder, num_shards=num_shards, index=index)
self._shard_filter = fn
return self
def map(self, fn):
"""
Performs specific function on the dataset to transform and update every sample.
Args:
fn (callable): Transformations to be performed. It receives single
sample as argument.
"""
self._transform_pipline.append(fn)
return self
class DatasetBuilder:
"""
A base class for all DatasetBuilder. It provides a `read()` function to turn
a data file into a MapDataset or IterDataset.
`_get_data()` function and `_read()` function should be implemented to download
data file and read data file into a `Iterable` of the examples.
For how to define a custom `DatasetBuilder`, please see `contribute_dataset
<https://paddlenlp.readthedocs.io/zh/latest/community/contribute_dataset.html>`__.
"""
lazy = False
def __init__(self, lazy=None, name=None, **config):
if lazy is not None:
self.lazy = lazy
self.name = name
self.config = config
def read_datasets(self, splits=None, data_files=None):
def remove_if_exit(filepath):
if isinstance(filepath, (list, tuple)):
for file in filepath:
try:
os.remove(file)
except OSError:
pass
else:
try:
os.remove(filepath)
except OSError:
pass
if data_files is None:
if splits is None:
splits = (
list(self.BUILDER_CONFIGS[self.name]["splits"].keys())
if hasattr(self, "BUILDER_CONFIGS")
else list(self.SPLITS.keys())
)
assert (
isinstance(splits, str)
or (isinstance(splits, list) and isinstance(splits[0], str))
or (isinstance(splits, tuple) and isinstance(splits[0], str))
), "`splits` should be a string or list of string or a tuple of string."
if isinstance(splits, str):
splits = [splits]
datasets = DatasetTuple(splits)
parallel_env = dist.ParallelEnv()
unique_endpoints = _get_unique_endpoints(parallel_env.trainer_endpoints[:])
# move register hook to first and register together
lock_files = []
for split in splits:
lock_file = os.path.join(DATA_HOME, self.__class__.__name__)
if self.name is not None:
lock_file = lock_file + "." + self.name
lock_file += "." + split + ".done" + "." + str(os.getppid())
lock_files.append(lock_file)
# Must register to all procs to make the lock file can be removed
# when any proc breaks. Otherwise, the single registered proc may
# not receive proper signal send by the parent proc to exit.
atexit.register(lambda: remove_if_exit(lock_files))
for split in splits:
filename = self._get_data(split)
lock_file = os.path.join(DATA_HOME, self.__class__.__name__)
if self.name is not None:
lock_file = lock_file + "." + self.name
lock_file += "." + split + ".done" + "." + str(os.getppid())
# `lock_file` indicates the finished status of`_get_data`.
# `_get_data` only works in the `unique_endpoints` specified
# proc since `get_path_from_url` only work for it. The other
# procs wait `_get_data` to be finished.
if parallel_env.current_endpoint in unique_endpoints:
f = open(lock_file, "w")
f.close()
else:
while not os.path.exists(lock_file):
time.sleep(1)
datasets[split] = self.read(filename=filename, split=split)
else:
assert (
isinstance(data_files, str) or isinstance(data_files, tuple) or isinstance(data_files, list)
), "`data_files` should be a string or tuple or list of strings."
if isinstance(data_files, str):
data_files = [data_files]
default_split = "train"
if splits:
if isinstance(splits, str):
splits = [splits]
datasets = DatasetTuple(splits)
assert len(splits) == len(
data_files
), "Number of `splits` and number of `data_files` should be the same if you want to specify the split of local data file."
for i in range(len(data_files)):
datasets[splits[i]] = self.read(filename=data_files[i], split=splits[i])
else:
datasets = DatasetTuple(["split" + str(i) for i in range(len(data_files))])
for i in range(len(data_files)):
datasets["split" + str(i)] = self.read(filename=data_files[i], split=default_split)
return datasets if len(datasets) > 1 else datasets[0]
def read(self, filename, split="train"):
"""
Returns a dataset containing all the examples that can be read from the file path.
If `self.lazy` is False, this eagerly reads all instances from `self._read()`
and returns a `MapDataset`.
If `self.lazy` is True, this returns an `IterDataset`, which internally
relies on the generator created from `self._read()` to lazily produce examples.
In this case your implementation of `_read()` must also be lazy
(that is, not load all examples into memory at once).
Args:
filename (str): Path of data file to read, usually provided by `_get_data`
function.
split (str, optional): The split name of selected dataset. This only makes
a different when data files of different splits have different structures.
Returns:
A `MapDataset|IterDataset`.
"""
label_list = self.get_labels()
vocab_info = self.get_vocab()
def _create_dict(labels):
# For multiple labels in the form of list.
if isinstance(labels[0], list) or isinstance(labels[0], tuple):
label_dict = []
for sub_labels in labels:
sub_dict = {}
for i, label in enumerate(sub_labels):
sub_dict[label] = i
label_dict.append(sub_dict)
else:
label_dict = {}
for i, label in enumerate(labels):
label_dict[label] = i
return label_dict
def _convert_label_to_id(labels, label_dict):
if isinstance(labels, list) or isinstance(labels, tuple):
for label_idx in range(len(labels)):
labels[label_idx] = label_dict[labels[label_idx]]
else:
labels = label_dict[labels]
return labels
if self.lazy:
def generate_examples():
generator = (
self._read(filename, split) if self._read.__code__.co_argcount > 2 else self._read(filename)
)
for example in generator:
# We need to check if the example contains label column and confirm its name.
# For now we only allow `label` or `labels` to be the name of label column.
if "labels" in example.keys():
label_col = "labels"
elif "label" in example.keys():
label_col = "label"
else:
label_col = None
# Convert class label to label ids.
if label_list is not None and example.get(label_col, None):
label_dict = _create_dict(label_list)
# For multiple labels in the form of list.
if isinstance(label_dict, list):
for idx, sub_dict in enumerate(label_dict):
example[label_col][idx] = _convert_label_to_id(example[label_col][idx], sub_dict)
else:
example[label_col] = _convert_label_to_id(example[label_col], label_dict)
yield example
else:
yield example
return IterDataset(generate_examples(), label_list=label_list, vocab_info=vocab_info)
else:
examples = self._read(filename, split) if self._read.__code__.co_argcount > 2 else self._read(filename)
# Then some validation.
if not isinstance(examples, list):
examples = list(examples)
if not examples:
raise ValueError(
"No instances were read from the given filepath {}. " "Is the path correct?".format(filename)
)
# We need to check if the example contains label column and confirm its name.
# For now we only allow `label` or `labels` to be the name of label column.
if "labels" in examples[0].keys():
label_col = "labels"
elif "label" in examples[0].keys():
label_col = "label"
else:
label_col = None
# Convert class label to label ids.
if label_list is not None and examples[0].get(label_col, None):
label_dict = _create_dict(label_list)
for idx in range(len(examples)):
# For multiple labels in the form of list.
if isinstance(label_dict, list):
for i, sub_dict in enumerate(label_dict):
examples[idx][label_col][i] = _convert_label_to_id(examples[idx][label_col][i], sub_dict)
else:
examples[idx][label_col] = _convert_label_to_id(examples[idx][label_col], label_dict)
return MapDataset(examples, label_list=label_list, vocab_info=vocab_info)
def _read(self, filename: str, *args):
"""
Reads examples from the given file_path and returns them as an
`Iterable` (which could be a list or a generator).
This method must be implemented in self-defined `DatasetBuilder`.
"""
raise NotImplementedError
def _get_data(self, mode: str):
"""
Downloads examples from the given URL and customized split
informations and returns a filepath.
This method must be implemented in self-defined `DatasetBuilder`.
"""
raise NotImplementedError
def get_labels(self):
"""
Returns list of class labels of the dataset if specified.
"""
return None
def get_vocab(self):
"""
Returns vocab file path of the dataset if specified.
"""
return None
class SimpleBuilder(DatasetBuilder):
def __init__(self, lazy, read_func):
self._read = read_func
self.lazy = lazy
def read(self, **kwargs):
if self.lazy:
def generate_examples():
generator = self._read(**kwargs)
for example in generator:
yield example
return IterDataset(generate_examples)
else:
examples = self._read(**kwargs)
if hasattr(examples, "__len__") and hasattr(examples, "__getitem__"):
return MapDataset(examples)
else:
return MapDataset(list(examples))