将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(三) 适配器微调(基础篇)
◍ 适配器微调在 MT5 策略里的落点
把通用语言模型塞进 EA 做策略生成,直接全量微调成本太高,适配器(adapter)思路是在预训练层之间插一小段可训练参数,只训这部分就能让模型贴合交易语料。原文给出的系列目录显示,这一路从环境设置、适配器模块创建,到重写 GPT2LMHeadModel 类,再到本节的适配器微调,是一条完整可复现的实验链。 对 MT5 用户来说,重点不是训出多强的模型,而是验证:用适配器微调后的 LLM,能否稳定输出可被 MQL5 解析的交易信号结构。系列后续会做不同微调方法的性能比较,当前这节先把微调管线搭通。 外汇与贵金属市场高杠杆、高波动,任何由模型生成的策略都只是概率倾向,实盘前必须在 MT5 策略测试器用历史数据回测。
「适配器微调前的边界与激进设定」
前面那篇已经把 LoRA 微调跑通并和全量微调做了对比,这一篇只补适配器(Adapter)微调这一种基础方法,不再逐个复现新出的变体。再写下去你大概也没耐心看,所以这是本系列讲大模型微调的最后一篇,后面直接切到把训好的模型接进 EA 做策略回测。 我们的示例里用了偏激进的设定:拿 20 个数据点去预测后面 40 个。选这么长是为了让不同方法的差异肉眼可见,实盘里你多半会用更保守的 20 进、5 出。外汇和贵金属波动大、杠杆高,真接实时信号前得把这个长度差记牢。 更务实的做法是把输入长度和输出长度都丢进超参数池,用遗传算法分货币对、分周期回测挑最优。这篇不展开这块,你自己搭一下不难。 边界说完,下面直接看怎么用适配器微调改 GPT-2 预训练模型。
跑通示例代码的本地环境参照
下面给出文中示例代码在我机器上的运行环境。这不表示你必须在完全一致的环境下才能跑,但若执行脚本时报错,可以先对照这套配置排查依赖冲突。 操作系统为 Ubuntu 22.04.5 LTS(也可用同版本 WSL);Python 解释器锁定在 3.10.14,避免高版本里 torch 与 transformers 的 API 偏移。 关键库版本实测组合:torch-2.4.1、numpy-1.26.3、pandas-2.2.3、transformers-4.45.1、peft-0.13.0、matplotlib-3.9.2。其中 peft 0.13.0 与 transformers 4.45.1 的 LoRA 接口是匹配的,自行升级时容易踩不兼容的坑。 AMD 显卡用户建议看本系列第 4 篇的 GPU 训练部署;NVIDIA 用户看第 2 篇环境部署示例。本文不再展开安装步骤,直接聚焦代码本身。
◍ 把适配器塞进预训练模型的写法
适配器微调的思路是在冻结的预训练 Transformer 各层之间插一小块独立神经网络,只训练这块小网络去捕捉具体任务的数据分布。相比 LoRA 用极低秩分解省参数,适配器会多出下采样—上采样的瓶颈结构,存储和计算负担更大,但在多任务堆叠、灵活替换模块的场景里更顺手。外汇与贵金属行情序列建模若数据量有限、任务切换频繁,这种模块化思路值得在回测框架里试。 下面这段 PyTorch 代码给出一个最小可用 Adapter 类:瓶颈维度默认 64,dropout 概率 0.1,权重用均值 0、标准差 0.02 的正态分布初始化,偏置全置 0。你直接复制到本地环境就能跑通前向,验证瓶颈层是否把 768 维 GPT-2 隐藏态压到 64 维再升回去。 代码逐行拆解: class Adapter(nn.Module): 定义适配器,继承 nn.Module 以便接入模型。 def __init__(self, in_features, bottleneck_features=64): 入参为输入维度和瓶颈维度,默认瓶颈 64。 super(Adapter, self).__init__() 调用父类初始化。 self.down_project = nn.Linear(in_features, bottleneck_features) 下采样线性层,降维到瓶颈。 self.up_project = nn.Linear(bottleneck_features, in_features) 上采样线性层,升回原维。 self.dropout = nn.Dropout(0.1) dropout 概率 0.1 防过拟合。 self.init_weights() 调用自定义初始化。 def init_weights(self): 权重初始化方法。 nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02) 下采样权重正态初始化。 nn.init.constant_(self.down_project.bias, 0) 下采样偏置设 0。 nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) 上采样权重同分布。 nn.init.constant_(self.up_project.bias, 0) 上采样偏置设 0。 def forward(self, hidden_states): 前向逻辑。 hidden_states = self.down_project(hidden_states) 先降维。 hidden_states = F.relu(hidden_states) ReLU 非线性。 hidden_states = self.dropout(hidden_states) 第一次丢弃。 hidden_states = self.up_project(hidden_states) 升回原维。 hidden_states = self.dropout(hidden_states) 第二次丢弃。 return hidden_states 返回处理后隐状态。 下一步是把这个 Adapter 挂到 GPT2LMHeadModel 的改造类里,而不是否定任务想法——若接进去后验证集表现仍差,优先换挂接层位置或瓶颈大小。
class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=class="num">64): super(Adapter, self).__init__() self.down_project = nn.Linear(in_features, bottleneck_features) self.up_project = nn.Linear(bottleneck_features, in_features) self.dropout = nn.Dropout(class="num">0.1) self.init_weights() def init_weights(self): nn.init.normal_(self.down_project.weight, mean=class="num">0.0, std=class="num">0.02) nn.init.constant_(self.down_project.bias, class="num">0) nn.init.normal_(self.up_project.weight, mean=class="num">0.0, std=class="num">0.02) nn.init.constant_(self.up_project.bias, class="num">0) def forward(self, hidden_states): hidden_states = self.down_project(hidden_states) hidden_states = F.relu(hidden_states) hidden_states = self.dropout(hidden_states) hidden_states = self.up_project(hidden_states) hidden_states = self.dropout(hidden_states) class="kw">return hidden_states
「把适配器塞进 GPT-2 的 LM 头」
完整重写 GPT2LMHeadModel 是个体力活,这里只做最小可用改造:新建 GPT2LMHeadModelWithAdapters 继承原类,在 __init__ 里按层数挂一组 Adapter,前向时把每层适配器输出叠加回隐藏状态,再走 lm_head 出 logits。 关键点在 forward():先取 transformer 的 hidden_states[0],用 for 循环逐层做 hidden_states = hidden_states + adapter(hidden_states),等价于在每层和残差流之间插了一个轻量旁路。config.n_layer 层就要建 config.n_layer 个 Adapter,维度对齐 config.n_embd,少一层都会形状报错。 语言模型训的是「下一个词」,所以算 loss 前必须把 lm_logits 和 labels 在时间步错开一位:shift_logits 砍掉末尾、shift_labels 砍掉开头,再用 CrossEntropyLoss 展平对齐。输入「我爱」、目标「爱编程」时,logits 要跟「编程」对齐而非「爱」。 return_dict 为 False 时手动拼 (loss,)+output 元组;为 True 直接返 CausalLMOutputWithCrossAttentions。这套写法只是示意,真实任务里适配器位置、是否加权残差都得按数据重新设计。
class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel): def __init__(self, config): super().__init__(config) self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)]) def forward( self, input_ids=None, past_key_values=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None, labels=None, use_cache=None, output_attentions=None, output_hidden_states=None, return_dict=None, ): transformer_outputs = self.transformer( input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids,