Files
paddlepaddle--paddle/test/dygraph_to_static/bert_utils.py
T
2026-07-13 12:40:42 +08:00

362 lines
12 KiB
Python

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# 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.
import numpy as np
SEED = 2020
def get_bert_config():
bert_config = {
"attention_probs_dropout_prob": 0.1,
"directionality": "bidi",
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 2,
"initializer_range": 0.02,
"intermediate_size": 72,
"max_position_embeddings": 512,
"num_attention_heads": 2,
"num_hidden_layers": 2,
"pooler_fc_size": 2,
"pooler_num_attention_heads": 2,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 8,
"pooler_type": "first_token_transform",
"type_vocab_size": 2,
"vocab_size": 21128,
}
return bert_config
def mask(batch_tokens, total_token_num, vocab_size, CLS=1, SEP=2, MASK=3):
"""
Add mask for batch_tokens, return out, mask_label, mask_pos;
Note: mask_pos responding the batch_tokens after padded;
"""
max_len = max([len(sent) for sent in batch_tokens])
mask_label = []
mask_pos = []
# NOTE: numpy random is not thread-safe, for async DataLoader,
# using np.random.seed() directly is risky, using RandomState
# class is a better way
self_random = np.random.RandomState(SEED)
prob_mask = self_random.rand(total_token_num)
# Note: the first token is [CLS], so [low=1]
replace_ids = self_random.randint(1, high=vocab_size, size=total_token_num)
pre_sent_len = 0
prob_index = 0
for sent_index, sent in enumerate(batch_tokens):
mask_flag = False
prob_index += pre_sent_len
for token_index, token in enumerate(sent):
prob = prob_mask[prob_index + token_index]
if prob > 0.15:
continue
elif 0.03 < prob <= 0.15:
# mask
if token != SEP and token != CLS:
mask_label.append(sent[token_index])
sent[token_index] = MASK
mask_flag = True
mask_pos.append(sent_index * max_len + token_index)
elif 0.015 < prob <= 0.03:
# random replace
if token != SEP and token != CLS:
mask_label.append(sent[token_index])
sent[token_index] = replace_ids[prob_index + token_index]
mask_flag = True
mask_pos.append(sent_index * max_len + token_index)
else:
# keep the original token
if token != SEP and token != CLS:
mask_label.append(sent[token_index])
mask_pos.append(sent_index * max_len + token_index)
pre_sent_len = len(sent)
# ensure at least mask one word in a sentence
while not mask_flag:
token_index = int(
self_random.randint(1, high=len(sent) - 1, size=1)
)
if sent[token_index] != SEP and sent[token_index] != CLS:
mask_label.append(sent[token_index])
sent[token_index] = MASK
mask_flag = True
mask_pos.append(sent_index * max_len + token_index)
mask_label = np.array(mask_label).astype("int64").reshape([-1, 1])
mask_pos = np.array(mask_pos).astype("int64").reshape([-1, 1])
return batch_tokens, mask_label, mask_pos
def pad_batch_data(
insts,
pad_idx=0,
return_pos=False,
return_input_mask=False,
return_max_len=False,
return_num_token=False,
):
"""
Pad the instances to the max sequence length in batch, and generate the
corresponding position data and input mask.
"""
return_list = []
max_len = max(len(inst) for inst in insts)
# Any token included in dict can be used to pad, since the paddings' loss
# will be masked out by weights and make no effect on parameter gradients.
inst_data = np.array(
[list(inst) + list([pad_idx] * (max_len - len(inst))) for inst in insts]
)
return_list += [inst_data.astype("int64").reshape([-1, max_len])]
# position data
if return_pos:
inst_pos = np.array(
[
list(range(0, len(inst))) + [pad_idx] * (max_len - len(inst))
for inst in insts
]
)
return_list += [inst_pos.astype("int64").reshape([-1, max_len])]
if return_input_mask:
# This is used to avoid attention on paddings.
input_mask_data = np.array(
[[1] * len(inst) + [0] * (max_len - len(inst)) for inst in insts]
)
input_mask_data = np.expand_dims(input_mask_data, axis=-1)
return_list += [input_mask_data.astype("float32")]
if return_max_len:
return_list += [max_len]
if return_num_token:
num_token = 0
for inst in insts:
num_token += len(inst)
return_list += [num_token]
return return_list if len(return_list) > 1 else return_list[0]
def prepare_batch_data(
insts,
total_token_num,
voc_size=0,
pad_id=None,
cls_id=None,
sep_id=None,
mask_id=None,
return_input_mask=True,
return_max_len=True,
return_num_token=False,
):
"""
1. generate Tensor of data
2. generate Tensor of position
3. generate self attention mask, [shape: batch_size * max_len * max_len]
"""
batch_src_ids = [inst[0] for inst in insts]
batch_sent_ids = [inst[1] for inst in insts]
batch_pos_ids = [inst[2] for inst in insts]
labels_list = []
for i in range(3, len(insts[0]), 1):
labels = [inst[i] for inst in insts]
labels = np.array(labels).astype("int64").reshape([-1, 1])
labels_list.append(labels)
# First step: do mask without padding
if mask_id >= 0:
out, mask_label, mask_pos = mask(
batch_src_ids,
total_token_num,
vocab_size=voc_size,
CLS=cls_id,
SEP=sep_id,
MASK=mask_id,
)
else:
out = batch_src_ids
# Second step: padding
src_id, self_input_mask = pad_batch_data(
out, pad_idx=pad_id, return_input_mask=True
)
pos_id = pad_batch_data(
batch_pos_ids, pad_idx=pad_id, return_pos=False, return_input_mask=False
)
sent_id = pad_batch_data(
batch_sent_ids,
pad_idx=pad_id,
return_pos=False,
return_input_mask=False,
)
if mask_id >= 0:
return_list = [
src_id,
pos_id,
sent_id,
self_input_mask,
mask_label,
mask_pos,
*labels_list,
]
else:
return_list = [src_id, pos_id, sent_id, self_input_mask, *labels_list]
res = return_list if len(return_list) > 1 else return_list[0]
return res
class DataReader:
def __init__(
self,
batch_size=4096,
in_tokens=True,
max_seq_len=512,
shuffle_files=False,
epoch=100,
voc_size=0,
is_test=False,
generate_neg_sample=False,
):
self.batch_size = batch_size
self.in_tokens = in_tokens
self.shuffle_files = shuffle_files
self.epoch = epoch
self.current_epoch = 0
self.current_file_index = 0
self.total_file = 0
self.current_file = None
self.voc_size = voc_size
self.max_seq_len = max_seq_len
self.pad_id = 0
self.cls_id = 101
self.sep_id = 102
self.mask_id = 103
self.is_test = is_test
self.generate_neg_sample = generate_neg_sample
if self.in_tokens:
assert self.batch_size >= self.max_seq_len, (
"The number of "
"tokens in batch should not be smaller than max seq length."
)
if self.is_test:
self.epoch = 1
self.shuffle_files = False
def build_fake_data(self):
for _ in range(1000000):
# NOTE: python random has bug in python2,
# we should avoid using random module,
# please using numpy.random
self_random = np.random.RandomState(SEED)
sent0_len = self_random.randint(50, 100)
sent1_len = self_random.randint(50, 100)
token_ids = (
[1]
+ [self_random.randint(0, 10000) for i in range(sent0_len - 1)]
+ [self_random.randint(0, 10000) for i in range(sent1_len - 1)]
+ [2]
)
sent_ids = [0 for i in range(sent0_len)] + [
1 for i in range(sent1_len)
]
pos_ids = list(range(sent0_len + sent1_len))
label = 1
yield token_ids, sent_ids, pos_ids, label
def data_generator(self):
def wrapper():
def reader():
for epoch in range(self.epoch):
self.current_epoch = epoch + 1
sample_generator = self.build_fake_data()
for sample in sample_generator:
if sample is None:
continue
yield sample
def batch_reader(reader, batch_size, in_tokens):
batch, total_token_num, max_len = [], 0, 0
for parsed_line in reader():
token_ids, sent_ids, pos_ids, label = parsed_line
max_len = max(max_len, len(token_ids))
if in_tokens:
to_append = (len(batch) + 1) * max_len <= batch_size
else:
to_append = len(batch) < batch_size
if to_append:
batch.append(parsed_line)
total_token_num += len(token_ids)
else:
yield batch, total_token_num
batch, total_token_num, max_len = (
[parsed_line],
len(token_ids),
len(token_ids),
)
if len(batch) > 0:
yield batch, total_token_num
for batch_data, total_token_num in batch_reader(
reader, self.batch_size, self.in_tokens
):
yield prepare_batch_data(
batch_data,
total_token_num,
voc_size=self.voc_size,
pad_id=self.pad_id,
cls_id=self.cls_id,
sep_id=self.sep_id,
mask_id=self.mask_id,
return_input_mask=True,
return_max_len=False,
return_num_token=False,
)
return wrapper
class ModelHyperParams:
generate_neg_sample = False
epoch = 100
max_seq_len = 512
batch_size = 8192
in_tokens = True
def get_feed_data_reader(bert_config):
args = ModelHyperParams()
data_reader = DataReader(
batch_size=args.batch_size,
in_tokens=args.in_tokens,
voc_size=bert_config['vocab_size'],
epoch=args.epoch,
max_seq_len=args.max_seq_len,
generate_neg_sample=args.generate_neg_sample,
)
return data_reader