Bert概述
Bert:Bidirectional Encoder Representations from Transformers,来自Transformer的双向编码器表示。其重点在于一个对未标记文本预训练的过程来训练一个双向的表示,在正式训练过程中通过一个额外的输出层对模型进行微调从而达到模型适用任务的广泛性。
Bert的大致过程为首先基于无标记的数据进行无监督的预训练,然后根据应用下游不同的任务类型进行微调。微调过程使用相应下游任务的带有标记的数据进行,使用预训练模型的参数对微调的模型进行初始化。
Bert预训练
Bert的模型架构是一个多层的双向的Transformer的编码器,Transformer由编码器Encoder和解码器Decoder组成。传统的语言模型通常使用单向的语言模型来进行训练,而Bert预训练使用双向Transformer,其构造的输入是通过给定的Token、其segment以及其position embedding的vector来进行求和得到。在给定的预训练未标记文本序列中设置[CLS](类别标识)和[SEP](分割标识)的特殊Token的存在,Bert使用拥有30000个Token的词汇表的单词块潜入Vector作为文本句子的初始的Token值。
Bert的一大特征是使用了Masked Language Model,并且作为深度双向的语言模型存在,这样可以让每一个Word更好的结合自己的Context,有利于在多层的Context中进行对目标Word的预测。训练深度的双向表示采用Masked的方法随机屏蔽某些百分比的输入信号,它最终只对Masked的Word进行预测,而不需要重新构造出整个输入。这里会出现一个问题:在预训练和后续的微调之间造成一定程度的不匹配,微调的过程不会出现被mask的标记,所以这里采用了一种机制,并不总是使用实际的[Mask]标记来对Masked的Word进行替换,包括10%的随机Token和10%的正确Token。
对于不同的下游任务,有一部分依赖于句子之间的关系,比如Q&A和NLI任务等,普通的基于Token的语言建模无法对这个需要的关系进行提取,所以引入了NSP即下一个句子的预测的预训练模式,在预训练过程中为每个示例选择50%的正确句子序列,剩下50%挑选随机的错误序列,这样进行的预训练对以上需要依赖句子关系的任务帮助很大。
Bert微调
微调是直接进行的,Transformer中的比较显著的自注意力机制可以通过切换适当的输入输出来模拟不同类型的下游任务。对于每个任务,我们只需要将特定于任务的Input和Output插入到Bert中,并在端到端的场景下微调其所有参数。在输出时,Token表示被反馈到输出层,用于Token级任务,如序列标记或问答,而[CLS]表示被反馈到输出层,用于分类,如定制或情绪分析。
Bert Elastic Demo
基于:https://github.com/649453932/Bert-Chinese-Text-Classification-Pytorch
该Demo是基于Bert-Pytorch进行完善的中文文本分类任务Demo, 为基于Bert中文语义的预训练模型的文本分类任务微调的训练Demo。根据Bert的训练过程,该Demo的训练过程大致可以分为:加载预训练模型、加载数据集、训练、验证这几个步骤。
预训练模型采用huggingface在AWS上开源的中文预训练模型,单词表采用中文的全角字符表,数据集采用开源的THUCNews数据集。因为训练的结构是一个神经网络的全连接层,作为通用的DeepLearning网络结构存在,所以其Elastic改造过程参考通用的深度学习改造过程。
Horovod作为Uber开源的一个支持弹性分布式训练的框架被广泛应用,Horovod弹性具体流程以及基于ResNet分类Imagenet的Demo见https://blog.csdn.net/qq_44564671/article/details/122125877。
首先是引入Horovod.torch,并设置rank=0的Worker作为LogWriter:
hvd.init()
dataset = 'THUCNews'
model_name = args.model
config = x.Config(dataset, args.batch_size, args.learning_rate)
np.random.seed(1)
torch.manual_seed(1)
torch.cuda.manual_seed_all(1)
torch.backends.cudnn.deterministic = True
log_writer = SummaryWriter(args.log_dir) if hvd.rank() == 0 else None
接着是对数据集的切分,因为该Demo的数据集为一个单一的文本,Horovod没有提供像对Torchvision Dataset这样相应的切割方法进行数据集的切割的API,所以真正的数据并行需要自行解决。这里有两种方式进行数据集的切分:1.采用实体文件的切割方式,将现有文本数据集读取转存为粒度更小的单个数据格式,然后再根据弹性节点的数量来进行读入,适合整体数据集体量较大的情况;2.直接读入全部数据集,然后进行逻辑上的数据集分割,每个Worker只在逻辑分配到自己节点上的数据集进行训练。由于本Demo的数据集较小,所以采用第二种方法进行数据集的读入,这里采用hvd.size()来获取实时的弹性节点数量,然后根据rank进行相应数据的读取:
train_data, dev_data, test_data = build_dataset(config)
train_dataset_size = int(len(train_data) / hvd.size())
test_dataset_size = int(len(test_data) / hvd.size())
train_iter = build_iterator(train_data[hvd.rank() * train_dataset_size:(hvd.rank() + 1) * train_dataset_size], config)
test_iter = build_iterator(test_data[hvd.rank() * test_dataset_size:(hvd.rank() + 1) * test_dataset_size], config)
接着就需要对Optimizer进行定义,采用BertAdam作为Optimizer。然后将其重定义为DistributedOptimizer,其参数均采用通用深度学习的参数进行赋值:
optimizer = BertAdam(optimizer_grouped_parameters,
lr=config.learning_rate,
warmup=0.05,
t_total=len(train_iter) * config.num_epochs)
compression = hvd.Compression.fp16
optimizer = hvd.DistributedOptimizer(
optimizer, named_parameters=model.named_parameters(),
compression=compression,
backward_passes_per_step=1,
op=hvd.Average,
gradient_predivide_factor=1.0)
为了进行深度学习参数同步的状态判断,所以引入TorchState进行全局状态传递:
state = hvd.elastic.TorchState(model=model,
optimizer=optimizer,
batch=0,
epoch=0)
full_train(state)
@hvd.elastic.run
def full_train(state):
while state.epoch < args.epochs:
train(state, model, train_iter)
evaluate(state.epoch, test_iter)
end_epoch(state)
def end_epoch(state):
state.epoch += 1
state.commit()
为了便于获取全局的指标,引入Metric类进行每个Worker上的相关指标的AllReduce,均采用均值计算:
class Metric(object):
def __init__(self, name):
self.name = name
self.sum = torch.tensor(0.)
self.n = torch.tensor(0.)
def update(self, val):
self.sum += hvd.allreduce(val.detach().cpu(), name=self.name)
self.n += 1
@property
def avg(self):
return self.sum / self.n
具体的训练过程与原Demo差别不大,只不过加入了对state传入的参数的判别,以及指标的同步,具体的模型的反向传播的参数的同步均由DistributedOptimizer完成:
def train(state, model, train_iter):
start_time = time.time()
model.train()
epoch = state.epoch
batch_offset = state.batch
numbers = 0
train_acc = Metric('train_accuracy')
train_loss = Metric('train_loss')
for idx, (trains, labels) in enumerate(train_iter):
state.batch = batch_offset + idx
if args.batches_per_commit > 0 and \
state.batch % args.batches_per_commit == 0:
state.commit()
elif args.batches_per_host_check > 0 and \
state.batch % args.batches_per_host_check == 0:
state.check_host_updates()
optimizer.zero_grad()
for i in range(0, len(trains), args.batch_size):
numbers += 1
data_batch = trains[i:i + args.batch_size]
labels_batch = labels[i:i + args.batch_size]
outputs = model(data_batch)
model.zero_grad()
loss = F.cross_entropy(outputs, labels)
loss.backward()
optimizer.step()
true = labels_batch.data.cpu()
predict = torch.max(outputs.data, 1)[1].cpu()
train_acc.update(metrics.accuracy_score(true, predict))
train_loss.update(loss)
time_dif = get_time_dif(start_time)
msg = 'Epoch: {0:>6}, Train Loss: {1:>5.2}, Train Acc: {2:>6.2%}, Time: {5}'
print(msg.format(epoch, train_loss.avg.item(), train_acc.avg.item(), time_dif))
model.train()
if log_writer:
log_writer.add_scalar('train/loss', train_loss.avg, epoch)
log_writer.add_scalar('train/accuracy', train_acc.avg, epoch)
state.commit()
由于评估方法不需要进行反向传播更新参数,所以需要修改的地方仅仅再指标的同步部分。
然后整体的Demo改造Elastic就基本完成了,最后需要根据需求编写相应Dockerfile进行Docker Image的build,通过编写Yaml或者Arena工具使用Elastic Training Operator进行部署即可运行在Kubernetes集群上。