【转载】【NLP】使用 PyTorch 通过 Hugging Face 使用 BERT 和 Transformers 进行情感分析

参考

https://blog.csdn.net/sikh_0529/article/details/127950840

目的:

用transformers加载自己的数据进行训练,然后做预测

知识点补充

什么是BERT?

BERT(在本文中介绍)代表来自 Transformers 的双向编码器表示。如果您不知道其中大部分是什么意思 - 您来对地方了!让我们解开主要思想:

  • 双向 - 要理解您正在查看的文本,您必须向后看(在前面的单词)和向前看(在下一个单词)
  • Transformers - The Attention Is All You Need论文介绍了 Transformer 模型。Transformer 一次读取整个令牌序列。从某种意义上说,该模型是非定向的,而 LSTM 是按顺序读取的(从左到右或从右到左)。注意机制允许学习单词之间的上下文关系(例如his,在一个句子中指的是吉姆)。
  • (预训练的)上下文词嵌入——ELMO 论文介绍了一种根据词义/上下文对词进行编码的方法。指甲有多重含义——手指甲和金属钉。
    ————————————————

BERT 通过屏蔽 15% 的标记进行训练,目的是猜测它们。另一个目标是预测下一句话。让我们看一下这些任务的示例:

掩码语言建模(Masked LM)

此任务的目的是猜测掩码标记。让我们看一个例子,尽量不要让它变得比它必须的更难:

That’s [mask] she [mask] -> That’s what she said

下一句预测(NSP)

给定一对两个句子,任务是判断第二个是否跟在第一个之后(二元分类)。让我们继续这个例子:

Input = [CLS] That’s [mask] she [mask]. [SEP] Hahaha, nice! [SEP]

Label = IsNext

Input = [CLS] That’s [mask] she [mask]. [SEP] Dwight, you ignorant [mask]! [SEP]

Label = NotNext

​ 训练语料库由两个条目组成:多伦多图书语料库(800M 词)和英语维基百科(2,500M 词)。原始的 Transformer 有一个编码器(用于读取输入)和一个解码器(进行预测),而 BERT 只使用解码器。

​ BERT 只是一组预训练的 Transformer 编码器。多少个编码器?我们有两个版本——12(BERT base)和 24(BERT Large)。
————————————————

这东西在实践中有用吗?

​ BERT 论文与源代码和预训练模型一起发布。

​ 最好的部分是,您可以使用 BERT 进行迁移学习(得益于 OpenAI Transformer 的想法)以完成许多 NLP 任务——分类、问答、实体识别等。您可以使用少量数据进行训练并获得出色的性能!

数据准备

数据来自于kaggle上面情感分析的数据,地址为:
https://www.kaggle.com/lava18/google-play-store-apps?select=googleplaystore_user_reviews.csv

①导入函数

# 导入函数
import transformers
# get_linear_schedule_with_warmup
from torch.utils.tensorboard import SummaryWriter
from transformers import BertModel, BertTokenizer, AdamW, get_linear_schedule_with_warmup
# from transformer
import torch
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report
from collections import defaultdict
from textwrap import wrap

from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import matplotlib.pyplot as plt
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)  # 设置随机数生成种子
torch.manual_seed(RANDOM_SEED) # 在 PyTorch 中设置一个全局的随机数种子,确保每次运行的随机数序列都是一样的
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 设备选择

②加载数据,预处理★★

​ 您可能已经知道机器学习模型不适用于原始文本。您需要将文本转换为数字(某种)。BERT 需要更多的关注(好的,对吧?)。以下是要求:

  • 添加特殊标记来分隔句子并进行分类
  • 传递恒定长度的序列(引入填充)
  • 创建 0s(pad token)和 1s(real token)的数组,称为注意力掩码

Transformers 库提供(您已经猜到了)各种各样的 Transformer 模型(包括 BERT。它适用于 TensorFlow 和 PyTorch!它还包括为我们完成繁重工作的预构建分词器! PRE_TRAINED_MODEL_NAME= ‘bert-base-cased’

让我们加载一个预训练的BertTokenizer

tokenizer = BertTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME)

我们将使用此文本来了解标记化过程

sample_txt = 'When was I last outside? I am stuck at home for 2 weeks.'

一些基本操作可以将文本转换为标记,并将标记转换为唯一的整数 (ids)

tokens = tokenizer.tokenize(sample_txt)
token_ids = tokenizer.convert_tokens_to_ids(tokens)

print(f' Sentence: {
       
       sample_txt}')
print(f'   Tokens: {
       
       tokens}')
print(f'Token IDs: {
       
       token_ids}')

Sentence: When was I last outside? I am stuck at home for 2 weeks.
Tokens: [‘When’, ‘was’, ‘I’, ‘last’, ‘outside’, ‘?’, ‘I’, ‘am’, ‘stuck’, ‘at’, ‘home’, ‘for’, ‘2’, ‘weeks’, ‘.’]
Token IDs: [1332, 1108, 146, 1314, 1796, 136, 146, 1821, 5342, 1120, 1313, 1111, 123, 2277, 119]

Special Tokens

[SEP]- 句子结束标记

tokenizer.sep_token, tokenizer.sep_token_id

(‘[SEP]’, 102)

[CLS]- 我们必须将此标记添加到每个句子的开头,以便 BERT 知道我们在进行分类

tokenizer.cls_token, tokenizer.cls_token_id

(‘[CLS]’, 101)

还有一个用于填充的特殊标记:

tokenizer.pad_token, tokenizer.pad_token_id

(‘[PAD]’, 0)

BERT 理解训练集中的标记。其他一切都可以使用[UNK](未知)令牌进行编码:

tokenizer.unk_token, tokenizer.unk_token_id

(‘[UNK]’, 100)

encode_plus()

所有这些工作都可以使用以下encode_plus()方法完成:

 # encode_plus()是Hugging Face Transformers库中一个用于将文本编码为模型输入的函数,它可以将原始文本转换为tokens,并且为每个token创建对应的编号(即token ID)和注意力掩码(即attention mask)。
        # 返回一个字典,其中包含编码后文本的整数表示(input_ids)以及对应的attention mask(attention_mask)。
        #eg:{'input_ids': [101, 1045, 2293, 3019, 2653, 19387, 999, 102, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 0, 0, 0, 0]}
        encoding = self.tokenizer.encode_plus(  # 将文本数据转换为模型可以处理的输入格式(它将文本分解为单词或者子词,并将它们转换为对应的整数表示)。
            review,                  # 输入的评论文本数据
            add_special_tokens=True,  # Add '[CLS]' and '[SEP]'【添加特殊标记来分隔句子并进行分类】
            max_length=self.max_len,  # 我们希望结果的数字向量最多包含元素个数 【传递恒定长度的序列(引入填充)】
            return_token_type_ids=False,  # 每个标记都会对应一个id值(eg:('[CLS]', 101))
            pad_to_max_length=True,  # 对文本进行padding  【传递恒定长度的序列(引入填充)】
            return_attention_mask=True,     # 函数会计算出注意力掩码和token类型ID信息,并将它们包含在返回结果中
            return_tensors='pt',        # 返回pytorch张量
        )
# 加载数据,预处理
df = pd.read_csv("archive/googleplaystore_user_reviews.csv")
df = df.dropna()  # 用于删除具有缺失值的行或列
def to_sentiment(rating):
    if rating == 'Positive':
        return 2
    elif rating == 'Neutral':
        return 1
    return 0
df['sentiment'] = df.Sentiment.apply(to_sentiment)
class_names=["Negative","Neutral","Positive"]
# 划分数据集(df_train、df_test、df_val)
df_train, df_test = train_test_split(df, test_size=0.1, random_state=RANDOM_SEED)  # 将原始数据集划分成训练集和测试集(函数的参数包括原始数据集、测试集占比、随机数种子等)
df_val, df_test = train_test_split(df_test, test_size=0.5, random _state=RANDOM_SEED)

③创建dataset和dataloader

使得我们的输入数据符合模型的要求。主要是通过self.tokenizer.encode_plus()

# 创建dataset和dataloader
class GPReviewDataset(Dataset):
    def __init__(self, reviews, targets, tokenizer, max_len):
        self.reviews = reviews   #
        self.targets = targets
        self.tokenizer = tokenizer  # 分词器;将文本数据转换为机器学习算法可以理解的数字表示,例如整数或向量
        # 通常,tokenizer会将文本拆分成一个个token,然后将这些token转换为数字或其他形式的向量,以便计算机可以更容易地理解和处理这些数据
        self.max_len = max_len

    def __len__(self):
        return len(self.reviews)

    def __getitem__(self, item):
        review = str(self.reviews[item])  # 得到特定的一条评论
        target = self.targets[item]     # 得到特定的一条评论情感值
        # encode_plus()是Hugging Face Transformers库中一个用于将文本编码为模型输入的函数,它可以将原始文本转换为tokens,并且为每个token创建对应的编号(即token ID)和注意力掩码(即attention mask)。
        # 返回一个字典,其中包含编码后文本的整数表示(input_ids)以及对应的attention mask(attention_mask)。
        #eg:{'input_ids': [101, 1045, 2293, 3019, 2653, 19387, 999, 102, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 0, 0, 0, 0]}
        encoding = self.tokenizer.encode_plus(  # 将文本数据转换为模型可以处理的输入格式(它将文本分解为单词或者子词,并将它们转换为对应的整数表示)。
            review,                  # 输入的评论文本数据
            add_special_tokens=True,  # Add '[CLS]' and '[SEP]'【添加特殊标记来分隔句子并进行分类】
            max_length=self.max_len,  # 我们希望结果的数字向量最多包含元素个数 【传递恒定长度的序列(引入填充)】
            return_token_type_ids=False,  # 每个标记都会对应一个id值(eg:('[CLS]', 101))
            pad_to_max_length=True,  # 对文本进行padding  【传递恒定长度的序列(引入填充)】
            return_attention_mask=True,     # 函数会计算出注意力掩码和token类型ID信息,并将它们包含在返回结果中
            return_tensors='pt',        # 返回pytorch张量
        )
        #         print(target)
        return {
    
    
            'review_text': review,
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'targets': torch.tensor(target, dtype=torch.long)
        }


def create_data_loader(df, tokenizer, max_len, batch_size):
    ds = GPReviewDataset(
        reviews=df.Translated_Review.to_numpy(), # 将数据集转换为Numpy数组
        targets=df.sentiment.to_numpy(),
        tokenizer=tokenizer,
        max_len=max_len
    )

    return DataLoader(   # 创建数据加载器
        ds,
        batch_size=batch_size,  # 批量大小
        num_workers=4   # 4个进程加载数据
    )

