200 lines
7.6 KiB
Python
200 lines
7.6 KiB
Python
# Copyright (c) Facebook, Inc. and its affiliates.
|
|
#
|
|
# This source code is licensed under the MIT license found in the
|
|
# LICENSE file in the root directory of this source tree.
|
|
|
|
import hashlib
|
|
import logging
|
|
import math
|
|
|
|
import numpy as np
|
|
from fairseq.data import SampledMultiDataset
|
|
|
|
from .sampled_multi_dataset import CollateFormat, default_virtual_size_func
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SampledMultiEpochDataset(SampledMultiDataset):
|
|
"""Samples from multiple sub-datasets according to sampling ratios
|
|
using virtual epoch sizes to speed up dataloading.
|
|
Args:
|
|
datasets (
|
|
List[~torch.utils.data.Dataset]
|
|
or OrderedDict[str, ~torch.utils.data.Dataset]
|
|
): datasets
|
|
sampling_ratios (List[float]): list of probability of each dataset to be sampled
|
|
(default: None, which corresponds to concating all dataset together).
|
|
seed (int): RNG seed to use (default: 2).
|
|
epoch (int): starting epoch number (default: 1).
|
|
eval_key (str, optional): a key used at evaluation time that causes
|
|
this instance to pass-through batches from *datasets[eval_key]*.
|
|
collate_format (CollateFormat): collater output format, either CollateFormat.ordered_dict or
|
|
CollateFormat.single (default: CollateFormat.single) where CollateFormat.single configures
|
|
the collater to output batches of data mixed from all sub-datasets,
|
|
and CollateFormat.ordered_dict configures the collater to output a dictionary of batches indexed by keys
|
|
of sub-datasets.
|
|
Note that not all sub-datasets will present in a single batch in both formats.
|
|
virtual_size (int, or callable): the expected virtual size of the dataset (default: default_virtual_size_func).
|
|
split (str): the split of the data, e.g. 'train', 'valid' or 'test'.
|
|
virtual_epoch_size (int): virtual epoch size, the dataset will go through the data by
|
|
this virtual epoch size one by one to speed up data loading, e.g. indicing and filtering
|
|
can be performed whenever a virtual epoch is loaded without waiting for the whole dataset to be loaded.
|
|
shared_collater (bool): whether or not to all sub-datasets have the same collater.
|
|
shard_epoch (int): the real epoch number for shard selection.
|
|
shuffle (bool): whether or not to shuffle data (default: True).
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
datasets,
|
|
sampling_ratios=None,
|
|
seed=2,
|
|
epoch=1,
|
|
eval_key=None,
|
|
collate_format=CollateFormat.single,
|
|
virtual_size=default_virtual_size_func,
|
|
split="",
|
|
virtual_epoch_size=None,
|
|
shared_collater=False,
|
|
shard_epoch=1,
|
|
shuffle=True,
|
|
):
|
|
self.virtual_epoch_size = virtual_epoch_size
|
|
self._current_epoch_start_index = None
|
|
self._random_global_indices = None
|
|
self.shard_epoch = shard_epoch if shard_epoch is not None else 1
|
|
self.load_next_shard = None
|
|
self._epoch_sizes = None
|
|
super().__init__(
|
|
datasets=datasets,
|
|
sampling_ratios=sampling_ratios,
|
|
seed=seed,
|
|
epoch=epoch,
|
|
eval_key=eval_key,
|
|
collate_format=collate_format,
|
|
virtual_size=virtual_size,
|
|
split=split,
|
|
shared_collater=shared_collater,
|
|
shuffle=shuffle,
|
|
)
|
|
|
|
def _setup(self, epoch):
|
|
self.virtual_epoch_size = (
|
|
self.virtual_epoch_size
|
|
if self.virtual_epoch_size is not None
|
|
else self.virtual_size
|
|
)
|
|
if self.virtual_epoch_size > self.virtual_size:
|
|
logger.warning(
|
|
f"virtual epoch size {self.virtual_epoch_size} "
|
|
f"is greater than virtual dataset size {self.virtual_size}"
|
|
)
|
|
self.virtual_epoch_size = self.virtual_size
|
|
self.num_virtual_epochs = math.ceil(self.virtual_size / self.virtual_epoch_size)
|
|
self._current_epoch_start_index = self._get_epoch_start_index(epoch)
|
|
logger.info(
|
|
f"virtual epoch size {self.virtual_epoch_size}; virtual dataset size {self.virtual_size}"
|
|
)
|
|
|
|
def _map_epoch_index_to_global(self, index):
|
|
index = self._current_epoch_start_index + index
|
|
# add randomness
|
|
return self._random_global_indices[index]
|
|
|
|
@property
|
|
def sizes(self):
|
|
if self._epoch_sizes is not None:
|
|
return self._epoch_sizes
|
|
_sizes = super().sizes
|
|
indices = self._random_global_indices[
|
|
self._current_epoch_start_index : self._current_epoch_start_index
|
|
+ len(self)
|
|
]
|
|
self._epoch_sizes = _sizes[indices]
|
|
# del super()._sizes to save memory
|
|
del self._sizes
|
|
self._sizes = None
|
|
return self._epoch_sizes
|
|
|
|
def _get_dataset_and_index(self, index):
|
|
i = self._map_epoch_index_to_global(index)
|
|
return super()._get_dataset_and_index(i)
|
|
|
|
def __len__(self):
|
|
return (
|
|
self.virtual_epoch_size
|
|
if self._current_epoch_start_index + self.virtual_epoch_size
|
|
< self.virtual_size
|
|
else self.virtual_size - self._current_epoch_start_index
|
|
)
|
|
|
|
def set_epoch(self, epoch):
|
|
if self._current_epoch_start_index is None:
|
|
# initializing epoch idnices of a virtual dataset
|
|
self._setup(epoch)
|
|
self._next_virtual_epoch(epoch)
|
|
else:
|
|
# working on already intialized epoch indices
|
|
if epoch == self._cur_epoch:
|
|
# re-enter so return
|
|
return
|
|
self._next_virtual_epoch(epoch)
|
|
|
|
def _get_epoch_start_index(self, epoch):
|
|
assert epoch >= 1 # fairseq is using 1-based epoch everywhere
|
|
return ((epoch - 1) % self.num_virtual_epochs) * self.virtual_epoch_size
|
|
|
|
def _next_global_indices(self, epoch):
|
|
rng = np.random.RandomState(
|
|
[
|
|
int(
|
|
hashlib.sha1(
|
|
str(self.__class__.__name__).encode("utf-8")
|
|
).hexdigest(),
|
|
16,
|
|
)
|
|
% (2 ** 32),
|
|
self.seed % (2 ** 32), # global seed
|
|
epoch, # epoch index,
|
|
]
|
|
)
|
|
del self._random_global_indices
|
|
self._random_global_indices = rng.choice(
|
|
self.virtual_size, self.virtual_size, replace=False
|
|
)
|
|
if self.load_next_shard is None:
|
|
self.load_next_shard = False
|
|
else:
|
|
# increase shard epoch for next loading
|
|
self.shard_epoch += 1
|
|
self.load_next_shard = True
|
|
logger.info(
|
|
"to load next epoch/shard in next load_dataset: "
|
|
f"epoch={epoch}/shard_epoch={self.shard_epoch}"
|
|
)
|
|
|
|
def _next_virtual_epoch(self, epoch):
|
|
index = self._get_epoch_start_index(epoch)
|
|
if index == 0 or self._random_global_indices is None:
|
|
# need to start from the beginning,
|
|
# so call super().set_epoch(epoch) to establish the global virtual indices
|
|
logger.info(
|
|
"establishing a new set of global virtual indices for "
|
|
f"epoch={epoch}/shard_epoch={self.shard_epoch}"
|
|
)
|
|
super().set_epoch(epoch)
|
|
self._next_global_indices(epoch)
|
|
else:
|
|
self._cur_epoch = epoch
|
|
|
|
# reset cache sizes and ordered_indices for the epoch after moving to a new epoch
|
|
self._clean_if_not_none(
|
|
[
|
|
self._epoch_sizes,
|
|
]
|
|
)
|
|
self._epoch_sizes = None
|
|
self._current_epoch_start_index = index
|