哈基鹏的大模型之旅(四)
大模型微调
大模型微调介绍
大模型微调(Fine-tuning),就是在预训练好的大语言模型(例如Qwen、GPT系列、DeepSeek等等)基础上,利用特定的数据集对模型的参数进行小规模训练,以更好地适应特定任务或领域。相比从零开始训练一个模型,微调所需的数据和计算资源显著减少;可以在特定任务上取得更好的性能,因为模型在微调过程中会重点学习与任务相关的特性;可以在多种领域(如情感分析、问答系统等)上进行微调,从而快速适应不同应用场景。
预训练是 LLM 强大能力的根本来源,事实上,LLM 所覆盖的海量知识基本都是源于预训练语料。但是经过预训练的 LLM 好像一个博览群书但又不求甚解的书生,对什么样的偏怪问题,都可以流畅地接出下文,但他偏偏又不知道问题本身的含义,只会“死板背书”。这一现象的本质是因为,LLM 的预训练任务就是经典的 CLM,也就是训练其预测下一个 token 的能力,在没有进一步微调之前,其无法与其他下游任务或是用户指令适配。
因此,我们还需要第二步来教这个博览群书的学生如何去使用它的知识,也就是 SFT(Supervised Fine-Tuning,有监督微调)。所谓有监督微调,对于能力有限的传统预训练模型,我们需要针对每一个下游任务单独对其进行微调以训练模型在该任务上的表现。例如要解决文本分类问题,需要对 BERT 进行文本分类的微调;要解决实体识别的问题,就需要进行实体识别任务的微调。
微调技术概览:LoRA、QLoRA、P-Tuning 与全参数微调
LoRA
LoRA(Low-Rank Adaptation,低秩适配):LoRA通过在模型的部分权重上添加可训练的低秩矩阵来实现微调。简单来说,就是冻结原模型的大部分参数,仅在每层中引入很小的瓶颈层进行训练。这样做大幅减少了需要更新的参数数量。LoRA的优点是内存开销小、训练高效,在下游任务上的效果通常接近全参数微调。并且多个LoRA适配器可以在一个基模型上切换,方便一个模型服务多种任务。LoRA不增加推理时延,因为微调完可以将低秩权重与原权重合并。实验表明,使用LoRA微调后的性能往往与全量微调相当,但显存占用和计算量却显著降低。缺点是LoRA仍需加载完整的预训练模型作为基础(但可以使用8-bit/4-bit量化减小内存,占用稍高于更轻量的P-Tuning)。LoRA适用于中大型模型在中小规模数据上的高效微调,是目前社区中极为流行的方案。
由于LoRA微调一般只需要模型在预训练的基础上学会一些较简单的任务,因此即使只更新部分插入参数,仍然能达到较好的微调效果。同时,LoRA 插入的低秩矩阵是对原参数的分解,在推理时,可通过矩阵计算直接将 LoRA 参数合并到原模型,从而避免了推理速度的降低。
如果一个大模型是将数据映射到高维空间进行处理,这里假定在处理一个细分的小任务时,是不需要那么复杂的大模型的,可能只需要在某个子空间范围内就可以解决,那么也就不需要对全量参数进行优化了,我们可以定义当对某个子空间参数进行优化时,能够达到全量参数优化的性能的一定水平(如90%精度)时,那么这个子空间参数矩阵的秩就可以称为对应当前待解决问题的本征秩(intrinsic rank)。
预训练模型本身就隐式地降低了本征秩,当针对特定任务进行微调后,模型中权重矩阵其实具有更低的本征秩(intrinsic rank)。同时,越简单的下游任务,对应的本征秩越低。(Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning)因此,权重更新的那部分参数矩阵尽管随机投影到较小的子空间,仍然可以有效的学习,可以理解为针对特定的下游任务这些权重矩阵就不要求满秩。我们可以通过优化密集层在适应过程中变化的秩分解矩阵来间接训练神经网络中的一些密集层,从而实现仅优化密集层的秩分解矩阵来达到微调效果。
例如,假设预训练参数为 $\textcolor{blue}{θ_0^D}$,在特定下游任务上密集层权重参数矩阵对应的本征秩为 $\textcolor{blue}{θ^d}$,对应特定下游任务微调参数为 $\textcolor{blue}{θ^D}$,那么有:
$\textcolor{blue}{θ^D=θ_0^D+θ^dM}$
这个 $\textcolor{blue}{M}$ 即为 LoRA 优化的秩分解矩阵。想对于其他高效微调方法,LoRA 存在以下优势:
- 可以针对不同的下游任务构建小型 LoRA 模块,从而在共享预训练模型参数基础上有效地切换下游任务。
- LoRA 使用自适应优化器(Adaptive Optimizer),不需要计算梯度或维护大多数参数的优化器状态,训练更有效、硬件门槛更低。
- LoRA 使用简单的线性设计,在部署时将可训练矩阵与冻结权重合并,不存在推理延迟。
- LoRA 与其他方法正交,可以组合。
因此,LoRA 成为目前高效微调 LLM 的主流方法,尤其是对于资源受限、有监督训练数据受限的情况下,LoRA 微调往往会成为 LLM 微调的首选方法。
LoRA微调的原理如下:
LoRA 假设权重更新的过程中也有一个较低的本征秩,对于预训练的权重参数矩阵 $\textcolor{blue}{W_0∈R^{d×k}}$ ( $\textcolor{blue}{d}$ 为上一层输出维度,$\textcolor{blue}{k}$ 为下一层输入维度),使用低秩分解来表示其更新:
$\textcolor{blue}{W_0+\Delta W = W_0 + BA,\quad where \quad B \in R^{d \times r},A\in R^{r\times k}}$
在训练过程中,$\textcolor{blue}{W_0}$ 冻结不更新,$\textcolor{blue}{A}$、$\textcolor{blue}{B}$ 包含可训练参数。
因此,LoRA 的前向传递函数为:
$\textcolor{blue}{h=W_0x+\Delta Wx = W_0x + BAx}$
在开始训练时,对 $\textcolor{blue}{A}$ 使用随机高斯初始化,对 $\textcolor{blue}{B}$ 使用零初始化,然后使用 Adam 进行优化。训练思路如图所示:

在 Transformer 结构中,LoRA 技术主要应用在注意力模块的四个权重矩阵:$\textcolor{blue}{W_q}$、$\textcolor{blue}{W_k}$、$\textcolor{blue}{W_v}$、$\textcolor{blue}{W_0}$,而冻结 MLP 的权重矩阵。
通过消融实验发现同时调整 $\textcolor{blue}{W_q}$ 和 $\textcolor{blue}{W_v}$ 会产生最佳结果。
在上述条件下,可训练参数个数为:
$\textcolor{blue}{\theta = 2 \times L_{LoRA}\times d_{model} \times r}$
其中,$\textcolor{blue}{L_{LoRA}}$为应用 LoRA 的权重矩阵的个数,$\textcolor{blue}{d_{model}}$ 为 Transformer 的输入输出维度,$\textcolor{blue}{r}$ 为设定的 LoRA 秩。一般情况下,$\textcolor{blue}{r}$ 取到 4、8、16。
目前一般通过 peft 库来实现模型的 LoRA 微调。peft 库是 huggingface 开发的第三方库,其中封装了包括 LoRA、Adapt Tuning、P-tuning 等多种高效微调方法,可以基于此便捷地实现模型的 LoRA 微调。LoRA 微调的内部实现流程主要包括以下几个步骤:
1️⃣确定要使用 LoRA 的层。peft 库目前支持调用 LoRA 的层包括:nn.Linear、nn.Embedding、nn.Conv2d 三种。
在进行 LoRA 微调时,首先需要确定 LoRA 微调参数,其中一个重要参数即是
target_modules。target_modules一般是一个字符串列表,每一个字符串是需要进行 LoRA 的层名称,例如:1
target_modules = ["q_proj","v_proj"]
这里的
q_proj即为注意力机制中的 $\textcolor{blue}{W_q}$,v_proj即为注意力机制中的 $\textcolor{blue}{W_v}$。我们可以根据模型架构和任务要求自定义需要进行 LoRA 操作的层。在创建 LoRA 模型时,会获取该参数,然后在原模型中找到对应的层,该操作主要通过使用 re 对层名进行正则匹配实现:
1
2
3# 找到模型的各个组件中,名字里带"q_proj","v_proj"的
target_module_found = re.fullmatch(self.peft_config.target_modules, key)
# 这里的 key,是模型的组件名
2️⃣对每一个要使用 LoRA 的层,替换为 LoRA 层。所谓 LoRA 层,实则是在该层原结果基础上增加了一个旁路,通过低秩分解(即矩阵 $\textcolor{blue}{A}$ 和矩阵 $\textcolor{blue}{B}$)来模拟参数更新。
对于找到的每一个目标层,会创建一个新的 LoRA 层进行替换。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class LoraLayer:
def __init__(
self,
r: int, # LoRA 的秩
lora_alpha: int, # 归一化参数
lora_dropout: float, # LoRA 层的 dropout 比例
merge_weights: bool, # eval 模式中,是否将 LoRA 矩阵的值加到原权重矩阵上
):
self.r = r
self.lora_alpha = lora_alpha
# Optional dropout
if lora_dropout > 0.0:
self.lora_dropout = nn.Dropout(p=lora_dropout)
else:
self.lora_dropout = lambda x: x
# Mark the weight as unmerged
self.merged = False
self.merge_weights = merge_weights
self.disable_adapters = Falsenn.Linear 就是 Pytorch 的线性层实现。Linear 类就是具体的 LoRA 层,其主要实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class Linear(nn.Linear, LoraLayer):
# LoRA 层
def __init__(
self,
in_features: int,
out_features: int,
r: int = 0,
lora_alpha: int = 1,
lora_dropout: float = 0.0,
fan_in_fan_out: bool = False,
merge_weights: bool = True,
**kwargs,
):
# 继承两个基类的构造函数
nn.Linear.__init__(self, in_features, out_features, **kwargs)
LoraLayer.__init__(self, r=r, lora_alpha=lora_alpha, lora_dropout=lora_dropout, merge_weights=merge_weights)
self.fan_in_fan_out = fan_in_fan_out
# Actual trainable parameters
if r > 0:
# 参数矩阵 A
self.lora_A = nn.Linear(in_features, r, bias=False)
# 参数矩阵 B
self.lora_B = nn.Linear(r, out_features, bias=False)
# 归一化系数
self.scaling = self.lora_alpha / self.r
# 冻结原参数,仅更新 A 和 B
self.weight.requires_grad = False
# 初始化 A 和 B
self.reset_parameters()
if fan_in_fan_out:
self.weight.data = self.weight.data.T替换时,直接将原层的 weight 和 bias 复制给新的 LoRA 层,再将新的 LoRA 层分配到指定设备即可。
3️⃣冻结原参数,进行微调,更新 LoRA 层参数.
实现了 LoRA 层的替换后,进行微调训练即可。由于在 LoRA 层中已冻结原参数,在训练中只有 A 和 B 的参数会被更新,从而实现了高效微调。训练的整体过程与原 Fine-tune 类似,此处不再赘述。由于采用了 LoRA 方式,forward 函数也会对应调整:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17def forward(self, x: torch.Tensor):
if self.disable_adapters:
if self.r > 0 and self.merged:
self.weight.data -= (
transpose(self.lora_B.weight @ self.lora_A.weight, self.fan_in_fan_out) * self.scaling
)
self.merged = False
return F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
'''主要分支'''
elif self.r > 0 and not self.merged:
result = F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)
if self.r > 0:
result += self.lora_B(self.lora_A(self.lora_dropout(x))) * self.scaling
return result
else:
return F.linear(x, transpose(self.weight, self.fan_in_fan_out), bias=self.bias)替换时,直接将原层的 weight 和 bias 复制给新的 LoRA 层,再将新的 LoRA 层分配到指定设备即可。
QLoRA
QLoRA(Quantized LoRA,量化 LoRA):QLoRA可以看作是在LoRA基础上的进一步优化。它的核心是在训练时将预训练模型权重以4比特精度加载,大幅降低显存占用,同时同样仅训练LoRA低秩适配器。创新之处在于4-bit量化采用了Norm浮点格式(NF4)等技术,尽可能减少量化带来的性能损。研究表明,QLoRA在单张48GB GPU上就能微调64B参数的模型,而且性能与全16位精度微调几乎持平!这意味着,即使只有一块高端GPU,也能微调过去需要数十张GPU的大模型,这对普通开发者来说是革命性的。QLoRA的优点是极致地节省显存(比LoRA进一步减半左右),使单卡可微调更大的模型。缺点是实现稍复杂,需要依赖如bitsandbytes、可能还需要DeepSpeed等库支持4-bit训练,而且由于进行了强烈的量化,极少数情况下可能出现略微的性能下降或兼容性问题。QLoRA非常适合GPU内存非常有限但又想微调超大模型的情况。例如只有一块16GB卡却希望微调13B或33B参数模型时,QLoRA是不错的选择。
P-Tuning
P-Tuning(Prompt Tuning,软提示微调):P-Tuning是一种Prompt学习方法,通过为模型的输入添加一些可训练的虚拟token来引导模型输出。与直接调整模型权重不同,P-Tuning让模型保持原有权重不变,只是在输入序列开头插入若干新参数(这些参数在微调时会被更新)。可以理解为我们为每个任务学到了一个“魔法开头咒语”,让预训练模型更好地完成特定任务。P-Tuning的参数规模非常小(只相当于几百个词的嵌入向量),因此训练开销极低。它的优点是实现简单、所需内存极小,非常适合极少样本(Few-Shot)或需要针对很多不同提示调优的场景。缺点是适用范围受限:由于只调整输入提示,模型本身的表示能力没有改变,因而对于复杂任务或需要模型深度调整的场景,效果不如LoRA或全量微调。此外,P-Tuning主要针对生成类任务(prefix-tuning用于NLG,P-Tuning最初用于NLU任务)。总的来说,P-Tuning适合小数据快速尝试,或者配合其他微调一起使用,以进一步提升性能。
全参数微调
全参数微调(Full Fine-Tuning):这是一种最朴素也最暴力的方式——解冻预训练模型的所有参数,在下游数据上继续训练,使模型完整学习新任务。它的优点是在足够数据下能够获得最充分的适应效果,模型可以自由调整每一层参数来拟合新任务;但缺点也显而易见:资源消耗巨大(需要显存随模型大小线性增加,14B以上模型往往单卡无法全参数微调),过拟合风险更高(尤其当下游数据较少时,大量参数容易记忆训练集导致泛化变差)。另外,全量微调后的模型参数完全改变,如果要服务多个任务需要保存多份完整模型,部署成本高。一般来说,全参数微调适用于小模型或下游数据非常丰富且有充足计算资源的情况。在大模型上,由于效率太低,我们更推荐LoRA/QLoRA这类参数高效微调手段
大模型微调实战所涉及知识
数据预处理
在模型训练中,数据预处理至关重要。如今训练流程已较为成熟,数据集的质量往往成为训练成败的关键。模型训练的一个核心环节是将文本转换为索引,这一过程依赖于分词器 (Tokenizer)。不同模型的分词器虽有差异,但其核心处理逻辑基本一致。
分词器如同”文本剪刀”,将句子切分为有意义的token(如单字或词语),再将每个token映射为一个唯一的数字索引,以供模型处理。不同模型的分词器差异主要体现在分词粒度(如子词、字符、词级)、词汇表构建方式与规模,以及文本标准化规则(如大小写、标点处理)和特殊符号设计等方面。文本到索引的转换过程如下:
- 分词 (Tokenization):分词器首先将句子切分为token(例如,”我爱月亮”被切分为”我”、”爱”、”月亮”)
- 索引映射 (Index Mapping):然后为每个token分配一个唯一的数字标识,即索引(例如,”我”对应索引 1,”爱”对应索引 2,”月亮”对应索引 3)
- 序列生成 (Sequence Generation):最终,句子”我爱月亮”就被转换为token索引序列 [1, 2, 3]

