English Русский 中文 Español Deutsch 日本語
preview
Integre seu próprio LLM ao EA (Parte 5): Desenvolva e Teste Estratégia de Trading com LLMs (III) – Adapter-Tuning

Integre seu próprio LLM ao EA (Parte 5): Desenvolva e Teste Estratégia de Trading com LLMs (III) – Adapter-Tuning

MetaTrader 5Negociação |
19 1
Yuqiang Pan
Yuqiang Pan

Índice


Introdução

No artigo anterior, apresentamos como realizar o fine-tuning do modelo pré-treinado GPT-2 usando o método LoRA e o comparamos com o modelo totalmente ajustado a partir de vários aspectos que nos interessam, incluindo, mas não se limitando a, custo de treinamento, custo de inferência e desempenho do modelo.

Neste artigo, usaremos o método Adapter-tuning para ajustar o modelo pré-treinado GPT-2 e compará-lo com os métodos de fine-tuning já apresentados. Claro, não continuaremos apresentando vários métodos de fine-tuning de grandes modelos de linguagem porque novos métodos de ajuste estão surgindo constantemente. Para reproduzir cada método um por um, temo que você não teria paciência para ler tudo, então apresentarei apenas alguns dos métodos de fine-tuning mais básicos (por exemplo, já introduzimos o LoRA-tuning e não gastaremos muito espaço apresentando o QLoRA-tuning, um método derivado do LoRA).

Isso significa que este será o último artigo sobre fine-tuning de grandes modelos de linguagem. Se quiser experimentar outros métodos, você pode consultar a lógica de fine-tuning mencionada nesta série de artigos e aplicá-la a outros métodos para continuar explorando. A partir do próximo artigo, focaremos em combinar o modelo treinado com o desenvolvimento de EA para desenvolver estratégias de trading e realizar backtests.

Em nosso exemplo, usamos uma abordagem relativamente agressiva, que consiste em fornecer 20 pontos de dados para prever os próximos 40 pontos de dados. Escolhemos isso porque é difícil comparar as diferenças se os valores previstos forem muito curtos. Isso é mais agressivo do que em aplicações práticas, onde você pode usar uma estratégia mais conservadora de inserir 20 valores para prever os próximos 5 valores. É importante ter isso em mente ao aplicar essas técnicas ao trading em tempo real. Uma solução mais prática é definir esses dois valores (comprimento de entrada e saída) como hiperparâmetros e então usar um algoritmo genético para realizar backtests em diferentes pares de moedas e diferentes períodos para encontrar os parâmetros ideais. Não discutiremos isso especificamente nesta série, e os leitores podem tentar fazê-lo por conta própria.

Agora vamos focar em como usar Adapter-tuning para ajustar o modelo pré-treinado GPT-2.


Configuração do Ambiente

A seguir descreve-se o ambiente operacional para os exemplos de código fornecidos neste artigo. Claro, isso não significa que seu ambiente precise ser idêntico ao meu, mas se você encontrar problemas ao executar o código, poderá consultar minha configuração.

Sistema Operacional: Ubuntu 22.04.5 LTS (ou a versão correspondente no WSL)

Versão do Python: 3.10.14

Bibliotecas Python Necessárias:

  • torch-2.4.1
  • numpy-1.26.3
  • pandas-2.2.3
  • transformers-4.45.1
  • peft-0.13.0
  • matplotlib-3.9.2

Se você não está familiarizado com como configurar o ambiente de execução, descrevi isso detalhadamente em outros artigos desta série:

Este artigo não apresentará essa parte em detalhes.


Criando o Módulo Adapter

Apresentamos brevemente o Adapter-tuning no primeiro artigo desta seção. Em geral, Adapter-tuning é um método modular de fine-tuning que insere módulos adaptadores especializados em diferentes camadas do modelo pré-treinado para realizar o ajuste. Cada módulo Adapter pode ser considerado uma pequena rede neural, responsável por capturar a distribuição de dados de uma tarefa específica. Além disso, o módulo Adapter pode ser treinado independentemente do modelo original, o que facilita a gestão e otimização.

Ao mesmo tempo, adaptadores para várias tarefas podem ser facilmente adicionados ao mesmo modelo pré-treinado para alcançar aprendizado multitarefa. Especialmente quando a tarefa é complexa e a quantidade de dados é limitada, o modelo ajustado via Adapter-tuning pode obter desempenho superior.

