English Deutsch 日本語
preview
Добавляем пользовательскую LLM в торгового робота (Часть 5): Разработка и тестирование торговой стратегии с помощью LLM (II) - Настройка LoRA

Добавляем пользовательскую LLM в торгового робота (Часть 5): Разработка и тестирование торговой стратегии с помощью LLM (II) - Настройка LoRA

MetaTrader 5Трейдинг | 15 мая 2025, 13:59
289 0
Yuqiang Pan
Yuqiang Pan

Содержание



Введение

В предыдущей статье мы рассказали, как выполнить тонкую настройку предварительно обученной модели GPT-2 с использованием наших собственных финансовых данных с помощью метода полной тонкой настройки параметров, а также оценили выходные результаты модели. В этой и следующих статьях мы более подробно рассмотрим, как реализовать другие методы тонкой настройки с примерами кода (мы обсудим только методы тонкой настройки, представленные в предыдущей статье, реализовать все методы невозможно. Я выберу лишь несколько наиболее часто используемых методов для реализации). В данной статье в качестве примера будет рассмотрен метод настройки LoRA.

Кроме того, перед нами стоит задача попытаться сравнить модели, обученные с использованием этих различных методов тонкой настройки, по горизонтали, а затем найти наиболее эффективную модель для текущей валютной пары (конечно, эффективность модели может также меняться в различных рыночных условиях, таких как восходящие тренды, нисходящие тренды или колебательные тренды). Это может более четко подсказать нам, какой метод обучения модели использовать на практике для достижения лучших результатов. Конечно, при более строгом подходе нам следует не только сравнить эти различные методы обработки по горизонтали, но и сравнить производительность тонко настроенных моделей различных валютных пар при различных методах обработки данных и методах тонкой настройки. Это кажется простой, но крайне утомительной задачей. Я лично считаю, что если мы действительно хотим применять эту серию методов в торговле, этот шаг имеет решающее значение. Однако я не собираюсь подробно останавливаться на нем в этой серии статей, поскольку считаю, что каждый может легко расширить функционал на основе наших примеров. Просто замените обучающие данные другими валютными парами, а затем сравните эффективность модели по горизонтали. Работа кропотливая, но не сложная.

В предыдущих статьях я забыл представить необходимую конфигурацию среды и зависимости библиотек в примере кода, из-за чего некоторые пользователи могут столкнуться с ошибками из-за отсутствующих зависимостей при попытке запустить примеры. В будущих статьях я предоставлю подробные объяснения конфигурации среды и зависимостей, используемых в текущем коде, чтобы помочь читателям легко запускать примеры.


Настройка окружения

Ниже представлена рабочая среда для примеров кода, приведенных в этой статье. Конечно, это не означает, что ваша среда кода должна быть такой же, как моя, но если у вас возникнут проблемы при запуске кода, вы можете обратиться к моей конфигурации среды.

  • Операционная система: Ubuntu 22.04.5 LTS (или соответствующая версия WSL)
  • Версия Python: 3.10.14
  • Необходимые библиотеки Python:

  1. torch-2.4.1
  2. numpy-1.26.3
  3. pandas-2.2.3
  4. transformers-4.45.1
  5. petf-0.13.0
  6. matplotlib-3.9.2

Если вы не знакомы с тем, как настроить среду выполнения кода, я подробно описал настройку в других статьях серии:

Здесь я не буду подробно об этом распространяться.


Библиотека peft

Мы уже представили LoRA в предыдущей статье, поэтому в этой статье не будем повторять описание. Чтобы сделать процесс тонкой настройки более простым и понятным, в этой статье не будет воспроизводиться пример оригинального кода LoRA. Вместо этого я использую более простую библиотеку peft.

Эта библиотека Python объединяет различные необходимые нам настройки, включая класс конфигурации параметров настройки LoRA (LoraConfig), метод инициализации модели настройки LoRA (get_peft_model) и класс загрузки точно настроенной модели LoRA (PeftModel).

Далее я познакомлю вас с настройками шаг за шагом, начиная с класса LoraConfig.

1. Класс LoraConfig

Класс LoraConfig принадлежит библиотеке peft и может быть напрямую импортирован из нее. После импорта класса LoraConfig необходимо задать параметры его конфигурации.

Представим конфигурацию параметров в классе LoraConfig:

  • r (`int`):

Измерение внимания Lora ("ранг").

  • target_modules (`Optional[Union[List[str], str]]`):

Названия модулей, к которым следует применить адаптер. Если параметр определен, будут заменены только модули с указанными именами. При передаче строки будет выполнено сопоставление с регулярным выражением. При передаче списка строк либо выполняется точное совпадение, либо проверяется, заканчивается ли имя модуля какой-либо из переданных строк. При значении all-linear, выбираются все линейные/Conv1D-модули, за исключением выходного слоя. В противном случае модули будут выбираться в соответствии с архитектурой модели. Если архитектура неизвестна, возникнет ошибка — в этом случае следует указать целевые модули вручную.

  • lora_alpha (`int`):

