image


BERT

1. BERT-DNN文本分类

image

基本结构及流程:假设现在有情感分类任务,定义其任务为输入一句话,判断其情感倾向(正向、负向、中性)

  1. 输入句子长度小于500的文本x
  2. 通过BERT获取句子表示(假设为v,其维度为768维)
  3. v输入到一个深度神经网络(Deep Neural Networks,DNN,其实就是个全连接层),得到输出为v_1,其维度为64维
  4. v_1接dropout层,得到输出v_1',其维度与v_1相同
  5. v_1'再接入第二个DNN,得到输出为v_2,其维度为3
  6. 将文本x对应的标签转为one_hot向量y
  7. 计算v_2y的交叉熵损失(tf.nn.softmax_cross_entropy_with_logits

通过以上过程,可以基于bert训练一个效果尚可的情感分类模型。

对于以上bert-dnn结构的模型,如果稍加修改即可变为多标签分类,即将tf.nn.softmax_cross_entropy_with_logits改为sigmoid_cross_entropy_with_logits即可。

实践过的项目:

  • 内容营销
  • 勘探院文档分类
tensorflow版bert_embed
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
def bert_embed(self, bert_init=True):
bert_config_file = self.bert_config_file
bert_config = BertConfig.from_json_file(bert_config_file)
batch_size, max_seq_length = get_shape_list(self.input_ids)
# bert_mask = tf.pad(self.input_x_mask, [[0, 0], [2, 0]], constant_values=1)
bert_mask = self.input_mask
model = BertModel(
config=bert_config,
is_training=self.is_training,
input_ids=self.input_ids,
input_mask=bert_mask,
token_type_ids=self.token_type_ids,
use_one_hot_embeddings=False)

layer_logits = []
for i, layer in enumerate(model.all_encoder_layers):
layer_logits.append(
tf.layers.dense(
layer, 1,
kernel_initializer=tf.truncated_normal_initializer(stddev=0.02),
kernel_regularizer=tf.contrib.layers.l2_regularizer(self.l2_lambda),
name="layer_logit%d" % i
) / np.sqrt(768)
)

layer_logits = tf.concat(layer_logits, axis=2)
# add /sqrt(dim)
# layer_logits = tf.multiply(layer_logits, 1.0/ math.sqrt(768))
layer_dist = tf.nn.softmax(layer_logits)
seq_out = tf.concat([tf.expand_dims(x, axis=2) for x in model.all_encoder_layers], axis=2)
pooled_output = tf.matmul(tf.expand_dims(layer_dist, axis=2), seq_out)
pooled_output = tf.squeeze(pooled_output, axis=2)
pooled_layer = pooled_output
# char_bert_outputs = pooled_layer[:, 1: max_seq_length - 1, :]
char_bert_outputs = pooled_layer[:, :, :]

final_hidden_states = char_bert_outputs
# final_hidden_states = model.all_encoder_layers[-1]
tvars = tf.trainable_variables()
init_checkpoint = self.bert_init_checkpoint
assignment_map, initialized_variable_names = get_assignment_map_from_checkpoint(tvars, init_checkpoint)
if bert_init:
tf.train.init_from_checkpoint(init_checkpoint, assignment_map)

tf.logging.info("**** Trainable Variables ****")
for var in tvars:
init_string = ""
if var.name in initialized_variable_names:
init_string = ", *INIT_FROM_CKPT*"
# tf.logging.info(" name = %s, shape = %s%s", var.name, var.shape, init_string)
print(" name = %s, shape = %s%s", var.name, var.shape, init_string)

return final_hidden_states

2. BERT-CNN文本分类

在勘探院任务中,除了上述的bert-dnn结构,还魔改了一个bert-cnn结构。简单来说是在获取句子表示后不直接接入全连接层,而是接入[3,4,5]的卷积层提取特征并预测,这部分结构与论文textcnn完全一致,故不赘述。

然而,最终的效果并没有提升,猜测的原因是bert emb后的向量本身就能够表示文本特征,这与word2vec+cnn提取特征的流程是类似的,因此在提取的特征上再用cnn提取反而将事情变得复杂,造成结果不佳。

  • bert-dnn
    • avg_f1: 0.38006882359742533
    • avg_f1_old: 0.41719368479128643
    • avg_精细油藏描述_f1: 0.4313315639006726
  • bert-cnn
    • avg_f1: 0.34315815627817314
    • avg_f1_old: 0.38315118709007473
    • avg_精细油藏描述_f1: 0.42514119487594343

上述实验记录在:https://gitlab.gridsum.com/zhuyuanqing/publicworktoolbox/-/issues/86


textcnn模型结构简单而有效,在很多项目中如果资源不够,可以优先考虑。例如在冀北电网智能工单项目中的工单分类任务中,客户要求对20多种类别的工单进行分类,每种类别下分别有多个标签,因此需要交付20多个模型文件完成。

image

然而由于能够提供的硬件环境和对方要求的响应时间要求,导致难以选用较重模型,在多次尝试后决定选用text-cnn模型完成交付,最终方案是textcnn+flask+gunicorn用docker封装,响应速度较快,占用资源较少。

3. BERT-DNN-pytorch

以上两部分内容均是tensorflow1.15版本的代码,后逐渐转为pytorch。

对于pytorch来说使用bert更为方便(当然其实tf也很方便),这里简述一下大名鼎鼎的huggingface维护的transformers。用其中的transformers.BertForSequenceClassification即可实现一个最简单的分类模型。

BERT的文档地址为:https://huggingface.co/transformers/v1.2.0/model_doc/bert.html

1
2
sequence_output = self.bert(input_ids=input_ids, token_type_ids=token_type_ids,attention_mask=attention_mask, output_hidden_states=True)[0]
first_token_tensor = torch.squeeze(sequence_output[:, 0:1, :], dim=1).cuda()

4. DistilBERT

前面提到对于某些项目BERT难以符合一些严苛要求,这时候就可以考虑魔改一些东西。这里罗列两个不是非常符合常规做法的魔改方法。

1. 在bert初始化时对config.json进行修改,减少要使用的bert参数量

具体来说,需要在bert init过程传入bert_config或者修改本地的config.json文件,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"attention_probs_dropout_prob": 0.1,
"directionality": "bidi",
"hidden_act": "gelu",
"hidden_dropout_prob": 0.1,
"hidden_size": 768,
"initializer_range": 0.02,
"intermediate_size": 3072,
"max_position_embeddings": 512,
"num_attention_heads": 3,
"num_hidden_layers": 3,
"pooler_fc_size": 768,
"pooler_num_attention_heads": 12,
"pooler_num_fc_layers": 3,
"pooler_size_per_head": 128,
"pooler_type": "first_token_transform",
"type_vocab_size": 2,
"vocab_size": 21128
}