BATCH_SIZE = 16
MAX_LEN = 160  # 根据数据集中评论的标记数目分布来确定的
PRE_TRAINED_MODEL_NAME = 'bert-base-uncased'    # 加载预训练的分词器需要下载相应的模型文件,本地没有这个模型的文件,Hugging Face Transformers库会自动下载并缓存它们
tokenizer = BertTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME) #用于加载预训练的BERT模型的分词器(tokenizer),Hugging Face Transformers库中的一个方法
# 创建三个数据集所对应的数据加载器
train_data_loader = create_data_loader(df_train, tokenizer, MAX_LEN, BATCH_SIZE)
val_data_loader = create_data_loader(df_val, tokenizer, MAX_LEN, BATCH_SIZE)
test_data_loader = create_data_loader(df_test, tokenizer, MAX_LEN, BATCH_SIZE)

④创建基于BERT的情感分析模型

通过前面对文本数据进行处理得到对应的数字向量之后(1),将其输入到这里的模型中进行处理。

(1)过程得到一个

return DataLoader(   # 创建数据加载器
    ds,
    batch_size=batch_size,  # 批量大小
    num_workers=4   # 4个进程加载数据
)

在训练过程中通过模型调用ds中的input_ids,attention_mask

for d in data_loader:
    input_ids = d["input_ids"].to(device) # 模型的输入数据进行GPU加速
    attention_mask = d["attention_mask"].to(device)
    targets = d["targets"].to(device)

    outputs = model(
        input_ids=input_ids,
        attention_mask=attention_mask
    )

得到各种分类的预测概率。

# 创建基于BERT的情感分析模型(创建一个使用 BERT 模型的分类器)
'''
分类器将大部分繁重的工作委托给了 BertModel。我们使用 dropout 层进行正则化,使用全连接层进行输出。
▲▲▲请注意,我们要返回最后一层的原始输出,因为 PyTorch 中的交叉熵损失函数需要它才能工作(梯度下降,优化参数)。
'''
class SentimentClassifier(nn.Module):
    def __init__(self, n_classes):
        super(SentimentClassifier, self).__init__()
        self.bert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME) # 加载预训练的 BERT 模型(该对象包含预训练模型的权重和配置信息)
        self.drop = nn.Dropout(p=0.3)  # 防止过拟合,以概率P来丢掉特征
        self.out = nn.Linear(self.bert.config.hidden_size, n_classes) # 全连接层(线性层)
        '''
        ①输入大小为self.bert.config.hidden_size,输出大小为n_classes。
        ②在自然语言处理任务中,通常使用预训练的语言模型作为特征提取器,然后在其之上添加一些额外的层来执行具体的任务,例如文本分类、命名实体识别等。
        在这种情况下,self.bert.config.hidden_size是预训练语言模型的隐藏层大小,代表了语言模型生成的特征的维度。
        n_classes代表了我们要执行的具体任务中的分类数目,例如在文本分类任务中,n_classes可以代表分类的标签数量。
        ③这一行代码的作用是构建一个线性层,该层将预训练语言模型的隐藏层特征作为输入,输出大小为n_classes的向量。
        这个向量通常会进一步送入softmax层来进行归一化处理,以得到每个类别的概率分布。
        这个过程也可以通过在PyTorch中定义一个nn.Sequential模型来完成。
        '''

    def forward(self, input_ids, attention_mask):
        _, pooled_output = self.bert(
            input_ids=input_ids,
            attention_mask=attention_mask,
            return_dict = False   # 报错“dropout(): argument 'input' (position 1) must be Tensor, not str” 修改
        )
        # pooled_output:形状为 [batch_size, hidden_size] 的张量(将其视为内容摘要),表示整个序列的池化向量,它是通过对last_hidden_state取第一个token(即[CLS])的隐藏状态进行线性变换和tanh激活得到的。
        '''
        这里的原型是:
        last_hidden_state, pooled_output = bert_model(
            input_ids=encoding['input_ids'],
            attention_mask=encoding['attention_mask']
        )
        ①last_hidden_state是模型最后一层的last_hidden_state一系列隐藏状态。获取pooled_output是通过在 上应用BertPooler来完成的last_hidden_state: 
        last_hidden_state.shape
        >>>torch.Size([1, 32, 768])   # 有 32 个标记(示例序列的长度)中每一个的隐藏状态、768是前馈网络中隐藏单元的数量,可以通过检查配置来验证:bert_model.config.hidden_size
        ②pooled_output根据 BERT,您可以将其视为内容摘要。输出的形状:
        pooled_output.shape
        >>>torch.Size([1, 768]) 
        '''
        output = self.drop(pooled_output) # 将BERT模型的编码结果随机失活,并将结果作为下一层神经网络的输入
        return self.out(output) # 将dropout后的BERT模型的编码结果作为输入,并将结果映射到一个n_classes维的向量,代表了不同类别的分数或概率【可以通过对输出结果进行softmax归一化进一步来获得预测的类型】

⑤训练和验证

# 训练和验证
'''
我们如何提出所有超参数?BERT 作者有一些微调建议:
        批量大小:16、32
        学习率(Adam):5e-5、3e-5、2e-5
        epochs数:2、3、4
我们将忽略 epochs 推荐的数量,但坚持使用其余的。请注意,增加批量大小会显着减少训练时间,但会降低准确性。
'''

# 创建一个实例并将其移动到 GPU
model = SentimentClassifier(len(class_names))  # 三分类问题:["Negative","Neutral","Positive"]
model = model.to(device) # 选择设备
EPOCHS = 10  # 训练迭代10轮

