将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(三) 适配器微调·综合运用
🧩

将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(三) 适配器微调·综合运用

(3/3)· 从环境配置到性能横评,用适配器微调给 GPT-2 收尾,并指明接 EA 的下一步

新手友好 第 3/3 篇
很多人把 20 点输入预测 40 点当成通用设定,实盘里这种激进度会放大外汇贵金属的高风险暴露。本文示例仅为拉开方法差异,真实回测该把输入输出长度当超参数交给遗传算法去跑。别拿着教学模型的激进参数直接上实盘。

「三种微调路线在收盘序列上的实测分野」

把全参数、LoRA、适配器三种微调放在同一组黄金/外汇式收盘序列上跑,效率差一眼可见。LoRA 训练耗时 69.56 秒、吃 4.1G 显存,生成一条序列 1.24 秒;全参数训练 101.79 秒、5.67G 显存、生成 0.88 秒;适配器训练 104.44 秒、5.52G 显存、生成 0.88 秒。LoRA 最省资源,全参和适配器在推理速度上略快半档。 准确度用最后一行前 20 个收盘价做输入,激进拉长到 40 步预测。全参数 MSE 1.257e-07、NRMSE 0.432;LoRA MSE 1.016e-07、NRMSE 0.389;适配器 MSE 1.564e-07、NRMSE 0.494。LoRA 在误差上反而压过全参,适配器最弱,外汇与贵金属序列的高噪声可能放大了适配器的拟合偏差。 指标量级不一致,画图时直接 plt.yscale('log') 才能同框比。下面这段 test.py 把三个模型从本地文件夹加载并推理,改 fine_tuning_path 等变量就能在 MT5 导出的 csv 上复跑。

