
Añadimos un LLM personalizado a un robot comercial (Parte 5): Desarrollar y probar la estrategia de negociación con LLMs (III) Ajuste del adaptador
Tabla de contenido
- Tabla de contenido
- Introducción
- Configuración del entorno
- Creación del módulo adaptador
- Reescribiendo la clase GPT2LMHeadModel
- Ajuste del adaptador
- Comparación del rendimiento de diferentes métodos de ajuste fino
- Conclusión
Introducción
En el artículo anterior, presentamos cómo ajustar el modelo preentrenado GPT-2 utilizando el método LoRA y lo comparamos con el modelo totalmente ajustado desde varios aspectos que nos preocupan, entre los que se incluyen, entre otros, la sobrecarga de entrenamiento, la sobrecarga de inferencia y el rendimiento del modelo.
En este artículo, utilizaremos el método de ajuste del adaptador para ajustar el modelo preentrenado GPT-2 y lo compararemos con los métodos de ajuste ya presentados. Por supuesto, no seguiremos introduciendo diversos métodos de ajuste fino de modelos lingüísticos de gran tamaño, ya que constantemente surgen nuevos métodos de ajuste fino. Para reproducir cada método uno por uno, me temo que no tendrás la paciencia para leerlos todos, así que solo presentaré algunos de los métodos de ajuste más básicos (por ejemplo, ya hemos presentado el ajuste LoRA y no dedicaremos mucho espacio a presentar el ajuste QLoRA, un método ampliado a partir de LoRA).
Esto significa que este será el último artículo sobre el ajuste fino de los modelos de lenguaje grandes. Si desea probar otros métodos, puede consultar la lógica del ajuste fino mencionada en esta serie de artículos y aplicarla a otros métodos de ajuste fino para seguir explorando. A partir del próximo artículo, nos centraremos en combinar el modelo entrenado con el desarrollo de EA para desarrollar estrategias de trading y realizar backtesting.
En nuestro ejemplo, utilizamos un enfoque relativamente agresivo, que consiste en introducir 20 puntos de datos para predecir los siguientes 40 puntos de datos. Elegimos esto porque es difícil comparar las diferencias si los valores previstos son demasiado cortos. Esto es más agresivo que en aplicaciones prácticas, donde se podría utilizar una estrategia más conservadora de introducir 20 valores para predecir los siguientes 5 valores. Es importante tener esto en cuenta al aplicar estas técnicas al trading en tiempo real. Una solución más práctica es establecer estos dos valores (longitud de entrada y salida) como hiperparámetros y, a continuación, utilizar un algoritmo genético para realizar pruebas retrospectivas en diferentes pares de divisas y diferentes períodos con el fin de encontrar los parámetros óptimos. No trataremos este tema específicamente en esta serie de artículos, y los lectores pueden intentar hacerlo por sí mismos.
Ahora centrémonos en cómo utilizar el ajuste del adaptador para ajustar con precisión el modelo preentrenado GPT-2.
Configuración del entorno
A continuación se describe el entorno operativo para los ejemplos de código proporcionados en este artículo. Por supuesto, esto no significa que tu entorno de código deba ser igual al mío, pero si tienes problemas al ejecutar el código, puedes consultar la configuración de mi entorno.
Sistema operativo: Ubuntu 22.04.5 LTS (o la versión correspondiente de WSL)
Versión de Python: 3.10.14
Bibliotecas Python necesarias:
- torch-2.4.1
- numpy-1.26.3
- pandas-2.2.3
- transformers-4.45.1
- peft-0.13.0
- matplotlib-3.9.2
Si no estás familiarizado con cómo configurar el entorno de ejecución del código, lo he descrito detalladamente en otros artículos de esta serie:
- Los usuarios de tarjetas gráficas AMD pueden consultar el artículo anterior: Añadimos un LLM personalizado a un robot comercial (Parte 4): Entrena tu propio LLM con GPU
- Los usuarios de tarjetas gráficas NVIDIA pueden consultar el segundo artículo de esta serie: Añadimos un LLM personalizado a un robot comercial (Parte 2): Ejemplo de despliegue del entorno
Este artículo no tratará esta parte en detalle.
Creación del módulo adaptador
En el primer artículo de esta sección, presentamos brevemente el ajuste del adaptador. En general, el ajuste del adaptador es un método de ajuste fino modular que logra un ajuste fino mediante la inserción de módulos adaptadores especializados en diferentes capas del modelo preentrenado. Cada módulo adaptador puede considerarse como una pequeña red neuronal, responsable de capturar la distribución de datos de una tarea específica. Además, el módulo adaptador se puede entrenar independientemente del modelo original, lo que resulta conveniente para la gestión y la optimización.
Al mismo tiempo, se pueden añadir fácilmente adaptadores para múltiples tareas al mismo modelo preentrenado para lograr el aprendizaje multitarea. Especialmente cuando la tarea es compleja y la cantidad de datos es limitada, el modelo ajustado mediante el ajuste del adaptador puede obtener un mayor rendimiento.
Por supuesto, en comparación con LoRA, el módulo adaptador puede introducir más parámetros, lo que aumenta la carga de almacenamiento y cálculo, y es necesario diseñar y ajustar el módulo adaptador correspondiente para cada tarea, por lo que el proceso de diseño es más complicado. El ajuste LoRA se centra más en mejorar la adaptabilidad del modelo con un número mínimo de parámetros, lo que resulta adecuado para escenarios con recursos limitados y que requieren un ajuste eficiente. Por otro lado, el ajuste del adaptador captura información específica de la tarea mediante la introducción de módulos independientes, lo que resulta adecuado para escenarios que requieren un aprendizaje multitarea o un ajuste flexible.
En la actualidad, una vez que se ha determinado el objetivo de una tarea, es fundamental elegir el método adecuado. Si el modelo entrenado no puede obtener buenos resultados, por mucho que ajustes los parámetros, deberías considerar cambiar el modelo o el método de entrenamiento en lugar de negar tus propias ideas.
A continuación, utilizaremos el ajuste del adaptador para ajustar el modelo GPT-2 paso a paso. En primer lugar, crearemos un módulo Adaptador y un módulo GPT2LMHeadModel (es decir, la clase GPT2LMHeadModelWithAdapters) y, a continuación, adaptaremos el módulo Adaptador a la clase GPT2LMHeadModelWithAdapters.
Para integrar el módulo Adaptador en GPT-2, crearemos una versión modificada de la clase GPT2LMHeadModel. Este ejemplo solo proporciona una implementación simplificada. Preste atención a las tecnologías clave de la integración del adaptador. La lógica general de implementación del módulo Adaptador no es complicada. En primer lugar, definimos una clase que hereda de nn.Module, que contiene dos operaciones principales: submuestreo (down_project) y sobremuestreo (up_project). down_project mapea las características de entrada a la capa de cuello de botella, pasa por la función de activación ReLU y añade dropout para evitar el sobreajuste; up_project mapea las características de la capa de cuello de botella de vuelta a la dimensión original y vuelve a utilizar dropout para evitar el sobreajuste.
Ahora implementemos el código. En primer lugar, define la clase Adapter, que hereda de nn.Module de torch: class Adapter(nn.Module):
Define el método de inicialización de la clase, aceptando dos parámetros: in_features y bottleneck_features: def __init__(self, in_features, bottleneck_features=64):
- in_features: Esta es la dimensión de las características de entrada. Para el modelo GPT-2, es la dimensión de su capa de incrustación.
- bottleneck_features: Es la dimensión de la capa de cuello de botella, es decir, la dimensión de la característica después de la capa de proyección lineal. El valor predeterminado es 64.
- Llama al método de inicialización de la clase principal (nn.Module): super(Adapter, self).__init__()
- Defina una capa lineal (nn.Linear) para reducir la dimensión de las características de entrada a la dimensión de la capa de cuello de botella: self.down_project = nn.Linear(in_features, bottleneck_features)
- Defina otra capa lineal para aumentar la dimensión de la característica desde la capa de cuello de botella hasta la dimensión de la característica de entrada: self.up_project = nn.Linear(bottleneck_features, in_features)
- Defina la capa Dropout, que se utiliza para descartar aleatoriamente una parte de las neuronas durante el entrenamiento con el fin de evitar el sobreajuste. La probabilidad de descarte se establece en 0,1: self.dropout = nn.Dropout(0.1)
- Llama al método de inicialización de pesos: self.init_weights()
Defina el método de inicialización de pesos init_weights():
- Inicializa los parámetros de peso de la capa down_project utilizando una distribución normal con una media de 0,0 y una desviación estándar de 0,02: nn.init.normal_(self.down_project.weight, mean=0.0, std=0.02)
- Inicializar los parámetros de sesgo de la capa down_project con una constante de 0: nn.init.constant_(self.down_project.bias, 0)
- Del mismo modo, inicializa los parámetros de peso de la capa up_project utilizando una distribución normal con una media de 0,0 y una desviación estándar de 0,02: nn.init.normal_(self.up_project.weight, mean=0.0, std=0.02)
- Inicializar los parámetros de sesgo de la capa up_project con una constante de 0: nn.init.constant_(self.up_project.bias, 0)
Defina el método de propagación hacia adelante forward(): def forward(self, hidden_states), que acepta un parámetro hidden_states
- Proyecta los estados ocultos de entrada a la dimensión de la capa de cuello de botella a través de la capa lineal down_project: hidden_states = self.down_project(hidden_states)
- Realiza una transformación no lineal en los estados ocultos de la capa de cuello de botella utilizando la función de activación ReLU: estados_ocultos = F.relu(estados_ocultos)
- Aplica Dropout a los estados ocultos transformados de forma no lineal, descartando aleatoriamente una parte de las neuronas: hidden_states = self.dropout(hidden_states)
- Aumenta los estados ocultos desde la dimensión de la capa de cuello de botella hasta la dimensión de las características de entrada a través de la capa lineal up_project: hidden_states = self.up_project(hidden_states)
- Aplica Dropout de nuevo a los estados ocultos sobremuestreados: hidden_states = self.dropout(hidden_states)
- Por último, devuelve los estados ocultos procesados por el módulo Adaptador: devuelve hidden_states.
La clase 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
De esta manera, simplemente hemos creado un módulo adaptador. El siguiente paso es adaptar este módulo a nuestro modelo GPT-2, por lo que debemos reescribir la clase GPT2LMHeadModel.
Reescribiendo la clase GPT2LMHeadModel
Si quieres reescribir la clase GPT2LMHeadModel de forma exhaustiva, será un proyecto enorme. Aquí solo ofrecemos una versión simplificada a modo de ejemplo, e implementamos únicamente las partes clave. Nuestra tarea aquí es adaptar el módulo Adaptador a la red GPT-2 y gestionar diversas condiciones de entrada y requisitos de salida del modelo. Después de la inicialización, también tenemos que reescribir la función de propagación hacia adelante forward(), llamar a la capa transformadora del modelo GPT-2 original para obtener el estado oculto hidden_states y, a continuación, aplicar cada módulo adaptador por turno, añadiendo la salida del módulo adaptador al estado oculto original. Por último, los logits finales se generan a través de la capa lineal del modelo de lenguaje (lm_head) y se calcula la pérdida. Ahora completemos el código.
Definimos nuestra clase reescrita como GPT2LMHeadModelWithAdapters, que hereda de GPT2LMHeadModel: class GPT2LMHeadModelWithAdapters(GPT2LMHeadModel)
Defina el método de inicialización __init__() de la clase GPT2LMHeadModelWithAdapters y llame al método de inicialización de la clase principal en el método de inicialización para añadir adaptadores:
- Defina el método de clase __init__(self, config), que recibe un parámetro de configuración config: def __init__(self, config):
- Llama al método de inicialización de la clase principal: super().__init__(config)
- Inicializar adaptadores, el tipo es nn. ModuleList, que contiene módulos Adapter que son iguales al número de capas del modelo GPT-2, donde config.n_embd es la dimensión de la capa de incrustación y config.n_layer es el número de capas: self.adapters = nn.ModuleList([Adapter(config.n_embd) for _ in range(config.n_layer)])
A continuación, implemente el método de propagación hacia adelante forward() en la clase GPT2LMHeadModelWithAdapters:
- Defina el método de propagación hacia adelante, aceptando los parámetros que necesitamos, que se utilizan para controlar el comportamiento y el formato de entrada del modelo (no introduciremos estos parámetros uno por uno aquí, los lectores interesados pueden intentar optimizar estos parámetros): 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,):
- A continuación, llama a la capa transformadora del modelo para la propagación hacia adelante, obtén la salida del modelo y pásala a la variable 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,)
- Obtener la salida del estado oculto hidden_states de la capa transformadora, que es la entrada que el módulo Adaptador necesita procesar: hidden_states = transformer_outputs[0]
- A continuación, recorra todos los módulos del adaptador utilizando un bucle «for» para preparar el siguiente paso de adaptación: for i, adaptador en enumerar(self.adapters):
- Añade la salida de cada capa del módulo Adapter al estado oculto original y asígnalo a hidden_states como el nuevo estado oculto que se pasa a la siguiente capa: hidden_states = hidden_states + adapter(hidden_states)
- Después de procesar hidden_states, también necesitamos convertir el estado oculto procesado (hidden_states) en la salida logits del modelo de lenguaje a través de la capa lm_head del modelo. Cada logit corresponde a la probabilidad de un vocabulario: lm_logits = self.lm_head(hidden_states)
Tras la conversión, este es el enlace para calcular la pérdida:
- Inicializar la pérdida como vacía: loss = None
- Comprueba si se proporcionan etiquetas: si las etiquetas no son None:
- Elimina el último token de la salida de logits porque necesitamos predecir el siguiente token: shift_logits = lm_logits[..., :-1, :].contiguous()
- Elimina el último token de la etiqueta porque necesitamos predecir el siguiente token: shift_labels = labels[..., 1:].contiguous()
- Defina la función de pérdida como pérdida de entropía cruzada (CrossEntropyLoss), que es una función de pérdida comúnmente utilizada para tareas de clasificación: loss_fct = nn.CrossEntropyLoss()
- Aplana shift_logits y shift_labels (view(-1, ...)) y luego utiliza la función de pérdida de entropía cruzada: loss = loss_fct(shift_logits.view(-1, shift_logits.size(-1)), shift_labels.view(-1))
Cabe destacar aquí que el modelo lingüístico suele entrenarse para predecir la siguiente palabra, en lugar de predecir directamente la palabra actual. Por lo tanto, la salida del modelo lm_logits y las etiquetas labels deben escalonarse una posición en el intervalo de tiempo para calcular con precisión la pérdida. Por ejemplo, si una frase es «Me encanta programar», entonces la entrada del modelo puede ser «Me encanta», y la salida del modelo lm_logits debería ser la distribución de probabilidad correspondiente a «me encanta programar». Para calcular la pérdida, necesitamos alinear la distribución de probabilidad de «programación del amor» con la etiqueta «programación».
- Comprueba la configuración de return_dict. Si se establece en False, calcular y fusionar la salida: if not return_dict:
- Combina la salida de logits con otras salidas de la capa transformadora (excepto la primera salida del estado oculto) en la salida de salida: salida = (lm_logits,) + transformer_outputs[1:]
- Si se proporcionan etiquetas y se calcula la pérdida, la pérdida se devuelve junto con la salida; de lo contrario, solo se devuelve la salida: return ((loss,) + output) si la pérdida no es None, salga
- Si return_dict se establece en True, devuelve directamente la salida causal: devuelve 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, )
De esta manera, hemos adaptado el módulo Adapter a nuestra clase GPT2LMHeadModelWithAdapters. Pero, por favor, tenga en cuenta nuevamente que esto es solo un ejemplo sencillo. En escenarios de aplicación reales, diseñe cuidadosamente los módulos relacionados de acuerdo con los requisitos de la tarea.
Ajuste del adaptador
Hemos creado la clase Adapter y la clase de modelo GPT-2 GPT2LMHeadModelWithAdapters adaptada con el módulo Adapter. A continuación, cargamos el modelo y los datos para comenzar el ajuste fino. Algunos códigos que se han interpretado en el artículo original no se interpretarán aquí en detalle. Consulte los artículos anteriores.
1. Preparación
Importa las bibliotecas necesarias, nada especial que añadir aquí.
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
Si hay una GPU disponible en el sistema (comprobado mediante torch.cuda.is_available()), utilice la GPU; de lo contrario, utilice la CPU. Defina el modelo cargado y el nombre del modelo ajustado.
dvc = 'cuda' if torch.cuda.is_available() else 'cpu' print(dvc) model_name_or_path = 'gpt2' Tuned_model = "gpt2_Adapter-tuning"
2. Cargar datos y tokenizador
Recuerda no olvidar colocar aquí el módulo Adapter que hemos creado y la clase GPT2LMHeadModelWithAdapters reescrita. También puede optar por colocarlos en otros scripts y luego importarlos al script de entrenamiento.
Lea los datos del archivo llm_data.csv y cree un objeto DataFrame, que se utiliza para probar el modelo ajustado.
df = pd.read_csv('llm_data.csv')
Cargar el tokenizador GPT-2 preentrenado.
tokenizer = GPT2Tokenizer.from_pretrained(model_name_or_path)
Cree un objeto de conjunto de datos de entrenamiento, especifique el tokenizador utilizado con el parámetro tokenizer, especifique la ruta del archivo de datos de entrenamiento con el parámetro file_path y especifique el tamaño del bloque como 60 con block_size=60. Tenga en cuenta que este valor no se puede establecer arbitrariamente y debe corresponder a los datos del conjunto de datos.
train_dataset = TextDataset(tokenizer=tokenizer, file_path="train.txt", block_size=60)
Combine varias muestras de datos en un solo lote y procese la tarea de modelado de lenguaje enmascarado (MLM) al mismo tiempo. Utilice el parámetro tokenizer para especificar el tokenizador utilizado y utilice mlm=False para especificar que no se utiliza el modelo de lenguaje enmascarado (MLM), sino el modelo de lenguaje causal (CLM).
data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
3. Cargar el modelo y ajustarlo
En primer lugar, utilice la clase TrainingArguments para instanciar el objeto de parámetros de entrenamiento.
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 el directorio de salida del entrenamiento como gpt2_Adapter-tuning.
- overwrite_output_dir=True: Si se debe sobrescribir si el directorio de salida ya existe.
- num_train_epochs=3: Especifica el número de épocas de entrenamiento como 3.
- per_device_train_batch_size=32: Especifica el tamaño del lote de entrenamiento de cada dispositivo como 32.
- save_strategy='no': Especifica que no se guarden puntos de control.
A continuación, carga e instancia el objeto del modelo GPT-2 preentrenado con el 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 el modelo que se va a entrenar.
- args=training_args: Especifica los parámetros de entrenamiento.
- data_collator=data_collator: Especifica el recopilador de datos.
- train_dataset=train_dataset: Especifica el conjunto de datos de entrenamiento.
Utiliza el método train() del objeto Trainer para iniciar el proceso de entrenamiento: trainer.train()
trainer.train()
Guarda el modelo ajustado después del entrenamiento: trainer.save_model(Tuned_model)
trainer.save_model(Tuned_model)
Después del ajuste fino, el modelo se guardará en la carpeta gpt2_Adapter-tuning, dentro del archivo donde se encuentra el script de entrenamiento.
4. Prueba del modelo ajustado
Después del ajuste fino, debemos cargar el modelo ajustado y realizar una inferencia para comprobar si el modelo ajustado funciona correctamente. Por supuesto, al cargar el modelo ajustado, necesitamos utilizar nuestra clase reescrita GPT2LMHeadModelWithAdapters para cargarlo. Después de cargar el modelo, también tenemos que configurarlo para la aceleración de la GPU y poner el modelo en modo de inferencia.
model = GPT2LMHeadModelWithAdapters.from_pretrained(Tuned_model) model.to(dvc) model.eval()
El siguiente paso es realizar pruebas de inferencia para comprobar si el modelo funciona correctamente. Este proceso es el mismo que el del artículo anterior. Para una interpretación detallada del código, consulte el artículo anterior. Este artículo no lo tratará.
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)
El resultado es el siguiente:
El script completo del código de ajuste fino es 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}")
Los archivos de datos se adjuntan al final del artículo. El archivo de datos original es llm_data.csv, y el archivo de datos preprocesados es train.txt.
Comparación del rendimiento de diferentes métodos de ajuste fino
A continuación, compararemos la eficiencia y el rendimiento de diferentes métodos de ajuste fino. Hasta ahora, solo hemos presentado el ajuste fino de parámetros completos y el ajuste fino LoRA. Añadiendo el ajuste del adaptador en este artículo, hay tres en total. A continuación, solo los compararemos.
1. Comparación de eficiencia
Proceso de entrenamiento de ajuste LoRA:
- train_runtime: 69.5605s
- VRAM: 4.1G
- generate_runtime: 1.242877s
Proceso de entrenamiento con ajuste fino de todos los parámetros:
- train_runtime: 101.7946s
- VRAM: 5.67G
- generate_runtime: 0.876525s
Proceso de entrenamiento de adaptación:
- train_runtime: 104.4355s
- VRAM: 5.52G
- generate_runtime: 0.882792s
Train_runtime(s) | VRAM(GB) | Generate_runtime(s) | |
---|---|---|---|
Ajuste fino de todos los parámetros | 101.7946 | 5.67 | 0.876525 |
LoRA-tuning | 69.5605 | 4.1 | 1.242877 |
Ajuste del adaptador | 104.4355 | 5.52 | 0.882792 |
2. Comparación de precisión
Al igual que en el artículo anterior, seguimos cargando las primeras 20 columnas de precios de cierre en la última fila de los datos originales como entrada, y los datos restantes como resultado para evaluar los modelos obtenidos mediante los dos métodos de entrenamiento. Cabe señalar aquí que, como se mencionó al principio del artículo, para que la comparación de los resultados fuera más significativa, elegimos una longitud de predicción más agresiva.
Los primeros 20 precios de cierre:
- 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]
Los precios de cierre 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]
A continuación, cargamos los modelos por separado (los parámetros del modelo de ajuste fino de parámetros completos se guardan en la carpeta gpt2_stock del directorio actual, el modelo de ajuste fino LoRA se guarda en la carpeta gpt2_LORA_None del directorio actual y el modelo de ajuste Adapter se almacena en gpt2_Adapter-tuning del directorio actual), ejecutamos la inferencia y calculamos su MSE, RMSE y NRMSE según los resultados obtenidos. Estos códigos se han presentado en el artículo anterior, por lo que en este artículo no se describirán en detalle. El script completo del código de prueba es 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'])
Nota:
Hay un problema que hay que tener en cuenta aquí, y es que el orden de magnitud de los indicadores que estamos midiendo no es el mismo, por lo que he utilizado una escala logarítmica: plt.yscale(“log”), lo que permite tratar eficazmente situaciones en las que la magnitud de los datos difiere considerablemente.
El resultado de la inferencia del modelo de ajuste fino 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 la inferencia del modelo de ajuste fino 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 la inferencia de ajuste del adaptador:
- 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
Visualización gráfica para comparación:
Conclusión
En este artículo, hemos analizado cómo utilizar el método de ajuste del adaptador para ajustar el modelo preentrenado GPT-2 y hemos realizado una comparación horizontal de los métodos de ajuste que hemos presentado, lo que nos permite elegir de forma intuitiva un método de entrenamiento y un modelo que se adapten mejor a nuestra estrategia comercial.
Observamos que, si bien el ajuste del adaptador puede requerir tiempos de entrenamiento ligeramente más largos y más VRAM que LoRA, ofrece un enfoque diferente para capturar información específica de la tarea. La elección del mejor método depende de los requisitos específicos del proyecto y de los recursos disponibles. El ajuste fino de todos los parámetros sigue siendo una base sólida, mientras que LoRA ofrece eficiencia y el ajuste del adaptador proporciona modularidad y posibles ventajas para escenarios multitarea.
En los siguientes artículos, ya no continuaremos probando diferentes métodos de ajuste fino. Intentaremos utilizar nuestro modelo perfeccionado para formular estrategias comerciales e integrarlas en EA. Una vez completados estos pasos, realizaremos pruebas retrospectivas y evaluaremos el EA. Si aún estás interesado en ajustar el modelo y deseas obtener mejores resultados, puedes intentar seguir mis ideas y completarlo paso a paso según el código de ejemplo. Créeme, no es un proceso difícil.
¡Nos vemos en el próximo artículo!
Apéndice:
Archivos | Descripción |
---|---|
adapter_tuning.py | Código para el ajuste del adaptador |
test.py | Código para comparar la eficiencia y el rendimiento de diferentes métodos de ajuste fino |
llm_data.csv | Archivo de datos sin procesar |
train.txt | Archivo de datos de entrenamiento |
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/13500
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
¿Por qué es necesario el muestreo ascendente al tamaño de entrada original justo después del muestreo descendente? La explicación de las capas parece idéntica (dropout para evitar el sobreajuste), y si los datos caben bien en el contenedor más pequeño con la misma funcionalidad, el remuestreo hacia atrás parece excesivo y un despilfarro (al menos no se obtiene nueva información de la transformación).
PS. La traducción automática del post de inglés a (al menos) ruso parece ridícula, así que por favor lee el post original.