Параметр alpha для масштабирования Lora.

  • lora_dropout (`float`):

Вероятность выпадения слоев Lora.

  • fan_in_fan_out (`bool`):

Установите значение True, если заменяемый слой сохраняет вес, например (fan_in, fan_out). Например, gpt-2 использует `Conv1D`, который хранит веса типа (fan_in, fan_out), поэтому для этого параметра следует задать значение True.

  • bias (`str`):

Тип смещения для LoRA. Возможные значения - none, all или lora_only. При all или lora_only соответствующие смещения будут обновлены во время обучения. Имейте в виду, что это означает, что даже при отключении адаптеров модель не будет выдавать те же выходные данные, что и базовая модель без адаптации.

  • use_rslora (`bool`):
 При True используется Rank-Stabilized LoRA, устанавливающий коэффициент масштабирования адаптера на lora_alpha/math.sqrt(r), поскольку было доказано, что он работает лучше. В противном случае будет использоваться исходное значение по умолчанию lora_alpha/r.
  • modules_to_save (`List[str]`):
 Список модулей, за исключением слоев адаптеров, которые следует установить как обучаемые и сохранить в конечной контрольной точке.
  • init_lora_weights (`bool` | `Literal["gaussian", "olora", "pissa", "pissa_niter_[number of iters]", "loftq"]`):
Как инициализировать веса слоев адаптера. Передача значения True (по умолчанию) приводит к инициализации по умолчанию из эталонной реализации от Microsoft. Передача gaussian приводит к гауссовой инициализации, масштабируемой по рангу LoRA для линейных модулей и слоёв. Установка инициализации в значение False приводит к совершенно случайной инициализации и не рекомендуется. loftq передается для использования инициализации LoftQ. olora - для использования инициализации OLoRA. pissa - инициализация адаптации главных сингулярных значений и сингулярных векторов (Principal Singular values and Singular vectors Adaptation - PiSSA), которая сходится быстрее, чем LoRA, и в конечном итоге достигает лучшей производительности. Более того, PiSSA уменьшает ошибку квантования по сравнению с QLoRA, что приводит к дальнейшим улучшениям. Передача
    pissa_niter_[number of iters] запускает инициализацию PiSSA на основе Fast-SVD, где [number of iters] указывает число итераций подпространства для выполнения FSVD и должно быть неотрицательным целым числом. Если [number of iters] установлено равным 16, то инициализация модели 7B может быть завершена за считанные секунды, а эффект обучения приблизительно эквивалентен использованию SVD.
  • layers_to_transform (`Union[List[int], int]`):
Индексы слоев для преобразования. Если передан список целых чисел, адаптер будет применен к индексам слоев, указанным в этом списке. Если передано одно целое число, преобразования будут применены к слою с этим индексом.
  • layers_pattern (`str`):
Имя шаблона слоя используется, только если layers_to_transform отличается от None.
  • rank_pattern (`dict`):
 Сопоставление имен слоев или регулярных выражений с рангами, которые отличаются от ранга по умолчанию, указанного `r`.
  • alpha_pattern (`dict`):
Сопоставление имен слоев или регулярных выражений с альфа-каналами, которые отличаются от альфа-канала по умолчанию, указанного lora_alpha.
  • megatron_config (`Optional[dict]`):
Аргументы TransformerConfig для Megatron. Используется для создания параллельного линейного слоя LoRA. Его можно получить в виде core_transformer_config_from_args(get_args()). Эти две функции из Megatron. Аргументы будут использоваться для инициализации TransformerConfig Megatron. Параметр нужно указать, если вы хотите применить LoRA к слоям ColumnParallelLinear и RowParallelLinear megatron.
  • megatron_core (`Optional[str]`):
Основной модуль Megatron, по умолчанию - megatron.core.
  • loftq_config (`Optional[LoftQConfig]`):
Настройка LoftQ. При None LoftQ будет использоваться для квантования весов основных каналов и инициализации слоев. Также передается init_lora_weights='loftq'. Обратите внимание, что в этом случае не следует передавать квантованную модель, поскольку LoftQ сам квантует ее.
  • use_dora (`bool`):
Включить "адаптацию низкого ранга с разложением по весу" (Weight-Decomposed Low-Rank Adaptation - DoRA). Этот метод разбивает обновления весов на две части: величину и направление. Направление обрабатывается стандартной LoRA, тогда как величина обрабатывается отдельным обучаемым параметром. Это может улучшить производительность LoRA, особенно на низких рангах. В настоящее время DoRA поддерживает только линейные и Conv2D слои. DoRA сопряжена с большими затратами, чем чистая LoRA, поэтому рекомендуется объединять веса для вывода. Больше информации можно найти здесь: https://arxiv.org/abs/2402.09353.
  • layer_replication (`List[Tuple[int, int]]`):
Создайте новый стек слоев, расположив исходные слои модели в соответствии с указанными диапазонами. Это позволяет расширять (или сжимать) модель без дублирования весов базовой модели. К каждому новому слою будут подключены отдельные адаптеры LoRA.
  • runtime_config (`LoraRuntimeConfig`):
Настройки времени выполнения (которые не сохраняются и не восстанавливаются).

Выше приведены все параметры класса LoraConfig. В ходе реального обучения мы обычно не задаем все значения, а задаем только некоторые важные параметры, которые нам нужны, а остальные оставляем по умолчанию. В используемом нами примере мы устанавливаем только следующие параметры: lora_alpha=32, lora_dropout=0.1, а остальные параметры оставляем по умолчанию. Конечно, настройки, приведенные в этой статье, не являются оптимальным выбором. Вы всегда можете выбрать несколько комбинаций параметров, чтобы попробовать разные настройки и найти оптимальную комбинацию параметров.

peft_config = LoraConfig(
                         lora_alpha=32, 
                         lora_dropout=0.1)

2. Функция get_peft_model()

Функцию get_peft_model() также можно напрямую импортировать из библиотеки peft. Нам необходимо использовать ее для загрузки нашей модели GPT-2 в качестве модели, соответствующей указанной конфигурации перед тонкой настройкой. В примере этой статьи мы загрузим GPT-2 как настроенную модель LoRA.

Аналогично, давайте сначала рассмотрим настройку параметров функции:

  • model ([`transformers.PreTrainedModel`]):

Модель для оборачивания.

  • peft_config ([`PeftConfig`]):

Объект конфигурации, содержащий параметры модели Peft.

  • adapter_name (`str`, `optional`, по умолчанию - `default`):

Имя внедряемого адаптера. Если не указано, используется имя адаптера по умолчанию (default).

  • mixed (`bool`, `optional`, по умолчанию - `False`):

Разрешать ли смешивание различных (совместимых) типов адаптеров.

  • autocast_adapter_dtype (`bool`, *optional*):

Следует ли автоматически приводить тип адаптера. По умолчанию - True. На данный момент это приведет только к преобразованию весов адаптера с использованием float16 или bfloat16 в float32, поскольку это обычно требуется для стабильного обучения и влияет только на некоторые тюнеры PEFT.

  • revision (`str`, `optional`, по умолчанию - `main`):

Версия базовой модели. Если параметр не установлен, сохраненная модель peft загрузит основную (main) версию базовой модели.

В этом примере мы используем только параметры model и peft_config, а остальные оставляем по умолчанию. Модель используется для передачи модели GPT-2, а peft_config используется для получения нашей конфигурации LoraConfig.

model = get_peft_model(model, peft_config)

3. Класс PeftModel

Класс PeftModel является базовым классом библиотеки peft. Он может инициализировать любой тип модели, поддерживаемый этой библиотекой. Нам необходимо использовать класс PeftModel для загрузки параметров LoRA, сохраненных во время тонкой настройки, и исходных параметров предварительно обученной модели GPT-2 в одну модель после завершения обучения, а затем использовать загруженную модель для проверки вывода. Аналогично, давайте сначала рассмотрим настройку параметров класса.

  • model ([`~transformers.PreTrainedModel`]) - базовая модель трансформера для Peft.
  • peft_config ([`PeftConfig`]) - настройка модели Peft.
  • adapter_name (`str`,  *optional*) - имя адаптера, по умолчанию - default.
  • autocast_adapter_dtype (`bool`, *optional*):

Следует ли автоматически приводить тип адаптера. По умолчанию - True. На данный момент это приведет только к преобразованию весов адаптера с использованием float16 и bfloat16 в float32, поскольку это обычно требуется для стабильного обучения и влияет только на некоторые тюнеры PEFT.

  • low_cpu_mem_usage (`bool`, `optional`, по умолчанию - `False`):

    Создает пустые веса адаптера на метаустройстве. Полезно для ускорения загрузки.

  • Атрибуты:

    - base_model ([`torch.nn.Module`]) -- базовая модель трансформера для Peft.

    - peft_config ([`PeftConfig`]) -- настройка модели Peft.

    - modules_to_save (`list` of `str`) -- список имен подмодулей, которые необходимо сохранить при сохранении модели.

    - prompt_encoder ([`PromptEncoder`]) -- кодировщик подсказок для Peft при использовании [`PromptLearningConfig`].

    - prompt_tokens (`torch.Tensor`) -- виртуальные токены подсказок для Peft при использовании [`PromptLearningConfig`].

    - transformer_backbone_name (`str`) -- имя основы трансформера в базовой модели при использовании [`PromptLearningConfig`].

    - word_embeddings (`torch.nn.Embedding`) -- вложения слов в основе трансформера в базовой модели при использовании [`PromptLearningConfig`].

При использовании класса PeftModel мы напрямую используем его метод класса PeftModel.from_pretrained(model, peft_model_id) для загрузки модели. Модель — GPT-2, а peft_model_id — это параметры модели LoRA, которые мы настроили.

model = PeftModel.from_pretrained(model, peft_model_id)

Примечание:

Не используйте low_cpu_mem_usage=True при создании нового адаптера PEFT для обучения.


Настройка LoRA

После ознакомления с настройкой LoRA с использованием библиотеки peft давайте завершим наш пример кода.