MQL5 / C++
<span class="keyword">class="kw">import</span> time
<span class="keyword">class="kw">import</span> pandas <span class="keyword">as</span> pd
<span class="keyword">from</span> transformers <span class="keyword">class="kw">import</span> GPT2LMHeadModel, GPT2Tokenizer, GPT2Config
<span class="keyword">from</span> sklearn.metrics <span class="keyword">class="kw">import</span> mean_squared_error
<span class="keyword">class="kw">import</span> torch
<span class="keyword">class="kw">import</span> numpy <span class="keyword">as</span> np
<span class="keyword">from</span> peft <span class="keyword">class="kw">import</span> PeftModel
<span class="keyword">class="kw">import</span> matplotlib.pyplot <span class="keyword">as</span> plt
<span class="keyword">from</span> adapter_tuning <span class="keyword">class="kw">import</span> GPT2LMHeadModelWithAdapters
df = pd.read_csv(<span class="class="type">class="kw">string">&class="macro">#x27;llm_data.csv&class="macro">#x27;</span>)
dvc=<span class="class="type">class="kw">string">&class="macro">#x27;cuda&class="macro">#x27;</span> <span class="keyword">if</span> torch.cuda.is_available() <span class="keyword">else</span> <span class="class="type">class="kw">string">&class="macro">#x27;cpu&class="macro">#x27;</span>
base_model=<span class="class="type">class="kw">string">&class="macro">#x27;gpt2&class="macro">#x27;</span>
fine_tuning_path=<span class="class="type">class="kw">string">&class="macro">#x27;./gpt2_stock&class="macro">#x27;</span>
lora_tuning_path =<span class="class="type">class="kw">string">&class="macro">#x27;./gpt2_LORA_None&class="macro">#x27;</span>
adpter_tuning_path=<span class="class="type">class="kw">string">&class="macro">#x27;./gpt2_Adapter-tuning&class="macro">#x27;</span>
pre_length=<span class="number">class="num">40</span>
tokenizer = GPT2Tokenizer.from_pretrained(base_model)
model_fine_tuning = GPT2LMHeadModel.from_pretrained(fine_tuning_path).to(dvc)
model_lora_tuning = GPT2LMHeadModel.from_pretrained(base_model)
model_lora_tuning=PeftModel.from_pretrained(model_lora_tuning, lora_tuning_path).to(dvc)
model_adapter_tuning = GPT2LMHeadModelWithAdapters.from_pretrained(adpter_tuning_path).to(dvc)
input_data=df.iloc[:,<span class="number">class="num">1</span>:<span class="number">class="num">20</span>].values[-<span class="number">class="num">1</span>]
true_prices= df.iloc[-<span class="number">class="num">1</span>:,<span class="number">class="num">21</span>:].values.tolist()[<span class="number">class="num">0</span>]
prompt = <span class="class="type">class="kw">string">&class="macro">#x27; &class="macro">#x27;</span>.join(<span class="built_in">map</span>(<span class="built_in">str</span>, input_data))
<span class="keyword">def</span> generater(model):
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">global</span> true_prices
&nbsp;&nbsp;&nbsp;&nbsp;model.<span class="built_in">eval</span>()
&nbsp;&nbsp;&nbsp;&nbsp;token=tokenizer.encode(prompt, return_tensors=<span class="class="type">class="kw">string">&class="macro">#x27;pt&class="macro">#x27;</span>).to(dvc)
&nbsp;&nbsp;&nbsp;&nbsp;start_=time.time()
&nbsp;&nbsp;&nbsp;&nbsp;generated = tokenizer.decode(model.generate(token, do_sample=<span class="literal">True</span>, max_length=<span class="number">class="num">200</span>)[<span class="number">class="num">0</span>], skip_special_tokens=<span class="literal">True</span>)
&nbsp;&nbsp;&nbsp;&nbsp;end_=time.time()
&nbsp;&nbsp;&nbsp;&nbsp;<span class="built_in">print</span>(<span class="class="type">class="kw">string">f&class="macro">#x27;generate time:<span class="subst">{end_-start_}</span>&class="macro">#x27;</span>)
&nbsp;&nbsp;&nbsp;&nbsp;generated_prices=generated.split(<span class="class="type">class="kw">string">&class="macro">#x27;\n&class="macro">#x27;</span>)[<span class="number">class="num">0</span>]
&nbsp;&nbsp;&nbsp;&nbsp;generated_prices=<span class="built_in">list</span>(<span class="built_in">map</span>(<span class="built_in">class="type">class="kw">float</span>,generated_prices.split()))
&nbsp;&nbsp;&nbsp;&nbsp;generated_prices=generated_prices[<span class="number">class="num">0</span>:pre_length]
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment"># def trim_lists(a, b):</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">#&nbsp;&nbsp;&nbsp;&nbsp; min_len = min(len(a), len(b))</span>

◍ 三类微调方案的回测指标对照

这段脚本把 fine-tuning、lora-tuning、adapter-tuning 三种模型在价格生成任务上的表现直接拉成可比数据。generater 函数先打印 input_data、true_prices、generated_prices,再用 mean_squared_error 算 MSE,顺手给出 RMSE 与 NRMSE——NRMSE 用 rmse 除以真实价最大值与生成价最小值之差做归一,避免量纲干扰。 groups_chart 里写死了三组硬件与时延底数:full fine-tune 训练 101.79s、推理 1.243s、占显存 5.67GB;LoRA 训练 69.56s、推理 0.877s、占 4.10GB;adapter 训练 104.44s、推理 0.883s、占 5.52GB。在 log 坐标下并列 Train_time、Infer_time、Memory、MSE、RMSE、NRMSE 六项,差距一眼可辨。 实盘意义在于:LoRA 在训练耗时和显存上明显占优,若你用 MT5 + Python 桥接做 AIGC 辅助报价,倾向优先试 lora_tuning。外汇与贵金属波动剧烈、杠杆风险高,任何生成价都只是概率参考,不能直接当入场依据。 把这段代码丢进本地 Python(需 numpy、sklearn、matplotlib)跑一遍,Comparison.png 和 predication.png 会落盘;重点看 NRMSE 那根柱,谁低谁对真实序列还原更稳。

MQL5 / C++
# class="kw">return a[:min_len], b[:min_len]
# true_prices,generated_prices=trim_lists(true_prices,generated_prices)
print(f"class="kw">input data:{input_data}")
print(f"true prices:{true_prices}")
print(f"generated prices:{generated_prices}")
mse = mean_squared_error(true_prices[:pre_length], generated_prices)
print(&class="macro">#x27;MSE:&class="macro">#x27;, mse)
rmse=np.sqrt(mse)
nrmse=rmse/(np.max(true_prices)-np.min(generated_prices))
print(f"RMSE:{rmse},NRMSE:{nrmse}")
class="kw">return generated_prices, mse, rmse, nrmse
def plot_(a,b,c,title):
	plt.figure(figsize=(class="num">7, class="num">6))
	if title==&class="macro">#x27;predication&class="macro">#x27;:
		plt.plot(true_prices[:pre_length], label=&class="macro">#x27;True Values&class="macro">#x27;, marker=&class="macro">#x27;o&class="macro">#x27;)
	plt.plot(a, label=&class="macro">#x27;fine_tuning&class="macro">#x27;, marker=&class="macro">#x27;x&class="macro">#x27;)
	plt.plot(b, label=&class="macro">#x27;lora_tuning&class="macro">#x27;, marker=&class="macro">#x27;s&class="macro">#x27;)
	plt.plot(c,label=&class="macro">#x27;adapter_tuning&class="macro">#x27;,marker=&class="macro">#x27;d&class="macro">#x27;)
	plt.title(title)
	plt.xlabel(&class="macro">#x27;Index&class="macro">#x27;)
	plt.ylabel(&class="macro">#x27;Value&class="macro">#x27;)
	plt.legend()
	plt.savefig(f"{title}.png")
def groups_chart(a,b,c,models):
	metrics = [&class="macro">#x27;Train_time(s)&class="macro">#x27;, &class="macro">#x27;Infer_time(s)&class="macro">#x27;, &class="macro">#x27;Memory(GB)&class="macro">#x27;, &class="macro">#x27;MSE&class="macro">#x27;, &class="macro">#x27;RMSE&class="macro">#x27;, &class="macro">#x27;NRMSE&class="macro">#x27;]
	plt.figure(figsize=(class="num">7, class="num">6))
	a=[class="num">101.7946,class="num">1.243,class="num">5.67,a[class="num">1],a[class="num">2],a[class="num">3]]
	b=[class="num">69.5605,class="num">0.877,class="num">4.10,b[class="num">1],b[class="num">2],b[class="num">3]]
	c=[class="num">104.4355,class="num">0.883,class="num">5.52,c[class="num">1],c[class="num">2],c[class="num">3]]# class="num">104.4355s,VRAM:class="num">5.52G  generate_runtime:class="num">0.882792s  
	bar_width = class="num">0.2
	r1 = np.arange(len(metrics))
	r2 = [x + bar_width for x in r1]
	r3 = [x + bar_width for x in r2]
	plt.bar(r1, a, class="type">class="kw">color=&class="macro">#x27;r&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">0])
	plt.bar(r2, b, class="type">class="kw">color=&class="macro">#x27;b&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">1])
	plt.bar(r3, c, class="type">class="kw">color=&class="macro">#x27;g&class="macro">#x27;, width=bar_width, edgecolor=&class="macro">#x27;grey&class="macro">#x27;, label=models[class="num">2])
	plt.yscale(&class="macro">#x27;log&class="macro">#x27;)
	plt.xlabel(&class="macro">#x27;Metrics&class="macro">#x27;, fontweight=&class="macro">#x27;bold&class="macro">#x27;)
	plt.xticks([r + bar_width for r in range(len(metrics))], metrics)
	plt.ylabel(&class="macro">#x27;Values(log scale)&class="macro">#x27;, fontweight=&class="macro">#x27;bold&class="macro">#x27;)
	plt.title(&class="macro">#x27;Model Comparison&class="macro">#x27;)
	plt.legend()
	# plt.show()
	plt.savefig(&class="macro">#x27;Comparison.png&class="macro">#x27;)
	
fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)
adapter_tuning_result=generater(model_adapter_tuning)
plot_(fine_tuning_result[class="num">0],lora_tuning_result[class="num">0],adapter_tuning_result[class="num">0],title=&class="macro">#x27;predication&class="macro">#x27;)
groups_chart(fine_tuning_result,lora_tuning_result,adapter_tuning_result,models=[&class="macro">#x27;fine-tuning&class="macro">#x27;,&class="macro">#x27;lora-tuning&class="macro">#x27;,&class="macro">#x27;adapter-tuning&class="macro">#x27;])

把工具请下神坛

适配器微调比 LoRA 多耗约 10%~20% 的 VRAM,训练步长也略长,但它把任务专属信息锁在独立模块里,换策略时不用动底座权重。全参数微调依旧是精度基线,LoRA 拼效率,适配器拼可插拔——选哪个只看你手头显卡和要跑几个品种。 下一阶段直接拿训好的模型写 EA 策略做回测,外汇与贵金属杠杆高、滑点跳空频繁,模型信号只是概率倾向而非保本凭证。代码包里 adapter_tuning.py 与 test.py 已就绪,自己一步步跑通比看十篇闲谈更有用。

让小布替你盯模型漂移
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到训练信号衰减与盘口异动,你专注把适配器权重接进 EA 的决策分支。

常见问题

适配器在推理时会增加少量串行层计算,但通常比完全微调的显存占用低,文中性能比较给出了同环境下的训练与推理耗时对照,实际差距随序列长度变化。
优先对齐 torch 2.4.1 与 transformers 4.45.1 的组合,若冲突可新建干净 venv 按文中库版本锁版本重装,AMD 用户参考本系列第 4 篇的 GPU 训练环境。
目前小布内置的是诊断与信号可视化,权重接入需在 EA 侧用 MQL5 调用推理服务,小布负责把推理输出和盘口状态并排呈现,降低你来回切工具的摩擦。
长输出便于肉眼和指标上分辨不同微调方法的拟合差异,实盘更保守的 20 预测 5 应作为超参数在回测中搜索,文中也提示了这一点。
下一篇起重点转向训练好的模型与 EA 开发结合制定策略并回测,关于 LoRA 的完整讨论见《将您自己的 LLM 集成到 EA 中·实战篇》。