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,从而可以被解释为概率分布。
标签:loss,BGE,示例,模型,labels,ids,意图,input,self From: https://www.cnblogs.com/bonelee/p/18112823