Hugging Face 中文预训练模型使用介绍及情感分析项目实战

Hugging Face
一直致力于自然语言处理NLP技术的平民化(democratize),希望每个人都能用上最先进(SOTA, state-of-the-art)的NLP技术,而非困窘于训练资源的匮乏"
在这里插入图片描述

其中,transformer库提供了NLP领域大量预训练语言模型和调用框架,方便根据自己的需求快速进行语言模型搭建,这里为自己学习做下简单总结(参考官方教程)。

目录

  1. Transformer整体介绍

    1. 语言模型与自然语言处理
      1. 语言模型
      2. 自然语言处理常见任务
      3. 其他非文本任务
    2. 经典模型处理能力(句子序列长度)
    3. transformer库piepeline接口介绍
  2. tokenizer和model介绍

    1. tokenizer介绍
    2. model介绍
  3. Transformer常用model架构(带head)接口输出

    1. AutoModel架构的输出(通用类,不带head部分)
    2. AutoModelForMaskedLM 架构的输出
    3. AutoModelForSequenceClassification架构的输出
    4. AutoModelForTokenClassification架构的输出
    5. 模型输出logits概率处理
  4. 微调预训练模型实战-情感分析

    • 数据集预处理
    • 模型搭建(预训练模型加载)
    • 数据集编码和数据加载器生成
    • 微调训练
    • 模型保存

