将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(二)-LoRA-调优·进阶篇
(2/3)· 全量微调太重?LoRA 如何用 1% 参数改动撬动 GPT-2 在货币对上的策略泛化
接上篇全参数微调的笨重流程,多数人卡在显存不够又舍不得丢精度。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 文件夹没用。
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=32、lora_dropout=0.1 这组配置:alpha 是缩放系数,dropout 0.1 在过拟合风险高的小样本价格序列上偏保守,可能比默认 0.05 更稳。
训练侧用 block_size=60 把序列切成 60 token 的块,num_train_epochs=3 配 per_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 收盘价,看生成的数字分布是否和真实波动量级接近。
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 未清干净。外汇与贵金属信号类文本经此类蒸馏后仍有误判可能,实盘前务必小样本核对。
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 就能本地重跑。
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)]