大语言模型Transformers库-Tokenizer组件实践
Tokenizer是自然语言处理中的一个核心组件,它的主要功能是**将原始文本转换为机器学习模型能够处理的格式。**这一过程看似简单,实则包含了许多复杂且精细的步骤。在深度学习中的Transformer架构及其衍生模型中,Tokenizer的工作流程通常包括两个关键步骤:
- 首先,是文本分解。这一步的目的是将原始的、连续的文本分割成更细的粒度单元,这些单元可以是单词级别,也可以是子词级别,甚至是字符级别。这一步骤的目标是将文本分解为可以被模型理解并处理的基本单元。
- 其次,是编码映射。这一步的目标是将这些基本单元转换为模型可以理解的数值形式,最常见的形式是整数序列。这样,我们就可以将这些数值输入到模型中,让模型进行学习和预测。
在接下来的内容中,我们将详细探讨Tokenizer的工作原理,以及如何在实际的自然语言处理任务中使用Tokenizer。
Tokenizer的工作原理
Tokenizer的工作原理涉及:
- 文本分解 :将文本分解为更小的单元。
- 词汇表 :使用词汇表将文本单元映射到数值ID。
- 特殊标记 :添加如[CLS]、[SEP]等特殊标记,以适应模型的特定需求。
Tokenizer的使用方法
Tokenizer的使用流程一般遵循以下步骤:
导入Tokenizer库 :从NLP库(例如Hugging Face的transformers)导入Tokenizer类。
加载预训练Tokenizer :通过指定模型名称加载预训练的Tokenizer实例。
文本转换 :将文本数据输入Tokenizer进行编码转换。
获取编码输出 :Tokenizer输出编码后的数据,通常包括:
输入ID(Input ID):转换后的整数序列,用于模型输入。
注意力掩码(Attention Mask):标识哪些输入ID是有效内容,哪些是填充(padding)。
类别ID(Token Type IDs):在某些任务中区分句子对的两个不同句子。
代码示例:
1 | from transformers import AutoTokenizer |
以DeepSeek-R1-Distill-Qwen-7B为例
1 | from transformers import AutoTokenizer |
输出结果:
1 | LlamaTokenizerFast(name_or_path='/root/autodl-tmp/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B', vocab_size=141643, model_max_length=16384, is_fast=True, padding_side='left', truncation_side='right', special_tokens={'bos_token': '<|begin▁of▁sentence|>', 'eos_token': '<|end▁of▁sentence|>', 'pad_token': '<|end▁of▁sentence|>'}, clean_up_tokenization_spaces=False, added_tokens_decoder={ |
将句子分词:
1 | sen = "吃葡萄不吐葡萄皮!" |
输出结果:
1 | ['åIJĥ', 'èij¡èIJĦ', 'ä¸į', 'åIJIJ', 'èij¡èIJĦ', 'çļ®', '!'] |
每个基础模型的词典不同,可以通过tokenizer.vocab进行查看,你可以试试换一个中文词典的模型去分词。如果是查看词典大小则通过tokenizer.vocab_size。
接着,我们可以将词序列转换为id序列:
1 | ids = tokenizer.convert_tokens_to_ids(tokens) |
输出结果:
1 | [99404, 101480, 16430, 101377, 101480, 99888, 0] |
将token序列转换为string:
1 | str = tokenizer.convert_tokens_to_string(tokens) |
输出结果:
1 | 吃葡萄不吐葡萄皮! |
更快捷的方式是,直接将句子进行编码转换成id序列,之后再通过解码转换为字符串:
1 | # 编码 |
输出结果:
1 | [141646, 99404, 101480, 16430, 101377, 101480, 99888, 0] |
编码结果中的141646是分词器自动添加的起始标记<|begin▁of▁sentence|>的token ID,用于辅助模型理解文本结构。不同模型的起始标记可能不同,例如<s>或[CLS]。
在编码时,可以依据最大句子长度max_length进行填充:
1 | ids = tokenizer.encode(sen, padding="max_length", max_length=14) |
输出结果为:
1 | [141643, 141643, 141643, 141643, 141643, 141643, 141643, 141646, 99404, 101480, 16430, 101377, 101480, 99888, 0] |
也可以进行截断操作:
1 | ids = tokenizer.encode(sen, max_length=4, truncation=True) |
输出结果:
1 | [141646, 99404, 101480, 16430, 101377] |
调用方式: encode_plus和tokenizer
encode_plus方法提供了更多的功能和更细粒度的控制,包括对分词、编码、填充、截断等过程的额外配置。encode_plus方法通常返回一个字典,包含了一系列的输出,如输入ID、注意力掩码、标记类型ID等,这些输出可以直接用于模型的输入。- 此方法允许用户指定更多的参数,如
return_tensors(指定返回张量类型)、return_token_type_ids(返回标记类型ID)、return_attention_mask(返回注意力掩码)等。
1 | inputs = tokenizer.encode_plus(sen, padding="max_length", max_length=14) |
输出结果:
1 | {'input_ids': [141643, 141643, 141643, 141643, 141643, 141643, 141643, 141646, 99404, 101480, 16430, 101377, 101480, 99888, 0], 'attention_mask': [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]} |
input_ids是文本对应的token序列。需注意,token数量通常少于实际字数,因为token化并非一个汉字对应一个token,模型可能合并词语或拆分英文单词。如示例中”葡萄”对应索引101480。attention_mask是一个与输入序列等长的二进制掩码(0/1组成),其中1表示对应位置的token需参与注意力计算。0表示该位置可被忽略(如填充token),以避免模型学习无效信息。
直接使用tokenizer与上面方式等价:
1 | inputs = tokenizer(sen, padding="max_length", max_length=14) |
输出结果:
1 | {'input_ids': [141643, 141643, 141643, 141643, 141643, 141643, 141643, 141646, 99404, 101480, 16430, 101377, 101480, 99888, 0], 'attention_mask': [0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]} |
参数方面,我们需要关注的主要参数如下:
padding:设置为True或者longest则填充到批次中的最长序列(如果只提供单个序列,则不进行填充);设置为max_length则填充到用参数max_length指定的最大长度,或者如果未提供该参数,则填充到模型可接受的最大输入长度;设置为False或者do_not_pad则不进行填充。truncation:设置为True或longest_first则截断到用参数max_length指定的最大长度,或者如果未提供该参数,则截断到模型可接受的最大输入长度;设置为False或do_not_truncate(默认值)则不截断。max_length:如果未设置或设置为None,在截断/填充参数需要最大长度时,将使用预定义的模型最大长度。return_tensors:默认None,如果设置了该值,将返回张量而不是Python整数列表。可接受的值有tf(返回 TensorFlow 的 tf.constant 对象)、pt(返回 PyTorch 的 torch.Tensor 对象)、np(返回 Numpy 的 np.ndarray 对象)。
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通常包含以下核心部分:
角色 (Role):标识对话参与者
system (系统): 类似于导演或旁白,用于设定对话的背景、模型扮演的角色以及需要遵守的规则。该角色通常只在对话开始时出现一次(可选但常用)。
user (用户): 代表真实人类用户输入的话语或提出的问题。
assistant (助手): 代表 AI 模型自身在对话历史中给出的回复(在连续对话中尤为重要)。
消息 (Message):对话的具体内容
- 指每个角色对应的实际文本。例如,
user的消息是用户的问题文本,assistant的消息是模型之前的回答文本。
- 指每个角色对应的实际文本。例如,
特殊标记 (Special Tokens):对话的结构分隔符
- 一些预定义、具有特定含义的词汇或符号。它们如同对话的”标点符号”,用于清晰标记对话的开始、结束、角色切换等结构边界。常见的例子包括
<|im_start|>,<|im_end|>,[INST],[/INST]等。
- 一些预定义、具有特定含义的词汇或符号。它们如同对话的”标点符号”,用于清晰标记对话的开始、结束、角色切换等结构边界。常见的例子包括
格式化规则 (Formatting Rules):组合各种元素的语法
- 这是一套具体的语法规则,定义了如何将”角色”、”消息”和”特殊标记”按照正确的顺序和格式拼接组合,形成最终输入给模型处理的完整文本序列。它规定了整个”剧本”的书写规范。
不同的LLM模型的Chat Template格式不一样,常见的如下几种:
1️⃣OpenAI ChatML,被 ChatGPT, GPT-4等使用,也是Hugging Face Transformers系列模型默认模板之一,DeepSeek系列和阿里的Qwen系列的Chat Template也采用类似结构:
1 | <|im_start|>system |
- 每条消息(包括系统提示、用户输入和助手回复)都以
<|im_start|>{role}开头,并以<|im_end|>结尾。 - 模型在预测/生成回复时,会从对话历史中最后一个
<|im_start|>assistant标记之后的位置开始输出内容。模型在生成过程中会自动补全其回复内容,并最终输出<|im_end|>标记来表示回复结束。
2️⃣Llama Chat Template,是Meta的Llama系列模型使用的对话格式,由 <s>[INST] 标记开始,包含系统消息(用 <<SYS>> 和 <</SYS>> 包裹)和用户消息,以 [/INST] 结束后接助手回复,多轮对话时通过 </s><s>[INST] 分隔。
1 | <s>[INST] <<SYS>> |
那么在大语言模型中,chat template是如何实现的呢?事实上,大语言模型会提供一个使用Jinja2模板语法定义的字符串,专门用于格式化对话历史生成chat template字符串。Jinja2是一种模板引擎,它提供变量、控制结构(如循环和条件判断)以及过滤器等功能,用于生成动态文本。
Chat template是Jinja2的具体应用:通过其循环语法遍历消息列表,并按特定格式构建对话历史。常见的chat template字符串通常预置如下Jinja2模板:
1 | chat_template = """ |
这个模板包含两种主要的Jinja2语法元素:
控制结构(由
{% ... %}包围):这是一个
for循环,用于遍历messages列表中的每个元素。循环内的message变量代表当前消息项:1
2
3{% for message in messages %}
...
{% endfor %}表达式(由
{{ ... }}包围):以下表达式生成单个消息的格式化文本。
message['role']和message['content']是动态变量,其值会根据当前消息替换:1
{{'<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>' + '\n'}}
渲染该模板时,会执行以下步骤:
解析模板:Jinja2解析器识别控制结构和表达式。
变量注入:需向模板提供包含
messages变量的上下文。例如:1
2
3
4
5
6context = {
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "您好!有什么可以帮您?"}
]
}执行控制结构:Jinja2 遍历
messages列表中的每个元素。计算表达式:对每条消息,生成格式化的文本。
合并结果:将所有消息文本合并为最终字符串。
对于上述 context,渲染结果如下:
1 | <|im_start|>user |
记录1:Transformers的TrainingArguments当中的参数
具体示例
1 | training_args = TrainingArguments( |
具体参数介绍
1、output_dir (str):模型预测和检查点输出的目录。
**2、overwrite_output_dir (bool, defaults to False):**如果该参数为True,在输出目录output_dir已经存在的情况下将删除该目录并重新创建。默认值是False。
3、do_train (bool, defaults to False):是否进行训练。Trainer没有直接使用此参数,它应用在我们写的training/evaluation脚本。
4、do_eval (bool):是否对验证集进行评估,evaluation_strategy如果不是no的话,应该设置为true,Trainer没有直接使用此参数,它应用在我们写的training/evaluation脚本。
4、do_predict (bool, defaults to False):是否在测试集上进行预测,Trainer没有直接使用此参数,它应用在我们写的training/evaluation脚本。
6、evaluation_strategy (str or [~trainer_utils.IntervalStrategy], defaults to "no"):训练期间采用的评估策略,可选的值有:
"no":训练期间不进行评估"steps":每一个eval_steps阶段之后都进行评估"epoch":每一个epoch之后进行评估
7、prediction_loss_only (bool, defaults to False):当执行评估和预测的时候,是否仅仅返回损失。
8、per_device_train_batch_size (int, defaults to 8):每一个GPU/TPU 或者CPU核心训练的批次大小。
**9、per_device_eval_batch_size (int, defaults to 8):**每一个GPU/TPU 或者CPU核心评估的批次大小。
**10、gradient_accumulation_steps (int, optional, defaults to 1):**在执行向后/更新过程之前,用于累积梯度的更新步骤数。
11、eval_accumulation_steps (int):在将结果移动到CPU之前,累积输出张量的预测步骤数。如果如果未设置,则在移动到CPU之前,整个预测都会在GPU/TPU上累积(速度更快需要更多的内存)。
12、eval_delay (float):在执行第一次评估之前要等待的epoch或step,具体取决于evaluation_strategy。
13、learning_rate (float, defaults to 4e-4):AdamW优化器初始化的学习率。
14、weight_decay (float, defaults to 0):在AdamW优化器中,除了bias和LayerNorm权重,如果weight_decay不是零,则应用于所有层。
14、adam_beta1 (float, defaults to 0.9):AdamW优化器的beta1超参。
16、adam_beta2 (float, defaults to 0.999):AdamW优化器的beta2超参。
17、adam_epsilon (float, defaults to 1e-8):AdamW优化器的epsilon超参。
18、max_grad_norm (float, defaults to 1.0):最大梯度范数(用于梯度剪裁)。
19、num_train_epochs(float, defaults to 3.0):要执行的训练epoch的次数(如果不是整数,将执行停止训练前的最后一个epoch的小数部分百分比)。
20、max_steps (int, defaults to -1):如果设置为正数,则表示要执行的训练step的次数。覆盖num_train_epochs。在使用有限可迭代数据集的情况下,训练可能在所有数据还没训练完成时因达到设定的步数而停止。
21、lr_scheduler_type (str, defaults to "linear"):选择什么类型的学习率调度器来更新模型的学习率。可选的值有:"linear","cosine","cosine_with_restarts","polynomial","constant","constant_with_warmup"。
22、warmup_ratio (float, defaults to 0.0):线性预热从0达到learning_rate时,每步学习率的增长率。
23、warmup_steps (int, defaults to 0):线性预热从0达到learning_rate时,预热阶段的步数,它会覆盖warmup_ratio的设置。
24、log_level (str, defaults to passive):设置主进程上使用的日志级别。可选择的值:’debug’’info’’warning’’error’ ‘critical’’passive’ (不设置任何值,由应用进行设置)。
**24、log_level_replica (str, defaults to passive):**控制训练过程中副本节点的日志级别,设置参数和log_level一样。
**26、log_on_each_node (bool, defaults to True):**在多节点分布式训练中,是每个节点使用“log_level”进行一次日志记录,还是仅在主节点。
27、logging_dir (str):日志目录。
**28、logging_strategy (str , defaults to "steps"):**训练期间采用的日志策略,可选的值有:
"no":训练期间不记录日志"steps":每一个logging_steps阶段之后都记录日志"epoch":每一个epoch之后记录日志
**29、logging_first_step (bool, defaults to False):**global_step 表示训练的全局步数。当训练开始时,global_step 被初始化为 0,每次更新模型时,global_step 会自动递增。是否打印日志和评估第一个global_step。
**30、logging_steps (int, defaults to 400):**如果 logging_strategy="steps",则两个日志中更新step的数量。
**31、logging_nan_inf_filter (bool, defaults to True):**是否在日志中过滤掉 nan 和 inf 损失,如果设置为 True,每步的损失如果是 nan或者inf将会被过滤,将会使用平均损失记录在日志当中。
**32、save_strategy (str, defaults to "steps"):**训练过程中,checkpoint的保存策略,可选择的值有:
"no":训练过程中,不保存checkpoint"epoch":每个epoch完成之后保存checkpoint"steps":每个save_steps完成之后checkpoint
**33、save_steps (int, defaults to 400):**如果save_strategy="steps",则两个checkpoint 保存的更新步骤数
**34、save_total_limit (int, ):**如果设置了值,则将限制checkpoint的总数量,output_dir里面超过数量的老的checkpoint将会被删掉
**34、save_on_each_node (bool, defaults to False):**当进行多节点分布式训练,是否在每个节点上保存模型和checkpoint还是仅仅在主节点上保存。当不同节点使用相同的存储时,不应激活此选项,因为文件将以相同的名称保存到每个节点
**36、no_cuda (bool, defaults to False):**当有CUDA可以使用时,是否不使用CUDA
**37、seed (int, defaults to 42):**训练开始时设置的随机种子,为了确保整个运行的可再现性,可使用~Trainer.model_init函数来初始化模型的随机初始化参数。
**38、data_seed (int):**数据采样器的随机种子,它将用于数据采样器的可重现性,其独立于模型种子。
**39、jit_mode_eval (bool, defaults to False):**是否使用PyTorch jit trace来进行推理
**40、use_ipex (bool, defaults to False):**当PyTorch 的intel扩展可用时,是否使用
**41、bf16 (bool, defaults to False):**是否使用bf16 16位 (mixed) 精度训练替代32位训练. 要求Ampere或者更高的NVIDIA架构,或者使用CPU训练.
**42、fp16 (bool, defaults to False):**是否使用bf16 16位 (mixed) 精度训练替代32位训练.
43、fp16_opt_level (str, defaults to ‘O1’):fp16训练时, Apex AMP 优化级别选择,可选择的值有: [‘O0’, ‘O1’, ‘O2’, ‘O3’]Apex 是 NVIDIA 开发的一个混合精度训练和优化工具库,主要用于加速深度学习模型的训练过程。
**44、fp16_backend (str, defaults to "auto"):**此参数已经废弃,使用half_precision_backend替代
**44、half_precision_backend (str, defaults to "auto"):**半精度计算的后端实现,必须是这几个值:
"auto":具体是使用CPU/CUDA AMP 还是APEX依赖于PyTorch版本检测"cuda_amp""apex""cpu_amp"
**46、bf16_full_eval (bool, defaults to False):**是否使用完整的bfloat16评估而不是32位。这将更快并节省内存,但可能会造成指标的损伤。
**47、fp16_full_eval (bool, defaults to False):**是否使用完整的float16评估而不是32位。这将更快并节省内存,但可能会造成指标的损伤。
**48、tf32 (bool):**是否启用TF32 模式,可以在Ampere 和更新的GPU架构上使用,默认值依赖于PyTorch的torch.backends.cuda.matmul.allow_tf32的默认值。
**49、local_rank (int, defaults to -1):**分布式训练中进程的编号。在分布式训练中,每个进程(一般对应支持多线程的 GPU 卡)都会有一个特定的 local_rank,用于标识该进程对应的 GPU 编号。local_rank 的起始编号为 0,后续的编号依次递增。
**40、xpu_backend (str):**xpu分布式训练中的后端,只能是 "mpi" 或者 "ccl"其中之一
**41、tpu_num_cores (int):**当使用TPU训练时,TPU核心数 (自动通过启动脚本传递)。
**42、dataloader_drop_last (bool, defaults to False):**是否删除最后一个不完整的批次(如果数据集的长度不能被批次大小整除)。
**43、eval_steps (int):**如果 evaluation_strategy="steps",两个评估之间更新step的数量Number of update steps,如果没有设置,则使用与 logging_steps一样的值。
**44、dataloader_num_workers (int, defaults to 0):**数据加载的子进程数量(用于PyTorch ). 0表示数据由主进程加载。
**44、past_index (int, defaults to -1):**有些模型比如TransformerXL或者XLNet使用过去隐藏状态进行预测。如果这个参数设置为正数,则 Trainer使用相应的输出(通常是索引2)作为过去的状态,并将其作为 mems参数提供给模型的下一个训练step。
**46、run_name (str):**运行描述符。通常用于wandb以及mlflow日志记录。
**47、disable_tqdm (bool):**是否禁用在Jupyter Notebooks中由~notebook.NotebookTrainingTracker生成的tqdm进度条和指标表格。如果日志级别设置为warn或者更低的基本则默认值为True,否则为False 。
**48、remove_unused_columns (bool, defaults to True):**是否自动删除模型forward方法不使用的列 (TFTrainer暂时还没有实现该功能)。
**49、label_names (List[str]):**我们的输入字典的key列表相一致的标签,最终都将默认为["labels"],除非使用XxxForQuestionAnswering系列的模型,该系列的模型最终默认为["start_positions", "end_positions"]。
**60、load_best_model_at_end (bool, defaults to False):**是否在训练结束时加载训练期间发现的最佳模型。当设置为“True”时,参数“save_strategy”需要与“evaluation_strategy”相同,并且在这种情况下, “steps”和 save_steps 必须是eval_steps的整数倍。
**61、metric_for_best_model (str):**与load_best_model_at_end一起使用,指定用于比较两个不同模型。必须是评估返回的度量的名称,带或不带前缀“eval_”。如果没有设定且load_best_model_at_end=True,则默认使用 "loss",如果我们设置了这个值,则greater_is_better需要设置为 True。如果我们的度量在较低时更好,请不要忘记将其设置为“False”。
**62、greater_is_better (bool):**与load_best_model_at_end 和 metric_for_best_model一起使用,说明好的模型是否应该有更好的度量值。默认值:
True:如果metric_for_best_model设置了值,并且该值不是"loss"或者"eval_loss"False:如果metric_for_best_model没有设置值,或者该值是"loss"或者"eval_loss".
63、ignore_data_skip (bool, defaults to False):当恢复训练时,是否跳过之前训练时epoch和batch加载的数据,如果设置为True, 训练将会更快的开始,但是也不会产生与中断训练生成的相同的结果。
**64、sharded_ddp (bool, str or list of [~trainer_utils.ShardedDDPOption], defaults to False):**是否启用分片式分布式数据并行(Sharded Distributed Data Parallel,简称ShardedDDP),以加快训练速度和效率。可选项有:"simple"、"zero_dp_2"、"zero_dp_3"、"offload"如果入参是字符串,它将会使用空格进行分隔,如果入参是了bool,它将被转换为空“False”的列表和[“simple”]的“True”列表。
**64、fsdp (bool, str or list of [~trainer_utils.FSDPOption], defaults to False):**使用PyTorch 分布式并行训练(仅仅用在分布式训练)。可选项:"full_shard"、"shard_grad_op"、"offload"、"auto_wrap":使用 default_auto_wrap_policy自动递归。
66、fsdp_min_num_params (int, defaults to 0):用于指定使用 Fully Sharded Data Parallel (FSDP)时,最小可分片的参数数量。(仅在传递“fsdp”字段时有用)。
**67、deepspeed (str or dict):**使用Deepspeed。这是一个实验性功能,其API可能在未来发展。
**68、label_smoothing_factor (float, defaults to 0.0):**要使用的标签平滑因子。它的取值范围在 0 到 1 之间。当 label_smoothing_factor 的值为 0 时,表示不使用标签平滑技术,此时模型接受到完整的 one-hot 标签,当 label_smoothing_factor 的值大于 0 时,表示使用标签平滑技术,此时真实标签将是一个加权平均值,其中每个标签的概率都等于 (1-label_smoothing_factor)/num_classes,其中 num_classes 表示标签的数量。
**69、debug (str or list of [~debug_utils.DebugOption], defaults to ""):**启用一个或多个调试功能。这是一个实验特性。可选项有:
"underflow_overflow":检测模型的输入/输出中的溢出,并报告导致事件的最后一帧"tpu_metrics_debug":在TPU上打印度量。这些选项通过空格进行分隔。
**70、optim (str or [training_args.OptimizerNames] defaults to "adamw_hf"):**可以使用的优化器:adamw_hf adamw_torch adamw_apex_fused adafactor。
71、adafactor (bool, defaults to False):此参数已经废弃,使用 --optim adafactor 替代。
**72、group_by_length (bool, defaults to False):**是否将训练数据集中长度大致相同的样本分组在一起(以最大限度地减少所应用的填充并提高效率)。仅在应用动态填充时有用。
**73、length_column_name (str, defaults to "length"):**预计算列名的长度,如果列存在,则在按长度分组时使用这些值,而不是在训练启动时计算这些值。例外情况是:group_by_length设置为true,且数据集是Dataset的实例。
**74、report_to (str or List[str], defaults to "all"):**报告结果和日志的integration列表,支持的平台有:"azure_ml", "comet_ml", "mlflow", "tensorboard" 和"wandb". 使用 "all"则报告到所有安装的integration,配置为"none"则不报报告到任何的integration。
**74、ddp_find_unused_parameters (bool):**使用分布式训练时,通过find_unused_parameters把该值传递给DistributedDataParallel。如果使用梯度checkpoint,则默认为false,否则为true。
**76、ddp_bucket_cap_mb (int):**使用分布式训练时,传递给“DistributedDataParallel”的标志“bucket_cap_mb”的值。
**77、dataloader_pin_memory (bool, defaults to True):**当设置为True 时,在数据加载过程中,batch 数据会被放入 CUDA 中固定的固定内存,从而避免了从主内存到 GPU 内存的冗余拷贝开销,提升了数据读取的效率。
**78、skip_memory_metrics (bool, defaults to True):**是否跳过将内存探查器报告添加到度量中。默认情况下会跳过此操作,因为它会降低训练和评估速度。
**79、push_to_hub (bool, defaults to False):**每次当模型保存的时候,是否把模型推送到Hub。
**80、resume_from_checkpoint (str):**我们模型的有效checkpoint的文件夹的路径。此参数不是由直接给[Trainer]使用,它用于我们写的训练和评估脚本。
**81、hub_model_id (str):**与本地的 output_dir保持同步的仓库名称。它可以是将会推送到我们的命名空间里的一个非常简单的模型ID . 否则它将需要完整的仓库名称,比如 "user_name/model",它允许我们推送到一个我们是一个组织的成员之一("organization_name/model")的仓库。默认设置为user_name/output_dir_name,其中output_dir_name 是output_dir的值。
**82、hub_strategy (str or [~trainer_utils.HubStrategy], defaults to "every_save"):**定义推送到hub的内容的范围以及何时推送到hub,可能的值有:
"end":当~Trainer.save_model方法被调用的时候,会推送模型,推送它的配置、tokenizer(如果传给了Trainer)和model card 的草稿。"every_save":在每次模型保存的时候,都会推送,推送它的配置、tokenizer(如果传给了Trainer)和model card 的草稿。推送是异步的,不会影响模型的训练,如果模型保存的非常频繁,则新的推送只会在旧的推送完成之后进行推送,最后的一个推送是在模型训练完成之后"checkpoint":类似于"every_save",只是最后一个 checkpoint会被推送到名字为 last-checkpoint的子目录,它将方便我们使用trainer.train(resume_from_checkpoint="last-checkpoint")重新开始训练。"all_checkpoints": 类似于"checkpoint",只是所有的checkpoints都推送,就像它们出现在输出目录一样 (这样你就可以在最终的仓库里面获取每一个checkpoint)
**83、hub_token (str):**用于将模型推送到Hub的token。默认将使用huggingface-cli login获得的缓存文件夹中的令牌。
**84、hub_private_repo (bool, defaults to False):**如果为True, Hub repo将会被设置为私有的。
84、gradient_checkpointing (bool, defaults to False):如果为True,则使用梯度检查点以节省内存为代价降低向后传递速度。
86、include_inputs_for_metrics (bool,defaults to False):是否将输入传递给“compute_metrics”函数。这适用于需要在Metric类中进行评分计算的输入、预测和参考的度量。
87、auto_find_batch_size (bool, defaults to False):是否通过指数衰减自动找到适合内存的batch size,避免CUDA内存不足错误.需要安装 accelerate (pip install accelerate)。
88、full_determinism (bool, defaults to False):如果为 True,则使用enable_full_determinism替代set_seed来确保在分布式训练下获得可重复的结果。
89、torchdynamo (str):用于设置TorchDynamo后端编译器的token。可能的选择是[“eager”,“nvfuser]。这是一个实验性API,可能会更改。
90、ray_scope (str, defaults to "last"):Ray Tune 是一个开元的分布式超参数优化库,可以用于自动搜索最佳的超参数配置,以及并行化训练作业。使用Ray进行超参搜索的范围。
91、num_train_epochs (int):训练所用epoch数。
大模型微调实战
本次实战围绕预训练模型DeepSeek-R1-Distill-Qwen-7B进行高效微调。
参考内容:DeepSeek-R1-Distill-Qwen-7B本地部署方案指南
模型下载
使用modelscope中的snapshot_download函数下载模型,第一个参数为模型名称,参数cache_dir为模型的下载路径。
新建 model_download.py 文件并在其中输入以下内容,粘贴代码后请及时保存文件,如下所示。并运行 python model_download.py 执行下载。
1 | from modelscope import snapshot_download |
Swanlab配置与初始化
SwanLab 是一款开源、轻量的 AI 模型训练跟踪与可视化工具,提供了一个跟踪、记录、比较、和协作实验的平台。
SwanLab 面向人工智能研究者,设计了友好的Python API 和漂亮的UI界面,并提供训练可视化、自动日志记录、超参数记录、实验对比、多人协同等功能。在SwanLab上,研究者能基于直观的可视化图表发现训练问题,对比多个实验找到研究灵感,并通过在线网页的分享与基于组织的多人协同训练,打破团队沟通的壁垒,提高组织训练效率。
SwanLab的安装非常简单,只需要使用Python的包管理工具pip,一行命令安装即可:
1 | pip install swanlab |
Python版本需要>=3.8
SwanLab的云端版体验是比较好的(非常推荐),能够支持你在随时随地访问训练过程。要使用云端版之前需要先注册一下账号:
1.在电脑或手机浏览器访问SwanLab官网:https://swanlab.cn

2.点击右上角的黑色按钮「注册/登录」:

3.复制API Key: 完成填写后点击「完成」按钮,会进入到下面的页面。然后点击左边的「设置」:

4.登录: 当你运行python程序或者使用以下命令即可登录:
1 | swanlab login |

在出现的提示里把API Key粘贴进去(粘贴完不显示密码是正常的,这是命令行的特性),然后按回车,完成登录。

需要用到了SwanLab最核心的两个API swanlab.init()和 swanlab.log()
swanlab.init:创建SwanLab实验,支持传入项目名project、实验名experiment_name、超参数config、笔记description等一系列参数。- project参数: SwanLab用项目作为区分单位。实验可以理解为「文件」,项目就是「文件夹」。
project参数用于指定这次的实验创建在哪个项目下。 - experiment_name参数: 这个参数用于指定本次实验的名称。实验名称也可以在网页上修改。
- config参数:这个参数的作用是记录「超参数」,传入是1个字典。
- project参数: SwanLab用项目作为区分单位。实验可以理解为「文件」,项目就是「文件夹」。
swanlab.log: 负责记录学习率、损失值等指标(Metric),将传入的字典进行记录
加载模型和分词器
使用AutoModelForCausalLM和AutoTokenizer来加载模型和分词器。
1 | # 加载模型 |
数据集准备
本文数据集来源,魔塔社区的medical-o1-reasoning-SFT · 数据集。
本文主要说明,数据集格式是:
1 | { |
在 DeepSeek 的蒸馏模型微调过程中,数据集中引入 Complex_CoT(复杂思维链)是关键设计差异。若仅使用基础问答对进行训练,模型将难以充分习得深度推理能力,导致最终性能显著低于预期水平。这一特性与常规大模型微调的数据要求存在本质区别。








