【LLM】第三章:BERT讲解+情感分析案例

【LLM】第三章:BERT讲解+情感分析案例
【LLM】第三章BERT讲解情感分析案例本篇开讲当年叱诧风云、风光无二的BERT大模型。说明本文是假定你已经非常了解Transformer了也不会重复写和注意力机制、Transformer相关的内容了。想了解详情的自行查阅我的NLP专栏https://blog.csdn.net/friday1203/category_12833594.html?spm1001.2014.3001.5482一、BERT概述2018年10月Google发布一篇论文《BERT:Pre-training of Deep Bidirectional Transformers for Language Understanding》BERT模型横空出世并横扫NLP领域11项目任务的最佳成绩在BERT出现之前NLP领域占据统治地位的是LSTM和GRU模型但是BERT出世后基本就是NLP领域的标配了。其强大的自然语言表示能力被广泛应用于文本分类、序列标注比如命名实体识别、句子匹配等场景。BERT论文地址是https://arxiv.org/pdf/1810.04805.pdf 大家可以自行下载原论文阅读。在BERT中发挥重要作用的结构就是Transformer之后又出现XLNETroBERT等模型击败了BERT但是它们的核心没有变仍然是Transformer。二、BERT模型架构BERT(Bidirectional Encoder Representation from Transformers)采用的是Transformer的编码器结构而编码器是用来理解语义的生成词语的token表示的。所以BERT其实就是一个词向量化模型只是它因使用了注意力机制而具有更加强大的自然语言表示能力。同时BERT也是一个预训练模型也就是谷歌已经用海量语料把模型预训练完毕了发布了两个版本BERT-base和BERT-large具体参数规格如下右图我们以BERT-base版本为例来讲解。1、从HF上下载BERT-base预训练模型到本地在前一个篇章【LLM】第二章HuggingFace入门学习-CSDN博客 如何配置环境变量、下载的文件及其地址都有详细的说明和示例。这里不再重复。我把cache中下载的文件复制粘贴到项目文件夹中如下图所示2、从本地加载预训练模型BERT-base#这是加载本地BERT预训练模型tokenizer的代码 from transformers import AutoModel, AutoTokenizer model_name bert-base-chinese cache_dir rD:\2MyWork\MyFile\pytorch-deeplearning\大模型\hf-models\bert-model #带盘符的本地地址也是1中的位置 model_bert AutoModel.from_pretrained(model_name, cache_dircache_dir) model_bert tokenizer_bert AutoTokenizer.from_pretrained(model_name, cache_dircache_dir) tokenizer_bert3、从2中看BERT的整体架构可见bert架构和transformer的编码器相差不大只要对transformer非常熟悉上面的架构就没啥新鲜的。4、澄清什么是“双向自注意力结构”网上很多资料强调BERT的创新在于它是一个双向自注意力结构在训练每个token表示时BERT是整合了左右两个方向上的上下文信息从而获得更准确、更丰富的语义表示:了解注意力机制的同学就会秒懂这本身就是注意力机制的固有特点因为计算注意力分值时计算的就是整个序列中样本与样本之间的分值。也所以它具有长期记忆能力所以这个创新根本不是啥创新没啥稀奇的。上图是BERT论文中的图个人感觉有些不对下篇我们学的GPTGPT是由12个transformer中的解码器串联而成不是上面右图那样的神经元连接的结构同理BERT也是编码器串联而成的不是像神经元链接的这种方式如果是这种方式那得要多少个编码器此外一个TM就可以输出被喂入的整个序列中的所有token的结果不是上图那种的一个TM就输出序列中对应的token的结果5、BERT的输入部分BERT的每个输入token表示由三部分组成Token Embedding(词嵌入)词本身的embedding表示Segment Embedding(段落编码)用于区分句子对任务中的两个句子分别用一个可学习的向量表示。Position Embedding(位置编码)每个词的位置编码1加入了段落编码Segment Embedding就是给序列也进行了位置编码。2位置编码用的是可训练的位置编码Trainable Positional Embedding。3BERT输入中一般会包含两个特殊标识[CLS]: 句首标识一个序列的起始标志。而且这个标识的输出一般被视为其所在序列的整体输出所以这个标识的输出经常被用于下游的文本分类任务。因为BERT是双向自注意力所以将第一个token当成整个序列的输出也是可以的也就是它可以被看作是专门用于汇总整个序列的语义信息的。[SEP]: 句间分隔符出现在每个句子的末尾。因为有这个标识所以BERT既可以处理单句任务也可以处理多句任务非常灵活。4BERT-base支持的最大序列长度是512。也就是我们一次最多只能喂入模型512个token再长就不支持了需要截断短了没事加padding直到512即可。5BERT-base的词表是21128个token。6、BERT的输出部分BERT的输出有2部分一部分是编码层最后的输出层的输出。是所有输入的tokens(512个)的输出(768维)。另一部分是pooler层的输出。pooler层是承接[CLS]token的就是处理[CLS]token的输出的。所以你可以根据你自己的具体下游任务类型给BERT接入不同的任务输出头(1) Token-Level任务(如命名实体识别、序列标注任务)需要使用每个位置的输出表示所以每个位置的输出都要接入任务输出头。而任务输出头一般就是一个线性层该回归该分类视你的具体下游任务而定。(2) Seqence-Level任务(如文本分类、句子对分类)可以使用特殊token[CLS]的输出表示来接入任务输出头。此时[CLS]就被视作是汇总整个序列语义信息的token。小结通过BERT预训练模型我们可以得到每个token的语义表示微调时我们可以通过添加简单的任务特定层适配不同类型的下游任务。三、BERT的预训练和微调过程BERT是谷歌的一个开源项目我们从HF上下载下来的BERT是人家谷歌已经使用大量自然语言语料训练好了的一个自然语言领域的预训练模型。所以BERT是自然语言领域的一个通用模型、预训练模型或者说BERT就是一个自然语言处理的通用解决方案、通用的解决框架因为它可以解决大部分的NLP任务。当你拿着BERT让它学比如法律、金融等子领域的专业知识你只需整理好你的法律或者金融领域的特定语料然后训练很少的参数就是fine-tune(微调)一下这个模型就是一个专业领域的专业语言模型了。所以下面我们详细看看Bert是如何进行预训练的。当我们拿到预训练的bert后又是如何进行微调的。一BERT是如何进行预训练的1、BERT预训练阶段有两个任务任务1掩码语言模型(MLM, Masked Language Modeling)就是把训练句子随机遮住15%的token然后让模型根据上下文去预测这15%的token。这个任务是为了让模型学习词级语义。说明被遮盖的这15%的token是80%的被替换成特殊token[MASK]10%被替换成随机词10%保持原词不变。为什么不全部替换成[MASK]这么设计是为了增强模型的泛化性就是今后模型遇到的不是[MASK]这个特殊token即使是其他随机词token模型依然可以预测出这个位置的词。任务2下一句预测(Next Sentence Prediction,NSP)为了提升模型理解句子和句子之间的逻辑关系BERT在预训练时模型被喂入的是两个句子让模型判断第二句是否是第一句的真实后续句子。其中50%的训练样本是上下文中真实相邻的句子(正例) 50%是从语料中随机采样的非相邻句子(反例)。2、在预训练时BERT是同时优化MLM和NSP两个目标具体操作如下其实NSP其实后面接的就是pooler层pooler层是一个线性层tanh激活层用来承接[CLS]token的输出的而[CLS]token的输出可以看作是对所有输入的总结信息。所以可以用这个头来承接Seqence-Level任务。重点损失函数是上图的两个任务头的损失加和通过优化加和损失来训练模型的。二BERT是如何进行微调的在模型预训练完成后BERT就可以通过微调来适配多种下游任务了比如文本分类、句子匹配、问答系统、序列标注等。老套路微调时模型主体结构保持不变仅在顶部添加一个特定任务的输出头使用少量的、带标注的、专业领域的任务数据对整个模型进行微调训练。不同下游任务的差异主要体现在输出层的设计、以及从模型输出中提取哪些信息来标识预测结果。此外微调时的格式依旧要和BERT的要求保持一致比如也是要以[CLS]为序列头标识以[SEP]为句子的结尾标识。这都是基本常识。1、句子对儿分类任务比如判断两个句子之间是否存在重复、蕴含、矛盾等关系。要实现上面的任务我们可以到GLUE网站上下载对应的数据集可以让你的模型在这些数据集上跑跑效果。2、单句分类任务3、问答任务上图是抽取式问答还有生成式问答后面讲T5是生成式问答。我们平时和AI助手聊天的应用就是生成式问答。4、序列标注任务比如命名实体识别任务总之bert模型本质上是一个分类任务的模型然而由于各种花式分类让bert不仅可以分类还可以填空。四、示例下载bert到本地--加载bert--实现分类示例由于我们想实现分类任务而且不想写模型架构了所以这里展示一下如何直接调用带有任务头的bert-base模型#这是下载的带任务头的BERT-base预训练模型tokenizer的代码 from transformers import BertTokenizer, BertForSequenceClassification model_namebert-base-chinese model BertForSequenceClassification.from_pretrained(model_name) tokenizer BertTokenizer.from_pretrained(model_name) model tokenizer其实不用再下载一遍其实这里下载的文件和地址都和上面的一样都在cache路径下都是一样的文件只是BertForSequenceClassification这个类会帮我们自动添加一个二分类的任务头#把从本地加载BertForSequenceClassification from transformers import BertTokenizer, BertForSequenceClassification model_name bert-base-chinese model_dir rD:\2MyWork\MyFile\pytorch-deeplearning\大模型\hf-models\bert-model\models--bert-base-chinese\snapshots\8f23c25b06e129b6c986331a13d8d025a92cf0ea model BertForSequenceClassification.from_pretrained(model_dir) tokenizer BertTokenizer.from_pretrained(model_dir) model tokenizer意思就是预训练模型bert主体架构是已经被训练好了的模型但是最后的线性层(任务头)是没有被训练的需要训练才能推理。#pipeline是在线访问我们用pipoline推理一下试试访问成功与否取决于你的网络状况 from transformers import pipeline classifier pipeline(text-classification, modelmodel, tokenizertokenizer) result classifier(这是一首歌) result只是一个简单的在线实现的任务其实很多时候我们是需要本地部署、本地推理预测的此时我们不仅得把模型下载下来还得用我们自己的数据集对模型进行训练才能完成我们自己设置的任务比如写诗啥的。下面是用我们自己数据训练的情感分类任务五、实战基于本地的bert、本地的数据实现评论的情感分析任务说明本例用的数据是 【NLP】第八章项目实操案例文本情感分析-CSDN博客 这个例子中的数据。一只要BERT主体架构我们自己手动添加适配具体任务的线性层1、代码架构是2、各个模块的代码及运行结果#config.py模块的代码 from pathlib import Path ROOT_DIR Path(__file__).parent #d:\2MyWork\MyFile\pytorch-deeplearning\大模型\BERT实战-情感分析 RAW_DATA ROOT_DIR/data/raw PROCESSED_DATA ROOT_DIR/data/processed PRETRAINED_DIR ROOT_DIR/pretrained MODELS_DIR ROOT_DIR/models SEQ_LEN 128 BATCH_SIZE16 LEARNING_RATE1e-6 #预训练模型的微调由于只调最后一个层所以尽量把学习率设置小一点避免梯度不稳定。 EPOCHS1#process.py模块的代码 from datasets import load_dataset, ClassLabel from transformers import AutoTokenizer import config from tqdm import tqdm def process(): print(开始处理数据。。。。) #读取原始数据文件 raw_data load_dataset(csv, data_filesstr(config.RAW_DATA/online_shopping_10_cats.csv)) dataset1 raw_data[train] #raw_data是个字典所以要用key把文件索引出来 #数据清洗 dataset2 dataset1.remove_columns(cat) #删除列 dataset3 dataset2.filter(lambda x:x[review] is not None) #删除空行 #划分数据集 dataset4 dataset3.cast_column(label, ClassLabel(names[negative,positive])) #转字段类型 dataset_train_test dataset4.train_test_split(test_size0.2, stratify_by_columnlabel) print(dataset_train_test) #创建Tokenizer tokenizer AutoTokenizer.from_pretrained(config.PRETRAINED_DIR) #构建可以直接喂入模型的训练集和测试集 def batch_encode(batch): inputs tokenizer(batch[review], paddingmax_length, truncationTrue,max_lengthconfig.SEQ_LEN) inputs[labels] batch[label] return inputs dataset_ids dataset_train_test.map(batch_encode, batchedTrue, remove_columns[review, label]) #保存训练集和测试集 dataset_ids.save_to_disk(config.PROCESSED_DATA) print(数据处理完成。。。。) if __name__ __main__: process()#dataset.py模块的代码 import config from datasets import load_from_disk from torch.utils.data import Dataset, DataLoader def get_dataloader(trainTrue): path str(config.PROCESSED_DATA/(train if train else test)) dataset load_from_disk(path) dataset.set_format(typetorch) return DataLoader(dataset, batch_sizeconfig.BATCH_SIZE, shuffleTrue)#model.py模块的代码 import config from transformers import AutoModel import torch class Bert_Main(torch.nn.Module): #只用主模型下游任务的线性层自己写 def __init__(self, ): super().__init__() self.bert AutoModel.from_pretrained(config.PRETRAINED_DIR) self.linear torch.nn.Linear(self.bert.config.hidden_size, 1) def forward(self, input_ids, attention_mask, token_type_ids): output self.bert(input_ids, attention_mask, token_type_ids) last_hidden_state output.last_hidden_state cls_hidden_state last_hidden_state[:,0,:] yhat self.linear(cls_hidden_state).squeeze(-1) return yhat#train.py模块的代码 import torch from transformers import AutoTokenizer import config, model, dataset from tqdm import tqdm def train(): device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 train_data dataset.get_dataloader() #数据 bert_main_model model.Bert_Main().to(device) #模型 criterion torch.nn.BCEWithLogitsLoss() #损失函数 optimizer torch.optim.Adam(bert_main_model.parameters(), lrconfig.LEARNING_RATE) #优化器 #开始训练 bert_main_model.train() epoch_loss [] best_loss float(inf) for epoch in range(config.EPOCHS): print(*10, fEpoch: {epoch1}, *10) batch_loss0 for batch in tqdm(train_data, desc模型训练中。。。): inputs {k : v.to(device) for k,v in batch.items()} #把张量都迁移到GPU上 labels inputs.pop(labels).to(dtypetorch.float) #把标签拿出来 outputs bert_main_model(**inputs) #正向传播 把除标签外的数据以解引用的方式传给预训练模型 loss criterion(outputs, labels) #求损失 loss.backward() #反向传播 optimizer.step() #更新网络参数 optimizer.zero_grad() #情况梯度 batch_loss loss.item() epoch_loss_mean batch_loss/len(train_data) print(f本轮epoch的平均损失是 {epoch_loss_mean}) epoch_loss.append(epoch_loss_mean) if epoch_loss_mean best_loss: best_loss epoch_loss_mean torch.save(bert_main_model.state_dict(),config.MODELS_DIR/best_model.pt) print(模型保存成功。。。) if __name____main__: train()#predict.py模块的代码 import torch from transformers import AutoTokenizer import config, model def predict(text, tokenizer, Model, device): tokens tokenizer(text,paddingmax_length,truncationTrue,max_lengthconfig.SEQ_LEN,return_tensorspt) inputs {k : v.to(device) for k,v in tokens.items()} #把张量都迁移到GPU上 Model.eval() with torch.no_grad(): output Model(**inputs) yhat torch.sigmoid(output) predict_re f正向评价置信度{yhat} if yhat0.5 else f负向评价置信度{1-yhat} return predict_re def run_predict(): print(欢迎使用BERT模型进行情感分析(输入q或者quit退出)) device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 my_tokenizer AutoTokenizer.from_pretrained(config.PRETRAINED_DIR) #tokenizer bert_main_model model.Bert_Main().to(device) #加载模型 bert_main_model.load_state_dict(torch.load(config.MODELS_DIR/best_model.pt)) #加载参数 while True: user_input input(请输入 ) if user_input in [q, quit]: print(欢迎下次再来) break if user_input.strip() : print(请输入内容) continue predict_re predict(user_input, my_tokenizer, bert_main_model, device) print(predict_re) if __name____main__: run_predict()#evaluate.py模块的代码 import torch import config, model, dataset from tqdm import tqdm def run_evaluate(): device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 Model model.Bert_Main().to(device) #加载模型 Model.load_state_dict(torch.load(config.MODELS_DIR/best_model.pt)) #加载参数 print(模型加载成功) test_dataloader dataset.get_dataloader(trainFalse) #测试数据 total_sample0 predict_correct 0 Model.eval() with torch.no_grad(): for batch in tqdm(test_dataloader, desc模型测试中。。。): inputs {k : v.to(device) for k,v in batch.items()} #把张量都迁移到GPU上 labels inputs.pop(labels).tolist() output Model(**inputs) #正向传播 output torch.sigmoid(output) output output.tolist() result [0 if i0.5 else 1 for i in output] total_sample len(output) for yhat, y_true in zip(result, labels): if yhat y_true: predict_correct 1 print(f模型在测试集的效果准确率{predict_correct/total_sample:.4f}) if __name__ __main__: run_evaluate()二直接调用带任务头的BERT1、项目架构2、各个模块的代码及运行结果展示#config.py模块的代码 from pathlib import Path ROOT_DIR Path(__file__).parent #d:\2MyWork\MyFile\pytorch-deeplearning\大模型\BERT实战-情感分析 RAW_DATA ROOT_DIR/data/raw PROCESSED_DATA ROOT_DIR/data/processed PRETRAINED_DIR ROOT_DIR/pretrained MODELS_DIR ROOT_DIR/models SEQ_LEN 128 BATCH_SIZE16 LEARNING_RATE1e-6 #预训练模型的微调由于只调最后一个层所以尽量把学习率设置小一点避免梯度不稳定。 EPOCHS1#process.py模块的代码 from datasets import load_dataset, ClassLabel from transformers import AutoTokenizer import config from tqdm import tqdm def process(): print(开始处理数据。。。。) #读取原始数据文件 raw_data load_dataset(csv, data_filesstr(config.RAW_DATA/online_shopping_10_cats.csv)) dataset1 raw_data[train] #raw_data是个字典所以要用key把文件索引出来 #数据清洗 dataset2 dataset1.remove_columns(cat) #删除列 dataset3 dataset2.filter(lambda x:x[review] is not None) #删除空行 #划分数据集 dataset4 dataset3.cast_column(label, ClassLabel(names[negative,positive])) #转字段类型 dataset_train_test dataset4.train_test_split(test_size0.2, stratify_by_columnlabel) print(dataset_train_test) #创建Tokenizer tokenizer AutoTokenizer.from_pretrained(config.PRETRAINED_DIR) #构建可以直接喂入模型的训练集和测试集 def batch_encode(batch): inputs tokenizer(batch[review], paddingmax_length, truncationTrue,max_lengthconfig.SEQ_LEN) inputs[labels] batch[label] return inputs dataset_ids dataset_train_test.map(batch_encode, batchedTrue, remove_columns[review, label]) #保存训练集和测试集 dataset_ids.save_to_disk(config.PROCESSED_DATA) print(数据处理完成。。。。) if __name__ __main__: process()#dataset.py模块的代码 import config from datasets import load_from_disk from torch.utils.data import Dataset, DataLoader def get_dataloader(trainTrue): path str(config.PROCESSED_DATA/(train if train else test)) dataset load_from_disk(path) dataset.set_format(typetorch) return DataLoader(dataset, batch_sizeconfig.BATCH_SIZE, shuffleTrue)#model.py模块的代码---只要加载一下预训练模型即可 from transformers import AutoModelForSequenceClassification import config import torch device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 model_full AutoModelForSequenceClassification.from_pretrained(config.PRETRAINED_DIR).to(device) print(model_full)#train.py模块的代码 import torch from transformers import AutoModelForSequenceClassification import config, dataset from tqdm import tqdm def train(): device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 train_data dataset.get_dataloader() #数据 model_full AutoModelForSequenceClassification.from_pretrained(config.PRETRAINED_DIR).to(device) #模型 optimizer torch.optim.Adam(model_full.parameters(), lrconfig.LEARNING_RATE) #优化器 #开始训练 model_full.train() epoch_loss [] best_loss float(inf) for epoch in range(config.EPOCHS): print(*10, fEpoch: {epoch1}, *10) batch_loss0 for batch in tqdm(train_data, desc模型训练中。。。): inputs {k : v.to(device) for k,v in batch.items()} #把张量都迁移到GPU上 outputs model_full(**inputs) #正向传播 把除标签外的数据以解引用的方式传给预训练模型 loss outputs.loss loss.backward() #反向传播 optimizer.step() #更新网络参数 optimizer.zero_grad() #情况梯度 batch_loss outputs.loss.item() epoch_loss_mean batch_loss/len(train_data) print(f本轮epoch的平均损失是 {epoch_loss_mean}) epoch_loss.append(epoch_loss_mean) if epoch_loss_mean best_loss: best_loss epoch_loss_mean model_full.save_pretrained(config.MODELS_DIR) #####保存模型的api##### print(模型保存成功。。。) if __name____main__: train()#predict.py模块的代码 import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer import config def predict(text, tokenizer, Model, device): tokens tokenizer(text,paddingmax_length,truncationTrue,max_lengthconfig.SEQ_LEN,return_tensorspt) inputs {k : v.to(device) for k,v in tokens.items()} #把张量都迁移到GPU上 Model.eval() with torch.no_grad(): output Model(**inputs) yhat torch.sigmoid(output.logits) ####用.logits取出结果 predict_re f负向评价置信度{yhat[0][0]} if yhat[0][0]yhat[0][1] else f正向评价置信度{yhat[0][1]} return predict_re def run_predict(): print(欢迎使用BERT模型进行情感分析(输入q或者quit退出)) device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 my_tokenizer AutoTokenizer.from_pretrained(config.PRETRAINED_DIR) #tokenizer model_full AutoModelForSequenceClassification.from_pretrained(config.MODELS_DIR).to(device) #加载模型 while True: user_input input(请输入 ) if user_input in [q, quit]: print(欢迎下次再来) break if user_input.strip() : print(请输入内容) continue predict_re predict(user_input, my_tokenizer, model_full, device) print(predict_re) if __name____main__: run_predict()#evaluate.py模块的代码 import torch import config, dataset from transformers import AutoModelForSequenceClassification from tqdm import tqdm def run_evaluate(): device torch.device(cuda if torch.cuda.is_available() else cpu) #设备 model_full AutoModelForSequenceClassification.from_pretrained(config.MODELS_DIR).to(device) #加载模型 print(模型加载成功) test_dataloader dataset.get_dataloader(trainFalse) #测试数据 total_sample0 predict_correct 0 model_full.eval() with torch.no_grad(): for batch in tqdm(test_dataloader, desc模型测试中。。。): inputs {k : v.to(device) for k,v in batch.items()} #把张量都迁移到GPU上 labels inputs.pop(labels).tolist() output model_full(**inputs) #正向传播 output torch.sigmoid(output.logits) output output.tolist() result [0 if i[0]i[1] else 1 for i in output] total_sample len(output) for yhat, y_true in zip(result, labels): if yhat y_true: predict_correct 1 print(f模型在测试集的效果准确率{predict_correct/total_sample:.4f}) if __name__ __main__: run_evaluate()这个结果和我们自己手动写最后线性层的结果一模一样。

最新新闻

日新闻

周新闻

月新闻