bert中文文本分类模型
1 模型介绍
提起bert模型,就要从bert的根源模型transformer开始,而transformer模型,则需要从注意力机制开始梳理,文章进行简要讲解,不做深究。
1.1 注意力机制
大量实践证明,在文本这种非标准化并且模糊的数据中,单纯的某段序列化数据无法准确表达序列中某段数据的真实含义,最明显的就是代词。
如:“你的新室友喜欢你的小狗,那它喜欢她吗?”。
这句话中,第二部分并未出现实体,如果单纯解析“它”和“她”,只能是面,但实际在这句话中,实际上两者对应的分别是“小狗”和“新室友”。
正是这样的原因,大量的实践将文本模型的方向指向了记忆行为和判断行为。正因如此,文本模型走上了两个方向:基于语法,基于语义。
基于语法的方法在本文中不做细说,本质上即基于词性,去训练文本的结构并保存至模型,新的文本进入后,对比模型中的结构,从语法层面解析出文本主体。在此方向上目前已经有了成熟的模型框架体系,其中具有代表性的如Spacy框架,感兴趣的朋友可滴滴我,后边再做整理。
而基于语义方面,前人做了大量尝试,文章中从RNN开始说起。RNN从之前的隐藏层状态取得上文信息,LSTM则额外维护一条记忆线(Cell State),目的都是希望能借由记忆来提高预测准确性。但是,两种算法都局限于上文的序列顺序,导致越靠近预测目标的信息,权重越大。
实际上当我们在阅读一篇文章时,往往会对文中的标题、人物/事情/时间/地点/强烈的形容词产生极高的关注程度,这就是所谓的注意力机制(Attention Mechanism),注意力机制表明,自然语言中产生的注意力点可能存在于一段文本中的任何位置:
“探月有新消息了”;
“明天预售新产品”;
“昨晚游戏十连胜”。
可以看到,在上方的三个例子中,注意力分别集中在“探月”,“预售”,“十连胜”,这样的现象表明了,在一句话中,注意力集中位置是不固定的。
只要透过注意力机制,在预测时可以额外把重点单字/词汇或部位纳入考虑,而不只是上下文, 即可在一定程度上让机器处理实现部分情感逻辑。
注意力机制常被应用到神经机器翻译(Neural Machine Translation,NMT),机器翻译是一个序列生成的模型,它是一种 Encoder-Decoder的变形,称为序列到序列(Sequence to Sequence, Seq2Seq)模型,图中的ContextVector是 Encoder输出的上下文向量,类似于CNN AutoEncoder 提取的特征向量,当匹配的是对应语义其他语种时,即可实现翻译能力。除此之外,当译码器的返回是训练向量中的下文信息时,实现的能力即为对话问答。
1.2 transformer
Google的学者 Ashish Vaswani等人于2017年依照Seq2Seq模型加上注意力机制,提出了Transtormer 架构。架构一推出后,马上跃身为NLP近年来最受欢迎的算法,《Attention Is All You Need》一文也被公认是必读的文章,各种改良的算法也纷纷出笼,如BERT、GPT-2、GPT-3、XLNet、ELMo、T5等,几乎抢占了NLP大部分的版面。接下来我们就来好好认识 Transformer与其相关的算法,下图为Transformer架构。
RNN/LSTM/GRU有个最大的缺点,因为要以上文预测目前的目标,必须以序列的方式,依序执行每一个节点的训练,进而导致执行效能过慢。而 Transformer克服了此问题,提出自注意力机制(Self-Attention Mechanism),能够并行计算出所有的输出。
计算步骤如下:
(1) 首先输入向量(Input Vector)被表征为Q、K、V向量。
① K: Key-Vector,为 Encoder 隐藏层状态的键值,即上下文的词向量。
② V: Value-Vector,为 Encoder隐藏层状态的输出值。
③ Q: Query-Vector,为Decoder 的前一期输出。
④ 故自注意力机制对应Q、K、V,共有三种权重,而单纯的注意力机制则只有一种权重(Attention Weight)。
⑤ 利用神经网络优化可以找到三种权重的最佳值,然后,以输入向量分别乘以三种权重,即可求得Q、K、V向量。
(2) Q、K、V再经过下图的运算即可得到自注意力矩阵。
① 点积运算:Q×K,计算输入向量与上下文词汇的相似度。
② 特征缩放:Q、K维度开根号,通常Q、K维度是64,故64^0.5=8。
③ Softmax运算:将上述结果转为概率。
④ 找出要重视的上下文词汇:以Value Vector乘以上述概率,较大值为要重视的上下文词汇。
下图是自注意力机制运算框架:
自注意力机制运算过程归纳为数学表达式,即自注意力矩阵公式为:
(3) 自注意力机制是多头(Multi-Head,计算句子中所有单词的注意力,然后将所有值连起来)的,通常是8个头,通过内积运算,将多头串联,示意图如下:
多头自注意力矩阵公式为:
(4) 以上框架,加上其他作用的神经层,即构建完成了我们常说的Transformer网络框架。
1.3 bert
在了解了transformer原理之后,我们就进一步来看看bert。BERT(Bidirectional Encoder Representations from Transformers),顾名思义,表示双向的 Transformer,由Google Jacob Devlin等学者发表,详细论文参考《BERT:Pretraining of Deep Bidirectional Transformers for Language Understanding》一文。在Word2 Vec/GloVe中一个单字只以一个词向量表示,但一词多义是所有语系共有现象。
例如:
“今夜天气晴朗,群星闪耀” 和 “那段历史之中,群星闪耀”;
“这些欺负人的害虫” 和 “这些啃木头的害虫”;
“那个同事病了” 和 “这个人有病吧”。
对于这种情况,bert就能够很好的解决,因为它使用的是上下文相关(Context Dependent)策略,输入是一个句子,而非一个单字。
BERT 算法比Transformer更复杂,要花更长的时间训练,但是有几个原因让我们无法拒绝这个模型框架。
(1) BERT支持中文;
(2) BERT跟CNN一样有预先训练好的模型(Pre-trained model),可以进行转移学习。BERT分为两阶段:
① 一般模型训练,即我们常说的预训练模型;
② 依不同应用领域进行模型的效能微调(Fine tuning)。
(3) 许多实验室(国内如哈工大-讯飞联合实验室等)都开源了自己的预训练模型;
(4) 开源库transformers完美支持bert模型。
这就相当于:在nlp的道路上,铸剑师造出了本世纪前20年的最大杀器,后起的英杰为这个杀器做好了完美的容器,正在路边要饭的我,还被顺手发了把容器钥匙。
这……谁能拒绝?
2 文件配置
2.1安装transformers库
安装命令:pip install transformers
如果下载缓慢,可以考虑挂载镜像源,以笔者常用中科大镜像源为例,安装语句为:
pip install transformers -i pypi.mirrors.ustc.edu.cn/simple/
此处顺便提供部分站点:
清华镜像源:-i pypi.tuna.tsinghua.edu.cn/simple/
阿里云镜像源:-i mirrors.aliyun.com/pypi/simple…
中科大镜像源:-i pypi.mirrors.ustc.edu.cn/simple/
华中科技大学镜像源:-i pypi.hustunique.com/
豆瓣镜像源:-i pypi.douban.com/simple/
完成安装后进入抱脸官网:huggingface.co/
数据戳这里
2.2 预训练模型下载
在搜索框中输入bert,在model块选择bert-base-chinese(因为笔者使用的是中文材料,所以此处使用中文模型,具体也可以点击下方”see 9039 model results for 'bert' 展开更多结果,寻找所需的bert相关模型“)
进入后选择Files and versions,点击下载(拼网速的时候到了,这方面笔者没有经验,下载失败请多次尝试),由于笔者使用的是tensorflow框架,所以此处只下载了tf_model,使用pytorch则选择下载pytorch_model。
完成后即可开始bert的使用了。
3 模型构建
3.1 模型的训练
import numpy as np
import pandas as pd
import tensorflow as tf
from transformers import TFBertForSequenceClassification, BertTokenizer
# 列表-编号字典制作
def label_to_id(lst):
'''
:param lst: 待转化list
:return: 第一位输出为原list-id字典,第二位输出为id-原list字典
'''
target_lst = list(set(lst))
lens = len(target_lst)
id_lst = list(range(lens))
tmp_dict1 = dict(zip(target_lst, id_lst))
tmp_dict2 = dict(zip(id_lst, target_lst))
return tmp_dict1, tmp_dict2
# 文本基础转化参数限制
def convert_example_to_feature(review):
'''
参数详解
review:编码语句原文
add_special_tokens:设置最大长度,如果不设置的话原模型设置的最大长度是512,此时,如果句子长度超过512会报错
max_length:获取最大字符串长度
pad_to_max_length:补全操作,效果同使用参数【padding = 'max_length'】,推荐使用参数padding,pad_to_max_length参数后续可能会移除
return_attention_mask:默认返回attention_mask(是否参与attention计算)
truncation=True:截断操作
'''
return tokenizer.encode_plus(review,
add_special_tokens=True,
max_length=max_length,
pad_to_max_length=True,
return_attention_mask=True,
truncation=True
)
# 编码训练的输出格式整理
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):
# 创建空列表
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)
# 数据加载与整理
g_df = pd.read_csv('./data/train_set.csv', sep='|')[['record_dtl_info', 'label']]
data = g_df.reindex(np.random.permutation(g_df.index)).astype(str).reset_index(drop=True)[['record_dtl_info', 'label']]
# 将label转化为label-id
tar_id_dict = label_to_id(data['label'])[0]
data['y'] = data['label'].map(tar_id_dict)
data.columns = ['text', 'label', 'y']
# 参数设定
# 往下顺序依次是
# 预训练模型位置
# 最大字符串长度
# 一次训练所抓取的数据样本数量
# 学习率
# 训练轮数
# 模型输出最终类别数量
# 从开始截取到的训练数据
# 从训练数据开始截取到的测试数据
# 从测试数据开始截取到的验证数据
# 需要注意的是,此处的训练数据测试数据验证数据可通过sklearn.model_selection下的train_test_split切割此处仅演示使用
model_path = "./bert_base_chinese"
max_length = 32
batch_size = 64
learning_rate = 2e-5
number_of_epochs = 5
num_classes = len(list(set(data.label)))
split_start = 10000
split_end = 11000
train_data = data.iloc[:split_start, :]
val_data = data.iloc[split_start:split_end, :]
test_data = data.iloc[split_end:, :]
# 加载预训练模型
tokenizer = BertTokenizer.from_pretrained(model_path)
# 构建encode
ds_train_encoded = encode_examples(train_data).batch(batch_size)
ds_val_encoded = encode_examples(val_data).batch(batch_size)
ds_test_encoded = encode_examples(test_data).batch(batch_size)
# 创建模型
# 创建优化器
# 创建损失
# 创建测评指标
model = TFBertForSequenceClassification.from_pretrained(model_path, num_labels=num_classes)
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate, epsilon=1e-08, clipnorm=1)
loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')
# 编译模型
# 训练模型
# 输出评估
model.compile(optimizer=optimizer, loss=loss, metrics=[metric])
bert_history = model.fit(ds_train_encoded, epochs=number_of_epochs, validation_data=ds_val_encoded)
print("# evaluate test_set:", model.evaluate(ds_test_encoded))
# 模型保存
model.save_weights('./model/bert_weight')
# model.load_weights('./model/bert_weight')
3.2 模型预测
在上一段脚本中已经详细讲解了相关参数,此处不再详细讲解
# -*- coding = utf-8 -*-
# @time: 2022/8/10 10:27
# @file: d_baidu_set_model_predict.py
# Author:yang
# code:
import numpy as np
import pandas as pd
import tensorflow as tf
from transformers import TFBertForSequenceClassification, BertTokenizer
def convert_example_to_feature(review):
return tokenizer.encode_plus(review,
add_special_tokens=True,
max_length=max_length,
pad_to_max_length=True,
return_attention_mask=True,
truncation=True
)
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):
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)
test = pd.read_csv('./data/test_set.csv', sep='|')[['record_dtl_info']]
test['label'] = ''
test['y'] = ''
test.columns = [['text', 'label', 'y']]
model_path = "./bert_base_chinese"
max_length = 32
batch_size = 64
num_classes = 79
tokenizer = BertTokenizer.from_pretrained(model_path)
ds_test_encoded = encode_examples(test).batch(batch_size)
model = TFBertForSequenceClassification.from_pretrained(model_path, num_labels=num_classes)
model.load_weights('./model/bert_weight')
pred1 = model.predict(ds_test_encoded).logits
pred2 = [np.argmax(x) for x in pred1]
test_df = test
test_df.to_csv('./submit.csv', index=False)
4 模型效果
此次模型为内部竞赛使用,最终提交平台返回f1-score为0.92。
5 应用场景
从单纯的模型角度来看,使用bert似乎是目前nlp工作的主流方向,但值得注意的是,bert不仅仅是一个模型,而是一种处理思想。在此之上,我们可以尝试文本匹配工作,问答工作等。
当然,作为nlp学习路上的一只菜鸟,提出新的观点思路可能不现实,期待国内的众多大佬们能够有更多的创造,让nlp技术再上一步台阶吧。