Sentence-BERT实现文本匹配【CoSENT损失】

news/2024/9/18 14:51:46/ 标签: bert, 人工智能, 深度学习

引言

还是基于Sentence-BERT架构,或者说Bi-Encoder架构,但是本文使用的是苏神提出的CoSENT损失函数1

点击来都是缘分,之前过时的方法可以不细看,别的文章可以不收藏,现在是最流行的方法,这篇文章建议收藏!

架构

image-20210923000654664

正如苏神所说的,参考了Circle Loss2理论,这里尝试详细展开一下。

绝大多数损失函数,都在拉近相似的句子对,推远不相似的句子对,即最大化类内相似性( s p s_p sp)同时最小化类间相似性( s n s_n sn)。综合起来,实际上在减少 s n − s p s_n -s_p snsp,增加 s p s_p sp等同于减少 s n s_n sn

这里我们还是用余弦相似度来衡量这个相似性,记 Ω p o s \Omega_{pos} Ωpos为所有正样本对(标签为1的样本对)集合, Ω n e g \Omega_{neg} Ωneg为所有负样本对(标签为0的样本对)的集合,所以我们希望任意的第 i i i个正样本对 i ∈ Ω p o s i \in \Omega_{pos} iΩpos和任意的第 j j j个负样本对 j ∈ Ω n e g j \in \Omega_{neg} jΩneg都有:
cos ⁡ ( u i , v i ) > cos ⁡ ( u j , v j ) (1) \cos(\pmb u_i,\pmb v_i) > \cos(\pmb u_j, \pmb v_j) \tag 1 cos(ui,vi)>cos(uj,vj)(1)
其中 u , v \pmb u,\pmb v u,v​都是句向量。这里我们只希望正样本对的相似性要大于负样本对的相似性,具体大多少由模型自己决定,即这里只是判断一个相对顺序而不是具体的值。

这里我们希望减少下式:
cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) (2) \cos(\pmb u_j, \pmb v_j) - \cos(\pmb u_i,\pmb v_i) \tag 2 cos(uj,vj)cos(ui,vi)(2)
记住这种表达形式。我们再来看交叉熵损失。

image-20240905155120594

上图是Softmax CrossEntropy Loss的图示,它应用于单标签分类中, s s s是logits。

我们来回顾下这个损失函数的公式(假设 y i = 1 y j = 0 ∀ j ≠ i y_i=1 \,\, y_j = 0 \,\, \forall j \neq i yi=1yj=0j=i):
L = − y i log ⁡ p i = − log ⁡ ( e s i ∑ j e s j ) = − log ⁡ ( e s i ⋅ e − s i e − s i ⋅ ∑ j e s j ) = − log ⁡ ( 1 ∑ j e s j − s i ) = log ⁡ ( ∑ j e s j − s i ) = log ⁡ ( 1 + ∑ j , j ≠ i e s j − s i ) (3) \begin{aligned} \mathcal L &= - y_i \log p_i \\ &= -\log \left( \frac{e^{s_i}}{\sum_j e^{s_j}}\right) \\ &= -\log \left( \frac{e^{s_i} \cdot e^{ -s_i}}{e^{ -s_i} \cdot \sum_j e^{s_j}}\right) \\ &= -\log \left( \frac{1}{\sum_j e^{s_j - s_i}}\right) \\ &= \log \left( \sum_j e^{s_j - s_i}\right) \\ &= \log \left(1 + \sum_{j, j\neq i} e^{s_j - s_i}\right) \end{aligned} \tag 3 L=yilogpi=log(jesjesi)=log(esijesjesiesi)=log(jesjsi1)=log(jesjsi)=log 1+j,j=iesjsi (3)
最后一步将 e s i − s i e^{s_i - s_i} esisi拿到求和符号外面来了,表达了希望减小 s j − s i s_j -s_i sjsi的意思。

用于多分类任务时,假设有很多个类别,但只有一个类别取值为1,其他取值为0。多分类任务时这里的 s s s为logits。注意我们这里希望 s i s_i si越大越好,要比其他的 s j s_j sj要大。

同时,假如我们用 s s s表示一个句子对之间的相似度,即 s = cos ⁡ ( u , v ) s = \cos(\pmb u, \pmb v) s=cos(u,v)

