BERT使用手册

作者: 晓柒NLP与药物设计 | 来源:发表于2021-10-22 15:02 被阅读0次

    transformers是huggingface提供的预训练模型库,可以轻松调用API来得到你的词向量。transformers的前身有pytorch-pretrained-bert,pytorch-transformers,原理基本都一致。本文以bert为例,主要介绍如何调用transformers库以及下游任务的使用方法。

    1. transformers相关配置

    • 在正式使用之前,首先要安装transformers包,此以python3.7为例:

      python == 3.7.3
      tensorflow == 2.0.0
      pytorch == 1.5.1
      transformers == 3.0.2
      

      若准备采用GPU加速,需自于Pytorch、Tensorflow官网上进行CUDA、CuDNN版本遴选配置

    2. 整理架构

    • transformers新版本中使用每个模型只需要三个标准类

      • configuration:configuration是模型具体的结构配置,例如可以配置多头的数量等,这里配置需要注意的地方就是,如果自定义配置不改变核心网络结构的则仍旧可以使用预训练模型权重,如果配置涉及到核心结构的修改,例如前馈网络的隐层神经元的个数,则无法使用预训练模型权重,这个时候transformers会默认你要重新自己预训练一个模型从而随机初始化整个模型的权重,这是是一种半灵活性的设计

        # configuration.json
        {
          "architectures": [
            "BertForMaskedLM"
          ],
          "attention_probs_dropout_prob": 0.1,
          "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": 12,
          "num_hidden_layers": 12,
          "type_vocab_size": 2,
          "vocab_size": 30522
        }
        

        将配置好的configuration.json配置加载如下:

        config = BertConfig.from_json_file(os.path.join(PATH, "config.json"))
        model = BertModel.from_pretrained(PATH, config=config)
        
        #Tensorflow2版本
        from transformers import TFBertModel
        import tensorflow as tf
        config = BertConfig.from_json_file(os.path.join(PATH, "config.json"))
        model = TFBertModel.from_pretrained(PATH, config=config)
        
      • models:models用于指定使用哪一种模型,例如model为bert,则相应的网络结构为bert的网络结构

        # Pytorch版本
        import torch
        from transformers import BertModel, BertConfig, BertTokenizer
        model = BertModel.from_pretrained("bert-base-uncased")
        
        # Tensorflow2版本
        import tensorflow as tf 
        from transformers import TFBertModel, BertConfig, BertTokenizer
        model = TFBertModel.from_pretrained("bert-base-uncased")
        
      • tokenizer

        # Pytorch版本
        import torch
        from transformers import BertModel, BertConfig, BertTokenizer
        tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
        
        # Tensorflow2版本
        import tensorflow as tf 
        from transformers import TFBertModel, BertConfig, BertTokenizer
        tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")
        

        所有这些类都可以使用通用的from_pretrained()实例化方法,以简单统一的方式从受过训练的实例中初始化

    3. 参数详解

    3.1 配置 Bert 模型 transformers.PretrainedConfig

    • transformers.BertConfig 可以自定义 Bert 模型的结构,以下参数都是可选的:

      • 通用参数
        • vocab_size:词汇数,默认30522
        • hidden_size:编码器内隐藏层神经元数量,默认768
        • num_hidden_layers:编码器内隐藏层层数,默认12
        • num_attention_heads:编码器内注意力头数,默认12
      • 其他参数
        • intermediate_size:编码器内全连接层的输入维度,默认3072
        • hidden_act:编码器内激活函数,默认"gelu",还可为"relu"、"swish"或 "gelu_new"
        • hidden_dropout_prob:词嵌入层或编码器的 dropout,默认为0.1
        • attention_probs_dropout_prob:注意力机制的 dropout,默认为0.1
        • max_position_embeddings:模型使用的最大序列长度,默认为512
        • type_vocab_size:词汇表类别,默认为2
        • initializer_range:神经元权重的标准差,默认为0.02
        • layer_norm_eps:layer normalization 的 epsilon 值,默认为 1e-12
    • transformers.BertConfig.from_json_file可以从json文件中进行配置参数的修改:

      transformers.BertConfig.from_json_file(json_file)
      
    • transformers.BertConfig.from_dict可以从字典中进行配置参数的修改

    • transformers.BertConfig.from_pretrained可以从预训练模型中进行配置参数的配置

      transformers.BertConfig.from_pretrained(pretrained_model_name_or_path)
      
    • Examples:

      config = BertConfig.from_pretrained('bert-base-uncased')
      config = BertConfig.from_pretrained('./test/saved_model/')
      config = BertConfig.from_pretrained('./test/saved_model/my_configuration.json')
      config = BertConfig.from_pretrained('bert-base-uncased', output_attentions=True, foo=False)
      

    3.2 加载 Bert 模型 transformers.PreTrainedModel

    • transformers.BertModel.from_pretrained 从预训练的模型配置实例化预训练的 pytorch 模型

    • BertModel 主要为transformer encoder结构,包含三个部分:

      • embeddings,即BertEmbeddings类的实体,根据单词符号获取对应的向量表示;

      • encoder,即BertEncoder类的实体;

      • pooler,即BertPooler类的实体,这一部分是可选的。

        transformers.BertModel.from_pretrained(pretrained_model_name_or_path)
        
        #Tensorflow2版本
        transformers.TFBertModel.from_pretrained(pretrained_model_name_or_path)
        
    • BertModel前向传播过程中各个参数的含义以及返回值:

      • input_ids:经过 tokenizer 分词后的 subword 对应的下标列表;
      • attention_mask:在 self-attention 过程中,这一块 mask 用于标记 subword 所处句子和
        padding 的区别,将 padding 部分填充为 0;
      • token_type_ids:标记 subword 当前所处句子(第一句/第二句/ padding);
      • position_ids:标记当前词所在句子的位置下标;
      • head_mask:用于将某些层的某些注意力计算无效化;
      • inputs_embeds:如果提供了,那就不需要input_ids,跨过 embedding lookup 过程直接作为 Embedding 进入 Encoder 计算;
      • encoder_hidden_states:这一部分在 BertModel 配置为 decoder 时起作用,将执行 cross-attention 而不是 self-attention;
      • encoder_attention_mask:同上,在 cross-attention 中用于标记 encoder 端输入的 padding;
      • past_key_values:这个参数貌似是把预先计算好的 K-V 乘积传入,以降低 cross-attention 的开销(因为原本这部分是重复计算);
      • use_cache:将保存上一个参数并传回,加速 decoding;
      • output_attentions:是否返回中间每层的 attention 输出;
      • output_hidden_states:是否返回中间每层的输出;
      • return_dict:是否按键值对的形式(ModelOutput 类,也可以当作 tuple 用)返回输出,默认为真。
        注意,这里的 head_mask 对注意力计算的无效化,和下文提到的注意力头剪枝不同,而仅仅把某些注意力的计算结果给乘以这一系数。
    • Examples:

    from transformers import BertConfig, BertModel
    # 从huggingface.co 下载模型和配置并缓存。
    model = BertModel.from_pretrained('bert-base-uncased')
    model = BertModel.from_pretrained('./test/saved_model/')
    # 在加载期间更新配置
    model = BertModel.from_pretrained('bert-base-uncased', output_attentions=True)
    assert model.config.output_attentions == True
    config = BertConfig.from_json_file('./tf_model/my_tf_model_config.json')
    model = BertModel.from_pretrained('./tf_model/my_tf_checkpoint.ckpt.index', from_tf=True, config=config)
    # Loading from a Flax checkpoint file instead of a PyTorch model (slower)
    model = BertModel.from_pretrained('bert-base-uncased', from_flax=True)
    

    3.3 优化器 Optimization

    3.3.1transformers.AdamW
    • AdamW:transformers 库实现了基于权重衰减的优化器,这个优化器初始化时有6个参数,第一个是params,可以是torch的Parameter,也可以是一个grouped参数。betas是Adam的beta参数,b1和b2。eps也是Adam为了数值稳定的参数。correct_bias,如果应用到tf的模型上时需要设置为False。具体参数配置如下:
      • params ( Iterable[nn.parameter.Parameter]) :可迭代的参数以优化或定义参数组的字典
      • lr ( float, optional , defaults to 1e-3) :要使用的学习率
      • betas ( Tuple[float,float], optional , defaults to (0.9, 0.999)) :Adam 的 betas 参数 (b1, b2)
      • eps ( float, optional , defaults to 1e-6) :Adam's epsilon 数值稳定性
      • weight_decay ( float, optional , defaults to 0) :要应用的解耦权重衰减
      • Correct_bias ( bool, optional , defaults to True ) :是否纠正 Adam 中的偏差
    3.3.2 transformers.Adafactor
    • Adafactor:具有次线性记忆代价的自适应学习率 。请注意,此优化器根据scale参数、相对步长和warmup_init选项在内部调整学习速率。要使用手动(外部)学习速率计划,您应将scale参数设置为False,相对步骤设置为False。具体参数配置如下:
      • params (Iterable[nn.parameter.Parameter]):用于优化的参数的Iterable或定义参数组的字典。
      • lr (float, optional) :要使用的学习率
      • eps (Tuple[float, float], optional, defaults to (1e-30, 1e-3)) :平方梯度和参数比例的正则化常数
      • clip_threshold (float, optional, defaults 1.0):最终梯度更新的均方根阈值
      • decay_rate (float, optional, defaults to -0.8):用于计算平方的运行平均值的系数
      • beta1 (float, optional) :用于计算梯度运行平均值的系数
      • weight_decay (float, optional, defaults to 0):权重衰减(L2惩罚)
      • scale_parameter (bool, optional, defaults to True):如果为True,则学习率按均方根进行缩放
      • relative_step (bool, optional, defaults to True):如果为True,则计算与时间相关的学习率,而不是外部学习率
      • warmup_init (bool, optional, defaults to False) :时间相关的学习速率计算取决于是否使用预热初始化
    3.3.3transformers.AdamWeightDecay(Tensorflow2)
    • AdamWeightDecay:transformers 库实现的对于TF2模型的基于权重衰减的优化器,这个优化器初始化时有10个参数,第一个是learning_rate,用于设置基础学习率。接下来是Adam的衰减稳定设置 beta_1,beta_2与epsilon。amsgrad决定是否应用Amsgrad,weight_decay_rate决定了衰减率, include_in_weight_decay决定了应用权重衰减的参数名字,exclude_from_weight_decay决定了不参与权重衰减的参数名字,name和kwargs是tensorflow优化器的常规参数。具体参数配置如下:
      • learning_rate ( float, optional, default to 1e-3 ) :基础学习率
      • beta_1, beta_2, epsilon ( float, optional , defaults to 0.9, 0.999, 1e-7 ) :Adam设置参数
      • amsgrad ( bool, optional, default to False ) :决定是否应用Amsgrad
      • weight_decay_rate ( float, optional , defaults to 0 ) :权重衰减率
      • include_in_decay ( List[str], optional) :应用权重衰减的参数名字
      • exclude_from_weight_deacy ( List[str],optional ) :不参与权重衰减的参数名字
      • name ( str, optional, defaults to ‘AdamWeightDecay’ ) :名称
    3.3.4 Examples:
    from transformers.optimization import Adafactor, AdafactorSchedule
    optimizer = Adafactor(model.parameters(), scale_parameter=True, relative_step=True, warmup_init=True, lr=None)
    lr_scheduler = AdafactorSchedule(optimizer)
    ***training step***
        optimizer.step()
        scheduler.step()
    

    3.4 加载分词模型 transformers.PreTrainedTokenizer

    • 所有分词器的基类:处理tokenization和special tokens的所有共享方法,以及下载/缓存/加载预训练标记器的方法以及向词汇表中添加标记的方法。

      • BasicTokenizer负责处理的第一步——按标点、空格等分割句子,并处理是否统一小写,以及清理非法字符
      • 对于中文字符,通过预处理(加空格)来按字分割;同时可以通过never_split指定对某些词不进行分割;这一步是可选的(默认执行)
      • Word_Piece_Tokenizer在词的基础上,进一步将词分解为子词(subword)。subword 介于 char 和 word 之间,既在一定程度保留了词的含义,又能够照顾到英文中单复数、时态导致的词表爆炸和未登录词的 OOV(Out-Of-Vocabulary)问题,将词根与时态词缀等分割出来,从而减小词表,也降低了训练难度
      • 例如,tokenizer 这个词就可以拆解为"token"和"##izer"两部分,注意后面一个词的"##"表示接在前一个词后面。
    • BertTokenizer有以下常用方法:

      • vocab_file (str):包含词汇表的文件
      • do_lower_case (bool, optional, defaults to True):标记化时是否将输入小写
      • do_basic_tokenize (bool, optional, defaults to True):是否在WordPiece之前进行基本分词化
      • never_split (Iterable, optional):在标记化过程中永远不会分割的标记集合。仅当do_basic_tokenize=True时才有效
      • unk_token (str, optional, defaults to "[UNK]"):未知标记。不在词汇表中的令牌无法转换为ID,而是设置为此标记
      • sep_token (str, optional, defaults to "[SEP]"):分隔符标记,用于从多个序列构建序列,例如用于序列分类的两个序列或用于文本和用于问答的问题。它还用作使用特殊令牌构建的序列的最后一个标记
      • pad_token (str, optional, defaults to "[PAD]"):用于填充的标记,例如在批处理不同长度的序列时补充
      • cls_token (str, optional, defaults to "[CLS]"):进行序列分类时使用的分类器标记(对整个序列进行分类,而不是按令牌分类)。当使用特殊标记构建时,它是序列的第一个标记。
      • mask_token (str, optional, defaults to "[MASK]"):用于MASK值的标记。这是使用掩码语言建模训练此模型时使用的标记,作为模型将尝试预测的标记
    • Example:

      # 获取最后一层隐层的embedding
      from transformers import BertTokenizer, BertModel
      import torch
      
      tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
      model = BertModel.from_pretrained('bert-base-uncased')
      
      inputs = tokenizer("Hello, my dog is cute", return_tensors="pt")
      inputs = torch.tensor([inputs])
      outputs = model(**inputs)
      
      last_hidden_states = outputs.last_hidden_state
      

    3.5 BERT的应用微调

    • 将数据集转换为可以训练BERT的格式

      • BERT令牌生成器

        • 要将文本提供给BERT,必须将其拆分为令牌,然后将这些令牌映射到令牌生成器词汇表中的索引

          tokenizer = BertTokenizer.from_pretrained('bert-base-uncased', do_lower_case=True)
          sentence = "Hello, my son is cuting."
          input_ids_method1 = torch.tensor(
              tokenizer.encode(sentence, add_special_tokens=True))  # Batch size 1
              # tensor([ 101, 7592, 1010, 2026, 2365, 2003, 3013, 2075, 1012,  102])
          input_token2 = tokenizer.tokenize(sentence)
          # ['hello', ',', 'my', 'son', 'is', 'cut', '##ing', '.']
          input_ids_method2 = tokenizer.convert_tokens_to_ids(input_token2)
          # tensor([7592, 1010, 2026, 2365, 2003, 3013, 2075, 1012])
          # 并没有开头和结尾的标记:[cls]、[sep]
          
      • 特殊令牌添加

        • 在每个句子的末尾,我们需要附加特殊[SEP]标记,该令牌是两句任务的产物,其中给BERT两个单独的句子并要求确定某些内容
        • 对于分类任务,必须[CLS]每个句子的开头添加特殊标记。此令牌具有特殊意义。BERT由12个Transformer层组成。每个转换器接收一个令牌嵌入列表,并在输出上产生相同数量的嵌入
        • 句子长度与掩码,BERT有两个制约因素
          • 所有句子都必须填充或截断为单个固定长度
          • 句子的最大长度为512个令牌
          • 使用特殊[PAD]令牌完成填充,该令牌在BERT词汇表中的索引为0处
      • examples:

        # Tokenize all of the sentences and map the tokens to thier word IDs.
        input_ids = []
        attention_masks = []
        
        # For every sentence...
        for sent in sentences:
            # `encode_plus` will:
            #   (1) Tokenize the sentence.
            #   (2) Prepend the `[CLS]` token to the start.
            #   (3) Append the `[SEP]` token to the end.
            #   (4) Map tokens to their IDs.
            #   (5) Pad or truncate the sentence to `max_length`
            #   (6) Create attention masks for [PAD] tokens.
            encoded_dict = tokenizer.encode_plus(
                                sent,                      # Sentence to encode.
                                add_special_tokens = True, # Add '[CLS]' and '[SEP]'
                                max_length = 64,           # Pad & truncate all sentences.
                                pad_to_max_length = True,
                                return_attention_mask = True,   # Construct attn. masks.
                                return_tensors = 'pt',     # Return pytorch tensors.
                           )
            # Add the encoded sentence to the list.    
            input_ids.append(encoded_dict['input_ids'])
        
            # And its attention mask (simply differentiates padding from non-padding).
            attention_masks.append(encoded_dict['attention_mask'])
        
        # Convert the lists into tensors.
        input_ids = torch.cat(input_ids, dim=0)
        attention_masks = torch.cat(attention_masks, dim=0)
        labels = torch.tensor(labels)
        
        # Print sentence 0, now as a list of IDs.
        print('Original: ', sentences[0])
        print('Token IDs:', input_ids[0])
        '''
        Original : Our friends won't buy this analysis, let alone the next one we propose.
        Token IDs: 
                    tensor([  101,  2256,  2814,  2180,  1005,  1056,  4965,  2023,  4106,  1010,
                             2292,  2894,  1996,  2279,  2028,  2057, 16599,  1012,   102,     0,
                                0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
                                0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
                                0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
                                0,     0,     0,     0,     0,     0,     0,     0,     0,     0,
                                0,     0,     0,     0])
        '''
        

        后续直接将BERT进行模型训练、模型存储即可

    4.1 模型应用案例

    BertForSequenceClassification(文本分类)

    本文将在这一节重点介绍Bert的全系使用方法,包括工具引入、数据处理、计算资源选择、模型训练与测试使用。由于Bert不同任务存在重叠性,后续讲不再赘述重合部分,而是专注于网络结构。

    • 所需引入工具包

      import random
      import torch
      from torch.utils.data import TensorDataset, DataLoader, random_split
      from transformers import BertTokenizer
      from transformers import BertForSequenceClassification, AdamW
      from transformers import get_linear_schedule_with_warmup
      
    • 使用计算资源为GPU还是CPU(Bert建议使用GPU启动加载,否则训练过程过慢),以及所用seed确定

      # device可选择cuda | cpu
      device = torch.device('cuda')
      # 随机种子数值确定
      random.seed(seed)
      np.random.seed(seed)
      torch.manual_seed(seed)
      torch.cuda.manual_seed_all(seed)
      torch.backends.cudnn.deterministic = True
      
    • 加载数据

      # pandas读取数据[sentence, type]
      data = pd.read_pickle("XXX.csv")
      # sentences
      text_values = list(df(['sentence']))
      # label
      label_sample = list(df['type'])
      
    • 加载bert模型

      # 加载预训练分词模型
      tokenizer = BertTokenizer.from_pretrained('bert-base-chinese', do_lower_case=False)
      # 加载预训练Bert模型
      model = BertForSequenceClassification.from_pretrained(
          'bert-base-uncased', 
          num_labels=num_labels, 
          output_attentions=False, 
          output_hidden_states=False
      )
      # 决定是否将模型推送到GPU
      model.cuda()
      
    • 训练数据准备

      # 函数获取文本列表的令牌ID
      def encode_fn(text_list):
          all_input_ids = []    
          for text in text_list:
              input_ids = tokenizer.encode(
                              text,                      
                              add_special_tokens = True,  # 添加special tokens, 也就是CLS和SEP
                              max_length = 160,           # 设定最大文本长度
                              pad_to_max_length = True,   # pad到最大的长度  
                              return_tensors = 'pt'       # 返回的类型为pytorch tensor
                         )
              all_input_ids.append(input_ids)    
          all_input_ids = torch.cat(all_input_ids, dim=0)
          return all_input_ids
      all_input_ids = encode_fn(text_values)
      
    • 讲数据分为训练集与验证集,并构建dataloader

      epochs = 4
      batch_size = 32
      
      # 将数据拆分为训练集和验证集
      dataset = TensorDataset(all_input_ids, labels)
      # 此处采用9:1的数据集构建
      train_size = int(0.90 * len(dataset))
      val_size = len(dataset) - train_size
      train_dataset, val_dataset = random_split(dataset, [train_size, val_size])
      
      # 创建训练和验证数据集的DataLoader
      train_dataloader = DataLoader(train_dataset, batch_size = batch_size, shuffle = True)
      val_dataloader = DataLoader(val_dataset, batch_size = batch_size, shuffle = False)
      
    • 定义Bert训练所需optimizer与learning rate scheduler

      # 创建优化器和学习率计划
      optimizer = AdamW(model.parameters(), lr=2e-5)
      total_steps = len(train_dataloader) * epochs
      scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=total_steps)
      
    • 定义一个可视化指标计算的方法,此处以sklearn的accuracy为例

      from sklearn.metrics import f1_score, accuracy_score
      def flat_accuracy(preds, labels):   
          pred_flat = np.argmax(preds, axis=1).flatten()
          labels_flat = labels.flatten()
          return accuracy_score(labels_flat, pred_flat)
      
    • Bert的训练与验证

      for epoch in range(epochs):
          # 训练集过程
          model.train()
          total_loss, total_val_loss = 0, 0
          total_eval_accuracy = 0
          for step, batch in enumerate(train_dataloader):
              model.zero_grad()
              loss, logits = model(batch[0].to(device), token_type_ids=None, attention_mask=(batch[0]>0).to(device), labels=batch[1].to(device))
              total_loss += loss.item()
              loss.backward()
              torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
              optimizer.step() 
              scheduler.step()
          # 测试集过程
          model.eval()
          for i, batch in enumerate(val_dataloader):
              with torch.no_grad():
                  loss, logits = model(batch[0].to(device), token_type_ids=None, attention_mask=(batch[0]>0).to(device), labels=batch[1].to(device))
                  total_val_loss += loss.item()
                  logits = logits.detach().cpu().numpy()
                  label_ids = batch[1].to('cpu').numpy()
                  total_eval_accuracy += flat_accuracy(logits, label_ids)
          avg_train_loss = total_loss / len(train_dataloader)
          avg_val_loss = total_val_loss / len(val_dataloader)
          avg_val_accuracy = total_eval_accuracy / len(val_dataloader)
          print(f'Train loss     : {avg_train_loss}')
          print(f'Validation loss: {avg_val_loss}')
          print(f'Accuracy: {avg_val_accuracy:.2f}')
          print('\n')
      '''
      输出格式
      Train loss     : 0.3275374324204257
      Validation loss: 0.3286557973672946
      Accuracy: 0.88
      '''
      
    • 模型预测过程,数据集构建参考训练集dataloader

      model.eval()
      preds = []
      for i, (batch,) in enumerate(pred_dataloader):
          with torch.no_grad():
              outputs = model(batch.to(device), token_type_ids=None, attention_mask=(batch>0).to(device))
              logits = outputs[0]
              logits = logits.detach().cpu().numpy()
              preds.append(logits)
      final_preds = np.concatenate(preds, axis=0)
      final_preds = np.argmax(final_preds, axis=1)
      '''
      输出格式如下,后续按照标签编号回推即可
      1 0 0 1 0
      '''
      

    BertForSequenceClassification(文本分类 Tensorflow2)

    Tensorflow2上应用预训练模型进行文本分类的一个简单例子。

    • 准备过程,加载模型和数据

      import tensorflow as tf
      import tensorflow_datasets
      from transformers import *
      tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
      model = TFBertForSequenceClassification.from_pretrained('bert-base-cased')
      data = tensorflow_datasets.load('glue/mrpc')
      
    • 训练数据准备

      train_dataset = glue_convert_examples_to_features(data['train'], tokenizer, 128, 'mrpc')
      valid_dataset = glue_convert_examples_to_features(data['validation'], tokenizer, 128, 'mrpc')
      train_dataset = train_dataset.shuffle(100).batch(32).repeat(2)
      valid_dataset = valid_dataset.batch(64)
      
    • 优化器设置以及训练

      optimizer = tf.keras.optimizer.Adam(learning_rate=3e-5, epsilon=1e-8, clipnorm=1.0)
      metric = tf.keras.metrics.SparseCategoricalAccuracy('accuracy')
      loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
      model.compile(optimizer=optimizer, loss=loss, metrics=[metric])
      
      model.fit(train_dataset, epochs=2, steps_per_epoch=115, validation_data=valid_dataset, validation_step=7)
      
    • 模型预测

      sentence_0 = "This research was consistent with his findings"
      sentence_1 = "his findings were compatible with this research"
      sentence_2 = "his findings were not compatible with this research"
      inputs1 = tokenizer.encoder_plus(sentence_0, sentence_1, add_special_tokens=True, return_tensors='pt')
      inputs2 = tokenizer.encoder_plus(sentence_0, sentence_2, add_special_tokens=True, return_tensors='pt')
      
      pred1 = tf.argmax(model.predict(**inputs1)[0])
      pred2 = tf.argmax(model.predict(**inputs2)[0])
      

    BertForTokenClassification(token分类)

    以NER任务为例,命名实体识别任务是NLP中的一个基础任务。主要是从一句话中识别出命名实体,下为Bert+CRF的模型结构,训练过程与数据预处理部分参考上章内容,后续不再赘述,此处展示两种BERT使用方法

    • 常规方案:直接使用Bert作为底层embedding,后续拼接其他网络,其网络结构如下:

      import torch
      from transformers import BertModel, BertPreTrainedModel
      import torch.nn as nn
      from torchcrf import CRF
      
      class BertNER(BertPreTrainedModel):
          def __init__(self, config):
              super(BertNER, self).__init__(config)
              self.num_labels = config.num_labels
              self.bert = BertModel(config)
              self.dropout = nn.Dropout(config.hidden_dropout_prob)
              self.classifier = nn.Linear(config.hidden_size, config.num_labels)
              self.crf = CRF(config.num_labels, batch_first=True)
              self.init_weights()
      
          def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None,
                      position_ids=None, inputs_embeds=None, head_mask=None):
              outputs = self.bert(input_ids=input_ids,
                                  attention_mask=attention_mask,
                                  token_type_ids=token_type_ids,
                                  position_ids=position_ids,
                                  head_mask=head_mask,
                                  inputs_embeds=inputs_embeds)
              sequence_output = outputs[0]
              sequence_output = self.dropout(sequence_output)
              # 得到判别值
              logits = self.classifier(sequence_output)
              outputs = (logits,)
              if labels is not None:
                  if attention_mask is not None:
                      active_loss = attention_mask.view(-1) == 1
                      active_logits = logits.view(-1, self.num_labels)
                      active_labels = torch.where(
                          active_loss, labels.view(-1), torch.tensor(-100).type_as(labels)
                      )  # [-100, -100, 1, 0...]
                  else:
                      active_logits = logits.view(-1, self.num_labels)
                      active_labels = labels.view(-1)
                  select_index = []
                  final_labels = []
                  for index, label in enumerate(active_labels):
                      if label != -100:
                          final_labels.append(label)
                          select_index.append(index)
                  device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
                  active_logits.to(device)
                  select_index = torch.tensor(select_index, device=active_logits.device)
                  final_labels = torch.tensor(final_labels, device=active_logits.device).unsqueeze(0)
                  final_logits = active_logits.index_select(0, select_index).unsqueeze(0)
                  loss = self.crf(final_logits, final_labels) * (-1)
                  outputs = (loss,) + outputs
              # contain: (loss), scores
              return outputs
      
    • 直接使用BertForTokenClassification类进行序列标注,可对其模型进行修改拼接,其模型结构如下

      import torch
      from transformers import BertForTokenClassification
      
      class TokenClassifier(BertForTokenClassification):
          def forward(self, input_ids, attention_mask=None, token_type_ids=None,
                      labels=None, valid_ids=None, attention_mask_label=None):
              sequence_output = self.bert(input_ids, attention_mask, token_type_ids,
                                          head_mask=None)[0]
              batch_size, max_len, feat_dim = sequence_output.shape
              valid_output = torch.zeros(
                  batch_size, max_len, feat_dim, dtype=torch.float32,
                  device=next(self.parameters()).device
              )
              for i in range(batch_size):
                  jj = -1
                  for j in range(max_len):
                      if valid_ids[i][j].item() == 1:
                          jj += 1
                          valid_output[i][jj] = sequence_output[i][j]
              sequence_output = self.dropout(valid_output)
              logits = self.classifier(sequence_output)
      
              if labels is not None:
                  loss_fct = torch.nn.CrossEntropyLoss(ignore_index=0)
                  attention_mask_label = None
                  if attention_mask_label is not None:
                      active_loss = attention_mask_label.view(-1) == 1
                      active_logits = logits.view(-1, self.num_labels)[active_loss]
                      active_labels = labels.view(-1)[active_loss]
                      loss = loss_fct(active_logits, active_labels)
                  else:
                      loss = loss_fct(logits.view(-1, self.num_labels),
                                      labels.view(-1))
                  return (loss, logits)
              else:
                  return (logits,)
      

    BertForTokenClassification(QA得分)

    此类适用于问答系统,输入中将上面几个模型中的label改成了start_position和end_position,即答案在原文中起始和结束位置,输出是将预测分数改成了对答案起始位置和结束位置的预测分数。下面为直接使用预训练模型的代码参考

    • 准备过程,加载模型

      from transformers import BertTokenizer, BertForQuestionAnswering
      model = BertForQuestionAnswering.from_pretrained(MODEL_PATH)
      tokenizer = BertTokenizer.from_pretrained(VOCAB_PATH)
      
    • 保留概率最大的答案

      with torch.no_grad():
          model.eval()
          pred_results = {}
          for batch in tqdm(test_dataloader):
              q_ids, raw_sentence, input_ids, segment_ids = batch
              input_ids, segment_ids = \
              input_ids.to(device), segment_ids.to(device)
              input_mask = (input_ids > 0).to(device)
              start_prob, end_prob = model(input_ids.to(device),
                                           token_type_ids=segment_ids.to(device),
                                           attention_mask=input_mask.to(device)
                                          )
              # start_prob = start_prob.squeeze(0)
              # end_prob = end_prob.squeeze(0)
              for i in range(len(batch[0])):
                  try:
                      (best_start, best_end), max_prob = find_best_answer_for_passage(start_prob[i], end_prob[i])
                      if type(max_prob) == int:
                          max_prob = 0
                      else:
                          max_prob = max_prob.cpu().numpy()[0]
                  except:
                      pass
                  if q_ids[i] in pred_results:
                      pred_results[q_ids[i]].append(
                          (raw_sentence[i][best_start.cpu().numpy()[0]:best_end.cpu().numpy()[0]], max_prob))
                  else:
                      pred_results[q_ids[i]] = [(raw_sentence[i][best_start.cpu().numpy()[0]:best_end.cpu().numpy()[0]], max_prob)]
          # 保留最大概率的答案
          for id in pred_results:
              pred_results[id] = sorted(pred_results[id], key=lambda x: x[1], reverse=True)[0]
      
          submit = {}
          for item in test_data:
              q_id = item[0]
              question = item[2]
              if q_id not in pred_results:continue
              submit[q_id] = pred_results[q_id][0].strip()
              print(question, pred_results[q_id][0].strip())
      

    5.1 Bert原理简介

    BERT的全称为Bidirectional Encoder Representation from Transformers,是一个预训练的语言表征模型。它强调了不再像以往一样采用传统的单向语言模型或者把两个单向语言模型进行浅层拼接的方法进行预训练,而是采用新的masked language model(MLM),以致能生成深度的双向语言表征。BERT论文发表时提及在11个NLP(Natural Language Processing,自然语言处理)任务中获得了新的state-of-the-art的结果,令人目瞪口呆。

    该模型有以下主要优点:

    • 采用MLM对双向的Transformers进行预训练,以生成深层的双向语言表征
    • 预训练后,只需要添加一个额外的输出层进行fine-tune,就可以在各种各样的下游任务中取得state-of-the-art的表现。在这过程中并不需要对BERT进行任务特定的结构修改。

    5.1.1 Bert结构

    以往的预训练模型的结构会受到单向语言模型(从左到右或者从右到左)的限制,因而也限制了模型的表征能力,使其只能获取单方向的上下文信息。而BERT利用MLM进行预训练并且采用深层的双向Transformer组件(单向的Transformer一般被称为Transformer decoder,其每一个token(符号)只会attend到目前往左的token。而双向的Transformer则被称为Transformer encoder,其每一个token会attend到所有的token)来构建整个模型,因此最终生成能融合左右上下文信息的深层双向语言表征。关于Transformer的详细解释可以参见Attention Is All You Need 或者 The Illustrated Transformer,经过多层Transformer结构的堆叠后,形成BERT的主体结构

    img

    5.1.2 模型输入

    BERT的输入为每一个token对应的表征(图中的粉红色块就是token,黄色块就是token对应的表征),并且单词字典是采用WordPiece算法来进行构建的。为了完成具体的分类任务,除了单词的token之外,作者还在输入的每一个序列开头都插入特定的分类token([CLS]),该分类token对应的最后一个Transformer层输出被用来起到聚集整个序列表征信息的作用。

    由于BERT是一个预训练模型,其必须要适应各种各样的自然语言任务,因此模型所输入的序列必须有能力包含一句话(文本情感分类,序列标注任务)或者两句话以上(文本摘要,自然语言推断,问答任务)。那么如何令模型有能力去分辨哪个范围是属于句子A,哪个范围是属于句子B呢?BERT采用了两种方法去解决:

    • 在序列tokens中把分割token([SEP])插入到每个句子后,以分开不同的句子tokens
    • 为每一个token表征都添加一个可学习的分割embedding来指示其属于句子A还是句子B

    上面提到了BERT的输入为每一个token对应的表征,实际上该表征是由三部分组成的,分别是对应的token分割位置 embeddings(位置embeddings的详细解释可参见Attention Is All You NeedThe Illustrated Transformer),如下图:

    preview

    5.1.3 模型输出

    C为分类token([CLS])对应最后一个Transformer的输出,T_i则代表其他token对应最后一个Transformer的输出。对于一些token级别的任务(如,序列标注和问答任务),就把T_i输入到额外的输出层中进行预测。对于一些句子级别的任务(如,自然语言推断和情感分类任务),就把C输入到额外的输出层中,这里也就解释了为什么要在每一个token序列前都要插入特定的分类token。

    5.1.4 Bert的预训练任务

    • Masked Language Model(MLM):是BERT能够不受单向语言模型所限制的原因。简单来说就是以15%的概率用[MASK]随机地对每一个训练序列中的token进行替换,然后预测出[MASK]位置原有的单词。然而,由于[MASK]并不会出现在下游任务的微调(fine-tuning)阶段,因此预训练阶段和微调阶段之间产生了不匹配。因此BERT采用了以下策略来解决这个问题:

      首先在每一个训练序列中以15%的概率随机地选中某个token位置用于预测,假如是第i个token被选中,则会被替换成以下三个token之一

      • 80%的时候是[MASK],如my dog is hairy——>my dog is [MASK]
      • 10%的时候是随机的其他token,如my dog is hairy——>my dog is apple
      • 10%的时候是原来的token,如my dog is hairy——>my dog is hairy
    • Next Sentence Prediction(NSP):MLM任务倾向于抽取token层次的表征,因此不能直接获取句子层次的表征。为了使模型能够有能力理解句子间的关系,BERT使用了NSP任务来预训练,简单来说就是预测两个句子是否连在一起。具体的做法是:对于每一个训练样例,我们在语料库中挑选出句子A和句子B来组成,50%的时候句子B就是句子A的下一句,剩下50%的时候句子B是语料库中的随机句子。接下来把训练样例输入到BERT模型中,用[CLS]对应的C信息去进行二分类的预测。

    5.2 Bert的优缺点

    5.2.1 BERT存在哪些优缺点?

    • 优点:

      • 能够获取上下文相关的双向特征表示
    • 缺点:

      • 生成任务表现不佳:预训练过程和生成过程的不一致,导致在生成任务上效果不佳
      • 采取独立性假设:没有考虑预测[MASK]之间的相关性,是对语言模型联合概率的有偏估计(不是密度估计)
      • 输入噪声[MASK],造成预训练-精调两阶段之间的差异
      • 无法文档级别的NLP任务,只适合于句子和段落级别的任务

    ALBERT:提出了两种参数缩减技术,以降低内存消耗并提高BERT的训练速度

    XLNet:XLnet是Transformer XL模型的一个扩展,该模型使用自回归方法预先训练,通过最大化输入序列分解顺序的所有排列的期望似然来学习双向上下文

    Roberta:建立在BERT的基础上,修改关键的超参数,删除next-sentence pretraining的预训练目标,并以更大的mini-batches和learning rates进行训练

    GPT2:超大规模语料上通过transformer结构进行的无监督训练的语言模型,适用于各项生成任务

    img

    相关文章

      网友评论

        本文标题:BERT使用手册

        本文链接:https://www.haomeiwen.com/subject/wmacaltx.html