使用BGE进行意图分类的示例代码

关于参数冻结:

 在业界使用BERT模型做分类任务时,是否冻结预训练模型的参数取决于具体的应用场景和目标。下面是一些常见的考虑因素:

不冻结参数(全参数微调)

  • 数据量较大:如果有足够多的标注数据,全参数微调可以充分利用这些数据来调整预训练模型的参数,以更好地适应特定任务。
  • 任务特异性较强:对于与预训练任务差异较大的下游任务,全参数微调可以帮助模型更好地适应新任务的特点。
  • 计算资源充足:全参数微调需要更多的计算资源和训练时间,如果资源允许,这通常能获得更好的性能。
 

冻结部分参数

  • 数据量较小:当标注数据不足时,冻结预训练模型的大部分参数,只微调部分层(如顶层)或者只训练一个新加的分类头,可以防止过拟合。
  • 计算资源有限:在资源受限的情况下,冻结大部分参数可以显著减少训练时间和内存消耗。
  • 快速原型和实验:在快速迭代开发或进行初步实验时,冻结参数可以加快训练速度,帮助快速验证想法。

实践建议

  • 实验决定:实际操作中,是否冻结参数以及冻结哪些参数往往需要根据任务的具体需求和实验结果来决定。一种常见的做法是先尝试全参数微调,观察模型的表现,然后根据需要调整策略。
  • 逐层冻结或解冻:有时,可以采用更细粒度的策略,比如只冻结预训练模型的前几层,或者在训练过程中逐渐解冻更多的层。这种策略可以在保持模型泛化能力的同时,适应特定的任务需求。 

总之,是否冻结BERT模型的参数以及如何冻结,需要根据具体任务的数据量、任务特性、计算资源等因素综合考虑。在实践中,通过实验来找到最适合当前任务的策略是非常重要的。

 

下面的代码是没有包含参数冻结的:

import torch
from torch.utils.data import DataLoader, RandomSampler, TensorDataset
from transformers import BertTokenizer, BertForSequenceClassification, AdamW

bge_model_name = "BAAI/bge-large-zh-v1.5"
bert_model_name = 'bert-base-uncased'

class TextClassifier:
    def __init__(self, model_name=bge_model_name, num_labels=2):
        self.tokenizer = BertTokenizer.from_pretrained(model_name)
        self.model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels)
        self.optimizer = AdamW(self.model.parameters(), lr=2e-5)

    def prepare_data(self, texts, labels, max_length=64):
        input_ids = []
        attention_masks = []
        for text in texts:
            encoded_dict = self.tokenizer.encode_plus(
                text,
                add_special_tokens=True,
                max_length=max_length,
                pad_to_max_length=True,
                return_attention_mask=True,
                return_tensors='pt',
            )
            input_ids.append(encoded_dict['input_ids'])
            attention_masks.append(encoded_dict['attention_mask'])

        input_ids = torch.cat(input_ids, dim=0)
        attention_masks = torch.cat(attention_masks, dim=0)
        labels = torch.tensor(labels)
        return TensorDataset(input_ids, attention_masks, labels)

    def train(self, dataset, epochs=4, batch_size=2):
        dataloader = DataLoader(dataset, sampler=RandomSampler(dataset), batch_size=batch_size)
        for epoch in range(epochs):
            print(f"Epoch {epoch + 1}/{epochs}")
            print('-' * 10)
            total_loss = 0
            self.model.train()
            for step, batch in enumerate(dataloader):
                b_input_ids, b_input_mask, b_labels = batch
                self.model.zero_grad()
                outputs = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels)
                loss = outputs.loss
                total_loss += loss.item()
                loss.backward()
                self.optimizer.step()
            avg_train_loss = total_loss / len(dataloader)
            print(f"Average training loss: {avg_train_loss:.2f}")
            # 在每个epoch后评估模型的精度
            test_accuracy = self.evaluate_accuracy(test_dataset, batch_size=batch_size)
            print(f"Test Accuracy after epoch {epoch + 1}: {test_accuracy:.2f}")

    def evaluate_accuracy(self, dataset, batch_size=2):
        dataloader = DataLoader(dataset, sampler=RandomSampler(dataset), batch_size=batch_size)
        correct = 0
        total = 0
        self.model.eval()
        with torch.no_grad():
            for batch in dataloader:
                b_input_ids, b_input_mask, b_labels = batch
                outputs = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask)
                logits = outputs.logits
                predictions = torch.argmax(logits, dim=1)
                total += b_labels.size(0)
                correct += (predictions == b_labels).sum().item()
        return correct / total

    def predict(self, text):
        inputs = self.tokenizer.encode_plus(
            text,
            add_special_tokens=True,
            max_length=64,
            pad_to_max_length=True,
            return_attention_mask=True,
            return_tensors='pt',
        )
        input_ids = inputs['input_ids']
        attention_mask = inputs['attention_mask']
        self.model.eval()
        with torch.no_grad():
            outputs = self.model(input_ids, token_type_ids=None, attention_mask=attention_mask)
            logits = outputs.logits
            predicted_class_id = logits.argmax().item()
        return predicted_class_id

