目录
1 遗留问题:
在上个章节中我们介绍了文本相似度任务及基于交互策略的实战流程,但是针对单个可变的句子A和固定集合{B,C,D,......Z}中哪个句子的相似度最高这个问题,如果仍然采用交互策略会导致效率低下的问题。
2 向量匹配策略:
向量匹配策略是一种用于计算文本相似度的方法,它通过将文本转化为向量表示,然后使用数学方法(如余弦相似度、欧氏距离等)来比较这些向量,从而评估文本之间的相似度。这一策略广泛应用于自然语言处理(NLP)领域,尤其是在文本相似度、信息检索、推荐系统等任务中。
首先把候选集合中所有句子的向量表示拿到并存储,当新的用户问题A来时,计算相似度时只需重新计算一次可变文本A的向量表示,然后跑一下模型匹配即可,显著提高了效率。
3 数据预处理:
还是要以句子对的方式处理数据
CosineEmbeddingLoss 是一种用于评估两个输入样本之间相似性(或距离)的损失函数。它特别适用于比较两个向量的相似性,广泛应用于嵌入学习和推荐系统中。Cosine Embedding Loss 主要用于处理二分类问题,尤其是在对比学习和相似性学习任务中。
可以把margin理解为一个阈值,低于margin以下的cos值视为简单样本,loss为0。使用 margin 可以帮助模型在判断相似性和不相似性时,有一个明确的阈值,使得模型对输入的微小变化不那么敏感。通过调整 margin 的值,可以控制模型对正样本和负样本的区分度,优化模型性能。
4 基于向量匹配策略的实战流程
4.1 导包
4.2 加载数据集
4.3 划分数据集
4.4 数据集预处理
按照之前的处理策略,需要合并为句子对(不同于之前的简单合并为一个句子),并按照CosineEmbeddingLoss的定义对标签进行处理
import torch
tokenizer = AutoTokenizer.from_pretrained("hfl/chinese-macbert-base")
def process_function(examples):
sentences = []
labels = []
for sen1, sen2, label in zip(examples["sentence1"], examples["sentence2"], examples["label"]):
sentences.append(sen1)
sentences.append(sen2)
labels.append(1 if int(label) == 1 else -1)
# input_ids, attention_mask, token_type_ids
tokenized_examples = tokenizer(sentences, max_length=128, truncation=True, padding="max_length")
tokenized_examples = {k: [v[i: i + 2] for i in range(0, len(v), 2)] for k, v in tokenized_examples.items()}
tokenized_examples["labels"] = labels
return tokenized_examples
tokenized_datasets = datasets.map(process_function, batched=True, remove_columns=datasets["train"].column_names)
tokenized_datasets
4.5 创建模型
huggingface中没有针对这种任务的预定义模型,因此需要我们自己实现
可以参考之前的BertForSequenceClassification进行编写
from transformers import BertForSequenceClassification, BertPreTrainedModel, BertModel
from typing import Optional
from transformers.configuration_utils import PretrainedConfig
from torch.nn import CosineSimilarity, CosineEmbeddingLoss
class DualModel(BertPreTrainedModel):
def __init__(self, config: PretrainedConfig, *inputs, **kwargs):
super().__init__(config, *inputs, **kwargs)
self.bert = BertModel(config)
self.post_init()
def forward(
self,
input_ids: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
token_type_ids: Optional[torch.Tensor] = None,
position_ids: Optional[torch.Tensor] = None,
head_mask: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
labels: Optional[torch.Tensor] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
):
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
# Step1 分别获取sentenceA 和 sentenceB的输入
senA_input_ids, senB_input_ids = input_ids[:, 0], input_ids[:, 1]
senA_attention_mask, senB_attention_mask = attention_mask[:, 0], attention_mask[:, 1]
senA_token_type_ids, senB_token_type_ids = token_type_ids[:, 0], token_type_ids[:, 1]
# Step2 分别获取sentenceA 和 sentenceB的向量表示
senA_outputs = self.bert(
senA_input_ids,
attention_mask=senA_attention_mask,
token_type_ids=senA_token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
senA_pooled_output = senA_outputs[1] # [batch, hidden]
senB_outputs = self.bert(
senB_input_ids,
attention_mask=senB_attention_mask,
token_type_ids=senB_token_type_ids,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
senB_pooled_output = senB_outputs[1] # [batch, hidden]
# step3 计算相似度
cos = CosineSimilarity()(senA_pooled_output, senB_pooled_output) # [batch, ]
# step4 计算loss
loss = None
if labels is not None:
loss_fct = CosineEmbeddingLoss(0.3)
loss = loss_fct(senA_pooled_output, senB_pooled_output, labels)
output = (cos,)
return ((loss,) + output) if loss is not None else output
model = DualModel.from_pretrained("hfl/chinese-macbert-base")
4.6 创建评估函数
余弦判断相似时,一般不用0.5作为阈值,余弦相似度为 0.5 表示两个向量之间的夹角大约是 60 度。在这个夹角下,它们并不是非常相似,因此将 0.5 作为阈值可能会导致误判。通常将大于 0.7 或 0.8 的余弦相似度作为两个向量相似的判断依据。
另外二分类计算F1分数时标签只能是0和1,所以需要将之前的-1转换为0
4.7 创建TrainingArguments
4.8 创建Trainer
这里不需要使用DataCollator了,因为我们在之前数据处理的时候已经手动补齐了,不指定的情况下会有一个默认的将其转换为tensor类型。
4.9 模型训练
4.10 模型评估
4.11 模型预测
这块也要手写
class SentenceSimilarityPipeline:
def __init__(self, model, tokenizer) -> None:
self.model = model.bert
self.tokenizer = tokenizer
self.device = model.device
def preprocess(self, senA, senB):
return self.tokenizer([senA, senB], max_length=128, truncation=True, return_tensors="pt", padding=True)
def predict(self, inputs):
inputs = {k: v.to(self.device) for k, v in inputs.items()}
return self.model(**inputs)[1] # [2, 768]
def postprocess(self, logits):
cos = CosineSimilarity()(logits[None, 0, :], logits[None,1, :]).squeeze().cpu().item()
return cos
def __call__(self, senA, senB, return_vector=False):
inputs = self.preprocess(senA, senB)
logits = self.predict(inputs)
result = self.postprocess(logits)
if return_vector:
return result, logits
else:
return result
这块就可以得到句子的向量表示了,也就可以实现我们之前的那种逻辑了。 本次章节,我们只是利用双塔模型拿到了输入的向量表示,但是没做向量库的搭建。这块我们将在下个章节给出。
不同于之前的交互策略-单塔模型(A和B合起来只过了一次Bert),我们这次采用的是向量匹配-双塔策略(A和B为句子对分别过了一次Bert);前者的优点在于准确率高,效果好,缺点就是推理效率很低,后者恰恰相反。
当然也有一些更加有效快捷的工具: