Type something to search...
Dynamic Lora: 5 Ways to Achieve Efficient LLM Adaptation and Optimization

Dynamic Lora: 5 Ways to Achieve Efficient LLM Adaptation and Optimization

从低秩理论到自适应秩选择和 RAG 集成 — 附带代码示例的综合指南

图片由 Jakub ŻerdzickiUnsplash 上拍摄

简介

LLM 需要持续更新 — 法律 AI 必须学习新法律,金融聊天机器人需要最新的市场数据,医疗模型应该适应新的研究。但传统的微调成本很高。LoRA 提供了帮助,但大多数版本都是静态的,使用固定的秩进行更新。我提出了一种更智能的方法:一种动态 LoRA,它根据数据复杂性调整秩,从而提高微调效率。

我从完全微调开始,过渡到 LoRA 理论,并引入 Rank-1 Sum LoRA。我没有使用一个固定的低秩矩阵,而是对多个秩为 1 的更新进行求和,并修剪掉不必要的更新,从而使训练更智能、更高效:

这使我能够有选择地激活最有用的更新,并修剪掉其余的更新。通过利用检索置信度或梯度信号,LoRA 可以更智能地学习。

完全微调预训练模型

传统上,微调 LLM 涉及解冻预训练模型中的所有权重,这个过程被称为“完全微调”。虽然这并不是本文的主要重点,但理解它为 LoRA 微调的运作方式提供了宝贵的背景。

预训练网络作为起点

假设我有一个已经在某个大型数据集上训练过的神经网络 NN1。从数学上讲,它有一个参数集:

其中 n 是参数的总数(权重、偏差等)。这些预训练参数是早期训练的结果(这可能需要大量的数据和计算)。

用于微调的新数据

现在我有一个新的数据集 DF,它可以是小的或特定于领域的,包含输入-输出对:

其中 x^{(i)} 是输入,y^{(i)} 是期望的输出(标签、目标等)。目标是使现有网络适应这些新数据,而不是从随机初始化开始。

使用预训练权重的正向传播

当我将 x^{(i)} 馈送到网络中时,正向传播使用预训练参数 W^{(0)} 来生成预测:

由于权重来自现有模型,因此该网络已经从其先前的训练中获得了一些通用知识。但是,它可能与您的新数据 DF 并不完全匹配。

计算新数据的损失

我定义了一个损失函数 L(y^​, y),它衡量模型的预测 y^​(i) 与目标 y^{(i)} 的匹配程度。

对于 DF 中的每个数据点,计算:

然后我对所有 m 个样本的这些损失求和或平均:

反向传播以更新权重

就像正常的训练一样,我计算了 相对于可训练权重的梯度。在完全微调中,允许更新 W^{(0)} 中的所有预训练权重。因此,让我们这样做:

梯度计算:

参数更新:

其中 η 是学习率。我在 DF 数据集上重复此过程(正向传播 → 损失 → 反向传播 → 权重更新)多次迭代或步骤。

结果:改编模型

经过足够的迭代后,权重收敛到某个新的集合:

它结合了来自 DF 的知识,但保留了从原始大型数据集中学到的很多知识。这就是为什么它仍然被称为微调的原因 — 我没有抹去旧的知识,我只是调整了参数以更好地拟合新数据。

LoRA:针对 LLM 的优化微调

完全微调预训练模型是 LLM 开发中的一种强大方法,但它带来了巨大的计算成本 — 模型中的每个参数都必须更新。当使用小型外部数据集作为知识库时,这变得不切实际。此挑战的一种有效解决方案是 LoRA(低秩自适应)。

LoRA(低秩自适应)不仅仅是矩阵分解 — 它充当一个特殊的神经网络层,具有冻结和可训练的权重。LoRA 没有更新所有模型参数,而是向特定层添加了低秩可训练更新,从而使微调高效,且计算成本最低。

从这种角度来看,LoRA 无缝地集成到标准的前向-后向传播中。预训练的模型权重保持固定,而仅训练低秩扩展。这种结构化方法支持动态秩选择、自适应更新和高效优化,使 LoRA 成为可扩展模型微调的强大工具。

结合 LoRA:冻结权重 + 可训练的低秩更新

LoRA 声明,我没有微调整个矩阵 W,而是保持 W 冻结(原始预训练参数),并引入一个低秩更新 ΔW。也就是说,

其中 W_0 ∈ R^{k×d} 是冻结的预训练矩阵,ΔW ∈ R^{k×d} 是一个小的更新,由低秩分解参数化。为了减少神经网络中的参数数量,ΔW 不作为完整矩阵存储,而是分解为:

其中

  • A ∈ R^{k×r}B ∈ R^{r×d},
  • r ≪ min⁡(k, d).

因此,ΔW 必须位于一个 rank-r 子空间中。我们只训练 AB 中的参数。原始的 W_0​ 保持不变。

将 LoRA 视为一个专业的 NN 层

