{ "cells": [ { "cell_type": "markdown", "id": "347c39eb-0974-4880-b6dd-0cbfc33d7145", "metadata": {}, "source": [ "# 简介\n", "\n", "> 指导文章:[14. PEFT:在大模型中快速应用 LoRA](https://github.com/Hoper-J/AI-Guide-and-Demos-zh_CN/blob/master/Guide/14.%20PEFT:在大模型中快速应用%20LoRA.md)\n", "\n", "在线链接:[Kaggle](https://www.kaggle.com/code/aidemos/12-lora-peft) | [Colab](https://colab.research.google.com/drive/119kODbfSnPhqgP5Oq3jE2g59Hxd9Fmx0?usp=sharing)\n" ] }, { "cell_type": "markdown", "id": "07c339a9-af32-494f-88f0-1a5859da483d", "metadata": {}, "source": [ "## 安装必要的库" ] }, { "cell_type": "code", "execution_count": null, "id": "64c27f94-265c-49f3-a624-7f089f426577", "metadata": { "scrolled": true }, "outputs": [], "source": [ "%pip install \\\n", " \"transformers==4.56.2\" \\\n", " \"peft==0.17.1\" \\\n", " \"datasets==4.0.0\"" ] }, { "cell_type": "markdown", "id": "2006b231-0d05-444a-bdf8-4dcfed09f1c0", "metadata": {}, "source": [ "## 加载预训练模型" ] }, { "cell_type": "code", "execution_count": 2, "id": "8899fbab-45d1-43be-b198-403903d36900", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "GPT2LMHeadModel(\n", " (transformer): GPT2Model(\n", " (wte): Embedding(50257, 768)\n", " (wpe): Embedding(1024, 768)\n", " (drop): Dropout(p=0.1, inplace=False)\n", " (h): ModuleList(\n", " (0-11): 12 x GPT2Block(\n", " (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " (attn): GPT2Attention(\n", " (c_attn): Conv1D(nf=2304, nx=768)\n", " (c_proj): Conv1D(nf=768, nx=768)\n", " (attn_dropout): Dropout(p=0.1, inplace=False)\n", " (resid_dropout): Dropout(p=0.1, inplace=False)\n", " )\n", " (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " (mlp): GPT2MLP(\n", " (c_fc): Conv1D(nf=3072, nx=768)\n", " (c_proj): Conv1D(nf=768, nx=3072)\n", " (act): NewGELUActivation()\n", " (dropout): Dropout(p=0.1, inplace=False)\n", " )\n", " )\n", " )\n", " (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " )\n", " (lm_head): Linear(in_features=768, out_features=50257, bias=False)\n", ")" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import os\n", "# 设置模型下载镜像\n", "os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'\n", "\n", "from transformers import AutoTokenizer, AutoModelForCausalLM\n", "\n", "# 加载预训练的 GPT-2 模型和分词器\n", "tokenizer = AutoTokenizer.from_pretrained('gpt2')\n", "model = AutoModelForCausalLM.from_pretrained('gpt2')\n", "\n", "# 使用eos_token作为pad_token\n", "tokenizer.pad_token = tokenizer.eos_token\n", "\n", "model" ] }, { "cell_type": "markdown", "id": "d9c18030-1a96-4ba9-a157-bc4bf9293352", "metadata": {}, "source": [ "## 使用 PEFT 应用 LoRA" ] }, { "cell_type": "code", "execution_count": 3, "id": "1dd0f0a4-f692-47c1-b153-3eb5f38aded4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n" ] }, { "data": { "text/html": [ "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "WARN  Python GIL is enabled: Multi-gpu quant acceleration for MoE models is sub-optimal and multi-core accelerated cpu packing is also disabled. We recommend Python >= 3.13.3t with Pytorch > 2.8 for mult-gpu quantization and multi-cpu packing with env `PYTHON_GIL=0`.\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "WARN  Feature `utils/Perplexity` requires python GIL or Python >= 3.13.3T (T for Threading-Free edition of Python) plus Torch 2.8. Feature is currently skipped/disabled.\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO  ENV: Auto setting PYTORCH_CUDA_ALLOC_CONF='expandable_segments:True' for memory saving.\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "INFO  ENV: Auto setting CUDA_DEVICE_ORDER=PCI_BUS_ID for correctness.          \n"
     ]
    },
    {
     "data": {
      "text/html": [
       "
"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/root/autodl-tmp/AI-Guide-and-Demos-zh_CN/.venv/lib/python3.12/site-packages/peft/tuners/lora/layer.py:2174: UserWarning: fan_in_fan_out is set to False but the target module is `Conv1D`. Setting fan_in_fan_out to True.\n",
      "  warnings.warn(\n"
     ]
    }
   ],
   "source": [
    "from peft import get_peft_model, LoraConfig, TaskType\n",
    "\n",
    "# 配置 LoRA\n",
    "lora_config = LoraConfig(\n",
    "    task_type=TaskType.CAUSAL_LM,  # 任务类型:因果语言模型\n",
    "    inference_mode=False,          # 推理模式关闭,以进行训练\n",
    "    r=8,                           # 低秩值 r\n",
    "    lora_alpha=32,                 # LoRA 的缩放因子\n",
    "    lora_dropout=0.1,              # Dropout 概率\n",
    ")\n",
    "\n",
    "# 将 LoRA 应用到模型中\n",
    "model = get_peft_model(model, lora_config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2658099e-7627-4950-b9ce-31167a9f6b3c",
   "metadata": {},
   "source": [
    "## 查看当前模型架构"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "2b200673-a8e1-42bf-ad23-a6944ee3f5c1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "PeftModelForCausalLM(\n",
      "  (base_model): LoraModel(\n",
      "    (model): GPT2LMHeadModel(\n",
      "      (transformer): GPT2Model(\n",
      "        (wte): Embedding(50257, 768)\n",
      "        (wpe): Embedding(1024, 768)\n",
      "        (drop): Dropout(p=0.1, inplace=False)\n",
      "        (h): ModuleList(\n",
      "          (0-11): 12 x GPT2Block(\n",
      "            (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n",
      "            (attn): GPT2Attention(\n",
      "              (c_attn): lora.Linear(\n",
      "                (base_layer): Conv1D(nf=2304, nx=768)\n",
      "                (lora_dropout): ModuleDict(\n",
      "                  (default): Dropout(p=0.1, inplace=False)\n",
      "                )\n",
      "                (lora_A): ModuleDict(\n",
      "                  (default): Linear(in_features=768, out_features=8, bias=False)\n",
      "                )\n",
      "                (lora_B): ModuleDict(\n",
      "                  (default): Linear(in_features=8, out_features=2304, bias=False)\n",
      "                )\n",
      "                (lora_embedding_A): ParameterDict()\n",
      "                (lora_embedding_B): ParameterDict()\n",
      "                (lora_magnitude_vector): ModuleDict()\n",
      "              )\n",
      "              (c_proj): Conv1D(nf=768, nx=768)\n",
      "              (attn_dropout): Dropout(p=0.1, inplace=False)\n",
      "              (resid_dropout): Dropout(p=0.1, inplace=False)\n",
      "            )\n",
      "            (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n",
      "            (mlp): GPT2MLP(\n",
      "              (c_fc): Conv1D(nf=3072, nx=768)\n",
      "              (c_proj): Conv1D(nf=768, nx=3072)\n",
      "              (act): NewGELUActivation()\n",
      "              (dropout): Dropout(p=0.1, inplace=False)\n",
      "            )\n",
      "          )\n",
      "        )\n",
      "        (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n",
      "      )\n",
      "      (lm_head): Linear(in_features=768, out_features=50257, bias=False)\n",
      "    )\n",
      "  )\n",
      ")\n"
     ]
    }
   ],
   "source": [
    "print(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1a89d20-f03d-4938-94c8-42c79dae4fc8",
   "metadata": {},
   "source": [
    "## 查看增加的参数量\n",
    "\n",
    "应用 LoRA 后,我们一般都希望了解模型参数量的变化,它的计算其实很简单。\n",
    "\n",
    "### 理论计算\n",
    "\n",
    "对于每个应用了 LoRA 的层,增加的参数量为:\n",
    "\n",
    "$$\n",
    "\\text{增加的参数量} = r \\times (\\text{输入维度} + \\text{输出维度})\n",
    "$$\n",
    "\n",
    "- **`r`**:LoRA 的低秩值。\n",
    "- **输入维度**:层的输入特征数。\n",
    "- **输出维度**:层的输出特征数。\n",
    "\n",
    "### 使用 PEFT 查看参数\n",
    "\n",
    "`peft` 提供了查看模型参数的便捷方法:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "db8ddcf3-1ea0-4bfc-989e-890608e65e69",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "trainable params: 294,912 || all params: 124,734,720 || trainable%: 0.2364\n"
     ]
    }
   ],
   "source": [
    "# 查看 LoRA 模块\n",
    "model.print_trainable_parameters()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "f6f2a584-f2f3-49c3-a010-0dbe3962fe41",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "可训练参数量: 294912\n",
      "总参数量: 124734720\n",
      "可训练参数占比: 0.24%\n"
     ]
    }
   ],
   "source": [
    "def print_trainable_parameters(model):\n",
    "    \"\"\"\n",
    "    打印模型的可训练参数信息。\n",
    "    \n",
    "    参数:\n",
    "        model: 要分析的模型\n",
    "    \"\"\"\n",
    "    trainable_params = 0\n",
    "    all_params = 0\n",
    "    for _, param in model.named_parameters():\n",
    "        num_params = param.numel()\n",
    "        all_params += num_params\n",
    "        if param.requires_grad:\n",
    "            trainable_params += num_params\n",
    "    print(f\"可训练参数量: {trainable_params}\")\n",
    "    print(f\"总参数量: {all_params}\")\n",
    "    print(f\"可训练参数占比: {100 * trainable_params / all_params:.2f}%\")\n",
    "    \n",
    "print_trainable_parameters(model)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "91fe2425-7bf6-4fb3-be8e-a7f66257828e",
   "metadata": {},
   "source": [
    "## 准备数据\n",
    "\n",
    "下面使用公开数据集进行演示。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "56f45ce2-6997-4208-8e3d-39f841e82bb8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "数据集大小: 500\n",
      "数据集列名: ['text', 'label']\n",
      "示例数据: {'text': 'I rented I AM CURIOUS-YELLOW from my video store because of all the controversy that surrounded it when it was first released in 1967. I also heard that at first it was seized by U.S. customs if it ever tried to enter this country, therefore being a fan of films considered \"controversial\" I really had to see this for myself.

The plot is centered around a young Swedish drama student named Lena who wants to learn everything she can about life. In particular she wants to focus her attentions to making some sort of documentary on what the average Swede thought about certain political issues such as the Vietnam War and race issues in the United States. In between asking politicians and ordinary denizens of Stockholm about their opinions on politics, she has sex with her drama teacher, classmates, and married men.

What kills me about I AM CURIOUS-YELLOW is that 40 years ago, this was considered pornographic. Really, the sex and nudity scenes are few and far between, even then it\\'s not shot like some cheaply made porno. While my countrymen mind find it shocking, in reality sex and nudity are a major staple in Swedish cinema. Even Ingmar Bergman, arguably their answer to good old boy John Ford, had sex scenes in his films.

I do commend the filmmakers for the fact that any sex shown in the film is shown for artistic purposes rather than just to shock people and make money to be shown in pornographic theaters in America. I AM CURIOUS-YELLOW is a good film for anyone wanting to study the meat and potatoes (no pun intended) of Swedish cinema. But really, this film doesn\\'t have much of a plot.', 'label': 0}\n", "正在预处理数据...\n", "预处理后的数据集大小: 500\n", "预处理后的数据集特征: {'input_ids': List(Value('int32')), 'attention_mask': List(Value('int8')), 'labels': List(Value('int64'))}\n" ] } ], "source": [ "from datasets import load_dataset\n", "from transformers import DataCollatorForLanguageModeling\n", "\n", "# 1. 使用英文小说数据集\n", "# dataset = load_dataset(\"roneneldan/TinyStories\", split=\"train[:1000]\") # 取前1000条\n", "\n", "# 2. 使用 imdb 电影评论数据集\n", "dataset = load_dataset(\"imdb\", split=\"train[:500]\")\n", "\n", "print(f\"数据集大小: {len(dataset)}\")\n", "print(f\"数据集列名: {dataset.column_names}\")\n", "print(f\"示例数据: {dataset[0]}\")\n", "\n", "def preprocess_function(examples):\n", " \"\"\"\n", " 数据预处理函数。\n", " \n", " 参数:\n", " examples: 包含数据集样例的字典\n", " \n", " 返回:\n", " 处理后的模型输入\n", " \"\"\"\n", " # 对于IMDB数据集,我们使用 'text' 字段\n", " # 你可以根据不同数据集调整字段名\n", " texts = examples['text']\n", " \n", " # Tokenization\n", " model_inputs = tokenizer(\n", " texts,\n", " truncation=True,\n", " padding=True,\n", " max_length=512, # 根据需要调整最大长度\n", " return_tensors=\"pt\" if len(texts) == 1 else None\n", " )\n", " \n", " # 对于因果语言模型,labels就是input_ids\n", " model_inputs[\"labels\"] = model_inputs[\"input_ids\"].copy()\n", " \n", " return model_inputs\n", "\n", "# 应用预处理\n", "print(\"正在预处理数据...\")\n", "train_dataset = dataset.map(\n", " preprocess_function,\n", " batched=True,\n", " remove_columns=dataset.column_names, # 移除原始列,只保留模型需要的\n", " desc=\"Tokenizing dataset\"\n", ")\n", "\n", "print(f\"预处理后的数据集大小: {len(train_dataset)}\")\n", "print(f\"预处理后的数据集特征: {train_dataset.features}\")\n", "\n", "# 创建数据整理器\n", "data_collator = DataCollatorForLanguageModeling(\n", " tokenizer=tokenizer,\n", " mlm=False, # 不使用masked language modeling,因为我们做的是causal LM\n", " pad_to_multiple_of=8 # 为了提高效率,填充到8的倍数\n", ")" ] }, { "cell_type": "markdown", "id": "6c6a63a5-2f90-4e38-a58c-db82f80ab984", "metadata": {}, "source": [ "## 开始微调" ] }, { "cell_type": "code", "execution_count": 8, "id": "6289cbbe-db23-4858-b58f-7b937e93afaa", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "`loss_type=None` was set in the config but it is unrecognized. Using the default loss: `ForCausalLMLoss`.\n" ] }, { "data": { "text/html": [ "\n", "
\n", " \n", " \n", " [375/375 00:36, Epoch 3/3]\n", "
\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
StepTraining Loss
104.027700
203.888200
303.802300
403.803300
503.823900
603.833800
703.808600
803.873400
903.762800
1003.767300
1103.926900
1203.858500
1303.888100
1403.733600
1503.806600
1603.789900
1703.837800
1803.737900
1903.746900
2003.749100
2103.843600
2203.766800
2303.836800
2403.897300
2503.822000
2603.697600
2703.778200
2803.782400
2903.785100
3003.724600
3103.794300
3203.953600
3303.731600
3403.688700
3503.711000
3603.870800
3703.949500

" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/plain": [ "TrainOutput(global_step=375, training_loss=3.813233113606771, metrics={'train_runtime': 37.2781, 'train_samples_per_second': 40.238, 'train_steps_per_second': 10.06, 'total_flos': 393297002496000.0, 'train_loss': 3.813233113606771, 'epoch': 3.0})" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from transformers import Trainer, TrainingArguments\n", "\n", "# 定义训练参数\n", "training_args = TrainingArguments(\n", " output_dir='./results', # 模型保存和日志输出的目录路径\n", " num_train_epochs=3, # 训练的总轮数(epochs)\n", " per_device_train_batch_size=4, # 每个设备(如GPU或CPU)上的训练批次大小,4表示每次输入模型的数据数量\n", " learning_rate=5e-5, # 学习率\n", " logging_steps=10, # 每隔多少步(steps)进行一次日志记录\n", " save_steps=100, # 每隔多少步保存模型\n", " report_to=\"none\", # 不使用wandb等工具记录\n", ")\n", "\n", "# 创建 Trainer\n", "trainer = Trainer(\n", " model=model, # 训练的模型对象,需要事先加载好\n", " args=training_args, # 上面定义的训练参数配置\n", " train_dataset=train_dataset, # 使用预处理后的数据集\n", " data_collator=data_collator # 数据整理器\n", ")\n", "\n", "# 开始训练\n", "trainer.train()" ] }, { "cell_type": "markdown", "id": "8c70f7e3-a20b-4e01-8931-cfcc1e655cb1", "metadata": {}, "source": [ "### 保存和加载 LoRA 微调的模型\n", "\n", "训练完成后,你可以保存或者加载 LoRA 微调的参数,下面是个简单的示例。" ] }, { "cell_type": "code", "execution_count": 9, "id": "f14e02e8-d3ab-457a-b8cc-0eb8ec715bb9", "metadata": {}, "outputs": [], "source": [ "# 保存 LoRA 参数\n", "model.save_pretrained('./lora_model')" ] }, { "cell_type": "code", "execution_count": 10, "id": "1d9771b8-de83-440c-9fa7-b1f1506ed64f", "metadata": {}, "outputs": [], "source": [ "# 加载原始模型\n", "base_model = AutoModelForCausalLM.from_pretrained(\"gpt2\")\n", "\n", "# 加载 LoRA 参数\n", "from peft import PeftModel\n", "\n", "model = PeftModel.from_pretrained(base_model, './lora_model')" ] }, { "cell_type": "markdown", "id": "c89196ee-0362-4a4d-ac4e-2afda20d8edb", "metadata": {}, "source": [ "### 合并 LoRA 权重并卸载 PEFT 包装\n", "\n", "在完成微调后,可以使用 `merge_and_unload()` 将 LoRA 的权重合并回原始模型。这在部署和推理阶段非常有用,因为这样可以:\n", "\n", "- **减少依赖**:合并后,模型成为标准的 `transformers` 模型,不再需要 `peft` 库。\n", "- **提高推理效率**:减少了额外的计算开销,推理速度可能会有所提升。\n", "- **简化模型保存和加载**:不需要分别保存基础模型和 LoRA 参数。\n", "\n", "运行下面的代码:" ] }, { "cell_type": "code", "execution_count": 11, "id": "9bd8e84e-f821-4e38-a5aa-66a37ae53866", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "合并前的模型结构:\n", "PeftModelForCausalLM(\n", " (base_model): LoraModel(\n", " (model): GPT2LMHeadModel(\n", " (transformer): GPT2Model(\n", " (wte): Embedding(50257, 768)\n", " (wpe): Embedding(1024, 768)\n", " (drop): Dropout(p=0.1, inplace=False)\n", " (h): ModuleList(\n", " (0-11): 12 x GPT2Block(\n", " (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " (attn): GPT2Attention(\n", " (c_attn): lora.Linear(\n", " (base_layer): Conv1D(nf=2304, nx=768)\n", " (lora_dropout): ModuleDict(\n", " (default): Dropout(p=0.1, inplace=False)\n", " )\n", " (lora_A): ModuleDict(\n", " (default): Linear(in_features=768, out_features=8, bias=False)\n", " )\n", " (lora_B): ModuleDict(\n", " (default): Linear(in_features=8, out_features=2304, bias=False)\n", " )\n", " (lora_embedding_A): ParameterDict()\n", " (lora_embedding_B): ParameterDict()\n", " (lora_magnitude_vector): ModuleDict()\n", " )\n", " (c_proj): Conv1D(nf=768, nx=768)\n", " (attn_dropout): Dropout(p=0.1, inplace=False)\n", " (resid_dropout): Dropout(p=0.1, inplace=False)\n", " )\n", " (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " (mlp): GPT2MLP(\n", " (c_fc): Conv1D(nf=3072, nx=768)\n", " (c_proj): Conv1D(nf=768, nx=3072)\n", " (act): NewGELUActivation()\n", " (dropout): Dropout(p=0.1, inplace=False)\n", " )\n", " )\n", " )\n", " (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " )\n", " (lm_head): Linear(in_features=768, out_features=50257, bias=False)\n", " )\n", " )\n", ")\n", "合并后的模型结构:\n", "GPT2LMHeadModel(\n", " (transformer): GPT2Model(\n", " (wte): Embedding(50257, 768)\n", " (wpe): Embedding(1024, 768)\n", " (drop): Dropout(p=0.1, inplace=False)\n", " (h): ModuleList(\n", " (0-11): 12 x GPT2Block(\n", " (ln_1): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " (attn): GPT2Attention(\n", " (c_attn): Conv1D(nf=2304, nx=768)\n", " (c_proj): Conv1D(nf=768, nx=768)\n", " (attn_dropout): Dropout(p=0.1, inplace=False)\n", " (resid_dropout): Dropout(p=0.1, inplace=False)\n", " )\n", " (ln_2): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " (mlp): GPT2MLP(\n", " (c_fc): Conv1D(nf=3072, nx=768)\n", " (c_proj): Conv1D(nf=768, nx=3072)\n", " (act): NewGELUActivation()\n", " (dropout): Dropout(p=0.1, inplace=False)\n", " )\n", " )\n", " )\n", " (ln_f): LayerNorm((768,), eps=1e-05, elementwise_affine=True)\n", " )\n", " (lm_head): Linear(in_features=768, out_features=50257, bias=False)\n", ")\n" ] } ], "source": [ "# 对比合并前后的模型\n", "print(\"合并前的模型结构:\")\n", "print(model)\n", "\n", "# 合并并卸载 LoRA 权重\n", "model = model.merge_and_unload()\n", "\n", "print(\"合并后的模型结构:\")\n", "print(model)" ] }, { "cell_type": "markdown", "id": "23163a34-8a1d-42d6-8c33-dc5a8b1ce163", "metadata": {}, "source": [ "你应该注意到,合并后模型的 LoRA 层将被去除。\n", "\n", "现在,你可以像保存普通模型一样保存:" ] }, { "cell_type": "code", "execution_count": 12, "id": "05d90a5c-d65e-4b40-82ce-b75f6ef32037", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "('./merged_model/tokenizer_config.json',\n", " './merged_model/special_tokens_map.json',\n", " './merged_model/vocab.json',\n", " './merged_model/merges.txt',\n", " './merged_model/added_tokens.json',\n", " './merged_model/tokenizer.json')" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 保存合并后的模型\n", "model.save_pretrained('./merged_model')\n", "tokenizer.save_pretrained('./merged_model')" ] }, { "cell_type": "code", "execution_count": 13, "id": "ebba1ffd-4faf-4999-bc6c-56b34b8f9a53", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Setting `pad_token_id` to `eos_token_id`:50256 for open-end generation.\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "Hello, World!!!!!!!�\n" ] } ], "source": [ "from transformers import AutoModelForCausalLM, AutoTokenizer\n", "\n", "# 加载合并后的模型\n", "tokenizer = AutoTokenizer.from_pretrained('./merged_model')\n", "model = AutoModelForCausalLM.from_pretrained('./merged_model')\n", "\n", "# 进行推理\n", "inputs = tokenizer(\"Hello, World!\", return_tensors=\"pt\")\n", "outputs = model.generate(**inputs)\n", "print(tokenizer.decode(outputs[0], skip_special_tokens=True))" ] }, { "cell_type": "markdown", "id": "025fd4c3-788c-4996-a7a2-723c823a3956", "metadata": {}, "source": [ "**注意**:\n", "\n", "- **不可逆操作**:合并操作是不可逆的。如果你之后还需要进一步微调 LoRA 参数,需确保在合并前备份模型。\n", "- **无需 PEFT 库**:合并后的模型不再包含 LoRA 适配器(Adapter)的信息,因此在加载时无需使用 `PeftModel`。" ] }, { "cell_type": "markdown", "id": "ad2426a1-d564-4d34-b4b2-5ff14970063e", "metadata": {}, "source": [ "## 可能的错误及解决方案(TypeError: Expected state_dict to be dict-like...)\n", "\n", "在使用 `PEFT` 和 `LoRA` 进行模型微调和保存加载时,可能会遇到如下错误:\n", "\n", "```\n", "TypeError: Expected state_dict to be dict-like, got .\n", "```\n", "\n", "### 错误原因\n", "\n", "一般是因为混合使用不同的保存和加载方式,这个错误不局限于 `PeftModel`,问题出在你用 `torch.save(model)` 保存整个模型却用 `load_state_dict()` 去加载,注意模型加载和保存的一致性。" ] }, { "cell_type": "markdown", "id": "e464c18f-0ec5-46a0-b01e-4d4c0d336977", "metadata": {}, "source": [ "### 错误重现\n", "\n", "下面我们来复现它,看是不是和你的操作一致(这里以 `PeftModelForCausalLM` 举例):\n", "\n", "#### 1. 错误地保存整个 `PeftModelForCausalLM` 对象而不是其 `state_dict`:" ] }, { "cell_type": "code", "execution_count": 14, "id": "0c2278f2-460e-4986-84b0-0fb3f1488261", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/root/autodl-tmp/AI-Guide-and-Demos-zh_CN/.venv/lib/python3.12/site-packages/peft/tuners/lora/layer.py:2174: UserWarning: fan_in_fan_out is set to False but the target module is `Conv1D`. Setting fan_in_fan_out to True.\n", " warnings.warn(\n" ] } ], "source": [ "import torch\n", "from transformers import AutoTokenizer, AutoModelForCausalLM\n", "from peft import get_peft_model, LoraConfig, TaskType\n", "\n", "# 加载预训练模型和分词器\n", "tokenizer = AutoTokenizer.from_pretrained('gpt2')\n", "model = AutoModelForCausalLM.from_pretrained('gpt2')\n", "\n", "# 配置 LoRA\n", "lora_config = LoraConfig(\n", " task_type=TaskType.CAUSAL_LM,\n", " inference_mode=False,\n", " r=8,\n", " lora_alpha=32,\n", " lora_dropout=0.1,\n", ")\n", "\n", "# 应用 LoRA\n", "model = get_peft_model(model, lora_config)\n", "\n", "# 错误地保存整个 PeftModel 对象\n", "torch.save(model, './model')" ] }, { "cell_type": "markdown", "id": "3f55b963-d0b0-4ae0-b19a-b6962706edad", "metadata": {}, "source": [ "#### 2. 加载时传入 `PeftModelForCausalLM` 对象\n", "\n", "> **注意(torch ≥ 2.6)**:`torch.load` 的默认参数 `weights_only` 由 `False` 改为 `True`,直接 `torch.load('./model')` 反序列化整个对象会先抛 `UnpicklingError: Weights only load failed ...`,而不是本节要演示的 `TypeError`。为复现早期的错误,下面显式传 `weights_only=False`(仅在信任文件来源时使用)。" ] }, { "cell_type": "code", "execution_count": 15, "id": "fa3e70a7-ef3d-4e44-8b37-9d746f81ba66", "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "Expected state_dict to be dict-like, got .", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[15]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# 初始化模型\u001b[39;00m\n\u001b[32m 2\u001b[39m model = AutoModelForCausalLM.from_pretrained(\u001b[33m\"gpt2\"\u001b[39m)\n\u001b[32m 3\u001b[39m \n\u001b[32m 4\u001b[39m \u001b[38;5;66;03m# 错误地加载模型,期望接收 state_dict 但实际加载了整个模型对象\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m model.load_state_dict(torch.load(\u001b[33m'./model'\u001b[39m, weights_only=\u001b[38;5;28;01mFalse\u001b[39;00m)) \u001b[38;5;66;03m# 这里会报错\u001b[39;00m\n", "\u001b[36mFile \u001b[39m\u001b[32m~/autodl-tmp/AI-Guide-and-Demos-zh_CN/.venv/lib/python3.12/site-packages/torch/nn/modules/module.py:2525\u001b[39m, in \u001b[36mModule.load_state_dict\u001b[39m\u001b[34m(self, state_dict, strict, assign)\u001b[39m\n\u001b[32m 2488\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33mr\u001b[39m\u001b[33;03m\"\"\"Copy parameters and buffers from :attr:`state_dict` into this module and its descendants.\u001b[39;00m\n\u001b[32m 2489\u001b[39m \n\u001b[32m 2490\u001b[39m \u001b[33;03mIf :attr:`strict` is ``True``, then\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 2522\u001b[39m \u001b[33;03m ``RuntimeError``.\u001b[39;00m\n\u001b[32m 2523\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 2524\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(state_dict, Mapping):\n\u001b[32m-> \u001b[39m\u001b[32m2525\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\n\u001b[32m 2526\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mExpected state_dict to be dict-like, got \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(state_dict)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 2527\u001b[39m )\n\u001b[32m 2529\u001b[39m missing_keys: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mstr\u001b[39m] = []\n\u001b[32m 2530\u001b[39m unexpected_keys: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mstr\u001b[39m] = []\n", "\u001b[31mTypeError\u001b[39m: Expected state_dict to be dict-like, got ." ] } ], "source": [ "# 初始化模型\n", "model = AutoModelForCausalLM.from_pretrained(\"gpt2\")\n", "\n", "# 错误地加载模型,期望接收 state_dict 但实际加载了整个模型对象\n", "model.load_state_dict(torch.load('./model', weights_only=False)) # 这里会报错" ] }, { "cell_type": "markdown", "id": "14b37526-74ec-4828-87e9-173dfabe3150", "metadata": {}, "source": [ "### 解决方法\n", "\n", "确保你保存和加载的对象是一致的:\n", "\n", "- `torch.save(model, '...')` 对应于 `model = torch.load('...', weights_only=False)`(torch ≥ 2.6 加载整个对象需显式传 `weights_only=False`)。\n", "- `torch.save(model.state_dict(), '...')` 对应于 `model.load_state_dict(torch.load('...'))`。" ] }, { "cell_type": "markdown", "id": "225f0c8f-071a-416d-89c9-b01ebd3dca44", "metadata": {}, "source": [ "## 一个导致微调看似无效的 Bug:应用 LoRA 前使用 get_peft_model()" ] }, { "cell_type": "code", "execution_count": 16, "id": "3d28d412-3042-4ee4-bff4-105c860b2f40", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "原始模型输出: [[-0.03600371]]\n", "训练后的 LoRA 模型输出: [[-0.03428639]]\n", "方法 1(先使用 get_peft_model,再加载 LoRA)输出: [[-0.03600371]]\n", "方法 2(直接加载 LoRA)输出: [[-0.03428639]]\n", "\n", "原始模型和方法 1 输出相同。\n", "训练后的 LoRA 模型和方法 2 输出相同。\n", "\n", "参数不匹配: \n", "base_model.model.base_model.model.linear.base_layer.weight\n", "base_model.model.linear.base_layer.weight\n", "方法 1 和方法 2 的 LoRA 模型参数不一致!\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/root/autodl-tmp/AI-Guide-and-Demos-zh_CN/.venv/lib/python3.12/site-packages/peft/tuners/tuners_utils.py:196: UserWarning: Already found a `peft_config` attribute in the model. This will lead to having multiple adapters in the model. Make sure to know what you are doing!\n", " warnings.warn(\n", "/root/autodl-tmp/AI-Guide-and-Demos-zh_CN/.venv/lib/python3.12/site-packages/peft/peft_model.py:585: UserWarning: Found missing adapter keys while loading the checkpoint: ['base_model.model.base_model.model.linear.lora_A.default.weight', 'base_model.model.base_model.model.linear.lora_B.default.weight'].\n", " warnings.warn(warn_message)\n" ] } ], "source": [ "import torch\n", "import torch.nn as nn\n", "from torch.optim import Adam\n", "from copy import deepcopy\n", "from peft import get_peft_model, LoraConfig, PeftModel\n", "\n", "# 固定随机数种子,确保结果可复现\n", "torch.manual_seed(42)\n", "\n", "# 定义一个简单的线性模型\n", "class LinearModel(nn.Module):\n", " \"\"\"\n", " 简单的线性模型。\n", " \n", " 属性:\n", " linear: 线性层\n", " \"\"\"\n", " \n", " def __init__(self, input_size, output_size):\n", " \"\"\"\n", " 初始化线性模型。\n", " \n", " 参数:\n", " input_size: 输入特征的维度\n", " output_size: 输出特征的维度\n", " \"\"\"\n", " super(LinearModel, self).__init__()\n", " self.linear = nn.Linear(input_size, output_size)\n", "\n", " def forward(self, x):\n", " \"\"\"\n", " 前向传播。\n", " \n", " 参数:\n", " x: 输入张量\n", " \n", " 返回:\n", " 线性层的输出\n", " \"\"\"\n", " return self.linear(x)\n", "\n", "# 实例化线性模型\n", "model = LinearModel(input_size=10, output_size=1)\n", "\n", "# 在应用 LoRA 之前深拷贝原始模型,确保后续公平比较\n", "original_model = deepcopy(model)\n", "\n", "# 配置 LoRA 参数\n", "config = LoraConfig(\n", " inference_mode=False,\n", " r=4,\n", " lora_alpha=16,\n", " target_modules=['linear'],\n", ")\n", "\n", "# 将 LoRA 应用到模型中\n", "lora_model = get_peft_model(model, config)\n", "\n", "# 定义一个简单的损失函数和优化器\n", "criterion = nn.MSELoss()\n", "optimizer = Adam(lora_model.parameters(), lr=1e-3)\n", "\n", "# 生成一些模拟的训练数据\n", "input_data = torch.randn(100, 10) # 100 个样本,每个样本有 10 个特征\n", "target_data = torch.randn(100, 1) # 对应的目标值\n", "\n", "# 训练一个回合\n", "lora_model.train()\n", "for epoch in range(1): # 训练 1 个回合\n", " optimizer.zero_grad()\n", " outputs = lora_model(input_data)\n", " loss = criterion(outputs, target_data)\n", " loss.backward()\n", " optimizer.step()\n", "\n", "# 训练后保存 LoRA 权重\n", "lora_model.save_pretrained('linear_lora_model')\n", "\n", "# 方法 1:先使用 get_peft_model,再加载 LoRA 权重\n", "model1 = PeftModel.from_pretrained(get_peft_model(deepcopy(original_model), config), 'linear_lora_model')\n", "\n", "# 方法 2:直接加载 LoRA 权重\n", "model2 = PeftModel.from_pretrained(deepcopy(original_model), 'linear_lora_model')\n", "\n", "# 生成相同的输入数据以进行输出比较\n", "test_input = torch.randn(1, 10)\n", "\n", "# 比较四个模型的输出(原始模型,LoRA,方法1,方法2)\n", "def compare_model_outputs(input_data):\n", " \"\"\"\n", " 比较四个模型的输出。\n", " \n", " 参数:\n", " input_data: 输入数据张量\n", " \"\"\"\n", " # 原始模型\n", " original_output = original_model(input_data)\n", " print(\"原始模型输出:\", original_output.detach().numpy())\n", "\n", " # 训练后的 LoRA 模型\n", " lora_output = lora_model(input_data)\n", " print(\"训练后的 LoRA 模型输出:\", lora_output.detach().numpy())\n", "\n", " # 方法 1:先使用 get_peft_model,再加载 LoRA\n", " output1 = model1(input_data)\n", " print(\"方法 1(先使用 get_peft_model,再加载 LoRA)输出:\", output1.detach().numpy())\n", "\n", " # 方法 2:直接加载 LoRA\n", " output2 = model2(input_data)\n", " print(\"方法 2(直接加载 LoRA)输出:\", output2.detach().numpy())\n", "\n", " if torch.allclose(original_output, output1):\n", " print(\"\\n原始模型和方法 1 输出相同。\")\n", " if torch.allclose(lora_output, output2):\n", " print(\"训练后的 LoRA 模型和方法 2 输出相同。\\n\")\n", "\n", "# 比较两个模型的参数\n", "def compare_params(m1, m2):\n", " \"\"\"\n", " 比较两个模型的参数是否一致。\n", " \n", " 参数:\n", " m1: 第一个模型\n", " m2: 第二个模型\n", " \n", " 返回:\n", " 如果参数一致返回True,否则返回False\n", " \"\"\"\n", " for (n1, p1), (n2, p2) in zip(m1.named_parameters(), m2.named_parameters()):\n", " if n1 != n2 or not torch.allclose(p1, p2):\n", " print(f\"参数不匹配: \\n{n1}\\n{n2}\")\n", " return False\n", " return True\n", "\n", "# 比较四个模型的输出\n", "compare_model_outputs(test_input)\n", "\n", "# 检查方法 1 和方法 2 的参数是否一致\n", "if compare_params(model1, model2):\n", " print(\"方法 1 和方法 2 的 LoRA 模型参数一致!\")\n", "else:\n", " print(\"方法 1 和方法 2 的 LoRA 模型参数不一致!\")" ] }, { "cell_type": "code", "execution_count": null, "id": "b6617e10-78fd-403c-9f69-d034a6a1f086", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "ai", "language": "python", "name": "ai" }, "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.12.12" } }, "nbformat": 4, "nbformat_minor": 5 }