# 其中AdamW是一种基于Adam优化算法的变种,它是为了解决Adam优化算法在权重衰减上的问题而提出的,具有更好的性能表现
# correct_bias=False表示是否进行偏差校正
optimizer = AdamW(model.parameters(), lr=2e-5, correct_bias=False)  # 优化器
total_steps = len(train_data_loader) * EPOCHS  # 训练的总步数=训练集的大小*迭代轮数

'''
定义学习率调度器的函数(这里使用没有预热步骤的线性调度程序):
①它将学习率与训练步数关联起来,使得在训练的早期,学习率逐渐增加,而在训练的后期,学习率逐渐降低。
这样的学习率调整策略可以帮助模型更快地收敛并达到更好的性能。
②
'''
scheduler = get_linear_schedule_with_warmup(  # 定义一个线性调度器
  optimizer,
  num_warmup_steps=0, # 学习率逐渐增加的步数,在这些步数内,学习率将从初始值逐渐增加到设定的最大值
  num_training_steps=total_steps # 表示总的训练步数
)

loss_fn = nn.CrossEntropyLoss().to(device) # 损失函数(交叉熵)

训练函数

# 训练函数(编写一个辅助函数来训练我们的模型一个时期)
def train_epoch(
        model,
        data_loader,
        loss_fn,
        optimizer,
        device,
        scheduler,
        n_examples
):
    model = model.train() # 使得模型处于训练模式
    '''
    在深度学习中,通常需要通过反向传播算法来优化神经网络的参数,以最小化损失函数。
    在 PyTorch 中,可以通过调用 train() 方法来将模型设置为训练模式,以便进行反向传播和参数优化。
    ①调用 model.train() 将使得模型处于训练模式,并开启 Batch Normalization 和 Dropout 等层的运行模式,以及允许梯度计算。
    '''
    losses = []  # 用于记录每次迭代后的损失值
    correct_predictions = 0

    for d in data_loader:
        '''
        将训练数据的示例批次移动到 GPU
        print(input_ids.shape) # batch size x seq length
        print(attention_mask.shape) # batch size x seq length
        >>>torch.Size([16, 160])
           torch.Size([16, 160])
        批处理训练数据,一次处理batch_size组数据,每组数据有seq_length个。
        '''
        input_ids = d["input_ids"].to(device) # 模型的输入数据进行GPU加速
        attention_mask = d["attention_mask"].to(device)
        targets = d["targets"].to(device)

        outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
        )

        _, preds = torch.max(outputs, dim=1) # 类属于argmax()函数,将概率映射到具体的分类类型值
        loss = loss_fn(outputs, targets) # 用损失函数计算模型预测结果与真实值之间的差距,得到对应的损失值

        correct_predictions += torch.sum(preds == targets) # 模型预测的准确数
        '''
        这段代码的作用是记录模型训练过程中每次迭代的损失值,并将它们存储在一个列表中。
        最终,我们可以通过分析这些损失值,来评估模型训练的效果。
        通常情况下,损失值应该随着训练的进行而逐渐减小,如果损失值一直保持不变或者增大,则可能需要调整模型结构、参数或者训练过程中的超参数等。
        '''
        losses.append(loss.item())

        # 优化器优化模型
        loss.backward()  # 反向传播, 这里要注意不能使用定义损失函数那里的 loss,而要使用 调用损失函数之后的 result_loss【反向传播 得到每个需要更新参数对应的梯度】
        '''
        一个梯度裁剪操作,用于限制梯度的大小。
        这个操作可以防止梯度爆炸的情况发生,即当梯度的范数超过一个阈值时,将梯度向量的大小缩小到这个阈值之内,防止模型参数更新过大。
        '''
        nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
        optimizer.step() # 根据当前迭代的梯度值,更新模型参数(会自动更新各个参数的值)
        scheduler.step() # 对学习率进行调整(会根据预先设定的学习率调整策略,动态地调整学习率的大小,以提高训练效果)
        optimizer.zero_grad() # 清空模型参数的梯度的操作。在进行下一次迭代前,需要将模型参数的梯度归零,以免与上一次迭代的梯度混淆

    return correct_predictions.double() / n_examples, np.mean(losses) # 返回一轮迭代之后总的预测准确率和loss均值

验证函数:

# 验证函数(同理训练模型,主要的区别就是验证函数就不需要在进行更新参数,更不需要梯度进行优化):
def eval_model(model, data_loader, loss_fn, device, n_examples):
    model = model.eval()
    '''
    ②调用 model.eval() 可以将模型设置为评估模式,使得在前向传播过程中禁止 Dropout 层的操作,
    并将 Batch Normalization 层固定为使用训练时计算出来的均值和方差,而不是在当前 batch 上重新计算。
    ③在模型完成训练后,需要调用 model.eval() 来将模型设置为评估模式,以便进行测试或推理操作。
    '''
    losses = []
    correct_predictions = 0

    '''
    ①with torch.no_grad() 是一个上下文管理器(Context Manager),用于在 PyTorch 中禁止梯度计算的情况下执行代码块。
    ②在使用 PyTorch 进行深度学习训练时,需要计算每个变量的梯度以更新参数。
    然而,在某些情况下,我们不希望计算梯度,例如在模型评估或推理阶段,或者在对模型进行微调时,我们希望固定预训练模型的参数而不更新它们。
    这时就可以使用 torch.no_grad() 来暂时禁止梯度计算。
    ③当进入 with torch.no_grad() 上下文管理器时,PyTorch 将禁止自动求导,即不会记录变量的操作历史和计算梯度信息。
    这可以提高代码的执行效率,因为不需要为每个操作计算梯度,同时也可以节省显存空间。
    '''
    with torch.no_grad(): # 这样后面就没有梯度了,测试的过程中,不需要更新参数,更不需要梯度进行优化
        for d in data_loader:
            input_ids = d["input_ids"].to(device)
            attention_mask = d["attention_mask"].to(device)
            targets = d["targets"].to(device)
            outputs = model(
            input_ids=input_ids,
            attention_mask=attention_mask
          )
            _, preds = torch.max(outputs, dim=1)

            loss = loss_fn(outputs, targets)

            correct_predictions += torch.sum(preds == targets)
            losses.append(loss.item())

    return correct_predictions.double() / n_examples, np.mean(losses)