Claro, em comparação com o LoRA, o módulo Adapter pode introduzir mais parâmetros, aumentando a carga de armazenamento e cálculo, e requer projetar e ajustar o módulo adaptador correspondente para cada tarefa, tornando o processo mais complexo. O LoRA-tuning foca mais em melhorar a adaptabilidade do modelo com um número mínimo de parâmetros, adequado para cenários com recursos limitados e necessidade de ajuste eficiente. Por outro lado, o Adapter-tuning captura informações específicas da tarefa ao introduzir módulos independentes, sendo adequado para cenários que exigem aprendizado multitarefa ou ajustes mais flexíveis.

Atualmente, uma vez determinado o objetivo da tarefa, escolher o método correto é crucial. Se o modelo treinado não obtiver bons resultados, independentemente de ajustes de parâmetros, deve-se considerar mudar o modelo ou o método de treinamento, e não descartar suas próprias ideias.

Em seguida, usaremos Adapter-tuning para ajustar o modelo GPT-2 passo a passo. Primeiro, criaremos um módulo Adapter e um módulo GPT2LMHeadModel (isto é, a classe GPT2LMHeadModelWithAdapters) e depois adaptaremos o módulo Adapter à classe GPT2LMHeadModelWithAdapters.

Para integrar o módulo Adapter ao GPT-2, criaremos uma versão modificada da classe GPT2LMHeadModel. Este exemplo fornece apenas uma implementação simplificada. Atente-se às tecnologias-chave da integração de Adapters. A lógica geral de implementação do módulo Adapter não é complicada. Primeiro, definimos uma classe que herda de nn.Module, contendo duas operações principais: redução de dimensionalidade (down_project) e aumento de dimensionalidade (up_project). down_project projeta os recursos de entrada para a camada bottleneck, passa pela função de ativação ReLU e adiciona dropout para evitar overfitting; up_project projeta os recursos da camada bottleneck de volta para a dimensão original e aplica dropout novamente para evitar overfitting.

Agora vamos implementar o código. Primeiro, defina a classe Adapter, herdando de nn.Module do torch: class Adapter(nn.Module):

Defina o método de inicialização da classe, aceitando dois parâmetros: in_features e bottleneck_features: def init(self, in_features, bottleneck_features=64):

  1. in_features: Esta é a dimensão dos recursos de entrada. Para o modelo GPT-2, é a dimensão de sua camada de embeddings.
  2. bottleneck_features: Esta é a dimensão da camada bottleneck, isto é, a dimensão dos recursos após a projeção linear. O padrão é 64.
  • Chame o método de inicialização da classe pai (nn.Module): super(Adapter, self).init()
  • Defina uma camada linear (nn.Linear) para reduzir a dimensão dos recursos de entrada para a dimensão da camada bottleneck: self.down_project = nn.Linear(in_features, bottleneck_features)
  • Defina outra camada linear para aumentar a dimensão dos recursos da camada bottleneck de volta à dimensão de entrada: self.up_project = nn.Linear(bottleneck_features, in_features)
  • Defina a camada Dropout, usada para descartar aleatoriamente parte dos neurônios durante o treinamento para evitar overfitting. A taxa de descarte é definida em 0.1: self.dropout = nn.Dropout(0.1)
  • Chame o método de inicialização de pesos: self.init_weights()

Defina o método de inicialização de pesos init_weights():

  • Inicialize os pesos da camada down_project com distribuição normal média 0.0 e desvio padrão 0.02: nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
  • Inicialize o viés da camada down_project com constante 0: nn.init.constant_(self.down_project.bias, 0)
  • Da mesma forma, inicialize os pesos da camada up_project com distribuição normal média 0.0 e desvio padrão 0.02: nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02)
  • Inicialize o viés da camada up_project com constante 0: nn.init.constant_(self.up_project.bias, 0)

