【大模型理论篇】大模型微调之指令微调(Instruction Fine-Tuning)

1. 背景介绍

        之前我们通过大量的文章,讲解了通用大模型的原理,业内也称基座大模型。通用大模型(例如GPT-4、Llama-3.1等)是基于大量的(包含公开的互联网爬取数据)文本数据训练的深度学习模型,具有通用的自然语言处理能力,可以处理多种任务,如文本生成、翻译、问答、情感分析等。但是这些通用模型在训练过程中一般没有针对特定的任务或领域进行微调。所以在具体到某一个细分场景,表现可能达不到商用的标准。

        因此,为了提升面向商用标准能力,往往就会需要对通用大模型进行微调,以使其在特定任务或领域上表现得更精确、更有效。可以用一个比喻来形容:很多学校大一并不直接开设专业课,而是以基础学科位置,比如数学、物理、语言等。到了大二,才开始开设具体的专业课程。相当于大一期间锻炼基础的学术、学科储备能力,大二才进行专业课程的微调,以使学生在某一个具体的专业方向上具备更针对性、更强的专业能力。而涉及到微调,又细分为多种方法。还是以大学生培养的例子继续描述,学生专业课程的学习方式,既有经验丰富的资深老师来进行直接授课,也有专业课考试来检测并指导进一步提升的方向,还有动手实践课程,甚至是去相应的岗位进行实习。不同的学习方式可以带来不同的效果,并在一定程度上全面提升学生的专利能力。

        通过浅显易懂的例子,相信对于通用大模型、模型微调有了一个大概的对比理解。接下来我们用技术性的文字来系统表述通用大模型与模型微调的差异对比、模型微调技术的概念等【1,2,3】。

        每个行业以及不同的应用场景都有其特定的个性化的需求,预训练的通用大模型往往难以全方位满足这些需求。模型微调是将预训练的通用大模型进一步调整,使其更好地适应特定任务或领域。一般是通过在专门的数据集上进行额外的训练来实现,目的是提升模型在特定任务上的性能。通过微调,模型能够针对特定任务进行优化,从而在该任务上表现更为出色。例如,经过情感分析微调的模型在情感分类任务上的表现会明显优于未微调的通用模型。微调所需的计算资源通常比训练通用大模型低得多,因为只需在已有模型基础上进行进一步的调整。微调后的模型在特定任务上的推理效率也可能更高。

        按照不同的技术手段、不同的资源要求、不同的业务需求,模型微调技术又细分为以下常见的类别:

1. 指令微调(Instruction Fine-Tuning)

  • 方法描述: 通过为模型提供特定任务的明确指令或示例来进行微调,通常保持预训练模型的知识。

  • 优点: 专注于特定任务的微调,适应性强,同时保留了模型的基础能力。

  • 缺点: 可能无法充分挖掘模型在某些高度复杂任务中的潜力。

2. 全量微调(Full Fine-Tuning)

  • 方法描述: 全量微调涉及对模型的所有参数进行微调。这种方法最为彻底,可以最大程度地适应特定任务,但也可能导致过拟合。

  • 优点: 高度适应特定任务,能够大幅度提高模型的性能。

  • 缺点: 需要大量计算资源和时间,同时可能导致模型在新任务上泛化能力下降。

3. 部分参数微调(Partial Fine-Tuning)

  • 方法描述: 仅微调模型的部分参数(如后期层或特定层),而不是全部参数。这种方法减少了计算成本,并降低了过拟合的风险。

  • 优点: 计算资源需求较低,过拟合风险较小。

  • 缺点: 对复杂任务的适应能力可能不如全量微调。

4. 适应性微调(Adapter Fine-Tuning)

  • 方法描述: 通过在预训练模型的特定层之间插入小型适应性模块(adapter),仅对这些模块进行微调,而保持主模型参数不变。

  • 优点: 高效灵活,减少了微调的计算成本和内存占用,适用于多个任务。

  • 缺点: 性能提升可能不如全量微调显著。