结合式子(2)我们可以得到一个损失:
log ⁡ ( 1 + ∑ i ∈ Ω p o s , j ∈ Ω n e g e s j − s i ) = log ⁡ ( 1 + ∑ i ∈ Ω p o s , j ∈ Ω n e g e cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) ) (4) \log \left(1 + \sum_{i \in \Omega_{pos}, j \in \Omega_{neg}} e^{s_j - s_i}\right) = \log \left(1 + \sum_{i \in \Omega_{pos}, j \in \Omega_{neg}} e^{\cos(\pmb u_j,\pmb v_j)- \cos(\pmb u_i,\pmb v_i)}\right) \tag 4 log 1+iΩpos,jΩnegesjsi =log 1+iΩpos,jΩnegecos(uj,vj)cos(ui,vi) (4)
然后类似Circle Loss,增加一个超参数 λ > 0 \lambda >0 λ>0,就得到了最终的CoSENT Loss表达式:
log ⁡ ( 1 + ∑ i ∈ Ω p o s , j ∈ Ω n e g e λ ( cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) ) ) (5) \log \left(1 + \sum_{i \in \Omega_{pos}, j \in \Omega_{neg}} e^{\lambda (\cos(\pmb u_j,\pmb v_j)- \cos(\pmb u_i,\pmb v_i))}\right) \tag 5 log 1+iΩpos,jΩnegeλ(cos(uj,vj)cos(ui,vi)) (5)
这里 λ \lambda λ默认等于 20 20 20,相当于除以温度系数 0.05 0.05 0.05

理论部分完毕,现在来看实现。

实现

实现采用类似Huggingface的形式,每个文件夹下面有一种模型。分为modelingargumentstrainer等不同的文件。不同的架构放置在不同的文件夹内。

modeling.py:

from dataclasses import dataclassimport torch
from torch import Tensor, nnfrom transformers.file_utils import ModelOutputfrom transformers import (AutoModel,AutoTokenizer,
)import numpy as np
from tqdm.autonotebook import trange
from typing import Optionalimport torch.nn.functional as F@dataclass
class BiOutput(ModelOutput):loss: Optional[Tensor] = Nonescores: Optional[Tensor] = Noneclass SentenceBert(nn.Module):def __init__(self,model_name: str,trust_remote_code: bool = True,max_length: int = None,scale: float = 20.0,pooling_mode: str = "mean",normalize_embeddings: bool = False,) -> None:super().__init__()self.model_name = model_nameself.normalize_embeddings = normalize_embeddingsself.device = "cuda" if torch.cuda.is_available() else "cpu"self.tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=trust_remote_code)self.model = AutoModel.from_pretrained(model_name, trust_remote_code=trust_remote_code).to(self.device)self.max_length = max_lengthself.pooling_mode = pooling_modeself.scale = scaledef sentence_embedding(self, last_hidden_state, attention_mask):if self.pooling_mode == "mean":attention_mask = attention_mask.unsqueeze(-1).float()return torch.sum(last_hidden_state * attention_mask, dim=1) / torch.clamp(attention_mask.sum(1), min=1e-9)else:# clsreturn last_hidden_state[:, 0]def encode(self,sentences: str | list[str],batch_size: int = 64,convert_to_tensor: bool = True,show_progress_bar: bool = False,):if isinstance(sentences, str):sentences = [sentences]all_embeddings = []for start_index in trange(0, len(sentences), batch_size, desc="Batches", disable=not show_progress_bar):batch = sentences[start_index : start_index + batch_size]features = self.tokenizer(batch,padding=True,truncation=True,return_tensors="pt",return_attention_mask=True,max_length=self.max_length,).to(self.device)out_features = self.model(**features, return_dict=True)embeddings = self.sentence_embedding(out_features.last_hidden_state, features["attention_mask"])if not self.training:embeddings = embeddings.detach()if self.normalize_embeddings:embeddings = torch.nn.functional.normalize(embeddings, p=2, dim=1)if not convert_to_tensor:embeddings = embeddings.cpu()all_embeddings.extend(embeddings)if convert_to_tensor:all_embeddings = torch.stack(all_embeddings)else:all_embeddings = np.asarray([emb.numpy() for emb in all_embeddings])return all_embeddingsdef compute_loss(self, scores, labels):"""Args:scores : (batch_size)labels : (labels)"""labels = torch.tensor(labels).to(self.device)scores = scores * self.scale# (batch_size, 1) - (1, batch_size)# scores (batch_size, batch_size)scores = scores[:, None] - scores[None, :]# labels (batch_size, batch_size)labels = labels[:, None] < labels[None, :]labels = labels.float()# mask out irrelevant pairs so they are negligible after exp()scores = scores - (1 - labels) * 1e12# append a zero as e^0 = 1scores = torch.cat((torch.zeros(1).to(self.device), scores.view(-1)), dim=0)loss = torch.logsumexp(scores, dim=0)return lossdef forward(self, source, target, labels) -> BiOutput:"""Args:source :target :"""# source_embed (batch_size, embed_dim)source_embed = self.encode(source)# target_embed (batch_size, embed_dim)target_embed = self.encode(target)# scores (batch_size)scores = F.cosine_similarity(source_embed, target_embed)loss = self.compute_loss(scores, labels)return BiOutput(loss, scores)def save_pretrained(self, output_dir: str):state_dict = self.model.state_dict()state_dict = type(state_dict)({k: v.clone().cpu().contiguous() for k, v in state_dict.items()})self.model.save_pretrained(output_dir, state_dict=state_dict)

