9、微调-1
什么时候需要 Fine-Tuning
- 有私有部署的需求
- 开源模型原生的能力不满足业务需求
订酒店机器人
1 | [ |
一、先找找感觉
上手操作一个简单的例子:
- 情感分类
- 输入:电影评论
- 输出:标签 [‘neg’,’pos’]
- 数据源:https://huggingface.co/datasets/rotten_tomatoes
1.1、工具:介绍一个模型训练利器 Hugging Face
- 官网:http://www.huggingface.co
- 相当于面向 NLP 模型的 Github
- 尤其基于 transformer 的开源模型非常全
- 封装了模型、数据集、训练器等,使模型的下载、使用、训练都非常方便
安装依赖
1 | # pip安装 |
1.2、操作流程

- 以下的代码,都不要在Jupyter笔记上直接运行,会死机!!
- 请下载左边的脚本`experiments/tiny/train.py`,在实验服务器上运行。
- 导入相关库
1 | import datasets |
- 加载数据集
通过 HuggingFace,可以指定数据集名称,运行时自动下载
1 | # 数据集名称 |
- 加载模型
通过 HuggingFace,可以指定模型名称,运行时自动下载
1 | # 模型名称 |
- 加载 Tokenizer
通过 HuggingFace,可以指定模型名称,运行时自动下载对应 Tokenizer
1 | # 加载tokenizer |
1 | # 其它相关公共变量赋值 |
- 处理数据集:转成模型接受的输入格式
- 模型的输入:<提示词…><输入文本><提示词…>
- 模型的输出:<输出文本>
- 文本转 Token IDs:<PROMPT_TOKEN_IDS><INPUT TOKEN IDS><PROMPT_TOKEN_IDS><OUTPUT TOKEN IDS>
- PAD 成相等长度:
- <PROMPT_TOKEN_IDS><INPUT 1.1><INPUT 1.2>…<PROMPT_TOKEN_IDS><OUTPUT TOKEN IDS><PAD>…<PAD>
- <PROMPT_TOKEN_IDS><INPUT 2.1><INPUT 2.2>…<PROMPT_TOKEN_IDS><OUTPUT TOKEN IDS><PAD>…<PAD>
- 标识出不参与 Attention 计算的 Tokens(Attention Mask)
- 标识出参与 Loss 计算的 Tokens (只有输出 Token 参与 Loss 计算)
- <-100><-100>…<OUTPUT TOKEN IDS><-100>…<-100>
- 经过拼接和 PADDING 的输入输出 Token 序列
- Attention Mask 序列,标识出 1 中的有效 Tokens(用于 Attention 计算)
- Labels 序列,标识出 1 中作为输出的 Tokens(用于 Loss 计算)
1 | MAX_LEN=512 #最大序列长度(输入+输出) |
1 | # 处理训练数据集 |
- 定义数据规整器:训练时自动将数据拆分成 Batch
1 | # 定义数据校准器(自动生成batch) |
- 定义训练 超参:比如学习率
1 | LR=2e-5 # 学习率 |
- 定义训练器
1 | # 节省显存 |
- 开始训练
1 | # 开始训练 |
- 使用
tensorboard
工具可视化训练过程(可选)
在系统命令行模式下运行:
1 | tensorboard --logdir output |

- 加载训练后的模型进行推理(参考)
1 | from transformers import AutoTokenizer, AutoModelForCausalLM |
「Checkpoint」指的是在特定时间点保存的模型的状态。这个状态包括了模型的参数权重和优化器的状态,使得训练可以从这个点重新开始而不是从头开始。
通常,我们通过观察在验证集上的评估结果,选择某个 checkpoint 作为最终用于推理的模型。
- 加载 checkpoint 并继续训练(选)
1 | trainer.train(resume_from_checkpoint="/path/to/checkpoint") |
- 以上实验建议在 4G 以上显存的 GPU 上运行
- 如显存低于 24G 的情况下,可通过修改以下超参来降低显存需求
1 | # 此处只列出需修改的超参,其它超参与之前配置一致 |
- 以上实验是从生成模型视角训练一个分类任务;
- 上述分类任务也可以从分类器视角建模,使用类似 BERT 加额外分类器层的形式。
总结上述过程
- 加载数据集
- 数据预处理:
- 将输入输出按特定格式拼接
- 文本转 Token IDs
- 通过 labels 标识出哪部分是输出(只有输出的 token 参与 loss 计算)
- 加载模型、Tokenizer
- 定义数据规整器
- 定义训练超参:学习率、批次大小、…
- 定义训练器
- 开始训练
- 注意:训练后推理时,输入数据的拼接方式要与训练时一致
- 记住上面的流程,你就能跑通模型训练过程
- 理解下面的知识,你就能训练好模型效果
二、什么是模型
2.1、通俗(不严谨)的说、模型是一个函数:
- 先举个最简单的例子:
- $y=ax+b$: 描述输入 $x$ 与输出 $y$ 的关系
- 这个例子中,$x$ 与 $y$ 是「线性关系」,画在 x-y 轴上就是一条直线
- 其中 $a$, $b$ 是参数,决定这条直线的斜率和偏离原点的距离
- $a$ 和 $b$ 是未知的,需要我们从一组数据(${(x_i,y_i)}_{i=1\cdots N}$)中推导出来(训练)
- 实际问题中,$x$ 与 $y$ 不一定是直线关系,所以我们用一个更广义的表示
- $y=F(x;\omega)$: $F$ 是任意一个函数形式
- 它接收输入$x$:可以是一个词、一个句子、一篇文章或图片、语音、视频 …
- 这些物体都被表示成一个数学「矩阵」(其实应该叫张量,tensor)
- 它预测输出$y$
- 可以是「是否」({0,1})、标签({0,1,2,3…})、一个数值(回归问题)、下一个词的概率 …
- $F$ 的数学表达式就是网络结构(这里特指深度学习)
- $F$ 有一组参数 $\omega$,这就是我们要训练的部分
- 每条数据就是一对儿 $(x,y)$ ,它们是常量
- 参数是未知数,是变量
- $F$ 就是表达式:我们不知道真实的公式是什么样的,所以假设了一个足够复杂的公式(比如,一个特定结构的神经网络)
- 这个求解这个方程(近似解)就是训练过程
- 用数学(数值分析)方法找到使模型在训练集上表现足够好的一个值
- 表现足够好,就是说,对每个数据样本$(x,y)$,使 $F(x;\omega)$ 的值尽可能接近 $y$
2.2、一个最简单的神经网络
一个神经元:$y=f(\sum_i w_i\cdot x_i)$

把很多神经元连接起来,就成了神经网络:$y=f(\sum_i w_i\cdot x_i)$、$z=f(\sum_i w’_i\cdot y_i)$、$\tau=f(\sum_i w’’_i\cdot z_i)$、…

这里的$f$叫激活函数,有很多种形式
现今的大模型中常用的激活函数包括:ReLU、GELU、Swish

三、什么是模型训练
我们希望找到一组参数$\omega$,使模型预测的输出$\hat{y}=F(x;\omega)$与真实的输出$y$,尽可能的接近
这里,我们(至少)需要两个要素:
- 一个数据集,包含$N$个输入输出的例子(称为样本):$D={(x_i,y_i)}_{i=1}^N$
- 一个损失函数,衡量模型预测的输出与真实输出之间的差距:$\mathrm{loss}(y,F(x;\omega))$
- 例如:$\mathrm{loss}(y,F(x;\omega))=|y-F(x;\omega)|$
3.1、模型训练本质上是一个求解最优化问题的过程
$\min_{\omega} L(D,\omega)$
$L(D,\omega)=\frac{1}{N}\sum_{i=1}^N\mathrm{loss}(y_i,F(x_i;\omega))$
3.2、怎么求解
回忆一下梯度的定义
从最简单的情况说起:梯度下降与凸问题

梯度决定了函数变化的方向,每次迭代更新我们会收敛到一个极值
$\omega_{n+1}\leftarrow \omega_n - \gamma \nabla_{\omega}L(D,\omega)$
其中,$\gamma<1$叫做学习率,它和梯度的模数共同决定了每步走多远
3.3、现实总是没那么简单(1):深度学习没有全局最优解(非凸问题)

3.4、现实总是没那么简单(2):在整个数据集上求梯度,计算量太大了

- 如果全量参数训练:条件允许的情况下,先尝试Batch Size大些
- 小参数量微调:Batch Size 大不一定就好,看稳定性
3.5、现实总是没那么简单(3):学习率也很关键,甚至需要动态调整

四、求解器
为了让训练过程更好的收敛,人们设计了很多更复杂的求解器
- 比如:SGD、L-BFGS、Rprop、RMSprop、Adam、AdamW、AdaGrad、AdaDelta 等等
- 但是,好在对于 Transformer 最常用的就是 Adam 或者 AdamW
五、一些常用的损失函数
两个数值的差距,Mean Squared Error:$\ell_{\mathrm{MSE}}=\frac{1}{N}\sum_{i=1}^N(y_i-\hat{y}_i)^2$ (等价于欧式距离,见下文)
两个向量之间的(欧式)距离:$\ell(\mathbf{y},\mathbf{\hat{y}})=|\mathbf{y}-\mathbf{\hat{y}}|$
两个向量之间的夹角(余弦距离):
两个概率分布之间的差异,交叉熵:$\ell_{\mathrm{CE}}(p,q)=-\sum_i p_i\log q_i$ ——假设是概率分布 p,q 是离散的
这些损失函数也可以组合使用(在模型蒸馏的场景常见这种情况),例如$L=L_1+\lambda L_2$,其中$\lambda$是一个预先定义的权重,也叫一个「超参」

六、再动手复习一下上述过程
用 PyTorch 训练一个最简单的神经网络
数据集(MNIST)样例:

输入一张 28×28 的图像,输出标签 0–9
- 以下的代码,都不要在Jupyter笔记上直接运行,会死机!!
- 请将左侧的 `experiments/mnist/train.py` 文件下载到本地
- 安装相关依赖包: pip install torch torchvision
- 运行:python3 train.py
- 普通的 CPU 也足够运行此实验
1 | from __future__ import print_function |