5. 低秩近似微调(Low-Rank Adaptation, LoRA)

  • 方法描述: 通过低秩矩阵来微调模型参数,这种方法只微调一个较小的参数子集,通常是模型权重矩阵的低秩近似部分。

  • 优点: 极大地减少了微调参数的数量和计算成本,适合资源受限的场景。

  • 缺点: 对某些复杂任务,可能不如全量微调效果好。

6. 微调后层(Fine-Tuning Last Layers)

  • 方法描述: 仅微调模型的最后几层,这种方法适用于希望在保持原模型能力的基础上增加一些特定任务的适应性。

  • 优点: 减少了微调的复杂性和资源需求,同时仍然能够提升模型在特定任务上的表现。

  • 缺点: 微调深度有限,可能无法充分适应任务需求。

7. 多任务微调(Multi-Task Fine-Tuning)

  • 方法描述: 在多个任务上同时进行微调,使模型能够在多个相关任务上均表现良好。

  • 优点: 提高了模型的泛化能力,能够在多个任务上取得较好的平衡性能。

  • 缺点: 需要处理不同任务之间的冲突,微调过程较为复杂。

        由于大模型的微调技术涉及多种,本文作为该系列的第一篇,将首先关注指令微调技术,后续会逐步对其他微调技术做相应的分析和讲解。

2. 指令微调

2.1 指令微调定义

        指令微调是一种在带有指令提示和相应输出的标记数据集上微调大模型的技术。通过提供一组概述所需操作的指南或指令,使预训练模型适应执行特定任务。能提高模型在特定任务上的性能,还能在总体上提高遵循指令的能力,有助于调整预训练模型用于实际应用。指令微调是用于调整预训练基础大模型以适应下游任务的一种微调技术。通用大模型可以出于各种目的进行微调,从风格定制到补充预训练模型的核心知识和词汇,再到针对特定用例优化性能。

        指令既可用于提示词,也可以用于微调。通过自然语言指令引导模型生成最符合需求的输出。这种机制类似于搜索引擎,添加更多关键词通常有助于首先找到最佳结果。对于大模型,你可以理解成是某种具有非常丰富知识的数据源,通过某种检索匹配的技术,找到你想要的答案。这里推荐一篇我之前整理的文章,帮助理解大模型是一种大型的知识压缩记忆的参数系统《压缩泛化-对大语言模型智能涌现的理解》。 一般来说,对期望输出描述得越好,结果通常就越符合要求。指令与上下文和进一步的输入文本(例如问题)一起放入提示词中,提示词实际上就是一个字符串。比如一般在问答场景中可能会设置一个较长的指令:

指令:“你是一个乐于助人、尊重他人且诚实的助手。始终尽可能有帮助地回答,同时确保安全。你的回答不应包含任何有害、不道德、种族主义、性别歧视、恶毒、危险或非法的内容。请确保你的回答在社会上不带有偏见,并且具有积极的性质。如果一个问题没有意义,或事实不连贯,请解释原因,而不是回答错误的内容。如果你不知道问题的答案,请不要分享错误信息。”

上下文:<<输入你的上下文>>

问题:<<输入你的问题>>

        指令微调是关于使用示例来训练模型,这些示例展示了模型应该如何响应查询【7】。用于微调大模型的数据集必须服务于你的指令目的。例如,假设微调模型以提高其总结能力。在这种情况下,应该构建一个示例数据集,以总结指令为开头,后面跟着文本或类似的短语。在翻译的情况下,应该包括像 “翻译这段文本” 这样的指令。这些提示补全对允许模型以一种新的特定方式思考并服务于给定的特定任务。                

        指令微调与其他微调技术不排斥。例如,聊天模型通常同时进行指令微调以及来自人类反馈的强化学习(RLHF),RLHF 也是一种微调技术。针对编码进行微调的模型通常同时进行指令微调和在特定编程的数据上进行额外微调。

2.2 为什么要对大模型进行指令微调

        指令微调,在于预训练基座大模型并未针对对话或遵循指令进行优化【4】。大模型不会回答提示:它们只是在提示后附加文本。指令微调有助于使生成的附加文本更有用。自回归生成式语言模型(如用于生成文本的GPT)的预训练过程会优化大模型使其简单地预测给定序列中的下一个单词,直到序列完成。

        大模型使用自监督学习在大量语料库上进行预训练,原理可以参考《Transformer原理及关键模块深入浅出》。在预训练中,自回归模型会得到文本样本的开头,并反复被要求预测序列中的下一个单词,直到生成结束。对于每个预测,原始样本句子的实际下一个单词作为 “真实值”。通过梯度下降等优化算法迭代地调整模型参数(权重和偏差),以使模型的预测更接近原始文本,模型学习出训练数据中的语言模式,更进一步可能产生一定的涌现现象。

        所以可以想象,如果没有微调,基础大模型可能会对 “教我如何游泳” 的提示回复 “去专业的游泳馆”。这在语法上是一种合理的完成句子的方式,但显然不能满足用户的需求,用户的初始用意应该是让大模型输出游泳的姿势动作以及如何掌控等,以帮助其学会怎么游泳。所以在实际使用大模型的时候,微调一般是必须的,只不过你可能还没有意识到。就像之前开玩笑说会出现大量的提示语工程师(Prompt Engineer),提示语工程其实就是在做任务的微调,以便模型输出的结果更可能满足业务所需。

2.3 指令微调与普通微调的差异

        指令微调与标准的有监督微调之间的主要区别在于模型所训练的数据。有监督微调是在示例输入及其得出的输出上训练模型,而指令微调则用指令来充实输入 - 输出示例。以这种方式微调的大模型能够变得更加多功能和透明有用。指令微调通过给予模型明确的指令和反馈,为使模型专门化提供了一种替代方法。与微调只是提供输入输出示例不同,指令微调能够利用自然语言和对话,解释期望的行为和评估标准。例如,像 “请专注于仅总结这份报告的要点” 这样的指令提示。当然就像我们之前提到的,指令微调和其他微调技术不冲突,指令微调通常与微调结合使用。微调提供领域知识基础,而指令微调允许高效适应。

        指令微调的主要优势:

  • 与微调相比,需要的数据更少,适应速度更快。指令直接解释需要改进的地方。
  • 如果需求发生变化,指令可以迭代。
  • 能够通过对话式指导。
  • 指令微调的大模型能力与给定的指令有明确的联系。这一点对于商用系统非常重要,因为它使机构组织能够理解和解释大模型是如何做出决策和生成响应的。

