跳转至

语言模型微调的知识补充

如何使用这份材料:

  • 最高效:这个文档中最实用的是代码(代码来自提供给大家的框架)解读部分,目的是帮助你了解这些代码的功能
  • 感兴趣的涨知识:其他的部分都是代码相关的背景内容,你不了解也完全可以完成本实验,但是写在这里只是为了给感兴趣的同学涨知识(以及向你展示怎么向ChatGPT提问来学习新知识)

微调是什么?

Question:

解释一下微调的含义,和预训练对比,体现在模型训练的语境中。

ChatGPT:

微调(Fine-Tuning)和预训练(Pretraining)是深度学习模型训练中两个重要的概念,它们体现在模型训练的语境中有着不同的含义和操作方式。

  1. 预训练(Pretraining)

    • 含义:预训练指的是在一个大规模数据集上训练一个深度学习模型,通常是一个通用任务的模型,比如自然语言处理中的语言模型(例如GPT-3)或图像分类中的卷积神经网络(例如ResNet)。
    • 过程:预训练模型在大量数据上学会了一些通用的特征和知识,但它还不知道如何执行特定任务。
    • 举例:一个预训练的语言模型可能已经学会了很多关于语言的知识,但不知道如何回答特定问题。
  2. 微调(Fine-Tuning)

    • 含义:微调是在一个已经预训练好的模型基础上,进一步调整模型的参数以适应特定任务或数据集。
    • 过程:微调包括冻结预训练模型的一部分(通常是底层的通用特征提取器),然后在这个基础上添加一些新的层,以便模型可以执行特定任务。然后,使用特定任务的数据集对整个模型进行训练,从而使模型适应新任务。
    • 举例:使用一个预训练的图像分类模型,冻结底层卷积神经网络,添加一个新的全连接层,并使用新的图像数据集对整个模型进行训练,以执行不同的图像分类任务。

总的来说,微调是在预训练模型的基础上进行的进一步训练,目的是将通用的预训练知识迁移到特定任务中。这种方法通常比从头开始训练一个模型更有效,因为预训练模型已经学会了很多通用的特征和知识,可以为新任务提供有用的信息。微调的关键在于在新任务上对模型进行适当的调整,以确保它能够更好地适应新的需求。

微调过程概览