整个模型的实现放到modeling.py文件中。

def compute_loss(self, scores, labels):"""Args:scores : (batch_size)labels : (labels)"""labels = torch.tensor(labels).to(self.device)scores = scores * self.scale# (batch_size, 1) - (1, batch_size)# scores (batch_size, batch_size)scores = scores[:, None] - scores[None, :]# labels (batch_size, batch_size)labels = labels[:, None] < labels[None, :]labels = labels.float()# mask out irrelevant pairs so they are negligible after exp()scores = scores - (1 - labels) * 1e12# append a zero as e^0 = 1scores = torch.cat((torch.zeros(1).to(self.device), scores.view(-1)), dim=0)loss = torch.logsumexp(scores, dim=0)return loss

由于compute_loss这部分还有点复杂,这里也展开分析一下。首先我们回顾一下公式(5):
log ⁡ ( 1 + ∑ i ∈ Ω p o s , j ∈ Ω n e g e λ ( cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) ) ) = log ⁡ ( e 0 + ∑ i ∈ Ω p o s , j ∈ Ω n e g e λ ( cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) ) ) \log \left(1 + \sum_{i \in \Omega_{pos}, j \in \Omega_{neg}} e^{\lambda (\cos(\pmb u_j,\pmb v_j)- \cos(\pmb u_i,\pmb v_i))}\right) = \log \left( e^0 + \sum_{i \in \Omega_{pos}, j \in \Omega_{neg}} e^{\lambda (\cos(\pmb u_j,\pmb v_j)- \cos(\pmb u_i,\pmb v_i))} \right ) log 1+iΩpos,jΩnegeλ(cos(uj,vj)cos(ui,vi)) =log e0+iΩpos,jΩnegeλ(cos(uj,vj)cos(ui,vi))
以一个例子来分析这个函数:

import torch
from torch import Tensor
import torch.nn.functional as F
from transformers import set_seedset_seed(0)batch_size = 6
embedding_dim = 64
# 随机初始化
source, target = torch.randn((batch_size, embedding_dim)), torch.randn((batch_size, embedding_dim))
# 定义标签, 1表示相似, 0表示不相似
labels = torch.tensor([0, 1, 1, 0, 1, 0])

这里假设批次内有6对样本,设置了每对样本的标签。

scores = F.cosine_similarity(source, target)
print(scores)
tensor([-0.0816, -0.1727, -0.2052,  0.0240,  0.2252,  0.0084])

计算对内的余弦相似度得分。

scores = scores * 20
scores
tensor([-1.6312, -3.4543, -4.1032,  0.4800,  4.5039,  0.1671])

乘上缩放因子 λ \lambda λ

# (batch_size, 1) - (1, batch_size)
# scores (batch_size, batch_size)
# 负例减正例的差值
scores = scores[:, None] - scores[None, :]
scores
tensor([[ 0.0000,  1.8231,  2.4720, -2.1113, -6.1351, -1.7984],[-1.8231,  0.0000,  0.6489, -3.9343, -7.9582, -3.6214],[-2.4720, -0.6489,  0.0000, -4.5832, -8.6071, -4.2703],[ 2.1113,  3.9343,  4.5832,  0.0000, -4.0238,  0.3129],[ 6.1351,  7.9582,  8.6071,  4.0238,  0.0000,  4.3367],[ 1.7984,  3.6214,  4.2703, -0.3129, -4.3367,  0.0000]])

scores[:, None]结果是一个(batch_size, 1)的张量,经过广播(按列广播)会变成(batch_size, batch_size)

tensor([[-1.6312, -1.6312, -1.6312, -1.6312, -1.6312, -1.6312],[-3.4543, -3.4543, -3.4543, -3.4543, -3.4543, -3.4543],[-4.1032, -4.1032, -4.1032, -4.1032, -4.1032, -4.1032],[ 0.4800,  0.4800,  0.4800,  0.4800,  0.4800,  0.4800],[ 4.5039,  4.5039,  4.5039,  4.5039,  4.5039,  4.5039],[ 0.1671,  0.1671,  0.1671,  0.1671,  0.1671,  0.1671]])