调用他们来进行训练:

# 调用他们来进行训练:
'''
①使用 defaultdict(list) 声明一个字典时,如果访问一个不存在的键,它会自动将这个键的值初始化为空列表,这使得在向字典中添加值时更加方便。
②可以使用 defaultdict(list) 来实现一个字典,用于存储一系列事件的历史记录,其中每个事件由一个时间戳和一些相关的数据组成。
在这个字典中,每个键对应一个时间戳,每个值都是一个列表,其中存储了该时间戳下的所有事件的数据。

'''
history = defaultdict(list) #
best_accuracy = 0
# 添加tensorboard
writer = SummaryWriter("./logs")

for epoch in range(EPOCHS):
    print(f'Epoch {
      
      epoch + 1}/{
      
      EPOCHS}')
    print('-' * 10)
    train_acc, train_loss = train_epoch(  # 训练模型
        model,
        train_data_loader,
        loss_fn,
        optimizer,
        device,
        scheduler,
        len(df_train)
      )
    print(f'Train loss {
      
      train_loss} accuracy {
      
      train_acc}')

    val_acc, val_loss = eval_model(  # 预测模型
        model,
        val_data_loader,
        loss_fn,
        device,
        len(df_val)
      )
    print(f'Val   loss {
      
      val_loss} accuracy {
      
      val_acc}')
    print()

    # 添加多个scalar值到同一个图表中
    # scalar_dict = {'train_loss': train_loss, 'val_loss': val_loss, 'train_acc': train_acc, 'val_acc': val_acc}
    # writer.add_scalars('Loss/Accuracy', scalar_dict=scalar_dict, global_step=epoch)
    writer.add_scalar("train_correct_predictions", train_acc, epoch)
    writer.add_scalar("train_loss", train_loss, epoch)
    writer.add_scalar("val_correct_predictions", val_acc, epoch)
    writer.add_scalar("val_loss", val_loss, epoch)

    history['train_acc'].append(train_acc)
    history['train_loss'].append(train_loss)
    history['val_acc'].append(val_acc)
    history['val_loss'].append(val_loss)

    if val_acc > best_accuracy: # 存储最佳模型的状态,以最高验证准确度表示
        torch.save(model.state_dict(), 'best_model_state.bin')  # 实现原理就是如果出现一个比当前预测准确率更高的模型就重写保存的文件。
        # torch.save(model.state_dict(), 'model{}_state(train_loss:{},train_acc:{}).pth'.format(epoch,train_loss,train_acc)) #网络模型的保存(只保存训练模型的参数,不保存其结构)
        '''
        保存:torch.save(model.state_dict,“abc.pth”)
        调用:
        model = torchvision.models.vgg16(pretrained=False)
        model.load_state_dict(torch.load(“abc.pth”))
        '''
        '''
        ①在 PyTorch 中,模型通常由两个主要部分组成:模型的结构和模型的参数。
        模型的结构通常由代码定义,而模型的参数则存储在模型状态字典中。
        状态字典是一个 Python 字典,其中包含了模型中所有可学习参数的名称和对应的张量值。
        ②通过执行 model.state_dict(),我们可以获得模型的状态字典。
        这个字典可以用于将模型的参数保存到磁盘上,以便后续加载模型时使用。
        
        '''
        best_accuracy = val_acc

# 关闭SummaryWriter对象
writer.close()


# 调用已经存储的训练历史文件,看看训练与验证的准确性:
plt.plot(history['train_acc'], label='train accuracy')
plt.plot(history['val_acc'], label='validation accuracy')

plt.title('Training history')
plt.ylabel('Accuracy')
plt.xlabel('Epoch')
plt.legend()
plt.ylim([0, 1])

做预测

主要一点就是如何加载自己已经训练好的模型(三步走)

# 加载已经训练好的模型
model = SentimentClassifier(len(class_names))
model.load_state_dict(torch.load("/home/qk/code/model9_state(train_loss:0.012110426034161507,train_acc:0.9976546728417053).pth"))
model = model.to(device)

然后就是同上面训练模型的步骤一致:

​ ①对待预测的文本review_text使用分词器对文本进行编码tokenizer.encode_plus()

​ ②从编码后的encoded_review 取出模型需要的参数

​ ③将参数喂入模型得到预测结果

'''
做预测
'''

# 导入函数
import transformers
# get_linear_schedule_with_warmup
from torch.utils.tensorboard import SummaryWriter
from transformers import BertModel, BertTokenizer, AdamW, get_linear_schedule_with_warmup
# from transformer
import torch
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report
from collections import defaultdict
from textwrap import wrap

from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F

RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)  # 设置随机数生成种子
torch.manual_seed(RANDOM_SEED)  # 在 PyTorch 中设置一个全局的随机数种子,确保每次运行的随机数序列都是一样的
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # 设备选择

# 加载数据,预处理