这里将num_hidden_layersnum_attention_heads进行了修改,再训练出的模型就会占用空间少,但通常来说效果也会变差,这需要具体问题具体分析,在效果和性能取得平衡。

2. 模型蒸馏

对于teacher模型,我在代码中返回的是return loss, dense_2_with_softmax, dense_2_output,其中dense_2_output即为logits,这个后面会用到

对于student模型,与teacher的模型结构基本上完全一样,但是在bert_config里面有不同的设置,我在这里将num_attention_heads设置为3,将num_hidden_layers分别设置成1和3进行了尝试。

训练部分有用部分如下:

1
2
model = torch.load("/data/zhuyuanqing/static_MODEL/event_extract/sentence_classify_daneng_teacher/fold_0/model_epoch_23_p_1.0000_r_1.0000_f_1.0000.pt")
student_model = Student(args.bert_model_toy, args.label_num)

这里model即为teacher,是直接从训练好的模型加载的,故设为*.eval()

do_train的训练过程中,对于每个batch数据,进行:

1
2
3
4
with torch.no_grad():
_, teacher_output_with_softmax, teacher_output = model(input_ids, segment_ids, input_mask, label_ids)

student_output, student_output_with_softmax = student_model(input_ids, segment_ids, input_mask, label_ids)

后面会用到student_outputteacher_output,实际上就是student去学习teacher的分布,对于论文比较常见的是:

image

在当前的实验中是摘抄了这段代码:

1
2
3
4
5
6
7
8
9
10
def distillation(y, teacher_scores, labels, T, alpha):
p = F.log_softmax(y/T, dim=1)
q = F.softmax(teacher_scores/T, dim=1)
l_kl = F.kl_div(p, q, size_average=False) * (T**2) / y.shape[0]
l_ce = F.cross_entropy(y, labels)

return l_kl * alpha + l_ce * (1. - alpha)