2.4 怎么进行指令微调

        指令微调需要在标记的(输入、输出)对上进行有监督学习。指令微调与其他形式的有监督微调(SFT)的区别在于,指令数据集中的输入样本完全由类似于用户在提示中可能提出的请求的任务组成;输出展示了对这些请求的理想响应。这里展示Watsonx.ai的截图【5】,可能对于理解会有一定的帮助。

        基于前述对于指令微调技术的介绍,我们再来看以下这张图【6】,就可以理解指令微调中的关键,其实是指令数据集的构建。有了训练的指令数据集后就可以通过SFT完成模型微调。另外构建数据集的挑战之一是创建用于微调的高质量指令,这个在稍后的 Alpaca数据集中给出解决示例。

        指令数据集可以由人工创建,也可以由其他大模型生成【4】。如《Finetuned Language Models are Zero-Shot Learners》中所描述的,指令微调的目标是提高大模型对自然语言处理(NLP)指令的响应能力。指令微调结合了预训练-微调和提示工程这两种范式的优势。本质上,通过将提示工程的原则有机地融入到监督微调中,指令微调减少了为从微调模型中获得有效准确响应所需的提示工程和示例的数量。

        在一个指令数据集中,每个训练样本包括三个要素:

  1. 指令:指定给定任务的自然语言文本输入。例如,“将这句话从英语翻译成中文。”
  2. 附加信息:可选的补充信息,提供与当前任务相关的上下文。例如,阅读理解任务的输入可能包括一段简短的文章(然后指示模型回答关于它的给定问题)。
  3. 期望输出:根据提供的指令和上下文生成的目标输出,即响应。作为模型预测的真实标准,模型根据此标准进行评估和优化。

        这里以Alpaca 数据集为例,来看下数据的格式。Alpaca 数据集是用于指令微调语言模型的公开数据集之一。这个数据集被用于训练Alpaca模型(Llama 2 的指令微调版本)。该数据集包含 52000 个样本,这些样本是使用 text-davinci-003 模型生成的。数据集可以直接从 Hugging Face 数据集下载。      

        数据字段如下:

  • instruction(指令):描述模型应执行的任务。52000 条指令中的每一条都是唯一的。
  • input(输入):任务的可选上下文或输入。大约 40% 的示例有输入。
  • output(输出):由 text-davinci-003 生成的指令答案。
  • text(文本):指令、输入和输出按照作者用于微调他们的模型的提示模板进行格式化。

        Alpaca数据集构建方式:在 Self-Instruct 框架【13】的数据生成管道基础上进行了以下修改:

  • 使用 text-davinci-003 引擎生成指令数据。
  • 编写一个新的提示,明确向 text-davinci-003 给出了生成指令的要求。
  • 使用更激进的批量解码,即一次生成 20 条指令,这显著降低了数据生成的成本。
  • 通过舍弃分类指令和非分类指令之间的差异,简化数据生成管道。
  • 每个指令只生成一个实例,而不像 Self-Instruct 中那样生成 2 到 3 个实例。

        Self-Instruct 过程是一种迭代自举算法,它从一组手动编写的指令种子集开始,并用它们来提示语言模型生成新的指令以及相应的输入 - 输出实例。然后对这些生成结果进行过滤以去除低质量或相似的实例,所得数据被添加回任务池中。这个过程可以重复多次,从而产生大量的指令数据集合,可用于微调语言模型以更有效地遵循指令。

2.5 为什么指令微调能显著提升模型能力

        Google的论文【14】指出,他们的LaMDA-PT模型通过指令微调生成的变体模型,在那些自然呈现为指令的任务上取得了最显著的改进,例如翻译、问答、阅读理解和自然语言推理(NLI),即确定给定“假设”是否合乎逻辑地从给定“前提”中推导出来。

        一种解释是,为什么未经额外微调的预训练LLM在处理自然语言推理等任务时表现不佳,是因为类似典型NLI任务的段落在用于自监督预训练的未标注数据语料库中不太可能自然出现(这种解释还是比较直观的,比如总结文本的任务在一般的文本数据集中不太会自然出现)。相反,对于那些更接近预训练语言建模目标的任务,如要求模型正确完成句子的常识推理任务,指令在很大程度上是多余的(因此指令微调的益处较小)。

        因此针对NLI任务,指令微调有助于弥合模型的基本目标与用户让模型遵循指令并执行特定任务的目标之间的差距。这使得模型行为更加有用和可预测。

3. 指令微调实践

        基于第二章节对于指令微调技术的介绍后,相信对于指令微调的概念有了较好的理解,接下来通过具体的实施案例【9, 10】,来进一步加深对该技术的理解。

        本文将使用 Alpaca 数据集,探索如何预处理和格式化该数据集,用来训练一个 Llama 模型。

3.1 Alpaca-GPT4 数据集

        Alpaca-GPT4 数据集只是一个单一的 JSON 文件,“alpaca_gpt4_data.json” 包含了由 GPT-4 根据 Alpaca 中的提示生成的 52000 条遵循指令的数据。这个 JSON 文件与 Alpaca 数据具有相同的格式,只是输出是由 GPT-4 生成的。数据格式包含3部分:instruction、input、output。

import json


with open("alpaca_gpt4_data.json", "r") as f:
    alpaca = json.load(f)

print(len(alpaca))  # 52002


# 探查
one_row_6 = alpaca[5]
print("index 6:", one_row_6)
print(prompt_input(one_row_6))

one_row_21 = alpaca[20]
print("index 21:", one_row_21)
print(prompt_no_input(one_row_6))

