文本二分类 | datawhale夏令营NLP赛道baseline逐行精读笔记(一)

182 阅读9分钟

简介

参加了datawhale夏令营的同时跑去自学kaggle课程了,现在B榜开了,出了新的baseline,跟着datawhale的直播看了一下,写一点笔记.

总体看一下这个baseline代码:

  • 利用预训练的 BERT 模型获取输入序列的语义表示。
  • 使用完全连接的网络作为预测层,将 BERT 的输出转换为二分类预测。
  • 在训练过程中,模型在每个 epoch 结束时都会在验证集上进行评估,这有助于监控模型的性能并防止过拟合。
  • 模型会保存在验证集上表现最好的模型,这有助于我们在训练结束后获得最好的模型。

可以解决的疑问有:

  • 随机采样方法和 sklearn的 train_test_split方法有什么区别?
  • 为什么要使用Binary Cross Entropy作为损失函数?

数据预处理

缺失值处理

填充空字符串

test_data['title'] = test_data['title'].fillna('')
test_data['abstract'] = test_data['abstract'].fillna('')
pd_train_data['text'] = pd_train_data['title'].fillna('') + ' ' +  pd_train_data['author'].fillna('') + ' ' + pd_train_data['abstract'].fillna('')+ ' ' + pd_train_data['Keywords'].fillna('')
test_data['text'] = test_data['title'].fillna('') + ' ' +  test_data['author'].fillna('') + ' ' + test_data['abstract'].fillna('')+ ' ' + pd_train_data['Keywords'].fillna('')

训练集和测试集的拆分

这里是随机采样

# 从训练集中随机采样测试集
validation_data = pd_train_data.sample(frac=validation_ratio)
train_data = pd_train_data[~pd_train_data.index.isin(validation_data.index)]

随机采样方法和 sklearn的 train_test_split方法有什么区别?

from sklearn.model_selection import train_test_split

pandas 的 sample 函数和 sklearn 的 train_test_split 函数都是用于将数据集分割成训练集和验证集。

虽然这两个函数在分割数据集的基本功能上是相同的,train_test_split 更为常见,因为它直接返回划分后的训练集和测试集,而使用 pandas.DataFrame.sample 则需要我们手动创建训练集(从原始数据中移除已被选为测试集的样本)。

所以, train_test_split 在易用性上更胜一筹。

如果数据集足够大,且对训练集和测试集的划分比例有具体的要求,train_test_split 更好

如果数据集较小,或者希望能够更灵活地控制抽样过程(例如,进行有放回的抽样或无放回的抽样),pandas.DataFrame.sample 可能会更有用

训练数据分批 (dataloader)

def collate_fn(batch):
    """
    将一个batch的文本句子转成tensor,并组成batch。
    :param batch: 一个batch的句子,例如: [('推文', target), ('推文', target), ...]
    :return: 处理后的结果,例如:
             src: {'input_ids': tensor([[ 101, ..., 102, 0, 0, ...], ...]), 'attention_mask': tensor([[1, ..., 1, 0, ...], ...])}
             target:[1, 1, 0, ...]
    """
    text, label = zip(*batch)
    text, label = list(text), list(label)

    # src是要送给bert的,所以不需要特殊处理,直接用tokenizer的结果即可
    # padding='max_length' 不够长度的进行填充
    # truncation=True 长度过长的进行裁剪
    src = tokenizer(text, padding='max_length', max_length=text_max_length, return_tensors='pt', truncation=True)

    return src, torch.LongTensor(label)

在 PyTorch 中,DataLoader 是一个用于批量处理数据的工具,它可以自动地将数据划分为多个批次,并且可以对每个批次的数据进行自定义的处理。这里,collate_fn 就是用于对批次数据进行处理的函数。

具体地,collate_fn 函数的输入是一个列表,列表中的每个元素都是一个数据对,包括一段文本和对应的标签。函数的任务是将这个列表中的所有数据对转化为两个 tensor:一个是输入数据 tensor(即 BERT 模型的输入),另一个是标签 tensor。

下面是我对 collate_fn 函数的详细解读:

  1. 首先,函数将输入的数据对列表分解为两个列表:一个是文本列表,另一个是标签列表。
  2. 对于文本列表,函数使用 BERT 的 tokenizer 对每个文本进行处理,得到每个文本的输入数据(包括 input_idsattention_mask)。这里,padding='max_length' 参数表示如果文本的长度小于 max_length,则会在后面添加 0 以补齐长度;truncation=True 参数表示如果文本的长度大于 max_length,则会将超出部分的文本截断。处理后的所有文本的输入数据被组合成一个字典。
  3. 对于标签列表,函数直接将其转化为一个长整型的 tensor。
  4. 最后,函数返回处理后的输入数据字典和标签 tensor。

在实际使用中,DataLoader 会自动调用 collate_fn 函数对每个批次的数据进行处理,然后将处理后的批次数据传递给模型进行训练。

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, collate_fn=collate_fn)
validation_loader = DataLoader(validation_dataset, batch_size=batch_size, shuffle=False, collate_fn=collate_fn)

定义bert模型

tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") 这里用的是bert-base-uncased模型

定义最后的预测全连接层

定义了一个预测层 predictor,这是一个完全连接的网络,用于将 BERT 的输出转换为最终的二分类预测。