我可以将一个 LoRA 增强的密集层定义如下:

其中:

W_0​ 和 b 被冻结(在训练期间不更新),

AB 是可训练的(通常以小规模初始化,例如接近零)。

这在数学上等同于一个标准的全连接层,其权重为 W_0 + ΔW。但从 NN 的角度来看,我们将 W_0​ 视为一个常数(就像一个已知函数),而 ΔW 则是具有可训练参数的部分。

前向传播

给定一个输入 x,LoRA 层的正向传递为:

  1. 基本输出:y_0 = W_0 x + b
  2. LoRA 输出:y_Δ = (AB) x
  3. 求和:y_{LoRA} = y_0 + y_Δ = (W_0 + AB) x + b

在深度网络中,y_{LoRA} 然后传递到后续层。请注意,大型矩阵乘法 W_0 x 不涉及参数更新,因为 W_0​ 被冻结。

LoRA 网络中的反向传播

假设网络有一个损失函数 L。梯度流将绕过 W_0​,因为它被冻结,并且专注于两个小矩阵 AB。具体来说:

由于 ΔW = AB,对于每个训练示例 (x,… ),偏导数遵循标准矩阵微积分:

但在实践中,autograd 在后台处理这些细节。关键是 W_0 不出现在梯度流中(没有更新)。

更新规则

在优化器步骤(SGD、Adam 等)期间,可训练变量 AB 会得到更新:

其中 η 是学习率。同时,W_0​ 保持不变。

LoRA 的解释和优势

  1. 低秩子空间学习

    通过将 ΔW 限制为 AB,我们可以有效地学习一个秩为 r 的子空间,模型可以在其中进行调整。这大大减少了可训练参数的数量,从 k×d 减少到 r(k + d),如果 r ≪ d,则要小得多。

  2. 参数冻结与全新层

    在标准 NN 设计中,我们有:

    其中 W′ 是完全可训练的。在 LoRA 中,我们可以将 W′ 分解为:

    可以将其视为:

    • “基础知识”:W_0​ 来自大规模预训练。
    • “适应层”:AB 用于完善新任务的知识。
  3. 合并还是不合并?

    在推理时,我们可以字面地计算一次 W_0 + AB,将其存储起来,然后继续进行通常的正向传递。或者,如果内存受限,我们可以将它们分开。

  4. 速度和内存节省

    速度:我们不会通过 W_0​ 进行反向传播。

    内存:我们不是存储 W_0​ 的所有梯度,而是仅存储 AB 的梯度。

示例算法伪代码

Given:
  Frozen matrix W0 in R^{k x d}
  Trainable A in R^{k x r}, B in R^{r x d}
  Optional bias b in R^k

Function forward_LoRA(x):
  # x is shape (d,)
  y0 = W0 x + b
  yDelta = (A B) x
  return y0 + yDelta

Function backward_LoRA(dL_dy):
  # dL_dy is gradient w.r.t. the layer's output
  # no update to W0
  # compute gradient w.r.t. A, B
  # e.g. using standard matrix calculus

## Pseudocode:
  dL_dDeltaW = dL_dy * x^T
  dL_dA = ?
  dL_dB = ?

## Typically done automatically via autograd

## update A, B using chosen optimizer
  A -= learning_rate * dL_dA
  B -= learning_rate * dL_dB

LoRA 在神经网络中自然地工作——它就像一个密集层,预训练的权重保持冻结,同时添加一个小的、可训练的更新。通过将适应分解为低秩更新和固定基,LoRA 很容易融入深度学习框架。这使其成为一种简单、高效且经济高效的微调大型语言模型的方法。

用于 LoRA 微调的基准风格代码

下面是使用 LoRA(低秩自适应)在我的数据(来自本地的“creditcard.txt”文件)上对 GPT-2 进行微调的直接演示,同时将结果与基线 GPT-2 模型进行比较。

我将通过 Hugging Face 加载 GPT-2,使用 LoRA(使用 PEFT 库)对其进行包装,并简要地在关于信用卡的 Q&A 对上进行训练。关键目标是说明 LoRA 如何仅修改 GPT-2 参数的一小部分,从而使微调更快、更节省内存。

在训练之后,我测试了每个模型(基线与 LoRA)如何响应一个示例问题,这样我就可以快速查看微调的 LoRA 模型是否从您的自定义文本中获取了相关信息。这种“基准风格”的方法为我提供了使用您自己的数据集进行参数高效微调的起点。

import os
import torch
from transformers import (
    GPT2LMHeadModel,
    GPT2Tokenizer,
    Trainer,
    TrainingArguments
)
from peft import LoraConfig, get_peft_model
from datasets import Dataset