Defina o método de propagação forward(): def forward(self, hidden_states), que aceita um parâmetro hidden_states

  • Projete os estados ocultos de entrada para a dimensão da camada bottleneck via a camada linear down_project: hidden_states = self.down_project(hidden_states)
  • Realize a transformação não linear dos estados ocultos da camada bottleneck usando a função ReLU: hidden_states = F.relu(hidden_states)
  • Aplique Dropout aos estados ocultos transformados, descartando aleatoriamente parte dos neurônios: hidden_states = self.dropout(hidden_states)
  • Aumente os estados ocultos da dimensão bottleneck de volta à dimensão de entrada via a camada linear up_project: hidden_states = self.up_project(hidden_states)
  • Aplique Dropout novamente aos estados ocultos ampliados: hidden_states = self.dropout(hidden_states)
  • Por fim, retorne os estados ocultos processados pelo módulo Adapter: return hidden_states

A classe Adapter completa:

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

Dessa forma, criamos de maneira simples um módulo Adapter. O próximo passo é adaptar esse módulo ao nosso modelo GPT-2, portanto precisamos reescrever a classe GPT2LMHeadModel.


Reescrevendo a Classe GPT2LMHeadModel

Se você quiser reescrever a classe GPT2LMHeadModel de forma abrangente, será um projeto enorme. Aqui fornecemos apenas uma versão simplificada para fins de exemplo, implementando apenas as partes essenciais. Nossa tarefa é adaptar o módulo Adapter à rede GPT-2 e lidar com várias condições de entrada e requisitos de saída do modelo. Após a inicialização, também precisamos reescrever a função de propagação forward(), chamar a camada transformer do modelo GPT-2 original para obter o estado oculto hidden_states e então aplicar cada módulo adapter em sequência, somando a saída do módulo adapter ao estado oculto original. Por fim, os logits finais são gerados por meio da camada linear do modelo de linguagem (lm_head), e a loss é calculada. Agora vamos concluir o código.

Definimos nossa classe reescrita como GPT2LMHeadModelWithAdapters, herdando de GPT2LMHeadModel: class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel)

Defina o método de inicialização init() da classe GPT2LMHeadModelWithAdapters e chame o método de inicialização da classe pai, adicionando os adapters:

  • Defina o método de classe init(self, config), que recebe o parâmetro de configuração config: def init(self, config):
  • Chame o método de inicialização da classe pai: super().init(config)
  • Inicialize os adapters, do tipo nn.ModuleList, contendo módulos Adapter no mesmo número de camadas do modelo GPT-2, onde config.n_embd é a dimensão da camada de embeddings e config.n_layer é o número de camadas: self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])

Em seguida, implemente o método de propagação forward() na classe GPT2LMHeadModelWithAdapters:

  • Defina o método forward, aceitando os parâmetros necessários para controlar o comportamento e o formato de entrada do modelo (não apresentaremos esses parâmetros um por um aqui; leitores interessados podem otimizá-los): 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,):
  • Em seguida, chame a camada transformer do modelo para a propagação forward e obtenha a saída do modelo, atribuindo-a à variável 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, 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,)

  • Obtenha o estado oculto hidden_states da camada transformer, que será processado pelo módulo Adapter: hidden_states = transformer_outputs[0]
  • Em seguida, percorra todos os módulos Adapter usando um for, preparando-se para o processo de adaptação: for i, adapter in enumerate(self.adapters):
  • Some a saída de cada camada do módulo Adapter ao estado oculto original e atribua o resultado a hidden_states como o novo estado oculto para a próxima camada: hidden_states = hidden_states + adapter(hidden_states)
  • Após o processamento de hidden_states, precisamos convertê-lo nos logits do modelo de linguagem por meio da camada lm_head. Cada logit representa a probabilidade de um vocabulário: lm_logits = self.lm_head(hidden_states)

Após a conversão, este é o link para calcular a loss:

  • Inicialize a loss como vazia: loss = None
  • Verificar se os rótulos foram fornecidos: se labels não for None:
  • Remova o último token dos logits, pois precisamos prever o próximo token: shift_logits = lm_logits[..., :-1, :].contiguous()
  • Remova o primeiro token dos labels, pois precisamos prever o próximo token: shift_labels = labels[..., 1:].contiguous()
  • Defina a função de loss como a loss de entropia cruzada (CrossEntropyLoss), amplamente usada para tarefas de classificação: loss_fct = nn.CrossEntropyLoss()
  • Apare os tensores (view(-1, …)) e aplique a função de entropia cruzada: loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))    

Aqui é importante observar que um modelo de linguagem geralmente é treinado para prever a próxima palavra, e não a palavra atual. Portanto, a saída lm_logits e os labels precisam estar defasados em uma posição para calcular a loss corretamente. Por exemplo, se a frase é "I love programming", a entrada pode ser "I love", e a saída do modelo deve ser a distribuição de probabilidade de "love programming". Para calcular a loss, alinhamos a distribuição de "love programming" com o label "programming".

  • Verifique a configuração de return_dict. Se estiver definida como False, calcule e mescle a saída: if not return_dict:
  • Mescle a saída dos logits com outras saídas da camada transformer (exceto o primeiro hidden_state) no objeto output: output = (lm_logits,) + transformer_outputs[1:]
  • Se labels foram fornecidos e a loss foi calculada, retorne a loss junto com a saída; caso contrário, retorne apenas a saída: return ((loss,) + output) if loss is not None else output
  • Se return_dict for True, retorne diretamente o objeto causal: 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,)
A classe GPT2LMHeadModelWithAdapters completa:

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,
        )

Dessa forma, adaptamos o módulo Adapter à nossa classe GPT2LMHeadModelWithAdapters. Mas observe novamente que este é apenas um exemplo simples. Em cenários reais, projete cuidadosamente os módulos conforme os requisitos da tarefa.


Adapter-tuning

Criamos a classe Adapter e a classe GPT-2 GPT2LMHeadModelWithAdapters adaptada com o módulo Adapter. Em seguida, carregamos o modelo e os dados para iniciar o fine-tuning. Alguns trechos de código já explicados no artigo original não serão detalhados aqui. Consulte os artigos anteriores.

1. Preparação

Importe as bibliotecas necessárias; nada de especial aqui.

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

Se houver uma GPU disponível no sistema (verificado por torch.cuda.is_available()), use-a; caso contrário, use a CPU. Defina o modelo carregado e o nome do modelo ajustado.

dvc = 'cuda' if torch.cuda.is_available() else 'cpu'

print(dvc)

model_name_or_path = 'gpt2'

Tuned_model = "gpt2_Adapter-tuning"

2. Carregar Dados e Tokenizer

Lembre-se de colocar aqui o módulo Adapter criado e a classe GPT2LMHeadModelWithAdapters reescrita. Você também pode colocá-los em outros scripts e importá-los no script de treinamento.

Leia os dados do arquivo llm_data.csv e crie um objeto DataFrame, usado para testar o modelo ajustado.

df = pd.read_csv('llm_data.csv')

Carregue o tokenizer pré-treinado do GPT-2.

tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)

Crie um objeto para o dataset de treinamento, especifique o tokenizer com o parâmetro tokenizer, indique o caminho dos dados de treinamento com file_path e defina block_size=60. Observe que este valor não pode ser definido arbitrariamente e deve corresponder aos dados do dataset.

train_dataset = TextDataset(tokenizer=tokenizer,

                            file_path="train.txt",

                            block_size=60)

Combine múltiplas amostras de dados em um único batch e processe a tarefa de masked language modeling (MLM). Use o parâmetro tokenizer para especificar o tokenizer usado e mlm=False para indicar que não será usado MLM, mas sim CLM (causal language modeling).

data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

3. Carregar o Modelo e Realizar o Fine-tuning

Primeiro, use a classe TrainingArguments para instanciar o objeto de parâmetros de treinamento.

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: Especifica o diretório de saída do treinamento como gpt2_Adapter-tuning.
  • overwrite_output_dir=True: Indica se deve sobrescrever caso o diretório já exista.
  • num_train_epochs=3: Número de épocas de treinamento definido como 3.
  • per_device_train_batch_size=32: Tamanho do batch por dispositivo definido como 32.
  • save_strategy='no': Indica que nenhum checkpoint será salvo.

Em seguida, carregue e instancie o modelo pré-treinado GPT-2 com o módulo Adapter:

    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: Especifica o modelo a ser treinado.
  • args=training_args: Especifica os parâmetros de treinamento.
  • data_collator=data_collator: Especifica o coletor de dados.
  • train_dataset=train_dataset: Especifica o conjunto de dados de treinamento.

Use o método train() do objeto Trainer para iniciar o processo de treinamento: trainer.train()

trainer.train()

Salve o modelo ajustado após o treinamento: trainer.save_model(Tuned_model)

trainer.save_model(Tuned_model)

Após o fine-tuning, o modelo será salvo na pasta gpt2_Adapter-tuning sob o arquivo onde o script de treinamento está localizado.

4. Testar o Modelo Fine-tuned

Após o fine-tuning, precisamos carregar o modelo ajustado e realizar uma inferência para verificar se o modelo fine-tuned está funcionando normalmente. Claro, ao carregar o modelo ajustado, precisamos usar nossa classe reescrita GPT2LMHeadModelWithAdapters para carregá-lo. Após carregar o modelo, também devemos configurá-lo para aceleração por GPU e colocar o modelo em modo de inferência.

    model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model)
    model.to(dvc)
    model.eval()

O próximo passo é o teste de inferência para ver se o modelo está funcionando corretamente. Este processo é o mesmo do artigo anterior. Para uma interpretação detalhada do código, consulte o artigo anterior. Este artigo não abordará isso.

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)

O resultado é o seguinte:

Treinamento

O script completo do código de fine-tuning é 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}")

Os arquivos de dados estão anexados ao final do artigo. O arquivo de dados original é llm_data.csv, e o arquivo de dados pré-processado é train.txt.


Comparação de Desempenho entre Diferentes Métodos de Fine-tuning

A seguir, compararemos a eficiência e o desempenho de diferentes métodos de fine-tuning. Até agora, introduzimos apenas o fine-tuning de parâmetros completos e o fine-tuning LoRA. Adicionando o Adapter-tuning neste artigo, são três no total. A seguir, faremos apenas a comparação entre eles.

1. Comparação de Eficiência

Processo de treinamento do LoRA-tuning:

  • train_runtime: 69.5605s
  • VRAM: 4.1G
  • generate_runtime: 1.242877s

Processo de treinamento do fine-tuning de parâmetros completos:

  • train_runtime: 101.7946s
  • VRAM: 5.67G
  • generate_runtime: 0.876525s

Processo de treinamento do Adapter-tuning:

  • train_runtime: 104.4355s
  • VRAM: 5.52G
  • generate_runtime: 0.882792s


Train_runtime(s)
VRAM(GB)
Generate_runtime(s)

Fine-tuning de parâmetros completos

101.7946  

5.67

 

0.876525

LoRA-tuning

 

69.5605

 

4.1

 

1.242877

Adapter-tuning

 

104.4355

 

5.52

 

0.882792

2. Comparação de Precisão

Como no artigo anterior, ainda carregamos as primeiras 20 colunas de preços de fechamento na última linha dos dados originais como entrada, e os dados restantes como resultado para avaliar os modelos obtidos pelos dois métodos de treinamento. Deve-se observar aqui que, como mencionado no início do artigo, para tornar a comparação dos resultados mais significativa, escolhemos um comprimento de previsão mais agressivo.

Os primeiros 20 preços de fechamento:

  • input data:[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]

Os preços de fechamento restantes:

  • true prices:[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]

Em seguida, carregamos os modelos separadamente (os parâmetros do modelo do fine-tuning de parâmetros completos são salvos na pasta gpt2_stock no diretório atual, o modelo de fine-tuning LoRA é salvo na pasta gpt2_LORA_None no diretório atual, e o modelo de Adapter-tuning é armazenado em gpt2_Adapter-tuning no diretório atual), executamos a inferência e calculamos seus MSE, RMSE e NRMSE de acordo com os resultados obtidos. Esses códigos já foram apresentados no artigo anterior, e este artigo não os descreverá em detalhes. O script completo de teste é 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'])

Notas

Há um ponto a observar aqui: a ordem de grandeza dos indicadores que estamos medindo não é a mesma, então usei uma escala logarítmica: plt.yscale('log'), isso pode lidar de forma eficaz com a situação em que a magnitude dos dados difere muito.