# 使用示例
classifier = TextClassifier()

# 通用知识场景/告警调查场景/调查响应场景/事件智能化处置/威胁建模等
# 分别对应0,1,2,3,4
# 定义训练数据
# 假设的数据集
trained_texts = ["如何配置防火墙产品?", "How to configure firewall product?", "EDR product performance?", "This event is attack successful?", "What is SQL injection?"]
# 通用知识场景/告警调查场景/调查响应场景/事件智能化处置/威胁建模等
# 分别对应0,1,2,3,4
trained_labels = [1, 1, 1, 0, 0]  # 1代表通用场景,0代表其他场景

# 假设的测试数据集
test_texts = ["How to configure firewall?", "防火墙的产品配置是如何做?", "Give me as SQL injection attack example?"]
test_labels = [1, 1, 0]

# 准备数据
train_dataset = classifier.prepare_data(trained_texts, trained_labels)
test_dataset = classifier.prepare_data(test_texts, test_labels)

# 训练模型
classifier.train(train_dataset, epochs=20)

# 评估模型
test_accuracy = classifier.evaluate_accuracy(test_dataset)
print(f"Test Accuracy: {test_accuracy:.2f}")

# 预测
test_text = "This is a firewall product, configure ok?"
test_text = "要如何配置防火墙呢?"
prediction = classifier.predict(test_text)
print(f"Predicted class for '{test_text}': {prediction}")

 

算法运行结果:

Epoch 1/20
----------
Average training loss: 1.10
Test Accuracy after epoch 1: 1.00
Epoch 2/20
----------
Average training loss: 0.17
Test Accuracy after epoch 2: 1.00
Epoch 3/20
----------
Average training loss: 0.09
Test Accuracy after epoch 3: 1.00
Epoch 4/20
----------
Average training loss: 0.06
Test Accuracy after epoch 4: 1.00
Epoch 5/20
----------
Average training loss: 0.03
Test Accuracy after epoch 5: 1.00
Epoch 6/20
----------
Average training loss: 0.02
Test Accuracy after epoch 6: 1.00
Epoch 7/20
----------
Average training loss: 0.01
Test Accuracy after epoch 7: 1.00
Epoch 8/20
----------
Average training loss: 0.00
Test Accuracy after epoch 8: 1.00
Epoch 9/20
----------
Average training loss: 0.00
Test Accuracy after epoch 9: 1.00
Epoch 10/20
----------
Average training loss: 0.00
Test Accuracy after epoch 10: 1.00
Epoch 11/20
----------
Average training loss: 0.00
Test Accuracy after epoch 11: 1.00
Epoch 12/20
----------
Average training loss: 0.00
Test Accuracy after epoch 12: 1.00
Epoch 13/20
----------
Average training loss: 0.00
Test Accuracy after epoch 13: 1.00
Epoch 14/20
----------
Average training loss: 0.00
Test Accuracy after epoch 14: 1.00
Epoch 15/20
----------
Average training loss: 0.00
Test Accuracy after epoch 15: 1.00
Epoch 16/20
----------
Average training loss: 0.00
Test Accuracy after epoch 16: 1.00
Epoch 17/20
----------
Average training loss: 0.00
Test Accuracy after epoch 17: 1.00
Epoch 18/20
----------
Average training loss: 0.00
Test Accuracy after epoch 18: 1.00
Epoch 19/20
----------
Average training loss: 0.00
Test Accuracy after epoch 19: 1.00
Epoch 20/20
----------
Average training loss: 0.00
Test Accuracy after epoch 20: 1.00
Test Accuracy: 1.00

  