### ------------------------------------------------------------------
### 1) Reading and Parsing External Q&A Data (creditcard.txt)
### ------------------------------------------------------------------
def parse_creditcard_data(file_path = "creditcard.txt"):
    """
    Expects a file with blocks like:
      Q: Some question?
      A: Some answer.
    Returns a list of Q&A strings suitable for causal LM fine-tuning,
    where each entry is "Question: <Q>\nAnswer: <A>".
    """
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"{file_path} not found.")

    with open(file_path, "r", encoding="utf-8") as f:
        lines = f.read().strip().split("\n")

    qa_data = []
    current_q = None
    current_a = None

    for line in lines:
        line = line.strip()
        if line.startswith("Q:"):
            current_q = line[2:].strip()
        elif line.startswith("A:"):
            current_a = line[2:].strip()
            if current_q and current_a:
                # Combine them into a single text for causal LM
                qa_data.append(f"Question: {current_q}\nAnswer: {current_a}")
                current_q = None
                current_a = None

    return qa_data

2) 创建用于微调的 Hugging Face 数据集

def create_qa_dataset(qa_samples):
    """
    将问答字符串转换为 Hugging Face 数据集对象,
    其中包含单个文本字段 "text"。
    """
    from datasets import Dataset  # 假设已安装 'datasets'
    data_dict = {"text": qa_samples}
    dataset = Dataset.from_dict(data_dict)
    return dataset

3) 标记化和准备数据整理器

def tokenize_function(examples, tokenizer):
    """
    对于因果 LM,我们只需将整个问答编码为提示
    并为标签进行移位。
    """
    return tokenizer(
        examples["text"],
        truncation=True,
        max_length=128,
        padding="max_length"
    )

def data_collator(batch):
    """
    用于因果 LM 的基本数据整理器:
    input_ids => labels
    """
    input_ids = torch.stack([torch.tensor(b["input_ids"]) for b in batch])
    attention_mask = torch.stack([torch.tensor(b["attention_mask"]) for b in batch])
    labels = input_ids.clone()  # 对于因果 LM,labels = input_ids
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels
    }

4) 主脚本:LoRA 微调和比较

def main():
    # 1) 从指定路径解析信用卡问答数据
    file_path = "creditcard.txt"
    qa_samples = parse_creditcard_data(file_path)
    print(f"从 {file_path} 加载了 {len(qa_samples)} 个问答对。")

    # 2) 创建一个小的 Hugging Face 数据集
    dataset = create_qa_dataset(qa_samples)

    # 3) 加载 GPT-2(基线)
    model_name = "gpt2"
    tokenizer = GPT2Tokenizer.from_pretrained(model_name)
    # GPT-2 没有 pad_token,所以我们将其设置为 eos
    tokenizer.pad_token = tokenizer.eos_token

    # 4) 预处理数据集(标记化)
    tokenized_dataset = dataset.map(
        lambda examples: tokenize_function(examples, tokenizer),
        batched=True
    )
    tokenized_dataset.set_format(type="torch", columns=["input_ids", "attention_mask"])

    # 5) 定义 LoRA 配置
    lora_config = LoraConfig(
        r=4,
        lora_alpha=32,
        target_modules=["c_attn", "c_proj"],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM"
    )

    # 6) 创建基线 GPT-2 模型和 LoRA 封装模型
    baseline_model = GPT2LMHeadModel.from_pretrained(model_name)
    lora_model = get_peft_model(GPT2LMHeadModel.from_pretrained(model_name), lora_config)

    # (可选) 确认只有 LoRA 层是可训练的
    lora_model.print_trainable_parameters()

    # 7) LoRA 模型的训练设置
    training_args = TrainingArguments(
        output_dir="./lora_creditcard",
        overwrite_output_dir=True,
        num_train_epochs=1,  # 演示简短
        per_device_train_batch_size=2,
        logging_steps=5,
        save_steps=10,
        evaluation_strategy="no",
        do_train=True,
        do_eval=False
    )

    trainer = Trainer(
        model=lora_model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=lambda batch: data_collator(batch),
    )

    # 8) 在信用卡问答数据上微调 LoRA 模型
    print("开始在信用卡问答数据上进行 LoRA 微调...")
    trainer.train()
    print("LoRA 微调完成。")

    # 9) 比较关于信用卡问题的回复
    test_question = "How do I choose a credit card?"
    inputs = tokenizer(test_question, return_tensors="pt")

    # 将输入移动到 baseline_model.device 以进行生成
    baseline_model_device = next(baseline_model.parameters()).device
    inputs_baseline = {k: v.to(baseline_model_device) for k, v in inputs.items()}

    with torch.no_grad():
        baseline_output = baseline_model.generate(
            **inputs_baseline,
            max_length=50,
            temperature=0.7,
            do_sample=True,
            top_p=0.9
        )
    baseline_text = tokenizer.decode(baseline_output[0], skip_special_tokens=True)

    # LoRA 模型可能在 CPU 或 GPU 上;让我们看看
    lora_model_device = next(lora_model.parameters()).device
    inputs_lora = {k: v.to(lora_model_device) for k, v in inputs.items()}

    with torch.no_grad():
        lora_output = lora_model.generate(
            **inputs_lora,
            max_length=50,
            temperature=0.7,
            do_sample=True,
            top_p=0.9
        )
    lora_text = tokenizer.decode(lora_output[0], skip_special_tokens=True)

    # 10) 打印和比较
    print("\n--- 回复比较 ---")
    print(f"问题:{test_question}\n")
    print("基线 GPT-2 说:")
    print(baseline_text)
    print("\nLoRA 微调的 GPT-2 说:")
    print(lora_text)