输出示例:

52002
index 6: {'instruction': 'Identify the odd one out.', 'input': 'Twitter, Instagram, Telegram', 'output': 'The odd one out is Telegram. Twitter and Instagram are social media platforms mainly for sharing information, images and videos while Telegram is a cloud-based instant messaging and voice-over-IP service.'}


Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
Identify the odd one out.

### Input:
Twitter, Instagram, Telegram

### Response:

index 21: {'instruction': 'What does DNA stand for?', 'input': '', 'output': 'DNA stands for Deoxyribonucleic Acid. It is the molecule that carries the genetic instructions used in the growth, development, functioning, and reproduction of all living organisms.'}


Below is an instruction that describes a task. Write a response that appropriately completes the request.

### Instruction:
Identify the odd one out.

### Response:

3.2 数据预处理

        对数据进行格式化处理,主要针对带input和不带input进行区分处理,添加以下代码:        

def prompt_no_input(row):
    return ("Below is an instruction that describes a task. "
            "Write a response that appropriately completes the request.\n\n"
            "### Instruction:\n{instruction}\n\n### Response:\n").format_map(row)


def prompt_input(row):
    return ("Below is an instruction that describes a task, paired with an input that provides further context. "
            "Write a response that appropriately completes the request.\n\n"
            "### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n").format_map(row)


def create_prompt(row):
    return prompt_no_input(row) if row["input"] == "" else prompt_input(row)


prompts = [create_prompt(row) for row in alpaca]  # all LLM inputs are here
print("prompts", prompts)

        结束标记(End of String Token (EOS))为 “</s>”,这个标记很重要,因为它告诉模型何时停止生成文本;对于 LLaMa 模型,结束标记为 “</s>”。在每个响应的末尾明确添加这个标记。 存储指令和输出的连接结果。

EOS_TOKEN = "</s>"
outputs = [row['output'] + EOS_TOKEN for row in alpaca]

dataset = [{"prompt":s, "output":t, "example": s+t} for s, t in zip(prompts, outputs)]

        接下来,将数据集tokenizer处理,也就是词元化。对文本进行token处理后,再将输出转换为 PyTorch 张量。对输入进行填充以匹配长度。我们需要明确告诉tokenizer使用什么标记来进行填充,在本文中,采用结束标记(EOS)。

model_id = 'meta-llama/Llama-2-7b-hf'
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token

        进行训练集和验证集的切分处理。这里使用wandb

random.shuffle(dataset) # shuffle inplace
train_dataset = dataset[:-1000]
eval_dataset = dataset[-1000:]
train_table = wandb.Table(dataframe=pd.DataFrame(train_dataset))
eval_table = wandb.Table(dataframe=pd.DataFrame(eval_dataset))
with wandb.init(project="alpaca_ft", job_type="split_data"):
    wandb.log({"train_dataset":train_table, "eval_dataset":eval_table})

        为了使训练更高效并利用这些大模型的更长上下文,引入 “打包” 的操作。将组合多个示例来填充模型的内存并使训练更高效,而不是单独提供示例。这样避免了进行大量填充和处理不同长度的情况。这里的主要思路是指令/输出样本很短,所以将它们连接在一起,用结束标记分隔开。

        假设最大序列长度为 1024,则:

max_seq_len = 1024


def pack(dataset, max_seq_len=1024):
    tkds_ids = tokenizer([s["example"] for s in dataset])["input_ids"]
    
    all_token_ids = []
    for tokenized_input in tkds_ids:
        all_token_ids.extend(tokenized_input + [tokenizer.eos_token_id])
    
    packed_ds = []
    for i in range(0, len(all_token_ids), max_seq_len+1):
        input_ids = all_token_ids[i : i + max_seq_len+1]
        if len(input_ids) == (max_seq_len+1):
            packed_ds.append({"input_ids": input_ids[:-1], "labels": input_ids[1:]})
    return packed_ds


train_ds_packed = pack(train_dataset)
eval_ds_packed = pack(eval_dataset)

