将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(三)—— 适配器微调
目录
概述
在上一篇文章中,我们介绍了如何使用 LoRA 方法对 GPT-2 预训练模型进行微调,并从我们关注的几个方面将其与完全微调模型进行了比较,包括但不限于训练开销、推理开销和模型性能。
在本文中,我们将使用适配器微调方法对 GPT-2 预训练模型进行微调,并与已经介绍过的微调方法进行比较。当然,我们不会继续介绍各种微调大型语言模型的方法,因为新的微调方法不断涌现。为了逐一重现每种方法,恐怕您没有耐心阅读所有方法,所以我将只介绍一些最基本的微调方法(例如,我们已经介绍了 LoRA 微调,不会花太多篇幅介绍 QLoRA 微调,这是LoRA的一种扩展方法)。
这意味着这将是最后一篇关于微调大型语言模型的文章。如果你想尝试其他方法,你可以参考本系列文章中提到的微调逻辑,并将其应用于其他微调方法以继续探索。从下一篇文章开始,我们将重点介绍将训练好的模型与 EA 开发相结合,以制定交易策略并进行回溯测试。
在我们的例子中,我们使用了一种相对激进的方法,即输入 20 个数据点来预测接下来的 40 个数据点。我们选择这个是因为如果预测值太短,很难比较差异。这比实际应用中更激进,在实际应用中,您可能会使用更保守的策略,输入 20 个值来预测接下来的 5 个值。在将这些技术应用于实时交易时,记住这一点很重要。一个更实用的解决方案是将这两个值(输入和输出长度)设置为超参数,然后使用遗传算法对不同货币对和不同时期进行回溯测试,以找到最佳参数。我们不会在本系列文章中具体讨论这个问题,读者可以自己尝试。
现在我们来关注如何使用适配器微调来微调 GPT-2 预训练模型。
环境设置
下面描述了本文中提供的代码示例的操作环境。当然,这并不意味着您的代码环境必须与我的相同,但如果您在运行代码时遇到问题,可以参考我的环境配置。
操作系统:Ubuntu 22.04.5 LTS(或对应版本的 WSL)
Python 版本:3.10.14
必要的 Python 库:
- torch-2.4.1
- numpy-1.26.3
- pandas-2.2.3
- transformers-4.45.1
- peft-0.13.0
- matplotlib-3.9.2
如果您不熟悉如何配置代码运行环境,我在本系列的其他文章中对此进行了详细描述:
- AMD 显卡用户可以参考之前的文章: 将您自己的 LLM 集成到 EA 中(第 4 部分):使用 GPU 训练你自己的 LLM
- NVIDIA 显卡用户可以参考本系列第二篇文章: 将您自己的 LLM 集成到 EA 中(第 2 部分):环境部署示例
本文将不详细介绍这一部分。
创建适配器模块
我们在本节的第一篇文章中简单介绍了适配器微调。总的来说,适配器微调是一种模块化的微调方法,通过在预训练模型的不同层中插入专门的适配器模块来实现微调。每个适配器模块可以看作是一个小型的神经网络,负责捕获特定任务的数据分布。而且适配器模块可以独立于原有模型进行训练,方便管理和优化。
同时,可以轻松将针对多种任务的适配器添加到同一个预训练模型中,实现多任务学习。尤其当任务复杂,数据量有限的情况下,使用适配器微调进行微调的模型可以获得更高的性能。
当然,与 LoRA 相比,适配器模块可能会引入更多的参数,增加存储和计算的负担,并且需要针对每个任务设计和调整相应的适配器模块,设计过程更加复杂。LoRA 微调更注重以最少的参数量提升模型的适应性,适合资源有限,需要高效微调的场景。另一方面,适配器调优通过引入独立模块来捕获特定于任务的信息,适用于需要多任务学习或灵活调整的场景。
目前,一旦你确定了任务目标,选择正确的方法至关重要。如果训练后的模型不能获得良好的结果,无论你如何调整参数,你都应该考虑改变模型或训练方法,而不是否定自己的想法。
接下来我们将逐步利用适配器微调对 GPT-2 模型进行微调。首先,我们将创建一个适配器模块和一个 GPT2LMHeadModel 模块(即 GPT2LMHeadModelWithAdapters 类),然后将适配器模块适配到 GPT2LMHeadModelWithAdapters 类。
为了将适配器模块集成到 GPT-2 中,我们将创建 GPT2LMHeadModel 类的修改版本。本示例仅提供一种简化的实现。请关注适配器集成的关键技术。适配器模块整体的实现逻辑并不复杂。首先我们定义一个继承自 nn.Module 的类,该类包含两个主要操作:下采样(down_project)和上采样(up_project)。down_project 将输入特征映射到瓶颈层,经过 ReLU 激活函数,并加入 dropout 防止过拟合;up_project 将瓶颈层的特征映射回原始维度,再次使用 dropout 防止过拟合。
现在我们来实现代码。首先定义 Adapter (适配器)类,继承自 torch 的nn.Module:class Adapter(nn.Module):
定义类的初始化方法,接受两个参数:in_features 和 bottleneck_features:def __init__(self, in_features, Bottleneck_features=64):
- in_features:这是输入特征的维度。对于 GPT-2 模型,它是其嵌入层的维度。
- bottleneck_features:这是瓶颈层的维度,也就是线性投影层之后的特征维度。默认设置为 64。
- 调用父类(nn.Module)的初始化方法:super(Adapter, self).__init__()
- 定义一个线性层(nn.Linear)将输入特征的维度降低到瓶颈层的维度:self.down_project = nn.Linear(in_features, Bottleneck_features)
- 定义另一个线性层,将瓶颈层的特征维度增加回输入特征维度:self.up_project = nn.Linear(bottleneck_features, in_features)
- 定义 Dropout 层,用于在训练时随机丢弃一部分神经元,防止过度拟合。丢弃概率设置为 0.1:self.dropout = nn.Dropout(0.1)
- 调用权重初始化方法:self.init_weights()
定义权重初始化方法 init_weights():
- 使用均值为 0.0,标准差为 0.02 的正态分布初始化 down_project 层的权重参数:nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
- 用常数 0 初始化 down_project 层的偏差参数:nn.init.constant_(self.down_project.bias, 0)
- 类似地,使用均值为 0.0、标准差为 0.02 的正态分布初始化 up_project 层的权重参数:nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02)
- 用常数 0 初始化 up_project 层的偏置参数:nn.init.constant_(self.up_project.bias, 0)
定义前向传播方法 forward():def forward(self, hidden_states),接受一个参数 hidden_states
- 通过 down_project 线性层将输入隐藏状态投影到瓶颈层的维度:hidden_states = self.down_project(hidden_states)
- 使用 ReLU 激活函数对瓶颈层的隐藏状态进行非线性变换:hidden_states = F.relu(hidden_states)
- 将 Dropout 应用于非线性变换的隐藏状态,随机丢弃一部分神经元:hidden_states = self.dropout(hidden_states)
- 通过 up_project 线性层将隐藏状态从瓶颈层的维度增加回输入特征的维度:hidden_states = self.up_project(hidden_states)
- 再次将 Dropout 应用于上采样的隐藏状态:hidden_states = self.dropout(hidden_states)
- 最后,返回适配器模块处理的隐藏状态:return hidden_states
完整的 Adapter 类:
class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=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) 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) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 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) return hidden_states
这样,我们就简单的创建了一个适配器模块。下一步是将这个模块适配到我们的 GPT-2 模型中,所以我们需要重写 GPT2LMHeadModel 类。
重写 GPT2LMHeadModel 类
如果要全面重写 GPT2LMHeadModel 类,那将是一个浩大的工程。我们这里只提供一个简化版本来提供示例,并且只实现关键部分。我们这里的任务是将适配器模块适配到 GPT-2 网络并处理模型的各种输入条件和输出要求。初始化之后,我们还要重写前向传播函数 forward(),调用原始 GPT-2 模型的 Transformer 层来获取隐藏状态 hidden_states,然后依次应用各个适配器模块,将适配器模块的输出添加到原始隐藏状态。最后经过语言模型的线性层(lm_head)生成最终的 logits,并计算 loss。现在让我们完成代码。
我们将重写的类定义为 GPT2LMHeadModelWithAdapters,继承自 GPT2LMHeadModel:class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel)
定义 GPT2LMHeadModelWithAdapters 类的初始化方法 __init__(),并在初始化方法中调用父类的初始化方法添加适配器:
- 定义类方法 __init__(self, config),接收一个配置参数 config:def __init__(self, config):
- 调用父类的初始化方法:super().__init__(config)
- 初始化适配器,类型为 nn.ModuleList,包含与 GPT-2 模型层数相同的 Adapter 模块,其中 config.n_embd 为 embedding 层的维度,config.n_layer 为层数:self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])
接下来,在 GPT2LMHeadModelWithAdapters 类中实现前向传播方法 forward():
- 定义前向传播方法,接受我们需要的参数,这些参数用于控制模型的行为和输入格式(这里就不一一介绍这些参数了,感兴趣的读者可以尝试优化这些参数):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 层进行前向传播,并获取模型的输出传递给变量 transformer_outputs:transformer_outputs = self.transformer(input_ids, past_key_values=past_key_values, attention_mask=attention_mask, token_type_ids=token_type_ids,position_ids=position_ids, head_mask=head_mask, input_embeds=inputs_embeds,coder_hidden_states=encoder_hidden_states,coder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict,)
- 获取 transformer 层的隐藏状态输出 hidden_states,即 Adapter 模块需要处理的输入:hidden_states = transform_outputs[0]
- 接下来使用 for 循环遍历所有 Adapter 模块,为下一步的适配做准备:for i, adapter in enumerate(self.adapters):
- 将 Adapter 模块每一层的输出与原隐藏状态相加,赋值给 hidden_states,作为传递到下一层的新隐藏状态:hidden_states = hidden_states + adapter(hidden_states)
- 在处理完 hidden_states 之后,我们还需要将处理好的隐藏状态(hidden_states)通过模型的 lm_head 层转换成语言模型的 logits 输出。每个 logit 对应一个词汇的概率:lm_logits = self.lm_head(hidden_states)
转换之后就是计算 loss 的环节:
- 将 loss 初始化为空:loss = None
- 检查是否提供了标签: if labels is not None:
- 删除 logits 输出的最后一个标记,因为我们需要预测下一个标记:shift_logits = lm_logits[..., :-1, :].contiguous()
- 删除标签的最后一个标记,因为我们需要预测下一个标记:shift_labels = tags[..., 1:].contiguous()
- 将损失函数定义为交叉熵损失(CrossEntropyLoss),这是分类任务常用的损失函数:loss_fct = nn.CrossEntropyLoss()
- 将 shift_logits 和 shift_labels (view(-1, ...)) 展平,然后使用交叉熵损失函数:loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
这里需要特别注意的是,语言模型通常是在预测下一个单词的时候进行训练的,而不是直接预测当前单词。因此模型输出 lm_logits 和标签 labels 需要在时间步上错开一个位置才能准确计算损失。比如一句话是“我爱编程”,那么模型的输入可能就是“我爱”,模型的输出 lm_logits 应该是“爱编程”对应的概率分布。为了计算损失,我们需要将“爱编程”的概率分布与“编程”的标签对齐。
- 检查 return_dict 的配置。如果设置为 False,则计算并合并输出:if not return_dict:
- 将 logits 输出与 transformer 层的其他输出(第一个隐藏状态输出除外)合并为输出 output:output = (lm_logits,) + transform_outputs[1:]
- 如果提供了标签并计算了损失,则损失将与输出一起返回,否则仅返回输出:return ((loss,) + output) if loss is not None else output
- 如果 return_dict 设置为 True,则直接返回因果输出:return models_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits,past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions,cross_attentions=transformer_outputs.cross_attentions,)
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, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, )
这样,我们就将 Adapter 模块适配到了我们的 GPT2LMHeadModelWithAdapters 类中。但请再次注意,这只是一个简单的例子。在实际应用场景中,请根据任务要求仔细设计相关模块。
适配器微调
我们创建了 Adapter 类以及与 Adapter 模块适配的 GPT-2 模型类 GPT2LMHeadModelWithAdapters。接下来我们加载模型和数据开始微调。本文将不详细解释原文章中解释的一些代码。请参考之前的文章。
1.准备
导入所需的库,这里没有什么特别需要介绍的。
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments, modeling_outputs import torch from torch import nn import torch.nn.functional as F
如果系统中有可用的 GPU(通过 torch.cuda.is_available() 检查),则使用该 GPU,否则使用 CPU。定义加载的模型和微调模型的名称。
dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model = "gpt2_Adapter-tuning"
2.加载数据和标记器
记住不要忘记把我们创建的 Adapter 模块和重写的 GPT2LMHeadModelWithAdapters 类放在这里。您也可以选择将它们放入其他脚本中,然后将其导入训练脚本中。
从 llm_data.csv 文件中读取数据并创建一个 DataFrame 对象,用于测试微调后的模型。
df = pd.read_csv('llm_data.csv') 加载预先训练的 GPT-2 标记器。
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
创建训练数据集对象,使用 tokenizer 参数指定使用的标记器,使用 file_path 参数指定训练数据文件的路径,使用 block_size=60 指定块大小为 60。注意,这个值不能随意设置,必须与数据集中的数据相对应。
train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60)
将多个数据样本合并为一个批次,同时处理掩码语言建模(MLM)任务。使用参数 tokenizer 指定使用的分词器,使用 mlm=False 指定不使用掩码语言建模(MLM),而是使用因果语言建模(CLM)。
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
3.加载模型并微调模型
首先,使用 TrainingArguments 类实例化训练参数对象。
training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy='no', )
- output_dir=Tuned_model:将训练输出目录指定为 gpt2_Adapter-tuning。
- overwrite_output_dir=True:如果输出目录已经存在,是否覆盖。
- num_train_epochs=3:将训练周期数指定为 3。
- per_device_train_batch_size=32:指定每个设备的训练批次大小为 32。
- save_strategy='no':指定不保存检查点。
接下来,使用适配器模块加载并实例化预先训练的 GPT-2 模型对象:
model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,)
- model=model:指定要训练的模型。
- args=training_args:指定训练参数。
- data_collator=data_collator:指定数据收集器。
- train_dataset=train_dataset:指定训练数据集。
使用 Trainer 对象的 train() 方法开始训练过程:trainer.train()
trainer.train()
训练完成后保存微调模型:trainer.save_model(Tuned_model)
trainer.save_model(Tuned_model)
微调完成后,模型会保存在训练脚本所在文件下的 gpt2_Adapter-tuning 文件夹下。
4.测试微调模型
微调后,我们需要加载微调模型并执行推理,以检查微调模型是否可以正常工作。当然,在加载微调模型的时候,需要使用我们重写的类 GPT2LMHeadModelWithAdapters 来加载。加载模型后,我们还要将其设置为 GPU 加速,并将模型转为推理模式。
model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval()
下一步是推理测试,看看模型是否正常工作。此过程与上一篇文章相同。有关详细的代码解释,请参阅上一篇文章,本文不对此进行讨论。
prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) 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(generated)
结果如下:

完整的微调代码脚本是 lora-tuning.py:
import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer from transformers import TextDataset, DataCollatorForLanguageModeling from transformers import Trainer, TrainingArguments,modeling_outputs import torch from torch import nn import torch.nn.functional as F dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model="gpt2_Adapter-tuning" # Define the Adapter module class Adapter(nn.Module): def __init__(self, in_features, bottleneck_features=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) 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) nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02) nn.init.constant_(self.up_project.bias, 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) return hidden_states # Integrate the Adapter into the model 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, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds, encoder_hidden_states=encoder_hidden_states, encoder_attention_mask=encoder_attention_mask, use_cache=use_cache, output_attentions=output_attentions, output_hidden_states=output_hidden_states, return_dict=return_dict, ) hidden_states = transformer_outputs[0] # Apply adapters for i, adapter in enumerate(self.adapters): hidden_states = hidden_states + adapter(hidden_states) lm_logits = self.lm_head(hidden_states) loss = None if labels is not None: # Shift so that tokens < n predict the next token shift_logits = lm_logits[..., :-1, :].contiguous() shift_labels = labels[..., 1:].contiguous() # Flatten the tokens loss_fct = nn.CrossEntropyLoss() loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1)) if not return_dict: output = (lm_logits,) + transformer_outputs[1:] return ((loss,) + output) if loss is not None else output return modeling_outputs.CausalLMOutputWithCrossAttentions( loss=loss, logits=lm_logits, past_key_values=transformer_outputs.past_key_values, hidden_states=transformer_outputs.hidden_states, attentions=transformer_outputs.attentions, cross_attentions=transformer_outputs.cross_attentions, ) if __name__=="__main__": # Load data df = pd.read_csv('llm_data.csv') tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path) train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60) data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False) training_args = TrainingArguments(output_dir=Tuned_model, overwrite_output_dir=True, num_train_epochs=3, per_device_train_batch_size=32, save_strategy= 'no', ) # Initialize model with adapters model = GPT2LMHeadModelWithAdapters.from_pretrained(model_name_or_path) trainer = Trainer(model=model, args=training_args, data_collator=data_collator, train_dataset=train_dataset,) trainer.train() trainer.save_model(Tuned_model) # Load the model for inference model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval() prompt = ' '.join(map(str, df.iloc[:, 1:20].values[-1])) 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}")
数据文件附于文章末尾。原始数据文件为 llm_data.csv,预处理后的数据文件为 train.txt。
不同微调方法的性能比较
接下来,我们将比较不同微调方法的效率和性能。到目前为止,我们仅介绍了全参数微调和 LoRA 微调。加上本文的适配器微调,一共是三个。接下来我们仅对它们进行比较。
1.效率比较
LoRA 微调训练过程:
- train_runtime:69.5605 秒
- VRAM:4.1 G
- generate_runtime:1.242877 秒
全参数微调训练流程:
- train_runtime:101.7946 秒
- VRAM:5.67 G
- generate_runtime:0.876525 秒
适配器微调训练过程:
- train_runtime:104.4355 秒
- VRAM:5.52 G
- generate_runtime:0.882792 秒
| 训练运行时(秒) | 显存(GB) | 生成运行时(秒) | |
|---|---|---|---|
| 全参数微调 | 101.7946 | 5.67 | 0.876525 |
| LoRA 微调 | 69.5605 | 4.1 | 1.242877 |
| 适配器微调 | 104.4355 | 5.52 | 0.882792 |
2.准确度比较
与前一篇文章一样,我们仍然将原始数据最后一行的前 20 列收盘价作为输入,将剩余数据作为结果加载,以评估通过两种训练方法获得的模型。这里应该指出的是,正如本文开头提到的,为了使结果的比较更加显著,我们选择了更激进的预测长度。
前 20 个收盘价:
- 输入数据:[0.61163 0.61162 0.61191 0.61195 0.61209 0.61231 0.61224 0.61207 0.61187 0.61184 0.6119 0.61169 0.61168 0.61162 0.61181 0.61184 0.61184 0.6118 0.61176]
其余收盘价:
- 真实价格:[0.6119, 0.61197, 0.61201, 0.61242, 0.61237, 0.6123, 0.61229, 0.61242, 0.61212, 0.61197, 0.61201, 0.61213, 0.61212, 0.61206, 0.61203, 0.61206, 0.6119, 0.61193, 0.61191, 0.61202, 0.61197, 0.6121, 0.61211, 0.61214, 0.61203, 0.61203, 0.61213, 0.61218, 0.61227, 0.61226]
接下来我们分别加载模型(全参微调的模型参数保存在当前目录下的 gpt2_stock 文件夹下,LoRA 微调的模型保存在当前目录下的 gpt2_LORA_None 文件夹下,适配器微调的模型保存在当前目录下的 gpt2_Adapter-tuning 文件夹下),并进行推理,根据得到的结果计算它们的MSE、RMSE、NRMSE。这些代码在前面的文章中已经介绍过,本文就不再详细描述。完整的测试代码脚本为 test.py:
import time import pandas as pd from transformers import GPT2LMHeadModel, GPT2Tokenizer, GPT2Config from sklearn.metrics import mean_squared_error import torch import numpy as np from peft import PeftModel import matplotlib.pyplot as plt from adapter_tuning import GPT2LMHeadModelWithAdapters df = pd.read_csv('llm_data.csv') dvc='cuda' if torch.cuda.is_available() else 'cpu' base_model='gpt2' fine_tuning_path='./gpt2_stock' lora_tuning_path ='./gpt2_LORA_None' adpter_tuning_path='./gpt2_Adapter-tuning' pre_length=40 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[:,1:20].values[-1] true_prices= df.iloc[-1:,21:].values.tolist()[0] prompt = ' '.join(map(str, input_data)) def generater(model): global true_prices model.eval() token=tokenizer.encode(prompt, return_tensors='pt').to(dvc) start_=time.time() generated = tokenizer.decode(model.generate(token, do_sample=True, max_length=200)[0], skip_special_tokens=True) end_=time.time() print(f'generate time:{end_-start_}') generated_prices=generated.split('\n')[0] generated_prices=list(map(float,generated_prices.split())) generated_prices=generated_prices[0:pre_length] # def trim_lists(a, b): # min_len = min(len(a), len(b)) # return a[:min_len], b[:min_len] # true_prices,generated_prices=trim_lists(true_prices,generated_prices) print(f"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('MSE:', mse) rmse=np.sqrt(mse) nrmse=rmse/(np.max(true_prices)-np.min(generated_prices)) print(f"RMSE:{rmse},NRMSE:{nrmse}") return generated_prices, mse, rmse, nrmse def plot_(a,b,c,title): plt.figure(figsize=(7, 6)) if title=='predication': plt.plot(true_prices[:pre_length], label='True Values', marker='o') plt.plot(a, label='fine_tuning', marker='x') plt.plot(b, label='lora_tuning', marker='s') plt.plot(c,label='adapter_tuning',marker='d') plt.title(title) plt.xlabel('Index') plt.ylabel('Value') plt.legend() plt.savefig(f"{title}.png") def groups_chart(a,b,c,models): metrics = ['Train_time(s)', 'Infer_time(s)', 'Memory(GB)', 'MSE', 'RMSE', 'NRMSE'] plt.figure(figsize=(7, 6)) a=[101.7946,1.243,5.67,a[1],a[2],a[3]] b=[69.5605,0.877,4.10,b[1],b[2],b[3]] c=[104.4355,0.883,5.52,c[1],c[2],c[3]]# 104.4355s,VRAM:5.52G generate_runtime:0.882792s bar_width = 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, color='r', width=bar_width, edgecolor='grey', label=models[0]) plt.bar(r2, b, color='b', width=bar_width, edgecolor='grey', label=models[1]) plt.bar(r3, c, color='g', width=bar_width, edgecolor='grey', label=models[2]) plt.yscale('log') plt.xlabel('Metrics', fontweight='bold') plt.xticks([r + bar_width for r in range(len(metrics))], metrics) plt.ylabel('Values (log scale)', fontweight='bold') plt.title('Model Comparison') plt.legend() # plt.show() plt.savefig('Comparison.png') 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[0],lora_tuning_result[0],adapter_tuning_result[0],title='predication') groups_chart(fine_tuning_result,lora_tuning_result,adapter_tuning_result,models=['fine-tuning','lora-tuning','adapter-tuning'])
请注意:
这里有一个问题需要注意,我们测量的指标的数量级并不相同,所以我这里使用了对数尺度:plt.yscale('log'),这样可以有效处理数据量级相差很大的情况。
全参数微调模型推理结果:
- 生成的价格:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.6118, 0.61176, 0.61165, 0.61169, 0.61186, 0.61171, 0.61171, 0.6116, 0.61165, 0.61168, 0.61165,0.61169,0.61173,0.61184,0.61176,0.61171,0.61176,0.61171,0.61207,0.61208,0.61202,0.6117,0.61207]
- MSE:1.257374999999991e-07
- RMSE:0.00035459483921794336
- NRMSE:0.43243273075362537
LoRA 微调模型推理结果:
- 生成的价格:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.6118, 0.61176, 0.61191, 0.61187, 0.6121, 0.61187, 0.61193, 0.61195, 0.61176, 0.61194, 0.61171,0.61198,0.61171,0.61171,0.61198,0.61172,0.61202,0.6116,0.61173,0.61199,0.61169,0.61171,0.61171]
- MSE:1.0161999999999925e-07
- RMSE:0.0003187789202566557
- NRMSE:0.3887547808008319
适配器微调推理结果:
- 生成的价格:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.61173, 0.61168, 0.61165, 0.61178, 0.61173, 0.61164, 0.61174, 0.61163, 0.61174,0.61163,0.61174,0.61162,0.61162,0.61167,0.61168,0.61165,0.61167,0.61168,0.61162,0.61167,0.61174]
- MSE:1.5644499999999023e-07
- RMSE:0.00039553128826932293
- NRMSE:0.4944141103367081
图表可视化比较:

结论
在本文中,我们讨论了如何使用适配器微调方法对 GPT-2 预训练模型进行微调,并对介绍的微调方法进行了横向比较,使我们能够直观地选择更适合我们交易策略的训练方法和模型。
我们观察到,虽然适配器调整可能需要比 LoRA 稍长的训练时间和更多的 VRAM,但它提供了一种捕获特定于任务的信息的不同方法。选择最佳方法取决于具体的项目要求和可用资源。全参数微调仍然是一个强大的基础,而 LoRA 提供了效率,适配器调整提供了模块化和多任务场景的潜在优势。
在后续的文章中,我们将不再继续尝试不同的微调方法。我们将尝试利用我们经过微调的模型来制定交易策略并将其集成到 EA 中。完成这些之后,我们将对 EA 进行回测和评估。如果你对微调模型还有兴趣,并且希望得到更好的结果,可以尝试按照我的思路,按照示例代码一步一步完成。相信我,这不是一个困难的过程。
下篇文章再见!
附录:
| 文件 | 描述 |
|---|---|
| adapter_tuning.py | 适配器调整代码 |
| test.py | 用于比较不同微调方法的效率和性能的代码 |
| llm_data.csv | 原始数据文件 |
| train.txt | 训练数据文件 |
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/13500
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
价格行为分析工具包开发(第12部分):外部资金流(3)趋势图谱(TrendMap)
从基础到中级:浮点数
MQL5中交易策略的自动化实现(第六部分):掌握智能资金交易中的订单块(Order Block)检测技巧
交易中的神经网络:使用小波变换和多任务注意力的模型
为什么需要在向下采样后立即向上采样到原始输入大小?对各层的解释看起来是一样的(为防止过度拟合而剔除),而且如果数据能很好地容纳在具有相同功能的较小容器中,那么后向的上采样看起来就过于浪费了(至少你不会从转换中获得新的信息)。
PS.自动将帖子从英文翻译成(至少)俄文看起来很荒谬,因此请阅读原帖。