if __name__ == "__main__":
    main()

以下是测试结果:

--- 回复比较 ---
问题:How do I choose a credit card?

基线 GPT-2 说:
How do I choose a credit card?

You can choose from different options.

When you get a credit card, you pay the card issuer for the card and pay the cost of the card. The card issuer also pays the cost of

LoRA 微调的 GPT-2 说:
How do I choose a credit card?

I can choose a credit card that will pay me money.

这里有个简短的故事:基线 GPT-2 的答案很泛泛——它冗长地谈论了成本和发卡机构。但 LoRA 微调版本至少试图引用一个直接的好处,说我可以“选择一张会付给我钱的信用卡”。对奖励的暗示表明它从我们的信用卡问答数据中获取了一些知识。而且由于 LoRA 只训练了 GPT-2 参数的一小部分,因此它能够更快地适应,而无需重新训练整个模型。

动态 LoRA:自适应秩-1 低秩自适应

我将介绍一种新的 LoRA 方法:动态秩-1 低秩自适应 (LoRA),它使用外部特定于域的数据微调预训练的 LLM。其核心思想来自矩阵分解——任何矩阵都可以分解为秩-1 矩阵的和,每个矩阵都由两个向量的乘积构成。这使我们能够应用有针对性的更新,而无需更改整个模型。

通过使用矩阵分解和动态门控,此方法仅更新一小组可训练参数,同时保持主模型权重冻结。这使得微调更高效、更轻量级,并且能够适应新数据,而无需进行大量重新训练。

LoRA 的基础:低秩自适应

对于预训练的 LLM,全量微调的标准方法是更新所有模型参数 W。然而,LoRA 假设参数更新 ΔW 可以用低秩形式表示:

其中:

  • AB 是秩为 r ≪ d 的低秩矩阵。
  • 这会将可训练参数从 O(d²) 减少到 O(dr),从而显著降低内存消耗。

在我提出的方法中,我没有直接对 AB 进行参数化,而是引入了秩-1 外积之和的公式:

其中 a_i ∈ Rb_i ∈ R 是可学习的向量对,α_i 是一个动态门控系数。

应用秩-1 LoRA 之和 + 动态门控

与传统的 LoRA 实现不同,我们的秩-1 分解方法允许对更新进行更精细的控制。我们的方法不是学习固定的秩更新,而是动态地修剪不必要的方向。步骤包括:

秩-1 展开:

每个秩-1 更新都表示为 a_i*b_i^T​,保持秩展开的高度模块化。

通过 α 进行动态门控:

每个秩-1 分量都乘以一个系数 α_i,该系数是学习的并动态更新的。前向传递计算:

其中 α_i 充当每个方向的重要性得分。

自适应修剪机制:

为了控制过拟合和不必要的复杂性,我引入了一种修剪机制,该机制会删除 α_i 在多个步骤中保持低于阈值 ϵ 的方向:

这实现了自动稀疏化,仅保留必要的秩-1 更新。

与预训练 LLM 的集成:OpenLLaMA

我们没有修改整个 LLM 架构,而是将这种动态 LoRA 机制注入到 OpenLLaMA 的特定层中,例如:

它将输入嵌入转换为注意力计算中使用的查询向量:

在这里,我用我们新颖的 LoRA 参数化替换 W_q

确保模型受益于额外的微调,而无需更改核心 LLM 权重。

附加说明

在某些线性代数上下文中,您可能会看到 a_i (a_i)^T(对称外积)。LoRA 并不要求对称性;通常我有:

在这里,每个秩-1 外积都可以被视为模型可以自由调整的权重空间中的一个 “方向”。对几个这样的方向求和(秩 = r)给出一个维度为 r 的子空间。

许多 LoRA 实现以分解形式存储 AB。如果您更喜欢将其编码为显式的向量之和 a_i(b_i)^T,结果是相同的——只是表达更新的不同(尽管等效)方式。

因此,我可以利用秩-1 之和的视角来提出一种新颖的 LoRA 方法。通过将 ΔW 表示为外积之和:

然后,我可以添加动态选择或提前停止,以确定哪些秩-1 方向是真正需要的

目标:高效的领域自适应

这种秩-1 LoRA 方法的目标是自适应地更新最重要的参数,同时保持较低的内存使用率。这使其特别适用于特定领域的微调,例如信用卡问答示例,其中:

  • 基础模型保持通用性。
  • LoRA 动态注入领域相关知识。
  • 修剪策略消除了冗余的自适应,保持微调效率。