O resultado de inferência do modelo de fine-tuning de parâmetros completos:

  • generated prices:[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.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

Resultados de inferência do modelo de fine-tuning LoRA:

  • generated prices:[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.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

Resultados de inferência do Adapter-tuning:

  • generated prices:[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

Visualização em gráfico para comparação:

pre

cp


Conclusão

Neste artigo, discutimos como usar o método Adapter-tuning para realizar o fine-tuning do modelo pré-treinado GPT-2 e fizemos uma comparação horizontal dos métodos de fine-tuning que apresentamos, o que nos permite escolher, de forma intuitiva, um método de treinamento e um modelo mais adequados para nossa estratégia de trading.

Observamos que, embora o Adapter-tuning possa exigir tempos de treinamento ligeiramente mais longos e mais VRAM do que o LoRA, ele oferece uma abordagem diferente para capturar informações específicas da tarefa. A escolha do melhor método depende dos requisitos específicos do projeto e dos recursos disponíveis. O fine-tuning de parâmetros completos continua sendo uma forte linha de base, enquanto o LoRA oferece eficiência e o Adapter-tuning fornece modularidade e possíveis benefícios em cenários multitarefa.

Nos artigos seguintes, não continuaremos a tentar diferentes métodos de fine-tuning. Tentaremos usar nosso modelo ajustado para formular estratégias de trading e integrá-las ao EA. Depois que isso estiver concluído, faremos backtests e avaliaremos o EA. Se você ainda estiver interessado em fazer o fine-tuning do modelo e quiser obter resultados melhores, pode tentar seguir minhas ideias e concluir o processo passo a passo de acordo com os códigos de exemplo. Acredite, não é um processo difícil.

Até o próximo artigo!

Apêndice:

Arquivo Descrição
adapter_tuning.py Código para Adapter-tuning
test.py Código para comparar a eficiência e o desempenho de diferentes métodos de fine-tuning
llm_data.csv Arquivo de dados brutos
train.txt Arquivo de dados de treinamento


Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/13500

Arquivos anexados |
adapter_tuning.py (5.39 KB)
test.py (3.83 KB)
llm_data.csv (1139.04 KB)
train.txt (1123.41 KB)
Últimos Comentários | Ir para discussão (1)
Stanislav Korotky
Stanislav Korotky | 9 ago. 2025 em 10:50

Por que você precisa aumentar a amostragem para o tamanho original da entrada logo após a redução da amostragem? A explicação das camadas parece idêntica (dropout para evitar o ajuste excessivo) e, se os dados se encaixarem bem no contêiner menor com a mesma funcionalidade, a amostragem ascendente posterior parece excessiva e um desperdício (pelo menos você não obtém novas informações da transformação).

PS. A tradução automática da postagem do inglês para (pelo menos) o russo parece ridícula, portanto, leia a postagem original.

Automatizando Estratégias de Trading em MQL5 (Parte 2): O Sistema Kumo Breakout com Ichimoku e Awesome Oscillator Automatizando Estratégias de Trading em MQL5 (Parte 2): O Sistema Kumo Breakout com Ichimoku e Awesome Oscillator
Neste artigo, criamos um Expert Advisor (EA) que automatiza a estratégia Kumo Breakout utilizando o indicador Ichimoku Kinko Hyo e o Awesome Oscillator. Percorremos o processo de inicialização dos identificadores de indicadores, detecção das condições de breakout e codificação das entradas e saídas automatizadas de trades. Além disso, implementamos trailing stops e lógica de gerenciamento de posição para aprimorar o desempenho e a adaptabilidade do EA às condições de mercado.
Dominando Operações de Arquivos em MQL5: Do I/O Básico à Construção de um Leitor CSV Personalizado Dominando Operações de Arquivos em MQL5: Do I/O Básico à Construção de um Leitor CSV Personalizado
Este artigo aborda técnicas essenciais de manipulação de arquivos em MQL5, abrangendo logs de operações, processamento de CSV e integração de dados externos. Ele oferece tanto compreensão conceitual quanto orientação prática de codificação. Os leitores aprenderão a construir uma classe personalizada de importação CSV passo a passo, adquirindo habilidades práticas para aplicações reais.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Estratégia de trading "Captura de Liquidez" (Liquidity Grab) Estratégia de trading "Captura de Liquidez" (Liquidity Grab)
A estratégia de captura de liquidez é um componente-chave do Smart Money Concepts (SMC), que visa identificar e aproveitar as ações dos participantes institucionais no mercado. Ela envolve mirar áreas de alta liquidez, como zonas de suporte ou resistência, onde ordens de grande volume podem provocar um movimento de preço antes que o mercado retome sua tendência. Este artigo explica em detalhes o conceito de captura de liquidez e descreve o processo de desenvolvimento de um EA para a estratégia de captura de liquidez em MQL5.