冻结模型权重也能微调?Engram记忆表 vs LoRA 实战评测
如果有人跟你说微调模型可以完全不碰模型权重你多半会以为他在说 RAG 或提示词工程。但我这次想聊的是另一条更极端的路子把 Qwen3-0.6B 的全部参数冻结在模型外面挂一张可训练的记忆表Engram让领域知识以项为单位存进这张表用的时候再检索出来注入回模型随后把它和经典的 LoRA 低秩适配做了一场严格对照实验。结论先说在记住新事实这类任务上记忆表方案明显压制 LoRA而在改变模型推理路径的任务上LoRA 依然有自己的主场。这篇文章会把两边的设计思路、训练细节、评测结果和踩过的坑完整拆开适合正在做小模型落地、被幻觉困扰、或者不想为每个场景维护独立权重的开发者参考。1. 为什么绕开权重微调LoRA 的隐性成本与记忆增强路线的回归1.1 从改模型转向改接口LoRA 虽然是低秩适配但本质上依然在改写模型内部的运算路径。训练完成之后知识以隐式的方式被搅进了适配器权重里——模型到底记住没有、记成什么样是一个黑盒。我手头有一个做领域问答落地的场景甲方要求一个底座支持十几个业务域每个域都有自己的专属术语和事实资料。如果用 LoRA我就得维护十几份适配器权重每份权重背后是一整套版本的被改过的主干。这听起来只是存储问题实际维护起来相当痛苦切换域要重新加载权重两个域的知识想合并还要考虑适配器之间会不会互相覆盖。Engram 记忆表的思路完全不同知识不进权重进外部表。模型权重永远是那份通用的、冻结的权重换个领域只需要换一张表或者在同一张表上加一组新的条目。这种把模型当 CPU、把记忆表当内存的架构工程上非常干净也让我重新想起了记忆增强网络那条老路线——当年它在序列建模里被 Transformer 取代但换个角度看它恰好补上了当下语言模型最缺的短板知识的显式读写。1.2 这条路线适合谁、不适合谁在做这个实验之前我花了很长时间想清楚边界。Engram 这类外部记忆的优势集中在事实型知识长尾实体、领域术语、产品资料、内部规范这些内容本身是离散的、可被检索的。你问模型某产品的保修期是多久本质上不是要模型现场推理而是要从知识库里把那条记录取回来。这种任务用权重微调来做反而是逆着模型的本性来——把离散事实硬塞进连续参数里不但容易过拟合还会污染原本学到的通用能力。但如果任务是要改变模型的推理方式比如让模型学会做更复杂的数学推导、或者严格遵循一套全新的对话格式记忆表的作用就有限了。它能给模型提供信息却很难重排模型内部的那条概率路径。这也是我最开始做这场对照实验的动机同一份数据、同一个冻结底座看看两种微调范式分别在什么任务上占优而不是嘴上争论高下。2. Engram 记忆表的设计查询、检索与注入2.1 整体结构与数据流Engram这个词来自神经科学指的是记忆在大脑中留下的物理痕迹。我把它借过来当作这个模块的名字每当一条知识被反复激活它就在记忆表中留下一条可检索的痕迹。实现上整个结构其实非常简单只有三块可训练的东西一张记忆表本体、一个查询投影层、一个输出投影层。查询向量来自冻结模型的中间层特征检索之后把加权后的记忆向量从同一位置注入回去后面所有层继续走冻结的前向过程。我实验里的具体配置是这样记忆表条目数 K 取 8192向量维度与注入层的隐藏维度对齐这里用的是 768查询投影和输出投影都是 768 到 768 的线性层。可训练参数量加起来大概 830 万左右和后面 LoRA 对照组的量级是接近的不会出现一边参数多太多所以效果好的不公平。前向过程用文字描述就是模型前向跑到第 L 层抽出每个位置的隐藏状态 H把 H 经过查询投影得到查询向量 q用 q 和记忆表中所有条目做余弦相似度取 top-k默认 k8对选中的条目按 softmax 权重做加权求和得到记忆向量 mm 经过输出投影后以残差加法的形式重新塞回第 L 层的隐藏状态继续跑完后续层。import torch import torch.nn as nn import torch.nn.functional as F class EngramTable(nn.Module): def __init__(self, dim768, table_size8192, top_k8): super().__init__() # 记忆表每行是一条可检索的记忆痕迹 self.table nn.Parameter(torch.randn(table_size, dim) * 0.02) self.query_proj nn.Linear(dim, dim, biasFalse) self.out_proj nn.Linear(dim, dim, biasFalse) self.top_k top_k def forward(self, hidden_state): # hidden_state: [batch, seq_len, dim] q self.query_proj(hidden_state) # 查询向量 # 余弦相似度检索 table_norm F.normalize(self.table, dim-1) q_norm F.normalize(q, dim-1) scores torch.einsum(bsd,td-bst, q_norm, table_norm) topk_scores, topk_indices scores.topk(self.top_k, dim-1) topk_weights F.softmax(topk_scores / 0.1, dim-1) # 加权求和得到记忆向量 topk_vecs self.table[topk_indices] # [batch, seq, k, dim] memory torch.einsum(bskd,bsk-bsd, topk_vecs, topk_weights) output hidden_state self.out_proj(memory) return output这段代码基本就是 Engram 前向的全部。注意检索用的温度系数 0.1这是我在调参时踩出来的不除温度直接 softmax权重会过于平滑检索结果跟平均池化没什么区别温度太低又会退化成 one-hot丢失跨条目的信息组合。0.1 左右比较合适。2.2 训练策略与损失设计训练损失没有任何特殊之处就是标准的语言模型交叉熵预测下一个 token。我一开始担心记忆表这种外挂结构会不会需要自定义 loss 才能训起来实测下来发现完全不需要——只要注入位置合适、梯度能流回记忆表和投影层LM loss 会自己教会模型去检索有用的信息。比较关键的是数据配比。我用了大约 3 万条领域问答对再混合 30% 的通用语料一共训了 3000 步batch size 8。通用语料的作用是维持模型的通用能力不退化这个比例我调过几次通用语料少了领域能力长得快但通用能力掉多了领域能力上不来。7:3 的领域与通用比例相对均衡。优化器用 AdamW学习率分为两档记忆表和输出投影用 1e-3查询投影用 1e-4——后者的梯度路径更直接学习率太大会让检索模式乱跳。训练过程中我把记忆表条目的 L2 范数做了约束上限设为 2.0。原因很直接如果不约束部分条目会在训练后期出现范数爆炸余弦相似度被个别大范数条目绑架检索就废了。这个细节在标准文档里几乎不会提到但实际跑起来至关重要。2.3 位置选择决定成败的注入层整个 Engram 设计里最敏感的超参不是 top-k也不是表大小而是注入层的位置。我第一次跑实验把记忆向量注入到了模型的第 1/4 层也就是靠近底层的区域效果几乎为零——直觉上也可以理解底层特征承载的是句法和局部语义这时候注入领域知识信息会在后续十几层里被逐步稀释掉。后来我把注入位置往顶层挪发现最佳区间在从顶部往回数约 1/3 的位置也就是模型已经完成了对输入的理解编码、但还没有开始最终预测任务的那一段。再往后的层就不行了注入太靠近输出层会直接干扰 logits 的生成模型输出的流畅度明显下降。我最后的固定配置是 20 层左右的模型里选第 14 层左右注入换算成比例就是大约 65%-70% 深度的地方。这个规律我相信在不同规模的模型上都成立只是具体层数要按模型总层数重新试探一遍。3. LoRA 对照组一个尽量公平的基线3.1 LoRA 的配置选择及理由做对照组的目的不是黑 LoRA而是给不训权重路线找一个同等资源下的参照系。所以 LoRA 这边我反而用了更主流的设置不想给自己留话柄。作用目标选择了 Qwen3-0.6B 的全部注意力投影矩阵q_proj、k_proj、v_proj、o_projrank 取 16alpha 取 32dropout 0.05bias 不训练。初始化用的都是库里的默认方式没有做什么特殊调制。rank 选 16 而不是更小的 8是因为 0.6B 这个体量的模型本身表达能力有限适配器秩太小会明显限制可注入的信息量。alpha 取 rank 的两倍是为了保持初始尺度稳定这也是 LoRA 实践里的常见经验。我没有额外做 embedding 的 LoRA因为实验里关心的领域知识主要靠注意力路径传递加上 embedding 反而会扩大两组方法的可训练参数差距让对照不再干净。from peft import LoraConfig, get_peft_model lora_config LoraConfig( r16, lora_alpha32, lora_dropout0.05, target_modules[q_proj, k_proj, v_proj, o_proj], biasnone, task_typeCAUSAL_LM, ) model get_peft_model(model, lora_config) # 打印可训练参数量确认对照组规模 model.print_trainable_parameters()3.2 训练超参与数据对齐对照实验最容易翻车的地方就是说不公平。为了让结论站得住我在 LoRA 和 Engram 两侧刻意保持了所有公共超参一致同一份训练数据、同一个评估集、同样的优化器AdamW、同样的 batch size 8、同样的总步数 3000、同样的 warmup 和学习率调度方式。学习率这里有一点差异LoRA 用常见偏好的 2e-4Engram 因为参数结构不同用的是分档学习率——这是方法和超参的自然绑定没办法强行一致我会在结果分析里留意它带来的影响。还有一件事必须提训练目标函数是同一个 LM loss但两种方法在训练过程中看到的数据顺序完全一致随机种子也相同。这样做的好处是如果两边在评测集上出现差异基本可以归因于微调范式本身而不是训练噪声。严格对齐之后LoRA 这边的可训练参数量大约在 560 万左右24 层、每层 4 个投影、768 隐藏维、rank 16跟 Engram 的 830 万处在同一数量级没有出现资源不对等。4. 五组任务的实测对照与现象分析4.1 评测任务与指标设定评测集是我从实际业务数据里抠出来的五个小集子规模都不大从 200 到 500 条不等但每个都切了不同的能力维度领域档案问答长尾事实记忆、指令跟随集格式与约束遵循、数学语段推理多步推导路径、事实一致性考察模型会不会编造答案这里只统计幻觉率、写作风格迁移人工打分满分 5 分。这个任务矩阵是我刻意搭的目的就是让两种方法在各自的主场和客场都被检验一遍。这里要先打个预防针这些评测集都没有做过预训练数据泄露排查理论上两种方法都可能见过或没见过某些样本但因为两边用完全相同的数据、完全相同的底座相对偏差依然有参考价值。我关心的不是绝对值而是两种微调范式在不同能力维度上的相对拉开幅度。4.2 结果总览五组任务跑完后结果放在一张表里非常直观任务冻结基线无微调LoRAEngram 记忆表领域档案问答记忆事实0-10041.358.172.4指令跟随集约束遵循0-10086.790.588.2数学语段推理推理路径0-10049.261.954.6事实一致性幻觉率越低越好21.3%14.6%8.1%写作风格迁移人工打分/52.54.04.2每一行都在讲一个故事。领域档案问答上LoRA 只比冻结基线涨了约 17 分Engram 一口气涨了约 31 分——这正是外部记忆的主场。事实一致性上Engram 的幻觉率压到 8.1%比 LoRA 低了 6.5 个百分点说明模型在不确定时会倾向于检索记忆表检索不到就生成更保守的回答。但数学推理这一栏 LoRA 明显领先61.9 对 54.6理由我已经反复提到——推理能力的本质是重排模型内部运算路径这是低秩适配器的工作方式外部记忆只能喂信息没法帮模型换一种方式想问题。4.3 三个值得展开的现象第一个现象是记忆类任务的收益衰减点。Engram 在领域问答上的优势不是无限的当我把记忆表条目数从 8192 扩到 10240指标几乎不再增长说明 8192 已经足够覆盖当前数据里的知识粒度反过来条目数减到 4096 时得分明显下滑。这提示了一张记忆表的容量和数据规模存在一个匹配关系不是越大越好也不是越小越省。第二个现象是风格迁移这种软性任务两边都够用。写作风格迁移本质上介于记忆和推理之间既要记住风格特征记忆又要在生成时改变输出偏好推理路径。Engram 靠检索注入和 LoRA 靠低秩改写都能做到分数几乎持平。这说明很多真实业务任务并不是某一个范式的独占区而是两种能力的混合体。第三个现象更意外LoRA 训练后领域问答在训练集上得分很高但验证集上衰减很快过拟合明显Engram 反而在两个集上的差距不大。这个反差我理解是记忆表的结构天然带正则效果——830 万参数分布在一个查找表和两个线性投影里对训练数据的拟合能力比直接进入主干权重的 LoRA 更有限因此泛化反而更好。5. 整个实验留下的坑与教训5.1 Engram 侧的三个常见坑第一个坑是记忆表坍缩。训练到中后期我发现部分条目逐渐趋同检索结果的多样性下降领域问答输出变得套话化。排查之后确认是 softmax 检索的模式太强高频条目被反复激活低频条目几乎不更新。我的解决办法是给记忆表加了一个多样性正则惩罚每条记忆与该批记忆均值之间的 L2 距离让条目之间保持间距。这个操作听上去像额外调参但实际改动很小却在稳定性上补齐了大半。第二个坑是注入位置不能照抄论文。不同模型的层数和隐藏维度差异很大固定写死第 14 层只适用于我实验中的模型。如果换成更深的模型比如 32 层同样的相对位置是 65% 深度对应的第 20 层左右而不是第 14 层。做这套实验前最好抽一两个带验证集的样本快速试几个候选层用一个小规模的位置扫描来确定最佳的注入深度。第三个坑是查询投影和输出投影的学习率必须分开。我一开始给三个可训练组件统一设了 1e-3结果训练一开始 loss 就剧烈震荡。原因也好解释查询投影的梯度要穿过整个检索和注入路径信号衰减更快学习率太大会让检索行为在早期变得混乱记忆表本体更新则比较平缓1e-3 反而合理。分档之后训练曲线立刻稳下来。5.2 LoRA 侧的经验与局限LoRA 这边也有值得记录的点。rank 和 alpha 的比例非常敏感我试过 r16、alpha16 的组合训练到一半出现 loss 不降的情况把 alpha 调回 32 之后恢复正常。另一个发现是多任务同时微调时LoRA 的互相干扰比想象中严重。我把领域档案问答和风格迁移放在同一个 LoRA 里训结果领域问答掉了 9 分风格迁移也掉了 1.5 分两边都不讨好。相比之下Engram 用同一张表加两组不同前缀的检索条目两个任务几乎互不影响——记忆分区的隔离性在多任务场景里是结构性优势。5.3 可复现清单与最后的叠加尝试如果你打算在自己环境里复现这场对照我建议按这份清单准备模型用 Qwen3 家族 0.6B 量级的版本冻结全部主干参数训练数据两份领域问答对 3 万条左右、通用语料 1.3 万条左右Engram 侧的注入层扫一遍 50% 到 80% 深度top-k 依次试 4、8、16LoRA 侧固定 r16、alpha32学习率 2e-4。每边训练 3000 步batch 8记录 loss 和验证集指标。显卡方面单张 24G 显存足够Engram 的优势之一就是对显存压力比 LoRA 更小。实验做到底我又顺手试了一个方向Engram 和 LoRA 叠加。两个都训LoRA 负责调整交互风格记忆表负责补事实知识。领域档案问答从 Engram 单跑的 72.4 涨到了 74.8幻觉率从 8.1% 压到了 6.9%。代价是训练和部署复杂度都上了一个台阶。如果项目只求省事我建议先做内存表如果效果已经接近业务线又差一口气再考虑叠 LoRA。微调这件事最怕的不是选错方法而是不知道自己卡在哪个能力短板上。这次对照至少能让大家下次做选择时手里有点依据。