1. Импорт необходимых библиотек

Здесь нет ничего особенно примечательного. Мы напрямую импортируем нужные нам библиотеки в среду Python:

import pandas as pd
from transformers import GPT2LMHeadModel, GPT2Tokenizer
from transformers import TextDataset, DataCollatorForLanguageModeling
from transformers import Trainer, TrainingArguments
import torch
from peft import get_peft_model, LoraConfig, PeftModel

2. Загрузка данных и настройка модели

Сначала мы проверяем, доступно ли ускорение графического процессора в текущей среде, чтобы убедиться в правильности ее настройки. Если у вас есть доступный графический процессор, но он не используется, вам следует проверить конфигурацию среды. Хотя процессор может выполнить задачу, это займет много времени.

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

Далее определяем параметры настройки LoRA. Эти параметры были введены ранее, поэтому мы будем использовать их напрямую:

model_name_or_path = 'gpt2'
peft_config = LoraConfig(
    lora_alpha=32, 
    lora_dropout=0.1
)

model_name_or_path - это наша предварительно обученная модель. Далее мы определяем путь для сохранения настроенной модели LoRA peft_model_id:

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"

Теперь загрузим llm_data.csv. Мы будем использовать последние 20 цен закрытия этого набора данных в качестве входных данных и сравним выходные данные модели с оставшимися ценами закрытия, чтобы проверить эффективность модели.

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

Далее нам необходимо загрузить предварительно обработанные данные train.txt (мы удалили часть кода, которая преобразует llm_data.csv в train.txt, поскольку мы уже преобразовали данные в предыдущей статье, поэтому нет необходимости преобразовывать их снова). Определим токенизатор, train_dataset и data_collator. Эта часть такая же, как и в нашей предыдущей статье, поэтому мы не будем вдаваться в подробности. Заинтересованные читатели могут обратиться к предыдущей статье.

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)

Нам также необходимо создать экземпляр TrainingArguments. Здесь мы удалили параметры save_steps и save_total_limit. Эти параметры в основном управляют сохранением контрольных точек во время обучения, но для настройки LoRA нам нужно сохранить только параметры LoRA. Чтобы избежать конфликтов, мы удалили эти два параметра и добавили параметр save_strategy='no', используя метод save_model в классе Trainer для сохранения модели.

training_args = TrainingArguments(
    output_dir=peft_model_id,     
    overwrite_output_dir=True,    
    num_train_epochs=3,     
    per_device_train_batch_size=32,
    save_strategy='no'
)

3. Загрузка и настройка модели

Сначала мы загружаем предварительно обученную модель GPT-2 как HeadModel:

model = GPT2LMHeadModel.from_pretrained(model_name_or_path)

Затем нам необходимо объединить настроенные параметры LoRA с предварительно обученной моделью GPT-2. Этот процесс, который был довольно сложным, теперь требует всего одной строки кода с использованием функции get_peft_model() из библиотеки peft. Эта библиотека достаточно сильно упростила нашу работу.

model = get_peft_model(model, peft_config)

Далее мы создаем экземпляр Trainer, выполняем тонкую настройку процесса обучения и сохраняем модель. Эта часть ничем не отличается от кода в предыдущей статье, поэтому мы не будем ее подробно обсуждать. Заинтересованные читатели могут обратиться к предыдущей статье.

trainer = Trainer(
    model=model,
    args=training_args,
    data_collator=data_collator,
    train_dataset=train_dataset
)
trainer.train()
trainer.save_model(peft_model_id)

Следует отметить, что модель, сохраненная с помощью trainer.save_model(peft_model_id), больше не является полной моделью, а содержит только веса LoRA. Во время настройки LoRA предварительно обученные веса GPT-2 замораживаются, и только веса LoRA проходят тонкую настройку. Поэтому при загрузке точно настроенной модели необходимо использовать метод from_pretrained() в классе PeftModel, чтобы перезагрузить эти две части весов вместе для правильной работы модели. Вы больше не можете использовать GPT2LMHeadModel.from_pretrained() для загрузки модели.

обучение

После тонкой настройки модель будет сохранена в папке gpt2_LORA_None в каталоге, где находится обучающий скрипт (поскольку мы не задали параметр task_type в классе LoraConfig, этот параметр по умолчанию имеет значение None, поэтому папка заканчивается на None).

4. Тестирование тонко настроенной модели

После тонкой настройки нам необходимо загрузить настроенную модель и выполнить вывод, чтобы проверить, правильно ли работает настроенная модель. Как упоминалось ранее, модель, настроенная с помощью LoRA, не поддерживает загрузку с помощью GPT2LMHeadModel.from_pretrained() и должна использовать метод from_pretrained()в классе PeftModelдля одновременной загрузки предварительно обученной модели GPT-2 и весов LoRA. Параметры метода PeftModel.from_pretrained() были представлены ранее, поэтому мы не будем их здесь обсуждать. После загрузки модели нам необходимо настроить ее на ускорение графического процессора и переключить модель в режим вывода.

model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
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}")

Результат:

тестирование модели: 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.61174 0.61175 0.61169 

0.6119 0.61174 0.6116 0.61144 0.61155 0.61207 0.61192 0.61203 0.61158 0.61202 0.61158 0.61156 

0.61146 0.61196 0.61144 0.656 0.61142 0.61141 0.61137 0.60952 0.611

Полный код скрипта тонкой настройки - lora-tuning.py.

import pandas as pd
from transformers import GPT2LMHeadModel, GPT2Tokenizer
from transformers import TextDataset, DataCollatorForLanguageModeling
from transformers import Trainer, TrainingArguments
import torch
from peft import get_peft_model, LoraConfig, PeftModel

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

peft_config = LoraConfig(
                        #  task_type=None, 
                        #  inference_mode=False, 
                        #  r=8, 
                         lora_alpha=32, 
                         lora_dropout=0.1,
                         )

peft_model_id = f"{model_name_or_path}_{peft_config.peft_type}_{peft_config.task_type}"
df = pd.read_csv('llm_data.csv')

# sentences = [' '.join(map(str, prices)) for prices in df.iloc[:-10,1:].values]
# with open('train.txt', 'w') as f:
#     for sentence in sentences:
#         f.write(sentence + '\n')

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=peft_model_id,     
                                  overwrite_output_dir=True,    
                                  num_train_epochs=3,     
                                  per_device_train_batch_size=32,
                                  save_strategy= 'no',   
                                #   save_steps=10_000,    
                                #   save_total_limit=2,
                                #   load_best_model_at_end=True,
                                  )

model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = get_peft_model(model, peft_config)

trainer = Trainer(model=model,
                  args=training_args,
                  data_collator=data_collator,
                  train_dataset=train_dataset,)

trainer.train()

# model.save_pretrained(peft_model_id)
trainer.save_model(peft_model_id)

# config = PeftConfig.from_pretrained(peft_model_id)
model = GPT2LMHeadModel.from_pretrained(model_name_or_path)
model = PeftModel.from_pretrained(model, peft_model_id)
model.to(dvc)
model.eval()

prompt = ' '.join(map(str, df.iloc[:,1:20].values[-1])) 
generated = tokenizer.decode(model.generate(tokenizer.encode(prompt, return_tensors='pt').to(dvc), 
                                            do_sample=True, 
                                            max_length=200)[0], 
                                            skip_special_tokens=True)

print(f"test the model:{generated}")

Файлы данных будут прикреплены в конце, при этом исходный файл данных будет иметь имя llm_data.csv, а предварительно обработанный файл данных — train.txt.


Сравнение различных методов тонкой настройки

После экспериментов с различными методами тонкой настройки мы получили новые модели GPT-2 с различными характеристиками. Это требует сравнения результатов и скоростей обучения различных методов, чтобы обоснованно выбрать наиболее подходящий метод для стратегии советника. Поскольку предварительно обученная модель GPT-2 не может распознать наши входные данные, нам не нужно включать предварительно обученную модель в последовательность сравнения. Поэтому для сравнения мы приводим только тонкую настройку по всем параметрам и настройку LoRA. Конечно, в последующих статьях я продолжу знакомить вас с различными методами, чтобы у нас было больше выбора.

1. Сравнение эффективности

Для начала нам нужно сравнить стоимость обучения. Мы отдаем предпочтение методам с высокой эффективностью обучения и низкой стоимостью. Здесь мы сравниваем время обучения, использование памяти и скорость вывода. Хотя различия могут быть незначительными в модели с малыми параметрами, такой как GPT-2, они становятся весьма заметными при выборе более крупных моделей (например, 7B, 13B, 34B или больше).


Время обучения (сек)
Видеопамять (ГБ)
Время генерации (сек)
Настройка LoRA
69.5605
4.1
1.242877
Полная тонкая настройка параметров
101.7946
5.67
0.876525

2. Сравнение точности

С точки зрения точности мы временно сравниваем модели, полученные различными методами точной настройки, используя MSE (среднюю квадратическую ошибку), RMSE (корень из средней квадратической ошибки) и NRMSE (нормализованную среднюю квадратическую ошибку). Другие показатели (такие как сложность, надежность и другие) на данный момент не оцениваются.

Далее мы загружаем цены закрытия последних 20 строк исходных данных в качестве входных данных и используем оставшиеся данные в качестве результата для оценки моделей, полученных двумя методами обучения.

  • Входные данные: [0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176]
  • Истинные цены: [0.6119, 0.61197, 0.61201, 0.61242, 0.61237, 0.6123, 0.61229, 0.61242, 0.61212, 0.61197, 0.61201, 0.61213, 0.61212, 0.61206, 0.61203, 0.61206, 0.6119, 0.61193, 0.61191, 0.61202, 0.61197, 0.6121, 0.61211, 0.61214, 0.61203, 0.61203, 0.61213, 0.61218, 0.61227, 0.61226]