简而言之,我希望表现力和效率之间的平衡使这种方法在微调专业数据集时更有效,而不会产生高昂的计算成本。

测试秩-1 LoRA:代码和结果

此实验介绍了一种用于 GPT-2 的新颖的秩-1 LoRA 微调方法,旨在提高效率和适应性。我没有使用标准的 LoRA 实现,而是直接修改了 GPT-2 的注意力投影层 (c_attn),集成了低秩自适应,同时保持大多数参数冻结。我们在 medquad.csv 上对模型进行微调,这是一个来自 Kaggle 的医学问答数据集,以评估此方法是否提高了 GPT-2 生成医学相关答案的能力。

本实验中使用的新技术

自定义 Rank-1 LoRA 集成

  • 与传统的 LoRA 实现不同,此方法将自定义 Rank-1 LoRA 更新直接注入到 GPT-2 的注意力层中。
  • 权重变换遵循低秩分解公式:

这降低了训练成本,同时允许在新数据集上进行高效的微调。

动态 LoRA 权重剪枝

  • 实现了一种基于阈值的剪枝机制,以去除无效的 LoRA 权重 (alpha)。
  • 如果 alpha 值低于预定义的剪枝阈值,则将其设置为零,从而减少不必要的计算。
  • 这确保了对最有用的低秩更新的自适应选择,优化了内存使用。

用于稳定性和效率的提前停止

  • 一种自定义的提前停止方法监视训练损失和 alpha 更新,以防止过拟合。
  • 如果验证损失在设定数量的步骤(耐心值)内没有改善,则模型停止训练。
  • 这可以防止冗余更新,并确保 LoRA 对医学数据的稳定适应。

选择性参数冻结

  • 此方法不是更新所有 LoRA 参数,而是动态地冻结不必要的参数。
  • 仅更新选定的 LoRA 组件 (a_vectors, b_vectors, alpha),从而降低训练复杂性。

使用特定于领域的数据集(medquad.csv)进行微调

  • 与通用数据集不同,medquad.csv 包含特定于医学主题的问答对。
  • 这使我们能够评估 LoRA 在专业知识上的有效性,确保 GPT-2 输出的显着改进。

自适应学习率和梯度裁剪

  • 设置较低的学习率以确保逐步适应,避免灾难性遗忘。
  • 应用梯度裁剪以防止更新低秩矩阵时的数值不稳定。

以下是代码:

import os
import math
import torch
import torch.nn as nn
import sys
import csv

### Ensure SentencePiece is installed.
try:
    import sentencepiece
except ImportError:
    sys.exit("SentencePiece is required. Please run: pip install sentencepiece and restart your runtime.")

from transformers import (
    GPT2LMHeadModel,
    GPT2Tokenizer,
    Trainer,
    TrainingArguments
)
from datasets import Dataset

### ------------------------------------------------------------------
### Reading Data from medquad.csv
### ------------------------------------------------------------------
def parse_medquad_data(file_path="medquad.csv"):
    """
    Reads medquad.csv, which has two columns: question, answer.
    Merges each row into "Question: <question>\nAnswer: <answer>"
    Returns a list of these merged Q&A strings.
    """
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"{file_path} not found.")

    merged_samples = []
    with open(file_path, "r", encoding="utf-8") as f:
        reader = csv.DictReader(f)  # Assumes headers: question, answer
        for row in reader:
            q = row["question"].strip()
            a = row["answer"].strip()
            # Merge them into a single text for training
            text = f"Question: {q}\nAnswer: {a}"
            merged_samples.append(text)

    return merged_samples

def create_dataset(samples):
    return Dataset.from_dict({"text": samples})