模型设计:

class TextClassifier:
    def __init__(self, model_name=bge_model_name, num_labels=2):
        self.tokenizer = BertTokenizer.from_pretrained(model_name)
        self.model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels)
        self.optimizer = AdamW(self.model.parameters(), lr=2e-5)

下面是一个简化的模型示意图,描述了其主要组成部分和数据流向:

 
[输入文本] --> [BertTokenizer] --> [编码后的输入]
       |
       v
[BertForSequenceClassification] --> [Logits] --> [Softmax] --> [预测概率]
       |
       v
   [损失函数] <-- [真实标签]
       |
       v
  [AdamW优化器] --> [参数更新]
 
 

1. 输入文本:这是模型的输入,可以是一句话或者一段文本。

2. BertTokenizer:这个分词器将输入文本转换为BERT模型能理解的格式,包括将文本分割成单词或子词单元(token),并将它们转换为对应的ID。同时,它还负责添加必要的特殊标记(如[CLS]、[SEP])和生成注意力掩码。

3. 编码后的输入:这是分词器处理后的结果,包括输入ID和注意力掩码,准备好被模型使用。

4. BertForSequenceClassification:这是一个预训练的BERT模型,经过微调用于序列分类任务。它接收编码后的输入,通过BERT模型的多层Transformer结构进行处理,最后通过一个顶层的线性层输出每个类别的logits。

5. Logits:这是模型输出的原始预测值,未经过softmax归一化,因此不直接表示概率。

6. Softmax:这一步通常在需要将logits转换为概率时进行,比如在评估模型性能或进行预测时。Softmax函数确保所有输出类别的概率和为1。

7. 预测概率:这是经过softmax处理后的模型输出,表示模型对每个类别的预测概率。

  • 真实标签:在训练过程中,每个输入样本都有一个对应的真实标签,用于计算损失函数。

9. 损失函数:根据模型的预测结果和真实标签计算损失值,这个值表示模型当前的性能,训练的目标是最小化这个损失。

10. AdamW优化器:这是用于模型训练的优化器,根据损失函数计算得到的梯度来更新模型的参数,以改进模型的预测性能。

整个流程涵盖了从输入文本到预测结果的转换,以及基于真实标签的模型训练过程。  

 

关键代码说明:

0、模型初始化:

self.model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels) 这个代码作用?

这行代码的作用是加载并初始化一个预训练的BERT模型,用于序列分类任务。具体来说,它执行以下操作:

1. 加载预训练模型:BertForSequenceClassification.from_pretrained(model_name) 通过指定的模型名称 model_name(例如 'bert-base-uncased' 或其他BERT模型),从预训练模型的仓库中加载模型。这个方法会自动下载模型的权重和配置(如果本地没有缓存的话),并将其加载到 BertForSequenceClassification 类的实例中。

2. 指定分类任务的标签数量:num_labels=num_labels 参数指定了模型用于序列分类任务的标签数量。例如,如果你的任务是二分类问题,num_labels 应该设置为2。这个参数决定了模型输出层的维度,使得模型的输出可以对应到指定数量的类别上。

BertForSequenceClassification 是Hugging Face的Transformers库中的一个类,专门用于处理序列分类任务,如情感分析、文本分类等。这个类在BERT的基础上添加了一个顶层的线性层,用于根据BERT的输出进行分类。

