bert中文文本分类模型

1,079 阅读10分钟

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 提取的特征向量,当匹配的是对应语义其他语种时,即可实现翻译能力。除此之外,当译码器的返回是训练向量中的下文信息时,实现的能力即为对话问答。

image.png

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架构。

image.png

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乘以上述概率,较大值为要重视的上下文词汇。

下图是自注意力机制运算框架:

image.png

自注意力机制运算过程归纳为数学表达式,即自注意力矩阵公式为:

image.png (3) 自注意力机制是多头(Multi-Head,计算句子中所有单词的注意力,然后将所有值连起来)的,通常是8个头,通过内积运算,将多头串联,示意图如下:

image.png

多头自注意力矩阵公式为:

image.png

(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相关模型“)

image.png

进入后选择Files and versions,点击下载(拼网速的时候到了,这方面笔者没有经验,下载失败请多次尝试),由于笔者使用的是tensorflow框架,所以此处只下载了tf_model,使用pytorch则选择下载pytorch_model。

image.png

image.png

image.png

完成后即可开始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技术再上一步台阶吧。