正文

  1. Transformer整体介绍

    1. 语言模型与自然语言处理

      1. 语言模型:简易理解就是求概率P(tm|t1、t2、…、tn)

      2. 自然语言处理常见任务

        • Classifying whole sentences: 情感分析,检测垃圾邮件,判断句子语法上是否正确或两个句子逻辑上是否有关联。
        • Classifying each word in a sentences: 识别句子的语法成分(动名词、形容词)或NER命名实体识别(人名,地名,组织)。
        • Generating text content: 文本自动生成,或文本填空。
        • Extracting an answer from a text : 问答系统,给定问题和上下文,根据上下文信息提取问题的答案。
        • Generating a new sentence from an input text: 文本摘要、文本翻译。
      3. 其他非文本任务

        图像:

        • 图像分类:对图像进行分类。

        • 图像分割:对图像中的每个像素进行分类。

        • 物体检测:检测图像中的物体。

          音频:

        • 音频分类:为给定的音频片段分配标签。

        • 自动语音识别 (ASR):将音频数据转录为文本。

    2. 经典模型处理能力(句子序列长度)

      • RNN 10-40
      • LSTM/GRU 50-100
      • Transformer BERT 521 GPT 1024
      • Transformer-XL LXNet 3000-5000
    3. transformer库piepeline接口介绍

      ​ Transformers library 中最基本对象是pipeline,它将数据预处理、预训练语言模型和目标结果处理步骤串联起来,使我们能够直接输入文本并获得自己需要的答案。

      Pipeline结构示意图:
      在这里插入图片描述

      pipeline的简单使用:

      import torch
      from transformers import pipeline
      
      # 哈工大中文预训练模型bert-roberta-wwm (已下载到本地)
      model_path = r"D:\appData\bert-wwm-ext\chinese-roberta-wwm-ext"
      
      # 测试中文填词任务
      fill_mask_model = pipeline(task='fill-mask', # 填写要做的任务类型,确定POST-processing
                                 model=model_path, # 处理任务要使用的预训练语言模型
                                 tokenizer=model_path # 输入数据预处理,需要的分词编码器(与要使用的语言模型相关)
                                )
      
      
      fill_mask_model(['我[MASK]你'])
      # 返回
      '''
      [{'score': 0.8178807497024536,
        'token': 4263,
        'token_str': '爱',
        'sequence': '我 爱 你'},
       {'score': 0.05486121028661728,
        'token': 2682,
        'token_str': '想',
        'sequence': '我 想 你'},
       {'score': 0.02797825261950493,
        'token': 3221,
        'token_str': '是',
        'sequence': '我 是 你'},
       {'score': 0.019031845033168793,
        'token': 1469,
        'token_str': '和',
        'sequence': '我 和 你'},
       {'score': 0.01068081520497799,
        'token': 3612,
        'token_str': '欠',
        'sequence': '我 欠 你'}]
      '''
      

      ​ 默认情况下,给定特定任务,pipeline会自动选择一个特定的预训练模型。

      ​ 将文本输入pipeline,pipeline的处理涉及3个步骤,对应上面的pipeline结构图

      • tokenizer: 文本预处理为对应model可以理解的格式

      • model: 预处理模型输入对应model

      • post-processing: 将model的输入经过全连接/激活,处理成任务需要输出的形式

      ​ 至此,只需要修改参数 task名+匹配的预训练语言模型,就能简单实现自然语言处理任务(同样也可以使用pipeline实现其他如图像、音频分类等任务)。

  1. tokenizer和model介绍

    ​ 通过pipeline接口,我们能实现特征的任务,但越简单的接口,往往伴随着缺乏灵活性的问题。一般情况下,不直接使用pipeline,使用其中tokenizer和model,配合模型后续处理生成适合自己任务的模型。

    1. tokenizer介绍

      from transformers import BertModel,BertTokenizer,BertConfig
      
      # 主要加载预训练模型词汇表,分词规则等
      tokenizer = BertTokenizer.from_pretrained(model_path)
      
      #分词器序列编码,解码
      data_encode = tokenizer(
          text = ['我 爱 你','我恨你','天空'], #  输入多条分本
          text_pair = None,
          add_special_tokens = True, # 
          padding = 'max_length', # 填充长度不够的序列
          truncation = True, # 过长序列截断
          max_length = 5, # 指定序列长度
          stride = 0,
          is_split_into_words = False, # text或text_pair参数输入字符串列表时,起作用,指定序列是否分批
          pad_to_multiple_of = None,
          
          # 指定数据处理后的返回
          return_tensors = 'pt',# 指定返回数据类型,pt:pytorch np:数组 'tf':tensorflow 不指定返回list
          return_token_type_ids = True, # 指定上下句子关系,上句为0,下句为1
          return_attention_mask = True, # 返回句子序列实际长度,1代表真实token,0代表填充
          return_overflowing_tokens = False,
          return_special_tokens_mask = True,# 指定特殊字符位置,1代表起始符[CLS],分隔符[sep],填充符[PAD]等,0代表正常token
          return_offsets_mapping = False,
          return_length = True,# 返回序列长度
          verbose = True
          )
      
      
      data_encode
      '''
      {'input_ids': tensor([[ 101, 2769, 4263,  872,  102],
                            [ 101, 2769, 2616,  872,  102],
                            [ 101, 1921, 4958,  102,    0]]), 
       'token_type_ids': tensor([[0, 0, 0, 0, 0],
                                 [0, 0, 0, 0, 0],
                                 [0, 0, 0, 0, 0]]), 
       'special_tokens_mask': tensor([[1, 0, 0, 0, 1],
                                      [1, 0, 0, 0, 1],
                                      [1, 0, 0, 1, 1]]), 
       'length': tensor([5, 5, 4]), 
       'attention_mask': tensor([[1, 1, 1, 1, 1],
                                 [1, 1, 1, 1, 1],
                                 [1, 1, 1, 1, 0]])}
      '''
      
      
      tokenizer.decode(token_ids=data_encode['input_ids'][0]) 
      
      # 字符解码 >> [CLS] 我 爱 你 [SEP] [PAD]
      
    2. model介绍

      # 加载预训练bert中文语言模型(主要是预训练参数)
      model = BertModel.from_pretrained(pretrained_model_name_or_path=model_path)
      
      bert_out = model.forward(
          input_ids = data_encode.input_ids,     # 对应tokener输出字符序列编码
          attention_mask = data_encode.attention_mask,
          token_type_ids = data_encode.token_type_ids,
          position_ids = None,
          head_mask = None,
          inputs_embeds = None,
          encoder_hidden_states = None,
          encoder_attention_mask = None,
          past_key_values = None,
          use_cache = None,
          output_attentions = None,
          output_hidden_states = True, # 是否返回
          return_dict = None
          )
      
      # bert_out.hidden_states # 返回每个block的隐藏层输出
      bert_out.last_hidden_state.shape # 返回bert模型最后一个block隐藏层输出
      bert_out.pooler_output.shape # 返回[CLS]标记处对应的向量后面接个全连接再接tanh激活后的输出
      
      '''
      torch.Size([3, 5, 768]) (batch,sequence_len,word_embedding_szie)
      torch.Size([3, 768]) (batch,word_embedding_szie)
      '''
      

      ​ transformers 中有tokenizer和model类还有很多其他的函数功能,这里只是bert预训练中文语言模型的使用,可以在model(经过tokenizer处理后分词数据输入)的带有语义的隐藏层输出后面,灵活添加后续处理层,实现自己特定的任务。

  2. Transformer常用model架构(带head)接口输出

    ​ transformer库有很多带有不同预设输出的model架构,针对预训练模型输出结果后续处理的层,称之为head层。head将transformer network(多头注意力机制)最终隐藏层输出的高维特征向量作为输入,并将他们投影到不同的维度上,head通常由一个或几个线性层组成。
    在这里插入图片描述

    1. AutoModel架构的输出(通用类,不带head部分)

      ​ In many cases, the architecture you want to use can be guessed from the name or the path of the pretrained model you are supplying to the from_pretrained() method. AutoClasses are here to do this job for you so that you automatically retrieve the relevant model given the name/path to the pretrained weights/config/vocabulary. – 官方原文

      大概意思 AutoModel是个通用类,能从输入的预训练模型名称中或预训练模型存储路径中,自动生成相应模型的architecture(架构)并自动加载checkpoint(可以理解为对应层的权重参数)

      – BertModel 专用于加载BERT架构模型的基础类。

      from transformers import (
          AutoTokenizer,
          AutoModel,
          AutoConfig,
          AutoModelForMaskedLM,
          AutoModelForSequenceClassification,
          AutoModelForTokenClassification)
      
      # 记载预训练模型
      auto_model = AutoModel.from_pretrained(model_path)
      bert_model = BertModel.from_pretrained(model_path)
      tokenizer = AutoTokenizer.from_pretrained(model_path)
      
      # 验证AutoModel和BertModel加载同一个模型输出结果
      data = tokenizer(text=['我爱你'],return_tensors='pt')
      auto_model(data.input_ids).last_hidden_state
      bert_model(data.input_ids).last_hidden_state
      
      '''
      tensor([[[ 0.0963,  0.0306, -0.0745,  ..., -0.6741,  0.0229, -0.3951],
               [ 0.8193, -0.4155,  0.4848,  ..., -0.7079,  0.5251, -0.2706],
               [ 0.2164,  0.2476, -0.2831,  ..., -0.1583,  0.1191, -1.0851],
               [ 0.8606, -0.5950,  0.8120,  ..., -0.4619,  0.1180, -0.4674],
               [ 0.0963,  0.0306, -0.0745,  ..., -0.6741,  0.0229, -0.3951]]],
             grad_fn=<NativeLayerNormBackward>)
             
      tensor([[[ 0.0963,  0.0306, -0.0745,  ..., -0.6741,  0.0229, -0.3951],
               [ 0.8193, -0.4155,  0.4848,  ..., -0.7079,  0.5251, -0.2706],
               [ 0.2164,  0.2476, -0.2831,  ..., -0.1583,  0.1191, -1.0851],
               [ 0.8606, -0.5950,  0.8120,  ..., -0.4619,  0.1180, -0.4674],
               [ 0.0963,  0.0306, -0.0745,  ..., -0.6741,  0.0229, -0.3951]]],
             grad_fn=<NativeLayerNormBackward>)
      '''
      
      

      AutoModel类输出:

      1. 输出为BaseModelOutputWithPoolingAndCrossAttentions。
      2. 包含’last_hidden_state’和’pooler_output’两个元素。
      3. 'last_hidden_state’的形状是(batch size,sequence length,768)
      data = tokenizer(text=['我爱你'],return_tensors='pt')
      model = AutoModel.from_pretrained(model_path)
      outputs = model(**data)
      
      outputs.keys()
      outputs.last_hidden_state.shape # 最后一层隐藏层输出
      outputs.pooler_output.shape # pooler output是取[CLS]标记处对应的向量后面接个全连接再接tanh激活后的输出
      
      '''
      odict_keys(['last_hidden_state', 'pooler_output'])
      torch.Size([1, 5, 768]) (batch,sequence_len,word_embedding_szie)
      torch.Size([1, 768]) (batch,word_embedding_szie)
      '''
      
    2. AutoModelForMaskedLM 架构的输出(with a masked language modeling head,可用于文本填空任务)

      AutoModelForMaskedLM输出:

      1. 输出为MaskedLMOutput
      2. 包含’logits’元素,形状为[batch size,sequence length,21128],21128是’vocab_size’。
      model = AutoModelForMaskedLM.from_pretrained(model_path)
      outputs = model(**data)
      outputs.keys()
      outputs.logits.shape
      
      '''
      odict_keys(['logits'])
      torch.Size([1, 5, 21128])
      '''
      
    3. AutoModelForSequenceClassification架构的输出(with a sequence classification head,文本分类型任务)

      data = tokenizer(text=['我爱你'],return_tensors='pt')
      model = AutoModelForSequenceClassification.from_pretrained(model_path)
      outputs = model(**data)
      
      outputs.keys()
      outputs.logits.shape
      
      '''
      odict_keys(['logits'])
      torch.Size([1, 2])
      '''
      
    4. AutoModelForTokenClassification架构的输出(with a token classification head,命名实体识别任务)

      AutoModelForTokenClassification输出:

      1. 输出为TokenClassifierOutput
      2. 包含’logits’元素,形状为[batch size,sequence length,2]。
      model = AutoModelForTokenClassification.from_pretrained(model_path)
      outputs = model(**data)
      
      outputs.keys()
      outputs.logits.shape
      
      
      '''
      odict_keys(['logits'])
      torch.Size([1, 5, 2])
      '''
      
    5. 模型输出logits概率处理

      ​ logits,即模型最后一层输出的原始的、非标准化的分数。要转换为概率,它们需要经过softmax(所有🤗transformers模型都会输出logits,因为用于训练的损失函数通常会将最后一个激活函数(如SoftMax)与实际损失函数(如交叉熵)融合在一起)

      import torch
      
      # 输出结果为可识别的概率分数
      predict_proab = torch.nn.functional.softmax(outputs.logits, dim=-1)
      
  3. 微调预训练模型实战-情感分析

    • 数据集预处理(大众点评数据集地址https://www.heywhale.com/mw/dataset/5ecdf41c12fba90036cf11fc/file)

      import pandas as pd
      import random
      
      # 读取大众点评评论数据,这里只读取一个店铺的在线评论,共13964条
      data = pd.read_csv('../data/data.csv').query('shopID == 518986')[['cus_comment','stars','comment_len']]
      # 删除评论为空数据
      data.dropna(subset='cus_comment',inplace=True)
      data.info()
      
      '''
      Data columns (total 3 columns):
       #   Column       Non-Null Count  Dtype  
      ---  ------       --------------  -----  
       0   cus_comment  13964 non-null  object 
       1   stars        11417 non-null  float64
       2   comment_len  13964 non-null  int64  
      dtypes: float64(1), int64(1), object(1)
      '''
      
      # 数据拆分为训练集、验证集和测试集后保存
      # 取出评论中评价星数非空的当训练数据,共 11417条
      data_ = data.loc[data['stars'].notnull(),:].copy()
      
      # 设定评价3星以上算好评,3星一下算差评,3星中评
      def gen_label(x):
          if x > 3: return 2
          elif x == 3: return 1
          else: return 0
      data_.loc[:,'stars'] = data_['stars'].apply(gen_label)
      
      
      # 按8:2拆分数据集重新生成训练集和验证集
      data_slice = list(range(data_.shape[0]))
      random.shuffle(data_slice)
      slice_train,slice_valid = data_slice[:int(data_.shape[0]*0.8)],data_slice[int(data_.shape[0]*0.8):]
      
      data_train = data_.iloc[slice_train,:] # iloc取数据下标取,不是index标号
      data_valid = data_.iloc[slice_valid,:]
      
      # 取出评论中评价星数为空的当作测试集,共 5636条
      data_test = data.loc[data['stars'].isnull(),:].copy()
      
      # 数据保存
      data_train.to_csv('../data/dataset/data_train.csv',index=False)
      data_valid.to_csv('../data/dataset/data_valid.csv',index=False)
      data_test.to_csv('../data/dataset/data_test.csv',index=False)
      
      data_test.head(3)
      
      
      '''
      cus_comment                             	stars	comment_len
      出差 到 广州 朋友 说 这家 甜品店 是 老字号 广州 人 	NaN	   242
      如果 不是 找 不到 地方 吃 东西 我 一般 是 不会 	 NaN	 83
      他家 的 甜品 真系 老字号 非常 的 好吃 喜欢 红      NaN	 73
      '''
      

      将数据处理为3个csv文件,放到指定文件夹内。

    在这里插入图片描述

    • 模型搭建(加载预训练模型)

      import pandas as pd
      import random
      import matplotlib.pyplot as plt
      import datasets
      
      import torch
      import torch.nn as nn
      from torch.utils.data import DataLoader
      from torch.optim import Adam
      
      from transformers import BertModel,BertTokenizer,BertForSequenceClassification
      from transformers import DataCollatorWithPadding
      
      
      # 设置参数
      batch_size = 128 # 训练批次
      epochs = 20 # 训练轮次
      device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
      
      # 加载预训练模型
      # 哈工大中文预训练模型bert-roberta-wwm
      model_path = r"../input/chineserobertawwmext/chinese-roberta-wwm-ext"
      
      # 加载预训练分词器和预训练模型参数
      model = BertForSequenceClassification.from_pretrained(pretrained_model_name_or_path=model_path,num_labels=3)
      #model = BertModel.from_pretrained(pretrained_model_name_or_path=model_path,num_labels=3)
      tokenizer = BertTokenizer.from_pretrained(model_path)
      
      
    • 数据编码和数据加载器生成

      ​ dataset模型load_dataset函数能自动读取指定路径并加载文件,区分测试训练集,通过dataset.map(func)函数,对数据进行相关编码处理,最后再形成数据加载器时,按每批次文本最大长度填充文本,无需按所有样本数据最大长度填充,节省内存并加快训练速度。

      # 加载数据集并数据编码
      raw_dataset = datasets.load_dataset(path='../input/dataset/dataset')
      
      raw_dataset
      
      # 记载元数据集
      '''
      DatasetDict({
          train: Dataset({
              features: ['cus_comment', 'stars', 'comment_len'],
              num_rows: 2987
          })
          test: Dataset({
              features: ['cus_comment', 'stars', 'comment_len'],
              num_rows: 506
          })
          validation: Dataset({
              features: ['cus_comment', 'stars', 'comment_len'],
              num_rows: 747
          })
      })
      '''
      
      # 数据编码
      def tokenize_func(data):
          return tokenizer(text=data['cus_comment'],truncation=True)
      
      tokenized_dataset = raw_dataset.map(function=tokenize_func,
                                          batched=True,
                                          batch_size=500, # 每次按多少数据进行数据编码
                                          num_proc=None)
      
      
      tokenized_dataset = tokenized_dataset.remove_columns(['cus_comment','comment_len']) # 删除原始中文评论及其长度列
      tokenized_dataset = tokenized_dataset.rename_columns({'stars':'labels'}) # 将评价星级列名重命名为'label'
      tokenized_dataset.set_format(type='torch')
      
      
      
      
      # 形成批数据,并按批次内最大长度,统一填充数据,
      #只需保证每一批内数据长度一致,无需按所有数据最最大长度填充,节省内存并加快训练速度
      
      # 数据合并函数,按批次最大长度填充
      data_collator = DataCollatorWithPadding(
                                             tokenizer = tokenizer,
                                             padding = True,
                                             max_length = None, #默认为None,按批次最大序列长度填充
                                             pad_to_multiple_of = None,
                                             return_tensors = 'pt')
      
      train_dataloader = DataLoader(dataset=tokenized_dataset['train'],batch_size=batch_size,shuffle=True,collate_fn=data_collator)
      valid_dataloader = DataLoader(dataset=tokenized_dataset['validation'],batch_size=batch_size,shuffle=True,collate_fn=data_collator)
      
      '''
      DatasetDict({
          train: Dataset({
              features: ['labels', 'input_ids', 'token_type_ids', 'attention_mask'],
              num_rows: 2987
          })
          test: Dataset({
              features: ['labels', 'input_ids', 'token_type_ids', 'attention_mask'],
              num_rows: 506
          })
          validation: Dataset({
              features: ['labels', 'input_ids', 'token_type_ids', 'attention_mask'],
              num_rows: 747
          })
      })
      '''
      
    • 微调训练

      # 设置损失函数和优化器
      model.to(device)
      optim = Adam(params=model.parameters(),lr=2e-6)
      
      # 训练
      valid_loss = []
      train_loss = []
      
      for epoch in range(epochs):
          model.train()
          batch_train_loss = []
          for batch_data in train_dataloader:
              batch_data = {k:v.to(device) for k,v in batch_data.items()}
              outputs = model.forward(input_ids=batch_data['input_ids'],
                                      attention_mask=batch_data['attention_mask'],
                                      labels=batch_data['labels'] # 数据标签用于计算损失,如果`config.num_labels == 1`,使用MSE计算损失,如果 `config.num_labels > 1`则使用Cross-Entropy
                                     )
              loss = outputs.loss # 获取损失
              batch_train_loss.append(loss.item())
              
              optim.zero_grad()
              loss.backward()
              optim.step()
          
          batch_train_mean_loss = sum(batch_train_loss)/len(batch_train_loss)
          train_loss.append( batch_train_mean_loss)
          print(f'训练第{epoch}轮:测试集损失:{batch_train_mean_loss}')
          
          model.eval()
          batch_valid_loss = []
          for batch_data in valid_dataloader:
              batch_data = {k:v.to(device) for k,v in batch_data.items()}
              with torch.no_grad():
                  outputs = model.forward(input_ids=batch_data['input_ids'],
                                          attention_mask=batch_data['attention_mask'],
                                          labels=batch_data['labels'] # 数据标签用于计算损失,如果`config.num_labels == 1`,使用MSE计算损失,如果 `config.num_labels > 1`则使用Cross-Entropy
                                           )
                  
              batch_valid_loss.append(outputs.loss.item())
          batch_valid_mean_loss = sum(batch_valid_loss)/len(batch_valid_loss)
          valid_loss.append( batch_valid_mean_loss)
          print(f'训练第{epoch}轮:验证集损失:{batch_valid_mean_loss}')
      
          
      '''
      训练第0轮:测试集损失:0.7641913120945295
      训练第0轮:验证集损失:0.6475795308748881
      训练第1轮:测试集损失:0.6255298803249995
      训练第1轮:验证集损失:0.5978605995575587
      训练第2轮:测试集损失:0.6003468309839567
      训练第2轮:验证集损失:0.5879005044698715
      训练第3轮:测试集损失:0.5734179131686687
      训练第3轮:验证集损失:0.5717155039310455
      训练第4轮:测试集损失:0.5593642219901085
               ......
      '''    
        
      
    • 模型保存

      # 模型及其训练结果保存
      torch.save(model.state_dict(),'./model_pramas.pkl')
      torch.save(valid_loss,'./valid_loss.pkl')
      torch.save(train_loss,'./train_loss.pkl')
      
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