使用微调BERT进行情感分类任务
BERT模型原理及其在NLP中的优势

1

预训练语言模型
BERT (Bidirectional Encoder Representations from Transformers) 是一种预训练的深度双向 Transformer 模型,由Google于2018年发布。它是一种基于大规模未标记文本数据进行自监督学习的方法,通过在大规模文本语料上进行大量训练,使得模型能够学习到词汇的上下文相关性,从而获得更好的文本表示。

2

迁移学习能力
BERT预训练模型可以在各种下游NLP任务上进行fine-tuning,快速获得出色的性能,这种迁移学习能力使得BERT在情感分析等任务上表现优异。

3

多任务学习
BERT模型可以在单个模型中同时学习多个NLP任务,如文本分类、问答、命名实体识别等,这种多任务学习使得BERT具有更强大的泛化能力。
任务定义和算法选型
情感分类是一个经典的NLP任务,常见于评论,推荐等实际应用。
Bert在NLP任务中易于迁移学习,通过在特定数据集上微调,可以使模型获得在特定任务下的能力。与常规统计算法不同的是,Bert的预训练Transformer结构encoder能够使其理解上下文信息,而不是仅限于小规模文本的统计概率计算。
Bert模型已经是一个成熟的NLP任务方案,在HuggingFace的transformers库中可用。本次实验使用BertForSequenceClassification,这是Bert在序列分类任务中的一个子类。
本次任务过程用到的库包含
import torch from transformers import AutoTokenizer,AutoModel, AdamW,BertForSequenceClassification from torch.utils.data import DataLoader, Dataset, random_split import pandas as pd from tqdm import tqdm import random from transformers import BertForSequenceClassification
模型结构(BertForSequenceClassification)
BERT模型用于序列分类任务(Sequence Classification)的结构,通常用于情感分析、文本分类等任务。下面对模型结构进行详细解释:
  1. BertForSequenceClassification:这是一个Bert模型的子类,专门用于序列分类任务。它包含了一个Bert模型和一个线性分类器。
  1. BertModel:BERT模型的主体部分,由嵌入层(Embeddings)、编码器(Encoder)和池化层(Pooler)组成。
  • Embeddings:负责将输入序列中的token转换为向量表示,包括词嵌入(word embeddings)、位置嵌入(position embeddings)和分段嵌入(token type embeddings)等。
  • Encoder:由多个BertLayer组成,每个BertLayer包含一个自注意力机制(BertAttention)、前馈神经网络(BertIntermediate)和层归一化(LayerNorm)等模块。
  • BertAttention:通过自注意力机制来捕捉输入序列中的全局依赖关系,包括查询(query)、键(key)、值(value)等操作。
  • BertIntermediate:负责将注意力机制的输出映射到更高维度的表示空间。
  • Pooler:通过对最后一个隐藏层的输出进行池化操作,得到整个序列的固定长度表示。
  1. dropout:用于模型训练过程中的随机失活,防止过拟合。
  1. classifier:一个线性分类器,将Bert模型的输出特征映射到最终的类别数量上。输出层的输出特征维度为768,输出类别数量为2,对应情感分析任务中的两个类别(如正面和负面)。