scores[None, :]结果是一个(1, batch_size)的张量,经过广播(按行广播)会变成(batch_size, batch_size)

tensor([[ 0.0000,  1.8231,  2.4720, -2.1113, -6.1351, -1.7984],[-1.8231,  0.0000,  0.6489, -3.9343, -7.9582, -3.6214],[-2.4720, -0.6489,  0.0000, -4.5832, -8.6071, -4.2703],[ 2.1113,  3.9343,  4.5832,  0.0000, -4.0238,  0.3129],[ 6.1351,  7.9582,  8.6071,  4.0238,  0.0000,  4.3367],[ 1.7984,  3.6214,  4.2703, -0.3129, -4.3367,  0.0000]])

第一个减去第二个刚好也得:

tensor([[ 0.0000,  1.8231,  2.4720, -2.1113, -6.1351, -1.7984],[-1.8231,  0.0000,  0.6489, -3.9343, -7.9582, -3.6214],[-2.4720, -0.6489,  0.0000, -4.5832, -8.6071, -4.2703],[ 2.1113,  3.9343,  4.5832,  0.0000, -4.0238,  0.3129],[ 6.1351,  7.9582,  8.6071,  4.0238,  0.0000,  4.3367],[ 1.7984,  3.6214,  4.2703, -0.3129, -4.3367,  0.0000]])

实际上是计算原scores列表第j个元素(语句对的相似度)减去第i个元素(语句对的相似度)的差值,对应上面矩阵的[j,i]处,即 cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) \cos(\pmb u_j,\pmb v_j)- \cos(\pmb u_i,\pmb v_i) cos(uj,vj)cos(ui,vi)

我们可以画图感受一下:

import numpy as np
import seaborn as sns
import matplotlib.pyplot as pltscores_np = scores.numpy()# 使用 seaborn 绘制热力图
plt.figure(figsize=(8, 6))
sns.heatmap(scores_np, annot=True, cmap='coolwarm', fmt='.2f', linecolor='white', linewidth=0.1)
plt.title('Scores Matrix')
plt.show()

image-20240905231603345

下面我们关心的是 j ∈ Ω n e g ∧ i ∈ Ω p o s j \in \Omega_{neg} ∧ i \in \Omega_{pos} jΩnegiΩpos的情形。

scores.shape
torch.Size([6, 6])

先确认下形状为(batch_size, batch_size)

