(以下内容搬运自 PaddleSpeech)
PaddleSpeech TTS 内使用的数据格式,数据处理惯例的说明。
在经历过一些开发实践之后,Parakeet 还是采用了先预处理数据再加载预处理好的数据进行训练的方式。此前我们曾经思考过把预处理写在 Dataset 的 __getitem__
里面,在访问到某一跳样例的时候进行再预处理,我们曾经这么实践过,但是遇到了一些问题。
首先是效率问题,在 Paddle 还没有用上多进程处理数据的时候,这么做很慢,即使 Paddle 有异步加载数据的设计,但是当 batch size 大的时候,每条样例都需要预处理,然后还要组 batch, 事实上花费的时间很多,甚至会严重拖慢训练过程。
其次是数据筛选问题,某些筛选条件依赖处理后的样例的特征,比如说根据文本长度来过滤太短的样本,但是如果我只有 __getitem__
之后才能知道文本的长度,那么筛选一次就需要把整个数据集处理一遍,那么懒惰加载的有点就消失了。另外,如果不预先筛选,在 __getitem__
中遇到长度太短的文本时抛异常,则会引起整个数据流异常,这样又不可行,因为 collate_fn
会预设每条样例的获取都能正常,这样才能正常工作。即使通过一些特殊标记,比如 None 来标记数据获取失败,让 collate_fn
跳过,也会导致出现 batch_size 变化这样的问题。因此把预处理完全放在__getitem__
来做并不现实。
因此,我们还是倾向于使用先预处理,再加载预处理后的数据这样的方式。在预处理的过程中,我们可以做筛选,然后也可以保存更多的中间特征,比如说文本长度,音频长度等,这些可以用于后续筛选。然后出于 TTS 方向的习惯,数据仍然常常以多个文件的方式存储,处理后的结果常用 npy 的格式存储起来。
我们此前不太愿意预处理是因为对于预处理产生的结果需要做一定的规范才方便使用,如果每个实验预处理的方式都不同,那么对于用户也会造成很多困扰,写 README 也会写得很麻烦。但是规定怎样的格式最合适这件事情,我们又不太敢给一个定论,因为可能后面会遇到表现力不足的问题。不过或来出于效率的问题,我们还是把预处理单独作为一个阶段,但是对于预处理后的文件组织方式没有一个很一贯的规定。
曾经一度是通过从一个文件夹中以一定的文件名 pattern 的方式筛选文件的方式来读取预处理后的数据集,但是最近经过最近参考其他的 repo, 以及参考了 kaldi 的 scp 格式。我们认为更好的方式是用一个类似列表的方式哦存储元数据,把文件路径存储在里面,这样就可以不受文件具体的存储位置制约,只要有元数据(文件清单)就可以按图索骥。除了文件路径之外,其他的元数据也可以存储在里面。比如文本的路径,音频的路径,频谱的路径,帧数,采样点个数等等。
然后对于路径来说,能做的事情当然就是打开。但是怎么打开就有很多选择,比如 sf.read
, np.load
等等,因此这里最好可以作为一个可以传的参数,我们甚至不想通过扩展名来对应读取方法,最好是让用户自己传,这样就可以自己定义 parse 数据的方法了。
那么,多个文件字段,就会需要多个读取方法。怎么传这些参数?何不更彻底一点,每个字段都需要一个读取方法,没有的则原样返回。至此,事情朝着 DataFrame 的方向迅速发展。其实这种元数据可以看作是一种表格数据,而读取方法其实就是数据类型,比如说 csv 读取的时候,指定每一列的数据类型,其实就是在进行数据解析,int('1')
和 np.load('0001.npy')
并没有本质的区别。
于是乎我们借鉴了 DataFrame 的设计,但是我们对于构建方式则简单一些,只需要一个 list of dicts, 这些 dicts 都是一条记录,我们就不支持 list of tuples 了,因为那样要考虑的东西比较多,list of dicts 这样的方式自带字段名称,也方便和 json, yaml 之类的格式交互。即使不是这个格式也可以处理成这个格式,然后再给一个字段名列表,表示只要这些字段的数据,然后再对于所选的每个字段,需要给一个 parser(接口里面叫做 converter), 这样就完成了。出于简单,我们也不考虑什么 NA, 缺失字段之类的事情。
然后我们再选定一种格式作为元数据保存到硬盘上的默认格式。json 存 list of records 的时候有两个方括号,也不方便流式读写拼接,所以我们用 jsonlines。不用 yaml 是因为 yaml 存 list of records 的时候占用的行数实在是太多了。
完成这些设计之后,DataTable 的表现力基本就足够了,可以应付我们日常用于表示数据集的需求了。同时,这里还添加了 cache 功能,并且使用了多进程的 Manager 来在多进程之间共享内存,在 num_workers 的时候保证不会每个子进程各自缓存一份。
DataTable 的实现可以参见 PaddleSpeech/paddlespeech/t2s/datasets/data_table.py.
class DataTable(Dataset):
"""Dataset to load and convert data for general purpose.
Parameters
----------
data : List[Dict[str, Any]]
Metadata, a list of meta datum, each of which is composed of
several fields
fields : List[str], optional
Fields to use, if not specified, all the fields in the data are
used, by default None
converters : Dict[str, Callable], optional
Converters used to process each field, by default None
use_cache : bool, optional
Whether to use cache, by default False
Raises
------
ValueError
If there is some field that does not exist in data.
ValueError
If there is some field in converters that does not exist in fields.
"""
def __init__(self,
data: List[Dict[str, Any]],
fields: List[str]=None,
converters: Dict[str, Callable]=None,
use_cache: bool=False):
其 __getitem__
方法则是每个字段用自己的 parser 解析一番,再组成字典返回。
def _convert(self, meta_datum: Dict[str, Any]) -> Dict[str, Any]:
"""Convert a meta datum to an example by applying the corresponding
converters to each fields requested.
Parameters
----------
meta_datum : Dict[str, Any]
Meta datum
Returns
-------
Dict[str, Any]
Converted example
"""
example = {}
for field in self.fields:
converter = self.converters.get(field, None)
meta_datum_field = meta_datum[field]
if converter is not None:
converted_field = converter(meta_datum_field)
else:
converted_field = meta_datum_field
example[field] = converted_field
return example
P.S. 欢迎关注我们的 github repo PaddleSpeech, 是基于飞桨 PaddlePaddle 的语音方向的开源模型库,用于语音和音频中的各种关键任务的开发,包含大量基于深度学习前沿和有影响力的模型。