借助chatgpt学习对比学习中的疑惑

  • 使用pytorch得到对比学习的正负样本,要求是NLP方向,正样本通过dropout(0.1)得到,负样本使用batch内其他样本。同时,帮我用pytorch实现他们的对比学习损失,用simcse的损失

以下是使用 PyTorch 得到对比学习的正负样本,并使用 SIMCSE 损失进行训练的示例代码,假设我们的任务是将一批文本表示为向量,同时使得同一类别的文本向量更加接近,不同类别的文本向量更加分散。

import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader

class ContrastiveDataset(torch.utils.data.Dataset):
    def __init__(self, data, tokenizer, max_length=128):
        self.data = data
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __getitem__(self, index):
        text1 = self.data[index]
        # 50% 的概率将 text1 和 text2 设为同一个文本
        text2 = text1 if torch.rand(1) < 0.5 else self.data[torch.randint(len(self.data), (1,))]
        # 将 text1 和 text2 都转换为输入张量
        input1 = self.tokenizer(text1, padding='max_length', truncation=True, max_length=self.max_length, return_tensors='pt')
        input2 = self.tokenizer(text2, padding='max_length', truncation=True, max_length=self.max_length, return_tensors='pt')

        # 使用 dropout 获取正样本对
        mask = torch.zeros_like(input1['input_ids'])
        mask[:, 1:-1] = torch.rand(input1['input_ids'][:, 1:-1].shape) < 0.1  # dropout 随机将词汇替换为 [MASK]
        input1_masked = input1['input_ids'].clone()
        input1_masked[mask] = self.tokenizer.mask_token_id
        input2_masked = input2['input_ids'].clone()
        input2_masked[mask] = self.tokenizer.mask_token_id
        return input1_masked, input2_masked, torch.tensor(1.0)

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

class ContrastiveModel(torch.nn.Module):
    def __init__(self, base_model):
        super().__init__()
        self.base_model = base_model

    def forward(self, input_ids):
        outputs = self.base_model(input_ids)
        return outputs.last_hidden_state[:, 0, :]  # 使用 CLS token 作为句子表示

class SimCSELoss(torch.nn.Module):
    def __init__(self, temperature=0.05):
        super().__init__()
        self.temperature = temperature

    def forward(self, z1, z2):
        # 将 z1 和 z2 单位化
        z1 = F.normalize(z1, dim=1)
        z2 = F.normalize(z2, dim=1)
        # 计算 z1 和 z2 的余弦相似度矩阵
        sim_matrix = torch.matmul(z1, z2.t()) / self.temperature
        # 构造同分布的标签,即每个样本的标签为它本身
        batch_size = sim_matrix.size(0)
        labels = torch.arange(batch_size, dtype=torch.long, device=sim_matrix.device)
        # 使用交叉熵损失
        loss = F.cross_entropy(sim_matrix, labels)
        return loss
  • 如何调用SimCSELoss

要在 PyTorch 中使用 SimCSELoss 损失函数,需要先将正样本和负样本分别通过模型前向传播得到它们的向量表示,然后将这些向量表示传递给 SimCSELoss 模块即可。以下是一个示例代码:

import torch
import torch.nn.functional as F

class SimCSELoss(torch.nn.Module):
    def __init__(self, temperature=0.05):
        super().__init__()
        self.temperature = temperature

    def forward(self, z1, z2):
        # 将 z1 和 z2 单位化
        z1 = F.normalize(z1, dim=1)
        z2 = F.normalize(z2, dim=1)
        # 计算 z1 和 z2 的余弦相似度矩阵
        sim_matrix = torch.matmul(z1, z2.t()) / self.temperature
        # 构造同分布的标签,即每个样本的标签为它本身
        batch_size = sim_matrix.size(0)
        labels = torch.arange(batch_size, dtype=torch.long, device=sim_matrix.device)
        # 使用交叉熵损失
        loss = F.cross_entropy(sim_matrix, labels)
        return loss

假设我们已经有了正样本 z1 和负样本 z2 的向量表示,那么我们可以通过以下方式调用 SimCSELoss:

loss_fn = SimCSELoss()
loss = loss_fn(z1, z2)

其中,z1 和 z2 的形状都应该是 (batch_size, embedding_size),分别表示正样本和负样本的向量表示。在这里,embedding_size 表示向量的维度,它应该与模型的最后一层的输出维度相同。在计算损失时,我们只需要将这些向量表示传递给 SimCSELoss 的 forward 方法即可,PyTorch 会自动执行反向传播并更新模型参数。

  • z1和z2是如何得到的

