preprocess_data.py 10.8 KB
Newer Older
Mohammad's avatar
Mohammad committed
1
# coding=utf-8
Mohammad's avatar
Mohammad committed
2
# Copyright (c) 2020, NVIDIA CORPORATION.  All rights reserved.
Mohammad's avatar
Mohammad committed
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#
# 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.

"""Processing data for pretraining."""

18
19
20
import argparse
import json
import multiprocessing
Mohammad's avatar
Mohammad committed
21
import os
22
import sys
Mohammad's avatar
Mohammad committed
23
24
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
                                             os.path.pardir)))
25
26
import time

27
import numpy as np
28
29
30
31
32
33
34
import torch
try:
    import nltk
    nltk_available = True
except ImportError:
    nltk_available = False

35

36
37
from megatron.tokenizer import build_tokenizer
from megatron.data import indexed_dataset
38
39
from megatron.data.realm_dataset_utils import id_to_str_pos_map

40

Mohammad's avatar
Mohammad committed
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
# https://stackoverflow.com/questions/33139531/preserve-empty-lines-with-nltks-punkt-tokenizer
class CustomLanguageVars(nltk.tokenize.punkt.PunktLanguageVars):

    _period_context_fmt = r"""
        \S*                          # some word material
        %(SentEndChars)s             # a potential sentence ending
        \s*                       #  <-- THIS is what I changed
        (?=(?P<after_tok>
            %(NonWord)s              # either other punctuation
            |
            (?P<next_tok>\S+)     #  <-- Normally you would have \s+ here
        ))"""

class IdentitySplitter(object):
    def tokenize(self, *text):
        return text

class Encoder(object):
    def __init__(self, args):
        self.args = args

    def initializer(self):
        # Use Encoder class as a container for global data
        Encoder.tokenizer = build_tokenizer(self.args)
        if self.args.split_sentences:
            if not nltk_available:
                print("NLTK is not available to split sentences.")
                exit()
            splitter = nltk.load("tokenizers/punkt/english.pickle")
            if self.args.keep_newlines:
                # this prevents punkt from eating newlines after sentences
                Encoder.splitter = nltk.tokenize.punkt.PunktSentenceTokenizer(
                    train_text = splitter._params,
                    lang_vars = CustomLanguageVars())
            else:
                Encoder.splitter = splitter

        else:
            Encoder.splitter = IdentitySplitter()

82
83
84
85
86
87
88
89
        try:
            import spacy
            print("> Loading spacy")
            Encoder.spacy = spacy.load('en_core_web_lg')
            print(">> Finished loading spacy")
        except:
            Encoder.spacy = None

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
    def encode(self, json_line):
        data = json.loads(json_line)
        ids = {}
        for key in self.args.json_keys:
            text = data[key]
            doc_ids = []
            for sentence in Encoder.splitter.tokenize(text):
                sentence_ids = Encoder.tokenizer.tokenize(sentence)
                if len(sentence_ids) > 0:
                    doc_ids.append(sentence_ids)
            if self.args.append_eod:
                doc_ids[-1].append(Encoder.tokenizer.eod)
            ids[key] = doc_ids
        return ids, len(json_line)

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
    def encode_with_ner(self, json_line):
        if self.spacy is None:
            raise ValueError('Cannot do NER without spacy')

        data = json.loads(json_line)
        ids = {}
        ner_masks = {}
        for key in self.args.json_keys:
            text = data[key]
            doc_ids = []
            doc_ner_mask = []
            for sentence in Encoder.splitter.tokenize(text):
                sentence_ids = Encoder.tokenizer.tokenize(sentence)
                if len(sentence_ids) > 0:
                    doc_ids.append(sentence_ids)
                # sentence is cased?
                # print(sentence)

                entities = self.spacy(sentence).ents
                undesired_types = ['CARDINAL', 'TIME', 'PERCENT', 'MONEY', 'QUANTITY', 'ORDINAL']
                entities = [e for e in entities if e.text != "CLS" and e.label_ not in undesired_types]
                # entities = []

                masked_positions = []
                if len(entities) > 0:
                    entity_idx = np.random.randint(0, len(entities))
                    selected_entity = entities[entity_idx]

                    token_pos_map = id_to_str_pos_map(sentence_ids, Encoder.tokenizer)
                    mask_start = mask_end = 0
                    set_mask_start = False
                    while mask_end < len(token_pos_map) and token_pos_map[mask_end] < selected_entity.end_char:
                        if token_pos_map[mask_start] > selected_entity.start_char:
                            set_mask_start = True
                        if not set_mask_start:
                            mask_start += 1
                        mask_end += 1
                    masked_positions = list(range(mask_start - 1, mask_end))
                ner_mask = [0] * len(sentence_ids)
                for pos in masked_positions:
                    ner_mask[pos] = 1
                doc_ner_mask.append(ner_mask)

            if self.args.append_eod:
                doc_ids[-1].append(Encoder.tokenizer.eod)
                doc_ner_mask[-1].append(0)
            ids[key] = doc_ids
            ner_masks[key + '-ner'] = doc_ner_mask
        return ids, ner_masks, len(json_line)

