将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(二)-LoRA-调优·进阶篇
🧠

将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(二)-LoRA-调优·进阶篇

(2/3)· 全量微调太重?LoRA 如何用 1% 参数改动撬动 GPT-2 在货币对上的策略泛化

案例拆解 第 2/3 篇

接上篇全参数微调的笨重流程,多数人卡在显存不够又舍不得丢精度。LoRA 把权重更新锁进低秩矩阵,训练成本断崖式下降,却常被误认为「效果必然打折」。本篇用同一份金融序列数据跑通它,看横向对比里谁更扛不同市况。

◍ 把 LoRA 权重和底座拼回可用模型

用 peft 做 GPT-2 微调时,预训练权重全程冻结,只动 LoRA 侧参数。训练脚本里 trainer.save_model(peft_model_id) 落盘的并不是完整模型,而是仅含 LoRA 权重的目录;若 LoraConfig 没设 task_type,默认是 None,本地文件夹就会以 _None 结尾(例如 gpt2_LORA_None)。 加载环节最容易踩坑:不能用 GPT2LMHeadModel.from_pretrained() 直接读这个目录,否则权重对不上、推理结果失真。正确做法是用 PeftModel.from_pretrained() 把原始 GPT-2 与 LoRA 权重重新拼合,再 .to(dvc).eval() 切到推理态。 实测一次 3 epoch、batch_size=32 的 LoRA 微调后,取 llm_data.csv 末 20 个收盘价做前向,模型吐出 40 个数值,区间集中在 0.6114–0.6123,中间冒出一个 0.656 的跳点——说明小样本下生成仍有偶发偏离,外汇与贵金属序列若直接套用这类轻量微调,高波动段可能放大误差,属高风险用法。 下面这段是可直接丢进 Python 环境跑的通关代码,关键行已用中文拆开: [CODE] import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments import torch from peft import get_peft_model, LoraConfig, PeftModel # 优先 cuda,无卡则退 cpu,CPU 跑完可能很慢 dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) # 底座用 gpt2 model_name_or_path = 'gpt2' # LoRA 超参:alpha=32,dropout=0.1,未设 task_type 故默认 None peft_config = LoraConfig( lora_alpha=32, lora_dropout=0.1 ) # 拼出保存路径,形如 gpt2_LORA_None peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}" # 读原始 csv,仅用于最后取 prompt pd.read_csv('llm_data.csv') df = pd.read_csv('llm_data.csv') # 分词器加载 tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path) # 用预处理好的 train.txt,块长 60 train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60) # 因果语言建模,不掩码 data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) # 训练参数:3 轮、批 32、不存中间检查点 training_args = TrainingArguments( output_dir=peft_model_id, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy='no' ) # 载底座 -> 套 LoRA model = GPT2LMHeadModel.from_pretrained(model_name_or_path) model = get_peft_model(model, peft_config) # Trainer 微调并只存 LoRA trainer = Trainer( model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset ) trainer.train() trainer.save_model(peft_model_id) # 重新拼合:底座 + LoRA 权重 model = GPT2LMHeadModel.from_pretrained(model_name_or_path) model = PeftModel.from_pretrained(model, peft_model_id) model.to(dvc) model.eval() # 取末行前 19 列做 prompt prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) # 生成最长 200 token,开采样 generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), do_sample=True, max_length=200)[0], skip_special_tokens=True) print(f"test the model: {generated}") [/CODE] 训练产物 gpt2_LORA_None 若想换机器复用,必须同时带着原始 gpt2 目录,单独拷 LoRA 文件夹没用。

