tensorflow 2.0+ 基于预训练BERT模型 的文本分类
简介
利用transformers架构构建的语言模型在多种NLP基准测试中展现出显著的能力。
迁移学习与大量transformers语言模型训练的融合已逐渐成为现代NLP的标准方法。
本文旨在对transformers架构及其在文本分类问题中的应用进行基础理论阐述。
接下来我们将详细演示预训练BERT模型如何在其微调过程中应用于文本分类任务。
这里运用的是TensorFlow 2.0+ 的 Keras API。
文本分类–问题及公式
在机器学习中, 分类任务的本质是在给定的数据集中识别和归类未知文本的类型。给定的数据集D, 其中每个文档都由一系列有序的文本组成, 如

这里 Xi 是每一段文本 而N 是文本的个数。
该算法通过实现进行分类称为分类器;文本分类按照其目标性质的不同;分为若干类型的任务;
多分类问题(multi-class classification)
多标签问题(multi-label classification)
在机器学习中,多分类问题通常被称为单标签分类任务。举个例子来说,在训练过程中会给每个样本分配唯一的标识符。其中的"多"字意味着我们在分类时涉及三个或更多类别;而对于仅分为两类的情况,则通常采用二元分类(binary classification)这一术语。相比之下,在多标签学习中所涉及的任务更加广泛
为什么选择transformers?
在本文中我们不打算深入探讨transformers架构这一技术细节但掌握NLP中的关键挑战仍然大有裨益作为语言处理领域的重要研究方向NLP涉及两大核心要素
Transformers被用于构建语言模型,而Embeddings作为预训练过程中的补充品出现.
基于 RNNs/LSTMs 的方法
传统的基于递归神经网络的语言建模方法主要依赖于 RNN 模型。然而, 传统 RNN 面临梯度消失与梯度爆炸的问题, 因此无法有效建模较长的上下文依赖关系。这些模型已被长短期记忆网络(LSTM)所取代, 并作为 RNN 的一种变体形式存在, 能够捕捉文档中较长段落的上下文信息。然而,LSTM 仅能处理单向序列数据, 因此基于 LSTM 的先驱技术发展出了双向 LSTM 模型。这些模型在语言处理任务方面取得了巨大成功, 如 ELMO 或 ULMFIT 等模型仍然适用于现在的 NLP 任务
基于transformers架构的方法
双向 LSTM 的主要缺点在于其依赖序列顺序的特点,导致并行训练存在挑战。transformer 架构借助于注意力机制(如 Vashvani 等人 2017 所述),彻底取代了 LSTM 来克服这一挑战。在注意力机制下,我们把整个序列视为一个整体结构,因此实现并行训练变得更加容易。我们不仅能够对整个文档的上下文进行建模与处理,并且可以通过无监督学习的方式利用大规模数据集进行预训练,在此基础上进一步微调适用于特定下游任务的模型。
最先进的transformers模型
有很多基于transformers的语言模型。最成功的是以下这些(截至2020年4月)
- Transformer (Google Brain/Research)
- BERT (Google Research)
- GPT-2 (OpenAI)
- XLNet (Google Brain)
- CTRL (SalesForce)
- Megatron (NVidia)
- Turing-NLG (Microsoft)
这些模型之间存在细微的差别,在NLP领域中,BERT曾被视为最先进的解决方案.然而,目前研究表明,已被同样来自谷歌公司的XLNet超越. XLNet采用了基于置换语言建模的方法,这种技术能够对句子中所有可能的单词排列组合进行自动回归处理. 在本文中将采用BERT为基础的语言模型.
BERT
BERT(Bidirectional Encoder Representations from Transformers)(Devlint等،2018)是一种预先训练的语言表示技术。我们不深入讨论细节问题,在基本版本中使用了12个编码器的架构与原始transformers(Vaswani等،2017)的主要差异在于:BERT没有解码器组件;而较大的预训练模型则会增加编码器的数量。这种架构不同于OpenAI的GPT-2模型;它是一个适合自然语言生成(NLG)的自回归语言模型。
Tokenizer
官方 BERT 语言模型基于切片词汇进行预训练,并结合段嵌入技术,在问答系统中得到了广泛应用。该模型不仅实现了对单个序列的信息提取能力,同时也具备处理复杂文本结构的能力。然而,在传统注意力机制中缺乏对位置信息的关注会导致其难以捕捉到序列中的位置依赖性。为此,在构建 BERT 模型时必须引入位置编码机制以增强其位置感知能力。
需要注意的是,BERT对序列的最大长度设置为512个token,因此对于比最大允许输入更短的序列,我们需在末尾添加零填充标记[PAD],而如果一个序列更长,则需截断该序列,以确保其满足BERT模型的要求;对于较为冗长的文本段,您需要了解BERT对sequence length设置的具体影响,可参考此GitHub issue](https://github.com/huggingface/transformers/issues/2295)以获得进一步解决方案
特别重要的是被称为特殊的token中包含两种类型即[CLS]标记与[SEP]标记。在自然语言处理中CLS标记被用来标识一个句子的开始通常位于句子的第一位而S标记则用于标识一个句子的结束位置。在处理一对序列时我们需要在最后一对序列的最后一端添加额外的一个SEP标记以帮助模型识别这两部分之间的界限

当我们使用transformers库时
第一步
第二步
第三步
第四步
第五步
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
max_length_test = 20
test_sentence = '曝梅西已通知巴萨他想离开'
# add special tokens
test_sentence_with_special_tokens = '[CLS]' + test_sentence + '[SEP]'
tokenized = tokenizer.tokenize(test_sentence_with_special_tokens)
print('tokenized', tokenized)
# convert tokens to ids in WordPiece
input_ids = tokenizer.convert_tokens_to_ids(tokenized)
# precalculation of pad length, so that we can reuse it later on
padding_length = max_length_test - len(input_ids)
# map tokens to WordPiece dictionary and add pad token for those text shorter than our max length
input_ids = input_ids + ([0] * padding_length)
# attention should focus just on sequence with non padded tokens
attention_mask = [1] * len(input_ids)
# do not focus attention on padded tokens
attention_mask = attention_mask + ([0] * padding_length)
# token types, needed for example for question answering, for our purpose we will just set 0 as we have just one sequence
token_type_ids = [0] * max_length_test
bert_input = {
"token_ids": input_ids,
"token_type_ids": token_type_ids,
"attention_mask": attention_mask
}
print(bert_input)
OUTPUT:
tokenized ['[CLS]', '曝', '梅', '西', '已', '通', '知', '巴', '萨', '他', '想', '离', '开', '[SEP]']
{'token_ids': [101, 3284, 3449, 6205, 2347, 6858, 4761, 2349, 5855, 800, 2682, 4895, 2458, 102, 0, 0, 0, 0, 0, 0], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0]}
在实际编码过程中, 我们仅会采用encode_plus函数这一工具, 该编码器能够为我们的工作流程承担所有相关步骤.
bert_input = tokenizer.encode_plus(
test_sentence,
add_special_tokens = True, # add [CLS], [SEP]
max_length = max_length_test, # max length of the text that can go to BERT
pad_to_max_length = True, # add [PAD] tokens
return_attention_mask = True, # add attention mask to not focus on pad tokens
)
print('encoded', bert_input)
预训练
在BERT的训练过程中,预训练任务属于其第一阶段,并通过无监督学习完成;该阶段由两个主要任务构成:
- masked language modelling (MLM)
- next sentence prediction (NSP)
在高级别的 MLM 任务中,在序列中的特定数量标记为[MASK]以实现这一目标。随后我们将注意力集中在预测这些被隐藏(或掩蔽)的tokens上。该任务涉及一些特定规则因此其描述不够全面准确,请参考原始论文(Devlin et al. 2018)获取更多详细信息
在训练集中包含上个时间 slice的真实数据,在测试集中则包含随机生成的数据
这两项任务均可在现有文本语料库中执行而无需进行标记样本的标注工作

