data_preprocess.rst 11.7 KB
Newer Older
yuguo-Jack's avatar
yuguo-Jack committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
================
数据处理
================

Dataset中通常为原始数据,需要经过一定的数据处理并进行采样组batch,而后通过 :class:`paddle.io.DataLoader` 为训练或预测使用,PaddleNLP中为其中各环节提供了相应的功能支持。

基于预训练模型的数据处理
------------------------

在使用预训练模型做NLP任务时,需要加载对应的Tokenizer,PaddleNLP在 :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):
        querys = [example['query'] for example in examples]
        titles = [example['title'] for example in examples]
        tokenized_examples = tokenizer(text=querys, 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` 方法的匹配。

之后的流程与基于预训练模型的数据处理相同。