• NLP与深度学习(六)BERT模型的使用


    从头开始训练一个BERT模型是一个成本非常高的工作,所以现在一般是直接去下载已经预训练好的BERT模型。结合迁移学习,实现所要完成的NLP任务。谷歌在github上已经开放了预训练好的不同大小的BERT模型,可以在谷歌官方的github repo中下载[1]

    以下是官方提供的可下载版本:

     

    其中L表示的是encoder的层数,H表示的是隐藏层的大小(也就是最后的前馈网络中的神经元个数,等同于特征输出维度)。

    除此之外,谷歌还提供了BERT-uncased与BERT-cased格式,分别对应是否包含大小写。一般来说,BERT-uncased(仅包含小写)比较常用,因为大部分场景下,单词是否大小写对任务的影响并不大。但是在部分特定场景,例如命名体识别(NER),则BERT-cased是更合适的。

    在应用BERT预训练模型时,实际上就是迁移学习,所以用法就是2个:

    1. 特征提取(feature extraction)
    2. 微调(fine-tune)

    下面我们会分别介绍这2种方法的使用。

    2. BERT特征提取

    特征提取非常简单,直接将单词序列输入到已经预训练好的BERT中,得到的输出即为单词以及句子的特征。

    举个例子,以情感分析任务为例,假设有条句子”I love Beijing” ,它的情感为正面情感。在对这个句子通过BERT做特征提取时,首先使用WordPiece对它进行分词,得到单词列表:

    Tokens = [ I, love, Beijing]

    然后加上特殊token [CLS] 与 [SEP](它们的作用已在前面的章节进行过介绍,在此不再赘述):

    Tokens = [ [CLS], I, love, Beijing, [SEP] ]

    接下来,为了使得训练集中所有句子的长度保持一致,我们会指定一个“最大长度” max_length。对于长度小于此max_length的句子,对它进行补全;而对于超过了此max_length的句子,对它进行裁剪。假设我们这里指定了max_length=7,则对上面的句子进行补全时,使用特殊token [PAD],将句子长度补全到7。如:

    Tokens = [ [CLS], I, love, Beijing, [SEP], [PAD], [PAD] ]

    在使用了[PAD] 作为填充后,还需要一个指示标志,用于表示 [PAD] 仅是用于填充,不代表任何意义。这里用到了一个称为attention mask的列表来表示,它的长度等同于max_length。对于每条句子,如果对应位置的单词为[PAD],则attention mask此位置的元素取值为0,反之为1。例如,对于这个例子,attention mask的值为:

    attention_mask = [ 1, 1, 1, 1, 1, 0, 0 ]

    最后,由于模型无法直接识别单词,仅能识别数字,所以还需要将单词映射为数字。我们首先会对整个单词库做一个词典,每个单词都有对应的1个序号(此序号为不重复的数字)。在这个例子中,假设我们已经构建了一个字典,则对应的这些单词的列表为:

    token_ids = [101, 200, 303, 408, 102, 0, 0]

    其中 101 即对应的是 [CLS] 的序号,200对应的即是单词“I”的序号,依此类推。

    在准备好以上数据后,即可将 token_ids 与 attention_mask 输入到预训练好的BERT模型中,便得到了每个单词的embedding表示。如下图所示:

    在上图中,为了表述方便,在输入时还是使用的单词,但是需要注意的是:实际的输入是token_ids 与 attention_mask。在经过了BERT的处理后,即得到了每个单词的嵌入表示(此嵌入表示包含了整句的上下文)。假设我们使用的是BERT-base模型,则每个词嵌入的维度即为768。

    在上一章介绍BERT训练的时候我们提到过,可以使用E([CLS]) 来表示整个句子的信息。并将它输入到前馈网络与softmax中进行分类任务。但是仅使用 E([CLS]) 作为整个句子的表示信息也并非总是最好的办法。一种更高效的方法是给所有token的嵌入表示做平均或是池化(pooling),来代表整句的信息。具体方法我们后续会做介绍。

    至此,我们已经介绍了使用预训练BERT做特征提取的过程,下面介绍如何使用python lib库来实现此过程。

    3. Hugging Face transformers

    Hugging Face是一个专注于NLP技术的公司,提供了很多预训练的模型以及数据集供直接使用,包括很多大家可能已经了解过的模型,例如bert-base、roberta、gpt2等等。其官网地址为: https://huggingface.co/

    除了提供预训练的模型外,Hugging Face提供的transformers库也是在NLP社区非常热门的库。并且transformers的库同时支持pytorch与tensorflow。

    安装transformers 库非常简单:

    !pip install transformers
    
    import transformers
    transformers.__version__
    '4.11.3'

    4. 生成BERT Embedding

    前面我们介绍了BERT特征提取,下面通过代码实现此功能。

    首先引入包并下载所需模型:

    from transformers import TFBertModel, BertTokenizer
    import tensorflow as tf
    
    # download bert-base-uncased model
    model = TFBertModel.from_pretrained('bert-base-uncased')
    tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

    我们使用的是tensorflow,所以引入的是TFBertModel。如果有使用pytorch的读者,可以直接引入BertModel。

    通过 from_pretrained() 方法可以下载指定的预训练好的模型以及分词器,这里我们使用的是bert-base-uncased。前面对bert-based 有过介绍,它包含12个堆叠的encoder,输出的embedding维度为768。

    对于所有可用预训练模型,可以通过Hugging face 官网查询[2]

    引入模型后,下面从一个例子来看输入数据的处理。

    先对句子做分词,然后加上特殊token [CLS] 与 [SEP]。假设我们指定的max_length 为7,则继续补上 [PAD]:

    sentence = 'I love Beijing'
    
    tokens = tokenizer.tokenize(sentence)
    print(tokens)
    
    tokens = ['[CLS]'] + tokens + ['[SEP]']
    print(tokens)
    
    tokens = tokens + ['[PAD]'] * 2
    print(tokens)
    
    ['i', 'love', 'beijing']
    ['[CLS]', 'i', 'love', 'beijing', '[SEP]']
    ['[CLS]', 'i', 'love', 'beijing', '[SEP]', '[PAD]', '[PAD]']

    然后根据tokens构造attention_mask:

    attention_mask = [ 1 if t != '[PAD]' else 0 for t in tokens]
    print(attention_mask)
    
    [1, 1, 1, 1, 1, 0, 0]

    将所有tokens 转为 token id:

    token_ids = tokenizer.convert_tokens_to_ids(tokens)
    print(token_ids)
    
    [101, 1045, 2293, 7211, 102, 0, 0]

    将token_ids 与 attention_mask 转为tensor:

    token_ids = tf.convert_to_tensor(token_ids)
    token_ids = tf.reshape(token_ids, [1, -1])
    
    attention_mask = tf.convert_to_tensor(attention_mask)
    attention_mask = tf.reshape(attention_mask, [1, -1])

    在这些步骤后,我们下一步即可将它们输入到预训练的模型中,得到embedding:

    output = model(token_ids, attention_mask = attention_mask)
    print(output[0].shape, output[1].shape)
    
    (1, 7, 768) (1, 768)

    根据TFModel的API说明[3],这2个返回分别为:

    1. BERT模型最后一层的输出。由于输入有7个tokens,所以对应有7个token的Embedding。其对应的维度为(batch_size, sequence_length, hidden_size)
    2. 输出层中第1个token(这里也就是对应 的[CLS])的Embedding,并且已被一个线性层 + Tanh激活层处理。线性层的权重由NSP作业预训练中得到。其对应的维度为(batch_size, hidden_size)

    4.1. 是否需要其他隐藏层输出

    上面介绍了如何获取BERT最后一层的输出表示,要获取每层的表示也非常简单,仅需要添加参数 output_hidden_states=True 即可,例如:

    output = model(token_ids, attention_mask = attention_mask, output_hidden_states=True)

    不过这里有一点需要讨论的是:是否需要中间隐藏层的输出?还是仅使用最后一层的输出就足够了?

    对于此问题,BERT的研究人员做了进一步研究。在命名体识别任务中,研究人员除了使用BERT最后一层的输出作为提取的特征外,还尝试使用了其他层的输出(例如通过拼接的方式进行组合),并得到以下F1分数:

     

    Fig.1 Sudharsan Ravichandiran. Getting Started with Google BERT[4]

    从这个结果可以看到,在使用最后4层(h9到h12层的拼接)的输出时,能得到比仅使用h12的输出更高的F1分数96.1。所以仅使用BERT最后一层的输出并非在所有场景下都是最好的选择,有时候也需要尝试使用其他层的输出,观察是否能得到更好的效果。

    5. Fine-Tune BERT

    上面介绍了BERT在迁移学习中的一种用法——特征提取(feature extraction)。除此之外,还有另一种用法,称为微调(Fine-tune)。两者主要的区别在于:特征提取直接获取预训练的BERT模型的输出作为特征,对预训练的BERT的模型参数不会有任何改动。而微调是将预训练的BERT与下游任务结合使用,在训练过程中预训练BERT模型的参数会被更新。

    下面我们会介绍如何将预训练的BERT与下游任务结合起来,对预训练的BERT进行Fine-Tune。一般下游任务包括:文本分类、自然语言推理(Natural language inference)、命名体识别(NER)、问答系统(QA)等。这里我们主要介绍一种文本分类任务:情感分析。

    5.1. 文本分类

    以情感分析为例,我们的数据集是一条条文本,每条文本对应一个label。Label可以是1或0,分别代表“正面情感”和“负面情感“。情感分析的任务是:输入一条文本,判断这条文本的label是0还是1。也就是说,这是一个二分类任务。当然,这只是一个最简单的情感分析任务。稍微复杂点的例如:label有多个分类,分别代表“高兴”、“悲伤”、“愤怒”等等,是一个多分类任务。再复杂一点的例如:除了判断这个文本的情感外,还要判断这个情感的强度,例如,label为“高兴”、程度为3。在这里我们仅介绍最简单的情感分类,label只有0和1。

    还是以之前的句子“I love Beijing”为例,我们对句子做分词、加上特定tokens、补全到max_length、生成token_id、attention_mask,并送入到BERT。得到最后一层输出的 [CLS]

    的Embedding表示。此时E([CLS]) 包含了整个句子的表示,所以可以将此表示输入到前馈网络与softmax中,输出类别概率,用于判断这个句子属于哪个类别。此时:

    1. 若是使用的Fine-Tune,则在训练过程中,预训练的BERT模型的参数与前馈网络的参数都会得到更新;
    2. 若是使用的Feature-Extraction,则在训练过程中,仅有前馈网络的参数会得到更新,预训练的BERT模型的参数不会更新

    下面以IMDB数据集为例,介绍BERT fine-tune的方法。

    首先引入依赖包、加载数据集、加载预训练的模型:

    import tensorflow as tf 
    from tensorflow import keras
    import tensorflow_datasets as tfds
    import numpy as np
    from transformers import TFBertForSequenceClassification, BertTokenizerFast
    
    # load dataset
    imdb_train, df_info = tfds.load(name='imdb_reviews', split='train', with_info=True, as_supervised=True)
    imdb_test = tfds.load(name='imdb_reviews', split='test', as_supervised=True)
    
    # load pretrained bert model
    model = TFBertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
    tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")

    这里需要注意的是,我们使用的是TFBertForSequenceClassification和BertTokenizerFast。TFBertForSequenceClassification是包装好的类,专门用于做分类,由1层bert、1层Dropout、1层前馈网络组成,其定义可以参考官网[5]。BertTokenizerFast 也是一个方便的tokenizer类,会比BertTokenizer更快一些。

    对输入数据做分词:

    # tokenize every sequence
    def bert_encoder(review):
        encoded = tokenizer(review.numpy().decode('utf-8'), truncation=True, max_length=150, pad_to_max_length=True)
        return encoded['input_ids'], encoded['token_type_ids'], encoded['attention_mask']
    
    bert_train = [bert_encoder(r) for r, l in imdb_train]
    bert_label = [l for r, l in imdb_train]
    
    bert_train = np.array(bert_train)
    bert_label = tf.keras.utils.to_categorical(bert_label, num_classes=2)
    
    print(bert_train.shape, bert_label.shape) 
    (25000, 3, 150) (25000, 2)

    训练数据的格式是(sentence, label),对每个sentence通过BertTokenizerFast做tokenize后,会直接得到input_ids, token_type_ids 以及attention_mask,它们便是需要输入到BERT的格式。最后将它们转为numpy 数组。

    bert_train的维度为(25000, 3, 150),即分别对应了:

    1. 数据总条数;
    2. 每条数据对应的input_ids、token_type_ids、attention_mask
    3. Max_length(指定的长度为150)

    将训练集继续分割为训练集与验证集:

    # create training and validation splits
    from sklearn.model_selection import train_test_split
    
    x_train, x_val, y_train, y_val = train_test_split(bert_train, 
                                             bert_label,
                                             test_size=0.2, 
                                             random_state=42)
    print(x_train.shape, y_train.shape)
    (20000, 3, 150) (20000, 2)

    进一步对输入数据进行处理:

    tr_reviews, tr_segments, tr_masks = np.split(x_train, 3, axis=1)
    val_reviews, val_segments, val_masks = np.split(x_val, 3, axis=1)
    
    tr_reviews = tr_reviews.squeeze()
    tr_segments = tr_segments.squeeze()
    tr_masks = tr_masks.squeeze()
    val_reviews = val_reviews.squeeze()
    val_segments = val_segments.squeeze()
    val_masks = val_masks.squeeze()
    
    def example_to_features(input_ids,attention_masks,token_type_ids,y):
        return {"input_ids": input_ids,
              "attention_mask": attention_masks,
              "token_type_ids": token_type_ids},y
    
    train_ds = tf.data.Dataset.from_tensor_slices((tr_reviews, tr_masks, tr_segments, y_train)).map(example_to_features).shuffle(100).batch(16)
    valid_ds = tf.data.Dataset.from_tensor_slices((val_reviews, val_masks, val_segments, y_val)).map(example_to_features).shuffle(100).batch(16)

    TFBertForSequenceClassification的输入格式(除label外的输入)可以以有多种形式提供,这里使用的是字典的形式。具体格式说明可以参考官方文档的说明[5]

    指定训练参数并进行训练:

    optimizer = tf.keras.optimizers.Adam(learning_rate=2e-5)
    loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
    model.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])
    
    bert_history = model.fit(train_ds, epochs=4, validation_data=valid_ds)
    
    Epoch 1/4
    1250/1250 [==============================] - 701s 551ms/step - loss: 0.4085 - accuracy: 0.8131 - val_loss: 0.3011 - val_accuracy: 0.8718
    Epoch 2/4
    1250/1250 [==============================] - 689s 551ms/step - loss: 0.2104 - accuracy: 0.9186 - val_loss: 0.3252 - val_accuracy: 0.8858
    Epoch 3/4
    1250/1250 [==============================] - 689s 551ms/step - loss: 0.1105 - accuracy: 0.9622 - val_loss: 0.4201 - val_accuracy: 0.8816
    Epoch 4/4
    1250/1250 [==============================] - 689s 551ms/step - loss: 0.0696 - accuracy: 0.9774 - val_loss: 0.4153 - val_accuracy: 0.8876

    这里作为演示,仅训练了4轮。从验证集的准确率来看,还有上升的趋势,所以理论上还可以增加epoch轮数。

    6. 总结

    BERT预训练模型与迁移学习的结合使用,当前仍是NLP各类应用以及比赛的主流。不过,当前我们仅介绍了BERT预训练模型中最基本的一种:bert-base。除此之外,BERT还有很多的变种,例如allbert、roberta、electra、spanbert等等,分别用于不同的场景。下一章我们继续介绍BERT的这些变种。

    References

    [1] https://github.com/google-research/bert

    [2] Pretrained models — transformers 4.11.2 documentation (huggingface.co)

    [3] BERT — transformers 4.12.0.dev0 documentation (huggingface.co)

    [4] Getting Hands-On with BERT | Getting Started with Google BERT (oreilly.com)

    [5]https://huggingface.co/transformers/master/_modules/transformers/models/bert/modeling_tf_bert.html#TFBertForSequenceClassification

    [6] https://learning.oreilly.com/library/view/advanced-natural-language/9781800200937/Chapter_4.xhtml

    [7] https://huggingface.co/transformers/master/_modules/transformers/models/bert/tokenization_bert_fast.html#BertTokenizerFast

  • 相关阅读:
    Objective-C 调用C++,C
    ios项目不能再用UDID了
    xcode 4 制作静态库详解
    Icon specified in the Info.plist not found under the top level app wrapper: Icon.png
    吼吼 尬English
    Redis
    处理android 经典蓝牙发送文件时接收包的问题
    Md5加密的文件流中是否会包含其md5值
    Android gradle buid failed case
    Android GDB 调试
  • 原文地址:https://www.cnblogs.com/zackstang/p/15387549.html
Copyright © 2020-2023  润新知