Tokenizer
在BERT(Bidirectional Encoder Representations from Transformers)中,Tokenizer是用于将文本转换为模型可以理解的输入表示的工具。Tokenizer的主要任务是将原始文本分割成单词、子词或字符,并将它们映射到模型词汇表中的标记。对于BERT来说,通常使用的是WordPiece Tokenizer,它将文本分割成单词或子词,并将它们映射到BERT词汇表中的标记。Tokenizer还负责添加特殊的标记,如[CLS]和[SEP],以及处理掩码(masking)等任务,以确保输入的正确格式化。Tokenizer在将文本转换为模型输入时起着至关重要的作用,因为它直接影响了模型的输入表示和性能。
情感分析任务的数据准备
数据预处理
在使用BERT模型之前,我们需要对原始文本数据进行适当的预处理。这包括将文本转换为BERT模型可接受的格式,使用Tokenizer,对文本进行分词和ID映射等。
1 { %# 2018LPL #% } 我 已 为 @ 皇族 电子竞技 俱乐部 送上 奖杯 ! 快来 为 你 喜爱 的 战队 加油 吧 ! { % LPL 战队 势力 榜 % }
观察数据集可以发现,每行开头用0,1表示情感的正负面,使用制表符分隔文本和标签。
# 定义一个自定义数据集类,用于加载情感分析数据集 class SentimentDataset(Dataset): def __init__(self, dataframe, tokenizer, max_length=128): self.dataframe = dataframe # 存储数据帧 self.tokenizer = tokenizer # 存储tokenizer对象 self.max_length = max_length # 存储最大长度参数 # 返回数据集中的样本数量 def __len__(self): return len(self.dataframe) # 根据给定索引返回对应的数据样本 def __getitem__(self, idx): text = self.dataframe.iloc[idx]['review'] # 获取指定索引处的文本数据 label = 1 if self.dataframe.iloc[idx]['label'] == 1 else 0 # 获取指定索引处的标签,并将其转换为0或1 # 使用tokenizer对文本进行处理,并返回模型所需的输入格式 encoding = self.tokenizer(text, padding='max_length', truncation=True, max_length=self.max_length, return_tensors='pt') return { 'input_ids': encoding['input_ids'].flatten(), # 输入token的ID 'attention_mask': encoding['attention_mask'].flatten(), # 注意力掩码 'labels': torch.tensor(label, dtype=torch.long) # 标签 }
  1. __init__(self, dataframe, tokenizer, max_length=128): 这是类的初始化方法,用于初始化数据集对象。它接受三个参数:dataframe表示包含文本和标签的数据帧,tokenizer表示用于处理文本的分词器对象,max_length表示输入文本的最大长度,默认为128。
  1. self.dataframe = dataframe: 在初始化方法中,将传入的数据帧dataframe赋值给类的dataframe属性,以便在整个类中可以访问数据帧。
  1. self.tokenizer = tokenizer: 将传入的分词器对象tokenizer赋值给类的tokenizer属性,以便在处理文本时使用该分词器。
  1. self.max_length = max_length: 将传入的最大长度参数max_length赋值给类的max_length属性,以便在处理文本时使用该参数。
  1. __len__(self): 这是一个特殊方法,用于返回数据集中样本的数量。在本例中,它返回数据帧中的行数,即数据集中的样本数量。
  1. __getitem__(self, idx): 这也是一个特殊方法,用于根据索引idx返回对应的数据样本。它首先从数据帧中获取索引处的文本和标签,然后使用分词器处理文本,并将其转换为模型所需的输入格式(包括input_ids、attention_mask和labels),最后返回处理后的数据样本。
训练及超参数
# 设置训练参数 optimizer = AdamW(model.parameters(), lr=5e-5) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device)
model.train() for epoch in range(3): for batch in tqdm(train_loader, desc="Epoch {}".format(epoch + 1)): # 获取输入的ID input_ids = batch['input_ids'].to(device) # 获取注意力掩码 attention_mask = batch['attention_mask'].to(device) # 获取标签 labels = batch['labels'].to(device) optimizer.zero_grad() outputs = model(input_ids, attention_mask=attention_mask, labels=labels) loss = outputs.loss loss.backward() optimizer.step()
模型评估
import torch from torch.utils.data import Dataset # 定义用于加载测试数据集的类 class TestDataset(Dataset): def __init__(self, file_path, tokenizer, max_length=128): self.data = [] with open(file_path, "r", encoding="utf-8") as file: for line in file: label, text = line.strip().split("\t") self.data.append((int(label), text)) # 添加到数据列表中 self.tokenizer = tokenizer # 存储分词器对象 self.max_length = max_length # 存储最大长度参数 def __len__(self): return len(self.data) # 返回数据集的样本数量 def __getitem__(self, idx): label, text = self.data[idx] # 获取指定索引处的标签和文本数据 # 使用tokenizer处理文本,并返回模型所需的输入格式 encoding = self.tokenizer(text, padding='max_length', truncation=True, max_length=self.max_length, return_tensors='pt') return { 'input_ids': encoding['input_ids'].flatten(), # 输入token的ID 'attention_mask': encoding['attention_mask'].flatten(), # 注意力掩码 'labels': torch.tensor(label, dtype=torch.long) # 标签 } # 加载测试数据集 test_dataset = TestDataset("weibo-emo/test.txt", tokenizer, max_length=128) #= test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=8, shuffle=False) # 评估模型 model.eval() total_eval_accuracy = 0 for batch in tqdm(test_loader, desc="Evaluating"): input_ids = batch['input_ids'].to(device) attention_mask = batch['attention_mask'].to(device) labels = batch['labels'].to(device) with torch.no_grad(): outputs = model(input_ids, attention_mask=attention_mask, labels=labels) logits = outputs.logits # 获取预测的logits preds = torch.argmax(logits, dim=1) # 获取预测结果 accuracy = (preds == labels).float().mean() total_eval_accuracy += accuracy.item() average_eval_accuracy = total_eval_accuracy / len(test_loader) # 计算平均准确率 print("Test Accuracy:", average_eval_accuracy)
测试结果参考
模型推理
def predict_sentiment(sentence): # 使用 tokenizer 对输入的句子进行编码,设置填充和截断方式,最大长度为 128,并返回张量格式 inputs = tokenizer(sentence, padding='max_length', truncation=True, max_length=128, return_tensors='pt').to(device) # 关闭梯度计算,在推理阶段不需要计算梯度,节省内存和计算资源 with torch.no_grad(): outputs = model(**inputs) # 获取模型的原始输出分数 logits logits = outputs.logits # 使用 softmax 函数将 logits 转换为概率分布 probs = torch.softmax(logits, dim=1) # 获取正面情感的概率 positive_prob = probs[0][1].item() # 1表示正面 # 返回正面情感的概率 return positive_prob def predict(sentence): positive_prob = predict_sentiment(sentence) threshold = 0.5 # 设置阈值 if positive_prob > threshold: print("正面") else: print("负面") predict("我草 真尼玛香")
Made with Gamma