MQL5 / C++
class="kw">import pandas as pd
from transformers class="kw">import GPT2LMHeadModel, GPT2Tokenizer
from transformers class="kw">import TextDataset, DataCollatorForLanguageModeling
from transformers class="kw">import Trainer, TrainingArguments
class="kw">import torch
from peft class="kw">import get_peft_model, LoraConfig, PeftModel
dvc = &class="macro">#x27;cuda&class="macro">#x27; if torch.cuda.is_available() else &class="macro">#x27;cpu&class="macro">#x27;
print(dvc)
model_name_or_path = &class="macro">#x27;gpt2&class="macro">#x27;
peft_config = LoraConfig(
    lora_alpha=class="num">32,
    lora_dropout=class="num">0.1
)
peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=class="num">60)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
training_args = TrainingArguments(
    output_dir=peft_model_id,
    overwrite_output_dir=True,
    num_train_epochs=class="num">3,
    per_device_train_batch_size=class="num">32,
    save_strategy=&class="macro">#x27;no&class="macro">#x27;
)
model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)
trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset
)
trainer.train()
trainer.save_model(peft_model_id)
model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
model.to(dvc)
model.eval()
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, df.iloc[:, class="num">1:class="num">20].values[-class="num">1]))
generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;).to(dvc),
    do_sample=True,
    max_length=class="num">200)[class="num">0],
    skip_special_tokens=True)
print(f"test the model: {generated}")

用 LoRA 微调 GPT-2 生成价格序列的实操链路

这段脚本把 GPT-2 基座接上 PEFT 的 LoRA 适配器,用来在已清洗的 llm_data.csv 上做语言模型式微调,目标是让模型续写外汇或贵金属的价格数字串。注意 lora_alpha=32lora_dropout=0.1 这组配置:alpha 是缩放系数,dropout 0.1 在过拟合风险高的小样本价格序列上偏保守,可能比默认 0.05 更稳。 训练侧用 block_size=60 把序列切成 60 token 的块,num_train_epochs=3per_device_train_batch_size=32,对单机消费级显卡是能跑的量级;save_strategy='no' 说明中途不落盘,只在 trainer.save_model(peft_model_id) 留最终权重。外汇与贵金属价格序列信噪比低,这种轻量微调大概率只能捕捉短程惯性,别预期宏观拐点。 推理时取 df.iloc[:,1:20].values[-1] 最后一行前 19 列当 prompt,用 do_sample=True 做随机解码续写。你可以直接把这段代码丢进 Colab,把 llm_data.csv 换成自己导出的 EURUSD 的 M1 收盘价,看生成的数字分布是否和真实波动量级接近。

MQL5 / C++
peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
train_dataset = TextDataset(tokenizer=tokenizer,
                            file_path="train.txt",
                            block_size=class="num">60)
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
training_args = TrainingArguments(output_dir=peft_model_id,
                                  overwrite_output_dir=True,
                                  num_train_epochs=class="num">3,
                                  per_device_train_batch_size=class="num">32,
                                  save_strategy= &class="macro">#x27;no&class="macro">#x27;,
                                  )
model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)
trainer = Trainer(model=model,
                  args=training_args,
                  data_collator=data_collator,
                  train_dataset=train_dataset,)
trainer.train()
trainer.save_model(peft_model_id)
model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
model.to(dvc)
model.eval()
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, df.iloc[:,class="num">1:class="num">20].values[-class="num">1]))
generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;).to(dvc),
                            do_sample=True,

「把蒸馏模型直接跑起来验证」

上面这段调用展示了如何在本地用 transformers 加载已蒸馏的小布知识库模型,并针对单条样本做生成测试。 关键参数 max_length=200 限制了输出 token 上限,skip_special_tokens=True 则避免把 [CLS]、[SEP] 这类控制符混进可读文本里。 在 MT5 之外的 Python 侧先跑通这条 print,能确认模型权重和分词器匹配;若生成的 generated 出现乱码或截断,多半是 max_length 给太小,或语料里特殊 token 未清干净。外汇与贵金属信号类文本经此类蒸馏后仍有误判可能,实盘前务必小样本核对。

MQL5 / C++
max_length=class="num">200)[class="num">0],
              skip_special_tokens=True)
print(f"test the model:{generated}")

◍ LoRA 与全参数微调的实测取舍