### ------------------------------------------------------------------
### Dynamic LoRA c_attn Module with Correct Dimension Handling
### ------------------------------------------------------------------
class DynamicLoRACAttn(nn.Module):
    """
    Replaces GPT-2's c_attn (a Conv1D layer). This module stores the base weight and bias,
    computes the base transform, and adds LoRA updates defined as:
      ΔW = Σ_i [α_i * a_i (b_i)^T].
    It automatically detects whether to transpose the base weight.
    """
    def __init__(self, base_weight, base_bias, rank=8, init_alpha=0.1):
        super().__init__()
        self.register_buffer("base_weight", base_weight)  # shape: [out_dim, in_dim]
        self.register_buffer("base_bias", base_bias)      # shape: [out_dim]
        self.rank = rank

        self.weight_shape = self.base_weight.shape  # e.g., (2304, 768)
        if self.weight_shape[0] > self.weight_shape[1]:
            self.out_dim, self.in_dim = self.weight_shape
            self.transpose_needed = True
        else:
            self.in_dim, self.out_dim = self.weight_shape
            self.transpose_needed = False

        print(f"Base c_attn weight shape: {self.weight_shape} -> in_dim={self.in_dim}, out_dim={self.out_dim}, transpose={self.transpose_needed}")

        self.a_vectors = nn.Parameter(torch.randn(rank, self.in_dim) * 0.01)
        self.b_vectors = nn.Parameter(torch.randn(rank, self.out_dim) * 0.01)
        self.alpha = nn.Parameter(torch.ones(rank) * init_alpha)
        self.scale = 1.0 / math.sqrt(rank)

    def forward(self, x: torch.Tensor):
        # x: [B, T, in_dim]
        B, T, C = x.shape
        x_2d = x.view(B * T, C)
        if self.transpose_needed:
            base_2d = torch.addmm(self.base_bias, x_2d, self.base_weight.transpose(0,1))
        else:
            base_2d = torch.addmm(self.base_bias, x_2d, self.base_weight)
        s = torch.matmul(x_2d, self.a_vectors.transpose(0,1))  # [B*T, rank]
        s_3d = s.unsqueeze(-1)  # [B*T, rank, 1]
        b_3d = self.b_vectors.unsqueeze(0)  # [1, rank, out_dim]
        alpha_3d = self.alpha.unsqueeze(-1).unsqueeze(0)  # [1, rank, 1]
        lora_2d = (s_3d * b_3d * alpha_3d * self.scale).sum(dim=1)  # [B*T, out_dim]
        out_2d = base_2d + lora_2d
        return out_2d.view(B, T, self.out_dim)

带有动态 LoRA c_attn 注入和 generate() 方法的 GPT-2 模型

class NovelLoRAGPT2(nn.Module):
    """
    加载 GPT-2 并用我们自定义的动态 LoRA 模块替换第一个块的 attn.c_attn。
    公开一个用于文本生成的 generate() 方法。
    """
    def __init__(self, base_model_name="gpt2", rank=8):
        super().__init__()
        print(f"从 {base_model_name} 加载 GPT-2...")
        self.base_model = GPT2LMHeadModel.from_pretrained(base_model_name, torch_dtype=torch.float32)
        if torch.cuda.is_available():
            self.base_model = self.base_model.to("cuda")
        block0_attn = self.base_model.transformer.h[0].attn
        old_c_attn = block0_attn.c_attn
        base_weight = old_c_attn.weight.detach().clone()
        base_bias = old_c_attn.bias.detach().clone()
        new_c_attn = DynamicLoRACAttn(base_weight=base_weight, base_bias=base_bias, rank=rank, init_alpha=0.1)
        block0_attn.c_attn = new_c_attn

    def forward(self, input_ids, attention_mask=None, labels=None):
        return self.base_model(input_ids=input_ids, attention_mask=attention_mask, labels=labels)

    def generate(self, **kwargs):
        # 在生成之前钳制 α 以确保稳定性。
        self.base_model.transformer.h[0].attn.c_attn.alpha.data.clamp_(min=1e-7, max=1e2)
        return self.base_model.generate(**kwargs)

标记化和数据整理

def tokenize_fn(examples, tokenizer, max_len=128):
    return tokenizer(examples["text"], max_length=max_len, truncation=True, padding="max_length")

def data_collator(examples, pad_id):
    input_ids = torch.stack([ex["input_ids"] for ex in examples])
    attention_mask = (input_ids != pad_id).long()
    return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": input_ids.clone()}

Main:在 medquad.csv 上微调 LoRA 并与基线 GPT-2 进行比较