在对比学习中,我们需要构造正负样本对来训练模型,其中正样本对应的是同一条数据的两个不同视角的嵌入向量(例如句子的不同表述或同义词的不同表示),而负样本对应的是来自不同数据的嵌入向量(例如在同一批次中,对于给定的正样本,其它所有样本作为负样本)。

具体地,在 NLP 领域,我们可以使用预训练模型(如 BERT、RoBERTa 等)对每条文本数据进行编码,得到其对应的嵌入向量。对于同一条文本数据,我们可以通过对原始文本进行随机扰动(例如在词级别进行替换或删除),从而得到不同的视角或表述,并使用模型分别编码这些扰动后的文本,即可得到正样本对应的两个嵌入向量。而负样本则可以从同一批次的其它样本中随机选择,使用同样的编码器得到嵌入向量。

在 PyTorch 中,我们可以使用预训练模型(如 transformers 库中的 BERT、RoBERTa 等)对文本数据进行编码,并得到其对应的嵌入向量。例如,对于一批次大小为 batch_size 的文本数据 texts,我们可以使用以下代码获取其嵌入向量:

import torch
import transformers

model_name = "bert-base-uncased"
tokenizer = transformers.BertTokenizer.from_pretrained(model_name)
model = transformers.BertModel.from_pretrained(model_name)

# 将文本转换为模型输入
inputs = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# 对文本进行编码并得到嵌入向量
outputs = model(**inputs)
embeddings = outputs.last_hidden_state

其中,texts 是一个列表,包含了批次中的所有文本数据。通过 tokenizer 将文本转换为模型输入,然后使用 model 对文本进行编码并得到嵌入向量。注意,embeddings 的形状应该是 (batch_size, sequence_length, hidden_size),其中 hidden_size 表示模型最后一层的输出维度,即嵌入向量的维度。我们可以使用 torch.mean 函数对 sequence_length 维度进行平均池化,得到每条文本数据对应的嵌入向量:

# 对每条文本数据进行平均池化,得到其对应的嵌入向量
z = torch.mean(embeddings, dim=1)

最后,我们可以通过对正样本的嵌入向量应用 dropout(在 PyTorch 中可以使用 torch.nn.Dropout 模块)和对批次中其它样本的嵌入向量应用负采样,得到对比学习的正负样本。具体地,在 PyTorch 中,可以使用以下代码实现:

import torch

# 对正样本的嵌入向量应用 dropout
z1 = torch.nn.Dropout(p=0.1)(z)
# 使用负采样得到负样本的嵌入向量
z2_indices = torch.randint(0, batch_size, size=(batch_size,))
z2 = z[z2_indices]

向量应用负采样,得到对比学习的正负样本。具体地,在 PyTorch 中,可以使用以下代码实现:

import torch

# 对正样本的嵌入向量应用 dropout
z1 = torch.nn.Dropout(p=0.1)(z)
# 使用负采样得到负样本的嵌入向量
z2_indices = torch.randint(0, batch_size, size=(batch_size,))
z2 = z[z2_indices]

其中,z 是形状为 (batch_size, hidden_size) 的嵌入向量矩阵,batch_size 表示批次大小,hidden_size 表示嵌入向量的维度。通过对 z 应用 dropout,可以得到正样本的嵌入向量 z1。同时,我们可以使用 torch.randint 函数在 [0, batch_size) 范围内随机采样 batch_size 个整数作为负样本的索引,然后从 z 中获取这些索引对应的嵌入向量,即可得到负样本的嵌入向量 z2。

注意,在上述代码中,由于每个样本可以作为负样本和正样本的对比样本,因此在采样负样本时,我们需要使用 torch.randint 函数而不是 torch.randperm 函数,以保证每个样本都有机会作为负样本出现。此外,在实现中,还需要确保负样本与正样本来自不同的数据,因此在实际使用中,可能需要对代码进行一些修改。

  • 上面那个Dataset的类好像没有必要实现

你说得没错。在我的代码示例中,我演示了如何使用 PyTorch 得到对比学习的正负样本。因此,在实际使用中,可以根据需要进行选择,如果数据已经提前处理好了,那么就不需要再使用 ContrastiveDataset 类了。另外,ContrastiveDataset 类的作用主要是为了方便使用 PyTorch 提供的数据加载和处理函数,可以实现更高效的批次化训练,因此如果不需要这些功能,也可以不使用该类。

posted @ 2023-02-27 11:50  douzujun  阅读(151)  评论(0编辑  收藏  举报