class_names = ["Negative", "Neutral", "Positive"]




BATCH_SIZE = 16
MAX_LEN = 160
PRE_TRAINED_MODEL_NAME = 'bert-base-uncased'  # 加载预训练的分词器需要下载相应的模型文件,本地没有这个模型的文件,Hugging Face Transformers库会自动下载并缓存它们
tokenizer = BertTokenizer.from_pretrained(
    PRE_TRAINED_MODEL_NAME)  # 用于加载预训练的BERT模型的分词器(tokenizer),Hugging Face Transformers库中的一个方法


# 创建基于BERT的情感分析模型
class SentimentClassifier(nn.Module):
    def __init__(self, n_classes):
        super(SentimentClassifier, self).__init__()
        self.bert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME)  # 加载预训练的 BERT 模型(该对象包含预训练模型的权重和配置信息)
        self.drop = nn.Dropout(p=0.3)  # 防止过拟合,以概率P来丢掉特征
        self.out = nn.Linear(self.bert.config.hidden_size, n_classes)  # 全连接层(线性层)
        '''
        ①输入大小为self.bert.config.hidden_size,输出大小为n_classes。
        ②在自然语言处理任务中,通常使用预训练的语言模型作为特征提取器,然后在其之上添加一些额外的层来执行具体的任务,例如文本分类、命名实体识别等。
        在这种情况下,self.bert.config.hidden_size是预训练语言模型的隐藏层大小,代表了语言模型生成的特征的维度。
        n_classes代表了我们要执行的具体任务中的分类数目,例如在文本分类任务中,n_classes可以代表分类的标签数量。
        ③这一行代码的作用是构建一个线性层,该层将预训练语言模型的隐藏层特征作为输入,输出大小为n_classes的向量。
        这个向量通常会进一步送入softmax层来进行归一化处理,以得到每个类别的概率分布。
        这个过程也可以通过在PyTorch中定义一个nn.Sequential模型来完成。
        '''

    def forward(self, input_ids, attention_mask):
        _, pooled_output = self.bert(
            input_ids=input_ids,
            attention_mask=attention_mask,
            return_dict=False  # 报错“dropout(): argument 'input' (position 1) must be Tensor, not str” 修改
        )
        # pooled_output:形状为 [batch_size, hidden_size] 的张量,表示整个序列的池化向量,它是通过对last_hidden_state取第一个token(即[CLS])的隐藏状态进行线性变换和tanh激活得到的。
        output = self.drop(pooled_output)  # 将BERT模型的编码结果随机失活,并将结果作为下一层神经网络的输入
        return self.out(
            output)  # 将dropout后的BERT模型的编码结果作为输入,并将结果映射到一个n_classes维的向量,代表了不同类别的分数或概率【可以通过对输出结果进行softmax归一化进一步来获得预测的类型】


# 预测
# 加载已经训练好的模型
model = SentimentClassifier(len(class_names))
model.load_state_dict(torch.load("/home/qk/code/model9_state(train_loss:0.012110426034161507,train_acc:0.9976546728417053).pth"))
model = model.to(device)
# print(model)

# model = SentimentClassifier(len(class_names))  # 三分类问题:["Negative","Neutral","Positive"]
# model = model.to(device)  # 选择设备
while 1:
    review_text = input("请输入一段文本:")
    # review_text = "I love completing my todos! Best app ever!!!"
    # ②使用分词器对文本进行编码
    encoded_review = tokenizer.encode_plus(
        review_text,
        max_length=MAX_LEN,
        add_special_tokens=True,
        return_token_type_ids=False,
        pad_to_max_length=True,
        return_attention_mask=True,
        return_tensors='pt',
    )
    input_ids = encoded_review['input_ids'].to(device)
    #print(input_ids)
    attention_mask = encoded_review['attention_mask'].to(device)
    #print(attention_mask)

    #③从我们的模型中得到预测
    model = model.eval()
    with torch.no_grad():
        output = model(input_ids, attention_mask)
        _, prediction = torch.max(output, dim=1)

    print(f'Review text: {
      
      review_text}')
    print(f'Sentiment  : {
      
      class_names[prediction]}')




做评估

同在模型训练中的预测过程,主要是定义一个辅助函数来从我们的模型中获取预测,得到我们评估所需要的指标并查看其分类报告:

精确度(Precision):预测为正类别的样本中,实际为正类别的比例。
召回率(Recall):实际为正类别的样本中,预测为正类别的比例。
F1值(F1-score):精确度和召回率的调和平均值,是精确度和召回率的综合指标。
支持度(Support):真实标签中每个类别的样本数量。
'''
评估
那么我们的模型在预测情绪方面有多好?让我们从计算测试数据的准确性开始:
'''

# 导入函数
import transformers
# get_linear_schedule_with_warmup
from torch.utils.tensorboard import SummaryWriter
from transformers import BertModel, BertTokenizer, AdamW, get_linear_schedule_with_warmup
# from transformer
import torch
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix, classification_report
from collections import defaultdict
from textwrap import wrap

from torch import nn, optim
from torch.utils.data import Dataset, DataLoader
import torch.nn.functional as F
import seaborn as sns
import matplotlib.pyplot as plt

RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)  # 设置随机数生成种子
torch.manual_seed(RANDOM_SEED)  # 在 PyTorch 中设置一个全局的随机数种子,确保每次运行的随机数序列都是一样的
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # 设备选择