labels = labels[:, None] < labels[None, :]
labels = labels.float()
# labels[j][i] 表示是否第j个语句对的标签 是否 小于 第 i 个 
labels
#   j   0    1   2   3   4   5     i  
tensor([[0., 1., 1., 0., 1., 0.],# 0  [0., 0., 0., 0., 0., 0.],# 1 [0., 0., 0., 0., 0., 0.],# 2[0., 1., 1., 0., 1., 0.],# 3[0., 0., 0., 0., 0., 0.],# 4[0., 1., 1., 0., 1., 0.]])#5

第j个语句对的标签小于 第 i 个满足我们的要求: j ∈ Ω n e g ∧ i ∈ Ω p o s j \in \Omega_{neg} ∧ i \in \Omega_{pos} jΩnegiΩpos,也就是说下面矩阵取值为 1 1 1的元素是我们关心的。

image-20240905175625954

我们也画出这个labels矩阵。

scores = scores - (1 - labels) * 1e12

把新矩阵labels=0处的元素减去一个负的比较大的数,负的大的数计算指数后变成0,即我们不关心labels取 0 0 0对应的元素。只关心 j ∈ Ω n e g ∧ i ∈ Ω p o s j \in \Omega_{neg} ∧ i \in \Omega_{pos} jΩnegiΩpos的。

现在scores都是我们关心的值,然后还缺一个 e 0 e^0 e0

scores = torch.cat((torch.zeros(1).to(self.device), scores.view(-1)), dim=0)

log ⁡ ( e 0 + ∑ i ∈ Ω p o s , j ∈ Ω n e g e λ ( cos ⁡ ( u j , v j ) − cos ⁡ ( u i , v i ) ) ) \log \left( e^0 + \sum_{i \in \Omega_{pos}, j \in \Omega_{neg}} e^{\lambda (\cos(\pmb u_j,\pmb v_j)- \cos(\pmb u_i,\pmb v_i))} \right ) log e0+iΩpos,jΩnegeλ(cos(uj,vj)cos(ui,vi))

如上公式所示。

最后加一个logsumexp

loss = torch.logsumexp(scores, dim=0)

得到最终的损失。

完毕。

arguments.py:

from dataclasses import dataclass, field
from typing import Optionalimport os@dataclass
class ModelArguments:model_name_or_path: str = field(metadata={"help": "Path to pretrained model"})config_name: Optional[str] = field(default=None,metadata={"help": "Pretrained config name or path if not the same as model_name"},)tokenizer_name: Optional[str] = field(default=None,metadata={"help": "Pretrained tokenizer name or path if not the same as model_name"},)@dataclass
class DataArguments:train_data_path: str = field(default=None, metadata={"help": "Path to train corpus"})eval_data_path: str = field(default=None, metadata={"help": "Path to eval corpus"})max_length: int = field(default=512,metadata={"help": "The maximum total input sequence length after tokenization for input text."},)def __post_init__(self):if not os.path.exists(self.train_data_path):raise FileNotFoundError(f"cannot find file: {self.train_data_path}, please set a true path")if not os.path.exists(self.eval_data_path):raise FileNotFoundError(f"cannot find file: {self.eval_data_path}, please set a true path")

定义了模型和数据相关参数。

dataset.py:

from torch.utils.data import Dataset
from datasets import Dataset as dt
import pandas as pdfrom utils import build_dataframe_from_csvclass PairDataset(Dataset):def __init__(self, data_path: str) -> None:df = build_dataframe_from_csv(data_path)self.dataset = dt.from_pandas(df, split="train")self.total_len = len(self.dataset)def __len__(self):return self.total_lendef __getitem__(self, index) -> dict[str, str]:query1 = self.dataset[index]["query1"]query2 = self.dataset[index]["query2"]label = self.dataset[index]["label"]return {"query1": query1, "query2": query2, "label": label}class PairCollator:def __call__(self, features) -> dict[str, list[str]]:queries1 = []queries2 = []labels = []for feature in features:queries1.append(feature["query1"])queries2.append(feature["query2"])labels.append(feature["label"])return {"source": queries1, "target": queries2, "labels": labels}

数据集类考虑了LCQMC数据集的格式,即成对的语句和一个数值标签。类似:

Hello.	Hi.	1
Nice to see you.	Nice	0

trainer.py:

import torch
from transformers.trainer import Trainerfrom typing import Optional
import os
import loggingfrom modeling import SentenceBertTRAINING_ARGS_NAME = "training_args.bin"
logger = logging.getLogger(__name__)class BiTrainer(Trainer):def compute_loss(self, model: SentenceBert, inputs, return_outputs=False):outputs = model(**inputs)loss = outputs.lossreturn (loss, outputs) if return_outputs else lossdef _save(self, output_dir: Optional[str] = None, state_dict=None):# If we are executing this function, we are the process zero, so we don't check for that.output_dir = output_dir if output_dir is not None else self.args.output_diros.makedirs(output_dir, exist_ok=True)logger.info(f"Saving model checkpoint to {output_dir}")self.model.save_pretrained(output_dir)if self.tokenizer is not None:self.tokenizer.save_pretrained(output_dir)# Good practice: save your training arguments together with the trained modeltorch.save(self.args, os.path.join(output_dir, TRAINING_ARGS_NAME))

继承🤗 Transformers的Trainer类,重写了compute_loss_save方法。

这样我们就可以利用🤗 Transformers来训练我们的模型了。

utils.py:

import torch
import pandas as pd
from scipy.stats import pearsonr, spearmanr
from typing import Tupledef build_dataframe_from_csv(dataset_csv: str) -> pd.DataFrame:df = pd.read_csv(dataset_csv,sep="\t",header=None,names=["query1", "query2", "label"],)return dfdef compute_spearmanr(x, y):return spearmanr(x, y).correlationdef compute_pearsonr(x, y):return pearsonr(x, y)[0]def find_best_acc_and_threshold(scores, labels, high_score_more_similar: bool):"""Copied from https://github.com/UKPLab/sentence-transformers/tree/master"""assert len(scores) == len(labels)rows = list(zip(scores, labels))rows = sorted(rows, key=lambda x: x[0], reverse=high_score_more_similar)print(rows)max_acc = 0best_threshold = -1# positive examples number so farpositive_so_far = 0# remain negative examplesremaining_negatives = sum(labels == 0)for i in range(len(rows) - 1):score, label = rows[i]if label == 1:positive_so_far += 1else:remaining_negatives -= 1acc = (positive_so_far + remaining_negatives) / len(labels)if acc > max_acc:max_acc = accbest_threshold = (rows[i][0] + rows[i + 1][0]) / 2return max_acc, best_thresholddef metrics(y: torch.Tensor, y_pred: torch.Tensor) -> Tuple[float, float, float, float]:TP = ((y_pred == 1) & (y == 1)).sum().float()  # True PositiveTN = ((y_pred == 0) & (y == 0)).sum().float()  # True NegativeFN = ((y_pred == 0) & (y == 1)).sum().float()  # False NegatvieFP = ((y_pred == 1) & (y == 0)).sum().float()  # False Positivep = TP / (TP + FP).clamp(min=1e-8)  # Precisionr = TP / (TP + FN).clamp(min=1e-8)  # RecallF1 = 2 * r * p / (r + p).clamp(min=1e-8)  # F1 scoreacc = (TP + TN) / (TP + TN + FP + FN).clamp(min=1e-8)  # Accuraryreturn acc, p, r, F1def compute_metrics(predicts, labels):return metrics(labels, predicts)

定义了一些帮助函数,从sentence-transformers库中拷贝了寻找最佳准确率阈值的实现find_best_acc_and_threshold

除了准确率,还计算了句嵌入的余弦相似度与真实标签之间的斯皮尔曼等级相关系数指标。

最后定义训练和测试脚本。

train.py:

from transformers import set_seed, HfArgumentParser, TrainingArgumentsimport logging
from pathlib import Pathfrom datetime import datetimefrom modeling import SentenceBert
from trainer import BiTrainer
from arguments import DataArguments, ModelArguments
from dataset import PairCollator, PairDatasetlogger = logging.getLogger(__name__)
logging.basicConfig(format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",datefmt="%m/%d/%Y %H:%M:%S",level=logging.INFO,
)def main():parser = HfArgumentParser((TrainingArguments, DataArguments, ModelArguments))training_args, data_args, model_args = parser.parse_args_into_dataclasses()# 根据当前时间生成输出目录output_dir = f"{training_args.output_dir}/{model_args.model_name_or_path.replace('/', '-')}-{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}"training_args.output_dir = output_dirlogger.info(f"Training parameters {training_args}")logger.info(f"Data parameters {data_args}")logger.info(f"Model parameters {model_args}")# 设置随机种子set_seed(training_args.seed)# 加载预训练模型model = SentenceBert(model_args.model_name_or_path,trust_remote_code=True,max_length=data_args.max_length,)tokenizer = model.tokenizer# 构建训练和测试集train_dataset = PairDataset(data_args.train_data_path)eval_dataset = PairDataset(data_args.eval_data_path)# 传入参数trainer = BiTrainer(model=model,args=training_args,train_dataset=train_dataset,eval_dataset=eval_dataset,data_collator=PairCollator(),tokenizer=tokenizer,)Path(training_args.output_dir).mkdir(parents=True, exist_ok=True)# 开始训练trainer.train()trainer.save_model()if __name__ == "__main__":main()

训练

基于train.py定义了train.sh传入相关参数:

timestamp=$(date +%Y%m%d%H%M)
logfile="train_${timestamp}.log"# change CUDA_VISIBLE_DEVICES
CUDA_VISIBLE_DEVICES=3 nohup python train.py \--model_name_or_path=hfl/chinese-macbert-large \--output_dir=output \--train_data_path=data/train.txt \--eval_data_path=data/dev.txt \--num_train_epochs=3 \--save_total_limit=5 \--learning_rate=2e-5 \--weight_decay=0.01 \--warmup_ratio=0.01 \--bf16=True \--eval_strategy=epoch \--save_strategy=epoch \--per_device_train_batch_size=64 \--report_to="none" \--remove_unused_columns=False \--max_length=128 \> "$logfile" 2>&1 &

以上参数根据个人环境修改,这里使用的是哈工大的chinese-macbert-large预训练模型。

注意:

  • --remove_unused_columns是必须的。
  • 通过bf16=True可以加速训练同时不影响效果。
  • 其他参数可以自己调整。
100%|██████████| 11193/11193 [46:54<00:00,  4.35it/s]
100%|██████████| 11193/11193 [46:54<00:00,  3.98it/s]
09/05/2024 17:35:20 - INFO - trainer - Saving model checkpoint to output/hfl-chinese-macbert-large-2024-09-05_18-48-21
{'eval_loss': 0.9763002395629883, 'eval_runtime': 56.9409, 'eval_samples_per_second': 154.581, 'eval_steps_per_second': 19.336, 'epoch': 3.0}
{'train_runtime': 2814.5056, 'train_samples_per_second': 254.502, 'train_steps_per_second': 3.977, 'train_loss': 4.296681343023402, 'epoch': 3.0}

这里仅训练了3轮,我们拿最后保存的模型output/hfl-chinese-macbert-large-2024-09-05_18-48-21进行测试。

测试

test.py: 测试脚本见后文的完整代码。

test.sh:

# change CUDA_VISIBLE_DEVICES
CUDA_VISIBLE_DEVICES=0 python test.py \--model_name_or_path=output/hfl-chinese-macbert-large-2024-09-05_18-48-21 \--test_data_path=data/test.txt

输出:

TestArguments(model_name_or_path='output/hfl-chinese-macbert-large-2024-09-05_18-48-21/checkpoint-11193', test_data_path='data/test.txt', max_length=64, batch_size=128)
Batches: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:11<00:00,  8.78it/s]
Batches: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 98/98 [00:11<00:00,  8.86it/s]
max_acc: 0.8940, best_threshold: 0.839080
spearman corr: 0.7989 |  pearson_corr corr: 0.7703 | compute time: 22.26s
accuracy=0.894 precision=0.911 recal=0.874 f1 score=0.8918

测试集上的准确率达到89.4%,spearman系数达到了目前本系列文章的SOTA结果

该方法计算出来的分类阈值0.839080看起来也比之前的更合理。

完整代码

完整代码: →点此←

参考


  1. CoSENT(一):比Sentence-BERT更有效的句向量方案 ↩︎

  2. [论文笔记]Circle Loss: A Unified Perspective of Pair Similarity Optimization ↩︎


http://www.ppmy.cn/news/1524581.html

相关文章

App推广新姿势:Xinstall带你玩转安装页面拉起功能!

在移动互联网时代&#xff0c;App已经成为我们生活中不可或缺的一部分。然而&#xff0c;随着App数量的不断增加&#xff0c;如何让自己的App在众多竞争者中脱颖而出&#xff0c;成为推广者面临的一大难题。今天&#xff0c;我们就来聊聊一个神奇的解决方案——Xinstall&#x…

项目日志——日志器模块一部缓冲区的设计、实现、测试

文章目录 异步缓冲区模块模块设计缓冲区设计单个缓冲区 实现测试 异步缓冲区模块 模块设计 异步日志器的思想是为了避免业务线程因为写日志的过程时间较长而长时间阻塞 异步日志器的工作就是把业务输出的日志内容放入内存缓冲区中&#xff0c;使用专门的线程进行日志写入 这…

C2免杀--手工shellcode编译,shellcode免杀思路

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文主要整理C2免杀中 shellcode代码免杀的相关部分 shellcode概念 我们也不啰嗦&#xff0c;我直接直观的描述一下他。 他就是一串机器能运行的代码&#xff0c;但是他不是正统的python&#xff0c;c&#xff…

vulhub spring 远程命令执行漏洞(CVE-2022-22963)

1.打开环境 进入环境所在的文件夹 docker-compose up -d 一键启动容器 2.浏览器访问环境 3.抓包 http://192.168.10.233:8080/functionRouter进行抓包 抓包修改请求方式 4.修改请求体内容 spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().e…

C++内存布局

文章目录 C内存布局1.文字介绍2.图片介绍3.代码介绍 C内存布局 1.文字介绍 1.内核态空间 2.用户态空间 (1)栈区&#xff1a;存储局部变量和函数调用的相关信息&#xff0c;栈的特点是自动分配和释放&#xff0c;由操作系统管理。栈由高地址向低地址生长&#xff0c;通常为0x…

HTTP 协议的工作过程

当我们在浏览器输入一个网址&#xff0c;此时浏览器就会给对应的服务器发送一个 HTTP 请求&#xff0c;对应的服务器收到这个请求之后&#xff0c;经过计算处理&#xff0c;就会返回一个 HTTP 响应。并且当我们访问一个网站时&#xff0c;可能涉及不止一次的 HTTP 请求和响应的…

C#索引器(Indexer)

索引器(Indexer)允许一个对象可以像数组一样使用下标的方式来访问. 当为类定义一个索引器时,该类的行为就会像一个虚拟数组(virtual array) 一样.可以使用数组访问运算符[]来访问该类的成员. 语法 一维索引器的语法如下: element-type this[int index] { // get 访问器 …

python 实现gamma 伽玛功能算法

gamma 伽玛功能算法介绍 Gamma&#xff08;伽玛&#xff09;功能算法通常与不同的领域和应用相关&#xff0c;包括但不限于图像处理、光学测试、数学计算等。以下是根据您提供的搜索结果&#xff0c;对Gamma伽玛功能算法的一些概述&#xff1a; 在图像处理中的Gamma校正 在图…

简单计算机项目2

if 条件判断 /* * if 如果 */ //条件判断是流程控制中的一部分 //条件判断是为了在指定条件下做指定的事情 int num 1 0; // if (判断条件&#xff09; //{ //满足条件做的事 …

2024.9.12 Python 累加数,子串操作,分割回文串,长度最小的子数组,整数拆分

1.分割回文串 给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是回文串返回 s 所有可能的分割方案。 示例 1&#xff1a; 输入&#xff1a;s “aab” 输出&#xff1a;[[“a”,“a”,“b”],[“aa”,“b”]] 示例 2&#xff1a; 输入&#xff…

《OpenCV计算机视觉》—— 图像形态学(腐蚀、膨胀等)

文章目录 一、图像形态学基本概念二、基本运算1.简单介绍2.代码实现 三、高级运算1.简单介绍2.代码实现 一、图像形态学基本概念 图像形态学是图像处理科学的一个独立分支&#xff0c;它基于集合论和数学形态学的理论&#xff0c;专门用于分析和处理图像中的形状和结构。图像形…

景联文科技:专业扫地机器人数据采集标注服务

景联文科技作为一家专业AI数据采集标注公司&#xff0c;提供高质量数据支持&#xff0c;致力于帮助扫地机器人制造商和研发机构提升产品的智能水平和用户体验。 扫地机器人需要通过大量的环境数据来训练其导航和清洁算法。高质量标注数据是确保机器人在各种环境下高效工作的关键…

即插即用篇 | YOLOv8 引入维度互补注意力混合Transformer模块 | 轻量级互补注意力网络:RAMiT引领图像修复新突破

本改进已同步到YOLO-Magic框架! 摘要:虽然许多近期的研究在图像修复(IR)领域取得了进展,但它们通常存在参数数量过多的问题。另一个问题是,大多数基于Transformer的图像修复方法只关注局部或全局特征,导致感受野有限或参数不足的问题。为了解决这些问题,我们提出了一种…

C++:类与对象——详解继承、多继承、虚继承

1. 继承 优点&#xff1a;减少重复的代码 语法&#xff1a;class 子类 : 继承方式 父类 子类也称为派生类&#xff0c;父类也称为基类 2. 继承方式 公共继承&#xff1a;public保护继承&#xff1a;protected私有继承&#xff1a;private 注意&#xff1a; public继承方…

C++设计模式——Memento备忘录模式

一&#xff0c;备忘录模式的定义 备忘录模式是一种行为型设计模式&#xff0c;它允许将对象的状态保存在外部&#xff0c;并在需要时恢复。 备忘录模式允许在不破坏封装性的前提下&#xff0c;捕获并存储一个对象的内部状态&#xff0c;并在需要时将其恢复到之前的状态。 在…

java设计模式day03--(结构型模式:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式)

5&#xff0c;结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式&#xff0c;前者采用继承机制来组织接口和类&#xff0c;后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低&#xff0c;满足“…

maya无法导出fbx

很多小伙伴使用Maya软件导出fbx格式文件时会提示“无法识别文件类型”&#xff0c;这主要是因为没有开启fbx插件造成的&#xff0c;只要开启fbx插件就能解决这个问题。 在Maya软件的“Modeling”模式下&#xff0c;点击菜单栏的“Windows”&#xff0c;在弹出的下拉框中选择“S…

[网络原理]关于网络的基本概念 及 协议

文章目录 一. 关于网络的概念介绍1. 局域⽹LAN2. ⼴域⽹WAN3. 主机4. 路由器5. 交换机IP地址端口号 二. 协议协议分层TCP/IP五层模型(或四层)OSI七层模型封装分用 一. 关于网络的概念介绍 1. 局域⽹LAN 局域⽹&#xff0c;即 Local Area Network&#xff0c;简称LAN。 Local …

看过来!2024 云栖大会操作系统技术 Workshop 怎么玩?

2024 年 9 月 19-21 日&#xff0c;2024 云栖大会操作系统开源专场将于杭州云栖小镇盛大召开&#xff0c;特邀阿里云、安谋科技、浪潮信息、英特尔、蚂蚁集团等多家单位的企业高层和技术大咖莅临。以龙蜥操作系统技术生态为牵引&#xff0c;阿里云打造了 AIibaba CIoud Linux 产…

ffmpeg的安装和使用教程

在Linux上安装和使用FFmpeg可以方便地完成音视频的编码、解码、转码等操作。以下是详细的安装和使用教程。 安装FFmpeg FFmpeg的安装方法会因为不同的Linux发行版有所不同。下面是几种常见的安装方法&#xff1a; Ubuntu/Debian 打开终端&#xff0c;更新包列表并安装FFmpe…