# 调用
loss = distillation(student_output, teacher_output, label_ids.long(), T, 0.2)

ps. 这个操作看起来有点像focal_loss的操作,可参考:知乎:Focal loss论文详解

optimizer也是用了两种进行尝试,分别是

1
2
3
4
5
6
7
8
9
10
11
12
13
# 第一种方法:teacher model即用的这个
param_optimizer = list(student_model.named_parameters())
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
'weight_decay': args.weight_decay},
{'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
warmup_steps = int(args.warmup_proportion * num_train_optimization_steps)
optimizer = AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)

# 第二种方法:从网上直接粘贴过来的
optimizer = torch.optim.SGD(student_model.parameters(), lr=0.05)

选择了一份内容营销业务数据进行了简单测试,结果如下:

teacher-student结果

teacher-dev
1
2
3
4
5
6
7
8
9
              precision    recall  f1-score   support

测评 0.7800 0.7800 0.7800 50
种草 0.8856 0.8754 0.8805 345
科普 0.6636 0.6887 0.6759 106

accuracy 0.8263 501
macro avg 0.7764 0.7813 0.7788 501 <
weighted avg 0.8281 0.8263 0.8272 501
student-dev
bert-layer-1
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
epoch 9
precision recall f1-score support

accuracy 0.6886 501
macro avg 0.2295 0.3333 0.2719 501
weighted avg 0.4742 0.6886 0.5616 501

epoch 19
precision recall f1-score support

accuracy 0.7265 501
macro avg 0.6689 0.6261 0.6331 501
weighted avg 0.7444 0.7265 0.7295 501

epoch 29
precision recall f1-score support

accuracy 0.7106 501
macro avg 0.6198 0.5992 0.6067 501
weighted avg 0.7152 0.7106 0.7118 501

epoch 38
precision recall f1-score support

accuracy 0.7146 501
macro avg 0.6200 0.5898 0.6007 501
weighted avg 0.7161 0.7146 0.7139 501

epoch 49
precision recall f1-score support

accuracy 0.7146 501
macro avg 0.6451 0.6156 0.6243 501 <
weighted avg 0.7269 0.7146 0.7182 501
bert-layer3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
epoch 9
precision recall f1-score support

accuracy 0.7605 501
macro avg 0.7092 0.6881 0.6920 501
weighted avg 0.7782 0.7605 0.7659 501

epoch 19
precision recall f1-score support

accuracy 0.7764 501
macro avg 0.7030 0.7069 0.7049 501 <
weighted avg 0.7787 0.7764 0.7775 501

epoch 29
precision recall f1-score support

accuracy 0.7764 501
macro avg 0.6991 0.6977 0.6981 501
weighted avg 0.7791 0.7764 0.7776 501
bert-layer3

修改optimizer为:torch.optim.SGD(student_model.parameters(), lr=0.05)AdamW(optimizer_grouped_parameters, lr=args.learning_rate, eps=args.adam_epsilon)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
epoch 9
precision recall f1-score support

accuracy 0.7685 501
macro avg 0.6756 0.6662 0.6690 501
weighted avg 0.7734 0.7685 0.7701 501

epoch 19
precision recall f1-score support

accuracy 0.8064 501
macro avg 0.7360 0.7261 0.7295 501 <
weighted avg 0.8106 0.8064 0.8078 501

epoch 29
precision recall f1-score support

accuracy 0.7784 501
macro avg 0.7089 0.6968 0.6998 501
weighted avg 0.7870 0.7784 0.7812 501

bert-layer-3(SGD)

bert_layer_3

bert-layer-3(Adamw)

bert_layer_3_optimizer