微调(Fine-tuning)
当我们自行进行了模型的预训练或者导入了先前预训练好的模型(例如BERT-based-uncased、BERT-based-chinese)时

在预训练阶段中所需的大量计算资源(BERT base需4天运行于16台TPU上;BERT large则需4天运行于64台TPU上)因此对于存储好这些经过预先训练好的模型后进行特定数据集的微调将会大有裨益

数据集
从THUCNews的数据集中提取一个子集,并分别用于训练和测试。请注意访问THUCTC以获取一个高效的中文文本分类工具包,并遵守其提供的开放源代码协议。
本次训练使用了其中的10个分类,每个分类2W条数据。
类别如下:
财经、房产、股票、教育、科技、社会、时政、体育、游戏、娱乐
该数据集存储于 data.txt 一并包含在 GitHub 仓库中
现将数据集按照层次抽样划分为训练集、验证集、测试集:
| 数据集 | 数据量 |
|---|---|
| 训练集 | 18万 |
| 验证集 | 1万 |
| 测试集 | 1万 |
from sklearn.model_selection import train_test_split
import pandas as pd
def split_dataset(df):
train_set, x = train_test_split(df,
stratify=df['label'],
test_size=0.1,
random_state=42)
val_set, test_set = train_test_split(x,
stratify=x['label'],
test_size=0.5,
random_state=43)
return train_set,val_set, test_set
df_raw = pd.read_csv("data.txt",sep="\t",header=None,names=["text","label"])
# label
df_label = pd.DataFrame({"label":["财经","房产","股票","教育","科技","社会","时政","体育","游戏","娱乐"],"y":list(range(10))})
df_raw = pd.merge(df_raw,df_label,on="label",how="left")
train_data,val_data, test_data = split_dataset(df_raw)
使用TensorFlow 2.0+ keras API微调BERT
为此,在全部样本数据集中部署BERT tokenizer。通过将每个token映射为词嵌入空间中的向量,我们可以利用encode_plus方法来实现这一过程。
def convert_example_to_feature(review):
return tokenizer.encode_plus(review,
add_special_tokens = True, # add [CLS], [SEP]
max_length = max_length, # max length of the text that can go to BERT
pad_to_max_length = True, # add [PAD] tokens
return_attention_mask = True, # add attention mask to not focus on pad tokens
)
# map to the expected input to TFBertForSequenceClassification, see here
def map_example_to_dict(input_ids, attention_masks, token_type_ids, label):
return {
"input_ids": input_ids,
"token_type_ids": token_type_ids,
"attention_mask": attention_masks,
}, label
def encode_examples(ds, limit=-1):
# prepare list, so that we can build up final TensorFlow dataset from slices.
input_ids_list = []
token_type_ids_list = []
attention_mask_list = []
label_list = []
if (limit > 0):
ds = ds.take(limit)
for index, row in ds.iterrows():
review = row["text"]
label = row["y"]
bert_input = convert_example_to_feature(review)
input_ids_list.append(bert_input['input_ids'])
token_type_ids_list.append(bert_input['token_type_ids'])
attention_mask_list.append(bert_input['attention_mask'])
label_list.append([label])
return tf.data.Dataset.from_tensor_slices((input_ids_list, attention_mask_list, token_type_ids_list, label_list)).map(map_example_to_dict)
我们可以使用以下函数对数据集进行编码:
# train dataset
ds_train_encoded = encode_examples(train_data).shuffle(10000).batch(batch_size)
# val dataset
ds_val_encoded = encode_examples(val_data).batch(batch_size)
# test dataset
ds_test_encoded = encode_examples(test_data).batch(batch_size)
创建模型
transformers 模块已经包含了分类模型的函数,直接使用即可
from transformers import TFBertForSequenceClassification
import tensorflow as tf
model = TFBertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=10)
编译与训练模型
# recommended learning rate for Adam 5e-5, 3e-5, 2e-5
learning_rate = 2e-5
# we will do just 1 epoch for illustration, though multiple epochs might be better as long as we will not overfit the model
number_of_epochs = 8
# model initialization
model = TFBertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=10)
# optimizer Adam recommended
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate,epsilon=1e-08, clipnorm=1)
# we do not have one-hot vectors, we can use sparce categorical cross entropy and accuracy
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')
model.compile(optimizer=optimizer, loss=loss, metrics=[metric])
# fit model
bert_history = model.fit(ds_train_encoded, epochs=number_of_epochs, validation_data=ds_val_encoded)
# evaluate test set
model.evaluate(ds_test_encoded)
以下是8个epochs的训练结果:
Epoch 1/8
1407/1407 [==============================] - 2012s 1s/step - loss: 1.5890 - accuracy: 0.8952 - val_loss: 1.5220 - val_accuracy: 0.9298
Epoch 2/8
1407/1407 [==============================] - 1998s 1s/step - loss: 1.5114 - accuracy: 0.9390 - val_loss: 1.5133 - val_accuracy: 0.9317
Epoch 3/8
1407/1407 [==============================] - 2003s 1s/step - loss: 1.4998 - accuracy: 0.9487 - val_loss: 1.5126 - val_accuracy: 0.9331
Epoch 4/8
1407/1407 [==============================] - 1995s 1s/step - loss: 1.4941 - accuracy: 0.9563 - val_loss: 1.5090 - val_accuracy: 0.9369
Epoch 5/8
1407/1407 [==============================] - 1998s 1s/step - loss: 1.4901 - accuracy: 0.9612 - val_loss: 1.5099 - val_accuracy: 0.9367
Epoch 6/8
1407/1407 [==============================] - 1995s 1s/step - loss: 1.4876 - accuracy: 0.9641 - val_loss: 1.5104 - val_accuracy: 0.9346
Epoch 7/8
1407/1407 [==============================] - 1994s 1s/step - loss: 1.4859 - accuracy: 0.9668 - val_loss: 1.5104 - val_accuracy: 0.9356
Epoch 8/8
1407/1407 [==============================] - 1999s 1s/step - loss: 1.4845 - accuracy: 0.9688 - val_loss: 1.5114 - val_accuracy: 0.9321
79/79 [==============================] - 37s 472ms/step - loss: 1.5037 - accuracy: 0.9437
[1.5037099123001099, 0.9437000155448914]
可以看到,训练集正确率96.88%,验证集正确率93.21%,测试集上正确率94.37%。
运行环境
linux: CentOS Linux release 7.6.1810
python: Python 3.6.10
packages:
tensorflow==2.3.0
transformers==3.02
pandas==1.1.0
scikit-learn==0.22.2
使用方式
git clone https://github.com/NZbryan/NLP_bert.git
cd NLP_bert
python3 tf2.0_bert_emb_ch_MultiClass.py
由于数据量较大,训练时间长,建议在GPU下运行,或者到colab去跑。
可参考以下链接了解详细信息:Text classification with transformers in TensorFlow 2: BERT