当你准备好模型和数据集之后,微调过程如下:

  1. 数据集处理:通过Dataloader的代码实现,将不同类型的数据处理成标准的矩阵tensor
  2. 训练设置:包括设置目标函数(损失函数)与超参数
  3. 训练与评测:模型的训练(train)和评测(inference

数据集处理

概念理解:什么是batch?

Question:

如何理解机器学习中的batch?

ChatGPT:

在机器学习中,"batch"(批次)是一种用于训练模型的数据组织方式,它通常包含多个样本。理解批次的概念对于机器学习非常重要,下面我将解释它的含义和作用:

  1. 批次是数据的集合:在机器学习中,我们通常有大量的训练数据。批次是将这些数据分成小块的方式,每个批次包含多个样本。每个样本都是一个数据点,比如一张图片、一段文本或其他形式的数据。

  2. 批次的大小:批次的大小是指每个批次中包含的样本数量。它是一个超参数,可以根据任务和计算资源进行调整。典型的批次大小可以是 32、64、128 等。

  3. 批次的作用

    • 并行计算:使用批次可以在计算机硬件上实现并行计算。现代硬件(如GPU)可以同时处理多个数据点,因此批次允许我们充分利用硬件的性能,加快模型的训练速度。
    • 稳定的梯度估计:批次中的样本通常会引入一些噪声,但平均后可以得到更稳定的梯度估计。这有助于模型更稳定地收敛,避免陷入局部极小值。
    • 内存效率:处理整个数据集可能需要大量内存,而使用批次可以将内存需求降低到合理范围内,从而使模型可以在资源受限的环境中运行。
  4. 批次的选择:选择合适的批次大小取决于任务的性质。对于计算资源充足的任务,较大的批次大小可能更有效。但是,在某些情况下,小批次可以帮助模型更好地泛化到新数据。

总之,理解批次的概念是机器学习中重要的一步,它有助于提高训练效率、稳定性和内存效率。选择合适的批次大小取决于任务的性质和可用的计算资源。批次是机器学习中数据处理和训练的基本组成部分之一。

代码解读

def init_data_pretrain(dataset):
    global train_data, val_data
    data_dir = os.path.join('data', dataset)
    train_data = np.memmap(os.path.join(data_dir, 'train.bin'), dtype=np.uint16, mode='r')
    val_data = np.memmap(os.path.join(data_dir, 'val.bin'), dtype=np.uint16, mode='r')

init_data_pretrain这个函数的目的是初始化数据集,它接受一个参数 dataset,表示数据集的名称(相对文件路径)。

在函数内部:

  1. 将数据集的路径构建为 data_dir 变量:os.path.join会将前后两个字符串表示的路径串联起来得到完整的路径。
  2. 使用 NumPy 的 np.memmap 函数加载数据集的训练和验证部分为 train_dataval_data。这两个变量是 NumPy 的内存映射数组,它们允许在磁盘上存储大量数据,但以类似于内存数组的方式进行访问。这是一种有效地处理大数据集的方式。
def get_batch_pretrain(split, batch_size, block_size, device):
    global train_data, val_data
    data = train_data if split == 'train' else val_data
    ix = torch.randint(len(data) - block_size, (batch_size,))
    x = torch.stack([torch.from_numpy((data[i:i+block_size]).astype(np.int64)) for i in ix])
    y = torch.stack([torch.from_numpy((data[i+1:i+1+block_size]).astype(np.int64)) for i in ix])
    loss_mask = torch.ones_like(x, dtype=torch.float64)
    device_type = 'cuda' if 'cuda' in device else 'cpu'
    if device_type == 'cuda':
        # pin arrays x,y, which allows us to move them to GPU asynchronously (non_blocking=True)
        x, y, loss_mask = x.pin_memory().to(device, non_blocking=True), y.pin_memory().to(device, non_blocking=True), loss_mask.pin_memory().to(device, non_blocking=True)
    else:
        x, y, loss_mask = x.to(device), y.to(device), loss_mask.to(device)
    return x, y, loss_mask

get_batch_pretrain(split, batch_size, block_size, device) 函数:这个函数用于获取训练或验证数据的批次(也就是batch)。它接受以下参数:

  • split:表示数据集的划分,可以是 'train'(训练数据)或 'val'(验证数据)。
  • batch_size:表示每个批次(batch)中的样本数量。
  • block_size:表示每个样本中的数据块大小。
  • device:表示数据将被加载到的设备(通常是 CPU 或 GPU)。

在函数内部:

  1. 根据 split 参数选择加载训练数据还是验证数据。
  2. 从数据中随机选择 batch_size 个索引,这些索引将用于提取批次(batch)的数据。
  3. 创建输入数据 x 和目标数据 y,其中 x 包含数据块的起始位置,y 包含相应数据块的下一个位置。这种设置通常用于语言模型等序列生成任务,其中模型尝试预测序列中的下一个元素。
  4. 函数返回了 xy,和一个 loss_maskloss_mask 是一个与 xy 具有相同形状的张量,其所有值都设置为 1,用于控制模型在训练过程中哪些位置计算损失(loss)。这可以在某些情况下用于掩盖不需要计算损失的部分。

目标函数

什么是目标函数?

Question:

如何理解机器学习中的目标函数?(带点数学术语来严谨描述吧)

ChatGPT:

在机器学习中,目标函数(Objective Function)是一个数学函数,通常用来衡量模型的性能或质量。其主要目的是定义一个可优化的数学指标,以便通过调整模型的参数来最小化或最大化这个指标。目标函数也被称为损失函数(Loss Function)或成本函数(Cost Function),其数学表示如下:

假设我们有一个模型 \(f\),它具有一组参数 \(\theta\)。对于给定的数据集,我们将数据表示为 \((X, Y)\),其中 \(X\) 是输入数据,\(Y\) 是对应的真实标签或目标值。

目标函数通常以以下形式表示:

\[ L(\theta) = \frac{1}{N} \sum_{i=1}^{N} \mathcal{L}(f(X_i; \theta), Y_i) \]

其中:

  • \(L(\theta)\) 表示目标函数,也是损失函数或成本函数。
  • \(\theta\) 是模型的参数,我们的目标是找到最优的 \(\theta\)
  • \(N\) 表示数据集中的样本数量。
  • \(X_i\) 是第 \(i\) 个样本的输入。
  • \(Y_i\) 是第 \(i\) 个样本的真实标签或目标值。
  • \(\mathcal{L}\) 是损失函数(损失函数的具体形式可以根据任务的性质而变化),用于度量模型的预测输出 \(f(X_i; \theta)\) 与真实标签 \(Y_i\) 之间的差距。

机器学习的目标通常可以分为两种:

  1. 最小化损失:在大多数情况下,我们的目标是通过调整模型的参数 \(\theta\) 来最小化目标函数 \(L(\theta)\)。这意味着我们希望模型的预测尽量接近真实标签,从而提高模型的性能。

  2. 最大化似然:在一些统计推断或生成模型的情况下,目标函数可能表示为最大化似然函数(Likelihood),而不是最小化损失。在这种情况下,我们希望通过调整模型参数,使模型能够最好地描述观测到的数据分布,以便进行推断或生成新数据。

总之,目标函数在机器学习中起到了至关重要的作用,它定义了模型的优化目标,允许我们通过参数调整来提高模型的性能,以便更好地适应给定的任务或数据。不同的任务和模型可以使用不同的目标函数,以满足特定的需求。

如何理解交叉熵函数

我们的大作业代码中使用的目标函数就是“交叉熵函数”

可以查阅各种网络资料,例如:

https://blog.csdn.net/wuliBob/article/details/104119616

这个文章《对PyTorch中F.cross_entropy()函数的理解》中有直观的理解。

代码解读

if loss_mask is not None:
  loss = (F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1, reduction='none') * loss_mask.view(-1)).sum() / loss_mask.sum()