def main():
    # 从 medquad.csv 加载数据
    file_path = "medquad.csv"
    samples = parse_medquad_data(file_path)
    if not samples:
        raise ValueError("在 medquad.csv 中未找到数据。")
    print(f"从 {file_path} 加载了 {len(samples)} 个 Q&A 样本。")
    dataset = create_dataset(samples)

    # 加载基线 GPT-2(无 LoRA)
    baseline_model_name = "gpt2"
    print("加载用于比较的基线 GPT-2...")
    baseline_model = GPT2LMHeadModel.from_pretrained(baseline_model_name, torch_dtype=torch.float32)
    tokenizer = GPT2Tokenizer.from_pretrained(baseline_model_name, use_fast=False)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token
    if torch.cuda.is_available():
        baseline_model = baseline_model.to("cuda")

    # 创建 LoRA 增强的 GPT-2
    print("创建 LoRA 增强的 GPT-2 (rank=8),覆盖 c_attn forward...")
    lora_model = NovelLoRAGPT2(base_model_name=baseline_model_name, rank=8)
    for name, param in lora_model.named_parameters():
        if any(x in name for x in ["a_vectors", "b_vectors", "alpha"]):
            param.requires_grad = True
        else:
            param.requires_grad = False

    # 标记化数据集
    tokenized_dataset = dataset.map(lambda ex: tokenize_fn(ex, tokenizer, max_len=128), batched=True)
    tokenized_dataset.set_format(type="torch", columns=["input_ids"])

    def my_data_collator(batch):
        return data_collator(batch, tokenizer.pad_token_id)

    # 训练参数
    from transformers import Trainer, TrainerCallback
    training_args = TrainingArguments(
        output_dir="./lora_gpt2_medquad",
        overwrite_output_dir=True,
        num_train_epochs=5,
        per_device_train_batch_size=2,
        learning_rate=1e-4,
        max_grad_norm=1.0,
        logging_steps=5,
        save_strategy="no",  # 禁用检查点保存以避免文件写入错误
        evaluation_strategy="no",
        do_train=True,
        do_eval=False,
        save_safetensors=False
    )

    trainer = Trainer(
        model=lora_model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=my_data_collator
    )

    # 动态 Alpha 剪枝回调
    alpha_threshold = 0.02
    patience_steps = 10
    alpha_history = {}
    class AlphaPruningCallback(TrainerCallback):
        def on_step_end(self, args, state, control, **kwargs):
            c_attn_lora = lora_model.base_model.transformer.h[0].attn.c_attn
            alpha = c_attn_lora.alpha.detach().cpu()
            for i, val in enumerate(alpha):
                if i not in alpha_history:
                    alpha_history[i] = []
                alpha_history[i].append(float(val))
            for i in range(len(alpha)):
                if len(alpha_history[i]) >= patience_steps:
                    recent_vals = alpha_history[i][-patience_steps:]
                    if all(a_ < alpha_threshold for a_ in recent_vals):
                        with torch.no_grad():
                            c_attn_lora.alpha[i] = 0.0
                        print(f"[Prune] alpha[{i}] 在步骤 {state.global_step} 被剪枝")
    trainer.add_callback(AlphaPruningCallback())

    # 在 medquad.csv 数据上微调 LoRA 模型
    print("在 medquad.csv 数据上训练 LoRA 增强的 GPT-2...")
    trainer.train()
    print("训练完成。\n")

    c_attn_layer = lora_model.base_model.transformer.h[0].attn.c_attn
    final_alpha = c_attn_layer.alpha.detach().cpu().numpy()
    print("最终 alpha 数组:", final_alpha)
    active_count = sum(a != 0 for a in final_alpha)
    print(f"活动方向:{active_count}/{len(final_alpha)}\n")
### H) 比较响应:Baseline vs. LoRA