Далее мы загружаем модели (модель тонкой настройки со всеми параметрами сохраняется в папке gpt2_stock в текущем каталоге, а настроенная модель LoRA сохраняется в папке gpt2_LORA_None в текущем каталоге) и запускаем вывод. На основе полученных результатов мы рассчитываем их MSE, RMSE и NRMSE. Эти коды были представлены в предыдущей статье, поэтому здесь они подробно не описываются.

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

# Load dataset
df = pd.read_csv('llm_data.csv')

# Set device (GPU or CPU)
dvc = 'cuda' if torch.cuda.is_available() else 'cpu'

# Define model paths
base_model = 'gpt2'
fine_tuning_path = './gpt2_stock'
lora_tuning_path = './gpt2_LORA_None'

# Initialize tokenizer and models
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)

# Extract input data and true prices
input_data = df.iloc[:, 1:20].values[-1]
true_prices = df.iloc[-1:, 21:].values.tolist()[0]

# Prepare prompt
prompt = ' '.join(map(str, input_data))

Мы инкапсулируем процесс вывода и вычисления MSE, RMSE и NRMSE в функцию generater(model) и используем прогнозируемое значение, MSE, RMSE и NRMSE в качестве возвращаемых значений. Когда мы используем различные модели для оценки выводов, мы просто передаем модель в качестве параметра. Здесь следует отметить, что true_prices, используемая в нашей функции, является глобальной переменной, и нам необходимо изменить ее значение в функции, поэтому мы должны объявить ее как глобальную переменную в функции, в противном случае будет выдано сообщение об ошибке.

def generater(model):
    global true_prices
    
    # Set the model to evaluation mode
    model.eval()
    
    # Tokenization and text generation using the model
    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_} seconds')
    
    # Process the generated data
    generated_prices = generated.split('\n')[0]
    generated_prices = list(map(float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    
    # Function to trim both lists to the same length
    def trim_lists(a, b):
        min_len = min(len(a), len(b))
        return a[:min_len], b[:min_len]
    
    # Trim the true_prices and generated_prices lists
    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}")
    
    # Calculate MSE, RMSE, NRMSE metrics
    mse = mean_squared_error(true_prices, 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 generater(model):
    global true_prices
    
    # Set the model to evaluation mode
    model.eval()
    
    # Tokenization and text generation using the model
    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_} seconds')
    
    # Process the generated data
    generated_prices = generated.split('\n')[0]
    generated_prices = list(map(float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    
    # Function to trim both lists to the same length
    def trim_lists(a, b):
        min_len = min(len(a), len(b))
        return a[:min_len], b[:min_len]
    
    # Trim the true_prices and generated_prices lists
    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}")
    
    # Calculate MSE, RMSE, NRMSE metrics
    mse = mean_squared_error(true_prices, 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

Инкапсулируем визуализацию результата вывода в функцию plot_(a, b, title):

def plot_(a, b, title):
    # Set up the figure size
    plt.figure(figsize=(10, 6))
    
    # Plot true_prices only if the title is 'prediction'
    if title == 'prediction':
        plt.plot(true_prices, label='True Values', marker='o')
    
    # Plot the fine-tuning and lora-tuning values
    plt.plot(a, label='fine_tuning', marker='x')
    plt.plot(b, label='lora_tuning', marker='s')
    
    # Set the title and labels for the axes
    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    
    # Display the legend and save the plot to a file
    plt.legend()
    plt.savefig(f"{title}.png")

Инкапсулируем эффективность модели и оценочные показатели, о которых мы упоминали ранее, в функцию groups_chart(a, b, models):

def groups_chart(a, b, models):
    # Define metrics for the chart
    metrics = ['Train Time(s)', 'Inference Time (s)', 'Memory Usage (GB)', 'MSE', 'RMSE', 'NRMSE']
    
    # Set figure size
    plt.figure(figsize=(10, 6))
    
    # Update values for model a and b
    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]]
    
    # Bar width for each group of bars
    bar_width = 0.2
    
    # Set the positions of the bars
    r1 = np.arange(len(metrics))  # Positions for model a
    r2 = [x + bar_width for x in r1]  # Positions for model b
    
    # Plot bars for both models
    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])
    
    # Set log scale for y-axis
    plt.yscale('log')
    
    # Set labels and title
    plt.xlabel('Metrics', fontweight='bold')
    plt.xticks([r + bar_width / 2 for r in range(len(metrics))], metrics)  # Center the x-axis ticks
    plt.ylabel('Values (log scale)', fontweight='bold')
    plt.title('Model Comparison')
    
    # Display legend and save the plot
    plt.legend()
    # plt.show()  # Uncomment to display the plot
    plt.savefig('Comparison.png')

Примечание:

Проблема здесь в том, что величина измеряемых нами показателей не одинакова, поэтому здесь я использую логарифмическую шкалу :plt.yscale('log'). Таким образом, можно эффективно справляться с ситуациями, когда объем данных существенно различается.

Различные модели выполняют вывод по отдельности:

fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)