else:
  loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-1)

F.cross_entropy的用法参见:

https://pytorch.org/docs/stable/generated/torch.nn.functional.cross_entropy.html

训练与评测

model.eval()model.train() 是 PyTorch 中用于控制模型状态的两个方法,它们通常用于训练和推断的不同阶段,以确保模型的行为正确。以下是它们的含义和作用:

  • model.train()
    • 含义:model.train() 方法将模型设置为训练模式。这意味着模型会启用一些特定于训练的功能,例如启用 Batch Normalization 和 Dropout 层的训练行为。
    • 作用:在训练阶段,模型需要学习并适应训练数据,因此需要启用训练相关的特性以执行反向传播和梯度更新。通常,当你在训练模型时,应该在训练循环开始时使用 model.train() 来确保模型处于训练状态。
  • model.eval()
    • 含义:model.eval() 方法将模型设置为评估(推断)模式。这意味着模型会禁用一些训练特定的功能,如 Dropout 层将被禁用,Batch Normalization 层的行为将被固定。
    • 作用:在评估或推断阶段,模型不需要学习或适应数据,因此禁用一些训练特性可以提高模型的预测稳定性和一致性。通常,在进行验证、测试或应用模型进行推断时,应该在评估前使用 model.eval()

搭配使用时,通常的做法是在训练循环之前使用 model.train(),然后在验证或测试循环之前使用 model.eval()。这确保了模型在训练和推断阶段的行为是正确的。

例如,一个典型的训练循环可能如下所示:

model.train()  # 切换到训练模式
for batch in training_data:
    # 计算损失和执行反向传播
    loss = compute_loss(batch)
    loss.backward()
    optimizer.step()
    optimizer.zero_grad()

而在验证或测试循环中,你会这样使用:

model.eval()  # 切换到评估模式
with torch.no_grad():
    for batch in validation_data:
        # 执行模型推断,不执行反向传播
        predictions = model(batch)

这种切换模型状态的方法有助于确保模型在不同阶段的行为正确,并且能够更好地控制 Dropout、Batch Normalization 等层的行为,从而提高模型的稳定性和可重复性。

附录:对机器学习的入门理解

借助了ChatGPT和我共同撰写这一部分内容,因此比较“冗长”。用ChatGPT来学习一些基本概念是值得尝试的。

