[深度学习] 大模型学习3下-模型训练与微调

在文章大语言模型基础知识里,模型训练与微调作为大语言模型(Large Language Model,LLM)应用构建的主要方式被简要提及,本系列文章将从技术原理、实施流程及应用场景等维度展开深度解析。相关知识的进一步参考见:LLM训练理论和实战。本文作为系列的下半部分,本文作为该系列的下半部分,包含第3章并聚焦于大语言模型构建的实操细节与技术要点。上半部分,即文章大模型学习3上-模型训练与微调已系统阐述了大语言模型的基础理论与核心结构。

目录

  • [3 大语言模型构建](#3 大语言模型构建)
    • [3.1 数据预处理](#3.1 数据预处理)
    • [3.2 Chat Template](#3.2 Chat Template)
      • [3.2.1 什么是Chat Template](#3.2.1 什么是Chat Template)
      • [3.2.2 Chat Template实现](#3.2.2 Chat Template实现)
    • [3.3 大语言模型训练路径选择](#3.3 大语言模型训练路径选择)
    • [3.4 训练方式介绍](#3.4 训练方式介绍)
      • [3.4.1 预训练训练过程介绍](#3.4.1 预训练训练过程介绍)
      • [3.4.2 指令微调训练过程介绍](#3.4.2 指令微调训练过程介绍)
      • [3.4.3 LoRA介绍](#3.4.3 LoRA介绍)
  • [4 参考](#4 参考)

3 大语言模型构建

本章涉及相关代码的介绍,且代码运行需要GPU支持。

3.1 数据预处理

在模型训练中,数据预处理至关重要。如今训练流程已较为成熟,数据集的质量往往成为训练成败的关键。模型训练的一个核心环节是将文本转换为索引,这一过程依赖于分词器 (Tokenizer)。不同模型的分词器虽有差异,但其核心处理逻辑基本一致。

分词器如同"文本剪刀",将句子切分为有意义的token(如单字或词语),再将每个token映射为一个唯一的数字索引,以供模型处理。不同模型的分词器差异主要体现在分词粒度(如子词、字符、词级)、词汇表构建方式与规模,以及文本标准化规则(如大小写、标点处理)和特殊符号设计等方面。文本到索引的转换过程如下:

  1. 分词 (Tokenization):分词器首先将句子切分为token(例如,"我爱月亮"被切分为"我"、"爱"、"月亮")
  2. 索引映射 (Index Mapping):然后为每个token分配一个唯一的数字标识,即索引(例如,"我"对应索引 1,"爱"对应索引 2,"月亮"对应索引 3)
  3. 序列生成 (Sequence Generation):最终,句子"我爱月亮"就被转换为token索引序列 [1, 2, 3]

以下代码展示了如何使用unsloth库调用DeepSeek-R1-Distill-Qwen-1.5B模型及其分词器,将输入文本快速转换为模型所需的token序列。unsloth是一个专注于优化大语言模型推理的库,而 DeepSeek-R1-Distill-Qwen-1.5B是轻量级大语言模型,也是DeepSeek-R1系列中最小的模型。若想具体了解DeepSeek-R1系列模型,可参考: 一张图彻底拆解DeepSeek V3和R1双模型

unsloth的安装方法见其官方仓库unslothunsloth微调环境搭建。unsloth预置了多种常见的大语言模型,且所有模型均托管于Hugging Face。示例代码如下,为加快模型加载速度,代码中提供了从modelScope或镜像网站加载的选项。

python 复制代码
from unsloth import FastLanguageModel
# torch2.5版本以下防止unsloth加载出问题
from transformers import modeling_utils
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
    modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none","colwise",'rowwise']

# 原始模型地址:https://huggingface.co/unsloth/DeepSeek-R1-Distill-Qwen-1.5B
# 1. 利用modelscope库下载模型到本体,然后通过unsloth加载模型
from modelscope import snapshot_download
# 加载预训练模型 ,利用modelscope库
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"  # 假设这是支持分词的模型名称
# 下载模型
model_dir = snapshot_download(model_name)

# 从huggingface的镜像https://hf-mirror.com/中下载模型到本地
# model_dir= "./DeepSeek-R1-Distill-Qwen-1.5B"

# 设置最大序列长度,表示模型在一次前向传递中可以处理的最大令牌数量。
max_seq_length = 2048 

# https://hf-mirror.com/
# 调用FastLanguageModel.from_pretrained()方法加载预训练的模型和对应的Tokenizer(分词器)。
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = model_dir, # 从hf中直接调用:model_name = unsloth/DeepSeek-R1-Distill-Qwen-1.5B
    max_seq_length = 2048,  # 最大序列长度,表示模型在一次前向传递中可以处理的最大令牌数量。
    dtype = None,           # 自动检测(BF16或FP16)。BF16范围大,FP16精度高
    local_files_only=True   # 只用本地文件
)

# 分词
text = "《Deep Learning》中文版"

# 将文本编码为模型输入格式
inputs = tokenizer(text, return_tensors="pt")
print("编码结果:", inputs)
# {'input_ids': tensor([[151646,  26940,  33464,  20909,  25067, 104811,  40301]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1]])}

# 将编码转换回文本
decoded = tokenizer.decode(inputs["input_ids"][0])
print("解码结果:", decoded)
# <|begin▁of▁sentence|>《Deep Learning》中文版

# 分词器的词汇表大小
print(len(tokenizer))

上述代码中,input_ids是文本对应的token序列。需注意,token数量通常少于实际字数,因为token化并非一个汉字对应一个token,模型可能合并词语或拆分英文单词。如示例中"中文"对应索引104811。
attention_mask是一个与输入序列等长的二进制掩码(0/1组成),其中1表示对应位置的token需参与注意力计算。0表示该位置可被忽略(如填充token),以避免模型学习无效信息。

编码结果中的151646是分词器自动添加的起始标记<|begin▁of▁sentence|>的token ID,用于辅助模型理解文本结构。不同模型的起始标记可能不同,例如<s>[CLS]

3.2 Chat Template

3.2.1 什么是Chat Template

前面提到大语言模型发布时通常会推出基础版与对话版两个版本。其中,基础模型是经过大规模语料无监督预训练的模型,这类模型虽然学习了大量通用知识,但没有经过任何行为指导;而对话模型则是专门为用户交互构建的,通常采用提问与回答的格式,它是在基础模型的基础上,通过指令监督微调与基于人类反馈的强化学习进行优化得到的,能够与人进行对话,并且输出的结果更加符合预期、更易于控制,也更加安全。

想让大语言模型理解并生成好的对话,需要给它一个清晰的"剧本",这就是 Chat Template(聊天模板)。LLM的Chat Template是一种预定义规则,其作用是将对话历史,包括多轮用户消息、助理回复、系统提示等,格式化为模型能够理解和处理的单一字符串。从本质上来说,它是对话结构的"编码指南",目的是确保模型接收的输入符合其训练时所见到的格式。

那么,为什么需要Chat Template呢?原因主要有以下几点:

  • 结构化输入:LLM本身处理的是连续文本字符串,而对话是包含不同角色,如用户、助理、系统等的多轮交互,Chat Template定义了如何将这些角色、消息内容以及必要的特殊标记组合成连贯的字符串。
  • 模型兼容性:不同的模型,像Llama 2、Mistral、ChatGPT、Claude等,在训练时使用的对话格式不同,例如用不同的特殊标记来表示角色、消息边界等,Chat Template能够确保输入符合特定模型期望的格式。
  • 区分角色:清晰地标明文本是来自用户、助理还是系统指令,这对于模型理解上下文、遵循指令以及生成符合角色的回复来说至关重要。
  • 添加必要标记:通常需要添加一些特殊标记,比如<|im_start|><|im_end|>等,这些标记用于标识消息的开始和结束、角色的切换,同时还包括角色标识符,如system、user、assistant,以及分隔符,如换行符\n,用于分隔不同的部分。
  • 统一处理:它为开发者提供了一种标准化的方式来处理各种对话场景,包括单轮、多轮以及包含系统提示的场景,从而简化代码逻辑。
  • 防止提示注入:正确的模板有助于分离用户输入与指令,进而降低模型被诱导执行意外操作的风险。

Chat Template通常包含以下核心部分:

  1. 角色 (Role):标识对话参与者

    • system (系统): 类似于导演或旁白,用于设定对话的背景、模型扮演的角色以及需要遵守的规则。该角色通常只在对话开始时出现一次(可选但常用)。
    • user (用户): 代表真实人类用户输入的话语或提出的问题。
    • assistant (助手): 代表 AI 模型自身在对话历史中给出的回复(在连续对话中尤为重要)。
  2. 消息 (Message):对话的具体内容

    • 指每个角色对应的实际文本。例如,user 的消息是用户的问题文本,assistant 的消息是模型之前的回答文本。
  3. 特殊标记 (Special Tokens):对话的结构分隔符

    • 一些预定义、具有特定含义的词汇或符号。它们如同对话的"标点符号",用于清晰标记对话的开始、结束、角色切换等结构边界。常见的例子包括 <|im_start|>, <|im_end|>, [INST], [/INST] 等。
  4. 格式化规则 (Formatting Rules):组合各种元素的语法

    • 这是一套具体的语法规则,定义了如何将"角色"、"消息"和"特殊标记"按照正确的顺序和格式拼接组合,形成最终输入给模型处理的完整文本序列。它规定了整个"剧本"的书写规范。

关于Chat Template的详细介绍,可参考Chat_templatesChat Template。不同的LLM模型的Chat Template格式不一样,常见的如下几种:

  1. OpenAI ChatML,被 ChatGPT, GPT-4等使用,也是Hugging Face Transformers系列模型默认模板之一,DeepSeek系列和阿里的Qwen系列的Chat Template也采用类似结构:
python 复制代码
<|im_start|>system
{system_message}<|im_end|>
<|im_start|>user
{user_message_1}<|im_end|>
<|im_start|>assistant
{assistant_message_1}<|im_end|> 
<|im_start|>user
{user_message_2}<|im_end|>
<|im_start|>assistant  # 模型会从这里开始预测
  • 每条消息(包括系统提示、用户输入和助手回复)都以<|im_start|>{role}开头,并以<|im_end|>结尾。

  • 模型在预测/生成回复时,会从对话历史中最后一个<|im_start|>assistant标记之后的位置开始输出内容。模型在生成过程中会自动补全其回复内容,并最终输出 <|im_end|>标记来表示回复结束。

  1. Llama Chat Template:

Llama Chat Template是Meta的Llama系列模型使用的对话格式,由 <s>[INST] 标记开始,包含系统消息(用 <<SYS>><</SYS>> 包裹)和用户消息,以 [/INST] 结束后接助手回复,多轮对话时通过 </s><s>[INST] 分隔。

python 复制代码
<s>[INST] <<SYS>>
{system_message}
<</SYS>>

{user_message_1} [/INST] {assistant_message_1} </s><s>[INST] {user_message_2} [/INST]

3.2.2 Chat Template实现

那么在大语言模型中,chat template是如何实现的呢?事实上,大语言模型会提供一个使用Jinja2模板语法定义的字符串,专门用于格式化对话历史生成chat template字符串。Jinja2是一种模板引擎,它提供变量、控制结构(如循环和条件判断)以及过滤器等功能,用于生成动态文本。更多细节可参考chat_templating

Chat template是Jinja2的具体应用:通过其循环语法遍历消息列表,并按特定格式构建对话历史。常见的chat template字符串通常预置如下Jinja2模板:

复制代码
chat_template = """
{% for message in messages %}
{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}
{% endfor %}
"""

这个模板包含两种主要的Jinja2语法元素:

  1. 控制结构(由 {% ... %} 包围):

    这是一个for循环,用于遍历 messages 列表中的每个元素。循环内的 message 变量代表当前消息项:

    复制代码
    {% for message in messages %}
    ...
    {% endfor %}
  2. 表达式(由 {{ ... }} 包围):

    以下表达式生成单个消息的格式化文本。message['role']message['content'] 是动态变量,其值会根据当前消息替换:

    复制代码
    {{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}

渲染该模板时,会执行以下步骤:

  1. 解析模板:Jinja2解析器识别控制结构和表达式。

  2. 变量注入:需向模板提供包含 messages 变量的上下文。例如:

    python 复制代码
    context = {
        "messages": [
            {"role": "user", "content": "你好"},
            {"role": "assistant", "content": "您好!有什么可以帮您?"}
        ]
    }
  3. 执行控制结构:Jinja2 遍历 messages 列表中的每个元素。

  4. 计算表达式:对每条消息,生成格式化的文本。

  5. 合并结果:将所有消息文本合并为最终字符串。

对于上述 context,渲染结果如下:

复制代码
<|im_start|>user
你好<|im_end|>
<|im_start|>assistant
您好!有什么可以帮您?<|im_end|>

对话模板

unsloth库提供了便捷的模板管理和应用功能。以下代码展示了如何查看和使用模型自带的ChatML格式对话模板,以及如何通过apply_chat_template方法将对话历史转换为模型可接受的输入格式。unsloth库中该模块的更多介绍见:Chat Templates

python 复制代码
from unsloth import FastLanguageModel
# torch2.5版本以下防止unsloth加载出问题
from transformers import modeling_utils
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
    modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none","colwise",'rowwise']

# 原始模型地址:https://huggingface.co/unsloth/DeepSeek-R1-Distill-Qwen-1.5B
# 用modelscope库下载模型到本体,然后通过unsloth加载模型
from modelscope import snapshot_download
# 加载预训练模型 ,利用modelscope库
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"  # 假设这是支持分词的模型名称
# 下载模型
model_dir = snapshot_download(model_name)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = model_dir, # 从hf中直接调用:model_name = unsloth/DeepSeek-R1-Distill-Qwen-1.5B
    max_seq_length = 2048,  # 最大序列长度,表示模型在一次前向传递中可以处理的最大令牌数量。
    dtype = None,           # 自动检测(BF16或FP16)。BF16范围大,FP16精度高
    local_files_only=True   # 只用本地文件
)


# 注意模板格式是ChatML模板源代码, 为什Jinja2模板格式
print(tokenizer.chat_template)  # 显示模型的对话模板(可能很长),便于了解模型期望的输入格式

# 使用apply_chat_template格式化对话
# messages是一个用于存储对话历史的列表,其中每个元素代表一条对话消息
messages = [
    {"role": "system", "content": "You are a pirate chatbot who talks in shanties!"},
    {"role": "user", "content": "What's the best way to hide treasure?"}
]

# 应用tokenizer.chat_template模板解析messages数据
formatted_input = tokenizer.apply_chat_template(
    messages,
    tokenize=False,  # 设为True则直接返回token IDs
    add_generation_prompt=True  # 在末尾添加提示模型开始生成的标记(如<|im_start|>assistant)
)

print(formatted_input)
'''
<|begin▁of▁sentence|>You are a pirate chatbot who talks in shanties!<|User|>What's the best way to hide treasure?<|Assistant|><think>
'''

# 如果模型没有默认模板,或者你想覆盖它,可以手动设置
chat_template = """{% for message in messages %}{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}{% endfor %}"""
tokenizer.chat_template = chat_template

# 应用自定义模板解析messages数据
messages = [
    {"role": "system", "content": "You are a pirate chatbot who talks in shanties!"},
    {"role": "user", "content": "What's the best way to hide treasure?"}
]

formatted_input = tokenizer.apply_chat_template(
    messages,
    tokenize=False,  
    add_generation_prompt=True  
)

print(formatted_input)
'''
<|im_start|>system
You are a pirate chatbot who talks in shanties!<|im_end|>
<|im_start|>user
What's the best way to hide treasure?<|im_end|>
'''

对话历史结构化

messages是对话历史的结构化表示,它的作用包括:

  1. 符合行业标准:OpenAI的Chat Completions API 和Hugging Face的对话模型都使用这种格式
  2. 清晰区分角色:明确区分系统指令、用户输入和模型回复
  3. 支持多轮对话:可以包含任意长度的对话历史

为什么需要这种格式?

上下文维护:

python 复制代码
[
    {"role": "user", "content": "法国的首都是哪里?"},
    {"role": "assistant", "content": "巴黎"},
    {"role": "user", "content": "它的人口是多少?"}  # "它"指代巴黎
]

角色设定:

python 复制代码
{"role": "system", "content": "你是一位18世纪的海盗船长"}

多轮对话

python 复制代码
[
    {"role": "user", "content": "推荐一部科幻电影"}, # 用户提问
    {"role": "assistant", "content": "《星际穿越》很不错"}, # 模型回复
    {"role": "user", "content": "为什么推荐这部?"}  # 后续用户提问
]

以下代码展示了如何利用unsloth库基于对话模板生成回答:

python 复制代码
from unsloth import FastLanguageModel
# torch2.5版本以下防止unsloth加载出问题
from transformers import modeling_utils
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
    modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none","colwise",'rowwise']

# 原始模型地址:https://huggingface.co/unsloth/DeepSeek-R1-Distill-Qwen-1.5B
# 用modelscope库下载模型到本体,然后通过unsloth加载模型
from modelscope import snapshot_download
# 加载预训练模型 ,利用modelscope库
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"  # 假设这是支持分词的模型名称
# 下载模型
model_dir = snapshot_download(model_name)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = model_dir, # 从hf中直接调用:model_name = unsloth/DeepSeek-R1-Distill-Qwen-1.5B
    max_seq_length = 2048,  # 最大序列长度,表示模型在一次前向传递中可以处理的最大令牌数量。
    dtype = None,           # 自动检测(BF16或FP16)。BF16范围大,FP16精度高
    local_files_only=True   # 只用本地文件
)

# 无模板生成
def generate_without_template(prompt):
    inputs = tokenizer(prompt, return_tensors="pt", padding=True, truncation=True).to("cuda")
    outputs = model.generate(**inputs, max_new_tokens=1024, 
                             repetition_penalty=1.1) # 减少重复0
    model_response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    # 返回回答,包括输入
    return model_response

# 使用模板生成
def generate_with_template(messages):
    # 将message转换为模型期望的chat template格式
    # add_generation_prompt用于在末尾添加回复引导符(如 <|assistant|>)以指示模型开始生成回复
    # 注意对于DeepSeek R1系列模型,add_generation_prompt还会生成<think>符号,以表示进行深度思考
    prompt = tokenizer.apply_chat_template(messages, 
                                           tokenize=False,
                                           add_generation_prompt=True
                                           )
    # 将message所有内容进行编码
    inputs = tokenizer(prompt, return_tensors="pt", padding=True, truncation=True).to("cuda")
    outputs = model.generate(**inputs, max_new_tokens=1024, 
                             repetition_penalty=1.1) # 减少重复
    model_response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    return model_response

# 测试用例
test_prompt = "你好,请描述一下你最喜欢的季节?"

# 无模板生成
print("=== 无message模板的输出 ===")
raw_output = generate_without_template(test_prompt)
print(raw_output)  # 生成风格较为随机

# 有模板生成
print("\n=== 使用message模板的输出 ===")
messages = [
    {"role": "system", "content": "你是一个吃货,将从吃货的角度来回答问题"},
    {"role": "user", "content": test_prompt}
]
templated_output = generate_with_template(messages)
print(templated_output) # 从美食来回答问题

以下代码演示了如何利用unsloth库实现多轮对话功能。核心机制是维护一个持续更新的对话列表(messages),完整记录:

  • 初始系统设定(如角色定位)
  • 用户每次输入
  • AI每次回复

当用户提出新问题时,将整个对话历史(含所有上下文)输入模型。模型基于完整上下文生成连贯回复后,将新回复追加到列表中,形成持续增长的记忆链。关键步骤:

  1. 初始化含系统设定的messages
  2. 追加用户问题
  3. 输入完整messages调用模型
  4. 追加AI回复到列表
  5. 循环2-4步实现持续对话

随着轮次增加,messages不断扩展,模型通过完整上下文保持对话一致性。实现代码如下:

python 复制代码
from unsloth import FastLanguageModel
from transformers import modeling_utils
# 防止旧版torch兼容性问题
if not hasattr(modeling_utils, "ALL_PARALLEL_STYLES") or modeling_utils.ALL_PARALLEL_STYLES is None:
    modeling_utils.ALL_PARALLEL_STYLES = ["tp", "none","colwise",'rowwise']

from modelscope import snapshot_download
# 加载预训练模型
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"
model_dir = snapshot_download(model_name)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = model_dir,
    max_seq_length = 2048,
    dtype = None,
    local_files_only=True
)

def generate_with_template(messages):
    """
    使用对话模板生成回复(支持多轮对话)
    原理:通过维护messages列表保存完整的对话历史
    每个消息包含角色(role)和内容(content):
      - system: 系统设定(仅出现在开头)
      - user: 用户输入
      - assistant: AI回复
    每次生成时,模型会根据完整的对话上下文生成连贯的回复
    """
    # 应用对话模板,添加生成引导符
    prompt = tokenizer.apply_chat_template(
        messages, 
        tokenize=False,
        add_generation_prompt=True
    )
    inputs = tokenizer(prompt, return_tensors="pt", padding=True, truncation=True).to("cuda")
    outputs = model.generate(
        **inputs, 
        max_new_tokens=1024,
        repetition_penalty=1.1
    )
    
    # 只提取新生成的回复(去掉输入部分)
    input_length = inputs.input_ids.shape[1]
    generated_ids = outputs[0][input_length:]
    return tokenizer.decode(generated_ids, skip_special_tokens=True)

# ===== 多轮对话演示 =====
# 原理说明:通过维护不断增长的messages列表实现上下文记忆
# 初始化对话系统设定
messages = [
    {"role": "system", "content": "你是一个资深吃货,回答问题时总是从美食角度展开"}
]

# 第一轮对话
messages.append({"role": "user", "content": "你好!你最喜欢的季节是什么?"})
response = generate_with_template(messages)
print(f"用户:{messages[-1]['content']}")
print(f"AI:{response}")
messages.append({"role": "assistant", "content": response})  # 关键:将AI回复加入历史

# 第二轮对话(基于之前的所有上下文)
messages.append({"role": "user", "content": "为什么喜欢这个季节?有什么特别的美食吗?"})
response = generate_with_template(messages)
print(f"\n用户:{messages[-1]['content']}")
print(f"AI:{response}")
messages.append({"role": "assistant", "content": response})  # 再次更新对话历史

# 第三轮对话(继续基于完整上下文)
messages.append({"role": "user", "content": "能详细说说这道菜怎么做吗?"})
response = generate_with_template(messages)
print(f"\n用户:{messages[-1]['content']}")
print(f"AI:{response}")

# ===== 多轮对话原理可视化 =====
print("\n\n=== 多轮对话的上下文结构 ===")
print("整个对话过程中维护的messages列表结构:")
for i, msg in enumerate(messages):
    print(f"[{i}] {msg['role']}: {msg['content'][:50]}{'...' if len(msg['content'])>50 else ''}")

3.3 大语言模型训练路径选择

本文梳理了开发自己的大语言模型应用的六大常见典型场景及其训练路径选择指南:

  1. 场景一:算力资源充沛,志在冲击排行榜的全新模型训练
  • 适用情形:拥有丰富的显卡设备和大量数据,期望训练一个全新模型以实现最优性能表现。
  • 实施策略:采用标准的预训练流程,配合大规模有监督微调(SFT),再进行对齐训练。该方案资源消耗极大,一般用户很少采用。
  • 优点:
    • 有机会训练出性能卓越的全新模型,在排行榜上取得优异成绩,提升技术影响力。
    • 完整的训练流程能充分挖掘数据潜力,使模型具备强大的泛化能力。
  • 缺点:
    • 资源消耗极大,成本高昂,包括显卡设备购置、电力消耗等。
    • 训练周期长,需要大量时间进行预训练、微调和对齐训练。
  1. 场景二:手握海量未标注领域数据
  • 适用情形:积累了大量未标注数据,且其中知识未被预训练模型涵盖,需将其融入模型以契合实际场景。
  • 实施策略:基于现有预训练模型开展持续预训练。该过程与常规预训练相似,但资源与时间成本较低。
  • 优点:
    • 能有效利用未标注数据,丰富模型的知识储备,使其更好地适应特定领域场景。
    • 相比常规预训练,资源与时间成本较低,性价比高。
  • 缺点:
    • 持续预训练的效果可能不如全新训练,对模型性能提升存在一定局限。
    • 未标注数据的质量参差不齐,可能影响模型训练效果。
  1. 场景三:凭借标注数据提升特定问答能力
  • 适用情形:具备标注数据,需让模型掌握特定问答技巧(如依据行业数据提炼大纲)。
  • 实施策略:对模型进行有监督微调(SFT),需确保标注数据的质量和代表性。
  • 优点:
    • 能针对性地提升模型在特定问答任务上的能力,满足具体业务需求。
    • 训练过程相对简单,易于实施和调整。
  • 缺点:
    • 标注数据的获取成本较高,需要投入大量人力进行标注工作。
    • 如果标注数据质量不佳或缺乏代表性,可能导致模型过拟合或泛化能力差。
  1. 场景四:回答内容需严格契合既定知识(如金融知识)
  • 适用情形:要求回答严格依据特定知识源(如法律法规、产品说明书)。
  • 实施策略:
    • 策略A:用自有数据微调模型(提升理解领域知识或利用检索结果的能力),再结合检索增强生成(RAG)进行知识检索。
    • 策略B:直接利用预训练模型搭配RAG完成检索,效果高度依赖检索系统的准确性和覆盖度。
  • 优点:
    • 策略A:通过微调模型能提升其对领域知识的理解和利用能力,结合RAG可确保回答的准确性。
    • 策略B:实施简单,无需对模型进行复杂训练,能快速利用预训练模型和RAG系统。
  • 缺点:
    • 策略A:微调模型需要一定的算力和时间成本,且对自有数据的质量要求较高。
    • 策略B:效果高度依赖检索系统的准确性和覆盖度,如果检索系统不完善,可能导致回答错误或不完整。
  1. 场景五:定制符合特定规范的领域问答机器人
  • 适用情形:需开发领域问答机器人,且回答需符合特定格式或风格要求。
  • 实施策略:先通过SFT学习领域知识,再开展对齐训练规范回答风格,确保输出规范性。
  • 优点:
    • 能开发出符合特定领域需求和规范的问答机器人,提供高质量的服务。
    • 对齐训练可以有效规范回答风格,提升用户体验。
  • 缺点:
    • 训练过程复杂,需要多个阶段的训练和调整,对技术要求较高。
    • 对齐训练的效果可能受到奖励模型或偏好模型的影响,需要不断优化和改进。
  1. 场景六:资源有限下的高效模型适配
  • 适用情形:算力紧张(如单卡/少量卡),仍需利用自有数据提升模型在特定任务或领域的表现,追求性价比。
  • 实施策略:
    • 采用参数高效微调(PEFT)技术(如LoRA、Adapter):
      • 有标注数据:直接在目标任务上使用PEFT进行SFT。
      • 大量未标注数据:结合PEFT进行轻量级持续预训练。
    • 可搭配RAG补充知识,减少模型记忆负担。
  • 优点:
    • 在算力有限的情况下,能有效利用自有数据提升模型性能,性价比高。
    • PEFT技术减少了需要训练的参数数量,降低了训练难度和成本。
    • 搭配RAG可以补充知识,进一步提升模型的回答质量。
  • 缺点:
    • PEFT技术的效果可能不如全参数微调,对模型性能提升存在一定限制。
    • 轻量级持续预训练可能无法充分挖掘未标注数据的潜力,影响模型的知识储备。

3.4 训练方式介绍

如前所述,大语言模型的训练通常包含三个核心阶段:预训练(构建基础语言能力)、指令微调(赋予任务适配能力)和人类对齐(优化输出的安全性与价值观一致性)。对于资源有限的小型开发者而言,前两个阶段(预训练与指令微调)的技术门槛相对较低,可通过轻量化方法实现高效训练;而人类对齐阶段因涉及复杂的强化学习框架(如 RLHF)与大规模人工标注数据,对算力和工程化能力要求较高,因此小型团队通常难以深度参与。关于构建自己的大语言模型的更多内容,可参考:大语言模型从零开始训练全面指南

计划开发自有模型并在预训练阶段继续训练,通常以基础模型(Base Model)为起点,该过程依赖大规模无监督数据。基础模型的进一步预训练有两种主流范式:

  1. 全参数微调(Full Fine-tuning):更新模型全部参数,计算资源消耗大且需充足无监督数据,适用于算力充沛的机构。
  2. 参数高效微调(Parameter-Efficient Fine-Tuning,PEFT):冻结基础模型参数,仅训练新增模块(如LoRA、Adapter),显著降低计算和存储成本。

注:预训练/继续预训练不推荐轻量化PEFT,因为会导致模型泛化能力受限。但在数据有限或资源受限时,其高效性可作为实用方案。

若需进行指令微调,则需使用标注的指令数据(输入-输出对)。其核心目的是引导模型应用预训练获得的知识与能力,使其能遵循指令并适应多样化任务。指令微调通常无法赋予模型全新知识领域,但对优化其在特定领域的表现往往是必要的,它能将模型内化知识以符合任务要求的形式展现。

此阶段,基础模型或已具备初步指令能力的对话模型均可作为起点。选择需结合数据规模、任务需求和资源限制:

  • 数据有限时:优先选择对话模型作为起点更具优势。
  • 数据充足时:基础模型通常提供更大的定制潜力。

若资源允许,建议对两种起点均进行实验,依据效果择优。此阶段可灵活选择全量微调或参数高效微调进行训练。

3.4.1 预训练训练过程介绍

预训练阶段的核心逻辑:

  • 目标:让模型掌握通用语言能力(语法、事实知识、基础推理),通过海量无标注文本学习语言的统计规律。
  • 数据:使用纯文本(如网页、书籍、百科),无需人工标注的 input-label 配对。
  • 训练任务:掩码语言建模(Masked Language Modeling, MLM) 或 自回归语言建模(Autoregressive LM)。
    以下以 自回归式预训练(GPT系列采用的方式)为例说明:
  1. 原始数据

    复制代码
    商品签收后7天内,保持完好可无理由退货。请登录您的账户提交退货申请。
  2. 分词与Token化

    复制代码
    # 分词后的Token序列(示例值)
    tokens:      ["商品", "签收", "后", "7", "天内", ",", "保持", "完好", ...]
    input_ids:   [1024, 5078, 208, 25, 3819, 6, 3340, 2865, ...]
  3. 标签(labels)的生成

    • 核心规则:预测下一个Token
      预训练的 labels就是输入序列向后平移一位
      • 输入(input_ids):[1024, 5078, 208, 25, 3819, ...]
      • 标签(labels): [5078, 208, 25, 3819, ..., -100]
      • 最后一个Token无下一个Token,用 -100 忽略**
    python 复制代码
    # 自回归预训练的输入与标签关系
    input_ids: [1024, 5078, 208, 25, 3819]   # 输入序列
    labels:    [5078, 208, 25, 3819, -100]   # 目标:预测下一个Token
  4. 训练过程(自回归预测)

    • 模型根据前k个Token,预测第k+1个Token(即 labels 中的第 k 个值):

      复制代码
      输入: [1024]          → 预测: 5078 (标签中的第1个值)
      输入: [1024, 5078]   → 预测: 208  (标签中的第2个值)
      输入: [1024,5078,208]→ 预测: 25   (标签中的第3个值)
      ...
    • 损失计算:对所有非 -100 位置的预测计算交叉熵损失(即整个序列除了最后一个位置)。

如果想在预训练模型的基础上进行增量预训练,可参考这篇关于增量预训练的文章:增量预训练

3.4.2 指令微调训练过程介绍

在大语言模型(LLM)的指令微调任务中,我们会使用标注好的数据集。这些数据不仅包含用户的输入(问题),还包含我们期望模型给出的标准答案(labels)。为了训练模型,我们需要将用户的输入和期望的答案按照一个特定的模板(template)组合成一个完整的文本序列。这个序列随后会被转换成模型能够处理的数字形式,也就是TokenID序列。关于模型微调的更多内容,可参考:大模型微调Fine-tuning

假设我们正在微调一个客服机器人模型。一条标注数据可能是:

  • 用户输入(Input):"如何办理退货?"
  • 期望答案(Label):"您好,商品签收后7天内,保持完好可无理由退货。请登录您的账户提交退货申请。"

根据设定的模板(如 "用户问:{input}\n客服答:{label}"),组合后的完整序列是:

复制代码
用户问:如何办理退货?\n客服答:您好,商品签收后7天内,保持完好可无理由退货。请登录您的账户提交退货申请。 

这个文本序列会被分词器(Tokenizer)转换成对应的Token ID序列(以下是省略版):

python 复制代码
# 组合序列的Token ID表示 (示例值)
input_ids: [18, 42, 77, 93, 25,     64, 88, 52, 39, 71]
           |-----用户问题部分-----| |----期望答案部分----|

同时,我们会准备一个对应的 labels 序列,用于指导模型的学习目标:

  • 用户问题部分对应的Token会被替换成一个特殊的忽略标记 -100
  • 期望答案部分对应的Token则保持不变。
python 复制代码
# 对应的 labels 序列
labels:    [-100, -100, -100, -100, -100, 64, 88, 52, 39, 71]
           |---------被忽略部分---------| |----学习目标部分----|

那么模型是如何学习的?

模型的核心训练任务是自回归预测(Autoregressive Prediction):它根据当前看到的所有前面的Token,预测序列中下一个最可能出现的Token是什么。这个过程就像一步步填空:

复制代码
已知Token序列开头        -> 模型预测的下一个Token (真实的下一个Token)
[18]                   -> 预测:50   (错误,真实是:42)
[18, 42]               -> 预测:29   (错误,真实是:77)
[18, 42, 77]           -> 预测:93   (正确!)
[18, 42, 77, 93]       -> 预测:10   (错误,真实是:25)
[18, 42, 77, 93, 25]   -> 预测:70   (错误,真实是:64)  # 从这里开始预测答案部分
... (后续预测以此类推)

训练过程中的关键点:

  1. 屏蔽输入,聚焦答案: 模型在预测用户问题部分(对应labels中-100的位置)时产生的任何错误预测,都会被-100标记屏蔽掉。这些错误不会被计入模型需要改进的部分,因为模型的任务不是复述问题。
  2. 关注答案生成: 只有当模型尝试生成答案部分(即对应labels中非-100的位置)时,它的预测才会被评估。
  3. 计算损失(Loss): 系统会比较模型在答案部分实际预测出的整个Token序列(例如 [70, 81, 47, 36, 65])与真实的期望答案序列([64, 88, 52, 39, 71]),通过交叉熵计算两者之间的差异。这个差异量化的指标称为损失值(Loss)。损失值越大,意味着预测与期望答案偏差越大。
  4. 反向传播与优化: 基于计算出的Loss,训练算法(通常是反向传播和梯度下降)会调整模型内部的参数(如神经网络权重)。调整的目标是让模型下次在相同或类似上下文下,预测出的答案部分Token序列尽可能接近真实序列,从而降低Loss。

回到客服机器人例子。训练时:

  • 模型看到用户问:如何办理退货?\n客服答:对应的Token ([18, 42, 77, 93, 25])。
  • 当模型开始预测客服回答的第一个Token(即期望答案 "您好" 对应的Token 64)时,它的预测(比如错误地预测了70)会被计算到Loss中。
  • 接着,模型看到 "用户问...客服答:您好",预测下一个Token(期望是 "商品" 对应的Token 88),如果预测错误(如81),这个错误也会计入Loss。
  • 如此继续,直到预测完整个期望答案序列。只有模型在生成您好,商品签收后7天内... 这部分答案时犯的错误才会影响Loss的计算和模型参数的更新。模型在问题部分用户问:如何办理退货?\n的任何预测错误都会被忽略(-100的作用)。
  • 通过大量这样的样本训练,模型逐渐学会在看到 "如何办理退货?" 这类问题时,生成 "您好,商品签收后7天内..." 这样的标准回答。

3.4.3 LoRA介绍

LoRA(Low-Rank Adaptation,低秩适应)是大语言模型训练中常见的方式。关于LoRA的变体介绍见:LoRA及其变体概述

直接修改庞大语言模型(如GPT、Stable Diffusion)的核心参数非常困难。LoRA提供了一种巧妙方法:不直接改动原模型,而是添加一个微小的"技能升级包"。

LoRA工作原理:

  1. 添加小模块: 针对模型中一个关键的大型参数矩阵(M x N),LoRA 添加两个极小的辅助矩阵(M x d 和 d x N,其中 d 远小于M或N)。
  2. 冻结原参数: 训练新任务时,原始大型矩阵的参数完全锁定(冻结),保持不变。
  3. 只训练小模块: 仅训练新添加的两个小矩阵。
  4. 组合与叠加: 训练完成后,将两个小矩阵相乘(得到 M x N 矩阵),再将这个结果叠加到原冻结的大矩阵上。这相当于应用了一个精密的"微调补丁"。

那么为什么要使用LoRA:

  • 信息压缩的: 研究发现,大语言模型适应新任务所需的"关键调整"远少于其总参数量(矩阵的"有效秩"很低)。LoRA 通过设定一个小的 d(秩),统一捕捉这些最关键的方向。虽然略有简化(可能损失微量精度),但换来了巨大的效率提升,对于微调通常是极佳权衡。
  • 效果好的原因:大语言模型"学有余力" - 预训练好的大语言模型已具备海量通用知识。微调新任务(如特定写作风格)通常只需小幅调整其行为方向。
    • LoRA 的两个小矩阵就像一张精准的"提示卡",引导模型侧重或微调其已有知识。模型本身足够强大,配合这个小指引就能高效完成新任务。
    • "低秩"设定 (d) 恰好匹配了模型微调所需的关键调整方向数量,实现了资源消耗与任务效果的高效平衡。

LoRA 的优势与应用:

  • 省资源:只需训练两个微小的矩阵(d 很小),所需的计算力(算力)和显存大幅降低。这使得在普通硬件(如消费级显卡)上微调超大模型成为可能。
  • 轻量便捷: 生成的"升级包"(两个小矩阵)文件极小(几MB到几十MB),易于分享、加载和使用。
  • 灵活部署:
    • 可动态加载:将"升级包"与原模型配合使用(稍有额外计算)。
    • 或完全融合:将"升级包"永久合并到原模型中,使用效率等同原模型。
  • 持续进化: 基于LoRA,出现了更智能的变体(如AdaLoRA, SoRA),可自动调整不同矩阵的 d 值,寻求更好的效率精度平衡。

4 参考