Integre seu próprio LLM ao EA (Parte 5): Desenvolva e Teste Estratégia de Trading com LLMs (III) – Adapter-Tuning
Índice
- Índice
- Introdução
- Configuração do Ambiente
- Criando o Módulo Adapter
- Reescrevendo a Classe GPT2LMHeadModel
- Adapter-tuning
- Comparação de Desempenho entre Diferentes Métodos de Fine-tuning
- Conclusão
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:
- Usuários de placas AMD podem consultar o artigo anterior: Integre seu próprio LLM ao EA (Parte 4): Treinando seu próprio LLM com GPU
- Usuários de placas NVIDIA podem consultar o segundo artigo da série: Integre seu próprio LLM ao EA (Parte 2): Exemplo de Implantação de Ambiente
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):
- in_features: Esta é a dimensão dos recursos de entrada. Para o modelo GPT-2, é a dimensão de sua camada de embeddings.
- 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,)
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:

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:


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
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Automatizando Estratégias de Trading em MQL5 (Parte 2): O Sistema Kumo Breakout com Ichimoku e Awesome Oscillator
Dominando Operações de Arquivos em MQL5: Do I/O Básico à Construção de um Leitor CSV Personalizado
Está chegando o novo MetaTrader 5 e MQL5
Estratégia de trading "Captura de Liquidez" (Liquidity Grab)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso
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.