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

213 lines
12 KiB
ReStructuredText
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
================
数据处理
================
Dataset中通常为原始数据,需要经过一定的数据处理并进行采样组batch,而后通过 :class:`paddle.io.DataLoader` 为训练或预测使用,PaddleNLP中为其中各环节提供了相应的功能支持。
基于预训练模型的数据处理
------------------------
在使用预训练模型做NLP任务时,需要加载对应的TokenizerPaddleNLP在 :class:`PreTrainedTokenizer` 中内置的 :func:`__call__` 方法可以实现基础的数据处理功能。PaddleNLP内置的所有预训练模型的Tokenizer都继承自 :class:`PreTrainedTokenizer` ,下面以BertTokenizer举例说明:
.. code-block::
from paddlenlp.transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
# 单句转换(单条数据)
print(tokenizer(text='天气不错')) # {'input_ids': [101, 1921, 3698, 679, 7231, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0]}
# 句对转换(单条数据)
print(tokenizer(text='天气',text_pair='不错')) # {'input_ids': [101, 1921, 3698, 102, 679, 7231, 102], 'token_type_ids': [0, 0, 0, 0, 1, 1, 1]}
# 单句转换(多条数据)
print(tokenizer(text=['天气','不错'])) # [{'input_ids': [101, 1921, 3698, 102], 'token_type_ids': [0, 0, 0, 0]},
# {'input_ids': [101, 679, 7231, 102], 'token_type_ids': [0, 0, 0, 0]}]
关于 :func:`__call__` 方法的其他参数和功能,请查阅PreTrainedTokenizer。
paddlenlp内置的 :class:`paddlenlp.datasets.MapDataset`:func:`map` 方法支持传入一个函数,对数据集内的数据进行统一转换。下面我们以 :obj:`LCQMC` 的数据处理流程为例:
.. code-block::
from paddlenlp.transformers import BertTokenizer
from paddlenlp.datasets import load_dataset
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
train_ds = load_dataset('lcqmc', splits='train')
print(train_ds[0]) # {'query': '喜欢打篮球的男生喜欢什么样的女生', 'title': '爱打篮球的男生喜欢什么样的女生', 'label': 1}
可以看到, :obj:`LCQMC` 是一个句对匹配任务,即判断两个句子的意思是否相似的2分类任务。我们需要处理的是key为 **query****title** 的文本数据,我们编写基于 :class:`PreTrainedTokenizer` 的数据处理函数并传入数据集的 :func:`map` 方法。
.. code-block::
def convert_example(example, tokenizer):
tokenized_example = tokenizer(
text=example['query'],
text_pair=example['title'])
# 加上label用于训练
tokenized_example['label'] = [example['label']]
return tokenized_example
from functools import partial
trans_func = partial(
convert_example,
tokenizer=tokenizer)
train_ds.map(trans_func)
print(train_ds[0]) # {'input_ids': [101, 1599, 3614, 2802, 5074, 4413, 4638, 4511, 4495,
# 1599, 3614, 784, 720, 3416, 4638, 1957, 4495, 102,
# 4263, 2802, 5074, 4413, 4638, 4511, 4495, 1599, 3614,
# 784, 720, 3416, 4638, 1957, 4495, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# 'label': [1]}
可以看到,数据集中的文本数据已经被处理成了模型可以接受的 *feature*
:func:`map` 方法有一个重要的参数 :attr:`batched`,当设置为 :obj:`True` 时(默认为 :obj:`False` ),数据处理函数 :func:`trans_func` 的输入不再是单条数据,而是数据集的所有数据:
.. code-block::
def convert_examples(examples, tokenizer):
queries = [example['query'] for example in examples]
titles = [example['title'] for example in examples]
tokenized_examples = tokenizer(text=queries, text_pair=titles,return_dict=False)
# 加上label用于训练
for idx in range(len(tokenized_examples)):
tokenized_examples[idx]['label'] = [examples[idx]['label']]
return tokenized_examples
from functools import partial
trans_func = partial(convert_examples, tokenizer=tokenizer)
train_ds.map(trans_func, batched=True)
print(train_ds[0]) # {'input_ids': [101, 1599, 3614, 2802, 5074, 4413, 4638, 4511, 4495,
# 1599, 3614, 784, 720, 3416, 4638, 1957, 4495, 102,
# 4263, 2802, 5074, 4413, 4638, 4511, 4495, 1599, 3614,
# 784, 720, 3416, 4638, 1957, 4495, 102],
# 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
# 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
# 'label': [1]}
可以看到,在本例中两种实现的结果是相同的。但是在诸如阅读理解,对话等任务中,一条原始数据可能会产生多个 *feature* 的情况(参见 `run_squad.py <https://github.com/PaddlePaddle/PaddleNLP/blob/develop/examples/machine_reading_comprehension/SQuAD/run_squad.py>`__ )通常需要将 :attr:`batched` 参数设置为 :obj:`True`
:func:`map` 方法还有一个 :attr:`num_workers` 参数,当其大于0时进行多进程数据处理,可以提高处理速度。但是需要注意如果在数据处理的函数中用到了 **数据index** 的相关信息,多进程处理可能会导致错误的结果。
关于 :func:`map` 方法的其他参数和 :class:`paddlenlp.datasets.MapDataset` 的其他数据处理方法,请查阅 :doc:`dataset <../source/paddlenlp.datasets.dataset>`
Batchify
-----------
PaddleNLP内置了多种collate function,配合 :class:`paddle.io.BatchSampler` 可以协助用户简单的完成组batch的操作。
我们继续以 :obj:`LCQMC` 的数据处理流程为例。从上一节最后可以看到,处理后的单条数据是一个 **字典** ,包含 `input_ids` `token_type_ids``label` 三个key。
其中 `input_ids``token_type_ids` 是需要进行 **padding** 操作后输入模型的,而 `label` 是需要 **stack** 之后传入loss function的。
因此,我们使用PaddleNLP内置的 :func:`Dict` :func:`Stack`:func:`Pad` 函数整理batch中的数据。最终的 :func:`batchify_fn` 如下:
.. code-block::
from paddlenlp.data import Dict, Stack, Pad
# 使用Dict函数将Pad,Stack等函数与数据中的键值相匹配
train_batchify_fn = lambda samples, fn=Dict({
'input_ids': Pad(axis=0, pad_val=tokenizer.pad_token_id),
'token_type_ids': Pad(axis=0, pad_val=tokenizer.pad_token_type_id),
'label': Stack(dtype="int64")
}): fn(samples)
之后使用 :class:`paddle.io.BatchSampler`:func:`batchify_fn` 构建 :class:`paddle.io.DataLoader`
.. code-block::
from paddle.io import DataLoader, BatchSampler
train_batch_sampler = BatchSampler(train_ds, batch_size=2, shuffle=True)
train_data_loader = DataLoader(dataset=train_ds, batch_sampler=train_batch_sampler, collate_fn=train_batchify_fn)
到此,一个完整的数据准备流程就完成了。关于更多batchify方法,请查阅 :doc:`collate <../source/paddlenlp.data.collate>`
.. note::
- 当需要进行 **单机多卡** 训练时,需要将 :class:`BatchSampler` 更换为 :class:`DistributedBatchSampler` 。更多有关 :class:`paddle.io.BatchSampler` 的信息,请查阅 `BatchSampler <https://www.paddlepaddle.org.cn/documentation/docs/zh/api/paddle/fluid/dataloader/batch_sampler/BatchSampler_cn.html>`_
- 当需要诸如batch内排序,按token组batch等更复杂的组batch功能时。可以使用PaddleNLP内置的 :class:`SamplerHelper` 。相关用例请参考 `reader.py <https://github.com/PaddlePaddle/PaddleNLP/blob/develop/examples/machine_translation/transformer/reader.py>`__
基于非预训练模型的数据处理
-------------------------
在使用非预训练模型做NLP任务时,我们可以借助PaddleNLP内置的 :class:`JiebaTokenizer`:class:`Vocab` 完成数据处理的相关功能,整体流程与使用预训练模型基本相似。我们以中文情感分析 :obj:`ChnSentiCorp` 数据集为例:
.. code-block::
from paddlenlp.data import JiebaTokenizer, Vocab
from paddlenlp.datasets import load_dataset
train_ds = load_dataset('chnsenticorp', splits='train')
print(train_ds[0]) # {'text': '选择珠江花园的原因就是方便,有电动扶梯直接到达海边,周围餐馆、食廊、商场、超市、摊位一应俱全。
# 酒店装修一般,但还算整洁。 泳池在大堂的屋顶,因此很小,不过女儿倒是喜欢。 包的早餐是西式的,还算丰富。
# 服务吗,一般', 'label': 1}
# 从本地词典文件构建Vocab
vocab = Vocab.load_vocabulary('./senta_word_dict.txt', unk_token='[UNK]', pad_token='[PAD]')
# 使用Vocab初始化JiebaTokenizer
tokenizer = JiebaTokenizer(vocab)
.. note::
- :class:`Vocab` 除了可以从本地词典文件初始化之外,还提供多种初始化方法,包括从 :class:`dictionary` 创建、从数据集创建等。详情请查阅Vocab。
- 除了使用内置的 :class:`JiebaTokenizer` 外,用户还可以使用任何自定义的方式或第三方库进行分词,之后使用 :func:`Vocab.to_indices` 方法将token转为id。
之后与基于预训练模型的数据处理流程相似,编写数据处理函数并传入 :func:`map` 方法:
.. code-block::
def convert_example(example, tokenizer):
input_ids = tokenizer.encode(example["text"])
valid_length = [len(input_ids)]
label = [example["label"]]
return input_ids, valid_length, label
trans_fn = partial(convert_example, tokenizer=tokenizer)
train_ds.map(trans_fn)
print(train_ds[0]) # ([417329, 128448, 140437, 173188, 118001, 213058, 595790, 1106339, 940533, 947744, 169206,
# 421258, 908089, 982848, 1106339, 35413, 1055821, 4782, 377145, 4782, 238721, 4782, 642263,
# 4782, 891683, 767091, 4783, 672971, 774154, 1250380, 1106339, 340363, 146708, 1081122,
# 4783, 1, 943329, 1008467, 319839, 173188, 909097, 1106339, 1010656, 261577, 1110707,
# 1106339, 770761, 597037, 1068649, 850865, 4783, 1, 993848, 173188, 689611, 1057229, 1239193,
# 173188, 1106339, 146708, 427691, 4783, 1, 724601, 179582, 1106339, 1250380],
# [67],
# [1])
可以看到,原始数据已经被处理成了 *feature* 。但是这里我们发现单条数据并不是一个 **字典** ,而是 **元组** 。所以我们的 :func:`batchify_fn` 也要相应的做一些调整:
.. code-block::
from paddlenlp.data import Tuple, Stack, Pad
# 使用Tuple函数将Pad,Stack等函数与数据中的键值相匹配
train_batchify_fn = lambda samples, fn=Tuple((
Pad(axis=0, pad_val=vocab.token_to_idx.get('[PAD]', 0)), # input_ids
Stack(dtype="int64"), # seq len
Stack(dtype="int64") # label
)): fn(samples)
可以看到,:func:`Dict` 函数是将单条数据中的键值与 :func:`Pad` 等函数进行对应,适用于单条数据是字典的情况。而 :func:`Tuple` 是通过单条数据中不同部分的index进行对应的。
所以需要 **注意** 的是 :func:`convert_example` 方法和 :func:`batchify_fn` 方法的匹配。
之后的流程与基于预训练模型的数据处理相同。