class MyModel(nn.Module):

    def __init__(self):
        super(MyModel, self).__init__()

        # 加载bert模型
        self.bert = BertModel.from_pretrained('bert-base-uncased', mirror='tuna')

        # 最后的预测层
        self.predictor = nn.Sequential(
            nn.Linear(768, 256),
            nn.ReLU(),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

模型的前向传播方法

class MyModel(nn.Module):
    ...
    def forward(self, src):
        """
        :param src: 分词后的推文数据
        """

        # 将src直接序列解包传入bert,因为bert和tokenizer是一套的,所以可以这么做。
        # 得到encoder的输出,用最前面[CLS]的输出作为最终线性层的输入
        outputs = self.bert(**src).last_hidden_state[:, 0, :]

        # 使用线性层来做最终的预测
        return self.predictor(outputs)
 

forward 方法中,模型首先通过 BERT 模型处理输入数据,然后取出每个输入序列对应的 [CLS] 标记的输出,这部分输出被视为整个输入序列的语义表示。然后,这个表示被传递给预测层,得到最终的二分类预测结果。

损失函数和优化器设置

criteria = nn.BCELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=lr)

在神经网络中,我们使用损失函数(或目标函数)来优化模型的参数,以便模型能够更好地预测目标变量。这里使用的 Binary Cross Entropy (BCE) 是一种常用于二分类问题的损失函数。

损失函数Binary Cross Entropy

二元交叉熵(Binary Cross Entropy, BCE)损失函数,这是一个常用于二元分类问题的损失函数。在二元分类问题中,每个实例都属于两个类别之一(通常标记为1和0)。模型的任务是预测每个实例属于类别1的概率。二元交叉熵损失函数就是用来衡量模型预测的概率与真实标签之间的差异。

二元交叉熵 (BCE) 的定义如下:

BCE=1Ni=1Nyilog(p(yi))+(1yi)log(1p(yi))B C E=-\frac{1}{N} \sum_{i=1}^N y_i \log \left(p\left(y_i\right)\right)+\left(1-y_i\right) \log \left(1-p\left(y_i\right)\right)

其中:

  • NN 是样本数量。
  • yiy_i 是样本 ii 的实际标签, 它的值为 0 或 1 。
  • p(yi)p\left(y_i\right) 是模型预测样本 ii 的标签为 1 的概率。

如果 y=1y=1, 那么损失函数就是 log(p)-\log (p) ,如果 pp 接近 1 ,损失就会接近0,如果 pp 接近0, 损失就会变得很大。相反,如果 y=0y=0, 那么损失函数就是 log(1p)-\log (1-p), 当 pp 接近0时, 损失接近0,当 pp 接近1时,损失变大。

也就是说当模型对正样本的预测概率越接近 1,对负样本的预测概率越接近 0,二元交叉熵损失就越小。因此,我们的优化目标就是通过调整模型参数来最小化二元交叉熵损失。

baseline代码中的 nn.BCELoss()是 PyTorch 框架中用来计算二元交叉熵损失的函数。在这个函数中,模型的预测值和实际标签都应为介于 0 和 1 之间的数值。

为什么要使用Binary Cross Entropy作为损失函数

除了二元交叉熵(Binary Cross-Entropy,BCE)损失函数之外,还有一些常见的用于二分类问题的损失函数,比如说:

  1. 对数损失(Log Loss) :它的形式和二元交叉熵类似,只是它直接对概率进行建模,而不是对数概率。
  2. Hinge损失函数:常用于支持向量机(SVM)。它试图确保模型对正负样本的预测之间有足够的间隔,以此提高模型的泛化能力。
  3. 平方误差损失(Squared Error Loss) :虽然它通常用于回归问题,但在某些情况下也可以用于二元分类。例如,如果你的输出层是一个线性函数,你可以用平方误差损失函数,然后设定一个阈值来决定分类。

这些损失函数的联系和区别主要表现在以下几个方面:

  • 计算方法
    • 对数损失和二元交叉熵损失基于概率进行计算,
    • 而Hinge损失和平方误差损失则基于模型预测值和实际值之间的差异进行计算。
  • 惩罚方式
    • 对数损失和二元交叉熵损失对于预测错误的样本给予了很大的惩罚(尤其是对于非常确定但预测错误的样本),这有助于模型在训练过程中更快地纠正错误。
    • 而Hinge损失则主要关注那些难以分类的样本(即那些位于决策边界附近的样本),这有助于模型找到一个良好的决策边界。

BCE损失函数适合处理每个类别是互斥的情况,即每个样本只能属于两个类别中的一个。在这种情况下,我们的目标是最小化模型预测的概率分布与真实的概率分布之间的"距离"。BCE损失函数提供了一种衡量这种"距离"的方式。 BCE损失函数的一些优点和适用场景:

  1. 概率解释:BCE损失函数直接对概率进行建模,它的输出可以被解释为概率。这在许多实际应用中非常有用,比如在风险建模或者医学诊断等领域,我们不仅关心预测的类别,同时也关心预测的概率。
  2. 对数形式:BCE损失函数的对数形式使得它对于那些模型预测正确但不够自信的情况(即预测的概率离1还很远)更为敏感。这可以鼓励模型更加自信地做出预测,从而提高模型的性能。
  3. 适用于不平衡数据:BCE损失函数适用于处理不平衡数据集,因为它对每个样本的贡献是相等的,不会因为某一类样本数量过多而导致模型偏向这一类。

优化器

optimizer = torch.optim.Adam(model.parameters(), lr=lr) 定义了一个优化器,用于更新模型的参数。

优化器的任务是根据损失函数的梯度来调整模型的参数,以减小损失函数的值。

Adam 优化器,它是一种常用的优化算法,具有自适应学习率等优点。

关于为什么用adam,adam和pytorch、tensorflow的区别且容下次再写TAT我还在搞云jupyter的环境,colab太不稳定了,kaggle的gpu好慢,启session时间都快赶上训练时间了...

参考资料

AI夏令营 - NLP实践教程 - 飞书云文档 (feishu.cn)

www.bilibili.com/video/BV1qu…