总之,这行代码通过加载预训练的BERT模型并根据指定的类别数量调整输出层,创建了一个用于序列分类任务的模型实例。这使得我们可以利用BERT强大的语言表示能力来解决具体的分类问题,同时享受到迁移学习带来的优势。

 

 

1、文本编码:

        input_ids = []
        attention_masks = []
        for text in texts:
            encoded_dict = self.tokenizer.encode_plus(
                text,
                add_special_tokens=True,
                max_length=max_length,
                pad_to_max_length=True,
                return_attention_mask=True,
                return_tensors='pt',
            )
            input_ids.append(encoded_dict['input_ids'])
            attention_masks.append(encoded_dict['attention_mask'])    

这段代码是 TextClassifier 类中 prepare_data 方法的一部分,负责将文本数据编码为模型可以理解的格式。具体来说,它执行以下步骤:

1. 初始化列表: 首先,初始化两个空列表 input_ids 和 attention_masks。这两个列表将用于存储所有文本的编码信息和注意力掩码。

2. 遍历文本: 然后,代码遍历传入的 texts 列表,对每个文本进行处理。texts 是一个字符串列表,每个字符串代表一个待分类的文本。

3. 文本编码: 对于每个文本,使用BERT分词器的 encode_plus 方法进行编码。这个方法将文本转换为模型需要的输入格式,包括:

  • 添加特殊标记: 通过 add_special_tokens=True 参数,自动在每个文本前后添加特殊标记(如CLS和SEP),这对于BERT模型理解句子的开始和结束很重要。
  • 最大长度限制: 通过 max_length 参数限制编码后的长度,超出这个长度的部分将被截断,不足的部分将通过填充来达到这个长度。
  • 填充: 通过 pad_to_max_length=True 参数,确保所有文本编码后的长度一致,不足 max_length 的部分会被填充。
  • 返回注意力掩码: 通过 return_attention_mask=True 参数,生成一个掩码来指示哪些部分是真实文本,哪些部分是填充的。这对模型正确处理输入很重要。
  • 返回张量: 通过 return_tensors='pt' 参数,将编码后的数据转换为PyTorch张量(Tensor),以便直接用于模型训练。

4. 收集编码结果: 将每个文本的 input_ids(编码后的输入ID)和 attention_masks(注意力掩码)分别添加到之前初始化的列表中。

通过这个过程,每个文本被转换成了模型可以处理的格式,包括它的编码ID和对应的注意力掩码,为后续的模型训练和预测准备好了输入数据。

 

 

使用BERT分词器的 encode_plus 方法进行编码,举一个实际例子说明。

假设我们使用BERT分词器对文本 "Hello, BERT!" 进行编码。这里展示的是一个简化的例子,以说明 encode_plus 方法的基本用法和输出。

from transformers import BertTokenizer
 
# 初始化分词器,这里以 'bert-base-uncased' 为例
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
 
# 待编码的文本
text = "Hello, BERT!"
 
# 使用encode_plus方法进行编码
encoded_dict = tokenizer.encode_plus(
    text,
    add_special_tokens=True,  # 添加特殊标记
    max_length=10,  # 设定最大长度
    pad_to_max_length=True,  # 进行填充
    return_attention_mask=True,  # 返回注意力掩码
    return_tensors='pt',  # 返回PyTorch张量
)
 
# 打印编码结果
print("Input IDs:", encoded_dict['input_ids'])
print("Attention Mask:", encoded_dict['attention_mask'])
 

输出可能如下(具体的ID可能会有所不同,取决于分词器的版本和配置):

Input IDs: tensor([[  101,  7592,  1010, 14324,   999,   102,     0,     0,     0,     0]])
Attention Mask: tensor([[1, 1, 1, 1, 1, 1, 0, 0, 0, 0]])
 

这里的输出解释如下:

  • Input IDs: 是文本 "Hello, BERT!" 经过编码后的数字序列。101 和 102 分别是特殊标记 [CLS] 和 [SEP] 的ID,它们分别在编码序列的开始和结束位置添加。7592, 1010, 14324, 999 分别对应 "hello", ",", "bert", "!" 的编码ID。剩余的 0 是因为我们设置了 max_length=10 并启用了填充,所以不足长度的部分被填充了 0。
  • Attention Mask: 是一个与Input IDs相同长度的掩码,用于指示哪些ID是真实文本的一部分(用 1 表示),哪些是填充的部分(用 0 表示)。在这个例子中,前6个位置是真实文本(包括特殊标记),因此对应的是 1,而后4个位置是填充的,因此是 0。

这个例子展示了如何使用BERT分词器的 encode_plus 方法对文本进行编码,以及如何理解编码后的结果。

 

在我们的实际代码中,

如何配置防火墙产品?这几个字对应的编码如下(是不是发现中文的字符是一一对应的ID,包括了标点符号):

[tensor([[ 101, 1963, 862, 6981, 5390, 7344, 4125, 1870, 772, 1501, 8043, 102,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0]])]

 

2、格式化数据:

         input_ids = torch.cat(input_ids, dim=0)
        attention_masks = torch.cat(attention_masks, dim=0)
        labels = torch.tensor(labels)
        return TensorDataset(input_ids, attention_masks, labels)    

这几行代码的作用是将处理过的输入ID、注意力掩码和标签整合成一个PyTorch的 TensorDataset 对象,以便于后续的数据加载和模型训练。具体步骤如下:

1. 合并输入ID 1: torch.cat(input_ids, dim=0) 将之前收集的所有文本的输入ID列表(每个文本的输入ID是一个张量)沿着第一个维度(dim=0)合并成一个大的张量。这样,每一行代表一个文本的输入ID。

2. 合并注意力掩码 (attention_masks): torch.cat(attention_masks, dim=0) 的作用与合并输入ID类似,它将所有文本的注意力掩码沿着第一个维度合并成一个大的张量。每一行代表一个文本的注意力掩码。

3. 转换标签 (labels): torch.tensor(labels) 将标签列表转换成一个PyTorch张量。这个张量包含了每个文本对应的标签,用于训练过程中的监督学习。

4. 创建 TensorDataset: TensorDataset(input_ids, attention_masks, labels) 利用合并后的输入ID张量、注意力掩码张量和标签张量创建一个 TensorDataset 对象。TensorDataset 是PyTorch中的一个便利类,用于将多个张量打包成一个数据集,使得在数据加载器(DataLoader)中使用时能够方便地同时访问所有相关数据。

总之,这几行代码将编码后的文本数据(包括输入ID和注意力掩码)和对应的标签整合成一个格式化的数据集,为模型的训练和评估准备好了数据。

 

3、模型训练:

            total_loss = 0
            self.model.train()
            for step, batch in enumerate(dataloader):
                b_input_ids, b_input_mask, b_labels = batch
                self.model.zero_grad()
                outputs = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels)
                loss = outputs.loss
                total_loss += loss.item()
                loss.backward()
                self.optimizer.step()
            avg_train_loss = total_loss / len(dataloader)
            print(f"Average training loss: {avg_train_loss:.2f}")      

这段代码位于 TextClassifier 类的 train 方法中,它实现了模型的训练过程。具体步骤和作用如下:

1. 初始化总损失 (total_loss = 0): 在训练开始前,将总损失初始化为0。这个变量用于累计一个epoch内所有批次的损失,以便计算平均损失。

2. 设置模型为训练模式 (self.model.train()): 通过调用 train() 方法,将模型设置为训练模式。这对于某些模型组件(如Dropout和BatchNorm)是必要的,因为它们在训练和评估时的行为是不同的。

3. 遍历数据加载器 (for step, batch in enumerate(dataloader)): 使用 enumerate 遍历训练数据加载器(DataLoader)中的所有批次。每个批次包含了一组文本的输入ID、注意力掩码和标签。

4. 准备批次数据: 从每个批次中解包输入ID (b_input_ids)、注意力掩码 (b_input_mask) 和标签 (b_labels)。

5. 梯度归零 (self.model.zero_grad()): 在进行反向传播之前,先将模型参数的梯度清零。这是因为默认情况下梯度是累加的,不清零的话会影响到下一批次的梯度计算。