# 加载数据,预处理
# 加载数据,预处理
df = pd.read_csv("archive/googleplaystore_user_reviews.csv")
df=df.dropna()  # 用于删除具有缺失值的行或列
def to_sentiment(rating):
    if rating == 'Positive':
        return 2
    elif rating == 'Neutral':
        return 1
    return 0
df['sentiment'] = df.Sentiment.apply(to_sentiment)
class_names=["Negative","Neutral","Positive"]
# 划分数据集(df_train、df_test、df_val)
df_train, df_test = train_test_split(df, test_size=0.1, random_state=RANDOM_SEED)  # 将原始数据集划分成训练集和测试集(函数的参数包括原始数据集、测试集占比、随机数种子等)
df_val, df_test = train_test_split(df_test, test_size=0.5, random_state=RANDOM_SEED)

# 创建dataset和dataloader
class GPReviewDataset(Dataset):
    def __init__(self, reviews, targets, tokenizer, max_len):
        self.reviews = reviews   #
        self.targets = targets
        self.tokenizer = tokenizer  # 分词器;将文本数据转换为机器学习算法可以理解的数字表示,例如整数或向量
        # 通常,tokenizer会将文本拆分成一个个token,然后将这些token转换为数字或其他形式的向量,以便计算机可以更容易地理解和处理这些数据
        self.max_len = max_len

    def __len__(self):
        return len(self.reviews)

    def __getitem__(self, item):
        review = str(self.reviews[item])  # 得到特定的一条评论
        target = self.targets[item]     # 得到特定的一条评论情感值
        # encode_plus()是Hugging Face Transformers库中一个用于将文本编码为模型输入的函数,它可以将原始文本转换为tokens,并且为每个token创建对应的编号(即token ID)和注意力掩码(即attention mask)。
        # 返回一个字典,其中包含编码后文本的整数表示(input_ids)以及对应的attention mask(attention_mask)。
        #eg:{'input_ids': [101, 1045, 2293, 3019, 2653, 19387, 999, 102, 0, 0], 'attention_mask': [1, 1, 1, 1, 1, 1, 0, 0, 0, 0]}
        encoding = self.tokenizer.encode_plus(  # 将文本数据转换为模型可以处理的输入格式(它将文本分解为单词或者子词,并将它们转换为对应的整数表示)。
            review,                  # 输入的评论文本数据
            add_special_tokens=True,  # Add '[CLS]' and '[SEP]'【添加特殊标记来分隔句子并进行分类】
            max_length=self.max_len,  # 我们希望结果的数字向量最多包含元素个数 【传递恒定长度的序列(引入填充)】
            return_token_type_ids=False,  # 每个标记都会对应一个id值(eg:('[CLS]', 101))
            pad_to_max_length=True,  # 对文本进行padding  【传递恒定长度的序列(引入填充)】
            return_attention_mask=True,     # 函数会计算出注意力掩码和token类型ID信息,并将它们包含在返回结果中
            return_tensors='pt',        # 返回pytorch张量
        )
        #         print(target)
        return {
    
    
            'review_text': review,
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'targets': torch.tensor(target, dtype=torch.long)
        }


def create_data_loader(df, tokenizer, max_len, batch_size):
    ds = GPReviewDataset(
        reviews=df.Translated_Review.to_numpy(), # 将数据集转换为Numpy数组
        targets=df.sentiment.to_numpy(),
        tokenizer=tokenizer,
        max_len=max_len
    )

    return DataLoader(   # 创建数据加载器
        ds,
        batch_size=batch_size,  # 批量大小
        num_workers=4   # 4个进程加载数据
    )



BATCH_SIZE = 16
MAX_LEN = 160
PRE_TRAINED_MODEL_NAME = 'bert-base-uncased'  # 加载预训练的分词器需要下载相应的模型文件,本地没有这个模型的文件,Hugging Face Transformers库会自动下载并缓存它们
tokenizer = BertTokenizer.from_pretrained(
    PRE_TRAINED_MODEL_NAME)  # 用于加载预训练的BERT模型的分词器(tokenizer),Hugging Face Transformers库中的一个方法
# 创建三个数据集所对应的数据加载器
train_data_loader = create_data_loader(df_train, tokenizer, MAX_LEN, BATCH_SIZE)
val_data_loader = create_data_loader(df_val, tokenizer, MAX_LEN, BATCH_SIZE)
test_data_loader = create_data_loader(df_test, tokenizer, MAX_LEN, BATCH_SIZE)