把 GPT-2 接上 EA 历史价做微调,最现实的问题是:训练成本与预测稳定性哪个更不能妥协。我们在小参数量模型上跑了一组对照,LoRA 训练耗时 69.56 秒、显存 4.1GB、生成 1.24 秒;全参数微调则是 101.79 秒、5.67GB、0.88 秒。差异看着不大,但换到 7B 以上模型,显存和时长会拉开数量级。 精度用最后 20 根收盘价做输入,比对生成序列与真实价的 MSE / RMSE / NRMSE。全参数微调的 NRMSE 稳定在 0.396 附近,LoRA 单次跑出 0.319,但重复执行时 LoRA 的 NRMSE 会在 0.17 到 0.766 之间跳。外汇与贵金属行情高波动,这种不收敛倾向意味着实盘前必须加早停逻辑。 别把一次脚本输出当结论 我多次跑 test.py,每次指标都漂移,你的机器上结果不同也正常。科学做法是在同一数据集先把各方法调至最佳超参并确认收敛,再做横向比选,而不是拿默认配置直接定生死。 代码里 generater() 把 true_prices 声明为 global 才能改写,否则推理长度对齐会报错;可视化因量级悬殊用了 plt.yscale('log')。下面这段是可复现的测试骨架,开 MT5 导出的 llm_data.csv 就能本地重跑。

MQL5 / C++
class="kw">import time
class="kw">import pandas as pd
from transformers class="kw">import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config
from sklearn.metrics class="kw">import mean_squared_error
class="kw">import torch
class="kw">import numpy as np
from peft class="kw">import PeftModel
class="kw">import matplotlib.pyplot as plt
# Load dataset
df = pd.read_csv(&class="macro">#x27;llm_data.csv&class="macro">#x27;)
# Set device(GPU or CPU)
dvc = &class="macro">#x27;cuda&class="macro">#x27; if torch.cuda.is_available() else &class="macro">#x27;cpu&class="macro">#x27;
# Define model paths
base_model = &class="macro">#x27;gpt2&class="macro">#x27;
fine_tuning_path = &class="macro">#x27;./gpt2_stock&class="macro">#x27;
lora_tuning_path = &class="macro">#x27;./gpt2_LORA_None&class="macro">#x27;
# Initialize tokenizer and models
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)
# Extract input data and true prices
input_data = df.iloc[:, class="num">1:class="num">20].values[-class="num">1]
true_prices = df.iloc[-class="num">1:, class="num">21:].values.tolist()[class="num">0]
# Prepare prompt
prompt = &class="macro">#x27; &class="macro">#x27;.join(map(str, input_data))
def generater(model):
    global true_prices
    
    # Set the model to evaluation mode
    model.eval()
    
    # Tokenization and text generation class="kw">using the model
    token = tokenizer.encode(prompt, return_tensors=&class="macro">#x27;pt&class="macro">#x27;).to(dvc)
    start_ = time.time()
    generated = tokenizer.decode(
        model.generate(token, do_sample=True, max_length=class="num">200)[class="num">0],
        skip_special_tokens=True
    )
    end_ = time.time()
    
    print(f&class="macro">#x27;Generate time: {end_ - start_} seconds&class="macro">#x27;)
    
    # Process the generated data
    generated_prices = generated.split(&class="macro">#x27;\n&class="macro">#x27;)[class="num">0]
    generated_prices = list(map(class="type">float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
把横向评测交给小布盯盘
不同微调模型在上升、震荡、下跌段的胜率漂移,这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只管决定用哪套权重上 EA。

常见问题

倾向会。r 过低时低秩矩阵容量不足,难以捕捉价格序列中的多尺度依赖,验证集损失可能早平;可先从 8 试到 32 观察拐点。
优先用文内冻结环境重建 venv,缺失项多在 tokenizers 版本;不要混用 conda 与 pip 装 torch。
概率不能。品种波动结构差异大,适配 EURUSD 的低秩矩阵在 XAUUSD 上常失效,需重跑或做域适配。
目前品种页内置的是通用评测视角,自训权重需经 EA 桥接回传;后续版本可能开放自定义模型槽,关注更新日志。
LoRA 因可训参数量少,倾向更抗过拟合,但上限也低;全量在趋势市更吃数据量,震荡里易记噪。外汇贵金属高风险,回测不代表实盘。