6. 前向传播: 通过模型进行前向传播,传入输入ID、注意力掩码和标签。这里没有传入 token_type_ids 参数,因为对于单句分类任务通常不需要。模型返回的 outputs 包含了多个输出,其中 outputs.loss 是这个批次的损失。

7. 累计损失 (total_loss += loss.item()): 将当前批次的损失加到总损失上。loss.item() 将损失值从张量转换为Python数值。

  • 反向传播 (loss.backward()): 对损失进行反向传播,计算模型参数的梯度。

9. 优化器步进 (self.optimizer.step()): 调用优化器的 step 方法来更新模型的参数,根据计算出的梯度进行一次参数更新。

10. 计算平均训练损失: 在遍历完所有批次后,计算这个epoch的平均训练损失,即总损失除以批次数量。

11. 打印平均训练损失: 打印出这个epoch的平均训练损失,以便监控训练过程。

总之,这段代码实现了模型的一个训练周期(epoch),包括前向传播、损失计算、反向传播和参数更新,以及监控训练损失的功能。  

 

注意:

 1、       optimizer.zero_grad() ==》这个梯度归零是模型参数归零吗?

不是的。optimizer.zero_grad() 这行代码并不是将模型参数归零,而是将模型参数的梯度归零。在PyTorch中,模型的每个参数都有与之对应的梯度,这个梯度用于在训练过程中通过反向传播算法更新参数值。

在每次训练迭代中,模型的前向传播会计算出损失,然后通过反向传播计算损失函数相对于每个参数的梯度。这些梯度累积存储在参数的 .grad 属性中。如果不手动清零,那么下一次反向传播时计算出的梯度会和前一次的梯度累加起来,导致训练过程出错。

因此,optimizer.zero_grad() 的作用是在每次训练迭代开始前清除累积的梯度,确保每次迭代的梯度计算都是基于最新的前向传播结果,而不是之前迭代的残留梯度。这是正确进行模型训练的重要步骤。

2、outputs = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask, labels=b_labels) 这行代码是什么作用

这行代码是在训练循环中,用于执行模型的前向传播和计算损失的关键步骤。具体来说,它的作用如下:

1. 前向传播:通过将输入数据(b_input_ids 和 b_input_mask)传递给模型 (self.model),执行前向传播过程。这里的 b_input_ids 是批次中每个文本输入的编码形式,b_input_mask 是对应的注意力掩码,用于指示模型哪些位置是实际的输入,哪些位置是填充的。token_type_ids 在这个调用中被设置为 None,这是因为对于单句输入或者不需要区分两个句子的场景,token_type_ids 不是必需的。

2. 计算损失:在这个调用中,还传入了 labels=b_labels,其中 b_labels 是当前批次中每个输入文本对应的标签。当 labels 参数被提供给模型时,除了执行前向传播外,模型还会根据预测结果和真实标签计算损失。这个损失是模型训练过程中需要最小化的目标。

3. 获取输出:调用返回的 outputs 包含了多个输出信息,其中最重要的是损失值(outputs.loss),它表示当前批次的平均损失。如果模型配置为输出其他信息(如分类任务中的 logits),这些信息也可以从 outputs 中获取。

总结来说,这行代码通过执行模型的前向传播来计算每个输入的预测结果,同时基于预测结果和真实标签计算损失,这个损失将用于后续的反向传播和模型参数的更新。这是模型训练过程中的核心步骤之一。

 

4、with torch.no_grad()  作用

with torch.no_grad() 是一个上下文管理器,用于在代码块内部暂时禁用梯度计算。在PyTorch中,这是一种常用的做法,特别是在模型评估(如验证和测试)或进行推理(预测)时,因为在这些场景下我们通常不需要计算梯度。

使用 with torch.no_grad() 的好处包括:

1. 减少内存消耗:由于不需要计算和存储梯度,这可以显著减少内存的使用,使得在资源有限的设备上运行大型模型变得更加可行。