3.3 模型微调

        如上述代码所示,标签将是向左移动一位的输入。使用常规的交叉熵进行训练,并在这个打包后的数据集上预测下一个标记。

from torch.utils.data import DataLoader
from transformers import default_data_collator


batch_size = 8

train_dataloader = DataLoader(
    train_ds_packed,
    batch_size=batch_size,
    collate_fn=default_data_collator,
)

eval_dataloader = DataLoader(
    eval_ds_packed,
    batch_size=batch_size,
    collate_fn=default_data_collator,
    shuffle=False,
)

检查一下数据:

b = next(iter(train_dataloader))

b.keys(), b["input_ids"][0][:25], b["labels"][0][:25]

>> (dict_keys(['input_ids', 'labels']),

tensor([ 1, 13866, 338, 385, 15278, 393, 16612, 263, 3414, 29889,

14350, 263, 2933, 393, 7128, 2486, 1614, 2167, 278, 2009,

29889, 13, 13, 2277, 29937]),

tensor([13866, 338, 385, 15278, 393, 16612, 263, 3414, 29889, 14350,

263, 2933, 393, 7128, 2486, 1614, 2167, 278, 2009, 29889,

13, 13, 2277, 29937, 2799])) ### <<< ---- 移动一个token

# input_ids.shape: (8, 1024), labels.shape: (8, 1024)

        使用PyTorch来训练模型,简单地让模型完成句子。将配置的超参数存储在一个SimpleNamespace中。它像字典一样,但可以通过点操作符访问属性。例如,可以通过config.batch_size来访问批量大小,而不是使用config["batch_size"]

        另外也需要使用一些必要的技巧:

  • 只训练模型参数的一个子集,而不是整个模型。
  • 使用梯度检查点(Gradient Checkpointing)来节省GPU内存。检查点是一种通过在反向传播过程中消除并重新计算某些层的激活值来减少内存使用的方法,它用更多的计算时间换取更少的内存消耗。
  • 自动混合精度(Automatic Mixed Precision):这种技术使得训练速度显著加快,因为计算是在半精度(float16或bfloat16)下完成的。
  • 实现一个评估步骤,定期从模型中采样。
from types import SimpleNamespace

# 计算梯度累积步骤数,32为总步数,batch_size为每批次的大小
gradient_accumulation_steps = 32 // batch_size

# 创建一个配置对象,用于存储模型训练的超参数
config = SimpleNamespace(
    model_id='meta-llama/Llama-2-7b-hf',  # 使用的预训练模型
    dataset_name="alpaca-gpt4",  # 使用的数据集名称
    precision="bf16",  # 选择精度,bf16比fp16更快更好
    n_freeze=24,  # 冻结的层数,Llama 7B模型有32层,这里冻结24层
    lr=2e-4,  # 学习率
    n_eval_samples=10,  # 验证时生成的样本数量
    max_seq_len=max_seq_len,  # 序列的最大长度
    epochs=3,  # 在数据集上训练的轮次
    gradient_accumulation_steps=gradient_accumulation_steps,  # 每隔多少次迭代更新梯度,模拟更大的批次大小
    batch_size=batch_size,  # GPU能够处理的批次大小,取决于训练的层数
    log_model=False,  # 是否将模型上传到W&B(Weights & Biases)
    mom=0.9,  # 优化器参数中的动量值
    gradient_checkpointing = True,  # 是否使用梯度检查点,以进一步节省内存
    freeze_embed = True,  # 是否冻结嵌入层,通常不需要训练嵌入层,所以保持冻结状态
)


config.total_train_steps = config.epochs * len(train_dataloader) // config.gradient_accumulation_steps

        GPU 对 2 的幂次的 batch 可以发挥更好性能,因此batchsize设置成 16、32等。关于该参数的讨论,可以参考【15】。

        设置预训练模型的参数:

model = AutoModelForCausalLM.from_pretrained(
    config.model_id,
    device_map=0,
    trust_remote_code=True,
    low_cpu_mem_usage=True,
    torch_dtype=torch.bfloat16,
    use_cache=False,
)

        冻结模型以节省内存,训练整个模型代价昂贵,因此本文只训练模型参数的一个子集。基于Transformer的模型(如Llama)是一层层堆叠的相同结构,最后有一个分类层。Llama 2-7b有32个Transformer层,因此我们只会训练最后8层。当然也可以尝试冻结多少层进行实验,但总要训练分类头,即对预测的最后一层进行训练。通过不在冻结的层上计算梯度,这样可以节省大量内存。甚至也可以通过冻结嵌入层来获得更多的内存节省。另外,关于如何在内存中适配大模型,可以参考【16】。

n_freeze = 24  #冻结前24层
for param in model.parameters(): param.requires_grad = False
for param in model.lm_head.parameters(): param.requires_grad = True
for param in model.model.layers[n_freeze:].parameters(): param.requires_grad = True

# 是否对嵌入层进行冻结
if config.freeze_embed:
    model.model.embed_tokens.weight.requires_grad_(False);

# 通过设置梯度检查点来节省内存
if config.gradient_checkpointing:
    model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})

        需要为训练设置优化器和学习率调度器。这两个关键配置来告诉 PyTorch 如何计算优化步骤并相应地调整学习率。这里使用Adam 和余弦调度。使用 bfloat 设置训练循环。把损失函数设置为交叉熵。

from transformers import get_cosine_schedule_with_warmup


optim = torch.optim.Adam(model.parameters(), lr=config.lr, betas=(0.9,0.99), eps=1e-5)
scheduler = get_cosine_schedule_with_warmup(
    optim,
    num_training_steps=config.total_train_steps,
    num_warmup_steps=config.total_train_steps // 10,
)

def loss_fn(x, y):
    "A Flat CrossEntropy" 
    return torch.nn.functional.cross_entropy(x.view(-1, x.shape[-1]), y.view(-1))

        接下来设置模型的训练过程观察对象。创建一个简单的函数从模型中采样,从而直观地看到模型的输出是什么,设置获取默认的采样参数,并传入相应的模型 ID。保存诸如温度、top_p 等参数的默认值。每十分之一的总训练步骤中在评估数据集上运行模型,并记录模型预测结果。

from transformers import GenerationConfig


gen_config = GenerationConfig.from_pretrained(config.model_id)


def generate(prompt, max_new_tokens=100, gen_config=gen_config):
    with torch.inference_mode():
        tokenized_prompt = tokenizer(prompt, return_tensors='pt')['input_ids'].cuda()
        output = model.generate(tokenized_prompt, 
                            max_new_tokens=max_new_tokens, 
                            generation_config=gen_config)
    return tokenizer.decode(output[0][len(tokenized_prompt[0]):], skip_special_tokens=True)


def prompt_table(prompts, log=True):
    table = wandb.Table(columns=["prompt", "generation", "concat", "max_new_tokens", "temperature", "top_p"])
    for prompt in progress_bar(prompts):
        out = generate(prompt, test_config.max_new_tokens, test_config.gen_config)
        table.add_data(prompt, out, prompt+out, test_config.max_new_tokens, test_config.gen_config.temperature, test_config.gen_config.top_p)
    if log:
        wandb.log({"predictions":table})
    return table

        接下来,是设置验证集评估。在验证数据集上计算指标可以对训练的进展有直观的了解。对于大模型,还需要从模型中采样以可视化与你的数据的对齐情况。遍历评估数据加载器并累积损失和准确率。

@torch.no_grad()
def validate():
    model.eval();
    eval_acc = Accuracy()
    for step, batch in enumerate(tqdm(eval_dataloader)):
        batch = to_gpu(batch)
        with torch.amp.autocast("cuda", dtype=torch.bfloat16):
            out = model(**batch)
            loss = loss_fn(out.logits, batch["labels"])  # you could use out.loss and not shift the dataset
        eval_acc.update(out.logits, batch["labels"])
    # we log results at the end
    wandb.log({"eval_loss": loss.item(),
               "eval_accuracy": eval_acc.compute()})
    prompt_table(eval_dataset[:config.n_eval_samples], log=True)
    model.train();

        最后就开始循环训练了。遍历训练数据加载器,并在固定的步数后进行评估。它在训练结束时保存模型。