Результаты вывода модели тонкой настройки со всеми параметрами:

  • сгенерированные цены:[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.61183, 0.61185, 0.61217, 0.61221, 0.61223, 0.61226, 0.61231, 0.61231, 0.61229, 0.61235, 0.61237, 0.61241, 0.61243, 0.61248, 0.61253, 0.61263, 0.61265, 0.61267, 0.61271, 0.61267, 0.61272]
  • MSE: 1.0064750000000609e-07
  • RMSE:0.0003172499014972362
  • NRMSE:0.3965623768715889

Результаты вывода модели настройки LoRA:

  • сгенерированные цены:[0.61163, 0.61162, 0.61191, 0.61195, 0.61209, 0.61231, 0.61224, 0.61207, 0.61187, 0.61184, 0.6119, 0.61169, 0.61168, 0.61162, 0.61181, 0.61184, 0.61184, 0.6118, 0.61176, 0.6116, 0.6116, 0.61194, 0.6118, 0.61195, 0.61197, 0.61196, 0.6123, 0.61181, 0.61172, 0.6119, 0.61155, 0.61149, 0.61197, 0.61198, 0.61192, 0.61136, 0.61092, 0.61091, 0.61098, 0.61099]
  • MSE: 2.3278249999999242e-07
  • RMSE:0.00048247538797330626
  • NRMSE:0.3195201244856309

Визуализируем результаты и сохраним их в виде изображений:

plot_(fine_tuning_result[0],lora_tuning_result[0],title='predication')
groups_chart(fine_tuning_result,lora_tuning_result,models=['fine-tuning','lora-tuning'])

Визуализация графиков для сравнения:

pre

cmp

Примечание:

Я запускал скрипт много раз для тестирования, и результаты каждого запуска будут разными, поэтому данные и графики, которые я привожу, предназначены только для справки, и это нормально, если ваши результаты запуска будут отличаться от моих.

3. Выбор правильной модели

С точки зрения эффективности очевидно, что настройка LoRA превосходит тонкую настройку со всеми параметрами с точки зрения скорости обучения, скорости вывода и использования памяти. Далее мы сравниваем точность выводов. Из наших графиков интуитивно понятно, что выходные данные двух моделей практически одинаковы для первых 18 прогнозируемых значений, тогда как для остальных значений ошибка постепенно увеличивается. Прогнозы тонко настроенной модели со всеми параметрами в целом относительно стабильны, о чем свидетельствуют значения NRMSE.

Я попытался запустить скрипт test.py несколько раз, чтобы проверить, совпадают ли результаты. Результаты различались: NRMSE точно настроенной модели LoRA иногда была небольшой (около 0.17, что намного ниже полнопараметрической точно настроенной NRMSE), а иногда огромной (до 0.76688). NRMSE тонкой настройки со всеми параметрами оставалась стабильной на уровне около 0,4. Важно отметить, что эти данные не обязательно означают, что тонко настроенная модель с полным набором параметров работает лучше, чем тонко настроенная модель LoRA. Вполне возможно, что настройка LoRA не сошлась с теми же настройками обучения, что и тонкая настройка по всем параметрам. Лучшим решением будет настройка соответствующей логики ранней остановки на основе потерь во время обучения для обеспечения сходимости модели. Эта часть пока не представлена в примере кода, но заинтересованные читатели могут реализовать ее самостоятельно.

Конечно, различные настройки параметров модели также могут влиять на ее производительность. Поэтому более научный подход должен заключаться в том, чтобы сначала найти оптимальные настройки параметров для модели или метода обучения на том же наборе данных и убедиться, что модель сходится при оптимальных настройках. Затем необходимо выполнить горизонтальное сравнение различных методов или моделей обучения, всесторонне оценить различные показатели и выбрать оптимальный метод или модель обучения.

Полный код тестового скрипта представлен в 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

# Load the dataset
df = pd.read_csv('llm_data.csv')

# Define the device (GPU if available)
dvc = 'cuda' if torch.cuda.is_available() else 'cpu'

# Model paths and base settings
base_model = 'gpt2'
fine_tuning_path = './gpt2_stock'
lora_tuning_path = './gpt2_LORA_None'

# Load the tokenizer and models
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)

# Extract the input data and true prices from the dataset
input_data = df.iloc[:, 1:20].values[-1]
true_prices = df.iloc[-1:, 21:].values.tolist()[0]
prompt = ' '.join(map(str, input_data))

