大模型的规模不断扩大以提升性能,但对更高效、更小巧模型的需求也日益增长。然而,在不损失核心功能的前提下缩减模型规模是一项复杂的任务。
量化和剪枝等技术常用于减小模型大小,而知识蒸馏或迁移学习等方法则有助于保留或恢复缩减过程中损失的功能。
其中,剪枝是缩减模型规模最有效的策略之一。与简化数值表示的量化不同,剪枝涉及移除模型的特定部分,例如神经元或整个层。但这种有效性是有代价的:剪枝难以正确应用。您不仅需要确定要剪枝的模型部分,还需要仔细选择要移除的元素,以最大限度地减少对模型能力的影响。
本文重点介绍结构化宽度剪枝(移除选定的神经元),并演示如何将其有效地应用于具有门控线性单元 (GLU) 结构的 MLP 层。通过遵循概述的步骤,您将了解剪枝如何显着减小模型大小,同时保留其生成连贯输出和在关键基准测试中表现良好的能力。
如前所述,剪枝涉及移除被认为对模型最终输出贡献最小的部分。通过仔细选择这些不太重要的组件,剪枝旨在创建一个更有效的模型,该模型具有更少的参数和更低的计算需求,而不会牺牲其核心能力。
剪枝的主要挑战在于决定要移除模型的哪些部分。模型并非所有部分对性能的影响都相同;每个部分都有其独特的作用。
为了说明这一点,让我们检查一下本文中使用的模型的结构:Llama 3.2–1B。
<code>LlamaForCausalLM( (model): LlamaModel( (embed_tokens): Embedding(128256, 2048) (layers): ModuleList( (0-15): 16 x LlamaDecoderLayer( (self_attn): LlamaSdpaAttention( (q_proj): Linear(in_features=2048, out_features=2048, bias=False) (k_proj): Linear(in_features=2048, out_features=512, bias=False) (v_proj): Linear(in_features=2048, out_features=512, bias=False) (o_proj): Linear(in_features=2048, out_features=2048, bias=False) (rotary_emb): LlamaRotaryEmbedding() ) (mlp): LlamaMLP( (gate_proj): Linear(in_features=2048, out_features=8192, bias=False) (up_proj): Linear(in_features=2048, out_features=8192, bias=False) (down_proj): Linear(in_features=8192, out_features=2048, bias=False) (act_fn): SiLU() ) (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05) (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05) ) ) (norm): LlamaRMSNorm((2048,), eps=1e-05) (rotary_emb): LlamaRotaryEmbedding() ) (lm_head): Linear(in_features=2048, out_features=128256, bias=False) )</code>
检查结构时,我们可以识别出三个可以作为剪枝目标的主要模块:嵌入、自注意力机制和 MLP 层。为了决定哪些部分应该成为剪枝过程的重点,必须了解潜在的好处和可能的影响。
第一步是评估这些部分在模型中占据的空间大小,以便了解潜在的缩减规模。
嵌入和输出层 (embed_tokens, lm_head):
自注意力机制 (self_attn):
MLP 层 (mlp):
我们可以看到,MLP 层占据了模型大小的 50% 以上,因此它们是明确的剪枝候选对象。但是,在做出这个决定之前,务必了解每个部分对模型行为的贡献。
嵌入层负责将输入转换为模型可以有效处理的密集向量表示。剪枝嵌入层会导致模型丧失理解某些单词的能力,或者至少降低创建正确捕捉输入语义含义的向量的能力。例如,如果您想创建一个仅使用其输入词汇表中非常特定部分的高度特定模型(例如,用于财务或医学分析的模型),则剪枝此层可能是一种选择。
注意力机制允许模型在处理每个标记时关注输入序列中最相关的部分。它计算输入序列中每对标记之间的加权重要性分数,使模型能够捕捉上下文并关注相关信息。剪枝此部分会降低模型执行需要广泛理解输入上下文的任务(例如文本摘要或翻译)的能力。它还会影响生成的文本的连贯性。
MLP 层与注意力机制一起增强模型通过一系列数据扩展和收缩来理解复杂模式的能力。剪枝此部分会限制模型对未见数据或训练期间未涵盖的任务的响应。换句话说,它降低了模型的泛化能力及其提供对不熟悉输入的连贯响应的能力。
一旦您决定要针对模型的哪个部分,下一步就是确定是执行宽度剪枝(移除单个神经元)还是深度剪枝(移除整个层)。
如您所见,剪枝模型是一个相当复杂的过程,涉及许多决策。您不仅必须评估生成的模型的能力,还必须评估其训练能力。这些模型的设计目的是进行微调,通常用于特定任务,因此它们对于创建它们的特定任务比基础模型更有效率。
门控线性单元 (GLU) 架构通常用于现代神经网络,包括 LLaMA、Gemma、Mistral、Qwen 和类似的大型语言模型。GLU 引入了一种逐元素门控机制,允许模型选择性地过滤和控制信息流。此架构由成对的层组成,通常为:gate_proj、up_proj 和 down_proj(如上所示的模型结构中所示),它们协同工作以扩展和收缩数据。
这种机制使模型能够处理更复杂的模式,同时保持效率。但是,这也意味着 GLU 结构中的层紧密耦合,剪枝这些层需要仔细考虑。
对一层(例如,移除神经元)的任何操作都必须在其相应的配对层中反映出来。例如,如果从 _gateproj 中移除一个神经元,则必须从 up_proj 中移除相同的神经元,并且必须相应地调整 _downproj 层的大小。最重要的是,在计算神经元的重要性以决定保留哪些神经元时,需要一起评估神经元对。
破坏这些层的平衡会导致性能下降,甚至模型完全失效,即使只移除少量神经元也是如此。
示例将使用 Llama 模型进行演示,但代码也已在 Gemma 和 QWen 模型上成功测试。
您可以在我的 Github 代码库中的笔记本中访问完整的代码。
GitHub – peremartra/Large-Language-Model-Notebooks-Course: 关于大型语言……的实用课程
我对内存中的原始模型所做的第一步是执行一个小提示并保存结果。这使我可以轻松、直观且快速地检查通过剪枝过程生成的模型是否连贯,或者相反,是否失去了生成可理解文本的能力。
我可以向您保证,在我第一次尝试中,由于没有遵守模型的 GLU 结构,产生的文本毫无疑问地表明剪枝过程存在根本性缺陷。
原始提示是:“巴黎是……的首府”。让我们看看原始模型的响应,并将其与我的第一次失败的剪枝尝试返回的响应进行比较。
基础模型:
“巴黎是法国的首府,也是世界上游客最多的城市之一。它是一个艺术、文化、时尚和美食之都。这座城市拥有丰富的历史,是许多著名地标的所在地,包括……”
仅剪枝 20% 的不正确模型:
“巴黎是法国的首府。这是……的主要地区。这是……法国的城市……”
很明显,第一次尝试中有些东西不起作用。这看起来可能微不足道,但像这样的经验检查可以为您节省大量时间。
让我们首先看看负责计算神经元重要性的函数,这最终将决定哪些神经元保留在模型中,哪些神经元被移除。
<code>LlamaForCausalLM( (model): LlamaModel( (embed_tokens): Embedding(128256, 2048) (layers): ModuleList( (0-15): 16 x LlamaDecoderLayer( (self_attn): LlamaSdpaAttention( (q_proj): Linear(in_features=2048, out_features=2048, bias=False) (k_proj): Linear(in_features=2048, out_features=512, bias=False) (v_proj): Linear(in_features=2048, out_features=512, bias=False) (o_proj): Linear(in_features=2048, out_features=2048, bias=False) (rotary_emb): LlamaRotaryEmbedding() ) (mlp): LlamaMLP( (gate_proj): Linear(in_features=2048, out_features=8192, bias=False) (up_proj): Linear(in_features=2048, out_features=8192, bias=False) (down_proj): Linear(in_features=8192, out_features=2048, bias=False) (act_fn): SiLU() ) (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05) (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05) ) ) (norm): LlamaRMSNorm((2048,), eps=1e-05) (rotary_emb): LlamaRotaryEmbedding() ) (lm_head): Linear(in_features=2048, out_features=128256, bias=False) )</code>
该函数接收 _gateproj 层和 _upproj 层的权重,正如我解释的那样,它们成对工作。因此,必须联合计算神经元的重要性。
计算非常简单:它计算每个神经元的权重的绝对值。正值和负值都被考虑在内,因为理论上,具有最极端值的神经元通过显着改变通过它们的值来对模型的输出产生更大的影响。
在这里,我必须感谢 MariusZ Kurman 为将最小值纳入计算所做的贡献。虽然该方法在没有它们的情况下也能正常工作,但包含它们可以改善结果。
每个层的重要性是分别计算的,但该函数返回组合值。
<code>LlamaForCausalLM( (model): LlamaModel( (embed_tokens): Embedding(128256, 2048) (layers): ModuleList( (0-15): 16 x LlamaDecoderLayer( (self_attn): LlamaSdpaAttention( (q_proj): Linear(in_features=2048, out_features=2048, bias=False) (k_proj): Linear(in_features=2048, out_features=512, bias=False) (v_proj): Linear(in_features=2048, out_features=512, bias=False) (o_proj): Linear(in_features=2048, out_features=2048, bias=False) (rotary_emb): LlamaRotaryEmbedding() ) (mlp): LlamaMLP( (gate_proj): Linear(in_features=2048, out_features=8192, bias=False) (up_proj): Linear(in_features=2048, out_features=8192, bias=False) (down_proj): Linear(in_features=8192, out_features=2048, bias=False) (act_fn): SiLU() ) (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05) (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05) ) ) (norm): LlamaRMSNorm((2048,), eps=1e-05) (rotary_emb): LlamaRotaryEmbedding() ) (lm_head): Linear(in_features=2048, out_features=128256, bias=False) )</code>
此函数创建新的、更小的层,同时保留最重要神经元。此过程包括:
<code>def compute_neuron_pair_importance(gate_weight, up_weight): """ 计算神经元对重要性分数(最大绝对权重) 参数: - gate_weight:来自 gate_proj 层的权重矩阵。 - up_weight:来自 up_weight 层的权重矩阵。 返回: - importance_scores:每个神经元对的重要性分数。 """ gate_max_abs = torch.max(gate_weight, dim=1).values + torch.abs(torch.min(gate_weight, dim=1).values) up_max_abs = torch.max(up_weight, dim=1).values + torch.abs(torch.min(up_weight, dim=1).values) importance_scores = gate_max_abs + up_max_abs return importance_scores</code>
<code>def prune_neuron_pairs(mlp, prune_percent): """ 减少**gate_proj**、**up_proj**、**down_proj**层的维度,移除不太重要的神经元。 参数: - mlp:要剪枝的层。 - prune_percent:要剪枝的神经元的百分比。 返回: - new_gate_proj, new_up_proj, new_down_proj:新的剪枝层。 - k:新的中间大小。 """ # 从 MLP 层提取权重 gate_weight = mlp.gate_proj.weight.data.float() up_weight = mlp.up_proj.weight.data.float() # 计算重要性分数 importance_scores = compute_neuron_pair_importance(gate_weight, up_weight) original_intermediate_size = gate_weight.size(0) # 计算要保留的神经元 num_neuron_pairs_to_prune = min(int(prune_percent * original_intermediate_size), original_intermediate_size - 1) k = original_intermediate_size - num_neuron_pairs_to_prune # 验证检查 if k < 1: raise ValueError("k must be greater than 0") # 选择要保留的神经元 _, indices_to_keep = torch.topk(importance_scores, k, largest=True, sorted=True) indices_to_keep = indices_to_keep.sort().values # 创建并填充新层 new_gate_proj = nn.Linear(mlp.gate_proj.in_features, k, bias=False).to(device) new_up_proj = nn.Linear(mlp.up_proj.in_features, k, bias=False).to(device) new_down_proj = nn.Linear(k, mlp.down_proj.out_features, bias=False).to(device) # 将选定的权重复制到新层。 new_gate_proj.weight.data = mlp.gate_proj.weight.data[indices_to_keep, :] new_up_proj.weight.data = mlp.up_proj.weight.data[indices_to_keep, :] new_down_proj.weight.data = mlp.down_proj.weight.data[:, indices_to_keep] return new_gate_proj, new_up_proj, new_down_proj, k</code>
获得一个张量,其中包含为每个神经元计算的重要性分数。这些分数反映了每个神经元对最终输出的贡献,指示应该保留哪些神经元。
<code># 从 MLP 层提取权重 gate_weight = mlp.gate_proj.weight.data.float() up_weight = mlp.up_proj.weight.data.float()</code>
使用作为参数提供的剪枝百分比和层的原始大小来计算要保留的神经元的总数。
<code># 计算重要性分数 importance_scores = compute_neuron_pair_importance(gate_weight, up_weight) original_intermediate_size = gate_weight.size(0)</code>
Torch 用于检索具有最高重要性分数的神经元,同时还将它们从最重要到最不重要的顺序排列。由于 torch 按降序返回数据,因此使用 sort 方法将其重新排列为升序,这就是我们需要的。
<code># 计算要保留的神经元 num_neuron_pairs_to_prune = min(int(prune_percent * original_intermediate_size), original_intermediate_size - 1) k = original_intermediate_size - num_neuron_pairs_to_prune</code>
创建三个新层,其维度根据所选索引进行调整。在 _new_gateproj 和 _new_upproj 中,保留输入维度,而输出维度则减小。相反,在 _new_downproj 中,调整输入维度,而输出维度保持不变。
<code># 选择要保留的神经元 _, indices_to_keep = torch.topk(importance_scores, k, largest=True, sorted=True) indices_to_keep = indices_to_keep.sort().values</code>
相关的权重从原始层转移到新层,确保只保留与所选神经元对应的权重。
现在,让我们看看负责迭代所有层并构建修改后的模型的函数。
<code># 创建并填充新层 new_gate_proj = nn.Linear(mlp.gate_proj.in_features, k, bias=False).to(device) new_up_proj = nn.Linear(mlp.up_proj.in_features, k, bias=False).to(device) new_down_proj = nn.Linear(k, mlp.down_proj.out_features, bias=False).to(device)</code>
此函数迭代模型的每一层,应用剪枝过程并更新模型的配置以反映新的架构。
如果不更新配置文件,则保存后无法使用该模型,无论是在 Hugging Face 上还是本地。许多库(例如 Hugging Face 的 Transformers)都依赖于 model.config 来解释模型的架构。如果配置与实际结构不匹配,则通过这些库执行的微调或推理操作可能会失败。
使用此代码,我创建了几个模型,这些模型可在 Hugging Face Hub 上获得。
这些包括:
您可以下载这些模型,除了使用它们之外,还可以研究它们的架构以及与它们所基于的原始模型相比发生了哪些变化。
让我们分析将 Llama3.2–1b 模型应用 20% 剪枝后的架构变化。
<code># 将选定的权重复制到新层。 new_gate_proj.weight.data = mlp.gate_proj.weight.data[indices_to_keep, :] new_up_proj.weight.data = mlp.up_proj.weight.data[indices_to_keep, :] new_down_proj.weight.data = mlp.down_proj.weight.data[:, indices_to_keep]</code>
除了 MLP 块中中间层的大小之外,模型的结构保持不变。如您所见,_gateproj 和 _upproj 层已从 8192 个特征减少到 6554 个,而 _downproj 层也发生了同样的变化,但在其输入特征中。
此更改与代码的功能完全一致:修改这些层,同时保留对模型性能至关重要的神经元。如果我们移除 8192 的 20%,我们将得到 6553.6,这证实已剪枝了正确比例的神经元。
现在,让我们看看剪枝后的模型在测试提示中的表现如何:
巴黎是法国的首府。它也是世界上最美丽的城市之一。巴黎有如此多值得一看和体验的东西,一天之内不可能全部涵盖。但是,有一些事情……
响应与原始模型的响应并不完全相同,但它保持了连贯性。这表明该模型保留了其大部分能力,更重要的是,它可以通过知识蒸馏或微调来恢复任何损失。
除了这种经验检查之外,我还使用一些最常见的基准测试评估了该模型。让我们分析不同程度的剪枝如何影响模型的性能。
如我们所见,剪枝的影响有些不对称。BoolQ 测试评估的任务没有经历明显的下降,对于在 MLP 层中损失了 40% 神经元的模型,仅下降了约 2%。
相比之下,对 Lambada 测试的影响非常显著,准确率下降了 50% 以上。
这表明该模型保留了其大部分理解能力,但在需要更开放式生成的测试中却难以应对。
BoolQ 只向模型呈现文本和需要用是/否回答的问题。这是一项专注于衡量模型理解输入文本中关系的能力的测试。
另一方面,Lambada 要求模型猜测段落的最后一个单词,这是一项复杂的任务,其中最后一个单词测试了模型在复杂语言建模中的能力。
在 Hugging Face 开放 LLM 排行榜上,剪枝到 20% 的模型的结果甚至更令人惊讶,因为它优于其基础模型和广泛使用的 TinyLlama-1.1B-v1.1。
在这个图表中,我们可以看到两个模型的结果。
通过研究此图表,我们可以得出以下结论:剪枝后的模型平均性能优于基础模型 (4.86 对 4.03)。这表明剪枝过程有效地保留或增强了关键领域的性能,同时减少了冗余。
通过研究结果,我们可以识别剪枝模型的优势和劣势。
优势:
劣势:
能源效率: 剪枝后的模型能源效率略高 (0.4 公斤对 0.42 公斤 CO₂),这与在保持竞争性能的同时降低计算开销的目标一致。
需要对模型在不同排名中的性能进行更全面的研究,但这些结果表明我们拥有一个很有前景的模型,可以通过适当的知识蒸馏或微调得到显着改进。最重要的是,这些结果与对 MLP 层执行的剪枝过程一致。
模型的剪枝过程取得了成功。这种处理 GLU 层的方法使我们能够在保留模型大部分能力的同时执行剪枝,从而大大减小其大小和资源消耗。
重要的是要注意,测试结果是在剪枝模型之前进行任何能力恢复过程(例如知识蒸馏或微调)获得的,这通常是对经过剪枝的模型进行的。
有很多值得探索的剪枝技术。也许最直接的是深度剪枝,它涉及移除对模型性能贡献最小的层。
另一个重要的研究领域是对这些剪枝后的模型进行知识蒸馏过程,并评估它们是否保留了学习新任务的能力。这可能会使它们的性能更接近基础模型,尤其是在剪枝后的模型显示出最大损失的基准测试中。
开发更轻量、更高效的模型仍然是一个极具吸引力的领域,特别是对于那些寻求在没有广泛基础设施要求的情况下部署 LLM 功能的公司而言。这项工作为进一步研究如何使这些强大的模型更容易访问和部署奠定了基础。
本文是关于大型语言模型的完整课程的一部分,可在 Github 上获得。要了解新文章的更新,请考虑关注代码库或加星标。 通过这种方式,您将在添加新内容时收到通知。
我是 Apress 出版社出版的《大型语言模型项目:应用和实施大型语言模型策略》一书的作者。
我定期撰写关于生成式 AI、深度学习和 TensorFlow 的文章。请考虑关注我在 Medium 上的账号,以获取有关新文章的更新。 当然,欢迎您在 LinkedIn 上与我联系。
以上是如何修剪美洲驼3.2和类似的大语言模型的详细内容。更多信息请关注PHP中文网其他相关文章!