wandb.init(project="alpaca_ft",
           tags=["baseline","7b"],
           job_type="train",
           config=config)


# 训练
acc = Accuracy()
model.train()
train_step = 0
pbar = tqdm(total=config.total_train_steps)
for epoch in range(config.epochs):
    for step, batch in enumerate(train_dataloader):
        batch = to_gpu(batch)
        with torch.amp.autocast("cuda", dtype=torch.bfloat16):
            out = model(**batch)
            loss = loss_fn(out.logits, batch["labels"]) / config.gradient_accumulation_steps  # you could use out.loss and not shift the dataset  
            loss.backward()
        if step%config.gradient_accumulation_steps == 0:
            wandb.log({"train/loss": loss.item() * config.gradient_accumulation_steps,
                       "train/accuracy": acc.update(out.logits, batch["labels"]),
                       "train/learning_rate": scheduler.get_last_lr()[0],
                       "train/global_step": train_step})
            optim.step()
            scheduler.step()
            optim.zero_grad(set_to_none=True)
            train_step += 1
            pbar.update(1)
    validate()
pbar.close()

# 保存 model checkpoint
save_model(
	model, 
	model_name=config.model_id.replace("/", "_"), 
	models_folder="models/", log=config.log_model)
    
wandb.finish()

        另外在大模型场景中,评估模型的好坏,还可以用更强大的预训练模型来作为评估器【10】,因为强大的LLM(比如gpt-4)可以说一定程度上超过人类的能力,而且人工成本更高,所以采用gpt-4来做评估器,不失为一种可行解。之前和业内某大佬聊起来,他们在某些to B场景中,也会采用公开的强大的预训练模型来快速获得基准评估结果作为参考。

def gpt4_judge(instruction, gen1, gen2, model="gpt-4"):
    system_prompt = ("You will be presented with a choice of two possible responses for an instruction"
                     "You have to pick the best one and give a reason why.\n"
                     "The reponse should follow the instructions and use the provided context if there is some\n"
                    "If both answers are equivalent, pick the value 0")
    message = "{instruction}\n Answer 1: \n{gen1}\n Answer 2:\n{gen2}".format(instruction=instruction, gen1=gen1, gen2=gen2)
    completion = openai.chat.completions.create(
        model=model,
        messages=[{"role": "system",
                   "content": system_prompt,
                  },
                  {"role": "user",
                   "content": message,
                  },],
        function_call = {"name": "make_choice"},
        functions = [{
                "name": "make_choice",
                "description": "Select the best generation and explain why",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "choice": {
                            "type": "integer",
                            "description": "the choosen alternative, zero if equivalent",
                        },
                        "argument":{
                            "type": "string",
                            "description": "Reason why the choice was made",},},},
                    "required": ["choice", "argument"],},
        ],)
    return completion

4. 参考材料

【1】Supervised fine-tuning (SFT)

【2】An Introductory Guide to Fine-Tuning LLMs

【3】Supervised Fine-tuning: customizing LLMs

【4】What is instruction tuning?

【5】Instruction Fine-tuning of Large Language Models

【6】Instruction Tuning for Large Language Models: A Survey

【7】Fine-tuning large language models (LLMs) in 2024

【8】What is instruction-tuning?

【9】How to Fine-Tune an LLM Part 1: Preparing a Dataset for Instruction Tuning

【10】How to Fine-Tune an LLM Part 2: Instruction Tuning Llama 2

【11】Instruction Tuning GPT2 on Alpaca Dataset

【12】Alpaca: A Strong, Replicable Instruction-Following Model

【13】Self-Instruct 框架

【14】LaMDA: Language Models for Dialog Applications

【15】深度学习Batch Size玄学被打破

【16】Performance and Scalability: How To Fit a Bigger Model and Train It Faster

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源泉的小广场

感谢大佬的支持和鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值