Hugging Face 中文预训练模型使用介绍及情感分析项目实战
Hugging Face
一直致力于自然语言处理NLP技术的平民化(democratize),希望每个人都能用上最先进(SOTA, state-of-the-art)的NLP技术,而非困窘于训练资源的匮乏"
其中,transformer库提供了NLP领域大量预训练语言模型和调用框架,方便根据自己的需求快速进行语言模型搭建,这里为自己学习做下简单总结(参考官方教程)。
目录
-
- 语言模型与自然语言处理
- 语言模型
- 自然语言处理常见任务
- 其他非文本任务
- 经典模型处理能力(句子序列长度)
- transformer库piepeline接口介绍
- 语言模型与自然语言处理
-
- tokenizer介绍
- model介绍
-
Transformer常用model架构(带head)接口输出
- AutoModel架构的输出(通用类,不带head部分)
- AutoModelForMaskedLM 架构的输出
- AutoModelForSequenceClassification架构的输出
- AutoModelForTokenClassification架构的输出
- 模型输出logits概率处理
-
- 数据集预处理
- 模型搭建(预训练模型加载)
- 数据集编码和数据加载器生成
- 微调训练
- 模型保存
正文
-
Transformer整体介绍
-
语言模型与自然语言处理
-
语言模型:简易理解就是求概率P(tm|t1、t2、…、tn)
-
自然语言处理常见任务
- 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: 文本摘要、文本翻译。
-
其他非文本任务:
图像:
-
图像分类:对图像进行分类。
-
图像分割:对图像中的每个像素进行分类。
-
物体检测:检测图像中的物体。
音频:
-
音频分类:为给定的音频片段分配标签。
-
自动语音识别 (ASR):将音频数据转录为文本。
-
-
-
经典模型处理能力(句子序列长度)
- RNN 10-40
- LSTM/GRU 50-100
- Transformer BERT 521 GPT 1024
- Transformer-XL LXNet 3000-5000
-
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实现其他如图像、音频分类等任务)。
-
-
-
tokenizer和model介绍
通过pipeline接口,我们能实现特征的任务,但越简单的接口,往往伴随着缺乏灵活性的问题。一般情况下,不直接使用pipeline,使用其中tokenizer和model,配合模型后续处理生成适合自己任务的模型。
-
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]
-
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处理后分词数据输入)的带有语义的隐藏层输出后面,灵活添加后续处理层,实现自己特定的任务。
-
-
Transformer常用model架构(带head)接口输出
transformer库有很多带有不同预设输出的model架构,针对预训练模型输出结果后续处理的层,称之为head层。head将transformer network(多头注意力机制)最终隐藏层输出的高维特征向量作为输入,并将他们投影到不同的维度上,head通常由一个或几个线性层组成。
-
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类输出:
- 输出为BaseModelOutputWithPoolingAndCrossAttentions。
- 包含’last_hidden_state’和’pooler_output’两个元素。
- '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) '''
-
AutoModelForMaskedLM 架构的输出(with a masked language modeling head,可用于文本填空任务)
AutoModelForMaskedLM输出:
- 输出为MaskedLMOutput
- 包含’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]) '''
-
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]) '''
-
AutoModelForTokenClassification架构的输出(with a token classification head,命名实体识别任务)
AutoModelForTokenClassification输出:
- 输出为TokenClassifierOutput
- 包含’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]) '''
-
模型输出logits概率处理
logits,即模型最后一层输出的原始的、非标准化的分数。要转换为概率,它们需要经过softmax(所有🤗transformers模型都会输出logits,因为用于训练的损失函数通常会将最后一个激活函数(如SoftMax)与实际损失函数(如交叉熵)融合在一起)
import torch # 输出结果为可识别的概率分数 predict_proab = torch.nn.functional.softmax(outputs.logits, dim=-1)
-
-
微调预训练模型实战-情感分析
-
数据集预处理(大众点评数据集地址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')
-