摘要
在AutoDL平台使用GPU和AdvertiseGen数据集对ChatGLM2模型进行微调.
平台信息
- AutoDL
- NVIDIA RTX 4090 / 24GB(单精 82.58 TFLOPS / 半精 165.2 Tensor TFLOPS)
AutoDL简介
[https://www.autodl.com/home]
更大更全更专业的AI算力集群,即刻开启算力租用.
按小时租用GPU进行计算,使用容器进行一键环境配置,使用JupyterLab进行代码编辑.
LLM模型:ChatGLM2:简介
[https://github.com/THUDM/ChatGLM2-6B]
ChatGLM2是ChatGLM的第二代版本,由清华技术成果转化的公司智谱AI研发。它基于GLM130B千亿基础模型训练,具备多领域知识、代码能力、常识推理及运用能力。它支持与用户通过自然语言对话进行交互,可以处理多种自然语言任务,如对话聊天、智能问答、创作文章、创作剧本、事件抽取、生成代码等。
相比于初代ChatGLM模型,ChatGLM2在以下方面有显著的提升:
- 更强大的性能:基于ChatGLM初代模型的开发经验,全面升级了ChatGLM2的基座模型。ChatGLM2使用了GLM的混合目标函数,经过了1.4T中英标识符的预训练与人类偏好对齐训练,评测结果显示,在多个数据集上,ChatGLM2的性能取得了大幅度的提升,具有更强的竞争力。
- 更长的上下文:基于FlashAttention技术,将基座模型的上下文长度(Context Length)由ChatGLM的2K扩展到了32K,并在对话阶段使用8K的上下文长度训练,这允许模型处理更长文档,进行更多轮次的对话。
然而,请注意,尽管ChatGLM2在处理长文档和理解能力上有所提升,但在处理单轮超长文档时,其理解能力仍然有限。此限制会在后续的迭代升级中得到优化。
- ChatGLM2如何组织多轮对话训练数据?
[https://github.com/THUDM/ChatGLM2-6B/blob/main/ptuning/main.py#L180]
可以看到模型最终的输入是由prompt、answer和结束符拼接而成。其中prompt是由 tokenizer.build_prompt(query,history)得到的,也就是将历史对话和当前轮次的用户输入 进行拼接,而answer则是当前轮次的回复。
只有最后一轮的回复内容参与计算loss,其他 轮次的回复内容不参与计算loss,训练数据没有被充分利用,被浪费了。
数据集:AdvertiseGen:简介
[https://zhuanlan.zhihu.com/p/584836589]
[https://www.luge.ai/#/luge/dataDetail?id=9]
- AdvertiseGen广告文案生成数据集
- 以key-value输入生成开放式的文案描述文本
AdvertiseGen以商品网页的标签与文案的信息对应关系为基础构造,是典型的开放式生成任务,在模型基于key-value输入生成开放式文案时,与输入信息的事实一致性需要得到重点关注。
数据预览
任务描述:给定商品信息的关键词和属性列表kv-list,生成适合该商品的广告文案adv;
数据规模:训练集114k,验证集1k,测试集3k;
数据来源:清华大学CoAI小组;
数据样例:
{
"content": "类型#上衣*材质#牛仔布*颜色#白色*风格#简约*图案#刺绣*衣样式#外套*衣款式#破洞",
"summary": "简约而不简单的牛仔外套,白色的衣身十分百搭。衣身多处有做旧破洞设计,打破单调乏味,增加一丝造型看点。衣身后背处有趣味刺绣装饰,丰富层次感,彰显别样时尚。"
}
- 参考:
Shao, Zhihong, et al. "Long and Diverse Text Generation with Planning-based Hierarchical Variational Model." Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing and the 9th International Joint Conference on Natural Language Processing (EMNLP-IJCNLP). 2019.
模型微调方法概论
[https://cloud.tencent.com/developer/article/2305264]
[https://zhuanlan.zhihu.com/p/35890660]
[https://zhuanlan.zhihu.com/p/379147111]
给定预训练模型(Pre_trained model),基于模型进行微调(Fine Tune)。相对于从头开始训练(Training a model from scatch),微调为你省去大量计算资源和计算时间,提高了计算效率,甚至提高准确率。
不做微调: (1)从头开始训练,需要大量的数据,计算时间和计算资源。 (2)存在模型不收敛,参数不够优化,准确率低,模型泛化能力低,容易过拟合等风险。
普通预训练模型的特点是: 用了大型数据集做训练,已经具备了提取浅层基础特征和深层抽象特征的能力。
预训练模型
预训练模型就是已经用数据集训练好了的模型。现在我们常用的预训练模型就是他人用常用模型,比如VGG16/19,Resnet等模型,并用大型数据集来做训练集,比如Imagenet, COCO等训练好的模型参数。正常情况下,我们常用的VGG16/19等网络已经是他人调试好的优秀网络,我们无需再修改其网络结构。
微调主要方法
- LoRA
paper:LoRA: Low-Rank Adaptation of Large Language Models(https://arxiv.org/pdf/2106.09685.pdf)
code:[GitHub - microsoft/LoRA: Code for loralib, an implementation of "LoRA: Low-Rank Adaptation of Large Language Models"](https://github.com/microsoft/LoRA "GitHub - microsoft/LoRA: Code for loralib, an implementation of "LoRA: Low-Rank Adaptation of Large Language Models"")
基于上述背景,论文作者得益于前人的一些关于内在维度(intrinsic dimension)的发现:模型是过参数化的,它们有更小的内在维度,模型主要依赖于这个低的内在维度(low intrinsic dimension)去做任务适配。假设模型在任务适配过程中权重的改变量是低秩(low rank)的,由此提出低秩自适应(LoRA)方法,LoRA 允许我们通过优化适应过程中密集层变化的秩分解矩阵来间接训练神经网络中的一些密集层,同时保持预先训练的权重不变。
LoRA 的实现思想很简单,如下图所示,就是冻结一个预训练模型的矩阵参数,并选择用 A 和 B 矩阵来替代,在下游任务时只更新 A 和 B。
LoRA 的实现流程如下:
● 在原始预训练语言模型(PLM)旁边增加一个旁路,做一个降维再升维的操作,来模拟所谓的内在秩。
● 训练的时候固定 PLM 的参数,只训练降维矩阵 A 与升维矩阵 B。
● 模型的输入输出维度不变,输出时将 BA 与 PLM 的参数叠加。
● 用随机高斯分布初始化 A,用 0 矩阵初始化 B,保证训练的开始此旁路矩阵依然是 0 矩阵。
总的来说,基于大模型的内在低秩特性,增加旁路矩阵来模拟 full finetuning,LoRA 是一个能达成 lightweight finetuning 的简单有效的方案。目前该技术已经广泛应用于大模型的微调,如 Alpaca,stable diffusion+LoRA,而且能和其它参数高效微调方法有效结合,例如 State-of-the-art Parameter-Efficient Fine-Tuning (PEFT)
- Adapter
paper:Parameter-Efficient Transfer Learning for NLP (https://arxiv.org/pdf/1902.00751.pdf)
MAD-X: An Adapter-Based Framework for Multi-Task Cross-Lingual Transfer(https://arxiv.org/pdf/2005.00052.pdf)
在预训练模型每一层(或某些层)中添加 Adapter 模块(如上图左侧结构所示),微调时冻结预训练模型主体,由 Adapter 模块学习特定下游任务的知识。每个 Adapter 模块由两个前馈子层组成,第一个前馈子层将 Transformer 块的输出作为输入,将原始输入维度 d 投影到 m,通过控制 m 的大小来限制 Adapter 模块的参数量,通常情况下 m << d。在输出阶段,通过第二个前馈子层还原输入维度,将 m 重新投影到 d,作为 Adapter 模块的输出(如上图右侧结构)。通过添加 Adapter 模块来产生一个易于扩展的下游模型,每当出现新的下游任务,通过添加 Adapter 模块来避免全模型微调与灾难性遗忘的问题。Adapter 方法不需要微调预训练模型的全部参数,通过引入少量针对特定任务的参数,来存储有关该任务的知识,降低对模型微调的算力要求。
AdapterFusion 将学习过程分为两个阶段:
● 1.「知识提取阶段」:训练 Adapter 模块学习下游任务的特定知识,将知识封装在 Adapter 模块参数中。
● 2.「知识组合阶段」:将预训练模型参数与特定于任务的 Adapter 参数固定,引入新参数学习组合多个 Adapter 中的知识,提高模型在目标任务中的表现。
其中首先,对于 N 的不同的下游任务训练 N 个 Adapter 模块。然后使用 AdapterFusion 组合 N 个适配器中的知识,将预训练参数 Θ 和全部的 Adapter 参数 Φ 固定,引入新的参数 Ψ,使用 N 个下游任务的数据集训练,让 AdapterFusion 学习如何组合 N 个适配器解决特定任务。参数 Ψ 在每一层中包含 Key、Value 和 Query(上图右侧架构所示)。
在 Transformer 每一层中将前馈网络子层的输出作为 Query,Value 和 Key 的输入是各自适配器的输出,将 Query 和 Key 做点积传入 SoftMax 函数中,根据上下文学习对适配器进行加权。在给定的上下文中,AdapterFusion 学习经过训练的适配器的参数混合,根据给定的输入识别和激活最有用的适配器。「作者通过将适配器的训练分为知识提取和知识组合两部分,解决了灾难性遗忘、任务间干扰和训练不稳定的问题。Adapter 模块的添加也导致模型整体参数量的增加,降低了模型推理时的性能」。
Adapter Fusion 在 Adapter 的基础上进行优化,通过将学习过程分为两阶段来提升下游任务表现。作者对全模型微调(Full)、Adapter、AdapterFusion 三种方法在各个数据集上进行和对比试验。AdapterFusion 在大多数情况下性能优于全模型微调和 Adapter,特别在 MRPC(相似性和释义任务数据集)与 RTE(识别文本蕴含数据集)中性能显著优于另外两种方法。
- Prefix-tuning
paper:Prefix-Tuning: Optimizing Continuous Prompts for Generation(https://arxiv.org/pdf/2101.00190.pdf)
code:GitHub - XiangLi1999/PrefixTuning: Prefix-Tuning: Optimizing Continuous Prompts for Generation[1]
前缀微调(prefix-tunning),用于生成任务的轻量微调。前缀微调将一个连续的特定于任务的向量序列添加到输入,称之为前缀,如下图中的红色块所示。与提示(prompt)不同的是,前缀完全由自由参数组成,与真正的 token 不对应。相比于传统的微调,前缀微调只优化了前缀。因此,我们只需要存储一个大型 Transformer 和已知任务特定前缀的副本,对每个额外任务产生非常小的开销。
-
P-tuning
P-tuning 是稍晚些的工作,主要针对 NLU 任务。 -
prompt-tuning
Prompt-tuning 给每个任务定义了自己的 Prompt,拼接到数据上作为输入,同时 freeze 预训练模型进行训练,在没有加额外层的情况下,可以看到随着模型体积增大效果越来越好,最终追上了精调的效果. -
Freeze
即参数冻结,对原始模型部分参数进行冻结操作,仅训练部分参数,就可以对大模型进行训练。
p-tuning-v2:模型微调方法简介
[https://www.heywhale.com/mw/project/64984a7b72ebe240516ae79c]
[https://github.com/THUDM/P-tuning-v2]
P-Tuning v2是一种参数高效的提示调优策略,可以在小型和中等规模的模型和序列标注挑战中实现与微调相当的性能。它通过在预训练的Transformer的每一层输入上应用连续提示来增加连续提示的容量,并弥合了与微调的差距,特别是在小型模型和困难任务方面。
paper:[2103.10385] GPT Understands, Too[2]
P-tuning 是稍晚些的工作,主要针对 NLU 任务。对于 BERT 类双向语言模型采用模版(P1, x, P2, [MASK], P3),对于单向语言模型采用(P1, x, P2, [MASK]).
同时加了两个改动:
(1). 考虑到预训练模型本身的 embedding 就比较离散了(随机初始化+梯度传回来小,最后只是小范围优化),同时 prompt 本身也是互相关联的,所以作者先用 LSTM 对 prompt 进行编码;
(2). 在输入上加入了 anchor,比如对于 RTE 任务,加上一个问号变成[PRE][prompt tokens][HYP]?[prompt tokens][mask]后效果会更好;
p-tuning 的效果很好,之前的 Prompt 模型都是主打小样本效果,而 P-tuning 终于在整个数据集上超越了精调的效果.
- prompt-tuning
Prompt-tuning 给每个任务定义了自己的 Prompt,拼接到数据上作为输入,同时 freeze 预训练模型进行训练,在没有加额外层的情况下,可以看到随着模型体积增大效果越来越好,最终追上了精调的效果.
同时,Prompt-tuning 还提出了 Prompt-ensembling,也就是在一个 batch 里同时训练同一个任务的不同 prompt,这样相当于训练了不同「模型」,比模型集成的成本小多了。
实现
1. 上传模型文件到/root/autodl-fs存储
2. 创建示例并进入
[https://www.codewithgpu.com/i/THUDM/ChatGLM2-6B/ChatGLM2-6B]
docker pull registry.cn-beijing.aliyuncs.com/codewithgpu/thudm-chatglm2-6b:SPawIKFit4
访问JupyterLab网页.
3. 开始训练
cd /root/temp/ptuning
touch /root/temp/ptuning/TranAndTest.ipynb
TrainAndTest.ipynb
%cd /root/temp/ptuning
!PRE_SEQ_LEN=128
!LR=2e-2
!NUM_GPUS=1
!torchrun --standalone --nnodes=1 --nproc-per-node=1 main.py \
--do_train \
--train_file AdvertiseGen/train.json \
--validation_file AdvertiseGen/dev.json \
--preprocessing_num_workers 10 \
--prompt_column content \
--response_column summary \
--overwrite_cache \
--model_name_or_path /root/autodl-fs/chatglm2-6b \
--output_dir output/adgen-chatglm2-6b-pt-128 \
--overwrite_output_dir \
--max_source_length 64 \
--max_target_length 128 \
--per_device_train_batch_size 1 \
--per_device_eval_batch_size 1 \
--gradient_accumulation_steps 16 \
--predict_with_generate \
--max_steps 3000 \
--logging_steps 10 \
--save_steps 1000 \
--learning_rate 2e-2 \
--pre_seq_len 128 \
--quantization_bit 4
TrainAndTest.ipynb
# 微调后
import os
import torch
from transformers import AutoConfig
# 加载模型
model_path = "/root/autodl-fs/chatglm2-6b"
from transformers import AutoTokenizer, AutoModel
# 载入Tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
# 使用 Markdown 格式打印模型输出
from IPython.display import display, Markdown, clear_output
def display_answer(model, query, history=[]):
for response, history in model.stream_chat(
tokenizer, query, history=history):
clear_output(wait=True)
display(Markdown(response))
return history
print("微调后")
#从预训练的模型路径model_path中加载配置文件,trust_remote_code=True表示信任远程代码,pre_seq_len=128表示设置输入序列的最大长度为128。
config = AutoConfig.from_pretrained(model_path, trust_remote_code=True, pre_seq_len=128)
#根据配置文件和模型路径,加载预训练模型,trust_remote_code=True表示信任远程代码。
model = AutoModel.from_pretrained(model_path, config=config, trust_remote_code=True)
#加载微调后的模型参数,使用torch.load函数将参数存储在prefix_state_dict中。
prefix_state_dict = torch.load(os.path.join("/root/temp/ptuning/output/adgen-chatglm2-6b-pt-128/checkpoint-3000", "pytorch_model.bin"))
#调后的模型参数中的前缀编码器相关的部分提取出来,存储在new_prefix_state_dict中。
new_prefix_state_dict = {}
for k, v in prefix_state_dict.items():
if k.startswith("transformer.prefix_encoder."):
new_prefix_state_dict[k[len("transformer.prefix_encoder."):]] = v
#将提取出来的前缀编码器相关参数加载到模型model的前缀编码器中
model.transformer.prefix_encoder.load_state_dict(new_prefix_state_dict)
#将模型转换为半精度浮点数并移动到GPU上进行加速计算。
model = model.half().cuda()
#将前缀编码器转换回单精度浮点数类型。
model.transformer.prefix_encoder.float()
#将模型设置为评估模式,这意味着模型将被用于推断而不是训练。
model = model.eval()
display_answer(model, "类型#上衣\*材质#牛仔布\*颜色#白色\*风格#简约\*图案#刺绣\*衣样式#外套\*衣款式#破洞")
4. 测试及效果
牛仔外套一直是时尚的代名词,这款外套采用白色调,简约大气,又具有时尚感。胸口刺绣图案,丰富层次,增加时尚感。衣身采用破洞设计,增加层次感,又增添酷酷的气息。