# 创建基于BERT的情感分析模型
class SentimentClassifier(nn.Module):
    def __init__(self, n_classes):
        super(SentimentClassifier, self).__init__()
        self.bert = BertModel.from_pretrained(PRE_TRAINED_MODEL_NAME)  # 加载预训练的 BERT 模型(该对象包含预训练模型的权重和配置信息)
        self.drop = nn.Dropout(p=0.3)  # 防止过拟合,以概率P来丢掉特征
        self.out = nn.Linear(self.bert.config.hidden_size, n_classes)  # 全连接层(线性层)
        '''
        ①输入大小为self.bert.config.hidden_size,输出大小为n_classes。
        ②在自然语言处理任务中,通常使用预训练的语言模型作为特征提取器,然后在其之上添加一些额外的层来执行具体的任务,例如文本分类、命名实体识别等。
        在这种情况下,self.bert.config.hidden_size是预训练语言模型的隐藏层大小,代表了语言模型生成的特征的维度。
        n_classes代表了我们要执行的具体任务中的分类数目,例如在文本分类任务中,n_classes可以代表分类的标签数量。
        ③这一行代码的作用是构建一个线性层,该层将预训练语言模型的隐藏层特征作为输入,输出大小为n_classes的向量。
        这个向量通常会进一步送入softmax层来进行归一化处理,以得到每个类别的概率分布。
        这个过程也可以通过在PyTorch中定义一个nn.Sequential模型来完成。
        '''

    def forward(self, input_ids, attention_mask):
        _, pooled_output = self.bert(
            input_ids=input_ids,
            attention_mask=attention_mask,
            return_dict=False  # 报错“dropout(): argument 'input' (position 1) must be Tensor, not str” 修改
        )
        # pooled_output:形状为 [batch_size, hidden_size] 的张量,表示整个序列的池化向量,它是通过对last_hidden_state取第一个token(即[CLS])的隐藏状态进行线性变换和tanh激活得到的。
        output = self.drop(pooled_output)  # 将BERT模型的编码结果随机失活,并将结果作为下一层神经网络的输入
        return self.out(
            output)  # 将dropout后的BERT模型的编码结果作为输入,并将结果映射到一个n_classes维的向量,代表了不同类别的分数或概率【可以通过对输出结果进行softmax归一化进一步来获得预测的类型】


# 评估
# 加载已经训练好的模型
model = SentimentClassifier(len(class_names))
model.load_state_dict(torch.load("/home/qk/code/model9_state(train_loss:0.012110426034161507,train_acc:0.9976546728417053).pth"))
model = model.to(device)

# test_acc, _ = eval_model(
#     model,
#     test_data_loader,
#     loss_fn,
#     device,
#     len(df_test)
# )
# test_acc.item()


# 定义一个辅助函数来从我们的模型中获取预测:
def get_predictions(model, data_loader):
    model = model.eval()

    review_texts = []
    predictions = []
    prediction_probs = []
    real_values = []

    with torch.no_grad():
        for d in data_loader:
            texts = d["review_text"]
            input_ids = d["input_ids"].to(device)
            attention_mask = d["attention_mask"].to(device)
            targets = d["targets"].to(device)

            outputs = model(
                input_ids=input_ids,
                attention_mask=attention_mask
            )
            _, preds = torch.max(outputs, dim=1)

            review_texts.extend(texts)
            predictions.extend(preds)
            prediction_probs.extend(outputs)
            real_values.extend(targets)
    '''
    ①具体来说,假设模型的预测结果以Python列表形式存储在变量 predictions 中,每个预测结果都是一个PyTorch张量。
    要将这些张量合并成一个张量并将其转移到CPU上,可以使用torch.stack函数和cpu()方法
    '''
    predictions = torch.stack(predictions).cpu()  # 用于将模型预测结果存储在一个张量中并将其转移到CPU上
    prediction_probs = torch.stack(prediction_probs).cpu()
    real_values = torch.stack(real_values).cpu()
    return review_texts, predictions, prediction_probs, real_values


# 类似于评估函数,除了我们存储评论文本和预测概率:
y_review_texts, y_pred, y_pred_probs, y_test = get_predictions(
    model,
    test_data_loader
)

# 看分类报告
'''
①classification_report 是 sklearn.metrics 模块中的一个函数,生成分类模型的分类报告,该报告包含关于分类模型性能的各种度量指标;
②该函数接受两个必需参数 y_test 和 y_pred,分别代表真实标签和预测标签。函数会将这两个参数传递给分类模型的评估方法,并计算以下几个度量指标:
    精确度(Precision):预测为正类别的样本中,实际为正类别的比例。
    召回率(Recall):实际为正类别的样本中,预测为正类别的比例。
    F1值(F1-score):精确度和召回率的调和平均值,是精确度和召回率的综合指标。
    支持度(Support):真实标签中每个类别的样本数量。
③除此之外,classification_report 函数还可以为每个类别生成单独的报告,包括每个类别的精确度、召回率、F1值和支持度。
要生成每个类别的报告,可以通过 target_names 参数将类别名称传递给函数;
调用 classification_report(y_test, y_pred, target_names=class_names) 函数,可以获取分类模型的分类报告,并了解模型在每个类别上的性能。
'''
print(classification_report(y_test, y_pred, target_names=class_names))


# 混淆矩阵
def show_confusion_matrix(confusion_matrix):
    hmap = sns.heatmap(confusion_matrix, annot=True, fmt="d", cmap="Blues")
    hmap.yaxis.set_ticklabels(hmap.yaxis.get_ticklabels(), rotation=0, ha='right')
    hmap.xaxis.set_ticklabels(hmap.xaxis.get_ticklabels(), rotation=30, ha='right')
    plt.ylabel('True sentiment')
    plt.xlabel('Predicted sentiment')


cm = confusion_matrix(y_test, y_pred)
df_cm = pd.DataFrame(cm, index=class_names, columns=class_names)
show_confusion_matrix(df_cm)

# 看一下测试数据中的一个例子:
idx = 2

review_text = y_review_texts[idx]
true_sentiment = y_test[idx]
pred_df = pd.DataFrame({
    
    
    'class_names': class_names,
    'values': y_pred_probs[idx]
})

print("\n".join(wrap(review_text)))
print()
print(f'True sentiment: {
      
      class_names[true_sentiment]}')



# 查看模型中每种情绪的置信度:
sns.barplot(x='values', y='class_names', data=pred_df, orient='h')
plt.ylabel('sentiment')
plt.xlabel('probability')
plt.xlim([0, 1])

猜你喜欢

转载自blog.csdn.net/mwcxz/article/details/129877681