2. 提高计算效率:禁用梯度计算可以减少计算的负担,从而加快模型的推理速度。

示例用法如下:

with torch.no_grad():
    # 在这个代码块中,所有的计算都不会跟踪梯度
    outputs = model(input_data)
    # 可以进行评估或推理操作,而不会影响梯度
 

在这个上下文管理器的作用域内,所有的Tensor操作都不会跟踪历史,因此不会计算梯度。这对于评估和推理阶段是非常有用的,因为在这些阶段我们只关心模型的输出,而不需要进行反向传播。

 

5、模型预测:

                 b_input_ids, b_input_mask, b_labels = batch
                outputs = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask)
                logits = outputs.logits
                predictions = torch.argmax(logits, dim=1)
                total += b_labels.size(0)
                correct += (predictions == b_labels).sum().item()       

这段代码是在模型评估或测试阶段使用的,用于计算模型在给定数据批次上的准确率。具体步骤如下:

1. 解包批次数据:b_input_ids, b_input_mask, b_labels = batch 从当前批次中解包出输入ID (b_input_ids)、注意力掩码 (b_input_mask) 和真实标签 (b_labels)。

2. 模型前向传播:outputs = self.model(b_input_ids, token_type_ids=None, attention_mask=b_input_mask) 通过模型进行前向传播,传入输入ID和注意力掩码。这里没有使用 token_type_ids 参数,因为它主要用于处理两个句子的输入(如问答任务),在单句子分类任务中通常不需要。

3. 获取预测结果:logits = outputs.logits 从模型输出中提取logits。Logits是模型最后一层的原始输出,通常需要通过softmax函数转换成概率。

4. 计算预测标签:predictions = torch.argmax(logits, dim=1) 使用 torch.argmax 函数沿着维度1(列)找到logits中的最大值的索引,这些索引即为模型预测的标签。

5. 累计总样本数:total += b_labels.size(0) 更新总样本数,b_labels.size(0) 返回当前批次的样本数。

6. 计算正确预测的数量:correct += (predictions == b_labels).sum().item() 通过比较预测标签和真实标签是否相等,计算出正确预测的数量,并累加到 correct 变量中。(predictions == b_labels) 生成一个布尔张量,表示每个样本是否被正确分类,.sum().item() 将这个布尔张量中的True值(即正确预测)求和,转换为Python的标量。

总之,这段代码通过模型对一批数据进行预测,并计算模型在这批数据上的准确性(即正确预测的样本数占总样本数的比例)。这是评估模型性能的常用方法之一。  

注意:

Logits本身是模型最后一层的输出,通常表示为未经归一化的预测值,它们不是概率分布,因此它们的和不一定为1。要将logits转换为概率分布(即使得各类别的预测概率之和为1),通常会通过softmax函数进行处理。

Softmax函数作用于logits上,按如下方式计算每个类别的概率:

softmax(��)=���∑����softmax(zi)=jezjezi

其中,��zi 是logits向量中第 i 个元素的值,分母是对所有logits元素应用指数函数后的值的总和。这样处理后,每个元素的值都被压缩到了(0, 1)区间内,且所有元素的值之和为1,从而可以被解释为概率分布。

 

最后补充,如果要包含参数冻结,则修改代码如下:

class TextClassifier:
    def __init__(self, model_name=bge_model_name, num_labels=2, need_freeze=False):
        self.tokenizer = BertTokenizer.from_pretrained(model_name)
        self.model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels)
        if need_freeze:
            # 冻结预训练模型的所有参数
            for param in self.model.base_model.parameters():
                param.requires_grad = False
            # 创建优化器,注意这里我们只传递requires_grad=True的参数
            self.optimizer = AdamW(filter(lambda p: p.requires_grad, self.model.parameters()), lr=2e-5)
        else:
            self.optimizer = AdamW(self.model.parameters(), lr=2e-5) 

 

就我自己的代码看,冻结参数后模型精度还是没有不冻结参数的时候高!

 

posted @ 2024-04-03 15:37  bonelee  阅读(800)  评论(0编辑  收藏  举报