当我们谈论深度学习时,有两个非常重要的概念是“模型”和“数据集”。

  1. 模型:在深度学习中,模型就像是一个数学函数或者一个计算机程序,它的任务是从输入数据中学习并做出有用的预测或决策。你可以把模型想象成一个黑盒子,你将数据输入这个黑盒子,然后它会输出你想要的结果。这个黑盒子内部有许多参数和算法,通过大量的数据训练来调整这些参数,使模型能够更好地完成特定任务。模型的类型有很多种,比如神经网络、决策树等,不同的模型适用于不同的问题。

  2. 数据集:数据集就是用来训练和测试深度学习模型的数据集合。它包含了许多输入数据和对应的目标或标签。你可以把数据集想象成一个大数据表,每一行是一个数据点,每一列是一个数据特征或者标签。例如,如果你想教一个模型识别猫和狗的图片,你的数据集将包含许多图片,每张图片都有一个标签,指明这是一只猫还是一只狗。深度学习模型通过分析这些数据集来学习如何将输入数据映射到正确的输出。

  3. 模型的输入和输出:输入数据经过神经网络的各个层(也称为神经元)进行复杂的数学运算,然后产生一个输出。这个输出通常代表了模型对输入数据的预测或分类结果。例如,如果我们训练一个神经网络来识别手写数字,输入可能是一个数字图像,而输出则是表示这个图像是哪个数字的概率分布。

深度学习中的"模型训练"就像是在教一只狗学习新技能的过程。

想象一下你有一只聪明的狗,你希望它能够学会坐下。一开始,狗不知道怎么做,所以你需要进行训练。你会反复告诉狗:"坐下",然后当它坐下时,你会给它一些奖励,比如食物或者玩具。你会反复进行这个过程,直到狗能够听懂指令,并且能够坐下。

在深度学习中,模型训练的过程与这个类似。模型就像是一只虚拟的"机器狗",它不知道如何做某个任务,比如识别猫和狗的图片。为了让模型学会这个任务,我们需要使用大量的数据,就像你训练狗一样。我们将向模型展示很多图片,并告诉它这些图片中哪些是猫,哪些是狗。然后,模型会根据这些信息尝试调整自己的"思考方式",以便在未来看到新的图片时能够正确地分类它们。

模型训练的过程就是反复地向模型展示数据,告诉它什么是正确的答案,然后让它自己学习如何预测正确的答案。这个过程可能需要很多次,就像训练一只狗学会坐下一样。一旦模型经过足够的训练,它就能够在未见过的图片上准确地识别猫和狗了。这就是深度学习中"模型训练"的基本思想。

我们可以用更多数学术语来解释:

  1. 模型是一个函数:在深度学习中,我们把模型表示为一个函数,通常用符号表示为Y = f(X),其中X是输入数据,Y是模型的预测输出。这个函数f是神经网络,它包含多个层和参数,这些参数需要被调整以使得函数能够正确地映射输入X到输出Y。
  2. 损失函数:为了度量模型的预测与真实标签之间的差距,我们引入一个损失函数L(Y, Y'),其中Y是模型的预测输出,Y'是真实的标签。损失函数可以是各种各样的数学公式,例如均方误差、交叉熵等。这个函数的值表示了模型预测的准确性,我们的目标是最小化这个损失函数,即找到最合适的参数,以使预测更接近真实标签。
  3. 梯度下降:为了最小化损失函数,我们使用梯度下降算法。这个算法通过计算损失函数对模型参数的梯度(即导数),然后向着梯度减小的方向微调参数。这个过程不断重复,逐渐减小损失函数的值,直到找到最优的参数配置,使得模型能够更好地拟合数据。
  4. 反向传播:神经网络中的反向传播是梯度下降的关键。在训练过程中,我们通过计算损失函数对网络中每个参数的梯度,然后反向传播这些梯度信息,以便调整每个参数的值。这样,模型的每一部分都能够对提高预测性能做出贡献。
  5. 迭代训练:训练过程是一个迭代的过程。我们将数据传递给模型,计算损失函数,然后使用梯度下降来更新模型的参数。这个过程不断重复,直到损失函数收敛到一个较小的值或达到预定的停止条件。在每一轮迭代中,模型的参数都会微调,以提高其性能。

总之,深度学习中的模型训练涉及将输入X通过神经网络函数f映射到输出Y,通过最小化损失函数L(Y, Y')来调整模型的参数,这是通过梯度下降和反向传播算法来实现的。这个过程是迭代的,通过多次迭代,模型逐渐学会更好地拟合训练数据,从而提高其预测能力。


最后更新: 2023年9月8日
作者:xujz18