```python
    def generate_response(model, prompt):
        device = next(model.parameters()).device
        inputs = tokenizer(prompt, return_tensors="pt")
        for k, v in inputs.items():
            inputs[k] = v.to(device)
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_length=100,
                temperature=0.2,
                do_sample=True,
                top_p=0.9,
                no_repeat_ngram_size=3,
                early_stopping=True,
                repetition_penalty=1.2
            )
        return tokenizer.decode(outputs[0], skip_special_tokens=True)

## 示例医学问题,以查看 LoRA 微调模型是否更好
    test_prompt = "What causes Glaucoma ?"
    print("--- Baseline GPT-2 Response ---")
    print(generate_response(baseline_model, test_prompt))
    print("--- LoRA-Fine-Tuned GPT-2 Response ---")
    print(generate_response(lora_model, test_prompt))

if __name__ == "__main__":
    main()

以下是结果:

Final alpha array:
[0.46387848 0.439271   0.56827366 0.5164119  0.61012256 0.63339126
 0.5025031  0.4222549 ]

Active directions: 8/8

--- Baseline GPT-2 Response ---
Setting `pad_token_id` to `eos_token_id`:50256 for open-end generation.
What causes Glaucoma ?
The most common cause of glucoidosis is the accumulation of a
protein called glycogen. This can be found in many tissues,
including bones and joints; it also occurs when blood vessels
are damaged or if there's an infection (such as herpes simplex virus).
The body releases this form into its bloodstream to help fight
off infections such that people with diabetes may develop severe
symptoms like fatigue syndrome . It could take several months
for your immune system adapts

--- LoRA-Fine-Tuned GPT-2 Response ---
What causes Glaucoma ?
Answer:    This is a common condition that affects about 1
in 3 million people. It can be caused by an infection or other
medical problem such as diabetes and heart disease (heart attack).
The most commonly diagnosed glioblastoma of the eye will usually
develop after several years if untreated treatment options are
not available for it. If you have this type of blindness then
your doctor's advice should help to treat with antibiotics before
starting any new treatments.

主要发现

那么,Rank-1 LoRA 微调方法真的有效吗?

LoRA 利用率: 最终的 alpha 值确认所有八个 rank-1 方向都保持活跃状态,这意味着在训练期间没有修剪低秩更新。这表明 LoRA 模块在整个微调过程中被完全激活和使用。

对响应的影响: Baseline 模型和微调模型仍然会产生一些幻觉——不准确或捏造的内容。但是,微调模型显示出向问答结构和更多与眼睛相关的上下文的轻微转变,表明部分适应了新的数据集。

改进建议

扩展和改进数据

  • 使用更广泛或更高质量的问答对来增强模型的领域知识。
  • 考虑专注于单个医学子领域以减少歧义。

超参数调整

  • 调整学习率、训练 epoch 的数量和 LoRA rank 以获得更好的模型稳定性。
  • 尝试不同的 LoRA 配置以优化适应性。

更好的数据清理和验证

  • 确保微调数据干净、一致且没有可能误导模型的错误。
  • 考虑过滤掉不完整或自相矛盾的条目。

更强的正则化和指导

  • 使用参数正则化来稳定训练并防止过度拟合。
  • 尝试更好的提示工程或检索增强生成 (RAG) 以减少幻觉并提高事实准确性。

探索更高级的模型

  • 考虑使用更强大的基础模型,例如OpenLLaMA,以获得更好的微调结果。请参阅实现:GitHub 链接

结论

LoRA (Low-Rank Adaptation) 通过添加小的、可训练的更新而不是重新训练数十亿个参数来改变 LLM 微调。它使适应更快、更便宜、更有效,但仍有改进的空间。

本文超越了标准的 LoRA——我探索了 Rank-1 Sum LoRA,它将更新分解为多个 rank-1 矩阵,允许根据数据复杂性进行动态选择和修剪。这使得微调更具适应性和精确性。

我涵盖了理论和代码,展示了如何将 LoRA 与动态 rank 选择集成。本文不仅仅是关于使用 LoRA——而是关于改进它以使 AI 模型更具适应性和效率。

这项研究的代码和数据集可在 GitHub 存储库 中找到。

Related Posts

结合chatgpt-o3-mini与perplexity Deep Research的3步提示:提升论文写作质量的终极指南

结合chatgpt-o3-mini与perplexity Deep Research的3步提示:提升论文写作质量的终极指南

AI 研究报告和论文写作 合并两个系统指令以获得两个模型的最佳效果 Perplexity AI 的 Deep Research 工具提供专家级的研究报告,而 OpenAI 的 ChatGPT-o3-mini-high 擅长推理。我发现你可以将它们结合起来生成令人难以置信的论文,这些论文比任何一个模型单独撰写的都要好。你只需要将这个一次性提示复制到 **

阅读更多
让 Excel 过时的 10 种 Ai 工具:实现数据分析自动化,节省手工作业时间

让 Excel 过时的 10 种 Ai 工具:实现数据分析自动化,节省手工作业时间

Non members click here作为一名软件开发人员,多年来的一个发现总是让我感到惊讶,那就是人们还在 Excel

阅读更多
使用 ChatGPT 搜索网络功能的 10 种创意方法

使用 ChatGPT 搜索网络功能的 10 种创意方法

例如,提示和输出 你知道可以使用 ChatGPT 的“搜索网络”功能来完成许多任务,而不仅仅是基本的网络搜索吗? 对于那些不知道的人,ChatGPT 新的“搜索网络”功能提供实时信息。 截至撰写此帖时,该功能仅对使用 ChatGPT 4o 和 4o-mini 的付费会员开放。 ![](https://images.weserv.nl/?url=https://cdn-im

阅读更多
掌握Ai代理:解密Google革命性白皮书的10个关键问题解答

掌握Ai代理:解密Google革命性白皮书的10个关键问题解答

10 个常见问题解答 本文是我推出的一个名为“10 个常见问题解答”的新系列的一部分。在本系列中,我旨在通过回答关于该主题的十个最常见问题来分解复杂的概念。我的目标是使用简单的语言和相关的类比,使这些想法易于理解。 图片来自 [Solen Feyissa](https://unsplash.com/@solenfeyissa?utm_source=medium&utm_medi

阅读更多
在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和技术领域保持领先地位的 10 项必学技能 📚

在人工智能和科技这样一个动态的行业中,保持领先意味着不断提升你的技能。无论你是希望深入了解人工智能模型性能、掌握数据分析,还是希望通过人工智能转变传统领域如法律,这些课程都是你成功的捷径。以下是一个精心策划的高价值课程列表,可以助力你的职业发展,并让你始终处于创新的前沿。 1. 生成性人工智能简介课程: [生成性人工智能简介](https://genai.works

阅读更多
揭开真相!深度探悉DeepSeek AI的十大误区,您被误导了吗?

揭开真相!深度探悉DeepSeek AI的十大误区,您被误导了吗?

在AI军备竞赛中分辨事实与虚构 DeepSeek AI真的是它所宣传的游戏规则改变者,还是仅仅聪明的营销和战略炒作?👀 虽然一些人将其视为AI效率的革命性飞跃,但另一些人则认为它的成功建立在借用(甚至窃取的)创新和可疑的做法之上。传言称,DeepSeek的首席执行官在疫情期间像囤积卫生纸一样囤积Nvidia芯片——这只是冰山一角。 从其声称的550万美元培训预算到使用Open

阅读更多
Type something to search...