155
156
157
158
159
160
161
162
163
164
165
166
167
168
def get_args():
    parser = argparse.ArgumentParser()
    group = parser.add_argument_group(title='input data')
    group.add_argument('--input', type=str, required=True,
                       help='Path to input JSON')
    group.add_argument('--json-keys', nargs='+', default=['text'],
                       help='space separate listed of keys to extract from json')
    group.add_argument('--split-sentences', action='store_true',
                       help='Split documents into sentences.')
    group.add_argument('--keep-newlines', action='store_true',
                       help='Keep newlines between sentences when splitting.')

    group = parser.add_argument_group(title='tokenizer')
    group.add_argument('--tokenizer-type', type=str, required=True,
Raul Puri's avatar
Raul Puri committed
169
                       choices=['BertWordPieceLowerCase','BertWordPieceCase',
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
                                'GPT2BPETokenizer'],
                       help='What type of tokenizer to use.')
    group.add_argument('--vocab-file', type=str, default=None,
                       help='Path to the vocab file')
    group.add_argument('--merge-file', type=str, default=None,
                       help='Path to the BPE merge file (if necessary).')
    group.add_argument('--append-eod', action='store_true',
                       help='Append an <eod> token to the end of a document.')


    group = parser.add_argument_group(title='output data')
    group.add_argument('--output-prefix', type=str, required=True,
                       help='Path to binary output file without suffix')
    group.add_argument('--dataset-impl', type=str, default='mmap',
                       choices=['lazy', 'cached', 'mmap'])

    group = parser.add_argument_group(title='runtime')
    group.add_argument('--workers', type=int, default=1,
                       help='Number of worker processes to launch')
    group.add_argument('--log-interval', type=int, default=100,
                       help='Interval between progress updates')
191
192
    group.add_argument('--create-ner-masks', action='store_true',
                       help='Also create mask tensors for salient span masking')
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
    args = parser.parse_args()
    args.keep_empty = False

    if args.tokenizer_type.lower().startswith('bert'):
        if not args.split_sentences:
            print("Bert tokenizer detected, are you sure you don't want to split sentences?")

    # some default/dummy values for the tokenizer
    args.rank = 0
    args.make_vocab_size_divisible_by = 128
    args.model_parallel_size = 1

    return args

def main():
    args = get_args()
    startup_start = time.time()

    print("Opening", args.input)
    fin = open(args.input, 'r', encoding='utf-8')

    if nltk_available and args.split_sentences:
        nltk.download("punkt", quiet=True)

    encoder = Encoder(args)
    tokenizer = build_tokenizer(args)
    pool = multiprocessing.Pool(args.workers, initializer=encoder.initializer)
220
221
222
223
224
    if args.create_ner_masks:
        encoded_docs = pool.imap(encoder.encode_with_ner, fin, 25)
    else:
        encoded_docs = pool.imap(encoder.encode, fin, 25)
        #encoded_docs = map(encoder.encode, fin)
225

226
227
228
229
    level = "document"
    if args.split_sentences:
        level = "sentence"

230
231
232
233
234
    print(f"Vocab size: {tokenizer.vocab_size}")
    print(f"Output prefix: {args.output_prefix}")
    output_bin_files = {}
    output_idx_files = {}
    builders = {}
235
236
237
238
    output_keys = args.json_keys.copy()
    if args.create_ner_masks:
        output_keys.extend([key + '-ner' for key in output_keys])
    for key in output_keys:
239
240
241
242
        output_bin_files[key] = "{}_{}_{}.bin".format(args.output_prefix,
                                                      key, level)
        output_idx_files[key] = "{}_{}_{}.idx".format(args.output_prefix,
                                                      key, level)
243
244
245
246
247
248
249
250
251
        builders[key] = indexed_dataset.make_builder(output_bin_files[key],
                                               impl=args.dataset_impl,
                                               vocab_size=tokenizer.vocab_size)

    startup_end = time.time()
    proc_start = time.time()
    total_bytes_processed = 0
    print("Time to startup:", startup_end - startup_start)

252
253
254
255
256
257
258
    # for i, (doc, bytes_processed) in enumerate(encoded_docs, start=1):
    for i, doc_data in enumerate(encoded_docs, start=1):
        if args.create_ner_masks:
            doc, ner_masks, bytes_processed = doc_data
        else:
            doc, bytes_processed = doc_data

259
260
261
262
263
        total_bytes_processed += bytes_processed
        for key, sentences in doc.items():
            for sentence in sentences:
                builders[key].add_item(torch.IntTensor(sentence))
            builders[key].end_document()
264
265
266
267
268
269
        if args.create_ner_masks:
            for key, sentence_masks in ner_masks.items():
                for mask in sentence_masks:
                    builders[key].add_item(torch.IntTensor(mask))
                builders[key].end_document()

270
271
272
273
274
275
276
277
        if i % args.log_interval == 0:
            current = time.time()
            elapsed = current - proc_start
            mbs = total_bytes_processed/elapsed/1024/1024
            print(f"Processed {i} documents",
                  f"({i/elapsed} docs/s, {mbs} MB/s).",
                  file=sys.stderr)

278
    for key in output_keys:
279
280
281
282
        builders[key].finalize(output_idx_files[key])

if __name__ == '__main__':
    main()