# Function to generate predictions
def generater(model):
    global true_prices
    model.eval()
    
    # Tokenization and text generation
    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_}')
    
    # Processing generated prices
    generated_prices = generated.split('\n')[0]
    generated_prices = list(map(float, generated_prices.split()))
    generated_prices = generated_prices[:len(true_prices)]
    
    # Function to trim lists to the same length
    def trim_lists(a, b):
        min_len = min(len(a), len(b))
        return a[:min_len], b[:min_len]
    
    # Trim the true prices and generated prices
    true_prices, generated_prices = trim_lists(true_prices, generated_prices)
    
    # Output metrics
    print(f"Input data: {input_data}")
    print(f"True prices: {true_prices}")
    print(f"Generated prices: {generated_prices}")
    
    mse = mean_squared_error(true_prices, 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

# Function to plot the comparison between true prices and predictions
def plot_(a, b, title):
    plt.figure(figsize=(10, 6))
    
    if title == 'prediction':
        plt.plot(true_prices, label='True Values', marker='o')
    
    plt.plot(a, label='fine_tuning', marker='x')
    plt.plot(b, label='lora_tuning', marker='s')
    
    plt.title(title)
    plt.xlabel('Index')
    plt.ylabel('Value')
    plt.legend()
    plt.savefig(f"{title}.png")

# Function to generate a bar chart comparing different metrics between models
def groups_chart(a, b, models):
    metrics = ['Train Time(s)', 'Inference Time (s)', 'Memory Usage (GB)', 'MSE', 'RMSE', 'NRMSE']
    plt.figure(figsize=(10, 6))
    
    # Data for the metrics
    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]]
    
    bar_width = 0.2
    r1 = np.arange(len(metrics))
    r2 = [x + bar_width for x in r1]
    
    # Plotting bars for both models
    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])
    
    # Set y-axis to log scale for better visibility of differences
    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.savefig('Comparison.png')

# Generate results for both fine-tuned and LORA-tuned models
fine_tuning_result = generater(model_fine_tuning)
lora_tuning_result = generater(model_lora_tuning)

# Plot the prediction comparison
plot_(fine_tuning_result[0], lora_tuning_result[0], title='prediction')

# Generate the comparison chart for the models
groups_chart(fine_tuning_result, lora_tuning_result, models=['fine-tuning', 'lora-tuning'])


Заключение

В этой статье мы обсудили, как выполнить тонкую настройку предварительно обученной модели GPT-2 с использованием метода настройки LoRA, а также сравнили представленные нами методы тонкой настройки. Это позволяет нам интуитивно выбирать метод и модель обучения, которые лучше всего подходят нашей торговой стратегии. Мы продолжим рассматривать другие методы и использовать их для тонкой настройки предварительно обученной модели GPT-2 с целью поиска более точных методов для нашей торговой стратегии. Учитывая масштаб параметров предварительно обученной модели GPT-2, конечный результат может существенно отличаться от идеального, но процесс поиска конечного результата тот же. Вы можете задаться вопросом, почему бы не рассмотреть горизонтальные сравнения различных моделей? Это хороший вопрос, но существует очень много моделей, из которых можно выбирать, и даже одна и та же модель может иметь разные масштабы параметров. Очевидно, что мы не сможем выполнить эту задачу с помощью нескольких простых примеров. Это очень кропотливый, но не сложный процесс, поэтому я предлагаю изучить, как искать наилучшие результаты среди различных моделей, основываясь на примерах методов, приведенных в статье.

Вы готовы продолжить исследования? Увидимся в следующей статье!

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/13499

Прикрепленные файлы |
llm_data.csv (1139.04 KB)
train.txt (1123.41 KB)
lora-tuning.py (2.64 KB)
test.py (3.23 KB)
От новичка до эксперта: Совместная отладка на MQL5 От новичка до эксперта: Совместная отладка на MQL5
Политика «решения проблем» может создать четкую программу для овладения сложными навыками, такими как программирование на MQL5. Такой подход позволяет сконцентрироваться на решении проблем, одновременно развивая свои навыки. Чем больше проблем вы решаете, тем более продвинутый опыт передается в ваш мозг. Лично я считаю, что отладка - это самый эффективный способ освоить программирование. Сегодня мы рассмотрим процесс очистки кода и обсудим лучшие методы преобразования запутанной программы в ясную и функциональную. Прочтите эту статью и откройте для себя ценную информацию.
Возможности Мастера MQL5, которые вам нужно знать (Часть 42): Осциллятор ADX Возможности Мастера MQL5, которые вам нужно знать (Часть 42): Осциллятор ADX
ADX — еще один относительно популярный технический индикатор, используемый некоторыми трейдерами для оценки силы преобладающего тренда. Действуя как комбинация двух других индикаторов, он представляет собой осциллятор, паттерны которого мы исследуем в этой статье с помощью Мастера MQL5 и его вспомогательных классов.
Нейросети в трейдинге: Обобщение временных рядов без привязки к данным (Базовые модули модели) Нейросети в трейдинге: Обобщение временных рядов без привязки к данным (Базовые модули модели)
Продолжаем знакомство с фреймворком Mamba4Cast. И сегодня мы погрузимся в практическую реализацию предложенных подходов. Mamba4Cast создавался не для долгого прогрева на каждом новом временном ряде, а для мгновенного включения в работу. Благодаря идее Zero‑Shot Forecasting модель способна сразу выдавать качественные прогнозы на реальных данных без дообучения и тонкой настройки гиперпараметров.
Нейросети в трейдинге: Обобщение временных рядов без привязки к данным (Mamba4Cast) Нейросети в трейдинге: Обобщение временных рядов без привязки к данным (Mamba4Cast)
В этой статье мы знакомимся с фреймворком Mamba4Cast и подробно рассматриваем один из его ключевых компонентов — позиционное кодирование на основе временных меток. Показано, как формируется временной эмбеддинг с учётом календарной структуры данных.