English Русский Español Deutsch 日本語 Português
preview
将您自己的 LLM 集成到 EA 中(第 5 部分):使用 LLM 开发和测试交易策略(三)—— 适配器微调

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

MetaTrader 5交易 |
488 1
Yuqiang Pan
Yuqiang Pan

目录


概述

上一篇文章中,我们介绍了如何使用 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

如果您不熟悉如何配置代码运行环境,我在本系列的其他文章中对此进行了详细描述:

本文将不详细介绍这一部分。


创建适配器模块

我们在本节的第一篇文章中简单介绍了适配器微调。总的来说,适配器微调是一种模块化的微调方法,通过在预训练模型的不同层中插入专门的适配器模块来实现微调。每个适配器模块可以看作是一个小型的神经网络,负责捕获特定任务的数据分布。而且适配器模块可以独立于原有模型进行训练,方便管理和优化。

同时,可以轻松将针对多种任务的适配器添加到同一个预训练模型中,实现多任务学习。尤其当任务复杂,数据量有限的情况下,使用适配器微调进行微调的模型可以获得更高的性能。

当然,与 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):

  1. in_features:这是输入特征的维度。对于 GPT-2 模型,它是其嵌入层的维度。
  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,)
完整的 GPT2LMHeadModelWithAdapters 类:

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

图表可视化比较:

pre

cp


结论

在本文中,我们讨论了如何使用适配器微调方法对 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

附加的文件 |
adapter_tuning.py (5.39 KB)
test.py (3.83 KB)
llm_data.csv (1139.04 KB)
train.txt (1123.41 KB)
最近评论 | 前往讨论 (1)
Stanislav Korotky
Stanislav Korotky | 9 8月 2025 在 10:50

为什么需要在向下采样后立即向上采样到原始输入大小?对各层的解释看起来是一样的(为防止过度拟合而剔除),而且如果数据能很好地容纳在具有相同功能的较小容器中,那么后向的上采样看起来就过于浪费了(至少你不会从转换中获得新的信息)。

PS.自动将帖子从英文翻译成(至少)俄文看起来很荒谬,因此请阅读原帖。

价格行为分析工具包开发(第12部分):外部资金流(3)趋势图谱(TrendMap) 价格行为分析工具包开发(第12部分):外部资金流(3)趋势图谱(TrendMap)
市场走势由多头与空头之间的力量博弈所决定。由于作用在这些水平上的力量,市场会尊重某些特定价位水平。斐波那契(Fibonacci)水平和成交量加权平均价(VWAP)水平在影响市场行为方面尤为强大。请随我一同探讨本文中基于VWAP和斐波那契水平生成交易信号的策略。
从基础到中级:浮点数 从基础到中级:浮点数
本文简要介绍浮点数的概念。由于这篇文章非常复杂,请仔细阅读,不要期望很快掌握浮点数系统。随着时间的推移,当你获得使用它的经验时,它才会变得清晰。但本文将帮助您理解为什么您的应用程序有时会产生与预期不同的结果。
MQL5中交易策略的自动化实现(第六部分):掌握智能资金交易中的订单块(Order Block)检测技巧 MQL5中交易策略的自动化实现(第六部分):掌握智能资金交易中的订单块(Order Block)检测技巧
在本文中,我们将运用纯粹的价格行为分析方法,在MQL5平台上实现订单块的自动化检测。我们将界定订单块的定义,实现其检测功能,并集成自动化交易执行系统。最后,我们通过回测来评估该策略的表现。
交易中的神经网络:使用小波变换和多任务注意力的模型 交易中的神经网络:使用小波变换和多任务注意力的模型
我们邀请您探索一个结合小波变换和多任务自注意力模型的框架,旨在提高波动市场条件下预测的响应能力、和准确性。小波变换可将资产回报分解为高频和低频,精心捕捉长期市场趋势、和短期波动。