minimind/notebook/6_lora.ipynb

600 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"cells": [
{
"cell_type": "markdown",
"id": "fd56e0ed-d6e3-4fdd-bed2-2a0c32fb61f9",
"metadata": {},
"source": [
"# 6-LoRA\n",
"\n",
"大语言模型的低秩适应(LoRA, Low-Rank Adaptation of Large Language Models) 是一项大模型参数高效微调技术,其可以显著减少可训练参数的数量.\n",
"\n",
"由于大模型参数量较大,直接进行全参微调需要消耗大量硬件资源,LoRA 的工作原理是将少量的新权重插入倒模型中,并且仅训练这些权重,这使得使用 LoRA 进行训练的速度更快、内存效率更高,并生成更小的模型权重.\n",
"\n",
"具体来说,LoRA 它冻结了预训练模型 W 的权重,并注入可训练的秩分解矩阵 A 与 B,在微调时,只训练降维矩阵 A 和 升维矩阵 B,微调结束后,将 AB 与 W 进行叠加.\n",
"\n",
"![images](./images/lora.png)\n",
"\n",
"其中,用随机高斯分布进行初始化 A,用 0 矩阵初始化 B,从而保证训练开始时旁路矩阵为 0 矩阵.\n",
"\n",
"具体来看,假设模型经过预训练主干的输出为 $W_0 x$,在 LoRA 微调阶段,我们可以用如下形式对输出进行表示.\n",
"\n",
"$$h=W_0x + \\Delta Wx = W_0x + BA x=(W_0 + BA)x$$\n",
"\n",
"其中, $B \\in \\mathbb{R}^{d \\times r},A \\in \\mathbb{R}^{r\\times k}$,r 为 LoRA 低秩矩阵的维数,$r << min(d, k)$."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "dad11c0c-043d-44cb-a9a7-4c36c2e909da",
"metadata": {},
"outputs": [],
"source": [
"import torch\n",
"from torch import optim, nn"
]
},
{
"cell_type": "markdown",
"id": "3031399b-9342-454f-86df-9a96b8c6ff50",
"metadata": {},
"source": [
"## LoRA Adapter\n",
"\n",
"简单的来说,LoRA 矩阵就是具有一个隐藏层的全连接网络,其挂接在主干网络边侧进行参数更新,我们来看看 MiniMind 模型是如何在主干网络外部定义 LoRA 网络结构的."
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "2f9bc623-f13d-4d67-bf2e-dce0784b43f8",
"metadata": {},
"outputs": [],
"source": [
"class LoRA(nn.Module):\n",
" def __init__(self, in_features, out_features, rank):\n",
" super().__init__()\n",
" self.rank = rank # LoRA 秩 控制低秩矩阵大小\n",
" self.A = nn.Linear(in_features, rank, bias=False)\n",
" self.B = nn.Linear(rank, out_features, bias=False)\n",
" # 矩阵 A 高斯分布初始化\n",
" self.A.weight.data.normal_(mean=0.0, std=0.02)\n",
" # 矩阵 B 全零初始化\n",
" self.B.weight.data.zero_()\n",
"\n",
" def forward(self, x):\n",
" return self.B(self.A(x))"
]
},
{
"cell_type": "markdown",
"id": "4a3557bd-d34d-4a15-b789-22afea60a87d",
"metadata": {},
"source": [
"可以看到,LoRA 的网络结构非常简单直观,我们接下来定义一个方法,将 LoRA 网络应用到 MiniMind 模型的特定线性层."
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "4d52e346-af62-40e2-a5b0-5cc0cf8ae6e8",
"metadata": {},
"outputs": [],
"source": [
"def apply_lora(model, rank=16):\n",
" \"\"\"将 LoRA 模块与目标模块进行绑定\"\"\"\n",
" for name, module in model.named_modules():\n",
" if isinstance(module, nn.Linear) and module.weight.shape[0] == module.weight.shape[1]:\n",
" lora = LoRA(module.weight.shape[0], module.weight.shape[1], rank=rank).to(model.device)\n",
" setattr(module, 'lora', lora) # 显式添加 LoRA 模块\n",
" original_forward = module.forward\n",
"\n",
" # 修改目标模块的 forward 函数\n",
" def forward_with_lora(x, layer1=original_forward, layer2=lora):\n",
" return layer1(x) + layer2(x)\n",
" \n",
" module.forward = forward_with_lora\n",
" # 打印 LoRA 绑定的模块名称\n",
" print(f'apply lora on module: {name}')"
]
},
{
"cell_type": "markdown",
"id": "753a54c6-a442-4ce5-be89-3bc601bc8d26",
"metadata": {},
"source": [
"我们可以声明一个小模型,对于 LoRA 的绑定进行测试."
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "f830221b-891e-4e27-b401-20b6fe5d3c59",
"metadata": {},
"outputs": [],
"source": [
"class TestModel(nn.Module):\n",
" def __init__(self):\n",
" super().__init__()\n",
" self.linear1 = nn.Linear(64, 512)\n",
" self.linear2 = nn.Linear(512, 512)\n",
" self.linear3 = nn.Linear(512, 64)\n",
"\n",
" @property\n",
" def device(self):\n",
" return next(self.parameters()).device\n",
" \n",
" def forward(self, x):\n",
" out = self.linear3(self.linear2(self.linear1))\n",
" return out"
]
},
{
"cell_type": "markdown",
"id": "1a8edbc2-f36c-4127-be3b-3c598872621a",
"metadata": {},
"source": [
"按照 apply_lora 的函数逻辑,LoRA 模块会应用在主干网络中满足 input_feature == output_feature 的模块上."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "eb24e24f-da09-4cd7-b4ce-7fe482194f75",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"apply lora on module: linear2\n",
"TestModel(\n",
" (linear1): Linear(in_features=64, out_features=512, bias=True)\n",
" (linear2): Linear(\n",
" in_features=512, out_features=512, bias=True\n",
" (lora): LoRA(\n",
" (A): Linear(in_features=512, out_features=16, bias=False)\n",
" (B): Linear(in_features=16, out_features=512, bias=False)\n",
" )\n",
" )\n",
" (linear3): Linear(in_features=512, out_features=64, bias=True)\n",
")\n"
]
}
],
"source": [
"test_model = TestModel()\n",
"apply_lora(test_model)\n",
"print(test_model)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "f882e6c0-65a6-47b2-9ee6-4a3fca988636",
"metadata": {},
"outputs": [],
"source": [
"del test_model"
]
},
{
"cell_type": "markdown",
"id": "9771475e-0c60-42db-b0cd-7e193de23f7c",
"metadata": {},
"source": [
"完成了 LoRA 模块在主干网络特定模块的绑定后,我们便可以冻结主干网络参数进行微调了,不过,考虑到主干网络权重在训练过程中并不会做任何参数更新,我们可以只保存 LoRA 模块的参数来节省内存,下面给出加载/保存 LoRA 权重的方法."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "95a453d7-2011-4575-9a51-36d91876874f",
"metadata": {},
"outputs": [],
"source": [
"def load_lora(model, path):\n",
" state_dict = torch.load(path, map_location=model.device)\n",
" for name, module in model.named_modules():\n",
" if hasattr(module, 'lora'):\n",
" lora_state = {k.replace(f'{name}.lora.', ''): v for k, v in state_dict.items() if f'{name}.lora.' in k}\n",
" module.lora.load_state_dict(lora_state)\n",
"\n",
"\n",
"def save_lora(model, path):\n",
" state_dict = {}\n",
" for name, module in model.named_modules():\n",
" if hasattr(module, 'lora'):\n",
" lora_state = {f'{name}.lora.{k}': v for k, v in module.lora.state_dict().items()}\n",
" state_dict.update(lora_state)\n",
" torch.save(state_dict, path)"
]
},
{
"cell_type": "markdown",
"id": "854b8afb-39b1-4175-8e69-4a9d5b25a3e5",
"metadata": {},
"source": [
"## Fine-Tuning MiniMind with LoRA"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "10baf158-6653-49e3-9ba3-22b2e341730f",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import platform\n",
"import argparse\n",
"import random\n",
"import time\n",
"import math\n",
"import warnings\n",
"import torch.distributed as dist\n",
"from contextlib import nullcontext\n",
"from torch.utils.data import DataLoader, DistributedSampler\n",
"from transformers import AutoTokenizer, AutoModelForCausalLM\n",
"from model.model import MiniMindLM\n",
"from model.LMConfig import LMConfig\n",
"from model.dataset import SFTDataset\n",
"\n",
"warnings.filterwarnings('ignore')"
]
},
{
"cell_type": "markdown",
"id": "f0b93272-80b3-45ce-9dec-4583fca8f87a",
"metadata": {},
"source": [
"### 可选参数设置\n",
"\n",
"首先,查看训练的可选参数,这些参数在实际使用时通过解析命令行进行导入,我们用 class 进行包装."
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "bb9d4bac-8a30-4ca9-b293-94df3f880826",
"metadata": {},
"outputs": [],
"source": [
"class args:\n",
" # out_dir: str = \"out\" # pytorch 格式权重文件保存位置 我们只展示训练过程 所以不使用\n",
" epochs: int = 1 # 训练轮数\n",
" batch_size: int = 2 # pretrain 数据集仅两个样本,设置 batch 为 2\n",
" learning_rate: float = 5e-4 # 学习率\n",
" device: str = 'cuda' if torch.cuda.is_available() else 'cpu'\n",
" dtype: str = 'bfloat16' # 16 bit 浮点数:8 bit 指数 + 7 bit 尾数\n",
" # use_wandb: bool = False # 是否使用 wandb 我们不使用\n",
" wandb_project: str = 'MiniMind-Notebook'\n",
" num_workers: int = 1 # 工作进程数\n",
" # ddp:bool = False # 单机多卡\n",
" accumulation_steps: int = 1 # 梯度累积步数\n",
" grad_clip: float = 1.0 # 梯度剪裁\n",
" warmup_iters: int = 0 # 学习率热启动\n",
" log_interval: int = 1 # 每一步打印日志 仅用于观察\n",
" # save_interval: int = 100 # checkpoint 保存点 我们不使用\n",
" local_rank: int = 1 # device 设备号\n",
" dim: int = 512 # 词嵌入维度 模型超参数\n",
" n_layers: int = 2 # MiniMind Block 数量 模型超参数\n",
" max_seq_len: int = 512 # 序列长度阈值\n",
" use_moe: bool = False # 是否启用混合专家\n",
" data_path: str = './toydata/lora_data.jsonl' # 数据集路径\n",
" lora_name: str = 'lora_identity' # 根据任务保存成lora_(英文/医学/心理...)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "3b496134-ac9a-4146-a236-fd8867f45bc6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"查看工作设备 cuda\n"
]
}
],
"source": [
"print(f'查看工作设备 {args.device}')"
]
},
{
"cell_type": "markdown",
"id": "32b956e6-e81b-49d9-bbf7-b78fbbc7eff5",
"metadata": {},
"source": [
"接下来,我们对分词器、MiniMindLM 和数据迭代器执行初始化."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "2fb7c880-b86e-4329-aeef-2827837425a3",
"metadata": {},
"outputs": [],
"source": [
"def init_model(lm_config):\n",
" tokenizer = AutoTokenizer.from_pretrained('../model/minimind_tokenizer')\n",
" model = MiniMindLM(lm_config)\n",
" moe_path = '_moe' if lm_config.use_moe else ''\n",
" # 热启动\n",
" # ckp = f'./out/rlhf_{lm_config.dim}{moe_path}.pth'\n",
" # state_dict = torch.load(ckp, map_location=args.device)\n",
" # model.load_state_dict(state_dict, strict=False)\n",
" return model.to(args.device), tokenizer"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "7f0c74d0-a191-40c1-b922-104f241aa879",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"apply lora on module: layers.0.attention.wq\n",
"apply lora on module: layers.0.attention.wo\n",
"apply lora on module: layers.1.attention.wq\n",
"apply lora on module: layers.1.attention.wo\n",
"模型位于设备:cuda:0, 词表长度:6400, DataLoader:<torch.utils.data.dataloader.DataLoader object at 0x0000022F5786D4E0>\n"
]
}
],
"source": [
"lm_config = LMConfig(dim=args.dim, n_layers=args.n_layers, max_seq_len=args.max_seq_len, use_moe=args.use_moe)\n",
"model, tokenizer = init_model(lm_config)\n",
"apply_lora(model)\n",
"\n",
"# 由于 MiniMind 用于 LoRA 微调的数据集和 SFT 数据集格式相同,可以用 SFT 数据集进行加载\n",
"train_ds = SFTDataset(args.data_path, tokenizer, max_length=lm_config.max_seq_len)\n",
"\n",
"train_loader = DataLoader(\n",
" train_ds,\n",
" batch_size=args.batch_size,\n",
" pin_memory=True,\n",
" drop_last=False,\n",
" shuffle=False,\n",
" num_workers=args.num_workers,\n",
")\n",
"\n",
"print(f'模型位于设备:{model.device}, 词表长度:{tokenizer.vocab_size}, DataLoader:{train_loader}')"
]
},
{
"cell_type": "markdown",
"id": "c35bb3ae-0b31-42ff-88d1-04d7e945bd31",
"metadata": {},
"source": [
"可以看到,LoRA 模块挂接在 Attention Block 的 Query 与 Output 线性层上,下面查看 LoRA 微调下可学习参数的占比:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "766ef2d5-1f24-4efb-9eb9-f969a57a85b0",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"LLM 总参数量: 8980992\n",
"LoRA 参数量: 65536\n",
"LoRA 参数占比: 0.73%\n"
]
}
],
"source": [
"total_params = sum(p.numel() for p in model.parameters()) # 总参数数量\n",
"lora_params_count = sum(p.numel() for name, p in model.named_parameters() if 'lora' in name) # LoRA 参数数量\n",
"print(f\"LLM 总参数量: {total_params}\")\n",
"print(f\"LoRA 参数量: {lora_params_count}\")\n",
"print(f\"LoRA 参数占比: {lora_params_count / total_params * 100:.2f}%\")"
]
},
{
"cell_type": "markdown",
"id": "8a487807-65da-4e19-ac2f-fce7077494c0",
"metadata": {},
"source": [
"接下来,冻结 MiniMindLM 主干网络的参数,做好 LoRA 微调准备."
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "db99fd52-4f07-4278-939f-47b91aeeedbe",
"metadata": {},
"outputs": [],
"source": [
"for name, param in model.named_parameters():\n",
" if 'lora' not in name:\n",
" param.requires_grad = False\n",
"lora_params = [] # 收集 LoRA 模块的可学习参数, 提供给优化器\n",
"for name, param in model.named_parameters():\n",
" if 'lora' in name:\n",
" lora_params.append(param)"
]
},
{
"cell_type": "markdown",
"id": "6f865b22-35a7-4c2c-85a1-a13d1ada12a7",
"metadata": {},
"source": [
"### 启动训练\n",
"\n",
"接下来,我们定义 MiniMind LoRA 微调所使用的优化器,损失函数和学习率调度,并进行一轮简单的训练."
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "2b57beaf-c04a-4b88-9617-7142054f2927",
"metadata": {},
"outputs": [],
"source": [
"# 学习率调度方面 采用余弦退火学习率\n",
"def get_lr(current_step, total_steps, lr):\n",
" return lr / 10 + 0.5 * lr * (1 + math.cos(math.pi * current_step / total_steps))\n",
"\n",
"# 优化器方面 选择 AdamW 优化器 并在混精度场景下创建 scaler 进行梯度缩放避免数值下溢\n",
"scaler = torch.cuda.amp.GradScaler(enabled=(args.dtype in ['float16', 'bfloat16']))\n",
"# 只训练 LoRA 模块参数\n",
"optimizer = optim.AdamW(lora_params, lr=args.learning_rate)\n",
"\n",
"device_type = \"cuda\" if \"cuda\" in args.device else \"cpu\"\n",
"ctx = nullcontext() if device_type == \"cpu\" else torch.cuda.amp.autocast() # 在 cuda 上启动混精度训练,否则空白上下文"
]
},
{
"cell_type": "markdown",
"id": "8037d2b2-1fa3-4e19-9ad5-9674ff866a36",
"metadata": {},
"source": [
"接下来,我们来看训练函数."
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "8f50d42d-da6d-4f8b-aa35-853b3900f8b5",
"metadata": {},
"outputs": [],
"source": [
"# 代码和 SFT 几乎一致\n",
"def train_epoch(epoch):\n",
" loss_fct = nn.CrossEntropyLoss(reduction='none')\n",
" start_time = time.time()\n",
" for step, (X, Y, loss_mask) in enumerate(train_loader):\n",
" X = X.to(args.device)\n",
" Y = Y.to(args.device)\n",
" loss_mask = loss_mask.to(args.device)\n",
" lr = get_lr(epoch * iter_per_epoch + step, args.epochs * iter_per_epoch, args.learning_rate)\n",
" for param_group in optimizer.param_groups:\n",
" param_group['lr'] = lr\n",
"\n",
" with ctx:\n",
" res = model(X)\n",
" loss = loss_fct(\n",
" res.logits.view(-1, res.logits.size(-1)),\n",
" Y.view(-1)\n",
" ).view(Y.size())\n",
" loss = (loss * loss_mask).sum() / loss_mask.sum()\n",
" loss += res.aux_loss\n",
" loss = loss / args.accumulation_steps\n",
"\n",
" scaler.scale(loss).backward() # 梯度缩放\n",
"\n",
" if (step + 1) % args.accumulation_steps == 0:\n",
" scaler.unscale_(optimizer)\n",
" torch.nn.utils.clip_grad_norm_(lora_params, args.grad_clip) # 梯度剪裁\n",
"\n",
" scaler.step(optimizer)\n",
" scaler.update()\n",
"\n",
" optimizer.zero_grad(set_to_none=True)\n",
"\n",
" if step % args.log_interval == 0:\n",
" spend_time = time.time() - start_time\n",
" print(\n",
" 'Epoch:[{}/{}]({}/{}) loss:{:.3f} lr:{:.12f} epoch_Time:{}min:'.format(\n",
" epoch + 1,\n",
" args.epochs,\n",
" step,\n",
" iter_per_epoch,\n",
" loss.item(),\n",
" optimizer.param_groups[-1]['lr'],\n",
" spend_time / (step + 1) * iter_per_epoch // 60 - spend_time // 60))\n",
"\n",
" # 只保存 LoRA 模块的权重\n",
" # if (step + 1) % args.save_interval == 0 and (not ddp or dist.get_rank() == 0):\n",
" # model.eval()\n",
" # # 【区别1】只保存lora权重即可\n",
" # save_lora(model, f'{args.save_dir}/lora/{args.lora_name}_{lm_config.dim}.pth')\n",
" # model.train()"
]
},
{
"cell_type": "markdown",
"id": "618876fa-7f0f-43b3-9eed-384be6cd66be",
"metadata": {},
"source": [
"接下来,我们启动一个 Epoch 的训练进行观察."
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "38584341-9ef6-4ebd-9024-0c44341de7c8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch:[1/1](0/1) loss:8.883 lr:0.000550000000 epoch_Time:0.0min:\n"
]
}
],
"source": [
"iter_per_epoch = len(train_loader)\n",
"for epoch in range(args.epochs):\n",
" train_epoch(epoch)"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "61150c72-39eb-4b90-b321-d4c7aa6c885a",
"metadata": {},
"outputs": [],
"source": [
"del model"
]
},
{
"cell_type": "markdown",
"id": "dd5e9aa8-b5a5-4380-989d-e54948f33a1b",
"metadata": {},
"source": [
"## 参考资料\n",
"\n",
"- [LoRA: Low-Rank Adaptation of Large Language Models](https://arxiv.org/abs/2106.09685)\n",
"- [HuggingFace LoRA](https://huggingface.co/docs/diffusers/en/training/lora)\n",
"- [LoRA 微调和低秩矩阵](https://www.cnblogs.com/ghj1976/p/18032882/lora-finetuning-he-di-zhi-ju-zhen)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.14"
}
},
"nbformat": 4,
"nbformat_minor": 5
}