通过上述实验得到的的结论:

  • 1层的layer没有3层的好使(废话
  • SGDAdamW没感觉到特别特别明显差异,先当作炼丹问题 | update:看下图的话感觉SGD相对更稳定一些
  • 两个loss得权重比例和Temperature取值也很玄学,可炼
  • 目前student部分不够完善
  • 如果teacher结果好,1层的student表现还行;如果teacher表现不是非常理想,那student如果结构弱也比较吃亏

5. NER

对于命名实体识别任务,在NLP领域可以归纳为序列标注任务,常见的方法有lstm+crf/pointer等等,这里想提到的就是transformers.BertForTokenClassification方法,它将NER任务直接考虑为在字粒度的分类任务,在很多时候可以作为baseline方法对任务有个大概摸底。

在冀北电网智能工单项目中,有一个需求是对文档进行知识点的抽取,从而构建知识点图谱,这里知识点是一段文本中的几句话或者几个关键词语。因此这里NER模型就被当作了一个知识点抽取模型来使用,而不仅仅局限在对 “实体” 的抽取。

该模型还在另一个任务中有所使用,这里一并列举以便参考:

在NL2SQL模型中,模型生成的SQL语句是不带value的(原因:作者实验发现如果同步抽取value会导致SQL组件的准确率降低),例如:SELECT 发电厂基本信息.电子邮箱 FROM 发电厂基本信息 WHERE 发电厂基本信息.发电厂名称 = 'terminal',这个问题其实容易通过后处理解决。

由于SQL语句中的value通常出现在Question中,那么这个问题就转化为根据列名去Question文本中找到对应内容的问题。

因此可以构建输入:Question+ColumnName,输出为列名对应value,训练模型即可实现对terminal的替换。

T5

1. t5_pegasus

原始链接:T5 PEGASUS:开源一个中文生成式预训练模型

  1. tokenizer的优化。mt5词表有二十余万的词语及embedding,但中文只占其中一部分,因此对此进行了优化。从jieba词表的前20w个高频词中,选取了在预训练语料中出现频次最高的5w个词,并将其作为词表vocab.txt。

  2. 借鉴了PEGASUS的思想,其思想为将mask的级别拓展到句子,即对于一篇文章,通过一些策略mask掉一些句子,然后用剩余的句子来预测被mask的句子的内容。

    假设一个文档有n个句子,我们从中挑出大约n/4个句子(可以不连续),使得这n/4个句子拼起来的文本,跟剩下的3n/4个句子拼起来的文本,最长公共子序列尽可能长,然后我们将3n/4个句子拼起来的文本视为原文,n/4个句子拼起来的文本视为摘要,这样就构成了一个“(原文, 摘要)”的伪摘要数据对了,就用这些数据对去训练Seq2Seq模型即可。

目前,NLP能力平台的文本摘要服务对keras版本进行了封装,能够支持训练预测,在milestone2中根据苏神博客层次分解位置编码,让BERT可以处理超长文本对t5-pegasus进行了修改,也能支持长文本的训练和预测。但由于长文本摘要数据集较少,并且长度增加后效果↓,建议此功能仅作体验。

考虑到公司技术栈以pytorch为主,因此后续工作考虑将keras版本代码修改为torch版本。

  • milestone2接口文档:略
  • backend地址:略

2. transformers.MT5ForConditionalGeneration

相比于上述方法,我们也可以调用transformers包的MT5ForConditionalGeneration方法完成摘要模型的训练,其代码更加简单。

具体来说,对于输入的文本-摘要(text-summary)有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
tokenizer = T5Tokenizer.from_pretrained("google/mt5-small")
text_encoded_dict = tokenizer.encode_plus(
text, # document to encode.
add_special_tokens=True, # add tokens relative to model
max_length=max_seq_length, # set max length
truncation=True, # truncate longer messages
pad_to_max_length=True, # add padding
return_attention_mask=True, # create attn. masks
return_tensors='pt' # return pytorch tensors
)
summary_encoded_dict = tokenizer.encode_plus(
summary, # document to encode.
add_special_tokens=True, # add tokens relative to model
max_length=max_seq_length, # set max length
truncation=True, # truncate longer messages
pad_to_max_length=True, # add padding
return_attention_mask=True, # create attn. masks
return_tensors='pt' # return pytorch tensors
)

模型的训练:

1
2
3
4
5
6
7
model = MT5ForConditionalGeneration.from_pretrained("google/mt5-small")
outputs = model(
input_ids=text_input_ids,
attention_mask=text_att_mask,
labels=summary_input_ids,
decoder_attention_mask=summary_att_mask
)

预测则调用model.generate即可:

1
2
3
4
5
6
with torch.no_grad():
outputs = model.generate(text_input_ids)
pred = []
for pp in outputs:
pp_ = tokenizer.decode(pp, skip_special_tokens=True)
pred.append(pp_)

在使用MT5ForConditionalGeneration过程中,在公司DL服务器上需要约束batch_size=1,max_seq_length=512能够跑起mt5-small,换成mt5-base会导致资源不够出错。

文本摘要结果评测

方案1: textrank

LCSTS数据集

  1. dev-20000:{'rouge-1': 0.270134366932949, 'rouge-2': 0.139016937291353, 'rouge-l': 0.2321998077627686, 'bleu': 0.06403468343830841}
  2. dev-全量-108915:{'rouge-1': 0.2686800362783573, 'rouge-2': 0.13793584256140828, 'rouge-l': 0.23100455741973097, 'bleu': 0.06322491915268577}

NLPCC数据集

  1. dev-全量-10000:{'rouge-1': 0.35543741211050656, 'rouge-2': 0.1890605775087208, 'rouge-l': 0.2830505612239267, 'bleu': 0.11451371060883851}

方案2:t5/mt5

LCSTS数据集

  1. train-20000,dev-20000:{'rouge-1': 0.3439869141909578, 'rouge-2': 0.20868820633717347, 'rouge-l': 0.31631151248246936, 'bleu': 0.11917552701595575}
  2. train-500000, dev-20000:{'rouge-1': 0.3876713112544026, 'rouge-2': 0.24454193676703398, 'rouge-l': 0.35648754834527996, 'bleu': 0.14983122744892452}
  3. train-1000000, dev-20000:{'rouge-1': 0.3972087215770807, 'rouge-2': 0.25194091382794775, 'rouge-l': 0.36479877982787084, 'bleu': 0.15722252621576416}

方案3:t5-pegasus

LCSTS数据集

  1. train-20000,dev-20000:{'rouge-1': 0.34544668882236546, 'rouge-2': 0.2003219803166334, 'rouge-l': 0.31674299284684254, 'bleu': 0.11230459933637414}
  2. train-500000, dev-20000:{'rouge-1': 0.3779512820266609, 'rouge-2': 0.22554697558542244, 'rouge-l': 0.3471512882173606, 'bleu': 0.13239432252520852}(尚未完成训练,此为epoch5的指标)
  3. train-1000000, dev-20000:{'rouge-1': 0.38598133347041713, 'rouge-2': 0.23048499632522085, 'rouge-l': 0.35434723315572103, 'bleu': 0.13600921010698191}(尚未完成训练,此为epoch3的指标)

3. mt5文本分类

T5天然适合做文本生成类任务,但是考虑到它又新又大,也可以魔改一下让它来做分类任务,提供思路如下:

  1. 利用transformers.MT5EncoderModel获取文本表示,单纯地替换bert作用
  2. 还是利用MT5ForConditionalGeneration,将标签当作待生成的文本,将分类任务当作seq2seq来做。这种方式对于多标签分类或者层次分类任务是值得尝试的一种方法。

ps. 在MT5EncoderModel实验中可以跑起mt5-base

4. 其他用到t5的模型

Autoregressive Structured Prediction with Language Models

项目地址:https://github.com/lyutyuh/ASP

该项目提供的代码实现了三项任务:命名实体识别、指代消解、实体关系抽取

根据之前的实验,命名实体识别、指代消解任务在更换为mt5和中文数据集后都达到了正常水平,实体关系抽取尚存在一些问题。

命名实体识别:Step 33000: evaluating on 1343 samples with batch_size 32

  • Eval_Ent_Precision: 74.3627
  • Eval_Ent_Recall: 66.4714
  • Eval_Ent_F1: 70.1959

指代消解:evaluating on 385 samples with batch_size 1

  • Eval_Avg_Precision: 64.9949
  • Eval_Avg_Recall: 60.5930
  • Eval_Avg_F1: 62.6821
  • Eval_Mention_Recall: 74.4342
  • muc_f1: 0.6883
  • muc_p: 0.7077
  • muc_r: 0.6699
  • b_cubed_f1: 0.6207
  • b_cubed_p: 0.6282
  • b_cubed_r: 0.6134
  • ceafe_f1: 0.5714
  • ceafe_p: 0.6139
  • ceafe_r: 0.5345

实体关系抽取:evaluating on 2000 samples with batch_size 8

  • Eval_Ent_Precision: 68.3107
  • Eval_Ent_Recall: 67.6750
  • Eval_Ent_F1: 67.9914
  • Eval_Rel_Precision: 57.2822
  • Eval_Rel_Recall: 48.2740
  • Eval_Rel_F1: 52.3937
  • Eval_Rel_p_Precision: 35.8257
  • Eval_Rel_p_Recall: 30.